You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
spec(automation): FlowRuntimeState gains a structured discriminator beside reason (deployment policy vs a binding failure), so a consumer can classify a disabled packaged flow without parsing the sentence (objectui#12609) #23842
Filing gate ③: the classification half of objectstack-ai/objectui#12609 (filed by the epic PM from the 2026-10-16 Setup review, under the maintainer's 「全部立卡」). It was split by triage at that card's pm:retriage (domain:ui seat 3, 6210398091, option B). ⛔ Not a claim.
Reader who acts: the domain:spec seat, as a vertical seam: the contract in packages/spec/src/contracts/automation-service.ts, and its producer in packages/services/service-automation, declared to domain:services.
Governing text
FlowRuntimeState.reason, verbatim: "A free-form sentence, deliberately — the shape the two surfaces that already carry this reason use. Consumers RENDER it; ⛔ do not parse it." The same docblock says a deployment-policy row and a real binding failure both arrive as enabled: true, bound: false with a reason.
The gap
The console wants an "Off by deployment policy" chip on exactly the policy-disabled rows, and a one-time banner. It cannot tell those rows apart without parsing reason, which the contract forbids. objectui already obeys the rule: PackagedFlowRow.reason is rendered, never read (objectui#9217).
The change
An optional, closed discriminator beside reason on FlowRuntimeState. For example unboundCause: 'deployment_policy' | 'binding_failure'; the spec seat names it by the spec's conventions.
service-automation stamps it wherever it composes bound: false today.
reason stays the rendered sentence.
Clause-②: yes (widening), with a contract review at tier.
Grade
p1 + target:v18. It is a widening of a spec contract (protocol-first class).
Downstream
The console half (the chip and the banner) is filed by this card's claimant on objectui once the field is in a published @objectstack/spec. objectstack-ai/objectui#12609 is narrowed meanwhile to what needs no classification.
Claim: PM loop round 8 · 2026-10-16T08:20Z · a vertical seam, in one PR, Fixes #23842. FlowRuntimeState (packages/spec/src/contracts/automation-service.ts) gains one optional, closed discriminator beside reason, named by the spec's conventions, that says WHY an enabled, triggered row is bound: false. It has one value per distinct cause the producer composes today; the card's example is deployment_policy vs binding_failure, and the dev measures the real set. service-automation stamps it at every site that composes reason. reason stays the rendered sentence, and its "⛔ do not parse it" rule stays true.
Session: session_01HjR8RUwTPQ3TgbfKihmxp9 (the dispatching seat; the dev runs as a cloud session the seat post names)
Account: os-elon-musk (the seat's linked user as GET /user answers it; this card's assignee)
Branch: claude/issue-23842-flow-unbound-cause
Worktree: objectstack-issue-23842 (in the cloud session's container)
Domain: domain:spec
Seat: domain:spec#1 (seat post #6017)
File surface (at origin/main3fed1eb00a or later; stop on a breach and explain it in the report):
packages/spec/src/contracts/automation-service.ts: FlowRuntimeState and its docblocks only, plus any exported union or constant the field needs.
packages/services/service-automation/src/engine.ts: the sites that compose bound: false with a reason (getFlowRuntimeStates and its helpers), and their tests in the same package.
Any wire mirror of FlowRuntimeState the sweep finds (a zod response schema, the /_status route's shape, an api-surface baseline), through its generator where one exists.
One minor changeset each for @objectstack/spec and @objectstack/service-automation.
The file lists of all 20 open PRs and the latest Claim: of all 24 open pm:dispatched cards were read in this act. No open PR touches contracts/automation-service.ts or service-automation/src/engine.ts.
Cross-lane: packages/services/service-automation is domain:services. This seat posts a cross-seat declaration on that lane's live seat post in this act.
Downstream: this card's claimant files the console half (the chip and the banner) on objectstack-ai/objectui once the field ships in a published @objectstack/spec.
{
"issue": 23842,
"status": "done",
"branch": "claude/issue-23842-flow-unbound-cause",
"pr": "https://github.com/objectstack-ai/objectstack/pull/23854",
"session": "session_01PU9ijL7gdKme3dFxWKcBE6",
"premise_still_valid": true,
"summary": "FlowRuntimeState gains optional unboundCause, closed over the exported tuple FLOW_UNBOUND_CAUSES (type FlowUnboundCause), following the APPROVAL_STATUSES convention. The measured producer is AutomationEngine.describeUnboundReason, the single producer of reason for both /_status and getTriggerBindingAudit(). It has four branches, each with a distinct owner and remedy: deployment_policy, disabled_subflow, missing_trigger and binding_failure. Each branch now returns { reason, cause }, and getFlowRuntimeStates() spreads the pair together, so on every row the two are either both present or both absent. The reason text, ScheduledWorkPolicy and the audit shape are unchanged. No zod wire mirror exists, and the /_status route passes rows verbatim. The api-surface and export-origins baselines were regenerated.",
"tests": "head 7ba22e988d. pnpm --filter @objectstack/service-automation exec vitest run --maxWorkers=2: Test Files 186 passed, Tests 2501 passed (VERDICT command-exit 0). spec vitest src/contracts/automation-service.test.ts: 18 passed. service-automation src/engine.test.ts + src/subflow-guard-every-door.test.ts: 182 passed. typecheck for spec and service-automation: exit 0. Pins: deployment_policy, binding_failure and missing_trigger in engine.test.ts; disabled_subflow, plus the cause clearing on re-arm, in subflow-guard-every-door.test.ts. Control: a bound, a disabled and a manual flow carry neither reason nor unboundCause. ABLATION: scripts/ablation-replace.mjs replaced 'return { reason: policyReason, cause: 'deployment_policy' };' with 'return { reason: policyReason } as any;' (anchor x1 -> x0, blob 4c735c08 -> 88ee7f8f). engine.test.ts went red on 2 pins: 'OFF: the status row names the policy, and NOT a binding failure' and 'the two doors answer the SAME sentence for the same flow'. Restore: the blob equals HEAD 4c735c08 and git diff HEAD is empty. No dist build was involved: the suite imports ./engine.js from source.",
"gates": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived the list at 7ba22e98. Each family was run with its exit code captured before any pipe. Every family exited 0 except two. (1) node scripts/check-plugin-teardown-shape.mjs --self-test exited 3 on the shallow clone and exited 0 after git fetch --unshallow. (2) pnpm check:dual-build-cjs-loads exited 3 because it needs a full pnpm build; that is NOT MEASURED locally and left to CI. --ran reconciliation with recorded exit codes: 90 derived, 89 run, 1 NOT-MEASURED, 0 UNRUN. pnpm --filter @objectstack/spec check:generated: exit 0, after gen:api-surface and gen:export-origins on a real build. check:dev-prereqs: exit 0. CI check-runs: in_progress at report time.",
"mcp_calls": "2: mcp__github__issue_read x2 (get, get_comments on #23842). No write tools.",
"api_writes": "2: fleet-write relay pr_create (POST /repos/objectstack-ai/objectstack/pulls, plus the assignees leg POST /issues/23854/assignees), and the os-dev-report comment via post-stamped (POST /repos/objectstack-ai/objectstack/issues/23842/comments). Plus git push. No label write: changesets are present, so skip-changeset does not apply.",
"line_budget": "179 (167 additions + 12 deletions, 9 files)",
"files_changed": [
".changeset/23842-automation-stamps-unbound-cause.md",
".changeset/23842-flow-runtime-state-unbound-cause.md",
"packages/services/service-automation/src/engine.test.ts",
"packages/services/service-automation/src/engine.ts",
"packages/services/service-automation/src/subflow-guard-every-door.test.ts",
"packages/spec/api-surface/contracts.json",
"packages/spec/export-origins/contracts.json",
"packages/spec/src/contracts/automation-service.test.ts",
"packages/spec/src/contracts/automation-service.ts"
],
"deviations": [
"Worktree: the shared checkout already had the branch checked out, so I detached it (git checkout --detach) and added ../objectstack-issue-23842 on the existing branch.",
"The first commit's harness trailer carried a model name. The pre-push hook refused it, so I amended the commit to the model-free pair before the first content push. No pushed history was rewritten.",
"pnpm check:dual-build-cjs-loads: NOT MEASURED locally (exit 3, needs a full build). It is left to CI.",
"test:repo was not run locally: the diff touches no repo-project test. It is left to CI's Test Core."
],
"open_questions": [
{ "question": "Union size: the card's example has 2 values; the producer measures 4 distinct branches. Is that right?", "options": ["A: 4 values, one per branch (shipped)", "B: collapse missing_trigger and binding_failure into binding_failure"], "recommendation": "A. The two have different remedies (add a dependency vs read the boot error), and the console can still map both to a single 'defect' style. Collapsing them later would be a narrowing." },
{ "question": "Should getTriggerBindingAudit() entries also carry the cause?", "options": ["A: no, status door only (shipped)", "B: add cause to audit entries"], "recommendation": "A. The card names FlowRuntimeState only, and the audit's consumers (the CLI summary, the boot warn) render the sentence. Widening a second surface with no consumer is out of scope." }
],
"out_of_scope_findings": []
}
ACCEPT → PR #23854 comment 6212425624 · domain:spec seat 1 · session session_01HjR8RUwTPQ3TgbfKihmxp9 · 2026-10-16T09:27Z · the contract review (CONTRACT_REVIEW_TIER, face packages/spec/src/contracts/**, a widening) and an all-green head stand before the queue.
The discriminator ships as unboundCause, with four values published as FLOW_UNBOUND_CAUSES: deployment_policy, disabled_subflow, missing_trigger and binding_failure. That is one per branch the producer really composes, not the card's example two. The cloud dev session is archived at this ACCEPT. A patch round, if one is owed, reopens it.
all 9 files' blobs at 6422da58e8 equal the reviewed head 7ba22e988d (ACCEPT 6212425624, contract review PASS 6212649956, same head)
queue branch
no gh-readonly-queue/main/pr-23854-* ref remains
mis-close scan
the body's only closing keyword is Fixes #23842; this card closed completed
State:pm:dispatched is removed in this act; enhancement, priority:p1, domain:spec, target:v18 and area:workflow stay. The cloud dev session was archived at the seat's ACCEPT.
FlowRuntimeState now carries unboundCause beside reason, on exactly the rows that carry reason. Its four values (FLOW_UNBOUND_CAUSES) are the four branches of the engine's one producer. A consumer can tell a deployment-policy flow from a binding failure without parsing the sentence.
Activity
objectstack-fleet commentedon Oct 16, 2026
Claim: PM loop round 8 · 2026-10-16T08:20Z · a vertical seam, in one PR,
Fixes #23842.FlowRuntimeState(packages/spec/src/contracts/automation-service.ts) gains one optional, closed discriminator besidereason, named by the spec's conventions, that says WHY an enabled, triggered row isbound: false. It has one value per distinct cause the producer composes today; the card's example isdeployment_policyvsbinding_failure, and the dev measures the real set.service-automationstamps it at every site that composesreason.reasonstays the rendered sentence, and its "⛔ do not parse it" rule stays true.Session:
session_01HjR8RUwTPQ3TgbfKihmxp9(the dispatching seat; the dev runs as a cloud session the seat post names)Account:
os-elon-musk(the seat's linked user asGET /useranswers it; this card's assignee)Branch:
claude/issue-23842-flow-unbound-causeWorktree:
objectstack-issue-23842(in the cloud session's container)Domain:
domain:specSeat:
domain:spec#1(seat post #6017)File surface (at
origin/main3fed1eb00aor later; stop on a breach and explain it in the report):packages/spec/src/contracts/automation-service.ts:FlowRuntimeStateand its docblocks only, plus any exported union or constant the field needs.packages/services/service-automation/src/engine.ts: the sites that composebound: falsewith areason(getFlowRuntimeStatesand its helpers), and their tests in the same package.FlowRuntimeStatethe sweep finds (a zod response schema, the/_statusroute's shape, an api-surface baseline), through its generator where one exists.minorchangeset each for@objectstack/specand@objectstack/service-automation.reason's text, notScheduledWorkPolicyorscheduledWorkDisabledReason, and not objectui.Container & model:
M,mode:cloud(the maintainer's word in this session's chat: 「batch 调到 5, 可以开云卡」),model: default tier(dispatch-gates --tier: no path-derived mandate). A widening of a published contract: the review owed before the queue is atCONTRACT_REVIEW_TIER, facepackages/spec/src/contracts/**.Clause-②: yes
Responsibility: n/a — not a defect card (filing gate ③, the maintainer's 「全部立卡」 via Packaged automation: a flow disabled by deployment policy shows a status chip; the policy is explained once objectui#12609)
Thread-read: none
Ruling-ref: none (triage's split
6210398091on Packaged automation: a flow disabled by deployment policy shows a status chip; the policy is explained once objectui#12609, option B)Serial constraints cleared:
Claim:of all 24 openpm:dispatchedcards were read in this act. No open PR touchescontracts/automation-service.tsorservice-automation/src/engine.ts.domain:engineseat 2) namesservice-automationonly to exclude it from its scope.packages/services/service-automationisdomain:services. This seat posts a cross-seat declaration on that lane's live seat post in this act.@objectstack/spec.objectstack-fleet commentedon Oct 16, 2026
os-dev-report
{ "issue": 23842, "status": "done", "branch": "claude/issue-23842-flow-unbound-cause", "pr": "https://github.com/objectstack-ai/objectstack/pull/23854", "session": "session_01PU9ijL7gdKme3dFxWKcBE6", "premise_still_valid": true, "summary": "FlowRuntimeState gains optional unboundCause, closed over the exported tuple FLOW_UNBOUND_CAUSES (type FlowUnboundCause), following the APPROVAL_STATUSES convention. The measured producer is AutomationEngine.describeUnboundReason, the single producer of reason for both /_status and getTriggerBindingAudit(). It has four branches, each with a distinct owner and remedy: deployment_policy, disabled_subflow, missing_trigger and binding_failure. Each branch now returns { reason, cause }, and getFlowRuntimeStates() spreads the pair together, so on every row the two are either both present or both absent. The reason text, ScheduledWorkPolicy and the audit shape are unchanged. No zod wire mirror exists, and the /_status route passes rows verbatim. The api-surface and export-origins baselines were regenerated.", "tests": "head 7ba22e988d. pnpm --filter @objectstack/service-automation exec vitest run --maxWorkers=2: Test Files 186 passed, Tests 2501 passed (VERDICT command-exit 0). spec vitest src/contracts/automation-service.test.ts: 18 passed. service-automation src/engine.test.ts + src/subflow-guard-every-door.test.ts: 182 passed. typecheck for spec and service-automation: exit 0. Pins: deployment_policy, binding_failure and missing_trigger in engine.test.ts; disabled_subflow, plus the cause clearing on re-arm, in subflow-guard-every-door.test.ts. Control: a bound, a disabled and a manual flow carry neither reason nor unboundCause. ABLATION: scripts/ablation-replace.mjs replaced 'return { reason: policyReason, cause: 'deployment_policy' };' with 'return { reason: policyReason } as any;' (anchor x1 -> x0, blob 4c735c08 -> 88ee7f8f). engine.test.ts went red on 2 pins: 'OFF: the status row names the policy, and NOT a binding failure' and 'the two doors answer the SAME sentence for the same flow'. Restore: the blob equals HEAD 4c735c08 and git diff HEAD is empty. No dist build was involved: the suite imports ./engine.js from source.", "gates": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived the list at 7ba22e98. Each family was run with its exit code captured before any pipe. Every family exited 0 except two. (1) node scripts/check-plugin-teardown-shape.mjs --self-test exited 3 on the shallow clone and exited 0 after git fetch --unshallow. (2) pnpm check:dual-build-cjs-loads exited 3 because it needs a full pnpm build; that is NOT MEASURED locally and left to CI. --ran reconciliation with recorded exit codes: 90 derived, 89 run, 1 NOT-MEASURED, 0 UNRUN. pnpm --filter @objectstack/spec check:generated: exit 0, after gen:api-surface and gen:export-origins on a real build. check:dev-prereqs: exit 0. CI check-runs: in_progress at report time.", "mcp_calls": "2: mcp__github__issue_read x2 (get, get_comments on #23842). No write tools.", "api_writes": "2: fleet-write relay pr_create (POST /repos/objectstack-ai/objectstack/pulls, plus the assignees leg POST /issues/23854/assignees), and the os-dev-report comment via post-stamped (POST /repos/objectstack-ai/objectstack/issues/23842/comments). Plus git push. No label write: changesets are present, so skip-changeset does not apply.", "line_budget": "179 (167 additions + 12 deletions, 9 files)", "files_changed": [ ".changeset/23842-automation-stamps-unbound-cause.md", ".changeset/23842-flow-runtime-state-unbound-cause.md", "packages/services/service-automation/src/engine.test.ts", "packages/services/service-automation/src/engine.ts", "packages/services/service-automation/src/subflow-guard-every-door.test.ts", "packages/spec/api-surface/contracts.json", "packages/spec/export-origins/contracts.json", "packages/spec/src/contracts/automation-service.test.ts", "packages/spec/src/contracts/automation-service.ts" ], "deviations": [ "Worktree: the shared checkout already had the branch checked out, so I detached it (git checkout --detach) and added ../objectstack-issue-23842 on the existing branch.", "The first commit's harness trailer carried a model name. The pre-push hook refused it, so I amended the commit to the model-free pair before the first content push. No pushed history was rewritten.", "pnpm check:dual-build-cjs-loads: NOT MEASURED locally (exit 3, needs a full build). It is left to CI.", "test:repo was not run locally: the diff touches no repo-project test. It is left to CI's Test Core." ], "open_questions": [ { "question": "Union size: the card's example has 2 values; the producer measures 4 distinct branches. Is that right?", "options": ["A: 4 values, one per branch (shipped)", "B: collapse missing_trigger and binding_failure into binding_failure"], "recommendation": "A. The two have different remedies (add a dependency vs read the boot error), and the console can still map both to a single 'defect' style. Collapsing them later would be a narrowing." }, { "question": "Should getTriggerBindingAudit() entries also carry the cause?", "options": ["A: no, status door only (shipped)", "B: add cause to audit entries"], "recommendation": "A. The card names FlowRuntimeState only, and the audit's consumers (the CLI summary, the boot warn) render the sentence. Widening a second surface with no consumer is out of scope." } ], "out_of_scope_findings": [] }Generated by Claude Code
objectstack-fleet commentedon Oct 16, 2026
ACCEPT → PR #23854 comment 6212425624 ·
domain:specseat 1 · sessionsession_01HjR8RUwTPQ3TgbfKihmxp9· 2026-10-16T09:27Z · the contract review (CONTRACT_REVIEW_TIER, facepackages/spec/src/contracts/**, a widening) and an all-green head stand before the queue.The discriminator ships as
unboundCause, with four values published asFLOW_UNBOUND_CAUSES:deployment_policy,disabled_subflow,missing_triggerandbinding_failure. That is one per branch the producer really composes, not the card's example two. The cloud dev session is archived at this ACCEPT. A patch round, if one is owed, reopens it.objectstack-fleet commentedon Oct 16, 2026
Landed · PR #23854 →
6422da58e8·domain:specseat 1 ·os-elon-musk· sessionsession_01HjR8RUwTPQ3TgbfKihmxp9· 2026-10-16T10:16Z6422da58e8origin/maingit merge-base --is-ancestor 6422da58e8 origin/main→ exit 06422da58e8equal the reviewed head7ba22e988d(ACCEPT6212425624, contract review PASS6212649956, same head)gh-readonly-queue/main/pr-23854-*ref remainsFixes #23842; this card closedcompletedState:
pm:dispatchedis removed in this act;enhancement,priority:p1,domain:spec,target:v18andarea:workflowstay. The cloud dev session was archived at the seat's ACCEPT.FlowRuntimeStatenow carriesunboundCausebesidereason, on exactly the rows that carryreason. Its four values (FLOW_UNBOUND_CAUSES) are the four branches of the engine's one producer. A consumer can tell a deployment-policy flow from a binding failure without parsing the sentence.automation.getRuntimeStatus()hand-types its rows as{ name, enabled, bound }, so an SDK caller cannot readreason(or the newunboundCause) thatGET /automation/_statusalready returns #23859, filed from the contract review's escalated finding:@objectstack/clienthand-typesgetRuntimeStatus()rows as three fields, so an SDK caller cannot readreasonorunboundCause.automation.getRuntimeStatus()hand-types its rows as{ name, enabled, bound }, so an SDK caller cannot readreason(or the newunboundCause) thatGET /automation/_statusalready returns #23859 lands, it readsunboundCauseoff the_statusrow, as it readsreasontoday.Generated by Claude Code