peaks-loop 4.0.47 → 4.0.49
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 +44 -0
- package/README-en.md +1 -1
- package/README.md +1 -1
- package/agents/karpathy-reviewer.md +11 -10
- package/dist/cli/cli-helpers.d.ts +34 -0
- package/dist/cli/cli-helpers.js +57 -0
- package/dist/cli/commands/code-job-shape-commands.js +8 -0
- package/dist/cli/commands/code-runtime-commands.js +48 -8
- package/dist/cli/commands/compact-command.js +110 -0
- package/dist/cli/commands/config-commands.js +15 -9
- package/dist/cli/commands/dashboard-long-run.js +6 -0
- package/dist/cli/commands/dispatch-commands.js +11 -1
- package/dist/cli/commands/doctor/invoke-from-code.js +6 -0
- package/dist/cli/commands/feedback-commands.d.ts +11 -7
- package/dist/cli/commands/feedback-commands.js +49 -17
- package/dist/cli/commands/final-review-commands.js +12 -0
- package/dist/cli/commands/hooks-commands.js +4 -4
- package/dist/cli/commands/job-commands.js +8 -0
- package/dist/cli/commands/loop-eval-commands.js +31 -0
- package/dist/cli/commands/perf-audit-commands.js +2 -0
- package/dist/cli/commands/playwright-commands.js +12 -0
- package/dist/cli/commands/prd-commands.js +1 -1
- package/dist/cli/commands/qa-commands.js +22 -0
- package/dist/cli/commands/request-commands.js +8 -0
- package/dist/cli/commands/scan-commands.js +1 -1
- package/dist/cli/commands/security-audit-commands.js +2 -0
- package/dist/cli/commands/slice-integrate-commands.js +22 -0
- package/dist/cli/commands/statusline-commands.js +44 -4
- package/dist/cli/commands/sub-agent/detached.d.ts +14 -1
- package/dist/cli/commands/sub-agent/detached.js +47 -22
- package/dist/cli/commands/sub-agent-shutdown-commands.js +11 -0
- package/dist/cli/commands/verdict-aggregate-command.js +95 -13
- package/dist/cli/commands/workflow-commands.js +1 -1
- package/dist/cli/index.js +5 -45
- package/dist/services/artifacts/artifact-prerequisites.d.ts +38 -7
- package/dist/services/artifacts/artifact-prerequisites.js +140 -65
- package/dist/services/artifacts/request-artifact-service.d.ts +8 -0
- package/dist/services/artifacts/request-artifact-service.js +77 -46
- package/dist/services/artifacts/request-artifact-state-helpers.d.ts +57 -0
- package/dist/services/artifacts/request-artifact-state-helpers.js +91 -10
- package/dist/services/audit/enforcers/active-skill-resolver.js +14 -1
- package/dist/services/audit-independent/perf-audit-service.d.ts +9 -0
- package/dist/services/audit-independent/perf-audit-service.js +27 -5
- package/dist/services/audit-independent/security-audit-service.d.ts +12 -2
- package/dist/services/audit-independent/security-audit-service.js +28 -6
- package/dist/services/code/auto-compact-lifecycle.d.ts +194 -0
- package/dist/services/code/auto-compact-lifecycle.js +229 -11
- package/dist/services/code/auto-compact-orchestrator.js +118 -7
- package/dist/services/code/compact-event-settle.d.ts +134 -0
- package/dist/services/code/compact-event-settle.js +240 -0
- package/dist/services/compact-history/compact-history-service.d.ts +14 -0
- package/dist/services/compact-statusline/compact-statusline-service.js +56 -22
- package/dist/services/config/config-restore.d.ts +12 -1
- package/dist/services/config/config-restore.js +35 -4
- package/dist/services/config/config-rollback.js +6 -1
- package/dist/services/context/auto-compact-types.d.ts +20 -2
- package/dist/services/context/harness-context-witness.d.ts +310 -0
- package/dist/services/context/harness-context-witness.js +606 -0
- package/dist/services/evidence/evidence-generator.js +86 -49
- package/dist/services/feedback/feedback-promotion-service.d.ts +137 -14
- package/dist/services/feedback/feedback-promotion-service.js +341 -20
- package/dist/services/feedback/promotion-artifact-evidence.d.ts +69 -0
- package/dist/services/feedback/promotion-artifact-evidence.js +332 -0
- package/dist/services/final-review/final-review-service.d.ts +9 -0
- package/dist/services/final-review/final-review-service.js +36 -12
- package/dist/services/ide/ide-registry.d.ts +19 -0
- package/dist/services/ide/ide-registry.js +21 -0
- package/dist/services/job/job-progress-store.js +18 -3
- package/dist/services/job/job-state-store.js +7 -0
- package/dist/services/observability/jsonl-store.d.ts +19 -0
- package/dist/services/observability/jsonl-store.js +27 -2
- package/dist/services/observability/observability-service.d.ts +10 -3
- package/dist/services/observability/observability-service.js +16 -3
- package/dist/services/polyrepo/polyrepo-dispatcher.js +11 -0
- package/dist/services/prd/handoff-auto-regen.js +31 -27
- package/dist/services/prd/handoff-frontmatter.d.ts +44 -0
- package/dist/services/prd/handoff-frontmatter.js +75 -0
- package/dist/services/prd/handoff-service.d.ts +41 -2
- package/dist/services/prd/handoff-service.js +124 -8
- package/dist/services/prd/handoff-types.d.ts +3 -2
- package/dist/services/prd/handoff-types.js +3 -2
- package/dist/services/qa/qa-business-review-state.js +23 -0
- package/dist/services/sc/sc-service.d.ts +8 -0
- package/dist/services/sc/sc-service.js +8 -1
- package/dist/services/scan/karpathy-service.js +2 -2
- package/dist/services/session/getSessionDir.d.ts +33 -0
- package/dist/services/session/getSessionDir.js +60 -0
- package/dist/services/session/session-checkpoint-service.js +8 -0
- package/dist/services/skill/resume-detector.js +29 -11
- package/dist/services/skills/hooks-codegate-superpowers.d.ts +6 -0
- package/dist/services/skills/hooks-codegate-superpowers.js +61 -2
- package/dist/services/skills/hooks-settings-service.js +14 -4
- package/dist/services/skills/session-start-hook-constants.d.ts +45 -0
- package/dist/services/skills/session-start-hook-constants.js +45 -0
- package/dist/services/skills/skill-statusline-service.d.ts +14 -0
- package/dist/services/slice/slice-check-service.js +29 -11
- package/dist/services/slice/slice-review-state.js +23 -0
- package/dist/services/workflow/pipeline-verify-gate-support.d.ts +47 -10
- package/dist/services/workflow/pipeline-verify-gate-support.js +221 -103
- package/dist/services/workflow/pipeline-verify-service.d.ts +1 -1
- package/dist/services/workflow/pipeline-verify-service.js +47 -33
- package/dist/services/workflow/pipeline-verify-types.d.ts +15 -6
- package/dist/services/workspace/claude-settings-template.d.ts +56 -8
- package/dist/services/workspace/claude-settings-template.js +98 -20
- package/dist/services/workspace/workspace-claude-settings-materializer.js +78 -7
- package/dist/shared/runtime-root.d.ts +73 -0
- package/dist/shared/runtime-root.js +77 -0
- package/package.json +6 -6
- package/skills/bee/peaks-prd/SKILL.md +7 -5
- package/skills/bee/peaks-qa/SKILL.md +5 -5
- package/skills/bee/peaks-qa/references/qa-runbook.md +2 -2
- package/skills/bee/peaks-qa/references/qa-transition-gates.md +7 -7
- package/skills/bee/peaks-rd/SKILL.md +8 -6
- package/skills/bee/peaks-rd/references/artifact-per-request.md +2 -2
- package/skills/bee/peaks-rd/references/parallel-review-fanout.md +7 -5
- package/skills/bee/peaks-rd/references/rd-fanout-contracts.md +13 -13
- package/skills/bee/peaks-rd/references/rd-runbook.md +9 -5
- package/skills/bee/peaks-rd/references/rd-transition-gates.md +9 -7
- package/skills/bee/peaks-rd/references/writing-handoff-frontmatter.md +6 -6
- package/skills/peaks-code/SKILL.md +1 -1
- package/skills/peaks-code/references/a2a-artifact-mapping.md +3 -3
- package/skills/peaks-code/references/local-artifact-workspace.md +1 -1
- package/skills/peaks-code/references/resume-detection.md +13 -7
- package/skills/peaks-code/references/runbook.md +3 -2
- package/skills/peaks-code/references/session-overload-signal-index.md +2 -1
- package/skills/peaks-code/references/workflow-gates-and-types.md +8 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,49 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.0.49 — 2026-09-15 (闸门验的是描述产物的话而不是产物本身 + id 轴三次布防后改走结构 + 两处吞异常与一个不是证据的 mtime + 套件自己把夹具写进了收集 glob)
|
|
4
|
+
|
|
5
|
+
**Highlights**:
|
|
6
|
+
|
|
7
|
+
1. **Gate H 验的是"描述产物的那句话",不是产物本身。** 三层检查都是 `text.includes()`。一棵**作为拒收而建**的树全部通过:注册表**不是合法 JSON**、模板上写着 `do NOT add a matcher`、`mode-gate.ts` 里是一句 `// TODO: … DELIBERATELY NOT a hard-floor category`。**同一份字节下 `enforceBashCommand` 返回 `{"decision":"allow"}`** —— 既满足了闸门,又解除了闸门所要证明的东西,而 `registry.json` 是 git 跟踪的。现在逐层解析并断言形状,产物不可读时**失败关闭**(缺失 / 非合法 JSON / 形状不对 / 解析成功但为空 / 不可读,五种全部拒绝);C 层要求**从文档里解析出来的引用**,而不是一个子串匹配到的名字。修的过程中发现闸门一直在**为一种不存在的保护背书**:`collectMembers` 把 union 与数组摊平、从任一边接受成员,而 `isHardFloorCategory` 只读**数组** —— 于是一个只写在 union 里的成员让闸门报 BACKED,而 `shouldPauseAtGate` 返回 `shouldPause: false`。**闸门为一道不会暂停任何东西的"硬地板"背书,并且与它自己的报错信息相矛盾。** 残留**如实披露而非关闭**:C 层的引用通道**就是散文**,所以一份"点名了路径、实际管的是另一个成员"的文档仍会通过 —— 这次买到的是 **cited ≠ mentioned**。
|
|
8
|
+
|
|
9
|
+
2. **id 轴:三次布防,最后一次比它替换掉的坏谓词更差 —— 于是改走结构。** 第一次把"一个 caller 提供的 id 进了路径"当作**一类**来 instrument。它没成立:守卫只盖**第一个** id、第二个敞着(四个命令各带一句"一道守卫盖住整族"的注释),而且**那次提交自己新引入了一处扫描器射程之外的双 id join,把项目根 README 覆盖掉了**。第二次修扫描器:12 个夹具里只抓住 **4** 个,而它替换掉的**按名字**谓词抓住 **8** 个 —— **五个是回归**。第三次把攻击从 4/12 提到 10/12,并**主动放弃**唯一能到 12/12 的改法:代价是活代码上 **7 处假阳性**,其中两处是结构性的(守卫合法地住在一个**助手函数**里,被守卫的值合法地作为**参数**传入)。判决:**按表达式文本的静态规则在这里做不健全** —— "守卫与 join 之间重新赋值"和"守卫在 join 之后"是**同一函数内部的支配性失败**,对任何按文本取键的办法都不可见;而收窄射程也不是免费的。于是换成**性质**:`src/shared/runtime-root.ts` 把守卫过的分段品牌在一个**非导出的 unique symbol** 后面、`RuntimeRoot` 用**私有 `#path`**、`join` 要求**至少一个**守卫分段(所以零参调用拿不回裸根)—— **新写一处未守卫的 join 直接编译不过(TS2345)**,而"拆掉守卫再变红"只能证明删除会被注意到。**诚实否定,明说而非糊过去**:旧站点根本不"取得一个根",它们把 `join(projectRoot,'.peaks','_runtime',id,…)` **直接拼在行内** —— 没有访问器可供类型加固,而 TS 无法拒绝一个字符串字面量的拼写;所以性质**只在用了缝的地方**成立。另外披露:`as GuardedSegment` 在模块外可以伪造品牌,`dir()` 是刻意的旁路。**95 个未守卫槽位 / 48 个文件**,编译器就是工作清单。
|
|
10
|
+
|
|
11
|
+
3. **压缩循环:两处吞异常、一行断言了从未发生的结算、一个不是证据的 mtime。** `settleOpenLifecycleRun` 的生命周期写入失败被吞掉 → run 停在 `armed` → **之后每一次探针都会再结算一次、再追加一行**,无界。两条结算路径中先只修了一条,另一条照旧追加;现在两条都**推迟**那一行并如实说明,run 记为**未结算**,而不是在仍处 `armed` 时报成功。状态线**把 history 文件的 mtime 当作"刚刚压缩过"的证据** —— 而那个文件**每次被"询问"就写一行 dispatch**,所以**新鲜度恰恰在什么都没压缩的时候达到峰值**(仓库自己的会话在该端点有 **1075 行 dispatch、0 次压缩**);现在要求窗口内存在一行 `observed`,且配了非真空性对照证明旧谓词确实会接受同一个夹具。`getSessionDir` 增加一个**总体变体**,让"不能抛"的调用方有东西可调而不是吞掉;可观测性服务恢复它被记录过两次的 never-throws 契约。
|
|
12
|
+
|
|
13
|
+
4. **套件自己把夹具写进了它收集的 glob 里。** `bdd-reporter.test.ts` 在 `tests/unit/` **里面**创建夹具、在 `afterEach` 删掉,而那是 vitest 的收集范围 —— 并发跑会在收集到它之后**四秒**导入失败,在**无关的套件**里报 `Cannot find module`。这件事**此前被记录过四次、一次都没修**(三份产物加一份已提交的记忆)。修完顺带量到一条比修复本身更大的事:**与测试同时跑的 `tsc` 不是证据** —— 中止时总数**错低**(一条孤立 `TS6053`、墙钟塌到约两秒),而**真实夹具在场时污染是隐形的**(总数恰好等于干净基线),是个**静默的错数**而不是显眼的错数;三选一里的判决是"在安静的树上取,并要求 `TS6053 = 0`"。非真空性对照:把夹具配置的 `include` 指向不匹配的 glob → 四个用例**全部失败**,证明它们确实依赖嵌套运行真的执行那个夹具。
|
|
14
|
+
|
|
15
|
+
5. **一行观测被追加了 2054 次,而原因是我们自己的基准测试。** 一次"只读"性能评审的微基准对着**真实** history 文件跑了两遍(`readFileSync` 取最后一行 → `appendFileSync` 写回同一文件 → 在计时循环里重复);因为载荷是**从文件里读回来**的,每次重放**逐字节相同、连 `ts` 都相同**。算术精确闭合:**1 条真事件 + 503 + 1550 = 2054 行**,占该文件的 **53.8%**。**没有产品缺陷** —— 两个候选机制(写入端有循环 / 存在一个 drain)**都被证伪**(orchestrator 里 0 个循环,且 `ts` 在对象字面量里构造,所以 N 次产品调用必然给出 N 个不同戳)。证据**一行未删**:删掉它让文件显得整洁,正是这条线存在的理由所要消灭的动作。
|
|
16
|
+
|
|
17
|
+
**验证**:三个版本常量一致(**4.0.49**);`pnpm build` 的 `build-integrity: OK` —— 并且**套件自己抓住了这次 bump 的遗漏**(`lockstep-three-packages` 报 `CLI_VERSION in dist/version.js is 4.0.48, but root package.json#version is 4.0.49`,即源码改了而构建陈旧;重建后绿);`tsc -p tsconfig.json` 保持 **140** 基线、**0 在 `src/`**、**`TS6053` = 0**;`tests/unit` **253 files / 2706 passed / 3 skipped / 0 failed**(在安静的树上取,先确认 0 个 vitest 进程)。套件的 `write-gate-decision-table` 与 `final-review-service` 用例是**负载敏感**的(并行负载下超时、单独跑通过)—— **点名,不两边计数**。
|
|
18
|
+
|
|
19
|
+
**明确未验证的(不当作已完成)**:**没有任何一次真实的 Claude Code `PostCompact` 事件被观察到** —— 两条结算路径的行为是用只读 store 驱动的,证明的是"命令会结算",**从不是"事件会到达"**;`handoff-service.ts` 未被路由到新缝(把它的相对形式接进来会把 5 号修掉的那条片段往返载体重新引回来);结构性质的 95 个剩余槽位**未迁移**,所以它目前只在 2 个文件上成立。
|
|
20
|
+
|
|
21
|
+
**本轮顺带发现、尚未处理(不属本版修复)**:**结构缝自己的注释里有一句假话**(`runtime-root.ts:36-37` 声称模块外 `x as GuardedSegment` 是类型错误,**实测为假**);行内拼写仍编译干净,而用 AST 普查做一条字面量禁令(本仓已写过两次这个模式)可以补上;`dir()` 旁路;规则 D 自己的套件在 import 时扫描整个 `tests/` 并会与并发夹具写入者竞争(本版修的正是这个类);49 个文件上的 95 个未守卫槽位;六个原始 rid 的闸门状态仍停在 `spec-locked`。
|
|
22
|
+
|
|
23
|
+
## 4.0.48 — 2026-09-14 (一个 id 没校验就进了路径 + 自己的消费者读不出的交接胶囊 + 结尾才写、开头就读的状态)
|
|
24
|
+
|
|
25
|
+
**Highlights**:
|
|
26
|
+
|
|
27
|
+
1. **一个 caller 提供的 id 没经校验就进了路径;这次是当作一类来 instrument,不是逐点打补丁。** session / request / job / change 这些 id 会被拼进产物路径,本仓库**对这件事恰好只有一种正确控制**,而**每个拼接点各自重新决定要不要用它** —— 其中一个没决定,CLI 就写到项目根之外,**并照样返回 `ok: true`**。上一轮量到六处这类点位;这一轮**先给探测装上自检**:在逃逸根植一个哨兵、要求猎手找到它,否则每一个 "escaped=no" 都一文不值 —— 关掉守卫同一探测报 **6/6 逃逸**,恢复后 **0/6**(上一轮的第一次扫描往上走了五层、对十四个用例全报"无逃逸",而逃逸目录就在盘上)。修复是**扩展仓库已有的 AST 守卫**而不是新写扫描器:射程从 0 涨到 `src/` 下 **152 个文件**,灵敏度用同样方式证明(去掉一个守卫它就失败并点名站点,恢复即通过),它的盲区被**精确记录而不是放宽**(`_runtime` 之后是钉死字面量时该站点对它不可见;七个这样的拼接点现在全部点名,一个钉死的普查测试让新增的那个失败)。instrument 找到最严重的一处:**`playwright stop --terminal ../../../../X` 返回 `ok: true`,同时 TERMINATE 了它点名的 pid、并 UNLINK 了项目根之外的一个文件** —— 一个 flag 就拿到任意 SIGTERM 加一次删除。它逃逸有两个原因,都值得记:那一行分析的是 `playwrightSessionsDir()`(其 `_runtime` 槽确实是钉死字面量),而 id 是在**另一个不含任何 `_runtime` 字面量的调用**里拼的;以及**两次扫描从未真的执行过 `playwright`** —— 一个否定结论不会留下"背后到底执行过什么"的痕迹。现在在三个会话操作共用的那个调用处设防,并配一个控制组证明**合法 id 仍然会 kill 和 unlink** —— 选择性的,不是一刀切拒绝。
|
|
28
|
+
|
|
29
|
+
2. **交接胶囊自己的消费者读不出来。** `peaks prd handoff init` 发出的是 `schemaVersion: "2"` 与裸 `handoffHash:`,而 `rd:qa-handoff` 闸要字面 `schemaVersion: 2` 加 `sha256:`,两个 audit loader 又要锚定的 `^schemaVersion:\s*(\d+)\s*$` —— **三个消费者、四条要求,而规范写入者一条都不满足**。上一轮会话精确诊断过这一点,却把分析写进了自己的 gitignored `_runtime` 树,于是下一轮从零重新发现了一遍。这次**修写入者而不是放宽闸**:没有任何子串编辑能教会一个锚定正则去读带引号的值,而一个接受 loader 仍然拒绝之物的闸,只是把分歧挪到闸后面、并且更安静。同一版另有**三处"两个组件对同一件事各说各话"**:`verify-pipeline` 索要 `rd/security-review.md`,而它本该镜像的表早已移到 `audit/security.md`,还索要两个 v2.11.0 已退役的 QA findings 文件 —— 于是**通过 `request transition` 的切片会在下一个检查里失败**;现在要求集从表自己的 tier 顺序经 `getPrerequisitesFor()` 派生。**状态读取者与写入者对"哪一行是权威"看法相反**:`request transition` 在**末尾**追加权威 Status 块,三个读取者取**第一个**匹配 —— 在 QA 追加过四次的文件上,检查器报 `qa-block` 而写入者写的是 `verdict-issued`;现在所有读取者共用一个定位器,**69 个在盘产物经新旧两条路径读出的结果全等,而两者只在那个错的上面不同**,顺手扫出的第三个 first-match 读取者曾把一份被追加过的 PRD 判成 `in-flight` 而非 `resume`。**同一会话里的两个切片静默互相覆盖审计证据**:受闸的产物路径不带 rid,第二个切片的复核替换掉第一个的 —— 而闸保持绿,因为它检查存在与形状,**从不检查归属**;一位审计者注意到并保留了文件、另一位没有,**这个差别是运气不是纪律**。路径现在按 rid 划界、legacy 层留作回退,经 **62 组真实 session×rid 对**验证。
|
|
30
|
+
|
|
31
|
+
3. **压缩触发点第一次有了目击者,而不是从"ratio 跌没跌"反推。** harness 每次压缩都会广播(`PreCompact` 带 `trigger: manual|auto`,`PostCompact` 在完成后触发),而 peaks-loop 从没在听 —— 它靠"下一个探针发现 ratio 掉了"来推断"发生过压缩"。于是本仓库存在的理由(标定)最想回答的那个问题,**"是 harness 自己压的,还是有人敲了 `/compact`"**,在这里**根本没有目击者**;实测本机全部 transcript:`compactMetadata.trigger` 读到 `manual x2`、`auto x0`。新增 `PostCompact` 钩子条目,事件到达当刻就结算打开的 run,并把 harness 自己的 trigger 写进观测行(**没有打开的 run 的压缩仍然不留痕迹 —— 归因不是证据**)。三件事是测量逼出来的:事件路径**不能把"lifecycle 写失败"报成"没有 run"**(`null` 现在只表示一个意思;写失败连 `lifecycleWritten: false` 一起返回 facts,观测行照写,因为那行是**关于 harness 说了什么**、不是关于我们写没写进去);payload 的 `session_id` 与自己的外层会话 id 比对,**子代理发起的事件不能结算主会话的 run**,而字段缺失或自己的 id 解析不出时**接受** —— 一个看不见自己名字的守卫不能变成死钩子;被事件结算的压缩**仍要闭合它的标定对**,填空推迟到下一个测到低于该 run 自身触发点的探针,**是推迟不是封死**。matcher 用空串而不是文档写的 `auto|manual`:两族钩子的 matcher 语义不同(`PreToolUse` 是工具名正则,`SessionStart` 的 `'compact'` 是精确来源标签),证据支持的是精确相等,空串是唯一不可能"永不触发"的形式。姊妹项把"两个窗口是不是同一个数"**从推断变成比对**:`used_percentage` 在 `src/` 里此前**零消费者**,现在默认渲染器把它捕获进会话级 ledger、`context-now` 拿自己的 ratio 与 harness 的百分比比对。三个设计点承担了风险:两个"窗口"**不是一个字段**(比的是 ratio 对 `used_percentage`,**绝不是窗口对窗口** —— 直接比会给出一个自信的错答案);**容差是论证出来的不是选出来的**(贡献项是 harness 的整数百分比取整、分子口径、以及两个在不同时刻算出的数之间的采样偏斜,偏斜被**减掉**而不是预算 —— 预算会让预算涨 x 而偏差涨 x(1+g),真实 gap 被自己的偏斜抵消);**锐度闸测的是目击者的 ratio 而不是 peaks-loop 的**(旧版闸错了对象,于是真实 3.3% 的 gap 在 r=0.2 处读成 `agree`,恰好落在实测采样偏斜的位置)。验收是一次 **274,188 次比对**的扫描、其最坏被掩蔽的 `agree` 是**空集**,同一扫描对旧闸失败。判决有三个而不是两个:`agree` / `disagree` / `unverifiable` —— 两答案的守卫分不清"正确地沉默"和"坏了"。
|
|
32
|
+
|
|
33
|
+
4. **上一版自己点名的"尚未处理"被兑现;其中两件的前提被实测证伪,而纠正前提比修复本身更重要。**
|
|
34
|
+
|
|
35
|
+
- **"模板只拥有它声明的条目。"** 4.0.47 把 settings materializer 的整键 `hooks` 所有权记为**潜伏**问题("今天没有东西走那条路")—— **这是错的,而且有一条活路径**:`installAutoCompactHook` 往那个文件里写 `Bash|Task` 条目,而 `peaks workspace init` 拥有整个键。实测:init → 装好 auto-compact(4 条)→ 再 init → **REFRESHED,3 条,`Bash|Task survived? false`** —— 而**没有任何东西会把它装回去,于是 auto-compact 静默停止**,刷新只报成 `refreshed`。现在模板只拥有它**声明**的条目(按 matcher 键控 + 每 matcher 配额),自己的一律排在最前,其余在盘条目**逐字保留**(未声明的 matcher、超出配额的条目、未声明的事件、不合规的条目);会被漏掉的那一半也修了 —— `templateContentMatches` 必须一起改成**多重集包含**,否则合并之后它会永远拿 3 条模板去比 4 条在盘、于是每次 init 都报 `refreshed`,正是整键所有权存在时要防的那个状态。五条验收条件全部**实测而非推理**。
|
|
36
|
+
- **`config restore` 与 `rollback` 对"没有备份"这件事说两种话**:`--list` 像失败一样 exit 1,`rollback` 像事实一样 exit 0 带 `available: false` —— **它们是同一件事实**。现在两者都 exit 0,且 `available` 出现在**每一个**信封上(`false` = 没有备份可恢复,`true` = 有备份、失败的是操作本身),区分从退出码搬进了一个 JSON 键。
|
|
37
|
+
- **`test:integration` 漏了 `--config`**,于是继承基础 config 的 `include`、**选中零个文件**:"No test files found, exiting with code 0" —— 一个**什么都没跑**的绿。脚本现在带上该 flag,`ci.yml` / `vitest.config.integration.ts` / 一个把旧绕行说成永久的测试注释也一并改正;**没有任何 CI 步骤会跑两遍该套件**(那个 job 直接调 vitest,从不走脚本)。
|
|
38
|
+
- **`detached.ts` 挂了一个可证明永远不会运行的 `child.on('error')`** —— Node 把 spawn 失败派发在 nextTick 队列上,该队列在 await 的调用方恢复之前就排空,所以 handler 总是挂得太晚。删掉它,启动失败路径改读 `spawnError`,而不是返回 `ok: true` 带 `pid: -1`;在一个真实 ENOENT 上端到端验证:`ok: false`、`spawnError {ENOENT}`、派发记录 `status=failed`。另外 `ProcessSupervisor.spawn` 曾在失败启动时写零字节 `pid` 文件,而 `Number('') === 0` —— 一次 `kill(Number(read(pid)))` 的清理会**给 pid 0 发信号,也就是整个进程组**;现在什么都不写,因为"不存在"才是真正的不变量。
|
|
39
|
+
- 以及 `--ide` help 列了两个适配器而注册表里有九个(六处不是三处,改为从注册表派生,从此不会漂移)、`tests/integration/_cli-helper.ts` 镜像了一个生产已不再发出的信封形状(爆炸半径是**一个** importer 不是十一个;改为让真实 CLI 与测试 helper 调用同一份 `cli-helpers.ts`,此后不可能各自漂移)。
|
|
40
|
+
|
|
41
|
+
**验证**:三个版本常量一致(**4.0.48**);`pnpm build` 的 `build-integrity: OK`;`tsc -p tsconfig.json` 保持 **140** 基线、**0 在 `src/`**、且 **TS6053 = 0**(孤立的 TS6053 是一次 abort 而不是一棵干净的树 —— 它报"找不到文件"并且什么都不编译,所以 `140 → 1` 会被读成"大幅改善"、`0 → 1` 被读成"新的小问题",两种读法都错);`tests/unit` **248 files / 2627 passed / 3 skipped / 0 failed**。套件的 `write-gate-decision-table` 与 `final-review-service` 用例是**负载敏感**的(并行负载下超时、单独跑通过)—— **点名,不两边计数**。
|
|
42
|
+
|
|
43
|
+
**明确未验证的(不当作已完成)**:**没有人在真实 Claude Code 上观察过一次 `PostCompact`** —— 测试证明的是"命令会结算",**从不是"事件会到达"**;同理,本仓库**从未观察过任何真实的 Claude Code statusline 载荷**(此仓没有 `statusLine` 键,装一个属于需要用户确认的 harness settings 变更),被验证的只是"真实载荷会被正确捕获并比对"。
|
|
44
|
+
|
|
45
|
+
**本轮顺带发现、尚未处理(不属本版修复)**:13 个守卫点位里 10 个没有实测控制组;17 个带 id 的文件既未分类也未点名;约 60 处 binding/env 拼接;31 个未追踪的 CLI 文件上仍有同一种路径逃逸形状;以及最刺眼的一条 —— **扫描清单自己的后续指令仍然排除了这一轮查出最严重缺陷的那个文件**。
|
|
46
|
+
|
|
3
47
|
## 4.0.47 — 2026-09-13 (90 个文件第一次进 CI 就绿了 + 弹窗的根在守卫射程 + 说"能做"而做不到的地方)
|
|
4
48
|
|
|
5
49
|
**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.49 (2026-09-15) |
|
|
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.49(2026-09-15) |
|
|
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 |
|
|
@@ -29,7 +29,8 @@ You are a **Karpathy-guidelines enforcement reviewer** for peaks-rd. You inspect
|
|
|
29
29
|
|
|
30
30
|
1. **The verbatim 4-section Karpathy-guidelines block** (injected by the parent RD loop via `rd-sub-agent-dispatch.md` §"Karpathy-guidelines context"). If this block is missing from the prompt, return `gateAction: 'block'` with a single `think-before-coding` violation.
|
|
31
31
|
2. **The slice's `git diff` against the base branch** (use `git diff <base>...HEAD` to get the full RD-side change set; fall back to `git diff` if no base is given, then `git status --short` to confirm scope).
|
|
32
|
-
3. **The slice's `rd/tech-doc.md
|
|
32
|
+
3. **The slice's `prd/handoff.md`** (the immutable peaks-prd design / scope record — v2.11.0: it replaces `rd/tech-doc.md`, which no gate requires any more). Read it at `.peaks/_runtime/<sessionId>/prd/handoff.md`; verify the handoff hash matches the value the parent dispatched before relying on it.
|
|
33
|
+
> `rd/tech-doc-<rid>.md` was evaluated as the alternative and rejected: nothing in the pipeline writes a rid-scoped tech-doc (`peaks evidence generate` writes the RIDLESS `rd/tech-doc.md` on purpose, since the `TECH_DOC` prereq was dropped in v2.11.0), so requiring one would BLOCK every slice. The ridless path was worse still — one file shared by every slice in a session, so a stale architecture summary from an earlier slice read as this slice's design doc. `prd/handoff.md` is the record the contract itself requires at `rd:qa-handoff` (`AUDIT_REQUIRES_HANDOFF`), and the fan-out prose already names it (`references/parallel-review-fanout.md`).
|
|
33
34
|
4. **The slice's PRD body / acceptance criteria** (look at `.peaks/_runtime/<sessionId>/prd/requests/<rid>.md`).
|
|
34
35
|
5. **Optional: the slice's `rd/code-review.md` / `rd/security-review.md` / `rd/perf-baseline.md`** for cross-context only — do not duplicate their findings.
|
|
35
36
|
|
|
@@ -47,7 +48,7 @@ If any of inputs 1-4 is unreadable / missing, return `gateAction: 'block'` with
|
|
|
47
48
|
- The diff adds a regex / parser / serializer without naming the input shape (language, locale, edge cases).
|
|
48
49
|
- The diff introduces a CLI option whose default value is not justified (why this default? what changed if a user picks a different value?).
|
|
49
50
|
- The diff contains the phrase "we'll handle that later" / "TODO: think about" / "TBD" in a code path.
|
|
50
|
-
- The slice's `
|
|
51
|
+
- The slice's `prd/handoff.md` body — the architecture / trade-offs record — is missing or empty.
|
|
51
52
|
|
|
52
53
|
### 3.2 Simplicity First
|
|
53
54
|
|
|
@@ -85,7 +86,7 @@ If any of inputs 1-4 is unreadable / missing, return `gateAction: 'block'` with
|
|
|
85
86
|
- The diff adds a new public function / exported type / CLI subcommand with NO test in the same diff.
|
|
86
87
|
- The slice's `qa/test-cases/<rid>.md` is empty or has < 3 test rows.
|
|
87
88
|
- The diff claims a behavior change in a commit message / PR body / docstring but no test exercises the change.
|
|
88
|
-
- The
|
|
89
|
+
- The slice's `prd/handoff.md` frontmatter carries no `acceptanceCriteria` entries — the handoff's equivalent of the old "Acceptance checks" section.
|
|
89
90
|
- The diff adds a new regex / parser / runtime check whose failure path is unreachable from any test.
|
|
90
91
|
|
|
91
92
|
## 4. Output (compact JSON envelope)
|
|
@@ -163,8 +164,8 @@ When `evaluationCost` is reported, the envelope is:
|
|
|
163
164
|
|---|---|
|
|
164
165
|
| 0 violations | `'pass'` |
|
|
165
166
|
| 1+ violations of any kind, none of which is a HARD-blocker | `'warn'` |
|
|
166
|
-
| `## Karpathy-Gate` header missing from `rd/karpathy-review
|
|
167
|
-
| Inputs 1-4 (guidelines verbatim / diff /
|
|
167
|
+
| `## Karpathy-Gate` header missing from `rd/karpathy-review-<rid>.md` file write | `'block'` |
|
|
168
|
+
| Inputs 1-4 (guidelines verbatim / diff / prd-handoff / PRD-AC) missing or unreadable | `'block'` |
|
|
168
169
|
| Diff is empty (no changed files) but the slice claims a feature change | `'block'` |
|
|
169
170
|
| The slice introduces secrets, executes arbitrary code, or modifies `~/.claude/settings.json` / `~/.claude/agents/` / `~/.claude/hooks/` | `'block'` |
|
|
170
171
|
| 3+ violations across 3+ different `kind` values | `'block'` (the slice is structurally misaligned, not just imperfect) |
|
|
@@ -181,7 +182,7 @@ If the `evaluationCost` field is absent (older reviewer, or the orchestrator did
|
|
|
181
182
|
|
|
182
183
|
## 5. File write contract
|
|
183
184
|
|
|
184
|
-
You MUST also write `rd/karpathy-review
|
|
185
|
+
You MUST also write `rd/karpathy-review-<rid>.md` (relative to the slice's project root), replacing `<rid>` with this slice's request id — the same id that names `.peaks/_runtime/<sessionId>/rd/requests/<rid>.md`. The rid is part of the filename because every slice in a session shares `rd/`; writing the ridless `rd/karpathy-review.md` silently destroys the previous slice's review (it did on 2026-09-13). The file must contain EXACTLY these section headers, in this order, with title-case capitalization (the existing `KARPATHY_REVIEW` prereq in `src/services/artifacts/artifact-prerequisites.ts` enforces this). The 5 literal lines below are indented by 4 spaces to keep them out of the sibling-reference heading inventory; copy them verbatim into the file you write.
|
|
185
186
|
|
|
186
187
|
```md
|
|
187
188
|
# Karpathy review — <rid>
|
|
@@ -209,14 +210,14 @@ You MUST also write `rd/karpathy-review.md` (relative to the slice's project roo
|
|
|
209
210
|
<bullet-list of evidence, one per finding, or "No violations" if clean>
|
|
210
211
|
```
|
|
211
212
|
|
|
212
|
-
If you cannot write this file (read-only filesystem, path collision, permission denied), include a `writeError: '<reason>'` field in your JSON envelope and set `gateAction: 'block'`. The transition CLI gate will refuse the `qa-handoff` transition if `rd/karpathy-review
|
|
213
|
+
If you cannot write this file (read-only filesystem, path collision, permission denied), include a `writeError: '<reason>'` field in your JSON envelope and set `gateAction: 'block'`. The transition CLI gate will refuse the `qa-handoff` transition if `rd/karpathy-review-<rid>.md` is missing.
|
|
213
214
|
|
|
214
215
|
## 6. Hard prohibitions
|
|
215
216
|
|
|
216
217
|
In addition to the 4-sub-agent block:
|
|
217
218
|
|
|
218
219
|
- **MUST NOT write code** — you review, you do not implement. The RD main loop owns code edits.
|
|
219
|
-
- **MUST NOT modify the request artifact** (`.peaks/_runtime/<sessionId>/rd/requests/<rid>.md` or the PRD body). Your only write target is `rd/karpathy-review
|
|
220
|
+
- **MUST NOT modify the request artifact** (`.peaks/_runtime/<sessionId>/rd/requests/<rid>.md` or the PRD body). Your only write target is `rd/karpathy-review-<rid>.md`.
|
|
220
221
|
- **MUST NOT call `peaks request transition`** — only the parent RD loop owns the transition state machine.
|
|
221
222
|
- **MUST NOT install hooks, agents, MCP servers, or modify settings** (this is the global peaks-rd red line; it applies to sub-agents too).
|
|
222
223
|
- **MUST NOT touch Slice 1+2+3+4+5 products** (zero regression). If the diff includes changes to `karpathy-service.ts` / `scan-commands.ts` / `artifact-prerequisites.ts` / `peaks-rd/SKILL.md` / `rd-fanout-contracts.md` / `rd-sub-agent-dispatch.md` / `karpathy-5way-fanout.test.ts` / `rd/karpathy-review.md`, flag them as `surgical-changes` violations unless the slice's PRD explicitly authorizes the touch.
|
|
@@ -234,11 +235,11 @@ In addition to the 4-sub-agent block:
|
|
|
234
235
|
|
|
235
236
|
## 8. Review process
|
|
236
237
|
|
|
237
|
-
1. Read the 4 inputs (guidelines / diff /
|
|
238
|
+
1. Read the 4 inputs (guidelines / diff / prd-handoff / PRD-AC).
|
|
238
239
|
2. For each of the 4 guidelines, walk the diff and apply the detection rules in §3.
|
|
239
240
|
3. Collect violations into a list, capped at 5 per kind.
|
|
240
241
|
4. Decide `gateAction` per the §4 decision table.
|
|
241
|
-
5. Write `rd/karpathy-review
|
|
242
|
+
5. Write `rd/karpathy-review-<rid>.md` with the 4 title-case section headers.
|
|
242
243
|
6. Emit the compact JSON envelope on the last line of your response.
|
|
243
244
|
|
|
244
245
|
## 9. Review summary format (informational, not part of envelope)
|
|
@@ -58,6 +58,40 @@ export declare function printCliEnvelope(io: ProgramIO, r: CliEnvelope): void;
|
|
|
58
58
|
* `src/cli/program.ts:162` is hereby resolved by this helper.
|
|
59
59
|
*/
|
|
60
60
|
export declare function printErrorEnvelope(io: ProgramIO, command: string, code: string, message: string, data: Record<string, unknown>, nextActions: string[]): void;
|
|
61
|
+
/**
|
|
62
|
+
* The subcommand path the caller actually invoked, in the same tokens they
|
|
63
|
+
* typed (`release canary`). Derived from the registered command tree by
|
|
64
|
+
* consuming leading non-`-` argv tokens, so it never disagrees with what
|
|
65
|
+
* Commander dispatched on.
|
|
66
|
+
*
|
|
67
|
+
* Commander's `CommanderError` carries a code and a message but no command
|
|
68
|
+
* reference — `missingMandatoryOptionValue` is raised by the leaf command and
|
|
69
|
+
* throws out of `parseAsync` with nothing naming it. Without this walk the
|
|
70
|
+
* missing-option envelope could only say `command: "cli"`, which is the field
|
|
71
|
+
* the caller needs to act on.
|
|
72
|
+
*/
|
|
73
|
+
export declare function resolveInvokedCommandPath(program: Command, argv: readonly string[]): string;
|
|
74
|
+
/**
|
|
75
|
+
* Render the `MISSING_REQUIRED_OPTION` envelope for a Commander
|
|
76
|
+
* `commander.missingMandatoryOptionValue`.
|
|
77
|
+
*
|
|
78
|
+
* A `.requiredOption()` the caller did not supply is raised as a plain
|
|
79
|
+
* `Error`-shaped `CommanderError`, so an unhandled `.catch()` used to file it
|
|
80
|
+
* under `UNHANDLED_ERROR` — "you left out an argument" reported as a crash,
|
|
81
|
+
* with empty `nextActions`, `command: "cli"` and no way to see WHICH option.
|
|
82
|
+
* The flags string Commander formats carries both the name and the accepted
|
|
83
|
+
* values (`--percent <10|50>`), so it is worth extracting rather than
|
|
84
|
+
* paraphrasing: it is the option's own declaration.
|
|
85
|
+
*
|
|
86
|
+
* Exported because it has TWO callers that must not drift: the real wrapper
|
|
87
|
+
* (`src/cli/index.ts`) and the in-process test runner
|
|
88
|
+
* (`tests/integration/_cli-helper.ts`), which re-creates the wrapper's
|
|
89
|
+
* behaviour for tests that call the program directly. That file used to
|
|
90
|
+
* hand-mirror the pre-fix shape, so every integration test that hit a missing
|
|
91
|
+
* required option asserted an envelope production no longer emits. Sharing the
|
|
92
|
+
* builder is what makes the mirror incapable of lying.
|
|
93
|
+
*/
|
|
94
|
+
export declare function printMissingRequiredOptionEnvelope(io: ProgramIO, invokedCommand: string, message: string): void;
|
|
61
95
|
export declare function isRecommendationWorkflow(value: string): value is RecommendationWorkflow;
|
|
62
96
|
export declare function isArtifactProvider(value: string): value is ArtifactProvider;
|
|
63
97
|
export declare function isArtifactSetupStep(value: string): value is GuidedArtifactSetup['step'];
|
package/dist/cli/cli-helpers.js
CHANGED
|
@@ -88,6 +88,63 @@ export function printErrorEnvelope(io, command, code, message, data, nextActions
|
|
|
88
88
|
io.stderr(JSON.stringify(envelope, null, 2));
|
|
89
89
|
process.exitCode = 1;
|
|
90
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* The subcommand path the caller actually invoked, in the same tokens they
|
|
93
|
+
* typed (`release canary`). Derived from the registered command tree by
|
|
94
|
+
* consuming leading non-`-` argv tokens, so it never disagrees with what
|
|
95
|
+
* Commander dispatched on.
|
|
96
|
+
*
|
|
97
|
+
* Commander's `CommanderError` carries a code and a message but no command
|
|
98
|
+
* reference — `missingMandatoryOptionValue` is raised by the leaf command and
|
|
99
|
+
* throws out of `parseAsync` with nothing naming it. Without this walk the
|
|
100
|
+
* missing-option envelope could only say `command: "cli"`, which is the field
|
|
101
|
+
* the caller needs to act on.
|
|
102
|
+
*/
|
|
103
|
+
export function resolveInvokedCommandPath(program, argv) {
|
|
104
|
+
let cmd = program;
|
|
105
|
+
const parts = [];
|
|
106
|
+
for (const token of argv) {
|
|
107
|
+
if (token.startsWith('-'))
|
|
108
|
+
break;
|
|
109
|
+
const next = cmd.commands.find((c) => c.name() === token || c.aliases().includes(token));
|
|
110
|
+
if (next === undefined)
|
|
111
|
+
break;
|
|
112
|
+
parts.push(next.name());
|
|
113
|
+
cmd = next;
|
|
114
|
+
}
|
|
115
|
+
return parts.length > 0 ? parts.join(' ') : program.name();
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Render the `MISSING_REQUIRED_OPTION` envelope for a Commander
|
|
119
|
+
* `commander.missingMandatoryOptionValue`.
|
|
120
|
+
*
|
|
121
|
+
* A `.requiredOption()` the caller did not supply is raised as a plain
|
|
122
|
+
* `Error`-shaped `CommanderError`, so an unhandled `.catch()` used to file it
|
|
123
|
+
* under `UNHANDLED_ERROR` — "you left out an argument" reported as a crash,
|
|
124
|
+
* with empty `nextActions`, `command: "cli"` and no way to see WHICH option.
|
|
125
|
+
* The flags string Commander formats carries both the name and the accepted
|
|
126
|
+
* values (`--percent <10|50>`), so it is worth extracting rather than
|
|
127
|
+
* paraphrasing: it is the option's own declaration.
|
|
128
|
+
*
|
|
129
|
+
* Exported because it has TWO callers that must not drift: the real wrapper
|
|
130
|
+
* (`src/cli/index.ts`) and the in-process test runner
|
|
131
|
+
* (`tests/integration/_cli-helper.ts`), which re-creates the wrapper's
|
|
132
|
+
* behaviour for tests that call the program directly. That file used to
|
|
133
|
+
* hand-mirror the pre-fix shape, so every integration test that hit a missing
|
|
134
|
+
* required option asserted an envelope production no longer emits. Sharing the
|
|
135
|
+
* builder is what makes the mirror incapable of lying.
|
|
136
|
+
*/
|
|
137
|
+
export function printMissingRequiredOptionEnvelope(io, invokedCommand, message) {
|
|
138
|
+
const option = /required option '([^']+)'/.exec(message)?.[1];
|
|
139
|
+
printErrorEnvelope(io, invokedCommand, 'MISSING_REQUIRED_OPTION', option === undefined
|
|
140
|
+
? message
|
|
141
|
+
: `Missing required option '${option}' for \`peaks ${invokedCommand}\`.`, { option: option ?? null }, [
|
|
142
|
+
option === undefined
|
|
143
|
+
? `Run \`peaks ${invokedCommand} --help\` to see the options this command requires.`
|
|
144
|
+
: `Supply ${option} — it is required, so the command has no default for it.`,
|
|
145
|
+
`Run \`peaks ${invokedCommand} --help\` for the option's accepted values and its siblings.`
|
|
146
|
+
]);
|
|
147
|
+
}
|
|
91
148
|
export function isRecommendationWorkflow(value) {
|
|
92
149
|
return value === 'code-refactor' || value === 'product-refactor' || value === 'frontend-design';
|
|
93
150
|
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { existsSync, readFileSync } from 'node:fs';
|
|
8
8
|
import { join } from 'node:path';
|
|
9
9
|
import { addJsonOption, getErrorMessage, printResult } from '../cli-helpers.js';
|
|
10
|
+
import { isUnsafePathInput } from '../../shared/path-safety.js';
|
|
10
11
|
import { fail, ok } from 'peaks-loop-shared/result';
|
|
11
12
|
import { readJobShapeDecision, writeJobShapeDecision, JobShapeDecisionError, JOB_SHAPE_NOT_DECIDED, JOB_SHAPE_ALREADY_DECIDED } from '../../services/code/job-shape-decision.js';
|
|
12
13
|
import { findProjectRoot } from '../../services/config/config-safety.js';
|
|
@@ -60,6 +61,13 @@ export function registerCodeJobShapeCommands(code, io) {
|
|
|
60
61
|
process.exitCode = 1;
|
|
61
62
|
return;
|
|
62
63
|
}
|
|
64
|
+
// Sid axis. `--session-id` reaches both the `last-prompt.txt` read and
|
|
65
|
+
// `writeJobShapeDecision`, so one guard at resolution covers the file.
|
|
66
|
+
if (isUnsafePathInput(sessionId)) {
|
|
67
|
+
printResult(io, fail('code.detect-job', 'INVALID_SESSION_ID', `Invalid session id: ${sessionId} (must be a single path segment)`, { provided: sessionId }, ['Pass a session id that is a single path segment']), opts.json);
|
|
68
|
+
process.exitCode = 1;
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
63
71
|
// Prompt source: explicit --prompt > last-prompt.txt > empty.
|
|
64
72
|
let promptText = opts.prompt ?? '';
|
|
65
73
|
if (opts.prompt === undefined) {
|
|
@@ -16,6 +16,7 @@ import { runAutoCompact } from '../../services/code/auto-compact-orchestrator.js
|
|
|
16
16
|
import { auditContext } from '../../services/context/context-audit.js';
|
|
17
17
|
import { syncHarnessWindowForProject } from '../../services/context/auto-compact-reader.js';
|
|
18
18
|
import { describeHarnessWindowSync, harnessWindowSyncWarning } from '../../services/context/harness-window-config.js';
|
|
19
|
+
import { describeHarnessWitness, readAndCompareHarnessWitness } from '../../services/context/harness-context-witness.js';
|
|
19
20
|
import { resolveCanonicalProjectRoot } from '../../services/config/config-service.js';
|
|
20
21
|
import { buildContextAuditHint } from '../../services/context/context-audit-hint.js';
|
|
21
22
|
import { evaluateStep08, STEP_08_BACKUP_REGEX } from '../../services/code/step-08-gate.js';
|
|
@@ -249,6 +250,16 @@ export function registerCodeRuntimeCommands(code, io) {
|
|
|
249
250
|
env: process.env,
|
|
250
251
|
promptSizeBytes
|
|
251
252
|
});
|
|
253
|
+
// Promote `--project .` (what the PreToolUse hook passes) to the git
|
|
254
|
+
// root ONCE, for both of the consumers below: the settings path the
|
|
255
|
+
// harness window is written to, and the session directory the witness
|
|
256
|
+
// is read from, must not depend on the caller's cwd — and an absolute
|
|
257
|
+
// path is what the envelope then reports back to the operator. This
|
|
258
|
+
// resolves through `git rev-parse --show-toplevel`, i.e. a whole
|
|
259
|
+
// process spawn (measured 2026-09-14: med 111.8 ms per call on this
|
|
260
|
+
// host), so calling it twice with the same argument charged the witness
|
|
261
|
+
// read it precedes — 0.027 ms — roughly 4,000x its own cost.
|
|
262
|
+
const canonicalProjectRoot = resolveCanonicalProjectRoot(opts.project);
|
|
252
263
|
// Slice 2026-09-13-auto-compact-trigger-ownership (T1 + T2): materialize
|
|
253
264
|
// the window this probe just divided by into the harness's own settings,
|
|
254
265
|
// so "85%" here and the harness's own trigger are one point on one
|
|
@@ -256,11 +267,7 @@ export function registerCodeRuntimeCommands(code, io) {
|
|
|
256
267
|
// no-op when the probe carried no token window — peaks-loop never
|
|
257
268
|
// invents a number it did not measure.
|
|
258
269
|
const harnessWindow = syncHarnessWindowForProject({
|
|
259
|
-
|
|
260
|
-
// root first: the settings path this writes to must not depend on
|
|
261
|
-
// the caller's cwd, and an absolute path is what the envelope then
|
|
262
|
-
// reports back to the operator.
|
|
263
|
-
projectRoot: resolveCanonicalProjectRoot(opts.project),
|
|
270
|
+
projectRoot: canonicalProjectRoot,
|
|
264
271
|
env: process.env,
|
|
265
272
|
tokens: probe.capacityTokens ?? null
|
|
266
273
|
});
|
|
@@ -271,6 +278,23 @@ export function registerCodeRuntimeCommands(code, io) {
|
|
|
271
278
|
// sentence; this one line rides `warnings` so a JSON consumer cannot
|
|
272
279
|
// miss it either.
|
|
273
280
|
const harnessWindowWarning = harnessWindowSyncWarning(harnessWindow);
|
|
281
|
+
// Slice 2026-09-13-statusline-window-witness (AC2/AC3): the harness's
|
|
282
|
+
// own number for the same quantity, captured by the statusline. This is
|
|
283
|
+
// OBSERVATION ONLY — it never feeds `verdict` / `action` / any threshold
|
|
284
|
+
// below. A wrong reading here must not be able to fire a compact.
|
|
285
|
+
const harnessWitness = readAndCompareHarnessWitness({
|
|
286
|
+
// The same promoted root as the harness-window write above, for the
|
|
287
|
+
// same reason: the statusline resolves its root from the harness
|
|
288
|
+
// payload (absolute), so a `--project .` from a hook would otherwise
|
|
289
|
+
// look for the witness somewhere else and report `absent` forever.
|
|
290
|
+
projectRoot: canonicalProjectRoot,
|
|
291
|
+
sessionId,
|
|
292
|
+
peaksRatio: probe.ratio,
|
|
293
|
+
peaksTokens: probe.rawTokens ?? null,
|
|
294
|
+
peaksWindowTokens: probe.capacityTokens ?? null,
|
|
295
|
+
outerSessionId: outerSessionId ?? null
|
|
296
|
+
});
|
|
297
|
+
const witnessNotice = describeHarnessWitness(harnessWitness);
|
|
274
298
|
const ratioPct = (probe.ratio * 100).toFixed(1);
|
|
275
299
|
let action = 'ok';
|
|
276
300
|
let next = null;
|
|
@@ -331,8 +355,16 @@ export function registerCodeRuntimeCommands(code, io) {
|
|
|
331
355
|
// window sync did on this probe. Reported rather than silent — the
|
|
332
356
|
// harness tells a user who overrides the window only via
|
|
333
357
|
// `/autocompact`, so peaks-loop must be the one that says it.
|
|
334
|
-
harnessWindow
|
|
335
|
-
|
|
358
|
+
harnessWindow,
|
|
359
|
+
// Slice 2026-09-13-statusline-window-witness: the second scale.
|
|
360
|
+
harnessWitness
|
|
361
|
+
}, [
|
|
362
|
+
...(harnessWindowWarning === null ? [] : [harnessWindowWarning]),
|
|
363
|
+
// AC3: the disagreement rides `warnings` — it is a fact about the
|
|
364
|
+
// state, not an instruction — and it is one-way. Never an
|
|
365
|
+
// AskUserQuestion (see .peaks/memory/auto-compact-threshold-policy.md).
|
|
366
|
+
...(witnessNotice === null ? [] : [witnessNotice])
|
|
367
|
+
], [
|
|
336
368
|
action === 'red-line'
|
|
337
369
|
? `RED LINE: ≥ 95%. Next: \`${next}\` — peaks-loop asks the harness to compact and KEEPS WORKING (dispatch is not blocked); re-probe to confirm it landed.`
|
|
338
370
|
: action === 'auto-compact-now'
|
|
@@ -343,7 +375,15 @@ export function registerCodeRuntimeCommands(code, io) {
|
|
|
343
375
|
gateModeNotice,
|
|
344
376
|
// Single wording, shared with `peaks code auto-compact` — see
|
|
345
377
|
// `describeHarnessWindowSync`.
|
|
346
|
-
describeHarnessWindowSync(harnessWindow)
|
|
378
|
+
describeHarnessWindowSync(harnessWindow),
|
|
379
|
+
// AC3: the one-way hint that accompanies the `warnings` entry. Both
|
|
380
|
+
// channels carry the SAME fact in the shape each is read for
|
|
381
|
+
// (machine-readable warning vs human/LLM advice) — see
|
|
382
|
+
// `harnessWindowSyncWarning` / `describeHarnessWindowSync` above for
|
|
383
|
+
// the same split, and note this one never asks a question.
|
|
384
|
+
...(witnessNotice === null
|
|
385
|
+
? []
|
|
386
|
+
: ['Re-probe with `peaks code context-now` to confirm; this is reported, not blocking.'])
|
|
347
387
|
]),
|
|
348
388
|
// Slice 2026-09-13-auto-compact-trigger-ownership: was hard-coded
|
|
349
389
|
// `true`, which made the declared `--json` flag a no-op and left the
|
|
@@ -9,6 +9,48 @@ import { readHarnessWindowState, resolveHarnessWindowLocation } from '../../serv
|
|
|
9
9
|
import { disableHarnessWindowSync, reenableHarnessWindowSync, resetHarnessWindow } from '../../services/context/harness-window-config.js';
|
|
10
10
|
import { buildRecommendEnvelopePure, dryRunCompact, suggestCompact } from '../../services/compact/suggest-service.js';
|
|
11
11
|
import { PHASES, SURVIVAL_TABLE, isPhase, lookupPhaseTransition } from '../../services/compact/decision-tables.js';
|
|
12
|
+
import { readSessionIdFromHookPayload, readTriggerFromHookPayload, settleCompactFromHarnessEvent } from '../../services/code/compact-event-settle.js';
|
|
13
|
+
/**
|
|
14
|
+
* Read the `PostCompact` hook payload from stdin.
|
|
15
|
+
*
|
|
16
|
+
* `PEAKS_HOOK_STDIN` is the test seam, verbatim the one `peaks gate enforce`
|
|
17
|
+
* uses (`gate-commands.ts`) — the CLI side owns `process.stdin`'s lifecycle, so
|
|
18
|
+
* the reader lives with the command rather than in the service.
|
|
19
|
+
*
|
|
20
|
+
* Returns `null` for every shape that is not a JSON object: empty stdin, a TTY
|
|
21
|
+
* (the `--json` diagnostic path run by a human), malformed JSON. A payload that
|
|
22
|
+
* cannot be read is not a failure — it is a payload with no `trigger`, which is
|
|
23
|
+
* an input this path is explicitly designed to accept.
|
|
24
|
+
*/
|
|
25
|
+
async function readHookPayload() {
|
|
26
|
+
const override = process.env.PEAKS_HOOK_STDIN;
|
|
27
|
+
let raw;
|
|
28
|
+
if (override !== undefined) {
|
|
29
|
+
raw = override;
|
|
30
|
+
}
|
|
31
|
+
else if (process.stdin.isTTY) {
|
|
32
|
+
raw = '';
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
raw = await new Promise((resolveStdin) => {
|
|
36
|
+
let data = '';
|
|
37
|
+
process.stdin.setEncoding('utf8');
|
|
38
|
+
process.stdin.on('data', (chunk) => {
|
|
39
|
+
data += chunk;
|
|
40
|
+
});
|
|
41
|
+
process.stdin.on('end', () => resolveStdin(data));
|
|
42
|
+
process.stdin.on('error', () => resolveStdin(data));
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
if (raw.trim().length === 0)
|
|
46
|
+
return null;
|
|
47
|
+
try {
|
|
48
|
+
return JSON.parse(raw);
|
|
49
|
+
}
|
|
50
|
+
catch { // TODO(g2): legacy silent catch — grace: 1 minor release (v2.14.0)
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
12
54
|
function splitList(value) {
|
|
13
55
|
if (!value)
|
|
14
56
|
return [];
|
|
@@ -429,4 +471,72 @@ export function registerCompactCommands(program, io) {
|
|
|
429
471
|
process.exitCode = 1;
|
|
430
472
|
}
|
|
431
473
|
}));
|
|
474
|
+
// 8. peaks compact settle [--json]
|
|
475
|
+
// (slice 2026-09-13-compact-event-settle)
|
|
476
|
+
//
|
|
477
|
+
// The `PostCompact` hook's transport. Before this command, peaks-loop knew a
|
|
478
|
+
// compaction had landed only because a LATER probe measured a ratio that had
|
|
479
|
+
// fallen — an inference. This command lets the harness's own event settle the
|
|
480
|
+
// run, and records what the harness said the compaction WAS (`manual` /
|
|
481
|
+
// `auto`), which is the one question a ratio can never answer.
|
|
482
|
+
//
|
|
483
|
+
// STDOUT IS CONTEXT HERE. `PostCompact`'s output contract is truncated in the
|
|
484
|
+
// retrievable docs, so the safe assumption is the `SessionStart` one: stdout
|
|
485
|
+
// is added to the model's context. The hook path therefore prints NOTHING and
|
|
486
|
+
// exits 0 for every outcome — success, no open run, unresolvable session,
|
|
487
|
+
// unparseable stdin. An error sentence in the model's context is a false
|
|
488
|
+
// problem handed to it mid-slice. Same discipline as `session reinject`;
|
|
489
|
+
// `--json` is the diagnostic surface, and the hook never passes it.
|
|
490
|
+
addJsonOption(compact
|
|
491
|
+
.command('settle')
|
|
492
|
+
.description('PostCompact hook handler: settle the open compact run because the HARNESS reported a ' +
|
|
493
|
+
'compaction completed, and append an `observed` compact-history row carrying the ' +
|
|
494
|
+
'harness-reported trigger (manual | auto). Reads the hook JSON on stdin; prints nothing ' +
|
|
495
|
+
'and exits 0 when run by the hook. Pass --json for the diagnostic envelope.')
|
|
496
|
+
.option('--project <path>', 'project root (defaults to git root or cwd)')
|
|
497
|
+
.option('--session-id <sid>', 'override the active session id (defaults to the canonical binding)')).action(async (options) => {
|
|
498
|
+
// One parse, two fields: reading stdin twice would let the two readers
|
|
499
|
+
// disagree about the payload they were handed.
|
|
500
|
+
const payload = await readHookPayload();
|
|
501
|
+
const trigger = readTriggerFromHookPayload(payload);
|
|
502
|
+
const hookSessionId = readSessionIdFromHookPayload(payload);
|
|
503
|
+
// The hook path returns before any `process.exitCode` assignment below, so
|
|
504
|
+
// the harness never sees a non-zero exit from this command.
|
|
505
|
+
try {
|
|
506
|
+
const project = options.project !== undefined
|
|
507
|
+
? resolveCanonicalProjectRoot(options.project)
|
|
508
|
+
: (findProjectRoot(process.cwd()) ?? process.cwd());
|
|
509
|
+
const session = resolveSessionId(project, options.sessionId);
|
|
510
|
+
if (session.error !== null) {
|
|
511
|
+
if (options.json !== true)
|
|
512
|
+
return;
|
|
513
|
+
printResult(io, fail('compact.settle', session.error.code, session.error.message, { projectRoot: project }, session.error.nextActions), true);
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
const result = settleCompactFromHarnessEvent({
|
|
517
|
+
projectRoot: project,
|
|
518
|
+
sessionId: session.sid,
|
|
519
|
+
trigger,
|
|
520
|
+
hookSessionId
|
|
521
|
+
});
|
|
522
|
+
if (options.json !== true)
|
|
523
|
+
return;
|
|
524
|
+
printResult(io, ok('compact.settle', { projectRoot: project, sessionId: session.sid, trigger: trigger ?? null, ...result }, [], [
|
|
525
|
+
result.settled
|
|
526
|
+
? result.lifecycleWritten
|
|
527
|
+
? result.historyWritten
|
|
528
|
+
? `Settled run ${result.runId}; appended a kind:'observed' row with pathway 'post-compact-hook'.`
|
|
529
|
+
: `Settled run ${result.runId}, but the kind:'observed' row could NOT be appended — check that .peaks/_runtime/<sid>/ is writable.`
|
|
530
|
+
: `The harness reported a compaction for run ${result.runId}, but the lifecycle record could NOT be written — that run is still open, so a later probe will settle it from its own measurement and append the row then. Nothing was recorded for this event: a row here would assert a settlement the store never made.`
|
|
531
|
+
: result.reason === 'different-session'
|
|
532
|
+
? `The hook payload names a different harness session, so this project's open compact run was left alone.`
|
|
533
|
+
: 'No compact run was open, so nothing was settled and no history row was appended.'
|
|
534
|
+
]), true);
|
|
535
|
+
}
|
|
536
|
+
catch (error) {
|
|
537
|
+
if (options.json !== true)
|
|
538
|
+
return;
|
|
539
|
+
printResult(io, fail('compact.settle', 'COMPACT_SETTLE_FAILED', getErrorMessage(error), {}, ['Verify the project path and session binding']), true);
|
|
540
|
+
}
|
|
541
|
+
});
|
|
432
542
|
}
|
|
@@ -91,10 +91,11 @@ function registerConfigMigrationCommands(config, io) {
|
|
|
91
91
|
printResult(io, ok('config.rollback', { ...plan, applied: false }), options.json);
|
|
92
92
|
}
|
|
93
93
|
catch (error) {
|
|
94
|
+
// No `NO_BACKUP` branch: a missing `.bak` is no longer an error, it is
|
|
95
|
+
// an `available: false` SUCCESS envelope (see `config-rollback.ts`).
|
|
94
96
|
const message = getErrorMessage(error);
|
|
95
|
-
|
|
96
|
-
io.
|
|
97
|
-
printResult(io, fail('config.rollback', code, message, {}, ['Re-run peaks config migrate --apply to recreate the .bak']), options.json);
|
|
97
|
+
io.stderr(`CONFIG_ROLLBACK_FAILED: ${message}`);
|
|
98
|
+
printResult(io, fail('config.rollback', 'CONFIG_ROLLBACK_FAILED', message, {}, ['Re-run peaks config migrate --apply to recreate the .bak']), options.json);
|
|
98
99
|
process.exitCode = 1;
|
|
99
100
|
}
|
|
100
101
|
});
|
|
@@ -108,9 +109,14 @@ function registerConfigMigrationCommands(config, io) {
|
|
|
108
109
|
.option('--json', 'JSON envelope output')
|
|
109
110
|
.action((options) => {
|
|
110
111
|
try {
|
|
112
|
+
// `available` is on every envelope this command prints (see
|
|
113
|
+
// `config-restore.ts`): `false` ⇒ this machine has no `.bak`, exit 0,
|
|
114
|
+
// nothing to restore; `true` on a FAILURE envelope ⇒ the backup is
|
|
115
|
+
// there and the field/guard was the problem, exit 1. One key replaces
|
|
116
|
+
// the exit code as the "was there ever a backup?" signal.
|
|
111
117
|
if (options.list === true || !options.field) {
|
|
112
|
-
const fields = listAvailableFields();
|
|
113
|
-
printResult(io, ok('config.restore', { fields, applied: false }), options.json);
|
|
118
|
+
const { available, fields } = listAvailableFields();
|
|
119
|
+
printResult(io, ok('config.restore', { available, fields, applied: false }), options.json);
|
|
114
120
|
return;
|
|
115
121
|
}
|
|
116
122
|
const apply = options.apply === true;
|
|
@@ -119,15 +125,15 @@ function registerConfigMigrationCommands(config, io) {
|
|
|
119
125
|
}
|
|
120
126
|
catch (error) {
|
|
121
127
|
const message = getErrorMessage(error);
|
|
128
|
+
// A missing `.bak` no longer reaches this block, so anything that does
|
|
129
|
+
// implies the backup exists — which is what `available: true` reports.
|
|
122
130
|
let code = 'CONFIG_RESTORE_FAILED';
|
|
123
|
-
if (message.startsWith('
|
|
124
|
-
code = 'NO_BACKUP';
|
|
125
|
-
else if (message.startsWith('RESTORE_GUARDED'))
|
|
131
|
+
if (message.startsWith('RESTORE_GUARDED'))
|
|
126
132
|
code = 'RESTORE_GUARDED';
|
|
127
133
|
else if (message.startsWith('FIELD_NOT_FOUND'))
|
|
128
134
|
code = 'FIELD_NOT_FOUND';
|
|
129
135
|
io.stderr(`${code}: ${message}`);
|
|
130
|
-
printResult(io, fail('config.restore', code, message, {}, ['Use --list to see available fields']), options.json);
|
|
136
|
+
printResult(io, fail('config.restore', code, message, { available: true, ...(options.field !== undefined ? { field: options.field } : {}) }, ['Use --list to see available fields']), options.json);
|
|
131
137
|
process.exitCode = 1;
|
|
132
138
|
}
|
|
133
139
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { resolveCanonicalProjectRoot } from '../../services/config/config-service.js';
|
|
4
|
+
import { isUnsafePathInput } from '../../shared/path-safety.js';
|
|
4
5
|
import { read24hState } from '../../services/24h-mode/index.js';
|
|
5
6
|
import { getErrorMessage } from '../cli-helpers.js';
|
|
6
7
|
const SINCE_PATTERN = /^(\d+)([smhd])$/i;
|
|
@@ -24,6 +25,11 @@ function parseSince(raw) {
|
|
|
24
25
|
return { ok: true, ms: Math.min(WINDOW_CAP_MS, value * multiplier) };
|
|
25
26
|
}
|
|
26
27
|
function readSlices(projectRoot, sessionId) {
|
|
28
|
+
// Sid axis: `--session-id` reaches this join unmodified, and a traversal
|
|
29
|
+
// value resolved `metrics/slices.jsonl` outside every project root.
|
|
30
|
+
if (isUnsafePathInput(sessionId)) {
|
|
31
|
+
throw new Error(`Invalid session id: ${sessionId} (must be a single path segment)`);
|
|
32
|
+
}
|
|
27
33
|
const path = join(projectRoot, '.peaks', '_runtime', sessionId, 'metrics', 'slices.jsonl');
|
|
28
34
|
if (!existsSync(path))
|
|
29
35
|
return 0;
|