peaks-cli 1.0.11 → 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 (94) hide show
  1. package/bin/peaks.js +0 -0
  2. package/dist/src/cli/commands/core-artifact-commands.js +23 -0
  3. package/dist/src/cli/commands/mcp-commands.d.ts +3 -0
  4. package/dist/src/cli/commands/mcp-commands.js +144 -0
  5. package/dist/src/cli/commands/openspec-commands.d.ts +3 -0
  6. package/dist/src/cli/commands/openspec-commands.js +169 -0
  7. package/dist/src/cli/commands/project-commands.d.ts +3 -0
  8. package/dist/src/cli/commands/project-commands.js +37 -0
  9. package/dist/src/cli/commands/request-commands.d.ts +3 -0
  10. package/dist/src/cli/commands/request-commands.js +140 -0
  11. package/dist/src/cli/commands/understand-commands.d.ts +3 -0
  12. package/dist/src/cli/commands/understand-commands.js +78 -0
  13. package/dist/src/cli/program.js +10 -0
  14. package/dist/src/services/artifacts/request-artifact-service.d.ts +58 -0
  15. package/dist/src/services/artifacts/request-artifact-service.js +432 -0
  16. package/dist/src/services/dashboard/project-dashboard-service.d.ts +64 -0
  17. package/dist/src/services/dashboard/project-dashboard-service.js +112 -0
  18. package/dist/src/services/doctor/doctor-service.d.ts +7 -0
  19. package/dist/src/services/doctor/doctor-service.js +139 -0
  20. package/dist/src/services/mcp/mcp-apply-service.d.ts +31 -0
  21. package/dist/src/services/mcp/mcp-apply-service.js +112 -0
  22. package/dist/src/services/mcp/mcp-call-service.d.ts +17 -0
  23. package/dist/src/services/mcp/mcp-call-service.js +34 -0
  24. package/dist/src/services/mcp/mcp-client-service.d.ts +14 -0
  25. package/dist/src/services/mcp/mcp-client-service.js +49 -0
  26. package/dist/src/services/mcp/mcp-install-registry.d.ts +11 -0
  27. package/dist/src/services/mcp/mcp-install-registry.js +38 -0
  28. package/dist/src/services/mcp/mcp-plan-service.d.ts +29 -0
  29. package/dist/src/services/mcp/mcp-plan-service.js +109 -0
  30. package/dist/src/services/mcp/mcp-protocol.d.ts +24 -0
  31. package/dist/src/services/mcp/mcp-protocol.js +41 -0
  32. package/dist/src/services/mcp/mcp-scan-service.d.ts +8 -0
  33. package/dist/src/services/mcp/mcp-scan-service.js +214 -0
  34. package/dist/src/services/mcp/mcp-stdio-transport.d.ts +10 -0
  35. package/dist/src/services/mcp/mcp-stdio-transport.js +50 -0
  36. package/dist/src/services/mcp/mcp-types.d.ts +31 -0
  37. package/dist/src/services/mcp/mcp-types.js +1 -0
  38. package/dist/src/services/openspec/openspec-archive-service.d.ts +12 -0
  39. package/dist/src/services/openspec/openspec-archive-service.js +28 -0
  40. package/dist/src/services/openspec/openspec-bridge-service.d.ts +16 -0
  41. package/dist/src/services/openspec/openspec-bridge-service.js +76 -0
  42. package/dist/src/services/openspec/openspec-render-service.d.ts +38 -0
  43. package/dist/src/services/openspec/openspec-render-service.js +130 -0
  44. package/dist/src/services/openspec/openspec-scan-service.d.ts +6 -0
  45. package/dist/src/services/openspec/openspec-scan-service.js +123 -0
  46. package/dist/src/services/openspec/openspec-types.d.ts +39 -0
  47. package/dist/src/services/openspec/openspec-types.js +1 -0
  48. package/dist/src/services/openspec/openspec-validate-service.d.ts +27 -0
  49. package/dist/src/services/openspec/openspec-validate-service.js +77 -0
  50. package/dist/src/services/recommendations/capability-seed-items.js +1 -0
  51. package/dist/src/services/skills/skill-runbook-service.d.ts +11 -0
  52. package/dist/src/services/skills/skill-runbook-service.js +60 -0
  53. package/dist/src/services/standards/project-standards-service.js +4 -9
  54. package/dist/src/services/understand/understand-scan-service.d.ts +28 -0
  55. package/dist/src/services/understand/understand-scan-service.js +157 -0
  56. package/dist/src/services/understand/understand-types.d.ts +24 -0
  57. package/dist/src/services/understand/understand-types.js +1 -0
  58. package/dist/src/shared/json-schema-mini.d.ts +10 -0
  59. package/dist/src/shared/json-schema-mini.js +113 -0
  60. package/dist/src/shared/paths.d.ts +1 -1
  61. package/dist/src/shared/paths.js +9 -1
  62. package/dist/src/shared/version.d.ts +1 -1
  63. package/dist/src/shared/version.js +1 -1
  64. package/package.json +1 -6
  65. package/schemas/doctor-report.schema.json +34 -0
  66. package/schemas/mcp-apply-result.schema.json +46 -0
  67. package/schemas/mcp-install-plan.schema.json +71 -0
  68. package/schemas/mcp-install-spec.schema.json +29 -0
  69. package/schemas/mcp-server.schema.json +29 -0
  70. package/schemas/openspec-change-summary.schema.json +68 -0
  71. package/schemas/openspec-render-request.schema.json +61 -0
  72. package/schemas/openspec-validation-result.schema.json +36 -0
  73. package/skills/peaks-prd/SKILL.md +59 -8
  74. package/skills/peaks-prd/references/artifact-per-request.md +78 -0
  75. package/skills/peaks-prd/references/workflow.md +7 -5
  76. package/skills/peaks-qa/SKILL.md +73 -7
  77. package/skills/peaks-qa/references/artifact-contracts.md +1 -1
  78. package/skills/peaks-qa/references/artifact-per-request.md +83 -0
  79. package/skills/peaks-qa/references/openspec-validation-gate.md +55 -0
  80. package/skills/peaks-qa/references/regression-gates.md +1 -1
  81. package/skills/peaks-rd/SKILL.md +94 -7
  82. package/skills/peaks-rd/references/artifact-per-request.md +90 -0
  83. package/skills/peaks-rd/references/openspec-mcp-cli.md +65 -0
  84. package/skills/peaks-sc/SKILL.md +44 -0
  85. package/skills/peaks-sc/references/openspec-commit-boundaries.md +33 -0
  86. package/skills/peaks-solo/SKILL.md +87 -4
  87. package/skills/peaks-solo/references/browser-workflow.md +114 -0
  88. package/skills/peaks-solo/references/external-skill-invocation.md +70 -0
  89. package/skills/peaks-solo/references/openspec-mcp-workflow.md +53 -0
  90. package/skills/peaks-solo/references/workflow.md +1 -1
  91. package/skills/peaks-txt/SKILL.md +42 -0
  92. package/skills/peaks-ui/SKILL.md +57 -33
  93. package/skills/peaks-ui/references/artifact-per-request.md +71 -0
  94. package/skills/peaks-ui/references/workflow.md +8 -11
@@ -4,7 +4,12 @@ import { registerCoreAndArtifactCommands } from './commands/core-artifact-comman
4
4
  import { registerWorkflowCommands } from './commands/workflow-commands.js';
5
5
  import { registerCapabilityWorkerConfigAndSCCommands } from './commands/capability-worker-config-sc-commands.js';
6
6
  import { registerCodegraphCommands } from './commands/codegraph-commands.js';
7
+ import { registerMcpCommands } from './commands/mcp-commands.js';
8
+ import { registerOpenSpecCommands } from './commands/openspec-commands.js';
9
+ import { registerProjectCommands } from './commands/project-commands.js';
10
+ import { registerRequestCommands } from './commands/request-commands.js';
7
11
  import { registerShadcnCommands } from './commands/shadcn-commands.js';
12
+ import { registerUnderstandCommands } from './commands/understand-commands.js';
8
13
  export { printResult } from './cli-helpers.js';
9
14
  export function createProgram(io = { stdout: (text) => console.log(text), stderr: (text) => console.error(text) }) {
10
15
  const program = new Command();
@@ -27,6 +32,11 @@ export function createProgram(io = { stdout: (text) => console.log(text), stderr
27
32
  registerWorkflowCommands(program, io);
28
33
  registerCapabilityWorkerConfigAndSCCommands(program, io);
29
34
  registerCodegraphCommands(program, io);
35
+ registerMcpCommands(program, io);
36
+ registerOpenSpecCommands(program, io);
37
+ registerProjectCommands(program, io);
38
+ registerRequestCommands(program, io);
30
39
  registerShadcnCommands(program, io);
40
+ registerUnderstandCommands(program, io);
31
41
  return program;
32
42
  }
@@ -0,0 +1,58 @@
1
+ export type RequestArtifactRole = 'prd' | 'ui' | 'rd' | 'qa';
2
+ export type CreateRequestArtifactOptions = {
3
+ role: RequestArtifactRole;
4
+ requestId: string;
5
+ projectRoot: string;
6
+ sessionId?: string;
7
+ apply?: boolean;
8
+ clock?: () => string;
9
+ };
10
+ export type CreateRequestArtifactResult = {
11
+ role: RequestArtifactRole;
12
+ requestId: string;
13
+ sessionId: string;
14
+ path: string;
15
+ content: string;
16
+ applied: boolean;
17
+ };
18
+ export declare function createRequestArtifact(options: CreateRequestArtifactOptions): Promise<CreateRequestArtifactResult>;
19
+ export type RequestArtifactSummary = {
20
+ role: RequestArtifactRole;
21
+ sessionId: string;
22
+ requestId: string;
23
+ path: string;
24
+ state: string;
25
+ createdAt?: string;
26
+ };
27
+ export type ListRequestArtifactsOptions = {
28
+ projectRoot: string;
29
+ sessionId?: string;
30
+ role?: RequestArtifactRole;
31
+ };
32
+ export type ShowRequestArtifactOptions = {
33
+ projectRoot: string;
34
+ role: RequestArtifactRole;
35
+ requestId: string;
36
+ sessionId?: string;
37
+ };
38
+ export type ShowRequestArtifactResult = RequestArtifactSummary & {
39
+ content: string;
40
+ };
41
+ export declare function listRequestArtifacts(options: ListRequestArtifactsOptions): Promise<RequestArtifactSummary[]>;
42
+ export declare function showRequestArtifact(options: ShowRequestArtifactOptions): Promise<ShowRequestArtifactResult | null>;
43
+ export type RequestArtifactState = 'draft' | 'confirmed-by-user' | 'direction-locked' | 'spec-locked' | 'implemented' | 'qa-handoff' | 'running' | 'verdict-issued' | 'handed-off' | 'blocked';
44
+ export declare function allowedStatesForRole(role: RequestArtifactRole): ReadonlyArray<RequestArtifactState>;
45
+ export type TransitionRequestArtifactOptions = {
46
+ role: RequestArtifactRole;
47
+ requestId: string;
48
+ projectRoot: string;
49
+ newState: RequestArtifactState;
50
+ sessionId?: string;
51
+ reason?: string;
52
+ clock?: () => string;
53
+ };
54
+ export type TransitionRequestArtifactResult = RequestArtifactSummary & {
55
+ previousState: string;
56
+ content: string;
57
+ };
58
+ export declare function transitionRequestArtifact(options: TransitionRequestArtifactOptions): Promise<TransitionRequestArtifactResult | null>;
@@ -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,64 @@
1
+ import { type RequestArtifactRole, type RequestArtifactSummary } from '../artifacts/request-artifact-service.js';
2
+ import type { OpenSpecChangeSummary } from '../openspec/openspec-types.js';
3
+ import type { McpScanReport } from '../mcp/mcp-types.js';
4
+ import type { CapabilityItem } from '../recommendations/recommendation-types.js';
5
+ export type ProjectDashboardRequests = {
6
+ count: number;
7
+ byRole: Record<RequestArtifactRole, RequestArtifactSummary[]>;
8
+ byState: Record<string, number>;
9
+ };
10
+ export type ProjectDashboardOpenSpec = {
11
+ exists: boolean;
12
+ count: number;
13
+ changes: OpenSpecChangeSummary[];
14
+ };
15
+ export type ProjectDashboardUnderstand = {
16
+ exists: boolean;
17
+ graphExists: boolean;
18
+ graphPath: string;
19
+ parseError?: string;
20
+ };
21
+ export type ProjectDashboardMcp = {
22
+ servers: McpScanReport['servers'];
23
+ scopes: McpScanReport['scopes'];
24
+ };
25
+ export type ProjectDashboardDoctor = {
26
+ ok: boolean;
27
+ passed: number;
28
+ failed: number;
29
+ };
30
+ export type ProjectDashboardRunbookHealth = {
31
+ ok: boolean;
32
+ required: number;
33
+ healthy: number;
34
+ missingRunbook: string[];
35
+ applyNoteFailed: string[];
36
+ };
37
+ export type ProjectDashboardCapabilities = {
38
+ count: number;
39
+ mcpCount: number;
40
+ sample: Array<Pick<CapabilityItem, 'capabilityId' | 'name' | 'itemType' | 'category'>>;
41
+ };
42
+ export type ProjectDashboard = {
43
+ generatedAt: string;
44
+ projectRoot: string;
45
+ requests: ProjectDashboardRequests;
46
+ openspec: ProjectDashboardOpenSpec;
47
+ understand: ProjectDashboardUnderstand;
48
+ mcp: ProjectDashboardMcp;
49
+ doctor: ProjectDashboardDoctor;
50
+ runbookHealth: ProjectDashboardRunbookHealth;
51
+ capabilities: ProjectDashboardCapabilities;
52
+ };
53
+ export type LoadProjectDashboardOptions = {
54
+ projectRoot: string;
55
+ sampleCapabilities?: number;
56
+ clock?: () => string;
57
+ doctorReport?: {
58
+ ok: boolean;
59
+ passed: number;
60
+ failed: number;
61
+ };
62
+ runbookHealth?: ProjectDashboardRunbookHealth;
63
+ };
64
+ export declare function loadProjectDashboard(options: LoadProjectDashboardOptions): Promise<ProjectDashboard>;
@@ -0,0 +1,112 @@
1
+ import { listRequestArtifacts } from '../artifacts/request-artifact-service.js';
2
+ import { scanOpenSpec } from '../openspec/openspec-scan-service.js';
3
+ import { scanMcpServers } from '../mcp/mcp-scan-service.js';
4
+ import { scanUnderstandAnything } from '../understand/understand-scan-service.js';
5
+ import { seedCapabilityItems } from '../recommendations/capability-seed-items.js';
6
+ import { requiredSkillNames } from '../../shared/paths.js';
7
+ function defaultClock() {
8
+ return new Date().toISOString();
9
+ }
10
+ function groupRequestsByRole(items) {
11
+ const byRole = { prd: [], ui: [], rd: [], qa: [] };
12
+ for (const item of items) {
13
+ byRole[item.role].push(item);
14
+ }
15
+ return byRole;
16
+ }
17
+ function countRequestsByState(items) {
18
+ const counts = {};
19
+ for (const item of items) {
20
+ counts[item.state] = (counts[item.state] ?? 0) + 1;
21
+ }
22
+ return counts;
23
+ }
24
+ async function loadDoctorAndRunbookHealth(doctorOverride, runbookOverride) {
25
+ if (doctorOverride !== undefined && runbookOverride !== undefined) {
26
+ return { doctor: doctorOverride, runbookHealth: runbookOverride };
27
+ }
28
+ if (doctorOverride !== undefined) {
29
+ return {
30
+ doctor: doctorOverride,
31
+ runbookHealth: { ok: true, required: 0, healthy: 0, missingRunbook: [], applyNoteFailed: [] }
32
+ };
33
+ }
34
+ const { runDoctor } = await import('../doctor/doctor-service.js');
35
+ const report = await runDoctor();
36
+ return {
37
+ doctor: { ok: report.summary.ok, passed: report.summary.passed, failed: report.summary.failed },
38
+ runbookHealth: runbookOverride ?? summarizeRunbookHealth(report.checks)
39
+ };
40
+ }
41
+ function summarizeRunbookHealth(checks) {
42
+ const missingRunbook = [];
43
+ const applyNoteFailed = [];
44
+ for (const check of checks) {
45
+ if (!check.ok && check.id.startsWith('skill-runbook:')) {
46
+ missingRunbook.push(check.id.slice('skill-runbook:'.length));
47
+ }
48
+ if (!check.ok && check.id.startsWith('skill-apply-note:')) {
49
+ applyNoteFailed.push(check.id.slice('skill-apply-note:'.length));
50
+ }
51
+ }
52
+ const required = requiredSkillNames.length;
53
+ const healthy = Math.max(0, required - missingRunbook.length - applyNoteFailed.length);
54
+ return {
55
+ ok: missingRunbook.length === 0 && applyNoteFailed.length === 0,
56
+ required,
57
+ healthy,
58
+ missingRunbook,
59
+ applyNoteFailed
60
+ };
61
+ }
62
+ function buildCapabilitiesSummary(sampleSize) {
63
+ const items = seedCapabilityItems;
64
+ return {
65
+ count: items.length,
66
+ mcpCount: items.filter((item) => item.itemType === 'mcp').length,
67
+ sample: items.slice(0, sampleSize).map((item) => ({
68
+ capabilityId: item.capabilityId,
69
+ name: item.name,
70
+ itemType: item.itemType,
71
+ category: item.category
72
+ }))
73
+ };
74
+ }
75
+ export async function loadProjectDashboard(options) {
76
+ const clock = options.clock ?? defaultClock;
77
+ const sampleSize = options.sampleCapabilities ?? 8;
78
+ const [items, openspecReport, mcpReport, understandReport, doctorAndRunbook] = await Promise.all([
79
+ listRequestArtifacts({ projectRoot: options.projectRoot }),
80
+ scanOpenSpec({ openspecRoot: `${options.projectRoot}/openspec` }),
81
+ scanMcpServers({ projectRoot: options.projectRoot }),
82
+ scanUnderstandAnything({ projectRoot: options.projectRoot }),
83
+ loadDoctorAndRunbookHealth(options.doctorReport, options.runbookHealth)
84
+ ]);
85
+ return {
86
+ generatedAt: clock(),
87
+ projectRoot: options.projectRoot,
88
+ requests: {
89
+ count: items.length,
90
+ byRole: groupRequestsByRole(items),
91
+ byState: countRequestsByState(items)
92
+ },
93
+ openspec: {
94
+ exists: openspecReport.exists,
95
+ count: openspecReport.changes.length,
96
+ changes: openspecReport.changes
97
+ },
98
+ understand: {
99
+ exists: understandReport.exists,
100
+ graphExists: understandReport.graph.exists,
101
+ graphPath: understandReport.graph.path,
102
+ ...(understandReport.graph.parseError !== undefined ? { parseError: understandReport.graph.parseError } : {})
103
+ },
104
+ mcp: {
105
+ servers: mcpReport.servers,
106
+ scopes: mcpReport.scopes
107
+ },
108
+ doctor: doctorAndRunbook.doctor,
109
+ runbookHealth: doctorAndRunbook.runbookHealth,
110
+ capabilities: buildCapabilitiesSummary(sampleSize)
111
+ };
112
+ }