peaks-loop 4.0.48 → 4.0.50
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/dist/cli/commands/audit-commands.js +1 -0
- package/dist/cli/commands/baseline-commands.js +163 -25
- package/dist/cli/commands/compact-command.js +1 -3
- package/dist/cli/commands/core/skill-command.js +53 -4
- package/dist/cli/commands/core/standards-command.d.ts +24 -0
- package/dist/cli/commands/core/standards-command.js +74 -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 +55 -38
- package/dist/cli/commands/loop-eval-commands.js +22 -6
- package/dist/cli/commands/share-commands.js +37 -11
- package/dist/cli/commands/slice-integrate-commands.js +17 -0
- package/dist/cli/commands/web-commands.js +8 -1
- package/dist/cli/commands/workflow-lifecycle-commands.d.ts +6 -0
- package/dist/cli/commands/workflow-lifecycle-commands.js +64 -3
- package/dist/services/adapter/adapter.d.ts +30 -0
- package/dist/services/adapter/auto-adapter.d.ts +13 -0
- package/dist/services/adapter/claude-adapter.js +12 -0
- package/dist/services/adapter/codex-adapter.d.ts +12 -0
- package/dist/services/adapter/codex-adapter.js +12 -0
- package/dist/services/adapter/copilot-adapter.d.ts +12 -0
- package/dist/services/adapter/copilot-adapter.js +12 -0
- package/dist/services/artifacts/artifact-prerequisites.js +10 -0
- package/dist/services/artifacts/request-artifact-service.js +59 -38
- package/dist/services/audit/backing-detector.d.ts +25 -7
- package/dist/services/audit/backing-detector.js +33 -17
- package/dist/services/audit/enforcer-liveness.d.ts +12 -0
- package/dist/services/audit/enforcer-liveness.js +100 -0
- package/dist/services/audit/enforcers/active-skill-resolver.js +14 -1
- package/dist/services/audit/enforcers/lint-catalog-governance.d.ts +23 -11
- package/dist/services/audit/enforcers/lint-catalog-governance.js +10 -14
- package/dist/services/audit/enforcers/lint-rd-handoff-coverage.d.ts +5 -15
- package/dist/services/audit/enforcers/lint-rd-handoff-coverage.js +94 -25
- package/dist/services/audit/enforcers/lint-style.d.ts +9 -1
- package/dist/services/audit/enforcers/lint-style.js +38 -2
- package/dist/services/audit/prose-ratio-calculator.d.ts +28 -17
- package/dist/services/audit/prose-ratio-calculator.js +25 -18
- package/dist/services/audit/red-line-catalog-p2-a.js +1 -1
- package/dist/services/audit/red-lines-service.js +51 -7
- package/dist/services/capability-audit-service/independent-checker.d.ts +15 -0
- package/dist/services/capability-audit-service/independent-checker.js +140 -0
- package/dist/services/capability-audit-service/index.d.ts +3 -1
- package/dist/services/capability-audit-service/index.js +1 -0
- package/dist/services/capability-audit-service/runner.d.ts +17 -13
- package/dist/services/capability-audit-service/runner.js +76 -15
- package/dist/services/capability-audit-service/types.d.ts +48 -0
- package/dist/services/capability-guard-runner/contracts/J01.js +21 -22
- package/dist/services/capability-guard-runner/contracts/J02.d.ts +1 -1
- package/dist/services/capability-guard-runner/contracts/J02.js +114 -28
- package/dist/services/capability-guard-runner/contracts/J03.d.ts +13 -0
- package/dist/services/capability-guard-runner/contracts/J03.js +72 -21
- package/dist/services/capability-guard-runner/contracts/J04.d.ts +6 -0
- package/dist/services/capability-guard-runner/contracts/J04.js +65 -32
- package/dist/services/capability-guard-runner/contracts/J05.js +118 -16
- package/dist/services/capability-guard-runner/contracts/J06.d.ts +14 -0
- package/dist/services/capability-guard-runner/contracts/J06.js +57 -39
- package/dist/services/capability-guard-runner/contracts/J07.d.ts +9 -0
- package/dist/services/capability-guard-runner/contracts/J07.js +76 -47
- package/dist/services/capability-guard-runner/contracts/J08.d.ts +11 -0
- package/dist/services/capability-guard-runner/contracts/J08.js +66 -39
- package/dist/services/capability-guard-runner/contracts/J09.d.ts +13 -0
- package/dist/services/capability-guard-runner/contracts/J09.js +95 -39
- package/dist/services/capability-guard-runner/contracts/J10.d.ts +12 -0
- package/dist/services/capability-guard-runner/contracts/J10.js +69 -35
- package/dist/services/capability-guard-runner/contracts/J11.d.ts +8 -0
- package/dist/services/capability-guard-runner/contracts/J11.js +73 -33
- package/dist/services/capability-guard-runner/contracts/J12.d.ts +12 -0
- package/dist/services/capability-guard-runner/contracts/J12.js +66 -30
- package/dist/services/capability-guard-runner/contracts/J13.d.ts +11 -0
- package/dist/services/capability-guard-runner/contracts/J13.js +62 -40
- package/dist/services/capability-guard-runner/contracts/J14.d.ts +11 -0
- package/dist/services/capability-guard-runner/contracts/J14.js +60 -31
- package/dist/services/capability-guard-runner/contracts/J15.d.ts +11 -0
- package/dist/services/capability-guard-runner/contracts/J15.js +70 -35
- package/dist/services/capability-guard-runner/contracts/_shared.d.ts +24 -0
- package/dist/services/capability-guard-runner/contracts/_shared.js +67 -0
- package/dist/services/capability-guard-runner/registry.d.ts +5 -0
- package/dist/services/capability-guard-runner/registry.js +140 -0
- package/dist/services/capability-guard-runner/runner.d.ts +26 -0
- package/dist/services/capability-guard-runner/runner.js +63 -6
- package/dist/services/code/auto-compact-lifecycle.d.ts +75 -0
- package/dist/services/code/auto-compact-lifecycle.js +65 -16
- package/dist/services/code/auto-compact-modes.d.ts +13 -2
- package/dist/services/code/auto-compact-modes.js +20 -4
- package/dist/services/code/auto-compact-orchestrator.js +119 -19
- package/dist/services/code/compact-event-settle.d.ts +20 -8
- package/dist/services/code/compact-event-settle.js +21 -0
- package/dist/services/code/post-compact-detector.js +20 -11
- package/dist/services/code/step-08-gate.js +21 -6
- package/dist/services/compact-statusline/compact-statusline-service.js +56 -22
- package/dist/services/config/config-safety.js +11 -9
- package/dist/services/context/auto-compact-types.d.ts +20 -2
- 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/pre-post-diff.js +10 -2
- package/dist/services/job/job-progress-store.js +18 -3
- 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 +11 -4
- package/dist/services/observability/observability-service.js +16 -3
- package/dist/services/prd/handoff-service.js +43 -0
- package/dist/services/qa/qa-business-review-state.js +19 -5
- package/dist/services/sc/sc-service.d.ts +8 -0
- package/dist/services/sc/sc-service.js +8 -1
- package/dist/services/scan/api-diff-types.js +20 -2
- package/dist/services/security/safe-settings-path.js +19 -1
- package/dist/services/session/getSessionDir.d.ts +33 -0
- package/dist/services/session/getSessionDir.js +60 -0
- package/dist/services/skill/skill-search-service.d.ts +3 -3
- package/dist/services/slice/slice-review-state.js +19 -4
- package/dist/services/standards/loop-engineering-lint.d.ts +1 -1
- package/dist/services/standards/loop-engineering-lint.js +6 -0
- package/dist/services/web/daemon-registry.js +27 -2
- package/dist/services/workflow/pipeline-verify-gate-support.js +10 -11
- package/dist/services/workflow/pipeline-verify-service.d.ts +1 -1
- package/dist/services/workflow/pipeline-verify-service.js +23 -10
- package/dist/services/workflow/pipeline-verify-types.d.ts +5 -3
- package/dist/services/workspace/claude-settings-template.d.ts +53 -37
- package/dist/services/workspace/claude-settings-template.js +105 -83
- package/dist/services/workspace/generated-artifacts-stamp.d.ts +119 -0
- package/dist/services/workspace/generated-artifacts-stamp.js +167 -0
- package/dist/services/workspace/workspace-claude-settings-materializer.d.ts +8 -0
- package/dist/services/workspace/workspace-claude-settings-materializer.js +38 -3
- package/dist/services/workspace/workspace-service.js +11 -1
- package/dist/shared/fs-utils.d.ts +26 -0
- package/dist/shared/fs-utils.js +35 -0
- package/dist/shared/runtime-root.d.ts +73 -0
- package/dist/shared/runtime-root.js +77 -0
- package/package.json +9 -7
- package/scripts/copy-templates.mjs +0 -12
- package/scripts/install-skills.mjs +154 -53
- package/skills/bee/peaks-qa/SKILL.md +0 -1
- package/skills/bee/peaks-rd/SKILL.md +0 -1
- package/skills/peaks-code/SKILL.md +12 -10
- package/skills/peaks-code/references/periodic-checkpoint.md +2 -2
- package/skills/peaks-code/references/runbook.md +3 -0
- package/skills/peaks-code/references/session-overload-signal-index.md +4 -2
- package/skills/peaks-code/references/startup-sequence.md +2 -2
- package/skills/peaks-code/references/step-0-8-gate.md +1 -1
- package/skills/peaks-code/references/sub-agent-dispatch.md +19 -19
- package/dist/cli/commands/context-builder-commands.d.ts +0 -11
- package/dist/cli/commands/context-builder-commands.js +0 -85
- package/dist/services/hooks/write-gate.js +0 -111
- package/skills/bee/peaks-prd/references/command-migration.md +0 -3
- package/skills/bee/peaks-qa/references/command-migration.md +0 -3
- package/skills/bee/peaks-rd/references/command-migration.md +0 -3
- package/skills/bee/peaks-sc/references/command-migration.md +0 -3
- package/skills/bee/peaks-txt/references/command-migration.md +0 -3
- package/skills/bee/peaks-ui/references/command-migration.md +0 -3
- package/skills/peaks-code/references/command-migration.md +0 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,49 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.0.50 — 2026-09-16 (自审把 29% 从分母里排除 + 反漂移闸门报 15/15 而只跑了 1 条 + 一条"no exceptions"的红线没有任何执行者 + 五个测试套件从未运行过 + macOS 从未进 CI)
|
|
4
|
+
|
|
5
|
+
**Highlights**:
|
|
6
|
+
|
|
7
|
+
1. **"散文比例 0%" 是一个重新定义了自己分母的指标。** `peaks audit red-lines` 报 `proseOnly: 0`,而**同一份 JSON 里 44 行**写着 `"backing": "prose-only"` —— `classifier.ts` 把所有未匹配的 marker 标为 `informational: true`,比例计算再把它**排除出分母**。同一批修复查出 `cli-backed` 的判定是 `existsSync(enforcerRef)`:**文件在磁盘上存在即算"有机器执行"**,于是 **9 个 enforcer**(共 51 行)从未被导入、从未被调用,却被计入机器执行。诚实重算,同一棵树:**cli-backed 105 → 54,prose-only 0 → 98(64.5%)**。指标变难看,是因为它开始度量该度量的东西。另外两个同族缺陷:`lint-rd-handoff-coverage` 检查的是 **SKILL.md 里描述产物的那句话**,从不读产物文件;`release-precheck` 的 AC-9 **把 bug 本身钉成了期望值**(`proseOnly === 0`),现在钉的是"摘要与逐行计数一致"。
|
|
8
|
+
|
|
9
|
+
2. **RL-10 指定的反漂移机制在自证,而它能自证是因为它只跑了一条。** `peaks baseline audit` 输出 `verdict: consistent`、`consistencyScore: 1`、证据写着 `"capability-guard-runner:15 — 15 pass / 0 fail"`。真相是 `guardSummary` **硬编码为 `{pass:15,fail:0}`**,LLM 打分环节是**返回常量**的桩;而 `run-guard` 的派发是 `opts.journey === 'J01' ? … : {status:'skipped'}` —— **不带参数时只跑 J01**,其余 14 条返回 `skipped` 且 **exit 0**,与"通过"在退出码上不可区分。15 个 guard contract 里 13 个是 `existsSync` + 常见词子串(J10 断言 `hooks-commands.ts` 含词 `hook`;J06 的注释书面承认把闸门改松以让它通过)。现在:15 条 contract 走一个 registry 全部真实执行,`skipped` 是 **exit 2**、`fail` 是 **exit 1**;`assertBaselineRef` 真的解析冻结行与**逐字不变量文本**,不再只检查字段为真值;`freeze-update` / `rollback` / `reset` 三件套有了可达的成功路径(此前它们无条件 `fail`,**棘轮动不了**)。**诚实否定**:J10 的 `HOOK_PERMISSION_DENIED` 在基线 JSON 里存在但从未实现 —— 没有把它编码成探针,因为那会造出一盏永久的红灯;J08 的不变量措辞夸大了四个 gate 实际塌缩成一个布尔。
|
|
10
|
+
|
|
11
|
+
3. **发布闸门从"只拒一个字面量"改成白名单,于是它立刻挡住了发布。** `publish.yml` 原本只在 `verdict` **精确等于 `"drifted"`** 时失败 —— `partial` / `inconclusive` / 任何别的值都会放行。改成白名单(仅 `consistent` 放行)之后,上面那条伪造的 15/15 不再能放行任何东西,**发布被挡住了**。这是刻意的、也是被接受的:闸门不再是摆设,代价是它开始真的挡。解开它的不是放宽,而是接上一个**能失败的确定性独立检查器**(见第 4 条)。同一轮里 `describeMode()` 被发现把自己描述成 `0.85/0.95`,**漏掉真正生效的 0.80 档**;往下挖还查出一条真实分叉:`peaks skill presence`(每轮都调)在 **≥0.80** 触发 `auto-fire`,而 `peaks code context-now` —— SKILL.md 自己声明的 single source of truth —— **硬编码了它自己的 0.85**。只改描述,**没碰阈值和行为**。
|
|
12
|
+
|
|
13
|
+
4. **独立打分器:独立性是信息通道的属性,不是基质的属性。** 桩打分器不能自证之后,需要的是一个**真正独立**的评估者。选择确定性检查器而非 LLM,有两个理由:其一,**LLM 喂原来那份 payload `{baselineJourneyId, guard}` 只是更贵的橡皮图章** —— 它会把守卫结果重述一遍;其二,也是决定性的,**`publish.yml` 是 secretless 的**(OIDC,`contents: read` / `id-token: write`,任何步骤都没有 token),LLM 打分器需要 key,加一个 key 等于**用一个无密钥的发布管线去换一个非确定性的外部依赖,还塞在闸门里**。检查器改为读**冻结行 + 武装它们的 contract registry + 守卫摘要**三者比对。`llmRunner` 接缝被**删除**,而不是留成一个带假调用者的死参数。裁决语义**收紧而非放宽**:`consistent` 现在要求 `guardFail === 0` **且**零 findings,`inconclusive` 只能从 `--scorer stub` 到达。**残留如实披露**:每次运行都报 `coverage`,本轮是 `invariantsArmed 15/45`、`forbiddenChangesUnverified 30` —— 所以 `consistent` 读作"在当前度量范围内一致",不可过度解读。注入验证(真实基线 + registry,三臂 + 干净对照):对照 `consistent`;把一个冻结的 `sourceFiles` 条目指向磁盘上不存在的路径 → **`drifted`**,`SOURCE_FILE_MISSING` 点名 J01。
|
|
14
|
+
|
|
15
|
+
5. **多个"闸门"的共同形状:声称有执行者,执行者不在。** (a) `CLAUDE.md:34` 称顶层目录禁令由 `tests/unit/workspace/top-level-change-id-guard.test.ts`「**will fail the suite**」守护 —— **该文件不存在**,死于 `457b9a87`;四层防护的第 2 层缺失。(b) 红线文件称 RL-0..RL-10 由 `peaks standards lint --category loop-engineering` 强制 —— **该子命令根本没注册**,其实现函数 `lintLoopEngineeringGuidelines` **全仓零调用者**;现在动词已注册,RL-10 进了校验集,`EXPECTED_RED_LINE_IDS` 不再止于 RL-9。(c) `CLAUDE.md` 描述的 `--change-id` 流程与 `current-change` 绑定文件**两者都不存在**,且与 `.peaks/PROJECT.md` 明文矛盾 —— 以 PROJECT.md 为准修正。(d) 三份权威设计文档(含 `docs/adr/` **整个目录**)不存在,定位所依赖的结晶设计 spec **只活在记忆的散文里**。(e) `package.json` 的 `test:race` 点名 4 个文件、**只有 1 个存在**,靠 `passWithNoTests` **静默空跑退出 0**。(f) `Co-Authored-By` 红线称 "no exceptions",而 `.git/hooks` 里只有 `.sample`、无 husky/lefthook/commitlint、`tests/` 里只出现在一个 fixture 的散文中 —— 现在有了 vitest 守卫(选它而非 hook,因为 `.git/hooks` 不被 git 跟踪、clone 后不存在),失败路径是一条**永久用例**:建真仓库、提交违规信息、断言变红,配干净对照。两条排除规则是被真实历史逼出来的:**只有行首 trailer**,且**括号里的说明不是身份**(`Co-Authored-By: (omitted per CLAUDE.md red rule)` 历史上出现过两次,天真搜索**一出生就是红的**)。**残留如实披露**:它检出,不阻止。另新增一个**引用完整性守卫** —— 文档里被反引号括起的仓库路径若不存在则变红(注入验证过)。**而它自己有作用域缺口**,见第 8 条。
|
|
16
|
+
|
|
17
|
+
6. **五个测试套件从未运行过。** 整个 `tests/e2e/`(5 个真实 CLI 关键路径测试 + 3 个 shell 脚本)被**两个 vitest 配置同时排除**,且**没有任何脚本或 CI 步骤选中它**。另有 7 个测试门控在一个**无处设置**的环境变量 `PEAKS_BUILD_AVAILABLE` 上 —— 以无人设置的环境变量为条件的 skip 就是穿着戏服的永久 skip。`stryker.vitest.config.mjs` 指向两个在 `f17aa377` 被删除的路径,**变异测试按当前配置跑不起来**;修好后它跑到变异阶段并诚实报 **0.00%**:4 个被变异的文件里**有 3 个没有任何测试导入**。两条不可能失败的断言被重写并注入验证(一条上一行刚构造出 `markerPath`、下一行断言它 `endsWith(...)`,注释自认目的是消除 lint 告警;另一条测试名声称 "preserves the original errorId",而被调函数**根本没有 errorId 参数**)。`vm-kvm` 的三个测试在 CI 的全部平台上永不执行,现在 skip 原因进了测试名。**一个测试套件复活后失败的代价被如实计入而非消除。**
|
|
18
|
+
|
|
19
|
+
7. **消费者真机路径:升级不刷新生成物,而 macOS 从未进过 CI。** `npm i -g` 升级**不刷新**已生成的 `.claude/*.json` —— 唯一写入者是 `initWorkspace`,而 `ensureSession` 在项目已有绑定时**提前返回**,刷新路径不再触发;逃生舱 `peaks upgrade --apply-init` 存在,但**没有任何东西告知用户其配置已陈旧**。生成物也因此**没有版本戳**,落后与否不可判定;现在有了戳和检测器(伪装一个旧版本戳 → 检测器报 `stale: true, package-upgraded`)。管理段 `.gitignore` 是**写入一次、内容永不传播**且无漂移检测。`postinstall` **无条件在 `$HOME` 创建 10 个 IDE 目录** —— 包括用户根本没有的工具;hermes/openclaw 的 profile 不在探测表里,**永远探测不到**;`installProjectConfig` 是有定义、有导出、**零调用**的死代码。**macOS 此前不在任何 workflow 里**,于是每一类 Mac 专属缺陷**在构造上就不可见**(`findTranscriptJsonl` 的 ESM `require` 假绿 —— vitest 因 esbuild 注入的 CommonJS shim 而在完全破损的生产路径上 6/6 全绿)。矩阵现在含 `macos-latest`,并**明确承诺不抑制红色单元格**。另外 `peaks workflow init` **不解析会话绑定、静默把图写进 `unknown-sid`** —— 那个桶自 2026-09-01 起累积了**至少 4 个不同 caller id** 的产物,即**对谁都没成功过**;现在解析失败会明确报错而不是静默兜底。
|
|
20
|
+
|
|
21
|
+
8. **一个"三个同名层"的适配器结构与一批从未被测过的路径。** 9 个 IDE 适配器只有 1 个有专属测试文件,且只覆盖 compact 路径;**4 个 sub-agent dispatcher**(claudeCode / trae / traeCn / cursor)在全仓测试中**零引用**,含它们产出的 `awaitByLlm` 字段 0 命中 —— 即**"扇出到非 claude-code IDE"这条路径从未被测过**。新增 5 个测试文件 / 114 个用例,6 次注入各配干净对照。两件本不该为真的事:`share-commands.ts` **告诉用户会出现 `awaitByLlm` 标记,而没有任何 adapter 产出它**;`src/services/adapter/{adapter,auto-adapter,claude,codex,copilot}` 是**第三个同名层且完全死掉**(零 importer,方法不收参数)—— 标注而非删除,因为诊断文档引用了那些路径,删除会制造本仓正在清理的那类悬空引用。**移交不吸收**:`traeCnSubAgentDispatcher` / `awaitByLlmFallback` / `registerClaudeCodeAwait` 全仓零引用、`peaks sub-agent await` 传 `recordPaths: []` 因而**永远报不出结果**、9 个 `supportsScope` 谓词全是同义反复 —— 全部报出,未修。同一轮查出**引用完整性守卫自身的作用域缺口**:`de0872b7`(2026-07-05)把 9 个技能移到 `skills/bee/` 却**没有更新指向它们的路径**,而守卫的语料(`CLAUDE.md` / `.peaks/PROJECT.md` / `README.md` / `.peaks/standards`)**不含 `skills/**`** —— 锚点正则接受 `skills/`,语料不含它。**未修,刻意**:扩围守卫会让套件变红,扩围与修复必须同一片落地;规模只作线索(临时扫描器给出 313 处引用 / 95 处缺失,但本仓规矩是用已有 AST 守卫而非另写正则扫描)。
|
|
22
|
+
|
|
23
|
+
9. **第二个编辑闸门是个空操作,而它的弃用理由论证是对的。** `write-gate.js` 的 `decide()` **对每一条路径都弃权**,且这是刻意的、有完整论证的:退出码 2 才阻塞,其他非零码是**非阻塞错误** —— 动作照常执行,但每次编辑都在 transcript 里刷 `<hook> hook error`,所以旧的"静默 fall through 到闸门"其实是**最吵的那个结果**。弃权正确,**安装不该存在**:它往每个消费者的配置里放了一条 `node "C:/…/nvm/v24.14.0/…"` 指令,**钉死在安装时解析到的那一个 Node 版本目录上**,外加一个 `shell: powershell` 钉。删除的是**安装**而非那个决策,论证保留在 `.claude/HOOKS.md`;**没有**恢复那个 handler —— 那正是本轮的清理对象:一个零调用者却因为"有东西指着它"而活着的东西。删除顺带暴露了刷新路径里的一个真 bug:`templateContentMatches` **把超集判成 current**,于是**已退役的条目原样存活**;修完后在真 CLI 上验证 3 条 → 2 条。
|
|
24
|
+
|
|
25
|
+
10. **两套编辑闸门装在同一 matcher 上而无人协调**(matcher 写法顺序都不同),且一个消费者项目的会话绑定解析、hook 装配与生成物刷新此前**没有任何一处**是端到端验证过的。本轮的每一次修复都配了**能失败的验证**:能红、能复现、红的理由精确到行号或文件名。**已知未闭合**:`src/` 下已无 `.js` 文件,因此 vendor-neutrality 守卫里那条"`.js` 扩展名盲区"断言被删除,该盲区现在由结构与散文覆盖、**不再由测试覆盖** —— 这是本轮唯一一处**把覆盖换成更弱而非更强**的地方,如实记录而非留给读者发现。
|
|
26
|
+
|
|
27
|
+
## 4.0.49 — 2026-09-15 (闸门验的是描述产物的话而不是产物本身 + id 轴三次布防后改走结构 + 两处吞异常与一个不是证据的 mtime + 套件自己把夹具写进了收集 glob)
|
|
28
|
+
|
|
29
|
+
**Highlights**:
|
|
30
|
+
|
|
31
|
+
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**。
|
|
32
|
+
|
|
33
|
+
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 个文件**,编译器就是工作清单。
|
|
34
|
+
|
|
35
|
+
3. **压缩循环:两处吞异常、一行断言了从未发生的结算、一个不是证据的 mtime。** `settleOpenLifecycleRun` 的生命周期写入失败被吞掉 → run 停在 `armed` → **之后每一次探针都会再结算一次、再追加一行**,无界。两条结算路径中先只修了一条,另一条照旧追加;现在两条都**推迟**那一行并如实说明,run 记为**未结算**,而不是在仍处 `armed` 时报成功。状态线**把 history 文件的 mtime 当作"刚刚压缩过"的证据** —— 而那个文件**每次被"询问"就写一行 dispatch**,所以**新鲜度恰恰在什么都没压缩的时候达到峰值**(仓库自己的会话在该端点有 **1075 行 dispatch、0 次压缩**);现在要求窗口内存在一行 `observed`,且配了非真空性对照证明旧谓词确实会接受同一个夹具。`getSessionDir` 增加一个**总体变体**,让"不能抛"的调用方有东西可调而不是吞掉;可观测性服务恢复它被记录过两次的 never-throws 契约。
|
|
36
|
+
|
|
37
|
+
4. **套件自己把夹具写进了它收集的 glob 里。** `bdd-reporter.test.ts` 在 `tests/unit/` **里面**创建夹具、在 `afterEach` 删掉,而那是 vitest 的收集范围 —— 并发跑会在收集到它之后**四秒**导入失败,在**无关的套件**里报 `Cannot find module`。这件事**此前被记录过四次、一次都没修**(三份产物加一份已提交的记忆)。修完顺带量到一条比修复本身更大的事:**与测试同时跑的 `tsc` 不是证据** —— 中止时总数**错低**(一条孤立 `TS6053`、墙钟塌到约两秒),而**真实夹具在场时污染是隐形的**(总数恰好等于干净基线),是个**静默的错数**而不是显眼的错数;三选一里的判决是"在安静的树上取,并要求 `TS6053 = 0`"。非真空性对照:把夹具配置的 `include` 指向不匹配的 glob → 四个用例**全部失败**,证明它们确实依赖嵌套运行真的执行那个夹具。
|
|
38
|
+
|
|
39
|
+
5. **一行观测被追加了 2054 次,而原因是我们自己的基准测试。** 一次"只读"性能评审的微基准对着**真实** history 文件跑了两遍(`readFileSync` 取最后一行 → `appendFileSync` 写回同一文件 → 在计时循环里重复);因为载荷是**从文件里读回来**的,每次重放**逐字节相同、连 `ts` 都相同**。算术精确闭合:**1 条真事件 + 503 + 1550 = 2054 行**,占该文件的 **53.8%**。**没有产品缺陷** —— 两个候选机制(写入端有循环 / 存在一个 drain)**都被证伪**(orchestrator 里 0 个循环,且 `ts` 在对象字面量里构造,所以 N 次产品调用必然给出 N 个不同戳)。证据**一行未删**:删掉它让文件显得整洁,正是这条线存在的理由所要消灭的动作。
|
|
40
|
+
|
|
41
|
+
**验证**:三个版本常量一致(**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` 用例是**负载敏感**的(并行负载下超时、单独跑通过)—— **点名,不两边计数**。
|
|
42
|
+
|
|
43
|
+
**明确未验证的(不当作已完成)**:**没有任何一次真实的 Claude Code `PostCompact` 事件被观察到** —— 两条结算路径的行为是用只读 store 驱动的,证明的是"命令会结算",**从不是"事件会到达"**;`handoff-service.ts` 未被路由到新缝(把它的相对形式接进来会把 5 号修掉的那条片段往返载体重新引回来);结构性质的 95 个剩余槽位**未迁移**,所以它目前只在 2 个文件上成立。
|
|
44
|
+
|
|
45
|
+
**本轮顺带发现、尚未处理(不属本版修复)**:**结构缝自己的注释里有一句假话**(`runtime-root.ts:36-37` 声称模块外 `x as GuardedSegment` 是类型错误,**实测为假**);行内拼写仍编译干净,而用 AST 普查做一条字面量禁令(本仓已写过两次这个模式)可以补上;`dir()` 旁路;规则 D 自己的套件在 import 时扫描整个 `tests/` 并会与并发夹具写入者竞争(本版修的正是这个类);49 个文件上的 95 个未守卫槽位;六个原始 rid 的闸门状态仍停在 `spec-locked`。
|
|
46
|
+
|
|
3
47
|
## 4.0.48 — 2026-09-14 (一个 id 没校验就进了路径 + 自己的消费者读不出的交接胶囊 + 结尾才写、开头就读的状态)
|
|
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.50 (2026-09-16) |
|
|
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.50(2026-09-16) |
|
|
144
144
|
| **覆盖域** | 代码(`peaks-code`) · 内容(`peaks-content`) · 项目健康(`peaks-doctor`) · 批量修 issue(`peaks-issue-fix-orchestrator`) · 自定义 SOP(`peaks-sop`) · 通用原语(`peaks-solo` 分诊 / `peaks-resume` 续 / `peaks-status` 看 / `peaks-test` 测 / `peaks-slice-decompose` 切片) |
|
|
145
145
|
| **沉淀池** | `~/.peaks/` 本地池 · 跑两次自动晋升成 bee · 跑翻车让你重定义 · bee 跟着你的口味长 |
|
|
146
146
|
| **测试套件** | 1096 cases · 4 packages (peaks-loop 1015 / runtime 39 / mut 22 / shared-channel 20) · **CI 首次全绿**(ubuntu + windows) · 14 BDD caller-binding coverage |
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
// src/cli/commands/baseline-commands.ts
|
|
2
|
-
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
3
4
|
import { historySnapshot, readBaselineFile, writeBaselineFile } from '../../services/capability-baseline/store.js';
|
|
4
5
|
import { validateBaselineFile } from '../../services/capability-baseline/validator.js';
|
|
6
|
+
import { P0_JOURNEY_IDS } from '../../services/capability-baseline/types.js';
|
|
7
|
+
import { GUARD_CONTRACTS, getGuardContract, isJourneyId } from '../../services/capability-guard-runner/registry.js';
|
|
8
|
+
import { exitCodeForGuardSummary, runAllGuards } from '../../services/capability-guard-runner/runner.js';
|
|
5
9
|
function fail(io, code, message, data = {}) {
|
|
6
10
|
io.stdout(JSON.stringify({ ok: false, command: `baseline`, code, message, data, warnings: [], nextActions: [] }));
|
|
7
11
|
process.exitCode = 1;
|
|
@@ -9,6 +13,23 @@ function fail(io, code, message, data = {}) {
|
|
|
9
13
|
function ok(io, command, data, nextActions = []) {
|
|
10
14
|
io.stdout(JSON.stringify({ ok: true, command, data, warnings: [], nextActions }));
|
|
11
15
|
}
|
|
16
|
+
const CURRENT_DIR = (root) => join(root, 'openspec', 'baselines', 'current');
|
|
17
|
+
const HISTORY_DIR = (root, version) => join(root, 'openspec', 'baselines', 'history', version);
|
|
18
|
+
/** Whitelist of supported `--scorer` values for `peaks baseline audit`. */
|
|
19
|
+
const SCORER_MODES = ['live', 'stub'];
|
|
20
|
+
/**
|
|
21
|
+
* `live` is the default because it is the credential-free one. Defaulting to
|
|
22
|
+
* `stub` would leave the publish gate permanently inconclusive; defaulting to a
|
|
23
|
+
* scorer that needs a key would leave it unrunnable in CI.
|
|
24
|
+
*/
|
|
25
|
+
const DEFAULT_SCORER_MODE = 'live';
|
|
26
|
+
function isScorerMode(v) {
|
|
27
|
+
return v !== undefined && SCORER_MODES.includes(v);
|
|
28
|
+
}
|
|
29
|
+
/** The guard `run-guard` / `audit` execute against. */
|
|
30
|
+
function guardContext(projectRoot) {
|
|
31
|
+
return { projectRoot, sessionId: 'cli', contract: {}, baselineInvariant: 'auto' };
|
|
32
|
+
}
|
|
12
33
|
export function registerBaselineCommands(program, io) {
|
|
13
34
|
const baseline = program.command('baseline').description('Manage the capability baseline (frozen product semantics for 15 P0 journeys).');
|
|
14
35
|
baseline
|
|
@@ -69,16 +90,38 @@ export function registerBaselineCommands(program, io) {
|
|
|
69
90
|
});
|
|
70
91
|
baseline
|
|
71
92
|
.command('run-guard')
|
|
72
|
-
.description('Run
|
|
73
|
-
.option('--journey <id>',
|
|
93
|
+
.description('Run the guard contracts over the frozen baseline. Runs all 15 journeys unless --journey is given.')
|
|
94
|
+
.option('--journey <id>', `Run only one journey (${P0_JOURNEY_IDS.join('|')}); default is all 15.`)
|
|
74
95
|
.option('--project <path>', 'Project root', '.')
|
|
75
96
|
.option('--json', 'Emit JSON envelope')
|
|
76
97
|
.action(async (opts) => {
|
|
77
98
|
const projectRoot = opts.project ?? '.';
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
99
|
+
let contracts;
|
|
100
|
+
if (opts.journey === undefined) {
|
|
101
|
+
contracts = GUARD_CONTRACTS;
|
|
102
|
+
}
|
|
103
|
+
else if (!isJourneyId(opts.journey)) {
|
|
104
|
+
fail(io, 'UNKNOWN_JOURNEY', `unknown journey "${opts.journey}"; expected one of ${P0_JOURNEY_IDS.join(', ')}`);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
const contract = getGuardContract(opts.journey);
|
|
109
|
+
if (contract === undefined) {
|
|
110
|
+
fail(io, 'UNKNOWN_JOURNEY', `no guard contract is registered for ${opts.journey}`);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
contracts = [contract];
|
|
114
|
+
}
|
|
115
|
+
const summary = await runAllGuards(contracts, guardContext(projectRoot));
|
|
116
|
+
const data = summary;
|
|
117
|
+
const exitCode = exitCodeForGuardSummary(summary);
|
|
118
|
+
if (exitCode === 0) {
|
|
119
|
+
ok(io, 'baseline.run-guard', data);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
fail(io, exitCode === 1 ? 'GUARD_FAILED' : 'GUARD_SKIPPED', `${String(summary.fail)} failed, ${String(summary.skipped)} skipped of ${String(summary.total)} guard contracts`, data);
|
|
123
|
+
// `fail` sets 1; a skipped run is a distinct outcome from a failed one.
|
|
124
|
+
process.exitCode = exitCode;
|
|
82
125
|
});
|
|
83
126
|
baseline
|
|
84
127
|
.command('diff')
|
|
@@ -96,51 +139,146 @@ export function registerBaselineCommands(program, io) {
|
|
|
96
139
|
});
|
|
97
140
|
baseline
|
|
98
141
|
.command('audit')
|
|
99
|
-
.description('Run the capability audit (independent-context scorer).')
|
|
142
|
+
.description('Run the capability audit (independent-context scorer). Exits non-zero unless the verdict is consistent.')
|
|
143
|
+
.option('--scorer <mode>', `Scorer to run: ${SCORER_MODES.join('|')}. 'live' (default) runs the deterministic independent checker, which needs no credentials; 'stub' performs no evaluation and can never be consistent.`, DEFAULT_SCORER_MODE)
|
|
100
144
|
.option('--project <path>', 'Project root', '.')
|
|
101
145
|
.option('--json', 'Emit JSON envelope')
|
|
102
146
|
.action(async (opts) => {
|
|
103
147
|
const projectRoot = opts.project ?? '.';
|
|
148
|
+
if (!isScorerMode(opts.scorer)) {
|
|
149
|
+
fail(io, 'UNKNOWN_SCORER', `unknown scorer "${String(opts.scorer)}"; expected one of ${SCORER_MODES.join(', ')}`);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
104
152
|
const r = readBaselineFile(projectRoot);
|
|
105
153
|
if (!r.ok) {
|
|
106
154
|
fail(io, r.error.code, r.error.message);
|
|
107
155
|
return;
|
|
108
156
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
157
|
+
// The guard summary is the REAL aggregate of the 15 contracts. It used to
|
|
158
|
+
// be a literal `{pass:15,fail:0,skipped:0,total:15}` that no run produced.
|
|
159
|
+
const guardSummary = await runAllGuards(GUARD_CONTRACTS, guardContext(projectRoot));
|
|
160
|
+
// `live` is the deterministic independent checker: a real
|
|
161
|
+
// separate-context evaluation that needs no credentials, which is why it
|
|
162
|
+
// is the only kind that can run inside the secretless OIDC publish gate.
|
|
163
|
+
// It is handed the frozen rows and the registry, not just the guard
|
|
164
|
+
// summary — a scorer that only sees the guard result is a restatement.
|
|
116
165
|
const { runAudit } = await import('../../services/capability-audit-service/runner.js');
|
|
117
|
-
const audit = await runAudit({
|
|
118
|
-
|
|
166
|
+
const audit = await runAudit({
|
|
167
|
+
projectRoot,
|
|
168
|
+
sessionId: 'cli',
|
|
169
|
+
journeyId: 'J01',
|
|
170
|
+
scorerMode: opts.scorer,
|
|
171
|
+
baselineRows: r.file.rows,
|
|
172
|
+
contracts: GUARD_CONTRACTS,
|
|
173
|
+
guardSummary
|
|
174
|
+
});
|
|
175
|
+
const data = audit;
|
|
176
|
+
if (audit.verdict === 'consistent' && !audit.degraded) {
|
|
177
|
+
ok(io, 'baseline.audit', data);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
fail(io, 'AUDIT_NOT_CONSISTENT', `capability audit verdict is "${audit.verdict}"${audit.degraded ? ' (degraded: stub scorer)' : ''}`, data);
|
|
119
181
|
});
|
|
120
182
|
baseline
|
|
121
183
|
.command('freeze-update')
|
|
122
|
-
.description('Update one or more baseline rows
|
|
184
|
+
.description('Update one or more baseline rows. The LLM authors the JSON; --confirm records the user\'s approval.')
|
|
123
185
|
.option('--from <path>', 'Path to the new baseline JSON input')
|
|
186
|
+
.option('--confirm', "Record the user's approval (collected via AskUserQuestion) for this ratchet change")
|
|
124
187
|
.option('--project <path>', 'Project root', '.')
|
|
125
188
|
.option('--json', 'Emit JSON envelope')
|
|
126
189
|
.action((opts) => {
|
|
127
|
-
|
|
190
|
+
const projectRoot = opts.project ?? '.';
|
|
191
|
+
if (opts.confirm !== true) {
|
|
192
|
+
fail(io, 'HUMAN_NL_DECISION_REQUIRED', 'freeze-update requires the user to approve the ratchet change via AskUserQuestion. The LLM must surface a multi-choice prompt, then re-run with --confirm.');
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (!opts.from) {
|
|
196
|
+
fail(io, 'MISSING_ARG', '--from is required');
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const r = readBaselineFile(projectRoot);
|
|
200
|
+
if (!r.ok) {
|
|
201
|
+
fail(io, r.error.code, r.error.message);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const file = JSON.parse(readFileSync(opts.from, 'utf8'));
|
|
205
|
+
const v = validateBaselineFile(file);
|
|
206
|
+
if (!v.ok) {
|
|
207
|
+
fail(io, v.error.code, v.error.message);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const out = writeBaselineFile({ projectRoot, file });
|
|
211
|
+
historySnapshot({ projectRoot, version: file.version });
|
|
212
|
+
ok(io, 'baseline.freeze-update', {
|
|
213
|
+
path: out.path,
|
|
214
|
+
lockPath: out.lockPath,
|
|
215
|
+
fromVersion: r.file.version,
|
|
216
|
+
version: file.version
|
|
217
|
+
});
|
|
128
218
|
});
|
|
129
219
|
baseline
|
|
130
220
|
.command('rollback')
|
|
131
|
-
.description('Roll the baseline back to a historical version
|
|
221
|
+
.description('Roll the baseline back to a historical version. --confirm records the user\'s approval.')
|
|
132
222
|
.option('--to <version>', 'Historical version to roll back to')
|
|
223
|
+
.option('--confirm', "Record the user's approval (collected via AskUserQuestion) for this rollback")
|
|
133
224
|
.option('--project <path>', 'Project root', '.')
|
|
134
225
|
.option('--json', 'Emit JSON envelope')
|
|
135
|
-
.action(() => {
|
|
136
|
-
|
|
226
|
+
.action((opts) => {
|
|
227
|
+
const projectRoot = opts.project ?? '.';
|
|
228
|
+
if (opts.confirm !== true) {
|
|
229
|
+
fail(io, 'HUMAN_NL_DECISION_REQUIRED', 'rollback requires the user to approve via AskUserQuestion. The LLM must surface a multi-choice prompt, then re-run with --confirm.');
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (!opts.to) {
|
|
233
|
+
fail(io, 'MISSING_ARG', '--to is required');
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
const source = HISTORY_DIR(projectRoot, opts.to);
|
|
237
|
+
const from = join(source, 'capability-baseline.json');
|
|
238
|
+
const fromLock = join(source, 'capability-baseline.lock');
|
|
239
|
+
if (!existsSync(from) || !existsSync(fromLock)) {
|
|
240
|
+
fail(io, 'BASELINE_HISTORY_GAP', `no frozen baseline recorded for version ${opts.to}`);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
const r = readBaselineFile(projectRoot);
|
|
244
|
+
if (!r.ok) {
|
|
245
|
+
fail(io, r.error.code, r.error.message);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
mkdirSync(CURRENT_DIR(projectRoot), { recursive: true });
|
|
249
|
+
copyFileSync(from, join(CURRENT_DIR(projectRoot), 'capability-baseline.json'));
|
|
250
|
+
copyFileSync(fromLock, join(CURRENT_DIR(projectRoot), 'capability-baseline.lock'));
|
|
251
|
+
// Re-read through the locked store so a tampered history entry cannot be
|
|
252
|
+
// installed: the copy is only reported successful if it verifies.
|
|
253
|
+
const after = readBaselineFile(projectRoot);
|
|
254
|
+
if (!after.ok) {
|
|
255
|
+
fail(io, after.error.code, after.error.message);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
ok(io, 'baseline.rollback', { fromVersion: r.file.version, toVersion: after.file.version, historyPath: source });
|
|
137
259
|
});
|
|
138
260
|
baseline
|
|
139
261
|
.command('reset')
|
|
140
|
-
.description('Wipe the baseline and require re-freeze
|
|
262
|
+
.description('Wipe the current baseline and require a re-freeze. --confirm records the user\'s approval.')
|
|
263
|
+
.option('--confirm', "Record the user's approval (collected via AskUserQuestion) for the wipe")
|
|
141
264
|
.option('--project <path>', 'Project root', '.')
|
|
142
265
|
.option('--json', 'Emit JSON envelope')
|
|
143
|
-
.action(() => {
|
|
144
|
-
|
|
266
|
+
.action((opts) => {
|
|
267
|
+
const projectRoot = opts.project ?? '.';
|
|
268
|
+
if (opts.confirm !== true) {
|
|
269
|
+
fail(io, 'HUMAN_NL_DECISION_REQUIRED', 'reset requires the user to approve via AskUserQuestion. The LLM must surface a multi-choice prompt, then re-run with --confirm.');
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
const r = readBaselineFile(projectRoot);
|
|
273
|
+
// A missing baseline is already "wiped" — but an unreadable one (hash
|
|
274
|
+
// mismatch / unsigned lock) must not be silently discarded.
|
|
275
|
+
if (!r.ok && r.error.code !== 'BASELINE_NOT_FOUND') {
|
|
276
|
+
fail(io, r.error.code, r.error.message);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
const wipedVersion = r.ok ? r.file.version : null;
|
|
280
|
+
rmSync(join(CURRENT_DIR(projectRoot), 'capability-baseline.json'), { force: true });
|
|
281
|
+
rmSync(join(CURRENT_DIR(projectRoot), 'capability-baseline.lock'), { force: true });
|
|
282
|
+
ok(io, 'baseline.reset', { wipedVersion, nextAction: 're-freeze with `baseline freeze --from <path>`' });
|
|
145
283
|
});
|
|
146
284
|
}
|
|
@@ -527,9 +527,7 @@ export function registerCompactCommands(program, io) {
|
|
|
527
527
|
? result.historyWritten
|
|
528
528
|
? `Settled run ${result.runId}; appended a kind:'observed' row with pathway 'post-compact-hook'.`
|
|
529
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
|
|
531
|
-
? " A kind:'observed' row was still appended for this event."
|
|
532
|
-
: " The kind:'observed' row could not be appended either."}`
|
|
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.`
|
|
533
531
|
: result.reason === 'different-session'
|
|
534
532
|
? `The hook payload names a different harness session, so this project's open compact run was left alone.`
|
|
535
533
|
: 'No compact run was open, so nothing was settled and no history row was appended.'
|
|
@@ -15,6 +15,7 @@ import { resolveAutoCompactProfile } from '../../../services/mode/mode-status-se
|
|
|
15
15
|
import { gcStalePresenceLeases } from '../../../services/skills/presence-lease-service.js';
|
|
16
16
|
import { fail, ok } from 'peaks-loop-shared/result';
|
|
17
17
|
import { stableRealPath } from '../../../shared/path-utils.js';
|
|
18
|
+
import { detectStaleGeneratedArtifacts } from '../../../services/workspace/generated-artifacts-stamp.js';
|
|
18
19
|
/**
|
|
19
20
|
* Canonicalize a user-supplied `--project <path>` value.
|
|
20
21
|
*
|
|
@@ -94,6 +95,52 @@ function canonicalizeProjectOption(project) {
|
|
|
94
95
|
return project;
|
|
95
96
|
}
|
|
96
97
|
}
|
|
98
|
+
/**
|
|
99
|
+
* D1 (2026-09-15) — generated-config staleness, carried on `skill presence`.
|
|
100
|
+
*
|
|
101
|
+
* WHY THIS CALL. `npm i -g peaks-loop@<newer>` upgrades the CLI and leaves the
|
|
102
|
+
* project's generated config exactly as the OLD release wrote it:
|
|
103
|
+
* `initWorkspace` is the only writer and `ensureSession` early-returns once a
|
|
104
|
+
* session is bound, so nothing re-runs the generator. `.claude/settings.local.json`
|
|
105
|
+
* is drift-checked — but only on an init that never comes. The user found the
|
|
106
|
+
* previous instance of this by deleting their `.claude/*.json` and restarting;
|
|
107
|
+
* nothing in the product told them to.
|
|
108
|
+
*
|
|
109
|
+
* `peaks skill presence` is the ONE peaks call every skill makes in every turn
|
|
110
|
+
* (CLAUDE.md mandates it at the start of every response), so it is the only
|
|
111
|
+
* channel guaranteed to carry a drift notice to the LLM that can act on it —
|
|
112
|
+
* the same reasoning as the loop-hygiene verdict attached one screen down. A
|
|
113
|
+
* rule that lives only in a SKILL.md body is compacted away; a warning that
|
|
114
|
+
* rides the per-turn tool output is not.
|
|
115
|
+
*
|
|
116
|
+
* The field is additive and present only when stale, so no existing consumer
|
|
117
|
+
* of the envelope changes shape. `null` projectRoot means the caller had no
|
|
118
|
+
* project to inspect — skipped, not assumed stale.
|
|
119
|
+
*/
|
|
120
|
+
function generatedConfigNotice(projectRoot) {
|
|
121
|
+
if (projectRoot === undefined)
|
|
122
|
+
return { field: {}, warnings: [] };
|
|
123
|
+
const staleness = detectStaleGeneratedArtifacts(projectRoot);
|
|
124
|
+
if (!staleness.stale)
|
|
125
|
+
return { field: {}, warnings: [] };
|
|
126
|
+
const onDiskVersion = staleness.onDisk?.packageVersion ?? '(unstamped)';
|
|
127
|
+
return {
|
|
128
|
+
field: {
|
|
129
|
+
generatedArtifacts: {
|
|
130
|
+
stale: true,
|
|
131
|
+
reasons: staleness.reasons,
|
|
132
|
+
onDiskPackageVersion: staleness.onDisk?.packageVersion ?? null,
|
|
133
|
+
installedPackageVersion: staleness.expected.packageVersion
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
warnings: [
|
|
137
|
+
`Generated config at '${projectRoot}' was produced by peaks-loop ${onDiskVersion} ` +
|
|
138
|
+
`but ${staleness.expected.packageVersion} is installed (${staleness.reasons.join(', ')}). ` +
|
|
139
|
+
`Re-run \`peaks workspace init\` (or the idempotent \`peaks upgrade --apply-init\`) ` +
|
|
140
|
+
`to regenerate .claude/settings.local.json and the offline template copy.`
|
|
141
|
+
]
|
|
142
|
+
};
|
|
143
|
+
}
|
|
97
144
|
import { addJsonOption, getErrorMessage, printResult } from '../../cli-helpers.js';
|
|
98
145
|
// Slice S0 (4.0.0-beta.5 peaks-solo dispatcher release):
|
|
99
146
|
// `peaks skill search` is the CLI primitive that feeds the
|
|
@@ -228,9 +275,10 @@ export function registerSkillCommand(program, io) {
|
|
|
228
275
|
.option('--check-stale', 'slice 002 (v2.15.0): also report whether the recorded outer session id still matches the current one. Default false (back-compat).')
|
|
229
276
|
.option('--project <path>', 'project root (default: cwd)')).action((options) => {
|
|
230
277
|
const projectOption = canonicalizeProjectOption(options.project);
|
|
278
|
+
const generatedConfig = generatedConfigNotice(projectOption ?? findProjectRoot(process.cwd()) ?? process.cwd());
|
|
231
279
|
const presence = getSkillPresence(projectOption);
|
|
232
280
|
if (presence === null) {
|
|
233
|
-
printResult(io, ok('skill.presence', { active: false }), options.json);
|
|
281
|
+
printResult(io, ok('skill.presence', { active: false, ...generatedConfig.field }, generatedConfig.warnings), options.json);
|
|
234
282
|
return;
|
|
235
283
|
}
|
|
236
284
|
// Loop-hygiene verdict: attached to every ACTIVE read, so the
|
|
@@ -251,11 +299,12 @@ export function registerSkillCommand(program, io) {
|
|
|
251
299
|
staleReason: staleness.reason,
|
|
252
300
|
currentOuterSessionId: staleness.currentOuterSessionId,
|
|
253
301
|
recordedOuterSessionId: staleness.recordedOuterSessionId,
|
|
254
|
-
...(verdict.context !== null ? { context: verdict.context } : {})
|
|
255
|
-
|
|
302
|
+
...(verdict.context !== null ? { context: verdict.context } : {}),
|
|
303
|
+
...generatedConfig.field
|
|
304
|
+
}, generatedConfig.warnings, verdict.nextActions), options.json);
|
|
256
305
|
return;
|
|
257
306
|
}
|
|
258
|
-
printResult(io, ok('skill.presence', { active: true, ...presence, ...(verdict.context !== null ? { context: verdict.context } : {}) },
|
|
307
|
+
printResult(io, ok('skill.presence', { active: true, ...presence, ...(verdict.context !== null ? { context: verdict.context } : {}), ...generatedConfig.field }, generatedConfig.warnings, verdict.nextActions), options.json);
|
|
259
308
|
});
|
|
260
309
|
addJsonOption(skill
|
|
261
310
|
.command('presence:set <name>')
|
|
@@ -1,3 +1,27 @@
|
|
|
1
1
|
import type { Command } from 'commander';
|
|
2
2
|
import { type ProgramIO } from '../../cli-helpers.js';
|
|
3
|
+
/** The only `--category` this subcommand implements today. */
|
|
4
|
+
export declare const LOOP_ENGINEERING_CATEGORY = "loop-engineering";
|
|
5
|
+
/** Project-relative location of the file the `loop-engineering` lint reads. */
|
|
6
|
+
export declare const LOOP_ENGINEERING_GUIDELINES_RELATIVE_PATH = ".peaks/standards/loop-engineering-guidelines.md";
|
|
7
|
+
export interface StandardsLintEnvelope {
|
|
8
|
+
ok: boolean;
|
|
9
|
+
code: string;
|
|
10
|
+
message: string;
|
|
11
|
+
data: {
|
|
12
|
+
category: string;
|
|
13
|
+
path: string;
|
|
14
|
+
redLineCount: number;
|
|
15
|
+
redLineIds: string[];
|
|
16
|
+
findings: string[];
|
|
17
|
+
};
|
|
18
|
+
nextActions: string[];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Pure helper: read the loop-engineering guideline file under `projectRoot`
|
|
22
|
+
* and lint it. Kept out of the Commander action so unit tests can drive it
|
|
23
|
+
* without spawning a child process (same shape as
|
|
24
|
+
* `runReadinessLint` in skill-loop-engineering-readiness-commands.ts).
|
|
25
|
+
*/
|
|
26
|
+
export declare function runLoopEngineeringLint(projectRoot: string): StandardsLintEnvelope;
|
|
3
27
|
export declare function registerStandardsCommand(program: Command, io: ProgramIO): void;
|
|
@@ -1,9 +1,58 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
1
3
|
import { summarizeProjectStandardsInitResult, summarizeProjectStandardsUpdateResult } from '../../../services/standards/project-standards-service.js';
|
|
2
4
|
import { executeProjectStandardsInitIdeAware, executeProjectStandardsUpdateIdeAware } from '../../../services/standards/ide-aware-standards-service.js';
|
|
3
5
|
import { migrateStandards } from '../../../services/standards/migrate-service.js';
|
|
4
6
|
import { migrateClaudeRules } from '../../../services/standards/migrate-claude-rules-service.js';
|
|
7
|
+
import { lintLoopEngineeringGuidelines, EXPECTED_RED_LINE_IDS } from '../../../services/standards/loop-engineering-lint.js';
|
|
5
8
|
import { fail, ok } from 'peaks-loop-shared/result';
|
|
6
9
|
import { addJsonOption, getErrorMessage, printResult } from '../../cli-helpers.js';
|
|
10
|
+
/** The only `--category` this subcommand implements today. */
|
|
11
|
+
export const LOOP_ENGINEERING_CATEGORY = 'loop-engineering';
|
|
12
|
+
/** Project-relative location of the file the `loop-engineering` lint reads. */
|
|
13
|
+
export const LOOP_ENGINEERING_GUIDELINES_RELATIVE_PATH = '.peaks/standards/loop-engineering-guidelines.md';
|
|
14
|
+
/**
|
|
15
|
+
* Pure helper: read the loop-engineering guideline file under `projectRoot`
|
|
16
|
+
* and lint it. Kept out of the Commander action so unit tests can drive it
|
|
17
|
+
* without spawning a child process (same shape as
|
|
18
|
+
* `runReadinessLint` in skill-loop-engineering-readiness-commands.ts).
|
|
19
|
+
*/
|
|
20
|
+
export function runLoopEngineeringLint(projectRoot) {
|
|
21
|
+
const root = resolve(projectRoot);
|
|
22
|
+
const guidelinePath = join(root, LOOP_ENGINEERING_GUIDELINES_RELATIVE_PATH);
|
|
23
|
+
const data = { category: LOOP_ENGINEERING_CATEGORY, path: guidelinePath, redLineCount: 0, redLineIds: [], findings: [] };
|
|
24
|
+
if (!existsSync(guidelinePath)) {
|
|
25
|
+
return {
|
|
26
|
+
ok: false,
|
|
27
|
+
code: 'LINT_FILE_NOT_FOUND',
|
|
28
|
+
message: `no ${LOOP_ENGINEERING_GUIDELINES_RELATIVE_PATH} under ${root}`,
|
|
29
|
+
data,
|
|
30
|
+
nextActions: [`Pass --project pointing at a peaks-loop checkout (the lint reads its own guideline file).`],
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
const result = lintLoopEngineeringGuidelines(readFileSync(guidelinePath, 'utf-8'));
|
|
34
|
+
const redLineIds = result.redLines.map((rl) => rl.id);
|
|
35
|
+
const fullData = { ...data, redLineCount: redLineIds.length, redLineIds };
|
|
36
|
+
if (result.ok) {
|
|
37
|
+
return {
|
|
38
|
+
ok: true,
|
|
39
|
+
code: 'STANDARDS_LINT_OK',
|
|
40
|
+
message: `${redLineIds.length} red line(s) present with all 4 sections (expected: ${EXPECTED_RED_LINE_IDS.join(', ')})`,
|
|
41
|
+
data: fullData,
|
|
42
|
+
nextActions: ['Any new red line must use the 4-section form or this lint will reject it.'],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
ok: false,
|
|
47
|
+
code: 'STANDARDS_LINT_FAILED',
|
|
48
|
+
message: `guideline file failed the lint (${result.findings.length} finding(s))`,
|
|
49
|
+
data: { ...fullData, findings: result.findings },
|
|
50
|
+
nextActions: [
|
|
51
|
+
'Address every finding in data.findings, then re-run the lint.',
|
|
52
|
+
'Each red line must be a `## RL-N — <title>` heading with the 4 sections: Failure modes / Rewrite / Self-check / Out-of-scope.',
|
|
53
|
+
],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
7
56
|
export function registerStandardsCommand(program, io) {
|
|
8
57
|
const standards = program.command('standards').description('Manage project-local coding standards');
|
|
9
58
|
addJsonOption(standards
|
|
@@ -92,4 +141,29 @@ export function registerStandardsCommand(program, io) {
|
|
|
92
141
|
process.exitCode = 1;
|
|
93
142
|
}
|
|
94
143
|
});
|
|
144
|
+
addJsonOption(standards
|
|
145
|
+
.command('lint')
|
|
146
|
+
.description('Lint a guideline file for structural completeness (the loop-engineering red lines)')
|
|
147
|
+
.requiredOption('--category <category>', `guideline category to lint (${LOOP_ENGINEERING_CATEGORY})`)
|
|
148
|
+
.option('--project <path>', 'project root holding .peaks/standards/ (default: cwd)')).action((options) => {
|
|
149
|
+
if (options.category !== LOOP_ENGINEERING_CATEGORY) {
|
|
150
|
+
printResult(io, fail('standards.lint', 'UNKNOWN_LINT_CATEGORY', `only --category ${LOOP_ENGINEERING_CATEGORY} is implemented`, { category: options.category }, [`Pass --category ${LOOP_ENGINEERING_CATEGORY}.`]), options.json);
|
|
151
|
+
process.exitCode = 1;
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
try {
|
|
155
|
+
const envelope = runLoopEngineeringLint(options.project ?? process.cwd());
|
|
156
|
+
const response = envelope.ok
|
|
157
|
+
? ok('standards.lint', envelope.data, [], envelope.nextActions)
|
|
158
|
+
: fail('standards.lint', envelope.code, envelope.message, envelope.data, envelope.nextActions);
|
|
159
|
+
printResult(io, response, options.json);
|
|
160
|
+
if (!envelope.ok) {
|
|
161
|
+
process.exitCode = 1;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
printResult(io, fail('standards.lint', 'STANDARDS_LINT_ERROR', getErrorMessage(error), { category: options.category }, ['Verify the guideline file is readable.']), options.json);
|
|
166
|
+
process.exitCode = 1;
|
|
167
|
+
}
|
|
168
|
+
});
|
|
95
169
|
}
|
|
@@ -5,13 +5,17 @@
|
|
|
5
5
|
* - `peaks feedback check-unpromoted --project <path> [--strict]`
|
|
6
6
|
*
|
|
7
7
|
* Companion to `sops/feedback-promotion-sop.md`. The promote command
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
8
|
+
* writes the promotion marker + sidecar + RD envelope, and — for the
|
|
9
|
+
* layers whose artifact it can produce (A: a registered SOP manifest)
|
|
10
|
+
* — the enforcement artifact itself. Layers B and C live in shared
|
|
11
|
+
* files it does not own, so there it records the requirement and
|
|
12
|
+
* reports `effective: false` instead of claiming success.
|
|
13
|
+
*
|
|
14
|
+
* The check-unpromoted command scans `.peaks/memory/*.md` for feedback
|
|
15
|
+
* memories whose promotion is missing OR not backed by its layer's
|
|
16
|
+
* artifact, and emits a structured list. `--strict` flips exit code to
|
|
17
|
+
* non-zero when any is found — used by `peaks workflow verify-pipeline`
|
|
18
|
+
* Gate H.
|
|
15
19
|
*/
|
|
16
20
|
import type { Command } from 'commander';
|
|
17
21
|
import { type ProgramIO } from '../cli-helpers.js';
|