peaks-loop 4.0.45 → 4.0.47
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 +48 -0
- package/README-en.md +1 -1
- package/README.md +1 -1
- package/dist/cli/commands/code-runtime-commands.d.ts +22 -0
- package/dist/cli/commands/code-runtime-commands.js +128 -34
- package/dist/cli/commands/compact-command.js +129 -1
- package/dist/cli/commands/container-commands.js +3 -3
- package/dist/cli/commands/core/skill-command.js +45 -10
- package/dist/cli/commands/cron-commands.js +2 -1
- package/dist/cli/commands/e2e-verify.js +3 -3
- package/dist/cli/commands/governance-classify-contract-commands.js +1 -0
- package/dist/cli/commands/hooks-commands.js +10 -1
- package/dist/cli/commands/loop-commands.js +1 -0
- package/dist/cli/commands/playwright-commands.js +2 -1
- package/dist/cli/commands/reinject-command.d.ts +72 -0
- package/dist/cli/commands/reinject-command.js +174 -0
- package/dist/cli/commands/request-commands.js +6 -3
- package/dist/cli/commands/shadcn-commands.js +1 -0
- package/dist/cli/commands/test-commands.js +2 -1
- package/dist/cli/commands/vm-commands.js +7 -7
- package/dist/cli/commands/workspace/init-command.js +24 -2
- package/dist/cli/commands/worktree-lease-commands.js +4 -4
- package/dist/cli/index.js +49 -2
- package/dist/cli/program.js +5 -0
- package/dist/hooks/pre-tool-use-sub-agent.js +1 -1
- package/dist/services/adapter/adapter-registry.js +1 -1
- package/dist/services/artifacts/artifact-service.js +1 -1
- package/dist/services/capability-guard-runner/contracts/J01.js +2 -1
- package/dist/services/capability-guard-runner/contracts/J02.js +3 -3
- package/dist/services/capability-guard-runner/contracts/J04.js +4 -2
- package/dist/services/capability-guard-runner/contracts/J07.js +2 -1
- package/dist/services/code/auto-compact-lifecycle.d.ts +50 -4
- package/dist/services/code/auto-compact-lifecycle.js +52 -10
- package/dist/services/code/auto-compact-orchestrator.d.ts +53 -9
- package/dist/services/code/auto-compact-orchestrator.js +173 -43
- package/dist/services/code/orchestrator-can-do.d.ts +4 -2
- package/dist/services/code/orchestrator-can-do.js +37 -5
- package/dist/services/codegraph/codegraph-exclude-reconciler.js +2 -1
- package/dist/services/codegraph/codegraph-process-runner.js +3 -2
- package/dist/services/compact/request-transition-hook.js +5 -2
- package/dist/services/compact-history/compact-history-service.d.ts +61 -0
- package/dist/services/compact-history/compact-history-service.js +49 -0
- package/dist/services/compact-statusline/compact-lifecycle-store.d.ts +11 -2
- package/dist/services/compact-statusline/compact-lifecycle-store.js +19 -1
- package/dist/services/compact-statusline/compact-statusline-service.d.ts +1 -1
- package/dist/services/compact-statusline/compact-statusline-service.js +22 -0
- package/dist/services/config/config-safety.d.ts +52 -0
- package/dist/services/config/config-safety.js +75 -1
- package/dist/services/context/auto-compact-dispatcher.d.ts +7 -37
- package/dist/services/context/auto-compact-dispatcher.js +113 -40
- package/dist/services/context/auto-compact-reader.d.ts +68 -28
- package/dist/services/context/auto-compact-reader.js +155 -1
- package/dist/services/context/auto-compact-types.d.ts +89 -12
- package/dist/services/context/auto-compact-types.js +16 -32
- package/dist/services/context/harness-window-config.d.ts +412 -0
- package/dist/services/context/harness-window-config.js +607 -0
- package/dist/services/context/main-session-monitor.d.ts +27 -0
- package/dist/services/context/main-session-monitor.js +32 -1
- package/dist/services/context/post-compact-reinjection.d.ts +221 -0
- package/dist/services/context/post-compact-reinjection.js +491 -0
- package/dist/services/dispatch/merge-back-runner.js +5 -5
- package/dist/services/dispatch/service-shutdown.js +3 -3
- package/dist/services/doc/doc-generator.js +2 -1
- package/dist/services/env/shell-probe.js +1 -1
- package/dist/services/fuzzy-matching/fzf-pick-service.js +2 -0
- package/dist/services/hooks/auto-compact-hook-install.d.ts +10 -2
- package/dist/services/hooks/auto-compact-hook-install.js +8 -0
- package/dist/services/ide/adapters/claude-code-adapter.d.ts +107 -3
- package/dist/services/ide/adapters/claude-code-adapter.js +154 -7
- package/dist/services/ide/ide-registry.d.ts +12 -0
- package/dist/services/ide/ide-registry.js +14 -0
- package/dist/services/ide/ide-types.d.ts +59 -0
- package/dist/services/lint/detect-eslint.js +2 -2
- package/dist/services/lint/eslint-runner.js +3 -1
- package/dist/services/loop/evaluator-dispatcher.js +2 -1
- package/dist/services/memory/project-memory-service/index/kind-dispatch.js +1 -1
- package/dist/services/memory/project-memory-service/store/paths.d.ts +9 -1
- package/dist/services/memory/project-memory-service/store/paths.js +15 -6
- package/dist/services/prd/best-practice-auto-trigger.js +1 -0
- package/dist/services/release/version-precheck-service.d.ts +2 -1
- package/dist/services/release/version-precheck-service.js +82 -12
- package/dist/services/runtime/vendor-adapter.d.ts +29 -4
- package/dist/services/runtime/vendors/claude-code.js +1 -1
- package/dist/services/runtime/vendors/codex.js +1 -1
- package/dist/services/runtime/vendors/copilot.js +1 -1
- package/dist/services/sc/sc-service.js +1 -1
- package/dist/services/scan/diff-scope-service.js +2 -2
- package/dist/services/scan/file-size-scan.js +2 -2
- package/dist/services/scan/orphan-service.js +2 -1
- package/dist/services/scan/type-sanity-service.js +2 -2
- package/dist/services/skillhub/tar-runtime.js +1 -0
- package/dist/services/skills/hooks-codegate-superpowers.d.ts +8 -0
- package/dist/services/skills/hooks-codegate-superpowers.js +40 -3
- package/dist/services/skills/hooks-settings-service.d.ts +12 -0
- package/dist/services/skills/hooks-settings-service.js +77 -10
- package/dist/services/skills/session-start-hook-constants.d.ts +41 -0
- package/dist/services/skills/session-start-hook-constants.js +41 -0
- package/dist/services/skills/skill-presence-service.js +9 -0
- package/dist/services/skills/skill-statusline-renderer.js +30 -5
- package/dist/services/skills/statusline-palette.d.ts +6 -0
- package/dist/services/skills/statusline-palette.js +4 -1
- package/dist/services/slice/slice-check-service.js +2 -1
- package/dist/services/slice/slice-decompose-runners.js +2 -1
- package/dist/services/upgrade/upgrade-service.js +1 -0
- package/dist/services/workflow/workflow-skip-service.js +2 -1
- package/dist/services/workspace/migrate-service.js +1 -1
- package/dist/services/workspace/workspace-claude-settings-materializer.js +51 -7
- package/dist/services/workspace/workspace-service.js +8 -0
- package/dist/services/worktree/host-worktree-reconciler.js +1 -0
- package/dist/services/worktree/long-path-cleanup.js +3 -2
- package/dist/shared/process.js +1 -1
- package/package.json +5 -5
- package/scripts/install-skills.mjs +1 -0
- package/scripts/watch.mjs +3 -1
- package/skills/bee/peaks-perf-audit/SKILL.md +1 -1
- package/skills/bee/peaks-prd/SKILL.md +1 -1
- package/skills/bee/peaks-qa/SKILL.md +2 -2
- package/skills/bee/peaks-rd/SKILL.md +2 -2
- package/skills/bee/peaks-reviewer/SKILL.md +1 -1
- package/skills/bee/peaks-sc/SKILL.md +1 -1
- package/skills/bee/peaks-security-audit/SKILL.md +1 -1
- package/skills/bee/peaks-txt/SKILL.md +1 -1
- package/skills/bee/peaks-ui/SKILL.md +1 -1
- package/skills/peaks-audit/SKILL.md +1 -1
- package/skills/peaks-code/SKILL.md +4 -4
- package/skills/peaks-code/references/sub-agent-dispatch.md +1 -1
- package/skills/peaks-content/SKILL.md +1 -1
- package/skills/peaks-doctor/SKILL.md +1 -1
- package/skills/peaks-final-review/SKILL.md +1 -1
- package/skills/peaks-ide/SKILL.md +1 -1
- package/skills/peaks-issue-fix-orchestrator/SKILL.md +1 -1
- package/skills/peaks-resume/SKILL.md +1 -1
- package/skills/peaks-slice-decompose/SKILL.md +1 -1
- package/skills/peaks-solo/SKILL.md +1 -1
- package/skills/peaks-sop/SKILL.md +1 -1
- package/skills/peaks-status/SKILL.md +1 -1
- package/skills/peaks-test/SKILL.md +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,53 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.0.47 — 2026-09-13 (90 个文件第一次进 CI 就绿了 + 弹窗的根在守卫射程 + 说"能做"而做不到的地方)
|
|
4
|
+
|
|
5
|
+
**Highlights**:
|
|
6
|
+
|
|
7
|
+
1. **`tests/integration/**`(90 个文件 / 463 个测试)第一次进了 CI,也第一次在 ubuntu 上绿了。** 它此前**从未在任何地方跑过** —— `ci.yml` 的测试步骤用默认 config,其 `include` 只含 `tests/unit/**`;所以"CI 绿"与"全量绿"都不覆盖它。接入后实测 **20 条失败**,逐条分诊后:**2 条是真产品 bug**(`memory extract` 把"文件不存在"报成沙箱逃逸;`release canary` 缺 `package.json` 时抛 `UNHANDLED_ERROR`)、**1 条分诊误判被拒绝**(`sop registry` 看着像读不到磁盘清单,实际是仓库从没跑过 `sop register` —— 内容缺口,不是代码缺口)、其余为陈旧期望与契约选择。
|
|
8
|
+
|
|
9
|
+
**那个 CI job 一开始是"有意变红"的,现在是普通闸门** —— 它的名字与注释都改成了如实描述:**红即真回归**,不再是"已知初始状态"。已验证:**ubuntu 与 windows 双双全绿**。
|
|
10
|
+
|
|
11
|
+
2. **空白 `cmd.exe` 弹窗:根在守卫的射程,不在扫描的深度。** 上一版给 14 个文件加了 `windowsHide` 并加了守卫,弹窗却没停 —— 因为守卫的 `GUARDED_FILES` 是**那 14 个文件的硬编码棘轮**,其它文件**按构造就在射程外**,于是守卫报零而症状继续。扩成**发现式**(遍历 `src|tests|scripts|packages`,**先收集完整列表再逐个解析**,不依赖单文件结果)后,查出 **214 个未隐藏调用点 / 105 个文件**,**全部修复,债务棘轮为空**。
|
|
12
|
+
|
|
13
|
+
扩围还暴露了守卫**自身 AST 遍历的 4 处缺陷** —— 那些缺陷正是这些站点长期隐形的原因:`execFile(cmd,args,OPTIONS,cb)` 的 options 读错位置(放过 `src/shared/process.ts`,**每次子代理派发都在跑**)、改名 import 跟不上、**`promisify(execFile)` 完全跟不上**(隐藏了 `orchestrator-can-do.ts` 上**每轮** `code context-now` 路径的两个活站点)、动态 import 命名空间漏。
|
|
14
|
+
|
|
15
|
+
3. **auto-compact:从"谁来决定何时压"到"压完之后还在不在"。** 两件事都做了:
|
|
16
|
+
|
|
17
|
+
- **尺度对齐。** peaks-loop 与 harness **各自独立解析"上下文窗口"**(前者走 env → config → 模型名启发式 → 硬编码 200K),**没有任何机制保证两者相同**。后果:启发式认不出模型时,peaks-loop 的"95%"落在 **190K token**、而 harness 要到 **~967K** 才压 —— **红线早触发约 5 倍,然后死锁**(没有任何东西能在 190K 处强制压缩)。现在两边**是同一个数**;红线**不再封死派发**,只请求并报告等待;阈值写入 `.claude/settings.local.json` 的 `env` 块,**带人类可读的告知与一键回滚**(harness 默认对被覆盖是静默的)。
|
|
18
|
+
- **压缩后回注。** 压缩由 harness 执行、摘要是**泛化的** —— 它不知道什么是 slice / request / gate / AC,所以丢掉的恰恰是本项目自己的状态,而**没有任何东西把它装回去**。新增 `SessionStart` + `matcher:"compact"` 钩子(官方支持的回注通道),**预算 3072 字节**:按**存活排名**保留块(身份 → **指针** → 运行期规则 → 下一步 → 当前工作),**指针优先于规则**(让人去读产物,而不是把产物搬进来);降级是 **greedy by rank(跳过并继续)**,不是一个超大块把后面的全挤掉。**单位是字节而非"窗口的百分之几"** —— 百分比预算会**随窗口变大而变大**,正好重新制造它要防的失效。
|
|
19
|
+
|
|
20
|
+
4. **一批"代码在说它做不到的事"** —— 本版最反复的一条主线:
|
|
21
|
+
|
|
22
|
+
- **一个谁都接不住的 spawn 失败。** 集成套件测试全过、进程却 exit 1。根因不是平台:`ProcessSupervisor.spawn` 调 `nodeSpawn` 时**没挂 `'error'` 监听器**,而 **Node 把 ENOENT 派发在 nextTick 上,那个队列在 `await` 恢复之前就排空了** —— 所以 `await spawn(); child.on('error', …)` **无论谁写都输掉竞速**,`DispatchResult.child`(注释写明"给调用方挂 handler 用")是个**无法兑现的承诺**,CLI 里自称 "canonical pattern" 的 handler **是死代码**。改为把结果做成**类型化的值**(`settled` 承诺 + `spawnError`),失败不再被记成 `running`。
|
|
23
|
+
- **缺必填选项报成 `UNHANDLED_ERROR`**(`command: "cli"`、`nextActions` 为空)—— 改为类型化的 `MISSING_REQUIRED_OPTION`,点名命令与选项。
|
|
24
|
+
- **`config restore` 读的是 `~/.peaks/*.bak` 而不是项目目录** —— 所以它在一台**恰好迁移过配置的开发机**上绿、在干净 runner 上红;而 `rollback` 因为优雅降级反而一直绿。
|
|
25
|
+
- **CI 里 `request init` 要解析 caller id**,无 IDE env 时落到 D2/EX_USAGE(exit 64),而 `workspace init` 从不解析它 —— 这就是"上一行成功、这一行失败"的不对称。
|
|
26
|
+
|
|
27
|
+
**验证**:三个版本常量一致(**4.0.47**);`tsc -p tsconfig.json` 保持 **142** 基线;`tests/unit` **228 files / 2397 passed / 3 skipped / 0 failed**;`tests/integration` **88 files / 2 skipped / 0 failed**,且 **CI 的 ubuntu 与 windows 两个平台都绿**。
|
|
28
|
+
|
|
29
|
+
**明确未验证的(不当作已完成)**:压缩后回注**没有人在一次*真实*压缩之后观察过** —— 单测无法让 harness 压缩、也无法观测它怎么捕获钩子 stdout,所以 `matcher:"compact"` 是当作已知条件接受的,**它的失效方式是静默的**;**下一次真实压缩才是第一次真检验**。以及"由 peaks-loop 决定**触发点**"仍未达成:写入的窗口等于真实窗口,只让两边共享尺度,**没有让 harness 提前压** —— 标定需要真实会话数据,`compact-history.jsonl` 已记录"意图 vs 实际"待读。
|
|
30
|
+
|
|
31
|
+
**本轮顺带发现、尚未处理(不属本版修复)**:三条 `SessionStart` 条目假设 Windows 上 shell 形式的 SessionStart 命令不会开控制台窗口 —— `shell: powershell` 那个钉子只加在了 per-tool-call 钩子上,**这是 windowsHide 家族还没扫到的一处**;`materializeClaudeSettingsLocal` 拥有本地 settings 文件的整个 `hooks` 键,落在那里的一条 SessionStart 条目会被模板刷新**静默删掉**(今天没有东西走那条路);`config restore --list` 在缺 `.bak` 时 exit 1 而 `rollback` exit 0 并返回 `{available:false}` —— 一处真实的不对称,需要产品决定;`test:integration` 脚本仍漏 `--config`,**选中的是零个文件**。
|
|
32
|
+
|
|
33
|
+
## 4.0.46 — 2026-09-12 (85% 该压不压 + 把"派发"记成了"正在压缩")
|
|
34
|
+
|
|
35
|
+
**Highlights**:
|
|
36
|
+
|
|
37
|
+
1. **同一个 0.85–0.95 带,四份权威说法互相打架,而代码挑了那个"什么都不做"的。** `peaks code context-now` 对**单 rid** 会话把该带降级成 `soft-warn`("仅建议");而**它自己的 help 文本**写着 *"≥0.85 emits action=auto-compact-now (**MANDATORY, not advisory**)"*;`peaks skill presence` 返回 `pre-compact`;`peaks-code` 的 SKILL.md 说该带 **auto-compact fires automatically** 并把 `context-now` 称作 **single source of truth**;你 2026-07-27 亲自校准的阈值策略写的是 **soft-mandatory**。
|
|
38
|
+
|
|
39
|
+
后果直接:一个**照着"唯一真相源"办事**的 LLM,在 0.85–0.95 里**永远不会压缩** —— 直接违反 zero-pause 契约。现在单 rid 与 job 模式一致:该带一律 `auto-compact-now`,并把过时的 notice/help 改成如实描述代码行为。
|
|
40
|
+
|
|
41
|
+
2. **`compact-lifecycle.json` 把"已派发"记成了"正在压缩",于是状态栏永久卡在 `stalled`。** claude-code 的 `ide-native` pathway **只在 ratio ≥ 0.95 才真的 in-band 压缩**(且 dispatcher 无论装没装上 hook 都返回 `ok: true`);于是 0.85–0.95 区间里**什么都没有在飞**,记录却在 2 分钟后被判 stale,状态栏画一条永不前进的进度条并报 `stalled`。
|
|
42
|
+
|
|
43
|
+
**比显示错更严重的**:结算逻辑 `settleOpenLifecycleRun` 只在 **ratio 跌破阈值**时结算 —— 而那条 pathway 根本不会造成下跌,**所以 `afterRatio` 永远写不上,"压缩后 ratio 确实降了"这条验证链是死的**,而它正是 zero-pause 契约唯一的验收依据。
|
|
44
|
+
|
|
45
|
+
两半一起修,方式是**让记录说实话**:新增**静止态 `armed`**(含义是"触发器已设好,等 95% 的 in-band 时机"),并被**类型系统**排除在 `ACTIVE_STAGES` 之外 —— 只有承诺过心跳的 stage(`queued`/`preparing`/`compacting`/`verifying`)才可能 stall。状态栏据此渲染成 `◔ armed · 88% · fires at 95%`,**不画进度条**。而**真的卡住的 `compacting` 仍然正确报 `stalled`** —— 这条能力是实测确认保住的,不是假设。
|
|
46
|
+
|
|
47
|
+
**验证**:三个版本常量一致(**4.0.46**);`tsc -p tsconfig.build.json` exit 0;宽 `tsconfig.json` 保持 **142** 基线;`tests/unit` **215 files / 2173 passed / 3 skipped / 0 failed**;`pnpm build` 的 `build-integrity` OK。
|
|
48
|
+
|
|
49
|
+
**本轮顺带发现、尚未处理(不属本版修复)**:`tests/integration/**`(90 个文件)**从未进过 CI** —— `ci.yml` 的测试步骤是默认 config,其 `include` 只含 `tests/unit/**`;CI 里另有一个 job 只为单个文件特判。实测该套件 **10 files / 20 tests / 3 errors 红**。也就是说"CI 绿"与"全量绿"都**不覆盖这 90 个文件**。同一形状(一个看似覆盖全局的绿信号,实际有射程外的东西)已由 `a-diff-scoped-gate-reporting-zero-violations-may-have-checked-nothing` 记录在案。建议下一片专门处理,含那 20 个失败的分诊。
|
|
50
|
+
|
|
3
51
|
## 4.0.45 — 2026-09-12 (一个永远无法通过的维度 + 一个验代理的守卫)
|
|
4
52
|
|
|
5
53
|
**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.47 (2026-09-13) |
|
|
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.47(2026-09-13) |
|
|
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 |
|
|
@@ -11,4 +11,26 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import type { Command } from 'commander';
|
|
13
13
|
import { type ProgramIO } from '../cli-helpers.js';
|
|
14
|
+
import { type ResultEnvelope } from 'peaks-loop-shared/result';
|
|
15
|
+
import { type AutoCompactResult } from '../../services/code/auto-compact-orchestrator.js';
|
|
16
|
+
/**
|
|
17
|
+
* Adapt `runAutoCompact`'s domain result to a `ResultEnvelope`.
|
|
18
|
+
*
|
|
19
|
+
* Extracted and exported so the CHANNEL each fact chooses can be asserted
|
|
20
|
+
* directly, without a real red-line probe.
|
|
21
|
+
*
|
|
22
|
+
* The harness-window CONFLICT ("peaks-loop divided this ratio by N but the
|
|
23
|
+
* settings file pins M") is a fact about the STATE, not an instruction, so it
|
|
24
|
+
* rides `warnings` — the very channel `peaks code context-now` already puts it
|
|
25
|
+
* on, via the same `harnessWindowSyncWarning`. Before this, the shim hard-coded
|
|
26
|
+
* `warnings: []`, so the identical fact reached a consumer as a sentence inside
|
|
27
|
+
* `nextActions` on this command and as a `warnings` entry on the other one. One
|
|
28
|
+
* fact, two shapes, depending on which of the two syncing commands a consumer
|
|
29
|
+
* happened to read: two parsers for one mechanism.
|
|
30
|
+
*
|
|
31
|
+
* The prose description (`describeHarnessWindowSync`) stays in `nextActions` on
|
|
32
|
+
* BOTH commands — that half is advice ("what to do about it") and both already
|
|
33
|
+
* agree on it.
|
|
34
|
+
*/
|
|
35
|
+
export declare function buildAutoCompactEnvelope(result: AutoCompactResult): ResultEnvelope<unknown>;
|
|
14
36
|
export declare function registerCodeRuntimeCommands(code: Command, io: ProgramIO): void;
|
|
@@ -14,6 +14,9 @@ import { fail, ok } from 'peaks-loop-shared/result';
|
|
|
14
14
|
import { detectPostCompactResume, formatPostCompactResumeLogLine } from '../../services/code/post-compact-detector.js';
|
|
15
15
|
import { runAutoCompact } from '../../services/code/auto-compact-orchestrator.js';
|
|
16
16
|
import { auditContext } from '../../services/context/context-audit.js';
|
|
17
|
+
import { syncHarnessWindowForProject } from '../../services/context/auto-compact-reader.js';
|
|
18
|
+
import { describeHarnessWindowSync, harnessWindowSyncWarning } from '../../services/context/harness-window-config.js';
|
|
19
|
+
import { resolveCanonicalProjectRoot } from '../../services/config/config-service.js';
|
|
17
20
|
import { buildContextAuditHint } from '../../services/context/context-audit-hint.js';
|
|
18
21
|
import { evaluateStep08, STEP_08_BACKUP_REGEX } from '../../services/code/step-08-gate.js';
|
|
19
22
|
import { evaluateEmitHandoff, JOB_NOT_INITIALIZED, JOB_REMAINING_BLOCKED } from '../../services/code/emit-handoff.js';
|
|
@@ -21,6 +24,38 @@ import { readJobShapeDecision, JobShapeDecisionError } from '../../services/code
|
|
|
21
24
|
import { getSkillPresence } from '../../services/skills/skill-presence-service.js';
|
|
22
25
|
import { probeInFlightBatch } from '../../services/workflow/workflow-inflight-probe.js';
|
|
23
26
|
import { resolveOuterSessionId } from '../../services/session/binding-status-service.js';
|
|
27
|
+
/**
|
|
28
|
+
* Adapt `runAutoCompact`'s domain result to a `ResultEnvelope`.
|
|
29
|
+
*
|
|
30
|
+
* Extracted and exported so the CHANNEL each fact chooses can be asserted
|
|
31
|
+
* directly, without a real red-line probe.
|
|
32
|
+
*
|
|
33
|
+
* The harness-window CONFLICT ("peaks-loop divided this ratio by N but the
|
|
34
|
+
* settings file pins M") is a fact about the STATE, not an instruction, so it
|
|
35
|
+
* rides `warnings` — the very channel `peaks code context-now` already puts it
|
|
36
|
+
* on, via the same `harnessWindowSyncWarning`. Before this, the shim hard-coded
|
|
37
|
+
* `warnings: []`, so the identical fact reached a consumer as a sentence inside
|
|
38
|
+
* `nextActions` on this command and as a `warnings` entry on the other one. One
|
|
39
|
+
* fact, two shapes, depending on which of the two syncing commands a consumer
|
|
40
|
+
* happened to read: two parsers for one mechanism.
|
|
41
|
+
*
|
|
42
|
+
* The prose description (`describeHarnessWindowSync`) stays in `nextActions` on
|
|
43
|
+
* BOTH commands — that half is advice ("what to do about it") and both already
|
|
44
|
+
* agree on it.
|
|
45
|
+
*/
|
|
46
|
+
export function buildAutoCompactEnvelope(result) {
|
|
47
|
+
const data = 'data' in result ? result.data : null;
|
|
48
|
+
const nextActions = 'nextActions' in result ? result.nextActions : [];
|
|
49
|
+
const warning = harnessWindowSyncWarning(data?.harnessWindow ?? null);
|
|
50
|
+
const warnings = warning === null ? [] : [warning];
|
|
51
|
+
if (result.ok) {
|
|
52
|
+
return ok('code.auto-compact', data ?? {}, warnings, [result.message, ...nextActions]);
|
|
53
|
+
}
|
|
54
|
+
// `fail()` hard-codes `warnings: []`, so the warning is spread back over it —
|
|
55
|
+
// a refused write can disagree with the ratio just as easily on the failure
|
|
56
|
+
// path, and the state does not become less true because the dispatch failed.
|
|
57
|
+
return { ...fail('code.auto-compact', result.code, result.message, data, [...nextActions]), warnings };
|
|
58
|
+
}
|
|
24
59
|
export function registerCodeRuntimeCommands(code, io) {
|
|
25
60
|
addJsonOption(code
|
|
26
61
|
.command('post-compact-detect')
|
|
@@ -58,9 +93,11 @@ export function registerCodeRuntimeCommands(code, io) {
|
|
|
58
93
|
.command('auto-compact')
|
|
59
94
|
.description('v2.13.0 AC-4: zero-human-intervention auto-compact. Probes current ' +
|
|
60
95
|
'context-fill % via the active IDE adapter; ≥ 0.85 writes a pre-compact ' +
|
|
61
|
-
'checkpoint + convergence plan + auto-decisions log; ≥ 0.95
|
|
62
|
-
'
|
|
63
|
-
'
|
|
96
|
+
'checkpoint + convergence plan + auto-decisions log; ≥ 0.95 ASKS the ' +
|
|
97
|
+
'harness to compact and reports that it is waiting for it — no ratio ' +
|
|
98
|
+
'blocks sub-agent dispatch, because peaks-loop has no way to compact a ' +
|
|
99
|
+
'running session and a gate nobody can satisfy gates nothing. The LLM / ' +
|
|
100
|
+
'runner keeps working at any ratio without human intervention. Pair with `peaks code ' +
|
|
64
101
|
'context-now` (AC-1), the read-only probe that reports the ratio this ' +
|
|
65
102
|
'command acts on. This command is also fired by the installed ' +
|
|
66
103
|
'PreToolUse hook, which passes `--project .` — without that argument ' +
|
|
@@ -71,7 +108,15 @@ export function registerCodeRuntimeCommands(code, io) {
|
|
|
71
108
|
.option('--session-id <sid>', 'override session id (default: read from active presence)')
|
|
72
109
|
.option('--in-flight-batch', 'defer if a sub-agent batch is in flight (D6.e)')
|
|
73
110
|
.option('--force', 'force compact at any ratio (test seam)')
|
|
74
|
-
.
|
|
111
|
+
// Accepted and inert. There is no longer a 95% gate to skip: peaks-loop
|
|
112
|
+
// cannot compact a running session, so the red line never blocked
|
|
113
|
+
// dispatch and `bypassRedLine` is read by nothing (slice
|
|
114
|
+
// 2026-09-13-auto-compact-trigger-ownership, T3/A1). The flag is KEPT
|
|
115
|
+
// rather than deleted because it is a published CLI surface — deleting it
|
|
116
|
+
// would make an existing caller fail on an unknown option, which is a
|
|
117
|
+
// harder break than a no-op — and because the honest fix here is to stop
|
|
118
|
+
// advertising it, not to change its meaning.
|
|
119
|
+
.option('--bypass-red-line', 'no-op: the 95% red line no longer gates dispatch, so there is nothing to bypass (accepted for backward compatibility)')
|
|
75
120
|
// No commander default here, deliberately. A declared default makes
|
|
76
121
|
// `opts.mode` permanently defined, which defeats the orchestrator's
|
|
77
122
|
// `input.mode ?? resolveAutoCompactMode(projectRoot)` fallback and
|
|
@@ -132,12 +177,7 @@ export function registerCodeRuntimeCommands(code, io) {
|
|
|
132
177
|
// generic accepts it. The orchestrator envelope carries
|
|
133
178
|
// `data` on success-path and `nextActions` on the error
|
|
134
179
|
// path; surface both directly to the user.
|
|
135
|
-
|
|
136
|
-
const nextActions = 'nextActions' in result ? result.nextActions : [];
|
|
137
|
-
const envelope = result.ok
|
|
138
|
-
? ok(`code.auto-compact`, data ?? {}, [], [result.message, ...nextActions])
|
|
139
|
-
: fail(`code.auto-compact`, code, result.message, data, [...nextActions]);
|
|
140
|
-
printResult(io, envelope, opts.json);
|
|
180
|
+
printResult(io, buildAutoCompactEnvelope(result), opts.json);
|
|
141
181
|
if (!exitOk)
|
|
142
182
|
process.exitCode = 1;
|
|
143
183
|
}
|
|
@@ -155,16 +195,19 @@ export function registerCodeRuntimeCommands(code, io) {
|
|
|
155
195
|
'Adapter-driven (no hard-coded IDE names): Claude Code is the MVP ' +
|
|
156
196
|
'implementation; trae / codex / cursor / qoder / tongyi-lingma / ' +
|
|
157
197
|
'hermes / openclaw register their own env-var via IdeAdapter.compact. ' +
|
|
158
|
-
'v3.1.2
|
|
159
|
-
'
|
|
160
|
-
'≥0.95 emits action=red-line (
|
|
198
|
+
'v3.1.2 / 2026-09-12: ≥0.85 emits action=auto-compact-now ' +
|
|
199
|
+
'(MANDATORY in every mode — single-rid included) and ' +
|
|
200
|
+
'≥0.95 emits action=red-line (the installed PreToolUse hook re-runs ' +
|
|
201
|
+
'this command on the next Bash/Task tool call; nothing is blocked). ' +
|
|
202
|
+
'--enforce-job-mode only changes the reported `jobMode` label; ' +
|
|
203
|
+
'the thresholds are identical. ' +
|
|
161
204
|
'Context-window override: set env PEAKS_CONTEXT_WINDOW_TOKENS=<positive int> ' +
|
|
162
205
|
'or `peaks config set --key context.windowTokens --value <positive int>`; ' +
|
|
163
206
|
'the JSON envelope reports the winning layer as capacitySource ' +
|
|
164
207
|
'(env-override | config | model-heuristic | default).')
|
|
165
208
|
.requiredOption('--project <path>', 'target project root')
|
|
166
209
|
.option('--session-id <sid>', 'override session id (default: read from active presence)')
|
|
167
|
-
.option('--enforce-job-mode', 'v3.1.2:
|
|
210
|
+
.option('--enforce-job-mode', 'v3.1.2: label the run as Job-shaped (jobMode=true). Auto-enabled when job-shape.json says isJob=true. Since 2026-09-12 the ≥0.85 MANDATORY auto-compact applies in single-rid mode too, so this flag no longer changes any action.')
|
|
168
211
|
.option('--prompt-size <bytes>', 'override the bytes-from-env path; takes priority over env / statusline / transcript. Useful when CLAUDE_CONTEXT_USAGE_PERCENT is absent (e.g. Mac Claude Code).')).action(async (opts) => {
|
|
169
212
|
try {
|
|
170
213
|
const { readContextPercent } = await import('../../services/context/auto-compact-reader.js');
|
|
@@ -206,21 +249,47 @@ export function registerCodeRuntimeCommands(code, io) {
|
|
|
206
249
|
env: process.env,
|
|
207
250
|
promptSizeBytes
|
|
208
251
|
});
|
|
252
|
+
// Slice 2026-09-13-auto-compact-trigger-ownership (T1 + T2): materialize
|
|
253
|
+
// the window this probe just divided by into the harness's own settings,
|
|
254
|
+
// so "85%" here and the harness's own trigger are one point on one
|
|
255
|
+
// scale. Idempotent (no write when the value is already in force) and a
|
|
256
|
+
// no-op when the probe carried no token window — peaks-loop never
|
|
257
|
+
// invents a number it did not measure.
|
|
258
|
+
const harnessWindow = syncHarnessWindowForProject({
|
|
259
|
+
// Promote `--project .` (what the PreToolUse hook passes) to the git
|
|
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),
|
|
264
|
+
env: process.env,
|
|
265
|
+
tokens: probe.capacityTokens ?? null
|
|
266
|
+
});
|
|
267
|
+
// The machine half of 要告知. A refused write is not automatically a
|
|
268
|
+
// non-event: the file may pin a window that disagrees with the one this
|
|
269
|
+
// probe just divided by, and then the ratio above describes a trigger
|
|
270
|
+
// the harness is not going to fire. `nextActions` carries the full
|
|
271
|
+
// sentence; this one line rides `warnings` so a JSON consumer cannot
|
|
272
|
+
// miss it either.
|
|
273
|
+
const harnessWindowWarning = harnessWindowSyncWarning(harnessWindow);
|
|
209
274
|
const ratioPct = (probe.ratio * 100).toFixed(1);
|
|
210
275
|
let action = 'ok';
|
|
211
276
|
let next = null;
|
|
212
277
|
if (probe.ratio >= 0.95) {
|
|
213
|
-
action =
|
|
278
|
+
action = 'red-line';
|
|
214
279
|
next = 'peaks code auto-compact';
|
|
215
280
|
}
|
|
216
281
|
else if (probe.ratio >= 0.85) {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
282
|
+
// Slice 2026-09-12-compact-band-policy (defect A): the
|
|
283
|
+
// 0.85–0.95 band is MANDATORY auto-compact for single-rid
|
|
284
|
+
// sessions too. It used to be downgraded to `soft-warn` here,
|
|
285
|
+
// which contradicted this command's own help text, the
|
|
286
|
+
// `pre-compact` action from `peaks skill presence`, the
|
|
287
|
+
// peaks-code SKILL.md, and the 2026-07-27 user-calibrated
|
|
288
|
+
// threshold policy — and left an LLM that follows the "single
|
|
289
|
+
// source of truth" (`context-now`) never compacting in the
|
|
290
|
+
// zone, violating the zero-pause contract.
|
|
291
|
+
action = 'auto-compact-now';
|
|
292
|
+
next = 'peaks code auto-compact';
|
|
224
293
|
}
|
|
225
294
|
else if (probe.ratio >= 0.5) {
|
|
226
295
|
action = 'soft-warn';
|
|
@@ -229,9 +298,16 @@ export function registerCodeRuntimeCommands(code, io) {
|
|
|
229
298
|
: action === 'auto-compact-now' ? 'pre-compact'
|
|
230
299
|
: action === 'soft-warn' ? 'soft-warn'
|
|
231
300
|
: 'ok';
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
301
|
+
// Slice 2026-09-12-compact-band-policy: the two modes no longer
|
|
302
|
+
// differ in behaviour ABOVE 0.50 — both auto-fire at ≥0.85 and
|
|
303
|
+
// both red-line at ≥0.95 — so the old "advisory mode
|
|
304
|
+
// (single-rid)" notice was a lie the moment the downgrade was
|
|
305
|
+
// removed. Job mode's remaining difference is that its threshold
|
|
306
|
+
// policy is recorded up front (`job-shape.json`), which the
|
|
307
|
+
// `jobMode` field already reports. The notice now says only that.
|
|
308
|
+
const gateModeNotice = isJobMode
|
|
309
|
+
? 'Job mode (job-shape.json isJob=true): the same ≥0.85 / ≥0.95 thresholds apply, and the decision is recorded in job-shape.json.'
|
|
310
|
+
: 'Single-rid mode: the same ≥0.85 / ≥0.95 thresholds apply — ≥0.85 is MANDATORY auto-compact, not advisory.';
|
|
235
311
|
printResult(io, ok('code.context-now', {
|
|
236
312
|
ratio: probe.ratio,
|
|
237
313
|
ratioPct: `${ratioPct}%`,
|
|
@@ -250,19 +326,37 @@ export function registerCodeRuntimeCommands(code, io) {
|
|
|
250
326
|
// default) — null for byte/percent sources, which have no window.
|
|
251
327
|
capacitySource: probe.capacitySource ?? null,
|
|
252
328
|
bytesPrompt: promptSizeBytes ?? null,
|
|
253
|
-
capturedAt: probe.capturedAt
|
|
254
|
-
|
|
329
|
+
capturedAt: probe.capturedAt,
|
|
330
|
+
// Slice 2026-09-13-auto-compact-trigger-ownership: what the harness
|
|
331
|
+
// window sync did on this probe. Reported rather than silent — the
|
|
332
|
+
// harness tells a user who overrides the window only via
|
|
333
|
+
// `/autocompact`, so peaks-loop must be the one that says it.
|
|
334
|
+
harnessWindow
|
|
335
|
+
}, harnessWindowWarning === null ? [] : [harnessWindowWarning], [
|
|
255
336
|
action === 'red-line'
|
|
256
|
-
? `RED LINE: ≥ 95%. Next: \`${next}\` (
|
|
337
|
+
? `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.`
|
|
257
338
|
: action === 'auto-compact-now'
|
|
258
|
-
? `
|
|
339
|
+
? `MANDATORY auto-compact (≥85%, every mode). Code MUST call \`${next}\` WITHOUT confirmation.`
|
|
259
340
|
: action === 'soft-warn'
|
|
260
|
-
?
|
|
261
|
-
? `Job mode soft-warn (50–85%). Continue working; the next \`peaks code auto-compact\` will re-check.`
|
|
262
|
-
: `Soft warn (50–85%). Continue working; the next \`peaks code auto-compact\` will re-check.`
|
|
341
|
+
? `Soft warn (50–85%). Continue working; the next \`peaks code auto-compact\` will re-check.`
|
|
263
342
|
: `Below 50%. No action required.`,
|
|
264
|
-
|
|
265
|
-
|
|
343
|
+
gateModeNotice,
|
|
344
|
+
// Single wording, shared with `peaks code auto-compact` — see
|
|
345
|
+
// `describeHarnessWindowSync`.
|
|
346
|
+
describeHarnessWindowSync(harnessWindow)
|
|
347
|
+
]),
|
|
348
|
+
// Slice 2026-09-13-auto-compact-trigger-ownership: was hard-coded
|
|
349
|
+
// `true`, which made the declared `--json` flag a no-op and left the
|
|
350
|
+
// human with raw JSON and no `next:` lines — so a person running this
|
|
351
|
+
// command could not see that peaks-loop had just written their
|
|
352
|
+
// harness settings. The notices above (including the key, the value
|
|
353
|
+
// and the rollback command) are `nextActions`, which `printResult`
|
|
354
|
+
// prints as `next: …` lines ONLY in the non-JSON path. Honoring the
|
|
355
|
+
// flag is what puts them in front of a human. See the consumer audit
|
|
356
|
+
// in the slice's RD artifact: every in-repo caller (the PreToolUse
|
|
357
|
+
// hooks, `orchestrator-can-do`, the skill runbooks) passes `--json`
|
|
358
|
+
// explicitly, so their byte-for-byte output is unchanged.
|
|
359
|
+
opts.json === true);
|
|
266
360
|
}
|
|
267
361
|
catch (err) {
|
|
268
362
|
printResult(io, fail('code.context-now', 'CONTEXT_NOW_FAILED', getErrorMessage(err), null, [
|
|
@@ -4,7 +4,9 @@ import { fail, ok, getErrorMessage } from 'peaks-loop-shared/result';
|
|
|
4
4
|
import { addJsonOption, printResult } from '../cli-helpers.js';
|
|
5
5
|
import { writeCheckpoint } from '../../services/session/session-checkpoint-service.js';
|
|
6
6
|
import { getSessionIdCanonical } from '../../services/session/session-manager.js';
|
|
7
|
-
import { readCompactHistory, summarizeCompactHistory, } from '../../services/compact-history/compact-history-service.js';
|
|
7
|
+
import { computeWindowCalibration, readCompactHistory, summarizeCompactHistory, } from '../../services/compact-history/compact-history-service.js';
|
|
8
|
+
import { readHarnessWindowState, resolveHarnessWindowLocation } from '../../services/context/auto-compact-reader.js';
|
|
9
|
+
import { disableHarnessWindowSync, reenableHarnessWindowSync, resetHarnessWindow } from '../../services/context/harness-window-config.js';
|
|
8
10
|
import { buildRecommendEnvelopePure, dryRunCompact, suggestCompact } from '../../services/compact/suggest-service.js';
|
|
9
11
|
import { PHASES, SURVIVAL_TABLE, isPhase, lookupPhaseTransition } from '../../services/compact/decision-tables.js';
|
|
10
12
|
function splitList(value) {
|
|
@@ -288,9 +290,16 @@ export function registerCompactCommands(program, io) {
|
|
|
288
290
|
return;
|
|
289
291
|
}
|
|
290
292
|
const summary = summarizeCompactHistory(result.events);
|
|
293
|
+
// Slice 2026-09-13-auto-compact-trigger-ownership (T4): the
|
|
294
|
+
// intent-vs-observed record. `pairs[i].requestedTokens` is the token
|
|
295
|
+
// point peaks-loop asked for; `observedTokens` is what the next real
|
|
296
|
+
// session actually measured. Unmeasured pairs are reported as such —
|
|
297
|
+
// this instrument states what happened, it does not predict.
|
|
298
|
+
const windowCalibration = computeWindowCalibration(result.events);
|
|
291
299
|
printResult(io, ok('compact.history', {
|
|
292
300
|
sessionId: session.sid,
|
|
293
301
|
...summary,
|
|
302
|
+
windowCalibration,
|
|
294
303
|
events: result.events,
|
|
295
304
|
parseErrors: result.parseErrors,
|
|
296
305
|
historyPath: result.path,
|
|
@@ -301,4 +310,123 @@ export function registerCompactCommands(program, io) {
|
|
|
301
310
|
process.exitCode = 1;
|
|
302
311
|
}
|
|
303
312
|
}));
|
|
313
|
+
// 7. peaks compact harness-window [--reset | --disable | --reenable]
|
|
314
|
+
// (slice 2026-09-13-auto-compact-trigger-ownership, T1 + T2)
|
|
315
|
+
//
|
|
316
|
+
// The window peaks-loop divides by and the window the harness compacts
|
|
317
|
+
// against must be ONE number, or "95%" lands at two different token counts.
|
|
318
|
+
// This command is the visible half of that write: the default reports what
|
|
319
|
+
// is in force, `--reset` removes it, `--disable` stops managing it.
|
|
320
|
+
//
|
|
321
|
+
// There is deliberately NO `--sync` flag. Materializing the window requires
|
|
322
|
+
// the ratio's own denominator, which only a probe has (it is the only place
|
|
323
|
+
// the active model is known). A `--sync` that re-derived the window here
|
|
324
|
+
// would be the second, independent resolution this slice exists to delete —
|
|
325
|
+
// `peaks code context-now` and `peaks code auto-compact` already sync.
|
|
326
|
+
addJsonOption(compact
|
|
327
|
+
.command('harness-window')
|
|
328
|
+
.description('Report / roll back the auto-compact window peaks-loop writes into the ' +
|
|
329
|
+
'harness\'s own machine-local settings (the adapter-declared ' +
|
|
330
|
+
'autoCompactWindowEnvVar, e.g. CLAUDE_CODE_AUTO_COMPACT_WINDOW in ' +
|
|
331
|
+
'.claude/settings.local.json). peaks-loop computes its context ratio ' +
|
|
332
|
+
'against exactly this number, so the value it reports as "85%" and the ' +
|
|
333
|
+
'point the harness compacts at are the same. Context probes ' +
|
|
334
|
+
'(`peaks code context-now`, `peaks code auto-compact`) materialize it ' +
|
|
335
|
+
'automatically. Two different intentions, two verbs: --reset removes ' +
|
|
336
|
+
'the key AND opts the project out so later probes do not put it back ' +
|
|
337
|
+
'(when the file holds no peaks-loop row at all there is nothing to ' +
|
|
338
|
+
'roll back, so --reset writes nothing); --disable records the opt-out ' +
|
|
339
|
+
'only, leaving whatever value is already there untouched, so "stop ' +
|
|
340
|
+
'managing this key" works on a project peaks-loop has never written ' +
|
|
341
|
+
'to; --reenable undoes either opt-out.')
|
|
342
|
+
.option('--project <path>', 'project root (defaults to git root or cwd)')
|
|
343
|
+
.option('--reset', 'rollback: remove the window key and stop managing it')
|
|
344
|
+
.option('--disable', 'record the opt-out only (do not manage this key), without removing a value that is already there; expressible before the first write')
|
|
345
|
+
.option('--reenable', 'undo a --reset or --disable opt-out (peaks-loop manages it again)')
|
|
346
|
+
.action((options) => {
|
|
347
|
+
try {
|
|
348
|
+
const project = options.project !== undefined
|
|
349
|
+
? resolveCanonicalProjectRoot(options.project)
|
|
350
|
+
: (findProjectRoot(process.cwd()) ?? process.cwd());
|
|
351
|
+
const location = resolveHarnessWindowLocation({ projectRoot: project, env: process.env });
|
|
352
|
+
if (location === null) {
|
|
353
|
+
printResult(io, ok('compact.harness-window', {
|
|
354
|
+
projectRoot: project,
|
|
355
|
+
managed: false,
|
|
356
|
+
message: 'The active IDE adapter declares no auto-compact window key, so peaks-loop cannot tie ' +
|
|
357
|
+
'the harness window to its own ratio. Nothing was written and there is nothing to roll back.',
|
|
358
|
+
}), options.json);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
if (options.reset === true) {
|
|
362
|
+
const result = resetHarnessWindow({ location, env: process.env });
|
|
363
|
+
printResult(io, ok('compact.harness-window', {
|
|
364
|
+
projectRoot: project,
|
|
365
|
+
action: result.action,
|
|
366
|
+
key: location.envVar,
|
|
367
|
+
settingsPath: result.settingsPath,
|
|
368
|
+
previousTokens: result.previousTokens,
|
|
369
|
+
}, [], [
|
|
370
|
+
result.action === 'removed'
|
|
371
|
+
? `Removed ${location.envVar} from ${result.settingsPath} and opted this project out, so later probes stop writing it. Undo with \`peaks compact harness-window --reenable\`.`
|
|
372
|
+
: `Nothing to remove — ${location.envVar} was already absent from ${result.settingsPath}, so nothing was written and no opt-out row was recorded: if a probe writes a window here later, run --reset again to remove it. To say "never manage this key here" WITHOUT waiting for that first write, run \`peaks compact harness-window --disable\`.`,
|
|
373
|
+
]), options.json);
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
if (options.disable === true) {
|
|
377
|
+
const result = disableHarnessWindowSync({ location });
|
|
378
|
+
printResult(io, ok('compact.harness-window', {
|
|
379
|
+
projectRoot: project,
|
|
380
|
+
action: result.action,
|
|
381
|
+
key: location.envVar,
|
|
382
|
+
settingsPath: result.settingsPath,
|
|
383
|
+
}, [], [
|
|
384
|
+
result.action === 'disabled'
|
|
385
|
+
? `Recorded the opt-out in ${result.settingsPath}: peaks-loop will not write ${location.envVar} here, and left any value already in the file exactly as it was. Undo with \`peaks compact harness-window --reenable\`.`
|
|
386
|
+
: result.action === 'already-opted-out'
|
|
387
|
+
? `Already opted out — ${result.settingsPath} carries ${location.envVar}'s opt-out, so nothing was written. Undo with \`peaks compact harness-window --reenable\`.`
|
|
388
|
+
: result.action === 'refused-unsafe-project-root'
|
|
389
|
+
? `Nothing written: the resolved project root is the user's own home directory, so ${result.settingsPath} is their PERSONAL harness settings, not a project's. peaks-loop already refuses to write ${location.envVar} there, so an opt-out would change nothing except which refusal you see. Point --project at a project (a subdirectory of home is fine) and the opt-out lands there.`
|
|
390
|
+
: `Nothing written — ${result.settingsPath} is not a JSON object peaks-loop can safely edit, so the opt-out could not be recorded there.`,
|
|
391
|
+
]), options.json);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
if (options.reenable === true) {
|
|
395
|
+
const result = reenableHarnessWindowSync({ location });
|
|
396
|
+
printResult(io, ok('compact.harness-window', { projectRoot: project, action: result.action, key: location.envVar, settingsPath: result.settingsPath }, [], [
|
|
397
|
+
result.action === 'reenabled'
|
|
398
|
+
? 'Opt-out cleared; the next context probe will materialize the window again.'
|
|
399
|
+
: 'No opt-out was recorded for this project.',
|
|
400
|
+
]), options.json);
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
const state = readHarnessWindowState({ projectRoot: project, env: process.env });
|
|
404
|
+
printResult(io, ok('compact.harness-window', {
|
|
405
|
+
projectRoot: project,
|
|
406
|
+
managed: true,
|
|
407
|
+
key: location.envVar,
|
|
408
|
+
settingsPath: location.settingsPath,
|
|
409
|
+
tokens: state?.tokens ?? null,
|
|
410
|
+
raw: state?.raw ?? null,
|
|
411
|
+
source: state?.source ?? null,
|
|
412
|
+
optedOut: state?.optedOut ?? false,
|
|
413
|
+
// Provenance: whether this value is peaks-loop's own write or
|
|
414
|
+
// one the user set by hand. It decides whether the late 1M
|
|
415
|
+
// rescue may override it, so it is not just diagnostics.
|
|
416
|
+
peakWritten: state?.peakWritten ?? false,
|
|
417
|
+
}, [], [
|
|
418
|
+
state?.tokens === null || state?.tokens === undefined
|
|
419
|
+
? 'No window is set yet. The next `peaks code context-now` probe materializes the window it computes its ratio against.'
|
|
420
|
+
: `peaks-loop computes its context ratio against ${state.tokens} tokens; the harness fires near the end of that window. ${state.peakWritten
|
|
421
|
+
? 'peaks-loop wrote this value; it may raise it if a session outgrows it.'
|
|
422
|
+
: 'This value is not peaks-loop\'s own write, so peaks-loop will not raise it — it is treated as your setting.'}`,
|
|
423
|
+
'Rollback: `peaks compact harness-window --reset`.',
|
|
424
|
+
'Intent-vs-observed calibration: `peaks compact history --json` → windowCalibration.',
|
|
425
|
+
]), options.json);
|
|
426
|
+
}
|
|
427
|
+
catch (error) {
|
|
428
|
+
printResult(io, fail('compact.harness-window', 'COMPACT_HARNESS_WINDOW_FAILED', getErrorMessage(error), {}, ['Verify the project path is writable and a session is bound']), options.json);
|
|
429
|
+
process.exitCode = 1;
|
|
430
|
+
}
|
|
431
|
+
}));
|
|
304
432
|
}
|
|
@@ -37,7 +37,7 @@ function detectContainerRuntime(explicit) {
|
|
|
37
37
|
const tryOrder = explicit ? [explicit] : ['docker', 'podman'];
|
|
38
38
|
for (const r of tryOrder) {
|
|
39
39
|
try {
|
|
40
|
-
const version = execSync(`${r} --version`, { stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf8' }).trim();
|
|
40
|
+
const version = execSync(`${r} --version`, { stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf8', windowsHide: true }).trim();
|
|
41
41
|
return { ok: true, runtime: r, binary: `${r} (${version.split('\n')[0] ?? ''})` };
|
|
42
42
|
}
|
|
43
43
|
catch {
|
|
@@ -117,7 +117,7 @@ export function registerContainerCommand(program, io) {
|
|
|
117
117
|
// when the lease file is missing.
|
|
118
118
|
const cidFile = `${normalizePath(joinPathSession(projectRoot, sessionId))}/.${runtime.runtime}-cid-${leaseId}`;
|
|
119
119
|
try {
|
|
120
|
-
execSync(`${runtime.runtime} run --rm -d --cidfile "${cidFile}" --label "peaks.leaseId=${leaseId}" --label "peaks.rid=${options.rid}" -v "${mount}:/work" -w /work ${image} sleep infinity`, { cwd: projectRoot, stdio: 'pipe', encoding: 'utf8' });
|
|
120
|
+
execSync(`${runtime.runtime} run --rm -d --cidfile "${cidFile}" --label "peaks.leaseId=${leaseId}" --label "peaks.rid=${options.rid}" -v "${mount}:/work" -w /work ${image} sleep infinity`, { cwd: projectRoot, stdio: 'pipe', encoding: 'utf8', windowsHide: true });
|
|
121
121
|
}
|
|
122
122
|
catch (err) {
|
|
123
123
|
printResult(io, fail('container.spawn', 'DOCKER_RUN_FAILED', getErrorMessage(err), { rid: options.rid, image, runtime: runtime.runtime, sessionId }, [
|
|
@@ -199,7 +199,7 @@ export function registerContainerCommand(program, io) {
|
|
|
199
199
|
try {
|
|
200
200
|
const releaseRuntime = detectContainerRuntime(undefined);
|
|
201
201
|
const runtimeCmd = releaseRuntime.ok ? releaseRuntime.runtime : 'docker';
|
|
202
|
-
execSync(`${runtimeCmd} rm --force "${lease.containerId}"`, { cwd: projectRoot, stdio: 'pipe', encoding: 'utf8' });
|
|
202
|
+
execSync(`${runtimeCmd} rm --force "${lease.containerId}"`, { cwd: projectRoot, stdio: 'pipe', encoding: 'utf8', windowsHide: true });
|
|
203
203
|
}
|
|
204
204
|
catch {
|
|
205
205
|
dockerRmFailed = true;
|
|
@@ -307,17 +307,18 @@ export function registerSkillCommand(program, io) {
|
|
|
307
307
|
});
|
|
308
308
|
addJsonOption(skill
|
|
309
309
|
.command('presence:clear')
|
|
310
|
-
.description('
|
|
310
|
+
.description('Unlink the DEPRECATED pre-4.0.11 single-slot presence marker files (`.peaks/_runtime/active-skill.json`, `.peaks/.active-skill.json`). It does NOT terminalize a live presence lease — a workflow-bound lease terminalizes through `peaks workflow terminalize --workflow <id> --reason <reason>`, an ad-hoc lease only at session exit, and raw unlink is FORBIDDEN for both. The envelope reports what is still active.')
|
|
311
311
|
.option('--project <path>', 'project root path (auto-detected from cwd when omitted)')).action(async (options) => {
|
|
312
312
|
const projectRoot = options.project ?? findProjectRoot(process.cwd()) ?? process.cwd();
|
|
313
|
-
//
|
|
314
|
-
//
|
|
315
|
-
//
|
|
316
|
-
//
|
|
317
|
-
//
|
|
318
|
-
//
|
|
319
|
-
//
|
|
320
|
-
//
|
|
313
|
+
// What this command actually is: a stale-marker cleanup for projects
|
|
314
|
+
// carrying a pre-4.0.11 single-slot file, and nothing more. The previous
|
|
315
|
+
// description claimed it "routes workflow leases through `workflow
|
|
316
|
+
// terminalize`" and that a "compat wrapper re-routes [it] to
|
|
317
|
+
// `terminalizePresenceLease` when a workflow binding is present" — there is
|
|
318
|
+
// no such wrapper, and no such routing happens here or in
|
|
319
|
+
// `clearSkillPresence`. A caller who read that line and ran this command
|
|
320
|
+
// expecting a live workflow lease to terminalize got a successful exit code
|
|
321
|
+
// and a still-running lease.
|
|
321
322
|
const removed = clearSkillPresence(options.project);
|
|
322
323
|
// Auto-update project context so future sessions have up-to-date history.
|
|
323
324
|
// Slice 2026-07-15-project-scan-bootstrap: generateProjectContext now also
|
|
@@ -329,7 +330,41 @@ export function registerSkillCommand(program, io) {
|
|
|
329
330
|
catch { // TODO(g2): legacy silent catch — grace: 1 minor release (v2.14.0)
|
|
330
331
|
// non-fatal: context update failure should not block presence clear
|
|
331
332
|
}
|
|
332
|
-
|
|
333
|
+
// Report the state the project is actually in, not the state this command
|
|
334
|
+
// intended to reach. `clearSkillPresence` unlinks only the DEPRECATED
|
|
335
|
+
// single-slot marker files; the canonical sid-scoped lease is deliberately
|
|
336
|
+
// left alive for an AD-HOC lease (raw unlink is FORBIDDEN — only session
|
|
337
|
+
// exit may terminalize one; workflow leases route through `workflow
|
|
338
|
+
// terminalize`). So after a `presence:set` the very next `peaks skill
|
|
339
|
+
// presence` call still reads `active: true`. A hardcoded `active: false`
|
|
340
|
+
// therefore reported a state the command never reached, and the caller had
|
|
341
|
+
// no way to tell that from a real clear.
|
|
342
|
+
//
|
|
343
|
+
// Re-reading through `getSkillPresence` — the same projection the
|
|
344
|
+
// `skill presence` command serves — is the only derivation that cannot
|
|
345
|
+
// drift from what the next command prints: it is the same read path, on
|
|
346
|
+
// the same project root, evaluated after every write this command makes.
|
|
347
|
+
const active = getSkillPresence(projectRoot) !== null;
|
|
348
|
+
// `cleared` is the live question the caller is actually asking ("is it gone
|
|
349
|
+
// now?"), which `removed` could never answer: a legacy marker being unlinked
|
|
350
|
+
// says nothing about the lease the next command will read. When it is false
|
|
351
|
+
// the envelope says WHY, in the two routes that can terminalize a lease —
|
|
352
|
+
// without this the caller saw `active: true, removed: false` and had no way
|
|
353
|
+
// to tell "you ran the wrong command" from "this command has no opinion".
|
|
354
|
+
const cleared = !active;
|
|
355
|
+
printResult(io, ok('skill.presence:clear', {
|
|
356
|
+
active,
|
|
357
|
+
removed,
|
|
358
|
+
cleared,
|
|
359
|
+
...(cleared ? {} : { reason: 'live-lease-survives-presence-clear' }),
|
|
360
|
+
projectContextUpdated: true
|
|
361
|
+
}, [], cleared
|
|
362
|
+
? []
|
|
363
|
+
: [
|
|
364
|
+
'A workflow-bound lease terminalizes through `peaks workflow terminalize --workflow <id> --reason <reason>`.',
|
|
365
|
+
'An ad-hoc lease (`peaks skill presence:set`) is terminalizable only at session exit; `presence:clear` will not remove it, and raw unlink is FORBIDDEN.',
|
|
366
|
+
'Run `peaks skill presence --json` to read the lease that survived.'
|
|
367
|
+
]), options.json);
|
|
333
368
|
});
|
|
334
369
|
// Slice 4.0.8 (RD §4): manual lease GC primitive. LLM-coordinated;
|
|
335
370
|
// never a user-typed requirement. The user / LLM runner invokes
|