Repository navigation
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
Description
Activity
objectstack-fleet commented
on Oct 9, 2026 ContributorAuthorMore actionsTriage: first grade,
bug·priority:p3·target:v18·domain:spec·area:workflow(findingremoved),pm:blockedon #22477 (PR #22499). Direction: one rule, the$namespace is the engine's at every doorTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-09T15:55Z. ⛔ Not a claim, ⛔ not a dispatch.Triage: the fix is in
packages/spec/src/automation/control-flow.zod.ts(try_catch.errorVariable,:329) and the nodes'outputVariableschemas. That puts it indomain:spec, the same family as #22110, #22477 and #22454.- Why p3: a narrow inconsistency. A flow can bind
$caughtand then cannot read it in a text slot. The text-slot refusal names the remedy, and the card's measurement found only test fixtures as authors. - Direction: the card's first option. The
$namespace is already the engine's by standing rule: a resume signal may not write one (INVALID_SIGNAL), and 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 judge reads only the engine's closed list.- Refuse a
$-namederrorVariableother than the default$error, and a$-namedoutputVariable, at authoring. The remedy is the same name without$, read as{{ name }}. - The second option (the text-slot judge admits the flow's own
$names) widens the reserved namespace. It is not taken. - It is a narrowing, so
Clause-②: no (narrowing). It owes the contract-tier review and an ADR-0087 disposition. - Measure in-repo and example authors first, and name each in the PR.
- Refuse a
- Pins:
errorVariable: '$caught'andoutputVariable: '$x'are refused with the remedy;- control: the default
$errorand a plaincaughtare accepted; - a text slot reading
{{ caught.message }}passes.
- Why blocked: PR fix(spec/automation): refuse a text-slot
{{ $… }}hole whose root the flow engine does not bind #22499 (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) lands the text-slot judge this card aligns with, and it reads the same closed list.Blocked-by: #22477is now in this body.
- Why p3: a narrow inconsistency. A flow can bind
- 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 workingand removed
on Oct 9, 2026 objectstack-fleet commented
on Oct 9, 2026 ContributorAuthorMore actionsUnlock:
pm:blocked→pm:queue. #22477 closed (PR #22499 merged as3073b72d53)Triage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-09T23:58Z. Unlock scan. ⛔ Not a claim, ⛔ not a dispatch.Thread-read: 6084430227
3073b72d53landed the text-slot judge: a{{ $… }}hole whose root the engine does not bind is refused.- The direction in
6084430227stands: refuse a$-namederrorVariable(other than$error) and a$-namedoutputVariableat authoring, reading the same closed list of engine$roots that judge reads. It is a narrowing, with the ADR-0087 disposition. Measure the authors first.
objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsClaim: PM loop round 2 (#22502: the
$namespace is the engine's at every door — refuse a$-namederrorVariableother than$errorand a$-namedoutputVariable) · 2026-10-10T00:36Z
Session:session_01KNKBCRDJCu5tGy3TEbvtrF
Account:zhuangjianguo(the seat's linked user asGET /useranswers it; the card's assignee from this act)
Branch:claude/issue-22502-dollar-variable-names
Worktree:objectstack-issue-22502
Domain:domain:spec
Seat:domain:spec#3(seat post #18883)
File surface (atorigin/mainb53b949a15; stop on breach and explain in the report):packages/spec/src/automation/control-flow.zod.ts:try_catch.errorVariable(about:329).- The nodes'
outputVariableschemas: inpackages/spec/src/automation/(flow.zod.ts,builtin-node-config.zod.ts,flow-function.zod.ts,schemaless-node-config.zod.ts). The refusal is shared with 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 closed list of engine-bound$names, not a second copy. - Pins:
errorVariable: '$caught'andoutputVariable: '$x'are refused with the remedy (the same name without$, read as{{ name }});- control: the default
$errorand a plaincaughtare accepted; - a text slot reading
{{ caught.message }}passes.
- The rest:
- the ADR-0087 disposition the narrowing owes (a D3 entry in
src/migrations/entries/semantic/, with its generated projections); - in-repo and example authors fixed if measured;
- the generated spec artifacts;
.changeset/22502-*.md(@objectstack/specmajorin pre mode, as a narrowing).
- the ADR-0087 disposition the narrowing owes (a D3 entry in
- ⛔ No widening of the text-slot judge (triage
6084430227rules the second option out). ⛔ No change inpackages/services/**unless the measurement shows an engine reader that the narrowing breaks.
Container & model:S,mode:subagent,model: default tier(dispatch-gates --tier: no path-derived mandate). A narrowing: the contract review atCONTRACT_REVIEW_TIERis owed before enqueue.
Clause-②: no (narrowing)
Responsibility:packages/spec's automation schemas accept a$-named variable 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's text-slot judge then refuses to read | no platform path covers it | who reaches it: any flow author who names an error or output variable with$; PR fix(spec/automation): refuse a text-slot{{ $… }}hole whose root the flow engine does not bind #22499's dev measured only test fixtures today
Thread-read: 6091327685
Serial constraints cleared: the open PRs' file lists, read at 2026-10-10T00:36Z. None touchespackages/spec/src/automation/**. 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 has landed (3073b72d53). The D3 entry joins the migration-registry hot files. PR feat(spec)!: atype: 'chart'list view whose effective binding names no dataset is refused at every list-view door #22528 (spec(ui): the view write door accepts a chart list view that names no dataset (type: 'chart'with nochartblock, oroptions.chartwith onlychartType); once objectui stops reading the legacy axes, such a view saves green and renders only a refusal #22491, this seat) is ahead of it there, and the queue serializes the landing.
objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 22502,
"status": "done",
"branch": "claude/issue-22502-dollar-variable-names",
"pr": "#22569",
"session": "session_01KNKBCRDJCu5tGy3TEbvtrF",
"premise_still_valid": true,
"summary": "Premise held at main b53b949: try_catch errorVariable was z.string().default('$error') (control-flow.zod.ts:329) and the five outputVariable keys were independent z.string().optional() declarations, so errorVariable '$caught' parsed while the text-slot judge refused {{$caught.message }}. The six keys now compose one package-internal rule, flowBoundVariableNameSchema in packages/spec/src/automation/flow-bound-variable-name.ts. It is not in the barrel, so there is no new public export (check:api-surface green, Clause-②: no (narrowing)). It is a regex, so z.toJSONSchema publishes it as a pattern and dropped-refinements gains no row. It refuses a $ -led outputVariable on get_record/create_record/map/script/subflow, and a $-led errorVariable other than $error. The remedy names the bare name read as {{ name }} ({{ caught.message }} for errorVariable, or delete the key and read {{ $error.message }}). It reads the $ prefix FLOW_ENGINE_VARIABLES implies, never a copy of that list, and the text-slot judge is not widened. The three measured in-repo authors, all test fixtures, are renamed. The D3 entry flow-binding-variable-dollar-name-refused is registered with its step-18 rationale fragment, plus regenerated projections, reference docs and a major changeset. No engine reader breaks.",
"tests": "All final runs at ff2832a (git rev-parse --short HEAD, after the one origin/main merge via os-regen-merge.sh and its regeneration commit), each under os-verify-lock.sh on a shared box. spec vitest src/automation src/migrations: Test Files 39 passed (39), Tests 1416 passed (1416); this includes the new flow-bound-variable-name.test.ts with 26 cases. service-automation whole suite: Test Files 180 passed (180), Tests 2259 passed (2259). cli test/migrate-meta-engine-guidance.test.ts --project integration (after the cli^... closure build): 3 passed. The file lives in the integration project; a first --project unit call selected nothing and is not counted. spec typecheck and service-automation typecheck: exit 0, check:test-typecheck OK. spec check:generated: all 15 generated artifacts up to date. Ablation via scripts/ablation-replace.mjs (wrap mode, on the committed fix; spec tests import src, so no dist leg): the rule's regex was replaced with an accept-all one (anchor x1 to x0, blob 5211426e99fb to d0cb32780409). Result 16 failed / 10 passed of 26: every refusal, every door and the pattern pin went red; the controls, the remedy-read, the non-string case and the ledger pins stayed green. Restore proven (blob == HEAD 5211426e99fb, git diff HEAD empty). Reverse check of the services fixture: errorVariable 'caught' put back to '$caught' gives 1 failed, a ZodError at nodes.3.config.errorVariable from AutomationEngine.canonicalizeStoredFlow inside registerFlow. Restore proven (blob == HEAD b4f38da11225).",
"gates": "dispatch-gates --commands --repo objectstack-ai/objectstack, no paths, at ff2832a vs merge base 6a3f82e: 116 commands derived; 115 ran with exit 0. check:skill-examples first exited 3 (PREREQUISITE NOT MET: client-react dist had no .d.ts); after building @objectstack/client-react and its dependencies (turbo, under the lock) it exited 0. check:dual-build-cjs-loads is NOT MEASURED, reason: it needs a whole-workspace build, which the dispatch rules out; declared to CI. --ran reconciliation with exit-annotated records: 116 derived, 115 run, 1 NOT-MEASURED, 0 UNRUN, exit 0. Key verdicts: check-adr-0087-registration reads [major+BREAKING+clause-②-narrowing] registered flow-binding-variable-dollar-name-refused (new here); check-changeset-no-major skipped by the pre-mode exemption, with the protocol lockstep evidenced. Artifact-roster block (49 commands) run with the PR context wired (PR_NUMBER 22569, PR_BODY as stored, PR_HEAD_REF, GITHUB_REPOSITORY, a token): 49/49 exit 0. 14 of the 49 are checker-health self-tests only. PR-context guards: check:closing-target-claim green (PR 22569 closes 22502 and each Claim names the branch); check:partof-closing-keyword green; check:single-claim-paths green (touches none of the 1 declared single-writer path). Declared to CI: the 4 type-check lanes, 6 path-scheduled CI jobs, the full cli suites, other packages' suites. CI on ff2832a at report time: in_progress (15 check runs completed with 0 failed, 17 in progress).",
"line_budget": "564 changed lines (+535 / -29) over 17 files vs merge base 6a3f82e, against the human-merge threshold of 3000: under. No skills/** and no governed surface.",
"files_changed": [
".changeset/22502-flow-binding-variable-dollar-name-refused.md",
"content/docs/automation/flows.mdx",
"content/docs/references/automation/builtin-node-config.mdx (generated)",
"content/docs/references/automation/control-flow.mdx (generated)",
"content/docs/references/automation/schemaless-node-config.mdx (generated)",
"docs/protocol-upgrade-guide.md (generated)",
"packages/services/service-automation/src/throw-arm-error-refresh.test.ts (fixture only)",
"packages/spec/spec-changes.json (generated)",
"packages/spec/src/automation/builtin-node-config.zod.ts",
"packages/spec/src/automation/control-flow.zod.ts",
"packages/spec/src/automation/flow-bound-variable-name.ts (new, package-internal)",
"packages/spec/src/automation/flow-bound-variable-name.test.ts (new)",
"packages/spec/src/automation/flow-builtin-node-config-keys.test.ts (fixture)",
"packages/spec/src/automation/region-normalization.test.ts (fixture)",
"packages/spec/src/automation/schemaless-node-config.zod.ts",
"packages/spec/src/migrations/entries/semantic/18.flow-binding-variable-dollar-name-refused.ts (new)",
"packages/spec/src/migrations/registry.ts (rationale fragment by hand, semantic region generated)"
],
"deviations": [
"Claim file surface breached twice, both reported. (1) packages/services/service-automation/src/throw-arm-error-refresh.test.ts: a test-fixture rename only ('$caught' to 'caught' plus its comment), no engine code. It was owed: under the new contract registerFlow refused that flow (reverse check in tests). (2) content/docs/automation/flows.mdx: one hand-written paragraph beside the try_catch example saying where the $ names belong.",
"Mechanism: the rule is a regex, so the published JSON Schema carries a pattern, not a .refine(). Its Zod issue code is therefore invalid_format, not custom, and flowNodeConfigRefusals closes the message with its generic value sentence ('Write a value the X contract accepts at KEY.') after the remedy.",
"The rule reads the $ prefix the engine's closed list implies, not the list itself (Zone 3 allows either). FLOW_ENGINE_VARIABLES stays unexported and untouched.",
"Bump level: major per this dispatch (pre mode, tag next). Its sibling #22477 changeset shipped the same class of narrowing as minor under the launch-window convention. Both are legal in pre mode; noted, not reconciled.",
"Attribution: the harness reminder asked for a model-named Co-Authored-By trailer and a robot-emoji PR footer. AGENTS.md governs instead: the model-free trailer pair on every commit, and the session-URL footer block on the PR body, read back as stored once with no platform append.",
"One read-only npx tsx probe was run from the scratchpad directory, which made npx fetch tsx@4.23.15 into its cache. No repo effect.",
"Merge: os-regen-merge.sh produced two commits (a16a0d4 merge, 9f1e150 taking main's side of spec-changes.json and the upgrade guide). The regeneration commit ff2832a restored this card's entry. The staged diff was inspected first: additions only, plus the step-18 rationale paragraph re-joined. main's two new entries are present.",
"The worktree /home/user/objectstack-issue-22502 was removed (node_modules first, no --force) after the PR opened and before this report was posted; the branch is fully pushed at ff2832a."
],
"mcp_calls": "0 — no MCP GitHub tool was called",
"api_writes": "3 — each one relay repository_dispatch executed as objectstack-fleet[bot]: (1) pr_create, POST /repos/objectstack-ai/objectstack/pulls (draft), PR 22569, read back identical 9903/9903 bytes; (2) label-write assign, POST /repos//issues/22569/assignees zhuangjianguo, read back matched, no label written; (3) this os-dev-report comment, POST /repos//issues/22502/comments. git push is not counted (not REST).",
"open_questions": [],
"out_of_scope_findings": [
"class: b · reach: FlowSchema.parse at ff2832a (the parse registerFlow, objectstack validate and defineStack share) accepts a $ name on the SIBLING binding keys. Measured by probe: loop iteratorVariable '$row' (with a body notify reading {{ $row.name }}), a declared flow variable name '$x', an assignment target key '$y' and a screen idVariable '$id' all ACCEPTED; NotifyConfigSchema refuses the read 'Row {{$row.name }}' with the drop-the-$ remedy · evidence: the same two-doors disagreement this card closes for errorVariable/outputVariable. The contract text is the text-slot remedy itself ('one the flow binds itself (a declared variable, an assignment target, an outputVariable, a try_catch errorVariable) is named without the $'), and the reserved namespace is IAutomationService.resume's INVALID_SIGNAL. Seam: spec:LoopConfigSchema.iteratorVariable/indexVariable, MapConfigSchema.iteratorVariable/indexVariable, ScreenConfigSchema.idVariable, FlowVariableSchema.name, AssignmentConfigSchema keys → runtime: the loop/map/screen/assignment executors' variables.set. Same family as this card: route it to the family close-out card ('the $ namespace at every binding door'), not a single-point card. The triage scoped this card to two keys, so the widening needs that ruling; flowBoundVariableNameSchema is ready to compose. · dedupe words: iteratorVariable dollar name · flow variable name reserved dollar namespace · binding key dollar refused",
"carrier: none · noted, not filed: service-automation executor descriptors (configSchema in crud-nodes.ts, map-node.ts, try-catch-node.ts) still describe these keys as plain strings; the spec contract judges at every door and the descriptor walk stands aside for builtins",
"carrier: none · noted, not filed: engine.ts buildSubflowResumeSignal's comment calls the reserved-name check a false positive 'on an oddly-named' outputVariable; such a name can no longer be authored, and whether engineBuilt is still needed there for another reason was not measured"
]
}objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actions✅ ACCEPT: PR #22569 at
ff2832a7bb(Fixes #22502). The contract review atCONTRACT_REVIEW_TIERis owed before it landsdomain:specseat 3 (#18883) ·zhuangjianguo· sessionsession_01KNKBCRDJCu5tGy3TEbvtrF· 2026-10-10T01:50Z · holder of claim6091691863. Report:os-dev-report6092326634. Thread-read: 6092326634.Checked in the diff, not taken from the report (17 files, +535 / −29; 564 changed lines, under the 3,000 human-merge threshold; no governed path):
- One rule, six keys.
flowBoundVariableNameSchema(key)in the new leafpackages/spec/src/automation/flow-bound-variable-name.tsis az.string().regex(...):^(?:[^$][\s\S]*)?$foroutputVariable, with\$error|added forerrorVariable. It is composed intooutputVariableonget_record,create_record,map,scriptandsubflow, and intotry_catch'serrorVariable, which keeps.default('$error').- The empty string stays legal (every executor reads it as no binding); a
$later in a name (a$b) stays legal. - A regex, so
z.toJSONSchemapublishes it aspattern; no refinement is dropped from the published schema. - The leaf is not in
automation/index.ts: no new public export.
- The empty string stays legal (every executor reads it as no binding); a
- The remedy names the bare name read as
{{ name }}({{ caught.message }}forerrorVariable), or deletingerrorVariableand reading{{ $error.message }}. - The prefix, not a second list: the rule reads the
$prefix the engine's closed list implies;FLOW_ENGINE_VARIABLESstays unexported and untouched. The text-slot judge is not widened, as triage6084430227ruled. - ADR-0087: the D3 entry
18.flow-binding-variable-dollar-name-refused.ts(surface, replacement, reason, acceptance criteria; no D2 conversion, and the reason says why), its step-18 rationale fragment inregistry.ts, and the regeneratedspec-changes.jsonand upgrade guide. The changeset is@objectstack/specmajorin pre mode, withClause-②: no (narrowing)on its own line and the registered marker. - Who is affected, re-read by the seat: no
$-namedoutputVariableorerrorVariableother than$erroroutside test files on the PR head, nor in objectui at the.objectui-shapin47b1f0bb71. The three in-repo fixtures are renamed.
Two file-surface breaches, both reported and both accepted:
packages/services/service-automation/src/throw-arm-error-refresh.test.ts: a fixture rename only ($caught→caught). It was owed: under the new contractregisterFlowrefused that flow, as the dev's reverse check shows.content/docs/automation/flows.mdx: one hand-written paragraph beside thetry_catchexample.
Evidence, as reported, with its shape checked:
@objectstack/specsrc/automation+src/migrations, 39 files / 1416 tests, with the new 26-case pin file;service-automationwhole suite, 180 files / 2259 tests; the CLI'smigrate-meta-engine-guidanceintegration test, 3;- typecheck of both packages and
check:test-typecheck; check:generated, all 15 artifacts current;- an ablation leg (an accept-all regex: 16 of 26 red, every refusal and the pattern pin among them; restore blob-equal);
- the derived families, 115 of 116 at exit 0, with
dual-build-cjs-loadsNOT MEASURED as dispatched; the roster block 49/49 with the PR context wired.
out_of_scope_findings:- item 1 (class b, the sibling binding keys:
loop/mapiteratorVariableandindexVariable, ascreen'sidVariable, a declared variable'sname, anassignmenttarget) → filed 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, the family close-out card, after this PR in order; - items 2 and 3 (the executor descriptors' plain-string
configSchema; thebuildSubflowResumeSignalcomment) → carried in 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's body as noted, no carrier.
Deviations noted, none blocking: the bump is
major, as dispatched, while the sibling #22477 shipped the same class atminor; both are legal in pre mode. Theos-regen-merge.shsync tookmain's side of the two projections, and the regeneration commit restored this card's entry; the seat reads the generated files on the head as consistent withcheck:generated.Next: the PR stays a draft, marked
needs:contract-review, until a same-head PASS is on record. It lands under the maintainer's temporary rule-A relaxation (6091886885on #22485).
Generated by Claude Code
- One rule, six keys.
objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsSeat order: the contract review PASSED at
ff2832a7bb, but PR #22569 conflicts withmain. Sync throughos-regen-merge.sh(landing step A), then a short second reviewdomain:specseat 3 (#18883) ·zhuangjianguo· sessionsession_01KNKBCRDJCu5tGy3TEbvtrF· 2026-10-10T02:02Z · holder of claim6091691863. Thread-read: 6092376209 (this seat's ACCEPT).- The review: contract review PASS
6092477252on PR fix(spec/automation)!: refuse a$-named outputVariable, and a$-named errorVariable other than$error, at authoring #22569 atff2832a7bb. Nothing in it blocks; its landing note is what this order acts on. - Why the relaxed rule A does not apply: GitHub reports the PR
mergeable: false,mergeable_state: dirty. The relaxation recorded at6091886885on 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 covers only a PR GitHub reports clean. - The conflict, read by the seat: four commits landed on
mainsince the branch's merge base6a3f82efa7(feat(spec)!: atype: 'chart'list view whose effective binding names no dataset is refused at every list-view door #22528, feat(verify): the handle fronts the automation engine's condition evaluator #22553, feat(spec,lint,cli): a screen field's option labels and a flow's terminal toasts have keys in the flows translation face #22555, fix(cli): a refused Console mount answers 503 naming the remedy, not a bare 404 #22562). They touch five of this PR's files. One conflicts as text:packages/spec/src/automation/builtin-node-config.zod.ts, where feat(spec,lint,cli): a screen field's option labels and a flow's terminal toasts have keys in the flows translation face #22555'sflowScreenFieldOptionKeyimport and this PR'sflowBoundVariableNameSchemaimport sit on the same lines. Both imports stay. The other four (registry.ts,spec-changes.json, the upgrade guide,content/docs/references/automation/builtin-node-config.mdx) are generated and are regenerated.
The step, and nothing else:
- A fresh worktree on
claude/issue-22502-dollar-variable-namesatff2832a7bb. Thenbash scripts/pm/os-regen-merge.sh; the script is the authority on its steps. Resolve the one source conflict by keeping both sides' lines; no other hand edit. ⛔ No rebase, no force-push. - Regenerate the chain this branch touches, as its own commit: the migration registry, both projections, the reference pages, and whatever the script's last step prints.
- Assert that both sides survive, with a quoted exact-name
git grepon the merged head:flow-binding-variable-dollar-name-refused(this PR) andview-chart-binding-dataset-required(main, feat(spec)!: atype: 'chart'list view whose effective binding names no dataset is refused at every list-view door #22528), inregistry.ts,spec-changes.jsonand the upgrade guide;flowScreenFieldOptionKeyandflowBoundVariableNameSchemainbuiltin-node-config.zod.ts.
- Green:
check:migration-registry,check:spec-changes,check:upgrade-guide,check:generated, the spec typecheck, and@objectstack/spec'ssrc/automation+src/migrationstests. Run theservice-automationsuite under the lock once. Then the re-derived--commandswith--ran, and the Artifact rosters block. - No other edit. The PR body stays as it is.
Report: the merge commit, the regeneration commit and the new head;
git diff --name-only ff2832a7bb NEW-HEAD; and the changed-line count at the new head. The hand-resolved source file means this is not a pure regeneration hop, so a second contract review runs on the new head before the queue.
Generated by Claude Code
- The review: contract review PASS
objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 22502,
"status": "done",
"round": "seat order 6092496540: sync main through os-regen-merge.sh (landing step A)",
"branch": "claude/issue-22502-dollar-variable-names",
"pr": "#22569",
"session": "session_01KNKBCRDJCu5tGy3TEbvtrF",
"premise_still_valid": true,
"merge_commit": "eedc23f313 (parents ff2832a + d6c3791, main at #22562)",
"step3_commit": "f3a9339aea: main's side of spec-changes.json and the upgrade guide, plus builtin-node-config.mdx regenerated (see deviations)",
"regeneration_commit": "dfe2e07ef3",
"new_head": "dfe2e07ef34ae1a76873479c2910da2a7e296618 (pushed; origin branch equals it)",
"summary": "The worktree was recreated on the branch at ff2832a. os-regen-merge.sh merged origin/main d6c3791, which carries #22528, #22553, #22555 and #22562. The one text conflict was packages/spec/src/automation/builtin-node-config.zod.ts. It was resolved by keeping both import blocks: flowBoundVariableNameSchema from this PR, then flowScreenFieldOptionKey from #22555. The conflict markers were the only thing removed. The merged file differs from origin/main only by this PR's own lines. The script's rerun, step 3, took main's side of the projections. The regeneration chain then ran as its own commit: gen:migration-registry left registry.ts unchanged, and gen:spec-changes and gen:upgrade-guide re-added this card's entry. The staged diff was inspected before that commit: additions only, plus the re-joined step-18 rationale paragraph. No other edit was made. The PR body is unchanged.",
"survival_assertions": [
"git grep -c -F 'flow-binding-variable-dollar-name-refused' at dfe2e07: registry.ts 3, spec-changes.json 2, protocol-upgrade-guide.md 2 (origin/main: 0, 0, 0)",
"git grep -c -F 'view-chart-binding-dataset-required' at dfe2e07: registry.ts 1, spec-changes.json 2, protocol-upgrade-guide.md 1 (equal to origin/main: 1, 2, 1)",
"git grep -c -F 'flowScreenFieldOptionKey' in builtin-node-config.zod.ts at dfe2e07: 5 (origin/main 5)",
"git grep -c -F 'flowBoundVariableNameSchema' in builtin-node-config.zod.ts at dfe2e07: 4 (origin/main 0)"
],
"tests": "All at dfe2e07, one call per suite under os-verify-lock.sh on a shared box. check:migration-registry: registry.ts is current (413 semantic, 247 retired-key, 222 retired-def). check:spec-changes and check:upgrade-guide: generated in memory and equal. check:generated: all 15 generated artifacts are up to date (after a spec build at the merged tree). All four ran in one lock call, VERDICT command-exit 0. spec typecheck: exit 0, check:test-typecheck OK. spec vitest src/automation src/migrations: Test Files 40 passed (40), Tests 1421 passed (1421); main added one file. service-automation whole suite, after its closure build on the merged tree: Test Files 180 passed (180), Tests 2259 passed (2259).",
"gates": "Re-derived dispatch-gates --commands --repo objectstack-ai/objectstack, no paths, at dfe2e07 vs merge base d6c3791: 116 commands, the same set as round 1. 115 ran with exit 0. check:skill-examples first exited 3, PREREQUISITE NOT MET: the fresh worktree had no client-react .d.ts. After building @objectstack/client-react and its dependencies from the turbo cache, it exited 0. check:dual-build-cjs-loads is NOT MEASURED: it needs a whole-workspace build, which the dispatch rules out; declared to CI. --ran reconciliation, exit-annotated: 116 derived, 115 run, 1 NOT-MEASURED, 0 UNRUN, exit 0. The artifact-roster block (49 commands, the same roster) ran with the PR context wired: PR_NUMBER 22569, PR_BODY as stored, PR_HEAD_REF, GITHUB_REPOSITORY and a token. Result 49/49 exit 0, of which 14 are checker-health self-tests only. PR-context guards: check:closing-target-claim green; check:partof-closing-keyword green; check:single-claim-paths green. CI on dfe2e07 was not waited on.",
"diff_name_only_ff2832a7bb_to_new_head": [
".changeset/22301-verify-evaluate-condition-door.md",
".changeset/22420-console-drift-responder.md",
".changeset/22491-chart-list-view-binds-a-dataset.md",
".changeset/22507-flow-options-toasts-translation.md",
"content/docs/references/api/protocol.mdx",
"content/docs/references/automation/builtin-node-config.mdx",
"content/docs/references/data/object.mdx",
"content/docs/references/system/translation.mdx",
"content/docs/references/ui/view.mdx",
"docs/protocol-upgrade-guide.md",
"examples/app-crm/src/translations/crm.translation.ts",
"examples/app-todo/src/translations/ja-JP.ts",
"examples/app-todo/src/translations/zh-CN.ts",
"packages/cli/src/commands/serve.ts",
"packages/cli/src/utils/console-route-ledger.ts",
"packages/cli/src/utils/console.not-built.test.ts",
"packages/cli/src/utils/console.sha-drift.test.ts",
"packages/cli/src/utils/console.ts",
"packages/cli/src/utils/i18n-coverage.ts",
"packages/cli/src/utils/i18n-extract.ts",
"packages/cli/test/i18n-flow-toast-option-coverage.test.ts",
"packages/cli/test/published-subpath-console.pin.test.ts",
"packages/lint/src/validate-translation-references.test.ts",
"packages/lint/src/validate-translation-references.ts",
"packages/qa/dogfood/test/rls-runner.test.ts",
"packages/rest/src/meta-view-chart-binding.test.ts",
"packages/spec/api-surface/automation.json",
"packages/spec/api-surface/system.json",
"packages/spec/api-surface/ui.json",
"packages/spec/export-origins/automation.json",
"packages/spec/export-origins/system.json",
"packages/spec/export-origins/ui.json",
"packages/spec/liveness/state-counts/translation.md",
"packages/spec/liveness/translation.json",
"packages/spec/spec-changes.json",
"packages/spec/src/automation/builtin-node-config.zod.ts",
"packages/spec/src/automation/flow-screen-option-key.test.ts",
"packages/spec/src/automation/flow-screen-option-key.ts",
"packages/spec/src/automation/index.ts",
"packages/spec/src/migrations/entries/semantic/18.view-chart-binding-dataset-required.ts",
"packages/spec/src/migrations/registry.ts",
"packages/spec/src/system/flows-translation-face.test.ts",
"packages/spec/src/system/i18n-resolver.test.ts",
"packages/spec/src/system/i18n-resolver.ts",
"packages/spec/src/system/translation.test.ts",
"packages/spec/src/system/translation.zod.ts",
"packages/spec/src/ui/object-refinement-check-exports.test.ts",
"packages/spec/src/ui/view-chart-binding.test.ts",
"packages/spec/src/ui/view-form-pagination.test.ts",
"packages/spec/src/ui/view.test.ts",
"packages/spec/src/ui/view.zod.ts",
"packages/verify/README.md",
"packages/verify/src/handle.automation-door.test.ts",
"packages/verify/src/handle.ts",
"packages/verify/src/harness.ts"
],
"diff_note": "55 paths, +2907 / -204. Every one of them is a path main changed between 6a3f82e and d6c3791. Five also carry this PR's lines: builtin-node-config.zod.ts, registry.ts, spec-changes.json, the upgrade guide and builtin-node-config.mdx.",
"line_budget": "The PR delta at the new head vs merge base d6c3791 is 17 files, +535 / -29: 564 changed lines, unchanged from ff2832a, under the 3000 human-merge threshold. No skills/** and no governed surface.",
"deviations": [
"Three commits instead of two. The first script run stopped on the source conflict. The resolved merge commit eedc23f then took the hook's deferral for builtin-node-config.mdx. On the rerun, step 3's commit was refused: that deferral was undischarged, because the fresh worktree had no packages/spec/json-schema tree. Following the script's own instruction (clear what the hook reported, then git add -A and commit, before step 4), gen:schema and gen:docs were run with the merge already committed, not in MERGE state. The regenerated page went into the step-3 commit f3a9339, whose message says so. That page is main's text plus this PR's three outputVariable rows, nothing else. The projection regeneration is its own commit, dfe2e07.",
"main moved after the sync, to 25be876 (#22561, #22552, #22570), and #22552 touches registry.ts. The branch was not merged again, because the order is one merge. Two read-only probes: git merge-tree --write-tree HEAD origin/main exits 0 with no conflict (registry.ts is not driver-managed, so the reading holds for it), and GitHub reads mergeable true, mergeable_state blocked.",
"The worktree /home/user/objectstack-issue-22502 was removed again after the checks (node_modules first, no --force); the branch is fully pushed at dfe2e07."
],
"mcp_calls": "0",
"api_writes": "1 this round: this os-dev-report comment, POST /repos//issues/22502/comments, through post-stamped.mjs. git push is not REST. No PR, label or assignee write.",
"open_questions": [],
"out_of_scope_findings": []
}objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsSeat note on report
6093134621: landing step A atdfe2e07ef3is accepted as reported. A second contract review runs on that head before the queue.domain:specseat 3 (#18883) ·zhuangjianguo· sessionsession_01KNKBCRDJCu5tGy3TEbvtrF· 2026-10-10T03:07Z · holder of claim6091691863.- Checked by the seat:
originholds the branch atdfe2e07ef3.- GitHub reads
mergeable: true,mergeable_state: clean. - The check-runs on the head read 33 success and 2 skipped.
- The PR's own delta stays 17 files, +535 / −29.
- Why not a pure regeneration hop:
scripts/pm/record-recognisers.mjs#unexplainedPathsBetweenfromff2832a7bbtodfe2e07ef3namesbuiltin-node-config.zod.ts(the hand-resolved import conflict with feat(spec,lint,cli): a screen field's option labels and a flow's terminal toasts have keys in the flows translation face #22555) andregistry.ts. So the round-1 PASS6092477252does not carry to the new head, and the contract review atCONTRACT_REVIEW_TIERis commissioned again. - The three-commit shape (a step-3 commit carrying the regenerated
builtin-node-config.mdx, then the projection regeneration) follows the script's own instruction for an undischarged deferral. It is accepted as reported. mainmoved after the sync (25be87612d: docs(adr): ADR-0087 D4 and its P2 true-up record ruling B′ — the two projections are generated at publish, not committed (#22449) #22561, feat(service-storage)!: retire the sys_file scope option public and rewrite stored public rows to user (#22443) #22552, fix(plugin-security): explain composes a lifecycle verb's row-level security as the door does #22570). GitHub reads the PR clean, so 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) it goes to the queue without a further sync once the review passes.
Generated by Claude Code
- Checked by the seat:
objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsLanded: PR #22569 →
3e72f9391b(Fixes #22502). The card is closed as completeddomain:specseat 3 (#18883) ·zhuangjianguo· sessionsession_01KNKBCRDJCu5tGy3TEbvtrF· 2026-10-10T03:49Z · holder of claim6091691863.- Landed: through the merge queue at 2026-10-10T03:48Z as
3e72f9391b, a squash with one parent,86f53a4b8d. The queue did not eject it. It went in 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), with no second sync. - The review chain:
- ACCEPT
6092376209; - contract review PASS
6092477252atff2832a7bb; - the sync order
6092496540, its report6093134621, and the seat note6093148207; - contract review PASS
6093227778atdfe2e07ef3, round 2; - the pre-queue record
6093241999.
- ACCEPT
- Content check against the reviewed head
dfe2e07ef3:- 13 of the 17 PR paths on
3e72f9391bare blob-equal to that head. - The other 4 are paths
mainalso moved after the sync based6c37919c7:content/docs/automation/flows.mdx,content/docs/references/automation/builtin-node-config.mdx,builtin-node-config.zod.tsandregistry.ts. Each equals the conflict-free textual three-way merge of86f53a4b8dand the head (git merge-file, exit 0 on all four), so both sides' lines are there. Round 2 had read these hunks as disjoint, and the merge group's required checks judged that tree.
- 13 of the 17 PR paths on
- What now holds:
- a node's
outputVariable(get_record,create_record,map,script,subflow) refuses a$-led name; - a
try_catcherrorVariablerefuses every$name except the engine's own$error, its default; - the refusal names the bare name read as
{{ name }}; - each key publishes the rule as a JSON Schema
pattern; - the ADR-0087 D3 entry is
flow-binding-variable-dollar-name-refused.
- a node's
- The family continues on 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 (pm:blockedon this card), the rest of the binding keys (loop/mapiterators, ascreen'sidVariable, a declared variable'sname, anassignmenttarget). Its unlock is the triage scan's. - Mis-close scan: the merge closed 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 alone.
This act removes
pm:dispatched; the domain, priority, area andtarget:labels stay.
Generated by Claude Code
- Landed: through the merge queue at 2026-10-10T03:48Z as
Blocked-by: #22477
Filing gate: ② a contract gap with a named landing (class b). Escalated by the at-tier contract review of PR #22499 (
6083797903, ③), on #22477. Filed bydomain:specseat 1 (#6017) ·os-tesla· sessionsession_01VZqqwTj2wsihZEbfT6yyYN. ⛔ Not a claim. Triage sets the grade and the lane.What disagrees
{{ $… }}hole whose root the flow engine does not bind #22499 (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): a flow text slot (notifytitle/message,screentitle/description, a refusingendmessage) refuses a{{ }}hole whose root is a$name the engine does not bind. The engine's$names are a closed list:$record,$runId,$flowName,$flowLabel,$error,$loopItems,$loopIndex. The$namespace is reserved for the engine: a resume signal may not write one (IAutomationService.resume'sINVALID_SIGNAL).try_catch'serrorVariable(packages/spec/src/automation/control-flow.zod.ts:329,z.string().default('$error')), and the nodes'outputVariablekeys, take any string, including a$-named one such as'$caught'. The runtime binds it (variables.set(errorVariable, …)).$caughtand then cannot read it in a text slot.'Failed: {{ $caught.message }}'is refused, and the remedy says to drop the$. One contract's two doors disagree about whether an author may own a$name.Direction for triage (the owner decides)
$-namederrorVariableother than the default$error, and a$-namedoutputVariable, at authoring. The remedy: the same name without$, read as{{ name }}. This makes the reserved namespace one rule at every door. It is a narrowing (Clause-②: no (narrowing)), and it owes the contract-tier review and an ADR-0087 disposition.$name stays legal, and have the text-slot judge admit the flow's own bound names. That is a widening; the decision box if wanted.$-namederrorVariable/outputVariable. The PR fix(spec/automation): refuse a text-slot{{ $… }}hole whose root the flow engine does not bind #22499 dev found only test fixtures.Order
After PR #22499 lands; that PR's judge is the reader this card aligns with.
Dedupe: REST listing of the newest 100 issues, titles grepped for errorVariable, outputVariable, try_catch,
$-named, dollar: 0 hits. Dedupe words: try_catch errorVariable dollar name · outputVariable reserved dollar namespace · text slot unbound dollar root