peaks-loop 4.0.29 → 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 +14 -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 +35 -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/request-commands.js +2 -96
- 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/context/build-dispatch-system-prompt.d.ts +26 -0
- package/dist/services/context/build-dispatch-system-prompt.js +38 -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
|
*
|
|
@@ -42,6 +42,32 @@ export interface DispatchPromptInput {
|
|
|
42
42
|
* and the memory/task content.
|
|
43
43
|
*/
|
|
44
44
|
codegraphBlock?: string | null;
|
|
45
|
+
/**
|
|
46
|
+
* Slice 2026-09-06-ui-lib-dispatch-priority: pre-composed
|
|
47
|
+
* `## Project stack` markdown block, rendered from the detected
|
|
48
|
+
* project context (component library + CSS framework + build tool) by
|
|
49
|
+
* the dispatch site BEFORE the sub-agent's task body is composed so
|
|
50
|
+
* the RD/UI sub-agent sees the library-first directive in-context.
|
|
51
|
+
*
|
|
52
|
+
* - `undefined` → no project-stack block (legacy callers unchanged).
|
|
53
|
+
* - `null` → the project has no detected component library; the
|
|
54
|
+
* composer renders nothing (byte-identical degradation).
|
|
55
|
+
* - `string` → the block, rendered verbatim between the codegraph
|
|
56
|
+
* block and the memory/task content.
|
|
57
|
+
*/
|
|
58
|
+
projectStackBlock?: string | null;
|
|
59
|
+
/**
|
|
60
|
+
* Slice 2026-09-07-search-first-preflight: the orchestrator-synthesized
|
|
61
|
+
* `## Fresh context` block (≤5 binding directives from a Context7 →
|
|
62
|
+
* WebSearch preflight), read from `.peaks/_runtime/<sessionId>/fresh-context.md`.
|
|
63
|
+
*
|
|
64
|
+
* - `undefined` → no fresh-context block (legacy callers unchanged).
|
|
65
|
+
* - `null` → the block is unavailable (missing file / no `## Fresh context`
|
|
66
|
+
* heading); the composer renders nothing (byte-identical degradation).
|
|
67
|
+
* - `string` → the block, rendered verbatim after the project-stack block
|
|
68
|
+
* and before the memory/task content.
|
|
69
|
+
*/
|
|
70
|
+
freshContextBlock?: string | null;
|
|
45
71
|
}
|
|
46
72
|
/**
|
|
47
73
|
* Slice 2026-07-29-worktree-l1: Layer 1 of the 3-layer worktree governance
|
|
@@ -98,13 +98,15 @@ export const LIFECYCLE_RULES = `## Sub-agent lifecycle rules (locked 2026-08-01)
|
|
|
98
98
|
* refusal is in scope before any task-specific prose arrives.
|
|
99
99
|
*/
|
|
100
100
|
export function buildDispatchSystemPrompt(input) {
|
|
101
|
-
const { taskBody, memoryBlock, contextProbe, codegraphBlock } = input;
|
|
101
|
+
const { taskBody, memoryBlock, contextProbe, codegraphBlock, projectStackBlock, freshContextBlock } = input;
|
|
102
102
|
const contextBlock = renderContextBlock(contextProbe ?? null);
|
|
103
103
|
const codegraphText = renderCodegraphBlock(codegraphBlock);
|
|
104
|
+
const projectStackText = renderProjectStackBlock(projectStackBlock);
|
|
105
|
+
const freshContextText = renderFreshContextBlock(freshContextBlock);
|
|
104
106
|
if (memoryBlock.available === true && typeof memoryBlock.block === 'string') {
|
|
105
|
-
return `${L1_WORKTREE_GOVERNANCE_BLOCK}\n${LIFECYCLE_RULES}\n${contextBlock}${codegraphText}${memoryBlock.block}\n## Task\n${taskBody}`;
|
|
107
|
+
return `${L1_WORKTREE_GOVERNANCE_BLOCK}\n${LIFECYCLE_RULES}\n${contextBlock}${codegraphText}${projectStackText}${freshContextText}${memoryBlock.block}\n## Task\n${taskBody}`;
|
|
106
108
|
}
|
|
107
|
-
return `${L1_WORKTREE_GOVERNANCE_BLOCK}\n${LIFECYCLE_RULES}\n${contextBlock}${codegraphText}${taskBody}`;
|
|
109
|
+
return `${L1_WORKTREE_GOVERNANCE_BLOCK}\n${LIFECYCLE_RULES}\n${contextBlock}${codegraphText}${projectStackText}${freshContextText}${taskBody}`;
|
|
108
110
|
}
|
|
109
111
|
/**
|
|
110
112
|
* Slice 2026-09-03-codegraph-preread: fixed degradation string emitted
|
|
@@ -133,6 +135,39 @@ function renderCodegraphBlock(codegraphBlock) {
|
|
|
133
135
|
const text = codegraphBlock === null ? CODEGRAPH_UNAVAILABLE_BLOCK : codegraphBlock;
|
|
134
136
|
return `${text.replace(/\s+$/, '')}\n\n`;
|
|
135
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* Render the project-stack insertion for a dispatch prompt.
|
|
140
|
+
*
|
|
141
|
+
* - `undefined` / `null` → empty (the composer is byte-identical to the
|
|
142
|
+
* legacy shape — no dangling "Project stack" heading).
|
|
143
|
+
* - `string` → the pre-composed block from the dispatch site, verbatim.
|
|
144
|
+
*
|
|
145
|
+
* Mirrors `renderCodegraphBlock`: every non-empty variant is normalized
|
|
146
|
+
* to end on its own paragraph (`\n\n`) so the following block (project
|
|
147
|
+
* memory or task body) starts cleanly.
|
|
148
|
+
*/
|
|
149
|
+
function renderProjectStackBlock(projectStackBlock) {
|
|
150
|
+
if (projectStackBlock === undefined || projectStackBlock === null)
|
|
151
|
+
return '';
|
|
152
|
+
return `${projectStackBlock.replace(/\s+$/, '')}\n\n`;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Render the fresh-context insertion for a dispatch prompt.
|
|
156
|
+
*
|
|
157
|
+
* - `undefined` / `null` → empty (the composer is byte-identical to the
|
|
158
|
+
* legacy shape — no dangling "Fresh context" heading).
|
|
159
|
+
* - `string` → the pre-composed block from the fresh-context preflight
|
|
160
|
+
* synthesis, verbatim.
|
|
161
|
+
*
|
|
162
|
+
* Mirrors `renderProjectStackBlock`: every non-empty variant is normalized
|
|
163
|
+
* to end on its own paragraph (`\n\n`) so the following block (project
|
|
164
|
+
* memory or task body) starts cleanly.
|
|
165
|
+
*/
|
|
166
|
+
function renderFreshContextBlock(freshContextBlock) {
|
|
167
|
+
if (freshContextBlock === undefined || freshContextBlock === null)
|
|
168
|
+
return '';
|
|
169
|
+
return `${freshContextBlock.replace(/\s+$/, '')}\n\n`;
|
|
170
|
+
}
|
|
136
171
|
/**
|
|
137
172
|
* Slice 2026-07-29-context-evaluation-accuracy: emit a
|
|
138
173
|
* `## Context window` block with the authoritative ratio so the
|