Repository navigation
Stage 3 P0: the seven handoff validations as one pure contract and its tests (nothing wired) #1853
Description
Written-by: Chief
P0 slice of #1550 (owner approved 2026-10-30). Scope: contract + tests only — no behaviour change, nothing wired. This is not the pool implementation; it fixes the seven validations of a handoff as one pure contract, so the P1 shadow run can be measured against it and the pool does not invent a second, silent policy later inside service.js.
Why this slice exists
Today a card's handoff is validated only for from / to / reason / at (core/src/host/tasks.js:436-444), and routeToPool (:494-501) turns handoff.to into the card's pool. Missing entirely: idempotency, per-edge attempt budget, per-exec quota, the acceptance-evidence gate, and escalation.
Deliverable 1 — plugins/workflow/src/pool-contract.js (new, pure)
Pure functions, no I/O, no import of service.js; nothing calls it in this PR.
validateHandoff(handoff, ctx):
handoff = { card, fromRole, toRole, reason, evidence, acceptance, attempt, idempotencyKey? }ctx = { roles, pools, execOwner, worker: { wip }, pool: { inflight, workers }, seen: { keys }, cycleMax = 3, quota: { steps, ms, tokens }, used: { steps, ms, tokens } }
Returns { ok: true, key }, or { ok: false, code, reason, escalate? }. Checks run in this order, with these codes:
target—toRoleis a role the workflow declares and its pool exists →unknown-role/no-pool.owner—ctx.execOwneris the exec's only owner; a write on behalf of another owner →not-owner.evidence— every item named byacceptance.requiresis present: a{ pass: { tip, ran } }requirement needs a PASS naming that tip and aRan:line; a{ pr, checks: 'tests-ok' }requirement needs both a PR number and a greentests-ok; a PR carryingholdmay not enter the merge pool →missing-evidence, andreasonnames the missing item.concurrency— workerwip > 1→wip-limit; poolinflight >= workers→pool-full.idempotency—key = idempotencyKey || sha256(card|fromRole|toRole|attempt); a key already inctx.seenanswers{ ok: true, duplicate: true }and records nothing.cycle—attempt > cycleMax(default 3) on the same edge →{ ok: false, code: 'cycle-budget', escalate: true }.quota— any ofused.steps/used.ms/used.tokensover its limit →{ ok: false, code: 'quota', escalate: true }.
Also export escalationTarget(action) → 'chief' for the two escalating cases: the owner's decision is that a used-up loop budget asks the Chief, who decides whether to open a meeting; nothing auto-escalates to a meeting.
Deliverable 2 — plugins/workflow/test/pool-contract.test.js (new)
At least one case per code above, plus: a duplicate key is accepted exactly once; the four non-escalating refusals record nothing; cycle-budget and quota set escalate: true; the key is stable when the object key order changes and changes when attempt changes.
Deliverable 3 — extend core/test/task-handoff-route.test.js
Pin that today's route semantics are unchanged (core/src/host/tasks.js:494-501): an unknown role still refuses with unknown-role, a recorded handoff still sets queueRole to to, a held card is requeued. Add one cross-check: give the same card facts to validateHandoff and assert checks 1–2 reach the same verdict the host reaches — the two policies must not drift apart silently.
Deliverable 4 — docs/subsystems/workflow.md
One short paragraph: src/pool-contract.js is the typed handoff contract for #1550 Stage 3 P0, pure, and not wired into any dispatch path; name P1 as the step that will call it. (plugins/workflow/ is an owned path — this page is required.)
Constraints
service.js,index.jsandgraph.jsdo not change; no new endpoint; no UI; no new task field.grep -rn "pool-contract" plugins/workflow/src plugins/workflow/index.jsmust stay empty at the tip (only tests and the subsystem page mention it).- No behaviour change anywhere in
core/. - Red first: commit the test file before the module, run
node --test test/pool-contract.test.jsagainst the absent module and keep that failing output in the PR. - No changelog fragment: nothing user-visible changes.
- Run
npm run verifywithTZ=UTCat the tip.
Acceptance
cd plugins/workflow && node --test test/pool-contract.test.jsgreen, covering all seven codes.cd core && node --test test/task-handoff-route.test.jsgreen, existing cases untouched.- Delete
plugins/workflow/src/pool-contract.jsand re-run the workflow test → red (the tests really test it). npm run verifygreen at the tip.- PR body:
Fixes #<this>,Owner:/Reviewer:lines, and a## Checkpointblock.
Activity
Birfy commentedon Oct 30, 2026
Written-by: Triage
agent-ready — scope is the issue body; one P0 slice, implemented here.
Files: new
plugins/workflow/src/pool-contract.js(purevalidateHandoff(handoff, ctx)with the seven ordered checks +escalationTarget, noservice.jsimport, nothing calls it); newplugins/workflow/test/pool-contract.test.js(one case per code, duplicate accepted once, stable key across key order, key changes withattempt); extendcore/test/task-handoff-route.test.js(today's route semantics pinned + checks 1–2 cross-checked against the host); one paragraph indocs/subsystems/workflow.md.Acceptance: red first (test committed before the module, failing output kept in the PR);
cd plugins/workflow && node --test test/pool-contract.test.jsgreen;cd core && node --test test/task-handoff-route.test.jsgreen; delete the module → red;TZ=UTC npm run verifygreen at the tip.Facts checked at triage (main):
plugins/workflow/src/holds service.js, graph.js, match.js, template.js, ephemeral.js, narrate.js andplugins/workflow/test/already exists;core/test/task-handoff-route.test.jsexists;core/src/host/tasks.js:434-504matches the body —readHandoffreads only from/to/reason/at,routeToPoolsetstask.queueRole = handoff.toand requeues a held card.graph.jsis atplugins/workflow/src/graph.js.Risk: T2 (new source file under
plugins/**) → a PASS at the tip is required, plus greentests-ok. Do not write.skip(or.only(in the tests (T3 diff pattern). No changelog fragment. Thegrep -rn "pool-contract" plugins/workflow/src plugins/workflow/index.jsempty-at-tip gate stands.Birfy commentedon Oct 30, 2026
Written-by: Dev
Claimed #1853 — branch
dev/1853-pool-contract, first step: the purevalidateHandoffcontract and its test.Add the pure handoff pool contract and its tests, not wired (ThinkFlo…