Skip to content

feat(spec,automation): FlowRuntimeState.unboundCause classifies an unbound flow row beside its reason - #23854

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-23842-flow-unbound-cause
Oct 16, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-23842-flow-unbound-cause

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #23842
Clause-②: yes

What

FlowRuntimeState (packages/spec/src/contracts/automation-service.ts) gains an optional, closed unboundCause beside reason. Its values are published as the tuple FLOW_UNBOUND_CAUSES, with the derived type FlowUnboundCause, so a consumer can enumerate them. This follows the APPROVAL_STATUSES / ApprovalStatus convention in the same contracts folder. reason stays the rendered sentence. Its docblock still says "do not parse it", and now points to unboundCause for anyone who needs to classify the row.

AutomationEngine.describeUnboundReason (packages/services/service-automation/src/engine.ts) is the single producer of reason. Both the status door and getTriggerBindingAudit() read it. Each of its branches now returns { reason, cause }, and getFlowRuntimeStates() spreads the two together, so on every row they are either both present or both absent. Neither the sentences nor the audit's shape changed.

The measured set of causes (4, not the card's example 2)

I read every branch of describeUnboundReason at 3fed1eb00. It composes four distinct causes, each with its own owner and remedy:

value branch owner / remedy
deployment_policy policyDisabledFlows (the recorded ScheduledWorkPolicy refusal) operator: the deployment switch or the host; not a defect
disabled_subflow subflowDeclinedFlows (ADR-0126 §7.3 arming gate) admin: enable the packaged subflow, and the caller arms
missing_trigger no trigger of the declared type is registered author: requires: ['triggers'] / the trigger package
binding_failure the trigger is registered, but binding threw engineering: the earlier boot warnings

None of them is merged with another, because no two share a remedy. missing_trigger and binding_failure both have an engineering owner, but one is fixed by adding a dependency and the other by reading a stack trace. There is no catch-all other: the branch set is closed.

Wire mirrors

GET /automation/_status (packages/runtime/src/domains/automation.ts) passes getFlowRuntimeStates() rows through verbatim and is typed against the contract, so it needs no change. No zod schema in packages/spec/src/api mirrors FlowRuntimeState: I searched for triggerType and _status and found only a docblock mention. I regenerated the api-surface/contracts.json and export-origins/contracts.json baselines with gen:api-surface and gen:export-origins. check:generated exit 0.

Tests (head 7ba22e988d)

  • Pairing pins, one per cause:
    • engine.test.ts: the status row for deployment_policy, binding_failure for a bind that throws, and missing_trigger in the two-doors test.
    • subflow-guard-every-door.test.ts: disabled_subflow, and that the cause clears when the subflow is re-enabled and the caller arms.
  • Control: the DARK test now covers a bound row, a disabled row and a manual (trigger-less) flow, and asserts that none of them carries reason or unboundCause.
  • Spec: type identities pin FlowRuntimeState['unboundCause'] to the tuple union. A runtime test checks that the values are unique and snake_case with no other, and that the docblock still forbids parsing reason and points at unboundCause.
  • pnpm --filter @objectstack/service-automation exec vitest run: 186 files / 2501 tests passed. Spec automation-service.test.ts: 18 passed. typecheck for both packages: exit 0.
  • Ablation: I replaced return { reason: policyReason, cause: 'deployment_policy' }; with return { reason: policyReason } as any; through scripts/ablation-replace.mjs (anchor x1 → x0, blob changed). engine.test.ts went red on 2 pins: "OFF: the status row names the policy…" and "the two doors answer the SAME sentence…". After the restore, the blob equals HEAD (4c735c08…) and git diff HEAD is empty. No build was needed, because the suite imports ./engine.js from source.

Gates

dispatch-gates --commands on this diff gives 90 derived families. --ran reconciliation: 89 run, 1 NOT MEASURED. Every family I ran exited 0. check:plugin-teardown-shape --self-test exited 3 at first on the shallow clone and exited 0 after git fetch --unshallow. NOT MEASURED: check:dual-build-cjs-loads, which exited 3 because it needs a full pnpm build. That is left to CI. check:dev-prereqs: exit 0.

Acceptance notes

🤖 Generated with Claude Code


Generated by Claude Code

…bound row beside its reason

FlowRuntimeState gains an optional, closed unboundCause (FLOW_UNBOUND_CAUSES:
deployment_policy, disabled_subflow, missing_trigger, binding_failure),
stamped by AutomationEngine.describeUnboundReason on exactly the rows that
carry reason, so a consumer classifies without parsing the sentence.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PU9ijL7gdKme3dFxWKcBE6
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 16, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/service-automation, @objectstack/spec, touching 12 documentable anchor(s). ⚠️ 2 changed file(s) yielded no anchor (packages/spec/api-surface/contracts.json, packages/spec/export-origins/contracts.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/automation/flows.mdx (via getFlowRuntimeStates (symbol, a method of class AutomationEngine), getTriggerBindingAudit (symbol, a method of class AutomationEngine))
  • content/docs/deployment/environment-variables.mdx (via getTriggerBindingAudit (symbol, a method of class AutomationEngine))
  • content/docs/deployment/tenancy-modes.mdx (via getTriggerBindingAudit (symbol, a method of class AutomationEngine))

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

  • content/docs/releases/v16.mdx (via AutomationEngine (symbol, a top-level class))
  • content/docs/releases/v17/17-0.mdx (via AutomationEngine (symbol, a top-level class))
  • content/docs/releases/v17/17-5.mdx (via getTriggerBindingAudit (symbol, a method of class AutomationEngine))
  • content/docs/releases/v17/17-6.mdx (via AutomationEngine (symbol, a top-level class), getTriggerBindingAudit (symbol, a method of class AutomationEngine))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 2 changed file(s) yielded no anchor (packages/spec/api-surface/contracts.json, packages/spec/export-origins/contracts.json) — pages documenting those are invisible to this run
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 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.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 142 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 1eac7ad751e833115544be405993efced6a726d3 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 1eac7ad751e833115544be405993efced6a726d3

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT · PR #23854 @ 7ba22e988d (#23842) · domain:spec seat 1 · os-elon-musk · session session_01HjR8RUwTPQ3TgbfKihmxp9 · 2026-10-16T09:26Z

Reviewer of record. Every item below was checked against GitHub and the code, not against the report (6211918732 on #23842).

  • Shape:
    • Draft, base main. The first line is Fixes #23842, the body's only closing keyword, then Clause-②: yes, matching claim 6211261093 and both changesets ((widening)).
    • 9 files, +167/−12, none governed (check-governed-merges --pr 23854: 0 of 9 paths). Assignee os-elon-musk.
  • The contract (automation-service.ts):
    • FlowRuntimeState gains an optional unboundCause?: FlowUnboundCause.
    • The values are a published tuple, FLOW_UNBOUND_CAUSES = ['deployment_policy', 'disabled_subflow', 'missing_trigger', 'binding_failure'] as const, so a consumer can enumerate them. The type is derived from it.
    • reason's "⛔ do not parse it" rule stays, now pointing a classifying consumer at the new field.
  • The producer, read in engine.ts: describeUnboundReason is the one place both /_status and getTriggerBindingAudit() take their sentence from.
    • Each of its four branches now returns { reason, cause }, with the cause written in the branch that decides it, never inferred from the text: the policy record, the subflow-declined record, a registered trigger whose binding failed, and no trigger registered.
    • getFlowRuntimeStates() spreads reason and unboundCause in one expression, so the pair is present together or absent together by construction.
    • The audit keeps its reason-only shape. Every sentence's text is byte-identical.
  • Four values, not the card's example two: each branch has its own owner and remedy (operator, admin, author, engineering). Merging missing_trigger into binding_failure would hide that "add a dependency" and "read the boot error" are different fixes. Collapsing values later would be a narrowing, while splitting one later would be a widening the console must chase.
  • Pins:
    • A pin for each cause, across engine.test.ts and subflow-guard-every-door.test.ts, including the cause clearing on re-arm.
    • A CONTROL in which a bound, a disabled and a manual flow carry neither field.
    • The reported ablation dropped the policy branch's cause; 2 pins went red, and the restore was byte-identical.
  • Wire mirrors: none in a zod schema. The /_status route passes rows through unchanged. api-surface/contracts.json and export-origins/contracts.json were regenerated for the two new exports.
  • The report's open questions, the seat's calls:
    • Four values: A.
    • The audit entries stay reason-only: A. The card names FlowRuntimeState alone, and the audit's readers render the sentence.
  • Changesets: minor for @objectstack/spec and @objectstack/service-automation, each Clause-②: yes (widening). The sentences were checked against the diff.
  • Before the queue: a CONTRACT_REVIEW_TIER contract review (face packages/spec/src/contracts/**, a widening), and every check green on the head. The console half (the chip and the banner) is filed on objectstack-ai/objectui once the field ships in a published @objectstack/spec.

Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Written: 2026-10-16T09:38Z

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 7ba22e988d571270ebb9faabd54ff1486e759bdf
Local-runs: none
Scope: packages/spec/src/contracts/automation-service.ts + packages/services/service-automation/src/engine.ts, packages/runtime/src/domains/automation.ts, packages/client/src/index.ts, packages/cli/src/commands/serve.ts, packages/services/service-automation/src/plugin.ts, packages/types/src/env.ts, packages/spec/src/contracts/approval-service.ts, packages/spec/src/api/automation-api.zod.ts, packages/spec/src/api/protocol.zod.ts, packages/services/service-automation/src/engine.test.ts, packages/services/service-automation/src/subflow-guard-every-door.test.ts, packages/spec/src/contracts/automation-service.test.ts

Inputs: card #23842 (body, claim 6211261093, dev report 6211918732), parent objectstack-ai/objectui#12609 (body, retriage 6210398091, answer 6210990723), PR #23854 (body, file list, net diff main...7ba22e98 at merge-base 3fed1eb00a), the check-runs on the head, and the cited code read at the head sha. The rest of the diff (changesets, baselines, tests, engine.ts) is the lane seat's ACCEPT at TIER_DEFAULT; it is read here only where the contract's docblocks make a claim about it.

Gate verdicts, read 2026-10-16T09:35Z: 41 check-runs on the head, 35 success, 6 skipped (path-filtered: Temporal Conformance, Console Pin Gate, Dogfood Verify CLI, the dogfood shard matrix placeholder, Build Docs, Packed-tarball smoke), 0 in progress. On the first read Test Core (3/6) was in_progress; it completed success before this record was written. Governed Surface Queue Guard, Check Changeset, Type Check (workspace, source gates, consumer gates), Lint & Repo Gates, Build Core, Test Core 1-6/6: all success. The PR touches no governed path; the review is owed by the published-schema face (packages/spec/src/contracts/**).

① Derived judgments

  1. FLOW_UNBOUND_CAUSES is exactly the producer's branch set — RIGHT. AutomationEngine.describeUnboundReason (engine.ts 4923-4947 at the head) has four non-undefined returns and the tuple has four values, one per return and in the same precedence order: policyDisabledFlows.get → deployment_policy; subflowDeclinedFlows.get → disabled_subflow; this.triggers.has(resolved.triggerType) → binding_failure; else → missing_trigger. The three undefined returns (no declared trigger, isFlowEnabled false, already in boundFlowTriggers) are the rows the contract says carry neither field. No catch-all; the spec test pins uniqueness, snake_case and the absence of other.

  2. No bound: false row with a reason is composed anywhere else — RIGHT. A pinned git grep at the head over packages, apps, skills and scripts (tests excluded) finds one producer of FlowRuntimeState rows: getFlowRuntimeStates (engine.ts 4846-4884). GET /automation/_status (runtime domains/automation.ts 2816-2828) is typed as Pick of IAutomationService and returns { flows, total } with the rows untouched, so the field reaches the wire with no route change, as the changeset says. getTriggerBindingAudit (engine.ts 4958-4972) reads the same computation and projects reason only; its shape { flowName, triggerType, reason } is not FlowRuntimeState and is not declared on IAutomationService, so it is not a row this contract governs (see ③). getFlowRegistration's refused.reason is FlowRegistration, a different surface; a withdrawn flow has no flows entry and so no runtime row. No host composes rows: service-automation/src/plugin.ts 1623 and packages/cli/src/commands/serve.ts 6777 consume them.

  3. "Present exactly on the rows that carry reason" is true by construction, re-arm included — RIGHT. There is one write site: the spread at engine.ts 4878-4880, ...(unbound === undefined ? {} : { reason: unbound.reason, unboundCause: unbound.cause }). Every clearing path changes the inputs of describeUnboundReason, never a second field: activateFlowTrigger deletes policyDisabledFlows past the gate (3968) and declineOntoDisabledSubflows deletes subflowDeclinedFlows when nothing is disabled (5664); boundFlowTriggers.set on a successful bind (4050) makes the function return undefined; rejudgeSubflowCallers (5702-5712) re-offers a declined caller through deactivateFlowTrigger then activateFlowTrigger; unregisterFlow drops both maps with the flow (4783-4784). Pinned on the re-arm path at subflow-guard-every-door.test.ts 236 (republished active arms it again; the row then carries neither key) and on the control rows (bound, disabled, manual) in engine.test.ts.

  4. Each value's docblock remedy is true of the code — RIGHT, with two narrower-than-stated notes that are not defects.

    • deployment_policy "package-authored scheduled work is switched off on this deployment (the ScheduledWorkPolicy refusal, read from the recorded refusal). Operator remedy: the deployment switch or the host." — ScheduledWorkPolicy.enabled is "whether package-authored scheduled work runs on this deployment at all" (types/env.ts 332); the sentence recorded at the bind (engine.ts 3943-3948) is scheduledWorkDisabledReason(policy) = policy.hostDisabledReason ?? SCHEDULED_WORK_DISABLED_REASON (types/env.ts 455-456): the host's reason when the per-kernel policy carries one, else the deployment switch's. Read from policyDisabledFlows, never re-derived. True. Note: the policy is asked only for a time-triggered kind (engine.ts 3938), so this cause is only ever stamped on a scheduled flow, which is what "scheduled work" says.
    • disabled_subflow "the arming gate declined the flow because a packaged subflow it calls is disabled (ADR-0126 §7.3). Admin remedy: enable the subflow, and the caller arms." — declineOntoDisabledSubflows (engine.ts 5660-5686) records it; rejudgeSubflowCallers arms the caller the moment the subflow is enabled (pinned). True. Note: only a packaged caller is declined (isPackagedFlow, 5662); the docblock does not say so, and the recorded sentence carries it. Narrower, not false.
    • missing_trigger "no trigger of the declared type is registered on this kernel. Author remedy: depend on the trigger package (requires: ['triggers'])." — the else arm of this.triggers.has, and the sentence names the same remedy. True.
    • binding_failure "the trigger is registered but binding the flow to it failed. Engineering remedy: the earlier boot warnings name the error." — the true arm; the catch in activateFlowTrigger (engine.ts 4051-4067) logs Failed to bind flow … The trigger's own failure is in this record's meta at warn. True; the branch is the same "registered but unbound" the sentence already calls a binding failure, so no new conflation is introduced.
  5. Wire mirrors — one lags, and it lagged before this PR. packages/client/src/index.ts 5668 hand-types automation.getRuntimeStatus() as { flows: Array of { name, enabled, bound }; total }. That is a transcription of the row, not a type read from the contract, and on main it already omits status, triggerType, object, reason, armedFrom and shadowed; at this head it also omits unboundCause. The dev's sweep ("no zod schema in packages/spec/src/api mirrors FlowRuntimeState") is true as scoped — automation-api.zod.ts has no bound/triggerType schema, protocol.zod.ts 285 is FlowRegistration's docblock, and no OpenAPI builder in packages/rest names _status — but the claim's "any wire mirror the sweep finds" reached packages/client and the sweep did not. This PR does not create the lag and the diff is correct without touching it; the lag is a pre-existing contract violation of the dispatcher 多个 domain 调用契约里没有的方法 —— #4087 的同类,只是方向相反(契约缺声明,不是调用点乱编) #4127 class (the route was cured of it; the SDK was not) and is escalated in ③, not failed here. A second hand-typed copy is consumer-local: packages/cli/src/commands/serve.ts 6762-6776 annotates states without reason or unboundCause; it reads reasons from the audit and renders nothing of either field, so it is not a wire. api-surface/contracts.json and export-origins/contracts.json carry the two new exports; contracts/index.ts is export *, so FLOW_UNBOUND_CAUSES reaches @objectstack/spec/contracts as the changeset says. The console's own row type (PackagedFlowRow, named in 6210398091) is the downstream card's consumer type; an optional field cannot break it.

  6. A closed union over an exported as const tuple is the contracts folder's shape — RIGHT. APPROVAL_STATUSES / ApprovalStatus, APPROVAL_ACTION_KINDS and CRYPTO_CONTEXT_SCOPES use (typeof X)[number], and APPROVAL_CANCEL_REASONS is the exact analogue: a closed machine value beside free text, "a VOCABULARY rather than free text because the reason has a consumer that is not a human reader". No _LABELS map is owed: those exist to derive a sys_* column's option labels, unboundCause has no column, and the chip wording is the console's (objectui#12609: "the console shortens the display, not the server's reason"). Not a Zod enum: FlowRuntimeState is a TypeScript service contract, as its siblings are; Prime Directive 1 governs metadata schemas. Optional is correct for an additive field. The reason docblock keeps "do not parse it" and now points at unboundCause; the spec test reads the source and pins both. The bound docblock is amended consistently.

② Semver level

@objectstack/spec minor and @objectstack/service-automation minor; both public (private: false), both in the changeset fixed group at 18.0.0-next.2. The diff publishes two new exports and one optional interface field from @objectstack/spec, and a new optional field on getFlowRuntimeStates() rows from @objectstack/service-automation: additive on both, minor is the floor Clause-②: yes requires and the right level. Clause-②: yes on the claim (6211261093) and the PR body; Clause-②: yes (widening) on both changesets, the one arm the closed pair permits for yes. The three agree; no (narrowing) anywhere; ScheduledWorkPolicy, scheduledWorkDisabledReason and the sentences are untouched, as the claim required.

③ Boundary flags

  • Dev open question: four values rather than the card's example two — answered, the shipped set stands. Contract axis: the claim asked for "one value per distinct cause the producer composes today", and the producer composes four, each decided at its own branch. Consumer axis: a console that wants one "defect" style maps missing_trigger and binding_failure to it in one line; a wider closed set never blocks a narrower render. Semver axis: collapsing later would be a narrowing (breaking); splitting later would be a widening — the wide set ships now at minor. Scope axis: the claim delegated the measurement to the dev. No escalation.
  • Dev open question: getTriggerBindingAudit() entries left reason-only — answered, acceptable. Contract axis: the audit's shape is not FlowRuntimeState and is not on IAutomationService, so the card's contract is complete without it. Consumer axis: its two readers (the CLI startup summary, the kernel:bootstrapped warn) render the sentence; a cause there would be declared and read by nothing. Semver axis: adding it later is additive to an engine-only method. Scope axis: the card names FlowRuntimeState only. No escalation.
  • Escalated — a card for packages/client: automation.getRuntimeStatus() (index.ts 5668) must type its answer as { flows: FlowRuntimeState[]; total: number } from @objectstack/spec/contracts instead of a hand-typed three-field row, the same cure dispatcher 多个 domain 调用契约里没有的方法 —— #4087 的同类,只是方向相反(契约缺声明,不是调用点乱编) #4127 applied to the route. Pre-existing on main, outside this PR's claimed surface, and a one-line change in the SDK lane; the dispatching seat files it (this review writes one comment and nothing else). Until it lands, the console half of objectui#12609 reads unboundCause off the _status row itself, as it reads reason today, not through the SDK's return type. The serve.ts annotation can ride the same card.
  • Dev deviations read: the shared checkout had the branch checked out and was detached before the worktree was added (worktree discipline, not this review's surface); the first commit's trailer was amended before any push (no pushed history rewritten); check:dual-build-cjs-loads and test:repo were left to CI — Build Core, Lint & Repo Gates and Test Core 1-6/6 are success on the head.
  • premise_still_valid: true holds: the parent's consumer need (a chip on exactly the policy-disabled rows, a once-banner) is met by unboundCause === 'deployment_policy' without parsing reason.

Implemented-by: claude/issue-23842-flow-unbound-cause
Reviewed-by: session_01HjR8RUwTPQ3TgbfKihmxp9

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 16, 2026 09:41
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 16, 2026 09:41
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 16, 2026
Merged via the queue into main with commit 6422da5 Oct 16, 2026
42 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-23842-flow-unbound-cause branch October 16, 2026 10:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

2 participants