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
@@ -1,15 +1,14 @@
1
1
  import { existsSync, realpathSync, statSync } from 'node:fs';
2
- import { spawn } from 'node:child_process';
3
2
  import { createRequire } from 'node:module';
4
- import { dirname, isAbsolute, relative, resolve, sep, win32 } from 'node:path';
3
+ import { dirname, isAbsolute, relative, resolve, sep } from 'node:path';
4
+ import { defaultCodegraphProcessRunner } from './codegraph-process-runner.js';
5
5
  const CODEGRAPH_PACKAGE_NAME = '@colbymchenry/codegraph';
6
6
  const CODEGRAPH_PACKAGE_VERSION = '0.7.10';
7
7
  const CODEGRAPH_EXECUTABLE = process.execPath;
8
8
  const CODEGRAPH_BINARY_PATH = resolveCodegraphBinaryPath();
9
- const CODEGRAPH_PROCESS_TIMEOUT_MS = 600_000;
10
- const CODEGRAPH_OUTPUT_LIMIT_BYTES = 10 * 1024 * 1024;
11
9
  const POSITIONAL_ARGUMENT_PREFIX = '-';
12
10
  const ALLOWED_SUBCOMMANDS = ['status', 'init', 'index', 'query', 'files', 'context', 'affected'];
11
+ const NUMERIC_FLAG_NAMES = ['limit', 'maxDepth'];
13
12
  const COMMON_OPTION_KEYS = ['subcommand', 'project'];
14
13
  const ALLOWED_OPTIONS_BY_SUBCOMMAND = {
15
14
  status: [],
@@ -20,16 +19,10 @@ const ALLOWED_OPTIONS_BY_SUBCOMMAND = {
20
19
  context: ['task'],
21
20
  affected: ['files', 'json']
22
21
  };
23
- function assertCodegraphBinaryExists(binaryPath) {
24
- if (!existsSync(binaryPath)) {
25
- throw new Error('Unable to resolve local codegraph binary from @colbymchenry/codegraph');
26
- }
27
- }
28
22
  function resolveCodegraphBinaryPath() {
29
23
  const require = createRequire(import.meta.url);
30
24
  const packageJsonPath = require.resolve('@colbymchenry/codegraph/package.json');
31
25
  const binaryPath = resolve(dirname(packageJsonPath), 'dist', 'bin', 'codegraph.js');
32
- assertCodegraphBinaryExists(binaryPath);
33
26
  return binaryPath;
34
27
  }
35
28
  function assertSupportedSubcommand(subcommand) {
@@ -37,15 +30,12 @@ function assertSupportedSubcommand(subcommand) {
37
30
  throw new Error(`Unsupported codegraph subcommand: ${subcommand}`);
38
31
  }
39
32
  }
40
- function assertProjectRootDirectory(projectRoot) {
41
- if (!statSync(projectRoot).isDirectory()) {
42
- throw new Error('Project path must exist and be a directory');
43
- }
44
- }
45
33
  function resolveProjectRoot(project) {
46
34
  const projectRoot = resolve(project);
47
35
  try {
48
- assertProjectRootDirectory(projectRoot);
36
+ if (!statSync(projectRoot).isDirectory()) {
37
+ throw new Error('Project path must exist and be a directory');
38
+ }
49
39
  return realpathSync.native(projectRoot);
50
40
  }
51
41
  catch {
@@ -90,19 +80,17 @@ function assertInsideProject(projectRoot, absolutePath) {
90
80
  throw new Error('Affected files must stay inside the project');
91
81
  }
92
82
  }
93
- function climbToExistingBoundary(currentPath, pathExists = existsSync) {
94
- while (!pathExists(currentPath)) {
83
+ function resolveExistingBoundary(absoluteFilePath) {
84
+ if (existsSync(absoluteFilePath)) {
85
+ return absoluteFilePath;
86
+ }
87
+ let currentPath = dirname(absoluteFilePath);
88
+ while (!existsSync(currentPath)) {
95
89
  const parentPath = dirname(currentPath);
96
- if (parentPath === currentPath) {
97
- return currentPath;
98
- }
99
90
  currentPath = parentPath;
100
91
  }
101
92
  return currentPath;
102
93
  }
103
- function resolveExistingBoundary(absoluteFilePath) {
104
- return existsSync(absoluteFilePath) ? absoluteFilePath : climbToExistingBoundary(dirname(absoluteFilePath));
105
- }
106
94
  function normalizeProjectRelativeFile(projectRoot, file) {
107
95
  assertPositionalArgument(file, 'Affected files');
108
96
  const absoluteFilePath = resolve(projectRoot, file);
@@ -111,13 +99,10 @@ function normalizeProjectRelativeFile(projectRoot, file) {
111
99
  assertInsideProject(projectRoot, realBoundary);
112
100
  return relative(projectRoot, absoluteFilePath).split(sep).join('/');
113
101
  }
114
- function assertAffectedFiles(files) {
102
+ function buildAffectedFileArgs(projectRoot, files) {
115
103
  if (!files || files.length < 1) {
116
104
  throw new Error('affected requires at least one file');
117
105
  }
118
- }
119
- function buildAffectedFileArgs(projectRoot, files) {
120
- assertAffectedFiles(files);
121
106
  return files.map((file) => normalizeProjectRelativeFile(projectRoot, file));
122
107
  }
123
108
  function buildCommandArgs(options, projectRoot) {
@@ -154,106 +139,6 @@ function buildCommandArgs(options, projectRoot) {
154
139
  }
155
140
  return args;
156
141
  }
157
- function createCodegraphEnvironment(sourceEnv = process.env) {
158
- const preservedKeys = ['PATH', 'Path', 'HOME', 'USERPROFILE', 'APPDATA', 'LOCALAPPDATA', 'TEMP', 'TMP', 'SystemRoot', 'WINDIR'];
159
- const environment = {};
160
- for (const key of preservedKeys) {
161
- const value = sourceEnv[key];
162
- if (value !== undefined) {
163
- environment[key] = value;
164
- }
165
- }
166
- return environment;
167
- }
168
- function assertOutputLimit(currentSize, chunkSize) {
169
- const nextSize = currentSize + chunkSize;
170
- if (nextSize > CODEGRAPH_OUTPUT_LIMIT_BYTES) {
171
- throw new Error(`codegraph output exceeded ${CODEGRAPH_OUTPUT_LIMIT_BYTES} bytes`);
172
- }
173
- return nextSize;
174
- }
175
- function getWindowsTaskkillPath(fileExists = existsSync) {
176
- const candidates = [
177
- win32.join('C:\\Windows', 'System32', 'taskkill.exe'),
178
- win32.join('C:\\WINNT', 'System32', 'taskkill.exe')
179
- ];
180
- return candidates.find((candidate) => fileExists(candidate)) ?? null;
181
- }
182
- function terminateCodegraphProcess(childProcess, platform = process.platform, killProcess = process.kill, spawnProcess = spawn, taskkillPath = getWindowsTaskkillPath()) {
183
- if (childProcess.pid === undefined) {
184
- childProcess.kill();
185
- return;
186
- }
187
- if (platform === 'win32') {
188
- if (!taskkillPath) {
189
- childProcess.kill();
190
- return;
191
- }
192
- const killerProcess = spawnProcess(taskkillPath, ['/pid', String(childProcess.pid), '/T', '/F'], { shell: false, stdio: 'ignore' });
193
- killerProcess.on('error', () => childProcess.kill());
194
- killerProcess.unref();
195
- return;
196
- }
197
- try {
198
- killProcess(-childProcess.pid, 'SIGTERM');
199
- }
200
- catch {
201
- childProcess.kill('SIGTERM');
202
- }
203
- }
204
- function runCodegraphProcess(invocation, timeoutMs = CODEGRAPH_PROCESS_TIMEOUT_MS) {
205
- return new Promise((resolveResult, reject) => {
206
- const childProcess = spawn(invocation.executable, invocation.args, {
207
- cwd: invocation.cwd,
208
- detached: process.platform !== 'win32',
209
- env: createCodegraphEnvironment(),
210
- shell: false
211
- });
212
- const timeout = setTimeout(() => {
213
- terminateCodegraphProcess(childProcess);
214
- reject(new Error(`codegraph process timed out after ${timeoutMs}ms`));
215
- }, timeoutMs);
216
- const stdoutChunks = [];
217
- const stderrChunks = [];
218
- let stdoutSize = 0;
219
- let stderrSize = 0;
220
- childProcess.stdout.on('data', (chunk) => {
221
- try {
222
- stdoutSize = assertOutputLimit(stdoutSize, chunk.length);
223
- stdoutChunks.push(chunk);
224
- }
225
- catch (error) {
226
- terminateCodegraphProcess(childProcess);
227
- reject(error);
228
- }
229
- });
230
- childProcess.stderr.on('data', (chunk) => {
231
- try {
232
- stderrSize = assertOutputLimit(stderrSize, chunk.length);
233
- stderrChunks.push(chunk);
234
- }
235
- catch (error) {
236
- terminateCodegraphProcess(childProcess);
237
- reject(error);
238
- }
239
- });
240
- childProcess.on('error', (error) => {
241
- clearTimeout(timeout);
242
- reject(error);
243
- });
244
- childProcess.on('close', (exitCode) => {
245
- clearTimeout(timeout);
246
- resolveResult({
247
- exitCode,
248
- stdout: Buffer.concat(stdoutChunks).toString('utf8'),
249
- stderr: Buffer.concat(stderrChunks).toString('utf8')
250
- });
251
- });
252
- });
253
- }
254
- function defaultCodegraphProcessRunner(invocation) {
255
- return runCodegraphProcess(invocation);
256
- }
257
142
  export function createCodegraphInvocation(options) {
258
143
  assertSupportedSubcommand(options.subcommand);
259
144
  const projectRoot = resolveProjectRoot(options.project);
@@ -156,25 +156,6 @@ function validateUserConfigPathForWrite(configPath) {
156
156
  }
157
157
  }
158
158
  }
159
- function getExistingBoundary(path) {
160
- let currentPath = resolve(path);
161
- while (!existsSync(currentPath)) {
162
- const parentPath = dirname(currentPath);
163
- if (parentPath === currentPath) {
164
- return currentPath;
165
- }
166
- currentPath = parentPath;
167
- }
168
- return currentPath;
169
- }
170
- function validateArtifactWorkspaceRootBeforeCreate(artifactRoot, workspaceRoot) {
171
- const workspaceRootReal = realpathSync(workspaceRoot);
172
- const existingBoundary = getExistingBoundary(artifactRoot);
173
- const existingBoundaryReal = realpathSync(existingBoundary);
174
- if (isInsidePath(resolve(artifactRoot), workspaceRoot) || isInsidePath(existingBoundaryReal, workspaceRootReal)) {
175
- throw new Error('Artifact workspace must stay outside the project root');
176
- }
177
- }
178
159
  function validateArtifactWorkspaceRoot(artifactRoot, workspaceRoot) {
179
160
  const artifactStats = lstatSync(artifactRoot);
180
161
  if (!artifactStats.isDirectory() || artifactStats.isSymbolicLink()) {
@@ -890,7 +871,6 @@ function ensureArtifactWorkspaceMarker(workspace) {
890
871
  const artifactRoot = getWorkspaceArtifactRoot(workspace);
891
872
  const peaksPath = resolve(artifactRoot, '.peaks');
892
873
  const markerPath = resolve(peaksPath, 'config.json');
893
- validateArtifactWorkspaceRootBeforeCreate(artifactRoot, workspace.rootPath);
894
874
  ensureDir(artifactRoot);
895
875
  validateArtifactWorkspaceRoot(artifactRoot, workspace.rootPath);
896
876
  ensureDir(peaksPath);
@@ -907,7 +887,7 @@ export function ensureWorkspaceConfigForPath(path = process.cwd()) {
907
887
  const existingWorkspace = findWorkspaceForPath(config.workspaces, path);
908
888
  if (existingWorkspace) {
909
889
  ensureArtifactWorkspaceMarker(existingWorkspace);
910
- if (config.currentWorkspace !== existingWorkspace.workspaceId) {
890
+ if (!config.currentWorkspace) {
911
891
  writeConfig({ currentWorkspace: existingWorkspace.workspaceId }, 'user');
912
892
  }
913
893
  return existingWorkspace;
@@ -923,7 +903,7 @@ export function ensureWorkspaceConfigForPath(path = process.cwd()) {
923
903
  };
924
904
  ensureArtifactWorkspaceMarker(workspace);
925
905
  const updatedWorkspaces = [...config.workspaces, workspace];
926
- writeConfig({ workspaces: updatedWorkspaces, currentWorkspace: workspace.workspaceId }, 'user');
906
+ writeConfig({ workspaces: updatedWorkspaces, ...(!config.currentWorkspace ? { currentWorkspace: workspace.workspaceId } : {}) }, 'user');
927
907
  return workspace;
928
908
  }
929
909
  export function getWorkspaceConfigForCurrentPath() {
@@ -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
+ }
@@ -11,8 +11,15 @@ export type DoctorReport = {
11
11
  failed: number;
12
12
  };
13
13
  };
14
+ export type CodegraphCapabilityProbe = {
15
+ packagePath: string;
16
+ version: string;
17
+ binaryPath: string;
18
+ binaryExists: boolean;
19
+ };
14
20
  export type DoctorOptions = {
15
21
  schemasBaseDir?: string;
16
22
  skillsBaseDir?: string;
23
+ codegraphProbe?: () => CodegraphCapabilityProbe;
17
24
  };
18
25
  export declare function runDoctor(options?: DoctorOptions): Promise<DoctorReport>;
@@ -1,10 +1,41 @@
1
1
  import { join } from 'node:path';
2
2
  import { homedir } from 'node:os';
3
3
  import { existsSync } from 'node:fs';
4
+ import { createRequire } from 'node:module';
5
+ import { dirname, resolve as resolvePath } from 'node:path';
4
6
  import { readText } from '../../shared/fs.js';
5
7
  import { requiredSchemaFiles, requiredSkillNames, schemasDir } from '../../shared/paths.js';
6
8
  import { getErrorMessage } from '../../shared/result.js';
7
9
  import { loadSkillRegistry } from '../skills/skill-registry.js';
10
+ const CODEGRAPH_EXPECTED_VERSION = '0.7.10';
11
+ function defaultCodegraphProbe() {
12
+ const require = createRequire(import.meta.url);
13
+ const packagePath = require.resolve('@colbymchenry/codegraph/package.json');
14
+ const pkg = require(packagePath);
15
+ const binaryPath = resolvePath(dirname(packagePath), 'dist', 'bin', 'codegraph.js');
16
+ return {
17
+ packagePath,
18
+ version: pkg.version ?? 'unknown',
19
+ binaryPath,
20
+ binaryExists: existsSync(binaryPath)
21
+ };
22
+ }
23
+ const DESTRUCTIVE_APPLY_PATTERNS = [
24
+ /peaks\s+memory\s+sync[^\n]*--apply/,
25
+ /peaks\s+memory\s+extract[^\n]*--apply/,
26
+ /peaks\s+artifacts\s+sync[^\n]*--apply/,
27
+ /peaks\s+openspec\s+archive[^\n]*--apply/,
28
+ /peaks\s+standards\s+(?:init|update)[^\n]*--apply/
29
+ ];
30
+ const AUTHORIZATION_KEYWORDS_PATTERN = /authoriz|explicit|--dry-run|approv|only after|only when/i;
31
+ function extractRunbookSection(body) {
32
+ const match = /## Default runbook\n+([\s\S]*?)(?=\n## |$)/.exec(body);
33
+ return match === null ? null : (match[1] ?? null);
34
+ }
35
+ function findDestructiveApplyLines(section) {
36
+ const lines = section.split(/\r?\n/);
37
+ return lines.filter((line) => DESTRUCTIVE_APPLY_PATTERNS.some((pattern) => pattern.test(line)));
38
+ }
8
39
  export async function runDoctor(options = {}) {
9
40
  const checks = [];
10
41
  const registry = await loadSkillRegistry(options.skillsBaseDir);
@@ -35,6 +66,51 @@ export async function runDoctor(options = {}) {
35
66
  message: `Skill ${failure.directory} has invalid metadata: ${failure.message}`
36
67
  });
37
68
  }
69
+ const requiredSkillNameSet = new Set(requiredSkillNames);
70
+ for (const skill of skills) {
71
+ if (!requiredSkillNameSet.has(skill.name)) {
72
+ continue;
73
+ }
74
+ try {
75
+ const body = await readText(skill.skillPath);
76
+ const hasRunbook = /## Default runbook\s/.test(body);
77
+ checks.push({
78
+ id: `skill-runbook:${skill.name}`,
79
+ ok: hasRunbook,
80
+ message: hasRunbook
81
+ ? `Skill ${skill.name} declares a Default runbook`
82
+ : `Skill ${skill.name} is missing a ## Default runbook section`
83
+ });
84
+ const runbookSection = extractRunbookSection(body);
85
+ if (runbookSection !== null) {
86
+ const destructiveLines = findDestructiveApplyLines(runbookSection);
87
+ if (destructiveLines.length === 0) {
88
+ checks.push({
89
+ id: `skill-apply-note:${skill.name}`,
90
+ ok: true,
91
+ message: `Skill ${skill.name} runbook has no destructive --apply commands to gate`
92
+ });
93
+ }
94
+ else {
95
+ const hasAuthorizationNote = AUTHORIZATION_KEYWORDS_PATTERN.test(runbookSection);
96
+ checks.push({
97
+ id: `skill-apply-note:${skill.name}`,
98
+ ok: hasAuthorizationNote,
99
+ message: hasAuthorizationNote
100
+ ? `Skill ${skill.name} gates ${destructiveLines.length} destructive --apply command(s) with an authorization note`
101
+ : `Skill ${skill.name} has ${destructiveLines.length} destructive --apply command(s) without an authorization/dry-run note in the runbook section`
102
+ });
103
+ }
104
+ }
105
+ }
106
+ catch (error) {
107
+ checks.push({
108
+ id: `skill-runbook:${skill.name}`,
109
+ ok: false,
110
+ message: `Skill ${skill.name} runbook check failed: ${getErrorMessage(error)}`
111
+ });
112
+ }
113
+ }
38
114
  const schemaRoot = options.schemasBaseDir ?? schemasDir;
39
115
  for (const schemaFile of requiredSchemaFiles) {
40
116
  try {
@@ -56,6 +132,69 @@ export async function runDoctor(options = {}) {
56
132
  ok: true,
57
133
  message: hasUserConfig ? 'User config exists at ~/.peaks/config.json' : 'Optional user config not found at ~/.peaks/config.json'
58
134
  });
135
+ const probe = options.codegraphProbe ?? defaultCodegraphProbe;
136
+ try {
137
+ const result = probe();
138
+ const versionOk = result.version === CODEGRAPH_EXPECTED_VERSION;
139
+ if (!versionOk) {
140
+ checks.push({
141
+ id: 'capability:codegraph',
142
+ ok: false,
143
+ message: `@colbymchenry/codegraph version mismatch: expected ${CODEGRAPH_EXPECTED_VERSION}, resolved ${result.version} at ${result.packagePath}`
144
+ });
145
+ }
146
+ else if (!result.binaryExists) {
147
+ checks.push({
148
+ id: 'capability:codegraph',
149
+ ok: false,
150
+ message: `@colbymchenry/codegraph@${result.version} resolved at ${result.packagePath} but binary is missing at ${result.binaryPath}`
151
+ });
152
+ }
153
+ else {
154
+ checks.push({
155
+ id: 'capability:codegraph',
156
+ ok: true,
157
+ message: `@colbymchenry/codegraph@${result.version} resolves with binary at ${result.binaryPath}`
158
+ });
159
+ }
160
+ }
161
+ catch (error) {
162
+ checks.push({
163
+ id: 'capability:codegraph',
164
+ ok: false,
165
+ message: `@colbymchenry/codegraph not resolvable: ${getErrorMessage(error)}`
166
+ });
167
+ }
168
+ try {
169
+ const schemaText = await readText(join(schemaRoot, 'doctor-report.schema.json'));
170
+ const schema = JSON.parse(schemaText);
171
+ const patternSource = schema.properties?.checks?.items?.properties?.id?.pattern;
172
+ if (typeof patternSource === 'string') {
173
+ const pattern = new RegExp(patternSource);
174
+ const mismatches = checks.filter((check) => !pattern.test(check.id)).map((check) => check.id);
175
+ checks.push({
176
+ id: 'doctor-self:check-id-pattern',
177
+ ok: mismatches.length === 0,
178
+ message: mismatches.length === 0
179
+ ? 'All doctor check IDs match the doctor-report schema pattern'
180
+ : `Doctor check IDs missing from schema pattern: ${mismatches.join(', ')}`
181
+ });
182
+ }
183
+ else {
184
+ checks.push({
185
+ id: 'doctor-self:check-id-pattern',
186
+ ok: false,
187
+ message: 'doctor-report.schema.json does not declare a check.id pattern'
188
+ });
189
+ }
190
+ }
191
+ catch (error) {
192
+ checks.push({
193
+ id: 'doctor-self:check-id-pattern',
194
+ ok: false,
195
+ message: `Failed to load doctor-report.schema.json for self-validation: ${getErrorMessage(error)}`
196
+ });
197
+ }
59
198
  const failed = checks.filter((check) => !check.ok).length;
60
199
  return {
61
200
  checks,
@@ -0,0 +1,31 @@
1
+ import { type PlanMcpInstallOptions, type McpInstallEnvCheck } from './mcp-plan-service.js';
2
+ export type McpApplyAction = 'add' | 'update' | 'claimed' | 'noop';
3
+ export type McpApplyBackupInfo = {
4
+ path: string | null;
5
+ skipped: boolean;
6
+ };
7
+ export type McpApplyResult = {
8
+ capabilityId: string;
9
+ action: McpApplyAction;
10
+ backup: McpApplyBackupInfo;
11
+ written: {
12
+ settingsPath: string;
13
+ managedMarkerPath: string;
14
+ };
15
+ envCheck: McpInstallEnvCheck;
16
+ };
17
+ export type McpApplyOptions = PlanMcpInstallOptions & {
18
+ claim?: boolean;
19
+ backupRoot?: string;
20
+ clock?: () => string;
21
+ };
22
+ export type McpRollbackOptions = {
23
+ backupPath: string;
24
+ globalSettingsPath?: string;
25
+ };
26
+ export type McpRollbackResult = {
27
+ restoredFrom: string;
28
+ restoredTo: string;
29
+ };
30
+ export declare function applyMcpInstall(capabilityId: string, options?: McpApplyOptions): Promise<McpApplyResult>;
31
+ export declare function rollbackMcpInstall(options: McpRollbackOptions): Promise<McpRollbackResult>;