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
approvals: escalation.enabled has zero readers — the SLA sweep gates only on timeoutHours, so enabled: false still escalates #12278
Filed by an os-dev agent working an unrelated card in www.objectos.ai (#63, an article about pause/resume and approval SLAs). Recording it rather than fixing it — out of scope for that card. No assignee.
What was measured
On origin/main at 107bb4b:
ApprovalEscalationSchema declares enabled with a false default:
packages/spec/src/automation/approval.zod.ts:616
enabled: z.boolean().default(false).describe('Enable SLA-based escalation for this node'),
The sweep that actually escalates never reads it. It gates on timeoutHours alone:
has stated, in the vocabulary the schema gave them, that this SLA is off. The sweep escalates anyway — and with action: 'auto_approve' that silently approves a request the author believed was not on a clock. The declared default (false) makes it worse: an author who sets timeoutHours and nothing else gets escalation they never turned on, while the schema tells them the feature defaults to off.
Note this is the mirror image of the waitEventConfig.timeoutMs / onTimeout retirement (#4158): those were removed because they claimed a behaviour nothing delivered. This one delivers a behaviour the key claims to gate.
Routes (for triage, not a recommendation to skip the ruling)
Enforce — add if (esc.enabled === false) continue; to runEscalations. Cheapest, but it changes behaviour for any stored flow that set enabled: false alongside a real timeoutHours and has been escalating; that population should be measured before the flip.
Remove — retire enabled and let the presence of timeoutHours be the switch, which is what the runtime already means. One fewer key, and the remaining one cannot disagree with itself.
Either way the ruling is a maintainer's, not this agent's. The liveness question ("who authors enabled, and what did they expect?") wants the usual three-repo read before the route is picked.
Not addressed here
#63 in www.objectos.ai is a content card and does not touch this code. The article deliberately does not document the defect — it shows enabled: true (the correct-as-declared spelling) and makes no claim about what happens when it is false, so it stays accurate under either route above.
Claim: PM loop round R7, wave 5 — measurement half ONLY (the triage charter's own step 1; the route ruling is the maintainer's — the card says so, and escalation/auto-approve semantics sit on the security-adjacent manual floor)
Session: session_01E5LFCYBJ3q2s6yW6oMLxwy
Branch: claude/issue-12278-escalation-enabled-census (measurement record only; expected zero code changes — a branch exists only if the dev needs to commit a measurement script artifact, which it should not)
Worktree: objectstack-issue-12278
Domain: domain:spec
File surface: NONE (read-only measurement across objectstack + objectui; cloud and hotcrm are outside this session's reach and will be declared unmeasured, not guessed). Any code change is a breach — stop and report.
Container & model: S, mode:subagent, model: opus — measurement work; the clause-② fable tier applies to the post-ruling IMPLEMENTING dispatch, not this census (recorded so the tier ledger stays honest)
Clause-②: no (this dispatch changes nothing; the card's eventual implementing dispatch is the clause-② yes one, per triage)
Serial constraints cleared: zero file surface ⇒ no serial constraints; in-flight #12248 (contracts/data-engine.ts) and landing-watch PRs untouched. H17: no hit.
Claim (dev): measurement-only — census half of the enforce-or-remove ruling. No branch, no worktree, no code change expected; every read below is against origin/main refs (git show <ref>:<path>), no checkout touched.
Session: session_01E5LFCYBJ3q2s6yW6oMLxwy — note this is the same session ID as the PM claim comment above, because this dev runs as a subagent of the dispatching PM seat and shares its session URL. It is not a competing claim; the PM claim and this one are the same actor. No branch will appear for this card.
Scope: objectstack + objectui (both reachable). cloud and hotcrm are outside this session's reach and will be declared unmeasured with the exact commands a seat with access would run — not guessed.
Measurement half only — no code change, no branch, no PR. Every read below is against an origin/main ref via git show <ref>:<path> / git grep <ref>; no checkout was modified.
repo
ref measured
card measured
objectstack-ai/objectstack
b6c96bcea5d4352c5a63b4aa9a8d6809b9ce15cf
107bb4b
objectstack-ai/objectui
7975f2d85e2c2dd20b27658618544818a337a495
not measured by the card
Headline: the card's "zero readers" holds for the RUNTIME, and is incomplete for the PLATFORM
The card grepped objectstack only. objectui carries a live, load-bearing reader of escalation.enabled — the metadata-admin flow-node inspector uses it as the progressive-disclosure controller for the entire escalation block. This does not contradict the card's runtime finding; it adds a consumer the route ruling has to price. Details in §3.
1. Who authors escalation.enabled?
objectstack — 5 authored writes, all true, all test/doc fixtures
Zero product code, zero example app, zero seed, zero template authors this key.examples/app-showcase and examples/app-todo mention "escalation" only as flow names, notify topics and priority-escalation prose — neither authors an approval config.escalation block at all (git grep -i escalat origin/main -- examples/ → 30 hits, none a config block).
Docs that instruct authors to set it:
content/docs/automation/approvals.mdx:629 — "escalation is real, not decorative: set enabled: true and timeoutHours"
No packages/spec/liveness/ row exists for ApprovalEscalation yet.
objectui — zero authored configs, one live reader (see §3)
git grep -n "timeoutHours" origin/main in objectui returns only field declarations and their tests. No stored/authored escalation config anywhere in the repo.
Instrument disclosure — the control caught a real instrument bug
Control (a) is why this census is trustworthy. My first instrument found zero of the three lint-fixture writes the card names: its opener regex excluded a .-prefixed member assignment, so node.config.escalation = { enabled: true, … } never matched. Fixed ([^A-Za-z0-9_.] → [^A-Za-z0-9_]), after which control (a) passes — all three card-named writes found (lines 140/151/153).
A second, independent instrument (every enabled token repo-wide whose ±8-line neighbourhood mentions escalat) then found two more real writes the fixed opener still missed (approval.test.ts:303,306 — single-line ApprovalEscalationSchema.parse({ enabled: true, … }), no escalation: opener on the line). Both instruments plus a same-line git grep -i -E "escalat.*enabled|enabled.*escalat" agree on the union above. Confirmed false positives, discarded: IncidentResponsePolicySchema.enabled (incident-response.zod.ts:327, different schema), authorable-defaults/system.json:115,120, ADR-0066 tenancy: { enabled: false }.
Control (b) — the neighbouring key, same instrument
timeoutHours is not zero, so a zero on enabled is about the key, not the tool:
The asymmetry is the finding: all 11 runtime-behaviour tests in plugin-approvals author escalation with timeoutHours and omit enabled entirely — e.g. approval-service.test.ts:1688:
Under the declared default (enabled: false) every one of these is a config the schema says is off, and the suite asserts it escalates. The runtime's own test suite encodes timeoutHours-is-the-switch.
2. Stored-flow hazard population (enabled: false + real timeoutHours)
In-repo population: ZERO. Every authored enabled in both reachable repos is true. No file in objectstack or objectui authors enabled: false alongside a timeoutHours.
But the repo is the wrong place to look, and zero here must not be read as zero in the field. The console manufactures exactly this shape:
objectui packages/app-shell/src/views/metadata-admin/inspectors/flow-node-config.ts:732-745 declares escalation.enabled (defaultValue: 'false') as the controller, and gates timeoutHours, action, escalateTo, notifySubmitter behind showWhen: { field: 'escalation.enabled', equals: ['true'] }.
isFieldVisible (same file, :1050-1070) additionally returns true "when the field already holds a stored value (so existing config is never hidden)".
FlowNodeInspector.tsx:229 — const visibleFields = fields.filter((f) => isFieldVisible(f, node, fields)); — filters rendering only. There is no prune-on-save: nothing deletes a hidden field's stored value.
So a console author who enables SLA escalation, sets timeoutHours: 24, then toggles the "SLA escalation" switch back off, commits { enabled: false, timeoutHours: 24 } — the hazard shape, one click away, produced by the platform's own authoring UI, whose toggle is labelled SLA escalation / SLA 升级 and helped as "Escalate when a decision is not recorded within the timeout."
The actual population lives in tenant databases (sys_approval_request.node_config_json / stored flow metadata), not in any repo, and is not measurable from a git checkout by any seat. It is the one number route 1 needs and this census cannot supply. A query against a live deployment is the only instrument; see §4.
3. Re-verification of the zero-reader claim on the current tree
Re-measured at b6c96bc (card measured 107bb4b).
Runtime — claim CONFIRMED, unchanged, same line numbers:
slaDueAt (:474-476) reads cfg?.escalation?.timeoutHours — also no enabled.
git grep -nE "(esc|escalation|escalationConfig|escalationCfg|sla)\s*\??\s*\.\s*enabled" origin/main across all of objectstack → zero matches. No new reader has appeared since 107bb4b.
Platform — claim needs AMENDING. objectui reads it, as an authoring-UI controller:
pins the derived field + all four showWhen wirings
So the accurate statement is: escalation.enabled has zero runtime readers and one authoring-UI reader. The key is inert where it is enforced and load-bearing where it is authored — which is precisely the disagreement the card describes, now with a name on both halves.
4. Unmeasured legs — declared, not guessed
cloud and hotcrm are outside this session's reach (not attached; only objectstack and objectui are present). Neither was measured; no inference is offered about them. www.objectos.ai was measured by the card's filer only (it reports the article shows enabled: true) — second-hand, not re-verified here.
A seat with access should run, per repo:
# 1. authored writes of the key (run BOTH instruments — they miss different shapes)
git grep -n -i -E "escalat.*enabled|enabled.*escalat" origin/main | grep -v CHANGELOG
git grep -n -C8 -E "[\"']?\benabled\b[\"']?[[:space:]]*[:=][^=]" origin/main | grep -i -C8 escalat
# 2. the hazard shape specifically
git grep -n -A6 -E "[\"']?escalation[\"']?[[:space:]]*[:=]" origin/main | grep "enabled.*false"# 3. runtime readers
git grep -nE "(esc|escalation|escalationConfig|sla)[[:space:]]*\??[[:space:]]*\.[[:space:]]*enabled" origin/main
# 4. control (b) — the neighbouring key must be non-zero or the instrument is lying
git grep -n "timeoutHours" origin/main | grep -v CHANGELOG
And, for the population route 1 actually changes behaviour for — the only instrument that can answer it, against a live deployment, not a repo:
-- stored approval nodes that declare the SLA off but carry a real deadlineSELECT id, node_config_json
FROM sys_approval_request
WHERE json_extract(node_config_json, '$.escalation.enabled') =0AND json_extract(node_config_json, '$.escalation.timeoutHours') >0;
plus the same predicate over stored flow metadata (sys_flow / package flow definitions) for nodes that have not yet opened a request.
The fork, stated neutrally (no recommendation — the ruling is the maintainer's)
Evidence pulling toward route 1 (enforce: gate the sweep on enabled):
The key is on the published authorable surface (authorable-surface/automation.json:25, authorable-surface.base.json:2071) with a documented default; removing it is an ADR-0087 retirement, not a delete.
It has a real consumer today: the console inspector's whole escalation disclosure tree hangs off it (5 wirings + a pinned test + a zh label). Route 2 must rebuild that disclosure on some other controller or flatten it.
Published docs and the published objectstack-automation skill both instruct authors to set it.
It fails open today, and enforcing closes an auto-approve path that currently fires against an explicit enabled: false.
Evidence pulling toward route 2 (remove: timeoutHours is the switch):
Zero runtime readers, re-confirmed at b6c96bc; the enforcement half has never read it.
The runtime's own 11 behaviour tests omit enabled and expect escalation — timeoutHours-as-switch is what the system already means and what its suite pins.
Zero product/example/seed authorship in either reachable repo; the only writers are test fixtures and doc prose.
Two keys that can disagree is the defect class itself; one key cannot contradict itself.
The tension the ruling has to resolve: route 1's blast radius is unmeasured and unmeasurable from here (the stored enabled: false + timeoutHours population lives in tenant DBs, and the console manufactures that shape one toggle away, so it is plausibly non-zero), while route 2's blast radius is fully measured and entirely in-repo (one inspector disclosure tree, one pinned test, one zh label, three doc sites, one published skill table) — but route 2 retires a published key and hands the "is it off?" question to the presence of timeoutHours, which no authoring UI currently presents as a switch.
ACCEPT (measurement half) — reviewer of record: spec seat R7, session_01E5LFCYBJ3q2s6yW6oMLxwy. Zero-trace census verified (no branch, no PR, both checkouts clean); the report comment read back intact. Quality notes for the record:
The instrument bug (member-assignment writes excluded by the opener regex) was caught by the card-named control, disclosed, and cross-checked with two independent instruments — the union is the population. This is the third instrument-discipline save this shift.
Material amendment to the card: "zero readers" is now "zero RUNTIME readers, one AUTHORING-UI reader" (objectui flow-node inspector uses escalation.enabled as the progressive-disclosure controller, 5 showWhen wirings + pinned test + zh label) — this changes both routes' blast radii and is load-bearing for the ruling.
Second finding: the console MANUFACTURES the hazard shape (no prune-on-save; toggling the switch off commits enabled:false + live timeoutHours) — one click from the escalating-against-declaration state.
The runtime's own 11 behaviour tests author timeoutHours and omit enabled — the suite itself pins timeoutHours-as-the-switch. The asymmetry is the finding.
Unmeasured legs declared with exact commands + the SQL predicate for the tenant-DB population — the only instrument that can answer route 1's stored-flow question.
State conversion follows in the next comment: pm:dispatched → needs-user-decision with the four-facet block — the route ruling is the maintainer's (security-adjacent manual floor), now with the evidence it was waiting for.
Maintainer ruling recorded — Route 1 adopted: enforce escalation.enabled
Provenance: maintainer, 2026-08-26, live PM chat (decision-inbox batch 5, skills seat session session_01JANH3y7qe3MD8aLaLXci8N), verbatim: 「12452 不处理,其他接受」 — accepting this card's presented recommendation (Route 1, per the four-facet block above), including the sub-question's disposition (the console inspector defect gets its own objectui card).
What this rules:
The SLA escalation sweep gates on the declared switch: a node whose config carries escalation.enabled === false is skipped — the runtime comes to mean what the entire authoring surface (console inspector, published docs, published skill) already teaches. This closes the measured fail-open path where enabled: false + action: 'auto_approve' silently approves a request the author declared off the clock.
The census's amendment is part of the record: zero RUNTIME readers, one AUTHORING-UI reader — enforcement aligns the runtime with the authoring surface rather than fighting it. Existing stored configs that carry enabled: false alongside a live timeoutHours (the shape the console manufactures) STOP escalating — that is the declared intent being honored, and the changeset says so.
Clause-② yes at the implementing dispatch (accept/escalation behaviour changes), contract-review tier per the standing rule — as the triage comment on this card already scoped.
The sub-defect is split out, not ridden: the metadata-admin inspector's no-prune-on-save behaviour (hidden showWhen fields keep stored values; generic across all groups, escalation is one instance) is filed as its own objectui card by this seat, with a link back here.
State: needs-user-decision → pm:queue in the same stroke — the domain:spec lane dispatches (enforcement lands in the plugin-approvals sweep; the spec-side key description stays as declared).
Sub-defect card filed as ruled: objectstack-ai/objectui#6499 (the inspector no-prune-on-save defect, generic across showWhen groups, objectui queue). This card's own scope stays the runtime enforcement half.
Claim: PM loop round 9 (domain:spec seat) — implementing dispatch under the recorded Option-C ruling (maintainer 2026-08-27, comment 5434864364, which explicitly reverses point 2 of the 2026-08-26 Route-1 ruling)
Session: session_01JvjTCjJQn9zSTXEhUKgT7s
Branch: claude/issue-12278-enforce-escalation-enabled (reusing the empty write-probe branch at f93df4d left by the R8 stopped run, as its report intended)
Worktree: objectstack-issue-12278
Domain: domain:spec
File surface: packages/spec/src/automation/approval.zod.ts (default flip + docstring) · packages/spec/authorable-defaults/automation.json + regenerated spec artifacts (generated reference pages, repo tooling only) · skills/objectstack-automation/SKILL.md (only the default-false statements; net-line budget 0) · content/docs/automation/approvals.mdx (only if a default-false statement exists there) · packages/plugins/plugin-approvals/src/approval-service.ts + approval-service.test.ts (+ approval-node.ts only if the chosen transition needs the executor seam) · .changeset/ (stop on breach; explain in the report)
Container & model: M, mode:subagent, model: claude-fable-5 — clause-② content mandate (spec semantic face + accept/escalation behaviour change, per the ruling's own scoping); dispatch-gates --tier run at d29e42f from a fresh origin/main worktree reports no path-derived mandate and flags packages/spec/src/automation/approval.zod.ts as clause-② suspect surface
Clause-②: yes
Serial constraints cleared: lane in-flight #3653 (mode:cloud, dev session_01EPEkvZe92KwvVUFhgdUR6S) — plugin-auth/platform-objects/workspace-pin surface, fully disjoint from approvals/automation. Lane landing #11846/PR #12718 (spec kernel retirement, CI converging) — source surfaces disjoint; BOTH regenerate shared spec generated artifacts ⇒ landing serialized per the generated-artifact relay (#12718 holds the first slot). Cross-seat hold #12702/PR #12705: disjoint. Open-PR sweep: no open PR touches plugin-approvals/** or automation/approval.zod.ts. H17 on-hold trigger files (#8345/#8346): no intersection. Sibling pin note: this card's runtime pin asserts escalation-sweep behaviour that no in-flight card touches; objectui#6499 (inspector no-prune defect) is deliberately out of scope per the ruling.
Premise re-checked at claim (origin/main d29e42f): enabled: z.boolean().default(false) still at approval.zod.ts:616; sweep still gates on timeoutHours alone (approval-service.ts:3473); zero runtime readers of escalation.enabled; authorable-defaults/automation.json:16 still records = false; probe branch present at f93df4d. Both rulings and the falsification chain (report 5428619265: parse-materialization at approval-node.ts:133-138 → snapshot spread/store at approval-service.ts:2069/2112) read in full before this claim.
Claim (dev): implementing dispatch under the recorded Option-C ruling (comment 5434864364, which reverses point 2 of the 2026-08-26 Route-1 ruling). I run as a mode:subagent dev inside PM session session_01JvjTCjJQn9zSTXEhUKgT7s — same session ID as the PM claim comment above; same actor, not a competing claim.
Branch: claude/issue-12278-enforce-escalation-enabled — continuing the empty write-probe branch at f93df4d left by the R8 stopped run, reset onto current origin/main d29e42f (verified fast-forward; no force). Worktree: objectstack-issue-12278.
Scope per the PM claim's declared file surface: spec default flip + docstring (packages/spec/src/automation/approval.zod.ts), authorable-defaults/automation.json + regenerated spec artifacts, skills/objectstack-automation/SKILL.md default-false statements only (net-line budget 0), content/docs/automation/approvals.mdx only if a default-false statement exists there, packages/plugins/plugin-approvals/src/approval-service.ts + tests (+ approval-node.ts only if the chosen transition needs the executor seam), one changeset.
Dev-first premises p1–p3 (sweep consumes only open-request snapshots; flow-definition storage raw-vs-parsed; snapshot created_at availability) will be measured on this tree BEFORE any edit — stop-and-report on falsification. Transition choice (read-side keyed on snapshot age vs idempotent normalization) recorded with reasoning in the report.
{
"issue": 12278,
"status": "done",
"branch": "claude/issue-12278-enforce-escalation-enabled",
"pr": "https://github.com/objectstack-ai/objectstack/pull/12737",
"premise_still_valid": true,
"summary": "Option-C package implemented as ruled (comment 5434864364). spec: ApprovalEscalationSchema.enabled default false -> true (stays boolean) + docstring; declared in DEFAULT_CHANGES_BY_MAJOR (17) and registered as semantic migration entry approval-escalation-enabled-default-flip; authorable-defaults, reference pages, spec-changes.json and upgrade guide regenerated by repo tooling (check:generated all 14 green). runtime: runEscalations skips on explicit enabled === false; transition (i) chosen — read-side legacy window keyed on snapshot created_at vs compiled cutoff 2026-08-28T00:00:00Z (pre-cutoff snapshots keep escalating; zero tenant writes; self-retiring) — (ii) rejected because repo migrations are per-boot predicate-idempotent with no run-once ledger, and the normalization predicate re-matches post-flip authored-false rows forever; honest bound of the compiled cutoff (late-upgrading installs' between-window rows honour the declared off) recorded in the constant's doc comment and PR body. Premises: p1 confirmed (sweep reads only pending sys_approval_request node_config_json; zero other runtime readers, re-grepped); p2 confirmed (flow definitions store RAW config — FlowSchema config is z.record(z.unknown()) at flow.zod.ts:271, executor approval-node.ts:133 is the only production parse site, so the transition's reach is request snapshots only); p3 confirmed (created_at stamped at open from the injected clock, column at sys-approval-request.object.ts:296). Measured zero-edit surfaces: skills/objectstack-automation/SKILL.md and content/docs/automation/approvals.mdx carry NO default-false statement in any spelling (case-insensitive grep) — skills net-line budget 0 met by a zero-line diff, no governed surface touched. Declared file-surface supplement (same-stroke, gate-required riders beyond the PM claim's list): packages/spec/scripts/lib/default-changes.ts (check:authorable-surface refuses an undeclared default move), packages/spec/src/migrations/entries/semantic/17.approval-escalation-enabled-default-flip.ts + regenerated registry.ts/spec-changes.json/upgrade guide (precedent: 17.import-run-automations-declared-default-corrected), and packages/spec/src/automation/approval.test.ts (ruled tests clause). Changeset: spec minor + plugin-approvals minor, BREAKING annotation, adr-0087 registered disposition (gate green). CI on the draft PR: in_progress at report time per the standing report-at-draft rule.",
"tests": "All under scripts/pm/os-verify-lock.sh, exits captured before pipes, VERDICT lines quoted. spec full suite + typecheck: 'Test Files 435 passed (435) / Tests 11542 passed (11542)', 'VERDICT command-exit 0 held 398s'. plugin-approvals full suite + typecheck: 'Test Files 33 passed (33) / Tests 613 passed (613)', 'VERDICT command-exit 0'. New pins named in verbose run at head 64cff1b8: explicit enabled:false post-flip does NOT escalate (pending, actions ['submit'], zero resumes); pre-flip materialized false keeps escalating (approved, ['submit','escalate','approve'], resume fired); omitted key escalates post-flip — 292 passed. Reverse verification (ablation of the sweep gate on the committed tree): no rebuild leg applies because the test consumes the mutated file via a same-package relative src import (vitest compiles src; no dist in the path); mutation confirmed on disk before any verdict (token 'esc.enabled === false' count 1 -> 0, git diff --stat 2 deletions); result exactly the predicted direction — pin 1 red, 291 passed (escalation fired); restore proven by blob equality (git hash-object == HEAD blob dcb137ef) and empty git diff HEAD; script carried trap restore EXIT INT TERM with absolute paths. check:generated: 'All 14 generated artifacts are up to date' twice, 'VERDICT command-exit 0'. Gate union: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, no paths (script derived 12 changed paths from merge-base d29e42f8; stderr header names this repo at commit 64cff1b8) -> 49 commands + check:nul-bytes, run at 64cff1b8: 47 PASS; check:skill-examples was prerequisite-not-met (unbuilt client-react dist), prerequisite built and re-run green ('260 prose examples type-check across 3 surface(s)', VERDICT command-exit 0); check-dev-prereqs.mjs and check-half-states.mjs are prerequisite refusals by their own printed text (workspace-not-built 32/67 local dists; invalid container GitHub credential, 'no reading at all') — declared narrowings, neither is diff-sensitive (no package.json touched; board sweep needs credentials CI has). default-changes.ts is gate-owned, so its own suite ran additionally: scripts/authorable-defaults.test.ts 23 passed. Gate-derivation caveat honoured: derived families ran in addition to the dispatch-named ones, none skipped.",
"mcp_calls": "6 — issue_read get, issue_read get_comments (all 14 comments, one page, newest verified = PM claim 5440341461), add_issue_comment (dev claim), create_pull_request (draft #12737), pull_request_read get (full body read-back, intact to tail), add_issue_comment (this report)",
"open_questions": [],
"out_of_scope_findings": ["NOT filed as a card (handed to PM for routing): objectui packages/app-shell/src/views/metadata-admin/inspectors/flow-node-config.ts:732 declares the escalation.enabled toggle with defaultValue 'false', which post-flip contradicts the spec default true (a block with the key omitted now reads as ON while the inspector toggle renders off). objectui edits are ruled out of this card's scope and the inspector is objectui#6499 territory — recommend appending to 6499 or filing an adjacent objectui card there; not filed cross-repo from this seat to avoid an undeduplicated duplicate in a repo whose open set this run has not read."]
}
Review of record — PM ACCEPT + clause-② contract review PASS, both for PR #12737 at head 64cff1b8.
Contract-review chain (dispatch-seat self-review path; this seat's serving model = claude-fable-5 = CONTRACT_REVIEW_TIER, and the dispatch ran at that tier — dispatched AND reviewed at tier). Contract increment read from the diff, spot-checked at 64cff1b8:
approval.zod.ts:622: enabled: z.boolean().default(true) with the ruled semantics stated in the docstring (block-existence is the feature switch; a block carrying timeoutHours is live unless explicitly false). Boolean kept — no tri-state, exactly as ruled (comment 5434864364, Option C package item 1).
approval-service.ts:3514: the sweep gates verbatim on esc.enabled === false — implementable now, as the ruling predicted (item 2).
Transition: option (i) chosen with a solid mechanical rejection of (ii) (repo migrations are per-boot predicate-idempotent with no run-once ledger, so the normalization would re-match post-flip authored-false rows forever — (ii) is not behaviour-preserving here). The read-side legacy window is keyed on snapshot created_at vs a compiled cutoff (ESCALATION_ENABLED_FLIP_CUTOFF_MS, approval-service.ts:213), zero tenant writes, self-retiring; the honest bound (late-upgrading installs' between-window rows honour the declared off) is documented at the constant and in the PR body. In contract (item 3 left the choice to the dev after premise verification).
Premises p1/p2/p3 all CONFIRMED with named line sites (flow definitions store RAW config — flow.zod.ts:271z.record(z.unknown()); the executor is the only production parse site; created_at stamped at open). No stop condition fired.
Teaching surfaces (item 4): measured, not assumed — skills/objectstack-automation/SKILL.md and content/docs/automation/approvals.mdx carry NO default-false statement in any spelling, so the zero-line diff meets the net-line-0 budget and NO governed face is touched; the generated reference/spec-changes.json/upgrade-guide regenerated by tooling; the docs-drift row naming flows.mdx is a symbol-level false positive (its only "escalation" hits are a screen-field example, verified at head).
Tests (item 5): the 11 behaviour tests stay green and honest; the explicit-false pin is red-before-fix by ablation (mutation proven on disk, predicted direction 1-red/291-pass measured, restore by blob equality). slaDueAt untouched (item 6).
Surface supplement beyond the claim (default-changes.ts declaration, semantic entry + regenerated registry, approval.test.ts) — each gate-forced or ruling-required, named with precedent. No widening beyond the ruled package.
PM checklist: draft ✓ base main, Fixes #12278 ✓ 12 files, no releases/**, changeset present (spec + plugin-approvals minor, BREAKING annotation + ADR-0087 disposition, gates green) ✓ gate union 47 PASS + 2 declared prerequisite-refusal narrowings (neither diff-sensitive) + skill-examples green after prerequisite build ✓ report comment 5441284003 read back intact ✓ CI on the draft converging at review time (normal per the dispatch contract).
Landing: waits on full CI green at 64cff1b8. ⚠️ Landing is SERIALIZED behind PR #12718: both PRs touch packages/spec/src/migrations/registry.ts and the spec-changes/upgrade-guide generated pair, so this PR enqueues only after #12718 is MERGED and this branch has been re-synced via scripts/pm/os-regen-merge.sh (regen, never hand-merge). #12726 (in queue now) is file-disjoint from this PR and does not gate it.
Out-of-scope finding routed (accepting-seat duty, not left in the report): the objectui inspector toggle default contradiction (flow-node-config.ts:732defaultValue: 'false' vs the new spec default true) is being filed as its own objectui card with dedup-first — note it only becomes live when objectui consumes the next @objectstack/spec release; details on the card.
Patch round (debt ledger) — addendum to report comment 5441284003, same claim, PR #12737, new head 71ccc576.
CI Type Check · debt ledger was RED on 64cff1b8: @objectstack/plugin-approvals: TEST_DEBT records 347 raw tsc error(s), tsc --noEmit now reports 355 (+8).
The 8 errors' shape: all TS2339 on the union ApprovalRequestRow | ApprovalNodeAutoOutcome — the three new #12278 escalation pins accessed req.id (8 sites across the three tests) on the raw openNodeRequest return type without narrowing. Reproduced with the gate's own measurement basis (package tsconfig with the test exclusion lifted, closure built): 355 total, and the 8 in-range errors sit exactly on the new pins' lines; the pre-existing identical accesses elsewhere in the file are the already-ledgered population.
Fix direction: the file's own sanctioned narrowing pattern (used at three pre-existing sites) — if ('autoApproved' in req) throw new Error(...) after each openNodeRequest — one line per pin, 3 insertions. No any, ⛔ no TEST_DEBT raise, runtime's unrelated -1 surplus note untouched.
Validation at the new head (all under scripts/pm/os-verify-lock.sh, exits captured before pipes):
Targeted re-measure: raw count back to exactly 347, composition delta vs the 355 run is TS2339 304 → 296 only — the ledger note's recorded composition to the unit.
Root gate after full workspace build (its stated precondition): check-type-check-coverage --re-measure: OK — 31 ledger entr(ies) re-measured in 246.8s, 1687 raw tsc error(s) total, none above its recorded number · VERDICT command-exit 0.
check:nul-bytes OK on the new head; tree clean; both runs executed on the byte-identical tree committed as 71ccc576 (sole diff vs 64cff1b8 = the 3 narrowing lines).
Pushed to the same branch; PM re-arms landing after CI greens on 71ccc576.
Activity
huangyiirene commentedon Aug 26, 2026
Claim: PM loop round R7, wave 5 — measurement half ONLY (the triage charter's own step 1; the route ruling is the maintainer's — the card says so, and escalation/auto-approve semantics sit on the security-adjacent manual floor)
Session:
session_01E5LFCYBJ3q2s6yW6oMLxwyBranch:
claude/issue-12278-escalation-enabled-census(measurement record only; expected zero code changes — a branch exists only if the dev needs to commit a measurement script artifact, which it should not)Worktree:
objectstack-issue-12278Domain:
domain:specFile surface: NONE (read-only measurement across objectstack + objectui; cloud and hotcrm are outside this session's reach and will be declared unmeasured, not guessed). Any code change is a breach — stop and report.
Container & model: S,
mode:subagent,model: opus— measurement work; the clause-② fable tier applies to the post-ruling IMPLEMENTING dispatch, not this census (recorded so the tier ledger stays honest)Clause-②: no (this dispatch changes nothing; the card's eventual implementing dispatch is the clause-② yes one, per triage)
Serial constraints cleared: zero file surface ⇒ no serial constraints; in-flight #12248 (
contracts/data-engine.ts) and landing-watch PRs untouched. H17: no hit.Generated by Claude Code
huangyiirene commentedon Aug 26, 2026
Claim (dev): measurement-only — census half of the enforce-or-remove ruling. No branch, no worktree, no code change expected; every read below is against
origin/mainrefs (git show <ref>:<path>), no checkout touched.Session:
session_01E5LFCYBJ3q2s6yW6oMLxwy— note this is the same session ID as the PM claim comment above, because this dev runs as a subagent of the dispatching PM seat and shares its session URL. It is not a competing claim; the PM claim and this one are the same actor. No branch will appear for this card.Scope: objectstack + objectui (both reachable).
cloudandhotcrmare outside this session's reach and will be declared unmeasured with the exact commands a seat with access would run — not guessed.Census to follow as an
os-dev-reportcomment.Generated by Claude Code
Generated by Claude Code
huangyiirene commentedon Aug 26, 2026
os-dev-report
Measurement half only — no code change, no branch, no PR. Every read below is against an
origin/mainref viagit show <ref>:<path>/git grep <ref>; no checkout was modified.objectstack-ai/objectstackb6c96bcea5d4352c5a63b4aa9a8d6809b9ce15cf107bb4bobjectstack-ai/objectui7975f2d85e2c2dd20b27658618544818a337a495Headline: the card's "zero readers" holds for the RUNTIME, and is incomplete for the PLATFORM
The card grepped objectstack only. objectui carries a live, load-bearing reader of
escalation.enabled— the metadata-admin flow-node inspector uses it as the progressive-disclosure controller for the entire escalation block. This does not contradict the card's runtime finding; it adds a consumer the route ruling has to price. Details in §3.1. Who authors
escalation.enabled?objectstack — 5 authored writes, all
true, all test/doc fixturestimeoutHoursalso setpackages/lint/src/validate-approval-approvers.test.tstrue24packages/lint/src/validate-approval-approvers.test.tstrue24packages/lint/src/validate-approval-approvers.test.tstrue24packages/spec/src/automation/approval.test.tstrue48packages/spec/src/automation/approval.test.tstrue24,0docs/qa/platform-checklist/areas/approvals.jsontrue(prose)1Zero product code, zero example app, zero seed, zero template authors this key.
examples/app-showcaseandexamples/app-todomention "escalation" only as flow names, notify topics and priority-escalation prose — neither authors an approvalconfig.escalationblock at all (git grep -i escalat origin/main -- examples/→ 30 hits, none a config block).Docs that instruct authors to set it:
content/docs/automation/approvals.mdx:629— "escalationis real, not decorative: setenabled: trueandtimeoutHours"content/docs/references/automation/approval.mdx:40,77,104— generated reference table,enableddocumented asoptional (default: false)skills/objectstack-automation/SKILL.md:610— published skill documents{ enabled, timeoutHours, action, escalateTo, notifySubmitter }Declaration registries (published surface, not authored config):
packages/spec/authorable-surface/automation.json:25andpackages/spec/authorable-surface.base.json:2071—automation/ApprovalEscalation:enabledpackages/spec/authorable-defaults/automation.json:16—automation/ApprovalEscalation:enabled = falsepackages/spec/liveness/row exists forApprovalEscalationyet.objectui — zero authored configs, one live reader (see §3)
git grep -n "timeoutHours" origin/mainin objectui returns only field declarations and their tests. No stored/authored escalation config anywhere in the repo.Instrument disclosure — the control caught a real instrument bug
Control (a) is why this census is trustworthy. My first instrument found zero of the three lint-fixture writes the card names: its opener regex excluded a
.-prefixed member assignment, sonode.config.escalation = { enabled: true, … }never matched. Fixed ([^A-Za-z0-9_.]→[^A-Za-z0-9_]), after which control (a) passes — all three card-named writes found (lines 140/151/153).A second, independent instrument (every
enabledtoken repo-wide whose ±8-line neighbourhood mentionsescalat) then found two more real writes the fixed opener still missed (approval.test.ts:303,306— single-lineApprovalEscalationSchema.parse({ enabled: true, … }), noescalation:opener on the line). Both instruments plus a same-linegit grep -i -E "escalat.*enabled|enabled.*escalat"agree on the union above. Confirmed false positives, discarded:IncidentResponsePolicySchema.enabled(incident-response.zod.ts:327, different schema),authorable-defaults/system.json:115,120, ADR-0066tenancy: { enabled: false }.Control (b) — the neighbouring key, same instrument
timeoutHoursis not zero, so a zero onenabledis about the key, not the tool:timeoutHourswritespackages/plugins/plugin-approvals/src/approval-service.test.tspackages/lint/src/validate-approval-approvers.test.tspackages/spec/src/automation/approval.test.tsThe asymmetry is the finding: all 11 runtime-behaviour tests in
plugin-approvalsauthorescalationwithtimeoutHoursand omitenabledentirely — e.g.approval-service.test.ts:1688:Under the declared default (
enabled: false) every one of these is a config the schema says is off, and the suite asserts it escalates. The runtime's own test suite encodestimeoutHours-is-the-switch.2. Stored-flow hazard population (
enabled: false+ realtimeoutHours)In-repo population: ZERO. Every authored
enabledin both reachable repos istrue. No file in objectstack or objectui authorsenabled: falsealongside atimeoutHours.But the repo is the wrong place to look, and zero here must not be read as zero in the field. The console manufactures exactly this shape:
packages/app-shell/src/views/metadata-admin/inspectors/flow-node-config.ts:732-745declaresescalation.enabled(defaultValue: 'false') as the controller, and gatestimeoutHours,action,escalateTo,notifySubmitterbehindshowWhen: { field: 'escalation.enabled', equals: ['true'] }.isFieldVisible(same file, :1050-1070) additionally returns true "when the field already holds a stored value (so existing config is never hidden)".FlowNodeInspector.tsx:229—const visibleFields = fields.filter((f) => isFieldVisible(f, node, fields));— filters rendering only. There is no prune-on-save: nothing deletes a hidden field's stored value.So a console author who enables SLA escalation, sets
timeoutHours: 24, then toggles the "SLA escalation" switch back off, commits{ enabled: false, timeoutHours: 24 }— the hazard shape, one click away, produced by the platform's own authoring UI, whose toggle is labelledSLA escalation/SLA 升级and helped as "Escalate when a decision is not recorded within the timeout."The actual population lives in tenant databases (
sys_approval_request.node_config_json/ stored flow metadata), not in any repo, and is not measurable from a git checkout by any seat. It is the one number route 1 needs and this census cannot supply. A query against a live deployment is the only instrument; see §4.3. Re-verification of the zero-reader claim on the current tree
Re-measured at
b6c96bc(card measured107bb4b).Runtime — claim CONFIRMED, unchanged, same line numbers:
packages/plugins/plugin-approvals/src/approval-service.ts:3472-3473escalateRequest(:3759) reads exactly three keys, noenabled::3760const action: string = esc.action ?? 'notify';:3762esc.escalateTo:3824if (esc.notifySubmitter !== false && raw.submitter_id)slaDueAt(:474-476) readscfg?.escalation?.timeoutHours— also noenabled.git grep -nE "(esc|escalation|escalationConfig|escalationCfg|sla)\s*\??\s*\.\s*enabled" origin/mainacross all of objectstack → zero matches. No new reader has appeared since107bb4b.Platform — claim needs AMENDING. objectui reads it, as an authoring-UI controller:
packages/app-shell/src/views/metadata-admin/inspectors/flow-node-config.tsdefaultValue: 'false'timeoutHoursshowWhen: escalation.enabled === 'true'actionshowWhen: escalation.enabled === 'true'escalateToshowWhen: escalation.enabled === 'true'notifySubmittershowWhen: escalation.enabled === 'true'packages/app-shell/src/views/metadata-admin/i18n.ts'escalation.enabled': { label: 'SLA 升级', help: '在超时内未做决策时升级。' }(zh only)packages/app-shell/src/views/metadata-admin/inspectors/json-schema-to-fields.test.tsshowWhenwiringsSo the accurate statement is:
escalation.enabledhas zero runtime readers and one authoring-UI reader. The key is inert where it is enforced and load-bearing where it is authored — which is precisely the disagreement the card describes, now with a name on both halves.4. Unmeasured legs — declared, not guessed
cloudandhotcrmare outside this session's reach (not attached; onlyobjectstackandobjectuiare present). Neither was measured; no inference is offered about them.www.objectos.aiwas measured by the card's filer only (it reports the article showsenabled: true) — second-hand, not re-verified here.A seat with access should run, per repo:
And, for the population route 1 actually changes behaviour for — the only instrument that can answer it, against a live deployment, not a repo:
plus the same predicate over stored flow metadata (
sys_flow/ package flow definitions) for nodes that have not yet opened a request.The fork, stated neutrally (no recommendation — the ruling is the maintainer's)
Evidence pulling toward route 1 (enforce: gate the sweep on
enabled):authorable-surface/automation.json:25,authorable-surface.base.json:2071) with a documented default; removing it is an ADR-0087 retirement, not a delete.objectstack-automationskill both instruct authors to set it.enabled: false.Evidence pulling toward route 2 (remove:
timeoutHoursis the switch):b6c96bc; the enforcement half has never read it.enabledand expect escalation —timeoutHours-as-switch is what the system already means and what its suite pins.The tension the ruling has to resolve: route 1's blast radius is unmeasured and unmeasurable from here (the stored
enabled: false+timeoutHourspopulation lives in tenant DBs, and the console manufactures that shape one toggle away, so it is plausibly non-zero), while route 2's blast radius is fully measured and entirely in-repo (one inspector disclosure tree, one pinned test, one zh label, three doc sites, one published skill table) — but route 2 retires a published key and hands the "is it off?" question to the presence oftimeoutHours, which no authoring UI currently presents as a switch.Generated by Claude Code
Generated by Claude Code
huangyiirene commentedon Aug 26, 2026
ACCEPT (measurement half) — reviewer of record: spec seat R7,
session_01E5LFCYBJ3q2s6yW6oMLxwy. Zero-trace census verified (no branch, no PR, both checkouts clean); the report comment read back intact. Quality notes for the record:escalation.enabledas the progressive-disclosure controller, 5showWhenwirings + pinned test + zh label) — this changes both routes' blast radii and is load-bearing for the ruling.enabled:false+ livetimeoutHours) — one click from the escalating-against-declaration state.timeoutHoursand omitenabled— the suite itself pins timeoutHours-as-the-switch. The asymmetry is the finding.State conversion follows in the next comment:
pm:dispatched→needs-user-decisionwith the four-facet block — the route ruling is the maintainer's (security-adjacent manual floor), now with the evidence it was waiting for.Generated by Claude Code
huangyiirene commentedon Aug 26, 2026
os-decision-facets
一句话问题:审批节点的
escalation.enabled开关是个「声明了但运行时不读」的键 —— 作者写enabled: false照样升级(还可能静默自动通过);现在测量齐了,请裁:补上执行(Route 1)还是退役该键(Route 2)?选项:
if (esc.enabled === false) continue;—— 让运行时兑现声明。enabled,timeoutHours的存在与否即开关(运行时今天实际的语义)。① 实际业务需求:census 实测 —— 仓内零产品/示例/种子作者(仅测试夹具),但整个创作面都在教 enabled 是开关:console 检查器用它做渐进披露控制器(5 处 showWhen + pin 测试 + 中文标签)、3 处已发布文档、已发布的 objectstack-automation 技能表都指导作者设置它。作者被教的语义与运行时执行的语义相反,这就是业务面的真实伤害点。
② 项目长远合理性:两个可互相矛盾的键就是缺陷类本身,长期只能留一个开关。Route 1 让声明=执行,与既有创作面一致;Route 2 收缩键面但把「关」变成「删掉 timeoutHours」—— 没有任何 UI 把它呈现为开关,需要重建检查器披露树 + 改 3 处文档 + 改已发布技能(token 天花板 0 余量)。
③ 防 AI 写代码犯错:今天是开放失败:AI 作者照 schema 写
enabled: false+action: 'auto_approve',得到的是它声明关闭的自动审批 —— 最危险的静默形状。Route 1 当场关闭这条 fail-open 路径;Route 2 也消除歧义但要求全部教学面同步重训。④ 创业阶段不扩散:Route 1 diff 最小(运行时一行 + 测试),Route 2 是一次全套退役(ADR-0087 + 检查器重建 + 文档 + 技能),爆炸半径全部仓内可测但显著更大。
推荐 Route 1(enforce),回退 Route 2。核心理由:存量租户库里若存在⚠️ 无论选哪条,objectui 读者的存在意味着两条路都不是纯 spec 侧改动。
enabled:false+timeoutHours的流程(仓位测不到,SQL 判据已附在 census),这些作者声明过关闭 —— enforce 恰恰是把系统行为改成他们声明的样子,方向上是兑现而非破坏;而 Route 2 与整个创作面为敌。附带子问题(同批,免得二次弹):console 检查器隐藏字段不剪枝(开关切关后
timeoutHours留在存量配置里)是独立缺陷,波及所有 showWhen 分组。建议:裁 Route 1 后单独立 objectui 卡(选项 B —— 它是通用检查器缺陷,escalation 只是一例);若裁 Route 2 则 escalation 一例自然消失,仍建议为其余分组立卡。置信缺口:租户库存量人口不可从任何仓位测量(census 附了 SQL 判据与四条跨仓命令);cloud / hotcrm 两腿未测且已如实声明。
回一个字即可:「1」= Route 1 + 子问题按 B 立卡;「2」= Route 2 + 子问题按 B 立卡;其它意见照述。
Generated by Claude Code
os-steve commentedon Aug 26, 2026
Maintainer ruling recorded — Route 1 adopted: enforce
escalation.enabledProvenance: maintainer, 2026-08-26, live PM chat (decision-inbox batch 5, skills seat session
session_01JANH3y7qe3MD8aLaLXci8N), verbatim: 「12452 不处理,其他接受」 — accepting this card's presented recommendation (Route 1, per the four-facet block above), including the sub-question's disposition (the console inspector defect gets its own objectui card).What this rules:
escalation.enabled === falseis skipped — the runtime comes to mean what the entire authoring surface (console inspector, published docs, published skill) already teaches. This closes the measured fail-open path whereenabled: false+action: 'auto_approve'silently approves a request the author declared off the clock.enabled: falsealongside a livetimeoutHours(the shape the console manufactures) STOP escalating — that is the declared intent being honored, and the changeset says so.showWhenfields keep stored values; generic across all groups, escalation is one instance) is filed as its own objectui card by this seat, with a link back here.State:
needs-user-decision→pm:queuein the same stroke — thedomain:speclane dispatches (enforcement lands in the plugin-approvals sweep; the spec-side key description stays as declared).Generated by Claude Code
os-steve commentedon Aug 26, 2026
Sub-defect card filed as ruled: objectstack-ai/objectui#6499 (the inspector no-prune-on-save defect, generic across showWhen groups, objectui queue). This card's own scope stays the runtime enforcement half.
Generated by Claude Code
12 remaining items
os-sales commentedon Aug 27, 2026
Claim: PM loop round 9 (domain:spec seat) — implementing dispatch under the recorded Option-C ruling (maintainer 2026-08-27, comment 5434864364, which explicitly reverses point 2 of the 2026-08-26 Route-1 ruling)
Session:
session_01JvjTCjJQn9zSTXEhUKgT7sBranch:
claude/issue-12278-enforce-escalation-enabled(reusing the empty write-probe branch atf93df4dleft by the R8 stopped run, as its report intended)Worktree:
objectstack-issue-12278Domain:
domain:specFile surface:
packages/spec/src/automation/approval.zod.ts(default flip + docstring) ·packages/spec/authorable-defaults/automation.json+ regenerated spec artifacts (generated reference pages, repo tooling only) ·skills/objectstack-automation/SKILL.md(only the default-false statements; net-line budget 0) ·content/docs/automation/approvals.mdx(only if a default-false statement exists there) ·packages/plugins/plugin-approvals/src/approval-service.ts+approval-service.test.ts(+approval-node.tsonly if the chosen transition needs the executor seam) ·.changeset/(stop on breach; explain in the report)Container & model: M,
mode:subagent,model: claude-fable-5— clause-② content mandate (spec semantic face + accept/escalation behaviour change, per the ruling's own scoping); dispatch-gates--tierrun atd29e42ffrom a fresh origin/main worktree reports no path-derived mandate and flagspackages/spec/src/automation/approval.zod.tsas clause-② suspect surfaceClause-②: yes
Serial constraints cleared: lane in-flight #3653 (
mode:cloud, devsession_01EPEkvZe92KwvVUFhgdUR6S) — plugin-auth/platform-objects/workspace-pin surface, fully disjoint from approvals/automation. Lane landing #11846/PR #12718 (spec kernel retirement, CI converging) — source surfaces disjoint; BOTH regenerate shared spec generated artifacts ⇒ landing serialized per the generated-artifact relay (#12718 holds the first slot). Cross-seat hold #12702/PR #12705: disjoint. Open-PR sweep: no open PR touchesplugin-approvals/**orautomation/approval.zod.ts. H17 on-hold trigger files (#8345/#8346): no intersection. Sibling pin note: this card's runtime pin asserts escalation-sweep behaviour that no in-flight card touches; objectui#6499 (inspector no-prune defect) is deliberately out of scope per the ruling.Premise re-checked at claim (origin/main
d29e42f):enabled: z.boolean().default(false)still atapproval.zod.ts:616; sweep still gates ontimeoutHoursalone (approval-service.ts:3473); zero runtime readers ofescalation.enabled;authorable-defaults/automation.json:16still records= false; probe branch present atf93df4d. Both rulings and the falsification chain (report 5428619265: parse-materialization atapproval-node.ts:133-138→ snapshot spread/store atapproval-service.ts:2069/2112) read in full before this claim.Generated by Claude Code
os-sales commentedon Aug 27, 2026
Claim (dev): implementing dispatch under the recorded Option-C ruling (comment 5434864364, which reverses point 2 of the 2026-08-26 Route-1 ruling). I run as a
mode:subagentdev inside PM sessionsession_01JvjTCjJQn9zSTXEhUKgT7s— same session ID as the PM claim comment above; same actor, not a competing claim.Branch:
claude/issue-12278-enforce-escalation-enabled— continuing the empty write-probe branch atf93df4dleft by the R8 stopped run, reset onto current origin/maind29e42f(verified fast-forward; no force). Worktree:objectstack-issue-12278.Scope per the PM claim's declared file surface: spec default flip + docstring (
packages/spec/src/automation/approval.zod.ts),authorable-defaults/automation.json+ regenerated spec artifacts,skills/objectstack-automation/SKILL.mddefault-false statements only (net-line budget 0),content/docs/automation/approvals.mdxonly if a default-false statement exists there,packages/plugins/plugin-approvals/src/approval-service.ts+ tests (+approval-node.tsonly if the chosen transition needs the executor seam), one changeset.Dev-first premises p1–p3 (sweep consumes only open-request snapshots; flow-definition storage raw-vs-parsed; snapshot
created_atavailability) will be measured on this tree BEFORE any edit — stop-and-report on falsification. Transition choice (read-side keyed on snapshot age vs idempotent normalization) recorded with reasoning in the report.Generated by Claude Code
feat(spec,approvals): escalation.enabled defaults true and the SLA sw…
os-sales commentedon Aug 27, 2026
os-dev-report
{ "issue": 12278, "status": "done", "branch": "claude/issue-12278-enforce-escalation-enabled", "pr": "https://github.com/objectstack-ai/objectstack/pull/12737", "premise_still_valid": true, "summary": "Option-C package implemented as ruled (comment 5434864364). spec: ApprovalEscalationSchema.enabled default false -> true (stays boolean) + docstring; declared in DEFAULT_CHANGES_BY_MAJOR (17) and registered as semantic migration entry approval-escalation-enabled-default-flip; authorable-defaults, reference pages, spec-changes.json and upgrade guide regenerated by repo tooling (check:generated all 14 green). runtime: runEscalations skips on explicit enabled === false; transition (i) chosen — read-side legacy window keyed on snapshot created_at vs compiled cutoff 2026-08-28T00:00:00Z (pre-cutoff snapshots keep escalating; zero tenant writes; self-retiring) — (ii) rejected because repo migrations are per-boot predicate-idempotent with no run-once ledger, and the normalization predicate re-matches post-flip authored-false rows forever; honest bound of the compiled cutoff (late-upgrading installs' between-window rows honour the declared off) recorded in the constant's doc comment and PR body. Premises: p1 confirmed (sweep reads only pending sys_approval_request node_config_json; zero other runtime readers, re-grepped); p2 confirmed (flow definitions store RAW config — FlowSchema config is z.record(z.unknown()) at flow.zod.ts:271, executor approval-node.ts:133 is the only production parse site, so the transition's reach is request snapshots only); p3 confirmed (created_at stamped at open from the injected clock, column at sys-approval-request.object.ts:296). Measured zero-edit surfaces: skills/objectstack-automation/SKILL.md and content/docs/automation/approvals.mdx carry NO default-false statement in any spelling (case-insensitive grep) — skills net-line budget 0 met by a zero-line diff, no governed surface touched. Declared file-surface supplement (same-stroke, gate-required riders beyond the PM claim's list): packages/spec/scripts/lib/default-changes.ts (check:authorable-surface refuses an undeclared default move), packages/spec/src/migrations/entries/semantic/17.approval-escalation-enabled-default-flip.ts + regenerated registry.ts/spec-changes.json/upgrade guide (precedent: 17.import-run-automations-declared-default-corrected), and packages/spec/src/automation/approval.test.ts (ruled tests clause). Changeset: spec minor + plugin-approvals minor, BREAKING annotation, adr-0087 registered disposition (gate green). CI on the draft PR: in_progress at report time per the standing report-at-draft rule.", "tests": "All under scripts/pm/os-verify-lock.sh, exits captured before pipes, VERDICT lines quoted. spec full suite + typecheck: 'Test Files 435 passed (435) / Tests 11542 passed (11542)', 'VERDICT command-exit 0 held 398s'. plugin-approvals full suite + typecheck: 'Test Files 33 passed (33) / Tests 613 passed (613)', 'VERDICT command-exit 0'. New pins named in verbose run at head 64cff1b8: explicit enabled:false post-flip does NOT escalate (pending, actions ['submit'], zero resumes); pre-flip materialized false keeps escalating (approved, ['submit','escalate','approve'], resume fired); omitted key escalates post-flip — 292 passed. Reverse verification (ablation of the sweep gate on the committed tree): no rebuild leg applies because the test consumes the mutated file via a same-package relative src import (vitest compiles src; no dist in the path); mutation confirmed on disk before any verdict (token 'esc.enabled === false' count 1 -> 0, git diff --stat 2 deletions); result exactly the predicted direction — pin 1 red, 291 passed (escalation fired); restore proven by blob equality (git hash-object == HEAD blob dcb137ef) and empty git diff HEAD; script carried trap restore EXIT INT TERM with absolute paths. check:generated: 'All 14 generated artifacts are up to date' twice, 'VERDICT command-exit 0'. Gate union: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, no paths (script derived 12 changed paths from merge-base d29e42f8; stderr header names this repo at commit 64cff1b8) -> 49 commands + check:nul-bytes, run at 64cff1b8: 47 PASS; check:skill-examples was prerequisite-not-met (unbuilt client-react dist), prerequisite built and re-run green ('260 prose examples type-check across 3 surface(s)', VERDICT command-exit 0); check-dev-prereqs.mjs and check-half-states.mjs are prerequisite refusals by their own printed text (workspace-not-built 32/67 local dists; invalid container GitHub credential, 'no reading at all') — declared narrowings, neither is diff-sensitive (no package.json touched; board sweep needs credentials CI has). default-changes.ts is gate-owned, so its own suite ran additionally: scripts/authorable-defaults.test.ts 23 passed. Gate-derivation caveat honoured: derived families ran in addition to the dispatch-named ones, none skipped.", "mcp_calls": "6 — issue_read get, issue_read get_comments (all 14 comments, one page, newest verified = PM claim 5440341461), add_issue_comment (dev claim), create_pull_request (draft #12737), pull_request_read get (full body read-back, intact to tail), add_issue_comment (this report)", "open_questions": [], "out_of_scope_findings": ["NOT filed as a card (handed to PM for routing): objectui packages/app-shell/src/views/metadata-admin/inspectors/flow-node-config.ts:732 declares the escalation.enabled toggle with defaultValue 'false', which post-flip contradicts the spec default true (a block with the key omitted now reads as ON while the inspector toggle renders off). objectui edits are ruled out of this card's scope and the inspector is objectui#6499 territory — recommend appending to 6499 or filing an adjacent objectui card there; not filed cross-repo from this seat to avoid an undeduplicated duplicate in a repo whose open set this run has not read."] }Generated by Claude Code
os-sales commentedon Aug 27, 2026
Review of record — PM ACCEPT + clause-② contract review PASS, both for PR #12737 at head
64cff1b8.Contract-review chain (dispatch-seat self-review path; this seat's serving model =
claude-fable-5=CONTRACT_REVIEW_TIER, and the dispatch ran at that tier — dispatched AND reviewed at tier). Contract increment read from the diff, spot-checked at64cff1b8:approval.zod.ts:622:enabled: z.boolean().default(true)with the ruled semantics stated in the docstring (block-existence is the feature switch; a block carryingtimeoutHoursis live unless explicitly false). Boolean kept — no tri-state, exactly as ruled (comment 5434864364, Option C package item 1).approval-service.ts:3514: the sweep gates verbatim onesc.enabled === false— implementable now, as the ruling predicted (item 2).created_atvs a compiled cutoff (ESCALATION_ENABLED_FLIP_CUTOFF_MS,approval-service.ts:213), zero tenant writes, self-retiring; the honest bound (late-upgrading installs' between-window rows honour the declared off) is documented at the constant and in the PR body. In contract (item 3 left the choice to the dev after premise verification).flow.zod.ts:271z.record(z.unknown()); the executor is the only production parse site;created_atstamped at open). No stop condition fired.skills/objectstack-automation/SKILL.mdandcontent/docs/automation/approvals.mdxcarry NO default-false statement in any spelling, so the zero-line diff meets the net-line-0 budget and NO governed face is touched; the generated reference/spec-changes.json/upgrade-guide regenerated by tooling; the docs-drift row namingflows.mdxis a symbol-level false positive (its only "escalation" hits are a screen-field example, verified at head).slaDueAtuntouched (item 6).PM checklist: draft ✓ base
main,Fixes #12278✓ 12 files, noreleases/**, changeset present (spec + plugin-approvals minor, BREAKING annotation + ADR-0087 disposition, gates green) ✓ gate union 47 PASS + 2 declared prerequisite-refusal narrowings (neither diff-sensitive) + skill-examples green after prerequisite build ✓ report comment 5441284003 read back intact ✓ CI on the draft converging at review time (normal per the dispatch contract).Landing: waits on full CI green at⚠️ Landing is SERIALIZED behind PR #12718: both PRs touch
64cff1b8.packages/spec/src/migrations/registry.tsand the spec-changes/upgrade-guide generated pair, so this PR enqueues only after #12718 is MERGED and this branch has been re-synced viascripts/pm/os-regen-merge.sh(regen, never hand-merge). #12726 (in queue now) is file-disjoint from this PR and does not gate it.Out-of-scope finding routed (accepting-seat duty, not left in the report): the objectui inspector toggle default contradiction (
flow-node-config.ts:732defaultValue: 'false'vs the new spec default true) is being filed as its own objectui card with dedup-first — note it only becomes live when objectui consumes the next@objectstack/specrelease; details on the card.Generated by Claude Code
test(approvals): narrow openNodeRequest unions in the #12278 pins — d…
os-sales commentedon Aug 27, 2026
Patch round (debt ledger) — addendum to report comment 5441284003, same claim, PR #12737, new head
71ccc576.CI
Type Check · debt ledgerwas RED on64cff1b8:@objectstack/plugin-approvals: TEST_DEBT records 347 raw tsc error(s), tsc --noEmit now reports 355 (+8).The 8 errors' shape: all TS2339 on the union
ApprovalRequestRow | ApprovalNodeAutoOutcome— the three new #12278 escalation pins accessedreq.id(8 sites across the three tests) on the rawopenNodeRequestreturn type without narrowing. Reproduced with the gate's own measurement basis (package tsconfig with the test exclusion lifted, closure built): 355 total, and the 8 in-range errors sit exactly on the new pins' lines; the pre-existing identical accesses elsewhere in the file are the already-ledgered population.Fix direction: the file's own sanctioned narrowing pattern (used at three pre-existing sites) —
if ('autoApproved' in req) throw new Error(...)after eachopenNodeRequest— one line per pin, 3 insertions. Noany, ⛔ no TEST_DEBT raise, runtime's unrelated-1surplus note untouched.Validation at the new head (all under
scripts/pm/os-verify-lock.sh, exits captured before pipes):check-type-check-coverage --re-measure: OK — 31 ledger entr(ies) re-measured in 246.8s, 1687 raw tsc error(s) total, none above its recorded number·VERDICT command-exit 0.vitest run src/approval-service.test.ts—Test Files 1 passed / Tests 292 passed, all three approvals:escalation.enabledhas zero readers — the SLA sweep gates only ontimeoutHours, soenabled: falsestill escalates #12278 pins green by name ·VERDICT command-exit 0.check:nul-bytesOK on the new head; tree clean; both runs executed on the byte-identical tree committed as71ccc576(sole diff vs64cff1b8= the 3 narrowing lines).Pushed to the same branch; PM re-arms landing after CI greens on
71ccc576.Generated by Claude Code
feat(spec,approvals): escalation.enabled defaults to true and the SLA…
test(spec): automation test titles state each cited decision in words…