peaks-cli 1.4.2 → 2.0.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 (169) hide show
  1. package/.claude-plugin/marketplace.json +51 -0
  2. package/CHANGELOG.md +238 -0
  3. package/README-en.md +226 -0
  4. package/README.md +152 -122
  5. package/dist/src/cli/commands/agent-commands.d.ts +20 -0
  6. package/dist/src/cli/commands/agent-commands.js +48 -0
  7. package/dist/src/cli/commands/audit-commands.d.ts +18 -0
  8. package/dist/src/cli/commands/audit-commands.js +138 -0
  9. package/dist/src/cli/commands/classify-classify-commands.d.ts +19 -0
  10. package/dist/src/cli/commands/classify-classify-commands.js +151 -0
  11. package/dist/src/cli/commands/code-review-commands.d.ts +34 -0
  12. package/dist/src/cli/commands/code-review-commands.js +83 -0
  13. package/dist/src/cli/commands/config-commands.js +90 -0
  14. package/dist/src/cli/commands/context-commands.d.ts +21 -0
  15. package/dist/src/cli/commands/context-commands.js +167 -0
  16. package/dist/src/cli/commands/core-artifact-commands.js +60 -2
  17. package/dist/src/cli/commands/hook-handle.js +50 -0
  18. package/dist/src/cli/commands/loop-commands.d.ts +21 -0
  19. package/dist/src/cli/commands/loop-commands.js +128 -0
  20. package/dist/src/cli/commands/openspec-commands.js +37 -0
  21. package/dist/src/cli/commands/preferences-commands.d.ts +2 -0
  22. package/dist/src/cli/commands/preferences-commands.js +147 -0
  23. package/dist/src/cli/commands/skill-conformance-commands.d.ts +9 -0
  24. package/dist/src/cli/commands/skill-conformance-commands.js +39 -0
  25. package/dist/src/cli/commands/understand-commands.js +34 -0
  26. package/dist/src/cli/commands/upgrade-commands.d.ts +23 -0
  27. package/dist/src/cli/commands/upgrade-commands.js +57 -0
  28. package/dist/src/cli/commands/workflow-commands.js +70 -0
  29. package/dist/src/cli/commands/workspace-commands.js +86 -0
  30. package/dist/src/cli/program.js +30 -0
  31. package/dist/src/services/agent/ecc-agent-service.d.ts +47 -0
  32. package/dist/src/services/agent/ecc-agent-service.js +143 -0
  33. package/dist/src/services/artifacts/request-artifact-service.js +14 -0
  34. package/dist/src/services/audit/backing-detector.d.ts +24 -0
  35. package/dist/src/services/audit/backing-detector.js +59 -0
  36. package/dist/src/services/audit/classifier.d.ts +38 -0
  37. package/dist/src/services/audit/classifier.js +127 -0
  38. package/dist/src/services/audit/enforcers/active-skill-resolver.d.ts +29 -0
  39. package/dist/src/services/audit/enforcers/active-skill-resolver.js +71 -0
  40. package/dist/src/services/audit/enforcers/design-draft-confirm.d.ts +25 -0
  41. package/dist/src/services/audit/enforcers/design-draft-confirm.js +54 -0
  42. package/dist/src/services/audit/enforcers/lint-audit-regression.d.ts +21 -0
  43. package/dist/src/services/audit/enforcers/lint-audit-regression.js +86 -0
  44. package/dist/src/services/audit/enforcers/lint-catalog-governance.d.ts +27 -0
  45. package/dist/src/services/audit/enforcers/lint-catalog-governance.js +38 -0
  46. package/dist/src/services/audit/enforcers/lint-cli-back.d.ts +16 -0
  47. package/dist/src/services/audit/enforcers/lint-cli-back.js +35 -0
  48. package/dist/src/services/audit/enforcers/lint-output-style.d.ts +11 -0
  49. package/dist/src/services/audit/enforcers/lint-output-style.js +94 -0
  50. package/dist/src/services/audit/enforcers/lint-reference-integrity.d.ts +6 -0
  51. package/dist/src/services/audit/enforcers/lint-reference-integrity.js +83 -0
  52. package/dist/src/services/audit/enforcers/lint-reference-shape.d.ts +30 -0
  53. package/dist/src/services/audit/enforcers/lint-reference-shape.js +272 -0
  54. package/dist/src/services/audit/enforcers/lint-style.d.ts +49 -0
  55. package/dist/src/services/audit/enforcers/lint-style.js +173 -0
  56. package/dist/src/services/audit/enforcers/lint-workflow-shape.d.ts +5 -0
  57. package/dist/src/services/audit/enforcers/lint-workflow-shape.js +141 -0
  58. package/dist/src/services/audit/enforcers/login-gate.d.ts +23 -0
  59. package/dist/src/services/audit/enforcers/login-gate.js +40 -0
  60. package/dist/src/services/audit/enforcers/mock-placement.d.ts +25 -0
  61. package/dist/src/services/audit/enforcers/mock-placement.js +48 -0
  62. package/dist/src/services/audit/enforcers/no-root-pollution.d.ts +21 -0
  63. package/dist/src/services/audit/enforcers/no-root-pollution.js +56 -0
  64. package/dist/src/services/audit/enforcers/pre-rd-scan.d.ts +22 -0
  65. package/dist/src/services/audit/enforcers/pre-rd-scan.js +23 -0
  66. package/dist/src/services/audit/enforcers/prototype-fidelity.d.ts +25 -0
  67. package/dist/src/services/audit/enforcers/prototype-fidelity.js +75 -0
  68. package/dist/src/services/audit/enforcers/resume-detection.d.ts +21 -0
  69. package/dist/src/services/audit/enforcers/resume-detection.js +52 -0
  70. package/dist/src/services/audit/enforcers/solo-code-ban.d.ts +23 -0
  71. package/dist/src/services/audit/enforcers/solo-code-ban.js +27 -0
  72. package/dist/src/services/audit/enforcers/sub-agent-sid.d.ts +25 -0
  73. package/dist/src/services/audit/enforcers/sub-agent-sid.js +63 -0
  74. package/dist/src/services/audit/enforcers/tech-doc-presence.d.ts +28 -0
  75. package/dist/src/services/audit/enforcers/tech-doc-presence.js +35 -0
  76. package/dist/src/services/audit/red-line-catalog-p2-a.d.ts +21 -0
  77. package/dist/src/services/audit/red-line-catalog-p2-a.js +233 -0
  78. package/dist/src/services/audit/red-line-catalog-p2-b.d.ts +19 -0
  79. package/dist/src/services/audit/red-line-catalog-p2-b.js +225 -0
  80. package/dist/src/services/audit/red-line-catalog.d.ts +51 -0
  81. package/dist/src/services/audit/red-line-catalog.js +210 -0
  82. package/dist/src/services/audit/red-lines-service.d.ts +23 -0
  83. package/dist/src/services/audit/red-lines-service.js +486 -0
  84. package/dist/src/services/audit/scanners/openspec-scanner.d.ts +15 -0
  85. package/dist/src/services/audit/scanners/openspec-scanner.js +55 -0
  86. package/dist/src/services/audit/scanners/rules-tree-scanner.d.ts +16 -0
  87. package/dist/src/services/audit/scanners/rules-tree-scanner.js +56 -0
  88. package/dist/src/services/audit/scanners/skills-tree-scanner.d.ts +17 -0
  89. package/dist/src/services/audit/scanners/skills-tree-scanner.js +46 -0
  90. package/dist/src/services/audit/static-service.d.ts +57 -0
  91. package/dist/src/services/audit/static-service.js +125 -0
  92. package/dist/src/services/audit/types.d.ts +69 -0
  93. package/dist/src/services/audit/types.js +13 -0
  94. package/dist/src/services/classify/classify-service.d.ts +42 -0
  95. package/dist/src/services/classify/classify-service.js +122 -0
  96. package/dist/src/services/classify/classify-types.d.ts +79 -0
  97. package/dist/src/services/classify/classify-types.js +90 -0
  98. package/dist/src/services/code-review/ocr-service.d.ts +129 -0
  99. package/dist/src/services/code-review/ocr-service.js +362 -0
  100. package/dist/src/services/config/config-migration.d.ts +32 -0
  101. package/dist/src/services/config/config-migration.js +92 -0
  102. package/dist/src/services/config/config-restore.d.ts +10 -0
  103. package/dist/src/services/config/config-restore.js +47 -0
  104. package/dist/src/services/config/config-rollback.d.ts +13 -0
  105. package/dist/src/services/config/config-rollback.js +26 -0
  106. package/dist/src/services/config/config-service.d.ts +35 -2
  107. package/dist/src/services/config/config-service.js +81 -0
  108. package/dist/src/services/config/config-types.d.ts +58 -0
  109. package/dist/src/services/config/config-types.js +6 -0
  110. package/dist/src/services/doctor/doctor-service.js +96 -0
  111. package/dist/src/services/ide/adapters/hermes-adapter.d.ts +21 -0
  112. package/dist/src/services/ide/adapters/hermes-adapter.js +51 -0
  113. package/dist/src/services/ide/adapters/openclaw-adapter.d.ts +14 -0
  114. package/dist/src/services/ide/adapters/openclaw-adapter.js +42 -0
  115. package/dist/src/services/ide/ide-registry.js +7 -0
  116. package/dist/src/services/ide/ide-types.d.ts +1 -1
  117. package/dist/src/services/openspec/openspec-propose-from-doctor-service.d.ts +31 -0
  118. package/dist/src/services/openspec/openspec-propose-from-doctor-service.js +95 -0
  119. package/dist/src/services/preferences/preferences-service.d.ts +6 -0
  120. package/dist/src/services/preferences/preferences-service.js +43 -0
  121. package/dist/src/services/preferences/preferences-types.d.ts +90 -0
  122. package/dist/src/services/preferences/preferences-types.js +38 -0
  123. package/dist/src/services/skills/skill-conformance-service.d.ts +40 -0
  124. package/dist/src/services/skills/skill-conformance-service.js +136 -0
  125. package/dist/src/services/skills/skill-runbook-service.js +44 -10
  126. package/dist/src/services/skills/sync-service.d.ts +43 -0
  127. package/dist/src/services/skills/sync-service.js +99 -0
  128. package/dist/src/services/slice/slice-check-service.js +166 -13
  129. package/dist/src/services/slice/slice-check-types.d.ts +1 -1
  130. package/dist/src/services/standards/migrate-claude-rules-service.d.ts +19 -0
  131. package/dist/src/services/standards/migrate-claude-rules-service.js +193 -0
  132. package/dist/src/services/understand/understand-scan-service.js +15 -2
  133. package/dist/src/services/understand/understand-types.d.ts +26 -0
  134. package/dist/src/services/upgrade/1x-detector-service.d.ts +7 -0
  135. package/dist/src/services/upgrade/1x-detector-service.js +94 -0
  136. package/dist/src/services/upgrade/gitignore-migrate-service.d.ts +56 -0
  137. package/dist/src/services/upgrade/gitignore-migrate-service.js +170 -0
  138. package/dist/src/services/upgrade/upgrade-service.d.ts +47 -0
  139. package/dist/src/services/upgrade/upgrade-service.js +381 -0
  140. package/dist/src/services/workspace/sid-naming-guard.d.ts +14 -0
  141. package/dist/src/services/workspace/sid-naming-guard.js +31 -0
  142. package/dist/src/services/workspace/workspace-archive-service.d.ts +19 -0
  143. package/dist/src/services/workspace/workspace-archive-service.js +32 -0
  144. package/dist/src/services/workspace/workspace-clean-service.d.ts +41 -0
  145. package/dist/src/services/workspace/workspace-clean-service.js +86 -0
  146. package/dist/src/services/workspace/workspace-state-service.d.ts +7 -0
  147. package/dist/src/services/workspace/workspace-state-service.js +43 -0
  148. package/dist/src/shared/change-id.js +4 -1
  149. package/dist/src/shared/version.d.ts +1 -1
  150. package/dist/src/shared/version.js +1 -1
  151. package/package.json +8 -2
  152. package/schemas/doctor-report.schema.json +1 -1
  153. package/scripts/install-skills.mjs +296 -12
  154. package/skills/peaks-doctor/SKILL.md +59 -0
  155. package/skills/peaks-doctor/references/doctor-check-catalog.md +31 -0
  156. package/skills/peaks-doctor/references/from-doctor-flow.md +64 -0
  157. package/skills/peaks-doctor/test_prompts.json +17 -0
  158. package/skills/peaks-ide/SKILL.md +2 -0
  159. package/skills/peaks-qa/SKILL.md +9 -7
  160. package/skills/peaks-qa/references/artifact-per-request.md +19 -5
  161. package/skills/peaks-qa/references/qa-perf-test-plan.md +6 -6
  162. package/skills/peaks-qa/references/qa-runbook.md +1 -1
  163. package/skills/peaks-rd/SKILL.md +25 -10
  164. package/skills/peaks-rd/references/ocr-integration.md +214 -0
  165. package/skills/peaks-rd/references/rd-fanout-contracts.md +70 -0
  166. package/skills/peaks-rd/references/rd-runbook.md +1 -1
  167. package/skills/peaks-solo/SKILL.md +10 -4
  168. package/skills/peaks-solo/references/step-0-55-1x-detection.md +82 -0
  169. package/skills/peaks-solo/references/workflow-gates-and-types.md +9 -0
@@ -2,6 +2,10 @@ import { addJsonOption, printResult } from '../cli-helpers.js';
2
2
  import { detectIdeFromContext, parseAdapterStdin, parseClaudeShapeStdin, pluckObject, pluckString } from '../../services/ide/hook-translator.js';
3
3
  import { buildCanonicalHook, formatDecisionResponse } from '../../services/ide/hook-protocol.js';
4
4
  import { getAdapter } from '../../services/ide/ide-registry.js';
5
+ import { evaluateSoloCodeBan } from '../../services/audit/enforcers/solo-code-ban.js';
6
+ import { isRootWrite } from '../../services/audit/enforcers/no-root-pollution.js';
7
+ import { checkLoginGate } from '../../services/audit/enforcers/login-gate.js';
8
+ import { resolveActiveSkillForCaller } from '../../services/audit/enforcers/active-skill-resolver.js';
5
9
  import { fail, ok } from '../../shared/result.js';
6
10
  /**
7
11
  * Read the hook payload. `PEAKS_HOOK_STDIN` is a test seam (same convention as
@@ -77,6 +81,21 @@ export function registerHookHandleCommand(program, io) {
77
81
  // Dispatch by toolName. For slice #1+, we only handle Bash. Task tool sub-agent dispatch goes through `peaks sub-agent dispatch` (slice #009) and does not need a hook entry.
78
82
  // Other tools: allow (no-op; future events will be added here).
79
83
  if (hook.toolName === 'Bash' && typeof fallbackCommand === 'string' && fallbackCommand.trim().length > 0) {
84
+ // L2.1 P0 #1: solo-code-ban. Deny `git commit` / `git apply` from peaks-* skills
85
+ // BEFORE the SOP gate runs. The active skill is read from the per-caller
86
+ // active-skill file (see active-skill-resolver.ts).
87
+ const activeSkill = resolveActiveSkillForCaller(projectRoot);
88
+ if (activeSkill.skill !== null) {
89
+ const soloDecision = evaluateSoloCodeBan({ skill: activeSkill.skill, command: fallbackCommand });
90
+ if (soloDecision.denied) {
91
+ const formatted = formatDecisionResponse(ide, 'deny', soloDecision.reason);
92
+ io.stdout(formatted.stdout);
93
+ if (options.json === true) {
94
+ io.stderr(JSON.stringify(ok('hook.handle', { ide, tool: hook.toolName, decision: 'deny', reason: soloDecision.reason, enforcer: 'solo-code-ban' })));
95
+ }
96
+ return;
97
+ }
98
+ }
80
99
  // Lazy import to avoid circular: peaks gate enforce logic
81
100
  const { enforceBashCommand } = await import('../../services/sop/gate-enforce-service.js');
82
101
  const decision = await enforceBashCommand(projectRoot, fallbackCommand);
@@ -89,6 +108,37 @@ export function registerHookHandleCommand(program, io) {
89
108
  return;
90
109
  }
91
110
  }
111
+ // L2.2 P1 #1: login-gate. After solo-code-ban + gate-enforce pass,
112
+ // flag destructive patterns (uninstall, force-push, --force, --hard,
113
+ // rm -rf) so the LLM gets a soft warning (still allow). The user
114
+ // gets the warning via the warn channel; the command proceeds.
115
+ if (hook.toolName === 'Bash' && typeof fallbackCommand === 'string') {
116
+ const gate = checkLoginGate({ command: fallbackCommand });
117
+ if (gate.destructive) {
118
+ io.stderr(`warning: login-gate: destructive command detected (pattern: ${gate.matchedPattern}). Confirm with the user before proceeding.`);
119
+ }
120
+ }
121
+ // L2.1 P0 #2: no-root-pollution. Deny Write/Edit to files outside the
122
+ // root allowlist. file_path is read from toolInput.file_path (Claude
123
+ // and most IDEs use the same shape). When the field is missing or the
124
+ // path is not at depth 1, the enforcer allows (no-op).
125
+ if (hook.toolName === 'Write' || hook.toolName === 'Edit' || hook.toolName === 'MultiEdit' || hook.toolName === 'Create') {
126
+ const filePath = pluckString(parsed, ['tool_input', 'file_path'])
127
+ ?? pluckString(parsed, ['toolInput', 'file_path'])
128
+ ?? pluckString(parsed, ['tool_input', 'path'])
129
+ ?? pluckString(parsed, ['toolInput', 'path']);
130
+ if (typeof filePath === 'string' && filePath.trim().length > 0) {
131
+ const rootCheck = isRootWrite({ projectRoot, filePath });
132
+ if (!rootCheck.allowed) {
133
+ const formatted = formatDecisionResponse(ide, 'deny', rootCheck.denyReason);
134
+ io.stdout(formatted.stdout);
135
+ if (options.json === true) {
136
+ io.stderr(JSON.stringify(ok('hook.handle', { ide, tool: hook.toolName, decision: 'deny', reason: rootCheck.denyReason, enforcer: 'no-root-pollution' })));
137
+ }
138
+ return;
139
+ }
140
+ }
141
+ }
92
142
  const allow = formatDecisionResponse(ide, 'allow');
93
143
  io.stdout(allow.stdout);
94
144
  if (options.json === true) {
@@ -0,0 +1,21 @@
1
+ /**
2
+ * peaks loop * CLI (Slice #14) — L4 Agent Loop Integration.
3
+ *
4
+ * Per docs/superpowers/specs/2026-06-11-peaks-cli-l1-l2-l3-redesign.md §5.4
5
+ * Slice #14, ships 5 sub-features:
6
+ *
7
+ * 14.1 peaks loop distill — extract patterns from past sessions
8
+ * (delegates to peaks memory extract)
9
+ * 14.2 peaks loop preflight — pre-run sanity checks (placeholder)
10
+ * 14.3 peaks loop detect-pattern — find repeating patterns (placeholder)
11
+ * 14.4 peaks loop check-consistency — verify state consistency (placeholder)
12
+ * 14.5 peaks goal compose — autonomous goal composition (placeholder;
13
+ * requires IDE adapter goalCommand capability
14
+ * per Slice #0.7)
15
+ *
16
+ * The 4 placeholders emit a clear nextActions list; the LLM-side UX
17
+ * layer composes the actual runtime today.
18
+ */
19
+ import { Command } from 'commander';
20
+ import { type ProgramIO } from '../cli-helpers.js';
21
+ export declare function registerLoopCommands(program: Command, io: ProgramIO): void;
@@ -0,0 +1,128 @@
1
+ /**
2
+ * peaks loop * CLI (Slice #14) — L4 Agent Loop Integration.
3
+ *
4
+ * Per docs/superpowers/specs/2026-06-11-peaks-cli-l1-l2-l3-redesign.md §5.4
5
+ * Slice #14, ships 5 sub-features:
6
+ *
7
+ * 14.1 peaks loop distill — extract patterns from past sessions
8
+ * (delegates to peaks memory extract)
9
+ * 14.2 peaks loop preflight — pre-run sanity checks (placeholder)
10
+ * 14.3 peaks loop detect-pattern — find repeating patterns (placeholder)
11
+ * 14.4 peaks loop check-consistency — verify state consistency (placeholder)
12
+ * 14.5 peaks goal compose — autonomous goal composition (placeholder;
13
+ * requires IDE adapter goalCommand capability
14
+ * per Slice #0.7)
15
+ *
16
+ * The 4 placeholders emit a clear nextActions list; the LLM-side UX
17
+ * layer composes the actual runtime today.
18
+ */
19
+ import { addJsonOption, getErrorMessage, printResult } from '../cli-helpers.js';
20
+ import { fail, ok } from '../../shared/result.js';
21
+ export function registerLoopCommands(program, io) {
22
+ // 14.5 peaks goal compose — registered as a TOP-LEVEL command (not under
23
+ // `peaks loop`) because IDE adapters expose it as `goalCommand`.
24
+ // The sub-agent dispatch path consumes it; the slice 0.7 hermes +
25
+ // openclaw adapters will thread it through.
26
+ addJsonOption(program
27
+ .command('goal')
28
+ .description('14.5: compose an autonomous goal (returns the goal envelope that the LLM-side UX layer feeds to peaks sub-agent dispatch)')
29
+ .requiredOption('--project <path>', 'target project root')
30
+ .requiredOption('--goal <text>', 'the high-level goal to compose')).action(async (options) => {
31
+ try {
32
+ printResult(io, ok('goal.compose', {
33
+ project: options.project,
34
+ goal: options.goal,
35
+ status: 'placeholder',
36
+ nextSteps: [
37
+ 'The composed goal is consumed by peaks sub-agent dispatch.',
38
+ 'The hermes + openclaw IDE adapters (Slice #0.7) surface this as a goalCommand.',
39
+ ],
40
+ }, [], [
41
+ 'goal.compose is a thin facade; the LLM-side UX layer decomposes the goal into sub-agent tasks.',
42
+ ]), options.json);
43
+ }
44
+ catch (error) {
45
+ printResult(io, fail('goal.compose', 'GOAL_COMPOSE_FAILED', getErrorMessage(error), { project: options.project, goal: options.goal }, ['Verify the project path and --goal value']), options.json);
46
+ process.exitCode = 1;
47
+ }
48
+ });
49
+ // peaks loop *
50
+ const loop = program.command('loop').description('Slice #14: L4 Agent Loop sub-features (distill / preflight / detect-pattern / check-consistency)');
51
+ // 14.1 distill
52
+ addJsonOption(loop.command('distill')
53
+ .description('14.1: distill patterns from past sessions into .peaks/memory/ (delegates to peaks memory extract)')
54
+ .requiredOption('--project <path>', 'target project root')
55
+ .option('--apply', 'write extracted memories to .peaks/memory/ (default: dry-run preview)', false)).action(async (options) => {
56
+ try {
57
+ const apply = options.apply === true;
58
+ // Delegate to the existing peaks memory extract CLI via dynamic
59
+ // import (avoids circular); the LLM-side UX layer composes the
60
+ // two commands.
61
+ const { execFileSync } = await import('node:child_process');
62
+ const args = ['memory', 'extract', '--project', options.project];
63
+ if (apply)
64
+ args.push('--apply');
65
+ const stdout = execFileSync('node', ['bin/peaks.js', ...args], {
66
+ cwd: options.project,
67
+ stdio: ['ignore', 'pipe', 'ignore'],
68
+ }).toString('utf-8');
69
+ printResult(io, ok('loop.distill', {
70
+ project: options.project,
71
+ apply,
72
+ delegateStdout: stdout.slice(0, 200),
73
+ }, [], [
74
+ apply ? 'peaks memory extract --apply was invoked' : 'peaks memory extract dry-run was invoked',
75
+ 'A future slice will inline the memory extract (not via execFileSync).',
76
+ ]), options.json);
77
+ }
78
+ catch (error) {
79
+ printResult(io, fail('loop.distill', 'LOOP_DISTILL_FAILED', getErrorMessage(error), { project: options.project }, ['Verify the project path']), options.json);
80
+ process.exitCode = 1;
81
+ }
82
+ });
83
+ // 14.2 preflight
84
+ addJsonOption(loop.command('preflight')
85
+ .description('14.2: pre-run sanity checks (placeholder; future slice runs peaks doctor + peaks audit before each loop iter)')
86
+ .requiredOption('--project <path>', 'target project root')).action(async (options) => {
87
+ printResult(io, ok('loop.preflight', {
88
+ project: options.project,
89
+ status: 'placeholder',
90
+ nextSteps: [
91
+ 'For each L4 loop iteration, call peaks doctor + peaks audit to surface regressions.',
92
+ 'A future slice will inline the preflight checks (not just placeholder).',
93
+ ],
94
+ }, [], [
95
+ 'loop.preflight is a thin facade; the LLM-side UX layer composes peaks doctor + peaks audit.',
96
+ ]), options.json);
97
+ });
98
+ // 14.3 detect-pattern
99
+ addJsonOption(loop.command('detect-pattern')
100
+ .description('14.3: detect repeating patterns across past sessions (placeholder; future slice uses peaks retrospective search)')
101
+ .requiredOption('--project <path>', 'target project root')).action(async (options) => {
102
+ printResult(io, ok('loop.detect-pattern', {
103
+ project: options.project,
104
+ status: 'placeholder',
105
+ nextSteps: [
106
+ 'Run peaks retrospective search --limit 50 to surface high-frequency patterns.',
107
+ 'A future slice will rank by frequency + LLM confidence.',
108
+ ],
109
+ }, [], [
110
+ 'loop.detect-pattern is a thin facade; the LLM-side UX layer composes peaks retrospective search.',
111
+ ]), options.json);
112
+ });
113
+ // 14.4 check-consistency
114
+ addJsonOption(loop.command('check-consistency')
115
+ .description('14.4: verify state consistency (placeholder; future slice compares .peaks/_runtime across sessions)')
116
+ .requiredOption('--project <path>', 'target project root')).action(async (options) => {
117
+ printResult(io, ok('loop.check-consistency', {
118
+ project: options.project,
119
+ status: 'placeholder',
120
+ nextSteps: [
121
+ 'Compare .peaks/_runtime/<sid>/session.json across recent sessions for drift.',
122
+ 'A future slice will report drift with severity (warn / fail).',
123
+ ],
124
+ }, [], [
125
+ 'loop.check-consistency is a thin facade; the LLM-side UX layer composes the drift scan.',
126
+ ]), options.json);
127
+ });
128
+ }
@@ -5,6 +5,8 @@ import { renderOpenSpecChange } from '../../services/openspec/openspec-render-se
5
5
  import { validateOpenSpecChange } from '../../services/openspec/openspec-validate-service.js';
6
6
  import { archiveOpenSpecChange } from '../../services/openspec/openspec-archive-service.js';
7
7
  import { executeOpenSpecInit } from '../../services/openspec/openspec-init-service.js';
8
+ import { proposeFromDoctor } from '../../services/openspec/openspec-propose-from-doctor-service.js';
9
+ import { runDoctor } from '../../services/doctor/doctor-service.js';
8
10
  import { fail, ok } from '../../shared/result.js';
9
11
  import { addJsonOption, getErrorMessage, printResult } from '../cli-helpers.js';
10
12
  function resolveScanOptions(project) {
@@ -197,4 +199,39 @@ export function registerOpenSpecCommands(program, io) {
197
199
  process.exitCode = 1;
198
200
  }
199
201
  });
202
+ addJsonOption(openspec
203
+ .command('from-doctor')
204
+ .description('Slice L3.3: generate an OpenSpec change draft (proposal.md) from a peaks doctor finding')
205
+ .requiredOption('--project <path>', 'target project root')
206
+ .requiredOption('--check-id <id>', 'the doctor check id to draft from (e.g. L3:l3-memory-health)')).action(async (options) => {
207
+ try {
208
+ const report = await runDoctor();
209
+ const finding = report.checks.find((c) => c.id === options.checkId);
210
+ if (finding === undefined) {
211
+ printResult(io, fail('openspec.from-doctor', 'CHECK_NOT_FOUND', `No doctor check with id "${options.checkId}"`, { availableIds: report.checks.map((c) => c.id) }, ['Run peaks doctor --json to list available check ids']), options.json);
212
+ process.exitCode = 1;
213
+ return;
214
+ }
215
+ if (finding.ok) {
216
+ printResult(io, fail('openspec.from-doctor', 'CHECK_ALREADY_PASSING', `Doctor check "${options.checkId}" is already passing; nothing to draft`, { checkId: options.checkId }, ['Pick a failing check from peaks doctor --json']), options.json);
217
+ process.exitCode = 1;
218
+ return;
219
+ }
220
+ const doctorFinding = {
221
+ id: finding.id,
222
+ rule: finding.rule ?? finding.id,
223
+ detail: finding.message,
224
+ severity: 'fail',
225
+ };
226
+ const result = proposeFromDoctor({ projectRoot: options.project, finding: doctorFinding });
227
+ printResult(io, ok('openspec.from-doctor', result, [], [
228
+ `draft proposal written to ${result.proposalPath}`,
229
+ 'Review + edit the draft, then run `peaks openspec validate <id>`',
230
+ ]), options.json);
231
+ }
232
+ catch (error) {
233
+ printResult(io, fail('openspec.from-doctor', 'OPENSPEC_FROM_DOCTOR_FAILED', getErrorMessage(error), { projectRoot: options.project, checkId: options.checkId }, ['Verify the project path and the check id']), options.json);
234
+ process.exitCode = 1;
235
+ }
236
+ });
200
237
  }
@@ -0,0 +1,2 @@
1
+ import type { Command } from 'commander';
2
+ export declare function registerPreferencesCommands(program: Command): void;
@@ -0,0 +1,147 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { dirname } from 'node:path';
3
+ import { loadPreferences, preferencesPath, savePreferences, } from '../../services/preferences/preferences-service.js';
4
+ const ALLOWED_KEYS = new Set([
5
+ 'economyMode',
6
+ 'swarmMode',
7
+ 'uaPrompt',
8
+ 'agentShieldPrompt',
9
+ 'classifyConservatism',
10
+ 'classifyRules',
11
+ 'headroom',
12
+ 'swarmSpeculative',
13
+ 'loopAutonomousEnabled',
14
+ ]);
15
+ export function registerPreferencesCommands(program) {
16
+ const prefs = program
17
+ .command('preferences')
18
+ .description('Manage project-local preferences (`.peaks/preferences.json`)');
19
+ prefs
20
+ .command('get')
21
+ .description('Get a preference value (from override or default)')
22
+ .requiredOption('--key <key>', 'preference key')
23
+ .option('--project <path>', 'project root', process.cwd())
24
+ .option('--json', 'JSON envelope output')
25
+ .action((opts) => {
26
+ try {
27
+ if (!ALLOWED_KEYS.has(opts.key)) {
28
+ throw new Error(`PREFERENCES_KEY_UNKNOWN: ${opts.key}`);
29
+ }
30
+ const all = loadPreferences(opts.project);
31
+ const value = all[opts.key];
32
+ const filePath = preferencesPath(opts.project);
33
+ // Spec bug fix: use the top-level existsSync import (the spec's
34
+ // `await import('node:fs')` would not compile inside a non-async action).
35
+ // Source is 'override' only when the key is explicitly present in
36
+ // the on-disk file (not merely when the file exists, since reset()
37
+ // may leave the file with other keys still overridden).
38
+ let source = 'default';
39
+ if (existsSync(filePath)) {
40
+ try {
41
+ const raw = JSON.parse(readFileSync(filePath, 'utf8'));
42
+ if (Object.prototype.hasOwnProperty.call(raw, opts.key)) {
43
+ source = 'override';
44
+ }
45
+ }
46
+ catch {
47
+ // Corrupt file: report the merged value but fall back to 'default'
48
+ // source to avoid claiming an override that cannot be parsed.
49
+ source = 'default';
50
+ }
51
+ }
52
+ // Stable ordering: source is computed, value reflects merged state.
53
+ const envelope = {
54
+ ok: true,
55
+ data: { key: opts.key, value, source },
56
+ };
57
+ process.stdout.write(JSON.stringify(envelope, null, 2) + '\n');
58
+ }
59
+ catch (err) {
60
+ process.stderr.write(err.message + '\n');
61
+ process.exit(1);
62
+ }
63
+ });
64
+ prefs
65
+ .command('set')
66
+ .description('Override a preference value (writes to .peaks/preferences.json)')
67
+ .requiredOption('--key <key>', 'preference key')
68
+ .requiredOption('--value <value>', 'value (parsed as JSON, or string if not JSON)')
69
+ .option('--project <path>', 'project root', process.cwd())
70
+ .option('--json', 'JSON envelope output')
71
+ .action((opts) => {
72
+ try {
73
+ if (!ALLOWED_KEYS.has(opts.key)) {
74
+ throw new Error(`PREFERENCES_KEY_UNKNOWN: ${opts.key}`);
75
+ }
76
+ let parsed = opts.value;
77
+ try {
78
+ parsed = JSON.parse(opts.value);
79
+ }
80
+ catch {
81
+ // Not valid JSON — keep the raw string.
82
+ }
83
+ const merged = savePreferences(opts.project, {
84
+ [opts.key]: parsed,
85
+ });
86
+ const envelope = {
87
+ ok: true,
88
+ data: {
89
+ key: opts.key,
90
+ value: merged[opts.key],
91
+ },
92
+ };
93
+ process.stdout.write(JSON.stringify(envelope, null, 2) + '\n');
94
+ }
95
+ catch (err) {
96
+ process.stderr.write(err.message + '\n');
97
+ process.exit(1);
98
+ }
99
+ });
100
+ prefs
101
+ .command('reset')
102
+ .description('Remove the override for a key (falls back to default)')
103
+ .requiredOption('--key <key>', 'preference key')
104
+ .option('--project <path>', 'project root', process.cwd())
105
+ .option('--json', 'JSON envelope output')
106
+ .action((opts) => {
107
+ try {
108
+ if (!ALLOWED_KEYS.has(opts.key)) {
109
+ throw new Error(`PREFERENCES_KEY_UNKNOWN: ${opts.key}`);
110
+ }
111
+ const filePath = preferencesPath(opts.project);
112
+ // Read raw on-disk state (not the merged view) so removing a key
113
+ // truly removes the override, instead of savePreferences re-inserting
114
+ // the default value. If no file exists, there is nothing to reset.
115
+ if (!existsSync(filePath)) {
116
+ const envelope = {
117
+ ok: true,
118
+ data: { key: opts.key, removed: false, reason: 'no-override-file' },
119
+ };
120
+ process.stdout.write(JSON.stringify(envelope) + '\n');
121
+ return;
122
+ }
123
+ let raw;
124
+ try {
125
+ raw = JSON.parse(readFileSync(filePath, 'utf8'));
126
+ }
127
+ catch (err) {
128
+ throw new Error(`PREFERENCES_JSON_INVALID: failed to parse ${filePath}: ${err.message}`);
129
+ }
130
+ const hadKey = Object.prototype.hasOwnProperty.call(raw, opts.key);
131
+ if (hadKey) {
132
+ delete raw[opts.key];
133
+ mkdirSync(dirname(filePath), { recursive: true });
134
+ writeFileSync(filePath, JSON.stringify(raw, null, 2) + '\n', 'utf8');
135
+ }
136
+ const envelope = {
137
+ ok: true,
138
+ data: { key: opts.key, removed: hadKey },
139
+ };
140
+ process.stdout.write(JSON.stringify(envelope) + '\n');
141
+ }
142
+ catch (err) {
143
+ process.stderr.write(err.message + '\n');
144
+ process.exit(1);
145
+ }
146
+ });
147
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * peaks skills audit-conformance CLI (Slice #12) — runs the
3
+ * skill-conformance-service against all 12 peaks-* SKILL.md files and
4
+ * reports the 5 standard checks (frontmatter, CLI-back, loadStrategy,
5
+ * 800-line cap, outputStyle).
6
+ */
7
+ import { Command } from 'commander';
8
+ import { type ProgramIO } from '../cli-helpers.js';
9
+ export declare function registerSkillConformanceCommands(program: Command, io: ProgramIO): void;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * peaks skills audit-conformance CLI (Slice #12) — runs the
3
+ * skill-conformance-service against all 12 peaks-* SKILL.md files and
4
+ * reports the 5 standard checks (frontmatter, CLI-back, loadStrategy,
5
+ * 800-line cap, outputStyle).
6
+ */
7
+ import { auditSkillConformance } from '../../services/skills/skill-conformance-service.js';
8
+ import { getErrorMessage, printResult } from '../cli-helpers.js';
9
+ import { fail, ok } from '../../shared/result.js';
10
+ export function registerSkillConformanceCommands(program, io) {
11
+ program
12
+ .command('skills:audit-conformance')
13
+ .description('Slice #12: audit all 12 peaks-* SKILL.md against the 5 alignment standards')
14
+ .requiredOption('--project <path>', 'target project root')
15
+ .option('--json', 'print machine-readable JSON envelope')
16
+ .action(async (options) => {
17
+ try {
18
+ const report = auditSkillConformance({ projectRoot: options.project });
19
+ const nextActions = [];
20
+ if (report.failed > 0) {
21
+ nextActions.push(`${report.failed} hard failure(s); fix before shipping.`);
22
+ for (const c of report.checks.filter((c) => c.level === 'fail')) {
23
+ nextActions.push(` - ${c.skill}: ${c.id} — ${c.message}`);
24
+ }
25
+ }
26
+ if (report.warned > 0) {
27
+ nextActions.push(`${report.warned} advisory warning(s); see envelope.checks for details.`);
28
+ }
29
+ if (report.failed === 0 && report.warned === 0) {
30
+ nextActions.push('All 13 skills pass the 5 alignment standards.');
31
+ }
32
+ printResult(io, ok('skills.audit-conformance', report, [], nextActions), options.json);
33
+ }
34
+ catch (error) {
35
+ printResult(io, fail('skills.audit-conformance', 'AUDIT_CONFORMANCE_FAILED', getErrorMessage(error), { projectRoot: options.project }, ['Verify the project path']), options.json);
36
+ process.exitCode = 1;
37
+ }
38
+ });
39
+ }
@@ -75,4 +75,38 @@ export function registerUnderstandCommands(program, io) {
75
75
  process.exitCode = 1;
76
76
  }
77
77
  });
78
+ // L3.1: opt-in UX subcommand. Returns the AskUserQuestion payload that
79
+ // the LLM-side UX layer (peaks-solo / peaks-ide) should surface when
80
+ // uaPrompt === 'unset' and UA is absent. When uaPrompt is skip-this-session
81
+ // or skip-forever, returns a no-op envelope (caller does not prompt).
82
+ addJsonOption(understand
83
+ .command('opt-in')
84
+ .description('Returns the UA opt-in prompt payload (Slice L3.1) when uaPrompt is unset; no-op otherwise')
85
+ .requiredOption('--project <path>', 'target project root')).action(async (options) => {
86
+ try {
87
+ const report = await scanUnderstandAnything({ projectRoot: options.project });
88
+ const uaPrompt = report.uaPrompt ?? 'unset';
89
+ if (report.exists || uaPrompt !== 'unset') {
90
+ // No prompt needed: UA is installed OR user already decided.
91
+ printResult(io, ok('understand.opt-in', { promptNeeded: false, uaPrompt, uaInstalled: report.exists }), options.json);
92
+ return;
93
+ }
94
+ const prompt = {
95
+ version: 1,
96
+ tool: 'ua-opt-in',
97
+ artifactDir: report.artifactDir,
98
+ reason: 'ua-artifact-missing',
99
+ options: [
100
+ { id: 'install', label: 'Install UA in Claude Code', description: INSTALL_HINT },
101
+ { id: 'fallback-this-session', label: 'Use codegraph fallback this session', description: 'Skip UA this run; do not write preferences.json' },
102
+ { id: 'fallback-forever', label: 'Use codegraph fallback forever', description: 'Write preferences.json:uaPrompt=skip-forever; suppress future prompts' }
103
+ ]
104
+ };
105
+ printResult(io, ok('understand.opt-in', { promptNeeded: true, uaPrompt, uaInstalled: false, prompt }), options.json);
106
+ }
107
+ catch (error) {
108
+ printResult(io, fail('understand.opt-in', 'UNDERSTAND_OPTIN_FAILED', getErrorMessage(error), { projectRoot: options.project }, ['Check the project path']), options.json);
109
+ process.exitCode = 1;
110
+ }
111
+ });
78
112
  }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * peaks upgrade * CLI surface — Slice: 1.x → 2.0 umbrella +
3
+ * Slice 3: --detect-1x flag.
4
+ *
5
+ * Per the "one-key completion" + "minimal-user-operation" tenets
6
+ * (2026-06-11), the user's typical upgrade path is
7
+ * `npm i -g peaks-cli@2.0` (the postinstall does the upgrade).
8
+ *
9
+ * The `peaks upgrade --to 2.0` CLI is the manual fallback for
10
+ * when the postinstall is skipped (e.g. CI uses
11
+ * `--ignore-scripts`). The umbrella orchestrates 7 sub-commands:
12
+ * config-migrate / standards-migrate / memory-extract /
13
+ * hooks-install / skill-sync / audit-verify + write-upgrade-record.
14
+ *
15
+ * The `--detect-1x` flag (added in slice 3) is a read-only
16
+ * probe that the peaks-solo skill calls to gate the
17
+ * AskUserQuestion that prompts the 1.x → 2.0 upgrade. The
18
+ * probe returns the JSON envelope from the
19
+ * 1x-detector-service; it does NOT modify any files.
20
+ */
21
+ import { Command } from 'commander';
22
+ import { type ProgramIO } from '../cli-helpers.js';
23
+ export declare function registerUpgradeCommands(program: Command, io: ProgramIO): void;
@@ -0,0 +1,57 @@
1
+ import { runUpgrade } from '../../services/upgrade/upgrade-service.js';
2
+ import { detect1xProjectState } from '../../services/upgrade/1x-detector-service.js';
3
+ import { addJsonOption, getErrorMessage, printResult } from '../cli-helpers.js';
4
+ import { fail, ok } from '../../shared/result.js';
5
+ export function registerUpgradeCommands(program, io) {
6
+ addJsonOption(program
7
+ .command('upgrade')
8
+ .description('Upgrade a peaks-cli 1.x project to 2.0. Umbrella that orquestrates 7 sub-commands: config-migrate / standards-migrate / memory-extract / hooks-install / skill-sync / audit-verify + write-upgrade-record. Per the "one-key completion" tenet, prefer letting `npm i -g peaks-cli@2.0` postinstall run this for you. Use `--detect-1x` for a read-only probe (no file writes) that the peaks-solo skill uses to gate the 1.x → 2.0 AskUserQuestion.')
9
+ .option('--to <version>', 'target version (only "2.0" supported)', '2.0')
10
+ .option('--project <path>', 'project root to upgrade (default: cwd)')
11
+ .option('--auto', 'non-interactive: accept soft-fail on any sub-step (used by the postinstall hook)')
12
+ .option('--detect-1x', 'read-only probe: returns the 1.x state as JSON (no file writes); consumed by peaks-solo Step 0.55 to gate the AskUserQuestion')).action((options) => {
13
+ const projectRoot = options.project ?? process.cwd();
14
+ // Branch 1: --detect-1x (read-only probe)
15
+ if (options.detect1x === true) {
16
+ try {
17
+ const state = detect1xProjectState(projectRoot);
18
+ const nextActions = [];
19
+ if (state.isOneX) {
20
+ nextActions.push(`Detected 1.x state. peaks-solo Step 0.55 should present an AskUserQuestion to invoke \`peaks upgrade --to 2.0 --auto --project ${state.projectRoot ?? projectRoot}\`.`);
21
+ }
22
+ else {
23
+ nextActions.push('No 1.x state detected. Proceed with the standing 2.0 layout.');
24
+ }
25
+ const envelope = ok('upgrade.detect-1x', state, [], nextActions);
26
+ printResult(io, envelope, options.json);
27
+ }
28
+ catch (error) {
29
+ const message = getErrorMessage(error);
30
+ printResult(io, fail('upgrade.detect-1x', 'DETECT_1X_FAILED', message, { isOneX: false, signals: [], projectRoot: null, configPath: null }, [message]), options.json);
31
+ process.exitCode = 1;
32
+ }
33
+ return;
34
+ }
35
+ // Branch 2: the umbrella (existing behavior)
36
+ try {
37
+ const result = runUpgrade({ projectRoot, auto: options.auto === true });
38
+ const nextActions = [...result.nextActions];
39
+ if (result.failedCount > 0) {
40
+ nextActions.unshift(`${result.failedCount} sub-step(s) failed. Re-run \`peaks upgrade --to 2.0\` to retry.`);
41
+ }
42
+ if (result.upgradeRecordPath !== null) {
43
+ nextActions.push(`Upgrade record written: ${result.upgradeRecordPath}`);
44
+ }
45
+ const envelope = ok('upgrade', result, [...result.warnings], nextActions);
46
+ printResult(io, envelope, options.json);
47
+ if (result.failedCount > 0) {
48
+ process.exitCode = 1;
49
+ }
50
+ }
51
+ catch (error) {
52
+ const message = getErrorMessage(error);
53
+ printResult(io, fail('upgrade', 'UPGRADE_FAILED', message, { applied: false }, [message]), options.json);
54
+ process.exitCode = 1;
55
+ }
56
+ });
57
+ }