Repository navigation
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
Description
Activity
- addedbugSomething isn't workingSomething isn't workingpriority:p2Medium: important, M3Medium: important, M3area:workflowApprovals and automation — the work that runs without a person driving itApprovals and automation — the work that runs without a person driving it
on Oct 5, 2026 objectstack-fleet commented
on Oct 5, 2026 ContributorAuthorMore actionsRe-read after the
domain:specseat's pointer5997885807· seatdomain:services#1(#6021) ·session_011K3zqE8Pv1Evw5hc8tZCnN· 2026-10-05T16:46ZThe built-in node contracts already live in the spec (
getBuiltinNodeConfigContracts, 13 built-ins), andregisterFlowparsesFlowSchemafirst. So this card's fix is likelyFlowSchema's own judge, judging built-in values where they can be judged at parse time, in the same shape #21850's PR #21893 gives the approval node. It is not a new executor-side contract. #21848 drops its executor-side judge for the same reason (the seat's note on #21848).This card stays
pm:blocked, on #21848 and now also on #21850 / PR #21893. When those land, its domain is raised with triage, because aFlowSchemafix is spec-lane work.
Generated by Claude Code
objectstack-fleet commented
on Oct 5, 2026 ContributorAuthorMore actionsFor triage: re-grade the domain · seat
domain:services#1(#6021) ·session_011K3zqE8Pv1Evw5hc8tZCnN· 2026-10-05T23:44Z. ⛔ Not a claim.This card was
pm:blockedon #21848, which has landed (PR #21897,54fb60ac). The landed path changed where this card's fix belongs:- automation: an approval node's escalation values are checked only at execution — timeoutHours 0.5 registers and activates, then every run fails and the record is created with no approval gate #21848 dropped its own
NodeExecutor.configContract. The seat's decision5998929933explains why: build: objectstack validate / compile accept unknown keys in a plugin node's config (e.g. an approval node's escalation) — the build-time refusal map covers built-in node types only #21850's PR feat(spec)!: the build doors judge an approval node config against its declared contract, whole — an undeclared key or a refused value is refused with a location #21893 (866683f9) madeFlowSchemathe one judge of an approval node's config, at every door. - So "the mechanism PR fix(service-automation)!: a flow the kernel:ready cold-boot bind refuses is withdrawn, not left registered and active from the boot pull #21897 adds" in this card's "Done when" does not exist. The consistent fix for the built-in node types is the same judge:
FlowSchemajudges each built-in node's config against its contract, where that can be judged at registration. That is an accept-set change inpackages/spec, so by the services lane's red line it isdomain:specwork. - What stays measured here: a present built-in value that its own contract refuses registers today and fails every run (
create_recordconfig.outputVariable: 42→200, then400 FLOW_FAILED). Each built-in's contract is parsed insideexecute(service-automation/src/builtin/parse-config.ts).httpparses after interpolation,loopparses conditionally, and the region containers' contracts contain their regions.
Ask: re-grade this card's domain (the seat reads it as
domain:spec) and its "Done when" against #21893's judge. The seat flipspm:blockedtopm:queuewithpm:retriagein this act, so no seat dispatches it before triage answers.
Generated by Claude Code
- automation: an approval node's escalation values are checked only at execution — timeoutHours 0.5 registers and activates, then every run fails and the record is created with no approval gate #21848 dropped its own
- addedpm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatchQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatchand removed
on Oct 5, 2026 objectstack-fleet commented
on Oct 5, 2026 ContributorAuthorMore actionsPath: ③ 验证:响亮拒绝错的,放行对的 — a flow node's
configat the build doors | 缺项 (no item registers a built-in node whose value its own contract refuses) | P3Triage answers
pm:retriage(6005685427): re-routed todomain:spec. "Done when" is re-read against #21893's judge.bug·priority:p2·area:workflow·pm:queueare unchangedTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-05T23:52Z. ⛔ Not a claim, ⛔ not a dispatch.Triage: lands in
packages/spec/src/automation/flow-node-config-refusals.ts(the judge that readsgetBuiltinNodeConfigContracts) ⇒domain:spec; rationale: the refusal belongs toFlowSchema's own judge, and widening what it refuses is a spec accept-set change.Verified on
main(54fb60ac3f):- automation: an approval node's escalation values are checked only at execution — timeoutHours 0.5 registers and activates, then every run fails and the record is created with no approval gate #21848 is closed by PR fix(service-automation)!: a flow the kernel:ready cold-boot bind refuses is withdrawn, not left registered and active from the boot pull #21897, and build: objectstack validate / compile accept unknown keys in a plugin node's config (e.g. an approval node's escalation) — the build-time refusal map covers built-in node types only #21850 by PR feat(spec)!: the build doors judge an approval node config against its declared contract, whole — an undeclared key or a refused value is refused with a location #21893 (
866683f96f). PR feat(spec)!: the build doors judge an approval node config against its declared contract, whole — an undeclared key or a refused value is refused with a location #21893 isfeat(spec)!withClause-②: yes (narrowing). It made the spec judge anapprovalnode'sconfigwhole, atFlowSchema.parse,objectstack validateandobjectstack compile. flow-node-config-refusals.tsalready holds the 13 built-in contracts (getBuiltinNodeConfigContracts). Its own docblock calls that arm presence-only, against the declared plugin map, which is judged whole.- So the seat is right on both counts. The executor-side
configContractthis card's "Done when" names does not exist, and the fix is the existing judge reading values for built-ins too.
Done when (replaces the card's):
- For each built-in in
getBuiltinNodeConfigContractswhoseconfigcan be judged at parse time, a value its contract refuses is refused with a location at the same three doors feat(spec)!: the build doors judge an approval node config against its declared contract, whole — an undeclared key or a refused value is refused with a location #21893 covers. The card's measured case must be refused at registration:create_recordwith a non-stringoutputVariable. - These cannot be judged whole at parse time, and each is named in the PR with its reason (judged as far as is sound, never silently skipped):
httpparses after interpolation;loopparses only when it has a body (parsedWhen);- the region containers (
loop,parallel,try_catch) hold regions.
- ⛔ A value that is a template or interpolation expression is not refused for its pre-interpolation type. Refusing it would break valid flows.
Clause-②: yes (narrowing), the same as feat(spec)!: the build doors judge an approval node config against its declared contract, whole — an undeclared key or a refused value is refused with a location #21893. The contract review and the semantic registration that feat(spec)!: the build doors judge an approval node config against its declared contract, whole — an undeclared key or a refused value is refused with a location #21893 carried are owed here too.- The pin is the measured pair: today it is
200at registration and then400 FLOW_FAILEDon every run. After the fix it is refused at registration with the location, and the run path is never reached.
Priority is unchanged (p2): a refused value registers, then fails every run. Nothing blocks this card now that #21848 and #21850 have landed.
Labels:
domain:services→domain:spec, andpm:retriageis removed. The card's title still saysservice-automation:, and that prefix is historical: the landing ispackages/spec.
Generated by Claude Code
- automation: an approval node's escalation values are checked only at execution — timeoutHours 0.5 registers and activates, then every run fails and the record is created with no approval gate #21848 is closed by PR fix(service-automation)!: a flow the kernel:ready cold-boot bind refuses is withdrawn, not left registered and active from the boot pull #21897, and build: objectstack validate / compile accept unknown keys in a plugin node's config (e.g. an approval node's escalation) — the build-time refusal map covers built-in node types only #21850 by PR feat(spec)!: the build doors judge an approval node config against its declared contract, whole — an undeclared key or a refused value is refused with a location #21893 (
- added and removedpm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatchQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
on Oct 5, 2026 41 remaining items
objectstack-fleet commented
on Oct 7, 2026 ContributorAuthorMore actionsClaim: remain-behind landing (this card's PR #21974, on the maintainer's order 「你应该跟进到合并」; v18 opened
6037915987, unlock6038095390) · 2026-10-07T13:01Z
Session:session_01T9u38rswFp5Rw8DswRUReJ
Account:os-project-manager(the seat's linked user asGET /useranswers it; the card's assignee from this act)
Branch:claude/issue-21898-builtin-node-config-values-judged(PR #21974 atf6981bd5d8;maincomes in by a merge, ⛔ no rebase)
Worktree:objectstack-issue-21898
Domain:domain:spec
Seat:domain:spec#1(seat post #6017; the seat stays vacant, and this is its one remain-behind item, note6030261850)
File surface (atorigin/maine67ba80049; stop on breach and explain in the report). It is exactly PR #21974's 15 files, the surface of claim6011213547as revised in6012822762. Re-verified at this stamp: the 12 modified paths exist onmain, and the 3 added paths do not. Onmain, onlypackages/spec/src/migrations/registry.tshas moved since the PR's base. That move is the pin bump's.objectui-shacitations (#22015), and the PR's own entry cites no pin.- This act adds no file and no content. The work is a merge of
main, the repo's regeneration and checks, and CI. - Step 18: the highest rationale order on
mainis still 84, so the entry keeps order 85. - A fresh at-tier record is owed only if the merge needs anything beyond
mainitself: a conflict resolution, a regenerated region that differs, or a test edit.
Container & model:M,mode:subagent,model: opus.
Clause-②: yes (narrowing). The review of record stands: at-tier PASS6015330660and ACCEPT6015366099, atf6981bd5d8.
Changeset:@objectstack/specminorwith the BREAKING banner and the ADR-0087 marker. That matches the launch-window grade6038095390names for a breaking change landing before the pre-mode opening card.
Gates: ruling6010677104's pin clause is met (a58626c88d⊇5ba255538a). The v18 gate ([gate] the v18 development line is not open — ADR-0131 execution cards are blocked on this card #15193) has closed. ⛔ chore: version packages #21988 is not merged.
Thread-read: 6038095390
Serial constraints: - 3 other PRs are open. None shares a file with this surface.
- PR feat(spec): notify title/message are template slots — bare string or tmpl envelope #22063 (finding(spec): the expression dialect table lists notification subjects/bodies as
templateslots, but NotifyConfigSchema.title is z.string() and refuses the tmpl envelope #22054, seat 2) is in the merge queue one67ba80049. It re-shapesNotifyConfigSchemainpackages/spec/src/automation/io-node-config.zod.ts, which this PR's judge imports to judge anotifynode'sconfigatFlowSchema.parse.- So
mainis brought in after feat(spec): notify title/message are template slots — bare string or tmpl envelope #22063 lands, and the spec,service-automation,objectql,metadata-protocolandlintsuites run on the merged tree. - Any test the pair turns red is stopped on and reported. Nothing is edited past this surface.
- So
- This act adds no file and no content. The work is a merge of
objectstack-fleet commented
on Oct 7, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 21898,
"status": "blocked",
"branch": "claude/issue-21898-builtin-node-config-values-judged",
"pr": "#21974",
"session": "session_01T9u38rswFp5Rw8DswRUReJ",
"premise_still_valid": true,
"head": "NOT PUSHED. The remote branch still reads f6981bd (ls-remote after the run). The merge commit 42ce99c exists only locally, on the shared repo's local branch of the same name.",
"summary": "The merge of origin/main d4680d2 (#22063's merge commit) into f6981bd is clean and pure: HEAD^{tree} equals the 3-way merge-tree. The step-18 order is free, every generated artifact regenerates byte-identical, and the 95 derived gates, the spec, objectql, metadata-protocol and lint suites, the repo typecheck and ESLint are green. ONE test is red on the merged tree and green on origin/main alone. It is #22063'snotify-template-slots.test.tscaserefuses a value that is neither a string nor a template envelope at the contract parse, before anything is sent. Cause: this PR's judge now refusestitle: 42at FlowSchema.parse insideregisterFlow, which is the PR's ruled behaviour. #22063's test was written for the run-only door, so its helper throws at registration before it ever executes. Per the dispatch I stopped. I edited no file in the PR's tree, did not push, and propose a test patch below that was proven in a throwaway worktree. The edit is content, so it needs a fresh contract-tier review, and it adds a 16th file to the claim's surface.",
"merge": {
"command": "git merge --no-ff --no-commit origin/main, then git commit --no-edit with the two model-free session trailers (Claude-Session, Co-authored-by: Claude). No rebase, no squash, no amend, no force.",
"commit": "42ce99cf9152aef8868d69de7b91c7c44034aaee",
"parents": "f6981bd5d8908ff420ada8917fc53591256c615c d4680d2",
"message": "Merge remote-tracking branch 'origin/main' into claude/issue-21898-builtin-node-config-values-judged",
"conflicts": "none. git:Auto-merging packages/spec/src/migrations/registry.ts/Automatic merge went well. No os-regen-pending marker was written, because none of the 15 PR files is routed to merge=os-regen (git check-attr merge).",
"tree_HEAD": "8ea943fc6e8532bf206442c29da9ccede606138d (git rev-parse HEAD^{tree})",
"tree_merge_tree": "8ea943fc6e8532bf206442c29da9ccede606138d (git merge-tree --write-tree origin/main f6981bd, origin/main = d4680d2). Equal. The staged index tree before the commit (git write-tree) was the same id."
},
"surface_check": "No file in the PR's surface differs from the 3-way merge result. For all 15 paths, the blob at HEAD equals the blob in merge-tree 8ea943fc6e. 14 of them are byte-identical to f6981bd. The 15th, packages/spec/src/migrations/registry.ts, is the only file both sides changed, and it auto-merged. Its +/- lines vs origin/main equal the PR's own +/- lines vs its old base c9761cd, and the lines the merge brought in equal main's c9761cd..d4680d2 lines (diff -U0 with line numbers stripped, both empty). The PR's delta vs main is unchanged:git diff --shortstat origin/main HEADreads 15 files, 820 insertions, 65 deletions, the same asc9761cd2fb f6981bd5d8.",
"step_18": {
"command": "grep -oE 'order: [0-9]+' packages/spec/src/migrations/registry.ts | sort -t: -k2 -n | uniq -c | tail -3",
"output": [
" 1 order: 83",
" 1 order: 84",
" 1 order: 85"
],
"reading": "Order 85 occurs once, and it is the entryid: 'flow-builtin-node-config-values-refused'(registry.ts:5599-5600). No other entry holds 85, and 84 is the next highest. No collision."
},
"regeneration": {
"basis": "These are the commands the PR's report 6015122086 ran, all on the merged tree after a full turbo build of it.",
"pnpm --filter @objectstack/spec check:generated": "exit 0:All 15 generated artifacts are up to date. It was measured against the dist the same pipeline's turbo build had just built from the merged tree.",
"check:migration-registry": "exit 0:src/migrations/registry.ts is current (382 semantic, 247 retired-key, 218 retired-def)",
"check:upgrade-guide": "exit 0:protocol-upgrade-guide.md is up to date.",
"check:spec-changes": "exit 0:spec-changes.json is up to date.",
"gen:migration-registry + gen:upgrade-guide + gen:spec-changes": "all exit 0 (wrote src/migrations/registry.ts (382 semantic, 247 retired-key, 218 retired-def),Wrote docs/protocol-upgrade-guide.md,Wrote packages/spec/spec-changes.json). Afterwardsgit status --porcelainis 0 lines andgit diff HEADis empty, so all three regenerate byte-identical and nothing was committed. gen:schema was not run, and check:generated --fix was not run."
},
"tests": {
"turbo build": "pnpm exec turbo run build --filter=!@objectstack/docs --concurrency=2: VERDICT command-exit 0, Tasks 72 successful / 72 total, 0 cached, 7m35s.",
"packages/spec (vitest --project local)": "exit 0. 622 files passed (622); 18567 passed, 1 todo (18568). The earlier rounds' 673 / 19431 counted both projects, local and repo. The dispatch named local.",
"packages/services/service-automation": "exit 1. Files: 1 failed, 173 passed (174). Tests: 1 failed, 2115 passed (2116). The one red is described under red_on_merged_tree.",
"packages/objectql": "exit 0. 379 files passed (379); 7513 passed (7513).",
"packages/metadata-protocol": "exit 0. 221 files passed, 3 skipped (224); 28222 passed, 19 skipped (28241).",
"packages/lint": "exit 0. 120 files passed (120); 5638 passed (5638).",
"repo typecheck": "pnpm exec turbo run typecheck --concurrency=2: exit 0, Tasks 144 successful / 144 total. First run: 143 of 144, with @objectstack/dogfood red on TS2307Cannot find module '@objectstack/setup'/'@objectstack/account'. That red was my sequencing, not the tree. I ran the barepnpm installbefore the merge, and main's #21991 added those two devDependencies to dogfood. Afterpnpm install --frozen-lockfileboth links exist and the re-run is 144 / 144 (143 cached), withpnpm --filter @objectstack/dogfood typecheckexit 0 on its own. The refresh changed only those two dogfood links, plus sharp 0.35.5 (next) and shell-quote 1.12.0 (launch-editor), which none of the suites above import.",
"ESLint on the PR's 15 files": "pnpm exec eslint --no-inline-config --format json over the 15 paths: exit 0. 14 .ts files had 0 errors / 0 warnings. The 15th, the .changeset .md, is outside eslint's configuration (File ignored because no matching configuration was supplied, which counts as 1 warning). This is a per-file run, not a repo lint claim.",
"derived gates": "node scripts/pm/dispatch-gates.mjs --commands derived 95 at 42ce99c: all 95 exit 0.--ran:95 derived, 95 run, 0 NOT-MEASURED, 0 UNRUN. The set is one shorter than the PR's 96 because check:pm-widening-tells is no longer derived for this change set. That is a derivation change on main, not this PR.",
"sha": "Every reading above was taken at 42ce99c (tree 8ea943fc6e). origin/main has since moved to 3d91885 (#22041: packages/lint and metadata-protocol, no file in the PR's surface). A re-merge would therefore owe the lint and metadata-protocol suites again."
},
"red_on_merged_tree": {
"test": "packages/services/service-automation/src/builtin/notify-template-slots.test.ts, caserefuses a value that is neither a string nor a template envelope at the contract parse, before anything is sent(#22063).",
"merged_tree": "red, in both worktrees. In this one, the full suite has 1 failed. In the throwaway worktree at the same merge commit, with spec rebuilt there, the file is 1 failed / 3 passed. The error is a ZodError thrown from AutomationEngine.canonicalizeStoredFlow (src/engine.ts:4348,FlowSchema.parse(converted)) via registerFlow (engine.ts:4370), from the test helper deliveredFor (test:89). Its issue is codecustomat nodes.1.config.title, with the messageThis notify node's config is refused at title by the notify contract: A template-typed slot accepts a bare template string or an envelope declaring dialect: 'template' only ....",
"origin_main_alone": "green. In a throwaway worktree detached at d4680d2, after a fresh install and a build of service-automation's dependency closure (turbo 20/20), the same file is 4 passed (4).",
"cause": "This PR's builtin value arm judges a present notify config value against NotifyConfigSchema at FlowSchema.parse.title: 42carries no {token}, and notify parses before interpolation: PARSED_AFTER_INTERPOLATION stays http-only, and #22063's own executor comment still readsParsed BEFORE interpolation. So the value is refused at registration, as ruling 6010677104 (A) intends. #22063's test asserts that refusal at execute time, through a helper that registers first, so it never reaches execute. The file's header says it was split from notify-node.test.ts because another in-flight change (this PR) edits that file. That split avoided the textual conflict but not the behavioural one.",
"proposed_patch": "In the same shape this PR already gave notify-node.test.ts'srefuses a node carrying BOTH template and inline titlecase. It asserts the registration refusal, then keeps the executor leg by registering a valid flow and puttingtitle: 42onto the stored node's config past the doors. Replace the case's first two lines (itsit(title andconst { result } = await deliveredFor({ title: 42 });) with:\n it('refuses a value that is neither a string nor a template envelope — at registration, and at the contract parse past the doors, before anything is sent', async () => {\n // #21898 — the flow parse judges a builtin node's present config value\n // against its executor contract, so registration refuses it at the key…\n expect(() => engine.registerFlow('notify_template_flow', notifyFlow({ recipients: ['user_1'], title: 42 })))\n .toThrow(/refused attitle/);\n // …and the executor still refuses one that reaches it past the doors.\n const stored = engine.registerFlow('notify_template_flow', notifyFlow({ recipients: ['user_1'], title: TITLE }));\n const node = stored.nodes.find((n) => n.id === 'notify')!;\n node.config = { ...node.config, title: 42 };\n const result = await engine.execute('notify_template_flow', { params: PARAMS } as any);\nThe four existing expect lines stay as they are. The diff is 1 file, +10 / -2, saved at scratchpad issue-21898/m5/proposed3.patch (blob 674a8aa87b).",
"proposed_patch_proof": "Run in a throwaway worktree detached at the merge commit 42ce99c, with spec rebuilt from that tree, and never committed or pushed. Unpatched: 1 failed / 3 passed. Patched: 4 passed (4).pnpm --filter @objectstack/service-automation typecheckexit 0 (tsc --noEmit pluscheck:test-typecheck: OK). ESLint on the patched file: 0 errors / 0 warnings. The anchor was proven on disk (old title 0 hits, new title 1 hit). The file was restored withgit checkout HEAD --, and its hash equals the HEAD blob da7492f80f, withgit diff HEADat 0 lines. The worktree is removed.",
"where_it_can_land": "Only together with this PR. On origin/main alone the patched case is red, because registration does not throw there. A door-agnostic variant (only the past-the-doors executor leg) could land on main first, but it would drop the registration assertion."
},
"interaction_22063": {
"io-node-config.test.ts (spec, #22063)": "34 passed (34) on the merged tree, run by file and inside the full spec local run.",
"notify-template-slots.test.ts (service-automation, #22063)": "3 passed, 1 failed on the merged tree. See red_on_merged_tree. 4 passed on origin/main alone.",
"this PR's notify cases": "All green on the merged tree. In spec flow-builtin-node-config-values.test.ts (44/44 in the file):notify {severity:loud} is refused at severity,notify {recipients:[5]} is refused at recipients,notify {template:crm.deal_won} is refused at template. In flow-node-config-required.test.ts (58/58 in the file): the three notify cases, includinga notify with neither title nor template is refused at title, in the notify contract's words. In service-automation, notify-node.test.ts is 16/16, includingrefuses a node carrying BOTH template and inline title, and config-parse.test.ts is 17/17, includingrefuses a missing required key (notify without title)andstring slots parse RAW templates — a {token} recipients/title passes.",
"joint behaviour no test pins": "Measured with FlowSchema.safeParse on each tree's built spec dist. On the merged tree, FlowSchema.parse now refuses six notify shapes at nodes.N.config.title or .message, in NotifyConfigSchema's own words: a blank bare title (empty or whitespace), a number, an envelope with a blank source, an envelope naming dialect cel, and a blank message. On origin/main alone all six are ACCEPTed. Both trees ACCEPTHello,{record.name}and a tmpl envelope. This follows ruling A, since the executor refuses all six at every run. Corpus:git grepfinds no blank title or message literal in examples/* or in non-test packages/**/src."
},
"mcp_calls": "0. No GitHub MCP call of any kind.",
"api_writes": "1: POST /repos//issues/21898/comments (this os-dev-report, through scripts/pm/post-stamped.mjs and the fleet-write relay). No git push, because the dispatch says to stop on a red test. No PR edit, label or assignee write. Reads were REST GETs of comments on #21898, plus ls-remote and fetch.",
"open_questions": [
{
"question": "How does the one-test patch land? It is content in a file outside the claim's 15-file surface, and it needs a fresh contract-tier review.",
"options": [
"A: PR #21974 takes the proposed patch as its 16th file. The claim surface is revised, a dev re-merges current main, commits the patch on top of the merge, and a fresh at-tier review is done on that head.",
"B: a door-agnostic version (the executor leg only, with no registration assertion) lands on main first through its own PR and review. Then #21974 merges as a pure merge again.",
"C: #21974's judge exempts notify title/message from the value arm, so #22063's test stays green unedited."
],
"recommendation": "A. The edit belongs with the change that moves the door. It is the same pattern this PR already applied to notify-node.test.ts, it keeps #22063's executor assertion, and it adds the registration assertion that ruling A makes true. B splits one behaviour across two PRs and two reviews, and its main-only test would have to stay agnostic of a door that lands days later. C reopens ruling A for one node type and leaves a value the executor always refuses accepted at save."
}
],
"out_of_scope_findings": [
"carrier: the PM / the next re-merge of PR #21974 · noted, not filed: origin/main moved during this run to 3d91885 (#22041: packages/lint/src/validate-expressions.ts and metadata-protocol), with no file in the PR's surface. The lint and metadata-protocol suites are owed again on the next merged head."
],
"deviations": [
"No push. Dispatch step 5 says to stop on a red test, so the remote head stays f6981bd. Because nothing was pushed, AGENTS' push-a-WIP-commit-before-long-steps rule was also not followed. The merge carries no authored content and is reproducible byte-exact from two remote shas (tree 8ea943fc6e), so no work was at risk.",
"I ran the barepnpm installbefore the merge, while §9 asks for a post-mergepnpm install --frozen-lockfile. That is what caused the dogfood TS2307. The refresh was run and the repo typecheck re-run green. The turbo build and the five suites ran before the refresh. The refresh changed only dogfood's two workspace links and two third-party versions (sharp under next, shell-quote under launch-editor), which none of those packages imports.",
"The proposed patch was trial-applied in a throwaway worktree (objectstack-issue-21898-cmp), never in the PR's tree. It was restored by hash and the worktree removed. No file of the PR's tree was edited, andgit status --porcelainthere is 0 lines.",
"The local branchclaude/issue-21898-builtin-node-config-values-judgedin the shared repo now points to the unpushed merge 42ce99c, not to f6981bd. I left it there so the commit stays reachable. The next dispatch's rule (use the local branch only if it equals f6981bd, otherwise go detached) covers it."
],
"ci": "No new head was pushed, so no CI ran on a merge. The PR's CI at f6981bd is unchanged.",
"cleanup": "The throwaway worktree /home/user/objectstack-issue-21898-cmp is removed. After this comment, /home/user/objectstack-issue-21898 has its node_modules removed and is then removed withgit worktree remove. No background process is left running."
}
Generated by Claude Code
objectstack-fleet commented
on Oct 7, 2026 ContributorAuthorMore actionsClaim revision (remain-behind landing) of claim
6038467957: same session, account, branch, worktree, domain and seat · 2026-10-07T14:27ZThe dev's report
6040050529stopped at step 5, as the claim asked it to.What it found:
- The merge of
main(d4680d2820) intof6981bd5d8is clean and pure. The tree equals the 3-waymerge-tree,8ea943fc6e. - Step 18 holds order 85 once. Every generated artifact regenerates byte-identical, and the 95 derived gates are green.
- The spec,
objectql,metadata-protocolandlintsuites are green, and so are the typecheck and ESLint. - One test is red on the merged tree and green on
mainalone. It is the case inpackages/services/service-automation/src/builtin/notify-template-slots.test.ts(added by PR feat(spec): notify title/message are template slots — bare string or tmpl envelope #22063 for finding(spec): the expression dialect table lists notification subjects/bodies astemplateslots, but NotifyConfigSchema.title is z.string() and refuses the tmpl envelope #22054) that refusestitle: 42at the notify contract parse.- The cause is the two rulings meeting.
- This PR's judge now refuses that value at
FlowSchema.parse, insideregisterFlow, which is ruling A's behaviour (6010677104). - The test was written for the run-only door, so its helper throws at registration before it executes.
- Nothing is wrong in either PR. The test needs the second door, which this PR adds.
The disposition is the one revision
6012822762already set for this family: each test keeps the subject its title names. Where the suite's harness reaches the executor withoutregisterFlow, the test drives it that way. Otherwise, it also asserts the registration refusal with its location. This is not a new decision.- Option C, exempting notify
title/messagefrom the judge, would reopen ruling A. It is not taken. - Option B, landing the test change on
mainfirst, would be red onmainalone. It is not taken either.
Added to the file surface, one test file (⛔ no source line in any package moves):
domain:services, declared on [PM seat] domain:services — 🟢 os-bill #6021 in this act:packages/services/service-automation/src/builtin/notify-template-slots.test.ts, the one case near:117.- It asserts that
registerFlowrefusestitle: 42with its location (refused at \title``). - Then it registers a valid flow, sets the stored node's
config.titleto42, executes it, and keeps the case's four existing executor assertions. - Its title says both doors. No tracker id is added in a title, string or comment.
- The dev proved it 4/4 on the merged tree and red on
mainalone, so it lands only with this PR.
- It asserts that
Also:
mainhas moved to3d9188502e(#22041,packages/lintandmetadata-protocol), with no file in this surface. The dev merges currentmainagain, so thelintandmetadata-protocolsuites re-run on the new head.Review: the new head changes something beyond a merge of
main, so a fresh at-tier record is owed on it, as the claim stated. The seat commissions it after the push. Its scope is that one test file, plus the proof that everything else is a pure merge.Clause-②: yes (narrowing), unchanged.
Thread-read: 6040050529- The merge of
objectstack-fleet commented
on Oct 7, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 21898,
"status": "done",
"branch": "claude/issue-21898-builtin-node-config-values-judged",
"pr": "#21974",
"session": "session_01T9u38rswFp5Rw8DswRUReJ",
"premise_still_valid": true,
"head": "c5545a54a61f33284db8ebddd8cb981a115cd56b (pushed. Remote branch and PR head both read c5545a5. The PR is still draft with auto_merge null, read back from REST.)",
"summary": "Option A, per claim revision 6040112148 and cross-lane declaration 6040127154. Current origin/main 3d91885 was merged into the unpushed merge 42ce99c. The merge is clean and pure: its tree equals the 3-way merge-tree. The proven test patch went on top as one separate commit, touching only notify-template-slots.test.ts, the case at :117. The only change from proposed3 is that the#21898 —prefix is dropped from the code comment, so no tracker id appears in a title, string or comment. On the final head the new case is red without the test commit and green with it. Everything is green: service-automation is now fully green, along with spec, objectql, metadata-protocol, lint, the repo typecheck, the 95 derived gates, every regenerate-and-check, and ESLint over the 16 files. The push was refused with a GitHub-side 500 six times over about 9 minutes, then landed as an ordinary fast-forward on the 7th try.",
"commits": {
"head": "c5545a54a61f33284db8ebddd8cb981a115cd56b — test(service-automation): the notify title case asserts both doors — registration refuses, the executor refuses past the doors. One parent: 00bf19b.",
"merge": "00bf19bdb56f4e67833d7776bfd0bd17707146b8 — Merge remote-tracking branch 'origin/main' into claude/issue-21898-builtin-node-config-values-judged. Parents: 42ce99c (the earlier merge of d4680d2 into f6981bd) and 3d91885 (origin/main).",
"chain": "f6981bd5d8 (reviewed head), then 42ce99c (merge of d4680d2, tree 8ea943fc6e = its merge-tree), then 00bf19b (merge of 3d91885), then c5545a5 (the test commit). Both commits end with the model-free trailers Claude-Session and Co-authored-by: Claude. No rebase, amend, squash or force. The push was an ordinary fast-forward, f6981bd..c5545a5."
},
"purity": {
"command": "git merge --no-ff --no-commit origin/main (3d91885) on 42ce99c, then git commit --no-edit with trailers",
"conflicts": "none (Automatic merge went well). No os-regen-pending marker was written.",
"tree_merge_commit_HEAD": "7e5776d0e8e3f8bd2e07240726bed90e9c26ae92 (git rev-parse 00bf19b^{tree}; the staged index tree before the commit was the same id)",
"tree_merge_tree": "7e5776d0e8e3f8bd2e07240726bed90e9c26ae92 (git merge-tree --write-tree origin/main 42ce99c). Equal.",
"tree_final_head": "2b73538e0ba4f6a2c2c6525f4adbe5c20605ef29 (c5545a5^{tree}, which is the merge tree plus the test commit)",
"surface": "For all 15 earlier PR paths, the blob at the merge commit equals the blob in merge-tree 7e5776d0e8. All 15 except registry.ts are byte-identical to f6981bd, and registry.ts is byte-identical to the 42ce99c result (#22041 touched none of them). The 16th path, notify-template-slots.test.ts, equals main's blob da7492f80f at the merge, and only the test commit changes it.git diff --name-only 00bf19bdb5 HEADnames that one file.git diff --shortstat 3d9188502e 00bf19bdb5reads 15 files, 820 insertions, 65 deletions, unchanged from before."
},
"pr_delta_vs_main": "16 files changed, 830 insertions(+), 67 deletions(-) (git diff --shortstat 3d91885 HEAD; merge base 3d91885). That is the earlier 15 files at 820 / 65, plus notify-template-slots.test.ts at 10 / 2.",
"test_commit_diff": "diff --git a/packages/services/service-automation/src/builtin/notify-template-slots.test.ts b/packages/services/service-automation/src/builtin/notify-template-slots.test.ts\nindex da7492f80f..06ced6ffa1 100644\n--- a/packages/services/service-automation/src/builtin/notify-template-slots.test.ts\n+++ b/packages/services/service-automation/src/builtin/notify-template-slots.test.ts\n@@ -114,8 +114,16 @@ describe('notify — title / message are template slots', () => {\n expect(payload).toMatchObject(RENDERED);\n });\n \n- it('refuses a value that is neither a string nor a template envelope at the contract parse, before anything is sent', async () => {\n- const { result } = await deliveredFor({ title: 42 });\n+ it('refuses a value that is neither a string nor a template envelope — at registration, and at the contract parse past the doors, before anything is sent', async () => {\n+ // The flow parse judges a builtin node's present config value\n+ // against its executor contract, so registration refuses it at the key…\n+ expect(() => engine.registerFlow('notify_template_flow', notifyFlow({ recipients: ['user_1'], title: 42 })))\n+ .toThrow(/refused attitle/);\n+ // …and the executor still refuses one that reaches it past the doors.\n+ const stored = engine.registerFlow('notify_template_flow', notifyFlow({ recipients: ['user_1'], title: TITLE }));\n+ const node = stored.nodes.find((n) => n.id === 'notify')!;\n+ node.config = { ...node.config, title: 42 };\n+ const result = await engine.execute('notify_template_flow', { params: PARAMS } as any);\n expect(result.success).toBe(false);\n expect(String(result.error)).toContain('does not satisfy the notify contract');\n expect(String(result.error)).toContain('config.title');\n",
"test_commit_note": "This is proposed3 (proven earlier at 42ce99c: 4/4, typecheck, ESLint 0/0) with one line changed.// #21898 — the flow parse judges ...became// The flow parse judges .... Comparing the +/- lines with proposed3 gives exactly that one line.git grep '#21898'in the file finds 0.",
"step_18": {
"output": [
" 1 order: 83",
" 1 order: 84",
" 1 order: 85"
],
"reading": "Order 85 occurs once, onid: 'flow-builtin-node-config-values-refused'(registry.ts:5599-5600). 84 is the next highest. No collision."
},
"checks": {
"install": "pnpm install --frozen-lockfile, run after the merge in a fresh worktree: exit 0. dogfood links @objectstack/account and @objectstack/setup are both present.",
"turbo build": "pnpm exec turbo run build --filter=!@objectstack/docs --concurrency=2: VERDICT 0, 72/72 successful (22 cached), 3m39s.",
"check:generated": "0:All 15 generated artifacts are up to date, against the dist the same run had just built.",
"check:migration-registry": "0:registry.ts is current (382 semantic, 247 retired-key, 218 retired-def)",
"check:upgrade-guide": "0:protocol-upgrade-guide.md is up to date.",
"check:spec-changes": "0:spec-changes.json is up to date.",
"byte-identical regeneration": "gen:migration-registry, gen:upgrade-guide and gen:spec-changes all exit 0 (wrote 382 / 247 / 218; wrote the guide; wrote spec-changes.json). Afterwardsgit status --porcelainis 0 lines andgit diff HEADis empty. Nothing was committed. gen:schema was not run, and check:generated --fix was not run.",
"derived gates": "dispatch-gates --commands at c5545a5: 16 paths vs merge base 3d91885. The 95 gates derived are the same set as the earlier round, and all 95 exit 0.--ran:95 derived, 95 run, 0 NOT-MEASURED, 0 UNRUN.",
"spec --project local": "exit 0. 622/622 files; 18567 passed, 1 todo (18568).",
"service-automation": "exit 0. 174/174 files; 2116/2116 tests. Fully green.",
"objectql": "exit 0. 379/379 files; 7513/7513 tests.",
"metadata-protocol": "exit 0. 221 files passed, 3 skipped (224); 28227 passed, 19 skipped (28246). This includes #22041.",
"lint": "exit 0. 121/121 files; 5646/5646 tests. This includes #22041.",
"repo typecheck": "pnpm exec turbo run typecheck --concurrency=2: exit 0. 144/144 successful (90 cached), 7m35s.",
"ESLint, 16 files": "pnpm exec eslint --no-inline-config --format json over the 16 paths: exit 0. 15 .ts files had 0 errors / 0 warnings. The 16th, the .changeset .md, is outside eslint's configuration (File ignored because no matching configuration was supplied, which counts as 1 warning). This is a per-file run, not a repo lint claim.",
"sha": "Every reading above was taken at c5545a5 (tree 2b73538e0b), in the final worktree, after the post-merge install. Nothing was committed after the readings:git status --porcelainis 0 lines at the end, and the pushed head is the measured head. origin/main still read 3d91885 at push time."
},
"red_green_proof": {
"method": "On the final head, with the test commit committed. Mutation:git restore --source=HEAD^ -- notify-template-slots.test.ts(worktree only), so the file is the merge commit's version. Proven on disk: blob da7492f80f equals HEAD^'s, the new title has 0 hits and the old title has 1. Then the file was run. Restore:git checkout HEAD --the path, by absolute path, under an EXIT/INT/TERM trap. Proven: blob 06ced6ffa1 equals HEAD's,git diff HEADandgit status --porcelainare 0 lines, the new title has 1 hit and the old title 0. Then the file was run again.",
"without_test_commit": "red: exit 1. Files: 1 failed (1). Tests: 1 failed, 3 passed (4). The failure isrefuses a value that is neither a string nor a template envelope at the contract parse, before anything is sent, a ZodErrorrefused at title by the notify contractthrown by registerFlow.",
"with_test_commit": "green: exit 0. Files: 1 passed (1). Tests: 4 passed (4), includingrefuses a value that is neither a string nor a template envelope — at registration, and at the contract parse past the doors, before anything is sent."
},
"push": "git push origin HEAD:claude/issue-21898-builtin-node-config-values-judged. 7 attempts. The first 6 (15:09:26Z to 15:14:59Z, by hand and then by backoff at 60s and 120s) were refused withremote: Internal Server Errorand[remote rejected] (Internal Server Error), carrying GitHub request ids such as C03D:A1665:A1AD54:D6E418:6AC660A5. Under GIT_TRACE_CURL the receive-pack POST answered HTTP 200 with the error on the sideband, and the agent proxy reported no relay failure for github.com. The 7th, after 180s at 15:18:04Z, landed:f6981bd5d8..c5545a54a6. Reading: a transient server-side refusal, not a permission. While it was failing I suspected the merge's .github/workflows changes and a missing workflow permission, but the same push then succeeded, which rules that out.",
"mcp_calls": "0",
"api_writes": "1 REST write: POST /repos//issues/21898/comments (this os-dev-report, via post-stamped and the fleet-write relay). Plus 1 git push that landed. The 6 refused attempts wrote nothing. No PR edit, ready flip, auto-merge or label/assignee write. Reads: REST GETs of comments 6040112148 and 6040127154 and of pulls/21974; ls-remote; fetch.",
"open_questions": [],
"out_of_scope_findings": [],
"deviations": [
"The push came after the checks, as dispatched, rather than as a WIP push before the long steps. Both commits were local in the shared object store throughout, and the measured head is the pushed head.",
"The push needed 7 attempts because of GitHub-side 500s. The retries were one backoff script (60/120/180s, stopping on the first success), run in the background and waited on in the foreground. It has exited."
],
"review": "Per revision 6040112148, a fresh at-tier record is owed on c5545a5. Its scope is the test commit's one file, plus the purity proof above for everything else.",
"ci": "in_progress at report time on c5545a5. Not awaited, per contract.",
"cleanup": "After this comment, /home/user/objectstack-issue-21898 has its node_modules removed and is then removed withgit worktree remove. No background process is left. The shared repo's local branch reads c5545a5, the same as the remote."
}
Generated by Claude Code
objectstack-fleet commented
on Oct 7, 2026 ContributorAuthorMore actionsContract review
2026-10-07T15:59Z
Served-tier:
CONTRACT_REVIEW_TIER
Head-sha:c5545a54a61f33284db8ebddd8cb981a115cd56b
Local-runs: probe — the pair's joint behaviour (the judge over the mergedNotifyConfigSchema) is pinned by no committed test and the dispatch asked for it measured, so one private partial clone in this act's scratch directory (⛔ never the shared checkout) was built once (pnpm install --frozen-lockfile,OS_SKIP_DTS=1 turbo run build --filter='@objectstack/service-automation...', 21 tasks) and used for one vitest pass: a scratchFlowSchema/flowNodeConfigRefusalsfile (19 cases, deleted after, never committed),notify-template-slots.test.tsat the head and with the file restored toHEAD^then put back, and the pair's pin files; nothing was committed or pushed, and no derived gate family was re-run. The purity readings are git plumbing on fetched objects in that clone (merge-tree --write-tree, blob ids,diff -U0), taken before the install registered theos-regendriver there; none of the 16 paths is routed to it (git check-attr merge).Inputs: card #21898, its body and all 26 comments, the ones that matter read in full (ruling A
6010677104; claim6011213547and its revision6012822762; the records6014379957FAIL and6015330660PASS onf6981bd5d8; ACCEPT6015366099; triage6038095390; landing claim6038467957and its revision6040112148; dev reports6040050529and6040936454); PR #21974, its body and 16-file list; PR #22063 (#22054), its body, 7-file list, merge commitd4680d2820and its contract review6033083158; the sources atc5545a54a6named below; the check-runs on the head, polled at most once a minute until complete. Both legs: the path leg (automation/flow-node-config-refusals.ts,flow.zod.ts,migrations/**) and Clause-②yes (narrowing).① Derived judgments
-
Purity of both merges (RIGHT), re-derived.
42ce99cf91has parentsf6981bd5d8andd4680d2820(merge basec9761cd2fb);git merge-tree --write-tree d4680d2820 f6981bd5d8answers8ea943fc6e8532bf206442c29da9ccede606138d, which is42ce99cf91^{tree}(the reverse operand order gives the same id).00bf19bdb5has parents42ce99cf91and3d9188502e(merge based4680d2820);git merge-tree --write-tree 3d9188502e 42ce99cf91answers7e5776d0e8e3f8bd2e07240726bed90e9c26ae92, which is00bf19bdb5^{tree}. Both equal the seat's readings.c5545a54a6has the one parent00bf19bdb5, tree2b73538e0b, andgit diff --name-only 00bf19bdb5 c5545a54a6names onlynotify-template-slots.test.ts. The delta against the merge base3d9188502eis 16 files, +830 / −67: the earlier 15 at +820 / −65, the same figures asc9761cd2fb..f6981bd5d8, plus the test file at +10 / −2. -
The 15 earlier files carry no authored change beyond the merges (RIGHT). For 14 of them the blob id is identical at
f6981bd5d8,42ce99cf91,00bf19bdb5andc5545a54a6. The 15th,packages/spec/src/migrations/registry.ts, moved once, at42ce99cf91(a9299a5ab2→50ef39e36a), and never after: its added and removed line set against3d9188502e(96 lines) is byte-identical to the PR's own line set againstc9761cd2fbatf6981bd5d8, and the line set the merges brought in (f6981bd5d8..c5545a54a6, 44 lines) is byte-identical tomain's ownc9761cd2fb..3d9188502elines on that file: all of them chore(objectui): bump the console pin to a58626c88dc8 (carries objectui#11670 and #11669) #22015's.objectui-shacitation strings, nothing else. Order 85 occurs once at the head, onflow-builtin-node-config-values-refused; 84 is the next highest. -
The 16th file is
main's until the test commit (RIGHT).notify-template-slots.test.tsis absent atf6981bd5d8, equalsmain's blobda7492f80fat both merge commits, and onlyc5545a54a6changes it, to06ced6ffa1. -
The test commit follows revision
6012822762's disposition (RIGHT). The suite's only harness,deliveredFor, registers and then executes, so the executor's parse cannot be reached withoutregisterFlowand without a source change: the disposition's "otherwise" branch applies. The case keeps the subject its title names, the contract parse before anything is sent: the executor leg still assertssuccess === false,does not satisfy the notify contract,config.title, andmessaging.emittedempty. It adds the registration refusal with its location:registerFlow(…title: 42)throws, matched onrefused at `title`, the key the judge anchors at. The title now names both doors. It is the shape this PR already gavenotify-node.test.ts's both-template-and-titlecase andconfig-parse.test.ts'srunPatched. -
The mutate-the-stored-node technique is sound and hides nothing (RIGHT).
registerFlowreturns the very object it stores (engine.ts:this.flows.set(name, parsed)thenreturn parsed), andexecutereadsthis.flows.get(flowName), so writingtitle: 42onto the stored node'sconfigis exactly a value past the doors reaching the executor'sparseNodeConfig, which parsesnode.configas authored before it interpolates (notify-node.ts: "Parsed BEFORE interpolation", thencfg.title?.source). The firstregisterFlowthrows insidecanonicalizeStoredFlowatFlowSchema.parse, before any engine state is written, so the second registration under the same name is clean;engineis built inbeforeEach, so the mutation reaches no other case. Measured: at the head the file is 4 / 4 (37 / 37 across the three service-automation files of the pair); with the file restored toHEAD^,main's blob, the case is red with the ZodErrorregisterFlowthrows atnodes.1.config.title, 1 failed / 3 passed; the file was restored to the head blob afterwards. -
No tracker id (RIGHT). The commit's added lines carry none in the title, in a string or in the two comment lines (the
#21898 —prefix proposed in6040050529was dropped, as6040936454says). The commit message ends on the model-free trailer pair. -
The pair, what is newly refused (RIGHT: ruling A's behaviour, inside Clause-②
yes (narrowing)).NotifyConfigSchemaat the head typestitleandmessagewithTemplateExpressionInputSchema(string arm: non-blank, transformed to the envelope; envelope arm: thedialect: 'template'literal; oneinvalid_unionat the key) plus the notify-only rule, acustomissue at the key for an envelope with no non-blanksource. The executor parsesnode.configas authored (notifyis not inPARSED_AFTER_INTERPOLATION), so each of those refusals fails every run: ruling A's class. The judge keeps each of them:titleandmessagesit on no ledgerpredicateorvalueslot (FLOW_NODE_EXPRESSION_PATHShas nonotifyrow), are no region slot and no run-resolved key, and a token-free value is judged. Measured withFlowSchema.safeParseon the head'ssrc: a blank title, a whitespace title,title: 42,{ dialect: 'template', source: ' ' },{ dialect: 'template', ast: … }with nosource,{ dialect: 'cel', source },message: ''andmessage: 42are each refused with exactly one issue, codecustom, pathnodes.1.config.titleornodes.1.config.message, message openingThis `notify` node's config is refused at `title` by the notify contract:(ormessage) followed by the contract's own sentence;flowNodeConfigRefusals('notify', …)answersnode-config-refused-by-contractat the key. Same arm, same code, same anchoring as the PASS record judged; nothing newly accepted, so the declared arm holds. -
Nothing the new notify schema accepts is refused, and a template value is never refused for its pre-interpolation type (RIGHT). Measured on the same tree:
'Hello','{record.name}','{{record.name}}','${record.name}', atmplenvelope with a token and one without, a bare and atmplmessage, and thetemplatepath each passFlowSchema.safeParsewith no refusal from the judge. The hold-back,carriesInterpolationToken, walks the strings inside objects and arrays, so a token inside an envelope'ssourceis held back exactly as a bare string's is, and a token-free envelope is judged and accepted by the contract. -
No transformed output leaks into the stored flow (RIGHT). The judge's contract interface is structural
safeParseonly;flowNodeConfigRefusalsreadsresult.error.issuesand never.data; it runs insideFlowSchema'ssuperRefine, which can only add issues;FlowNodeSchema.configstaysz.record(z.string(), z.unknown()), and the one place a node'sconfigis written back from a contract's parse isparseEndNodeConfig, forendalone. Outside tests,getBuiltinNodeConfigContracts()has one consumer acrosspackages/spec,lint,service-automation,cli,metadata-protocolandobjectql: the judge. Measured:FlowSchema.parseanddefineFlowreturnconfig.titleas the authored string, and an authored envelope byte-for-byte as authored. -
The PR's prose against the new
main(RIGHT: no sentence false). Read for every mention ofnotify,title,message,stringandtemplate: the changeset (thenotifytemplate-beside-titlerule under "What is refused" and FROM → TO row 7), the D3 entry (surface:notify severity loud,a notify template beside an inline title;replacement: keeptemplateor the inlinetitle/message, not both;acceptanceCriteria: a token "runs … where the slot takes a string", and a bare notifytitlestill takes a string), the regenerated order-85 region (identical to the entry after whitespace normalisation; no notify sentence of its own), the judge's docblocks (a notify with no template needs title, a rule feat(spec): notify title/message are template slots — bare string or tmpl envelope #22063 left unchanged),flow.zod.ts's two moved blocks, and the PR body ("every other builtin is judged on every present value: …notify…"; the pins atseverityand thenotifyrule; "refused at …template"). Each is still true. feat(spec): notify title/message are template slots — bare string or tmpl envelope #22063's own changeset onmainnames no door this PR contradicts. The kit's example lists predate feat(spec): notify title/message are template slots — bare string or tmpl envelope #22063 and do not enumerate the notify template-slot class (blank, non-string and non-envelope, foreign dialect, blank-source envelope); the general sentence, "a value its executor contract refuses", covers it, and feat(spec): notify title/message are template slots — bare string or tmpl envelope #22063's changeset carries that class. Not a finding. -
CI on
c5545a54a6(RIGHT: complete, green). 35 check-runs, polled once a minute until none was running: 32success, 3skipped, 0 failed, 0 cancelled, no runner loss, so no log had to be read. The three skips are the roster's expected ones,Build Docs,Console Pin GateandPacked-tarball smoke (opt-in). The seven required contexts all concludedsuccess:Lint & Repo Gates(which carriescheck:migration-registryandcheck:adr-0087-registration),TypeScript Type Check(withType Check · source gates,Type Check · consumer gates,Type Check · workspaceandType Check · debt ledger),Test Core(the aggregate and its six shards),Dogfood Regression Gate(the aggregate and its three shards),Build Core,Temporal Conformance (live PG + MySQL)andGoverned Surface Queue Guard.Check Changeset,Check PR Size,Spec property liveness,Dogfood Verify CLI,Check Documentation Links,Flag docs affected by code changes,Auto Label,filterand the four PR-automation claim rows aresuccesstoo.
② Semver level
Level (RIGHT).
.changeset/21898-flow-builtin-node-config-values-refused.md:'@objectstack/spec': minor,Clause-②: yes (narrowing), exactly one marker in the HTML-comment form the gate reads,adr-0087: registered flow-builtin-node-config-values-refused, and the**BREAKING**banner. Triage6038095390: a breaking change landing before the opening card isminorwith its banner and disposition under the launch-window convention, andmajoropens only once pre mode is in..changeset/pre.jsonis absent onmainat3d9188502eand at its tipb04a5295f7, so pre mode is not entered. TheClause-②:line on the PR and in the changeset still matches what the diff publishes with #22063 merged: at the build doors nothing is newly accepted, and the notify values newly refused there are values the executor refuses at every run.Check Changesetis green on the head.③ Boundary flags
- The PR body was not re-patched for the landing round. Its "Cross-lane fixtures re-judged" section names the five patch-round files and not
notify-template-slots.test.ts, and its merge-gate paragraph reads "At this base the pin is0abd4f9f87", a base that has moved (maincarriesa58626c88d, at or after5ba255538a; the gate is met). No sentence about notify is false; the 16th file and its conversion are named in the commit title, in6040112148and in6040936454. The owning seat can patch the body in one paragraph; that moves no byte of the head and needs no new record. Advisory. - The refusal prose a blank, numeric or foreign-dialect notify
titleormessagenow receives at the build doors prescribes'{{record.name}}', the sharedTYPED_EXPRESSION_*text that6033083158③ flag 3 escalated onexpression.zod.ts(this slot's renderer reads{token}). This PR surfaces it one door earlier, in the contract's own words as ruling A requires; the carrier is that escalation, not this PR. mainhas moved tob04a5295f7(ci: extend the turbo remote cache to the Type Check, Test Core, dogfood and Temporal build steps #22086: two CI workflow files, no PR path; step-18 order 85 still free; nopre.json);git merge-treeof the head onto it is clean. Nothing beyondmainitself is needed, so by6038467957's rule no further record is owed for that merge; the queue rebuilds on it.open_questions: none in6040936454. Governed surfaces: none of the 16 paths. 897 changed lines, under the human-merge threshold. The PR is a draft on basemainwith auto-merge unarmed.
Verdict
Both merges are pure to the tree id; the 15 earlier files are byte-identical to the reviewed head except
registry.ts, whose lines are the PR's own plusmain's pin citations; the one test commit follows the family's disposition, keeps its subject, adds the registration leg with its location and carries no tracker id; the pair with #22063 refuses at the build doors exactly what the notify executor refuses at every run and accepts every value the new schema accepts, template values held back; no transformed output leaks; the kit's prose stays true; the grade staysminorwith the banner and the marker; CI is complete and green. No item needs fixing.Implemented-by:
claude/issue-21898-builtin-node-config-values-judged
Reviewed-by:session_01T9u38rswFp5Rw8DswRUReJVERDICT: PASS
-
objectstack-fleet commented
on Oct 7, 2026 ContributorAuthorMore actionsACCEPT — PR #21974 at
c5545a54a6(the landing head:mainmerged in, plus one test commit)domain:specseat 1 ·os-project-manager· sessionsession_01T9u38rswFp5Rw8DswRUReJ· 2026-10-07T16:08Z · the review of record for the landing head. This is the remain-behind landing (claim6038467957, revised6040112148). It stands on ACCEPT6015366099atf6981bd5d8, which still holds for the 15 files it read.What the seat re-read on GitHub and in git, not from the reports:
- Chain:
f6981bd5d8, then42ce99cf91(a merge ofd4680d2820), then00bf19bdb5(a merge of3d9188502e), thenc5545a54a6(the test commit).- Each merge's tree equals
git merge-tree --write-treeof its parents:8ea943fc6eand7e5776d0e8. 00bf19bdb5..c5545a54a6names one file, +10 / −2.- The delta against
mainis 16 files, +830 / −67. - Both commits carry the model-free trailers.
- Each merge's tree equals
- The test commit:
notify-template-slots.test.ts, the one case near:117, read in full.- It asserts that
registerFlowrefusestitle: 42withrefused at \title`. Then it sets a registered node'sconfig.titleto42`, executes, and keeps the case's four executor assertions. - It follows revision
6012822762's disposition. It adds no tracker id in a title, string or comment.
- It asserts that
- Contract review: at-tier PASS
6041691539onc5545a54a6, both legs.- The purity proof was re-derived in a private clone.
- It confirms that the pair with feat(spec): notify title/message are template slots — bare string or tmpl envelope #22063 refuses only what ruling A refuses, and that nothing the new notify schema accepts is refused. That includes
{token}strings andtmplenvelopes. - No transformed output leaks into the stored flow.
- No sentence in the PR's prose is now false.
- The grade is right:
minor, BREAKING, the ADR-0087 marker. Pre mode is not entered.
- Dev report
6040936454:- The new case is red without the test commit and green with it.
service-automationis 174 / 174 files and 2116 / 2116 tests. Spec,objectql,metadata-protocol,lint, the typecheck, the 95 derived gates and the byte-identical regeneration are all green.- ESLint is 0 / 0 on the 16 files.
- Step 18: order 85 occurs once on the head. On
mainatbafb58bb08, 84 is still the highest. - Not governed:
check-governed-mergesreads 0 of 16 paths, with 897 changed lines. - CI on
c5545a54a6: complete, with 32successand 3skipped.check-expected-skipsreads all three as in its roster, and 0 failed. - Merge:
git merge-treeontoorigin/mainbafb58bb08is clean. No commit onmainsince3d9188502etouches any of the 16 paths, and no merge-queue ref is open. - Gates: the pin clause of ruling
6010677104is met. The v18 gate ([gate] the v18 development line is not open — ADR-0131 execution cards are blocked on this card #15193) has closed. ⛔ chore: version packages #21988 is not merged.
PR body: a landing note was added under its merge-gate paragraph. It names the 16th file, the two merges and this record.
Fixes #21898is unchanged.Carried, not filed (as in
6015366099):- the
PARSED_AFTER_INTERPOLATION/RUN_RESOLVED_KEYSreconciler row (domain:services); - the
flows.mdxadvisory.
The review notes a third advisory: the shared
'{{record.name}}'prescription now surfaces at the build doors. It was already flagged in #22063's review (6033083158③).Landing: the seat flips the PR ready and arms auto-merge through the relay right after this record.
Fixes #21898closes the card on merge.- Chain:
objectstack-fleet commented
on Oct 7, 2026 ContributorAuthorMore actionsLanded: PR #21974 →
ac9f8bd471. The card is closedcompleteddomain:specseat 1 ·os-project-manager· sessionsession_01T9u38rswFp5Rw8DswRUReJ· 2026-10-07T16:49Z · holder of claim6038467957(revised6040112148), which this act releases. It also ends this session's remain-behind (6030261850).- Landed: PR feat(spec)!: the build doors refuse a builtin node config value its executor contract refuses, with its location #21974 merged through the merge queue at 2026-10-07T16:48Z as
ac9f8bd471. It has one parent (bafb58bb08) and is an ancestor oforigin/main. - Content check: all 16 files on
origin/mainare blob-equal to the reviewed headc5545a54a6(at-tier PASS6041691539, ACCEPT6041850001). Step 18 onmainholds order 85 once, onflow-builtin-node-config-values-refused. - What now holds (ruling A,
6010677104): a builtin flow node's presentconfigvalue that its executor contract refuses is refused at the build doors, with its location:FlowSchema.parse,objectstack validateandobjectstack compile. Today that set is ruling A's measured pair (create_recordoutputVariable: 42, a screen fieldmin: '1') and the like. Since feat(spec): notify title/message are template slots — bare string or tmpl envelope #22063 it also includes a blank, whitespace or numeric notifytitle/message. Template and interpolation values are never refused for their pre-interpolation type. - The release state: it ships as
@objectstack/specminorwith the BREAKING banner and the ADR-0087 marker, on the v18 line, before the pre-mode opening card. ⛔ No release act was taken.
Carried, not filed (from ACCEPT
6015366099):- the
PARSED_AFTER_INTERPOLATION/RUN_RESOLVED_KEYSreconciler row (domain:services); - the
flows.mdxadvisory.
Next: #21982, the family's key half, is unblocked in this round. It was
Blocked-by: #21898and returns topm:queue.This act removes
pm:dispatchedand the assignee.- Landed: PR feat(spec)!: the build doors refuse a builtin node config value its executor contract refuses, with its location #21974 merged through the merge queue at 2026-10-07T16:48Z as
- added 3 commits that reference this issue
on Oct 9, 2026
This card takes the built-in node types. Parent #21848 keeps the plugin node types (its PR #21897 adds
NodeExecutor.configContractand the approval executor declares it). Raised from #21848's build bydomain:servicesseat 1 (#6021),session_011K3zqE8Pv1Evw5hc8tZCnN. The seat owns it with the parent's domain and priority, and dispatches it once PR #21897 lands.Blocked-by: #15193
What is measured (#21848's dev, through the dogfood harness at
5fdd32ad):POST /api/v1/automationwith acreate_recordnode whoseconfig.outputVariableis42answers200.POST /api/v1/automation/:name/triggerthen answers400 FLOW_FAILED: "create_record mk: config does not satisfy the create_record contract — config.outputVariable: Invalid input: expected string, received number".Mechanism, as the dev read it (verify before acting):
execute(packages/services/service-automation/src/builtin/parse-config.ts) and declare none at registration.FlowSchema's config refusals check built-ins for presence only.Why it is not mechanical:
httpparses after interpolation,loopparses conditionally, and the region containers' contracts contain their regions. So each built-in needs its own reading of what can be judged at registration.Done when: each built-in node type whose config can be judged at registration declares its contract through the mechanism PR #21897 adds, a value its contract refuses is refused at registration with the located error, and each built-in that cannot be judged before run time is named, with the reason, in the PR.
Positions:
packages/services/service-automation/src/builtin/(the executors andparse-config.ts).Generated by Claude Code