peaks-loop 4.0.3 → 4.0.5
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 +55 -0
- package/README-en.md +26 -0
- package/README.md +26 -0
- package/dist/cli/commands/_register.js +3 -0
- package/dist/cli/commands/code-runtime-commands.js +15 -2
- package/dist/cli/commands/core/skill-command.js +2 -0
- package/dist/cli/commands/dispatch-commands.js +41 -2
- package/dist/cli/commands/e2e-verify.d.ts +47 -0
- package/dist/cli/commands/e2e-verify.js +211 -0
- package/dist/cli/commands/gate-commands.js +17 -4
- package/dist/cli/commands/statusline-commands.d.ts +6 -0
- package/dist/cli/commands/statusline-commands.js +48 -11
- package/dist/cli/commands/sub-agent-shutdown-commands.d.ts +31 -0
- package/dist/cli/commands/sub-agent-shutdown-commands.js +110 -0
- package/dist/cli/commands/worktree-auth-commands.js +27 -0
- package/dist/services/code/auto-compact-orchestrator.d.ts +39 -0
- package/dist/services/code/auto-compact-orchestrator.js +358 -41
- package/dist/services/code/post-compact-detector.js +12 -4
- package/dist/services/code/step-08-gate.js +6 -2
- package/dist/services/compact-statusline/compact-lifecycle-store.d.ts +68 -0
- package/dist/services/compact-statusline/compact-lifecycle-store.js +222 -0
- package/dist/services/compact-statusline/compact-statusline-service.d.ts +25 -5
- package/dist/services/compact-statusline/compact-statusline-service.js +244 -54
- package/dist/services/context/auto-compact-reader.d.ts +33 -4
- package/dist/services/context/auto-compact-reader.js +77 -22
- package/dist/services/context/auto-compact-types.d.ts +10 -2
- package/dist/services/context/build-dispatch-system-prompt.d.ts +26 -0
- package/dist/services/context/build-dispatch-system-prompt.js +33 -2
- package/dist/services/dispatch/conflict-replay.d.ts +32 -0
- package/dist/services/dispatch/conflict-replay.js +44 -0
- package/dist/services/dispatch/dispatch-record-writer.d.ts +31 -1
- package/dist/services/dispatch/dispatch-record-writer.js +27 -7
- package/dist/services/dispatch/e2e-fixtures.d.ts +18 -0
- package/dist/services/dispatch/e2e-fixtures.js +72 -0
- package/dist/services/dispatch/merge-back-runner.d.ts +18 -0
- package/dist/services/dispatch/merge-back-runner.js +83 -0
- package/dist/services/dispatch/post-merge.d.ts +21 -0
- package/dist/services/dispatch/post-merge.js +13 -0
- package/dist/services/dispatch/service-shutdown.d.ts +22 -0
- package/dist/services/dispatch/service-shutdown.js +51 -0
- package/dist/services/hooks/presence-marker-detector.js +12 -4
- package/dist/services/hooks/worktree-authorization-gate.d.ts +4 -0
- package/dist/services/hooks/worktree-authorization-gate.js +22 -0
- package/dist/services/skills/skill-statusline-renderer.d.ts +86 -3
- package/dist/services/skills/skill-statusline-renderer.js +374 -29
- package/dist/services/skills/skill-statusline-service.d.ts +2 -0
- package/dist/services/skills/skill-statusline-service.js +34 -5
- package/dist/services/skills/sync-service.d.ts +3 -0
- package/dist/services/skills/sync-service.js +1 -0
- package/dist/services/standards/project-context.d.ts +2 -1
- package/dist/services/standards/project-context.js +17 -2
- package/dist/services/standards/project-standards-service.d.ts +9 -0
- package/dist/services/standards/project-standards-service.js +26 -0
- package/dist/services/worktree/dispatch-provenance.d.ts +25 -0
- package/dist/services/worktree/dispatch-provenance.js +47 -0
- package/dist/services/worktree/git-worktree-parser.d.ts +8 -0
- package/dist/services/worktree/git-worktree-parser.js +45 -0
- package/dist/services/worktree/host-worktree-reconciler.d.ts +15 -0
- package/dist/services/worktree/host-worktree-reconciler.js +39 -0
- package/dist/services/worktree/long-path-cleanup.d.ts +10 -0
- package/dist/services/worktree/long-path-cleanup.js +38 -0
- package/dist/services/worktree/playwright-profile.d.ts +8 -0
- package/dist/services/worktree/playwright-profile.js +23 -0
- package/dist/services/worktree/worktree-gc-guard.d.ts +8 -0
- package/dist/services/worktree/worktree-gc-guard.js +38 -0
- package/package.json +4 -4
- package/scripts/install-skills.mjs +11 -2
|
@@ -2,7 +2,7 @@ import { fail, ok } from 'peaks-loop-shared/result';
|
|
|
2
2
|
import { addJsonOption, printResult, getErrorMessage } from '../cli-helpers.js';
|
|
3
3
|
import { findProjectRoot } from '../../services/config/config-safety.js';
|
|
4
4
|
import { buildStatusLineModel, parseStatusLineStdin } from '../../services/skills/skill-statusline-service.js';
|
|
5
|
-
import { renderStatusLine } from '../../services/skills/skill-statusline-renderer.js';
|
|
5
|
+
import { renderStatusLine, resolveStatusLineCapability, } from '../../services/skills/skill-statusline-renderer.js';
|
|
6
6
|
import { applyStatusLineInstall, planStatusLineInstall, removeStatusLineInstall } from '../../services/skills/statusline-settings-service.js';
|
|
7
7
|
import { readHookStatus as readSettingsStatus } from '../../services/skills/hooks-settings-service.js';
|
|
8
8
|
import { detectIdeFromContext } from '../../services/ide/hook-translator.js';
|
|
@@ -61,6 +61,12 @@ function resolveIdeForCommand(options, projectRoot) {
|
|
|
61
61
|
* action (Bug-02 dispatch) and the explicit `render` subcommand. Exported
|
|
62
62
|
* so a unit test can exercise the JSON / text output paths without going
|
|
63
63
|
* through commander.
|
|
64
|
+
*
|
|
65
|
+
* Capability resolution is delegated to {@link resolveStatusLineCapability}
|
|
66
|
+
* with only `env` and `isTTY` — the env-driven path is the single first-
|
|
67
|
+
* version source of truth. No CLI flag widens the surface. The model
|
|
68
|
+
* itself is read-only: `buildStatusLineModel` is the sole I/O boundary,
|
|
69
|
+
* and it now also resolves compact state.
|
|
64
70
|
*/
|
|
65
71
|
export async function runDefaultStatuslineRender(options, io) {
|
|
66
72
|
const raw = await readStdin();
|
|
@@ -69,7 +75,11 @@ export async function runDefaultStatuslineRender(options, io) {
|
|
|
69
75
|
? { ...(stdin ?? {}), workspace: { current_dir: options.project } }
|
|
70
76
|
: stdin;
|
|
71
77
|
const model = buildStatusLineModel(seeded, Date.now());
|
|
72
|
-
const
|
|
78
|
+
const capability = resolveStatusLineCapability({
|
|
79
|
+
env: process.env,
|
|
80
|
+
isTTY: Boolean(process.stdout.isTTY),
|
|
81
|
+
});
|
|
82
|
+
const text = renderStatusLine(model, { capability });
|
|
73
83
|
if (options.json === true) {
|
|
74
84
|
io.stdout(JSON.stringify({ ok: true, command: 'statusline.render', data: { text } }, null, 2));
|
|
75
85
|
return;
|
|
@@ -193,19 +203,46 @@ export function registerStatusLineCommands(program, io) {
|
|
|
193
203
|
}
|
|
194
204
|
});
|
|
195
205
|
// peaks statusline compact (slice 2026-07-30-compact-visibility)
|
|
206
|
+
//
|
|
207
|
+
// --session-id contract: this flag is registered on the compact
|
|
208
|
+
// subcommand but NOT on the default `peaks statusline` render path.
|
|
209
|
+
// The flag is the internal-supported surface for the QA / LLM layer
|
|
210
|
+
// (the integration test, the QA criteria, and any future
|
|
211
|
+
// peaks-loop-internal tool that wants to read a non-canonical session's
|
|
212
|
+
// compact state). The IDE consumer (Claude Code) does NOT need this
|
|
213
|
+
// flag — its primary-line path binds to the canonical session via
|
|
214
|
+
// `getSessionIdCanonical(projectRoot)`. Documenting both paths here
|
|
215
|
+
// keeps the surface small and consistent: the compact subcommand is
|
|
216
|
+
// the "I have a specific session id in mind" surface; the default
|
|
217
|
+
// render is the "use the canonical binding" surface.
|
|
196
218
|
addJsonOption(statusline
|
|
197
219
|
.command('compact')
|
|
198
220
|
.description('Render the single-line auto-compact indicator the LLM embeds ' +
|
|
199
221
|
"in Claude Code's statusline. Default output is plain text; " +
|
|
200
|
-
'pass --json for the structured envelope.'
|
|
222
|
+
'pass --json for the structured envelope. ' +
|
|
223
|
+
'Reads the lifecycle record for the canonical session ' +
|
|
224
|
+
'(`.peaks/_runtime/<sid>/compact-lifecycle.json`); pass ' +
|
|
225
|
+
'--session-id to target a non-canonical session explicitly.')
|
|
201
226
|
.option('--project <path>', 'project root (auto-detected from cwd when omitted)')
|
|
202
|
-
.option('--session-id <sid>', 'override the active session id (defaults to the canonical binding)')
|
|
203
|
-
.action((options) => {
|
|
227
|
+
.option('--session-id <sid>', 'override the active session id (defaults to the canonical binding; supported for QA / internal tooling)')
|
|
228
|
+
.action((options, command) => {
|
|
229
|
+
// Re-resolve options via `command.optsWithGlobals()` (Commander 12.x
|
|
230
|
+
// compatibility): when the parent `statusline` command has its own
|
|
231
|
+
// `--json` registered AND the user types `peaks statusline compact
|
|
232
|
+
// --json`, commander's parent-vs-child option parser can land
|
|
233
|
+
// `--json` on the parent scope only. `optsWithGlobals()` merges
|
|
234
|
+
// parent + child options so the JSON branch fires regardless of
|
|
235
|
+
// which scope commander assigned the flag to. Verified by the
|
|
236
|
+
// `compact --json emits the documented envelope` integration test.
|
|
237
|
+
const merged = {
|
|
238
|
+
...options,
|
|
239
|
+
...command.optsWithGlobals?.(),
|
|
240
|
+
};
|
|
204
241
|
try {
|
|
205
|
-
const projectRoot =
|
|
206
|
-
?
|
|
242
|
+
const projectRoot = merged.project !== undefined
|
|
243
|
+
? merged.project
|
|
207
244
|
: (findProjectRoot(process.cwd()) ?? process.cwd());
|
|
208
|
-
const sid =
|
|
245
|
+
const sid = merged.sessionId ?? getSessionIdCanonical(projectRoot) ?? null;
|
|
209
246
|
const state = decideCompactStatusline({
|
|
210
247
|
projectRoot,
|
|
211
248
|
sessionId: sid,
|
|
@@ -215,8 +252,8 @@ export function registerStatusLineCommands(program, io) {
|
|
|
215
252
|
// For non-JSON mode we print the LABEL DIRECTLY to stdout
|
|
216
253
|
// (no envelope wrapping) because the consumer is the IDE
|
|
217
254
|
// statusline, not a peaks-Loop caller.
|
|
218
|
-
if (
|
|
219
|
-
printResult(io, ok('statusline.compact', { label, state }),
|
|
255
|
+
if (merged.json === true) {
|
|
256
|
+
printResult(io, ok('statusline.compact', { label, state }), merged.json);
|
|
220
257
|
}
|
|
221
258
|
else {
|
|
222
259
|
process.stdout.write(`${label}\n`);
|
|
@@ -224,7 +261,7 @@ export function registerStatusLineCommands(program, io) {
|
|
|
224
261
|
}
|
|
225
262
|
catch (error) {
|
|
226
263
|
const message = getErrorMessage(error);
|
|
227
|
-
printResult(io, fail('statusline.compact', 'STATUSLINE_COMPACT_FAILED', message, {}, [message]),
|
|
264
|
+
printResult(io, fail('statusline.compact', 'STATUSLINE_COMPACT_FAILED', message, {}, [message]), merged.json);
|
|
228
265
|
process.exitCode = 1;
|
|
229
266
|
}
|
|
230
267
|
}));
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `peaks sub-agent shutdown register|unregister|list` CLI verbs.
|
|
3
|
+
*
|
|
4
|
+
* Slice 2026-08-01-subagent-merge-and-e2e (Task 6). The sub-agent
|
|
5
|
+
* dispatches a long-lived local process (vite dev, mock API, docker
|
|
6
|
+
* proxy, etc.). Before it exits, it MUST register the process with
|
|
7
|
+
* `peaks sub-agent shutdown register --pid <pid> --name <label>`. The
|
|
8
|
+
* parent session reads the resulting `service-registrations.json`
|
|
9
|
+
* before the merge-back step and best-effort-kills each entry via
|
|
10
|
+
* killRegisteredServices (src/services/dispatch/service-shutdown.ts).
|
|
11
|
+
*
|
|
12
|
+
* The CLI is intentionally tiny: it appends / removes entries from a
|
|
13
|
+
* JSON file at `.peaks/_runtime/<sid>/dispatch/<dispatchId>/service-registrations.json`.
|
|
14
|
+
* The file is the contract between the sub-agent (writer) and the
|
|
15
|
+
* merge-back-runner (reader). No locking is needed — the sub-agent
|
|
16
|
+
* writes at most once before exit, and the parent reads after the
|
|
17
|
+
* sub-agent's process is gone.
|
|
18
|
+
*
|
|
19
|
+
* Skill-first / CLI-auxiliary red line: the user does not invoke
|
|
20
|
+
* these commands directly. They are invoked by the sub-agent (or its
|
|
21
|
+
* own scripts) at teardown.
|
|
22
|
+
*/
|
|
23
|
+
import type { Command } from 'commander';
|
|
24
|
+
import { type ProgramIO } from '../cli-helpers.js';
|
|
25
|
+
/** Re-exported shape — kept here so the CLI owns the on-disk contract. */
|
|
26
|
+
export type ServiceRegistration = {
|
|
27
|
+
readonly pid: number;
|
|
28
|
+
readonly name: string;
|
|
29
|
+
readonly url?: string;
|
|
30
|
+
};
|
|
31
|
+
export declare function registerSubAgentShutdownCommands(program: Command, io: ProgramIO): void;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { fail, getErrorMessage, ok } from 'peaks-loop-shared/result';
|
|
4
|
+
import { getCurrentSessionId } from '../../services/skills/skill-presence-service.js';
|
|
5
|
+
import { printResult } from '../cli-helpers.js';
|
|
6
|
+
const REGISTRATIONS_FILE = 'service-registrations.json';
|
|
7
|
+
function registrationsPath(input) {
|
|
8
|
+
return join(input.projectRoot, '.peaks', '_runtime', input.sessionId, 'dispatch', input.dispatchId, REGISTRATIONS_FILE);
|
|
9
|
+
}
|
|
10
|
+
function readAll(file) {
|
|
11
|
+
if (!existsSync(file))
|
|
12
|
+
return [];
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(readFileSync(file, 'utf8'));
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function writeAll(file, regs) {
|
|
21
|
+
mkdirSync(join(file, '..'), { recursive: true });
|
|
22
|
+
writeFileSync(file, JSON.stringify(regs, null, 2), 'utf8');
|
|
23
|
+
}
|
|
24
|
+
function resolveDispatchId(input) {
|
|
25
|
+
if (typeof input.dispatchId === 'string' && input.dispatchId.length > 0)
|
|
26
|
+
return input.dispatchId;
|
|
27
|
+
return process.env.PEAKS_DISPATCH_ID ?? 'current';
|
|
28
|
+
}
|
|
29
|
+
export function registerSubAgentShutdownCommands(program, io) {
|
|
30
|
+
// The existing `sub-agent-commands.ts` already creates the `sub-agent`
|
|
31
|
+
// parent; this file attaches the `shutdown` subcommand to the same
|
|
32
|
+
// parent via `findCommand` so we do not double-register. Fall back
|
|
33
|
+
// to a standalone `peaks sub-agent-shutdown` parent if the parent
|
|
34
|
+
// is not yet registered (e.g. when this file is loaded in isolation).
|
|
35
|
+
const existing = program.commands.find((c) => c.name() === 'sub-agent');
|
|
36
|
+
const root = existing ?? program.command('sub-agent-shutdown')
|
|
37
|
+
.description('Sub-agent shutdown registration (forensic hook)');
|
|
38
|
+
const shutdown = root
|
|
39
|
+
.command('shutdown')
|
|
40
|
+
.description('Register local services for the parent to kill before merge-back');
|
|
41
|
+
const sessionRoot = process.cwd();
|
|
42
|
+
shutdown
|
|
43
|
+
.command('register')
|
|
44
|
+
.requiredOption('--pid <pid>', 'process id')
|
|
45
|
+
.requiredOption('--name <label>', 'human-readable label (vite / mock-api / etc.)')
|
|
46
|
+
.option('--url <url>', 'optional URL the service exposes')
|
|
47
|
+
.option('--dispatch-id <id>', 'dispatch id; default = PEAKS_DISPATCH_ID env or "current"')
|
|
48
|
+
.action((options) => {
|
|
49
|
+
try {
|
|
50
|
+
const sid = getCurrentSessionId(sessionRoot) ?? 'unknown-sid';
|
|
51
|
+
const file = registrationsPath({
|
|
52
|
+
projectRoot: sessionRoot,
|
|
53
|
+
sessionId: sid,
|
|
54
|
+
dispatchId: resolveDispatchId(options),
|
|
55
|
+
});
|
|
56
|
+
const all = readAll(file);
|
|
57
|
+
const reg = {
|
|
58
|
+
pid: Number(options.pid),
|
|
59
|
+
name: options.name,
|
|
60
|
+
...(options.url !== undefined ? { url: options.url } : {}),
|
|
61
|
+
};
|
|
62
|
+
writeAll(file, [...all, reg]);
|
|
63
|
+
printResult(io, ok('sub-agent.shutdown.register', { file, reg }), options.json);
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
printResult(io, fail('sub-agent.shutdown.register', 'REGISTER_FAILED', getErrorMessage(error), {}, [getErrorMessage(error)]), options.json);
|
|
67
|
+
process.exitCode = 1;
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
shutdown
|
|
71
|
+
.command('unregister')
|
|
72
|
+
.requiredOption('--pid <pid>', 'process id to remove from the registration list')
|
|
73
|
+
.option('--dispatch-id <id>', 'dispatch id; default = PEAKS_DISPATCH_ID env or "current"')
|
|
74
|
+
.action((options) => {
|
|
75
|
+
try {
|
|
76
|
+
const sid = getCurrentSessionId(sessionRoot) ?? 'unknown-sid';
|
|
77
|
+
const file = registrationsPath({
|
|
78
|
+
projectRoot: sessionRoot,
|
|
79
|
+
sessionId: sid,
|
|
80
|
+
dispatchId: resolveDispatchId(options),
|
|
81
|
+
});
|
|
82
|
+
const filtered = readAll(file).filter((r) => r.pid !== Number(options.pid));
|
|
83
|
+
writeAll(file, filtered);
|
|
84
|
+
printResult(io, ok('sub-agent.shutdown.unregister', { file, removed: options.pid }), options.json);
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
printResult(io, fail('sub-agent.shutdown.unregister', 'UNREGISTER_FAILED', getErrorMessage(error), {}, [getErrorMessage(error)]), options.json);
|
|
88
|
+
process.exitCode = 1;
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
shutdown
|
|
92
|
+
.command('list')
|
|
93
|
+
.option('--dispatch-id <id>', 'dispatch id; default = PEAKS_DISPATCH_ID env or "current"')
|
|
94
|
+
.option('--json', 'emit JSON envelope')
|
|
95
|
+
.action((options) => {
|
|
96
|
+
try {
|
|
97
|
+
const sid = getCurrentSessionId(sessionRoot) ?? 'unknown-sid';
|
|
98
|
+
const file = registrationsPath({
|
|
99
|
+
projectRoot: sessionRoot,
|
|
100
|
+
sessionId: sid,
|
|
101
|
+
dispatchId: resolveDispatchId(options),
|
|
102
|
+
});
|
|
103
|
+
printResult(io, ok('sub-agent.shutdown.list', { file, registrations: readAll(file) }), options.json);
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
printResult(io, fail('sub-agent.shutdown.list', 'LIST_FAILED', getErrorMessage(error), {}, [getErrorMessage(error)]), options.json);
|
|
107
|
+
process.exitCode = 1;
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}
|
|
@@ -33,6 +33,7 @@ import { atomicWriteJson } from '../../services/ide/shared/atomic-json.js';
|
|
|
33
33
|
import { emitLeaseEvent } from '../../services/observability/observability-service.js';
|
|
34
34
|
import { deserializeLease, finalizeLease, generateLeaseId, isLeaseActive, isLeaseGcEligible, leaseFilePath, leaseStoreDir, listLeasesSync, markExpired, markGc, markReleased, renewLease, ttlForRole, worktreePath, } from '../../services/worktree/worktree-lease.js';
|
|
35
35
|
import { execSync } from 'node:child_process';
|
|
36
|
+
import { reconcileHostWorktrees } from '../../services/worktree/host-worktree-reconciler.js';
|
|
36
37
|
import { existsSync, readdirSync, readFileSync as readFileSyncNode, statSync } from 'node:fs';
|
|
37
38
|
const DEFAULT_TTL_MS = 5 * 60 * 1_000;
|
|
38
39
|
const ALLOWED_OPERATIONS = [
|
|
@@ -65,6 +66,32 @@ export function registerWorktreeAuthCommand(program, io) {
|
|
|
65
66
|
'The grant is current-task scoped: the LLM must invoke grant after the user has explicitly ' +
|
|
66
67
|
'asked for the operation. The PreToolUse gate fail-closes on missing or expired grants.');
|
|
67
68
|
const auth_ = auth.command('auth').description('Manage worktree authorization grants (granted by the LLM after explicit user opt-in).');
|
|
69
|
+
addJsonOption(auth
|
|
70
|
+
.command('reconcile-host')
|
|
71
|
+
.description('Read-only reconciliation of host-created .claude/worktrees/agent-* against the canonical Peaks lease store.')
|
|
72
|
+
.option('--session <sid>', 'override session id')
|
|
73
|
+
.option('--project <path>', 'project root (default: findProjectRoot(cwd))')
|
|
74
|
+
.option('--host-root <path>', 'override host worktree root')).action((options) => {
|
|
75
|
+
const projectRoot = resolveProjectRoot(options);
|
|
76
|
+
const sessionId = resolveSessionId(options, projectRoot);
|
|
77
|
+
try {
|
|
78
|
+
const result = reconcileHostWorktrees({
|
|
79
|
+
projectRoot,
|
|
80
|
+
sessionId,
|
|
81
|
+
...(options.hostRoot !== undefined ? { hostRoot: options.hostRoot } : {}),
|
|
82
|
+
});
|
|
83
|
+
const warnings = result.unmanaged.length > 0
|
|
84
|
+
? [`${result.unmanaged.length} host worktree(s) are outside Peaks lease governance.`]
|
|
85
|
+
: [];
|
|
86
|
+
printResult(io, ok('worktree.reconcile-host', { ...result, sessionId, projectRoot }, warnings), options.json);
|
|
87
|
+
if (result.unmanaged.length > 0)
|
|
88
|
+
process.exitCode = 1;
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
printResult(io, fail('worktree.reconcile-host', 'HOST_WORKTREE_RECONCILE_FAILED', getErrorMessage(error), { sessionId, projectRoot }, []), options.json);
|
|
92
|
+
process.exitCode = 1;
|
|
93
|
+
}
|
|
94
|
+
});
|
|
68
95
|
addJsonOption(auth_
|
|
69
96
|
.command('grant')
|
|
70
97
|
.description('Append a single grant to the current session\'s worktree authorization file.')
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type CompactTrigger, type ConvergencePlan, type InFlightBatchProbe, type AutoCompactResult } from '../context/auto-compact-types.js';
|
|
2
2
|
import type { CompactTarget } from '../context/auto-compact-dispatcher.js';
|
|
3
3
|
import { type AutoCompactMode } from './auto-compact-modes.js';
|
|
4
|
+
import { type CompactLifecycleRecord, type CompactLifecycleStage } from '../compact-statusline/compact-lifecycle-store.js';
|
|
4
5
|
export interface AutoCompactInput {
|
|
5
6
|
/** Project root for context (default cwd). */
|
|
6
7
|
readonly projectRoot: string;
|
|
@@ -35,6 +36,37 @@ export interface AutoCompactInput {
|
|
|
35
36
|
* `--mode <mode>` overrides the 24h-mode auto-detection.
|
|
36
37
|
*/
|
|
37
38
|
readonly mode?: AutoCompactMode | undefined;
|
|
39
|
+
/**
|
|
40
|
+
* Slice 2026-08-01-compact-lifecycle (Task 5): observer fired on
|
|
41
|
+
* every lifecycle stage this process actually proved. Telemetry
|
|
42
|
+
* only — it can neither change the threshold decision nor the
|
|
43
|
+
* dispatch outcome, and a throwing observer is swallowed.
|
|
44
|
+
*/
|
|
45
|
+
readonly onLifecycleStage?: ((stage: CompactLifecycleStage, record: CompactLifecycleRecord) => void) | undefined;
|
|
46
|
+
/**
|
|
47
|
+
* Test-only injection seams for lifecycle failure paths. NEVER set in
|
|
48
|
+
* production — these exist so the unit suite can drive the `failed`
|
|
49
|
+
* transition and the store-outage branch without mocking the SUT.
|
|
50
|
+
* Each flag toggles a single, well-scoped throw at a documented point:
|
|
51
|
+
* - `failPreparing`: throw inside the checkpoint/plan/recovery phase
|
|
52
|
+
* (so the record rests at `failedAt: 'preparing'`)
|
|
53
|
+
* - `failCompacting`: throw inside the IDE-dispatch phase
|
|
54
|
+
* (so the record rests at `failedAt: 'compacting'`)
|
|
55
|
+
* - `failLifecycleWrite`: make every lifecycle-store write throw
|
|
56
|
+
* (so the compact envelope is proven independent of telemetry)
|
|
57
|
+
*/
|
|
58
|
+
readonly testHooks?: AutoCompactTestHooks | undefined;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Test-only injection seams. The `boolean` shape (vs. an injected error)
|
|
62
|
+
* is deliberate: tests assert the *transition*, not the thrown value —
|
|
63
|
+
* the value would just be a brittle fingerprint. The real error path
|
|
64
|
+
* is still exercised by `summarizeLifecycleError`'s tests.
|
|
65
|
+
*/
|
|
66
|
+
export interface AutoCompactTestHooks {
|
|
67
|
+
readonly failPreparing?: boolean | undefined;
|
|
68
|
+
readonly failCompacting?: boolean | undefined;
|
|
69
|
+
readonly failLifecycleWrite?: boolean | undefined;
|
|
38
70
|
}
|
|
39
71
|
/**
|
|
40
72
|
* Map a context ratio to a `CompactTrigger` action. Pure; the side
|
|
@@ -68,6 +100,13 @@ export declare function evaluateAutoCompactDecision(input: {
|
|
|
68
100
|
force?: boolean | undefined;
|
|
69
101
|
bypassRedLine?: boolean | undefined;
|
|
70
102
|
mode?: AutoCompactMode | undefined;
|
|
103
|
+
/**
|
|
104
|
+
* Source tag from `readContextPercent.source`. Optional — when absent, the
|
|
105
|
+
* function behaves exactly as pre-rid (ratio-only). Slice 2026-07-31-rid-
|
|
106
|
+
* mac-transcript-estimate-trigger uses this to carry forward an explicit
|
|
107
|
+
* carve-out for the Mac-only `transcript-estimate` signal.
|
|
108
|
+
*/
|
|
109
|
+
source?: string | undefined;
|
|
71
110
|
}): {
|
|
72
111
|
shouldCompact: boolean;
|
|
73
112
|
reason: 'below-threshold' | 'in-flight-batch' | 'pre-compact' | 'red-line';
|