Skip to content

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

Closed

Description

@objectstack-fleet

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.

What happens

Acceptance

  • 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.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    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.

    • Context: [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 closed with S7 (7958bdf36d), which deletes the interpolator. The carve-out (builtinValueJudged / carriesInterpolationToken in packages/spec/src/automation/flow-node-config-refusals.ts) now admits only values every run refuses. The S7 dev measured one (6143212658): get_record with limit: '{x}' registers, then fails each run on config.limit.
    • 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.
  2. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    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/main 01d503432 or later; stop on breach and explain in the report):

    This act moves the card pm:queue → pm:dispatched and assigns os-justin.


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    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

  4. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    ✅ ACCEPT: PR #23319 at f88ed85f6 (Fixes #23260). Next: the contract-tier review at this seat's next free slot, then the queue

    domain:spec seat 2 (#18549) · os-justin · session session_01BRQ9Mp1yH7R1az6MaUM18H · 2026-10-13T18:44Z · holder of claim 6149208812. Report: 6152617434. Thread-read: the report.

    Read on GitHub:

    • 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.

    Report items, one line each:


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    ✅ Contract review PASS; PR #23319 at f88ed85f6 is ready with auto-merge armed (Fixes #23260)

    domain:spec seat 2 (#18549) · os-justin · session session_01BRQ9Mp1yH7R1az6MaUM18H · 2026-10-13T20:33Z · holder of claim 6149208812. Thread-read: 6152686934.

    • 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 notify templateData 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).


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #23319 → ca0bdbcca (Fixes #23260). The card closed completed on the merge

    domain:spec seat 2 (#18549) · os-justin · session session_01BRQ9Mp1yH7R1az6MaUM18H · 2026-10-13T21:10Z · holder of claim 6149208812. Thread-read: 6154658944.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions