peaks-cli 1.0.21 → 1.0.23

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 (50) hide show
  1. package/bin/peaks.js +0 -0
  2. package/dist/src/cli/commands/capability-commands.d.ts +12 -1
  3. package/dist/src/cli/commands/capability-commands.js +9 -11
  4. package/dist/src/cli/commands/config-commands.js +2 -85
  5. package/dist/src/cli/commands/core-artifact-commands.js +6 -1
  6. package/dist/src/cli/commands/request-commands.js +82 -2
  7. package/dist/src/cli/commands/scan-commands.js +30 -0
  8. package/dist/src/cli/commands/workflow-commands.js +35 -5
  9. package/dist/src/services/artifacts/artifact-prerequisites.js +53 -13
  10. package/dist/src/services/artifacts/artifact-service.js +2 -2
  11. package/dist/src/services/artifacts/request-artifact-service.d.ts +32 -0
  12. package/dist/src/services/artifacts/request-artifact-service.js +148 -16
  13. package/dist/src/services/artifacts/workspace-service.d.ts +1 -1
  14. package/dist/src/services/artifacts/workspace-service.js +10 -20
  15. package/dist/src/services/config/config-safety.d.ts +1 -1
  16. package/dist/src/services/config/config-safety.js +4 -6
  17. package/dist/src/services/config/config-service.d.ts +1 -1
  18. package/dist/src/services/config/config-service.js +67 -69
  19. package/dist/src/services/config/config-types.d.ts +0 -2
  20. package/dist/src/services/config/config-types.js +0 -2
  21. package/dist/src/services/mode/bypass-tracker.d.ts +4 -0
  22. package/dist/src/services/mode/bypass-tracker.js +31 -0
  23. package/dist/src/services/mode/mode-enforcement.d.ts +14 -0
  24. package/dist/src/services/mode/mode-enforcement.js +81 -0
  25. package/dist/src/services/sc/sc-service.js +5 -5
  26. package/dist/src/services/scan/acceptance-coverage-service.js +1 -4
  27. package/dist/src/services/scan/archetype-service.js +4 -15
  28. package/dist/src/services/scan/diff-scope-service.js +3 -3
  29. package/dist/src/services/scan/file-size-scan.d.ts +19 -0
  30. package/dist/src/services/scan/file-size-scan.js +39 -0
  31. package/dist/src/services/scan/type-sanity-service.js +1 -3
  32. package/dist/src/services/session/index.d.ts +1 -0
  33. package/dist/src/services/session/index.js +1 -0
  34. package/dist/src/services/session/session-manager.d.ts +60 -0
  35. package/dist/src/services/session/session-manager.js +150 -0
  36. package/dist/src/services/skills/skill-presence-service.d.ts +4 -1
  37. package/dist/src/services/skills/skill-presence-service.js +11 -1
  38. package/dist/src/services/workflow/pipeline-verify-service.d.ts +31 -0
  39. package/dist/src/services/workflow/pipeline-verify-service.js +180 -0
  40. package/dist/src/services/workspace/workspace-service.js +6 -0
  41. package/dist/src/shared/change-id.d.ts +13 -0
  42. package/dist/src/shared/change-id.js +29 -1
  43. package/dist/src/shared/incrementing-number.d.ts +31 -0
  44. package/dist/src/shared/incrementing-number.js +58 -0
  45. package/dist/src/shared/version.d.ts +1 -1
  46. package/dist/src/shared/version.js +1 -1
  47. package/package.json +2 -1
  48. package/skills/peaks-rd/SKILL.md +3 -0
  49. package/skills/peaks-solo/SKILL.md +42 -11
  50. package/skills/peaks-ui/SKILL.md +3 -0
package/bin/peaks.js CHANGED
File without changes
@@ -1,5 +1,16 @@
1
1
  import { Command } from 'commander';
2
2
  import type { PeaksConfig } from '../../services/config/config-types.js';
3
+ import type { CapabilityMapSourceFilter } from '../../services/recommendations/recommendation-types.js';
3
4
  import { type ProgramIO } from '../cli-helpers.js';
5
+ type CapabilityMapOptions = {
6
+ json?: boolean;
7
+ source: string;
8
+ };
4
9
  export declare function registerCapabilityCommands(program: Command, io: ProgramIO): void;
5
- export declare function getInstalledCapabilityIds(config: PeaksConfig): string[];
10
+ export declare function runCapabilityStatus(io: ProgramIO, options: {
11
+ json?: boolean;
12
+ }): void;
13
+ export declare function runCapabilityMap(io: ProgramIO, options: CapabilityMapOptions): void;
14
+ export declare function getInstalledCapabilityIds(_config: PeaksConfig): string[];
15
+ export declare function parseCapabilityMapSource(source: string): CapabilityMapSourceFilter | null;
16
+ export {};
@@ -7,17 +7,18 @@ import { addJsonOption, printResult } from '../cli-helpers.js';
7
7
  const CAPABILITY_SOURCE_FILTERS = new Set(['all', 'access-repo', 'mcp-server']);
8
8
  export function registerCapabilityCommands(program, io) {
9
9
  const capability = program.command('capability').description('Inspect Peaks capability catalog and runtime availability');
10
- addJsonOption(capability.command('status').description('Show seed capability availability')).action((options) => {
11
- const availability = resolveCapabilityAvailability(seedCapabilityItems);
12
- printResult(io, ok('capability.status', { sources: seedCapabilitySources, items: seedCapabilityItems, availability }), options.json);
13
- });
10
+ addJsonOption(capability.command("status").description("Show seed capability availability")).action((options) => runCapabilityStatus(io, options));
14
11
  addCapabilityMapOptions(capability.command('map').description('Show dry-run external capability landing map')).action((options) => runCapabilityMap(io, options));
15
12
  addCapabilityMapOptions(program.command('capabilities').description('Show dry-run external capability landing map')).action((options) => runCapabilityMap(io, options));
16
13
  }
14
+ export function runCapabilityStatus(io, options) {
15
+ const availability = resolveCapabilityAvailability(seedCapabilityItems);
16
+ printResult(io, ok("capability.status", { sources: seedCapabilitySources, items: seedCapabilityItems, availability }), options.json);
17
+ }
17
18
  function addCapabilityMapOptions(command) {
18
19
  return addJsonOption(command.option('--source <source>', 'Filter source group: all, access-repo, or mcp-server', 'all'));
19
20
  }
20
- function runCapabilityMap(io, options) {
21
+ export function runCapabilityMap(io, options) {
21
22
  const source = parseCapabilityMapSource(options.source);
22
23
  if (!source) {
23
24
  printResult(io, fail('capabilities.map', 'UNSUPPORTED_CAPABILITY_SOURCE', 'Supported capability sources are all, access-repo, and mcp-server', { source: options.source }, ['Rerun with --source all, --source access-repo, or --source mcp-server']), options.json);
@@ -32,13 +33,10 @@ function runCapabilityMap(io, options) {
32
33
  ...(config.proxy.httpProxy === undefined ? {} : { httpProxy: config.proxy.httpProxy })
33
34
  })), options.json);
34
35
  }
35
- export function getInstalledCapabilityIds(config) {
36
- const currentWorkspace = config.workspaces.find((workspace) => workspace.workspaceId === config.currentWorkspace);
37
- if (!currentWorkspace)
38
- return [];
39
- return [...currentWorkspace.installedCapabilityIds];
36
+ export function getInstalledCapabilityIds(_config) {
37
+ return [];
40
38
  }
41
- function parseCapabilityMapSource(source) {
39
+ export function parseCapabilityMapSource(source) {
42
40
  if (CAPABILITY_SOURCE_FILTERS.has(source)) {
43
41
  return source;
44
42
  }
@@ -1,12 +1,11 @@
1
- import { addWorkspace, getConfig, getMiniMaxProviderConfig, getMiniMaxProviderStatus, isSensitiveConfigPath, readConfig, redactConfigSecrets, removeWorkspace, setConfig, setCurrentWorkspace, setMiniMaxProviderConfig } from '../../services/config/config-service.js';
1
+ import { getConfig, getMiniMaxProviderConfig, getMiniMaxProviderStatus, isSensitiveConfigPath, redactConfigSecrets, setConfig, setMiniMaxProviderConfig } from '../../services/config/config-service.js';
2
2
  import { testMiniMaxProvider } from '../../services/providers/minimax-provider-service.js';
3
3
  import { fail, ok } from '../../shared/result.js';
4
- import { addJsonOption, getErrorMessage, isArtifactProvider, isArtifactRepoSegment, isMiniMaxHttpsUrl, parseConfigLayer, printInvalidConfigLayer, printResult, redactSensitiveErrorMessage, summarizeMiniMaxSmokeResult } from '../cli-helpers.js';
4
+ import { addJsonOption, getErrorMessage, isMiniMaxHttpsUrl, parseConfigLayer, printInvalidConfigLayer, printResult, redactSensitiveErrorMessage, summarizeMiniMaxSmokeResult } from '../cli-helpers.js';
5
5
  export function registerConfigCommands(program, io) {
6
6
  const config = program.command('config').description('Manage Peaks configuration');
7
7
  registerConfigGetSetCommands(config, io);
8
8
  registerMiniMaxProviderCommands(config, io);
9
- registerWorkspaceCommands(config, io);
10
9
  }
11
10
  function registerConfigGetSetCommands(config, io) {
12
11
  addJsonOption(config.command('get').description('Get current config or a specific key').option('--key <path>', 'dot-notation key path').option('--layer <layer>', 'user or project')).action((options) => {
@@ -129,85 +128,3 @@ function printMiniMaxProviderSetError(io, error, asJson) {
129
128
  printResult(io, fail('config.provider.minimax.set', 'MINIMAX_PROVIDER_SET_FAILED', getErrorMessage(error), {}, ['Check MiniMax provider settings and retry']), asJson);
130
129
  process.exitCode = 1;
131
130
  }
132
- function registerWorkspaceCommands(config, io) {
133
- const configWorkspace = config.command('workspace').description('Manage workspaces');
134
- addJsonOption(configWorkspace.command('list').description('List all workspaces')).action((options) => {
135
- const cfg = readConfig();
136
- printResult(io, ok('config.workspace.list', { currentWorkspace: cfg.currentWorkspace, workspaces: cfg.workspaces }), options.json);
137
- });
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) => {
139
- const layer = parseConfigLayer(options.layer);
140
- if (layer === null) {
141
- printInvalidConfigLayer(io, 'config.workspace.add', options.json);
142
- return;
143
- }
144
- const artifactRepo = parseArtifactRepoInput(io, options, options.json);
145
- if (artifactRepo === null)
146
- return;
147
- const artifactStorage = artifactRepo ? { mode: 'local-with-remote-sync', remote: artifactRepo } : { mode: 'local' };
148
- const workspace = { workspaceId: options.id, name: options.name, rootPath: options.path, installedCapabilityIds: [], artifactStorage };
149
- const configLayer = layer ?? 'user';
150
- if (artifactRepo) {
151
- addWorkspace({ ...workspace, artifactRepo }, configLayer);
152
- }
153
- else {
154
- addWorkspace(workspace, configLayer);
155
- }
156
- printResult(io, ok('config.workspace.add', { workspaceId: options.id, name: options.name, rootPath: options.path, artifactRepo, artifactStorage }), options.json);
157
- });
158
- addJsonOption(configWorkspace.command('remove').description('Remove a workspace').requiredOption('--id <id>', 'workspace identifier').option('--layer <layer>', 'user or project')).action((options) => {
159
- const layer = parseConfigLayer(options.layer);
160
- if (layer === null) {
161
- printInvalidConfigLayer(io, 'config.workspace.remove', options.json);
162
- return;
163
- }
164
- const configLayer = layer ?? 'user';
165
- const removed = removeWorkspace(options.id, configLayer);
166
- if (removed) {
167
- printResult(io, ok('config.workspace.remove', { workspaceId: options.id }), options.json);
168
- }
169
- else {
170
- printWorkspaceNotFound(io, 'config.workspace.remove', `Workspace ${options.id} not found`, options.json);
171
- }
172
- });
173
- addJsonOption(configWorkspace.command('switch').description('Switch current workspace').requiredOption('--id <id>', 'workspace identifier').option('--layer <layer>', 'user or project')).action((options) => {
174
- const layer = parseConfigLayer(options.layer);
175
- if (layer === null) {
176
- printInvalidConfigLayer(io, 'config.workspace.switch', options.json);
177
- return;
178
- }
179
- const configLayer = layer ?? 'user';
180
- const switched = setCurrentWorkspace(options.id, configLayer);
181
- if (switched) {
182
- printResult(io, ok('config.workspace.switch', { currentWorkspace: options.id }), options.json);
183
- }
184
- else {
185
- printWorkspaceNotFound(io, 'config.workspace.switch', `Workspace ${options.id} not found`, options.json);
186
- }
187
- });
188
- }
189
- function parseArtifactRepoInput(io, options, asJson) {
190
- const hasArtifactRepoInput = options.provider !== undefined || options.repoOwner !== undefined || options.repoName !== undefined;
191
- if (!hasArtifactRepoInput)
192
- return undefined;
193
- if (!options.provider || !options.repoOwner || !options.repoName) {
194
- printResult(io, fail('config.workspace.add', 'INVALID_ARTIFACT_REPO_CONFIG', 'Artifact repo config requires --provider, --repo-owner, and --repo-name together', {}, ['Provide all three artifact repo options together, or omit them all']), asJson);
195
- process.exitCode = 1;
196
- return null;
197
- }
198
- if (!isArtifactProvider(options.provider)) {
199
- printResult(io, fail('config.workspace.add', 'UNSUPPORTED_ARTIFACT_PROVIDER', `Unsupported provider ${options.provider}`, {}, ['Use --provider github or --provider gitlab']), asJson);
200
- process.exitCode = 1;
201
- return null;
202
- }
203
- if (!isArtifactRepoSegment(options.repoOwner) || !isArtifactRepoSegment(options.repoName)) {
204
- printResult(io, fail('config.workspace.add', 'INVALID_ARTIFACT_REPO_CONFIG', 'Artifact repo owner and name must use safe GitHub/GitLab path segments', {}, ['Use letters, numbers, dots, underscores, or hyphens without path traversal']), asJson);
205
- process.exitCode = 1;
206
- return null;
207
- }
208
- return { provider: options.provider, owner: options.repoOwner, name: options.repoName };
209
- }
210
- function printWorkspaceNotFound(io, command, message, asJson) {
211
- printResult(io, fail(command, 'WORKSPACE_NOT_FOUND', message, {}, ['List workspaces with: peaks config workspace list']), asJson);
212
- process.exitCode = 1;
213
- }
@@ -7,7 +7,7 @@ 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
9
  import { inspectSkillRunbook } from '../../services/skills/skill-runbook-service.js';
10
- import { setSkillPresence, clearSkillPresence, getSkillPresence } from '../../services/skills/skill-presence-service.js';
10
+ import { setSkillPresence, clearSkillPresence, getSkillPresence, isSkillPresenceMode } from '../../services/skills/skill-presence-service.js';
11
11
  import { fail, ok } from '../../shared/result.js';
12
12
  import { addJsonOption, failUnsupportedNonDryRun, getErrorMessage, isArtifactProvider, isArtifactSetupStep, printResult } from '../cli-helpers.js';
13
13
  export function registerCoreAndArtifactCommands(program, io) {
@@ -72,6 +72,11 @@ export function registerCoreAndArtifactCommands(program, io) {
72
72
  .description('Set the currently active Peaks skill for session-wide visibility')
73
73
  .option('--mode <mode>', 'execution mode')
74
74
  .option('--gate <gate>', 'current gate')).action((name, options) => {
75
+ if (options.mode !== undefined && !isSkillPresenceMode(options.mode)) {
76
+ printResult(io, fail('skill.presence:set', 'INVALID_MODE', `Invalid mode: ${options.mode} (expected one of: full-auto, assisted, swarm, strict)`, { name, mode: options.mode }, ['Use a valid mode: full-auto, assisted, swarm, or strict']), options.json);
77
+ process.exitCode = 1;
78
+ return;
79
+ }
75
80
  const presence = setSkillPresence(name, options.mode, options.gate);
76
81
  printResult(io, ok('skill.presence:set', { active: true, ...presence }), options.json);
77
82
  });
@@ -1,5 +1,7 @@
1
1
  import { InvalidArgumentError } from 'commander';
2
- import { allowedStatesForRole, createRequestArtifact, listRequestArtifacts, showRequestArtifact, transitionRequestArtifact, PrerequisitesNotSatisfiedError, VALID_REQUEST_TYPES, isRequestType } from '../../services/artifacts/request-artifact-service.js';
2
+ import { allowedStatesForRole, createRequestArtifact, listRequestArtifacts, showRequestArtifact, transitionRequestArtifact, PrerequisitesNotSatisfiedError, LintGateError, TypeSanityViolationError, FileSizeViolationError, VALID_REQUEST_TYPES, isRequestType } from '../../services/artifacts/request-artifact-service.js';
3
+ import { ConfirmationRequiredError } from '../../services/mode/mode-enforcement.js';
4
+ import { recordBypass, isBypassLimitReached, MAX_BYPASSES_PER_SESSION } from '../../services/mode/bypass-tracker.js';
3
5
  import { lintRequestArtifact } from '../../services/artifacts/artifact-lint-service.js';
4
6
  import { getRepairCycleStatus } from '../../services/artifacts/repair-cycle-service.js';
5
7
  import { fail, ok } from '../../shared/result.js';
@@ -118,7 +120,9 @@ export function registerRequestCommands(program, io) {
118
120
  .requiredOption('--project <path>', 'target project root')
119
121
  .option('--session-id <session>', 'restrict to a specific session id')
120
122
  .option('--reason <text>', 'reason appended as a transition note; required when --allow-incomplete is set')
121
- .option('--allow-incomplete', 'bypass artifact prerequisite checks; requires --reason and records the bypass in the artifact')).action(async (requestId, options) => {
123
+ .option('--allow-incomplete', 'bypass artifact prerequisite checks; requires --reason and records the bypass in the artifact')
124
+ .option('--confirm', 'skip interactive confirmation prompt (for non-interactive / LLM contexts)')
125
+ .option('--force-confirm', 'bypass mode-enforced confirmation (use with caution)')).action(async (requestId, options) => {
122
126
  try {
123
127
  const role = options.role;
124
128
  const newState = parseStateForRole(role, options.state);
@@ -127,6 +131,26 @@ export function registerRequestCommands(program, io) {
127
131
  process.exitCode = 1;
128
132
  return;
129
133
  }
134
+ // Restrict --allow-incomplete in assisted/strict modes: require --confirm
135
+ if (options.allowIncomplete === true && options.forceConfirm !== true) {
136
+ const { getSkillPresence } = await import('../../services/skills/skill-presence-service.js');
137
+ const presence = getSkillPresence();
138
+ if (presence?.mode === 'assisted' || presence?.mode === 'strict') {
139
+ if (options.confirm !== true) {
140
+ printResult(io, fail('request.transition', 'ALLOW_INCOMPLETE_RESTRICTED', `--allow-incomplete requires --confirm in ${presence.mode} mode`, { role, requestId, mode: presence.mode }, ['Add --confirm to proceed non-interactively, or run in an interactive terminal.']), options.json);
141
+ process.exitCode = 1;
142
+ return;
143
+ }
144
+ // Check bypass count
145
+ const sessionRoot = (await import('node:path')).join(options.project, '.peaks', options.sessionId ?? 'default');
146
+ if (isBypassLimitReached(sessionRoot)) {
147
+ printResult(io, fail('request.transition', 'BYPASS_LIMIT_REACHED', `--allow-incomplete limit reached (${MAX_BYPASSES_PER_SESSION} per session)`, { role, requestId, limit: MAX_BYPASSES_PER_SESSION }, ['Produce the missing artifacts instead of bypassing.']), options.json);
148
+ process.exitCode = 1;
149
+ return;
150
+ }
151
+ recordBypass(sessionRoot);
152
+ }
153
+ }
130
154
  const transitionOptions = {
131
155
  role,
132
156
  requestId,
@@ -142,6 +166,28 @@ export function registerRequestCommands(program, io) {
142
166
  if (options.allowIncomplete === true) {
143
167
  transitionOptions.allowIncomplete = true;
144
168
  }
169
+ if (options.confirm === true) {
170
+ transitionOptions.confirmed = true;
171
+ }
172
+ if (options.forceConfirm === true) {
173
+ transitionOptions.forceConfirm = true;
174
+ }
175
+ // Type sanity check for PRD handoff
176
+ if (role === 'prd' && newState === 'handed-off') {
177
+ const { showRequestArtifact: showForType } = await import('../../services/artifacts/request-artifact-service.js');
178
+ const showTypeOptions = {
179
+ projectRoot: options.project,
180
+ role: 'prd',
181
+ requestId
182
+ };
183
+ if (options.sessionId !== undefined) {
184
+ showTypeOptions.sessionId = options.sessionId;
185
+ }
186
+ const existing = await showForType(showTypeOptions);
187
+ if (existing !== null) {
188
+ transitionOptions.typeSanityCheck = { projectRoot: options.project, declaredType: existing.requestType };
189
+ }
190
+ }
145
191
  const result = await transitionRequestArtifact(transitionOptions);
146
192
  if (result === null) {
147
193
  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);
@@ -163,6 +209,40 @@ export function registerRequestCommands(program, io) {
163
209
  process.exitCode = 1;
164
210
  return;
165
211
  }
212
+ if (error instanceof LintGateError) {
213
+ printResult(io, fail('request.transition', error.code, error.message, { role: error.role, newState: error.newState, errorCount: error.errorCount }, [
214
+ 'Fix lint errors in the artifact before transitioning.',
215
+ 'Run `peaks request lint --role <role> --id <rid> --project <path>` to see details.',
216
+ 'Or bypass with: --allow-incomplete --reason "<justification>"'
217
+ ]), options.json);
218
+ process.exitCode = 1;
219
+ return;
220
+ }
221
+ if (error instanceof TypeSanityViolationError) {
222
+ printResult(io, fail('request.transition', error.code, error.message, { declaredType: error.declaredType, suggestedTypes: error.suggestedTypes, rationale: error.rationale }, [
223
+ `Re-classify the request — likely correct type: ${error.suggestedTypes.join(' | ')}`,
224
+ 'Or, if the declared type is correct, surface the mismatch reason to the user.'
225
+ ]), options.json);
226
+ process.exitCode = 1;
227
+ return;
228
+ }
229
+ if (error instanceof FileSizeViolationError) {
230
+ printResult(io, fail('request.transition', error.code, error.message, { violations: error.violations, threshold: error.threshold }, [
231
+ ...error.violations.map((v) => `Split ${v.file} (${v.lines} lines) into smaller modules (< ${error.threshold} lines)`),
232
+ 'Or bypass with: --allow-incomplete --reason "<justification>"'
233
+ ]), options.json);
234
+ process.exitCode = 1;
235
+ return;
236
+ }
237
+ if (error instanceof ConfirmationRequiredError) {
238
+ printResult(io, fail('request.transition', 'CONFIRMATION_REQUIRED', error.message, { role: options.role, requestId }, [
239
+ 'Add --confirm to proceed non-interactively.',
240
+ 'Or run in an interactive terminal.',
241
+ 'In assisted/strict mode, major workflow boundaries require explicit user approval.'
242
+ ]), options.json);
243
+ process.exitCode = 1;
244
+ return;
245
+ }
166
246
  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);
167
247
  process.exitCode = 1;
168
248
  }
@@ -4,6 +4,7 @@ import { scanExistingSystem } from '../../services/scan/existing-system-service.
4
4
  import { checkTypeSanity } from '../../services/scan/type-sanity-service.js';
5
5
  import { getAcceptanceCoverage, isAcceptanceCoverageError } from '../../services/scan/acceptance-coverage-service.js';
6
6
  import { getDiffVsScope, isDiffScopeError } from '../../services/scan/diff-scope-service.js';
7
+ import { scanFileSize, DEFAULT_FILE_SIZE_THRESHOLD } from '../../services/scan/file-size-scan.js';
7
8
  import { isRequestType, VALID_REQUEST_TYPES } from '../../services/artifacts/artifact-prerequisites.js';
8
9
  import { fail, ok } from '../../shared/result.js';
9
10
  import { addJsonOption, getErrorMessage, printResult } from '../cli-helpers.js';
@@ -191,4 +192,33 @@ export function registerScanCommands(program, io) {
191
192
  process.exitCode = 1;
192
193
  }
193
194
  });
195
+ addJsonOption(scan
196
+ .command('file-size')
197
+ .description('Check git diff for files exceeding a line count threshold (karpathy-skills "Simplicity First")')
198
+ .requiredOption('--project <path>', 'target project root')
199
+ .option('--base-ref <ref>', 'compare working tree against this git ref (default: HEAD)')
200
+ .option('--threshold <n>', `line count threshold (default: ${DEFAULT_FILE_SIZE_THRESHOLD})`)).action((options) => {
201
+ try {
202
+ const threshold = options.threshold !== undefined && /^\d+$/.test(options.threshold)
203
+ ? Number(options.threshold)
204
+ : undefined;
205
+ const result = scanFileSize({
206
+ projectRoot: options.project,
207
+ ...(options.baseRef !== undefined ? { baseRef: options.baseRef } : {}),
208
+ ...(threshold !== undefined ? { threshold } : {})
209
+ });
210
+ const nextActions = [];
211
+ if (!result.ok) {
212
+ nextActions.push(`${result.violations.length} file(s) exceed ${result.threshold} lines. Split into smaller modules.`);
213
+ }
214
+ printResult(io, ok('scan.file-size', result, [], nextActions), options.json);
215
+ if (!result.ok) {
216
+ process.exitCode = 1;
217
+ }
218
+ }
219
+ catch (error) {
220
+ printResult(io, fail('scan.file-size', 'FILE_SIZE_SCAN_FAILED', getErrorMessage(error), { projectRoot: options.project }, ['Verify the project path is a git repository']), options.json);
221
+ process.exitCode = 1;
222
+ }
223
+ });
194
224
  }
@@ -5,21 +5,26 @@ import { createAutonomousWorkflowPlan } from '../../services/workflow/workflow-a
5
5
  import { writeAutonomousResumeArtifacts } from '../../services/workflow/autonomous-resume-writer.js';
6
6
  import { createRecommendationPlan } from '../../services/recommendations/recommendation-service.js';
7
7
  import { createRefactorDryRun } from '../../services/refactor/refactor-service.js';
8
- import { ensureWorkspaceConfigForCurrentPath, getCurrentWorkspaceConfig, readConfig } from '../../services/config/config-service.js';
8
+ import { getWorkspaceConfigForPath, readConfig } from '../../services/config/config-service.js';
9
9
  import { validateChangeIdOrThrow } from '../../shared/change-id.js';
10
10
  import { getEconomyAwareExecutionModelId } from '../../services/config/model-routing.js';
11
11
  import { getLocalArtifactPath } from '../../services/artifacts/workspace-service.js';
12
+ import { getSessionId } from '../../services/session/session-manager.js';
13
+ import { verifyPipeline } from '../../services/workflow/pipeline-verify-service.js';
12
14
  import { fail, ok } from '../../shared/result.js';
13
15
  import { addJsonOption, failUnsupportedNonDryRun, getErrorMessage, isRecommendationWorkflow, printResult } from '../cli-helpers.js';
14
16
  function getCurrentWorkspaceContext() {
15
- const workspace = getCurrentWorkspaceConfig();
16
- if (!workspace)
17
+ try {
18
+ const sessionId = getSessionId(process.cwd());
19
+ return sessionId ? { sessionId, sessionDir: `.peaks/${sessionId}` } : {};
20
+ }
21
+ catch {
17
22
  return {};
18
- return { workspace, artifactWorkspacePath: getLocalArtifactPath(workspace) };
23
+ }
19
24
  }
20
25
  function getWorkflowWorkspaceContext() {
21
26
  try {
22
- const workspace = ensureWorkspaceConfigForCurrentPath() ?? getCurrentWorkspaceConfig();
27
+ const workspace = getWorkspaceConfigForPath(process.cwd());
23
28
  if (!workspace)
24
29
  return {};
25
30
  return { workspace, artifactWorkspacePath: getLocalArtifactPath(workspace) };
@@ -299,6 +304,31 @@ export function registerWorkflowCommands(program, io) {
299
304
  addAutonomousResumeInitOptions(autonomousResume.command('init')).action((options) => runAutonomousResumeInit(io, options));
300
305
  const autonomousResumeAlias = program.command('autonomous-resume').description('Manage autonomous workflow resume artifacts');
301
306
  addAutonomousResumeInitOptions(autonomousResumeAlias.command('init')).action((options) => runAutonomousResumeInit(io, options));
307
+ addJsonOption(workflow
308
+ .command('verify-pipeline')
309
+ .description('Verify the complete rd→qa pipeline was followed for a request')
310
+ .requiredOption('--rid <rid>', 'request identifier')
311
+ .requiredOption('--project <path>', 'project root path')
312
+ .requiredOption('--session-id <id>', 'session identifier')
313
+ .option('--type <type>', 'request type: feature, bugfix, refactor, docs, config, chore', 'feature')).action(async (options) => {
314
+ try {
315
+ const result = await verifyPipeline({
316
+ projectRoot: options.project,
317
+ rid: options.rid,
318
+ sessionId: options.sessionId,
319
+ ...(options.type ? { requestType: options.type } : {})
320
+ });
321
+ const exitOk = result.complete ? 0 : 1;
322
+ printResult(io, result.complete
323
+ ? ok('workflow.verify-pipeline', result)
324
+ : fail('workflow.verify-pipeline', 'PIPELINE_INCOMPLETE', `${result.violations.length} violation(s): ${result.violations.join('; ')}`, result, result.nextActions), options.json);
325
+ process.exitCode = exitOk;
326
+ }
327
+ catch (error) {
328
+ printResult(io, fail('workflow.verify-pipeline', 'VERIFY_FAILED', getErrorMessage(error), {}, ['Check that --project, --rid, and --session-id are correct']), options.json);
329
+ process.exitCode = 1;
330
+ }
331
+ });
302
332
  const swarm = program.command('swarm').description('Plan RD swarm dry-run graphs');
303
333
  addSwarmPlanOptions(swarm.command('plan'), true).action((options) => runSwarmPlan(io, options));
304
334
  addSwarmPlanOptions(program.command('swarm-plan'), false).action((options) => runSwarmPlan(io, options));
@@ -14,14 +14,42 @@ export function isRequestType(value) {
14
14
  return VALID_REQUEST_TYPES.includes(value);
15
15
  }
16
16
  // Shared prerequisite fragments
17
- const TECH_DOC = { relativePath: 'rd/tech-doc.md', description: 'RD technical design doc (architecture, files changed, data flow)' };
18
- const BUG_ANALYSIS = { relativePath: 'rd/bug-analysis.md', description: 'Bug root-cause analysis (reproduction, affected paths, fix approach, regression test plan)' };
19
- const CODE_REVIEW = { relativePath: 'rd/code-review.md', description: 'Code review evidence (CRITICAL/HIGH must be fixed before handoff)' };
17
+ const TECH_DOC = {
18
+ relativePath: 'rd/tech-doc.md',
19
+ description: 'RD technical design doc (architecture, files changed, data flow)',
20
+ mustContain: ['## Red-line scope', '## Implementation evidence']
21
+ };
22
+ const BUG_ANALYSIS = {
23
+ relativePath: 'rd/bug-analysis.md',
24
+ description: 'Bug root-cause analysis (reproduction, affected paths, fix approach, regression test plan)',
25
+ mustContain: ['## Root cause', '## Fix approach']
26
+ };
27
+ const CODE_REVIEW = {
28
+ relativePath: 'rd/code-review.md',
29
+ description: 'Code review evidence (CRITICAL/HIGH must be fixed before handoff)',
30
+ mustContain: ['## Findings', 'CRITICAL']
31
+ };
20
32
  const SECURITY_REVIEW = { relativePath: 'rd/security-review.md', description: 'Security review evidence for the changed surface' };
21
- const TEST_CASES = { relativePath: 'qa/test-cases/<rid>.md', description: 'Generated test cases (unit / integration / UI regression)' };
22
- const TEST_REPORT = { relativePath: 'qa/test-reports/<rid>.md', description: 'Test execution report with actual pass/fail/coverage results' };
23
- const SECURITY_FINDINGS = { relativePath: 'qa/security-findings.md', description: 'Security test findings (record "no findings" inside if truly clean)' };
24
- const PERFORMANCE_FINDINGS = { relativePath: 'qa/performance-findings.md', description: 'Performance test findings (record baseline/after numbers or explicit "not applicable" rationale)' };
33
+ const TEST_CASES = {
34
+ relativePath: 'qa/test-cases/<rid>.md',
35
+ description: 'Generated test cases (unit / integration / UI regression)',
36
+ mustContain: ['## Test cases']
37
+ };
38
+ const TEST_REPORT = {
39
+ relativePath: 'qa/test-reports/<rid>.md',
40
+ description: 'Test execution report with actual pass/fail/coverage results',
41
+ mustContain: ['## Test execution']
42
+ };
43
+ const SECURITY_FINDINGS = {
44
+ relativePath: 'qa/security-findings.md',
45
+ description: 'Security test findings (record "no findings" inside if truly clean)',
46
+ mustContain: ['## Findings']
47
+ };
48
+ const PERFORMANCE_FINDINGS = {
49
+ relativePath: 'qa/performance-findings.md',
50
+ description: 'Performance test findings (record baseline/after numbers or explicit "not applicable" rationale)',
51
+ mustContain: ['## Baseline']
52
+ };
25
53
  // PRD content prereq: ensures the PRD artifact has actual scope/acceptance content
26
54
  // before handoff to RD/UI/QA. The SKILL says "Handoff to RD/UI/QA is blocked while
27
55
  // the artifact is missing or in `draft` state" — this gives that claim a CLI gate.
@@ -30,10 +58,19 @@ const PRD_CONTENT = {
30
58
  description: 'PRD artifact must contain Goal and Acceptance criteria sections before handoff',
31
59
  mustContain: ['## Goals', '## Acceptance']
32
60
  };
61
+ const UNIT_TESTS = {
62
+ relativePath: 'qa/test-cases/<rid>.md',
63
+ description: 'Unit test files for the implemented changes (enforces peaks-rd Gate B2)',
64
+ mustContain: ['## Test cases', 'test(']
65
+ };
66
+ const QA_INITIATED = {
67
+ relativePath: 'qa/.initiated',
68
+ description: 'QA skill must be invoked before RD handoff (run peaks request init --role qa)'
69
+ };
33
70
  const FEATURE_TABLE = {
34
71
  'prd:handed-off': [PRD_CONTENT],
35
72
  'rd:implemented': [TECH_DOC],
36
- 'rd:qa-handoff': [TECH_DOC, CODE_REVIEW, SECURITY_REVIEW],
73
+ 'rd:qa-handoff': [TECH_DOC, CODE_REVIEW, SECURITY_REVIEW, UNIT_TESTS, QA_INITIATED],
37
74
  'qa:running': [TEST_CASES],
38
75
  'qa:verdict-issued': [TEST_CASES, TEST_REPORT, SECURITY_FINDINGS, PERFORMANCE_FINDINGS]
39
76
  };
@@ -42,14 +79,17 @@ const FEATURE_TABLE = {
42
79
  const BUGFIX_TABLE = {
43
80
  'prd:handed-off': [PRD_CONTENT],
44
81
  'rd:implemented': [BUG_ANALYSIS],
45
- 'rd:qa-handoff': [BUG_ANALYSIS, CODE_REVIEW, SECURITY_REVIEW],
82
+ 'rd:qa-handoff': [BUG_ANALYSIS, CODE_REVIEW, SECURITY_REVIEW, UNIT_TESTS, QA_INITIATED],
46
83
  'qa:running': [TEST_CASES],
47
84
  'qa:verdict-issued': [TEST_CASES, TEST_REPORT, SECURITY_FINDINGS]
48
85
  };
49
86
  // Refactor: same as feature; refactor hard gates (coverage ≥ 95%) are enforced separately in peaks-rd SKILL.
50
87
  const REFACTOR_TABLE = FEATURE_TABLE;
51
- // Docs / chore: no artifact gates. Use sparingly for genuinely doc-only or formatter-only changes.
52
- const NO_GATES = {};
88
+ // Docs / chore: minimal gaterequire PRD content before proceeding.
89
+ // Prevents jumping to implementation without planning.
90
+ const MINIMAL_TABLE = {
91
+ 'prd:handed-off': [PRD_CONTENT]
92
+ };
53
93
  // Config: security review is the only mandatory check (config changes can break auth, CORS, CSP, secrets handling).
54
94
  const CONFIG_TABLE = {
55
95
  'prd:handed-off': [PRD_CONTENT],
@@ -60,9 +100,9 @@ const PREREQUISITES_BY_TYPE = {
60
100
  feature: FEATURE_TABLE,
61
101
  bugfix: BUGFIX_TABLE,
62
102
  refactor: REFACTOR_TABLE,
63
- docs: NO_GATES,
103
+ docs: MINIMAL_TABLE,
64
104
  config: CONFIG_TABLE,
65
- chore: NO_GATES
105
+ chore: MINIMAL_TABLE
66
106
  };
67
107
  export function getPrerequisitesFor(role, newState, requestType = DEFAULT_REQUEST_TYPE) {
68
108
  const table = PREREQUISITES_BY_TYPE[requestType];
@@ -2,7 +2,7 @@ import { existsSync } from 'node:fs';
2
2
  import { execFileSync } from 'node:child_process';
3
3
  import { homedir } from 'node:os';
4
4
  import { resolve } from 'node:path';
5
- import { getCurrentWorkspaceConfig } from '../config/config-service.js';
5
+ import { getWorkspaceConfigForPath } from '../config/config-service.js';
6
6
  import { getArtifactRemoteRepo, getLocalArtifactPath } from './workspace-service.js';
7
7
  function getRemoteUrl(artifactRepo) {
8
8
  if (!artifactRepo)
@@ -45,7 +45,7 @@ export function createArtifactInitPlan(options) {
45
45
  };
46
46
  }
47
47
  export function createGuidedArtifactSetup() {
48
- const workspace = getCurrentWorkspaceConfig();
48
+ const workspace = getWorkspaceConfigForPath(process.cwd());
49
49
  const artifactRepo = workspace ? getArtifactRemoteRepo(workspace) : null;
50
50
  const validationResult = {
51
51
  workspaceExists: workspace !== null,
@@ -54,6 +54,12 @@ export type TransitionRequestArtifactOptions = {
54
54
  sessionId?: string;
55
55
  reason?: string;
56
56
  allowIncomplete?: boolean;
57
+ confirmed?: boolean;
58
+ forceConfirm?: boolean;
59
+ typeSanityCheck?: {
60
+ projectRoot: string;
61
+ declaredType: RequestType;
62
+ };
57
63
  clock?: () => string;
58
64
  };
59
65
  export type TransitionRequestArtifactResult = RequestArtifactSummary & {
@@ -69,4 +75,30 @@ export declare class PrerequisitesNotSatisfiedError extends Error {
69
75
  readonly missing: PrerequisiteCheckResult['missing'];
70
76
  constructor(role: RequestArtifactRole, newState: RequestArtifactState, sessionId: string, missing: PrerequisiteCheckResult['missing']);
71
77
  }
78
+ export declare class LintGateError extends Error {
79
+ readonly code = "LINT_GATE_FAILED";
80
+ readonly role: RequestArtifactRole;
81
+ readonly newState: RequestArtifactState;
82
+ readonly errorCount: number;
83
+ constructor(role: RequestArtifactRole, newState: RequestArtifactState, errorCount: number);
84
+ }
85
+ export declare class TypeSanityViolationError extends Error {
86
+ readonly code = "TYPE_SANITY_VIOLATION";
87
+ readonly declaredType: RequestType;
88
+ readonly suggestedTypes: ReadonlyArray<RequestType>;
89
+ readonly rationale: string;
90
+ constructor(declaredType: RequestType, suggestedTypes: ReadonlyArray<RequestType>, rationale: string);
91
+ }
92
+ export declare class FileSizeViolationError extends Error {
93
+ readonly code = "FILE_SIZE_VIOLATION";
94
+ readonly violations: Array<{
95
+ file: string;
96
+ lines: number;
97
+ }>;
98
+ readonly threshold: number;
99
+ constructor(violations: Array<{
100
+ file: string;
101
+ lines: number;
102
+ }>, threshold: number);
103
+ }
72
104
  export declare function transitionRequestArtifact(options: TransitionRequestArtifactOptions): Promise<TransitionRequestArtifactResult | null>;