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
@@ -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
+ }
@@ -1,6 +1,6 @@
1
1
  export type BuildTool = 'umi' | 'next' | 'vite' | 'rsbuild' | 'rspack' | 'farm' | 'craco' | 'webpack' | 'gulp' | 'angular' | 'custom' | 'unknown';
2
2
  export type ComponentLibrary = {
3
- readonly name: 'antd' | 'antd-pro' | 'mui' | 'shadcn' | 'element-plus' | 'element-ui' | 'arco' | 'tdesign' | 'semi' | 'nextui' | 'chakra' | 'vant' | 'none';
3
+ readonly name: 'antd' | 'antd-pro' | 'mui' | 'element-plus' | 'element-ui' | 'arco' | 'tdesign' | 'semi' | 'nextui' | 'chakra' | 'vant' | 'none';
4
4
  readonly majorVersion?: string;
5
5
  readonly hasProSuite?: boolean;
6
6
  };
@@ -64,8 +64,6 @@ function detectComponentLibrary(deps) {
64
64
  }
65
65
  if ('@mui/material' in deps)
66
66
  return { name: 'mui', ...(majorOf(deps['@mui/material']) !== undefined ? { majorVersion: majorOf(deps['@mui/material']) } : {}) };
67
- if ('tailwindcss' in deps && Object.keys(deps).some((d) => d.startsWith('@radix-ui/')))
68
- return { name: 'shadcn' };
69
67
  if ('element-plus' in deps)
70
68
  return { name: 'element-plus' };
71
69
  if ('element-ui' in deps)
@@ -279,8 +277,6 @@ export function componentLibraryLabel(lib) {
279
277
  return 'Ant Design + Ant Design Pro';
280
278
  case 'mui':
281
279
  return 'Material UI';
282
- case 'shadcn':
283
- return 'shadcn/ui (Tailwind + Radix)';
284
280
  case 'element-plus':
285
281
  return 'Element Plus';
286
282
  case 'element-ui':
@@ -159,8 +159,6 @@ function renderCommonCodingStyle(ctx) {
159
159
  }
160
160
  if (lib === 'mui')
161
161
  stackRules.push('- Style MUI via `sx`, `styled()`, and `theme`. Do NOT apply TailwindCSS utility classes directly to MUI components.');
162
- if (lib === 'shadcn')
163
- stackRules.push('- Use existing shadcn component variants and Tailwind utility classes. Do not introduce a competing component library.');
164
162
  if (ctx.cssFrameworks.includes('tailwind') && (lib === 'antd' || lib === 'antd-pro' || lib === 'mui')) {
165
163
  stackRules.push('- TailwindCSS is for layout/utility only; component-library tokens own component styling.');
166
164
  }
@@ -185,7 +183,7 @@ function renderCodeReview(ctx) {
185
183
  const extra = [];
186
184
  const lib = ctx.componentLibrary.name;
187
185
  if (lib === 'antd' || lib === 'antd-pro') {
188
- extra.push('- Block PRs that introduce a second component library (MUI/shadcn/Chakra) alongside antd.');
186
+ extra.push('- Block PRs that introduce a second component library (MUI/Chakra) alongside antd.');
189
187
  extra.push('- Block PRs that import antd v3/v4 APIs in this v5 project, or vice versa.');
190
188
  }
191
189
  if (ctx.cssFrameworks.includes('tailwind') && (lib === 'antd' || lib === 'antd-pro' || lib === 'mui')) {
@@ -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
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * The 2.0 canonical ignore block. Mirrors the same paths
3
+ * peaks-cli's own .gitignore uses; kept in lockstep manually
4
+ * (the peaks-cli root .gitignore is the source of truth).
5
+ *
6
+ * Sentinel: every emitted line carries the marker comment so
7
+ * the migration can detect "already-migrated" and stay
8
+ * idempotent.
9
+ */
10
+ export declare const PEAKS_2_0_BLOCK_SENTINEL = "# peaks-cli 2.0 canonical ignore block \u2014 managed by `peaks upgrade --to 2.0`";
11
+ export declare const CANONICAL_2_0_PEAKS_BLOCK: string;
12
+ /**
13
+ * Returns true when `line` is a wholesale ignore of the entire
14
+ * `.peaks/` tree (the 1.x pattern that breaks 2.0 tracking).
15
+ *
16
+ * Examples that match: `.peaks` `.peaks/` `/.peaks` `/.peaks/` ` .peaks/ `
17
+ * Examples that DON'T: `.peaks/_runtime/` `# .peaks/ comment` `.peaks_other`
18
+ */
19
+ export declare function isStaleWholesalePeaksRule(line: string): boolean;
20
+ export interface MigrateGitignoreContentResult {
21
+ /** True if at least one stale wholesale .peaks rule was removed OR the canonical block was appended. */
22
+ readonly changed: boolean;
23
+ /** The exact lines that were removed (verbatim, with original whitespace). */
24
+ readonly removedRules: readonly string[];
25
+ /** The migrated content (always ends with a newline). */
26
+ readonly content: string;
27
+ }
28
+ /**
29
+ * Pure function: takes the .gitignore content, returns the
30
+ * migrated content + diff summary. No I/O.
31
+ */
32
+ export declare function migrateGitignoreContent(input: string): MigrateGitignoreContentResult;
33
+ export interface MigrateGitignoreFileInput {
34
+ readonly projectRoot: string;
35
+ readonly apply?: boolean;
36
+ }
37
+ export interface MigrateGitignoreFileResult {
38
+ /** True if .gitignore is absent (nothing to migrate). */
39
+ readonly missing: boolean;
40
+ /** True if the migration would change the file. */
41
+ readonly changed: boolean;
42
+ /** True only if `apply: true` AND `changed: true`. */
43
+ readonly appliedWrite: boolean;
44
+ /** Absolute path to the timestamped backup file, or null. */
45
+ readonly backupPath: string | null;
46
+ /** Removed lines (verbatim). */
47
+ readonly removedRules: readonly string[];
48
+ }
49
+ /**
50
+ * FS variant: reads .gitignore, runs migration, optionally
51
+ * writes the result + a timestamped backup of the original.
52
+ *
53
+ * Date.now() is used for the backup suffix; in test environments
54
+ * where determinism matters, the caller can stub Date via vi.
55
+ */
56
+ export declare function migrateGitignoreFile(input: MigrateGitignoreFileInput): MigrateGitignoreFileResult;
@@ -0,0 +1,170 @@
1
+ /**
2
+ * .gitignore 1.x → 2.0 migration service.
3
+ *
4
+ * Real bug surfaced by ice-cola dogfood 2026-06-12:
5
+ * - 1.x consumer projects often have a wholesale `/.peaks/` (or
6
+ * `.peaks/` / `.peaks`) entry in their .gitignore — the 1.x
7
+ * install treated .peaks/ as the tool's ephemeral working
8
+ * directory.
9
+ * - In 2.0 the .peaks/ tree is split: most subdirectories
10
+ * remain ignored (_runtime, _sub_agents, audit, etc.) but
11
+ * several MUST be tracked: .peaks/standards/,
12
+ * .peaks/memory/*.md (durable LLM-authored memories),
13
+ * .peaks/PROJECT.md, and the user opt-in dotfiles.
14
+ * - A wholesale `.peaks/` ignore silently hides every 2.0
15
+ * tracked artifact, violating the "one-key completion" tenet
16
+ * because the user runs upgrade, sees 6/6 pass, then git
17
+ * status shows no new tracked files — the upgrade looks
18
+ * "done" but nothing landed in git.
19
+ *
20
+ * The service:
21
+ * 1. Reads the project's .gitignore (string in / string out;
22
+ * no I/O in the pure function).
23
+ * 2. Detects every line that is a wholesale-ignore of .peaks/
24
+ * (with or without leading slash, with or without trailing
25
+ * slash, ignoring surrounding whitespace).
26
+ * 3. Removes those lines and appends the canonical 2.0 block
27
+ * (granular subpaths) if not already present.
28
+ * 4. The FS variant `migrateGitignoreFile` adds backup +
29
+ * atomic write on top.
30
+ *
31
+ * Idempotent: re-running on a migrated .gitignore is a no-op.
32
+ *
33
+ * Spec reference: docs/superpowers/specs/2026-06-11-peaks-cli-l1-l2-l3-redesign.md §8.4
34
+ * (per-project state in .peaks/preferences.json, durable
35
+ * memories in .peaks/memory/*.md).
36
+ */
37
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
38
+ import { join } from 'node:path';
39
+ /**
40
+ * The 2.0 canonical ignore block. Mirrors the same paths
41
+ * peaks-cli's own .gitignore uses; kept in lockstep manually
42
+ * (the peaks-cli root .gitignore is the source of truth).
43
+ *
44
+ * Sentinel: every emitted line carries the marker comment so
45
+ * the migration can detect "already-migrated" and stay
46
+ * idempotent.
47
+ */
48
+ export const PEAKS_2_0_BLOCK_SENTINEL = '# peaks-cli 2.0 canonical ignore block — managed by `peaks upgrade --to 2.0`';
49
+ export const CANONICAL_2_0_PEAKS_BLOCK = [
50
+ PEAKS_2_0_BLOCK_SENTINEL,
51
+ '.peaks/_runtime/',
52
+ '.peaks/_dogfood/',
53
+ '.peaks/_sub_agents/',
54
+ '.peaks/audit/',
55
+ '.peaks/system/',
56
+ '.peaks/runtime/',
57
+ '.peaks/preferences.json',
58
+ '.peaks/memory/upgrade-2.0-*.md',
59
+ ].join('\n');
60
+ /**
61
+ * Returns true when `line` is a wholesale ignore of the entire
62
+ * `.peaks/` tree (the 1.x pattern that breaks 2.0 tracking).
63
+ *
64
+ * Examples that match: `.peaks` `.peaks/` `/.peaks` `/.peaks/` ` .peaks/ `
65
+ * Examples that DON'T: `.peaks/_runtime/` `# .peaks/ comment` `.peaks_other`
66
+ */
67
+ export function isStaleWholesalePeaksRule(line) {
68
+ const trimmed = line.trim();
69
+ if (trimmed.length === 0)
70
+ return false;
71
+ if (trimmed.startsWith('#'))
72
+ return false;
73
+ // Strip leading slash (relative-to-repo-root convention) and
74
+ // trailing slash (directory marker); both equivalent in
75
+ // .gitignore semantics for wholesale ignore.
76
+ const normalized = trimmed.replace(/^\//, '').replace(/\/$/, '');
77
+ return normalized === '.peaks';
78
+ }
79
+ /**
80
+ * Pure function: takes the .gitignore content, returns the
81
+ * migrated content + diff summary. No I/O.
82
+ */
83
+ export function migrateGitignoreContent(input) {
84
+ const lines = input.split('\n');
85
+ const removed = [];
86
+ const kept = [];
87
+ for (const line of lines) {
88
+ if (isStaleWholesalePeaksRule(line)) {
89
+ removed.push(line.trim());
90
+ }
91
+ else {
92
+ kept.push(line);
93
+ }
94
+ }
95
+ // Already-migrated detection: if the canonical block is
96
+ // already present AND nothing was removed, this is a no-op.
97
+ const hasCanonicalBlock = input.includes(PEAKS_2_0_BLOCK_SENTINEL);
98
+ if (removed.length === 0 && hasCanonicalBlock) {
99
+ return { changed: false, removedRules: [], content: input };
100
+ }
101
+ // Already-migrated AND nothing stale: no-op, return original
102
+ // verbatim so byte-equality holds for idempotency.
103
+ if (removed.length === 0 && !hasCanonicalBlock) {
104
+ // Check whether there's anything peaks-related at all; if
105
+ // not, treat as no-op (the user explicitly chose not to
106
+ // ignore .peaks/ — respect that).
107
+ return { changed: false, removedRules: [], content: input };
108
+ }
109
+ // Removed at least one stale rule. Append canonical block
110
+ // if not already present.
111
+ let body = kept.join('\n');
112
+ if (!body.endsWith('\n') && body.length > 0) {
113
+ body += '\n';
114
+ }
115
+ if (!hasCanonicalBlock) {
116
+ body += CANONICAL_2_0_PEAKS_BLOCK + '\n';
117
+ }
118
+ return { changed: true, removedRules: removed, content: body };
119
+ }
120
+ /**
121
+ * FS variant: reads .gitignore, runs migration, optionally
122
+ * writes the result + a timestamped backup of the original.
123
+ *
124
+ * Date.now() is used for the backup suffix; in test environments
125
+ * where determinism matters, the caller can stub Date via vi.
126
+ */
127
+ export function migrateGitignoreFile(input) {
128
+ const path = join(input.projectRoot, '.gitignore');
129
+ if (!existsSync(path)) {
130
+ return {
131
+ missing: true,
132
+ changed: false,
133
+ appliedWrite: false,
134
+ backupPath: null,
135
+ removedRules: [],
136
+ };
137
+ }
138
+ const before = readFileSync(path, 'utf8');
139
+ const result = migrateGitignoreContent(before);
140
+ if (!result.changed) {
141
+ return {
142
+ missing: false,
143
+ changed: false,
144
+ appliedWrite: false,
145
+ backupPath: null,
146
+ removedRules: [],
147
+ };
148
+ }
149
+ if (input.apply !== true) {
150
+ return {
151
+ missing: false,
152
+ changed: true,
153
+ appliedWrite: false,
154
+ backupPath: null,
155
+ removedRules: result.removedRules,
156
+ };
157
+ }
158
+ // Apply: write backup first, then atomic-overwrite the original.
159
+ const ts = new Date().toISOString().replace(/[:.]/g, '-');
160
+ const backupPath = join(input.projectRoot, `.gitignore.peaks-2.0-backup-${ts}`);
161
+ writeFileSync(backupPath, before, 'utf8');
162
+ writeFileSync(path, result.content, 'utf8');
163
+ return {
164
+ missing: false,
165
+ changed: true,
166
+ appliedWrite: true,
167
+ backupPath,
168
+ removedRules: result.removedRules,
169
+ };
170
+ }