peaks-loop 3.0.2 → 3.1.0
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 +66 -0
- package/README-en.md +1 -1
- package/README.md +1 -1
- package/dist/src/cli/commands/core/session-command.js +6 -0
- package/dist/src/cli/commands/job-commands.d.ts +3 -0
- package/dist/src/cli/commands/job-commands.js +236 -0
- package/dist/src/cli/commands/session-auto-compact-hook-command.d.ts +37 -0
- package/dist/src/cli/commands/session-auto-compact-hook-command.js +127 -0
- package/dist/src/cli/program.js +6 -0
- package/dist/src/services/context/auto-compact-dispatcher.js +60 -10
- package/dist/src/services/context/main-session-monitor.d.ts +12 -0
- package/dist/src/services/context/main-session-monitor.js +12 -0
- package/dist/src/services/hooks/auto-compact-hook-install.d.ts +104 -0
- package/dist/src/services/hooks/auto-compact-hook-install.js +157 -0
- package/dist/src/services/ide/adapters/claude-code-adapter.js +21 -9
- package/dist/src/services/job/job-event-emitter.d.ts +30 -0
- package/dist/src/services/job/job-event-emitter.js +5 -0
- package/dist/src/services/job/job-orchestrator.d.ts +50 -0
- package/dist/src/services/job/job-orchestrator.js +104 -0
- package/dist/src/services/job/job-resource-snapshot.d.ts +7 -0
- package/dist/src/services/job/job-resource-snapshot.js +47 -0
- package/dist/src/services/job/job-rotation.d.ts +17 -0
- package/dist/src/services/job/job-rotation.js +30 -0
- package/dist/src/services/job/job-state-store.d.ts +24 -0
- package/dist/src/services/job/job-state-store.js +60 -0
- package/dist/src/services/job/job-types.d.ts +444 -0
- package/dist/src/services/job/job-types.js +87 -0
- package/dist/src/services/job/subagent-job-wrapper.d.ts +35 -0
- package/dist/src/services/job/subagent-job-wrapper.js +30 -0
- package/dist/src/shared/version.d.ts +1 -1
- package/dist/src/shared/version.js +1 -1
- package/package.json +1 -1
- package/skills/peaks-solo/SKILL.md +91 -77
- package/skills/peaks-solo/references/completion-handoff.md +9 -4
- package/skills/peaks-solo/references/job-loop.md +68 -0
- package/skills/peaks-solo/references/periodic-checkpoint.md +11 -1
- package/skills/peaks-solo/references/runbook.md +18 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,71 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.1.0 — 2026-07-03
|
|
4
|
+
|
|
5
|
+
### Fixed — peaks-solo external-references authority declaration
|
|
6
|
+
|
|
7
|
+
- `skills/peaks-solo/SKILL.md` External-references paragraph now ends with `Peaks-Loop Solo gates and artifacts remain authoritative.` to satisfy `tests/unit/skill-external-invocation.test.ts:63` (`PEAKS_AUTHORITATIVE_PATTERN`). One-line surgical patch; no behaviour change.
|
|
8
|
+
|
|
9
|
+
### Added — Peaks-Loop Job
|
|
10
|
+
|
|
11
|
+
- New `peaks job *` subcommand family: init / status / checkpoint / continue / resume / block / handoff / rotate-now / subagent-cleanup (9 commands).
|
|
12
|
+
- New CLI flag `--main-loop-strategy single|rotating` on `job init`, with rotating-mode hard default for ≥3 slices and `rotateEvery=3` cadence.
|
|
13
|
+
- New `--watch` poll mode + statusline event stub (`emitJobEvent`) for ambient progress visibility. Real statusline wire-up deferred to a follow-up slice.
|
|
14
|
+
- Sub-agent wrapper `SubAgentJobWrapper` enforces `--budget-mb 512` default + cleanup gate before slice checkpoint.
|
|
15
|
+
- New peaks-solo SKILL.md Steps 0.8 / 0.81 / 0.85 / 0.86 / 0.87 wrapping the existing single-rid runbook for multi-slice jobs.
|
|
16
|
+
- 9 hard-red-line rules embedded in the skill prose (cost re-ask ban, slice-coalesce ban, fake-completion ban, detached-mode ban, cleanup-skip ban, rotate-skip ban, etc).
|
|
17
|
+
|
|
18
|
+
### New services
|
|
19
|
+
|
|
20
|
+
- `src/services/job/job-types.ts` — Zod schemas + types for SliceState, JobState, ResourceSnapshot, JobStatusSummary, plus CLI input schemas (JobInitInput, JobCheckpointInput, JobBlockInput).
|
|
21
|
+
- `src/services/job/job-state-store.ts` — on-disk state store with per-job lockfile.
|
|
22
|
+
- `src/services/job/job-orchestrator.ts` — state machine with strict / best-effort exit policies.
|
|
23
|
+
- `src/services/job/job-rotation.ts` — main-session rotation cadence (single / rotating mode).
|
|
24
|
+
- `src/services/job/subagent-job-wrapper.ts` — budget + cleanup gate.
|
|
25
|
+
- `src/services/job/job-resource-snapshot.ts` — cpu / mem / disk / context collector.
|
|
26
|
+
- `src/services/job/job-event-emitter.ts` — stderr-stub event emit (real statusline wire-up pending).
|
|
27
|
+
|
|
28
|
+
### Migration
|
|
29
|
+
|
|
30
|
+
- Existing single-rid flows are unchanged. The Job is opt-in via peaks-solo Step 0.8 detection; users who do not invoke multi-slice requests see no behavior difference.
|
|
31
|
+
- The `peaks session rotate` and `peaks session cycle-summary` subcommands referenced in the spec are NOT shipped in v1; the rotation path uses constructor-injected stub callbacks. M6.5 follow-up will add the real subcommands.
|
|
32
|
+
|
|
33
|
+
### Spec / plan
|
|
34
|
+
|
|
35
|
+
- Design: `docs/superpowers/specs/2026-07-03-peaks-loop-job-design.md` (v3).
|
|
36
|
+
- Plans: `docs/superpowers/plans/2026-07-03-peaks-job/` (M1-M7 + README).
|
|
37
|
+
|
|
38
|
+
## [3.0.3] — 2026-07-02 — auto-compact zero-pause (ide-native pathway)
|
|
39
|
+
|
|
40
|
+
**PATCH bump from 3.0.2**. Closes the user-pause gap on long Claude Code sessions: at context ratio ≥ 0.95 the runner now compacts itself via a PreToolUse hook, no human intervention required.
|
|
41
|
+
|
|
42
|
+
### Added — `ide-native` pathway + `peaks session auto-compact-hook`
|
|
43
|
+
|
|
44
|
+
The v2.13.0 auto-compact design shipped `shell-exec` as the only pathway, which spawns a NEW claude child process and cannot compact the current runner. Long-task users hit a UX gap: AI CLI still stopped and asked the human to run `/compact` manually.
|
|
45
|
+
|
|
46
|
+
- **New `ide-native` pathway** — `src/services/context/auto-compact-dispatcher.ts:138-155` routes main-session compacts through `installAutoCompactHook`, which writes a PreToolUse hook into `.claude/settings.local.json` (matcher `Bash|Task`, command `peaks session auto-compact-hook`).
|
|
47
|
+
- **New `peaks session auto-compact-hook` CLI** — `src/cli/commands/session-auto-compact-hook-command.ts`. Reads `CLAUDE_CONTEXT_USAGE_PERCENT`; below 0.95 exits 0 silent; at ratio ≥ 0.95 spawns `claude --compact` in-band against the **current** runner (detached + unref, never blocks the runner's tool call). ENOENT-safe: if `claude` is not on PATH (e.g. user runs Claude Code as an MCP), the hook logs a stderr hint and exits 0 silent rather than crashing the runner.
|
|
48
|
+
- **New `installAutoCompactHook` / `removeAutoCompactHook` service** — `src/services/hooks/auto-compact-hook-install.ts`. Idempotent: re-install returns `already-installed` and does NOT re-write the file; remove preserves other PreToolUse entries (the existing fact-forcing bypass hook on every peaks-loop consumer project survives).
|
|
49
|
+
- **Lazy install** — hook is installed on first `peaks solo auto-compact` invocation, NOT on `peaks workspace init`. User has opted in by running the command; no zero-touch surprise.
|
|
50
|
+
- **Claude Code adapter** — `compactPathway` changed from `shell-exec` to `ide-native` for `target='main'`. Sub-agent shells still get the legacy `shell-exec` pathway.
|
|
51
|
+
|
|
52
|
+
### Fixed — Solo Step N+2 prose (was 75% / "do NOT auto-execute")
|
|
53
|
+
|
|
54
|
+
- `skills/peaks-solo/SKILL.md` Step N+2 paragraph rewritten: thresholds now 0.85 pre-compact / 0.95 red-line (matches v2.13.0 auto-compact), explicit Karpathy §4 compact-red-line exception cited, `peaks solo context-now --json` is the canonical probe primitive (replaces `peaks context check --prompt-size <bytes>` hand-pass).
|
|
55
|
+
- `src/services/context/main-session-monitor.ts` — `@deprecated` JSDoc on `evaluateMainSessionThreshold` cross-links to `evaluateCompactTrigger` (0.85/0.95); legacy 4-tier envelope retained for statusline callers (migration to v2.15.0).
|
|
56
|
+
|
|
57
|
+
### Tests — 4 new suites + 3 modified files
|
|
58
|
+
|
|
59
|
+
- `tests/unit/services/hooks/auto-compact-hook-install.test.ts` (7 cases) — install / idempotent / remove / preservation of unrelated entries.
|
|
60
|
+
- `tests/unit/services/context/auto-compact-dispatcher-ide-native.test.ts` (9 cases) — `dispatchIdeCompact({ target: 'main' })` routes to `ide-native`; round-trip install / remove.
|
|
61
|
+
- `tests/unit/skills/solo-step-n-plus-2-prose.test.ts` (6 cases) — pins the 0.85 / 0.95 / Karpathy §4 contract; asserts `75%` is NOT in the paragraph.
|
|
62
|
+
- `tests/unit/cli/session-auto-compact-hook-command.test.ts` (3 cases) — dogfood surfacing: below-threshold silent, missing env-var silent, red-line + ENOENT exits 0 with stderr hint.
|
|
63
|
+
- `tests/unit/services/context/main-session-monitor.test.ts` (+2 AC-4 cases), `tests/unit/context/auto-compact-main-target.test.ts` (pathway assertion updated) — coverage.
|
|
64
|
+
|
|
65
|
+
### Dogfood verified
|
|
66
|
+
|
|
67
|
+
Validated on `C:/Users/smallMark/Desktop/peaksclaw/ice-cola` (consumer project). End-to-end: install hook alongside existing fact-forcing bypass → idempotent re-run → below-threshold silent → red-line + no-claude-on-PATH exits 0 with hint → remove strips only the auto-compact matcher. No regressions in 5,139 / 5,158 vitest pass.
|
|
68
|
+
|
|
3
69
|
## [3.0.2] — 2026-07-02 — change-id shim retirement (v2.19.0) + Understand Anything hybrid context (3.0.2)
|
|
4
70
|
|
|
5
71
|
**PATCH bump from 3.0.1**. Two independent slices bundled into one release:
|
package/README-en.md
CHANGED
|
@@ -137,7 +137,7 @@ peaks project dashboard --project . --json # one-shot project view
|
|
|
137
137
|
|
|
138
138
|
**3 solo wrappers + 7 role skills + 2 standalone audits + 1 orchestrator = 13 skills.** In daily use, 1 skill (`peaks-solo`) covers ≥ 90% of needs.
|
|
139
139
|
|
|
140
|
-
> **v2.13.0**: zero-human-intervention **auto-compact** (`peaks solo context-now` + `peaks solo auto-compact`) — peaks-loop probes context-fill % autonomously. ≥85% writes a pre-compact checkpoint + convergence plan + dispatches IDE compact. ≥95% forces a synchronous RED-LINE compact. The LLM-runner stays alive with context < 95% without human intervention. **v2.13.4 fix**: auto-compact now targets the **main-session** context (not a sub-agent shell)
|
|
140
|
+
> **v2.13.0**: zero-human-intervention **auto-compact** (`peaks solo context-now` + `peaks solo auto-compact`) — peaks-loop probes context-fill % autonomously. ≥85% writes a pre-compact checkpoint + convergence plan + dispatches IDE compact. ≥95% forces a synchronous RED-LINE compact. The LLM-runner stays alive with context < 95% without human intervention. **v2.13.4 fix**: auto-compact now targets the **main-session** context (not a sub-agent shell). **3.0.3 new `ide-native` pathway**: installs a PreToolUse hook into `.claude/settings.local.json` (`Bash|Task` matcher → `peaks session auto-compact-hook`); the runner's next Bash/Task tool call in-band spawns `claude --compact`, no longer relying on the LLM to remember to fire `/compact`. Hook is ENOENT-safe (exits 0 + stderr hint if `claude` is not on PATH; runner is never interrupted).
|
|
141
141
|
|
|
142
142
|
## 🚧 Killer feature: un-bypassable gates
|
|
143
143
|
|
package/README.md
CHANGED
|
@@ -175,7 +175,7 @@ peaks project dashboard --project . --json # 当前项目一眼看完
|
|
|
175
175
|
|
|
176
176
|
**3 solo 包装 + 7 角色技能 + 2 独立审计 + 1 编排器 = 13 个。日常 1 个 `peaks-solo` 覆盖 ≥ 90%。**
|
|
177
177
|
|
|
178
|
-
> **v2.13.0 新增**:zero-human-intervention **auto-compact**(`peaks solo context-now` + `peaks solo auto-compact`)——peaks-loop 自己探测 context %,≥85% 主动收敛(写 checkpoint + 收敛计划 + 触发 IDE compact),≥95% 强制同步压缩红线。LLM-runner 上下文始终 < 95% 自主运行,零介入。**v2.13.4 修复**:`auto-compact` 现在默认压缩**主会话**上下文(不是 sub-agent shell
|
|
178
|
+
> **v2.13.0 新增**:zero-human-intervention **auto-compact**(`peaks solo context-now` + `peaks solo auto-compact`)——peaks-loop 自己探测 context %,≥85% 主动收敛(写 checkpoint + 收敛计划 + 触发 IDE compact),≥95% 强制同步压缩红线。LLM-runner 上下文始终 < 95% 自主运行,零介入。**v2.13.4 修复**:`auto-compact` 现在默认压缩**主会话**上下文(不是 sub-agent shell 自己的)。**3.0.3 新增 `ide-native` 通路**:在 `.claude/settings.local.json` 装 PreToolUse hook(`Bash|Task` matcher → `peaks session auto-compact-hook`),runner 下一次 Bash/Task tool call **in-band** 触发 `claude --compact`,不再依赖 LLM 自己 fire `/compact`。Hook ENOENT-safe(claude CLI 不在 PATH 时退出 0 + stderr 提示,runner 不会被中断)。
|
|
179
179
|
|
|
180
180
|
## 🚧 杀手锏:不可绕过的门禁
|
|
181
181
|
|
|
@@ -13,6 +13,7 @@ import { addJsonOption, getErrorMessage, printResult } from '../../cli-helpers.j
|
|
|
13
13
|
// saw them — they were effectively hidden.
|
|
14
14
|
import { registerSessionCheckpointCommand } from '../session-checkpoint-command.js';
|
|
15
15
|
import { registerSessionResumeCommand } from '../session-resume-command.js';
|
|
16
|
+
import { registerSessionAutoCompactHookCommand } from '../session-auto-compact-hook-command.js';
|
|
16
17
|
export function registerSessionCommand(program, io) {
|
|
17
18
|
const session = program.command('session').description('Manage Peaks session directories');
|
|
18
19
|
addJsonOption(session
|
|
@@ -186,6 +187,11 @@ export function registerSessionCommand(program, io) {
|
|
|
186
187
|
// `<TAB>`-discovery and the LLM never saw them.
|
|
187
188
|
registerSessionCheckpointCommand(session, io);
|
|
188
189
|
registerSessionResumeCommand(session, io);
|
|
190
|
+
// Slice 2026-07-02-auto-compact-zero-pause: the Bash command the
|
|
191
|
+
// PreToolUse hook in `.claude/settings.local.json` fires. Keeping
|
|
192
|
+
// it eager (same fix as checkpoint + resume) so `peaks session --help`
|
|
193
|
+
// lists it for LLM `<TAB>`-discovery.
|
|
194
|
+
registerSessionAutoCompactHookCommand(session);
|
|
189
195
|
addJsonOption(session
|
|
190
196
|
.command('rotate')
|
|
191
197
|
.description('Drop the project-level session binding so the next peaks call auto-generates a fresh session id. The on-disk session directory is left intact — only .peaks/.session.json is removed.')
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
// src/cli/commands/job-commands.ts
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import { fail, ok } from '../../shared/result.js';
|
|
5
|
+
import { addJsonOption, printResult } from '../cli-helpers.js';
|
|
6
|
+
import { JobStateStore } from '../../services/job/job-state-store.js';
|
|
7
|
+
import { JobOrchestrator } from '../../services/job/job-orchestrator.js';
|
|
8
|
+
import { JobRotation } from '../../services/job/job-rotation.js';
|
|
9
|
+
import { SubAgentJobWrapper } from '../../services/job/subagent-job-wrapper.js';
|
|
10
|
+
import { emitJobEvent } from '../../services/job/job-event-emitter.js';
|
|
11
|
+
import { JobInitInputSchema, JobCheckpointInputSchema, JobBlockInputSchema, } from '../../services/job/job-types.js';
|
|
12
|
+
import { getCurrentSessionId } from '../../services/skills/skill-presence-service.js';
|
|
13
|
+
function projectRoot(opts) {
|
|
14
|
+
// Reuse the workspace root resolver from peaks CLI; for now, CWD as a safe placeholder.
|
|
15
|
+
return opts.project ?? process.cwd();
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Resolves the on-disk root for Job state files.
|
|
19
|
+
*
|
|
20
|
+
* Per spec §3.3 + §4.5 (2.7.1 single-scope-axis layout), Job state lives at:
|
|
21
|
+
* `<projectRoot>/.peaks/_runtime/<sessionId>/job/<jobId>/state.json`
|
|
22
|
+
*
|
|
23
|
+
* The `JobStateStore` itself only knows its `rootDir` + `jobId` and joins them. We
|
|
24
|
+
* compute the canonical root here (per-call) so the store can stay layout-agnostic.
|
|
25
|
+
*
|
|
26
|
+
* Resolution order:
|
|
27
|
+
* 1. `--session-id` flag (explicit override)
|
|
28
|
+
* 2. `getCurrentSessionId(project)` — reads `.peaks/_runtime/session.json` per peaks-solo
|
|
29
|
+
* 3. Error (NO_ACTIVE_SESSION) — must never silently fall back to a random uuid
|
|
30
|
+
*/
|
|
31
|
+
function resolveJobStateRoot(opts) {
|
|
32
|
+
const project = projectRoot(opts);
|
|
33
|
+
const sessionId = opts.sessionId ?? getCurrentSessionId(project);
|
|
34
|
+
if (!sessionId) {
|
|
35
|
+
throw new Error('NO_ACTIVE_SESSION: peaks job requires --session-id or an active peaks-solo session via peaks workspace init');
|
|
36
|
+
}
|
|
37
|
+
const rootDir = join(project, '.peaks', '_runtime', sessionId, 'job');
|
|
38
|
+
return { rootDir, sessionId, projectRoot: project };
|
|
39
|
+
}
|
|
40
|
+
export function registerJobCommands(program, io = { stdout: (t) => process.stdout.write(t), stderr: (t) => process.stderr.write(t) }) {
|
|
41
|
+
const job = new Command('job').description('Drive long multi-slice work as one Job (peaks-solo Step 0.8+)');
|
|
42
|
+
job
|
|
43
|
+
.command('init')
|
|
44
|
+
.requiredOption('--job-id <jid>')
|
|
45
|
+
.requiredOption('--slice-list <list>')
|
|
46
|
+
.option('--parallelism-hint <serial|llm-decides>', 'llm-decides')
|
|
47
|
+
.option('--exit-policy <strict|best-effort>', 'strict')
|
|
48
|
+
.option('--main-loop-strategy <single|rotating>', 'rotating')
|
|
49
|
+
.option('--rotate-every <n>', 'rotate every N slices (rotating mode)', '3')
|
|
50
|
+
.option('--session-id <sid>', 'session id (default: read from .peaks/_runtime/session.json; required to land in the 2.7.1 single-scope-axis layout)')
|
|
51
|
+
.option('--project <repo>')
|
|
52
|
+
.action(async (opts) => {
|
|
53
|
+
const project = projectRoot(opts);
|
|
54
|
+
// Resolve sessionId: explicit flag > canonical session binding > FAIL.
|
|
55
|
+
// Per spec §3.3, Job state lives at .peaks/_runtime/<sessionId>/job/<jobId>/state.json —
|
|
56
|
+
// a random UUID would scatter state across dirs and break resume/auto-compact.
|
|
57
|
+
let sessionId = opts.sessionId ?? getCurrentSessionId(project);
|
|
58
|
+
if (!sessionId) {
|
|
59
|
+
return printResult(io, fail('init', 'NO_ACTIVE_SESSION', 'peaks job init requires --session-id (or an active peaks-solo session via peaks workspace init)', { project }, [
|
|
60
|
+
'Re-run with --session-id <sid>',
|
|
61
|
+
'Or run `peaks workspace init` to create a session first'
|
|
62
|
+
]), opts);
|
|
63
|
+
}
|
|
64
|
+
const parsed = JobInitInputSchema.safeParse({
|
|
65
|
+
jobId: opts.jobId,
|
|
66
|
+
sessionId,
|
|
67
|
+
sliceList: opts.sliceList.split(',').map((s) => s.trim()).filter(Boolean),
|
|
68
|
+
parallelismHint: opts.parallelismHint,
|
|
69
|
+
exitPolicy: opts.exitPolicy,
|
|
70
|
+
mainLoopStrategy: opts.mainLoopStrategy,
|
|
71
|
+
rotateEvery: Number(opts.rotateEvery),
|
|
72
|
+
project,
|
|
73
|
+
json: opts.json,
|
|
74
|
+
});
|
|
75
|
+
if (!parsed.success)
|
|
76
|
+
return printResult(io, fail('init', 'INVALID_INIT', parsed.error.message, {}), opts);
|
|
77
|
+
const jobRoot = resolveJobStateRoot(opts);
|
|
78
|
+
const store = new JobStateStore(jobRoot.rootDir);
|
|
79
|
+
const orch = new JobOrchestrator(store);
|
|
80
|
+
const state = orch.init({
|
|
81
|
+
jobId: parsed.data.jobId,
|
|
82
|
+
sessionId: parsed.data.sessionId,
|
|
83
|
+
sliceList: parsed.data.sliceList,
|
|
84
|
+
parallelismHint: parsed.data.parallelismHint,
|
|
85
|
+
exitPolicy: parsed.data.exitPolicy,
|
|
86
|
+
mainLoopStrategy: parsed.data.mainLoopStrategy,
|
|
87
|
+
rotateEvery: parsed.data.rotateEvery,
|
|
88
|
+
});
|
|
89
|
+
try {
|
|
90
|
+
emitJobEvent({ kind: 'job-started', jobId: state.jobId, total: state.slices.length, strategy: state.mainLoopStrategy });
|
|
91
|
+
}
|
|
92
|
+
catch (e) {
|
|
93
|
+
// best-effort: event emission failures must not abort job init
|
|
94
|
+
void e;
|
|
95
|
+
}
|
|
96
|
+
printResult(io, ok('init', { jobId: state.jobId, sliceCount: state.slices.length, statePath: `${jobRoot.rootDir}/${state.jobId}/state.json` }), opts);
|
|
97
|
+
});
|
|
98
|
+
addJsonOption(job.commands.find(c => c.name() === 'init'));
|
|
99
|
+
job
|
|
100
|
+
.command('status')
|
|
101
|
+
.requiredOption('--job-id <jid>')
|
|
102
|
+
.option('--watch', 'poll every 3s')
|
|
103
|
+
.option('--show-cost', 'overlay cost from peaks budget')
|
|
104
|
+
.option('--project <repo>')
|
|
105
|
+
.action(async (opts) => {
|
|
106
|
+
const store = new JobStateStore(resolveJobStateRoot(opts).rootDir);
|
|
107
|
+
const orch = new JobOrchestrator(store);
|
|
108
|
+
const s = orch.status(opts.jobId);
|
|
109
|
+
if (opts.watch) {
|
|
110
|
+
const draw = () => {
|
|
111
|
+
const bar = `[${'='.repeat(s.done)}${' '.repeat(s.total - s.done)}]`;
|
|
112
|
+
process.stdout.write(`\rjob ${opts.jobId}: ${bar} ${s.done}/${s.total}${s.currentSlice ? ` next=${s.currentSlice}` : ''} `);
|
|
113
|
+
};
|
|
114
|
+
draw();
|
|
115
|
+
const iv = setInterval(() => { const u = orch.status(opts.jobId); Object.assign(s, u); draw(); if (u.done + u.failed + u.skipped + u.blocked >= u.total)
|
|
116
|
+
clearInterval(iv); }, 3000);
|
|
117
|
+
process.on('SIGINT', () => { clearInterval(iv); process.stdout.write('\n'); process.exit(0); });
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
emitJobEvent({ kind: 'job-progress', jobId: opts.jobId, done: s.done, total: s.total, ...(s.currentSlice ? { currentSlice: s.currentSlice } : {}) });
|
|
122
|
+
}
|
|
123
|
+
catch (e) {
|
|
124
|
+
// best-effort: event emission failures must not abort job status
|
|
125
|
+
void e;
|
|
126
|
+
}
|
|
127
|
+
printResult(io, ok('status', s), opts);
|
|
128
|
+
});
|
|
129
|
+
addJsonOption(job.commands.find(c => c.name() === 'status'));
|
|
130
|
+
// M4.2: wire rotate-now to JobRotation (session-rotate callbacks are stubs pending M6.5 batch-fix).
|
|
131
|
+
job.command('rotate-now')
|
|
132
|
+
.requiredOption('--job-id <jid>')
|
|
133
|
+
.option('--project <repo>')
|
|
134
|
+
.action(async (opts) => {
|
|
135
|
+
const store = new JobStateStore(resolveJobStateRoot(opts).rootDir);
|
|
136
|
+
const rotation = new JobRotation(store, async (_jid) => { /* delegate to peaks session rotate — implementation wired in M6.5 batch-fix */ return { rotated: true }; }, async (jid) => ({ jobId: jid, cycle: 0 }));
|
|
137
|
+
const r = await rotation.rotateNow(opts.jobId);
|
|
138
|
+
printResult(io, ok('rotate-now', r), opts);
|
|
139
|
+
});
|
|
140
|
+
addJsonOption(job.commands.find(c => c.name() === 'rotate-now'));
|
|
141
|
+
job.command('subagent-cleanup')
|
|
142
|
+
.requiredOption('--job-id <jid>')
|
|
143
|
+
.requiredOption('--batch-id <bid>')
|
|
144
|
+
.option('--force')
|
|
145
|
+
.option('--project <repo>')
|
|
146
|
+
.action(async (opts) => {
|
|
147
|
+
const wrapper = new SubAgentJobWrapper(new JobStateStore(resolveJobStateRoot(opts).rootDir), async () => ({ batchId: opts.batchId }));
|
|
148
|
+
const r = await wrapper.cleanup({ jobId: opts.jobId, batchId: opts.batchId, force: !!opts.force });
|
|
149
|
+
printResult(io, ok('subagent-cleanup', r), opts);
|
|
150
|
+
});
|
|
151
|
+
addJsonOption(job.commands.find(c => c.name() === 'subagent-cleanup'));
|
|
152
|
+
// M3.2: wire the remaining 5 subcommand slots — block, checkpoint, continue, handoff, resume.
|
|
153
|
+
job
|
|
154
|
+
.command('checkpoint')
|
|
155
|
+
.requiredOption('--job-id <jid>')
|
|
156
|
+
.requiredOption('--slice-id <rid>')
|
|
157
|
+
.requiredOption('--state <done|failed|skipped>')
|
|
158
|
+
.option('--commit-sha <sha>')
|
|
159
|
+
.option('--reason <text>')
|
|
160
|
+
.option('--project <repo>')
|
|
161
|
+
.action(async (opts) => {
|
|
162
|
+
const parsed = JobCheckpointInputSchema.safeParse({
|
|
163
|
+
jobId: opts.jobId, sliceId: opts.sliceId, state: opts.state,
|
|
164
|
+
commitSha: opts.commitSha, reason: opts.reason,
|
|
165
|
+
project: projectRoot(opts), json: opts.json,
|
|
166
|
+
});
|
|
167
|
+
if (!parsed.success)
|
|
168
|
+
return printResult(io, fail('checkpoint', 'INVALID_CHECKPOINT', parsed.error.message, {}), opts);
|
|
169
|
+
const store = new JobStateStore(resolveJobStateRoot(opts).rootDir);
|
|
170
|
+
const orch = new JobOrchestrator(store);
|
|
171
|
+
if (parsed.data.state === 'done') {
|
|
172
|
+
await orch.checkpointDone({ jobId: parsed.data.jobId, sliceId: parsed.data.sliceId, ...(parsed.data.commitSha ? { commitSha: parsed.data.commitSha } : {}) });
|
|
173
|
+
}
|
|
174
|
+
else if (parsed.data.state === 'skipped') {
|
|
175
|
+
await orch.checkpointSkipped({ jobId: parsed.data.jobId, sliceId: parsed.data.sliceId, reason: parsed.data.reason });
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
await orch.checkpointFailed({ jobId: parsed.data.jobId, sliceId: parsed.data.sliceId, reason: parsed.data.reason });
|
|
179
|
+
}
|
|
180
|
+
printResult(io, ok('checkpoint', { sliceId: parsed.data.sliceId, status: parsed.data.state }), opts);
|
|
181
|
+
});
|
|
182
|
+
addJsonOption(job.commands.find(c => c.name() === 'checkpoint'));
|
|
183
|
+
job
|
|
184
|
+
.command('block')
|
|
185
|
+
.requiredOption('--job-id <jid>')
|
|
186
|
+
.requiredOption('--slice-id <rid>')
|
|
187
|
+
.requiredOption('--reason <text>')
|
|
188
|
+
.option('--project <repo>')
|
|
189
|
+
.action(async (opts) => {
|
|
190
|
+
const parsed = JobBlockInputSchema.safeParse({
|
|
191
|
+
jobId: opts.jobId, sliceId: opts.sliceId, reason: opts.reason,
|
|
192
|
+
project: projectRoot(opts), json: opts.json,
|
|
193
|
+
});
|
|
194
|
+
if (!parsed.success)
|
|
195
|
+
return printResult(io, fail('block', 'INVALID_BLOCK', parsed.error.message, {}), opts);
|
|
196
|
+
const store = new JobStateStore(resolveJobStateRoot(opts).rootDir);
|
|
197
|
+
const orch = new JobOrchestrator(store);
|
|
198
|
+
await orch.blockSlice(parsed.data);
|
|
199
|
+
printResult(io, ok('block', { blocked: parsed.data.sliceId, reason: parsed.data.reason }), opts);
|
|
200
|
+
});
|
|
201
|
+
addJsonOption(job.commands.find(c => c.name() === 'block'));
|
|
202
|
+
job
|
|
203
|
+
.command('continue')
|
|
204
|
+
.requiredOption('--job-id <jid>')
|
|
205
|
+
.option('--project <repo>')
|
|
206
|
+
.action(async (opts) => {
|
|
207
|
+
const store = new JobStateStore(resolveJobStateRoot(opts).rootDir);
|
|
208
|
+
const orch = new JobOrchestrator(store);
|
|
209
|
+
const r = orch.continueNow(opts.jobId);
|
|
210
|
+
printResult(io, ok('continue', r), opts);
|
|
211
|
+
});
|
|
212
|
+
addJsonOption(job.commands.find(c => c.name() === 'continue'));
|
|
213
|
+
job
|
|
214
|
+
.command('resume')
|
|
215
|
+
.requiredOption('--job-id <jid>')
|
|
216
|
+
.option('--project <repo>')
|
|
217
|
+
.action(async (opts) => {
|
|
218
|
+
const store = new JobStateStore(resolveJobStateRoot(opts).rootDir);
|
|
219
|
+
const orch = new JobOrchestrator(store);
|
|
220
|
+
const s = orch.status(opts.jobId);
|
|
221
|
+
printResult(io, ok('resume', { resumed: opts.jobId, ...s }), opts);
|
|
222
|
+
});
|
|
223
|
+
addJsonOption(job.commands.find(c => c.name() === 'resume'));
|
|
224
|
+
job
|
|
225
|
+
.command('handoff')
|
|
226
|
+
.requiredOption('--job-id <jid>')
|
|
227
|
+
.option('--project <repo>')
|
|
228
|
+
.action(async (opts) => {
|
|
229
|
+
const store = new JobStateStore(resolveJobStateRoot(opts).rootDir);
|
|
230
|
+
const orch = new JobOrchestrator(store);
|
|
231
|
+
const s = orch.status(opts.jobId);
|
|
232
|
+
printResult(io, ok('handoff', { handoffFor: opts.jobId, ...s }), opts);
|
|
233
|
+
});
|
|
234
|
+
addJsonOption(job.commands.find(c => c.name() === 'handoff'));
|
|
235
|
+
program.addCommand(job);
|
|
236
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `peaks session auto-compact-hook` — slice 2026-07-02-auto-compact-zero-pause.
|
|
3
|
+
*
|
|
4
|
+
* The Bash entrypoint the PreToolUse hook fires on every Bash/Task
|
|
5
|
+
* tool call from the Claude Code runner. Behaviour:
|
|
6
|
+
*
|
|
7
|
+
* 1. Read `CLAUDE_CONTEXT_USAGE_PERCENT` (or any adapter-declared
|
|
8
|
+
* env-var via the registry — claude-code MVP). If ratio is below
|
|
9
|
+
* the red-line threshold (0.95), exit 0 silent — the runner's
|
|
10
|
+
* normal flow continues.
|
|
11
|
+
* 2. If ratio is ≥ 0.95 (red line), write a one-line stderr hint
|
|
12
|
+
* so the human sees what happened, then in-band spawn
|
|
13
|
+
* `claude --compact` so the **current** runner's context
|
|
14
|
+
* window collapses (not a new claude process).
|
|
15
|
+
* 3. Always exit 0. The hook is a probe, not a gate — the gate is
|
|
16
|
+
* `claude --compact` itself returning 0.
|
|
17
|
+
*
|
|
18
|
+
* Idempotency: re-running the hook is safe; if `claude --compact`
|
|
19
|
+
* is already in flight, the new process just exits 0 immediately
|
|
20
|
+
* (Claude Code's compact is a one-shot operation against the
|
|
21
|
+
* current runner).
|
|
22
|
+
*
|
|
23
|
+
* Why a CLI subcommand instead of an inline JS hook:
|
|
24
|
+
* - Keeps the hook payload in `.claude/settings.local.json` small
|
|
25
|
+
* (one line: `peaks session auto-compact-hook`).
|
|
26
|
+
* - Lets the heavy lifting (ratio parsing, IDE-aware env-var
|
|
27
|
+
* resolution, spawning `claude --compact`) live in TypeScript
|
|
28
|
+
* where it's unit-testable.
|
|
29
|
+
* - Discoverable via `peaks session --help` — future LLM sessions
|
|
30
|
+
* see the primitive exists.
|
|
31
|
+
*
|
|
32
|
+
* Karpathy §2: 50 lines, no abstractions, no speculative error
|
|
33
|
+
* handling. The hook command is a single-purpose muscle; if it
|
|
34
|
+
* grows beyond ~80 lines, that's a signal to split.
|
|
35
|
+
*/
|
|
36
|
+
import type { Command } from 'commander';
|
|
37
|
+
export declare function registerSessionAutoCompactHookCommand(session: Command): void;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `peaks session auto-compact-hook` — slice 2026-07-02-auto-compact-zero-pause.
|
|
3
|
+
*
|
|
4
|
+
* The Bash entrypoint the PreToolUse hook fires on every Bash/Task
|
|
5
|
+
* tool call from the Claude Code runner. Behaviour:
|
|
6
|
+
*
|
|
7
|
+
* 1. Read `CLAUDE_CONTEXT_USAGE_PERCENT` (or any adapter-declared
|
|
8
|
+
* env-var via the registry — claude-code MVP). If ratio is below
|
|
9
|
+
* the red-line threshold (0.95), exit 0 silent — the runner's
|
|
10
|
+
* normal flow continues.
|
|
11
|
+
* 2. If ratio is ≥ 0.95 (red line), write a one-line stderr hint
|
|
12
|
+
* so the human sees what happened, then in-band spawn
|
|
13
|
+
* `claude --compact` so the **current** runner's context
|
|
14
|
+
* window collapses (not a new claude process).
|
|
15
|
+
* 3. Always exit 0. The hook is a probe, not a gate — the gate is
|
|
16
|
+
* `claude --compact` itself returning 0.
|
|
17
|
+
*
|
|
18
|
+
* Idempotency: re-running the hook is safe; if `claude --compact`
|
|
19
|
+
* is already in flight, the new process just exits 0 immediately
|
|
20
|
+
* (Claude Code's compact is a one-shot operation against the
|
|
21
|
+
* current runner).
|
|
22
|
+
*
|
|
23
|
+
* Why a CLI subcommand instead of an inline JS hook:
|
|
24
|
+
* - Keeps the hook payload in `.claude/settings.local.json` small
|
|
25
|
+
* (one line: `peaks session auto-compact-hook`).
|
|
26
|
+
* - Lets the heavy lifting (ratio parsing, IDE-aware env-var
|
|
27
|
+
* resolution, spawning `claude --compact`) live in TypeScript
|
|
28
|
+
* where it's unit-testable.
|
|
29
|
+
* - Discoverable via `peaks session --help` — future LLM sessions
|
|
30
|
+
* see the primitive exists.
|
|
31
|
+
*
|
|
32
|
+
* Karpathy §2: 50 lines, no abstractions, no speculative error
|
|
33
|
+
* handling. The hook command is a single-purpose muscle; if it
|
|
34
|
+
* grows beyond ~80 lines, that's a signal to split.
|
|
35
|
+
*/
|
|
36
|
+
import { spawn } from 'node:child_process';
|
|
37
|
+
import { detectIdeFromEnv } from '../../services/context/main-session-monitor.js';
|
|
38
|
+
import { getAdapter } from '../../services/ide/ide-registry.js';
|
|
39
|
+
/**
|
|
40
|
+
* Red-line ratio threshold. Must match the orchestrator's
|
|
41
|
+
* AUTO_COMPACT_RED_LINE_RATIO constant (0.95). Kept here as a
|
|
42
|
+
* literal so the hook command stays self-contained — a future
|
|
43
|
+
* slice may move this to a shared constant module if the value
|
|
44
|
+
* starts drifting.
|
|
45
|
+
*/
|
|
46
|
+
const RED_LINE_RATIO = 0.95;
|
|
47
|
+
/**
|
|
48
|
+
* Parse `CLAUDE_CONTEXT_USAGE_PERCENT` (or the adapter-declared
|
|
49
|
+
* env-var) into a ratio in [0, 1]. Returns null on missing /
|
|
50
|
+
* malformed input. Mirrors `readEnvPercent` in
|
|
51
|
+
* `auto-compact-reader.ts` so the hook command and the
|
|
52
|
+
* orchestrator agree on parsing semantics.
|
|
53
|
+
*/
|
|
54
|
+
function readRatioFromEnv(env) {
|
|
55
|
+
const detected = detectIdeFromEnv(env);
|
|
56
|
+
const ideId = (detected === 'unknown' ? 'claude-code' : detected);
|
|
57
|
+
const adapter = getAdapter(ideId);
|
|
58
|
+
if (!adapter.compact)
|
|
59
|
+
return null;
|
|
60
|
+
const varName = adapter.compact.envVarForContextPercent;
|
|
61
|
+
const raw = env[varName];
|
|
62
|
+
if (typeof raw !== 'string' || raw.length === 0)
|
|
63
|
+
return null;
|
|
64
|
+
const parsed = Number.parseFloat(raw);
|
|
65
|
+
if (!Number.isFinite(parsed))
|
|
66
|
+
return null;
|
|
67
|
+
// Allow either "0..1" or "0..100" encoding. Claude Code writes
|
|
68
|
+
// "0..1" today; statusline tools sometimes write "0..100". Normalize.
|
|
69
|
+
const ratio = parsed > 1.5 ? parsed / 100 : Math.max(0, Math.min(1, parsed));
|
|
70
|
+
return { ratio, varName };
|
|
71
|
+
}
|
|
72
|
+
export function registerSessionAutoCompactHookCommand(session) {
|
|
73
|
+
session
|
|
74
|
+
.command('auto-compact-hook')
|
|
75
|
+
.description('PreToolUse hook entrypoint — read CLAUDE_CONTEXT_USAGE_PERCENT and, ' +
|
|
76
|
+
'at ratio ≥ 0.95, in-band spawn `claude --compact` against the current ' +
|
|
77
|
+
'runner. Designed for invocation from `.claude/settings.local.json` ' +
|
|
78
|
+
'PreToolUse hooks; safe to re-run.')
|
|
79
|
+
.action(() => {
|
|
80
|
+
const probe = readRatioFromEnv(process.env);
|
|
81
|
+
if (probe === null) {
|
|
82
|
+
// No signal → exit 0 silent. The hook must never block the
|
|
83
|
+
// runner's normal flow on a missing env-var.
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (probe.ratio < RED_LINE_RATIO) {
|
|
87
|
+
// Below red line → exit 0 silent. The orchestrator's
|
|
88
|
+
// pre-compact zone (0.85–0.95) is handled out-of-band by
|
|
89
|
+
// the LLM firing `peaks solo auto-compact --execute`; the
|
|
90
|
+
// hook only fires on the synchronous red-line path.
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
// Red line reached — compact the current runner in-band.
|
|
94
|
+
// The `detached: true` + `unref()` pair lets the hook return
|
|
95
|
+
// immediately while Claude Code's own slash-command processor
|
|
96
|
+
// picks up `claude --compact` against the runner's session.
|
|
97
|
+
// This is the fix for the shell-exec-spawns-a-child-process
|
|
98
|
+
// bug documented in `.peaks/memory/2026-06-27-auto-compact-design.md:139-152`.
|
|
99
|
+
//
|
|
100
|
+
// Dogfood 2026-07-02 fix: when `claude` is not on PATH (the
|
|
101
|
+
// user runs Claude Code as an MCP rather than via the CLI
|
|
102
|
+
// binary), `spawn` raises ENOENT. Catching this MUST NOT crash
|
|
103
|
+
// the runner's tool call — the hook must exit 0 silently with
|
|
104
|
+
// a one-line stderr hint so the runner keeps working. The user
|
|
105
|
+
// can then run `/compact` manually (or `peaks solo auto-compact`
|
|
106
|
+
// from another terminal).
|
|
107
|
+
try {
|
|
108
|
+
const child = spawn('claude', ['--compact'], {
|
|
109
|
+
stdio: 'ignore',
|
|
110
|
+
detached: true,
|
|
111
|
+
env: process.env
|
|
112
|
+
});
|
|
113
|
+
child.on('error', () => {
|
|
114
|
+
// Spawn failed (ENOENT or EPERM). Exit 0 silently with a
|
|
115
|
+
// stderr hint — the hook contract is "never crash the
|
|
116
|
+
// runner's tool call".
|
|
117
|
+
process.stderr.write('[peaks:auto-compact-hook] claude CLI not on PATH; cannot fire in-band. Run `/compact` manually or set up the Claude Code CLI binary.\n');
|
|
118
|
+
});
|
|
119
|
+
child.unref();
|
|
120
|
+
process.stderr.write(`[peaks:auto-compact-hook] ratio=${(probe.ratio * 100).toFixed(1)}% ≥ ${(RED_LINE_RATIO * 100).toFixed(0)}%; firing claude --compact in-band\n`);
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
// Defensive: `spawn` can throw synchronously on some platforms.
|
|
124
|
+
process.stderr.write('[peaks:auto-compact-hook] failed to spawn claude --compact; run `/compact` manually.\n');
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
}
|
package/dist/src/cli/program.js
CHANGED
|
@@ -74,6 +74,8 @@ import { registerObservabilityCommands } from './commands/observability-commands
|
|
|
74
74
|
// CLI primitives. See src/services/compact/ and
|
|
75
75
|
// src/cli/commands/compact-command.ts.
|
|
76
76
|
import { registerCompactCommands } from './commands/compact-command.js';
|
|
77
|
+
// Slice 2026-07-01-job-m3.1: `peaks job init|status|...` — Job orchestration CLI surface.
|
|
78
|
+
import { registerJobCommands } from './commands/job-commands.js';
|
|
77
79
|
import { applyRetention } from '../services/log/retention.js';
|
|
78
80
|
import { writeLogEntry, maybeWriteStderr } from '../services/log/logger.js';
|
|
79
81
|
export { printResult } from './cli-helpers.js';
|
|
@@ -341,5 +343,9 @@ Run peaks (no arguments) for a quickstart. You likely want one of:
|
|
|
341
343
|
// suggest|recommend|survival|dry-run|force` — strategic-compact CLI
|
|
342
344
|
// primitives. See src/cli/commands/compact-command.ts.
|
|
343
345
|
registerCompactCommands(program, io);
|
|
346
|
+
// Slice 2026-07-01-job-m3.1: `peaks job init|status|...` — Job
|
|
347
|
+
// orchestration CLI surface. M3.1 wires init + status + 2 stubs;
|
|
348
|
+
// the remaining 5 subcommands land in M3.2.
|
|
349
|
+
registerJobCommands(program, io);
|
|
344
350
|
return program;
|
|
345
351
|
}
|
|
@@ -100,16 +100,36 @@ export async function dispatchIdeCompact(input) {
|
|
|
100
100
|
timeoutMs
|
|
101
101
|
});
|
|
102
102
|
case 'ide-native':
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
103
|
+
// Slice 2026-07-02-auto-compact-zero-pause: write the auto-compact
|
|
104
|
+
// PreToolUse hook into `.claude/settings.local.json`. The hook
|
|
105
|
+
// command (`peaks session auto-compact-hook`) reads
|
|
106
|
+
// `CLAUDE_CONTEXT_USAGE_PERCENT` on every subsequent Bash/Task
|
|
107
|
+
// tool call from the runner and, at ratio ≥ 0.95, in-band spawns
|
|
108
|
+
// `claude --compact` against the CURRENT runner (not a child
|
|
109
|
+
// process — the bug documented in
|
|
110
|
+
// `.peaks/memory/2026-06-27-auto-compact-design.md:139-152`).
|
|
111
|
+
//
|
|
112
|
+
// `ide-native` is ONLY for the main-session runner. When the
|
|
113
|
+
// caller is a sub-agent shell (which has its own ephemeral
|
|
114
|
+
// context window — not the runner's), fall through to
|
|
115
|
+
// shell-exec so the sub-agent's `compactCommand` spawns a
|
|
116
|
+
// child claude process and the sub-agent's own runner doesn't
|
|
117
|
+
// get a PreToolUse hook installed in the wrong place.
|
|
118
|
+
if (target === 'sub-agent') {
|
|
119
|
+
return await dispatchShellExec({
|
|
120
|
+
ideId,
|
|
121
|
+
command: profile.compactCommand,
|
|
122
|
+
timeoutMs
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
// Lazy install: we only get here when the caller explicitly
|
|
126
|
+
// invokes `peaks solo auto-compact --execute`, so the user has
|
|
127
|
+
// already opted in. No zero-touch surprise on workspace init.
|
|
128
|
+
return await dispatchIdeNativeHook({
|
|
129
|
+
projectRoot: input.projectRoot,
|
|
130
|
+
sessionId: input.sessionId,
|
|
131
|
+
target
|
|
132
|
+
});
|
|
113
133
|
case 'llm-self-compress':
|
|
114
134
|
return {
|
|
115
135
|
ok: true,
|
|
@@ -188,3 +208,33 @@ function dispatchShellExec(input) {
|
|
|
188
208
|
});
|
|
189
209
|
});
|
|
190
210
|
}
|
|
211
|
+
/**
|
|
212
|
+
* Slice 2026-07-02-auto-compact-zero-pause: implement the
|
|
213
|
+
* `ide-native` pathway. Writes the auto-compact PreToolUse hook
|
|
214
|
+
* into `.claude/settings.local.json` (idempotent; the install
|
|
215
|
+
* service is a no-op if the hook is already present). On the next
|
|
216
|
+
* Bash/Task tool call from the runner, the hook fires
|
|
217
|
+
* `peaks session auto-compact-hook` which in-band spawns
|
|
218
|
+
* `claude --compact` against the CURRENT runner session.
|
|
219
|
+
*
|
|
220
|
+
* Returns `ok: true, pathway: 'ide-native'` regardless of install
|
|
221
|
+
* action (`installed` vs `already-installed`) — both states
|
|
222
|
+
* achieve the operational goal: the hook is wired and the next
|
|
223
|
+
* Bash call will trigger it.
|
|
224
|
+
*/
|
|
225
|
+
async function dispatchIdeNativeHook(input) {
|
|
226
|
+
// Lazy dynamic import — matches the existing pattern in
|
|
227
|
+
// `runAutoCompact` for `auto-compact-reader.ts` (line 311) and
|
|
228
|
+
// avoids a static cycle if future slices add cross-imports
|
|
229
|
+
// between dispatcher and hook-install.
|
|
230
|
+
const { installAutoCompactHook } = await import('../hooks/auto-compact-hook-install.js');
|
|
231
|
+
const result = installAutoCompactHook({ projectRoot: input.projectRoot });
|
|
232
|
+
return {
|
|
233
|
+
ok: true,
|
|
234
|
+
ide: 'claude-code',
|
|
235
|
+
pathway: 'ide-native',
|
|
236
|
+
message: result.action === 'installed'
|
|
237
|
+
? `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%.`
|
|
238
|
+
: `Auto-compact PreToolUse hook already installed at ${result.settingsPath}; next Bash/Task tool call will trigger compact in-band at ratio ≥ 95%.`
|
|
239
|
+
};
|
|
240
|
+
}
|
|
@@ -56,6 +56,18 @@ export declare function detectIdeFromEnv(env?: NodeJS.ProcessEnv): IdeKind;
|
|
|
56
56
|
* Threshold check. Pure function (no IO). `capacityBytes` is injectable
|
|
57
57
|
* for tests and future per-IDE capacity overrides. Defaults to the
|
|
58
58
|
* G9 256K proxy (matches G9 for cognitive continuity).
|
|
59
|
+
*
|
|
60
|
+
* @deprecated Slice 2026-07-02-auto-compact-zero-pause: the
|
|
61
|
+
* 50/75/90 tier thresholds pre-date the v2.13.0 auto-compact
|
|
62
|
+
* design. The authoritative tier table for triggering compaction
|
|
63
|
+
* now lives in `evaluateCompactTrigger` (auto-compact-orchestrator.ts)
|
|
64
|
+
* with the 0.85 pre-compact / 0.95 red-line thresholds. New
|
|
65
|
+
* callers should call `evaluateCompactTrigger(ratio)` directly.
|
|
66
|
+
* This function is retained because legacy `peaks context check
|
|
67
|
+
* --prompt-size` still emits its 4-tier envelope for callers that
|
|
68
|
+
* need the soft signal (e.g. statusline display). Migration is
|
|
69
|
+
* scheduled for v2.15.0; the @deprecated tag flips callers'
|
|
70
|
+
* eslint warnings so the migration is mechanical.
|
|
59
71
|
*/
|
|
60
72
|
export declare function evaluateMainSessionThreshold(promptSize: number, capacityBytes?: number): MainSessionEvaluation;
|
|
61
73
|
export interface InFlightBatchProbe {
|