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
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
QA-source: #21845 · approvals.sla-escalation · outside the item's clauses
Found in the follow-up run #21845 (subject 316be321e) and reproduced by an independent verifier (RUNNER rule 7) through a second door (an authored package): medium, predates 17.6.0.
Reproduction
Register (admin POST /api/v1/automation, or ship in a package) an active record-triggered flow whose approval node has escalation {enabled:true, timeoutHours:0.5, action:'notify'} → 200 / loaded active.
Create a record that matches the trigger → 201.
Expected: registration refuses the value (the schema says timeoutHours >= 1).
Actual: every trigger-fired run fails with Approval node 'gate' has invalid config: escalation.timeoutHours: Too small…; no approval request opens; the record exists without the gate and the user sees nothing.
Mechanism
packages/services/service-automation/src/engine.ts (~10425-10480) checks only undeclared config key names at registration; values are parsed only at execute (packages/plugins/plugin-approvals/src/approval-node.ts:133 against approval.zod.ts:666).
Done when
Registration (and package load) runs the node's full config schema and refuses invalid values with a located error; a test covers the 0.5-hour case.
Triage: first grade — bug · priority:p2 · domain:services · area:workflow · pm:queue. Registration and package load run the node's full config schema
Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-05T08:57Z. ⛔ Not a claim, ⛔ not a dispatch.
Triage: lands in packages/services/service-automation/src/engine.ts (registration, about :10425–:10480) ⇒ domain:services; rationale: registration checks key names only, so an invalid value is accepted and fails every run.
Why p2. A flow with an invalid approval escalation registers as active. Every triggered run then fails, and the record is created without its approval gate, while the user sees nothing. A control silently does not hold. It predates 17.6.0.
Direction. Registration (the admin door and package load) parses each node's config with the same schema execute uses (approval.zod.ts:666 for the approval node) and refuses with a located error. ⛔ No second, hand-written value check.
Pins:timeoutHours: 0.5 is refused at registration through both doors, and a valid escalation still registers and runs.
Claim: PM loop round 1 (17.7 pre-release follow-up, dispatched on the maintainer's direct order)
Session: session_018zT8d8NpiQ1ExhuNd5TxY6
Account: hotlong (the seat's linked user as GET /user answers it; always the card's assignee)
Branch: claude/issue-21848-escalation-config-values
Worktree: objectstack-issue-21848
Domain: domain:services (card label, as triage set it)
File surface: packages/services/service-automation/src, .changeset/
Container & model: M, mode:subagent, model: opus (default tier; no path-derived mandate)
Clause-②: no
Thread-read: 5991301033
Serial constraints cleared: none named
Provenance: the maintainer, in Claude Code session session_018zT8d8NpiQ1ExhuNd5TxY6, 2026-10-05, verbatim: 「都开单派发」; landing per the standing order 「开发完整就进队列合并」. Dispatched one at a time within the session's load cap. Where the card withholds detail, this session holds it and the dispatch carries it privately.
Release: session session_018zT8d8NpiQ1ExhuNd5TxY6 · cause: re-route before dispatch (no dev was started on this card) · destination: back to pm:queue for the domain:services lane seat to claim.
Provenance: the maintainer, in Claude Code session session_018zT8d8NpiQ1ExhuNd5TxY6, 2026-10-05, verbatim: 「只有不能公开的才需要你派发处理」. This card withholds nothing, so its dispatch belongs to the lane. The claim above is withdrawn, the assignee is cleared, and the triage grading stands.
Claim: PM loop round 3 · 2026-10-05T15:04Z
Session: session_011K3zqE8Pv1Evw5hc8tZCnN
Account: os-steve (the seat's linked user as GET /user answers it; the card's assignee)
Branch: claude/issue-21848-node-config-values-at-registration
Worktree: objectstack-issue-21848
Domain: domain:services
Seat: domain:services#1 (seat post #6021)
File surface (at origin/maine864db56), per triage's direction 5991301033:
packages/services/service-automation/src/engine.ts (registration, about :10425–:10480): registration, through the admin door and on package load, parses each node's config with the same schema execute uses (for the approval node, the escalation schema in approval.zod.ts). It refuses an invalid value with a located error.
service-automation tests, plus a door pin in a NEW file under packages/qa/dogfood/test/ (declared cross-lane on [PM seat] domain:cli — ⏳ vacant #6024 in this act).
content/docs/** sentences this makes false, and a minor changeset with the ! banner.
⛔ No second, hand-written value check: the node's own schema is the one judge. ⛔ No edit to the build door (objectstack validate / compile): that is #21850, claimed in domain:spec. ⛔ No change to what execute accepts.
Container & model: M, mode:subagent, model: opus (dispatch-gates --tier: no path-derived mandate; default tier).
Clause-②: no (narrowing)
A flow whose node config breaks its node's schema used to register and load as active, then fail every run (an approval node's record was created with no approval gate). It is now refused at registration and on package load. An authored package that loaded before can stop loading after upgrade. The changeset says so, with the handling: correct the value the error locates.
Thread-read: 5995690371
Serial constraints cleared: at 2026-10-05T15:04Z:
The earlier claim by session_018zT8d8NpiQ1ExhuNd5TxY6 was released before any dev started (5995690371), on the maintainer's routing 「只有不能公开的才需要你派发处理」.
{
"issue": 21848,
"status": "done",
"branch": "claude/issue-21848-node-config-values-at-registration",
"pr": "#21897",
"session": "session_011K3zqE8Pv1Evw5hc8tZCnN — the parent PM session (this run is a subagent); the harness Claude-Session line names the same id",
"premise_still_valid": true,
"summary": "Registration now parses a node's config VALUES with the contract its executor parses at run time: NodeExecutor gains an optional configContract, AutomationEngine.registerFlow runs validateNodeConfigValues after the key-name check and refuses the flow with a located error of the key-name refusal's class (flow, node id and type, config path, the contract's own sentence; 400 VALIDATION_FAILED at the /automation doors), and the approval executor declares ApprovalNodeConfigSchema, the very object its execute parses. Measuring H5 found the package-load door could not hold any boot refusal of a plugin node: the boot pull registers package flows before ApprovalsServicePlugin registers the approval executor, and the kernel:ready bind's refusal only WARNed while the boot pull's registration stayed active and bound (measured on main with an escalation.bogusKey flow); the cold-boot bind now withdraws a flow it refuses, so a refused flow is not registered whichever boot step refused it, and only the flow, never the package, is refused. Every other node type (all builtins, approval_revise, third-party types) declares no contract and keeps key-name-only judgement; no D3 entry and no spec edit: the changeset declares not-required (no-migration-prescription).",
"tests": "All at 5fdd32a (final): service-automation src/node-config-values-at-registration.test.ts 7 passed; plugin-approvals src/approval-node-config-contract.test.ts 2 passed; dogfood test/flow-node-config-values-at-registration.dogfood.test.ts (--project isolated) 5 passed; typecheck service-automation + plugin-approvals both Done (tsc + check:test-typecheck); dogfood typecheck exit 0 with the new file in the program (tsc --listFilesOnly). Full suites at 2ef0c61 (code byte-identical at HEAD: git diff 2ef0c61..HEAD -- packages/services packages/plugins packages/qa empty): service-automation 173 files / 2117 tests passed, plugin-approvals 61 files / 897 tests passed. RED first: the dogfood pin on unmodified main code (tree = 07c842d + the pin, committed as be7450c) gave 3 failed / 2 passed (package GET 200 active, no located boot line, admin POST 200; controls green). Ablation A (validateNodeConfigValues call replaced by a marker via scripts/ablation-replace.mjs, anchor 1->0, blob b5e94fb23b67->bd7099e9875b; service-automation rebuilt in the leg, ablation-dist-preflight: marker in 2 dist files; DTS step exit 1 on TS6133 only, ESM/CJS emitted): service-automation 5 failed/2 passed (controls), plugin-approvals 1 failed/1 passed, dogfood 3 failed/2 passed. Ablation B (cold-boot withdraw replaced, anchor 1->0, blob fb06fd8ad148->0d523279cf1c, preflight marker present): service-automation 1 failed (boot pin)/6 passed, plugin-approvals 2 passed, dogfood 1 failed (package 404)/4 passed. Restore both legs: blob after restore == HEAD blob, git diff HEAD empty; rebuild exit 0; ablation-dist-preflight --absent A=0 B=0 (absent from all 6 dist files, tree clean). Census: 36 flows / 19 approval nodes across showcase, crm, multi-package and the dogfood approval fixtures, 0 refused by ApprovalNodeConfigSchema. ESLint narrowed to the 7 touched TS files (--no-inline-config --format json): 7 files, 0 errors, 0 warnings; config globs cover all 7; eslint.config.mjs enables no type-aware linting, so untouched files' verdicts cannot move; repo-wide lint is CI's.",
"mcp_calls": "0 — no MCP GitHub tool was called",
"api_writes": "3 relay strokes, each one POST /repos/objectstack-ai/objectstack/dispatches executed by fleet-write.yml as objectstack-fleet[bot]: (1) pr_create -> POST /repos/objectstack-ai/objectstack/pulls (draft #21897, run 37341288483); (2) label-write --assign os-steve -> POST /repos//issues/21897/assignees (run 37341393259; zero labels written); (3) this os-dev-report comment -> POST /repos//issues/21848/comments via post-stamped.mjs. git push is not REST. Reads: gh api GET of the card, its comments, #21850 and its comments, the PR read-back.",
"open_questions": [],
"out_of_scope_findings": [
"class: a · reach: POST /automation with a create_record node carrying config.outputVariable: 42 answered 200, then POST /automation/:name/trigger answered 400 FLOW_FAILED 'create_record mk: config does not satisfy the create_record contract — config.outputVariable: Invalid input: expected string, received number' (measured through the dogfood harness at 5fdd32a) · evidence: the builtin executors parse their Zod contract inside execute (builtin/parse-config.ts) and declare none at registration, and FlowSchema's flowNodeConfigRefusals judges builtins on presence only, so a present builtin value its contract refuses registers and fails every run — the builtin half of this card's own class; wiring them is not mechanical (http parses after interpolation, loop parses conditionally, region containers' contracts contain their regions) · family: same family as #21848, for the seat to fold into that family's close-out rather than a single-point card · dedupe words: builtin node config value registration, configContract builtin executors, create_record outputVariable wrong type registers, flow registers then fails every run",
"carrier: #21850's PR (branch claude/issue-21850-approval-node-config-build-refusal) · noted, not filed: that card's repro step 3 says the unknown-key approval flow is 'dropped with only a WARN' at boot; measured on main it was NOT dropped (it stayed active and bound behind the kernel:ready WARN); this PR's cold-boot withdraw makes it dropped. Its WIP also judges the approval contract whole inside FlowSchema, which registerFlow parses first, so whichever lands second reconciles (this PR's pins assert status, code and the located path only).",
"carrier: 承接者:无 · noted in the PR's Acceptance notes only: a third-party executor registered from a kernel:ready handler that runs after this plugin's cold-boot bind would leave a boot-pulled flow unjudged until its next registration; no in-repo node type does this (builtins at init, approval at start). Not measured further.",
"carrier: 承接者:无 · noted in the PR's Acceptance notes only: the metadata save door (PUT /meta/flow/:name) re-arms through the mutation sync, whose refusal handling this PR does not change. Not measured."
],
"gates": {
"derived_at": "5fdd32adc9 (node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, no paths)",
"derived": [
"node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0",
"node scripts/check-adr-0087-registration.mjs --self-test :: exit 0",
"node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0",
"node scripts/check-changeset-no-major.mjs --self-test :: exit 0",
"node scripts/check-ci-filter-parity.mjs :: exit 0",
"node scripts/check-closing-keyword-parity.mjs :: exit 0",
"node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0",
"node scripts/check-comment-mask-adoption.mjs :: exit 0",
"node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0",
"node scripts/check-comment-mask-corpus.mjs :: exit 0",
"node scripts/check-doc-frontmatter.mjs :: exit 0",
"node scripts/check-doc-frontmatter.mjs --self-test :: exit 0",
"node scripts/check-doc-route-spelling.mjs --advisory :: exit 0",
"node scripts/check-doc-route-spelling.mjs --self-test :: exit 0",
"node scripts/check-docs-section-name.mjs :: exit 0",
"node scripts/check-docs-section-name.mjs --self-test :: exit 0",
"node scripts/check-dts-emitted.mjs --self-test :: exit 0",
"node scripts/check-empty-changeset.mjs --base origin/main :: exit 0",
"node scripts/check-empty-changeset.mjs --self-test :: exit 0",
"node scripts/check-issue-citations.mjs :: exit 0",
"node scripts/check-keyed-text-bounds.mjs :: exit 0",
"node scripts/check-keyed-text-bounds.mjs --self-test :: exit 0",
"node scripts/check-platform-object-tenancy-census.mjs :: exit 0",
"node scripts/check-platform-object-tenancy-census.mjs --self-test :: exit 0",
"node scripts/check-plugin-teardown-shape.mjs :: exit 0",
"node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0",
"node scripts/check-registry-log-declared.mjs :: exit 0",
"node scripts/check-registry-log-declared.mjs --self-test :: exit 0",
"node scripts/check-rest-log-spy-declared.mjs :: exit 0",
"node scripts/check-rest-log-spy-declared.mjs --self-test :: exit 0",
"node scripts/check-section-landing-index.mjs :: exit 0",
"node scripts/check-section-landing-index.mjs --self-test :: exit 0",
"node scripts/check-system-context-census.mjs :: exit 0",
"node scripts/check-system-context-census.mjs --self-test :: exit 0",
"node scripts/check-tenant-audit-census.mjs :: exit 0",
"node scripts/check-tenant-audit-census.mjs --self-test :: exit 0",
"node scripts/check-undeclared-dep-imports.mjs :: exit 0",
"node scripts/check-undeclared-dep-imports.mjs --self-test :: exit 0",
"node scripts/docs-audit/check-affected-docs.mjs :: exit 0",
"node scripts/docs-audit/check-drift-comment.mjs :: exit 0",
"node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0",
"node scripts/release-pending-publish.mjs --self-test :: exit 0",
"pnpm --filter @objectstack/lint run check:doc-formula-expressions :: exit 0",
"pnpm --filter @objectstack/lint run check:doc-security-posture :: exit 0",
"pnpm --filter @objectstack/spec run check:docs :: exit 0",
"pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0",
"pnpm --filter @objectstack/spec run check:empty-state :: exit 0",
"pnpm --filter @objectstack/spec run check:liveness :: exit 0",
"pnpm --filter @objectstack/spec run check:skill-examples :: exit 0",
"pnpm --filter @objectstack/spec run check:strictness-ledger :: exit 0",
"pnpm --filter @objectstack/spec run check:variant-docs :: exit 0",
"pnpm --filter @objectstack/spec run check:yaml-examples :: exit 0",
"pnpm check:changeset-gate-self-tests :: exit 0",
"pnpm check:corpus-claim-drift :: exit 0",
"pnpm check:cross-package-test-inputs :: exit 0",
"pnpm check:dispatcher-error-vocabulary :: exit 0",
"pnpm check:doc-anchors :: exit 0",
"pnpm check:doc-authoring :: exit 0",
"pnpm check:docs-audit-scope :: exit 0",
"pnpm check:docs-redirects :: exit 0",
"pnpm check:docs-single-h1 :: exit 0",
"pnpm check:docs-spec-enumerations :: exit 0",
"pnpm check:docs-transcript-drift :: exit 0",
"pnpm check:driver-memory-census :: exit 0",
"pnpm check:dts-closure :: exit 0",
"pnpm check:dual-build-cjs-loads :: exit 0",
"pnpm check:engine-double-contract :: exit 0",
"pnpm check:gitlink-declared :: exit 0",
"pnpm check:i18n :: exit 0",
"pnpm check:i18n-stale-fill :: exit 0",
"pnpm check:issue-citations :: exit 0",
"pnpm check:lean-entry-closure :: exit 0",
"pnpm check:logger-receiver-detach :: exit 0",
"pnpm check:nul-bytes :: exit 0",
"pnpm check:objectql-double-limit :: exit 0",
"pnpm check:objectui-changeset :: exit 0",
"pnpm check:org-identifier :: exit 0",
"pnpm check:page-declaration-shape :: exit 0",
"pnpm check:pm-changeset-deadline-census :: exit 0",
"pnpm check:published-files :: exit 0",
"pnpm check:published-readme-links :: exit 0",
"pnpm check:query-options-erasure :: exit 0",
"pnpm check:react-page-adapter-contract :: exit 0",
"pnpm check:refd-timer-probe :: exit 0",
"pnpm check:role-word :: exit 0",
"pnpm check:skill-identifier-liveness :: exit 0",
"pnpm check:slot-lookup :: exit 0",
"pnpm check:sourcemap-no-sources-content :: exit 0",
"pnpm check:test-source-alias :: exit 0",
"pnpm check:tier-file-adoption :: exit 0",
"pnpm check:type-check-coverage :: exit 0",
"pnpm check:type-check-debt :: exit 0",
"pnpm check:vendor-version-stamps :: exit 0",
"pnpm check:watch-hint-literal :: exit 0",
"pnpm check:where-matcher :: exit 0"
],
"reruns": [
"node scripts/check-comment-mask-corpus.mjs — first run exit 1: ENOENT on a scratch probe file deleted mid-sweep (my own dogfood probe, removed before the sweep read it); rerun with no probe in flight exit 0",
"pnpm --filter @objectstack/spec run check:skill-examples — first run exit 3 PREREQUISITE NOT MET: packages/spec dist declarations predated src after the origin/main merge (spec test titles); spec rebuilt (turbo cache hit), rerun exit 0",
"pnpm check:dual-build-cjs-loads — first run exit 3 PREREQUISITE NOT MET: 8 packages outside this diff had no dist/ (studio, client-react, embedder-openai, knowledge-memory, knowledge-ragflow, organizations, service-cluster-redis, service-knowledge); built (41/41 cache hits), rerun exit 0"
],
"dispatch_named_beyond_derivation": [
"node scripts/check-dev-prereqs.mjs --self-test :: exit 0",
"node scripts/check-spec-docblock-symbol-anchors.mjs :: exit 0",
"node scripts/check-spec-docblock-symbol-anchors.mjs --self-test :: exit 0",
"pnpm --filter @objectstack/spec run check:api-surface :: exit 0",
"pnpm --filter @objectstack/spec run check:browser-reachable-entries :: exit 0",
"pnpm --filter @objectstack/spec run check:dual-source-exports :: exit 0",
"pnpm --filter @objectstack/spec run check:entry-nameability :: exit 0",
"pnpm --filter @objectstack/spec run check:export-origins :: exit 0",
"pnpm --filter @objectstack/spec run check:exported-any :: exit 0",
"pnpm --filter @objectstack/spec run check:llms-txt :: exit 0",
"pnpm --filter @objectstack/spec run check:migration-registry :: exit 0",
"pnpm --filter @objectstack/spec run check:objectui-pin-citations :: exit 0",
"pnpm --filter @objectstack/spec run check:skill-refs :: exit 0",
"pnpm check:merge-driver :: exit 0",
"pnpm check:pm-prior-rulings :: exit 0",
"pnpm check:pm-widening-tells :: exit 0",
"pnpm check:spec-parsed-alias :: exit 0"
],
"ran_verdict": "✓ dispatch-gates --ran: 95 derived famil(ies) accounted for — 95 run, 0 NOT-MEASURED, 0 UNRUN (exit 0)",
"files": "every exit code in scratchpad issue-21848/gates/results.tsv, named-results.tsv, ran.list, ran-verdict.log"
},
"line_budget": "n/a",
"deviations": [
"packages/services/service-automation/src/plugin.ts is edited, outside the expected landing (engine.ts plus, if needed, approval-node.ts): H5 measured that the kernel:ready cold-boot bind refused a flow with a WARN while the boot pull registration stayed active and bound, so the package-load door could not hold any refusal of a plugin node without the bind withdrawing what it refuses. Same package; declared in the PR body.",
"packages/services/service-automation/src/builtin/parse-config.ts: formatIssuePath is exported from that internal module so the value refusal spells paths as the execute-time refusal does. Not re-exported from index.ts (the package has one exports entry), so no exported symbol is added to any package entry.",
"No D3 ledger entry and no packages/spec edit: the claim's conditional did not trigger — the honest ADR-0087 disposition is not-required (no-migration-prescription) (no key, spelling, export or stored shape moves; the house disposition for runtime refusals of what already failed later); check-adr-0087-registration exit 0.",
"Worktree based on origin/main 07c842d (origin/main had moved past the dispatch base e864db5 at creation), then merged origin/main e085a8c (spec test titles only, no overlap) before the final verification.",
"The dogfood fixture is built with defineStack(..., { strict: false }) so the invalid body reaches the runtime doors under test even once the build door (#21850's) refuses it; the pins assert status, code and the located path, not a full sentence.",
"Commit trailers use the model-free pair AGENTS.md requires (Claude-Session + Co-authored-by: Claude), not the harness reminder's model-named Co-Authored-By; the PR body ends with the session-URL footer form AGENTS.md and the dispatch name, not the harness reminder's robot-emoji footer.",
"Ablation A: its in-leg build exited 1 at the DTS step only (TS6133, the ablated private judge left unused); the ESM/CJS bundles the suites load were emitted and ablation-dist-preflight confirmed the marker in dist before any reading.",
"Three gate commands were rerun after a measured prerequisite cause outside the diff (listed under gates.reruns); the ran file records the rerun exit codes."
],
"files_changed": [
".changeset/21848-node-config-values-at-registration.md",
"content/docs/automation/flows.mdx",
"packages/plugins/plugin-approvals/src/approval-node-config-contract.test.ts",
"packages/plugins/plugin-approvals/src/approval-node.ts",
"packages/qa/dogfood/test/flow-node-config-values-at-registration.dogfood.test.ts",
"packages/services/service-automation/src/builtin/parse-config.ts",
"packages/services/service-automation/src/engine.ts",
"packages/services/service-automation/src/node-config-values-at-registration.test.ts",
"packages/services/service-automation/src/plugin.ts"
]
}
Verdict on the dev report 5998741315 (status: done), checked against GitHub and the tree. The build is accepted. Its Clause-② spelling is corrected in this act, and an at-tier contract review is owed before landing.needs:contract-review is put on the PR in this act.
Checked:
Registration now parses a node's config VALUES with the contract its executor parses at run time. NodeExecutor gains an optional configContract, and registerFlow refuses a finding with a located error (flow, node id and type, config path, the schema's own sentence) in the same class as the key-name refusal: 400 VALIDATION_FAILED at the /automation doors. The approval executor declares ApprovalNodeConfigSchema, the very object its execute parses. There is no second, hand-written check.
H5 measured a real gap and closed it inside the package. At boot, the kernel:ready bind refused such a flow with a WARN, but the boot pull's earlier registration stayed active and bound. The cold-boot bind now withdraws a flow it refuses, so only that flow is refused, never the whole package. The service-automation/src/plugin.ts edit is accepted as required by the card's package-load door.
Every node type that declares no contract (all builtins, approval_revise, third-party types) keeps key-name checking.
Census: 36 flows and 19 approval nodes across the examples and fixtures; none is refused.
Ablations: the value check removed turned the refusal pins red at both doors; the boot withdraw removed turned the package-load pin red. The restores were proven.
Clause-② correction (one round: the claim, the PR line and the changeset move together).NodeExecutor is on @objectstack/service-automation's published entry (src/index.ts), so its new optional member widens a published type, while registration narrows. scripts/pm/clause2-line.mjs defines that case: yes (narrowing), "a diff that widens one surface and narrows another; both facts are true and both are read". The claim 5997179481 read no (narrowing), and the seat's order did not foresee the member. Amended here:
Clause-②: yes (narrowing)
The changeset's line moves to the same spelling in a patch round. The PR body's line-start line moves in the same round. The minor level with the ! banner and the not-required (no-migration-prescription) marker stand.
The declaration limb of the Clause-② gate now hits. An at-tier contract review is owed and runs once the patch round's head is green.
Coordination:#21850 (the build door, domain:spec) also checks the approval contract. Whichever PR lands second reconciles the two; this PR's pins assert status, code and the located path only.
Carried:
The builtin node types have the same class (a present builtin value its contract refuses still registers, then fails every run). That half is not mechanical (interpolated and conditional parses, and region containers), so the seat files it as a follow-up card in this act.
A third-party executor registered after the cold-boot bind, and the metadata save door's re-arm, are noted in the PR's Acceptance notes and not measured.
Seat decision on the landing path · seat domain:services#1 (#6021) · session_011K3zqE8Pv1Evw5hc8tZCnN · 2026-10-05T16:44Z. It supersedes the patch plan in the verdict 5998847976. The build verdict stands.
What changed: the domain:spec seat's pointer 5997885807 on #6021. Its PR #21893 (#21850) makes the approval node's config contract part of FlowSchema. registerFlow parses FlowSchema first (engine.ts about :4348), so once #21893 lands, registration and the package-load boot pull refuse these approval values through the spec's own judge, with a located issue.
Decided with the four-axis frame: PR #21897 drops its own judge for the approval node. All four axes agree, so nothing goes to the maintainer:
Long-term: one judge, in the spec (contract-first).
AI-error: two judges of one contract can diverge.
Startup: NodeExecutor.configContract would be a published member whose only use the spec path covers, and retiring it later would be a breaking change.
Business: the approval values are refused at registration either way.
It keeps the cold-boot bind's withdraw of a flow it refuses. That is a separate, measured defect: a refused flow stayed active.
It keeps its pins. They now pass through the spec's judge, and the PR re-measures them on the merged tree.
Its Clause-② goes back to no (narrowing) (the withdraw narrows what stays loaded), its changeset says only what it still does, and needs:contract-review comes off once the reworked head hits no Clause-② limb.
#21898 (the built-in node types) is re-read the same way: the built-in contracts already live in the spec (getBuiltinNodeConfigContracts), so its fix is likely the spec's FlowSchema judge. A note on #21898 says so; it stays blocked.
{
"issue": 21848,
"status": "done",
"round": "patch round 1 (after #21893 landed, seat decision 5998929933)",
"branch": "claude/issue-21848-node-config-values-at-registration",
"pr": "#21897",
"head": "7333fd869d",
"session": "session_011K3zqE8Pv1Evw5hc8tZCnN — the parent PM session (this run is a subagent); the harness Claude-Session line names the same id",
"premise_still_valid": true,
"summary": "Patch round 1: origin/main merged twice (866683f with #21893, then cab6396), no conflict; #21893's engine.test.ts line came in untouched. Removed NodeExecutor.configContract, validateNodeConfigValues and its registerFlow call, the formatIssuePath export, and the approval executor's declaration. engine.ts, builtin/parse-config.ts and plugin-approvals/src/approval-node.ts are now byte-identical to main, and plugin-approvals has no change (its test file is deleted: it pinned configContract, and the engine-level refusal is #21893's pin). Kept: the kernel:ready cold-boot bind's withdraw of a flow it refuses (service-automation/src/plugin.ts). Since #21893 refuses approval configs at the boot pull itself, the withdraw is now pinned and ablated with a plugin node type the spec does not declare, its executor registered from a later plugin's start(). The door pins were re-measured through FlowSchema: out-of-range escalation and undeclared escalation key are refused at the admin door (400 VALIDATION_FAILED, details.fields at the config path) and at package load (404, boot line at the path). The changeset (service-automation only, minor, !, Clause-②: no (narrowing), no-migration-prescription) and flows.mdx now say only what still holds.",
"tests": "All at 7333fd8, one locked turn (each step's exit printed): dogfood closure build 0 (63 tasks); service-automation typecheck 0 (tsc + check:test-typecheck); dogfood typecheck 0; service-automation src/flow-cold-boot-refusal-withdraw.test.ts 2 passed (withdraw pin + the control that the same body registers with no plugin contributing the type); dogfood test/flow-node-config-values-at-registration.dogfood.test.ts 6 passed (package load: sub-hour and undeclared-key approval flows 404 and the boot names each flow with nodes[1].config.escalation.timeoutHours / nodes[1].config.escalation.bogusKey; valid escalation active and opens one pending request; plugin-node undeclared key refused by the kernel:ready bind -> 404 with its valid sibling 200; admin door: both 400 VALIDATION_FAILED with details.fields carrying nodes.1.config.escalation.timeoutHours / nodes.1.config.escalation.bogusKey, then GET 404; valid escalation registers). Full service-automation suite: 173 files / 2112 tests passed. plugin-approvals: no source change (git diff origin/main...HEAD -- packages/plugins empty), so not re-run. #21893's own pins live in packages/spec and read no file of this PR. Ablation (scripts/ablation-replace.mjs, WRAP): the withdraw call replaced by a marker, anchor 1->0, blob e36e7077781b->5f67fd9d27bd, on-disk withdraw-call count 0; service-automation rebuilt in the leg (0); ablation-dist-preflight marker present in dist/index.js and index.cjs. Unit 1 failed / 1 passed (withdraw pin red, control green); dogfood 1 failed / 5 passed (only the plugin-node package-load pin: 200 active). Restore: blob after restore == HEAD blob e36e7077781b, git diff HEAD empty; rebuild 0; ablation-dist-preflight --absent: absent from all 6 dist files, tree clean. ESLint narrowed to the 3 touched TS files (--no-inline-config --format json): 3 files, 0 errors, 0 warnings; no type-aware linting in eslint.config.mjs. Control-byte self-scan of the 5 touched files: no hits.",
"mcp_calls": "0 — no MCP GitHub tool was called",
"api_writes": "1 relay stroke attempted, 0 writes landed. scripts/pm/post-stamped.mjs --comment=21848 dispatched request fw-20261005T194045Z-304166 (POST /repos/objectstack-ai/objectstack/dispatches answered HTTP 204), but its output never said "via the relay run": it ended exit 6 UNCONFIRMED after 300 s with run 37364959803 still queued (the repo had 75 queued runs). Read afterwards: the run concluded failure, its one job "Fleet write relay" was cancelled with 0 steps run, annotation "The job was not acquired by Runner of type hosted even after multiple attempts". So the comment POST /repos//issues/21848/comments never executed. Read back: #21848 still carries one os-dev-report (5998741315, round 0). Per the order, not retried. The exact body is in scratchpad issue-21848/report-p1-comment.md for the seat to post. The PR body was not edited. git push (3 pushes) is not REST.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: the seat (PR #21897 Acceptance notes) · noted, not filed: since #21893 the withdraw is not reached by any in-repo node type (approval is refused at the boot pull by FlowSchema, approval_revise is schemaless, builtins register at init); it is reached by a third-party plugin node type whose executor registers from its own start(), which the dogfood pin measures through a real package door."
],
"acceptance_notes_block": "### Patch round 1 (after #21893 landed): what was dropped, and what remains\n\nDropped.#21893 (merged as 866683f96f) made the approval node's config contract part of FlowSchema: the flow parse now judges an approval node's config against ApprovalNodeConfigSchema, whole. registerFlow parses FlowSchema first, so the admin door and the package-load boot pull already refuse an out-of-range escalation and an undeclared escalation key, located at the config path. Per the seat's decision 5998929933, there is now one judge in the spec, so this PR drops its own:\n- NodeExecutor.configContract (the optional published member) is removed, and so are AutomationEngine.validateNodeConfigValues and its call in registerFlow. engine.ts is byte-identical to main (blob 597e506a7711).\n- The formatIssuePath export in builtin/parse-config.ts is removed (nothing else needed it). The file is byte-identical to main.\n- plugin-approvals ends with no change. approval-node.ts is byte-identical to main (blob e2027bb0c675). approval-node-config-contract.test.ts is deleted: everything it pinned depended on configContract, and the engine-level refusal it also covered is #21893's pin.\n- The changeset no longer names @objectstack/plugin-approvals.\n\nRemains.\n- The cold-boot withdraw (service-automation/src/plugin.ts). This is a separate, measured defect. The boot pull registers a package's flows before a plugin that contributes a node type has registered its executor, and with it the descriptor configSchema the key check reads, so the pull registers such a flow unchecked and arms it. The kernel:ready bind then re-registers the flow and refuses it, but used to only warn, so the flow stayed active and bound. The bind now withdraws a flow it refuses, and only that flow. Since #21893, approval configs are refused at the boot pull itself, so the withdraw is measured on a plugin node type that the spec does not declare.\n- The door pins (packages/qa/dogfood/test/flow-node-config-values-at-registration.dogfood.test.ts) now pass through the spec's judge. Re-measured at 7333fd869d: 6 passed.\n - Package load. The sub-hour flow and the undeclared-key flow both answer 404. The boot line names each flow and the located path (nodes[1].config.escalation.timeoutHours, nodes[1].config.escalation.bogusKey). The valid escalation registers active and runs: it opens one pending approval request.\n - Package load, plugin node type. A flow with an undeclared key on that node type is refused by the kernel:ready bind and answers 404. Its valid sibling answers 200.\n - Admin door. Both approval refusals answer 400 VALIDATION_FAILED, with details.fields carrying nodes.1.config.escalation.timeoutHours and nodes.1.config.escalation.bogusKey respectively. GET answers 404 for both. The valid escalation registers.\n - Only the assertions that named this PR's old message text changed: node 'gate' became the spec's located path.\n- Unit pinservice-automation/src/flow-cold-boot-refusal-withdraw.test.ts replaces node-config-values-at-registration.test.ts. The withdraw is tested on a real LiteKernel, with the plugin node type registered from a later plugin's start(). The control shows that the same body registers when no plugin contributes the type.\n- Ablation (scripts/ablation-replace.mjs): the withdraw call was replaced by a marker statement (blob e36e7077781b to 5f67fd9d27bd) and present in dist/ per ablation-dist-preflight. Results: unit 1 failed / 1 passed (the withdraw pin red, the control green); dogfood 1 failed / 5 passed (only the plugin-node package-load pin, 200 active). Restore: blob equals HEAD and git diff HEAD is empty. After a rebuild, --absent reads the marker absent from all 6 built files, with the tree clean.\n- Clause-② returns to no (narrowing). Nothing published widens now, and the withdraw narrows what stays loaded at boot. The changeset is minor with the ! banner for @objectstack/service-automation only. The ADR-0087 marker stays not-required (no-migration-prescription).\n- Docs.content/docs/automation/flows.mdx now says the flow parse judges an approval node's config whole, and that a flow refused at boot is not left registered.\n",
"gates": {
"derived_at": "7333fd869d (node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, no paths; no stale-tree warning)",
"derived": [
"node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0",
"node scripts/check-adr-0087-registration.mjs --self-test :: exit 0",
"node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0",
"node scripts/check-changeset-no-major.mjs --self-test :: exit 0",
"node scripts/check-ci-filter-parity.mjs :: exit 0",
"node scripts/check-closing-keyword-parity.mjs :: exit 0",
"node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0",
"node scripts/check-comment-mask-adoption.mjs :: exit 0",
"node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0",
"node scripts/check-comment-mask-corpus.mjs :: exit 0",
"node scripts/check-doc-frontmatter.mjs :: exit 0",
"node scripts/check-doc-frontmatter.mjs --self-test :: exit 0",
"node scripts/check-doc-route-spelling.mjs --advisory :: exit 0",
"node scripts/check-doc-route-spelling.mjs --self-test :: exit 0",
"node scripts/check-docs-section-name.mjs :: exit 0",
"node scripts/check-docs-section-name.mjs --self-test :: exit 0",
"node scripts/check-dts-emitted.mjs --self-test :: exit 0",
"node scripts/check-empty-changeset.mjs --base origin/main :: exit 0",
"node scripts/check-empty-changeset.mjs --self-test :: exit 0",
"node scripts/check-issue-citations.mjs :: exit 0",
"node scripts/check-keyed-text-bounds.mjs :: exit 0",
"node scripts/check-keyed-text-bounds.mjs --self-test :: exit 0",
"node scripts/check-platform-object-tenancy-census.mjs :: exit 0",
"node scripts/check-platform-object-tenancy-census.mjs --self-test :: exit 0",
"node scripts/check-plugin-teardown-shape.mjs :: exit 0",
"node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0",
"node scripts/check-registry-log-declared.mjs :: exit 0",
"node scripts/check-registry-log-declared.mjs --self-test :: exit 0",
"node scripts/check-rest-log-spy-declared.mjs :: exit 0",
"node scripts/check-rest-log-spy-declared.mjs --self-test :: exit 0",
"node scripts/check-section-landing-index.mjs :: exit 0",
"node scripts/check-section-landing-index.mjs --self-test :: exit 0",
"node scripts/check-system-context-census.mjs :: exit 0",
"node scripts/check-system-context-census.mjs --self-test :: exit 0",
"node scripts/check-tenant-audit-census.mjs :: exit 0",
"node scripts/check-tenant-audit-census.mjs --self-test :: exit 0",
"node scripts/check-undeclared-dep-imports.mjs :: exit 0",
"node scripts/check-undeclared-dep-imports.mjs --self-test :: exit 0",
"node scripts/docs-audit/check-affected-docs.mjs :: exit 0",
"node scripts/docs-audit/check-drift-comment.mjs :: exit 0",
"node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0",
"node scripts/release-pending-publish.mjs --self-test :: exit 0",
"pnpm --filter @objectstack/lint run check:doc-formula-expressions :: exit 0",
"pnpm --filter @objectstack/lint run check:doc-security-posture :: exit 0",
"pnpm --filter @objectstack/spec run check:docs :: exit 0",
"pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0",
"pnpm --filter @objectstack/spec run check:empty-state :: exit 0",
"pnpm --filter @objectstack/spec run check:liveness :: exit 0",
"pnpm --filter @objectstack/spec run check:skill-examples :: exit 0",
"pnpm --filter @objectstack/spec run check:strictness-ledger :: exit 0",
"pnpm --filter @objectstack/spec run check:variant-docs :: exit 0",
"pnpm --filter @objectstack/spec run check:yaml-examples :: exit 0",
"pnpm check:changeset-gate-self-tests :: exit 0",
"pnpm check:corpus-claim-drift :: exit 0",
"pnpm check:cross-package-test-inputs :: exit 0",
"pnpm check:doc-anchors :: exit 0",
"pnpm check:doc-authoring :: exit 0",
"pnpm check:docs-audit-scope :: exit 0",
"pnpm check:docs-redirects :: exit 0",
"pnpm check:docs-single-h1 :: exit 0",
"pnpm check:docs-spec-enumerations :: exit 0",
"pnpm check:docs-transcript-drift :: exit 0",
"pnpm check:driver-memory-census :: exit 0",
"pnpm check:dts-closure :: exit 0",
"pnpm check:dual-build-cjs-loads :: exit 0",
"pnpm check:engine-double-contract :: exit 0",
"pnpm check:gitlink-declared :: exit 0",
"pnpm check:issue-citations :: exit 0",
"pnpm check:lean-entry-closure :: exit 0",
"pnpm check:logger-receiver-detach :: exit 0",
"pnpm check:nul-bytes :: exit 0",
"pnpm check:objectql-double-limit :: exit 0",
"pnpm check:objectui-changeset :: exit 0",
"pnpm check:org-identifier :: exit 0",
"pnpm check:page-declaration-shape :: exit 0",
"pnpm check:pm-changeset-deadline-census :: exit 0",
"pnpm check:published-files :: exit 0",
"pnpm check:published-readme-links :: exit 0",
"pnpm check:query-options-erasure :: exit 0",
"pnpm check:react-page-adapter-contract :: exit 0",
"pnpm check:refd-timer-probe :: exit 0",
"pnpm check:role-word :: exit 0",
"pnpm check:skill-identifier-liveness :: exit 0",
"pnpm check:slot-lookup :: exit 0",
"pnpm check:sourcemap-no-sources-content :: exit 0",
"pnpm check:test-source-alias :: exit 0",
"pnpm check:tier-file-adoption :: exit 0",
"pnpm check:type-check-coverage :: exit 0",
"pnpm check:type-check-debt :: exit 0",
"pnpm check:vendor-version-stamps :: exit 0",
"pnpm check:watch-hint-literal :: exit 0",
"pnpm check:where-matcher :: exit 0"
],
"reruns": [
"pnpm --filter @objectstack/spec run check:skill-examples — first run exit 3 PREREQUISITE NOT MET: no built @objectstack/client-react in the recreated worktree; built (cache hit), rerun exit 0",
"pnpm check:dual-build-cjs-loads — first run exit 3 PREREQUISITE NOT MET: 8 packages outside this diff had no dist/ (studio, client-react, embedder-openai, knowledge-memory, knowledge-ragflow, organizations, service-cluster-redis, service-knowledge); built (41/41 cache hits), rerun exit 0"
],
"ran_verdict": "✓ dispatch-gates --ran: 92 derived famil(ies) accounted for — 92 run, 0 NOT-MEASURED (exit 0)",
"files": "scratchpad issue-21848/gates-p1/results.tsv, ran.list, ran-verdict.log"
},
"line_budget": "n/a",
"deviations": [
"origin/main was merged a second time (cab6396, plugin-security only, no overlap): the first derivation on 195134d printed STALE TREE (scripts/engine-double-contract.pinned.json changed on main), so the gates were derived and run on the re-merged head 7333fd8.",
"The withdraw's pins and ablation moved from the approval node to a plugin node type the spec does not declare, because #21893 refuses approval configs at the boot pull and an approval-based withdraw pin would pass without the withdraw (vacuous). The dogfood file adds that case and an undeclared-key approval case beside the sub-hour one; it stays one new file.",
"The unit test file is renamed: node-config-values-at-registration.test.ts -> flow-cold-boot-refusal-withdraw.test.ts (its value-judge cases went with the judge).",
"Three lock turns ended 99 (queue-timeout, never acquired; holder pid 18739, an issue-21794 run.sh, alive and progressing); recorded NOT MEASURED, not as failures, and retried with the same slot name until acquired.",
"Two gate commands were rerun after a prerequisite cause outside the diff (listed under gates.reruns).",
"Commit trailers stay the model-free pair AGENTS.md requires."
],
"files_changed": [
".changeset/21848-node-config-values-at-registration.md",
"content/docs/automation/flows.mdx",
"packages/qa/dogfood/test/flow-node-config-values-at-registration.dogfood.test.ts",
"packages/services/service-automation/src/flow-cold-boot-refusal-withdraw.test.ts",
"packages/services/service-automation/src/plugin.ts"
],
"files_reverted_to_main": [
"packages/services/service-automation/src/engine.ts",
"packages/services/service-automation/src/builtin/parse-config.ts",
"packages/plugins/plugin-approvals/src/approval-node.ts",
"packages/plugins/plugin-approvals/src/approval-node-config-contract.test.ts (deleted)",
"packages/services/service-automation/src/node-config-values-at-registration.test.ts (renamed and rewritten)"
],
"report_comment": "NOT POSTED: relay run 37364959803 cancelled before any step (no hosted runner acquired); not retried; body at scratchpad issue-21848/report-p1-comment.md"
}
ACCEPT. The dev's patch-round report is posted verbatim beside this comment. Its own relay run was cancelled before any step ran: no hosted runner was acquired, and the dev correctly did not retry.
Clause-②: no (narrowing) on PR line 2 and in the changeset. It is minor with the ! banner, and its ADR-0087 marker is not-required (no-migration-prescription). What stays loaded at boot narrows, and no published symbol moves. The net diff is 5 files.
Contract review: not owed. There is no packages/spec source, no Clause-②: yes and no governed text. The seat removes needs:contract-review in this act.
The PR title and opening sections described round 0. The seat retitles the PR to what it now ships, and puts the dev's patch-round notes at the top of the body. The round-0 sections stay below as the record.
Carried, not filed: since #21893, no in-repo node type reaches the withdraw. A third-party plugin node type whose executor registers from its own start() does, and the dogfood pin measures that through a real package door. #21898 (the built-in half) stays pm:blocked until this lands. Then the seat raises its domain with triage, because a fix in FlowSchema is spec-lane work.
Fixes #21848 closed this card completed. In this act the seat clears the pm:dispatched state label and the assignee os-steve. The domain:services, area:workflow, priority:p2 and bug labels stay.
Activity
objectstack-fleet commentedon Oct 5, 2026
Path: approvals and automation — flows run: node types, trigger types | automation.flow-node-type-matrix | P2
Triage: first grade —
bug·priority:p2·domain:services·area:workflow·pm:queue. Registration and package load run the node's full config schemaTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-05T08:57Z. ⛔ Not a claim, ⛔ not a dispatch.Triage: lands in
packages/services/service-automation/src/engine.ts(registration, about:10425–:10480) ⇒domain:services; rationale: registration checks key names only, so an invalid value is accepted and fails every run.executeuses (approval.zod.ts:666for the approval node) and refuses with a located error. ⛔ No second, hand-written value check.timeoutHours: 0.5is refused at registration through both doors, and a valid escalation still registers and runs.Generated by Claude Code
objectstack-fleet commentedon Oct 5, 2026
Claim: PM loop round 1 (17.7 pre-release follow-up, dispatched on the maintainer's direct order)
Session:
session_018zT8d8NpiQ1ExhuNd5TxY6Account:
hotlong(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-21848-escalation-config-valuesWorktree:
objectstack-issue-21848Domain:
domain:services(card label, as triage set it)File surface:
packages/services/service-automation/src,.changeset/Container & model:
M,mode:subagent,model: opus(default tier; no path-derived mandate)Clause-②: no
Thread-read: 5991301033
Serial constraints cleared: none named
Provenance: the maintainer, in Claude Code session
session_018zT8d8NpiQ1ExhuNd5TxY6, 2026-10-05, verbatim: 「都开单派发」; landing per the standing order 「开发完整就进队列合并」. Dispatched one at a time within the session's load cap. Where the card withholds detail, this session holds it and the dispatch carries it privately.Generated by Claude Code
objectstack-fleet commentedon Oct 5, 2026
Release: session
session_018zT8d8NpiQ1ExhuNd5TxY6· cause: re-route before dispatch (no dev was started on this card) · destination: back topm:queuefor thedomain:serviceslane seat to claim.Provenance: the maintainer, in Claude Code session
session_018zT8d8NpiQ1ExhuNd5TxY6, 2026-10-05, verbatim: 「只有不能公开的才需要你派发处理」. This card withholds nothing, so its dispatch belongs to the lane. The claim above is withdrawn, the assignee is cleared, and the triage grading stands.Generated by Claude Code
objectstack-fleet commentedon Oct 5, 2026
Claim: PM loop round 3 · 2026-10-05T15:04Z
Session:
session_011K3zqE8Pv1Evw5hc8tZCnNAccount:
os-steve(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-21848-node-config-values-at-registrationWorktree:
objectstack-issue-21848Domain:
domain:servicesSeat:
domain:services#1(seat post #6021)File surface (at
origin/maine864db56), per triage's direction5991301033:packages/services/service-automation/src/engine.ts(registration, about:10425–:10480): registration, through the admin door and on package load, parses each node's config with the same schemaexecuteuses (for the approval node, the escalation schema inapproval.zod.ts). It refuses an invalid value with a located error.service-automationtests, plus a door pin in a NEW file underpackages/qa/dogfood/test/(declared cross-lane on [PM seat] domain:cli — ⏳ vacant #6024 in this act).content/docs/**sentences this makes false, and aminorchangeset with the!banner.⛔ No second, hand-written value check: the node's own schema is the one judge. ⛔ No edit to the build door (
objectstack validate/compile): that is #21850, claimed indomain:spec. ⛔ No change to whatexecuteaccepts.Container & model:
M,mode:subagent,model: opus(dispatch-gates --tier: no path-derived mandate; default tier).Clause-②: no (narrowing)
active, then fail every run (an approval node's record was created with no approval gate). It is now refused at registration and on package load. An authored package that loaded before can stop loading after upgrade. The changeset says so, with the handling: correct the value the error locates.Thread-read: 5995690371
Serial constraints cleared: at 2026-10-05T15:04Z:
session_018zT8d8NpiQ1ExhuNd5TxY6was released before any dev started (5995690371), on the maintainer's routing 「只有不能公开的才需要你派发处理」.service-automation/src/engine.ts, the approval node or the approval schema. The seat's in-flight approvals: every approval notification reaches the recipient with an empty body — the service puts its text in payload.message, messaging reads payload.body (comment and request-info text are lost) #21847 (area:workflow) editsplugin-approvals's notify helper, which is file-disjoint from this card.Selection: the lane read fresh at this pick holds three p2 cards in
pm:queue: 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, plugin-audit's recordLabel guesses from a fixed key list instead of the ADR-0079 resolver, so an object titled by company_name shows its record id on every activity row #21878 and security(automation): a paused flow run's stored state keeps record-change values that #21830's mask removes elsewhere — detail withheld pending maintainer #21867. security(automation): a paused flow run's stored state keeps record-change values that #21830's mask removes elsewhere — detail withheld pending maintainer #21867 withholds its detail, and by the maintainer's routing it is dispatched by the session holding that detail. This card is the older of the other two, and it takes the slot service-datasource: onobjectstack startthe federation service reads ametadataservice it captured at init, before that service registers —external/validateanswers no rows and the boot gate checks zero federated objects #21876's build freed.Generated by Claude Code
objectstack-fleet commentedon Oct 5, 2026
os-dev-report
{
"issue": 21848,
"status": "done",
"branch": "claude/issue-21848-node-config-values-at-registration",
"pr": "#21897",
"session": "session_011K3zqE8Pv1Evw5hc8tZCnN — the parent PM session (this run is a subagent); the harness Claude-Session line names the same id",
"premise_still_valid": true,
"summary": "Registration now parses a node's config VALUES with the contract its executor parses at run time: NodeExecutor gains an optional configContract, AutomationEngine.registerFlow runs validateNodeConfigValues after the key-name check and refuses the flow with a located error of the key-name refusal's class (flow, node id and type, config path, the contract's own sentence; 400 VALIDATION_FAILED at the /automation doors), and the approval executor declares ApprovalNodeConfigSchema, the very object its execute parses. Measuring H5 found the package-load door could not hold any boot refusal of a plugin node: the boot pull registers package flows before ApprovalsServicePlugin registers the approval executor, and the kernel:ready bind's refusal only WARNed while the boot pull's registration stayed active and bound (measured on main with an escalation.bogusKey flow); the cold-boot bind now withdraws a flow it refuses, so a refused flow is not registered whichever boot step refused it, and only the flow, never the package, is refused. Every other node type (all builtins, approval_revise, third-party types) declares no contract and keeps key-name-only judgement; no D3 entry and no spec edit: the changeset declares not-required (no-migration-prescription).",
"tests": "All at 5fdd32a (final): service-automation src/node-config-values-at-registration.test.ts 7 passed; plugin-approvals src/approval-node-config-contract.test.ts 2 passed; dogfood test/flow-node-config-values-at-registration.dogfood.test.ts (--project isolated) 5 passed; typecheck service-automation + plugin-approvals both Done (tsc + check:test-typecheck); dogfood typecheck exit 0 with the new file in the program (tsc --listFilesOnly). Full suites at 2ef0c61 (code byte-identical at HEAD: git diff 2ef0c61..HEAD -- packages/services packages/plugins packages/qa empty): service-automation 173 files / 2117 tests passed, plugin-approvals 61 files / 897 tests passed. RED first: the dogfood pin on unmodified main code (tree = 07c842d + the pin, committed as be7450c) gave 3 failed / 2 passed (package GET 200 active, no located boot line, admin POST 200; controls green). Ablation A (validateNodeConfigValues call replaced by a marker via scripts/ablation-replace.mjs, anchor 1->0, blob b5e94fb23b67->bd7099e9875b; service-automation rebuilt in the leg, ablation-dist-preflight: marker in 2 dist files; DTS step exit 1 on TS6133 only, ESM/CJS emitted): service-automation 5 failed/2 passed (controls), plugin-approvals 1 failed/1 passed, dogfood 3 failed/2 passed. Ablation B (cold-boot withdraw replaced, anchor 1->0, blob fb06fd8ad148->0d523279cf1c, preflight marker present): service-automation 1 failed (boot pin)/6 passed, plugin-approvals 2 passed, dogfood 1 failed (package 404)/4 passed. Restore both legs: blob after restore == HEAD blob, git diff HEAD empty; rebuild exit 0; ablation-dist-preflight --absent A=0 B=0 (absent from all 6 dist files, tree clean). Census: 36 flows / 19 approval nodes across showcase, crm, multi-package and the dogfood approval fixtures, 0 refused by ApprovalNodeConfigSchema. ESLint narrowed to the 7 touched TS files (--no-inline-config --format json): 7 files, 0 errors, 0 warnings; config globs cover all 7; eslint.config.mjs enables no type-aware linting, so untouched files' verdicts cannot move; repo-wide lint is CI's.",
"mcp_calls": "0 — no MCP GitHub tool was called",
"api_writes": "3 relay strokes, each one POST /repos/objectstack-ai/objectstack/dispatches executed by fleet-write.yml as objectstack-fleet[bot]: (1) pr_create -> POST /repos/objectstack-ai/objectstack/pulls (draft #21897, run 37341288483); (2) label-write --assign os-steve -> POST /repos//issues/21897/assignees (run 37341393259; zero labels written); (3) this os-dev-report comment -> POST /repos//issues/21848/comments via post-stamped.mjs. git push is not REST. Reads: gh api GET of the card, its comments, #21850 and its comments, the PR read-back.",
"open_questions": [],
"out_of_scope_findings": [
"class: a · reach: POST /automation with a create_record node carrying config.outputVariable: 42 answered 200, then POST /automation/:name/trigger answered 400 FLOW_FAILED 'create_record mk: config does not satisfy the create_record contract — config.outputVariable: Invalid input: expected string, received number' (measured through the dogfood harness at 5fdd32a) · evidence: the builtin executors parse their Zod contract inside execute (builtin/parse-config.ts) and declare none at registration, and FlowSchema's flowNodeConfigRefusals judges builtins on presence only, so a present builtin value its contract refuses registers and fails every run — the builtin half of this card's own class; wiring them is not mechanical (http parses after interpolation, loop parses conditionally, region containers' contracts contain their regions) · family: same family as #21848, for the seat to fold into that family's close-out rather than a single-point card · dedupe words: builtin node config value registration, configContract builtin executors, create_record outputVariable wrong type registers, flow registers then fails every run",
"carrier: #21850's PR (branch claude/issue-21850-approval-node-config-build-refusal) · noted, not filed: that card's repro step 3 says the unknown-key approval flow is 'dropped with only a WARN' at boot; measured on main it was NOT dropped (it stayed active and bound behind the kernel:ready WARN); this PR's cold-boot withdraw makes it dropped. Its WIP also judges the approval contract whole inside FlowSchema, which registerFlow parses first, so whichever lands second reconciles (this PR's pins assert status, code and the located path only).",
"carrier: 承接者:无 · noted in the PR's Acceptance notes only: a third-party executor registered from a kernel:ready handler that runs after this plugin's cold-boot bind would leave a boot-pulled flow unjudged until its next registration; no in-repo node type does this (builtins at init, approval at start). Not measured further.",
"carrier: 承接者:无 · noted in the PR's Acceptance notes only: the metadata save door (PUT /meta/flow/:name) re-arms through the mutation sync, whose refusal handling this PR does not change. Not measured."
],
"gates": {
"derived_at": "5fdd32adc9 (node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, no paths)",
"derived": [
"node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0",
"node scripts/check-adr-0087-registration.mjs --self-test :: exit 0",
"node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0",
"node scripts/check-changeset-no-major.mjs --self-test :: exit 0",
"node scripts/check-ci-filter-parity.mjs :: exit 0",
"node scripts/check-closing-keyword-parity.mjs :: exit 0",
"node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0",
"node scripts/check-comment-mask-adoption.mjs :: exit 0",
"node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0",
"node scripts/check-comment-mask-corpus.mjs :: exit 0",
"node scripts/check-doc-frontmatter.mjs :: exit 0",
"node scripts/check-doc-frontmatter.mjs --self-test :: exit 0",
"node scripts/check-doc-route-spelling.mjs --advisory :: exit 0",
"node scripts/check-doc-route-spelling.mjs --self-test :: exit 0",
"node scripts/check-docs-section-name.mjs :: exit 0",
"node scripts/check-docs-section-name.mjs --self-test :: exit 0",
"node scripts/check-dts-emitted.mjs --self-test :: exit 0",
"node scripts/check-empty-changeset.mjs --base origin/main :: exit 0",
"node scripts/check-empty-changeset.mjs --self-test :: exit 0",
"node scripts/check-issue-citations.mjs :: exit 0",
"node scripts/check-keyed-text-bounds.mjs :: exit 0",
"node scripts/check-keyed-text-bounds.mjs --self-test :: exit 0",
"node scripts/check-platform-object-tenancy-census.mjs :: exit 0",
"node scripts/check-platform-object-tenancy-census.mjs --self-test :: exit 0",
"node scripts/check-plugin-teardown-shape.mjs :: exit 0",
"node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0",
"node scripts/check-registry-log-declared.mjs :: exit 0",
"node scripts/check-registry-log-declared.mjs --self-test :: exit 0",
"node scripts/check-rest-log-spy-declared.mjs :: exit 0",
"node scripts/check-rest-log-spy-declared.mjs --self-test :: exit 0",
"node scripts/check-section-landing-index.mjs :: exit 0",
"node scripts/check-section-landing-index.mjs --self-test :: exit 0",
"node scripts/check-system-context-census.mjs :: exit 0",
"node scripts/check-system-context-census.mjs --self-test :: exit 0",
"node scripts/check-tenant-audit-census.mjs :: exit 0",
"node scripts/check-tenant-audit-census.mjs --self-test :: exit 0",
"node scripts/check-undeclared-dep-imports.mjs :: exit 0",
"node scripts/check-undeclared-dep-imports.mjs --self-test :: exit 0",
"node scripts/docs-audit/check-affected-docs.mjs :: exit 0",
"node scripts/docs-audit/check-drift-comment.mjs :: exit 0",
"node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0",
"node scripts/release-pending-publish.mjs --self-test :: exit 0",
"pnpm --filter @objectstack/lint run check:doc-formula-expressions :: exit 0",
"pnpm --filter @objectstack/lint run check:doc-security-posture :: exit 0",
"pnpm --filter @objectstack/spec run check:docs :: exit 0",
"pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0",
"pnpm --filter @objectstack/spec run check:empty-state :: exit 0",
"pnpm --filter @objectstack/spec run check:liveness :: exit 0",
"pnpm --filter @objectstack/spec run check:skill-examples :: exit 0",
"pnpm --filter @objectstack/spec run check:strictness-ledger :: exit 0",
"pnpm --filter @objectstack/spec run check:variant-docs :: exit 0",
"pnpm --filter @objectstack/spec run check:yaml-examples :: exit 0",
"pnpm check:changeset-gate-self-tests :: exit 0",
"pnpm check:corpus-claim-drift :: exit 0",
"pnpm check:cross-package-test-inputs :: exit 0",
"pnpm check:dispatcher-error-vocabulary :: exit 0",
"pnpm check:doc-anchors :: exit 0",
"pnpm check:doc-authoring :: exit 0",
"pnpm check:docs-audit-scope :: exit 0",
"pnpm check:docs-redirects :: exit 0",
"pnpm check:docs-single-h1 :: exit 0",
"pnpm check:docs-spec-enumerations :: exit 0",
"pnpm check:docs-transcript-drift :: exit 0",
"pnpm check:driver-memory-census :: exit 0",
"pnpm check:dts-closure :: exit 0",
"pnpm check:dual-build-cjs-loads :: exit 0",
"pnpm check:engine-double-contract :: exit 0",
"pnpm check:gitlink-declared :: exit 0",
"pnpm check:i18n :: exit 0",
"pnpm check:i18n-stale-fill :: exit 0",
"pnpm check:issue-citations :: exit 0",
"pnpm check:lean-entry-closure :: exit 0",
"pnpm check:logger-receiver-detach :: exit 0",
"pnpm check:nul-bytes :: exit 0",
"pnpm check:objectql-double-limit :: exit 0",
"pnpm check:objectui-changeset :: exit 0",
"pnpm check:org-identifier :: exit 0",
"pnpm check:page-declaration-shape :: exit 0",
"pnpm check:pm-changeset-deadline-census :: exit 0",
"pnpm check:published-files :: exit 0",
"pnpm check:published-readme-links :: exit 0",
"pnpm check:query-options-erasure :: exit 0",
"pnpm check:react-page-adapter-contract :: exit 0",
"pnpm check:refd-timer-probe :: exit 0",
"pnpm check:role-word :: exit 0",
"pnpm check:skill-identifier-liveness :: exit 0",
"pnpm check:slot-lookup :: exit 0",
"pnpm check:sourcemap-no-sources-content :: exit 0",
"pnpm check:test-source-alias :: exit 0",
"pnpm check:tier-file-adoption :: exit 0",
"pnpm check:type-check-coverage :: exit 0",
"pnpm check:type-check-debt :: exit 0",
"pnpm check:vendor-version-stamps :: exit 0",
"pnpm check:watch-hint-literal :: exit 0",
"pnpm check:where-matcher :: exit 0"
],
"reruns": [
"node scripts/check-comment-mask-corpus.mjs — first run exit 1: ENOENT on a scratch probe file deleted mid-sweep (my own dogfood probe, removed before the sweep read it); rerun with no probe in flight exit 0",
"pnpm --filter @objectstack/spec run check:skill-examples — first run exit 3 PREREQUISITE NOT MET: packages/spec dist declarations predated src after the origin/main merge (spec test titles); spec rebuilt (turbo cache hit), rerun exit 0",
"pnpm check:dual-build-cjs-loads — first run exit 3 PREREQUISITE NOT MET: 8 packages outside this diff had no dist/ (studio, client-react, embedder-openai, knowledge-memory, knowledge-ragflow, organizations, service-cluster-redis, service-knowledge); built (41/41 cache hits), rerun exit 0"
],
"dispatch_named_beyond_derivation": [
"node scripts/check-dev-prereqs.mjs --self-test :: exit 0",
"node scripts/check-spec-docblock-symbol-anchors.mjs :: exit 0",
"node scripts/check-spec-docblock-symbol-anchors.mjs --self-test :: exit 0",
"pnpm --filter @objectstack/spec run check:api-surface :: exit 0",
"pnpm --filter @objectstack/spec run check:browser-reachable-entries :: exit 0",
"pnpm --filter @objectstack/spec run check:dual-source-exports :: exit 0",
"pnpm --filter @objectstack/spec run check:entry-nameability :: exit 0",
"pnpm --filter @objectstack/spec run check:export-origins :: exit 0",
"pnpm --filter @objectstack/spec run check:exported-any :: exit 0",
"pnpm --filter @objectstack/spec run check:llms-txt :: exit 0",
"pnpm --filter @objectstack/spec run check:migration-registry :: exit 0",
"pnpm --filter @objectstack/spec run check:objectui-pin-citations :: exit 0",
"pnpm --filter @objectstack/spec run check:skill-refs :: exit 0",
"pnpm check:merge-driver :: exit 0",
"pnpm check:pm-prior-rulings :: exit 0",
"pnpm check:pm-widening-tells :: exit 0",
"pnpm check:spec-parsed-alias :: exit 0"
],
"ran_verdict": "✓ dispatch-gates --ran: 95 derived famil(ies) accounted for — 95 run, 0 NOT-MEASURED, 0 UNRUN (exit 0)",
"files": "every exit code in scratchpad issue-21848/gates/results.tsv, named-results.tsv, ran.list, ran-verdict.log"
},
"line_budget": "n/a",
"deviations": [
"packages/services/service-automation/src/plugin.ts is edited, outside the expected landing (engine.ts plus, if needed, approval-node.ts): H5 measured that the kernel:ready cold-boot bind refused a flow with a WARN while the boot pull registration stayed active and bound, so the package-load door could not hold any refusal of a plugin node without the bind withdrawing what it refuses. Same package; declared in the PR body.",
"packages/services/service-automation/src/builtin/parse-config.ts: formatIssuePath is exported from that internal module so the value refusal spells paths as the execute-time refusal does. Not re-exported from index.ts (the package has one exports entry), so no exported symbol is added to any package entry.",
"No D3 ledger entry and no packages/spec edit: the claim's conditional did not trigger — the honest ADR-0087 disposition is not-required (no-migration-prescription) (no key, spelling, export or stored shape moves; the house disposition for runtime refusals of what already failed later); check-adr-0087-registration exit 0.",
"Worktree based on origin/main 07c842d (origin/main had moved past the dispatch base e864db5 at creation), then merged origin/main e085a8c (spec test titles only, no overlap) before the final verification.",
"The dogfood fixture is built with defineStack(..., { strict: false }) so the invalid body reaches the runtime doors under test even once the build door (#21850's) refuses it; the pins assert status, code and the located path, not a full sentence.",
"Commit trailers use the model-free pair AGENTS.md requires (Claude-Session + Co-authored-by: Claude), not the harness reminder's model-named Co-Authored-By; the PR body ends with the session-URL footer form AGENTS.md and the dispatch name, not the harness reminder's robot-emoji footer.",
"Ablation A: its in-leg build exited 1 at the DTS step only (TS6133, the ablated private judge left unused); the ESM/CJS bundles the suites load were emitted and ablation-dist-preflight confirmed the marker in dist before any reading.",
"Three gate commands were rerun after a measured prerequisite cause outside the diff (listed under gates.reruns); the ran file records the rerun exit codes."
],
"files_changed": [
".changeset/21848-node-config-values-at-registration.md",
"content/docs/automation/flows.mdx",
"packages/plugins/plugin-approvals/src/approval-node-config-contract.test.ts",
"packages/plugins/plugin-approvals/src/approval-node.ts",
"packages/qa/dogfood/test/flow-node-config-values-at-registration.dogfood.test.ts",
"packages/services/service-automation/src/builtin/parse-config.ts",
"packages/services/service-automation/src/engine.ts",
"packages/services/service-automation/src/node-config-values-at-registration.test.ts",
"packages/services/service-automation/src/plugin.ts"
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 5, 2026
Review: PR #21897 at
5fdd32ad· seatdomain:services#1·session_011K3zqE8Pv1Evw5hc8tZCnN· 2026-10-05T16:39ZVerdict on the dev report
5998741315(status: done), checked against GitHub and the tree. The build is accepted. Its Clause-② spelling is corrected in this act, and an at-tier contract review is owed before landing.needs:contract-reviewis put on the PR in this act.Checked:
NodeExecutorgains an optionalconfigContract, andregisterFlowrefuses a finding with a located error (flow, node id and type, config path, the schema's own sentence) in the same class as the key-name refusal:400 VALIDATION_FAILEDat the/automationdoors. The approval executor declaresApprovalNodeConfigSchema, the very object itsexecuteparses. There is no second, hand-written check.kernel:readybind refused such a flow with a WARN, but the boot pull's earlier registration stayed active and bound. The cold-boot bind now withdraws a flow it refuses, so only that flow is refused, never the whole package. Theservice-automation/src/plugin.tsedit is accepted as required by the card's package-load door.approval_revise, third-party types) keeps key-name checking.Clause-② correction (one round: the claim, the PR line and the changeset move together).
NodeExecutoris on@objectstack/service-automation's published entry (src/index.ts), so its new optional member widens a published type, while registration narrows.scripts/pm/clause2-line.mjsdefines that case:yes (narrowing), "a diff that widens one surface and narrows another; both facts are true and both are read". The claim5997179481readno (narrowing), and the seat's order did not foresee the member. Amended here:Clause-②: yes (narrowing)
minorlevel with the!banner and thenot-required (no-migration-prescription)marker stand.Coordination: #21850 (the build door,
domain:spec) also checks the approval contract. Whichever PR lands second reconciles the two; this PR's pins assert status, code and the located path only.Carried:
Generated by Claude Code
objectstack-fleet commentedon Oct 5, 2026
Seat decision on the landing path · seat
domain:services#1(#6021) ·session_011K3zqE8Pv1Evw5hc8tZCnN· 2026-10-05T16:44Z. It supersedes the patch plan in the verdict5998847976. The build verdict stands.What changed: the
domain:specseat's pointer5997885807on #6021. Its PR #21893 (#21850) makes the approval node's config contract part ofFlowSchema.registerFlowparsesFlowSchemafirst (engine.tsabout:4348), so once #21893 lands, registration and the package-load boot pull refuse these approval values through the spec's own judge, with a located issue.Decided with the four-axis frame: PR #21897 drops its own judge for the approval node. All four axes agree, so nothing goes to the maintainer:
NodeExecutor.configContractwould be a published member whose only use the spec path covers, and retiring it later would be a breaking change.So, after #21893 lands:
mainand removesNodeExecutor.configContract, the approval executor's declaration andvalidateNodeConfigValues.no (narrowing)(the withdraw narrows what stays loaded), its changeset says only what it still does, andneeds:contract-reviewcomes off once the reworked head hits no Clause-② limb.#21898 (the built-in node types) is re-read the same way: the built-in contracts already live in the spec (
getBuiltinNodeConfigContracts), so its fix is likely the spec'sFlowSchemajudge. A note on #21898 says so; it stays blocked.Generated by Claude Code
objectstack-fleet commentedon Oct 5, 2026
os-dev-report
{
"issue": 21848,
"status": "done",
"round": "patch round 1 (after #21893 landed, seat decision 5998929933)",
"branch": "claude/issue-21848-node-config-values-at-registration",
"pr": "#21897",
"head": "7333fd869d",
"session": "session_011K3zqE8Pv1Evw5hc8tZCnN — the parent PM session (this run is a subagent); the harness Claude-Session line names the same id",
"premise_still_valid": true,
"summary": "Patch round 1: origin/main merged twice (866683f with #21893, then cab6396), no conflict; #21893's engine.test.ts line came in untouched. Removed NodeExecutor.configContract, validateNodeConfigValues and its registerFlow call, the formatIssuePath export, and the approval executor's declaration. engine.ts, builtin/parse-config.ts and plugin-approvals/src/approval-node.ts are now byte-identical to main, and plugin-approvals has no change (its test file is deleted: it pinned configContract, and the engine-level refusal is #21893's pin). Kept: the kernel:ready cold-boot bind's withdraw of a flow it refuses (service-automation/src/plugin.ts). Since #21893 refuses approval configs at the boot pull itself, the withdraw is now pinned and ablated with a plugin node type the spec does not declare, its executor registered from a later plugin's start(). The door pins were re-measured through FlowSchema: out-of-range escalation and undeclared escalation key are refused at the admin door (400 VALIDATION_FAILED, details.fields at the config path) and at package load (404, boot line at the path). The changeset (service-automation only, minor, !, Clause-②: no (narrowing), no-migration-prescription) and flows.mdx now say only what still holds.",
"tests": "All at 7333fd8, one locked turn (each step's exit printed): dogfood closure build 0 (63 tasks); service-automation typecheck 0 (tsc + check:test-typecheck); dogfood typecheck 0; service-automation src/flow-cold-boot-refusal-withdraw.test.ts 2 passed (withdraw pin + the control that the same body registers with no plugin contributing the type); dogfood test/flow-node-config-values-at-registration.dogfood.test.ts 6 passed (package load: sub-hour and undeclared-key approval flows 404 and the boot names each flow with nodes[1].config.escalation.timeoutHours / nodes[1].config.escalation.bogusKey; valid escalation active and opens one pending request; plugin-node undeclared key refused by the kernel:ready bind -> 404 with its valid sibling 200; admin door: both 400 VALIDATION_FAILED with details.fields carrying nodes.1.config.escalation.timeoutHours / nodes.1.config.escalation.bogusKey, then GET 404; valid escalation registers). Full service-automation suite: 173 files / 2112 tests passed. plugin-approvals: no source change (git diff origin/main...HEAD -- packages/plugins empty), so not re-run. #21893's own pins live in packages/spec and read no file of this PR. Ablation (scripts/ablation-replace.mjs, WRAP): the withdraw call replaced by a marker, anchor 1->0, blob e36e7077781b->5f67fd9d27bd, on-disk withdraw-call count 0; service-automation rebuilt in the leg (0); ablation-dist-preflight marker present in dist/index.js and index.cjs. Unit 1 failed / 1 passed (withdraw pin red, control green); dogfood 1 failed / 5 passed (only the plugin-node package-load pin: 200 active). Restore: blob after restore == HEAD blob e36e7077781b, git diff HEAD empty; rebuild 0; ablation-dist-preflight --absent: absent from all 6 dist files, tree clean. ESLint narrowed to the 3 touched TS files (--no-inline-config --format json): 3 files, 0 errors, 0 warnings; no type-aware linting in eslint.config.mjs. Control-byte self-scan of the 5 touched files: no hits.",
"mcp_calls": "0 — no MCP GitHub tool was called",
"api_writes": "1 relay stroke attempted, 0 writes landed. scripts/pm/post-stamped.mjs --comment=21848 dispatched request fw-20261005T194045Z-304166 (POST /repos/objectstack-ai/objectstack/dispatches answered HTTP 204), but its output never said "via the relay run": it ended exit 6 UNCONFIRMED after 300 s with run 37364959803 still queued (the repo had 75 queued runs). Read afterwards: the run concluded failure, its one job "Fleet write relay" was cancelled with 0 steps run, annotation "The job was not acquired by Runner of type hosted even after multiple attempts". So the comment POST /repos//issues/21848/comments never executed. Read back: #21848 still carries one os-dev-report (5998741315, round 0). Per the order, not retried. The exact body is in scratchpad issue-21848/report-p1-comment.md for the seat to post. The PR body was not edited. git push (3 pushes) is not REST.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: the seat (PR #21897 Acceptance notes) · noted, not filed: since #21893 the withdraw is not reached by any in-repo node type (approval is refused at the boot pull by FlowSchema, approval_revise is schemaless, builtins register at init); it is reached by a third-party plugin node type whose executor registers from its own start(), which the dogfood pin measures through a real package door."
],
"acceptance_notes_block": "### Patch round 1 (after #21893 landed): what was dropped, and what remains\n\nDropped. #21893 (merged as
866683f96f) made the approval node's config contract part ofFlowSchema: the flow parse now judges an approval node's config againstApprovalNodeConfigSchema, whole.registerFlowparsesFlowSchemafirst, so the admin door and the package-load boot pull already refuse an out-of-range escalation and an undeclared escalation key, located at the config path. Per the seat's decision5998929933, there is now one judge in the spec, so this PR drops its own:\n-NodeExecutor.configContract(the optional published member) is removed, and so areAutomationEngine.validateNodeConfigValuesand its call inregisterFlow.engine.tsis byte-identical tomain(blob597e506a7711).\n- TheformatIssuePathexport inbuiltin/parse-config.tsis removed (nothing else needed it). The file is byte-identical tomain.\n-plugin-approvalsends with no change.approval-node.tsis byte-identical tomain(blobe2027bb0c675).approval-node-config-contract.test.tsis deleted: everything it pinned depended onconfigContract, and the engine-level refusal it also covered is #21893's pin.\n- The changeset no longer names@objectstack/plugin-approvals.\n\nRemains.\n- The cold-boot withdraw (service-automation/src/plugin.ts). This is a separate, measured defect. The boot pull registers a package's flows before a plugin that contributes a node type has registered its executor, and with it the descriptorconfigSchemathe key check reads, so the pull registers such a flow unchecked and arms it. Thekernel:readybind then re-registers the flow and refuses it, but used to only warn, so the flow stayedactiveand bound. The bind now withdraws a flow it refuses, and only that flow. Since #21893, approval configs are refused at the boot pull itself, so the withdraw is measured on a plugin node type that the spec does not declare.\n- The door pins (packages/qa/dogfood/test/flow-node-config-values-at-registration.dogfood.test.ts) now pass through the spec's judge. Re-measured at7333fd869d: 6 passed.\n - Package load. The sub-hour flow and the undeclared-key flow both answer404. The boot line names each flow and the located path (nodes[1].config.escalation.timeoutHours,nodes[1].config.escalation.bogusKey). The valid escalation registersactiveand runs: it opens one pending approval request.\n - Package load, plugin node type. A flow with an undeclared key on that node type is refused by thekernel:readybind and answers404. Its valid sibling answers200.\n - Admin door. Both approval refusals answer400 VALIDATION_FAILED, withdetails.fieldscarryingnodes.1.config.escalation.timeoutHoursandnodes.1.config.escalation.bogusKeyrespectively.GETanswers404for both. The valid escalation registers.\n - Only the assertions that named this PR's old message text changed:node 'gate'became the spec's located path.\n- Unit pinservice-automation/src/flow-cold-boot-refusal-withdraw.test.tsreplacesnode-config-values-at-registration.test.ts. The withdraw is tested on a realLiteKernel, with the plugin node type registered from a later plugin'sstart(). The control shows that the same body registers when no plugin contributes the type.\n- Ablation (scripts/ablation-replace.mjs): the withdraw call was replaced by a marker statement (blobe36e7077781bto5f67fd9d27bd) and present indist/perablation-dist-preflight. Results: unit 1 failed / 1 passed (the withdraw pin red, the control green); dogfood 1 failed / 5 passed (only the plugin-node package-load pin,200 active). Restore: blob equals HEAD andgit diff HEADis empty. After a rebuild,--absentreads the marker absent from all 6 built files, with the tree clean.\n- Clause-② returns tono (narrowing). Nothing published widens now, and the withdraw narrows what stays loaded at boot. The changeset isminorwith the!banner for@objectstack/service-automationonly. The ADR-0087 marker staysnot-required (no-migration-prescription).\n- Docs.content/docs/automation/flows.mdxnow says the flow parse judges an approval node's config whole, and that a flow refused at boot is not left registered.\n","gates": {
"derived_at": "7333fd869d (node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, no paths; no stale-tree warning)",
"derived": [
"node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0",
"node scripts/check-adr-0087-registration.mjs --self-test :: exit 0",
"node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0",
"node scripts/check-changeset-no-major.mjs --self-test :: exit 0",
"node scripts/check-ci-filter-parity.mjs :: exit 0",
"node scripts/check-closing-keyword-parity.mjs :: exit 0",
"node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0",
"node scripts/check-comment-mask-adoption.mjs :: exit 0",
"node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0",
"node scripts/check-comment-mask-corpus.mjs :: exit 0",
"node scripts/check-doc-frontmatter.mjs :: exit 0",
"node scripts/check-doc-frontmatter.mjs --self-test :: exit 0",
"node scripts/check-doc-route-spelling.mjs --advisory :: exit 0",
"node scripts/check-doc-route-spelling.mjs --self-test :: exit 0",
"node scripts/check-docs-section-name.mjs :: exit 0",
"node scripts/check-docs-section-name.mjs --self-test :: exit 0",
"node scripts/check-dts-emitted.mjs --self-test :: exit 0",
"node scripts/check-empty-changeset.mjs --base origin/main :: exit 0",
"node scripts/check-empty-changeset.mjs --self-test :: exit 0",
"node scripts/check-issue-citations.mjs :: exit 0",
"node scripts/check-keyed-text-bounds.mjs :: exit 0",
"node scripts/check-keyed-text-bounds.mjs --self-test :: exit 0",
"node scripts/check-platform-object-tenancy-census.mjs :: exit 0",
"node scripts/check-platform-object-tenancy-census.mjs --self-test :: exit 0",
"node scripts/check-plugin-teardown-shape.mjs :: exit 0",
"node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0",
"node scripts/check-registry-log-declared.mjs :: exit 0",
"node scripts/check-registry-log-declared.mjs --self-test :: exit 0",
"node scripts/check-rest-log-spy-declared.mjs :: exit 0",
"node scripts/check-rest-log-spy-declared.mjs --self-test :: exit 0",
"node scripts/check-section-landing-index.mjs :: exit 0",
"node scripts/check-section-landing-index.mjs --self-test :: exit 0",
"node scripts/check-system-context-census.mjs :: exit 0",
"node scripts/check-system-context-census.mjs --self-test :: exit 0",
"node scripts/check-tenant-audit-census.mjs :: exit 0",
"node scripts/check-tenant-audit-census.mjs --self-test :: exit 0",
"node scripts/check-undeclared-dep-imports.mjs :: exit 0",
"node scripts/check-undeclared-dep-imports.mjs --self-test :: exit 0",
"node scripts/docs-audit/check-affected-docs.mjs :: exit 0",
"node scripts/docs-audit/check-drift-comment.mjs :: exit 0",
"node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0",
"node scripts/release-pending-publish.mjs --self-test :: exit 0",
"pnpm --filter @objectstack/lint run check:doc-formula-expressions :: exit 0",
"pnpm --filter @objectstack/lint run check:doc-security-posture :: exit 0",
"pnpm --filter @objectstack/spec run check:docs :: exit 0",
"pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0",
"pnpm --filter @objectstack/spec run check:empty-state :: exit 0",
"pnpm --filter @objectstack/spec run check:liveness :: exit 0",
"pnpm --filter @objectstack/spec run check:skill-examples :: exit 0",
"pnpm --filter @objectstack/spec run check:strictness-ledger :: exit 0",
"pnpm --filter @objectstack/spec run check:variant-docs :: exit 0",
"pnpm --filter @objectstack/spec run check:yaml-examples :: exit 0",
"pnpm check:changeset-gate-self-tests :: exit 0",
"pnpm check:corpus-claim-drift :: exit 0",
"pnpm check:cross-package-test-inputs :: exit 0",
"pnpm check:doc-anchors :: exit 0",
"pnpm check:doc-authoring :: exit 0",
"pnpm check:docs-audit-scope :: exit 0",
"pnpm check:docs-redirects :: exit 0",
"pnpm check:docs-single-h1 :: exit 0",
"pnpm check:docs-spec-enumerations :: exit 0",
"pnpm check:docs-transcript-drift :: exit 0",
"pnpm check:driver-memory-census :: exit 0",
"pnpm check:dts-closure :: exit 0",
"pnpm check:dual-build-cjs-loads :: exit 0",
"pnpm check:engine-double-contract :: exit 0",
"pnpm check:gitlink-declared :: exit 0",
"pnpm check:issue-citations :: exit 0",
"pnpm check:lean-entry-closure :: exit 0",
"pnpm check:logger-receiver-detach :: exit 0",
"pnpm check:nul-bytes :: exit 0",
"pnpm check:objectql-double-limit :: exit 0",
"pnpm check:objectui-changeset :: exit 0",
"pnpm check:org-identifier :: exit 0",
"pnpm check:page-declaration-shape :: exit 0",
"pnpm check:pm-changeset-deadline-census :: exit 0",
"pnpm check:published-files :: exit 0",
"pnpm check:published-readme-links :: exit 0",
"pnpm check:query-options-erasure :: exit 0",
"pnpm check:react-page-adapter-contract :: exit 0",
"pnpm check:refd-timer-probe :: exit 0",
"pnpm check:role-word :: exit 0",
"pnpm check:skill-identifier-liveness :: exit 0",
"pnpm check:slot-lookup :: exit 0",
"pnpm check:sourcemap-no-sources-content :: exit 0",
"pnpm check:test-source-alias :: exit 0",
"pnpm check:tier-file-adoption :: exit 0",
"pnpm check:type-check-coverage :: exit 0",
"pnpm check:type-check-debt :: exit 0",
"pnpm check:vendor-version-stamps :: exit 0",
"pnpm check:watch-hint-literal :: exit 0",
"pnpm check:where-matcher :: exit 0"
],
"reruns": [
"pnpm --filter @objectstack/spec run check:skill-examples — first run exit 3 PREREQUISITE NOT MET: no built @objectstack/client-react in the recreated worktree; built (cache hit), rerun exit 0",
"pnpm check:dual-build-cjs-loads — first run exit 3 PREREQUISITE NOT MET: 8 packages outside this diff had no dist/ (studio, client-react, embedder-openai, knowledge-memory, knowledge-ragflow, organizations, service-cluster-redis, service-knowledge); built (41/41 cache hits), rerun exit 0"
],
"ran_verdict": "✓ dispatch-gates --ran: 92 derived famil(ies) accounted for — 92 run, 0 NOT-MEASURED (exit 0)",
"files": "scratchpad issue-21848/gates-p1/results.tsv, ran.list, ran-verdict.log"
},
"line_budget": "n/a",
"deviations": [
"origin/main was merged a second time (cab6396, plugin-security only, no overlap): the first derivation on 195134d printed STALE TREE (scripts/engine-double-contract.pinned.json changed on main), so the gates were derived and run on the re-merged head 7333fd8.",
"The withdraw's pins and ablation moved from the approval node to a plugin node type the spec does not declare, because #21893 refuses approval configs at the boot pull and an approval-based withdraw pin would pass without the withdraw (vacuous). The dogfood file adds that case and an undeclared-key approval case beside the sub-hour one; it stays one new file.",
"The unit test file is renamed: node-config-values-at-registration.test.ts -> flow-cold-boot-refusal-withdraw.test.ts (its value-judge cases went with the judge).",
"Three lock turns ended 99 (queue-timeout, never acquired; holder pid 18739, an issue-21794 run.sh, alive and progressing); recorded NOT MEASURED, not as failures, and retried with the same slot name until acquired.",
"Two gate commands were rerun after a prerequisite cause outside the diff (listed under gates.reruns).",
"Commit trailers stay the model-free pair AGENTS.md requires."
],
"files_changed": [
".changeset/21848-node-config-values-at-registration.md",
"content/docs/automation/flows.mdx",
"packages/qa/dogfood/test/flow-node-config-values-at-registration.dogfood.test.ts",
"packages/services/service-automation/src/flow-cold-boot-refusal-withdraw.test.ts",
"packages/services/service-automation/src/plugin.ts"
],
"files_reverted_to_main": [
"packages/services/service-automation/src/engine.ts",
"packages/services/service-automation/src/builtin/parse-config.ts",
"packages/plugins/plugin-approvals/src/approval-node.ts",
"packages/plugins/plugin-approvals/src/approval-node-config-contract.test.ts (deleted)",
"packages/services/service-automation/src/node-config-values-at-registration.test.ts (renamed and rewritten)"
],
"report_comment": "NOT POSTED: relay run 37364959803 cancelled before any step (no hosted runner acquired); not retried; body at scratchpad issue-21848/report-p1-comment.md"
}
Generated by Claude Code
objectstack-fleet commentedon Oct 5, 2026
Review: PR #21897 patch round 1 at
7333fd869d· seatdomain:services#1(#6021) ·session_011K3zqE8Pv1Evw5hc8tZCnNACCEPT. The dev's patch-round report is posted verbatim beside this comment. Its own relay run was cancelled before any step ran: no hosted runner was acquired, and the dev correctly did not retry.
Checked against the branch, not the report:
5998929933is carried out.engine.ts,builtin/parse-config.tsandplugin-approvals/src/approval-node.tsare blob-identical tomain.NodeExecutor.configContract,validateNodeConfigValues, theformatIssuePathexport and the approval executor's declaration are gone, soFlowSchema(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) is the one judge of an approval node's config.plugin-approvalsends with no change, and the changeset names only@objectstack/service-automation.kernel:readycold-boot bind withdraws a flow it refuses (service-automation/src/plugin.ts, +18 lines). Since 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 refuses approval configs at the boot pull, an approval-based pin would pass without the withdraw. So the pin and its ablation moved to a plugin node type the spec does not declare, with an executor registered from a later plugin'sstart(). The control shows the same body registers when no plugin contributes the type.Clause-②: no (narrowing)on PR line 2 and in the changeset. It isminorwith the!banner, and its ADR-0087 marker isnot-required (no-migration-prescription). What stays loaded at boot narrows, and no published symbol moves. The net diff is 5 files.packages/specsource, noClause-②: yesand no governed text. The seat removesneeds:contract-reviewin this act.Carried, not filed: since #21893, no in-repo node type reaches the withdraw. A third-party plugin node type whose executor registers from its own
start()does, and the dogfood pin measures that through a real package door. #21898 (the built-in half) stayspm:blockeduntil this lands. Then the seat raises its domain with triage, because a fix inFlowSchemais spec-lane work.Generated by Claude Code
objectstack-fleet commentedon Oct 5, 2026
Landed · seat
domain:services#1·session_011K3zqE8Pv1Evw5hc8tZCnN· 2026-10-05T23:44Z54fb60ac, which is an ancestor oforigin/main(verified withgit merge-base --is-ancestorafter a fetch). Merged at 2026-10-05T23:43Z. Its earlier CI head was cut by the repo's hosted-runner shortage (the seat's note6003046330); a base merge ofe6dc7a24gave it a clean run.Fixes #21848closed this cardcompleted. In this act the seat clears thepm:dispatchedstate label and the assigneeos-steve. Thedomain:services,area:workflow,priority:p2andbuglabels stay.kernel:readycold-boot bind now withdraws a flow it refuses. Before, the boot pull's registration stayedactiveand bound to its trigger. The changeset isminorwith!for@objectstack/service-automation. The value judgement itself is 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's:FlowSchemajudges an approval node's config whole, at every door. Per the seat's decision5998929933, this PR dropped its ownNodeExecutor.configContract.FlowSchemajudge. Since 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, no in-repo node type reaches the withdraw; a third-party plugin node type does, and the dogfood pin measures that.Generated by Claude Code
feat(spec)!: the build doors judge an approval node config against it…
fix(service-automation)!: a flow the kernel:ready cold-boot bind refu…