DUNIN7 · LOOMWORKS · RECORD
record.dunin7.com
Status Current
Path change-requests/cr-2026-197-loomworks-b8-slice-two-companion-drafts-v0_1.md

DUNIN7-M4 — INFRASTRUCTURE CHANGE REQUEST

CR-2026-197 — B-8 slice two, part one: the Companion's first writes — Shaping and Rendering drafts, per-action only (v0.1)

Version. 0.1 · Date. 2026-08-10 · Author. Claude Code (drafting, from the 2026-08-10 scoping read) / Marvin Percival (approving). Target. /Users/dunin7/loomworks-engine. Surface work is verify-then-do (§5 Step 5); /Users/dunin7/loomworks is touched only if the verify fails. Baseline. Engine d85154d, surface 62b2f27. CC confirms both at Step 0. The live surface is /Users/dunin7/loomworks — not loomworks-ui, which is six weeks stale. Companion to. scoping-notes/loomworks-b8-slice-two-scoping-note-v0_1.md (the framing; its §6 lean is inverted by the code read recorded in build list v0.55) and the B-8 entry in standing-notes/dunin7-build-list-v0_55.md (the rulings). Every file:line in this CR was read at the baseline SHA; where the CR and the code disagree, the code governs and CC reports the divergence. Charter. Autonomous regime. Status. DRAFT — awaiting the gate. Do not execute.


0. Settled — do not re-raise

Operator rulings, 2026-08-10:

  1. Per-action only. pre_authorized stays supported in the machinery but nothing ships using it in this slice. A standing grant can be created in one conversational sentence and makes dispatch immediate with only an informational row — authority given before anyone has watched the Companion write once. Loosening later is cheap; the reverse is not.
  2. Shaping + Rendering together, Manifestation waits. The Manifestation write is B-78, its own item, with the stated-facts consent shape named there. Nothing in this CR touches Manifestation.
  3. Shape confirmation stays out (Q-7: defer). B-79 additionally records that the confirm's auto-dispatch hook is dead in production; this CR does not register it, call it, or reason about it.
  4. Dispatcher semantics follow the capability docstrings verbatim (delegation/schema.py:33-45), per the single-source-of-truth vocabulary discipline: produce_specification = shape production with specification grammar; produce_artifact = render production for a non-specification artifact; initiate_render = ad-hoc/re-run render from the revision path. The docstrings are the declared meaning of the granted authority; the dispatchers must not reinterpret them.

0b. Decisions taken in drafting — vetoable at the gate, then settled

  1. Resolution happens at dispatch time, and the outcome is named after. The approval card is created at propose time with the request as typed ("Draft a specification?"); the dispatcher resolves it deterministically at approval and the execution_result (and the Companion's reply) names exactly what was resolved and enqueued. Moving resolution to propose time — so the card itself names the target — is the tightening move if this proves too blind; it is not built now.
  2. Ambiguity fails; it never guesses. Zero or multiple candidates at any resolution step is a graceful failure naming each blocker separately (per the comment-averaging lesson: "no matching declared type," "two active Shapings match," and "no current Manifestation" are three different sentences, not one).
  3. A consequence of ruling §0.4, stated so it is chosen rather than discovered: non-specification shape production ("produce a storybook shape") has no conversational trigger in this slice — produce_artifact is a render capability by declaration. The Shaping room surface remains the way to produce non-spec shapes.

1. Summary

Slice one taught the Companion to describe the rooms. The path for it to act is fully built and ends in a deliberate hole: classification (request_draft / request_revision), default-deny authorization (verify_companion_authorization, delegation/authorization.py:98), conversational grant/revoke, approval cards with click-time re-verification (notifications/service.py:702-728), and audit-grade ActorRef attribution (memory/base.py:96-153) all exist and run — but dispatch_action finds no handler for any Tier-1 capability and returns {"status": "failed", "reason": "No dispatcher registered…"} (delegation/action_dispatch.py:170-180). The only registered dispatchers are the three credit-layer ones.

This CR registers the three Tier-1 dispatchers — produce_specification, produce_artifact, initiate_render — and forces per-action approval for them at the registry level. No new intent, no new capability, no new consent machinery: companion-triggered jobs inherit the existing spend gates unchanged, including the pre-debit spend-pause park with its cap-named card.


2. Scope

Engine, four pieces:

  1. Per-action enforcement at the registry. _DispatcherEntry (action_dispatch.py:73-87) gains per_action_only: bool = False; register_action_dispatcher (:93-123) gains the matching keyword; a small accessor exposes it. _route_request_draft (router.py:2043) and _route_request_revision consult it before the pre_authorized branch: when the entry demands per-action, an authorization that came back pre_authorized is handled on the approval-card path instead, and the Companion's reply says so plainly (the standing grant is honored as authority, not as immediacy). execute_pre_authorized is not called for such entries from any path.
  2. A dispatcher modulesrc/loomworks/agents/companion_room_dispatchers.py — with three handlers matching the dispatcher contract exactly (async def …(*, action_params: dict, actor: ActorRef, engagement_id: UUID, db: AsyncSession) -> dict, per action_dispatch.py:148-165), a configure_companion_room_dispatch(agent_runner=…) mirroring configure_spend_pause_dispatch (app.py:160-163), and a register_companion_room_dispatchers() called from the lifespan next to the existing three registrations (app.py:107-127). All three register with delegation_required=True (the default) and per_action_only=True.
  3. Resolution, deterministic and total-or-failed:
  1. Actor and audit, for free and verified: the approve path already builds ActorRef(kind="companion", capability_ref=auth.delegation_ref, approval_mode="explicit") and re-verifies authorization at click time (notifications/service.py:702-728). The dispatchers pass that actor through as triggered_by, so every companion-triggered job and event carries the authorizing delegation's id. Nothing new is built; the acceptance gate proves it arrives.

Spend posture — nothing new, both existing gates bind, and this is load-bearing: the jobs run gated_room_complete with room="shaping" / "rendering" exactly as surface-triggered jobs do. The agent_spend_authorized license (credit/authorization.py, default-absent = deny, covering companion actors) must be on for the engagement or the job fails with the refusal recorded; with spend_pause on, the job parks pre-debit and the existing cap-named card (credit/spend_pause_card.py) goes to the Operator. A Companion-triggered draft therefore costs nothing until the Operator has approved twice: once for the act, once (if pause is on) for the capped spend.


3. What this CR does not do

4. Out of scope

The Q-7 conversational confirm; B-77's cache remedy; B-78; B-79; any surface redesign of the inbox cards; the sync→job re-architecture; the request_redirect stub.


5. Build steps

  1. Confirm baselines (engine d85154d, surface 62b2f27, clean trees). Confirm the hole: grep shows no register_action_dispatcher call for any Tier-1 capability outside tests. Confirm companion_notifications.action_type column width admits the three capability strings — they are already written by the card-creation path today, so this is a re-verify, not a risk (pre-flight shape 4).
  2. Registry flag + route consult (§2.1), with the forced-per-action reply wording server-composed from the route result, not left to the model. Touch action_dispatch.py, router.py, and the request_draft / request_revision instruction templates only if their prose contradicts the new behavior.
  3. Dispatcher module (§2.2–2.3): resolvers, three handlers, configure + register functions.
  4. Lifespan wiring: register next to the credit dispatchers (app.py:107-127); configure with the runner after it is built (app.py:158-163 pattern).
  5. Tests — through the entry point, per the spend-context standing note (a green gate test says the gate works, never that anything reaches it):

a. Per-action delegation committed; converse "draft a specification" → outcome: "approval_card", card row exists, no shaping_jobs row. b. Approve the card → shaping_jobs row exists; the shape event's triggered_by has kind="companion" and capability_ref equal to the delegation assertion id; execution_result carries the job and object ids. c. Decline → no job, card marked declined. d. Pre-authorized delegation present → still an approval card, and the reply states the posture. execute_pre_authorized is not called (assert via recording stub). e. No delegation → outcome: "denied" with the grant instruction (existing behavior, regression-pinned). f. Resolution failures: no matching declared type / no active Shaping / no current Manifestation / no confirmed shape — each its own test asserting its own sentence. g. agent_spend_authorized absent → the dispatched job fails with the refusal recorded and visible (the metered branch proven reachable from the conversation entry point; the refusal, not a 502, is what surfaces). h. spend_pause on → job parks pending_approval, spend-pause card created, no debit — companion-triggered jobs inherit the park unchanged. i. Own-key control: an engagement bringing its own key dispatches unmetered, no card, no park (standing-note item 5). j. Revoked-between-card-and-click → denied at approve (existing service.py:702 behavior, regression-pinned for these action types). k. Slice-one guard test untouched and green.

  1. Surface verify-then-do: confirm the inbox (/Users/dunin7/loomworks src/components/notifications/InboxView.tsx) renders a card whose action_type is a Tier-1 capability with approve/decline reachable — the generic proposal path may already cover it. If it does not render, the fix is the smallest component addition that does, and nothing more.
  2. Close: implementation notes; completion record; build-list line for B-8; the §6 heading below recorded in both.

6. Seed-mutability impact

Recorded per the B-69 standing instruction; entries state their Kind.


7. Acceptance gate

  1. All Step-4 tests pass; full engine suite green (zero failures — the baseline discipline); both push gates clean.
  2. In the product: with a per-action delegation granted conversationally, asking the Companion for a specification draft produces a card; approving it produces a pending shape visible in the Shaping room; the shape's provenance shows the Companion as actor with the delegation reference. With spend_pause on, the cap-named card appears before any debit.
  3. In the product, the refusal: with no delegation, the ask is declined with the grant instruction; with a delegation but no agent_spend_authorized, the job's failure is visible and names the spend refusal.
  4. A pre_authorized grant does not bypass the card for these three action types — demonstrated in the product, not only in tests.
  5. Completion record filed; build list moved; seed-mutability entries recorded verbatim as §6.

8. Kickoff prompt


Execute CR-2026-197 v0.1 at
change-requests/cr-2026-197-loomworks-b8-slice-two-companion-drafts-v0_1.md
in loomworks-record. Confirm the CR number and both baselines first.

§0 carries four Operator rulings and §0b three drafting decisions the
gate approved. Don't re-raise any of them.

The live frontend is /Users/dunin7/loomworks. loomworks-ui is stale —
do not read it as the surface.

The tests that matter are 4d and 4g: (d) proves per-action is forced at
the registry, not promised in prose — execute_pre_authorized must be
shown NOT called; (g) proves the spend refusal is reachable from the
conversation entry point, which unit coverage against the gate cannot
show.

HALT if resolution needs a judgment call the CR doesn't decide —
ambiguity fails by design (§0b.2); do not add a heuristic to make a
test pass.

DUNIN7 — Done In Seven LLC — Miami, Florida CR-2026-197 — B-8 slice two, part one — v0.1 — 2026-08-10 Per-action only: the Operator watches the first writes happen one approval at a time.