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.
- package/bin/peaks.js +0 -0
- package/dist/src/cli/commands/capability-commands.d.ts +12 -1
- package/dist/src/cli/commands/capability-commands.js +9 -11
- package/dist/src/cli/commands/config-commands.js +2 -85
- package/dist/src/cli/commands/core-artifact-commands.js +6 -1
- package/dist/src/cli/commands/request-commands.js +82 -2
- package/dist/src/cli/commands/scan-commands.js +30 -0
- package/dist/src/cli/commands/workflow-commands.js +35 -5
- package/dist/src/services/artifacts/artifact-prerequisites.js +53 -13
- package/dist/src/services/artifacts/artifact-service.js +2 -2
- package/dist/src/services/artifacts/request-artifact-service.d.ts +32 -0
- package/dist/src/services/artifacts/request-artifact-service.js +148 -16
- package/dist/src/services/artifacts/workspace-service.d.ts +1 -1
- package/dist/src/services/artifacts/workspace-service.js +10 -20
- 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 +67 -69
- package/dist/src/services/config/config-types.d.ts +0 -2
- package/dist/src/services/config/config-types.js +0 -2
- package/dist/src/services/mode/bypass-tracker.d.ts +4 -0
- package/dist/src/services/mode/bypass-tracker.js +31 -0
- package/dist/src/services/mode/mode-enforcement.d.ts +14 -0
- package/dist/src/services/mode/mode-enforcement.js +81 -0
- package/dist/src/services/sc/sc-service.js +5 -5
- 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.d.ts +19 -0
- package/dist/src/services/scan/file-size-scan.js +39 -0
- package/dist/src/services/scan/type-sanity-service.js +1 -3
- package/dist/src/services/session/index.d.ts +1 -0
- package/dist/src/services/session/index.js +1 -0
- package/dist/src/services/session/session-manager.d.ts +60 -0
- package/dist/src/services/session/session-manager.js +150 -0
- package/dist/src/services/skills/skill-presence-service.d.ts +4 -1
- package/dist/src/services/skills/skill-presence-service.js +11 -1
- 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/services/workspace/workspace-service.js +6 -0
- package/dist/src/shared/change-id.d.ts +13 -0
- package/dist/src/shared/change-id.js +29 -1
- package/dist/src/shared/incrementing-number.d.ts +31 -0
- package/dist/src/shared/incrementing-number.js +58 -0
- 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-rd/SKILL.md +3 -0
- package/skills/peaks-solo/SKILL.md +42 -11
- package/skills/peaks-ui/SKILL.md +3 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
export const MAX_BYPASSES_PER_SESSION = 3;
|
|
4
|
+
const BYPASS_FILE = '.bypass-count.json';
|
|
5
|
+
function bypassFilePath(sessionRoot) {
|
|
6
|
+
return join(sessionRoot, BYPASS_FILE);
|
|
7
|
+
}
|
|
8
|
+
export function getBypassCount(sessionRoot) {
|
|
9
|
+
const filePath = bypassFilePath(sessionRoot);
|
|
10
|
+
if (!existsSync(filePath)) {
|
|
11
|
+
return 0;
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
const raw = readFileSync(filePath, 'utf8');
|
|
15
|
+
const parsed = JSON.parse(raw);
|
|
16
|
+
return typeof parsed.count === 'number' ? parsed.count : 0;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return 0;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function recordBypass(sessionRoot) {
|
|
23
|
+
const current = getBypassCount(sessionRoot);
|
|
24
|
+
const next = current + 1;
|
|
25
|
+
const filePath = bypassFilePath(sessionRoot);
|
|
26
|
+
writeFileSync(filePath, JSON.stringify({ count: next }, null, 2), 'utf8');
|
|
27
|
+
return next;
|
|
28
|
+
}
|
|
29
|
+
export function isBypassLimitReached(sessionRoot) {
|
|
30
|
+
return getBypassCount(sessionRoot) >= MAX_BYPASSES_PER_SESSION;
|
|
31
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type SkillPresenceMode } from '../skills/skill-presence-service.js';
|
|
2
|
+
type TransitionKey = `${string}:${string}`;
|
|
3
|
+
export declare function requiresConfirmation(mode: SkillPresenceMode, transitionKey: TransitionKey): boolean;
|
|
4
|
+
export type ConfirmationOptions = {
|
|
5
|
+
projectRoot: string;
|
|
6
|
+
transitionKey: TransitionKey;
|
|
7
|
+
confirmed?: boolean | undefined;
|
|
8
|
+
forceConfirm?: boolean | undefined;
|
|
9
|
+
};
|
|
10
|
+
export declare class ConfirmationRequiredError extends Error {
|
|
11
|
+
constructor(transitionKey: TransitionKey);
|
|
12
|
+
}
|
|
13
|
+
export declare function requireUserConfirmation(options: ConfirmationOptions): Promise<void>;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import * as readline from 'node:readline';
|
|
2
|
+
import { getSkillPresence } from '../skills/skill-presence-service.js';
|
|
3
|
+
const ASSISTED_CONFIRM_TRANSITIONS = new Set([
|
|
4
|
+
'prd:confirmed-by-user',
|
|
5
|
+
'rd:qa-handoff',
|
|
6
|
+
'qa:verdict-issued'
|
|
7
|
+
]);
|
|
8
|
+
export function requiresConfirmation(mode, transitionKey) {
|
|
9
|
+
if (mode === 'full-auto' || mode === 'swarm') {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
if (mode === 'strict') {
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
// assisted: only specific transitions
|
|
16
|
+
return ASSISTED_CONFIRM_TRANSITIONS.has(transitionKey);
|
|
17
|
+
}
|
|
18
|
+
function describeTransition(transitionKey) {
|
|
19
|
+
const parts = transitionKey.split(':');
|
|
20
|
+
const role = parts[0] ?? 'unknown';
|
|
21
|
+
const state = parts[1] ?? 'unknown';
|
|
22
|
+
return `Transition ${role.toUpperCase()} → ${state}`;
|
|
23
|
+
}
|
|
24
|
+
export class ConfirmationRequiredError extends Error {
|
|
25
|
+
constructor(transitionKey) {
|
|
26
|
+
const description = describeTransition(transitionKey);
|
|
27
|
+
super(`Confirmation required for: ${description}\n` +
|
|
28
|
+
'Add --confirm to proceed non-interactively, or run in an interactive terminal.\n' +
|
|
29
|
+
'In assisted/strict mode, major workflow boundaries require explicit user approval.');
|
|
30
|
+
this.name = 'ConfirmationRequiredError';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export async function requireUserConfirmation(options) {
|
|
34
|
+
const presence = getSkillPresence();
|
|
35
|
+
if (!presence?.mode) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const mode = presence.mode;
|
|
39
|
+
if (!requiresConfirmation(mode, options.transitionKey)) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
// --confirm flag bypasses interactive prompt
|
|
43
|
+
if (options.confirmed) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
// PEAKS_AUTO_CONFIRM=1 only works for full-auto/swarm (already returned above)
|
|
47
|
+
// For assisted/strict, env var is ignored unless --force-confirm is also set
|
|
48
|
+
if (process.env.PEAKS_AUTO_CONFIRM === '1') {
|
|
49
|
+
if (options.forceConfirm) {
|
|
50
|
+
console.error(`[WARNING] --force-confirm used in ${mode} mode. ` +
|
|
51
|
+
'This bypasses user confirmation. Use with caution.');
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
throw new ConfirmationRequiredError(options.transitionKey);
|
|
55
|
+
}
|
|
56
|
+
// --force-confirm without env var
|
|
57
|
+
if (options.forceConfirm) {
|
|
58
|
+
console.error(`[WARNING] --force-confirm used in ${mode} mode. ` +
|
|
59
|
+
'This bypasses user confirmation. Use with caution.');
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
// Interactive prompt
|
|
63
|
+
const rl = readline.createInterface({
|
|
64
|
+
input: process.stdin,
|
|
65
|
+
output: process.stderr
|
|
66
|
+
});
|
|
67
|
+
return new Promise((resolve, reject) => {
|
|
68
|
+
const description = describeTransition(options.transitionKey);
|
|
69
|
+
const prompt = `\n[CONFIRM] ${description}\nProceed? (y/N) `;
|
|
70
|
+
rl.question(prompt, (answer) => {
|
|
71
|
+
rl.close();
|
|
72
|
+
const normalized = answer.trim().toLowerCase();
|
|
73
|
+
if (normalized === 'y' || normalized === 'yes') {
|
|
74
|
+
resolve();
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
reject(new ConfirmationRequiredError(options.transitionKey));
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
}
|
|
@@ -2,7 +2,7 @@ import { existsSync, lstatSync, readFileSync, realpathSync } from 'node:fs';
|
|
|
2
2
|
import { execFileSync } from 'node:child_process';
|
|
3
3
|
import { basename, relative, resolve } from 'node:path';
|
|
4
4
|
import { isInsidePath } from '../../shared/path-utils.js';
|
|
5
|
-
import {
|
|
5
|
+
import { getWorkspaceConfigForPath } from '../config/config-service.js';
|
|
6
6
|
import { getArtifactRemoteRepo, getArtifactWorkspaceStatus, getLocalArtifactPath } from '../artifacts/workspace-service.js';
|
|
7
7
|
const REQUIRED_ARTIFACTS = [
|
|
8
8
|
{ name: 'retention-boundary.md', path: ['sc', 'retention-boundary.md'] },
|
|
@@ -109,7 +109,7 @@ function isRetainedArtifactFile(filePath, artifactWorkspacePath, changesRoot, ch
|
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
111
|
export function getChangeTraceabilityStatus() {
|
|
112
|
-
const workspace =
|
|
112
|
+
const workspace = getWorkspaceConfigForPath(process.cwd());
|
|
113
113
|
const artifactStatus = getArtifactWorkspaceStatus(workspace?.workspaceId);
|
|
114
114
|
if (!workspace) {
|
|
115
115
|
return {
|
|
@@ -155,7 +155,7 @@ export function getChangeTraceabilityStatus() {
|
|
|
155
155
|
};
|
|
156
156
|
}
|
|
157
157
|
export function createChangeImpact(options) {
|
|
158
|
-
const workspace =
|
|
158
|
+
const workspace = getWorkspaceConfigForPath(process.cwd());
|
|
159
159
|
const artifactRepo = workspace ? getArtifactRemoteRepo(workspace) : null;
|
|
160
160
|
return {
|
|
161
161
|
changeId: options.changeId,
|
|
@@ -193,7 +193,7 @@ export function createArtifactRetentionReport(options) {
|
|
|
193
193
|
};
|
|
194
194
|
}
|
|
195
195
|
export function recordCommitBoundary(options) {
|
|
196
|
-
const workspace =
|
|
196
|
+
const workspace = getWorkspaceConfigForPath(process.cwd());
|
|
197
197
|
const artifactStatus = getArtifactWorkspaceStatus(workspace?.workspaceId);
|
|
198
198
|
const commitHash = getCurrentCommitHash(workspace?.rootPath);
|
|
199
199
|
return {
|
|
@@ -207,7 +207,7 @@ export function recordCommitBoundary(options) {
|
|
|
207
207
|
};
|
|
208
208
|
}
|
|
209
209
|
export function validateArtifactRetention(sliceId) {
|
|
210
|
-
const workspace =
|
|
210
|
+
const workspace = getWorkspaceConfigForPath(process.cwd());
|
|
211
211
|
if (!workspace) {
|
|
212
212
|
return {
|
|
213
213
|
valid: false,
|
|
@@ -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) {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export declare const DEFAULT_FILE_SIZE_THRESHOLD = 800;
|
|
2
|
+
export type FileSizeViolation = {
|
|
3
|
+
file: string;
|
|
4
|
+
lines: number;
|
|
5
|
+
};
|
|
6
|
+
export type FileSizeScanResult = {
|
|
7
|
+
ok: boolean;
|
|
8
|
+
threshold: number;
|
|
9
|
+
checkedFiles: number;
|
|
10
|
+
violations: FileSizeViolation[];
|
|
11
|
+
};
|
|
12
|
+
export type FileSizeScanOptions = {
|
|
13
|
+
projectRoot: string;
|
|
14
|
+
/** Compare working tree against this ref. Default 'HEAD'. */
|
|
15
|
+
baseRef?: string;
|
|
16
|
+
/** Line count threshold. Default 800. */
|
|
17
|
+
threshold?: number;
|
|
18
|
+
};
|
|
19
|
+
export declare function scanFileSize(options: FileSizeScanOptions): FileSizeScanResult;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
export const DEFAULT_FILE_SIZE_THRESHOLD = 800;
|
|
5
|
+
function getChangedFiles(projectRoot, baseRef) {
|
|
6
|
+
try {
|
|
7
|
+
const trackedRaw = execFileSync('git', ['-C', projectRoot, 'diff', '--name-only', baseRef], { encoding: 'utf8' });
|
|
8
|
+
const tracked = trackedRaw.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
9
|
+
const untrackedRaw = execFileSync('git', ['-C', projectRoot, 'ls-files', '--others', '--exclude-standard'], { encoding: 'utf8' });
|
|
10
|
+
const untracked = untrackedRaw.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
11
|
+
return Array.from(new Set([...tracked, ...untracked]));
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return [];
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function countLines(filePath) {
|
|
18
|
+
const content = readFileSync(filePath, 'utf8');
|
|
19
|
+
return content.split(/\r?\n/).length;
|
|
20
|
+
}
|
|
21
|
+
export function scanFileSize(options) {
|
|
22
|
+
const baseRef = options.baseRef ?? 'HEAD';
|
|
23
|
+
const threshold = options.threshold ?? DEFAULT_FILE_SIZE_THRESHOLD;
|
|
24
|
+
const files = getChangedFiles(options.projectRoot, baseRef);
|
|
25
|
+
const violations = [];
|
|
26
|
+
for (const file of files) {
|
|
27
|
+
const absolute = join(options.projectRoot, file);
|
|
28
|
+
const lines = countLines(absolute);
|
|
29
|
+
if (lines > threshold) {
|
|
30
|
+
violations.push({ file, lines });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
ok: violations.length === 0,
|
|
35
|
+
threshold,
|
|
36
|
+
checkedFiles: files.length,
|
|
37
|
+
violations
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -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 @@
|
|
|
1
|
+
export { ensureSession, getSessionId, getCurrentSessionDir, listSessions, getProjectScanPath, hasProjectScan, type SessionInfo } from './session-manager.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ensureSession, getSessionId, getCurrentSessionDir, listSessions, getProjectScanPath, hasProjectScan } from './session-manager.js';
|
|
@@ -0,0 +1,60 @@
|
|
|
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
|
+
export type SessionInfo = {
|
|
9
|
+
sessionId: string;
|
|
10
|
+
createdAt: string;
|
|
11
|
+
projectRoot: string;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Get or create the current session for a project.
|
|
15
|
+
* If a valid session already exists, returns it.
|
|
16
|
+
* Otherwise, creates a new session with auto-generated ID.
|
|
17
|
+
*
|
|
18
|
+
* @param projectRoot - Root directory of the project
|
|
19
|
+
* @returns Session ID (e.g., "2026-05-26-session-a3f8b1")
|
|
20
|
+
*/
|
|
21
|
+
export declare function ensureSession(projectRoot: string): Promise<string>;
|
|
22
|
+
/**
|
|
23
|
+
* Get the current session ID without creating a new one.
|
|
24
|
+
* Returns null if no session exists.
|
|
25
|
+
*
|
|
26
|
+
* @param projectRoot - Root directory of the project
|
|
27
|
+
* @returns Session ID or null
|
|
28
|
+
*/
|
|
29
|
+
export declare function getSessionId(projectRoot: string): string | null;
|
|
30
|
+
/**
|
|
31
|
+
* Get the absolute path to the current session directory.
|
|
32
|
+
* Creates the session if it doesn't exist.
|
|
33
|
+
*
|
|
34
|
+
* @param projectRoot - Root directory of the project
|
|
35
|
+
* @returns Absolute path to session directory (e.g., "/path/to/project/.peaks/2026-05-26-session-a3f8b1")
|
|
36
|
+
*/
|
|
37
|
+
export declare function getCurrentSessionDir(projectRoot: string): Promise<string>;
|
|
38
|
+
/**
|
|
39
|
+
* List all session directories in the .peaks folder.
|
|
40
|
+
* Returns session IDs (directory names) sorted by date.
|
|
41
|
+
*
|
|
42
|
+
* @param projectRoot - Root directory of the project
|
|
43
|
+
* @returns Array of session IDs
|
|
44
|
+
*/
|
|
45
|
+
export declare function listSessions(projectRoot: string): string[];
|
|
46
|
+
/**
|
|
47
|
+
* Get the path to project-scan.md for the current session.
|
|
48
|
+
* Creates the session if it doesn't exist.
|
|
49
|
+
*
|
|
50
|
+
* @param projectRoot - Root directory of the project
|
|
51
|
+
* @returns Absolute path to project-scan.md
|
|
52
|
+
*/
|
|
53
|
+
export declare function getProjectScanPath(projectRoot: string): Promise<string>;
|
|
54
|
+
/**
|
|
55
|
+
* Check if project-scan.md exists for the current session.
|
|
56
|
+
*
|
|
57
|
+
* @param projectRoot - Root directory of the project
|
|
58
|
+
* @returns true if project-scan.md exists
|
|
59
|
+
*/
|
|
60
|
+
export declare function hasProjectScan(projectRoot: string): boolean;
|
|
@@ -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?:
|
|
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
|
-
...(
|
|
23
|
+
...(validatedMode ? { mode: validatedMode } : {}),
|
|
14
24
|
...(gate ? { gate } : {}),
|
|
15
25
|
setAt: new Date().toISOString()
|
|
16
26
|
};
|
|
@@ -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>;
|