peaks-loop 4.0.43 → 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.
- package/CHANGELOG.md +32 -0
- package/README-en.md +1 -1
- package/README.md +1 -1
- package/dist/cli/commands/codegraph-commands.js +191 -6
- package/dist/cli/commands/final-review-commands.d.ts +34 -10
- package/dist/cli/commands/final-review-commands.js +130 -34
- package/dist/cli/commands/share-commands.d.ts +49 -0
- package/dist/cli/commands/share-commands.js +114 -14
- package/dist/services/codegraph/codegraph-autorefresh.js +12 -0
- package/dist/services/codegraph/codegraph-exclude-integrity.d.ts +61 -0
- package/dist/services/codegraph/codegraph-exclude-integrity.js +98 -0
- package/dist/services/codegraph/codegraph-exclude-reconciler.d.ts +26 -0
- package/dist/services/codegraph/codegraph-exclude-reconciler.js +217 -0
- package/dist/services/codegraph/codegraph-exclude-repair.d.ts +102 -0
- package/dist/services/codegraph/codegraph-exclude-repair.js +266 -0
- package/dist/services/codegraph/codegraph-preflight-service.js +12 -0
- package/dist/services/codegraph/codegraph-service.d.ts +0 -1
- package/dist/services/codegraph/codegraph-service.js +5 -4
- package/dist/services/doctor/doctor-service/checks/codegraph-exclude-integrity.d.ts +29 -0
- package/dist/services/doctor/doctor-service/checks/codegraph-exclude-integrity.js +88 -0
- package/dist/services/doctor/doctor-service/plugin-registry.js +2 -0
- package/dist/services/doctor/doctor-service/types.d.ts +27 -0
- package/dist/services/final-review/final-review-service.d.ts +154 -0
- package/dist/services/final-review/final-review-service.js +621 -7
- package/dist/services/final-review/index.d.ts +1 -1
- package/dist/services/final-review/index.js +1 -1
- package/dist/services/prd/handoff-auto-regen.js +0 -1
- package/dist/services/prd/handoff-service.d.ts +9 -1
- package/dist/services/prd/handoff-service.js +48 -6
- package/package.json +7 -5
- package/skills/peaks-final-review/SKILL.md +43 -32
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
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
|
+
|
|
3
35
|
## 4.0.43 — 2026-09-12 (一个从来拦不住东西的闸门 + 前端接口防腐层)
|
|
4
36
|
|
|
5
37
|
**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** | [](https://www.npmjs.com/package/peaks-loop) — 4.0.
|
|
143
|
+
| **Latest** | [](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
|
-
| **最新版本** | [](https://www.npmjs.com/package/peaks-loop) — 4.0.
|
|
143
|
+
| **最新版本** | [](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 |
|
|
@@ -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
|
-
},
|
|
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
|
-
|
|
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) =>
|
|
215
|
-
addProjectOption(codegraph
|
|
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')
|
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
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 (
|
|
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
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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
|
-
|
|
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:
|
|
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;
|
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
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 (
|
|
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
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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`),
|
|
@@ -21,8 +28,18 @@ import { existsSync, statSync } from 'node:fs';
|
|
|
21
28
|
import { join, resolve } from 'node:path';
|
|
22
29
|
import { addJsonOption, getErrorMessage, printResult } from '../cli-helpers.js';
|
|
23
30
|
import { fail, ok } from 'peaks-loop-shared/result';
|
|
31
|
+
import { createAnthropicRunner, LlmBindingError, LlmRequestError, resolveAnthropicConfig, } from '../../services/llm/anthropic-runner.js';
|
|
32
|
+
import { IncompleteFinalReviewError, prepareFinalReview, } from '../../services/final-review/final-review-service.js';
|
|
24
33
|
/** Whitelist of supported `--llm-provider` values for `peaks prepare-final-review`. */
|
|
25
|
-
const SUPPORTED_LLM_PROVIDERS = ['stub'];
|
|
34
|
+
const SUPPORTED_LLM_PROVIDERS = ['anthropic', 'stub'];
|
|
35
|
+
/**
|
|
36
|
+
* Default stays `stub` (unlike `peaks audit goal`, whose default is the real
|
|
37
|
+
* provider): the scaffold route is what CI and the registered e2e contract
|
|
38
|
+
* exercise without credentials, and the stub envelope is labelled
|
|
39
|
+
* `status: 'scaffold-only'` / `providerBinding: 'stub'` so it can never be
|
|
40
|
+
* read as a review.
|
|
41
|
+
*/
|
|
42
|
+
const DEFAULT_LLM_PROVIDER = 'stub';
|
|
26
43
|
function isSupportedLlmProvider(value) {
|
|
27
44
|
return SUPPORTED_LLM_PROVIDERS.includes(value);
|
|
28
45
|
}
|
|
@@ -32,7 +49,7 @@ function isSupportedLlmProvider(value) {
|
|
|
32
49
|
* "this is a placeholder on a failure" apart from "this is a real
|
|
33
50
|
* scaffold-only success".
|
|
34
51
|
*/
|
|
35
|
-
function emptyFinalReviewData(rid, sessionId, auditGoalPath) {
|
|
52
|
+
function emptyFinalReviewData(rid, sessionId, auditGoalPath, missingEnv) {
|
|
36
53
|
return {
|
|
37
54
|
status: 'not-applicable',
|
|
38
55
|
rid,
|
|
@@ -40,6 +57,7 @@ function emptyFinalReviewData(rid, sessionId, auditGoalPath) {
|
|
|
40
57
|
auditGoalPath,
|
|
41
58
|
serviceWired: false,
|
|
42
59
|
providerBinding: 'unknown',
|
|
60
|
+
...(missingEnv === undefined ? {} : { missingEnv }),
|
|
43
61
|
};
|
|
44
62
|
}
|
|
45
63
|
function validateProjectRoot(projectArg) {
|
|
@@ -95,7 +113,7 @@ export function registerFinalReviewCommands(program, io) {
|
|
|
95
113
|
.description('Prepare the 4-dimension business review (final-review primitive) for human acceptance (W2 T9 service; CLI surface in W5 M2)')
|
|
96
114
|
.requiredOption('--project <path>', 'target project root')
|
|
97
115
|
.requiredOption('--session-id <sid>', 'session id whose .peaks/_runtime/<sid>/audit-goal/<rid>.json is the approved goal source')
|
|
98
|
-
.option('--llm-provider <name>',
|
|
116
|
+
.option('--llm-provider <name>', `LLM provider name: ${SUPPORTED_LLM_PROVIDERS.join(' | ')} (default: ${DEFAULT_LLM_PROVIDER} — performs no review)`, DEFAULT_LLM_PROVIDER)).action(async (rid, options) => {
|
|
99
117
|
// 1. Project root must exist and be a directory.
|
|
100
118
|
const projectValidation = validateProjectRoot(options.project);
|
|
101
119
|
if (!projectValidation.ok) {
|
|
@@ -128,34 +146,112 @@ export function registerFinalReviewCommands(program, io) {
|
|
|
128
146
|
process.exitCode = 1;
|
|
129
147
|
return;
|
|
130
148
|
}
|
|
131
|
-
// 5. Provider check:
|
|
132
|
-
|
|
149
|
+
// 5. Provider check: unknown names fail loudly — a silent fallback to
|
|
150
|
+
// `stub` would hand the caller a scaffold envelope that reads as a
|
|
151
|
+
// review route, which is the defect class this gate exists to stop.
|
|
152
|
+
const provider = options.llmProvider ?? DEFAULT_LLM_PROVIDER;
|
|
133
153
|
if (!isSupportedLlmProvider(provider)) {
|
|
134
|
-
printResult(io, fail('final-review.prepare', 'LLM_PROVIDER_NOT_IMPLEMENTED', `
|
|
135
|
-
|
|
136
|
-
'Real provider binding is tracked as a follow-up slice.',
|
|
154
|
+
printResult(io, fail('final-review.prepare', 'LLM_PROVIDER_NOT_IMPLEMENTED', `LLM provider "${provider}" is not implemented. Supported providers: ${SUPPORTED_LLM_PROVIDERS.join(', ')}.`, emptyFinalReviewData(rid, sessionValidation.sessionId, auditGoalPath), [
|
|
155
|
+
`Re-run with \`--llm-provider anthropic\` for a real 4-dim review, or \`--llm-provider ${DEFAULT_LLM_PROVIDER}\` for an offline scaffold.`,
|
|
137
156
|
]), options.json);
|
|
138
157
|
process.exitCode = 1;
|
|
139
158
|
return;
|
|
140
159
|
}
|
|
141
160
|
// 6. Stub path: surface a structured "scaffold ready" envelope.
|
|
142
|
-
// We DO NOT call the service
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
161
|
+
// We DO NOT call the service here — the stub runner answers the
|
|
162
|
+
// audit-goal shape, not the 4-dim review shape, so running it through
|
|
163
|
+
// `prepareFinalReview()` would only manufacture a malformed review.
|
|
164
|
+
// The envelope confirms the route is wired end-to-end and reports the
|
|
165
|
+
// audit-goal path the service WOULD read.
|
|
166
|
+
if (provider === 'stub') {
|
|
167
|
+
const data = {
|
|
168
|
+
status: 'scaffold-only',
|
|
169
|
+
rid,
|
|
170
|
+
sessionId: sessionValidation.sessionId,
|
|
171
|
+
auditGoalPath,
|
|
172
|
+
serviceWired: true,
|
|
173
|
+
providerBinding: 'stub',
|
|
174
|
+
};
|
|
175
|
+
const envelope = ok('final-review.prepare', data, [], [
|
|
176
|
+
'Stub provider: no 4-dim review was performed. This envelope only proves the route is wired and reachable.',
|
|
177
|
+
`Audit-goal file is present at: ${auditGoalPath}`,
|
|
178
|
+
'Re-run with `--llm-provider anthropic` to produce a real review.',
|
|
179
|
+
]);
|
|
180
|
+
printResult(io, envelope, options.json);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
// 7. Real provider path: bind a real `LlmRunner` and run the service.
|
|
184
|
+
// Binding happens INSIDE the try so an absent credential surfaces as
|
|
185
|
+
// `LlmBindingError`'s own code instead of escaping as a crash. No
|
|
186
|
+
// envelope is emitted on that path — a "successful" empty review would
|
|
187
|
+
// be worse than an error.
|
|
188
|
+
try {
|
|
189
|
+
const config = resolveAnthropicConfig();
|
|
190
|
+
const llmRunner = createAnthropicRunner(config);
|
|
191
|
+
const review = await prepareFinalReview(rid, {
|
|
192
|
+
projectRoot: projectValidation.projectRoot,
|
|
193
|
+
sessionId: sessionValidation.sessionId,
|
|
194
|
+
llmRunner,
|
|
195
|
+
});
|
|
196
|
+
const data = {
|
|
197
|
+
status: 'review-complete',
|
|
198
|
+
rid,
|
|
199
|
+
sessionId: sessionValidation.sessionId,
|
|
200
|
+
auditGoalPath,
|
|
201
|
+
serviceWired: true,
|
|
202
|
+
providerBinding: 'anthropic-messages-api',
|
|
203
|
+
model: config.model,
|
|
204
|
+
review,
|
|
205
|
+
};
|
|
206
|
+
const envelope = ok('final-review.prepare', data, review.allPass ? [] : [
|
|
207
|
+
`Dimensions needing human attention: ${review.needsAttention.join(', ') || 'none flagged'}.`,
|
|
208
|
+
], [
|
|
209
|
+
`4-dim review produced by anthropic-messages-api (model: ${config.model}).`,
|
|
210
|
+
`allPass: ${String(review.allPass)}.`,
|
|
211
|
+
]);
|
|
212
|
+
printResult(io, envelope, options.json);
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
const code = finalReviewErrorCode(error);
|
|
216
|
+
printResult(io, fail('final-review.prepare', code, getErrorMessage(error), emptyFinalReviewData(rid, sessionValidation.sessionId, auditGoalPath, error instanceof LlmBindingError ? error.missingEnv : undefined), finalReviewNextActions(code)), options.json);
|
|
217
|
+
process.exitCode = 1;
|
|
218
|
+
}
|
|
160
219
|
});
|
|
161
220
|
}
|
|
221
|
+
/**
|
|
222
|
+
* Map a thrown error to the CLI's error code. The LLM-layer errors already
|
|
223
|
+
* carry codes precise enough to act on (`LLM_CREDENTIAL_MISSING`,
|
|
224
|
+
* `LLM_REQUEST_FAILED`, …), so they are passed through rather than flattened
|
|
225
|
+
* into one opaque failure.
|
|
226
|
+
*/
|
|
227
|
+
function finalReviewErrorCode(error) {
|
|
228
|
+
if (error instanceof LlmBindingError ||
|
|
229
|
+
error instanceof LlmRequestError ||
|
|
230
|
+
error instanceof IncompleteFinalReviewError) {
|
|
231
|
+
return error.code;
|
|
232
|
+
}
|
|
233
|
+
return 'FINAL_REVIEW_FAILED';
|
|
234
|
+
}
|
|
235
|
+
function finalReviewNextActions(code) {
|
|
236
|
+
switch (code) {
|
|
237
|
+
case 'LLM_CREDENTIAL_MISSING':
|
|
238
|
+
return [
|
|
239
|
+
'Export ANTHROPIC_AUTH_TOKEN (or ANTHROPIC_API_KEY) in the environment that launches peaks, then re-run.',
|
|
240
|
+
'For an offline scaffold instead of a review, re-run with `--llm-provider stub` — it performs NO review.',
|
|
241
|
+
];
|
|
242
|
+
case 'LLM_MODEL_MISSING':
|
|
243
|
+
return [
|
|
244
|
+
'Export ANTHROPIC_MODEL (or CLAUDE_CODE_SUBAGENT_MODEL) in the environment that launches peaks, then re-run.',
|
|
245
|
+
];
|
|
246
|
+
case 'LLM_REQUEST_FAILED':
|
|
247
|
+
return [
|
|
248
|
+
'Check ANTHROPIC_BASE_URL and network reachability, then re-run — a transport failure produces no review.',
|
|
249
|
+
];
|
|
250
|
+
case 'INCOMPLETE_FINAL_REVIEW':
|
|
251
|
+
return [
|
|
252
|
+
'The LLM reply was not valid JSON or omitted a required dimension; re-run so the gate is never read as complete.',
|
|
253
|
+
];
|
|
254
|
+
default:
|
|
255
|
+
return ['Re-run with `--llm-provider stub` to validate the CLI route without a real LLM.'];
|
|
256
|
+
}
|
|
257
|
+
}
|
|
@@ -12,6 +12,55 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import type { Command } from 'commander';
|
|
14
14
|
import { type ProgramIO } from '../cli-helpers.js';
|
|
15
|
+
/**
|
|
16
|
+
* A `dispatch-*.json` candidate for `finalize --request-id`, as read off disk.
|
|
17
|
+
* The record's own `createdAt` is carried as the recency key — NOT the file's
|
|
18
|
+
* mtime, which any heartbeat or finalize rewrites, so a `done` record can look
|
|
19
|
+
* newer than the `queued` one that superseded it. Keeping the key on the
|
|
20
|
+
* candidate makes the selection rule a pure function of the records and
|
|
21
|
+
* testable without a filesystem.
|
|
22
|
+
*/
|
|
23
|
+
export interface FinalizeCandidate {
|
|
24
|
+
readonly recordPath: string;
|
|
25
|
+
readonly requestId: string;
|
|
26
|
+
readonly status: string;
|
|
27
|
+
readonly createdAt: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* What `--request-id` actually did, reported in the envelope. N3: the branch
|
|
31
|
+
* used to resolve silently and could not say WHY a record was or was not the
|
|
32
|
+
* one finalized.
|
|
33
|
+
*/
|
|
34
|
+
export interface FinalizeSelection {
|
|
35
|
+
readonly requestId: string;
|
|
36
|
+
readonly rule: string;
|
|
37
|
+
readonly matched: number;
|
|
38
|
+
readonly chosen: string | null;
|
|
39
|
+
readonly rejected: readonly {
|
|
40
|
+
readonly recordPath: string;
|
|
41
|
+
readonly status: string;
|
|
42
|
+
readonly reason: string;
|
|
43
|
+
}[];
|
|
44
|
+
}
|
|
45
|
+
/** The one selection rule `--request-id` and `--batch` now share. */
|
|
46
|
+
export declare const FINALIZE_SELECTION_RULE: string;
|
|
47
|
+
/**
|
|
48
|
+
* N3 — pick the record `finalize --request-id` should act on.
|
|
49
|
+
*
|
|
50
|
+
* The branch this replaces `break`ed on the FIRST file whose record carried
|
|
51
|
+
* the requestId and never looked at `status`. With a re-dispatched request
|
|
52
|
+
* (this session holds six records for `2026-09-12-defect-remediation`) it
|
|
53
|
+
* always resolved to the OLDEST one — the already-`done` RD record — so
|
|
54
|
+
* finalizing reported success while the newer `queued` QA record stayed
|
|
55
|
+
* queued forever. `--batch` never had that bug: it filters on `queued`.
|
|
56
|
+
*
|
|
57
|
+
* Both branches are now the same rule. Return `null` when nothing is queued:
|
|
58
|
+
* a record that already left `queued` is precisely the one that must NOT be
|
|
59
|
+
* re-finalized, so the caller reports the survivors instead of touching one.
|
|
60
|
+
*/
|
|
61
|
+
export declare function selectFinalizeTarget(candidates: readonly FinalizeCandidate[]): FinalizeCandidate | null;
|
|
62
|
+
/** Why a candidate was not the chosen one — reported, never guessed at. */
|
|
63
|
+
export declare function describeFinalizeRejection(candidate: FinalizeCandidate, chosen: FinalizeCandidate | null): string;
|
|
15
64
|
export declare function registerShareCommand(parent: Command, io: ProgramIO): void;
|
|
16
65
|
export declare function registerSharedReadCommand(parent: Command, io: ProgramIO): void;
|
|
17
66
|
export declare function registerAwaitCommand(parent: Command, io: ProgramIO): void;
|