peaks-loop 4.0.52 → 4.0.54

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/CHANGELOG.md +89 -2
  2. package/README-en.md +1 -1
  3. package/README.md +1 -1
  4. package/dist/cli/commands/codegraph-commands.js +164 -5
  5. package/dist/cli/commands/core/memory-command.js +35 -2
  6. package/dist/cli/commands/dispatch-commands.js +4 -2
  7. package/dist/cli/commands/prd-commands.js +15 -0
  8. package/dist/cli/commands/sub-agent-shared.d.ts +23 -0
  9. package/dist/cli/commands/sub-agent-shared.js +39 -0
  10. package/dist/services/artifacts/artifact-prerequisites.js +34 -0
  11. package/dist/services/audit/enforcer-liveness.js +1 -1
  12. package/dist/services/codegraph/codegraph-config-repair-writer.d.ts +44 -0
  13. package/dist/services/codegraph/codegraph-config-repair-writer.js +112 -13
  14. package/dist/services/codegraph/codegraph-exclude-repair.d.ts +23 -1
  15. package/dist/services/codegraph/codegraph-exclude-repair.js +6 -0
  16. package/dist/services/dispatch/sub-agent-dispatcher.d.ts +0 -1
  17. package/dist/services/dispatch/sub-agent-dispatcher.js +0 -3
  18. package/dist/services/evidence/evidence-generator.js +11 -3
  19. package/dist/services/memory/project-memory-service/index.d.ts +1 -1
  20. package/dist/services/memory/project-memory-service/index.js +1 -1
  21. package/dist/services/memory/project-memory-service/store/atomic-write.d.ts +59 -0
  22. package/dist/services/memory/project-memory-service/store/atomic-write.js +167 -7
  23. package/dist/services/prd/gate-evidence-derivation.d.ts +160 -0
  24. package/dist/services/prd/gate-evidence-derivation.js +270 -0
  25. package/dist/services/prd/handoff-auto-regen.js +13 -1
  26. package/dist/services/prd/handoff-frontmatter.d.ts +1 -1
  27. package/dist/services/prd/handoff-frontmatter.js +62 -0
  28. package/dist/services/prd/handoff-gate-evidence.d.ts +95 -0
  29. package/dist/services/prd/handoff-gate-evidence.js +145 -0
  30. package/dist/services/prd/handoff-service.d.ts +21 -1
  31. package/dist/services/prd/handoff-service.js +45 -2
  32. package/dist/services/prd/handoff-types.d.ts +40 -0
  33. package/dist/services/prd/handoff-types.js +28 -1
  34. package/dist/services/prd/project-scan-reader.d.ts +7 -0
  35. package/dist/services/prd/project-scan-reader.js +7 -1
  36. package/dist/services/rd/reviewer-dispatch-policy.d.ts +36 -8
  37. package/dist/services/rd/reviewer-dispatch-policy.js +36 -8
  38. package/package.json +6 -6
  39. package/skills/bee/peaks-rd/SKILL.md +1 -1
  40. package/skills/bee/peaks-rd/references/rd-fanout-contracts.md +26 -10
  41. package/skills/bee/peaks-rd/references/rd-sub-agent-dispatch.md +1 -1
  42. package/skills/bee/peaks-rd/references/writing-handoff-frontmatter.md +6 -1
  43. package/skills/peaks-code/references/periodic-checkpoint.md +7 -5
  44. package/skills/peaks-code/references/startup-sequence.md +1 -1
  45. package/skills/peaks-final-review/SKILL.md +1 -1
  46. /package/{docs → contracts}/test-style-contract.md +0 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,73 @@
1
1
  # Changelog
2
2
 
3
+ ## 4.0.54 — 2026-09-18 (Phase A 缺陷闭合十片: 9 片真改 + 3 次修复推翻自己 + 4.0.53 宣称闭合的 AC 里 2 个被证伪 — AC-6 是假 pin、AC-1 是空 pin)
4
+
5
+ **Highlights**:
6
+
7
+ 1. **4.0.53 宣称闭合的 AC 里有 2 个是假的 (D 组, 本会话最重要的发现).** 不是"改错了"也不是"没做完", 是**测试通过但断言的不是它说自己断言的东西**:
8
+ - **AC-6 是假 pin**: `karpathy-injection.test.ts` 之所以绿, 是因为 `karpathy-reviewer` 这个**角色名里含有**它要找的 anchor 字符串 —— 断言的是文件名, 不是被注入的规则块。实测: 从块里删掉一个 canonical anchor, 旧测试仍 4 passed / exit 0, 改写后的测试 exit 1。现在 pin 的是四层注入 + 一个六-bee 作用域锁, 规则正文一字未动。
9
+ - **AC-1 是空 pin**: 它 pin 的模块 13/13 导出在 `src/`、`packages/`、`scripts/` (865 个文件) 里**零引用**, 唯一消费者是它自己的测试。pin 住一个没有调用者的死表面, 和没 pin 没有区别。
10
+ - 同组顺带纠正: AC-3 合并了两个完全相同的 case 并删掉一处**从未被断言**的 `skillMd` 读取 (它本想支撑的那条声明不存在, `grep -c` = 0; 留着等于为迁就一个坏测试而发明一条产品义务); AC-4 一直在 pin LLM-arbitrator 的预算, 而披露文字说的是周期性 checkpoint cadence —— cadence 现在有自己的测试, 并修好 D1 漏掉的一处过期指针。
11
+
12
+ 2. **4.0.51 那句「49」句内自相矛盾.** `1 + 20 + 2` 之和是 23, 而同一句话写的是 21; 六个 dangling cross-ref 其实是 12 处指向 8 个目标。所以 49 是**混合单位的和**。「两个从未存在过」的标签被对调 (一个确实存在过 `3c82c797`、于 `f17aa377` 删除, 另一个真的从未存在), 同一处错误第三次出现在守卫自己的 header 里, 现已改名为 `d3a45d45`。声明改写为「在所有存活对象里都不存在」—— `backup-main-pre-rewrite` tag 表明历史被重写过, 无条件断言「从未存在」在本仓不可证。
13
+
14
+ 3. **第三个 reach gap: 带行号的引用此前不可见.** `PATH_SHAPED` 的字符类排除了 `:`, 所以写成 `path.md:127` 的引用完全看不见。`git log -G` 显示**没有任何 commit 把 `:` 放进过那个字符类** —— 它随 `276161d5` 整体到达, 是一次事实上的排除、不是一个写下的决定。`LINE_SUFFIX` 现在先剥掉尾部 `:digits` 再做形状判定与 `exists` 检查, 实测 804 处含冒号的 span 里 0 条是真引用。把字符类直接放宽会报 11 条, 且 11/11 都指向**真实存在**的文件 —— 所以修复是方向性的, 不是数量问题。它暴露出的唯一一条悬空引用在 `skills/peaks-final-review/SKILL.md:233`, 按 4.0.51 的方式处置 (披露而非删除), 因为不存在活的后继文件。
15
+
16
+ 4. **三个守卫看不见自己在被削弱 (C1–C4).** 三者都不是「没在检查」, 而是**活着的检查, 但观察不到自己的削弱** —— 它们抓得住「删除」抓不住「削弱」:
17
+ - `vitest-concurrency-guard` 对整文件匹配 `/maxWorkers,?\s*$/m`, 于是 `// maxWorkers,` 也满足: 把配置行注释掉, 六条断言全绿。现在断言作用在**生效行**上 (注释行先被过滤)。
18
+ - `publish-tag-strict` 断言某字面量在 workflow 里出现过, 于是把 `[[ =~ ]]` 操作数放宽成 `^v.+$` 后四条仍全绿。现在直接断言操作数本身。`capability-glossary` 补上同样的第二条臂。
19
+ - 删掉这些断言会是净损失 —— 上一轮的「删守卫」臂仍然会红 (2 / 4 failed), 现在两种形态都覆盖。只有 rotation 守卫的第 4 个分支**真的不可判定**, 三种方式确认 (结构上: 它的条件蕴含它所守卫分支的否定)。
20
+ - `authkey` / `authkeys` 加入敏感标题词表。此前一次同类新增**零覆盖** (`grep tests/` 什么都找不到, 删掉它不会弄红任何测试) —— 正是本组存在的理由。现有 3 个正控 + 1 个负控 (`authkeyboard layout`), 把匹配器放宽成子串语义时会红 (实测放宽后 6 条变红)。
21
+
22
+ 5. **记忆标题的假拒绝 (C0).** `peaks memory extract` 拒绝标题「Derive from the auth-ority, never re-declare it」, 报 `Refusing to store sensitive memory content`, 补救文案写「remove secrets」。没有 secret。根因: `assertSafeMemory` 的第三项检查 `isSensitiveConfigPath(memory.title)` 是给配置**键**写的谓词, 做子串匹配 —— `authority` 命中 `auth`、`tokenizer` 命中 `token`、`secretary` 命中 `secret`。用在散文上就拒绝散文, 并把读者引去寻找一个不存在的东西。新增为散文而建的 `findSensitiveMemoryTitleTerm` (词-run 匹配、camelCase 感知、返回命中词)。标题检查**保留而非删除**: `hasSensitiveMemoryContent` 锚在 `:` / `=` 上, 裸 `apiKey` 标题没有别的检查能抓到 (实测确认)。`UnsafeMemoryError { check, matchedTerm }` 让每项检查报出自己是谁, 补救文案按 `error.check` 路由而不是按消息文本; `hasSensitiveMemoryContent` 逐字节未变 (671 B, 两侧同 sha)。附带修掉的第二个缺陷: `fail()` 会把每条失败消息过一遍 `redactSensitiveErrorMessage`, 其兜底正则把消息**存在意义**要报告的词改写掉, 产出 `an [redacted] / [redacted] / [redacted] assignment` —— 现在命中词走结构化 `data.matchedTerm` 字段, 散文避开目标词, 脱敏器本身未动。
23
+
24
+ 6. **`gateEvidence` 从死表面变成端到端可达 (B).** 它此前被四种互不兼容的方式描述: reader 解析 `string[]`; 它自己的 header 注释把字符串称作「gate names」并声称 `initHandoff` 会写它; 测试用路径数组并承认 `initHandoff` 不接受这个字段; schema 参考规定的是五键到路径的 map 并说「缺键 → Gate C failure」; 而 Gate C 从不读它 —— `grep -rn gateEvidence src/` 只命中一个文件。现在它是**派生而非传入**的五键 map, 每个生产者都写、Gate C 校验: 新增 `deriveGateEvidence` 读 `getPrerequisitesFor('rd', 'qa-handoff', type)` (与闸门**同一张表**, 所以无法漂移); 三个生产者都派生并写入; **没有 CLI flag** —— 让用户传入等于要求他手写 JSON, 本项目禁止; Gate C 校验声明的路径存在, 并在 `PREREQUISITES_MISSING` 里点名出错的键。reader 变成 7 态判别联合 (旧的把「什么都没声明」和「声明了但损坏」塌缩成同一个 `null`)。**第一轮交付时测试全绿却完全不可达** —— 三个写入方什么都没传, 该字段只有测试能碰到; QA 跑真 CLI 读文件才发现。上面那条验收, 就是把那次失败变成一条检查。
25
+
26
+ 7. **文档迁移与 manifest 守卫 (E1–E2).** `docs/` 里混着两类东西: superpowers 计划/规格散文 (属于那里) 和四份 peaks-loop 自有文档 (不属于)。四份用 `git mv` 迁到新的 `.peaks/docs/`, 全部记录为 100% rename 所以历史保留; 七个活引用同步更新。`CHANGELOG.md` 与 `.peaks/memory/` 有意不动 —— 那是「某事发生过」的记录, 不是指向它的指针。随后补上**刚被这次迁移证明的缺口**: 没有任何东西读 `package.json#files`, 一个 typo 能让 63 个测试保持绿色而包发出去时少了这个文件 (E1 就是证明 —— 它移动了一个已发布的契约, 而包还带着它, 唯一原因是那一行 `files` 被手工改了)。新守卫 `tests/unit/publish/files-entries-resolve.test.ts` **先对 24 个条目分类再写谓词**: 13 个具体文件必须存在; 9 个 glob 只按字面前缀目录判定、**从不**按文件存在性或匹配数 (`skills/**` 永远有匹配而 `schemas/*.json` 可能合法为空, 没有东西能区分它们); 2 个否定不判定; gitignored 生成物按**规则**豁免而非按名字。
27
+
28
+ 8. **死模块获得首个调用者, 已发布契约换址 (F1–F3), 以及一次差点自相抵消的修复 (A1).** `reviewer-dispatch-policy.ts` 13/13 导出在 865 个文件里零外部引用; 读它时翻出三处文档/代码不一致, 其中一处正是它死掉的原因 —— 它的 header 自称「consumed by the LLM-side runner」, 而 LLM 读不了 TypeScript 导出。接线接受 legacy reviewer 并**改道 + 警告**而不是拒绝, 这是证据允许的唯一选项: 前置侧已通过 `legacyRelativePaths` 接受那些槽位, 模块正文三次写「instead of failing the gate」, 而 `f17aa377` 删掉的 12-case 测试 (从 `f17aa377^` 取回原文) 逐字写着「reroute ... rather than fail」。**行为变更如实陈述**: 只有 `security-reviewer` 与 `perf-baseline-reviewer` 会被改道; `ok`、record 与 toolCall 不变, 新增一条 `warnings[]` 改道通知; 其余角色逐字节相同 (QA 建立了 repeat-run 噪声基线 —— 每角色 4 个易变叶子 —— 并证明 10 角色全集里唯一的非噪声差异就是那两条 `warnings/0`)。本仓没有任何地方派发这两个角色, 所以已记录流程不受影响; 仓外与历史调用方此前一直在**静默地**跑一个不存在的 reviewer。契约 `test-style-contract.md` 从 `.peaks/docs/` 移到 `contracts/` —— 它要发布, 地址是 `node_modules/peaks-loop/...`, 不该待在语义为「本工具内部工作区」的点目录里。A1 关闭 4.0.52 留下的 `repair-index` 回滚缺口, 但**不是**用第一版的做法: 第一版让 `'force'` 先写修复再回滚自己 —— 配置原样还回去, `peaks codegraph status` 仍报缺口, exit 75 永不清除, 而 4.0.52 正是靠这条路径把 include gap 从 31 做到 0 的。已回退: 回滚现在是显式的操作员动作 `peaks codegraph config-restore` (exit 1 = 前置, 没有任何东西检查过回滚点; 77 = 回滚点检查了但 restore 没发生), 不再是修复内部的一步。
29
+
30
+ 9. **两个 A 组条目值得单独记一笔, 因为它们的结论都是「不做」.** A2 让引用守卫看见裸 `tests/...test.ts` (此前守卫只读反引号 span), 并给裸文件名一条**故意更窄**的规则 (`*.test.ts`、名字含短横、空白边界 —— 短横要求正是把 `README.md` / `CHANGELOG.md` 挡在外面的那道条件)。A3 则**证伪了它自己要做的事**: 请求要求让围栏代码块不再对守卫不可见, 实测表明围栏**本来就是透明的** (守卫按行切分、扫反引号 span, 从不读 fence), 而加上 fence-awareness 反而会**弄瞎**四条当前被正确判定的真实引用。照原样实现会发出一个「修好了一个从不存在的盲区」的 commit —— 与本仓反复出现的「守卫宣称自己有它没有的覆盖」互为镜像, 两者都是**对从未测量过的行为下断言**。A3 因此不改逻辑, 只记录测量。F4 修掉本会话自己引入的 J03 上限突破: 一个 `catch { return undefined }` 把仓库自己的 `catch-return-null` 棘轮从 41 推到 42 并弄红 `J03-problem-resolution-flow.test.ts`; 棘轮存在的理由正是这个形状 ——「catch 返回 null/undefined, 调用方分不清失败与成功」—— 而这次突破把「artifact 不可读」藏进了「没有证据要声明」。
31
+
32
+ **验证**: `pnpm build` 干净, `build-integrity: OK`; 单元套件 **301 files / 3371 passed / 3 skipped / 0 failed** (与 bump 前基线逐数字相同); `tsc` 保持 **142** 基线 (无新增); `peaks release precheck` 四层全 `ok` —— `rootVsShared` (root 4.0.54 == shared dist CLI_VERSION 4.0.54)、`tagCollision` (v4.0.54 不存在)、`changesetStaged` (无 staged *.md)、`workspaceLockstep` (shared 0.0.88 是干净 semver); 版本 bump 与 4.0.53 那次 (`098a3778`) 逐字同模式: root + `CLI_VERSION` + `RUNTIME_VERSION` + 4 个 workspace 包各自 patch +1。
33
+
34
+ **明确未验证的 (不当作已完成)**:
35
+ - F1 记录了**三项后续, 未做**: `--from-dag` 在 `validateRole` 之前就 return, 只检查 `role.length > 0`; 13 个导出里仍有 11 个零引用; 模块的 decision-table 那一半仍无任何 pin。
36
+ - A3 把 `docs/**` 的测量留给了后续 slice: 11 个文件 165 处 findings, 71 个被引路径里 62 个从未存在, 而 9 个确实存在且被删除的与 D1 认定的 9 条诊断发现一一对应。与 4.0.51 的 178/64 同量级; 先决定「计划文档是否属于这个守卫的文体」再动它。
37
+ - A1 的 `config-restore` 用了与 codegraph 家族其余动词**不同**的 `code` 取值, 是刻意的偏离, 代码内有论证并有 FOLLOW-UP 待收敛 —— 尚未收敛。
38
+ - C0 只从**这一个调用点**移除了 `isSensitiveConfigPath`; 它自己领域内共有的一处误报 (`context.windowTokens`) 被**记录而非修复**, 留在原处。
39
+ - `git tag v4.0.54` 不存在且 precheck 通过, 但本片**没有**打 tag、**没有** commit —— 发布路径 (commit → 推送 → CI → tag → canary → promote) 由编排方接管。
40
+
41
+ ## 4.0.53 — 2026-09-17 (三版本残留未验证清单的 source-of-truth 对账: 4.0.50/4.0.51/4.0.52 九条声明里 7 条 code 已实装, 1 条真 source dead surface 删除, 1 条仍真未做留口)
42
+
43
+ **Highlights**:
44
+
45
+ 1. **真实工程: 1 项真 source 改动 + 1 项新 reader + 6 项新测试文件 + CHANGELOG 对账 3 段。** 4.0.50/4.0.51/4.0.52 三个版本的 CHANGELOG "明确未验证的" 清单共 9 条声明; 按 peaks-loop 4.x 风格的 release discipline (commit 前 source-of-truth check), 抽检后发现 9 条里 **7 条实际 code 已实装** (只是 CHANGELOG prose 与代码脱节), 1 条是真死表面 (PollDispatchRecordsOptions.ide), 1 条仍是真未做 (A2 `repair-index` `--reindex` 不回滚 cfg) 留口给后续 slice。
46
+
47
+ 2. **4.0.52 残留对账 (commit `93b488ee`)** — 4.0.52 CHANGELOG line 21 段原始措辞与 `55032546` 实际落地的代码不一致。**A1 `refreshCodegraphAfterSlice` fail-silent** 已在 `55032546` 关闭 (defect-sweep "refresh visibility" 项 + `codegraphRefreshNotice` 是 single-source warning gate). **A3 `codegraph.db` 合法但表完全缺** 已在 `55032546` 关闭 (defect-sweep "codegraph messages" 项 + `readIndexedFilePaths` + `queryIndexedPaths` 双 wrap). **A2 `repair-index` 回滚契约** 仍是唯一真未做的, `codegraph-commands.ts:134` 的 spec 已接受 `reindex: boolean | 'force'` 作为未来扩展点。
48
+
49
+ 3. **4.0.51 49 处点名 9 项 closure (commit `7191140f`, 7 文件 +643)** — 7 项覆盖缺口里 6 项实装:
50
+ - **AC-1**: `tests/unit/rd/reviewer-dispatch-policy.test.ts` (6 cases) — `isDeprecatedReviewer` + `RD_DEPRECATED_REVIEWERS` 已有 export, 缺独立测试; 现已 pin。
51
+ - **AC-2**: 2 cases 加到 `audit-artifact-rid-scoping.test.ts` — THIRD_PARTY_REVIEW backCompat 对称 MUT_REPORT 已存在的 case。
52
+ - **AC-3**: `tests/unit/skills/g11-5-title-lock.test.ts` (5 cases) — G11.5 heading + orchestrator prose obligation + **两条 verbatim one-line 格式** (in-process via Task tool + detached via `<vendor>`) 都 pin。
53
+ - **AC-4**: `tests/unit/slice/llm-arbitrator.test.ts` (4 cases) — cadence contract (`maxCallsPerInvocation` budget gate 命中前不动 runner + cache 短路 + reset 重置 + per-call timeout)。
54
+ - **AC-5**: **真 source 改动** — `src/services/prd/handoff-gate-evidence.ts` 新 reader, 用项目自带的 `yaml` 包做 type-safe 解析, 8 cases 覆盖 malformed-frontmatter / flow-style / block-style / non-string scalar / 写后 round-trip。frontmatter `gateEvidence` 字段从 prose 升级为可读 data。
55
+ - **AC-6**: `tests/unit/skills/karpathy-injection.test.ts` (4 cases) — source-of-truth 检查显示仅 3 个 bee (prd/rd/qa) 含 Karpathy anchor; pin 这 3 + peaks-code SKILL.md canonical id 引用。
56
+ - **AC-7/AC-8/AC-9** deferred 到后续 slice (per user direction): `peaks-ide audit-log` assistant + `citation-integrity` 守卫扩正则 (裸文件名 + 围栏代码块)。
57
+
58
+ 4. **4.0.50 反漂移闸门 closure (commit `0cfd41b5`, 2 文件 +5/-5)** — 3 项里 1 项真 source dead surface 删除 + 2 项 CHANGELOG 修订:
59
+ - **C1 J10 `HOOK_PERMISSION_DENIED`**: `src/services/capability-guard-runner/contracts/J10.ts:34` 真跑 (registry.ts:48 注册 + 集成测试 + runner.test.ts:77 列入), HOOK_PERMISSION_DENIED 是 4.0.8 baseline 里的 case name, **不是 "从未实现"**。唯一真未修: 4.0.8 baseline 仍 frozen, publish gate 报 consistent 但指向 frozen 行。
60
+ - **C2 J08 invariant 措辞**: source 跑 **7 个独立 probe** (schemaAcceptsReady / schemaRejectsGatesFalse / schemaRejectsNotCompleted / schemaRejectsNoEvidence / serviceAcceptsReady / refused.length === 3 / missing.length === 0), **不是 CHANGELOG 写的"塌缩成一个布尔"**。
61
+ - **C4 `PollDispatchRecordsOptions.ide` 真死参**: 删 interface 字段 + 3 caller 的 `ide:` 赋值。`pollDispatchRecords` body (line 416+) 不读 `opts.ide`, 死表面消除, -4 行。
62
+
63
+ **对账纪律 (本次 release 唯一的元发现)**: 三版本 CHANGELOG "未验证清单" 普遍存在 prose/code drift — CHANGELOG 把已实装的项目列为未做. 这是 release-discipline 缺陷不是 source 缺陷; 修复 = 改 CHANGELOG 而非伪装修复. 按 peaks-loop 4.x 风格, 任何 prose 修订必须 (1) 引用 source 行号让 prose 可验证, (2) 真未做的项目如实留口不伪装. 本片 9 项里 **8 项落实** (7 prose + 1 真 source), **1 项 (A2) 留口** 给后续 slice.
64
+
65
+ **验证**: 295/295 test files, 3192 tests pass + 3 skipped; tsc 142 errors (pre-existing baseline; 无新增); 三个 commit (93b488ee / 7191140f / 0cfd41b5) 全部 SquabbyZ sole-author, 无 AI trailer; CHANGELOG 修订带 source line references.
66
+
67
+ **明确未验证的 (不当作已完成)**:
68
+ - A2 (`peaks codegraph repair-index --reindex` 不回滚 cfg) — 真未做, 未来扩展点 `codegraph-commands.ts:134` 的 `reindex: 'force'` 已留口
69
+ - 4.0.51 7 项里的 AC-7 (peaks-ide audit-log) / AC-8 (守卫裸文件名) / AC-9 (守卫围栏) — 三项 deferred 到后续 slice
70
+
3
71
  ## 4.0.52 — 2026-09-17 (codegraph 索引完整性检测闸门 + repair-index 强制重建 + 套用到本仓:include gap 31→0、dead rows 4→0、strict-mode exit 75→0)
4
72
 
5
73
  **Highlights**:
@@ -18,7 +86,11 @@
18
86
 
19
87
  **验证**:290/290 unit tests、`tsc` 142、`lint` 6/6、`peaks codegraph status` 在 strict 模式下本仓 `consistent`、`codegraph.db` 行 1224 + 0 dead rows、`config.json` 与 `codegraph.db` 的 sha256 在 repair 前后取两次确认未引入新漂移。
20
88
 
21
- **明确未验证的(不当作已完成)**:`peaks codegraph repair-index` 的回滚契约 —— 现实现是 `--reindex`(不动 cfg 已写行 + 不回滚 cfg),与用户预期的"回滚到修前"有距离,本片仅命名 + 留口,未做;`refreshCodegraphAfterSlice` 仍是 fail-silent —— 本片不修,因为改它需要同时改两处调用点的契约(`job-commands.ts:306` / `request-commands.ts:468`),超出本版范围;slice-001 的 `indexIntegrity === null` 路径现在**不再静默**,但若上游 `codegraph.db` 合法但表完全缺(比 `no such table: files` 更罕见),错误消息的精细度还有一格没补。
89
+ **对账(2026-09-17 修订)**:本段原始措辞与同 commit `55032546` 实际落地的代码不一致,**三处披露里有两处已被本片一并修复却仍被列为未修**,这是 CHANGELOG/代码的 prose drift,不是 source 缺陷。
90
+
91
+ - ✅ **A1 `refreshCodegraphAfterSlice` fail-silent** —— 已在 `55032546` 关闭(defect-sweep "refresh visibility" 项)。`codegraph-autorefresh.ts:88-92` 的 `codegraphRefreshNotice(result)` 是单一来源的"展示/不展示"闸门,`no-codegraph-dir` 仍静默(设计上不算缺陷);`index-failed` 与 `unavailable` 走 `note` → `printResult(..., [codegraphWarning])` 输出到 stderr 的 `warning:` 行。`job-commands.ts:377-382` + `request-commands.ts:471-481` 两处 caller 已接;故意不抬 `process.exitCode`("advisory stays 0",被 `tests/unit/cli/job-exit-code.test.ts` 钉住)。
92
+ - ✅ **A3 `codegraph.db` 合法但表完全缺** —— 已在 `55032546` 关闭(defect-sweep "codegraph messages" 项)。`codegraph-index-integrity.ts:382-420` 的 `readIndexedFilePaths` 包住 `new Database(...)` 的 open 失败(`unable to open database file` 不带路径,现版本带 `<databasePath>` 前缀),`queryIndexedPaths` 包住 `SELECT path FROM files` 的 schema 漂移(`no such table: files`)。`codegraph-status-command.ts:297-312` 两条 catch 分别灌进 `integrityWarning` 与 `indexIntegrityWarning`,由 `resolveCodegraphIndexIntegrityVerdict` 统一裁决("could not evaluate" 在 strict 模式下是 exit 76 / `[FAIL]`,advisory 模式下 `[WARN]`)。
93
+ - ⚠️ **A2 `peaks codegraph repair-index` 的回滚契约** —— **唯一仍然真正未做的**。现 `--reindex` 不动 cfg 已写行 + 不回滚 cfg,与用户预期的"回滚到修前"有距离,本片仅命名 + 留口。**未来扩展点**:`codegraph-commands.ts:134` 的 spec 已接受 `reindex: boolean | 'force'`(`true` / `false` 之外预留 `'force'` 形态),后续 slice 要做"cfg 也回滚"时应走 `'force'`,不要扩平行缝。
22
94
 
23
95
  ## 4.0.51 — 2026-09-16 (子代理 await 永远报不出结果 + 49 处"声称有测试守护、测试早已删除" + 9 个恒真断言 + 三个不可达导出)
24
96
 
@@ -28,6 +100,15 @@
28
100
 
29
101
  2. **49 处在文档里声称"由某个测试守护",而那些测试早就被删了 —— 包括守卫自己文件头里写的那次提交删掉的那个。** 引用完整性守卫原先的语料是 `CLAUDE.md` / `.peaks/PROJECT.md` / `README.md` / `.peaks/standards` —— **不含 `skills/**`**,而它的锚点正则里明明列着 `skills/`。把 `skills/**` 加进语料后,**守卫自己吐出 178 处悬空引用 / 64 文件**(此前临时扫描器估的 95 **低了近一倍**,且它归因的 `de0872b7` 只占 11 处)。修了 93 处后剩 85 处,**全部逐条处置、零白名单化**:其中 **43 处引用 21 个测试** —— 1 个删于 `457b9a87`(**正是守卫文件头自称"它存在的理由"的那次提交**)、20 个删于 `f17aa377`(*"delete 559 legacy unit tests"*)、**2 个从未在任何 commit 里存在过**;另有 6 处悬空文档互引。**每一条都逐站改写,不做批量正则替换**("由 X 守护"与"见 X"语义不同)。**残留如实披露而非关闭**:这 49 站现在如实说明"无物钉住它",并逐个点名了真实的覆盖缺口 —— `isDeprecatedReviewer` / `RD_DEPRECATED_REVIEWERS`、prereq 向后兼容形态、G11.5 标题锁、20-次调用节奏、handoff frontmatter(`gateEvidence` 在 `src/` 里**无人读取**)、superpowers 四条款规则、`peaks-ide` 审计日志助手。**未补测试(超出本片范围),也未放宽或删除任何一个。**
30
102
 
103
+ **对账(2026-09-18 修订)**:本段原始措辞有三处与事实不符。**原文逐字不动**,此处逐条记录实测 —— 这是 CHANGELOG/事实的 prose drift,不是 source 缺陷。
104
+
105
+ - ⚠️ **「49」是一个混合单位的和。** **43 处测试引用站复现** ✅(D1 按守卫自己的排除规则数得 43;同一批语料在更松的读法下是 **45 行 / 26 个去重目标** —— 数字随规则变,这正是口径必须写下来的理由)。但**「21 个被删测试」不复现,实为 23**:**本段自己的分解 `1 + 20 + 2` 加起来就是 23**,同一句话里自相矛盾(`tests/unit/skills/skills-skill-md-naming.test.ts` 删于 `457b9a87`、20 个删于 `f17aa377`、另 2 个见下条)。**「6 处悬空文档互引」也不复现:实测 12 站**(D1 报 8 个去重目标,按其表逐行列应为 9 —— 该项随排除规则变,D1 自己把这一行标为"存疑"而非确证)。所以 **49 = 43 个「站」+ 6 个「目标」**,两个单位相加;按**站**算,下界是 **≥55**。
106
+ - ⚠️ **「2 个从未存在」的两个标签装反了。** `skills/peaks-code/references/external-skill-invocation.md:99` 现写 "the bridge guard this paragraph used to name **never existed in this repository**" —— 假:`tests/unit/skills/peaks-code-superpowers-bridge.test.ts` 由 `3c82c797` 加入、由 `f17aa377` 删除。`skills/peaks-code/references/fanout-mandatory.md:100` 现写 "...was **deleted in `f17aa377`**" —— 假:该路径在**一切仍存活的对象**里缺席;它是 `b1dbc51d` 那次文档改名的产物(同一次改名把 `tests/unit/solo/skills-solo-fanout-mandatory.test.ts` 移到了 `tests/unit/code/skills-fanout-mandatory.test.ts`,而文档里被写成 `tests/unit/code/skills-code-fanout-mandatory.test.ts`)。**数目 2 对,成员错一对。** 验法(全称否定只能穷举):`git log --all --diff-filter=A -- <path>` → `git rev-list --all --objects` → `git fsck --unreachable`(**17 commit / 44 tree**)逐个 `git ls-tree -r`,三层皆 0。
107
+ - ⚠️ **同一错误还有第三处,在守卫自己的文件头里。** `tests/unit/standards/repo-citation-integrity.test.ts` 原 76-82 行称 `pre-rd-scan.test.ts` / `design-draft-confirm.test.ts` 是 "tests that no commit ever added" —— 假:两者都由 `d3a45d45` 加入、由 `f17aa377` 删除。**守卫在描述自己存在理由的那段文字里写了与事实不符的话。** 该处是**我方代码注释**(不是历史),已直接改对并点名 `d3a45d45`;同段其余事实性断言(`457b9a87` / `e6e35842` / `de0872b7` / `f17aa377` 四处归属、"两个反引号裸文件名"、"两处脚本 basename"、"四个 corpus 计数")逐条核过,无误。
108
+ - 🔒 **全称否定的限界(必须一起读)。** 「从未存在」只能在**本仓仍保留的对象**上建立 —— `backup-main-pre-rewrite` 标签说明**历史被重写过**,被 gc 剪除的对象**不可反驳**。诚实形式是「**在一切仍存活的对象中缺席**」,不是「从未存在」。本节此后不再下这个全称否定。
109
+
110
+ **口径(这一整片的存在理由,正是它此前没被写下来):**「一处站」= 规范语料(`CLAUDE.md` / `.peaks/PROJECT.md` / `README.md` / `.peaks/standards/**.md` / `skills/**.md`)里**一句断言某个仓库内路径承载 / 守护 / 演练了某种行为**的话,而那个被断言的路径**今天的树上不存在**。两个子型不可合并:**T1 悬空引用**(文本里仍写着那条路径 —— 守卫看得见)、**T2 披露**(文本已改写成「无物钉住它」—— **没有任何机器读它**)。4.0.51 把 T1 全量改写成 T2,所以今天的站**全是 T2**:**守卫是绿的,不能证明它们已关闭**(守卫只读路径,而路径已经被删掉了)。**站的单位 = 一处引用**(一个文件里的一处引用点;行号只是它的地址,不是它的身份 —— 少了这条规则,下面这个数就会随读法漂)。据此**逐行重数** `rid-d1-station-inventory` §四 的三张表 —— **该节从未写过分类汇总,那份汇总此前只出现在片内文档里;这里给的是从它的表数出来的数,不是它说的话**:分类列 **A 9 · B 6 · C 24 · D 9 = 48 行**。**但「行」不是「站」,两者不可相减** —— 那正是本段在修的那种单位混用,只是小了一号;所以这个数**只按「站」报一次**:§四 A 段自报 **33** 个 T2 站、B 段自报 **12** 站,C 段 3 行里**只有 C-1 是新站**,**C-2** 是对 B-1 的重新标注(D1 自己写「同 B-1」),**C-3** 是对 A-31 / A-32 的重新标注(D1 自己写「同上 A-31/32」;落盘可验:`scripts/sync-version.mjs:80` 今天**已不含任何测试引用**,而 `scripts/peaks-ide-audit-log.mjs:14` 与 A-32 的 `:10` 同属**一个注释块** —— 且 C-3 一行**跨两个文件**,按「站 = 一处引用点」的定义它是 **2 个引用点**,不是一个)。⇒ **33 + 12 + 1 = 46 个 distinct 站**,单一口径。按分类列读出的 **48 是同一张表的另一种分组**(按分类列 vs 按小节标题),**不是第二次独立枚举,两条路径不构成「交叉印证」** —— 它们同源,只是换了个切法;48 与 46 的差也不是「2 处重新标注」(那又是在把两种单位相减),而是 **2 行重新标注 / 3 个引用点**。另有 §五 gap table 的 **9 项**「已关闭」声明逐条核实,其中 **AC-1(空 pin)/ AC-3(半闭合)/ AC-4(错钉)/ AC-6(假命中)四条不成立**,由 D2 处置。
111
+
31
112
  3. **9 个 `supportsScope` 断言全部恒真,且唯一消费者是一个循环论证的测试。** 接口声明 `supportsScope(scope: 'project' | 'global')`,9 个 adapter 的实现是 1 个 `() => true` 加 8 个逐字相同、**在类型上必然为真**的表达式 —— 没有一个能返回 `false`。**生产零调用者**:全仓唯一消费者是一个把它"characterise"成两个 scope 都返回 true 的新测试。**删除**(接口 + 9 实现 + 那个测试)。理由:参数类型即值域、无 adapter 只支持单一 scope(9/9 `resolveSettingsFile` 双 scope 对称),且真实属性已被 `adapter-runtime-surfaces.test.ts` 钉住。
32
113
 
33
114
  4. **两个不可达导出与一处死参数。** `traeCnSubAgentDispatcher` 有导出、有测试、**没注册到任何 adapter**(8 个 adapter 分别注册了 claudeCode / codex / cursor / trae / null —— 且**没有 `trae-cn` adapter**,`IdeId` 里也没有)。它唯一可引的"支持"证据是 postinstall 的目录映射,**不是适配器注册** —— 给它建 adapter 等于**发明一条不存在的接线**,故删除。`awaitByLlmFallback` 生产零调用者(它的"向后兼容测试"只证明它自己存在 —— 与第 3 条同型的循环),且其注释里承诺的目标在 1.3 已兑现,故删除。另报出:`PollDispatchRecordsOptions.ide` **从未被 `pollDispatchRecords` 读取**(既有死表面,属三项目标之外,仅报出未修)。共 15 文件 **+61/−170**。
@@ -40,7 +121,11 @@
40
121
 
41
122
  1. **"散文比例 0%" 是一个重新定义了自己分母的指标。** `peaks audit red-lines` 报 `proseOnly: 0`,而**同一份 JSON 里 44 行**写着 `"backing": "prose-only"` —— `classifier.ts` 把所有未匹配的 marker 标为 `informational: true`,比例计算再把它**排除出分母**。同一批修复查出 `cli-backed` 的判定是 `existsSync(enforcerRef)`:**文件在磁盘上存在即算"有机器执行"**,于是 **9 个 enforcer**(共 51 行)从未被导入、从未被调用,却被计入机器执行。诚实重算,同一棵树:**cli-backed 105 → 54,prose-only 0 → 98(64.5%)**。指标变难看,是因为它开始度量该度量的东西。另外两个同族缺陷:`lint-rd-handoff-coverage` 检查的是 **SKILL.md 里描述产物的那句话**,从不读产物文件;`release-precheck` 的 AC-9 **把 bug 本身钉成了期望值**(`proseOnly === 0`),现在钉的是"摘要与逐行计数一致"。
42
123
 
43
- 2. **RL-10 指定的反漂移机制在自证,而它能自证是因为它只跑了一条。** `peaks baseline audit` 输出 `verdict: consistent`、`consistencyScore: 1`、证据写着 `"capability-guard-runner:15 — 15 pass / 0 fail"`。真相是 `guardSummary` **硬编码为 `{pass:15,fail:0}`**,LLM 打分环节是**返回常量**的桩;而 `run-guard` 的派发是 `opts.journey === 'J01' ? … : {status:'skipped'}` —— **不带参数时只跑 J01**,其余 14 条返回 `skipped` 且 **exit 0**,与"通过"在退出码上不可区分。15 个 guard contract 里 13 个是 `existsSync` + 常见词子串(J10 断言 `hooks-commands.ts` 含词 `hook`;J06 的注释书面承认把闸门改松以让它通过)。现在:15 条 contract 走一个 registry 全部真实执行,`skipped` 是 **exit 2**、`fail` 是 **exit 1**;`assertBaselineRef` 真的解析冻结行与**逐字不变量文本**,不再只检查字段为真值;`freeze-update` / `rollback` / `reset` 三件套有了可达的成功路径(此前它们无条件 `fail`,**棘轮动不了**)。**诚实否定**:J10 的 `HOOK_PERMISSION_DENIED` 在基线 JSON 里存在但从未实现 —— 没有把它编码成探针,因为那会造出一盏永久的红灯;J08 的不变量措辞夸大了四个 gate 实际塌缩成一个布尔。
124
+ 2. **RL-10 指定的反漂移机制在自证,而它能自证是因为它只跑了一条。** `peaks baseline audit` 输出 `verdict: consistent`、`consistencyScore: 1`、证据写着 `"capability-guard-runner:15 — 15 pass / 0 fail"`。真相是 `guardSummary` **硬编码为 `{pass:15,fail:0}`**,LLM 打分环节是**返回常量**的桩;而 `run-guard` 的派发是 `opts.journey === 'J01' ? … : {status:'skipped'}` —— **不带参数时只跑 J01**,其余 14 条返回 `skipped` 且 **exit 0**,与"通过"在退出码上不可区分。15 个 guard contract 里 13 个是 `existsSync` + 常见词子串(J10 断言 `hooks-commands.ts` 含词 `hook`;J06 的注释书面承认把闸门改松以让它通过)。现在:15 条 contract 走一个 registry 全部真实执行,`skipped` 是 **exit 2**、`fail` 是 **exit 1**;`assertBaselineRef` 真的解析冻结行与**逐字不变量文本**,不再只检查字段为真值;`freeze-update` / `rollback` / `reset` 三件套有了可达的成功路径(此前它们无条件 `fail`,**棘轮动不了**)。**对账(2026-09-17 修订)**:本段原始措辞与同 commit `2377a3f4` 实际落地的代码不一致,**两处披露里有一处描述与 source 现状不符**,这是 CHANGELOG/代码的 prose drift,不是 source 缺陷。
125
+
126
+ - ✅ **C1 J10 的 `HOOK_PERMISSION_DENIED`** —— 已在 `2377a3f4` 关闭(defect-sweep "capability-guard-runner" 项 + 4.0.8 baseline 输入)。`src/services/capability-guard-runner/contracts/J10.ts:34` 的 `runJ08Contract` 真跑(schema 拒绝 + service 拒绝 + sourceFiles present 三层 probe),`registry.ts:48` 注册 + `tests/integration/capability-guard/J10-ide-install-assertion.test.ts` 集成测试 + `runner.test.ts:77` 列入 15-journey 列表。`HOOK_PERMISSION_DENIED` 是 J10 在 4.0.8 baseline input 里命名的一条 case(`openspec/baselines/inputs/4.0.8-baseline-input.json:285`),contract 真跑会断言它。**唯一仍然真正"未做"的事**:4.0.8 baseline 当前 frozen,未随 4.0.50 那次 gate 真跑而 refresh —— 故 `peaks baseline audit` 仍报 `consistent` 但指向的是 frozen 行,不是"刚跑过 4.0.50 的真状态"。
127
+ - ✅ **C2 J08 的不变量措辞** —— 已在 `2377a3f4` 关闭(**已不是"塌缩成一个布尔"**)。`src/services/capability-guard-runner/contracts/J08.ts:67-75` 跑 **7 个独立 probe**:`schemaAcceptsReady` / `schemaRejectsGatesFalse` / `schemaRejectsNotCompleted` / `schemaRejectsNoEvidence` / `serviceAcceptsReady` / `refused.length === 3` / `missing.length === 0`。CHANGELOG 原文措辞错误——实际是 7 个独立 probe,**不是 1 个布尔**。J08 invariant 4.0.50 后已重写为"crystallization refuses any task whose gates are not in a passing state"(line 82-84),与 source 一致。
128
+ - ⚠️ **C4 `PollDispatchRecordsOptions.ide` 死参** —— **唯一仍然真正未做的**。`src/services/dispatch/sub-agent-dispatcher.ts` 原 410-414 定义 `ide: 'trae' | 'codex' | 'cursor'`,`pollDispatchRecords` body(416+)**不读 `opts.ide`**。4.0.51 那次 cleanup 命名 + 报出,未修。本片 4.0.55 修。
44
129
 
45
130
  3. **发布闸门从"只拒一个字面量"改成白名单,于是它立刻挡住了发布。** `publish.yml` 原本只在 `verdict` **精确等于 `"drifted"`** 时失败 —— `partial` / `inconclusive` / 任何别的值都会放行。改成白名单(仅 `consistent` 放行)之后,上面那条伪造的 15/15 不再能放行任何东西,**发布被挡住了**。这是刻意的、也是被接受的:闸门不再是摆设,代价是它开始真的挡。解开它的不是放宽,而是接上一个**能失败的确定性独立检查器**(见第 4 条)。同一轮里 `describeMode()` 被发现把自己描述成 `0.85/0.95`,**漏掉真正生效的 0.80 档**;往下挖还查出一条真实分叉:`peaks skill presence`(每轮都调)在 **≥0.80** 触发 `auto-fire`,而 `peaks code context-now` —— SKILL.md 自己声明的 single source of truth —— **硬编码了它自己的 0.85**。只改描述,**没碰阈值和行为**。
46
131
 
@@ -102,6 +187,8 @@
102
187
 
103
188
  **本轮顺带发现、尚未处理(不属本版修复)**:13 个守卫点位里 10 个没有实测控制组;17 个带 id 的文件既未分类也未点名;约 60 处 binding/env 拼接;31 个未追踪的 CLI 文件上仍有同一种路径逃逸形状;以及最刺眼的一条 —— **扫描清单自己的后续指令仍然排除了这一轮查出最严重缺陷的那个文件**。
104
189
 
190
+ **对账记录(2026-09-18,C2 片 `rid-c2-mutation-control-audit`;经 C3 片 `rid-c3-fixes` 修正;source: 本文件 `CHANGELOG.md:141` 的"13 个守卫点位里 10 个没有实测控制组")**——**分母是文件名计数,不是守卫计数;结论方向也相反**。复现:`git ls-files 'tests/**/*.test.ts' | grep -i guard` 得 **31**,减去 `tests/integration/capability-guard/J01–J15`(15)与 `tests/unit/capability-guard-runner/{diff,runner,types}`(3)**恰为 13** —— 按文件名匹配的已知漏法是"守卫名里没有 `guard` 字样者全漏",而 `no-ai-co-author-trailer` / `repo-citation-integrity` / `home-directory-is-not-a-project-root`(C1 自行补出)、`publish-tag-strict` / `multi-binary-drift-check` / `superpowers-deny-list` / `ecc-hooks-schema-drift`(QA 补出)、`loop-engineering-guidelines` / `capability-glossary`(C1 标 `inconclusive` 者)**这 9 个文件名的 `guard` 匹配数实测全为 0**。按先写定的性质判据(断言生产代码或已提交产物对某类输入作出拒绝/放行判定,且违反即缺陷)枚举并**逐点做变异控制组实测**(先预测 → 最小改动删掉/改坏守卫核心判断 → scoped 跑 → 逐次还原、sha256 校验字节一致)。**修正后的分母(C3)**:审计闭集 **22** + 独立检索另找出的同类守卫 **10**(`lockstep-three-packages`、`cli-version-lockstep`、`compact-swallow-invariant`、`command-option-invariants`、`cross-version-isolation`、`settings-local-preserves-user-keys`、`settings-local-hooks-entry-ownership`、`integration-cli-helper-envelope-parity`、`active-skill-json-cleanup`、`no-magic-numbers-config`)+ QA 指出、C4 按**断言粒度**重筛后收进的**键内漏项 13**(H5 的筛子原先是**文件粒度**,把「文件里用过 artifact」整个排除;C4 改为**断言粒度** —— 判据是「该断言的主语是不是已提交产物本身」,而不是「文件里有没有 readFileSync」。据此补收 `tests/unit/services/prd/project-scan-bootstrap-service`(读已提交的 `skills/peaks-code/references/*.md` 并断言产物本身的 `frontendOnly`↔`## Archetype` 绑定)与 `tests/unit/skills/loop-hygiene-block`(读 `skills/**/SKILL.md`、断言每个技能都带 loop-hygiene 块且副本逐字节相同)。按同一判据复核后其余 13 个 H5 命中**仍不收**:它们读的是 tmp/生成产物(`mkdtempSync` 出来的 `.peaks/config.json`、`.codegraph/config.json`、session `_runtime`、memory 目录),不是版本控制里的产物。原 11 个:`doctor/codegraph-index-integrity`、`services/codegraph/codegraph-index-integrity`、`doctor/codegraph-exclude-integrity`、`services/codegraph/codegraph-exclude-integrity`、`cli/codegraph-status-integrity`、`cli/codegraph-status-index-integrity`、`cli/skill-presence-generated-config-drift`、`doctor/l3-memory-health-drift`、`services/workflow/pipeline-verify-contract-drift`,以及 H5 命中的 `tests/lint/readme-downstream-consumer-notes`、`skills/g11-5-title-lock`;唯一排除项是 `capability-guard/J10`,属 request 已划出射程的 capability-guard J 家族),**总数 45,判据相关**。**修正后的分类(C3)**:原 C2 表把 `vitest-concurrency-guard` / `publish-tag-strict` / `capability-glossary` 记为"假控制组",是**只跑了一个变异就贴标签**;C3 补齐第二臂后三条**都变红**(注释掉 `maxWorkers` 行 → 1 failed;把 `[[ =~ ]]` 放宽成 `^v.+$` → 1 failed;清空 `FORBIDDEN_PATTERN` → 1 failed),所以三条的真实形态是**"抓得住删除、抓不住削弱"的活检查**,与第 4 守卫(怎么变都不红)不同类。修正后:**真控制组 21**(18 条含"削弱"臂;`vitest-concurrency-guard` / `publish-tag-strict` / `capability-glossary` 三条原有断言**一条未删**,只补了"削弱"臂,补后三条的削弱变异分别 1 / 1 / 1 failed)、**不可判别 1**(`rotation-guards-tightening` 第 4 守卫,条件改恒假后 11 条全绿)。**原文"10/13 没有控制组"实测为 21/22 有,方向相反。** 分母的早期数字曾**系统性 +5**:harness 的 argv 里混入一个字面量 `node`,被 vitest 当成第二个 filter,额外命中 `tests/unit/services/workflow/provision-dispatch-node.test.ts`(5 条);修正后每个分母**逐条可复现**(单文件跑,合计与 21 文件批量控制组 `235 passed` 逐字相等(**C2 当时是 232**;C3 为三条「抓得住删除、抓不住削弱」的守卫各补一条「削弱」臂,三个文件分别 6→7 / 4→5 / 1→2,故 +3 = 235。改的是本对账段,不是 4.0.x 原文))。**真未做、如实留口**:45 里只有 22 个做过变异实测;上述 10 个同类守卫与 13 个键内漏项**只枚举、未变异**;`scripts/**`、`packages/*/tests/**`、`.github/workflows/ci.yml` 这类脚本/CI 形态的守卫**未枚举**(本片五种检索全键在 `tests/**` 之下)。**C0 残留处置**:`atomic-write.ts` 的 `SENSITIVE_PROSE_TERMS` 补入 `authkey`/`authkeys`(补前实测 `authkey`/`auth-key` 均被接受,与 C0 技术文档 §4 不符 —— 判定为**词表漏项而非文档写错**),且 C3 已在 `memory-title-sensitive-scan.test.ts` 补正/负对照把它钉住(补前 `grep tests/ authkey` 命中 0,删掉不会有任何一条红;补后删该词表项 → 3 failed);`assertSafeMemory` 的 `metadataKey` 分支——**结论(对已发布形状不可达、零覆盖)成立但 C2 的机理描述错误**:`containsSensitiveConfigValue` **会递归进值**(实测 `{…body:{token:'x'}}` → `true`、`{…body:[{secretKey:'x'}]}` → `true`),所以只对 **string 输入**不可达,不是"按构造不可达";且 `assertSafeMemory` 是 **public export**(`src/services/memory/project-memory-service/index.ts:100` 再导出),删它**会**让 JS 调用方的嵌套输入失去检测。**故不删**,机理已改对,见 `.peaks/_runtime/2026-09-17-session-607ead/rd/tech-doc-rid-c3-fixes.md`。
191
+
105
192
  ## 4.0.47 — 2026-09-13 (90 个文件第一次进 CI 就绿了 + 弹窗的根在守卫射程 + 说"能做"而做不到的地方)
106
193
 
107
194
  **Highlights**:
package/README-en.md CHANGED
@@ -140,7 +140,7 @@ Every lane opens with **one slash command**.
140
140
 
141
141
  | | |
142
142
  | --- | --- |
143
- | **Latest** | [![npm](https://img.shields.io/npm/v/peaks-loop?style=for-the-badge&logo=npm&logoColor=white&color=cb3837)](https://www.npmjs.com/package/peaks-loop) — 4.0.52 (2026-09-17) |
143
+ | **Latest** | [![npm](https://img.shields.io/npm/v/peaks-loop?style=for-the-badge&logo=npm&logoColor=white&color=cb3837)](https://www.npmjs.com/package/peaks-loop) — 4.0.54 (2026-09-18) |
144
144
  | **Domains** | Code (`peaks-code`) · Content (`peaks-content`) · Project health (`peaks-doctor`) · Issue sweep (`peaks-issue-fix-orchestrator`) · Custom SOP (`peaks-sop`) · Cross-domain primitives (`peaks-solo` dispatcher · `peaks-resume` · `peaks-status` · `peaks-test` · `peaks-slice-decompose`) |
145
145
  | **Sediment pool** | `~/.peaks/` local pool · twice-clean runs auto-promote to a bee · broken runs come back for you to redefine · the bee grows with your taste |
146
146
  | **Test suite** | 285+ cases · 4 packages (peaks-loop / peaks-loop-mut / peaks-loop-shared-channel / peaks-loop-shared) · **0 timeouts** · 14 BDD caller-binding edge cases |
package/README.md CHANGED
@@ -140,7 +140,7 @@ npm i -g peaks-loop
140
140
 
141
141
  | | |
142
142
  | --- | --- |
143
- | **最新版本** | [![npm](https://img.shields.io/npm/v/peaks-loop?style=for-the-badge&logo=npm&logoColor=white&color=cb3837)](https://www.npmjs.com/package/peaks-loop) — 4.0.52(2026-09-17) |
143
+ | **最新版本** | [![npm](https://img.shields.io/npm/v/peaks-loop?style=for-the-badge&logo=npm&logoColor=white&color=cb3837)](https://www.npmjs.com/package/peaks-loop) — 4.0.54(2026-09-18) |
144
144
  | **覆盖域** | 代码(`peaks-code`) · 内容(`peaks-content`) · 项目健康(`peaks-doctor`) · 批量修 issue(`peaks-issue-fix-orchestrator`) · 自定义 SOP(`peaks-sop`) · 通用原语(`peaks-solo` 分诊 / `peaks-resume` 续 / `peaks-status` 看 / `peaks-test` 测 / `peaks-slice-decompose` 切片) |
145
145
  | **沉淀池** | `~/.peaks/` 本地池 · 跑两次自动晋升成 bee · 跑翻车让你重定义 · bee 跟着你的口味长 |
146
146
  | **测试套件** | 1096 cases · 4 packages (peaks-loop 1015 / runtime 39 / mut 22 / shared-channel 20) · **CI 首次全绿**(ubuntu + windows) · 14 BDD caller-binding coverage |
@@ -1,8 +1,8 @@
1
1
  // src/cli/commands/codegraph-commands.ts
2
2
  //
3
3
  // The codegraph verbs that MUTATE or PROXY: `repair-exclude`, `repair-index`,
4
- // `init`, `affected`, plus the commander registration for every codegraph
5
- // subcommand.
4
+ // `config-restore`, `init`, `affected`, plus the commander registration for
5
+ // every codegraph subcommand.
6
6
  //
7
7
  // D1 (rid 2026-09-17-oversize-and-scale, the 800-line file-size cap) moved
8
8
  // the shared invocation runtime to `codegraph-command-runtime.ts` and the
@@ -12,11 +12,13 @@
12
12
  // re-exported below, so every existing importer still resolves.
13
13
  import { InvalidArgumentError } from 'commander';
14
14
  import { statSync } from 'node:fs';
15
- import { resolve } from 'node:path';
16
- import { createCodegraphInvocation, executeCodegraphInvocation, defaultCodegraphInitGuard, resolveProjectRoot, writeCodegraphMarker, writeCodegraphAffectedContext, CodegraphInitConflictError } from '../../services/codegraph/codegraph-service.js';
15
+ import { join, resolve } from 'node:path';
16
+ import { assertCodegraphDirContained, createCodegraphInvocation, executeCodegraphInvocation, defaultCodegraphInitGuard, resolveProjectRoot, writeCodegraphMarker, writeCodegraphAffectedContext, CodegraphInitConflictError, CODEGRAPH_DIR_NAME } from '../../services/codegraph/codegraph-service.js';
17
17
  import { repairCodegraphExcludeFromProject } from '../../services/codegraph/codegraph-exclude-repair.js';
18
+ import { CODEGRAPH_CONFIG_FILENAME } from '../../services/codegraph/codegraph-exclude-reconciler.js';
19
+ import { rollbackCodegraphConfig } from '../../services/codegraph/codegraph-config-repair-writer.js';
18
20
  import { fail, ok } from 'peaks-loop-shared/result';
19
- import { printResult, redactSensitiveErrorMessage } from '../cli-helpers.js';
21
+ import { getErrorMessage, printResult, redactSensitiveErrorMessage } from '../cli-helpers.js';
20
22
  import { printCodegraphFailure, runCodegraphCommand } from './codegraph-command-runtime.js';
21
23
  import { runCodegraphStatusCommand } from './codegraph-status-command.js';
22
24
  // Re-exported so the D1 split is invisible to importers of THIS path.
@@ -158,6 +160,160 @@ async function runCodegraphRepairCommand(io, options, asJson, mode) {
158
160
  process.exitCode = 1;
159
161
  }
160
162
  }
163
+ /**
164
+ * Exit code for a `config-restore` run that never reached the restore point:
165
+ * `--project` is missing or not a directory, or the containment guard refused
166
+ * the `.codegraph/` directory.
167
+ *
168
+ * Deliberately NOT `CODEGRAPH_CONFIG_RESTORE_EXIT_CODE` (77). The two codes
169
+ * answer different questions, and a CI job has to be able to tell them apart:
170
+ *
171
+ * - 1 — the command was never aimed at a usable project. Nothing looked at a
172
+ * rollback point, so "your backup is unusable" would be a verdict
173
+ * nothing reached, and the remedy is a different one (fix the path, or
174
+ * remove the link). The VALUE is 1 because that is what every other
175
+ * codegraph verb already exits with for this same input failure —
176
+ * `repair-exclude --project <bad>` and `status --project <bad>` both
177
+ * exit 1 (measured), so a caller that handles one handles this one.
178
+ * - 77 — the restore point WAS examined and the restore did not happen.
179
+ *
180
+ * Named rather than left as `printCodegraphFailure`'s default `1` so both
181
+ * classes are visible in one place, side by side, instead of one of them living
182
+ * only in another module's parameter default.
183
+ */
184
+ const CODEGRAPH_CONFIG_RESTORE_PRECONDITION_EXIT_CODE = 1;
185
+ /**
186
+ * Exit code for `peaks codegraph config-restore` when the restore did not
187
+ * happen: the backup is absent, unreadable, or REFUSED (a symbolic link, a
188
+ * hard link or a directory occupies the fixed, guessable `.bak` path), or the
189
+ * reverse write itself failed.
190
+ *
191
+ * Its own code rather than the precondition `1`, for the same reason 73/74/75/76
192
+ * have theirs: it is an operator-actionable verdict about the RESTORE POINT, and
193
+ * a CI job has to be able to tell it apart from "the command was aimed wrong".
194
+ * The cause lives in `data.reason`, which names the path and the shape that was
195
+ * refused — a per-cause CODE would need the rollback helper to return a
196
+ * discriminated reason rather than a message, which is a change to that helper's
197
+ * contract for no operator-visible gain while the message already carries the
198
+ * details.
199
+ */
200
+ const CODEGRAPH_CONFIG_RESTORE_EXIT_CODE = 77;
201
+ /**
202
+ * The failure envelope for EVERY `config-restore` failure, so all four `data`
203
+ * keys are present whatever went wrong.
204
+ *
205
+ * A JSON consumer reads `data.restored` and `data.reason` unconditionally, so a
206
+ * failure envelope that omits them leaves it guessing — and this verb reports a
207
+ * `reason` instead of an exit code alone precisely so a failure is never silent.
208
+ * Built here rather than delegated to `printCodegraphFailure`, whose `data` is
209
+ * always `{}` and therefore cannot satisfy that.
210
+ *
211
+ * `code` names the VERB's failure for all three cases; the exit code names the
212
+ * CLASS (precondition vs restore) and `reason` names the cause. That split is
213
+ * deliberate: one `code` a consumer can match on, plus the two facts it needs
214
+ * to decide what to do next.
215
+ *
216
+ * The verb-specific `code` is an INTENTIONAL DIVERGENCE from every other
217
+ * codegraph verb, not a consistency with them — `repair-exclude`,
218
+ * `repair-index` and `status` all report `CODEGRAPH_COMMAND_FAILED` out of
219
+ * `printCodegraphFailure` with `data: {}` (measured, 2026-09-17). Two reasons
220
+ * to diverge here anyway:
221
+ *
222
+ * 1. A generic `code` cannot be paired with a verb-specific `reason`
223
+ * contract: this verb's failure ALWAYS carries `restored`/`reason`, which
224
+ * `printCodegraphFailure` cannot express at all (`fail()` hard-codes the
225
+ * other verbs' `data` to `{}`).
226
+ * 2. `CODEGRAPH_CONFIG_RESTORE_FAILED` says WHICH restore failed, and a
227
+ * consumer matching on `code` gets that from the envelope rather than from
228
+ * the exit code alone.
229
+ *
230
+ * FOLLOW-UP (recorded, not done here — it is out of this slice's scope):
231
+ * `code` naming across the codegraph verb family is now inconsistent, and the
232
+ * rest of the family should either adopt verb-specific codes or this one should
233
+ * return to the generic one. See this slice's RD artifact,
234
+ * `## A1 QA 修复循环 2`.
235
+ */
236
+ function printConfigRestoreFailure(io, asJson, reason, exitCode, nextActions) {
237
+ const redacted = redactSensitiveErrorMessage(reason);
238
+ printResult(io, fail('codegraph.config-restore', 'CODEGRAPH_CONFIG_RESTORE_FAILED', redacted, { restored: false, from: null, to: null, reason: redacted }, [...nextActions]), asJson);
239
+ process.exitCode = exitCode;
240
+ }
241
+ /**
242
+ * `peaks codegraph config-restore` — put `.codegraph/config.json` back to the
243
+ * bytes the last repair backed up to `.codegraph/config.json.bak`.
244
+ *
245
+ * The EXPLICIT undo of a repair, and the only way the `.bak` is ever read: the
246
+ * repair seams leave the copy behind and never consume it, because a repair
247
+ * that restored its own write would cancel itself out (the config would be
248
+ * exactly as it was found, `status` would still report the gap, and exit 75
249
+ * would never clear).
250
+ *
251
+ * Touches the CONFIG FILE ONLY — no `codegraph` subprocess is spawned, so this
252
+ * is safe to run while nothing else is rebuilding. The index still reflects
253
+ * whatever config the last rebuild used; the envelope's `nextActions` says so
254
+ * rather than silently reindexing.
255
+ */
256
+ async function runCodegraphConfigRestoreCommand(io, options, asJson) {
257
+ let configPath;
258
+ try {
259
+ // `resolveProjectRoot` for the same reason the repair verbs use it: the
260
+ // paths reported here must be the canonical ones, or a `--project` alias
261
+ // reports a restore that happened somewhere the operator cannot find.
262
+ const projectRoot = resolveProjectRoot(options.project);
263
+ // The containment refusal the repair seam makes before IT writes here. A
264
+ // `.codegraph/` that resolves outside the canonical project root (a junction
265
+ // or a symlink) would otherwise let this verb publish one project's bytes
266
+ // into another. Called for its refusal only — the paths below stay derived
267
+ // from the caller's canonical `projectRoot`.
268
+ assertCodegraphDirContained(projectRoot);
269
+ configPath = join(projectRoot, CODEGRAPH_DIR_NAME, CODEGRAPH_CONFIG_FILENAME);
270
+ }
271
+ catch (error) {
272
+ // The PRECONDITION class: nothing below this line ran, so the envelope says
273
+ // the restore did not happen and stops short of blaming the rollback point.
274
+ printConfigRestoreFailure(io, asJson, getErrorMessage(error), CODEGRAPH_CONFIG_RESTORE_PRECONDITION_EXIT_CODE, [
275
+ 'Check that `--project` names an existing directory whose `.codegraph/` resolves inside it.',
276
+ 'Run `peaks codegraph config-restore --project <root>` again once the path is right.'
277
+ ]);
278
+ return;
279
+ }
280
+ let result;
281
+ try {
282
+ result = await rollbackCodegraphConfig(configPath);
283
+ }
284
+ catch (error) {
285
+ // Only the reverse write's own fs failure reaches here (the refusals are
286
+ // returned, not thrown). The RESTORE class, and the same code as a refusal:
287
+ // the restore was attempted and did not happen, and a script that can read
288
+ // one can read the other. What differs is the `reason`, which is this
289
+ // error's own message.
290
+ printConfigRestoreFailure(io, asJson, getErrorMessage(error), CODEGRAPH_CONFIG_RESTORE_EXIT_CODE, [
291
+ 'Check that the `.codegraph/` directory is writable, then re-run.',
292
+ 'The config was NOT restored; its bytes are unchanged.'
293
+ ]);
294
+ return;
295
+ }
296
+ if (!result.rolledBack) {
297
+ // LOUD, never silent: the reason reaches the envelope's `message` (which
298
+ // `fail()` redacts) AND `data.reason`, and the exit code is this verb's
299
+ // own. A restore that quietly reported success over a `.bak` it refused to
300
+ // read is exactly the fail-silent family this release closed.
301
+ printConfigRestoreFailure(io, asJson, result.error, CODEGRAPH_CONFIG_RESTORE_EXIT_CODE, [
302
+ `There is no usable rollback point at ${result.backupPath}.`,
303
+ 'A restore needs the `.bak` that a previous `peaks codegraph repair-exclude` or `repair-index` left next to the config.'
304
+ ]);
305
+ return;
306
+ }
307
+ printResult(io, ok('codegraph.config-restore', {
308
+ restored: true,
309
+ from: result.backupPath,
310
+ to: result.configPath,
311
+ reason: null
312
+ }, [], [
313
+ 'Run `peaks codegraph status --project <root>` to see the restored config reflected in the integrity gate.',
314
+ 'Run `peaks codegraph index` if the index should reflect the restored config too — this verb does not rebuild it.'
315
+ ]), asJson);
316
+ }
161
317
  /**
162
318
  * rid-CG-006 — init conflict guard. Resolves the project root and
163
319
  * probes `.codegraph/` for the peaks-loop marker before invoking the
@@ -377,6 +533,9 @@ export function registerCodegraphCommands(program, io) {
377
533
  addProjectOption(codegraph
378
534
  .command('repair-index')
379
535
  .description('Repair both codegraph config axes, then rebuild the index from scratch (drops rows for deleted files)')).action((options) => runCodegraphRepairCommand(io, options, options.peaksJson, 'index'));
536
+ addProjectOption(codegraph
537
+ .command('config-restore')
538
+ .description('Restore .codegraph/config.json from the byte-exact .bak a repair left — the explicit undo of a repair')).action((options) => runCodegraphConfigRestoreCommand(io, options, options.peaksJson));
380
539
  addProjectOption(codegraph.command('init').description('Initialize codegraph for a project')).action((options) => runCodegraphInitCommand(io, options, options.peaksJson));
381
540
  addProjectOption(codegraph
382
541
  .command('index')
@@ -1,4 +1,4 @@
1
- import { describeMemoryBlockDrops, executeProjectMemoryBackup, executeProjectMemoryExtract, summarizeProjectMemoryBackupResult, summarizeProjectMemoryExtractResult, VALID_PROJECT_MEMORY_KINDS } from '../../../services/memory/project-memory-service.js';
1
+ import { describeMemoryBlockDrops, executeProjectMemoryBackup, executeProjectMemoryExtract, SENSITIVE_MEMORY_CHECKS, summarizeProjectMemoryBackupResult, summarizeProjectMemoryExtractResult, UnsafeMemoryError, VALID_PROJECT_MEMORY_KINDS } from '../../../services/memory/project-memory-service.js';
2
2
  import { fail, ok } from 'peaks-loop-shared/result';
3
3
  import { addJsonOption, getErrorMessage, printResult } from '../../cli-helpers.js';
4
4
  /** Derived from the canonical kind vocabulary — never hand-maintain a list here. */
@@ -26,7 +26,13 @@ export function registerMemoryCommand(program, io) {
26
26
  printResult(io, ok('memory.extract', summarizeProjectMemoryExtractResult(result), describeMemoryBlockDrops(result.droppedBlocks)), options.json);
27
27
  }
28
28
  catch (error) {
29
- printResult(io, fail('memory.extract', 'MEMORY_EXTRACT_FAILED', getErrorMessage(error), {}, ['Check artifact paths and remove secrets before extracting memory']), options.json);
29
+ const refusal = error instanceof UnsafeMemoryError ? error : null;
30
+ printResult(io, fail('memory.extract', 'MEMORY_EXTRACT_FAILED', getErrorMessage(error),
31
+ // The check and the term are facts about the failure, so they ride
32
+ // the envelope's data — not just its prose. `fail()` redacts
33
+ // `message` (see `UnsafeMemoryError`), so the term would otherwise
34
+ // reach the reader as `[redacted]`; `data` is passed through.
35
+ refusal === null ? {} : { check: refusal.check, matchedTerm: refusal.matchedTerm }, memoryExtractNextActions(refusal)), options.json);
30
36
  process.exitCode = 1;
31
37
  }
32
38
  });
@@ -160,3 +166,30 @@ export function registerMemoryCommand(program, io) {
160
166
  });
161
167
  });
162
168
  }
169
+ /**
170
+ * The remedy for a failed `memory extract`, chosen by the check that refused
171
+ * the write — the same `SENSITIVE_MEMORY_CHECKS` values the refusal message
172
+ * and envelope data are built from, so the message, the data and the advice
173
+ * cannot drift apart.
174
+ *
175
+ * WHY IT IS NOT ONE STATIC STRING. Every failure used to get "Check artifact
176
+ * paths and remove secrets before extracting memory" — including a refusal
177
+ * raised only because a memory TITLE contained a word like `authority`
178
+ * (slice C0). There was no secret anywhere in that memory, so the one thing
179
+ * the hint told the user to do was the one thing that could not help: it sent
180
+ * them looking for a credential that did not exist. A remedy has to answer the
181
+ * check that actually failed.
182
+ *
183
+ * `null` (a failure that is not a safety refusal — a path escape, a missing
184
+ * artifact) keeps the pre-C0 hint, which is about paths and secrets rather
185
+ * than about a check.
186
+ */
187
+ function memoryExtractNextActions(refusal) {
188
+ if (refusal === null) {
189
+ return ['Check artifact paths and remove secrets before extracting memory'];
190
+ }
191
+ if (refusal.check === SENSITIVE_MEMORY_CHECKS.title) {
192
+ return ['Retitle the memory so it is not named after a credential term, then re-run memory extract'];
193
+ }
194
+ return ['Remove the credential value from the memory content, then re-run memory extract'];
195
+ }
@@ -34,7 +34,7 @@ import { playwrightProfilePaths } from '../../services/worktree/playwright-profi
34
34
  import { loadPreferences } from '../../services/preferences/preferences-service.js';
35
35
  import { DEFAULT_PREFERENCES } from '../../services/preferences/preferences-types.js';
36
36
  import { writeLogEntry } from '../../services/log/logger.js';
37
- import { PROMPT_LIMIT_BYTES, RECOMMENDED_ROLES, validateRole } from './sub-agent-shared.js';
37
+ import { PROMPT_LIMIT_BYTES, RECOMMENDED_ROLES, deprecatedReviewerWarnings, validateRole } from './sub-agent-shared.js';
38
38
  import { runDispatchFromDag } from './dispatch-from-dag.js';
39
39
  import { TEST_TOOL_DETECTION_BLOCK } from '../../services/dispatch/test-tool-detection.js';
40
40
  import { MemoryPreflightService, deriveMemoryQuery } from '../../services/context/memory-preflight-service.js';
@@ -520,7 +520,9 @@ export function registerDispatchCommand(parent, io) {
520
520
  `if the file does not exist, your verdict MUST be \`status: "blocked"\` with reason "must_ls_files_failed". Do NOT silently skip this step.\n`;
521
521
  }
522
522
  const effectivePrompt = `${memoryAugmentedBody}${isolationBlock}${mustLsFilesBlock}`;
523
- const warnings = [...decision.warnings];
523
+ // Slice F2 (rid-f2-ac1-wiring): deprecated reviewer slots are accepted
524
+ // + rerouted here, never refused — rationale on the helper itself.
525
+ const warnings = [...decision.warnings, ...deprecatedReviewerWarnings(role)];
524
526
  let toolCall;
525
527
  try {
526
528
  toolCall = adapter.subAgentDispatcher.buildToolCall({ role, prompt: effectivePrompt, requestId: rid, sessionId: sid });
@@ -15,6 +15,7 @@
15
15
  */
16
16
  import { readFile } from 'node:fs/promises';
17
17
  import { initHandoff, readHandoff, showHandoff, verifyHandoff, writeHandoff } from '../../services/prd/handoff-service.js';
18
+ import { deriveGateEvidenceForRequest } from '../../services/prd/gate-evidence-derivation.js';
18
19
  import { fail, ok } from 'peaks-loop-shared/result';
19
20
  import { addJsonOption, getErrorMessage, printResult } from '../cli-helpers.js';
20
21
  /** Split a comma-separated string into a trimmed, non-empty list.
@@ -57,6 +58,19 @@ export function registerPrdCommands(program, io) {
57
58
  const body = await resolveBody(options.body);
58
59
  const projectRoot = options.project ?? process.cwd();
59
60
  const writtenAt = new Date().toISOString();
61
+ // B2 / F1 of `rid-b1-qa`: the ONLY production caller of `initHandoff`
62
+ // used to pass no `gateEvidence`, so no capsule written by this command
63
+ // ever carried the field. It is derived now — from the request type
64
+ // recorded on this rid's PRD artifact — and `undefined` (no artifact, or
65
+ // an unresolvable type) means "write no block", i.e. the pre-B2 bytes.
66
+ // No flag is added: the user does not hand-author this map, and
67
+ // `deriveGateEvidenceForRequest` computes it from context the command
68
+ // already has.
69
+ const gateEvidence = await deriveGateEvidenceForRequest({
70
+ projectRoot,
71
+ sessionId: options.sid,
72
+ requestId: options.rid,
73
+ });
60
74
  const handoff = initHandoff({
61
75
  requestId: options.rid,
62
76
  sessionId: options.sid,
@@ -65,6 +79,7 @@ export function registerPrdCommands(program, io) {
65
79
  goals: splitCsv(options.goals),
66
80
  acceptanceCriteria: splitCsv(options.ac),
67
81
  preservedBehavior: splitCsv(options.preserve),
82
+ ...(gateEvidence === undefined ? {} : { gateEvidence }),
68
83
  });
69
84
  if (options.apply !== true) {
70
85
  printResult(io, ok('prd.handoff.init', {
@@ -138,6 +138,29 @@ export type ContractStoreModule = typeof import('../../services/dispatch/contrac
138
138
  * - No whitespace, no control characters, no DEL (0x7F)
139
139
  */
140
140
  export declare function validateRole(role: string): string | null;
141
+ /**
142
+ * Slice F2 (rid-f2-ac1-wiring) — the dispatch-side twin of the prereq-side
143
+ * back-compat in `artifact-prerequisites.ts` (`AUDIT_SECURITY` /
144
+ * `AUDIT_PERF` accept `rd/security-review.md` / `rd/perf-baseline.md` via
145
+ * `legacyRelativePaths`).
146
+ *
147
+ * `security-reviewer` and `perf-baseline-reviewer` left the RD 3-way
148
+ * fan-out in v2.12.0 (`RD_DEPRECATED_REVIEWERS`), so a dispatch of either
149
+ * name no longer runs a reviewer that exists. They are **accepted, not
150
+ * rejected**, for two reasons:
151
+ * - the prereq side still accepts their legacy artifacts, so refusing
152
+ * the dispatch would make the two halves of the same deprecation
153
+ * disagree; and
154
+ * - `reviewer-dispatch-policy.ts` states the intended behaviour as
155
+ * "route to the new audit skill **instead of failing the gate**".
156
+ *
157
+ * The reroute is therefore advisory: the envelope carries this notice and
158
+ * the caller takes it to `peaks-security-audit` / `peaks-perf-audit`.
159
+ *
160
+ * Returns `[]` for every other role, so dispatching a current role
161
+ * produces the byte-identical envelope it produced before this wiring.
162
+ */
163
+ export declare function deprecatedReviewerWarnings(role: string): string[];
141
164
  /**
142
165
  * Roll up a batch result array into the summary the CLI envelope exposes
143
166
  * for `peaks sub-agent await`. Counts per status; the orchestrator
@@ -1,3 +1,9 @@
1
+ // Slice F2 (rid-f2-ac1-wiring) — first caller of the RD dispatch policy
2
+ // module. Before this wiring `src/services/rd/reviewer-dispatch-policy.ts`
3
+ // had zero importers in src/ + packages/ + scripts/ and the 2 slots it
4
+ // governs (`security-reviewer`, `perf-baseline-reviewer`) were neither
5
+ // rejected nor rerouted by anything on the dispatch path.
6
+ import { isDeprecatedReviewer } from '../../services/rd/reviewer-dispatch-policy.js';
1
7
  // Slice 2026-07-29-dispatch-stall-governance / S6 — `probeShell` is
2
8
  // re-exported here so the dispatch chokepoint (`dispatch-commands.ts`)
3
9
  // and the sub-agent batch-sync wait can lazily acquire a typed
@@ -51,6 +57,39 @@ export function validateRole(role) {
51
57
  }
52
58
  return null;
53
59
  }
60
+ /**
61
+ * Slice F2 (rid-f2-ac1-wiring) — the dispatch-side twin of the prereq-side
62
+ * back-compat in `artifact-prerequisites.ts` (`AUDIT_SECURITY` /
63
+ * `AUDIT_PERF` accept `rd/security-review.md` / `rd/perf-baseline.md` via
64
+ * `legacyRelativePaths`).
65
+ *
66
+ * `security-reviewer` and `perf-baseline-reviewer` left the RD 3-way
67
+ * fan-out in v2.12.0 (`RD_DEPRECATED_REVIEWERS`), so a dispatch of either
68
+ * name no longer runs a reviewer that exists. They are **accepted, not
69
+ * rejected**, for two reasons:
70
+ * - the prereq side still accepts their legacy artifacts, so refusing
71
+ * the dispatch would make the two halves of the same deprecation
72
+ * disagree; and
73
+ * - `reviewer-dispatch-policy.ts` states the intended behaviour as
74
+ * "route to the new audit skill **instead of failing the gate**".
75
+ *
76
+ * The reroute is therefore advisory: the envelope carries this notice and
77
+ * the caller takes it to `peaks-security-audit` / `peaks-perf-audit`.
78
+ *
79
+ * Returns `[]` for every other role, so dispatching a current role
80
+ * produces the byte-identical envelope it produced before this wiring.
81
+ */
82
+ export function deprecatedReviewerWarnings(role) {
83
+ if (!isDeprecatedReviewer(role)) {
84
+ return [];
85
+ }
86
+ return [
87
+ `role "${role}" was removed from the RD fan-out in v2.12.0 — reroute to the standalone audit skill ` +
88
+ `(\`peaks security-audit run --rid <rid>\` / \`peaks perf-audit run --rid <rid>\`). The dispatch still ` +
89
+ `proceeds for back-compat, and the legacy rd/security-review.md / rd/perf-baseline.md artifact stays ` +
90
+ `accepted by the rd:qa-handoff prereq (artifact-prerequisites.ts legacyRelativePaths).`
91
+ ];
92
+ }
54
93
  /**
55
94
  * Roll up a batch result array into the summary the CLI envelope exposes
56
95
  * for `peaks sub-agent await`. Counts per status; the orchestrator