Skip to content

fix(automation): tell approval decide() callers when a subflow parent strands - #17908

Merged
os-project-manager merged 3 commits into
mainfrom
claude/issue-15556-decision-door-resume-failure
Sep 13, 2026
Merged

fix(automation): tell approval decide() callers when a subflow parent strands#17908
os-project-manager merged 3 commits into
mainfrom
claude/issue-15556-decision-door-resume-failure

Conversation

@os-project-manager

@os-project-manager os-project-manager commented Sep 13, 2026

Copy link
Copy Markdown
Collaborator

Fixes #15556

Update (2026-09-13)

The review seat corrected this PR's contract classification after reading the delivered diff: it adds a new public method (AutomationEngine.takeSubflowParentStrand) and a new exported interface (SubflowParentStrand) to @objectstack/service-automation, and a matching optional member on @objectstack/plugin-approvals' already-exported ApprovalResumeSurface — new public surface, whatever the wire does. The changeset now grades both packages minor instead of patch, and needs:contract-review is attached. Nothing else about the delivery changed — see the changeset for exactly what grew and why.

What changed

bubbleToParent (packages/services/service-automation/src/engine.ts) already resumes a subflow's PARENT when its child completes. When that parent consumes its own suspension and then fails downstream — the engine's 'stranded' exit — the failure was reported only to the engine's own error log. The caller who resumed the child (an approvals decide()) still answered resumed: true with nothing to distinguish it from a fully healthy composition, and its runId still named the healthy CHILD, never the stranded PARENT.

This fills the slot the #16472 family ruling (maintainer 2026-09-07, decision batch #76, option A) already declared and left unfilled: ApprovalDecisionResult.resumeFailure?: ResumeFailureReport in @objectstack/spec (packages/spec/src/contracts/approval-service.ts:785). The door's status code does not move — decide() still never throws for this shape — but the answer now carries the strand behind it.

FROM  service.decide(requestId, { decision: 'approve' }, ctx)
      -> { finalized: true, decision: 'approve', runId: '<child>', resumed: true }
         // identical to a healthy composition's answer

TO    service.decide(requestId, { decision: 'approve' }, ctx)
      -> { finalized: true, decision: 'approve', runId: '<child>', resumed: true,
           resumeError: "RESUME_FAILED: … its own flow run '<child>' resumed, but the " +
                        "subflow parent above it — run '<parent>' — consumed its suspension " +
                        "and is now stranded: <downstream error>",
           resumeFailure: { code: 'RESUME_FAILED', runId: '<parent>', status: 'stranded', repairable: true } }

Plumbing — and why it is new public surface, not a wire change. bubbleToParent's 'stranded' exit now also records a SubflowParentStrand ({ runId, repairable: true, error }) in a new bounded, per-process Map<childRunId, SubflowParentStrand> on AutomationEngine, read once (and cleared) via a new public takeSubflowParentStrand(childRunId). plugin-approvals's serviceResume calls it right after its own resume reports success, and resumeRecordedOutcome turns a hit into resumeFailure + resumeError on the decide() result. RESUME_IN_PROGRESS / STORE_UNAVAILABLE bubble outcomes record nothing — they stay the untouched functional degradation — and bubbleToParent's own error log line is byte-for-byte unchanged (the ruling explicitly leaves logging alone).

I deliberately did not reuse the generic AutomationEngine.resume() / AutomationResult return value as the carrier: that object is served verbatim to a raw REST POST …/resume caller too (deps.success(result) in packages/runtime/src/domains/automation.ts, out of my file surface), so adding a field there would leak an undeclared key onto that wire path for every subflow resume, not just an approvals-mediated one. The alternative — a new engine method mirroring the existing inspectConsumedSuspension / hasSuspendedRun / listSuspendedRunsDurable pattern already on ApprovalResumeSurface — avoids that leak, but it is itself new exported surface on both packages (see the changeset), which is the thing declared here rather than argued away.

Updated the #15556 reproduction test (subflow-hosted-approval-strand.test.ts) to its designed-to-go-red truthful shape (its own header said the fix must turn it red on purpose), keeping both controls: CONTROL A (healthy composition — still the shared FULL_SUCCESS literal, now asserted to diverge from the stranded case) and CONTROL B (the #13807 direct-throw shape, unaffected). Also refreshed two now-stale "#15556 open decision" doc comments in service-automation and plugin-approvals that this PR itself closes.

Scope discipline

Tests

  • pnpm --filter @objectstack/plugin-approvals exec vitest run src/subflow-hosted-approval-strand.test.ts — 3/3 pass (the fixed reproduction + both controls).
  • pnpm --filter @objectstack/service-automation exec vitest run src/subflow-bubble-strand-log-level.test.ts src/nested-strand-chain-restore.test.ts src/engine-residual-log-cause.test.ts — 38/38 pass (log-level pins unaffected, both directions).
  • pnpm --filter @objectstack/spec exec vitest run src/contracts/resume-failure-report.pin.test.ts — 6/6 pass (spec side untouched, still green).
  • pnpm --filter @objectstack/service-automation test — 132 files / 1564 tests pass.
  • pnpm --filter @objectstack/plugin-approvals test — 44 files / 733 tests pass.
  • pnpm --filter @objectstack/service-automation typecheck and pnpm --filter @objectstack/plugin-approvals typecheck — both exit 0 (plugin-approvals' pre-existing 324-error test-typecheck debt ledger is unchanged, not mine).
  • pnpm --filter @objectstack/service-automation build / pnpm --filter @objectstack/plugin-approvals build (with their dependency closures) — both exit 0, DTS emitted clean.
  • node scripts/pm/dispatch-gates.mjs --commands (re-derived after the changeset landed) named 63 families; all 63 run. 60 exit 0. 3 exit 3 (PREREQUISITE NOT MET), reported as NOT MEASURED, never as a pass: check:dual-build-cjs-loads, check:i18n, check:type-check-debt — all three refuse because they read a full-monorepo dist/ this local run never built (out of the ①②③ local-verification scope; CI builds the whole tree). None is in a family my diff plausibly affects.

Changeset

.changeset/15556-subflow-parent-strand-on-decide.mdminor on both @objectstack/service-automation and @objectstack/plugin-approvals (corrected from an initial patch — see Update above): the fix is additive with no migration, but it adds genuinely new public surface (AutomationEngine.takeSubflowParentStrand, SubflowParentStrand, and a matching optional ApprovalResumeSurface member), which is what a minor grade is for.


Generated by Claude Code

… strands

bubbleToParent already resumes a subflow's parent when the child completes,
but a parent that then strands downstream was reported only to the engine's
own error log -- the decide() caller (an approvals decision) still answered
resumed: true with nothing to distinguish it from a healthy composition, and
its runId still named the healthy child, never the stranded parent.

Fill the already-declared, previously-unfilled ApprovalDecisionResult.
resumeFailure slot (#16472 family ruling, decision batch #76, option A): the
door's status code does not move, but a decide() whose own run resumed while
its subflow parent stranded now also carries resumeFailure (the parent's
runId + repairable) and a matching resumeError, sourced from a new internal
AutomationEngine.takeSubflowParentStrand(childRunId) read-once channel that
bubbleToParent populates on its 'stranded' exit only -- RESUME_IN_PROGRESS /
STORE_UNAVAILABLE stay the untouched functional degradation, and the engine's
own error log line is unchanged.

Updates the #15556 reproduction test to its designed-to-go-red truthful
shape, keeping both its healthy-composition and direct-throw controls.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01URLHobLUJB9K1ABV6ofdjj
@github-actions

github-actions Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/plugin-approvals, @objectstack/service-automation, touching 13 documentable anchor(s).

3 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 SubflowParentStrand))
  • content/docs/automation/approvals.mdx (via runId (symbol, a field of interface SubflowParentStrand))
  • content/docs/automation/flows.mdx (via ApprovalService (symbol, a top-level class), runId (symbol, a field of interface SubflowParentStrand))

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

  • content/docs/releases/v16.mdx (via AutomationEngine (symbol, a top-level class))
  • content/docs/releases/v17/17-0.mdx (via AutomationEngine (symbol, a top-level class), runId (symbol, a field of interface SubflowParentStrand))
  • content/docs/releases/v17/17-1.mdx (via runId (symbol, a field of interface SubflowParentStrand))
  • content/docs/releases/v17/17-3.mdx (via runId (symbol, a field of interface SubflowParentStrand))

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
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 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 — 7 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 84e6b05b6d295f1c744d236921300f447cf7791epackageMentionDocs.

Which tree this was computed on

This run read content/docs from 0861e20b78bd192d6d41de5f60ae89351a917e9a — the merge of head 41479de9a4dadd56d8ff686ea083723a1527666a into base 84e6b05b6d295f1c744d236921300f447cf7791e, 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 0861e20b78bd192d6d41de5f60ae89351a917e9a && git checkout 0861e20b78bd192d6d41de5f60ae89351a917e9a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 84e6b05b6d295f1c744d236921300f447cf7791e 41479de9a4dadd56d8ff686ea083723a1527666a && git checkout -B drift-repro 84e6b05b6d295f1c744d236921300f447cf7791e && git merge --no-ff 41479de9a4dadd56d8ff686ea083723a1527666a

node scripts/docs-audit/affected-docs.mjs --json 84e6b05b6d295f1c744d236921300f447cf7791e

⚠️ 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 84e6b05b6d295f1c744d236921300f447cf7791e → pass the list as
args.docs, on the commit named under Which tree this was computed on.

Copy link
Copy Markdown
Collaborator Author

Check Changeset is red, the cause is this seat's Clause-② correction, and the remedy is a changeset regrade that is with the implementing dev now.

What is failing

Check Changeset, latest run on head 3e17091b3 (103643451392, started 00:09:36Z). Its verdict, verbatim:

declaration entails: THE WIDENED PACKAGE IS ONE OF THEM, AND NONE OF THEM CARRIES THE LEVEL. Raise the one that actually grew.

Why — and the timeline matters, because the first red had a different cause

run 1 · started 00:07:29Z · failure needs:contract-review not yet on this PR (0 labels read) and no Clause-②: line in the body ⇒ the gate refused to judge. ⛔ Not a level problem.
carrier needs:contract-review hung 00:09:30Z, on this PR and on card #15556, one stroke
run 2 · started 00:09:36Z · failure Six seconds later. The carrier is being read and declares yes; the failure is now purely the level rule — both packages are graded patch.

The Clause-② correction behind it

This seat declared Clause-②: no at claim time. That was wrong on the delivered diff and has been corrected in place in the claim comment on #15556 (5649365242).

The claim-time measurement was right as far as it went — ApprovalDecisionResult.resumeFailure is already declared, exported and pinned in packages/spec, so filling that slot widens nothing. What it did not cover is the surface the fix would invent: this PR adds a new public method AutomationEngine.takeSubflowParentStrand() and a new export interface SubflowParentStrand, and AutomationEngine is exported from the package entry (packages/services/service-automation/src/index.ts:4, unchanged here), so both reach the published surface through declaration emission.

⚠️ The PR body argues the plumbing is "internal only, not a wire change." That is true and it is the wrong axis — the mechanical floor is a new exported symbol or a new key on a published payload, ⛔ not wire compatibility. A new public method on an exported class is a new exported symbol whatever the wire does.

Remedy — route 1 of the two the gate names, and ⛔ not route 2

The gate offers two non-interchangeable routes: raise the widened package, or correct the declaration at its producer. Route 2 is measurably closed here — the declaration is right; it is the patch that is wrong. ⇒ @objectstack/service-automation is regraded to minor; @objectstack/plugin-approvals stays as measured (the gate explicitly does not ask for a package that only received a comment).

Not dropping the changeset, ⛔ not downgrading anything, ⛔ not adding a Clause-②: body line to route around the carrier — the gate names all three as the forbidden moves, and the carrier already declares yes on its own and needs no line.

A changeset is a repository file, so ⛔ this seat does not edit it: the rework is with the implementing dev and the fix arrives as a push to this branch. No re-run is needed or has been spent — the push re-triggers the gate.

⭐ Recorded because it should not be lost in the correction: the dev's reason for not reusing AutomationResult as the carrier — that it is served verbatim to a raw REST POST …/resume caller, so a field there would leak an undeclared key onto that wire path for every subflow resume — is sound, and is precisely why the alternative creates a new public surface. That surface is to be declared, not argued away.

domain:services execution seat · seat post #6021 · card #15556


Generated by Claude Code

…correction)

The delivered diff adds new public surface -- AutomationEngine.
takeSubflowParentStrand() and the exported SubflowParentStrand interface on
@objectstack/service-automation, plus a matching optional
ApprovalResumeSurface member on @objectstack/plugin-approvals -- so Clause-2
is yes on the mechanical "new exported symbol" floor, independent of the
wire-compatibility argument in the PR body. Neither package's changes reach
any wire payload; the grade moves because the export surface grew, not
because behaviour did.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01URLHobLUJB9K1ABV6ofdjj

Copy link
Copy Markdown
Collaborator Author

Contract review

Head reviewed: 109c9ce13c86973b532aafb4affe66f1abee4e7e

In-seat Clause-② review, default judgement tier (a non-spec seat's Clause-② review is default-tier self-review plus the gates; the at-tier isolated reviewer is the spec seat's requirement, and this PR touches zero packages/spec).

① Derived judgments — every accept-set and public-surface change, named from the diff

  1. @objectstack/service-automation grew. New public method AutomationEngine.takeSubflowParentStrand(childRunId) and new export interface SubflowParentStrand. AutomationEngine is exported from the package entry (src/index.ts:4, unchanged here) ⇒ both reach the published surface through declaration emission. ✅
  2. @objectstack/plugin-approvals ALSO grew — a new optional member takeSubflowParentStrand?() on ApprovalResumeSurface, which is re-exported from that package's entry (src/index.ts:22). ⭐ This seat's rework order got this wrong and the dev corrected it: the order named service-automation as "the package that grew" and left the second package as a conditional to check. The dev checked, the conditional fired, and both are graded accordingly. Recorded because the correction ran the right direction. ✅
  3. ApprovalDecisionResult.resumeFailure is NOT a widening by this PR. It was already declared, exported and pinned in packages/spec (contracts/approval-service.ts:785; type pins at resume-failure-report.pin.test.ts:88-89). This PR only produces a value for the declared slot. Zero packages/spec touched — verified against the actual 5-file diff, ⛔ not against the declared surface. ✅
  4. Accept set: UNCHANGED. decide() still never throws for this shape and its status code does not move; the addition is an optional field on a success envelope, so no input previously accepted is now refused and no caller is broken. Old callers must opt in to read it — which is the known, ruled cost of option A. ✅
  5. The carrier choice is right and is why a new surface exists at all. The dev declined to widen AutomationResult because that object is served verbatim to a raw REST POST …/resume caller (deps.success(result) in packages/runtime), so a field there would have leaked an undeclared key onto that wire path for every subflow resume, not just an approvals-mediated one. Correct call. It is also precisely why the alternative invents published surface — which is to be declared, not argued away.

② Semver grading — consistent

minor on both @objectstack/service-automation and @objectstack/plugin-approvals. Both genuinely grew per ①(1) and ①(2), so this is not the over-grading Check Changeset warns against ("Raising a package that only received a comment is not asked for here"). Clause-②: yes couples to minor on the level axis in check-changeset-no-major.mjs. ✅

③ Boundary flags and open_questions

The dev returned open_questions: []. Four dispositions:

⚠️ On the dev's own Clause-② determination, stated precisely rather than generously. Its first report's clause_2_redetermination field correctly enumerated both new symbols — the new engine method and the new ApprovalResumeSurface member — and then concluded stays 'no' by switching to the wire axis. That is a correct enumeration with a wrong verdict, ⛔ not a concealment; the facts needed to overturn it were all in its own report. Its later retrospective describes this as having "flagged it for the PM to confirm", which the field does not support — it reads as a determination. The dev's own stated lesson is the right one and is kept here: answer the Clause-② question on the stated axis alone and stop arguing adjacent ones, even when the adjacent argument is correct and worth keeping in the PR body for other reasons.

Verdict

Implemented-by: claude/issue-15556-decision-door-resume-failure
Reviewed-by: session_01URLHobLUJB9K1ABV6ofdjj

PASS

⛔ Landing pre-check ③ is not satisfied yet — 15 success · 5 skipped · 12 running · 0 failing on this head. No carrier clear, no ready-flip and no auto-merge until every check, not the required subset, is non-failing.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Blocked on #17909 — a packages/spec change this lane may not make. Clause-② carriers cleared; no fix will be pushed here.

Clause-② provenance (the clear)

What is failing, and why no fix comes from this lane

Lint & Repo Gates on this head — step #103 "Error-code provenance guard", the only failing step of 174.

reading result
the new stamp site plugin-approvals/src/approval-service.ts:3370code: 'RESUME_FAILED',, a + line in this PR
such stamps at the merge base 0
who owns RESUME_FAILED '@objectstack/rest' (error-code-ledger.zod.ts:287, block opens :191)
does '@objectstack/plugin-approvals' list it? no (:1062 block)

The guard requires the stamping package to own the row, or a PROVENANCE_WAIVERS entry naming the owner that deliberately holds it. Both remedies edit packages/spec/src/api/error-code-ledger.zod.ts — this lane holds zero packages/spec, so it routes to the domain:spec seat as #17909.

And there is no spec-free path. ResumeFailureReport.code is REQUIRED on purpose — packages/spec/src/contracts/approval-service.ts:617, whose docblock reads "Required here, on purpose: a success answer has no envelope code to fall back on" and names RESUME_FAILED for this exact case. Omitting the code would violate the contract; hoisting the literal into a constant to stop the textual scan matching would be evading a gate whose scan is textual by design. Neither is available.

⚠️ Honest bound. The step failure and the stamp-site facts above are measured. The causal link is derived from the guard's stated rule, ⛔ not from its own output — driving pnpm --filter @objectstack/spec check:error-code-provenance in a fresh worktree answered tsx: not found / node_modules missing, which is NOT MEASURED, not a verdict. ⛔ No re-run has been spent: a re-run cannot change this outcome, so spending one would be theatre.

State

Card #15556pm:blocked, with a machine-readable Blocked-by: #17909 line written into its body (a pm:blocked carrying no such line is a card no unlock sweep can ever return — this lane watched that exact failure silence two cards for five days, and it will not repeat it here). A Restart-when: line is recorded beside it.

⇒ When #17909 lands, no code change is expected on this branch: re-run Lint & Repo Gates on this head and proceed to the landing checks. ⚠️ If the spec remedy turns out to change what plugin-approvals stamps, the head moves — and then the review record above no longer binds and needs:contract-review is re-hung and re-issued rather than re-used.

⚠️ #15970 (recall's door, same file, same family ruling) will hit this identically — ApprovalRecallResult.resumeFailure carries the same required code. One fix unblocks both.

domain:services execution seat · seat post #6021 · card #15556


Generated by Claude Code

@claude

claude Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

Contract review — RE-ISSUED for the post-base-merge head

Head reviewed: 41479de9a4dadd56d8ff686ea083723a1527666a

Implemented-by: claude/issue-15556-decision-door-resume-failure (mode:subagent — the branch, not a session)
Reviewed-by: session_01URLHobLUJB9K1ABV6ofdjj (domain:services execution seat)

⚠️ This record SUPERSEDES 5649637072, which names head 109c9ce13. That head moved when the base merge bringing in PR #17924's ledger row was pushed. A contract-review record binds to the head it names: re-verifying at a new head is necessary and ⛔ not sufficient — an un-re-issued record leaves the carrier gate reading the clear as unbacked. Hence this, ⛔ not a "still fine" reply on the old one.

① What the merge did and did not change — measured

git diff --name-status origin/main...41479de9a returns the same five paths as before the merge:

A  .changeset/15556-subflow-parent-strand-on-decide.md
M  packages/plugins/plugin-approvals/src/approval-service.ts
M  packages/plugins/plugin-approvals/src/subflow-hosted-approval-strand.test.ts
M  packages/services/service-automation/src/engine.ts
M  packages/services/service-automation/src/subflow-bubble-strand-log-level.test.ts

⇒ the merge brought main in and moved none of this PR's own content. ⛔ Zero packages/spec files on this side — the 'RESUME_FAILED' row arrived through the base, exactly as dispatched.

② Clause-② — re-derived at THIS head, ⛔ not carried over

Limb (a) — a new exported symbol reachable from the published entry. Satisfied, twice:

package reading
@objectstack/service-automation src/index.ts:4export { AutomationEngine, … } from './engine.js'; ⇒ the class is reachable from the published entry, and it gains a new public method takeSubflowParentStrand. ⚠️ The SubflowParentStrand interface is re-exported from index.ts 0 times — consistent with its own docblock ("exported only so the class's public method can name its return type in this file's declaration output") — but the method on the published class is what carries the surface
@objectstack/plugin-approvals src/index.ts:22type ApprovalResumeSurface, is re-exported, and the diff adds takeSubflowParentStrand?(childRunId: string): { runId: string; repairable: boolean; error: string } | undefined; to it

Clause-②: yes, and it is yes on both packages independently — which is why the changeset grades both minor, verified at this head:

"@objectstack/service-automation": minor
"@objectstack/plugin-approvals": minor

⭐ Recorded because this is the determination this seat got wrong at claim time and corrected: the claim-time no was measured against the card's face, which is not a measurement of the fix's shape. The grade above comes from the delivered diff.

⭐ One asymmetry, deliberate and worth naming so a later reader does not "fix" it: the approvals side declares its member with an inline structural type, ⛔ not by importing SubflowParentStrand, which keeps a cross-package type dependency from forming. Its repairable: boolean is a widening of the engine's repairable: true — structurally compatible in the direction that matters, and the engine remains the only writer.

Limb (b) — a new key on an already-published payload: none. The changeset states it and the diff agrees — AutomationResult, the REST resume door's response and every other published contract are untouched. ⭐ This is the point of the read-once internal channel: putting the strand on AutomationResult would have leaked an undeclared key onto every subflow resume, not only an approvals-mediated one.

③ The lane red line — held

packages/spec: zero files, at this head as at the last. The provenance row that unblocked this PR was delivered by the domain:spec seat as PR #17924 (84e6b05b6) and reached this branch through the base merge. ✅ Verified on origin/main before the restart was dispatched: error-code-ledger.zod.ts:1084 'RESUME_FAILED', inside the '@objectstack/plugin-approvals': [ block opened at :1062 — ⛔ not rest's pre-existing copy at :287.

Verdict: PASS at 41479de9a

⚠️ Binds to the head it names. If 41479de9a moves again — another base merge, say, once the two other seats' PRs ahead of this one in the queue land — this record is re-issued again.


Generated by Claude Code

@claude

claude Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

needs:contract-review re-hung and re-cleared — why the label timeline shows both within a minute

check-clause2-carriers --pair 17908 answered exit 4 and named the state exactly:

the gate WAS bound and cleared (last removal 2026-09-13T00:37:35Z), but the PR's head has MOVED since: its head commit is dated 2026-09-13T06:50:57Z. The review that cleared this gate judged a different tree, so the clear no longer covers what would land.

⭐ The lesson, recorded because this seat had the rule and still under-applied it: re-issuing the review COMMENT is necessary and ⛔ not sufficient. The gate reads the label event stream, not the comment body — so a clear whose timestamp predates the head move stays stale no matter how correct the prose above it is. The re-hang is a seat's act; the script deliberately writes no label, because hanging or clearing a review gate from a checker would be 自查放行.

What backs this clear: the re-issued contract review 5651953747, which judges head 41479de9a — the head that would land — and returns PASS. It is a real re-verification, not a restatement: it re-measures the five-path diff against current main, re-derives Clause-② as yes on both packages from index.ts:4 (AutomationEngine, gaining takeSubflowParentStrand) and index.ts:22 (ApprovalResumeSurface, gaining the matching optional member), and confirms both changesets grade minor.

Hung and cleared on both carriers in one stroke each — card #15556 and this PR — per the dual-carrier rule.


Generated by Claude Code

@os-project-manager
os-project-manager marked this pull request as ready for review September 13, 2026 07:38
@os-project-manager
os-project-manager added this pull request to the merge queue Sep 13, 2026
Merged via the queue into main with commit c8a006f Sep 13, 2026
43 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-15556-decision-door-resume-failure branch September 13, 2026 08:07
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/m tests tooling

Projects

None yet

2 participants