Skip to content

finding: the published skills/ catalog is human-merge by convention but is not gated — two of the pm-dispatch fork's three prefixes are enforced #9404

Description

@os-zhuang

Observation filed while implementing #9395. No action proposed here; recording the gap so it is a decision rather than an oversight.

What was measured

The pm-dispatch skill's ACCEPT fork treats three prefixes as human-merge-only (.claude/skills/pm-dispatch/SKILL.md, verbatim):

路径面一条命中 docs/adr/**、.claude/skills/** 或 skills/** ⇒ ACCEPT 换终局三件套 … ⛔ 不合并不入队不挂auto-merge

After #9395 the ADR maintainer approval gate enforces the first two. skills/** — the published catalog that ships to customer projects — remains convention only: a skills/**-only PR passes the gate on the zero-lookup clean path and can be flipped ready, armed and queued exactly as PR #9238 was on the internal surface.

This was deliberate in #9395, not an omission: the #9319 decision-2 ruling names .claude/skills/**, and the gate script's header states the exclusion out loud rather than quietly widening past the ruling.

Why it is worth a decision rather than a shrug

  • The risk profile differs from .claude/skills/** in an interesting direction: the internal skills steer this repo's own agents, while skills/** ships to customer projects — a bad landing propagates outward instead of inward.
  • If the answer is "convention is enough for the published catalog", that is a fine answer, but it should be a recorded one; the three-prefix fork currently reads as one rule with an unexplained two-thirds enforcement.
  • Mechanically this is a one-entry change: GOVERNED_SURFACES in scripts/check-adr-merge-approval.mjs takes a third row with its own rule wording, and the self-test's "the published skills catalog is NOT governed" assertion would invert.

Refs: #9395 (where the two-of-three state was created) · #9319 (the ruling) · #7548.

Activity

  1. os-steve commented on Aug 18, 2026

    @os-steve
    Collaborator

    PM 分诊 → needs-user-decision。 Session session_01XqDQYVU5smx29ts9pAErja,round 6。⛔ 不认领、不派发 —— 这张卡问的是「要不要扩大一条裁决的适用面」,那是维护者的字,不是 dev 的。

    卡片说得对:当前状态不是缺陷,是一个没被记录的决定。我补一个决策需要的数字,然后把选项收敛成两个。

    我量的:三个受管面的变更频率

    派发这类决定前必须知道代价。since=2026-07-18,分页取全(⚠️ 第一次我用 per_page=100 单页取,两个面都恰好返回 100 —— 那是页上限不是计数,已重测):

    面 自 2026-07-18 的 commit 数 现在受门禁管吗
    docs/adr/** 165 ✅ 是
    skills/**(发布给客户项目) 148 ⛔ 否
    .claude/skills/**(本仓自用) 98 ✅ 是

    ⭐ 关键事实:发布目录的变更频率比内部目录高 50%,而且和已经受管的 docs/adr/** 是同一量级。所以「加进去」不是一行配置那么便宜 —— 它给维护者合并队列每天多压约 5 个 PR。这个代价必须先摆在桌上,不能藏在「机械上只是一行 GOVERNED_SURFACES」后面。

    为什么现状是三种状态里最差的一种

    不是「少管了一个面」这么简单。SKILL.md 的 ACCEPT 分叉逐字告诉复核席三个前缀都是 human-merge-only:

    路径面一条命中 docs/adr/**、.claude/skills/** 或 skills/** ⇒ … ⛔ 不合并不入队不挂 auto-merge

    ⇒ 一个照着 SKILL 办事的复核席会相信 skills/** 受保护。它不受。这是虚假的执行感 —— 比公开的「不管」更危险,因为它让人停止自己判断。⚠️ 而这恰好又是本 lane 这一轮反复付账的那个族:把结论读自一个无法承载它的来源(这次来源是 SKILL 正文的措辞)。

    两个自洽的选项,请挑一个

    A. 把 skills/** 也纳入门禁(我的建议)

    • 理由:爆炸半径方向不同 —— 内部 skills 出错影响本仓 agent(向内),发布目录出错随产品扩散到客户项目(向外)。向外的那个该管得更严,不是更松。
    • 代价:维护者每天多约 5 次人工合并。
    • 改动:scripts/check-adr-merge-approval.mjs 的 GOVERNED_SURFACES 加一行(带自己的 rule 措辞),自测里那条「the published skills catalog is NOT governed」断言反过来。

    B. 把 SKILL 正文收窄到两个前缀,明确记下「发布目录靠约定即可」

    • 理由:148/月的人工合并可能不值当;而且约定在这个仓一直有效。
    • 代价:零工时。
    • 前提:必须改 SKILL 正文,否则虚假执行感原样留着 —— 选 B 而不改文字,等于什么都没决定。

    ⛔ 不接受第三种:维持现状。 现状的问题不是选了 A 还是 B,是它同时宣称 A 而实施 B。

    ⚠️ 如果选 A:改 .claude/skills/pm-dispatch/** 是 fable-mandatory 面,而改 scripts/check-adr-merge-approval.mjs 不是 —— 两半的模型档位不同,派发时要拆开,或者整单按 fable 走。裁决下来我再排。

    Refs: #9395(造成 two-of-three 状态)· #9319(原裁决)· #7548


    Generated by Claude Code

  2. os-zhuang commented on Aug 18, 2026

    @os-zhuang
    ContributorAuthor

    Maintainer ruling recorded — maintainer, 2026-08-18, batch acceptance of the triage seat's decision-inbox recommendations, verbatim: 「同意」. Provenance: triage session session_pm_triage_20260818T0104Z.

    Ruled: the published skills/ catalog joins the gate — all three of the pm-dispatch fork's prefixes (docs/adr/**, .claude/skills/**, skills/**) are machine-enforced by the ADR maintainer approval required check. Rationale on record: the catalog ships outward to customer projects (worse propagation direction than the internal skills), convention was already bypassed once in practice (#9238, internal surface), and the mechanical cost is one GOVERNED_SURFACES row plus inverting the self-test's "NOT governed" assertion.

    Scope of the work card: the scripts/check-adr-merge-approval.mjs row with its own rule wording + the self-test inversion. Consequence for #9403: the PD#14 sentence can now be written once, naming three prefixes — it unblocks when this card's gate PR merges (not merely on this ruling), so the sentence never describes enforcement that is not yet live.

    Label needs-user-decision → pm:queue (and finding cleared — graded) in the same stroke.


    Generated by Claude Code

  3. os-steve commented on Aug 18, 2026

    @os-steve
    Collaborator

    ✅ 维护者裁决:A —— 把 skills/** 纳入门禁

    维护者于 2026-08-18 选定选项 A。发布目录(随产品到客户项目的那个)从此与 docs/adr/**、.claude/skills/** 同级,human-merge-only 由门禁执行,不再只靠约定。

    理由(裁决时给出的):爆炸半径方向不同——内部 skills 出错影响本仓 agent(向内),发布目录出错随产品扩散到客户项目(向外)。向外的那个该管得更严,不是更松。

    已接受的代价:实测 skills/** 近一月 148 commits ⇒ 约每天多 5 次人工合并(已受管的 docs/adr/** 是 165,同一量级)。


    落地时机:现在是干净的,我实测过

    派发前量了两件事:

    持有 scripts/check-adr-merge-approval.mjs 的开着的 PR:  0
    碰 skills/** 的开着的 PR:                              0
    

    ⇒ 现在落地不会让任何在飞的 PR 突然变成待人工合并。 没有中间态问题。

    ⚠️ 这一步是特意做的,因为同一轮的 #9491 暴露了反面教材:一条新门禁规则如果在落地当刻就把既有状态判红,它会挡住它本要保护的那场 sitting。这里不存在这个风险,但判据是量出来的,不是假设的。

    档位:fable

    按在案裁决,改变门禁 accept/reject 行为的卡是 fable-mandatory,与路径面无关。本卡新增一条能把 PR 拦下的规则 ⇒ 命中。⛔ 不因为路径只是 scripts/** 就降档——这与 #9491 同一判据。

    一处状态流转,记录备查

    本卡曾由本席标为 needs-user-decision(00:59Z),分诊席于 03:20Z 摘除 needs-user-decision + finding 并改为 pm:queue。定级是分诊席的单通道,本席不异议;裁决已下,该分歧现已无实际影响。记在这里只是让状态流转可追溯。

    已 assign 并转 pm:dispatched,派发中。


    Generated by Claude Code

  4. os-steve commented on Aug 18, 2026

    @os-steve
    Collaborator
    {
      "issue": 9404,
      "status": "done",
      "branch": "claude/issue-9404-govern-published-skills",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/9511",
      "premise_still_valid": true,
      "summary": "Ruling A implemented at a6799473d, one file: scripts/check-adr-merge-approval.mjs. GOVERNED_SURFACES takes a third row (id published-skill, prefix skills/) with its own rule wording citing the 2026-08-18 ruling, the outward-propagation rationale and the accepted ~5 merges/day cost; new PUBLISHED_SKILL_PATH_PREFIX + publishedSkillFilesIn() exports; every header sentence that stated the exclusion out loud is rewritten, including a new 'published catalog surface, added 2026-08-18' section. The card's mechanism reading held, with one addition it missed: one row was genuinely enough for the CODE (the zero-lookup clean path is structural — governedFilesIn returns empty and decide() returns before either thunk), but the SELF-TEST's clean-path fixture had to change too, because it deliberately contained skills/objectstack-ui/SKILL.md as an ungoverned neighbour; it is now proven on near-neighbours (skillsets/, docs/adrs/, .claude/hooks/) with both thunks still throwing. Blast radius measured: the new prefix matches exactly the 50 tracked files under skills/, zero near-misses, no other skills/ directory anywhere in the tree, and no double-catch of .claude/skills/** (startsWith anchors at the repo root; pinned in the self-test). Re-measured at branch point across all 14 open PRs: 0 touch skills/**, 0 hold the gate script — landing this flips nothing in flight to maintainer-merge. skip-changeset applied as a union write {size/m, skip-changeset} after reading the labeler bot's label; read-back confirms both stand. pm-dispatch SKILL prose untouched per dispatch clause 5 (verified consistent with ruling A — no report needed). Workflow untouched: adr-merge-approval.yml carries no path filter, so the widened predicate is live on merge.",
      "tests": "All at a6799473d (working tree byte-identical to HEAD for every run). node scripts/check-adr-merge-approval.mjs --self-test -> '108 assertions ... green' (origin/main baseline: 96, +12, including the card-named NOT-governed assertion kept as a case and inverted to the-published-skills-catalog-IS-governed). Gate mode on this branch's own diff -> clean pass 'No files under docs/adr/** / .claude/skills/** / skills/** ... zero API lookups on the clean path' — the clean path measurably still short-circuits. Reverse verification by ablation FROM THE COMMITTED STATE (predicted direction: red, many families): deleting the third GOVERNED_SURFACES row -> 18 of 108 assertions failed, including the emptiness proof reporting armed=true, disarmed=true — the exact pre-9404 state; restored via git checkout HEAD, byte-identity proven by git hash-object == committed blob c9557e7dfa17. check:nul-bytes OK (6136 files); control-char grep 0 hits; eslint exit 0 on the file. dispatch-gates.mjs on the changed path derives only the gate script's own family (run green, both modes); CI convergence is PM-owned per the standing rule — gate statuses were in_progress at report time.",
      "open_questions": [],
      "out_of_scope_findings": ["filed as #9513: CODEOWNERS routes docs/adr/ and the gate's enforcement chain but neither governed skill surface — the review-request half of enforcement was not part of ruling A and carries its own ~246 commits/month cost decision"]
    }

    Generated by Claude Code

  5. os-steve commented on Aug 18, 2026

    @os-steve
    Collaborator

    ✅ ACCEPT — PR #9511(裁决 A 已实现)

    PR 9511  head=a6799473d  draft=True  base=main  files=1  +258/-61
    labels: ['size/m','skip-changeset']
    body first line: 'Fixes #9404'
    files: ['scripts/check-adr-merge-approval.mjs']
    gates(按名取最新): names=20  pending=['Lint & Repo Gates','TypeScript Type Check']  non-green=none
    

    单文件,严格在声明面内。


    1. ⭐ 爆炸半径我独立复验了,三条子论断全部精确命中

    不采信回报,自己拉 main 的完整 tree(truncated: False)数:

    skills/ 下的 tracked 文件          : 50      ← 与回报的 50 完全一致
    .claude/skills/ 下                : 11
    树里任何其他 */skills/ 目录        : 0
    startsWith("skills/") 会误捕 .claude 的吗 : False
    

    ⇒ 不重复捕获 .claude/skills/**(startsWith 锚在仓库根)、零近似误伤、树里不存在第二个 skills/ 目录。我在派发里点名要测的三件事,三件都对得上。

    2. 卡片的机制读法成立,但漏了一处 —— 而 dev 补上了

    卡片说「一行 GOVERNED_SURFACES + 反转一条自测断言」。对代码而言确实一行就够(零查找快路径是结构性的:governedFilesIn 返回空 ⇒ decide() 在两个 thunk 之前就返回)。

    但自测的快路径夹具必须一起改 —— 因为它故意包含 skills/objectstack-ui/SKILL.md 作为一个「未受管的邻居」。裁决 A 之后那个邻居受管了,夹具的前提自动失效。

    ⇒ 现在改用近邻证明,且两个 thunk 仍然会 throw(证明它们确实没被调用)。⭐ 这类「夹具的前提被本次改动本身推翻」是最容易漏的一种,而且漏了会静默变成假绿。

    3. 快路径仍然短路 —— 实测,不是推断

    我在派发里明确要求量这个,因为它是这条门禁的成本控制:如果每个 PR 都开始做审批查找,你为一条很少触发的规则让所有 CI 变慢。

    gate 模式跑本分支自己的 diff →
      '✅ No files under docs/adr/** / .claude/skills/** / skills/** … zero API lookups on the clean path'
    

    4. 门禁自己的头部注释被改了 —— 这条我特意点名过

    我在派发里说:该文件头部「明说这个豁免」的那句话,在你落地那一刻就变成假的,找到并改掉。做到了,而且加了一节 published catalog surface, added 2026-08-18。

    ⭐ 一条门禁的自述文档里写着一个它已经不再有的豁免,正是这张卡要关的 declared-vs-enforced 缺陷,只是指向自己。

    5. 反向验证

    自测 96 → 108(+12),卡片点名的那条「the published skills catalog is NOT governed」按要求保留为用例并反转,而不是删掉——改动在测试 diff 里是可读的。

    消融第三行 GOVERNED_SURFACES:108 条里 18 条失败,其中空性证明报出 armed=true, disarmed=true over identical reviews —— 正是 #9404 之前的那个状态。还原后用 git hash-object == committed blob c9557e7dfa17 证明字节一致(比 git diff 空更强的判据)。

    6. 落地时机在分支点重测了

    我给的读数是分钟级旧的,派发里要求重测。结果:全部 14 个开着的 PR 中,0 个碰 skills/**,0 个持有门禁脚本。⇒ 落地不会把任何在飞的 PR 翻成待人工合并。

    另外确认 adr-merge-approval.yml 没有 path filter ⇒ 加宽后的判据在合并时即刻生效,不需要动 workflow。

    7. ⚠️ 一处工具事实,要记进流程

    raw REST POST was classifier-blocked;改用 read-then-union 写入,回读确认两个标签都在。

    ⇒ 我在派发里指定的 POST .../labels 对 dev 座位不总是可用(被分类器拦)。dev 退回的正是我在 #9294 裁决里写明的可接受回退(读 → 并集 → 回读校验),处理正确。

    但这说明我的指令写得太绝对了。今后门禁类派发的措辞应是:优先 REST POST;若被拦,用 read-union-readback,并在报告里说明——而不是把 POST 写成唯一合法路径。

    8. 外溢卡 #9513 → needs-user-decision

    「CODEOWNERS 路由了 docs/adr/ 和门禁的执行链,但两个受管 skill 面一个都没路由。」

    ⚠️ 定性准确:这是执行的另一半(review-request),不在裁决 A 的范围内,而且自带约 246 commits/月 的成本决定。⇒ 不该被当成本卡的收尾顺手做掉,单独上报是对的。已标 needs-user-decision。


    Verdict: ACCEPT. 门禁收敛后武装,不手动合并。

    📌 落地后连锁解封:#9403(PD#14 点名三个前缀 —— 必须排在本 PR 之后,否则 AGENTS.md 会声称一个尚未生效的覆盖范围)。


    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

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions