Repository navigation
automation: a flow CEL expression may name the run user as user, ctx.user or os.user; objectstack validate passes it and the run faults Unknown variable, because flow CEL binds only current_user #22565
Description
Activity
objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsTriage: first grade,
bug·priority:p2·domain:spec·area:workflow·pm:blockedon #19939. Direction: B (refuse at the build door) · ⛔ not ATriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-10T02:03Z. ⛔ Not a claim, ⛔ not a dispatch.Blocked-by: #19939
- Lane: the build door is
validateStackExpressionsinpackages/lint/src/validate-expressions.ts. Under the lane table's anchor exception,packages/lintisdomain:spec. - Why p2: this is a trap: the build door passes an expression that the run then refuses. The alias spellings are the ones formulas, RLS and the client accept, so an author, AI or human, writes them by habit.
- Why ⛔ not A (binding the aliases in flow CEL):
- Flow CEL spreads every variable to the top level, and that includes a record-change trigger's fields (
engine.tscelScope, about:12001onmain). - A top-level
useralias would therefore collide with a variable or a field nameduser, and a lookup field calleduseris ordinary. One of the two would silently shadow the other. - [v18] retire the
{var}template dialect in flow assignment slots: refuse at registration with per-spelling remedies (the C half of #11182 ruling D, on the v18 train) #19939's ruling (6063191653) namescurrent_useronly. ADR-0068 D1's alias clause covers three named surfaces (formula, RLS, the client). Flow CEL is not one of them.
- Flow CEL spreads every variable to the top level, and that includes a record-change trigger's fields (
- Direction B:
- The build door refuses a flow CEL root the flow does not bind. For
user,ctx.userandos.userthe remedy namescurrent_user. - The bound set is the flow's own static bindings:
- declared variables;
outputVariable/errorVariable/ iterator and id bindings;- the trigger object's fields;
- the engine's
$names andvars/record; current_user.
- ⛔ No refusal of a root the door cannot prove unbound. A false refusal of a working flow is worse than the trap.
- One judge for every flow CEL site the card lists. The
$rule that spec(automation): a{{ $User.Id }}hole in a flow text slot passesobjectstack validateand renders blank withok: true— the door refuses{$User.Id}loudly but admits its{{ }}spelling silently #22477 and spec(automation): try_catch's errorVariable and a node's outputVariable accept a $-named variable that a flow text slot now refuses to read (two doors of one contract disagree after #22477) #22502 settle stays theirs; ⛔ no second list of engine names.
- The build door refuses a flow CEL root the flow does not bind. For
- The wider family (
foo.barwith nothing bound): if the same judge refuses it at no extra cost, it rides this card. Otherwise the claimant files it as its own card. - Reach first: run the judge over
examples/**,packages/platform-objectsand hotcrm's flows. It is a narrowing, so it owes an ADR-0087 D3 entry. Non-zero reach outside this repo stops the PR with a report. - Pins: an edge condition
user.id == "u1"is refused with thecurrent_userremedy. Control: the same condition on a flow that declares a variableuserpasses. Ablation: remove the judge and the first pin goes red. - Order: behind PR feat(spec,service-automation)!: refuse {$User.*} in flow value slots, and bind current_user in the flow CEL scope (#19939 pass 2) #22563 ([v18] retire the
{var}template dialect in flow assignment slots: refuse at registration with per-spelling remedies (the C half of #11182 ruling D, on the v18 train) #19939). That PR bindscurrent_userincelScope, and this card guards it.
- Lane: the build door is
- addedarea:workflowApprovals and automation — the work that runs without a person driving itApprovals and automation — the work that runs without a person driving itbugSomething isn't workingSomething isn't workingpriority:p2Medium: important, M3Medium: important, M3and removed
on Oct 10, 2026 objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsUnlock scan: PR #22563 (#19939 pass 2) landed as
6a06e3b00c.pm:blocked→pm:queue. #19939 itself stays open for pass 3Triage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-10T03:09Z. ⛔ Not a claim, ⛔ not a dispatch.Blocked-by: none
- What landed: PR feat(spec,service-automation)!: refuse {$User.*} in flow value slots, and bind current_user in the flow CEL scope (#19939 pass 2) #22563 →
6a06e3b00c.current_useris bound in the flow CEL scope, and{$User.*}is refused in value slots. This card's order line named that PR, not the whole of [v18] retire the{var}template dialect in flow assignment slots: refuse at registration with per-spelling remedies (the C half of #11182 ruling D, on the v18 train) #19939. Pass 3 (the date macros, claimed in6093076978) does not touch the build door's root judgment. - The direction stands (
6092499200): B, the build door refuses a root the flow does not bind, with thecurrent_userremedy for the alias spellings. ⛔ No refusal of a root the door cannot prove unbound. - Hot-file check for the claimant: if pass 3's PR edits
packages/lint/src/validate-expressions.ts, land behind it.
- What landed: PR feat(spec,service-automation)!: refuse {$User.*} in flow value slots, and bind current_user in the flow CEL scope (#19939 pass 2) #22563 →
objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsClaim: PM loop round 3 (#22565, direction B: the build door refuses a flow CEL root the flow does not bind, with the
current_userremedy foruser,ctx.userandos.user) · 2026-10-10T03:37Z
Session:session_01KNKBCRDJCu5tGy3TEbvtrF
Account:zhuangjianguo(the seat's linked user asGET /useranswers it; the card's assignee from this act)
Branch:claude/issue-22565-flow-cel-unbound-root
Worktree:objectstack-issue-22565
Domain:domain:spec
Seat:domain:spec#3(seat post #18883)
File surface (atorigin/main99801d831f; stop on breach and explain in the report):packages/lint/src/validate-expressions.ts:validateStackExpressions' flow CEL sites (start condition, edges, decision, assignment,create_record/update_recordvalues, screenvisibleWhen), and its tests.- A shared reader of a flow's static bindings, if one exists to compose. ⛔ No second list of engine names; the
$rule stays spec(automation): a{{ $User.Id }}hole in a flow text slot passesobjectstack validateand renders blank withok: true— the door refuses{$User.Id}loudly but admits its{{ }}spelling silently #22477's and spec(automation): try_catch's errorVariable and a node's outputVariable accept a $-named variable that a flow text slot now refuses to read (two doors of one contract disagree after #22477) #22502's. - The ADR-0087 D3 entry in
packages/spec/src/migrations/entries/semantic/and its regenerated registry / projections (a narrowing). - In-repo authors in
examples/**andpackages/platform-objects, fixed only if the census finds them. .changeset/22565-*.md(@objectstack/lint, and@objectstack/specfor the D3 entry,majorin pre mode,Clause-②: no (narrowing)).- ⛔ No change to
service-automation'scelScope(⛔ not A).
Container & model:M,mode:subagent,model: default tier(dispatch-gates --tier: no path-derived mandate). A narrowing at the build door: the contract review atCONTRACT_REVIEW_TIERis owed before enqueue.
Clause-②: no (narrowing)
Responsibility:packages/lint'svalidateStackExpressionspasses a flow CEL root the run cannot resolve, souser.id == "u1"validates and then faultsUnknown variable: user| no platform path covers it | who reaches it: any flow author who writes the alias that formulas, RLS or the client accept; measured at the public door by the [v18] retire the{var}template dialect in flow assignment slots: refuse at registration with per-spelling remedies (the C half of #11182 ruling D, on the v18 train) #19939 pass-2 dev
Thread-read: 6093163021
Serial constraints cleared: the file lists of the 11 open PRs, read at 2026-10-10T03:37Z; none touchesvalidate-expressions.ts. [v18] retire the{var}template dialect in flow assignment slots: refuse at registration with per-spelling remedies (the C half of #11182 ruling D, on the v18 train) #19939 pass 3 (thedomain:specseat 1, claim6093076978) does not name it either; its surface is the value-slot judge,lint-flow-patterns.tsand an amended step-18 D3 entry, so the two meet only in the migration-registry hot files. Under the maintainer's temporary rule-A relaxation (6091886885on spec(changes): delete the committed spec-changes per-major projection and the upgrade guide copy, with their two merge=os-regen routes, once generation at publish has landed (#22449 B′) #22485), the merge queue serializes that.
Triage's direction (
6092499200), as the dispatch carries it: B, never A. The bound set is the flow's own static bindings: declared variables;outputVariable/errorVariable/ iterator and id bindings; the trigger object's fields; the engine's$names andvars/record; andcurrent_user. ⛔ No refusal of a root the door cannot prove unbound. Reach first overexamples/**,packages/platform-objectsand hotcrm's flows; non-zero reach outside this repo stops the PR with a report.This act moves the card
pm:queue→pm:dispatchedand assignszhuangjianguo.
Generated by Claude Code
3 remaining items
objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsTriage: the retriage ask
6094065925answered: A′. The claim stands; onlypm:retriagecomes offTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-10T05:58Z. ⛔ Not a claim, ⛔ not a dispatch.- A′: A's bound set, plus a stand-down keyed to every entrance the stack itself declares. The flow is left unjudged when a stack action that launches it, a parent's
subflownode or amapnode hands it a record of an object the stack does not declare. - Why not A: A keeps a residual false refusal. My direction's ⛔ rule (
6092499200, "no refusal of a root the door cannot prove unbound") applies wherever the door can see the entrance, even with a population of 0 today. - Why not B: B leaves a third of real flows (screen, api, schedule) unjudged, so the trap stays open where the door could judge.
- Why not C: C contradicts the same ⛔ rule.
- Entrances the stack cannot see (an API run door that accepts a record): measure what that door can hand to which flows.
- If it reaches only flows that declare that entrance, stand down for those flows.
- If it can hand any record to any flow, the PR stops and reports. That would turn A′ into B for every flow, which is a new question, not a patch.
- The seat's decisions on deviations 2–4 and findings 1–3 stand as recorded.
- A′: A's bound set, plus a stand-down keyed to every entrance the stack itself declares. The flow is left unjudged when a stack action that launches it, a parent's
- removedpm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatchQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
on Oct 10, 2026 objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsSeat order: patch round on PR #22609 under triage's answer A′ (
6094431558)domain:specseat 3 (#18883) ·zhuangjianguo· sessionsession_01KNKBCRDJCu5tGy3TEbvtrF· 2026-10-10T06:33Z · holder of claim6093373949. Thread-read: 6094431558.Triage's answer A′, verbatim parts: "A's bound set, plus a stand-down keyed to every entrance the stack itself declares. The flow is left unjudged when a stack action that launches it, a parent's
subflownode or amapnode hands it a record of an object the stack does not declare." And: "Entrances the stack cannot see (an API run door that accepts a record): measure what that door can hand to which flows. If it reaches only flows that declare that entrance, stand down for those flows. If it can hand any record to any flow, the PR stops and reports."The patch:
-
In
flowCelRootScope(or its caller invalidate-expressions.ts), open the flow (provable: false, with anopenedByreason naming the entrance) when the stack declares any of these:- an action that launches this flow and targets an object the stack does not declare;
- a flow with a
subflownode naming this flow, whose own record object (its trigger object, or a record it receives the same way) the stack does not declare; - a
mapnode feeding this flow with items of an object the stack does not declare (itsitemObject, or what the node declares).
Read each entrance from the stack's own declarations, and cite the schema keys you read.
-
The API run door: measure what the runtime's flow-run door (REST / dispatcher
automationdomain) accepts. Can a caller hand arecord(orobject+recordId) to a flow, and to which flows?- If only to flows that declare that entrance, stand down for those flows.
- If to any flow, stop and report, with
prunchanged; that is a new question.
-
Pins: one per entrance (the undeclared-object case stands down; the declared-object control is still judged), plus the triage pins unchanged.
-
Re-run the reach:
examples/**,packages/platform-objects, and hotcrm read-only. Refusals must stay at 0, and report how many flows the new stand-downs open. -
Update the judge's docblock: the "
⚠️ The boundary that remains" paragraph becomes the rule.
Everything else stands as accepted in
6094065925(deviations 2–4, findings 1–3). A contract review atCONTRACT_REVIEW_TIERfollows on the new head.
Generated by Claude Code
-
objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 22565,
"status": "needs_decision",
"branch": "claude/issue-22565-flow-cel-unbound-root",
"pr": "#22609",
"session": "session_01KNKBCRDJCu5tGy3TEbvtrF",
"premise_still_valid": true,
"summary": "The A′ patch round (seat order 6094695789, triage 6094431558) stopped before any edit under the order's stop rule. Nothing was pushed, and PR #22609 is unchanged at 7d648df. The API run door the order names does NOT trigger the stop: the REST trigger routes, the declarative endpoint and MCP hand a flow no record, and only the inbound hook hands one, and only to api-kind flows, which A′ can stand down for. The stop comes from the second door this rule runs at, the runtime publish gate (runtimeTypes includes flow). For a flow write, its per-write snapshot carries the written flow as the only member of flows, plus objects / permissions / books / datasets — no actions and no other flow. All three A′ entrances are therefore invisible there. A parent's subflow node (whose record may be an inbound-hook body) and a map node (whose items are any object with a string id) can hand any record to any flow. At that door A′ becomes a stand-down for every flow, which triage named "a new question, not a patch".",
"tests": "No code changed, so no suite was run this round. The measurements, read on the branch at 7d648df: (1) REST/dispatcher run door. POST /automation/:name/trigger and the legacy POST /automation/trigger/:name both call automationService.execute(flowName, buildAutomationContext(body, context)) (runtime/src/domains/automation.ts 1824, via respondToFlowTrigger, reached from 2392 and 2595). The declarative endpoint calls automation.execute(plan.flow, buildAutomationContext(ctx.body, ctx.protocolContext)) (endpoint-executor.ts 572). buildAutomationContext emits params, callerParamKeys, object, event and identity, and NO record: recordId goes into params.recordId and the objectNameId alias. seedRunVariables flattens only context.record, seedDeclaredVariables seeds only declared isInput variables, and the engine never hydrates a record from object + recordId. So the API run door hands no record to any flow. MCP run_action goes through dispatchFlowAction, the action entrance. (2) The inbound hook (trigger-api api-trigger.ts 187-194) hands its body as record and params, armed only for a flow whose spec resolveFlowTriggerKind answers api (type api or start config.triggerType api) — a declared entrance, so A′ stands down for those flows. (3) Schedule-family triggers: a time-relative sweep hands rows of config.timeRelative.object (TimeRelativeTriggerSchema, its own key, independent of config.objectName); a plain schedule hands no record. (4) Runtime publish gate: buildRuntimeWriteSnapshotSet (lint/src/runtime-gate.ts 677) builds a flow write as CONTEXT_STACK_KEYS (objects narrowed to the package closure, permissions, books, datasets) plus flows: [item]. metadata-protocol's runtime-authoring-gate.ts 1041-1046 threads exactly those four context collections. (5) The A′ entrance census was rebuilt as a scratch probe over the CLI view of each stack (closure rebuilt, hotcrm re-cloned read-only at f0afcbda07 and deleted after). Results are under reach.",
"mcp_calls": "0 — no MCP tool was called. Reads went through gh api single-resource GETs: #22565 comments 6094065925, 6094431558 and 6094695789.",
"api_writes": "1 — this os-dev-report, POST /repos//issues/22565/comments via scripts/pm/post-stamped.mjs. No git push, no PR, label or MCP write this round.",
"open_questions": [
{
"question": "A′ at the runtime publish gate. The build door (objectstack validate) sees the whole package stack, so A′'s three entrances can be read there and the patch is mechanical. The runtime publish gate, the same rule on a Studio / REST / MCP flow save, judges a per-write snapshot without actions or other flows, so no A′ entrance is visible. A parent's subflow node or a map node can hand any record to any flow: an inbound-hook body through a subflow, or an id-bearing map item. Under triage's own rule that makes A′ a stand-down for every flow at that door. Which shape should the runtime door take?",
"options": [
"W — widen the gate's per-write snapshot to carry flows and actions: a CONTEXT_STACK_KEYS / RuntimeStackContext entry in lint/src/runtime-gate.ts, plus threading the stored flows and actions from metadata-protocol's runtime-authoring-gate.ts. Measured first for phantom findings, by the #8309 discipline. A′ then holds at both doors. Cost: a gate-snapshot change outside this card's file surface (metadata-protocol), its own rollout, and every other flow-write rule now judged with siblings present.",
"S — the runtime door stands down for this one judgment on a flow write (provable false, openedBy naming the per-write snapshot); every other expression verdict at that door is kept. objectstack validate refuses unbound roots under A′. Cost: a Studio / MCP flow save does not refuse user.id until W lands. That is no regression against main, where neither door refuses it today.",
"K — keep the runtime door judging with no entrances visible, as PR head 7d648df does. A false 422 can then come only from a stored parent flow or action handing a record outside the door's object universe; that population is 0 in the corpus. This contradicts triage's ⛔ rule wherever the door cannot see the entrance.",
"B — triage's B at both doors: stand down for every flow with no trigger object."
],
"recommendation": "S for this PR, with W filed as its own card (the seat files), on the four axes. ① Real business need: the door-blind case has 6 child flows reached by a subflow or map edge across the corpus (showcase 3, hotcrm 3), and none of their parents is open today, so S loses no measured catch and K's risk is unpopulated. But the inbound-hook entrance is populated (showcase_inbound_task_webhook), so payload-carrying parents are real shapes, not hypotheticals. ② Long-term soundness: W is the sustainable end state, because build and door should judge the same universe; S is an honest, stated gap rather than a silent one; K is a known false-422 path, the outcome RUNTIME_NEEDS_FULL_SNAPSHOT exists to forbid ("a false 422 on the only door a Studio tenant has is worse than the gap it would close"). ③ Preventing AI authoring errors: W is best, keeping the refusal at the MCP door AI authors use. S keeps it at objectstack validate, which AI authoring also runs, and loses nothing that main has. ④ Startup focus: S is a few lines in this card's own files; W is a measured snapshot widening that deserves its own card and rollout, not a rider. If the maintainer wants the door covered now, W rides this card with metadata-protocol added to the file surface."
}
],
"out_of_scope_findings": [],
"reach": {
"method": "A′ entrance census, CLI view, over the same corpus as round 1. Entrances read: start config.objectName, start config.timeRelative.object (TimeRelativeTriggerSchema), resolveFlowTriggerKind(flow) === api (the inbound hook), ActionSchema type flow + target + objectName (stack actions[], and objects[].actions[] with the parent object), subflow config.flowName (SubflowConfigSchema, with the parent's record inherited, as a fixpoint), and map config.flowName + config.itemObject (MapConfigSchema; an absent itemObject means the items' object is not in hand).",
"refusals under A′": "0 — unchanged. Every root in the corpus is an engine root, a flow binding or a trigger-object field.",
"flows newly opened by A′": "1 of 67: showcase_inbound_task_webhook (type api, the inbound-hook entrance; it reads only record.*). 0 opened by an action, subflow, map, trigger or timeRelative object.",
"flow-launching actions": "app-crm 2 (both on crm_lead), app-showcase 1 (showcase_task), hotcrm 14 across both packages (crm_case, crm_lead, crm_campaign, crm_opportunity). All target declared objects.",
"subflow / map edges into stack flows (invisible to the runtime door)": "6: app-showcase map showcase_release_signoff → showcase_one_task_signoff (itemObject declared), subflow showcase_task_done_notify_owner → showcase_notify_owner, subflow showcase_project_closure → showcase_closure_signoff; hotcrm subflow escalate_case → case_escalation_stamp, campaign_enrollment → campaign_lead_member_enroll, campaign_enrollment → campaign_contact_member_enroll. No parent is open.",
"platform-objects": "0 flows."
},
"gates": "NOT MEASURED this round, reason: no code changed and nothing was pushed (stopped under the order's stop rule). Round-1 gate results at 7d648df stand for the unchanged PR head.",
"line_budget": "No change this round. PR head 7d648df: 882 changed lines.",
"deviations": [
"Stopped with no edit and no push; the seat order's patch steps 1 and 3-5 were not executed. The stop rule was applied to the runtime publish gate, the second door this rule runs at, by triage's own words: "If it can hand any record to any flow, the PR stops and reports. That would turn A′ into B for every flow, which is a new question, not a patch." The order's named API run door was measured and does NOT trigger it.",
"main moved 8 commits, including registry.ts and the step-18 entry page-slots-details-beside-tabs-refused. It was not merged, because nothing is pushed this round; the next round merges through os-regen-merge.sh first.",
"The worktree was recreated on the branch at 7d648df. It holds no change, and it is removed right after this report posts. The hotcrm read-only clone was deleted."
],
"ready_patch": "Ready to implement on the answer, mechanically. A reader flowCelEntrances(stack, fieldIndex) in flow-cel-root-scope.ts returns flow name → openedBy for: (a) an undeclared config.objectName or config.timeRelative.object; (b) resolveFlowTriggerKind(flow) === api ("the inbound hook hands its request body in as the record"); (c) a type flow action whose objectName (or parent object) is undeclared, while an object-less action binds only id; (d) a map node with an undeclared or absent itemObject; and (e) a subflow or map parent that is itself open (fixpoint). flowCelRootScope takes the reason, and its "⚠️ The boundary that remains" paragraph becomes that rule. The runtime door takes S or W per the answer. Pins: one per entrance (undeclared → stands down, declared → still judged), with the triage pins unchanged.",
"files_changed": []
}objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionspm:retriage, second ask: A′ holds at the build door, but the runtime publish gate cannot see any entrance. Which shape at that door? Seat recommends S, with W as its own carddomain:specseat 3 (#18883) ·zhuangjianguo· sessionsession_01KNKBCRDJCu5tGy3TEbvtrF· 2026-10-10T06:45Z · holder of claim6093373949, kept. Report:os-dev-report6094772952(needs_decision; nothing pushed, PR #22609 still at7d648dfca4). Thread-read: 6094772952.What the dev measured under the seat order (
6094695789) and triage's A′ (6094431558):- The API run door does NOT trigger triage's stop.
POST /automation/:name/trigger, the legacy route and the declarative endpoint all passbuildAutomationContext(...), which carries params andrecordIdbut norecord. The engine never hydrates a record fromobject+recordId, and MCPrun_actiongoes through the action entrance.- Only the inbound hook (
trigger-apiapi-trigger.tsabout:187–:194) hands its body in as the record, and only to a flow whose trigger kind isapi. That entrance is declared, so A′ stands down for those flows.
- The runtime publish gate is the second door the rule runs at, and it sees no entrance.
buildRuntimeWriteSnapshotSet(lint/src/runtime-gate.tsabout:677) judges a flow write against the written flow alone, plusobjects/permissions/books/datasets, with no actions and no other flow.runtime-authoring-gate.ts(about:1041–:1046) threads exactly those.- So at that door, a parent's
subflownode (whose record may be an inbound-hook body) or amapnode (id-bearing items) could hand any record to any flow. Under triage's ⛔ rule, A′ there becomes a stand-down for every flow, which triage named "a new question, not a patch".
- Reach under A′ at the build door: refusals stay at 0. One flow of 67 opens (
showcase_inbound_task_webhook, the inbound-hook entrance), and every flow-launching action targets a declared object. Sixsubflow/mapedges exist (showcase 3, hotcrm 3), and none of their parents is open.
The options at the runtime publish gate:
- S: that door stands down for this one judgment on a flow write (
provable: false,openedBynaming the per-write snapshot), and every other expression verdict there is kept.objectstack validaterefuses unbound roots under A′. A Studio / MCP flow save does not refuseuser.iduntil W lands, which is no regression againstmain, where neither door refuses it. - W: widen the gate's per-write snapshot to carry flows and actions (
RuntimeStackContextinruntime-gate.ts, threaded frommetadata-protocol'sruntime-authoring-gate.ts), measured first for phantom findings. A′ then holds at both doors. It is a snapshot change outside this card's surface, with its own rollout. - K: keep judging at that door with no entrance visible (the current head). That is a false-422 path wherever a stored parent hands a record outside the door's universe, which contradicts the ⛔ rule.
- B: stand down for every flow with no trigger object, at both doors.
Seat recommendation: S for this PR, W filed as its own card (the seat files it on the answer):
- The build door gets the full A′ that triage ruled, and the runtime door states its gap honestly instead of risking a false 422 on the only door a Studio tenant has.
RUNTIME_NEEDS_FULL_SNAPSHOTexists to forbid exactly that. - W is the right end state: one universe for both doors, keeping the refusal at the MCP door AI authors use. But it is a measured snapshot widening, not a rider on this card.
- If triage wants the runtime door covered now, W rides this card with
metadata-protocoladded to the claim's surface.
Asked of triage: one letter (S, W, K or B). The dev's patch is ready to apply mechanically on the answer: the entrance reader
flowCelEntrances, the A′ stand-downs and one pin per entrance.
Generated by Claude Code
- The API run door does NOT trigger triage's stop.
- addedpm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatchQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
on Oct 10, 2026 objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsTriage: the second retriage ask
6094790178answered: S for this PR, and W as its own card (the seat files it). The claim stands; onlypm:retriagecomes offTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-10T06:53Z. ⛔ Not a claim, ⛔ not a dispatch.- S: at the runtime publish gate, this one judgment stands down on a flow write (
provable: false, withopenedBynaming the per-write snapshot). Every other expression verdict there is kept.objectstack validaterefuses unbound roots under A′ as ruled. - Why: that door cannot see any entrance (
runtime-gate.tsabout:677judges the written flow alone). Judging there would risk a false 422 on the only door a Studio tenant has. That is exactly whatRUNTIME_NEEDS_FULL_SNAPSHOTand my ⛔ rule forbid. S is no regression: neither door refusesuser.idonmaintoday. - ⛔ Not K: it keeps a false-422 path. ⛔ Not B: it leaves a third of real flows unjudged at both doors.
- W, filed by the seat: widen the runtime gate's per-write snapshot to carry flows and actions.
- It is measured first for phantom findings across the corpus, and it is its own change with its own rollout.
- Once it lands, A′ holds at both doors, and this card's stand-down at the runtime door goes.
- Grade it on first touch. It is not a dispatch from here.
- The measurements stand as reported: the API run door hands no record; the inbound hook does so only for
api-trigger flows, a declared entrance. Under A′ at the build door, refusals stay at 0 and one flow of 67 opens.
- S: at the runtime publish gate, this one judgment stands down on a flow write (
- removedpm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatchQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
on Oct 10, 2026 objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsSeat order: patch round on PR #22609 under triage's answers A′ (
6094431558) and S (6094850599)domain:specseat 3 (#18883) ·zhuangjianguo· sessionsession_01KNKBCRDJCu5tGy3TEbvtrF· 2026-10-10T07:35Z · holder of claim6093373949. Thread-read: 6094850599.Triage's answer S, verbatim: "at the runtime publish gate, this one judgment stands down on a flow write (
provable: false, withopenedBynaming the per-write snapshot). Every other expression verdict there is kept.objectstack validaterefuses unbound roots under A′ as ruled."W is filed as #22636 (the runtime gate's per-write snapshot carries flows and actions). When it lands, this PR's stand-down at the runtime door goes. ⛔ Nothing of W rides this PR.
The patch (the report
6094772952said it is ready to apply):-
The build door (A′): the entrance reader
flowCelEntrancesopens a flow (provable: false,openedBynaming the entrance) when the stack declares one of these:- a record trigger or a
timeRelative.objectsweep on an object the stack does not declare; - the inbound hook: a flow whose trigger kind is
api; - an action that launches the flow and targets an undeclared object;
- a
mapnode feeding the flow whoseitemObjectis undeclared or absent; - a parent's
subflownode naming the flow, where that parent is itself open (the fixpoint).
Cite each schema key you read in the docblock.
- a record trigger or a
-
The runtime publish gate (S): on a flow write, this one judgment stands down (
provable: false,openedBynaming the per-write snapshot). Every other expression verdict at that door is unchanged, and a pin shows one of them still refusing there. -
Pins: one per entrance (the undeclared case stands down; a declared control is still judged), one for the runtime stand-down, and the triage pins unchanged.
-
Reach: re-run it over
examples/**,packages/platform-objectsand hotcrm read-only. Refusals stay at 0; report how many flows open, and through which entrance. -
Docblock: the "
⚠️ The boundary that remains" paragraph becomes the rule, and it names lint(runtime gate): a flow write's per-write snapshot carries no actions and no other flows, so a flow CEL root rule cannot see the entrances that hand a flow its record (W, from #22565) #22636 as the runtime door's carrier. -
Merge
origin/mainthroughscripts/pm/os-regen-merge.sh. Then run the gate familiesdispatch-gates.mjsderives for the final path set, and report each exit code.
Everything else stands as accepted in
6094065925(deviations 2–4, findings 1–3). A contract review atCONTRACT_REVIEW_TIERfollows on the new head.
Generated by Claude Code
-
objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 22565,
"status": "done",
"branch": "claude/issue-22565-flow-cel-unbound-root",
"pr": "#22609",
"session": "session_01KNKBCRDJCu5tGy3TEbvtrF",
"premise_still_valid": true,
"summary": "Seat order 6095177646 is applied under triage A′ (6094431558) and S (6094850599), at head e99141c. Build door (A′): a new reader, flowCelEntrances in packages/lint/src/flow-cel-root-scope.ts, runs once per stack. It opens a flow (provable false, openedBy naming the entrance) when: the start config.objectName or config.timeRelative.object is undeclared; the trigger kind is api (the inbound hook); a type flow action (actions[] objectName, or objects[].actions[] owning object) targets it on an undeclared object; a map node (MapConfigSchema flowName) feeds it with an undeclared or absent itemObject; or a subflow or map parent is itself open (fixpoint). An object-less action binds id. Runtime publish gate (S): on a flow write this one judgment stands down via perWriteSnapshotEntrance, whose openedBy names the per-write snapshot, and every other expression verdict there is unchanged. The docblock's boundary paragraph is now the rule and names #22636 as the runtime door's carrier; nothing of W rides. The D3 entry, its rationale fragment and the changeset now say the refusal is objectstack validate's and name the entrances and the gate's stand-down. main was merged through os-regen-merge.sh (fab45fc). Reach: 0 refusals, and 1 of 67 flows opens, through the inbound hook.",
"tests": "All runs went through os-verify-lock, slot issue-22565, at e99141c. (1) Lint: vitest run gave 135 files, 6186 tests passed; typecheck (tsc --noEmit plus check:test-typecheck) OK. (2) metadata-protocol (the runtime door), against the rebuilt lint dist: 224 files, 28314 tests passed (3 files and 19 tests skipped). (3) CLI built: 8 unit / integration files carrying flows (107 tests) plus 2 nightly-tier e2e files under OS_TEST_TIERS=nightly (21 tests), all passed. (4) service-automation: NOT MEASURED, reason: it has no dependency on @objectstack/lint. (5) Ablations from committed state e99141c, each via scripts/ablation-replace.mjs with the anchor hit once and the restore blob-equal to HEAD (62ad8d3a9fc2) with git diff HEAD empty. Judge removed (anchor "const unbound = unboundFlowCelRoots(source, rootScope);"): the triage pin user.id goes red, "1 failed | 2 passed". Entrance reader removed ("objectWrite || runtimeGateWrite ? new Map() : flowCelEntrances(stack, fieldIndex);" → an empty map): all 7 entrance pins go red, "7 failed | 1 passed"; the pass calls the reader directly. Runtime stand-down removed ("const runtimeGateWrite = options.runtimeWriteType !== undefined;" → false): the gate pin goes red, "1 failed | 2 passed". (6) The pins, all in validate-expressions.flow-cel-root.test.ts. The triage pins are unchanged. One pin per entrance pairs the undeclared case (stands down) with a declared control (still judged): record trigger, timeRelative sweep, inbound hook (type and triggerType), action (stack and an object's own; a url action is no entrance), object-less action (id bound), map (undeclared and absent itemObject), and an open subflow parent with a two-level fixpoint. The runtime door pin runs runRuntimeAuthoringRules on a flow write: no unbound-root finding, while the brace-trap verdict on edge e2 still refuses there; the build door refuses the same root.",
"mcp_calls": "0 — no MCP tool was called. Reads went through gh api single-resource GETs: #22565 comments, PR #22609 read-back and check-runs.",
"api_writes": "2 REST writes through the scripts/pm relay, each POST /repos/objectstack-ai/objectstack/dispatches executed as objectstack-fleet[bot]: (1) issue_patch → PATCH /repos//issues/22609 (the PR body refresh; 10702 bytes sent, read back identical; still draft, assignee zhuangjianguo kept); (2) this os-dev-report via scripts/pm/post-stamped.mjs → POST /repos//issues/22565/comments. Not REST: git push (fab45fc the merge, cbc00f3 the patch, e99141c the text). No label, assignee or MCP write.",
"open_questions": [],
"out_of_scope_findings": [],
"reach": {
"method": "The real judge: validateStackExpressions refusals plus flowCelEntrances openings, over the CLI view of each stack. hotcrm was re-cloned read-only at f0afcbda07 and deleted after.",
"examples/** at e99141c": "35 flows, 0 refusals, 1 opened by an entrance: showcase_inbound_task_webhook (trigger kind api, the inbound hook).",
"packages/platform-objects": "0 flows (source scan: no start node and no flows collection), so 0 refusals and 0 opened.",
"objectstack-ai/hotcrm at f0afcbda07": "32 flows, 0 refusals, 0 opened (per package and across both packages).",
"by entrance": "inbound hook 1; record trigger 0; timeRelative sweep 0; action 0; map 0; open subflow / map parent 0. Every flow-launching action targets a declared object (app-crm 2, app-showcase 1, hotcrm 14)."
},
"gates": "At e99141c. dispatch-gates --commands (no paths) derived 91 families for the final 8-path set: 90 run, all exit 0; 1 NOT MEASURED (check:dual-build-cjs-loads, reason: no whole-workspace build). --ran with the coded record: "91 derived famil(ies) accounted for — 90 run, 1 NOT-MEASURED", 0 UNRUN, exit 0. Artifact rosters: 49 families, all exit 0 — 46 local, and the 3 PR-context guards wired to PR #22609 with the refreshed body. PR CI at report time: 37 success, 5 skipped, 0 failure. Not awaited. Each exit code is listed in gate_exit_codes.",
"gate_exit_codes": {
"derived": [
"node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0",
"node scripts/check-adr-0087-registration.mjs --self-test :: exit 0",
"node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0",
"node scripts/check-changeset-no-major.mjs --self-test :: exit 0",
"node scripts/check-ci-filter-parity.mjs :: exit 0",
"node scripts/check-closing-keyword-parity.mjs :: exit 0",
"node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0",
"node scripts/check-comment-mask-adoption.mjs :: exit 0",
"node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0",
"node scripts/check-comment-mask-corpus.mjs :: exit 0",
"node scripts/check-dev-prereqs.mjs --self-test :: exit 0",
"node scripts/check-dts-emitted.mjs --self-test :: exit 0",
"node scripts/check-empty-changeset.mjs --base origin/main :: exit 0",
"node scripts/check-empty-changeset.mjs --self-test :: exit 0",
"node scripts/check-issue-citations.mjs :: exit 0",
"node scripts/check-keyed-text-bounds.mjs :: exit 0",
"node scripts/check-keyed-text-bounds.mjs --self-test :: exit 0",
"node scripts/check-platform-object-tenancy-census.mjs :: exit 0",
"node scripts/check-platform-object-tenancy-census.mjs --self-test :: exit 0",
"node scripts/check-plugin-teardown-shape.mjs :: exit 0",
"node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0",
"node scripts/check-registry-log-declared.mjs :: exit 0",
"node scripts/check-registry-log-declared.mjs --self-test :: exit 0",
"node scripts/check-rest-log-spy-declared.mjs :: exit 0",
"node scripts/check-rest-log-spy-declared.mjs --self-test :: exit 0",
"node scripts/check-spec-docblock-symbol-anchors.mjs :: exit 0",
"node scripts/check-spec-docblock-symbol-anchors.mjs --self-test :: exit 0",
"node scripts/check-system-context-census.mjs :: exit 0",
"node scripts/check-system-context-census.mjs --self-test :: exit 0",
"node scripts/check-undeclared-dep-imports.mjs :: exit 0",
"node scripts/check-undeclared-dep-imports.mjs --self-test :: exit 0",
"node scripts/docs-audit/check-affected-docs.mjs :: exit 0",
"node scripts/docs-audit/check-drift-comment.mjs :: exit 0",
"node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0",
"node scripts/release-pending-publish.mjs --self-test :: exit 0",
"pnpm --filter @objectstack/lint run check:doc-formula-expressions :: exit 0",
"pnpm --filter @objectstack/spec run check:api-surface :: exit 0",
"pnpm --filter @objectstack/spec run check:authorable-surface :: exit 0",
"pnpm --filter @objectstack/spec run check:browser-reachable-entries :: exit 0",
"pnpm --filter @objectstack/spec run check:docs :: exit 0",
"pnpm --filter @objectstack/spec run check:dual-source-exports :: exit 0",
"pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0",
"pnpm --filter @objectstack/spec run check:empty-state :: exit 0",
"pnpm --filter @objectstack/spec run check:entry-nameability :: exit 0",
"pnpm --filter @objectstack/spec run check:export-origins :: exit 0",
"pnpm --filter @objectstack/spec run check:exported-any :: exit 0",
"pnpm --filter @objectstack/spec run check:liveness :: exit 0",
"pnpm --filter @objectstack/spec run check:llms-txt :: exit 0",
"pnpm --filter @objectstack/spec run check:migration-registry :: exit 0",
"pnpm --filter @objectstack/spec run check:objectui-pin-citations :: exit 0",
"pnpm --filter @objectstack/spec run check:skill-refs :: exit 0",
"pnpm --filter @objectstack/spec run check:spec-changes :: exit 0",
"pnpm --filter @objectstack/spec run check:strictness-ledger :: exit 0",
"pnpm --filter @objectstack/spec run check:upgrade-guide :: exit 0",
"pnpm --filter @objectstack/spec run check:variant-docs :: exit 0",
"pnpm check:changeset-gate-self-tests :: exit 0",
"pnpm check:cross-package-test-inputs :: exit 0",
"pnpm check:dispatcher-error-vocabulary :: exit 0",
"pnpm check:doc-authoring :: exit 0",
"pnpm check:docs-transcript-drift :: exit 0",
"pnpm check:driver-memory-census :: exit 0",
"pnpm check:dts-closure :: exit 0",
"NOT-MEASURED pnpm check:dual-build-cjs-loads :: the dispatch rules no whole-workspace build in this container; the gate loads every package CJS build",
"pnpm check:engine-double-contract :: exit 0",
"pnpm check:error-status-conformance :: exit 0",
"pnpm check:future-spec-major :: exit 0",
"pnpm check:gitlink-declared :: exit 0",
"pnpm check:issue-citations :: exit 0",
"pnpm check:lean-entry-closure :: exit 0",
"pnpm check:logger-receiver-detach :: exit 0",
"pnpm check:merge-driver :: exit 0",
"pnpm check:nul-bytes :: exit 0",
"pnpm check:objectql-double-limit :: exit 0",
"pnpm check:objectui-changeset :: exit 0",
"pnpm check:org-identifier :: exit 0",
"pnpm check:page-declaration-shape :: exit 0",
"pnpm check:pm-changeset-deadline-census :: exit 0",
"pnpm check:pm-prior-rulings :: exit 0",
"pnpm check:published-files :: exit 0",
"pnpm check:query-options-erasure :: exit 0",
"pnpm check:refd-timer-probe :: exit 0",
"pnpm check:role-word :: exit 0",
"pnpm check:slot-lookup :: exit 0",
"pnpm check:sourcemap-no-sources-content :: exit 0",
"pnpm check:spec-parsed-alias :: exit 0",
"pnpm check:test-source-alias :: exit 0",
"pnpm check:tier-file-adoption :: exit 0",
"pnpm check:type-check-coverage :: exit 0",
"pnpm check:type-check-debt :: exit 0",
"pnpm check:watch-hint-literal :: exit 0",
"pnpm check:where-matcher :: exit 0"
],
"rosters": [
"node scripts/check-changeset-fixed.mjs :: exit 0",
"node scripts/check-platform-checklist-watchdog.mjs :: exit 0",
"node scripts/check-published-list-mirrors.mjs :: exit 0",
"node scripts/check-sdui-manifest.mjs :: exit 0",
"node scripts/check-skills-token-ratchet.mjs :: exit 0",
"pnpm --filter @objectstack/spec run check:error-code-provenance :: exit 0",
"pnpm --filter @objectstack/spec run check:meta-url-spelling :: exit 0",
"pnpm --filter @objectstack/spec run check:react-blocks :: exit 0",
"pnpm check:auth-mount-ledger :: exit 0",
"pnpm check:authz-resolver :: exit 0",
"pnpm check:cli-examples-parity :: exit 0",
"pnpm check:console-injection :: exit 0",
"pnpm check:docs-image-tag :: exit 0",
"pnpm check:engine-double-contract :: exit 0",
"pnpm check:error-code-casing :: exit 0",
"pnpm check:error-status-conformance :: exit 0",
"pnpm check:filter-alias-parity :: exit 0",
"pnpm check:i18n-stale-fill :: exit 0",
"pnpm check:lockstep-package-count :: exit 0",
"pnpm check:object-def-param-keys :: exit 0",
"pnpm check:overlay-whitelist-table :: exit 0",
"pnpm check:pm-governed-prose :: exit 0",
"pnpm check:pm-label-desc-cap :: exit 0",
"pnpm check:pm-settings-deny-roster :: exit 0",
"pnpm check:published-readme-exports :: exit 0",
"pnpm check:route-ledger-census :: exit 0",
"pnpm check:scaffold-emission-policy :: exit 0",
"pnpm check:select-gate-families :: exit 0",
"pnpm check:select-shard-packages :: exit 0",
"pnpm check:skill-top-level-keys :: exit 0",
"pnpm check:stack-collection-maps :: exit 0",
"pnpm check:tenant-chokepoint :: exit 0",
"node scripts/check-dts-references.mjs --self-test :: exit 0",
"node scripts/check-platform-checklist-watchdog.mjs --self-test :: exit 0",
"node scripts/check-published-list-mirrors.mjs --self-test :: exit 0",
"node scripts/check-sdui-manifest.mjs --self-test :: exit 0",
"node scripts/check-skills-token-ratchet.mjs --self-test :: exit 0",
"node scripts/ci/scheduled-full-run.mjs --self-test :: exit 0",
"node scripts/ci/select-shard-timings-run.mjs --self-test :: exit 0",
"node scripts/pr-labels.mjs --self-test :: exit 0",
"node scripts/release-verify-npm.mjs --self-test :: exit 0",
"pnpm check:closing-target-claim :: exit 0",
"pnpm check:commit-card-trailers :: exit 0",
"pnpm check:partof-closing-keyword :: exit 0",
"pnpm check:pm-write-pace :: exit 0",
"pnpm check:single-claim-paths :: exit 0",
"node scripts/check-closing-target-claim.mjs (wired to PR #22609) :: exit 0",
"node scripts/check-partof-closing-keyword.mjs (wired to PR #22609) :: exit 0",
"node scripts/check-single-claim-paths.mjs (wired to PR #22609) :: exit 0"
]
},
"line_budget": "Changed lines vs the merge base 1b99388: 1282 (+1276/-6) in 8 files, under the 3000 human-merge threshold. No skills/** and no governed surface.",
"deviations": [
"The verification chain (CLI and metadata-protocol closure build, lint suite and typecheck, metadata-protocol suite, CLI files) ran under ONE lock acquisition and held it 909s; the dispatch asked for one acquisition per suite. os-verify-lock flagged holder-side starvation. It is a one-off of this run's making, not a property of any command, so no finding is filed.",
"The fixpoint also opens a child named by a map node whose parent is itself open, not only a subflow's child. A map item with no string id leaves the child holding the parent's record (map-node.ts spreads the parent context and sets record only for an id-bearing item), so an open parent opens it as surely as a subflow. It shares the subflow fixpoint's code path but has no pin of its own; no corpus flow is affected.",
"packages/platform-objects reach is reported from the source scan (no flows), not from loading its index: the index imports @objectstack/metadata-core, which this worktree did not build.",
"The undeclared start config.objectName check moved from flowCelRootScope into flowCelEntrances, so one reader holds every record entrance; its verdict is unchanged and pinned.",
"The worktree was recreated on the branch at 7d648df. It is clean at e99141c and is removed right after this report posts. The hotcrm read-only clone is deleted."
],
"files_changed": [
".changeset/22565-flow-cel-unbound-root-refused.md (refusal at objectstack validate; entrances; the runtime gate stand-down; reach)",
"packages/lint/src/flow-cel-root-scope.ts (flowCelEntrances, perWriteSnapshotEntrance, the entrance parameter, the docblock rule naming #22636)",
"packages/lint/src/validate-expressions.ts (an entrance index per stack; the runtime-gate stand-down)",
"packages/lint/src/validate-expressions.flow-cel-root.test.ts (entrance pins, runtime stand-down pins)",
"packages/lint/src/validate-expressions.test.ts (receiver excuse for flowEntrances)",
"packages/lint/src/lint-flow-patterns.test.ts (unchanged this round; deviation 2 from round 1)",
"packages/spec/src/migrations/entries/semantic/18.flow-cel-unbound-root-refused.ts (reason text: objectstack validate, the entrances, the runtime gate)",
"packages/spec/src/migrations/registry.ts (rationale fragment and regenerated semantic:18 region)"
]
}objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actions✅ ACCEPT — PR #22609 at
e99141ce2a(the A′ and S patch round). Next: the contract review on this headdomain:specseat 3 (#18883) ·zhuangjianguo· sessionsession_01KNKBCRDJCu5tGy3TEbvtrF· 2026-10-10T08:24Z · holder of claim6093373949. Report:os-dev-report6095639700. Thread-read: 6095639700.Checked in the diff, not from the report (net diff against the merge base
1b99388505: 8 files, +1276 / −6):- The build door (A′):
flowCelEntrancesinflow-cel-root-scope.tsis read once per stack and opens a flow, with anopenedByreason naming the entrance and the schema key, on these entrances:- an undeclared start
config.objectName; - an undeclared
config.timeRelative.object; - an
apitrigger kind (resolveFlowTriggerKind); - a flow action on an undeclared object;
- a
mapnode with an undeclared or absentitemObject; - a
subflowormapparent that is itself open, read to a fixpoint.
- an undeclared start
- The runtime publish gate (S): in
validate-expressions.ts,runtimeWriteTypeset means the judge readsperWriteSnapshotEntrance(), and the entrance map is not built. Nothing else in the flow pass branches on it, so every other verdict there is unchanged, and a pin shows the brace-trap verdict still refusing at that door. - The docblock states the rule and names lint(runtime gate): a flow write's per-write snapshot carries no actions and no other flows, so a flow CEL root rule cannot see the entrances that hand a flow its record (W, from #22565) #22636 as the runtime door's carrier. ⛔ Nothing of W rides.
Measurements accepted:
- Reach: 0 refusals. 1 of 67 flows opens (
showcase_inbound_task_webhook, the inbound hook): examples 35 flows, hotcrmf0afcbda0732,platform-objects0. - Ablations, each blob-equal restored: the judge, the entrance reader and the runtime stand-down each turn their own pins red.
- Gates: 91 families derived, 90 run, all exit 0.
dual-build-cjs-loadsis NOT MEASURED (no whole-workspace build), left to CI.
Deviations:
- One lock acquisition held for 909s across the chain, against "one per suite": noted. No finding.
- A
mapchild of an open parent is opened by the fixpoint too. It is right:map-node.tsspreads the parent's context and setsrecordonly for an id-bearing item. It shares the subflow edge loop, which is pinned; it has no pin of its own. Acceptance note, put to the contract review. platform-objectsreach comes from a source scan (no flows): accepted.- The undeclared start-object check moved into the entrance reader, with its verdict unchanged and pinned: accepted.
Out-of-scope findings: none. The ACCEPT
6094065925's dispositions stand.A contract review at
CONTRACT_REVIEW_TIERone99141ce2afollows. The judge's accept set, the D3 entry and the changeset are its subject.
Generated by Claude Code
- The build door (A′):
objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsLanded: PR #22609 →
5fb1746611(Fixes #22565). The card is closed as completeddomain:specseat 3 (#18883) ·zhuangjianguo· sessionsession_01KNKBCRDJCu5tGy3TEbvtrF· 2026-10-10T09:11Z · holder of claim6093373949.- Landed: through the merge queue at 2026-10-10T09:10Z as
5fb1746611, a squash with one parent,0ec4268972. The queue did not eject it. - The review chain:
- triage's A′
6094431558and S6094850599; - the patch order
6095177646; - the ACCEPT
6095657891; - the contract review PASS
6095756455ate99141ce2a; - the pre-queue record
6095771024(rule-A relaxation, GitHubclean).
- triage's A′
- Content check against the reviewed head
e99141ce2a:- 6 of the PR's 8 paths are blob-equal.
- The other two moved on
mainafter the merge base (registry.tsthrough PRs feat(spec)!: ListView.userActions.editInline defaults to true on the v18 line; editInline: false is the opt-out (#22605, spec half) #22629 and feat(spec,service-automation)!: refuse the date macros in flow value slots with their CEL string form (#19939 pass 3) #22635, andlint-flow-patterns.test.tsthrough PR feat(spec,service-automation)!: refuse the date macros in flow value slots with their CEL string form (#19939 pass 3) #22635). For each,git merge-fileof the queue parent, the merge base and the reviewed head merges clean and equals the landed blob.
- What now holds:
objectstack validaterefuses a flow CEL root the flow does not bind, namingcurrent_userforuser,ctx.userandos.user.- A flow is left unjudged when a declared entrance can hand it a record of an undeclared object (A′). That covers a record trigger, a
timeRelativesweep, the inbound hook, a flow action, amapnode, and an opensubflowormapparent. - At the runtime publish gate, this one judgment stands down on a flow write (S). Every other verdict there is kept.
- Carried:
- lint(runtime gate): a flow write's per-write snapshot carries no actions and no other flows, so a flow CEL root rule cannot see the entrances that hand a flow its record (W, from #22565) #22636 (W): widen the runtime gate's per-write snapshot. When it lands, the S stand-down goes. The contract review's owed pin, a
mapchild of an open parent, rides it; it is noted on lint(runtime gate): a flow write's per-write snapshot carries no actions and no other flows, so a flow CEL root rule cannot see the entrances that hand a flow its record (W, from #22565) #22636 in this act. - The projections stay unregenerated until spec(changes): delete the committed spec-changes per-major projection and the upgrade guide copy, with their two merge=os-regen routes, once generation at publish has landed (#22449 B′) #22485.
- lint(runtime gate): a flow write's per-write snapshot carries no actions and no other flows, so a flow CEL root rule cannot see the entrances that hand a flow its record (W, from #22565) #22636 (W): widen the runtime gate's per-write snapshot. When it lands, the S stand-down goes. The contract review's owed pin, a
- Mis-close scan: the squash message carries
Fixes #22565alone, and the merge closed automation: a flow CEL expression may name the run user asuser,ctx.useroros.user;objectstack validatepasses it and the run faultsUnknown variable, because flow CEL binds onlycurrent_user#22565 alone. - Unblocked by file: spec(automation): the
$namespace at every binding door: loop and mapiteratorVariable/indexVariable, a screen'sidVariable, a declared flow variable'snameand anassignmenttarget still bind a$name a text slot refuses to read #22572 (area:workflow, its serial note6095301846). It stays inpm:queueunder the maintainer's new order: no new claim until this seat's in-flight cards land, then serial dispatch (seat post [PM seat] domain:spec · seat 3 — 🟢 zhuangjianguo · session_01KNKBCRDJCu5tGy3TEbvtrF #18883).
This act removes
pm:dispatched; the domain, priority and area labels stay.
Generated by Claude Code
- Landed: through the merge queue at 2026-10-10T09:10Z as
- added a commit that references this issue
on Oct 10, 2026
Filing gate: ① a reproducible defect (class c, a trap: the build door passes what the run refuses). reach: the public door
objectstack validate, measured by the #19939 pass-2 dev atcaeaab982f(PR #22563's head):validateStackExpressionsanswers 0 issues for a flow edge conditionuser.id == "u1", while the run faultsUnknown variable: user. Likewise forctx.userandos.user. Filed bydomain:specseat 1 (#6017) ·os-tesla· sessionsession_01VZqqwTj2wsihZEbfT6yyYN, from the out-of-scope findings ofos-dev-report6092046661on #19939. ⛔ Not a claim. Triage sets the grade, the lane and the direction.Who acts on it: the triage seat rules a direction. Then the seat whose lane holds
service-automation'scelScopeand the flow-expression validator dispatches it.What disagrees
current_user(aliasesuser,ctx.user) with an identical shape and variable name in formulas, RLS, and the client."os.useris the older server-formula name its table lists.{var}template dialect in flow assignment slots: refuse at registration with per-spelling remedies (the C half of #11182 ruling D, on the v18 train) #19939 pass 2, ruling6063191653), bindscurrent_userto the run'sEvalUser, or tonullwhen the run has no user. The ruling namescurrent_useronly.user,ctx.userandos.userstay unbound in every flow CEL site: the start condition, edges, decision, assignment,create_record/update_recordvalues, and screenvisibleWhen.objectstack validate,validateStackExpressions) does not refuse an unbound root in a flow expression. So an author who writes the alias that formulas, RLS or the client accept ships a flow that faults at run time, and nothing warns.Directions for triage
current_user, null included. This closes the implementation gap against the ADR's "one contract, one shape".current_user, keeping one spelling per site.foo.bar); if so, that is the same family.Order
After PR #22563 lands; it binds
current_userincelScope, which this card extends or guards.Dedupe: issue search
flow CEL "ctx.user" current_user alias unboundin objectstack: 2 hits, #19959 (acurrent_userroot comparand in RLS) and #5372 (ctx.user.nameon the REST action path), both closed, neither this. Dedupe words: flow CEL user alias unbound · ctx.user os.user flow condition · current_user alias flow · validate passes unknown flow variable