feat(claim): two-phase dispatch — claim before Phase -1, probe sees it (SHM S2) - #1467
Merged
Conversation
added 2 commits
August 18, 2026 12:05
S2 of the skill-harmonization-mechanisms umbrella (spec §5.3, D-H5/P-5). Four parts, all four required — the diff is deliberately not prose-only. 1. Runtime-bridge split (`AifHandoffBackend`): `dispatch()` was create+unpause in one atomic call, so a lane became observable only AFTER the work started. Split into `claim()` (POST /tasks paused:true), `release()` (unpause) and `cancelClaim()` (DELETE), behind an optional `ClaimCapableBackend` interface + `supportsClaims()` guard. `dispatch()` is now literally claim+release with the original rollback, so the one-shot callers are behaviourally unchanged — all 227 pre-existing runtime-bridge tests pass untouched. New CLI entrypoint `cli/claim.ts` exposes the three verbs; unlike `dispatch.ts` it exits NON-zero and never falls back to ManualBackend, because a claim that silently failed is worse than no claim. 2. `/pipeline` §6 Step 3 reordered: claim-create fires BEFORE the Phase -1 window, release on GO, cancel on RED. Detail routed to `references/claim-machinery.md` + `references/stage-gates.md` so SKILL.md stays at 599 lines (the 600 gate). 3. `probe-inflight.sh` signal 6 (`claim`): the jq filter for signal 5 selects only done/verified tasks carrying a branch, so a fresh paused claim was invisible to ALL five existing signals. Matching is by slug in title+description — no claim marker field, because a second status vocabulary is what P-5 forbids. 4. Orphan expiry: a claim past `PROBE_CLAIM_TTL_MIN` (default 120min) reads `STALE-CLAIM` instead of blocking the stage forever (starvation mode TD-F5). Surfaced, never auto-cancelled — an automatic sweep would race the sessions it protects. Unparseable/missing createdAt counts as live (fails toward blocking). Also: `cli/claim.ts` + the split backend are added to the runtime-bridge vendor drop. The shipped `/pipeline` now instructs a consumer to run it, and the vendor README's own admitting criterion (PR #1432 precedent) is «vendor what a shipped skill instructs the consumer to run» — otherwise the promise is a documented lie. Install baselines regenerated (`SNAPSHOT_MODE=capture`); the reviewed delta is exactly 4 lines per JS fixture: the pipeline SKILL.md hash, the two new reference docs, and `.prettierignore` (the installer auto-adds shipped docs to its managed ignore block — reproduced and confirmed against a pristine staging checkout, which is 15/15 clean). Live proof (probe P4, spec §6) against the running aif queue, not the diff: FRESH → claim-create → CLAIMED (age=0m live) → cancel → 404 → FRESH; orphan branch proven on the real task's own createdAt with the clock advanced 3h → STALE-CLAIM; slug isolation and cwd-independence both confirmed. Own cold review of the diff before handoff (T19) caught three defects, all fixed here with regression tests: (a) the stale count was `grep -c ' stale '` over the whole detail line, so a claim titled "demo fix stale refs" counted as expired one minute after creation — the verdict then told the operator to cancel a lane somebody was actively holding. Counts now read the fixed field-3 state token, and titles are squashed to one line so they cannot inflate the count either. (b) `cancelClaim()` returned void, so the CLI printed "lane is free" even when the DELETE failed. It returns a boolean now and the CLI exits non-zero with an honest message — best-effort must not mean unreported. (c) the live proof then showed (b) firing on a REPEAT cancel: a 404 means the claim is already gone, i.e. success. 404 is now treated as cancelled, so an idempotent retry stops raising a false alarm. The kickoff §3 sanctioned a `Prior-art: skipped` escape hatch here; the pre-push gate rejected it, correctly — `cli/claim.ts` is 140 LOC under `packages/`, which is a capability commit by CLAUDE.md's own definition. The gate won, and the fresh context7 pass it forced (3 phrasings, `/lee-to/aif-handoff`, 2026-08-18) paid for itself twice. It CONFIRMED the design premise — «paused tasks are excluded from coordinator selection queries», so a claim really does occupy no lane — and it surfaced prior art the design had not accounted for: upstream already claims tasks atomically via `lockedBy`/`lockedUntil` with automatic stale-claim release. T16 check says that is a different problem class (mutual exclusion between poll cycles over an EXISTING row, vs. our intent registered BEFORE any row exists), so it is not the surface to reuse; its stale-claim vocabulary IS adopted for the orphan branch, with the auto-release half deliberately declined — our claim's owner may still be alive. Recorded as SSOT #258 (ADAPT) following the #125 precedent for re-evaluating a candidate against a different capability; #28 keeps its row and gets a `Last reviewed` bump. §1.7: forward-check applied — the new claim protocol is itself gated, not trusted: probe verdicts are asserted in packages/core/skills/dispatcher/probe-inflight.test.ts:1 (claim signal, orphan expiry, precedence) and the backend split in packages/runtime-bridge/test/aif-claim-split.test.ts:1, so the guard cannot silently regress to attention; backward-check sweep — every probe consumer enumerated by grep, not by memory: .claude/skills/dispatcher/SKILL.md:65 was the only surface documenting the signal table + verdict ladder and is updated (signal 6 + CLAIMED/STALE-CLAIM), the sibling CLI entrypoint guard in packages/runtime-bridge/test/dispatch-import-no-sideeffect.test.ts:69 was extended to claim.ts, the shipped vendor drop was re-vendored so the promise in .claude/skills/pipeline/SKILL.md:433 reaches a consumer, and CLAUDE.md's pre-dispatch section enumerates no verdicts so it needed no edit (surfaced, not silently skipped). Prior-art: prior-art-evaluations.md#258 (verdict ADAPT — the claim reuses the existing aif `paused` REST primitive rather than adding one; upstream's own `lockedBy`/`lockedUntil` claim is a T16 problem-class mismatch, vocabulary adopted for STALE-CLAIM, mechanism not). Prior-art: prior-art-evaluations.md#28 (verdict DEFER — its «pipeline automation requires machine-readable state without human presence» trigger fired; the capability-specific re-evaluation is #258, per the #125 precedent).
This was referenced Aug 18, 2026
artyhoo
added a commit
that referenced
this pull request
Aug 18, 2026
…inal PR #1468) (#1470) All three build stages of the operator-axis harmonization tail merged: S1 #1464 (§5.2 CONTEXT.md pointer-rule principle test), S2 #1467 (§5.3 two-phase claim machinery), S3 #1468 (§5.4 mechanical dependency frontier + slicing vocabulary). Written by the session that merged the last stage, per the umbrella closure convention (docs/meta-factory/operational-conventions.md §1) — `done.md` is the load-bearing `priority-score.sh` Layer C3 fallback, so without it `/pipeline` keeps offering a closed umbrella. Verified live, both consumers: - `priority-score.sh` → `status=DONE done_pr=1468 basis=done-md` - `frontier.sh skill-harmonization-mechanisms` → `DONE: S1 S2 S3` / `FRONTIER: (none)` (layer 1 answering first — the stage rows themselves carry no merge markers) Also records what this umbrella deliberately did NOT close: the seven observations S3's two PR-blind cold seats surfaced outside the ratified stage scopes (the weightiest being that `/dispatcher` §2.7 and `night-mode` still advance stages without reading the frontier), plus the parent spec's own open residue (§8 item 1 attended CONTEXT.md run, D-H18 consumer-axis contour, the ARMED D-H7 counter). The operator routed items 1-7 to a follow-up session on 2026-08-18. Co-authored-by: Test <test@example.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
S2 «claim» of the
skill-harmonization-mechanismsumbrella — spec §5.3 (D-H5, premise P-5). Four parts, all four present; a prose-only diff would have been non-conformant by construction (kickoff trap T-SHM-B).The race this closes was measured, not imagined: every historical double-dispatch materialised inside the Phase -1 cold-review window. Session A probes clean, opens a cold review; session B probes during that review, also sees clean, dispatches. Neither did anything wrong — there was simply no artefact representing «A intends to take this stage» until A actually dispatched. The claim moves that artefact to the front of the window.
1 — runtime-bridge split (
packages/runtime-bridge/src/AifHandoffBackend.ts:183).dispatch()was create+unpause in one atomic call. Split intoclaim()(POST/taskspaused:true),release()(unpause) andcancelClaim()(DELETE), behind an optionalClaimCapableBackend+supportsClaims()guard (packages/runtime-bridge/src/backend.ts:46) soManualBackendis not forced to fake a queue.dispatch()is now literally claim+release with the original rollback — all 227 pre-existing runtime-bridge tests pass untouched. New entrypointpackages/runtime-bridge/src/cli/claim.ts:1exposes the three verbs; unlikedispatch.tsit exits non-zero and never degrades toManualBackend, because a claim that silently failed is worse than no claim (the next session would probe a lane that looks clean).2 —
/pipeline§6 Step 3 reorder (.claude/skills/pipeline/SKILL.md:425). Claim-create before Phase -1,releaseon GO,cancelon RED. Detail routed toreferences/claim-machinery.md+references/stage-gates.md; SKILL.md stays at 599 lines (600 gate).3 — probe widening (
.claude/skills/dispatcher/helpers/probe-inflight.sh:126). Signal 5's jq filter selects onlydone|verifiedtasks carrying a branch, so a fresh paused claim was invisible to all five existing signals. Signal 6 matches a paused, unfinished task by slug in title+description — deliberately no claim marker field, because the tasktitleis already the slug and inventing one would be the second status vocabulary P-5 forbids.4 — orphan expiry. A claim past
PROBE_CLAIM_TTL_MIN(default 120min) readsSTALE-CLAIMrather than blocking the stage forever (starvation mode TD-F5). Surfaced, never auto-cancelled — an automatic sweep would race the sessions it protects. Unparseable/missingcreatedAtcounts as live: the guard fails toward blocking.Closes probe P4 (spec §6) — live proof, not the code merge
Kickoff trap T-SHM-A says the signal is not done at merge. Run against the live aif queue on
localhost:3009:The GO branch is proven live too — including the half that is easiest to hand-wave («and then the coordinator picks it up»). Held the claim for 6s as a control, then released it:
A claim therefore genuinely occupies no lane while paused (three coordinator polls ignored it), and
release()genuinely hands it over (backlog -> planningwithin ~6s). Residue, stated because it is the honest cost of proving this: reachingplanningmade aif create a per-task worktree inside the container before the task was deleted. The worktree has been removed; one empty branch (feature/shm-release-probe-e0f6e8, 0 commits ahead of staging, 0 uncommitted changes) is still in the container — force-deleting a branch is agent-blocked machine-wide, so it needs one maintainer command.Entry-point coverage, honestly. The claim covers the aif-queue surface; host branches, container branches and PRs stay covered by signals 1/4/2 exactly as before. Verified cwd-independent — the same live claim reported
CLAIMEDfrom three unrelated working directories. Measured limit worth recording: each session runs the probe from its own checkout, so a sibling worktree on a pre-merge base reportedFRESHagainst a live claim. Until this lands onstagingthe guard protects only sessions based on it. All of this is written down inreferences/claim-machinery.md, not just here.Own cold review (T19) caught three defects before handoff
All three fixed here, each with a regression test:
demo fix stale refscounted as expired one minute after creation, and the verdict told the operator to cancel a lane somebody was actively holding. The inverse of the starvation mode it was meant to prevent. Counting now reads the fixed field-3 state token; titles are squashed to one line so a multi-line title cannot inflate the count either (packages/core/skills/dispatcher/probe-inflight.test.ts:308).cancelClaim()returnedvoid, so the CLI printed «lane is free» even when the DELETE failed. Best-effort must not mean unreported: it returns a boolean and the CLI exits non-zero with an honest message.Prior-art — the gate overruled the kickoff, and was right
Kickoff §3 sanctioned
Prior-art: skipped. The pre-push gate rejected it:cli/claim.tsis 140 LOC underpackages/, a capability commit by CLAUDE.md's own definition. The forced context7 pass (3 phrasings,/lee-to/aif-handoff, 2026-08-18) paid for itself twice — it confirmed the design premise («paused tasks are excluded from coordinator selection queries», so a claim genuinely occupies no lane), and it surfaced prior art the design had missed: upstream already claims tasks atomically vialockedBy/lockedUntilwith automatic stale-claim release. T16 check: different problem class (mutual exclusion between poll cycles over an existing row vs. our intent registered before any row exists), so not the surface to reuse — but its stale-claim vocabulary is adopted for the orphan branch, with the auto-release half deliberately declined. Recorded as SSOT #258 (ADAPT) per the #125 precedent; #28 keeps its row and gets aLast reviewedbump.Shipped-surface obligation
The shipped
/pipelinenow instructs a consumer to runclaim.ts, so the vendor drop carries it (packages/runtime-bridge/vendor/src/cli/claim.ts) along with the split backend — the vendor README's own admitting criterion (PR #1432 precedent): vendor what a shipped skill instructs the consumer to run, or the promise is a documented lie. Install baselines regenerated with the diff reviewed first: exactly 4 lines per JS fixture — the SKILL.md hash, the two new reference docs, and.prettierignore(the installer auto-adds shipped docs to its managed ignore block; reproduced against a pristine staging checkout, which is 15/15 clean).Fidelity verdict
FIDELITY: skipped — no cold fidelity-auditor seat was dispatched for this stage; the acceptance evidence is the author's own T19 cold review (three defects found and fixed, listed above) plus the live RED/GREEN proof against the running aif queue. Dispatching a cold seat before merge remains the operator's call.
Prior-art: prior-art-evaluations.md#258 (verdict ADAPT — the claim reuses the existing aif
pausedREST primitive rather than adding one; upstream's ownlockedBy/lockedUntilclaim is a T16 problem-class mismatch, vocabulary adopted for STALE-CLAIM, mechanism not).Prior-art: prior-art-evaluations.md#28 (verdict DEFER — its «pipeline automation requires machine-readable state without human presence» trigger fired; the capability-specific re-evaluation is #258, per the #125 precedent).
Verification
§1.7 Forward-check applied
The claim protocol is gated, not trusted. Probe verdicts are asserted in
packages/core/skills/dispatcher/probe-inflight.test.ts:1— claim signal, orphan expiry, precedence, and both title-collision regressions — and the backend split inpackages/runtime-bridge/test/aif-claim-split.test.ts:1, so the guard cannot regress to bare attention (.claude/rules/attention-is-not-a-mechanism.md:1§1: a load-bearing check must be a deterministic gate, never «someone will notice»). TheSTALE-CLAIMbranch is deliberately a surfaced state rather than an auto-sweep, because expiry is a decision, and gating a judgment is#gate-where-judgment-needed.§1.7 Backward-check applied
Every probe consumer was enumerated by grep, not from memory.
.claude/skills/dispatcher/SKILL.md:65was the only surface documenting the signal table and verdict ladder — updated with signal 6 and both new verdicts, and the existing wiring test atpackages/core/skills/dispatcher/probe-inflight.test.ts:394went RED first for exactly that omission before the doc was written. The sibling CLI entrypoint guard atpackages/runtime-bridge/test/dispatch-import-no-sideeffect.test.ts:69was extended toclaim.ts, which carries the identicalisDirectCliInvocationshape and could regress identically. The shipped vendor drop was re-vendored so the promise made at.claude/skills/pipeline/SKILL.md:433reaches a consumer.CLAUDE.md:1's pre-dispatch section names the script but enumerates no verdicts, so it needed no edit — surfaced here rather than silently skipped.Out of scope (surfaced, not actioned)
CLAUDE.md's «Pre-dispatch in-flight probe» section could gain a sentence about claims. It enumerates no verdicts today so nothing there is stale, and CLAUDE.md is outside this stage's scope — flagging rather than drive-by editing (PR-strategy incident 2026-05-11, chore(pr-template): add §1.7 forward+backward stubs + Skipped guidance #33)..claude/skills/pipeline/SKILL.md; scopes were kept disjoint (mine is §6 Step 3, theirs is the frontier derivation). No overlap in this diff.