@girardmedia/bootspring 1.2.0 → 2.0.3

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 (253) hide show
  1. package/README.md +107 -14
  2. package/bin/bootspring.js +166 -27
  3. package/cli/agent.js +189 -17
  4. package/cli/analyze.js +499 -0
  5. package/cli/audit.js +557 -0
  6. package/cli/auth.js +495 -38
  7. package/cli/billing.js +302 -0
  8. package/cli/build.js +695 -0
  9. package/cli/business.js +109 -26
  10. package/cli/checkpoint-utils.js +168 -0
  11. package/cli/checkpoint.js +639 -0
  12. package/cli/cloud-sync.js +447 -0
  13. package/cli/content.js +198 -0
  14. package/cli/context.js +1 -1
  15. package/cli/deploy.js +543 -0
  16. package/cli/fundraise.js +112 -50
  17. package/cli/github-cmd.js +435 -0
  18. package/cli/health.js +477 -0
  19. package/cli/init.js +84 -13
  20. package/cli/legal.js +107 -95
  21. package/cli/log.js +2 -2
  22. package/cli/loop.js +976 -73
  23. package/cli/manager.js +711 -0
  24. package/cli/metrics.js +480 -0
  25. package/cli/monitor.js +812 -0
  26. package/cli/onboard.js +521 -0
  27. package/cli/orchestrator.js +12 -24
  28. package/cli/prd.js +594 -0
  29. package/cli/preseed-start.js +1483 -0
  30. package/cli/preseed.js +2302 -0
  31. package/cli/project.js +436 -0
  32. package/cli/quality.js +233 -0
  33. package/cli/security.js +913 -0
  34. package/cli/seed.js +1441 -5
  35. package/cli/skill.js +273 -211
  36. package/cli/suggest.js +989 -0
  37. package/cli/switch.js +453 -0
  38. package/cli/visualize.js +527 -0
  39. package/cli/watch.js +769 -0
  40. package/cli/workspace.js +607 -0
  41. package/core/analyze-workflow.js +1134 -0
  42. package/core/api-client.js +535 -22
  43. package/core/audit-workflow.js +1350 -0
  44. package/core/build-orchestrator.js +480 -0
  45. package/core/build-state.js +577 -0
  46. package/core/checkpoint-engine.js +408 -0
  47. package/core/config.js +1109 -26
  48. package/core/context-loader.js +21 -1
  49. package/core/deploy-workflow.js +836 -0
  50. package/core/entitlements.js +93 -22
  51. package/core/github-sync.js +610 -0
  52. package/core/index.js +8 -1
  53. package/core/ingest.js +1111 -0
  54. package/core/metrics-engine.js +768 -0
  55. package/core/onboard-workflow.js +1007 -0
  56. package/core/preseed-workflow.js +934 -0
  57. package/core/preseed.js +1617 -0
  58. package/core/project-context.js +325 -0
  59. package/core/project-state.js +694 -0
  60. package/core/r2-sync.js +583 -0
  61. package/core/scaffold.js +525 -7
  62. package/core/session.js +258 -0
  63. package/core/task-extractor.js +758 -0
  64. package/core/telemetry.js +28 -6
  65. package/core/tier-enforcement.js +737 -0
  66. package/core/utils.js +38 -14
  67. package/generators/questionnaire.js +15 -12
  68. package/generators/sections/ai.js +7 -7
  69. package/generators/sections/content.js +300 -0
  70. package/generators/sections/index.js +3 -0
  71. package/generators/sections/plugins.js +7 -6
  72. package/generators/templates/build-planning.template.js +596 -0
  73. package/generators/templates/content.template.js +819 -0
  74. package/generators/templates/index.js +2 -1
  75. package/hooks/git-autopilot.js +1250 -0
  76. package/hooks/index.js +9 -0
  77. package/intelligence/agent-collab.js +2057 -0
  78. package/intelligence/auto-suggest.js +634 -0
  79. package/intelligence/content-gen.js +1589 -0
  80. package/intelligence/cross-project.js +1647 -0
  81. package/intelligence/index.js +184 -0
  82. package/intelligence/learning/insights.json +517 -7
  83. package/intelligence/learning/pattern-learner.js +1008 -14
  84. package/intelligence/memory/decision-tracker.js +1431 -31
  85. package/intelligence/memory/decisions.jsonl +0 -0
  86. package/intelligence/orchestrator.js +2896 -1
  87. package/intelligence/prd.js +92 -1
  88. package/intelligence/recommendation-weights.json +14 -2
  89. package/intelligence/recommendations.js +463 -9
  90. package/intelligence/workflow-composer.js +1451 -0
  91. package/marketplace/index.d.ts +324 -0
  92. package/marketplace/index.js +1921 -0
  93. package/mcp/contracts/mcp-contract.v1.json +342 -4
  94. package/mcp/registry.js +680 -3
  95. package/mcp/response-formatter.js +23 -0
  96. package/mcp/tools/assist-tool.js +78 -4
  97. package/mcp/tools/autopilot-tool.js +408 -0
  98. package/mcp/tools/content-tool.js +571 -0
  99. package/mcp/tools/dashboard-tool.js +251 -5
  100. package/mcp/tools/mvp-tool.js +344 -0
  101. package/mcp/tools/plugin-tool.js +23 -1
  102. package/mcp/tools/prd-tool.js +579 -0
  103. package/mcp/tools/seed-tool.js +447 -0
  104. package/mcp/tools/skill-tool.js +43 -14
  105. package/mcp/tools/suggest-tool.js +147 -0
  106. package/package.json +15 -6
  107. package/agents/README.md +0 -93
  108. package/agents/ai-integration-expert/context.md +0 -386
  109. package/agents/api-expert/context.md +0 -416
  110. package/agents/architecture-expert/context.md +0 -454
  111. package/agents/auth-expert/context.md +0 -399
  112. package/agents/backend-expert/context.md +0 -483
  113. package/agents/business-strategy-expert/context.md +0 -180
  114. package/agents/code-review-expert/context.md +0 -365
  115. package/agents/competitive-analysis-expert/context.md +0 -239
  116. package/agents/data-modeling-expert/context.md +0 -352
  117. package/agents/database-expert/context.md +0 -250
  118. package/agents/devops-expert/context.md +0 -446
  119. package/agents/email-expert/context.md +0 -379
  120. package/agents/financial-expert/context.md +0 -213
  121. package/agents/frontend-expert/context.md +0 -364
  122. package/agents/fundraising-expert/context.md +0 -257
  123. package/agents/growth-expert/context.md +0 -249
  124. package/agents/index.js +0 -140
  125. package/agents/investor-relations-expert/context.md +0 -266
  126. package/agents/legal-expert/context.md +0 -284
  127. package/agents/marketing-expert/context.md +0 -236
  128. package/agents/monitoring-expert/context.md +0 -362
  129. package/agents/operations-expert/context.md +0 -279
  130. package/agents/partnerships-expert/context.md +0 -286
  131. package/agents/payment-expert/context.md +0 -340
  132. package/agents/performance-expert/context.md +0 -377
  133. package/agents/private-equity-expert/context.md +0 -246
  134. package/agents/railway-expert/context.md +0 -284
  135. package/agents/research-expert/context.md +0 -245
  136. package/agents/sales-expert/context.md +0 -241
  137. package/agents/security-expert/context.md +0 -343
  138. package/agents/testing-expert/context.md +0 -414
  139. package/agents/ui-ux-expert/context.md +0 -448
  140. package/agents/vercel-expert/context.md +0 -426
  141. package/skills/index.js +0 -787
  142. package/skills/patterns/README.md +0 -163
  143. package/skills/patterns/ai/agents.md +0 -281
  144. package/skills/patterns/ai/claude.md +0 -138
  145. package/skills/patterns/ai/embeddings.md +0 -150
  146. package/skills/patterns/ai/rag.md +0 -266
  147. package/skills/patterns/ai/streaming.md +0 -170
  148. package/skills/patterns/ai/structured-output.md +0 -162
  149. package/skills/patterns/ai/tools.md +0 -154
  150. package/skills/patterns/analytics/tracking.md +0 -220
  151. package/skills/patterns/api/errors.md +0 -296
  152. package/skills/patterns/api/graphql.md +0 -440
  153. package/skills/patterns/api/middleware.md +0 -279
  154. package/skills/patterns/api/openapi.md +0 -285
  155. package/skills/patterns/api/rate-limiting.md +0 -231
  156. package/skills/patterns/api/route-handler.md +0 -217
  157. package/skills/patterns/api/server-action.md +0 -249
  158. package/skills/patterns/api/versioning.md +0 -443
  159. package/skills/patterns/api/webhooks.md +0 -247
  160. package/skills/patterns/auth/clerk.md +0 -132
  161. package/skills/patterns/auth/mfa.md +0 -313
  162. package/skills/patterns/auth/nextauth.md +0 -140
  163. package/skills/patterns/auth/oauth.md +0 -237
  164. package/skills/patterns/auth/rbac.md +0 -152
  165. package/skills/patterns/auth/session-management.md +0 -367
  166. package/skills/patterns/auth/session.md +0 -120
  167. package/skills/patterns/database/audit.md +0 -177
  168. package/skills/patterns/database/migrations.md +0 -177
  169. package/skills/patterns/database/pagination.md +0 -230
  170. package/skills/patterns/database/pooling.md +0 -357
  171. package/skills/patterns/database/prisma.md +0 -180
  172. package/skills/patterns/database/relations.md +0 -187
  173. package/skills/patterns/database/seeding.md +0 -246
  174. package/skills/patterns/database/soft-delete.md +0 -153
  175. package/skills/patterns/database/transactions.md +0 -162
  176. package/skills/patterns/deployment/ci-cd.md +0 -231
  177. package/skills/patterns/deployment/docker.md +0 -188
  178. package/skills/patterns/deployment/monitoring.md +0 -387
  179. package/skills/patterns/deployment/vercel.md +0 -160
  180. package/skills/patterns/email/resend.md +0 -143
  181. package/skills/patterns/email/templates.md +0 -245
  182. package/skills/patterns/email/transactional.md +0 -503
  183. package/skills/patterns/email/verification.md +0 -176
  184. package/skills/patterns/files/download.md +0 -243
  185. package/skills/patterns/files/upload.md +0 -239
  186. package/skills/patterns/i18n/nextintl.md +0 -188
  187. package/skills/patterns/logging/structured.md +0 -292
  188. package/skills/patterns/notifications/email-queue.md +0 -248
  189. package/skills/patterns/notifications/push.md +0 -279
  190. package/skills/patterns/payments/checkout.md +0 -303
  191. package/skills/patterns/payments/invoices.md +0 -287
  192. package/skills/patterns/payments/portal.md +0 -245
  193. package/skills/patterns/payments/stripe.md +0 -272
  194. package/skills/patterns/payments/subscriptions.md +0 -300
  195. package/skills/patterns/payments/usage.md +0 -279
  196. package/skills/patterns/performance/caching.md +0 -276
  197. package/skills/patterns/performance/code-splitting.md +0 -233
  198. package/skills/patterns/performance/edge.md +0 -254
  199. package/skills/patterns/performance/isr.md +0 -266
  200. package/skills/patterns/performance/lazy-loading.md +0 -281
  201. package/skills/patterns/realtime/sse.md +0 -327
  202. package/skills/patterns/realtime/websockets.md +0 -336
  203. package/skills/patterns/search/filtering.md +0 -329
  204. package/skills/patterns/search/fulltext.md +0 -260
  205. package/skills/patterns/security/audit-logging.md +0 -444
  206. package/skills/patterns/security/csrf.md +0 -234
  207. package/skills/patterns/security/headers.md +0 -252
  208. package/skills/patterns/security/sanitization.md +0 -258
  209. package/skills/patterns/security/secrets.md +0 -261
  210. package/skills/patterns/security/validation.md +0 -268
  211. package/skills/patterns/security/xss.md +0 -229
  212. package/skills/patterns/seo/metadata.md +0 -252
  213. package/skills/patterns/state/context.md +0 -349
  214. package/skills/patterns/state/react-query.md +0 -313
  215. package/skills/patterns/state/url-state.md +0 -482
  216. package/skills/patterns/state/zustand.md +0 -262
  217. package/skills/patterns/testing/api.md +0 -259
  218. package/skills/patterns/testing/component.md +0 -233
  219. package/skills/patterns/testing/coverage.md +0 -207
  220. package/skills/patterns/testing/fixtures.md +0 -225
  221. package/skills/patterns/testing/integration.md +0 -436
  222. package/skills/patterns/testing/mocking.md +0 -177
  223. package/skills/patterns/testing/playwright.md +0 -162
  224. package/skills/patterns/testing/snapshot.md +0 -175
  225. package/skills/patterns/testing/vitest.md +0 -307
  226. package/skills/patterns/ui/accordions.md +0 -395
  227. package/skills/patterns/ui/cards.md +0 -299
  228. package/skills/patterns/ui/dropdowns.md +0 -476
  229. package/skills/patterns/ui/empty-states.md +0 -320
  230. package/skills/patterns/ui/forms.md +0 -405
  231. package/skills/patterns/ui/inputs.md +0 -319
  232. package/skills/patterns/ui/layouts.md +0 -282
  233. package/skills/patterns/ui/loading.md +0 -291
  234. package/skills/patterns/ui/modals.md +0 -338
  235. package/skills/patterns/ui/navigation.md +0 -374
  236. package/skills/patterns/ui/tables.md +0 -407
  237. package/skills/patterns/ui/toasts.md +0 -300
  238. package/skills/patterns/ui/tooltips.md +0 -396
  239. package/skills/patterns/utils/dates.md +0 -435
  240. package/skills/patterns/utils/errors.md +0 -451
  241. package/skills/patterns/utils/formatting.md +0 -345
  242. package/skills/patterns/utils/validation.md +0 -434
  243. package/templates/bootspring.config.js +0 -83
  244. package/templates/business/business-model-canvas.md +0 -246
  245. package/templates/business/business-plan.md +0 -266
  246. package/templates/business/competitive-analysis.md +0 -312
  247. package/templates/fundraising/data-room-checklist.md +0 -300
  248. package/templates/fundraising/investor-research.md +0 -243
  249. package/templates/fundraising/pitch-deck-outline.md +0 -253
  250. package/templates/legal/gdpr-checklist.md +0 -339
  251. package/templates/legal/privacy-policy.md +0 -285
  252. package/templates/legal/terms-of-service.md +0 -222
  253. package/templates/mcp.json +0 -9
@@ -0,0 +1,1250 @@
1
+ /**
2
+ * Bootspring Git Autopilot
3
+ *
4
+ * Automatically triggers workflows based on git events.
5
+ * Monitors git activity and starts appropriate workflows.
6
+ *
7
+ * Features:
8
+ * - Real-time git event watching (commits, branches, merges)
9
+ * - Configurable via bootspring.config.js
10
+ * - Integration with orchestrator workflows
11
+ * - Smart pattern matching for branch names and commit messages
12
+ *
13
+ * @package bootspring
14
+ * @module hooks/git-autopilot
15
+ */
16
+
17
+ const { execSync } = require('child_process');
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+ const EventEmitter = require('events');
21
+
22
+ /**
23
+ * Git event to workflow mapping
24
+ */
25
+ const EVENT_WORKFLOWS = {
26
+ // Branch patterns
27
+ 'branch:feature/*': 'feature-development',
28
+ 'branch:feat/*': 'feature-development',
29
+ 'branch:fix/*': 'feature-development',
30
+ 'branch:hotfix/*': 'security-audit',
31
+ 'branch:release/*': 'launch-preparation',
32
+ 'branch:perf/*': 'performance-optimization',
33
+ 'branch:security/*': 'security-audit',
34
+ 'branch:api/*': 'api-development',
35
+ 'branch:db/*': 'database-migration',
36
+ 'branch:migration/*': 'database-migration',
37
+
38
+ // Commit message patterns
39
+ 'commit:feat:': 'feature-development',
40
+ 'commit:feature:': 'feature-development',
41
+ 'commit:fix:': 'feature-development',
42
+ 'commit:security:': 'security-audit',
43
+ 'commit:perf:': 'performance-optimization',
44
+ 'commit:refactor:': 'performance-optimization',
45
+ 'commit:db:': 'database-migration',
46
+ 'commit:migration:': 'database-migration',
47
+
48
+ // File change patterns
49
+ 'files:prisma/': 'database-migration',
50
+ 'files:schema.prisma': 'database-migration',
51
+ 'files:src/api/': 'api-development',
52
+ 'files:app/api/': 'api-development',
53
+ 'files:pages/api/': 'api-development',
54
+
55
+ // Merge patterns
56
+ 'merge:main': 'launch-preparation',
57
+ 'merge:master': 'launch-preparation',
58
+ 'merge:production': 'launch-preparation',
59
+
60
+ // Tag patterns
61
+ 'tag:v*': 'launch-preparation',
62
+ 'tag:release-*': 'launch-preparation'
63
+ };
64
+
65
+ /**
66
+ * Autopilot configuration defaults
67
+ */
68
+ const DEFAULT_CONFIG = {
69
+ enabled: false,
70
+ autoStart: false, // Automatically start suggested workflow
71
+ requireConfirmation: true,
72
+ watchInterval: 5000, // Interval for polling git state (ms)
73
+ ignoreBranches: ['main', 'master', 'develop'],
74
+ ignorePatterns: ['wip:', 'WIP:', '[skip ci]', '[skip workflow]'],
75
+ eventMapping: EVENT_WORKFLOWS,
76
+ hooks: {
77
+ onBranchCreate: true,
78
+ onCommit: true,
79
+ onMerge: true,
80
+ onTag: true,
81
+ onFileChange: true
82
+ },
83
+ // Security: only allow specific workflows to auto-start
84
+ allowedAutoStartWorkflows: [
85
+ 'feature-development',
86
+ 'api-development'
87
+ ],
88
+ // Cooldown period between workflow triggers (ms)
89
+ cooldownPeriod: 60000
90
+ };
91
+
92
+ /**
93
+ * Git Autopilot Event Emitter
94
+ * Emits events for git activities
95
+ */
96
+ class GitAutopilotEmitter extends EventEmitter {}
97
+ const autopilotEmitter = new GitAutopilotEmitter();
98
+
99
+ /**
100
+ * In-memory state for the watcher
101
+ */
102
+ const watcherState = {
103
+ isWatching: false,
104
+ watcherId: null,
105
+ lastBranch: null,
106
+ lastCommit: null,
107
+ lastMergeBase: null,
108
+ lastTags: [],
109
+ lastTriggerTime: 0,
110
+ processedCommits: new Set()
111
+ };
112
+
113
+ /**
114
+ * Get autopilot state file path
115
+ */
116
+ function getAutopilotPath() {
117
+ const projectRoot = process.cwd();
118
+ return path.join(projectRoot, '.bootspring', 'autopilot-state.json');
119
+ }
120
+
121
+ /**
122
+ * Load configuration from bootspring.config.js
123
+ */
124
+ function loadConfig() {
125
+ const projectRoot = process.cwd();
126
+ const configPaths = [
127
+ path.join(projectRoot, 'bootspring.config.js'),
128
+ path.join(projectRoot, 'bootspring.config.mjs'),
129
+ path.join(projectRoot, '.bootspringrc.js')
130
+ ];
131
+
132
+ for (const configPath of configPaths) {
133
+ if (fs.existsSync(configPath)) {
134
+ try {
135
+ // Clear require cache to get fresh config
136
+ delete require.cache[require.resolve(configPath)];
137
+ const config = require(configPath);
138
+
139
+ if (config.autopilot || config.gitAutopilot) {
140
+ return mergeConfig(DEFAULT_CONFIG, config.autopilot || config.gitAutopilot);
141
+ }
142
+ } catch (err) {
143
+ console.warn(`Failed to load config from ${configPath}:`, err.message);
144
+ }
145
+ }
146
+ }
147
+
148
+ return DEFAULT_CONFIG;
149
+ }
150
+
151
+ /**
152
+ * Deep merge configuration objects
153
+ */
154
+ function mergeConfig(defaults, overrides) {
155
+ const result = { ...defaults };
156
+
157
+ for (const [key, value] of Object.entries(overrides)) {
158
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
159
+ result[key] = mergeConfig(defaults[key] || {}, value);
160
+ } else {
161
+ result[key] = value;
162
+ }
163
+ }
164
+
165
+ return result;
166
+ }
167
+
168
+ /**
169
+ * Load autopilot state
170
+ */
171
+ function loadAutopilotState() {
172
+ const statePath = getAutopilotPath();
173
+ if (fs.existsSync(statePath)) {
174
+ try {
175
+ const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
176
+ // Load config from bootspring.config.js first
177
+ const fileConfig = loadConfig();
178
+ // Merge: defaults <- file config <- persisted state config
179
+ // This ensures persisted runtime state (enabled, autoStart) takes precedence
180
+ state.config = mergeConfig(mergeConfig(DEFAULT_CONFIG, fileConfig), state.config || {});
181
+ return state;
182
+ } catch {
183
+ return createDefaultState();
184
+ }
185
+ }
186
+ return createDefaultState();
187
+ }
188
+
189
+ /**
190
+ * Create default autopilot state
191
+ */
192
+ function createDefaultState() {
193
+ const config = loadConfig();
194
+ return {
195
+ config,
196
+ lastCheck: null,
197
+ pendingSuggestions: [],
198
+ history: [],
199
+ triggeredWorkflows: [],
200
+ watcherActive: false
201
+ };
202
+ }
203
+
204
+ /**
205
+ * Save autopilot state
206
+ */
207
+ function saveAutopilotState(state) {
208
+ const statePath = getAutopilotPath();
209
+ const dir = path.dirname(statePath);
210
+ if (!fs.existsSync(dir)) {
211
+ fs.mkdirSync(dir, { recursive: true });
212
+ }
213
+ state.lastUpdated = new Date().toISOString();
214
+ fs.writeFileSync(statePath, JSON.stringify(state, null, 2));
215
+ }
216
+
217
+ /**
218
+ * Get current git branch
219
+ */
220
+ function getCurrentBranch() {
221
+ try {
222
+ return execSync('git branch --show-current', { encoding: 'utf-8' }).trim();
223
+ } catch {
224
+ return null;
225
+ }
226
+ }
227
+
228
+ /**
229
+ * Get the current HEAD commit hash
230
+ */
231
+ function getCurrentCommit() {
232
+ try {
233
+ return execSync('git rev-parse HEAD', { encoding: 'utf-8' }).trim();
234
+ } catch {
235
+ return null;
236
+ }
237
+ }
238
+
239
+ /**
240
+ * Get recent commits
241
+ */
242
+ function getRecentCommits(count = 5) {
243
+ try {
244
+ const output = execSync(`git log -${count} --pretty=format:"%H|%s|%an|%ai"`, { encoding: 'utf-8' });
245
+ return output.split('\n').filter(Boolean).map(line => {
246
+ const [hash, message, author, date] = line.split('|');
247
+ return { hash, message, author, date };
248
+ });
249
+ } catch {
250
+ return [];
251
+ }
252
+ }
253
+
254
+ /**
255
+ * Get changed files in recent commits
256
+ */
257
+ function getChangedFiles(count = 5) {
258
+ try {
259
+ const output = execSync(`git diff --name-only HEAD~${count}..HEAD 2>/dev/null || git diff --name-only HEAD`, { encoding: 'utf-8' });
260
+ return output.split('\n').filter(Boolean);
261
+ } catch {
262
+ return [];
263
+ }
264
+ }
265
+
266
+ /**
267
+ * Get recent tags
268
+ */
269
+ function getRecentTags(count = 3) {
270
+ try {
271
+ const output = execSync(`git tag --sort=-creatordate 2>/dev/null | head -${count}`, { encoding: 'utf-8', shell: true });
272
+ return output.split('\n').filter(Boolean);
273
+ } catch {
274
+ return [];
275
+ }
276
+ }
277
+
278
+ /**
279
+ * Check if current branch was recently merged from another
280
+ */
281
+ function checkForMerge(targetBranch = 'main') {
282
+ try {
283
+ // Get the merge base between HEAD and the target branch
284
+ const mergeBase = execSync(`git merge-base HEAD origin/${targetBranch} 2>/dev/null || echo ""`, {
285
+ encoding: 'utf-8',
286
+ shell: true
287
+ }).trim();
288
+
289
+ // Check if HEAD is an ancestor of target branch (meaning we merged TO it)
290
+ const isMerged = execSync(`git branch -r --contains HEAD 2>/dev/null | grep -E "origin/${targetBranch}$" || echo ""`, {
291
+ encoding: 'utf-8',
292
+ shell: true
293
+ }).trim();
294
+
295
+ return {
296
+ mergeBase,
297
+ isMerged: isMerged.length > 0,
298
+ targetBranch
299
+ };
300
+ } catch {
301
+ return { mergeBase: null, isMerged: false, targetBranch };
302
+ }
303
+ }
304
+
305
+ /**
306
+ * Detect if a new branch was created
307
+ */
308
+ function detectBranchCreation() {
309
+ try {
310
+ // Check if this branch has only a few commits that aren't on main/master
311
+ const baseCommitCount = execSync(
312
+ 'git rev-list --count HEAD ^origin/main 2>/dev/null || git rev-list --count HEAD ^origin/master 2>/dev/null || echo "0"',
313
+ { encoding: 'utf-8', shell: true }
314
+ ).trim();
315
+
316
+ const count = parseInt(baseCommitCount, 10);
317
+
318
+ // If less than 5 unique commits, likely a new feature branch
319
+ return count > 0 && count < 5;
320
+ } catch {
321
+ return false;
322
+ }
323
+ }
324
+
325
+ /**
326
+ * Match a pattern against a value
327
+ */
328
+ function matchPattern(pattern, value) {
329
+ // Simple glob matching
330
+ const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
331
+ return regex.test(value);
332
+ }
333
+
334
+ /**
335
+ * Check if a message should be ignored
336
+ */
337
+ function shouldIgnoreMessage(message, ignorePatterns) {
338
+ const msgLower = message.toLowerCase();
339
+ return ignorePatterns.some(pattern =>
340
+ msgLower.includes(pattern.toLowerCase())
341
+ );
342
+ }
343
+
344
+ /**
345
+ * Analyze git state and suggest workflow
346
+ */
347
+ function analyzeGitState(options = {}) {
348
+ const state = loadAutopilotState();
349
+
350
+ if (!state.config.enabled && !options.force) {
351
+ return { enabled: false, suggestions: [] };
352
+ }
353
+
354
+ const config = state.config;
355
+ const branch = getCurrentBranch();
356
+ const commits = getRecentCommits();
357
+ const files = getChangedFiles();
358
+ const tags = getRecentTags();
359
+ const mergeInfo = checkForMerge('main');
360
+ const isNewBranch = detectBranchCreation();
361
+
362
+ const suggestions = [];
363
+ const mapping = config.eventMapping || EVENT_WORKFLOWS;
364
+
365
+ // Check branch patterns
366
+ if (branch && !config.ignoreBranches.includes(branch)) {
367
+ for (const [pattern, workflow] of Object.entries(mapping)) {
368
+ if (pattern.startsWith('branch:')) {
369
+ const branchPattern = pattern.replace('branch:', '');
370
+ if (matchPattern(branchPattern, branch)) {
371
+ suggestions.push({
372
+ trigger: 'branch',
373
+ triggerType: isNewBranch ? 'branch_create' : 'branch_active',
374
+ pattern: branchPattern,
375
+ value: branch,
376
+ workflow,
377
+ confidence: isNewBranch ? 0.9 : 0.7,
378
+ isNewBranch,
379
+ autoStartEligible: config.allowedAutoStartWorkflows.includes(workflow)
380
+ });
381
+ }
382
+ }
383
+ }
384
+ }
385
+
386
+ // Check merge patterns
387
+ if (config.hooks.onMerge && mergeInfo.isMerged) {
388
+ for (const [pattern, workflow] of Object.entries(mapping)) {
389
+ if (pattern.startsWith('merge:')) {
390
+ const targetBranch = pattern.replace('merge:', '');
391
+ if (mergeInfo.targetBranch === targetBranch ||
392
+ matchPattern(targetBranch, mergeInfo.targetBranch)) {
393
+ suggestions.push({
394
+ trigger: 'merge',
395
+ triggerType: 'merge_to_main',
396
+ pattern: targetBranch,
397
+ value: `Merged to ${mergeInfo.targetBranch}`,
398
+ workflow,
399
+ confidence: 0.95,
400
+ mergeBase: mergeInfo.mergeBase,
401
+ autoStartEligible: config.allowedAutoStartWorkflows.includes(workflow)
402
+ });
403
+ }
404
+ }
405
+ }
406
+ }
407
+
408
+ // Check commit message patterns
409
+ if (config.hooks.onCommit) {
410
+ for (const commit of commits) {
411
+ // Skip ignored patterns
412
+ if (shouldIgnoreMessage(commit.message, config.ignorePatterns)) {
413
+ continue;
414
+ }
415
+
416
+ // Skip already processed commits
417
+ if (watcherState.processedCommits.has(commit.hash)) {
418
+ continue;
419
+ }
420
+
421
+ for (const [pattern, workflow] of Object.entries(mapping)) {
422
+ if (pattern.startsWith('commit:')) {
423
+ const messagePattern = pattern.replace('commit:', '');
424
+ if (commit.message.toLowerCase().startsWith(messagePattern)) {
425
+ suggestions.push({
426
+ trigger: 'commit',
427
+ triggerType: 'commit_message',
428
+ pattern: messagePattern,
429
+ value: commit.message,
430
+ workflow,
431
+ confidence: 0.8,
432
+ commitHash: commit.hash,
433
+ autoStartEligible: config.allowedAutoStartWorkflows.includes(workflow)
434
+ });
435
+ break; // Only one suggestion per commit
436
+ }
437
+ }
438
+ }
439
+ }
440
+ }
441
+
442
+ // Check file patterns
443
+ if (config.hooks.onFileChange) {
444
+ for (const file of files) {
445
+ for (const [pattern, workflow] of Object.entries(mapping)) {
446
+ if (pattern.startsWith('files:')) {
447
+ const filePattern = pattern.replace('files:', '');
448
+ if (file.startsWith(filePattern) || file.includes(filePattern)) {
449
+ suggestions.push({
450
+ trigger: 'file',
451
+ triggerType: 'file_change',
452
+ pattern: filePattern,
453
+ value: file,
454
+ workflow,
455
+ confidence: 0.7,
456
+ autoStartEligible: config.allowedAutoStartWorkflows.includes(workflow)
457
+ });
458
+ break; // Only one suggestion per file
459
+ }
460
+ }
461
+ }
462
+ }
463
+ }
464
+
465
+ // Check tag patterns
466
+ if (config.hooks.onTag) {
467
+ for (const tag of tags) {
468
+ for (const [pattern, workflow] of Object.entries(mapping)) {
469
+ if (pattern.startsWith('tag:')) {
470
+ const tagPattern = pattern.replace('tag:', '');
471
+ if (matchPattern(tagPattern, tag)) {
472
+ suggestions.push({
473
+ trigger: 'tag',
474
+ triggerType: 'tag_create',
475
+ pattern: tagPattern,
476
+ value: tag,
477
+ workflow,
478
+ confidence: 0.95,
479
+ autoStartEligible: config.allowedAutoStartWorkflows.includes(workflow)
480
+ });
481
+ }
482
+ }
483
+ }
484
+ }
485
+ }
486
+
487
+ // Deduplicate by workflow (keep highest confidence)
488
+ const uniqueWorkflows = new Map();
489
+ for (const suggestion of suggestions) {
490
+ const existing = uniqueWorkflows.get(suggestion.workflow);
491
+ if (!existing || existing.confidence < suggestion.confidence) {
492
+ uniqueWorkflows.set(suggestion.workflow, suggestion);
493
+ }
494
+ }
495
+
496
+ const finalSuggestions = Array.from(uniqueWorkflows.values())
497
+ .sort((a, b) => b.confidence - a.confidence);
498
+
499
+ // Update state
500
+ state.lastCheck = new Date().toISOString();
501
+ state.pendingSuggestions = finalSuggestions;
502
+ saveAutopilotState(state);
503
+
504
+ return {
505
+ enabled: true,
506
+ branch,
507
+ isNewBranch,
508
+ mergeInfo,
509
+ suggestions: finalSuggestions,
510
+ autoStartEnabled: config.autoStart
511
+ };
512
+ }
513
+
514
+ /**
515
+ * Enable autopilot
516
+ */
517
+ function enable(options = {}) {
518
+ const state = loadAutopilotState();
519
+ state.config.enabled = true;
520
+ state.config = { ...state.config, ...options };
521
+
522
+ state.history.push({
523
+ action: 'enabled',
524
+ timestamp: new Date().toISOString(),
525
+ options
526
+ });
527
+
528
+ saveAutopilotState(state);
529
+
530
+ return { success: true, config: state.config };
531
+ }
532
+
533
+ /**
534
+ * Disable autopilot
535
+ */
536
+ function disable() {
537
+ const state = loadAutopilotState();
538
+ state.config.enabled = false;
539
+
540
+ // Stop watcher if running
541
+ if (watcherState.isWatching) {
542
+ stopWatcher();
543
+ }
544
+
545
+ state.history.push({
546
+ action: 'disabled',
547
+ timestamp: new Date().toISOString()
548
+ });
549
+
550
+ saveAutopilotState(state);
551
+
552
+ return { success: true };
553
+ }
554
+
555
+ /**
556
+ * Get autopilot status
557
+ */
558
+ function getStatus() {
559
+ const state = loadAutopilotState();
560
+ return {
561
+ enabled: state.config.enabled,
562
+ config: state.config,
563
+ lastCheck: state.lastCheck,
564
+ pendingSuggestions: state.pendingSuggestions.length,
565
+ suggestions: state.pendingSuggestions,
566
+ watcherActive: watcherState.isWatching,
567
+ triggeredWorkflows: state.triggeredWorkflows || []
568
+ };
569
+ }
570
+
571
+ /**
572
+ * Accept a workflow suggestion
573
+ * @param {string} workflow - Workflow name to accept
574
+ */
575
+ function acceptSuggestion(workflow) {
576
+ const state = loadAutopilotState();
577
+
578
+ const suggestion = state.pendingSuggestions.find(s => s.workflow === workflow);
579
+ if (!suggestion) {
580
+ return { success: false, error: 'Suggestion not found' };
581
+ }
582
+
583
+ // Remove from pending
584
+ state.pendingSuggestions = state.pendingSuggestions.filter(s => s.workflow !== workflow);
585
+
586
+ state.history.push({
587
+ action: 'suggestion_accepted',
588
+ workflow,
589
+ trigger: suggestion.trigger,
590
+ triggerType: suggestion.triggerType,
591
+ timestamp: new Date().toISOString()
592
+ });
593
+
594
+ // Track triggered workflows
595
+ state.triggeredWorkflows = state.triggeredWorkflows || [];
596
+ state.triggeredWorkflows.push({
597
+ workflow,
598
+ trigger: suggestion.trigger,
599
+ timestamp: new Date().toISOString()
600
+ });
601
+
602
+ saveAutopilotState(state);
603
+
604
+ // Start the workflow
605
+ const orchestrator = require('../intelligence/orchestrator');
606
+ const result = orchestrator.startWorkflow(workflow);
607
+
608
+ // Emit event
609
+ autopilotEmitter.emit('workflow:started', {
610
+ workflow,
611
+ trigger: suggestion.trigger,
612
+ triggerType: suggestion.triggerType,
613
+ result
614
+ });
615
+
616
+ return {
617
+ success: true,
618
+ workflow,
619
+ startResult: result
620
+ };
621
+ }
622
+
623
+ /**
624
+ * Dismiss a workflow suggestion
625
+ * @param {string} workflow - Workflow name to dismiss
626
+ */
627
+ function dismissSuggestion(workflow) {
628
+ const state = loadAutopilotState();
629
+
630
+ state.pendingSuggestions = state.pendingSuggestions.filter(s => s.workflow !== workflow);
631
+
632
+ state.history.push({
633
+ action: 'suggestion_dismissed',
634
+ workflow,
635
+ timestamp: new Date().toISOString()
636
+ });
637
+
638
+ saveAutopilotState(state);
639
+
640
+ return { success: true, workflow };
641
+ }
642
+
643
+ /**
644
+ * Add custom event mapping
645
+ * @param {string} pattern - Event pattern (e.g., 'branch:custom/*')
646
+ * @param {string} workflow - Workflow to trigger
647
+ */
648
+ function addEventMapping(pattern, workflow) {
649
+ const state = loadAutopilotState();
650
+ state.config.eventMapping = state.config.eventMapping || {};
651
+ state.config.eventMapping[pattern] = workflow;
652
+ saveAutopilotState(state);
653
+
654
+ return { success: true, pattern, workflow };
655
+ }
656
+
657
+ /**
658
+ * Remove event mapping
659
+ * @param {string} pattern - Event pattern to remove
660
+ */
661
+ function removeEventMapping(pattern) {
662
+ const state = loadAutopilotState();
663
+ if (state.config.eventMapping && state.config.eventMapping[pattern]) {
664
+ delete state.config.eventMapping[pattern];
665
+ saveAutopilotState(state);
666
+ return { success: true, pattern };
667
+ }
668
+ return { success: false, error: 'Pattern not found' };
669
+ }
670
+
671
+ /**
672
+ * Start the git watcher
673
+ * Monitors git state and triggers workflows automatically
674
+ */
675
+ function startWatcher() {
676
+ const state = loadAutopilotState();
677
+
678
+ if (!state.config.enabled) {
679
+ return { success: false, error: 'Autopilot is not enabled. Call enable() first.' };
680
+ }
681
+
682
+ if (watcherState.isWatching) {
683
+ return { success: false, error: 'Watcher is already running' };
684
+ }
685
+
686
+ // Initialize watcher state
687
+ watcherState.lastBranch = getCurrentBranch();
688
+ watcherState.lastCommit = getCurrentCommit();
689
+ watcherState.lastTags = getRecentTags();
690
+ watcherState.isWatching = true;
691
+ watcherState.processedCommits = new Set();
692
+
693
+ const interval = state.config.watchInterval || 5000;
694
+
695
+ // Start the polling interval
696
+ watcherState.watcherId = setInterval(() => {
697
+ pollGitState();
698
+ }, interval);
699
+
700
+ // Update state
701
+ state.watcherActive = true;
702
+ state.watcherStartedAt = new Date().toISOString();
703
+ saveAutopilotState(state);
704
+
705
+ autopilotEmitter.emit('watcher:started', { interval });
706
+
707
+ return {
708
+ success: true,
709
+ interval,
710
+ message: `Git watcher started. Polling every ${interval}ms`
711
+ };
712
+ }
713
+
714
+ /**
715
+ * Stop the git watcher
716
+ */
717
+ function stopWatcher() {
718
+ if (!watcherState.isWatching) {
719
+ return { success: false, error: 'Watcher is not running' };
720
+ }
721
+
722
+ if (watcherState.watcherId) {
723
+ clearInterval(watcherState.watcherId);
724
+ watcherState.watcherId = null;
725
+ }
726
+
727
+ watcherState.isWatching = false;
728
+
729
+ const state = loadAutopilotState();
730
+ state.watcherActive = false;
731
+ state.watcherStoppedAt = new Date().toISOString();
732
+ saveAutopilotState(state);
733
+
734
+ autopilotEmitter.emit('watcher:stopped', {});
735
+
736
+ return { success: true, message: 'Git watcher stopped' };
737
+ }
738
+
739
+ /**
740
+ * Poll git state for changes
741
+ * Called periodically by the watcher
742
+ */
743
+ function pollGitState() {
744
+ const state = loadAutopilotState();
745
+ const config = state.config;
746
+
747
+ // Check for branch change
748
+ const currentBranch = getCurrentBranch();
749
+ if (currentBranch !== watcherState.lastBranch) {
750
+ const isNewBranch = detectBranchCreation();
751
+
752
+ autopilotEmitter.emit('git:branch_change', {
753
+ from: watcherState.lastBranch,
754
+ to: currentBranch,
755
+ isNew: isNewBranch
756
+ });
757
+
758
+ if (isNewBranch && config.hooks.onBranchCreate) {
759
+ handleBranchCreate(currentBranch);
760
+ }
761
+
762
+ watcherState.lastBranch = currentBranch;
763
+ }
764
+
765
+ // Check for new commits
766
+ const currentCommit = getCurrentCommit();
767
+ if (currentCommit !== watcherState.lastCommit) {
768
+ autopilotEmitter.emit('git:commit', {
769
+ from: watcherState.lastCommit,
770
+ to: currentCommit
771
+ });
772
+
773
+ if (config.hooks.onCommit) {
774
+ handleNewCommit(currentCommit);
775
+ }
776
+
777
+ watcherState.lastCommit = currentCommit;
778
+ }
779
+
780
+ // Check for merges to main
781
+ if (config.hooks.onMerge) {
782
+ const mergeInfo = checkForMerge('main');
783
+ if (mergeInfo.isMerged && mergeInfo.mergeBase !== watcherState.lastMergeBase) {
784
+ autopilotEmitter.emit('git:merge', {
785
+ targetBranch: mergeInfo.targetBranch,
786
+ mergeBase: mergeInfo.mergeBase
787
+ });
788
+
789
+ handleMerge(mergeInfo);
790
+ watcherState.lastMergeBase = mergeInfo.mergeBase;
791
+ }
792
+ }
793
+
794
+ // Check for new tags
795
+ if (config.hooks.onTag) {
796
+ const currentTags = getRecentTags();
797
+ const newTags = currentTags.filter(t => !watcherState.lastTags.includes(t));
798
+
799
+ if (newTags.length > 0) {
800
+ autopilotEmitter.emit('git:tag', { newTags });
801
+ handleNewTags(newTags);
802
+ watcherState.lastTags = currentTags;
803
+ }
804
+ }
805
+ }
806
+
807
+ /**
808
+ * Handle branch creation event
809
+ */
810
+ function handleBranchCreate(branch) {
811
+ const state = loadAutopilotState();
812
+ const config = state.config;
813
+
814
+ // Skip ignored branches
815
+ if (config.ignoreBranches.includes(branch)) {
816
+ return;
817
+ }
818
+
819
+ // Check cooldown
820
+ if (Date.now() - watcherState.lastTriggerTime < config.cooldownPeriod) {
821
+ return;
822
+ }
823
+
824
+ // Find matching workflow
825
+ for (const [pattern, workflow] of Object.entries(config.eventMapping)) {
826
+ if (pattern.startsWith('branch:')) {
827
+ const branchPattern = pattern.replace('branch:', '');
828
+ if (matchPattern(branchPattern, branch)) {
829
+ const suggestion = {
830
+ trigger: 'branch',
831
+ triggerType: 'branch_create',
832
+ pattern: branchPattern,
833
+ value: branch,
834
+ workflow,
835
+ confidence: 0.9,
836
+ isNewBranch: true,
837
+ autoStartEligible: config.allowedAutoStartWorkflows.includes(workflow),
838
+ timestamp: new Date().toISOString()
839
+ };
840
+
841
+ // Add to pending suggestions
842
+ state.pendingSuggestions = state.pendingSuggestions || [];
843
+ state.pendingSuggestions.push(suggestion);
844
+ saveAutopilotState(state);
845
+
846
+ autopilotEmitter.emit('suggestion:created', suggestion);
847
+
848
+ // Auto-start if configured
849
+ if (config.autoStart && suggestion.autoStartEligible) {
850
+ watcherState.lastTriggerTime = Date.now();
851
+ acceptSuggestion(workflow);
852
+ }
853
+
854
+ return; // Only one workflow per branch
855
+ }
856
+ }
857
+ }
858
+ }
859
+
860
+ /**
861
+ * Handle new commit event
862
+ */
863
+ function handleNewCommit(commitHash) {
864
+ const state = loadAutopilotState();
865
+ const config = state.config;
866
+
867
+ // Skip if already processed
868
+ if (watcherState.processedCommits.has(commitHash)) {
869
+ return;
870
+ }
871
+
872
+ // Get commit details
873
+ const commits = getRecentCommits(1);
874
+ if (commits.length === 0) return;
875
+
876
+ const commit = commits[0];
877
+
878
+ // Skip ignored patterns
879
+ if (shouldIgnoreMessage(commit.message, config.ignorePatterns)) {
880
+ watcherState.processedCommits.add(commitHash);
881
+ return;
882
+ }
883
+
884
+ // Check cooldown
885
+ if (Date.now() - watcherState.lastTriggerTime < config.cooldownPeriod) {
886
+ watcherState.processedCommits.add(commitHash);
887
+ return;
888
+ }
889
+
890
+ // Find matching workflow
891
+ for (const [pattern, workflow] of Object.entries(config.eventMapping)) {
892
+ if (pattern.startsWith('commit:')) {
893
+ const messagePattern = pattern.replace('commit:', '');
894
+ if (commit.message.toLowerCase().startsWith(messagePattern)) {
895
+ const suggestion = {
896
+ trigger: 'commit',
897
+ triggerType: 'commit_message',
898
+ pattern: messagePattern,
899
+ value: commit.message,
900
+ workflow,
901
+ confidence: 0.8,
902
+ commitHash,
903
+ autoStartEligible: config.allowedAutoStartWorkflows.includes(workflow),
904
+ timestamp: new Date().toISOString()
905
+ };
906
+
907
+ // Add to pending suggestions
908
+ state.pendingSuggestions = state.pendingSuggestions || [];
909
+ state.pendingSuggestions.push(suggestion);
910
+ saveAutopilotState(state);
911
+
912
+ autopilotEmitter.emit('suggestion:created', suggestion);
913
+
914
+ // Auto-start if configured
915
+ if (config.autoStart && suggestion.autoStartEligible) {
916
+ watcherState.lastTriggerTime = Date.now();
917
+ acceptSuggestion(workflow);
918
+ }
919
+
920
+ watcherState.processedCommits.add(commitHash);
921
+ return; // Only one workflow per commit
922
+ }
923
+ }
924
+ }
925
+
926
+ watcherState.processedCommits.add(commitHash);
927
+ }
928
+
929
+ /**
930
+ * Handle merge event
931
+ */
932
+ function handleMerge(mergeInfo) {
933
+ const state = loadAutopilotState();
934
+ const config = state.config;
935
+
936
+ // Check cooldown
937
+ if (Date.now() - watcherState.lastTriggerTime < config.cooldownPeriod) {
938
+ return;
939
+ }
940
+
941
+ // Find matching workflow
942
+ for (const [pattern, workflow] of Object.entries(config.eventMapping)) {
943
+ if (pattern.startsWith('merge:')) {
944
+ const targetBranch = pattern.replace('merge:', '');
945
+ if (mergeInfo.targetBranch === targetBranch ||
946
+ matchPattern(targetBranch, mergeInfo.targetBranch)) {
947
+ const suggestion = {
948
+ trigger: 'merge',
949
+ triggerType: 'merge_to_main',
950
+ pattern: targetBranch,
951
+ value: `Merged to ${mergeInfo.targetBranch}`,
952
+ workflow,
953
+ confidence: 0.95,
954
+ mergeBase: mergeInfo.mergeBase,
955
+ autoStartEligible: config.allowedAutoStartWorkflows.includes(workflow),
956
+ timestamp: new Date().toISOString()
957
+ };
958
+
959
+ // Add to pending suggestions
960
+ state.pendingSuggestions = state.pendingSuggestions || [];
961
+ state.pendingSuggestions.push(suggestion);
962
+ saveAutopilotState(state);
963
+
964
+ autopilotEmitter.emit('suggestion:created', suggestion);
965
+
966
+ // Always suggest launch-preparation on merge to main, but don't auto-start
967
+ // as it's a critical workflow
968
+ if (config.autoStart && suggestion.autoStartEligible) {
969
+ watcherState.lastTriggerTime = Date.now();
970
+ acceptSuggestion(workflow);
971
+ }
972
+
973
+ return; // Only one workflow per merge
974
+ }
975
+ }
976
+ }
977
+ }
978
+
979
+ /**
980
+ * Handle new tags event
981
+ */
982
+ function handleNewTags(tags) {
983
+ const state = loadAutopilotState();
984
+ const config = state.config;
985
+
986
+ // Check cooldown
987
+ if (Date.now() - watcherState.lastTriggerTime < config.cooldownPeriod) {
988
+ return;
989
+ }
990
+
991
+ for (const tag of tags) {
992
+ for (const [pattern, workflow] of Object.entries(config.eventMapping)) {
993
+ if (pattern.startsWith('tag:')) {
994
+ const tagPattern = pattern.replace('tag:', '');
995
+ if (matchPattern(tagPattern, tag)) {
996
+ const suggestion = {
997
+ trigger: 'tag',
998
+ triggerType: 'tag_create',
999
+ pattern: tagPattern,
1000
+ value: tag,
1001
+ workflow,
1002
+ confidence: 0.95,
1003
+ autoStartEligible: config.allowedAutoStartWorkflows.includes(workflow),
1004
+ timestamp: new Date().toISOString()
1005
+ };
1006
+
1007
+ // Add to pending suggestions
1008
+ state.pendingSuggestions = state.pendingSuggestions || [];
1009
+ state.pendingSuggestions.push(suggestion);
1010
+ saveAutopilotState(state);
1011
+
1012
+ autopilotEmitter.emit('suggestion:created', suggestion);
1013
+
1014
+ // Tags usually indicate releases - don't auto-start
1015
+ if (config.autoStart && suggestion.autoStartEligible) {
1016
+ watcherState.lastTriggerTime = Date.now();
1017
+ acceptSuggestion(workflow);
1018
+ }
1019
+
1020
+ return; // Only one workflow per tag
1021
+ }
1022
+ }
1023
+ }
1024
+ }
1025
+ }
1026
+
1027
+ /**
1028
+ * Install git hooks for real-time triggering
1029
+ * This provides more immediate response than polling
1030
+ */
1031
+ function installGitHooks() {
1032
+ const projectRoot = process.cwd();
1033
+ const hooksDir = path.join(projectRoot, '.git', 'hooks');
1034
+
1035
+ if (!fs.existsSync(hooksDir)) {
1036
+ return { success: false, error: 'Not a git repository or .git/hooks not found' };
1037
+ }
1038
+
1039
+ const hooks = {
1040
+ 'post-commit': `#!/bin/sh
1041
+ # Bootspring Git Autopilot - Post Commit Hook
1042
+ node -e "try { require('bootspring/hooks/git-autopilot').onGitHook('post-commit'); } catch(e) {}" 2>/dev/null || true
1043
+ `,
1044
+ 'post-checkout': `#!/bin/sh
1045
+ # Bootspring Git Autopilot - Post Checkout Hook
1046
+ # $3 is 1 if this is a branch checkout
1047
+ if [ "$3" = "1" ]; then
1048
+ node -e "try { require('bootspring/hooks/git-autopilot').onGitHook('post-checkout', { branch: true }); } catch(e) {}" 2>/dev/null || true
1049
+ fi
1050
+ `,
1051
+ 'post-merge': `#!/bin/sh
1052
+ # Bootspring Git Autopilot - Post Merge Hook
1053
+ node -e "try { require('bootspring/hooks/git-autopilot').onGitHook('post-merge'); } catch(e) {}" 2>/dev/null || true
1054
+ `
1055
+ };
1056
+
1057
+ const installed = [];
1058
+ const skipped = [];
1059
+
1060
+ for (const [hookName, hookContent] of Object.entries(hooks)) {
1061
+ const hookPath = path.join(hooksDir, hookName);
1062
+
1063
+ // Check if hook already exists
1064
+ if (fs.existsSync(hookPath)) {
1065
+ const existing = fs.readFileSync(hookPath, 'utf8');
1066
+ if (existing.includes('Bootspring Git Autopilot')) {
1067
+ skipped.push(hookName);
1068
+ continue;
1069
+ }
1070
+ // Append to existing hook
1071
+ fs.appendFileSync(hookPath, '\n' + hookContent);
1072
+ } else {
1073
+ fs.writeFileSync(hookPath, hookContent);
1074
+ }
1075
+
1076
+ // Make executable
1077
+ fs.chmodSync(hookPath, '755');
1078
+ installed.push(hookName);
1079
+ }
1080
+
1081
+ return {
1082
+ success: true,
1083
+ installed,
1084
+ skipped,
1085
+ message: `Installed ${installed.length} hooks, skipped ${skipped.length} (already present)`
1086
+ };
1087
+ }
1088
+
1089
+ /**
1090
+ * Remove git hooks
1091
+ */
1092
+ function removeGitHooks() {
1093
+ const projectRoot = process.cwd();
1094
+ const hooksDir = path.join(projectRoot, '.git', 'hooks');
1095
+
1096
+ if (!fs.existsSync(hooksDir)) {
1097
+ return { success: false, error: 'Not a git repository' };
1098
+ }
1099
+
1100
+ const hookNames = ['post-commit', 'post-checkout', 'post-merge'];
1101
+ const removed = [];
1102
+
1103
+ for (const hookName of hookNames) {
1104
+ const hookPath = path.join(hooksDir, hookName);
1105
+
1106
+ if (fs.existsSync(hookPath)) {
1107
+ const content = fs.readFileSync(hookPath, 'utf8');
1108
+
1109
+ if (content.includes('Bootspring Git Autopilot')) {
1110
+ // Remove bootspring-specific lines
1111
+ const lines = content.split('\n');
1112
+ const filtered = lines.filter(line =>
1113
+ !line.includes('Bootspring Git Autopilot') &&
1114
+ !line.includes('bootspring/hooks/git-autopilot')
1115
+ );
1116
+
1117
+ if (filtered.filter(l => l.trim() && !l.startsWith('#!')).length === 0) {
1118
+ // If only shebang remains, delete the file
1119
+ fs.unlinkSync(hookPath);
1120
+ } else {
1121
+ fs.writeFileSync(hookPath, filtered.join('\n'));
1122
+ }
1123
+ removed.push(hookName);
1124
+ }
1125
+ }
1126
+ }
1127
+
1128
+ return {
1129
+ success: true,
1130
+ removed,
1131
+ message: `Removed ${removed.length} hooks`
1132
+ };
1133
+ }
1134
+
1135
+ /**
1136
+ * Git hook handler - called from git hooks
1137
+ */
1138
+ function onGitHook(hookType, options = {}) {
1139
+ const state = loadAutopilotState();
1140
+
1141
+ if (!state.config.enabled) {
1142
+ return;
1143
+ }
1144
+
1145
+ switch (hookType) {
1146
+ case 'post-commit':
1147
+ if (state.config.hooks.onCommit) {
1148
+ const commit = getCurrentCommit();
1149
+ handleNewCommit(commit);
1150
+ }
1151
+ break;
1152
+
1153
+ case 'post-checkout':
1154
+ if (options.branch && state.config.hooks.onBranchCreate) {
1155
+ const branch = getCurrentBranch();
1156
+ const isNew = detectBranchCreation();
1157
+ if (isNew) {
1158
+ handleBranchCreate(branch);
1159
+ }
1160
+ }
1161
+ break;
1162
+
1163
+ case 'post-merge':
1164
+ if (state.config.hooks.onMerge) {
1165
+ const mergeInfo = checkForMerge('main');
1166
+ if (mergeInfo.isMerged) {
1167
+ handleMerge(mergeInfo);
1168
+ }
1169
+ }
1170
+ break;
1171
+ }
1172
+ }
1173
+
1174
+ /**
1175
+ * Update configuration
1176
+ */
1177
+ function updateConfig(updates) {
1178
+ const state = loadAutopilotState();
1179
+ state.config = mergeConfig(state.config, updates);
1180
+ saveAutopilotState(state);
1181
+ return { success: true, config: state.config };
1182
+ }
1183
+
1184
+ /**
1185
+ * Get event emitter for custom handlers
1186
+ */
1187
+ function getEmitter() {
1188
+ return autopilotEmitter;
1189
+ }
1190
+
1191
+ /**
1192
+ * Clear all pending suggestions
1193
+ */
1194
+ function clearSuggestions() {
1195
+ const state = loadAutopilotState();
1196
+ const count = state.pendingSuggestions.length;
1197
+ state.pendingSuggestions = [];
1198
+ saveAutopilotState(state);
1199
+ return { success: true, cleared: count };
1200
+ }
1201
+
1202
+ /**
1203
+ * Get history of autopilot actions
1204
+ */
1205
+ function getHistory(limit = 50) {
1206
+ const state = loadAutopilotState();
1207
+ return (state.history || []).slice(-limit);
1208
+ }
1209
+
1210
+ module.exports = {
1211
+ // Core functions
1212
+ analyzeGitState,
1213
+ enable,
1214
+ disable,
1215
+ getStatus,
1216
+
1217
+ // Suggestion management
1218
+ acceptSuggestion,
1219
+ dismissSuggestion,
1220
+ clearSuggestions,
1221
+
1222
+ // Event mapping
1223
+ addEventMapping,
1224
+ removeEventMapping,
1225
+
1226
+ // Watcher
1227
+ startWatcher,
1228
+ stopWatcher,
1229
+ pollGitState,
1230
+
1231
+ // Git hooks
1232
+ installGitHooks,
1233
+ removeGitHooks,
1234
+ onGitHook,
1235
+
1236
+ // Configuration
1237
+ loadConfig,
1238
+ updateConfig,
1239
+ loadAutopilotState,
1240
+
1241
+ // Events
1242
+ getEmitter,
1243
+
1244
+ // History
1245
+ getHistory,
1246
+
1247
+ // Constants
1248
+ EVENT_WORKFLOWS,
1249
+ DEFAULT_CONFIG
1250
+ };