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.
- package/dist/src/cli/commands/core-artifact-commands.js +17 -6
- package/dist/src/cli/commands/project-commands.js +137 -0
- package/dist/src/services/memory/project-context-service.d.ts +49 -0
- package/dist/src/services/memory/project-context-service.js +449 -0
- package/dist/src/services/skills/skill-presence-service.d.ts +5 -5
- package/dist/src/services/skills/skill-presence-service.js +20 -18
- package/dist/src/shared/version.d.ts +1 -1
- package/dist/src/shared/version.js +1 -1
- package/package.json +1 -1
- package/skills/peaks-prd/SKILL.md +10 -4
- package/skills/peaks-qa/SKILL.md +10 -4
- package/skills/peaks-rd/SKILL.md +10 -4
- package/skills/peaks-sc/SKILL.md +10 -4
- package/skills/peaks-solo/SKILL.md +22 -5
- package/skills/peaks-txt/SKILL.md +10 -4
- package/skills/peaks-ui/SKILL.md +10 -4
|
@@ -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')
|
|
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')
|
|
96
|
-
|
|
97
|
-
|
|
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.
|
|
1
|
+
export declare const CLI_VERSION = "1.0.28";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const CLI_VERSION = "1.0.
|
|
1
|
+
export const CLI_VERSION = "1.0.28";
|
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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:
|
package/skills/peaks-qa/SKILL.md
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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.
|
package/skills/peaks-rd/SKILL.md
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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.
|
package/skills/peaks-sc/SKILL.md
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
package/skills/peaks-ui/SKILL.md
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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`.
|