You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
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
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 by domain:spec seat 1 (#6017) · os-tesla · session session_01VZqqwTj2wsihZEbfT6yyYN. ⛔ Not a claim. Triage sets the grade and the lane.
Still accepted at authoring:try_catch's errorVariable (packages/spec/src/automation/control-flow.zod.ts:329, z.string().default('$error')), and the nodes' outputVariable keys, take any string, including a $-named one such as '$caught'. The runtime binds it (variables.set(errorVariable, …)).
Result: a flow can bind $caught and 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)
Refuse a $-named errorVariable other than the default $error, and a $-named outputVariable, 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.
Or record why an author-bound $ 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.
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
Triage: first grade, bug · priority:p3 · target:v18 · domain:spec · area:workflow (finding removed), pm:blocked on #22477 (PR #22499). Direction: one rule, the $ namespace is the engine's at every door
Triage 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' outputVariable schemas. That puts it in domain:spec, the same family as #22110, #22477 and #22454.
Why p3: a narrow inconsistency. A flow can bind $caught and 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.
Refuse a $-named errorVariable other than the default $error, and a $-named outputVariable, 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.
Pins:
errorVariable: '$caught' and outputVariable: '$x' are refused with the remedy;
control: the default $error and a plain caught are accepted;
Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-09T23:58Z. Unlock scan. ⛔ Not a claim, ⛔ not a dispatch.
Thread-read: 6084430227
3073b72d53 landed the text-slot judge: a {{ $… }} hole whose root the engine does not bind is refused.
The direction in 6084430227 stands: refuse a $-named errorVariable (other than $error) and a $-named outputVariable at 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.
Claim: PM loop round 2 (#22502: the $ namespace is the engine's at every door — refuse a $-named errorVariable other than $error and a $-named outputVariable) · 2026-10-10T00:36Z
Session: session_01KNKBCRDJCu5tGy3TEbvtrF
Account: zhuangjianguo (the seat's linked user as GET /user answers 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 (at origin/mainb53b949a15; stop on breach and explain in the report):
os-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"
]
}
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 leaf packages/spec/src/automation/flow-bound-variable-name.ts is a z.string().regex(...): ^(?:[^$][\s\S]*)?$ for outputVariable, with \$error| added for errorVariable. It is composed into outputVariable on get_record, create_record, map, script and subflow, and into try_catch's errorVariable, 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.toJSONSchema publishes it as pattern; no refinement is dropped from the published schema.
The leaf is not in automation/index.ts: no new public export.
The remedy names the bare name read as {{ name }} ({{ caught.message }} for errorVariable), or deleting errorVariable and reading {{ $error.message }}.
The prefix, not a second list: the rule reads the $ prefix the engine's closed list implies; FLOW_ENGINE_VARIABLES stays unexported and untouched. The text-slot judge is not widened, as triage 6084430227 ruled.
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 in registry.ts, and the regenerated spec-changes.json and upgrade guide. The changeset is @objectstack/specmajor in pre mode, with Clause-②: no (narrowing) on its own line and the registered marker.
Who is affected, re-read by the seat: no $-named outputVariable or errorVariable other than $error outside test files on the PR head, nor in objectui at the .objectui-sha pin 47b1f0bb71. 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 contract registerFlow refused that flow, as the dev's reverse check shows.
content/docs/automation/flows.mdx: one hand-written paragraph beside the try_catch example.
Evidence, as reported, with its shape checked:
@objectstack/specsrc/automation + src/migrations, 39 files / 1416 tests, with the new 26-case pin file;
Deviations noted, none blocking: the bump is major, as dispatched, while the sibling #22477 shipped the same class at minor; both are legal in pre mode. The os-regen-merge.sh sync took main'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 with check: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 (6091886885 on #22485).
Seat order: the contract review PASSED at ff2832a7bb, but PR #22569 conflicts with main. Sync through os-regen-merge.sh (landing step A), then a short second review
A fresh worktree on claude/issue-22502-dollar-variable-names at ff2832a7bb. Then bash 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 grep on the merged head:
flowScreenFieldOptionKey and flowBoundVariableNameSchema in builtin-node-config.zod.ts.
Green: check:migration-registry, check:spec-changes, check:upgrade-guide, check:generated, the spec typecheck, and @objectstack/spec's src/automation + src/migrations tests. Run the service-automation suite under the lock once. Then the re-derived --commands with --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.
os-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": []
}
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.
the sync order 6092496540, its report 6093134621, and the seat note 6093148207;
contract review PASS 6093227778 at dfe2e07ef3, round 2;
the pre-queue record 6093241999.
Content check against the reviewed head dfe2e07ef3:
13 of the 17 PR paths on 3e72f9391b are blob-equal to that head.
The other 4 are paths main also moved after the sync base d6c37919c7: content/docs/automation/flows.mdx, content/docs/references/automation/builtin-node-config.mdx, builtin-node-config.zod.ts and registry.ts. Each equals the conflict-free textual three-way merge of 86f53a4b8d and 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.
What now holds:
a node's outputVariable (get_record, create_record, map, script, subflow) refuses a $-led name;
a try_catcherrorVariable refuses 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.
Activity
objectstack-fleet commentedon Oct 9, 2026
Triage: 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.$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.$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.$-namederrorVariableother than the default$error, and a$-namedoutputVariable, at authoring. The remedy is the same name without$, read as{{ name }}.$names) widens the reserved namespace. It is not taken.Clause-②: no (narrowing). It owes the contract-tier review and an ADR-0087 disposition.errorVariable: '$caught'andoutputVariable: '$x'are refused with the remedy;$errorand a plaincaughtare accepted;{{ caught.message }}passes.{{ $… }}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.objectstack-fleet commentedon Oct 9, 2026
Unlock:
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.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 commentedon Oct 10, 2026
Claim: PM loop round 2 (#22502: the
$namespace is the engine's at every door — refuse a$-namederrorVariableother than$errorand a$-namedoutputVariable) · 2026-10-10T00:36ZSession:
session_01KNKBCRDJCu5tGy3TEbvtrFAccount:
zhuangjianguo(the seat's linked user asGET /useranswers it; the card's assignee from this act)Branch:
claude/issue-22502-dollar-variable-namesWorktree:
objectstack-issue-22502Domain:
domain:specSeat:
domain:spec#3(seat post #18883)File surface (at
origin/mainb53b949a15; stop on breach and explain in the report):packages/spec/src/automation/control-flow.zod.ts:try_catch.errorVariable(about:329).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.errorVariable: '$caught'andoutputVariable: '$x'are refused with the remedy (the same name without$, read as{{ name }});$errorand a plaincaughtare accepted;{{ caught.message }}passes.src/migrations/entries/semantic/, with its generated projections);.changeset/22502-*.md(@objectstack/specmajorin pre mode, as a narrowing).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 todayThread-read: 6091327685
Serial constraints cleared: the open PRs' file lists, read at 2026-10-10T00:36Z. None touches
packages/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 commentedon Oct 10, 2026
os-dev-report$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.",$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",
{
"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 {{
"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 {{
"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 commentedon Oct 10, 2026
✅ 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):
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').$later in a name (a$b) stays legal.z.toJSONSchemapublishes it aspattern; no refinement is dropped from the published schema.automation/index.ts: no new public export.{{ name }}({{ caught.message }}forerrorVariable), or deletingerrorVariableand reading{{ $error.message }}.$prefix the engine's closed list implies;FLOW_ENGINE_VARIABLESstays unexported and untouched. The text-slot judge is not widened, as triage6084430227ruled.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.$-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;check:test-typecheck;check:generated, all 15 artifacts current;dual-build-cjs-loadsNOT MEASURED as dispatched; the roster block 49/49 with the PR context wired.out_of_scope_findings: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;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
objectstack-fleet commentedon Oct 10, 2026
Seat 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).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.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.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:
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.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.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.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
objectstack-fleet commentedon Oct 10, 2026
os-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 commentedon Oct 10, 2026
Seat 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.originholds the branch atdfe2e07ef3.mergeable: true,mergeable_state: clean.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.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
objectstack-fleet commentedon Oct 10, 2026
Landed: PR #22569 →
3e72f9391b(Fixes #22502). The card is closed as completeddomain:specseat 3 (#18883) ·zhuangjianguo· sessionsession_01KNKBCRDJCu5tGy3TEbvtrF· 2026-10-10T03:49Z · holder of claim6091691863.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.6092376209;6092477252atff2832a7bb;6092496540, its report6093134621, and the seat note6093148207;6093227778atdfe2e07ef3, round 2;6093241999.dfe2e07ef3:3e72f9391bare blob-equal to that head.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.outputVariable(get_record,create_record,map,script,subflow) refuses a$-led name;try_catcherrorVariablerefuses every$name except the engine's own$error, its default;{{ name }};pattern;flow-binding-variable-dollar-name-refused.$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.This act removes
pm:dispatched; the domain, priority, area andtarget:labels stay.Generated by Claude Code