@planu/cli 1.12.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 (128) 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 +17 -1
  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/autopilot/action-executor.d.ts +18 -0
  14. package/dist/engine/autopilot/action-executor.d.ts.map +1 -0
  15. package/dist/engine/autopilot/action-executor.js +91 -0
  16. package/dist/engine/autopilot/action-executor.js.map +1 -0
  17. package/dist/engine/autopilot/event-bus.d.ts +8 -0
  18. package/dist/engine/autopilot/event-bus.d.ts.map +1 -0
  19. package/dist/engine/autopilot/event-bus.js +28 -0
  20. package/dist/engine/autopilot/event-bus.js.map +1 -0
  21. package/dist/engine/autopilot/trigger-rules.d.ts +3 -0
  22. package/dist/engine/autopilot/trigger-rules.d.ts.map +1 -0
  23. package/dist/engine/autopilot/trigger-rules.js +125 -0
  24. package/dist/engine/autopilot/trigger-rules.js.map +1 -0
  25. package/dist/engine/competitive/gap-analyzer.d.ts +12 -0
  26. package/dist/engine/competitive/gap-analyzer.d.ts.map +1 -0
  27. package/dist/engine/competitive/gap-analyzer.js +214 -0
  28. package/dist/engine/competitive/gap-analyzer.js.map +1 -0
  29. package/dist/engine/hook-generator/ai-hook-templates.d.ts +8 -0
  30. package/dist/engine/hook-generator/ai-hook-templates.d.ts.map +1 -0
  31. package/dist/engine/hook-generator/ai-hook-templates.js +43 -0
  32. package/dist/engine/hook-generator/ai-hook-templates.js.map +1 -0
  33. package/dist/engine/hook-generator/hook-merger.d.ts +13 -0
  34. package/dist/engine/hook-generator/hook-merger.d.ts.map +1 -0
  35. package/dist/engine/hook-generator/hook-merger.js +148 -0
  36. package/dist/engine/hook-generator/hook-merger.js.map +1 -0
  37. package/dist/engine/hook-generator/stack-hook-templates.d.ts +10 -0
  38. package/dist/engine/hook-generator/stack-hook-templates.d.ts.map +1 -0
  39. package/dist/engine/hook-generator/stack-hook-templates.js +105 -0
  40. package/dist/engine/hook-generator/stack-hook-templates.js.map +1 -0
  41. package/dist/engine/project-dna/ai-tool-detector.d.ts +12 -0
  42. package/dist/engine/project-dna/ai-tool-detector.d.ts.map +1 -0
  43. package/dist/engine/project-dna/ai-tool-detector.js +103 -0
  44. package/dist/engine/project-dna/ai-tool-detector.js.map +1 -0
  45. package/dist/engine/project-dna/rules-generator.d.ts +18 -0
  46. package/dist/engine/project-dna/rules-generator.d.ts.map +1 -0
  47. package/dist/engine/project-dna/rules-generator.js +193 -0
  48. package/dist/engine/project-dna/rules-generator.js.map +1 -0
  49. package/dist/engine/project-dna/stack-detector.d.ts +24 -0
  50. package/dist/engine/project-dna/stack-detector.d.ts.map +1 -0
  51. package/dist/engine/project-dna/stack-detector.js +309 -0
  52. package/dist/engine/project-dna/stack-detector.js.map +1 -0
  53. package/dist/index.js +10 -0
  54. package/dist/index.js.map +1 -1
  55. package/dist/storage/agent-registry-store.d.ts +11 -0
  56. package/dist/storage/agent-registry-store.d.ts.map +1 -0
  57. package/dist/storage/agent-registry-store.js +45 -0
  58. package/dist/storage/agent-registry-store.js.map +1 -0
  59. package/dist/tools/competitive-handlers.d.ts +30 -0
  60. package/dist/tools/competitive-handlers.d.ts.map +1 -0
  61. package/dist/tools/competitive-handlers.js +155 -0
  62. package/dist/tools/competitive-handlers.js.map +1 -0
  63. package/dist/tools/create-spec/post-creation.d.ts +1 -1
  64. package/dist/tools/create-spec/post-creation.d.ts.map +1 -1
  65. package/dist/tools/create-spec/post-creation.js +13 -1
  66. package/dist/tools/create-spec/post-creation.js.map +1 -1
  67. package/dist/tools/create-spec.js +1 -1
  68. package/dist/tools/create-spec.js.map +1 -1
  69. package/dist/tools/hook-generator-handler.d.ts +8 -0
  70. package/dist/tools/hook-generator-handler.d.ts.map +1 -0
  71. package/dist/tools/hook-generator-handler.js +154 -0
  72. package/dist/tools/hook-generator-handler.js.map +1 -0
  73. package/dist/tools/project-dna-handler.d.ts +34 -0
  74. package/dist/tools/project-dna-handler.d.ts.map +1 -0
  75. package/dist/tools/project-dna-handler.js +261 -0
  76. package/dist/tools/project-dna-handler.js.map +1 -0
  77. package/dist/tools/register-agent-registry.d.ts +5 -0
  78. package/dist/tools/register-agent-registry.d.ts.map +1 -0
  79. package/dist/tools/register-agent-registry.js +254 -0
  80. package/dist/tools/register-agent-registry.js.map +1 -0
  81. package/dist/tools/register-autopilot.d.ts +3 -0
  82. package/dist/tools/register-autopilot.d.ts.map +1 -0
  83. package/dist/tools/register-autopilot.js +78 -0
  84. package/dist/tools/register-autopilot.js.map +1 -0
  85. package/dist/tools/register-competitive.d.ts +3 -0
  86. package/dist/tools/register-competitive.d.ts.map +1 -0
  87. package/dist/tools/register-competitive.js +88 -0
  88. package/dist/tools/register-competitive.js.map +1 -0
  89. package/dist/tools/register-hook-generator.d.ts +3 -0
  90. package/dist/tools/register-hook-generator.d.ts.map +1 -0
  91. package/dist/tools/register-hook-generator.js +96 -0
  92. package/dist/tools/register-hook-generator.js.map +1 -0
  93. package/dist/tools/register-project-dna.d.ts +3 -0
  94. package/dist/tools/register-project-dna.d.ts.map +1 -0
  95. package/dist/tools/register-project-dna.js +43 -0
  96. package/dist/tools/register-project-dna.js.map +1 -0
  97. package/dist/tools/update-status/side-effects.d.ts.map +1 -1
  98. package/dist/tools/update-status/side-effects.js +32 -0
  99. package/dist/tools/update-status/side-effects.js.map +1 -1
  100. package/dist/types/agent-registry.d.ts +53 -0
  101. package/dist/types/agent-registry.d.ts.map +1 -0
  102. package/dist/types/agent-registry.js +2 -0
  103. package/dist/types/agent-registry.js.map +1 -0
  104. package/dist/types/autopilot.d.ts +36 -0
  105. package/dist/types/autopilot.d.ts.map +1 -0
  106. package/dist/types/autopilot.js +3 -0
  107. package/dist/types/autopilot.js.map +1 -0
  108. package/dist/types/competitive.d.ts +41 -0
  109. package/dist/types/competitive.d.ts.map +1 -0
  110. package/dist/types/competitive.js +3 -0
  111. package/dist/types/competitive.js.map +1 -0
  112. package/dist/types/hook-generator.d.ts +49 -0
  113. package/dist/types/hook-generator.d.ts.map +1 -0
  114. package/dist/types/hook-generator.js +3 -0
  115. package/dist/types/hook-generator.js.map +1 -0
  116. package/dist/types/index.d.ts +5 -0
  117. package/dist/types/index.d.ts.map +1 -1
  118. package/dist/types/index.js +5 -0
  119. package/dist/types/index.js.map +1 -1
  120. package/dist/types/project-dna.d.ts +46 -0
  121. package/dist/types/project-dna.d.ts.map +1 -0
  122. package/dist/types/project-dna.js +4 -0
  123. package/dist/types/project-dna.js.map +1 -0
  124. package/package.json +1 -1
  125. package/src/config/ai-tool-registry.json +71 -0
  126. package/src/config/autopilot-config.json +21 -0
  127. package/src/config/competitive-catalog.json +83 -0
  128. package/src/config/license-plans.json +17 -1
@@ -0,0 +1,125 @@
1
+ // engine/autopilot/trigger-rules.ts — SPEC-413: Catalog of default trigger rules
2
+ export const DEFAULT_TRIGGER_RULES = [
3
+ // ── post spec:created ──────────────────────────────────────────────────────
4
+ {
5
+ id: 'post-create:validate_criteria_quality',
6
+ event: 'spec:created',
7
+ action: 'validate_criteria_quality',
8
+ enabled: true,
9
+ description: 'Scores EARS quality of acceptance criteria after spec creation',
10
+ },
11
+ {
12
+ id: 'post-create:suggest_criteria',
13
+ event: 'spec:created',
14
+ action: 'suggest_criteria',
15
+ enabled: true,
16
+ description: 'Suggests missing domain criteria (security, compliance, edge cases)',
17
+ },
18
+ {
19
+ id: 'post-create:detect_contradictions',
20
+ event: 'spec:created',
21
+ action: 'detect_contradictions',
22
+ enabled: true,
23
+ description: 'Runs full contradiction detection on new spec criteria',
24
+ },
25
+ // ── post spec:status:approved ──────────────────────────────────────────────
26
+ {
27
+ id: 'post-approved:challenge_spec',
28
+ event: 'spec:status:approved',
29
+ action: 'challenge_spec',
30
+ enabled: true,
31
+ condition: { minEstimatedHours: 20, riskLevel: ['high', 'critical'] },
32
+ description: 'Stress-tests approved specs with >20h estimate or high risk',
33
+ },
34
+ {
35
+ id: 'post-approved:spec_quality_score',
36
+ event: 'spec:status:approved',
37
+ action: 'spec_quality_score',
38
+ enabled: true,
39
+ description: 'Generates A-F quality score at approval time',
40
+ },
41
+ {
42
+ id: 'post-approved:generate_orchestration_plan',
43
+ event: 'spec:status:approved',
44
+ action: 'generate_orchestration_plan',
45
+ enabled: true,
46
+ condition: { minEstimatedHours: 20 },
47
+ description: 'Suggests multi-agent plan for large specs (>20h)',
48
+ },
49
+ // ── post spec:status:implementing ─────────────────────────────────────────
50
+ {
51
+ id: 'post-implementing:tdd_scaffold',
52
+ event: 'spec:status:implementing',
53
+ action: 'tdd_scaffold',
54
+ enabled: true,
55
+ description: 'Generates test scaffold from acceptance criteria when implementation starts',
56
+ },
57
+ {
58
+ id: 'post-implementing:watch_spec_drift_start',
59
+ event: 'spec:status:implementing',
60
+ action: 'watch_spec_drift',
61
+ enabled: true,
62
+ description: 'Starts file watcher for real-time drift detection',
63
+ },
64
+ // ── post spec:status:done ──────────────────────────────────────────────────
65
+ {
66
+ id: 'post-done:watch_spec_drift_stop',
67
+ event: 'spec:status:done',
68
+ action: 'watch_spec_drift',
69
+ enabled: true,
70
+ description: 'Stops drift watcher when spec is marked done',
71
+ },
72
+ {
73
+ id: 'post-done:auto_fix_health',
74
+ event: 'spec:status:done',
75
+ action: 'auto_fix_health',
76
+ enabled: true,
77
+ description: 'Auto-repairs deterministic health issues post-done',
78
+ },
79
+ {
80
+ id: 'post-done:generate_changelog',
81
+ event: 'spec:status:done',
82
+ action: 'generate_changelog',
83
+ enabled: true,
84
+ description: 'Generates changelog entry when spec completes',
85
+ },
86
+ // ── drift:detected ────────────────────────────────────────────────────────
87
+ {
88
+ id: 'drift-detected:resolve_drift_violations',
89
+ event: 'drift:detected',
90
+ action: 'resolve_drift_violations',
91
+ enabled: true,
92
+ condition: { minDriftScore: 0.3 },
93
+ description: 'Auto-resolves drift violations when score exceeds threshold',
94
+ },
95
+ {
96
+ id: 'drift-detected:log_lesson',
97
+ event: 'drift:detected',
98
+ action: 'log_lesson',
99
+ enabled: true,
100
+ description: 'Logs lesson learned when drift is detected',
101
+ },
102
+ // ── cron:daily ────────────────────────────────────────────────────────────
103
+ {
104
+ id: 'cron:health_check_all',
105
+ event: 'cron:daily',
106
+ action: 'health_check_all',
107
+ enabled: false,
108
+ description: 'Daily health check across all specs (opt-in)',
109
+ },
110
+ {
111
+ id: 'cron:velocity_report',
112
+ event: 'cron:daily',
113
+ action: 'velocity_report',
114
+ enabled: false,
115
+ description: 'Daily velocity metrics (opt-in)',
116
+ },
117
+ {
118
+ id: 'cron:estimation_accuracy_report',
119
+ event: 'cron:daily',
120
+ action: 'estimation_accuracy_report',
121
+ enabled: false,
122
+ description: 'Daily estimation calibration (opt-in)',
123
+ },
124
+ ];
125
+ //# sourceMappingURL=trigger-rules.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trigger-rules.js","sourceRoot":"","sources":["../../../src/engine/autopilot/trigger-rules.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ import type { CompetitiveCatalog, GapAnalysisResult } from '../../types/index.js';
2
+ /**
3
+ * Extracts lowercase keywords from free text, removing stop words and
4
+ * short tokens. Returns a deduplicated array.
5
+ */
6
+ export declare function extractKeywords(text: string): string[];
7
+ /**
8
+ * Analyzes a feature (expressed as keywords) against all competitors in the
9
+ * catalog and returns a structured GapAnalysisResult.
10
+ */
11
+ export declare function analyzeGap(featureKeywords: string[], catalog: CompetitiveCatalog): GapAnalysisResult;
12
+ //# sourceMappingURL=gap-analyzer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gap-analyzer.d.ts","sourceRoot":"","sources":["../../../src/engine/competitive/gap-analyzer.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAmHlF;;;GAGG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAQtD;AA4CD;;;GAGG;AACH,wBAAgB,UAAU,CACxB,eAAe,EAAE,MAAM,EAAE,EACzB,OAAO,EAAE,kBAAkB,GAC1B,iBAAiB,CAyEnB"}
@@ -0,0 +1,214 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Planu unique capabilities — source of truth
3
+ // ---------------------------------------------------------------------------
4
+ const PLANU_CAPABILITIES = [
5
+ 'mcp-protocol',
6
+ 'spec-driven-development',
7
+ 'drift-detection',
8
+ 'drift-watcher',
9
+ 'compliance-reporting',
10
+ 'ears-notation',
11
+ 'workflow-checkpoints',
12
+ 'context-profiles',
13
+ 'competitive-gap-analysis',
14
+ 'velocity-metrics',
15
+ 'auto-fix-health',
16
+ 'license-tiers',
17
+ 'multi-language-docs',
18
+ 'ai-code-alignment',
19
+ 'spec-health-scoring',
20
+ 'spec-history',
21
+ 'bdd-gherkin',
22
+ 'tdd-scaffold',
23
+ 'coverage-gap-analysis',
24
+ 'red-team',
25
+ 'challenge-spec',
26
+ 'require-checkpoint',
27
+ 'approve-checkpoint',
28
+ 'generate-compliance-evidence',
29
+ 'pull-from-notion',
30
+ 'pull-from-asana',
31
+ 'pull-from-monday',
32
+ 'validate-criteria-quality',
33
+ 'rewrite-criteria-ears',
34
+ 'session-checkpoint',
35
+ 'audit-claude-config',
36
+ ];
37
+ // ---------------------------------------------------------------------------
38
+ // Stop words for keyword extraction
39
+ // ---------------------------------------------------------------------------
40
+ const STOP_WORDS = new Set([
41
+ 'a',
42
+ 'an',
43
+ 'the',
44
+ 'and',
45
+ 'or',
46
+ 'is',
47
+ 'in',
48
+ 'on',
49
+ 'at',
50
+ 'to',
51
+ 'for',
52
+ 'of',
53
+ 'with',
54
+ 'by',
55
+ 'from',
56
+ 'as',
57
+ 'be',
58
+ 'are',
59
+ 'was',
60
+ 'were',
61
+ 'has',
62
+ 'have',
63
+ 'had',
64
+ 'do',
65
+ 'does',
66
+ 'did',
67
+ 'will',
68
+ 'would',
69
+ 'can',
70
+ 'could',
71
+ 'should',
72
+ 'may',
73
+ 'might',
74
+ 'must',
75
+ 'shall',
76
+ 'it',
77
+ 'its',
78
+ 'this',
79
+ 'that',
80
+ 'which',
81
+ 'who',
82
+ 'what',
83
+ 'when',
84
+ 'where',
85
+ 'how',
86
+ 'not',
87
+ 'no',
88
+ 'so',
89
+ 'if',
90
+ 'but',
91
+ 'yet',
92
+ 'than',
93
+ 'then',
94
+ 'also',
95
+ 'any',
96
+ 'all',
97
+ 'each',
98
+ 'such',
99
+ 'we',
100
+ 'our',
101
+ 'you',
102
+ 'your',
103
+ 'they',
104
+ 'their',
105
+ 'he',
106
+ 'she',
107
+ 'his',
108
+ 'her',
109
+ ]);
110
+ /**
111
+ * Extracts lowercase keywords from free text, removing stop words and
112
+ * short tokens. Returns a deduplicated array.
113
+ */
114
+ export function extractKeywords(text) {
115
+ const tokens = text
116
+ .toLowerCase()
117
+ .split(/[\s\-,.:;!?()[\]{}"'/\\|<>@#$%^&*+=~`]+/)
118
+ .map((t) => t.trim())
119
+ .filter((t) => t.length >= 3 && !STOP_WORDS.has(t));
120
+ return [...new Set(tokens)];
121
+ }
122
+ // ---------------------------------------------------------------------------
123
+ // Capability matching helpers
124
+ // ---------------------------------------------------------------------------
125
+ /**
126
+ * Returns true if any capability in `capabilities` contains `keyword` as a
127
+ * substring (case-insensitive after normalization).
128
+ */
129
+ function capabilityMatchesKeyword(capabilities, keyword) {
130
+ const lower = keyword.toLowerCase();
131
+ return capabilities.some((cap) => cap.toLowerCase().includes(lower));
132
+ }
133
+ /**
134
+ * Checks whether a list of capabilities matches at least one of the feature
135
+ * keywords.
136
+ */
137
+ function hasFeatureMatch(capabilities, keywords) {
138
+ return keywords.some((kw) => capabilityMatchesKeyword(capabilities, kw));
139
+ }
140
+ // ---------------------------------------------------------------------------
141
+ // Positioning thresholds
142
+ // ---------------------------------------------------------------------------
143
+ const PARITY_MIN = 2;
144
+ const PARITY_MAX = 4;
145
+ function computePositioning(matchCount) {
146
+ if (matchCount < PARITY_MIN) {
147
+ return 'differentiator';
148
+ }
149
+ if (matchCount <= PARITY_MAX) {
150
+ return 'parity';
151
+ }
152
+ return 'lagging';
153
+ }
154
+ // ---------------------------------------------------------------------------
155
+ // Main analysis function
156
+ // ---------------------------------------------------------------------------
157
+ /**
158
+ * Analyzes a feature (expressed as keywords) against all competitors in the
159
+ * catalog and returns a structured GapAnalysisResult.
160
+ */
161
+ export function analyzeGap(featureKeywords, catalog) {
162
+ const allCompetitors = Object.values(catalog.competitors);
163
+ // Build coverage map: competitorId -> whether they match any keyword
164
+ const coverage = {};
165
+ let matchCount = 0;
166
+ for (const competitor of allCompetitors) {
167
+ const matched = hasFeatureMatch(competitor.capabilities, featureKeywords);
168
+ coverage[competitor.id] = matched;
169
+ if (matched) {
170
+ matchCount++;
171
+ }
172
+ }
173
+ // Determine positioning
174
+ const positioning = computePositioning(matchCount);
175
+ // Compute Planu-unique capabilities: in PLANU_CAPABILITIES but not in any competitor
176
+ const allCompetitorCaps = allCompetitors.flatMap((c) => c.capabilities.map((cap) => cap.toLowerCase()));
177
+ const uniqueToPlanu = PLANU_CAPABILITIES.filter((cap) => !allCompetitorCaps.some((cc) => cc.includes(cap.toLowerCase())));
178
+ // Gaps: feature keywords not covered by Planu or any competitor
179
+ const planusCapabilitiesLower = PLANU_CAPABILITIES.map((c) => c.toLowerCase());
180
+ const gaps = featureKeywords.filter((kw) => !capabilityMatchesKeyword(planusCapabilitiesLower, kw) &&
181
+ !hasFeatureMatch(allCompetitors.flatMap((c) => c.capabilities), [kw]));
182
+ // Suggestions: derived from weaknesses of competitors that DO have the feature
183
+ const matchingCompetitors = allCompetitors.filter((c) => coverage[c.id] === true);
184
+ const suggestionSet = new Set();
185
+ for (const competitor of matchingCompetitors) {
186
+ for (const weakness of competitor.weaknesses) {
187
+ suggestionSet.add(`Differentiate from ${competitor.name}: ${weakness}`);
188
+ }
189
+ }
190
+ const suggestions = [...suggestionSet];
191
+ // Summary
192
+ const competitorNames = matchingCompetitors.map((c) => c.name);
193
+ const positioningLabel = positioning === 'differentiator'
194
+ ? 'a strong differentiator'
195
+ : positioning === 'parity'
196
+ ? 'at parity'
197
+ : 'potentially lagging';
198
+ const summary = `Feature analysis: Planu is ${positioningLabel} in this area. ` +
199
+ (competitorNames.length > 0
200
+ ? `Competitors with similar capabilities: ${competitorNames.join(', ')}. `
201
+ : 'No direct competitor matches found. ') +
202
+ (uniqueToPlanu.length > 0
203
+ ? `Planu-unique capabilities: ${uniqueToPlanu.slice(0, 5).join(', ')}${uniqueToPlanu.length > 5 ? ` (+${uniqueToPlanu.length - 5} more)` : ''}.`
204
+ : 'No unique capabilities identified for this query.');
205
+ return {
206
+ positioning,
207
+ coverage,
208
+ uniqueToPlanu,
209
+ gaps,
210
+ suggestions,
211
+ summary,
212
+ };
213
+ }
214
+ //# sourceMappingURL=gap-analyzer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gap-analyzer.js","sourceRoot":"","sources":["../../../src/engine/competitive/gap-analyzer.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"}