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
@@ -1,17 +1,55 @@
1
1
  import { execFileSync } from 'node:child_process';
2
- import { existsSync, statSync } from 'node:fs';
2
+ import { existsSync, readFileSync, statSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
4
  import { isDirectory } from '../../shared/fs.js';
5
5
  import { getCurrentChangeId } from '../../shared/change-id.js';
6
6
  import { verifyPipeline } from '../workflow/pipeline-verify-service.js';
7
- function runCommand(command, args, cwd, timeoutMs) {
7
+ import { findMockViolations } from '../audit/enforcers/mock-placement.js';
8
+ import { runRedLinesAudit } from '../audit/red-lines-service.js';
9
+ /**
10
+ * Resolve a CLI binary to a project-local path, falling back to
11
+ * the system `npx`. pnpm (and npm/yarn) all create
12
+ * `node_modules/.bin/<name>`:
13
+ *
14
+ * - On Unix, this is a symlink to the package's executable.
15
+ * - On Windows, this is a `.cmd` shim; `execFileSync` only
16
+ * resolves `.cmd` through the shell (PATHEXT), so we pass
17
+ * `shell: true` when invoking one. Without this, the
18
+ * Windows `npx ENOENT` false-positive from
19
+ * observations 2317 + 2792 reproduces for every local
20
+ * binary.
21
+ *
22
+ * Returns the command + args + a `shell` flag that the
23
+ * `runCommand` helper threads into `execFileSync`.
24
+ */
25
+ function resolveLocalBinary(projectRoot, name) {
26
+ // pnpm creates `node_modules/.bin/<name>` (symlink on Unix,
27
+ // `.cmd` shim on Windows). We probe both shapes; the
28
+ // `process.platform === 'win32'` extension probe is the most
29
+ // portable approach.
30
+ const isWin = process.platform === 'win32';
31
+ const candidateNames = isWin ? [`${name}.cmd`, `${name}.ps1`, `${name}`] : [name];
32
+ for (const candidate of candidateNames) {
33
+ const cmdPath = join(projectRoot, 'node_modules', '.bin', candidate);
34
+ if (existsSync(cmdPath)) {
35
+ return { command: cmdPath, args: [], shell: isWin };
36
+ }
37
+ }
38
+ // Fallback: system npx. On Windows this still has the ENOENT
39
+ // issue, but the fallback is at least informative when it
40
+ // fires (the user can see "npx not found" instead of a
41
+ // silent exit 1).
42
+ return { command: 'npx', args: [name], shell: false };
43
+ }
44
+ function runCommand(command, args, cwd, timeoutMs, shell = false) {
8
45
  const start = Date.now();
9
46
  try {
10
47
  const stdout = execFileSync(command, args, {
11
48
  cwd,
12
49
  stdio: ['ignore', 'pipe', 'pipe'],
13
50
  timeout: timeoutMs,
14
- maxBuffer: 32 * 1024 * 1024
51
+ maxBuffer: 32 * 1024 * 1024,
52
+ shell
15
53
  }).toString('utf8');
16
54
  return {
17
55
  status: 'pass',
@@ -41,11 +79,17 @@ function tailLines(text, max) {
41
79
  }
42
80
  async function runTypecheck(projectRoot) {
43
81
  const start = Date.now();
44
- const result = runCommand('npx', ['tsc', '--noEmit'], projectRoot, 180_000);
82
+ // Per Windows npx ENOENT (observations 2317+2792 from
83
+ // 2026-06-09), prefer the project-local `node_modules/.bin/tsc`
84
+ // (symlink on Unix, .cmd on Windows). The local binary is
85
+ // installed by pnpm at workspace-install time and avoids the
86
+ // npx PATH-lookup issue.
87
+ const tsc = resolveLocalBinary(projectRoot, 'tsc');
88
+ const result = runCommand(tsc.command, [...tsc.args, '--noEmit'], projectRoot, 180_000, tsc.shell);
45
89
  const testFiles = result.stdout.match(/(tests?\/.*\.test\.ts)/g) ?? [];
46
90
  return {
47
91
  name: 'typecheck',
48
- description: 'npx tsc --noEmit (no JS emit, type-only check)',
92
+ description: `${tsc.command} --noEmit (no JS emit, type-only check)`,
49
93
  status: result.status,
50
94
  durationMs: result.durationMs,
51
95
  detail: result.status === 'pass'
@@ -76,13 +120,17 @@ async function runUnitTests(projectRoot, runTests) {
76
120
  // state. Opt-in to the full suite via `runTests: true` (CLI flag
77
121
  // `--run-tests`). See `references/runbook.md` for the rationale and
78
122
  // `tests/unit/slice-check-service.test.ts` for the regression net.
79
- const args = runTests
80
- ? ['vitest', 'run', '--reporter=default', '--coverage=false']
81
- : ['vitest', 'run', '--changed', '--reporter=default', '--coverage=false'];
123
+ // Per Windows npx ENOENT (observations 2317+2792), resolve
124
+ // the project-local vitest binary instead of shelling out
125
+ // through npx.
126
+ const vitest = resolveLocalBinary(projectRoot, 'vitest');
127
+ const vitestArgs = runTests
128
+ ? ['run', '--reporter=default', '--coverage=false']
129
+ : ['run', '--changed', '--reporter=default', '--coverage=false'];
82
130
  const description = runTests
83
- ? 'npx vitest run (full test suite, coverage off)'
84
- : 'npx vitest run --changed (tests for git-changed files only, coverage off)';
85
- const result = runCommand('npx', args, projectRoot, 600_000);
131
+ ? `${vitest.command} run (full test suite, coverage off)`
132
+ : `${vitest.command} run --changed (tests for git-changed files only, coverage off)`;
133
+ const result = runCommand(vitest.command, [...vitest.args, ...vitestArgs], projectRoot, 600_000, vitest.shell);
86
134
  const summary = parseVitestSummary(result.stdout, result.durationMs);
87
135
  // Vitest doesn't always print the per-bucket counts cleanly; infer "passed"
88
136
  // as total - failed - skipped when failed/skipped buckets are present.
@@ -225,7 +273,7 @@ export async function sliceCheck(options) {
225
273
  if (options.skipTests) {
226
274
  stages.push({
227
275
  name: 'unit-tests',
228
- description: 'npx vitest run (skipped per --skip-tests)',
276
+ description: 'vitest run (skipped per --skip-tests)',
229
277
  status: 'skipped',
230
278
  durationMs: 0,
231
279
  detail: 'Skipped: --skip-tests was set. Use the peaks-solo-test skill to run the full suite manually.'
@@ -244,7 +292,7 @@ export async function sliceCheck(options) {
244
292
  const failureCount = unitTests.data?.failed ?? 0;
245
293
  stages.push({
246
294
  name: 'unit-tests',
247
- description: `npx vitest run ${options.runTests === true ? '' : '--changed '} (overridden via --allow-pre-existing-failures)`.trim(),
295
+ description: `vitest run ${options.runTests === true ? '' : '--changed '} (overridden via --allow-pre-existing-failures)`.trim(),
248
296
  status: 'skipped',
249
297
  durationMs: unitTests.durationMs,
250
298
  detail: `pre-existing failures: ${failureCount} failing test(s) under coverage.exclude or unrelated to this slice; user opted in via --allow-pre-existing-failures. For the long-term fix, mark these tests .skip or move to coverage.exclude (see dogfood-2-f1-f4.md F17c).`,
@@ -261,6 +309,17 @@ export async function sliceCheck(options) {
261
309
  stages.push(await runReviewFanout(options.projectRoot, rid, options.refreshFanout));
262
310
  // Stage 4: gate verify-pipeline
263
311
  stages.push(await runGateVerifyPipeline(options.projectRoot, rid, rid));
312
+ // Stage 5: mock-placement (L2.1 P0 #5) — refuse inline mock data in src/ or skills/.
313
+ // Lifts changed files via `git diff --name-only HEAD`; falls back to a
314
+ // warning when the diff is empty (e.g. a fresh tree). Lighter than the
315
+ // full `peaks scan diff-vs-scope` and keeps the slice check self-contained.
316
+ stages.push(await runMockPlacement(options.projectRoot));
317
+ // Stage 6 (Slice #7 L2.4 P2-b): audit-regression — assert
318
+ // catalog integrity (no orphan enforcers, no orphan catalog
319
+ // entries), catalog size lower bound, and runtime budget.
320
+ // The stage runs `peaks audit red-lines` in-process (no
321
+ // subprocess) and is gating: failure exits non-zero.
322
+ stages.push(await runAuditRegression(options.projectRoot));
264
323
  const boundaryReady = stages.every((s) => s.status === 'pass' || s.status === 'skipped');
265
324
  const nextActions = [];
266
325
  if (!boundaryReady) {
@@ -283,3 +342,97 @@ export async function sliceCheck(options) {
283
342
  nextActions
284
343
  };
285
344
  }
345
+ async function runAuditRegression(projectRoot) {
346
+ const start = Date.now();
347
+ try {
348
+ const result = runRedLinesAudit({ projectRoot });
349
+ const durationMs = Date.now() - start;
350
+ // Slice #7 L2.4 P2-b acceptance A3 + A4:
351
+ // - totalRedLines >= 60 (catalog grew to 66; pins the lower bound)
352
+ // - enforcerFindings has no rl-audit-no-orphan-enforcer / rl-audit-no-orphan-catalog hits
353
+ const issues = [];
354
+ if (result.audit.totalRedLines < 60) {
355
+ issues.push(`totalRedLines ${result.audit.totalRedLines} < 60`);
356
+ }
357
+ const orphanFindings = result.audit.enforcerFindings.filter((f) => f.enforcerId === 'rl-audit-no-orphan-enforcer-001' ||
358
+ f.enforcerId === 'rl-audit-no-orphan-catalog-001');
359
+ if (orphanFindings.length > 0) {
360
+ issues.push(`${orphanFindings.length} orphan-enforcer / orphan-catalog finding(s)`);
361
+ }
362
+ if (issues.length > 0) {
363
+ return {
364
+ name: 'audit-regression',
365
+ description: 'audit-regression: catalog integrity + runtime budget (L2.4 P2-b stage 6)',
366
+ status: 'fail',
367
+ durationMs,
368
+ detail: issues.join('; '),
369
+ };
370
+ }
371
+ return {
372
+ name: 'audit-regression',
373
+ description: 'audit-regression: catalog integrity + runtime budget (L2.4 P2-b stage 6)',
374
+ status: 'pass',
375
+ durationMs,
376
+ detail: `catalog: ${result.audit.totalRedLines} entries (${result.audit.cliBacked} cli-backed, ${result.audit.proseOnly} prose-only); audit ran in ${durationMs}ms`,
377
+ };
378
+ }
379
+ catch (error) {
380
+ return {
381
+ name: 'audit-regression',
382
+ description: 'audit-regression: catalog integrity + runtime budget (L2.4 P2-b stage 6)',
383
+ status: 'fail',
384
+ durationMs: Date.now() - start,
385
+ detail: 'audit-regression failed: ' + (error?.message ?? String(error)),
386
+ };
387
+ }
388
+ }
389
+ async function runMockPlacement(projectRoot) {
390
+ const start = Date.now();
391
+ // List changed files via git. `--name-only` produces one path per line;
392
+ // we filter to text files in scope and read each.
393
+ const diffResult = runCommand('git', ['diff', '--name-only', '--diff-filter=ACMR', 'HEAD'], projectRoot, 30_000);
394
+ if (diffResult.status !== 'pass') {
395
+ return {
396
+ name: 'mock-placement',
397
+ description: 'mock-placement: no inline mock data in src/ or skills/ (L2.1 P0 #5)',
398
+ status: 'skipped',
399
+ durationMs: Date.now() - start,
400
+ detail: 'git diff failed or returned no changed files; mock-placement scan skipped.'
401
+ };
402
+ }
403
+ const changed = diffResult.stdout
404
+ .split('\n')
405
+ .map((l) => l.trim())
406
+ .filter(Boolean);
407
+ if (changed.length === 0) {
408
+ return {
409
+ name: 'mock-placement',
410
+ description: 'mock-placement: no inline mock data in src/ or skills/ (L2.1 P0 #5)',
411
+ status: 'skipped',
412
+ durationMs: Date.now() - start,
413
+ detail: 'no changed files in HEAD diff; mock-placement scan skipped.'
414
+ };
415
+ }
416
+ const files = changed
417
+ .filter((p) => p.startsWith('src/') || p.startsWith('skills/'))
418
+ .filter((p) => p.endsWith('.ts') || p.endsWith('.tsx') || p.endsWith('.js') || p.endsWith('.mjs'))
419
+ .map((filePath) => {
420
+ const abs = join(projectRoot, filePath);
421
+ if (!existsSync(abs))
422
+ return null;
423
+ const content = readFileSync(abs, 'utf-8');
424
+ return { filePath, content };
425
+ })
426
+ .filter((f) => f !== null);
427
+ const violations = findMockViolations(files);
428
+ return {
429
+ name: 'mock-placement',
430
+ description: 'mock-placement: no inline mock data in src/ or skills/ (L2.1 P0 #5)',
431
+ status: violations.length === 0 ? 'pass' : 'fail',
432
+ durationMs: Date.now() - start,
433
+ detail: violations.length === 0
434
+ ? `Scanned ${files.length} changed file(s); no inline mock data found.`
435
+ : `${violations.length} violation(s): ${violations.map((v) => `${v.filePath} (${v.snippet})`).join('; ')}`,
436
+ data: { scannedFiles: files.length, violations: violations.map((v) => ({ filePath: v.filePath, pattern: v.pattern, snippet: v.snippet })) }
437
+ };
438
+ }
@@ -28,7 +28,7 @@
28
28
  export type SliceCheckStageStatus = 'pass' | 'fail' | 'skipped';
29
29
  export type SliceCheckStage = {
30
30
  /** Stable id for the stage (matches the runbook's check list). */
31
- name: 'typecheck' | 'unit-tests' | 'review-fanout' | 'gate-verify-pipeline';
31
+ name: 'typecheck' | 'unit-tests' | 'review-fanout' | 'gate-verify-pipeline' | 'mock-placement' | 'audit-regression';
32
32
  /** Human-readable description. */
33
33
  description: string;
34
34
  status: SliceCheckStageStatus;
@@ -0,0 +1,19 @@
1
+ export interface MigrateClaudeRulesInput {
2
+ readonly projectRoot: string;
3
+ readonly apply?: boolean;
4
+ }
5
+ export interface MigrateClaudeRulesData {
6
+ readonly backupPath: string | null;
7
+ readonly thinnedFiles: readonly string[];
8
+ readonly scaffoldedFiles: readonly string[];
9
+ readonly preservedFiles: readonly string[];
10
+ readonly wouldChange: boolean;
11
+ readonly applied: boolean;
12
+ readonly nextActions: readonly string[];
13
+ }
14
+ export interface MigrateClaudeRulesResult {
15
+ readonly ok: true;
16
+ readonly data: MigrateClaudeRulesData;
17
+ readonly warnings: readonly string[];
18
+ }
19
+ export declare function migrateClaudeRules(input: MigrateClaudeRulesInput): MigrateClaudeRulesResult;
@@ -0,0 +1,193 @@
1
+ /**
2
+ * peaks standards migrate — .claude/rules/ tree thinning.
3
+ * Slice: 2026-06-12-standards-migrate-claude-rules.
4
+ *
5
+ * The 1.x peaks-cli install copied a thick .claude/rules
6
+ * tree (skill-first / CLI-auxiliary / dogfood / commit-trailer
7
+ * rules) into consumer projects. In 2.0, the canonical rules
8
+ * live at .peaks/standards/ and every markdown file under
9
+ * .claude/rules becomes a 2-line pointer to the canonical path.
10
+ *
11
+ * The service:
12
+ * 1. Backs up the existing `.claude/rules/` tree to
13
+ * `.claude/rules/.peaks-2.0-backup-<ts>/` (timestamped;
14
+ * safe to run multiple times).
15
+ * 2. Replaces each .md file under .claude/rules (recursive)
16
+ * with a 2-line pointer.
17
+ * 3. Scaffolds the 2.0 canonical rules at
18
+ * `.peaks/standards/{common,typescript}/`, but
19
+ * never overwrites existing files in `.peaks/standards/`.
20
+ *
21
+ * All operations are gated by `apply: true`. Dry-run mode
22
+ * returns the would-change diff without writing.
23
+ */
24
+ import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
25
+ import { join } from 'node:path';
26
+ const POINTER_TEXT = (canonicalPath) => `# Canonical peaks-cli 2.0 rules live at: ${canonicalPath}\n# This file is a 2-line pointer. Edit the canonical file instead.\n`;
27
+ function timestampSlug() {
28
+ return new Date().toISOString().replace(/[:.]/g, '-');
29
+ }
30
+ function readMarkdownFilesRecursive(root) {
31
+ if (!existsSync(root))
32
+ return [];
33
+ const out = [];
34
+ const stat = statSync(root);
35
+ if (stat.isFile()) {
36
+ return root.endsWith('.md') ? [root] : [];
37
+ }
38
+ if (!stat.isDirectory())
39
+ return [];
40
+ for (const entry of readdirSync(root)) {
41
+ out.push(...readMarkdownFilesRecursive(join(root, entry)));
42
+ }
43
+ return out;
44
+ }
45
+ function isAlreadyPointer(filePath) {
46
+ if (!existsSync(filePath))
47
+ return false;
48
+ try {
49
+ const body = readFileSync(filePath, 'utf8');
50
+ return body.includes('Canonical peaks-cli 2.0 rules live at:');
51
+ }
52
+ catch {
53
+ return false;
54
+ }
55
+ }
56
+ const CANONICAL_2_0_DEV_PREFERENCE = `# Peaks-Cli dev preference (2.0 canonical)
57
+
58
+ > Project-local preference, captured from the 1.x install + re-rendered with the 2.0 vocabulary.
59
+ > Scope: applies to every iteration, adjustment, fix, or tweak on this project.
60
+ > Reading: read this **before** opening a new CLI command or routing a new feature through a CLI surface.
61
+
62
+ ## Rule 1 — Skill-first, CLI-auxiliary
63
+
64
+ When designing or modifying a peaks-cli feature, default to the **skill-first** design. CLI commands are **invoked by the skill prompt** when they are the right primitive: a side effect that must be atomic, a gate that must be machine-enforced, a probe that needs structured JSON, or a backstop that prevents the LLM from skipping a step. Behaviour only an LLM in a skill prompt would use lives **in the relevant skill's SKILL.md**, not as a new CLI command. See \`.claude/rules/common/dev-preference.md\` for the decision template.
65
+
66
+ ## Rule 2 — Dogfood on every adjustment
67
+
68
+ **Every adjustment, iteration, or fix-problem operation must be dogfood-tested in the current project before the work is declared complete.** No exceptions for "it's a small change", "just a comment update", or "just a SKILL.md line". The unit test suite is a subset of "current effect"; the dogfood is the full set. If a change passes unit tests but breaks a CLI command, the change is a regression.
69
+
70
+ ## Rule 3 — Commits belong to the human
71
+
72
+ **No AI co-author trailer.** The commit is the human's. **Identity is global gitconfig only** (\`~/.gitconfig\`). Do not set, override, or shadow \`user.name\` / \`user.email\` at the repo level, via env vars, or via \`git -c user.*=...\`. The commit's recorded author and committer must both equal the global identity.
73
+ `;
74
+ const CANONICAL_2_0_CODING_STYLE_TS = `# TypeScript Coding Standards (2.0 canonical)
75
+
76
+ > Project-local standards, derived from the 1.x install + re-rendered with the 2.0 vocabulary.
77
+
78
+ - Apply project-local conventions before generic typescript guidance.
79
+ - Keep public APIs typed or documented according to typescript ecosystem norms.
80
+ - Do not add new \`any\` types; use explicit domain types, generics, or \`unknown\` with narrowing.
81
+ - Prefer standard tooling and existing project scripts for formatting, linting, tests, and coverage.
82
+ - peaks-rd must check this file before planning code changes in typescript projects.
83
+ `;
84
+ const CANONICAL_2_0_COMMON_FILES = [
85
+ { relPath: 'common/dev-preference.md', content: CANONICAL_2_0_DEV_PREFERENCE },
86
+ {
87
+ relPath: 'common/coding-style.md',
88
+ content: '# Coding Standards (2.0 canonical)\n\n- Prefer simple, readable code over clever abstractions.\n- Keep functions focused and files cohesive.\n- Use immutable updates unless a language-specific convention explicitly favors mutation.\n- Validate user input, external data, file paths, and configuration at system boundaries.\n- Preserve existing project conventions when they are stricter than this baseline.\n',
89
+ },
90
+ {
91
+ relPath: 'common/code-review.md',
92
+ content: '# Code Review Standards (2.0 canonical)\n\n- Review diffs for correctness, maintainability, test coverage, and regression risk.\n- Treat missing tests for changed behavior as a blocker unless the change is documentation-only.\n- Verify code paths that handle filesystem, external APIs, credentials, user input, or generated artifacts.\n',
93
+ },
94
+ {
95
+ relPath: 'common/security.md',
96
+ content: '# Security Review Standards (2.0 canonical)\n\n- Never hardcode secrets, API keys, passwords, tokens, or credentials.\n- Do not send private code or secrets to external services without explicit user authorization.\n- Guard filesystem writes against path traversal, symlink, and junction escapes.\n- Require explicit confirmation for destructive actions, external state changes, and credential use.\n',
97
+ },
98
+ { relPath: 'typescript/coding-style.md', content: CANONICAL_2_0_CODING_STYLE_TS },
99
+ ];
100
+ export function migrateClaudeRules(input) {
101
+ const projectRoot = input.projectRoot;
102
+ const apply = input.apply === true;
103
+ const warnings = [];
104
+ const nextActions = [];
105
+ const claudeRulesDir = join(projectRoot, '.claude', 'rules');
106
+ const peaksStandardsDir = join(projectRoot, '.peaks', 'standards');
107
+ const canonicalRelPath = '.peaks/standards/';
108
+ const existingRulesFiles = readMarkdownFilesRecursive(claudeRulesDir);
109
+ const thickFiles = existingRulesFiles.filter((f) => !isAlreadyPointer(f));
110
+ const hasThickFiles = thickFiles.length > 0;
111
+ // The backup path is computed eagerly (so dry-run can preview
112
+ // the would-create location) but only created on disk in
113
+ // apply mode. In dry-run mode we still return the path so
114
+ // the user can see where the backup will land.
115
+ const computedBackupPath = hasThickFiles ? join(claudeRulesDir, `.peaks-2.0-backup-${timestampSlug()}`) : null;
116
+ const backupPath = apply ? computedBackupPath : null;
117
+ const thinnedFiles = [];
118
+ const scaffoldedFiles = [];
119
+ const preservedFiles = [];
120
+ // wouldChange is true iff there is at least one thick file to
121
+ // thin. An empty .claude/rules/ is NOT a wouldChange (no-op).
122
+ const wouldChange = hasThickFiles;
123
+ if (apply && hasThickFiles) {
124
+ // Step 1: backup
125
+ if (backupPath !== null) {
126
+ try {
127
+ mkdirSync(backupPath, { recursive: true });
128
+ for (const file of thickFiles) {
129
+ const body = readFileSync(file, 'utf8');
130
+ const rel = file.slice(claudeRulesDir.length + 1);
131
+ writeFileSync(join(backupPath, rel), body, 'utf8');
132
+ }
133
+ }
134
+ catch (err) {
135
+ warnings.push(`Backup step failed: ${err instanceof Error ? err.message : String(err)}`);
136
+ }
137
+ }
138
+ // Step 2: replace each .md with a 2-line pointer
139
+ for (const file of thickFiles) {
140
+ try {
141
+ writeFileSync(file, POINTER_TEXT(canonicalRelPath), 'utf8');
142
+ thinnedFiles.push(file);
143
+ }
144
+ catch (err) {
145
+ warnings.push(`Thin step failed for ${file}: ${err instanceof Error ? err.message : String(err)}`);
146
+ }
147
+ }
148
+ // Step 3: scaffold .peaks/standards/ — never overwrite existing
149
+ for (const file of CANONICAL_2_0_COMMON_FILES) {
150
+ const dest = join(peaksStandardsDir, file.relPath);
151
+ if (existsSync(dest)) {
152
+ preservedFiles.push(dest);
153
+ continue;
154
+ }
155
+ try {
156
+ mkdirSync(join(dest, '..'), { recursive: true });
157
+ writeFileSync(dest, file.content, 'utf8');
158
+ scaffoldedFiles.push(dest);
159
+ }
160
+ catch (err) {
161
+ warnings.push(`Scaffold step failed for ${file.relPath}: ${err instanceof Error ? err.message : String(err)}`);
162
+ }
163
+ }
164
+ }
165
+ if (thinnedFiles.length > 0) {
166
+ nextActions.push(`Thinned ${thinnedFiles.length} .md file(s) under .claude/rules (recursive) → 2-line pointer.`);
167
+ }
168
+ if (scaffoldedFiles.length > 0) {
169
+ nextActions.push(`Scaffolded ${scaffoldedFiles.length} 2.0 canonical rule(s) at .peaks/standards/.`);
170
+ }
171
+ if (preservedFiles.length > 0) {
172
+ nextActions.push(`Preserved ${preservedFiles.length} existing .peaks/standards/ file(s) (no overwrite).`);
173
+ }
174
+ if (backupPath !== null) {
175
+ nextActions.push(`Backup at ${backupPath} (git-ignored).`);
176
+ }
177
+ if (!apply && wouldChange) {
178
+ nextActions.push('Re-run with --apply to perform the migration.');
179
+ }
180
+ return {
181
+ ok: true,
182
+ data: {
183
+ backupPath,
184
+ thinnedFiles,
185
+ scaffoldedFiles,
186
+ preservedFiles,
187
+ wouldChange,
188
+ applied: apply && hasThickFiles,
189
+ nextActions,
190
+ },
191
+ warnings,
192
+ };
193
+ }
@@ -2,6 +2,7 @@ import { stat } from 'node:fs/promises';
2
2
  import { join } from 'node:path';
3
3
  import { isDirectory, pathExists, readText } from '../../shared/fs.js';
4
4
  import { getErrorMessage } from '../../shared/result.js';
5
+ import { loadPreferences } from '../preferences/preferences-service.js';
5
6
  function defaultArtifactDir(projectRoot) {
6
7
  return join(projectRoot, '.understand-anything');
7
8
  }
@@ -54,19 +55,31 @@ async function readGraph(graphPath) {
54
55
  export async function scanUnderstandAnything(options) {
55
56
  const artifactDir = options.artifactDir ?? defaultArtifactDir(options.projectRoot);
56
57
  const exists = await isDirectory(artifactDir);
58
+ // L3.1: read uaPrompt from preferences.json (graceful — returns 'unset' if missing/broken)
59
+ const uaPrompt = await readUaPrompt(options.projectRoot);
57
60
  if (!exists) {
58
61
  return {
59
62
  exists: false,
60
63
  artifactDir,
61
64
  graph: { exists: false, path: join(artifactDir, 'knowledge-graph.json') },
62
65
  intermediate: { exists: false, path: join(artifactDir, 'intermediate') },
63
- diffOverlay: { exists: false, path: join(artifactDir, 'diff-overlay.json') }
66
+ diffOverlay: { exists: false, path: join(artifactDir, 'diff-overlay.json') },
67
+ uaPrompt
64
68
  };
65
69
  }
66
70
  const graph = await readGraph(join(artifactDir, 'knowledge-graph.json'));
67
71
  const intermediate = await readFlag(join(artifactDir, 'intermediate'));
68
72
  const diffOverlay = await readFlag(join(artifactDir, 'diff-overlay.json'));
69
- return { exists: true, artifactDir, graph, intermediate, diffOverlay };
73
+ return { exists: true, artifactDir, graph, intermediate, diffOverlay, uaPrompt };
74
+ }
75
+ async function readUaPrompt(projectRoot) {
76
+ try {
77
+ const prefs = await loadPreferences(projectRoot);
78
+ return prefs.uaPrompt;
79
+ }
80
+ catch {
81
+ return 'unset';
82
+ }
70
83
  }
71
84
  function pickStringId(value) {
72
85
  if (value === null || typeof value !== 'object' || Array.isArray(value)) {
@@ -15,10 +15,36 @@ export type UnderstandFlagReport = {
15
15
  exists: boolean;
16
16
  path: string;
17
17
  };
18
+ /**
19
+ * Slice L3.1 — UA opt-in UX state. 'unset' triggers an opt-in prompt on
20
+ * first scan; 'skip-this-session' suppresses the prompt for the current
21
+ * session; 'skip-forever' writes to .peaks/preferences.json to suppress
22
+ * all future prompts. Mirrors preferences.json:uaPrompt.
23
+ */
24
+ export type UaPromptDecision = 'unset' | 'skip-this-session' | 'skip-forever';
18
25
  export type UnderstandScanReport = {
19
26
  exists: boolean;
20
27
  artifactDir: string;
21
28
  graph: UnderstandGraphReport;
22
29
  intermediate: UnderstandFlagReport;
23
30
  diffOverlay: UnderstandFlagReport;
31
+ /** Slice L3.1: opt-in UX state from preferences.json:uaPrompt. */
32
+ readonly uaPrompt?: UaPromptDecision;
24
33
  };
34
+ /**
35
+ * Slice L3.1 — opt-in prompt payload. When uaPrompt === 'unset' and UA is
36
+ * absent, the peaks-solo / peaks-ide layer surfaces this to the user via
37
+ * AskUserQuestion. The CLI does not prompt directly; it returns this
38
+ * payload so the LLM-side UX layer can decide.
39
+ */
40
+ export interface UaOptInPrompt {
41
+ readonly version: 1;
42
+ readonly tool: 'ua-opt-in';
43
+ readonly artifactDir: string;
44
+ readonly reason: 'ua-artifact-missing';
45
+ readonly options: readonly {
46
+ readonly id: 'install' | 'fallback-this-session' | 'fallback-forever';
47
+ readonly label: string;
48
+ readonly description: string;
49
+ }[];
50
+ }
@@ -0,0 +1,7 @@
1
+ export interface OneXState {
2
+ readonly isOneX: boolean;
3
+ readonly signals: readonly string[];
4
+ readonly projectRoot: string | null;
5
+ readonly configPath: string | null;
6
+ }
7
+ export declare function detect1xProjectState(cwd?: string): OneXState;
@@ -0,0 +1,94 @@
1
+ /**
2
+ * 1.x → 2.0 detection service — TypeScript mirror of
3
+ * `scripts/install-skills.mjs:detect1xProjectState`.
4
+ *
5
+ * Slice: 2026-06-12-solo-step-0-55-1x-detection.
6
+ *
7
+ * The canonical implementation lives in the .mjs postinstall
8
+ * (because the postinstall runs before any TS compile step).
9
+ * This TS mirror exists so the peaks-solo skill can call
10
+ * `peaks upgrade --detect-1x --project <root> --json` and
11
+ * read a structured JSON envelope to gate the
12
+ * AskUserQuestion that prompts the 1.x → 2.0 upgrade.
13
+ *
14
+ * The two implementations MUST stay in parity. The
15
+ * `tests/integration/upgrade/1x-detector-parity.test.ts`
16
+ * test exercises both on the same fixture and asserts
17
+ * their outputs match.
18
+ */
19
+ import { existsSync, readFileSync } from 'node:fs';
20
+ import { homedir } from 'node:os';
21
+ import { dirname, join } from 'node:path';
22
+ const MAX_WALK_UP = 8;
23
+ export function detect1xProjectState(cwd = process.cwd()) {
24
+ const home = homedir();
25
+ const signals = [];
26
+ let projectRoot = null;
27
+ let configPath = null;
28
+ // Walk up from cwd looking for .peaks/_runtime (signals
29
+ // we're inside a peaks project).
30
+ let dir = cwd;
31
+ for (let i = 0; i < MAX_WALK_UP; i += 1) {
32
+ const peaksRuntime = join(dir, '.peaks', '_runtime');
33
+ if (existsSync(peaksRuntime)) {
34
+ projectRoot = dir;
35
+ break;
36
+ }
37
+ const parent = dirname(dir);
38
+ if (parent === dir)
39
+ break;
40
+ dir = parent;
41
+ }
42
+ // Signal 1: ~/.peaks/config.json with 1.x version
43
+ const globalConfig = join(home, '.peaks', 'config.json');
44
+ if (existsSync(globalConfig)) {
45
+ try {
46
+ const raw = JSON.parse(readFileSync(globalConfig, 'utf8'));
47
+ if (typeof raw['version'] === 'string' && /^1\./.test(raw['version'])) {
48
+ signals.push(`global config at ${globalConfig} is 1.x (${raw['version']})`);
49
+ if (configPath === null)
50
+ configPath = globalConfig;
51
+ }
52
+ }
53
+ catch {
54
+ // ignore parse error — the 1.x detection is best-effort
55
+ }
56
+ }
57
+ // Signal 2: .claude/rules/common/dev-preference.md with peaks progress
58
+ if (projectRoot !== null) {
59
+ const devPref = join(projectRoot, '.claude', 'rules', 'common', 'dev-preference.md');
60
+ if (existsSync(devPref)) {
61
+ try {
62
+ const body = readFileSync(devPref, 'utf8');
63
+ if (/peaks progress/i.test(body)) {
64
+ signals.push(`${devPref} references "peaks progress" (1.x CLI surface, removed in slice #014)`);
65
+ }
66
+ }
67
+ catch {
68
+ // ignore
69
+ }
70
+ }
71
+ // Signal 3: project preferences.json missing or 1.x
72
+ const prefs = join(projectRoot, '.peaks', 'preferences.json');
73
+ if (!existsSync(prefs)) {
74
+ signals.push(`${prefs} does not exist (1.x project never migrated)`);
75
+ }
76
+ else {
77
+ try {
78
+ const raw = JSON.parse(readFileSync(prefs, 'utf8'));
79
+ if (raw['schema_version'] !== '2.0.0') {
80
+ signals.push(`${prefs} has schema_version ${JSON.stringify(raw['schema_version'])}, expected '2.0.0'`);
81
+ }
82
+ }
83
+ catch {
84
+ signals.push(`${prefs} exists but is not valid JSON`);
85
+ }
86
+ }
87
+ }
88
+ return {
89
+ isOneX: signals.length > 0,
90
+ signals,
91
+ projectRoot,
92
+ configPath,
93
+ };
94
+ }