Skip to content

docs(automation): add the fourth onEmptyApprovers member (fallback) to the skill config table and the approvals checklist - #18795

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-18527-on-empty-approvers-fallback-member
Sep 18, 2026
Merged

os-zhuang merged 1 commit into
mainfrom
claude/issue-18527-on-empty-approvers-fallback-member

Conversation

@os-justin

Copy link
Copy Markdown
Collaborator

Fixes #18527

Clause-②: no

What

The approval node's empty-slate policy has four members since PR #18525 (card #17931) landed on main, and two carriers still enumerated three. This PR adds the fourth member — onEmptyApprovers: 'fallback' with its sibling fallbackApprovers — to each carrier in that carrier's own register. Two files, nothing else: git diff --stat at 2b1d5784e8 = docs/qa/platform-checklist/areas/approvals.json | 15 ++++++++------- and skills/objectstack-automation/SKILL.md | 2 +- (9 insertions, 8 deletions; the SKILL.md change is one table row edited in place).

The vocabulary, and where the fourth member is read from

Read on origin/main e5705a8ea (the merge base of this branch), from the schema and its tests — not from the card's prose:

  • packages/spec/src/automation/approval.zod.ts:884onEmptyApprovers: z.enum(['admin_rescue', 'fail', 'auto_approve', 'fallback']).default('admin_rescue')
  • :886-887 .describe("'fallback' opens the request on fallbackApprovers instead"
  • :868-869 docblock: fallback — open the request on {@link ApprovalNodeConfig.fallbackApprovers} instead. The only policy that NAMES people
  • :878-882 docblock: The fallback slate is resolved by the SAME resolver as approvers … A fallback that itself resolves to nobody degrades to admin_rescue
  • :906-907fallbackApprovers: z.array(ApprovalNodeApproverSchema).min(1).optional().describe("Approvers the request opens on when onEmptyApprovers is 'fallback'")
  • :891-892 docblock: Same shape as approvers; :897-898: Required iff the policy is 'fallback', and refused otherwise — both arms are enforced by {@link checkFallbackApproversPairing} (the refinement at :728-767)
  • packages/spec/src/automation/approval.test.ts:352 describe("ApprovalNodeConfigSchema — onEmptyApprovers: 'fallback' + fallbackApprovers" — accepts the pair, refuses 'fallback' without a list, refuses an empty list, refuses a list under the default policy.
  • Runtime, because the checklist carrier asserts behaviour: packages/plugins/plugin-approvals/src/approval-service.ts:2782-2836 (openNodeRequest) — under 'fallback' the declared list goes through the same expandApprovers as the primary slate; if it lands a concrete person the request opens on it (approvers = fallbackApprovers, :2817) with the warn resolved to no concrete approver — opening on the declared fallbackApprovers per onEmptyApprovers: 'fallback'. (:2805-2807); if it too resolves to nobody the warn is … AND its declared fallbackApprovers resolved to nobody either — falling back to admin_rescue. (:2820-2822) and the admin_rescue path follows (:2846-2851).

Carrier 1 — skills/objectstack-automation/SKILL.md :728 (the only enumerating line)

:635 names the key without enumerating, :653 and :693 are examples that pick 'fail'; only the Node Config table row enumerates. Before:

| `onEmptyApprovers` | What an EMPTY resolved slate does: `admin_rescue` (default — request opens, only a privileged admin can act via Reassign; never waves through, never kills the run), `fail` (node fails — treat an empty slate as a config bug), `auto_approve` (skip the request, continue down `approve` with `output.autoApproved = true` — opt-in because it silently waves the record through). Declare it explicitly on any node with an `expression` approver (linted) |

After (the fourth parenthetical, same register; the tail sentence unchanged):

| `onEmptyApprovers` | What an EMPTY resolved slate does: `admin_rescue` (default — request opens, only a privileged admin can act via Reassign; never waves through, never kills the run), `fail` (node fails — treat an empty slate as a config bug), `auto_approve` (skip the request, continue down `approve` with `output.autoApproved = true` — opt-in because it silently waves the record through), `fallback` (request opens on the sibling `fallbackApprovers` instead — same shape as `approvers`, required by this policy and refused under any other; a fallback that itself resolves to nobody degrades to `admin_rescue`). Declare it explicitly on any node with an `expression` approver (linted) |

Every clause of the gloss maps to a schema line above (opens on fallbackApprovers :868; same shape :892; required iff / refused otherwise :898; degrades to admin_rescue :880-881). No new row for fallbackApprovers, no example, no other line.

Token ratchet (node scripts/check-skills-token-ratchet.mjs, convention ceil(utf8 bytes / 4)): before, on e5705a8ea: 「✓ check-skills-token-ratchet: skills/objectstack-automation/SKILL.md is 12511 tokens (ceiling 12768; headroom 257).」 — after, on 2b1d5784e8: 「✓ check-skills-token-ratchet: skills/objectstack-automation/SKILL.md is 12567 tokens (ceiling 12768; headroom 201).」 (+56 tokens = 224 bytes; ceiling untouched, no ruling needed). Line readings (the skills/** rule): file 961 → 961 lines; package — all ten SKILL.md under skills/ — 6145 → 6145 lines. The edited row was already the file's widest line (512 chars); it is now 700 chars. check:doc-authoring, check:skill-frame-sync and check:skill-compatibility are green on it.

Carrier 2 — docs/qa/platform-checklist/areas/approvals.json: the enumeration exists, and it was edited

Premise correction to the claim (5721118247), not to the card: the seat's grep found onEmptyApprovers only at :580 / :631 (prose, 'fail' on a showcase stage) and the variants list at :825 (the escalation action vocabulary). On e5705a8ea the file has 15 hits, and the item approvals.approver-resolution-matrix (:1176-:1265, revision 1 dated 2026-08-30, i.e. older than the card) enumerates the members three times, plus the runtime fork's outcomes once:

  • :1207 steps — 「empty-slate forks, one scratch flow per policy over an UNSTAFFED position: onEmptyApprovers absent/admin_rescue → …; 'fail' → …; 'auto_approve' → …」
  • :1236 acceptance clause — 「the empty-slate fork honors the declared policy: admin_rescue (the default) …; 'fail' …; 'auto_approve' …」 with its verify at :1238
  • :1256 source — 「(onEmptyApprovers admin_rescue|fail|auto_approve, default admin_rescue)」
  • :1257 source — 「(the empty-slate fork: 'empty' = no concrete person, NO_APPROVERS / autoApproved / admin_rescue warn)」

The escalation variants at :825 are untouched (a different vocabulary). Edits, each in the line's own register:

  • :1207 steps — appended 「; 'fallback' (fallbackApprovers naming a STAFFED target, e.g. org_membership_level 'owner') → the request opens on the fallback slate — pending_approvers holds the fallback's resolved ids, no literal slot — and the 'opening on the declared fallbackApprovers' warn lands; a fallback that itself resolves to nobody degrades to admin_rescue with its own 'resolved to nobody either' warn」 (both warn fragments are verbatim substrings of approval-service.ts:2807 and :2821).
  • :1236 clause — inserted 「; 'fallback' opens the request on the declared fallbackApprovers (resolved by the same resolver as approvers) and warns that the fallback fired — a fallback that resolves to nobody too degrades to admin_rescue」 before the trailing 「— 'empty' meaning no CONCRETE person …」.
  • :1238 verify — appended 「; the fallback request's pending_approvers holds the fallback slate, never the abandoned literal slot」 (the clause's verification half; the other three policies' verify already names the admin_rescue assertion).
  • :1256 source — 「admin_rescue|fail|auto_approve|fallback, default admin_rescue), (fallbackApprovers — same shape as approvers, required iff 'fallback' and refused under any other policy: checkFallbackApproversPairing)」.
  • :1257 source — 「NO_APPROVERS / autoApproved / fallbackApprovers re-expansion / admin_rescue warn」.
  • The checklist's own change register (docs/qa/platform-checklist/README.md:287 「Change — edit the fields, bump revision, append a history entry」; enforced by scripts/check-platform-checklist.mjs:2170): revision 1 → 2 and one history row { "revision": 2, "date": "2026-09-17", … "ref": "#18527" }. The revision-1 history text (「all three onEmptyApprovers forks were untested」) is a dated record and stays as written.

Not touched: the item's title, knownGaps, negative and the escalation item. pnpm check:platform-checklist → exit 0 (「check-platform-checklist: OK — 15 areas, 264 items (264 active) … symbol anchors: 577/633 resolved」, same counts as before the edit).

Reverse check of the register, from the committed head (trap-restored, absolute paths): with only the revision reverted to 1 on disk (node exact-once replacement; git diff --numstat = 1 1), node scripts/check-platform-checklist.mjs → exit 1: 「✗ approvals.json · approvals.approver-resolution-matrix: "revision" (1) must equal the last history entry's revision (2) — a semantic edit bumps both」. Restored with git checkout HEAD -- docs/qa/platform-checklist/areas/approvals.json: git hash-object = HEAD blob 4dd89a76661948c3b1108e35a4a5df92d7a6283a, git diff HEAD empty, git status --porcelain empty, validator exit 0 again.

Changeset

skip-changeset, decided by the repo's own gate — the 「Require a changeset (or the skip-changeset label)」 step of .github/workflows/pr-automation.yml (:757-:780): route 2 「It releases nothing (.github/, .claude/, skills/, docs/, content/, …) → apply the 'skip-changeset' label. PREFERRED」, with skills/** spelled out at :771. Measured on this tree: a walk over the 83 tracked package.json manifests finds 0 whose files[] names a skills path and 0 naming docs/qa/platform-checklist (positive control: 70 name dist); no skills/*/package.json, no docs/qa/package.json. The 14 changeset-triggered families the derivation lists under 「Once a changeset exists」 do not apply (no changeset written). node scripts/check-changeset-fixed.mjs (roster family) → exit 0.

Gates (worktree at 2b1d5784e8)

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack — change set derived from git, 2 paths vs merge base e5705a8ea; 24 commands derived, all 24 run with redirect-then-$? capture; --ran reconcile: 「Run reconciliation — 24 derived, 24 run, 0 NOT-MEASURED, 0 UNRUN.」, exit 0. pnpm --filter @objectstack/lint run check:doc-formula-expressions was run after building @objectstack/lint... under scripts/pm/os-verify-lock.sh (「VERDICT command-exit 0 · held the lock 137s · waited 0s」).

node scripts/check-ci-filter-parity.mjs :: exit 0
node scripts/check-closing-keyword-parity.mjs :: exit 0
node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0
node scripts/check-comment-mask-corpus.mjs :: exit 0
node scripts/check-doc-route-spelling.mjs --advisory :: exit 0
node scripts/check-doc-route-spelling.mjs --self-test :: exit 0
node scripts/check-skills-token-ratchet.mjs :: exit 0
node scripts/check-skills-token-ratchet.mjs --self-test :: exit 0
pnpm --filter @objectstack/lint run check:doc-formula-expressions :: exit 0
pnpm --filter @objectstack/spec run check:skill-docs :: exit 0
pnpm check:agent-test-spelling :: exit 0
pnpm check:corpus-claim-drift :: exit 0
pnpm check:cross-package-test-inputs :: exit 0
pnpm check:doc-authoring :: exit 0
pnpm check:driver-memory-census :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:platform-checklist :: exit 0
pnpm check:pm-governed-merges :: exit 0
pnpm check:refd-timer-probe :: exit 0
pnpm check:role-word :: exit 0
pnpm check:skill-compatibility :: exit 0
pnpm check:skill-frame-sync :: exit 0
pnpm check:skill-identifier-liveness :: exit 0
pnpm check:watch-hint-literal :: exit 0

Beyond the derived set: pnpm lint (repo-wide, eslint . --no-inline-config) → exit 0 at 2b1d5784e8; roster families node scripts/check-published-list-mirrors.mjs → exit 0 and node scripts/check-platform-checklist-watchdog.mjs → exit 0; control-character self-scan (grep -naP over both files) → no match. No ① dependency-closure build and no ② package test are owed — the diff touches no package.

Governed: node scripts/pm/check-governed-merges.mjs --test skills/objectstack-automation/SKILL.md docs/qa/platform-checklist/areas/approvals.json → exit 3, skills/** ×1 hit (the published skills catalog), 「One hit governs the whole PR」. Served tier: MANDATORY, derived from the skills/** surface by dispatch-gates.mjs --tier. Draft at the governed terminal; the seat's four-piece, the maintainer lands.

Acceptance notes

  • noted, not filed — 承接者: the skills seat: the claim's carrier-2 reading (three onEmptyApprovers hits) undercounted a 15-hit file; the enumerating item predates the card. Corrected above; nothing to file.
  • noted, not filed — 承接者: 无: the onEmptyApprovers table row is now 700 characters on one line; the file's table rows are single lines by construction and no gate reads line width here. Not a defect.

维护者速读(草稿)

改了什么:审批节点空名单策略 onEmptyApprovers 自 PR #18525 起有四个成员,两处仍只列三个。① skills/objectstack-automation/SKILL.md 第 728 行 Node Config 表的该行,按本行既有的括号注释格式补上第四个成员 fallback(请求改在兄弟键 fallbackApprovers 上开出;与 approvers 同形;此策略下必填、其它策略下拒收;备选名单也解析为空时退化为 admin_rescue)。② docs/qa/platform-checklist/areas/approvals.jsonapprovals.approver-resolution-matrix 这一条的空名单分叉:steps、验收子句及其 verify、两条 source 引用各补上 fallback 一腿,并按清单自身规则 revision 1→2、加一条 history。两文件之外一字未动。

为什么改:词表是 spec 声明的(approval.zod.ts:884),已发布 skill 与测试清单各自枚举它;少列一个成员,AI 作者按 skill 写元数据就不会知道有 fallback,测试清单也测不到这条分叉。语义全部取自 schema docblock 与运行时分叉,不取卡面。

风险与代价(含回滚):纯文档/清单改动,不发布任何 npm 内容(files[] 实测 0 命中);skills token 棘轮 12511→12567(上限 12768,余量 201),不动上限。回滚 = revert 这一个提交。

席位意见:

你要做的:确认第 728 行新增括号注释与 schema 语义一致(见上文逐句对照),点合并;怎么验:node scripts/check-skills-token-ratchet.mjspnpm check:platform-checklist 两条门禁的判定行已在正文引用。


Generated by Claude Code

…'s config table and the approvals checklist

The approval node's empty-slate policy gained a fourth member,
`onEmptyApprovers: 'fallback'` paired with `fallbackApprovers`
(ApprovalNodeConfigSchema, packages/spec/src/automation/approval.zod.ts).
Two carriers still enumerated three of the four:

- skills/objectstack-automation/SKILL.md — the `onEmptyApprovers` row of
  the Node Config table gains `fallback` in the row's own parenthetical
  register, naming `fallbackApprovers` as its sibling. Token ratchet
  12511 -> 12567 of 12768 (headroom 257 -> 201).
- docs/qa/platform-checklist/areas/approvals.json — the
  approvals.approver-resolution-matrix item's empty-slate fork gains the
  `fallback` leg in its steps, its fork clause and verify, and both
  source citations; revision 1 -> 2 with a history row, per the
  checklist's own change register.

Semantics are read from the schema docblock and the runtime fork in
plugin-approvals (openNodeRequest), not from the card's prose.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu

Copy link
Copy Markdown
Collaborator Author

维护者速读(终稿)

skills 席 session_01Gqi43smmqjJ5sUrhfoPeKu,2026-09-17T21:40Z。对照本席自己读的 diff 校正 dev 草稿;席位意见已填。

改了什么 — 审批节点的空名单策略 onEmptyApprovers 自 PR #18525(#17931)起有四个成员,两处仍只列三个。① skills/objectstack-automation/SKILL.md :728(Node Config 表里唯一枚举该词表的行)按本行既有的括号注释格式补第四个成员:「fallback(请求改在兄弟键 fallbackApprovers 上开出;与 approvers 同形;此策略下必填、其它策略下拒收;备选名单也解析为空时退化为 admin_rescue)」;文内 :635 只点名该键、:653 / :693 只是示例取值,均不动。② docs/qa/platform-checklist/areas/approvals.jsonapprovals.approver-resolution-matrix 一条(revision 1,2026-08-30,早于本卡——本席认领时说这个载体可能没有枚举,是本席 grep 少数了,卡面前提成立):steps :1207、验收子句 :1236 及其 verify :1238、两条 source 引用 :1256 / :1257 各补 fallback 一腿,按清单自身规则 revision 1→2、加一条 history(ref #18527);:825 的升级动作词表不动。两文件之外一字未动。

为什么改 — 词表是 spec 声明的(approval.zod.ts :884 枚举;:906–:907 fallbackApprovers;:897–:898 与 checkFallbackApproversPairing 的配对约束;运行时分叉 approval-service.ts :2782–:2836),已发布 skill 与测试清单各自枚举它;少列一个成员,AI 作者照 skill 写元数据就不知道有 fallback,清单也测不到这条分叉。语义全部取自 schema 与运行时,本席逐行复核 :884 / :897–:898 / :906–:907 / test :352,与新增括号一致。

风险与代价(含回滚) — 纯文档与清单改动,不发布任何 npm 内容(files[] 实测 0 命中,changeset 由仓库自己的 Require-a-changeset 步骤判 skip-changeset);skills token 棘轮 12511 → 12567(上限 12768,余量 201),不动上限、不花裁决;清单校验 check-platform-checklist 本席自跑 OK(15 areas / 264 items);门禁 24 族全绿,pnpm lint 0。回滚 = revert 这一个提交。

席位意见 — 荐合。这是「spec 声明 > 实现 > 文档面」的最小对齐:声明已落,文档面补齐,无新语义。

你要做的(一个动作) — Approve 本 PR(或直接人工合并);席位随后按裁定 C 落地。


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review September 18, 2026 01:14
@os-zhuang
os-zhuang enabled auto-merge September 18, 2026 01:14
@os-zhuang
os-zhuang added this pull request to the merge queue Sep 18, 2026
Merged via the queue into main with commit a09d797 Sep 18, 2026
44 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-18527-on-empty-approvers-fallback-member branch September 18, 2026 02:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/s skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

two declared enumerations of the approval onEmptyApprovers vocabulary go to 3 of 4 members when #17931 lands

3 participants