peaks-loop 4.0.29 → 4.0.31

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 (75) hide show
  1. package/CHANGELOG.md +27 -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 +35 -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/request-commands.js +2 -96
  10. package/dist/cli/commands/request-format-helpers.d.ts +19 -0
  11. package/dist/cli/commands/request-format-helpers.js +102 -0
  12. package/dist/cli/commands/workspace/init-command.js +27 -14
  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.js +44 -6
  22. package/dist/services/codegraph/codegraph-preflight-service.d.ts +5 -2
  23. package/dist/services/codegraph/codegraph-preflight-service.js +13 -6
  24. package/dist/services/codegraph/codegraph-service.d.ts +17 -2
  25. package/dist/services/codegraph/codegraph-service.js +27 -2
  26. package/dist/services/context/build-dispatch-system-prompt.d.ts +26 -0
  27. package/dist/services/context/build-dispatch-system-prompt.js +38 -3
  28. package/dist/services/dispatch/dispatch-record-types.d.ts +278 -0
  29. package/dist/services/dispatch/dispatch-record-types.js +15 -0
  30. package/dist/services/dispatch/dispatch-record-upgrade.d.ts +5 -0
  31. package/dist/services/dispatch/dispatch-record-upgrade.js +230 -0
  32. package/dist/services/dispatch/dispatch-record-writer.d.ts +3 -278
  33. package/dist/services/dispatch/dispatch-record-writer.js +3 -245
  34. package/dist/services/dispatch/dispatch-sub-agent.d.ts +43 -0
  35. package/dist/services/dispatch/dispatch-sub-agent.js +55 -0
  36. package/dist/services/dispatch/isolation-lease.d.ts +43 -0
  37. package/dist/services/dispatch/isolation-lease.js +129 -0
  38. package/dist/services/evidence/evidence-generator.d.ts +26 -0
  39. package/dist/services/evidence/evidence-generator.js +349 -0
  40. package/dist/services/fresh-context/config.d.ts +1 -0
  41. package/dist/services/fresh-context/config.js +13 -0
  42. package/dist/services/fresh-context/fresh-context-block.d.ts +12 -0
  43. package/dist/services/fresh-context/fresh-context-block.js +40 -0
  44. package/dist/services/fresh-context/trigger-scan.d.ts +30 -0
  45. package/dist/services/fresh-context/trigger-scan.js +29 -0
  46. package/dist/services/skills/hooks-codegate-superpowers.d.ts +65 -0
  47. package/dist/services/skills/hooks-codegate-superpowers.js +204 -0
  48. package/dist/services/skills/hooks-settings-service.d.ts +2 -64
  49. package/dist/services/skills/hooks-settings-service.js +2 -215
  50. package/dist/services/skills/skill-statusline-renderer.d.ts +2 -34
  51. package/dist/services/skills/skill-statusline-renderer.js +1 -184
  52. package/dist/services/skills/statusline-palette.d.ts +62 -0
  53. package/dist/services/skills/statusline-palette.js +190 -0
  54. package/dist/services/slice/slice-decompose-import-edges.d.ts +8 -0
  55. package/dist/services/slice/slice-decompose-import-edges.js +102 -0
  56. package/dist/services/slice/slice-decompose-service.js +3 -198
  57. package/dist/services/slice/slice-decompose-tarjan.d.ts +8 -0
  58. package/dist/services/slice/slice-decompose-tarjan.js +108 -0
  59. package/dist/services/standards/project-standards-service.d.ts +1 -9
  60. package/dist/services/standards/project-standards-service.js +3 -232
  61. package/dist/services/standards/standards-render.d.ts +23 -0
  62. package/dist/services/standards/standards-render.js +238 -0
  63. package/dist/services/standards/ui-library-dispatch-block.d.ts +27 -0
  64. package/dist/services/standards/ui-library-dispatch-block.js +48 -0
  65. package/dist/services/workspace/reconcile-migrate.d.ts +77 -0
  66. package/dist/services/workspace/reconcile-migrate.js +230 -0
  67. package/dist/services/workspace/reconcile-service.d.ts +0 -72
  68. package/dist/services/workspace/reconcile-service.js +3 -220
  69. package/dist/services/workspace/workspace-service.js +13 -2
  70. package/dist/shared/incrementing-number.d.ts +11 -0
  71. package/dist/shared/incrementing-number.js +16 -3
  72. package/package.json +5 -5
  73. package/skills/peaks-code/SKILL.md +6 -2
  74. package/skills/peaks-code/references/fresh-context-preflight.md +81 -0
  75. package/skills/peaks-code/references/sub-agent-dispatch.md +27 -8
@@ -0,0 +1,102 @@
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 { type CompactLifecycleRecord, type CompactLifecycleStage } from '../compact-statusline/compact-lifecycle-store.js';
15
+ /**
16
+ * Stages a compact *attempt* can prove from inside the dispatching
17
+ * process. `verifying` / `completed` are deliberately absent — see
18
+ * `CompactLifecyclePublisher` and `settleOpenLifecycleRun` for why.
19
+ */
20
+ type ObservableDispatchStage = Extract<CompactLifecycleStage, 'queued' | 'preparing' | 'compacting'>;
21
+ /**
22
+ * One id per compact attempt. Timestamp-prefixed so a human reading
23
+ * the raw record can order runs by eye; the random suffix keeps two
24
+ * attempts inside the same millisecond distinct.
25
+ */
26
+ export declare function newCompactRunId(now: Date): string;
27
+ /**
28
+ * Slice 2026-08-01-compact-lifecycle (Task 5): the local transition
29
+ * builder. Carries `runId`, `triggerRatio` and `redLine` forward from
30
+ * the run that opened, and remembers the prior stage so a failure can
31
+ * name the stage it died in.
32
+ *
33
+ * TRUTHFULNESS: this publisher only ever emits a stage the calling
34
+ * process has actually PROVED. It never emits `verifying` or
35
+ * `completed` off the back of a successful dispatch — see
36
+ * `runAutoCompact` and `settleOpenLifecycleRun` for the reason.
37
+ *
38
+ * Telemetry is strictly subordinate to the compact itself: every write
39
+ * is best-effort, and a store failure must not change the threshold
40
+ * decision, the dispatch, or the returned envelope.
41
+ */
42
+ export declare class CompactLifecyclePublisher {
43
+ private readonly ctx;
44
+ private lastStage;
45
+ constructor(ctx: {
46
+ readonly projectRoot: string;
47
+ readonly sessionId: string;
48
+ readonly runId: string;
49
+ readonly triggerRatio: number;
50
+ readonly redLine: boolean;
51
+ readonly onLifecycleStage?: ((stage: CompactLifecycleStage, record: CompactLifecycleRecord) => void) | undefined;
52
+ readonly failLifecycleWrite?: boolean | undefined;
53
+ });
54
+ /** Publish an active stage the process has proved. */
55
+ advance(stage: ObservableDispatchStage): void;
56
+ /** Publish the terminal failure, attributed to the last stage reached. */
57
+ fail(error: unknown): void;
58
+ private write;
59
+ }
60
+ /**
61
+ * Reduce an arbitrary thrown value to a single-line, bounded summary
62
+ * fit for a statusline. Stack frames are dropped (the record is a
63
+ * human-facing indicator, not a crash dump); the store clamps the
64
+ * result to its own 160-character cap as a second line of defence.
65
+ *
66
+ * `null` / `undefined` thrown values — a real possibility from
67
+ * `Promise.reject(null)` or a thrown `undefined` — must not collapse
68
+ * to the empty string, which the store would then reject as missing.
69
+ * They map to a fixed "unknown error" sentinel so the record always
70
+ * carries some diagnostic text.
71
+ */
72
+ export declare function summarizeLifecycleError(error: unknown): string;
73
+ /**
74
+ * Slice 2026-08-01-compact-lifecycle (Task 5, Step 4): close out an
75
+ * open compact run using a REAL measurement.
76
+ *
77
+ * This is the integration with the actual post-compact detection path.
78
+ * The claude-code adapter's `postCompactDetectCommand` is
79
+ * `peaks compact auto --json` — i.e. the next probe through this very
80
+ * function. So when a probe finds a run still sitting at `compacting`
81
+ * and MEASURES a ratio that has dropped below the auto-fire threshold,
82
+ * that measurement is the proof the compact landed. Only then do we
83
+ * emit `verifying` (we have a measurement in hand) followed by
84
+ * `completed` (it confirms the drop), carrying the measured
85
+ * `afterRatio`.
86
+ *
87
+ * We refuse to complete when:
88
+ * - the probe could not measure anything (`conservative-fallback`
89
+ * returns `ratio: 0`, which means "unknown", NOT "empty"). Writing
90
+ * `afterRatio: 0` there would publish a fabricated number;
91
+ * - the ratio is still at or above the auto-fire threshold — the
92
+ * compact has not landed, so the run stays open.
93
+ */
94
+ export declare function settleOpenLifecycleRun(input: {
95
+ readonly projectRoot: string;
96
+ readonly sessionId: string;
97
+ readonly measuredRatio: number;
98
+ readonly source: string;
99
+ readonly autoFireThreshold: number;
100
+ readonly onLifecycleStage?: ((stage: CompactLifecycleStage, record: CompactLifecycleRecord) => void) | undefined;
101
+ }): void;
102
+ export {};
@@ -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
  *
@@ -20,17 +20,24 @@
20
20
  // - No `<projectRoot>/.codegraph/` directory → skip (codegraph was
21
21
  // never initialized for this project; `peaks codegraph init` is a
22
22
  // one-time setup the orchestrator owns).
23
+ // - `.codegraph/` present WITH `codegraph.db` → `index` (incremental).
24
+ // - `.codegraph/` present with the peaks-loop marker but NO
25
+ // `codegraph.db` (dangling) → self-heal: `init` then `index`.
26
+ // - `.codegraph/` present without a marker and without `codegraph.db`
27
+ // (foreign schema) → skip; never touch a foreign store.
23
28
  // - The upstream index exits non-zero → return `index-failed` with a
24
29
  // human-readable note.
25
30
  // - Any unexpected error → return `unavailable` with a note.
26
31
  //
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
+ // We do NOT auto-init a genuinely fresh (no `.codegraph/` dir) project:
33
+ // the orchestrator owns that one-time setup. The dangling self-heal above
34
+ // IS an auto-init, but `codegraph init` (WITHOUT `--index`) is fast (~1 s)
35
+ // and offline-safe — it only `Parser.init()`s WASM grammars that resolve
36
+ // from node_modules. The slow/offline concern applies to `index` (full
37
+ // build), not `init`.
38
+ import { existsSync, statSync } from 'node:fs';
32
39
  import { join } from 'node:path';
33
- import { CODEGRAPH_DIR_NAME, createCodegraphInvocation, executeCodegraphInvocation, } from './codegraph-service.js';
40
+ import { CODEGRAPH_DIR_NAME, CODEGRAPH_MARKER_NAME, createCodegraphInvocation, executeCodegraphInvocation, isCodegraphInitialized, } from './codegraph-service.js';
34
41
  /**
35
42
  * True when `<projectRoot>/.codegraph/` exists and is a directory.
36
43
  * Pure fs probe; never throws.
@@ -43,6 +50,14 @@ export function isCodegraphPresent(projectRoot) {
43
50
  return false;
44
51
  }
45
52
  }
53
+ /**
54
+ * True when `<projectRoot>/.codegraph/` carries the peaks-loop marker
55
+ * (i.e. peaks-loop manages it, as opposed to a foreign tool). Pure fs
56
+ * probe; never throws.
57
+ */
58
+ function isCodegraphPeaksLoopManaged(projectRoot) {
59
+ return existsSync(join(projectRoot, CODEGRAPH_DIR_NAME, CODEGRAPH_MARKER_NAME));
60
+ }
46
61
  function errorMessage(error) {
47
62
  return error instanceof Error ? error.message : String(error);
48
63
  }
@@ -70,6 +85,29 @@ export async function refreshCodegraphAfterSlice(projectRoot, runner) {
70
85
  };
71
86
  }
72
87
  try {
88
+ // A `.codegraph/` dir without a `codegraph.db` is uninitialized:
89
+ // - NOT peaks-loop-managed → foreign schema, never touch it.
90
+ // - peaks-loop-managed → the dangling state left by the pre-fix
91
+ // rid-CG-001 auto-stake (marker stamped, no upstream init). Run
92
+ // init (fast, offline-safe — no full index) so the subsequent
93
+ // index has a schema to write into.
94
+ if (!isCodegraphInitialized(projectRoot)) {
95
+ if (!isCodegraphPeaksLoopManaged(projectRoot)) {
96
+ return {
97
+ refreshed: false,
98
+ reason: 'no-codegraph-dir',
99
+ note: `auto codegraph refresh skipped: ${CODEGRAPH_DIR_NAME}/ exists without a codegraph.db and is not peaks-loop-managed. Run \`peaks codegraph init\` once to enable post-slice auto-refresh.`,
100
+ };
101
+ }
102
+ const initResult = await executeCodegraphInvocation(createCodegraphInvocation({ subcommand: 'init', project: projectRoot }), runner);
103
+ if (initResult.exitCode !== 0) {
104
+ return {
105
+ refreshed: false,
106
+ reason: 'index-failed',
107
+ note: `auto codegraph refresh self-heal init failed (exit ${String(initResult.exitCode)}): ${firstMeaningfulLine(initResult.stderr || initResult.stdout)}`,
108
+ };
109
+ }
110
+ }
73
111
  const invocation = createCodegraphInvocation({ subcommand: 'index', project: projectRoot, quiet: true });
74
112
  const result = await executeCodegraphInvocation(invocation, runner);
75
113
  if (result.exitCode !== 0) {
@@ -42,8 +42,11 @@ export declare function renderCodegraphStructureBlock(files: readonly CodegraphS
42
42
  *
43
43
  * Behavior matrix (acceptance criteria):
44
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.
45
+ * - `.codegraph/` present with peaks-loop marker AND `codegraph.db` →
46
+ * skip init/index (fresh), read directly. No redundant re-index on
47
+ * every dispatch.
48
+ * - `.codegraph/` present with peaks-loop marker but NO `codegraph.db`
49
+ * (dangling) → init + index (self-heal), then read.
47
50
  * - `.codegraph/` present WITHOUT marker (foreign schema) → fail-soft;
48
51
  * never clobber a foreign store.
49
52
  *