Skip to content

Stage 3 P0: the seven handoff validations as one pure contract and its tests (nothing wired) #1853

Closed

Description

@Birfy

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:

  1. target — toRole is a role the workflow declares and its pool exists → unknown-role / no-pool.
  2. owner — ctx.execOwner is the exec's only owner; a write on behalf of another owner → not-owner.
  3. evidence — every item named by acceptance.requires is present: a { pass: { tip, ran } } requirement needs a PASS naming that tip and a Ran: line; a { pr, checks: 'tests-ok' } requirement needs both a PR number and a green tests-ok; a PR carrying hold may not enter the merge pool → missing-evidence, and reason names the missing item.
  4. concurrency — worker wip > 1 → wip-limit; pool inflight >= workers → pool-full.
  5. idempotency — key = idempotencyKey || sha256(card|fromRole|toRole|attempt); a key already in ctx.seen answers { ok: true, duplicate: true } and records nothing.
  6. cycle — attempt > cycleMax (default 3) on the same edge → { ok: false, code: 'cycle-budget', escalate: true }.
  7. quota — any of used.steps / used.ms / used.tokens over 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.js and graph.js do not change; no new endpoint; no UI; no new task field.
  • grep -rn "pool-contract" plugins/workflow/src plugins/workflow/index.js must 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.js against the absent module and keep that failing output in the PR.
  • No changelog fragment: nothing user-visible changes.
  • Run npm run verify with TZ=UTC at the tip.

Acceptance

  1. cd plugins/workflow && node --test test/pool-contract.test.js green, covering all seven codes.
  2. cd core && node --test test/task-handoff-route.test.js green, existing cases untouched.
  3. Delete plugins/workflow/src/pool-contract.js and re-run the workflow test → red (the tests really test it).
  4. npm run verify green at the tip.
  5. PR body: Fixes #<this>, Owner: / Reviewer: lines, and a ## Checkpoint block.

Activity

  1. Birfy commented on Oct 30, 2026

    @Birfy
    CollaboratorAuthor

    Written-by: Triage

    agent-ready — scope is the issue body; one P0 slice, implemented here.

    Files: new plugins/workflow/src/pool-contract.js (pure validateHandoff(handoff, ctx) with the seven ordered checks + escalationTarget, no service.js import, nothing calls it); new plugins/workflow/test/pool-contract.test.js (one case per code, duplicate accepted once, stable key across key order, key changes with attempt); extend core/test/task-handoff-route.test.js (today's route semantics pinned + checks 1–2 cross-checked against the host); one paragraph in docs/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.js green; cd core && node --test test/task-handoff-route.test.js green; delete the module → red; TZ=UTC npm run verify green at the tip.

    Facts checked at triage (main): plugins/workflow/src/ holds service.js, graph.js, match.js, template.js, ephemeral.js, narrate.js and plugins/workflow/test/ already exists; core/test/task-handoff-route.test.js exists; core/src/host/tasks.js:434-504 matches the body — readHandoff reads only from/to/reason/at, routeToPool sets task.queueRole = handoff.to and requeues a held card. graph.js is at plugins/workflow/src/graph.js.

    Risk: T2 (new source file under plugins/**) → a PASS at the tip is required, plus green tests-ok. Do not write .skip( or .only( in the tests (T3 diff pattern). No changelog fragment. The grep -rn "pool-contract" plugins/workflow/src plugins/workflow/index.js empty-at-tip gate stands.

  2. added
    agent-readyMaintainer-approved spec: an agent may take it. Only maintainers set this; never in a template
    on Oct 30, 2026
  3. self-assigned this
    on Oct 30, 2026
  4. added
    agent-workingAn agent has claimed this issue (the claim comment names the branch)
    on Oct 30, 2026
  5. Birfy commented on Oct 30, 2026

    @Birfy
    CollaboratorAuthor

    Written-by: Dev

    Claimed #1853 — branch dev/1853-pool-contract, first step: the pure validateHandoff contract and its test.

  6. removed
    agent-workingAn agent has claimed this issue (the claim comment names the branch)
    on Oct 30, 2026
  7. removed
    agent-readyMaintainer-approved spec: an agent may take it. Only maintainers set this; never in a template
    on Oct 30, 2026
  8. added a commit that references this issue on Oct 30, 2026
    9a0b7c8
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:automationScheduled tasks and automationenhancementNew feature or request

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions