Version. 0.1
Date. 2026-07-28
Status. Pre-flight verification report. Executes §4 (all eight items) of loomworks-ova-slice-1-cr-drafting-handoff-v0_1 as inspection only.
Author. Claude Code on DUNIN7-M4. Operator: Marvin Percival.
Role boundary. This report verifies and evidence-cites. It does not draft the change request, does not resolve the §2 open design point, and does not decide anything — CR drafting is Claude.ai's role per the standing two-role discipline. Where the handoff asks for a read (item 4), the read is offered as evidence-grounded input to the decision, explicitly not as the decision.
Engine repo. /Users/dunin7/loomworks-engine
Branch. main
SHA read. 1aac8154ca440b831f5ba2bd0fc02d6af890614d
HEAD commit. 1aac815 — Archive CR-2026-155 v0.2 alongside executed v0.1 — 2026-07-28 16:46:59 -0400
Drift note. No drift. git rev-list --count 1aac815..HEAD = 0 (item 8). Both prior reports — the venture-diligence inspection and the OVA Step 0 report — assume this tree, and it is unchanged. All three describe one identical commit. Working tree clean apart from M uv.lock (dependency lockfile, no source drift).
Read-only. Nothing committed, nothing pushed, no engine file modified.
Next number: CR-2026-157.
The handoff directs the number be taken from docs/phase-crs/. That directory's highest is cr-2026-155, and the record's change-requests/ also tops out at cr-2026-155 — so both of the obvious sources point at 156.
156 is taken. A repo-wide scan of loomworks-record (excluding .git/ and record-site/) surfaces:
./stele/change-requests/CR-2026-156-published-front-door-v0_2.md
CR-2026-156 was filed into the Stele change-request directory, not the main change-requests/ one, so it is invisible to both directories the handoff's item 7 names. It landed in record commit fa7c8c1 (File CR-2026-156 and the orientation-pack grounding pair) — the pre-existing unpushed commit carried in this session's first push.
Assigning from docs/phase-crs/ alone would have collided. 157 is the first free number; the drafter should confirm against stele/change-requests/ as well as the two conventional sources.
HTTP entry. POST /engagements/{engagement_id}/contributions — src/loomworks/api/routers/contributions.py:97-158.
Handler signature (:111-118):
async def contribute_route(
body: ContributionRequest,
engagement_id: UUID = PathParam(...),
session: AsyncSession = Depends(get_db_session),
actor: ResolvedActor = Depends(get_resolved_actor),
registry: ExtractionSkillRegistry = Depends(get_extraction_registry),
secret_key: str = Depends(get_secret_key),
) -> ContributionResponse:
Present gate. Two in-body calls at :139-140: _require_contributor(actor) then _validate_one_of(body). _require_contributor (:57-72) refuses persons lacking commit-eligible designations:
if actor.actor_ref.kind == "person" and not actor.can_commit:
raise HTTPException(403, "Contributing requires the 'contributor' designation on this engagement.")
Note the membership requirement is upstream of this, in get_resolved_actor itself (deps.py:1160-1170) — the endpoint never sees a non-member.
Branching. Text mode → _contribute_text (:161-…); file mode → _contribute_file (:151-158).
Data-layer function. add_assertion — src/loomworks/engagement/assertions.py:157-228. Signature (:157-166):
async def add_assertion(
*, engagement_id: UUID, content: str,
grammar_element: Literal["definition", "constraint"],
normative_force: Literal["required", "recommended", "optional"] = "required",
actor: ActorRef, db: AsyncSession,
metadata: dict[str, Any] | None = None,
) -> Assertion:
Behavior: allocates a per-engagement display_number via atomic UPDATE … RETURNING (:186-201, CR-2026-121 — number minted at add, not commit); constructs the Assertion with state="held" (:217) and Provenance(wasAttributedTo=actor, …) (:211-214); appends the event via append_event(event_kind="assertion_added", …) (:223-229). Raises EngagementNotFound when the engagement row is absent (:202-205).
The text path calls it at contributions.py:166-174 with grammar_element="definition", normative_force="optional", metadata={"source_mode": "text"}.
Where an authorizer call would insert (observation, not design). Two candidate points are visible in the code as it stands: the route body alongside _require_contributor (:139), which is surface-level; and inside add_assertion before the display-number allocation (:181), which is the substrate write path the scoping note §3 names as the chokepoint-discipline requirement. The two differ in blast radius — add_assertion has callers beyond this endpoint. The drafter should enumerate those callers; this report did not.
_alpha_authorizer_stub verbatim, its caller, and what has grown around it
Verbatim — src/loomworks/credit/cross_engagement_memory.py:65-85:
def _alpha_authorizer_stub(
*,
requesting_engagement_id: UUID,
requesting_person_id: UUID,
target_engagement_id: UUID,
) -> bool:
"""OVA-stub-style alpha-scope authorization.
Phase 49 alpha is single-Operator (Marvin); all engagements share
one Operator; cross-engagement read access is unconditional within
Marvin's scope. The function takes the would-be Authorizer
arguments so the long-horizon contract is exercised in production-
shaped use ahead of the OVA substrate's readiness; the alpha stub
answers ``True`` for every request.
The unused arguments are intentional: the contract surface is the
point. When OVA proper deploys, this function swaps to a real
Authorizer call without callers changing.
"""
_ = (requesting_engagement_id, requesting_person_id, target_engagement_id)
return True
Its one call site — :120-125, inside read_credit_management_assertions (:88-…):
if not _alpha_authorizer_stub(
requesting_engagement_id=requesting_engagement_id,
requesting_person_id=requesting_person_id,
target_engagement_id=CREDIT_MANAGEMENT_ENGAGEMENT_ID,
):
Deny path already written (:125-131): logs cross_engagement_memory.denied and returns an empty list, with the docstring noting callers' degradation path tolerates the empty case.
Nothing has grown around the stub itself. Module imports are unchanged and minimal (:38-47): logging, UUID, SQLAlchemy text/AsyncSession, CREDIT_MANAGEMENT_ENGAGEMENT_ID, Assertion. The stub remains module-private, single-call-site, unconditionally true.
One divergence from the handoff's framing, flagged. The handoff (§4 item 2) and the scoping note §3 both describe the stub as having "one caller." That is exactly true of _alpha_authorizer_stub — one call site. But its wrapping function read_credit_management_assertions has three call sites across three modules:
src/loomworks/credit/conversion_credit_override.py:182 (imported :59)src/loomworks/credit/grant_decision_dispatcher.py:169 (imported :53)src/loomworks/orchestration/exhaustion_choice.py:206 (imported :39)So "migrate the stub's single existing caller onto the general interface with behavior unchanged" (handoff §1a) touches one call site, but the behavior surface it must hold unchanged spans three downstream consumers in the credit and orchestration packages. This is a scoping input for the drafter, not a contradiction of the handoff — the stub's caller count is right; the caller's caller count is what the migration's regression surface is measured against. This report does not judge whether that changes the slice's size.
Data layer — commit_assertion, src/loomworks/engagement/assertions.py:230-…. The complete check set before the transition:
:281-285 — if actor.kind in NON_HUMAN_ACTOR_KINDS: raise AgentCannotCommitError(...):287-292 — load current version; if current.state != "held": raise AssertionNotHeldError(...):294-301 — conditional AssertionRequiresCommentError when voice_interpretation_disabled metadata is set and no operator_comment exists (CR-2026-146 v0.6)There is no comparison between the committing actor and the contributing actor at the data layer.
HTTP layer — get_committing_resolved_actor, src/loomworks/api/deps.py:1182-1204:
:1195-1199 — refuses NON_HUMAN_ACTOR_KINDS with 403 ("Commit requires a human actor (R-B20)."):1200-1204 — if not actor.can_commit: raise HTTPException(403, "Actor does not have commit authority on this engagement.")
The commit route takes this dependency at api/routers/assertions.py:322.
The structural non-member exclusion still holds. get_committing_resolved_actor depends on get_resolved_actor (:1183), which raises 403 when no membership row exists (deps.py:1163-1170), before can_commit is ever evaluated. A principal with no membership on the engagement therefore cannot reach the commit transition through the HTTP surface — the admission-gate composition the scoping note §2 relies on is intact at this SHA.
Verdict: HOLDS — the structural exclusion the scoping note depends on is confirmed unchanged.
What exists. A complete, working invitation → signup → confer-at-claim machine, plus a second parallel instance of the same pattern.
Module — src/loomworks/authority/standing_invitations.py. Module docstring (:1-13) describes the whole lifecycle: "A founder issues an invitation; a link goes to a recipient email; the recipient enrolls a passkey through the ordinary signup ceremony; and at the end their fresh host_account row is set to the invited standing — atomically with the mint."
Surface:
| Function | Lines | Shape |
|---|---:|---|
| generate_invitation_token | :53-57 | secrets.token_urlsafe(48) — 64-char URL-safe, "matching the credit grant's shape and the VARCHAR(64) column" |
| create_standing_invitation | :59-97 | (recipient_email, granted_standing, expires_at, created_by_person_id) → row dict; does not commit |
| validate_and_read_invitation | :100-142 | front-half check; raises InvitationNotFound / InvitationAlreadyClaimed / InvitationExpired; flips expired rows to expired |
| confer_standing_for_token | :144-200 | the claim-time seam; SELECT … FOR UPDATE, re-validates atomically, writes the standing, burns the invitation single-use, returns False if the token belongs to another token space |
| list_invitations | :203-… | issuer's listing |
Typed error family at :29-50, including WrongInvitationType (:45-50) whose docstring notes "the two token spaces are separate tables."
Schema — migrations/versions/0101_standing_invitations.py. Table standing_invitations (:51): claim_token VARCHAR(64) unique (:58, :85), granted_standing TEXT (:63), status (:65), expires_at TIMESTAMPTZ (:70), index on created_by (:101-102). Two check constraints:
:90-93 — granted_standing IN ('dunin7_representative'):94-97 — status IN ('pending_claim', 'claimed', 'expired')
The wiring seam — src/loomworks/persons/onboard.py:100-121. The comment block (:100-103) states the dispatch explicitly: "two invitation kinds in separate tables: a STANDING invitation … Try standing first — confer_standing_for_token returns True when the token was a standing invitation and it handled it, False" — then falls through to seam.claim_grant (:121) for the credit-grant token space.
The read the handoff asks for. The evidence points toward cheap composition rather than two-tier drag, on four grounds. First, the pattern is already instantiated twice — standing invitations and credit grants are two independent token spaces sharing one token shape, one lifecycle vocabulary (pending_claim/claimed/expired), and one dispatch point in onboard, which is what a proven extension pattern looks like rather than a bespoke one-off. Second, the dispatch seam at onboard.py:100-121 is explicitly written as a try-then-fall-through chain across token spaces, so a third space is an additional branch at a site already shaped for it, not a restructure. Third, the whole mechanism rides "the ordinary signup ceremony" (module docstring :5-7) — it confers something at the end of normal passkey enrollment rather than introducing an alternative entry, so a contribution credential composing with it would inherit open sign-up as-is and not require the volume-limit lift that seed v0.12 defers to specification. Fourth, the standing table cannot itself be reused — granted_standing is constrained to the single value 'dunin7_representative' at both the code layer (standing_invitations.py:73-77) and the DB layer (0101:90-93), and it writes host_account.platform_standing, which is platform-wide standing rather than anything engagement-scoped — so the shape a contribution credential would take is a sibling table following the same proven pattern, not an extension of this one. The counterweight the drafter should weigh against this read: nothing here establishes what an engagement-scoped credential does at claim time, since both existing spaces confer platform-level attributes and neither writes anything engagement-scoped; that gap is where unanticipated work could still surface. The decision remains Claude.ai's; this is input to it.
Definition — src/loomworks/memory/base.py:66-118. Four kinds documented at :69-87: contributor (bearer-token path), agent, person (session-cookie path, Phase 16), companion (Phase 41, carries the person's UUID). Optional Phase 45 delegation fields at :89-91. display_name: str | None at :118, commented "Phase 16: populated for person-auth ActorRefs."
Embedding into provenance — Provenance.wasAttributedTo: ActorRef at :146; the field name appears in the serialized payload key list at :52.
Canonical constructors — src/loomworks/stele/actor.py, two public functions:
actor_from_person(person: Principal) -> ActorRef — :25-37; returns kind="person", id=person.id, display_name=person.display_nameactor_from_companion(*, id, companion_name) — :40-…
Snapshot behavior confirmed. The display name is captured into the value object at construction and serialized with the event — there is no join at render time. Combined with the Step 0 report's V5 finding (memory_events declares exactly one FK, to engagements.id, at memory/events.py:92), the attribution path 1c would use carries no membership dependency.
Other construction sites (module-level constants and agent actors, listed for completeness, none on the person-contribution path): credit/specialists.py:78, credit/grant_decision_dispatcher.py:70, :797, :1018, credit/form_submitter.py:43, agents/render_specialist_bootstrap.py:80, :149, :256, :294.
Module — src/loomworks/foray/anchor_priority.py. The registry is _ANCHOR_PRIORITY: dict[str, str] opening at :31 and closing at :82, exported at :84. The module docstring (:13) notes the symbol name is deliberately retained as _ANCHOR_PRIORITY.
The two most recent entries — the 1e template. Both carry a comment block naming the phase, the CR and section, what the event captures, and the priority rationale by analogy to prior conventions:
# Phase 58 §5 / §6.6 — upload pathway event. Captures the full
# provenance chain for a file (or folder) entering the substrate
# via the Phase 58 upload pathway: source artifact, detection
# result, Operator purpose declaration + classifier read, selected
# transformation chain, per-step provenance, held assertions
# drafted, status. "standard" priority per CR §5 by analogy to
# Phase 53 P53-D12 / Phase 50 / Phase 52 conventions.
"upload_event_received": "standard",
# Phase 59 Sub-arc 4a (CR-2026-092 v0.4 §19) — manual-content
# fallback affordance event. Operator-typed content provided via
# the fallback endpoint when an upload's detection failed; carries
# an upload_event_id reference to the original failed upload so
# the audit chain preserves the failed-detection → manual-fallback
# transition. "standard" priority per the upload pathway family.
"manual_content_contributed": "standard",
manual_content_contributed (:80) is the most recent registration and the closest structural analogue for a new contribution-family event: same package, same "standard" priority, and its comment demonstrates the convention of citing the priority decision to a named family precedent.
Observed priority vocabulary across the block: "high" (e.g. key_provided), "standard" (the overwhelming majority), "low" (e.g. candidate_engagement_discarded).
docs/phase-crs/ listing and CR number
Covered in full under CR number determination above. Summary: docs/phase-crs/ holds 124 files, mixing a phase-N-cr- naming family with cr-2026-NNN- entries; its highest CR-2026 number is cr-2026-155, matching the record's change-requests/. CR-2026-156 exists at stele/change-requests/CR-2026-156-published-front-door-v0_2.md, outside both. Next free: CR-2026-157.
git rev-parse HEAD = 1aac8154ca440b831f5ba2bd0fc02d6af890614d, branch main, commit 1aac815. git rev-list --count 1aac815..HEAD = 0 — zero drift.
The venture-diligence inspection report, the OVA reach-enforcement Step 0 report, and this pre-flight all read the same tree, so no finding in any of the three needs reconciliation against the others. Working tree carries only M uv.lock.
| Item | Subject | Outcome |
|---|---|---|
| 1 | Contribution write path | Located end to end; two candidate authorizer insertion points observed, blast radius differs |
| 2 | _alpha_authorizer_stub | Verbatim, unchanged, one call site — but its wrapper has three downstream consumers |
| 3 | Commit transition | HOLDS — structural non-member exclusion intact |
| 4 | Invitation / claim-token machinery | Two proven token spaces + a dispatch seam; read favours cheap composition via a sibling table, with one named gap |
| 5 | ActorRef embedding | Person-anchored, display name snapshotted, no membership dependency |
| 6 | Event registration block | _ANCHOR_PRIORITY; manual_content_contributed is the closest template |
| 7 | CR number | CR-2026-157 — 156 taken in stele/ |
| 8 | HEAD / drift | 1aac815, zero drift |
Two items the drafter should carry forward as live inputs, both flagged above and neither resolved here: the wrapper's three downstream consumers (item 2), which set the real regression surface of the stub migration; and the absence of any existing engagement-scoped claim-time precedent (item 4), which is where composition cost could still surface despite the otherwise favourable pattern evidence.
No architectural divergence was found that would warrant halting. Nothing in this report decides the §2 design point.
DUNIN7 — Done In Seven LLC — Miami, Florida
Loomworks — OVA Slice 1 — CR drafting pre-flight report — v0.1 — 2026-07-28
Engine main @ 1aac8154ca440b831f5ba2bd0fc02d6af890614d — zero drift from 1aac815