Repository navigation
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
Activity
PM 分诊 →
needs-user-decision。 Sessionsession_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
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 theADR maintainer approvalrequired 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 oneGOVERNED_SURFACESrow plus inverting the self-test's "NOT governed" assertion.Scope of the work card: the
scripts/check-adr-merge-approval.mjsrow 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(andfindingcleared — graded) in the same stroke.
Generated by Claude Code
✅ 维护者裁决: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
- added a commit that references this issue
on Aug 18, 2026 { "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
✅ 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
- added a commit that references this issue
on Aug 23, 2026
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):After #9395 the
ADR maintainer approvalgate enforces the first two.skills/**— the published catalog that ships to customer projects — remains convention only: askills/**-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
.claude/skills/**in an interesting direction: the internal skills steer this repo's own agents, whileskills/**ships to customer projects — a bad landing propagates outward instead of inward.GOVERNED_SURFACESinscripts/check-adr-merge-approval.mjstakes 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.