peaks-cli 1.4.1 → 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 (219) 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 +142 -165
  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 +81 -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/memory-commands.d.ts +13 -0
  21. package/dist/src/cli/commands/memory-commands.js +60 -0
  22. package/dist/src/cli/commands/openspec-commands.js +37 -0
  23. package/dist/src/cli/commands/preferences-commands.d.ts +2 -0
  24. package/dist/src/cli/commands/preferences-commands.js +147 -0
  25. package/dist/src/cli/commands/retrospective-commands.d.ts +9 -0
  26. package/dist/src/cli/commands/retrospective-commands.js +58 -0
  27. package/dist/src/cli/commands/skill-conformance-commands.d.ts +9 -0
  28. package/dist/src/cli/commands/skill-conformance-commands.js +39 -0
  29. package/dist/src/cli/commands/understand-commands.js +34 -0
  30. package/dist/src/cli/commands/upgrade-commands.d.ts +23 -0
  31. package/dist/src/cli/commands/upgrade-commands.js +57 -0
  32. package/dist/src/cli/commands/workflow-commands.js +70 -0
  33. package/dist/src/cli/commands/workspace-commands.js +86 -0
  34. package/dist/src/cli/program.js +46 -22
  35. package/dist/src/services/agent/ecc-agent-service.d.ts +47 -0
  36. package/dist/src/services/agent/ecc-agent-service.js +143 -0
  37. package/dist/src/services/artifacts/request-artifact-service.js +14 -0
  38. package/dist/src/services/audit/backing-detector.d.ts +24 -0
  39. package/dist/src/services/audit/backing-detector.js +59 -0
  40. package/dist/src/services/audit/classifier.d.ts +38 -0
  41. package/dist/src/services/audit/classifier.js +127 -0
  42. package/dist/src/services/audit/enforcers/active-skill-resolver.d.ts +29 -0
  43. package/dist/src/services/audit/enforcers/active-skill-resolver.js +71 -0
  44. package/dist/src/services/audit/enforcers/design-draft-confirm.d.ts +25 -0
  45. package/dist/src/services/audit/enforcers/design-draft-confirm.js +54 -0
  46. package/dist/src/services/audit/enforcers/lint-audit-regression.d.ts +21 -0
  47. package/dist/src/services/audit/enforcers/lint-audit-regression.js +86 -0
  48. package/dist/src/services/audit/enforcers/lint-catalog-governance.d.ts +27 -0
  49. package/dist/src/services/audit/enforcers/lint-catalog-governance.js +38 -0
  50. package/dist/src/services/audit/enforcers/lint-cli-back.d.ts +16 -0
  51. package/dist/src/services/audit/enforcers/lint-cli-back.js +35 -0
  52. package/dist/src/services/audit/enforcers/lint-output-style.d.ts +11 -0
  53. package/dist/src/services/audit/enforcers/lint-output-style.js +94 -0
  54. package/dist/src/services/audit/enforcers/lint-reference-integrity.d.ts +6 -0
  55. package/dist/src/services/audit/enforcers/lint-reference-integrity.js +83 -0
  56. package/dist/src/services/audit/enforcers/lint-reference-shape.d.ts +30 -0
  57. package/dist/src/services/audit/enforcers/lint-reference-shape.js +272 -0
  58. package/dist/src/services/audit/enforcers/lint-style.d.ts +49 -0
  59. package/dist/src/services/audit/enforcers/lint-style.js +173 -0
  60. package/dist/src/services/audit/enforcers/lint-workflow-shape.d.ts +5 -0
  61. package/dist/src/services/audit/enforcers/lint-workflow-shape.js +141 -0
  62. package/dist/src/services/audit/enforcers/login-gate.d.ts +23 -0
  63. package/dist/src/services/audit/enforcers/login-gate.js +40 -0
  64. package/dist/src/services/audit/enforcers/mock-placement.d.ts +25 -0
  65. package/dist/src/services/audit/enforcers/mock-placement.js +48 -0
  66. package/dist/src/services/audit/enforcers/no-root-pollution.d.ts +21 -0
  67. package/dist/src/services/audit/enforcers/no-root-pollution.js +56 -0
  68. package/dist/src/services/audit/enforcers/pre-rd-scan.d.ts +22 -0
  69. package/dist/src/services/audit/enforcers/pre-rd-scan.js +23 -0
  70. package/dist/src/services/audit/enforcers/prototype-fidelity.d.ts +25 -0
  71. package/dist/src/services/audit/enforcers/prototype-fidelity.js +75 -0
  72. package/dist/src/services/audit/enforcers/resume-detection.d.ts +21 -0
  73. package/dist/src/services/audit/enforcers/resume-detection.js +52 -0
  74. package/dist/src/services/audit/enforcers/solo-code-ban.d.ts +23 -0
  75. package/dist/src/services/audit/enforcers/solo-code-ban.js +27 -0
  76. package/dist/src/services/audit/enforcers/sub-agent-sid.d.ts +25 -0
  77. package/dist/src/services/audit/enforcers/sub-agent-sid.js +63 -0
  78. package/dist/src/services/audit/enforcers/tech-doc-presence.d.ts +28 -0
  79. package/dist/src/services/audit/enforcers/tech-doc-presence.js +35 -0
  80. package/dist/src/services/audit/red-line-catalog-p2-a.d.ts +21 -0
  81. package/dist/src/services/audit/red-line-catalog-p2-a.js +233 -0
  82. package/dist/src/services/audit/red-line-catalog-p2-b.d.ts +19 -0
  83. package/dist/src/services/audit/red-line-catalog-p2-b.js +225 -0
  84. package/dist/src/services/audit/red-line-catalog.d.ts +51 -0
  85. package/dist/src/services/audit/red-line-catalog.js +210 -0
  86. package/dist/src/services/audit/red-lines-service.d.ts +23 -0
  87. package/dist/src/services/audit/red-lines-service.js +486 -0
  88. package/dist/src/services/audit/scanners/openspec-scanner.d.ts +15 -0
  89. package/dist/src/services/audit/scanners/openspec-scanner.js +55 -0
  90. package/dist/src/services/audit/scanners/rules-tree-scanner.d.ts +16 -0
  91. package/dist/src/services/audit/scanners/rules-tree-scanner.js +56 -0
  92. package/dist/src/services/audit/scanners/skills-tree-scanner.d.ts +17 -0
  93. package/dist/src/services/audit/scanners/skills-tree-scanner.js +46 -0
  94. package/dist/src/services/audit/static-service.d.ts +57 -0
  95. package/dist/src/services/audit/static-service.js +125 -0
  96. package/dist/src/services/audit/types.d.ts +69 -0
  97. package/dist/src/services/audit/types.js +13 -0
  98. package/dist/src/services/classify/classify-service.d.ts +42 -0
  99. package/dist/src/services/classify/classify-service.js +122 -0
  100. package/dist/src/services/classify/classify-types.d.ts +79 -0
  101. package/dist/src/services/classify/classify-types.js +90 -0
  102. package/dist/src/services/code-review/ocr-service.d.ts +129 -0
  103. package/dist/src/services/code-review/ocr-service.js +362 -0
  104. package/dist/src/services/config/config-migration.d.ts +32 -0
  105. package/dist/src/services/config/config-migration.js +92 -0
  106. package/dist/src/services/config/config-restore.d.ts +10 -0
  107. package/dist/src/services/config/config-restore.js +47 -0
  108. package/dist/src/services/config/config-rollback.d.ts +13 -0
  109. package/dist/src/services/config/config-rollback.js +26 -0
  110. package/dist/src/services/config/config-service.d.ts +35 -2
  111. package/dist/src/services/config/config-service.js +81 -0
  112. package/dist/src/services/config/config-types.d.ts +58 -0
  113. package/dist/src/services/config/config-types.js +6 -0
  114. package/dist/src/services/doctor/doctor-service.js +96 -0
  115. package/dist/src/services/fuzzy-matching/fuzzy-match-service.d.ts +15 -0
  116. package/dist/src/services/fuzzy-matching/fuzzy-match-service.js +56 -0
  117. package/dist/src/services/fuzzy-matching/types.d.ts +20 -0
  118. package/dist/src/services/fuzzy-matching/types.js +1 -0
  119. package/dist/src/services/ide/adapters/hermes-adapter.d.ts +21 -0
  120. package/dist/src/services/ide/adapters/hermes-adapter.js +51 -0
  121. package/dist/src/services/ide/adapters/openclaw-adapter.d.ts +14 -0
  122. package/dist/src/services/ide/adapters/openclaw-adapter.js +42 -0
  123. package/dist/src/services/ide/ide-registry.js +7 -0
  124. package/dist/src/services/ide/ide-types.d.ts +1 -1
  125. package/dist/src/services/memory/memory-search-service.d.ts +61 -0
  126. package/dist/src/services/memory/memory-search-service.js +80 -0
  127. package/dist/src/services/openspec/openspec-propose-from-doctor-service.d.ts +31 -0
  128. package/dist/src/services/openspec/openspec-propose-from-doctor-service.js +95 -0
  129. package/dist/src/services/preferences/preferences-service.d.ts +6 -0
  130. package/dist/src/services/preferences/preferences-service.js +43 -0
  131. package/dist/src/services/preferences/preferences-types.d.ts +90 -0
  132. package/dist/src/services/preferences/preferences-types.js +38 -0
  133. package/dist/src/services/recommendations/capability-seed-items.js +0 -1
  134. package/dist/src/services/recommendations/capability-seed-mappings.js +0 -1
  135. package/dist/src/services/recommendations/capability-seed-sources.js +0 -1
  136. package/dist/src/services/retrospective/retrospective-search-service.d.ts +37 -0
  137. package/dist/src/services/retrospective/retrospective-search-service.js +75 -0
  138. package/dist/src/services/skills/skill-conformance-service.d.ts +40 -0
  139. package/dist/src/services/skills/skill-conformance-service.js +136 -0
  140. package/dist/src/services/skills/skill-runbook-service.js +44 -10
  141. package/dist/src/services/skills/sync-service.d.ts +43 -0
  142. package/dist/src/services/skills/sync-service.js +99 -0
  143. package/dist/src/services/slice/slice-check-service.js +166 -13
  144. package/dist/src/services/slice/slice-check-types.d.ts +1 -1
  145. package/dist/src/services/standards/migrate-claude-rules-service.d.ts +19 -0
  146. package/dist/src/services/standards/migrate-claude-rules-service.js +193 -0
  147. package/dist/src/services/standards/project-context.d.ts +1 -1
  148. package/dist/src/services/standards/project-context.js +0 -4
  149. package/dist/src/services/standards/project-standards-service.js +1 -3
  150. package/dist/src/services/understand/understand-scan-service.js +15 -2
  151. package/dist/src/services/understand/understand-types.d.ts +26 -0
  152. package/dist/src/services/upgrade/1x-detector-service.d.ts +7 -0
  153. package/dist/src/services/upgrade/1x-detector-service.js +94 -0
  154. package/dist/src/services/upgrade/gitignore-migrate-service.d.ts +56 -0
  155. package/dist/src/services/upgrade/gitignore-migrate-service.js +170 -0
  156. package/dist/src/services/upgrade/upgrade-service.d.ts +47 -0
  157. package/dist/src/services/upgrade/upgrade-service.js +381 -0
  158. package/dist/src/services/workspace/migrate-1-4-1-service.js +1 -1
  159. package/dist/src/services/workspace/sid-naming-guard.d.ts +14 -0
  160. package/dist/src/services/workspace/sid-naming-guard.js +31 -0
  161. package/dist/src/services/workspace/workspace-archive-service.d.ts +19 -0
  162. package/dist/src/services/workspace/workspace-archive-service.js +32 -0
  163. package/dist/src/services/workspace/workspace-clean-service.d.ts +41 -0
  164. package/dist/src/services/workspace/workspace-clean-service.js +86 -0
  165. package/dist/src/services/workspace/workspace-state-service.d.ts +7 -0
  166. package/dist/src/services/workspace/workspace-state-service.js +43 -0
  167. package/dist/src/shared/change-id.js +4 -1
  168. package/dist/src/shared/version.d.ts +1 -1
  169. package/dist/src/shared/version.js +1 -1
  170. package/package.json +10 -8
  171. package/schemas/doctor-report.schema.json +1 -1
  172. package/scripts/install-skills.mjs +296 -12
  173. package/skills/peaks-doctor/SKILL.md +59 -0
  174. package/skills/peaks-doctor/references/doctor-check-catalog.md +31 -0
  175. package/skills/peaks-doctor/references/from-doctor-flow.md +64 -0
  176. package/skills/peaks-doctor/test_prompts.json +17 -0
  177. package/skills/peaks-ide/SKILL.md +2 -0
  178. package/skills/peaks-qa/SKILL.md +9 -7
  179. package/skills/peaks-qa/references/artifact-per-request.md +19 -5
  180. package/skills/peaks-qa/references/qa-perf-test-plan.md +6 -6
  181. package/skills/peaks-qa/references/qa-runbook.md +1 -1
  182. package/skills/peaks-rd/SKILL.md +25 -10
  183. package/skills/peaks-rd/references/ocr-integration.md +214 -0
  184. package/skills/peaks-rd/references/rd-fanout-contracts.md +70 -0
  185. package/skills/peaks-rd/references/rd-runbook.md +1 -1
  186. package/skills/peaks-solo/SKILL.md +11 -5
  187. package/skills/peaks-solo/references/completion-handoff.md +3 -1
  188. package/skills/peaks-solo/references/step-0-55-1x-detection.md +82 -0
  189. package/skills/peaks-solo/references/workflow-gates-and-types.md +9 -0
  190. package/dist/src/cli/commands/shadcn-commands.d.ts +0 -3
  191. package/dist/src/cli/commands/shadcn-commands.js +0 -35
  192. package/dist/src/cli/commands/skill-context-stats-command.d.ts +0 -40
  193. package/dist/src/cli/commands/skill-context-stats-command.js +0 -96
  194. package/dist/src/cli/commands/skill-scope-commands.d.ts +0 -51
  195. package/dist/src/cli/commands/skill-scope-commands.js +0 -310
  196. package/dist/src/services/shadcn/shadcn-service.d.ts +0 -27
  197. package/dist/src/services/shadcn/shadcn-service.js +0 -128
  198. package/dist/src/services/skill-scope/adapters/_stub-helper.d.ts +0 -39
  199. package/dist/src/services/skill-scope/adapters/_stub-helper.js +0 -98
  200. package/dist/src/services/skill-scope/adapters/claude-code.d.ts +0 -59
  201. package/dist/src/services/skill-scope/adapters/claude-code.js +0 -304
  202. package/dist/src/services/skill-scope/adapters/codex.d.ts +0 -2
  203. package/dist/src/services/skill-scope/adapters/codex.js +0 -12
  204. package/dist/src/services/skill-scope/adapters/cursor.d.ts +0 -2
  205. package/dist/src/services/skill-scope/adapters/cursor.js +0 -13
  206. package/dist/src/services/skill-scope/adapters/qoder.d.ts +0 -2
  207. package/dist/src/services/skill-scope/adapters/qoder.js +0 -13
  208. package/dist/src/services/skill-scope/adapters/tongyi.d.ts +0 -2
  209. package/dist/src/services/skill-scope/adapters/tongyi.js +0 -13
  210. package/dist/src/services/skill-scope/adapters/trae.d.ts +0 -2
  211. package/dist/src/services/skill-scope/adapters/trae.js +0 -12
  212. package/dist/src/services/skill-scope/detect.d.ts +0 -81
  213. package/dist/src/services/skill-scope/detect.js +0 -513
  214. package/dist/src/services/skill-scope/registry.d.ts +0 -41
  215. package/dist/src/services/skill-scope/registry.js +0 -83
  216. package/dist/src/services/skill-scope/source-of-truth.d.ts +0 -44
  217. package/dist/src/services/skill-scope/source-of-truth.js +0 -118
  218. package/dist/src/services/skill-scope/types.d.ts +0 -195
  219. package/dist/src/services/skill-scope/types.js +0 -97
@@ -6,10 +6,12 @@ import { executeProjectMemoryBackup, executeProjectMemoryExtract, summarizeProje
6
6
  import { summarizeProjectStandardsInitResult, summarizeProjectStandardsUpdateResult } from '../../services/standards/project-standards-service.js';
7
7
  import { executeProjectStandardsInitIdeAware, executeProjectStandardsUpdateIdeAware } from '../../services/standards/ide-aware-standards-service.js';
8
8
  import { migrateStandards } from '../../services/standards/migrate-service.js';
9
+ import { migrateClaudeRules } from '../../services/standards/migrate-claude-rules-service.js';
9
10
  import { listProfiles } from '../../services/profiles/profile-service.js';
10
11
  import { planProxyTest } from '../../services/proxy/proxy-service.js';
11
12
  import { runDoctor } from '../../services/doctor/doctor-service.js';
12
13
  import { listSkills } from '../../services/skills/skill-registry.js';
14
+ import { runSkillSync, SYNC_PLATFORMS } from '../../services/skills/sync-service.js';
13
15
  import { inspectSkillRunbook } from '../../services/skills/skill-runbook-service.js';
14
16
  import { setSkillPresence, clearSkillPresence, getSkillPresence, isSkillPresenceMode, touchSkillHeartbeat } from '../../services/skills/skill-presence-service.js';
15
17
  import { detectPresenceMarker } from '../../services/hooks/presence-marker-detector.js';
@@ -88,6 +90,42 @@ export function registerCoreAndArtifactCommands(program, io) {
88
90
  process.exitCode = 1;
89
91
  }
90
92
  });
93
+ // Slice #12 final piece (per spec §9 line 1105):
94
+ // `peaks skills sync 8 平台分发`. Idempotent: re-running is a
95
+ // no-op when the symlinks are already correct.
96
+ addJsonOption(skill
97
+ .command('sync')
98
+ .description(`Sync the peaks-* skill family to one or all of the 8 supported LLM-CLI platforms (${SYNC_PLATFORMS.join(', ')}). Idempotent.`)
99
+ .option('--platform <id>', `sync only one platform (default: --all). Valid: ${SYNC_PLATFORMS.join(', ')}`)
100
+ .option('--all', 'sync all 8 platforms (default if --platform is omitted)')
101
+ .option('--dry-run', 'do not write; emit the same shape with applied=false')
102
+ .option('--project <path>', 'project root (default: cwd)')).action(async (options) => {
103
+ try {
104
+ const projectRoot = options.project ?? process.cwd();
105
+ const platforms = options.platform !== undefined ? [options.platform] : undefined;
106
+ const result = await runSkillSync({
107
+ projectRoot,
108
+ ...(platforms !== undefined ? { platforms } : {}),
109
+ ...(options.dryRun === true ? { dryRun: true } : {}),
110
+ });
111
+ const envelope = ok('skill.sync', result, [], [
112
+ `syncedCount: ${result.syncedCount}/${result.perPlatform.length} platforms`,
113
+ `totalInstalled: ${result.totalInstalled} skill symlinks`,
114
+ result.failedCount > 0
115
+ ? `failedCount: ${result.failedCount} (run \`peaks skill status\` for details)`
116
+ : 'no failures',
117
+ ]);
118
+ printResult(io, envelope, options.json);
119
+ if (result.failedCount > 0) {
120
+ process.exitCode = 1;
121
+ }
122
+ }
123
+ catch (error) {
124
+ const message = getErrorMessage(error);
125
+ printResult(io, fail('skill.sync', 'SKILL_SYNC_FAILED', message, { applied: false }, [message]), options.json);
126
+ process.exitCode = 1;
127
+ }
128
+ });
91
129
  addJsonOption(skill
92
130
  .command('runbook <name>')
93
131
  .description('Inspect a skill Default runbook section and its --apply authorization-note status')).action(async (name, options) => {
@@ -463,10 +501,30 @@ export function registerCoreAndArtifactCommands(program, io) {
463
501
  });
464
502
  addJsonOption(standards
465
503
  .command('migrate')
466
- .description('Rewrite a consumer project CLAUDE.md to drop the legacy heartbeat block (slice 028). Dry-run by default; pass --apply to write.')
504
+ .description('Rewrite a consumer project CLAUDE.md to drop the legacy heartbeat block (slice 028). Dry-run by default; pass --apply to write. With --from-claude-rules, thins the 1.x .claude/rules/ tree to 2-line pointers and scaffolds .peaks/standards/ (slice 2026-06-12-standards-migrate-claude-rules).')
467
505
  .option('--project <path>', 'target project root')
468
- .option('--apply', 'rewrite the legacy block in place; default is dry-run')).action((options) => {
506
+ .option('--apply', 'rewrite the legacy block in place; default is dry-run')
507
+ .option('--from-claude-rules', 'thin .claude/rules/ to pointers and scaffold .peaks/standards/ (used by `peaks upgrade --to 2.0`)')).action((options) => {
469
508
  const projectRoot = options.project ?? process.cwd();
509
+ if (options.fromClaudeRules === true) {
510
+ try {
511
+ const result = migrateClaudeRules({ projectRoot, apply: options.apply === true });
512
+ printResult(io, ok('standards.migrate', result.data, [], [...result.data.nextActions]), options.json);
513
+ }
514
+ catch (error) {
515
+ printResult(io, fail('standards.migrate', 'STANDARDS_MIGRATE_FAILED', getErrorMessage(error), {
516
+ backupPath: null,
517
+ thinnedFiles: [],
518
+ scaffoldedFiles: [],
519
+ preservedFiles: [],
520
+ wouldChange: false,
521
+ applied: false,
522
+ nextActions: [],
523
+ }, [getErrorMessage(error)]), options.json);
524
+ process.exitCode = 1;
525
+ }
526
+ return;
527
+ }
470
528
  try {
471
529
  const result = migrateStandards({ project: projectRoot, apply: options.apply === true });
472
530
  printResult(io, ok('standards.migrate', result.data, [], result.data.nextActions), options.json);
@@ -519,6 +577,27 @@ export function registerCoreAndArtifactCommands(program, io) {
519
577
  process.exitCode = 1;
520
578
  }
521
579
  });
580
+ addJsonOption(memory
581
+ .command('search <query>')
582
+ .description('Fuzzy-search the memory index (deterministic, local, zero-token). Default --limit 6.')
583
+ .option('--kind <kind>', 'filter by memory kind (one of: project, rule, decision, reference, feedback, convention, module, lesson)')
584
+ .option('--limit <n>', 'maximum number of matches to return', (value) => Number(value))
585
+ .option('--project <path>', 'target project root (defaults to git root or cwd)')).action((query, options) => {
586
+ // Lazy import avoids a top-of-file import cycle (memory-commands.ts
587
+ // imports services that the rest of this file may also touch).
588
+ void import('./memory-commands.js').then(({ runMemorySearch }) => {
589
+ runMemorySearch(io, {
590
+ query,
591
+ ...(options.kind !== undefined ? { kind: options.kind } : {}),
592
+ ...(options.limit !== undefined ? { limit: options.limit } : {}),
593
+ ...(options.project !== undefined ? { project: options.project } : {}),
594
+ ...(options.json !== undefined ? { json: options.json } : {}),
595
+ });
596
+ }).catch((error) => {
597
+ printResult(io, fail('memory.search', 'MEMORY_SEARCH_BOOTSTRAP_FAILED', getErrorMessage(error), {}, []), options.json);
598
+ process.exitCode = 1;
599
+ });
600
+ });
522
601
  const proxy = program.command('proxy').description('Manage proxy settings');
523
602
  addJsonOption(proxy
524
603
  .command('test')
@@ -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
+ }
@@ -0,0 +1,13 @@
1
+ import { type ProgramIO } from '../cli-helpers.js';
2
+ export interface MemorySearchCommandOptions {
3
+ query: string;
4
+ kind?: string;
5
+ limit?: number;
6
+ project?: string;
7
+ json?: boolean;
8
+ }
9
+ /**
10
+ * Run the memory search subcommand. Extracted so unit tests can
11
+ * exercise the full envelope without spawning a subprocess.
12
+ */
13
+ export declare function runMemorySearch(io: ProgramIO, options: MemorySearchCommandOptions): void;
@@ -0,0 +1,60 @@
1
+ import { findProjectRoot } from '../../services/config/config-safety.js';
2
+ import { resolveCanonicalProjectRoot } from '../../services/config/config-service.js';
3
+ import { searchMemory } from '../../services/memory/memory-search-service.js';
4
+ import { fail, ok } from '../../shared/result.js';
5
+ import { getErrorMessage, printResult } from '../cli-helpers.js';
6
+ const VALID_KINDS = [
7
+ 'project',
8
+ 'rule',
9
+ 'decision',
10
+ 'reference',
11
+ 'feedback',
12
+ 'convention',
13
+ 'module',
14
+ 'lesson',
15
+ ];
16
+ /**
17
+ * Run the memory search subcommand. Extracted so unit tests can
18
+ * exercise the full envelope without spawning a subprocess.
19
+ */
20
+ export function runMemorySearch(io, options) {
21
+ const projectRoot = options.project !== undefined
22
+ ? resolveCanonicalProjectRoot(options.project)
23
+ : (findProjectRoot(process.cwd()) ?? process.cwd());
24
+ const kindFilter = options.kind !== undefined && VALID_KINDS.includes(options.kind)
25
+ ? options.kind
26
+ : undefined;
27
+ // When the user passes --kind but the value isn't in the valid set,
28
+ // we silently pass `undefined` so the search returns the full set;
29
+ // that's friendlier than a hard error and matches the spec's
30
+ // "invalid kind -> empty matches" semantic for the filter path.
31
+ // (For the loader unit test we exercise the explicit-invalid path
32
+ // directly; here the CLI side is forgiving.)
33
+ try {
34
+ const matches = searchMemory({
35
+ query: options.query,
36
+ projectRoot,
37
+ ...(options.limit !== undefined ? { limit: options.limit } : {}),
38
+ ...(kindFilter !== undefined ? { kind: kindFilter } : {}),
39
+ });
40
+ printResult(io, ok('memory.search', {
41
+ query: options.query,
42
+ total: matches.length,
43
+ matches,
44
+ warnings: [],
45
+ }, []), options.json);
46
+ }
47
+ catch (error) {
48
+ const message = getErrorMessage(error);
49
+ const code = error.code ?? 'MEMORY_SEARCH_FAILED';
50
+ const suggestions = [];
51
+ if (code === 'INDEX_MISSING') {
52
+ suggestions.push('Run `peaks memory extract --apply` to build the index from memory/*.md files');
53
+ }
54
+ if (code === 'EMPTY_QUERY') {
55
+ suggestions.push('Use `peaks memory index` to list all entries');
56
+ }
57
+ printResult(io, fail('memory.search', code, message, { projectRoot, query: options.query }, suggestions), options.json);
58
+ process.exitCode = 1;
59
+ }
60
+ }
@@ -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
+ }
@@ -1,3 +1,12 @@
1
1
  import { Command } from 'commander';
2
2
  import { type ProgramIO } from '../cli-helpers.js';
3
+ export interface RetrospectiveSearchCommandOptions {
4
+ query: string;
5
+ type?: string;
6
+ outcome?: string;
7
+ limit?: number;
8
+ project?: string;
9
+ json?: boolean;
10
+ }
11
+ export declare function runRetrospectiveSearch(io: ProgramIO, options: RetrospectiveSearchCommandOptions): void;
3
12
  export declare function registerRetrospectiveCommands(program: Command, io: ProgramIO): void;