peaks-cli 1.2.3 → 1.2.5

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 (32) hide show
  1. package/dist/src/cli/commands/openspec-commands.js +31 -0
  2. package/dist/src/cli/commands/project-commands.js +51 -1
  3. package/dist/src/cli/commands/sop-commands.js +2 -2
  4. package/dist/src/cli/commands/workspace-commands.js +38 -4
  5. package/dist/src/services/memory/project-memory-service.d.ts +50 -0
  6. package/dist/src/services/memory/project-memory-service.js +412 -35
  7. package/dist/src/services/openspec/openspec-init-service.d.ts +23 -0
  8. package/dist/src/services/openspec/openspec-init-service.js +122 -0
  9. package/dist/src/services/session/index.d.ts +1 -1
  10. package/dist/src/services/session/index.js +1 -1
  11. package/dist/src/services/session/session-manager.d.ts +11 -0
  12. package/dist/src/services/session/session-manager.js +19 -0
  13. package/dist/src/services/skills/skill-presence-service.js +11 -0
  14. package/dist/src/services/sop/sop-check-service.d.ts +16 -0
  15. package/dist/src/services/sop/sop-check-service.js +35 -2
  16. package/dist/src/services/sop/sop-service.d.ts +8 -0
  17. package/dist/src/services/sop/sop-service.js +13 -2
  18. package/dist/src/services/sop/sop-types.d.ts +7 -0
  19. package/dist/src/services/workspace/workspace-service.d.ts +15 -0
  20. package/dist/src/services/workspace/workspace-service.js +60 -1
  21. package/dist/src/shared/version.d.ts +1 -1
  22. package/dist/src/shared/version.js +1 -1
  23. package/package.json +1 -1
  24. package/skills/peaks-prd/SKILL.md +36 -0
  25. package/skills/peaks-qa/SKILL.md +92 -2
  26. package/skills/peaks-rd/SKILL.md +70 -2
  27. package/skills/peaks-solo/SKILL.md +253 -40
  28. package/skills/peaks-solo/references/swarm-dispatch-contract.md +186 -0
  29. package/skills/peaks-sop/SKILL.md +17 -0
  30. package/skills/peaks-sop/references/sop-authoring.md +1 -1
  31. package/skills/peaks-txt/SKILL.md +16 -0
  32. package/skills/peaks-ui/SKILL.md +61 -2
@@ -4,6 +4,7 @@ import { projectOpenSpecToRdInput } from '../../services/openspec/openspec-bridg
4
4
  import { renderOpenSpecChange } from '../../services/openspec/openspec-render-service.js';
5
5
  import { validateOpenSpecChange } from '../../services/openspec/openspec-validate-service.js';
6
6
  import { archiveOpenSpecChange } from '../../services/openspec/openspec-archive-service.js';
7
+ import { executeOpenSpecInit } from '../../services/openspec/openspec-init-service.js';
7
8
  import { fail, ok } from '../../shared/result.js';
8
9
  import { addJsonOption, getErrorMessage, printResult } from '../cli-helpers.js';
9
10
  function resolveScanOptions(project) {
@@ -166,4 +167,34 @@ export function registerOpenSpecCommands(program, io) {
166
167
  process.exitCode = 1;
167
168
  }
168
169
  });
170
+ addJsonOption(openspec
171
+ .command('init')
172
+ .description('Scaffold the openspec/ directory in the target project (changes/, archive/, README.md, CHANGES.md). Idempotent — refuses to overwrite an existing openspec/')
173
+ .requiredOption('--project <path>', 'target project root')
174
+ .option('--apply', 'write files to disk (default: dry-run preview)', false)).action(async (options) => {
175
+ try {
176
+ const result = await executeOpenSpecInit({
177
+ projectRoot: options.project,
178
+ apply: options.apply === true
179
+ });
180
+ const nextActions = [];
181
+ if (result.alreadyInitialized) {
182
+ nextActions.push('openspec/ already exists; no files were created or modified.');
183
+ if (result.existingFiles.length > 0) {
184
+ nextActions.push(`Existing files preserved: ${result.existingFiles.join(', ')}`);
185
+ }
186
+ }
187
+ else if (!result.apply) {
188
+ nextActions.push('Re-run with --apply to write the planned files to disk.');
189
+ }
190
+ else {
191
+ nextActions.push('Run `peaks openspec render --request <path> --apply` to scaffold a first change proposal.');
192
+ }
193
+ printResult(io, ok('openspec.init', result, [], nextActions), options.json);
194
+ }
195
+ catch (error) {
196
+ printResult(io, fail('openspec.init', 'OPENSPEC_INIT_FAILED', getErrorMessage(error), { projectRoot: options.project }, ['Verify the project path exists and is writable']), options.json);
197
+ process.exitCode = 1;
198
+ }
199
+ });
169
200
  }
@@ -1,6 +1,6 @@
1
1
  import { loadProjectDashboard } from '../../services/dashboard/project-dashboard-service.js';
2
2
  import { generateProjectContext, readProjectContext } from '../../services/memory/project-context-service.js';
3
- import { readProjectMemories } from '../../services/memory/project-memory-service.js';
3
+ import { extractSessionMemories, readMemoryIndex, readProjectMemories } from '../../services/memory/project-memory-service.js';
4
4
  import { fail, ok } from '../../shared/result.js';
5
5
  import { addJsonOption, getErrorMessage, printResult } from '../cli-helpers.js';
6
6
  export function registerProjectCommands(program, io) {
@@ -68,6 +68,56 @@ export function registerProjectCommands(program, io) {
68
68
  process.exitCode = 1;
69
69
  }
70
70
  });
71
+ // --- Extract memories from a session's artifacts into .peaks/memory ---
72
+ addJsonOption(project
73
+ .command('memories:extract')
74
+ .description('Scan a session artifact directory and extract <!-- peaks-memory:start --> blocks into .peaks/memory/')
75
+ .requiredOption('--session-id <id>', 'session id (e.g. 2026-05-29-session-89ff35)')
76
+ .requiredOption('--project <path>', 'target project root')
77
+ .option('--dry-run', 'preview writes without changing files', true)
78
+ .option('--apply', 'write extracted memories into .peaks/memory/')).action((options) => {
79
+ if (options.dryRun === true && options.apply === true) {
80
+ printResult(io, fail('project.memories:extract', 'INVALID_MEMORY_EXTRACT_FLAGS', 'Use either --dry-run or --apply, not both', { sessionId: options.sessionId, projectRoot: options.project }, ['Run without --apply to preview writes, or pass --apply to write memories']), options.json);
81
+ process.exitCode = 1;
82
+ return;
83
+ }
84
+ try {
85
+ const result = extractSessionMemories({
86
+ projectRoot: options.project,
87
+ sessionId: options.sessionId,
88
+ apply: options.apply === true
89
+ });
90
+ printResult(io, ok('project.memories:extract', {
91
+ scannedFiles: result.scannedFiles,
92
+ extractedCount: result.extractedCount,
93
+ writtenFiles: result.writtenFiles,
94
+ memoryDir: result.primaryMemoryDir,
95
+ indexUpdated: result.updatedIndex
96
+ }), options.json);
97
+ }
98
+ catch (error) {
99
+ printResult(io, fail('project.memories:extract', 'MEMORY_EXTRACT_FAILED', getErrorMessage(error), { sessionId: options.sessionId, projectRoot: options.project }, ['Check the session-id and project path']), options.json);
100
+ process.exitCode = 1;
101
+ }
102
+ });
103
+ // --- Read memory index (lightweight, always-safe to load) ---
104
+ addJsonOption(project
105
+ .command('memory-index')
106
+ .description('Read the memory index — lightweight hot/warm分层 view of all project memories')
107
+ .requiredOption('--project <path>', 'target project root')).action((options) => {
108
+ try {
109
+ const index = readMemoryIndex(options.project);
110
+ if (!index) {
111
+ printResult(io, ok('project.memory-index', { exists: false, message: 'No memory index found. Run `peaks project memories:extract` first.' }), options.json);
112
+ return;
113
+ }
114
+ printResult(io, ok('project.memory-index', { exists: true, index }), options.json);
115
+ }
116
+ catch (error) {
117
+ printResult(io, fail('project.memory-index', 'MEMORY_INDEX_FAILED', getErrorMessage(error), { projectRoot: options.project }, ['Check the project path and .peaks/memory directory']), options.json);
118
+ process.exitCode = 1;
119
+ }
120
+ });
71
121
  // --- Structured project memory (durable, LLM-authored, stored under .peaks/memory) ---
72
122
  addJsonOption(project
73
123
  .command('memories')
@@ -105,8 +105,8 @@ export function registerSopCommands(program, io) {
105
105
  });
106
106
  addJsonOption(sop
107
107
  .command('registry')
108
- .description('List registered SOPs and gates (global; --project merges in the repo layer)')
109
- .option('--project <path>', 'also include and prefer the repo layer (<path>/.peaks/sops)')).action(async (options) => {
108
+ .description('List registered SOPs and gates (global; merges in the cwd project layer by default)')
109
+ .option('--project <path>', 'also include and prefer the repo layer (<path>/.peaks/sops) (default: current directory)', '.')).action(async (options) => {
110
110
  try {
111
111
  const registry = await readRegistry(options.project);
112
112
  printResult(io, ok('sop.registry', registry), options.json);
@@ -1,16 +1,39 @@
1
- import { initWorkspace, InvalidSessionIdError } from '../../services/workspace/workspace-service.js';
1
+ import { initWorkspace, InvalidSessionIdError, ConflictingSessionError } from '../../services/workspace/workspace-service.js';
2
+ import { ensureSession } from '../../services/session/session-manager.js';
2
3
  import { fail, ok } from '../../shared/result.js';
3
4
  import { addJsonOption, getErrorMessage, printResult } from '../cli-helpers.js';
4
5
  export function registerWorkspaceCommands(program, io) {
5
6
  const workspace = program.command('workspace').description('Manage the Peaks per-session artifact workspace (.peaks/<session-id>/)');
6
7
  addJsonOption(workspace
7
8
  .command('init')
8
- .description('Create the .peaks/<session-id>/ directory structure (prd, ui, rd, qa, sc, txt, system) validates the session id format')
9
+ .description('Create the .peaks/<session-id>/ directory structure (prd, ui, rd, qa, sc, txt, system) and bind the session as the project current one. Pass --session-id to use a specific id, or omit it to auto-generate one (and adopt an existing binding if present).')
9
10
  .requiredOption('--project <path>', 'target project root')
10
- .requiredOption('--session-id <id>', 'session id in YYYY-MM-DD-<kebab-slug> format')).action(async (options) => {
11
+ .option('--session-id <id>', 'optional session id in YYYY-MM-DD-<kebab-slug> format. When omitted, the CLI is the single source of truth: an existing binding is reused, otherwise a fresh id is auto-generated.')
12
+ .option('--allow-session-rebind', 'overwrite an existing session binding when the requested session id differs from the project current one', false)).action(async (options) => {
11
13
  try {
12
- const report = await initWorkspace({ projectRoot: options.project, sessionId: options.sessionId });
14
+ // Resolve the session id. Two paths:
15
+ // - explicit --session-id: use it as the requested binding target
16
+ // (ConflictingSessionError fires if it conflicts with an in-flight
17
+ // session, unless --allow-session-rebind is set)
18
+ // - omitted: defer to ensureSession(), which reuses an existing
19
+ // binding or auto-generates a fresh one. The init then writes
20
+ // .session.json so the binding sticks.
21
+ let sessionId;
22
+ if (options.sessionId !== undefined && options.sessionId.length > 0) {
23
+ sessionId = options.sessionId;
24
+ }
25
+ else {
26
+ sessionId = await ensureSession(options.project);
27
+ }
28
+ const report = await initWorkspace({
29
+ projectRoot: options.project,
30
+ sessionId,
31
+ allowSessionRebind: options.allowSessionRebind === true
32
+ });
13
33
  const nextActions = [];
34
+ if (report.previousSessionId !== null && report.bound) {
35
+ nextActions.push(`Replaced prior session binding "${report.previousSessionId}" with "${report.sessionId}".`);
36
+ }
14
37
  if (report.created.length === 0) {
15
38
  nextActions.push('Workspace already initialized — proceed to project scan.');
16
39
  }
@@ -25,6 +48,17 @@ export function registerWorkspaceCommands(program, io) {
25
48
  process.exitCode = 1;
26
49
  return;
27
50
  }
51
+ if (error instanceof ConflictingSessionError) {
52
+ printResult(io, fail('workspace.init', error.code, error.message, {
53
+ existingSessionId: error.existingSessionId,
54
+ requestedSessionId: error.requestedSessionId
55
+ }, [
56
+ `Finish or abandon session "${error.existingSessionId}" first, then re-run workspace init.`,
57
+ 'Or pass --allow-session-rebind to override the binding (overwrites the prior binding).'
58
+ ]), options.json);
59
+ process.exitCode = 1;
60
+ return;
61
+ }
28
62
  printResult(io, fail('workspace.init', 'WORKSPACE_INIT_FAILED', getErrorMessage(error), { projectRoot: options.project, sessionId: options.sessionId }, ['Verify the project path exists and is writable']), options.json);
29
63
  process.exitCode = 1;
30
64
  }
@@ -74,6 +74,36 @@ export type ProjectMemoryReadResult = {
74
74
  byKind: Record<ProjectMemoryKind, StoredProjectMemory[]>;
75
75
  memories: StoredProjectMemory[];
76
76
  };
77
+ export type MemoryIndexEntry = {
78
+ name: string;
79
+ kind: ProjectMemoryKind;
80
+ description: string;
81
+ sourcePath: string;
82
+ sourceArtifact: string | null;
83
+ updatedAt: string;
84
+ };
85
+ export type MemoryIndex = {
86
+ version: 1;
87
+ updatedAt: string;
88
+ hot: Record<ProjectMemoryKind, MemoryIndexEntry[]>;
89
+ warm: Record<ProjectMemoryKind, MemoryIndexEntry[]>;
90
+ };
91
+ export type ExtractSessionMemoriesOptions = {
92
+ projectRoot: string;
93
+ sessionId: string;
94
+ apply?: boolean;
95
+ };
96
+ export type ExtractSessionMemoriesResult = {
97
+ apply: boolean;
98
+ projectRoot: string;
99
+ sessionId: string;
100
+ primaryMemoryDir: string;
101
+ memoryIndexPath: string;
102
+ scannedFiles: number;
103
+ extractedCount: number;
104
+ writtenFiles: string[];
105
+ updatedIndex: boolean;
106
+ };
77
107
  type ExtractPlanOptions = {
78
108
  projectRoot: string;
79
109
  artifactPaths: string[];
@@ -84,6 +114,8 @@ type BackupPlanOptions = {
84
114
  artifactWorkspacePath: string;
85
115
  apply?: boolean;
86
116
  };
117
+ export declare function readMemoryIndex(projectRoot: string): MemoryIndex | null;
118
+ export declare function extractSessionMemories(options: ExtractSessionMemoriesOptions): ExtractSessionMemoriesResult;
87
119
  export declare function extractStableProjectMemories(content: string, sourceArtifact: string): ExtractedProjectMemory[];
88
120
  export declare function createProjectMemoryExtractPlan(options: ExtractPlanOptions): ProjectMemoryExtractPlan;
89
121
  export declare function executeProjectMemoryExtract(options: ExtractPlanOptions): ProjectMemoryExtractResult;
@@ -91,5 +123,23 @@ export declare function createProjectMemoryBackupPlan(options: BackupPlanOptions
91
123
  export declare function executeProjectMemoryBackup(options: BackupPlanOptions): ProjectMemoryBackupResult;
92
124
  export declare function summarizeProjectMemoryExtractResult(result: ProjectMemoryExtractResult): ProjectMemoryExtractSummary;
93
125
  export declare function summarizeProjectMemoryBackupResult(result: ProjectMemoryBackupResult): ProjectMemoryBackupSummary;
126
+ /**
127
+ * Ensure `.peaks/memory/` and its `index.json` exist for a project, with
128
+ * the same full-shape empty index the generator emits when there are zero
129
+ * memories. Idempotent — safe to call on every skill activation.
130
+ *
131
+ * Why this exists: before this helper, `.peaks/memory/` was only created
132
+ * by `extractSessionMemories` when at least one memory markdown was being
133
+ * written, and `index.json` was only emitted by the generator when at
134
+ * least one markdown was on disk. Stock projects therefore had no
135
+ * `.peaks/memory/` directory and no index, even after `peaks project
136
+ * memories` was read. Bootstrap closes that cold-start gap.
137
+ *
138
+ * This function is fail-open for the same reason the rest of the
139
+ * presence layer is fail-open: a failure here must NOT block skill
140
+ * activation. Any error is swallowed and surfaced only via the returned
141
+ * boolean. Callers that need the truth should check the result.
142
+ */
143
+ export declare function ensureMemoryBootstrap(projectRoot: string): boolean;
94
144
  export declare function readProjectMemories(projectRoot: string): ProjectMemoryReadResult;
95
145
  export {};