peaks-cli 1.0.12 → 1.0.14

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 (112) 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 +48 -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-process-runner.d.ts +2 -0
  19. package/dist/src/services/codegraph/codegraph-process-runner.js +93 -0
  20. package/dist/src/services/codegraph/codegraph-service.js +13 -128
  21. package/dist/src/services/config/config-service.js +2 -22
  22. package/dist/src/services/dashboard/project-dashboard-service.d.ts +64 -0
  23. package/dist/src/services/dashboard/project-dashboard-service.js +112 -0
  24. package/dist/src/services/doctor/doctor-service.d.ts +7 -0
  25. package/dist/src/services/doctor/doctor-service.js +139 -0
  26. package/dist/src/services/mcp/mcp-apply-service.d.ts +31 -0
  27. package/dist/src/services/mcp/mcp-apply-service.js +112 -0
  28. package/dist/src/services/mcp/mcp-call-service.d.ts +17 -0
  29. package/dist/src/services/mcp/mcp-call-service.js +34 -0
  30. package/dist/src/services/mcp/mcp-client-service.d.ts +14 -0
  31. package/dist/src/services/mcp/mcp-client-service.js +49 -0
  32. package/dist/src/services/mcp/mcp-install-registry.d.ts +11 -0
  33. package/dist/src/services/mcp/mcp-install-registry.js +38 -0
  34. package/dist/src/services/mcp/mcp-plan-service.d.ts +29 -0
  35. package/dist/src/services/mcp/mcp-plan-service.js +109 -0
  36. package/dist/src/services/mcp/mcp-protocol.d.ts +24 -0
  37. package/dist/src/services/mcp/mcp-protocol.js +41 -0
  38. package/dist/src/services/mcp/mcp-scan-service.d.ts +8 -0
  39. package/dist/src/services/mcp/mcp-scan-service.js +214 -0
  40. package/dist/src/services/mcp/mcp-stdio-transport.d.ts +10 -0
  41. package/dist/src/services/mcp/mcp-stdio-transport.js +50 -0
  42. package/dist/src/services/mcp/mcp-types.d.ts +31 -0
  43. package/dist/src/services/mcp/mcp-types.js +1 -0
  44. package/dist/src/services/openspec/openspec-archive-service.d.ts +12 -0
  45. package/dist/src/services/openspec/openspec-archive-service.js +28 -0
  46. package/dist/src/services/openspec/openspec-bridge-service.d.ts +16 -0
  47. package/dist/src/services/openspec/openspec-bridge-service.js +76 -0
  48. package/dist/src/services/openspec/openspec-render-service.d.ts +38 -0
  49. package/dist/src/services/openspec/openspec-render-service.js +130 -0
  50. package/dist/src/services/openspec/openspec-scan-service.d.ts +6 -0
  51. package/dist/src/services/openspec/openspec-scan-service.js +123 -0
  52. package/dist/src/services/openspec/openspec-types.d.ts +39 -0
  53. package/dist/src/services/openspec/openspec-types.js +1 -0
  54. package/dist/src/services/openspec/openspec-validate-service.d.ts +27 -0
  55. package/dist/src/services/openspec/openspec-validate-service.js +77 -0
  56. package/dist/src/services/recommendations/capability-seed-items.js +2 -1
  57. package/dist/src/services/recommendations/capability-seed-mappings.js +1 -1
  58. package/dist/src/services/recommendations/capability-seed-sources.js +1 -1
  59. package/dist/src/services/shadcn/shadcn-service.d.ts +4 -0
  60. package/dist/src/services/shadcn/shadcn-service.js +15 -30
  61. package/dist/src/services/skills/skill-presence-service.d.ts +10 -0
  62. package/dist/src/services/skills/skill-presence-service.js +54 -0
  63. package/dist/src/services/skills/skill-runbook-service.d.ts +11 -0
  64. package/dist/src/services/skills/skill-runbook-service.js +60 -0
  65. package/dist/src/services/standards/project-standards-service.js +4 -9
  66. package/dist/src/services/understand/understand-scan-service.d.ts +28 -0
  67. package/dist/src/services/understand/understand-scan-service.js +157 -0
  68. package/dist/src/services/understand/understand-types.d.ts +24 -0
  69. package/dist/src/services/understand/understand-types.js +1 -0
  70. package/dist/src/services/workflow/workflow-autonomous-service.js +7 -13
  71. package/dist/src/shared/json-schema-mini.d.ts +10 -0
  72. package/dist/src/shared/json-schema-mini.js +113 -0
  73. package/dist/src/shared/paths.d.ts +1 -1
  74. package/dist/src/shared/paths.js +9 -1
  75. package/dist/src/shared/version.d.ts +1 -1
  76. package/dist/src/shared/version.js +1 -1
  77. package/package.json +2 -8
  78. package/schemas/doctor-report.schema.json +34 -0
  79. package/schemas/mcp-apply-result.schema.json +46 -0
  80. package/schemas/mcp-install-plan.schema.json +71 -0
  81. package/schemas/mcp-install-spec.schema.json +29 -0
  82. package/schemas/mcp-server.schema.json +29 -0
  83. package/schemas/openspec-change-summary.schema.json +68 -0
  84. package/schemas/openspec-render-request.schema.json +61 -0
  85. package/schemas/openspec-validation-result.schema.json +36 -0
  86. package/skills/peaks-prd/SKILL.md +61 -8
  87. package/skills/peaks-prd/references/artifact-per-request.md +78 -0
  88. package/skills/peaks-prd/references/workflow.md +7 -5
  89. package/skills/peaks-qa/SKILL.md +76 -8
  90. package/skills/peaks-qa/references/artifact-contracts.md +2 -2
  91. package/skills/peaks-qa/references/artifact-per-request.md +83 -0
  92. package/skills/peaks-qa/references/openspec-validation-gate.md +55 -0
  93. package/skills/peaks-qa/references/regression-gates.md +2 -2
  94. package/skills/peaks-rd/SKILL.md +98 -9
  95. package/skills/peaks-rd/references/artifact-contracts.md +2 -2
  96. package/skills/peaks-rd/references/artifact-per-request.md +90 -0
  97. package/skills/peaks-rd/references/openspec-mcp-cli.md +65 -0
  98. package/skills/peaks-rd/references/refactor-workflow.md +2 -2
  99. package/skills/peaks-sc/SKILL.md +46 -0
  100. package/skills/peaks-sc/references/openspec-commit-boundaries.md +33 -0
  101. package/skills/peaks-solo/SKILL.md +92 -9
  102. package/skills/peaks-solo/references/artifact-contracts.md +2 -2
  103. package/skills/peaks-solo/references/browser-workflow.md +114 -0
  104. package/skills/peaks-solo/references/external-skill-invocation.md +70 -0
  105. package/skills/peaks-solo/references/openspec-mcp-workflow.md +53 -0
  106. package/skills/peaks-solo/references/refactor-mode.md +2 -2
  107. package/skills/peaks-solo/references/workflow.md +1 -1
  108. package/skills/peaks-txt/SKILL.md +44 -0
  109. package/skills/peaks-ui/SKILL.md +59 -33
  110. package/skills/peaks-ui/references/artifact-per-request.md +71 -0
  111. package/skills/peaks-ui/references/workflow.md +8 -11
  112. 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
+ }
@@ -0,0 +1,2 @@
1
+ import type { CodegraphExecutionResult, CodegraphInvocation } from './codegraph-service.js';
2
+ export declare function defaultCodegraphProcessRunner(invocation: CodegraphInvocation): Promise<CodegraphExecutionResult>;
@@ -0,0 +1,93 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { join } from 'node:path';
3
+ const CODEGRAPH_PROCESS_TIMEOUT_MS = 600_000;
4
+ const CODEGRAPH_OUTPUT_LIMIT_BYTES = 10 * 1024 * 1024;
5
+ function createCodegraphEnvironment(sourceEnv = process.env) {
6
+ const preservedKeys = ['PATH', 'Path', 'HOME', 'USERPROFILE', 'APPDATA', 'LOCALAPPDATA', 'TEMP', 'TMP', 'SystemRoot', 'WINDIR'];
7
+ const environment = {};
8
+ for (const key of preservedKeys) {
9
+ const value = sourceEnv[key];
10
+ if (value !== undefined) {
11
+ environment[key] = value;
12
+ }
13
+ }
14
+ return environment;
15
+ }
16
+ function assertOutputLimit(currentSize, chunkSize) {
17
+ const nextSize = currentSize + chunkSize;
18
+ if (nextSize > CODEGRAPH_OUTPUT_LIMIT_BYTES) {
19
+ throw new Error(`codegraph output exceeded ${CODEGRAPH_OUTPUT_LIMIT_BYTES} bytes`);
20
+ }
21
+ return nextSize;
22
+ }
23
+ function terminateCodegraphProcess(childProcess) {
24
+ if (childProcess.pid === undefined) {
25
+ childProcess.kill();
26
+ return;
27
+ }
28
+ if (process.platform === 'win32') {
29
+ if (process.env.SystemRoot) {
30
+ spawn(join(process.env.SystemRoot, 'System32', 'taskkill.exe'), ['/pid', String(childProcess.pid), '/T', '/F'], { shell: false, stdio: 'ignore' });
31
+ }
32
+ else {
33
+ childProcess.kill();
34
+ }
35
+ return;
36
+ }
37
+ try {
38
+ process.kill(-childProcess.pid, 'SIGTERM');
39
+ }
40
+ catch {
41
+ childProcess.kill('SIGTERM');
42
+ }
43
+ }
44
+ export function defaultCodegraphProcessRunner(invocation) {
45
+ return new Promise((resolveResult, reject) => {
46
+ const childProcess = spawn(invocation.executable, invocation.args, {
47
+ cwd: invocation.cwd,
48
+ detached: process.platform !== 'win32',
49
+ env: createCodegraphEnvironment(),
50
+ shell: false
51
+ });
52
+ const timeout = setTimeout(() => {
53
+ terminateCodegraphProcess(childProcess);
54
+ reject(new Error(`codegraph process timed out after ${CODEGRAPH_PROCESS_TIMEOUT_MS}ms`));
55
+ }, CODEGRAPH_PROCESS_TIMEOUT_MS);
56
+ const stdoutChunks = [];
57
+ const stderrChunks = [];
58
+ let stdoutSize = 0;
59
+ let stderrSize = 0;
60
+ childProcess.stdout.on('data', (chunk) => {
61
+ try {
62
+ stdoutSize = assertOutputLimit(stdoutSize, chunk.length);
63
+ stdoutChunks.push(chunk);
64
+ }
65
+ catch (error) {
66
+ terminateCodegraphProcess(childProcess);
67
+ reject(error);
68
+ }
69
+ });
70
+ childProcess.stderr.on('data', (chunk) => {
71
+ try {
72
+ stderrSize = assertOutputLimit(stderrSize, chunk.length);
73
+ stderrChunks.push(chunk);
74
+ }
75
+ catch (error) {
76
+ terminateCodegraphProcess(childProcess);
77
+ reject(error);
78
+ }
79
+ });
80
+ childProcess.on('error', (error) => {
81
+ clearTimeout(timeout);
82
+ reject(error);
83
+ });
84
+ childProcess.on('close', (exitCode) => {
85
+ clearTimeout(timeout);
86
+ resolveResult({
87
+ exitCode,
88
+ stdout: Buffer.concat(stdoutChunks).toString('utf8'),
89
+ stderr: Buffer.concat(stderrChunks).toString('utf8')
90
+ });
91
+ });
92
+ });
93
+ }