feat(scripts): 派发前框架新鲜度自检门禁 —— 三份框架副本 vs origin/main (#5866) - #6105
Merged
Conversation
#5866 的通道:派发提示词由 harness 从磁盘上的工作树装配,而非 origin/main。 共享 PM 检出长期停在长命分支上就会漂移 —— 2026-08-06 实测一棵落后 173 个提交 的检出,三份框架文档全部还是 #5130 之前的两轴决策框架,从它派发出去的每个 dev agent 都少了 business-need 那条轴,而该轴按 #5130 的裁决是会改变结论的 (#5021 与 #4936 同形状反向判,只有这条轴能把它们分开)。 与 #5865 的 check:skill-frame-sync 是两条不同的不变量,按分诊裁决分脚本: sync 问「同一棵树内四份副本是否同构」,本门禁问「这棵树相对 origin/main 是否 新鲜」。一棵一致地停在两轴的旧树在 sync 上是绿的(副本确实同构,按设计正确), 却正是 #5866 报告的缺陷 —— self-test 把这条独立性钉死:同一份 fixture 跑两个 门禁,断言 sync 绿、freshness 红。 COPIES / AXIS_MAP / analyzeCopy / runAllChecks 从 check-skill-frame-sync.mjs 导入(仅加 export 关键字 + 入口守卫,零行为改动),使「框架结构」只有一份定义; 把锚点复制进第二个脚本,等于在门禁自己身上复刻它们所要治的手抄漂移病。 判据是轴数与轴序(经 AXIS_MAP 映射),不是字节:字节比对会在每条合法改这段 文字的分支上变红,一天之内就会被绕过。字节差异只作为信息打印,从不参与判定。 离线降级为警告而非硬红(分诊明确要求):fetch 失败时按 refs/remotes/origin/main 降级比对,无该引用则告警放行。实现为单一代码路径上的严重度钳制 —— 离线跑的是 同一段比对、打同一段诊断,只有退出码不同,避免降级分支无人执行而腐烂。 不接 lint.yml:CI 检出的是全新 merge ref,本门禁在那里恒绿;而在任何合法修改 框架的 PR 上,「与 main 不同」恰恰是该 PR 的目的,接进去就是假红。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BDmDsu2575gDxeMCxXhDE3
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
hotlong
marked this pull request as ready for review
August 7, 2026 02:01
This was referenced Aug 7, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #5866
按分诊裁定,严格只做处置 3:给派发前加一道「框架新鲜度」自检门禁。⛔ 处置 1(harness 从 origin/main 读 agent 定义,非本仓代码路径)与处置 2(PM 纪律对齐检出)明确不做,也不在本 PR 讨论范围内。
前提复核(Prime Directive #6,落地前实测 origin/main)
scripts/check-skill-frame-sync.mjs在 main 上AXIS_MAP是否已导出const AXIS_MAP,第 147 行);COPIES、analyzeCopy、runAllChecks同样未导出,且main()在模块顶层无守卫直接执行scripts/下唯一带 fresh 的是check-objectui-pin-fresh.mjs,判的是 objectui pin,与本单无关/home/user/objectstack停在claude/pm-dispatch-devx-rgxpjf,git rev-list --count HEAD..origin/main= 59。这与 issue 记录的claude/pm-dispatch-devx-987d90(落后 173)是不同分支、不同座位——devx 座位当场 ff 只治好了那一次实例,系统性通道原样敞着前提成立。附带一条对本门禁设计有直接影响的观察:这棵落后 59 个提交的检出,它的框架恰好还是三轴的。也就是说「落后多少提交」和「框架是否过时」是两件事——门禁若按提交距离判,今天就会对这棵完全健康的树报红。判据必须是框架结构本身(见下)。
两条不变量的边界:为什么是新脚本,不是塞进 #5865
分诊这一条写得很明确,这里把它钉在代码里:
check:skill-frame-sync(#5798 / PR #5865)check:skill-frame-freshness(本 PR)origin/main是否新鲜两者互相独立:一棵落后 173 提交、四份副本一致地都停在两轴的旧树,在 sync 上是绿的——副本确实同构,那是按设计的正确行为——而它正是 #5866 报告的缺陷本体。
这条独立性不是写在注释里就算数,self-test 把它钉死了:同一份两轴 fixture 同时喂给两个门禁,断言 sync 绿、freshness 红。实跑输出:
这就是分诊说的「说谎的绿灯」形状,现在有机器证据。
判据复用:一份结构定义,不是第二套锚点
COPIES/AXIS_MAP/analyzeCopy/runAllChecks从check-skill-frame-sync.mjsimport,不复制。对该文件的改动只有 4 个export关键字、1 个 import 补充和 1 个入口守卫,零行为改动(git diff中所有其余增行都是注释):入口守卫是必需的:没有它,本门禁的 import 会把整个 sync 门禁连同它的
process.exit(1)当作 import 副作用跑掉。这不是新发明,scripts/objectui-changeset-digest.mjs被check-objectui-pin-fresh.mjsimport 时用的就是同一套写法(第 665 行)。pnpm check:skill-frame-sync→ 12 个 self-test 用例 + 真实扫描全部照跑,输出未变。理由:把锚点抄进第二个脚本,等于在门禁自己身上复刻它们所要治的手抄文本漂移病。现在「框架结构」全仓只有一份定义。
判据是结构,不是字节
轴数 + 轴序(经
AXIS_MAP映射)。不是字节比对——字节比对会在每一条合法修改这段文字的分支上变红(连重新折行都会),一天之内就会被绕过。字节差异只作为信息打印,从不参与判定:同理,「落后 N 个提交」只作为上下文打印,不是判据。前提复核里那棵落后 59 提交、框架完好的检出,门禁对它说的是:
反方向也处理了:如果 HEAD 已经包含了那个 ref,本树按构造不可能过时,此时的结构差异是有意的本地修改(多半就是正在改框架的那个 PR 自己),门禁降级为提示并明说,不去骚扰它。
离线降级:探针选择与实现形态
分诊的硬要求:判据要读
origin/main,因此依赖一次网络 fetch,离线必须降级为警告而非硬红。降级阶梯:git fetch --quiet --no-tags origin main,20 秒超时(派发时刻没人愿意为一个门禁等一分钟),成功 → 判FETCH_HEAD,权威,结构落后即硬红 exit 1;refs/remotes/origin/main,降级:同样比、同样报,但只警告 exit 0,并打印该 ref 的最后更新时间——它自己可能和你的树一样旧,所以那份比对是证据不是证明;实现上,降级是单一代码路径上的一次严重度钳制,不是第二条分支:离线跑的是同一段比对、打同一段诊断,只有退出码不同。写成独立的 degraded 分支就没人会执行到,然后腐烂——这道门禁的离线路径就是它的在线路径。
self-test 里最要紧的一对用例正是钉这条契约:同一份 fixture、同一段诊断,严重度只由「有没有拿到活 ref」决定。
与
check-objectui-pin-fresh.mjs「网络失败绝不算绿」的对照值得写明:两边都对,因为受众不同——那道门禁跑在 CI 的 release PR 上,网络是契约的一部分;这道跑在长命的人/agent 工作树上,离线是很平常的一天。同一个词 freshness,作用半径完全不同。硬红一次就会教会所有人绕过它,那比没有门禁更糟(与 #5864「开局就误报的门禁」同一条教训)。门禁跑在哪里,以及为什么不接 CI
跑在:根
package.json的check:skill-frame-freshness,由人 / PM / agent 在派发时刻在长命工作树里手动跑。不接
lint.yml,两条理由,第二条是决定性的:objectstack-pm-dispatch仍是两轴决策框架 —— 与.claude/内部三轴(#5130)分叉,且「创业聚焦」轴是否该泛化给第三方项目未定 #5451/PR docs(skills): 已发布 skillobjectstack-pm-dispatch的决策框架由两轴升为三轴(裁决 B:泛化内核后搬) #5799 那类),「与 main 结构不同」恰恰是该 PR 的目的。接进 CI 会让门禁去红那些正确的 PR,然后它会被删掉。所以本 PR 完全没有 CI 接线,非阻塞的也没有——刻意不扩大爆炸半径。附带确认:根
package.json里没有check:*聚合脚本(各门禁在 lint.yml 里逐条显式调用),所以新增这一行不会被任何聚合隐式带进 CI。命名按族内惯例取
check-skill-frame-freshness.mjs/check:skill-frame-freshness,与check-skill-frame-sync共用skill-frame前缀,使这对「同构 / 新鲜度」不变量在ls scripts/和 package.json scripts 块里一眼可辨。已知边界,写出来而不是藏着
一棵老到不包含本文件的树,跑不了本门禁。所以它是前瞻性的:防止树从 main 漂走,治不了已经漂到它自己诞生之前的树。#5866 的处置 1(harness 直接从
origin/main读 agent 定义)才是治本,且在本仓之外——本门禁是缓解不是根治,分诊原文如此,脚本头注释里也如此写明。self-test:12 个用例,双向 + 降级
--self-test接在check:skill-frame-freshness同一条链上(--self-test &&真跑),不存在未被调用的幻影自检。fixture 用真实文档(不是合成文本)+ 临时 git 仓库(族内惯例,见check-empty-changeset.mjs/check-nul-bytes.mjs),origin指向一个不存在的路径,所以离线那一档是被真实代码真实失败地跑到的,不依赖网络、不用 mock。两处值得单独说:
反向验证(方向在跑之前先声明)
声明的方向:普通「红」。 一棵一致停在两轴的工作树、对着三轴的模拟
origin/main,必须响亮变红并点名过时文件;把树恢复成三轴文档,必须变绿。这里不可能出现 #5046 的「诊断变多」或 #5009 的「反转」方向:本门禁是给一条此前完全无人看守的不变量新加的钉子,没有任何过度伸张的旧规则可供反转,也没有被删掉的
??限支去改变某个计数。方向就是最朴素的那一种,先声明、后实跑。实跑记录(真实 git 仓库,真实退出码):
方向与声明一致:删轴 → 红且点名全部 4 份副本、缺的正是 business-need;恢复 → 绿。红文案里带着分诊指定的那一句修法原文。
本树实跑(两种模式)
门禁
pnpm check:skill-frame-freshness(self-test + 真跑)pnpm check:skill-frame-sync(改动后回归)npx eslint scripts/check-skill-frame-freshness.mjs scripts/check-skill-frame-sync.mjs --no-inline-confignode scripts/check-nul-bytes.mjsgrep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]'package.json 邻接(PM 排序用)
新增行落在 line 40,紧跟
check:skill-frame-sync(line 39)——语义相邻,且刻意远离冲突热区。建 PR 前实测origin/main:在飞的 PR #6093(check:override-consistency)插在check:prerelease-pins(line 56)之后,尚未合并。两处相隔 16 行,属不同 hunk,不会冲突,落地顺序无所谓。交付面
仅三个文件,无
packages/、无content/docs/、未改那三份框架文档本身、无.claude/内容改动:scripts/check-skill-frame-freshness.mjsscripts/check-skill-frame-sync.mjs:4 个 export + 入口守卫,零行为改动package.json:一行接线scripts-only ⇒ 不发版 ⇒ 无 changeset,走
skip-changeset标签(绝不提交空 changeset——#6059 的门禁已在 main 上生效)。Generated by Claude Code