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.
- package/README.md +42 -375
- package/bin/peaks.js +0 -0
- package/dist/src/cli/commands/capability-commands.d.ts +1 -1
- package/dist/src/cli/commands/capability-commands.js +2 -5
- 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 +9 -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.js +8 -9
- package/dist/src/services/config/config-service.js +54 -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/file-size-scan.d.ts +19 -0
- package/dist/src/services/scan/file-size-scan.js +44 -0
- 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/workspace/workspace-service.js +6 -0
- package/dist/src/shared/change-id.d.ts +13 -0
- package/dist/src/shared/change-id.js +32 -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 +1 -1
- package/skills/peaks-rd/SKILL.md +3 -0
- package/skills/peaks-solo/SKILL.md +9 -11
- package/skills/peaks-ui/SKILL.md +3 -0
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { existsSync, mkdirSync } from 'node:fs';
|
|
2
|
-
import {
|
|
3
|
-
import { basename, dirname, isAbsolute, resolve } from 'node:path';
|
|
2
|
+
import { dirname, isAbsolute, resolve } from 'node:path';
|
|
4
3
|
import { DEFAULT_CONFIG } from './config-types.js';
|
|
5
4
|
import { stablePath } from '../../shared/path-utils.js';
|
|
6
5
|
import { findProjectRoot, getProjectBootstrapConfigPath, getProjectConfigPath, getUserConfigPath, isInsidePath, readConfigFileSafely, resolveProjectRootForConfig, validateArtifactWorkspaceMarkerPath, validateArtifactWorkspaceRoot, validateProjectBootstrapConfigPathForWrite, validateUserConfigPathForWrite, writeConfigFileSafely, writeProjectConfigFile, writeUserConfigFile } from './config-safety.js';
|
|
@@ -199,10 +198,6 @@ function isRecord(value) {
|
|
|
199
198
|
function isSafeConfigSegment(value) {
|
|
200
199
|
return /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(value) && !value.includes('..') && !value.endsWith('.');
|
|
201
200
|
}
|
|
202
|
-
function toSafeConfigSegment(value) {
|
|
203
|
-
const normalized = value.toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '').replace(/\.+$/g, '');
|
|
204
|
-
return isSafeConfigSegment(normalized) ? normalized : 'workspace';
|
|
205
|
-
}
|
|
206
201
|
function toArtifactRemoteRepoConfig(value) {
|
|
207
202
|
if (!isRecord(value) || (value.provider !== 'github' && value.provider !== 'gitlab') || typeof value.owner !== 'string' || typeof value.name !== 'string') {
|
|
208
203
|
return null;
|
|
@@ -246,15 +241,6 @@ function toWorkspaceConfig(value) {
|
|
|
246
241
|
function toWorkspaceConfigs(value) {
|
|
247
242
|
return Array.isArray(value) ? value.map(toWorkspaceConfig).filter((workspace) => workspace !== null) : [];
|
|
248
243
|
}
|
|
249
|
-
function mergeWorkspaceConfigs(userWorkspaces, projectWorkspaces) {
|
|
250
|
-
const merged = new Map(userWorkspaces.map((workspace) => [workspace.workspaceId, workspace]));
|
|
251
|
-
for (const workspace of projectWorkspaces) {
|
|
252
|
-
if (!merged.has(workspace.workspaceId)) {
|
|
253
|
-
merged.set(workspace.workspaceId, workspace);
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
return [...merged.values()];
|
|
257
|
-
}
|
|
258
244
|
function toProviderModelConfig(value) {
|
|
259
245
|
if (!isRecord(value))
|
|
260
246
|
return {};
|
|
@@ -400,8 +386,6 @@ function toPeaksConfig(value) {
|
|
|
400
386
|
const proxy = toProxyConfig(value.proxy);
|
|
401
387
|
return {
|
|
402
388
|
...(typeof value.version === 'string' ? { version: value.version } : {}),
|
|
403
|
-
...(typeof value.currentWorkspace === 'string' ? { currentWorkspace: value.currentWorkspace } : {}),
|
|
404
|
-
...(Array.isArray(value.workspaces) ? { workspaces: toWorkspaceConfigs(value.workspaces) } : {}),
|
|
405
389
|
...(typeof value.language === 'string' ? { language: value.language } : {}),
|
|
406
390
|
...(typeof value.model === 'string' && ['haiku', 'sonnet', 'opus', 'minimax'].includes(value.model) ? { model: value.model } : {}),
|
|
407
391
|
...(typeof value.economyMode === 'boolean' ? { economyMode: value.economyMode } : {}),
|
|
@@ -424,15 +408,23 @@ export function readConfig(projectRoot) {
|
|
|
424
408
|
const detectedRoot = projectRoot ?? findProjectRoot(process.cwd());
|
|
425
409
|
const userConfig = toPeaksConfig(readUserJsonFile());
|
|
426
410
|
const projectConfig = removeProjectSensitiveConfig(toPeaksConfig(readProjectJsonFile(detectedRoot)));
|
|
427
|
-
const { proxy: projectProxy,
|
|
428
|
-
const userWorkspaces = userConfig.workspaces ?? [];
|
|
411
|
+
const { proxy: projectProxy, ...projectConfigWithoutProxy } = projectConfig;
|
|
429
412
|
return {
|
|
430
413
|
...DEFAULT_CONFIG,
|
|
431
414
|
...userConfig,
|
|
432
|
-
...projectConfigWithoutProxy
|
|
433
|
-
workspaces: mergeWorkspaceConfigs(userWorkspaces, projectWorkspaces ?? [])
|
|
415
|
+
...projectConfigWithoutProxy
|
|
434
416
|
};
|
|
435
417
|
}
|
|
418
|
+
function sanitizeWorkspacePartial(partial) {
|
|
419
|
+
const result = { ...partial };
|
|
420
|
+
if (Array.isArray(result.workspaces)) {
|
|
421
|
+
result.workspaces = toWorkspaceConfigs(result.workspaces);
|
|
422
|
+
}
|
|
423
|
+
if (typeof result.currentWorkspace !== 'string' && result.currentWorkspace !== null && result.currentWorkspace !== undefined) {
|
|
424
|
+
delete result.currentWorkspace;
|
|
425
|
+
}
|
|
426
|
+
return result;
|
|
427
|
+
}
|
|
436
428
|
export function writeConfig(partial, layer = 'user') {
|
|
437
429
|
if (!isConfigLayer(layer)) {
|
|
438
430
|
throw new Error('Invalid config layer');
|
|
@@ -446,29 +438,28 @@ export function writeConfig(partial, layer = 'user') {
|
|
|
446
438
|
const { projectRoot, configPath } = getProjectWriteTarget();
|
|
447
439
|
ensureDir(dirname(configPath));
|
|
448
440
|
const existing = readJsonFile(configPath, () => validateProjectBootstrapConfigPathForWrite(projectRoot, configPath)) ?? {};
|
|
449
|
-
const merged = { ...existing, ...partial };
|
|
441
|
+
const merged = sanitizeWorkspacePartial({ ...existing, ...partial });
|
|
450
442
|
writeProjectConfigFile(projectRoot, configPath, JSON.stringify(merged, null, 2));
|
|
451
443
|
return;
|
|
452
444
|
}
|
|
453
445
|
const userPath = getUserConfigPath();
|
|
454
446
|
ensureDir(dirname(userPath));
|
|
455
447
|
const existing = readJsonFile(userPath, () => validateUserConfigPathForWrite(userPath)) ?? {};
|
|
456
|
-
const merged = { ...existing, ...partial };
|
|
448
|
+
const merged = sanitizeWorkspacePartial({ ...existing, ...partial });
|
|
457
449
|
writeUserConfigFile(userPath, JSON.stringify(merged, null, 2));
|
|
458
450
|
}
|
|
459
451
|
export function getConfig(options = {}) {
|
|
460
452
|
const projectRoot = findProjectRoot(process.cwd());
|
|
461
453
|
const userConfig = readUserJsonFile() ?? {};
|
|
462
454
|
const projectConfig = removeProjectSensitiveConfig(readProjectJsonFile(projectRoot) ?? {});
|
|
463
|
-
const { proxy: projectProxy,
|
|
455
|
+
const { proxy: projectProxy, ...projectConfigWithoutProxy } = projectConfig;
|
|
464
456
|
const source = options.layer === 'user'
|
|
465
457
|
? userConfig
|
|
466
458
|
: options.layer === 'project'
|
|
467
459
|
? projectConfig
|
|
468
460
|
: {
|
|
469
461
|
...userConfig,
|
|
470
|
-
...projectConfigWithoutProxy
|
|
471
|
-
workspaces: mergeWorkspaceConfigs(toWorkspaceConfigs(userConfig.workspaces), toWorkspaceConfigs(projectWorkspaces))
|
|
462
|
+
...projectConfigWithoutProxy
|
|
472
463
|
};
|
|
473
464
|
const config = isRecord(source) ? { ...source, ...(source.tokens !== undefined ? { tokens: toTokenConfig(source.tokens) } : {}) } : source;
|
|
474
465
|
if (options.key !== undefined) {
|
|
@@ -514,11 +505,7 @@ export function setConfig(options) {
|
|
|
514
505
|
writeUserConfigFile(targetPath, content);
|
|
515
506
|
}
|
|
516
507
|
}
|
|
517
|
-
|
|
518
|
-
const config = readConfig(projectRoot ?? findProjectRoot(process.cwd()));
|
|
519
|
-
return config.workspaces.find((w) => w.workspaceId === workspaceId) ?? null;
|
|
520
|
-
}
|
|
521
|
-
function readLayerConfig(layer) {
|
|
508
|
+
function readRawWorkspaceData(layer) {
|
|
522
509
|
const config = getConfig({ layer });
|
|
523
510
|
return isRecord(config)
|
|
524
511
|
? {
|
|
@@ -527,61 +514,74 @@ function readLayerConfig(layer) {
|
|
|
527
514
|
}
|
|
528
515
|
: { currentWorkspace: null, workspaces: [] };
|
|
529
516
|
}
|
|
517
|
+
function writeRawWorkspaceData(data, layer) {
|
|
518
|
+
const projectTarget = layer === 'project' ? getProjectWriteTarget() : null;
|
|
519
|
+
const targetPath = projectTarget?.configPath ?? getUserConfigPath();
|
|
520
|
+
ensureDir(dirname(targetPath));
|
|
521
|
+
const existing = projectTarget
|
|
522
|
+
? readJsonFile(targetPath, () => validateProjectBootstrapConfigPathForWrite(projectTarget.projectRoot, targetPath)) ?? {}
|
|
523
|
+
: readJsonFile(targetPath, () => validateUserConfigPathForWrite(targetPath)) ?? {};
|
|
524
|
+
const merged = { ...existing, ...data };
|
|
525
|
+
const content = JSON.stringify(merged, null, 2);
|
|
526
|
+
if (projectTarget) {
|
|
527
|
+
writeProjectConfigFile(projectTarget.projectRoot, targetPath, content);
|
|
528
|
+
}
|
|
529
|
+
else {
|
|
530
|
+
writeUserConfigFile(targetPath, content);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
export function getWorkspaceConfig(workspaceId, projectRoot) {
|
|
534
|
+
const { workspaces } = readRawWorkspaceData('user');
|
|
535
|
+
return workspaces.find((w) => w.workspaceId === workspaceId) ?? null;
|
|
536
|
+
}
|
|
537
|
+
function readLayerConfig(layer) {
|
|
538
|
+
return readRawWorkspaceData(layer);
|
|
539
|
+
}
|
|
530
540
|
export function addWorkspace(workspace, layer = 'user') {
|
|
531
541
|
if (!isSafeConfigSegment(workspace.workspaceId)) {
|
|
532
542
|
throw new Error('Workspace id must only contain letters, numbers, dots, underscores, or hyphens and must not contain path traversal');
|
|
533
543
|
}
|
|
534
|
-
const config =
|
|
544
|
+
const config = readRawWorkspaceData(layer);
|
|
535
545
|
const workspaces = config.workspaces;
|
|
536
546
|
const existing = workspaces.findIndex((w) => w.workspaceId === workspace.workspaceId);
|
|
537
547
|
const updatedWorkspaces = existing >= 0
|
|
538
548
|
? workspaces.map((existingWorkspace) => existingWorkspace.workspaceId === workspace.workspaceId ? workspace : existingWorkspace)
|
|
539
549
|
: [...workspaces, workspace];
|
|
540
|
-
|
|
550
|
+
writeRawWorkspaceData({ workspaces: updatedWorkspaces }, layer);
|
|
541
551
|
}
|
|
542
552
|
export function removeWorkspace(workspaceId, layer = 'user') {
|
|
543
553
|
if (!isSafeConfigSegment(workspaceId))
|
|
544
554
|
return false;
|
|
545
|
-
const config =
|
|
555
|
+
const config = readRawWorkspaceData(layer);
|
|
546
556
|
const workspaces = config.workspaces;
|
|
547
557
|
const idx = workspaces.findIndex((w) => w.workspaceId === workspaceId);
|
|
548
558
|
if (idx < 0)
|
|
549
559
|
return false;
|
|
550
560
|
const updatedWorkspaces = workspaces.filter((w) => w.workspaceId !== workspaceId);
|
|
551
561
|
const currentWorkspace = config.currentWorkspace === workspaceId ? updatedWorkspaces[0]?.workspaceId ?? null : config.currentWorkspace ?? null;
|
|
552
|
-
|
|
562
|
+
writeRawWorkspaceData({ workspaces: updatedWorkspaces, currentWorkspace }, layer);
|
|
553
563
|
return true;
|
|
554
564
|
}
|
|
555
565
|
export function setCurrentWorkspace(workspaceId, layer = 'user') {
|
|
556
566
|
if (!isSafeConfigSegment(workspaceId))
|
|
557
567
|
return false;
|
|
558
|
-
const config =
|
|
568
|
+
const config = readRawWorkspaceData(layer);
|
|
559
569
|
const workspaces = config.workspaces;
|
|
560
570
|
const exists = workspaces.some((w) => w.workspaceId === workspaceId);
|
|
561
571
|
if (!exists)
|
|
562
572
|
return false;
|
|
563
|
-
|
|
573
|
+
writeRawWorkspaceData({ currentWorkspace: workspaceId }, layer);
|
|
564
574
|
return true;
|
|
565
575
|
}
|
|
566
576
|
export function getCurrentWorkspaceConfig() {
|
|
567
|
-
const
|
|
568
|
-
if (!
|
|
577
|
+
const { currentWorkspace, workspaces } = readRawWorkspaceData('user');
|
|
578
|
+
if (!currentWorkspace)
|
|
569
579
|
return null;
|
|
570
|
-
return
|
|
580
|
+
return workspaces.find((w) => w.workspaceId === currentWorkspace) ?? null;
|
|
571
581
|
}
|
|
572
582
|
export function getWorkspaceConfigForPath(path = process.cwd()) {
|
|
573
|
-
const
|
|
574
|
-
return findWorkspaceForPath(
|
|
575
|
-
}
|
|
576
|
-
function createWorkspaceId(projectRoot, existingIds) {
|
|
577
|
-
const base = toSafeConfigSegment(basename(projectRoot));
|
|
578
|
-
if (!existingIds.has(base))
|
|
579
|
-
return base;
|
|
580
|
-
let suffix = 2;
|
|
581
|
-
while (existingIds.has(`${base}-${suffix}`)) {
|
|
582
|
-
suffix += 1;
|
|
583
|
-
}
|
|
584
|
-
return `${base}-${suffix}`;
|
|
583
|
+
const { workspaces } = readRawWorkspaceData('user');
|
|
584
|
+
return findWorkspaceForPath(workspaces, path);
|
|
585
585
|
}
|
|
586
586
|
function findWorkspaceForPath(workspaces, path) {
|
|
587
587
|
const targetPath = stablePath(path);
|
|
@@ -596,7 +596,7 @@ function findWorkspaceForPath(workspaces, path) {
|
|
|
596
596
|
return matches.reduce((best, match) => match.rootPath.length > best.rootPath.length ? match : best).workspace;
|
|
597
597
|
}
|
|
598
598
|
function getWorkspaceArtifactRoot(workspace) {
|
|
599
|
-
return workspace.artifactStorage?.localPath ? resolve(workspace.artifactStorage.localPath) : resolve(
|
|
599
|
+
return workspace.artifactStorage?.localPath ? resolve(workspace.artifactStorage.localPath) : resolve(workspace.rootPath, '.peaks', 'artifacts');
|
|
600
600
|
}
|
|
601
601
|
function ensureArtifactWorkspaceMarker(workspace) {
|
|
602
602
|
const artifactRoot = getWorkspaceArtifactRoot(workspace);
|
|
@@ -618,24 +618,9 @@ export function ensureWorkspaceConfigForPath(path = process.cwd()) {
|
|
|
618
618
|
const existingWorkspace = findWorkspaceForPath(config.workspaces, path);
|
|
619
619
|
if (existingWorkspace) {
|
|
620
620
|
ensureArtifactWorkspaceMarker(existingWorkspace);
|
|
621
|
-
if (!config.currentWorkspace) {
|
|
622
|
-
writeConfig({ currentWorkspace: existingWorkspace.workspaceId }, 'user');
|
|
623
|
-
}
|
|
624
621
|
return existingWorkspace;
|
|
625
622
|
}
|
|
626
|
-
|
|
627
|
-
const workspaceId = createWorkspaceId(projectRoot, existingIds);
|
|
628
|
-
const workspace = {
|
|
629
|
-
workspaceId,
|
|
630
|
-
name: basename(projectRoot) || 'Workspace',
|
|
631
|
-
rootPath: stablePath(projectRoot),
|
|
632
|
-
artifactStorage: { mode: 'local', localPath: resolve(homedir(), '.peaks', 'workspaces', workspaceId, 'artifacts') },
|
|
633
|
-
installedCapabilityIds: []
|
|
634
|
-
};
|
|
635
|
-
ensureArtifactWorkspaceMarker(workspace);
|
|
636
|
-
const updatedWorkspaces = [...config.workspaces, workspace];
|
|
637
|
-
writeConfig({ workspaces: updatedWorkspaces, ...(!config.currentWorkspace ? { currentWorkspace: workspace.workspaceId } : {}) }, 'user');
|
|
638
|
-
return workspace;
|
|
623
|
+
return null;
|
|
639
624
|
}
|
|
640
625
|
export function getWorkspaceConfigForCurrentPath() {
|
|
641
626
|
return getWorkspaceConfigForPath(process.cwd());
|
|
@@ -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,
|
|
@@ -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,44 @@
|
|
|
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
|
+
try {
|
|
19
|
+
const content = readFileSync(filePath, 'utf8');
|
|
20
|
+
return content.split(/\r?\n/).length;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return 0;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export function scanFileSize(options) {
|
|
27
|
+
const baseRef = options.baseRef ?? 'HEAD';
|
|
28
|
+
const threshold = options.threshold ?? DEFAULT_FILE_SIZE_THRESHOLD;
|
|
29
|
+
const files = getChangedFiles(options.projectRoot, baseRef);
|
|
30
|
+
const violations = [];
|
|
31
|
+
for (const file of files) {
|
|
32
|
+
const absolute = join(options.projectRoot, file);
|
|
33
|
+
const lines = countLines(absolute);
|
|
34
|
+
if (lines > threshold) {
|
|
35
|
+
violations.push({ file, lines });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
ok: violations.length === 0,
|
|
40
|
+
threshold,
|
|
41
|
+
checkedFiles: files.length,
|
|
42
|
+
violations
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -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;
|