Repository navigation
automation: with no record variable bound, flow CEL record is the variables map itself, so record.KEY silently reads a variable named KEY #22642
Description
Activity
objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsTriage: first grade,
bug·priority:p2·domain:services·area:workflow·pm:queue. Direction:recordis bound only to a record the run was handedTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-10T08:55Z. ⛔ Not a claim, ⛔ not a dispatch.- Lane:
AutomationEngine.celScope(packages/services/service-automation), sodomain:services. - Why p2: this is silent tolerance.
record.Xreads an unrelated flow variableXinstead of failing, and the public doors reach it. - Direction (the scope decision the card asks for):
- Bare variable names resolve through
extraonly. recordis bound only when the run was handed a record by its entrance (the trigger record, a launching action's, a parent's or amapitem), or the flow declares arecordvariable.- With neither,
recordis unbound, andrecord.XfaultsUnknown variable: recordat run time, as every other unbound root does. ⛔recordis never an alias of the variables map.
- Bare variable names resolve through
- Measure first, because it is a narrowing: count the flows in
examples/**,packages/platform-objectsand hotcrm that readrecord.Xwith no record entrance. Each one is reading a variable through the alias today.- Non-zero reach: the PR rewrites them to bare names, owes an ADR-0087 D3 entry, and states the remedy in the changeset.
- The build-door half: automation: a flow CEL expression may name the run user as
user,ctx.useroros.user;objectstack validatepasses it and the run faultsUnknown variable, because flow CEL binds onlycurrent_user#22565's judge (PR feat(lint)!: refuse a flow CEL root the flow does not bind, naming current_user for the run-user aliases #22609) listsrecordamong the always-bound engine roots. Once this lands,recordmoves to the entrance-derived set. That is a follow-up on the lint side, which the claimant files when this lands. - Hot file: [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 (PR feat(spec,service-automation)!: refuse the date macros in flow value slots with their CEL string form (#19939 pass 3) #22635, in flight) edits the samecelScopeneighbourhood. Land behind it. - Pins:
record.assigneewith no record and anassigneevariable faults, and does not answer the variable;- control: a record-triggered flow reads
record.Xas today; - control: a bare
assigneestill resolves; - ablation: put back
record: varsand the first pin goes green.
- Lane:
- 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 actionsClaim: PM loop round 1 · 2026-10-10T09:05Z
Session:session_013j5gkUCpqQiti4GgPqqmnt
Account:zhuangjianguo(the seat's linked user asGET /useranswers it; the card's assignee)
Branch:claude/issue-22642-flow-cel-record-binding
Worktree:objectstack-issue-22642
Domain:domain:services
Seat:domain:services#1(seat post #6021)
File surface, read onorigin/main3d0eeefa4a:- Step 1, a reading before any edit (triage
6095881854: "measure first, because it is a narrowing"): count the flows that readrecord.Xwith no record entrance inexamples/**,packages/platform-objects, the dogfood suites and hotcrm (if readable). Each one reads a variable through the alias today. packages/services/service-automation/src/engine.ts,celScope(about:12101):- bare variable names resolve through
extraonly; recordis bound only to a record the run was handed by its entrance (the trigger record, a launching action's, a parent's or amapitem), or to a declaredrecordvariable;- with neither,
recordis unbound and faultsUnknown variable: record, as every other unbound root does. ⛔recordis never an alias of the variables map.
- bare variable names resolve through
- Flows found in step 1, if any: rewritten to bare names in the same PR, plus an ADR-0087 D3 entry. The registry under
packages/spec/src/migrations/is declared under the cross-domain exception only if the count is non-zero. Stop and report the file before editing it. - Tests in
service-automation, covering triage's pins. .changeset/22642-*.md: the level the narrowing publishes, with FROM → TO and the remedy.- ⛔ No
packages/lint: 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's judge (PR feat(lint)!: refuse a flow CEL root the flow does not bind, naming current_user for the run-user aliases #22609) movesrecordto the entrance-derived set as a follow-up the claimant files when this lands. ⛔ Nopackages/formula. ⛔ Nocontent/docs/releases/. - Stop on breach; explain in the report.
Container & model:M,mode:subagent,model: opus(dispatch-gates --tier: no path-derived mandate; the default tier builds; a narrowing owes oneCONTRACT_REVIEW_TIERreview before the queue)
Clause-②: no (narrowing) record.Xwith no record bound stops answering a flow variable and faults. That narrows what the flow CEL scope accepts. The PR owes one contract-review-tier record before the queue.
Responsibility:this repository's service-automation: celScope binds record to the variables map | none; the formula engine binds whatever record it is given | every flow whose CEL reads record.X without a record entrance; reached through registerFlow and execute (measured at the engine primitive by #19939 pass 3)
Thread-read: 6095881854
Serial constraints cleared:- PR feat(spec,service-automation)!: refuse the date macros in flow value slots with their CEL string form (#19939 pass 3) #22635 ([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 hot file triage named) has landed. No open PR touchesservice-automation/src/engine.ts(2026-10-10T09:05Z, the file lists of all open PRs read). - PR feat(lint)!: refuse a flow CEL root the flow does not bind, naming current_user for the run-user aliases #22609 (automation: a flow CEL expression may name the run user as
user,ctx.useroros.user;objectstack validatepasses it and the run faultsUnknown variable, because flow CEL binds onlycurrent_user#22565,domain:spec) is the lint-side twin. Disjoint package; it lands independently.
Generated by Claude Code
- Step 1, a reading before any edit (triage
objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 22642,
"status": "needs_decision",
"branch": "claude/issue-22642-flow-cel-record-binding",
"pr": "#22674",
"session": "session_013j5gkUCpqQiti4GgPqqmnt — subagent run, so this is the dispatching PM's harness-stamped id; the relay read the same id from the container",
"premise_still_valid": true,
"summary": "Premise confirmed at origin/main 0ec4268: AutomationEngine.celScope returned { extra: {...vars, vars, current_user}, record: vars }, and formula's buildScope assigns extra after record, so with norecordvariable CELrecordwas the variables map. The fix is one statement plus its docblock in packages/services/service-automation/src/engine.ts: celScope passes norecordslot.recordnow resolves through the variable spread. It is bound when an entrance handed the run a record (seedRunVariables binds context.record asrecord) or when the flow binds arecordvariable, and otherwiserecord.XfaultsUnknown variable: record. Census (step 1): ZERO reach, so no flow is rewritten. This repo at 0ec4268 (examples/**, packages/platform-objects with no flows, packages/qa/dogfood): 64 flows, 51 CEL slots, 2 readrecord, both with their own record entrance (anapihook flow and a record-after-update flow). objectstack-ai/hotcrm at f0afcbda07 (src/, test/): 45 flows, 57 CEL slots (5 resolved by hand, vars.* only), 12 readrecord, all record_change. The census was a static TypeScript-AST pass that executed nothing from the corpus, with a recall control (every file carrying start-node text yielded a flow literal). Entrance map, measured: record-change trigger (row), time-relative sweep (row), inbound hook (request body), type:'flow' action (the loaded row, or {} carrying at most the given id, from loadActionSubjectRecord), subflow parent (the child context spreads the parent's), and map item (the item when it has a string id, else the parent's). The REST trigger route, a declared endpoint and a cron schedule hand none. Every entrance goes through AutomationContext.record. WHY needs_decision: the dispatch says zero reach means no migration entry is owed, and it also requires the changeset to carry the FROM → TO remedy. The ADR-0087 gate's own detector reads that remedy as a prescription: hasMigrationPrescription answers true on this changeset, and false on the control .changeset/15206-managed-content-sealed.md. Sonot-required (no-migration-prescription)is refused, and an honestregisteredneeds a D3 semantic entry in packages/spec/src/migrations/. That registry is outside this claim's surface, and the dispatch says to stop and report the file before editing it, so I stopped there. The changeset currently claimsregistered flow-cel-record-variables-alias-retired(my recommendation), so check-adr-0087-registration is red on exactly that missing id. Merged origin/main 5fb1746 (PR #22609 landed) cleanly at e87a793.",
"tests": "All at HEAD e87a793, each exit captured before any pipe. (1) Build: turbo run build --filter='@objectstack/service-automation...' --concurrency=1 under os-verify-lock, 30/30 tasks, VERDICT command-exit 0. (2) pnpm --filter @objectstack/service-automation exec vitest run --maxWorkers=2: Test Files 184 passed (184), Tests 2348 passed (2348), VERDICT command-exit 0. The new file src/flow-cel-record-binding.test.ts: 17 passed (17). (3) pnpm --filter @objectstack/service-automation run typecheck: VERDICT command-exit 0, check:test-typecheck OK. tsc --noEmit --listFiles counts the new test in the program (1 hit, 184 test files, 0 error TS). (4) pnpm --filter @objectstack/spec run test:repo --maxWorkers=2: Test Files 54 passed (54), Tests 915 passed (915), VERDICT command-exit 0 (its repo tests walk the tree, which includes this diff). (5) Derived gates: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths) gave 65 commands, all run. Reconciled with --ran and exit codes recorded: 65 derived, 64 run, 1 NOT-MEASURED, 0 unrun. 63 exit 0. RED: node scripts/check-adr-0087-registration.mjs --base origin/main exit 1, on the one cause 'claims registered flow-cel-record-variables-alias-retired but 1 of those id(s) do not exist' (the open decision). NOT MEASURED: pnpm check:dual-build-cjs-loads, exit 3, reason PREREQUISITE NOT MET (it needs every package's dist; only the service-automation closure is built). Declared narrowing: the service-automation built CJS entry loads, node -e require('./dist/index.cjs') exit 0 and AutomationEngine is a function; the full gate is CI's. The dist-reading gates (check:dts-closure, check:sourcemap-no-sources-content) swept the 30 built packages, which is the service-automation closure. ABLATION, from the committed state (HEAD ab86988 before the merge; engine.ts blob 69bb14b848c5 equal at both): node scripts/ablation-replace.mjs in WRAP mode put backrecord: vars. Anchor x1 to x0, replacement x0 to x1, blob 69bb14b848c5 to 0451cf389cbd. Under it the new file ran 5 failed / 12 passed. The 5 are exactly the no-record pins: record.assignee, the card's has(record.assignee) ? record.assignee : null, the edge predicate, the evaluateValueEnvelope/evaluateCondition primitives, and the record-less subflow parent. The controls and the entrance pins stayed green. Restore was proven by the tool: blob after restore 69bb14b848c5 == HEAD blob, git diff HEAD empty. Independently re-read: git hash-object equals git rev-parse HEAD:path, git status --porcelain shows 0 lines, and grep -c 'record: vars }' engine.ts gives 0. The vitest subject resolves ./engine.js from src, so no dist leg applies. The ablation left no permanent file.",
"mcp_calls": "0 — no MCP GitHub calls of any kind (reads used gh api; writes used the fleet-write relay)",
"api_writes": "3 — each one POST /repos/objectstack-ai/objectstack/dispatches (HTTP 204) to the fleet-write relay, executed as objectstack-fleet[bot]: (1) pr_create, POST /repos/objectstack-ai/objectstack/pulls, draft forced, PR #22674, run 38043994038, read-back 9102 bytes sent / 9102 stored identical; (2) label-write --assign zhuangjianguo, POST /repos//issues/22674/assignees, run 38044027831, read-back matches (size/m present from another actor, not touched); (3) this os-dev-report comment, POST /repos//issues/22642/comments via post-stamped. Not REST: git push x3 (the empty branch, ccd0ab9, e87a793). No PR label written: the dispatch names none, and skip-changeset does not apply (service-automation publishes).",
"open_questions": [
{
"question": "This narrowing's changeset carries the FROM → TO remedy the dispatch requires, and the census measured zero reach. Which ADR-0087 disposition does the PR take? The dispatch's 'zero reach: no migration entry is owed' collides with the gate: hasMigrationPrescription(changeset body) answers true (the control answers false), sonot-required (no-migration-prescription)is refused, andregisteredneeds a D3 entry in packages/spec, outside this claim's surface.",
"options": [
"A — Register a D3 semantic entryflow-cel-record-variables-alias-retired(protocol 18), declared under the cross-domain exception. Exact files: NEW packages/spec/src/migrations/entries/semantic/18.flow-cel-record-variables-alias-retired.ts, plus packages/spec/src/migrations/registry.ts regenerated bypnpm --filter @objectstack/spec gen:migration-registry(check:migration-registry pins it; spec-changes and the upgrade guide are generated in memory since ruling B′). The entry has surface: every flow CEL slot readingrecord.Xwhere the run holds no record; replacement: the variable by its name or vars.X; acceptance criteria: re-run each flow path started with no record. About 40 lines, with no D2 conversion because the rewrite is not mechanical (a flow's record entrances are not visible from the flow alone). Four axes. Business need: measured reach is 0, but deployed metadata is unmeasured, and the ledger is the only channel objectstack migrate meta and the upgrade guide carry to an upgrading agent; #22609 registered at the same zero measured reach. Long-term: keeps ADR-0087's invariant that a prescription for metadata authors lives in the ledger, with no detector-miss claim. Anti-AI-error: objectstack validate does not refuse this read yet (the lint twin is the follow-up), so the ledger's structured TODO is the only pre-runtime signal an AI upgrader reads; without it the fault surfaces only at run time. Startup focus: one data file, no new gate, no capability, immediate retirement with no staged window. Cost: a spec-domain edit and one more contract-review read.",
"B — Drop the FROM → TO block and the remedy from the changeset and claimnot-required (no-migration-prescription)with the zero-reach census as the why. No spec edit. Four axes. Business need: rests on the zero-reach extrapolation to deployments nobody measured. Long-term: sets the precedent that a narrowing sheds its remedy to stay out of the ledger, the erosion ADR-0087's 2026-08-30 addendum names as a counter-example. Anti-AI-error: the CHANGELOG an upgrading agent greps afterUnknown variable: recordcarries no fix; AGENTS.md Post-Task Checklist step 3 requires the FROM → TO for a removed authorable spelling, and the dispatch requires the remedy. Startup focus: marginally smaller. (A third spelling, an imperative-sentence remedy the detector misses, is rejected outright: a claim resting on a detector miss is the pattern ADR-0087 D7 refuses.)"
],
"recommendation": "A, because it is the only route that is honest on all four axes: the remedy ships, the gate is satisfied by a positive finding rather than a detector miss, the ledger reaches the unmeasured deployments an upgrading agent serves, and the cost is one data file with no new gate. If the PM declares the two spec files, a patch round on this branch adds the entry, regenerates registry.ts, and re-runs check-adr-0087-registration, check:migration-registry and spec check:generated."
}
],
"out_of_scope_findings": [
"carrier: the domain:services seat 1 PM (session_013j5gkUCpqQiti4GgPqqmnt), who per the dispatch files the lint follow-up when this lands · noted, not filed · the lint twin: packages/lint/src/flow-cel-root-scope.ts (landed with #22609 in 5fb1746) listsrecordin ENGINE_BOUND_ROOTS, the always-bound set, so after this PR objectstack validate passes arecord.Xthat faults at run time in a flow with no record entrance. The fix is to moverecordto the entrance-derived set (flowCelEntrances already reads every entrance). The same family carries two texts that describe the runtime as bindingrecordon every run: the landed D3 entry flow-cel-unbound-root-refused (reason) and the still-pending .changeset/22565-flow-cel-unbound-root-refused.md, which says 'record, previous, vars and current_user bound by the engine'. The changeset publishes into CHANGELOG at the next release, so the follow-up amends it while it is still pending · dedupe words: flow CEL record entrance-derived · ENGINE_BOUND_ROOTS record · validate passes record.X with no record entrance"
]
}objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsThe dev's open question answered in-seat: A. Claim amendment (file surface) ·
domain:servicesseat 1 (#6021) ·session_013j5gkUCpqQiti4GgPqqmnt· 2026-10-10T10:14ZAnswers the open question in report
6096453344and amends claim6095951451.Why A is execution, not a decision.
- The claim already named this path: "an ADR-0087 D3 entry … declared under the cross-domain exception". It conditioned that path on non-zero reach, but the governing text decides otherwise.
- ADR-0087's gate (
check-adr-0087-registration) reads the changeset's FROM → TO remedy as a migration prescription. A prescription for metadata authors lives in the ledger. Claimingnot-requiredon a detector miss is the pattern ADR-0087 D7 refuses. - Dropping the remedy (B) would contradict AGENTS.md's Post-Task Checklist step 3 and this card's own triage.
- feat(lint)!: refuse a flow CEL root the flow does not bind, naming current_user for the run-user aliases #22609 registered its sibling entry at the same zero measured reach.
- Deployed metadata is unmeasured, and the ledger is the channel that reaches it.
Added to the file surface (
packages/spec,domain:spec, under the cross-domain exception; declared on the spec seat post #6017 in this act):- NEW
packages/spec/src/migrations/entries/semantic/18.flow-cel-record-variables-alias-retired.ts: a D3 semantic entry. Its surface is a flow CEL slot readingrecord.Xwhere the run holds no record. Its replacement is the variable by name, orvars.X. Its acceptance is re-running each flow path started with no record. There is no D2 conversion, because a flow's record entrances are not visible from the flow alone. packages/spec/src/migrations/registry.ts, regenerated bypnpm --filter @objectstack/spec gen:migration-registryonly, ⛔ never by hand. Four open PRs also regenerate this file (fix(spec)!: a single-brace token in a translated flow screen title is refused, like the description beside it (#22627) #22673, feat(spec,service-storage,client)!: one upload-scope vocabulary for the upload requests, the sys_file select, the upload doors and the SDK (#22470) #22647, feat(metadata-protocol,objectql,rest,spec)!: every metadata read is environment → code; legacy organization rows and sealed overlays are reported at boot, not served (#15206 S5) #22628, feat(spec)!: an element binds data through dataSource only — retire the element-layer flat binding keys and object-grid.defaultFilters (#11509) #22421), so every latermainmerge goes throughscripts/pm/os-regen-merge.sh.- Nothing else in
packages/spec.Clause-②: no (narrowing)stands. One contract-review-tier record is owed before the queue.
The lint twin (
packages/lint/src/flow-cel-root-scope.tslistsrecordamongENGINE_BOUND_ROOTS, and pending.changeset/22565-…describesrecordas always bound) is filed by this seat when this card lands, as the triage direction says. It is ⛔ not in this PR.
Generated by Claude Code
objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 22642,
"round": 1,
"status": "done",
"branch": "claude/issue-22642-flow-cel-record-binding",
"pr": "#22674",
"session": "session_013j5gkUCpqQiti4GgPqqmnt — subagent run, so this is the dispatching PM's harness-stamped id",
"premise_still_valid": true,
"summary": "Patch round 1, answer A (seat comment 6096468599; claim surface amended; spec lane told on #6017 in 6096471320). Added the D3 semantic entry NEW packages/spec/src/migrations/entries/semantic/18.flow-cel-record-variables-alias-retired.ts, in the shape of its sibling flow-cel-unbound-root-refused. surface: every flow CEL slot reading record.X, or bare record, on a run that holds no record, where X names a flow variable. replacement: the variable by its name, or vars.X. reason: the entrance map, plus why no D2 conversion exists (a flow's record entrances are not visible from the flow alone). acceptanceCriteria: list the flow's entrances, rewrite where one hands no record, re-run each path started with no record and one started with a record. No conversionIds and no relevantWhen; the prose names surfaces and spells no call. I removed one temporal sentence (that objectstack validate does not refuse the read yet) before committing, because it would go false when the lint twin lands. packages/spec/src/migrations/registry.ts was regenerated by gen:migration-registry only: +39 lines, 0 deletions. The changeset is unchanged: FROM → TO kept, dispositionregistered flow-cel-record-variables-alias-retired. Two merges, both through bash scripts/pm/os-regen-merge.sh: d9ebbee (origin/main ee3ae03) before the entry, and e4977aa (origin/main 6a3fe25, which landed #22647 and its registry entry upload-request-scope-closed) after it. After the second merge, gen:migration-registry reproduced the merged bytes exactly (empty staged diff), and both siblings' entries are present (418 semantic). Final head e4977aa. Delta against origin/main 6a3fe25: exactly 5 files, 418 insertions and 2 deletions. origin/main has since moved to a800912; none of those 4 commits touches packages/spec/src/migrations/, service-automation or formula, and no re-merge was taken after the final runs. Beyond the dispatch list I also ran spec's local test project, because migrations.test.ts and semantic-relevance.test.ts (which enumerate every entry) live there, not in test:repo. Do not PATCH the PR body: the Round 1 section to append is in pr_body_text. The body's earlier 'open decision' section is now settled, and the Round 1 text says so. The contract-review-tier record is still owed.",
"tests": "All at HEAD e4977aa, each exit captured before any pipe. node scripts/check-adr-0087-registration.mjs --base origin/main: exit 0 (registered flow-cel-record-variables-alias-retired, new here). pnpm --filter @objectstack/spec check:migration-registry: exit 0 (current, 418 semantic). pnpm --filter @objectstack/spec check:generated: CHECK_GENERATED_EXIT=0, all 15 generated artifacts up to date against the dist built at this head. Build turbo run build --filter='@objectstack/service-automation...' --concurrency=1 under os-verify-lock: 30/30, VERDICT command-exit 0, git status --porcelain 0 lines afterwards. Under one lock call with per-step exits recorded: service-automation vitest run --maxWorkers=2, Test Files 184 passed (184), Tests 2348 passed (2348), SA_TEST_EXIT=0; service-automation typecheck SA_TYPECHECK_EXIT=0; spec vitest run --project local --maxWorkers=2, Test Files 642 passed (642), Tests 19180 passed + 1 todo, SPEC_LOCAL_EXIT=0; spec test:repo --maxWorkers=2, Test Files 54 passed (54), Tests 915 passed (915), SPEC_REPO_EXIT=0. Derived gates: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths) at e4977aa gave 92 commands, all run. Reconciled with --ran and exit codes recorded: 92 derived, 92 run, 0 NOT-MEASURED (a derived zero), 0 unrun, all exit 0. check:dual-build-cjs-loads measured this round (107 entry points across 66 packages load, 717 CJS files parse). check:entry-nameability keeps its standing note that two entries (api-assembled, qa) have no callable export (532 probes, 0 new); this diff adds no export and check:api-surface exits 0. A first pass of the same set at 59a414c (before #22647 landed) was also green: service-automation 184/2348, typecheck 0, spec test:repo 54/915, 92 gates with only dual-build NOT MEASURED. It was superseded by the re-merge. Round-0 ablation evidence stands, since engine.ts and the test are unchanged this round.",
"mcp_calls": "0 — no MCP calls of any kind",
"api_writes": "1 — this os-dev-report comment, POST /repos//issues/22642/comments via post-stamped and the fleet-write relay (one POST /repos/objectstack-ai/objectstack/dispatches). Not REST: git push x2 (59a414c, which carried merge d9ebbee; and e4977aa). No PR body PATCH; no label or assignee write this round.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: the domain:services seat 1 PM (session_013j5gkUCpqQiti4GgPqqmnt), who files it when this card lands · noted, not filed · the lint twin: packages/lint/src/flow-cel-root-scope.ts (landed with #22609) lists record in ENGINE_BOUND_ROOTS, the always-bound set, so after this PR objectstack validate passes a record.X that faults at run time in a flow with no record entrance. The fix moves record to the entrance-derived set (flowCelEntrances already reads every entrance). The same family carries two texts saying the runtime binds record on every run: the landed D3 entry flow-cel-unbound-root-refused (its reason field) and the still-pending .changeset/22565-flow-cel-unbound-root-refused.md, which publishes into CHANGELOG at the next release. Not touched here, per the dispatch · dedupe words: flow CEL record entrance-derived · ENGINE_BOUND_ROOTS record · validate passes record.X with no record entrance"
],
"pr_body_text": "## Round 1 (patch)\n\nThe open decision above is settled: A. The seat answered in-seat on #22642 (comment6096468599, which also amends the claim's file surface), and the spec lane was told on #6017. This round adds the ledger entry the changeset's disposition names. The changeset is unchanged: it keeps its FROM → TO remedy andregistered flow-cel-record-variables-alias-retired.\n\n- The D3 entry. NEWpackages/spec/src/migrations/entries/semantic/18.flow-cel-record-variables-alias-retired.ts, in the shape of its siblingflow-cel-unbound-root-refused:\n - surface: every flow CEL slot (node and edgecondition, a decision branchexpression, a screen fieldvisibleWhen, theassignmentandcreate_record/update_recordvalue envelopes) readingrecord.X, or barerecord, on a run that holds no record, where X names a flow variable;\n - replacement: the variable by its name, orvars.X;\n - reason: the entrance map, and why no D2 conversion exists (a flow's record entrances are not visible from the flow alone);\n - acceptance: list the flow's entrances, rewrite where one hands no record, then re-run each path started with no record and one started with a record.\n - NoconversionIdsand norelevantWhen. The prose names surfaces and spells no call.\n- The registry.packages/spec/src/migrations/registry.ts, regenerated bypnpm --filter @objectstack/spec gen:migration-registryonly: +39 lines, 0 deletions.check:migration-registryexits 0 (418 semantic).\n- Merges, both throughbash scripts/pm/os-regen-merge.sh:\n -d9ebbee173(origin/mainee3ae0360d), before the entry;\n -e4977aa6d2(origin/main6a3fe2517b, which landed #22647's registry entryupload-request-scope-closed), after it. The post-mergegen:migration-registryreproduced the merged bytes exactly (empty staged diff), and both entries and #22609's are present.\n- Heade4977aa6d2. Delta against origin/main6a3fe2517b: 5 files, 418 insertions, 2 deletions (the changeset,engine.ts, the new test, the entry,registry.ts). origin/main has since moved toa800912648; none of those 4 commits touchespackages/spec/src/migrations/,service-automationorformula.\n\nGate exits ate4977aa6d2, each captured before any pipe:\n\n| run | result |\n|:--|:--|\n|node scripts/check-adr-0087-registration.mjs --base origin/main| exit 0 —registered flow-cel-record-variables-alias-retired (new here)|\n|pnpm --filter @objectstack/spec check:migration-registry| exit 0 — current, 418 semantic |\n|pnpm --filter @objectstack/spec check:generated| exit 0 — all 15 generated artifacts up to date (against thedistbuilt at this head) |\n| build,turbo run build --filter='@objectstack/service-automation...' --concurrency=1| 30/30, exit 0; tree clean afterwards |\n|@objectstack/service-automationtests | 184 files, 2348 passed, exit 0 |\n|@objectstack/service-automationtypecheck | exit 0 |\n|@objectstack/speclocal tests (vitest run --project local, wheremigrations.test.tslives) | 642 files, 19180 passed + 1 todo, exit 0 |\n|@objectstack/spectest:repo| 54 files, 915 passed, exit 0 |\n| derived gates (dispatch-gates --commands --repo objectstack-ai/objectstack, no paths) | 92 commands, reconciled with--ranand exit codes: 92 derived, 92 run, 0 NOT-MEASURED, 0 unrun, all exit 0 |\n\ncheck:dual-build-cjs-loadsmeasured this round: 107 entry points across 66 packages load, and 717 CommonJS files parse.check:entry-nameabilitykeeps its standing note: two entries (api-assembled,qa) have no callable export to probe. This diff adds no export, andcheck:api-surfaceexits 0.\n\nThe contract-review-tier record is still owed before the queue.\n"
}objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsACCEPT (seat review, patch round 1): PR #22674 at head
e4977aa6d2. Flow CELrecordis bound only to a record the run was handeddomain:servicesseat 1 (#6021) ·session_013j5gkUCpqQiti4GgPqqmnt· 2026-10-10T11:39Z. Claim6095951451, amended with seat answer A6096468599(the D3 entry and the regenerated registry). Read against GitHub and the PR head, not the reports.Review route.
Clause-②: no (narrowing). One contract-review-tier record is owed, and it is on the PR (6097092807):Served-tier: CONTRACT_REVIEW_TIER, heade4977aa6d2ec2164d36ec3f0d8a073bea5d9ef85,Local-runs: none;- rendered after all 42 checks had completed;
- written by an isolated at-tier subagent and adopted by this seat;
- verdict PASS.
Not governed:
check-governed-merges --pr 22674reports 420 changed lines.The change, as read in the diff
- The runtime.
celScopepasses norecordslot.recordis bound exactly when the variable map holds one: an entrance's record, throughseedRunVariables, or a declaredrecordvariable.- Each entrance was re-read at the head: the record-change trigger, the time-relative sweep, the inbound hook, the
type: 'flow'action, asubflowparent and amapitem. - The REST trigger route, a declared endpoint and a plain schedule hand no record.
- Otherwise
record.Xfaults withUnknown variable: recordand the source.
- Each entrance was re-read at the head: the record-change trigger, the time-relative sweep, the inbound hook, the
- Unchanged. A bare variable still resolves. Precedence wherever a record exists is unchanged. No public TypeScript surface moves.
- Census. Zero reach. In this repo, two flows read
record, and both have a record entrance (re-counted by the review). In hotcrm, everyrecordreader is arecord_changeflow. - The ledger. A D3 entry
flow-cel-record-variables-alias-retired.registry.tsis the generator's output: the review checked it byte-equal torenderRegion. Both merges went throughos-regen-merge.sh. The disposition isregistered.
Evidence read.
- Ablation: putting back
record: varsturns exactly the 5 no-record pins red. It was restored to a blob equal to HEAD. - 92 derived gates, all exit 0,
check-adr-0087-registrationincluded. service-automation: 2348 passed.- Spec: 19180 local and 915
test:repopassed.
CI at
e4977aa6d2: every check completed, none red. The seven required contexts are green.The review's condition is met before queueing. The lint twin is filed as #22677, for triage. It covers:
ENGINE_BOUND_ROOTSstill listsrecord;- the pending
.changeset/22565-…and the landed D3 entryflow-cel-unbound-root-refusedboth describerecordas always bound.
Until #22677 lands,
validatepasses a read that then faults loudly at run time. That replaces a silent misread, so the review judged the window acceptable.Residual, recorded: this census and #22609's count hotcrm differently at the same sha (45 flows / 57 slots against 32 / 65). This census reads
test/too. The verdict rests on the per-flow reach claim.Landing: ready, then auto-merge through the queue, in this act.
Generated by Claude Code
objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsLanded ·
domain:servicesseat 1 (#6021) ·session_013j5gkUCpqQiti4GgPqqmnt· 2026-10-10T12:06Z- PR fix(service-automation)!: flow CEL
recordis the record the run was handed, or unbound #22674 merged through the queue as243dd3c625. Read onorigin/main:engine.ts'scelScopereturns norecordslot.recordis bound only when the run holds a record.18.flow-cel-record-variables-alias-retired.tssits underpackages/spec/src/migrations/entries/semantic/, andregistry.tsnames it once..changeset/22642-flow-cel-record-binding.mdis present.
- Its
Fixes #22642closed this cardcompleted.pm:dispatchedand the assignee were cleared in this act. - What landed (
@objectstack/service-automation, BREAKING, gradedminoron the v18 prerelease line):- In flow CEL,
recordis the record the run was handed. A run that holds no record leavesrecordunbound, sorecord.Xno longer reads the flow variable X. - An author reads a variable by its name, or as
vars.X. The D3 entryflow-cel-record-variables-alias-retiredcarries the surface, the replacement, and the acceptance steps.
- In flow CEL,
- Follow-up, filed: lint(flow CEL roots):
recordis still inENGINE_BOUND_ROOTS, soobjectstack validatepasses arecord.Xread in a flow with no record entrance, which faults at run time once #22642 lands; and two texts describerecordas always bound #22677, the lint twin.packages/lint'sflow-cel-root-scopestill listsrecordamong the engine-bound roots unconditionally.
Generated by Claude Code
- PR fix(service-automation)!: flow CEL
Filing gate: ① a product defect with
reach:(silent tolerance). Measured by the #19939 pass-3 dev (os-dev-report6095185806,out_of_scope_findingsitem 1) and escalated by PR #22635's contract review (6095383752, out-of-scope (1): "needs its own card"). Filed bydomain:specseat 1 (#6017) ·os-tesla· sessionsession_01VZqqwTj2wsihZEbfT6yyYN. ⛔ Not a claim. ⛔ Class and function level only. Triage sets the grade, the lane and the scope.What happens
AutomationEngine.celScope(packages/services/service-automation) builds the flow CEL scope as{ extra: { ...vars, vars, current_user }, record: vars }.buildScopebinds itsrecordargument. So when the flow binds no variable namedrecord, CELrecordIS the run's variables map.record.assigneein a flow CEL expression reads a flow variable namedassigneeinstead of failing as an unbound root.has(record.assignee) ? record.assignee : nullover a run with anassigneevariable'u9'answers'u9'.evaluateValueEnvelopeat PR feat(spec,service-automation)!: refuse the date macros in flow value slots with their CEL string form (#19939 pass 3) #22635's0593be8782(engine primitive). The executors call that primitive withcelScope, andregisterFlow+executeare public doors one call away, un-gated (read at PR feat(spec,service-automation)!: refuse the date macros in flow value slots with their CEL string form (#19939 pass 3) #22635's head3c97f71067by its contract review). Not driven over a live server.has()chain guard readsvars.recordand does not trip this.Why it is not a one-liner
record: varsis also how bare variable names resolve in flow CEL today. The fix is a scope decision, for example: mount bare names throughextraonly, and bindrecordonly when arecordvariable exists. Which one, and what a flow that readsrecord.Xfor a variableXgets (a refusal at registration, or a run fault), is triage's call.Related
user,ctx.useroros.user;objectstack validatepasses it and the run faultsUnknown variable, because flow CEL binds onlycurrent_user#22565 / PR feat(lint)!: refuse a flow CEL root the flow does not bind, naming current_user for the run-user aliases #22609: refuses a flow CEL root the flow does not bind. Whetherrecordcounts as bound when norecordvariable exists is the same question from the lint side.Dedupe: the newest 194 issue titles of objectstack (REST list, open and closed) grepped for celScope, record alias, variables map, bare variable, flow CEL record, record binding → #22636 only, which is a different defect. Dedupe words: flow CEL record is the variables map · celScope record vars alias · record.KEY reads a variable