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.
- package/bin/peaks.js +0 -0
- package/dist/src/cli/commands/core-artifact-commands.js +23 -0
- package/dist/src/cli/commands/mcp-commands.d.ts +3 -0
- package/dist/src/cli/commands/mcp-commands.js +144 -0
- package/dist/src/cli/commands/openspec-commands.d.ts +3 -0
- package/dist/src/cli/commands/openspec-commands.js +169 -0
- package/dist/src/cli/commands/project-commands.d.ts +3 -0
- package/dist/src/cli/commands/project-commands.js +37 -0
- package/dist/src/cli/commands/request-commands.d.ts +3 -0
- package/dist/src/cli/commands/request-commands.js +140 -0
- package/dist/src/cli/commands/understand-commands.d.ts +3 -0
- package/dist/src/cli/commands/understand-commands.js +78 -0
- package/dist/src/cli/program.js +10 -0
- package/dist/src/services/artifacts/request-artifact-service.d.ts +58 -0
- package/dist/src/services/artifacts/request-artifact-service.js +432 -0
- package/dist/src/services/dashboard/project-dashboard-service.d.ts +64 -0
- package/dist/src/services/dashboard/project-dashboard-service.js +112 -0
- package/dist/src/services/doctor/doctor-service.d.ts +7 -0
- package/dist/src/services/doctor/doctor-service.js +139 -0
- package/dist/src/services/mcp/mcp-apply-service.d.ts +31 -0
- package/dist/src/services/mcp/mcp-apply-service.js +112 -0
- package/dist/src/services/mcp/mcp-call-service.d.ts +17 -0
- package/dist/src/services/mcp/mcp-call-service.js +34 -0
- package/dist/src/services/mcp/mcp-client-service.d.ts +14 -0
- package/dist/src/services/mcp/mcp-client-service.js +49 -0
- package/dist/src/services/mcp/mcp-install-registry.d.ts +11 -0
- package/dist/src/services/mcp/mcp-install-registry.js +38 -0
- package/dist/src/services/mcp/mcp-plan-service.d.ts +29 -0
- package/dist/src/services/mcp/mcp-plan-service.js +109 -0
- package/dist/src/services/mcp/mcp-protocol.d.ts +24 -0
- package/dist/src/services/mcp/mcp-protocol.js +41 -0
- package/dist/src/services/mcp/mcp-scan-service.d.ts +8 -0
- package/dist/src/services/mcp/mcp-scan-service.js +214 -0
- package/dist/src/services/mcp/mcp-stdio-transport.d.ts +10 -0
- package/dist/src/services/mcp/mcp-stdio-transport.js +50 -0
- package/dist/src/services/mcp/mcp-types.d.ts +31 -0
- package/dist/src/services/mcp/mcp-types.js +1 -0
- package/dist/src/services/openspec/openspec-archive-service.d.ts +12 -0
- package/dist/src/services/openspec/openspec-archive-service.js +28 -0
- package/dist/src/services/openspec/openspec-bridge-service.d.ts +16 -0
- package/dist/src/services/openspec/openspec-bridge-service.js +76 -0
- package/dist/src/services/openspec/openspec-render-service.d.ts +38 -0
- package/dist/src/services/openspec/openspec-render-service.js +130 -0
- package/dist/src/services/openspec/openspec-scan-service.d.ts +6 -0
- package/dist/src/services/openspec/openspec-scan-service.js +123 -0
- package/dist/src/services/openspec/openspec-types.d.ts +39 -0
- package/dist/src/services/openspec/openspec-types.js +1 -0
- package/dist/src/services/openspec/openspec-validate-service.d.ts +27 -0
- package/dist/src/services/openspec/openspec-validate-service.js +77 -0
- package/dist/src/services/recommendations/capability-seed-items.js +1 -0
- package/dist/src/services/skills/skill-runbook-service.d.ts +11 -0
- package/dist/src/services/skills/skill-runbook-service.js +60 -0
- package/dist/src/services/standards/project-standards-service.js +4 -9
- package/dist/src/services/understand/understand-scan-service.d.ts +28 -0
- package/dist/src/services/understand/understand-scan-service.js +157 -0
- package/dist/src/services/understand/understand-types.d.ts +24 -0
- package/dist/src/services/understand/understand-types.js +1 -0
- package/dist/src/shared/json-schema-mini.d.ts +10 -0
- package/dist/src/shared/json-schema-mini.js +113 -0
- package/dist/src/shared/paths.d.ts +1 -1
- package/dist/src/shared/paths.js +9 -1
- package/dist/src/shared/version.d.ts +1 -1
- package/dist/src/shared/version.js +1 -1
- package/package.json +1 -6
- package/schemas/doctor-report.schema.json +34 -0
- package/schemas/mcp-apply-result.schema.json +46 -0
- package/schemas/mcp-install-plan.schema.json +71 -0
- package/schemas/mcp-install-spec.schema.json +29 -0
- package/schemas/mcp-server.schema.json +29 -0
- package/schemas/openspec-change-summary.schema.json +68 -0
- package/schemas/openspec-render-request.schema.json +61 -0
- package/schemas/openspec-validation-result.schema.json +36 -0
- package/skills/peaks-prd/SKILL.md +59 -8
- package/skills/peaks-prd/references/artifact-per-request.md +78 -0
- package/skills/peaks-prd/references/workflow.md +7 -5
- package/skills/peaks-qa/SKILL.md +73 -7
- package/skills/peaks-qa/references/artifact-contracts.md +1 -1
- package/skills/peaks-qa/references/artifact-per-request.md +83 -0
- package/skills/peaks-qa/references/openspec-validation-gate.md +55 -0
- package/skills/peaks-qa/references/regression-gates.md +1 -1
- package/skills/peaks-rd/SKILL.md +94 -7
- package/skills/peaks-rd/references/artifact-per-request.md +90 -0
- package/skills/peaks-rd/references/openspec-mcp-cli.md +65 -0
- package/skills/peaks-sc/SKILL.md +44 -0
- package/skills/peaks-sc/references/openspec-commit-boundaries.md +33 -0
- package/skills/peaks-solo/SKILL.md +87 -4
- package/skills/peaks-solo/references/browser-workflow.md +114 -0
- package/skills/peaks-solo/references/external-skill-invocation.md +70 -0
- package/skills/peaks-solo/references/openspec-mcp-workflow.md +53 -0
- package/skills/peaks-solo/references/workflow.md +1 -1
- package/skills/peaks-txt/SKILL.md +42 -0
- package/skills/peaks-ui/SKILL.md +57 -33
- package/skills/peaks-ui/references/artifact-per-request.md +71 -0
- package/skills/peaks-ui/references/workflow.md +8 -11
package/dist/src/cli/program.js
CHANGED
|
@@ -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
|
+
}
|