Uh oh!
There was an error while loading. Please reload this page.
feat(spec,approvals): escalation.enabled defaults to true and the SLA sweep enforces it (#12278) - #12737
Conversation
…eep enforces it (#12278) Maintainer ruling 2026-08-27 (Option C): the feature switch is whether an escalation block exists at all; within a block carrying timeoutHours, escalation is on unless explicitly turned off. - spec: ApprovalEscalationSchema.enabled .default(false) -> .default(true), declared in DEFAULT_CHANGES_BY_MAJOR (17) and registered as the approval-escalation-enabled-default-flip semantic migration entry; authorable-defaults, reference docs, spec-changes and upgrade guide regenerated by repo tooling. - runtime: runEscalations skips snapshots carrying an explicit enabled === false; snapshots created before ESCALATION_ENABLED_FLIP_CUTOFF_MS ride a read-side legacy window (they carry the old default materialized by the executor parse and are escalating today) - zero tenant writes, self-retiring as pending requests drain. - tests: explicit-false-does-not-escalate pin, legacy-window pin, omitted-key pin; spec pin for the new default materialization. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JvjTCjJQn9zSTXEhUKgT7s
📓 Docs Drift CheckThis PR changes 2 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 126 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 0554f4cd70f8f4ef4dd101244f795cd24341a8ea && git checkout 0554f4cd70f8f4ef4dd101244f795cd24341a8ea
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 86df0c99e80ad8d280015994cf4b4dbdf0b77342 71ccc576dd90c88117c9a53b0047ed7ea1d8084a && git checkout -B drift-repro 86df0c99e80ad8d280015994cf4b4dbdf0b77342 && git merge --no-ff 71ccc576dd90c88117c9a53b0047ed7ea1d8084a
node scripts/docs-audit/affected-docs.mjs --json 86df0c99e80ad8d280015994cf4b4dbdf0b77342
|
…ebt ratchet back to 347
The three new escalation pins accessed row properties (.id, indirectly
created_at) on the raw openNodeRequest return type
(ApprovalRequestRow | ApprovalNodeAutoOutcome), adding 8 raw TS2339
errors to the plugin's shrink-only TEST_DEBT layer (347 recorded, 355
measured). Fixed with the file's own sanctioned narrowing pattern
('autoApproved' in req => throw) after each open — no ledger change, no
any. Raw count re-measured at exactly 347; composition returns to the
ledger note to the unit (TS2339 x296).
Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JvjTCjJQn9zSTXEhUKgT7sUh oh!
There was an error while loading. Please reload this page.
Fixes#12278
Implements the Option-C package ruled by the maintainer on 2026-08-27 (issue comment 5434864364, which explicitly reverses point 2 of the 2026-08-26 Route-1 ruling): the spec declaration moves to the truth, and the ruled sweep gate lands verbatim on top of it.
What changed
ApprovalEscalationSchema.enabled:.default(false)→.default(true)(staysz.boolean(), no tri-state), with a docstring teaching the ruled semantics: the feature switch is whether anescalationblock exists at all; within a block carryingtimeoutHours, escalation is on unless explicitly turned off (packages/spec/src/automation/approval.zod.ts:616).packages/spec/scripts/lib/default-changes.tsgains theautomation/ApprovalEscalation:enabledentry under major 17 (check:authorable-surfacerefuses an in-place default move until it is declared there — measured, the gate is green with the entry and the flip).packages/spec/src/migrations/entries/semantic/17.approval-escalation-enabled-default-flip.tsregisters the semantic migration entry (precedent:17.import-run-automations-declared-default-corrected, the previous declared-default correction).runEscalationsnow skips a pending request whose snapshot carries an explicitescalation.enabled === false(packages/plugins/plugin-approvals/src/approval-service.ts). Strict=== false: an absent key escalates (pre-materialization historical rows keep behaviour, and the new default materializestrue).created_at— snapshots created beforeESCALATION_ENABLED_FLIP_CUTOFF_MS(2026-08-28T00:00:00Z) are exempt from the gate: their storedfalseis the old schema default materialized by the executor parse, and every storedfalse + timeoutHoursrow is escalating today (the ruling's measured basis), so exempting them is behaviour-preserving. Zero tenant rows rewritten; the window self-retires as pending requests drain.authorable-defaults/automation.jsonnow records= true(regenerated); generated reference pages (content/docs/references/automation/approval.mdx) regenerated by repo tooling;spec-changes.json+docs/protocol-upgrade-guide.mdregenerated from the registry.@objectstack/specminor +@objectstack/plugin-approvalsminor, with the BREAKING annotation (launch-window convention) and theadr-0087: registered approval-escalation-enabled-default-flipdisposition (the marker thecheck:adr-0087-registrationgate requires for a declared-breaking changeset — measured green).Measured: surfaces the ruling listed that needed NO edit
skills/objectstack-automation/SKILL.md— measured for every spelling of a default-false statement (default.*false,false.*default,off by default,disabled by default, case-insensitive): zero hits. Line 610 documents the key set without stating a default and the "setenabled: true" guidance stays valid, so the net-line-budget-0 constraint is met by a zero-line diff. No skills edit in this PR.content/docs/automation/approvals.mdx— same measurement, zero default statements; its "setenabled: trueandtimeoutHours" guidance stays valid per the ruling. Untouched.docs/adr/0042-approval-sla-escalation.mdnever mentionsenabled(grep, zero hits) — no ADR governs the flipped default; ADR-0049 is the gap this closes.Transition choice: (i) read-side over (ii) normalization — reasoning
The ruling offered (i) read-side keyed on snapshot
created_at(zero tenant writes, self-retiring) or (ii) idempotent normalizationenabled := true where timeoutHours > 0, pick the cheaper after verifying premises. Measured: this repo's migration precedents (metadata-protocol/src/migrations/*, plugin-approvals' ownbackfill-platform-row-organizations.ts) run per-boot and rely on predicate idempotency — there is no run-once ledger. Normalization's predicate (enabled=false ∧ timeoutHours>0) re-matches post-flip authored-false rows forever, so a per-boot (ii) would eventually rewrite exactly the rows the new pin protects (an explicitfalsea code-first author writes after the flip), and a safe (ii) needs new run-once machinery plus tenant-data rewrites. (i) needs neither. Chosen: (i).Honest bound, recorded in the constant's doc comment: the cutoff is a compiled constant, so an install that upgrades long after 2026-08-28 has pending requests created between the cutoff and its upgrade whose materialized
falsepost-dates the cutoff; those honour the declared off. The population is bounded by each request's owntimeoutHourswindow around the upgrade and self-drains. The alternatives (per-install marker row, or normalization) buy that edge back at the price of new machinery or tenant writes — both the more expensive option under the ruling's instruction.Premise readings (dev-FIRST, measured before any edit)
d29e42f.runEscalationsreadssys_approval_request(status: 'pending') →node_config_json;escalateRequestis called only from the sweep; repo-wide grep for(esc|escalation|escalationConfig|escalationCfg|sla)\.enabledoverpackages/+apps/: zero hits outside the declaration.slaDueAtreads onlytimeoutHoursand stays ungated (ruled, point 6).FlowSchemanodeconfigisz.record(z.string(), z.unknown())(flow.zod.ts:271) — flow-level parse never descends intoApprovalNodeConfigSchema; the executor (approval-node.ts:133, re-verified) is the only production parse site, and its parse happens at request-open time. So the transition's reach is request snapshots only: a definition that omitsenabledopens post-flip requests that materializetrue(behaviour preserved), and a definition with authoredfalseopens requests that are skipped (the declared intent, per the ruling).created_atavailability: confirmed.openNodeRequeststampscreated_atfrom the injected clock (approval-service.ts:2064/2117), the sweep already readsraw.created_atfor the due computation, and the column is declared onsys-approval-request.object.ts:296. A row with a missing/unparsablecreated_atfails the cutoff comparison into the legacy (escalate) branch and is then dropped by the existing due-date check — no new failure mode.getApprovalNodeConfigJsonSchema, republished to the Studio designer with the new default automatically). Spec tests and lint fixtures authorenabled: trueexplicitly and are unaffected. objectui mirrors are out of this PR's surface — one drift noted for the PM below.Tests & verification (gate union, pin file and ablation at head
64cff1b8; the full suites ran on the byte-identical tree the single commit captured — zero edits between suite runs and commit,git statusclean before and after)@objectstack/spec: full suite 435 files / 11542 tests passed +typecheckgreen in the same lock-held run (VERDICT command-exit 0). New pin: omittedenabledinside a block materializestrue; explicitfalsesurvives; a config with no escalation block keeps the key absent (no-block vs declared-off stay distinguishable).@objectstack/plugin-approvals: full suite 33 files / 613 tests passed +typecheckgreen (VERDICT command-exit 0). The 11 pre-existing behaviour tests (omitenabled, expect escalation) stay green and are now honest. Three new pins, each asserting concrete outcomes (status, audit-action sequence, resume calls — not just counters):enabled: falsepost-flip snapshot does not escalate (auto_approveshape: stayspending, actions =['submit'], zero resumes);falsekeeps escalating (legacy window:approved,['submit','escalate','approve'], resume fired);esc.enabled === falsetoken count 1→0,git diff --stat2 deletions; the test consumes the mutated file via a same-package relative src import, so no build leg applies), run: exactly pin 1 red, 291 passed (predicted direction: escalation fires), restore proven by blob equality (git hash-object= HEAD blobdcb137ef…) + emptygit diff HEAD. Script carriedtrap restore EXIT INT TERMwith absolute paths.check:generated: all 14 artifacts up to date (two passes).check:authorable-surfacegreen with the declared default change;check:adr-0087-registrationgreen on the changeset marker;check:changeset-no-majorgreen.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(no paths — change set derived by the script from merge-based29e42f8, 12 paths, stderr header verified naming this repo at commit64cff1b8): 49 derived commands +check:nul-bytes, all run at64cff1b8. 47 passed outright; three were prerequisite refusals, not red measurements, each read from the gate's own printed verdict:check:skill-examplesrefused on an unbuiltclient-react/dist— prerequisite built, gate re-run:✅ 260 prose examples type-check across 3 surface(s)(VERDICT command-exit 0).check-dev-prereqs.mjsprintedThe workspace is not built — 1 unmet precondition, not a list of problems(32/67 packages have no local dist; this worktree deliberately builds only affected closures). The gate comparespackage.jsonentry declarations against dist presence; this diff touches nopackage.json, so the reading is about local build state, not this change — CI runs it on a fully built tree. Declared narrowing.check-half-states.mjsexited 3 with its ownPREREQUISITE NOT MET — the token in the environment is not a valid GitHub credentialtext (container proxy placeholder token; the gate's text: "It is not a clean board and it is not a dirty one — it is no reading at all"). Not diff-sensitive; CI carries credentials. Declared narrowing.default-changes.tsedit is gate-owned surface, so its own suite ran in addition to the derived families:packages/spec/scripts/authorable-defaults.test.ts— 23 tests passed.Note for the PM (out of this PR's surface)
objectui's flow-node inspector declares the
escalation.enabledtoggle withdefaultValue: 'false'(flow-node-config.ts:732) — post-flip that reads "off" for a block the runtime now treats as on. Same territory as objectui#6499 (no-prune-on-save); flagged in the report for routing rather than edited here.This PR touches no governed surface (
skills/**measured zero-diff), carries the required changeset, and is delivered as a draft for PM review — no ready/enqueue action taken by this seat.Generated by Claude Code
Generated by Claude Code