peaks-cli 1.2.7 → 1.2.9
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/README.md +12 -0
- package/dist/src/cli/commands/core-artifact-commands.js +36 -1
- package/dist/src/cli/commands/perf-commands.d.ts +3 -0
- package/dist/src/cli/commands/perf-commands.js +41 -0
- package/dist/src/cli/commands/progress-close-kill.d.ts +51 -0
- package/dist/src/cli/commands/progress-close-kill.js +152 -0
- package/dist/src/cli/commands/progress-commands.d.ts +3 -0
- package/dist/src/cli/commands/progress-commands.js +348 -0
- package/dist/src/cli/commands/progress-start-spawn.d.ts +59 -0
- package/dist/src/cli/commands/progress-start-spawn.js +114 -0
- package/dist/src/cli/commands/progress-watch-render.d.ts +80 -0
- package/dist/src/cli/commands/progress-watch-render.js +308 -0
- package/dist/src/cli/commands/project-commands.js +1 -1
- package/dist/src/cli/commands/scan-commands.js +22 -0
- package/dist/src/cli/program.js +4 -0
- package/dist/src/services/config/config-types.d.ts +20 -0
- package/dist/src/services/config/config-types.js +5 -1
- package/dist/src/services/memory/project-memory-service.d.ts +1 -1
- package/dist/src/services/memory/project-memory-service.js +52 -23
- package/dist/src/services/perf/perf-baseline-service.d.ts +70 -0
- package/dist/src/services/perf/perf-baseline-service.js +213 -0
- package/dist/src/services/progress/progress-service.d.ts +179 -0
- package/dist/src/services/progress/progress-service.js +276 -0
- package/dist/src/services/scan/libraries-service.d.ts +24 -0
- package/dist/src/services/scan/libraries-service.js +419 -0
- package/dist/src/services/scan/libraries-types.d.ts +59 -0
- package/dist/src/services/scan/libraries-types.js +9 -0
- package/dist/src/services/session/index.d.ts +1 -1
- package/dist/src/services/session/index.js +1 -1
- package/dist/src/services/session/session-manager.d.ts +53 -8
- package/dist/src/services/session/session-manager.js +150 -3
- package/dist/src/services/skills/skill-presence-service.d.ts +27 -1
- package/dist/src/services/skills/skill-presence-service.js +112 -9
- package/dist/src/services/skills/skill-runbook-service.js +34 -1
- package/dist/src/services/workflow/autonomous-resume-writer.js +7 -7
- package/dist/src/shared/change-id.d.ts +30 -0
- package/dist/src/shared/change-id.js +40 -6
- package/dist/src/shared/paths.d.ts +1 -1
- package/dist/src/shared/paths.js +2 -1
- package/dist/src/shared/version.d.ts +1 -1
- package/dist/src/shared/version.js +1 -1
- package/package.json +6 -2
- package/schemas/library-breaking-changes.data.json +141 -0
- package/schemas/library-breaking-changes.meta.json +6 -0
- package/schemas/library-breaking-changes.schema.json +50 -0
- package/skills/peaks-qa/SKILL.md +25 -0
- package/skills/peaks-rd/SKILL.md +221 -2
- package/skills/peaks-solo/SKILL.md +76 -316
- package/skills/peaks-solo/references/runbook.md +166 -0
- package/skills/peaks-solo/references/workflow-gates-and-types.md +177 -0
- package/skills/peaks-solo-resume/SKILL.md +81 -0
- package/skills/peaks-solo-status/SKILL.md +120 -0
- package/skills/peaks-solo-test/SKILL.md +84 -0
- package/skills/peaks-txt/SKILL.md +8 -5
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-place progress renderer for `peaks progress watch`.
|
|
3
|
+
*
|
|
4
|
+
* Goals (in order of importance):
|
|
5
|
+
* 1. **In-place overwrite.** Every tick the dynamic rows
|
|
6
|
+
* (status line + progress bar) get rewritten, NOT
|
|
7
|
+
* appended. We bypass `io.stdout` (which is
|
|
8
|
+
* `console.log` and adds a trailing `\n` per call) and
|
|
9
|
+
* write directly to `process.stdout`. The cursor-up
|
|
10
|
+
* and erase-line escapes are the same ones terminal-kit
|
|
11
|
+
* emits; we do not need a terminal-kit dependency for
|
|
12
|
+
* them.
|
|
13
|
+
* 2. **Clear PEAKS-CLI branding.** A static 3-line header
|
|
14
|
+
* with the brand bar, the project root, and the
|
|
15
|
+
* progress-file path is painted once. The user always
|
|
16
|
+
* sees what they are looking at, even after the
|
|
17
|
+
* watch loop has overwritten the dynamic rows a
|
|
18
|
+
* thousand times.
|
|
19
|
+
* 3. **Graceful degrade.** When stdout is not a TTY
|
|
20
|
+
* (CI / pipe / `--json`), we fall back to a single
|
|
21
|
+
* static snapshot per tick (no cursor moves, no
|
|
22
|
+
* SGR colour) and a single newline. This keeps the
|
|
23
|
+
* tool scriptable without dropping into a wall of
|
|
24
|
+
* escape codes.
|
|
25
|
+
*
|
|
26
|
+
* Token-cost note: this module is rendered to the user's
|
|
27
|
+
* terminal, never into LLM context. The watch side has
|
|
28
|
+
* zero token cost.
|
|
29
|
+
*/
|
|
30
|
+
import chalk from 'chalk';
|
|
31
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
32
|
+
// Raw byte writes. We do NOT go through `io.stdout` because the
|
|
33
|
+
// default `io.stdout` is `console.log` and appends a trailing
|
|
34
|
+
// newline, which would defeat in-place overwrite.
|
|
35
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
36
|
+
function rawWrite(text) {
|
|
37
|
+
process.stdout.write(text);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Number of lines that the dynamic dashboard occupies. We
|
|
41
|
+
* rewrite exactly this many rows per tick via cursor-up +
|
|
42
|
+
* erase-line, so the static header above and the
|
|
43
|
+
* `press Ctrl-C` footer below stay put.
|
|
44
|
+
*/
|
|
45
|
+
const DYNAMIC_LINES = 2;
|
|
46
|
+
/** ANSI: cursor up N rows. */
|
|
47
|
+
const CURSOR_UP_N = (n) => `\x1b[${n}A`;
|
|
48
|
+
/** ANSI: erase the current row from cursor to end-of-line. */
|
|
49
|
+
const ERASE_LINE = '\x1b[2K';
|
|
50
|
+
/** ANSI: reset all SGR attributes. */
|
|
51
|
+
const RESET = '\x1b[0m';
|
|
52
|
+
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
53
|
+
const PHASE_LABEL = {
|
|
54
|
+
starting: 'starting',
|
|
55
|
+
running: 'running',
|
|
56
|
+
verifying: 'verifying',
|
|
57
|
+
completing: 'completing',
|
|
58
|
+
finished: 'finished',
|
|
59
|
+
failed: 'failed',
|
|
60
|
+
idle: 'idle'
|
|
61
|
+
};
|
|
62
|
+
const PHASE_COLOR = {
|
|
63
|
+
starting: chalk.cyan,
|
|
64
|
+
running: chalk.cyan,
|
|
65
|
+
verifying: chalk.cyan,
|
|
66
|
+
completing: chalk.cyan,
|
|
67
|
+
finished: chalk.green,
|
|
68
|
+
failed: chalk.red,
|
|
69
|
+
idle: chalk.gray
|
|
70
|
+
};
|
|
71
|
+
function pickSpinnerFrame(tick) {
|
|
72
|
+
return SPINNER_FRAMES[Math.abs(tick) % SPINNER_FRAMES.length];
|
|
73
|
+
}
|
|
74
|
+
function formatElapsed(ms) {
|
|
75
|
+
const totalSeconds = Math.max(0, Math.floor(ms / 1000));
|
|
76
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
77
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
78
|
+
const seconds = totalSeconds % 60;
|
|
79
|
+
if (hours > 0) {
|
|
80
|
+
return `${hours}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
|
|
81
|
+
}
|
|
82
|
+
return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* `elapsedMs` based fake progress, 0..1, capped at 1 after
|
|
86
|
+
* 10 minutes. Visual cue that the watch is alive; NOT a real
|
|
87
|
+
* percent-complete.
|
|
88
|
+
*/
|
|
89
|
+
function computeFakeProgress(data) {
|
|
90
|
+
if (data === null)
|
|
91
|
+
return 0;
|
|
92
|
+
const startedAtMs = new Date(data.current.startedAt).getTime();
|
|
93
|
+
if (Number.isNaN(startedAtMs))
|
|
94
|
+
return 0;
|
|
95
|
+
const elapsedMs = Math.max(0, Date.now() - startedAtMs);
|
|
96
|
+
const upperBoundMs = 10 * 60 * 1000;
|
|
97
|
+
return Math.min(1, elapsedMs / upperBoundMs);
|
|
98
|
+
}
|
|
99
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
100
|
+
// ASCII art for the PEAKS-CLI brand bar. Kept in code (not a
|
|
101
|
+
// dependency) so the user sees the brand even when terminal-kit
|
|
102
|
+
// is unavailable.
|
|
103
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
104
|
+
/**
|
|
105
|
+
* Three-row ASCII wordmark for PEAKS-CLI. Each letter is 5
|
|
106
|
+
* columns wide; P-E-A-K-S take 5 letters (25 cols), the
|
|
107
|
+
* dash takes 9 cols (with surrounding space), C-L-I take 3
|
|
108
|
+
* letters (15 cols). Total ≈ 49 cols, fits in 80-col
|
|
109
|
+
* terminals without wrapping.
|
|
110
|
+
*
|
|
111
|
+
* The block-character density (full blocks on the
|
|
112
|
+
* prominent rows) makes the brand visually dominant
|
|
113
|
+
* against the gray "sub-agent progress watch" tagline
|
|
114
|
+
* that sits below — so the user always knows whose
|
|
115
|
+
* dashboard they are looking at, even when the title
|
|
116
|
+
* bar is hidden.
|
|
117
|
+
*/
|
|
118
|
+
const PEAKS_CLI_ASCII = [
|
|
119
|
+
'█████ █████ █████ ██ ██ ██████ █████ ██ ███',
|
|
120
|
+
'█ █ █ █ █ █ ██ ██ ██ ██ ██ ██ ',
|
|
121
|
+
'█████ █████ █████ ██████ ██ ████ █████ ███'
|
|
122
|
+
];
|
|
123
|
+
/**
|
|
124
|
+
* Render the static header: the three-line big PEAKS-CLI
|
|
125
|
+
* wordmark, a separator, a small "sub-agent progress watch"
|
|
126
|
+
* tagline, and the project / file path. Painted ONCE at the
|
|
127
|
+
* top of the watch, then never touched again.
|
|
128
|
+
*
|
|
129
|
+
* Visual hierarchy (per user feedback):
|
|
130
|
+
* 1. PEAKS-CLI — 3-line big block art, bold cyan
|
|
131
|
+
* 2. separator
|
|
132
|
+
* 3. sub-agent progress watch — 1 line, smaller, gray
|
|
133
|
+
* 4. project / path — 1 line each, gray
|
|
134
|
+
*/
|
|
135
|
+
function renderHeader(projectRoot, progressFilePath, isTty) {
|
|
136
|
+
if (!isTty) {
|
|
137
|
+
// Non-TTY: emit a single header line, no colour. The
|
|
138
|
+
// dashboard still emits 2 dynamic lines per tick, so
|
|
139
|
+
// consumers that need to parse the output can rely on
|
|
140
|
+
// a known line count.
|
|
141
|
+
return [
|
|
142
|
+
`PEAKS-CLI · sub-agent progress watch · project=${projectRoot}`,
|
|
143
|
+
`path: ${progressFilePath}`
|
|
144
|
+
].join('\n') + '\n';
|
|
145
|
+
}
|
|
146
|
+
// PEAKS-CLI block-art rows in bold cyan (the "big" brand).
|
|
147
|
+
const brandLines = PEAKS_CLI_ASCII.map((row) => chalk.bold.cyan(` ${row}`));
|
|
148
|
+
// The tagline below the brand — small, gray, no big
|
|
149
|
+
// chrome. The user sees the brand first, the
|
|
150
|
+
// descriptor second.
|
|
151
|
+
const tagline = chalk.gray(' sub-agent progress watch');
|
|
152
|
+
const projLine = chalk.gray(` project: ${projectRoot}`);
|
|
153
|
+
const pathLine = chalk.gray(` path: ${progressFilePath}`);
|
|
154
|
+
const separator = chalk.gray(' ' + '─'.repeat(60));
|
|
155
|
+
return [
|
|
156
|
+
brandLines[0] ?? '',
|
|
157
|
+
brandLines[1] ?? '',
|
|
158
|
+
brandLines[2] ?? '',
|
|
159
|
+
separator,
|
|
160
|
+
tagline,
|
|
161
|
+
projLine,
|
|
162
|
+
pathLine,
|
|
163
|
+
''
|
|
164
|
+
].join('\n');
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Render the 2-line dynamic dashboard. Status row on top
|
|
168
|
+
* (spinner + phase + elapsed + step), progress bar on the
|
|
169
|
+
* bottom. When `data` is null (no progress file yet), we
|
|
170
|
+
* still show a live spinner + a "waiting…" message so the
|
|
171
|
+
* user knows the watch is alive.
|
|
172
|
+
*/
|
|
173
|
+
function renderDynamicRows(data, tick, width, isTty) {
|
|
174
|
+
const progressFraction = computeFakeProgress(data);
|
|
175
|
+
if (data === null) {
|
|
176
|
+
return {
|
|
177
|
+
status: isTty
|
|
178
|
+
? ` ${chalk.gray(pickSpinnerFrame(tick))} ${chalk.gray('idle')} ${chalk.gray('(no progress file yet — sub-agent has not started)')}`
|
|
179
|
+
: `idle (no progress file yet)`,
|
|
180
|
+
bar: isTty ? renderBar(0, width, isTty) : ''
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
const startedAtMs = new Date(data.current.startedAt).getTime();
|
|
184
|
+
const elapsedMs = Number.isNaN(startedAtMs) ? 0 : Math.max(0, Date.now() - startedAtMs);
|
|
185
|
+
const phase = PHASE_LABEL[data.current.phase];
|
|
186
|
+
const step = data.current.step.length > 60 ? data.current.step.slice(0, 57) + '...' : data.current.step;
|
|
187
|
+
const verdict = data.current.verdict ? ` verdict=${data.current.verdict}` : '';
|
|
188
|
+
const role = data.role ? ` role=${data.role}` : '';
|
|
189
|
+
const spinner = pickSpinnerFrame(tick);
|
|
190
|
+
if (!isTty) {
|
|
191
|
+
return {
|
|
192
|
+
status: `${spinner} ${phase} ${formatElapsed(elapsedMs)} ${step}${role}${verdict}`,
|
|
193
|
+
bar: ''
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
const colorize = PHASE_COLOR[data.current.phase] ?? chalk.cyan;
|
|
197
|
+
const spinnerColor = phase === 'failed' ? chalk.red
|
|
198
|
+
: phase === 'finished' ? chalk.green
|
|
199
|
+
: chalk.cyan;
|
|
200
|
+
const statusLine = ` ${spinnerColor(spinner)} ${colorize(phase.padEnd(11))} ${chalk.yellow(formatElapsed(elapsedMs))} ${step}${chalk.gray(role)}${verdict ? chalk.gray(verdict) : ''}`;
|
|
201
|
+
return {
|
|
202
|
+
status: statusLine,
|
|
203
|
+
bar: renderBar(progressFraction, width, isTty)
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
function renderBar(fraction, width, isTty) {
|
|
207
|
+
if (!isTty)
|
|
208
|
+
return '';
|
|
209
|
+
// 8ths-of-cell block characters: ░ (empty) U+2591, full blocks
|
|
210
|
+
// U+2588..U+258F for the partial last cell.
|
|
211
|
+
const barCells = Math.max(10, Math.min(50, Math.floor(width * 0.4)));
|
|
212
|
+
const filled = Math.round(barCells * Math.max(0, Math.min(1, fraction)) * 8);
|
|
213
|
+
const fullBlocks = Math.floor(filled / 8);
|
|
214
|
+
const fracBlock = filled % 8;
|
|
215
|
+
const emptyCells = barCells * 8 - filled;
|
|
216
|
+
const fullStr = '█'.repeat(fullBlocks);
|
|
217
|
+
const fracStr = fracBlock > 0 ? String.fromCharCode(0x2588 + (8 - fracBlock)) : '';
|
|
218
|
+
const emptyStr = '░'.repeat(Math.floor(emptyCells / 8));
|
|
219
|
+
const percent = String(Math.round(fraction * 100)).padStart(3, ' ');
|
|
220
|
+
return ` ${chalk.green(fullStr + fracStr + emptyStr)} ${chalk.gray(`${percent}%`)}`;
|
|
221
|
+
}
|
|
222
|
+
export class WatchRenderer {
|
|
223
|
+
projectRoot;
|
|
224
|
+
progressFilePath;
|
|
225
|
+
width;
|
|
226
|
+
isTty;
|
|
227
|
+
hasRenderedDynamic = false;
|
|
228
|
+
constructor(options) {
|
|
229
|
+
this.projectRoot = options.projectRoot;
|
|
230
|
+
this.progressFilePath = options.progressFilePath;
|
|
231
|
+
this.width = (process.stdout.columns ?? 120) - 1;
|
|
232
|
+
this.isTty = process.stdout.isTTY === true;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Paint the static header once at the top of the watch
|
|
236
|
+
* (the PEAKS-CLI wordmark, separator, project, path). Then
|
|
237
|
+
* paint the dynamic rows for the first tick. From here on
|
|
238
|
+
* the dynamic rows are the only thing we touch.
|
|
239
|
+
*/
|
|
240
|
+
start() {
|
|
241
|
+
rawWrite(renderHeader(this.projectRoot, this.progressFilePath, this.isTty));
|
|
242
|
+
this.paintDynamicOnce(null, 0);
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Repaint the 2 dynamic rows in place. First call moves
|
|
246
|
+
* the cursor up N rows from the bottom of the previously
|
|
247
|
+
* painted block; subsequent calls do the same.
|
|
248
|
+
*/
|
|
249
|
+
tick(data, tickCount) {
|
|
250
|
+
if (this.hasRenderedDynamic) {
|
|
251
|
+
// Move cursor up to the top of the previously-painted
|
|
252
|
+
// dynamic block.
|
|
253
|
+
rawWrite(CURSOR_UP_N(DYNAMIC_LINES));
|
|
254
|
+
}
|
|
255
|
+
this.paintDynamicOnce(data, tickCount);
|
|
256
|
+
}
|
|
257
|
+
paintDynamicOnce(data, tick) {
|
|
258
|
+
const { status, bar } = renderDynamicRows(data, tick, this.width, this.isTty);
|
|
259
|
+
// Erase-then-rewrite the status row.
|
|
260
|
+
rawWrite(ERASE_LINE + status + '\n');
|
|
261
|
+
// Erase-then-rewrite the bar row. (In non-TTY mode the
|
|
262
|
+
// bar is empty; we still emit a newline so the line
|
|
263
|
+
// count stays consistent.)
|
|
264
|
+
rawWrite(ERASE_LINE + bar + '\n');
|
|
265
|
+
this.hasRenderedDynamic = true;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Paint a final 2-line verdict + a farewell line below the
|
|
269
|
+
* dashboard, then return. The cursor stays at the bottom
|
|
270
|
+
* of the farewell so the user's shell prompt lands on the
|
|
271
|
+
* next row.
|
|
272
|
+
*/
|
|
273
|
+
finalize(data) {
|
|
274
|
+
// Rewrite the dynamic block one last time so the user can
|
|
275
|
+
// read the verdict.
|
|
276
|
+
if (this.hasRenderedDynamic) {
|
|
277
|
+
rawWrite(CURSOR_UP_N(DYNAMIC_LINES));
|
|
278
|
+
}
|
|
279
|
+
this.paintDynamicOnce(data, Number.MAX_SAFE_INTEGER);
|
|
280
|
+
// Then emit the farewell BELOW the dashboard, in green.
|
|
281
|
+
const verdictSuffix = data.current.verdict !== undefined ? ` (verdict=${data.current.verdict})` : '';
|
|
282
|
+
const farewell = this.isTty
|
|
283
|
+
? chalk.green(`✔ peaks progress watch: sub-agent reached phase=${data.current.phase}${verdictSuffix} at ${new Date().toISOString()}. Auto-closing watch window.`)
|
|
284
|
+
: `peaks progress watch: sub-agent reached phase=${data.current.phase}${verdictSuffix} at ${new Date().toISOString()}. Auto-closing watch window.`;
|
|
285
|
+
rawWrite(ERASE_LINE + farewell + '\n');
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Force a non-ANSI snapshot of the current state, used by
|
|
289
|
+
* the `--once` mode and for fallback when stdout is not a
|
|
290
|
+
* TTY. Does NOT touch the cursor state — safe to call from
|
|
291
|
+
* any context.
|
|
292
|
+
*/
|
|
293
|
+
static snapshot(data) {
|
|
294
|
+
return renderDynamicRows(data, 0, 80, false);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Strip ANSI escapes from a string. Used for visible-length
|
|
299
|
+
* accounting; not for re-painting.
|
|
300
|
+
*/
|
|
301
|
+
export function stripAnsi(input) {
|
|
302
|
+
// eslint-disable-next-line no-control-regex
|
|
303
|
+
return input.replace(/\x1b\[[0-9;]*m/g, '');
|
|
304
|
+
}
|
|
305
|
+
/** Reset terminal SGR — used on early-return error paths. */
|
|
306
|
+
export function resetTerminal() {
|
|
307
|
+
rawWrite(RESET);
|
|
308
|
+
}
|
|
@@ -123,7 +123,7 @@ export function registerProjectCommands(program, io) {
|
|
|
123
123
|
.command('memories')
|
|
124
124
|
.description('Read durable project memories (decisions, conventions, modules, rules) from .peaks/memory for LLM consumption')
|
|
125
125
|
.requiredOption('--project <path>', 'target project root')
|
|
126
|
-
.option('--kind <kind>', 'filter by kind: project, rule, decision, reference, feedback, convention, module')).action((options) => {
|
|
126
|
+
.option('--kind <kind>', 'filter by kind: project, rule, decision, reference, feedback, convention, module, lesson')).action((options) => {
|
|
127
127
|
try {
|
|
128
128
|
const result = readProjectMemories(options.project);
|
|
129
129
|
if (options.kind) {
|
|
@@ -5,6 +5,7 @@ import { checkTypeSanity } from '../../services/scan/type-sanity-service.js';
|
|
|
5
5
|
import { getAcceptanceCoverage, isAcceptanceCoverageError } from '../../services/scan/acceptance-coverage-service.js';
|
|
6
6
|
import { getDiffVsScope, isDiffScopeError } from '../../services/scan/diff-scope-service.js';
|
|
7
7
|
import { scanFileSize, DEFAULT_FILE_SIZE_THRESHOLD } from '../../services/scan/file-size-scan.js';
|
|
8
|
+
import { scanLibraries } from '../../services/scan/libraries-service.js';
|
|
8
9
|
import { isRequestType, VALID_REQUEST_TYPES } from '../../services/artifacts/artifact-prerequisites.js';
|
|
9
10
|
import { fail, ok } from '../../shared/result.js';
|
|
10
11
|
import { addJsonOption, getErrorMessage, printResult } from '../cli-helpers.js';
|
|
@@ -221,4 +222,25 @@ export function registerScanCommands(program, io) {
|
|
|
221
222
|
process.exitCode = 1;
|
|
222
223
|
}
|
|
223
224
|
});
|
|
225
|
+
addJsonOption(scan
|
|
226
|
+
.command('libraries')
|
|
227
|
+
.description('Enumerate every dependency + devDependency + peerDependency + optionalDependency in package.json with parsed major version (read-only). Output goes to ## Library versions in rd/project-scan.md.')
|
|
228
|
+
.requiredOption('--project <path>', 'target project root')).action(async (options) => {
|
|
229
|
+
try {
|
|
230
|
+
const report = await scanLibraries({ projectRoot: options.project });
|
|
231
|
+
const nextActions = [];
|
|
232
|
+
if (report.libraries.length === 0) {
|
|
233
|
+
nextActions.push('No dependencies found — verify package.json exists and is valid JSON.');
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
nextActions.push('Paste the report under `## Library versions` in .peaks/<sid>/rd/project-scan.md.');
|
|
237
|
+
nextActions.push('peaks-rd preflight will cross-check diff imports against schemas/library-breaking-changes.data.json.');
|
|
238
|
+
}
|
|
239
|
+
printResult(io, ok('scan.libraries', report, [], nextActions), options.json);
|
|
240
|
+
}
|
|
241
|
+
catch (error) {
|
|
242
|
+
printResult(io, fail('scan.libraries', 'SCAN_LIBRARIES_FAILED', getErrorMessage(error), { projectRoot: options.project }, ['Verify the project path exists and is readable']), options.json);
|
|
243
|
+
process.exitCode = 1;
|
|
244
|
+
}
|
|
245
|
+
});
|
|
224
246
|
}
|
package/dist/src/cli/program.js
CHANGED
|
@@ -9,6 +9,8 @@ import { registerCapabilityWorkerConfigAndSCCommands } from './commands/capabili
|
|
|
9
9
|
import { registerCodegraphCommands } from './commands/codegraph-commands.js';
|
|
10
10
|
import { registerMcpCommands } from './commands/mcp-commands.js';
|
|
11
11
|
import { registerOpenSpecCommands } from './commands/openspec-commands.js';
|
|
12
|
+
import { registerPerfCommands } from './commands/perf-commands.js';
|
|
13
|
+
import { registerProgressCommands } from './commands/progress-commands.js';
|
|
12
14
|
import { registerProjectCommands } from './commands/project-commands.js';
|
|
13
15
|
import { registerRequestCommands } from './commands/request-commands.js';
|
|
14
16
|
import { registerScanCommands } from './commands/scan-commands.js';
|
|
@@ -79,6 +81,8 @@ Run peaks (no arguments) for a quickstart. You likely want one of:
|
|
|
79
81
|
registerCodegraphCommands(program, io);
|
|
80
82
|
registerMcpCommands(program, io);
|
|
81
83
|
registerOpenSpecCommands(program, io);
|
|
84
|
+
registerPerfCommands(program, io);
|
|
85
|
+
registerProgressCommands(program, io);
|
|
82
86
|
registerProjectCommands(program, io);
|
|
83
87
|
registerRequestCommands(program, io);
|
|
84
88
|
registerScanCommands(program, io);
|
|
@@ -58,6 +58,26 @@ export type PeaksConfig = {
|
|
|
58
58
|
tokens: TokenConfig;
|
|
59
59
|
providers: ModelProviderConfig;
|
|
60
60
|
proxy: ProxyConfig;
|
|
61
|
+
/**
|
|
62
|
+
* Sub-agent progress surfacing knobs. The `peaks progress watch`
|
|
63
|
+
* CLI (intended to be run in a separate terminal tab while the
|
|
64
|
+
* LLM is working) reads `.peaks/<sid>/system/subagent-progress.json`
|
|
65
|
+
* and renders elapsed / spinner / sub-step in real time. The
|
|
66
|
+
* `enabled` flag is a kill-switch for users who find the watch
|
|
67
|
+
* distracting; the `heartbeatIntervalMs` lets power users tune
|
|
68
|
+
* the write cadence. Both default to sensible values so stock
|
|
69
|
+
* projects get the feature out of the box.
|
|
70
|
+
*
|
|
71
|
+
* Optional on the type level so older test fixtures / hand-
|
|
72
|
+
* written config files do not have to know about it; the
|
|
73
|
+
* `DEFAULT_CONFIG.progress` block supplies the runtime defaults
|
|
74
|
+
* and `config get` will surface a synthesised block when the
|
|
75
|
+
* field is absent.
|
|
76
|
+
*/
|
|
77
|
+
progress?: {
|
|
78
|
+
enabled: boolean;
|
|
79
|
+
heartbeatIntervalMs: number;
|
|
80
|
+
};
|
|
61
81
|
};
|
|
62
82
|
export type ConfigLayer = 'user' | 'project';
|
|
63
83
|
export type ConfigGetOptions = {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type ProjectMemoryKind = 'project' | 'rule' | 'decision' | 'reference' | 'feedback' | 'convention' | 'module';
|
|
1
|
+
export type ProjectMemoryKind = 'project' | 'rule' | 'decision' | 'reference' | 'feedback' | 'convention' | 'module' | 'lesson';
|
|
2
2
|
export type ExtractedProjectMemory = {
|
|
3
3
|
title: string;
|
|
4
4
|
kind: ProjectMemoryKind;
|
|
@@ -3,13 +3,20 @@ import { dirname, basename, isAbsolute, join, relative, resolve } from 'node:pat
|
|
|
3
3
|
import { isInsidePath, isWindowsAbsolutePath, normalizePath, resolveInputPath, stablePath, stableRealPath } from '../../shared/path-utils.js';
|
|
4
4
|
import { containsSensitiveConfigValue, isSensitiveConfigPath } from '../config/config-service.js';
|
|
5
5
|
// Hot kinds: full body kept in index for always-available context
|
|
6
|
-
const HOT_KINDS = new Set(['feedback', 'decision', 'rule', 'convention', 'module']);
|
|
6
|
+
const HOT_KINDS = new Set(['feedback', 'decision', 'rule', 'convention', 'module', 'lesson']);
|
|
7
7
|
// ---------------------------------------------------------------------------
|
|
8
8
|
// Internal helpers (kept from original, sorted by dependency order)
|
|
9
9
|
// ---------------------------------------------------------------------------
|
|
10
10
|
const START_MARKER = '<!-- peaks-memory:start -->';
|
|
11
11
|
const END_MARKER = '<!-- peaks-memory:end -->';
|
|
12
|
-
const VALID_MEMORY_KINDS = new Set(['project', 'rule', 'decision', 'reference', 'feedback', 'convention', 'module']);
|
|
12
|
+
const VALID_MEMORY_KINDS = new Set(['project', 'rule', 'decision', 'reference', 'feedback', 'convention', 'module', 'lesson']);
|
|
13
|
+
// Length bounds for index entry descriptions. The numbers were chosen when
|
|
14
|
+
// summarizeMemoryBody was first introduced; locking them in as named
|
|
15
|
+
// constants is a doc-as-code move so the truncation rule is no longer
|
|
16
|
+
// "magic". Bump MAX_DESCRIPTION_LENGTH deliberately if downstream UIs grow.
|
|
17
|
+
const MIN_BODY_SENTENCE_LENGTH = 20; // skip fragments shorter than this when picking a leading sentence
|
|
18
|
+
const MAX_DESCRIPTION_LENGTH = 120; // hard cap on description length in the memory index entry
|
|
19
|
+
const ELLIPSIS_RESERVE = 3; // length of the trailing "..." when truncating with an ellipsis
|
|
13
20
|
function normalizeRoot(path) {
|
|
14
21
|
return resolveInputPath(path);
|
|
15
22
|
}
|
|
@@ -214,15 +221,15 @@ function summarizeMemoryBody(body) {
|
|
|
214
221
|
.replace(/^\s*[-*+]\s+/gm, '')
|
|
215
222
|
.replace(/\n+/g, ' ')
|
|
216
223
|
.trim();
|
|
217
|
-
const sentences = cleaned.split(/(?<=[.!?])\s+/).filter((s) => s.length >
|
|
224
|
+
const sentences = cleaned.split(/(?<=[.!?])\s+/).filter((s) => s.length > MIN_BODY_SENTENCE_LENGTH && !/^\[.+\]$/.test(s));
|
|
218
225
|
if (sentences.length === 0) {
|
|
219
|
-
return cleaned.slice(0,
|
|
226
|
+
return cleaned.slice(0, MAX_DESCRIPTION_LENGTH) || 'Project memory';
|
|
220
227
|
}
|
|
221
228
|
const first = sentences[0];
|
|
222
|
-
if (first.length <=
|
|
229
|
+
if (first.length <= MAX_DESCRIPTION_LENGTH) {
|
|
223
230
|
return first;
|
|
224
231
|
}
|
|
225
|
-
return first.slice(0,
|
|
232
|
+
return first.slice(0, MAX_DESCRIPTION_LENGTH - ELLIPSIS_RESERVE) + '...';
|
|
226
233
|
}
|
|
227
234
|
// ---------------------------------------------------------------------------
|
|
228
235
|
// Session memory extraction (new extract path)
|
|
@@ -296,7 +303,7 @@ function readStoredMemoryNames(memoryDir) {
|
|
|
296
303
|
function generateMemoryIndexFile(projectRoot, memoryDir, indexPath) {
|
|
297
304
|
const memories = readProjectMemories(projectRoot);
|
|
298
305
|
const hot = {
|
|
299
|
-
feedback: [], decision: [], rule: [], convention: [], module: []
|
|
306
|
+
feedback: [], decision: [], rule: [], convention: [], module: [], lesson: []
|
|
300
307
|
};
|
|
301
308
|
const warm = {
|
|
302
309
|
project: [], reference: []
|
|
@@ -350,29 +357,49 @@ function readExistingIndex(indexPath) {
|
|
|
350
357
|
return null;
|
|
351
358
|
}
|
|
352
359
|
}
|
|
360
|
+
// Decide whether readMemoryIndex should rebuild the on-disk index.json.
|
|
361
|
+
// The rule is: rebuild iff index.json is missing OR any memory.md has an
|
|
362
|
+
// mtime strictly greater than index.json's mtime. Any statSync failure
|
|
363
|
+
// falls back to "rebuild" — a safe default that matches the prior
|
|
364
|
+
// always-rebuild behaviour and avoids serving a stale index from a
|
|
365
|
+
// partially-corrupt dir.
|
|
366
|
+
function shouldRegenerateIndex(indexPath, memoryFiles) {
|
|
367
|
+
let indexMtimeMs = 0;
|
|
368
|
+
try {
|
|
369
|
+
indexMtimeMs = statSync(indexPath).mtimeMs;
|
|
370
|
+
}
|
|
371
|
+
catch {
|
|
372
|
+
return true; // no index → must regenerate
|
|
373
|
+
}
|
|
374
|
+
for (const memoryPath of memoryFiles) {
|
|
375
|
+
try {
|
|
376
|
+
const memoryMtimeMs = statSync(memoryPath).mtimeMs;
|
|
377
|
+
if (memoryMtimeMs > indexMtimeMs)
|
|
378
|
+
return true;
|
|
379
|
+
}
|
|
380
|
+
catch {
|
|
381
|
+
return true; // unreadable file → safe default is regenerate
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
353
386
|
export function readMemoryIndex(projectRoot) {
|
|
354
387
|
const normalizedRoot = normalizeRoot(projectRoot);
|
|
355
388
|
const memoryDir = assertSafeProjectMemoryDir(normalizedRoot);
|
|
356
389
|
const indexPath = join(memoryDir, 'index.json');
|
|
357
|
-
// Read-side bootstrap: if the memory dir is missing entirely, build
|
|
358
|
-
//
|
|
359
|
-
//
|
|
360
|
-
//
|
|
361
|
-
//
|
|
390
|
+
// Read-side bootstrap: if the memory dir is missing entirely, build it and
|
|
391
|
+
// return whatever index is on disk (likely null on a fresh project). We
|
|
392
|
+
// deliberately do NOT pre-write an empty index here: the mtime-based
|
|
393
|
+
// regeneration guard below is the sole authority on whether index.json
|
|
394
|
+
// gets materialised, and pre-writing an empty index would race the guard
|
|
395
|
+
// (giving it a current-time mtime that defeats "memory older than index"
|
|
396
|
+
// detection on the first read).
|
|
362
397
|
if (!existsSync(memoryDir)) {
|
|
363
398
|
ensureMemoryBootstrap(normalizedRoot);
|
|
364
399
|
return readExistingIndex(indexPath);
|
|
365
400
|
}
|
|
366
|
-
if (!existsSync(indexPath)) {
|
|
367
|
-
try {
|
|
368
|
-
writeFileSync(indexPath, renderEmptyIndex(), { mode: 0o644 });
|
|
369
|
-
}
|
|
370
|
-
catch {
|
|
371
|
-
// fall through — readExistingIndex will return null
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
401
|
const files = listMarkdownFiles(memoryDir);
|
|
375
|
-
if (files.length > 0) {
|
|
402
|
+
if (files.length > 0 && shouldRegenerateIndex(indexPath, files)) {
|
|
376
403
|
try {
|
|
377
404
|
generateMemoryIndexFile(normalizedRoot, memoryDir, indexPath);
|
|
378
405
|
}
|
|
@@ -659,7 +686,8 @@ function emptyByKind() {
|
|
|
659
686
|
reference: [],
|
|
660
687
|
feedback: [],
|
|
661
688
|
convention: [],
|
|
662
|
-
module: []
|
|
689
|
+
module: [],
|
|
690
|
+
lesson: []
|
|
663
691
|
};
|
|
664
692
|
}
|
|
665
693
|
function emptyIndex() {
|
|
@@ -676,7 +704,8 @@ function emptyIndex() {
|
|
|
676
704
|
decision: [],
|
|
677
705
|
rule: [],
|
|
678
706
|
convention: [],
|
|
679
|
-
module: []
|
|
707
|
+
module: [],
|
|
708
|
+
lesson: []
|
|
680
709
|
},
|
|
681
710
|
warm: {
|
|
682
711
|
project: [],
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Performance baseline scaffolding for the RD stage.
|
|
3
|
+
*
|
|
4
|
+
* peaks-solo's RD stage runs before the QA stage. The user-facing pain is
|
|
5
|
+
* that performance tests (lighthouse / k6 / project-local benches / etc.)
|
|
6
|
+
* have historically only been run at QA Gate A4 — too late in the loop,
|
|
7
|
+
* because a slow regression discovered at QA triggers a return-to-rd
|
|
8
|
+
* cycle, the RD ships another "fix", QA re-runs, and the same cycle
|
|
9
|
+
* repeats up to 3 times before the slice ships.
|
|
10
|
+
*
|
|
11
|
+
* `peaks perf baseline` is the user-visible artifact of a deliberate
|
|
12
|
+
* compromise: keep the heavy performance measurement as something the
|
|
13
|
+
* RD runs themselves (lighthouse is project-shape dependent and we
|
|
14
|
+
* don't want to bake a lighthouse dependency into the CLI), but capture
|
|
15
|
+
* the result in a stable, scaffolded file under
|
|
16
|
+
* `.peaks/<sid>/rd/perf-baseline.md` so QA Gate A4 has a known-good
|
|
17
|
+
* reference to diff against. The CLI itself only writes the scaffold
|
|
18
|
+
* and records the path; the actual measurement is a project-local
|
|
19
|
+
* concern that lives in the README, not in peaks-cli.
|
|
20
|
+
*
|
|
21
|
+
* The four-grounds check (per the skill-primary-CLI-auxiliary dev
|
|
22
|
+
* preference):
|
|
23
|
+
* 1. hook/script/CI invokability — yes, a hook can call this CLI
|
|
24
|
+
* to scaffold the file on session
|
|
25
|
+
* init, similar to session bootstrap.
|
|
26
|
+
* 2. JSON envelope that gates a downstream decision — yes,
|
|
27
|
+
* peaks-rd reads the result and
|
|
28
|
+
* attaches it to the handoff.
|
|
29
|
+
* 3. Destructive --apply side effect — yes, default dry-run.
|
|
30
|
+
* 4. Machine-enforced gate that prose cannot enforce — no, the
|
|
31
|
+
* measurement still lives in
|
|
32
|
+
* the LLM / project tools. We do
|
|
33
|
+
* NOT add a lint gate here.
|
|
34
|
+
*
|
|
35
|
+
* Net: CLI is justified. The destructive --apply default is dry-run,
|
|
36
|
+
* matching the rest of peaks-cli's scaffolding pattern.
|
|
37
|
+
*/
|
|
38
|
+
export type PerfBaselineInitOptions = {
|
|
39
|
+
projectRoot: string;
|
|
40
|
+
apply?: boolean;
|
|
41
|
+
reason?: string;
|
|
42
|
+
};
|
|
43
|
+
export type PerfBaselinePlan = {
|
|
44
|
+
apply: boolean;
|
|
45
|
+
projectRoot: string;
|
|
46
|
+
sessionId: string | null;
|
|
47
|
+
perfBaselinePath: string | null;
|
|
48
|
+
plannedWrites: Array<{
|
|
49
|
+
path: string;
|
|
50
|
+
kind: 'directory' | 'file';
|
|
51
|
+
bytes: number;
|
|
52
|
+
content: string;
|
|
53
|
+
}>;
|
|
54
|
+
alreadyInitialized: boolean;
|
|
55
|
+
existingFiles: string[];
|
|
56
|
+
};
|
|
57
|
+
export type PerfBaselineResult = PerfBaselinePlan & {
|
|
58
|
+
writtenFiles: string[];
|
|
59
|
+
createdDirectories: string[];
|
|
60
|
+
reason?: string;
|
|
61
|
+
};
|
|
62
|
+
export declare function executePerfBaselineInit(options: PerfBaselineInitOptions): Promise<PerfBaselineResult>;
|
|
63
|
+
/**
|
|
64
|
+
* Re-exported so the CLI command can fall back to a project-root
|
|
65
|
+
* resolution when the caller did not pass --project. The CLI does
|
|
66
|
+
* the same findProjectRoot walk that `workspace init` does; this
|
|
67
|
+
* helper exists for the command layer to import without reaching
|
|
68
|
+
* into config-safety directly.
|
|
69
|
+
*/
|
|
70
|
+
export declare function resolveProjectRootFromCwd(cwd: string): string;
|