peaks-cli 1.3.8 → 1.4.0

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 (130) hide show
  1. package/dist/src/cli/commands/core-artifact-commands.js +27 -0
  2. package/dist/src/cli/commands/project-commands.js +58 -1
  3. package/dist/src/cli/commands/request-commands.js +93 -3
  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/commands/skill-scope-commands.d.ts +49 -0
  7. package/dist/src/cli/commands/skill-scope-commands.js +305 -0
  8. package/dist/src/cli/commands/workflow-commands.js +1 -1
  9. package/dist/src/cli/commands/workflow-plan-commands.d.ts +39 -0
  10. package/dist/src/cli/commands/workflow-plan-commands.js +163 -0
  11. package/dist/src/cli/program.js +8 -0
  12. package/dist/src/services/doctor/doctor-service.d.ts +40 -0
  13. package/dist/src/services/doctor/doctor-service.js +160 -0
  14. package/dist/src/services/hooks/presence-marker-detector.d.ts +16 -0
  15. package/dist/src/services/hooks/presence-marker-detector.js +105 -0
  16. package/dist/src/services/memory/project-memory-service.d.ts +19 -0
  17. package/dist/src/services/memory/project-memory-service.js +33 -0
  18. package/dist/src/services/retrospective/migrate-from-md.d.ts +37 -0
  19. package/dist/src/services/retrospective/migrate-from-md.js +528 -0
  20. package/dist/src/services/retrospective/retrospective-index.d.ts +37 -0
  21. package/dist/src/services/retrospective/retrospective-index.js +110 -0
  22. package/dist/src/services/retrospective/retrospective-show.d.ts +40 -0
  23. package/dist/src/services/retrospective/retrospective-show.js +109 -0
  24. package/dist/src/services/skill-scope/adapters/_stub-helper.d.ts +39 -0
  25. package/dist/src/services/skill-scope/adapters/_stub-helper.js +98 -0
  26. package/dist/src/services/skill-scope/adapters/claude-code.d.ts +59 -0
  27. package/dist/src/services/skill-scope/adapters/claude-code.js +304 -0
  28. package/dist/src/services/skill-scope/adapters/codex.d.ts +2 -0
  29. package/dist/src/services/skill-scope/adapters/codex.js +12 -0
  30. package/dist/src/services/skill-scope/adapters/cursor.d.ts +2 -0
  31. package/dist/src/services/skill-scope/adapters/cursor.js +13 -0
  32. package/dist/src/services/skill-scope/adapters/qoder.d.ts +2 -0
  33. package/dist/src/services/skill-scope/adapters/qoder.js +13 -0
  34. package/dist/src/services/skill-scope/adapters/tongyi.d.ts +2 -0
  35. package/dist/src/services/skill-scope/adapters/tongyi.js +13 -0
  36. package/dist/src/services/skill-scope/adapters/trae.d.ts +2 -0
  37. package/dist/src/services/skill-scope/adapters/trae.js +12 -0
  38. package/dist/src/services/skill-scope/detect.d.ts +75 -0
  39. package/dist/src/services/skill-scope/detect.js +480 -0
  40. package/dist/src/services/skill-scope/registry.d.ts +41 -0
  41. package/dist/src/services/skill-scope/registry.js +83 -0
  42. package/dist/src/services/skill-scope/source-of-truth.d.ts +44 -0
  43. package/dist/src/services/skill-scope/source-of-truth.js +118 -0
  44. package/dist/src/services/skill-scope/types.d.ts +176 -0
  45. package/dist/src/services/skill-scope/types.js +74 -0
  46. package/dist/src/services/standards/migrate-service.d.ts +63 -0
  47. package/dist/src/services/standards/migrate-service.js +193 -0
  48. package/dist/src/services/standards/project-standards-service.js +1 -23
  49. package/dist/src/services/workflow/artifact-paths.d.ts +59 -0
  50. package/dist/src/services/workflow/artifact-paths.js +127 -0
  51. package/dist/src/services/workflow/pipeline-verify-service.d.ts +6 -0
  52. package/dist/src/services/workflow/pipeline-verify-service.js +49 -4
  53. package/dist/src/services/workflow/plan-reader.d.ts +29 -0
  54. package/dist/src/services/workflow/plan-reader.js +158 -0
  55. package/dist/src/services/workflow/plan-refresher.d.ts +32 -0
  56. package/dist/src/services/workflow/plan-refresher.js +353 -0
  57. package/dist/src/services/workflow/plan-trigger-detector.d.ts +55 -0
  58. package/dist/src/services/workflow/plan-trigger-detector.js +142 -0
  59. package/dist/src/shared/format-md-compact.d.ts +32 -0
  60. package/dist/src/shared/format-md-compact.js +297 -0
  61. package/dist/src/shared/stale-policy.d.ts +67 -0
  62. package/dist/src/shared/stale-policy.js +85 -0
  63. package/dist/src/shared/version.d.ts +1 -1
  64. package/dist/src/shared/version.js +1 -1
  65. package/package.json +3 -2
  66. package/schemas/doctor-report.schema.json +2 -2
  67. package/skills/peaks-qa/SKILL.md +103 -507
  68. package/skills/peaks-qa/references/artifact-per-request.md +7 -79
  69. package/skills/peaks-qa/references/browser-validation-contracts.md +51 -0
  70. package/skills/peaks-qa/references/codegraph-regression-focus.md +5 -0
  71. package/skills/peaks-qa/references/external-capability-guidance.md +9 -0
  72. package/skills/peaks-qa/references/qa-compact-handoff.md +3 -0
  73. package/skills/peaks-qa/references/qa-context-governance.md +24 -0
  74. package/skills/peaks-qa/references/qa-fanout-contract.md +8 -0
  75. package/skills/peaks-qa/references/qa-gstack-integration.md +7 -0
  76. package/skills/peaks-qa/references/qa-local-artifacts.md +3 -0
  77. package/skills/peaks-qa/references/qa-matt-pocock-integration.md +9 -0
  78. package/skills/peaks-qa/references/qa-perf-test-plan.md +67 -0
  79. package/skills/peaks-qa/references/qa-refactor-role.md +3 -0
  80. package/skills/peaks-qa/references/qa-runbook.md +74 -0
  81. package/skills/peaks-qa/references/qa-security-test-plan.md +73 -0
  82. package/skills/peaks-qa/references/qa-skill-presence.md +22 -0
  83. package/skills/peaks-qa/references/qa-standards-preflight.md +8 -0
  84. package/skills/peaks-qa/references/qa-sub-agent-dispatch.md +38 -0
  85. package/skills/peaks-qa/references/qa-transition-gates.md +83 -0
  86. package/skills/peaks-qa/references/requirement-boundary-recheck.md +9 -0
  87. package/skills/peaks-qa/references/test-case-generation.md +27 -0
  88. package/skills/peaks-qa/references/test-report-output.md +14 -0
  89. package/skills/peaks-rd/SKILL.md +85 -612
  90. package/skills/peaks-rd/references/artifact-and-standards-output.md +9 -0
  91. package/skills/peaks-rd/references/artifact-per-request.md +20 -0
  92. package/skills/peaks-rd/references/browser-self-test-contracts.md +29 -0
  93. package/skills/peaks-rd/references/codegraph-project-analysis.md +5 -0
  94. package/skills/peaks-rd/references/compact-handoff.md +3 -0
  95. package/skills/peaks-rd/references/external-references.md +11 -0
  96. package/skills/peaks-rd/references/frontend-project-generation.md +11 -0
  97. package/skills/peaks-rd/references/library-version-awareness.md +30 -0
  98. package/skills/peaks-rd/references/mandatory-perf-baseline.md +42 -0
  99. package/skills/peaks-rd/references/mandatory-tech-doc.md +18 -0
  100. package/skills/peaks-rd/references/matt-pocock-integration.md +11 -0
  101. package/skills/peaks-rd/references/mock-data-placement.md +40 -0
  102. package/skills/peaks-rd/references/parallel-review-fanout.md +81 -0
  103. package/skills/peaks-rd/references/rd-context-governance.md +36 -0
  104. package/skills/peaks-rd/references/rd-gstack-integration.md +16 -0
  105. package/skills/peaks-rd/references/rd-runbook.md +125 -0
  106. package/skills/peaks-rd/references/rd-standards-preflight.md +8 -0
  107. package/skills/peaks-rd/references/rd-sub-agent-dispatch.md +39 -0
  108. package/skills/peaks-rd/references/rd-transition-gates.md +1 -1
  109. package/skills/peaks-rd/references/skill-presence-and-title.md +22 -0
  110. package/skills/peaks-solo/SKILL.md +87 -595
  111. package/skills/peaks-solo/references/anchoring-and-session-info.md +25 -0
  112. package/skills/peaks-solo/references/boundaries.md +21 -0
  113. package/skills/peaks-solo/references/codegraph-orchestration.md +5 -0
  114. package/skills/peaks-solo/references/completion-handoff.md +16 -0
  115. package/skills/peaks-solo/references/context-governance.md +51 -0
  116. package/skills/peaks-solo/references/external-references.md +17 -0
  117. package/skills/peaks-solo/references/frontend-only-mode.md +14 -0
  118. package/skills/peaks-solo/references/gstack-integration.md +7 -0
  119. package/skills/peaks-solo/references/local-artifact-workspace.md +79 -0
  120. package/skills/peaks-solo/references/micro-cycle.md +68 -0
  121. package/skills/peaks-solo/references/mode-selection.md +21 -0
  122. package/skills/peaks-solo/references/openspec-workflow.md +43 -0
  123. package/skills/peaks-solo/references/project-memory-loading.md +17 -0
  124. package/skills/peaks-solo/references/quality-gate-cheatsheet.md +13 -0
  125. package/skills/peaks-solo/references/resume-detection.md +63 -0
  126. package/skills/peaks-solo/references/runbook.md +1 -1
  127. package/skills/peaks-solo/references/skill-presence-and-title.md +31 -0
  128. package/skills/peaks-solo/references/standards-preflight.md +23 -0
  129. package/skills/peaks-solo/references/sub-agent-dispatch.md +46 -0
  130. package/skills/peaks-solo/references/swarm-dispatch-contract.md +56 -0
@@ -0,0 +1,305 @@
1
+ /**
2
+ * `peaks skill scope` CLI surface (slice 025.1).
3
+ *
4
+ * Four subcommands (mutually exclusive):
5
+ * - `--detect` — dry-run; prints the relevance matrix, never touches files.
6
+ * - `--apply` — writes the source-of-truth + IDE-native config.
7
+ * - `--show` — reads the source-of-truth + native config back.
8
+ * - `--reset` — removes the source-of-truth + IDE-native config.
9
+ *
10
+ * Exit code matrix (tech-doc §6.3):
11
+ * 0 success
12
+ * 1 uncaught error
13
+ * 2 invalid usage (missing/incompatible flags)
14
+ * 3 source-of-truth written but adapter returned NOT_SUPPORTED
15
+ * 4 adapter failure other than NOT_SUPPORTED
16
+ */
17
+ import { existsSync } from 'node:fs';
18
+ import { join } from 'node:path';
19
+ import { detectSkillScope, } from '../../services/skill-scope/detect.js';
20
+ import { resolveActiveAdapter, getScopeAdapter } from '../../services/skill-scope/registry.js';
21
+ import { ideCompanionFilePath, readIdeCompanion, readSourceOfTruth, removeIfExists, scopeFilePath, writeSourceOfTruth, } from '../../services/skill-scope/source-of-truth.js';
22
+ import { ALWAYS_RELEVANT_SKILLS } from '../../services/skill-scope/types.js';
23
+ import { fail, getErrorMessage, ok } from '../../shared/result.js';
24
+ import { addJsonOption, printResult } from '../cli-helpers.js';
25
+ const VALID_ACTIONS = ['detect', 'apply', 'show', 'reset'];
26
+ const VALID_IDES = ['claude-code', 'trae', 'codex', 'cursor', 'qoder', 'tongyi-lingma'];
27
+ function isValidIde(value) {
28
+ return VALID_IDES.includes(value);
29
+ }
30
+ /**
31
+ * G6: enforce the peaks-* allowlist. Re-adds any peak-* skill that is
32
+ * missing from the allowlist, and removes any peak-* skill from the
33
+ * denylist. The list is the same one declared in `types.ts`.
34
+ */
35
+ function enforcePeaksAllowlist(allowlist) {
36
+ const set = new Set(allowlist);
37
+ for (const name of ALWAYS_RELEVANT_SKILLS) {
38
+ if (name.startsWith('peaks-'))
39
+ set.add(name);
40
+ }
41
+ return [...set];
42
+ }
43
+ function stripPeaksFromDenylist(denylist) {
44
+ return denylist.filter((name) => !name.startsWith('peaks-'));
45
+ }
46
+ /**
47
+ * Determine the IDE. Caller-supplied `--ide` wins; otherwise the registry
48
+ * probes the project root.
49
+ */
50
+ async function resolveIde(projectRoot, override) {
51
+ if (override !== undefined) {
52
+ if (!isValidIde(override)) {
53
+ throw new Error(`Unknown IDE: ${override}. Valid: ${VALID_IDES.join(', ')}`);
54
+ }
55
+ return { ide: override, isFallback: false };
56
+ }
57
+ const resolved = await resolveActiveAdapter(projectRoot);
58
+ return { ide: resolved.adapter.ide, isFallback: resolved.isFallback };
59
+ }
60
+ /**
61
+ * Stable timestamp (no millisecond jitter) for the `generatedAt` field.
62
+ * `Date.now()` would still be deterministic per-run; we keep the natural
63
+ * one to ensure `generatedAt` matches what the user sees on disk.
64
+ */
65
+ function nowIso() {
66
+ return new Date().toISOString();
67
+ }
68
+ /** Run the --detect subcommand. */
69
+ async function runDetect(input) {
70
+ try {
71
+ const result = await detectSkillScope({ projectRoot: input.project });
72
+ const envelope = ok('skill.scope.detect', result);
73
+ const stdout = input.json === true ? JSON.stringify(envelope, null, 2) : JSON.stringify(result, null, 2);
74
+ return { exitCode: 0, envelope, stdout, stderr: '' };
75
+ }
76
+ catch (error) {
77
+ const envelope = fail('skill.scope.detect', 'DETECT_FAILED', getErrorMessage(error), null);
78
+ return { exitCode: 1, envelope, stdout: '', stderr: envelope.message ?? 'detect failed' };
79
+ }
80
+ }
81
+ /** Build the final ScopeConfig (applies G6 enforcement + override). */
82
+ function buildScopeConfig(args) {
83
+ const strict = args.strict;
84
+ const detected = args.detected;
85
+ // Build allowlist from detected.relevant + (in loose) borderline.
86
+ const allowFromDetect = detected.skills
87
+ .filter((s) => s.relevance === 'relevant' || (!strict && s.relevance === 'borderline'))
88
+ .map((s) => s.name);
89
+ const merged = args.allowOverride !== undefined ? [...args.allowOverride, ...allowFromDetect] : allowFromDetect;
90
+ const enforced = enforcePeaksAllowlist(merged);
91
+ // Denylist: irrelevant skills (strict + loose both), minus anything in allowlist.
92
+ const denyFromDetect = detected.skills
93
+ .filter((s) => s.relevance === 'irrelevant' && !enforced.includes(s.name))
94
+ .map((s) => s.name);
95
+ const finalDeny = stripPeaksFromDenylist(denyFromDetect);
96
+ return {
97
+ generatedAt: nowIso(),
98
+ ide: args.ide,
99
+ strict,
100
+ allowlist: enforced,
101
+ denylist: finalDeny,
102
+ skills: detected.skills,
103
+ signals: detected.projectSignals,
104
+ };
105
+ }
106
+ /** Run the --apply subcommand. */
107
+ async function runApply(input) {
108
+ // 1. Detect the scope.
109
+ const detected = await detectSkillScope({ projectRoot: input.project });
110
+ // --strict wins when both flags are passed. Default is --loose per PRD.
111
+ const isStrict = input.strict === true && input.loose !== true;
112
+ const loose = !isStrict;
113
+ const { ide, isFallback } = await resolveIde(input.project, input.ide);
114
+ const adapter = getScopeAdapter(ide);
115
+ const config = buildScopeConfig({
116
+ ide,
117
+ strict: isStrict,
118
+ detected,
119
+ ...(input.overrideAllowlist !== undefined ? { allowOverride: input.overrideAllowlist } : {}),
120
+ });
121
+ // 2. Write the source-of-truth first (atomic). Test seam: simulate failure.
122
+ let writtenFiles = [];
123
+ let sourceWritten = false;
124
+ try {
125
+ if (input.simulateSourceOfTruthWriteFailure) {
126
+ throw new Error('simulated source-of-truth write failure');
127
+ }
128
+ const file = await writeSourceOfTruth(input.project, config);
129
+ writtenFiles.push(file);
130
+ sourceWritten = true;
131
+ }
132
+ catch (error) {
133
+ const envelope = fail('skill.scope.apply', 'WRITE_FAILED', getErrorMessage(error), { ide, sourceWritten: false }, ['Fix filesystem permissions on the project root and retry']);
134
+ return { exitCode: 4, envelope, stdout: '', stderr: envelope.message ?? 'write failed' };
135
+ }
136
+ // 3. Call the adapter. Stub adapters return notSupported=true; we surface it.
137
+ const adapterInput = {
138
+ allowlist: config.allowlist,
139
+ denylist: config.denylist,
140
+ strict: config.strict,
141
+ projectRoot: input.project,
142
+ sourceConfig: config,
143
+ shadowFallback: input.shadowFallback === true,
144
+ };
145
+ let result;
146
+ try {
147
+ result = await adapter.applyScope(adapterInput);
148
+ }
149
+ catch (error) {
150
+ // Roll back the source-of-truth on adapter failure.
151
+ await removeIfExists(scopeFilePath(input.project));
152
+ const envelope = fail('skill.scope.apply', 'ADAPTER_FAILED', getErrorMessage(error), { ide, sourceWritten: false, writtenFiles: [] }, ['Inspect the adapter error and retry']);
153
+ return { exitCode: 4, envelope, stdout: '', stderr: envelope.message ?? 'adapter failed' };
154
+ }
155
+ // The stub adapter also writes the canonical skills.json — that's
156
+ // already on disk from step 2, so its second write is a no-op update.
157
+ const finalWrittenFiles = [...writtenFiles, ...result.writtenFiles];
158
+ const envelope = ok('skill.scope.apply', {
159
+ ide,
160
+ isFallback,
161
+ strict: isStrict,
162
+ loose,
163
+ allowlist: config.allowlist,
164
+ denylist: config.denylist,
165
+ signals: config.signals,
166
+ writtenFiles: finalWrittenFiles,
167
+ usedShadowStub: result.usedShadowStub,
168
+ notSupported: result.notSupported,
169
+ strippedFromDenylist: result.strippedFromDenylist ?? [],
170
+ error: result.error,
171
+ });
172
+ const stdout = input.json === true ? JSON.stringify(envelope, null, 2) : JSON.stringify(envelope.data, null, 2);
173
+ if (result.notSupported) {
174
+ // Stub adapter: NOT_SUPPORTED → exit 3, write error to stderr.
175
+ const stderr = `${result.error?.code ?? 'NOT_SUPPORTED'}: ${result.error?.message ?? 'not supported'}`;
176
+ return { exitCode: 3, envelope, stdout, stderr };
177
+ }
178
+ return { exitCode: 0, envelope, stdout, stderr: '' };
179
+ }
180
+ /** Run the --show subcommand. */
181
+ async function runShow(input) {
182
+ const source = await readSourceOfTruth(input.project);
183
+ const { ide } = await resolveIde(input.project, input.ide);
184
+ const companionPath = ideCompanionFilePath(input.project, ide);
185
+ const companion = await readIdeCompanion(input.project, ide);
186
+ // For Claude Code, the native config is `.claude/settings.local.json`.
187
+ const nativeSettingsPath = join(input.project, '.claude', 'settings.local.json');
188
+ const nativeExists = existsSync(nativeSettingsPath);
189
+ let native = companion;
190
+ if (nativeExists) {
191
+ try {
192
+ const { readFile } = await import('node:fs/promises');
193
+ native = JSON.parse(await readFile(nativeSettingsPath, 'utf8'));
194
+ }
195
+ catch {
196
+ native = null;
197
+ }
198
+ }
199
+ const data = {
200
+ ide,
201
+ source,
202
+ native,
203
+ nativeSettingsPath: nativeExists ? '.claude/settings.local.json' : null,
204
+ companionPath: existsSync(companionPath) ? companionPath : null,
205
+ };
206
+ const envelope = ok('skill.scope.show', data);
207
+ const stdout = input.json === true ? JSON.stringify(envelope, null, 2) : JSON.stringify(data, null, 2);
208
+ return { exitCode: 0, envelope, stdout, stderr: '' };
209
+ }
210
+ /** Run the --reset subcommand. */
211
+ async function runReset(input) {
212
+ const { ide } = await resolveIde(input.project, input.ide);
213
+ const adapter = getScopeAdapter(ide);
214
+ const resetResult = await adapter.resetScope({ projectRoot: input.project });
215
+ const sourceFile = scopeFilePath(input.project);
216
+ const sourceRemoved = await removeIfExists(sourceFile);
217
+ const allRemoved = [...resetResult.removedFiles, ...(sourceRemoved ? [sourceFile] : [])];
218
+ const envelope = ok('skill.scope.reset', {
219
+ ide,
220
+ removedFiles: allRemoved,
221
+ });
222
+ // Always include the canonical source-of-truth path in the human-readable
223
+ // summary, even if it didn't exist (so the user knows what was targeted).
224
+ const displayFiles = allRemoved.length > 0 ? allRemoved : [sourceFile, join(input.project, '.claude', 'settings.local.json')];
225
+ const summary = `removed: ${displayFiles.join(', ')}`;
226
+ const stdout = input.json === true ? JSON.stringify(envelope, null, 2) : summary;
227
+ return { exitCode: 0, envelope, stdout, stderr: '' };
228
+ }
229
+ /**
230
+ * Programmatic entry point for `peaks skill scope`. Used by the CLI shim
231
+ * AND by the unit tests.
232
+ */
233
+ export async function runSkillScopeCommand(input) {
234
+ if (!VALID_ACTIONS.includes(input.subcommand)) {
235
+ const envelope = fail('skill.scope', 'INVALID_USAGE', `Unknown action: ${input.subcommand}`, null);
236
+ return { exitCode: 2, envelope, stdout: '', stderr: envelope.message ?? 'invalid usage' };
237
+ }
238
+ switch (input.subcommand) {
239
+ case 'detect': return runDetect(input);
240
+ case 'apply': return runApply(input);
241
+ case 'show': return runShow(input);
242
+ case 'reset': return runReset(input);
243
+ }
244
+ }
245
+ /**
246
+ * Register the `peaks skill scope` subcommand on the `skill` command group.
247
+ * Mutually-exclusive flags: exactly one of --detect / --apply / --show / --reset.
248
+ */
249
+ export function registerSkillScopeCommands(program, io) {
250
+ // Find the existing 'skill' subcommand if any.
251
+ let skillCmd = program.commands.find((c) => c.name() === 'skill');
252
+ if (skillCmd === undefined) {
253
+ skillCmd = program.command('skill').description('Manage Peaks skills');
254
+ }
255
+ const scope = skillCmd
256
+ .command('scope')
257
+ .description('Per-project skill scoping: detect, apply, show, reset');
258
+ addJsonOption(scope
259
+ .option('--detect', 'dry-run: print the relevance matrix')
260
+ .option('--apply', 'apply the scope (writes source-of-truth + IDE config)')
261
+ .option('--show', 'show the currently applied scope')
262
+ .option('--reset', 'remove the scope config')
263
+ .option('--project <path>', 'target project root (defaults to cwd)', process.cwd())
264
+ .option('--strict', '--apply: only `relevant` skills in the allowlist')
265
+ .option('--loose', '--apply: `relevant` + `borderline` in the allowlist (default)')
266
+ .option('--ide <name>', 'force a specific IDE adapter (overrides auto-detect)')
267
+ .option('--shadow-fallback', '--apply: Claude Code uses shadow stubs for the denylist')).action(async (options) => {
268
+ const flags = [options.detect, options.apply, options.show, options.reset].filter(Boolean).length;
269
+ if (flags !== 1) {
270
+ const envelope = fail('skill.scope', 'INVALID_USAGE', 'Exactly one of --detect / --apply / --show / --reset is required', null, ['Pass exactly one action flag']);
271
+ printResult(io, envelope, options.json === true);
272
+ process.exitCode = 2;
273
+ return;
274
+ }
275
+ const subcommand = options.detect
276
+ ? 'detect'
277
+ : options.apply
278
+ ? 'apply'
279
+ : options.show
280
+ ? 'show'
281
+ : 'reset';
282
+ const result = await runSkillScopeCommand({
283
+ subcommand,
284
+ project: options.project ?? process.cwd(),
285
+ ...(options.strict !== undefined ? { strict: options.strict } : {}),
286
+ ...(options.loose !== undefined ? { loose: options.loose } : {}),
287
+ ...(options.ide !== undefined ? { ide: options.ide } : {}),
288
+ ...(options.shadowFallback !== undefined ? { shadowFallback: options.shadowFallback } : {}),
289
+ ...(options.json !== undefined ? { json: options.json } : {}),
290
+ });
291
+ if (options.json === true) {
292
+ if (result.envelope !== null)
293
+ printResult(io, result.envelope, true);
294
+ }
295
+ else {
296
+ if (result.stdout.length > 0)
297
+ io.stdout(result.stdout);
298
+ if (result.stderr.length > 0)
299
+ io.stderr(result.stderr);
300
+ }
301
+ if (result.exitCode !== 0) {
302
+ process.exitCode = result.exitCode;
303
+ }
304
+ });
305
+ }
@@ -329,7 +329,7 @@ export function registerWorkflowCommands(program, io) {
329
329
  process.exitCode = exitOk;
330
330
  }
331
331
  catch (error) {
332
- printResult(io, fail('workflow.verify-pipeline', 'VERIFY_FAILED', getErrorMessage(error), {}, ['Check that --project and --rid are correct; --change-id is optional (resolved from the artifact otherwise)']), options.json);
332
+ printResult(io, fail('workflow.verify-pipeline', 'VERIFY_FAILED', getErrorMessage(error), { acceptedForm: 'none', gateC: 'fail' }, ['Check that --project and --rid are correct; --change-id is optional (resolved from the artifact otherwise)']), options.json);
333
333
  process.exitCode = 1;
334
334
  }
335
335
  });
@@ -0,0 +1,39 @@
1
+ /**
2
+ * `peaks workflow plan <read|refresh|detect-trigger>` — slice 025 CLI.
3
+ *
4
+ * Three subcommands under the existing `peaks workflow` verb:
5
+ * - `read <security|perf> --project <repo> --json`
6
+ * - `refresh <security|perf> --project <repo> [--apply] --json`
7
+ * - `detect-trigger --project <repo> --rid <rid> [--refresh] --json`
8
+ *
9
+ * CLI justification (per dev-preference rules):
10
+ * - `read` (2) JSON-gated — slice workflow reads plan hash.
11
+ * - `refresh` (3) destructive write needs explicit `--apply`.
12
+ * - `detect-trigger` (2) JSON-gated — slice workflow needs the verdict.
13
+ */
14
+ import { Command } from 'commander';
15
+ import { type ProgramIO } from '../cli-helpers.js';
16
+ declare function runPlanRead(io: ProgramIO, options: {
17
+ type: string;
18
+ project?: string;
19
+ sessionId?: string;
20
+ json?: boolean;
21
+ }): void;
22
+ declare function runPlanRefresh(io: ProgramIO, options: {
23
+ type: string;
24
+ project?: string;
25
+ sessionId?: string;
26
+ apply?: boolean;
27
+ json?: boolean;
28
+ }): void;
29
+ declare function runPlanDetectTrigger(io: ProgramIO, options: {
30
+ project?: string;
31
+ rid?: string;
32
+ sessionId?: string;
33
+ refresh?: boolean;
34
+ json?: boolean;
35
+ }): void;
36
+ export declare function registerWorkflowPlanCommands(program: Command, io: ProgramIO): void;
37
+ export { runPlanRead as _runPlanRead };
38
+ export { runPlanRefresh as _runPlanRefresh };
39
+ export { runPlanDetectTrigger as _runPlanDetectTrigger };
@@ -0,0 +1,163 @@
1
+ import { fail, getErrorMessage } from '../../shared/result.js';
2
+ import { addJsonOption, printResult } from '../cli-helpers.js';
3
+ import { readPlan } from '../../services/workflow/plan-reader.js';
4
+ import { refreshPlan } from '../../services/workflow/plan-refresher.js';
5
+ import { detectTrigger } from '../../services/workflow/plan-trigger-detector.js';
6
+ import { getSessionId } from '../../services/session/session-manager.js';
7
+ import { findProjectRoot } from '../../services/config/config-safety.js';
8
+ const VALID_TYPES = ['security', 'perf'];
9
+ // F-1 (slice 025 security): reject session ids that look like path
10
+ // traversal payloads. Canonical pattern is YYYY-MM-DD-<slug>.
11
+ const SESSION_ID_PATTERN = /^\d{4}-\d{2}-\d{2}-[a-z][a-z0-9-]*[a-z0-9]$/;
12
+ // F-1 (slice 025 security): reject rids that contain path separators,
13
+ // null bytes, or traversal sequences.
14
+ const REQUEST_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
15
+ function isPlanType(value) {
16
+ return VALID_TYPES.includes(value);
17
+ }
18
+ function isValidSessionId(value) {
19
+ return SESSION_ID_PATTERN.test(value);
20
+ }
21
+ function isValidRequestId(value) {
22
+ return REQUEST_ID_PATTERN.test(value);
23
+ }
24
+ function resolveSessionId(io, command, projectRoot, explicit, asJson) {
25
+ if (explicit !== undefined && explicit.length > 0) {
26
+ if (!isValidSessionId(explicit)) {
27
+ printResult(io, fail(command, 'INVALID_SESSION_ID', 'session id must match YYYY-MM-DD-slug pattern', { sessionId: explicit }, ['Use --session-id <YYYY-MM-DD-slug>']), asJson === true);
28
+ process.exitCode = 1;
29
+ return null;
30
+ }
31
+ return explicit;
32
+ }
33
+ const sid = getSessionId(projectRoot);
34
+ if (sid === null || sid === undefined) {
35
+ printResult(io, fail(command, 'NO_ACTIVE_SESSION', 'No active session — pass --session-id explicitly or run peaks workspace init', { projectRoot }, ['Run peaks workspace init or pass --session-id <YYYY-MM-DD-slug>']), asJson === true);
36
+ process.exitCode = 1;
37
+ return null;
38
+ }
39
+ // Defensive: even active-session resolution must satisfy the pattern.
40
+ if (!isValidSessionId(sid)) {
41
+ printResult(io, fail(command, 'INVALID_SESSION_ID', 'session id must match YYYY-MM-DD-slug pattern', { sessionId: sid }, ['Use --session-id <YYYY-MM-DD-slug>']), asJson === true);
42
+ process.exitCode = 1;
43
+ return null;
44
+ }
45
+ return sid;
46
+ }
47
+ function resolveProjectRoot(projectArg) {
48
+ if (projectArg === undefined || projectArg === '') {
49
+ return findProjectRoot(process.cwd()) ?? process.cwd();
50
+ }
51
+ return projectArg;
52
+ }
53
+ function runPlanRead(io, options) {
54
+ if (!isPlanType(options.type)) {
55
+ printResult(io, fail('workflow.plan.read', 'INVALID_TYPE', `Unsupported plan type: ${options.type}`, { supportedTypes: VALID_TYPES }, ['Use --type security or --type perf']), options.json === true);
56
+ process.exitCode = 1;
57
+ return;
58
+ }
59
+ const projectRoot = resolveProjectRoot(options.project);
60
+ const sessionId = resolveSessionId(io, 'workflow.plan.read', projectRoot, options.sessionId, options.json);
61
+ if (sessionId === null)
62
+ return;
63
+ try {
64
+ const result = readPlan({ type: options.type, project: projectRoot, sessionId });
65
+ printResult(io, result, options.json === true);
66
+ }
67
+ catch (error) {
68
+ printResult(io, fail('workflow.plan.read', 'READ_FAILED', getErrorMessage(error), null, ['Check that --project is a valid repo root with a peaks session']), options.json === true);
69
+ process.exitCode = 1;
70
+ }
71
+ }
72
+ function runPlanRefresh(io, options) {
73
+ if (!isPlanType(options.type)) {
74
+ printResult(io, fail('workflow.plan.refresh', 'INVALID_TYPE', `Unsupported plan type: ${options.type}`, { supportedTypes: VALID_TYPES }, ['Use --type security or --type perf']), options.json === true);
75
+ process.exitCode = 1;
76
+ return;
77
+ }
78
+ const projectRoot = resolveProjectRoot(options.project);
79
+ const sessionId = resolveSessionId(io, 'workflow.plan.refresh', projectRoot, options.sessionId, options.json);
80
+ if (sessionId === null)
81
+ return;
82
+ try {
83
+ const result = refreshPlan({
84
+ type: options.type,
85
+ project: projectRoot,
86
+ sessionId,
87
+ apply: options.apply === true
88
+ });
89
+ printResult(io, result, options.json === true);
90
+ }
91
+ catch (error) {
92
+ printResult(io, fail('workflow.plan.refresh', 'REFRESH_FAILED', getErrorMessage(error), null, ['Check that --project is a valid repo root and the session exists']), options.json === true);
93
+ process.exitCode = 1;
94
+ }
95
+ }
96
+ function runPlanDetectTrigger(io, options) {
97
+ if (options.rid === undefined || options.rid === '') {
98
+ printResult(io, fail('workflow.plan.detect-trigger', 'MISSING_RID', 'Missing --rid', null, ['Pass --rid <request-id>']), options.json === true);
99
+ process.exitCode = 1;
100
+ return;
101
+ }
102
+ if (!isValidRequestId(options.rid)) {
103
+ printResult(io, fail('workflow.plan.detect-trigger', 'INVALID_RID', 'request id must match [A-Za-z0-9][A-Za-z0-9._-]*', { rid: options.rid }, ['Pass --rid <alphanumeric.request-id>']), options.json === true);
104
+ process.exitCode = 1;
105
+ return;
106
+ }
107
+ const projectRoot = resolveProjectRoot(options.project);
108
+ const sessionId = resolveSessionId(io, 'workflow.plan.detect-trigger', projectRoot, options.sessionId, options.json);
109
+ if (sessionId === null)
110
+ return;
111
+ try {
112
+ const result = detectTrigger({
113
+ project: projectRoot,
114
+ rid: options.rid,
115
+ sessionId,
116
+ ...(options.refresh === true ? { manualOverride: true } : {})
117
+ });
118
+ printResult(io, result, options.json === true);
119
+ }
120
+ catch (error) {
121
+ printResult(io, fail('workflow.plan.detect-trigger', 'DETECT_FAILED', getErrorMessage(error), null, ['Check that --project is a valid repo root and --rid is set']), options.json === true);
122
+ process.exitCode = 1;
123
+ }
124
+ }
125
+ export function registerWorkflowPlanCommands(program, io) {
126
+ let workflowCmd = program.commands.find((c) => c.name() === 'workflow');
127
+ if (workflowCmd === undefined) {
128
+ workflowCmd = program.command('workflow').description('Plan workflow routing dry-run graphs');
129
+ }
130
+ const plan = workflowCmd
131
+ .command('plan')
132
+ .description('Read, refresh, or detect-trigger for security / perf plans (slice 025)');
133
+ addJsonOption(plan
134
+ .command('read')
135
+ .description('Read the project-level plan envelope (exists, path, hash, refreshedAt)')
136
+ .requiredOption('--type <type>', 'plan type: security or perf')
137
+ .option('--project <path>', 'project root', process.cwd())
138
+ .option('--session-id <sid>', 'session id (defaults to the active session)')).action((options) => {
139
+ runPlanRead(io, options);
140
+ });
141
+ addJsonOption(plan
142
+ .command('refresh')
143
+ .description('Regenerate the plan (deterministic, idempotent; --apply to write)')
144
+ .requiredOption('--type <type>', 'plan type: security or perf')
145
+ .option('--project <path>', 'project root', process.cwd())
146
+ .option('--session-id <sid>', 'session id (defaults to the active session)')
147
+ .option('--apply', 'write the plan to disk (default is dry-run preview)')).action((options) => {
148
+ runPlanRefresh(io, options);
149
+ });
150
+ addJsonOption(plan
151
+ .command('detect-trigger')
152
+ .description('Detect whether a plan refresh is warranted for the slice diff')
153
+ .requiredOption('--rid <rid>', 'request identifier')
154
+ .option('--project <path>', 'project root', process.cwd())
155
+ .option('--session-id <sid>', 'session id (defaults to the active session)')
156
+ .option('--refresh', 'force triggered=true (manual override)')).action((options) => {
157
+ runPlanDetectTrigger(io, options);
158
+ });
159
+ }
160
+ // Re-export for tests that need a programmatic entry point.
161
+ export { runPlanRead as _runPlanRead };
162
+ export { runPlanRefresh as _runPlanRefresh };
163
+ export { runPlanDetectTrigger as _runPlanDetectTrigger };
@@ -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';
@@ -26,6 +27,8 @@ import { registerHooksCommands } from './commands/hooks-commands.js';
26
27
  import { registerStatusLineCommands } from './commands/statusline-commands.js';
27
28
  import { registerUnderstandCommands } from './commands/understand-commands.js';
28
29
  import { registerWorkspaceCommands } from './commands/workspace-commands.js';
30
+ import { registerSkillScopeCommands } from './commands/skill-scope-commands.js';
31
+ import { registerWorkflowPlanCommands } from './commands/workflow-plan-commands.js';
29
32
  export { printResult } from './cli-helpers.js';
30
33
  export function createProgram(io = { stdout: (text) => console.log(text), stderr: (text) => console.error(text) }) {
31
34
  const program = new Command();
@@ -90,6 +93,7 @@ Run peaks (no arguments) for a quickstart. You likely want one of:
90
93
  registerPerfCommands(program, io);
91
94
  registerProjectCommands(program, io);
92
95
  registerRequestCommands(program, io);
96
+ registerRetrospectiveCommands(program, io);
93
97
  registerScanCommands(program, io);
94
98
  registerShadcnCommands(program, io);
95
99
  registerSliceCommands(program, io);
@@ -105,5 +109,9 @@ Run peaks (no arguments) for a quickstart. You likely want one of:
105
109
  registerStatusLineCommands(program, io);
106
110
  registerUnderstandCommands(program, io);
107
111
  registerWorkspaceCommands(program, io);
112
+ // Slice 025: peaks skill scope — per-project multi-IDE skill scoping.
113
+ registerSkillScopeCommands(program, io);
114
+ // Slice 025: peaks workflow plan — security/perf plan/result split CLI.
115
+ registerWorkflowPlanCommands(program, io);
108
116
  return program;
109
117
  }
@@ -44,6 +44,43 @@ export type WorkspaceLayoutInspection = {
44
44
  perChangeIdDirs?: string[];
45
45
  };
46
46
  export type WorkspaceLayoutProbe = () => WorkspaceLayoutInspection;
47
+ /**
48
+ * 2026-06-10 — `gateguard-fact-force` (a third-party PreToolUse hook,
49
+ * NOT peaks-cli) fires on Edit / Write and demands a 4-fact questionnaire
50
+ * before allowing the edit. When the LLM is in a peaks-qa flow and tries
51
+ * to update `.peaks/_runtime/<sid>/qa/requests/*.md` via the Edit/Write
52
+ * tool, the hook demands facts that are inapplicable to QA envelope
53
+ * templates (no importers, no public API, no data files, user
54
+ * instruction already in the conversation context). The check detects
55
+ * this hook in the user's global and project `.claude/settings.json` and
56
+ * warns when no `.peaks/**` skip is configured. The probe is injected so
57
+ * tests do not depend on the real `~/.claude/settings.json` state.
58
+ */
59
+ export type GateguardHookLocation = {
60
+ /** Source file the hook was discovered in (`global` or `project .claude/settings.json`). */
61
+ source: 'global' | 'project';
62
+ /** Resolved absolute path to the source file (for the message). */
63
+ sourcePath: string;
64
+ /** The PreToolUse entry that contains a gateguard hook command. */
65
+ entry: {
66
+ matcher?: string;
67
+ hooks: ReadonlyArray<{
68
+ type?: string;
69
+ command?: string;
70
+ }>;
71
+ };
72
+ };
73
+ export type GateguardProbeResult = {
74
+ /** Absolute path to `~/.claude/settings.json` (or null when the probe could not resolve it). */
75
+ globalSettingsPath: string | null;
76
+ /** Parsed global settings payload (or null when missing / unreadable / malformed). */
77
+ globalSettings: unknown;
78
+ /** Absolute path to the project `.claude/settings.json` (or null when the project root is not in a peaks project). */
79
+ projectSettingsPath: string | null;
80
+ /** Parsed project settings payload (or null when missing / unreadable / malformed). */
81
+ projectSettings: unknown;
82
+ };
83
+ export type GateguardProbe = () => GateguardProbeResult;
47
84
  export type DoctorOptions = {
48
85
  schemasBaseDir?: string;
49
86
  skillsBaseDir?: string;
@@ -59,6 +96,8 @@ export type DoctorOptions = {
59
96
  distVersionProbe?: DistVersionProbe;
60
97
  /** Injected for the build:workspace-layout-canonical check (defaults to inspectWorkspaceLayout on disk). */
61
98
  workspaceLayoutProbe?: WorkspaceLayoutProbe;
99
+ /** Injected for the integration:gateguard-peaks-conflict check (defaults to defaultGateguardProbe on disk). */
100
+ gateguardProbe?: GateguardProbe;
62
101
  };
63
102
  /**
64
103
  * Pure helper extracted from `defaultWorkspaceInitializedProbe` so tests can
@@ -99,4 +138,5 @@ export declare function inspectWorkspaceLayout(opts: {
99
138
  dotfileScanner?: (root: string) => string[];
100
139
  perChangeIdScanner?: (root: string) => string[];
101
140
  }): WorkspaceLayoutInspection;
141
+ export declare function collectGateguardEntries(probe: GateguardProbeResult): GateguardHookLocation[];
102
142
  export declare function runDoctor(options?: DoctorOptions): Promise<DoctorReport>;