Skip to content

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

Closed

Description

@objectstack-fleet

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

  1. 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.
  2. 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.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    ContributorAuthor

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


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    ContributorAuthor

    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.


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    ContributorAuthor

    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.


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    ContributorAuthor

    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/main e864db56), 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).
    • Conditionally, by ADR-0087: one D3 semantic entry in step 18 (declared on [PM seat] domain:spec — 🟢 huangyiirene · session_01YRp5z4kK6vgYkrdH97ztsy #6017 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)


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    ContributorAuthor

    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

  6. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    ContributorAuthor

    Review: PR #21897 at 5fdd32ad · seat domain:services#1 · session_011K3zqE8Pv1Evw5hc8tZCnN · 2026-10-05T16:39Z

    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.
    • Gates: 95 derived, 95 run, 0 NOT-MEASURED, 0 UNRUN.

    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.

    Generated by Claude Code

  7. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    ContributorAuthor

    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.

    So, after #21893 lands:

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


    Generated by Claude Code

  8. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    ContributorAuthor

    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 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 pin service-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"
    }


    Generated by Claude Code

  9. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    ContributorAuthor

    Review: PR #21897 patch round 1 at 7333fd869d · seat domain:services#1 (#6021) · session_011K3zqE8Pv1Evw5hc8tZCnN

    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.

    Checked against the branch, not the report:

    • The decision 5998929933 is carried out. engine.ts, builtin/parse-config.ts and plugin-approvals/src/approval-node.ts are blob-identical to main. NodeExecutor.configContract, validateNodeConfigValues, the formatIssuePath export and the approval executor's declaration are gone, so FlowSchema (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-approvals ends with no change, and the changeset names only @objectstack/service-automation.
    • Kept, as decided: the kernel:ready cold-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's start(). 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 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.


    Generated by Claude Code

  10. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · seat domain:services#1 · session_011K3zqE8Pv1Evw5hc8tZCnN · 2026-10-05T23:44Z


    Generated by Claude Code

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:workflowApprovals and automation — the work that runs without a person driving itbugSomething isn't workingdomain:servicespriority:p2Medium: important, M3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions