peaks-loop 4.0.28 → 4.0.30

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.
Files changed (74) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/cli/commands/_register.js +4 -0
  3. package/dist/cli/commands/dispatch-commands.d.ts +5 -41
  4. package/dist/cli/commands/dispatch-commands.js +59 -185
  5. package/dist/cli/commands/evidence-commands.d.ts +12 -0
  6. package/dist/cli/commands/evidence-commands.js +43 -0
  7. package/dist/cli/commands/fresh-context-commands.d.ts +15 -0
  8. package/dist/cli/commands/fresh-context-commands.js +24 -0
  9. package/dist/cli/commands/job-commands.js +14 -1
  10. package/dist/cli/commands/request-commands.js +15 -97
  11. package/dist/cli/commands/request-format-helpers.d.ts +19 -0
  12. package/dist/cli/commands/request-format-helpers.js +102 -0
  13. package/dist/cli/commands/worktree-auth-commands.js +2 -472
  14. package/dist/cli/commands/worktree-lease-commands.d.ts +21 -0
  15. package/dist/cli/commands/worktree-lease-commands.js +500 -0
  16. package/dist/services/artifacts/request-artifact-service.js +13 -3
  17. package/dist/services/code/auto-compact-lifecycle.d.ts +102 -0
  18. package/dist/services/code/auto-compact-lifecycle.js +235 -0
  19. package/dist/services/code/auto-compact-orchestrator.d.ts +1 -1
  20. package/dist/services/code/auto-compact-orchestrator.js +1 -222
  21. package/dist/services/codegraph/codegraph-autorefresh.d.ts +22 -0
  22. package/dist/services/codegraph/codegraph-autorefresh.js +91 -0
  23. package/dist/services/codegraph/codegraph-preflight-service.d.ts +53 -0
  24. package/dist/services/codegraph/codegraph-preflight-service.js +226 -0
  25. package/dist/services/context/build-dispatch-system-prompt.d.ts +52 -0
  26. package/dist/services/context/build-dispatch-system-prompt.js +70 -3
  27. package/dist/services/dispatch/dispatch-record-types.d.ts +278 -0
  28. package/dist/services/dispatch/dispatch-record-types.js +15 -0
  29. package/dist/services/dispatch/dispatch-record-upgrade.d.ts +5 -0
  30. package/dist/services/dispatch/dispatch-record-upgrade.js +230 -0
  31. package/dist/services/dispatch/dispatch-record-writer.d.ts +3 -278
  32. package/dist/services/dispatch/dispatch-record-writer.js +3 -245
  33. package/dist/services/dispatch/dispatch-sub-agent.d.ts +43 -0
  34. package/dist/services/dispatch/dispatch-sub-agent.js +55 -0
  35. package/dist/services/dispatch/isolation-lease.d.ts +43 -0
  36. package/dist/services/dispatch/isolation-lease.js +129 -0
  37. package/dist/services/evidence/evidence-generator.d.ts +26 -0
  38. package/dist/services/evidence/evidence-generator.js +349 -0
  39. package/dist/services/fresh-context/config.d.ts +1 -0
  40. package/dist/services/fresh-context/config.js +13 -0
  41. package/dist/services/fresh-context/fresh-context-block.d.ts +12 -0
  42. package/dist/services/fresh-context/fresh-context-block.js +40 -0
  43. package/dist/services/fresh-context/trigger-scan.d.ts +30 -0
  44. package/dist/services/fresh-context/trigger-scan.js +29 -0
  45. package/dist/services/skills/hooks-codegate-superpowers.d.ts +65 -0
  46. package/dist/services/skills/hooks-codegate-superpowers.js +204 -0
  47. package/dist/services/skills/hooks-settings-service.d.ts +2 -64
  48. package/dist/services/skills/hooks-settings-service.js +2 -215
  49. package/dist/services/skills/skill-statusline-renderer.d.ts +2 -34
  50. package/dist/services/skills/skill-statusline-renderer.js +1 -184
  51. package/dist/services/skills/statusline-palette.d.ts +62 -0
  52. package/dist/services/skills/statusline-palette.js +190 -0
  53. package/dist/services/slice/slice-decompose-import-edges.d.ts +8 -0
  54. package/dist/services/slice/slice-decompose-import-edges.js +102 -0
  55. package/dist/services/slice/slice-decompose-service.js +3 -198
  56. package/dist/services/slice/slice-decompose-tarjan.d.ts +8 -0
  57. package/dist/services/slice/slice-decompose-tarjan.js +108 -0
  58. package/dist/services/standards/project-standards-service.d.ts +1 -9
  59. package/dist/services/standards/project-standards-service.js +3 -232
  60. package/dist/services/standards/standards-render.d.ts +23 -0
  61. package/dist/services/standards/standards-render.js +238 -0
  62. package/dist/services/standards/ui-library-dispatch-block.d.ts +27 -0
  63. package/dist/services/standards/ui-library-dispatch-block.js +48 -0
  64. package/dist/services/workspace/reconcile-migrate.d.ts +77 -0
  65. package/dist/services/workspace/reconcile-migrate.js +230 -0
  66. package/dist/services/workspace/reconcile-service.d.ts +0 -72
  67. package/dist/services/workspace/reconcile-service.js +3 -220
  68. package/dist/services/workspace/workspace-service.js +13 -2
  69. package/dist/shared/incrementing-number.d.ts +11 -0
  70. package/dist/shared/incrementing-number.js +16 -3
  71. package/package.json +5 -5
  72. package/skills/peaks-code/SKILL.md +6 -2
  73. package/skills/peaks-code/references/fresh-context-preflight.md +81 -0
  74. package/skills/peaks-code/references/sub-agent-dispatch.md +27 -8
@@ -0,0 +1,235 @@
1
+ /**
2
+ * Auto-compact lifecycle telemetry (v2.13.0 AC-2 + AC-3 + AC-4,
3
+ * slice 2026-08-01-compact-lifecycle Task 5).
4
+ *
5
+ * Extracted from `auto-compact-orchestrator.ts` to keep that file under
6
+ * the 800 LOC cap (mechanical verbatim move). Publishes the stages a
7
+ * compact attempt actually PROVED in-process (`queued` / `preparing` /
8
+ * `compacting`) plus the terminal `failed` stage, and settles an open
9
+ * `compacting` run using a real post-compact measurement. Telemetry is
10
+ * strictly subordinate to the compact itself: every write is best-effort
11
+ * and never changes the threshold decision, the dispatch, or the
12
+ * returned envelope.
13
+ */
14
+ import { readCompactLifecycle, writeCompactLifecycle } from '../compact-statusline/compact-lifecycle-store.js';
15
+ /**
16
+ * PRD-002b slice 2 — extract the few cross-cutting magic numbers
17
+ * that actually appear at runtime call sites in this orchestrator.
18
+ * Threshold ratios (0.85 / 0.95) live in `auto-compact-types.ts`
19
+ * and are NOT extracted here because they ARE the contract; comment
20
+ * + spec prose references must keep the literal value visible.
21
+ */
22
+ const COLLAPSED_ERROR_MAX_CHARS = 160;
23
+ /**
24
+ * One id per compact attempt. Timestamp-prefixed so a human reading
25
+ * the raw record can order runs by eye; the random suffix keeps two
26
+ * attempts inside the same millisecond distinct.
27
+ */
28
+ export function newCompactRunId(now) {
29
+ const stamp = now.toISOString().replace(/[:.]/g, '-');
30
+ const suffix = Math.random().toString(16).slice(2, 8);
31
+ return `compact-${stamp}-${suffix}`;
32
+ }
33
+ /**
34
+ * Slice 2026-08-01-compact-lifecycle (Task 5): read an open compact
35
+ * record ignoring staleness.
36
+ *
37
+ * The lifecycle store classifies an active record as `stalled` after a
38
+ * caller-supplied `staleAfterMs` window. That classification exists for
39
+ * the statusline renderer (Task 2/3) — it tells the user "this run
40
+ * hasn't heartbeated for a while".
41
+ *
42
+ * For settling a run by measurement, staleness is irrelevant: if a
43
+ * record exists at `compacting`, a post-compact probe that measures a
44
+ * drop should still complete it, even if the probe itself ran hours
45
+ * later. Wrapping that unbounded read here keeps the magic number out
46
+ * of the settle path and makes the intent self-documenting.
47
+ *
48
+ * Returns `null` for any non-`valid` kind (missing / invalid /
49
+ * stalled). Errors from the underlying read are swallowed — settling
50
+ * is best-effort telemetry and must never bubble.
51
+ */
52
+ function readOpenCompactLifecycle(input) {
53
+ let out;
54
+ try {
55
+ out = readCompactLifecycle({
56
+ projectRoot: input.projectRoot,
57
+ sessionId: input.sessionId,
58
+ nowMs: Date.now(),
59
+ // Number.MAX_SAFE_INTEGER disables the store's staleness gate
60
+ // for this call — see the doc comment above for why settling
61
+ // intentionally ignores staleness.
62
+ staleAfterMs: Number.MAX_SAFE_INTEGER
63
+ });
64
+ }
65
+ catch {
66
+ return null;
67
+ }
68
+ return out.kind === 'valid' ? out.record : null;
69
+ }
70
+ /**
71
+ * Slice 2026-08-01-compact-lifecycle (Task 5): the local transition
72
+ * builder. Carries `runId`, `triggerRatio` and `redLine` forward from
73
+ * the run that opened, and remembers the prior stage so a failure can
74
+ * name the stage it died in.
75
+ *
76
+ * TRUTHFULNESS: this publisher only ever emits a stage the calling
77
+ * process has actually PROVED. It never emits `verifying` or
78
+ * `completed` off the back of a successful dispatch — see
79
+ * `runAutoCompact` and `settleOpenLifecycleRun` for the reason.
80
+ *
81
+ * Telemetry is strictly subordinate to the compact itself: every write
82
+ * is best-effort, and a store failure must not change the threshold
83
+ * decision, the dispatch, or the returned envelope.
84
+ */
85
+ export class CompactLifecyclePublisher {
86
+ ctx;
87
+ lastStage = 'queued';
88
+ constructor(ctx) {
89
+ this.ctx = ctx;
90
+ }
91
+ /** Publish an active stage the process has proved. */
92
+ advance(stage) {
93
+ this.lastStage = stage;
94
+ this.write({
95
+ schemaVersion: 1,
96
+ runId: this.ctx.runId,
97
+ stage,
98
+ updatedAt: new Date().toISOString(),
99
+ triggerRatio: this.ctx.triggerRatio,
100
+ redLine: this.ctx.redLine
101
+ });
102
+ }
103
+ /** Publish the terminal failure, attributed to the last stage reached. */
104
+ fail(error) {
105
+ this.write({
106
+ schemaVersion: 1,
107
+ runId: this.ctx.runId,
108
+ stage: 'failed',
109
+ updatedAt: new Date().toISOString(),
110
+ triggerRatio: this.ctx.triggerRatio,
111
+ redLine: this.ctx.redLine,
112
+ failedAt: this.lastStage,
113
+ errorSummary: summarizeLifecycleError(error)
114
+ });
115
+ }
116
+ write(record) {
117
+ try {
118
+ if (this.ctx.failLifecycleWrite)
119
+ throw new Error('lifecycle store unavailable');
120
+ writeCompactLifecycle({
121
+ projectRoot: this.ctx.projectRoot,
122
+ sessionId: this.ctx.sessionId,
123
+ record
124
+ });
125
+ }
126
+ catch {
127
+ // Best-effort telemetry: a lifecycle write failure must never
128
+ // change the compact decision, the dispatch, or the envelope.
129
+ return;
130
+ }
131
+ try {
132
+ this.ctx.onLifecycleStage?.(record.stage, record);
133
+ }
134
+ catch {
135
+ // An observer is a passive listener; its failure is not ours.
136
+ }
137
+ }
138
+ }
139
+ /**
140
+ * Reduce an arbitrary thrown value to a single-line, bounded summary
141
+ * fit for a statusline. Stack frames are dropped (the record is a
142
+ * human-facing indicator, not a crash dump); the store clamps the
143
+ * result to its own 160-character cap as a second line of defence.
144
+ *
145
+ * `null` / `undefined` thrown values — a real possibility from
146
+ * `Promise.reject(null)` or a thrown `undefined` — must not collapse
147
+ * to the empty string, which the store would then reject as missing.
148
+ * They map to a fixed "unknown error" sentinel so the record always
149
+ * carries some diagnostic text.
150
+ */
151
+ export function summarizeLifecycleError(error) {
152
+ let raw;
153
+ if (error instanceof Error)
154
+ raw = error.message;
155
+ else if (error === null || error === undefined)
156
+ raw = 'unknown error';
157
+ else
158
+ raw = String(error);
159
+ if (raw.length === 0)
160
+ raw = 'unknown error';
161
+ const firstLine = raw.split('\n')[0] ?? '';
162
+ const collapsed = firstLine.replace(/\s+/g, ' ').trim();
163
+ return collapsed.length === 0 ? 'unknown error' : (collapsed.length > COLLAPSED_ERROR_MAX_CHARS ? collapsed.slice(0, COLLAPSED_ERROR_MAX_CHARS) : collapsed);
164
+ }
165
+ /**
166
+ * Slice 2026-08-01-compact-lifecycle (Task 5, Step 4): close out an
167
+ * open compact run using a REAL measurement.
168
+ *
169
+ * This is the integration with the actual post-compact detection path.
170
+ * The claude-code adapter's `postCompactDetectCommand` is
171
+ * `peaks compact auto --json` — i.e. the next probe through this very
172
+ * function. So when a probe finds a run still sitting at `compacting`
173
+ * and MEASURES a ratio that has dropped below the auto-fire threshold,
174
+ * that measurement is the proof the compact landed. Only then do we
175
+ * emit `verifying` (we have a measurement in hand) followed by
176
+ * `completed` (it confirms the drop), carrying the measured
177
+ * `afterRatio`.
178
+ *
179
+ * We refuse to complete when:
180
+ * - the probe could not measure anything (`conservative-fallback`
181
+ * returns `ratio: 0`, which means "unknown", NOT "empty"). Writing
182
+ * `afterRatio: 0` there would publish a fabricated number;
183
+ * - the ratio is still at or above the auto-fire threshold — the
184
+ * compact has not landed, so the run stays open.
185
+ */
186
+ export function settleOpenLifecycleRun(input) {
187
+ // A `conservative-fallback` probe means no signal was available at
188
+ // all. Its `ratio: 0` is the absence of a measurement, so it can
189
+ // never be evidence that the context shrank.
190
+ if (input.source === 'conservative-fallback')
191
+ return;
192
+ if (input.measuredRatio >= input.autoFireThreshold)
193
+ return;
194
+ const prior = readOpenCompactLifecycle({
195
+ projectRoot: input.projectRoot,
196
+ sessionId: input.sessionId
197
+ });
198
+ if (prior === null)
199
+ return;
200
+ // Only a run that was actually dispatched (`compacting`) can be
201
+ // completed by a post-compact measurement.
202
+ if (prior.stage !== 'compacting')
203
+ return;
204
+ const emit = (stage, withAfterRatio) => {
205
+ const record = {
206
+ schemaVersion: 1,
207
+ runId: prior.runId,
208
+ stage,
209
+ updatedAt: new Date().toISOString(),
210
+ triggerRatio: prior.triggerRatio,
211
+ redLine: prior.redLine,
212
+ ...(withAfterRatio ? { afterRatio: input.measuredRatio } : {})
213
+ };
214
+ try {
215
+ writeCompactLifecycle({
216
+ projectRoot: input.projectRoot,
217
+ sessionId: input.sessionId,
218
+ record
219
+ });
220
+ }
221
+ catch {
222
+ return;
223
+ }
224
+ try {
225
+ input.onLifecycleStage?.(stage, record);
226
+ }
227
+ catch {
228
+ // Observer failures are not ours to propagate.
229
+ }
230
+ };
231
+ // `verifying` = we hold a measurement and are checking it.
232
+ emit('verifying', false);
233
+ // `completed` = the measurement confirms the drop; publish it.
234
+ emit('completed', true);
235
+ }
@@ -1,7 +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
+ import type { CompactLifecycleRecord, CompactLifecycleStage } from '../compact-statusline/compact-lifecycle-store.js';
5
5
  export interface AutoCompactInput {
6
6
  /** Project root for context (default cwd). */
7
7
  readonly projectRoot: string;
@@ -35,16 +35,8 @@ import { resolveOuterSessionId } from '../session/binding-status-service.js';
35
35
  import { AUTO_COMPACT_PRE_COMPACT_RATIO, AUTO_COMPACT_THRESHOLD_RATIO } from '../context/auto-compact-types.js';
36
36
  import { describeMode, thresholdFor } from './auto-compact-modes.js';
37
37
  import { read24hState } from '../24h-mode/store.js';
38
- import { readCompactLifecycle, writeCompactLifecycle } from '../compact-statusline/compact-lifecycle-store.js';
38
+ import { CompactLifecyclePublisher, newCompactRunId, settleOpenLifecycleRun, summarizeLifecycleError } from './auto-compact-lifecycle.js';
39
39
  const PRE_COMPACT_REASON = 'pre-compact-auto';
40
- /**
41
- * PRD-002b slice 2 — extract the few cross-cutting magic numbers
42
- * that actually appear at runtime call sites in this orchestrator.
43
- * Threshold ratios (0.85 / 0.95) live in `auto-compact-types.ts`
44
- * and are NOT extracted here because they ARE the contract; comment
45
- * + spec prose references must keep the literal value visible.
46
- */
47
- const COLLAPSED_ERROR_MAX_CHARS = 160;
48
40
  /**
49
41
  * Map a context ratio to a `CompactTrigger` action. Pure; the side
50
42
  * effects (checkpoint + IDE dispatch) live in `runAutoCompact`. Two
@@ -278,219 +270,6 @@ function resolveAutoCompactMode(projectRoot, sessionId) {
278
270
  return 'standard';
279
271
  }
280
272
  }
281
- /**
282
- * One id per compact attempt. Timestamp-prefixed so a human reading
283
- * the raw record can order runs by eye; the random suffix keeps two
284
- * attempts inside the same millisecond distinct.
285
- */
286
- function newCompactRunId(now) {
287
- const stamp = now.toISOString().replace(/[:.]/g, '-');
288
- const suffix = Math.random().toString(16).slice(2, 8);
289
- return `compact-${stamp}-${suffix}`;
290
- }
291
- /**
292
- * Slice 2026-08-01-compact-lifecycle (Task 5): read an open compact
293
- * record ignoring staleness.
294
- *
295
- * The lifecycle store classifies an active record as `stalled` after a
296
- * caller-supplied `staleAfterMs` window. That classification exists for
297
- * the statusline renderer (Task 2/3) — it tells the user "this run
298
- * hasn't heartbeated for a while".
299
- *
300
- * For settling a run by measurement, staleness is irrelevant: if a
301
- * record exists at `compacting`, a post-compact probe that measures a
302
- * drop should still complete it, even if the probe itself ran hours
303
- * later. Wrapping that unbounded read here keeps the magic number out
304
- * of the settle path and makes the intent self-documenting.
305
- *
306
- * Returns `null` for any non-`valid` kind (missing / invalid /
307
- * stalled). Errors from the underlying read are swallowed — settling
308
- * is best-effort telemetry and must never bubble.
309
- */
310
- function readOpenCompactLifecycle(input) {
311
- let out;
312
- try {
313
- out = readCompactLifecycle({
314
- projectRoot: input.projectRoot,
315
- sessionId: input.sessionId,
316
- nowMs: Date.now(),
317
- // Number.MAX_SAFE_INTEGER disables the store's staleness gate
318
- // for this call — see the doc comment above for why settling
319
- // intentionally ignores staleness.
320
- staleAfterMs: Number.MAX_SAFE_INTEGER
321
- });
322
- }
323
- catch {
324
- return null;
325
- }
326
- return out.kind === 'valid' ? out.record : null;
327
- }
328
- /**
329
- * Slice 2026-08-01-compact-lifecycle (Task 5): the local transition
330
- * builder. Carries `runId`, `triggerRatio` and `redLine` forward from
331
- * the run that opened, and remembers the prior stage so a failure can
332
- * name the stage it died in.
333
- *
334
- * TRUTHFULNESS: this publisher only ever emits a stage the calling
335
- * process has actually PROVED. It never emits `verifying` or
336
- * `completed` off the back of a successful dispatch — see
337
- * `runAutoCompact` and `settleOpenLifecycleRun` for the reason.
338
- *
339
- * Telemetry is strictly subordinate to the compact itself: every write
340
- * is best-effort, and a store failure must not change the threshold
341
- * decision, the dispatch, or the returned envelope.
342
- */
343
- class CompactLifecyclePublisher {
344
- ctx;
345
- lastStage = 'queued';
346
- constructor(ctx) {
347
- this.ctx = ctx;
348
- }
349
- /** Publish an active stage the process has proved. */
350
- advance(stage) {
351
- this.lastStage = stage;
352
- this.write({
353
- schemaVersion: 1,
354
- runId: this.ctx.runId,
355
- stage,
356
- updatedAt: new Date().toISOString(),
357
- triggerRatio: this.ctx.triggerRatio,
358
- redLine: this.ctx.redLine
359
- });
360
- }
361
- /** Publish the terminal failure, attributed to the last stage reached. */
362
- fail(error) {
363
- this.write({
364
- schemaVersion: 1,
365
- runId: this.ctx.runId,
366
- stage: 'failed',
367
- updatedAt: new Date().toISOString(),
368
- triggerRatio: this.ctx.triggerRatio,
369
- redLine: this.ctx.redLine,
370
- failedAt: this.lastStage,
371
- errorSummary: summarizeLifecycleError(error)
372
- });
373
- }
374
- write(record) {
375
- try {
376
- if (this.ctx.failLifecycleWrite)
377
- throw new Error('lifecycle store unavailable');
378
- writeCompactLifecycle({
379
- projectRoot: this.ctx.projectRoot,
380
- sessionId: this.ctx.sessionId,
381
- record
382
- });
383
- }
384
- catch {
385
- // Best-effort telemetry: a lifecycle write failure must never
386
- // change the compact decision, the dispatch, or the envelope.
387
- return;
388
- }
389
- try {
390
- this.ctx.onLifecycleStage?.(record.stage, record);
391
- }
392
- catch {
393
- // An observer is a passive listener; its failure is not ours.
394
- }
395
- }
396
- }
397
- /**
398
- * Reduce an arbitrary thrown value to a single-line, bounded summary
399
- * fit for a statusline. Stack frames are dropped (the record is a
400
- * human-facing indicator, not a crash dump); the store clamps the
401
- * result to its own 160-character cap as a second line of defence.
402
- *
403
- * `null` / `undefined` thrown values — a real possibility from
404
- * `Promise.reject(null)` or a thrown `undefined` — must not collapse
405
- * to the empty string, which the store would then reject as missing.
406
- * They map to a fixed "unknown error" sentinel so the record always
407
- * carries some diagnostic text.
408
- */
409
- function summarizeLifecycleError(error) {
410
- let raw;
411
- if (error instanceof Error)
412
- raw = error.message;
413
- else if (error === null || error === undefined)
414
- raw = 'unknown error';
415
- else
416
- raw = String(error);
417
- if (raw.length === 0)
418
- raw = 'unknown error';
419
- const firstLine = raw.split('\n')[0] ?? '';
420
- const collapsed = firstLine.replace(/\s+/g, ' ').trim();
421
- return collapsed.length === 0 ? 'unknown error' : (collapsed.length > COLLAPSED_ERROR_MAX_CHARS ? collapsed.slice(0, COLLAPSED_ERROR_MAX_CHARS) : collapsed);
422
- }
423
- /**
424
- * Slice 2026-08-01-compact-lifecycle (Task 5, Step 4): close out an
425
- * open compact run using a REAL measurement.
426
- *
427
- * This is the integration with the actual post-compact detection path.
428
- * The claude-code adapter's `postCompactDetectCommand` is
429
- * `peaks compact auto --json` — i.e. the next probe through this very
430
- * function. So when a probe finds a run still sitting at `compacting`
431
- * and MEASURES a ratio that has dropped below the auto-fire threshold,
432
- * that measurement is the proof the compact landed. Only then do we
433
- * emit `verifying` (we have a measurement in hand) followed by
434
- * `completed` (it confirms the drop), carrying the measured
435
- * `afterRatio`.
436
- *
437
- * We refuse to complete when:
438
- * - the probe could not measure anything (`conservative-fallback`
439
- * returns `ratio: 0`, which means "unknown", NOT "empty"). Writing
440
- * `afterRatio: 0` there would publish a fabricated number;
441
- * - the ratio is still at or above the auto-fire threshold — the
442
- * compact has not landed, so the run stays open.
443
- */
444
- function settleOpenLifecycleRun(input) {
445
- // A `conservative-fallback` probe means no signal was available at
446
- // all. Its `ratio: 0` is the absence of a measurement, so it can
447
- // never be evidence that the context shrank.
448
- if (input.source === 'conservative-fallback')
449
- return;
450
- if (input.measuredRatio >= input.autoFireThreshold)
451
- return;
452
- const prior = readOpenCompactLifecycle({
453
- projectRoot: input.projectRoot,
454
- sessionId: input.sessionId
455
- });
456
- if (prior === null)
457
- return;
458
- // Only a run that was actually dispatched (`compacting`) can be
459
- // completed by a post-compact measurement.
460
- if (prior.stage !== 'compacting')
461
- return;
462
- const emit = (stage, withAfterRatio) => {
463
- const record = {
464
- schemaVersion: 1,
465
- runId: prior.runId,
466
- stage,
467
- updatedAt: new Date().toISOString(),
468
- triggerRatio: prior.triggerRatio,
469
- redLine: prior.redLine,
470
- ...(withAfterRatio ? { afterRatio: input.measuredRatio } : {})
471
- };
472
- try {
473
- writeCompactLifecycle({
474
- projectRoot: input.projectRoot,
475
- sessionId: input.sessionId,
476
- record
477
- });
478
- }
479
- catch {
480
- return;
481
- }
482
- try {
483
- input.onLifecycleStage?.(stage, record);
484
- }
485
- catch {
486
- // Observer failures are not ours to propagate.
487
- }
488
- };
489
- // `verifying` = we hold a measurement and are checking it.
490
- emit('verifying', false);
491
- // `completed` = the measurement confirms the drop; publish it.
492
- emit('completed', true);
493
- }
494
273
  /**
495
274
  * Execute the auto-compact flow.
496
275
  *
@@ -0,0 +1,22 @@
1
+ import { type CodegraphProcessRunner } from './codegraph-service.js';
2
+ export type CodegraphAutorefreshResult = {
3
+ refreshed: true;
4
+ } | {
5
+ refreshed: false;
6
+ reason: 'no-codegraph-dir' | 'index-failed' | 'unavailable';
7
+ note: string;
8
+ };
9
+ /**
10
+ * True when `<projectRoot>/.codegraph/` exists and is a directory.
11
+ * Pure fs probe; never throws.
12
+ */
13
+ export declare function isCodegraphPresent(projectRoot: string): boolean;
14
+ /**
15
+ * Run a best-effort `codegraph index` refresh for `projectRoot` after a
16
+ * slice-complete boundary. NEVER throws — every failure path returns a
17
+ * non-refreshed result so the caller keeps its ok envelope.
18
+ *
19
+ * The optional `runner` is a test seam mirroring
20
+ * `CodegraphProcessRunner`; when omitted the real process runner is used.
21
+ */
22
+ export declare function refreshCodegraphAfterSlice(projectRoot: string, runner?: CodegraphProcessRunner): Promise<CodegraphAutorefreshResult>;
@@ -0,0 +1,91 @@
1
+ // src/services/codegraph/codegraph-autorefresh.ts
2
+ //
3
+ // Slice 2026-09-03-codegraph-autorefresh — Option 1: CLI-internal
4
+ // auto codegraph refresh at the slice-complete boundary.
5
+ //
6
+ // `peaks codegraph index` is incremental + idempotent, so re-running it
7
+ // after a slice that changed code is cheap and safe. Rather than rely on
8
+ // the orchestrator LLM to remember the prose rule in
9
+ // `skills/peaks-code/references/codegraph-orchestration.md` ("MUST
10
+ // proactively run `peaks codegraph index --project <path>` after each
11
+ // slice"), the checkpoint/transition command itself triggers the refresh
12
+ // right before it returns its ok envelope. This is the vendor-neutral
13
+ // CLI-internal form of "hook on slice-complete": it is un-bypassable
14
+ // (fires even when the LLM dispatches the command through any IDE / no
15
+ // hook install surface needed), fires exactly once at the true slice
16
+ // boundary, and needs no IDE hook plumbing.
17
+ //
18
+ // The refresh is best-effort and FAIL-SILENT — it never throws and never
19
+ // blocks the checkpoint/transition ok envelope:
20
+ // - No `<projectRoot>/.codegraph/` directory → skip (codegraph was
21
+ // never initialized for this project; `peaks codegraph init` is a
22
+ // one-time setup the orchestrator owns).
23
+ // - The upstream index exits non-zero → return `index-failed` with a
24
+ // human-readable note.
25
+ // - Any unexpected error → return `unavailable` with a note.
26
+ //
27
+ // We deliberately do NOT auto-init: `codegraph init` can prompt / take a
28
+ // long time on first run, which would make the background side-effect
29
+ // block the slice boundary. Projects that want auto-refresh first run
30
+ // `peaks codegraph init` once (per the orchestration doc).
31
+ import { statSync } from 'node:fs';
32
+ import { join } from 'node:path';
33
+ import { CODEGRAPH_DIR_NAME, createCodegraphInvocation, executeCodegraphInvocation, } from './codegraph-service.js';
34
+ /**
35
+ * True when `<projectRoot>/.codegraph/` exists and is a directory.
36
+ * Pure fs probe; never throws.
37
+ */
38
+ export function isCodegraphPresent(projectRoot) {
39
+ try {
40
+ return statSync(join(projectRoot, CODEGRAPH_DIR_NAME)).isDirectory();
41
+ }
42
+ catch {
43
+ return false;
44
+ }
45
+ }
46
+ function errorMessage(error) {
47
+ return error instanceof Error ? error.message : String(error);
48
+ }
49
+ function firstMeaningfulLine(text) {
50
+ const trimmed = text.trim();
51
+ if (trimmed.length === 0)
52
+ return 'no upstream output';
53
+ const first = trimmed.split(/\r?\n/)[0];
54
+ return first !== undefined ? first.slice(0, 200) : 'no upstream output';
55
+ }
56
+ /**
57
+ * Run a best-effort `codegraph index` refresh for `projectRoot` after a
58
+ * slice-complete boundary. NEVER throws — every failure path returns a
59
+ * non-refreshed result so the caller keeps its ok envelope.
60
+ *
61
+ * The optional `runner` is a test seam mirroring
62
+ * `CodegraphProcessRunner`; when omitted the real process runner is used.
63
+ */
64
+ export async function refreshCodegraphAfterSlice(projectRoot, runner) {
65
+ if (!isCodegraphPresent(projectRoot)) {
66
+ return {
67
+ refreshed: false,
68
+ reason: 'no-codegraph-dir',
69
+ note: `auto codegraph refresh skipped: no ${CODEGRAPH_DIR_NAME} directory at ${join(projectRoot, CODEGRAPH_DIR_NAME)}. Run \`peaks codegraph init\` once to enable post-slice auto-refresh.`,
70
+ };
71
+ }
72
+ try {
73
+ const invocation = createCodegraphInvocation({ subcommand: 'index', project: projectRoot, quiet: true });
74
+ const result = await executeCodegraphInvocation(invocation, runner);
75
+ if (result.exitCode !== 0) {
76
+ return {
77
+ refreshed: false,
78
+ reason: 'index-failed',
79
+ note: `auto codegraph refresh failed (exit ${String(result.exitCode)}): ${firstMeaningfulLine(result.stderr || result.stdout)}`,
80
+ };
81
+ }
82
+ return { refreshed: true };
83
+ }
84
+ catch (error) {
85
+ return {
86
+ refreshed: false,
87
+ reason: 'unavailable',
88
+ note: `auto codegraph refresh unavailable: ${errorMessage(error)}`,
89
+ };
90
+ }
91
+ }
@@ -0,0 +1,53 @@
1
+ import { type CodegraphProcessRunner } from './codegraph-service.js';
2
+ export type CodegraphPreflightResult = {
3
+ available: true;
4
+ block: string;
5
+ fileCount: number;
6
+ truncated: boolean;
7
+ } | {
8
+ available: false;
9
+ note: string;
10
+ };
11
+ /** Cap for the directory histogram in the rendered structure block. */
12
+ export declare const CODEGRAPH_STRUCTURE_MAX_DIRS = 40;
13
+ /** Cap for bare root files listed in the rendered structure block. */
14
+ export declare const CODEGRAPH_STRUCTURE_MAX_ROOT_FILES = 12;
15
+ export interface CodegraphStructureFileEntry {
16
+ readonly path: string;
17
+ }
18
+ export interface CodegraphStructureRenderOptions {
19
+ readonly maxDirs?: number;
20
+ readonly maxRootFiles?: number;
21
+ }
22
+ export interface CodegraphStructureSummary {
23
+ /** Full `## Codegraph structure` markdown block, ending on its own paragraph. */
24
+ block: string;
25
+ total: number;
26
+ truncated: boolean;
27
+ }
28
+ /**
29
+ * Pure renderer: turn the codegraph `files --json` payload into a bounded
30
+ * `## Codegraph structure` block. Files are aggregated into a directory
31
+ * histogram (immediate parent dir; bare root files bucket separately),
32
+ * sorted by file count descending then name ascending, and capped at
33
+ * `maxDirs` rows / `maxRootFiles` root entries. Exported separately so the
34
+ * bounded-output contract is unit-testable without a filesystem.
35
+ */
36
+ export declare function renderCodegraphStructureBlock(files: readonly CodegraphStructureFileEntry[], options?: CodegraphStructureRenderOptions): CodegraphStructureSummary;
37
+ /**
38
+ * Pre-dispatch codegraph preflight. Returns a `## Codegraph structure`
39
+ * block when the index is (or becomes) readable; otherwise an
40
+ * `{ available: false, note }` result. NEVER throws — the caller must be
41
+ * able to degrade gracefully on every failure path.
42
+ *
43
+ * Behavior matrix (acceptance criteria):
44
+ * - `.codegraph/` absent → init + index (best-effort), then read.
45
+ * - `.codegraph/` present with peaks-loop marker → skip init/index
46
+ * (fresh), read directly. No redundant re-index on every dispatch.
47
+ * - `.codegraph/` present WITHOUT marker (foreign schema) → fail-soft;
48
+ * never clobber a foreign store.
49
+ *
50
+ * The optional `runner` mirrors `CodegraphProcessRunner` and is the ONLY
51
+ * injected boundary (tests fake the upstream binary).
52
+ */
53
+ export declare function buildCodegraphPreflightBlock(projectRoot: string, runner?: CodegraphProcessRunner): Promise<CodegraphPreflightResult>;