peaks-loop 4.0.42 → 4.0.44

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 (76) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README-en.md +1 -1
  3. package/README.md +1 -1
  4. package/dist/cli/commands/_register.js +4 -0
  5. package/dist/cli/commands/api-diff-commands.d.ts +16 -0
  6. package/dist/cli/commands/api-diff-commands.js +55 -0
  7. package/dist/cli/commands/audit-commands.d.ts +16 -3
  8. package/dist/cli/commands/audit-commands.js +84 -31
  9. package/dist/cli/commands/codegraph-commands.js +191 -6
  10. package/dist/cli/commands/final-review-commands.d.ts +34 -10
  11. package/dist/cli/commands/final-review-commands.js +130 -34
  12. package/dist/cli/commands/job-commands.js +4 -2
  13. package/dist/cli/commands/scan-commands.js +1 -1
  14. package/dist/cli/commands/share-commands.d.ts +49 -0
  15. package/dist/cli/commands/share-commands.js +114 -14
  16. package/dist/cli/commands/test-commands.d.ts +60 -3
  17. package/dist/cli/commands/test-commands.js +125 -7
  18. package/dist/services/audit/audit-goal-service.js +38 -3
  19. package/dist/services/codegraph/codegraph-autorefresh.js +12 -0
  20. package/dist/services/codegraph/codegraph-exclude-integrity.d.ts +61 -0
  21. package/dist/services/codegraph/codegraph-exclude-integrity.js +98 -0
  22. package/dist/services/codegraph/codegraph-exclude-reconciler.d.ts +26 -0
  23. package/dist/services/codegraph/codegraph-exclude-reconciler.js +217 -0
  24. package/dist/services/codegraph/codegraph-exclude-repair.d.ts +102 -0
  25. package/dist/services/codegraph/codegraph-exclude-repair.js +266 -0
  26. package/dist/services/codegraph/codegraph-preflight-service.js +12 -0
  27. package/dist/services/codegraph/codegraph-service.d.ts +0 -1
  28. package/dist/services/codegraph/codegraph-service.js +5 -4
  29. package/dist/services/doctor/doctor-service/checks/codegraph-exclude-integrity.d.ts +29 -0
  30. package/dist/services/doctor/doctor-service/checks/codegraph-exclude-integrity.js +88 -0
  31. package/dist/services/doctor/doctor-service/checks/ecc-hooks-schema-drift.d.ts +65 -0
  32. package/dist/services/doctor/doctor-service/checks/ecc-hooks-schema-drift.js +186 -0
  33. package/dist/services/doctor/doctor-service/plugin-registry.js +4 -0
  34. package/dist/services/doctor/doctor-service/types.d.ts +47 -0
  35. package/dist/services/final-review/final-review-service.d.ts +154 -0
  36. package/dist/services/final-review/final-review-service.js +621 -7
  37. package/dist/services/final-review/index.d.ts +1 -1
  38. package/dist/services/final-review/index.js +1 -1
  39. package/dist/services/llm/anthropic-runner.d.ts +87 -0
  40. package/dist/services/llm/anthropic-runner.js +171 -0
  41. package/dist/services/llm/stub-runner.d.ts +11 -0
  42. package/dist/services/llm/stub-runner.js +33 -0
  43. package/dist/services/prd/handoff-auto-regen.js +0 -1
  44. package/dist/services/prd/handoff-service.d.ts +9 -1
  45. package/dist/services/prd/handoff-service.js +48 -6
  46. package/dist/services/prd/project-scan-bootstrap-service.js +7 -7
  47. package/dist/services/scan/api-diff-openapi.d.ts +32 -0
  48. package/dist/services/scan/api-diff-openapi.js +359 -0
  49. package/dist/services/scan/api-diff-recorded.d.ts +96 -0
  50. package/dist/services/scan/api-diff-recorded.js +577 -0
  51. package/dist/services/scan/api-diff-service.d.ts +34 -0
  52. package/dist/services/scan/api-diff-service.js +407 -0
  53. package/dist/services/scan/api-diff-types.d.ts +116 -0
  54. package/dist/services/scan/api-diff-types.js +46 -0
  55. package/dist/services/scan/archetype-service.js +27 -1
  56. package/dist/services/scan/existing-system-service.js +17 -4
  57. package/dist/services/scan/hook-convention-service.d.ts +26 -0
  58. package/dist/services/scan/hook-convention-service.js +562 -0
  59. package/dist/services/scan/scan-types.d.ts +47 -0
  60. package/dist/services/session/caller-binding-service.d.ts +28 -0
  61. package/dist/services/session/caller-binding-service.js +10 -2
  62. package/dist/services/session/caller-id-types.d.ts +12 -2
  63. package/dist/services/session/index.d.ts +2 -2
  64. package/dist/services/session/index.js +2 -2
  65. package/dist/services/session/session-binding-bridge.js +11 -6
  66. package/dist/services/session/session-manager.d.ts +33 -1
  67. package/dist/services/session/session-manager.js +84 -25
  68. package/dist/services/skills/skill-presence-service.d.ts +17 -3
  69. package/dist/services/skills/skill-presence-service.js +23 -3
  70. package/package.json +7 -5
  71. package/skills/bee/peaks-rd/SKILL.md +11 -3
  72. package/skills/peaks-code/references/existing-system-extraction.md +5 -1
  73. package/skills/peaks-code/references/frontend-only-mode.md +48 -6
  74. package/skills/peaks-code/references/project-scan-checklist.md +20 -1
  75. package/skills/peaks-doctor/references/doctor-check-catalog.md +1 -0
  76. package/skills/peaks-final-review/SKILL.md +43 -32
package/CHANGELOG.md CHANGED
@@ -1,5 +1,64 @@
1
1
  # Changelog
2
2
 
3
+ ## 4.0.44 — 2026-09-12 (被静默排除的源文件 + 一个验错东西的验收闸)
4
+
5
+ **Highlights**:
6
+
7
+ 1. **`peaks codegraph status` 报"索引是最新的",而 26 个 git 跟踪的源文件根本不在索引里。** 上游默认 exclude 表**按目录名**匹配,而本仓库恰好把 `artifacts/` `release/` `vendor/` `bin/` `publish/` 用作了源码目录名,于是 5 条默认规则把 26 个真实源文件挡在门外。`status` 说的是"图与上次扫描一致",不是"图覆盖了仓库" —— 所以这个洞是隐形的。
8
+
9
+ 本版按**一条原则**建了 reconcile / repair / verify:被 git 跟踪的源文件不得被 exclude 规则挡住。
10
+
11
+ - 对账用 `picomatch`(从传递依赖提升为直接依赖),与上游**同一个匹配引擎**;此前自实现的 glob 对 `{}` 模式**漏报**,且会灾难性回溯
12
+ - `status` 现在**失败(exit 74)**并指名违规规则与文件,而不是报 OK;doctor 也报这个缺口
13
+ - `init` 自愈,且 preflight / autorefresh 走**同一个 helper** —— 它们此前会跑上游 init 并盖 marker 却**不做修复**,使"全新 clone 自愈"在真实流程里**不可达**
14
+ - 匹配不到任何 tracked 文件的规则**永不删除**;未跟踪文件仍被排除(原语义不变)
15
+
16
+ 2. **4 维人工验收闸干不了活 —— 四层独立失效,每一层都只有真机跑才看得见。**
17
+
18
+ - **(a)** 服务只把目标的 `successCriteria` 喂给模型,**一条证据都不给**,却要求它产出**带证据的裁决**。现在从磁盘收集真实证据(有界),并用**结构而非提示词措辞**保证:支撑源全部缺失的 `pass` 一律降级为 `inconclusive`
19
+ - **(b)** CLI 只能跑 `stub`;`--llm-provider anthropic` 现在真的构造 runner,`providerBinding` 如实上报
20
+ - **(c)** `maxTokens` 硬编码 3000,低于一份完整证据包所需,回复被**截在 JSON 中间**;预算随内联证据量伸缩,截断被诊断为"输出预算失败"而非"JSON 非法",并给出 `PEAKS_FINAL_REVIEW_MAX_OUTPUT_TOKENS` 逃生阀
21
+ - **(d)** 证据预算是**先到先得**,于是最后排序的源 —— 也就是四个维度里的一个 —— 被**永久饿死**;现在每个维度有保留配额
22
+
23
+ ⚠️ **`existing-functionality-intact` 仍然无法通过**:它要求的 `pre-post-diff` 产物**全仓没有任何生产者**。这是缺功能,不是 bug;已在 SKILL.md 记为**工具状态**,而**不是**把一个不相关的源重映射进去把闸弄绿。
24
+
25
+ 3. **同一个"闸门验代理而非性质"的形状,又抓到三处。**
26
+
27
+ - `handoff-auto-regen` 把 `sessionId` **写了两遍**,产出的 frontmatter 仓库自己的 YAML 解析器**拒绝** —— 而 `AUDIT_REQUIRES_HANDOFF` 只做**子串**检查,所以放行
28
+ - `sub-agent finalize --request-id` 扫会话目录下**所有** `.json`,读到 `active-dispatches.json` 就抛;两个分支现在都容忍坏记录,且 `--request-id` 与 `--batch` 共用**优先 queued** 的选择逻辑,并报出选了哪条、为何跳过其它
29
+ - `skills/peaks-final-review/SKILL.md` 断言它自己的 CLI "**尚不存在**",让调用方去手写 service 调用 —— 而它存在且已注册。这句话在本版开发中**真实地浪费了工作**:orchestrator 为一个早已存在的命令手写了一个调用脚本
30
+
31
+ **验证**:三个版本常量一致(**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。
32
+
33
+ **已知未修**:`existing-functionality-intact` 缺证据生产者(见第 2 条);`RUNTIME_NPM_VERSION`(0.0.21)与 internal-runtime 包版本已不同步(4.0.43 起即如此)。
34
+
35
+ ## 4.0.43 — 2026-09-12 (一个从来拦不住东西的闸门 + 前端接口防腐层)
36
+
37
+ **Highlights**:
38
+
39
+ 1. **`peaks audit goal` 是门面 —— 现在是真的了。** 它是所有 peaks-* 工作流的入口闸门:把一个需求变成 6 维审计加一个目标,并在审计不完整时**拒绝**让自主工作继续。而它无论输入什么都返回同一个 `scaffold-only` 空壳,所以它什么都没拦。校验服务本身是好的,**缺的只是 provider 绑定**。本版建了 peaks-loop 的第一个 LLM 客户端:读会话已经带着的环境(`ANTHROPIC_BASE_URL`;`ANTHROPIC_AUTH_TOKEN` 或 `ANTHROPIC_API_KEY`;`ANTHROPIC_MODEL` 或 `CLAUDE_CODE_SUBAGENT_MODEL`),走全局 `fetch`,**零新增运行时依赖**。
40
+
41
+ - **一个静默回退会让这个闸门和以前一样假**,所以凭据缺失时它响亮失败:exit 1,并指名缺失的那个变量。`stub` 保留给 CI,且明确标注自己是 stub。
42
+ - 真机跑出的第一份审计暴露了第二个洞:**提示词从没写过 `severity` 的合法取值**,模型于是自己发明了 `high` —— 而闸门放它过去了。也就是说它验了**覆盖**和**字段存在**,没验**取值合法**。现在提示词明写合法值,解析器真校验,并**从不规整坏值**:把坏值改写成好值,正是让枚举变成装饰品的做法。
43
+
44
+ 2. **同一个项目开两个窗口会互相串 session —— 已修。** `getCurrentSessionId` 只读一个项目级全局文件,最后开窗口的赢;而 **17 个命令**从它解析 session(job / dispatch / worktree / share / vm / web / slice-* …)。报出来的症状只是无害的那一半(`peaks job status` 报 `JOB_NOT_IN_SESSION`);有害的一半是**两个窗口各有同名 job 或 slice 时,写入落到另一个窗口的目录树里,而且不报错**。现在 caller 优先 —— 由 `peaks session info --active` 用的同一个来源解析 —— 没有绑定时回退全局文件,所以 CI 与非 IDE 调用的行为逐字节不变,且**17 个调用点一行未改**。
45
+
46
+ - 对抗 QA 随后发现底下的信任模型没有防护:绑定指向的 session 目录已不存在时仍被信任,而轮转从不清理绑定,于是轮转后 `peaks job init` 会解析到旧 id、并在其下重建 job 目录树。现在绑定分 `bound | stale | absent`,目录不存在即视为陈旧,且**回落这件事是可观测的**。
47
+ - `lastActivityAt` **删除而非启用**,理由值得记下:它的写入侧本来就是坏的(只在首次绑定和显式重绑时写、复用时不写),所以时间戳在一个**活着的**窗口上照常变老 —— 做 TTL 反而会把"活着但空闲"的窗口解绑,复活刚修掉的那个串档。而时钟也看不见它报的两个缺陷(目录没了、轮转)。
48
+
49
+ 3. **前端/全栈的接口防腐层。** 新增 `peaks scan api-diff <doc>`:解析 OpenAPI 3.x 文档,与项目**已经**产出的三份东西对账(mock-plan、`*-api.types.ts`、TXT handoff 端点清单),输出分成**精确段**(两侧都解析过)与**候选段**(名字 grep,明标会过报漏报)。不建新产物、不做 codegen、不加运行时依赖。
50
+
51
+ - 关键取舍写在**输出里**而不是文档里:**生成的类型只能保证"文档↔代码",永远保证不了"文档↔服务器"。** 如果文档本身过时,类型、客户端、mapper 会从**同一份错文档**重新生成、**编译干净通过**、页面渲染 `undefined` —— 而"文档到手"正是这个需求的触发点。所以**诚实的边界是功能的一部分**,不是免责声明。
52
+ - 两侧遵循同一条规则:**读不全就不说。** 任何无法完整读取的接口或位置都被压制,并附一条能定位到文件/接口/操作 + 原因的 note。这条规则必须同时应用在记录侧**和**文档侧 —— 两者不对称是最后被发现的那处结构性缺口。
53
+ - 配套:项目扫描新增**三态**集成模式(`full-stack` / `prd-plus-interface-doc` / `prd-only`,从已有信号确定性推导,不新增探测)、缺失的 `## API` 扫描节、RD **按模式路由**(否则三态只是个没人读的死字段)、以及读取 hook **内容**并报告观察到的约定,以**不一致**为信号。
54
+ - 明确**不做**的事:不检测"组件直接消费 DTO"。那是**类型流**性质,正则表达式只会命中唯一合法的 mapper import、却漏掉真正的违规 —— 一条会打偏的规则比没有规则更糟。
55
+
56
+ 4. **`peaks test <file>` 在 Windows 上从来就没跑通过。** 它 spawn 裸名 `vitest`,而 Windows 上 runner 是 `node_modules/.bin/vitest.cmd` —— 而**这个命令自己的文档**写着"会替你解析本地二进制,Windows 感知"。现在解析项目本地 runner。两种形式都是**实测而非猜测**:直接 spawn `.CMD` 返回 `EINVAL`,而那个"显然的修法" `{shell:true}` 会**破坏 argv**(`tests/a b/x.test.ts` 会变成 3 个参数),所以走显式 `cmd.exe`。找不到 runner 时列出**每一条探过的路径**加修法,而不是抛裸 `ENOENT`。
57
+
58
+ 5. **一处三方分歧:生成的 `frontendOnly` 写在一个标题下、模板写在另一个、读取方从第三个读。** 实际是**四方** —— 找出来的第四方是一份**写入指令**,不改它,下一次 LLM 写的扫描就会重新制造这个漂移。生成器已对齐,新守卫**同时断言该行存在、且在旧位置缺席**(只断言存在的守卫,正是这一整类缺陷反复出厂的原因)。
59
+
60
+ **验证**:三个版本常量一致(**4.0.43**);`tsc -p tsconfig.build.json` exit 0;宽 `tsconfig.json` 保持 **142** 基线;`tests/unit` **201 files / 1975 passed / 3 skipped / 0 failed**;`pnpm build` 的 `build-integrity` OK。
61
+
3
62
  ## 4.0.42 — 2026-09-12 (peaks 自己的钩子在每次编辑时报错)
4
63
 
5
64
  **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.42 (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.44 (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.42(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.44(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 |
@@ -60,6 +60,7 @@ import { registerReviewerCommands } from './reviewer-commands.js';
60
60
  import { registerRoleCommands } from './role-commands.js';
61
61
  import { registerRuntimeCommands } from './runtime-commands.js';
62
62
  import { registerScanCommands } from './scan-commands.js';
63
+ import { registerApiDiffCommands } from './api-diff-commands.js';
63
64
  import { registerShadcnCommands } from './shadcn-commands.js';
64
65
  import { registerSecurityAuditCommands } from './security-audit-commands.js';
65
66
  import { registerSedimentCommands } from './sediment-commands.js';
@@ -99,6 +100,9 @@ const REGISTRATIONS = [
99
100
  ['project-commands', registerProjectCommands], ['prd-commands', registerPrdCommands],
100
101
  ['request-commands', registerRequestCommands], ['retrospective-commands', registerRetrospectiveCommands],
101
102
  ['scan-commands', registerScanCommands], ['shadcn-commands', registerShadcnCommands],
103
+ // Registered after `scan-commands` on purpose: it attaches `scan api-diff`
104
+ // to the existing parent instead of creating a second `scan` command.
105
+ ['api-diff-commands', registerApiDiffCommands],
102
106
  ['slice-commands', registerSliceCommands],
103
107
  ['sop-commands', registerSopCommands], ['feedback-commands', registerFeedbackCommands],
104
108
  ['fork-commands', registerForkCommands], ['impact-commands', registerImpactCommands],
@@ -0,0 +1,16 @@
1
+ /**
2
+ * S1 / rid=api-diff-report — `peaks scan api-diff <doc>`.
3
+ *
4
+ * Group choice: `scan`, not a new top-level `api` group. `scan` already hosts
5
+ * `api-surface` — a read-only, `--project`-scoped API analysis that writes no
6
+ * artifact — so `api-diff` is a sibling of an existing command with identical
7
+ * semantics and an identical option shape. A new `peaks api` group would be a
8
+ * brand-new verb family whose cost the design (docs/superpowers/specs/
9
+ * 2026-09-12-frontend-acl-contract-design.md §3/§5) records as medium and
10
+ * acknowledged; nothing about this slice needs it.
11
+ *
12
+ * The command is read-only: it creates no contract artifact and writes no file.
13
+ */
14
+ import type { Command } from 'commander';
15
+ import { type ProgramIO } from '../cli-helpers.js';
16
+ export declare function registerApiDiffCommands(program: Command, io: ProgramIO): void;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * S1 / rid=api-diff-report — `peaks scan api-diff <doc>`.
3
+ *
4
+ * Group choice: `scan`, not a new top-level `api` group. `scan` already hosts
5
+ * `api-surface` — a read-only, `--project`-scoped API analysis that writes no
6
+ * artifact — so `api-diff` is a sibling of an existing command with identical
7
+ * semantics and an identical option shape. A new `peaks api` group would be a
8
+ * brand-new verb family whose cost the design (docs/superpowers/specs/
9
+ * 2026-09-12-frontend-acl-contract-design.md §3/§5) records as medium and
10
+ * acknowledged; nothing about this slice needs it.
11
+ *
12
+ * The command is read-only: it creates no contract artifact and writes no file.
13
+ */
14
+ import { ApiDiffInputError, diffApiDocument, formatApiDiffText } from '../../services/scan/api-diff-service.js';
15
+ import { fail, ok } from 'peaks-loop-shared/result';
16
+ import { addJsonOption, printResult } from '../cli-helpers.js';
17
+ export function registerApiDiffCommands(program, io) {
18
+ // Reuse the existing `scan` parent — the add-a-new-subcommand-check-for-
19
+ // existing-top-level-first rule (same guard as bee-commands / asset-commands).
20
+ const scan = program.commands.find((c) => c.name() === 'scan') ?? program
21
+ .command('scan')
22
+ .description('Read-only project scans for tech-doc and RD handoffs');
23
+ addJsonOption(scan
24
+ .command('api-diff')
25
+ .description('Diff an OpenAPI 3.x document (.json/.yaml/.yml) against what this project already recorded — ' +
26
+ 'the mock-plan, the recorded *-api.types.ts interfaces, and the TXT handoff endpoint list. ' +
27
+ 'Read-only: creates no contract artifact. Output separates an Exact section (both sides parsed) ' +
28
+ 'from a Candidate mentions section (name-grep, may over- and under-report), and always prints ' +
29
+ 'what this command cannot detect.')
30
+ .argument('<doc>', 'path to the OpenAPI 3.x document (relative to --project, or absolute)')
31
+ .option('--project <path>', 'consumer project root (default: cwd)')).action((doc, options) => {
32
+ const projectRoot = options.project ?? process.cwd();
33
+ const asJson = options.json ?? false;
34
+ try {
35
+ const report = diffApiDocument({ projectRoot, docPath: doc });
36
+ const nextActions = report.notes.length > 0
37
+ ? ['Read the notes: at least one recorded source was missing, so the Exact section is partial by construction.']
38
+ : [];
39
+ if (asJson) {
40
+ printResult(io, ok('scan.api-diff', report, [], nextActions), true);
41
+ return;
42
+ }
43
+ io.stdout(`${formatApiDiffText(report)}\n`);
44
+ }
45
+ catch (error) {
46
+ // A non-OpenAPI input must exit non-zero AND produce no diff output — a
47
+ // silent empty report is the exact failure this command exists to prevent.
48
+ const code = error instanceof ApiDiffInputError ? error.code : 'API_DIFF_FAILED';
49
+ printResult(io, fail('scan.api-diff', code, error.message, { document: doc }, [
50
+ 'Pass a path to an OpenAPI 3.x document: a top-level `openapi: 3.x` string plus a non-empty `paths` object.'
51
+ ]), asJson);
52
+ process.exitCode = 1;
53
+ }
54
+ });
55
+ }
@@ -10,14 +10,27 @@
10
10
  import { Command } from 'commander';
11
11
  import { type AgentShieldState } from '../../services/audit/static-service.js';
12
12
  import { type ProgramIO } from '../cli-helpers.js';
13
+ import type { AuditGoalOutput } from '../../services/audit/audit-goal-types.js';
13
14
  import type { RedLineAudit } from '../../services/audit/types.js';
14
15
  import { type AuditDecisionRecord } from '../../services/audit/decision-writer.js';
16
+ /** `audit-failed` is a failure envelope's status — never a scaffold, never a success. */
17
+ export type AuditGoalStatus = 'audit-complete' | 'scaffold-only' | 'audit-failed';
15
18
  export interface AuditGoalData {
16
- readonly status: 'scaffold-only';
17
- readonly serviceWired: true;
18
- readonly providerBinding: 'pending-follow-up-slice';
19
+ readonly status: AuditGoalStatus;
20
+ /** Which LLM produced (or failed to produce) the audit. `unresolved` = rejected before binding. */
21
+ readonly providerBinding: 'anthropic-messages-api' | 'stub' | 'unresolved';
19
22
  readonly need: string;
20
23
  readonly projectRoot: string;
24
+ /** The validated 6-dimension audit. Present on success only. */
25
+ readonly result?: AuditGoalOutput;
26
+ /** The bound model. Present on a real run only. */
27
+ readonly model?: string;
28
+ /**
29
+ * Environment variables the binding needed and did not find. Present on a
30
+ * binding failure only, and verbatim: `fail()` redacts `message`, so this
31
+ * is the channel that reliably names what the operator must set.
32
+ */
33
+ readonly missingEnv?: readonly string[];
21
34
  }
22
35
  export interface StaticAuditData {
23
36
  readonly audit: RedLineAudit;
@@ -11,6 +11,9 @@ import { existsSync, readFileSync, statSync } from 'node:fs';
11
11
  import { resolve } from 'node:path';
12
12
  import { runRedLinesAudit } from '../../services/audit/red-lines-service.js';
13
13
  import { runStaticAudit } from '../../services/audit/static-service.js';
14
+ import { auditGoal, IncompleteAuditError } from '../../services/audit/audit-goal-service.js';
15
+ import { createAnthropicRunner, LlmBindingError, LlmRequestError, resolveAnthropicConfig, } from '../../services/llm/anthropic-runner.js';
16
+ import { createStubRunner } from '../../services/llm/stub-runner.js';
14
17
  import { addJsonOption, getErrorMessage, printResult } from '../cli-helpers.js';
15
18
  import { fail, ok } from 'peaks-loop-shared/result';
16
19
  import { writeAuditDecision } from '../../services/audit/decision-writer.js';
@@ -26,7 +29,13 @@ function isSupportedArtifactKind(value) {
26
29
  return SUPPORTED_ARTIFACT_KINDS.includes(value);
27
30
  }
28
31
  /** Whitelist of supported `--llm-provider` values for `peaks audit goal`. */
29
- const SUPPORTED_LLM_PROVIDERS = ['stub'];
32
+ const SUPPORTED_LLM_PROVIDERS = ['anthropic', 'stub'];
33
+ /**
34
+ * The real provider is the default: `peaks audit goal` is the entry gate for
35
+ * every peaks-* workflow, so it must audit by default and only scaffold when
36
+ * a caller explicitly asks for `stub`.
37
+ */
38
+ const DEFAULT_LLM_PROVIDER = 'anthropic';
30
39
  function isSupportedLlmProvider(value) {
31
40
  return SUPPORTED_LLM_PROVIDERS.includes(value);
32
41
  }
@@ -195,47 +204,69 @@ export function registerAuditCommands(program, io) {
195
204
  process.exitCode = 1;
196
205
  }
197
206
  });
198
- // Fix M1 (W5) — `peaks audit goal` CLI wrapper around `auditGoal()`.
199
- // The service is correctly implemented but is NOT yet wired to a real
200
- // LLM provider; this CLI exposes the route with a `stub` provider that
201
- // returns a scaffold envelope. A follow-up slice will bind a real
202
- // provider. Until then, non-stub providers fail loudly with
203
- // `LLM_PROVIDER_NOT_IMPLEMENTED` so callers cannot silently no-op.
207
+ // Slice 2026-09-12-llm-provider-binding — `peaks audit goal` now runs the
208
+ // gate it advertises. `auditGoal()` was already correct (one `LlmRunner`
209
+ // call, 6-dimension validation, `IncompleteAuditError` on a partial audit);
210
+ // what was missing was a provider binding, so the command answered with a
211
+ // fixed `scaffold-only` envelope no matter what it was asked.
212
+ //
213
+ // `anthropic` is now the default and reads the session's own environment
214
+ // (see `resolveAnthropicConfig`). `stub` stays for CI/tests but is
215
+ // reported as a scaffold, and a missing credential fails loudly — a silent
216
+ // fall back to the scaffold envelope would leave the gate exactly as fake
217
+ // as it was before this slice.
204
218
  addJsonOption(audit
205
219
  .command('goal')
206
220
  .description('Audit a human need across 6 dimensions and propose a goal (peaks-audit primitive)')
207
221
  .requiredOption('--project <path>', 'target project root')
208
222
  .requiredOption('--need <text>', 'the human need to audit (becomes input.need for auditGoal())')
209
- .option('--llm-provider <name>', 'LLM provider name (default: stub)', 'stub')).action(async (options) => {
223
+ .option('--llm-provider <name>', `LLM provider (${SUPPORTED_LLM_PROVIDERS.join(' | ')}); stub performs no audit`, DEFAULT_LLM_PROVIDER)).action(async (options) => {
210
224
  const validation = validateProjectRoot(options.project);
211
225
  if (!validation.ok) {
212
- printResult(io, fail('audit.goal', validation.code, validation.message, emptyAuditGoalData(options.need, options.project), ['Verify the project path exists and is a directory']), options.json);
226
+ printResult(io, fail('audit.goal', validation.code, validation.message, auditGoalFailureData(options.need, options.project), ['Verify the project path exists and is a directory']), options.json);
213
227
  process.exitCode = 1;
214
228
  return;
215
229
  }
216
- const provider = options.llmProvider ?? 'stub';
230
+ const provider = options.llmProvider ?? DEFAULT_LLM_PROVIDER;
217
231
  if (!isSupportedLlmProvider(provider)) {
218
- printResult(io, fail('audit.goal', 'LLM_PROVIDER_NOT_IMPLEMENTED', `LLM provider "${provider}" is not implemented. Supported providers: ${SUPPORTED_LLM_PROVIDERS.join(', ')}.`, emptyAuditGoalData(options.need, validation.projectRoot), [
219
- 'Re-run with `--llm-provider stub` (default) to exercise the wired route.',
220
- 'Real provider binding is tracked as a follow-up slice; see peaks-audit skill notes.'
232
+ printResult(io, fail('audit.goal', 'LLM_PROVIDER_NOT_IMPLEMENTED', `LLM provider "${provider}" is not implemented. Supported providers: ${SUPPORTED_LLM_PROVIDERS.join(', ')}.`, auditGoalFailureData(options.need, validation.projectRoot), [
233
+ `Re-run with \`--llm-provider ${DEFAULT_LLM_PROVIDER}\` for a real audit, or \`--llm-provider stub\` for an offline scaffold.`
221
234
  ]), options.json);
222
235
  process.exitCode = 1;
223
236
  return;
224
237
  }
225
- // Stub provider: surface a structured "scaffold ready" envelope so the
226
- // CLI route is wired and a CI test can verify it without a real LLM.
227
- const data = {
228
- status: 'scaffold-only',
229
- serviceWired: true,
230
- providerBinding: 'pending-follow-up-slice',
231
- need: options.need,
232
- projectRoot: validation.projectRoot,
233
- };
234
- const envelope = ok('audit.goal', data, [], [
235
- 'auditGoal() service is wired and reachable. The stub provider returns a scaffold envelope so CI can verify the route without a real LLM.',
236
- 'A follow-up slice will bind a real LLM provider; until then, non-stub providers fail loudly with `LLM_PROVIDER_NOT_IMPLEMENTED`.'
237
- ]);
238
- printResult(io, envelope, options.json);
238
+ const isStub = provider === 'stub';
239
+ const providerBinding = isStub ? 'stub' : 'anthropic-messages-api';
240
+ try {
241
+ let model;
242
+ let llmRunner;
243
+ if (isStub) {
244
+ llmRunner = createStubRunner();
245
+ }
246
+ else {
247
+ const config = resolveAnthropicConfig();
248
+ model = config.model;
249
+ llmRunner = createAnthropicRunner(config);
250
+ }
251
+ const result = await auditGoal({ need: options.need }, llmRunner);
252
+ const data = {
253
+ status: isStub ? 'scaffold-only' : 'audit-complete',
254
+ providerBinding,
255
+ need: options.need,
256
+ projectRoot: validation.projectRoot,
257
+ result,
258
+ ...(model === undefined ? {} : { model }),
259
+ };
260
+ const envelope = ok('audit.goal', data, [], isStub
261
+ ? [`Stub provider: the 6 dimensions below are placeholders, not findings. Re-run with \`--llm-provider ${DEFAULT_LLM_PROVIDER}\` for a real audit.`]
262
+ : [`Audit produced by ${providerBinding}${model === undefined ? '' : ` (model: ${model})`}.`]);
263
+ printResult(io, envelope, options.json);
264
+ }
265
+ catch (error) {
266
+ const code = auditGoalErrorCode(error);
267
+ printResult(io, fail('audit.goal', code, getErrorMessage(error), auditGoalFailureData(options.need, validation.projectRoot, providerBinding, error instanceof LlmBindingError ? error.missingEnv : undefined), auditGoalNextActions(code)), options.json);
268
+ process.exitCode = 1;
269
+ }
239
270
  });
240
271
  // ---------------------------------------------------------------------------
241
272
  // peaks audit artifact write — Slice 2026-06-26-audit-artifact-writer-generalization
@@ -387,15 +418,37 @@ function emptyStaticAuditData() {
387
418
  },
388
419
  };
389
420
  }
390
- function emptyAuditGoalData(need, projectRoot) {
421
+ function auditGoalFailureData(need, projectRoot, providerBinding = 'unresolved', missingEnv) {
391
422
  return {
392
- status: 'scaffold-only',
393
- serviceWired: true,
394
- providerBinding: 'pending-follow-up-slice',
423
+ status: 'audit-failed',
424
+ providerBinding,
395
425
  need,
396
426
  projectRoot,
427
+ ...(missingEnv === undefined ? {} : { missingEnv }),
397
428
  };
398
429
  }
430
+ /** The slice-owned error codes are carried verbatim so callers can gate on them. */
431
+ function auditGoalErrorCode(error) {
432
+ if (error instanceof IncompleteAuditError || error instanceof LlmBindingError || error instanceof LlmRequestError) {
433
+ return error.code;
434
+ }
435
+ return 'AUDIT_GOAL_FAILED';
436
+ }
437
+ function auditGoalNextActions(code) {
438
+ switch (code) {
439
+ case 'LLM_CREDENTIAL_MISSING':
440
+ return [
441
+ 'Export ANTHROPIC_AUTH_TOKEN (or ANTHROPIC_API_KEY) in the environment that launches peaks, then re-run.',
442
+ 'For an offline scaffold instead of an audit, re-run with `--llm-provider stub` — it performs NO audit.',
443
+ ];
444
+ case 'LLM_MODEL_MISSING':
445
+ return ['Export ANTHROPIC_MODEL (or CLAUDE_CODE_SUBAGENT_MODEL) in the environment that launches peaks, then re-run.'];
446
+ case 'INCOMPLETE_AUDIT':
447
+ return ['The LLM reply omitted a required dimension; re-run so autonomous work never proceeds on a partial audit.'];
448
+ default:
449
+ return ['Inspect the failure above, then re-run with the same --need.'];
450
+ }
451
+ }
399
452
  function emptyProseRatioResult() {
400
453
  return {
401
454
  totalRedLines: 0,
@@ -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) {
@@ -61,6 +63,140 @@ async function runCodegraphCommand(io, command, options, asJson) {
61
63
  printCodegraphFailure(io, command, error, asJson);
62
64
  }
63
65
  }
66
+ /**
67
+ * `--peaks-json` machine report for `status`. Carries the upstream
68
+ * result AND the peaks-loop integrity verdict as one JSON document so a
69
+ * CI job can gate on `data.integrity.gap` / `data.integrity.rulesToRemove`
70
+ * without scraping human text.
71
+ */
72
+ async function runCodegraphStatusJson(io, options, integrity, integrityWarning) {
73
+ let result;
74
+ try {
75
+ result = await executeCodegraphInvocation(createCodegraphInvocation({ subcommand: 'status', project: options.project }));
76
+ }
77
+ catch (error) {
78
+ printCodegraphFailure(io, 'codegraph.status', error, true);
79
+ return;
80
+ }
81
+ const upstream = {
82
+ exitCode: result.exitCode,
83
+ stdout: rewriteBareCodegraphHints(result.stdout).trimEnd(),
84
+ stderr: redactSensitiveErrorMessage(rewriteBareCodegraphHints(result.stderr)).trimEnd()
85
+ };
86
+ const upstreamFailed = result.exitCode !== null && result.exitCode !== 0;
87
+ if (integrity?.gap === true) {
88
+ 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);
89
+ }
90
+ else if (upstreamFailed) {
91
+ 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);
92
+ }
93
+ else {
94
+ printResult(io, ok('codegraph.status', { upstream, integrity, integrityWarning }), true);
95
+ }
96
+ if (upstreamFailed) {
97
+ process.exitCode = result.exitCode ?? 1;
98
+ }
99
+ }
100
+ /**
101
+ * `peaks codegraph status` with an integrity gate.
102
+ *
103
+ * The upstream status is still proxied verbatim (that is what the
104
+ * command has always done), but a clean upstream "index is up to date"
105
+ * is no longer sufficient: when git-tracked source files are being
106
+ * excluded by the config, the command says so, names the rules and
107
+ * files, and exits non-zero.
108
+ *
109
+ * Read-only by construction — it imports the integrity inspector, never
110
+ * the repair writer. Fixing the config is `peaks codegraph init`
111
+ * (fresh) or `peaks codegraph repair-exclude` (explicit).
112
+ */
113
+ async function runCodegraphStatusCommand(io, options, asJson) {
114
+ let integrity = null;
115
+ let integrityWarning = null;
116
+ const projectRoot = resolve(options.project);
117
+ try {
118
+ // Never initialized here → no exclude list is in play, so there is
119
+ // nothing to report. Staying silent keeps `status` honest and
120
+ // unchanged for projects that do not use codegraph at all.
121
+ integrity = isCodegraphExcludeConfigPresent(projectRoot)
122
+ ? inspectCodegraphExcludeIntegrity(projectRoot)
123
+ : null;
124
+ }
125
+ catch (error) {
126
+ // Not a git work tree, no config yet, malformed config — the
127
+ // upstream status is still worth printing, so degrade to a warning
128
+ // instead of failing the whole command.
129
+ integrityWarning = getErrorMessage(error);
130
+ }
131
+ if (asJson === true) {
132
+ await runCodegraphStatusJson(io, options, integrity, integrityWarning);
133
+ }
134
+ else {
135
+ await runCodegraphCommand(io, 'codegraph.status', { subcommand: 'status', project: options.project });
136
+ if (integrityWarning !== null) {
137
+ io.stdout(`[WARN] codegraph exclude integrity not evaluated: ${integrityWarning}`);
138
+ }
139
+ else if (integrity !== null) {
140
+ for (const line of renderCodegraphExcludeIntegrityLines(integrity)) {
141
+ io.stdout(line);
142
+ }
143
+ }
144
+ }
145
+ if (integrity?.gap === true) {
146
+ process.exitCode = CODEGRAPH_INTEGRITY_EXIT_CODE;
147
+ }
148
+ }
149
+ /**
150
+ * Explicit repair path: reconcile → drop offending rules → back up the
151
+ * config → rebuild the index. Mirrors the automatic step `init` runs
152
+ * after a fresh upstream init, for workspaces that were already
153
+ * initialized before the integrity gate existed.
154
+ */
155
+ async function runCodegraphRepairExcludeCommand(io, options, asJson) {
156
+ let projectRoot;
157
+ try {
158
+ const candidate = resolve(options.project);
159
+ if (!statSync(candidate).isDirectory()) {
160
+ throw new Error('Project path must exist and be a directory');
161
+ }
162
+ projectRoot = candidate;
163
+ }
164
+ catch (error) {
165
+ printCodegraphFailure(io, 'codegraph.repair-exclude', error, asJson);
166
+ return;
167
+ }
168
+ const report = await repairCodegraphExcludeFromProject(projectRoot);
169
+ // Where the notes go matters: `printResult` renders every `warnings`
170
+ // entry to stderr with a `warning: ` prefix, so a confirmation parked
171
+ // in the third slot reads as a problem — and a real warning parked
172
+ // there double-prefixes. Confirmations go to `nextActions`; only a
173
+ // genuine `report.warning` reaches `warnings`, verbatim.
174
+ const confirmations = [];
175
+ if (report.applied) {
176
+ confirmations.push(`Removed ${report.rulesRemoved.length} exclude rule(s), recovering ${report.filesRecovered} tracked source file(s). Config backed up to ${report.backupPath}.`);
177
+ }
178
+ else {
179
+ confirmations.push('No tracked source file is excluded by the codegraph config; nothing to repair.');
180
+ }
181
+ if (report.applied) {
182
+ confirmations.push('Re-run `peaks codegraph status --project <root>` to confirm the gap is closed.');
183
+ }
184
+ printResult(io, ok('codegraph.repair-exclude', {
185
+ applied: report.applied,
186
+ rulesRemoved: report.rulesRemoved,
187
+ filesRecovered: report.filesRecovered,
188
+ trackedSourceCount: report.trackedSourceCount,
189
+ configPath: report.configPath,
190
+ backupPath: report.backupPath,
191
+ reindexed: report.reindexed,
192
+ warning: report.warning
193
+ }, report.warning === null ? [] : [report.warning], confirmations), asJson);
194
+ // A repair that could not run to completion (or could not reindex)
195
+ // must not report success to a shell.
196
+ if (report.warning !== null) {
197
+ process.exitCode = 1;
198
+ }
199
+ }
64
200
  /**
65
201
  * rid-CG-006 — init conflict guard. Resolves the project root and
66
202
  * probes `.codegraph/` for the peaks-loop marker before invoking the
@@ -92,7 +228,14 @@ async function runCodegraphInitCommand(io, options, asJson) {
92
228
  guard: guardOutcome.status,
93
229
  codegraphDir: guardOutcome.codegraphDir,
94
230
  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);
231
+ },
232
+ // A no-op init is a SUCCESS. This message used to sit in the
233
+ // `warnings` slot and was therefore printed as
234
+ // `warning: .codegraph/ is already managed by peaks-loop...`.
235
+ [], [
236
+ `.codegraph/ is already managed by peaks-loop; init is a no-op. Marker: ${guardOutcome.codegraphDir}/.peaks-loop-marker`,
237
+ 'Run `peaks codegraph index` to (re)build the index without touching the schema.'
238
+ ]), asJson);
96
239
  return;
97
240
  }
98
241
  if (guardOutcome.status === 'conflict-foreign-schema') {
@@ -110,8 +253,7 @@ async function runCodegraphInitCommand(io, options, asJson) {
110
253
  try {
111
254
  const invocation = createCodegraphInvocation({
112
255
  subcommand: 'init',
113
- project: options.project,
114
- ...(options.yes === true ? { yes: true } : {})
256
+ project: options.project
115
257
  });
116
258
  const result = await executeCodegraphInvocation(invocation);
117
259
  const didFail = result.exitCode !== null && result.exitCode !== 0;
@@ -137,7 +279,47 @@ async function runCodegraphInitCommand(io, options, asJson) {
137
279
  catch {
138
280
  // intentionally swallowed — surface as warning below
139
281
  }
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);
282
+ // Upstream `init` writes its 99-rule default `exclude` template,
283
+ // some of which collide with real source directories in this
284
+ // project. Left alone, a fresh clone / new machine gets an index
285
+ // that silently omits tracked source files while `status` says it
286
+ // is up to date. Reconcile now, drop the offending rules, and
287
+ // rebuild the index — a fresh init is the one moment this is both
288
+ // safe (nothing has been indexed yet) and necessary (`.codegraph/`
289
+ // is gitignored, so every clone starts from the default template).
290
+ //
291
+ // Never throws: a failure here is reported as a warning, not a
292
+ // failed init (the init itself already succeeded).
293
+ const excludeRepair = await repairCodegraphExcludeFromProject(projectRoot);
294
+ // These are confirmations, not warnings: `printResult` renders every
295
+ // `warnings` entry to stderr behind a `warning: ` prefix, so a fully
296
+ // successful init used to print a wall of `warning:` lines for what
297
+ // were plain success messages.
298
+ const initNotes = [
299
+ `Stamped peaks-loop marker at ${guardOutcome.codegraphDir}/.peaks-loop-marker`
300
+ ];
301
+ if (excludeRepair.applied) {
302
+ 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}.`);
303
+ if (excludeRepair.reindexed) {
304
+ initNotes.push('Rebuilt the codegraph index over the recovered files.');
305
+ }
306
+ }
307
+ printResult(io, ok('codegraph.init', {
308
+ guard: guardOutcome.status,
309
+ codegraphDir: guardOutcome.codegraphDir,
310
+ markerWritten: true,
311
+ excludeRepair: {
312
+ applied: excludeRepair.applied,
313
+ rulesRemoved: excludeRepair.rulesRemoved,
314
+ filesRecovered: excludeRepair.filesRecovered,
315
+ reindexed: excludeRepair.reindexed,
316
+ backupPath: excludeRepair.backupPath,
317
+ warning: excludeRepair.warning
318
+ }
319
+ },
320
+ // Verbatim: `printResult` supplies the `warning: ` prefix, so a
321
+ // prefix added here would render as `warning: warning: ...`.
322
+ excludeRepair.warning === null ? [] : [excludeRepair.warning], initNotes), asJson);
141
323
  }
142
324
  catch (error) {
143
325
  printCodegraphFailure(io, 'codegraph.init', error, asJson);
@@ -211,8 +393,11 @@ async function runCodegraphAffectedCommand(io, files, options, asJson) {
211
393
  }
212
394
  export function registerCodegraphCommands(program, io) {
213
395
  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));
396
+ addProjectOption(codegraph.command('status').description('Show codegraph status, including the exclude integrity gate')).action((options) => runCodegraphStatusCommand(io, options, options.peaksJson));
397
+ addProjectOption(codegraph
398
+ .command('repair-exclude')
399
+ .description('Drop codegraph exclude rules that block tracked source files, then rebuild the index')).action((options) => runCodegraphRepairExcludeCommand(io, options, options.peaksJson));
400
+ addProjectOption(codegraph.command('init').description('Initialize codegraph for a project')).action((options) => runCodegraphInitCommand(io, options, options.peaksJson));
216
401
  addProjectOption(codegraph
217
402
  .command('index')
218
403
  .description('Index a project with codegraph')