Version. 0.1
Date. 2026-06-25 (filed). Built, verified, merged, and pushed 2026-06-24.
Author. Marvin Percival (DUNIN7 Operator) with Claude.ai (CR drafting) on DUNIN7-M4; CC executing on DUNIN7-M4.
Status. Completion record. CR-2026-121 (Companion Human Commit Affordance) is built, verified on playground_dev + playground_test, and merged + pushed to both product repos' main: loomworks-engine at 0ae231b (08d0c0c..0ae231b, a --no-ff merge of the five-commit branch) and loomworks (Companion) at bafdb07 (c17539a..bafdb07, a --no-ff merge of the one-commit branch). loomworks-ui (Workshop) untouched — port reference only. Markdown primary (technical consumer).
Governing CR. cr-2026-121-companion-commit-affordance-v0_1.md (planned trajectory). Grounded by Step 0 inspection companion-commit-step0-v0_1.md (2026-06-24).
What this did. When an Operator adds knowledge through the Companion, it lands as a held draft awaiting their approval — but until now the Companion had no way to give that approval. The only place to commit a held assertion was the Workshop, the harder surface the Companion exists to replace. This change gives the Companion its own commit affordance, two ways that share one addressing scheme:
Two correctness properties are built in. First, commit is the Operator's reserved act (R-B20): every commit records the human as the confirming party, never the Companion — including the verbal path, which executes as the authenticated human Operator. Second, the Companion's language stops blurring state: a held draft is "held, awaiting your commit", and only after commit "committed to the record".
Why it is the keystone. This is build item #1 of the Companion build order. Until commit works on the Companion, nothing downstream (deriving a Manifestation, creating a Shape, producing a Render) can be reached or tested there, because each needs committed knowledge to operate on. The open loop from the build map is now closed.
A durable, lifecycle-stable held number (engine). Assertions already had a per-engagement display_number, but it was allocated at commit and was None while held — so no held assertion had a speakable handle. The number is now allocated at add_assertion from the engagement's existing next_display_number counter (the same single counter commit-time allocation has always used, so held and committed share one sequence — no collision). Commit adopts the existing number rather than minting a fresh one: one number for the assertion's whole life ("held 3" → "committed 3"). Migration 0094 backfilled every currently-held assertion that carried no number.
Feed enrichment. The held number travels to the Companion. The held_assertion arm of /me/dashboard/needs_you emits it (mirroring the sibling pending_shape arm), and the operator-vocabulary translation that the Companion actually consumes (/operator/dashboard, /operator/inbox) carries it through DashboardInboxPreview and InboxItem. The number rides in the payload, decoupled from feed order.
An actionable Companion held surface. The dashboard "Needs you" zone renders held assertions (operator vocab: waiting_note) as an actionable card — visible "Held N", a selection checkbox, and zone controls "Commit selected (N)" / "Commit all" that loop the one-per-call commit endpoint through the Operator's own session. Selection / commit affordances appear only where a commit is possible (no disabled controls).
A commit_assertion converse intent. Classifier (IntentLabel gains commit_assertion; taxonomy + held_reference extraction with confirmation carry-forward), router dispatch branch, _route_commit_assertion (two-turn confirm-back; resolves spoken number → held assertion; "all" → every held; unknown number commits nothing), and an instruction template.
Honest fidelity language. Held vs committed are named distinctly in the card, the confirm-back, and the post-commit reply. The confirm-back and post-commit replies are server-composed (delegated_response) so the held-vs-committed distinction is guaranteed, not left to the responder LLM (the established behaviour-guarantee bypass pattern).
| # | Decision | Resolution | |---|---|---| | D1 | Commit shape | Both — card (primary) and conversational intent, sharing one identifier scheme. | | D3 | Identifier durability | Durable add-time held number, not an ephemeral read-time ordinal. | | D4 | Identifier lifecycle | Lifecycle-stable — the add-time number carries through to committed. Gated on V1. | | D6 | Verbal addressing | The Operator speaks the held number; the intent resolves it within the engagement. | | D7 | Authority (R-B20) | The verbal commit executes as the authenticated human Operator, recorded as the human confirming party. Gated on V6. | | D8 | Fidelity | Confirm-back before commit; "held, awaiting your commit" vs "committed to the record" never confused. |
Run read-only before any change. V1 and V6 are the two gated decisions; both were cleared by the Operator before the build began.
display_number. list_assertions orders by created_at DESC, object_id ASC; the shape/render display_numbers.py helpers are a separate per-type number space; every assertion display_number site is a display label with a null-guard. → Operator cleared lifecycle-stable (D4).operator_converse authenticates person: Principal; route_intent already threads person + db + engagement_id into the dispatch chain; the human committing actor is actor_from_person(person) (the exact analogue _resolve_cookie_actor uses), gated on operator/contributor membership. The data layer accepts a kind="person" actor (rejects only kind="agent"). → Operator cleared both card + verbal paths.next_display_number counter; no collision.display_number = None. Refinement (a finding the CR did not anticipate): held assertions are not uniformly None — revise_assertion carries a committed assertion's number forward onto the new held version, so a revised-from-committed draft (E0007 #14) is held with its number. The migration backfills only display_number IS NULL; the downgrade restores only never-committed drafts.ORDER BY surfaced_at DESC, item_id DESC; the held number rides in a dedicated payload field, decoupled from order.engagement_id, last_updated_at, object_id), drawn from each engagement's counter, only display_number IS NULL held.HeldAssertionCard pattern), not a converse call — R-B20 satisfied with no special handling.0ae231b
| # | Commit | What |
|---|---|---|
| 1 | 464dc28 | Migration 0094 — backfill held-assertion display numbers (run + reversible; dev & test at head 0094) |
| 2 | 9b7c93c | Allocate held number at add_assertion (lifecycle-stable); commit adopts existing |
| 3 | 48ef463 | needs_you feed emits the held number |
| 4 | e79dd71 | Held number reaches the operator-vocab feed (DashboardInboxPreview / InboxItem + translators) |
| 5 | 40eff76 | commit_assertion converse intent (classifier + router + _route_commit_assertion + template) |
Alembic 0093 → 0094. Migration verified on playground_dev (Fenwick E0060 → held numbers 1, 2, 3; E0007 #14 revise-from-committed untouched; projection and event-log agree) and up/down/up roundtrip-clean on playground_test. Engine suite 2938 passed / 0 failed / 46 skipped at head. Merged --no-ff into main at 0ae231b (16 files, +916 / −24); origin/main confirmed 08d0c0c pre-merge.
bafdb07
| # | Commit | What |
|---|---|---|
| 7 | e53080a | Actionable held card (held number, multi-select, Commit selected/all), fidelity language, verbal-path wiring via existing converse |
DashboardInboxPreview + InboxItem types gain display_number; new commitAssertion helper (direct authenticated POST); HELD_COMMIT strings block. Companion suite 470 passed / 0 failed; ESLint clean; tsc adds zero new errors (3 pre-existing TagChipArea.test.tsx errors, unrelated). Merged --no-ff into main at bafdb07 (5 files, +307 / −2); origin/main confirmed c17539a pre-merge.
Unchanged. Port reference only (HeldAssertionCard).
display_number unchanged across held→committed (DB-confirmed).assertion_committed event with committed_by = the human; item leaves the held surface.committed_by; every commit is the human (R-B20).commit_assertion route (resolution, human authority, confirm-back, commit-all, unknown rejection, no-authority refusal).E0060's three held drafts now carry stable numbers 1, 2, 3. Both commit paths were exercised against the real engagement as Marvin Percival (operator), inside a rolled-back transaction so the drafts stay held:
actor_kind = person, actor_id = Marvin, display_number = 1 preserved.committed_held_numbers = [2], reply "Committed held 2 to the record: …", actor_kind = person, display_number = 2 preserved.DB re-verified afterward: three held, numbers 1/2/3, untouched.
revise_assertion carries a committed number forward onto a held revision (E0007 #14). The migration backfills only display_number IS NULL; the downgrade restores null only for assertions with no committed version anywhere in their history (the never-committed set add-time allocation introduces). This refinement also de-risks D4: the carry-forward mechanism already existed on the revise edge; only the initial add edge was new.needs_you arm; the Companion consumes the Phase-40 operator-vocab /operator/dashboard (and /operator/inbox), so the held number also had to thread through translate_needs_you_item / translate_needs_you_to_inbox into DashboardInboxPreview / InboxItem. Without it the number stopped at the engine boundary.op.execute() calls.test_phase_54, test_phase_55) hardcode the intent count; updated 22 → 23.EngagementNotFound contract preserved — moving the allocation before append_event in add_assertion re-raises EngagementNotFound for a missing engagement, keeping the prior caller/test contract.delegated_response), guaranteeing D8 fidelity rather than relying on the responder LLM. The two-turn idiom survives because turn-recording (step 7) stamps the companion turn's classified_intent independently of the delegated path.0094 PENDING in prod
The pushes moved code; they did not run the migration in production. The engine repo has a single GitHub workflow — api-docs.yml (API-docs freshness) — and no deploy or alembic upgrade workflow. Where 0094 has run: dev (playground_dev) and test (playground_test), both local, at head 0094. Where it has not: production (the engine DB behind api.loomworks.dunin7.com). Until 0094 runs there, prod held assertions will not carry backfilled numbers and new held assertions will not be born with one — the code is live on main, the schema/data change is not. 0094 is a counter-drawing data backfill; it should run as part of the same deploy that ships this code (it is order-independent and idempotent on the display_number IS NULL predicate). The held-commit feature is not fully functional in prod until 0094 runs there.
0094 — pending the engine deploy (§7). The one true outstanding item.cr-2026-121-companion-commit-affordance exists on both loomworks-engine and loomworks, kept pending this record; safe to delete now that the record is filed.held_reference on the confirmation turn relies on the classifier prompt guidance); the route logic re-derives from the current held set on every turn, so a missed carry-forward degrades to a clarification, never a wrong commit.loomworks-ui (Workshop) unchanged; no requirement forces a catch-up.
Completion record. Pre-flight V1–V7, five engine commits + one Companion commit, full-suite green on both repos, and a non-destructive end-to-end proof on real Fenwick E0060 data. The keystone commit affordance is live on both main branches; production migration 0094 is the single remaining deploy-time step.