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
@@ -1,2 +0,0 @@
1
- import type { SkillScopeAdapter } from '../types.js';
2
- export declare const TONGYI_SKILL_SCOPE: SkillScopeAdapter;
@@ -1,13 +0,0 @@
1
- // TODO(slice-025.6-tongyi): research Tongyi Lingma's per-project skill
2
- // scoping config format. Tongyi Lingma is an AI IDE by Alibaba; per-
3
- // project config dir is unconfirmed at slice 025.1.
4
- //
5
- // Until the real format is known, this stub:
6
- // 1. Writes `.peaks/scope/tongyi-lingma-skills.json` (source-of-truth)
7
- // on every `applyScope` so the user's intent is captured on disk.
8
- // 2. Returns NOT_SUPPORTED with a clear message pointing at this slice.
9
- //
10
- // When implementing, replace the makeStubAdapter call with the real
11
- // TongyiSkillScope class.
12
- import { makeStubAdapter } from './_stub-helper.js';
13
- export const TONGYI_SKILL_SCOPE = makeStubAdapter('tongyi-lingma', 'slice-025.6-tongyi', 'Tongyi Lingma');
@@ -1,2 +0,0 @@
1
- import type { SkillScopeAdapter } from '../types.js';
2
- export declare const TRAE_SKILL_SCOPE: SkillScopeAdapter;
@@ -1,12 +0,0 @@
1
- // TODO(slice-025.2-trae): research Trae's per-project skill scoping config
2
- // format. Likely candidates: `.trae/skills.json` or `.trae/settings.json`.
3
- //
4
- // Until the real format is known, this stub:
5
- // 1. Writes `.peaks/scope/trae-skills.json` (source-of-truth) on every
6
- // `applyScope` so the user's intent is captured on disk.
7
- // 2. Returns NOT_SUPPORTED with a clear message pointing at this slice.
8
- //
9
- // When implementing, replace the makeStubAdapter call with the real
10
- // TraeSkillScope class.
11
- import { makeStubAdapter } from './_stub-helper.js';
12
- export const TRAE_SKILL_SCOPE = makeStubAdapter('trae', 'slice-025.2-trae', 'Trae IDE');
@@ -1,81 +0,0 @@
1
- /**
2
- * Detection algorithm for `peaks skill scope`.
3
- *
4
- * Pure function: given a project root and the installed-skills path, the
5
- * algorithm produces a `DetectResult` (signals + per-skill classification
6
- * + counts). No filesystem writes, no randomness, no time-of-day. AC11.
7
- *
8
- * Three layers:
9
- * 1. `extractProjectSignals(projectRoot)` — read package.json + tsconfig +
10
- * file tree (top-50 extensions).
11
- * 2. `classifySkill(skill, signals, hardcodedRules)` — keyword matching
12
- * against the skill's SKILL.md description.
13
- * 3. `detectSkillScope({ projectRoot, installedSkillsPath })` — top-level
14
- * orchestrator that returns the JSON envelope (AC1).
15
- */
16
- import type { ProjectSignals, SkillKind, SkillScopeCounts, SkillScopeRecord } from './types.js';
17
- /**
18
- * Walk `src/` (recursively) AND the project root, collecting the top-50
19
- * unique file extensions (sorted lexicographically) AND the per-extension
20
- * file count (used by R003.1 to compute fractional share).
21
- */
22
- export interface ScanResult {
23
- readonly extensions: readonly string[];
24
- readonly counts: Readonly<Record<string, number>>;
25
- readonly totalFiles: number;
26
- }
27
- export declare function scanFileTree(projectRoot: string, maxExtensions?: number): ScanResult;
28
- /**
29
- * Build the `ProjectSignals` object from the project root.
30
- */
31
- export declare function extractProjectSignals(projectRoot: string): Promise<ProjectSignals>;
32
- export interface InstalledSkill {
33
- readonly name: string;
34
- readonly description: string;
35
- readonly skillPath: string;
36
- }
37
- interface HardcodedRules {
38
- readonly alwaysRelevant: ReadonlySet<string>;
39
- readonly nonTsPrefixes: readonly string[];
40
- }
41
- /**
42
- * Infer the `SkillKind` for a skill by name. Used for the JSON envelope.
43
- */
44
- export declare function inferSkillKind(name: string, alwaysRelevant: ReadonlySet<string>): SkillKind;
45
- /**
46
- * Classify a single skill given the project signals. Returns the relevance
47
- * + a list of human-readable reasons (stable for fixtures, so unit tests
48
- * can assert exact strings).
49
- */
50
- export declare function classifySkill(skill: InstalledSkill, signals: ProjectSignals, rules: HardcodedRules): SkillScopeRecord;
51
- export interface DetectInput {
52
- readonly projectRoot: string;
53
- readonly installedSkillsPath?: string;
54
- readonly detectedIde?: string | null;
55
- }
56
- export interface DetectResult {
57
- readonly detectedIde: string | null;
58
- readonly projectSignals: ProjectSignals;
59
- readonly skills: readonly SkillScopeRecord[];
60
- readonly counts: SkillScopeCounts;
61
- }
62
- /**
63
- * Discover the installed skills under `installedSkillsPath` (default:
64
- * `~/.claude/skills`). Each subdir containing a SKILL.md counts as an
65
- * installed skill.
66
- */
67
- export declare function listInstalledSkills(installedSkillsPath: string): Promise<InstalledSkill[]>;
68
- /**
69
- * The default installed-skills path: `~/.claude/skills`. Resolved at call
70
- * time so the orchestrator stays pure-ish (no module-level side effects).
71
- */
72
- export declare function defaultInstalledSkillsPath(): string;
73
- /**
74
- * Top-level orchestrator. Reads package.json + tsconfig + file tree,
75
- * discovers installed skills, classifies each one, returns the JSON
76
- * envelope. Idempotent: same input → same output. No filesystem writes.
77
- */
78
- export declare function detectSkillScope(input: DetectInput): Promise<DetectResult>;
79
- /** Compute a stable summary hash (used by tests to assert no time-dependent fields). */
80
- export declare function detectSummary(result: DetectResult): string;
81
- export {};
@@ -1,513 +0,0 @@
1
- /**
2
- * Detection algorithm for `peaks skill scope`.
3
- *
4
- * Pure function: given a project root and the installed-skills path, the
5
- * algorithm produces a `DetectResult` (signals + per-skill classification
6
- * + counts). No filesystem writes, no randomness, no time-of-day. AC11.
7
- *
8
- * Three layers:
9
- * 1. `extractProjectSignals(projectRoot)` — read package.json + tsconfig +
10
- * file tree (top-50 extensions).
11
- * 2. `classifySkill(skill, signals, hardcodedRules)` — keyword matching
12
- * against the skill's SKILL.md description.
13
- * 3. `detectSkillScope({ projectRoot, installedSkillsPath })` — top-level
14
- * orchestrator that returns the JSON envelope (AC1).
15
- */
16
- import { existsSync, readdirSync, statSync } from 'node:fs';
17
- import { readFile } from 'node:fs/promises';
18
- import { join } from 'node:path';
19
- import { ALWAYS_RELEVANT_SKILLS, NON_TS_SKILL_PREFIXES, TRACKED_EXTENSIONS, readScopeThreshold, } from './types.js';
20
- function hasAnyDep(pkg, names) {
21
- const all = { ...(pkg.dependencies ?? {}), ...(pkg.devDependencies ?? {}) };
22
- return names.some((name) => Object.prototype.hasOwnProperty.call(all, name));
23
- }
24
- function parseNodeEngineMajor(enginesNode) {
25
- if (enginesNode === undefined)
26
- return null;
27
- // Match patterns like '>=20', '^20.0.0', '>=20.0.0 <21.0.0'
28
- const match = enginesNode.match(/(\d+)/);
29
- return match === null ? null : Number(match[1]);
30
- }
31
- /**
32
- * Read and parse a JSON file. Returns null on parse error or missing file.
33
- */
34
- async function readJson(path) {
35
- if (!existsSync(path))
36
- return null;
37
- try {
38
- const raw = await readFile(path, 'utf8');
39
- return JSON.parse(raw);
40
- }
41
- catch {
42
- return null;
43
- }
44
- }
45
- function asPackageJson(value) {
46
- if (value === null || typeof value !== 'object')
47
- return null;
48
- return value;
49
- }
50
- function asTsConfig(value) {
51
- if (value === null || typeof value !== 'object')
52
- return null;
53
- return value;
54
- }
55
- export function scanFileTree(projectRoot, maxExtensions = 50) {
56
- const roots = [];
57
- if (existsSync(join(projectRoot, 'src')))
58
- roots.push(join(projectRoot, 'src'));
59
- roots.push(projectRoot);
60
- const counts = {};
61
- // Bound the walk: at most 2000 files, 5 levels deep.
62
- let visited = 0;
63
- const MAX_FILES = 2000;
64
- const MAX_DEPTH = 5;
65
- for (const root of roots) {
66
- const stack = [root];
67
- while (stack.length > 0 && visited < MAX_FILES) {
68
- const dir = stack.pop();
69
- if (!existsSync(dir))
70
- continue;
71
- let entries;
72
- try {
73
- entries = readdirSync(dir, { withFileTypes: true, encoding: 'utf8' });
74
- }
75
- catch {
76
- continue;
77
- }
78
- for (const entry of entries) {
79
- const name = entry.name;
80
- if (typeof name !== 'string')
81
- continue;
82
- const full = join(dir, name);
83
- // Skip hidden dirs (e.g. node_modules, .git) and the fixture `skills/` dir.
84
- if (name === 'node_modules' || name === '.git' || name === 'skills' || name === 'dist')
85
- continue;
86
- if (entry.isDirectory()) {
87
- const depth = full.split(/[/\\]/).length - projectRoot.split(/[/\\]/).length;
88
- if (depth < MAX_DEPTH)
89
- stack.push(full);
90
- }
91
- else if (entry.isFile()) {
92
- visited += 1;
93
- if (visited >= MAX_FILES)
94
- break;
95
- const dot = name.lastIndexOf('.');
96
- if (dot < 0)
97
- continue;
98
- const ext = name.slice(dot).toLowerCase();
99
- counts[ext] = (counts[ext] ?? 0) + 1;
100
- }
101
- }
102
- }
103
- }
104
- const extensions = Object.keys(counts).sort().slice(0, maxExtensions);
105
- return { extensions, counts, totalFiles: visited };
106
- }
107
- function hasExt(exts, ext) {
108
- return exts.includes(ext);
109
- }
110
- /**
111
- * Build the `ProjectSignals` object from the project root.
112
- */
113
- export async function extractProjectSignals(projectRoot) {
114
- const pkgRaw = await readJson(join(projectRoot, 'package.json'));
115
- const pkg = asPackageJson(pkgRaw);
116
- const hasPackageJson = pkg !== null;
117
- const isTypeScript = hasPackageJson &&
118
- (hasAnyDep(pkg, ['typescript', 'tsx', '@types/node']) ||
119
- existsSync(join(projectRoot, 'tsconfig.json')));
120
- const tsRaw = await readJson(join(projectRoot, 'tsconfig.json'));
121
- const tsConfig = asTsConfig(tsRaw);
122
- const isTypeScriptESM = (pkg?.type === 'module') ||
123
- (tsConfig?.compilerOptions?.module !== undefined &&
124
- ['ESNext', 'NodeNext', 'ES2022'].includes(tsConfig.compilerOptions.module));
125
- const isReact = pkg !== null && hasAnyDep(pkg, ['react', 'react-dom', 'preact']);
126
- const isVue = pkg !== null && hasAnyDep(pkg, ['vue']);
127
- const isSvelte = pkg !== null && hasAnyDep(pkg, ['svelte']);
128
- const isNext = pkg !== null && hasAnyDep(pkg, ['next']);
129
- const isNestJS = pkg !== null && hasAnyDep(pkg, ['@nestjs/core', '@nestjs/common']);
130
- const isExpress = pkg !== null && hasAnyDep(pkg, ['express']);
131
- const isFastify = pkg !== null && hasAnyDep(pkg, ['fastify']);
132
- const isPostgres = pkg !== null && (hasAnyDep(pkg, ['pg', 'postgres', 'postgresql', 'prisma', '@prisma/client']));
133
- const isMysql = pkg !== null && hasAnyDep(pkg, ['mysql', 'mysql2']);
134
- const isMongo = pkg !== null && hasAnyDep(pkg, ['mongodb', 'mongoose']);
135
- const isRedis = pkg !== null && hasAnyDep(pkg, ['redis', 'ioredis']);
136
- const isDocker = existsSync(join(projectRoot, 'Dockerfile')) ||
137
- existsSync(join(projectRoot, 'docker-compose.yml')) ||
138
- existsSync(join(projectRoot, 'docker-compose.yaml'));
139
- const isK8s = existsSync(join(projectRoot, 'k8s')) ||
140
- existsSync(join(projectRoot, 'kubernetes')) ||
141
- existsSync(join(projectRoot, 'deployment.yaml')) ||
142
- existsSync(join(projectRoot, 'deployment.yml'));
143
- const isCommander = pkg !== null && hasAnyDep(pkg, ['commander']);
144
- // Detect Python projects (requirements.txt, pyproject.toml, setup.py, .py presence).
145
- const isPython = !hasPackageJson ||
146
- existsSync(join(projectRoot, 'requirements.txt')) ||
147
- existsSync(join(projectRoot, 'pyproject.toml')) ||
148
- existsSync(join(projectRoot, 'setup.py'));
149
- const isCodegraph = pkg !== null && hasAnyDep(pkg, ['@colbymchenry/codegraph']);
150
- const isHeadroom = pkg !== null && (hasAnyDep(pkg, ['headroom-ai']) ||
151
- Object.keys({ ...(pkg.dependencies ?? {}), ...(pkg.devDependencies ?? {}) }).some((k) => k.startsWith('@headroom/')));
152
- const nodeEngineMajor = parseNodeEngineMajor(pkg?.engines?.node);
153
- const scan = scanFileTree(projectRoot);
154
- const topExtensions = scan.extensions;
155
- // Build the per-extension presence flag map (R003.1: kept for backwards-compat).
156
- const hasFileExtension = {};
157
- for (const ext of TRACKED_EXTENSIONS) {
158
- hasFileExtension[ext.slice(1)] = hasExt(topExtensions, ext);
159
- }
160
- // Build the per-extension fractional share map (R003.1).
161
- const shareByExtension = {};
162
- if (scan.totalFiles > 0) {
163
- for (const [ext, count] of Object.entries(scan.counts)) {
164
- shareByExtension[ext.slice(1)] = count / scan.totalFiles;
165
- }
166
- }
167
- return {
168
- hasPackageJson,
169
- isTypeScript,
170
- isTypeScriptESM,
171
- isReact,
172
- isVue,
173
- isSvelte,
174
- isNext,
175
- isNestJS,
176
- isExpress,
177
- isFastify,
178
- isPostgres,
179
- isMysql,
180
- isMongo,
181
- isRedis,
182
- isDocker,
183
- isK8s,
184
- isCommander,
185
- isCodegraph,
186
- isHeadroom,
187
- isPython,
188
- nodeEngineMajor,
189
- topExtensions,
190
- hasFileExtension,
191
- shareByExtension,
192
- };
193
- }
194
- /**
195
- * Infer the `SkillKind` for a skill by name. Used for the JSON envelope.
196
- */
197
- export function inferSkillKind(name, alwaysRelevant) {
198
- if (alwaysRelevant.has(name) && name.startsWith('peaks-'))
199
- return 'peaks-family';
200
- if (alwaysRelevant.has(name))
201
- return 'generic-ai';
202
- if (NON_TS_SKILL_PREFIXES.some((p) => name.startsWith(p)))
203
- return 'language-specific';
204
- return 'other';
205
- }
206
- /**
207
- * Classify a single skill given the project signals. Returns the relevance
208
- * + a list of human-readable reasons (stable for fixtures, so unit tests
209
- * can assert exact strings).
210
- */
211
- export function classifySkill(skill, signals, rules) {
212
- const reasons = [];
213
- // 1. Hard-coded allowlist always wins.
214
- if (rules.alwaysRelevant.has(skill.name)) {
215
- reasons.push('hard-coded always-relevant');
216
- return {
217
- name: skill.name,
218
- kind: inferSkillKind(skill.name, rules.alwaysRelevant),
219
- relevance: 'relevant',
220
- reasons,
221
- };
222
- }
223
- // 2. Non-TS prefix → irrelevant when the project is TS.
224
- if (rules.nonTsPrefixes.some((prefix) => skill.name.startsWith(prefix))) {
225
- if (signals.isTypeScript && !isNonTsProject(signals)) {
226
- reasons.push('non-TS skill prefix; project is TS');
227
- return {
228
- name: skill.name,
229
- kind: 'language-specific',
230
- relevance: 'irrelevant',
231
- reasons,
232
- };
233
- }
234
- }
235
- // 3. Keyword matching against the description (strong + weak hits).
236
- const desc = skill.description.toLowerCase();
237
- // Special-case: when the project is a non-TS project (Python, etc.),
238
- // language-specific skills with matching keywords should be relevant.
239
- // R003.1: gate this on the language's fractional share >= threshold,
240
- // so a 1-file stray `.cpp` does not flip cpp-coding-standards to relevant.
241
- if (isNonTsProject(signals)) {
242
- const langMatch = languageKeywordMatch(desc);
243
- if (langMatch !== null) {
244
- const ext = LANGUAGE_TO_EXTENSION[langMatch];
245
- const share = ext === undefined ? 1 : (signals.shareByExtension?.[ext] ?? 0);
246
- const threshold = readScopeThreshold();
247
- if (share < threshold) {
248
- reasons.push(`${langMatch} keyword but share ${(share * 100).toFixed(1)}% < threshold ${(threshold * 100).toFixed(0)}%`);
249
- return {
250
- name: skill.name,
251
- kind: inferSkillKind(skill.name, rules.alwaysRelevant),
252
- relevance: 'irrelevant',
253
- reasons,
254
- };
255
- }
256
- reasons.push(`${langMatch} keyword + non-TS project (share ${(share * 100).toFixed(1)}%)`);
257
- return {
258
- name: skill.name,
259
- kind: inferSkillKind(skill.name, rules.alwaysRelevant),
260
- relevance: 'relevant',
261
- reasons,
262
- };
263
- }
264
- }
265
- const strong = strongMatches(desc, signals);
266
- const weak = weakMatches(desc, signals);
267
- if (strong.length > 0) {
268
- reasons.push(...strong);
269
- return {
270
- name: skill.name,
271
- kind: inferSkillKind(skill.name, rules.alwaysRelevant),
272
- relevance: 'relevant',
273
- reasons,
274
- };
275
- }
276
- if (weak.length > 0) {
277
- reasons.push(...weak);
278
- return {
279
- name: skill.name,
280
- kind: inferSkillKind(skill.name, rules.alwaysRelevant),
281
- relevance: 'borderline',
282
- reasons,
283
- };
284
- }
285
- return {
286
- name: skill.name,
287
- kind: inferSkillKind(skill.name, rules.alwaysRelevant),
288
- relevance: 'irrelevant',
289
- reasons: ['no project-signal match'],
290
- };
291
- }
292
- /**
293
- * Strong matches: keyword in description that maps to a confirmed project signal.
294
- */
295
- function strongMatches(description, signals) {
296
- const matches = [];
297
- if (signals.isReact && /\breact\b/.test(description))
298
- matches.push('react project + react skill');
299
- if (signals.isVue && /\bvue\b/.test(description))
300
- matches.push('vue project + vue skill');
301
- if (signals.isSvelte && /\bsvelte\b/.test(description))
302
- matches.push('svelte project + svelte skill');
303
- if (signals.isNext && /\bnext\.?js\b|\bnextjs\b/.test(description))
304
- matches.push('next project + nextjs skill');
305
- if (signals.isNestJS && /\bnest\.?js\b|\bnestjs\b/.test(description))
306
- matches.push('nestjs project + nestjs skill');
307
- if (signals.isExpress && /\bexpress\b/.test(description))
308
- matches.push('express project + express skill');
309
- if (signals.isFastify && /\bfastify\b/.test(description))
310
- matches.push('fastify project + fastify skill');
311
- if (signals.isPostgres && /\bpostgres|\bpostgresql\b/.test(description))
312
- matches.push('postgres project + postgres skill');
313
- if (signals.isMysql && /\bmysql\b/.test(description))
314
- matches.push('mysql project + mysql skill');
315
- if (signals.isMongo && /\bmongo(?:db)?\b/.test(description))
316
- matches.push('mongo project + mongo skill');
317
- if (signals.isRedis && /\bredis\b/.test(description))
318
- matches.push('redis project + redis skill');
319
- if (signals.isDocker && /\bdocker\b/.test(description))
320
- matches.push('docker project + docker skill');
321
- if (signals.isK8s && /\bkubernetes\b|\bk8s\b/.test(description))
322
- matches.push('k8s project + k8s skill');
323
- if (signals.isCommander && /\bcommander\b|\bcli\b/.test(description))
324
- matches.push('cli project + cli skill');
325
- if (/\btdd\b|\btest-driven\b/.test(description))
326
- matches.push('tdd keyword (always relevant)');
327
- if (/\brefactor\b/.test(description))
328
- matches.push('refactor keyword (always relevant)');
329
- return matches;
330
- }
331
- /**
332
- * Weak matches: keyword that's a hint but not a confirmed signal.
333
- */
334
- function weakMatches(description, signals) {
335
- const matches = [];
336
- if (/\bfrontend\b/.test(description) &&
337
- (signals.isReact || signals.isVue || signals.isSvelte || signals.isNext)) {
338
- matches.push('frontend keyword + frontend project');
339
- }
340
- if (/\bbackend\b/.test(description) &&
341
- (signals.isNestJS || signals.isExpress || signals.isFastify)) {
342
- matches.push('backend keyword + backend project');
343
- }
344
- if (/\bdatabase\b/.test(description) &&
345
- (signals.isPostgres || signals.isMysql || signals.isMongo || signals.isRedis)) {
346
- matches.push('database keyword + db project');
347
- }
348
- return matches;
349
- }
350
- /**
351
- * Map a skill description to a non-TS language when the description
352
- * explicitly mentions that language. Returns null when there's no match.
353
- */
354
- function languageKeywordMatch(description) {
355
- if (/\bpython\b/.test(description))
356
- return 'python';
357
- if (/\bkotlin\b/.test(description))
358
- return 'kotlin';
359
- if (/\bjava\b/.test(description))
360
- return 'java';
361
- if (/\brust\b/.test(description))
362
- return 'rust';
363
- if (/\bgo\b|\bgolang\b/.test(description))
364
- return 'go';
365
- if (/\bruby\b/.test(description))
366
- return 'ruby';
367
- if (/\bswift\b/.test(description))
368
- return 'swift';
369
- if (/\bc#\b|\bcsharp\b/.test(description))
370
- return 'csharp';
371
- if (/\bc\+\+|\bcpp\b/.test(description))
372
- return 'cpp';
373
- return null;
374
- }
375
- /**
376
- * R003.1: map a non-TS language keyword to its primary file extension
377
- * (used to look up the fractional share from `signals.shareByExtension`).
378
- */
379
- const LANGUAGE_TO_EXTENSION = {
380
- python: 'py',
381
- kotlin: 'kt',
382
- java: 'java',
383
- rust: 'rs',
384
- go: 'go',
385
- ruby: 'rb',
386
- swift: 'swift',
387
- csharp: 'cs',
388
- cpp: 'cpp',
389
- };
390
- /**
391
- * Multi-language project heuristic: if the project's file tree contains
392
- * extensions matching a non-TS language, OR the project is a Python project,
393
- * treat it as a non-TS project and let the language-specific skills be relevant.
394
- */
395
- function isNonTsProject(signals) {
396
- if (signals.isPython)
397
- return true;
398
- const nonTsExts = ['swift', 'kt', 'kts', 'java', 'scala', 'py', 'pyx', 'go', 'rs', 'rb', 'cs'];
399
- return nonTsExts.some((ext) => signals.hasFileExtension[ext] === true);
400
- }
401
- /**
402
- * Discover the installed skills under `installedSkillsPath` (default:
403
- * `~/.claude/skills`). Each subdir containing a SKILL.md counts as an
404
- * installed skill.
405
- */
406
- export async function listInstalledSkills(installedSkillsPath) {
407
- if (!existsSync(installedSkillsPath))
408
- return [];
409
- let entries;
410
- try {
411
- entries = readdirSync(installedSkillsPath, { withFileTypes: true, encoding: 'utf8' });
412
- }
413
- catch {
414
- return [];
415
- }
416
- const skills = [];
417
- for (const entry of entries) {
418
- if (!entry.isDirectory())
419
- continue;
420
- const name = entry.name;
421
- if (typeof name !== 'string')
422
- continue;
423
- const skillPath = join(installedSkillsPath, name, 'SKILL.md');
424
- if (!existsSync(skillPath))
425
- continue;
426
- try {
427
- const raw = await readFile(skillPath, 'utf8');
428
- const frontmatter = parseFrontmatterLoose(raw);
429
- skills.push({
430
- name: frontmatter.name ?? name,
431
- description: frontmatter.description ?? '',
432
- skillPath,
433
- });
434
- }
435
- catch {
436
- skills.push({ name, description: '', skillPath });
437
- }
438
- }
439
- skills.sort((a, b) => a.name.localeCompare(b.name));
440
- return skills;
441
- }
442
- /**
443
- * Lightweight YAML frontmatter parser (good enough for `name` + `description`).
444
- * Falls back to regex when the file is malformed.
445
- */
446
- function parseFrontmatterLoose(content) {
447
- const lines = content.split(/\r?\n/);
448
- if (lines[0] !== '---')
449
- return {};
450
- const end = lines.findIndex((line, index) => index > 0 && line === '---');
451
- if (end === -1)
452
- return {};
453
- const out = {};
454
- for (let i = 1; i < end; i += 1) {
455
- const line = lines[i];
456
- if (line === undefined)
457
- continue;
458
- const match = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
459
- if (match === null || match[1] === undefined)
460
- continue;
461
- out[match[1]] = (match[2] ?? '').replace(/^['"]|['"]$/g, '').trim();
462
- }
463
- return out;
464
- }
465
- /**
466
- * The default installed-skills path: `~/.claude/skills`. Resolved at call
467
- * time so the orchestrator stays pure-ish (no module-level side effects).
468
- */
469
- export function defaultInstalledSkillsPath() {
470
- const home = process.env.HOME ?? process.env.USERPROFILE ?? '';
471
- return join(home, '.claude', 'skills');
472
- }
473
- const ALWAYS_RELEVANT_SET = new Set(ALWAYS_RELEVANT_SKILLS);
474
- /**
475
- * Top-level orchestrator. Reads package.json + tsconfig + file tree,
476
- * discovers installed skills, classifies each one, returns the JSON
477
- * envelope. Idempotent: same input → same output. No filesystem writes.
478
- */
479
- export async function detectSkillScope(input) {
480
- const projectRoot = input.projectRoot;
481
- const skillsPath = input.installedSkillsPath ?? defaultInstalledSkillsPath();
482
- const signals = await extractProjectSignals(projectRoot);
483
- const installed = await listInstalledSkills(skillsPath);
484
- const rules = {
485
- alwaysRelevant: ALWAYS_RELEVANT_SET,
486
- nonTsPrefixes: NON_TS_SKILL_PREFIXES,
487
- };
488
- const skills = installed.map((skill) => classifySkill(skill, signals, rules));
489
- const counts = {
490
- relevant: skills.filter((s) => s.relevance === 'relevant').length,
491
- borderline: skills.filter((s) => s.relevance === 'borderline').length,
492
- irrelevant: skills.filter((s) => s.relevance === 'irrelevant').length,
493
- };
494
- return {
495
- detectedIde: input.detectedIde ?? null,
496
- projectSignals: signals,
497
- skills,
498
- counts,
499
- };
500
- }
501
- // ---------------------------------------------------------------------------
502
- // Idempotency guard helper for tests
503
- // ---------------------------------------------------------------------------
504
- /** Compute a stable summary hash (used by tests to assert no time-dependent fields). */
505
- export function detectSummary(result) {
506
- const sorted = [...result.skills].sort((a, b) => a.name.localeCompare(b.name));
507
- return JSON.stringify({
508
- counts: result.counts,
509
- skills: sorted.map((s) => ({ name: s.name, relevance: s.relevance })),
510
- });
511
- }
512
- // Quiet the "unused" warning on statSync when used only in tests paths
513
- void statSync;
@@ -1,41 +0,0 @@
1
- /**
2
- * `peaks skill scope` — adapter registry.
3
- *
4
- * The registry owns the map from IdeId → SkillScopeAdapter. It exposes two
5
- * functions:
6
- * - `getScopeAdapter(ide)` — direct lookup. Throws on unknown ide.
7
- * - `resolveActiveAdapter(projectRoot)` — discover the best adapter by
8
- * probing every registered adapter's `detect(projectRoot)`. Falls back
9
- * to Claude Code with a synthetic score of 0.5 when no adapter scores
10
- * ≥ 0.5 (R3: "Claude Code shipped, Trae in progress" per package.json).
11
- *
12
- * See tech-doc-025 §7 for the discovery flow + fallback semantics.
13
- */
14
- import type { IdeId } from '../ide/ide-types.js';
15
- import type { SkillScopeAdapter } from './types.js';
16
- /** Get the adapter for a given IDE id. Throws on unsupported IDE. */
17
- export declare function getScopeAdapter(ide: IdeId): SkillScopeAdapter;
18
- /** All registered adapter ids (insertion order). */
19
- export declare function listScopeAdapterIds(): readonly IdeId[];
20
- /** All registered adapters (insertion order). */
21
- export declare function listScopeAdapters(): readonly SkillScopeAdapter[];
22
- export interface ResolvedAdapter {
23
- readonly adapter: SkillScopeAdapter;
24
- readonly score: number;
25
- /** True when the score is synthetic (no real adapter hit ≥ 0.5). */
26
- readonly isFallback: boolean;
27
- }
28
- /**
29
- * Discover the active adapter for a project root. Returns the highest-
30
- * scoring adapter; if all adapters score < 0.5, falls back to the Claude
31
- * Code adapter with a synthetic score of 0.5 (R3). Stubs (Trae, Cursor,
32
- * Codex, Qoder, Tongyi) return 0.0 from `detect()` so they never win.
33
- */
34
- export declare function resolveActiveAdapter(projectRoot: string): Promise<ResolvedAdapter>;
35
- /**
36
- * Test seam: replace the registry (used by stub-adapter tests to inject
37
- * a fresh adapter for an IDE without restarting the module).
38
- */
39
- export declare function _setScopeAdapterForTesting(ide: IdeId, adapter: SkillScopeAdapter): void;
40
- /** Test seam: reset to built-in defaults. */
41
- export declare function _resetScopeAdaptersForTesting(): void;