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
@@ -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
+ }
@@ -15,6 +15,8 @@ const SUBDIRECTORIES = [
15
15
  ];
16
16
  const SESSION_ID_PATTERN = /^\d{4}-\d{2}-\d{2}-[a-z][a-z0-9-]*[a-z0-9]$/;
17
17
  const PROHIBITED_SUFFIXES = ['session', 'work', 'task', 'test', 'temp', 'tmp'];
18
+ // Auto-generated session ID pattern: YYYY-MM-DD-session-<6位hex>
19
+ const AUTO_SESSION_PATTERN = /^\d{4}-\d{2}-\d{2}-session-[a-f0-9]{6}$/;
18
20
  export class InvalidSessionIdError extends Error {
19
21
  code = 'INVALID_SESSION_ID';
20
22
  constructor(message) {
@@ -23,6 +25,10 @@ export class InvalidSessionIdError extends Error {
23
25
  }
24
26
  }
25
27
  export function validateSessionId(sessionId) {
28
+ // Auto-generated session IDs (YYYY-MM-DD-session-<hex>) bypass manual validation
29
+ if (AUTO_SESSION_PATTERN.test(sessionId)) {
30
+ return;
31
+ }
26
32
  if (/^\d+$/.test(sessionId)) {
27
33
  throw new InvalidSessionIdError(`Session id "${sessionId}" is numeric-only. Use the format YYYY-MM-DD-<kebab-slug> with a 2-5 word topic description.`);
28
34
  }
@@ -11,5 +11,18 @@ export declare class ChangeIdValidationError extends Error {
11
11
  constructor(changeId: string);
12
12
  }
13
13
  export declare function isUnsafeArtifactPath(path: string): boolean;
14
+ /**
15
+ * Build an artifact-relative path using session-based storage.
16
+ *
17
+ * If a session exists, files are stored in:
18
+ * .peaks/<sessionId>/<role>/<number>-<changeId>.md
19
+ *
20
+ * If no session exists, falls back to legacy behavior:
21
+ * .peaks/<changeId>/<segments>
22
+ *
23
+ * @param changeId - Used as file description/slug (e.g., "auth-system", "add-user-auth")
24
+ * @param segments - Optional path segments (first segment is typically the role: 'prd', 'rd', 'qa', etc.)
25
+ * @returns Relative path to the artifact file
26
+ */
14
27
  export declare function buildArtifactRelativePath(changeId: string, ...segments: string[]): string;
15
28
  export declare function isPathInsideArtifactRoot(path: string, artifactRoot: string): boolean;
@@ -3,7 +3,9 @@
3
3
  * All Peaks planner commands must use these to prevent path traversal
4
4
  * and keep artifacts inside the Peaks artifact workspace.
5
5
  */
6
- import { posix } from 'node:path';
6
+ import { posix, join } from 'node:path';
7
+ import { getNextNumber, buildNumberedFilename } from './incrementing-number.js';
8
+ import { getSessionId } from '../services/session/session-manager.js';
7
9
  const CHANGE_ID_PATTERN = /^[A-Za-z0-9._-]+$/;
8
10
  function normalizeForwardSlashes(input) {
9
11
  return input.replace(/\\/g, '/');
@@ -58,8 +60,34 @@ export class ChangeIdValidationError extends Error {
58
60
  export function isUnsafeArtifactPath(path) {
59
61
  return isUnsafePathInput(path);
60
62
  }
63
+ /**
64
+ * Build an artifact-relative path using session-based storage.
65
+ *
66
+ * If a session exists, files are stored in:
67
+ * .peaks/<sessionId>/<role>/<number>-<changeId>.md
68
+ *
69
+ * If no session exists, falls back to legacy behavior:
70
+ * .peaks/<changeId>/<segments>
71
+ *
72
+ * @param changeId - Used as file description/slug (e.g., "auth-system", "add-user-auth")
73
+ * @param segments - Optional path segments (first segment is typically the role: 'prd', 'rd', 'qa', etc.)
74
+ * @returns Relative path to the artifact file
75
+ */
61
76
  export function buildArtifactRelativePath(changeId, ...segments) {
62
77
  validateChangeIdOrThrow(changeId);
78
+ const sessionId = getSessionId(process.cwd());
79
+ if (sessionId && segments.length > 0 && segments[0]) {
80
+ const role = normalizeForwardSlashes(segments[0]);
81
+ const dirPath = join(process.cwd(), '.peaks', sessionId, role);
82
+ if (isUnsafeArtifactPath(role) || isUnsafeArtifactPath(sessionId)) {
83
+ throw new ChangeIdValidationError(changeId);
84
+ }
85
+ const number = getNextNumber(dirPath);
86
+ const filename = buildNumberedFilename(number, changeId);
87
+ const candidatePath = `.peaks/${sessionId}/${role}/${filename}`;
88
+ return normalizeArtifactPath(candidatePath);
89
+ }
90
+ // Fallback: no session or no segments - use legacy behavior
63
91
  const joined = segments.map((segment) => normalizeForwardSlashes(segment)).join('/');
64
92
  const candidatePath = `.peaks/${changeId}/${joined}`;
65
93
  if (isUnsafeArtifactPath(joined) || isUnsafeArtifactPath(candidatePath)) {
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Utilities for generating incrementing numbered filenames.
3
+ * Used by session-based artifact storage to create files like 001-feature.md.
4
+ */
5
+ /**
6
+ * Get the next available number in a directory.
7
+ * Scans existing .md files and finds the highest numeric prefix.
8
+ * Returns 1 if directory is empty or doesn't exist.
9
+ *
10
+ * @param dirPath - Directory to scan for numbered files
11
+ * @returns Next available number (1, 2, 3, ...)
12
+ */
13
+ export declare function getNextNumber(dirPath: string): number;
14
+ /**
15
+ * Build a numbered filename from a number and description.
16
+ * Format: 001-description-slug.md
17
+ *
18
+ * @param number - The file number (will be zero-padded to 3 digits)
19
+ * @param description - Human-readable description (converted to kebab-case slug)
20
+ * @returns Formatted filename like "001-feature-name.md"
21
+ */
22
+ export declare function buildNumberedFilename(number: number, description: string): string;
23
+ /**
24
+ * Get the next numbered file path in a directory.
25
+ * Combines getNextNumber() and buildNumberedFilename().
26
+ *
27
+ * @param dirPath - Directory to scan
28
+ * @param description - Description for the filename
29
+ * @returns Full path to the new numbered file
30
+ */
31
+ export declare function getNextNumberedFilePath(dirPath: string, description: string): string;
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Utilities for generating incrementing numbered filenames.
3
+ * Used by session-based artifact storage to create files like 001-feature.md.
4
+ */
5
+ import { existsSync, readdirSync } from 'node:fs';
6
+ import { join } from 'node:path';
7
+ /**
8
+ * Get the next available number in a directory.
9
+ * Scans existing .md files and finds the highest numeric prefix.
10
+ * Returns 1 if directory is empty or doesn't exist.
11
+ *
12
+ * @param dirPath - Directory to scan for numbered files
13
+ * @returns Next available number (1, 2, 3, ...)
14
+ */
15
+ export function getNextNumber(dirPath) {
16
+ if (!existsSync(dirPath))
17
+ return 1;
18
+ const files = readdirSync(dirPath).filter(f => f.endsWith('.md'));
19
+ if (files.length === 0)
20
+ return 1;
21
+ const numbers = files
22
+ .map(f => {
23
+ const match = /^(\d+)-/.exec(f);
24
+ return match && match[1] ? parseInt(match[1], 10) : NaN;
25
+ })
26
+ .filter(n => !isNaN(n));
27
+ return numbers.length > 0 ? Math.max(...numbers) + 1 : 1;
28
+ }
29
+ /**
30
+ * Build a numbered filename from a number and description.
31
+ * Format: 001-description-slug.md
32
+ *
33
+ * @param number - The file number (will be zero-padded to 3 digits)
34
+ * @param description - Human-readable description (converted to kebab-case slug)
35
+ * @returns Formatted filename like "001-feature-name.md"
36
+ */
37
+ export function buildNumberedFilename(number, description) {
38
+ const padded = String(number).padStart(3, '0');
39
+ const slug = description
40
+ .toLowerCase()
41
+ .replace(/[^a-z0-9]+/g, '-')
42
+ .replace(/^-|-$/g, '')
43
+ .slice(0, 50); // Limit slug length
44
+ return `${padded}-${slug}.md`;
45
+ }
46
+ /**
47
+ * Get the next numbered file path in a directory.
48
+ * Combines getNextNumber() and buildNumberedFilename().
49
+ *
50
+ * @param dirPath - Directory to scan
51
+ * @param description - Description for the filename
52
+ * @returns Full path to the new numbered file
53
+ */
54
+ export function getNextNumberedFilePath(dirPath, description) {
55
+ const number = getNextNumber(dirPath);
56
+ const filename = buildNumberedFilename(number, description);
57
+ return join(dirPath, filename);
58
+ }
@@ -1 +1 @@
1
- export declare const CLI_VERSION = "1.0.21";
1
+ export declare const CLI_VERSION = "1.0.23";
@@ -1 +1 @@
1
- export const CLI_VERSION = "1.0.21";
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.21",
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": {
@@ -68,6 +68,9 @@ peaks codegraph affected --project <repo> <changed-files...> --json
68
68
  # **STOP if .peaks/<session-id>/rd/project-scan.md does not exist.**
69
69
  # **Do not write any code, do not plan any implementation, do not pass go.**
70
70
  # **Create the project-scan first, then proceed.**
71
+ # NOTE: project-scan.md is a session-scoped singleton. Check if it already exists
72
+ # before regenerating (e.g. via `ls .peaks/<id>/rd/project-scan.md`). If it exists
73
+ # and is complete (has `## Archetype` and `## Project mode` sections), reuse it.
71
74
  # Required sections in project-scan:
72
75
  # - build tool and framework
73
76
  # - component library (antd, MUI, shadcn, etc.) and version
@@ -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)
@@ -77,27 +110,24 @@ For frontend workflows, RD and QA must use Playwright MCP (`mcp__playwright__` t
77
110
 
78
111
  ### Workspace initialization gate
79
112
 
80
- Before ANY role handoff or artifact write, Peaks Solo MUST create the workspace. The session-id uses the format `YYYY-MM-DD-<kebab-slug>` where `<kebab-slug>` is the **request-id or a 2-5 word topic description** (e.g. `2026-05-25-v3-indicator-model`, `2026-05-25-add-user-auth`).
113
+ Before ANY role handoff or artifact write, Peaks Solo MUST create the workspace. Session IDs are now **auto-generated** with the format `YYYY-MM-DD-session-<6位hex>` (e.g. `2026-05-26-session-a3f8b1`). The user does not provide a session ID — the system creates and persists it in `.peaks/.session.json`.
81
114
 
82
- **PROHIBITED session-id patterns** block the workflow if any of these appear:
83
- - Numeric-only: `1779674289`, `1779672642`
84
- - Generic suffixes: `session`, `work`, `task`, `test`, `temp`, `tmp`
85
- - Bare dates without topic: `2026-05-25`
86
- - Timestamps: `20260525T093000`
115
+ When `peaks workspace init` is run without `--session-id`, it automatically generates a new session ID using today's date and a random hex suffix. If `.peaks/.session.json` already exists with a valid session, the existing session is reused.
87
116
 
88
- **Existing old-session cleanup**: If `.peaks/` contains numeric-only or generic session directories from prior runs, create the new correctly-named session, migrate any reusable artifacts into it, and note the migration in the TXT handoff. Delete empty old-session directories.
117
+ **Existing old-session cleanup**: If `.peaks/` contains numeric-only or generic session directories from prior runs (e.g. `2026-05-25-auth-system`), create the new correctly-named session, migrate any reusable artifacts into it, and note the migration in the TXT handoff. Delete empty old-session directories.
89
118
 
90
119
  ```bash
91
- peaks workspace init --project <repo> --session-id <YYYY-MM-DD-<slug>> --json
120
+ peaks workspace init --project <repo> --json
92
121
  ```
93
122
 
94
- The workspace initialization creates this structure under `.peaks/<session-id>/`:
123
+ The workspace initialization creates this structure under `.peaks/<session-id>/` (where `<session-id>` is auto-generated as `YYYY-MM-DD-session-<6位hex>`):
95
124
 
96
125
  ```
97
126
  prd/source/ # PRD source documents (Feishu exports, pasted content)
98
127
  prd/requests/ # PRD request artifacts (goals, non-goals, acceptance, frontend delta)
99
128
  ui/requests/ # UI request artifacts (visual direction, taste reports)
100
129
  rd/requests/ # RD request artifacts (slice specs, coverage, CR findings)
130
+ rd/project-scan.md # Project scan (session-scoped singleton, generated once per session)
101
131
  qa/test-cases/ # QA test cases
102
132
  qa/test-reports/ # QA test reports (regression matrices, browser evidence)
103
133
  qa/requests/ # QA request artifacts
@@ -138,7 +168,7 @@ Do not default to git-backed storage or automatic commits for intermediate artif
138
168
 
139
169
  ## Pre-RD project scan checklist (MANDATORY)
140
170
 
141
- Before handing off to `peaks-rd`, scan the project and record findings to `.peaks/<session-id>/rd/project-scan.md`. RD and UI roles read this before starting work.
171
+ Before handing off to `peaks-rd`, scan the project and record findings to `.peaks/<session-id>/rd/project-scan.md`. RD and UI roles read this before starting work. **project-scan.md is a session-scoped singleton** — check if it already exists before regenerating (e.g. via `ls .peaks/<session-id>/rd/project-scan.md`). If it exists and is complete (has `## Archetype` and `## Project mode` sections), reuse it. Only regenerate if missing or incomplete.
142
172
 
143
173
  ### 0. Project archetype detection (MANDATORY — run FIRST, deterministic CLI)
144
174
 
@@ -388,6 +418,7 @@ ls .peaks/<id>/rd/project-scan.md
388
418
  # Expected output: .peaks/<id>/rd/project-scan.md
389
419
  # "No such file" → STOP, run project scan first
390
420
  # File present but missing `## Archetype` or `## Project mode` sections → INCOMPLETE, rerun scan
421
+ # File present and complete → reuse (project-scan is a session-scoped singleton)
391
422
  ```
392
423
 
393
424
  **Gate A.5 — Existing-system extraction (legacy projects only):**
@@ -571,7 +602,7 @@ The end-to-end CLI sequence for the `full-auto` profile. `assisted` and `strict`
571
602
  peaks doctor --json
572
603
  peaks project dashboard --project <repo> --json
573
604
  peaks skill runbook peaks-solo --json
574
- peaks workspace init --project <repo> --session-id <YYYY-MM-DD-<slug>> --json
605
+ peaks workspace init --project <repo> --json
575
606
  peaks scan archetype --project <repo> --json
576
607
  # → copy archetype, frontendOnly, signals into .peaks/<session-id>/rd/project-scan.md (Gate A)
577
608
  # → if archetype != greenfield AND archetype != unknown:
@@ -67,6 +67,9 @@ peaks mcp apply --capability playwright-mcp.browser-validation --yes --json
67
67
 
68
68
  # 3. read project-scan for component library and CSS framework context
69
69
  # check .peaks/<session-id>/rd/project-scan.md (blocking if missing for existing projects)
70
+ # NOTE: project-scan.md is a session-scoped singleton — check if it already exists before
71
+ # regenerating. If it exists and is complete (has `## Archetype` and `## Project mode`
72
+ # sections), reuse it.
70
73
 
71
74
  # 4. PROTOTYPE FIDELITY CHECK (MANDATORY before any design work):
72
75
  # Check if a Figma file, PRD screenshots, or explicit PRD visuals exist.