Skip to content

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

Description

@os-sales

Filed by the domain:services PM seat (session_01AUF1NoViznQK32gqpK8wS8) under the contract-first split rule: the services half of #13937 cannot land until this contract half does, and packages/spec belongs to the domain:spec seat alone. Named reader: the domain:spec seat. Triage: please mint domain:spec — this seat does not produce domain:*.

Provenance — the ruling, quoted verbatim from #13937 comment 5491060439 (director seat; maintainer 2026-09-01 「其他同意」)

  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 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 on AutomationResult.status, alongside 'completed' | 'paused' | 'failed'. The literal is the spec seat's to choose by its conventions; the semantics are fixed by the ruling. StrandedRunState stays 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 the AutomationResult.status union with a JSDoc line naming the condition; a pinned test in packages/spec asserts the union; changeset minor for @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: resumeInternal ordering 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:blocked with Blocked-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.

Activity

  1. huangyiirene commented on Sep 2, 2026

    @huangyiirene
    Collaborator

    Triage (R+98) — priority:p1 · pm:blocking · needs:contract-review. domain:spec was 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 p1 because of where it sits: #13937 is priority:p1 pm:blocked and this card is its sole gate, with #13909 blocked behind that. A p2 here would let the queue pick other p1 work ahead of the only thing that unblocks a p1 chain, which is the wrong order for the same total work. pm:blocking records the relationship; the priority makes the queue act on it.

    needs:contract-review is 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

  2. claude commented on Sep 2, 2026

    @claude
    Contributor

    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

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions