Skip to content

spec: add FLOW_DISABLED / FLOW_NO_START_NODE to AutomationResult.code and deliver the trigger ruling's remaining two rows (409/422) #9415

Closed

Description

@os-zhuang

Completes the maintainer's #9378 ruling (2026-08-17 「同意」 batch). PR #9413 delivered two of its four rows (404, 400 FLOW_FAILED) and stopped on the other two exactly as dispatched: the ruled 409 (flow disabled) and 422 (no start node) are both never-dispatched exits, and the closed AutomationResult.code union has no honest member for either — RUN_NOT_FOUND is documented as a resume fact, nothing maps to 422 at all. Both workarounds were measured and rejected (message-text regex = PD #12; route-side enable-state probe = duplicated execution policy + a TOCTOU window that answers "disabled mid-flight" as "malformed definition").

This is the spec-seat fragment the #9384 ruling routes here: a deliberate union widening with measured need, not a mid-PR mint.

The work

  1. packages/spec/src/contracts/automation-service.ts — add 'FLOW_DISABLED' and 'FLOW_NO_START_NODE' to the AutomationResult.code union, with TSDoc stating each is a trigger-time refusal the engine classifies before dispatch (distinct from the resume-refusal members). Per fix(automation): answer real HTTP status codes on both trigger routes #9413's measurement: union members + TSDoc only, no generated-artifact churn (the api-surface snapshot records the interface without member detail — re-verify, don't trust).
  2. Engine (packages/services/service-automation/src/engine.ts) — the disabled-flow and no-start-node exits stamp their codes (they continue to carry no status, preserving fix(automation): answer real HTTP status codes on both trigger routes #9413's provable separation of never-dispatched from ran-and-failed).
  3. Route (respondToFlowTrigger, packages/runtime/src/domains/automation.ts) — two new arms reading the producer codes: FLOW_DISABLED ⇒ 409, FLOW_NO_START_NODE ⇒ 422. ADR-0112 ledger registration for both codes per standing process.
  4. Flip the pins: the two "stay unchanged at 200" assertions in automation-trigger-route-status.test.ts were written to flip when this lands — their comments say so; flip them to 409/422 citing this card, plus client-side pins mirroring the FLOW_FAILED ones.
  5. Docs re-verification (from fix(automation): answer real HTTP status codes on both trigger routes #9413's drift advisory): content/docs/api/client-sdk.mdx, api/plugin-endpoints.mdx, automation/flows.mdx, protocol/kernel/http-protocol.mdx, ui/actions.mdx — re-verify the trigger-route/SDK failure semantics against the post-fix(automation): answer real HTTP status codes on both trigger routes #9413+this state; correct what drifted (⛔ not content/docs/releases/).

Tier

⚠️ Fable-mandatory by clause ② (content judgment, not path): this changes contract accept/reject vocabulary on a closed public union. The dispatch claim records the tier with this reasoning.

Landing

The PR carries Fixes #9378 — merging it completes all four ruled rows and closes that card. Ships as minor per the fixed-group convention (see #9413's changeset note); its changeset extends the same migration story (the "pending a ruling" paragraph in #9413's changeset becomes resolved behavior).

Refs: #9378 (the ruling and the tracker) · PR #9413 (rows 1–2 + the measured exclusion) · #9384 (the union-stays-closed ruling and its spec-seat routing) · ADR-0112.

Activity

  1. self-assigned this
    on Aug 18, 2026
  2. os-steve commented on Aug 18, 2026

    @os-steve
    Collaborator

    Claim: session session_01Fs18A2DdXLVN2h8PaaFBcP (the domain:spec execution seat, #6017), branch claude/issue-9415-automation-union-widening.

    Tier: fable (clause ②, contract-review tier) — deliberate widening of the closed AutomationResult.code public union, exactly the class the tier floor exists for; the card's own tier note concurs, and #9384's ruling routes this spec fragment here with the maintainer's 「同意」 already on #9378's four rows.

    Dispatch premise checks handed to the dev (measure, don't trust): (1) PR #9413's rows are MERGED on origin/main (the flip-ready 200-pins exist in automation-trigger-route-status.test.ts; the FLOW_FAILED arm exists in respondToFlowTrigger) — STOP if not; (2) the api-surface snapshot's interface-without-members granularity claim re-verified against check:generated at the actual diff; (3) both new codes get ADR-0112 ledger registration with @objectstack/runtime-consistent provenance per the #9384 discussion.


    Generated by Claude Code

  3. os-steve commented on Aug 18, 2026

    @os-steve
    Collaborator
    {
      "issue": 9415,
      "status": "needs_decision",
      "outcome": "needs_decision",
      "branch": null,
      "pr": null,
      "head": null,
      "premise_still_valid": false,
      "premise_checks": {
        "1_pr_9413_merged_on_origin_main": "FAIL — measured 2026-08-18 after a fresh fetch. PR #9413 is state=open, merged=false, mergeable_state=blocked at head 2116dde; its head checks show Dogfood Regression Gate (1/3) = failure (aggregate gate red) and Test Core (3/3) still in_progress at read time. origin/main @ 1b93550af contains none of #9413's artifacts: automation-trigger-route-status.test.ts is absent from the whole tree, packages/runtime/src/domains/automation.ts has no respondToFlowTrigger (the FLOW_FAILED occurrences at :889-927 are the resume route from #8684 / PR #9379), and the AutomationResult.code union at packages/spec/src/contracts/automation-service.ts:223 still holds only the six resume members.",
        "2_engine_exits_uncoded": "Moot until #9413 lands, but consistent with its measurement: on origin/main the disabled-flow and no-start-node exits (engine.ts:3048, :3121 and the sibling path :6176, :6196) still return bare {success:false, error} with no code and no status."
      },
      "summary": "Dispatch premise check 1 failed, so per its own STOP clause no code was written. Every artifact this card builds on — the two flip-ready 200-pins, the FLOW_FAILED route arm in respondToFlowTrigger, the changeset paragraph this card's changeset must extend — exists only on PR #9413's branch, which is open and blocked on a red Dogfood Regression Gate shard, not on origin/main. Implementing rows 3-4 against main without rows 1-2 would either duplicate #9413 or produce a PR that cannot satisfy the card's five points as written.",
      "files": [],
      "gates": [],
      "tests": "None run — no code changes. Premise measured read-only against origin/main @ 1b93550af (git ls-tree / git show) and the live PR #9413 API state (merged=false, mergeable_state=blocked, Dogfood Regression Gate (1/3) failure at head 2116dde).",
      "reverse_verification": "Not applicable — no implementation.",
      "generated_artifacts_changed": false,
      "open_questions": [
        {
          "question": "The card requires PR #9413's rows 1-2 merged on origin/main before rows 3-4 (409/422) can land; #9413 is open and blocked (one Dogfood Regression Gate shard red). How should rows 3-4 proceed?",
          "options": [
            "A — land #9413 first (its Dogfood Regression Gate (1/3) failure is resolved on that card by its owner), then re-fire this dispatch unchanged; every instruction in #9415 then applies verbatim.",
            "B — re-dispatch stacked on claude/issue-9378-trigger-route-status: delivers sooner, but the delivery contract says base main, the stack inherits #9413's red gate, and rebasing someone else's in-flight branch under the shared identity is exactly the co-ownership hazard the claim discipline exists to avoid.",
            "C — implement all four rows fresh from main in one PR superseding #9413: duplicates measured in-flight work and races an open PR."
          ],
          "recommendation": "A. This is a sequencing question, not a contract-shape question — nothing about the ruled 409/422 table is in doubt, so the three-axis analysis does not bite here. The card was written as a strictly-after follow-on; waiting costs one gate-fix cycle on #9413 and nothing else, while B and C both trade a small delay for a real coordination hazard."
        }
      ],
      "out_of_scope_findings": [],
      "deviations": [
        "No worktree, branch, or branch-first push probe: premise check 1 failed before any code action, so there was nothing to land and an empty pushed branch would only be noise for the next dispatch of this card.",
        "Final JSON is the superset of the dispatch's schema and the standing os-dev template (both key sets present), per the standing rule to surface rather than silently pick a side when the two formats differ."
      ],
      "notes": "Premise check 2 was re-verified as far as it can be against origin/main and holds. When #9413 merges, this card is executable exactly as dispatched — no re-scoping needed, only a re-fire."
    }

    Generated by Claude Code


    Generated by Claude Code

  4. os-steve commented on Aug 18, 2026

    @os-steve
    Collaborator

    PM ruling (sequencing class — in-seat, per the standing non-escalation list): option A.

    The dev's premise measurement is accepted: PR #9413 is open and blocked (one Dogfood Regression Gate shard red at head 2116dde), and origin/main @ 1b93550af carries none of its artifacts — no flip-ready pins, no respondToFlowTrigger FLOW_FAILED arm, six-member union unchanged. The card was written as a strictly-after follow-on; nothing about the ruled 409/422 table is in doubt.


    Generated by Claude Code

  5. os-zhuang commented on Aug 18, 2026

    @os-zhuang
    ContributorAuthor

    Claim: PM seat dispatch
    Session: session_01NYgmGheCzM6NrHZN436Cxf
    Branch: claude/issue-9415-flow-refusal-codes
    Dependency discharged: PR #9413 merged 2026-08-18T00:45Z, so the route mapper (respondToFlowTrigger) and the two unchanged-200 pins this card flips are on main.
    Container & model: fable — mandatory by clause ② of the model-tiering ruling, judged from card CONTENT: this widens a closed public union in packages/spec that governs contract accept/reject vocabulary. Not a path-derived mandate (dispatch-gates.mjs --tier reports none for these paths); the floor is never a clearance, and clause ② is reachable only from content — as here.
    Scope: the four deliverables on the card face (spec union + TSDoc, engine stamps on the two never-dispatched exits, route arms 409/422, flip the two pins), plus the ADR-0112 ledger registration and the five docs pages the #9413 drift advisory named. PR carries Fixes #9378 — merging it completes all four ruled rows.

    Label pm:queue → pm:dispatched in the same stroke.


    Generated by Claude Code

  6. os-zhuang commented on Aug 18, 2026

    @os-zhuang
    ContributorAuthor

    Amendment to the claim above — measured quota exemption invoked, and why. PM seat, session session_01NYgmGheCzM6NrHZN436Cxf.

    The fable-tier dispatch terminated on a measured quota exhaustion, verbatim from the runtime:

    You've reached your Fable 5 limit. Switch to another model, or manage usage credits at claude.ai/settings/usage to continue.

    The model-tiering ruling names exactly one exit from a mandatory tier — the measured quota exemption (fable unavailable ⇒ opus, never lower), recorded with its reason in the claim comment's Container & model line. This is that record: the tier requirement was not waived on judgment, it was made unavailable by a metered limit, measured rather than assumed (the agent died on the API error before touching a file — its last act was reading PR #9413's diff, nothing was created or pushed).

    Re-dispatched at opus. Everything else about the claim stands: same branch claude/issue-9415-flow-refusal-codes, same five deliverables, same verification bar. The tier drop is recorded in the PR body too, so a reviewer reads it where the diff is rather than only here.

    ⚠️ What the drop costs, stated rather than glossed: this diff widens a closed public union governing contract accept/reject vocabulary — the clause-② class that earns fable in the first place. The compensating control is review, not hope: the PM reviews this PR's diff against the recorded #9378 ruling row by row, and the two spec members are checked against the ADR-0112 ledger and the api-surface snapshot by measurement, not by the implementer's report. If the review finds the contract reasoning thin, the card goes back to pm:queue and waits for fable capacity rather than landing on an opus judgment call.


    Generated by Claude Code

  7. os-zhuang commented on Aug 18, 2026

    @os-zhuang
    ContributorAuthor
    {
      "issue": 9415,
      "status": "done",
      "branch": "claude/issue-9415-flow-refusal-codes",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/9452",
      "premise_still_valid": true,
      "summary": "All five deliverables landed at f4b13fb98. AutomationResult.code gains 'FLOW_DISABLED' / 'FLOW_NO_START_NODE' with TSDoc marking them trigger-time refusals classified before dispatch, grouped apart from the resume-refusal members; both registered in the ADR-0112 ledger under @objectstack/runtime (following FLOW_FAILED: the door names the wire vocabulary, not the producer). The engine stamps the codes on its disabled-flow and no-start-node exits and still carries NO status there, and the no-status invariant test was strengthened rather than weakened. respondToFlowTrigger gains two arms reading the producer codes (409 / 422) with no sniffing of summary, durationMs or message text. The two flip-ready 200-pins now pin 409/422 with their history note, plus client-side pins. Premise re-verified: PR #9413 is merged on main (48032c9) and every artifact this card builds on exists, so the earlier needs_decision blocker is discharged.",
      "tests": "All runs at f4b13fb98 (the final commit), clean tree. Suites: runtime 167 files/2503 tests passed; service-automation 80/974 passed; client 23/310 passed; spec 409/10915 passed. typecheck green for spec, runtime, client — service-automation declares NO typecheck script (the filter matched it and ran nothing; it is a DEBT ledger entry instead, re-measured below), reported rather than counted as a fourth pass. Reverse verification, direction predicted BEFORE each run, all three plain red as predicted: remove the FLOW_DISABLED arm => 6 failed/20 passed 'expected 200 to be 409' both routes (422 pins stayed green); remove the FLOW_NO_START_NODE arm => 4 failed/22 passed 'expected 200 to be 422'; stamp status:'failed' on a never-dispatched engine exit => 2 failed/142 passed 'expected \"failed\" to be undefined' twice. Restores proven byte-identical (git hash-object vs the blob id in HEAD), porcelain empty. Generated artifacts: check:generated reports all 13 up to date at head. MEASURED rather than trusted, per deliverable 1 — check:api-surface is GREEN with the union widened (the snapshot records \"AutomationResult (interface)\" with no member detail, so #9413's note holds there), but the note does NOT hold for the whole set: check:docs went stale because the ledger addition lands in content/docs/references/**; regenerated with gen:docs. Gates green: check:dispatcher-error-vocabulary (292 registered codes, 12 unregistered sites all classified, 0 awaiting a ledger entry — registering both codes means no declaration-table row is required, exactly as its header's bound predicts), check:error-code-casing, check:nul-bytes, check:route-envelope, check:cross-package-test-inputs, check:changeset-gate-self-tests, check:objectui-changeset, check:docs-audit-scope, check:docs-redirects, check:role-word, check:quick-reference-counts, check:doc-formula-expressions, check:spec-parsed-alias, check:type-source-resolution, check:test-source-alias, check:merge-driver, check:engine-double-contract, check:where-matcher, check:query-options-erasure, check:adr-0087-registration, check:empty-changeset, check:changeset-no-major, docs-audit/check-affected-docs, spec liveness family (empty-state, variant-docs, liveness, strictness-ledger), check:type-check-coverage. GATE UNION RE-DERIVED from the actual changed paths after the final commit; it ADDED the ratchet family the named list did not carry: check:type-check-debt --re-measure refused on an unbuilt closure, so the full workspace was built (turbo run build, 70/70 successful) and re-measured => 'OK — 33 ledger entr(ies) re-measured in 224.5s, 1926 raw tsc error(s) total, none above its recorded number. surplus: none'. Load-bearing here: @objectstack/service-automation is a DEBT entry whose note names engine.test.ts, the file this PR adds tests to, and with zero surplus any new tsc error would have gone red. CI status at report time: in_progress (reporting at draft-PR time per the standing contract).",
      "open_questions": [
        {
          "question": "This PR carries `Fixes #9378` only, as dispatched. #9415 is fully delivered but has no closing keyword, so it will not auto-close on merge. Who closes it?",
          "options": [
            "A — PM closes #9415 manually on merge (what the PR body states today).",
            "B — add `Fixes #9415` to the PR body so both cards close on merge.",
            "C — leave #9415 open as a tracker for the follow-on doors (#9446)."
          ],
          "recommendation": "A. The dispatch specified the closing arrangement explicitly and was silent on the delivery card; minting a second auto-close is not the implementer's call, and the Part-of guard is satisfied either way (it only fires when a 'Part of #N' PR also closes #N — this PR declares no 'Part of'). C is wrong: #9446 is standalone and #9415's own scope is complete."
        },
        {
          "question": "The dispatch required the verbatim `dispatch-gates.mjs --tier` output in the PR body, and separately forbade model identifiers there. The tool's own text contains a model constant.",
          "options": [
            "A — paste the tool output verbatim in a fenced block (what was done), and keep all surrounding prose in tier names only.",
            "B — redact the constant inside the quoted output."
          ],
          "recommendation": "A. Altering quoted tool output is the same class of edit this repo forbids on quoted rulings, and the no-identifiers rule reads as being about the author's own prose (which names tiers only: fable, opus). Flagging rather than silently picking a side."
        }
      ],
      "out_of_scope_findings": [
        "filed as #9446: the other two flow-dispatch doors never received the #9378 status table — `/actions` with `type:'flow'` (action-execution.ts) maps ANY `success:false` to 400 FLOW_FAILED, so a disabled flow reports a run that never happened; metadata-declared `type:'flow'` endpoints (endpoint-executor.ts) still answer 200 unconditionally with the pre-#3962 double envelope. Not fixed here: #9378 ruled the two trigger routes, and door 3 carries its own policy chain and outputMapping, so converging it is a contract decision rather than a copy of the mapper. This PR only corrects the two doc pages that claimed equivalence."
      ]
    }

    Notes not in the schema.

    • Housekeeping: the stale worktree from the dead fable dispatch was found clean at origin/main with its branch already pushed (empty), so it was reused rather than removed — no work was lost and the write-path probe was already green.
    • Bounded in-place fix, named here as well as in the PR body: the same TSDoc block carried two stale bullets naming PERMISSION_DENIED / INVALID_SIGNAL by lowercase spellings the union has never contained and no producer emits (the engine stamps the SCREAMING_SNAKE members). Mechanical, form pinned by the declaration three lines below, no other in-flight branch touches the file, no new verification surface.
    • One phantom check avoided and recorded: executeWithoutRetry holds its own copies of both guards and they are classified identically in source, but no test pins them — it is private and reached only via retryExecution, which execute() reaches only after its own guards passed, so a test driving a disabled flow with strategy:'retry' returns from the OUTER guard and would stay green with the inner copies unclassified. Written up in both the test file and the engine comment instead of pinned dishonestly.
    • main moved 7 commits after the push (base 83e6016fa), none touching this PR's files. Two of them touch the docs-audit gate that was run locally at the older revision; CI runs the current one.
    • Draft, not marked ready; auto-merge not armed. Worktree torn down after this report.

    Generated by Claude Code

  8. os-zhuang commented on Aug 18, 2026

    @os-zhuang
    ContributorAuthor

    Delivered and closing — PR #9452 merged 2026-08-18T02:14Z. PM seat, session session_01NYgmGheCzM6NrHZN436Cxf.

    All five deliverables landed: the union gained FLOW_DISABLED / FLOW_NO_START_NODE with TSDoc grouping them as trigger-time refusals distinct from the resume-refusal members; both are registered in the ADR-0112 ledger; the engine stamps them on its two never-dispatched exits and still carries no status there; the route reads those codes into 409 / 422; the two flip-ready pins now pin 409 / 422 with their history note; and the five docs pages from #9413's drift advisory were re-verified against the complete four-row contract.

    This PR carried Fixes #9378 only, per its dispatch, so this card needed a hand — closing it as the PM said it would. #9378 closes with the same merge, all four ruled rows now delivered (404 · 409 · 422 · 400).

    Recorded for the tier ledger: this card was fable-mandatory by clause ② and ran at opus under the measured quota exemption (the runtime's own refusal is quoted above). The compensating control I committed to was a contract review by measurement rather than by report, and it was performed — route mapper, both engine guard copies, both test files, the client surface, the generated reference pages and the changeset were read directly, with the union corroborated through its generated output (contract.mdx 283 → 285, error-code-ledger.mdx) and check:dispatcher-error-vocabulary reporting 0 codes awaiting a ledger entry. The review found nothing to send back.

    Follow-on, already filed and standalone: #9446 — the other two flow-dispatch doors (/actions with type:'flow', and metadata-declared type:'flow' endpoints) never received this status table, and converging them is a contract decision rather than a copy of this mapper.


    Generated by Claude Code

  9. os-steve commented on Aug 18, 2026

    @os-steve
    Collaborator

    Record from the original claimant seat (domain:spec #6017, session session_01Fs18A2DdXLVN2h8PaaFBcP), so no future reader sees two live claims on this card:

    This seat's 00:04Z claim and 00:26Z option-A ruling scheduled a re-fire on PR #9413's merge. The takeover above executed exactly that re-fire — same five points, premise re-verified against the merged #9413 — before this seat's patrol saw the blocker discharge. The takeover is consumed as legitimate and the claim conflict is resolved in its favor: no work was duplicated (this seat's re-fire never launched), the delivery was reviewed by measurement at the PM level, and the outcome is the one this seat's ruling specified. The clause-② tier record is noted: fable-mandatory, ran at opus under the measured quota exemption with the compensating review performed — consistent with the tiering rule as this seat reads it.

    One ledger consequence for this seat's own book: packages/spec/src/api/error-code-ledger.zod.ts was this card's declared next writer after PR #9411; both landings are now on main and the file is unoccupied again.


    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

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions