peaks-loop 4.0.4 → 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 +34 -0
- package/README-en.md +26 -0
- package/README.md +26 -0
- package/dist/cli/commands/_register.js +3 -0
- 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.js +12 -4
- 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
|
@@ -31,9 +31,10 @@
|
|
|
31
31
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, appendFileSync } from 'node:fs';
|
|
32
32
|
import { dirname, join } from 'node:path';
|
|
33
33
|
import { getSessionIdCanonical } from '../session/session-manager.js';
|
|
34
|
-
import { AUTO_COMPACT_THRESHOLD_RATIO } from '../context/auto-compact-types.js';
|
|
34
|
+
import { AUTO_COMPACT_PRE_COMPACT_RATIO, AUTO_COMPACT_THRESHOLD_RATIO } from '../context/auto-compact-types.js';
|
|
35
35
|
import { describeMode, thresholdFor } from './auto-compact-modes.js';
|
|
36
36
|
import { read24hState } from '../24h-mode/store.js';
|
|
37
|
+
import { readCompactLifecycle, writeCompactLifecycle } from '../compact-statusline/compact-lifecycle-store.js';
|
|
37
38
|
const PRE_COMPACT_REASON = 'pre-compact-auto';
|
|
38
39
|
/**
|
|
39
40
|
* Map a context ratio to a `CompactTrigger` action. Pure; the side
|
|
@@ -121,6 +122,16 @@ export function evaluateAutoCompactDecision(input) {
|
|
|
121
122
|
if (input.force) {
|
|
122
123
|
return { shouldCompact: true, reason: 'pre-compact', trigger };
|
|
123
124
|
}
|
|
125
|
+
// Slice 2026-07-31-rid-mac-transcript-estimate-trigger: transcript-estimate
|
|
126
|
+
// is the ONLY signal available on Mac Claude Code (no env-var, no statusline
|
|
127
|
+
// poll). The gate above already returns shouldCompact: true at ratio ≥ 0.85,
|
|
128
|
+
// but this forward-compat carve-out makes the source-aware rule explicit so
|
|
129
|
+
// any future source-aware downgrading cannot silently re-introduce the
|
|
130
|
+
// Mac auto-compact silent-failure mode without an audit. No higher-priority
|
|
131
|
+
// source is present (`claude-code-env` would have been P1, `statusline-poll`
|
|
132
|
+
// P2, `user-overridden` P4) — Mac's only signal is `transcript-estimate`.
|
|
133
|
+
if (input.source === 'transcript-estimate' && input.ratio >= AUTO_COMPACT_PRE_COMPACT_RATIO)
|
|
134
|
+
return { shouldCompact: true, reason: 'pre-compact', trigger };
|
|
124
135
|
// Default: peaks-loop drives pre-compact autonomously.
|
|
125
136
|
return { shouldCompact: true, reason: 'pre-compact', trigger };
|
|
126
137
|
}
|
|
@@ -248,6 +259,219 @@ function resolveAutoCompactMode(projectRoot, sessionId) {
|
|
|
248
259
|
return 'standard';
|
|
249
260
|
}
|
|
250
261
|
}
|
|
262
|
+
/**
|
|
263
|
+
* One id per compact attempt. Timestamp-prefixed so a human reading
|
|
264
|
+
* the raw record can order runs by eye; the random suffix keeps two
|
|
265
|
+
* attempts inside the same millisecond distinct.
|
|
266
|
+
*/
|
|
267
|
+
function newCompactRunId(now) {
|
|
268
|
+
const stamp = now.toISOString().replace(/[:.]/g, '-');
|
|
269
|
+
const suffix = Math.random().toString(16).slice(2, 8);
|
|
270
|
+
return `compact-${stamp}-${suffix}`;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Slice 2026-08-01-compact-lifecycle (Task 5): read an open compact
|
|
274
|
+
* record ignoring staleness.
|
|
275
|
+
*
|
|
276
|
+
* The lifecycle store classifies an active record as `stalled` after a
|
|
277
|
+
* caller-supplied `staleAfterMs` window. That classification exists for
|
|
278
|
+
* the statusline renderer (Task 2/3) — it tells the user "this run
|
|
279
|
+
* hasn't heartbeated for a while".
|
|
280
|
+
*
|
|
281
|
+
* For settling a run by measurement, staleness is irrelevant: if a
|
|
282
|
+
* record exists at `compacting`, a post-compact probe that measures a
|
|
283
|
+
* drop should still complete it, even if the probe itself ran hours
|
|
284
|
+
* later. Wrapping that unbounded read here keeps the magic number out
|
|
285
|
+
* of the settle path and makes the intent self-documenting.
|
|
286
|
+
*
|
|
287
|
+
* Returns `null` for any non-`valid` kind (missing / invalid /
|
|
288
|
+
* stalled). Errors from the underlying read are swallowed — settling
|
|
289
|
+
* is best-effort telemetry and must never bubble.
|
|
290
|
+
*/
|
|
291
|
+
function readOpenCompactLifecycle(input) {
|
|
292
|
+
let out;
|
|
293
|
+
try {
|
|
294
|
+
out = readCompactLifecycle({
|
|
295
|
+
projectRoot: input.projectRoot,
|
|
296
|
+
sessionId: input.sessionId,
|
|
297
|
+
nowMs: Date.now(),
|
|
298
|
+
// Number.MAX_SAFE_INTEGER disables the store's staleness gate
|
|
299
|
+
// for this call — see the doc comment above for why settling
|
|
300
|
+
// intentionally ignores staleness.
|
|
301
|
+
staleAfterMs: Number.MAX_SAFE_INTEGER
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
catch {
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
return out.kind === 'valid' ? out.record : null;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Slice 2026-08-01-compact-lifecycle (Task 5): the local transition
|
|
311
|
+
* builder. Carries `runId`, `triggerRatio` and `redLine` forward from
|
|
312
|
+
* the run that opened, and remembers the prior stage so a failure can
|
|
313
|
+
* name the stage it died in.
|
|
314
|
+
*
|
|
315
|
+
* TRUTHFULNESS: this publisher only ever emits a stage the calling
|
|
316
|
+
* process has actually PROVED. It never emits `verifying` or
|
|
317
|
+
* `completed` off the back of a successful dispatch — see
|
|
318
|
+
* `runAutoCompact` and `settleOpenLifecycleRun` for the reason.
|
|
319
|
+
*
|
|
320
|
+
* Telemetry is strictly subordinate to the compact itself: every write
|
|
321
|
+
* is best-effort, and a store failure must not change the threshold
|
|
322
|
+
* decision, the dispatch, or the returned envelope.
|
|
323
|
+
*/
|
|
324
|
+
class CompactLifecyclePublisher {
|
|
325
|
+
ctx;
|
|
326
|
+
lastStage = 'queued';
|
|
327
|
+
constructor(ctx) {
|
|
328
|
+
this.ctx = ctx;
|
|
329
|
+
}
|
|
330
|
+
/** Publish an active stage the process has proved. */
|
|
331
|
+
advance(stage) {
|
|
332
|
+
this.lastStage = stage;
|
|
333
|
+
this.write({
|
|
334
|
+
schemaVersion: 1,
|
|
335
|
+
runId: this.ctx.runId,
|
|
336
|
+
stage,
|
|
337
|
+
updatedAt: new Date().toISOString(),
|
|
338
|
+
triggerRatio: this.ctx.triggerRatio,
|
|
339
|
+
redLine: this.ctx.redLine
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
/** Publish the terminal failure, attributed to the last stage reached. */
|
|
343
|
+
fail(error) {
|
|
344
|
+
this.write({
|
|
345
|
+
schemaVersion: 1,
|
|
346
|
+
runId: this.ctx.runId,
|
|
347
|
+
stage: 'failed',
|
|
348
|
+
updatedAt: new Date().toISOString(),
|
|
349
|
+
triggerRatio: this.ctx.triggerRatio,
|
|
350
|
+
redLine: this.ctx.redLine,
|
|
351
|
+
failedAt: this.lastStage,
|
|
352
|
+
errorSummary: summarizeLifecycleError(error)
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
write(record) {
|
|
356
|
+
try {
|
|
357
|
+
if (this.ctx.failLifecycleWrite)
|
|
358
|
+
throw new Error('lifecycle store unavailable');
|
|
359
|
+
writeCompactLifecycle({
|
|
360
|
+
projectRoot: this.ctx.projectRoot,
|
|
361
|
+
sessionId: this.ctx.sessionId,
|
|
362
|
+
record
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
catch {
|
|
366
|
+
// Best-effort telemetry: a lifecycle write failure must never
|
|
367
|
+
// change the compact decision, the dispatch, or the envelope.
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
try {
|
|
371
|
+
this.ctx.onLifecycleStage?.(record.stage, record);
|
|
372
|
+
}
|
|
373
|
+
catch {
|
|
374
|
+
// An observer is a passive listener; its failure is not ours.
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Reduce an arbitrary thrown value to a single-line, bounded summary
|
|
380
|
+
* fit for a statusline. Stack frames are dropped (the record is a
|
|
381
|
+
* human-facing indicator, not a crash dump); the store clamps the
|
|
382
|
+
* result to its own 160-character cap as a second line of defence.
|
|
383
|
+
*
|
|
384
|
+
* `null` / `undefined` thrown values — a real possibility from
|
|
385
|
+
* `Promise.reject(null)` or a thrown `undefined` — must not collapse
|
|
386
|
+
* to the empty string, which the store would then reject as missing.
|
|
387
|
+
* They map to a fixed "unknown error" sentinel so the record always
|
|
388
|
+
* carries some diagnostic text.
|
|
389
|
+
*/
|
|
390
|
+
function summarizeLifecycleError(error) {
|
|
391
|
+
let raw;
|
|
392
|
+
if (error instanceof Error)
|
|
393
|
+
raw = error.message;
|
|
394
|
+
else if (error === null || error === undefined)
|
|
395
|
+
raw = 'unknown error';
|
|
396
|
+
else
|
|
397
|
+
raw = String(error);
|
|
398
|
+
if (raw.length === 0)
|
|
399
|
+
raw = 'unknown error';
|
|
400
|
+
const firstLine = raw.split('\n')[0] ?? '';
|
|
401
|
+
const collapsed = firstLine.replace(/\s+/g, ' ').trim();
|
|
402
|
+
return collapsed.length === 0 ? 'unknown error' : (collapsed.length > 160 ? collapsed.slice(0, 160) : collapsed);
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Slice 2026-08-01-compact-lifecycle (Task 5, Step 4): close out an
|
|
406
|
+
* open compact run using a REAL measurement.
|
|
407
|
+
*
|
|
408
|
+
* This is the integration with the actual post-compact detection path.
|
|
409
|
+
* The claude-code adapter's `postCompactDetectCommand` is
|
|
410
|
+
* `peaks compact auto --json` — i.e. the next probe through this very
|
|
411
|
+
* function. So when a probe finds a run still sitting at `compacting`
|
|
412
|
+
* and MEASURES a ratio that has dropped below the auto-fire threshold,
|
|
413
|
+
* that measurement is the proof the compact landed. Only then do we
|
|
414
|
+
* emit `verifying` (we have a measurement in hand) followed by
|
|
415
|
+
* `completed` (it confirms the drop), carrying the measured
|
|
416
|
+
* `afterRatio`.
|
|
417
|
+
*
|
|
418
|
+
* We refuse to complete when:
|
|
419
|
+
* - the probe could not measure anything (`conservative-fallback`
|
|
420
|
+
* returns `ratio: 0`, which means "unknown", NOT "empty"). Writing
|
|
421
|
+
* `afterRatio: 0` there would publish a fabricated number;
|
|
422
|
+
* - the ratio is still at or above the auto-fire threshold — the
|
|
423
|
+
* compact has not landed, so the run stays open.
|
|
424
|
+
*/
|
|
425
|
+
function settleOpenLifecycleRun(input) {
|
|
426
|
+
// A `conservative-fallback` probe means no signal was available at
|
|
427
|
+
// all. Its `ratio: 0` is the absence of a measurement, so it can
|
|
428
|
+
// never be evidence that the context shrank.
|
|
429
|
+
if (input.source === 'conservative-fallback')
|
|
430
|
+
return;
|
|
431
|
+
if (input.measuredRatio >= input.autoFireThreshold)
|
|
432
|
+
return;
|
|
433
|
+
const prior = readOpenCompactLifecycle({
|
|
434
|
+
projectRoot: input.projectRoot,
|
|
435
|
+
sessionId: input.sessionId
|
|
436
|
+
});
|
|
437
|
+
if (prior === null)
|
|
438
|
+
return;
|
|
439
|
+
// Only a run that was actually dispatched (`compacting`) can be
|
|
440
|
+
// completed by a post-compact measurement.
|
|
441
|
+
if (prior.stage !== 'compacting')
|
|
442
|
+
return;
|
|
443
|
+
const emit = (stage, withAfterRatio) => {
|
|
444
|
+
const record = {
|
|
445
|
+
schemaVersion: 1,
|
|
446
|
+
runId: prior.runId,
|
|
447
|
+
stage,
|
|
448
|
+
updatedAt: new Date().toISOString(),
|
|
449
|
+
triggerRatio: prior.triggerRatio,
|
|
450
|
+
redLine: prior.redLine,
|
|
451
|
+
...(withAfterRatio ? { afterRatio: input.measuredRatio } : {})
|
|
452
|
+
};
|
|
453
|
+
try {
|
|
454
|
+
writeCompactLifecycle({
|
|
455
|
+
projectRoot: input.projectRoot,
|
|
456
|
+
sessionId: input.sessionId,
|
|
457
|
+
record
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
catch {
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
try {
|
|
464
|
+
input.onLifecycleStage?.(stage, record);
|
|
465
|
+
}
|
|
466
|
+
catch {
|
|
467
|
+
// Observer failures are not ours to propagate.
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
// `verifying` = we hold a measurement and are checking it.
|
|
471
|
+
emit('verifying', false);
|
|
472
|
+
// `completed` = the measurement confirms the drop; publish it.
|
|
473
|
+
emit('completed', true);
|
|
474
|
+
}
|
|
251
475
|
/**
|
|
252
476
|
* Execute the auto-compact flow.
|
|
253
477
|
*
|
|
@@ -293,9 +517,28 @@ export async function runAutoCompact(input) {
|
|
|
293
517
|
inFlightBatch: input.inFlightBatch,
|
|
294
518
|
force: input.force,
|
|
295
519
|
bypassRedLine: input.bypassRedLine,
|
|
296
|
-
mode
|
|
520
|
+
mode,
|
|
521
|
+
// Slice 2026-07-31-rid-mac-transcript-estimate-trigger: pipe the source
|
|
522
|
+
// tag through so `evaluateAutoCompactDecision` can apply the
|
|
523
|
+
// source-aware carve-out for Mac's `transcript-estimate` signal.
|
|
524
|
+
source: probe.source
|
|
297
525
|
});
|
|
298
526
|
if (!decision.shouldCompact) {
|
|
527
|
+
// Slice 2026-08-01-compact-lifecycle (Task 5): this probe IS the
|
|
528
|
+
// adapter's `postCompactDetectCommand`. A ratio that has fallen
|
|
529
|
+
// back below the auto-fire threshold is the real, measured proof
|
|
530
|
+
// that a previously-dispatched compact landed — so settle any run
|
|
531
|
+
// still open at `compacting`. Nothing is written when there is no
|
|
532
|
+
// open run, when the ratio is still high, or when the probe could
|
|
533
|
+
// not measure at all.
|
|
534
|
+
settleOpenLifecycleRun({
|
|
535
|
+
projectRoot: input.projectRoot,
|
|
536
|
+
sessionId,
|
|
537
|
+
measuredRatio: probe.ratio,
|
|
538
|
+
source: probe.source,
|
|
539
|
+
autoFireThreshold: thresholdFor(mode, 'autoFire'),
|
|
540
|
+
onLifecycleStage: input.onLifecycleStage
|
|
541
|
+
});
|
|
299
542
|
return {
|
|
300
543
|
ok: true,
|
|
301
544
|
code: decision.reason === 'in-flight-batch' ? 'AUTO_COMPACT_WAIT' : 'AUTO_COMPACT_SKIP',
|
|
@@ -314,55 +557,129 @@ export async function runAutoCompact(input) {
|
|
|
314
557
|
}
|
|
315
558
|
const isRedLine = decision.reason === 'red-line';
|
|
316
559
|
const now = input.now ?? new Date();
|
|
317
|
-
|
|
560
|
+
// Slice 2026-08-01-compact-lifecycle (Task 5): the decision has now
|
|
561
|
+
// committed to compacting, so the run is `queued`. One runId per
|
|
562
|
+
// attempt; every later transition carries it forward.
|
|
563
|
+
const lifecycle = new CompactLifecyclePublisher({
|
|
318
564
|
projectRoot: input.projectRoot,
|
|
319
565
|
sessionId,
|
|
320
|
-
now,
|
|
321
|
-
|
|
566
|
+
runId: newCompactRunId(now),
|
|
567
|
+
triggerRatio: probe.ratio,
|
|
568
|
+
redLine: isRedLine,
|
|
569
|
+
onLifecycleStage: input.onLifecycleStage,
|
|
570
|
+
failLifecycleWrite: input.testHooks?.failLifecycleWrite
|
|
322
571
|
});
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
572
|
+
lifecycle.advance('queued');
|
|
573
|
+
let checkpointPath;
|
|
574
|
+
let plan;
|
|
575
|
+
let nextActions;
|
|
576
|
+
try {
|
|
577
|
+
// `preparing` covers checkpoint + convergence-plan + recovery writes.
|
|
578
|
+
lifecycle.advance('preparing');
|
|
579
|
+
if (input.testHooks?.failPreparing)
|
|
580
|
+
throw new Error('disk full while writing checkpoint');
|
|
581
|
+
checkpointPath = writePreCompactCheckpoint({
|
|
582
|
+
projectRoot: input.projectRoot,
|
|
583
|
+
sessionId,
|
|
584
|
+
now,
|
|
585
|
+
redLine: isRedLine
|
|
586
|
+
});
|
|
587
|
+
nextActions = isRedLine
|
|
588
|
+
? [
|
|
589
|
+
'RED-LINE compact dispatched — further sub-agent dispatch BLOCKED until ratio < 0.85',
|
|
590
|
+
'Post-compact resume picks up the convergence plan from auto-decisions.md',
|
|
591
|
+
'Next `peaks compact auto` probe will confirm ratio dropped below 0.85'
|
|
592
|
+
]
|
|
593
|
+
: [
|
|
594
|
+
'Pre-compact dispatched — IDE compact in progress (async)',
|
|
595
|
+
'Post-compact resume picks up the convergence plan from auto-decisions.md',
|
|
596
|
+
'Next `peaks compact auto` probe will confirm ratio dropped below 0.85'
|
|
597
|
+
];
|
|
598
|
+
plan = buildConvergencePlan({
|
|
599
|
+
sessionId,
|
|
600
|
+
projectRoot: input.projectRoot,
|
|
601
|
+
ratio: probe.ratio,
|
|
602
|
+
checkpointPath,
|
|
603
|
+
nextActions,
|
|
604
|
+
redLine: isRedLine
|
|
605
|
+
});
|
|
606
|
+
appendAutoDecisionLog({ projectRoot: input.projectRoot, sessionId, plan });
|
|
607
|
+
}
|
|
608
|
+
catch (error) {
|
|
609
|
+
lifecycle.fail(error);
|
|
610
|
+
// Preserve the original error contract: the caller gets the same
|
|
611
|
+
// `AUTO_COMPACT_DISPATCH_FAILED` envelope shape it already handles,
|
|
612
|
+
// not a thrown exception and not a widened type.
|
|
613
|
+
return {
|
|
614
|
+
ok: false,
|
|
615
|
+
code: 'AUTO_COMPACT_DISPATCH_FAILED',
|
|
616
|
+
message: `Auto-compact preparation failed before IDE dispatch: ${summarizeLifecycleError(error)}`,
|
|
617
|
+
data: {
|
|
618
|
+
sessionId,
|
|
619
|
+
ratio: probe.ratio,
|
|
620
|
+
source: probe.source,
|
|
621
|
+
target: input.target ?? 'main',
|
|
622
|
+
mode,
|
|
623
|
+
redLineGated: isRedLine
|
|
624
|
+
}
|
|
625
|
+
};
|
|
626
|
+
}
|
|
343
627
|
// Lazy import to keep AC-3 (IDE dispatch) pluggable; tests mock this module.
|
|
344
628
|
const { dispatchIdeCompact } = await import('../context/auto-compact-dispatcher.js');
|
|
345
629
|
const target = input.target ?? 'main';
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
630
|
+
let dispatch;
|
|
631
|
+
try {
|
|
632
|
+
// `compacting` is the last stage this process can prove: the IDE
|
|
633
|
+
// performs the actual compaction out-of-band, so a successful
|
|
634
|
+
// dispatch return is NOT evidence the context shrank.
|
|
635
|
+
lifecycle.advance('compacting');
|
|
636
|
+
if (input.testHooks?.failCompacting)
|
|
637
|
+
throw new Error('IDE dispatch exploded');
|
|
638
|
+
// Slice 2026-06-28: when targeting the main session, write an
|
|
639
|
+
// intent record so the next main-session LLM turn fires `/compact`
|
|
640
|
+
// in-band. Without this record the LLM has no signal that the
|
|
641
|
+
// orchestrator asked for compact; the dispatcher alone would have
|
|
642
|
+
// been a no-op against the main Claude Code window.
|
|
643
|
+
if (target === 'main') {
|
|
644
|
+
writeMainSessionCompactIntent({
|
|
645
|
+
projectRoot: input.projectRoot,
|
|
646
|
+
sessionId,
|
|
647
|
+
ratio: probe.ratio,
|
|
648
|
+
redLine: isRedLine,
|
|
649
|
+
now
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
dispatch = await dispatchIdeCompact({
|
|
353
653
|
projectRoot: input.projectRoot,
|
|
354
654
|
sessionId,
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
now
|
|
655
|
+
env: input.env,
|
|
656
|
+
target
|
|
358
657
|
});
|
|
359
658
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
659
|
+
catch (error) {
|
|
660
|
+
lifecycle.fail(error);
|
|
661
|
+
return {
|
|
662
|
+
ok: false,
|
|
663
|
+
code: 'AUTO_COMPACT_DISPATCH_FAILED',
|
|
664
|
+
message: `Auto-compact checkpoint written but IDE dispatch threw: ${summarizeLifecycleError(error)}`,
|
|
665
|
+
data: {
|
|
666
|
+
sessionId,
|
|
667
|
+
ratio: probe.ratio,
|
|
668
|
+
source: probe.source,
|
|
669
|
+
checkpointPath,
|
|
670
|
+
convergencePlan: plan,
|
|
671
|
+
target,
|
|
672
|
+
mode,
|
|
673
|
+
redLineGated: isRedLine
|
|
674
|
+
}
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
// A dispatcher that returns `ok: false` did not compact anything —
|
|
678
|
+
// record that as a failure at `compacting` rather than leaving the
|
|
679
|
+
// run looking like it is still in progress.
|
|
680
|
+
if (!dispatch.ok) {
|
|
681
|
+
lifecycle.fail(new Error(dispatch.message));
|
|
682
|
+
}
|
|
366
683
|
// Slice 2026-07-30-compact-visibility: append a compact-history
|
|
367
684
|
// event so the new 'peaks compact history' CLI and the
|
|
368
685
|
// 'peaks statusline compact' indicator have a record. The
|
|
@@ -65,8 +65,12 @@ function safeReadCheckpoint(absPath) {
|
|
|
65
65
|
const content = mutable;
|
|
66
66
|
return { path: absPath, mtime: stat.mtime, content };
|
|
67
67
|
}
|
|
68
|
-
catch { // TODO(g2): legacy silent catch — grace: 1 minor release
|
|
69
|
-
|
|
68
|
+
catch (err) { // TODO(g2): legacy silent catch — now narrows to IO errors only (grace: 1 minor release, v2.14.0)
|
|
69
|
+
if (err instanceof ReferenceError)
|
|
70
|
+
throw err; // surface module-load bugs
|
|
71
|
+
if (err instanceof SyntaxError)
|
|
72
|
+
throw err; // surface parse bugs
|
|
73
|
+
return null; // only swallow IO errors
|
|
70
74
|
}
|
|
71
75
|
}
|
|
72
76
|
function readLatestTodayCheckpoint(runtimeDir, now) {
|
|
@@ -223,8 +227,12 @@ function readActiveSkillName(projectRoot) {
|
|
|
223
227
|
const presence = getSkillPresence(projectRoot);
|
|
224
228
|
return presence?.skill;
|
|
225
229
|
}
|
|
226
|
-
catch { // TODO(g2): legacy silent catch — grace: 1 minor release
|
|
227
|
-
|
|
230
|
+
catch (err) { // TODO(g2): legacy silent catch — now narrows to IO errors only (grace: 1 minor release, v2.14.0)
|
|
231
|
+
if (err instanceof ReferenceError)
|
|
232
|
+
throw err; // surface module-load bugs
|
|
233
|
+
if (err instanceof SyntaxError)
|
|
234
|
+
throw err; // surface parse bugs
|
|
235
|
+
return undefined; // only swallow IO errors
|
|
228
236
|
}
|
|
229
237
|
}
|
|
230
238
|
/**
|
|
@@ -85,8 +85,12 @@ function readProgressIfAny(projectRoot, sessionId, jid) {
|
|
|
85
85
|
}
|
|
86
86
|
return null;
|
|
87
87
|
}
|
|
88
|
-
catch { //
|
|
89
|
-
|
|
88
|
+
catch (err) { // narrows to IO errors only — surface ReferenceError / SyntaxError so future module-load or parse bugs fail loudly (instead of silently masking progress.json corruption)
|
|
89
|
+
if (err instanceof ReferenceError)
|
|
90
|
+
throw err; // surface module-load bugs
|
|
91
|
+
if (err instanceof SyntaxError)
|
|
92
|
+
throw err; // surface parse bugs (corrupt progress.json)
|
|
93
|
+
return null; // only swallow IO errors (ENOENT, EACCES, …)
|
|
90
94
|
}
|
|
91
95
|
}
|
|
92
96
|
function readPromptFromLastPromptFile(projectRoot, sessionId) {
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export type CompactLifecycleStage = 'queued' | 'preparing' | 'compacting' | 'verifying' | 'completed' | 'failed';
|
|
2
|
+
export interface CompactLifecycleRecord {
|
|
3
|
+
readonly schemaVersion: 1;
|
|
4
|
+
readonly runId: string;
|
|
5
|
+
readonly stage: CompactLifecycleStage;
|
|
6
|
+
readonly updatedAt: string;
|
|
7
|
+
readonly triggerRatio: number;
|
|
8
|
+
readonly afterRatio?: number;
|
|
9
|
+
readonly redLine: boolean;
|
|
10
|
+
readonly failedAt?: Exclude<CompactLifecycleStage, 'failed' | 'completed'>;
|
|
11
|
+
readonly errorSummary?: string;
|
|
12
|
+
}
|
|
13
|
+
export type CompactLifecycleRead = {
|
|
14
|
+
readonly kind: 'missing';
|
|
15
|
+
} | {
|
|
16
|
+
readonly kind: 'valid';
|
|
17
|
+
readonly record: CompactLifecycleRecord;
|
|
18
|
+
} | {
|
|
19
|
+
readonly kind: 'invalid';
|
|
20
|
+
readonly reason: string;
|
|
21
|
+
} | {
|
|
22
|
+
readonly kind: 'stalled';
|
|
23
|
+
readonly record: CompactLifecycleRecord;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Atomically write a compact lifecycle record. Uses a temp file in
|
|
27
|
+
* the same directory plus `renameSync`, so a crash mid-write never
|
|
28
|
+
* leaves a half-written record (atomic on POSIX, near-atomic on
|
|
29
|
+
* Windows — same contract `24h-mode/store.ts` and
|
|
30
|
+
* `session-checkpoint-service.ts` use).
|
|
31
|
+
*
|
|
32
|
+
* Side effects:
|
|
33
|
+
* - creates `<sessionDir>/` if missing
|
|
34
|
+
* - clamps `errorSummary` to 160 characters (longer strings are
|
|
35
|
+
* truncated at the head, not padded)
|
|
36
|
+
* - removes the temp file on rename failure (best effort)
|
|
37
|
+
*
|
|
38
|
+
* Throws on I/O failure or validation rejection; the caller decides
|
|
39
|
+
* whether to retry / log / surface to the user.
|
|
40
|
+
*/
|
|
41
|
+
export declare function writeCompactLifecycle(input: {
|
|
42
|
+
readonly projectRoot: string;
|
|
43
|
+
readonly sessionId: string;
|
|
44
|
+
readonly record: CompactLifecycleRecord;
|
|
45
|
+
}): void;
|
|
46
|
+
/**
|
|
47
|
+
* Read a compact lifecycle record and classify it.
|
|
48
|
+
*
|
|
49
|
+
* Return kinds:
|
|
50
|
+
* - `missing` — no record has ever been written for this session
|
|
51
|
+
* - `valid` — record parsed and validated; stage is terminal
|
|
52
|
+
* (completed/failed) OR is active but still fresh
|
|
53
|
+
* - `invalid` — file exists but is malformed / wrong schema / out
|
|
54
|
+
* of range. `reason` is a single-line English message
|
|
55
|
+
* (no CLI verbs, no stack traces). The caller MUST
|
|
56
|
+
* surface this to a human — we never silently turn
|
|
57
|
+
* a corrupted record into `missing`.
|
|
58
|
+
* - `stalled` — record is valid and is currently an ACTIVE stage,
|
|
59
|
+
* but `updatedAt` is older than `staleAfterMs` (a
|
|
60
|
+
* heartbeat that never arrived). Terminal stages
|
|
61
|
+
* never go stalled.
|
|
62
|
+
*/
|
|
63
|
+
export declare function readCompactLifecycle(input: {
|
|
64
|
+
readonly projectRoot: string;
|
|
65
|
+
readonly sessionId: string;
|
|
66
|
+
readonly nowMs: number;
|
|
67
|
+
readonly staleAfterMs: number;
|
|
68
|
+
}): CompactLifecycleRead;
|