peaks-cli 1.0.12 → 1.0.13

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.
Files changed (107) hide show
  1. package/bin/peaks.js +0 -0
  2. package/dist/src/cli/commands/config-commands.js +1 -17
  3. package/dist/src/cli/commands/core-artifact-commands.js +23 -0
  4. package/dist/src/cli/commands/mcp-commands.d.ts +3 -0
  5. package/dist/src/cli/commands/mcp-commands.js +144 -0
  6. package/dist/src/cli/commands/openspec-commands.d.ts +3 -0
  7. package/dist/src/cli/commands/openspec-commands.js +169 -0
  8. package/dist/src/cli/commands/project-commands.d.ts +3 -0
  9. package/dist/src/cli/commands/project-commands.js +37 -0
  10. package/dist/src/cli/commands/request-commands.d.ts +3 -0
  11. package/dist/src/cli/commands/request-commands.js +140 -0
  12. package/dist/src/cli/commands/understand-commands.d.ts +3 -0
  13. package/dist/src/cli/commands/understand-commands.js +78 -0
  14. package/dist/src/cli/commands/workflow-commands.js +56 -94
  15. package/dist/src/cli/program.js +10 -0
  16. package/dist/src/services/artifacts/request-artifact-service.d.ts +58 -0
  17. package/dist/src/services/artifacts/request-artifact-service.js +432 -0
  18. package/dist/src/services/codegraph/codegraph-service.js +26 -45
  19. package/dist/src/services/config/config-service.js +2 -22
  20. package/dist/src/services/dashboard/project-dashboard-service.d.ts +64 -0
  21. package/dist/src/services/dashboard/project-dashboard-service.js +112 -0
  22. package/dist/src/services/doctor/doctor-service.d.ts +7 -0
  23. package/dist/src/services/doctor/doctor-service.js +139 -0
  24. package/dist/src/services/mcp/mcp-apply-service.d.ts +31 -0
  25. package/dist/src/services/mcp/mcp-apply-service.js +112 -0
  26. package/dist/src/services/mcp/mcp-call-service.d.ts +17 -0
  27. package/dist/src/services/mcp/mcp-call-service.js +34 -0
  28. package/dist/src/services/mcp/mcp-client-service.d.ts +14 -0
  29. package/dist/src/services/mcp/mcp-client-service.js +49 -0
  30. package/dist/src/services/mcp/mcp-install-registry.d.ts +11 -0
  31. package/dist/src/services/mcp/mcp-install-registry.js +38 -0
  32. package/dist/src/services/mcp/mcp-plan-service.d.ts +29 -0
  33. package/dist/src/services/mcp/mcp-plan-service.js +109 -0
  34. package/dist/src/services/mcp/mcp-protocol.d.ts +24 -0
  35. package/dist/src/services/mcp/mcp-protocol.js +41 -0
  36. package/dist/src/services/mcp/mcp-scan-service.d.ts +8 -0
  37. package/dist/src/services/mcp/mcp-scan-service.js +214 -0
  38. package/dist/src/services/mcp/mcp-stdio-transport.d.ts +10 -0
  39. package/dist/src/services/mcp/mcp-stdio-transport.js +50 -0
  40. package/dist/src/services/mcp/mcp-types.d.ts +31 -0
  41. package/dist/src/services/mcp/mcp-types.js +1 -0
  42. package/dist/src/services/openspec/openspec-archive-service.d.ts +12 -0
  43. package/dist/src/services/openspec/openspec-archive-service.js +28 -0
  44. package/dist/src/services/openspec/openspec-bridge-service.d.ts +16 -0
  45. package/dist/src/services/openspec/openspec-bridge-service.js +76 -0
  46. package/dist/src/services/openspec/openspec-render-service.d.ts +38 -0
  47. package/dist/src/services/openspec/openspec-render-service.js +130 -0
  48. package/dist/src/services/openspec/openspec-scan-service.d.ts +6 -0
  49. package/dist/src/services/openspec/openspec-scan-service.js +123 -0
  50. package/dist/src/services/openspec/openspec-types.d.ts +39 -0
  51. package/dist/src/services/openspec/openspec-types.js +1 -0
  52. package/dist/src/services/openspec/openspec-validate-service.d.ts +27 -0
  53. package/dist/src/services/openspec/openspec-validate-service.js +77 -0
  54. package/dist/src/services/recommendations/capability-seed-items.js +2 -1
  55. package/dist/src/services/recommendations/capability-seed-mappings.js +1 -1
  56. package/dist/src/services/recommendations/capability-seed-sources.js +1 -1
  57. package/dist/src/services/shadcn/shadcn-service.d.ts +4 -0
  58. package/dist/src/services/shadcn/shadcn-service.js +15 -30
  59. package/dist/src/services/skills/skill-runbook-service.d.ts +11 -0
  60. package/dist/src/services/skills/skill-runbook-service.js +60 -0
  61. package/dist/src/services/standards/project-standards-service.js +4 -9
  62. package/dist/src/services/understand/understand-scan-service.d.ts +28 -0
  63. package/dist/src/services/understand/understand-scan-service.js +157 -0
  64. package/dist/src/services/understand/understand-types.d.ts +24 -0
  65. package/dist/src/services/understand/understand-types.js +1 -0
  66. package/dist/src/shared/json-schema-mini.d.ts +10 -0
  67. package/dist/src/shared/json-schema-mini.js +113 -0
  68. package/dist/src/shared/paths.d.ts +1 -1
  69. package/dist/src/shared/paths.js +9 -1
  70. package/dist/src/shared/version.d.ts +1 -1
  71. package/dist/src/shared/version.js +1 -1
  72. package/package.json +2 -8
  73. package/schemas/doctor-report.schema.json +34 -0
  74. package/schemas/mcp-apply-result.schema.json +46 -0
  75. package/schemas/mcp-install-plan.schema.json +71 -0
  76. package/schemas/mcp-install-spec.schema.json +29 -0
  77. package/schemas/mcp-server.schema.json +29 -0
  78. package/schemas/openspec-change-summary.schema.json +68 -0
  79. package/schemas/openspec-render-request.schema.json +61 -0
  80. package/schemas/openspec-validation-result.schema.json +36 -0
  81. package/skills/peaks-prd/SKILL.md +59 -8
  82. package/skills/peaks-prd/references/artifact-per-request.md +78 -0
  83. package/skills/peaks-prd/references/workflow.md +7 -5
  84. package/skills/peaks-qa/SKILL.md +74 -8
  85. package/skills/peaks-qa/references/artifact-contracts.md +2 -2
  86. package/skills/peaks-qa/references/artifact-per-request.md +83 -0
  87. package/skills/peaks-qa/references/openspec-validation-gate.md +55 -0
  88. package/skills/peaks-qa/references/regression-gates.md +2 -2
  89. package/skills/peaks-rd/SKILL.md +96 -9
  90. package/skills/peaks-rd/references/artifact-contracts.md +2 -2
  91. package/skills/peaks-rd/references/artifact-per-request.md +90 -0
  92. package/skills/peaks-rd/references/openspec-mcp-cli.md +65 -0
  93. package/skills/peaks-rd/references/refactor-workflow.md +2 -2
  94. package/skills/peaks-sc/SKILL.md +44 -0
  95. package/skills/peaks-sc/references/openspec-commit-boundaries.md +33 -0
  96. package/skills/peaks-solo/SKILL.md +90 -9
  97. package/skills/peaks-solo/references/artifact-contracts.md +2 -2
  98. package/skills/peaks-solo/references/browser-workflow.md +114 -0
  99. package/skills/peaks-solo/references/external-skill-invocation.md +70 -0
  100. package/skills/peaks-solo/references/openspec-mcp-workflow.md +53 -0
  101. package/skills/peaks-solo/references/refactor-mode.md +2 -2
  102. package/skills/peaks-solo/references/workflow.md +1 -1
  103. package/skills/peaks-txt/SKILL.md +42 -0
  104. package/skills/peaks-ui/SKILL.md +57 -33
  105. package/skills/peaks-ui/references/artifact-per-request.md +71 -0
  106. package/skills/peaks-ui/references/workflow.md +8 -11
  107. package/scripts/strip-internal-exports.mjs +0 -33
@@ -0,0 +1,432 @@
1
+ import { mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
2
+ import { dirname, join } from 'node:path';
3
+ import { isDirectory, listDirectories, pathExists } from '../../shared/fs.js';
4
+ const REQUEST_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
5
+ const VALID_ROLES = new Set(['prd', 'ui', 'rd', 'qa']);
6
+ function defaultClock() {
7
+ return new Date().toISOString();
8
+ }
9
+ function dateSlugFromIso(iso) {
10
+ return iso.slice(0, 10);
11
+ }
12
+ function defaultSessionId(iso) {
13
+ return `${dateSlugFromIso(iso)}-session`;
14
+ }
15
+ function renderPrdTemplate(requestId, sessionId, timestamp) {
16
+ return `# PRD Request ${requestId}
17
+
18
+ - session: ${sessionId}
19
+ - type: feature | bug | refactor | clarification
20
+ - source: <ticket, message URL, or "verbal" with a short sanitized quote>
21
+ - raw input (sanitized): <one-paragraph restatement of what the user actually asked for>
22
+
23
+ ## Goals
24
+
25
+ - ...
26
+
27
+ ## Non-goals
28
+
29
+ - ...
30
+
31
+ ## Preserved behavior
32
+
33
+ - ...
34
+
35
+ ## Acceptance criteria
36
+
37
+ - ... (browser-verifiable when frontend is in scope)
38
+
39
+ ## Frontend delta (only when target is frontend)
40
+
41
+ - pages / routes / components / states / permissions / data deps / edge cases
42
+ - 待联调态: ...
43
+ - API contracts pending: ...
44
+
45
+ ## Risks and open questions
46
+
47
+ - ...
48
+
49
+ ## Handoff
50
+
51
+ - to peaks-rd: .peaks/${sessionId}/rd/requests/${requestId}.md
52
+ - to peaks-qa: .peaks/${sessionId}/qa/requests/${requestId}.md
53
+ - to peaks-ui: .peaks/${sessionId}/ui/requests/${requestId}.md (when UI involved)
54
+
55
+ ## Status
56
+
57
+ - created: ${timestamp}
58
+ - last update: ${timestamp}
59
+ - state: draft
60
+ `;
61
+ }
62
+ function renderUiTemplate(requestId, sessionId, timestamp) {
63
+ return `# UI Request ${requestId}
64
+
65
+ - session: ${sessionId}
66
+ - linked-prd: .peaks/${sessionId}/prd/requests/${requestId}.md
67
+ - scope: full new surface | iteration on existing surface | regression fix | visual refresh
68
+ - design direction: editorial | bento | Swiss | luxury | retro-futurist | glass | product-system | other-explicit-name
69
+
70
+ ## Affected surfaces
71
+
72
+ - pages / routes / components / modals / states (loading, empty, error, hover, focus, active, responsive)
73
+ - explicit out-of-scope surfaces (do not modify)
74
+
75
+ ## UX flow and page states
76
+
77
+ - entry points, primary flow, secondary flows, exit points
78
+ - state machine per surface when state transitions matter
79
+
80
+ ## Visual constraints
81
+
82
+ - typography pair: ...
83
+ - palette: ...
84
+ - density and motion intensity dials: ...
85
+ - rejected generic patterns
86
+
87
+ ## Interaction constraints
88
+
89
+ - keyboard, focus order, ARIA roles, gesture support, accessibility minima
90
+
91
+ ## UI regression seeds
92
+
93
+ - list of visible regressions QA must check against the prior state
94
+
95
+ ## Browser evidence
96
+
97
+ - sanitized observations only — no login URLs, cookies, headers, tokens, storage state, browser traces, or screenshots/logs with PII / SSO / MFA material
98
+
99
+ ## Handoff
100
+
101
+ - to peaks-rd: .peaks/${sessionId}/rd/requests/${requestId}.md
102
+ - to peaks-qa: .peaks/${sessionId}/qa/requests/${requestId}.md
103
+
104
+ ## Status
105
+
106
+ - created: ${timestamp}
107
+ - last update: ${timestamp}
108
+ - state: draft
109
+ `;
110
+ }
111
+ function renderRdTemplate(requestId, sessionId, timestamp) {
112
+ return `# RD Request ${requestId}
113
+
114
+ - session: ${sessionId}
115
+ - linked-prd: .peaks/${sessionId}/prd/requests/${requestId}.md
116
+ - linked-ui: .peaks/${sessionId}/ui/requests/${requestId}.md (when UI involved)
117
+ - type: feature | bug | refactor | clarification
118
+
119
+ ## Red-line scope
120
+
121
+ - in-scope files / routes / API paths / data models
122
+ - explicit out-of-scope surfaces (do not modify, mock, delete, or replace)
123
+
124
+ ## Standards preflight
125
+
126
+ - peaks standards init/update --project <path> --dry-run output paths and status
127
+ - planned application: apply | review-only | blocked
128
+
129
+ ## OpenSpec linkage (when openspec/ exists)
130
+
131
+ - change-id: <openspec change id>
132
+ - entry validate: peaks openspec validate <change-id> data.valid status
133
+ - to-rd projection: peaks openspec to-rd <change-id> artifact path
134
+ - exit validate (after implementation): status
135
+
136
+ ## Coverage status
137
+
138
+ - current total UT coverage: <percent>
139
+ - new/changed code coverage: <percent>
140
+ - gate verdict: pass | legacy-accepted | blocked
141
+
142
+ ## Slice contract
143
+
144
+ - slice id, functional boundary, pre-refactor behavior, target structure, unit-test requirements, acceptance checks, rollback plan, commit boundary
145
+
146
+ ## Implementation evidence
147
+
148
+ - diff paths, test commands + outputs, code review findings + fixes, security review findings + fixes, dry-run output
149
+
150
+ ## MCP usage (when external docs lookup was used)
151
+
152
+ - capabilityId / tool / sanitized args
153
+ - artifact path of stored result
154
+ - no secrets, no full network bodies
155
+
156
+ ## Handoff
157
+
158
+ - to peaks-qa: .peaks/${sessionId}/qa/requests/${requestId}.md
159
+ - to peaks-sc: .peaks/${sessionId}/sc/commit-boundaries/${requestId}.md
160
+
161
+ ## Status
162
+
163
+ - created: ${timestamp}
164
+ - last update: ${timestamp}
165
+ - state: draft
166
+ `;
167
+ }
168
+ function renderQaTemplate(requestId, sessionId, timestamp) {
169
+ return `# QA Request ${requestId}
170
+
171
+ - session: ${sessionId}
172
+ - linked-prd: .peaks/${sessionId}/prd/requests/${requestId}.md
173
+ - linked-rd: .peaks/${sessionId}/rd/requests/${requestId}.md
174
+ - linked-ui: .peaks/${sessionId}/ui/requests/${requestId}.md (when UI involved)
175
+ - type: feature | bug | refactor | clarification
176
+
177
+ ## Red-line boundary check
178
+
179
+ - in-scope changes seen in the diff (match PRD + RD scope)
180
+ - out-of-scope changes flagged (any extra file, route, mock, fixture, behavior)
181
+ - verdict: clean | boundary-violation
182
+
183
+ ## OpenSpec exit gate (when openspec/ exists)
184
+
185
+ - change-id: <id>
186
+ - peaks openspec validate <id> data.valid: true | false
187
+ - issues: ...
188
+
189
+ ## Acceptance checks
190
+
191
+ - per-criterion: check method, result (pass | fail | blocked), evidence path
192
+
193
+ ## Mandatory validation gates
194
+
195
+ - unit tests: command + pass/fail + coverage delta
196
+ - API validation (when applicable): request paths exercised, evidence
197
+ - browser E2E (when frontend): headed gstack/browse/dist/browse visible-browser confirmation, sanitized route/actions, console/network observations
198
+ - browser-error feedback loop: page errors, console exceptions, broken network, hydration failures → return-to-RD evidence
199
+ - security check: tool used, findings, fixes, unresolved risks
200
+ - performance check: tool used, baseline vs after numbers when available
201
+ - validation report path
202
+
203
+ ## Regression matrix
204
+
205
+ - list of surfaces / API paths / browser flows checked
206
+ - pass/fail per row
207
+
208
+ ## Browser evidence
209
+
210
+ - sanitized observations only — no login URLs, cookies, headers, tokens, storage state, browser traces, or screenshots/logs with PII / SSO / MFA material
211
+
212
+ ## Verdict
213
+
214
+ - overall: pass | return-to-rd | blocked
215
+
216
+ ## Status
217
+
218
+ - created: ${timestamp}
219
+ - last update: ${timestamp}
220
+ - state: draft
221
+ `;
222
+ }
223
+ function renderTemplate(role, requestId, sessionId, timestamp) {
224
+ switch (role) {
225
+ case 'prd':
226
+ return renderPrdTemplate(requestId, sessionId, timestamp);
227
+ case 'ui':
228
+ return renderUiTemplate(requestId, sessionId, timestamp);
229
+ case 'rd':
230
+ return renderRdTemplate(requestId, sessionId, timestamp);
231
+ case 'qa':
232
+ return renderQaTemplate(requestId, sessionId, timestamp);
233
+ }
234
+ }
235
+ export async function createRequestArtifact(options) {
236
+ if (!VALID_ROLES.has(options.role)) {
237
+ throw new Error(`Invalid role: ${String(options.role)} (expected prd, ui, rd, or qa)`);
238
+ }
239
+ if (!REQUEST_ID_PATTERN.test(options.requestId)) {
240
+ throw new Error(`Invalid request id: ${options.requestId} (expected letters, digits, dots, underscores, or dashes)`);
241
+ }
242
+ const clock = options.clock ?? defaultClock;
243
+ const timestamp = clock();
244
+ const sessionId = options.sessionId ?? defaultSessionId(timestamp);
245
+ const path = join(options.projectRoot, '.peaks', sessionId, options.role, 'requests', `${options.requestId}.md`);
246
+ const content = renderTemplate(options.role, options.requestId, sessionId, timestamp);
247
+ if (options.apply !== true) {
248
+ return { role: options.role, requestId: options.requestId, sessionId, path, content, applied: false };
249
+ }
250
+ if (await pathExists(path)) {
251
+ throw new Error(`Refusing to write: ${path} already exists. Update it in place or remove it before re-running peaks request init.`);
252
+ }
253
+ await mkdir(dirname(path), { recursive: true });
254
+ await writeFile(path, content, 'utf8');
255
+ return { role: options.role, requestId: options.requestId, sessionId, path, content, applied: true };
256
+ }
257
+ function extractStateAndCreated(markdown) {
258
+ let state = 'unknown';
259
+ let createdAt;
260
+ for (const rawLine of markdown.split(/\r?\n/)) {
261
+ const line = rawLine.trim();
262
+ const stateMatch = /^-\s*state:\s*(.+?)\s*$/.exec(line);
263
+ if (stateMatch !== null && stateMatch[1] !== undefined) {
264
+ state = stateMatch[1];
265
+ continue;
266
+ }
267
+ const createdMatch = /^-\s*created:\s*(.+?)\s*$/.exec(line);
268
+ if (createdMatch !== null && createdMatch[1] !== undefined) {
269
+ createdAt = createdMatch[1];
270
+ }
271
+ }
272
+ return createdAt === undefined ? { state } : { state, createdAt };
273
+ }
274
+ async function readSummary(projectRoot, sessionId, role, fileName) {
275
+ const path = join(projectRoot, '.peaks', sessionId, role, 'requests', fileName);
276
+ const body = await readFile(path, 'utf8');
277
+ const { state, createdAt } = extractStateAndCreated(body);
278
+ const requestId = fileName.replace(/\.md$/, '');
279
+ const summary = { role, sessionId, requestId, path, state };
280
+ if (createdAt !== undefined) {
281
+ summary.createdAt = createdAt;
282
+ }
283
+ return summary;
284
+ }
285
+ async function listMarkdownFiles(dir) {
286
+ if (!(await isDirectory(dir))) {
287
+ return [];
288
+ }
289
+ const entries = await readdir(dir, { withFileTypes: true });
290
+ return entries
291
+ .filter((entry) => entry.isFile() && entry.name.endsWith('.md'))
292
+ .map((entry) => entry.name)
293
+ .sort();
294
+ }
295
+ export async function listRequestArtifacts(options) {
296
+ const peaksRoot = join(options.projectRoot, '.peaks');
297
+ if (!(await isDirectory(peaksRoot))) {
298
+ return [];
299
+ }
300
+ const sessions = options.sessionId !== undefined ? [options.sessionId] : await listDirectories(peaksRoot);
301
+ const roles = options.role !== undefined ? [options.role] : Array.from(VALID_ROLES);
302
+ const summaries = [];
303
+ for (const sessionId of sessions) {
304
+ for (const role of roles) {
305
+ const dir = join(peaksRoot, sessionId, role, 'requests');
306
+ const fileNames = await listMarkdownFiles(dir);
307
+ for (const fileName of fileNames) {
308
+ summaries.push(await readSummary(options.projectRoot, sessionId, role, fileName));
309
+ }
310
+ }
311
+ }
312
+ return summaries;
313
+ }
314
+ export async function showRequestArtifact(options) {
315
+ if (!VALID_ROLES.has(options.role)) {
316
+ throw new Error(`Invalid role: ${String(options.role)} (expected prd, ui, rd, or qa)`);
317
+ }
318
+ if (!REQUEST_ID_PATTERN.test(options.requestId)) {
319
+ throw new Error(`Invalid request id: ${options.requestId} (expected letters, digits, dots, underscores, or dashes)`);
320
+ }
321
+ const fileName = `${options.requestId}.md`;
322
+ if (options.sessionId !== undefined) {
323
+ const path = join(options.projectRoot, '.peaks', options.sessionId, options.role, 'requests', fileName);
324
+ if (!(await pathExists(path))) {
325
+ return null;
326
+ }
327
+ const summary = await readSummary(options.projectRoot, options.sessionId, options.role, fileName);
328
+ const content = await readFile(path, 'utf8');
329
+ return { ...summary, content };
330
+ }
331
+ const peaksRoot = join(options.projectRoot, '.peaks');
332
+ if (!(await isDirectory(peaksRoot))) {
333
+ return null;
334
+ }
335
+ const sessions = await listDirectories(peaksRoot);
336
+ for (const sessionId of sessions) {
337
+ const path = join(peaksRoot, sessionId, options.role, 'requests', fileName);
338
+ if (await pathExists(path)) {
339
+ const summary = await readSummary(options.projectRoot, sessionId, options.role, fileName);
340
+ const content = await readFile(path, 'utf8');
341
+ return { ...summary, content };
342
+ }
343
+ }
344
+ return null;
345
+ }
346
+ const ALLOWED_STATES_PER_ROLE = {
347
+ prd: ['draft', 'confirmed-by-user', 'handed-off', 'blocked'],
348
+ ui: ['draft', 'direction-locked', 'handed-off', 'blocked'],
349
+ rd: ['draft', 'spec-locked', 'implemented', 'qa-handoff', 'handed-off', 'blocked'],
350
+ qa: ['draft', 'running', 'verdict-issued', 'blocked']
351
+ };
352
+ export function allowedStatesForRole(role) {
353
+ return ALLOWED_STATES_PER_ROLE[role];
354
+ }
355
+ function updateStatusBlock(markdown, newState, timestamp, reason) {
356
+ const lines = markdown.split(/\r?\n/);
357
+ let previousState = 'unknown';
358
+ let stateLineIndex = -1;
359
+ let lastUpdateLineIndex = -1;
360
+ for (const [index, raw] of lines.entries()) {
361
+ const trimmed = raw.trim();
362
+ const stateMatch = /^-\s*state:\s*(.+?)\s*$/.exec(trimmed);
363
+ if (stateMatch !== null && stateMatch[1] !== undefined) {
364
+ previousState = stateMatch[1];
365
+ stateLineIndex = index;
366
+ continue;
367
+ }
368
+ if (/^-\s*last update:\s*/.test(trimmed)) {
369
+ lastUpdateLineIndex = index;
370
+ }
371
+ }
372
+ if (stateLineIndex >= 0) {
373
+ lines[stateLineIndex] = `- state: ${newState}`;
374
+ }
375
+ else {
376
+ lines.push('', '## Status', '', `- state: ${newState}`);
377
+ }
378
+ if (lastUpdateLineIndex >= 0) {
379
+ lines[lastUpdateLineIndex] = `- last update: ${timestamp}`;
380
+ }
381
+ else if (stateLineIndex >= 0) {
382
+ lines.splice(stateLineIndex, 0, `- last update: ${timestamp}`);
383
+ }
384
+ else {
385
+ lines.push(`- last update: ${timestamp}`);
386
+ }
387
+ if (reason !== undefined && reason.length > 0) {
388
+ lines.push(`- transition note (${timestamp}): ${reason}`);
389
+ }
390
+ return { updated: lines.join('\n'), previousState };
391
+ }
392
+ export async function transitionRequestArtifact(options) {
393
+ if (!VALID_ROLES.has(options.role)) {
394
+ throw new Error(`Invalid role: ${String(options.role)} (expected prd, ui, rd, or qa)`);
395
+ }
396
+ if (!REQUEST_ID_PATTERN.test(options.requestId)) {
397
+ throw new Error(`Invalid request id: ${options.requestId} (expected letters, digits, dots, underscores, or dashes)`);
398
+ }
399
+ const allowed = ALLOWED_STATES_PER_ROLE[options.role];
400
+ if (!allowed.includes(options.newState)) {
401
+ throw new Error(`Invalid state for role ${options.role}: ${options.newState} (expected one of ${allowed.join(', ')})`);
402
+ }
403
+ const showOptions = {
404
+ projectRoot: options.projectRoot,
405
+ role: options.role,
406
+ requestId: options.requestId
407
+ };
408
+ if (options.sessionId !== undefined) {
409
+ showOptions.sessionId = options.sessionId;
410
+ }
411
+ const existing = await showRequestArtifact(showOptions);
412
+ if (existing === null) {
413
+ return null;
414
+ }
415
+ const clock = options.clock ?? defaultClock;
416
+ const timestamp = clock();
417
+ const { updated, previousState } = updateStatusBlock(existing.content, options.newState, timestamp, options.reason);
418
+ await writeFile(existing.path, updated, 'utf8');
419
+ const result = {
420
+ role: options.role,
421
+ sessionId: existing.sessionId,
422
+ requestId: options.requestId,
423
+ path: existing.path,
424
+ state: options.newState,
425
+ previousState,
426
+ content: updated
427
+ };
428
+ if (existing.createdAt !== undefined) {
429
+ result.createdAt = existing.createdAt;
430
+ }
431
+ return result;
432
+ }
@@ -1,15 +1,19 @@
1
1
  import { existsSync, realpathSync, statSync } from 'node:fs';
2
2
  import { spawn } from 'node:child_process';
3
3
  import { createRequire } from 'node:module';
4
- import { dirname, isAbsolute, relative, resolve, sep, win32 } from 'node:path';
4
+ import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
5
5
  const CODEGRAPH_PACKAGE_NAME = '@colbymchenry/codegraph';
6
6
  const CODEGRAPH_PACKAGE_VERSION = '0.7.10';
7
7
  const CODEGRAPH_EXECUTABLE = process.execPath;
8
8
  const CODEGRAPH_BINARY_PATH = resolveCodegraphBinaryPath();
9
9
  const CODEGRAPH_PROCESS_TIMEOUT_MS = 600_000;
10
10
  const CODEGRAPH_OUTPUT_LIMIT_BYTES = 10 * 1024 * 1024;
11
+ const NODE_OPTIONS_ENV_KEY = 'NODE_OPTIONS';
12
+ const NPM_CONFIG_PREFIX = 'npm_config_';
13
+ const NPM_CONFIG_UPPER_PREFIX = 'NPM_CONFIG_';
11
14
  const POSITIONAL_ARGUMENT_PREFIX = '-';
12
15
  const ALLOWED_SUBCOMMANDS = ['status', 'init', 'index', 'query', 'files', 'context', 'affected'];
16
+ const NUMERIC_FLAG_NAMES = ['limit', 'maxDepth'];
13
17
  const COMMON_OPTION_KEYS = ['subcommand', 'project'];
14
18
  const ALLOWED_OPTIONS_BY_SUBCOMMAND = {
15
19
  status: [],
@@ -20,16 +24,13 @@ const ALLOWED_OPTIONS_BY_SUBCOMMAND = {
20
24
  context: ['task'],
21
25
  affected: ['files', 'json']
22
26
  };
23
- function assertCodegraphBinaryExists(binaryPath) {
24
- if (!existsSync(binaryPath)) {
25
- throw new Error('Unable to resolve local codegraph binary from @colbymchenry/codegraph');
26
- }
27
- }
28
27
  function resolveCodegraphBinaryPath() {
29
28
  const require = createRequire(import.meta.url);
30
29
  const packageJsonPath = require.resolve('@colbymchenry/codegraph/package.json');
31
30
  const binaryPath = resolve(dirname(packageJsonPath), 'dist', 'bin', 'codegraph.js');
32
- assertCodegraphBinaryExists(binaryPath);
31
+ if (!existsSync(binaryPath)) {
32
+ throw new Error('Unable to resolve local codegraph binary from @colbymchenry/codegraph');
33
+ }
33
34
  return binaryPath;
34
35
  }
35
36
  function assertSupportedSubcommand(subcommand) {
@@ -37,15 +38,12 @@ function assertSupportedSubcommand(subcommand) {
37
38
  throw new Error(`Unsupported codegraph subcommand: ${subcommand}`);
38
39
  }
39
40
  }
40
- function assertProjectRootDirectory(projectRoot) {
41
- if (!statSync(projectRoot).isDirectory()) {
42
- throw new Error('Project path must exist and be a directory');
43
- }
44
- }
45
41
  function resolveProjectRoot(project) {
46
42
  const projectRoot = resolve(project);
47
43
  try {
48
- assertProjectRootDirectory(projectRoot);
44
+ if (!statSync(projectRoot).isDirectory()) {
45
+ throw new Error('Project path must exist and be a directory');
46
+ }
49
47
  return realpathSync.native(projectRoot);
50
48
  }
51
49
  catch {
@@ -90,8 +88,12 @@ function assertInsideProject(projectRoot, absolutePath) {
90
88
  throw new Error('Affected files must stay inside the project');
91
89
  }
92
90
  }
93
- function climbToExistingBoundary(currentPath, pathExists = existsSync) {
94
- while (!pathExists(currentPath)) {
91
+ function resolveExistingBoundary(absoluteFilePath) {
92
+ if (existsSync(absoluteFilePath)) {
93
+ return absoluteFilePath;
94
+ }
95
+ let currentPath = dirname(absoluteFilePath);
96
+ while (!existsSync(currentPath)) {
95
97
  const parentPath = dirname(currentPath);
96
98
  if (parentPath === currentPath) {
97
99
  return currentPath;
@@ -100,9 +102,6 @@ function climbToExistingBoundary(currentPath, pathExists = existsSync) {
100
102
  }
101
103
  return currentPath;
102
104
  }
103
- function resolveExistingBoundary(absoluteFilePath) {
104
- return existsSync(absoluteFilePath) ? absoluteFilePath : climbToExistingBoundary(dirname(absoluteFilePath));
105
- }
106
105
  function normalizeProjectRelativeFile(projectRoot, file) {
107
106
  assertPositionalArgument(file, 'Affected files');
108
107
  const absoluteFilePath = resolve(projectRoot, file);
@@ -111,13 +110,10 @@ function normalizeProjectRelativeFile(projectRoot, file) {
111
110
  assertInsideProject(projectRoot, realBoundary);
112
111
  return relative(projectRoot, absoluteFilePath).split(sep).join('/');
113
112
  }
114
- function assertAffectedFiles(files) {
113
+ function buildAffectedFileArgs(projectRoot, files) {
115
114
  if (!files || files.length < 1) {
116
115
  throw new Error('affected requires at least one file');
117
116
  }
118
- }
119
- function buildAffectedFileArgs(projectRoot, files) {
120
- assertAffectedFiles(files);
121
117
  return files.map((file) => normalizeProjectRelativeFile(projectRoot, file));
122
118
  }
123
119
  function buildCommandArgs(options, projectRoot) {
@@ -172,36 +168,24 @@ function assertOutputLimit(currentSize, chunkSize) {
172
168
  }
173
169
  return nextSize;
174
170
  }
175
- function getWindowsTaskkillPath(fileExists = existsSync) {
176
- const candidates = [
177
- win32.join('C:\\Windows', 'System32', 'taskkill.exe'),
178
- win32.join('C:\\WINNT', 'System32', 'taskkill.exe')
179
- ];
180
- return candidates.find((candidate) => fileExists(candidate)) ?? null;
181
- }
182
- function terminateCodegraphProcess(childProcess, platform = process.platform, killProcess = process.kill, spawnProcess = spawn, taskkillPath = getWindowsTaskkillPath()) {
171
+ function terminateCodegraphProcess(childProcess) {
183
172
  if (childProcess.pid === undefined) {
184
173
  childProcess.kill();
185
174
  return;
186
175
  }
187
- if (platform === 'win32') {
188
- if (!taskkillPath) {
189
- childProcess.kill();
190
- return;
191
- }
192
- const killerProcess = spawnProcess(taskkillPath, ['/pid', String(childProcess.pid), '/T', '/F'], { shell: false, stdio: 'ignore' });
193
- killerProcess.on('error', () => childProcess.kill());
194
- killerProcess.unref();
176
+ if (process.platform === 'win32') {
177
+ const taskkillPath = process.env.SystemRoot ? join(process.env.SystemRoot, 'System32', 'taskkill.exe') : 'taskkill.exe';
178
+ spawn(taskkillPath, ['/pid', String(childProcess.pid), '/T', '/F'], { shell: false, stdio: 'ignore' });
195
179
  return;
196
180
  }
197
181
  try {
198
- killProcess(-childProcess.pid, 'SIGTERM');
182
+ process.kill(-childProcess.pid, 'SIGTERM');
199
183
  }
200
184
  catch {
201
185
  childProcess.kill('SIGTERM');
202
186
  }
203
187
  }
204
- function runCodegraphProcess(invocation, timeoutMs = CODEGRAPH_PROCESS_TIMEOUT_MS) {
188
+ function defaultCodegraphProcessRunner(invocation) {
205
189
  return new Promise((resolveResult, reject) => {
206
190
  const childProcess = spawn(invocation.executable, invocation.args, {
207
191
  cwd: invocation.cwd,
@@ -211,8 +195,8 @@ function runCodegraphProcess(invocation, timeoutMs = CODEGRAPH_PROCESS_TIMEOUT_M
211
195
  });
212
196
  const timeout = setTimeout(() => {
213
197
  terminateCodegraphProcess(childProcess);
214
- reject(new Error(`codegraph process timed out after ${timeoutMs}ms`));
215
- }, timeoutMs);
198
+ reject(new Error(`codegraph process timed out after ${CODEGRAPH_PROCESS_TIMEOUT_MS}ms`));
199
+ }, CODEGRAPH_PROCESS_TIMEOUT_MS);
216
200
  const stdoutChunks = [];
217
201
  const stderrChunks = [];
218
202
  let stdoutSize = 0;
@@ -251,9 +235,6 @@ function runCodegraphProcess(invocation, timeoutMs = CODEGRAPH_PROCESS_TIMEOUT_M
251
235
  });
252
236
  });
253
237
  }
254
- function defaultCodegraphProcessRunner(invocation) {
255
- return runCodegraphProcess(invocation);
256
- }
257
238
  export function createCodegraphInvocation(options) {
258
239
  assertSupportedSubcommand(options.subcommand);
259
240
  const projectRoot = resolveProjectRoot(options.project);
@@ -156,25 +156,6 @@ function validateUserConfigPathForWrite(configPath) {
156
156
  }
157
157
  }
158
158
  }
159
- function getExistingBoundary(path) {
160
- let currentPath = resolve(path);
161
- while (!existsSync(currentPath)) {
162
- const parentPath = dirname(currentPath);
163
- if (parentPath === currentPath) {
164
- return currentPath;
165
- }
166
- currentPath = parentPath;
167
- }
168
- return currentPath;
169
- }
170
- function validateArtifactWorkspaceRootBeforeCreate(artifactRoot, workspaceRoot) {
171
- const workspaceRootReal = realpathSync(workspaceRoot);
172
- const existingBoundary = getExistingBoundary(artifactRoot);
173
- const existingBoundaryReal = realpathSync(existingBoundary);
174
- if (isInsidePath(resolve(artifactRoot), workspaceRoot) || isInsidePath(existingBoundaryReal, workspaceRootReal)) {
175
- throw new Error('Artifact workspace must stay outside the project root');
176
- }
177
- }
178
159
  function validateArtifactWorkspaceRoot(artifactRoot, workspaceRoot) {
179
160
  const artifactStats = lstatSync(artifactRoot);
180
161
  if (!artifactStats.isDirectory() || artifactStats.isSymbolicLink()) {
@@ -890,7 +871,6 @@ function ensureArtifactWorkspaceMarker(workspace) {
890
871
  const artifactRoot = getWorkspaceArtifactRoot(workspace);
891
872
  const peaksPath = resolve(artifactRoot, '.peaks');
892
873
  const markerPath = resolve(peaksPath, 'config.json');
893
- validateArtifactWorkspaceRootBeforeCreate(artifactRoot, workspace.rootPath);
894
874
  ensureDir(artifactRoot);
895
875
  validateArtifactWorkspaceRoot(artifactRoot, workspace.rootPath);
896
876
  ensureDir(peaksPath);
@@ -907,7 +887,7 @@ export function ensureWorkspaceConfigForPath(path = process.cwd()) {
907
887
  const existingWorkspace = findWorkspaceForPath(config.workspaces, path);
908
888
  if (existingWorkspace) {
909
889
  ensureArtifactWorkspaceMarker(existingWorkspace);
910
- if (config.currentWorkspace !== existingWorkspace.workspaceId) {
890
+ if (!config.currentWorkspace) {
911
891
  writeConfig({ currentWorkspace: existingWorkspace.workspaceId }, 'user');
912
892
  }
913
893
  return existingWorkspace;
@@ -923,7 +903,7 @@ export function ensureWorkspaceConfigForPath(path = process.cwd()) {
923
903
  };
924
904
  ensureArtifactWorkspaceMarker(workspace);
925
905
  const updatedWorkspaces = [...config.workspaces, workspace];
926
- writeConfig({ workspaces: updatedWorkspaces, currentWorkspace: workspace.workspaceId }, 'user');
906
+ writeConfig({ workspaces: updatedWorkspaces, ...(!config.currentWorkspace ? { currentWorkspace: workspace.workspaceId } : {}) }, 'user');
927
907
  return workspace;
928
908
  }
929
909
  export function getWorkspaceConfigForCurrentPath() {