peaks-cli 1.0.22 → 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.
- package/bin/peaks.js +0 -0
- package/dist/src/cli/commands/capability-commands.d.ts +11 -0
- package/dist/src/cli/commands/capability-commands.js +7 -6
- package/dist/src/cli/commands/workflow-commands.js +26 -0
- package/dist/src/services/artifacts/workspace-service.d.ts +1 -1
- package/dist/src/services/artifacts/workspace-service.js +2 -11
- package/dist/src/services/config/config-safety.d.ts +1 -1
- package/dist/src/services/config/config-safety.js +4 -6
- package/dist/src/services/config/config-service.d.ts +1 -1
- package/dist/src/services/config/config-service.js +17 -4
- package/dist/src/services/scan/acceptance-coverage-service.js +1 -4
- package/dist/src/services/scan/archetype-service.js +4 -15
- package/dist/src/services/scan/diff-scope-service.js +3 -3
- package/dist/src/services/scan/file-size-scan.js +2 -7
- package/dist/src/services/scan/type-sanity-service.js +1 -3
- package/dist/src/services/workflow/pipeline-verify-service.d.ts +31 -0
- package/dist/src/services/workflow/pipeline-verify-service.js +180 -0
- package/dist/src/shared/change-id.js +0 -3
- package/dist/src/shared/version.d.ts +1 -1
- package/dist/src/shared/version.js +1 -1
- package/package.json +2 -1
- package/skills/peaks-solo/SKILL.md +33 -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;
|
|
10
|
+
export declare function runCapabilityStatus(io: ProgramIO, options: {
|
|
11
|
+
json?: boolean;
|
|
12
|
+
}): void;
|
|
13
|
+
export declare function runCapabilityMap(io: ProgramIO, options: CapabilityMapOptions): void;
|
|
5
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(
|
|
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);
|
|
@@ -35,7 +36,7 @@ function runCapabilityMap(io, options) {
|
|
|
35
36
|
export function getInstalledCapabilityIds(_config) {
|
|
36
37
|
return [];
|
|
37
38
|
}
|
|
38
|
-
function parseCapabilityMapSource(source) {
|
|
39
|
+
export function parseCapabilityMapSource(source) {
|
|
39
40
|
if (CAPABILITY_SOURCE_FILTERS.has(source)) {
|
|
40
41
|
return source;
|
|
41
42
|
}
|
|
@@ -10,6 +10,7 @@ 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
12
|
import { getSessionId } from '../../services/session/session-manager.js';
|
|
13
|
+
import { verifyPipeline } from '../../services/workflow/pipeline-verify-service.js';
|
|
13
14
|
import { fail, ok } from '../../shared/result.js';
|
|
14
15
|
import { addJsonOption, failUnsupportedNonDryRun, getErrorMessage, isRecommendationWorkflow, printResult } from '../cli-helpers.js';
|
|
15
16
|
function getCurrentWorkspaceContext() {
|
|
@@ -303,6 +304,31 @@ export function registerWorkflowCommands(program, io) {
|
|
|
303
304
|
addAutonomousResumeInitOptions(autonomousResume.command('init')).action((options) => runAutonomousResumeInit(io, options));
|
|
304
305
|
const autonomousResumeAlias = program.command('autonomous-resume').description('Manage autonomous workflow resume artifacts');
|
|
305
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
|
+
});
|
|
306
332
|
const swarm = program.command('swarm').description('Plan RD swarm dry-run graphs');
|
|
307
333
|
addSwarmPlanOptions(swarm.command('plan'), true).action((options) => runSwarmPlan(io, options));
|
|
308
334
|
addSwarmPlanOptions(program.command('swarm-plan'), false).action((options) => runSwarmPlan(io, options));
|
|
@@ -20,7 +20,7 @@ export type SyncResult = {
|
|
|
20
20
|
error?: string;
|
|
21
21
|
};
|
|
22
22
|
export declare function getLocalArtifactPath(workspace: WorkspaceConfig): string;
|
|
23
|
-
export declare function isArtifactWorkspaceOutsideTarget(
|
|
23
|
+
export declare function isArtifactWorkspaceOutsideTarget(_workspace: WorkspaceConfig, _artifactWorkspacePath?: string): boolean;
|
|
24
24
|
export declare function hasValidArtifactWorkspace(workspace: WorkspaceConfig, artifactWorkspacePath?: string): boolean;
|
|
25
25
|
export declare function getArtifactRemoteRepo(workspace: WorkspaceConfig): WorkspaceConfig['artifactRepo'] | null;
|
|
26
26
|
export declare function executeArtifactSync(workspaceId?: string): Promise<SyncResult>;
|
|
@@ -17,10 +17,8 @@ export function getLocalArtifactPath(workspace) {
|
|
|
17
17
|
}
|
|
18
18
|
return resolve(workspace.rootPath, '.peaks', 'artifacts');
|
|
19
19
|
}
|
|
20
|
-
export function isArtifactWorkspaceOutsideTarget(
|
|
21
|
-
|
|
22
|
-
const artifactRoot = canonicalPath(artifactWorkspacePath);
|
|
23
|
-
return !isInsidePath(artifactRoot, targetRoot);
|
|
20
|
+
export function isArtifactWorkspaceOutsideTarget(_workspace, _artifactWorkspacePath) {
|
|
21
|
+
return true;
|
|
24
22
|
}
|
|
25
23
|
export function hasValidArtifactWorkspace(workspace, artifactWorkspacePath = getLocalArtifactPath(workspace)) {
|
|
26
24
|
if (!isArtifactWorkspaceOutsideTarget(workspace, artifactWorkspacePath))
|
|
@@ -29,7 +27,6 @@ export function hasValidArtifactWorkspace(workspace, artifactWorkspacePath = get
|
|
|
29
27
|
const peaksRoot = canonicalChildPath(artifactWorkspacePath, '.peaks');
|
|
30
28
|
const changesRoot = canonicalChildPath(artifactWorkspacePath, '.peaks', 'changes');
|
|
31
29
|
const configPath = canonicalChildPath(artifactWorkspacePath, '.peaks', 'config.json');
|
|
32
|
-
const targetRoot = canonicalPath(workspace.rootPath);
|
|
33
30
|
if (!existsSync(resolve(artifactWorkspacePath, '.peaks', 'config.json')))
|
|
34
31
|
return false;
|
|
35
32
|
if (!isInsidePath(peaksRoot, artifactRoot))
|
|
@@ -38,12 +35,6 @@ export function hasValidArtifactWorkspace(workspace, artifactWorkspacePath = get
|
|
|
38
35
|
return false;
|
|
39
36
|
if (!isInsidePath(configPath, artifactRoot))
|
|
40
37
|
return false;
|
|
41
|
-
if (isInsidePath(peaksRoot, targetRoot))
|
|
42
|
-
return false;
|
|
43
|
-
if (isInsidePath(changesRoot, targetRoot))
|
|
44
|
-
return false;
|
|
45
|
-
if (isInsidePath(configPath, targetRoot))
|
|
46
|
-
return false;
|
|
47
38
|
return true;
|
|
48
39
|
}
|
|
49
40
|
export function getArtifactRemoteRepo(workspace) {
|
|
@@ -6,7 +6,7 @@ export declare function getProjectConfigPath(projectRoot: string | null): string
|
|
|
6
6
|
export declare function getProjectBootstrapConfigPath(projectRoot: string): string;
|
|
7
7
|
export declare function validateProjectBootstrapConfigPathForWrite(projectRoot: string, configPath: string): void;
|
|
8
8
|
export declare function validateUserConfigPathForWrite(configPath: string): void;
|
|
9
|
-
export declare function validateArtifactWorkspaceRoot(artifactRoot: string,
|
|
9
|
+
export declare function validateArtifactWorkspaceRoot(artifactRoot: string, _workspaceRoot: string): void;
|
|
10
10
|
export declare function validateArtifactWorkspaceMarkerPath(artifactRoot: string, peaksPath: string, markerPath: string): void;
|
|
11
11
|
export declare function readConfigFileSafely(configPath: string, errorMessage: string): string;
|
|
12
12
|
export declare function writeConfigFileSafely(configPath: string, content: string, validateBeforeWrite: () => void, errorMessage: string): void;
|
|
@@ -54,6 +54,9 @@ export function findProjectRoot(startPath) {
|
|
|
54
54
|
if (existsSync(resolve(current, '.peaks', 'config.json')) && isSafeProjectConfigMarker(current)) {
|
|
55
55
|
return current;
|
|
56
56
|
}
|
|
57
|
+
if (existsSync(resolve(current, 'package.json')) || existsSync(resolve(current, '.git'))) {
|
|
58
|
+
return current;
|
|
59
|
+
}
|
|
57
60
|
parent = current;
|
|
58
61
|
current = dirname(parent);
|
|
59
62
|
}
|
|
@@ -154,16 +157,11 @@ export function validateUserConfigPathForWrite(configPath) {
|
|
|
154
157
|
}
|
|
155
158
|
}
|
|
156
159
|
}
|
|
157
|
-
export function validateArtifactWorkspaceRoot(artifactRoot,
|
|
160
|
+
export function validateArtifactWorkspaceRoot(artifactRoot, _workspaceRoot) {
|
|
158
161
|
const artifactStats = lstatSync(artifactRoot);
|
|
159
162
|
if (!artifactStats.isDirectory() || artifactStats.isSymbolicLink()) {
|
|
160
163
|
throw new Error('Artifact workspace marker must stay inside the artifact workspace');
|
|
161
164
|
}
|
|
162
|
-
const artifactRootReal = realpathSync(artifactRoot);
|
|
163
|
-
const workspaceRootReal = realpathSync(workspaceRoot);
|
|
164
|
-
if (isInsidePath(artifactRootReal, workspaceRootReal)) {
|
|
165
|
-
throw new Error('Artifact workspace must stay outside the project root');
|
|
166
|
-
}
|
|
167
165
|
}
|
|
168
166
|
export function validateArtifactWorkspaceMarkerPath(artifactRoot, peaksPath, markerPath) {
|
|
169
167
|
const artifactStats = lstatSync(artifactRoot);
|
|
@@ -23,7 +23,7 @@ export declare function readConfig(projectRoot?: string | null): PeaksConfig;
|
|
|
23
23
|
export declare function writeConfig(partial: Partial<PeaksConfig>, layer?: ConfigLayer): void;
|
|
24
24
|
export declare function getConfig(options?: ConfigGetOptions): unknown;
|
|
25
25
|
export declare function setConfig(options: ConfigSetOptions): void;
|
|
26
|
-
export declare function getWorkspaceConfig(workspaceId: string,
|
|
26
|
+
export declare function getWorkspaceConfig(workspaceId: string, _projectRoot?: string | null): WorkspaceConfig | null;
|
|
27
27
|
export declare function addWorkspace(workspace: WorkspaceConfig, layer?: ConfigLayer): void;
|
|
28
28
|
export declare function removeWorkspace(workspaceId: string, layer?: ConfigLayer): boolean;
|
|
29
29
|
export declare function setCurrentWorkspace(workspaceId: string, layer?: ConfigLayer): boolean;
|
|
@@ -530,8 +530,21 @@ function writeRawWorkspaceData(data, layer) {
|
|
|
530
530
|
writeUserConfigFile(targetPath, content);
|
|
531
531
|
}
|
|
532
532
|
}
|
|
533
|
-
|
|
534
|
-
const
|
|
533
|
+
function readAllWorkspaces() {
|
|
534
|
+
const userData = readRawWorkspaceData('user');
|
|
535
|
+
const projectData = readRawWorkspaceData('project');
|
|
536
|
+
const mergedWorkspaces = new Map();
|
|
537
|
+
for (const w of userData.workspaces)
|
|
538
|
+
mergedWorkspaces.set(w.workspaceId, w);
|
|
539
|
+
for (const w of projectData.workspaces)
|
|
540
|
+
mergedWorkspaces.set(w.workspaceId, w);
|
|
541
|
+
return {
|
|
542
|
+
currentWorkspace: projectData.currentWorkspace ?? userData.currentWorkspace,
|
|
543
|
+
workspaces: [...mergedWorkspaces.values()]
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
export function getWorkspaceConfig(workspaceId, _projectRoot) {
|
|
547
|
+
const { workspaces } = readAllWorkspaces();
|
|
535
548
|
return workspaces.find((w) => w.workspaceId === workspaceId) ?? null;
|
|
536
549
|
}
|
|
537
550
|
function readLayerConfig(layer) {
|
|
@@ -574,13 +587,13 @@ export function setCurrentWorkspace(workspaceId, layer = 'user') {
|
|
|
574
587
|
return true;
|
|
575
588
|
}
|
|
576
589
|
export function getCurrentWorkspaceConfig() {
|
|
577
|
-
const { currentWorkspace, workspaces } =
|
|
590
|
+
const { currentWorkspace, workspaces } = readAllWorkspaces();
|
|
578
591
|
if (!currentWorkspace)
|
|
579
592
|
return null;
|
|
580
593
|
return workspaces.find((w) => w.workspaceId === currentWorkspace) ?? null;
|
|
581
594
|
}
|
|
582
595
|
export function getWorkspaceConfigForPath(path = process.cwd()) {
|
|
583
|
-
const { workspaces } =
|
|
596
|
+
const { workspaces } = readAllWorkspaces();
|
|
584
597
|
return findWorkspaceForPath(workspaces, path);
|
|
585
598
|
}
|
|
586
599
|
function findWorkspaceForPath(workspaces, path) {
|
|
@@ -9,16 +9,13 @@ function extractAcceptanceItems(prdBody) {
|
|
|
9
9
|
return [];
|
|
10
10
|
}
|
|
11
11
|
// Find the line where the header starts.
|
|
12
|
-
let headerLine =
|
|
12
|
+
let headerLine = 0;
|
|
13
13
|
for (let i = 0; i < lines.length; i += 1) {
|
|
14
14
|
if (ACCEPTANCE_SECTION_PATTERN.test((lines[i] ?? '') + '\n')) {
|
|
15
15
|
headerLine = i;
|
|
16
16
|
break;
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
|
-
if (headerLine === -1) {
|
|
20
|
-
return [];
|
|
21
|
-
}
|
|
22
19
|
const items = [];
|
|
23
20
|
let counter = 0;
|
|
24
21
|
for (let i = headerLine + 1; i < lines.length; i += 1) {
|
|
@@ -101,13 +101,7 @@ async function countSrcFiles(projectRoot, max = 500) {
|
|
|
101
101
|
const current = queue.shift();
|
|
102
102
|
if (current === undefined)
|
|
103
103
|
break;
|
|
104
|
-
|
|
105
|
-
try {
|
|
106
|
-
entries = await readdir(current, { withFileTypes: true });
|
|
107
|
-
}
|
|
108
|
-
catch {
|
|
109
|
-
continue;
|
|
110
|
-
}
|
|
104
|
+
const entries = await readdir(current, { withFileTypes: true });
|
|
111
105
|
for (const entry of entries) {
|
|
112
106
|
if (entry.name.startsWith('.') || entry.name === 'node_modules')
|
|
113
107
|
continue;
|
|
@@ -129,14 +123,9 @@ async function lockfileAgeDays(projectRoot) {
|
|
|
129
123
|
for (const candidate of candidates) {
|
|
130
124
|
const full = join(projectRoot, candidate);
|
|
131
125
|
if (await pathExists(full)) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
return Math.floor(ageMs / (1000 * 60 * 60 * 24));
|
|
136
|
-
}
|
|
137
|
-
catch {
|
|
138
|
-
return null;
|
|
139
|
-
}
|
|
126
|
+
const stats = await stat(full);
|
|
127
|
+
const ageMs = Date.now() - stats.mtimeMs;
|
|
128
|
+
return Math.floor(ageMs / (1000 * 60 * 60 * 24));
|
|
140
129
|
}
|
|
141
130
|
}
|
|
142
131
|
return null;
|
|
@@ -57,9 +57,9 @@ export function globToRegex(pattern) {
|
|
|
57
57
|
}
|
|
58
58
|
// If the pattern ends with no trailing slash and no extension wildcard, also allow it to match files under the path (treat as dir prefix)
|
|
59
59
|
// E.g. `src/services/login` should match `src/services/login/handler.ts`.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
body = (!trimmed.includes("*") && !trimmed.includes("?") && !trimmed.includes("."))
|
|
61
|
+
? `${body}(?:/.*)?`
|
|
62
|
+
: body;
|
|
63
63
|
return new RegExp(`^${body}$`);
|
|
64
64
|
}
|
|
65
65
|
function classifyPatternLine(raw) {
|
|
@@ -15,13 +15,8 @@ function getChangedFiles(projectRoot, baseRef) {
|
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
17
|
function countLines(filePath) {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
return content.split(/\r?\n/).length;
|
|
21
|
-
}
|
|
22
|
-
catch {
|
|
23
|
-
return 0;
|
|
24
|
-
}
|
|
18
|
+
const content = readFileSync(filePath, 'utf8');
|
|
19
|
+
return content.split(/\r?\n/).length;
|
|
25
20
|
}
|
|
26
21
|
export function scanFileSize(options) {
|
|
27
22
|
const baseRef = options.baseRef ?? 'HEAD';
|
|
@@ -78,9 +78,7 @@ function isConsistent(declared, suggested) {
|
|
|
78
78
|
return suggested.includes(declared);
|
|
79
79
|
}
|
|
80
80
|
function buildRationale(declared, breakdown, suggested, consistent) {
|
|
81
|
-
const summary = breakdown.
|
|
82
|
-
? 'no changed files detected'
|
|
83
|
-
: breakdown.map((entry) => `${entry.category}=${entry.count}`).join(', ');
|
|
81
|
+
const summary = breakdown.map((entry) => `${entry.category}=${entry.count}`).join(', ');
|
|
84
82
|
if (consistent) {
|
|
85
83
|
return `declared --type=${declared} is consistent with the changed files (${summary})`;
|
|
86
84
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { RequestType } from '../artifacts/artifact-prerequisites.js';
|
|
2
|
+
export type PipelineGate = {
|
|
3
|
+
name: string;
|
|
4
|
+
description: string;
|
|
5
|
+
passed: boolean;
|
|
6
|
+
detail: string;
|
|
7
|
+
};
|
|
8
|
+
export type PipelineVerification = {
|
|
9
|
+
rid: string;
|
|
10
|
+
sessionId: string;
|
|
11
|
+
requestType: RequestType;
|
|
12
|
+
complete: boolean;
|
|
13
|
+
rdPhase: {
|
|
14
|
+
invoked: boolean;
|
|
15
|
+
state: string;
|
|
16
|
+
gates: PipelineGate[];
|
|
17
|
+
};
|
|
18
|
+
qaPhase: {
|
|
19
|
+
invoked: boolean;
|
|
20
|
+
state: string;
|
|
21
|
+
gates: PipelineGate[];
|
|
22
|
+
};
|
|
23
|
+
violations: string[];
|
|
24
|
+
nextActions: string[];
|
|
25
|
+
};
|
|
26
|
+
export declare function verifyPipeline(options: {
|
|
27
|
+
projectRoot: string;
|
|
28
|
+
rid: string;
|
|
29
|
+
sessionId: string;
|
|
30
|
+
requestType?: string;
|
|
31
|
+
}): Promise<PipelineVerification>;
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
4
|
+
import { isRequestType } from '../artifacts/artifact-prerequisites.js';
|
|
5
|
+
async function readFileContent(path) {
|
|
6
|
+
try {
|
|
7
|
+
return await readFile(path, 'utf8');
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function extractState(markdown) {
|
|
14
|
+
for (const rawLine of markdown.split(/\r?\n/)) {
|
|
15
|
+
const match = /^-\s*state:\s*(.+?)\s*$/.exec(rawLine.trim());
|
|
16
|
+
if (match?.[1])
|
|
17
|
+
return match[1];
|
|
18
|
+
}
|
|
19
|
+
return 'unknown';
|
|
20
|
+
}
|
|
21
|
+
async function findRequestFile(projectRoot, sessionId, role, rid) {
|
|
22
|
+
const dir = join(projectRoot, '.peaks', sessionId, role, 'requests');
|
|
23
|
+
if (!existsSync(dir))
|
|
24
|
+
return null;
|
|
25
|
+
const { readdir } = await import('node:fs/promises');
|
|
26
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
27
|
+
for (const entry of entries) {
|
|
28
|
+
if (!entry.isFile() || !entry.name.endsWith('.md'))
|
|
29
|
+
continue;
|
|
30
|
+
if (entry.name === `${rid}.md` || (/^\d+-/.test(entry.name) && entry.name.endsWith(`-${rid}.md`))) {
|
|
31
|
+
const path = join(dir, entry.name);
|
|
32
|
+
const content = await readFileContent(path);
|
|
33
|
+
if (content)
|
|
34
|
+
return { path, content };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
function rdGatesForType(requestType) {
|
|
40
|
+
const gates = [
|
|
41
|
+
{ name: 'rd-request-exists', description: 'RD request artifact created', passed: false, detail: '' }
|
|
42
|
+
];
|
|
43
|
+
if (requestType === 'feature' || requestType === 'refactor') {
|
|
44
|
+
gates.push({ name: 'tech-doc', description: 'Technical design doc', passed: false, detail: '' });
|
|
45
|
+
}
|
|
46
|
+
if (requestType === 'bugfix') {
|
|
47
|
+
gates.push({ name: 'bug-analysis', description: 'Bug root-cause analysis', passed: false, detail: '' });
|
|
48
|
+
}
|
|
49
|
+
if (requestType !== 'docs' && requestType !== 'chore' && requestType !== 'config') {
|
|
50
|
+
gates.push({ name: 'code-review', description: 'Code review evidence', passed: false, detail: '' });
|
|
51
|
+
}
|
|
52
|
+
if (requestType === 'feature' || requestType === 'refactor' || requestType === 'bugfix' || requestType === 'config') {
|
|
53
|
+
gates.push({ name: 'security-review', description: 'Security review evidence', passed: false, detail: '' });
|
|
54
|
+
}
|
|
55
|
+
return gates;
|
|
56
|
+
}
|
|
57
|
+
function qaGatesForType(requestType) {
|
|
58
|
+
const gates = [
|
|
59
|
+
{ name: 'qa-request-exists', description: 'QA request artifact created', passed: false, detail: '' }
|
|
60
|
+
];
|
|
61
|
+
if (requestType === 'feature' || requestType === 'refactor' || requestType === 'bugfix') {
|
|
62
|
+
gates.push({ name: 'test-cases', description: 'QA test cases', passed: false, detail: '' });
|
|
63
|
+
gates.push({ name: 'test-report', description: 'QA test report with execution results', passed: false, detail: '' });
|
|
64
|
+
}
|
|
65
|
+
if (requestType === 'feature' || requestType === 'refactor' || requestType === 'bugfix' || requestType === 'config') {
|
|
66
|
+
gates.push({ name: 'security-findings', description: 'QA security findings', passed: false, detail: '' });
|
|
67
|
+
}
|
|
68
|
+
if (requestType === 'feature' || requestType === 'refactor') {
|
|
69
|
+
gates.push({ name: 'performance-findings', description: 'QA performance findings', passed: false, detail: '' });
|
|
70
|
+
}
|
|
71
|
+
return gates;
|
|
72
|
+
}
|
|
73
|
+
const RD_QA_HANDOFF_STATES = new Set(['qa-handoff', 'handed-off', 'implemented']);
|
|
74
|
+
const QA_COMPLETE_STATES = new Set(['verdict-issued']);
|
|
75
|
+
export async function verifyPipeline(options) {
|
|
76
|
+
const requestType = isRequestType(options.requestType ?? '') ? options.requestType : 'feature';
|
|
77
|
+
const violations = [];
|
|
78
|
+
const nextActions = [];
|
|
79
|
+
const rdGates = rdGatesForType(requestType);
|
|
80
|
+
const qaGates = qaGatesForType(requestType);
|
|
81
|
+
// Check RD phase
|
|
82
|
+
const rdFile = await findRequestFile(options.projectRoot, options.sessionId, 'rd', options.rid);
|
|
83
|
+
let rdInvoked = false;
|
|
84
|
+
let rdState = 'missing';
|
|
85
|
+
if (rdFile) {
|
|
86
|
+
rdInvoked = true;
|
|
87
|
+
rdState = extractState(rdFile.content);
|
|
88
|
+
rdGates[0].passed = true;
|
|
89
|
+
rdGates[0].detail = `found at ${rdFile.path}`;
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
violations.push('RD phase skipped: peaks-rd was never invoked for this request (no RD request artifact found)');
|
|
93
|
+
nextActions.push('Invoke Skill(skill="peaks-rd") with the request-id, then run unit tests + code review + security review');
|
|
94
|
+
rdGates[0].detail = 'not found';
|
|
95
|
+
}
|
|
96
|
+
// Check RD evidence files
|
|
97
|
+
const RD_EVIDENCE_FILE = {
|
|
98
|
+
'tech-doc': 'tech-doc.md',
|
|
99
|
+
'bug-analysis': 'bug-analysis.md',
|
|
100
|
+
'code-review': 'code-review.md',
|
|
101
|
+
'security-review': 'security-review.md'
|
|
102
|
+
};
|
|
103
|
+
for (const gate of rdGates.slice(1)) {
|
|
104
|
+
const fileName = RD_EVIDENCE_FILE[gate.name];
|
|
105
|
+
const evidencePath = join(options.projectRoot, '.peaks', options.sessionId, 'rd', fileName);
|
|
106
|
+
if (existsSync(evidencePath)) {
|
|
107
|
+
gate.passed = true;
|
|
108
|
+
gate.detail = evidencePath;
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
gate.detail = `missing: ${evidencePath}`;
|
|
112
|
+
violations.push(`RD evidence missing: ${gate.description} (${fileName})`);
|
|
113
|
+
nextActions.push(`Create .peaks/${options.sessionId}/rd/${fileName}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// Check if RD reached qa-handoff
|
|
117
|
+
if (rdInvoked && !RD_QA_HANDOFF_STATES.has(rdState)) {
|
|
118
|
+
violations.push(`RD not ready for QA: state is "${rdState}" — must reach "qa-handoff" (unit tests, karpathy standards, code review, security review complete)`);
|
|
119
|
+
nextActions.push(`Complete RD gates → peaks request transition ${options.rid} --role rd --state qa-handoff`);
|
|
120
|
+
}
|
|
121
|
+
// Check QA phase
|
|
122
|
+
const qaFile = await findRequestFile(options.projectRoot, options.sessionId, 'qa', options.rid);
|
|
123
|
+
let qaInvoked = false;
|
|
124
|
+
let qaState = 'missing';
|
|
125
|
+
if (qaFile) {
|
|
126
|
+
qaInvoked = true;
|
|
127
|
+
qaState = extractState(qaFile.content);
|
|
128
|
+
qaGates[0].passed = true;
|
|
129
|
+
qaGates[0].detail = `found at ${qaFile.path}`;
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
violations.push('QA phase skipped: peaks-qa was never invoked for this request (no QA request artifact found)');
|
|
133
|
+
nextActions.push('Invoke Skill(skill="peaks-qa") with the request-id for functional/performance/security testing');
|
|
134
|
+
qaGates[0].detail = 'not found';
|
|
135
|
+
}
|
|
136
|
+
// Check QA evidence files
|
|
137
|
+
const QA_EVIDENCE_FILE = {
|
|
138
|
+
'test-cases': `test-cases/${options.rid}.md`,
|
|
139
|
+
'test-report': `test-reports/${options.rid}.md`,
|
|
140
|
+
'security-findings': 'security-findings.md',
|
|
141
|
+
'performance-findings': 'performance-findings.md'
|
|
142
|
+
};
|
|
143
|
+
for (const gate of qaGates.slice(1)) {
|
|
144
|
+
const fileName = QA_EVIDENCE_FILE[gate.name];
|
|
145
|
+
const evidencePath = join(options.projectRoot, '.peaks', options.sessionId, 'qa', fileName);
|
|
146
|
+
if (existsSync(evidencePath)) {
|
|
147
|
+
gate.passed = true;
|
|
148
|
+
gate.detail = evidencePath;
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
gate.detail = `missing: ${evidencePath}`;
|
|
152
|
+
violations.push(`QA evidence missing: ${gate.description} (${fileName})`);
|
|
153
|
+
nextActions.push(`Create .peaks/${options.sessionId}/qa/${fileName}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// Check if QA reached verdict-issued
|
|
157
|
+
if (qaInvoked && !QA_COMPLETE_STATES.has(qaState)) {
|
|
158
|
+
violations.push(`QA not complete: state is "${qaState}" — must reach "verdict-issued" (functional + performance + security checks done)`);
|
|
159
|
+
nextActions.push(`Complete QA gates → peaks request transition ${options.rid} --role qa --state verdict-issued`);
|
|
160
|
+
}
|
|
161
|
+
// RD invoked without QA
|
|
162
|
+
if (rdInvoked && !qaInvoked) {
|
|
163
|
+
violations.push('CRITICAL: peaks-rd was invoked but peaks-qa was NOT — QA functional/performance/security testing is mandatory after all RD work');
|
|
164
|
+
nextActions.push('MUST invoke Skill(skill="peaks-qa") before declaring workflow complete');
|
|
165
|
+
}
|
|
166
|
+
const allRdGatesPassed = rdGates.every((g) => g.passed);
|
|
167
|
+
const allQaGatesPassed = qaGates.every((g) => g.passed);
|
|
168
|
+
const complete = rdInvoked && qaInvoked && allRdGatesPassed && allQaGatesPassed
|
|
169
|
+
&& RD_QA_HANDOFF_STATES.has(rdState) && QA_COMPLETE_STATES.has(qaState);
|
|
170
|
+
return {
|
|
171
|
+
rid: options.rid,
|
|
172
|
+
sessionId: options.sessionId,
|
|
173
|
+
requestType,
|
|
174
|
+
complete,
|
|
175
|
+
rdPhase: { invoked: rdInvoked, state: rdState, gates: rdGates },
|
|
176
|
+
qaPhase: { invoked: qaInvoked, state: qaState, gates: qaGates },
|
|
177
|
+
violations,
|
|
178
|
+
nextActions
|
|
179
|
+
};
|
|
180
|
+
}
|
|
@@ -85,9 +85,6 @@ export function buildArtifactRelativePath(changeId, ...segments) {
|
|
|
85
85
|
const number = getNextNumber(dirPath);
|
|
86
86
|
const filename = buildNumberedFilename(number, changeId);
|
|
87
87
|
const candidatePath = `.peaks/${sessionId}/${role}/${filename}`;
|
|
88
|
-
if (isUnsafeArtifactPath(candidatePath)) {
|
|
89
|
-
throw new ChangeIdValidationError(changeId);
|
|
90
|
-
}
|
|
91
88
|
return normalizeArtifactPath(candidatePath);
|
|
92
89
|
}
|
|
93
90
|
// Fallback: no session or no segments - use legacy behavior
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const CLI_VERSION = "1.0.
|
|
1
|
+
export declare const CLI_VERSION = "1.0.23";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const CLI_VERSION = "1.0.
|
|
1
|
+
export const CLI_VERSION = "1.0.23";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "peaks-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.23",
|
|
4
4
|
"description": "Peaks CLI and short skill family for Claude Code automation.",
|
|
5
5
|
"author": "SquabbyZ",
|
|
6
6
|
"license": "MIT",
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"dev:watch": "node ./scripts/watch.mjs",
|
|
36
36
|
"test": "vitest run",
|
|
37
37
|
"test:coverage": "vitest run --coverage",
|
|
38
|
+
"pretest:coverage": "pkill -f vitest 2>/dev/null; rm -rf coverage; exit 0",
|
|
38
39
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
39
40
|
},
|
|
40
41
|
"engines": {
|
|
@@ -9,6 +9,39 @@ Peaks Solo is the orchestration facade for the Peaks short skill family.
|
|
|
9
9
|
|
|
10
10
|
Use this skill to identify the user scenario, recommend an execution mode, coordinate role skills, and produce the final handoff report. Do not collapse role responsibilities into this skill.
|
|
11
11
|
|
|
12
|
+
## Code-Change Red Line (BLOCKING — read before ANY tool call)
|
|
13
|
+
|
|
14
|
+
**Peaks Solo is an orchestrator, NOT an implementer. You MUST NOT write, edit, or modify any application source code directly.**
|
|
15
|
+
|
|
16
|
+
Every code change — bugfix, feature, refactor, or config — MUST go through the full pipeline:
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
peaks-solo (orchestrate only)
|
|
20
|
+
→ Skill(skill="peaks-rd") ← ALL code changes happen HERE
|
|
21
|
+
→ Unit tests written + pass (Gate B2)
|
|
22
|
+
→ Karpathy standards enforced (file-size ≤800 lines, TypeScript rules)
|
|
23
|
+
→ Code review evidence (Gate B3)
|
|
24
|
+
→ Security review evidence (Gate B4)
|
|
25
|
+
→ Skill(skill="peaks-qa") ← ALL validation happens HERE
|
|
26
|
+
→ Functional test execution (Gate A2)
|
|
27
|
+
→ Performance check (Gate A4)
|
|
28
|
+
→ Security test (Gate A3)
|
|
29
|
+
→ Browser E2E (when frontend; Gate D)
|
|
30
|
+
→ Verdict: pass | return-to-rd | blocked
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**Violations (BLOCKING — Solo must refuse to proceed):**
|
|
34
|
+
|
|
35
|
+
1. Writing implementation code directly instead of calling `Skill(skill="peaks-rd")`
|
|
36
|
+
2. Declaring work "done" without invoking `Skill(skill="peaks-qa")` after RD
|
|
37
|
+
3. Skipping unit tests ("it's a small change")
|
|
38
|
+
4. Skipping code review or security review
|
|
39
|
+
5. Skipping QA functional/performance/security validation
|
|
40
|
+
|
|
41
|
+
**If you catch yourself about to write code in this skill, STOP. Call `Skill(skill="peaks-rd")` instead.**
|
|
42
|
+
|
|
43
|
+
**Before declaring workflow complete, run:** `peaks workflow verify-pipeline --rid <rid> --project <repo> --json`
|
|
44
|
+
|
|
12
45
|
## Startup sequence (MANDATORY — execute in order)
|
|
13
46
|
|
|
14
47
|
### Step 1: Mode selection (MUST run before presence:set)
|