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.
- package/CHANGELOG.md +27 -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/workspace/init-command.js +27 -14
- 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.js +44 -6
- package/dist/services/codegraph/codegraph-preflight-service.d.ts +5 -2
- package/dist/services/codegraph/codegraph-preflight-service.js +13 -6
- package/dist/services/codegraph/codegraph-service.d.ts +17 -2
- package/dist/services/codegraph/codegraph-service.js +27 -2
- 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,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 {
|
|
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
|
*
|
|
@@ -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
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
|
|
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
|
|
46
|
-
* (fresh), read directly. No redundant re-index on
|
|
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
|
*
|