peaks-cli 1.0.20 → 1.0.22

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 (41) hide show
  1. package/README.md +42 -375
  2. package/bin/peaks.js +0 -0
  3. package/dist/src/cli/commands/capability-commands.d.ts +1 -1
  4. package/dist/src/cli/commands/capability-commands.js +2 -5
  5. package/dist/src/cli/commands/config-commands.js +2 -85
  6. package/dist/src/cli/commands/core-artifact-commands.js +6 -1
  7. package/dist/src/cli/commands/request-commands.js +82 -2
  8. package/dist/src/cli/commands/scan-commands.js +30 -0
  9. package/dist/src/cli/commands/workflow-commands.js +9 -5
  10. package/dist/src/services/artifacts/artifact-prerequisites.js +53 -13
  11. package/dist/src/services/artifacts/artifact-service.js +2 -2
  12. package/dist/src/services/artifacts/request-artifact-service.d.ts +32 -0
  13. package/dist/src/services/artifacts/request-artifact-service.js +148 -16
  14. package/dist/src/services/artifacts/workspace-service.js +8 -9
  15. package/dist/src/services/config/config-service.js +54 -69
  16. package/dist/src/services/config/config-types.d.ts +0 -2
  17. package/dist/src/services/config/config-types.js +0 -2
  18. package/dist/src/services/mode/bypass-tracker.d.ts +4 -0
  19. package/dist/src/services/mode/bypass-tracker.js +31 -0
  20. package/dist/src/services/mode/mode-enforcement.d.ts +14 -0
  21. package/dist/src/services/mode/mode-enforcement.js +81 -0
  22. package/dist/src/services/sc/sc-service.js +5 -5
  23. package/dist/src/services/scan/file-size-scan.d.ts +19 -0
  24. package/dist/src/services/scan/file-size-scan.js +44 -0
  25. package/dist/src/services/session/index.d.ts +1 -0
  26. package/dist/src/services/session/index.js +1 -0
  27. package/dist/src/services/session/session-manager.d.ts +60 -0
  28. package/dist/src/services/session/session-manager.js +150 -0
  29. package/dist/src/services/skills/skill-presence-service.d.ts +4 -1
  30. package/dist/src/services/skills/skill-presence-service.js +11 -1
  31. package/dist/src/services/workspace/workspace-service.js +6 -0
  32. package/dist/src/shared/change-id.d.ts +13 -0
  33. package/dist/src/shared/change-id.js +32 -1
  34. package/dist/src/shared/incrementing-number.d.ts +31 -0
  35. package/dist/src/shared/incrementing-number.js +58 -0
  36. package/dist/src/shared/version.d.ts +1 -1
  37. package/dist/src/shared/version.js +1 -1
  38. package/package.json +1 -1
  39. package/skills/peaks-rd/SKILL.md +3 -0
  40. package/skills/peaks-solo/SKILL.md +9 -11
  41. package/skills/peaks-ui/SKILL.md +3 -0
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Session management service for Peaks artifact storage.
3
+ * Manages session lifecycle: creation, retrieval, and directory initialization.
4
+ *
5
+ * Sessions are automatically created when any skill is invoked.
6
+ * Each session gets a unique directory under .peaks/ with incrementing numbered files.
7
+ */
8
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
9
+ import { join } from 'node:path';
10
+ import { randomBytes } from 'node:crypto';
11
+ import { initWorkspace } from '../workspace/workspace-service.js';
12
+ const SESSION_FILE = '.session.json';
13
+ /**
14
+ * Generate a new session ID.
15
+ * Format: YYYY-MM-DD-session-<6位hex>
16
+ * Example: 2026-05-26-session-a3f8b1
17
+ */
18
+ function generateSessionId() {
19
+ const now = new Date();
20
+ const year = now.getFullYear();
21
+ const month = String(now.getMonth() + 1).padStart(2, '0');
22
+ const day = String(now.getDate()).padStart(2, '0');
23
+ const date = `${year}-${month}-${day}`;
24
+ const random = randomBytes(3).toString('hex'); // 6位hex
25
+ return `${date}-session-${random}`;
26
+ }
27
+ /**
28
+ * Get the path to the session file for a project.
29
+ */
30
+ function getSessionFilePath(projectRoot) {
31
+ return join(projectRoot, '.peaks', SESSION_FILE);
32
+ }
33
+ /**
34
+ * Read existing session info from disk.
35
+ * Returns null if no session file exists or if it's invalid.
36
+ */
37
+ function readSessionFile(projectRoot) {
38
+ const sessionFile = getSessionFilePath(projectRoot);
39
+ if (!existsSync(sessionFile))
40
+ return null;
41
+ try {
42
+ const data = JSON.parse(readFileSync(sessionFile, 'utf8'));
43
+ if (data.sessionId && data.projectRoot === projectRoot) {
44
+ return data;
45
+ }
46
+ return null;
47
+ }
48
+ catch {
49
+ return null;
50
+ }
51
+ }
52
+ /**
53
+ * Write session info to disk.
54
+ */
55
+ function writeSessionFile(projectRoot, info) {
56
+ const sessionFile = getSessionFilePath(projectRoot);
57
+ const dir = join(projectRoot, '.peaks');
58
+ if (!existsSync(dir)) {
59
+ mkdirSync(dir, { recursive: true });
60
+ }
61
+ writeFileSync(sessionFile, JSON.stringify(info, null, 2), 'utf8');
62
+ }
63
+ /**
64
+ * Get or create the current session for a project.
65
+ * If a valid session already exists, returns it.
66
+ * Otherwise, creates a new session with auto-generated ID.
67
+ *
68
+ * @param projectRoot - Root directory of the project
69
+ * @returns Session ID (e.g., "2026-05-26-session-a3f8b1")
70
+ */
71
+ export async function ensureSession(projectRoot) {
72
+ const existing = readSessionFile(projectRoot);
73
+ if (existing) {
74
+ return existing.sessionId;
75
+ }
76
+ const sessionId = generateSessionId();
77
+ const info = {
78
+ sessionId,
79
+ createdAt: new Date().toISOString(),
80
+ projectRoot
81
+ };
82
+ writeSessionFile(projectRoot, info);
83
+ await initWorkspace({ projectRoot, sessionId });
84
+ return sessionId;
85
+ }
86
+ /**
87
+ * Get the current session ID without creating a new one.
88
+ * Returns null if no session exists.
89
+ *
90
+ * @param projectRoot - Root directory of the project
91
+ * @returns Session ID or null
92
+ */
93
+ export function getSessionId(projectRoot) {
94
+ const info = readSessionFile(projectRoot);
95
+ return info?.sessionId ?? null;
96
+ }
97
+ /**
98
+ * Get the absolute path to the current session directory.
99
+ * Creates the session if it doesn't exist.
100
+ *
101
+ * @param projectRoot - Root directory of the project
102
+ * @returns Absolute path to session directory (e.g., "/path/to/project/.peaks/2026-05-26-session-a3f8b1")
103
+ */
104
+ export async function getCurrentSessionDir(projectRoot) {
105
+ const sessionId = await ensureSession(projectRoot);
106
+ return join(projectRoot, '.peaks', sessionId);
107
+ }
108
+ /**
109
+ * List all session directories in the .peaks folder.
110
+ * Returns session IDs (directory names) sorted by date.
111
+ *
112
+ * @param projectRoot - Root directory of the project
113
+ * @returns Array of session IDs
114
+ */
115
+ export function listSessions(projectRoot) {
116
+ const peaksRoot = join(projectRoot, '.peaks');
117
+ if (!existsSync(peaksRoot))
118
+ return [];
119
+ const { readdirSync } = require('node:fs');
120
+ const entries = readdirSync(peaksRoot, { withFileTypes: true });
121
+ return entries
122
+ .filter((entry) => entry.isDirectory() && /^\d{4}-\d{2}-\d{2}-session-[a-f0-9]+$/.test(entry.name))
123
+ .map((entry) => entry.name)
124
+ .sort()
125
+ .reverse(); // Most recent first
126
+ }
127
+ /**
128
+ * Get the path to project-scan.md for the current session.
129
+ * Creates the session if it doesn't exist.
130
+ *
131
+ * @param projectRoot - Root directory of the project
132
+ * @returns Absolute path to project-scan.md
133
+ */
134
+ export async function getProjectScanPath(projectRoot) {
135
+ const sessionId = await ensureSession(projectRoot);
136
+ return join(projectRoot, '.peaks', sessionId, 'rd', 'project-scan.md');
137
+ }
138
+ /**
139
+ * Check if project-scan.md exists for the current session.
140
+ *
141
+ * @param projectRoot - Root directory of the project
142
+ * @returns true if project-scan.md exists
143
+ */
144
+ export function hasProjectScan(projectRoot) {
145
+ const info = readSessionFile(projectRoot);
146
+ if (!info)
147
+ return false;
148
+ const scanPath = join(projectRoot, '.peaks', info.sessionId, 'rd', 'project-scan.md');
149
+ return existsSync(scanPath);
150
+ }
@@ -1,6 +1,9 @@
1
+ export type SkillPresenceMode = 'full-auto' | 'assisted' | 'swarm' | 'strict';
2
+ export declare const VALID_SKILL_PRESENCE_MODES: ReadonlyArray<SkillPresenceMode>;
3
+ export declare function isSkillPresenceMode(value: string): value is SkillPresenceMode;
1
4
  export type SkillPresence = {
2
5
  skill: string;
3
- mode?: string;
6
+ mode?: SkillPresenceMode;
4
7
  gate?: string;
5
8
  setAt: string;
6
9
  };
@@ -1,5 +1,14 @@
1
1
  import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
2
2
  import { dirname, resolve } from 'node:path';
3
+ export const VALID_SKILL_PRESENCE_MODES = [
4
+ 'full-auto',
5
+ 'assisted',
6
+ 'swarm',
7
+ 'strict'
8
+ ];
9
+ export function isSkillPresenceMode(value) {
10
+ return VALID_SKILL_PRESENCE_MODES.includes(value);
11
+ }
3
12
  const PRESENCE_FILE = '.peaks/.active-skill.json';
4
13
  function resolvePresencePath() {
5
14
  return resolve(process.cwd(), PRESENCE_FILE);
@@ -8,9 +17,10 @@ export function exportSkillPresence() {
8
17
  return resolvePresencePath();
9
18
  }
10
19
  export function setSkillPresence(skill, mode, gate) {
20
+ const validatedMode = mode && isSkillPresenceMode(mode) ? mode : undefined;
11
21
  const presence = {
12
22
  skill,
13
- ...(mode ? { mode } : {}),
23
+ ...(validatedMode ? { mode: validatedMode } : {}),
14
24
  ...(gate ? { gate } : {}),
15
25
  setAt: new Date().toISOString()
16
26
  };
@@ -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,37 @@ 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
+ if (isUnsafeArtifactPath(candidatePath)) {
89
+ throw new ChangeIdValidationError(changeId);
90
+ }
91
+ return normalizeArtifactPath(candidatePath);
92
+ }
93
+ // Fallback: no session or no segments - use legacy behavior
63
94
  const joined = segments.map((segment) => normalizeForwardSlashes(segment)).join('/');
64
95
  const candidatePath = `.peaks/${changeId}/${joined}`;
65
96
  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.20";
1
+ export declare const CLI_VERSION = "1.0.22";
@@ -1 +1 @@
1
- export const CLI_VERSION = "1.0.20";
1
+ export const CLI_VERSION = "1.0.22";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "peaks-cli",
3
- "version": "1.0.20",
3
+ "version": "1.0.22",
4
4
  "description": "Peaks CLI and short skill family for Claude Code automation.",
5
5
  "author": "SquabbyZ",
6
6
  "license": "MIT",
@@ -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
@@ -77,27 +77,24 @@ For frontend workflows, RD and QA must use Playwright MCP (`mcp__playwright__` t
77
77
 
78
78
  ### Workspace initialization gate
79
79
 
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`).
80
+ 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
81
 
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`
82
+ 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
83
 
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.
84
+ **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
85
 
90
86
  ```bash
91
- peaks workspace init --project <repo> --session-id <YYYY-MM-DD-<slug>> --json
87
+ peaks workspace init --project <repo> --json
92
88
  ```
93
89
 
94
- The workspace initialization creates this structure under `.peaks/<session-id>/`:
90
+ The workspace initialization creates this structure under `.peaks/<session-id>/` (where `<session-id>` is auto-generated as `YYYY-MM-DD-session-<6位hex>`):
95
91
 
96
92
  ```
97
93
  prd/source/ # PRD source documents (Feishu exports, pasted content)
98
94
  prd/requests/ # PRD request artifacts (goals, non-goals, acceptance, frontend delta)
99
95
  ui/requests/ # UI request artifacts (visual direction, taste reports)
100
96
  rd/requests/ # RD request artifacts (slice specs, coverage, CR findings)
97
+ rd/project-scan.md # Project scan (session-scoped singleton, generated once per session)
101
98
  qa/test-cases/ # QA test cases
102
99
  qa/test-reports/ # QA test reports (regression matrices, browser evidence)
103
100
  qa/requests/ # QA request artifacts
@@ -138,7 +135,7 @@ Do not default to git-backed storage or automatic commits for intermediate artif
138
135
 
139
136
  ## Pre-RD project scan checklist (MANDATORY)
140
137
 
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.
138
+ 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
139
 
143
140
  ### 0. Project archetype detection (MANDATORY — run FIRST, deterministic CLI)
144
141
 
@@ -388,6 +385,7 @@ ls .peaks/<id>/rd/project-scan.md
388
385
  # Expected output: .peaks/<id>/rd/project-scan.md
389
386
  # "No such file" → STOP, run project scan first
390
387
  # File present but missing `## Archetype` or `## Project mode` sections → INCOMPLETE, rerun scan
388
+ # File present and complete → reuse (project-scan is a session-scoped singleton)
391
389
  ```
392
390
 
393
391
  **Gate A.5 — Existing-system extraction (legacy projects only):**
@@ -571,7 +569,7 @@ The end-to-end CLI sequence for the `full-auto` profile. `assisted` and `strict`
571
569
  peaks doctor --json
572
570
  peaks project dashboard --project <repo> --json
573
571
  peaks skill runbook peaks-solo --json
574
- peaks workspace init --project <repo> --session-id <YYYY-MM-DD-<slug>> --json
572
+ peaks workspace init --project <repo> --json
575
573
  peaks scan archetype --project <repo> --json
576
574
  # → copy archetype, frontendOnly, signals into .peaks/<session-id>/rd/project-scan.md (Gate A)
577
575
  # → 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.