peaks-loop 4.0.37 → 4.0.39
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 +37 -0
- package/README-en.md +1 -1
- package/README.md +1 -1
- package/dist/cli/commands/code-review-commands.js +43 -1
- package/dist/cli/commands/code-runtime-commands.js +16 -4
- package/dist/cli/commands/core/skill-command.d.ts +44 -0
- package/dist/cli/commands/core/skill-command.js +67 -3
- package/dist/cli/commands/dispatch-commands.js +42 -12
- package/dist/cli/commands/hooks-commands.js +31 -6
- package/dist/services/code/auto-compact-orchestrator.js +2 -2
- package/dist/services/context/auto-compact-dispatcher.js +3 -1
- package/dist/services/context/build-dispatch-system-prompt.d.ts +35 -1
- package/dist/services/context/build-dispatch-system-prompt.js +55 -3
- package/dist/services/hooks/auto-compact-hook-install.d.ts +14 -1
- package/dist/services/hooks/auto-compact-hook-install.js +39 -15
- package/dist/services/lint/detect-ocr-18.d.ts +9 -1
- package/dist/services/lint/detect-ocr-18.js +114 -10
- package/dist/services/lint/ocr-18-acquire.d.ts +113 -0
- package/dist/services/lint/ocr-18-acquire.js +350 -0
- package/dist/services/lint/ocr-multilang-adapter.js +12 -2
- package/dist/services/skills/hooks-codegate-superpowers.d.ts +41 -0
- package/dist/services/skills/hooks-codegate-superpowers.js +95 -0
- package/dist/services/skills/hooks-settings-service.d.ts +16 -0
- package/dist/services/skills/hooks-settings-service.js +46 -13
- package/dist/services/web/playwright-loader.js +5 -24
- package/dist/services/workflow/provision-dispatch-node.d.ts +42 -0
- package/dist/services/workflow/provision-dispatch-node.js +66 -0
- package/dist/services/workspace/claude-settings-template.d.ts +19 -3
- package/dist/services/workspace/claude-settings-template.js +25 -5
- package/dist/services/workspace/workspace-claude-settings-materializer.js +111 -29
- package/dist/shared/npm-cache.d.ts +2 -0
- package/dist/shared/npm-cache.js +31 -0
- package/package.json +5 -5
- package/skills/bee/peaks-perf-audit/SKILL.md +24 -0
- package/skills/bee/peaks-prd/SKILL.md +24 -0
- package/skills/bee/peaks-qa/SKILL.md +24 -0
- package/skills/bee/peaks-rd/SKILL.md +24 -0
- package/skills/bee/peaks-reviewer/SKILL.md +24 -0
- package/skills/bee/peaks-sc/SKILL.md +24 -0
- package/skills/bee/peaks-security-audit/SKILL.md +24 -0
- package/skills/bee/peaks-txt/SKILL.md +24 -0
- package/skills/bee/peaks-ui/SKILL.md +24 -0
- package/skills/peaks-audit/SKILL.md +24 -0
- package/skills/peaks-code/SKILL.md +24 -0
- package/skills/peaks-content/SKILL.md +24 -0
- package/skills/peaks-doctor/SKILL.md +24 -0
- package/skills/peaks-final-review/SKILL.md +24 -0
- package/skills/peaks-ide/SKILL.md +24 -0
- package/skills/peaks-issue-fix-orchestrator/SKILL.md +24 -0
- package/skills/peaks-resume/SKILL.md +24 -0
- package/skills/peaks-slice-decompose/SKILL.md +24 -0
- package/skills/peaks-solo/SKILL.md +24 -0
- package/skills/peaks-sop/SKILL.md +24 -0
- package/skills/peaks-status/SKILL.md +24 -0
- package/skills/peaks-test/SKILL.md +24 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,42 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.0.39 — 2026-09-11 (四条"死机制" + 可见的 OCR 获取)
|
|
4
|
+
|
|
5
|
+
**Highlights**:
|
|
6
|
+
|
|
7
|
+
1. **本轮查出的东西有一个共同形状:机制写好了、但从没执行过。** 四条各自独立,每一条都能通过全部现有测试,因为**测试断言的是"声明存在",不是"它真的跑"**:
|
|
8
|
+
|
|
9
|
+
- **`24h` 模式从没选过 `partial`。** `--mode` 声明里带了 commander 默认值 `'standard'`,于是 `opts.mode` **永远有值**,orchestrator 里 `input.mode ?? resolveAutoCompactMode(projectRoot)` 的右边**永远不执行**。每个 24h 会话都在按 standard 的 0.80/0.85 跑,而 help 文本写着 0.65/0.70。症状看起来是"两个命令报的 mode 不一致",根因是**默认值顶掉了一条分支**。修的时候要动两处:只删默认值不够,handler 仍会把 `'standard'` 显式传下去。
|
|
10
|
+
- **auto-compact 的 hook 一次都没触发过。** 安装进 `settings.local.json` 的命令是 `peaks code auto-compact`,而 `--project` 是 `.requiredOption` —— 于是它挂在 `Bash|Task` 上,**每次工具调用都以"缺 --project"失败**。非阻塞错误,桌面上什么都看不到。更要紧的是:光改常量**到不了任何已有安装**,因为安装器只比对 matcher、从不比对命令,一看到 `Bash|Task` 条目就返回"已安装"。现在它是一起迁移。
|
|
11
|
+
- **那条"忽略离线模板"的 managed 规则,在任何项目里都没生效过。** 模式是按**项目根**写的,却被写进 `.peaks/.gitignore` —— 带 `/` 的 gitignore 模式锚定到所在目录,于是解析成 `.peaks/.peaks/...`,匹配不到任何东西;而根级的 `.claude/settings.local.json` 更是**根本无法**从 `.peaks/` 内部表达(gitignore 没有 `..`)。后果在本仓库可见:模板文件因为这条规则never生效而一直被跟踪,每次发版都显示 modified,被当"机器路径泄漏"**回滚了三次**——它从来不是泄漏。snippet 已改写到根,并加了**行为式**守卫(逐条模式用 `git check-ignore --no-index` 验证生效;已实测对旧行为失败)。顺带:搬家会留下僵尸块,`workspace init` 现在把它剥掉(保留用户写的每一行,畸形块不动)。
|
|
12
|
+
- **`--graph-node` 的强制只由 `.requiredOption` 支撑。** 传一个**根本不存在的**节点,dispatch 照样成功 —— 下游从不校验,`PEAKS_GRAPH_NODE_NOT_PREPARED` 只被 import、从未抛出;而且 `graphNodeId`/`workflowId`/`graphRef` **从未传给记录写入器**,记录上恒为 null,那条 transition 永远不触发。**这个特性整条链是死的,唯一真实的是它的前置条件** —— 而那个前置条件正好把每个没有图基础设施的项目(即:除了 peaks-loop 自己以外的所有项目)全打断。那句"友好提示"指向的路径本身也走不通:`workflow node prepare` **从不落盘**(打印一个节点就丢),而它要求的图没有任何命令会创建。现在可选 + 按需 provision,三步压成一步,并把三个字段真正写进记录。
|
|
13
|
+
|
|
14
|
+
2. **契约与指导,现在能到达它们该到的地方。** 零暂停契约(「compaction 是技能自己的动作,不是用户的」)早就写在 `peaks-code/SKILL.md` 里,`:180` 甚至点名禁止"prompt the user to run `/compact`" —— **而它依然被违反了,因为 SKILL.md 正文只加载一次、随后会被 compact 掉,规则恰好在上下文压力大到需要它的那一刻失效**;而且 22 个技能里只有 1 个有这段。现在它挂在**每轮必调的 tool 输出**上(`peaks skill presence --json` 的返回值里带 `context: { ratioPct, action, mode }`),compact 不掉,且技能无关。同样地,read-first 指导此前只注入了**子代理**的 system prompt,真正被闸门拒的**主会话**从没收到 —— 现在两者都在 22 个技能共享的、有**漂移守卫**的块里。
|
|
15
|
+
|
|
16
|
+
3. **OCR 获取变成显式、可见的一步。** dogfood 时桌面上弹出了一个标题为 `npm i @alibaba-group/open-code-review@1.11.9` 的窗口,而它来自一个自称 **"Read-only probe"** 的命令 —— `npx --package …` 在包未缓存时会**安装**。因果值得记下:**是今天的 unblock 让它暴露的** —— 修好之前,探针在启动步就死了,根本走不到安装。现在探针**只看不取**,获取成为独立动词,带安装锁与下载前告警,输出走 `inherit` 让 npm 自己的进度到达用户终端。Shell 偏好 **Git Bash → PowerShell**,且**每次都上报用了哪个** —— 静默换壳正是这场混乱的起点。
|
|
17
|
+
|
|
18
|
+
4. **本轮的一条自查:我写的守卫,自己犯了它要防的病。** 漂移守卫用 `execSync('find skills -name SKILL.md')` 找文件。`execSync` 走平台 shell,CI 的 Windows 上是 `cmd.exe`,那里的 `find` 是 `System32\find.exe`(另一个程序,不认 `-name`)。**本地能过,只因为 Git Bash 的 `find` 在 PATH 上胜出** —— 典型的"测试断言的是这台笔记本"。CI 第一次推就抓到。已改成 `fs` 遍历,无 shell 参与。
|
|
19
|
+
|
|
20
|
+
**验证**:三个版本常量一致(4.0.39);`tsc -p tsconfig.build.json` exit 0;宽 `tsconfig.json` 保持 **142** 基线;`tests/unit` **188 files / 1793 passed / 3 skipped / 0 failed**;CI 在 ubuntu + windows(Node 22)双平台绿。本轮新增的守卫**每一条都实测过"对旧行为会失败"**。
|
|
21
|
+
|
|
22
|
+
## 4.0.38 — 2026-09-11 (适配外部闸门 + 多语言评审复活 + 测试不再弹窗)
|
|
23
|
+
|
|
24
|
+
**Highlights**:
|
|
25
|
+
|
|
26
|
+
1. **子代理被事先告知"先读再改",闸门不再被误判** — 外部插件(ECC)注册了一个 `PreToolUse` 闸门,会在编辑一个文件时要求 LLM 先建立事实,否则**拒绝**。问题不在闸门本身,而在它的提示词**读起来像工具坏了**——它从不说"你的编辑没有被应用",也从不点名真正的前置动作是**先读文件**。现在每个派发出去的子代理的前缀里都有一段 ~693 B 的说明,**规矩在前、范围点名、失败声明在后**:先说"编辑前先读这个文件,这是这里的正常工作方式",点名作用范围(`.peaks/**` 之外的所有路径),**最后**才说万一跳过会撞上什么、以及那不是失败、编辑未被应用。
|
|
27
|
+
|
|
28
|
+
- **代价如实记录**:这段是 7 个角色各加 694 B,大致**把 4.0.36 那一轮省下的 18% 还了回去**。这是用户明确要求的适配,它值这些字节(否则每次首次编辑都要多一轮往返),但提示词经济性的账今天倒退了,这件事应当被看见。
|
|
29
|
+
|
|
30
|
+
2. **`.peaks/**` 豁免被自动声明** — peaks-loop **从 `2.0.1-bug3` 起就已经决定了** `.peaks/**` 不该被 fact-gate(那个切片就叫 *fact-forcing bypass*),只是外部闸门从没听说过。现在 `peaks hooks install` / `workspace init` 会把豁免写进**机器本地**的 `settings.local.json`。四条约束都做到了:**第三方变量名全仓库只出现一处**(adapter 单点映射)、**只进机器本地**(绝不进已提交的共享模板——一个第三方变量名不该推给每个消费者)、**合并而非覆盖**(用户已有的豁免取并集,无关 `env` 键保留)、范围用 **`.peaks/**` 而非 `_runtime/`**。15 个用例覆盖,含 uninstall(只剥自己的 glob)与非 Claude Code IDE(不写)。
|
|
31
|
+
|
|
32
|
+
3. **测试套件不再往桌面弹 PowerShell 窗口** — `write-gate-decision-table.test.ts` 是全仓库唯一 spawn `powershell` 的测试,**没设 `windowsHide`**,且**对决策表每一行都 spawn 一次**。决定性的证据是它的隔壁文件:`gate-enforce-machine-local-shell.test.ts` 的开头就写明了"执行 hook 就是弹窗的原因",因此**刻意不执行**——一个文件知道并回避,另一个照做。修法是**加 `windowsHide` 但保留执行**(真的把命令跑过每个 shell 正是当初抓出 C5 `argv` 反转的原因)。6 处已修,并确认仍在真实执行(`✓ given bash … 1583ms`、`✓ given powershell … 3755ms`)。
|
|
33
|
+
|
|
34
|
+
4. **多语言评审在 Windows 上复活** — `peaks code-review detect-ocr-18` 在一台 `npx --version` 正常返回 `11.9.0` 的机器上报 `npxAvailable: false`,于是**八种语言**(python/go/java/rust/cpp/csharp/ruby/php,经 `@alibaba-group/open-code-review`)的评审路径**在这个平台上从未可达**。三处仍在 spawn 裸 `npx`(Windows 上是 `.cmd`,无法这样启动)——而**同一个目录里 `detect-eslint.ts` 早就改成正确形式了**。现在三处都走既有的 `resolveNpxInvocation`,并且**"npx 不存在"与"启动不了"分开报**(正是这个混淆让一台正常的机器报成缺工具)。修前 `ocr18-missing / npxAvailable:false` → 修后 **`ready / npxAvailable:true`**。
|
|
35
|
+
|
|
36
|
+
5. **`peaks hooks install --global` 此前从未成功过** — ESM 包里用了裸 `__dirname`,抛 `__dirname is not defined`,于是**每一次** `--global` 都返回失败——而且是**在 settings 已经写完之后**才失败,把半成品报成失败。同时 `--dry-run --global` **真的会往 `~/.claude/` 拷文件**。两者都已修。
|
|
37
|
+
|
|
38
|
+
**验证**:build clean、`tsc -p tsconfig.build.json` exit 0、宽口径 `tsconfig.json` 维持既有 142;全量 **179 files / 1736 passed(3 skipped)**;版本三处常量(`package.json` / `CLI_VERSION` / `RUNTIME_VERSION`)一致——4.0.37 补上的那条 sync 与那个"能真失败"的守卫当场兑现。
|
|
39
|
+
|
|
3
40
|
## 4.0.37 — 2026-09-10 (Windows spawn 根因 + 会话解析 + 诚实性修复)
|
|
4
41
|
|
|
5
42
|
**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.39 (2026-09-11) |
|
|
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.39(2026-09-11) |
|
|
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,8 +1,11 @@
|
|
|
1
1
|
import { addJsonOption, getErrorMessage, printResult } from '../cli-helpers.js';
|
|
2
2
|
import { fail, ok } from 'peaks-loop-shared/result';
|
|
3
3
|
import { detectOcr18 } from '../../services/lint/detect-ocr-18.js';
|
|
4
|
-
import {
|
|
4
|
+
import { acquireOcr18 } from '../../services/lint/ocr-18-acquire.js';
|
|
5
|
+
import { OCR_18_LANGUAGES, OCR_18_PACKAGE, runOcr18 } from '../../services/lint/ocr-multilang-adapter.js';
|
|
5
6
|
const SUPPORTED_LANGUAGES_SET = new Set(OCR_18_LANGUAGES);
|
|
7
|
+
/** What a caller can do when the acquisition did not land the package. */
|
|
8
|
+
const ACQUIRE_RETRY_ACTION = 'Re-run `peaks code-review acquire-ocr-18` once the network is available.';
|
|
6
9
|
export function registerCodeReviewCommands(program, io) {
|
|
7
10
|
const codeReview = program
|
|
8
11
|
.command('code-review', { hidden: true })
|
|
@@ -23,6 +26,45 @@ export function registerCodeReviewCommands(program, io) {
|
|
|
23
26
|
printResult(io, fail('code-review.detect-ocr-18', 'DETECT_FAILED', getErrorMessage(error), { state: 'detection-failed' }, ['Re-run with a valid npx on PATH.']), options.json);
|
|
24
27
|
}
|
|
25
28
|
});
|
|
29
|
+
addJsonOption(codeReview
|
|
30
|
+
.command('acquire-ocr-18')
|
|
31
|
+
.description('Acquire the OCR 1.8.x reviewer: runs the pinned npx package VISIBLY (npm progress on the ' +
|
|
32
|
+
'terminal) and installs it once when it is not already cached. The companion to the read-only ' +
|
|
33
|
+
'`detect-ocr-18` probe, which never installs.')).action(async (options) => {
|
|
34
|
+
const command = 'code-review.acquire-ocr-18';
|
|
35
|
+
try {
|
|
36
|
+
// R6's other half, mirrored from `peaks web install`: "nothing to fetch"
|
|
37
|
+
// is the only reason not to fetch, so an acquisition with nothing to do
|
|
38
|
+
// says so without spawning anything.
|
|
39
|
+
const before = detectOcr18();
|
|
40
|
+
if (before.state === 'ready') {
|
|
41
|
+
printResult(io, ok(command, { acquired: false, state: before.state, package: before.package, shell: null, shellNote: null }, [], []), options.json);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
// `asJson` is threaded in so the installer cannot write its stdout into
|
|
45
|
+
// the envelope this verb prints to the same stream.
|
|
46
|
+
const outcome = await acquireOcr18({ asJson: options.json === true });
|
|
47
|
+
if (!outcome.ok) {
|
|
48
|
+
printResult(io, fail(command, outcome.code, outcome.message, { state: 'acquire-failed', shell: outcome.shell.kind, shellNote: outcome.shell.note }, [ACQUIRE_RETRY_ACTION]), options.json);
|
|
49
|
+
process.exitCode = 1;
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
// R7, mirrored: an installer that exits 0 without landing the package is
|
|
53
|
+
// CONSULTED, not assumed — a proxy filtering the registry, or `npx`
|
|
54
|
+
// resolving into a different cache root, both exit 0.
|
|
55
|
+
const after = detectOcr18();
|
|
56
|
+
if (after.state !== 'ready') {
|
|
57
|
+
printResult(io, fail(command, 'OCR18_ACQUIRE_INCOMPLETE', `the acquisition exited 0 but ${OCR_18_PACKAGE} still does not resolve (${after.state})`, { state: after.state, shell: outcome.shell.kind, shellNote: outcome.shell.note }, [...after.nextActions]), options.json);
|
|
58
|
+
process.exitCode = 1;
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
printResult(io, ok(command, { acquired: true, state: after.state, package: after.package, shell: outcome.shell.kind, shellNote: outcome.shell.note, durationMs: outcome.durationMs }, [...outcome.warnings]), options.json);
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
printResult(io, fail(command, 'OCR18_ACQUIRE_FAILED', getErrorMessage(error), { state: 'acquire-failed' }, [ACQUIRE_RETRY_ACTION]), options.json);
|
|
65
|
+
process.exitCode = 1;
|
|
66
|
+
}
|
|
67
|
+
});
|
|
26
68
|
addJsonOption(codeReview
|
|
27
69
|
.command('run-ocr-18')
|
|
28
70
|
.description('Invoke `ocr review --filter-language <lang>` (or `ocr delegate preview` when --delegate) for one of 8 supported languages.')
|
|
@@ -60,8 +60,11 @@ export function registerCodeRuntimeCommands(code, io) {
|
|
|
60
60
|
'context-fill % via the active IDE adapter; ≥ 0.85 writes a pre-compact ' +
|
|
61
61
|
'checkpoint + convergence plan + auto-decisions log; ≥ 0.95 forces ' +
|
|
62
62
|
'synchronous IDE-side compact. The LLM / runner keeps working with ' +
|
|
63
|
-
'context < 95% without human intervention.
|
|
64
|
-
'now` (AC-1)
|
|
63
|
+
'context < 95% without human intervention. Pair with `peaks code ' +
|
|
64
|
+
'context-now` (AC-1), the read-only probe that reports the ratio this ' +
|
|
65
|
+
'command acts on. This command is also fired by the installed ' +
|
|
66
|
+
'PreToolUse hook, which passes `--project .` — without that argument ' +
|
|
67
|
+
'the hook could never run at all. rid-027 ' +
|
|
65
68
|
'adds `--mode <mode>`: `standard` (0.85/0.95) or `partial` (0.70/0.85 ' +
|
|
66
69
|
'for 24h long-run mode).')
|
|
67
70
|
.requiredOption('--project <path>', 'target project root')
|
|
@@ -69,7 +72,12 @@ export function registerCodeRuntimeCommands(code, io) {
|
|
|
69
72
|
.option('--in-flight-batch', 'defer if a sub-agent batch is in flight (D6.e)')
|
|
70
73
|
.option('--force', 'force compact at any ratio (test seam)')
|
|
71
74
|
.option('--bypass-red-line', 'skip the 95% red-line gate (test seam; never true in production)')
|
|
72
|
-
|
|
75
|
+
// No commander default here, deliberately. A declared default makes
|
|
76
|
+
// `opts.mode` permanently defined, which defeats the orchestrator's
|
|
77
|
+
// `input.mode ?? resolveAutoCompactMode(projectRoot)` fallback and
|
|
78
|
+
// silently disables "24h mode auto-selects partial" — the help text
|
|
79
|
+
// below promises it. Absence must stay absent.
|
|
80
|
+
.option('--mode <mode>', 'auto-compact mode (standard | partial). Default: standard. 24h mode auto-selects partial.')).action(async (opts) => {
|
|
73
81
|
try {
|
|
74
82
|
const { isValidMode } = await import('../../services/code/auto-compact-modes.js');
|
|
75
83
|
const modeName = opts.mode ?? 'standard';
|
|
@@ -112,7 +120,11 @@ export function registerCodeRuntimeCommands(code, io) {
|
|
|
112
120
|
: {}),
|
|
113
121
|
force: opts.force === true,
|
|
114
122
|
bypassRedLine: opts.bypassRedLine === true,
|
|
115
|
-
|
|
123
|
+
// Forward the FLAG verbatim — `undefined` when the user named no
|
|
124
|
+
// mode — so the orchestrator's fallback to the presence-derived
|
|
125
|
+
// mode can actually run. Forwarding the validated `modeName` is
|
|
126
|
+
// what disabled 24h → partial.
|
|
127
|
+
mode: opts.mode === undefined ? undefined : modeName
|
|
116
128
|
});
|
|
117
129
|
const code = result.code;
|
|
118
130
|
const exitOk = result.ok || code === 'AUTO_COMPACT_SKIP' || code === 'AUTO_COMPACT_WAIT';
|
|
@@ -1,3 +1,47 @@
|
|
|
1
1
|
import type { Command } from 'commander';
|
|
2
|
+
import type { AutoCompactMode } from '../../../services/code/auto-compact-modes.js';
|
|
3
|
+
/**
|
|
4
|
+
* Canonicalize a user-supplied `--project <path>` value.
|
|
5
|
+
*
|
|
6
|
+
* Git Bash on Windows hands us forward-slash paths
|
|
7
|
+
* (`C:/Users/.../peaks-loop`) while `peaks workspace init` writes the
|
|
8
|
+
* backslash form, and either side may carry a trailing separator or
|
|
9
|
+
* differing case. Resolving to the real path here means every
|
|
10
|
+
* downstream consumer (`getSessionId`, `setSessionMeta`,
|
|
11
|
+
* `setSkillPresence`) sees one stable form.
|
|
12
|
+
*
|
|
13
|
+
* Returns the input unchanged when it cannot be resolved (path does
|
|
14
|
+
* not exist yet, or is not readable) so a bad `--project` still
|
|
15
|
+
* reaches the existing error handling rather than throwing here.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* The loop-hygiene verdict attached to every ACTIVE `skill.presence` read.
|
|
19
|
+
*
|
|
20
|
+
* Why it lives here instead of only in a SKILL.md body: the zero-pause
|
|
21
|
+
* contract ("auto-compact is the skill's own action, never the user's")
|
|
22
|
+
* was already written into `peaks-code/SKILL.md`, including an explicit
|
|
23
|
+
* ban on the exact phrasing "prompt the user to run `/compact`" — and it
|
|
24
|
+
* was still violated. A SKILL.md body is loaded once and is then
|
|
25
|
+
* compacted away, so the rule stops governing at precisely the moment
|
|
26
|
+
* context pressure makes it matter. `skill.presence` is the one call
|
|
27
|
+
* EVERY skill makes on EVERY turn, in EVERY consumer project and EVERY
|
|
28
|
+
* mode, so a value carried here cannot be forgotten, and no skill has to
|
|
29
|
+
* be edited for the obligation to reach the model.
|
|
30
|
+
*
|
|
31
|
+
* READ-ONLY by contract. It probes and reports; it never compacts.
|
|
32
|
+
* Executing `peaks code auto-compact` stays the skill's own action — a
|
|
33
|
+
* side effect here would fire on every single turn.
|
|
34
|
+
*
|
|
35
|
+
* Failure swallows to `null`: the hygiene verdict is advisory and must
|
|
36
|
+
* never be able to break the presence read itself.
|
|
37
|
+
*/
|
|
38
|
+
export declare function buildContextVerdict(ratio: number, mode: AutoCompactMode): {
|
|
39
|
+
context: {
|
|
40
|
+
ratioPct: string;
|
|
41
|
+
action: string;
|
|
42
|
+
mode: string;
|
|
43
|
+
};
|
|
44
|
+
nextActions: string[];
|
|
45
|
+
};
|
|
2
46
|
import { type ProgramIO } from '../../cli-helpers.js';
|
|
3
47
|
export declare function registerSkillCommand(program: Command, io: ProgramIO): void;
|
|
@@ -8,6 +8,10 @@ import { findProjectRoot } from '../../../services/config/config-safety.js';
|
|
|
8
8
|
import { generateProjectContext } from '../../../services/memory/project-context-service.js';
|
|
9
9
|
import { getSessionId, setSessionMeta } from '../../../services/session/session-manager.js';
|
|
10
10
|
import { resolveCallerProjection } from '../../../services/session/resolve-caller-id.js';
|
|
11
|
+
import { readContextPercent } from '../../../services/context/auto-compact-reader.js';
|
|
12
|
+
import { resolveOuterSessionId } from '../../../services/session/binding-status-service.js';
|
|
13
|
+
import { evaluateCompactTrigger } from '../../../services/code/auto-compact-orchestrator.js';
|
|
14
|
+
import { resolveAutoCompactProfile } from '../../../services/mode/mode-status-service.js';
|
|
11
15
|
import { gcStalePresenceLeases } from '../../../services/skills/presence-lease-service.js';
|
|
12
16
|
import { fail, ok } from 'peaks-loop-shared/result';
|
|
13
17
|
import { stableRealPath } from '../../../shared/path-utils.js';
|
|
@@ -25,6 +29,61 @@ import { stableRealPath } from '../../../shared/path-utils.js';
|
|
|
25
29
|
* not exist yet, or is not readable) so a bad `--project` still
|
|
26
30
|
* reaches the existing error handling rather than throwing here.
|
|
27
31
|
*/
|
|
32
|
+
/**
|
|
33
|
+
* The loop-hygiene verdict attached to every ACTIVE `skill.presence` read.
|
|
34
|
+
*
|
|
35
|
+
* Why it lives here instead of only in a SKILL.md body: the zero-pause
|
|
36
|
+
* contract ("auto-compact is the skill's own action, never the user's")
|
|
37
|
+
* was already written into `peaks-code/SKILL.md`, including an explicit
|
|
38
|
+
* ban on the exact phrasing "prompt the user to run `/compact`" — and it
|
|
39
|
+
* was still violated. A SKILL.md body is loaded once and is then
|
|
40
|
+
* compacted away, so the rule stops governing at precisely the moment
|
|
41
|
+
* context pressure makes it matter. `skill.presence` is the one call
|
|
42
|
+
* EVERY skill makes on EVERY turn, in EVERY consumer project and EVERY
|
|
43
|
+
* mode, so a value carried here cannot be forgotten, and no skill has to
|
|
44
|
+
* be edited for the obligation to reach the model.
|
|
45
|
+
*
|
|
46
|
+
* READ-ONLY by contract. It probes and reports; it never compacts.
|
|
47
|
+
* Executing `peaks code auto-compact` stays the skill's own action — a
|
|
48
|
+
* side effect here would fire on every single turn.
|
|
49
|
+
*
|
|
50
|
+
* Failure swallows to `null`: the hygiene verdict is advisory and must
|
|
51
|
+
* never be able to break the presence read itself.
|
|
52
|
+
*/
|
|
53
|
+
export function buildContextVerdict(ratio, mode) {
|
|
54
|
+
const trigger = evaluateCompactTrigger(ratio, mode);
|
|
55
|
+
const ratioPct = `${(ratio * 100).toFixed(1)}%`;
|
|
56
|
+
// `auto-fire` belongs in the in-zone set: it is the tier where
|
|
57
|
+
// peaks-loop preempts rather than asking the LLM to decide.
|
|
58
|
+
const inZone = trigger.kind === 'auto-fire' || trigger.kind === 'pre-compact' || trigger.kind === 'red-line';
|
|
59
|
+
return {
|
|
60
|
+
context: { ratioPct, action: trigger.kind, mode },
|
|
61
|
+
nextActions: inZone
|
|
62
|
+
? [
|
|
63
|
+
`Context at ${ratioPct} is in the '${trigger.kind}' zone (mode=${mode}).`,
|
|
64
|
+
'Run `peaks code auto-compact --project .` YOURSELF now, then continue. Do NOT ask the user to run /compact — that is the regression the zero-pause contract forbids.'
|
|
65
|
+
]
|
|
66
|
+
: []
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function contextVerdict(projectRoot) {
|
|
70
|
+
try {
|
|
71
|
+
const sessionId = getSessionId(projectRoot);
|
|
72
|
+
if (sessionId === null)
|
|
73
|
+
return { context: null, nextActions: [] };
|
|
74
|
+
const mode = resolveAutoCompactProfile(projectRoot);
|
|
75
|
+
// `outerSessionId` is NOT optional in practice: without it the
|
|
76
|
+
// adapter cannot find the harness transcript and falls back to
|
|
77
|
+
// conservative-zero, reporting a serene "0.0%" that never crosses
|
|
78
|
+
// any threshold. Same resolution order the orchestrator uses.
|
|
79
|
+
const outerSessionId = resolveOuterSessionId(projectRoot, sessionId, process.env);
|
|
80
|
+
const probe = readContextPercent({ projectRoot, sessionId, outerSessionId, env: process.env });
|
|
81
|
+
return buildContextVerdict(probe.ratio, mode);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return { context: null, nextActions: [] };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
28
87
|
function canonicalizeProjectOption(project) {
|
|
29
88
|
if (project === undefined)
|
|
30
89
|
return undefined;
|
|
@@ -174,6 +233,10 @@ export function registerSkillCommand(program, io) {
|
|
|
174
233
|
printResult(io, ok('skill.presence', { active: false }), options.json);
|
|
175
234
|
return;
|
|
176
235
|
}
|
|
236
|
+
// Loop-hygiene verdict: attached to every ACTIVE read, so the
|
|
237
|
+
// zero-pause contract travels with the one call every skill already
|
|
238
|
+
// makes — in every mode and every consumer project.
|
|
239
|
+
const verdict = contextVerdict(projectOption ?? process.cwd());
|
|
177
240
|
if (options.checkStale === true) {
|
|
178
241
|
// Slice 002 (v2.15.0) AC-1: pair the read with a staleness
|
|
179
242
|
// check so callers (peaks-code Step 1, statusline) get both
|
|
@@ -187,11 +250,12 @@ export function registerSkillCommand(program, io) {
|
|
|
187
250
|
stale: staleness.stale,
|
|
188
251
|
staleReason: staleness.reason,
|
|
189
252
|
currentOuterSessionId: staleness.currentOuterSessionId,
|
|
190
|
-
recordedOuterSessionId: staleness.recordedOuterSessionId
|
|
191
|
-
|
|
253
|
+
recordedOuterSessionId: staleness.recordedOuterSessionId,
|
|
254
|
+
...(verdict.context !== null ? { context: verdict.context } : {})
|
|
255
|
+
}, [], verdict.nextActions), options.json);
|
|
192
256
|
return;
|
|
193
257
|
}
|
|
194
|
-
printResult(io, ok('skill.presence', { active: true, ...presence }), options.json);
|
|
258
|
+
printResult(io, ok('skill.presence', { active: true, ...presence, ...(verdict.context !== null ? { context: verdict.context } : {}) }, [], verdict.nextActions), options.json);
|
|
195
259
|
});
|
|
196
260
|
addJsonOption(skill
|
|
197
261
|
.command('presence:set <name>')
|
|
@@ -24,6 +24,7 @@ import { SubAgentNotSupportedError } from '../../services/dispatch/sub-agent-dis
|
|
|
24
24
|
import { emitObservabilityEvent, OBSERVABILITY_SUBAGENT_ROLES } from '../../services/observability/observability-service.js';
|
|
25
25
|
import { noteDispatched, BATCH_LIMIT } from '../../services/dispatch/batch-counter.js';
|
|
26
26
|
import { writeInitialDispatchRecord } from '../../services/dispatch/dispatch-record-writer.js';
|
|
27
|
+
import { provisionDispatchNode } from '../../services/workflow/provision-dispatch-node.js';
|
|
27
28
|
import { evaluatePromptSize } from '../../services/context/context-guard.js';
|
|
28
29
|
import { getCurrentSessionId } from '../../services/skills/skill-presence-service.js';
|
|
29
30
|
import { resolveOuterSessionId } from '../../services/session/binding-status-service.js';
|
|
@@ -69,9 +70,13 @@ export function registerDispatchCommand(parent, io) {
|
|
|
69
70
|
.option('--force', 'G9: override the 80% hard reject threshold at CLI (NOT allowed at hook layer per RL-30 strict)')
|
|
70
71
|
.option('--from-dag <file>', '2.7.0 slice-dag-dispatcher MVP: read a SliceDag JSON file, dispatch one sub-agent per node in topological order; --batch-id overrides the auto-generated batch id (mutually exclusive with <role>)')
|
|
71
72
|
.option('--isolation <mode>', 'slice 2026-07-29-worktree-l2-extended Part 2.C: isolation mode for the sub-agent. Accepts "worktree" (Part 2.C + Part 12 L2 surface), "container" (Part 8 contract + Part 12 L4 docker runtime), or "vm" (Part 25 contract; the VM runtime is a follow-up rid and fail-fasts with ISOLATION_VM_NOT_YET_IMPLEMENTED). Auto-spawns a lease + injects PEAKS_<MODE>_LEASE_ID into the dispatch envelope so the sub-agent can write to the isolated surface without a separate auth grant.')
|
|
72
|
-
// Slice 4.0.8 RD §4
|
|
73
|
-
//
|
|
74
|
-
|
|
73
|
+
// Slice 4.0.8 RD §4 made this a `.requiredOption`. It is optional now:
|
|
74
|
+
// the requirement was enforced but never validated — nothing downstream
|
|
75
|
+
// reads the node, and the record writer's graph transition is
|
|
76
|
+
// best-effort — so its only observable effect was to block dispatch in
|
|
77
|
+
// every project without graph infrastructure. See
|
|
78
|
+
// `provisionDispatchNode`.
|
|
79
|
+
.option('--graph-node <id>', 'graph node id this dispatch binds to (default: a node is provisioned on demand)')
|
|
75
80
|
.option('--workflow-id <id>', 'workflow id the graph node belongs to (defaults to derived from session)')
|
|
76
81
|
.option('--graph-ref <ref>', 'graphRef (defaults to graphs/<workflow-id>.json)')
|
|
77
82
|
// rid-001 detached sub-agent dispatch: 4 new options. Default
|
|
@@ -175,15 +180,9 @@ export function registerDispatchCommand(parent, io) {
|
|
|
175
180
|
process.exitCode = 1;
|
|
176
181
|
return;
|
|
177
182
|
}
|
|
178
|
-
// Slice 4.0.8 RD §4 D4c
|
|
179
|
-
//
|
|
180
|
-
// the
|
|
181
|
-
// enforce it so tests / service callers can't bypass it.
|
|
182
|
-
if (typeof options.graphNode !== 'string' || options.graphNode.length === 0) {
|
|
183
|
-
printResult(io, fail('sub-agent.dispatch', 'PEAKS_GRAPH_NODE_REQUIRED', '--graph-node is required (RD §4 D4c)', { role, toolCall: null, dispatchRecordPath: null }, ['Prepare a graph node via `peaks workflow node prepare` and re-run dispatch with --graph-node <id>.']), asJson);
|
|
184
|
-
process.exitCode = 1;
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
183
|
+
// Slice 4.0.8 RD §4 D4c required `--graph-node` here, before the session
|
|
184
|
+
// id existed, so the only recovery it could offer was prose. The node is
|
|
185
|
+
// now provisioned inside the try block below, once `sid` is known.
|
|
187
186
|
// DOGFOOD ONLY: --prompt-length overrides the actual prompt content with
|
|
188
187
|
// a synthetic prompt of the given size in bytes. The original --prompt
|
|
189
188
|
// is still required (commander needs it). This avoids ARG_MAX limits
|
|
@@ -623,12 +622,43 @@ export function registerDispatchCommand(parent, io) {
|
|
|
623
622
|
warnings.push(`ARTIFACT_PATH_INVALID: ${getErrorMessage(err)}`);
|
|
624
623
|
}
|
|
625
624
|
}
|
|
625
|
+
// Slice 4.0.8 RD §4 D4c: bind this dispatch to a graph node. When the
|
|
626
|
+
// caller names none, provision one — that is what collapses the
|
|
627
|
+
// documented three-step ritual (create graph -> `peaks workflow node
|
|
628
|
+
// prepare` -> dispatch) into a single call. Those three fields were
|
|
629
|
+
// also never passed to the writer, so the record's graph binding was
|
|
630
|
+
// always null and the writer's transition never fired: the feature was
|
|
631
|
+
// inert, and only its precondition (the required flag) was real.
|
|
632
|
+
const graphBinding = (() => {
|
|
633
|
+
if (typeof options.graphNode === 'string' && options.graphNode.length > 0) {
|
|
634
|
+
return {
|
|
635
|
+
nodeId: options.graphNode,
|
|
636
|
+
workflowId: options.workflowId ?? null,
|
|
637
|
+
graphRef: options.graphRef ?? null
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
const provisioned = provisionDispatchNode({
|
|
641
|
+
projectRoot,
|
|
642
|
+
sessionId: sid,
|
|
643
|
+
role,
|
|
644
|
+
workflowId: options.workflowId,
|
|
645
|
+
graphRef: options.graphRef
|
|
646
|
+
});
|
|
647
|
+
return {
|
|
648
|
+
nodeId: provisioned.nodeId,
|
|
649
|
+
workflowId: provisioned.workflowId,
|
|
650
|
+
graphRef: provisioned.graphRef
|
|
651
|
+
};
|
|
652
|
+
})();
|
|
626
653
|
const { path: dispatchRecordPath } = writeInitialDispatchRecord({
|
|
627
654
|
projectRoot,
|
|
628
655
|
sessionId: sid,
|
|
629
656
|
requestId: rid,
|
|
630
657
|
role,
|
|
631
658
|
prompt: effectivePrompt,
|
|
659
|
+
workflowId: graphBinding.workflowId,
|
|
660
|
+
graphNodeId: graphBinding.nodeId,
|
|
661
|
+
graphRef: graphBinding.graphRef,
|
|
632
662
|
toolCall,
|
|
633
663
|
batchId,
|
|
634
664
|
// Slice 2026-07-29-worktree-l2-extended Part 3.A: persist the
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, copyFileSync, mkdirSync } from 'node:fs';
|
|
2
2
|
import { dirname, resolve } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
3
4
|
import { fail, ok } from 'peaks-loop-shared/result';
|
|
4
5
|
import { addJsonOption, printResult, getErrorMessage } from '../cli-helpers.js';
|
|
5
6
|
import { findProjectRoot } from '../../services/config/config-safety.js';
|
|
@@ -7,6 +8,16 @@ import { applyHookInstall, planHookInstall, readHookStatus, readInstalledEntries
|
|
|
7
8
|
import { readJsonObjectFile } from '../../services/ide/shared/atomic-json.js';
|
|
8
9
|
import { detectIdeFromContext } from '../../services/ide/hook-translator.js';
|
|
9
10
|
import { getAdapter } from '../../services/ide/ide-registry.js';
|
|
11
|
+
/**
|
|
12
|
+
* This module's own directory — `<root>/src/cli/commands` in the source tree,
|
|
13
|
+
* `<root>/dist/cli/commands` in a build. Same reason as
|
|
14
|
+
* `claude-settings-template.ts`: `package.json#type` is `module`, so the CJS
|
|
15
|
+
* module-directory global does not exist here (it is a ReferenceError under
|
|
16
|
+
* both tsx and the shipped `dist` build — see the guard in
|
|
17
|
+
* `tests/unit/hooks/gate-enforce-machine-local-shell.test.ts`), and
|
|
18
|
+
* `process.argv[1]` names a different file per entry point.
|
|
19
|
+
*/
|
|
20
|
+
const MODULE_DIR = dirname(fileURLToPath(import.meta.url));
|
|
10
21
|
function resolveScope(options) {
|
|
11
22
|
return options.global ? 'global' : 'project';
|
|
12
23
|
}
|
|
@@ -97,12 +108,15 @@ function readOnDiskDenyEntries(settings) {
|
|
|
97
108
|
return [];
|
|
98
109
|
return deny.filter((d) => typeof d === 'string');
|
|
99
110
|
}
|
|
100
|
-
function copyBridgeHookIfPresent(userHome) {
|
|
101
|
-
const source = resolve(
|
|
111
|
+
function copyBridgeHookIfPresent(userHome, dryRun = false) {
|
|
112
|
+
const source = resolve(MODULE_DIR, '..', '..', 'services', 'hooks', 'pre-tool-superpowers-bridge.sh');
|
|
102
113
|
const target = resolve(userHome, '.claude', 'skills', 'peaks-code', 'hooks', 'pre-tool-superpowers-bridge.sh');
|
|
103
114
|
if (!existsSync(source)) {
|
|
104
115
|
return { copied: false, source, target };
|
|
105
116
|
}
|
|
117
|
+
if (dryRun) {
|
|
118
|
+
return { copied: true, source, target };
|
|
119
|
+
}
|
|
106
120
|
mkdirSync(dirname(target), { recursive: true });
|
|
107
121
|
copyFileSync(source, target);
|
|
108
122
|
return { copied: true, source, target };
|
|
@@ -117,12 +131,15 @@ function copyBridgeHookIfPresent(userHome) {
|
|
|
117
131
|
* runtime entry; the shell script is the canonical artifact distributed
|
|
118
132
|
* alongside the bridge hook.
|
|
119
133
|
*/
|
|
120
|
-
function copyCodeGateHookIfPresent(userHome) {
|
|
121
|
-
const source = resolve(
|
|
134
|
+
function copyCodeGateHookIfPresent(userHome, dryRun = false) {
|
|
135
|
+
const source = resolve(MODULE_DIR, '..', '..', 'services', 'hooks', 'pre-tool-code-gate.sh');
|
|
122
136
|
const target = resolve(userHome, '.claude', 'skills', 'peaks-code', 'hooks', 'pre-tool-code-gate.sh');
|
|
123
137
|
if (!existsSync(source)) {
|
|
124
138
|
return { copied: false, source, target };
|
|
125
139
|
}
|
|
140
|
+
if (dryRun) {
|
|
141
|
+
return { copied: true, source, target };
|
|
142
|
+
}
|
|
126
143
|
mkdirSync(dirname(target), { recursive: true });
|
|
127
144
|
copyFileSync(source, target);
|
|
128
145
|
return { copied: true, source, target };
|
|
@@ -147,8 +164,10 @@ export function registerHooksCommands(program, io) {
|
|
|
147
164
|
if (options.dryRun === true) {
|
|
148
165
|
const plan = planHookInstall(scope, projectRoot, { ide, skipProgress });
|
|
149
166
|
const dryRunEntries = listExpectedEntriesForIde(ide, skipProgress);
|
|
167
|
+
// `dryRun: true` — a --dry-run must not write anything, and these
|
|
168
|
+
// helpers' target is the user's home directory.
|
|
150
169
|
const bridgeCopy = scope === 'global'
|
|
151
|
-
? copyBridgeHookIfPresent(process.env.USERPROFILE ?? process.env.HOME ?? '')
|
|
170
|
+
? copyBridgeHookIfPresent(process.env.USERPROFILE ?? process.env.HOME ?? '', true)
|
|
152
171
|
: { copied: false, source: '', target: '' };
|
|
153
172
|
// Slice 2026-08-06-codegate-vendor-neutral: also copy the
|
|
154
173
|
// code-gate hook script. The runtime gate lives at
|
|
@@ -156,7 +175,7 @@ export function registerHooksCommands(program, io) {
|
|
|
156
175
|
// in the install output); the script is the build artifact
|
|
157
176
|
// distributed alongside the bridge hook.
|
|
158
177
|
const codeGateCopy = scope === 'global'
|
|
159
|
-
? copyCodeGateHookIfPresent(process.env.USERPROFILE ?? process.env.HOME ?? '')
|
|
178
|
+
? copyCodeGateHookIfPresent(process.env.USERPROFILE ?? process.env.HOME ?? '', true)
|
|
160
179
|
: { copied: false, source: '', target: '' };
|
|
161
180
|
printResult(io, ok('hooks.install', {
|
|
162
181
|
...plan,
|
|
@@ -176,6 +195,12 @@ export function registerHooksCommands(program, io) {
|
|
|
176
195
|
permissionsDenyEntries: listSuperpowersDenyEntries()
|
|
177
196
|
}, [], [
|
|
178
197
|
`would install ${dryRunEntries.length} peaks-managed hook entries`,
|
|
198
|
+
// Name the target file of every entry: the gate-enforce entry is
|
|
199
|
+
// routed to the machine-local settings file (see
|
|
200
|
+
// `resolveHookTargets`), so a summary that only listed
|
|
201
|
+
// `settingsPath` + the entry names read as if it landed in the
|
|
202
|
+
// committed, shared file.
|
|
203
|
+
...plan.entryTargets.map((entry) => `would write ${entry.matcher || '(no matcher)'} → ${entry.sentinel} to ${entry.settingsPath}`),
|
|
179
204
|
`would write ${listSuperpowersDenyEntries().length} permissions.deny entries (Layer 3 worktree governance)`,
|
|
180
205
|
bridgeCopy.copied
|
|
181
206
|
? `would copy bridge hook from ${bridgeCopy.source} to ${bridgeCopy.target}`
|
|
@@ -32,7 +32,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync, appendFileSync } fr
|
|
|
32
32
|
import { dirname, join } from 'node:path';
|
|
33
33
|
import { getSessionIdCanonical } from '../session/session-manager.js';
|
|
34
34
|
import { resolveOuterSessionId } from '../session/binding-status-service.js';
|
|
35
|
-
import { AUTO_COMPACT_PRE_COMPACT_RATIO
|
|
35
|
+
import { AUTO_COMPACT_PRE_COMPACT_RATIO } from '../context/auto-compact-types.js';
|
|
36
36
|
import { describeMode, thresholdFor } from './auto-compact-modes.js';
|
|
37
37
|
import { resolveAutoCompactProfile } from '../mode/mode-status-service.js';
|
|
38
38
|
import { CompactLifecyclePublisher, newCompactRunId, settleOpenLifecycleRun, summarizeLifecycleError } from './auto-compact-lifecycle.js';
|
|
@@ -352,7 +352,7 @@ export async function runAutoCompact(input) {
|
|
|
352
352
|
? decision.trigger.message
|
|
353
353
|
: decision.reason === 'in-flight-batch'
|
|
354
354
|
? `In-flight batch detected; deferring pre-compact (ratio=${(probe.ratio * 100).toFixed(1)}%); next probe will re-evaluate.`
|
|
355
|
-
: `Context at ${(probe.ratio * 100).toFixed(1)}%; below ${(
|
|
355
|
+
: `Context at ${(probe.ratio * 100).toFixed(1)}%; below the ${(thresholdFor(mode, 'autoFire') * 100).toFixed(0)}% auto-fire threshold (mode=${mode}).`,
|
|
356
356
|
data: {
|
|
357
357
|
sessionId,
|
|
358
358
|
ratio: probe.ratio,
|
|
@@ -165,6 +165,8 @@ async function dispatchIdeNativeHook(input) {
|
|
|
165
165
|
pathway: 'ide-native',
|
|
166
166
|
message: result.action === 'installed'
|
|
167
167
|
? `Auto-compact PreToolUse hook installed at ${result.settingsPath}. Next Bash/Task tool call will read CLAUDE_CONTEXT_USAGE_PERCENT and compact in-band at ratio ≥ 95%.`
|
|
168
|
-
:
|
|
168
|
+
: result.action === 'updated'
|
|
169
|
+
? `Auto-compact PreToolUse hook REPAIRED at ${result.settingsPath} — the installed entry carried a stale command and was rewritten.`
|
|
170
|
+
: `Auto-compact PreToolUse hook already installed at ${result.settingsPath}; next Bash/Task tool call will trigger compact in-band at ratio ≥ 95%.`
|
|
169
171
|
};
|
|
170
172
|
}
|
|
@@ -150,6 +150,40 @@ export declare const LIFECYCLE_RULES = "## Sub-agent lifecycle rules (locked 202
|
|
|
150
150
|
* the ones the orchestrator must have to decide the next gate.
|
|
151
151
|
*/
|
|
152
152
|
export declare const REPORT_CAP_BLOCK = "## Final report cap (mandatory)\n\nYour FINAL report to the parent MUST be \u2264 40 lines and \u2264 2 KB. Write any longer detail into the artifact file you already own \u2014 the parent can `Read` that file for the full detail, so nothing is lost. The report itself MUST still carry: changed files (one line each), the exact commands you ran, pass/fail counts, tsc status, and any blocker. Do NOT paste file contents, full tool output, or logs into the report.\n";
|
|
153
|
+
/**
|
|
154
|
+
* Slice 2026-09-10-fact-force-gate-adaptation: stand IN FRONT of an external
|
|
155
|
+
* `PreToolUse` gate instead of explaining its denial after the fact.
|
|
156
|
+
*
|
|
157
|
+
* ECC (a third-party plugin under `~/.claude/plugins/`) registers
|
|
158
|
+
* `gateguard-fact-force.js` on `Edit|Write|MultiEdit`. It denies the first
|
|
159
|
+
* edit of a file whose facts the agent has not established, and its four-item
|
|
160
|
+
* message never says two things the agent needs: that the file must be READ
|
|
161
|
+
* first, and that the edit was NOT applied. Measured failure mode
|
|
162
|
+
* (session 2026-09-10-session-528a63): the sub-agent reads the denial as
|
|
163
|
+
* "the tool is broken" and abandons the edit.
|
|
164
|
+
*
|
|
165
|
+
* So the block below leads with the STANDING RULE (read before you edit) and
|
|
166
|
+
* names the paths it covers, and mentions the denial only as the consequence
|
|
167
|
+
* of skipping that rule. A sub-agent that has never seen the gate can read
|
|
168
|
+
* this once and never trip it.
|
|
169
|
+
*
|
|
170
|
+
* The gate itself is untouched: peaks-loop adapts to it, and does NOT disable,
|
|
171
|
+
* bypass, or re-implement it. Nor is `ECC_GATEGUARD=off` part of this.
|
|
172
|
+
*
|
|
173
|
+
* Always rendered — no opt-out flag and no role split. Every role edits files
|
|
174
|
+
* outside `.peaks/**`, and only sub-agent #1 of a session sees a denial (the
|
|
175
|
+
* gate fires once per file), so a role-scoped or opt-in block would leave the
|
|
176
|
+
* rest of the fleet untold. It joins the stable boilerplate prefix: constant
|
|
177
|
+
* bytes for every dispatch, prompt-cache friendly.
|
|
178
|
+
*
|
|
179
|
+
* The `.peaks/**` exemption is real and pre-dates this slice — peaks-loop
|
|
180
|
+
* materialises `.claude/settings.local.json` so the gate skips `.peaks/**`
|
|
181
|
+
* (slice 2.0.1-bug3-fact-forcing-bypass; see
|
|
182
|
+
* `src/cli/commands/workspace/init-command.ts`).
|
|
183
|
+
*/
|
|
184
|
+
export declare const FACT_FORCE_GATE_BLOCK = "## Read before you edit (Fact-Forcing Gate)\n\nRead a file BEFORE your first `Edit` / `Write` / `MultiEdit` on it \u2014 the normal way to work here, not an optional step. It applies to every path OUTSIDE `.peaks/**` (source, tests, docs, config); `.peaks/**` writes are exempt.\n\nSkipping that read trips a `PreToolUse` plugin gate (ECC's \"Fact-Forcing Gate\"), which denies the edit. A denial is NOT a failure and the tool is NOT broken \u2014 your edit was NOT applied. Read the file, state the four facts the gate asks for (importers, affected API, data schemas if any, the user's verbatim instruction), then retry the same operation. Do not switch tools, do not give up, do not re-attempt blindly.\n";
|
|
185
|
+
/** Always-on renderer for {@link FACT_FORCE_GATE_BLOCK}. */
|
|
186
|
+
export declare function renderFactForceGateBlock(): string;
|
|
153
187
|
/**
|
|
154
188
|
* Compose the system-prompt body for a sub-agent dispatch.
|
|
155
189
|
*
|
|
@@ -161,7 +195,7 @@ export declare const REPORT_CAP_BLOCK = "## Final report cap (mandatory)\n\nYour
|
|
|
161
195
|
* Byte-identical degradation contract (slice 2026-07-22-orchestrator-memory-preflight
|
|
162
196
|
* controller brief): when the memory block is unavailable, the composed body is
|
|
163
197
|
* exactly `formatTestToolDetection() + "\n\n" + L1 + "\n" + LIFECYCLE +
|
|
164
|
-
* "\n" + REPORT_CAP + "\n" + contextBlock + taskBody`, so the unavailable
|
|
198
|
+
* "\n" + REPORT_CAP + "\n" + FACT_FORCE_GATE + "\n" + contextBlock + taskBody`, so the unavailable
|
|
165
199
|
* branch MUST return `taskBody` unwrapped (NOT a `# title\n\n` wrap).
|
|
166
200
|
* (REPORT_CAP joined the stable prefix in slice
|
|
167
201
|
* 2026-09-10-context-audit-and-discipline, Slice C.)
|