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
objectstack validate passes a flow whose try_catch catch region has several sinks; the engine refuses it at registration ("region must be single-exit") with only a WARN, so the flow never runs #28893
A flow whose try_catch catch region ends in several sinks, for example a decision fanning out to three notify nodes with no join, passes objectstack validate (exit 0) and objectstack build.
At kernel registration the automation engine refuses the same flow: "region must be single-exit". It logs this only at WARN, the boot continues, and the flow is never registered, so its trigger never fires. Nothing in the author-time gates or in the boot's exit status tells the author.
Measured twice by the duly card thread: once while building, and again by removing the join node from the final shape.
Expected
The single-exit rule the engine enforces is enforced at author time, in validate and build (spec or lint), with the same message. A flow the engine will refuse should not pass the gate that exists to catch it. Separately, triage may want a registration refusal to be louder than a WARN, for example by counting in the boot summary as a binding failure.
Read fresh (filed from duly, measured twice on 18.2.0). objectstack validate and build pass a flow whose try_catch catch region has several sinks. The engine then refuses it at registration ("region must be single-exit") at WARN only, and the flow silently never runs. A wrong shape must be refused loudly at author time (「错的必须被响亮拒绝并给处方」). domain:server, p2: the single-exit rule moves to the author-time judge the engine already uses, and the registration refusal stops being a WARN the boot ignores.
Claim: PM loop round 1 · card thread · 2026-11-02T04:33Z
Session: session_01FEkQ5oc56xChD6Wrc4cGSt (the seat); the card thread is session_01FEkQ5kV6ptpTnU6Hjb2mE4 (the #28844 / #28856 / #28880 dev thread, continuing)
Account: os-project-manager (the seat's linked user; the card's assignee)
Branch: claude/project-thread-1minft (the card thread's harness-pinned branch, to be restarted from origin/main after PR #28917's squash merge; it names no issue, so this claim is the identity record)
Worktree: the card thread's own container checkout
Domain: domain:server
Seat: domain:server#5
Ruling-ref: 「定级你可以自己决定」 (Leehom, 2026-11-01T12:11Z) with triage's grade (p2) and this seat's rulings in the order; pick announced to seats 1-3, seat 3 confirmed no clash (keep out of subflow-guards.ts and region-suspension-refusal.ts).
File surface: packages/cli/src/ (validate, build, their tests, boot summary only per ruling 3), changesets. ⛔ No packages/spec/**, ⛔ no packages/lint/** without a report, ⛔ not service-automation/src/subflow-guards.ts / region-suspension-refusal.ts.
Container & model: S, mode:thread, model and effort as the thread was opened
Clause-②: yes (narrowing; author-time validate/build refuse a flow the engine already refuses at registration; the dev confirms against the gates)
Metadata-protocol: none
Responsibility: the code is the author-time flow gate · a flow whose region is not single-exit passes validate and build, then is silently not registered at boot, so its trigger never fires · reached by every app author shipping a flow with structured regions
Thread-read: 6568658643
Premise: holds (read at origin/maina3640933: validateControlFlow is called at registerFlow only; no packages/cli source calls it)
Serial constraints cleared: waits for PR #28917 to merge (same card-thread branch); keep out of seat 3's subflow-guards / region-suspension files (#28846, #28897).
Resume-from: none — fresh from origin/main after #28917
State: pm:queue → pm:dispatched, assigned to os-project-manager, recorded by this claim. Order: pm-orders/server/order-28893.md in the project.
{
"issue": 28893,
"status": "done",
"branch": "claude/project-thread-1minft",
"pr": "https://github.com/objectstack-ai/objectstack/pull/29043",
"session": "session_01FEkQ5kV6ptpTnU6Hjb2mE4",
"premise_still_valid": true,
"summary": "os validate (step 3a-vi) and os build (compile.ts step 3b-iii-ter) now refuse a flow whose ADR-0031 regions the engine refuses at registerFlow: each flow (each packages[] entry's flows, or the stack's own) goes through FlowSchema.safeParse then the spec's exported validateControlFlow (@objectstack/spec/automation), at every depth; the row is { path, code: VALIDATION_ERROR, flow, message: \"Flow '<name>' would be refused at registration: <engine message verbatim>\" }, exit 1, build writes no dist/. Placement (A) per the seat: packages/cli/src/utils/flow-region-shape.ts, following #28727; no second copy of the region rules, no spec, lint or service-automation edit, seat 3's files untouched. os lint's blindness recorded as blindTo: ['lint'] in the parity test (second table OUTSIDE_THE_TABLE, asserted). Boot side measured, not changed (see out_of_scope_findings). Clause-② declared no (narrowing), PR title carries !, changeset patch with BREAKING banner and ADR-0087 not-required disposition.",
"tests": "New validate-flow-region-shape.test.ts (in-process Validate/Compile, 4): duly (try_catch 'guard' catch region d -> x,y,z unjoined) refused by validate --json (exit 1, row with \"try_catch 'guard' catch: region must be single-exit but has 3: x, y, z\" and the flow name) and by build --json (exit 1, success:false, same row, no dist/); nested (loop body fan-out inside a try region, \"try_catch 'outer' try → loop 'sweep' body: …\"); single-branch parallel (\"parallel 'fan': a parallel block needs at least 2 branches\"); joined control validates and builds. Base ff881050: 3 red, control green. Ablations on disk, restored: validate.ts call -> [] 3 red; compile.ts call -> [] 3 red. Parity-test pin: runAuthoringRules('lint') carries no single-exit finding (green). Real-CLI os validate text over the duly fixture: base 'Validation passed', exit 0; head '✗ The automation engine would refuse to register 1 flow for a malformed region (ADR-0031)' + \"flows[0] [VALIDATION_ERROR] Flow 'rgn_lead_time' would be refused at registration: try_catch 'guard' catch: region must be single-exit but has 3: x, y, z — add one node …\", exit 1 (both in the PR body). cli unit tier: first run 1 red (validate-build-gate-parity 'every call site is classified' named findFlowRegionRefusals), fixed by a SHARED_NON_REGISTRY_GATES row; final 369/369 files, 5201/5201. Integration tier declared to CI.",
"gates": "dispatch-gates --commands derived 62; all 62 exit 0; --ran: 62 derived, 62 run, 0 NOT-MEASURED. pnpm --filter @objectstack/cli typecheck (check:test-typecheck) exit 0. spec check:error-code-provenance exit 0. Scoped eslint over 6 changed .ts exit 0. Merged main twice (00d0668d, then ab085a04 after the push; nothing under packages/cli or packages/lint, packages/spec only region-currency and a save-notice comment/placement change). PR check-runs not yet read at report time.",
"dogfood_battery": "832bd06d · os-verify-lock VERDICT command-exit 0 · Test Files 383 passed | 3 skipped (386) · Tests 3163 passed | 28 skipped (3191) · no failures. Final head d246cd7e adds only the gate-ledger row in a cli test file and the ab085a04 main merge; not re-run.",
"line_budget": "+381 / -0 across 7 files (PR #29043 as GitHub counts it)",
"files_changed": [
".changeset/28893-flow-region-shape-at-validate-build.md",
"packages/cli/src/commands/compile.ts",
"packages/cli/src/commands/validate.ts",
"packages/cli/src/utils/flow-region-shape.ts",
"packages/cli/test/authoring-rule-command-parity.test.ts",
"packages/cli/test/validate-build-gate-parity.test.ts",
"packages/cli/test/validate-flow-region-shape.test.ts"
],
"deviations": [
"Clause-②: declared `no (narrowing)`, not `yes (narrowing)`: clause2-line.mjs reads `yes (narrowing)` as widening one surface and narrowing another, and nothing here widens (no export, schema key or accepted input). Same spelling as #28727.",
"lint blindTo: recorded in a second table (OUTSIDE_THE_TABLE) of authoring-rule-command-parity.test.ts, not in CASES, because every CASES row asserts the rule gates on all three commands via runAuthoringRules and this check is not an authoring rule; the new row asserts lint's blindness so closing it reds the pin.",
"Also touched packages/cli/test/validate-build-gate-parity.test.ts (one SHARED_NON_REGISTRY_GATES row), which the cli unit tier requires for any new call site in both commands.",
"Boot summary: no change (ruling 3); no exact one-line same-file count exists."
],
"mcp_calls": "3 — mcp__github__issue_read get (#28893), mcp__github__issue_read get_comments (#28893, claim check), mcp__github__pull_request_read get (#29043); no write tool",
"api_writes": "4 — git push x2 (claude/project-thread-1minft: new branch, then the main merge, both plain); relay pr_create via scripts/pm/fleet-write/dispatch.mjs (#29043, draft, read-back identical 9242 bytes, assignees none, no labels: changeset present); this report via scripts/pm/post-stamped.mjs",
"open_questions": [],
"out_of_scope_findings": [
"class: b (domain:devx) · reach: os lint is blind to the flow region-shape check validate and build now run, because the check is a cli-side gate, not an entry in packages/lint/src/authoring-rules.ts · evidence: authoring-rule-command-parity.test.ts OUTSIDE_THE_TABLE pin (runAuthoringRules('lint') carries no single-exit finding) · close: register a region-shape authoring rule over validateControlFlow (lint already depends on spec), then move the case into CASES and the gate-ledger row out of SHARED_NON_REGISTRY_GATES · dedupe words: os lint flow region single-exit parity",
"class: b · reach: a cycle in a flow's TOP-LEVEL graph (start → a → b → a → end) still passes os validate and os build (measured: validate exit 0) while the engine refuses it at registerFlow · where: detectCycles in packages/services/service-automation/src/flow-registration-checks.ts (~738), called from canonicalizeFlow in flow-definitions.ts before validateControlFlow; exported from that file but NOT from the service-automation package index; region bodies are already cycle-checked by validateControlFlow · what a home would take: either a service-automation index export (a new published name on that package, Clause-② widening) that the cli imports, or moving the top-level DAG check into spec beside validateControlFlow (a spec export, domain:spec) so the engine, cli and lint share one function; not widened here per the seat · dedupe words: flow top-level cycle detectCycles validate",
"class: c · reach: the boot banner's Flows: line (packages/cli/src/utils/format.ts printAutomationSummary ~1718) counts registered (flowCount = getFlowRuntimeStates().length), bound and draft flows; declaredFlowCount (commands/serve.ts ~5708, resolveStackCollection(config,'flows')) feeds only the engine-disabled line; a flow refused at registration is in no count and appears only as its '[Automation] failed to register flow' line in Boot diagnostics · where a count would sit: one more parts.push beside the draft count in printAutomationSummary · why not here: declaredFlowCount - flowCount is also moved by runtime overlay rows and same-named contenders, and the engine exposes refusals only per name (getFlowRegistration), so an exact count needs a new AutomationReadySummary field collected in serve.ts (two files) · dedupe words: boot banner flows refused count",
"carrier: PR #29043 Acceptance notes · noted, not filed"
]
}
Landed · 2026-11-02T07:23Z · PR #29043 merged through the merge queue as 5d3667e292; this card closed as completed by its Fixes line.
reading
value
PR state
merged, 5d3667e292
card
closed (completed); pm:dispatched removed and the seat unassigned in this stroke, domain:* / area:* kept
Unblocked in the same round: #29049 (lint parity), #29051 (top-level cycle at author time), #29059 (review should-fixes and the boot refused-flow count). Dispatch is paused at the maintainer's wrap-up, so they wait in pm:queue.
Activity
objectstack-fleet commentedon Nov 2, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: none
Triage: p2 ·
domain:server·area:automation·pm:queue· 2026-11-02T03:55Z⛔ Not a claim.
objectstack validateandbuildpass a flow whosetry_catchcatch region has several sinks. The engine then refuses it at registration ("region must be single-exit") at WARN only, and the flow silently never runs. A wrong shape must be refused loudly at author time (「错的必须被响亮拒绝并给处方」).domain:server, p2: the single-exit rule moves to the author-time judge the engine already uses, and the registration refusal stops being a WARN the boot ignores.Generated by Claude Code
objectstack-fleet commentedon Nov 2, 2026
domain:server#5 · os-project-manager · session_01FEkQ5oc56xChD6Wrc4cGSt
Claim: PM loop round 1 · card thread · 2026-11-02T04:33Z
Session:
session_01FEkQ5oc56xChD6Wrc4cGSt(the seat); the card thread issession_01FEkQ5kV6ptpTnU6Hjb2mE4(the #28844 / #28856 / #28880 dev thread, continuing)Account:
os-project-manager(the seat's linked user; the card's assignee)Branch:
claude/project-thread-1minft(the card thread's harness-pinned branch, to be restarted fromorigin/mainafter PR #28917's squash merge; it names no issue, so this claim is the identity record)Worktree: the card thread's own container checkout
Domain:
domain:serverSeat:
domain:server#5Ruling-ref: 「定级你可以自己决定」 (Leehom, 2026-11-01T12:11Z) with triage's grade (p2) and this seat's rulings in the order; pick announced to seats 1-3, seat 3 confirmed no clash (keep out of subflow-guards.ts and region-suspension-refusal.ts).
File surface:
packages/cli/src/(validate, build, their tests, boot summary only per ruling 3), changesets. ⛔ Nopackages/spec/**, ⛔ nopackages/lint/**without a report, ⛔ notservice-automation/src/subflow-guards.ts/region-suspension-refusal.ts.Container & model:
S,mode:thread, model and effort as the thread was openedClause-②: yes (narrowing; author-time validate/build refuse a flow the engine already refuses at registration; the dev confirms against the gates)
Metadata-protocol: none
Responsibility: the code is the author-time flow gate · a flow whose region is not single-exit passes validate and build, then is silently not registered at boot, so its trigger never fires · reached by every app author shipping a flow with structured regions
Thread-read: 6568658643
Premise: holds (read at
origin/maina3640933:validateControlFlowis called atregisterFlowonly; nopackages/clisource calls it)Serial constraints cleared: waits for PR #28917 to merge (same card-thread branch); keep out of seat 3's subflow-guards / region-suspension files (#28846, #28897).
Resume-from: none — fresh from
origin/mainafter #28917State:
pm:queue→pm:dispatched, assigned toos-project-manager, recorded by this claim. Order:pm-orders/server/order-28893.mdin the project.objectstack-fleet commentedon Nov 2, 2026
domain:server#5 · os-project-manager · session_01FEkQ5kV6ptpTnU6Hjb2mE4
os-dev-report
{ "issue": 28893, "status": "done", "branch": "claude/project-thread-1minft", "pr": "https://github.com/objectstack-ai/objectstack/pull/29043", "session": "session_01FEkQ5kV6ptpTnU6Hjb2mE4", "premise_still_valid": true, "summary": "os validate (step 3a-vi) and os build (compile.ts step 3b-iii-ter) now refuse a flow whose ADR-0031 regions the engine refuses at registerFlow: each flow (each packages[] entry's flows, or the stack's own) goes through FlowSchema.safeParse then the spec's exported validateControlFlow (@objectstack/spec/automation), at every depth; the row is { path, code: VALIDATION_ERROR, flow, message: \"Flow '<name>' would be refused at registration: <engine message verbatim>\" }, exit 1, build writes no dist/. Placement (A) per the seat: packages/cli/src/utils/flow-region-shape.ts, following #28727; no second copy of the region rules, no spec, lint or service-automation edit, seat 3's files untouched. os lint's blindness recorded as blindTo: ['lint'] in the parity test (second table OUTSIDE_THE_TABLE, asserted). Boot side measured, not changed (see out_of_scope_findings). Clause-② declared no (narrowing), PR title carries !, changeset patch with BREAKING banner and ADR-0087 not-required disposition.", "tests": "New validate-flow-region-shape.test.ts (in-process Validate/Compile, 4): duly (try_catch 'guard' catch region d -> x,y,z unjoined) refused by validate --json (exit 1, row with \"try_catch 'guard' catch: region must be single-exit but has 3: x, y, z\" and the flow name) and by build --json (exit 1, success:false, same row, no dist/); nested (loop body fan-out inside a try region, \"try_catch 'outer' try → loop 'sweep' body: …\"); single-branch parallel (\"parallel 'fan': a parallel block needs at least 2 branches\"); joined control validates and builds. Base ff881050: 3 red, control green. Ablations on disk, restored: validate.ts call -> [] 3 red; compile.ts call -> [] 3 red. Parity-test pin: runAuthoringRules('lint') carries no single-exit finding (green). Real-CLI os validate text over the duly fixture: base 'Validation passed', exit 0; head '✗ The automation engine would refuse to register 1 flow for a malformed region (ADR-0031)' + \"flows[0] [VALIDATION_ERROR] Flow 'rgn_lead_time' would be refused at registration: try_catch 'guard' catch: region must be single-exit but has 3: x, y, z — add one node …\", exit 1 (both in the PR body). cli unit tier: first run 1 red (validate-build-gate-parity 'every call site is classified' named findFlowRegionRefusals), fixed by a SHARED_NON_REGISTRY_GATES row; final 369/369 files, 5201/5201. Integration tier declared to CI.", "gates": "dispatch-gates --commands derived 62; all 62 exit 0; --ran: 62 derived, 62 run, 0 NOT-MEASURED. pnpm --filter @objectstack/cli typecheck (check:test-typecheck) exit 0. spec check:error-code-provenance exit 0. Scoped eslint over 6 changed .ts exit 0. Merged main twice (00d0668d, then ab085a04 after the push; nothing under packages/cli or packages/lint, packages/spec only region-currency and a save-notice comment/placement change). PR check-runs not yet read at report time.", "dogfood_battery": "832bd06d · os-verify-lock VERDICT command-exit 0 · Test Files 383 passed | 3 skipped (386) · Tests 3163 passed | 28 skipped (3191) · no failures. Final head d246cd7e adds only the gate-ledger row in a cli test file and the ab085a04 main merge; not re-run.", "line_budget": "+381 / -0 across 7 files (PR #29043 as GitHub counts it)", "files_changed": [ ".changeset/28893-flow-region-shape-at-validate-build.md", "packages/cli/src/commands/compile.ts", "packages/cli/src/commands/validate.ts", "packages/cli/src/utils/flow-region-shape.ts", "packages/cli/test/authoring-rule-command-parity.test.ts", "packages/cli/test/validate-build-gate-parity.test.ts", "packages/cli/test/validate-flow-region-shape.test.ts" ], "deviations": [ "Clause-②: declared `no (narrowing)`, not `yes (narrowing)`: clause2-line.mjs reads `yes (narrowing)` as widening one surface and narrowing another, and nothing here widens (no export, schema key or accepted input). Same spelling as #28727.", "lint blindTo: recorded in a second table (OUTSIDE_THE_TABLE) of authoring-rule-command-parity.test.ts, not in CASES, because every CASES row asserts the rule gates on all three commands via runAuthoringRules and this check is not an authoring rule; the new row asserts lint's blindness so closing it reds the pin.", "Also touched packages/cli/test/validate-build-gate-parity.test.ts (one SHARED_NON_REGISTRY_GATES row), which the cli unit tier requires for any new call site in both commands.", "Boot summary: no change (ruling 3); no exact one-line same-file count exists." ], "mcp_calls": "3 — mcp__github__issue_read get (#28893), mcp__github__issue_read get_comments (#28893, claim check), mcp__github__pull_request_read get (#29043); no write tool", "api_writes": "4 — git push x2 (claude/project-thread-1minft: new branch, then the main merge, both plain); relay pr_create via scripts/pm/fleet-write/dispatch.mjs (#29043, draft, read-back identical 9242 bytes, assignees none, no labels: changeset present); this report via scripts/pm/post-stamped.mjs", "open_questions": [], "out_of_scope_findings": [ "class: b (domain:devx) · reach: os lint is blind to the flow region-shape check validate and build now run, because the check is a cli-side gate, not an entry in packages/lint/src/authoring-rules.ts · evidence: authoring-rule-command-parity.test.ts OUTSIDE_THE_TABLE pin (runAuthoringRules('lint') carries no single-exit finding) · close: register a region-shape authoring rule over validateControlFlow (lint already depends on spec), then move the case into CASES and the gate-ledger row out of SHARED_NON_REGISTRY_GATES · dedupe words: os lint flow region single-exit parity", "class: b · reach: a cycle in a flow's TOP-LEVEL graph (start → a → b → a → end) still passes os validate and os build (measured: validate exit 0) while the engine refuses it at registerFlow · where: detectCycles in packages/services/service-automation/src/flow-registration-checks.ts (~738), called from canonicalizeFlow in flow-definitions.ts before validateControlFlow; exported from that file but NOT from the service-automation package index; region bodies are already cycle-checked by validateControlFlow · what a home would take: either a service-automation index export (a new published name on that package, Clause-② widening) that the cli imports, or moving the top-level DAG check into spec beside validateControlFlow (a spec export, domain:spec) so the engine, cli and lint share one function; not widened here per the seat · dedupe words: flow top-level cycle detectCycles validate", "class: c · reach: the boot banner's Flows: line (packages/cli/src/utils/format.ts printAutomationSummary ~1718) counts registered (flowCount = getFlowRuntimeStates().length), bound and draft flows; declaredFlowCount (commands/serve.ts ~5708, resolveStackCollection(config,'flows')) feeds only the engine-disabled line; a flow refused at registration is in no count and appears only as its '[Automation] failed to register flow' line in Boot diagnostics · where a count would sit: one more parts.push beside the draft count in printAutomationSummary · why not here: declaredFlowCount - flowCount is also moved by runtime overlay rows and same-named contenders, and the engine exposes refusals only per name (getFlowRegistration), so an exact count needs a new AutomationReadySummary field collected in serve.ts (two files) · dedupe words: boot banner flows refused count", "carrier: PR #29043 Acceptance notes · noted, not filed" ] }Generated by Claude Code
objectstack-fleet commentedon Nov 2, 2026
domain:server#5 · os-project-manager · session_01FEkQ5oc56xChD6Wrc4cGSt
ACCEPT → PR #29043 comment 6570813381 · head
d246cd7e· 2026-11-02T06:39Z. Contract review PASS (narrowing face) on this head, comment 6570752949. Landing:pr_ready+automerge_enablethrough the relay; the merge queue closes this card. Follow-ups: #29059 (review should-fixes + boot refused-flow count, i.e. this card's "louder than a WARN" ask), #29049 (lint parity), #29051 (top-level cycle).objectstack-fleet commentedon Nov 2, 2026
domain:server#5 · os-project-manager · session_01FEkQ5oc56xChD6Wrc4cGSt
Landed · 2026-11-02T07:23Z · PR #29043 merged through the merge queue as
5d3667e292; this card closed as completed by itsFixesline.5d3667e292pm:dispatchedremoved and the seat unassigned in this stroke,domain:*/area:*keptUnblocked in the same round: #29049 (lint parity), #29051 (top-level cycle at author time), #29059 (review should-fixes and the boot refused-flow count). Dispatch is paused at the maintainer's wrap-up, so they wait in
pm:queue.