peaks-loop 4.0.44 → 4.0.46
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 +59 -0
- package/README-en.md +1 -1
- package/README.md +1 -1
- package/dist/cli/commands/code-runtime-commands.js +30 -19
- package/dist/cli/commands/codegraph-commands.d.ts +1 -0
- package/dist/cli/commands/codegraph-commands.js +49 -3
- package/dist/cli/commands/final-review-commands.js +3 -1
- package/dist/services/code/auto-compact-lifecycle.d.ts +39 -3
- package/dist/services/code/auto-compact-lifecycle.js +41 -6
- package/dist/services/code/auto-compact-orchestrator.js +20 -11
- 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/final-review/final-review-service.d.ts +224 -44
- package/dist/services/final-review/final-review-service.js +934 -97
- package/dist/services/final-review/index.d.ts +2 -1
- package/dist/services/final-review/index.js +2 -1
- package/dist/services/final-review/pre-post-diff.d.ts +137 -0
- package/dist/services/final-review/pre-post-diff.js +657 -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/package.json +5 -5
- package/skills/peaks-code/SKILL.md +2 -2
- package/skills/peaks-final-review/SKILL.md +51 -18
- package/skills/peaks-final-review/references/4-dimensions.md +42 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,64 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.0.46 — 2026-09-12 (85% 该压不压 + 把"派发"记成了"正在压缩")
|
|
4
|
+
|
|
5
|
+
**Highlights**:
|
|
6
|
+
|
|
7
|
+
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**。
|
|
8
|
+
|
|
9
|
+
后果直接:一个**照着"唯一真相源"办事**的 LLM,在 0.85–0.95 里**永远不会压缩** —— 直接违反 zero-pause 契约。现在单 rid 与 job 模式一致:该带一律 `auto-compact-now`,并把过时的 notice/help 改成如实描述代码行为。
|
|
10
|
+
|
|
11
|
+
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`。
|
|
12
|
+
|
|
13
|
+
**比显示错更严重的**:结算逻辑 `settleOpenLifecycleRun` 只在 **ratio 跌破阈值**时结算 —— 而那条 pathway 根本不会造成下跌,**所以 `afterRatio` 永远写不上,"压缩后 ratio 确实降了"这条验证链是死的**,而它正是 zero-pause 契约唯一的验收依据。
|
|
14
|
+
|
|
15
|
+
两半一起修,方式是**让记录说实话**:新增**静止态 `armed`**(含义是"触发器已设好,等 95% 的 in-band 时机"),并被**类型系统**排除在 `ACTIVE_STAGES` 之外 —— 只有承诺过心跳的 stage(`queued`/`preparing`/`compacting`/`verifying`)才可能 stall。状态栏据此渲染成 `◔ armed · 88% · fires at 95%`,**不画进度条**。而**真的卡住的 `compacting` 仍然正确报 `stalled`** —— 这条能力是实测确认保住的,不是假设。
|
|
16
|
+
|
|
17
|
+
**验证**:三个版本常量一致(**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。
|
|
18
|
+
|
|
19
|
+
**本轮顺带发现、尚未处理(不属本版修复)**:`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 个失败的分诊。
|
|
20
|
+
|
|
21
|
+
## 4.0.45 — 2026-09-12 (一个永远无法通过的维度 + 一个验代理的守卫)
|
|
22
|
+
|
|
23
|
+
**Highlights**:
|
|
24
|
+
|
|
25
|
+
1. **4 维验收闸里有一维从构造上就不可能通过。** `existing-functionality-intact` 的契约要求一份 `pre-post-diff`(前后结构对比:测试面 / 公开 API 面),而**全仓没有任何东西生产它**。于是它被喂的是 `rd/tech-doc.md`(设计意图)与 `prd/handoff.md`(批准范围)—— 而这个模块自己的文件头注释,把"拿设计意图文档当回归评估"列为它存在的理由。结果:`allPass === true` 对**任何**工作流都不可达。
|
|
26
|
+
|
|
27
|
+
本版补上生产者(`pre-post-diff.ts`):以 base ref 对比工作树,报**测试面**(文件数 / 用例数)、**源文件清单**、**公开 API 面**(顶层 `export` 语句数与名字集合),写入 `.peaks/_runtime/<sessionId>/final-review/api-diff.txt`。**诚实边界写在产物里**:export 检测是行锚定的近似,能发现"少了一个导出"这类结构丢失,**发现不了签名变更**。算不出 baseline 时**不产出 artifact、不编造 diff** —— 该维保持 `inconclusive`。
|
|
28
|
+
|
|
29
|
+
2. **消费它的那个闸,键在"送达",不在"产物存在"。** 独立复核连做七轮,每轮都在下一层找到同一个形状 —— 一个接受**代理**的闸("评审者看到了吗"被替换成"磁盘上有吗"):
|
|
30
|
+
|
|
31
|
+
| 轮 | 被判为"检查过了"的东西 |
|
|
32
|
+
|---|---|
|
|
33
|
+
| 1 | `status === 'computed'`(磁盘上有文件) |
|
|
34
|
+
| 2 | `status === 'found'`(≥1 字节送达) |
|
|
35
|
+
| 3 | `totalBytes === 0`(把 missing 与 empty 混为一谈) |
|
|
36
|
+
|
|
37
|
+
共同根因是:一个源**可以被部分内联**,于是"送达了吗"永远有一个律师式答案。现在分配器是 **per-source 全有或全无**(要么整份、要么 omitted,不存在截在中间),且 `isDelivered` 是这个问题**唯一的家** —— 每个源声明自己的送达规则:`whole`(整份文档;被截断过的文档是另一份文档)或 `conclusion`(一个**就是结论本身**的字面量)。
|
|
38
|
+
|
|
39
|
+
3. **随之而来的两处实质变化,都要求诚实。**
|
|
40
|
+
- 证据预算改为**推导**(`10,240 = 总额 / 4`)而不是常量 —— 因为走 `whole` 规则的源一旦超过它,就**永远送不到**。
|
|
41
|
+
- 一个维度若其全部支撑源都装不下,现在会被**响亮报出**(prompt 里出 `## Evidence delivery reachability` 段、维度 summary 挂 `[delivery-reachability: …]` 标记、并进 `needsAttention`),而不是**静默地永远红**。**故意不抛错** —— 抛错会连带毁掉那三个本来可送达的维度。
|
|
42
|
+
|
|
43
|
+
4. **反回归守卫本身,就是同一个形状的第 7 个实例 —— 且它一开始是坏的。** 初版守卫按**语法形状**切分源码(只认行首 `function name`),于是**箭头函数、生成器、`export default function`、类方法**一律不被扫描:**8 种等价写法全部放行**,端到端实测"顶层箭头里放 2 个代理字面量"仍 `3 passed EXIT=0`。**一个能放行 8/8 的守卫比没有守卫更糟** —— 它把"没人在看"变成"有东西在看"。
|
|
44
|
+
|
|
45
|
+
现在它用仓库自带的 `typescript` 解析 AST、读**函数体内**的 token,于是那 4 种形状绕过全部关闭(实测:往真实源码注入箭头式代理 → 守卫转红)。剩下 4 种**不含该字面量**的改写(拆变量 `const F='found'`、`==`、`item['status']`、字符串拼接)**仍然放行** —— 这一点被**固定成一条通过的测试**,让守卫**陈述自己的边界**而不是暗示一个。
|
|
46
|
+
|
|
47
|
+
5. **`peaks codegraph status` 在真有缺口时会同时印出两行互相矛盾的话。** 退出码(74)与 `[FAIL]` 细节都是对的,但人第一眼看到的是:
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
[OK] Index is up to date
|
|
51
|
+
[FAIL] codegraph index is incomplete: 2 of 1133 tracked source files are excluded …
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
两个闸在回答不同的问题:上游答"图与上次扫描一致"(确实一致),peaks 答"图覆盖了仓库吗"(没有)。**都对,并排印出来就是矛盾。** 现在上游那行被**归因而非抑制**(措辞与统计保留,只是把它的适用范围说明白),只在这一行、且只在缺口门**确实触发**时改动;无缺口路径逐字节不变(含 ANSI),机器可读 envelope 未动。
|
|
55
|
+
|
|
56
|
+
**验证**:三个版本常量一致(**4.0.45**);`tsc -p tsconfig.build.json` exit 0;宽 `tsconfig.json` 保持 **142** 基线;`tests/unit` **214 files / 2165 passed / 3 skipped / 0 failed**;`pnpm build` 的 `build-integrity` OK。
|
|
57
|
+
|
|
58
|
+
**已知未修 / 边界**:`conclusion` 规则下超大的源不算"不可送达"(结论标记可能在截断点之后);单一可送达源的**穿越前余量**(本 session 实测 748 B)**没有**告警,只报"已经不可能"的状态;4 维中的 `existing-functionality-intact` 在**非 git 项目**里永久 `inconclusive`(这是诚实结果,已写进 SKILL.md 与 `4-dimensions.md`)。
|
|
59
|
+
|
|
60
|
+
**备注**:本版的 `prepare-final-review` 闸**未经过独立复核**(前六轮每一轮都经过并各找到 1 个 HIGH;第 7 轮由 orchestrator 自行验证,含箭头注入实测),这一点如实记录以便日后回溯。
|
|
61
|
+
|
|
3
62
|
## 4.0.44 — 2026-09-12 (被静默排除的源文件 + 一个验错东西的验收闸)
|
|
4
63
|
|
|
5
64
|
**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.46 (2026-09-12) |
|
|
144
144
|
| **Domains** | Code (`peaks-code`) · Content (`peaks-content`) · Project health (`peaks-doctor`) · Issue sweep (`peaks-issue-fix-orchestrator`) · Custom SOP (`peaks-sop`) · Cross-domain primitives (`peaks-solo` dispatcher · `peaks-resume` · `peaks-status` · `peaks-test` · `peaks-slice-decompose`) |
|
|
145
145
|
| **Sediment pool** | `~/.peaks/` local pool · twice-clean runs auto-promote to a bee · broken runs come back for you to redefine · the bee grows with your taste |
|
|
146
146
|
| **Test suite** | 285+ cases · 4 packages (peaks-loop / peaks-loop-mut / peaks-loop-shared-channel / peaks-loop-shared) · **0 timeouts** · 14 BDD caller-binding edge cases |
|
package/README.md
CHANGED
|
@@ -140,7 +140,7 @@ npm i -g peaks-loop
|
|
|
140
140
|
|
|
141
141
|
| | |
|
|
142
142
|
| --- | --- |
|
|
143
|
-
| **最新版本** | [](https://www.npmjs.com/package/peaks-loop) — 4.0.
|
|
143
|
+
| **最新版本** | [](https://www.npmjs.com/package/peaks-loop) — 4.0.46(2026-09-12) |
|
|
144
144
|
| **覆盖域** | 代码(`peaks-code`) · 内容(`peaks-content`) · 项目健康(`peaks-doctor`) · 批量修 issue(`peaks-issue-fix-orchestrator`) · 自定义 SOP(`peaks-sop`) · 通用原语(`peaks-solo` 分诊 / `peaks-resume` 续 / `peaks-status` 看 / `peaks-test` 测 / `peaks-slice-decompose` 切片) |
|
|
145
145
|
| **沉淀池** | `~/.peaks/` 本地池 · 跑两次自动晋升成 bee · 跑翻车让你重定义 · bee 跟着你的口味长 |
|
|
146
146
|
| **测试套件** | 1096 cases · 4 packages (peaks-loop 1015 / runtime 39 / mut 22 / shared-channel 20) · **CI 首次全绿**(ubuntu + windows) · 14 BDD caller-binding coverage |
|
|
@@ -155,16 +155,18 @@ export function registerCodeRuntimeCommands(code, io) {
|
|
|
155
155
|
'Adapter-driven (no hard-coded IDE names): Claude Code is the MVP ' +
|
|
156
156
|
'implementation; trae / codex / cursor / qoder / tongyi-lingma / ' +
|
|
157
157
|
'hermes / openclaw register their own env-var via IdeAdapter.compact. ' +
|
|
158
|
-
'v3.1.2
|
|
159
|
-
'
|
|
158
|
+
'v3.1.2 / 2026-09-12: ≥0.85 emits action=auto-compact-now ' +
|
|
159
|
+
'(MANDATORY in every mode — single-rid included) and ' +
|
|
160
160
|
'≥0.95 emits action=red-line (forced hook fires next turn). ' +
|
|
161
|
+
'--enforce-job-mode only changes the reported `jobMode` label; ' +
|
|
162
|
+
'the thresholds are identical. ' +
|
|
161
163
|
'Context-window override: set env PEAKS_CONTEXT_WINDOW_TOKENS=<positive int> ' +
|
|
162
164
|
'or `peaks config set --key context.windowTokens --value <positive int>`; ' +
|
|
163
165
|
'the JSON envelope reports the winning layer as capacitySource ' +
|
|
164
166
|
'(env-override | config | model-heuristic | default).')
|
|
165
167
|
.requiredOption('--project <path>', 'target project root')
|
|
166
168
|
.option('--session-id <sid>', 'override session id (default: read from active presence)')
|
|
167
|
-
.option('--enforce-job-mode', 'v3.1.2:
|
|
169
|
+
.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
170
|
.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
171
|
try {
|
|
170
172
|
const { readContextPercent } = await import('../../services/context/auto-compact-reader.js');
|
|
@@ -210,17 +212,21 @@ export function registerCodeRuntimeCommands(code, io) {
|
|
|
210
212
|
let action = 'ok';
|
|
211
213
|
let next = null;
|
|
212
214
|
if (probe.ratio >= 0.95) {
|
|
213
|
-
action =
|
|
215
|
+
action = 'red-line';
|
|
214
216
|
next = 'peaks code auto-compact';
|
|
215
217
|
}
|
|
216
218
|
else if (probe.ratio >= 0.85) {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
219
|
+
// Slice 2026-09-12-compact-band-policy (defect A): the
|
|
220
|
+
// 0.85–0.95 band is MANDATORY auto-compact for single-rid
|
|
221
|
+
// sessions too. It used to be downgraded to `soft-warn` here,
|
|
222
|
+
// which contradicted this command's own help text, the
|
|
223
|
+
// `pre-compact` action from `peaks skill presence`, the
|
|
224
|
+
// peaks-code SKILL.md, and the 2026-07-27 user-calibrated
|
|
225
|
+
// threshold policy — and left an LLM that follows the "single
|
|
226
|
+
// source of truth" (`context-now`) never compacting in the
|
|
227
|
+
// zone, violating the zero-pause contract.
|
|
228
|
+
action = 'auto-compact-now';
|
|
229
|
+
next = 'peaks code auto-compact';
|
|
224
230
|
}
|
|
225
231
|
else if (probe.ratio >= 0.5) {
|
|
226
232
|
action = 'soft-warn';
|
|
@@ -229,9 +235,16 @@ export function registerCodeRuntimeCommands(code, io) {
|
|
|
229
235
|
: action === 'auto-compact-now' ? 'pre-compact'
|
|
230
236
|
: action === 'soft-warn' ? 'soft-warn'
|
|
231
237
|
: 'ok';
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
238
|
+
// Slice 2026-09-12-compact-band-policy: the two modes no longer
|
|
239
|
+
// differ in behaviour ABOVE 0.50 — both auto-fire at ≥0.85 and
|
|
240
|
+
// both red-line at ≥0.95 — so the old "advisory mode
|
|
241
|
+
// (single-rid)" notice was a lie the moment the downgrade was
|
|
242
|
+
// removed. Job mode's remaining difference is that its threshold
|
|
243
|
+
// policy is recorded up front (`job-shape.json`), which the
|
|
244
|
+
// `jobMode` field already reports. The notice now says only that.
|
|
245
|
+
const gateModeNotice = isJobMode
|
|
246
|
+
? 'Job mode (job-shape.json isJob=true): the same ≥0.85 / ≥0.95 thresholds apply, and the decision is recorded in job-shape.json.'
|
|
247
|
+
: 'Single-rid mode: the same ≥0.85 / ≥0.95 thresholds apply — ≥0.85 is MANDATORY auto-compact, not advisory.';
|
|
235
248
|
printResult(io, ok('code.context-now', {
|
|
236
249
|
ratio: probe.ratio,
|
|
237
250
|
ratioPct: `${ratioPct}%`,
|
|
@@ -255,13 +268,11 @@ export function registerCodeRuntimeCommands(code, io) {
|
|
|
255
268
|
action === 'red-line'
|
|
256
269
|
? `RED LINE: ≥ 95%. Next: \`${next}\` (PreToolUse hook fires next turn).`
|
|
257
270
|
: action === 'auto-compact-now'
|
|
258
|
-
? `
|
|
271
|
+
? `MANDATORY auto-compact (≥85%, every mode). Code MUST call \`${next}\` WITHOUT confirmation.`
|
|
259
272
|
: 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.`
|
|
273
|
+
? `Soft warn (50–85%). Continue working; the next \`peaks code auto-compact\` will re-check.`
|
|
263
274
|
: `Below 50%. No action required.`,
|
|
264
|
-
|
|
275
|
+
gateModeNotice
|
|
265
276
|
]), true);
|
|
266
277
|
}
|
|
267
278
|
catch (err) {
|
|
@@ -10,4 +10,5 @@ import { type ProgramIO } from '../cli-helpers.js';
|
|
|
10
10
|
* untouched.
|
|
11
11
|
*/
|
|
12
12
|
export declare function rewriteBareCodegraphHints(text: string): string;
|
|
13
|
+
export declare function attributeUpstreamUpToDateLine(stdout: string): string;
|
|
13
14
|
export declare function registerCodegraphCommands(program: Command, io: ProgramIO): void;
|
|
@@ -38,7 +38,48 @@ function printCodegraphFailure(io, command, error, asJson, exitCode = 1) {
|
|
|
38
38
|
export function rewriteBareCodegraphHints(text) {
|
|
39
39
|
return text.replace(/(?<![\w-])(?<!peaks\s)codegraph(?=\s+(?:status|init|index|query|files|context|affected)\b)/g, 'peaks codegraph');
|
|
40
40
|
}
|
|
41
|
-
|
|
41
|
+
const ANSI_SGR_PATTERN = /\x1b\[[0-9;]*m/g;
|
|
42
|
+
/**
|
|
43
|
+
* Upstream `status` answers a different question than peaks-loop's
|
|
44
|
+
* integrity gate: upstream says "the on-disk graph matches the last scan"
|
|
45
|
+
* (true), peaks says "that graph covers the repository" (false when rules
|
|
46
|
+
* exclude tracked files). Both verdicts are correct, but an unqualified
|
|
47
|
+
* `[OK] Index is up to date` printed above our `[FAIL] ...` reads as
|
|
48
|
+
* "nothing to see here" — and the OK is the line the eye lands on first.
|
|
49
|
+
* The exit code and the JSON envelope are already right; only this line
|
|
50
|
+
* lies by juxtaposition.
|
|
51
|
+
*
|
|
52
|
+
* So: keep upstream's wording — the line stays recognizable, and the
|
|
53
|
+
* Files/Nodes counts around it are untouched — but drop the bare OK
|
|
54
|
+
* marker and name the only question it answers. Clean runs never reach
|
|
55
|
+
* this, so their output stays byte-identical.
|
|
56
|
+
*
|
|
57
|
+
* The match is anchored to THAT line. An earlier version keyed on
|
|
58
|
+
* `includes('up to date')`, which is content-blind: upstream prints other
|
|
59
|
+
* `[OK] ... are up to date` lines (a language-server or watcher line is the
|
|
60
|
+
* observed one), and each of those was rewritten into a claim about the
|
|
61
|
+
* INDEX — a misattribution introduced by a change whose entire purpose was
|
|
62
|
+
* to stop misleading output. Anything that is not the index line is passed
|
|
63
|
+
* through byte-for-byte, tail note and all.
|
|
64
|
+
*/
|
|
65
|
+
const INDEX_UP_TO_DATE_RE = /^\[OK\]\s+Index is up to date\b/i;
|
|
66
|
+
export function attributeUpstreamUpToDateLine(stdout) {
|
|
67
|
+
return stdout
|
|
68
|
+
.split('\n')
|
|
69
|
+
.map((line) => {
|
|
70
|
+
const visible = line.replace(ANSI_SGR_PATTERN, '').trim();
|
|
71
|
+
if (!INDEX_UP_TO_DATE_RE.test(visible)) {
|
|
72
|
+
return line;
|
|
73
|
+
}
|
|
74
|
+
// Only the OK marker is downgraded and the attribution appended: the
|
|
75
|
+
// rest of the line — including whatever upstream wrote after it — is
|
|
76
|
+
// preserved, so nothing upstream actually said is replaced.
|
|
77
|
+
const withoutOk = visible.replace(/^\[OK\]\s*/, '');
|
|
78
|
+
return `[i] ${withoutOk} (upstream: matches the last scan only; repository coverage is answered below)`;
|
|
79
|
+
})
|
|
80
|
+
.join('\n');
|
|
81
|
+
}
|
|
82
|
+
async function runCodegraphCommand(io, command, options, asJson, attributeStdout) {
|
|
42
83
|
try {
|
|
43
84
|
const invocation = createCodegraphInvocation(options);
|
|
44
85
|
const result = await executeCodegraphInvocation(invocation);
|
|
@@ -47,7 +88,8 @@ async function runCodegraphCommand(io, command, options, asJson) {
|
|
|
47
88
|
return;
|
|
48
89
|
}
|
|
49
90
|
const didFail = result.exitCode !== null && result.exitCode !== 0;
|
|
50
|
-
const
|
|
91
|
+
const rewritten = rewriteBareCodegraphHints(result.stdout);
|
|
92
|
+
const stdout = attributeStdout === undefined ? rewritten : attributeStdout(rewritten);
|
|
51
93
|
const stderr = rewriteBareCodegraphHints(result.stderr);
|
|
52
94
|
if (stdout.length > 0) {
|
|
53
95
|
io.stdout((didFail ? redactSensitiveErrorMessage(stdout) : stdout).trimEnd());
|
|
@@ -132,7 +174,11 @@ async function runCodegraphStatusCommand(io, options, asJson) {
|
|
|
132
174
|
await runCodegraphStatusJson(io, options, integrity, integrityWarning);
|
|
133
175
|
}
|
|
134
176
|
else {
|
|
135
|
-
|
|
177
|
+
// Only when the gate found a gap: upstream's `[OK] Index is up to
|
|
178
|
+
// date` answers "consistent with the last scan", and printing it
|
|
179
|
+
// unqualified right above our `[FAIL]` tells the reader two opposite
|
|
180
|
+
// things at once. Clean runs get no transform and stay byte-identical.
|
|
181
|
+
await runCodegraphCommand(io, 'codegraph.status', { subcommand: 'status', project: options.project }, false, integrity?.gap === true ? attributeUpstreamUpToDateLine : undefined);
|
|
136
182
|
if (integrityWarning !== null) {
|
|
137
183
|
io.stdout(`[WARN] codegraph exclude integrity not evaluated: ${integrityWarning}`);
|
|
138
184
|
}
|
|
@@ -113,7 +113,8 @@ export function registerFinalReviewCommands(program, io) {
|
|
|
113
113
|
.description('Prepare the 4-dimension business review (final-review primitive) for human acceptance (W2 T9 service; CLI surface in W5 M2)')
|
|
114
114
|
.requiredOption('--project <path>', 'target project root')
|
|
115
115
|
.requiredOption('--session-id <sid>', 'session id whose .peaks/_runtime/<sid>/audit-goal/<rid>.json is the approved goal source')
|
|
116
|
-
.option('--llm-provider <name>', `LLM provider name: ${SUPPORTED_LLM_PROVIDERS.join(' | ')} (default: ${DEFAULT_LLM_PROVIDER} — performs no review)`, DEFAULT_LLM_PROVIDER)
|
|
116
|
+
.option('--llm-provider <name>', `LLM provider name: ${SUPPORTED_LLM_PROVIDERS.join(' | ')} (default: ${DEFAULT_LLM_PROVIDER} — performs no review)`, DEFAULT_LLM_PROVIDER)
|
|
117
|
+
.option('--base <ref>', 'base ref for the pre/post baseline diff (`existing-functionality-intact`); default: merge-base with origin/HEAD, then origin/main, then origin/master, then HEAD~1 — pass this explicitly when none of those resolve')).action(async (rid, options) => {
|
|
117
118
|
// 1. Project root must exist and be a directory.
|
|
118
119
|
const projectValidation = validateProjectRoot(options.project);
|
|
119
120
|
if (!projectValidation.ok) {
|
|
@@ -192,6 +193,7 @@ export function registerFinalReviewCommands(program, io) {
|
|
|
192
193
|
projectRoot: projectValidation.projectRoot,
|
|
193
194
|
sessionId: sessionValidation.sessionId,
|
|
194
195
|
llmRunner,
|
|
196
|
+
...(options.base === undefined ? {} : { baseRef: options.base }),
|
|
195
197
|
});
|
|
196
198
|
const data = {
|
|
197
199
|
status: 'review-complete',
|
|
@@ -16,8 +16,35 @@ import { type CompactLifecycleRecord, type CompactLifecycleStage } from '../comp
|
|
|
16
16
|
* Stages a compact *attempt* can prove from inside the dispatching
|
|
17
17
|
* process. `verifying` / `completed` are deliberately absent — see
|
|
18
18
|
* `CompactLifecyclePublisher` and `settleOpenLifecycleRun` for why.
|
|
19
|
+
*
|
|
20
|
+
* `armed` is the honest resting stage for a dispatch that only
|
|
21
|
+
* REGISTERED a trigger (slice 2026-09-12-compact-band-policy, defect B)
|
|
22
|
+
* — see `resolveDispatchedStage`.
|
|
23
|
+
*/
|
|
24
|
+
type ObservableDispatchStage = Extract<CompactLifecycleStage, 'queued' | 'preparing' | 'compacting' | 'armed'>;
|
|
25
|
+
/** Stage a failure is attributed to (mirrors the store's `failedAt` domain). */
|
|
26
|
+
type FailableStage = Exclude<CompactLifecycleStage, 'failed' | 'completed'>;
|
|
27
|
+
/**
|
|
28
|
+
* Slice 2026-09-12-compact-band-policy (defect B): which stage a
|
|
29
|
+
* *successful* dispatch can honestly claim.
|
|
30
|
+
*
|
|
31
|
+
* A dispatch is only evidence that a compact is IN FLIGHT when the
|
|
32
|
+
* adapter's own trigger is already satisfied. claude-code's
|
|
33
|
+
* `ide-native` pathway installs a PreToolUse hook that compacts
|
|
34
|
+
* in-band at ratio ≥ 0.95 (`AUTO_COMPACT_RED_LINE_RATIO`); at or above
|
|
35
|
+
* that ratio the very next tool call fires it, so `compacting` (and
|
|
36
|
+
* therefore `stalled` if it never lands) is the truthful reading.
|
|
37
|
+
*
|
|
38
|
+
* Below it — and for every pathway that only writes an intent
|
|
39
|
+
* (`llm-self-compress`), or whose shell-exec branch is a deprecated
|
|
40
|
+
* no-op — NOTHING is compacting. The process merely ARMED a trigger.
|
|
41
|
+
* Recording `compacting` there published a heartbeat nobody would ever
|
|
42
|
+
* send, which is what pinned the statusline at `stalled` forever.
|
|
19
43
|
*/
|
|
20
|
-
|
|
44
|
+
export declare function resolveDispatchedStage(input: {
|
|
45
|
+
readonly pathway: string;
|
|
46
|
+
readonly ratio: number;
|
|
47
|
+
}): Extract<CompactLifecycleStage, 'compacting' | 'armed'>;
|
|
21
48
|
/**
|
|
22
49
|
* One id per compact attempt. Timestamp-prefixed so a human reading
|
|
23
50
|
* the raw record can order runs by eye; the random suffix keeps two
|
|
@@ -53,8 +80,17 @@ export declare class CompactLifecyclePublisher {
|
|
|
53
80
|
});
|
|
54
81
|
/** Publish an active stage the process has proved. */
|
|
55
82
|
advance(stage: ObservableDispatchStage): void;
|
|
56
|
-
/**
|
|
57
|
-
|
|
83
|
+
/**
|
|
84
|
+
* Publish the terminal failure, attributed to the last stage reached.
|
|
85
|
+
*
|
|
86
|
+
* `at` overrides that attribution for the case where the failure
|
|
87
|
+
* happened INSIDE a phase that is only named after its outcome
|
|
88
|
+
* (slice 2026-09-12-compact-band-policy): the dispatch call is the
|
|
89
|
+
* compacting phase even though the resting stage is now chosen from
|
|
90
|
+
* the pathway the dispatch returned. `failedAt` names the phase the
|
|
91
|
+
* attempt died in — it is not a published heartbeat.
|
|
92
|
+
*/
|
|
93
|
+
fail(error: unknown, at?: FailableStage): void;
|
|
58
94
|
private write;
|
|
59
95
|
}
|
|
60
96
|
/**
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* returned envelope.
|
|
13
13
|
*/
|
|
14
14
|
import { readCompactLifecycle, writeCompactLifecycle } from '../compact-statusline/compact-lifecycle-store.js';
|
|
15
|
+
import { AUTO_COMPACT_RED_LINE_RATIO } from '../context/auto-compact-types.js';
|
|
15
16
|
/**
|
|
16
17
|
* PRD-002b slice 2 — extract the few cross-cutting magic numbers
|
|
17
18
|
* that actually appear at runtime call sites in this orchestrator.
|
|
@@ -20,6 +21,28 @@ import { readCompactLifecycle, writeCompactLifecycle } from '../compact-statusli
|
|
|
20
21
|
* + spec prose references must keep the literal value visible.
|
|
21
22
|
*/
|
|
22
23
|
const COLLAPSED_ERROR_MAX_CHARS = 160;
|
|
24
|
+
/**
|
|
25
|
+
* Slice 2026-09-12-compact-band-policy (defect B): which stage a
|
|
26
|
+
* *successful* dispatch can honestly claim.
|
|
27
|
+
*
|
|
28
|
+
* A dispatch is only evidence that a compact is IN FLIGHT when the
|
|
29
|
+
* adapter's own trigger is already satisfied. claude-code's
|
|
30
|
+
* `ide-native` pathway installs a PreToolUse hook that compacts
|
|
31
|
+
* in-band at ratio ≥ 0.95 (`AUTO_COMPACT_RED_LINE_RATIO`); at or above
|
|
32
|
+
* that ratio the very next tool call fires it, so `compacting` (and
|
|
33
|
+
* therefore `stalled` if it never lands) is the truthful reading.
|
|
34
|
+
*
|
|
35
|
+
* Below it — and for every pathway that only writes an intent
|
|
36
|
+
* (`llm-self-compress`), or whose shell-exec branch is a deprecated
|
|
37
|
+
* no-op — NOTHING is compacting. The process merely ARMED a trigger.
|
|
38
|
+
* Recording `compacting` there published a heartbeat nobody would ever
|
|
39
|
+
* send, which is what pinned the statusline at `stalled` forever.
|
|
40
|
+
*/
|
|
41
|
+
export function resolveDispatchedStage(input) {
|
|
42
|
+
return input.pathway === 'ide-native' && input.ratio >= AUTO_COMPACT_RED_LINE_RATIO
|
|
43
|
+
? 'compacting'
|
|
44
|
+
: 'armed';
|
|
45
|
+
}
|
|
23
46
|
/**
|
|
24
47
|
* One id per compact attempt. Timestamp-prefixed so a human reading
|
|
25
48
|
* the raw record can order runs by eye; the random suffix keeps two
|
|
@@ -100,8 +123,17 @@ export class CompactLifecyclePublisher {
|
|
|
100
123
|
redLine: this.ctx.redLine
|
|
101
124
|
});
|
|
102
125
|
}
|
|
103
|
-
/**
|
|
104
|
-
|
|
126
|
+
/**
|
|
127
|
+
* Publish the terminal failure, attributed to the last stage reached.
|
|
128
|
+
*
|
|
129
|
+
* `at` overrides that attribution for the case where the failure
|
|
130
|
+
* happened INSIDE a phase that is only named after its outcome
|
|
131
|
+
* (slice 2026-09-12-compact-band-policy): the dispatch call is the
|
|
132
|
+
* compacting phase even though the resting stage is now chosen from
|
|
133
|
+
* the pathway the dispatch returned. `failedAt` names the phase the
|
|
134
|
+
* attempt died in — it is not a published heartbeat.
|
|
135
|
+
*/
|
|
136
|
+
fail(error, at) {
|
|
105
137
|
this.write({
|
|
106
138
|
schemaVersion: 1,
|
|
107
139
|
runId: this.ctx.runId,
|
|
@@ -109,7 +141,7 @@ export class CompactLifecyclePublisher {
|
|
|
109
141
|
updatedAt: new Date().toISOString(),
|
|
110
142
|
triggerRatio: this.ctx.triggerRatio,
|
|
111
143
|
redLine: this.ctx.redLine,
|
|
112
|
-
failedAt: this.lastStage,
|
|
144
|
+
failedAt: at ?? this.lastStage,
|
|
113
145
|
errorSummary: summarizeLifecycleError(error)
|
|
114
146
|
});
|
|
115
147
|
}
|
|
@@ -197,9 +229,12 @@ export function settleOpenLifecycleRun(input) {
|
|
|
197
229
|
});
|
|
198
230
|
if (prior === null)
|
|
199
231
|
return;
|
|
200
|
-
// Only a run that was actually dispatched
|
|
201
|
-
//
|
|
202
|
-
|
|
232
|
+
// Only a run that was actually dispatched can be completed by a
|
|
233
|
+
// post-compact measurement. Both `compacting` (the in-band trigger is
|
|
234
|
+
// satisfied) and `armed` (a trigger was registered and could fire at
|
|
235
|
+
// any time) qualify: the measured drop is proof that SOME compact
|
|
236
|
+
// landed, and this is the only open run to attribute it to.
|
|
237
|
+
if (prior.stage !== 'compacting' && prior.stage !== 'armed')
|
|
203
238
|
return;
|
|
204
239
|
const emit = (stage, withAfterRatio) => {
|
|
205
240
|
const record = {
|
|
@@ -35,7 +35,7 @@ import { resolveOuterSessionId } from '../session/binding-status-service.js';
|
|
|
35
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
|
-
import { CompactLifecyclePublisher, newCompactRunId, settleOpenLifecycleRun, summarizeLifecycleError } from './auto-compact-lifecycle.js';
|
|
38
|
+
import { CompactLifecyclePublisher, newCompactRunId, resolveDispatchedStage, settleOpenLifecycleRun, summarizeLifecycleError } from './auto-compact-lifecycle.js';
|
|
39
39
|
const PRE_COMPACT_REASON = 'pre-compact-auto';
|
|
40
40
|
/**
|
|
41
41
|
* Map a context ratio to a `CompactTrigger` action. Pure; the side
|
|
@@ -435,10 +435,6 @@ export async function runAutoCompact(input) {
|
|
|
435
435
|
const target = input.target ?? 'main';
|
|
436
436
|
let dispatch;
|
|
437
437
|
try {
|
|
438
|
-
// `compacting` is the last stage this process can prove: the IDE
|
|
439
|
-
// performs the actual compaction out-of-band, so a successful
|
|
440
|
-
// dispatch return is NOT evidence the context shrank.
|
|
441
|
-
lifecycle.advance('compacting');
|
|
442
438
|
if (input.testHooks?.failCompacting)
|
|
443
439
|
throw new Error('IDE dispatch exploded');
|
|
444
440
|
// Slice 2026-06-28: when targeting the main session, write an
|
|
@@ -463,7 +459,9 @@ export async function runAutoCompact(input) {
|
|
|
463
459
|
});
|
|
464
460
|
}
|
|
465
461
|
catch (error) {
|
|
466
|
-
|
|
462
|
+
// `compacting` is the phase this failure died in — a phase label,
|
|
463
|
+
// not a claim that a compaction was in flight.
|
|
464
|
+
lifecycle.fail(error, 'compacting');
|
|
467
465
|
return {
|
|
468
466
|
ok: false,
|
|
469
467
|
code: 'AUTO_COMPACT_DISPATCH_FAILED',
|
|
@@ -480,11 +478,22 @@ export async function runAutoCompact(input) {
|
|
|
480
478
|
}
|
|
481
479
|
};
|
|
482
480
|
}
|
|
483
|
-
//
|
|
484
|
-
//
|
|
485
|
-
//
|
|
486
|
-
|
|
487
|
-
|
|
481
|
+
// Slice 2026-09-12-compact-band-policy (defect B): the stage is
|
|
482
|
+
// chosen from what the dispatch ACTUALLY did, never from the hope
|
|
483
|
+
// that it compacted. `ide-native` on claude-code only installs a
|
|
484
|
+
// PreToolUse hook that fires at ratio ≥ 0.95 — below that nothing is
|
|
485
|
+
// in flight and the honest stage is `armed` (see
|
|
486
|
+
// `resolveDispatchedStage`). Writing `compacting` there published a
|
|
487
|
+
// heartbeat that would never arrive, which is exactly what pinned
|
|
488
|
+
// the statusline at `stalled` for 92 minutes in the field.
|
|
489
|
+
if (dispatch.ok) {
|
|
490
|
+
lifecycle.advance(resolveDispatchedStage({ pathway: dispatch.pathway, ratio: probe.ratio }));
|
|
491
|
+
}
|
|
492
|
+
else {
|
|
493
|
+
// A dispatcher that returns `ok: false` did not compact anything —
|
|
494
|
+
// record that as a failure at `compacting` rather than leaving the
|
|
495
|
+
// run looking like it is still in progress.
|
|
496
|
+
lifecycle.fail(new Error(dispatch.message), 'compacting');
|
|
488
497
|
}
|
|
489
498
|
// Slice 2026-07-30-compact-visibility: append a compact-history
|
|
490
499
|
// event so the new 'peaks compact history' CLI and the
|
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
export type CompactLifecycleStage = 'queued' | 'preparing' | 'compacting'
|
|
1
|
+
export type CompactLifecycleStage = 'queued' | 'preparing' | 'compacting'
|
|
2
|
+
/**
|
|
3
|
+
* Slice 2026-09-12-compact-band-policy: a compact trigger was
|
|
4
|
+
* REGISTERED but no compaction has started — e.g. claude-code's
|
|
5
|
+
* `ide-native` pathway only installs the PreToolUse hook, which
|
|
6
|
+
* compacts in-band at ratio ≥ 0.95. In the 0.80–0.95 band nothing
|
|
7
|
+
* is in flight, so claiming `compacting` was a false heartbeat.
|
|
8
|
+
*/
|
|
9
|
+
| 'armed' | 'verifying' | 'completed' | 'failed';
|
|
2
10
|
export interface CompactLifecycleRecord {
|
|
3
11
|
readonly schemaVersion: 1;
|
|
4
12
|
readonly runId: string;
|
|
@@ -49,7 +57,8 @@ export declare function writeCompactLifecycle(input: {
|
|
|
49
57
|
* Return kinds:
|
|
50
58
|
* - `missing` — no record has ever been written for this session
|
|
51
59
|
* - `valid` — record parsed and validated; stage is terminal
|
|
52
|
-
* (completed/failed) OR is active
|
|
60
|
+
* (completed/failed), resting (`armed`) OR is active
|
|
61
|
+
* but still fresh
|
|
53
62
|
* - `invalid` — file exists but is malformed / wrong schema / out
|
|
54
63
|
* of range. `reason` is a single-line English message
|
|
55
64
|
* (no CLI verbs, no stack traces). The caller MUST
|
|
@@ -19,11 +19,27 @@
|
|
|
19
19
|
// compacting / verifying) is `stalled` when (nowMs - updatedAt) >
|
|
20
20
|
// staleAfterMs. Terminal stages (`completed`, `failed`) NEVER go
|
|
21
21
|
// stalled — the record is the historical answer, not a heartbeat.
|
|
22
|
+
//
|
|
23
|
+
// Slice 2026-09-12-compact-band-policy (defect B): `armed` is a third
|
|
24
|
+
// category — a RESTING stage. It records a fact ("we registered a
|
|
25
|
+
// compact trigger") that stays true until a measurement supersedes it;
|
|
26
|
+
// no heartbeat was ever promised, so waiting in it is NORMAL and it
|
|
27
|
+
// must never be reported as `stalled`. Only `compacting` means "a
|
|
28
|
+
// compaction should be in flight right now", which is the only state
|
|
29
|
+
// where a missing heartbeat is real evidence of a stall.
|
|
22
30
|
import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
23
31
|
import { dirname, join } from 'node:path';
|
|
24
32
|
import { getSessionDir } from '../session/getSessionDir.js';
|
|
25
33
|
const LIFECYCLE_FILENAME = 'compact-lifecycle.json';
|
|
26
34
|
const ERROR_SUMMARY_MAX = 160;
|
|
35
|
+
/**
|
|
36
|
+
* Stages that promise a heartbeat: each one is only written while the
|
|
37
|
+
* dispatching process (or the IDE it armed) is expected to make
|
|
38
|
+
* progress, so a stale `updatedAt` is real evidence of a stall.
|
|
39
|
+
*
|
|
40
|
+
* `armed` is deliberately NOT here — see the header. It is a resting
|
|
41
|
+
* state, not a heartbeat.
|
|
42
|
+
*/
|
|
27
43
|
const ACTIVE_STAGES = [
|
|
28
44
|
'queued',
|
|
29
45
|
'preparing',
|
|
@@ -34,6 +50,7 @@ const ALL_STAGES = [
|
|
|
34
50
|
'queued',
|
|
35
51
|
'preparing',
|
|
36
52
|
'compacting',
|
|
53
|
+
'armed',
|
|
37
54
|
'verifying',
|
|
38
55
|
'completed',
|
|
39
56
|
'failed',
|
|
@@ -169,7 +186,8 @@ export function writeCompactLifecycle(input) {
|
|
|
169
186
|
* Return kinds:
|
|
170
187
|
* - `missing` — no record has ever been written for this session
|
|
171
188
|
* - `valid` — record parsed and validated; stage is terminal
|
|
172
|
-
* (completed/failed) OR is active
|
|
189
|
+
* (completed/failed), resting (`armed`) OR is active
|
|
190
|
+
* but still fresh
|
|
173
191
|
* - `invalid` — file exists but is malformed / wrong schema / out
|
|
174
192
|
* of range. `reason` is a single-line English message
|
|
175
193
|
* (no CLI verbs, no stack traces). The caller MUST
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type CompactLifecycleStage } from './compact-lifecycle-store.js';
|
|
2
|
-
export type CompactDisplayKind = 'none' | 'queued' | 'preparing' | 'compacting' | 'verifying' | 'completed' | 'failed' | 'stalled' | 'invalid';
|
|
2
|
+
export type CompactDisplayKind = 'none' | 'queued' | 'preparing' | 'compacting' | 'verifying' | 'completed' | 'failed' | 'stalled' | 'invalid' | 'armed';
|
|
3
3
|
export interface CompactStatuslineState {
|
|
4
4
|
readonly kind: CompactDisplayKind;
|
|
5
5
|
readonly filledCells: 0 | 2 | 4 | 6 | 8;
|
|
@@ -28,6 +28,9 @@
|
|
|
28
28
|
// none → 0 cells
|
|
29
29
|
// invalid → 0 cells (no false reassurance)
|
|
30
30
|
// stalled → keep the active stage's cell
|
|
31
|
+
// armed → NO bar at all (slice 2026-09-12-compact-band-policy):
|
|
32
|
+
// a registered-but-idle trigger has no progress to
|
|
33
|
+
// report, and a bar would imply one.
|
|
31
34
|
//
|
|
32
35
|
// Render contract: the rendered label is a fixed-width 8-cell bar
|
|
33
36
|
// (`[████░░░░]` filled from the left). NO `?` characters. NO guessed
|
|
@@ -36,6 +39,7 @@
|
|
|
36
39
|
// measurement" hint.
|
|
37
40
|
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
38
41
|
import { getSessionDir } from '../session/getSessionDir.js';
|
|
42
|
+
import { AUTO_COMPACT_RED_LINE_RATIO } from '../context/auto-compact-types.js';
|
|
39
43
|
import { readCompactLifecycle, } from './compact-lifecycle-store.js';
|
|
40
44
|
/**
|
|
41
45
|
* Concrete first-version stale timeout. Adjustable after real timing
|
|
@@ -70,6 +74,8 @@ const CELL_BY_STAGE = new Map([
|
|
|
70
74
|
['verifying', STAGE_CELL_VERIFYING],
|
|
71
75
|
['completed', STAGE_CELL_COMPLETED],
|
|
72
76
|
['failed', STAGE_CELL_COMPACTING],
|
|
77
|
+
// `armed` is not in the cell table: it renders WITHOUT a bar (see
|
|
78
|
+
// renderCompactStatusline). Lookups therefore miss and fall back to 0.
|
|
73
79
|
]);
|
|
74
80
|
const FILLED = '█';
|
|
75
81
|
const EMPTY = '░';
|
|
@@ -245,6 +251,12 @@ export function renderCompactStatusline(state) {
|
|
|
245
251
|
return `compact ${renderBar(2)}`;
|
|
246
252
|
case 'compacting':
|
|
247
253
|
return `compact ${renderBar(STAGE_CELL_COMPACTING)}`;
|
|
254
|
+
// Slice 2026-09-12-compact-band-policy: a registered trigger that has
|
|
255
|
+
// not fired is NOT progress. Rendering a bar would imply movement
|
|
256
|
+
// that is not happening, so `armed` deliberately renders no bar at
|
|
257
|
+
// all — the label states what is true and what would change it.
|
|
258
|
+
case 'armed':
|
|
259
|
+
return `compact armed${formatArmedRatio(state)} — fires in-band at ${arRedLinePct()}`;
|
|
248
260
|
case 'verifying':
|
|
249
261
|
return `compact ${renderBar(STAGE_CELL_VERIFYING)}`;
|
|
250
262
|
case 'completed':
|
|
@@ -257,6 +269,16 @@ export function renderCompactStatusline(state) {
|
|
|
257
269
|
return formatInvalid(state);
|
|
258
270
|
}
|
|
259
271
|
}
|
|
272
|
+
/** The red-line ratio the armed trigger waits for, as a display percentage. */
|
|
273
|
+
function arRedLinePct() {
|
|
274
|
+
return `${Math.round(AUTO_COMPACT_RED_LINE_RATIO * 100)}%`;
|
|
275
|
+
}
|
|
276
|
+
/** ` (88% now)` when the opening trigger ratio is known; empty otherwise. */
|
|
277
|
+
function formatArmedRatio(state) {
|
|
278
|
+
return typeof state.triggerRatio === 'number'
|
|
279
|
+
? ` (${Math.round(state.triggerRatio * 100)}% now)`
|
|
280
|
+
: '';
|
|
281
|
+
}
|
|
260
282
|
function formatCompleted(state) {
|
|
261
283
|
const bar = renderBar(STAGE_CELL_COMPLETED);
|
|
262
284
|
if (typeof state.afterRatio === 'number') {
|