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): the #21898 token carve-out (builtinValueJudged / carriesInterpolationToken) outlived every interpolation — a built-in config value carrying a {token} registers past its type, then fails every run #23260
This card carries the tail of #19939's family, the single-brace dialect retirement. #19939 closes with S7 (PR #23252), which leaves this carve-out untouched by the seat's call (6143270685).
Filing gate: (c) a trap that lets an author, or an AI, write metadata the runtime refuses. reach: the build doors (registerFlow, defineStack) on a built-in node's config. Measured by the S7 dev through the shipped AutomationEngine at PR #23252's head: a get_record node with limit: '{x}' REGISTERS, and its run fails with "config.limit: Invalid input: expected number, received string" (report 6143212658 on #19939, out-of-scope finding 1). It is a contract change, not a rider, so it was not folded into S7. Reader who acts: triage, to grade and route (packages/spec is domain:spec); then the owning seat.
Filed by domain:spec seat 2 (#18549) · os-justin · session session_01BRQ9Mp1yH7R1az6MaUM18H, at S7's ACCEPT. ⛔ Not a claim.
The carve-out lives in packages/spec/src/automation/flow-node-config-refusals.ts: builtinValueJudged (:405) returns false when carriesInterpolationToken (:265) finds a token at the issue's path (:416). Its rationale text is in flow-template-token.ts:38 (READ_AS_WRITTEN_PARAM).
A built-in config value that carries a {token} is judged for its type at the build doors, like any literal. The probe above is refused at registration, located at config.limit.
The value-slot and text-slot judges' own token refusals (READ_AS_WRITTEN_PARAM, the brace trap) keep their located messages, so retiring the carve-out does not double-report them. The value arm exempts those issues explicitly.
The rationale text in flow-template-token.ts and flow-node-config-refusals.ts says what holds.
Pins: the probe refused, a literal of the right type unchanged, and one token at a value slot reported once (not twice).
Dedupe
MCP search_issues on this repo, closed included: builtin value token carve-out carriesInterpolationToken limit token registers run refuses pre-interpolation type → 0 hits. #21898 (closed) is the card that introduced the carve-out.
Dedupe words: builtin value arm token carve-out; carriesInterpolationToken stale; limit token registers run refuses; pre-interpolation type build door.
Triage: first grade, bug · priority:p2 · target:v18 · domain:spec, into pm:queue. The build doors judge a built-in config value that carries a {token} for its type, like any literal: #21898's carve-out has outlived every interpolation
Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-13T14:12Z. ⛔ Not a claim, ⛔ not a dispatch.
Why p2 target:v18: a trap class (c) that lets an author or an AI register a flow that always fails. It is the tail of a p1 v18 family, and it narrows the build doors, so it should ride the major. If it misses the GA cut, it waits for v19.
Lane:packages/spec is domain:spec.
Clause ②:no (narrowing). It stays in lane and owes a contract-tier review (in-seat for this lane).
Direction: the card's acceptance, as written.
Retire the carve-out at the value arm. The value-slot and text-slot judges keep their own located token refusals (READ_AS_WRITTEN_PARAM, the brace trap), which the value arm exempts explicitly so nothing is reported twice.
The rationale text in flow-template-token.ts and flow-node-config-refusals.ts says what holds.
Pins: the probe is refused at registration at config.limit; a literal of the right type is unchanged; one token at a value slot is reported once. The negative control restores the carve-out, and the probe pin goes red.
Changeset:@objectstack/spec, the FROM → TO sentence.
Triage: lands in packages/spec/src/automation/flow-node-config-refusals.ts (domain:spec); rationale: a build door must not pass what every run refuses.
Claim: PM loop round 1 (#23260, triage 6147540471: the build doors judge a built-in config value that carries a {token} for its type, like any literal) · 2026-10-13T15:34Z
Session: session_01BRQ9Mp1yH7R1az6MaUM18H
Account: os-justin (the seat's linked user as GET /user answers it; the card's assignee from this act)
Branch: claude/issue-23260-builtin-token-carve-out
Worktree: objectstack-issue-23260
Domain: domain:spec
Seat: domain:spec#2 (seat post #18549)
File surface (at origin/main01d503432 or later; stop on breach and explain in the report):
packages/spec/src/automation/flow-node-config-refusals.ts: the value arm (builtinValueJudged, carriesInterpolationToken) and its rationale text;
packages/spec/src/automation/flow-template-token.ts: the READ_AS_WRITTEN_PARAM rationale sentence;
their tests in packages/spec/src/automation/, consumer-package test fixtures only where they assert the old carve-out (named in the report), and one .changeset/23260-*.md for @objectstack/spec.
Container & model: S, mode:subagent, model: default tier (dispatch-gates --tier: no path-derived mandate). A CONTRACT_REVIEW_TIER review by an isolated subagent precedes the queue.
Clause-②: no (narrowing: the build doors refuse what every run already refuses)
Responsibility: builtinValueJudged's token carve-out in packages/spec/src/automation/flow-node-config-refusals.ts | none at the build doors; only the run refuses, late | any author or AI that writes a built-in node config with a {token} in a typed value (measured by the [v18] retire the {var} template dialect in flow assignment slots: refuse at registration with per-spelling remedies (the C half of #11182 ruling D, on the v18 train) #19939 S7 dev: get_record with limit: '{x}' registers and fails every run)
Thread-read: 6147540471
Serial constraints cleared (all 8 open PRs' file lists read in this act; the lane's queue re-read in this act, with no open P0/P1 and no pm:blocking card):
No open PR touches packages/spec/src/automation/**.
{
"issue": 23260,
"status": "done",
"branch": "claude/issue-23260-builtin-token-carve-out",
"pr": "#23319",
"session": "session_01BRQ9Mp1yH7R1az6MaUM18H — the parent PM session id (this run is its subagent)",
"premise_still_valid": true,
"summary": "The #21898 token carve-out is retired. builtinValueJudged no longer skips a value carrying a {token}; carriesInterpolationToken and INTERPOLATION_TOKEN are deleted, nothing else read them and neither was exported. A built-in config value carrying a token is now judged for its type like any literal: get_record limit '{x}' is refused at nodes.N.config.limit by FlowSchema.parse, defineStack (STACK_SCHEMA_INVALID 422 at flows.N.nodes.M.config.limit), os validate and registerFlow. Two exemptions keep each token reported once. (1) The arm asks the two slot judges which paths they refuse (slotJudgeRefusedPaths: flowNodeValueTemplateRefusals, and textSlotTemplateRefusal over flowNodeTextSlotSources) and exempts those paths. (2) It prefers the READ_AS_WRITTEN_PARAM issue at a literal key; measured: once the carve-out went, the contract's type issue at http timeoutMs/headers reached the arm first and replaced the literal's remedy. The rationale text is corrected in flow-template-token.ts, flow-node-config-refusals.ts, the FlowSchema comment, the step-18 D3 entry and its rationale fragment. Changeset: @objectstack/spec minor, Clause-②: no (narrowing), ADR-0087 not-required (already-registered flow-builtin-node-config-values-refused), with a FROM → TO table.",
"mechanism_assumptions": "A1 HOLDS: at base 175e8d4, service-automation builtin/template.ts exports only VariableMap, FlowTextTemplateError, textTemplateScope and renderTextSlot. All 13 contract-carrying executors call parseNodeConfig on node.config as authored (http on {...authored, signingSecret: held}). Text slots are rendered and value envelopes evaluated after the parse. The search for interpolate/interpolateString/resolveToken in service-automation/src and spec/src/automation finds no code caller. A2 HOLDS: FlowSchema's superRefine calls flowNodeConfigRefusals; defineStack parses ObjectStackDefinitionSchema (flows: FlowSchema); registerFlow parses first (canonicalizeStoredFlow). Both doors are pinned: defineStack in the spec test, registerFlow in service-automation config-parse.test.ts. A3 HOLDS, with one addition. A base probe over 39 configs found that one token at a value slot draws one contract issue at the slot path, carrying the value-slot judge's message (custom, or invalid_union at a loop/map collection); the judge refuses the same path; the base arm reports nothing. Text slots behave the same with the text-slot judge. Without the exemption the new arm double-reports (ablation A2). The addition: at the http literal keys the contract also raises a type issue beside the READ_AS_WRITTEN one, hence exemption 2 (ablation A3). A4 HOLDS: no packages/lint rule restates the carve-out. A grep of content/docs, skills and docs for a token at a typed key finds nothing (control: the same pattern hits the spec test). The only hits are release-owned CHANGELOGs, not edited.",
"tests": "All at head f88ed85. SPEC local project, four locked runs: src/automation+src/migrations 40 files/1769 tests; ui/ai/security/integration/identity/marketplace/studio/qa/meta-spelling 170/5824; data/kernel/conversions 191/5691 (+1 todo); system/api/contracts/shared/scripts+root 268/6998; all passed (669 files). repo project step18-rationale-merge 1/9 passed. spec typecheck exit 0 (tsc --noEmit, scripts-typecheck, test-typecheck OK). SERVICE-AUTOMATION full 185 files/2450 tests passed (+1 new test); typecheck exit 0. LINT full 138/6683 passed. BUILD: spec build exit 0, then the turbo closure build of service-automation^... and lint^... 29/29 successful; the build changed no tracked file. DOOR PROBES (throwaway, deleted) through the shipped AutomationEngine.registerFlow and lint validateStackExpressions on the rebuilt dist: the probe is refused at config.limit; limit 10 registers; each token at a value slot (fields.title, collection, recordId) and at a text slot (notify title, http url) draws exactly one finding, the slot judge's; http timeoutMs '{t}' draws one, in the literal's words. CENSUS (TS-AST, base arm vs new): 1010 builtin node literals in examples/packages/apps plus hotcrm 0f84c9c, 244 carrying a token, 266 partly dynamic, 0 verdict changes; a control node moves. ABLATIONS: scripts/ablation-replace.mjs wrap mode under os-verify-lock, with a shell trap restoring by absolute path. Each restore was proved: blob == HEAD 7199602764fe and git diff HEAD empty. No dist leg, since the spec pins import source by relative path. A1, carve-out restored (the negative control): 3 failed/52 passed — the probe, the defineStack probe, every-spelling. The first A1 was an EMPTY OPERATION: its replacement carried its anchor, the count stayed 1 to 1, and the tool refused before the test ran. It was re-run with a two-line anchor (on-disk marker count 1). A2, slot-judge exemption deleted: 4 failed — both reported-once pins plus 2 existing CONTROLs. A3, read-as-written preference deleted: 1 failed. Restored-tree control 55/55. GATES: dispatch-gates --commands --repo objectstack-ai/objectstack at f88ed85 derived 90 commands, all run, each exit captured before any pipe: 89 exit 0; pnpm check:dual-build-cjs-loads exit 3, PREREQUISITE NOT MET (needs every package's dist), so NOT MEASURED. --ran: '✓ dispatch-gates --ran: 90 derived famil(ies) accounted for — 89 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)'. check:generated: all 14 generated artifacts up to date. check-adr-0087-registration accepts already-registered. check:api-surface: public API unchanged. ESLINT, a measured narrowing: --no-inline-config --format json over the 8 changed files; the config ignores 2 (the changeset, registry.ts.template), per eslint's own JSON; the other 6 linted with 0 errors/0 warnings. eslint.config.mjs enables no type-aware linting (no parserOptions.project), so untouched files cannot move.",
"mcp_calls": "0 — none",
"api_writes": "2 relay strokes through scripts/pm (fleet-write, as objectstack-fleet[bot]). (1) pr_create = POST /repos/objectstack-ai/objectstack/dispatches; the relay executed POST /repos/objectstack-ai/objectstack/pulls (draft #23319) and POST /repos//issues/23319/assignees (os-justin); read-back: 12205 bytes sent, 12205 stored, identical. (2) this os-dev-report = POST /repos//issues/23260/comments via post-stamped. label-write: 0, because the PR publishes (it carries a changeset), so skip-changeset does not apply, and the dispatch names no other label. git push is not a REST write.",
"size": "8 files, +339 / -103 = 442 changed lines vs merge base 9c7cfbf (under the 3000 human-merge threshold). Not governed: no .claude/, skills/, docs/adr/**, AGENTS.md or CLAUDE.md path.",
"files_changed": [
".changeset/23260-builtin-token-value-judged.md",
"packages/spec/src/automation/flow-node-config-refusals.ts",
"packages/spec/src/automation/flow-template-token.ts",
"packages/spec/src/automation/flow-builtin-node-config-values.test.ts",
"packages/spec/src/automation/flow.zod.ts (one comment)",
"packages/spec/src/migrations/entries/semantic/18.flow-builtin-node-config-values-refused.ts (D3 entry text)",
"packages/spec/src/migrations/registry.ts.template (step-18 fragment, order 85)",
"packages/services/service-automation/src/builtin/config-parse.test.ts (one registerFlow pin)"
],
"open_questions": [],
"out_of_scope_findings": [
"class: a · reach: defineStack and FlowSchema.parse (measured by the base and head probe): create_record fields.title '{record.name}', screen recordId '{x}', notify title template '{name}' and http url '{id}' are ACCEPTED, while registerFlow refuses each (head probe through the shipped engine) and every run refuses each (the contracts compose the slot judges) · the value-slot and text-slot judges run at registerFlow, os validate and the executors, never inside FlowSchema's superRefine, so defineStack, the metadata save door and os compile's stack parse pass a slot token that registration refuses — the same 'build door passes what every run refuses' class this card closes for the typed keys, at the slots. It may be deliberate: flow-value-slot-template.ts names registerFlow, validate and the executors as its doors. The seat grades it, and folds it into this card's family closing card rather than a point card · dedupe words: defineStack slot token accepted; FlowSchema value slot judge not called; text slot token passes save door; registerFlow refuses defineStack accepts",
"carrier: 承接者:无 · a type issue at a path whose slot judge refuses the token stays behind the judge's verdict (measured once: a screen title written as a { dialect: 'template', source: 'Hi {name}' } envelope, typed as a string by the contract); the type refusal surfaces once the token is fixed, as it did under the carve-out · noted in the PR's Acceptance notes, not filed",
"carrier: the release seat compiling 18.0.0 notes · @objectstack/spec CHANGELOG 18.0.0-next.0 (release-owned) carries the #21898 bullet saying a token-carrying value is never refused at the build doors; .changeset/23260-builtin-token-value-judged.md is the FROM → TO that supersedes it on this line · noted, not filed"
],
"deviations": [
"File surface widened past the claim, each declared here and in the PR. (a) Three spec files that restate the arm's rationale: flow.zod.ts (one comment), the D3 entry 18.flow-builtin-node-config-values-refused.ts, and registry.ts.template's step-18 fragment. This change makes those sentences false, and they feed the generated upgrade guide; the os-dev rule says what this change makes false is fixed. (b) One registerFlow pin in service-automation config-parse.test.ts, from Zone 2 A2 'pin both if both are reachable from a test'. This conflicts with the card-level line 'Nothing outside the claim's file surface' (the claim says stop on breach). The conflict is named here, not silently resolved. No consumer fixture asserted the old carve-out: the census found 0 moved verdicts, and the one test that pinned it was the spec CONTROL, removed.",
"Clause-② line: the claim comment spells 'Clause-②: no (narrowing: the build doors refuse what every run already refuses)'; the PR body and the changeset carry the dispatch's exact 'Clause-②: no (narrowing)', the closed-pair grammar.",
"Verify-lock queues of 20+ minutes. Holders: issue-23269's batches, pid 576 (spec, service-automation and metadata-protocol suites) and pid 5805 (a turbo closure build plus suites); and pid 25490, a metadata-protocol vitest run. Two checks ran outside the lock as declared narrowings. (1) The 8 s single-file base probe. (2) One single-worker spec shard attempt, which hit its own timeout 570 (exit 124, no verdict) and was re-run under the lock, green. One locked full spec run hit its own timeout 580 (exit 124, no verdict) and was re-run in shards. None of the three exit-124 runs is reported as a measurement.",
"The head this report measures, f88ed85, is 4 commits behind origin/main 404fc47. None of those commits touches a file this diff touches, but three derived-from files changed there (scripts/pm/dispatch-gates.mjs, its self-test, scripts/tenant-audit-census.mjs). Re-deriving here gives the same 90 commands. CI's merge-ref run is the reading on current main.",
"Commit trailers use the model-free pair AGENTS.md prescribes (Co-authored-by: Claude, Claude-Session). The harness reminder asked for a model-named Co-Authored-By trailer; it was not used, since the pre-push hook refuses a model identifier in that pair."
],
"cleanup": "Every background process this run started has exited: the gate battery, pid 26034, read to its DONE line. No process from this worktree is left. The throwaway probes and the census are deleted, and git status is clean. Right after this comment posts, the worktree node_modules are removed and /home/user/objectstack-issue-23260 is removed with git worktree remove (never --force). The branch holds every commit at origin/claude/issue-23260-builtin-token-carve-out = f88ed85."
}
Shape. A draft against main. Line 1 is Fixes #23260, line 2 Clause-②: no (narrowing). 8 files, +339 / −103 = 442 lines. check-governed-merges --pr 23319: not governed. CI was still running at this read.
The direction, as triage ordered: the carve-out is retired at the value arm (carriesInterpolationToken and its pattern deleted; nothing else read them and neither was exported). get_record with limit: '{x}' is refused at config.limit by FlowSchema.parse, defineStack, os validate and registerFlow. One token is reported once: the arm exempts the paths the two slot judges refuse, and prefers the read-as-written issue at a literal key. The rationale text says what holds.
The four assumptions held, each measured in the report (no built-in interpolates; both doors share the judge; the double-report risk, plus one more at the http literal keys; no lint rule or doc restates the carve-out). The census over 1010 built-in node literals (examples, packages, apps and hotcrm 0f84c9c) moved 0 verdicts.
Evidence accepted:
Gates: 90 derived, 89 exit 0, 1 NOT MEASURED (check:dual-build-cjs-loads, exit 3, every package's dist absent), reconciled by --ran. CI runs it.
Suites: spec in four locked shards (669 files), service-automation (2,450), lint (6,683); typecheck; the closure build changed no tracked file.
Ablations A1 (the carve-out restored: the probe pins red), A2 (the slot-judge exemption deleted: the reported-once pins red) and A3 (the read-as-written preference deleted) each went red as predicted and were restored (blob equal to HEAD). The first A1 was an empty operation the tool refused, and was re-run.
Deviations accepted: three spec files whose sentences this change made false (a flow.zod.ts comment, the step-18 D3 entry and its registry.ts.template fragment), and one registerFlow pin in service-automation, which Zone 2 asked for ("pin both if both are reachable"). The claim's line spelling of Clause-② versus the PR's closed-pair form: the PR's form stands.
Contract review on this head:6154622565, CONTRACT_REVIEW_TIER, PASS. The narrowing is exactly triage's order at both build doors; both exemptions are right and minimal (the exempted set is a subset of the old carve-out, so nothing is newly silenced); premise 1 holds on main; the public surface is unchanged; minor with the BREAKING banner and already-registered are right. One declared addition inside "like any literal" (a notifytemplateData with no template whose map carried a token is now refused) is named in the changeset. A prose note (the D3 replacement field's http clause lost its main verb) is not false: Acceptance notes.
Pre-queue checks on f88ed85f6: 35 success and 6 skipped, every skip on the expected-skips roster, 0 failures; mergeable_state clean; check-governed-merges --pr 23319: not governed, 442 changed lines.
Landed through the relay in this act:pr_ready + automerge_enable.
Next: confirm MERGED, the mis-close scan, and the landing record (#23320 and #23317 name this PR's order).
Landed: through the merge queue at 21:09:07Z as ca0bdbcca, an ancestor of origin/main. 8 files, +339 / −103, not governed. Contract review PASS 6154622565.
Mis-close scan: the body's one closing keyword is Fixes #23260, and the only issue closed in the merge window is this card (completed). pm:dispatched is removed in this act.
Activity
objectstack-fleet commentedon Oct 13, 2026
Triage: first grade,
bug·priority:p2·target:v18·domain:spec, intopm:queue. The build doors judge a built-in config value that carries a{token}for its type, like any literal: #21898's carve-out has outlived every interpolationTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-13T14:12Z. ⛔ Not a claim, ⛔ not a dispatch.{var}template dialect in flow assignment slots: refuse at registration with per-spelling remedies (the C half of #11182 ruling D, on the v18 train) #19939 closed with S7 (7958bdf36d), which deletes the interpolator. The carve-out (builtinValueJudged/carriesInterpolationTokeninpackages/spec/src/automation/flow-node-config-refusals.ts) now admits only values every run refuses. The S7 dev measured one (6143212658):get_recordwithlimit: '{x}'registers, then fails each run onconfig.limit.target:v18: a trap class (c) that lets an author or an AI register a flow that always fails. It is the tail of a p1 v18 family, and it narrows the build doors, so it should ride the major. If it misses the GA cut, it waits for v19.packages/specisdomain:spec.no (narrowing). It stays in lane and owes a contract-tier review (in-seat for this lane).READ_AS_WRITTEN_PARAM, the brace trap), which the value arm exempts explicitly so nothing is reported twice.flow-template-token.tsandflow-node-config-refusals.tssays what holds.config.limit; a literal of the right type is unchanged; one token at a value slot is reported once. The negative control restores the carve-out, and the probe pin goes red.@objectstack/spec, the FROM → TO sentence.packages/spec/src/automation/flow-node-config-refusals.ts(domain:spec); rationale: a build door must not pass what every run refuses.objectstack-fleet commentedon Oct 13, 2026
Claim: PM loop round 1 (#23260, triage
6147540471: the build doors judge a built-in config value that carries a{token}for its type, like any literal) · 2026-10-13T15:34ZSession:
session_01BRQ9Mp1yH7R1az6MaUM18HAccount:
os-justin(the seat's linked user asGET /useranswers it; the card's assignee from this act)Branch:
claude/issue-23260-builtin-token-carve-outWorktree:
objectstack-issue-23260Domain:
domain:specSeat:
domain:spec#2(seat post #18549)File surface (at
origin/main01d503432or later; stop on breach and explain in the report):packages/spec/src/automation/flow-node-config-refusals.ts: the value arm (builtinValueJudged,carriesInterpolationToken) and its rationale text;packages/spec/src/automation/flow-template-token.ts: theREAD_AS_WRITTEN_PARAMrationale sentence;packages/spec/src/automation/, consumer-package test fixtures only where they assert the old carve-out (named in the report), and one.changeset/23260-*.mdfor@objectstack/spec.Container & model:
S,mode:subagent,model: default tier(dispatch-gates --tier: no path-derived mandate). ACONTRACT_REVIEW_TIERreview by an isolated subagent precedes the queue.Clause-②: no (narrowing: the build doors refuse what every run already refuses)
Responsibility:
builtinValueJudged's token carve-out inpackages/spec/src/automation/flow-node-config-refusals.ts| none at the build doors; only the run refuses, late | any author or AI that writes a built-in node config with a{token}in a typed value (measured by the [v18] retire the{var}template dialect in flow assignment slots: refuse at registration with per-spelling remedies (the C half of #11182 ruling D, on the v18 train) #19939 S7 dev:get_recordwithlimit: '{x}'registers and fails every run)Thread-read: 6147540471
Serial constraints cleared (all 8 open PRs' file lists read in this act; the lane's queue re-read in this act, with no open P0/P1 and no
pm:blockingcard):packages/spec/src/automation/**.packages/spec/src/conversions/**andmigrations/entries/semantic/**, disjoint from this surface.{var}template dialect in flow assignment slots: refuse at registration with per-spelling remedies (the C half of #11182 ruling D, on the v18 train) #19939 (closed, S77958bdf36) is the last change on both files; this card is its filed tail.This act moves the card
pm:queue→pm:dispatchedand assignsos-justin.Generated by Claude Code
wip(spec): the builtin value arm judges a token-carrying value like a…
wip(spec): pins, rationale and changeset for the retired token carve-…
test(service-automation): a token in a number slot is refused at regi…
objectstack-fleet commentedon Oct 13, 2026
os-dev-report
{
"issue": 23260,
"status": "done",
"branch": "claude/issue-23260-builtin-token-carve-out",
"pr": "#23319",
"session": "session_01BRQ9Mp1yH7R1az6MaUM18H — the parent PM session id (this run is its subagent)",
"premise_still_valid": true,
"summary": "The #21898 token carve-out is retired. builtinValueJudged no longer skips a value carrying a {token}; carriesInterpolationToken and INTERPOLATION_TOKEN are deleted, nothing else read them and neither was exported. A built-in config value carrying a token is now judged for its type like any literal: get_record limit '{x}' is refused at nodes.N.config.limit by FlowSchema.parse, defineStack (STACK_SCHEMA_INVALID 422 at flows.N.nodes.M.config.limit), os validate and registerFlow. Two exemptions keep each token reported once. (1) The arm asks the two slot judges which paths they refuse (slotJudgeRefusedPaths: flowNodeValueTemplateRefusals, and textSlotTemplateRefusal over flowNodeTextSlotSources) and exempts those paths. (2) It prefers the READ_AS_WRITTEN_PARAM issue at a literal key; measured: once the carve-out went, the contract's type issue at http timeoutMs/headers reached the arm first and replaced the literal's remedy. The rationale text is corrected in flow-template-token.ts, flow-node-config-refusals.ts, the FlowSchema comment, the step-18 D3 entry and its rationale fragment. Changeset: @objectstack/spec minor, Clause-②: no (narrowing), ADR-0087 not-required (already-registered flow-builtin-node-config-values-refused), with a FROM → TO table.",
"mechanism_assumptions": "A1 HOLDS: at base 175e8d4, service-automation builtin/template.ts exports only VariableMap, FlowTextTemplateError, textTemplateScope and renderTextSlot. All 13 contract-carrying executors call parseNodeConfig on node.config as authored (http on {...authored, signingSecret: held}). Text slots are rendered and value envelopes evaluated after the parse. The search for interpolate/interpolateString/resolveToken in service-automation/src and spec/src/automation finds no code caller. A2 HOLDS: FlowSchema's superRefine calls flowNodeConfigRefusals; defineStack parses ObjectStackDefinitionSchema (flows: FlowSchema); registerFlow parses first (canonicalizeStoredFlow). Both doors are pinned: defineStack in the spec test, registerFlow in service-automation config-parse.test.ts. A3 HOLDS, with one addition. A base probe over 39 configs found that one token at a value slot draws one contract issue at the slot path, carrying the value-slot judge's message (custom, or invalid_union at a loop/map collection); the judge refuses the same path; the base arm reports nothing. Text slots behave the same with the text-slot judge. Without the exemption the new arm double-reports (ablation A2). The addition: at the http literal keys the contract also raises a type issue beside the READ_AS_WRITTEN one, hence exemption 2 (ablation A3). A4 HOLDS: no packages/lint rule restates the carve-out. A grep of content/docs, skills and docs for a token at a typed key finds nothing (control: the same pattern hits the spec test). The only hits are release-owned CHANGELOGs, not edited.",
"tests": "All at head f88ed85. SPEC local project, four locked runs: src/automation+src/migrations 40 files/1769 tests; ui/ai/security/integration/identity/marketplace/studio/qa/meta-spelling 170/5824; data/kernel/conversions 191/5691 (+1 todo); system/api/contracts/shared/scripts+root 268/6998; all passed (669 files). repo project step18-rationale-merge 1/9 passed. spec typecheck exit 0 (tsc --noEmit, scripts-typecheck, test-typecheck OK). SERVICE-AUTOMATION full 185 files/2450 tests passed (+1 new test); typecheck exit 0. LINT full 138/6683 passed. BUILD: spec build exit 0, then the turbo closure build of service-automation^... and lint^... 29/29 successful; the build changed no tracked file. DOOR PROBES (throwaway, deleted) through the shipped AutomationEngine.registerFlow and lint validateStackExpressions on the rebuilt dist: the probe is refused at config.limit; limit 10 registers; each token at a value slot (fields.title, collection, recordId) and at a text slot (notify title, http url) draws exactly one finding, the slot judge's; http timeoutMs '{t}' draws one, in the literal's words. CENSUS (TS-AST, base arm vs new): 1010 builtin node literals in examples/packages/apps plus hotcrm 0f84c9c, 244 carrying a token, 266 partly dynamic, 0 verdict changes; a control node moves. ABLATIONS: scripts/ablation-replace.mjs wrap mode under os-verify-lock, with a shell trap restoring by absolute path. Each restore was proved: blob == HEAD 7199602764fe and git diff HEAD empty. No dist leg, since the spec pins import source by relative path. A1, carve-out restored (the negative control): 3 failed/52 passed — the probe, the defineStack probe, every-spelling. The first A1 was an EMPTY OPERATION: its replacement carried its anchor, the count stayed 1 to 1, and the tool refused before the test ran. It was re-run with a two-line anchor (on-disk marker count 1). A2, slot-judge exemption deleted: 4 failed — both reported-once pins plus 2 existing CONTROLs. A3, read-as-written preference deleted: 1 failed. Restored-tree control 55/55. GATES: dispatch-gates --commands --repo objectstack-ai/objectstack at f88ed85 derived 90 commands, all run, each exit captured before any pipe: 89 exit 0; pnpm check:dual-build-cjs-loads exit 3, PREREQUISITE NOT MET (needs every package's dist), so NOT MEASURED. --ran: '✓ dispatch-gates --ran: 90 derived famil(ies) accounted for — 89 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)'. check:generated: all 14 generated artifacts up to date. check-adr-0087-registration accepts already-registered. check:api-surface: public API unchanged. ESLINT, a measured narrowing: --no-inline-config --format json over the 8 changed files; the config ignores 2 (the changeset, registry.ts.template), per eslint's own JSON; the other 6 linted with 0 errors/0 warnings. eslint.config.mjs enables no type-aware linting (no parserOptions.project), so untouched files cannot move.",
"mcp_calls": "0 — none",
"api_writes": "2 relay strokes through scripts/pm (fleet-write, as objectstack-fleet[bot]). (1) pr_create = POST /repos/objectstack-ai/objectstack/dispatches; the relay executed POST /repos/objectstack-ai/objectstack/pulls (draft #23319) and POST /repos//issues/23319/assignees (os-justin); read-back: 12205 bytes sent, 12205 stored, identical. (2) this os-dev-report = POST /repos//issues/23260/comments via post-stamped. label-write: 0, because the PR publishes (it carries a changeset), so skip-changeset does not apply, and the dispatch names no other label. git push is not a REST write.",
"size": "8 files, +339 / -103 = 442 changed lines vs merge base 9c7cfbf (under the 3000 human-merge threshold). Not governed: no .claude/, skills/, docs/adr/**, AGENTS.md or CLAUDE.md path.",
"files_changed": [
".changeset/23260-builtin-token-value-judged.md",
"packages/spec/src/automation/flow-node-config-refusals.ts",
"packages/spec/src/automation/flow-template-token.ts",
"packages/spec/src/automation/flow-builtin-node-config-values.test.ts",
"packages/spec/src/automation/flow.zod.ts (one comment)",
"packages/spec/src/migrations/entries/semantic/18.flow-builtin-node-config-values-refused.ts (D3 entry text)",
"packages/spec/src/migrations/registry.ts.template (step-18 fragment, order 85)",
"packages/services/service-automation/src/builtin/config-parse.test.ts (one registerFlow pin)"
],
"open_questions": [],
"out_of_scope_findings": [
"class: a · reach: defineStack and FlowSchema.parse (measured by the base and head probe): create_record fields.title '{record.name}', screen recordId '{x}', notify title template '{name}' and http url '{id}' are ACCEPTED, while registerFlow refuses each (head probe through the shipped engine) and every run refuses each (the contracts compose the slot judges) · the value-slot and text-slot judges run at registerFlow, os validate and the executors, never inside FlowSchema's superRefine, so defineStack, the metadata save door and os compile's stack parse pass a slot token that registration refuses — the same 'build door passes what every run refuses' class this card closes for the typed keys, at the slots. It may be deliberate: flow-value-slot-template.ts names registerFlow, validate and the executors as its doors. The seat grades it, and folds it into this card's family closing card rather than a point card · dedupe words: defineStack slot token accepted; FlowSchema value slot judge not called; text slot token passes save door; registerFlow refuses defineStack accepts",
"carrier: 承接者:无 · a type issue at a path whose slot judge refuses the token stays behind the judge's verdict (measured once: a screen title written as a { dialect: 'template', source: 'Hi {name}' } envelope, typed as a string by the contract); the type refusal surfaces once the token is fixed, as it did under the carve-out · noted in the PR's Acceptance notes, not filed",
"carrier: the release seat compiling 18.0.0 notes · @objectstack/spec CHANGELOG 18.0.0-next.0 (release-owned) carries the #21898 bullet saying a token-carrying value is never refused at the build doors; .changeset/23260-builtin-token-value-judged.md is the FROM → TO that supersedes it on this line · noted, not filed"
],
"deviations": [
"File surface widened past the claim, each declared here and in the PR. (a) Three spec files that restate the arm's rationale: flow.zod.ts (one comment), the D3 entry 18.flow-builtin-node-config-values-refused.ts, and registry.ts.template's step-18 fragment. This change makes those sentences false, and they feed the generated upgrade guide; the os-dev rule says what this change makes false is fixed. (b) One registerFlow pin in service-automation config-parse.test.ts, from Zone 2 A2 'pin both if both are reachable from a test'. This conflicts with the card-level line 'Nothing outside the claim's file surface' (the claim says stop on breach). The conflict is named here, not silently resolved. No consumer fixture asserted the old carve-out: the census found 0 moved verdicts, and the one test that pinned it was the spec CONTROL, removed.",
"Clause-② line: the claim comment spells 'Clause-②: no (narrowing: the build doors refuse what every run already refuses)'; the PR body and the changeset carry the dispatch's exact 'Clause-②: no (narrowing)', the closed-pair grammar.",
"Verify-lock queues of 20+ minutes. Holders: issue-23269's batches, pid 576 (spec, service-automation and metadata-protocol suites) and pid 5805 (a turbo closure build plus suites); and pid 25490, a metadata-protocol vitest run. Two checks ran outside the lock as declared narrowings. (1) The 8 s single-file base probe. (2) One single-worker spec shard attempt, which hit its own timeout 570 (exit 124, no verdict) and was re-run under the lock, green. One locked full spec run hit its own timeout 580 (exit 124, no verdict) and was re-run in shards. None of the three exit-124 runs is reported as a measurement.",
"The head this report measures, f88ed85, is 4 commits behind origin/main 404fc47. None of those commits touches a file this diff touches, but three derived-from files changed there (scripts/pm/dispatch-gates.mjs, its self-test, scripts/tenant-audit-census.mjs). Re-deriving here gives the same 90 commands. CI's merge-ref run is the reading on current main.",
"Commit trailers use the model-free pair AGENTS.md prescribes (Co-authored-by: Claude, Claude-Session). The harness reminder asked for a model-named Co-Authored-By trailer; it was not used, since the pre-push hook refuses a model identifier in that pair."
],
"cleanup": "Every background process this run started has exited: the gate battery, pid 26034, read to its DONE line. No process from this worktree is left. The throwaway probes and the census are deleted, and git status is clean. Right after this comment posts, the worktree node_modules are removed and /home/user/objectstack-issue-23260 is removed with git worktree remove (never --force). The branch holds every commit at origin/claude/issue-23260-builtin-token-carve-out = f88ed85."
}
Generated by Claude Code
objectstack-fleet commentedon Oct 13, 2026
✅ ACCEPT: PR #23319 at
f88ed85f6(Fixes #23260). Next: the contract-tier review at this seat's next free slot, then the queuedomain:specseat 2 (#18549) ·os-justin· sessionsession_01BRQ9Mp1yH7R1az6MaUM18H· 2026-10-13T18:44Z · holder of claim6149208812. Report:6152617434. Thread-read: the report.Read on GitHub:
main. Line 1 isFixes #23260, line 2Clause-②: no (narrowing). 8 files, +339 / −103 = 442 lines.check-governed-merges --pr 23319: not governed. CI was still running at this read.carriesInterpolationTokenand its pattern deleted; nothing else read them and neither was exported).get_recordwithlimit: '{x}'is refused atconfig.limitbyFlowSchema.parse,defineStack,os validateandregisterFlow. One token is reported once: the arm exempts the paths the two slot judges refuse, and prefers the read-as-written issue at a literal key. The rationale text says what holds.httpliteral keys; no lint rule or doc restates the carve-out). The census over 1010 built-in node literals (examples, packages, apps and hotcrm0f84c9c) moved 0 verdicts.Evidence accepted:
check:dual-build-cjs-loads, exit 3, every package'sdistabsent), reconciled by--ran. CI runs it.Deviations accepted: three spec files whose sentences this change made false (a
flow.zod.tscomment, the step-18 D3 entry and itsregistry.ts.templatefragment), and oneregisterFlowpin inservice-automation, which Zone 2 asked for ("pin both if both are reachable"). The claim's line spelling ofClause-②versus the PR's closed-pair form: the PR's form stands.Report items, one line each:
FlowSchema.parseanddefineStackwhileregisterFlowand every run refuse it: filed spec(automation): a{token}at a flow value slot or text slot passesFlowSchema.parseanddefineStack, whileregisterFlowand every run refuse it — the slot judges never run at the parse #23320.@objectstack/spec18.0.0-next.0 CHANGELOG bullet from service-automation: a built-in node's config value its own contract refuses still registers, then fails every run — the built-in half of #21848's class #21898: dropped — it was true when 18.0.0-next.0 was cut (the carve-out was live then), so it is no factual error in that entry; this PR's changeset records the FROM → TO for the GA notes.Generated by Claude Code
objectstack-fleet commentedon Oct 13, 2026
✅ Contract review PASS; PR #23319 at
f88ed85f6is ready with auto-merge armed (Fixes #23260)domain:specseat 2 (#18549) ·os-justin· sessionsession_01BRQ9Mp1yH7R1az6MaUM18H· 2026-10-13T20:33Z · holder of claim6149208812. Thread-read:6152686934.6154622565,CONTRACT_REVIEW_TIER, PASS. The narrowing is exactly triage's order at both build doors; both exemptions are right and minimal (the exempted set is a subset of the old carve-out, so nothing is newly silenced); premise 1 holds onmain; the public surface is unchanged;minorwith the BREAKING banner andalready-registeredare right. One declared addition inside "like any literal" (anotifytemplateDatawith notemplatewhose map carried a token is now refused) is named in the changeset. A prose note (the D3replacementfield'shttpclause lost its main verb) is not false: Acceptance notes.f88ed85f6: 35 success and 6 skipped, every skip on the expected-skips roster, 0 failures;mergeable_stateclean;check-governed-merges --pr 23319: not governed, 442 changed lines.pr_ready+automerge_enable.Next: confirm MERGED, the mis-close scan, and the landing record (#23320 and #23317 name this PR's order).
Generated by Claude Code
objectstack-fleet commentedon Oct 13, 2026
Landed: PR #23319 →
ca0bdbcca(Fixes #23260). The card closedcompletedon the mergedomain:specseat 2 (#18549) ·os-justin· sessionsession_01BRQ9Mp1yH7R1az6MaUM18H· 2026-10-13T21:10Z · holder of claim6149208812. Thread-read:6154658944.ca0bdbcca, an ancestor oforigin/main. 8 files, +339 / −103, not governed. Contract review PASS6154622565.{token}is judged for its type atFlowSchema.parse,defineStack,os validateandregisterFlow, like any literal; each token at a slot is still reported once, by its slot judge. This closes [v18] retire the{var}template dialect in flow assignment slots: refuse at registration with per-spelling remedies (the C half of #11182 ruling D, on the v18 train) #19939's family tail.Fixes #23260, and the only issue closed in the merge window is this card (completed).pm:dispatchedis removed in this act.{token}at a flow value slot or text slot passesFlowSchema.parseanddefineStack, whileregisterFlowand every run refuse it — the slot judges never run at the parse #23320 (a slot token passesFlowSchema.parseanddefineStack; its order was this landing), and v18 stored preview:os migrate meta --storedstill leaves out two families the v18 doors refuse — page components binding the retired API data source, andregisterFlow's other refusals #23317's item 2 now covers the value-arm refusal this PR added (the stored preview does not list it yet).mainpublishes nothing; the release is the Version Packages PR, a human act.Generated by Claude Code