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
showcase: showcase_project_escalation fails terminally (CEL no such overload on null budget) whenever a project with no budget turns red, so the escalation notifies nobody #28951
Filed by the dispatching PM seat session for the W4-C3a runner session_01JQwxbZsvATYMNMArStUo2M, whose own write was refused by its session permissions (RUNNER rule 2: staged text, posted verbatim through the same door).
立卡门类别: ① 产品缺陷 — 具名落点 (examples/app-showcase/src/automation/flows/index.ts, ProjectEscalationFlow edges e2/e3) + 复现
reach: 公开入口实测一次错误结果 — PATCH /api/v1/data/showcase_project/ID {"health":"red"} on a project without a budget makes the flow run fail terminally (two fresh boots at 7fa9873)
业务理由: the showcase is the reference app new users read and copy. Its escalation flow fails on an ordinary record (no budget, or no owner), so the "red project alerts someone" scenario it demonstrates does not happen, and the pattern it teaches (an unguarded comparison on an optional field) is the bug. Not doing it: the reference teaches a failing pattern and its own demo escalations notify nobody.
动手的读者: the domain:devx seat (example apps / dogfood); triage first touch confirms the lane, then os-dev.
查重: showcase_project_escalation → 3 hits, 0 open (#7746, #4414, #16405 closed, all different); escalation budget null → 187 hits, 1 open (#6367, a seat post). No duplicate.
QA-source: #28942 · records-forms.validation-rule-type-matrix · observed beside the clauses
What happens. The flow triggers on health == "red" && previous.health != "red", then branches at the triage decision on edge conditions budget > 200000 (e2) and budget <= 200000 (e3). budget is an optional currency field (project.object.ts) and nothing defaults it. For a project with no budget the CEL comparison has no overload for null, so the run dies at the decision. The server logs ERROR Trigger-fired run of flow 'showcase_project_escalation' failed (trigger 'record_change') … "condition failed to evaluate as CEL: no such overload: dyn‹null› > int … budget > 200000"; nothing retries, and neither the owner nor the exec is alerted. The record write itself succeeds (200).
Reproduction (stock objectstack dev --seed-admin, framework 7fa9873; reproduced on two fresh boots):
Admin. POST /api/v1/data/showcase_project {"name":"esc","account":"ANY_SHOWCASE_ACCOUNT_ID","status":"planned","health":"yellow","owner":"admin@objectos.ai"} → 201 (no budget).
The server log shows the ERROR line above; the flow run fails.
Control: the same with "budget":1000 → no error; the run takes e3 → notify_normal.
Related, on the same flow (seen once): a budgeted project with no owner fails at notify_normal with "notify: at least one recipient is required, but every recipient value expression evaluated to nothing: has(record.owner) ? record.owner : null". owner is optional too.
Expected: the reference app's flow handles its own optional fields — guard the decision (has(record.budget) && record.budget > 200000, plus an otherwise edge), and route around the owner notify when there is no owner, as the node's own remedy text suggests.
Actual: a terminal flow failure on an ordinary record.
Read fresh. The fix lands in ProjectEscalationFlow edges e2/e3 and the owner notify in examples/app-showcase/src/automation/flows/index.ts; the body's Path: line is a file, not a defined path item, and the card was observed beside records-forms.validation-rule-type-matrix, not on that capability. Examples route to the subsystem they exercise (automation), so the lane is domain:server, not devx. The reference flow failing terminally on an ordinary record is a user-visible defect, so it is p2 and goes to pm:queue. Under 「平台相关的功能,应该尽量先改平台」 the claimant should first say whether the flow validator can warn about a decision with no otherwise edge, and treat guarding the showcase as the minimum fix.
Claim: PM loop round 1 · card thread · 2026-11-02T05:18Z
Session: session_01FEkQ5oc56xChD6Wrc4cGSt (the seat); the card thread is session_01FEkQ5kZjdnPFmYVCV7PUQU
Account: os-project-manager (the seat's linked user; the card's assignee)
Branch: claude/project-thread-zxfajt (the card thread's harness-pinned branch; 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 the card's grade and this seat's rulings in the order; card assigned to seat 5 by seat 1's queue split after Leehom's 「队列进了很多任务,抓紧派发」 (2026-11-02T05:15Z).
File surface: examples/app-showcase/src/automation/ and its tests (packages/qa/dogfood if the test lives there), changesets. ⛔ No packages/services/**, ⛔ no packages/spec/**.
Container & model: S, mode:thread, model and effort as the thread was opened
Clause-②: no
Metadata-protocol: none
Responsibility: the code is the showcase escalation flow · a red-turned project with no budget (or no owner) fails the run terminally and notifies nobody · reached by every showcase user and every reader who copies the reference flow
Thread-read: 6569465543
Premise: holds (read at origin/main135054eb: ProjectEscalationFlow edges e2/e3 compare budget unguarded; the owner notify has no route around a missing owner)
Serial constraints cleared: none needed.
Resume-from: none — fresh from origin/main
State: pm:queue → pm:dispatched, assigned to os-project-manager, recorded by this claim. Order: pm-orders/server/order-28951.md in the project.
os-dev-report
{
"issue": 28951,
"status": "done",
"branch": "claude/project-thread-zxfajt",
"pr": "#29018",
"head": "39cd0102",
"session": "session_01FEkQ5kZjdnPFmYVCV7PUQU",
"premise_still_valid": true,
"summary": "ProjectEscalationFlow now runs to the end on a project with no budget or no owner. e2 is record.budget != null && record.budget > 200000 and e3 is the triage decision's isDefault edge, so no budget routes as the normal path (the flow's wording 'Critical budget?' reads no budget as not critical). notify_normal and converge fall back to role:owner (the exec) when the owner is blank (isBlank), and the critical branch's owner alert sits behind its own has_owner decision, since the Exec branch beside it already alerts role:owner. The notify node's required-recipient refusal is untouched; no engine, CEL, services or spec file changed.",
"tests": "New examples/app-showcase/test/project-escalation-optional-fields.test.ts, 7 cases on a real ObjectKernel (ObjectQL over sqlite + automation plugin, the app's real showcase_project and flow; the run is handed the stored row, which each case asserts carries budget: null / owner: null; messaging is an audience recorder, fetch stubbed). Red on base (flows file at 135054e, 6 cases then): 4 failed / 2 passed: condition failed to evaluate as CEL: no such overload: dyn‹null› > int (no budget; neither), Node 'notify_normal' failed: notify: at least one recipient is required … (no owner), Node 'alert' failed: parallel 'alert': branch failed — Node 'alert_owner' failed … (critical, no owner); both controls green. Head: Tests 7 passed (7). Ablation 1 (ablation-replace, anchor x1, blob 9272a372 -> 9aa0d008, e2 guard dropped): 2 failed / 5 passed; restored, blob == HEAD, git diff HEAD empty. Ablation 2 (blob 9272a372 -> 900018be, has_owner edge -> 'true'): 1 failed / 6 passed; restored the same way. No dist on the path (test imports src). Live REST on pnpm dev -- --fresh, card steps (create as admin, PATCH health red -> 200) for no budget / no owner / control: base logs two ERROR Trigger-fired run of flow 'showcase_project_escalation' failed (trigger 'record_change') lines (no such overload: dyn‹null› > int … budget > 200000 and the notify_normal recipient refusal), runs completed/failed/failed; head logs none, runs completed x3 (start, triage, alert skipped, notify_normal, converge). On 39cd010 (merges origin/main 00d0668): pnpm --filter @objectstack/example-showcase test Test Files 43 passed (43), Tests 518 passed (518); typecheck exit 0; validate exit 0 (no row names the flow); eslint --no-inline-config on both files exit 0.",
"gates": "dispatch-gates --commands --repo objectstack-ai/objectstack: 40 families, each exit captured before any pipe; 39 exit 0. --ran: ✓ dispatch-gates --ran: 40 derived famil(ies) accounted for — 39 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3). NOT MEASURED: check:dual-build-cjs-loads, reason: needs every published package's dist; read 3 (seven unbuilt packages), then 1 after building them, with 82 Cannot find module '../chunk-….js' findings in packages this diff does not touch (stale chunks from overlapping local rebuilds). The diff changes no published package; CI's fresh build is the reading. PR check-runs: in_progress at report time.",
"line_budget": "263 changed lines (+257 / -6) over 2 files, under the 3000 human-merge threshold",
"files_changed": [
"examples/app-showcase/src/automation/flows/index.ts",
"examples/app-showcase/test/project-escalation-optional-fields.test.ts"
],
"deviations": [
"Ruling 1 spelling: the order named has(record.budget) && record.budget > 200000. Measured with ExpressionEngine on {budget: null} it still faults (no such overload: dyn‹null› > int): has() is true on a null key, and the flow's record is total over declared fields (the card's own log shows dyn‹null›). Used record.budget != null && record.budget > 200000, the spelling skills/objectstack-formula teaches, showcase_project's own validation rules use, and validate-null-guards accepts as dominating. The ruling's intent (guard the optional field, no budget routes normal) is kept; the no-budget edge is the decision's isDefault 'otherwise'.",
"The test runs the flow through automation.execute with the stored row rather than the record-change trigger: @objectstack/trigger-record-change is not a dependency of the showcase app. The live REST repro covers the real trigger.",
"Changeset: none, skip-changeset on the create; @objectstack/example-showcase is private."
],
"dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm",
"mcp_calls": "0",
"api_writes": "2 — relay pr_create (fw-20261102T061052Z-c65425, run 41398309972: PR #29018 + label skip-changeset, body read back identical, 10021 bytes) and this os-dev-report comment via post-stamped. git push is not a REST write.",
"open_questions": [],
"out_of_scope_findings": [
"class: c · reach: named producer — this very flow's e2 budget > 200000 on the nullable showcase_project.budget passed os validate on main and faulted at runtime (live base repro above) · the publish-time null-guard gate (packages/lint/src/validate-null-guards.ts) still lists flow / edge condition as excluded in its surface ledger, though its totality blocker is gone since #4953 and the widening card #4811 is closed; so an author or AI can ship an unguarded ordering comparison on a nullable field in a flow condition · dedupe words: null-guard flow edge condition, validate-null-guards flow surface, nullable ordering flow condition, has() null flow",
"Triage's platform-first question: no lint rule warns about a decision with no otherwise edge (flow-decision-* rules are inclusive-overlap, mode-invalid, unconditional-branch). A reading, not filed; carrier: none · noted in the PR's Acceptance notes",
"The same has(FIELD) ? FIELD : null recipient guard is in ten other notify slots of the showcase flows file on other flows (assignee, owner, task.owner, task.watcher); it answers null on a null column and would leave that notify with no recipient. Reach not measured per slot, so not filed; carrier: none · noted in the PR's Acceptance notes",
"showcase_committee_quorum start condition … && total_amount >= 5000 on a sum summary: whether it reads null on a report with no lines is not measured; showcase_budget_approval start condition budget > 100000 … faults on no budget but in the live run logged nothing and did not fire (benign). carrier: none · noted in the PR's Acceptance notes"
]
}
Activity
objectstack-fleet commentedon Nov 2, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: none
Triage: priority:p2 · domain:server · pm:queue · 2026-11-02T05:00Z
⛔ Not a claim.
ProjectEscalationFlowedges e2/e3 and the owner notify in examples/app-showcase/src/automation/flows/index.ts; the body'sPath:line is a file, not a defined path item, and the card was observed besiderecords-forms.validation-rule-type-matrix, not on that capability. Examples route to the subsystem they exercise (automation), so the lane isdomain:server, not devx. The reference flow failing terminally on an ordinary record is a user-visible defect, so it is p2 and goes topm:queue. Under 「平台相关的功能,应该尽量先改平台」 the claimant should first say whether the flow validator can warn about a decision with no otherwise edge, and treat guarding the showcase as the minimum fix.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-02T05:18Z
Session:
session_01FEkQ5oc56xChD6Wrc4cGSt(the seat); the card thread issession_01FEkQ5kZjdnPFmYVCV7PUQUAccount:
os-project-manager(the seat's linked user; the card's assignee)Branch:
claude/project-thread-zxfajt(the card thread's harness-pinned branch; 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 the card's grade and this seat's rulings in the order; card assigned to seat 5 by seat 1's queue split after Leehom's 「队列进了很多任务,抓紧派发」 (2026-11-02T05:15Z).
File surface:
examples/app-showcase/src/automation/and its tests (packages/qa/dogfoodif the test lives there), changesets. ⛔ Nopackages/services/**, ⛔ nopackages/spec/**.Container & model:
S,mode:thread, model and effort as the thread was openedClause-②: no
Metadata-protocol: none
Responsibility: the code is the showcase escalation flow · a red-turned project with no budget (or no owner) fails the run terminally and notifies nobody · reached by every showcase user and every reader who copies the reference flow
Thread-read: 6569465543
Premise: holds (read at
origin/main135054eb:ProjectEscalationFlowedges e2/e3 comparebudgetunguarded; the owner notify has no route around a missing owner)Serial constraints cleared: none needed.
Resume-from: none — fresh from
origin/mainState:
pm:queue→pm:dispatched, assigned toos-project-manager, recorded by this claim. Order:pm-orders/server/order-28951.mdin the project.objectstack-fleet commentedon Nov 2, 2026
domain:server#5 · os-project-manager · session_01FEkQ5kZjdnPFmYVCV7PUQU
os-dev-report
{
"issue": 28951,
"status": "done",
"branch": "claude/project-thread-zxfajt",
"pr": "#29018",
"head": "39cd0102",
"session": "session_01FEkQ5kZjdnPFmYVCV7PUQU",
"premise_still_valid": true,
"summary": "ProjectEscalationFlow now runs to the end on a project with no budget or no owner. e2 is
record.budget != null && record.budget > 200000and e3 is the triage decision's isDefault edge, so no budget routes as the normal path (the flow's wording 'Critical budget?' reads no budget as not critical). notify_normal and converge fall back torole:owner(the exec) when the owner is blank (isBlank), and the critical branch's owner alert sits behind its ownhas_ownerdecision, since the Exec branch beside it already alerts role:owner. The notify node's required-recipient refusal is untouched; no engine, CEL, services or spec file changed.","tests": "New examples/app-showcase/test/project-escalation-optional-fields.test.ts, 7 cases on a real ObjectKernel (ObjectQL over sqlite + automation plugin, the app's real showcase_project and flow; the run is handed the stored row, which each case asserts carries
budget: null/owner: null; messaging is an audience recorder, fetch stubbed). Red on base (flows file at 135054e, 6 cases then): 4 failed / 2 passed:condition failed to evaluate as CEL: no such overload: dyn‹null› > int(no budget; neither),Node 'notify_normal' failed: notify: at least one recipient is required …(no owner),Node 'alert' failed: parallel 'alert': branch failed — Node 'alert_owner' failed …(critical, no owner); both controls green. Head: Tests 7 passed (7). Ablation 1 (ablation-replace, anchor x1, blob 9272a372 -> 9aa0d008, e2 guard dropped): 2 failed / 5 passed; restored, blob == HEAD, git diff HEAD empty. Ablation 2 (blob 9272a372 -> 900018be, has_owner edge -> 'true'): 1 failed / 6 passed; restored the same way. No dist on the path (test imports src). Live REST onpnpm dev -- --fresh, card steps (create as admin, PATCH health red -> 200) for no budget / no owner / control: base logs twoERROR Trigger-fired run of flow 'showcase_project_escalation' failed (trigger 'record_change')lines (no such overload: dyn‹null› > int … budget > 200000and the notify_normal recipient refusal), runs completed/failed/failed; head logs none, runs completed x3 (start, triage, alert skipped, notify_normal, converge). On 39cd010 (merges origin/main 00d0668):pnpm --filter @objectstack/example-showcase testTest Files 43 passed (43), Tests 518 passed (518); typecheck exit 0; validate exit 0 (no row names the flow); eslint --no-inline-config on both files exit 0.","gates": "dispatch-gates --commands --repo objectstack-ai/objectstack: 40 families, each exit captured before any pipe; 39 exit 0.
--ran:✓ dispatch-gates --ran: 40 derived famil(ies) accounted for — 39 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3). NOT MEASURED: check:dual-build-cjs-loads, reason: needs every published package's dist; read 3 (seven unbuilt packages), then 1 after building them, with 82Cannot find module '../chunk-….js'findings in packages this diff does not touch (stale chunks from overlapping local rebuilds). The diff changes no published package; CI's fresh build is the reading. PR check-runs: in_progress at report time.","line_budget": "263 changed lines (+257 / -6) over 2 files, under the 3000 human-merge threshold",
"files_changed": [
"examples/app-showcase/src/automation/flows/index.ts",
"examples/app-showcase/test/project-escalation-optional-fields.test.ts"
],
"deviations": [
"Ruling 1 spelling: the order named
has(record.budget) && record.budget > 200000. Measured with ExpressionEngine on{budget: null}it still faults (no such overload: dyn‹null› > int): has() is true on a null key, and the flow's record is total over declared fields (the card's own log shows dyn‹null›). Usedrecord.budget != null && record.budget > 200000, the spelling skills/objectstack-formula teaches, showcase_project's own validation rules use, and validate-null-guards accepts as dominating. The ruling's intent (guard the optional field, no budget routes normal) is kept; the no-budget edge is the decision's isDefault 'otherwise'.","The test runs the flow through automation.execute with the stored row rather than the record-change trigger: @objectstack/trigger-record-change is not a dependency of the showcase app. The live REST repro covers the real trigger.",
"Changeset: none,
skip-changeseton the create; @objectstack/example-showcase is private."],
"dogfood_battery": "n/a — no
!in the PR title and no (narrowing) arm","mcp_calls": "0",
"api_writes": "2 — relay pr_create (fw-20261102T061052Z-c65425, run 41398309972: PR #29018 + label skip-changeset, body read back identical, 10021 bytes) and this os-dev-report comment via post-stamped. git push is not a REST write.",
"open_questions": [],
"out_of_scope_findings": [
"class: c · reach: named producer — this very flow's e2
budget > 200000on the nullableshowcase_project.budgetpassedos validateon main and faulted at runtime (live base repro above) · the publish-time null-guard gate (packages/lint/src/validate-null-guards.ts) still lists flow / edgeconditionas excluded in its surface ledger, though its totality blocker is gone since #4953 and the widening card #4811 is closed; so an author or AI can ship an unguarded ordering comparison on a nullable field in a flow condition · dedupe words: null-guard flow edge condition, validate-null-guards flow surface, nullable ordering flow condition, has() null flow","Triage's platform-first question: no lint rule warns about a decision with no otherwise edge (flow-decision-* rules are inclusive-overlap, mode-invalid, unconditional-branch). A reading, not filed; carrier: none · noted in the PR's Acceptance notes",
"The same
has(FIELD) ? FIELD : nullrecipient guard is in ten other notify slots of the showcase flows file on other flows (assignee, owner, task.owner, task.watcher); it answers null on a null column and would leave that notify with no recipient. Reach not measured per slot, so not filed; carrier: none · noted in the PR's Acceptance notes","showcase_committee_quorum start condition
… && total_amount >= 5000on a sum summary: whether it reads null on a report with no lines is not measured; showcase_budget_approval start conditionbudget > 100000 …faults on no budget but in the live run logged nothing and did not fire (benign). carrier: none · noted in the PR's Acceptance notes"]
}
Generated by Claude Code
objectstack-fleet commentedon Nov 2, 2026
domain:server#5 · os-project-manager · session_01FEkQ5oc56xChD6Wrc4cGSt
ACCEPT → PR #29018 comment 6570492269 · head
39cd0102· 2026-11-02T06:17Z. No contract review owed. Landing:pr_ready+automerge_enablethrough the relay; the merge queue closes this card. Follow-ups: #29026 (null-guard gate on flow conditions, devx) and #29028 (other showcase recipient guards).objectstack-fleet commentedon Nov 2, 2026
domain:server#5 · os-project-manager · session_01FEkQ5oc56xChD6Wrc4cGSt
Landed · 2026-11-02T06:58Z · PR #29018 merged through the merge queue as
6b3ba82cbb; this card closed as completed by itsFixesline.6b3ba82cbbpm:dispatchedremoved and the seat unassigned in this stroke,domain:*/area:*keptFollow-up already dispatched: #29028 (the other
has()notify slots and two start conditions), whose order waited for this merge.