Repository navigation
spec: name the terminally-failed run state on AutomationResult.status — contract half of #13937 (shape 4 ruling) #14384
Description
Activity
- addedpriority:p1High: required for production / M2High: required for production / M2
on Sep 2, 2026 Triage (R+98) —
priority:p1·pm:blocking·needs:contract-review.domain:specwas already right and stays.On the priority, since it is higher than the card's own weight would suggest. The contract increment itself is one union member plus a JSDoc line — small, and not urgent in isolation. It is
p1because of where it sits: #13937 ispriority:p1pm:blockedand this card is its sole gate, with #13909 blocked behind that. Ap2here would let the queue pick otherp1work ahead of the only thing that unblocks ap1chain, which is the wrong order for the same total work.pm:blockingrecords the relationship; the priority makes the queue act on it.needs:contract-reviewis applied on the issue, not left for the PR, because the ruling you quote says so in as many words — 「触 spec ⇒ 条款②契约复审档位适用」. Dispatch at the contract-review tier.Nothing else about the card needed changing. The scope is exact, the ruling is quoted verbatim with its provenance, the acceptance is executable, and the out-of-scope section correctly keeps the services half (#13937) and the operator-verb question (#13953) off it.
Generated by Claude Code
os-dev-report
{ "issue": 14384, "status": "done", "branch": "claude/issue-14384-automation-result-terminal-failed-status", "pr": "https://github.com/objectstack-ai/objectstack/pull/14636", "head": "2c4bc2903701f758f1c6c5aa107a3c597888e9de", "base": "a5b95ee6edd1e21e04cc43200f41fb730538b197 (origin/main at claim; the card's 2a26536 moved on with zero commits touching packages/spec/src/contracts/automation-service.ts between them)", "premise_still_valid": true, "literal": "stranded", "alternatives_measured": [ "dead (TaskStatus precedent = retries exhausted, dead-lettered; reads final where the ruling's point is re-armable)", "failed_resume / resume_failed (house snake_case shape, but names the consumption mechanism #13937 may still reorder, not the operator-facing state)", "unresumable (false once the re-arm verb exists)", "suspended (spec uses it for admin-suspended tenant/license/environment; collides with 'suspension' = the pause)", "stalled / stuck / halted / incident (zero uses in spec, service-automation, plugin-approvals; 'incident' is a noun)" ], "vocabularies_read": [ "AutomationResult.status: completed|paused|failed", "ExecutionStatus (automation/execution.zod.ts): pending|running|paused|completed|failed|cancelled|timed_out|retrying", "TaskStatus (system/worker.zod.ts): ...|failed|cancelled|timeout|dead", "JobExecutionStatus (system/job.zod.ts): running|success|failed|timeout|degraded", "StrandedRunState (plugin-approvals, report-only): missing|failed — NOT promoted, stated in the JSDoc", "prose lineage: 'stranded' in #13807/#13909/#13937/PR #13934, inspectStrandedRequests, StrandedApprovalRequest, 8 comment sites in engine.ts" ], "summary": "One contract increment: AutomationResult.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; distinct from 'failed'; StrandedRunState explicitly not promoted). The one Zod mirror in packages/spec/src (TriggerFlowResponseSchema.data.status, api/automation-api.zod.ts) carries the same four; a new pin test (contracts/automation-result-status.pin.test.ts) binds contract and wire at the type level (Assert over Eq, the automation-api.zod.test.ts form, compiled by check:test-typecheck) and the value level (.options + parse/refuse), and reads the JSDoc. content/docs/references/api/automation-api.mdx regenerated by gen:docs; changeset @objectstack/spec minor (additive, no ADR-0087 marker owed). No engine/verb/route change; no consumer needed even a type-only accommodation (zero exhaustive switches over the union; objectui's flowResponse.ts already types status as the three literals | string). Measured for #13937's services half: resumeInternal's catch arm returns NO status on the resumed-failure envelope today (only the run row says failed), and ExecutionStatus (what listRuns filters on) is not widened here.", "files_changed": [ ".changeset/automation-result-stranded-status.md", "content/docs/references/api/automation-api.mdx", "packages/spec/src/api/automation-api.zod.ts", "packages/spec/src/contracts/automation-result-status.pin.test.ts", "packages/spec/src/contracts/automation-service.ts" ], "tests": "All on head 2c4bc29, exit codes captured before any pipe, heavy runs via scripts/pm/os-verify-lock.sh. spec: build exit 0 (check-dts-emitted 34/34); whole suite `Test Files 453 passed | 1 skipped (454) · Tests 12214 passed | 1 skipped (12215)` — the skip is scripts/root-entry-type-nameability.pin.test.ts (inspectDistFreshness saw dist older than src because the reverse verification touched the contract file's mtime after the build; content byte-identical), re-run after a rebuild on the same head `Test Files 1 passed (1) · Tests 2 passed (2)`; the earlier fresh-dist full run on the pre-describe-edit tree was 454 passed / 12216 passed, 0 skipped; typecheck exit 0 (`check:test-typecheck: OK — 54 file(s) / 262 error(s) / 146 pinned signature(s)`, ledger unchanged); pin file + contracts/automation-service.test.ts + api/automation-api.zod.test.ts `Test Files 3 passed (3) · Tests 61 passed (61)`; check:generated exit 0 after --fix regenerated the one stale artifact (content/docs/references/**); check:api-surface exit 0 (twice, incl. after the final rebuild); all 15 spec-scoped derived gates exit 0. Consumers (downstream filter direction; closure built first via turbo `^...` of each consumer, 55 tasks, exit 0): runtime typecheck 0 + 5 files/81 tests (narrowed to the AutomationResult-reading files); service-automation 9 files/239 tests (narrowed; package has no typecheck script — NOT MEASURED for types, ledgered by check:type-check-coverage which exits 0); plugin-approvals typecheck 0 (check:test-typecheck OK) + whole test 36 files/662; client typecheck 0 + whole test 32 files/420; verify typecheck 0 + 1 file/4 tests; rest typecheck 0; cli typecheck 0; dogfood NOT MEASURED locally. Reverse verification from the committed state: union narrowed back to three members on disk (anchor counts four-member 0 / three-member 1, diff-stat 1 file) → tsc -p packages/spec/tsconfig.test.json exit 2 with 266 error lines vs the ledgered 262 on the restored tree; the 4 new errors name exactly automation-api.zod.test.ts(52,54) TriggerFlowDataMatchesContract and automation-result-status.pin.test.ts lines 64/72/74 — direction red as predicted; restore via `git checkout HEAD -- FILE` from an absolute-path trap, proven by `git diff HEAD --quiet` exit 0 and hash-object == HEAD blob 85b3d15a. No ablation (no dist-resolved subject).", "gates": { "exit_0": [ "spec build", "spec test (whole)", "spec typecheck", "spec check:generated", "spec check:api-surface", "spec check:authorable-surface", "spec check:browser-reachable-entries", "spec check:docs", "spec check:dual-source-exports", "spec check:empty-state", "spec check:entry-nameability", "spec check:export-origins", "spec check:exported-any", "spec check:liveness", "spec check:llms-txt", "spec check:skill-refs", "spec check:strictness-ledger", "spec check:variant-docs", "spec check:yaml-examples", "check:nul-bytes (scanned 7981 text files, no raw control bytes)", "check:doc-authoring (14504 customer-facing strings clean)", "check-adr-0087-registration", "check-changeset-no-major", "check-empty-changeset", "check-ci-filter-parity", "check-comment-mask-adoption", "check-cross-package-test-inputs (both spellings)", "check-keyed-text-bounds", "check-plugin-teardown-shape", "check-shard-attestation", "check-system-context-census", "check-undeclared-dep-imports", "docs-audit/check-affected-docs", "docs-audit/check-drift-comment", "pm/release-rehearsal-clone --self-test", "check:changeset-gate-self-tests", "check:dispatcher-error-vocabulary", "check:engine-double-contract", "check:logger-receiver-detach", "check:merge-driver", "check:objectql-double-limit", "check:objectui-changeset", "check:page-declaration-shape", "check:pm-half-states", "check:published-files", "check:query-options-erasure", "check:slot-lookup", "check:spec-parsed-alias", "check:test-source-alias", "check:type-check-coverage", "check:type-source-resolution", "check:where-matcher", "lint check:doc-formula-expressions (exit 0 once the closure was built)", "runtime typecheck", "plugin-approvals typecheck", "client typecheck", "verify typecheck", "rest typecheck", "cli typecheck" ], "not_measured": [ "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 its GitHub network leg; PM-side gate)", "service-automation typecheck (no script)", "dogfood typecheck/tests (needs the whole app closure)" ], "ci": "in_progress — not awaited (draft PR delivered at 2c4bc29)" }, "deviations": [ "Test narrowing, declared: runtime 6 patterns → 5 files/81 tests; service-automation 9 files/239 tests; verify 1 file/4 tests — the files that read AutomationResult; plugin-approvals and client ran whole.", "check:doc-authoring went red on the first spelling of the .describe() (issue id in customer-facing text); the id moved to an adjacent line comment, gate re-run green — no other text changed.", "One test skipped in the final whole-suite run (root-entry-type-nameability, dist-freshness gated by the reverse verification's mtime touch); re-run green after a rebuild on the same head.", "PM mechanism assumption falsified: packages/rest/src/plugin-type-closed-set.pin.test.ts does not exist on origin/main; the house type-level form used is the Assert/Eq pair from packages/spec/src/api/automation-api.zod.test.ts (no @ts-expect-error; spec's typecheck compiles test files via check:test-typecheck).", "Channel: REST repo-scoped read probed 200, so PR creation, PR label (additive POST) and the report comment went over REST; MCP used once (claim comment). No issue search was needed (nothing filed).", "PR body read-back: my 13016 bytes stored byte-identical, then the platform appended a second bare-then-session attribution footer (90 bytes) after my session footer — no content lost, first line `Fixes #14384` intact.", "Labels: needs:contract-review was already on the card from triage (read back: priority:p1 · pm:dispatched · domain:spec · pm:blocking · needs:contract-review); added to PR #14636 via additive POST and read back (PR labels: needs:contract-review)." ], "labels": { "card_14384": [ "priority:p1", "pm:dispatched", "domain:spec", "pm:blocking", "needs:contract-review" ], "pr_14636": [ "needs:contract-review" ] }, "mcp_calls": "1 — add_issue_comment (claim). Everything else: git, the public issue-page payload channel (3 pages), REST (probe, PR create, PR read-back, PR labels ×2, report comment + read-back).", "open_questions": [], "notes_for_downstream_13937": [ "resumeInternal's catch arm returns { success:false, error, durationMs, errorMessage, summary } with NO status today — the services half owns stamping 'stranded' on the result.", "ExecutionStatus (run-row vocabulary; listRuns({ status }) filter) is not widened here; whether the row ever carries 'stranded' is the services half's to measure.", "runtime's flow-dispatch-status.ts classifies failure by success/code, never by status — a 'stranded' envelope maps to 400 FLOW_FAILED until that half teaches it otherwise.", "plugin-approvals' classifyStrandedRunState switches over the run ROW status with a silent default; it does not read AutomationResult.status." ], "out_of_scope_findings": [] }
Generated by Claude Code
- added a commit that references this issue
on Sep 4, 2026 - added a commit that references this issue
on Sep 9, 2026 - added a commit that references this issue
on Oct 7, 2026
Filed by the
domain:servicesPM seat (session_01AUF1NoViznQK32gqpK8wS8) under the contract-first split rule: the services half of #13937 cannot land until this contract half does, andpackages/specbelongs to thedomain:specseat alone. Named reader: thedomain:specseat. Triage: please mintdomain:spec— this seat does not producedomain:*.Provenance — the ruling, quoted verbatim from #13937 comment 5491060439 (director seat; maintainer 2026-09-01 「其他同意」)
What this card asks of the spec seat
One contract increment in
packages/spec/src/contracts/automation-service.ts: give the terminally-failed-but-repairable run 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 — #13909's condition) a platform-level name onAutomationResult.status, alongside'completed' | 'paused' | 'failed'. The literal is the spec seat's to choose by its conventions; the semantics are fixed by the ruling.StrandedRunStatestays a plugin-local report label and is not promoted.Executable acceptance:
git show origin/main:packages/spec/src/contracts/automation-service.ts | grep -n "status"shows the new literal in theAutomationResult.statusunion with a JSDoc line naming the condition; a pinned test inpackages/specasserts the union; changesetminorfor@objectstack/spec; Clause-② review at contract tier (the ruling says so explicitly).What it does NOT ask
No engine change, no operator verb, no REST route — those are the services half (#13937 shape 4:
resumeInternalordering untouched, explicit operator re-arm verb) and, separately, #13953 (whose door and permission model are an open question and are not bundled here).Downstream
#13937 goes
pm:blockedwithBlocked-by:this card; #13909 stays blocked on #13937. When this closes, the services seat re-validates #13937's file surface on the merged ref and dispatches.