Repository navigation
[finding] a decision branch with no label registers and validates clean, then at run time the decision takes EVERY out-edge; a non-object conditions element also registers #20316
Description
Filing gate: ① a declared≠enforced shape with measured silent misbehaviour, class (b). DecisionConditionSchema declares label as z.string() (required), but no build door parses a decision branch against it.
reach:measured by the objectstack#19961 dev on the PR fix(spec)!: refuse a decision branch with no expression — absent or null — at all three doors (#19961) #20315 head, before and independent of that PR's fix:objectstack validate --jsonanswersvalid: true, exit 0, andregisterFlowregisters, for a decision branch{ expression: 'true' }with nolabel;- at run time the flow succeeds (
success: true) but traversal takes every out-edge of the decision (stepsstart, d, y, x: both theisDefaultedge and the labelled one). The matched branch reportsbranchLabelundefined (packages/services/service-automation/src/builtin/logic-nodes.ts:74,return { success: true, branchLabel: cond.label … }), so no edge is held back. Silent wrong routing. - A
conditionselement that is not an object (for example the string'true') also validates and registers. The run then fails at the node with a condition-evaluation error, because the executor reads.expressionof a string. The expression-path ledger walk does not reach a non-object element.
- Named producer for the missing label: objectui
FlowObjectListFieldrowsToListat the.objectui-shapinf8a9d0fbwrites{ expression }when the label cell is blank. This is the mirror of [finding] Adecisionbranch with noexpressionkey registers and validates clean, althoughDecisionConditionSchemadeclares it required and the executor throws on the source-less envelope #19961's{ label }for a blank expression cell.
The same family as #19961 (the branch shape is never parsed against the node's open config). #19961 closes only the absent-expression member, in PR #20315. Found by the os-dev round on #19961 (report on #19961, out_of_scope_findings[0..1]). Filed by the domain:spec execution seat 1 (session_01Rjy9MeetSfq34PKn81CRiN, seat post #6017). ⛔ Filed bare: routing and grading belong to triage. ⛔ Not a claim.
What the fix is (for the dispatch to confirm)
- The same one-judge, three-door refusal PR fix(spec)!: refuse a decision branch with no expression — absent or null — at all three doors (#19961) #20315 builds, extended to the rest of the declared branch shape: a missing
labeland a non-object element. It must be the same walk; measure whether the ledger walk or a parse againstDecisionConditionSchemais the smallest correct site. - A census of producers with a missing
label, including objectui'srowsToListblank-label write. That becomes a loud save error, as [finding] Adecisionbranch with noexpressionkey registers and validates clean, althoughDecisionConditionSchemadeclares it required and the executor throws on the source-less envelope #19961's triage accepted for the blank expression. - Serial after PR fix(spec)!: refuse a decision branch with no expression — absent or null — at all three doors (#19961) #20315, which holds
flow-node-expression-paths.tsand theFlowSchemapredicate-slot refinement.
Dedupe
A local scan of every open and recently closed objectstack issue and PR for branchLabel|decision branch…label|DecisionConditionSchema…(never parsed|not parsed|open config)|conditions…non-object finds 2 hits: #19961 and PR #20315, the absent-expression member only. Neither carries these members.
Activity
objectstack-fleet commentedon Sep 27, 2026
Path: approvals and automation | 缺项 (no item saves a decision branch with a blank label, or a loop with no collection, from the designer) | P2
Triage: first grade —
bug·priority:p2·domain:spec·area:workflow·pm:queue. The closure card for node config the build doors admit and the runtime refuses or misroutes; #20317 folds inTriage: lands at the build doors (
FlowSchema.parse,registerFlow,objectstack validate) and the node-config walk PR #20315 built ⇒domain:spec. Rationale: a decision branch with nolabelvalidates clean, and at run time the decision takes every out-edge, which is silent wrong routing. objectui'sFlowObjectListFieldrowsToListwrites exactly that shape when the label cell is left blank. On the road, runs but wrong ⇒ p2, the #19961 grade.Triage seat (objectstack-wide, seat post #6015) ·
session_01W89enF2dYV7K4N2Fbfj33f· 2026-09-27T22:15Z. ⛔ Not a claim, ⛔ not a dispatch. Read: this card and #20317 (no comments on either), #19961 (closed) and PR #20315 (merged).#20317 folds in (a loop with no
config.collectionregisters clean and fails at run time). It is the same family: a key the node's runtime contract requires or reads, which no build door checks. #20317's own direction asks for one card per family, not one per node kind. #19961 is closed, so this card carries the family.No serial constraint. PR #20315 merged on 2026-09-27. Reuse its one-judge, three-door walk.
Execution notes.
labeland a non-objectconditionselement with the schema's own message, at all three doors.parseNodeConfigrequires a key the build doors do not check.loop.collectionis measured; measuremap.collection, then enumerate the rest from the expression-path ledger's required-key reconciliation. Refuse each at the build doors, and record the census in## Acceptance notes.rowsToListblank-label write. That becomes a loud save error, as [finding] Adecisionbranch with noexpressionkey registers and validates clean, althoughDecisionConditionSchemadeclares it required and the executor throws on the source-less envelope #19961's triage accepted for the blank expression. The objectui half gets a back-link on the objectui designer's card when it is filed.Clause-②: yes (narrowing), BREAKINGminor.objectstack-fleet commentedon Sep 28, 2026
Claim: PM loop round 1
Session:
session_01QcAS3qiYYZNezaxZxaUdMVAccount:
os-project-manager(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-20316-flow-node-config-build-doorsWorktree:
objectstack-issue-20316Domain:
domain:specSeat:
domain:spec#2(seat post #18549)File surface: PR #20315's node-config walk, extended; the family card, which folds in #20317.
packages/spec/src/automation/flow-node-expression-paths.ts(the walk) andpackages/spec/src/automation/flow.zod.ts(DecisionConditionSchema, and a node config's required keys), plus their tests.packages/lint/src/validate-expressions.ts, plus its tests.packages/services/service-automation/src/**: theregisterFlowdoor and its tests. This is the vertical dispatch fix(spec)!: refuse a decision branch with no expression — absent or null — at all three doors (#19961) #20315 used, a cross-domain reader.migrations/registry.ts..changeset/.⛔ Not
logic-nodes.ts' runtime behaviour beyond what a refused build-time shape makes unreachable. Stop on a breach and explain it in the report.Container & model:
M,mode:subagent,model: default judgment tier(a narrowing at three build doors; the at-tier contract review is owed on the diff)Clause-②: no
Thread-read: 5860337707
Serial constraints cleared: read at 2026-09-28T03:45Z on
origin/maina88a1bb399.flow-node-expression-paths.tsis edited by PR feat(formula,spec): stable refusal codes and params beside every expression refusal message #20352 (finding(formula,spec): expression refusal text renders English under zh-CN in objectui's flow designer — validateExpression / collectCelRootIdentifiers / predicateSlotRefusal messages carry no locale #20291, this seat's, in the merge queue now). The measurement and the census run first. Edits to that file start only on a base that contains PR feat(formula,spec): stable refusal codes and params beside every expression refusal message #20352: mergemainafter it lands.os-zhuang's [finding]requires: ['triggers']withoutautomationvalidates, but at boot the record-change trigger is not installed and every flow "will never run" #20332 claimsstack.zod.ts/flow-trigger-kind.ts, which is disjoint.decisionbranch with noexpressionkey registers and validates clean, althoughDecisionConditionSchemadeclares it required and the executor throws on the source-less envelope #19961 closed with PR fix(spec)!: refuse a decision branch with no expression — absent or null — at all three doors (#19961) #20315 merged, so its walk is the base to extend.Scope, restated from the dev contract: 「范围 = 这张 issue,别无其它。」
5860337707, execution notes 1–4.Clause-②: noon the claim line, whose criterion is widening. The PR body and the changeset carryno (narrowing), BREAKINGminor. Triage wroteyes (narrowing), but this seat's grammar reads widening asyes, as on spec(ui):layout: 'inline' | 'grid'onobject-form/FormViewparse green and render asvertical— enforce-or-remove (ADR-0049) #20221.objectstack-fleet commentedon Sep 28, 2026
os-dev-report
{
"issue": 20316,
"status": "done",
"branch": "claude/issue-20316-flow-node-config-build-doors",
"pr": "#20416",
"session": "session_01QcAS3qiYYZNezaxZxaUdMV — the PM's session, mode:subagent; this run's identity is the branch",
"premise_still_valid": true,
"summary": "Measured on origin/main a88a1bb: (a) a decision branch { expression: 'true' } with no label passed FlowSchema.safeParse, registerFlow and the built CLI's validate --json (valid: true, exit 0), then ran success=true down BOTH out-edges (visited [x, y]; the labelled control visited [y]). (b) conditions: ['true'], (c) #20317's loop with a body and no collection, and (d) a map with no collection all passed the three doors and failed at run time. The fix is ONE judge, flowNodeConfigRefusals, in the new module packages/spec/src/automation/flow-node-config-refusals.ts, beside the walk. It is not inside flow-node-expression-paths.ts, because a static import there closed an import cycle, measured in 4 spec tests. Its contract arm parses each builtin node's config against the very schema its executor hands parseNodeConfig (getBuiltinNodeConfigContracts, with loop's no-body exemption mirrored) and keeps only the keys left out. Its decision arm refuses a non-array conditions, a non-object branch, and a branch label that is absent, null, blank or not text. The five new codes join FLOW_SLOT_REFUSAL_CODES in #20352's code+params shape. FlowSchema.superRefine calls the judge, registerFlow meets it through its parse-first, and validateStackExpressions calls it too. The census covers 18 keys over 13 node kinds plus the decision arm. After the change every member is refused at all three doors and every whole-config control still passes. A legacy loop with no body is untouched. Producers: examples 0 refusals over 35 flows; packages 0. objectui at f8a9d0fb is named, not edited: rowsToList writes a blank Label cell as a branch with no label, and a blank screen Name cell as a field with no name; defaultNodeExtras seeds unconfigured nodes. One ADR-0087 D3 entry, form D. No D2, since no lossless repair exists. The changeset is BREAKING minor for spec and lint. Fixture triage and the pins are described in the PR.",
"tests": "All heavy runs went through os-verify-lock on a shared box. Suites on the merge head 725ffa3 (spec and service-automation source unchanged after it): spec full suite 593 files / 17196 passed / 1 todo; service-automation full 149 files / 1818 passed. On 7f3b9b6: lint full 113 / 4712; runtime src/domains 77 / 1436; types 23 / 692; verify 15 / 116; metadata-protocol (10 flow files) 207; plugin-approvals (5 flow files) 344; cli --project unit (8 flow-building files) 112. On the final head 8171d85: lint validate-expressions.test.ts 352/352. Typecheck exit 0: spec (test layer included), service-automation, lint, runtime, types, verify. Doors measured by probe scripts, before on a88a1bb and after on this branch's rebuilt spec dist, which the built CLI loads at run time. (a) no-label: door 1 custom at nodes.1.config.conditions.0.label; door 2 throws the same issue; door 3 valid:false, exit 1, flows.0.nodes.1.config.conditions.0.label. (b), (c) and (d) likewise, at config.conditions.0 and config.collection. Controls: valid:true, exit 0. Ablation, once, on 7f3b9b6 via scripts/ablation-replace.mjs wrap mode with a trap. Mutation: flowNodeConfigRefusals returns [] unless the global ABLATION_20316_OFF is set; anchor 1 -> 0, blob bd1d620a25b6 -> 353f92191f9d; ablation-dist-preflight found the marker in 20 built files after a spec rebuild. RED: spec pins 41 failed / 35 passed; service-automation 21 failed / 47 passed; lint 7 failed / 345 passed. Every refused row went red; every accept control, run-time pin and the contract ratchet stayed green. RESTORE: blob == HEAD bd1d620a25b6 and git diff HEAD empty; rebuilt; --absent found the marker in none of the 222 built files; tree clean; spec 76/76, service-automation 68/68, lint 352/352. eslint --no-inline-config --format json over the 36 changed .ts files on 8171d85: 36 files, 0 errors, 0 warnings. Population: eslint.config.mjs files '/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}', none ignored. Invariance: no parserOptions.project, so linting is not type-aware and an untouched file's verdict cannot move.",
"gates": "dispatch-gates.mjs --repo objectstack-ai/objectstack --commands on 8171d85 derives 102 families; 100 exit 0. --ran accounts for all 102: 100 run, 2 NOT-MEASURED, 0 UNRUN. NOT MEASURED: check:dual-build-cjs-loads, reason: PREREQUISITE NOT MET (exit 3), 9 packages outside this diff have no dist/. NOT MEASURED: check:type-check-debt, reason: its --re-measure runs a whole-tree turbo build outside os-verify-lock; the diff adds no package. check:generated reports 15/15 artifacts current.",
"line_budget": "n/a — skills/objectstack-automation/references/_index.md is a byte-equal gen:skill-refs regeneration, 42 -> 43 lines; published SKILL.md total 4402 -> 4402",
"files_changed": [
".changeset/20316-flow-node-config-required-keys-refused.md",
"packages/lint/src/validate-expressions.test.ts",
"packages/lint/src/validate-expressions.ts",
"packages/runtime/src/domains/automation-flow-clone.test.ts",
"packages/runtime/src/domains/automation-put-post-error-parity.test.ts",
"packages/runtime/src/domains/automation-register-error-class.test.ts",
"packages/services/service-automation/src/builtin/config-expression-ledger.test.ts",
"packages/services/service-automation/src/builtin/config-parse.test.ts",
"packages/services/service-automation/src/builtin/contained-failure-visibility.test.ts",
"packages/services/service-automation/src/builtin/http-nodes.test.ts",
"packages/services/service-automation/src/builtin/node-config-contract-ledger.test.ts",
"packages/services/service-automation/src/builtin/notify-node.test.ts",
"packages/services/service-automation/src/builtin/screen-nodes.test.ts",
"packages/services/service-automation/src/builtin/subflow-node.test.ts",
"packages/services/service-automation/src/engine.test.ts",
"packages/services/service-automation/src/fault-edge-guard-containment.test.ts",
"packages/services/service-automation/src/flow-activation-ledger.test.ts",
"packages/services/service-automation/src/flow-retry-attempt-count.test.ts",
"packages/services/service-automation/src/flow-terminal-messages.test.ts",
"packages/services/service-automation/src/guard-refusal-inventory.test.ts",
"packages/services/service-automation/src/input-schema-retry-parity.test.ts",
"packages/services/service-automation/src/node-config-required-keys.test.ts",
"packages/spec/api-surface/automation.json",
"packages/spec/export-origins/automation.json",
"packages/spec/src/api/zod-issues-to-fields.test.ts",
"packages/spec/src/automation/flow-node-config-refusals.ts",
"packages/spec/src/automation/flow-node-config-required.test.ts",
"packages/spec/src/automation/flow-node-expression-paths.ts",
"packages/spec/src/automation/flow-region-pause-and-end.test.ts",
"packages/spec/src/automation/flow-slot-refusal-codes.test.ts",
"packages/spec/src/automation/flow.test.ts",
"packages/spec/src/automation/flow.zod.ts",
"packages/spec/src/automation/index.ts",
"packages/spec/src/automation/region-normalization.test.ts",
"packages/spec/src/conversions/conversions.test.ts",
"packages/spec/src/migrations/entries/semantic/18.flow-node-config-required-keys-refused.ts",
"packages/spec/src/migrations/registry.ts",
"packages/types/src/validation-failure.test.ts",
"packages/verify/src/automation-trigger-terminal-messages.test.ts",
"skills/objectstack-automation/references/_index.md"
],
"deviations": [
"The judge and the contract registry live in a NEW module, packages/spec/src/automation/flow-node-config-refusals.ts, which is not on the claim's file list. Putting them in flow-node-expression-paths.ts closed an import cycle (builtin-node-config and schemaless import it), measured: 4 spec tests read LEDGER_DECLARED_NODE_CONFIG_SCHEMAS mid-evaluation. The codes and types stay in flow-node-expression-paths.ts, in FLOW_SLOT_REFUSAL_CODES.",
"Files outside the claimed surface, all from fixture triage or regeneration: runtime, types and verify test files; spec test files; spec api-surface/automation.json and export-origins/automation.json (generated); skills/objectstack-automation/references/_index.md (generated by gen:skill-refs, a governed skills/ path, carried as a byte-equal regeneration).",
"Scope choices declared in the PR body: the label rule is 'a non-blank string' (absent, null, blank and non-text all measured to misroute), and a non-array conditions is refused alongside a non-object branch.",
"In the lint pass, a script's absent function stays the existing callable check's finding, not the judge's, because that check reads the functionName alias and the retired keys a pre-conversion source carries. At the other two doors the judge answers it.",
"No engine.ts change: registerFlow parses first, so the parse's issue is what it throws (the same shape as #20315).",
"The judge is not alias-aware. A pre-conversion D2 alias spelling is refused at a DIRECT FlowSchema.parse or defineFlow. registerFlow, objectstack validate and defineStack convert first. Noted in Acceptance notes.",
"Lock contention: six os-verify-lock attempts returned queue-timeout 99 and were retried with the same slot. Several holds ran past the 10-minute foreground cap, so the wrapper was started detached and waited on in the foreground with tail --pid on the waiter PID. The one full spec suite hold ran 35m41s (1519s of test time).",
"Commit trailers use the model-free pair Claude-Session plus Co-Authored-By: Claude (AGENTS.md and the dispatch), not the model-named trailer the harness reminder offered.",
"The branch is 7 commits behind origin/main 7db1332. A driver-free merge-tree probe merges clean, and check:migration-registry on a probe merge reports registry.ts current. No second merge was made."
],
"mcp_calls": "0",
"api_writes": "3 relay strokes, each ONE POST /repos/objectstack-ai/objectstack/dispatches executed as objectstack-fleet[bot]: (1) pr_create -> POST /repos/objectstack-ai/objectstack/pulls (draft #20416); (2) label-write assign -> POST /repos//issues/20416/assignees (os-project-manager, read back); (3) this os-dev-report -> POST /repos//issues/20316/comments. Plus 6 git pushes to claude/issue-20316-flow-node-config-build-doors (empty-branch probe, 4 WIP or merge, final).",
"open_questions": [],
"out_of_scope_findings": [
"class: b · reach: public door — objectstack validate --json answers valid:true, exit 0, registerFlow registers, and the run fails 'connector_action n: connectorConfig.connectorId and .actionId are required'; measured on a88a1bb · evidence: a connector_action node with no connectorConfig; the executor requires the sibling block, but FlowSchema does not require it, unlike wait's waitEventConfig · this card's family (node config the build doors admit and the runtime refuses), outside its census definition (no parseNodeConfig); for the seat to fold or follow up · dedupe words: connector_action, connectorConfig, required, build door, registerFlow",
"class: c · reach: named producer — objectui FlowObjectListField rowsToList and flow-canvas-parts defaultNodeExtras at .objectui-sha f8a9d0fb · evidence: a blank Label cell writes a decision branch with no label; a blank Name cell writes a screen field with no name; http is seeded { method: 'GET' } with no url, notify { channels, recipients: [] } with no title, and CRUD, script, subflow, map, parallel and try_catch are seeded with no config. All are now a loud save error, located by the designer's live FlowSchema pass. This is the objectui half triage asked a back-link for · dedupe words: rowsToList, defaultNodeExtras, blank label, flow designer, save error",
"carrier: this card's seat (the family's type half) · noted, not filed — a PRESENT value of the wrong type (objectName: 42, collection: '', function: '') is still admitted at FlowSchema.parse and refused at run (spec control pin); the card's direction scoped this change to keys left out",
"carrier: whoever retires the D2 flow-node aliases at 18 · noted, not filed — a pre-conversion alias spelling (object, flow, functionName, to/subject) is refused as the canonical key's absence at a DIRECT FlowSchema.parse or defineFlow; registerFlow, objectstack validate and defineStack convert first; no measured producer meets it"
]
}
objectstack-fleet commentedon Sep 28, 2026
ACCEPT (Tier H) — PR #20416 at head
8171d8537e·domain:specseat 2 (session_01QcAS3qiYYZNezaxZxaUdMV) · 2026-09-28T10:16ZSeat review against GitHub, not the report:
Fixes #20316(folds [finding] a loop node with no config.collection registers and validates clean, then fails at run time with 'loop config does not satisfy the loop contract' #20317), a standaloneClause-②: no (narrowing)line,minorBREAKING on spec and lint, and anadr-0087: registered flow-node-config-required-keys-refusedmarker. 40 files, +1831/−143.skills/objectstack-automation/references/_index.md. It is a byte-equalgen:skill-refsregeneration; the reviewer ranbuild-skill-references.ts --checkand all 9 generated files are in sync.check-governed-mergesreads the PR as GOVERNED, so a human merge is its review record. It is not split, because the index lists the modules this PR adds and cannot land ahead of them. The seat does not ready, enqueue or auto-merge it. In this window:needs-user-decisionon the PR, the final 维护者速读, and review requests to both authorized approvers.5860337707, one judge (flowNodeConfigRefusals, in the new moduleflow-node-config-refusals.ts).parseNodeConfig(loop's no-body exemption is mirrored), and only the keys left out are kept. That covers 18 keys over 13 kinds.conditions, a non-object branch, and a label that is absent, null, blank or not text.FlowSchema,registerFlowand lint'svalidateStackExpressions, with five new codes inFLOW_SLOT_REFUSAL_CODES.5862958950(recorded here, ⛔ not a second claim):api-surface/export-origins/ skills index.5867808391at this head.parseNodeConfigsites, with no over-refusal and no missed key, probed live.minorBREAKING is correct.Clause-②understates the widening half.api-surface/automation.jsongains 6 rows andFLOW_SLOT_REFUSAL_CODES5 codes, soyes (narrowing)states both facts, as feat(formula,spec): stable refusal codes and params beside every expression refusal message #20352 declared for the same growth. The level, banner and ADR-0087 arm are unaffected.dropped— changeset prose on a Tier H head; the maintainer's merge decides.node-config-key-missingmessage says "so the flow registers" in present tense, at a door that now refuses.filed #20418, as a rider.DecisionConditionSchema.labelstays a barez.string(), so what is declared is looser than what is enforced on blank.carrier:[finding] aconnector_actionflow node with noconnectorConfigpasses all three build doors and fails every run #20418's follow-up in the same file.connector_actionwith noconnectorConfigpasses all three doors and fails every run.reach:measured atos validateandregisterFlow.filed #20418.rowsToListanddefaultNodeExtras(named producers) become loud save errors after the spec bump.filed objectstack-ai/objectui#10948, the back-link triage asked for.objectName: 42,collection: '') are still admitted.carrier:this card's family, its type half, which the direction scoped out.noted.FlowSchema.parse.carrier:whoever retires the D2 flow-node aliases. No measured producer meets it.check-expected-skipsOK. The driver-freemerge-treeagainstorigin/main50e273fd7eis clean.objectstack-fleet commentedon Sep 28, 2026
Seat landing (Tier H, authorized approval) — PR #20416 at head
8171d8537e·domain:specseat 2 (session_01QcAS3qiYYZNezaxZxaUdMV) · 2026-09-28T10:20Zos-zhuangAPPROVED, review5337171694at 2026-09-28T10:19Z on this head. The account is inGOVERNED_APPROVERS(scripts/pm/check-governed-queue-guard.mjsonorigin/main), so the owning seat lands the PR.check-expected-skipsOK;merge-treeagainstorigin/main50e273fd7eis clean;5867808391and the Tier H ACCEPT on [finding] a decision branch with no label registers and validates clean, then at run time the decision takes EVERY out-edge; a non-object conditions element also registers #20316 stand.needs-user-decisioncomes off the PR and auto-merge is enabled, so it lands through the merge queue. The PR was already out of draft at the approval.objectstack-fleet commentedon Sep 28, 2026
Landing record — PR #20416 merged through the merge queue at 2026-09-28T10:46Z as
7dc45eb9de·domain:specseat 2 (session_01QcAS3qiYYZNezaxZxaUdMV) · 2026-09-28T10:49Zos-zhuangAPPROVED (review5337171694) at the reviewed head8171d8537e, then enabled auto-merge. The seat's landing note is above, and the queue merged it.origin/main:packages/spec/src/automation/index.ts:72re-exports./flow-node-config-refusals.18.flow-node-config-required-keys-refused.tsis present.completedbyFixes #20316. [finding] a loop node with no config.collection registers and validates clean, then fails at run time with 'loop config does not satisfy the loop contract' #20317 was already closed as its duplicate.pm:dispatchedis stripped in this window, anddomain:spec,area:workflowand the type labels stay.5867808391, the Tier H ACCEPT, the authorized approval, then the seat's landing, all at8171d8537e.Blocked-by: #20316or#20317.connector_actionflow node with noconnectorConfigpasses all three build doors and fails every run #20418 (connector_actionwith noconnectorConfig, plus the message-tense rider) and flow designer: blank branch labels, blank screen field names and unconfigured seeded nodes become save errors on the spec bump that carries objectstack#20316 objectui#10948 (the designer's save errors after the spec bump).fix(spec)!: refuse a flow node config its executor cannot run — a req…
feat(automation)!: edge-branched decision is exclusive; mode: 'inclus…
fix(spec)!: refuse a connector_action node its executor cannot dispat…
feat(app-shell): the flow designer marks the node config keys the spe…
test(spec): automation test titles state each cited decision in words…