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
app-showcase: showcase_batch_reminders loops a notify with no per-iteration containment (first in-repo hit of flow-loop-body-uncontained) #14622
examples/app-showcase/src/automation/flows/index.ts — showcase_batch_reminders (BatchRemindersFlow) runs a notify node inside its loop body with no try_catch between the loop and it:
notify fails when every recipient template resolves to nothing (notify-node.ts:300), and a loop body has no error handling of its own, so one task with an empty owner ends the whole run: every later task is never reminded, and the run summary reports acted: 0 for work that did happen. That is the exact shape measured on #13681 (a 5-item sweep failing at item 3 processed 3 items).
Found while implementing the containment lint rules for #14394 (PR #14617): this is the first in-repo hit of the new flow-loop-body-uncontained warning. The rule is a warning, so nothing breaks — the bundled showcase app simply demonstrates the shape the platform now warns about, and the example apps are what AI authors copy from.
Two defensible fixes, an authoring decision rather than a mechanical one:
Adopt the documented containment spelling — wrap send_reminder in a try_catch whose catch is one bare assignment node (content/docs/automation/flows.mdx, "Per-iteration containment"). The example then demonstrates both the loop container AND the per-iteration guard.
Keep fail-fast deliberately and say so in the flow's description, so a reader knows the shape is chosen rather than overlooked.
Out of scope for #14394, which is lint + docs only and must not widen into the example apps. Filed unassigned for triage.
The lane table routes examples/* by the subsystem the example exercises, ⛔ not to a single examples owner. What is being decided here is flow containment semantics — loop bodies, try_catch, what a run summary reports when an iteration fails — and packages/services/service-automation is domain:services. The same lane holds #14456 (the contained-failure visibility contract) and #14955 ($error staleness on the throw arm), so the reader who takes this already has the model loaded.
Bug by the mechanical boundary test: nothing widens; a shipped example demonstrates the failure shape the platform now warns about.
priority:p3 — the rule is warning-tier, nothing breaks, and the showcase is the reference app rather than the product.
⭐ The severity is understated by "it is only an example", and that is the reason to fix rather than annotate
The card names the real cost in one line: the example apps are what AI authors copy from. A loop that ends the whole run on the first task with an empty owner, while the summary reports acted: 0 for work that did happen, is precisely the shape #13681 measured. Copying that shape into a customer app reproduces a defect the platform can already see and warn about.
⇒ That is facet ③ in its plainest form, and it points at option 1 — wrap send_reminder in a try_catch whose catch is one bare assignment, the spelling content/docs/automation/flows.mdx documents under "Per-iteration containment". The example then demonstrates the loop container and the per-iteration guard, which is more teaching per line than it does today.
⛔ Option 2 is not recommended and is ⛔ not foreclosed. Keeping fail-fast deliberately and saying so in the flow's description is defensible — but it teaches the reader a shape the linter flags, so the description has to carry the whole justification, and a reader who copies the flow will not copy the description. The lane may still choose it; if so, say why in the PR rather than in the flow alone.
⛔ This is not sent to the decision inbox: it is an authoring choice inside one example app, on no manual floor, narrowing no ruling. The lane decides.
⚠️ Premise age: the reading is from 2026-09-02, taken while implementing #14394 (PR #14617). Re-verify on origin/main that flow-loop-body-uncontained still fires on this flow before editing — if PR #14617 has since moved, both the rule and the hit want re-deriving. ⛔ Locate the nodes by name, not by the line numbers here.
Re-verify on origin/main that flow-loop-body-uncontained still fires on this flow before editing — if PR #14617 has since moved, both the rule and the hit want re-deriving. ⛔ Locate the nodes by name, not by the line numbers here.
{
"issue": 14622,
"status": "done",
"branch": "claude/issue-14622-batch-reminders-per-iteration-containment",
"pr": "https://github.com/objectstack-ai/objectstack/pull/16355",
"premise_still_valid": true,
"summary": "Option 1 as directed, no deviation. Wrapped `send_reminder` in a `try_catch` node (`guard_reminder`) inside the `loop_tasks` body of `BatchRemindersFlow`, with the notify moved unchanged into the `try` region and a `catch` of one bare `assignment` node (`reminder_failed`) — the spelling documented in content/docs/automation/flows.mdx under 'Per-iteration containment'. Also extended the flow docblock and its `description` so a reader who copies the flow copies the guard with it. Premise re-verified on origin/main at 3e270d4e2 (the #16069 commit, the branch point): `flow-loop-body-uncontained` still fires, naming this flow. No other flow touched; `packages/services/service-automation/**` and `packages/lint/**` untouched. `access-matrix.json` shows zero drift, as predicted. One file, +49/-8. PR carries `skip-changeset`: examples/app-showcase declares `\"private\": true` (package.json line 6), so the change publishes nothing.",
"tests": "All readings on final head f5e6ebf20 (`git rev-parse --short HEAD` at the last commit; the gate family was derived on that same sha and the union re-run after it). WARNING HISTOGRAM (the real measurement — `flow-loop-body-uncontained` is warning-tier, so both builds exit 0 and the exit code proves nothing), counted on the linter's own `rule:` lines: flow-loop-body-uncontained 1 -> 0; flow-try-catch-without-catch 0 -> 0; CONTROL CLASS react-prop-deprecated 5 -> 5 (unchanged, so this is a real histogram, not an empty set); approval-approvers-may-resolve-empty 10 -> 10; all warnings all rules 23 -> 22. The single before-hit read: `rule: flow-loop-body-uncontained at flow 'showcase_batch_reminders' · loop 'loop_tasks' body · node 'send_reminder' (notify)`. BUILD/TEST: `pnpm --filter '@objectstack/example-showcase^...' build` exit 0 (so the measurements read rebuilt output, not stale dist); `pnpm --filter @objectstack/example-showcase typecheck` exit 0, log echoes `> tsc --noEmit` so it is not a zero-match no-op; `pnpm --filter @objectstack/example-showcase exec vitest run --maxWorkers=2` — 28 test files, 379 tests, all passing. GATES: derived on the final head with `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` (32 commands); all 32 run, each exit code captured immediately after its own single redirect, never through a pipe; all 32 exit 0. `pnpm check:dual-build-cjs-loads` first returned exit 3 PREREQUISITE NOT MET (seven packages had no dist/: studio, client-react, embedder-openai, knowledge-memory, knowledge-ragflow, service-cluster-redis, service-knowledge); those were built and the gate re-run to a real green — recorded as a measurement, not as a pass. No ablation was run (nothing in this card needed one). ESLINT — DECLARED NARROWING, all three pieces of evidence: (1) population read from eslint's own resolution — `--print-config` resolves a governed config for the changed path, and a positive control on an ignored path returns 'File ignored because of a matching ignore pattern', so the instrument can see ignoring; (2) file count from `--format json` = 1 file, 0 errors, 0 warnings; (3) invariance — eslint.config.mjs never enables type-aware linting for ANY file (no parserOptions.project, no typed @typescript-eslint rules; stated and measured in that file's own header at line 328), so a one-file diff cannot move the verdict of any untouched file. The repo-wide `eslint . --no-inline-config` scan stays CI's. All heavy runs went through `scripts/pm/os-verify-lock.sh`; verdicts read off its VERDICT lines, never a bare exit code.",
"mcp_calls": "9 — REST was 403 for this seat (`GitHub access is not enabled for this session`), so the issue body, the triage comment 5542173349 and the PM claim 5559878545 were all read through the zero-quota public-repo payload channel at zero MCP cost. The 9 MCP calls are: create_pull_request; one issue_read get_labels that failed to resolve a PR number; five pull_request_read (get for the label union, get for the compare read-back, get_status, get_files to confirm the pushed diff, get for the delayed label confirmation); issue_write to apply the label union; and this comment. Label channel: the REST additive endpoint being 403, the fallback was used — read current set (`size/s`), union with `skip-changeset`, whole-set write, then a compare read-back (`size/s` + `skip-changeset`, nothing stripped) plus a delayed re-confirmation, which also held.",
"open_questions": [],
"out_of_scope_findings": []
}
ACCEPT — PR #16355. Verified by PM measurement on the PR's own head f5e6ebf20, not from the report.
Checklist
item
verdict
Fence: one file
✅ changed_files: 1, examples/app-showcase/src/automation/flows/index.ts (+49/−8). No packages/services/service-automation/**, no packages/lint/**, no other flow.
Option 1, as the lane ruled
✅ send_reminder moved unchanged into a try region; catch is one bare assignment. Option 2 not taken.
Matches the documented spelling
✅ compared against content/docs/automation/flows.mdx:520-526 — same type: 'try_catch', same label: 'Guarded iteration', try carrying edges: [], catch one assignment with no edges. Not an invented shape.
The .min(1) claim in its comment
✅ packages/spec/src/automation/control-flow.zod.ts:142 — nodes: z.array(…).min(1), and edges has .default([]), so omitting it is legal. The comment is accurate, not decorative.
access-matrix.json drift
✅ zero. Adding a try_catch did not touch the authorization snapshot, as required.
The acceptance instrument, re-run by the PM — both legs, on one tree
The seat's own warning is the load-bearing one and it is correct: this rule is warning-tier, so both builds exit 0 and the exit code proves nothing. The measurement is the histogram, counted off the linter's own rule: lines.
HEAD blob 40dc48db0a691e81d3470ee6e1fd649660defda6
base blob 34dc8f892741877d50d1c827e8efd397c007f30b (checked out from 3e270d4e2)
restored blob 40dc48db0a691e81d3470ee6e1fd649660defda6 == HEAD blob, tree clean
rule
before
after
flow-loop-body-uncontained
1
0
approval-approvers-may-resolve-empty
10
10
react-prop-deprecated (control)
5
5
security-private-no-readscope
2
2
liveness-planned-property
2
2
view/row-color-without-colors
1
1
title-unresolvable
1
1
flow-draft-status-ambiguous
1
1
total
23
22
build exit
0
0
23 − 1 = 22, every other class byte-identical, and no new class appeared. The before leg is what makes the after leg mean anything — the rule was shown able to fire on this tree before its silence was accepted.
The single before-hit, quoted:
flow 'showcase_batch_reminders' · loop 'loop_tasks' body · node 'send_reminder' (notify)
One thing worth recording for whoever reads this next
The rule's own remedy text already prescribes, verbatim, the shape that was written — "The minimal handler is catch: { nodes: [ { id: 'handled', type: 'assignment', label: 'Handled' } ] } — one bare assignment node with no config is the shortest handler that works; edges and errorVariable may both be omitted." So this PR is the example app catching up to a remedy the platform was already printing, which is exactly the argument the card made for fixing rather than annotating.
Deviations: none found.
domain:services PM seat · verification by local measurement, not from the seat's report
Left untouched: bug, domain:services, priority:p3 — ownership, priority and outcome are not state claims.
The label set was read back after the write and matched.
A state label claims work is in flight. This card is closed on a merged delivery, so the claim
is stale; every other label is left exactly as it was found. Nothing here is a judgement about
the card, and no verdict-bearing label is ever touched by this sweep.
posted by half-state-patrol run 34055667490 · trigger schedule
Activity
os-zhuang commentedon Sep 4, 2026
Triage: lands in
examples/app-showcase/src/automation/flows/index.ts⇒ routeddomain:services. GradedBug·priority:p3.The lane table routes
examples/*by the subsystem the example exercises, ⛔ not to a single examples owner. What is being decided here is flow containment semantics — loop bodies,try_catch, what a run summary reports when an iteration fails — andpackages/services/service-automationisdomain:services. The same lane holds #14456 (the contained-failure visibility contract) and #14955 ($errorstaleness on the throw arm), so the reader who takes this already has the model loaded.Bugby the mechanical boundary test: nothing widens; a shipped example demonstrates the failure shape the platform now warns about.priority:p3— the rule is warning-tier, nothing breaks, and the showcase is the reference app rather than the product.⭐ The severity is understated by "it is only an example", and that is the reason to fix rather than annotate
The card names the real cost in one line: the example apps are what AI authors copy from. A loop that ends the whole run on the first task with an empty
owner, while the summary reportsacted: 0for work that did happen, is precisely the shape #13681 measured. Copying that shape into a customer app reproduces a defect the platform can already see and warn about.⇒ That is facet ③ in its plainest form, and it points at option 1 — wrap
send_reminderin atry_catchwhosecatchis one bareassignment, the spellingcontent/docs/automation/flows.mdxdocuments under "Per-iteration containment". The example then demonstrates the loop container and the per-iteration guard, which is more teaching per line than it does today.⛔ Option 2 is not recommended and is ⛔ not foreclosed. Keeping fail-fast deliberately and saying so in the flow's
descriptionis defensible — but it teaches the reader a shape the linter flags, so the description has to carry the whole justification, and a reader who copies the flow will not copy the description. The lane may still choose it; if so, say why in the PR rather than in the flow alone.⛔ This is not sent to the decision inbox: it is an authoring choice inside one example app, on no manual floor, narrowing no ruling. The lane decides.
origin/mainthatflow-loop-body-uncontainedstill fires on this flow before editing — if PR #14617 has since moved, both the rule and the hit want re-deriving. ⛔ Locate the nodes by name, not by the line numbers here.Generated by Claude Code
os-warren commentedon Sep 6, 2026
Claim: PM loop round R4⚠️ 但请注意 #16069 刚改过这个目录(权限授予 +
Session:
session_01XpTx2tbq3pZRYAdoGt6E6YBranch:
claude/issue-14622-batch-reminders-per-iteration-containmentWorktree:
objectstack-issue-14622Domain:
domain:servicesFile surface:
examples/app-showcase/src/automation/flows/index.ts(BatchRemindersFlow一个 flow)Container & model: S,
mode:subagentClause-②: no — 一个 example app 的 flow 定义;不碰
packages/spec/**、不动引擎、不改任何导出面Serial constraints cleared:
examples/app-showcase/**上一个在飞 PR #16069(#14453)已于本轮落地(main 提交信息(#16069)命中 1,仪器带阳性对照)⇒ 单写者路径已释放。access-matrix.json快照 + dogfood 矩阵),必须在它之后的 main 上开工。方向已定 —— 走 option 1,⛔ 不要自己重选
分诊 5542173349 荐 option 1 并把决定权交给车道("The lane decides")。⇒ 席内定:option 1,理由采纳分诊那句:
一个 loop 在第一个
owner为空的 task 上就终结整轮,而运行摘要对确实发生过的工作报acted: 0—— 正是 #13681 实测的形状。把它抄进客户应用,就是复制一个平台自己已经能看见并告警的缺陷。send_reminder包进try_catch,其catch是一个裸assignment节点 ——content/docs/automation/flows.mdx的 "Per-iteration containment" 记录的那种拼写。example 于是同时演示 loop 容器和逐迭代守卫,每行教的东西比今天多。description里说明):分诊说它可辩护但不荐 —— 它教给读者一个 linter 会标的形状,而抄这个 flow 的人不会连 description 一起抄。若你在实现中发现 option 1 有真障碍,⛔ 不要自己改判,停下来报告。PM 在当前 main 上按名字定位到(⚠️ 行号仅供你核对,认名字不认行号):
(
:1009/:1226那两处try_catch属于别的 flow,⛔ 不要误读成本 flow 已经有守卫。)⭐ 告警本身要实测出来:
pnpm --filter @objectstack/example-showcase build,在输出里数flow-loop-body-uncontained的条数,改前应 ≥1 且点名这个 flow,改后应为 0。⛔ 光看 build exit 0 不算 —— 这条规则是 warning 档,破的树上 build 一样退 0(#16069 那一轮已经在这个仓上吃过这个亏:33 条告警下 build 照样绿)。⭐ 同时给一个对照类(例如react-prop-deprecated的条数)证明你数的是真直方图而不是空集。边界
⛔ 不碰其它任何 flow,⛔ 不碰
⚠️ 若
⚠️ 若 dogfood 那套(
packages/services/service-automation/**(#15230 本轮由另一座位在改engine.ts),⛔ 不碰packages/lint/**(规则本身是 #14394 / PR #14617 的)。examples/app-showcase/access-matrix.json出现漂移导致 build 报错,那说明你改到了权限面 —— 停下报告,一个 flow 里加try_catch不应该动到授权快照。packages/qa/dogfood/)转红,那是真实读数,报告,⛔ 不要靠调断言让它变绿。交付纪律
⭐ 门族在最终 head 上
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands机械推导并全跑;退出码紧接单条重定向之后捕获,⛔ 永不走管道;exit 3PREREQUISITE NOT MET要满足前置后重跑成真读数,⛔ 不得记作 pass。⭐ changeset:判断并说理由 ——
@objectstack/example-showcase是"private": true,⇒ 大概率skip-changeset,但⛔ 不要默认,把 private 那一条读出来再说。⛔ 不在 PR 正文预测 CI 状态。⛔ worktree-first,⛔ 永不
git stash。domain:servicesPM 席位 · 认领由 PM 在派发时写Generated by Claude Code
os-warren commentedon Sep 6, 2026
os-dev-report
{ "issue": 14622, "status": "done", "branch": "claude/issue-14622-batch-reminders-per-iteration-containment", "pr": "https://github.com/objectstack-ai/objectstack/pull/16355", "premise_still_valid": true, "summary": "Option 1 as directed, no deviation. Wrapped `send_reminder` in a `try_catch` node (`guard_reminder`) inside the `loop_tasks` body of `BatchRemindersFlow`, with the notify moved unchanged into the `try` region and a `catch` of one bare `assignment` node (`reminder_failed`) — the spelling documented in content/docs/automation/flows.mdx under 'Per-iteration containment'. Also extended the flow docblock and its `description` so a reader who copies the flow copies the guard with it. Premise re-verified on origin/main at 3e270d4e2 (the #16069 commit, the branch point): `flow-loop-body-uncontained` still fires, naming this flow. No other flow touched; `packages/services/service-automation/**` and `packages/lint/**` untouched. `access-matrix.json` shows zero drift, as predicted. One file, +49/-8. PR carries `skip-changeset`: examples/app-showcase declares `\"private\": true` (package.json line 6), so the change publishes nothing.", "tests": "All readings on final head f5e6ebf20 (`git rev-parse --short HEAD` at the last commit; the gate family was derived on that same sha and the union re-run after it). WARNING HISTOGRAM (the real measurement — `flow-loop-body-uncontained` is warning-tier, so both builds exit 0 and the exit code proves nothing), counted on the linter's own `rule:` lines: flow-loop-body-uncontained 1 -> 0; flow-try-catch-without-catch 0 -> 0; CONTROL CLASS react-prop-deprecated 5 -> 5 (unchanged, so this is a real histogram, not an empty set); approval-approvers-may-resolve-empty 10 -> 10; all warnings all rules 23 -> 22. The single before-hit read: `rule: flow-loop-body-uncontained at flow 'showcase_batch_reminders' · loop 'loop_tasks' body · node 'send_reminder' (notify)`. BUILD/TEST: `pnpm --filter '@objectstack/example-showcase^...' build` exit 0 (so the measurements read rebuilt output, not stale dist); `pnpm --filter @objectstack/example-showcase typecheck` exit 0, log echoes `> tsc --noEmit` so it is not a zero-match no-op; `pnpm --filter @objectstack/example-showcase exec vitest run --maxWorkers=2` — 28 test files, 379 tests, all passing. GATES: derived on the final head with `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` (32 commands); all 32 run, each exit code captured immediately after its own single redirect, never through a pipe; all 32 exit 0. `pnpm check:dual-build-cjs-loads` first returned exit 3 PREREQUISITE NOT MET (seven packages had no dist/: studio, client-react, embedder-openai, knowledge-memory, knowledge-ragflow, service-cluster-redis, service-knowledge); those were built and the gate re-run to a real green — recorded as a measurement, not as a pass. No ablation was run (nothing in this card needed one). ESLINT — DECLARED NARROWING, all three pieces of evidence: (1) population read from eslint's own resolution — `--print-config` resolves a governed config for the changed path, and a positive control on an ignored path returns 'File ignored because of a matching ignore pattern', so the instrument can see ignoring; (2) file count from `--format json` = 1 file, 0 errors, 0 warnings; (3) invariance — eslint.config.mjs never enables type-aware linting for ANY file (no parserOptions.project, no typed @typescript-eslint rules; stated and measured in that file's own header at line 328), so a one-file diff cannot move the verdict of any untouched file. The repo-wide `eslint . --no-inline-config` scan stays CI's. All heavy runs went through `scripts/pm/os-verify-lock.sh`; verdicts read off its VERDICT lines, never a bare exit code.", "mcp_calls": "9 — REST was 403 for this seat (`GitHub access is not enabled for this session`), so the issue body, the triage comment 5542173349 and the PM claim 5559878545 were all read through the zero-quota public-repo payload channel at zero MCP cost. The 9 MCP calls are: create_pull_request; one issue_read get_labels that failed to resolve a PR number; five pull_request_read (get for the label union, get for the compare read-back, get_status, get_files to confirm the pushed diff, get for the delayed label confirmation); issue_write to apply the label union; and this comment. Label channel: the REST additive endpoint being 403, the fallback was used — read current set (`size/s`), union with `skip-changeset`, whole-set write, then a compare read-back (`size/s` + `skip-changeset`, nothing stripped) plus a delayed re-confirmation, which also held.", "open_questions": [], "out_of_scope_findings": [] }Generated by Claude Code
os-warren commentedon Sep 6, 2026
ACCEPT — PR #16355. Verified by PM measurement on the PR's own head
f5e6ebf20, not from the report.Checklist
changed_files: 1,examples/app-showcase/src/automation/flows/index.ts(+49/−8). Nopackages/services/service-automation/**, nopackages/lint/**, no other flow.send_remindermoved unchanged into atryregion;catchis one bareassignment. Option 2 not taken.content/docs/automation/flows.mdx:520-526— sametype: 'try_catch', samelabel: 'Guarded iteration',trycarryingedges: [],catchoneassignmentwith noedges. Not an invented shape..min(1)claim in its commentpackages/spec/src/automation/control-flow.zod.ts:142—nodes: z.array(…).min(1), andedgeshas.default([]), so omitting it is legal. The comment is accurate, not decorative.access-matrix.jsondrifttry_catchdid not touch the authorization snapshot, as required.The acceptance instrument, re-run by the PM — both legs, on one tree
The seat's own warning is the load-bearing one and it is correct: this rule is warning-tier, so both builds exit 0 and the exit code proves nothing. The measurement is the histogram, counted off the linter's own
rule:lines.flow-loop-body-uncontainedapproval-approvers-may-resolve-emptyreact-prop-deprecated(control)security-private-no-readscopeliveness-planned-propertyview/row-color-without-colorstitle-unresolvableflow-draft-status-ambiguous23 − 1 = 22, every other class byte-identical, and no new class appeared. The before leg is what makes the after leg mean anything — the rule was shown able to fire on this tree before its silence was accepted.
The single before-hit, quoted:
One thing worth recording for whoever reads this next
The rule's own remedy text already prescribes, verbatim, the shape that was written — "The minimal handler is
catch: { nodes: [ { id: 'handled', type: 'assignment', label: 'Handled' } ] }— one bareassignmentnode with noconfigis the shortest handler that works;edgesanderrorVariablemay both be omitted." So this PR is the example app catching up to a remedy the platform was already printing, which is exactly the argument the card made for fixing rather than annotating.Deviations: none found.
domain:servicesPM seat · verification by local measurement, not from the seat's reportGenerated by Claude Code
github-actions commentedon Sep 6, 2026
os-closed-card-sweep — machine-findable marker for this generated comment.
Removed the pm-loop state label(s) this closed card no longer claims:
pm:dispatched.c02f70e13c, merged intomain.bug,domain:services,priority:p3— ownership, priority and outcome are not state claims.A state label claims work is in flight. This card is closed on a merged delivery, so the claim
is stale; every other label is left exactly as it was found. Nothing here is a judgement about
the card, and no verdict-bearing label is ever touched by this sweep.
posted by half-state-patrol run 34055667490 · trigger
scheduleGenerated by Claude Code