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.
- package/CHANGELOG.md +30 -0
- package/dist/cli/commands/_register.js +4 -0
- package/dist/cli/commands/dispatch-commands.d.ts +5 -41
- package/dist/cli/commands/dispatch-commands.js +59 -185
- package/dist/cli/commands/evidence-commands.d.ts +12 -0
- package/dist/cli/commands/evidence-commands.js +43 -0
- package/dist/cli/commands/fresh-context-commands.d.ts +15 -0
- package/dist/cli/commands/fresh-context-commands.js +24 -0
- package/dist/cli/commands/job-commands.js +14 -1
- package/dist/cli/commands/request-commands.js +15 -97
- package/dist/cli/commands/request-format-helpers.d.ts +19 -0
- package/dist/cli/commands/request-format-helpers.js +102 -0
- package/dist/cli/commands/worktree-auth-commands.js +2 -472
- package/dist/cli/commands/worktree-lease-commands.d.ts +21 -0
- package/dist/cli/commands/worktree-lease-commands.js +500 -0
- package/dist/services/artifacts/request-artifact-service.js +13 -3
- package/dist/services/code/auto-compact-lifecycle.d.ts +102 -0
- package/dist/services/code/auto-compact-lifecycle.js +235 -0
- package/dist/services/code/auto-compact-orchestrator.d.ts +1 -1
- package/dist/services/code/auto-compact-orchestrator.js +1 -222
- package/dist/services/codegraph/codegraph-autorefresh.d.ts +22 -0
- package/dist/services/codegraph/codegraph-autorefresh.js +91 -0
- package/dist/services/codegraph/codegraph-preflight-service.d.ts +53 -0
- package/dist/services/codegraph/codegraph-preflight-service.js +226 -0
- package/dist/services/context/build-dispatch-system-prompt.d.ts +52 -0
- package/dist/services/context/build-dispatch-system-prompt.js +70 -3
- package/dist/services/dispatch/dispatch-record-types.d.ts +278 -0
- package/dist/services/dispatch/dispatch-record-types.js +15 -0
- package/dist/services/dispatch/dispatch-record-upgrade.d.ts +5 -0
- package/dist/services/dispatch/dispatch-record-upgrade.js +230 -0
- package/dist/services/dispatch/dispatch-record-writer.d.ts +3 -278
- package/dist/services/dispatch/dispatch-record-writer.js +3 -245
- package/dist/services/dispatch/dispatch-sub-agent.d.ts +43 -0
- package/dist/services/dispatch/dispatch-sub-agent.js +55 -0
- package/dist/services/dispatch/isolation-lease.d.ts +43 -0
- package/dist/services/dispatch/isolation-lease.js +129 -0
- package/dist/services/evidence/evidence-generator.d.ts +26 -0
- package/dist/services/evidence/evidence-generator.js +349 -0
- package/dist/services/fresh-context/config.d.ts +1 -0
- package/dist/services/fresh-context/config.js +13 -0
- package/dist/services/fresh-context/fresh-context-block.d.ts +12 -0
- package/dist/services/fresh-context/fresh-context-block.js +40 -0
- package/dist/services/fresh-context/trigger-scan.d.ts +30 -0
- package/dist/services/fresh-context/trigger-scan.js +29 -0
- package/dist/services/skills/hooks-codegate-superpowers.d.ts +65 -0
- package/dist/services/skills/hooks-codegate-superpowers.js +204 -0
- package/dist/services/skills/hooks-settings-service.d.ts +2 -64
- package/dist/services/skills/hooks-settings-service.js +2 -215
- package/dist/services/skills/skill-statusline-renderer.d.ts +2 -34
- package/dist/services/skills/skill-statusline-renderer.js +1 -184
- package/dist/services/skills/statusline-palette.d.ts +62 -0
- package/dist/services/skills/statusline-palette.js +190 -0
- package/dist/services/slice/slice-decompose-import-edges.d.ts +8 -0
- package/dist/services/slice/slice-decompose-import-edges.js +102 -0
- package/dist/services/slice/slice-decompose-service.js +3 -198
- package/dist/services/slice/slice-decompose-tarjan.d.ts +8 -0
- package/dist/services/slice/slice-decompose-tarjan.js +108 -0
- package/dist/services/standards/project-standards-service.d.ts +1 -9
- package/dist/services/standards/project-standards-service.js +3 -232
- package/dist/services/standards/standards-render.d.ts +23 -0
- package/dist/services/standards/standards-render.js +238 -0
- package/dist/services/standards/ui-library-dispatch-block.d.ts +27 -0
- package/dist/services/standards/ui-library-dispatch-block.js +48 -0
- package/dist/services/workspace/reconcile-migrate.d.ts +77 -0
- package/dist/services/workspace/reconcile-migrate.js +230 -0
- package/dist/services/workspace/reconcile-service.d.ts +0 -72
- package/dist/services/workspace/reconcile-service.js +3 -220
- package/dist/services/workspace/workspace-service.js +13 -2
- package/dist/shared/incrementing-number.d.ts +11 -0
- package/dist/shared/incrementing-number.js +16 -3
- package/package.json +5 -5
- package/skills/peaks-code/SKILL.md +6 -2
- package/skills/peaks-code/references/fresh-context-preflight.md +81 -0
- 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 {
|
|
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 {
|
|
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>;
|