peaks-loop 4.0.43 → 4.0.45

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 (35) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/README-en.md +1 -1
  3. package/README.md +1 -1
  4. package/dist/cli/commands/codegraph-commands.d.ts +1 -0
  5. package/dist/cli/commands/codegraph-commands.js +239 -8
  6. package/dist/cli/commands/final-review-commands.d.ts +34 -10
  7. package/dist/cli/commands/final-review-commands.js +132 -34
  8. package/dist/cli/commands/share-commands.d.ts +49 -0
  9. package/dist/cli/commands/share-commands.js +114 -14
  10. package/dist/services/codegraph/codegraph-autorefresh.js +12 -0
  11. package/dist/services/codegraph/codegraph-exclude-integrity.d.ts +61 -0
  12. package/dist/services/codegraph/codegraph-exclude-integrity.js +98 -0
  13. package/dist/services/codegraph/codegraph-exclude-reconciler.d.ts +26 -0
  14. package/dist/services/codegraph/codegraph-exclude-reconciler.js +217 -0
  15. package/dist/services/codegraph/codegraph-exclude-repair.d.ts +102 -0
  16. package/dist/services/codegraph/codegraph-exclude-repair.js +266 -0
  17. package/dist/services/codegraph/codegraph-preflight-service.js +12 -0
  18. package/dist/services/codegraph/codegraph-service.d.ts +0 -1
  19. package/dist/services/codegraph/codegraph-service.js +5 -4
  20. package/dist/services/doctor/doctor-service/checks/codegraph-exclude-integrity.d.ts +29 -0
  21. package/dist/services/doctor/doctor-service/checks/codegraph-exclude-integrity.js +88 -0
  22. package/dist/services/doctor/doctor-service/plugin-registry.js +2 -0
  23. package/dist/services/doctor/doctor-service/types.d.ts +27 -0
  24. package/dist/services/final-review/final-review-service.d.ts +335 -1
  25. package/dist/services/final-review/final-review-service.js +1457 -6
  26. package/dist/services/final-review/index.d.ts +2 -1
  27. package/dist/services/final-review/index.js +2 -1
  28. package/dist/services/final-review/pre-post-diff.d.ts +137 -0
  29. package/dist/services/final-review/pre-post-diff.js +657 -0
  30. package/dist/services/prd/handoff-auto-regen.js +0 -1
  31. package/dist/services/prd/handoff-service.d.ts +9 -1
  32. package/dist/services/prd/handoff-service.js +48 -6
  33. package/package.json +7 -5
  34. package/skills/peaks-final-review/SKILL.md +79 -35
  35. package/skills/peaks-final-review/references/4-dimensions.md +42 -5
package/CHANGELOG.md CHANGED
@@ -1,5 +1,78 @@
1
1
  # Changelog
2
2
 
3
+ ## 4.0.45 — 2026-09-12 (一个永远无法通过的维度 + 一个验代理的守卫)
4
+
5
+ **Highlights**:
6
+
7
+ 1. **4 维验收闸里有一维从构造上就不可能通过。** `existing-functionality-intact` 的契约要求一份 `pre-post-diff`(前后结构对比:测试面 / 公开 API 面),而**全仓没有任何东西生产它**。于是它被喂的是 `rd/tech-doc.md`(设计意图)与 `prd/handoff.md`(批准范围)—— 而这个模块自己的文件头注释,把"拿设计意图文档当回归评估"列为它存在的理由。结果:`allPass === true` 对**任何**工作流都不可达。
8
+
9
+ 本版补上生产者(`pre-post-diff.ts`):以 base ref 对比工作树,报**测试面**(文件数 / 用例数)、**源文件清单**、**公开 API 面**(顶层 `export` 语句数与名字集合),写入 `.peaks/_runtime/<sessionId>/final-review/api-diff.txt`。**诚实边界写在产物里**:export 检测是行锚定的近似,能发现"少了一个导出"这类结构丢失,**发现不了签名变更**。算不出 baseline 时**不产出 artifact、不编造 diff** —— 该维保持 `inconclusive`。
10
+
11
+ 2. **消费它的那个闸,键在"送达",不在"产物存在"。** 独立复核连做七轮,每轮都在下一层找到同一个形状 —— 一个接受**代理**的闸("评审者看到了吗"被替换成"磁盘上有吗"):
12
+
13
+ | 轮 | 被判为"检查过了"的东西 |
14
+ |---|---|
15
+ | 1 | `status === 'computed'`(磁盘上有文件) |
16
+ | 2 | `status === 'found'`(≥1 字节送达) |
17
+ | 3 | `totalBytes === 0`(把 missing 与 empty 混为一谈) |
18
+
19
+ 共同根因是:一个源**可以被部分内联**,于是"送达了吗"永远有一个律师式答案。现在分配器是 **per-source 全有或全无**(要么整份、要么 omitted,不存在截在中间),且 `isDelivered` 是这个问题**唯一的家** —— 每个源声明自己的送达规则:`whole`(整份文档;被截断过的文档是另一份文档)或 `conclusion`(一个**就是结论本身**的字面量)。
20
+
21
+ 3. **随之而来的两处实质变化,都要求诚实。**
22
+ - 证据预算改为**推导**(`10,240 = 总额 / 4`)而不是常量 —— 因为走 `whole` 规则的源一旦超过它,就**永远送不到**。
23
+ - 一个维度若其全部支撑源都装不下,现在会被**响亮报出**(prompt 里出 `## Evidence delivery reachability` 段、维度 summary 挂 `[delivery-reachability: …]` 标记、并进 `needsAttention`),而不是**静默地永远红**。**故意不抛错** —— 抛错会连带毁掉那三个本来可送达的维度。
24
+
25
+ 4. **反回归守卫本身,就是同一个形状的第 7 个实例 —— 且它一开始是坏的。** 初版守卫按**语法形状**切分源码(只认行首 `function name`),于是**箭头函数、生成器、`export default function`、类方法**一律不被扫描:**8 种等价写法全部放行**,端到端实测"顶层箭头里放 2 个代理字面量"仍 `3 passed EXIT=0`。**一个能放行 8/8 的守卫比没有守卫更糟** —— 它把"没人在看"变成"有东西在看"。
26
+
27
+ 现在它用仓库自带的 `typescript` 解析 AST、读**函数体内**的 token,于是那 4 种形状绕过全部关闭(实测:往真实源码注入箭头式代理 → 守卫转红)。剩下 4 种**不含该字面量**的改写(拆变量 `const F='found'`、`==`、`item['status']`、字符串拼接)**仍然放行** —— 这一点被**固定成一条通过的测试**,让守卫**陈述自己的边界**而不是暗示一个。
28
+
29
+ 5. **`peaks codegraph status` 在真有缺口时会同时印出两行互相矛盾的话。** 退出码(74)与 `[FAIL]` 细节都是对的,但人第一眼看到的是:
30
+
31
+ ```
32
+ [OK] Index is up to date
33
+ [FAIL] codegraph index is incomplete: 2 of 1133 tracked source files are excluded …
34
+ ```
35
+
36
+ 两个闸在回答不同的问题:上游答"图与上次扫描一致"(确实一致),peaks 答"图覆盖了仓库吗"(没有)。**都对,并排印出来就是矛盾。** 现在上游那行被**归因而非抑制**(措辞与统计保留,只是把它的适用范围说明白),只在这一行、且只在缺口门**确实触发**时改动;无缺口路径逐字节不变(含 ANSI),机器可读 envelope 未动。
37
+
38
+ **验证**:三个版本常量一致(**4.0.45**);`tsc -p tsconfig.build.json` exit 0;宽 `tsconfig.json` 保持 **142** 基线;`tests/unit` **214 files / 2165 passed / 3 skipped / 0 failed**;`pnpm build` 的 `build-integrity` OK。
39
+
40
+ **已知未修 / 边界**:`conclusion` 规则下超大的源不算"不可送达"(结论标记可能在截断点之后);单一可送达源的**穿越前余量**(本 session 实测 748 B)**没有**告警,只报"已经不可能"的状态;4 维中的 `existing-functionality-intact` 在**非 git 项目**里永久 `inconclusive`(这是诚实结果,已写进 SKILL.md 与 `4-dimensions.md`)。
41
+
42
+ **备注**:本版的 `prepare-final-review` 闸**未经过独立复核**(前六轮每一轮都经过并各找到 1 个 HIGH;第 7 轮由 orchestrator 自行验证,含箭头注入实测),这一点如实记录以便日后回溯。
43
+
44
+ ## 4.0.44 — 2026-09-12 (被静默排除的源文件 + 一个验错东西的验收闸)
45
+
46
+ **Highlights**:
47
+
48
+ 1. **`peaks codegraph status` 报"索引是最新的",而 26 个 git 跟踪的源文件根本不在索引里。** 上游默认 exclude 表**按目录名**匹配,而本仓库恰好把 `artifacts/` `release/` `vendor/` `bin/` `publish/` 用作了源码目录名,于是 5 条默认规则把 26 个真实源文件挡在门外。`status` 说的是"图与上次扫描一致",不是"图覆盖了仓库" —— 所以这个洞是隐形的。
49
+
50
+ 本版按**一条原则**建了 reconcile / repair / verify:被 git 跟踪的源文件不得被 exclude 规则挡住。
51
+
52
+ - 对账用 `picomatch`(从传递依赖提升为直接依赖),与上游**同一个匹配引擎**;此前自实现的 glob 对 `{}` 模式**漏报**,且会灾难性回溯
53
+ - `status` 现在**失败(exit 74)**并指名违规规则与文件,而不是报 OK;doctor 也报这个缺口
54
+ - `init` 自愈,且 preflight / autorefresh 走**同一个 helper** —— 它们此前会跑上游 init 并盖 marker 却**不做修复**,使"全新 clone 自愈"在真实流程里**不可达**
55
+ - 匹配不到任何 tracked 文件的规则**永不删除**;未跟踪文件仍被排除(原语义不变)
56
+
57
+ 2. **4 维人工验收闸干不了活 —— 四层独立失效,每一层都只有真机跑才看得见。**
58
+
59
+ - **(a)** 服务只把目标的 `successCriteria` 喂给模型,**一条证据都不给**,却要求它产出**带证据的裁决**。现在从磁盘收集真实证据(有界),并用**结构而非提示词措辞**保证:支撑源全部缺失的 `pass` 一律降级为 `inconclusive`
60
+ - **(b)** CLI 只能跑 `stub`;`--llm-provider anthropic` 现在真的构造 runner,`providerBinding` 如实上报
61
+ - **(c)** `maxTokens` 硬编码 3000,低于一份完整证据包所需,回复被**截在 JSON 中间**;预算随内联证据量伸缩,截断被诊断为"输出预算失败"而非"JSON 非法",并给出 `PEAKS_FINAL_REVIEW_MAX_OUTPUT_TOKENS` 逃生阀
62
+ - **(d)** 证据预算是**先到先得**,于是最后排序的源 —— 也就是四个维度里的一个 —— 被**永久饿死**;现在每个维度有保留配额
63
+
64
+ ⚠️ **`existing-functionality-intact` 仍然无法通过**:它要求的 `pre-post-diff` 产物**全仓没有任何生产者**。这是缺功能,不是 bug;已在 SKILL.md 记为**工具状态**,而**不是**把一个不相关的源重映射进去把闸弄绿。
65
+
66
+ 3. **同一个"闸门验代理而非性质"的形状,又抓到三处。**
67
+
68
+ - `handoff-auto-regen` 把 `sessionId` **写了两遍**,产出的 frontmatter 仓库自己的 YAML 解析器**拒绝** —— 而 `AUDIT_REQUIRES_HANDOFF` 只做**子串**检查,所以放行
69
+ - `sub-agent finalize --request-id` 扫会话目录下**所有** `.json`,读到 `active-dispatches.json` 就抛;两个分支现在都容忍坏记录,且 `--request-id` 与 `--batch` 共用**优先 queued** 的选择逻辑,并报出选了哪条、为何跳过其它
70
+ - `skills/peaks-final-review/SKILL.md` 断言它自己的 CLI "**尚不存在**",让调用方去手写 service 调用 —— 而它存在且已注册。这句话在本版开发中**真实地浪费了工作**:orchestrator 为一个早已存在的命令手写了一个调用脚本
71
+
72
+ **验证**:三个版本常量一致(**4.0.44**);`tsc -p tsconfig.build.json` exit 0;宽 `tsconfig.json` 保持 **142** 基线;`tests/unit` **213 files / 2116 passed / 3 skipped / 0 failed**;`pnpm build` 的 `build-integrity` OK。
73
+
74
+ **已知未修**:`existing-functionality-intact` 缺证据生产者(见第 2 条);`RUNTIME_NPM_VERSION`(0.0.21)与 internal-runtime 包版本已不同步(4.0.43 起即如此)。
75
+
3
76
  ## 4.0.43 — 2026-09-12 (一个从来拦不住东西的闸门 + 前端接口防腐层)
4
77
 
5
78
  **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.43 (2026-09-12) |
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.45 (2026-09-12) |
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.43(2026-09-12) |
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.45(2026-09-12) |
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 |
@@ -10,4 +10,5 @@ import { type ProgramIO } from '../cli-helpers.js';
10
10
  * untouched.
11
11
  */
12
12
  export declare function rewriteBareCodegraphHints(text: string): string;
13
+ export declare function attributeUpstreamUpToDateLine(stdout: string): string;
13
14
  export declare function registerCodegraphCommands(program: Command, io: ProgramIO): void;
@@ -2,6 +2,8 @@ import { InvalidArgumentError } from 'commander';
2
2
  import { statSync } from 'node:fs';
3
3
  import { resolve } from 'node:path';
4
4
  import { createCodegraphInvocation, executeCodegraphInvocation, defaultCodegraphInitGuard, writeCodegraphMarker, writeCodegraphAffectedContext, CodegraphInitConflictError } from '../../services/codegraph/codegraph-service.js';
5
+ import { CODEGRAPH_INTEGRITY_EXIT_CODE, inspectCodegraphExcludeIntegrity, isCodegraphExcludeConfigPresent, renderCodegraphExcludeIntegrityLines } from '../../services/codegraph/codegraph-exclude-integrity.js';
6
+ import { repairCodegraphExcludeFromProject } from '../../services/codegraph/codegraph-exclude-repair.js';
5
7
  import { fail, ok } from 'peaks-loop-shared/result';
6
8
  import { getErrorMessage, printResult, redactSensitiveErrorMessage } from '../cli-helpers.js';
7
9
  function addPeaksJsonOption(command) {
@@ -36,7 +38,48 @@ function printCodegraphFailure(io, command, error, asJson, exitCode = 1) {
36
38
  export function rewriteBareCodegraphHints(text) {
37
39
  return text.replace(/(?<![\w-])(?<!peaks\s)codegraph(?=\s+(?:status|init|index|query|files|context|affected)\b)/g, 'peaks codegraph');
38
40
  }
39
- async function runCodegraphCommand(io, command, options, asJson) {
41
+ const ANSI_SGR_PATTERN = /\x1b\[[0-9;]*m/g;
42
+ /**
43
+ * Upstream `status` answers a different question than peaks-loop's
44
+ * integrity gate: upstream says "the on-disk graph matches the last scan"
45
+ * (true), peaks says "that graph covers the repository" (false when rules
46
+ * exclude tracked files). Both verdicts are correct, but an unqualified
47
+ * `[OK] Index is up to date` printed above our `[FAIL] ...` reads as
48
+ * "nothing to see here" — and the OK is the line the eye lands on first.
49
+ * The exit code and the JSON envelope are already right; only this line
50
+ * lies by juxtaposition.
51
+ *
52
+ * So: keep upstream's wording — the line stays recognizable, and the
53
+ * Files/Nodes counts around it are untouched — but drop the bare OK
54
+ * marker and name the only question it answers. Clean runs never reach
55
+ * this, so their output stays byte-identical.
56
+ *
57
+ * The match is anchored to THAT line. An earlier version keyed on
58
+ * `includes('up to date')`, which is content-blind: upstream prints other
59
+ * `[OK] ... are up to date` lines (a language-server or watcher line is the
60
+ * observed one), and each of those was rewritten into a claim about the
61
+ * INDEX — a misattribution introduced by a change whose entire purpose was
62
+ * to stop misleading output. Anything that is not the index line is passed
63
+ * through byte-for-byte, tail note and all.
64
+ */
65
+ const INDEX_UP_TO_DATE_RE = /^\[OK\]\s+Index is up to date\b/i;
66
+ export function attributeUpstreamUpToDateLine(stdout) {
67
+ return stdout
68
+ .split('\n')
69
+ .map((line) => {
70
+ const visible = line.replace(ANSI_SGR_PATTERN, '').trim();
71
+ if (!INDEX_UP_TO_DATE_RE.test(visible)) {
72
+ return line;
73
+ }
74
+ // Only the OK marker is downgraded and the attribution appended: the
75
+ // rest of the line — including whatever upstream wrote after it — is
76
+ // preserved, so nothing upstream actually said is replaced.
77
+ const withoutOk = visible.replace(/^\[OK\]\s*/, '');
78
+ return `[i] ${withoutOk} (upstream: matches the last scan only; repository coverage is answered below)`;
79
+ })
80
+ .join('\n');
81
+ }
82
+ async function runCodegraphCommand(io, command, options, asJson, attributeStdout) {
40
83
  try {
41
84
  const invocation = createCodegraphInvocation(options);
42
85
  const result = await executeCodegraphInvocation(invocation);
@@ -45,7 +88,8 @@ async function runCodegraphCommand(io, command, options, asJson) {
45
88
  return;
46
89
  }
47
90
  const didFail = result.exitCode !== null && result.exitCode !== 0;
48
- const stdout = rewriteBareCodegraphHints(result.stdout);
91
+ const rewritten = rewriteBareCodegraphHints(result.stdout);
92
+ const stdout = attributeStdout === undefined ? rewritten : attributeStdout(rewritten);
49
93
  const stderr = rewriteBareCodegraphHints(result.stderr);
50
94
  if (stdout.length > 0) {
51
95
  io.stdout((didFail ? redactSensitiveErrorMessage(stdout) : stdout).trimEnd());
@@ -61,6 +105,144 @@ async function runCodegraphCommand(io, command, options, asJson) {
61
105
  printCodegraphFailure(io, command, error, asJson);
62
106
  }
63
107
  }
108
+ /**
109
+ * `--peaks-json` machine report for `status`. Carries the upstream
110
+ * result AND the peaks-loop integrity verdict as one JSON document so a
111
+ * CI job can gate on `data.integrity.gap` / `data.integrity.rulesToRemove`
112
+ * without scraping human text.
113
+ */
114
+ async function runCodegraphStatusJson(io, options, integrity, integrityWarning) {
115
+ let result;
116
+ try {
117
+ result = await executeCodegraphInvocation(createCodegraphInvocation({ subcommand: 'status', project: options.project }));
118
+ }
119
+ catch (error) {
120
+ printCodegraphFailure(io, 'codegraph.status', error, true);
121
+ return;
122
+ }
123
+ const upstream = {
124
+ exitCode: result.exitCode,
125
+ stdout: rewriteBareCodegraphHints(result.stdout).trimEnd(),
126
+ stderr: redactSensitiveErrorMessage(rewriteBareCodegraphHints(result.stderr)).trimEnd()
127
+ };
128
+ const upstreamFailed = result.exitCode !== null && result.exitCode !== 0;
129
+ if (integrity?.gap === true) {
130
+ printResult(io, fail('codegraph.status', 'CODEGRAPH_INDEX_INCOMPLETE', `codegraph index is incomplete: ${integrity.excludedTrackedCount} of ${integrity.trackedSourceCount} tracked source files are excluded by ${integrity.rulesToRemove.length} rule(s).`, { upstream, integrity, integrityWarning }, ['Run `peaks codegraph repair-exclude --project <root>` to drop the offending rules and rebuild the index.']), true);
131
+ }
132
+ else if (upstreamFailed) {
133
+ printResult(io, fail('codegraph.status', 'CODEGRAPH_COMMAND_FAILED', redactSensitiveErrorMessage(upstream.stderr || upstream.stdout || `codegraph exited with code ${String(result.exitCode)}`), { upstream, integrity, integrityWarning }, ['Check the codegraph project path before retrying']), true);
134
+ }
135
+ else {
136
+ printResult(io, ok('codegraph.status', { upstream, integrity, integrityWarning }), true);
137
+ }
138
+ if (upstreamFailed) {
139
+ process.exitCode = result.exitCode ?? 1;
140
+ }
141
+ }
142
+ /**
143
+ * `peaks codegraph status` with an integrity gate.
144
+ *
145
+ * The upstream status is still proxied verbatim (that is what the
146
+ * command has always done), but a clean upstream "index is up to date"
147
+ * is no longer sufficient: when git-tracked source files are being
148
+ * excluded by the config, the command says so, names the rules and
149
+ * files, and exits non-zero.
150
+ *
151
+ * Read-only by construction — it imports the integrity inspector, never
152
+ * the repair writer. Fixing the config is `peaks codegraph init`
153
+ * (fresh) or `peaks codegraph repair-exclude` (explicit).
154
+ */
155
+ async function runCodegraphStatusCommand(io, options, asJson) {
156
+ let integrity = null;
157
+ let integrityWarning = null;
158
+ const projectRoot = resolve(options.project);
159
+ try {
160
+ // Never initialized here → no exclude list is in play, so there is
161
+ // nothing to report. Staying silent keeps `status` honest and
162
+ // unchanged for projects that do not use codegraph at all.
163
+ integrity = isCodegraphExcludeConfigPresent(projectRoot)
164
+ ? inspectCodegraphExcludeIntegrity(projectRoot)
165
+ : null;
166
+ }
167
+ catch (error) {
168
+ // Not a git work tree, no config yet, malformed config — the
169
+ // upstream status is still worth printing, so degrade to a warning
170
+ // instead of failing the whole command.
171
+ integrityWarning = getErrorMessage(error);
172
+ }
173
+ if (asJson === true) {
174
+ await runCodegraphStatusJson(io, options, integrity, integrityWarning);
175
+ }
176
+ else {
177
+ // Only when the gate found a gap: upstream's `[OK] Index is up to
178
+ // date` answers "consistent with the last scan", and printing it
179
+ // unqualified right above our `[FAIL]` tells the reader two opposite
180
+ // things at once. Clean runs get no transform and stay byte-identical.
181
+ await runCodegraphCommand(io, 'codegraph.status', { subcommand: 'status', project: options.project }, false, integrity?.gap === true ? attributeUpstreamUpToDateLine : undefined);
182
+ if (integrityWarning !== null) {
183
+ io.stdout(`[WARN] codegraph exclude integrity not evaluated: ${integrityWarning}`);
184
+ }
185
+ else if (integrity !== null) {
186
+ for (const line of renderCodegraphExcludeIntegrityLines(integrity)) {
187
+ io.stdout(line);
188
+ }
189
+ }
190
+ }
191
+ if (integrity?.gap === true) {
192
+ process.exitCode = CODEGRAPH_INTEGRITY_EXIT_CODE;
193
+ }
194
+ }
195
+ /**
196
+ * Explicit repair path: reconcile → drop offending rules → back up the
197
+ * config → rebuild the index. Mirrors the automatic step `init` runs
198
+ * after a fresh upstream init, for workspaces that were already
199
+ * initialized before the integrity gate existed.
200
+ */
201
+ async function runCodegraphRepairExcludeCommand(io, options, asJson) {
202
+ let projectRoot;
203
+ try {
204
+ const candidate = resolve(options.project);
205
+ if (!statSync(candidate).isDirectory()) {
206
+ throw new Error('Project path must exist and be a directory');
207
+ }
208
+ projectRoot = candidate;
209
+ }
210
+ catch (error) {
211
+ printCodegraphFailure(io, 'codegraph.repair-exclude', error, asJson);
212
+ return;
213
+ }
214
+ const report = await repairCodegraphExcludeFromProject(projectRoot);
215
+ // Where the notes go matters: `printResult` renders every `warnings`
216
+ // entry to stderr with a `warning: ` prefix, so a confirmation parked
217
+ // in the third slot reads as a problem — and a real warning parked
218
+ // there double-prefixes. Confirmations go to `nextActions`; only a
219
+ // genuine `report.warning` reaches `warnings`, verbatim.
220
+ const confirmations = [];
221
+ if (report.applied) {
222
+ confirmations.push(`Removed ${report.rulesRemoved.length} exclude rule(s), recovering ${report.filesRecovered} tracked source file(s). Config backed up to ${report.backupPath}.`);
223
+ }
224
+ else {
225
+ confirmations.push('No tracked source file is excluded by the codegraph config; nothing to repair.');
226
+ }
227
+ if (report.applied) {
228
+ confirmations.push('Re-run `peaks codegraph status --project <root>` to confirm the gap is closed.');
229
+ }
230
+ printResult(io, ok('codegraph.repair-exclude', {
231
+ applied: report.applied,
232
+ rulesRemoved: report.rulesRemoved,
233
+ filesRecovered: report.filesRecovered,
234
+ trackedSourceCount: report.trackedSourceCount,
235
+ configPath: report.configPath,
236
+ backupPath: report.backupPath,
237
+ reindexed: report.reindexed,
238
+ warning: report.warning
239
+ }, report.warning === null ? [] : [report.warning], confirmations), asJson);
240
+ // A repair that could not run to completion (or could not reindex)
241
+ // must not report success to a shell.
242
+ if (report.warning !== null) {
243
+ process.exitCode = 1;
244
+ }
245
+ }
64
246
  /**
65
247
  * rid-CG-006 — init conflict guard. Resolves the project root and
66
248
  * probes `.codegraph/` for the peaks-loop marker before invoking the
@@ -92,7 +274,14 @@ async function runCodegraphInitCommand(io, options, asJson) {
92
274
  guard: guardOutcome.status,
93
275
  codegraphDir: guardOutcome.codegraphDir,
94
276
  markerPresent: true
95
- }, [`.codegraph/ is already managed by peaks-loop; init is a no-op. Marker: ${guardOutcome.codegraphDir}/.peaks-loop-marker`], ['Run `peaks codegraph index` to (re)build the index without touching the schema.']), asJson);
277
+ },
278
+ // A no-op init is a SUCCESS. This message used to sit in the
279
+ // `warnings` slot and was therefore printed as
280
+ // `warning: .codegraph/ is already managed by peaks-loop...`.
281
+ [], [
282
+ `.codegraph/ is already managed by peaks-loop; init is a no-op. Marker: ${guardOutcome.codegraphDir}/.peaks-loop-marker`,
283
+ 'Run `peaks codegraph index` to (re)build the index without touching the schema.'
284
+ ]), asJson);
96
285
  return;
97
286
  }
98
287
  if (guardOutcome.status === 'conflict-foreign-schema') {
@@ -110,8 +299,7 @@ async function runCodegraphInitCommand(io, options, asJson) {
110
299
  try {
111
300
  const invocation = createCodegraphInvocation({
112
301
  subcommand: 'init',
113
- project: options.project,
114
- ...(options.yes === true ? { yes: true } : {})
302
+ project: options.project
115
303
  });
116
304
  const result = await executeCodegraphInvocation(invocation);
117
305
  const didFail = result.exitCode !== null && result.exitCode !== 0;
@@ -137,7 +325,47 @@ async function runCodegraphInitCommand(io, options, asJson) {
137
325
  catch {
138
326
  // intentionally swallowed — surface as warning below
139
327
  }
140
- printResult(io, ok('codegraph.init', { guard: guardOutcome.status, codegraphDir: guardOutcome.codegraphDir, markerWritten: true }, [], [`Stamped peaks-loop marker at ${guardOutcome.codegraphDir}/.peaks-loop-marker`]), asJson);
328
+ // Upstream `init` writes its 99-rule default `exclude` template,
329
+ // some of which collide with real source directories in this
330
+ // project. Left alone, a fresh clone / new machine gets an index
331
+ // that silently omits tracked source files while `status` says it
332
+ // is up to date. Reconcile now, drop the offending rules, and
333
+ // rebuild the index — a fresh init is the one moment this is both
334
+ // safe (nothing has been indexed yet) and necessary (`.codegraph/`
335
+ // is gitignored, so every clone starts from the default template).
336
+ //
337
+ // Never throws: a failure here is reported as a warning, not a
338
+ // failed init (the init itself already succeeded).
339
+ const excludeRepair = await repairCodegraphExcludeFromProject(projectRoot);
340
+ // These are confirmations, not warnings: `printResult` renders every
341
+ // `warnings` entry to stderr behind a `warning: ` prefix, so a fully
342
+ // successful init used to print a wall of `warning:` lines for what
343
+ // were plain success messages.
344
+ const initNotes = [
345
+ `Stamped peaks-loop marker at ${guardOutcome.codegraphDir}/.peaks-loop-marker`
346
+ ];
347
+ if (excludeRepair.applied) {
348
+ initNotes.push(`Removed ${excludeRepair.rulesRemoved.length} exclude rule(s) that blocked tracked source files, recovering ${excludeRepair.filesRecovered} file(s); config backed up to ${excludeRepair.backupPath}.`);
349
+ if (excludeRepair.reindexed) {
350
+ initNotes.push('Rebuilt the codegraph index over the recovered files.');
351
+ }
352
+ }
353
+ printResult(io, ok('codegraph.init', {
354
+ guard: guardOutcome.status,
355
+ codegraphDir: guardOutcome.codegraphDir,
356
+ markerWritten: true,
357
+ excludeRepair: {
358
+ applied: excludeRepair.applied,
359
+ rulesRemoved: excludeRepair.rulesRemoved,
360
+ filesRecovered: excludeRepair.filesRecovered,
361
+ reindexed: excludeRepair.reindexed,
362
+ backupPath: excludeRepair.backupPath,
363
+ warning: excludeRepair.warning
364
+ }
365
+ },
366
+ // Verbatim: `printResult` supplies the `warning: ` prefix, so a
367
+ // prefix added here would render as `warning: warning: ...`.
368
+ excludeRepair.warning === null ? [] : [excludeRepair.warning], initNotes), asJson);
141
369
  }
142
370
  catch (error) {
143
371
  printCodegraphFailure(io, 'codegraph.init', error, asJson);
@@ -211,8 +439,11 @@ async function runCodegraphAffectedCommand(io, files, options, asJson) {
211
439
  }
212
440
  export function registerCodegraphCommands(program, io) {
213
441
  const codegraph = program.command('codegraph').description('Run upstream codegraph commands through the Peaks launcher');
214
- addProjectOption(codegraph.command('status').description('Show codegraph status')).action((options) => runCodegraphCommand(io, 'codegraph.status', { subcommand: 'status', project: options.project }, options.peaksJson));
215
- addProjectOption(codegraph.command('init').description('Initialize codegraph for a project').option('--yes', 'answer yes to upstream prompts')).action((options) => runCodegraphInitCommand(io, options, options.peaksJson));
442
+ addProjectOption(codegraph.command('status').description('Show codegraph status, including the exclude integrity gate')).action((options) => runCodegraphStatusCommand(io, options, options.peaksJson));
443
+ addProjectOption(codegraph
444
+ .command('repair-exclude')
445
+ .description('Drop codegraph exclude rules that block tracked source files, then rebuild the index')).action((options) => runCodegraphRepairExcludeCommand(io, options, options.peaksJson));
446
+ addProjectOption(codegraph.command('init').description('Initialize codegraph for a project')).action((options) => runCodegraphInitCommand(io, options, options.peaksJson));
216
447
  addProjectOption(codegraph
217
448
  .command('index')
218
449
  .description('Index a project with codegraph')
@@ -1,14 +1,21 @@
1
1
  /**
2
- * W5 Fix M2 — `peaks prepare-final-review <rid>` CLI wrapper.
2
+ * `peaks prepare-final-review <rid>` CLI wrapper (W5 Fix M2; real provider
3
+ * binding added by the S3 defect-remediation slice).
3
4
  *
4
- * Exposes the `prepareFinalReview()` service (added in W2 T9 on
5
+ * Exposes the `prepareFinalReview()` service (W2 T9 on
5
6
  * `feature/slice-topology-multipass`) via the CLI surface. The service
6
- * depends on an injected `LlmRunner`; this slice wires the CLI route
7
- * with a `stub` provider that returns a structured "scaffold ready"
8
- * envelope so CI can verify the route without a real LLM. A follow-up
9
- * slice will bind a real provider. Until then, non-stub providers fail
10
- * loudly with `LLM_PROVIDER_NOT_IMPLEMENTED` so callers cannot silently
11
- * no-op.
7
+ * depends on an injected `LlmRunner`; this file owns the binding:
8
+ * - `--llm-provider stub` (default) returns a structured "scaffold ready"
9
+ * envelope WITHOUT calling the service, so CI can verify the route
10
+ * offline. It performs no review, and says so in every hint it emits.
11
+ * - `--llm-provider anthropic` binds the real Messages-API runner
12
+ * (`resolveAnthropicConfig()` + `createAnthropicRunner()`) and runs the
13
+ * service for real, carrying the 4-dim result back in the envelope.
14
+ * An absent credential or model raises `LlmBindingError`, reported under
15
+ * its own error code — never degraded into a scaffold a caller could
16
+ * mistake for a review.
17
+ * Unknown provider names still fail loudly with
18
+ * `LLM_PROVIDER_NOT_IMPLEMENTED` rather than silently falling back to stub.
12
19
  *
13
20
  * Per the dev-preference "Default-no on new CLI commands" rule and the
14
21
  * W4 T14 spec, this is a NEW top-level command (`prepare-final-review`),
@@ -19,13 +26,30 @@
19
26
  */
20
27
  import { Command } from 'commander';
21
28
  import { type ProgramIO } from '../cli-helpers.js';
22
- export type FinalReviewStatus = 'scaffold-only' | 'not-applicable';
29
+ import type { FinalReviewOutput } from '../../services/final-review/final-review-types.js';
30
+ export type FinalReviewStatus = 'scaffold-only' | 'review-complete' | 'not-applicable';
31
+ /**
32
+ * Which LLM produced this envelope. `unknown` is reserved for failure
33
+ * envelopes, where no binding was ever established.
34
+ */
35
+ export type FinalReviewProviderBinding = 'stub' | 'anthropic-messages-api' | 'unknown';
23
36
  export interface FinalReviewData {
24
37
  readonly status: FinalReviewStatus;
25
38
  readonly rid: string;
26
39
  readonly sessionId: string;
27
40
  readonly auditGoalPath: string;
28
41
  readonly serviceWired: boolean;
29
- readonly providerBinding: 'pending-follow-up-slice' | 'unknown';
42
+ readonly providerBinding: FinalReviewProviderBinding;
43
+ /** Model id the bound provider answered with (real-provider runs only). */
44
+ readonly model?: string;
45
+ /** The 4-dim review the service produced (real-provider runs only). */
46
+ readonly review?: FinalReviewOutput;
47
+ /**
48
+ * Environment variables that were absent when binding failed. Surfaced on
49
+ * `data` because `fail()` redacts `message` through
50
+ * `redactSensitiveErrorMessage`, whose catch-all pattern matches the words
51
+ * `token` / `api_key` and would blank out the very names an operator needs.
52
+ */
53
+ readonly missingEnv?: readonly string[];
30
54
  }
31
55
  export declare function registerFinalReviewCommands(program: Command, io: ProgramIO): void;