peaks-cli 1.0.27 → 1.0.28

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.
@@ -10,6 +10,7 @@ import { inspectSkillRunbook } from '../../services/skills/skill-runbook-service
10
10
  import { setSkillPresence, clearSkillPresence, getSkillPresence, isSkillPresenceMode, touchSkillHeartbeat } from '../../services/skills/skill-presence-service.js';
11
11
  import { ensureSession, getSessionMeta, setSessionMeta, setSessionTitle, listSessionMetas } from '../../services/session/session-manager.js';
12
12
  import { findProjectRoot } from '../../services/config/config-safety.js';
13
+ import { generateProjectContext } from '../../services/memory/project-context-service.js';
13
14
  import { fail, ok } from '../../shared/result.js';
14
15
  import { addJsonOption, failUnsupportedNonDryRun, getErrorMessage, isArtifactProvider, isArtifactSetupStep, printResult } from '../cli-helpers.js';
15
16
  export function registerCoreAndArtifactCommands(program, io) {
@@ -73,15 +74,16 @@ export function registerCoreAndArtifactCommands(program, io) {
73
74
  .command('presence:set <name>')
74
75
  .description('Set the currently active Peaks skill for session-wide visibility')
75
76
  .option('--mode <mode>', 'execution mode')
76
- .option('--gate <gate>', 'current gate')).action(async (name, options) => {
77
+ .option('--gate <gate>', 'current gate')
78
+ .option('--project <path>', 'project root path (auto-detected from cwd when omitted)')).action(async (name, options) => {
79
+ const projectRoot = options.project ?? findProjectRoot(process.cwd()) ?? process.cwd();
77
80
  if (options.mode !== undefined && !isSkillPresenceMode(options.mode)) {
78
81
  printResult(io, fail('skill.presence:set', 'INVALID_MODE', `Invalid mode: ${options.mode} (expected one of: full-auto, assisted, swarm, strict)`, { name, mode: options.mode }, ['Use a valid mode: full-auto, assisted, swarm, or strict']), options.json);
79
82
  process.exitCode = 1;
80
83
  return;
81
84
  }
82
- const presence = setSkillPresence(name, options.mode, options.gate);
85
+ const presence = setSkillPresence(name, options.mode, options.gate, options.project);
83
86
  // Also update session metadata so session dirs self-document
84
- const projectRoot = findProjectRoot(process.cwd()) ?? process.cwd();
85
87
  const sessionId = await ensureSession(projectRoot);
86
88
  setSessionMeta(projectRoot, sessionId, {
87
89
  skill: name,
@@ -92,9 +94,18 @@ export function registerCoreAndArtifactCommands(program, io) {
92
94
  });
93
95
  addJsonOption(skill
94
96
  .command('presence:clear')
95
- .description('Clear the active Peaks skill presence indicator')).action((options) => {
96
- const removed = clearSkillPresence();
97
- printResult(io, ok('skill.presence:clear', { active: false, removed }), options.json);
97
+ .description('Clear the active Peaks skill presence indicator and update project context')
98
+ .option('--project <path>', 'project root path (auto-detected from cwd when omitted)')).action((options) => {
99
+ const projectRoot = options.project ?? findProjectRoot(process.cwd()) ?? process.cwd();
100
+ const removed = clearSkillPresence(options.project);
101
+ // Auto-update project context so future sessions have up-to-date history
102
+ try {
103
+ generateProjectContext(projectRoot);
104
+ }
105
+ catch {
106
+ // non-fatal: context update failure should not block presence clear
107
+ }
108
+ printResult(io, ok('skill.presence:clear', { active: false, removed, projectContextUpdated: true }), options.json);
98
109
  });
99
110
  addJsonOption(skill
100
111
  .command('heartbeat')
@@ -1,6 +1,15 @@
1
1
  import { loadProjectDashboard } from '../../services/dashboard/project-dashboard-service.js';
2
+ import { generateProjectContext, loadOntology, readProjectContext, upsertDecision, upsertModule } from '../../services/memory/project-context-service.js';
2
3
  import { fail, ok } from '../../shared/result.js';
3
4
  import { addJsonOption, getErrorMessage, printResult } from '../cli-helpers.js';
5
+ function defined(obj) {
6
+ const result = {};
7
+ for (const [k, v] of Object.entries(obj)) {
8
+ if (v !== undefined)
9
+ result[k] = v;
10
+ }
11
+ return result;
12
+ }
4
13
  export function registerProjectCommands(program, io) {
5
14
  const project = program.command('project').description('Aggregate Peaks state for a target project (read-only)');
6
15
  addJsonOption(project
@@ -39,4 +48,132 @@ export function registerProjectCommands(program, io) {
39
48
  process.exitCode = 1;
40
49
  }
41
50
  });
51
+ addJsonOption(project
52
+ .command('context')
53
+ .description('Generate or read persistent project context for cross-session Peaks understanding')
54
+ .requiredOption('--project <path>', 'target project root')
55
+ .option('--read', 'read existing PROJECT.md without regenerating')).action((options) => {
56
+ try {
57
+ if (options.read) {
58
+ const content = readProjectContext(options.project);
59
+ if (content === null) {
60
+ printResult(io, ok('project.context', { exists: false, path: `${options.project}/.peaks/PROJECT.md` }), options.json);
61
+ return;
62
+ }
63
+ printResult(io, ok('project.context', { exists: true, path: `${options.project}/.peaks/PROJECT.md`, content }), options.json);
64
+ return;
65
+ }
66
+ const result = generateProjectContext(options.project);
67
+ printResult(io, ok('project.context', {
68
+ path: result.path,
69
+ sessionCount: result.sessionCount,
70
+ content: result.content
71
+ }), options.json);
72
+ }
73
+ catch (error) {
74
+ printResult(io, fail('project.context', 'PROJECT_CONTEXT_FAILED', getErrorMessage(error), { projectRoot: options.project }, ['Check the project path and .peaks directory']), options.json);
75
+ process.exitCode = 1;
76
+ }
77
+ });
78
+ // --- Ontology commands (structured project memory for LLM consumption) ---
79
+ const ontology = project.command('ontology').description('Query structured project memory (modules, decisions, conventions)');
80
+ addJsonOption(ontology
81
+ .command('show')
82
+ .description('Read the full ontology JSON for LLM consumption')
83
+ .requiredOption('--project <path>', 'target project root')).action((options) => {
84
+ try {
85
+ const onto = loadOntology(options.project);
86
+ if (onto === null) {
87
+ // Auto-generate if missing
88
+ const result = generateProjectContext(options.project);
89
+ printResult(io, ok('project.ontology', result.ontology), options.json);
90
+ return;
91
+ }
92
+ printResult(io, ok('project.ontology', onto), options.json);
93
+ }
94
+ catch (error) {
95
+ printResult(io, fail('project.ontology', 'ONTOLOGY_FAILED', getErrorMessage(error), { projectRoot: options.project }, ['Check the project path']), options.json);
96
+ process.exitCode = 1;
97
+ }
98
+ });
99
+ addJsonOption(ontology
100
+ .command('module')
101
+ .description('Record or query a project module')
102
+ .requiredOption('--project <path>', 'target project root')
103
+ .option('--id <id>', 'module id (kebab-case)')
104
+ .option('--path <path>', 'file path for the module')
105
+ .option('--risk <level>', 'risk level: low, medium, high')
106
+ .option('--summary <text>', 'brief module description')
107
+ .option('--session <id>', 'session id')
108
+ .option('--put', 'write/update the module entry')).action((options) => {
109
+ try {
110
+ if (options.put) {
111
+ if (!options.id || !options.path || !options.session) {
112
+ printResult(io, fail('project.ontology.module', 'MISSING_FIELDS', '--id, --path, --session required with --put', {}, ['Provide all required fields']), options.json);
113
+ process.exitCode = 1;
114
+ return;
115
+ }
116
+ const risk = (options.risk === 'low' || options.risk === 'medium' || options.risk === 'high') ? options.risk : undefined;
117
+ const result = upsertModule(options.project, defined({
118
+ id: options.id,
119
+ path: options.path,
120
+ session: options.session,
121
+ risk,
122
+ summary: options.summary
123
+ }));
124
+ printResult(io, ok('project.ontology.module', { modules: result.modules }), options.json);
125
+ return;
126
+ }
127
+ const onto = loadOntology(options.project) ?? generateProjectContext(options.project).ontology;
128
+ if (options.id) {
129
+ const mod = onto.modules.find((m) => m.id === options.id);
130
+ printResult(io, ok('project.ontology.module', mod ?? { notFound: true, id: options.id }), options.json);
131
+ return;
132
+ }
133
+ printResult(io, ok('project.ontology.module', { modules: onto.modules }), options.json);
134
+ }
135
+ catch (error) {
136
+ printResult(io, fail('project.ontology.module', 'ONTOLOGY_MODULE_FAILED', getErrorMessage(error), {}, []), options.json);
137
+ process.exitCode = 1;
138
+ }
139
+ });
140
+ addJsonOption(ontology
141
+ .command('decision')
142
+ .description('Record or query architectural decisions')
143
+ .requiredOption('--project <path>', 'target project root')
144
+ .option('--id <id>', 'decision id')
145
+ .option('--what <text>', 'what was decided')
146
+ .option('--why <text>', 'rationale behind the decision')
147
+ .option('--scope <modules>', 'comma-separated module ids')
148
+ .option('--session <id>', 'session id')
149
+ .option('--date <date>', 'decision date')
150
+ .option('--put', 'write/update the decision')).action((options) => {
151
+ try {
152
+ if (options.put) {
153
+ if (!options.id || !options.what || !options.session || !options.date) {
154
+ printResult(io, fail('project.ontology.decision', 'MISSING_FIELDS', '--id, --what, --session, --date required with --put', {}, []), options.json);
155
+ process.exitCode = 1;
156
+ return;
157
+ }
158
+ const result = upsertDecision(options.project, defined({
159
+ id: options.id, what: options.what, why: options.why,
160
+ scope: options.scope ? options.scope.split(',').map((s) => s.trim()).filter(Boolean) : [],
161
+ session: options.session, date: options.date
162
+ }));
163
+ printResult(io, ok('project.ontology.decision', { decisions: result.decisions }), options.json);
164
+ return;
165
+ }
166
+ const onto = loadOntology(options.project) ?? generateProjectContext(options.project).ontology;
167
+ if (options.id) {
168
+ const dec = onto.decisions.find((d) => d.id === options.id);
169
+ printResult(io, ok('project.ontology.decision', dec ?? { notFound: true, id: options.id }), options.json);
170
+ return;
171
+ }
172
+ printResult(io, ok('project.ontology.decision', { decisions: onto.decisions }), options.json);
173
+ }
174
+ catch (error) {
175
+ printResult(io, fail('project.ontology.decision', 'ONTOLOGY_DECISION_FAILED', getErrorMessage(error), {}, []), options.json);
176
+ process.exitCode = 1;
177
+ }
178
+ });
42
179
  }
@@ -0,0 +1,49 @@
1
+ export type ProjectContextSection = {
2
+ heading: string;
3
+ body: string;
4
+ };
5
+ export type Module = {
6
+ id: string;
7
+ path: string;
8
+ risk?: 'low' | 'medium' | 'high';
9
+ sessions: string[];
10
+ summary?: string;
11
+ };
12
+ export type Decision = {
13
+ id: string;
14
+ what: string;
15
+ why?: string;
16
+ scope: string[];
17
+ session: string;
18
+ date: string;
19
+ };
20
+ export type Convention = {
21
+ id: string;
22
+ rule: string;
23
+ category: 'code-style' | 'architecture' | 'naming' | 'tooling' | 'other';
24
+ source: string;
25
+ date: string;
26
+ };
27
+ export type Ontology = {
28
+ version: 1;
29
+ updated: string;
30
+ project: string;
31
+ modules: Module[];
32
+ decisions: Decision[];
33
+ conventions: Convention[];
34
+ };
35
+ export declare function loadOntology(projectRoot: string): Ontology | null;
36
+ export declare function saveOntology(projectRoot: string, onto: Ontology): void;
37
+ export declare function upsertModule(projectRoot: string, mod: Omit<Module, 'sessions'> & {
38
+ session: string;
39
+ }): Ontology;
40
+ export declare function upsertDecision(projectRoot: string, dec: Decision): Ontology;
41
+ export declare function upsertConvention(projectRoot: string, conv: Convention): Ontology;
42
+ export declare function generateProjectContext(projectRoot: string): {
43
+ path: string;
44
+ content: string;
45
+ sessionCount: number;
46
+ ontology: Ontology;
47
+ };
48
+ export declare function readProjectContext(projectRoot: string): string | null;
49
+ export declare function getProjectContextPath(projectRoot: string): string;
@@ -0,0 +1,449 @@
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { join, relative } from 'node:path';
3
+ import { listSessionMetas } from '../session/session-manager.js';
4
+ const PROJECT_CONTEXT_FILE = '.peaks/PROJECT.md';
5
+ const ONTOLOGY_FILE = '.peaks/ontology.json';
6
+ const CONTEXT_HEADER = `# Peaks Project Context
7
+
8
+ > Auto-generated project memory. Peaks reads this at the start of each session to understand
9
+ > the project's history, tech stack, conventions, and past decisions.
10
+ > Last updated: `;
11
+ const MANAGED_BLOCK_START = '<!-- peaks-managed:session-history-start -->';
12
+ const MANAGED_BLOCK_END = '<!-- peaks-managed:session-history-end -->';
13
+ function projectName(projectRoot) {
14
+ const pkgPath = join(projectRoot, 'package.json');
15
+ if (!existsSync(pkgPath))
16
+ return projectRoot.split(/[\\/]/).pop() ?? 'unknown';
17
+ try {
18
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
19
+ return pkg.name ?? projectRoot.split(/[\\/]/).pop() ?? 'unknown';
20
+ }
21
+ catch {
22
+ return projectRoot.split(/[\\/]/).pop() ?? 'unknown';
23
+ }
24
+ }
25
+ function listMdFiles(dir, maxDepth = 3) {
26
+ const results = [];
27
+ if (!existsSync(dir) || maxDepth <= 0)
28
+ return results;
29
+ let entries;
30
+ try {
31
+ entries = readdirSync(dir, { withFileTypes: true });
32
+ }
33
+ catch {
34
+ return results;
35
+ }
36
+ for (const entry of entries) {
37
+ if (entry.name.startsWith('.'))
38
+ continue;
39
+ const full = join(dir, entry.name);
40
+ if (entry.isDirectory()) {
41
+ results.push(...listMdFiles(full, maxDepth - 1));
42
+ }
43
+ else if (entry.isFile() && entry.name.endsWith('.md')) {
44
+ results.push(full);
45
+ }
46
+ }
47
+ return results;
48
+ }
49
+ function extractArtifactSummary(filePath, sessionRoot) {
50
+ try {
51
+ const content = readFileSync(filePath, 'utf8');
52
+ const lines = content.split(/\r?\n/);
53
+ const firstHeading = lines.find((l) => /^#\s/.test(l))?.replace(/^#\s+/, '').trim();
54
+ const stateLine = lines.find((l) => /^\-\s*state:\s*/.test(l))?.trim();
55
+ const relPath = relative(sessionRoot, filePath).split(/[\\/]/).join('/');
56
+ const parts = [];
57
+ if (firstHeading)
58
+ parts.push(firstHeading);
59
+ if (stateLine)
60
+ parts.push(stateLine.replace(/^-\s*state:\s*/, ''));
61
+ if (parts.length === 0)
62
+ return `- \`${relPath}\``;
63
+ return `- \`${relPath}\` — ${parts.join(' | ')}`;
64
+ }
65
+ catch {
66
+ return null;
67
+ }
68
+ }
69
+ function extractOneLineSummary(sessionRoot) {
70
+ const artifacts = listMdFiles(sessionRoot, 4);
71
+ for (const artifact of artifacts.slice(0, 5)) {
72
+ try {
73
+ const content = readFileSync(artifact, 'utf8');
74
+ // Grab the first non-heading, non-empty line after the front section
75
+ const lines = content.split(/\r?\n/);
76
+ for (const line of lines) {
77
+ const trimmed = line.trim();
78
+ if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('-') || trimmed.startsWith('`'))
79
+ continue;
80
+ if (trimmed.length > 10 && trimmed.length < 200)
81
+ return trimmed;
82
+ break;
83
+ }
84
+ }
85
+ catch {
86
+ // skip unreadable
87
+ }
88
+ }
89
+ return null;
90
+ }
91
+ function renderSessionBlock(meta, projectRoot) {
92
+ const title = meta.title ?? 'Untitled';
93
+ const date = meta.createdAt ? meta.createdAt.slice(0, 10) : '?';
94
+ const skill = meta.skill ?? '-';
95
+ const mode = meta.mode ?? '-';
96
+ let block = `### ${date} — ${title}\n`;
97
+ block += `- ${skill} (${mode})`;
98
+ const sessionRoot = join(projectRoot, '.peaks', meta.sessionId);
99
+ const summary = extractOneLineSummary(sessionRoot);
100
+ if (summary) {
101
+ block += ` — ${summary.slice(0, 120)}`;
102
+ }
103
+ block += '\n';
104
+ // Key artifact paths only
105
+ const artifacts = listMdFiles(sessionRoot, 3);
106
+ if (artifacts.length > 0) {
107
+ const paths = artifacts.slice(0, 8).map((f) => relative(sessionRoot, f).split(/[\\/]/).join('/'));
108
+ block += ` ${paths.join(' ')}\n`;
109
+ }
110
+ return block;
111
+ }
112
+ function buildSessionHistory(projectRoot) {
113
+ const metas = listSessionMetas(projectRoot);
114
+ if (metas.length === 0) {
115
+ return `${MANAGED_BLOCK_START}\n\n_No sessions recorded yet._\n\n${MANAGED_BLOCK_END}`;
116
+ }
117
+ const maxSessions = 15;
118
+ // Sort by createdAt descending (most recent first)
119
+ const sorted = [...metas].sort((a, b) => (b.createdAt || '').localeCompare(a.createdAt || ''));
120
+ const recent = sorted.slice(0, maxSessions);
121
+ let body = `${MANAGED_BLOCK_START}\n\n## Timeline (${metas.length} sessions`;
122
+ if (metas.length > maxSessions)
123
+ body += `, showing last ${maxSessions}`;
124
+ body += ')\n\n';
125
+ // Human-readable timeline: date | name | brief
126
+ body += `| Date | Directory | Title | What |\n`;
127
+ body += `|------|-----------|-------|------|\n`;
128
+ for (const meta of recent) {
129
+ const date = meta.createdAt ? meta.createdAt.slice(0, 10) : '?';
130
+ const dir = meta.sessionId;
131
+ const title = (meta.title ?? 'Untitled').slice(0, 40);
132
+ const skill = meta.skill ?? '-';
133
+ // Extract one-line summary from artifacts for the "What" column
134
+ const sessionRoot = join(projectRoot, '.peaks', meta.sessionId);
135
+ const summary = extractOneLineSummary(sessionRoot);
136
+ const brief = summary ? summary.slice(0, 70) : skill;
137
+ body += `| ${date} | \`${dir}\` | ${title} | ${brief} |\n`;
138
+ }
139
+ body += `\n${MANAGED_BLOCK_END}`;
140
+ return body;
141
+ }
142
+ // --- Ontology engine ---
143
+ function emptyOntology(projectName) {
144
+ return {
145
+ version: 1,
146
+ updated: new Date().toISOString(),
147
+ project: projectName,
148
+ modules: [],
149
+ decisions: [],
150
+ conventions: []
151
+ };
152
+ }
153
+ function ontoPath(projectRoot) {
154
+ return join(projectRoot, ONTOLOGY_FILE);
155
+ }
156
+ export function loadOntology(projectRoot) {
157
+ const path = ontoPath(projectRoot);
158
+ if (!existsSync(path))
159
+ return null;
160
+ try {
161
+ const raw = JSON.parse(readFileSync(path, 'utf8'));
162
+ if (raw?.version === 1 && Array.isArray(raw.modules)) {
163
+ return raw;
164
+ }
165
+ return null;
166
+ }
167
+ catch {
168
+ return null;
169
+ }
170
+ }
171
+ function slugify(text) {
172
+ return text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'unknown';
173
+ }
174
+ function scanModulesFromArtifacts(sessionRoot, sessionId) {
175
+ const artifacts = listMdFiles(sessionRoot, 4);
176
+ const modules = [];
177
+ const seen = new Set();
178
+ for (const artifact of artifacts.slice(0, 10)) {
179
+ try {
180
+ const content = readFileSync(artifact, 'utf8');
181
+ // Extract file paths — non-capturing groups for extensions
182
+ const patterns = [
183
+ /\b(src\/[^\s)`\]}"]+\.(?:tsx?|jsx?|css|less|scss|vue|svelte))\b/g,
184
+ /\b(packages\/[^\s)`\]}"]+\.(?:tsx?|jsx?))\b/g
185
+ ];
186
+ for (const pattern of patterns) {
187
+ let m;
188
+ while ((m = pattern.exec(content)) !== null) {
189
+ const filePath = m[1] ?? '';
190
+ if (!filePath || filePath.length > 120 || filePath.length < 5)
191
+ continue;
192
+ const id = slugify(filePath.replace(/\.[^.]+$/, '').replace(/[\/\\]/g, '-'));
193
+ if (seen.has(id))
194
+ continue;
195
+ seen.add(id);
196
+ modules.push({ id, path: filePath });
197
+ if (modules.length >= 30)
198
+ break;
199
+ }
200
+ if (modules.length >= 30)
201
+ break;
202
+ }
203
+ }
204
+ catch {
205
+ // skip unreadable
206
+ }
207
+ }
208
+ return modules;
209
+ }
210
+ function scanDecisionsFromArtifacts(sessionRoot, session) {
211
+ const artifacts = listMdFiles(sessionRoot, 4);
212
+ const decisions = [];
213
+ const date = session.createdAt ? session.createdAt.slice(0, 10) : new Date().toISOString().slice(0, 10);
214
+ for (const artifact of artifacts.slice(0, 10)) {
215
+ try {
216
+ const content = readFileSync(artifact, 'utf8');
217
+ // Look for decision markers: "- Decision: ..." or "Decision: ..." or "ADR: ..."
218
+ const decRegex = /^[\s-]*(?:decision|adr|决定|决策)\s*:\s*(.+?)$/gim;
219
+ let m;
220
+ while ((m = decRegex.exec(content)) !== null) {
221
+ const what = (m[1] ?? '').trim().slice(0, 200);
222
+ if (what.length < 5)
223
+ continue;
224
+ const id = slugify(what.slice(0, 40));
225
+ // Collect scope from surrounding context (modules mentioned within 3 lines before/after)
226
+ const scope = [];
227
+ const lineIdx = content.slice(0, m.index).split('\n').length;
228
+ const lines = content.split('\n');
229
+ for (let i = Math.max(0, lineIdx - 3); i < Math.min(lines.length, lineIdx + 3); i++) {
230
+ const line = lines[i] ?? '';
231
+ const pathMatch = /src\/[^\s)`\]}"]+\.(tsx?|jsx?)/.exec(line);
232
+ if (pathMatch?.[0])
233
+ scope.push(pathMatch[0]);
234
+ }
235
+ decisions.push({ id, what, scope: [...new Set(scope)].slice(0, 5), session: session.sessionId, date });
236
+ if (decisions.length >= 10)
237
+ break;
238
+ }
239
+ }
240
+ catch {
241
+ // skip unreadable
242
+ }
243
+ }
244
+ return decisions;
245
+ }
246
+ function scanConventionsFromArtifacts(sessionRoot, session) {
247
+ const artifacts = listMdFiles(sessionRoot, 4);
248
+ const conventions = [];
249
+ const date = session.createdAt ? session.createdAt.slice(0, 10) : new Date().toISOString().slice(0, 10);
250
+ for (const artifact of artifacts.slice(0, 10)) {
251
+ try {
252
+ const content = readFileSync(artifact, 'utf8');
253
+ const convRegex = /^[\s-]*(?:convention|约定|规范)\s*:\s*(.+?)$/gim;
254
+ let m;
255
+ while ((m = convRegex.exec(content)) !== null) {
256
+ const rule = (m[1] ?? '').trim().slice(0, 200);
257
+ if (rule.length < 5)
258
+ continue;
259
+ const id = slugify(rule.slice(0, 40));
260
+ // Infer category from keywords
261
+ let category = 'other';
262
+ if (/class|function|interface|type|hook|component/i.test(rule))
263
+ category = 'code-style';
264
+ else if (/service|layer|package|module|shared|extract/i.test(rule))
265
+ category = 'architecture';
266
+ else if (/naming|命名|文件名|prefix|suffix/i.test(rule))
267
+ category = 'naming';
268
+ else if (/tooling|lint|format|build|test/i.test(rule))
269
+ category = 'tooling';
270
+ conventions.push({ id, rule, category, source: session.sessionId, date });
271
+ if (conventions.length >= 10)
272
+ break;
273
+ }
274
+ }
275
+ catch {
276
+ // skip unreadable
277
+ }
278
+ }
279
+ return conventions;
280
+ }
281
+ function buildOntology(projectRoot) {
282
+ const name = projectName(projectRoot);
283
+ const existing = loadOntology(projectRoot);
284
+ const onto = existing ?? emptyOntology(name);
285
+ onto.updated = new Date().toISOString();
286
+ onto.project = name;
287
+ const metas = listSessionMetas(projectRoot);
288
+ const knownSessions = new Set(metas.map((m) => m.sessionId));
289
+ // Prune: remove modules/decisions/conventions from sessions that no longer exist
290
+ onto.modules = onto.modules.filter((m) => m.sessions.some((s) => knownSessions.has(s)));
291
+ onto.decisions = onto.decisions.filter((d) => knownSessions.has(d.session));
292
+ onto.conventions = onto.conventions.filter((c) => knownSessions.has(c.source));
293
+ // Merge: scan each session for new modules and decisions
294
+ const moduleMap = new Map();
295
+ for (const m of onto.modules)
296
+ moduleMap.set(m.id, m);
297
+ const decisionMap = new Map();
298
+ for (const d of onto.decisions)
299
+ decisionMap.set(d.id, d);
300
+ for (const meta of metas) {
301
+ const sessionRoot = join(projectRoot, '.peaks', meta.sessionId);
302
+ // Modules
303
+ const foundModules = scanModulesFromArtifacts(sessionRoot, meta.sessionId);
304
+ for (const fm of foundModules) {
305
+ if (moduleMap.has(fm.id)) {
306
+ const existing = moduleMap.get(fm.id);
307
+ if (!existing.sessions.includes(meta.sessionId)) {
308
+ existing.sessions.push(meta.sessionId);
309
+ }
310
+ }
311
+ else {
312
+ moduleMap.set(fm.id, {
313
+ id: fm.id,
314
+ path: fm.path,
315
+ sessions: [meta.sessionId]
316
+ });
317
+ }
318
+ }
319
+ // Decisions
320
+ const foundDecisions = scanDecisionsFromArtifacts(sessionRoot, meta);
321
+ for (const fd of foundDecisions) {
322
+ if (!decisionMap.has(fd.id)) {
323
+ decisionMap.set(fd.id, fd);
324
+ }
325
+ }
326
+ // Conventions
327
+ const foundConventions = scanConventionsFromArtifacts(sessionRoot, meta);
328
+ const convMap = new Map();
329
+ for (const c of onto.conventions)
330
+ convMap.set(c.id, c);
331
+ for (const fc of foundConventions) {
332
+ if (!convMap.has(fc.id)) {
333
+ convMap.set(fc.id, fc);
334
+ }
335
+ }
336
+ onto.conventions = [...convMap.values()].sort((a, b) => a.date.localeCompare(b.date));
337
+ }
338
+ // Dedup: remove shorter paths that are substring-matches of longer paths
339
+ const modules = [...moduleMap.values()];
340
+ const deduped = modules.filter((m) => {
341
+ return !modules.some((other) => other !== m && other.path.length > m.path.length && other.path.endsWith(m.path));
342
+ });
343
+ onto.modules = deduped.sort((a, b) => b.sessions.length - a.sessions.length);
344
+ onto.decisions = [...decisionMap.values()].sort((a, b) => b.date.localeCompare(a.date));
345
+ return onto;
346
+ }
347
+ export function saveOntology(projectRoot, onto) {
348
+ const peaksDir = join(projectRoot, '.peaks');
349
+ if (!existsSync(peaksDir))
350
+ mkdirSync(peaksDir, { recursive: true });
351
+ writeFileSync(ontoPath(projectRoot), JSON.stringify(onto, null, 2), 'utf8');
352
+ }
353
+ // Mutations for skills to call when they discover new facts
354
+ export function upsertModule(projectRoot, mod) {
355
+ const onto = buildOntology(projectRoot);
356
+ const existing = onto.modules.find((m) => m.id === mod.id);
357
+ if (existing) {
358
+ if (!existing.sessions.includes(mod.session))
359
+ existing.sessions.push(mod.session);
360
+ if (mod.risk)
361
+ existing.risk = mod.risk;
362
+ if (mod.summary)
363
+ existing.summary = mod.summary;
364
+ }
365
+ else {
366
+ onto.modules.push({ ...mod, sessions: [mod.session] });
367
+ }
368
+ onto.updated = new Date().toISOString();
369
+ saveOntology(projectRoot, onto);
370
+ return onto;
371
+ }
372
+ export function upsertDecision(projectRoot, dec) {
373
+ const onto = buildOntology(projectRoot);
374
+ const idx = onto.decisions.findIndex((d) => d.id === dec.id);
375
+ if (idx >= 0) {
376
+ onto.decisions[idx] = dec;
377
+ }
378
+ else {
379
+ onto.decisions.push(dec);
380
+ }
381
+ onto.updated = new Date().toISOString();
382
+ saveOntology(projectRoot, onto);
383
+ return onto;
384
+ }
385
+ export function upsertConvention(projectRoot, conv) {
386
+ const onto = buildOntology(projectRoot);
387
+ const idx = onto.conventions.findIndex((c) => c.id === conv.id);
388
+ if (idx >= 0) {
389
+ onto.conventions[idx] = conv;
390
+ }
391
+ else {
392
+ onto.conventions.push(conv);
393
+ }
394
+ onto.updated = new Date().toISOString();
395
+ saveOntology(projectRoot, onto);
396
+ return onto;
397
+ }
398
+ // --- Context generator (unified: PROJECT.md + ontology.json) ---
399
+ export function generateProjectContext(projectRoot) {
400
+ const peaksDir = join(projectRoot, '.peaks');
401
+ if (!existsSync(peaksDir)) {
402
+ mkdirSync(peaksDir, { recursive: true });
403
+ }
404
+ const contextPath = join(projectRoot, PROJECT_CONTEXT_FILE);
405
+ const name = projectName(projectRoot);
406
+ const now = new Date().toISOString();
407
+ const sessionHistory = buildSessionHistory(projectRoot);
408
+ const header = `${CONTEXT_HEADER}${now}\n\n## Project: ${name}\n`;
409
+ let content;
410
+ if (existsSync(contextPath)) {
411
+ const existing = readFileSync(contextPath, 'utf8');
412
+ const startIdx = existing.indexOf(MANAGED_BLOCK_START);
413
+ const endIdx = existing.indexOf(MANAGED_BLOCK_END);
414
+ // Update the Last-updated timestamp in the header
415
+ const updatedExisting = existing.replace(/Last updated: .*/, `Last updated: ${now}`);
416
+ if (startIdx >= 0 && endIdx > startIdx) {
417
+ // Replace managed block, preserve user content outside it
418
+ const before = updatedExisting.slice(0, startIdx);
419
+ const after = updatedExisting.slice(endIdx + MANAGED_BLOCK_END.length);
420
+ content = before + sessionHistory + after;
421
+ }
422
+ else {
423
+ // No managed block found — append
424
+ content = updatedExisting.trimEnd() + '\n\n' + sessionHistory + '\n';
425
+ }
426
+ }
427
+ else {
428
+ content = header + '\n' + sessionHistory + '\n';
429
+ }
430
+ writeFileSync(contextPath, content, 'utf8');
431
+ // Build and save ontology alongside PROJECT.md
432
+ const ontology = buildOntology(projectRoot);
433
+ saveOntology(projectRoot, ontology);
434
+ return { path: contextPath, content, sessionCount: listSessionMetas(projectRoot).length, ontology };
435
+ }
436
+ export function readProjectContext(projectRoot) {
437
+ const contextPath = join(projectRoot, PROJECT_CONTEXT_FILE);
438
+ if (!existsSync(contextPath))
439
+ return null;
440
+ try {
441
+ return readFileSync(contextPath, 'utf8');
442
+ }
443
+ catch {
444
+ return null;
445
+ }
446
+ }
447
+ export function getProjectContextPath(projectRoot) {
448
+ return join(projectRoot, PROJECT_CONTEXT_FILE);
449
+ }
@@ -9,8 +9,8 @@ export type SkillPresence = {
9
9
  setAt: string;
10
10
  lastHeartbeat?: string;
11
11
  };
12
- export declare function exportSkillPresence(): string;
13
- export declare function setSkillPresence(skill: string, mode?: string, gate?: string): SkillPresence;
14
- export declare function getSkillPresence(): SkillPresence | null;
15
- export declare function touchSkillHeartbeat(): SkillPresence | null;
16
- export declare function clearSkillPresence(): boolean;
12
+ export declare function exportSkillPresence(projectRootOverride?: string): string;
13
+ export declare function setSkillPresence(skill: string, mode?: string, gate?: string, projectRootOverride?: string): SkillPresence;
14
+ export declare function getSkillPresence(projectRootOverride?: string): SkillPresence | null;
15
+ export declare function touchSkillHeartbeat(projectRootOverride?: string): SkillPresence | null;
16
+ export declare function clearSkillPresence(projectRootOverride?: string): boolean;
@@ -12,14 +12,16 @@ export function isSkillPresenceMode(value) {
12
12
  }
13
13
  const PRESENCE_FILE = '.peaks/.active-skill.json';
14
14
  const SESSION_FILE = '.peaks/.session.json';
15
- function resolveProjectRoot() {
15
+ function resolveProjectRoot(override) {
16
+ if (override)
17
+ return resolve(override);
16
18
  return findProjectRoot(process.cwd()) ?? process.cwd();
17
19
  }
18
- function resolvePresencePath() {
19
- return resolve(resolveProjectRoot(), PRESENCE_FILE);
20
+ function resolvePresencePath(projectRootOverride) {
21
+ return resolve(resolveProjectRoot(projectRootOverride), PRESENCE_FILE);
20
22
  }
21
- function getCurrentSessionId() {
22
- const sessionPath = resolve(resolveProjectRoot(), SESSION_FILE);
23
+ function getCurrentSessionId(projectRootOverride) {
24
+ const sessionPath = resolve(resolveProjectRoot(projectRootOverride), SESSION_FILE);
23
25
  if (!existsSync(sessionPath))
24
26
  return null;
25
27
  try {
@@ -32,12 +34,12 @@ function getCurrentSessionId() {
32
34
  return null;
33
35
  }
34
36
  }
35
- export function exportSkillPresence() {
36
- return resolvePresencePath();
37
+ export function exportSkillPresence(projectRootOverride) {
38
+ return resolvePresencePath(projectRootOverride);
37
39
  }
38
- export function setSkillPresence(skill, mode, gate) {
40
+ export function setSkillPresence(skill, mode, gate, projectRootOverride) {
39
41
  const validatedMode = mode && isSkillPresenceMode(mode) ? mode : undefined;
40
- const sessionId = getCurrentSessionId();
42
+ const sessionId = getCurrentSessionId(projectRootOverride);
41
43
  const now = new Date().toISOString();
42
44
  const presence = {
43
45
  skill,
@@ -47,7 +49,7 @@ export function setSkillPresence(skill, mode, gate) {
47
49
  setAt: now,
48
50
  lastHeartbeat: now
49
51
  };
50
- const presencePath = resolvePresencePath();
52
+ const presencePath = resolvePresencePath(projectRootOverride);
51
53
  const presenceDir = dirname(presencePath);
52
54
  if (!existsSync(presenceDir)) {
53
55
  mkdirSync(presenceDir, { recursive: true });
@@ -55,8 +57,8 @@ export function setSkillPresence(skill, mode, gate) {
55
57
  writeFileSync(presencePath, JSON.stringify(presence, null, 2), 'utf8');
56
58
  return presence;
57
59
  }
58
- export function getSkillPresence() {
59
- const presencePath = resolvePresencePath();
60
+ export function getSkillPresence(projectRootOverride) {
61
+ const presencePath = resolvePresencePath(projectRootOverride);
60
62
  if (!existsSync(presencePath)) {
61
63
  return null;
62
64
  }
@@ -67,7 +69,7 @@ export function getSkillPresence() {
67
69
  return null;
68
70
  }
69
71
  if (typeof parsed.sessionId === 'string' && parsed.sessionId.length > 0) {
70
- const currentSessionId = getCurrentSessionId();
72
+ const currentSessionId = getCurrentSessionId(projectRootOverride);
71
73
  if (currentSessionId && parsed.sessionId !== currentSessionId) {
72
74
  unlinkSync(presencePath);
73
75
  return null;
@@ -79,8 +81,8 @@ export function getSkillPresence() {
79
81
  return null;
80
82
  }
81
83
  }
82
- export function touchSkillHeartbeat() {
83
- const presencePath = resolvePresencePath();
84
+ export function touchSkillHeartbeat(projectRootOverride) {
85
+ const presencePath = resolvePresencePath(projectRootOverride);
84
86
  if (!existsSync(presencePath)) {
85
87
  return null;
86
88
  }
@@ -91,7 +93,7 @@ export function touchSkillHeartbeat() {
91
93
  return null;
92
94
  }
93
95
  if (typeof parsed.sessionId === 'string' && parsed.sessionId.length > 0) {
94
- const currentSessionId = getCurrentSessionId();
96
+ const currentSessionId = getCurrentSessionId(projectRootOverride);
95
97
  if (currentSessionId && parsed.sessionId !== currentSessionId) {
96
98
  unlinkSync(presencePath);
97
99
  return null;
@@ -105,8 +107,8 @@ export function touchSkillHeartbeat() {
105
107
  return null;
106
108
  }
107
109
  }
108
- export function clearSkillPresence() {
109
- const presencePath = resolvePresencePath();
110
+ export function clearSkillPresence(projectRootOverride) {
111
+ const presencePath = resolvePresencePath(projectRootOverride);
110
112
  if (!existsSync(presencePath)) {
111
113
  return false;
112
114
  }
@@ -1 +1 @@
1
- export declare const CLI_VERSION = "1.0.27";
1
+ export declare const CLI_VERSION = "1.0.28";
@@ -1 +1 @@
1
- export const CLI_VERSION = "1.0.27";
1
+ export const CLI_VERSION = "1.0.28";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "peaks-cli",
3
- "version": "1.0.27",
3
+ "version": "1.0.28",
4
4
  "description": "Peaks CLI and short skill family for Claude Code automation.",
5
5
  "author": "SquabbyZ",
6
6
  "license": "MIT",
@@ -12,10 +12,16 @@ Peaks-Cli PRD turns user intent into verifiable product artifacts.
12
12
  Before any analysis or tool call, immediately run:
13
13
 
14
14
  ```bash
15
- peaks skill presence:set peaks-prd --mode <mode> --gate startup
15
+ peaks skill presence:set peaks-prd --project <repo> --mode <mode> --gate startup
16
16
  ```
17
+ Read persistent project memory via CLI (structured ontology for LLM):
17
18
 
18
- Then display: `Peaks-Cli Skill: peaks-prd | Peaks-Cli Gate: startup | Next: <one short action>`. Update with `peaks skill presence:set peaks-prd --mode <mode> --gate <gate>` when gates change. When the role's work ends, run `peaks skill presence:clear`.
19
+ ```bash
20
+ peaks project ontology show --project <repo> --json
21
+ ```
22
+
23
+ This returns `.peaks/ontology.json` — structured modules, decisions, and conventions from past sessions. (`.peaks/PROJECT.md` is a human-readable timeline only.)
24
+ Then display: `Peaks-Cli Skill: peaks-prd | Peaks-Cli Gate: startup | Next: <one short action>`. Update with `peaks skill presence:set peaks-prd --project <repo> --mode <mode> --gate <gate>` when gates change. When the role's work ends, run `peaks skill presence:clear --project <repo>`.
19
25
 
20
26
  ## Responsibilities
21
27
 
@@ -54,7 +60,7 @@ For a feature / bug / clarification request with no authenticated source documen
54
60
  ```bash
55
61
  # 0. confirm PRD's own runbook integrity before driving any phase
56
62
  peaks skill runbook peaks-prd --json
57
- peaks skill presence:set peaks-prd # show persistent skill presence every turn
63
+ peaks skill presence:set peaks-prd --project <repo> # show persistent skill presence every turn
58
64
 
59
65
  # 1. capture the request as the canonical PRD artifact (preview, then apply)
60
66
  peaks request init --role prd --id <request-id> --project <repo> --json
@@ -74,7 +80,7 @@ peaks codegraph status --project <repo> # local index status
74
80
 
75
81
  # 5. write goals / non-goals / acceptance into the artifact body, then hand off
76
82
  peaks request show <request-id> --role prd --project <repo> --json
77
- peaks skill presence:clear # handoff complete, remove presence indicator
83
+ peaks skill presence:clear --project <repo> # handoff complete, remove presence indicator
78
84
  ```
79
85
 
80
86
  For an authenticated product document request (Feishu/Lark/wiki), add before step 5:
@@ -12,10 +12,16 @@ Peaks-Cli QA proves that planned changes are protected and accepted.
12
12
  Before any analysis or tool call, immediately run:
13
13
 
14
14
  ```bash
15
- peaks skill presence:set peaks-qa --mode <mode> --gate startup
15
+ peaks skill presence:set peaks-qa --project <repo> --mode <mode> --gate startup
16
16
  ```
17
+ Read persistent project memory via CLI (structured ontology for LLM):
17
18
 
18
- Then display: `Peaks-Cli Skill: peaks-qa | Peaks-Cli Gate: startup | Next: <one short action>`. Update with `peaks skill presence:set peaks-qa --mode <mode> --gate <gate>` when gates change. When the role's work ends, run `peaks skill presence:clear`.
19
+ ```bash
20
+ peaks project ontology show --project <repo> --json
21
+ ```
22
+
23
+ This returns `.peaks/ontology.json` — structured modules, decisions, and conventions from past sessions. (`.peaks/PROJECT.md` is a human-readable timeline only.)
24
+ Then display: `Peaks-Cli Skill: peaks-qa | Peaks-Cli Gate: startup | Next: <one short action>`. Update with `peaks skill presence:set peaks-qa --project <repo> --mode <mode> --gate <gate>` when gates change. When the role's work ends, run `peaks skill presence:clear --project <repo>`.
19
25
 
20
26
  ## Responsibilities
21
27
 
@@ -47,7 +53,7 @@ The default sequence the QA skill should execute. Do not skip the boundary check
47
53
  ```bash
48
54
  # 0. confirm QA's own runbook integrity before validating anything
49
55
  peaks skill runbook peaks-qa --json
50
- peaks skill presence:set peaks-qa # show persistent skill presence every turn
56
+ peaks skill presence:set peaks-qa --project <repo> # show persistent skill presence every turn
51
57
 
52
58
  # 1. capture the QA request artifact and read upstream scope
53
59
  peaks request init --role qa --id <request-id> --project <repo> --apply --json
@@ -134,7 +140,7 @@ peaks request lint <rid> --role qa --project <repo> --json
134
140
  # 9. on verdict=return-to-rd, route findings back through the request id; otherwise close.
135
141
  peaks request show <request-id> --role qa --project <repo> --json
136
142
  peaks openspec archive <change-id> --project <repo> --json # preview, then --apply on full pass
137
- peaks skill presence:clear # QA complete, remove presence indicator
143
+ peaks skill presence:clear --project <repo> # QA complete, remove presence indicator
138
144
  ```
139
145
 
140
146
  Verdict `pass` is blocked until every applicable validation gate has evidence in the artifact.
@@ -12,10 +12,16 @@ Peaks-Cli RD owns engineering analysis, implementation planning, and refactor ex
12
12
  Before any analysis or tool call, immediately run:
13
13
 
14
14
  ```bash
15
- peaks skill presence:set peaks-rd --mode <mode> --gate startup
15
+ peaks skill presence:set peaks-rd --project <repo> --mode <mode> --gate startup
16
16
  ```
17
+ Read persistent project memory via CLI (structured ontology for LLM):
17
18
 
18
- Then display: `Peaks-Cli Skill: peaks-rd | Peaks-Cli Gate: startup | Next: <one short action>`. Update with `peaks skill presence:set peaks-rd --mode <mode> --gate <gate>` when gates change. When the role's work ends, run `peaks skill presence:clear`.
19
+ ```bash
20
+ peaks project ontology show --project <repo> --json
21
+ ```
22
+
23
+ This returns `.peaks/ontology.json` — structured modules, decisions, and conventions from past sessions. (`.peaks/PROJECT.md` is a human-readable timeline only.)
24
+ Then display: `Peaks-Cli Skill: peaks-rd | Peaks-Cli Gate: startup | Next: <one short action>`. Update with `peaks skill presence:set peaks-rd --project <repo> --mode <mode> --gate <gate>` when gates change. When the role's work ends, run `peaks skill presence:clear --project <repo>`.
19
25
 
20
26
  ## Responsibilities
21
27
 
@@ -41,7 +47,7 @@ The default sequence the RD skill should execute for a code-touching request. Sk
41
47
  ```bash
42
48
  # 0. confirm RD's own runbook integrity before any code edit
43
49
  peaks skill runbook peaks-rd --json
44
- peaks skill presence:set peaks-rd # show persistent skill presence every turn
50
+ peaks skill presence:set peaks-rd --project <repo> # show persistent skill presence every turn
45
51
 
46
52
  # 1. capture the RD request artifact and read upstream PRD / UI scope
47
53
  peaks request init --role rd --id <request-id> --project <repo> --apply --json
@@ -138,7 +144,7 @@ peaks openspec validate <change-id> --project <repo> --json # exit gate (re-r
138
144
  # 8. hand off to QA via the cross-linked request id
139
145
  peaks request init --role qa --id <request-id> --project <repo> --apply --json
140
146
  peaks request show <request-id> --role rd --project <repo> --json
141
- peaks skill presence:clear # handoff complete, remove presence indicator
147
+ peaks skill presence:clear --project <repo> # handoff complete, remove presence indicator
142
148
  ```
143
149
 
144
150
  For refactor work, the coverage ≥ 95% gate in `Refactor hard gates` still applies and must be recorded in the artifact before slicing begins.
@@ -12,10 +12,16 @@ Peaks-Cli SC records how product, RD, QA, code, and artifacts move together.
12
12
  Before any analysis or tool call, immediately run:
13
13
 
14
14
  ```bash
15
- peaks skill presence:set peaks-sc --mode <mode> --gate startup
15
+ peaks skill presence:set peaks-sc --project <repo> --mode <mode> --gate startup
16
16
  ```
17
+ Read persistent project memory via CLI (structured ontology for LLM):
17
18
 
18
- Then display: `Peaks-Cli Skill: peaks-sc | Peaks-Cli Gate: startup | Next: <one short action>`. Update with `peaks skill presence:set peaks-sc --mode <mode> --gate <gate>` when gates change. When the role's work ends, run `peaks skill presence:clear`.
19
+ ```bash
20
+ peaks project ontology show --project <repo> --json
21
+ ```
22
+
23
+ This returns `.peaks/ontology.json` — structured modules, decisions, and conventions from past sessions. (`.peaks/PROJECT.md` is a human-readable timeline only.)
24
+ Then display: `Peaks-Cli Skill: peaks-sc | Peaks-Cli Gate: startup | Next: <one short action>`. Update with `peaks skill presence:set peaks-sc --project <repo> --mode <mode> --gate <gate>` when gates change. When the role's work ends, run `peaks skill presence:clear --project <repo>`.
19
25
 
20
26
  ## Responsibilities
21
27
 
@@ -77,7 +83,7 @@ Use this sequence when SC owns the change-control pass for a refactor or release
77
83
  # in: none
78
84
  # out: runbook version, presence set
79
85
  peaks skill runbook peaks-sc --json
80
- peaks skill presence:set peaks-sc # show persistent skill presence every turn
86
+ peaks skill presence:set peaks-sc --project <repo> # show persistent skill presence every turn
81
87
 
82
88
  # 1. Derive commit boundaries (OpenSpec preferred, git diff fallback)
83
89
  # in: change-id, repo path
@@ -115,7 +121,7 @@ peaks sc boundary --slice-id <slice-id> --artifact <artifact-path> --code <code-
115
121
  # out: sync result or dry-run preview
116
122
  peaks memory sync --project <repo> --workspace <workspace> --apply --json
117
123
  peaks artifacts sync --workspace <workspace> --apply --json
118
- peaks skill presence:clear # SC complete, remove presence indicator
124
+ peaks skill presence:clear --project <repo> # SC complete, remove presence indicator
119
125
  ```
120
126
 
121
127
  The final two `--apply` calls require explicit authorization. Without it, default to `--dry-run` or omit the sync calls entirely and keep the boundary evidence local under `.peaks/<session-id>/`.
@@ -71,12 +71,29 @@ If the user already names a profile in their invocation (e.g. `/peaks-solo --ful
71
71
  Only after the mode is known (user selected or explicitly named), run:
72
72
 
73
73
  ```bash
74
- peaks skill presence:set peaks-solo --mode <mode-value> --gate startup
74
+ peaks skill presence:set peaks-solo --project <repo> --mode <mode-value> --gate startup
75
75
  ```
76
76
 
77
77
  Then display the compact status header: `Peaks-Cli Skill: peaks-solo | Peaks-Cli Gate: startup | Next: <one short action>`. Display this header on EVERY turn while the skill is active.
78
78
 
79
- Update with `peaks skill presence:set peaks-solo --mode <mode> --gate <gate>` when gates change. The presence file persists across the full workflow lifecycle — do NOT clear it at workflow end.
79
+ Update with `peaks skill presence:set peaks-solo --project <repo> --mode <mode> --gate <gate>` when gates change. The presence file persists across the full workflow lifecycle — do NOT clear it at workflow end.
80
+
81
+ ### Peaks-Cli Step 2.3: Load project memory (structured ontology for LLM)
82
+
83
+ Before planning any work, read the project's persistent memory — structured data that survives across sessions:
84
+
85
+ ```bash
86
+ peaks project ontology show --project <repo> --json
87
+ ```
88
+
89
+ This returns `.peaks/ontology.json` containing:
90
+ - **modules** — code areas touched, with risk levels and which sessions modified them
91
+ - **decisions** — architectural choices, why they were made, what modules they affect
92
+ - **conventions** — discovered project patterns (code style, naming, tooling)
93
+
94
+ Use this to understand what exists, what was decided, and what to avoid re-litigating. The ontology is auto-updated on `peaks skill presence:clear`.
95
+
96
+ `.peaks/PROJECT.md` is a human-readable timeline only — do NOT use it for LLM context.
80
97
 
81
98
  ### Peaks-Cli Step 2.5: Set session title
82
99
 
@@ -579,7 +596,7 @@ Solo is itself a skill running in the current session. To "invoke peaks-rd" or "
579
596
  **Presence restoration after role skill returns (MANDATORY):** Role skills (peaks-rd, peaks-qa, peaks-ui) call `peaks skill presence:set <role>` internally, which overwrites `.peaks/.active-skill.json`. After EVERY role skill returns — whether success, repair-needed, or failure — Solo MUST immediately restore the orchestrator presence by re-running the same presence command from Step 2:
580
597
 
581
598
  ```bash
582
- peaks skill presence:set peaks-solo --mode <mode> --gate <current-gate>
599
+ peaks skill presence:set peaks-solo --project <repo> --mode <mode> --gate <current-gate>
583
600
  ```
584
601
 
585
602
  This keeps the CLAUDE.md status header accurate (`Peaks-Cli Skill: peaks-solo`) instead of showing a stale role name. Use the current mode and gate values; the gate may have advanced since startup. Skipping this step causes the header to display the last role skill name permanently.
@@ -606,7 +623,7 @@ When `peaks-qa` returns `verdict=return-to-rd`, Solo does NOT manually rewrite R
606
623
  6. Repeat steps 1-5 until QA returns `verdict=pass`, or the cap below fires.
607
624
  **After each repair iteration** (after peaks-rd and peaks-qa both return), Solo MUST restore presence:
608
625
  ```bash
609
- peaks skill presence:set peaks-solo --mode <mode> --gate repair-cycle-<N>
626
+ peaks skill presence:set peaks-solo --project <repo> --mode <mode> --gate repair-cycle-<N>
610
627
  ```
611
628
 
612
629
  **Repair cycle cap**: After 3 repair cycles without a passing QA verdict, emit a blocked TXT handoff regardless of remaining issues. Do not loop indefinitely. If a specific issue cannot be resolved within 3 cycles, mark it as a known blocker in the TXT handoff and proceed to the SC phase.
@@ -781,7 +798,7 @@ Use Peaks-Cli TXT for the compact handoff capsule: mode, validated decisions, ar
781
798
 
782
799
  ### Workflow completion (no auto-exit)
783
800
 
784
- Do NOT call `peaks skill presence:clear` at workflow end. The presence file and header remain active so the user stays inside the workflow context. The user can continue with follow-up requirements naturally — no need to re-invoke `/peaks-solo`. The header continues to display the active skill and current gate.
801
+ Do NOT call `peaks skill presence:clear --project <repo>` at workflow end. The presence file and header remain active so the user stays inside the workflow context. The user can continue with follow-up requirements naturally — no need to re-invoke `/peaks-solo`. The header continues to display the active skill and current gate.
785
802
 
786
803
  ## Peaks-Cli External references and lifecycle
787
804
 
@@ -12,10 +12,16 @@ Peaks-Cli TXT compresses workflow context into portable, role-specific artifacts
12
12
  Before any analysis or tool call, immediately run:
13
13
 
14
14
  ```bash
15
- peaks skill presence:set peaks-txt --mode <mode> --gate startup
15
+ peaks skill presence:set peaks-txt --project <repo> --mode <mode> --gate startup
16
16
  ```
17
+ Read persistent project memory via CLI (structured ontology for LLM):
17
18
 
18
- Then display: `Peaks-Cli Skill: peaks-txt | Peaks-Cli Gate: startup | Next: <one short action>`. Update with `peaks skill presence:set peaks-txt --mode <mode> --gate <gate>` when gates change. When the role's work ends, run `peaks skill presence:clear`.
19
+ ```bash
20
+ peaks project ontology show --project <repo> --json
21
+ ```
22
+
23
+ This returns `.peaks/ontology.json` — structured modules, decisions, and conventions from past sessions. (`.peaks/PROJECT.md` is a human-readable timeline only.)
24
+ Then display: `Peaks-Cli Skill: peaks-txt | Peaks-Cli Gate: startup | Next: <one short action>`. Update with `peaks skill presence:set peaks-txt --project <repo> --mode <mode> --gate <gate>` when gates change. When the role's work ends, run `peaks skill presence:clear --project <repo>`.
19
25
 
20
26
  ## Responsibilities
21
27
 
@@ -161,7 +167,7 @@ Use this sequence when TXT compresses an in-flight workflow into a portable, com
161
167
  ```bash
162
168
  # 0. Confirm TXT's own runbook integrity before compressing a handoff
163
169
  peaks skill runbook peaks-txt --json
164
- peaks skill presence:set peaks-txt # show persistent skill presence every turn
170
+ peaks skill presence:set peaks-txt --project <repo> # show persistent skill presence every turn
165
171
 
166
172
  # 1. Inventory per-role artifacts already produced for the request
167
173
  peaks request list --project <repo> --json
@@ -180,7 +186,7 @@ peaks capabilities --json
180
186
  # 5. Memory extraction — dry-run by default, apply only when authorized
181
187
  peaks memory extract --project <repo> --artifact <artifact-path> --dry-run --json
182
188
  peaks memory extract --project <repo> --artifact <artifact-path> --apply --json
183
- peaks skill presence:clear # handoff capsule complete, remove presence indicator
189
+ peaks skill presence:clear --project <repo> # handoff capsule complete, remove presence indicator
184
190
  ```
185
191
 
186
192
  The final `--apply` call requires explicit user or profile authorization. Without it, keep the capsule under `.peaks/<session-id>/txt/` and reference artifact paths from other roles instead of duplicating their content.
@@ -12,10 +12,16 @@ Peaks-Cli UI handles experience, interaction, visual direction, and UI-specific
12
12
  Before any analysis or tool call, immediately run:
13
13
 
14
14
  ```bash
15
- peaks skill presence:set peaks-ui --mode <mode> --gate startup
15
+ peaks skill presence:set peaks-ui --project <repo> --mode <mode> --gate startup
16
16
  ```
17
+ Read persistent project memory via CLI (structured ontology for LLM):
17
18
 
18
- Then display: `Peaks-Cli Skill: peaks-ui | Peaks-Cli Gate: startup | Next: <one short action>`. Update with `peaks skill presence:set peaks-ui --mode <mode> --gate <gate>` when gates change. When the role's work ends, run `peaks skill presence:clear`.
19
+ ```bash
20
+ peaks project ontology show --project <repo> --json
21
+ ```
22
+
23
+ This returns `.peaks/ontology.json` — structured modules, decisions, and conventions from past sessions. (`.peaks/PROJECT.md` is a human-readable timeline only.)
24
+ Then display: `Peaks-Cli Skill: peaks-ui | Peaks-Cli Gate: startup | Next: <one short action>`. Update with `peaks skill presence:set peaks-ui --project <repo> --mode <mode> --gate <gate>` when gates change. When the role's work ends, run `peaks skill presence:clear --project <repo>`.
19
25
 
20
26
  ## Responsibilities
21
27
 
@@ -48,7 +54,7 @@ The default sequence the UI skill should execute. Skip steps that do not apply;
48
54
  ```bash
49
55
  # 0. confirm UI's own runbook integrity before driving any phase
50
56
  peaks skill runbook peaks-ui --json
51
- peaks skill presence:set peaks-ui # show persistent skill presence every turn
57
+ peaks skill presence:set peaks-ui --project <repo> # show persistent skill presence every turn
52
58
 
53
59
  # 1. capture the UI request as a durable artifact tied to the same PRD request id
54
60
  peaks request init --role ui --id <request-id> --project <repo> --json
@@ -113,7 +119,7 @@ peaks mcp apply --capability playwright-mcp.browser-validation --yes --json
113
119
  # 7. hand off to RD / QA via the cross-linked request id
114
120
  peaks request list --project <repo> --json
115
121
  peaks request show <request-id> --role ui --project <repo> --json
116
- peaks skill presence:clear # handoff complete, remove presence indicator
122
+ peaks skill presence:clear --project <repo> # handoff complete, remove presence indicator
117
123
  ```
118
124
 
119
125
  Handoff is blocked until the UI artifact's `state` reaches `direction-locked` or `handed-off`.