feat(spec): name the terminally-failed-but-repairable run 'stranded' on AutomationResult.status (#14384) - #14636

Merged
os-sam merged 1 commit into
mainfrom
claude/issue-14384-automation-result-terminal-failed-status
Sep 2, 2026
Merged

feat(spec): name the terminally-failed-but-repairable run 'stranded' on AutomationResult.status (#14384)#14636
os-sam merged 1 commit into
mainfrom
claude/issue-14384-automation-result-terminal-failed-status

Conversation

@claude

@claudeclaudeBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes#14384

Contract half of the #13937 shape-4 ruling (director seat, comment 5491060439; maintainer 2026-09-01 「其他同意」), quoted verbatim on the card and untranslated here:

  1. 命名同批定(service-automation: a resume consumes the pause BEFORE running downstream nodes, so any node that throws leaves the run terminally unresumable — and the only inspector for it reports all clear #13909 交付项 3 在此收口):该条件成为运维要操作的持久状态 ⇒ 平台级名字落 packages/spec/src/contracts/automation-service.ts(AutomationResult.status 现为 'completed' | 'paused' | 'failed')⇒ 触 spec ⇒ 条款②契约复审档位适用StrandedRunState 保持 plugin 局部报告标签不升格。

What changed — one contract increment

  • packages/spec/src/contracts/automation-service.tsAutomationResult.status is now 'completed' | 'paused' | 'failed' | 'stranded', with a JSDoc paragraph naming the condition: a resume CONSUMED the suspension, a downstream node threw, the run is recorded as failed and can be re-armed only by an explicit operator verb (never by resume, which answers RUN_NOT_FOUND for it, and never automatically). Distinct from 'failed' on purpose: that one says the run ran and was rejected; this one says a recorded continuation stopped mid-flight and an operator has something to repair. The JSDoc also states that plugin-approvals' StrandedRunState is NOT promoted.
  • packages/spec/src/api/automation-api.zod.ts — the wire mirror TriggerFlowResponseSchema.data.status carries the same four (the existing TriggerFlowDataMatchesContract pin goes red if only one side moves; see the reverse verification). The .describe() prose names the condition without an issue id: check:doc-authoring refused the first spelling (customer-facing text must not carry #NNNN), so the card references moved to an adjacent line comment.
  • packages/spec/src/contracts/automation-result-status.pin.test.ts (new) — pins, each on its own: (1) the union's exact membership at the type level (an Assert over an Eq identity, the automation-api.zod.test.ts form, compiled by check:test-typecheck, plus a satisfies list); (2) wire-to-contract parity at both levels (the Zod enum's .options read at runtime, and its inferred type bound to the contract's); (3) that the doc block above the declaration names the condition (source read); (4) a stranded envelope parses AND is preserved through the strip-mode object; (5) a literal outside the four is refused at data.status with invalid_value.
  • content/docs/references/api/automation-api.mdx — regenerated by gen:docs (check:generated --fix; the one artifact it proved stale). api-surface/ is unchanged: the snapshot records that AutomationResult exists, not its members.
  • .changeset/automation-result-stranded-status.md@objectstack/spec: minor, additive (no BREAKING declaration, so no ADR-0087 marker is owed; check:adr-0087-registration exit 0).

Not in this PR, by the card: no engine change, no operator verb, no REST route, no ExecutionStatus widening. Those are #13937 (services half, stays open) and #13953.

The literal — what was measured before choosing

Vocabularies read (packages/spec/src, packages/services/service-automation, packages/plugins/plugin-approvals):

vocabularymembersshape
AutomationResult.status (this union)completedpausedfailedsingle-word participles
ExecutionStatus (automation/execution.zod.ts, the run-row vocabulary listRuns filters on)pendingrunningpausedcompletedfailedcancelledtimed_outretryingsingle words; snake_case for two words
TaskStatus (system/worker.zod.ts)pendingqueuedprocessingcompletedfailedcancelledtimeoutdeaddead = moved to the dead-letter queue
JobExecutionStatus (system/job.zod.ts)runningsuccessfailedtimeoutdegraded
StrandedRunState (plugin-approvals, report-only)missingfailedclassifies WHY a request's run is unrecoverable
prose lineage"stranded" / "strands" / "stranding"#13807, #13909, #13937, PR #13934; inspectStrandedRequests, StrandedApprovalRequest; 8 comment sites in engine.ts

Candidates:

Producer and consumer measurements

  • AutomationResult.status is a TypeScript interface member, not Zod — verified. Its one Zod mirror in packages/spec/src is TriggerFlowResponseSchema.data.status; both updated, parity pinned.
  • Producer today: AutomationEngine.resumeInternal's catch arm records the run ROW failed and returns { success: false, error, durationMs, errorMessage, summary } — no status at all on the resumed-failure envelope (only execute()'s dispatched-and-rejected exit stamps status: 'failed'). So stamping 'stranded' on the result is the services half's, and whether the run ROW (ExecutionStatus) ever carries the word is that half's to measure; this PR does not widen ExecutionStatus.
  • Exhaustive switches over AutomationResult.status in packages/{runtime,services,plugins,rest,cli,client}: zero. The only case 'completed' | 'paused' | 'failed' outside spec is plugin-approvals' classifyStrandedRunState, which switches over the run ROW's status string with a deliberately silent default, not over this union. Narrow re-declarations of the three-member union outside spec: zero in code (two in comments, engine.ts:4182 and approval-service.ts:315).
  • Sibling repo objectui: app-shell/src/utils/flowResponse.ts:108 types status?: 'completed' | 'paused' | 'failed' | string and branches on success === false — tolerant of the new literal; nothing to change there for this half.
  • No consumer needed even a type-only accommodation.

Verification — all readings on head 2c4bc29, exit codes captured before any pipe

@objectstack/spec:

  • pnpm --filter @objectstack/spec build — exit 0 (check-dts-emitted: 34/34 declared declaration file(s) present).
  • pnpm --filter @objectstack/spec test (whole suite) — Test Files 453 passed | 1 skipped (454) · Tests 12214 passed | 1 skipped (12215) on the committed head. The one skip is scripts/root-entry-type-nameability.pin.test.ts: its inspectDistFreshness predicate saw dist older than src because the reverse verification below touched the contract file's mtime after the build (content byte-identical), and that pin skips by default on a stale dist. After rebuilding the dist that file re-ran on the same head: Test Files 1 passed (1) · Tests 2 passed (2); check:api-surface on the rebuilt dist: exit 0. The earlier full run on the pre-.describe()-edit tree with a fresh dist was 454 passed · 12216 passed, 0 skipped.
  • pnpm --filter @objectstack/spec typecheck — exit 0; check:test-typecheck: OK — 54 file(s) / 262 error(s) / 146 pinned signature(s) (ledger unchanged; the new pin file compiles under tsconfig.test.json).
  • The pin file with its two neighbours (contracts/automation-service.test.ts, api/automation-api.zod.test.ts) via exec vitest run --maxWorkers=2Test Files 3 passed (3) · Tests 61 passed (61).
  • check:generated — exit 0 after --fix regenerated the one stale artifact; check:api-surface exit 0; the 15 spec-scoped gates dispatch-gates.mjs derived (api-surface, authorable-surface, browser-reachable-entries, docs, dual-source-exports, empty-state, entry-nameability, export-origins, exported-any, liveness, llms-txt, skill-refs, strictness-ledger, variant-docs, yaml-examples) — every one exit 0.

Root gates, derived with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack from the merge-base change set (the derivation named this checkout at a5b95ee):

  • exit 0: adr-0087-registration, changeset-no-major, empty-changeset, ci-filter-parity, comment-mask-adoption, cross-package-test-inputs (both spellings), keyed-text-bounds, plugin-teardown-shape, shard-attestation, system-context-census, undeclared-dep-imports, docs-audit/check-affected-docs, docs-audit/check-drift-comment, pm/release-rehearsal-clone --self-test, changeset-gate-self-tests, dispatcher-error-vocabulary, engine-double-contract, logger-receiver-detach, merge-driver, objectql-double-limit, objectui-changeset, page-declaration-shape, pm-half-states, published-files, query-options-erasure, slot-lookup, spec-parsed-alias, test-source-alias, type-check-coverage, type-source-resolution, where-matcher, @objectstack/lint check:doc-formula-expressions (exit 0 once the closure was built), pnpm check:doc-authoring (14504 customer-facing string(s) across 710 spec sources clean), pnpm check:nul-bytes (scanned 7981 text file(s) … no raw ASCII control bytes).
  • NOT MEASURED, never green: check-test-completeness (exit 3, PREREQUISITE NOT MET), check-dual-build-cjs-loads (exit 3: needs every package's dist), check:type-check-debt (exit 3: --re-measure needs the full ledgered closure), check-dev-prereqs (exit 1: asks for a full pnpm build), pm/check-half-states (timed out at 240 s on the GitHub network leg — a PM-side gate). CI owns these.

Consumers — filter direction is DOWNSTREAM of spec; their dependency closures were built first (turbo run build over the ^... of each, 55 tasks, exit 0):

  • @objectstack/runtimetypecheck exit 0; the 6 test files that read AutomationResult (flow-dispatch-status, automation-trigger-route-status, automation-trigger-paused-run, action-activation-dispatch, endpoint-flow-dispatch-status, actions-flow-dispatch-status) — Test Files 5 passed (5) · Tests 81 passed (81) (one pattern matched no file on its own; narrowed to these files, declared).
  • @objectstack/service-automation — has no typecheck script (types NOT MEASURED here; the package is ledgered by check:type-check-coverage, exit 0); the 9 test files that read AutomationResultTest Files 9 passed (9) · Tests 239 passed (239) (narrowed, declared).
  • @objectstack/plugin-approvalstypecheck exit 0 (check:test-typecheck: OK); whole testTest Files 36 passed (36) · Tests 662 passed (662).
  • @objectstack/clienttypecheck exit 0 (check:test-typecheck: OK); whole testTest Files 32 passed (32) · Tests 420 passed (420).
  • @objectstack/verifytypecheck exit 0; automation-trigger-terminal-messages.test.tsTest Files 1 passed (1) · Tests 4 passed (4).
  • @objectstack/resttypecheck exit 0 (check:test-typecheck: OK); @objectstack/clitypecheck exit 0. Neither imports AutomationResult.
  • @objectstack/dogfood — NOT MEASURED locally (its closure is the whole app); CI.

Reverse verification — from the committed state

Mutation leg: the contract union narrowed back to three members on disk (confirmed by anchor counts: four-member 0 / three-member 1; git diff --stat 1 file, 1 insertion, 1 deletion), then tsc --noEmit -p packages/spec/tsconfig.test.json: exit 2 with 266 error lines against the ledgered 262 on the restored tree — the 4 new ones name exactly the two pins: src/api/automation-api.zod.test.ts(52,54) (TriggerFlowDataMatchesContract, TS2344) and src/contracts/automation-result-status.pin.test.ts at lines 64 (TS2322, 'stranded' not assignable), 72 and 74 (TS2344). Direction: red, as predicted. Restore leg: git checkout HEAD -- FILE from a trap with an absolute path, proven by git diff HEAD --quiet exit 0 and git hash-object equal to the HEAD blob 85b3d15a….

Review tier

needs:contract-review — clause ②, contract tier, as the ruling says in as many words. Draft; no seat marks it ready or arms auto-merge.

🤖 Generated with Claude Code

Generated by Claude Code


Generated by Claude Code

…onResult.status as 'stranded'
Contract half of the #13937 shape-4 ruling (maintainer 2026-09-01): the run
whose resume consumed its suspension and then had a downstream node throw is
recorded as failed and can be re-armed only by an explicit operator verb.
`AutomationResult.status` now carries `'stranded'` beside
`'completed' | 'paused' | 'failed'`, the wire mirror
`TriggerFlowResponseSchema.data.status` carries the same four, and
`contracts/automation-result-status.pin.test.ts` binds the two at the type
level and the value level and reads the JSDoc that names the condition.
No engine, route or client behaviour changes; plugin-approvals'
report-only `StrandedRunState` is deliberately not promoted.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017RbbUMnxkUnWhE4j94v8FE
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 3 documentable anchor(s).

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

  • content/docs/api/client-sdk.mdx(via AutomationResult (symbol, a top-level interface))
  • content/docs/automation/flows.mdx(via AutomationResult (symbol, a top-level interface))

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

  • content/docs/releases/v17.mdx(via AutomationResult (symbol, a top-level interface))

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
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 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 — 128 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 d8024f050e1f457117f914d6960a204d1af03966packageMentionDocs.

Which tree this was computed on

This run read content/docs from fcdd5f04c1fffa48b2df2a94e9348af681e8c85a — the merge of head 2c4bc2903701f758f1c6c5aa107a3c597888e9de into base d8024f050e1f457117f914d6960a204d1af03966, 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 fcdd5f04c1fffa48b2df2a94e9348af681e8c85a && git checkout fcdd5f04c1fffa48b2df2a94e9348af681e8c85a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin d8024f050e1f457117f914d6960a204d1af03966 2c4bc2903701f758f1c6c5aa107a3c597888e9de && git checkout -B drift-repro d8024f050e1f457117f914d6960a204d1af03966 && git merge --no-ff 2c4bc2903701f758f1c6c5aa107a3c597888e9de
node scripts/docs-audit/affected-docs.mjs --json d8024f050e1f457117f914d6960a204d1af03966

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

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

spec: name the terminally-failed run state on AutomationResult.status — contract half of #13937 (shape 4 ruling)

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat(spec): name the terminally-failed-but-repairable run 'stranded' on AutomationResult.status (#14384) - #14636

Merged
os-sam merged 1 commit into
mainfrom
claude/issue-14384-automation-result-terminal-failed-status
Sep 2, 2026
Merged

feat(spec): name the terminally-failed-but-repairable run 'stranded' on AutomationResult.status (#14384)#14636
os-sam merged 1 commit into
mainfrom
claude/issue-14384-automation-result-terminal-failed-status

Conversation

@claude

@claudeclaudeBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes#14384

Contract half of the #13937 shape-4 ruling (director seat, comment 5491060439; maintainer 2026-09-01 「其他同意」), quoted verbatim on the card and untranslated here:

  1. 命名同批定(service-automation: a resume consumes the pause BEFORE running downstream nodes, so any node that throws leaves the run terminally unresumable — and the only inspector for it reports all clear #13909 交付项 3 在此收口):该条件成为运维要操作的持久状态 ⇒ 平台级名字落 packages/spec/src/contracts/automation-service.ts(AutomationResult.status 现为 'completed' | 'paused' | 'failed')⇒ 触 spec ⇒ 条款②契约复审档位适用StrandedRunState 保持 plugin 局部报告标签不升格。

What changed — one contract increment

  • packages/spec/src/contracts/automation-service.tsAutomationResult.status is now 'completed' | 'paused' | 'failed' | 'stranded', with a JSDoc paragraph naming the condition: a resume CONSUMED the suspension, a downstream node threw, the run is recorded as failed and can be re-armed only by an explicit operator verb (never by resume, which answers RUN_NOT_FOUND for it, and never automatically). Distinct from 'failed' on purpose: that one says the run ran and was rejected; this one says a recorded continuation stopped mid-flight and an operator has something to repair. The JSDoc also states that plugin-approvals' StrandedRunState is NOT promoted.
  • packages/spec/src/api/automation-api.zod.ts — the wire mirror TriggerFlowResponseSchema.data.status carries the same four (the existing TriggerFlowDataMatchesContract pin goes red if only one side moves; see the reverse verification). The .describe() prose names the condition without an issue id: check:doc-authoring refused the first spelling (customer-facing text must not carry #NNNN), so the card references moved to an adjacent line comment.
  • packages/spec/src/contracts/automation-result-status.pin.test.ts (new) — pins, each on its own: (1) the union's exact membership at the type level (an Assert over an Eq identity, the automation-api.zod.test.ts form, compiled by check:test-typecheck, plus a satisfies list); (2) wire-to-contract parity at both levels (the Zod enum's .options read at runtime, and its inferred type bound to the contract's); (3) that the doc block above the declaration names the condition (source read); (4) a stranded envelope parses AND is preserved through the strip-mode object; (5) a literal outside the four is refused at data.status with invalid_value.
  • content/docs/references/api/automation-api.mdx — regenerated by gen:docs (check:generated --fix; the one artifact it proved stale). api-surface/ is unchanged: the snapshot records that AutomationResult exists, not its members.
  • .changeset/automation-result-stranded-status.md@objectstack/spec: minor, additive (no BREAKING declaration, so no ADR-0087 marker is owed; check:adr-0087-registration exit 0).

Not in this PR, by the card: no engine change, no operator verb, no REST route, no ExecutionStatus widening. Those are #13937 (services half, stays open) and #13953.

The literal — what was measured before choosing

Vocabularies read (packages/spec/src, packages/services/service-automation, packages/plugins/plugin-approvals):

vocabularymembersshape
AutomationResult.status (this union)completedpausedfailedsingle-word participles
ExecutionStatus (automation/execution.zod.ts, the run-row vocabulary listRuns filters on)pendingrunningpausedcompletedfailedcancelledtimed_outretryingsingle words; snake_case for two words
TaskStatus (system/worker.zod.ts)pendingqueuedprocessingcompletedfailedcancelledtimeoutdeaddead = moved to the dead-letter queue
JobExecutionStatus (system/job.zod.ts)runningsuccessfailedtimeoutdegraded
StrandedRunState (plugin-approvals, report-only)missingfailedclassifies WHY a request's run is unrecoverable
prose lineage"stranded" / "strands" / "stranding"#13807, #13909, #13937, PR #13934; inspectStrandedRequests, StrandedApprovalRequest; 8 comment sites in engine.ts

Candidates:

Producer and consumer measurements

  • AutomationResult.status is a TypeScript interface member, not Zod — verified. Its one Zod mirror in packages/spec/src is TriggerFlowResponseSchema.data.status; both updated, parity pinned.
  • Producer today: AutomationEngine.resumeInternal's catch arm records the run ROW failed and returns { success: false, error, durationMs, errorMessage, summary } — no status at all on the resumed-failure envelope (only execute()'s dispatched-and-rejected exit stamps status: 'failed'). So stamping 'stranded' on the result is the services half's, and whether the run ROW (ExecutionStatus) ever carries the word is that half's to measure; this PR does not widen ExecutionStatus.
  • Exhaustive switches over AutomationResult.status in packages/{runtime,services,plugins,rest,cli,client}: zero. The only case 'completed' | 'paused' | 'failed' outside spec is plugin-approvals' classifyStrandedRunState, which switches over the run ROW's status string with a deliberately silent default, not over this union. Narrow re-declarations of the three-member union outside spec: zero in code (two in comments, engine.ts:4182 and approval-service.ts:315).
  • Sibling repo objectui: app-shell/src/utils/flowResponse.ts:108 types status?: 'completed' | 'paused' | 'failed' | string and branches on success === false — tolerant of the new literal; nothing to change there for this half.
  • No consumer needed even a type-only accommodation.

Verification — all readings on head 2c4bc29, exit codes captured before any pipe

@objectstack/spec:

  • pnpm --filter @objectstack/spec build — exit 0 (check-dts-emitted: 34/34 declared declaration file(s) present).
  • pnpm --filter @objectstack/spec test (whole suite) — Test Files 453 passed | 1 skipped (454) · Tests 12214 passed | 1 skipped (12215) on the committed head. The one skip is scripts/root-entry-type-nameability.pin.test.ts: its inspectDistFreshness predicate saw dist older than src because the reverse verification below touched the contract file's mtime after the build (content byte-identical), and that pin skips by default on a stale dist. After rebuilding the dist that file re-ran on the same head: Test Files 1 passed (1) · Tests 2 passed (2); check:api-surface on the rebuilt dist: exit 0. The earlier full run on the pre-.describe()-edit tree with a fresh dist was 454 passed · 12216 passed, 0 skipped.
  • pnpm --filter @objectstack/spec typecheck — exit 0; check:test-typecheck: OK — 54 file(s) / 262 error(s) / 146 pinned signature(s) (ledger unchanged; the new pin file compiles under tsconfig.test.json).
  • The pin file with its two neighbours (contracts/automation-service.test.ts, api/automation-api.zod.test.ts) via exec vitest run --maxWorkers=2Test Files 3 passed (3) · Tests 61 passed (61).
  • check:generated — exit 0 after --fix regenerated the one stale artifact; check:api-surface exit 0; the 15 spec-scoped gates dispatch-gates.mjs derived (api-surface, authorable-surface, browser-reachable-entries, docs, dual-source-exports, empty-state, entry-nameability, export-origins, exported-any, liveness, llms-txt, skill-refs, strictness-ledger, variant-docs, yaml-examples) — every one exit 0.

Root gates, derived with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack from the merge-base change set (the derivation named this checkout at a5b95ee):

  • exit 0: adr-0087-registration, changeset-no-major, empty-changeset, ci-filter-parity, comment-mask-adoption, cross-package-test-inputs (both spellings), keyed-text-bounds, plugin-teardown-shape, shard-attestation, system-context-census, undeclared-dep-imports, docs-audit/check-affected-docs, docs-audit/check-drift-comment, pm/release-rehearsal-clone --self-test, changeset-gate-self-tests, dispatcher-error-vocabulary, engine-double-contract, logger-receiver-detach, merge-driver, objectql-double-limit, objectui-changeset, page-declaration-shape, pm-half-states, published-files, query-options-erasure, slot-lookup, spec-parsed-alias, test-source-alias, type-check-coverage, type-source-resolution, where-matcher, @objectstack/lint check:doc-formula-expressions (exit 0 once the closure was built), pnpm check:doc-authoring (14504 customer-facing string(s) across 710 spec sources clean), pnpm check:nul-bytes (scanned 7981 text file(s) … no raw ASCII control bytes).
  • NOT MEASURED, never green: check-test-completeness (exit 3, PREREQUISITE NOT MET), check-dual-build-cjs-loads (exit 3: needs every package's dist), check:type-check-debt (exit 3: --re-measure needs the full ledgered closure), check-dev-prereqs (exit 1: asks for a full pnpm build), pm/check-half-states (timed out at 240 s on the GitHub network leg — a PM-side gate). CI owns these.

Consumers — filter direction is DOWNSTREAM of spec; their dependency closures were built first (turbo run build over the ^... of each, 55 tasks, exit 0):

  • @objectstack/runtimetypecheck exit 0; the 6 test files that read AutomationResult (flow-dispatch-status, automation-trigger-route-status, automation-trigger-paused-run, action-activation-dispatch, endpoint-flow-dispatch-status, actions-flow-dispatch-status) — Test Files 5 passed (5) · Tests 81 passed (81) (one pattern matched no file on its own; narrowed to these files, declared).
  • @objectstack/service-automation — has no typecheck script (types NOT MEASURED here; the package is ledgered by check:type-check-coverage, exit 0); the 9 test files that read AutomationResultTest Files 9 passed (9) · Tests 239 passed (239) (narrowed, declared).
  • @objectstack/plugin-approvalstypecheck exit 0 (check:test-typecheck: OK); whole testTest Files 36 passed (36) · Tests 662 passed (662).
  • @objectstack/clienttypecheck exit 0 (check:test-typecheck: OK); whole testTest Files 32 passed (32) · Tests 420 passed (420).
  • @objectstack/verifytypecheck exit 0; automation-trigger-terminal-messages.test.tsTest Files 1 passed (1) · Tests 4 passed (4).
  • @objectstack/resttypecheck exit 0 (check:test-typecheck: OK); @objectstack/clitypecheck exit 0. Neither imports AutomationResult.
  • @objectstack/dogfood — NOT MEASURED locally (its closure is the whole app); CI.

Reverse verification — from the committed state

Mutation leg: the contract union narrowed back to three members on disk (confirmed by anchor counts: four-member 0 / three-member 1; git diff --stat 1 file, 1 insertion, 1 deletion), then tsc --noEmit -p packages/spec/tsconfig.test.json: exit 2 with 266 error lines against the ledgered 262 on the restored tree — the 4 new ones name exactly the two pins: src/api/automation-api.zod.test.ts(52,54) (TriggerFlowDataMatchesContract, TS2344) and src/contracts/automation-result-status.pin.test.ts at lines 64 (TS2322, 'stranded' not assignable), 72 and 74 (TS2344). Direction: red, as predicted. Restore leg: git checkout HEAD -- FILE from a trap with an absolute path, proven by git diff HEAD --quiet exit 0 and git hash-object equal to the HEAD blob 85b3d15a….

Review tier

needs:contract-review — clause ②, contract tier, as the ruling says in as many words. Draft; no seat marks it ready or arms auto-merge.

🤖 Generated with Claude Code

Generated by Claude Code


Generated by Claude Code

…onResult.status as 'stranded'
Contract half of the #13937 shape-4 ruling (maintainer 2026-09-01): the run
whose resume consumed its suspension and then had a downstream node throw is
recorded as failed and can be re-armed only by an explicit operator verb.
`AutomationResult.status` now carries `'stranded'` beside
`'completed' | 'paused' | 'failed'`, the wire mirror
`TriggerFlowResponseSchema.data.status` carries the same four, and
`contracts/automation-result-status.pin.test.ts` binds the two at the type
level and the value level and reads the JSDoc that names the condition.
No engine, route or client behaviour changes; plugin-approvals'
report-only `StrandedRunState` is deliberately not promoted.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017RbbUMnxkUnWhE4j94v8FE
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 3 documentable anchor(s).

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

  • content/docs/api/client-sdk.mdx(via AutomationResult (symbol, a top-level interface))
  • content/docs/automation/flows.mdx(via AutomationResult (symbol, a top-level interface))

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

  • content/docs/releases/v17.mdx(via AutomationResult (symbol, a top-level interface))

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
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 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 — 128 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 d8024f050e1f457117f914d6960a204d1af03966packageMentionDocs.

Which tree this was computed on

This run read content/docs from fcdd5f04c1fffa48b2df2a94e9348af681e8c85a — the merge of head 2c4bc2903701f758f1c6c5aa107a3c597888e9de into base d8024f050e1f457117f914d6960a204d1af03966, 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 fcdd5f04c1fffa48b2df2a94e9348af681e8c85a && git checkout fcdd5f04c1fffa48b2df2a94e9348af681e8c85a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin d8024f050e1f457117f914d6960a204d1af03966 2c4bc2903701f758f1c6c5aa107a3c597888e9de && git checkout -B drift-repro d8024f050e1f457117f914d6960a204d1af03966 && git merge --no-ff 2c4bc2903701f758f1c6c5aa107a3c597888e9de
node scripts/docs-audit/affected-docs.mjs --json d8024f050e1f457117f914d6960a204d1af03966

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

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

spec: name the terminally-failed run state on AutomationResult.status — contract half of #13937 (shape 4 ruling)

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(spec): name the terminally-failed-but-repairable run 'stranded' on AutomationResult.status (#14384) - #14636

Merged
os-sam merged 1 commit into
mainfrom
claude/issue-14384-automation-result-terminal-failed-status
Sep 2, 2026
Merged

feat(spec): name the terminally-failed-but-repairable run 'stranded' on AutomationResult.status (#14384)#14636
os-sam merged 1 commit into
mainfrom
claude/issue-14384-automation-result-terminal-failed-status

Conversation

@claude

@claudeclaudeBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes#14384

Contract half of the #13937 shape-4 ruling (director seat, comment 5491060439; maintainer 2026-09-01 「其他同意」), quoted verbatim on the card and untranslated here:

  1. 命名同批定(service-automation: a resume consumes the pause BEFORE running downstream nodes, so any node that throws leaves the run terminally unresumable — and the only inspector for it reports all clear #13909 交付项 3 在此收口):该条件成为运维要操作的持久状态 ⇒ 平台级名字落 packages/spec/src/contracts/automation-service.ts(AutomationResult.status 现为 'completed' | 'paused' | 'failed')⇒ 触 spec ⇒ 条款②契约复审档位适用StrandedRunState 保持 plugin 局部报告标签不升格。

What changed — one contract increment

  • packages/spec/src/contracts/automation-service.tsAutomationResult.status is now 'completed' | 'paused' | 'failed' | 'stranded', with a JSDoc paragraph naming the condition: a resume CONSUMED the suspension, a downstream node threw, the run is recorded as failed and can be re-armed only by an explicit operator verb (never by resume, which answers RUN_NOT_FOUND for it, and never automatically). Distinct from 'failed' on purpose: that one says the run ran and was rejected; this one says a recorded continuation stopped mid-flight and an operator has something to repair. The JSDoc also states that plugin-approvals' StrandedRunState is NOT promoted.
  • packages/spec/src/api/automation-api.zod.ts — the wire mirror TriggerFlowResponseSchema.data.status carries the same four (the existing TriggerFlowDataMatchesContract pin goes red if only one side moves; see the reverse verification). The .describe() prose names the condition without an issue id: check:doc-authoring refused the first spelling (customer-facing text must not carry #NNNN), so the card references moved to an adjacent line comment.
  • packages/spec/src/contracts/automation-result-status.pin.test.ts (new) — pins, each on its own: (1) the union's exact membership at the type level (an Assert over an Eq identity, the automation-api.zod.test.ts form, compiled by check:test-typecheck, plus a satisfies list); (2) wire-to-contract parity at both levels (the Zod enum's .options read at runtime, and its inferred type bound to the contract's); (3) that the doc block above the declaration names the condition (source read); (4) a stranded envelope parses AND is preserved through the strip-mode object; (5) a literal outside the four is refused at data.status with invalid_value.
  • content/docs/references/api/automation-api.mdx — regenerated by gen:docs (check:generated --fix; the one artifact it proved stale). api-surface/ is unchanged: the snapshot records that AutomationResult exists, not its members.
  • .changeset/automation-result-stranded-status.md@objectstack/spec: minor, additive (no BREAKING declaration, so no ADR-0087 marker is owed; check:adr-0087-registration exit 0).

Not in this PR, by the card: no engine change, no operator verb, no REST route, no ExecutionStatus widening. Those are #13937 (services half, stays open) and #13953.

The literal — what was measured before choosing

Vocabularies read (packages/spec/src, packages/services/service-automation, packages/plugins/plugin-approvals):

vocabularymembersshape
AutomationResult.status (this union)completedpausedfailedsingle-word participles
ExecutionStatus (automation/execution.zod.ts, the run-row vocabulary listRuns filters on)pendingrunningpausedcompletedfailedcancelledtimed_outretryingsingle words; snake_case for two words
TaskStatus (system/worker.zod.ts)pendingqueuedprocessingcompletedfailedcancelledtimeoutdeaddead = moved to the dead-letter queue
JobExecutionStatus (system/job.zod.ts)runningsuccessfailedtimeoutdegraded
StrandedRunState (plugin-approvals, report-only)missingfailedclassifies WHY a request's run is unrecoverable
prose lineage"stranded" / "strands" / "stranding"#13807, #13909, #13937, PR #13934; inspectStrandedRequests, StrandedApprovalRequest; 8 comment sites in engine.ts

Candidates:

Producer and consumer measurements

  • AutomationResult.status is a TypeScript interface member, not Zod — verified. Its one Zod mirror in packages/spec/src is TriggerFlowResponseSchema.data.status; both updated, parity pinned.
  • Producer today: AutomationEngine.resumeInternal's catch arm records the run ROW failed and returns { success: false, error, durationMs, errorMessage, summary } — no status at all on the resumed-failure envelope (only execute()'s dispatched-and-rejected exit stamps status: 'failed'). So stamping 'stranded' on the result is the services half's, and whether the run ROW (ExecutionStatus) ever carries the word is that half's to measure; this PR does not widen ExecutionStatus.
  • Exhaustive switches over AutomationResult.status in packages/{runtime,services,plugins,rest,cli,client}: zero. The only case 'completed' | 'paused' | 'failed' outside spec is plugin-approvals' classifyStrandedRunState, which switches over the run ROW's status string with a deliberately silent default, not over this union. Narrow re-declarations of the three-member union outside spec: zero in code (two in comments, engine.ts:4182 and approval-service.ts:315).
  • Sibling repo objectui: app-shell/src/utils/flowResponse.ts:108 types status?: 'completed' | 'paused' | 'failed' | string and branches on success === false — tolerant of the new literal; nothing to change there for this half.
  • No consumer needed even a type-only accommodation.

Verification — all readings on head 2c4bc29, exit codes captured before any pipe

@objectstack/spec:

  • pnpm --filter @objectstack/spec build — exit 0 (check-dts-emitted: 34/34 declared declaration file(s) present).
  • pnpm --filter @objectstack/spec test (whole suite) — Test Files 453 passed | 1 skipped (454) · Tests 12214 passed | 1 skipped (12215) on the committed head. The one skip is scripts/root-entry-type-nameability.pin.test.ts: its inspectDistFreshness predicate saw dist older than src because the reverse verification below touched the contract file's mtime after the build (content byte-identical), and that pin skips by default on a stale dist. After rebuilding the dist that file re-ran on the same head: Test Files 1 passed (1) · Tests 2 passed (2); check:api-surface on the rebuilt dist: exit 0. The earlier full run on the pre-.describe()-edit tree with a fresh dist was 454 passed · 12216 passed, 0 skipped.
  • pnpm --filter @objectstack/spec typecheck — exit 0; check:test-typecheck: OK — 54 file(s) / 262 error(s) / 146 pinned signature(s) (ledger unchanged; the new pin file compiles under tsconfig.test.json).
  • The pin file with its two neighbours (contracts/automation-service.test.ts, api/automation-api.zod.test.ts) via exec vitest run --maxWorkers=2Test Files 3 passed (3) · Tests 61 passed (61).
  • check:generated — exit 0 after --fix regenerated the one stale artifact; check:api-surface exit 0; the 15 spec-scoped gates dispatch-gates.mjs derived (api-surface, authorable-surface, browser-reachable-entries, docs, dual-source-exports, empty-state, entry-nameability, export-origins, exported-any, liveness, llms-txt, skill-refs, strictness-ledger, variant-docs, yaml-examples) — every one exit 0.

Root gates, derived with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack from the merge-base change set (the derivation named this checkout at a5b95ee):

  • exit 0: adr-0087-registration, changeset-no-major, empty-changeset, ci-filter-parity, comment-mask-adoption, cross-package-test-inputs (both spellings), keyed-text-bounds, plugin-teardown-shape, shard-attestation, system-context-census, undeclared-dep-imports, docs-audit/check-affected-docs, docs-audit/check-drift-comment, pm/release-rehearsal-clone --self-test, changeset-gate-self-tests, dispatcher-error-vocabulary, engine-double-contract, logger-receiver-detach, merge-driver, objectql-double-limit, objectui-changeset, page-declaration-shape, pm-half-states, published-files, query-options-erasure, slot-lookup, spec-parsed-alias, test-source-alias, type-check-coverage, type-source-resolution, where-matcher, @objectstack/lint check:doc-formula-expressions (exit 0 once the closure was built), pnpm check:doc-authoring (14504 customer-facing string(s) across 710 spec sources clean), pnpm check:nul-bytes (scanned 7981 text file(s) … no raw ASCII control bytes).
  • NOT MEASURED, never green: check-test-completeness (exit 3, PREREQUISITE NOT MET), check-dual-build-cjs-loads (exit 3: needs every package's dist), check:type-check-debt (exit 3: --re-measure needs the full ledgered closure), check-dev-prereqs (exit 1: asks for a full pnpm build), pm/check-half-states (timed out at 240 s on the GitHub network leg — a PM-side gate). CI owns these.

Consumers — filter direction is DOWNSTREAM of spec; their dependency closures were built first (turbo run build over the ^... of each, 55 tasks, exit 0):

  • @objectstack/runtimetypecheck exit 0; the 6 test files that read AutomationResult (flow-dispatch-status, automation-trigger-route-status, automation-trigger-paused-run, action-activation-dispatch, endpoint-flow-dispatch-status, actions-flow-dispatch-status) — Test Files 5 passed (5) · Tests 81 passed (81) (one pattern matched no file on its own; narrowed to these files, declared).
  • @objectstack/service-automation — has no typecheck script (types NOT MEASURED here; the package is ledgered by check:type-check-coverage, exit 0); the 9 test files that read AutomationResultTest Files 9 passed (9) · Tests 239 passed (239) (narrowed, declared).
  • @objectstack/plugin-approvalstypecheck exit 0 (check:test-typecheck: OK); whole testTest Files 36 passed (36) · Tests 662 passed (662).
  • @objectstack/clienttypecheck exit 0 (check:test-typecheck: OK); whole testTest Files 32 passed (32) · Tests 420 passed (420).
  • @objectstack/verifytypecheck exit 0; automation-trigger-terminal-messages.test.tsTest Files 1 passed (1) · Tests 4 passed (4).
  • @objectstack/resttypecheck exit 0 (check:test-typecheck: OK); @objectstack/clitypecheck exit 0. Neither imports AutomationResult.
  • @objectstack/dogfood — NOT MEASURED locally (its closure is the whole app); CI.

Reverse verification — from the committed state

Mutation leg: the contract union narrowed back to three members on disk (confirmed by anchor counts: four-member 0 / three-member 1; git diff --stat 1 file, 1 insertion, 1 deletion), then tsc --noEmit -p packages/spec/tsconfig.test.json: exit 2 with 266 error lines against the ledgered 262 on the restored tree — the 4 new ones name exactly the two pins: src/api/automation-api.zod.test.ts(52,54) (TriggerFlowDataMatchesContract, TS2344) and src/contracts/automation-result-status.pin.test.ts at lines 64 (TS2322, 'stranded' not assignable), 72 and 74 (TS2344). Direction: red, as predicted. Restore leg: git checkout HEAD -- FILE from a trap with an absolute path, proven by git diff HEAD --quiet exit 0 and git hash-object equal to the HEAD blob 85b3d15a….

Review tier

needs:contract-review — clause ②, contract tier, as the ruling says in as many words. Draft; no seat marks it ready or arms auto-merge.

🤖 Generated with Claude Code

Generated by Claude Code


Generated by Claude Code

…onResult.status as 'stranded'
Contract half of the #13937 shape-4 ruling (maintainer 2026-09-01): the run
whose resume consumed its suspension and then had a downstream node throw is
recorded as failed and can be re-armed only by an explicit operator verb.
`AutomationResult.status` now carries `'stranded'` beside
`'completed' | 'paused' | 'failed'`, the wire mirror
`TriggerFlowResponseSchema.data.status` carries the same four, and
`contracts/automation-result-status.pin.test.ts` binds the two at the type
level and the value level and reads the JSDoc that names the condition.
No engine, route or client behaviour changes; plugin-approvals'
report-only `StrandedRunState` is deliberately not promoted.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017RbbUMnxkUnWhE4j94v8FE
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 3 documentable anchor(s).

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

  • content/docs/api/client-sdk.mdx(via AutomationResult (symbol, a top-level interface))
  • content/docs/automation/flows.mdx(via AutomationResult (symbol, a top-level interface))

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

  • content/docs/releases/v17.mdx(via AutomationResult (symbol, a top-level interface))

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
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 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 — 128 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 d8024f050e1f457117f914d6960a204d1af03966packageMentionDocs.

Which tree this was computed on

This run read content/docs from fcdd5f04c1fffa48b2df2a94e9348af681e8c85a — the merge of head 2c4bc2903701f758f1c6c5aa107a3c597888e9de into base d8024f050e1f457117f914d6960a204d1af03966, 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 fcdd5f04c1fffa48b2df2a94e9348af681e8c85a && git checkout fcdd5f04c1fffa48b2df2a94e9348af681e8c85a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin d8024f050e1f457117f914d6960a204d1af03966 2c4bc2903701f758f1c6c5aa107a3c597888e9de && git checkout -B drift-repro d8024f050e1f457117f914d6960a204d1af03966 && git merge --no-ff 2c4bc2903701f758f1c6c5aa107a3c597888e9de
node scripts/docs-audit/affected-docs.mjs --json d8024f050e1f457117f914d6960a204d1af03966

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

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

spec: name the terminally-failed run state on AutomationResult.status — contract half of #13937 (shape 4 ruling)

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(spec): name the terminally-failed-but-repairable run 'stranded' on AutomationResult.status (#14384) - #14636

Merged
os-sam merged 1 commit into
mainfrom
claude/issue-14384-automation-result-terminal-failed-status
Sep 2, 2026
Merged

feat(spec): name the terminally-failed-but-repairable run 'stranded' on AutomationResult.status (#14384)#14636
os-sam merged 1 commit into
mainfrom
claude/issue-14384-automation-result-terminal-failed-status

Conversation

@claude

@claudeclaudeBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes#14384

Contract half of the #13937 shape-4 ruling (director seat, comment 5491060439; maintainer 2026-09-01 「其他同意」), quoted verbatim on the card and untranslated here:

  1. 命名同批定(service-automation: a resume consumes the pause BEFORE running downstream nodes, so any node that throws leaves the run terminally unresumable — and the only inspector for it reports all clear #13909 交付项 3 在此收口):该条件成为运维要操作的持久状态 ⇒ 平台级名字落 packages/spec/src/contracts/automation-service.ts(AutomationResult.status 现为 'completed' | 'paused' | 'failed')⇒ 触 spec ⇒ 条款②契约复审档位适用StrandedRunState 保持 plugin 局部报告标签不升格。

What changed — one contract increment

  • packages/spec/src/contracts/automation-service.tsAutomationResult.status is now 'completed' | 'paused' | 'failed' | 'stranded', with a JSDoc paragraph naming the condition: a resume CONSUMED the suspension, a downstream node threw, the run is recorded as failed and can be re-armed only by an explicit operator verb (never by resume, which answers RUN_NOT_FOUND for it, and never automatically). Distinct from 'failed' on purpose: that one says the run ran and was rejected; this one says a recorded continuation stopped mid-flight and an operator has something to repair. The JSDoc also states that plugin-approvals' StrandedRunState is NOT promoted.
  • packages/spec/src/api/automation-api.zod.ts — the wire mirror TriggerFlowResponseSchema.data.status carries the same four (the existing TriggerFlowDataMatchesContract pin goes red if only one side moves; see the reverse verification). The .describe() prose names the condition without an issue id: check:doc-authoring refused the first spelling (customer-facing text must not carry #NNNN), so the card references moved to an adjacent line comment.
  • packages/spec/src/contracts/automation-result-status.pin.test.ts (new) — pins, each on its own: (1) the union's exact membership at the type level (an Assert over an Eq identity, the automation-api.zod.test.ts form, compiled by check:test-typecheck, plus a satisfies list); (2) wire-to-contract parity at both levels (the Zod enum's .options read at runtime, and its inferred type bound to the contract's); (3) that the doc block above the declaration names the condition (source read); (4) a stranded envelope parses AND is preserved through the strip-mode object; (5) a literal outside the four is refused at data.status with invalid_value.
  • content/docs/references/api/automation-api.mdx — regenerated by gen:docs (check:generated --fix; the one artifact it proved stale). api-surface/ is unchanged: the snapshot records that AutomationResult exists, not its members.
  • .changeset/automation-result-stranded-status.md@objectstack/spec: minor, additive (no BREAKING declaration, so no ADR-0087 marker is owed; check:adr-0087-registration exit 0).

Not in this PR, by the card: no engine change, no operator verb, no REST route, no ExecutionStatus widening. Those are #13937 (services half, stays open) and #13953.

The literal — what was measured before choosing

Vocabularies read (packages/spec/src, packages/services/service-automation, packages/plugins/plugin-approvals):

vocabularymembersshape
AutomationResult.status (this union)completedpausedfailedsingle-word participles
ExecutionStatus (automation/execution.zod.ts, the run-row vocabulary listRuns filters on)pendingrunningpausedcompletedfailedcancelledtimed_outretryingsingle words; snake_case for two words
TaskStatus (system/worker.zod.ts)pendingqueuedprocessingcompletedfailedcancelledtimeoutdeaddead = moved to the dead-letter queue
JobExecutionStatus (system/job.zod.ts)runningsuccessfailedtimeoutdegraded
StrandedRunState (plugin-approvals, report-only)missingfailedclassifies WHY a request's run is unrecoverable
prose lineage"stranded" / "strands" / "stranding"#13807, #13909, #13937, PR #13934; inspectStrandedRequests, StrandedApprovalRequest; 8 comment sites in engine.ts

Candidates:

Producer and consumer measurements

  • AutomationResult.status is a TypeScript interface member, not Zod — verified. Its one Zod mirror in packages/spec/src is TriggerFlowResponseSchema.data.status; both updated, parity pinned.
  • Producer today: AutomationEngine.resumeInternal's catch arm records the run ROW failed and returns { success: false, error, durationMs, errorMessage, summary } — no status at all on the resumed-failure envelope (only execute()'s dispatched-and-rejected exit stamps status: 'failed'). So stamping 'stranded' on the result is the services half's, and whether the run ROW (ExecutionStatus) ever carries the word is that half's to measure; this PR does not widen ExecutionStatus.
  • Exhaustive switches over AutomationResult.status in packages/{runtime,services,plugins,rest,cli,client}: zero. The only case 'completed' | 'paused' | 'failed' outside spec is plugin-approvals' classifyStrandedRunState, which switches over the run ROW's status string with a deliberately silent default, not over this union. Narrow re-declarations of the three-member union outside spec: zero in code (two in comments, engine.ts:4182 and approval-service.ts:315).
  • Sibling repo objectui: app-shell/src/utils/flowResponse.ts:108 types status?: 'completed' | 'paused' | 'failed' | string and branches on success === false — tolerant of the new literal; nothing to change there for this half.
  • No consumer needed even a type-only accommodation.

Verification — all readings on head 2c4bc29, exit codes captured before any pipe

@objectstack/spec:

  • pnpm --filter @objectstack/spec build — exit 0 (check-dts-emitted: 34/34 declared declaration file(s) present).
  • pnpm --filter @objectstack/spec test (whole suite) — Test Files 453 passed | 1 skipped (454) · Tests 12214 passed | 1 skipped (12215) on the committed head. The one skip is scripts/root-entry-type-nameability.pin.test.ts: its inspectDistFreshness predicate saw dist older than src because the reverse verification below touched the contract file's mtime after the build (content byte-identical), and that pin skips by default on a stale dist. After rebuilding the dist that file re-ran on the same head: Test Files 1 passed (1) · Tests 2 passed (2); check:api-surface on the rebuilt dist: exit 0. The earlier full run on the pre-.describe()-edit tree with a fresh dist was 454 passed · 12216 passed, 0 skipped.
  • pnpm --filter @objectstack/spec typecheck — exit 0; check:test-typecheck: OK — 54 file(s) / 262 error(s) / 146 pinned signature(s) (ledger unchanged; the new pin file compiles under tsconfig.test.json).
  • The pin file with its two neighbours (contracts/automation-service.test.ts, api/automation-api.zod.test.ts) via exec vitest run --maxWorkers=2Test Files 3 passed (3) · Tests 61 passed (61).
  • check:generated — exit 0 after --fix regenerated the one stale artifact; check:api-surface exit 0; the 15 spec-scoped gates dispatch-gates.mjs derived (api-surface, authorable-surface, browser-reachable-entries, docs, dual-source-exports, empty-state, entry-nameability, export-origins, exported-any, liveness, llms-txt, skill-refs, strictness-ledger, variant-docs, yaml-examples) — every one exit 0.

Root gates, derived with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack from the merge-base change set (the derivation named this checkout at a5b95ee):

  • exit 0: adr-0087-registration, changeset-no-major, empty-changeset, ci-filter-parity, comment-mask-adoption, cross-package-test-inputs (both spellings), keyed-text-bounds, plugin-teardown-shape, shard-attestation, system-context-census, undeclared-dep-imports, docs-audit/check-affected-docs, docs-audit/check-drift-comment, pm/release-rehearsal-clone --self-test, changeset-gate-self-tests, dispatcher-error-vocabulary, engine-double-contract, logger-receiver-detach, merge-driver, objectql-double-limit, objectui-changeset, page-declaration-shape, pm-half-states, published-files, query-options-erasure, slot-lookup, spec-parsed-alias, test-source-alias, type-check-coverage, type-source-resolution, where-matcher, @objectstack/lint check:doc-formula-expressions (exit 0 once the closure was built), pnpm check:doc-authoring (14504 customer-facing string(s) across 710 spec sources clean), pnpm check:nul-bytes (scanned 7981 text file(s) … no raw ASCII control bytes).
  • NOT MEASURED, never green: check-test-completeness (exit 3, PREREQUISITE NOT MET), check-dual-build-cjs-loads (exit 3: needs every package's dist), check:type-check-debt (exit 3: --re-measure needs the full ledgered closure), check-dev-prereqs (exit 1: asks for a full pnpm build), pm/check-half-states (timed out at 240 s on the GitHub network leg — a PM-side gate). CI owns these.

Consumers — filter direction is DOWNSTREAM of spec; their dependency closures were built first (turbo run build over the ^... of each, 55 tasks, exit 0):

  • @objectstack/runtimetypecheck exit 0; the 6 test files that read AutomationResult (flow-dispatch-status, automation-trigger-route-status, automation-trigger-paused-run, action-activation-dispatch, endpoint-flow-dispatch-status, actions-flow-dispatch-status) — Test Files 5 passed (5) · Tests 81 passed (81) (one pattern matched no file on its own; narrowed to these files, declared).
  • @objectstack/service-automation — has no typecheck script (types NOT MEASURED here; the package is ledgered by check:type-check-coverage, exit 0); the 9 test files that read AutomationResultTest Files 9 passed (9) · Tests 239 passed (239) (narrowed, declared).
  • @objectstack/plugin-approvalstypecheck exit 0 (check:test-typecheck: OK); whole testTest Files 36 passed (36) · Tests 662 passed (662).
  • @objectstack/clienttypecheck exit 0 (check:test-typecheck: OK); whole testTest Files 32 passed (32) · Tests 420 passed (420).
  • @objectstack/verifytypecheck exit 0; automation-trigger-terminal-messages.test.tsTest Files 1 passed (1) · Tests 4 passed (4).
  • @objectstack/resttypecheck exit 0 (check:test-typecheck: OK); @objectstack/clitypecheck exit 0. Neither imports AutomationResult.
  • @objectstack/dogfood — NOT MEASURED locally (its closure is the whole app); CI.

Reverse verification — from the committed state

Mutation leg: the contract union narrowed back to three members on disk (confirmed by anchor counts: four-member 0 / three-member 1; git diff --stat 1 file, 1 insertion, 1 deletion), then tsc --noEmit -p packages/spec/tsconfig.test.json: exit 2 with 266 error lines against the ledgered 262 on the restored tree — the 4 new ones name exactly the two pins: src/api/automation-api.zod.test.ts(52,54) (TriggerFlowDataMatchesContract, TS2344) and src/contracts/automation-result-status.pin.test.ts at lines 64 (TS2322, 'stranded' not assignable), 72 and 74 (TS2344). Direction: red, as predicted. Restore leg: git checkout HEAD -- FILE from a trap with an absolute path, proven by git diff HEAD --quiet exit 0 and git hash-object equal to the HEAD blob 85b3d15a….

Review tier

needs:contract-review — clause ②, contract tier, as the ruling says in as many words. Draft; no seat marks it ready or arms auto-merge.

🤖 Generated with Claude Code

Generated by Claude Code


Generated by Claude Code

…onResult.status as 'stranded'
Contract half of the #13937 shape-4 ruling (maintainer 2026-09-01): the run
whose resume consumed its suspension and then had a downstream node throw is
recorded as failed and can be re-armed only by an explicit operator verb.
`AutomationResult.status` now carries `'stranded'` beside
`'completed' | 'paused' | 'failed'`, the wire mirror
`TriggerFlowResponseSchema.data.status` carries the same four, and
`contracts/automation-result-status.pin.test.ts` binds the two at the type
level and the value level and reads the JSDoc that names the condition.
No engine, route or client behaviour changes; plugin-approvals'
report-only `StrandedRunState` is deliberately not promoted.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017RbbUMnxkUnWhE4j94v8FE
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 3 documentable anchor(s).

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

  • content/docs/api/client-sdk.mdx(via AutomationResult (symbol, a top-level interface))
  • content/docs/automation/flows.mdx(via AutomationResult (symbol, a top-level interface))

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

  • content/docs/releases/v17.mdx(via AutomationResult (symbol, a top-level interface))

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
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 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 — 128 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 d8024f050e1f457117f914d6960a204d1af03966packageMentionDocs.

Which tree this was computed on

This run read content/docs from fcdd5f04c1fffa48b2df2a94e9348af681e8c85a — the merge of head 2c4bc2903701f758f1c6c5aa107a3c597888e9de into base d8024f050e1f457117f914d6960a204d1af03966, 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 fcdd5f04c1fffa48b2df2a94e9348af681e8c85a && git checkout fcdd5f04c1fffa48b2df2a94e9348af681e8c85a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin d8024f050e1f457117f914d6960a204d1af03966 2c4bc2903701f758f1c6c5aa107a3c597888e9de && git checkout -B drift-repro d8024f050e1f457117f914d6960a204d1af03966 && git merge --no-ff 2c4bc2903701f758f1c6c5aa107a3c597888e9de
node scripts/docs-audit/affected-docs.mjs --json d8024f050e1f457117f914d6960a204d1af03966

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

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

spec: name the terminally-failed run state on AutomationResult.status — contract half of #13937 (shape 4 ruling)

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat(spec): name the terminally-failed-but-repairable run 'stranded' on AutomationResult.status (#14384) - #14636

Merged
os-sam merged 1 commit into
mainfrom
claude/issue-14384-automation-result-terminal-failed-status
Sep 2, 2026
Merged

feat(spec): name the terminally-failed-but-repairable run 'stranded' on AutomationResult.status (#14384)#14636
os-sam merged 1 commit into
mainfrom
claude/issue-14384-automation-result-terminal-failed-status

Conversation

@claude

@claudeclaudeBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes#14384

Contract half of the #13937 shape-4 ruling (director seat, comment 5491060439; maintainer 2026-09-01 「其他同意」), quoted verbatim on the card and untranslated here:

  1. 命名同批定(service-automation: a resume consumes the pause BEFORE running downstream nodes, so any node that throws leaves the run terminally unresumable — and the only inspector for it reports all clear #13909 交付项 3 在此收口):该条件成为运维要操作的持久状态 ⇒ 平台级名字落 packages/spec/src/contracts/automation-service.ts(AutomationResult.status 现为 'completed' | 'paused' | 'failed')⇒ 触 spec ⇒ 条款②契约复审档位适用StrandedRunState 保持 plugin 局部报告标签不升格。

What changed — one contract increment

  • packages/spec/src/contracts/automation-service.tsAutomationResult.status is now 'completed' | 'paused' | 'failed' | 'stranded', with a JSDoc paragraph naming the condition: a resume CONSUMED the suspension, a downstream node threw, the run is recorded as failed and can be re-armed only by an explicit operator verb (never by resume, which answers RUN_NOT_FOUND for it, and never automatically). Distinct from 'failed' on purpose: that one says the run ran and was rejected; this one says a recorded continuation stopped mid-flight and an operator has something to repair. The JSDoc also states that plugin-approvals' StrandedRunState is NOT promoted.
  • packages/spec/src/api/automation-api.zod.ts — the wire mirror TriggerFlowResponseSchema.data.status carries the same four (the existing TriggerFlowDataMatchesContract pin goes red if only one side moves; see the reverse verification). The .describe() prose names the condition without an issue id: check:doc-authoring refused the first spelling (customer-facing text must not carry #NNNN), so the card references moved to an adjacent line comment.
  • packages/spec/src/contracts/automation-result-status.pin.test.ts (new) — pins, each on its own: (1) the union's exact membership at the type level (an Assert over an Eq identity, the automation-api.zod.test.ts form, compiled by check:test-typecheck, plus a satisfies list); (2) wire-to-contract parity at both levels (the Zod enum's .options read at runtime, and its inferred type bound to the contract's); (3) that the doc block above the declaration names the condition (source read); (4) a stranded envelope parses AND is preserved through the strip-mode object; (5) a literal outside the four is refused at data.status with invalid_value.
  • content/docs/references/api/automation-api.mdx — regenerated by gen:docs (check:generated --fix; the one artifact it proved stale). api-surface/ is unchanged: the snapshot records that AutomationResult exists, not its members.
  • .changeset/automation-result-stranded-status.md@objectstack/spec: minor, additive (no BREAKING declaration, so no ADR-0087 marker is owed; check:adr-0087-registration exit 0).

Not in this PR, by the card: no engine change, no operator verb, no REST route, no ExecutionStatus widening. Those are #13937 (services half, stays open) and #13953.

The literal — what was measured before choosing

Vocabularies read (packages/spec/src, packages/services/service-automation, packages/plugins/plugin-approvals):

vocabularymembersshape
AutomationResult.status (this union)completedpausedfailedsingle-word participles
ExecutionStatus (automation/execution.zod.ts, the run-row vocabulary listRuns filters on)pendingrunningpausedcompletedfailedcancelledtimed_outretryingsingle words; snake_case for two words
TaskStatus (system/worker.zod.ts)pendingqueuedprocessingcompletedfailedcancelledtimeoutdeaddead = moved to the dead-letter queue
JobExecutionStatus (system/job.zod.ts)runningsuccessfailedtimeoutdegraded
StrandedRunState (plugin-approvals, report-only)missingfailedclassifies WHY a request's run is unrecoverable
prose lineage"stranded" / "strands" / "stranding"#13807, #13909, #13937, PR #13934; inspectStrandedRequests, StrandedApprovalRequest; 8 comment sites in engine.ts

Candidates:

Producer and consumer measurements

  • AutomationResult.status is a TypeScript interface member, not Zod — verified. Its one Zod mirror in packages/spec/src is TriggerFlowResponseSchema.data.status; both updated, parity pinned.
  • Producer today: AutomationEngine.resumeInternal's catch arm records the run ROW failed and returns { success: false, error, durationMs, errorMessage, summary } — no status at all on the resumed-failure envelope (only execute()'s dispatched-and-rejected exit stamps status: 'failed'). So stamping 'stranded' on the result is the services half's, and whether the run ROW (ExecutionStatus) ever carries the word is that half's to measure; this PR does not widen ExecutionStatus.
  • Exhaustive switches over AutomationResult.status in packages/{runtime,services,plugins,rest,cli,client}: zero. The only case 'completed' | 'paused' | 'failed' outside spec is plugin-approvals' classifyStrandedRunState, which switches over the run ROW's status string with a deliberately silent default, not over this union. Narrow re-declarations of the three-member union outside spec: zero in code (two in comments, engine.ts:4182 and approval-service.ts:315).
  • Sibling repo objectui: app-shell/src/utils/flowResponse.ts:108 types status?: 'completed' | 'paused' | 'failed' | string and branches on success === false — tolerant of the new literal; nothing to change there for this half.
  • No consumer needed even a type-only accommodation.

Verification — all readings on head 2c4bc29, exit codes captured before any pipe

@objectstack/spec:

  • pnpm --filter @objectstack/spec build — exit 0 (check-dts-emitted: 34/34 declared declaration file(s) present).
  • pnpm --filter @objectstack/spec test (whole suite) — Test Files 453 passed | 1 skipped (454) · Tests 12214 passed | 1 skipped (12215) on the committed head. The one skip is scripts/root-entry-type-nameability.pin.test.ts: its inspectDistFreshness predicate saw dist older than src because the reverse verification below touched the contract file's mtime after the build (content byte-identical), and that pin skips by default on a stale dist. After rebuilding the dist that file re-ran on the same head: Test Files 1 passed (1) · Tests 2 passed (2); check:api-surface on the rebuilt dist: exit 0. The earlier full run on the pre-.describe()-edit tree with a fresh dist was 454 passed · 12216 passed, 0 skipped.
  • pnpm --filter @objectstack/spec typecheck — exit 0; check:test-typecheck: OK — 54 file(s) / 262 error(s) / 146 pinned signature(s) (ledger unchanged; the new pin file compiles under tsconfig.test.json).
  • The pin file with its two neighbours (contracts/automation-service.test.ts, api/automation-api.zod.test.ts) via exec vitest run --maxWorkers=2Test Files 3 passed (3) · Tests 61 passed (61).
  • check:generated — exit 0 after --fix regenerated the one stale artifact; check:api-surface exit 0; the 15 spec-scoped gates dispatch-gates.mjs derived (api-surface, authorable-surface, browser-reachable-entries, docs, dual-source-exports, empty-state, entry-nameability, export-origins, exported-any, liveness, llms-txt, skill-refs, strictness-ledger, variant-docs, yaml-examples) — every one exit 0.

Root gates, derived with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack from the merge-base change set (the derivation named this checkout at a5b95ee):

  • exit 0: adr-0087-registration, changeset-no-major, empty-changeset, ci-filter-parity, comment-mask-adoption, cross-package-test-inputs (both spellings), keyed-text-bounds, plugin-teardown-shape, shard-attestation, system-context-census, undeclared-dep-imports, docs-audit/check-affected-docs, docs-audit/check-drift-comment, pm/release-rehearsal-clone --self-test, changeset-gate-self-tests, dispatcher-error-vocabulary, engine-double-contract, logger-receiver-detach, merge-driver, objectql-double-limit, objectui-changeset, page-declaration-shape, pm-half-states, published-files, query-options-erasure, slot-lookup, spec-parsed-alias, test-source-alias, type-check-coverage, type-source-resolution, where-matcher, @objectstack/lint check:doc-formula-expressions (exit 0 once the closure was built), pnpm check:doc-authoring (14504 customer-facing string(s) across 710 spec sources clean), pnpm check:nul-bytes (scanned 7981 text file(s) … no raw ASCII control bytes).
  • NOT MEASURED, never green: check-test-completeness (exit 3, PREREQUISITE NOT MET), check-dual-build-cjs-loads (exit 3: needs every package's dist), check:type-check-debt (exit 3: --re-measure needs the full ledgered closure), check-dev-prereqs (exit 1: asks for a full pnpm build), pm/check-half-states (timed out at 240 s on the GitHub network leg — a PM-side gate). CI owns these.

Consumers — filter direction is DOWNSTREAM of spec; their dependency closures were built first (turbo run build over the ^... of each, 55 tasks, exit 0):

  • @objectstack/runtimetypecheck exit 0; the 6 test files that read AutomationResult (flow-dispatch-status, automation-trigger-route-status, automation-trigger-paused-run, action-activation-dispatch, endpoint-flow-dispatch-status, actions-flow-dispatch-status) — Test Files 5 passed (5) · Tests 81 passed (81) (one pattern matched no file on its own; narrowed to these files, declared).
  • @objectstack/service-automation — has no typecheck script (types NOT MEASURED here; the package is ledgered by check:type-check-coverage, exit 0); the 9 test files that read AutomationResultTest Files 9 passed (9) · Tests 239 passed (239) (narrowed, declared).
  • @objectstack/plugin-approvalstypecheck exit 0 (check:test-typecheck: OK); whole testTest Files 36 passed (36) · Tests 662 passed (662).
  • @objectstack/clienttypecheck exit 0 (check:test-typecheck: OK); whole testTest Files 32 passed (32) · Tests 420 passed (420).
  • @objectstack/verifytypecheck exit 0; automation-trigger-terminal-messages.test.tsTest Files 1 passed (1) · Tests 4 passed (4).
  • @objectstack/resttypecheck exit 0 (check:test-typecheck: OK); @objectstack/clitypecheck exit 0. Neither imports AutomationResult.
  • @objectstack/dogfood — NOT MEASURED locally (its closure is the whole app); CI.

Reverse verification — from the committed state

Mutation leg: the contract union narrowed back to three members on disk (confirmed by anchor counts: four-member 0 / three-member 1; git diff --stat 1 file, 1 insertion, 1 deletion), then tsc --noEmit -p packages/spec/tsconfig.test.json: exit 2 with 266 error lines against the ledgered 262 on the restored tree — the 4 new ones name exactly the two pins: src/api/automation-api.zod.test.ts(52,54) (TriggerFlowDataMatchesContract, TS2344) and src/contracts/automation-result-status.pin.test.ts at lines 64 (TS2322, 'stranded' not assignable), 72 and 74 (TS2344). Direction: red, as predicted. Restore leg: git checkout HEAD -- FILE from a trap with an absolute path, proven by git diff HEAD --quiet exit 0 and git hash-object equal to the HEAD blob 85b3d15a….

Review tier

needs:contract-review — clause ②, contract tier, as the ruling says in as many words. Draft; no seat marks it ready or arms auto-merge.

🤖 Generated with Claude Code

Generated by Claude Code


Generated by Claude Code

…onResult.status as 'stranded'
Contract half of the #13937 shape-4 ruling (maintainer 2026-09-01): the run
whose resume consumed its suspension and then had a downstream node throw is
recorded as failed and can be re-armed only by an explicit operator verb.
`AutomationResult.status` now carries `'stranded'` beside
`'completed' | 'paused' | 'failed'`, the wire mirror
`TriggerFlowResponseSchema.data.status` carries the same four, and
`contracts/automation-result-status.pin.test.ts` binds the two at the type
level and the value level and reads the JSDoc that names the condition.
No engine, route or client behaviour changes; plugin-approvals'
report-only `StrandedRunState` is deliberately not promoted.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017RbbUMnxkUnWhE4j94v8FE
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 3 documentable anchor(s).

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

  • content/docs/api/client-sdk.mdx(via AutomationResult (symbol, a top-level interface))
  • content/docs/automation/flows.mdx(via AutomationResult (symbol, a top-level interface))

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

  • content/docs/releases/v17.mdx(via AutomationResult (symbol, a top-level interface))

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
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 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 — 128 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 d8024f050e1f457117f914d6960a204d1af03966packageMentionDocs.

Which tree this was computed on

This run read content/docs from fcdd5f04c1fffa48b2df2a94e9348af681e8c85a — the merge of head 2c4bc2903701f758f1c6c5aa107a3c597888e9de into base d8024f050e1f457117f914d6960a204d1af03966, 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 fcdd5f04c1fffa48b2df2a94e9348af681e8c85a && git checkout fcdd5f04c1fffa48b2df2a94e9348af681e8c85a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin d8024f050e1f457117f914d6960a204d1af03966 2c4bc2903701f758f1c6c5aa107a3c597888e9de && git checkout -B drift-repro d8024f050e1f457117f914d6960a204d1af03966 && git merge --no-ff 2c4bc2903701f758f1c6c5aa107a3c597888e9de
node scripts/docs-audit/affected-docs.mjs --json d8024f050e1f457117f914d6960a204d1af03966

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

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

spec: name the terminally-failed run state on AutomationResult.status — contract half of #13937 (shape 4 ruling)

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(spec): name the terminally-failed-but-repairable run 'stranded' on AutomationResult.status (#14384) - #14636

Merged
os-sam merged 1 commit into
mainfrom
claude/issue-14384-automation-result-terminal-failed-status
Sep 2, 2026
Merged

feat(spec): name the terminally-failed-but-repairable run 'stranded' on AutomationResult.status (#14384)#14636
os-sam merged 1 commit into
mainfrom
claude/issue-14384-automation-result-terminal-failed-status

Conversation

@claude

@claudeclaudeBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes#14384

Contract half of the #13937 shape-4 ruling (director seat, comment 5491060439; maintainer 2026-09-01 「其他同意」), quoted verbatim on the card and untranslated here:

  1. 命名同批定(service-automation: a resume consumes the pause BEFORE running downstream nodes, so any node that throws leaves the run terminally unresumable — and the only inspector for it reports all clear #13909 交付项 3 在此收口):该条件成为运维要操作的持久状态 ⇒ 平台级名字落 packages/spec/src/contracts/automation-service.ts(AutomationResult.status 现为 'completed' | 'paused' | 'failed')⇒ 触 spec ⇒ 条款②契约复审档位适用StrandedRunState 保持 plugin 局部报告标签不升格。

What changed — one contract increment

  • packages/spec/src/contracts/automation-service.tsAutomationResult.status is now 'completed' | 'paused' | 'failed' | 'stranded', with a JSDoc paragraph naming the condition: a resume CONSUMED the suspension, a downstream node threw, the run is recorded as failed and can be re-armed only by an explicit operator verb (never by resume, which answers RUN_NOT_FOUND for it, and never automatically). Distinct from 'failed' on purpose: that one says the run ran and was rejected; this one says a recorded continuation stopped mid-flight and an operator has something to repair. The JSDoc also states that plugin-approvals' StrandedRunState is NOT promoted.
  • packages/spec/src/api/automation-api.zod.ts — the wire mirror TriggerFlowResponseSchema.data.status carries the same four (the existing TriggerFlowDataMatchesContract pin goes red if only one side moves; see the reverse verification). The .describe() prose names the condition without an issue id: check:doc-authoring refused the first spelling (customer-facing text must not carry #NNNN), so the card references moved to an adjacent line comment.
  • packages/spec/src/contracts/automation-result-status.pin.test.ts (new) — pins, each on its own: (1) the union's exact membership at the type level (an Assert over an Eq identity, the automation-api.zod.test.ts form, compiled by check:test-typecheck, plus a satisfies list); (2) wire-to-contract parity at both levels (the Zod enum's .options read at runtime, and its inferred type bound to the contract's); (3) that the doc block above the declaration names the condition (source read); (4) a stranded envelope parses AND is preserved through the strip-mode object; (5) a literal outside the four is refused at data.status with invalid_value.
  • content/docs/references/api/automation-api.mdx — regenerated by gen:docs (check:generated --fix; the one artifact it proved stale). api-surface/ is unchanged: the snapshot records that AutomationResult exists, not its members.
  • .changeset/automation-result-stranded-status.md@objectstack/spec: minor, additive (no BREAKING declaration, so no ADR-0087 marker is owed; check:adr-0087-registration exit 0).

Not in this PR, by the card: no engine change, no operator verb, no REST route, no ExecutionStatus widening. Those are #13937 (services half, stays open) and #13953.

The literal — what was measured before choosing

Vocabularies read (packages/spec/src, packages/services/service-automation, packages/plugins/plugin-approvals):

vocabularymembersshape
AutomationResult.status (this union)completedpausedfailedsingle-word participles
ExecutionStatus (automation/execution.zod.ts, the run-row vocabulary listRuns filters on)pendingrunningpausedcompletedfailedcancelledtimed_outretryingsingle words; snake_case for two words
TaskStatus (system/worker.zod.ts)pendingqueuedprocessingcompletedfailedcancelledtimeoutdeaddead = moved to the dead-letter queue
JobExecutionStatus (system/job.zod.ts)runningsuccessfailedtimeoutdegraded
StrandedRunState (plugin-approvals, report-only)missingfailedclassifies WHY a request's run is unrecoverable
prose lineage"stranded" / "strands" / "stranding"#13807, #13909, #13937, PR #13934; inspectStrandedRequests, StrandedApprovalRequest; 8 comment sites in engine.ts

Candidates:

Producer and consumer measurements

  • AutomationResult.status is a TypeScript interface member, not Zod — verified. Its one Zod mirror in packages/spec/src is TriggerFlowResponseSchema.data.status; both updated, parity pinned.
  • Producer today: AutomationEngine.resumeInternal's catch arm records the run ROW failed and returns { success: false, error, durationMs, errorMessage, summary } — no status at all on the resumed-failure envelope (only execute()'s dispatched-and-rejected exit stamps status: 'failed'). So stamping 'stranded' on the result is the services half's, and whether the run ROW (ExecutionStatus) ever carries the word is that half's to measure; this PR does not widen ExecutionStatus.
  • Exhaustive switches over AutomationResult.status in packages/{runtime,services,plugins,rest,cli,client}: zero. The only case 'completed' | 'paused' | 'failed' outside spec is plugin-approvals' classifyStrandedRunState, which switches over the run ROW's status string with a deliberately silent default, not over this union. Narrow re-declarations of the three-member union outside spec: zero in code (two in comments, engine.ts:4182 and approval-service.ts:315).
  • Sibling repo objectui: app-shell/src/utils/flowResponse.ts:108 types status?: 'completed' | 'paused' | 'failed' | string and branches on success === false — tolerant of the new literal; nothing to change there for this half.
  • No consumer needed even a type-only accommodation.

Verification — all readings on head 2c4bc29, exit codes captured before any pipe

@objectstack/spec:

  • pnpm --filter @objectstack/spec build — exit 0 (check-dts-emitted: 34/34 declared declaration file(s) present).
  • pnpm --filter @objectstack/spec test (whole suite) — Test Files 453 passed | 1 skipped (454) · Tests 12214 passed | 1 skipped (12215) on the committed head. The one skip is scripts/root-entry-type-nameability.pin.test.ts: its inspectDistFreshness predicate saw dist older than src because the reverse verification below touched the contract file's mtime after the build (content byte-identical), and that pin skips by default on a stale dist. After rebuilding the dist that file re-ran on the same head: Test Files 1 passed (1) · Tests 2 passed (2); check:api-surface on the rebuilt dist: exit 0. The earlier full run on the pre-.describe()-edit tree with a fresh dist was 454 passed · 12216 passed, 0 skipped.
  • pnpm --filter @objectstack/spec typecheck — exit 0; check:test-typecheck: OK — 54 file(s) / 262 error(s) / 146 pinned signature(s) (ledger unchanged; the new pin file compiles under tsconfig.test.json).
  • The pin file with its two neighbours (contracts/automation-service.test.ts, api/automation-api.zod.test.ts) via exec vitest run --maxWorkers=2Test Files 3 passed (3) · Tests 61 passed (61).
  • check:generated — exit 0 after --fix regenerated the one stale artifact; check:api-surface exit 0; the 15 spec-scoped gates dispatch-gates.mjs derived (api-surface, authorable-surface, browser-reachable-entries, docs, dual-source-exports, empty-state, entry-nameability, export-origins, exported-any, liveness, llms-txt, skill-refs, strictness-ledger, variant-docs, yaml-examples) — every one exit 0.

Root gates, derived with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack from the merge-base change set (the derivation named this checkout at a5b95ee):

  • exit 0: adr-0087-registration, changeset-no-major, empty-changeset, ci-filter-parity, comment-mask-adoption, cross-package-test-inputs (both spellings), keyed-text-bounds, plugin-teardown-shape, shard-attestation, system-context-census, undeclared-dep-imports, docs-audit/check-affected-docs, docs-audit/check-drift-comment, pm/release-rehearsal-clone --self-test, changeset-gate-self-tests, dispatcher-error-vocabulary, engine-double-contract, logger-receiver-detach, merge-driver, objectql-double-limit, objectui-changeset, page-declaration-shape, pm-half-states, published-files, query-options-erasure, slot-lookup, spec-parsed-alias, test-source-alias, type-check-coverage, type-source-resolution, where-matcher, @objectstack/lint check:doc-formula-expressions (exit 0 once the closure was built), pnpm check:doc-authoring (14504 customer-facing string(s) across 710 spec sources clean), pnpm check:nul-bytes (scanned 7981 text file(s) … no raw ASCII control bytes).
  • NOT MEASURED, never green: check-test-completeness (exit 3, PREREQUISITE NOT MET), check-dual-build-cjs-loads (exit 3: needs every package's dist), check:type-check-debt (exit 3: --re-measure needs the full ledgered closure), check-dev-prereqs (exit 1: asks for a full pnpm build), pm/check-half-states (timed out at 240 s on the GitHub network leg — a PM-side gate). CI owns these.

Consumers — filter direction is DOWNSTREAM of spec; their dependency closures were built first (turbo run build over the ^... of each, 55 tasks, exit 0):

  • @objectstack/runtimetypecheck exit 0; the 6 test files that read AutomationResult (flow-dispatch-status, automation-trigger-route-status, automation-trigger-paused-run, action-activation-dispatch, endpoint-flow-dispatch-status, actions-flow-dispatch-status) — Test Files 5 passed (5) · Tests 81 passed (81) (one pattern matched no file on its own; narrowed to these files, declared).
  • @objectstack/service-automation — has no typecheck script (types NOT MEASURED here; the package is ledgered by check:type-check-coverage, exit 0); the 9 test files that read AutomationResultTest Files 9 passed (9) · Tests 239 passed (239) (narrowed, declared).
  • @objectstack/plugin-approvalstypecheck exit 0 (check:test-typecheck: OK); whole testTest Files 36 passed (36) · Tests 662 passed (662).
  • @objectstack/clienttypecheck exit 0 (check:test-typecheck: OK); whole testTest Files 32 passed (32) · Tests 420 passed (420).
  • @objectstack/verifytypecheck exit 0; automation-trigger-terminal-messages.test.tsTest Files 1 passed (1) · Tests 4 passed (4).
  • @objectstack/resttypecheck exit 0 (check:test-typecheck: OK); @objectstack/clitypecheck exit 0. Neither imports AutomationResult.
  • @objectstack/dogfood — NOT MEASURED locally (its closure is the whole app); CI.

Reverse verification — from the committed state

Mutation leg: the contract union narrowed back to three members on disk (confirmed by anchor counts: four-member 0 / three-member 1; git diff --stat 1 file, 1 insertion, 1 deletion), then tsc --noEmit -p packages/spec/tsconfig.test.json: exit 2 with 266 error lines against the ledgered 262 on the restored tree — the 4 new ones name exactly the two pins: src/api/automation-api.zod.test.ts(52,54) (TriggerFlowDataMatchesContract, TS2344) and src/contracts/automation-result-status.pin.test.ts at lines 64 (TS2322, 'stranded' not assignable), 72 and 74 (TS2344). Direction: red, as predicted. Restore leg: git checkout HEAD -- FILE from a trap with an absolute path, proven by git diff HEAD --quiet exit 0 and git hash-object equal to the HEAD blob 85b3d15a….

Review tier

needs:contract-review — clause ②, contract tier, as the ruling says in as many words. Draft; no seat marks it ready or arms auto-merge.

🤖 Generated with Claude Code

Generated by Claude Code


Generated by Claude Code

…onResult.status as 'stranded'
Contract half of the #13937 shape-4 ruling (maintainer 2026-09-01): the run
whose resume consumed its suspension and then had a downstream node throw is
recorded as failed and can be re-armed only by an explicit operator verb.
`AutomationResult.status` now carries `'stranded'` beside
`'completed' | 'paused' | 'failed'`, the wire mirror
`TriggerFlowResponseSchema.data.status` carries the same four, and
`contracts/automation-result-status.pin.test.ts` binds the two at the type
level and the value level and reads the JSDoc that names the condition.
No engine, route or client behaviour changes; plugin-approvals'
report-only `StrandedRunState` is deliberately not promoted.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017RbbUMnxkUnWhE4j94v8FE
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 3 documentable anchor(s).

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

  • content/docs/api/client-sdk.mdx(via AutomationResult (symbol, a top-level interface))
  • content/docs/automation/flows.mdx(via AutomationResult (symbol, a top-level interface))

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

  • content/docs/releases/v17.mdx(via AutomationResult (symbol, a top-level interface))

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
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 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 — 128 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 d8024f050e1f457117f914d6960a204d1af03966packageMentionDocs.

Which tree this was computed on

This run read content/docs from fcdd5f04c1fffa48b2df2a94e9348af681e8c85a — the merge of head 2c4bc2903701f758f1c6c5aa107a3c597888e9de into base d8024f050e1f457117f914d6960a204d1af03966, 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 fcdd5f04c1fffa48b2df2a94e9348af681e8c85a && git checkout fcdd5f04c1fffa48b2df2a94e9348af681e8c85a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin d8024f050e1f457117f914d6960a204d1af03966 2c4bc2903701f758f1c6c5aa107a3c597888e9de && git checkout -B drift-repro d8024f050e1f457117f914d6960a204d1af03966 && git merge --no-ff 2c4bc2903701f758f1c6c5aa107a3c597888e9de
node scripts/docs-audit/affected-docs.mjs --json d8024f050e1f457117f914d6960a204d1af03966

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

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

spec: name the terminally-failed run state on AutomationResult.status — contract half of #13937 (shape 4 ruling)

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(spec): name the terminally-failed-but-repairable run 'stranded' on AutomationResult.status (#14384) - #14636

Merged
os-sam merged 1 commit into
mainfrom
claude/issue-14384-automation-result-terminal-failed-status
Sep 2, 2026
Merged

feat(spec): name the terminally-failed-but-repairable run 'stranded' on AutomationResult.status (#14384)#14636
os-sam merged 1 commit into
mainfrom
claude/issue-14384-automation-result-terminal-failed-status

Conversation

@claude

@claudeclaudeBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes#14384

Contract half of the #13937 shape-4 ruling (director seat, comment 5491060439; maintainer 2026-09-01 「其他同意」), quoted verbatim on the card and untranslated here:

  1. 命名同批定(service-automation: a resume consumes the pause BEFORE running downstream nodes, so any node that throws leaves the run terminally unresumable — and the only inspector for it reports all clear #13909 交付项 3 在此收口):该条件成为运维要操作的持久状态 ⇒ 平台级名字落 packages/spec/src/contracts/automation-service.ts(AutomationResult.status 现为 'completed' | 'paused' | 'failed')⇒ 触 spec ⇒ 条款②契约复审档位适用StrandedRunState 保持 plugin 局部报告标签不升格。

What changed — one contract increment

  • packages/spec/src/contracts/automation-service.tsAutomationResult.status is now 'completed' | 'paused' | 'failed' | 'stranded', with a JSDoc paragraph naming the condition: a resume CONSUMED the suspension, a downstream node threw, the run is recorded as failed and can be re-armed only by an explicit operator verb (never by resume, which answers RUN_NOT_FOUND for it, and never automatically). Distinct from 'failed' on purpose: that one says the run ran and was rejected; this one says a recorded continuation stopped mid-flight and an operator has something to repair. The JSDoc also states that plugin-approvals' StrandedRunState is NOT promoted.
  • packages/spec/src/api/automation-api.zod.ts — the wire mirror TriggerFlowResponseSchema.data.status carries the same four (the existing TriggerFlowDataMatchesContract pin goes red if only one side moves; see the reverse verification). The .describe() prose names the condition without an issue id: check:doc-authoring refused the first spelling (customer-facing text must not carry #NNNN), so the card references moved to an adjacent line comment.
  • packages/spec/src/contracts/automation-result-status.pin.test.ts (new) — pins, each on its own: (1) the union's exact membership at the type level (an Assert over an Eq identity, the automation-api.zod.test.ts form, compiled by check:test-typecheck, plus a satisfies list); (2) wire-to-contract parity at both levels (the Zod enum's .options read at runtime, and its inferred type bound to the contract's); (3) that the doc block above the declaration names the condition (source read); (4) a stranded envelope parses AND is preserved through the strip-mode object; (5) a literal outside the four is refused at data.status with invalid_value.
  • content/docs/references/api/automation-api.mdx — regenerated by gen:docs (check:generated --fix; the one artifact it proved stale). api-surface/ is unchanged: the snapshot records that AutomationResult exists, not its members.
  • .changeset/automation-result-stranded-status.md@objectstack/spec: minor, additive (no BREAKING declaration, so no ADR-0087 marker is owed; check:adr-0087-registration exit 0).

Not in this PR, by the card: no engine change, no operator verb, no REST route, no ExecutionStatus widening. Those are #13937 (services half, stays open) and #13953.

The literal — what was measured before choosing

Vocabularies read (packages/spec/src, packages/services/service-automation, packages/plugins/plugin-approvals):

vocabularymembersshape
AutomationResult.status (this union)completedpausedfailedsingle-word participles
ExecutionStatus (automation/execution.zod.ts, the run-row vocabulary listRuns filters on)pendingrunningpausedcompletedfailedcancelledtimed_outretryingsingle words; snake_case for two words
TaskStatus (system/worker.zod.ts)pendingqueuedprocessingcompletedfailedcancelledtimeoutdeaddead = moved to the dead-letter queue
JobExecutionStatus (system/job.zod.ts)runningsuccessfailedtimeoutdegraded
StrandedRunState (plugin-approvals, report-only)missingfailedclassifies WHY a request's run is unrecoverable
prose lineage"stranded" / "strands" / "stranding"#13807, #13909, #13937, PR #13934; inspectStrandedRequests, StrandedApprovalRequest; 8 comment sites in engine.ts

Candidates:

Producer and consumer measurements

  • AutomationResult.status is a TypeScript interface member, not Zod — verified. Its one Zod mirror in packages/spec/src is TriggerFlowResponseSchema.data.status; both updated, parity pinned.
  • Producer today: AutomationEngine.resumeInternal's catch arm records the run ROW failed and returns { success: false, error, durationMs, errorMessage, summary } — no status at all on the resumed-failure envelope (only execute()'s dispatched-and-rejected exit stamps status: 'failed'). So stamping 'stranded' on the result is the services half's, and whether the run ROW (ExecutionStatus) ever carries the word is that half's to measure; this PR does not widen ExecutionStatus.
  • Exhaustive switches over AutomationResult.status in packages/{runtime,services,plugins,rest,cli,client}: zero. The only case 'completed' | 'paused' | 'failed' outside spec is plugin-approvals' classifyStrandedRunState, which switches over the run ROW's status string with a deliberately silent default, not over this union. Narrow re-declarations of the three-member union outside spec: zero in code (two in comments, engine.ts:4182 and approval-service.ts:315).
  • Sibling repo objectui: app-shell/src/utils/flowResponse.ts:108 types status?: 'completed' | 'paused' | 'failed' | string and branches on success === false — tolerant of the new literal; nothing to change there for this half.
  • No consumer needed even a type-only accommodation.

Verification — all readings on head 2c4bc29, exit codes captured before any pipe

@objectstack/spec:

  • pnpm --filter @objectstack/spec build — exit 0 (check-dts-emitted: 34/34 declared declaration file(s) present).
  • pnpm --filter @objectstack/spec test (whole suite) — Test Files 453 passed | 1 skipped (454) · Tests 12214 passed | 1 skipped (12215) on the committed head. The one skip is scripts/root-entry-type-nameability.pin.test.ts: its inspectDistFreshness predicate saw dist older than src because the reverse verification below touched the contract file's mtime after the build (content byte-identical), and that pin skips by default on a stale dist. After rebuilding the dist that file re-ran on the same head: Test Files 1 passed (1) · Tests 2 passed (2); check:api-surface on the rebuilt dist: exit 0. The earlier full run on the pre-.describe()-edit tree with a fresh dist was 454 passed · 12216 passed, 0 skipped.
  • pnpm --filter @objectstack/spec typecheck — exit 0; check:test-typecheck: OK — 54 file(s) / 262 error(s) / 146 pinned signature(s) (ledger unchanged; the new pin file compiles under tsconfig.test.json).
  • The pin file with its two neighbours (contracts/automation-service.test.ts, api/automation-api.zod.test.ts) via exec vitest run --maxWorkers=2Test Files 3 passed (3) · Tests 61 passed (61).
  • check:generated — exit 0 after --fix regenerated the one stale artifact; check:api-surface exit 0; the 15 spec-scoped gates dispatch-gates.mjs derived (api-surface, authorable-surface, browser-reachable-entries, docs, dual-source-exports, empty-state, entry-nameability, export-origins, exported-any, liveness, llms-txt, skill-refs, strictness-ledger, variant-docs, yaml-examples) — every one exit 0.

Root gates, derived with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack from the merge-base change set (the derivation named this checkout at a5b95ee):

  • exit 0: adr-0087-registration, changeset-no-major, empty-changeset, ci-filter-parity, comment-mask-adoption, cross-package-test-inputs (both spellings), keyed-text-bounds, plugin-teardown-shape, shard-attestation, system-context-census, undeclared-dep-imports, docs-audit/check-affected-docs, docs-audit/check-drift-comment, pm/release-rehearsal-clone --self-test, changeset-gate-self-tests, dispatcher-error-vocabulary, engine-double-contract, logger-receiver-detach, merge-driver, objectql-double-limit, objectui-changeset, page-declaration-shape, pm-half-states, published-files, query-options-erasure, slot-lookup, spec-parsed-alias, test-source-alias, type-check-coverage, type-source-resolution, where-matcher, @objectstack/lint check:doc-formula-expressions (exit 0 once the closure was built), pnpm check:doc-authoring (14504 customer-facing string(s) across 710 spec sources clean), pnpm check:nul-bytes (scanned 7981 text file(s) … no raw ASCII control bytes).
  • NOT MEASURED, never green: check-test-completeness (exit 3, PREREQUISITE NOT MET), check-dual-build-cjs-loads (exit 3: needs every package's dist), check:type-check-debt (exit 3: --re-measure needs the full ledgered closure), check-dev-prereqs (exit 1: asks for a full pnpm build), pm/check-half-states (timed out at 240 s on the GitHub network leg — a PM-side gate). CI owns these.

Consumers — filter direction is DOWNSTREAM of spec; their dependency closures were built first (turbo run build over the ^... of each, 55 tasks, exit 0):

  • @objectstack/runtimetypecheck exit 0; the 6 test files that read AutomationResult (flow-dispatch-status, automation-trigger-route-status, automation-trigger-paused-run, action-activation-dispatch, endpoint-flow-dispatch-status, actions-flow-dispatch-status) — Test Files 5 passed (5) · Tests 81 passed (81) (one pattern matched no file on its own; narrowed to these files, declared).
  • @objectstack/service-automation — has no typecheck script (types NOT MEASURED here; the package is ledgered by check:type-check-coverage, exit 0); the 9 test files that read AutomationResultTest Files 9 passed (9) · Tests 239 passed (239) (narrowed, declared).
  • @objectstack/plugin-approvalstypecheck exit 0 (check:test-typecheck: OK); whole testTest Files 36 passed (36) · Tests 662 passed (662).
  • @objectstack/clienttypecheck exit 0 (check:test-typecheck: OK); whole testTest Files 32 passed (32) · Tests 420 passed (420).
  • @objectstack/verifytypecheck exit 0; automation-trigger-terminal-messages.test.tsTest Files 1 passed (1) · Tests 4 passed (4).
  • @objectstack/resttypecheck exit 0 (check:test-typecheck: OK); @objectstack/clitypecheck exit 0. Neither imports AutomationResult.
  • @objectstack/dogfood — NOT MEASURED locally (its closure is the whole app); CI.

Reverse verification — from the committed state

Mutation leg: the contract union narrowed back to three members on disk (confirmed by anchor counts: four-member 0 / three-member 1; git diff --stat 1 file, 1 insertion, 1 deletion), then tsc --noEmit -p packages/spec/tsconfig.test.json: exit 2 with 266 error lines against the ledgered 262 on the restored tree — the 4 new ones name exactly the two pins: src/api/automation-api.zod.test.ts(52,54) (TriggerFlowDataMatchesContract, TS2344) and src/contracts/automation-result-status.pin.test.ts at lines 64 (TS2322, 'stranded' not assignable), 72 and 74 (TS2344). Direction: red, as predicted. Restore leg: git checkout HEAD -- FILE from a trap with an absolute path, proven by git diff HEAD --quiet exit 0 and git hash-object equal to the HEAD blob 85b3d15a….

Review tier

needs:contract-review — clause ②, contract tier, as the ruling says in as many words. Draft; no seat marks it ready or arms auto-merge.

🤖 Generated with Claude Code

Generated by Claude Code


Generated by Claude Code

…onResult.status as 'stranded'
Contract half of the #13937 shape-4 ruling (maintainer 2026-09-01): the run
whose resume consumed its suspension and then had a downstream node throw is
recorded as failed and can be re-armed only by an explicit operator verb.
`AutomationResult.status` now carries `'stranded'` beside
`'completed' | 'paused' | 'failed'`, the wire mirror
`TriggerFlowResponseSchema.data.status` carries the same four, and
`contracts/automation-result-status.pin.test.ts` binds the two at the type
level and the value level and reads the JSDoc that names the condition.
No engine, route or client behaviour changes; plugin-approvals'
report-only `StrandedRunState` is deliberately not promoted.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017RbbUMnxkUnWhE4j94v8FE
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 3 documentable anchor(s).

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

  • content/docs/api/client-sdk.mdx(via AutomationResult (symbol, a top-level interface))
  • content/docs/automation/flows.mdx(via AutomationResult (symbol, a top-level interface))

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

  • content/docs/releases/v17.mdx(via AutomationResult (symbol, a top-level interface))

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
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 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 — 128 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 d8024f050e1f457117f914d6960a204d1af03966packageMentionDocs.

Which tree this was computed on

This run read content/docs from fcdd5f04c1fffa48b2df2a94e9348af681e8c85a — the merge of head 2c4bc2903701f758f1c6c5aa107a3c597888e9de into base d8024f050e1f457117f914d6960a204d1af03966, 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 fcdd5f04c1fffa48b2df2a94e9348af681e8c85a && git checkout fcdd5f04c1fffa48b2df2a94e9348af681e8c85a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin d8024f050e1f457117f914d6960a204d1af03966 2c4bc2903701f758f1c6c5aa107a3c597888e9de && git checkout -B drift-repro d8024f050e1f457117f914d6960a204d1af03966 && git merge --no-ff 2c4bc2903701f758f1c6c5aa107a3c597888e9de
node scripts/docs-audit/affected-docs.mjs --json d8024f050e1f457117f914d6960a204d1af03966

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

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

spec: name the terminally-failed run state on AutomationResult.status — contract half of #13937 (shape 4 ruling)

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat(spec): name the terminally-failed-but-repairable run 'stranded' on AutomationResult.status (#14384) - #14636

Merged
os-sam merged 1 commit into
mainfrom
claude/issue-14384-automation-result-terminal-failed-status
Sep 2, 2026
Merged

feat(spec): name the terminally-failed-but-repairable run 'stranded' on AutomationResult.status (#14384)#14636
os-sam merged 1 commit into
mainfrom
claude/issue-14384-automation-result-terminal-failed-status

Conversation

@claude

@claudeclaudeBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes#14384

Contract half of the #13937 shape-4 ruling (director seat, comment 5491060439; maintainer 2026-09-01 「其他同意」), quoted verbatim on the card and untranslated here:

  1. 命名同批定(service-automation: a resume consumes the pause BEFORE running downstream nodes, so any node that throws leaves the run terminally unresumable — and the only inspector for it reports all clear #13909 交付项 3 在此收口):该条件成为运维要操作的持久状态 ⇒ 平台级名字落 packages/spec/src/contracts/automation-service.ts(AutomationResult.status 现为 'completed' | 'paused' | 'failed')⇒ 触 spec ⇒ 条款②契约复审档位适用StrandedRunState 保持 plugin 局部报告标签不升格。

What changed — one contract increment

  • packages/spec/src/contracts/automation-service.tsAutomationResult.status is now 'completed' | 'paused' | 'failed' | 'stranded', with a JSDoc paragraph naming the condition: a resume CONSUMED the suspension, a downstream node threw, the run is recorded as failed and can be re-armed only by an explicit operator verb (never by resume, which answers RUN_NOT_FOUND for it, and never automatically). Distinct from 'failed' on purpose: that one says the run ran and was rejected; this one says a recorded continuation stopped mid-flight and an operator has something to repair. The JSDoc also states that plugin-approvals' StrandedRunState is NOT promoted.
  • packages/spec/src/api/automation-api.zod.ts — the wire mirror TriggerFlowResponseSchema.data.status carries the same four (the existing TriggerFlowDataMatchesContract pin goes red if only one side moves; see the reverse verification). The .describe() prose names the condition without an issue id: check:doc-authoring refused the first spelling (customer-facing text must not carry #NNNN), so the card references moved to an adjacent line comment.
  • packages/spec/src/contracts/automation-result-status.pin.test.ts (new) — pins, each on its own: (1) the union's exact membership at the type level (an Assert over an Eq identity, the automation-api.zod.test.ts form, compiled by check:test-typecheck, plus a satisfies list); (2) wire-to-contract parity at both levels (the Zod enum's .options read at runtime, and its inferred type bound to the contract's); (3) that the doc block above the declaration names the condition (source read); (4) a stranded envelope parses AND is preserved through the strip-mode object; (5) a literal outside the four is refused at data.status with invalid_value.
  • content/docs/references/api/automation-api.mdx — regenerated by gen:docs (check:generated --fix; the one artifact it proved stale). api-surface/ is unchanged: the snapshot records that AutomationResult exists, not its members.
  • .changeset/automation-result-stranded-status.md@objectstack/spec: minor, additive (no BREAKING declaration, so no ADR-0087 marker is owed; check:adr-0087-registration exit 0).

Not in this PR, by the card: no engine change, no operator verb, no REST route, no ExecutionStatus widening. Those are #13937 (services half, stays open) and #13953.

The literal — what was measured before choosing

Vocabularies read (packages/spec/src, packages/services/service-automation, packages/plugins/plugin-approvals):

vocabularymembersshape
AutomationResult.status (this union)completedpausedfailedsingle-word participles
ExecutionStatus (automation/execution.zod.ts, the run-row vocabulary listRuns filters on)pendingrunningpausedcompletedfailedcancelledtimed_outretryingsingle words; snake_case for two words
TaskStatus (system/worker.zod.ts)pendingqueuedprocessingcompletedfailedcancelledtimeoutdeaddead = moved to the dead-letter queue
JobExecutionStatus (system/job.zod.ts)runningsuccessfailedtimeoutdegraded
StrandedRunState (plugin-approvals, report-only)missingfailedclassifies WHY a request's run is unrecoverable
prose lineage"stranded" / "strands" / "stranding"#13807, #13909, #13937, PR #13934; inspectStrandedRequests, StrandedApprovalRequest; 8 comment sites in engine.ts

Candidates:

Producer and consumer measurements

  • AutomationResult.status is a TypeScript interface member, not Zod — verified. Its one Zod mirror in packages/spec/src is TriggerFlowResponseSchema.data.status; both updated, parity pinned.
  • Producer today: AutomationEngine.resumeInternal's catch arm records the run ROW failed and returns { success: false, error, durationMs, errorMessage, summary } — no status at all on the resumed-failure envelope (only execute()'s dispatched-and-rejected exit stamps status: 'failed'). So stamping 'stranded' on the result is the services half's, and whether the run ROW (ExecutionStatus) ever carries the word is that half's to measure; this PR does not widen ExecutionStatus.
  • Exhaustive switches over AutomationResult.status in packages/{runtime,services,plugins,rest,cli,client}: zero. The only case 'completed' | 'paused' | 'failed' outside spec is plugin-approvals' classifyStrandedRunState, which switches over the run ROW's status string with a deliberately silent default, not over this union. Narrow re-declarations of the three-member union outside spec: zero in code (two in comments, engine.ts:4182 and approval-service.ts:315).
  • Sibling repo objectui: app-shell/src/utils/flowResponse.ts:108 types status?: 'completed' | 'paused' | 'failed' | string and branches on success === false — tolerant of the new literal; nothing to change there for this half.
  • No consumer needed even a type-only accommodation.

Verification — all readings on head 2c4bc29, exit codes captured before any pipe

@objectstack/spec:

  • pnpm --filter @objectstack/spec build — exit 0 (check-dts-emitted: 34/34 declared declaration file(s) present).
  • pnpm --filter @objectstack/spec test (whole suite) — Test Files 453 passed | 1 skipped (454) · Tests 12214 passed | 1 skipped (12215) on the committed head. The one skip is scripts/root-entry-type-nameability.pin.test.ts: its inspectDistFreshness predicate saw dist older than src because the reverse verification below touched the contract file's mtime after the build (content byte-identical), and that pin skips by default on a stale dist. After rebuilding the dist that file re-ran on the same head: Test Files 1 passed (1) · Tests 2 passed (2); check:api-surface on the rebuilt dist: exit 0. The earlier full run on the pre-.describe()-edit tree with a fresh dist was 454 passed · 12216 passed, 0 skipped.
  • pnpm --filter @objectstack/spec typecheck — exit 0; check:test-typecheck: OK — 54 file(s) / 262 error(s) / 146 pinned signature(s) (ledger unchanged; the new pin file compiles under tsconfig.test.json).
  • The pin file with its two neighbours (contracts/automation-service.test.ts, api/automation-api.zod.test.ts) via exec vitest run --maxWorkers=2Test Files 3 passed (3) · Tests 61 passed (61).
  • check:generated — exit 0 after --fix regenerated the one stale artifact; check:api-surface exit 0; the 15 spec-scoped gates dispatch-gates.mjs derived (api-surface, authorable-surface, browser-reachable-entries, docs, dual-source-exports, empty-state, entry-nameability, export-origins, exported-any, liveness, llms-txt, skill-refs, strictness-ledger, variant-docs, yaml-examples) — every one exit 0.

Root gates, derived with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack from the merge-base change set (the derivation named this checkout at a5b95ee):

  • exit 0: adr-0087-registration, changeset-no-major, empty-changeset, ci-filter-parity, comment-mask-adoption, cross-package-test-inputs (both spellings), keyed-text-bounds, plugin-teardown-shape, shard-attestation, system-context-census, undeclared-dep-imports, docs-audit/check-affected-docs, docs-audit/check-drift-comment, pm/release-rehearsal-clone --self-test, changeset-gate-self-tests, dispatcher-error-vocabulary, engine-double-contract, logger-receiver-detach, merge-driver, objectql-double-limit, objectui-changeset, page-declaration-shape, pm-half-states, published-files, query-options-erasure, slot-lookup, spec-parsed-alias, test-source-alias, type-check-coverage, type-source-resolution, where-matcher, @objectstack/lint check:doc-formula-expressions (exit 0 once the closure was built), pnpm check:doc-authoring (14504 customer-facing string(s) across 710 spec sources clean), pnpm check:nul-bytes (scanned 7981 text file(s) … no raw ASCII control bytes).
  • NOT MEASURED, never green: check-test-completeness (exit 3, PREREQUISITE NOT MET), check-dual-build-cjs-loads (exit 3: needs every package's dist), check:type-check-debt (exit 3: --re-measure needs the full ledgered closure), check-dev-prereqs (exit 1: asks for a full pnpm build), pm/check-half-states (timed out at 240 s on the GitHub network leg — a PM-side gate). CI owns these.

Consumers — filter direction is DOWNSTREAM of spec; their dependency closures were built first (turbo run build over the ^... of each, 55 tasks, exit 0):

  • @objectstack/runtimetypecheck exit 0; the 6 test files that read AutomationResult (flow-dispatch-status, automation-trigger-route-status, automation-trigger-paused-run, action-activation-dispatch, endpoint-flow-dispatch-status, actions-flow-dispatch-status) — Test Files 5 passed (5) · Tests 81 passed (81) (one pattern matched no file on its own; narrowed to these files, declared).
  • @objectstack/service-automation — has no typecheck script (types NOT MEASURED here; the package is ledgered by check:type-check-coverage, exit 0); the 9 test files that read AutomationResultTest Files 9 passed (9) · Tests 239 passed (239) (narrowed, declared).
  • @objectstack/plugin-approvalstypecheck exit 0 (check:test-typecheck: OK); whole testTest Files 36 passed (36) · Tests 662 passed (662).
  • @objectstack/clienttypecheck exit 0 (check:test-typecheck: OK); whole testTest Files 32 passed (32) · Tests 420 passed (420).
  • @objectstack/verifytypecheck exit 0; automation-trigger-terminal-messages.test.tsTest Files 1 passed (1) · Tests 4 passed (4).
  • @objectstack/resttypecheck exit 0 (check:test-typecheck: OK); @objectstack/clitypecheck exit 0. Neither imports AutomationResult.
  • @objectstack/dogfood — NOT MEASURED locally (its closure is the whole app); CI.

Reverse verification — from the committed state

Mutation leg: the contract union narrowed back to three members on disk (confirmed by anchor counts: four-member 0 / three-member 1; git diff --stat 1 file, 1 insertion, 1 deletion), then tsc --noEmit -p packages/spec/tsconfig.test.json: exit 2 with 266 error lines against the ledgered 262 on the restored tree — the 4 new ones name exactly the two pins: src/api/automation-api.zod.test.ts(52,54) (TriggerFlowDataMatchesContract, TS2344) and src/contracts/automation-result-status.pin.test.ts at lines 64 (TS2322, 'stranded' not assignable), 72 and 74 (TS2344). Direction: red, as predicted. Restore leg: git checkout HEAD -- FILE from a trap with an absolute path, proven by git diff HEAD --quiet exit 0 and git hash-object equal to the HEAD blob 85b3d15a….

Review tier

needs:contract-review — clause ②, contract tier, as the ruling says in as many words. Draft; no seat marks it ready or arms auto-merge.

🤖 Generated with Claude Code

Generated by Claude Code


Generated by Claude Code

…onResult.status as 'stranded'
Contract half of the #13937 shape-4 ruling (maintainer 2026-09-01): the run
whose resume consumed its suspension and then had a downstream node throw is
recorded as failed and can be re-armed only by an explicit operator verb.
`AutomationResult.status` now carries `'stranded'` beside
`'completed' | 'paused' | 'failed'`, the wire mirror
`TriggerFlowResponseSchema.data.status` carries the same four, and
`contracts/automation-result-status.pin.test.ts` binds the two at the type
level and the value level and reads the JSDoc that names the condition.
No engine, route or client behaviour changes; plugin-approvals'
report-only `StrandedRunState` is deliberately not promoted.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017RbbUMnxkUnWhE4j94v8FE
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 3 documentable anchor(s).

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

  • content/docs/api/client-sdk.mdx(via AutomationResult (symbol, a top-level interface))
  • content/docs/automation/flows.mdx(via AutomationResult (symbol, a top-level interface))

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

  • content/docs/releases/v17.mdx(via AutomationResult (symbol, a top-level interface))

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
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 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 — 128 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 d8024f050e1f457117f914d6960a204d1af03966packageMentionDocs.

Which tree this was computed on

This run read content/docs from fcdd5f04c1fffa48b2df2a94e9348af681e8c85a — the merge of head 2c4bc2903701f758f1c6c5aa107a3c597888e9de into base d8024f050e1f457117f914d6960a204d1af03966, 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 fcdd5f04c1fffa48b2df2a94e9348af681e8c85a && git checkout fcdd5f04c1fffa48b2df2a94e9348af681e8c85a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin d8024f050e1f457117f914d6960a204d1af03966 2c4bc2903701f758f1c6c5aa107a3c597888e9de && git checkout -B drift-repro d8024f050e1f457117f914d6960a204d1af03966 && git merge --no-ff 2c4bc2903701f758f1c6c5aa107a3c597888e9de
node scripts/docs-audit/affected-docs.mjs --json d8024f050e1f457117f914d6960a204d1af03966

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

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

spec: name the terminally-failed run state on AutomationResult.status — contract half of #13937 (shape 4 ruling)

2 participants

@os-sam@claude