DUNIN7 · LOOMWORKS · RECORD
record.dunin7.com
Status Current
Path inspection-briefs/loomworks-agent-commissioning-stage-1-readiness-v0_1.md

Loomworks — Agent-Commissioning Stage 1 Readiness Inspection — v0.1

Version. 0.1 Date. 2026-08-16 Status. Read-only readiness inspection for Stage 1 of the agent-commissioning chain note's staged test plan. No branch, no code changes, no engine commits; no engagements created, no files uploaded. Commissioned by. Operator direction 2026-08-16, per investigations/loomworks-agent-commissioning-chain-note-v0_1 §5 Stage 1. Inspected. loomworks-engine main at 88723c3 (exactly the expected SHA; working tree clean). Canonical home. loomworks-record.


Plain-language summary

Stage 1 is runnable today, with one architectural correction to the chain note's assumption and one operational precondition. The correction: the chain note asked whether /seed/extract inducts "into the path-parameter engagement or creates a new engagement" — the answer is neither. Extraction mints a Seed homed on the ADMINISTRATIVE engagement, points the candidate row's candidate_seed_id at it, and runs induction entirely on the ADMIN log, keyed by seed id; the target engagement is created-or-promoted only later, at commit. This is exactly the candidate → induct → commit shape the product already uses, so the current mechanism supports the chain directly — Stage 1's "upload; extract; induct" lands a candidate whose commit is the existing, gated promote-in-place path. The operational precondition: an Anthropic key must be reachable through the DB-resident cascade (engagement key, person key, or loomworks_llm_key system config) — nothing on this path reads an environment variable, and without a key the request 503s at dependency time. Suite state, route shapes, and the render-registration inventory follow; the render-specialist half of Stage 2 has a named API gap (§6).

1. Suite state on main

Full suite run on main at 88723c3 via the project venv: 3757 passed, 68 skipped, 0 failed (exit 0, 488s). ZERO failures — the baseline holds with no exception in play.

The B-33 discipline is applied to this report explicitly: B-33 itself is CLOSED (CR-2026-166 — a clean report must be readable as clean, with no standing exception held in mind), and the engine baseline is ZERO failures with no known-failures allowance. Any failure count above zero in the line above is therefore a finding, not an expected residue, and is named individually — nothing hides behind a remembered exception.

2. POST /engagements/{engagement_id}/seed/extract — live, and its contract

Live. Handler at api/routers/seed_extraction.py:86-105; router mounted bare (no prefix) at api/app.py:774.

Request body (ExtractSeedFromDiscoveryRequest, api/schemas.py:523-539): exactly one field — file_id: UUID, required. Response carries extracted_seed_payload, seed_id, seed_version, induction_outcome, discovery_to_seed_event_id, gap_fields.

Auth/designation: session cookie only (get_current_principal — passkey/TOTP session; no bearer path), plus assert_engagement_creator (api/deps.py:517-580): the caller must equal engagements.created_by_person_id. No designation, membership, or operator-role check on this route. Two edges worth recording: (a) a legacy row with NULL created_by_person_id passes the creator check for any authenticated caller (deps.py:560-566); (b) the seed-induction agent dependency resolves its LLM client at request-parse time, so a missing key 503s before the handler body runs (deps.py:261-269).

File-side gates (the only real ones): the referenced file must exist, belong to {eid} (404/403, seed_extraction.py:118-149), and have content_type == 'text/markdown' (415, seed_extraction.py:155-162).

3. The critical question — induction target

Answer: induct_seed neither inducts into {eid} nor creates a new engagement. It is seed-scoped and takes no engagement id at all. The hops, verified in source:

  1. The handler drafts the seed via draft_seed, which takes no engagement argument (engagement/creation.py:86-98); the Seed object is stamped engagement_id=ADMINISTRATIVE_ENGAGEMENT_ID (creation.py:122) and its seed_drafted event lands on the ADMIN log (creation.py:140) — the docstring states it: "No Engagement object is created yet."
  2. The only write to {eid} is the wiring update — UPDATE engagements SET candidate_seed_id = :sid WHERE id = :eid (seed_extraction.py:299-307) — plus a name derivation. Note: unconditional, no WHERE state='candidate' guard.
  3. The handler then calls induct_seed(seed=seed, agent=..., runner=..., actor=..., db=...) with no target passed (seed_extraction.py:321-327); induct_seed's signature has no engagement parameter (creation.py:421-429), and every induction event — dispatch, finding_produced, induction_cycle, finding_addressed — is written under ADMINISTRATIVE_ENGAGEMENT_ID, keyed per-seed (creation.py:465,512,557,578,613,680,698).
  4. Engagement rows are created or promoted only at commit: _create_engagement inserts a new row only when no id is supplied (creation.py:1019-1022) and otherwise promotes the existing candidate in placeUPDATE engagements SET state='active', candidate_seed_id = NULL ... WHERE id = :id (creation.py:1028-1034) — callers being commit_engagement / commit_engagement_divergent (creation.py:797,849). The seed is rehomed onto the real engagement on that path (_rehome_seed_onto_engagement, creation.py:944-998).

Consequence for the chain: Stage 1's route supports the chain shape directly — Engagement 2 exists as a candidate row from draft time, extraction attaches and inducts its seed on the ADMIN side, and commit promotes the same row in place. The chain note's alternative reading ("only the single-engagement bootstrap") is moot. One hazard filed for awareness, not fixed: because the route never checks {eid}'s state, calling it against an already-active engagement would re-populate candidate_seed_id (which commit had NULLed) with a fresh un-inducted seed. Stage 1 should simply not do that; whether it deserves a state guard is an Operator call.

4. Phase 16 upload — text/markdown

Confirmed accepted. _ACCEPTED_MARKDOWN = {"text/markdown"} unioned into the accept set at api/routers/files.py:102-103.

The route that feeds /seed/extract: POST /engagements/{engagement_id}/files (files.py:179-197, 201 Created), multipart with exactly one part — file (no purpose/context fields), auth get_resolved_actor + contributor-designation check (files.py:207). Validation is content-type-based, not suffix-based (files.py:106-120,216): the part must be sent with Content-Type: text/markdown; a .md file sent as application/octet-stream is 415'd. Size cap via loomworks_max_upload_bytes (413).

Trap named: the Phase 58 family POST /engagements/{eid}/uploads (purpose_declaration + accompanying_message, uploads.py:668-676) writes upload events, not the UploadedFileRow the extract endpoint requires — a file_id for extraction can only come from the /files route.

5. Extraction skill — live-run requirements

6. Render-type + specialist registration, API-only path (inventory)

Declaring, amending, retiring, and listing render types is fully covered by HTTP — POST/GET /engagements/{eid}/declared-render-types (+ /amend, /retire) at api/routers/renders.py:280-400, auth get_contributing_contributor, with declared shape types (which a render type must reference) equally covered at declared_shape_types.py:107-249. Binding a specialist to a declared render type has no API route — that is the finding. The durable render_specialist_binding table (keyed UNIQUE(engagement_id, declared_render_type_id); agent identity is an overwritable payload column) is written only by set_render_specialist_binding, whose sole caller is the ad-hoc path inside open_ad_hoc_render_production — surfaced as POST /engagements/{eid}/renders/ad-hoc (renders.py:502-519), which fuses declare + bind + register + dispatch into one call, creates its own inline render type, and gives no control over agent identity. The in-process dispatch registry is populated only by that ad-hoc path or by the lifespan bootstrap (api/app.py:208-212), so a binding row created out-of-band is invisible until process restart. Net: today's API-only route to a working pair is the ad-hoc fusion; the clean "declare, then bind" sequence Stage 2 wants has no route for step two — Stage 2's "small change request" should expect to include that route (or accept the ad-hoc shape).

7. Verdict

Stage 1: GO, with the §3 mechanism understanding and §5 preconditions. The route is live, markdown upload is live, the extraction skill is runnable in the current environment, and the candidate → extract/induct → commit shape is the built path. Stage 2: the render-specialist binding gap in §6 is the one piece its CR must add or consciously route around. Nothing was created, uploaded, or changed by this inspection.


DUNIN7 — Done In Seven LLC — Miami, Florida Agent-Commissioning Stage 1 Readiness Inspection — v0.1 — 2026-08-16