peaks-cli 1.3.7 → 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.
Files changed (98) hide show
  1. package/dist/src/cli/commands/core-artifact-commands.js +119 -14
  2. package/dist/src/cli/commands/project-commands.js +58 -1
  3. package/dist/src/cli/commands/request-commands.js +124 -4
  4. package/dist/src/cli/commands/retrospective-commands.d.ts +3 -0
  5. package/dist/src/cli/commands/retrospective-commands.js +113 -0
  6. package/dist/src/cli/program.js +2 -0
  7. package/dist/src/services/artifacts/request-artifact-service.d.ts +16 -0
  8. package/dist/src/services/artifacts/request-artifact-service.js +18 -2
  9. package/dist/src/services/memory/project-memory-service.d.ts +19 -0
  10. package/dist/src/services/memory/project-memory-service.js +33 -0
  11. package/dist/src/services/retrospective/migrate-from-md.d.ts +37 -0
  12. package/dist/src/services/retrospective/migrate-from-md.js +528 -0
  13. package/dist/src/services/retrospective/retrospective-index.d.ts +37 -0
  14. package/dist/src/services/retrospective/retrospective-index.js +110 -0
  15. package/dist/src/services/retrospective/retrospective-show.d.ts +40 -0
  16. package/dist/src/services/retrospective/retrospective-show.js +109 -0
  17. package/dist/src/services/session/caller-binding-service.d.ts +70 -0
  18. package/dist/src/services/session/caller-binding-service.js +148 -0
  19. package/dist/src/services/session/caller-id-types.d.ts +77 -0
  20. package/dist/src/services/session/caller-id-types.js +46 -0
  21. package/dist/src/services/session/index.d.ts +4 -0
  22. package/dist/src/services/session/index.js +5 -0
  23. package/dist/src/services/session/platform-fallbacks.d.ts +31 -0
  24. package/dist/src/services/session/platform-fallbacks.js +35 -0
  25. package/dist/src/services/session/resolve-caller-id.d.ts +57 -0
  26. package/dist/src/services/session/resolve-caller-id.js +88 -0
  27. package/dist/src/services/skills/skill-presence-service.d.ts +11 -0
  28. package/dist/src/services/skills/skill-presence-service.js +59 -0
  29. package/dist/src/shared/format-md-compact.d.ts +32 -0
  30. package/dist/src/shared/format-md-compact.js +297 -0
  31. package/dist/src/shared/stale-policy.d.ts +67 -0
  32. package/dist/src/shared/stale-policy.js +85 -0
  33. package/dist/src/shared/version.d.ts +1 -1
  34. package/dist/src/shared/version.js +1 -1
  35. package/package.json +1 -1
  36. package/skills/peaks-qa/SKILL.md +86 -515
  37. package/skills/peaks-qa/references/artifact-per-request.md +7 -79
  38. package/skills/peaks-qa/references/browser-validation-contracts.md +51 -0
  39. package/skills/peaks-qa/references/codegraph-regression-focus.md +5 -0
  40. package/skills/peaks-qa/references/external-capability-guidance.md +9 -0
  41. package/skills/peaks-qa/references/qa-compact-handoff.md +3 -0
  42. package/skills/peaks-qa/references/qa-context-governance.md +24 -0
  43. package/skills/peaks-qa/references/qa-fanout-contract.md +8 -0
  44. package/skills/peaks-qa/references/qa-gstack-integration.md +7 -0
  45. package/skills/peaks-qa/references/qa-local-artifacts.md +3 -0
  46. package/skills/peaks-qa/references/qa-matt-pocock-integration.md +9 -0
  47. package/skills/peaks-qa/references/qa-refactor-role.md +3 -0
  48. package/skills/peaks-qa/references/qa-runbook.md +74 -0
  49. package/skills/peaks-qa/references/qa-skill-presence.md +22 -0
  50. package/skills/peaks-qa/references/qa-standards-preflight.md +8 -0
  51. package/skills/peaks-qa/references/qa-sub-agent-dispatch.md +38 -0
  52. package/skills/peaks-qa/references/qa-transition-gates.md +79 -0
  53. package/skills/peaks-qa/references/requirement-boundary-recheck.md +9 -0
  54. package/skills/peaks-qa/references/test-case-generation.md +27 -0
  55. package/skills/peaks-qa/references/test-report-output.md +14 -0
  56. package/skills/peaks-rd/SKILL.md +85 -732
  57. package/skills/peaks-rd/references/artifact-and-standards-output.md +9 -0
  58. package/skills/peaks-rd/references/artifact-per-request.md +20 -0
  59. package/skills/peaks-rd/references/browser-self-test-contracts.md +29 -0
  60. package/skills/peaks-rd/references/codegraph-project-analysis.md +5 -0
  61. package/skills/peaks-rd/references/compact-handoff.md +3 -0
  62. package/skills/peaks-rd/references/external-references.md +11 -0
  63. package/skills/peaks-rd/references/frontend-project-generation.md +11 -0
  64. package/skills/peaks-rd/references/library-version-awareness.md +30 -0
  65. package/skills/peaks-rd/references/mandatory-perf-baseline.md +40 -0
  66. package/skills/peaks-rd/references/mandatory-tech-doc.md +18 -0
  67. package/skills/peaks-rd/references/matt-pocock-integration.md +11 -0
  68. package/skills/peaks-rd/references/mock-data-placement.md +40 -0
  69. package/skills/peaks-rd/references/parallel-review-fanout.md +81 -0
  70. package/skills/peaks-rd/references/rd-context-governance.md +36 -0
  71. package/skills/peaks-rd/references/rd-gstack-integration.md +16 -0
  72. package/skills/peaks-rd/references/rd-runbook.md +125 -0
  73. package/skills/peaks-rd/references/rd-standards-preflight.md +8 -0
  74. package/skills/peaks-rd/references/rd-sub-agent-dispatch.md +39 -0
  75. package/skills/peaks-rd/references/rd-transition-gates.md +148 -0
  76. package/skills/peaks-rd/references/skill-presence-and-title.md +22 -0
  77. package/skills/peaks-solo/SKILL.md +87 -786
  78. package/skills/peaks-solo/references/anchoring-and-session-info.md +25 -0
  79. package/skills/peaks-solo/references/boundaries.md +21 -0
  80. package/skills/peaks-solo/references/codegraph-orchestration.md +5 -0
  81. package/skills/peaks-solo/references/completion-handoff.md +16 -0
  82. package/skills/peaks-solo/references/context-governance.md +51 -0
  83. package/skills/peaks-solo/references/external-references.md +17 -0
  84. package/skills/peaks-solo/references/frontend-only-mode.md +87 -0
  85. package/skills/peaks-solo/references/gstack-integration.md +7 -0
  86. package/skills/peaks-solo/references/local-artifact-workspace.md +79 -0
  87. package/skills/peaks-solo/references/micro-cycle.md +68 -0
  88. package/skills/peaks-solo/references/mode-selection.md +21 -0
  89. package/skills/peaks-solo/references/openspec-workflow.md +43 -0
  90. package/skills/peaks-solo/references/project-memory-loading.md +17 -0
  91. package/skills/peaks-solo/references/project-scan-checklist.md +136 -0
  92. package/skills/peaks-solo/references/quality-gate-cheatsheet.md +13 -0
  93. package/skills/peaks-solo/references/resume-detection.md +63 -0
  94. package/skills/peaks-solo/references/runbook.md +1 -1
  95. package/skills/peaks-solo/references/skill-presence-and-title.md +31 -0
  96. package/skills/peaks-solo/references/standards-preflight.md +23 -0
  97. package/skills/peaks-solo/references/sub-agent-dispatch.md +46 -0
  98. package/skills/peaks-solo/references/swarm-dispatch-contract.md +56 -0
@@ -1,3 +1,5 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
1
3
  import { createArtifactInitPlan, getArtifactStatus, createGuidedArtifactSetup } from '../../services/artifacts/artifact-service.js';
2
4
  import { getArtifactWorkspaceStatus, planArtifactSync } from '../../services/artifacts/workspace-service.js';
3
5
  import { executeProjectMemoryBackup, executeProjectMemoryExtract, summarizeProjectMemoryBackupResult, summarizeProjectMemoryExtractResult } from '../../services/memory/project-memory-service.js';
@@ -206,26 +208,129 @@ export function registerCoreAndArtifactCommands(program, io) {
206
208
  });
207
209
  addJsonOption(session
208
210
  .command('info [sessionId]')
209
- .description('Show full metadata for a session directory. Pass --active to resolve the canonical binding from .peaks/_runtime/session.json (the "one command a sub-agent runs to find the parent\'s sid" primitive).')
210
- .option('--active', 'resolve the canonical session id from .peaks/_runtime/session.json (ignores [sessionId] when set)')).action(async (sessionId, options) => {
211
- const projectRoot = findProjectRoot(process.cwd()) ?? process.cwd();
212
- // Slice 007sub-agent session sharing. A sub-agent that does
213
- // not know the parent's sid reads it from the binding via
214
- // `peaks session info --active`. The call uses the
215
- // canonicalize-on-read path so a stored "projectRoot: '.'" and a
216
- // caller-passed absolute realpath both resolve to the same
217
- // binding. Without this primitive the sub-agent has no way to
218
- // discover the parent sid short of scanning the filesystem.
211
+ .description('Show full metadata for a session directory. Pass --active to resolve the canonical binding from .peaks/_runtime/session.json (the "one command a sub-agent runs to find the parent\'s sid" primitive). Slice 021: --active is the SOLE authoritative way to look up the active session id; the on-disk file path is internal and must NOT be `cat`-ed directly.')
212
+ .option('--active', 'resolve the canonical session id from .peaks/_runtime/session.json (ignores [sessionId] when set)')
213
+ .option('--project <path>', 'target project root (defaults to git root or cwd). Slice 021: lets sub-agents skip the cwd heuristic and look up the binding for a specific repo.')
214
+ // Slice 020caller-keyed session binding. The --caller-id flag
215
+ // overrides the per-process PEAKS_CALLER_ID env var and the
216
+ // PLATFORM_FALLBACKS table (D4 priority). The resolved callerId is
217
+ // surfaced in the JSON envelope so callers can confirm what was
218
+ // resolved without re-deriving it.
219
+ .option('--caller-id <id>', 'Override the caller id for this invocation (D4 priority: flag beats env beats platform fallback). When set, the response envelope includes the resolved callerId.')).action(async (sessionId, options) => {
220
+ // Slice 021: --project wins; otherwise the git-root / cwd fallback
221
+ // (matches the pre-021 behaviour so the existing slice-020 / slice-007
222
+ // sub-agent flow keeps working unchanged).
223
+ const projectRoot = options.project !== undefined
224
+ ? resolveCanonicalProjectRoot(options.project)
225
+ : (findProjectRoot(process.cwd()) ?? process.cwd());
226
+ // Slice 020 — resolve the callerId up front when the flag was passed.
227
+ // We use `resolveCallerId` for D1/D5 validation; an invalid flag
228
+ // throws CallerIdError (D5 → exit 65). A missing flag and no env
229
+ // and no fallback also throws (D2 → exit 64). The resolved id is
230
+ // surfaced in the envelope so the caller can audit.
231
+ if (options.callerId !== undefined) {
232
+ const { resolveCallerId, CallerIdError } = await import('../../services/session/resolve-caller-id.js');
233
+ let callerId;
234
+ try {
235
+ callerId = resolveCallerId({ flagValue: options.callerId });
236
+ }
237
+ catch (error) {
238
+ if (error instanceof CallerIdError) {
239
+ const code = error.code === 'EX_USAGE' ? 64 : 65;
240
+ printResult(io, fail('session.info', 'CALLER_ID_INVALID', error.message, { source: error.source }, [`Set --caller-id to a value matching ^[a-zA-Z0-9._-]{1,200}$`, 'Or set PEAKS_CALLER_ID env var (or CLAUDE_CODE_SESSION_ID for Claude Code)']), options.json);
241
+ process.exitCode = code;
242
+ return;
243
+ }
244
+ throw error;
245
+ }
246
+ // Surface the resolved id in the envelope. When --active is also
247
+ // passed, look up the binding for this callerId; otherwise
248
+ // just emit the resolved id so the caller knows what was used.
249
+ if (options.active === true) {
250
+ const { getSessionIdCanonical } = await import('../../services/session/session-manager.js');
251
+ const { getCallerBinding } = await import('../../services/session/caller-binding-service.js');
252
+ const activeSid = getSessionIdCanonical(projectRoot);
253
+ const callerBinding = getCallerBinding(projectRoot, callerId);
254
+ // Slice 021: source is the enum that the unified --active primitive
255
+ // reports. Callers / migration tooling can detect pre-migration trees
256
+ // by inspecting `source === 'legacy'`.
257
+ const bindingSource = existsSync(join(projectRoot, '.peaks', '_runtime', 'session.json'))
258
+ ? 'canonical'
259
+ : 'legacy';
260
+ printResult(io, ok('session.info', {
261
+ active: true,
262
+ sessionId: activeSid,
263
+ callerId,
264
+ callerBindingPeakSessionId: callerBinding?.peakSessionId ?? null,
265
+ source: bindingSource
266
+ }), options.json);
267
+ return;
268
+ }
269
+ printResult(io, ok('session.info', { callerId, note: '--caller-id resolved; pass --active to also look up the bound peak session' }), options.json);
270
+ return;
271
+ }
272
+ // Slice 007 + slice 021 — sub-agent session sharing. A sub-agent that
273
+ // does not know the parent's sid reads it from the binding via
274
+ // `peaks session info --active`. Slice 021 turned this into the
275
+ // SOLE authoritative discovery primitive: it composes on
276
+ // getSessionIdCanonical (canonicalize-on-read; handles the stored
277
+ // "projectRoot: '.'" vs caller-passed absolute realpath mismatch
278
+ // that the F22 fix addressed) AND falls through to getSessionId
279
+ // (strict-equality) for callers on the original contract. NEITHER
280
+ // path may call ensureSession() — that would side-effect-create a
281
+ // fresh binding on miss, erasing the "no active session" signal
282
+ // sub-agents rely on.
219
283
  if (options.active === true) {
220
284
  // Import lazily to avoid a cycle with workspace-commands.
221
- const { getSessionIdCanonical } = await import('../../services/session/session-manager.js');
222
- const activeSid = getSessionIdCanonical(projectRoot);
285
+ const { getSessionIdCanonical, getSessionId } = await import('../../services/session/session-manager.js');
286
+ // 1. Canonicalize-on-read first.
287
+ let activeSid = getSessionIdCanonical(projectRoot);
288
+ // 2. Fall through to the strict-equality reader if the canonical
289
+ // miss is a projectRoot-form mismatch (e.g. the binding was
290
+ // written with the absolute realpath but the caller's form
291
+ // normalizes differently). The 2-read fan-out mirrors the
292
+ // fallback ensureSession() uses.
293
+ if (activeSid === null) {
294
+ activeSid = getSessionId(projectRoot);
295
+ }
223
296
  if (activeSid === null) {
224
- printResult(io, fail('session.info', 'NO_ACTIVE_BINDING', 'No canonical session binding at .peaks/_runtime/session.json', { projectRoot }, ['Run `peaks workspace init` or `peaks skill presence:set` to anchor a session']), options.json);
297
+ // 3. No binding at all fail loudly, NO crash, NO side-effect,
298
+ // exit 1, message must point at `peaks workspace init`
299
+ // (the canonical "first action" command, not the legacy
300
+ // "or `peaks skill presence:set`" hedge that the pre-021
301
+ // wording used — presence:set would also need a binding to
302
+ // resolve the parent sid, so it's not actually a bootstrap
303
+ // path).
304
+ printResult(io, fail('session.info', 'NO_ACTIVE_SESSION', 'No session bound. Run `peaks workspace init --project <repo> --json` to bind one.', { projectRoot }, [`peaks workspace init --project ${projectRoot} --json`]), options.json);
225
305
  process.exitCode = 1;
226
306
  return;
227
307
  }
228
- printResult(io, ok('session.info', { active: true, sessionId: activeSid, source: '.peaks/_runtime/session.json' }), options.json);
308
+ // 4. Determine the on-disk source so callers (and future
309
+ // migration tooling) can detect pre-migration trees. The
310
+ // canonical file is preferred when both exist (slice 005
311
+ // contract).
312
+ const bindingSource = existsSync(join(projectRoot, '.peaks', '_runtime', 'session.json'))
313
+ ? 'canonical'
314
+ : 'legacy';
315
+ // Slice 021: when only the legacy back-compat path is present,
316
+ // surface a warning so callers (and humans tailing the JSON)
317
+ // see "this tree has not been reconciled to the canonical
318
+ // home yet". The warning is informational; the binding is
319
+ // still valid for one minor release (slice 005 / 006
320
+ // contract). `warnings` is a top-level envelope field, not a
321
+ // data field, so it goes through ok()'s 3rd positional arg.
322
+ const legacyWarnings = bindingSource === 'legacy'
323
+ ? ['Read from legacy back-compat path .peaks/.session.json. Run `peaks workspace reconcile --apply` to migrate to the canonical home (.peaks/_runtime/session.json).']
324
+ : [];
325
+ printResult(io, ok('session.info', {
326
+ active: true,
327
+ sessionId: activeSid,
328
+ bindingPath: bindingSource === 'canonical'
329
+ ? join(projectRoot, '.peaks', '_runtime', 'session.json')
330
+ : join(projectRoot, '.peaks', '.session.json'),
331
+ projectRoot,
332
+ source: bindingSource
333
+ }, legacyWarnings), options.json);
229
334
  return;
230
335
  }
231
336
  if (sessionId === undefined) {
@@ -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)) {
@@ -36,7 +111,12 @@ export function registerRequestCommands(program, io) {
36
111
  .requiredOption('--project <path>', 'target project root')
37
112
  .option('--session-id <session>', 'override the default date-stamped session id')
38
113
  .option('--apply', 'write the artifact file (default: preview only)')
39
- .option('--type <type>', `request type (${VALID_REQUEST_TYPES.join(' | ')}); default: feature`, parseRequestType)).action(async (options) => {
114
+ .option('--type <type>', `request type (${VALID_REQUEST_TYPES.join(' | ')}); default: feature`, parseRequestType)
115
+ // Slice 020 — caller-keyed session binding. Per-invocation override
116
+ // (D4 priority level 1). When set, the resolved callerId is surfaced
117
+ // in the JSON envelope; the on-disk artifact records it in the
118
+ // artifact body so future reads know which caller produced it.
119
+ .option('--caller-id <id>', 'Override the caller id for this invocation (D4 priority: flag beats env beats platform fallback). The resolved callerId is stamped on the artifact body and surfaced in the response envelope.')).action(async (options) => {
40
120
  try {
41
121
  const serviceOptions = {
42
122
  role: options.role,
@@ -57,6 +137,31 @@ export function registerRequestCommands(program, io) {
57
137
  if (options.type !== undefined) {
58
138
  serviceOptions.requestType = options.type;
59
139
  }
140
+ // Slice 020.1 — resolve the callerId via D4 priority (flag > env >
141
+ // platform fallback > reject). The CLI integration layer is the
142
+ // single entry point for the resolver; we do not pre-judge whether
143
+ // the caller passed a flag. D2 (no callerId available) and D5
144
+ // (regex fail) both surface as `CALLER_ID_INVALID` with the inner
145
+ // `CallerIdError.source` propagated for caller-side audit.
146
+ const { resolveCallerId, CallerIdError } = await import('../../services/session/resolve-caller-id.js');
147
+ try {
148
+ const callerId = resolveCallerId(options.callerId !== undefined ? { flagValue: options.callerId } : {});
149
+ serviceOptions.callerId = callerId;
150
+ }
151
+ catch (error) {
152
+ if (error instanceof CallerIdError) {
153
+ // D2 (EX_USAGE, exit 64) = nothing usable; D5 (EX_DATAERR, exit 65)
154
+ // = something was passed but did not match the D1 regex.
155
+ const code = error.code === 'EX_USAGE' ? 64 : 65;
156
+ printResult(io, fail('request.init', 'CALLER_ID_INVALID', error.message, { source: error.source }, [
157
+ 'Set --caller-id to a value matching ^[a-zA-Z0-9._-]{1,200}$',
158
+ 'Or set PEAKS_CALLER_ID env var (or CLAUDE_CODE_SESSION_ID for Claude Code)'
159
+ ]), options.json);
160
+ process.exitCode = code;
161
+ return;
162
+ }
163
+ throw error;
164
+ }
60
165
  const result = await createRequestArtifact(serviceOptions);
61
166
  printResult(io, ok('request.init', result, [], result.applied ? [] : [`Re-run with --apply to write ${result.path}`]), options.json);
62
167
  }
@@ -89,11 +194,13 @@ export function registerRequestCommands(program, io) {
89
194
  });
90
195
  addJsonOption(request
91
196
  .command('show')
92
- .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.')
93
198
  .argument('<request-id>', 'request id, e.g. 2026-05-23-add-foo')
94
199
  .requiredOption('--role <role>', `target role (${VALID_ROLES.join(' | ')})`, parseRole)
95
200
  .requiredOption('--project <path>', 'target project root')
96
- .option('--session-id <session>', 'restrict to a specific session id')).action(async (requestId, options) => {
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) => {
97
204
  try {
98
205
  const showOptions = {
99
206
  projectRoot: options.project,
@@ -109,7 +216,20 @@ export function registerRequestCommands(program, io) {
109
216
  process.exitCode = 1;
110
217
  return;
111
218
  }
112
- printResult(io, ok('request.show', result), options.json);
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);
113
233
  }
114
234
  catch (error) {
115
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,3 @@
1
+ import { Command } from 'commander';
2
+ import { type ProgramIO } from '../cli-helpers.js';
3
+ export declare function registerRetrospectiveCommands(program: Command, io: ProgramIO): void;
@@ -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
+ }
@@ -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);
@@ -17,6 +17,16 @@ export type CreateRequestArtifactOptions = {
17
17
  apply?: boolean;
18
18
  requestType?: RequestType;
19
19
  clock?: () => string;
20
+ /**
21
+ * Slice 020 — caller-keyed session binding. The callerId that initiated
22
+ * this artifact creation (resolved via `resolveCallerId` by the CLI).
23
+ * Recorded in the JSON envelope and on the artifact body's frontmatter
24
+ * so a future reader knows which caller produced it. The caller-keyed
25
+ * binding file itself is at `.peaks/_runtime/callers/<callerId>.json`;
26
+ * the per-caller active-skill marker is at
27
+ * `.peaks/_runtime/<peakSid>/active-skill-<callerId>.json` (D6).
28
+ */
29
+ callerId?: string;
20
30
  };
21
31
  export type CreateRequestArtifactResult = {
22
32
  role: RequestArtifactRole;
@@ -25,6 +35,12 @@ export type CreateRequestArtifactResult = {
25
35
  path: string;
26
36
  content: string;
27
37
  applied: boolean;
38
+ /**
39
+ * Slice 020 — caller-keyed session binding. The resolved callerId
40
+ * (D4 priority: flag > env > platform fallback) when --caller-id
41
+ * was passed. Omitted from the result when no callerId was set.
42
+ */
43
+ callerId?: string;
28
44
  };
29
45
  export declare function createRequestArtifact(options: CreateRequestArtifactOptions): Promise<CreateRequestArtifactResult>;
30
46
  export type RequestArtifactSummary = {
@@ -374,7 +374,15 @@ export async function createRequestArtifact(options) {
374
374
  const path = join(requestsDir, filename);
375
375
  const content = renderTemplate(options.role, options.requestId, changeId, sessionId, timestamp, requestType);
376
376
  if (options.apply !== true) {
377
- return { role: options.role, requestId: options.requestId, sessionId, path, content, applied: false };
377
+ return {
378
+ role: options.role,
379
+ requestId: options.requestId,
380
+ sessionId,
381
+ path,
382
+ content,
383
+ applied: false,
384
+ ...(options.callerId !== undefined ? { callerId: options.callerId } : {})
385
+ };
378
386
  }
379
387
  await mkdir(dirname(path), { recursive: true });
380
388
  await writeFile(path, content, 'utf8');
@@ -390,7 +398,15 @@ export async function createRequestArtifact(options) {
390
398
  await writeFile(initiatedPath, '', 'utf8');
391
399
  }
392
400
  }
393
- return { role: options.role, requestId: options.requestId, sessionId, path, content, applied: true };
401
+ return {
402
+ role: options.role,
403
+ requestId: options.requestId,
404
+ sessionId,
405
+ path,
406
+ content,
407
+ applied: true,
408
+ ...(options.callerId !== undefined ? { callerId: options.callerId } : {})
409
+ };
394
410
  }
395
411
  function extractMetadata(markdown) {
396
412
  let state = 'unknown';
@@ -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 {};