Part of #13681 — the B-branch's lint and docs limbs, filed into the domain:devx queue by the domain:spec seat (session session_01GDA48PuRFrHyRfdkBz8m21) under the cross-seat path the retriage comment on that card prescribes (comment 5479171811: the lint limb is handed to its lane by a proper card, never privately). The visibility half (item 3, the run-summary contract) stays on #13681 and is in flight in the spec lane; this card does not depend on it — the lint rules and the docs spelling are authoring-time facts measured on origin/main today.
Reader: the domain:devx execution seat, at its next selection pass (this card is a plain pm:queue candidate; triage adds domain:* and type).
Ruling of record (do not re-decide)
Maintainer 2026-08-31, director decision batch #18, verbatim 「其他同意」 on the conditional ruling (issue comment 5478768627); branch selected by measurement (comment 5478879587): B — no new key (⛔ no loop.config.onIterationError), because loop { body: [ try_catch { try, catch } ] } measured as real per-iteration containment on the real AutomationEngine (5 elements, third fails, 5/5 iterations, elements 4–5 processed, run completes; measurement comment 5478851960).
Deliverables (from comment 5478879587 §B, items 1–2, and the retriage's three standing prohibitions in comment 5479171811)
- Lint rule A — loop without containment: a
loop whose body region contains a fallible node (any node that can return success: false or throw — at minimum every builtin action node, notify, connector_action, record writes) and no try_catch enclosing it ⇒ an authoring-time warning that names the loop node, the fallible node, and the prescribed spelling. Lands in packages/lint under the flow lint family. - Lint rule B — the near-miss shape, first target not an extra: a
try_catch node whose catch region is absent gives zero containment and dies exactly like an unwrapped node (measured). Name it at authoring time; the fix text is the minimal catch spelling below. - Docs: write
loop { try_catch { … } } into the flow docs as the orthography for per-iteration containment, carrying the measured minimal catch spelling verbatim (do not re-derive): catch cannot be empty — the region's nodes is .min(1), so catch: {} and catch: { nodes: [] } are both refused; the shortest usable catch is one bare assignment node with no config; edges may be omitted; errorVariable may be omitted. - Both lint rules ship with the positive control (a fixture that trips each rule) and the negative control (the documented spelling passes clean), and the docs example must type-check under
check:skill-examples / check:yaml-examples where applicable.
Acceptance (executable)
pnpm --filter @objectstack/lint exec vitest run <the flow-lint test file> shows: the unwrapped-loop fixture → warning naming the loop and the node; the try_catch-without-catch fixture → warning; the documented loop { try_catch { try, catch: [assignment] } } fixture → no finding.content/docs/automation/flows.mdx (or the page the devx seat judges canonical) documents the spelling with the minimal catch form above, and the example parses under FlowSchema.
Out of scope here
Generated by Claude Code
Part of #13681 — the B-branch's lint and docs limbs, filed into the
domain:devxqueue by thedomain:specseat (sessionsession_01GDA48PuRFrHyRfdkBz8m21) under the cross-seat path the retriage comment on that card prescribes (comment 5479171811: the lint limb is handed to its lane by a proper card, never privately). The visibility half (item 3, the run-summary contract) stays on #13681 and is in flight in the spec lane; this card does not depend on it — the lint rules and the docs spelling are authoring-time facts measured onorigin/maintoday.Reader: the
domain:devxexecution seat, at its next selection pass (this card is a plainpm:queuecandidate; triage addsdomain:*and type).Ruling of record (do not re-decide)
Maintainer 2026-08-31, director decision batch #18, verbatim 「其他同意」 on the conditional ruling (issue comment 5478768627); branch selected by measurement (comment 5478879587): B — no new key (⛔ no
loop.config.onIterationError), becauseloop { body: [ try_catch { try, catch } ] }measured as real per-iteration containment on the realAutomationEngine(5 elements, third fails, 5/5 iterations, elements 4–5 processed, run completes; measurement comment 5478851960).Deliverables (from comment 5478879587 §B, items 1–2, and the retriage's three standing prohibitions in comment 5479171811)
loopwhose body region contains a fallible node (any node that can returnsuccess: falseor throw — at minimum every builtin action node,notify,connector_action, record writes) and notry_catchenclosing it ⇒ an authoring-time warning that names the loop node, the fallible node, and the prescribed spelling. Lands inpackages/lintunder the flow lint family.try_catchnode whosecatchregion is absent gives zero containment and dies exactly like an unwrapped node (measured). Name it at authoring time; the fix text is the minimalcatchspelling below.loop { try_catch { … } }into the flow docs as the orthography for per-iteration containment, carrying the measured minimalcatchspelling verbatim (do not re-derive):catchcannot be empty — the region'snodesis.min(1), socatch: {}andcatch: { nodes: [] }are both refused; the shortest usablecatchis one bareassignmentnode with noconfig;edgesmay be omitted;errorVariablemay be omitted.check:skill-examples/check:yaml-exampleswhere applicable.Acceptance (executable)
pnpm --filter @objectstack/lint exec vitest run <the flow-lint test file>shows: the unwrapped-loop fixture → warning naming the loop and the node; thetry_catch-without-catchfixture → warning; the documentedloop { try_catch { try, catch: [assignment] } }fixture → no finding.content/docs/automation/flows.mdx(or the page the devx seat judges canonical) documents the spelling with the minimalcatchform above, and the example parses underFlowSchema.Out of scope here
FlowRunSummarySchemahas no failure count; nestedtry/catchsteps do not inherit the loop iteration;$errorbinds no row identity) — that is Aloopnode aborts the entire flow run when one iteration's node fails — a single bad row kills a whole scheduled sweep, and there is no per-iteration containment to opt into #13681's spec half, followed by a services card for the engine.loopdiscards its body's completed steps wholesale — the run summary reportsacted: 0while 5 writes actually happened (childSteps splice only after a SUCCESSFUL node result) #13803 (summary reportsacted: 0after a mid-loop death) — independent,domain:services, ⛔ do not merge.Generated by Claude Code