peaks-cli 1.0.12 → 1.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/bin/peaks.js +0 -0
  2. package/dist/src/cli/commands/config-commands.js +1 -17
  3. package/dist/src/cli/commands/core-artifact-commands.js +23 -0
  4. package/dist/src/cli/commands/mcp-commands.d.ts +3 -0
  5. package/dist/src/cli/commands/mcp-commands.js +144 -0
  6. package/dist/src/cli/commands/openspec-commands.d.ts +3 -0
  7. package/dist/src/cli/commands/openspec-commands.js +169 -0
  8. package/dist/src/cli/commands/project-commands.d.ts +3 -0
  9. package/dist/src/cli/commands/project-commands.js +37 -0
  10. package/dist/src/cli/commands/request-commands.d.ts +3 -0
  11. package/dist/src/cli/commands/request-commands.js +140 -0
  12. package/dist/src/cli/commands/understand-commands.d.ts +3 -0
  13. package/dist/src/cli/commands/understand-commands.js +78 -0
  14. package/dist/src/cli/commands/workflow-commands.js +56 -94
  15. package/dist/src/cli/program.js +10 -0
  16. package/dist/src/services/artifacts/request-artifact-service.d.ts +58 -0
  17. package/dist/src/services/artifacts/request-artifact-service.js +432 -0
  18. package/dist/src/services/codegraph/codegraph-service.js +26 -45
  19. package/dist/src/services/config/config-service.js +2 -22
  20. package/dist/src/services/dashboard/project-dashboard-service.d.ts +64 -0
  21. package/dist/src/services/dashboard/project-dashboard-service.js +112 -0
  22. package/dist/src/services/doctor/doctor-service.d.ts +7 -0
  23. package/dist/src/services/doctor/doctor-service.js +139 -0
  24. package/dist/src/services/mcp/mcp-apply-service.d.ts +31 -0
  25. package/dist/src/services/mcp/mcp-apply-service.js +112 -0
  26. package/dist/src/services/mcp/mcp-call-service.d.ts +17 -0
  27. package/dist/src/services/mcp/mcp-call-service.js +34 -0
  28. package/dist/src/services/mcp/mcp-client-service.d.ts +14 -0
  29. package/dist/src/services/mcp/mcp-client-service.js +49 -0
  30. package/dist/src/services/mcp/mcp-install-registry.d.ts +11 -0
  31. package/dist/src/services/mcp/mcp-install-registry.js +38 -0
  32. package/dist/src/services/mcp/mcp-plan-service.d.ts +29 -0
  33. package/dist/src/services/mcp/mcp-plan-service.js +109 -0
  34. package/dist/src/services/mcp/mcp-protocol.d.ts +24 -0
  35. package/dist/src/services/mcp/mcp-protocol.js +41 -0
  36. package/dist/src/services/mcp/mcp-scan-service.d.ts +8 -0
  37. package/dist/src/services/mcp/mcp-scan-service.js +214 -0
  38. package/dist/src/services/mcp/mcp-stdio-transport.d.ts +10 -0
  39. package/dist/src/services/mcp/mcp-stdio-transport.js +50 -0
  40. package/dist/src/services/mcp/mcp-types.d.ts +31 -0
  41. package/dist/src/services/mcp/mcp-types.js +1 -0
  42. package/dist/src/services/openspec/openspec-archive-service.d.ts +12 -0
  43. package/dist/src/services/openspec/openspec-archive-service.js +28 -0
  44. package/dist/src/services/openspec/openspec-bridge-service.d.ts +16 -0
  45. package/dist/src/services/openspec/openspec-bridge-service.js +76 -0
  46. package/dist/src/services/openspec/openspec-render-service.d.ts +38 -0
  47. package/dist/src/services/openspec/openspec-render-service.js +130 -0
  48. package/dist/src/services/openspec/openspec-scan-service.d.ts +6 -0
  49. package/dist/src/services/openspec/openspec-scan-service.js +123 -0
  50. package/dist/src/services/openspec/openspec-types.d.ts +39 -0
  51. package/dist/src/services/openspec/openspec-types.js +1 -0
  52. package/dist/src/services/openspec/openspec-validate-service.d.ts +27 -0
  53. package/dist/src/services/openspec/openspec-validate-service.js +77 -0
  54. package/dist/src/services/recommendations/capability-seed-items.js +2 -1
  55. package/dist/src/services/recommendations/capability-seed-mappings.js +1 -1
  56. package/dist/src/services/recommendations/capability-seed-sources.js +1 -1
  57. package/dist/src/services/shadcn/shadcn-service.d.ts +4 -0
  58. package/dist/src/services/shadcn/shadcn-service.js +15 -30
  59. package/dist/src/services/skills/skill-runbook-service.d.ts +11 -0
  60. package/dist/src/services/skills/skill-runbook-service.js +60 -0
  61. package/dist/src/services/standards/project-standards-service.js +4 -9
  62. package/dist/src/services/understand/understand-scan-service.d.ts +28 -0
  63. package/dist/src/services/understand/understand-scan-service.js +157 -0
  64. package/dist/src/services/understand/understand-types.d.ts +24 -0
  65. package/dist/src/services/understand/understand-types.js +1 -0
  66. package/dist/src/shared/json-schema-mini.d.ts +10 -0
  67. package/dist/src/shared/json-schema-mini.js +113 -0
  68. package/dist/src/shared/paths.d.ts +1 -1
  69. package/dist/src/shared/paths.js +9 -1
  70. package/dist/src/shared/version.d.ts +1 -1
  71. package/dist/src/shared/version.js +1 -1
  72. package/package.json +2 -8
  73. package/schemas/doctor-report.schema.json +34 -0
  74. package/schemas/mcp-apply-result.schema.json +46 -0
  75. package/schemas/mcp-install-plan.schema.json +71 -0
  76. package/schemas/mcp-install-spec.schema.json +29 -0
  77. package/schemas/mcp-server.schema.json +29 -0
  78. package/schemas/openspec-change-summary.schema.json +68 -0
  79. package/schemas/openspec-render-request.schema.json +61 -0
  80. package/schemas/openspec-validation-result.schema.json +36 -0
  81. package/skills/peaks-prd/SKILL.md +59 -8
  82. package/skills/peaks-prd/references/artifact-per-request.md +78 -0
  83. package/skills/peaks-prd/references/workflow.md +7 -5
  84. package/skills/peaks-qa/SKILL.md +74 -8
  85. package/skills/peaks-qa/references/artifact-contracts.md +2 -2
  86. package/skills/peaks-qa/references/artifact-per-request.md +83 -0
  87. package/skills/peaks-qa/references/openspec-validation-gate.md +55 -0
  88. package/skills/peaks-qa/references/regression-gates.md +2 -2
  89. package/skills/peaks-rd/SKILL.md +96 -9
  90. package/skills/peaks-rd/references/artifact-contracts.md +2 -2
  91. package/skills/peaks-rd/references/artifact-per-request.md +90 -0
  92. package/skills/peaks-rd/references/openspec-mcp-cli.md +65 -0
  93. package/skills/peaks-rd/references/refactor-workflow.md +2 -2
  94. package/skills/peaks-sc/SKILL.md +44 -0
  95. package/skills/peaks-sc/references/openspec-commit-boundaries.md +33 -0
  96. package/skills/peaks-solo/SKILL.md +90 -9
  97. package/skills/peaks-solo/references/artifact-contracts.md +2 -2
  98. package/skills/peaks-solo/references/browser-workflow.md +114 -0
  99. package/skills/peaks-solo/references/external-skill-invocation.md +70 -0
  100. package/skills/peaks-solo/references/openspec-mcp-workflow.md +53 -0
  101. package/skills/peaks-solo/references/refactor-mode.md +2 -2
  102. package/skills/peaks-solo/references/workflow.md +1 -1
  103. package/skills/peaks-txt/SKILL.md +42 -0
  104. package/skills/peaks-ui/SKILL.md +57 -33
  105. package/skills/peaks-ui/references/artifact-per-request.md +71 -0
  106. package/skills/peaks-ui/references/workflow.md +8 -11
  107. package/scripts/strip-internal-exports.mjs +0 -33
package/bin/peaks.js CHANGED
File without changes
@@ -1,5 +1,4 @@
1
- import { getLocalArtifactPath } from '../../services/artifacts/workspace-service.js';
2
- import { addWorkspace, ensureWorkspaceConfigForPath, getConfig, getMiniMaxProviderConfig, getMiniMaxProviderStatus, isSensitiveConfigPath, readConfig, redactConfigSecrets, removeWorkspace, setConfig, setCurrentWorkspace, setMiniMaxProviderConfig } from '../../services/config/config-service.js';
1
+ import { addWorkspace, getConfig, getMiniMaxProviderConfig, getMiniMaxProviderStatus, isSensitiveConfigPath, readConfig, redactConfigSecrets, removeWorkspace, setConfig, setCurrentWorkspace, setMiniMaxProviderConfig } from '../../services/config/config-service.js';
3
2
  import { testMiniMaxProvider } from '../../services/providers/minimax-provider-service.js';
4
3
  import { fail, ok } from '../../shared/result.js';
5
4
  import { addJsonOption, getErrorMessage, isArtifactProvider, isArtifactRepoSegment, isMiniMaxHttpsUrl, parseConfigLayer, printInvalidConfigLayer, printResult, redactSensitiveErrorMessage, summarizeMiniMaxSmokeResult } from '../cli-helpers.js';
@@ -136,21 +135,6 @@ function registerWorkspaceCommands(config, io) {
136
135
  const cfg = readConfig();
137
136
  printResult(io, ok('config.workspace.list', { currentWorkspace: cfg.currentWorkspace, workspaces: cfg.workspaces }), options.json);
138
137
  });
139
- addJsonOption(configWorkspace.command('ensure').description('Ensure a user workspace exists for a project path and make it current').option('--path <path>', 'project path to ensure, defaults to cwd')).action((options) => {
140
- try {
141
- const workspace = ensureWorkspaceConfigForPath(options.path ?? process.cwd());
142
- if (!workspace) {
143
- printResult(io, fail('config.workspace.ensure', 'WORKSPACE_ENSURE_FAILED', 'Could not resolve a workspace for the provided path', {}, ['Run from inside a project or pass --path <project>']), options.json);
144
- process.exitCode = 1;
145
- return;
146
- }
147
- printResult(io, ok('config.workspace.ensure', { workspace, currentWorkspace: workspace.workspaceId, artifactWorkspacePath: getLocalArtifactPath(workspace) }), options.json);
148
- }
149
- catch (error) {
150
- printResult(io, fail('config.workspace.ensure', 'WORKSPACE_ENSURE_FAILED', getErrorMessage(error), {}, ['Check that the project path exists and artifact workspace markers are safe']), options.json);
151
- process.exitCode = 1;
152
- }
153
- });
154
138
  addJsonOption(configWorkspace.command('add').description('Add a workspace').requiredOption('--id <id>', 'workspace identifier').requiredOption('--name <name>', 'workspace display name').requiredOption('--path <path>', 'workspace root path').option('--provider <provider>', 'artifact repo provider: github or gitlab').option('--repo-owner <owner>', 'artifact repo owner').option('--repo-name <name>', 'artifact repo name').option('--layer <layer>', 'user or project')).action((options) => {
155
139
  const layer = parseConfigLayer(options.layer);
156
140
  if (layer === null) {
@@ -6,6 +6,7 @@ import { listProfiles } from '../../services/profiles/profile-service.js';
6
6
  import { planProxyTest } from '../../services/proxy/proxy-service.js';
7
7
  import { runDoctor } from '../../services/doctor/doctor-service.js';
8
8
  import { listSkills } from '../../services/skills/skill-registry.js';
9
+ import { inspectSkillRunbook } from '../../services/skills/skill-runbook-service.js';
9
10
  import { fail, ok } from '../../shared/result.js';
10
11
  import { addJsonOption, failUnsupportedNonDryRun, getErrorMessage, isArtifactProvider, isArtifactSetupStep, printResult } from '../cli-helpers.js';
11
12
  export function registerCoreAndArtifactCommands(program, io) {
@@ -33,6 +34,28 @@ export function registerCoreAndArtifactCommands(program, io) {
33
34
  process.exitCode = 1;
34
35
  }
35
36
  });
37
+ addJsonOption(skill
38
+ .command('runbook <name>')
39
+ .description('Inspect a skill Default runbook section and its --apply authorization-note status')).action(async (name, options) => {
40
+ try {
41
+ const inspection = await inspectSkillRunbook(name);
42
+ const result = inspection.ok
43
+ ? ok('skill.runbook', inspection)
44
+ : fail('skill.runbook', inspection.hasRunbook ? 'SKILL_RUNBOOK_APPLY_UNGATED' : 'SKILL_RUNBOOK_MISSING', inspection.hasRunbook
45
+ ? `Skill ${inspection.name} has ${inspection.destructiveApplyLines.length} destructive --apply command(s) without an authorization/dry-run note`
46
+ : `Skill ${inspection.name} is missing a ## Default runbook section`, inspection, inspection.hasRunbook
47
+ ? ['Add an authorization or --dry-run note next to destructive --apply lines in the runbook section']
48
+ : ['Add a `## Default runbook` section to the skill SKILL.md']);
49
+ printResult(io, result, options.json);
50
+ if (!inspection.ok) {
51
+ process.exitCode = 1;
52
+ }
53
+ }
54
+ catch (error) {
55
+ printResult(io, fail('skill.runbook', 'SKILL_NOT_FOUND', getErrorMessage(error), { name }), options.json);
56
+ process.exitCode = 1;
57
+ }
58
+ });
36
59
  const profile = program.command('profile').description('Manage runtime profiles');
37
60
  addJsonOption(profile.command('list').description('List available profiles')).action((options) => {
38
61
  printResult(io, ok('profile.list', { profiles: listProfiles() }), options.json);
@@ -0,0 +1,3 @@
1
+ import { Command } from 'commander';
2
+ import { type ProgramIO } from '../cli-helpers.js';
3
+ export declare function registerMcpCommands(program: Command, io: ProgramIO): void;
@@ -0,0 +1,144 @@
1
+ import { InvalidArgumentError } from 'commander';
2
+ import { readFile } from 'node:fs/promises';
3
+ import { scanMcpServers } from '../../services/mcp/mcp-scan-service.js';
4
+ import { planMcpInstall } from '../../services/mcp/mcp-plan-service.js';
5
+ import { applyMcpInstall, rollbackMcpInstall } from '../../services/mcp/mcp-apply-service.js';
6
+ import { callMcpTool } from '../../services/mcp/mcp-call-service.js';
7
+ import { createStdioTransportFromSpec } from '../../services/mcp/mcp-stdio-transport.js';
8
+ import { fail, ok } from '../../shared/result.js';
9
+ import { addJsonOption, failUnsupportedNonDryRun, getErrorMessage, printResult } from '../cli-helpers.js';
10
+ function parsePositiveInteger(value) {
11
+ if (!/^\d+$/.test(value)) {
12
+ throw new InvalidArgumentError('must be a positive integer');
13
+ }
14
+ const parsed = Number(value);
15
+ if (!Number.isSafeInteger(parsed) || parsed < 1) {
16
+ throw new InvalidArgumentError('must be a positive integer');
17
+ }
18
+ return parsed;
19
+ }
20
+ async function resolveCallArgs(options) {
21
+ if (options.argsJson !== undefined && options.args !== undefined) {
22
+ throw new Error('Pass either --args-json or --args, not both');
23
+ }
24
+ const raw = options.argsJson !== undefined
25
+ ? options.argsJson
26
+ : options.args !== undefined ? await readFile(options.args, 'utf8') : '{}';
27
+ const parsed = JSON.parse(raw);
28
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
29
+ throw new Error('MCP tool arguments must be a JSON object');
30
+ }
31
+ return parsed;
32
+ }
33
+ export function registerMcpCommands(program, io) {
34
+ const mcp = program.command('mcp').description('Manage Claude Code MCP servers');
35
+ addJsonOption(mcp
36
+ .command('list')
37
+ .alias('scan')
38
+ .description('Scan Claude Code settings for configured MCP servers')
39
+ .option('--project <path>', 'project root to also scan project-level .claude/settings.json')).action(async (options) => {
40
+ try {
41
+ const report = await scanMcpServers(options.project !== undefined ? { projectRoot: options.project } : {});
42
+ printResult(io, ok('mcp.list', report), options.json);
43
+ }
44
+ catch (error) {
45
+ printResult(io, fail('mcp.list', 'MCP_LIST_FAILED', getErrorMessage(error), {}, ['Check Claude settings path and permissions before retrying']), options.json);
46
+ process.exitCode = 1;
47
+ }
48
+ });
49
+ addJsonOption(mcp
50
+ .command('plan')
51
+ .description('Plan an MCP server install diff for a capability (dry-run only)')
52
+ .requiredOption('--capability <id>', 'capability id from the MCP install registry')
53
+ .option('--project <path>', 'project root for scoped scan')
54
+ .option('--dry-run', 'preview the install diff (always true)', true)
55
+ .option('--no-dry-run', 'unsupported: peaks mcp plan never writes settings')).action(async (options) => {
56
+ if (options.dryRun === false) {
57
+ failUnsupportedNonDryRun(io, 'mcp.plan', options.json);
58
+ return;
59
+ }
60
+ try {
61
+ const planOptions = options.project !== undefined ? { projectRoot: options.project } : {};
62
+ const plan = await planMcpInstall(options.capability, planOptions);
63
+ if (plan.action === 'unknown-capability') {
64
+ printResult(io, fail('mcp.plan', 'MCP_UNKNOWN_CAPABILITY', `No MCP install spec registered for capability ${options.capability}`, plan, plan.nextActions), options.json);
65
+ process.exitCode = 1;
66
+ return;
67
+ }
68
+ printResult(io, ok('mcp.plan', plan, [], plan.nextActions), options.json);
69
+ }
70
+ catch (error) {
71
+ printResult(io, fail('mcp.plan', 'MCP_PLAN_FAILED', getErrorMessage(error), { capabilityId: options.capability }, ['Check Claude settings path and the capability id before retrying']), options.json);
72
+ process.exitCode = 1;
73
+ }
74
+ });
75
+ addJsonOption(mcp
76
+ .command('apply')
77
+ .description('Apply an MCP server install for a capability (writes .claude/settings.json with backup)')
78
+ .requiredOption('--capability <id>', 'capability id from the MCP install registry')
79
+ .option('--yes', 'confirm the write — required for any real side effect')
80
+ .option('--claim', 'take ownership of an existing non-peaks-managed server entry')
81
+ .option('--project <path>', 'project root for scoped scan')).action(async (options) => {
82
+ if (options.yes !== true) {
83
+ printResult(io, fail('mcp.apply', 'MCP_APPLY_REQUIRES_YES', 'Refusing to apply without --yes', { capabilityId: options.capability }, ['Re-run with --yes to confirm the write']), options.json);
84
+ process.exitCode = 1;
85
+ return;
86
+ }
87
+ try {
88
+ const applyOptions = {};
89
+ if (options.project !== undefined) {
90
+ applyOptions.projectRoot = options.project;
91
+ }
92
+ if (options.claim === true) {
93
+ applyOptions.claim = true;
94
+ }
95
+ const result = await applyMcpInstall(options.capability, applyOptions);
96
+ printResult(io, ok('mcp.apply', result), options.json);
97
+ }
98
+ catch (error) {
99
+ printResult(io, fail('mcp.apply', 'MCP_APPLY_FAILED', getErrorMessage(error), { capabilityId: options.capability }, ['Check the plan first with peaks mcp plan, then re-run apply']), options.json);
100
+ process.exitCode = 1;
101
+ }
102
+ });
103
+ addJsonOption(mcp
104
+ .command('rollback')
105
+ .description('Restore Claude Code settings.json from a peaks-managed MCP backup file')
106
+ .requiredOption('--backup <path>', 'path to a previously created backup settings.json')).action(async (options) => {
107
+ try {
108
+ const result = await rollbackMcpInstall({ backupPath: options.backup });
109
+ printResult(io, ok('mcp.rollback', result), options.json);
110
+ }
111
+ catch (error) {
112
+ printResult(io, fail('mcp.rollback', 'MCP_ROLLBACK_FAILED', getErrorMessage(error), { backupPath: options.backup }, ['Verify the backup path and rerun']), options.json);
113
+ process.exitCode = 1;
114
+ }
115
+ });
116
+ addJsonOption(mcp
117
+ .command('call')
118
+ .description('Invoke a tool on an installed MCP server via stdio (spawns the server, calls tools/call, closes)')
119
+ .requiredOption('--capability <id>', 'capability id from the MCP install registry')
120
+ .requiredOption('--tool <name>', 'MCP tool name to invoke')
121
+ .option('--args <path>', 'path to a JSON file describing the tool arguments object')
122
+ .option('--args-json <jsonString>', 'inline JSON object describing the tool arguments')
123
+ .option('--timeout <ms>', 'per-request timeout in milliseconds', parsePositiveInteger)).action(async (options) => {
124
+ try {
125
+ const args = await resolveCallArgs(options);
126
+ const factory = createStdioTransportFromSpec;
127
+ const callOptions = {
128
+ capabilityId: options.capability,
129
+ toolName: options.tool,
130
+ args,
131
+ transportFactory: factory
132
+ };
133
+ if (options.timeout !== undefined) {
134
+ callOptions.timeoutMs = Number(options.timeout);
135
+ }
136
+ const result = await callMcpTool(callOptions);
137
+ printResult(io, ok('mcp.call', result), options.json);
138
+ }
139
+ catch (error) {
140
+ printResult(io, fail('mcp.call', 'MCP_CALL_FAILED', getErrorMessage(error), { capabilityId: options.capability, toolName: options.tool }, ['Check the capability id, tool name, args JSON, and required env vars before retrying']), options.json);
141
+ process.exitCode = 1;
142
+ }
143
+ });
144
+ }
@@ -0,0 +1,3 @@
1
+ import { Command } from 'commander';
2
+ import { type ProgramIO } from '../cli-helpers.js';
3
+ export declare function registerOpenSpecCommands(program: Command, io: ProgramIO): void;
@@ -0,0 +1,169 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { loadOpenSpecChange, scanOpenSpec } from '../../services/openspec/openspec-scan-service.js';
3
+ import { projectOpenSpecToRdInput } from '../../services/openspec/openspec-bridge-service.js';
4
+ import { renderOpenSpecChange } from '../../services/openspec/openspec-render-service.js';
5
+ import { validateOpenSpecChange } from '../../services/openspec/openspec-validate-service.js';
6
+ import { archiveOpenSpecChange } from '../../services/openspec/openspec-archive-service.js';
7
+ import { fail, ok } from '../../shared/result.js';
8
+ import { addJsonOption, getErrorMessage, printResult } from '../cli-helpers.js';
9
+ function resolveScanOptions(project) {
10
+ if (project === undefined) {
11
+ return {};
12
+ }
13
+ return { openspecRoot: `${project.replace(/\\/g, '/').replace(/\/$/, '')}/openspec` };
14
+ }
15
+ async function loadRenderRequest(requestPath) {
16
+ const raw = await readFile(requestPath, 'utf8');
17
+ const parsed = JSON.parse(raw);
18
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
19
+ throw new Error('Render request file must contain a JSON object');
20
+ }
21
+ return parsed;
22
+ }
23
+ export function registerOpenSpecCommands(program, io) {
24
+ const openspec = program.command('openspec').description('Inspect OpenSpec changes inside the target project');
25
+ addJsonOption(openspec
26
+ .command('list')
27
+ .description('List OpenSpec changes detected under <project>/openspec/changes')
28
+ .option('--project <path>', 'project root containing an openspec/ directory')).action(async (options) => {
29
+ try {
30
+ const report = await scanOpenSpec(resolveScanOptions(options.project));
31
+ printResult(io, ok('openspec.list', report), options.json);
32
+ }
33
+ catch (error) {
34
+ printResult(io, fail('openspec.list', 'OPENSPEC_LIST_FAILED', getErrorMessage(error), {}, ['Check the project path and openspec/ layout before retrying']), options.json);
35
+ process.exitCode = 1;
36
+ }
37
+ });
38
+ addJsonOption(openspec
39
+ .command('show')
40
+ .description('Show parsed proposal and tasks progress for a single OpenSpec change')
41
+ .argument('<changeId>', 'OpenSpec change directory name under openspec/changes')
42
+ .option('--project <path>', 'project root containing an openspec/ directory')).action(async (changeId, options) => {
43
+ try {
44
+ const detail = await loadOpenSpecChange(changeId, resolveScanOptions(options.project));
45
+ if (detail === null) {
46
+ printResult(io, fail('openspec.show', 'OPENSPEC_CHANGE_NOT_FOUND', `OpenSpec change ${changeId} was not found`, { changeId }, [`Verify openspec/changes/${changeId}/ exists`]), options.json);
47
+ process.exitCode = 1;
48
+ return;
49
+ }
50
+ printResult(io, ok('openspec.show', detail), options.json);
51
+ }
52
+ catch (error) {
53
+ printResult(io, fail('openspec.show', 'OPENSPEC_SHOW_FAILED', getErrorMessage(error), { changeId }, ['Check the project path and openspec/ layout before retrying']), options.json);
54
+ process.exitCode = 1;
55
+ }
56
+ });
57
+ addJsonOption(openspec
58
+ .command('to-rd')
59
+ .description('Project an OpenSpec change into an RD/SC input shape (acceptance, what-changes, commit boundaries)')
60
+ .argument('<changeId>', 'OpenSpec change directory name under openspec/changes')
61
+ .option('--project <path>', 'project root containing an openspec/ directory')).action(async (changeId, options) => {
62
+ try {
63
+ const projection = await projectOpenSpecToRdInput(changeId, resolveScanOptions(options.project));
64
+ if (projection === null) {
65
+ printResult(io, fail('openspec.to-rd', 'OPENSPEC_CHANGE_NOT_FOUND', `OpenSpec change ${changeId} was not found`, { changeId }, [`Verify openspec/changes/${changeId}/ exists`]), options.json);
66
+ process.exitCode = 1;
67
+ return;
68
+ }
69
+ printResult(io, ok('openspec.to-rd', projection), options.json);
70
+ }
71
+ catch (error) {
72
+ printResult(io, fail('openspec.to-rd', 'OPENSPEC_TO_RD_FAILED', getErrorMessage(error), { changeId }, ['Check the project path and openspec/ layout before retrying']), options.json);
73
+ process.exitCode = 1;
74
+ }
75
+ });
76
+ addJsonOption(openspec
77
+ .command('render')
78
+ .description('Render an OpenSpec change pack from a JSON request file (dry-run by default)')
79
+ .requiredOption('--request <path>', 'path to a JSON file describing the render request')
80
+ .option('--project <path>', 'project root containing an openspec/ directory')
81
+ .option('--apply', 'write the rendered files into openspec/changes/<id>/')
82
+ .option('--overwrite', 'overwrite an existing change directory when --apply is set')).action(async (options) => {
83
+ try {
84
+ const request = await loadRenderRequest(options.request);
85
+ const scan = resolveScanOptions(options.project);
86
+ const renderOptions = {};
87
+ if (scan.openspecRoot !== undefined) {
88
+ renderOptions.openspecRoot = scan.openspecRoot;
89
+ }
90
+ if (options.apply === true) {
91
+ renderOptions.apply = true;
92
+ }
93
+ if (options.overwrite === true) {
94
+ renderOptions.overwrite = true;
95
+ }
96
+ const result = await renderOpenSpecChange(request, renderOptions);
97
+ printResult(io, ok('openspec.render', result), options.json);
98
+ }
99
+ catch (error) {
100
+ printResult(io, fail('openspec.render', 'OPENSPEC_RENDER_FAILED', getErrorMessage(error), { requestPath: options.request }, ['Check the request JSON shape and the openspec root before retrying']), options.json);
101
+ process.exitCode = 1;
102
+ }
103
+ });
104
+ addJsonOption(openspec
105
+ .command('validate')
106
+ .description('Validate an OpenSpec change against internal lint rules (and optionally the external openspec CLI)')
107
+ .argument('<changeId>', 'OpenSpec change directory name under openspec/changes')
108
+ .option('--project <path>', 'project root containing an openspec/ directory')
109
+ .option('--prefer-external', 'use the external openspec CLI when available, fall back to internal lint')).action(async (changeId, options) => {
110
+ try {
111
+ const scan = resolveScanOptions(options.project);
112
+ const validateOptions = {};
113
+ if (scan.openspecRoot !== undefined) {
114
+ validateOptions.openspecRoot = scan.openspecRoot;
115
+ }
116
+ if (options.preferExternal === true) {
117
+ validateOptions.preferExternal = true;
118
+ }
119
+ const result = await validateOpenSpecChange(changeId, validateOptions);
120
+ if (result === null) {
121
+ printResult(io, fail('openspec.validate', 'OPENSPEC_CHANGE_NOT_FOUND', `OpenSpec change ${changeId} was not found`, { changeId }, [`Verify openspec/changes/${changeId}/ exists`]), options.json);
122
+ process.exitCode = 1;
123
+ return;
124
+ }
125
+ if (!result.valid) {
126
+ printResult(io, fail('openspec.validate', 'OPENSPEC_VALIDATE_INVALID', `OpenSpec change ${changeId} failed validation`, result, result.issues.map((issue) => `${issue.level}: ${issue.rule}: ${issue.message}`)), options.json);
127
+ process.exitCode = 1;
128
+ return;
129
+ }
130
+ printResult(io, ok('openspec.validate', result, result.issues.filter((issue) => issue.level === 'warning').map((issue) => `${issue.rule}: ${issue.message}`)), options.json);
131
+ }
132
+ catch (error) {
133
+ printResult(io, fail('openspec.validate', 'OPENSPEC_VALIDATE_FAILED', getErrorMessage(error), { changeId }, ['Check the project path and openspec/ layout before retrying']), options.json);
134
+ process.exitCode = 1;
135
+ }
136
+ });
137
+ addJsonOption(openspec
138
+ .command('archive')
139
+ .description('Move an OpenSpec change under openspec/changes/<archiveDir>/<id>/ (dry-run by default)')
140
+ .argument('<changeId>', 'OpenSpec change directory name under openspec/changes')
141
+ .option('--project <path>', 'project root containing an openspec/ directory')
142
+ .option('--apply', 'actually move the change directory')
143
+ .option('--archive-dir <name>', 'archive subdirectory name (default: archive)')).action(async (changeId, options) => {
144
+ try {
145
+ const scan = resolveScanOptions(options.project);
146
+ const archiveOptions = {};
147
+ if (scan.openspecRoot !== undefined) {
148
+ archiveOptions.openspecRoot = scan.openspecRoot;
149
+ }
150
+ if (options.apply === true) {
151
+ archiveOptions.apply = true;
152
+ }
153
+ if (options.archiveDir !== undefined) {
154
+ archiveOptions.archiveDirName = options.archiveDir;
155
+ }
156
+ const result = await archiveOpenSpecChange(changeId, archiveOptions);
157
+ if (result === null) {
158
+ printResult(io, fail('openspec.archive', 'OPENSPEC_CHANGE_NOT_FOUND', `OpenSpec change ${changeId} was not found`, { changeId }, [`Verify openspec/changes/${changeId}/ exists`]), options.json);
159
+ process.exitCode = 1;
160
+ return;
161
+ }
162
+ printResult(io, ok('openspec.archive', result, [], result.applied ? [] : [`Re-run with --apply to move ${result.from} → ${result.to}`]), options.json);
163
+ }
164
+ catch (error) {
165
+ printResult(io, fail('openspec.archive', 'OPENSPEC_ARCHIVE_FAILED', getErrorMessage(error), { changeId }, ['Check the project path and openspec/ layout before retrying']), options.json);
166
+ process.exitCode = 1;
167
+ }
168
+ });
169
+ }
@@ -0,0 +1,3 @@
1
+ import { Command } from 'commander';
2
+ import { type ProgramIO } from '../cli-helpers.js';
3
+ export declare function registerProjectCommands(program: Command, io: ProgramIO): void;
@@ -0,0 +1,37 @@
1
+ import { loadProjectDashboard } from '../../services/dashboard/project-dashboard-service.js';
2
+ import { fail, ok } from '../../shared/result.js';
3
+ import { addJsonOption, getErrorMessage, printResult } from '../cli-helpers.js';
4
+ export function registerProjectCommands(program, io) {
5
+ const project = program.command('project').description('Aggregate Peaks state for a target project (read-only)');
6
+ addJsonOption(project
7
+ .command('dashboard')
8
+ .description('One-call snapshot of doctor / MCP / OpenSpec / requests / Understand Anything / capabilities for a project')
9
+ .requiredOption('--project <path>', 'target project root')).action(async (options) => {
10
+ try {
11
+ const dashboard = await loadProjectDashboard({ projectRoot: options.project });
12
+ if (!dashboard.runbookHealth.ok) {
13
+ const suggestions = [
14
+ dashboard.runbookHealth.missingRunbook.length > 0
15
+ ? `Add a ## Default runbook section to: ${dashboard.runbookHealth.missingRunbook.join(', ')}`
16
+ : null,
17
+ dashboard.runbookHealth.applyNoteFailed.length > 0
18
+ ? `Add authorization/--dry-run notes next to destructive --apply lines in: ${dashboard.runbookHealth.applyNoteFailed.join(', ')}`
19
+ : null
20
+ ].filter((line) => line !== null);
21
+ printResult(io, fail('project.dashboard', 'PROJECT_DASHBOARD_RUNBOOK_UNHEALTHY', `Skill runbook health failing: ${dashboard.runbookHealth.healthy}/${dashboard.runbookHealth.required} healthy`, dashboard, suggestions), options.json);
22
+ process.exitCode = 1;
23
+ return;
24
+ }
25
+ if (!dashboard.doctor.ok) {
26
+ printResult(io, fail('project.dashboard', 'PROJECT_DASHBOARD_DOCTOR_FAILED', `Doctor reports ${dashboard.doctor.failed} failed check(s) (${dashboard.doctor.passed} passed)`, dashboard, ['Run `peaks doctor --json` and resolve the failing checks before re-running the dashboard']), options.json);
27
+ process.exitCode = 1;
28
+ return;
29
+ }
30
+ printResult(io, ok('project.dashboard', dashboard), options.json);
31
+ }
32
+ catch (error) {
33
+ printResult(io, fail('project.dashboard', 'PROJECT_DASHBOARD_FAILED', getErrorMessage(error), { projectRoot: options.project }, ['Check the project path before retrying']), options.json);
34
+ process.exitCode = 1;
35
+ }
36
+ });
37
+ }
@@ -0,0 +1,3 @@
1
+ import { Command } from 'commander';
2
+ import { type ProgramIO } from '../cli-helpers.js';
3
+ export declare function registerRequestCommands(program: Command, io: ProgramIO): void;
@@ -0,0 +1,140 @@
1
+ import { InvalidArgumentError } from 'commander';
2
+ import { allowedStatesForRole, createRequestArtifact, listRequestArtifacts, showRequestArtifact, transitionRequestArtifact } from '../../services/artifacts/request-artifact-service.js';
3
+ import { fail, ok } from '../../shared/result.js';
4
+ import { addJsonOption, getErrorMessage, printResult } from '../cli-helpers.js';
5
+ const VALID_ROLES = ['prd', 'ui', 'rd', 'qa'];
6
+ function parseRole(value) {
7
+ if (!VALID_ROLES.includes(value)) {
8
+ throw new InvalidArgumentError(`must be one of ${VALID_ROLES.join(', ')}`);
9
+ }
10
+ return value;
11
+ }
12
+ function parseStateForRole(role, value) {
13
+ const allowed = allowedStatesForRole(role);
14
+ if (!allowed.includes(value)) {
15
+ throw new InvalidArgumentError(`must be one of ${allowed.join(', ')} for role ${role}`);
16
+ }
17
+ return value;
18
+ }
19
+ export function registerRequestCommands(program, io) {
20
+ const request = program.command('request').description('Manage per-request Peaks role artifacts (PRD / UI / RD / QA)');
21
+ addJsonOption(request
22
+ .command('init')
23
+ .description('Create the per-request artifact template for a Peaks role (dry-run by default)')
24
+ .requiredOption('--role <role>', `target role (${VALID_ROLES.join(' | ')})`, parseRole)
25
+ .requiredOption('--id <request-id>', 'request id, e.g. 2026-05-23-add-foo')
26
+ .requiredOption('--project <path>', 'target project root')
27
+ .option('--session-id <session>', 'override the default date-stamped session id')
28
+ .option('--apply', 'write the artifact file (default: preview only)')).action(async (options) => {
29
+ try {
30
+ const serviceOptions = {
31
+ role: options.role,
32
+ requestId: options.id,
33
+ projectRoot: options.project
34
+ };
35
+ if (options.sessionId !== undefined) {
36
+ serviceOptions.sessionId = options.sessionId;
37
+ }
38
+ if (options.apply === true) {
39
+ serviceOptions.apply = true;
40
+ }
41
+ const result = await createRequestArtifact(serviceOptions);
42
+ printResult(io, ok('request.init', result, [], result.applied ? [] : [`Re-run with --apply to write ${result.path}`]), options.json);
43
+ }
44
+ catch (error) {
45
+ printResult(io, fail('request.init', 'REQUEST_INIT_FAILED', getErrorMessage(error), { role: options.role, requestId: options.id }, ['Check role, request id, and project path before retrying']), options.json);
46
+ process.exitCode = 1;
47
+ }
48
+ });
49
+ addJsonOption(request
50
+ .command('list')
51
+ .description('List per-request artifacts under a project workspace')
52
+ .requiredOption('--project <path>', 'target project root')
53
+ .option('--session-id <session>', 'limit to a specific session id')
54
+ .option('--role <role>', `limit to a single role (${VALID_ROLES.join(' | ')})`, parseRole)).action(async (options) => {
55
+ try {
56
+ const listOptions = { projectRoot: options.project };
57
+ if (options.sessionId !== undefined) {
58
+ listOptions.sessionId = options.sessionId;
59
+ }
60
+ if (options.role !== undefined) {
61
+ listOptions.role = options.role;
62
+ }
63
+ const items = await listRequestArtifacts(listOptions);
64
+ printResult(io, ok('request.list', { count: items.length, items }), options.json);
65
+ }
66
+ catch (error) {
67
+ printResult(io, fail('request.list', 'REQUEST_LIST_FAILED', getErrorMessage(error), { projectRoot: options.project }, ['Check project path before retrying']), options.json);
68
+ process.exitCode = 1;
69
+ }
70
+ });
71
+ addJsonOption(request
72
+ .command('show')
73
+ .description('Show a single per-request artifact, optionally scoped to a session')
74
+ .argument('<request-id>', 'request id, e.g. 2026-05-23-add-foo')
75
+ .requiredOption('--role <role>', `target role (${VALID_ROLES.join(' | ')})`, parseRole)
76
+ .requiredOption('--project <path>', 'target project root')
77
+ .option('--session-id <session>', 'restrict to a specific session id')).action(async (requestId, options) => {
78
+ try {
79
+ const showOptions = {
80
+ projectRoot: options.project,
81
+ role: options.role,
82
+ requestId
83
+ };
84
+ if (options.sessionId !== undefined) {
85
+ showOptions.sessionId = options.sessionId;
86
+ }
87
+ const result = await showRequestArtifact(showOptions);
88
+ if (result === null) {
89
+ printResult(io, fail('request.show', 'REQUEST_NOT_FOUND', `No artifact found for role=${options.role} requestId=${requestId}`, { role: options.role, requestId }, ['Verify the request id, role, and session id']), options.json);
90
+ process.exitCode = 1;
91
+ return;
92
+ }
93
+ printResult(io, ok('request.show', result), options.json);
94
+ }
95
+ catch (error) {
96
+ printResult(io, fail('request.show', 'REQUEST_SHOW_FAILED', getErrorMessage(error), { role: options.role, requestId }, ['Check role, request id, and project path before retrying']), options.json);
97
+ process.exitCode = 1;
98
+ }
99
+ });
100
+ addJsonOption(request
101
+ .command('transition')
102
+ .description('Move a per-request artifact to a new state defined by its role state machine')
103
+ .argument('<request-id>', 'request id, e.g. 2026-05-23-add-foo')
104
+ .requiredOption('--role <role>', `target role (${VALID_ROLES.join(' | ')})`, parseRole)
105
+ .requiredOption('--state <state>', 'new state name; allowed values depend on role')
106
+ .requiredOption('--project <path>', 'target project root')
107
+ .option('--session-id <session>', 'restrict to a specific session id')
108
+ .option('--reason <text>', 'optional reason appended as a transition note')).action(async (requestId, options) => {
109
+ try {
110
+ const role = options.role;
111
+ const newState = parseStateForRole(role, options.state);
112
+ const transitionOptions = {
113
+ role,
114
+ requestId,
115
+ projectRoot: options.project,
116
+ newState
117
+ };
118
+ if (options.sessionId !== undefined) {
119
+ transitionOptions.sessionId = options.sessionId;
120
+ }
121
+ if (options.reason !== undefined) {
122
+ transitionOptions.reason = options.reason;
123
+ }
124
+ const result = await transitionRequestArtifact(transitionOptions);
125
+ if (result === null) {
126
+ printResult(io, fail('request.transition', 'REQUEST_NOT_FOUND', `No artifact found for role=${role} requestId=${requestId}`, { role, requestId }, ['Verify the request id, role, and session id']), options.json);
127
+ process.exitCode = 1;
128
+ return;
129
+ }
130
+ printResult(io, ok('request.transition', result), options.json);
131
+ }
132
+ catch (error) {
133
+ if (error instanceof InvalidArgumentError) {
134
+ throw error;
135
+ }
136
+ printResult(io, fail('request.transition', 'REQUEST_TRANSITION_FAILED', getErrorMessage(error), { role: options.role, requestId }, ['Check role, request id, state, and project path before retrying']), options.json);
137
+ process.exitCode = 1;
138
+ }
139
+ });
140
+ }
@@ -0,0 +1,3 @@
1
+ import { Command } from 'commander';
2
+ import { type ProgramIO } from '../cli-helpers.js';
3
+ export declare function registerUnderstandCommands(program: Command, io: ProgramIO): void;