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
@@ -1,6 +1,5 @@
1
1
  import { existsSync, mkdirSync } from 'node:fs';
2
- import { homedir } from 'node:os';
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, workspaces: projectWorkspaces, ...projectConfigWithoutProxy } = projectConfig;
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, workspaces: projectWorkspaces, ...projectConfigWithoutProxy } = projectConfig;
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
- export function getWorkspaceConfig(workspaceId, projectRoot) {
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 = readLayerConfig(layer);
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
- writeConfig({ workspaces: updatedWorkspaces }, layer);
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 = readLayerConfig(layer);
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
- writeConfig({ workspaces: updatedWorkspaces, currentWorkspace }, layer);
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 = readLayerConfig(layer);
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
- writeConfig({ currentWorkspace: workspaceId }, layer);
573
+ writeRawWorkspaceData({ currentWorkspace: workspaceId }, layer);
564
574
  return true;
565
575
  }
566
576
  export function getCurrentWorkspaceConfig() {
567
- const config = readConfig();
568
- if (!config.currentWorkspace)
577
+ const { currentWorkspace, workspaces } = readRawWorkspaceData('user');
578
+ if (!currentWorkspace)
569
579
  return null;
570
- return getWorkspaceConfig(config.currentWorkspace);
580
+ return workspaces.find((w) => w.workspaceId === currentWorkspace) ?? null;
571
581
  }
572
582
  export function getWorkspaceConfigForPath(path = process.cwd()) {
573
- const config = readConfig(findProjectRoot(path));
574
- return findWorkspaceForPath(config.workspaces, path);
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(homedir(), '.peaks', 'workspaces', workspace.workspaceId, 'artifacts');
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
- const existingIds = new Set(config.workspaces.map((workspace) => workspace.workspaceId));
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());
@@ -51,8 +51,6 @@ export type WorkspaceConfig = {
51
51
  };
52
52
  export type PeaksConfig = {
53
53
  version: string;
54
- currentWorkspace: string | null;
55
- workspaces: WorkspaceConfig[];
56
54
  language: string;
57
55
  model: ModelPreference;
58
56
  economyMode: boolean;
@@ -1,8 +1,6 @@
1
1
  import { CLI_VERSION } from '../../shared/version.js';
2
2
  export const DEFAULT_CONFIG = {
3
3
  version: CLI_VERSION,
4
- currentWorkspace: null,
5
- workspaces: [],
6
4
  language: 'en',
7
5
  model: 'sonnet',
8
6
  economyMode: true,
@@ -0,0 +1,4 @@
1
+ export declare const MAX_BYPASSES_PER_SESSION = 3;
2
+ export declare function getBypassCount(sessionRoot: string): number;
3
+ export declare function recordBypass(sessionRoot: string): number;
4
+ export declare function isBypassLimitReached(sessionRoot: string): boolean;
@@ -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 { getCurrentWorkspaceConfig } from '../config/config-service.js';
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 = getCurrentWorkspaceConfig();
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 = getCurrentWorkspaceConfig();
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 = getCurrentWorkspaceConfig();
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 = getCurrentWorkspaceConfig();
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;