@planu/cli 1.11.0 → 1.13.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 (298) hide show
  1. package/dist/config/ai-tool-registry.json +71 -0
  2. package/dist/config/autopilot-config.json +21 -0
  3. package/dist/config/competitive-catalog.json +83 -0
  4. package/dist/config/license-plans.json +43 -2
  5. package/dist/engine/agent-registry/lifecycle-manager.d.ts +8 -0
  6. package/dist/engine/agent-registry/lifecycle-manager.d.ts.map +1 -0
  7. package/dist/engine/agent-registry/lifecycle-manager.js +81 -0
  8. package/dist/engine/agent-registry/lifecycle-manager.js.map +1 -0
  9. package/dist/engine/agent-registry/role-catalog.d.ts +17 -0
  10. package/dist/engine/agent-registry/role-catalog.d.ts.map +1 -0
  11. package/dist/engine/agent-registry/role-catalog.js +55 -0
  12. package/dist/engine/agent-registry/role-catalog.js.map +1 -0
  13. package/dist/engine/api-compat/compatibility-checker.d.ts +4 -0
  14. package/dist/engine/api-compat/compatibility-checker.d.ts.map +1 -0
  15. package/dist/engine/api-compat/compatibility-checker.js +118 -0
  16. package/dist/engine/api-compat/compatibility-checker.js.map +1 -0
  17. package/dist/engine/autopilot/action-executor.d.ts +18 -0
  18. package/dist/engine/autopilot/action-executor.d.ts.map +1 -0
  19. package/dist/engine/autopilot/action-executor.js +91 -0
  20. package/dist/engine/autopilot/action-executor.js.map +1 -0
  21. package/dist/engine/autopilot/event-bus.d.ts +8 -0
  22. package/dist/engine/autopilot/event-bus.d.ts.map +1 -0
  23. package/dist/engine/autopilot/event-bus.js +28 -0
  24. package/dist/engine/autopilot/event-bus.js.map +1 -0
  25. package/dist/engine/autopilot/trigger-rules.d.ts +3 -0
  26. package/dist/engine/autopilot/trigger-rules.d.ts.map +1 -0
  27. package/dist/engine/autopilot/trigger-rules.js +125 -0
  28. package/dist/engine/autopilot/trigger-rules.js.map +1 -0
  29. package/dist/engine/checkpoint/checkpoint-manager.d.ts +22 -0
  30. package/dist/engine/checkpoint/checkpoint-manager.d.ts.map +1 -0
  31. package/dist/engine/checkpoint/checkpoint-manager.js +76 -0
  32. package/dist/engine/checkpoint/checkpoint-manager.js.map +1 -0
  33. package/dist/engine/checkpoint/policy-engine.d.ts +10 -0
  34. package/dist/engine/checkpoint/policy-engine.d.ts.map +1 -0
  35. package/dist/engine/checkpoint/policy-engine.js +87 -0
  36. package/dist/engine/checkpoint/policy-engine.js.map +1 -0
  37. package/dist/engine/competitive/gap-analyzer.d.ts +12 -0
  38. package/dist/engine/competitive/gap-analyzer.d.ts.map +1 -0
  39. package/dist/engine/competitive/gap-analyzer.js +214 -0
  40. package/dist/engine/competitive/gap-analyzer.js.map +1 -0
  41. package/dist/engine/compliance/auto-remediator.d.ts +9 -0
  42. package/dist/engine/compliance/auto-remediator.d.ts.map +1 -0
  43. package/dist/engine/compliance/auto-remediator.js +118 -0
  44. package/dist/engine/compliance/auto-remediator.js.map +1 -0
  45. package/dist/engine/context-profile/profile-catalog.d.ts +5 -0
  46. package/dist/engine/context-profile/profile-catalog.d.ts.map +1 -0
  47. package/dist/engine/context-profile/profile-catalog.js +145 -0
  48. package/dist/engine/context-profile/profile-catalog.js.map +1 -0
  49. package/dist/engine/critical-path/path-analyzer.d.ts +3 -0
  50. package/dist/engine/critical-path/path-analyzer.d.ts.map +1 -0
  51. package/dist/engine/critical-path/path-analyzer.js +145 -0
  52. package/dist/engine/critical-path/path-analyzer.js.map +1 -0
  53. package/dist/engine/drift/violation-resolver.d.ts +9 -0
  54. package/dist/engine/drift/violation-resolver.d.ts.map +1 -0
  55. package/dist/engine/drift/violation-resolver.js +128 -0
  56. package/dist/engine/drift/violation-resolver.js.map +1 -0
  57. package/dist/engine/ears/criterion-scorer.d.ts +7 -0
  58. package/dist/engine/ears/criterion-scorer.d.ts.map +1 -0
  59. package/dist/engine/ears/criterion-scorer.js +87 -0
  60. package/dist/engine/ears/criterion-scorer.js.map +1 -0
  61. package/dist/engine/ears/pattern-matcher.d.ts +5 -0
  62. package/dist/engine/ears/pattern-matcher.d.ts.map +1 -0
  63. package/dist/engine/ears/pattern-matcher.js +48 -0
  64. package/dist/engine/ears/pattern-matcher.js.map +1 -0
  65. package/dist/engine/ears/rewriter.d.ts +7 -0
  66. package/dist/engine/ears/rewriter.d.ts.map +1 -0
  67. package/dist/engine/ears/rewriter.js +45 -0
  68. package/dist/engine/ears/rewriter.js.map +1 -0
  69. package/dist/engine/ears/spec-linter.d.ts +7 -0
  70. package/dist/engine/ears/spec-linter.d.ts.map +1 -0
  71. package/dist/engine/ears/spec-linter.js +127 -0
  72. package/dist/engine/ears/spec-linter.js.map +1 -0
  73. package/dist/engine/health/auto-fixer.d.ts +7 -0
  74. package/dist/engine/health/auto-fixer.d.ts.map +1 -0
  75. package/dist/engine/health/auto-fixer.js +130 -0
  76. package/dist/engine/health/auto-fixer.js.map +1 -0
  77. package/dist/engine/hook-generator/ai-hook-templates.d.ts +8 -0
  78. package/dist/engine/hook-generator/ai-hook-templates.d.ts.map +1 -0
  79. package/dist/engine/hook-generator/ai-hook-templates.js +43 -0
  80. package/dist/engine/hook-generator/ai-hook-templates.js.map +1 -0
  81. package/dist/engine/hook-generator/hook-merger.d.ts +13 -0
  82. package/dist/engine/hook-generator/hook-merger.d.ts.map +1 -0
  83. package/dist/engine/hook-generator/hook-merger.js +148 -0
  84. package/dist/engine/hook-generator/hook-merger.js.map +1 -0
  85. package/dist/engine/hook-generator/stack-hook-templates.d.ts +10 -0
  86. package/dist/engine/hook-generator/stack-hook-templates.d.ts.map +1 -0
  87. package/dist/engine/hook-generator/stack-hook-templates.js +105 -0
  88. package/dist/engine/hook-generator/stack-hook-templates.js.map +1 -0
  89. package/dist/engine/mcp-catalog/catalog-advisor.d.ts +3 -0
  90. package/dist/engine/mcp-catalog/catalog-advisor.d.ts.map +1 -0
  91. package/dist/engine/mcp-catalog/catalog-advisor.js +180 -0
  92. package/dist/engine/mcp-catalog/catalog-advisor.js.map +1 -0
  93. package/dist/engine/project-dna/ai-tool-detector.d.ts +12 -0
  94. package/dist/engine/project-dna/ai-tool-detector.d.ts.map +1 -0
  95. package/dist/engine/project-dna/ai-tool-detector.js +103 -0
  96. package/dist/engine/project-dna/ai-tool-detector.js.map +1 -0
  97. package/dist/engine/project-dna/rules-generator.d.ts +18 -0
  98. package/dist/engine/project-dna/rules-generator.d.ts.map +1 -0
  99. package/dist/engine/project-dna/rules-generator.js +193 -0
  100. package/dist/engine/project-dna/rules-generator.js.map +1 -0
  101. package/dist/engine/project-dna/stack-detector.d.ts +24 -0
  102. package/dist/engine/project-dna/stack-detector.d.ts.map +1 -0
  103. package/dist/engine/project-dna/stack-detector.js +309 -0
  104. package/dist/engine/project-dna/stack-detector.js.map +1 -0
  105. package/dist/engine/similar-problems/similarity-finder.d.ts +3 -0
  106. package/dist/engine/similar-problems/similarity-finder.d.ts.map +1 -0
  107. package/dist/engine/similar-problems/similarity-finder.js +144 -0
  108. package/dist/engine/similar-problems/similarity-finder.js.map +1 -0
  109. package/dist/engine/sync/asana-puller.d.ts +9 -0
  110. package/dist/engine/sync/asana-puller.d.ts.map +1 -0
  111. package/dist/engine/sync/asana-puller.js +91 -0
  112. package/dist/engine/sync/asana-puller.js.map +1 -0
  113. package/dist/engine/sync/conflict-resolver.d.ts +17 -0
  114. package/dist/engine/sync/conflict-resolver.d.ts.map +1 -0
  115. package/dist/engine/sync/conflict-resolver.js +58 -0
  116. package/dist/engine/sync/conflict-resolver.js.map +1 -0
  117. package/dist/engine/sync/monday-puller.d.ts +9 -0
  118. package/dist/engine/sync/monday-puller.d.ts.map +1 -0
  119. package/dist/engine/sync/monday-puller.js +110 -0
  120. package/dist/engine/sync/monday-puller.js.map +1 -0
  121. package/dist/engine/sync/notion-puller.d.ts +15 -0
  122. package/dist/engine/sync/notion-puller.d.ts.map +1 -0
  123. package/dist/engine/sync/notion-puller.js +101 -0
  124. package/dist/engine/sync/notion-puller.js.map +1 -0
  125. package/dist/engine/verifier/code-scanner.d.ts +8 -0
  126. package/dist/engine/verifier/code-scanner.d.ts.map +1 -0
  127. package/dist/engine/verifier/code-scanner.js +73 -0
  128. package/dist/engine/verifier/code-scanner.js.map +1 -0
  129. package/dist/engine/verifier/compliance-scorer.d.ts +17 -0
  130. package/dist/engine/verifier/compliance-scorer.d.ts.map +1 -0
  131. package/dist/engine/verifier/compliance-scorer.js +131 -0
  132. package/dist/engine/verifier/compliance-scorer.js.map +1 -0
  133. package/dist/engine/verifier/criterion-matcher.d.ts +15 -0
  134. package/dist/engine/verifier/criterion-matcher.d.ts.map +1 -0
  135. package/dist/engine/verifier/criterion-matcher.js +210 -0
  136. package/dist/engine/verifier/criterion-matcher.js.map +1 -0
  137. package/dist/index.js +24 -0
  138. package/dist/index.js.map +1 -1
  139. package/dist/storage/agent-registry-store.d.ts +11 -0
  140. package/dist/storage/agent-registry-store.d.ts.map +1 -0
  141. package/dist/storage/agent-registry-store.js +45 -0
  142. package/dist/storage/agent-registry-store.js.map +1 -0
  143. package/dist/storage/compliance-score-store.d.ts +16 -0
  144. package/dist/storage/compliance-score-store.d.ts.map +1 -0
  145. package/dist/storage/compliance-score-store.js +30 -0
  146. package/dist/storage/compliance-score-store.js.map +1 -0
  147. package/dist/storage/context-profile-store.d.ts +14 -0
  148. package/dist/storage/context-profile-store.d.ts.map +1 -0
  149. package/dist/storage/context-profile-store.js +34 -0
  150. package/dist/storage/context-profile-store.js.map +1 -0
  151. package/dist/storage/workflow-checkpoint-store.d.ts +16 -0
  152. package/dist/storage/workflow-checkpoint-store.d.ts.map +1 -0
  153. package/dist/storage/workflow-checkpoint-store.js +71 -0
  154. package/dist/storage/workflow-checkpoint-store.js.map +1 -0
  155. package/dist/tools/checkpoint/approve-checkpoint-handler.d.ts +3 -0
  156. package/dist/tools/checkpoint/approve-checkpoint-handler.d.ts.map +1 -0
  157. package/dist/tools/checkpoint/approve-checkpoint-handler.js +32 -0
  158. package/dist/tools/checkpoint/approve-checkpoint-handler.js.map +1 -0
  159. package/dist/tools/checkpoint/configure-policy-handler.d.ts +3 -0
  160. package/dist/tools/checkpoint/configure-policy-handler.d.ts.map +1 -0
  161. package/dist/tools/checkpoint/configure-policy-handler.js +60 -0
  162. package/dist/tools/checkpoint/configure-policy-handler.js.map +1 -0
  163. package/dist/tools/checkpoint/list-checkpoints-handler.d.ts +3 -0
  164. package/dist/tools/checkpoint/list-checkpoints-handler.d.ts.map +1 -0
  165. package/dist/tools/checkpoint/list-checkpoints-handler.js +25 -0
  166. package/dist/tools/checkpoint/list-checkpoints-handler.js.map +1 -0
  167. package/dist/tools/checkpoint/reject-checkpoint-handler.d.ts +3 -0
  168. package/dist/tools/checkpoint/reject-checkpoint-handler.d.ts.map +1 -0
  169. package/dist/tools/checkpoint/reject-checkpoint-handler.js +32 -0
  170. package/dist/tools/checkpoint/reject-checkpoint-handler.js.map +1 -0
  171. package/dist/tools/checkpoint/require-checkpoint-handler.d.ts +3 -0
  172. package/dist/tools/checkpoint/require-checkpoint-handler.d.ts.map +1 -0
  173. package/dist/tools/checkpoint/require-checkpoint-handler.js +44 -0
  174. package/dist/tools/checkpoint/require-checkpoint-handler.js.map +1 -0
  175. package/dist/tools/competitive-handlers.d.ts +30 -0
  176. package/dist/tools/competitive-handlers.d.ts.map +1 -0
  177. package/dist/tools/competitive-handlers.js +155 -0
  178. package/dist/tools/competitive-handlers.js.map +1 -0
  179. package/dist/tools/create-spec/post-creation.d.ts +1 -1
  180. package/dist/tools/create-spec/post-creation.d.ts.map +1 -1
  181. package/dist/tools/create-spec/post-creation.js +13 -1
  182. package/dist/tools/create-spec/post-creation.js.map +1 -1
  183. package/dist/tools/create-spec.js +1 -1
  184. package/dist/tools/create-spec.js.map +1 -1
  185. package/dist/tools/hook-generator-handler.d.ts +8 -0
  186. package/dist/tools/hook-generator-handler.d.ts.map +1 -0
  187. package/dist/tools/hook-generator-handler.js +154 -0
  188. package/dist/tools/hook-generator-handler.js.map +1 -0
  189. package/dist/tools/project-dna-handler.d.ts +34 -0
  190. package/dist/tools/project-dna-handler.d.ts.map +1 -0
  191. package/dist/tools/project-dna-handler.js +261 -0
  192. package/dist/tools/project-dna-handler.js.map +1 -0
  193. package/dist/tools/pull-sync-handler.d.ts +25 -0
  194. package/dist/tools/pull-sync-handler.d.ts.map +1 -0
  195. package/dist/tools/pull-sync-handler.js +161 -0
  196. package/dist/tools/pull-sync-handler.js.map +1 -0
  197. package/dist/tools/register-agent-registry.d.ts +5 -0
  198. package/dist/tools/register-agent-registry.d.ts.map +1 -0
  199. package/dist/tools/register-agent-registry.js +254 -0
  200. package/dist/tools/register-agent-registry.js.map +1 -0
  201. package/dist/tools/register-auto-remediation.d.ts +3 -0
  202. package/dist/tools/register-auto-remediation.d.ts.map +1 -0
  203. package/dist/tools/register-auto-remediation.js +174 -0
  204. package/dist/tools/register-auto-remediation.js.map +1 -0
  205. package/dist/tools/register-autopilot.d.ts +3 -0
  206. package/dist/tools/register-autopilot.d.ts.map +1 -0
  207. package/dist/tools/register-autopilot.js +78 -0
  208. package/dist/tools/register-autopilot.js.map +1 -0
  209. package/dist/tools/register-checkpoints.d.ts +3 -0
  210. package/dist/tools/register-checkpoints.d.ts.map +1 -0
  211. package/dist/tools/register-checkpoints.js +134 -0
  212. package/dist/tools/register-checkpoints.js.map +1 -0
  213. package/dist/tools/register-competitive.d.ts +3 -0
  214. package/dist/tools/register-competitive.d.ts.map +1 -0
  215. package/dist/tools/register-competitive.js +88 -0
  216. package/dist/tools/register-competitive.js.map +1 -0
  217. package/dist/tools/register-context-profile.d.ts +3 -0
  218. package/dist/tools/register-context-profile.d.ts.map +1 -0
  219. package/dist/tools/register-context-profile.js +106 -0
  220. package/dist/tools/register-context-profile.js.map +1 -0
  221. package/dist/tools/register-ears.d.ts +3 -0
  222. package/dist/tools/register-ears.d.ts.map +1 -0
  223. package/dist/tools/register-ears.js +148 -0
  224. package/dist/tools/register-ears.js.map +1 -0
  225. package/dist/tools/register-enterprise-compliance.js +1 -1
  226. package/dist/tools/register-enterprise-compliance.js.map +1 -1
  227. package/dist/tools/register-hook-generator.d.ts +3 -0
  228. package/dist/tools/register-hook-generator.d.ts.map +1 -0
  229. package/dist/tools/register-hook-generator.js +96 -0
  230. package/dist/tools/register-hook-generator.js.map +1 -0
  231. package/dist/tools/register-project-dna.d.ts +3 -0
  232. package/dist/tools/register-project-dna.d.ts.map +1 -0
  233. package/dist/tools/register-project-dna.js +43 -0
  234. package/dist/tools/register-project-dna.js.map +1 -0
  235. package/dist/tools/register-pull-sync.d.ts +3 -0
  236. package/dist/tools/register-pull-sync.d.ts.map +1 -0
  237. package/dist/tools/register-pull-sync.js +71 -0
  238. package/dist/tools/register-pull-sync.js.map +1 -0
  239. package/dist/tools/register-spec405-tools.d.ts +7 -0
  240. package/dist/tools/register-spec405-tools.d.ts.map +1 -0
  241. package/dist/tools/register-spec405-tools.js +194 -0
  242. package/dist/tools/register-spec405-tools.js.map +1 -0
  243. package/dist/tools/register-verifier.d.ts +3 -0
  244. package/dist/tools/register-verifier.d.ts.map +1 -0
  245. package/dist/tools/register-verifier.js +141 -0
  246. package/dist/tools/register-verifier.js.map +1 -0
  247. package/dist/tools/update-status/side-effects.d.ts.map +1 -1
  248. package/dist/tools/update-status/side-effects.js +32 -0
  249. package/dist/tools/update-status/side-effects.js.map +1 -1
  250. package/dist/types/agent-registry.d.ts +53 -0
  251. package/dist/types/agent-registry.d.ts.map +1 -0
  252. package/dist/types/agent-registry.js +2 -0
  253. package/dist/types/agent-registry.js.map +1 -0
  254. package/dist/types/analysis.d.ts +98 -0
  255. package/dist/types/analysis.d.ts.map +1 -1
  256. package/dist/types/autopilot.d.ts +36 -0
  257. package/dist/types/autopilot.d.ts.map +1 -0
  258. package/dist/types/autopilot.js +3 -0
  259. package/dist/types/autopilot.js.map +1 -0
  260. package/dist/types/competitive.d.ts +41 -0
  261. package/dist/types/competitive.d.ts.map +1 -0
  262. package/dist/types/competitive.js +3 -0
  263. package/dist/types/competitive.js.map +1 -0
  264. package/dist/types/context-profile.d.ts +22 -0
  265. package/dist/types/context-profile.d.ts.map +1 -0
  266. package/dist/types/context-profile.js +2 -0
  267. package/dist/types/context-profile.js.map +1 -0
  268. package/dist/types/ears.d.ts +34 -0
  269. package/dist/types/ears.d.ts.map +1 -0
  270. package/dist/types/ears.js +3 -0
  271. package/dist/types/ears.js.map +1 -0
  272. package/dist/types/health.d.ts +40 -0
  273. package/dist/types/health.d.ts.map +1 -0
  274. package/dist/types/health.js +3 -0
  275. package/dist/types/health.js.map +1 -0
  276. package/dist/types/hook-generator.d.ts +49 -0
  277. package/dist/types/hook-generator.d.ts.map +1 -0
  278. package/dist/types/hook-generator.js +3 -0
  279. package/dist/types/hook-generator.js.map +1 -0
  280. package/dist/types/index.d.ts +9 -0
  281. package/dist/types/index.d.ts.map +1 -1
  282. package/dist/types/index.js +9 -0
  283. package/dist/types/index.js.map +1 -1
  284. package/dist/types/notion-asana-monday.d.ts +38 -0
  285. package/dist/types/notion-asana-monday.d.ts.map +1 -1
  286. package/dist/types/project-dna.d.ts +46 -0
  287. package/dist/types/project-dna.d.ts.map +1 -0
  288. package/dist/types/project-dna.js +4 -0
  289. package/dist/types/project-dna.js.map +1 -0
  290. package/dist/types/workflow-checkpoint.d.ts +66 -0
  291. package/dist/types/workflow-checkpoint.d.ts.map +1 -0
  292. package/dist/types/workflow-checkpoint.js +4 -0
  293. package/dist/types/workflow-checkpoint.js.map +1 -0
  294. package/package.json +1 -1
  295. package/src/config/ai-tool-registry.json +71 -0
  296. package/src/config/autopilot-config.json +21 -0
  297. package/src/config/competitive-catalog.json +83 -0
  298. package/src/config/license-plans.json +43 -2
@@ -0,0 +1,127 @@
1
+ // engine/ears/spec-linter.ts — EARS-based spec linting (SPEC-410)
2
+ import { readFile } from 'node:fs/promises';
3
+ import { existsSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ import { glob } from 'glob';
6
+ import { scoreCriterion, gradeSpec } from './criterion-scorer.js';
7
+ // Regex to extract acceptance criteria lines (checkbox format)
8
+ const AC_LINE_REGEX = /^\s*-\s+\[[ xX]\]\s+(.+)$/;
9
+ /**
10
+ * Extract acceptance criteria texts from spec.md content.
11
+ * Recognizes lines formatted as "- [ ] criterion" or "- [x] criterion".
12
+ */
13
+ function extractCriteria(content) {
14
+ return content
15
+ .split('\n')
16
+ .map((line) => AC_LINE_REGEX.exec(line))
17
+ .filter((m) => m !== null)
18
+ .map((m) => {
19
+ const text = m[1];
20
+ // This is safe: AC_LINE_REGEX always captures group 1 when match succeeds
21
+ return (text ?? '').trim();
22
+ })
23
+ .filter((t) => t.length > 0);
24
+ }
25
+ function buildSpecReport(specId, criteria) {
26
+ if (criteria.length === 0) {
27
+ return {
28
+ specId,
29
+ criteriaCount: 0,
30
+ overallScore: 0,
31
+ compliantCount: 0,
32
+ partialCount: 0,
33
+ nonCompliantCount: 0,
34
+ criteria: [],
35
+ grade: 'F',
36
+ };
37
+ }
38
+ const overallScore = Math.round((criteria.reduce((sum, c) => sum + c.overallScore, 0) / criteria.length) * 10) / 10;
39
+ const compliantCount = criteria.filter((c) => c.earsCompliant).length;
40
+ const nonCompliantCount = criteria.filter((c) => !c.earsCompliant).length;
41
+ const partialCount = 0; // reserved for future partial-match detection
42
+ return {
43
+ specId,
44
+ criteriaCount: criteria.length,
45
+ overallScore,
46
+ compliantCount,
47
+ partialCount,
48
+ nonCompliantCount,
49
+ criteria,
50
+ grade: gradeSpec(overallScore),
51
+ };
52
+ }
53
+ /**
54
+ * Lint a single spec by ID — reads spec.md from the planu/specs directory.
55
+ */
56
+ export async function lintSpec(specId, projectPath) {
57
+ const specDir = join(projectPath, 'planu', 'specs');
58
+ const pattern = `${specId}-*/spec.md`;
59
+ const matches = await glob(pattern, { cwd: specDir, absolute: false });
60
+ if (matches.length === 0) {
61
+ // Try exact match without trailing glob
62
+ const directPath = join(specDir, specId, 'spec.md');
63
+ if (existsSync(directPath)) {
64
+ const content = await readFile(directPath, 'utf-8');
65
+ const extracted = extractCriteria(content);
66
+ return buildSpecReport(specId, extracted.map(scoreCriterion));
67
+ }
68
+ return buildSpecReport(specId, []);
69
+ }
70
+ const specPath = join(specDir, matches[0] ?? '');
71
+ try {
72
+ const content = await readFile(specPath, 'utf-8');
73
+ const extracted = extractCriteria(content);
74
+ return buildSpecReport(specId, extracted.map(scoreCriterion));
75
+ }
76
+ catch {
77
+ // Unreadable — return empty report
78
+ return buildSpecReport(specId, []);
79
+ }
80
+ }
81
+ // Lint all specs in the project — reads all planu/specs/SPEC-NNN/spec.md files.
82
+ export async function lintAllSpecs(projectPath) {
83
+ const specDir = join(projectPath, 'planu', 'specs');
84
+ const specFiles = await glob('*/spec.md', { cwd: specDir, absolute: false });
85
+ const specReports = [];
86
+ for (const relPath of specFiles) {
87
+ const specDirName = relPath.split('/')[0] ?? '';
88
+ // Extract SPEC-XXX from directory name like "SPEC-042-some-slug"
89
+ const specIdMatch = /^(SPEC-\d+)/i.exec(specDirName);
90
+ const specId = specIdMatch?.[1] ?? specDirName;
91
+ const fullPath = join(specDir, relPath);
92
+ let extracted;
93
+ try {
94
+ const content = await readFile(fullPath, 'utf-8');
95
+ extracted = extractCriteria(content);
96
+ }
97
+ catch {
98
+ continue;
99
+ }
100
+ if (extracted.length > 0) {
101
+ const report = buildSpecReport(specId, extracted.map(scoreCriterion));
102
+ specReports.push(report);
103
+ }
104
+ }
105
+ const totalSpecs = specReports.length;
106
+ const averageScore = totalSpecs > 0
107
+ ? Math.round((specReports.reduce((sum, r) => sum + r.overallScore, 0) / totalSpecs) * 10) / 10
108
+ : 0;
109
+ // Collect all criteria across specs and find the 10 worst by overallScore
110
+ const allCriteria = [];
111
+ for (const report of specReports) {
112
+ for (const c of report.criteria) {
113
+ allCriteria.push({ criterion: c.text, specId: report.specId, score: c.overallScore });
114
+ }
115
+ }
116
+ allCriteria.sort((a, b) => a.score - b.score);
117
+ const topWorstCriteria = allCriteria.slice(0, 10);
118
+ return {
119
+ projectPath,
120
+ totalSpecs,
121
+ averageScore,
122
+ topWorstCriteria,
123
+ specReports,
124
+ generatedAt: new Date().toISOString(),
125
+ };
126
+ }
127
+ //# sourceMappingURL=spec-linter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spec-linter.js","sourceRoot":"","sources":["../../../src/engine/ears/spec-linter.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ import type { AutoFixResult } from '../../types/health.js';
2
+ /**
3
+ * Reads health check output and applies deterministic fixes where possible.
4
+ * When dryRun=true, reports what would be fixed without writing any files.
5
+ */
6
+ export declare function autoFixHealth(projectPath: string, projectId: string, dryRun?: boolean): Promise<AutoFixResult>;
7
+ //# sourceMappingURL=auto-fixer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auto-fixer.d.ts","sourceRoot":"","sources":["../../../src/engine/health/auto-fixer.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,aAAa,EAAyB,MAAM,uBAAuB,CAAC;AAqHlF;;;GAGG;AACH,wBAAsB,aAAa,CACjC,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,EACjB,MAAM,UAAQ,GACb,OAAO,CAAC,aAAa,CAAC,CAiCxB"}
@@ -0,0 +1,130 @@
1
+ // engine/health/auto-fixer.ts — Auto-fix for health check issues (SPEC-408)
2
+ import { mkdir, writeFile, access } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { specStore } from '../../storage/index.js';
5
+ import { computeSpecHealth } from '../validation-loop.js';
6
+ async function collectHealthIssues(projectPath, projectId) {
7
+ const specs = await specStore.listSpecs(projectId);
8
+ const issues = [];
9
+ const scores = await Promise.all(specs.map(async (spec) => {
10
+ const score = await computeSpecHealth(projectPath, spec);
11
+ return { spec, score };
12
+ }));
13
+ for (const { spec, score } of scores) {
14
+ if (score.breakdown.specComplete < 20) {
15
+ issues.push({
16
+ type: 'incomplete-spec',
17
+ specId: spec.id,
18
+ specPath: spec.specPath,
19
+ projectPath,
20
+ });
21
+ }
22
+ if (score.breakdown.testsExist < 20) {
23
+ issues.push({ type: 'no-tests', specId: spec.id, projectPath });
24
+ }
25
+ if (score.breakdown.noDrift < 20) {
26
+ issues.push({ type: 'drift-detected', specId: spec.id, projectPath });
27
+ }
28
+ }
29
+ return issues;
30
+ }
31
+ // ---------------------------------------------------------------------------
32
+ // Fix handlers
33
+ // ---------------------------------------------------------------------------
34
+ async function fixMissingProgress(issue, dryRun) {
35
+ const specId = issue.specId;
36
+ const progressPath = join(issue.projectPath, 'planu', 'specs', specId, 'progress.md');
37
+ try {
38
+ await access(progressPath);
39
+ return null; // Already exists
40
+ }
41
+ catch {
42
+ // Does not exist — create it
43
+ if (!dryRun) {
44
+ await mkdir(join(issue.projectPath, 'planu', 'specs', specId), { recursive: true });
45
+ const content = `# ${specId} — Progress\n\nstatus: draft\n\n## Tasks\n\n- [ ] Define acceptance criteria\n`;
46
+ await writeFile(progressPath, content, 'utf-8');
47
+ }
48
+ return {
49
+ issue: 'missing-progress.md',
50
+ action: dryRun
51
+ ? 'would create progress.md from template'
52
+ : 'created progress.md from template',
53
+ affectedItem: specId,
54
+ success: true,
55
+ };
56
+ }
57
+ }
58
+ function buildSkipForIncompleteSpec(specId) {
59
+ return {
60
+ issue: `incomplete-spec: ${specId}`,
61
+ reason: 'Missing required spec fields cannot be auto-fixed — human authoring required',
62
+ manualSteps: [
63
+ `Open planu/specs/${specId}/spec.md`,
64
+ 'Add title, description, and at least one acceptance criterion',
65
+ 'Re-run health_check_all to verify',
66
+ ],
67
+ };
68
+ }
69
+ function buildSkipForNoTests(specId) {
70
+ return {
71
+ issue: `no-tests: ${specId}`,
72
+ reason: 'Test files must be authored by a developer — cannot auto-generate without implementation context',
73
+ manualSteps: [
74
+ `Create tests/ file referencing ${specId}`,
75
+ 'Implement at least one test case covering a key acceptance criterion',
76
+ 'Run pnpm test to confirm',
77
+ ],
78
+ };
79
+ }
80
+ function buildSkipForDrift(specId) {
81
+ return {
82
+ issue: `drift-detected: ${specId}`,
83
+ reason: 'Drift requires manual reconciliation between spec and implementation',
84
+ manualSteps: [
85
+ `Run detect_drift with specId: "${specId}" for a detailed report`,
86
+ 'Review which criteria are no longer implemented',
87
+ 'Either update the spec or fix the code to match',
88
+ ],
89
+ };
90
+ }
91
+ // ---------------------------------------------------------------------------
92
+ // Public API
93
+ // ---------------------------------------------------------------------------
94
+ /**
95
+ * Reads health check output and applies deterministic fixes where possible.
96
+ * When dryRun=true, reports what would be fixed without writing any files.
97
+ */
98
+ export async function autoFixHealth(projectPath, projectId, dryRun = false) {
99
+ const issues = await collectHealthIssues(projectPath, projectId);
100
+ const fixes = [];
101
+ const skipped = [];
102
+ for (const issue of issues) {
103
+ switch (issue.type) {
104
+ case 'missing-progress': {
105
+ const fix = await fixMissingProgress(issue, dryRun);
106
+ if (fix) {
107
+ fixes.push(fix);
108
+ }
109
+ break;
110
+ }
111
+ case 'incomplete-spec':
112
+ skipped.push(buildSkipForIncompleteSpec(issue.specId));
113
+ break;
114
+ case 'no-tests':
115
+ skipped.push(buildSkipForNoTests(issue.specId));
116
+ break;
117
+ case 'drift-detected':
118
+ skipped.push(buildSkipForDrift(issue.specId));
119
+ break;
120
+ }
121
+ }
122
+ return {
123
+ fixedCount: fixes.filter((f) => f.success).length,
124
+ skippedCount: skipped.length,
125
+ fixes,
126
+ skipped,
127
+ executedAt: new Date().toISOString(),
128
+ };
129
+ }
130
+ //# sourceMappingURL=auto-fixer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auto-fixer.js","sourceRoot":"","sources":["../../../src/engine/health/auto-fixer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ import type { HookSection } from '../../types/index.js';
2
+ /**
3
+ * Returns HookSection entries specific to the given AI tool.
4
+ * Returns an empty array for unknown / unsupported tools — extensible by
5
+ * adding entries to AI_HOOK_REGISTRY.
6
+ */
7
+ export declare function getAIHookSections(aiTool: string): HookSection[];
8
+ //# sourceMappingURL=ai-hook-templates.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ai-hook-templates.d.ts","sourceRoot":"","sources":["../../../src/engine/hook-generator/ai-hook-templates.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAyCxD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,WAAW,EAAE,CAG/D"}
@@ -0,0 +1,43 @@
1
+ // engine/hook-generator/ai-hook-templates.ts — AI-tool-specific hook sections (SPEC-416)
2
+ // ---------------------------------------------------------------------------
3
+ // Claude Code — post-spec-create validation hook comment reminder
4
+ // ---------------------------------------------------------------------------
5
+ const CLAUDE_SECTIONS = [
6
+ {
7
+ id: 'claude-validate-on-commit',
8
+ phase: 'pre-commit',
9
+ stack: 'claude',
10
+ content: '# Claude Code: run planu validate before committing spec changes\n' +
11
+ '# Configure via .claude/settings.json PostToolUse hooks',
12
+ description: 'Claude Code spec validation reminder',
13
+ },
14
+ ];
15
+ // ---------------------------------------------------------------------------
16
+ // Kiro — hooks YAML reminder
17
+ // ---------------------------------------------------------------------------
18
+ const KIRO_SECTIONS = [
19
+ {
20
+ id: 'kiro-spec-sync',
21
+ phase: 'pre-commit',
22
+ stack: 'kiro',
23
+ content: '# Kiro: ensure .kiro/hooks/*.yaml is committed with spec changes',
24
+ description: 'Kiro hooks YAML sync reminder',
25
+ },
26
+ ];
27
+ // ---------------------------------------------------------------------------
28
+ // Registry
29
+ // ---------------------------------------------------------------------------
30
+ const AI_HOOK_REGISTRY = {
31
+ claude: CLAUDE_SECTIONS,
32
+ kiro: KIRO_SECTIONS,
33
+ };
34
+ /**
35
+ * Returns HookSection entries specific to the given AI tool.
36
+ * Returns an empty array for unknown / unsupported tools — extensible by
37
+ * adding entries to AI_HOOK_REGISTRY.
38
+ */
39
+ export function getAIHookSections(aiTool) {
40
+ const sections = AI_HOOK_REGISTRY[aiTool];
41
+ return sections !== undefined ? [...sections] : [];
42
+ }
43
+ //# sourceMappingURL=ai-hook-templates.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ai-hook-templates.js","sourceRoot":"","sources":["../../../src/engine/hook-generator/ai-hook-templates.ts"],"names":[],"mappings":"AAAA,yFAAyF;AAIzF,8EAA8E;AAC9E,kEAAkE;AAClE,8EAA8E;AAE9E,MAAM,eAAe,GAAkB;IACrC;QACE,EAAE,EAAE,2BAA2B;QAC/B,KAAK,EAAE,YAAY;QACnB,KAAK,EAAE,QAAQ;QACf,OAAO,EACL,oEAAoE;YACpE,yDAAyD;QAC3D,WAAW,EAAE,sCAAsC;KACpD;CACF,CAAC;AAEF,8EAA8E;AAC9E,6BAA6B;AAC7B,8EAA8E;AAE9E,MAAM,aAAa,GAAkB;IACnC;QACE,EAAE,EAAE,gBAAgB;QACpB,KAAK,EAAE,YAAY;QACnB,KAAK,EAAE,MAAM;QACb,OAAO,EAAE,kEAAkE;QAC3E,WAAW,EAAE,+BAA+B;KAC7C;CACF,CAAC;AAEF,8EAA8E;AAC9E,WAAW;AACX,8EAA8E;AAE9E,MAAM,gBAAgB,GAAkC;IACtD,MAAM,EAAE,eAAe;IACvB,IAAI,EAAE,aAAa;CACpB,CAAC;AAEF;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,MAAc;IAC9C,MAAM,QAAQ,GAAG,gBAAgB,CAAC,MAAM,CAAC,CAAC;IAC1C,OAAO,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;AACrD,CAAC"}
@@ -0,0 +1,13 @@
1
+ import type { HookSection, MergeResult, HookPreview } from '../../types/index.js';
2
+ /**
3
+ * Previews what mergeHooks would do WITHOUT writing any files.
4
+ */
5
+ export declare function previewHooks(projectPath: string, sections: HookSection[]): Promise<HookPreview[]>;
6
+ /**
7
+ * Merges HookSection entries into the appropriate hook files.
8
+ * Uses idempotent markers so manual edits outside markers are preserved.
9
+ *
10
+ * @param dryRun When true, computes results but does NOT write to disk.
11
+ */
12
+ export declare function mergeHooks(projectPath: string, sections: HookSection[], dryRun: boolean): Promise<MergeResult[]>;
13
+ //# sourceMappingURL=hook-merger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hook-merger.d.ts","sourceRoot":"","sources":["../../../src/engine/hook-generator/hook-merger.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,WAAW,EAAa,MAAM,sBAAsB,CAAC;AAuG7F;;GAEG;AACH,wBAAsB,YAAY,CAChC,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,WAAW,EAAE,GACtB,OAAO,CAAC,WAAW,EAAE,CAAC,CA+BxB;AAED;;;;;GAKG;AACH,wBAAsB,UAAU,CAC9B,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,WAAW,EAAE,EACvB,MAAM,EAAE,OAAO,GACd,OAAO,CAAC,WAAW,EAAE,CAAC,CAkCxB"}
@@ -0,0 +1,148 @@
1
+ // engine/hook-generator/hook-merger.ts — Idempotent hook file merge engine (SPEC-416)
2
+ import { readFile, writeFile, access, mkdir } from 'node:fs/promises';
3
+ import { join, dirname } from 'node:path';
4
+ import { markerStart, markerEnd } from './stack-hook-templates.js';
5
+ // ---------------------------------------------------------------------------
6
+ // File path resolution
7
+ // ---------------------------------------------------------------------------
8
+ /**
9
+ * Resolves the path to the hook file for a given phase.
10
+ * Prefers .husky/ when the directory exists, falls back to .git/hooks/.
11
+ */
12
+ async function hookFilePath(projectPath, phase) {
13
+ const huskyDir = join(projectPath, '.husky');
14
+ try {
15
+ await access(huskyDir);
16
+ return join(huskyDir, phase);
17
+ }
18
+ catch {
19
+ return join(projectPath, '.git', 'hooks', phase);
20
+ }
21
+ }
22
+ // ---------------------------------------------------------------------------
23
+ // File I/O helpers
24
+ // ---------------------------------------------------------------------------
25
+ /** Reads an existing hook file. Returns null if it does not exist. */
26
+ async function readHookFile(filePath) {
27
+ try {
28
+ return await readFile(filePath, 'utf8');
29
+ }
30
+ catch {
31
+ return null;
32
+ }
33
+ }
34
+ /** Returns a minimal shell script header for new hook files. */
35
+ function shellHeader() {
36
+ return '#!/usr/bin/env sh\n';
37
+ }
38
+ /**
39
+ * Injects a HookSection into existing hook file content using planu markers.
40
+ *
41
+ * - If markers already present with IDENTICAL body → 'skipped'
42
+ * - If markers present with different body → 'updated' (replaces between markers)
43
+ * - If no markers → 'added' (appends block at end)
44
+ */
45
+ function injectSection(content, section) {
46
+ const start = markerStart(section.id);
47
+ const end = markerEnd(section.id);
48
+ const block = [`\n${start}`, `# ${section.description}`, section.content, end].join('\n');
49
+ const startIdx = content.indexOf(start);
50
+ const endIdx = content.indexOf(end);
51
+ if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
52
+ // Markers exist — extract current body between them
53
+ const currentBlock = content.slice(startIdx, endIdx + end.length);
54
+ if (currentBlock === block.trimStart()) {
55
+ return { content, action: 'skipped' };
56
+ }
57
+ // Replace the existing block
58
+ const updated = content.slice(0, startIdx) + block.trimStart() + content.slice(endIdx + end.length);
59
+ return { content: updated, action: 'updated' };
60
+ }
61
+ // No markers — append
62
+ const sep = content.endsWith('\n') ? '' : '\n';
63
+ return { content: content + sep + block + '\n', action: 'added' };
64
+ }
65
+ // ---------------------------------------------------------------------------
66
+ // Group sections by phase
67
+ // ---------------------------------------------------------------------------
68
+ function groupByPhase(sections) {
69
+ const map = new Map();
70
+ for (const section of sections) {
71
+ const existing = map.get(section.phase) ?? [];
72
+ existing.push(section);
73
+ map.set(section.phase, existing);
74
+ }
75
+ return map;
76
+ }
77
+ // ---------------------------------------------------------------------------
78
+ // Public API
79
+ // ---------------------------------------------------------------------------
80
+ /**
81
+ * Previews what mergeHooks would do WITHOUT writing any files.
82
+ */
83
+ export async function previewHooks(projectPath, sections) {
84
+ const grouped = groupByPhase(sections);
85
+ const previews = [];
86
+ for (const [phase, phaseSections] of grouped) {
87
+ const filePath = await hookFilePath(projectPath, phase);
88
+ const currentContent = await readHookFile(filePath);
89
+ const conflicts = [];
90
+ const proposedAdditions = [];
91
+ for (const section of phaseSections) {
92
+ const start = markerStart(section.id);
93
+ if (currentContent?.includes(start) === true) {
94
+ conflicts.push(section.id);
95
+ }
96
+ else {
97
+ proposedAdditions.push(section);
98
+ }
99
+ }
100
+ previews.push({
101
+ phase,
102
+ filePath,
103
+ currentContent,
104
+ proposedAdditions,
105
+ wouldModify: proposedAdditions.length > 0 || conflicts.length > 0,
106
+ conflicts,
107
+ });
108
+ }
109
+ return previews;
110
+ }
111
+ /**
112
+ * Merges HookSection entries into the appropriate hook files.
113
+ * Uses idempotent markers so manual edits outside markers are preserved.
114
+ *
115
+ * @param dryRun When true, computes results but does NOT write to disk.
116
+ */
117
+ export async function mergeHooks(projectPath, sections, dryRun) {
118
+ const grouped = groupByPhase(sections);
119
+ const results = [];
120
+ for (const [phase, phaseSections] of grouped) {
121
+ const filePath = await hookFilePath(projectPath, phase);
122
+ const existing = await readHookFile(filePath);
123
+ let content = existing ?? shellHeader();
124
+ const sectionsAdded = [];
125
+ const sectionsUpdated = [];
126
+ const sectionsSkipped = [];
127
+ for (const section of phaseSections) {
128
+ const outcome = injectSection(content, section);
129
+ content = outcome.content;
130
+ if (outcome.action === 'added') {
131
+ sectionsAdded.push(section.id);
132
+ }
133
+ else if (outcome.action === 'updated') {
134
+ sectionsUpdated.push(section.id);
135
+ }
136
+ else {
137
+ sectionsSkipped.push(section.id);
138
+ }
139
+ }
140
+ if (!dryRun && (sectionsAdded.length > 0 || sectionsUpdated.length > 0)) {
141
+ await mkdir(dirname(filePath), { recursive: true });
142
+ await writeFile(filePath, content, { encoding: 'utf8', mode: 0o755 });
143
+ }
144
+ results.push({ filePath, sectionsAdded, sectionsUpdated, sectionsSkipped, content });
145
+ }
146
+ return results;
147
+ }
148
+ //# sourceMappingURL=hook-merger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hook-merger.js","sourceRoot":"","sources":["../../../src/engine/hook-generator/hook-merger.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ import type { HookSection } from '../../types/index.js';
2
+ export declare function markerStart(id: string): string;
3
+ export declare function markerEnd(id: string): string;
4
+ export declare const STACK_HOOK_TEMPLATES: Record<string, HookSection[]>;
5
+ /**
6
+ * Returns all HookSection entries matching the requested stack tokens.
7
+ * Unknown tokens are silently skipped.
8
+ */
9
+ export declare function getHookSectionsForStack(stack: string[]): HookSection[];
10
+ //# sourceMappingURL=stack-hook-templates.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stack-hook-templates.d.ts","sourceRoot":"","sources":["../../../src/engine/hook-generator/stack-hook-templates.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAMxD,wBAAgB,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAE9C;AAED,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAE5C;AAMD,eAAO,MAAM,oBAAoB,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,EAAE,CAyE9D,CAAC;AAMF;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,WAAW,EAAE,CAStE"}
@@ -0,0 +1,105 @@
1
+ // engine/hook-generator/stack-hook-templates.ts — Git hook templates per stack (SPEC-416)
2
+ // ---------------------------------------------------------------------------
3
+ // Marker helpers — used by hook-merger for idempotent injection
4
+ // ---------------------------------------------------------------------------
5
+ export function markerStart(id) {
6
+ return `# <!-- planu:generated:${id} -->`;
7
+ }
8
+ export function markerEnd(id) {
9
+ return `# <!-- /planu:generated:${id} -->`;
10
+ }
11
+ // ---------------------------------------------------------------------------
12
+ // Templates
13
+ // ---------------------------------------------------------------------------
14
+ export const STACK_HOOK_TEMPLATES = {
15
+ typescript: [
16
+ {
17
+ id: 'typescript-typecheck',
18
+ phase: 'pre-commit',
19
+ stack: 'typescript',
20
+ content: 'pnpm typecheck || exit 1',
21
+ description: 'TypeScript type checking',
22
+ },
23
+ ],
24
+ eslint: [
25
+ {
26
+ id: 'eslint-fix',
27
+ phase: 'pre-commit',
28
+ stack: 'eslint',
29
+ content: 'pnpm lint --fix || exit 1',
30
+ description: 'ESLint auto-fix',
31
+ },
32
+ ],
33
+ prettier: [
34
+ {
35
+ id: 'prettier-format',
36
+ phase: 'pre-commit',
37
+ stack: 'prettier',
38
+ content: 'pnpm format || exit 1',
39
+ description: 'Prettier formatting',
40
+ },
41
+ ],
42
+ vitest: [
43
+ {
44
+ id: 'vitest-coverage',
45
+ phase: 'pre-push',
46
+ stack: 'vitest',
47
+ content: 'pnpm test:coverage || exit 1',
48
+ description: 'Vitest coverage threshold check',
49
+ },
50
+ ],
51
+ jest: [
52
+ {
53
+ id: 'jest-coverage',
54
+ phase: 'pre-push',
55
+ stack: 'jest',
56
+ content: 'pnpm test --coverage || exit 1',
57
+ description: 'Jest coverage check',
58
+ },
59
+ ],
60
+ python: [
61
+ {
62
+ id: 'python-ruff',
63
+ phase: 'pre-commit',
64
+ stack: 'python',
65
+ content: 'ruff check --fix . || exit 1',
66
+ description: 'Python ruff linting',
67
+ },
68
+ ],
69
+ go: [
70
+ {
71
+ id: 'go-vet',
72
+ phase: 'pre-commit',
73
+ stack: 'go',
74
+ content: 'go vet ./... || exit 1',
75
+ description: 'Go vet check',
76
+ },
77
+ ],
78
+ commitlint: [
79
+ {
80
+ id: 'commitlint',
81
+ phase: 'commit-msg',
82
+ stack: 'commitlint',
83
+ content: 'npx --no -- commitlint --edit "$1" || exit 1',
84
+ description: 'Conventional commits validation',
85
+ },
86
+ ],
87
+ };
88
+ // ---------------------------------------------------------------------------
89
+ // Public helper
90
+ // ---------------------------------------------------------------------------
91
+ /**
92
+ * Returns all HookSection entries matching the requested stack tokens.
93
+ * Unknown tokens are silently skipped.
94
+ */
95
+ export function getHookSectionsForStack(stack) {
96
+ const sections = [];
97
+ for (const token of stack) {
98
+ const templates = STACK_HOOK_TEMPLATES[token];
99
+ if (templates !== undefined) {
100
+ sections.push(...templates);
101
+ }
102
+ }
103
+ return sections;
104
+ }
105
+ //# sourceMappingURL=stack-hook-templates.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stack-hook-templates.js","sourceRoot":"","sources":["../../../src/engine/hook-generator/stack-hook-templates.ts"],"names":[],"mappings":"AAAA,0FAA0F;AAI1F,8EAA8E;AAC9E,gEAAgE;AAChE,8EAA8E;AAE9E,MAAM,UAAU,WAAW,CAAC,EAAU;IACpC,OAAO,0BAA0B,EAAE,MAAM,CAAC;AAC5C,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,EAAU;IAClC,OAAO,2BAA2B,EAAE,MAAM,CAAC;AAC7C,CAAC;AAED,8EAA8E;AAC9E,YAAY;AACZ,8EAA8E;AAE9E,MAAM,CAAC,MAAM,oBAAoB,GAAkC;IACjE,UAAU,EAAE;QACV;YACE,EAAE,EAAE,sBAAsB;YAC1B,KAAK,EAAE,YAAY;YACnB,KAAK,EAAE,YAAY;YACnB,OAAO,EAAE,0BAA0B;YACnC,WAAW,EAAE,0BAA0B;SACxC;KACF;IACD,MAAM,EAAE;QACN;YACE,EAAE,EAAE,YAAY;YAChB,KAAK,EAAE,YAAY;YACnB,KAAK,EAAE,QAAQ;YACf,OAAO,EAAE,2BAA2B;YACpC,WAAW,EAAE,iBAAiB;SAC/B;KACF;IACD,QAAQ,EAAE;QACR;YACE,EAAE,EAAE,iBAAiB;YACrB,KAAK,EAAE,YAAY;YACnB,KAAK,EAAE,UAAU;YACjB,OAAO,EAAE,uBAAuB;YAChC,WAAW,EAAE,qBAAqB;SACnC;KACF;IACD,MAAM,EAAE;QACN;YACE,EAAE,EAAE,iBAAiB;YACrB,KAAK,EAAE,UAAU;YACjB,KAAK,EAAE,QAAQ;YACf,OAAO,EAAE,8BAA8B;YACvC,WAAW,EAAE,iCAAiC;SAC/C;KACF;IACD,IAAI,EAAE;QACJ;YACE,EAAE,EAAE,eAAe;YACnB,KAAK,EAAE,UAAU;YACjB,KAAK,EAAE,MAAM;YACb,OAAO,EAAE,gCAAgC;YACzC,WAAW,EAAE,qBAAqB;SACnC;KACF;IACD,MAAM,EAAE;QACN;YACE,EAAE,EAAE,aAAa;YACjB,KAAK,EAAE,YAAY;YACnB,KAAK,EAAE,QAAQ;YACf,OAAO,EAAE,8BAA8B;YACvC,WAAW,EAAE,qBAAqB;SACnC;KACF;IACD,EAAE,EAAE;QACF;YACE,EAAE,EAAE,QAAQ;YACZ,KAAK,EAAE,YAAY;YACnB,KAAK,EAAE,IAAI;YACX,OAAO,EAAE,wBAAwB;YACjC,WAAW,EAAE,cAAc;SAC5B;KACF;IACD,UAAU,EAAE;QACV;YACE,EAAE,EAAE,YAAY;YAChB,KAAK,EAAE,YAAY;YACnB,KAAK,EAAE,YAAY;YACnB,OAAO,EAAE,8CAA8C;YACvD,WAAW,EAAE,iCAAiC;SAC/C;KACF;CACF,CAAC;AAEF,8EAA8E;AAC9E,gBAAgB;AAChB,8EAA8E;AAE9E;;;GAGG;AACH,MAAM,UAAU,uBAAuB,CAAC,KAAe;IACrD,MAAM,QAAQ,GAAkB,EAAE,CAAC;IACnC,KAAK,MAAM,KAAK,IAAI,KAAK,EAAE,CAAC;QAC1B,MAAM,SAAS,GAAG,oBAAoB,CAAC,KAAK,CAAC,CAAC;QAC9C,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;YAC5B,QAAQ,CAAC,IAAI,CAAC,GAAG,SAAS,CAAC,CAAC;QAC9B,CAAC;IACH,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC"}