peaks-cli 1.3.0 → 1.3.2
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 +62 -46
- package/bin/peaks.js +0 -0
- package/dist/src/cli/commands/core-artifact-commands.js +49 -11
- package/dist/src/cli/commands/hooks-commands.js +24 -9
- package/dist/src/cli/commands/progress-commands.js +26 -2
- package/dist/src/cli/commands/request-commands.js +5 -0
- package/dist/src/cli/commands/slice-commands.d.ts +3 -0
- package/dist/src/cli/commands/slice-commands.js +44 -0
- package/dist/src/cli/commands/workflow-commands.js +3 -3
- package/dist/src/cli/commands/workspace-commands.d.ts +63 -0
- package/dist/src/cli/commands/workspace-commands.js +349 -12
- package/dist/src/cli/program.js +4 -0
- package/dist/src/services/artifacts/artifact-prerequisites.d.ts +29 -1
- package/dist/src/services/artifacts/artifact-prerequisites.js +69 -5
- package/dist/src/services/artifacts/request-artifact-service.d.ts +22 -0
- package/dist/src/services/artifacts/request-artifact-service.js +214 -56
- package/dist/src/services/doctor/doctor-service.d.ts +69 -0
- package/dist/src/services/doctor/doctor-service.js +296 -3
- package/dist/src/services/progress/progress-service.d.ts +26 -0
- package/dist/src/services/progress/progress-service.js +25 -0
- package/dist/src/services/sc/sc-service.js +71 -13
- package/dist/src/services/scan/acceptance-coverage-service.js +6 -2
- package/dist/src/services/session/session-manager.d.ts +22 -1
- package/dist/src/services/session/session-manager.js +149 -30
- package/dist/src/services/skills/hooks-settings-service.d.ts +25 -3
- package/dist/src/services/skills/hooks-settings-service.js +57 -13
- package/dist/src/services/slice/slice-check-service.d.ts +2 -0
- package/dist/src/services/slice/slice-check-service.js +267 -0
- package/dist/src/services/slice/slice-check-types.d.ts +70 -0
- package/dist/src/services/slice/slice-check-types.js +18 -0
- package/dist/src/services/workflow/pipeline-verify-service.d.ts +5 -2
- package/dist/src/services/workflow/pipeline-verify-service.js +35 -35
- package/dist/src/services/workspace/migrate-service.d.ts +2 -0
- package/dist/src/services/workspace/migrate-service.js +606 -0
- package/dist/src/services/workspace/migrate-types.d.ts +127 -0
- package/dist/src/services/workspace/migrate-types.js +21 -0
- package/dist/src/services/workspace/reconcile-service.d.ts +33 -0
- package/dist/src/services/workspace/reconcile-service.js +160 -42
- package/dist/src/services/workspace/reconcile-types.d.ts +25 -0
- package/dist/src/services/workspace/workspace-service.d.ts +11 -0
- package/dist/src/services/workspace/workspace-service.js +71 -24
- package/dist/src/shared/change-id.d.ts +59 -0
- package/dist/src/shared/change-id.js +194 -16
- package/dist/src/shared/version.d.ts +1 -1
- package/dist/src/shared/version.js +1 -1
- package/package.json +10 -2
- package/schemas/doctor-report.schema.json +2 -2
- package/skills/peaks-qa/SKILL.md +1 -0
- package/skills/peaks-rd/SKILL.md +2 -1
- package/skills/peaks-solo/SKILL.md +17 -1
- package/skills/peaks-solo/references/micro-cycle.md +155 -0
- package/skills/peaks-txt/SKILL.md +2 -0
- package/skills/peaks-ui/SKILL.md +1 -0
|
@@ -3,7 +3,8 @@ import { existsSync } from 'node:fs';
|
|
|
3
3
|
import { dirname, join } from 'node:path';
|
|
4
4
|
import { isDirectory, listDirectories } from '../../shared/fs.js';
|
|
5
5
|
import { checkPrerequisites, DEFAULT_REQUEST_TYPE, isRequestType, VALID_REQUEST_TYPES } from './artifact-prerequisites.js';
|
|
6
|
-
import { ensureSession } from '../session/session-manager.js';
|
|
6
|
+
import { ensureSession, getSessionIdCanonical } from '../session/session-manager.js';
|
|
7
|
+
import { getCurrentChangeId } from '../../shared/change-id.js';
|
|
7
8
|
import { getNextNumber, buildNumberedFilename } from '../../shared/incrementing-number.js';
|
|
8
9
|
import { lintRequestArtifact } from './artifact-lint-service.js';
|
|
9
10
|
import { checkTypeSanity } from '../scan/type-sanity-service.js';
|
|
@@ -21,10 +22,11 @@ function dateSlugFromIso(iso) {
|
|
|
21
22
|
function defaultSessionId(iso) {
|
|
22
23
|
return `${dateSlugFromIso(iso)}-session`;
|
|
23
24
|
}
|
|
24
|
-
function renderPrdTemplate(requestId, sessionId, timestamp, requestType) {
|
|
25
|
+
function renderPrdTemplate(requestId, changeId, sessionId, timestamp, requestType) {
|
|
25
26
|
return `# PRD Request ${requestId}
|
|
26
27
|
|
|
27
28
|
- session: ${sessionId}
|
|
29
|
+
- change-id: ${changeId}
|
|
28
30
|
- type: ${requestType}
|
|
29
31
|
- source: <ticket, message URL, or "verbal" with a short sanitized quote>
|
|
30
32
|
- raw input (sanitized): <one-paragraph restatement of what the user actually asked for>
|
|
@@ -57,9 +59,9 @@ function renderPrdTemplate(requestId, sessionId, timestamp, requestType) {
|
|
|
57
59
|
|
|
58
60
|
## Handoff
|
|
59
61
|
|
|
60
|
-
- to peaks-rd: .peaks/${
|
|
61
|
-
- to peaks-qa: .peaks/${
|
|
62
|
-
- to peaks-ui: .peaks/${
|
|
62
|
+
- to peaks-rd: .peaks/${changeId}/rd/requests/${requestId}.md
|
|
63
|
+
- to peaks-qa: .peaks/${changeId}/qa/requests/${requestId}.md
|
|
64
|
+
- to peaks-ui: .peaks/${changeId}/ui/requests/${requestId}.md (when UI involved)
|
|
63
65
|
|
|
64
66
|
## Status
|
|
65
67
|
|
|
@@ -68,11 +70,12 @@ function renderPrdTemplate(requestId, sessionId, timestamp, requestType) {
|
|
|
68
70
|
- state: draft
|
|
69
71
|
`;
|
|
70
72
|
}
|
|
71
|
-
function renderUiTemplate(requestId, sessionId, timestamp, requestType) {
|
|
73
|
+
function renderUiTemplate(requestId, changeId, sessionId, timestamp, requestType) {
|
|
72
74
|
return `# UI Request ${requestId}
|
|
73
75
|
|
|
74
76
|
- session: ${sessionId}
|
|
75
|
-
-
|
|
77
|
+
- change-id: ${changeId}
|
|
78
|
+
- linked-prd: .peaks/${changeId}/prd/requests/${requestId}.md
|
|
76
79
|
- type: ${requestType}
|
|
77
80
|
- scope: full new surface | iteration on existing surface | regression fix | visual refresh
|
|
78
81
|
- design direction: editorial | bento | Swiss | luxury | retro-futurist | glass | product-system | other-explicit-name
|
|
@@ -108,8 +111,8 @@ function renderUiTemplate(requestId, sessionId, timestamp, requestType) {
|
|
|
108
111
|
|
|
109
112
|
## Handoff
|
|
110
113
|
|
|
111
|
-
- to peaks-rd: .peaks/${
|
|
112
|
-
- to peaks-qa: .peaks/${
|
|
114
|
+
- to peaks-rd: .peaks/${changeId}/rd/requests/${requestId}.md
|
|
115
|
+
- to peaks-qa: .peaks/${changeId}/qa/requests/${requestId}.md
|
|
113
116
|
|
|
114
117
|
## Status
|
|
115
118
|
|
|
@@ -118,12 +121,13 @@ function renderUiTemplate(requestId, sessionId, timestamp, requestType) {
|
|
|
118
121
|
- state: draft
|
|
119
122
|
`;
|
|
120
123
|
}
|
|
121
|
-
function renderRdTemplate(requestId, sessionId, timestamp, requestType) {
|
|
124
|
+
function renderRdTemplate(requestId, changeId, sessionId, timestamp, requestType) {
|
|
122
125
|
return `# RD Request ${requestId}
|
|
123
126
|
|
|
124
127
|
- session: ${sessionId}
|
|
125
|
-
-
|
|
126
|
-
- linked-
|
|
128
|
+
- change-id: ${changeId}
|
|
129
|
+
- linked-prd: .peaks/${changeId}/prd/requests/${requestId}.md
|
|
130
|
+
- linked-ui: .peaks/${changeId}/ui/requests/${requestId}.md (when UI involved)
|
|
127
131
|
- type: ${requestType}
|
|
128
132
|
|
|
129
133
|
## Red-line scope
|
|
@@ -165,8 +169,8 @@ function renderRdTemplate(requestId, sessionId, timestamp, requestType) {
|
|
|
165
169
|
|
|
166
170
|
## Handoff
|
|
167
171
|
|
|
168
|
-
- to peaks-qa: .peaks/${
|
|
169
|
-
- to peaks-sc: .peaks/${
|
|
172
|
+
- to peaks-qa: .peaks/${changeId}/qa/requests/${requestId}.md
|
|
173
|
+
- to peaks-sc: .peaks/${changeId}/sc/commit-boundaries/${requestId}.md
|
|
170
174
|
|
|
171
175
|
## Status
|
|
172
176
|
|
|
@@ -175,13 +179,14 @@ function renderRdTemplate(requestId, sessionId, timestamp, requestType) {
|
|
|
175
179
|
- state: draft
|
|
176
180
|
`;
|
|
177
181
|
}
|
|
178
|
-
function renderQaTemplate(requestId, sessionId, timestamp, requestType) {
|
|
182
|
+
function renderQaTemplate(requestId, changeId, sessionId, timestamp, requestType) {
|
|
179
183
|
return `# QA Request ${requestId}
|
|
180
184
|
|
|
181
185
|
- session: ${sessionId}
|
|
182
|
-
-
|
|
183
|
-
- linked-
|
|
184
|
-
- linked-
|
|
186
|
+
- change-id: ${changeId}
|
|
187
|
+
- linked-prd: .peaks/${changeId}/prd/requests/${requestId}.md
|
|
188
|
+
- linked-rd: .peaks/${changeId}/rd/requests/${requestId}.md
|
|
189
|
+
- linked-ui: .peaks/${changeId}/ui/requests/${requestId}.md (when UI involved)
|
|
185
190
|
- type: ${requestType}
|
|
186
191
|
|
|
187
192
|
## Red-line boundary check
|
|
@@ -230,14 +235,15 @@ function renderQaTemplate(requestId, sessionId, timestamp, requestType) {
|
|
|
230
235
|
- state: draft
|
|
231
236
|
`;
|
|
232
237
|
}
|
|
233
|
-
function renderScTemplate(requestId, sessionId, timestamp, requestType) {
|
|
238
|
+
function renderScTemplate(requestId, changeId, sessionId, timestamp, requestType) {
|
|
234
239
|
return `# SC Request ${requestId}
|
|
235
240
|
|
|
236
241
|
- session: ${sessionId}
|
|
237
|
-
-
|
|
238
|
-
- linked-
|
|
239
|
-
- linked-
|
|
240
|
-
- linked-
|
|
242
|
+
- change-id: ${changeId}
|
|
243
|
+
- linked-prd: .peaks/${changeId}/prd/requests/${requestId}.md
|
|
244
|
+
- linked-rd: .peaks/${changeId}/rd/requests/${requestId}.md
|
|
245
|
+
- linked-qa: .peaks/${changeId}/qa/requests/${requestId}.md
|
|
246
|
+
- linked-ui: .peaks/${changeId}/ui/requests/${requestId}.md (when UI involved)
|
|
241
247
|
- type: ${requestType}
|
|
242
248
|
|
|
243
249
|
## Change impact
|
|
@@ -262,7 +268,7 @@ function renderScTemplate(requestId, sessionId, timestamp, requestType) {
|
|
|
262
268
|
|
|
263
269
|
## Sync / authorization
|
|
264
270
|
|
|
265
|
-
- artifact workspace path: .peaks/${
|
|
271
|
+
- artifact workspace path: .peaks/${changeId}/
|
|
266
272
|
- memory sync authorized: yes | no
|
|
267
273
|
- artifact sync authorized: yes | no
|
|
268
274
|
- rationale if not authorized: keep local
|
|
@@ -273,7 +279,7 @@ function renderScTemplate(requestId, sessionId, timestamp, requestType) {
|
|
|
273
279
|
|
|
274
280
|
## Handoff
|
|
275
281
|
|
|
276
|
-
- to peaks-txt: .peaks/${
|
|
282
|
+
- to peaks-txt: .peaks/${changeId}/txt/skill-usage-lessons.md (when reusable lesson exists)
|
|
277
283
|
|
|
278
284
|
## Status
|
|
279
285
|
|
|
@@ -282,18 +288,18 @@ function renderScTemplate(requestId, sessionId, timestamp, requestType) {
|
|
|
282
288
|
- state: draft
|
|
283
289
|
`;
|
|
284
290
|
}
|
|
285
|
-
function renderTemplate(role, requestId, sessionId, timestamp, requestType) {
|
|
291
|
+
function renderTemplate(role, requestId, changeId, sessionId, timestamp, requestType) {
|
|
286
292
|
switch (role) {
|
|
287
293
|
case 'prd':
|
|
288
|
-
return renderPrdTemplate(requestId, sessionId, timestamp, requestType);
|
|
294
|
+
return renderPrdTemplate(requestId, changeId, sessionId, timestamp, requestType);
|
|
289
295
|
case 'ui':
|
|
290
|
-
return renderUiTemplate(requestId, sessionId, timestamp, requestType);
|
|
296
|
+
return renderUiTemplate(requestId, changeId, sessionId, timestamp, requestType);
|
|
291
297
|
case 'rd':
|
|
292
|
-
return renderRdTemplate(requestId, sessionId, timestamp, requestType);
|
|
298
|
+
return renderRdTemplate(requestId, changeId, sessionId, timestamp, requestType);
|
|
293
299
|
case 'qa':
|
|
294
|
-
return renderQaTemplate(requestId, sessionId, timestamp, requestType);
|
|
300
|
+
return renderQaTemplate(requestId, changeId, sessionId, timestamp, requestType);
|
|
295
301
|
case 'sc':
|
|
296
|
-
return renderScTemplate(requestId, sessionId, timestamp, requestType);
|
|
302
|
+
return renderScTemplate(requestId, changeId, sessionId, timestamp, requestType);
|
|
297
303
|
}
|
|
298
304
|
}
|
|
299
305
|
export async function createRequestArtifact(options) {
|
|
@@ -306,10 +312,49 @@ export async function createRequestArtifact(options) {
|
|
|
306
312
|
const requestType = options.requestType ?? DEFAULT_REQUEST_TYPE;
|
|
307
313
|
const clock = options.clock ?? defaultClock;
|
|
308
314
|
const timestamp = clock();
|
|
309
|
-
// Use provided session ID or get/create current session
|
|
315
|
+
// Use provided session ID or get/create current session. The session
|
|
316
|
+
// id is the binding for the artifact file's location.
|
|
317
|
+
//
|
|
318
|
+
// Slice 006 collapses the per-change-id top-level dirs. The artifact
|
|
319
|
+
// file is now written under the SESSION dir
|
|
320
|
+
// (`.peaks/_runtime/<sid>/<role>/requests/`) instead of the
|
|
321
|
+
// change-id dir. The 2-tier fallback (canonical session → legacy
|
|
322
|
+
// session) replaces the F3 3-tier fallback (per-change-id →
|
|
323
|
+
// canonical session → legacy session). The change-id is preserved
|
|
324
|
+
// in the artifact body's frontmatter (under `- change-id:`) for
|
|
325
|
+
// human navigation; it is no longer a filesystem path key.
|
|
310
326
|
const sessionId = options.sessionId ?? await ensureSession(options.projectRoot);
|
|
311
|
-
|
|
312
|
-
|
|
327
|
+
const boundChangeId = getCurrentChangeId(options.projectRoot);
|
|
328
|
+
// Resolution order for the change-id (file body metadata):
|
|
329
|
+
// 1. Explicit `options.changeId` (CLI `--change-id`).
|
|
330
|
+
// 2. `current-change` binding (live developer working context).
|
|
331
|
+
// 3. The requestId itself (every request is its own scope by default).
|
|
332
|
+
const changeId = options.changeId ?? boundChangeId ?? options.requestId;
|
|
333
|
+
// Slice 008 (F21 fix): fail fast when the resolved session id
|
|
334
|
+
// looks like a real session id (matches the date+session prefix)
|
|
335
|
+
// but does NOT correspond to an actual session dir under
|
|
336
|
+
// `.peaks/_runtime/`. Pre-F21 a sub-agent with a typo or stale
|
|
337
|
+
// binding (e.g. `2025-01-01-session-deadbe`) silently planned
|
|
338
|
+
// to write to a non-existent path. The check is intentionally
|
|
339
|
+
// scoped to "looks like a real session id" — a sid like
|
|
340
|
+
// `test-session` or `s` (no date prefix) is allowed through so
|
|
341
|
+
// the existing F3 / slice-007 back-compat flows (e.g. the
|
|
342
|
+
// `peaks request init --session-id <arbitrary-scope>` tests)
|
|
343
|
+
// can still create the dir on demand via the writer's
|
|
344
|
+
// `mkdir(..., { recursive: true })`.
|
|
345
|
+
const LOOKS_LIKE_SESSION_ID = /^\d{4}-\d{2}-\d{2}-session-/;
|
|
346
|
+
if (LOOKS_LIKE_SESSION_ID.test(sessionId)) {
|
|
347
|
+
const sessionDir = join(options.projectRoot, '.peaks', '_runtime', sessionId);
|
|
348
|
+
if (!(await isDirectory(sessionDir))) {
|
|
349
|
+
const canonicalSid = getSessionIdCanonical(options.projectRoot);
|
|
350
|
+
const hint = canonicalSid !== null
|
|
351
|
+
? `Use --session-id ${canonicalSid} or run 'peaks workspace init' to create a new session.`
|
|
352
|
+
: `Run 'peaks workspace init' to create a new session.`;
|
|
353
|
+
throw new Error(`session id '${sessionId}' does not exist in _runtime/. Current canonical binding is '${canonicalSid ?? '<none>'}'. ${hint}`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
// Build numbered path under the session dir (canonical post-F3 home).
|
|
357
|
+
const requestsDir = join(options.projectRoot, '.peaks', '_runtime', sessionId, options.role, 'requests');
|
|
313
358
|
// Check if a file with this requestId already exists (regardless of number prefix)
|
|
314
359
|
if (await isDirectory(requestsDir)) {
|
|
315
360
|
const existingFiles = await listMarkdownFiles(requestsDir);
|
|
@@ -327,15 +372,18 @@ export async function createRequestArtifact(options) {
|
|
|
327
372
|
const number = getNextNumber(requestsDir);
|
|
328
373
|
const filename = buildNumberedFilename(number, options.requestId);
|
|
329
374
|
const path = join(requestsDir, filename);
|
|
330
|
-
const content = renderTemplate(options.role, options.requestId, sessionId, timestamp, requestType);
|
|
375
|
+
const content = renderTemplate(options.role, options.requestId, changeId, sessionId, timestamp, requestType);
|
|
331
376
|
if (options.apply !== true) {
|
|
332
377
|
return { role: options.role, requestId: options.requestId, sessionId, path, content, applied: false };
|
|
333
378
|
}
|
|
334
379
|
await mkdir(dirname(path), { recursive: true });
|
|
335
380
|
await writeFile(path, content, 'utf8');
|
|
336
|
-
// Create QA initiated marker so rd:qa-handoff gate can verify QA was invoked
|
|
381
|
+
// Create QA initiated marker so rd:qa-handoff gate can verify QA was invoked.
|
|
382
|
+
// Slice 006: the marker lives under the SESSION dir (canonical post-F3
|
|
383
|
+
// home), not the change-id dir. The gate's prereq scan finds it at
|
|
384
|
+
// `.peaks/_runtime/<sid>/qa/.initiated`.
|
|
337
385
|
if (options.role === 'qa') {
|
|
338
|
-
const qaDir = join(options.projectRoot, '.peaks', sessionId, 'qa');
|
|
386
|
+
const qaDir = join(options.projectRoot, '.peaks', '_runtime', sessionId, 'qa');
|
|
339
387
|
const initiatedPath = join(qaDir, '.initiated');
|
|
340
388
|
if (!existsSync(initiatedPath)) {
|
|
341
389
|
await mkdir(qaDir, { recursive: true });
|
|
@@ -348,6 +396,7 @@ function extractMetadata(markdown) {
|
|
|
348
396
|
let state = 'unknown';
|
|
349
397
|
let createdAt;
|
|
350
398
|
let requestType = DEFAULT_REQUEST_TYPE;
|
|
399
|
+
let sessionId;
|
|
351
400
|
for (const rawLine of markdown.split(/\r?\n/)) {
|
|
352
401
|
const line = rawLine.trim();
|
|
353
402
|
const stateMatch = /^-\s*state:\s*(.+?)\s*$/.exec(line);
|
|
@@ -367,21 +416,41 @@ function extractMetadata(markdown) {
|
|
|
367
416
|
requestType = candidate;
|
|
368
417
|
}
|
|
369
418
|
// Placeholder values (e.g. "feature | bug | refactor | clarification") fall back to default.
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
const sessionMatch = /^-\s*session:\s*(.+?)\s*$/.exec(line);
|
|
422
|
+
if (sessionMatch !== null && sessionMatch[1] !== undefined) {
|
|
423
|
+
sessionId = sessionMatch[1];
|
|
424
|
+
continue;
|
|
370
425
|
}
|
|
371
426
|
}
|
|
372
427
|
const base = { state, requestType };
|
|
373
428
|
if (createdAt !== undefined)
|
|
374
429
|
base.createdAt = createdAt;
|
|
430
|
+
if (sessionId !== undefined)
|
|
431
|
+
base.sessionId = sessionId;
|
|
375
432
|
return base;
|
|
376
433
|
}
|
|
377
|
-
async function readSummary(projectRoot,
|
|
378
|
-
const path = join(projectRoot, '.peaks',
|
|
434
|
+
async function readSummary(projectRoot, changeId, role, fileName) {
|
|
435
|
+
const path = join(projectRoot, '.peaks', changeId, role, 'requests', fileName);
|
|
379
436
|
const body = await readFile(path, 'utf8');
|
|
380
|
-
const { state, createdAt, requestType } = extractMetadata(body);
|
|
437
|
+
const { state, createdAt, requestType, sessionId: bodySessionId } = extractMetadata(body);
|
|
381
438
|
// Strip numbered prefix (e.g., "001-requestId.md" -> "requestId")
|
|
382
439
|
// Only strip 3-digit zero-padded prefixes (our incrementing number format)
|
|
383
440
|
const requestId = fileName.replace(/^0\d{2}-/, '').replace(/\.md$/, '');
|
|
384
|
-
|
|
441
|
+
// `changeId` is the durable scope (the directory the file lives in).
|
|
442
|
+
// `sessionId` is metadata (the session that wrote the file, parsed from
|
|
443
|
+
// the `- session:` body line). They may differ when a request is read
|
|
444
|
+
// across sessions (back-compat) or after a session re-bind.
|
|
445
|
+
const summary = {
|
|
446
|
+
role,
|
|
447
|
+
changeId,
|
|
448
|
+
sessionId: bodySessionId ?? changeId,
|
|
449
|
+
requestId,
|
|
450
|
+
path,
|
|
451
|
+
state,
|
|
452
|
+
requestType
|
|
453
|
+
};
|
|
385
454
|
if (createdAt !== undefined) {
|
|
386
455
|
summary.createdAt = createdAt;
|
|
387
456
|
}
|
|
@@ -402,15 +471,53 @@ export async function listRequestArtifacts(options) {
|
|
|
402
471
|
if (!(await isDirectory(peaksRoot))) {
|
|
403
472
|
return [];
|
|
404
473
|
}
|
|
405
|
-
|
|
474
|
+
// Slice 006 collapsed the per-change-id top-level dirs. The 2-tier
|
|
475
|
+
// resolution model is:
|
|
476
|
+
// 1. `.peaks/_runtime/<sid>/<role>/requests/` (post-F3 canonical
|
|
477
|
+
// session home; slice 006's primary home for request artifacts).
|
|
478
|
+
// 2. `.peaks/<sid>/<role>/requests/` (pre-F3 legacy home; back-compat
|
|
479
|
+
// for users who have not yet migrated).
|
|
480
|
+
//
|
|
481
|
+
// When `sessionId` is pinned, the function scans that one session's
|
|
482
|
+
// two tiers (canonical + legacy). When `sessionId` is NOT pinned,
|
|
483
|
+
// the function scans every session dir under `.peaks/_runtime/`
|
|
484
|
+
// (canonical) AND every legacy session dir under `.peaks/`
|
|
485
|
+
// (top-level). Per-change-id dirs (the old `.peaks/<changeId>/<role>/`
|
|
486
|
+
// layout) are NOT scanned — slice 008 will migrate the 5
|
|
487
|
+
// already-shipped slices' artifacts to the new layout; new request
|
|
488
|
+
// artifacts are written to the session dir directly.
|
|
489
|
+
const scopes = [];
|
|
490
|
+
if (options.sessionId !== undefined) {
|
|
491
|
+
scopes.push(join('_runtime', options.sessionId));
|
|
492
|
+
scopes.push(options.sessionId);
|
|
493
|
+
}
|
|
494
|
+
else {
|
|
495
|
+
const runtimeRoot = join(peaksRoot, '_runtime');
|
|
496
|
+
if (await isDirectory(runtimeRoot)) {
|
|
497
|
+
for (const sid of await listDirectories(runtimeRoot)) {
|
|
498
|
+
scopes.push(join('_runtime', sid));
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
// Legacy top-level session dirs: scan every non-`._peaks` top-level
|
|
502
|
+
// dir as a potential legacy scope. Slice 006 dropped per-change-id
|
|
503
|
+
// dirs, so any top-level dir name under `.peaks/` that is NOT
|
|
504
|
+
// `_runtime` (and not a well-known umbrella like retrospective,
|
|
505
|
+
// _dogfood, memory, etc. — those have no `<role>/requests/` tree)
|
|
506
|
+
// is treated as a candidate legacy session dir.
|
|
507
|
+
for (const dir of await listDirectories(peaksRoot)) {
|
|
508
|
+
if (dir === '_runtime')
|
|
509
|
+
continue;
|
|
510
|
+
scopes.push(dir);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
406
513
|
const roles = options.role !== undefined ? [options.role] : Array.from(VALID_ROLES);
|
|
407
514
|
const summaries = [];
|
|
408
|
-
for (const
|
|
515
|
+
for (const scope of scopes) {
|
|
409
516
|
for (const role of roles) {
|
|
410
|
-
const dir = join(peaksRoot,
|
|
517
|
+
const dir = join(peaksRoot, scope, role, 'requests');
|
|
411
518
|
const fileNames = await listMarkdownFiles(dir);
|
|
412
519
|
for (const fileName of fileNames) {
|
|
413
|
-
summaries.push(await readSummary(options.projectRoot,
|
|
520
|
+
summaries.push(await readSummary(options.projectRoot, scope, role, fileName));
|
|
414
521
|
}
|
|
415
522
|
}
|
|
416
523
|
}
|
|
@@ -438,32 +545,76 @@ export async function showRequestArtifact(options) {
|
|
|
438
545
|
}
|
|
439
546
|
return null;
|
|
440
547
|
};
|
|
548
|
+
// As of slice 2026-06-05-change-id-as-unit-of-work, the dir key is the
|
|
549
|
+
// change-id (not the session-id). When the caller pins `sessionId` we
|
|
550
|
+
// use it as the scope anyway (legacy callers, and tests that pass
|
|
551
|
+
// `STABLE_SESSION` as a stand-in).
|
|
552
|
+
//
|
|
553
|
+
// As of slice 2026-06-06-session-layout-canonicalize (F3), the
|
|
554
|
+
// canonical home for session dirs is `.peaks/_runtime/<sid>/`.
|
|
555
|
+
// The pre-F3 layout `.peaks/<sid>/` is preserved as a one-minor
|
|
556
|
+
// back-compat fallback (the new path wins when both exist). We
|
|
557
|
+
// resolve the dir to use UP FRONT (not lazily after a miss) so the
|
|
558
|
+
// prerequisite gate's "request artifact present" check observes
|
|
559
|
+
// the same path the rest of the canonical layout uses.
|
|
441
560
|
if (options.sessionId !== undefined) {
|
|
442
|
-
const
|
|
561
|
+
const canonicalDir = join(options.projectRoot, '.peaks', '_runtime', options.sessionId, options.role, 'requests');
|
|
562
|
+
const legacyDir = join(options.projectRoot, '.peaks', options.sessionId, options.role, 'requests');
|
|
563
|
+
// Try the canonical (post-F3) path first; fall back to the legacy
|
|
564
|
+
// path only if the canonical path is absent. The legacy path is
|
|
565
|
+
// expected to be empty after a `peaks workspace migrate --to-runtime`
|
|
566
|
+
// run; this fallback exists for users who have not yet migrated.
|
|
567
|
+
const dir = (await isDirectory(canonicalDir)) ? canonicalDir : legacyDir;
|
|
568
|
+
const scope = dir === canonicalDir
|
|
569
|
+
? join('_runtime', options.sessionId)
|
|
570
|
+
: options.sessionId;
|
|
443
571
|
const found = await findFileInDir(dir);
|
|
444
572
|
if (found === null) {
|
|
445
573
|
return null;
|
|
446
574
|
}
|
|
447
|
-
|
|
448
|
-
const content = await readFile(found.path, 'utf8');
|
|
449
|
-
return { ...summary, content };
|
|
575
|
+
return await readRequestArtifact(options.projectRoot, scope, options.role, found);
|
|
450
576
|
}
|
|
451
577
|
const peaksRoot = join(options.projectRoot, '.peaks');
|
|
452
578
|
if (!(await isDirectory(peaksRoot))) {
|
|
453
579
|
return null;
|
|
454
580
|
}
|
|
455
|
-
|
|
456
|
-
for
|
|
457
|
-
|
|
458
|
-
|
|
581
|
+
// Slice 006: scan only session-scoped dirs (canonical + legacy)
|
|
582
|
+
// for the artifact. The per-change-id top-level dirs are no longer
|
|
583
|
+
// scanned — they are frozen until slice 008 migrates them.
|
|
584
|
+
const runtimeRoot = join(peaksRoot, '_runtime');
|
|
585
|
+
if (await isDirectory(runtimeRoot)) {
|
|
586
|
+
for (const sid of await listDirectories(runtimeRoot)) {
|
|
587
|
+
const dir = join(runtimeRoot, sid, options.role, 'requests');
|
|
588
|
+
const found = await findFileInDir(dir);
|
|
589
|
+
if (found !== null) {
|
|
590
|
+
return await readRequestArtifact(options.projectRoot, join('_runtime', sid), options.role, found);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
for (const dir of await listDirectories(peaksRoot)) {
|
|
595
|
+
if (dir === '_runtime')
|
|
596
|
+
continue;
|
|
597
|
+
const target = join(peaksRoot, dir, options.role, 'requests');
|
|
598
|
+
const found = await findFileInDir(target);
|
|
459
599
|
if (found !== null) {
|
|
460
|
-
|
|
461
|
-
const content = await readFile(found.path, 'utf8');
|
|
462
|
-
return { ...summary, content };
|
|
600
|
+
return await readRequestArtifact(options.projectRoot, dir, options.role, found);
|
|
463
601
|
}
|
|
464
602
|
}
|
|
465
603
|
return null;
|
|
466
604
|
}
|
|
605
|
+
/** Read the summary + content for a found request file; treat a read
|
|
606
|
+
* error on the content as "not found" so the caller can fall through
|
|
607
|
+
* to the next candidate (the on-disk file may be partially written). */
|
|
608
|
+
async function readRequestArtifact(projectRoot, scope, role, found) {
|
|
609
|
+
const summary = await readSummary(projectRoot, scope, role, found.fileName);
|
|
610
|
+
try {
|
|
611
|
+
const content = await readFile(found.path, 'utf8');
|
|
612
|
+
return { ...summary, content };
|
|
613
|
+
}
|
|
614
|
+
catch {
|
|
615
|
+
return null;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
467
618
|
const ALLOWED_STATES_PER_ROLE = {
|
|
468
619
|
prd: ['draft', 'confirmed-by-user', 'handed-off', 'blocked'],
|
|
469
620
|
ui: ['draft', 'direction-locked', 'handed-off', 'blocked'],
|
|
@@ -601,6 +752,12 @@ export async function transitionRequestArtifact(options) {
|
|
|
601
752
|
});
|
|
602
753
|
const prerequisiteResult = await checkPrerequisites({
|
|
603
754
|
projectRoot: options.projectRoot,
|
|
755
|
+
changeId: existing.changeId,
|
|
756
|
+
// F3 repair cycle 1: pass the session binding so the gate can fall
|
|
757
|
+
// back to `.peaks/_runtime/<sid>/<role>/` (and the legacy
|
|
758
|
+
// `.peaks/<sid>/<role>/`) for prerequisite artifacts that still
|
|
759
|
+
// live under the session dir rather than the change-id dir. This
|
|
760
|
+
// mirrors the F1/F2 back-compat pattern.
|
|
604
761
|
sessionId: existing.sessionId,
|
|
605
762
|
role: options.role,
|
|
606
763
|
newState: options.newState,
|
|
@@ -653,6 +810,7 @@ export async function transitionRequestArtifact(options) {
|
|
|
653
810
|
await writeFile(existing.path, updated, 'utf8');
|
|
654
811
|
const result = {
|
|
655
812
|
role: options.role,
|
|
813
|
+
changeId: existing.changeId,
|
|
656
814
|
sessionId: existing.sessionId,
|
|
657
815
|
requestId: options.requestId,
|
|
658
816
|
path: existing.path,
|
|
@@ -18,6 +18,32 @@ export type CodegraphCapabilityProbe = {
|
|
|
18
18
|
binaryPath: string;
|
|
19
19
|
binaryExists: boolean;
|
|
20
20
|
};
|
|
21
|
+
export type DistVersionComparison = {
|
|
22
|
+
dist: string | null;
|
|
23
|
+
source: string;
|
|
24
|
+
match: boolean;
|
|
25
|
+
distReadable: boolean;
|
|
26
|
+
};
|
|
27
|
+
export type DistVersionProbe = () => DistVersionComparison;
|
|
28
|
+
export type WorkspaceLayoutInspection = {
|
|
29
|
+
topLevelSessionDirs: string[];
|
|
30
|
+
legacyDotfiles: string[];
|
|
31
|
+
/**
|
|
32
|
+
* Slice 007 — per-change-id top-level dirs (e.g. `.peaks/001-2026-06-06-.../`).
|
|
33
|
+
* The pre-F3 canonical layout put reviewable artifacts under a
|
|
34
|
+
* per-change-id top-level dir; the post-F3 canonical layout
|
|
35
|
+
* consolidates them under `.peaks/_runtime/<sid>/<role>/`. Any
|
|
36
|
+
* leftover per-change-id top-level dir is a regression to flag.
|
|
37
|
+
* Slice 008's migration will consolidate these; until then, the
|
|
38
|
+
* check reports them as `ok: false`.
|
|
39
|
+
*
|
|
40
|
+
* Optional in the type for back-compat with test probes that
|
|
41
|
+
* pre-date the slice 007 broadening; the check itself falls back
|
|
42
|
+
* to an empty array when the field is missing.
|
|
43
|
+
*/
|
|
44
|
+
perChangeIdDirs?: string[];
|
|
45
|
+
};
|
|
46
|
+
export type WorkspaceLayoutProbe = () => WorkspaceLayoutInspection;
|
|
21
47
|
export type DoctorOptions = {
|
|
22
48
|
schemasBaseDir?: string;
|
|
23
49
|
skillsBaseDir?: string;
|
|
@@ -29,5 +55,48 @@ export type DoctorOptions = {
|
|
|
29
55
|
workspaceInitializedProbe?: () => boolean;
|
|
30
56
|
/** Platform string (defaults to process.platform); injectable for tests. */
|
|
31
57
|
platform?: NodeJS.Platform;
|
|
58
|
+
/** Injected for the build:dist-version-matches-source check (defaults to compareDistVersion on disk). */
|
|
59
|
+
distVersionProbe?: DistVersionProbe;
|
|
60
|
+
/** Injected for the build:workspace-layout-canonical check (defaults to inspectWorkspaceLayout on disk). */
|
|
61
|
+
workspaceLayoutProbe?: WorkspaceLayoutProbe;
|
|
32
62
|
};
|
|
63
|
+
/**
|
|
64
|
+
* Pure helper extracted from `defaultWorkspaceInitializedProbe` so tests can
|
|
65
|
+
* drive the filesystem check without monkey-patching `process.cwd()` or
|
|
66
|
+
* `findProjectRoot`. Returns `true` when EITHER the canonical
|
|
67
|
+
* `.peaks/_runtime/session.json` OR the legacy `.peaks/.session.json` exists.
|
|
68
|
+
*/
|
|
69
|
+
export declare function isWorkspaceInitializedAt(projectRoot: string): boolean;
|
|
70
|
+
/**
|
|
71
|
+
* Pure helper that compares the published dist `CLI_VERSION` against the
|
|
72
|
+
* source-of-truth `package.json#version`. Default readers fail-soft to `null`
|
|
73
|
+
* on missing/unreadable/malformed input. Exported so tests can drive the
|
|
74
|
+
* filesystem reads without monkey-patching `process.cwd()`.
|
|
75
|
+
*/
|
|
76
|
+
export declare function compareDistVersion(opts: {
|
|
77
|
+
projectRoot: string;
|
|
78
|
+
distVersionReader?: (root: string) => string | null;
|
|
79
|
+
sourceVersionReader?: (root: string) => string | null;
|
|
80
|
+
}): DistVersionComparison;
|
|
81
|
+
/**
|
|
82
|
+
* Pure helper that inspects the on-disk workspace layout for
|
|
83
|
+
* post-F3-canonical violations. The post-F3 canonical layout puts
|
|
84
|
+
* session dirs under `.peaks/_runtime/<sid>/` and the runtime
|
|
85
|
+
* binding at `.peaks/_runtime/session.json`; the legacy paths
|
|
86
|
+
* (top-level `<YYYY-MM-DD-session-<hex>>/` dirs and the legacy
|
|
87
|
+
* top-level `.peaks/.session.json` / `.peaks/.active-skill.json`
|
|
88
|
+
* dotfiles) must be absent. This helper is exported so tests can
|
|
89
|
+
* drive the filesystem walk without monkey-patching `process.cwd()`
|
|
90
|
+
* or `findProjectRoot`.
|
|
91
|
+
*
|
|
92
|
+
* Both scanners fail-soft (return `[]` on read errors) so a flaky
|
|
93
|
+
* filesystem read on a non-fatal probe path never escalates into a
|
|
94
|
+
* doctor failure.
|
|
95
|
+
*/
|
|
96
|
+
export declare function inspectWorkspaceLayout(opts: {
|
|
97
|
+
projectRoot: string;
|
|
98
|
+
topLevelScanner?: (root: string) => string[];
|
|
99
|
+
dotfileScanner?: (root: string) => string[];
|
|
100
|
+
perChangeIdScanner?: (root: string) => string[];
|
|
101
|
+
}): WorkspaceLayoutInspection;
|
|
33
102
|
export declare function runDoctor(options?: DoctorOptions): Promise<DoctorReport>;
|