Skip to content

approvals: escalation.enabled has zero readers — the SLA sweep gates only on timeoutHours, so enabled: false still escalates #12278

Closed

Description

@hotlong

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:

packages/plugins/plugin-approvals/src/approval-service.ts:3472-3473
  const esc = cfg?.escalation;
  if (!esc || typeof esc.timeoutHours !== 'number' || esc.timeoutHours <= 0) continue;

escalateRequest reads esc.action, esc.escalateTo and esc.notifySubmitter. It does not read esc.enabled either.

Grep for a reader anywhere in packages/ and apps/:

  • escalation.enabled / esc.enabled / escalation?.enabled — zero hits outside the declaration itself.
  • The only other occurrences are three fixture lines in packages/lint/src/validate-approval-approvers.test.ts that write enabled: true.

Why it matters

This is the ADR-0049 declared-but-unenforced shape, and it fails open, which is the worse direction. An author who writes

escalation: { enabled: false, timeoutHours: 24, action: 'auto_approve' }

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)

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


Generated by Claude Code

Activity

  1. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Aug 26, 2026
  2. self-assigned this
    on Aug 26, 2026
  3. huangyiirene commented on Aug 26, 2026

    @huangyiirene
    Collaborator

    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.


    Generated by Claude Code

  4. huangyiirene commented on Aug 26, 2026

    @huangyiirene
    Collaborator

    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.

    Census to follow as an os-dev-report comment.


    Generated by Claude Code


    Generated by Claude Code

  5. huangyiirene commented on Aug 26, 2026

    @huangyiirene
    Collaborator

    os-dev-report

    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

    # file line value timeoutHours also set kind
    1 packages/lint/src/validate-approval-approvers.test.ts 140 true 24 lint fixture
    2 packages/lint/src/validate-approval-approvers.test.ts 151 true 24 lint fixture
    3 packages/lint/src/validate-approval-approvers.test.ts 153 true 24 lint fixture
    4 packages/spec/src/automation/approval.test.ts 264 true 48 spec parse test
    5 packages/spec/src/automation/approval.test.ts 303, 306 true 24, 0 spec parse test
    — docs/qa/platform-checklist/areas/approvals.json 803, 814 true (prose) 1 checklist step text, not executable config

    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"
    • content/docs/references/automation/approval.mdx:40,77,104 — generated reference table, enabled documented as optional (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:25 and packages/spec/authorable-surface.base.json:2071 — automation/ApprovalEscalation:enabled
    • packages/spec/authorable-defaults/automation.json:16 — automation/ApprovalEscalation:enabled = false
    • 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:

    file authored timeoutHours writes
    packages/plugins/plugin-approvals/src/approval-service.test.ts 11 (lines 1583, 1668, 1688, 1703, 1713, 1729, 1748, 1758, 1770, 3417)
    packages/lint/src/validate-approval-approvers.test.ts 3
    packages/spec/src/automation/approval.test.ts 3

    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:

    openInput(['u9'], {}, { escalation: { timeoutHours: 1, action: 'auto_approve', notifySubmitter: false } }), CTX,

    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:

    packages/plugins/plugin-approvals/src/approval-service.ts:3472-3473

    const esc = cfg?.escalation;
    if (!esc || typeof esc.timeoutHours !== 'number' || esc.timeoutHours <= 0) continue;

    escalateRequest (:3759) reads exactly three keys, no enabled:

    • :3760 const action: string = esc.action ?? 'notify';
    • :3762 esc.escalateTo
    • :3824 if (esc.notifySubmitter !== false && raw.submitter_id)

    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:

    file line read
    packages/app-shell/src/views/metadata-admin/inspectors/flow-node-config.ts 732 declares the controller field, defaultValue: 'false'
    same 733 timeoutHours showWhen: escalation.enabled === 'true'
    same 742 action showWhen: escalation.enabled === 'true'
    same 744 escalateTo showWhen: escalation.enabled === 'true'
    same 745 notifySubmitter showWhen: escalation.enabled === 'true'
    packages/app-shell/src/views/metadata-admin/i18n.ts 4060 'escalation.enabled': { label: 'SLA 升级', help: '在超时内未做决策时升级。' } (zh only)
    packages/app-shell/src/views/metadata-admin/inspectors/json-schema-to-fields.test.ts 64, 99, 310-331 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 deadline
    SELECT id, node_config_json
    FROM   sys_approval_request
    WHERE  json_extract(node_config_json, '$.escalation.enabled')      = 0
      AND  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.


    Generated by Claude Code


    Generated by Claude Code

  6. huangyiirene commented on Aug 26, 2026

    @huangyiirene
    Collaborator

    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:

    1. 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.
    2. 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.
    3. 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.
    4. 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.
    5. 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.


    Generated by Claude Code

  7. huangyiirene commented on Aug 26, 2026

    @huangyiirene
    Collaborator

    os-decision-facets

    一句话问题:审批节点的 escalation.enabled 开关是个「声明了但运行时不读」的键 —— 作者写 enabled: false 照样升级(还可能静默自动通过);现在测量齐了,请裁:补上执行(Route 1)还是退役该键(Route 2)?

    选项:

    • Route 1(enforce):在升级扫描加一行 if (esc.enabled === false) continue; —— 让运行时兑现声明。
    • Route 2(remove):按 ADR-0087 流程退役 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。核心理由:存量租户库里若存在 enabled:false + timeoutHours 的流程(仓位测不到,SQL 判据已附在 census),这些作者声明过关闭 —— enforce 恰恰是把系统行为改成他们声明的样子,方向上是兑现而非破坏;而 Route 2 与整个创作面为敌。⚠️ 无论选哪条,objectui 读者的存在意味着两条路都不是纯 spec 侧改动。

    附带子问题(同批,免得二次弹):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

  8. added
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    and removed
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    on Aug 26, 2026
  9. removed their assignment
    on Aug 26, 2026
  10. os-steve commented on Aug 26, 2026

    @os-steve
    Collaborator

    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:

    1. 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.
    2. 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.
    3. 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.
    4. 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).


    Generated by Claude Code

  11. added
    pm:queueReady for the PM dispatch loop
    and removed
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    on Aug 26, 2026
  12. os-steve commented on Aug 26, 2026

    @os-steve
    Collaborator

    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

  13. 12 remaining items

  14. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Aug 27, 2026
  15. self-assigned this
    on Aug 27, 2026
  16. os-sales commented on Aug 27, 2026

    @os-sales
    Collaborator

    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.


    Generated by Claude Code

  17. os-sales commented on Aug 27, 2026

    @os-sales
    Collaborator

    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.


    Generated by Claude Code

  18. os-sales commented on Aug 27, 2026

    @os-sales
    Collaborator

    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

  19. os-sales commented on Aug 27, 2026

    @os-sales
    Collaborator

    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:271 z.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:732 defaultValue: '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.


    Generated by Claude Code

  20. os-sales commented on Aug 27, 2026

    @os-sales
    Collaborator

    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.
    • Pin suite: vitest run src/approval-service.test.ts — Test Files 1 passed / Tests 292 passed, all three approvals: escalation.enabled has zero readers — the SLA sweep gates only on timeoutHours, so enabled: false still escalates #12278 pins green by name · 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.


    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

Labels

domain:specDomain lane — seat card indexed by label:pm:seat

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions