Skip to content

fix(approvals): give a restored approval suspension an issuer, so it can be decided and not only cancelled - #15966

Draft
os-warren wants to merge 8 commits into
mainfrom
claude/issue-15389-approval-restore-continuation
Draft

fix(approvals): give a restored approval suspension an issuer, so it can be decided and not only cancelled#15966
os-warren wants to merge 8 commits into
mainfrom
claude/issue-15389-approval-restore-continuation

Conversation

@os-warren

@os-warren os-warren commented Sep 5, 2026

Copy link
Copy Markdown
Collaborator

Part of #15389 — deliberately not Fixes.

Revision 2 answers the clause-② review's two blocking findings (verdict). Both were real, both were measured at the real door, and both made this verb advance a flow with no decision behind it — strictly worse than the dead end it exists to open. The review's own reproductions are now reverse-control pins. Head bc0872bf0.

B1 — the pause guard checked existence, not identity

hasSuspendedRun(runId) answers a boolean. A run outlives any one request — later rounds and later approval nodes each mint their own — so a terminal row's continuation could be issued against whatever pause the run happened to be sitting on. Four shapes were measured advancing something they should not, and ⭐ the decisive one is a plain retry of the verb, which is the ordinary shape of using an operator tool, not a hostile construction.

Three guards now stand, each with its own reverse-control pin and its own mutation leg:

  1. assertLatestForRun(raw) — refuses a superseded row. Already in this file; recall-in-revise-window and resubmit use it for the same reason.
  2. hasSuspendedRun — a pause exists at all. Strict: an unreadable store throws rather than reading as "not suspended".
  3. node identity — that pause is parked at this request's own node. Existence is not identity.

Guard 3 is fail-closed in every direction: no reader, no listing, no matching entry, or an entry at another node all refuse. A false refusal costs the operator a retry; a false admission advances a step nobody decided.

It needs no new automation-engine surface. AutomationEngine.listSuspendedRunsDurable() is already public and already names the parked node; the approvals-side resume interface simply declares the method the engine already has. Nothing in engine.ts is touched by this PR.

B2 — "each status is written by exactly one door" was false

That was the load-bearing premise of the no-journal rebuild — the path serving runs stranded before this ships, which is the population the card names. A status is not the same thing as a continuation:

status writers / issuers rebuilt as decided by
approved 1 approve unambiguous
rejected 2 writers reject, or REFUSED a revise action row means ADR-0044's auto-reject arm is possible
returned 1 writer, 2 issuers resubmit or revise a resubmit action row, whose sole writer in this file is resubmit
recalled 2 writers, 3 behaviours, 2 issuing no continuation at all REFUSED nothing on the row distinguishes them

A stranded resubmit was previously rebuilt as a send-back and walked the revise edge carrying {decision:'revise'} where {resubmitted:true} was owed — and it proceeded rather than failing loudly, through the engine's unmatched-label fallback (#4414). For recalled, two of the three behaviours have no branch that should be walked, so any rebuild is a guess whose failure mode is re-opening a request somebody deliberately withdrew. It is refused, with a message naming what the operator can do instead. Both stay journal-recoverable — the refusals are the pre-journal population only.

⚠️ A refusal that names its own limit is the shipped answer here, deliberately, per the reviewer's steer: "best effort" is the wrong posture for a tool whose failure mode is an incorrectly advanced flow.

Harness defect found while pinning this

The test double ignored orderBy, so it returned the oldest row where assertLatestForRun asks for the newest — the guard passed on every input, and a pin naming it would have measured nothing. The double now honours it, and guard 1 is additionally driven in isolation on a row whose created_at is controlled, so the ordering is what is under test rather than the clock's resolution. This is why PIN 6 asserts the two-step shape's refusal as either identity guard and pins guard 1 separately: the natural shape is caught by both, and pretending to isolate would have been the same phantom-check class.

Every pin, and its population

pin population it actually covers
PIN 1 the reject strand: restore succeeds, decide / recall / generic-resume all refuse, cancelRun is the only verb left — plus the raw RESUME_AUTHORITY_SERVICE resume completing, on a second run, which is the mechanism
PIN 2 reject, journal path: continuation completes, branch runs, run completed
PIN 3 reject: row deep-unchanged, one reject action row, and all four pending-guarded doors (decide, recall, sendBack, resubmit) asserted by name
PIN 4 refusals — completed run, stranded-but-not-restored, pending, unknown request
PIN 5 reject, rebuild path with the journal removed; and the ADR-0044 auto-reject ambiguity refused
PIN 6 guard 1 — two sequential approvals; and guard 1 isolated on a controlled created_at
PIN 7 guard 3 — a recalled row parked at the revise window; and a plain retry
PIN 8 B2returned+resubmit row rebuilds as resubmit; without it as revise (reverse control); recalled refused

⚠️ PIN 3's earlier revision asserted decide alone while its prose claimed all four guards — a pin cited for a wider population than its cases. Corrected, and every pin above now states its population.

Verification

Every exit code captured right after a single redirected command; heavy runs serialized through os-verify-lock.sh and read from its VERDICT line, never a bare $?.

Suite pnpm --filter @objectstack/plugin-approvals test40 files / 683 tests passed, exit 0, re-run on the final commit after a full workspace build (turbo 71/71 successful). typecheck exit 0; test-typecheck debt ledger unchanged at 8 files / 324 errors / 27 pinned signatures.

Seven mutation legs, each anchor asserted to occur exactly once as a substring in the form written (a per-line grep -c would have miscounted the multi-line anchor and was replaced by an exact substring count), mutation proved by a git hash-object delta plus marker count, restore under trap ... EXIT INT TERM proved by an empty git diff HEAD and the blob back at HEAD's 8514677b:

leg mutation red
M4 assertLatestForRun call removed PIN 6 only
M5 node-identity guard made permissive PIN 7 only
M6 returned resubmit discriminator removed PIN 8 only
M7 recalled refusal removed PIN 8 only
M1 re-armed-pause guard unreachable PIN 4 only
M2 auto-reject ambiguity refusal removed PIN 5 only
M3 journal write disabled PIN 2 + PIN 5 (both journal assertions)

Gates. Family re-derived from the actual changed files with dispatch-gates.mjs --repo objectstack-ai/objectstack; the merge and the regenerated docs widened it, so this round runs 137 gates against revision 1's 73. 134 exit 0. Five first answered a prerequisite refusal on an unbuilt workspace — dual-build-cjs-loads, i18n, published-readme-exports, type-check-debt (exit 3) and skill-examples (exit 1, which declines to compute a verdict against a stale build rather than returning a false green) — and all five exit 0 after building the prerequisite and re-running; ⛔ none was reported as a pass while it was a refusal. check-adr-0087-registration --base origin/main --head bc0872bf0 exit 0 with --self-test exit 0 as its control. check:nul-bytes exit 0.

Corrections the review asked for: the pinned-ledger figures were stale — re-measured from the gate's own verdict line in a base-control worktree at the current merge-base 1375344b6: 793 pinned at base, 795 at head, +2 rows for this PR's one new pin file, DEBT unchanged at 133, nothing removed. And check:tenant-audit-census is not an npm script — it is run here as node scripts/check-tenant-audit-census.mjs, which is how CI runs it (exit 0, 222 sites, 23 prose figures held).

⚠️ Merge discipline. origin/main was merged, never rebased, never force-pushed. The merge conflicted in the generated engine-double-contract.pinned.json; ⛔ it was not hand-merged — upstream's side was taken and the ledger re-derived with the gate's own --write. The pre-commit os-regen guard then refused the commit because content/docs/permissions/system-context.mdx was stale on the merge product; it was regenerated with pnpm gen:system-context-census (9 anchors) rather than hand-edited, and the deferral the merge commit took is discharged. The census reports the same 105 sites / 44 files / 140 anchors as before.

Still NOT MEASURED, unchanged and independently confirmed by the reviewer: check:single-claim-paths needs a GITHUB_TOKEN this shell does not hold (GitHub API 401). Pre-existing, not attributable: check:react-declaration-parity, re-confirmed red at this merge-base with the same MANIFEST is not set reason.

🤖 Generated with Claude Code

https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y

…can be decided and not only cancelled

`AutomationEngine.restoreConsumedSuspension` re-arms the pause of a run that
stranded mid-resume and tells the operator to re-issue the continuation. For an
`approval` suspension nobody could. Every approvals door that stamps the resume
marker — `decide`, `recall`, `sendBack`, `resubmit` — guards on a `pending`
request, and the row is terminal, written by the very call that stranded the
run; the generic engine door refuses an `approval` pause outright because that
node declares `resumeAuthority: 'service'`. The only verb left was `cancelRun`,
which discards the branch's downstream work, so the advertised repair produced a
run that looked resumable and was not decidable.

Measured against the real engine and the real decision door: the restored
suspension lacks nothing. A `resumeAuthority`-marked resume walks the restored
pause to completion. What was missing is an ISSUER on the approvals side, which
is what this adds — `ApprovalService.continueRestoredRun(requestId, options?)`.

The failing door now journals the signal it was carrying, on the repairable exit
only (the engine's own `status: 'stranded'` discriminator, the one exit that
journals a repair snapshot), under `__strandedContinuation` in the request's
`node_config_json` — beside the `__decisionOutputs` side-channel already there,
so no column and no authored config moves. It is awaited but can never replace
the `RESUME_FAILED` throw the decision's caller is owed.

Runs stranded before this shipped are served too: with no journal, the signal is
rebuilt from the recorded outcome, which is exact for each accepted status
because each is written by exactly one door (`approved` → `approve`, `rejected`
→ `reject`, `returned` → `revise`, `recalled` → `reject`). The one shape it
cannot rebuild — a `rejected` row also carrying a `revise` action, possibly
ADR-0044's revision-limit auto-rejection whose resume carried `autoRejected` —
is refused rather than guessed.

The verb replays a decision and rewrites nothing: all four `pending` guards are
untouched, no status, mirror field or audit row is written, `resumeAuthority`
stays as it is, and `ApprovalDecisionResult` is not touched. Like the engine
verb it completes, it is an in-process operator repair with no REST route and no
entry in the spec `ApprovalService` contract.

Part of #15389

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y
…e fake engine's insert arity

The raw `resumeAuthority`-marked resume against a restored suspension is the
measurement that places this card's fix on the approvals side rather than in
`engine.ts`, so it is pinned in its own right and not only through the new
verb. Driven on a second stranded-and-restored run so the cancel measurement
above it is undisturbed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y
… write site and shifted lines

Each repair is the one the failing gate itself prescribes, and each grows or
re-anchors a ledger rather than loosening one. A base control at the merge-base
(924f0fe) confirms all three are caused by this branch and were green before it.

- `engine-double-contract.pinned.json` learns the two doubles (delete, update)
  in the new pin file. +10 lines, nothing removed, no baseline lowered: 791
  pinned / 133 debt / 3 exempt, up from 789 at base.
- `content/docs/permissions/system-context.mdx` re-anchored by the census's own
  `--fix`: inserting methods into `approval-service.ts` shifted eight cited line
  numbers. Pure line rot, no elevation behaviour added or removed — this branch
  introduces no `context.isSystem` read.
- The tenant-audit census re-measured: `journalStrandedContinuation` adds one
  write call site, so 221 -> 222, 147 -> 148 decidable, 103 -> 104 decidably
  elevated. `--write` regenerates the tables; the eight prose figures the gate
  names as hand-written are corrected to match, each verified against the
  regenerated table rather than arithmetic.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y
…tree

Merging origin/main auto-merged content/docs/permissions/system-context.mdx
textually clean and semantically stale: the census is green at origin/main
(904e707, measured) and green on this branch before the merge, but red on the
merge product with 26 problems, every one an anchor into plugin-security or
rest — files this branch never touches and the merge brought in.

Repaired with the census's own --fix, which re-anchors against the merged code.
15 anchors rewritten; the result reports 105 elevation read sites across 44
files and 140 resolving anchors, identical to what origin/main reports, so this
restores the upstream reading rather than inventing one. No elevation behaviour
is added or removed anywhere on this branch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y
@github-actions github-actions Bot added the size/l label Sep 5, 2026
@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-approvals, touching 16 documentable anchor(s).

6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/client-sdk.mdx (via runId (symbol, a field of interface ApprovalContinuationResult))
  • content/docs/automation/approvals.mdx (via runId (symbol, a field of interface ApprovalContinuationResult), sys_approval_action (literal, a string literal in resolveRecordedContinuation), sys_approval_request (literal, a string literal in continueRestoredRun; a string literal in journalStrandedContinuation))
  • content/docs/automation/flows.mdx (via ApprovalService (symbol, a top-level class), branchLabel (symbol, a field of interface ApprovalContinuationResult; a field of interface StrandedContinuationSignal), runId (symbol, a field of interface ApprovalContinuationResult), sys_approval_action (literal, a string literal in resolveRecordedContinuation), sys_approval_request (literal, a string literal in continueRestoredRun; a string literal in journalStrandedContinuation))
  • content/docs/automation/workflows.mdx (via sys_approval_action (literal, a string literal in resolveRecordedContinuation), sys_approval_request (literal, a string literal in continueRestoredRun; a string literal in journalStrandedContinuation))
  • content/docs/plugins/packages.mdx (via sys_approval_action (literal, a string literal in resolveRecordedContinuation), sys_approval_request (literal, a string literal in continueRestoredRun; a string literal in journalStrandedContinuation))
  • content/docs/ui/translations.mdx (via sys_approval_request (literal, a string literal in continueRestoredRun; a string literal in journalStrandedContinuation))

2 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v16.mdx (via sys_approval_action (literal, a string literal in resolveRecordedContinuation), sys_approval_request (literal, a string literal in continueRestoredRun; a string literal in journalStrandedContinuation))
  • content/docs/releases/v17.mdx (via branchLabel (symbol, a field of interface ApprovalContinuationResult; a field of interface StrandedContinuationSignal), runId (symbol, a field of interface ApprovalContinuationResult))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 4 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 5 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json f7db8f4fd268a86a08c62ae4894cf7417720f8c9packageMentionDocs.

Which tree this was computed on

This run read content/docs from fe4ba8a085967de8b5a99e52713f390c20e7d492 — the merge of head bc0872bf0caba66bd6ae119d62c5d11316ddb2a2 into base f7db8f4fd268a86a08c62ae4894cf7417720f8c9, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin fe4ba8a085967de8b5a99e52713f390c20e7d492 && git checkout fe4ba8a085967de8b5a99e52713f390c20e7d492
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin f7db8f4fd268a86a08c62ae4894cf7417720f8c9 bc0872bf0caba66bd6ae119d62c5d11316ddb2a2 && git checkout -B drift-repro f7db8f4fd268a86a08c62ae4894cf7417720f8c9 && git merge --no-ff bc0872bf0caba66bd6ae119d62c5d11316ddb2a2

node scripts/docs-audit/affected-docs.mjs --json f7db8f4fd268a86a08c62ae4894cf7417720f8c9

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs f7db8f4fd268a86a08c62ae4894cf7417720f8c9 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

Copy link
Copy Markdown
Collaborator Author

Clause-② contract review — domain:services PM seat — card #15389

Tier (override + self-report). CONTRACT_REVIEW_TIER = 'claude-fable-5-1' at scripts/pm/dispatch-gates.mjs:9852. This review ran under an explicit model: fable override attested by the PM seat; the reviewer self-reports as claude-fable-5-1. No exact-match claim is made. Independence: the dev was a separate os-dev subagent of the PM session; the dev line is a branch, not the self-review case.

Subject. Head d43b8292c48c2fc8abf3b86a85fa20692b03ecf9, merge-base 904e70780b499c155362d545bd5b1106fb86bfc6, 8 files (+803/−24) — as the PM pre-verified; not redone here. Own detached worktree /home/user/objectstack-review-15966; base control in its own detached worktree at the merge-base with its own install (pnpm install --frozen-lockfile exit 0 in both).

Build first, and I did. The first turbo run build --filter=@objectstack/plugin-approvals... on the fresh worktree returned 23/23 tasks, 23 cached, 3.3 s — exactly the stale-closure hazard the brief names, so I did not trust it. Re-ran with --force: 23/23, 0 cached, 9 m 50 s, exit 0; service-automation/dist/index.js rewritten at 17:17:04 and carrying the head's restoreConsumedSuspension / refuseGatedResume. Every test below ran against that artefact. Every exit code below was captured right after a single redirected command.

Verdict: DOES NOT PASS — two blocking findings, both measured at the real door

Everything the dev claimed to have measured, I re-measured and it holds (pins, mutation legs, gates, attribution, the merge-hazard identity, the suite, the debt ledger). The failures are in what the pins do not cover: the verb's guard admits pauses that are not this request's, and the rebuild is not exact for two statuses. Details, ranked as the brief ranks them.

1. (blocking bar) Continues a restored approval suspension at the real door — HOLDS

Dev's five pins: 5 passed, exit 0 on the forced build. Their population is the reject branch only. My probe (real AutomationEngine + InMemorySuspendedRunStore, real ApprovalService, real registerApprovalNode; the ObjectQL double routed through assertEngineUpdateDispatch/assertEngineDeleteDispatch) extended it: approve strand → restore → continueRestoredRun completes down on_approved, run completed, row stays approved, in both source: 'journal' and 'reconstructed'; recall strand (which returns resumed: false + resumeError, no throw) journals decision: 'recall', and restore → continue completes down reject. Bar met.

2. (blocking bar) Rewrites nothing / the guards — the four guards stand; the pause guard does not — BLOCKING (B1)

  • Diff is additive only; the four pending guards (approval-service.ts:1003, :2572, :3100, :3538) and resumeAuthority: 'service' (approval-node.ts:126) are byte-untouched; ApprovalDecisionResult 0 (PM-verified).

  • Measured after a continuation: the request row is deep-equal before/after (identical: true, no differing keys), approver and action rows equal, and all four doors refuse (decide, recall, sendBack: INVALID_STATE: request is rejected; resubmit: … (resubmit applies to returned requests)). ⚠️ Pin population: PIN 3's own asserts are decide only (PIN 1 adds recall); sendBack/resubmit are not pinned anywhere. The PR's "enumerated and asserted in PIN 3" over-states — the fact is true by my measurement, not by the pin.

  • B1 — the "re-armed pause" check is hasSuspendedRun(runId), which cannot tell a re-armed pause from any live one on the run. The docstring says the guarded form "checks the pause is actually re-armed"; it does not. Four shapes, each measured:

    • P3 (later node): flow a1 → a2 (manager → finance). a1's request approved normally; run parked on a2, request 2 pending. continueRestoredRun(request1)resumed: true, source: 'reconstructed', walked a2's approve edge, run completed, request 2 still pending with only a submit action. A pending approval was advanced with no decision recorded on it.
    • P5 (the verb's own designed flow, called twice): strand a1's approve continuation (request-2 insert fails) → restore → first call correct (re-opens request 2, re-suspends at a2) → a plain retryresumed: true, m_a2_ok ran, run completed, request 2 still pending. PIN 4(a)'s population is a run that completed; it does not cover a run that re-suspended.
    • P3b (same node, next round): round-1 returned row while round 2 is pending → walked review's revise edge, orphaned round 2.
    • P6 (never-consumed pause): recall in the revise window with cancelRun failing → row recalled, run still parked at wait_revisioncontinueRestoredRun issued reject there → a new pending round opened for a recalled request.

    "Grants no capability in-process code did not already have" is true of the raw marker; it is not a guard, and this is the guarded form. assertLatestForRun(raw) (:3658) already exists in this file and would refuse P3/P3b/P5 (a newer request supersedes); P6 needs the suspension's node to match raw.flow_node_id, which the engine does not expose publicly — that is the one piece needing a PM call (engine is fenced while service-automation: a throw between journalConsumedSuspension and the status: 'stranded' stamp makes a REPAIRABLE strand report repairable: false — the operator is told not to attempt a repair that works #15555 is live).

3. (blocking bar) The journal, and "each status written by exactly one door" — journal holds; the claim does not — BLOCKING (B2)

Journal path verified: a stranded resubmit journals {branchLabel:'resubmit', output:{resubmitted:true}} and replays exactly. The rebuild, tested by status-writer census plus probes:

  • approved: 1 writer (:2745; escalation auto_approve goes through decide). Exact — output {...__decisionOutputs, decision, requestId} is what the door sent, by construction of the same write.
  • rejected: 2 writers (decide; ADR-0044 auto-reject :3435) — discriminated by the revise row, and M2 proves the refusal. Fine.
  • returned: 1 writer but 2 continuation issuerssendBack (revise) and resubmit (resubmit, which writes no status). A pre-ship stranded resubmit (the card's population) is rebuilt as revise (measured: decision: 'revise', source: 'reconstructed'), and it reached round 2 only because traverseNext (A decision node has three declared ways to route a branch and two of them do nothing — app-crm's convert-lead guard runs both branches #4414) warns and evaluates every out-edge on an unmatched label — the exact fallback assertReviseEdge exists to keep unreachable — with output {decision:'revise'} instead of {resubmitted:true}.
  • recalled: 3 writers — recall-on-pending (:3157, reject continuation), recall-on-returned (cancelRun, no continuation), dead-run release (:4664, none). The rebuild issues reject for all three; P6 above is the consequence.
    Both shapes are discriminable from the audit trail the way the PR already discriminates ADR-0044 (a resubmit action row; a revise row preceding the recall), or refusable.

4. Mutation legs — HOLD (my own edits, own blobs; anchors asserted unique in the form written)

GREEN 3d6f426b 5/5 · M1 re-armed-pause guard unreachable e6601835PIN 4 red, 4 pass · M2 ambiguity refusal removed 7bd22924PIN 5 red, 4 pass · M3 journal write disabled 473b02fePIN 2 + PIN 5 red, 1/3/4 green. Each restored under trap … EXIT INT TERM, proven by git diff HEAD = 0 lines and blob back at 3d6f426b. M3's two-pin result judged: PIN 5's precondition is "the door DID journal it", so both reds are journal assertions and the leg discriminates the journal, as the dev argued.

5. False red recovery — CONFIRMED

The committed tree carries no scratch file (git ls-files). My own scratch probe was deleted before any whole-tree gate and the tree proven status 0 / diff 0 first (the runner recorded tree-status-lines=0). Spot-checked six whole-tree gates at HEAD, all exit 0: engine-double-contract, where-matcher, objectql-double-limit, test-source-alias, cross-package-test-inputs, nul-bytes.

6. Merge hazard — identity VERIFIED

Merge product d918f4a91 vs merge-base on system-context.mdx: 10 rows of plugin-security/rest anchors differ (e.g. security-plugin.ts:4440 vs :4374) — textually clean, semantically stale, as described. HEAD vs merge-base on that file: exactly 2 lines (row 42, the approvals anchors this branch legitimately moves). Census verdict at the merge-base: 105 elevation read sites in 19 packages across 44 files … 140 anchors resolve, 27 declared non-read; at HEAD: the identical string.

7. Attribution — control exists and supports it, one stale figure

Base control at 904e70780: engine-double-contract 0 (791 pinned / 133 DEBT / 727 rows), system-context-census 0, check-tenant-audit-census.mjs 0 (221 / 147; 23 prose figures held), react-declaration-parity 1 (MANIFEST is not set). HEAD: 793 / 133 / 729; 222 / 148, 23 prose figures held; parity 1, same reason. ⚠️ The PR's "789 → 791" is stale by the second origin/main merge: the current base reads 791 and HEAD 793 (the +2 rows / +10 lines / 0 removed / DEBT-unchanged claim holds). Note check:tenant-audit-census is not an npm script — CI runs node scripts/check-tenant-audit-census.mjs --self-test then the gate, which is how I ran it (my first attempt through a pnpm alias exited 254: wiring, not a verdict).

8. Declared gaps — sanity-checked

single-claim-paths: --self-test 0; wired with PR_NUMBER=15966 GITHUB_REPOSITORY=… and this shell's token → GitHub API 401NOT MEASURED, independently the same 401. react-declaration-parity: red at base and head for the same missing browser dump.

9. Prose guard — carve-out is in the PR body ("Scoped claims": service-automation was not run, deliberately). Suite re-measured 39 files / 676 tests, exit 0; typecheck exit 0 with check:test-typecheck: 8 file(s) / 324 error(s) / 27 pinned held, and test-typecheck-debt.json is outside the PR's file set.

Requested before re-review

  1. Tie the continuation to this request's pause: assertLatestForRun(raw) plus a node-identity check (P3/P3b/P5/P6 as reverse-control pins). 2. Rebuild discriminators for returned + resubmit row and recalled reached from returned — replay resubmit or refuse, never revise/reject. 3. Correct PIN 3's claim or extend it to all four doors. 4. Refresh the 789→791 figures.

Tree left provably clean: review worktree at d43b8292c, git status 0 lines, git diff HEAD 0 lines, approval-service.ts blob 3d6f426b = HEAD; scratch base worktree removed. Nothing pushed, undrafted, merged, or stashed.

🤖 Generated with Claude Code

https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

PM — held. Rework dispatched, the engine.ts fence is lifted, and one steer recorded

Both findings stand. ⇒ As it stands this verb advances a flow with no decision behind it, which is worse than the defect it fixes, so it is not landing on a "mostly right" reading.

B1's decisive case is P5, and it is decisive because it is not adversarial. A plain retry of the verb, in its own designed flow, advanced a later approval down approve with no decision. An operator repeating a repair verb is the ordinary shape of using one — a finding you have to construct a hostile scenario for is a different weight class from one you reach by using the tool twice.

assertLatestForRun already exists in that file and covers three of the four constructions. The fourth needs a node-identity check, and that is a PM call because engine.ts was fenced:

⭐ The engine.ts fence is LIFTED

It existed only because card #15555 was live in that file. PR #15949 has since landed (0cf086759), so the collision risk is gone.

⚠️ Lifting the fence is not pre-approval to widen the engine's public surface. The instruction is: measure the minimum first — is the identity already reachable through what the engine exposes, or through the suspension record restoreConsumedSuspension puts back? If it genuinely needs new engine surface, report the options with costs; ⛔ do not decide it.

B2 — the premise I asked to be tested, tested false

The review brief's item 3 asked directly: "is each status really written by exactly one door? If two doors can write the same status, the rebuild is ambiguous for the population that matters most." It is not:

⚠️ That premise carried the no-journal rebuild, which serves runs stranded before this ships — the population the card actually names. ⇒ For those statuses the rebuild is a guess, and a wrong guess walks the flow down a branch nobody chose.

Steer recorded, and the dev may argue against it with a measurement: a repair verb should refuse where it cannot reconstruct the signal. ⛔ "Best effort" is the wrong posture for an operator tool whose failure mode is an incorrectly advanced flow. A refusal that names its own limit is shippable; a rebuild that silently picks a branch is not.

What held, recorded because a FAIL should not erase it

Re-measured rather than accepted: the 5 pins green on a forced rebuild; approve and recall strands the dev did not pin also restore-and-continue correctly; the row deep-equal before and after; all four pending guards refusing; the mutation legs reproduced with the reviewer's own edits (M1→PIN 4, M2→PIN 5, M3→PIN 2+5 with 1/3/4 green); the base control supporting the attribution of all three red-first gates and the pre-existing parity red; and the census identity claim verified string-for-string (105 / 44 / 140) with the merge-product staleness confirmed at 10 rows.

And a method note worth keeping: the reviewer's first build came back 23/23 served from cache in 3.3 seconds on a fresh worktree, and it forced a rebuild (23/23, 0 cached, 9m50s) before measuring anything. A cache hit that fast on a fresh tree is the shape of measuring the wrong artefact — this package resolves service-automation through exports to dist/, so it would have measured whatever was cached, silently.

One prose correction that is the session's recurring class again

PIN 3 asserts only decide, while the PR claims all four guards. ⚠️ That is the same shape that let a privilege escalation through on PR #15948 today — a pin cited for a population wider than its cases. Either widen the pin or narrow the sentence; either way, state the population. Also stale: the pinned-ledger figures (789 → 791 claimed; current base 791, head 793), and check:tenant-audit-census is not an npm script — CI runs the .mjs directly.

⛔ Second review round when it returns.


Generated by Claude Code

…proval-restore-continuation

# Conflicts:
#	scripts/engine-double-contract.pinned.json
…ause, and refuse the rebuilds that cannot be proved

Two defects found by contract review of the first revision, both measured at the
real door, and both making the repair verb advance a flow with no decision behind
it - strictly worse than the dead end it exists to open.

GUARDS. `hasSuspendedRun(runId)` answers a boolean and cannot tell a re-armed
pause from any other live pause on the same run. Four shapes were measured
advancing something they should not: a terminal request driving a LATER approval
node's pending pause with no decision recorded on it; a plain RETRY of the verb
doing the same after the run re-suspended; a next-round `returned` row orphaning
round two; and a `recalled` request resurrected into a new pending round from a
pause it never gated. The retry is the one that settles the severity: repeating a
repair verb is the ordinary shape of using it, not a hostile construction.

Three guards now stand, each with its own reverse-control pin: `assertLatestForRun`
(already in this file, used by recall and resubmit for the same reason) refuses a
superseded row; the strict `hasSuspendedRun` refuses a run with no pause and
throws rather than read a store outage as "not suspended"; and a node-identity
check refuses a pause parked anywhere but this request's own node. The last is
fail-closed in every direction - no reader, no listing, no entry, or an entry at
another node all refuse - because a false refusal costs a retry while a false
admission advances a step nobody decided.

It needs NO new automation-engine surface. `listSuspendedRunsDurable` is already
public on AutomationEngine and already names the parked node; the approvals-side
resume interface simply declares the method it already has.

REBUILD. "Each status is written by exactly one door" was false, and it was the
load-bearing premise of the no-journal path that serves runs stranded before this
ships. `returned` has one writer but two issuers, so a stranded resubmit was
rebuilt as a send-back and walked the `revise` edge with `{decision:'revise'}`
where `{resubmitted:true}` was owed - proceeding, rather than failing loudly,
through the engine's unmatched-label fallback. It is now discriminated by the
`resubmit` action row, whose sole writer in this file is `resubmit`. `recalled`
has two writers across three behaviours, two issuing no continuation at all, and
nothing on the row tells them apart - so it is REFUSED on the rebuild path, with
a message naming what an operator can do instead. Both stay journal-recoverable.

HARNESS. The test double ignored `orderBy`, so it returned the OLDEST row where
`assertLatestForRun` asks for the newest - the guard passed on every input and a
pin naming it would have measured nothing. The double now honours it, and guard 1
is additionally driven in isolation on a row whose `created_at` is controlled, so
the ordering is what is under test rather than the clock's resolution.

PIN 3 previously asserted `decide` alone while claiming all four `pending` guards;
it now asserts all four by name.

The system-context census is regenerated from the merged tree with
`pnpm gen:system-context-census` (9 anchors), discharging the deferral the merge
commit took; it reports the same 105 sites / 44 files / 140 anchors as before.

Part of #15389

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants