peaks-cli 1.3.8 → 1.3.9
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/project-commands.js +58 -1
- package/dist/src/cli/commands/request-commands.js +93 -3
- package/dist/src/cli/commands/retrospective-commands.d.ts +3 -0
- package/dist/src/cli/commands/retrospective-commands.js +113 -0
- package/dist/src/cli/program.js +2 -0
- package/dist/src/services/memory/project-memory-service.d.ts +19 -0
- package/dist/src/services/memory/project-memory-service.js +33 -0
- package/dist/src/services/retrospective/migrate-from-md.d.ts +37 -0
- package/dist/src/services/retrospective/migrate-from-md.js +528 -0
- package/dist/src/services/retrospective/retrospective-index.d.ts +37 -0
- package/dist/src/services/retrospective/retrospective-index.js +110 -0
- package/dist/src/services/retrospective/retrospective-show.d.ts +40 -0
- package/dist/src/services/retrospective/retrospective-show.js +109 -0
- package/dist/src/shared/format-md-compact.d.ts +32 -0
- package/dist/src/shared/format-md-compact.js +297 -0
- package/dist/src/shared/stale-policy.d.ts +67 -0
- package/dist/src/shared/stale-policy.js +85 -0
- package/dist/src/shared/version.d.ts +1 -1
- package/dist/src/shared/version.js +1 -1
- package/package.json +1 -1
- package/skills/peaks-qa/SKILL.md +86 -515
- package/skills/peaks-qa/references/artifact-per-request.md +7 -79
- package/skills/peaks-qa/references/browser-validation-contracts.md +51 -0
- package/skills/peaks-qa/references/codegraph-regression-focus.md +5 -0
- package/skills/peaks-qa/references/external-capability-guidance.md +9 -0
- package/skills/peaks-qa/references/qa-compact-handoff.md +3 -0
- package/skills/peaks-qa/references/qa-context-governance.md +24 -0
- package/skills/peaks-qa/references/qa-fanout-contract.md +8 -0
- package/skills/peaks-qa/references/qa-gstack-integration.md +7 -0
- package/skills/peaks-qa/references/qa-local-artifacts.md +3 -0
- package/skills/peaks-qa/references/qa-matt-pocock-integration.md +9 -0
- package/skills/peaks-qa/references/qa-refactor-role.md +3 -0
- package/skills/peaks-qa/references/qa-runbook.md +74 -0
- package/skills/peaks-qa/references/qa-skill-presence.md +22 -0
- package/skills/peaks-qa/references/qa-standards-preflight.md +8 -0
- package/skills/peaks-qa/references/qa-sub-agent-dispatch.md +38 -0
- package/skills/peaks-qa/references/qa-transition-gates.md +79 -0
- package/skills/peaks-qa/references/requirement-boundary-recheck.md +9 -0
- package/skills/peaks-qa/references/test-case-generation.md +27 -0
- package/skills/peaks-qa/references/test-report-output.md +14 -0
- package/skills/peaks-rd/SKILL.md +85 -612
- package/skills/peaks-rd/references/artifact-and-standards-output.md +9 -0
- package/skills/peaks-rd/references/artifact-per-request.md +20 -0
- package/skills/peaks-rd/references/browser-self-test-contracts.md +29 -0
- package/skills/peaks-rd/references/codegraph-project-analysis.md +5 -0
- package/skills/peaks-rd/references/compact-handoff.md +3 -0
- package/skills/peaks-rd/references/external-references.md +11 -0
- package/skills/peaks-rd/references/frontend-project-generation.md +11 -0
- package/skills/peaks-rd/references/library-version-awareness.md +30 -0
- package/skills/peaks-rd/references/mandatory-perf-baseline.md +40 -0
- package/skills/peaks-rd/references/mandatory-tech-doc.md +18 -0
- package/skills/peaks-rd/references/matt-pocock-integration.md +11 -0
- package/skills/peaks-rd/references/mock-data-placement.md +40 -0
- package/skills/peaks-rd/references/parallel-review-fanout.md +81 -0
- package/skills/peaks-rd/references/rd-context-governance.md +36 -0
- package/skills/peaks-rd/references/rd-gstack-integration.md +16 -0
- package/skills/peaks-rd/references/rd-runbook.md +125 -0
- package/skills/peaks-rd/references/rd-standards-preflight.md +8 -0
- package/skills/peaks-rd/references/rd-sub-agent-dispatch.md +39 -0
- package/skills/peaks-rd/references/rd-transition-gates.md +1 -1
- package/skills/peaks-rd/references/skill-presence-and-title.md +22 -0
- package/skills/peaks-solo/SKILL.md +87 -595
- package/skills/peaks-solo/references/anchoring-and-session-info.md +25 -0
- package/skills/peaks-solo/references/boundaries.md +21 -0
- package/skills/peaks-solo/references/codegraph-orchestration.md +5 -0
- package/skills/peaks-solo/references/completion-handoff.md +16 -0
- package/skills/peaks-solo/references/context-governance.md +51 -0
- package/skills/peaks-solo/references/external-references.md +17 -0
- package/skills/peaks-solo/references/frontend-only-mode.md +14 -0
- package/skills/peaks-solo/references/gstack-integration.md +7 -0
- package/skills/peaks-solo/references/local-artifact-workspace.md +79 -0
- package/skills/peaks-solo/references/micro-cycle.md +68 -0
- package/skills/peaks-solo/references/mode-selection.md +21 -0
- package/skills/peaks-solo/references/openspec-workflow.md +43 -0
- package/skills/peaks-solo/references/project-memory-loading.md +17 -0
- package/skills/peaks-solo/references/quality-gate-cheatsheet.md +13 -0
- package/skills/peaks-solo/references/resume-detection.md +63 -0
- package/skills/peaks-solo/references/runbook.md +1 -1
- package/skills/peaks-solo/references/skill-presence-and-title.md +31 -0
- package/skills/peaks-solo/references/standards-preflight.md +23 -0
- package/skills/peaks-solo/references/sub-agent-dispatch.md +46 -0
- package/skills/peaks-solo/references/swarm-dispatch-contract.md +56 -0
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { loadProjectDashboard } from '../../services/dashboard/project-dashboard-service.js';
|
|
2
2
|
import { generateProjectContext, readProjectContext } from '../../services/memory/project-context-service.js';
|
|
3
|
-
import { extractSessionMemories, readMemoryIndex, readProjectMemories } from '../../services/memory/project-memory-service.js';
|
|
3
|
+
import { extractSessionMemories, readMemoryIndex, readProjectMemories, readProjectMemoryBody } from '../../services/memory/project-memory-service.js';
|
|
4
|
+
import { applyStalePolicy, DEFAULT_STALE_DAYS } from '../../shared/stale-policy.js';
|
|
5
|
+
import { formatMdCompact } from '../../shared/format-md-compact.js';
|
|
4
6
|
import { fail, ok } from '../../shared/result.js';
|
|
5
7
|
import { addJsonOption, getErrorMessage, printResult } from '../cli-helpers.js';
|
|
6
8
|
export function registerProjectCommands(program, io) {
|
|
@@ -158,4 +160,59 @@ export function registerProjectCommands(program, io) {
|
|
|
158
160
|
process.exitCode = 1;
|
|
159
161
|
}
|
|
160
162
|
});
|
|
163
|
+
// --- Show one project memory's body (R3: default = compact) ---
|
|
164
|
+
addJsonOption(project
|
|
165
|
+
.command('memories:show <name>')
|
|
166
|
+
.description('Show one project memory body by name. Default format is `compact` (LLM-primary); pass --pretty for the disk verbatim. Stale entries (default ≥30 days) are excluded; pass --include-stale or --stale-days <N> to override.')
|
|
167
|
+
.requiredOption('--project <path>', 'target project root')
|
|
168
|
+
.option('--pretty', 'return the on-disk body verbatim; overrides the compact default')
|
|
169
|
+
.option('--include-stale', 'include stale entries (the default excludes them)')
|
|
170
|
+
.option('--stale-days <n>', 'override the 30-day stale threshold (must be > 0)', (value) => Number(value))).action((name, options) => {
|
|
171
|
+
try {
|
|
172
|
+
const memory = readProjectMemoryBody(options.project, name);
|
|
173
|
+
if (memory === null) {
|
|
174
|
+
printResult(io, fail('project.memories:show', 'MEMORY_NOT_FOUND', `memory ${name} not found in .peaks/memory`, { name, projectRoot: options.project }, ['Run `peaks project memories --json` to see available names']), options.json);
|
|
175
|
+
process.exitCode = 1;
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
// Compute the stale decision. R4: stale is computed at CLI load time
|
|
179
|
+
// only; the source `.md` file is never modified.
|
|
180
|
+
const updatedAt = memory.updatedAt;
|
|
181
|
+
const thresholdDays = options.staleDays !== undefined && Number.isFinite(options.staleDays) && options.staleDays > 0
|
|
182
|
+
? options.staleDays
|
|
183
|
+
: DEFAULT_STALE_DAYS;
|
|
184
|
+
const policy = applyStalePolicy([{ name: memory.name, updatedAt }], {
|
|
185
|
+
thresholdDays,
|
|
186
|
+
includeStale: options.includeStale === true
|
|
187
|
+
});
|
|
188
|
+
if (policy.entries.length === 0) {
|
|
189
|
+
const ageDays = policy.entries.length === 0 && policy.droppedCount > 0
|
|
190
|
+
? applyStalePolicy([{ name: memory.name, updatedAt }], { thresholdDays, includeStale: true }).entries[0]?.ageDays ?? 0
|
|
191
|
+
: 0;
|
|
192
|
+
printResult(io, fail('project.memories:show', 'MEMORY_STALE', `memory ${name} is stale (age ${ageDays} days > ${thresholdDays} day threshold); pass --include-stale to override`, { name, ageDays, thresholdDays }, ['Pass --include-stale to load stale memories; pass --stale-days <N> to override the threshold']), options.json);
|
|
193
|
+
process.exitCode = 1;
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const ageDays = policy.entries[0]?.ageDays ?? 0;
|
|
197
|
+
const isStale = policy.entries[0]?.stale ?? false;
|
|
198
|
+
const format = options.pretty === true ? 'pretty' : 'compact';
|
|
199
|
+
const body = format === 'pretty' ? memory.body : formatMdCompact(memory.body);
|
|
200
|
+
printResult(io, ok('project.memories:show', {
|
|
201
|
+
name: memory.name,
|
|
202
|
+
title: memory.title,
|
|
203
|
+
kind: memory.kind,
|
|
204
|
+
sourcePath: memory.filePath,
|
|
205
|
+
updatedAt,
|
|
206
|
+
ageDays,
|
|
207
|
+
stale: isStale,
|
|
208
|
+
body,
|
|
209
|
+
format,
|
|
210
|
+
bodyBytes: Buffer.byteLength(body, 'utf8')
|
|
211
|
+
}), options.json);
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
printResult(io, fail('project.memories:show', 'PROJECT_MEMORY_SHOW_FAILED', getErrorMessage(error), { name, projectRoot: options.project }, ['Check the project path and .peaks/memory directory']), options.json);
|
|
215
|
+
process.exitCode = 1;
|
|
216
|
+
}
|
|
217
|
+
});
|
|
161
218
|
}
|
|
@@ -5,7 +5,82 @@ import { recordBypass, isBypassLimitReached, MAX_BYPASSES_PER_SESSION } from '..
|
|
|
5
5
|
import { lintRequestArtifact } from '../../services/artifacts/artifact-lint-service.js';
|
|
6
6
|
import { getRepairCycleStatus } from '../../services/artifacts/repair-cycle-service.js';
|
|
7
7
|
import { fail, ok } from '../../shared/result.js';
|
|
8
|
+
import { formatMdCompact } from '../../shared/format-md-compact.js';
|
|
8
9
|
import { addJsonOption, getErrorMessage, printResult } from '../cli-helpers.js';
|
|
10
|
+
/**
|
|
11
|
+
* Per-artifact default format. Only PRD and tech-doc are user-review
|
|
12
|
+
* surfaces; all other RD/QA/TXT artifacts default to compact. The
|
|
13
|
+
* `--pretty` / `--compact` flags override uniformly. See tech-doc
|
|
14
|
+
* §1.3.
|
|
15
|
+
*/
|
|
16
|
+
const DEFAULT_FORMAT_BY_ARTIFACT = {
|
|
17
|
+
prd: 'pretty',
|
|
18
|
+
'tech-doc': 'pretty',
|
|
19
|
+
'code-review': 'compact',
|
|
20
|
+
'security-review': 'compact',
|
|
21
|
+
'perf-baseline': 'compact',
|
|
22
|
+
'bug-analysis': 'compact',
|
|
23
|
+
'test-cases': 'compact',
|
|
24
|
+
'test-reports': 'compact',
|
|
25
|
+
'security-findings': 'compact',
|
|
26
|
+
'performance-findings': 'compact',
|
|
27
|
+
handoff: 'compact'
|
|
28
|
+
};
|
|
29
|
+
function resolveDefaultFormat(artifactName) {
|
|
30
|
+
return DEFAULT_FORMAT_BY_ARTIFACT[artifactName] ?? 'compact';
|
|
31
|
+
}
|
|
32
|
+
function applyPerArtifactFormat(envelope, override) {
|
|
33
|
+
if (override === null || envelope === null || typeof envelope !== 'object')
|
|
34
|
+
return envelope;
|
|
35
|
+
// The service returns `{ id, sessionId, role, body, ... }` for a
|
|
36
|
+
// single-artifact show. The per-artifact `body` field is the only
|
|
37
|
+
// thing that changes; we attach a `format` field to surface the
|
|
38
|
+
// choice to the caller. Slice 023 (R3) AC6 / AC7.
|
|
39
|
+
const obj = envelope;
|
|
40
|
+
if (typeof obj.body === 'string') {
|
|
41
|
+
return {
|
|
42
|
+
...obj,
|
|
43
|
+
body: override === 'compact' ? formatMdCompact(obj.body) : obj.body,
|
|
44
|
+
format: override
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
return envelope;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Map a request-artifact envelope to a `DEFAULT_FORMAT_BY_ARTIFACT` key.
|
|
51
|
+
* The service returns the path of the artifact in `path`; the role +
|
|
52
|
+
* filename stem gives us the artifact name. Falls back to the role
|
|
53
|
+
* itself when no `path` field is present.
|
|
54
|
+
*/
|
|
55
|
+
function inferArtifactName(envelope, role) {
|
|
56
|
+
if (envelope === null || typeof envelope !== 'object')
|
|
57
|
+
return role;
|
|
58
|
+
const obj = envelope;
|
|
59
|
+
const pathField = typeof obj.path === 'string' ? obj.path : '';
|
|
60
|
+
// Path looks like `.peaks/_runtime/<sid>/<role>/requests/<file>.md`.
|
|
61
|
+
// The role is the second-to-last directory; the file stem is the
|
|
62
|
+
// last segment. For RD role, the artifact is one of {code-review,
|
|
63
|
+
// security-review, perf-baseline, bug-analysis, tech-doc}; the file
|
|
64
|
+
// stem usually carries the artifact name (e.g. `tech-doc.md`,
|
|
65
|
+
// `code-review-002.md`). We strip the trailing `-<digits>` suffix
|
|
66
|
+
// when present.
|
|
67
|
+
const stem = pathField.split(/[\\/]/).pop()?.replace(/\.md$/, '') ?? '';
|
|
68
|
+
if (stem.length > 0) {
|
|
69
|
+
// Try the stem verbatim first.
|
|
70
|
+
if (DEFAULT_FORMAT_BY_ARTIFACT[stem] !== undefined)
|
|
71
|
+
return stem;
|
|
72
|
+
// Strip a trailing -<digits> (e.g. code-review-002 -> code-review).
|
|
73
|
+
const trimmed = stem.replace(/-\d+$/, '');
|
|
74
|
+
if (DEFAULT_FORMAT_BY_ARTIFACT[trimmed] !== undefined)
|
|
75
|
+
return trimmed;
|
|
76
|
+
// Last-ditch: match by prefix.
|
|
77
|
+
for (const key of Object.keys(DEFAULT_FORMAT_BY_ARTIFACT)) {
|
|
78
|
+
if (stem.startsWith(key))
|
|
79
|
+
return key;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return role;
|
|
83
|
+
}
|
|
9
84
|
const VALID_ROLES = ['prd', 'ui', 'rd', 'qa', 'sc'];
|
|
10
85
|
function parseRole(value) {
|
|
11
86
|
if (!VALID_ROLES.includes(value)) {
|
|
@@ -119,11 +194,13 @@ export function registerRequestCommands(program, io) {
|
|
|
119
194
|
});
|
|
120
195
|
addJsonOption(request
|
|
121
196
|
.command('show')
|
|
122
|
-
.description('Show a single per-request artifact, optionally scoped to a session')
|
|
197
|
+
.description('Show a single per-request artifact, optionally scoped to a session. R3: default body format is per-artifact (PRD/tech-doc pretty; everything else compact); pass --pretty or --compact to override uniformly.')
|
|
123
198
|
.argument('<request-id>', 'request id, e.g. 2026-05-23-add-foo')
|
|
124
199
|
.requiredOption('--role <role>', `target role (${VALID_ROLES.join(' | ')})`, parseRole)
|
|
125
200
|
.requiredOption('--project <path>', 'target project root')
|
|
126
|
-
.option('--session-id <session>', 'restrict to a specific session id')
|
|
201
|
+
.option('--session-id <session>', 'restrict to a specific session id')
|
|
202
|
+
.option('--pretty', 'force the body to render pretty (overrides the per-artifact default)')
|
|
203
|
+
.option('--compact', 'force the body to render compact (overrides the per-artifact default)')).action(async (requestId, options) => {
|
|
127
204
|
try {
|
|
128
205
|
const showOptions = {
|
|
129
206
|
projectRoot: options.project,
|
|
@@ -139,7 +216,20 @@ export function registerRequestCommands(program, io) {
|
|
|
139
216
|
process.exitCode = 1;
|
|
140
217
|
return;
|
|
141
218
|
}
|
|
142
|
-
|
|
219
|
+
// R3: pick the per-artifact default format and apply the override
|
|
220
|
+
// if either flag is set. Last-flag-wins if both are passed.
|
|
221
|
+
const override = options.compact === true
|
|
222
|
+
? 'compact'
|
|
223
|
+
: options.pretty === true
|
|
224
|
+
? 'pretty'
|
|
225
|
+
: null;
|
|
226
|
+
const artifactName = inferArtifactName(result, options.role);
|
|
227
|
+
const format = override ?? resolveDefaultFormat(artifactName);
|
|
228
|
+
const transformed = applyPerArtifactFormat(result, override ?? format);
|
|
229
|
+
const payload = transformed === result
|
|
230
|
+
? { ...result, format }
|
|
231
|
+
: transformed;
|
|
232
|
+
printResult(io, ok('request.show', payload), options.json);
|
|
143
233
|
}
|
|
144
234
|
catch (error) {
|
|
145
235
|
printResult(io, fail('request.show', 'REQUEST_SHOW_FAILED', getErrorMessage(error), { role: options.role, requestId }, ['Check role, request id, and project path before retrying']), options.json);
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { findProjectRoot } from '../../services/config/config-safety.js';
|
|
2
|
+
import { resolveCanonicalProjectRoot } from '../../services/config/config-service.js';
|
|
3
|
+
import { loadRetrospectiveIndex } from '../../services/retrospective/retrospective-index.js';
|
|
4
|
+
import { showRetrospective } from '../../services/retrospective/retrospective-show.js';
|
|
5
|
+
import { migrateRetrospectiveFromMd } from '../../services/retrospective/migrate-from-md.js';
|
|
6
|
+
import { fail, ok } from '../../shared/result.js';
|
|
7
|
+
import { addJsonOption, getErrorMessage, printResult } from '../cli-helpers.js';
|
|
8
|
+
export function registerRetrospectiveCommands(program, io) {
|
|
9
|
+
const retrospective = program.command('retrospective').description('Read the peaks retrospective index (R3: index.json, not the legacy <id>/ MD tree)');
|
|
10
|
+
addJsonOption(retrospective
|
|
11
|
+
.command('index')
|
|
12
|
+
.description('List all retrospective entries from .peaks/retrospective/index.json (R3: replaces the per-workflow MD dirs)')
|
|
13
|
+
.option('--project <path>', 'target project root (defaults to git root or cwd)')).action((options) => {
|
|
14
|
+
const projectRoot = options.project !== undefined
|
|
15
|
+
? resolveCanonicalProjectRoot(options.project)
|
|
16
|
+
: (findProjectRoot(process.cwd()) ?? process.cwd());
|
|
17
|
+
try {
|
|
18
|
+
const result = loadRetrospectiveIndex(projectRoot);
|
|
19
|
+
const warnings = result.warning === null ? [] : [result.warning];
|
|
20
|
+
printResult(io, ok('retrospective.index', {
|
|
21
|
+
indexPath: result.indexPath,
|
|
22
|
+
source: result.source,
|
|
23
|
+
total: result.totalCount,
|
|
24
|
+
entries: result.entries
|
|
25
|
+
}, warnings), options.json);
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
printResult(io, fail('retrospective.index', 'RETROSPECTIVE_INDEX_FAILED', getErrorMessage(error), { projectRoot }, ['Check the project path and .peaks/retrospective/index.json']), options.json);
|
|
29
|
+
process.exitCode = 1;
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
addJsonOption(retrospective
|
|
33
|
+
.command('show <id>')
|
|
34
|
+
.description('Show one retrospective entry by id. Default format is `compact` (LLM-primary); pass --pretty to get the disk / re-hydrated pretty form.')
|
|
35
|
+
.option('--project <path>', 'target project root (defaults to git root or cwd)')
|
|
36
|
+
.option('--pretty', 'return the pretty form (re-hydrated from source artifacts); overrides the compact default')).action((id, options) => {
|
|
37
|
+
const projectRoot = options.project !== undefined
|
|
38
|
+
? resolveCanonicalProjectRoot(options.project)
|
|
39
|
+
: (findProjectRoot(process.cwd()) ?? process.cwd());
|
|
40
|
+
try {
|
|
41
|
+
const format = options.pretty === true ? 'pretty' : 'compact';
|
|
42
|
+
const result = showRetrospective({ projectRoot, id, format });
|
|
43
|
+
if (!result.ok) {
|
|
44
|
+
const suggestions = [];
|
|
45
|
+
if (result.code === 'INDEX_MISSING')
|
|
46
|
+
suggestions.push('Run `peaks retrospective migrate --apply` to build the index');
|
|
47
|
+
if (result.code === 'NOT_FOUND')
|
|
48
|
+
suggestions.push('Run `peaks retrospective index --json` to see available ids');
|
|
49
|
+
if (result.code === 'ARTIFACT_MISSING' || result.missingArtifacts !== undefined) {
|
|
50
|
+
suggestions.push('Re-hydrate from the legacy archive at .peaks/_archive/retrospective-2026-06-09-pre-r3.tar.gz');
|
|
51
|
+
}
|
|
52
|
+
printResult(io, fail('retrospective.show', result.code, result.message, { id, projectRoot, ...(result.missingArtifacts !== undefined ? { missingArtifacts: result.missingArtifacts } : {}) }, suggestions), options.json);
|
|
53
|
+
process.exitCode = 1;
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
printResult(io, ok('retrospective.show', {
|
|
57
|
+
id: result.entry.id,
|
|
58
|
+
sessionId: result.entry.sessionId,
|
|
59
|
+
sliceId: result.entry.sliceId ?? null,
|
|
60
|
+
type: result.entry.type,
|
|
61
|
+
title: result.entry.title,
|
|
62
|
+
summary: result.entry.summary,
|
|
63
|
+
outcome: result.entry.outcome,
|
|
64
|
+
keyDecisions: result.entry.keyDecisions,
|
|
65
|
+
lessonsLearned: result.entry.lessonsLearned,
|
|
66
|
+
artifactPaths: result.entry.artifactPaths,
|
|
67
|
+
updatedAt: result.entry.updatedAt,
|
|
68
|
+
body: result.body,
|
|
69
|
+
format: result.format
|
|
70
|
+
}, result.warnings), options.json);
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
printResult(io, fail('retrospective.show', 'RETROSPECTIVE_SHOW_FAILED', getErrorMessage(error), { id, projectRoot }, ['Check the project path and id']), options.json);
|
|
74
|
+
process.exitCode = 1;
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
addJsonOption(retrospective
|
|
78
|
+
.command('migrate')
|
|
79
|
+
.description('One-time migration from per-workflow .peaks/retrospective/<id>/*.md dirs to a single .peaks/retrospective/index.json + .peaks/_archive/retrospective-2026-06-09-pre-r3.tar.gz archive. Dry-run by default; --apply is destructive.')
|
|
80
|
+
.option('--project <path>', 'target project root (defaults to git root or cwd)')
|
|
81
|
+
.option('--apply', 'write the index.json + archive + delete legacy MDs (default: dry-run preview)')
|
|
82
|
+
.option('--include-failed', 'include malformed MDs as best-effort entries; default is to skip + warn')
|
|
83
|
+
.option('--expected-entries <n>', 'override the default expected entry count (88) for the no-op check', (value) => Number(value))).action((options) => {
|
|
84
|
+
const projectRoot = options.project !== undefined
|
|
85
|
+
? resolveCanonicalProjectRoot(options.project)
|
|
86
|
+
: (findProjectRoot(process.cwd()) ?? process.cwd());
|
|
87
|
+
try {
|
|
88
|
+
const result = migrateRetrospectiveFromMd({
|
|
89
|
+
projectRoot,
|
|
90
|
+
apply: options.apply === true,
|
|
91
|
+
includeFailed: options.includeFailed === true,
|
|
92
|
+
...(options.expectedEntries !== undefined && Number.isFinite(options.expectedEntries) ? { expectedEntries: options.expectedEntries } : {})
|
|
93
|
+
});
|
|
94
|
+
const exitCode = result.status === 'failed' ? 1 : 0;
|
|
95
|
+
printResult(io, ok('retrospective.migrate', {
|
|
96
|
+
status: result.status,
|
|
97
|
+
indexPath: result.indexPath,
|
|
98
|
+
archivePath: result.archivePath,
|
|
99
|
+
totalLegacyDirs: result.totalLegacyDirs,
|
|
100
|
+
totalLegacyMds: result.totalLegacyMds,
|
|
101
|
+
parsedEntries: result.parsedEntries,
|
|
102
|
+
failedEntries: result.failedEntries,
|
|
103
|
+
archiveVerified: result.archiveVerified
|
|
104
|
+
}, result.warnings), options.json);
|
|
105
|
+
if (exitCode !== 0)
|
|
106
|
+
process.exitCode = exitCode;
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
printResult(io, fail('retrospective.migrate', 'RETROSPECTIVE_MIGRATE_FAILED', getErrorMessage(error), { projectRoot }, ['Check the project path and .peaks/retrospective/ directory']), options.json);
|
|
110
|
+
process.exitCode = 1;
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
}
|
package/dist/src/cli/program.js
CHANGED
|
@@ -14,6 +14,7 @@ import { registerPerfCommands } from './commands/perf-commands.js';
|
|
|
14
14
|
// surfaced via `peaks sub-agent dispatch|heartbeat|share`.
|
|
15
15
|
import { registerProjectCommands } from './commands/project-commands.js';
|
|
16
16
|
import { registerRequestCommands } from './commands/request-commands.js';
|
|
17
|
+
import { registerRetrospectiveCommands } from './commands/retrospective-commands.js';
|
|
17
18
|
import { registerScanCommands } from './commands/scan-commands.js';
|
|
18
19
|
import { registerShadcnCommands } from './commands/shadcn-commands.js';
|
|
19
20
|
import { registerSliceCommands } from './commands/slice-commands.js';
|
|
@@ -90,6 +91,7 @@ Run peaks (no arguments) for a quickstart. You likely want one of:
|
|
|
90
91
|
registerPerfCommands(program, io);
|
|
91
92
|
registerProjectCommands(program, io);
|
|
92
93
|
registerRequestCommands(program, io);
|
|
94
|
+
registerRetrospectiveCommands(program, io);
|
|
93
95
|
registerScanCommands(program, io);
|
|
94
96
|
registerShadcnCommands(program, io);
|
|
95
97
|
registerSliceCommands(program, io);
|
|
@@ -142,4 +142,23 @@ export declare function summarizeProjectMemoryBackupResult(result: ProjectMemory
|
|
|
142
142
|
*/
|
|
143
143
|
export declare function ensureMemoryBootstrap(projectRoot: string): boolean;
|
|
144
144
|
export declare function readProjectMemories(projectRoot: string): ProjectMemoryReadResult;
|
|
145
|
+
export interface ProjectMemoryShowResult {
|
|
146
|
+
projectRoot: string;
|
|
147
|
+
memoryDir: string;
|
|
148
|
+
name: string;
|
|
149
|
+
body: string;
|
|
150
|
+
filePath: string;
|
|
151
|
+
updatedAt: string | null;
|
|
152
|
+
kind: ProjectMemoryKind | null;
|
|
153
|
+
title: string;
|
|
154
|
+
/** Whether the on-disk body bytes are returned (true) or a compact form (false). */
|
|
155
|
+
pretty: boolean;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Read a single project memory's full body by name. Returns null when
|
|
159
|
+
* the memory does not exist. The on-disk body is returned verbatim
|
|
160
|
+
* (pretty). The CLI layer applies `formatMdCompact` when `format: 'compact'`
|
|
161
|
+
* is requested. Slice 023 (R3).
|
|
162
|
+
*/
|
|
163
|
+
export declare function readProjectMemoryBody(projectRoot: string, name: string): ProjectMemoryShowResult | null;
|
|
145
164
|
export {};
|
|
@@ -779,3 +779,36 @@ export function readProjectMemories(projectRoot) {
|
|
|
779
779
|
memories
|
|
780
780
|
};
|
|
781
781
|
}
|
|
782
|
+
/**
|
|
783
|
+
* Read a single project memory's full body by name. Returns null when
|
|
784
|
+
* the memory does not exist. The on-disk body is returned verbatim
|
|
785
|
+
* (pretty). The CLI layer applies `formatMdCompact` when `format: 'compact'`
|
|
786
|
+
* is requested. Slice 023 (R3).
|
|
787
|
+
*/
|
|
788
|
+
export function readProjectMemoryBody(projectRoot, name) {
|
|
789
|
+
const normalizedRoot = normalizeRoot(projectRoot);
|
|
790
|
+
const memoryDir = assertSafeProjectMemoryDir(normalizedRoot);
|
|
791
|
+
if (!existsSync(memoryDir)) {
|
|
792
|
+
ensureMemoryBootstrap(normalizedRoot);
|
|
793
|
+
}
|
|
794
|
+
for (const filePath of listMarkdownFiles(memoryDir)) {
|
|
795
|
+
if (basename(filePath, '.md') !== name)
|
|
796
|
+
continue;
|
|
797
|
+
const parsed = parseStoredMemoryFile(readFileSync(filePath, 'utf8'), filePath);
|
|
798
|
+
if (parsed === null)
|
|
799
|
+
continue;
|
|
800
|
+
const updatedAt = readMemoryFileMtime(filePath);
|
|
801
|
+
return {
|
|
802
|
+
projectRoot: normalizedRoot,
|
|
803
|
+
memoryDir,
|
|
804
|
+
name: parsed.name,
|
|
805
|
+
body: parsed.body,
|
|
806
|
+
filePath,
|
|
807
|
+
updatedAt,
|
|
808
|
+
kind: parsed.kind,
|
|
809
|
+
title: parsed.title,
|
|
810
|
+
pretty: true
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
return null;
|
|
814
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* migrate-from-md — one-time migration from `.peaks/retrospective/<id>/*.md`
|
|
3
|
+
* per-workflow MD dirs to a single `.peaks/retrospective/index.json` plus a
|
|
4
|
+
* `.peaks/_archive/retrospective-2026-06-09-pre-r3.tar.gz` archive.
|
|
5
|
+
*
|
|
6
|
+
* Slice 023 (R3) G9. Idempotent: re-run is a no-op when `index.json` has
|
|
7
|
+
* 88 entries with matching `updatedAt`.
|
|
8
|
+
*
|
|
9
|
+
* The legacy MDs in this repo use a *bullet-list* metadata format (no YAML
|
|
10
|
+
* frontmatter). The fields we need are extracted from the leading bullet
|
|
11
|
+
* block: `session:`, `rid:`, `type:`, `sliceId:`, plus the first `# Title`
|
|
12
|
+
* heading.
|
|
13
|
+
*/
|
|
14
|
+
export interface MigrateOptions {
|
|
15
|
+
projectRoot: string;
|
|
16
|
+
apply?: boolean;
|
|
17
|
+
includeFailed?: boolean;
|
|
18
|
+
expectedEntries?: number;
|
|
19
|
+
}
|
|
20
|
+
export interface MigrateResult {
|
|
21
|
+
apply: boolean;
|
|
22
|
+
projectRoot: string;
|
|
23
|
+
indexPath: string;
|
|
24
|
+
archivePath: string | null;
|
|
25
|
+
sourceDir: string;
|
|
26
|
+
totalLegacyDirs: number;
|
|
27
|
+
totalLegacyMds: number;
|
|
28
|
+
parsedEntries: number;
|
|
29
|
+
failedEntries: Array<{
|
|
30
|
+
id: string;
|
|
31
|
+
reason: string;
|
|
32
|
+
}>;
|
|
33
|
+
archiveVerified: boolean;
|
|
34
|
+
status: 'applied' | 'no-op' | 'partial' | 'failed';
|
|
35
|
+
warnings: string[];
|
|
36
|
+
}
|
|
37
|
+
export declare function migrateRetrospectiveFromMd(options: MigrateOptions): MigrateResult;
|