Version. 0.1
Date. 2026-08-06
Author. Claude Code (inspection session). Operator: Marvin Percival.
Brief. inspection-briefs/loomworks-small-work-sweep-step-0-inspection-brief-v0_1.md, confirmed the only (and so highest) version present.
Charter. standing-notes/dunin7-standing-authorization-charter-v0_1. R-5 inspection run.
Status. Read-only. No fix, no change request, no design landed. Establishes ground for two future change requests — one engine, one surface.
/Users/dunin7/loomworks-engine), branch main, clean tree, HEAD 1706248b626255c09673e9165e8e82030bb7b767 (2026-08-05T20:33:41-04:00)./Users/dunin7/loomworks), branch main, clean tree, HEAD 1245f1c7584db19921f430b429f69a844221b4ed (2026-08-05T12:18:21-04:00).playground_dev / playground_test were not touched — no database access was needed for this inspection; every finding below was reached by reading source and, in two cases (B-53/B-54, B-35), by reading the test tree to confirm what is and is not exercised. No throwaway database was stood up.standing-notes/dunin7-build-list-v0_28.md (highest version present) for each item's one-line description; standing-notes/dunin7-standing-authorization-charter-v0_1.md for the fences.Seven of the thirteen are small — one or two files, no design decision, no new mechanism, completable and verified in one session:
| Item | Small? | Repo | |---|---|---| | B-53 (render sequence number never arrives) | Small | engine only | | B-54 (nothing requires the number to exist) | Small | engine only | | B-35 (missing key reports a crash) | Small | engine only | | B-44 (untitled titles) | Small | engine only | | B-57 (screen argues with itself) | Small | surface only | | B-38 (RenderingRoom's own state handling) | Small | surface only | | B-36 (no way to retire a render) | Small | surface only (engine route is already complete) |
Six are not small and come out of the sweep rather than enlarging it:
| Item | Why not small | Repo |
|---|---|---|
| B-37 (failed shaping production looks like a running one) | Needs a job-creation fix, a new lookup route or response field, and a surface UI branch — three engine files plus a surface change, and a design choice between "new route" and "field on existing response" | spans both |
| B-52 (sources vanish on reload) | Sources are never persisted into conversation history at all — needs a new structured_data variant engine-side plus a new surface mapping branch, not a bug fix to existing plumbing | spans both |
| B-55 ("of its type" without knowing the type) | The wire response for both renders and shapes carries only a type id, never a name — needs a new resolved-name field on two response schemas, a batch-resolution join in two routers, and two surface adapter changes | spans both |
| B-56 (dev sign-in reachable in production) | /auth is hardcoded as the real redirect target in 8+ surface files (session-expiry, sign-out, passkey/recovery-code flows) — hiding it in production means retargeting all of them to /signin, a design decision, not an env-guard | surface, but not small |
| B-47 (credential issuance screen) | The request body is trivial (label + expiry), but the credential is a bearer-style claim link granting unmoderated write access to engagement Memory, shown once — the UI needs a one-time-reveal pattern and a paired revoke affordance, which are product judgment calls, not a button | surface, but not small |
| B-32 (re-derive a stale summary) | Re-deriving with a new proposed organization means calling the metered preview step (LLM, system-key gated) before the free derive step — the missing control needs a spend-consent decision in front of it | surface, but not small |
The natural split, per the brief's own grouping:
Correction to the brief's own framing. Section 6 named B-37 and B-44 as "the candidates" for spanning both repositories. Investigation confirms B-37 does span both — but B-44 does not (it is a one-file engine fix; see below). Two items the brief did not flag as candidates turned out to span both repositories anyway: B-52 and B-55. Three cross-repo items exist, not the two the brief anticipated, and the pairing is different from the one the brief guessed at.
The diagnosis holds exactly as stated. The render list query never selects the column:
src/loomworks/engagement/render_events_view_query.py:107-127 — the SELECT backing list_render_events names 21 columns and does not include display_number.src/loomworks/api/routers/renders.py:205 — _render_row_to_schema reads row.get("display_number"), which is None for every row because the key was never in the row dict, not because the value is null in the database.get_render_event in the same query file, render_events_view_query.py:134-169) also selects only current_version, then rehydrates the full RenderEvent from memory_events for the historical branch — _render_event_to_schema at renders.py:263 reads re.display_number off the rehydrated object, which does carry it. So the bug is specific to the list endpoint, not the single-record read.
Would a test have caught it? No — confirmed. tests/test_list_loading_renders.py asserts on pagination/cursor behavior only, never on display_number in the response body. tests/test_phase_36_render_lineage_helpers.py and tests/test_phase_36_backfill.py test the assignment helper and the migration's backfill directly against the view/table — neither exercises the HTTP list route. No test in the tree asserts display_number survives the list endpoint's SQL→schema round trip.
What the fix touches. One SELECT — add display_number to the column list at render_events_view_query.py:109-122. One file, one line.
Unanticipated finding, same shape, not in the build list. src/loomworks/engagement/shape_events_view_query.py:109-133 — the Shape list query's SELECT also never selects display_number, for the identical reason. src/loomworks/api/routers/shape_events.py:150 reads row.get("display_number") off that same query's rows. This is the same defect as B-53, on the sibling resource, and it is not named anywhere in the build list. Flagging it rather than folding it in — per the charter, absorbing unscoped work into an inspection is exactly the failure this brief warns against.
migrations/versions/0054_phase_36_display_numbers_and_lineage.py:53-64 adds display_number as INTEGER, nullable=True on both views, with only a partial unique constraint (WHERE display_number IS NOT NULL, lines 140-163) — no NOT NULL. The migration's own backfill (lines 95-134) assigns every existing row a number unconditionally via ROW_NUMBER() with no filtering WHERE clause, so as of that migration no row was left null.
But nothing keeps a future row null. src/loomworks/engagement/display_numbers.py's assign_render_display_number / assign_shape_display_number always compute and return a number, and the dispatch call sites (agents/render_dispatch.py:794, engagement/composition_orchestrator.py:1011, agents/shaping.py:682, engagement/shaping_skill.py:369) all thread it through — but the lower-level constructors they call (agents/render_specialist.py:262,569,759) accept display_number: int | None = None as a bare optional kwarg with no assertion that a caller supplied one. A caller that bypasses the assign_* helper (directly invoking the specialist/event constructor) produces a silently numberless row.
Whether tightening it is safe to establish without touching production: yes, in principle — the backfill migration's unconditional ROW_NUMBER() assignment means every row existing at the time 0054 ran got a number, and every row created since goes through assign_render_display_number/assign_shape_display_number, which never return None. Whether that has held in practice on the live playground_dev/production data was not checked — the brief's fence explicitly bars touching those databases, so this is reported unread: a NOT NULL migration's safety against existing rows would need a read-only count query against production, which is Operator territory per the charter (B-12's own two facts are handled the same way).
What the fix touches. A migration adding NOT NULL to both columns (one file), gated on the unread production check above. Small in code; the production-safety check is the one piece that must happen before it, not instead of it.
The buggy site: src/loomworks/api/errors.py:164-166
@app.exception_handler(NoCredentialError)
async def _no_credential(request: Request, exc: NoCredentialError) -> JSONResponse:
return _json(500, code="no_credential", message=str(exc))
NoCredentialError is raised by CredentialStore.resolve_key (src/loomworks/credentials/store.py:139) when no Anthropic key is configured at the system scope. It reaches this handler live via get_llm_client (src/loomworks/api/deps.py:160-171), which calls store.resolve_key(provider="anthropic") with no engagement/operator scope — used as a FastAPI dependency by submit_summarize_route (src/loomworks/api/routers/considerations.py:170-177, llm_client: LLMClient = Depends(get_llm_client)). Dependencies resolve before the handler body runs, so an unconfigured system key on POST /engagements/{id}/considerations/{id}/summarize produces a genuine, request/response HTTP 500 — verified directly by reading both files; not inferred.
The sibling that gets it right, same underlying condition: src/loomworks/api/deps.py:252-266, inside resolve_engagement_llm_client —
if not api_key:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail=(
"No API key is configured for this engagement. "
...
),
)
— the pattern every other "no key reachable" path in the engine follows (deps.py:111-129; api/routers/manifestations.py:153,304; api/routers/api_keys.py:116-118; api/routers/uploads.py:799,1206,1340; and others).
Whether any test exercises the unconfigured path: no. tests/conftest.py:660 and tests/test_auth_dev.py:99 unconditionally override get_llm_client in the test app, so the 500 path is never hit through the router. tests/test_credential_store.py::test_no_credential_error_names_scopes asserts on the exception's message at the library level only, not on any HTTP status.
What the fix touches. One site — either remap the global exception handler at errors.py:165 from 500 to 503, or have get_llm_client itself catch NoCredentialError and raise HTTPException(503, ...) the way resolve_engagement_llm_client does. NoCredentialError is raised from exactly one place in the source tree (store.py:139), so no other caller's behavior depends on the current 500.
Job-state mechanism. shaping_jobs (created by migrations/versions/0018_phase_9_shape_events_view_and_shaping_jobs.py), status IN ('queued','dispatched','completed','failed') with later additions 'declined'/'pending_approval' (src/loomworks/engagement/shaping_jobs.py:143-199). Failure is recorded by mark_failed (shaping_jobs.py:123-140), which sets status='failed' and an error_message.
The link the brief says already exists — confirmed, but not populated at creation. shaping_jobs.shape_event_object_id is a real column (migration 0018), but create_job (shaping_jobs.py:37-72) — verified directly by reading its INSERT — does not write it:
INSERT INTO shaping_jobs (
id, engagement_id, declared_shape_type_ref, shaping_agent_ref,
trigger, triggered_by, self_consumer, status, created_at
) VALUES (...)
No shape_event_object_id in that column list. It's populated only later, on success, inside mark_completed (shaping_jobs.py:93-120). request_shape_production (src/loomworks/engagement/shaping.py:227-256) pre-allocates the shape's object id (line 227) but never threads it into create_job (line 229-237). So for a job that fails before completion, shaping_jobs.shape_event_object_id stays NULL forever — there is no reverse lookup from a Shape's id to its job for the one case (failure) that matters.
Is a way to read it already missing entirely, or does it exist unused? Partially exists. GET /engagements/{eid}/shaping-jobs/{shaping_job_id} (src/loomworks/api/routers/shape_events.py, get_shaping_job_route) already returns status and error_message including 'failed' — job failure is queryable today, by job id. But ShapeEventResponse carries no shaping_job_id field, so a client holding only a Shape has no id to poll with, and the surface never calls this route at all (grep for shaping-jobs/shaping_job_id in the surface repo returns nothing).
What exposing it costs. Three engine changes, not one: (1) create_job must accept and persist the pre-allocated shape id at INSERT time; (2) request_shape_production must pass it; (3) either a new lookup route or a shaping_job_id/status field folded onto ShapeEventResponse. This is a design choice (new route vs. field-on-response), which is exactly the "or something larger" branch the brief asked to watch for.
Surface change confirmed needed. src/app/operator/engagement/[engagement_address]/ShapingRoom.tsx (lines 7-20, 83-86) already documents this gap in its own comments ("the job carrying the failure is unreachable from this surface") and hardcodes a binary not-produced-yet/produced state with no failure branch. Once the engine exposes the field, the surface needs a new field on its wire type plus a UI branch — it will not pick this up for free.
B-37 and B-44 do not share a code path, despite both concerning Shapes and both living partly in shape_events.py: B-37's mechanism is shaping_jobs.py / shaping.py / the job-poll route; B-44's is shape_event_title.py and the title-read helpers. Different tables, different functions.
Both manifestations trace to the same shape of gap, in the same file, and the fix pattern already exists and is proven elsewhere in the codebase:
src/app/operator/engagement/[engagement_address]/WalkPanel.tsx:124 falls back to SURFACE.walkShapeUntitled ("an untitled draft", src/lib/strings.ts:1173) when shape.title is null. That field comes from GET /engagements/{id}/shape-events/{id}?version=N → get_shape_event_route (src/loomworks/api/routers/shape_events.py:472-496), which calls get_shape_event_title (src/loomworks/engagement/shape_event_title.py:100-115) — a stored-title-only lookup.ShapingRoom.tsx:119 falls back to SURFACE.shapeUntitled ("Untitled shape", strings.ts:1221). That field comes from GET /engagements/{id}/shape-events → list_shape_events_route (shape_events.py:398-460), which calls get_shape_event_titles_for_engagement (shape_event_title.py:118-134) — the same stored-title-only lookup.
The fix pattern already exists, proven in production, on a sibling resource. src/loomworks/engagement/shape_event_title.py:137-201 (resolve_shape_event_titles) cascades: stored title → derive_title_from_shape_content (line 195, a markdown-heading/first-sentence derivation defined at lines 22-49 of the same file) → None only as a last resort. This cascade is called from api/routers/renders.py (four call sites: lines 649-676, 915-918, 1257-1266, 1304-1314) to resolve source_shape_title on render rows — confirmed by direct read, including confirming derive_title_from_shape_content is in fact called (line 195 of shape_event_title.py), which corrects an earlier-stage subagent claim that the function was unreferenced dead code. It was added specifically for the render row's title and never wired into the two Shape-side routes that have the identical problem.
Since almost no Shape ever gets an operator-set title (titles are optional at request time), both routes return None for nearly every shape today, which is why "Untitled shape" reads as universal rather than occasional.
What the fix touches. Swap get_shape_event_title → resolve_shape_event_titles in get_shape_event_route (shape_events.py:493) and swap get_shape_event_titles_for_engagement → resolve_shape_event_titles in list_shape_events_route (shape_events.py:441). One file, two call sites, no new mechanism — the cascade function already exists and is already proven correct via the render path.
Not a persistence bug in the ordinary sense — sources are never written to history at all. The live-send path attaches sources straight from the response: src/app/operator/engagement/[engagement_address]/useConversation.ts:399 sets sources: res.sources ?? undefined from ConverseResponse.sources (src/lib/types.ts:204; engine ConverseResponse.sources at src/loomworks/orchestration/schemas.py:993). On reload, history comes through fetchConversationHistory → toHistoryPage → toDisplayTurns (useConversation.ts:100-136), which maps heldItems/matchedFiles/uploadResults/voiceRecordingUploadEventId off t.structured_data — there is no sources branch, and DisplayTurn.sources (line 97) is simply left undefined for every persisted turn.
That's because the engine never writes sources into structured_data at record time: src/loomworks/orchestration/routers/converse.py:1366-1395 builds structured_data only for held/matched/upload-result kinds before calling record_turn — there is no source-kind branch, and the structured_data discriminated union itself (schemas.py:1427-1430+) has no sources variant to hold one even if the write existed.
Not surface-only. The engine needs a new structured_data kind written at record_turn time and returned via conversation_history.py; the surface needs a new toDisplayTurns branch to map it — mirroring the pattern the held/matched/upload kinds already establish, but that is a new persisted variant, not a wiring fix.
The sentence is composed in src/app/operator/engagement/[engagement_address]/ManifestationRoom.tsx's StalenessBlock (lines 61-90): the changed-flag sentence (status.memoryHasChanged) picks SURFACE.composeMemoryMoved / SURFACE.composeMemoryCurrent, and the version-pair sentence is rendered separately via SURFACE.composeVersionPair(derivedFrom, currentVersion). Both strings live in src/lib/strings.ts:1200-1205:
composeMemoryCurrent: "Memory has not changed since this was organized."composeVersionPair: (derivedFrom, current) => "Organized from Memory at version ${derivedFrom}. Memory is now at version ${current}."
The component's own comment block (lines 49-60) already documents that showing both is intentional and correct — the flag tracks content, the counter moves for unrelated reasons. This is pure string composition: no logic change is needed, only a clarifying clause (e.g., naming that the counter also moves for reasons unrelated to content) added to one of the two strings in strings.ts, and optionally a one-line JSX adjustment in ManifestationRoom.tsx if the clause needs to reference both values at once.
What the fix touches. One file (strings.ts), possibly a one-line touch in ManifestationRoom.tsx. Small.
src/lib/strings.ts:1101 (renderSequenceInType) and :1214 (shapeSequenceInType) both produce "#${n} of its type" — a fixed label, no type name — rendered at RenderingRoom.tsx:90 and ShapingRoom.tsx:114. The surface's wire types don't even request a type name: src/lib/api/renders.ts:60-71 (WireRender) carries display_number but no type-reference field at all.
Engine-side, RenderEventResponse.declared_render_type_ref and ShapeEventResponse.declared_shape_type_ref (src/loomworks/api/schemas.py:6119-6123, 5443-5447) are both MemoryRefSchema — id + version only (schemas.py:3728-3746), never a name. A resolution pattern already exists elsewhere (RenderImpact.declared_render_type_name, schemas.py:6971-6976, resolved via the same batch-lookup shape as source_shape_title), but it isn't wired to the routes these two rooms actually call.
Cost, honestly larger than the build list implies. Engine: add a resolved name field to two response schemas and wire a batch name-resolution join into two routers (render list/get, shape list/get) — the pattern exists but touches four engine files, not one. Surface: add the field to two wire interfaces, two adapters, and change two strings. Not surface-only, and not small.
src/app/auth/page.tsx has no NODE_ENV/env guard anywhere (grep for process.env/NODE_ENV across the surface repo finds only NEXT_PUBLIC_API_URL) — it renders in production unconditionally. It does refuse correctly, verified by reading the catch block: a POST to /auth/dev/issue-session 404s (page.tsx:68-71) and is caught and shown as "Dev-auth is not available — the substrate is running in production mode."
But it is load-bearing, not incidental — /auth is hardcoded as the real target in at least 8 places: the global 401-redirect (src/lib/api.ts:92), the auth-flow path list (api.ts:46-49), AuthProvider.tsx, the nav-hide list in AppShell.tsx:24-28, NavBar.tsx:5, uploads.ts:64-67, the sign-out redirect in UserMenu.tsx:57, PasskeysSection.tsx:45, RecoveryCodesSection.tsx:33. A real production sign-in route already exists at /signin.
What "does not appear in production" actually costs. Not a route deletion or an env guard on one file — every one of those 8+ hardcoded redirect targets would need retargeting to /signin, which is a design decision (is /signin the correct universal replacement for every one of those flows, including sign-out and passkey/recovery-code redirects?), not a mechanical change. Not small.
The shared contract is ReadState<T>, defined and derived inside usePagedList (src/hooks/usePagedList.ts:50-55, 214-226), exposed as .state on the hook's return (line 72) — the hook's own comment (lines 69-72) states it exists precisely so consumers adopt it "instead of re-deriving the combination itself." ShapingRoom.tsx:214-260 does exactly that, switching on state.status. MemoryRoom.tsx and ManifestationRoom.tsx hand-roll the same ReadState<T> type for single-object (non-paginated) reads, which is a different, legitimate case (usePagedList doesn't apply to them).
RenderingRoom.tsx (lines 154-167, 178-190) does call usePagedList, but destructures loading/error/items directly instead of .state, and re-derives the same three-way branch (if (loading), if (error), if (items.length === 0)) that .state's discriminated union already computes. Only one call site instantiates RenderingRoom (RoomView.tsx:53-58), passing no props tied to the internal state shape, so the change is contained entirely inside one file.
Behavior check, not just possibility. usePagedList.ts:47-49's own comment states unloaded/loading render identically, and the .state's failed-outranks-empty/populated precedence matches RenderingRoom's existing loading → error → empty check order — this is a drop-in, not a behavior change, verified by reading both the hook's derivation logic and the room's current branches side by side.
What the fix touches. One file (RenderingRoom.tsx): destructure state instead of the three raw fields, replace the three if-blocks with a switch over state.status (four cases), mirroring ShapingRoom.tsx:214-260. Small.
POST /engagements/{engagement_id}/renders/{object_id}/retire already exists (src/loomworks/api/routers/renders.py:1283-1291), calling _retire_render (src/loomworks/engagement/render.py:430-493). The state machine is confirmed irreversible: _retire_render requires prior state 'produced' (lines 460-464), transitions to 'retired' via _advance_render_state (lines 312-319, whose new_state parameter is typed Literal["retired", "invalidated"] — no third value exists to come back through), and the function's own docstring states retired/invalidated are terminal (lines 91-94). No function anywhere in render.py transitions a render out of retired — confirmed by grep, zero hits for any un-retire/reactivate path.
The request body is a single required field, rationale: str (RenderRetireRequest, src/loomworks/api/schemas.py:6259-6268). The surface has no caller of this route at all (grep -rn "retire" across the surface repo finds only the RenderState type-literal declaration, no fetch/POST).
Because it's irreversible, the control needs a confirmation step, not just a button — a rationale field and a confirm dialog, not a bare action. The endpoint's shape is otherwise sufficient as-is; no schema change needed.
What the fix touches. One button, one confirmation dialog with a rationale input, wired to an already-complete engine endpoint. Small.
POST /engagements/{engagement_id}/contribution-credentials (src/loomworks/api/routers/contribution_credentials.py:146-157), Operator-only. Request body (IssueContributionCredentialRequest, lines 114-116) has exactly two fields: recipient_label: str | None and expires_at: datetime. The response returns claim_token/claim_url once and never again (lines 132-134, 152-154) — confirmed by reading the route's own description string, which states this explicitly.
The credential is not a convenience link. external_contribution_route (lines 247-278) lets the holder write held assertions into engagement Memory with no membership created or checked at all — confirmed by the route's own docstring ("no membership is ever consulted or created") and by reading the handler body, which calls add_assertion directly off the credential's snapshotted actor ref. revoke_contribution_credential_route already exists engine-side (lines 203-235). The surface has no screen at all — grep for "contribution-credential" across the surface repo returns nothing, confirming the brief's premise.
Why "small" doesn't fit. Not because the form is complex — it has two fields — but because minting the credential hands out a bearer-style claim link granting real, unmoderated write access, displayed exactly once. That argues for a one-time-reveal-then-copy pattern and a listing/revoke affordance shown alongside issuance, so an Operator isn't handed a link with no way to see or undo it later. Those are product-judgment calls about how much friction and visibility a real-consequence action needs, not a wiring exercise.
derive_manifestation / derive_manifestation_route (src/loomworks/engagement/manifestation.py:483-538; src/loomworks/api/routers/manifestations.py:411-438) store the Operator-confirmed organized_groups verbatim with no LLM call — confirmed by reading the route body, which threads the request's organized_groups straight through with no re-validation (the docstring calls this out explicitly, "D2 option a"). preview_manifestation / preview_manifestation_route (manifestation.py:398-480; manifestations.py:336-393) does call the LLM to propose an organization when committed assertions exist, and that call is metered: get_manifestation_spend_context builds a system-key RoomSpendContext (manifestations.py:282-318), and a refused spend surfaces as HTTP 402 with exits (lines 357-372).
The surface already states the design intent in a comment: ManifestationRoom.tsx:21-24 — "The room NEVER re-derives on its own... Re-deriving is an artifact the Operator has authority over; a surface that quietly regenerates it is a category error." It shows the staleness block but has no button wired to either preview or derive.
What "rebuild" actually means, and why that matters. The missing control is "get a new proposed organization" — i.e., call the metered preview step, then the free derive step. Since the cost sits at the first step, the missing UI needs a spend-consent surface in front of it (the 402/exits shape already exists engine-side to build on). That is a product decision about how and when to show the cost, not a wire-an-existing-free-endpoint fix — which is exactly the distinction the brief asked this inspection to draw out.
derive_title_from_shape_content (shape_event_title.py:22-49) is dead code, never called outside its own file, was checked directly and found wrong — resolve_shape_event_titles calls it at shape_event_title.py:195, and that function is itself called from four sites in api/routers/renders.py. The corrected finding (the cascade exists, is proven via renders, and simply was never wired into the two Shape-side routes) is what appears in §4 above.display_number to NOT NULL would violate any existing row on playground_dev or production — was not run. The charter's R-4/A-7 fences bar touching those databases beyond read-only metadata during an inspection session; this is a one-query check for the Operator's cadence session or a scoped follow-on, not a gap in this inspection's diligence.
A first pass of this document sourced B-57's engine-side field names from the surface's compose.ts:229-231 mapping alone (w.memory_has_changed, w.manifestation_engagement_version, w.current_engagement_version) without re-deriving them engine-side. Checking directly: memory_has_changed is not produced by manifestation.py's preview_manifestation (which instead returns a changes_since_last_manifestation dict of deltas — manifestation.py:434-453 — with no flat boolean in it). The memory_has_changed boolean lives in a separate module, src/loomworks/engagement/memory_status.py (field defined at src/loomworks/api/schemas.py:6924; computed at memory_status.py:122,135-141,193,216 — driven specifically by Memory-content changes, per the comment at line 135, so a version counter that moves for other reasons can disagree with it, exactly as the finding describes). This is a different engine module from the one implied by the room's name, but it does not change B-57's sizing: the fix is still a surface-only string composition in strings.ts, no logic change on either side.
DUNIN7 — Done In Seven LLC — Miami, Florida Loomworks — small-work sweep Step 0 inspection findings — v0.1 — 2026-08-06