@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
package/cli/loop.js CHANGED
@@ -3,8 +3,11 @@
3
3
  /**
4
4
  * Bootspring Loop CLI
5
5
  *
6
- * Autonomous task execution loop - spawns fresh AI instances
7
- * iteratively until all PRD stories are complete.
6
+ * Enhanced autonomous task execution loop - spawns fresh AI instances
7
+ * iteratively until all tasks complete with intelligent exit detection,
8
+ * session continuity, rate limiting, and circuit breakers.
9
+ *
10
+ * Inspired by: https://github.com/frankbria/ralph-claude-code
8
11
  *
9
12
  * @package bootspring
10
13
  * @module cli/loop
@@ -13,8 +16,11 @@
13
16
  const { spawn, execSync } = require('child_process');
14
17
  const path = require('path');
15
18
  const fs = require('fs');
19
+ const readline = require('readline');
16
20
  const prd = require('../intelligence/prd');
17
21
  const utils = require('../core/utils');
22
+ const config = require('../core/config');
23
+ const telemetry = require('../core/telemetry');
18
24
 
19
25
  // Colors
20
26
  const c = {
@@ -25,9 +31,256 @@ const c = {
25
31
  blue: '\x1b[34m',
26
32
  yellow: '\x1b[33m',
27
33
  cyan: '\x1b[36m',
28
- red: '\x1b[31m'
34
+ red: '\x1b[31m',
35
+ magenta: '\x1b[35m'
36
+ };
37
+
38
+ // Loop configuration
39
+ const LOOP_CONFIG = {
40
+ // Rate limiting
41
+ maxCallsPerHour: 100,
42
+ minDelayBetweenCalls: 2000, // 2 seconds minimum
43
+
44
+ // Circuit breaker
45
+ maxConsecutiveErrors: 3,
46
+ errorCooldownMs: 30000, // 30 seconds cooldown after errors
47
+
48
+ // Exit detection (dual-condition)
49
+ requiredCompletionIndicators: 2,
50
+ exitSignals: ['EXIT_SIGNAL', 'ALL_COMPLETE', 'LOOP_EXIT'],
51
+ completionIndicators: [
52
+ 'all tasks complete',
53
+ 'all stories complete',
54
+ 'nothing left to do',
55
+ 'no more work',
56
+ 'feature complete',
57
+ 'implementation complete',
58
+ 'all done',
59
+ 'finished all',
60
+ 'completed successfully'
61
+ ],
62
+
63
+ // Session
64
+ sessionDir: '.bootspring/loop',
65
+ stateFile: 'session-state.json',
66
+ logFile: 'session.log',
67
+ metricsFile: 'metrics.json'
29
68
  };
30
69
 
70
+ /**
71
+ * Loop Session State
72
+ */
73
+ class LoopSession {
74
+ constructor(projectRoot) {
75
+ this.projectRoot = projectRoot;
76
+ this.sessionDir = path.join(projectRoot, LOOP_CONFIG.sessionDir);
77
+ this.statePath = path.join(this.sessionDir, LOOP_CONFIG.stateFile);
78
+ this.logPath = path.join(this.sessionDir, LOOP_CONFIG.logFile);
79
+ this.metricsPath = path.join(this.sessionDir, LOOP_CONFIG.metricsFile);
80
+
81
+ this.state = this.loadState();
82
+ this.metrics = this.loadMetrics();
83
+ }
84
+
85
+ loadState() {
86
+ try {
87
+ if (fs.existsSync(this.statePath)) {
88
+ return JSON.parse(fs.readFileSync(this.statePath, 'utf-8'));
89
+ }
90
+ } catch {
91
+ // Corrupted state, start fresh
92
+ }
93
+
94
+ return {
95
+ sessionId: this.generateSessionId(),
96
+ startedAt: null,
97
+ lastUpdated: null,
98
+ iteration: 0,
99
+ maxIterations: 10,
100
+ tool: 'claude',
101
+ source: 'prd', // prd, todo, plan
102
+ sourcePath: null,
103
+ status: 'idle', // idle, running, paused, completed, failed
104
+ completionIndicatorCount: 0,
105
+ exitSignalReceived: false,
106
+ consecutiveErrors: 0,
107
+ callsThisHour: [],
108
+ taskHistory: [],
109
+ currentTask: null
110
+ };
111
+ }
112
+
113
+ loadMetrics() {
114
+ try {
115
+ if (fs.existsSync(this.metricsPath)) {
116
+ return JSON.parse(fs.readFileSync(this.metricsPath, 'utf-8'));
117
+ }
118
+ } catch {
119
+ // Start fresh
120
+ }
121
+
122
+ return {
123
+ totalIterations: 0,
124
+ successfulTasks: 0,
125
+ failedTasks: 0,
126
+ blockedTasks: 0,
127
+ totalDuration: 0,
128
+ averageTaskDuration: 0,
129
+ errorRate: 0,
130
+ completionRate: 0
131
+ };
132
+ }
133
+
134
+ generateSessionId() {
135
+ return `loop-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
136
+ }
137
+
138
+ save() {
139
+ if (!fs.existsSync(this.sessionDir)) {
140
+ fs.mkdirSync(this.sessionDir, { recursive: true });
141
+ }
142
+
143
+ this.state.lastUpdated = new Date().toISOString();
144
+ fs.writeFileSync(this.statePath, JSON.stringify(this.state, null, 2));
145
+ fs.writeFileSync(this.metricsPath, JSON.stringify(this.metrics, null, 2));
146
+ }
147
+
148
+ log(message, level = 'info') {
149
+ const timestamp = new Date().toISOString();
150
+ const logLine = `[${timestamp}] [${level.toUpperCase()}] ${message}\n`;
151
+
152
+ fs.appendFileSync(this.logPath, logLine);
153
+
154
+ // Also track in telemetry
155
+ telemetry.track('loop:log', { message, level, sessionId: this.state.sessionId });
156
+ }
157
+
158
+ canMakeCall() {
159
+ // Check rate limiting
160
+ const now = Date.now();
161
+ const oneHourAgo = now - (60 * 60 * 1000);
162
+
163
+ // Clean old calls
164
+ this.state.callsThisHour = this.state.callsThisHour.filter(t => t > oneHourAgo);
165
+
166
+ if (this.state.callsThisHour.length >= LOOP_CONFIG.maxCallsPerHour) {
167
+ return { allowed: false, reason: 'rate_limit', waitMs: this.state.callsThisHour[0] - oneHourAgo };
168
+ }
169
+
170
+ // Check circuit breaker
171
+ if (this.state.consecutiveErrors >= LOOP_CONFIG.maxConsecutiveErrors) {
172
+ const lastError = this.state.taskHistory
173
+ .filter(t => t.status === 'error')
174
+ .slice(-1)[0];
175
+
176
+ if (lastError) {
177
+ const cooldownEnd = new Date(lastError.endedAt).getTime() + LOOP_CONFIG.errorCooldownMs;
178
+ if (now < cooldownEnd) {
179
+ return { allowed: false, reason: 'circuit_breaker', waitMs: cooldownEnd - now };
180
+ }
181
+ }
182
+ }
183
+
184
+ return { allowed: true };
185
+ }
186
+
187
+ recordCall() {
188
+ this.state.callsThisHour.push(Date.now());
189
+ this.save();
190
+ }
191
+
192
+ recordTaskStart(task) {
193
+ this.state.currentTask = {
194
+ id: task.id || task.title,
195
+ title: task.title,
196
+ startedAt: new Date().toISOString(),
197
+ status: 'running'
198
+ };
199
+ this.save();
200
+ }
201
+
202
+ recordTaskEnd(status, details = {}) {
203
+ if (this.state.currentTask) {
204
+ const task = {
205
+ ...this.state.currentTask,
206
+ endedAt: new Date().toISOString(),
207
+ status,
208
+ ...details
209
+ };
210
+
211
+ this.state.taskHistory.push(task);
212
+
213
+ // Update metrics
214
+ this.metrics.totalIterations++;
215
+ if (status === 'complete') {
216
+ this.metrics.successfulTasks++;
217
+ this.state.consecutiveErrors = 0;
218
+ } else if (status === 'error') {
219
+ this.metrics.failedTasks++;
220
+ this.state.consecutiveErrors++;
221
+ } else if (status === 'blocked') {
222
+ this.metrics.blockedTasks++;
223
+ }
224
+
225
+ // Calculate durations
226
+ const duration = new Date(task.endedAt) - new Date(task.startedAt);
227
+ this.metrics.totalDuration += duration;
228
+ this.metrics.averageTaskDuration = this.metrics.totalDuration / this.metrics.totalIterations;
229
+ this.metrics.errorRate = this.metrics.failedTasks / this.metrics.totalIterations;
230
+ this.metrics.completionRate = this.metrics.successfulTasks / this.metrics.totalIterations;
231
+
232
+ this.state.currentTask = null;
233
+ this.save();
234
+ }
235
+ }
236
+
237
+ checkExitConditions(output) {
238
+ const lowerOutput = output.toLowerCase();
239
+
240
+ // Check for explicit exit signals
241
+ for (const signal of LOOP_CONFIG.exitSignals) {
242
+ if (output.includes(signal) || output.includes(`<loop-status>${signal}</loop-status>`)) {
243
+ this.state.exitSignalReceived = true;
244
+ this.save();
245
+ return { shouldExit: true, reason: 'exit_signal', signal };
246
+ }
247
+ }
248
+
249
+ // Check for completion indicators (dual-condition)
250
+ let indicatorCount = 0;
251
+ for (const indicator of LOOP_CONFIG.completionIndicators) {
252
+ if (lowerOutput.includes(indicator)) {
253
+ indicatorCount++;
254
+ }
255
+ }
256
+
257
+ if (indicatorCount > 0) {
258
+ this.state.completionIndicatorCount += indicatorCount;
259
+ this.save();
260
+
261
+ // Dual condition: need both multiple indicators AND explicit confirmation
262
+ if (this.state.completionIndicatorCount >= LOOP_CONFIG.requiredCompletionIndicators) {
263
+ return { shouldExit: true, reason: 'completion_indicators', count: this.state.completionIndicatorCount };
264
+ }
265
+ }
266
+
267
+ return { shouldExit: false };
268
+ }
269
+
270
+ reset() {
271
+ this.state = this.loadState();
272
+ this.state.sessionId = this.generateSessionId();
273
+ this.state.iteration = 0;
274
+ this.state.status = 'idle';
275
+ this.state.completionIndicatorCount = 0;
276
+ this.state.exitSignalReceived = false;
277
+ this.state.consecutiveErrors = 0;
278
+ this.state.taskHistory = [];
279
+ this.state.currentTask = null;
280
+ this.save();
281
+ }
282
+ }
283
+
31
284
  /**
32
285
  * Check if a tool is available
33
286
  */
@@ -50,41 +303,604 @@ function detectTool() {
50
303
  }
51
304
 
52
305
  /**
53
- * Run the loop via shell script
306
+ * Get tasks from different sources
54
307
  */
55
- function runLoop(options = {}) {
56
- // Require MCP for autonomous loop execution
57
- if (!utils.requireMCP('Autonomous loop execution')) {
58
- return;
308
+ function getTasks(source, sourcePath, projectRoot) {
309
+ switch (source) {
310
+ case 'build': {
311
+ // Load tasks from build orchestrator
312
+ const { BuildOrchestrator } = require('../core/build-orchestrator');
313
+ const orchestrator = new BuildOrchestrator(projectRoot);
314
+
315
+ if (!orchestrator.hasState()) {
316
+ return { tasks: [], source: 'build', path: null, error: 'No build state found' };
317
+ }
318
+
319
+ orchestrator.loadState();
320
+ const tasks = orchestrator.getTasksForLoop();
321
+
322
+ return {
323
+ tasks,
324
+ source: 'build',
325
+ path: path.join(projectRoot, 'planning', 'BUILD_STATE.json'),
326
+ name: orchestrator.state?.projectName || 'Build'
327
+ };
328
+ }
329
+
330
+ case 'prd': {
331
+ const prdPath = sourcePath || path.join(prd.DEFAULT_PRD_DIR, prd.DEFAULT_PRD_FILE);
332
+ const data = prd.loadPRD(prdPath);
333
+ if (!data) return { tasks: [], source: 'prd', path: prdPath };
334
+
335
+ return {
336
+ tasks: data.stories.map(s => ({
337
+ id: s.id || s.title,
338
+ title: s.title,
339
+ description: s.description,
340
+ status: s.status,
341
+ acceptance: s.acceptance
342
+ })),
343
+ source: 'prd',
344
+ path: prdPath,
345
+ name: data.name
346
+ };
347
+ }
348
+
349
+ case 'todo': {
350
+ const todoPath = sourcePath || path.join(projectRoot, 'todo.md');
351
+ if (!fs.existsSync(todoPath)) return { tasks: [], source: 'todo', path: todoPath };
352
+
353
+ const content = fs.readFileSync(todoPath, 'utf-8');
354
+ const tasks = [];
355
+ const lines = content.split('\n');
356
+
357
+ for (let i = 0; i < lines.length; i++) {
358
+ const match = lines[i].match(/^- \[([ x])\] (.+)$/i);
359
+ if (match) {
360
+ const done = match[1].toLowerCase() === 'x';
361
+ tasks.push({
362
+ id: `todo-${i}`,
363
+ title: match[2],
364
+ status: done ? 'complete' : 'pending',
365
+ line: i + 1
366
+ });
367
+ }
368
+ }
369
+
370
+ return { tasks, source: 'todo', path: todoPath };
371
+ }
372
+
373
+ case 'plan': {
374
+ const planDir = sourcePath || path.join(projectRoot, 'planning');
375
+ if (!fs.existsSync(planDir)) return { tasks: [], source: 'plan', path: planDir };
376
+
377
+ // Look for IMPLEMENTATION_PLAN.md or similar
378
+ const planFiles = ['IMPLEMENTATION_PLAN.md', 'PLAN.md', 'ROADMAP.md'];
379
+ let planPath = null;
380
+
381
+ for (const file of planFiles) {
382
+ const fp = path.join(planDir, file);
383
+ if (fs.existsSync(fp)) {
384
+ planPath = fp;
385
+ break;
386
+ }
387
+ }
388
+
389
+ if (!planPath) return { tasks: [], source: 'plan', path: planDir };
390
+
391
+ const content = fs.readFileSync(planPath, 'utf-8');
392
+ const tasks = [];
393
+
394
+ // Parse markdown checklist items
395
+ const lines = content.split('\n');
396
+ let currentSection = '';
397
+
398
+ for (let i = 0; i < lines.length; i++) {
399
+ const headerMatch = lines[i].match(/^##+ (.+)$/);
400
+ if (headerMatch) {
401
+ currentSection = headerMatch[1];
402
+ continue;
403
+ }
404
+
405
+ const taskMatch = lines[i].match(/^- \[([ x])\] (.+)$/i);
406
+ if (taskMatch) {
407
+ const done = taskMatch[1].toLowerCase() === 'x';
408
+ tasks.push({
409
+ id: `plan-${i}`,
410
+ title: taskMatch[2],
411
+ section: currentSection,
412
+ status: done ? 'complete' : 'pending',
413
+ line: i + 1
414
+ });
415
+ }
416
+ }
417
+
418
+ return { tasks, source: 'plan', path: planPath };
419
+ }
420
+
421
+ default:
422
+ return { tasks: [], source, path: sourcePath };
423
+ }
424
+ }
425
+
426
+ /**
427
+ * Get next incomplete task
428
+ */
429
+ function getNextTask(tasks) {
430
+ return tasks.find(t => t.status === 'pending' || t.status === 'in_progress');
431
+ }
432
+
433
+ /**
434
+ * Generate loop prompt
435
+ */
436
+ function generatePrompt(task, taskInfo, session, options = {}) {
437
+ const parts = [];
438
+
439
+ parts.push(`# Bootspring Autonomous Task
440
+
441
+ You are executing a single task from a ${taskInfo.source}. Follow these rules strictly:
442
+
443
+ ## Current Task
444
+ **${task.title}**
445
+ ${task.description || ''}
446
+ ${task.acceptance ? `\n### Acceptance Criteria\n${task.acceptance.map(a => `- ${a}`).join('\n')}` : ''}
447
+
448
+ ## Rules
449
+ 1. Implement ONLY this one task - do not work on other tasks
450
+ 2. Run quality checks (tests, typecheck, lint) before committing
451
+ 3. If checks pass: commit with a descriptive message
452
+ 4. Update the task status to complete when done
453
+ 5. If you discover important patterns, update CLAUDE.md
454
+
455
+ ## Quality Gates
456
+ - All tests must pass
457
+ - No TypeScript/lint errors
458
+ - No security vulnerabilities introduced
459
+
460
+ ## Exit Signals
461
+ When you successfully complete the task, output exactly:
462
+ <loop-status>TASK_COMPLETE</loop-status>
463
+
464
+ If you cannot complete the task, output exactly:
465
+ <loop-status>TASK_BLOCKED</loop-status>
466
+ Reason: [explanation]
467
+
468
+ If all tasks are done, output exactly:
469
+ <loop-status>ALL_COMPLETE</loop-status>
470
+ EXIT_SIGNAL
471
+
472
+ ## Session Info
473
+ - Session: ${session.state.sessionId}
474
+ - Iteration: ${session.state.iteration + 1} of ${session.state.maxIterations}
475
+ - Tool: ${session.state.tool}
476
+ - Source: ${taskInfo.source} (${taskInfo.path})
477
+ - Timestamp: ${new Date().toISOString()}
478
+
479
+ ## Context
480
+ The CLAUDE.md file contains project context. Read it first.
481
+ Git history contains decisions and learnings from previous iterations.
482
+ `);
483
+
484
+ // Add task history context
485
+ if (session.state.taskHistory.length > 0) {
486
+ const recentHistory = session.state.taskHistory.slice(-5);
487
+ parts.push('\n## Recent History');
488
+ for (const h of recentHistory) {
489
+ const icon = h.status === 'complete' ? '✓' : h.status === 'error' ? '✗' : '○';
490
+ parts.push(`- [${icon}] ${h.title}`);
491
+ }
59
492
  }
60
493
 
61
- const scriptPath = path.join(__dirname, '..', 'scripts', 'loop.sh');
494
+ parts.push('\nNow, begin work on the task above.');
62
495
 
63
- if (!fs.existsSync(scriptPath)) {
64
- console.error(`${c.red}Loop script not found: ${scriptPath}${c.reset}`);
496
+ return parts.join('\n');
497
+ }
498
+
499
+ /**
500
+ * Run a single iteration
501
+ */
502
+ async function runIteration(session, taskInfo, options = {}) {
503
+ const task = getNextTask(taskInfo.tasks);
504
+
505
+ if (!task) {
506
+ return { status: 'all_complete', message: 'No more tasks' };
507
+ }
508
+
509
+ // Check rate limiting and circuit breaker
510
+ const canCall = session.canMakeCall();
511
+ if (!canCall.allowed) {
512
+ if (canCall.reason === 'rate_limit') {
513
+ const waitSecs = Math.ceil(canCall.waitMs / 1000);
514
+ session.log(`Rate limited. Waiting ${waitSecs} seconds...`, 'warn');
515
+ await sleep(canCall.waitMs);
516
+ } else if (canCall.reason === 'circuit_breaker') {
517
+ const waitSecs = Math.ceil(canCall.waitMs / 1000);
518
+ session.log(`Circuit breaker active. Cooling down for ${waitSecs} seconds...`, 'warn');
519
+ console.log(`${c.yellow}Circuit breaker active. Waiting ${waitSecs}s...${c.reset}`);
520
+ await sleep(canCall.waitMs);
521
+ session.state.consecutiveErrors = 0;
522
+ session.save();
523
+ }
524
+ }
525
+
526
+ // Generate prompt
527
+ const prompt = generatePrompt(task, taskInfo, session, options);
528
+
529
+ // Create temp prompt file
530
+ const promptFile = path.join(session.sessionDir, 'current-prompt.md');
531
+ fs.writeFileSync(promptFile, prompt);
532
+
533
+ // Record task start
534
+ session.recordTaskStart(task);
535
+ session.recordCall();
536
+
537
+ console.log(`\n${c.cyan}[${session.state.iteration + 1}/${session.state.maxIterations}]${c.reset} ${c.bold}${task.title}${c.reset}`);
538
+
539
+ // Run AI tool
540
+ return new Promise((resolve) => {
541
+ let output = '';
542
+ let aiCmd, aiArgs;
543
+
544
+ if (session.state.tool === 'claude') {
545
+ aiCmd = 'claude';
546
+ aiArgs = ['--print'];
547
+ } else if (session.state.tool === 'amp') {
548
+ aiCmd = 'amp';
549
+ aiArgs = [];
550
+ } else {
551
+ resolve({ status: 'error', message: `Unknown tool: ${session.state.tool}` });
552
+ return;
553
+ }
554
+
555
+ const child = spawn(aiCmd, aiArgs, {
556
+ cwd: session.projectRoot,
557
+ stdio: options.live ? ['pipe', 'inherit', 'inherit'] : ['pipe', 'pipe', 'pipe']
558
+ });
559
+
560
+ // Send prompt via stdin
561
+ child.stdin.write(prompt);
562
+ child.stdin.end();
563
+
564
+ if (!options.live) {
565
+ child.stdout.on('data', (data) => {
566
+ output += data.toString();
567
+ if (options.verbose) {
568
+ process.stdout.write(data);
569
+ }
570
+ });
571
+
572
+ child.stderr.on('data', (data) => {
573
+ if (options.verbose) {
574
+ process.stderr.write(data);
575
+ }
576
+ });
577
+ }
578
+
579
+ child.on('close', (code) => {
580
+ // Check exit conditions
581
+ const exitCheck = session.checkExitConditions(output);
582
+
583
+ if (exitCheck.shouldExit) {
584
+ session.recordTaskEnd('complete', { exitReason: exitCheck.reason });
585
+ resolve({ status: 'all_complete', reason: exitCheck.reason, output });
586
+ return;
587
+ }
588
+
589
+ // Check for task status in output
590
+ if (output.includes('<loop-status>TASK_COMPLETE</loop-status>')) {
591
+ session.recordTaskEnd('complete');
592
+ resolve({ status: 'complete', output });
593
+ } else if (output.includes('<loop-status>TASK_BLOCKED</loop-status>')) {
594
+ const reasonMatch = output.match(/Reason:\s*(.+)/);
595
+ session.recordTaskEnd('blocked', { reason: reasonMatch?.[1] || 'Unknown' });
596
+ resolve({ status: 'blocked', reason: reasonMatch?.[1], output });
597
+ } else if (code !== 0) {
598
+ session.recordTaskEnd('error', { exitCode: code });
599
+ resolve({ status: 'error', exitCode: code, output });
600
+ } else {
601
+ // No explicit status - assume progress made
602
+ session.recordTaskEnd('complete');
603
+ resolve({ status: 'complete', output });
604
+ }
605
+ });
606
+
607
+ child.on('error', (err) => {
608
+ session.recordTaskEnd('error', { error: err.message });
609
+ resolve({ status: 'error', error: err.message });
610
+ });
611
+ });
612
+ }
613
+
614
+ /**
615
+ * Sleep helper
616
+ */
617
+ function sleep(ms) {
618
+ return new Promise(resolve => setTimeout(resolve, ms));
619
+ }
620
+
621
+ /**
622
+ * Run the main loop
623
+ */
624
+ async function runLoop(projectRoot, options = {}) {
625
+ const session = new LoopSession(projectRoot);
626
+
627
+ // Initialize or resume session
628
+ if (options.resume && session.state.status === 'paused') {
629
+ console.log(`${c.cyan}Resuming session ${session.state.sessionId}${c.reset}`);
630
+ session.log('Session resumed');
631
+ } else if (session.state.status === 'running') {
632
+ console.log(`${c.yellow}Session already running. Use --force to restart.${c.reset}`);
633
+ if (!options.force) return;
634
+ session.reset();
635
+ } else {
636
+ session.reset();
637
+ }
638
+
639
+ // Configure session
640
+ session.state.maxIterations = options.iterations || session.state.maxIterations || 10;
641
+ session.state.tool = options.tool || detectTool() || 'claude';
642
+ session.state.source = options.source || 'prd';
643
+ session.state.sourcePath = options.sourcePath || null;
644
+ session.state.startedAt = new Date().toISOString();
645
+ session.state.status = 'running';
646
+ session.save();
647
+
648
+ // Validate tool
649
+ if (!toolAvailable(session.state.tool)) {
650
+ console.error(`${c.red}AI tool '${session.state.tool}' not found. Install claude or amp CLI.${c.reset}`);
65
651
  process.exit(1);
66
652
  }
67
653
 
68
- const args = [
69
- options.iterations || '10',
70
- '--tool', options.tool || 'claude'
71
- ];
654
+ // Get tasks
655
+ const taskInfo = getTasks(session.state.source, session.state.sourcePath, projectRoot);
72
656
 
73
- if (options.prd) {
74
- args.push('--prd', options.prd);
657
+ if (taskInfo.tasks.length === 0) {
658
+ console.error(`${c.red}No tasks found from ${session.state.source}${c.reset}`);
659
+ console.log('Create tasks with: bootspring loop prd <name>');
660
+ process.exit(1);
75
661
  }
76
662
 
77
- console.log(`${c.cyan}Starting autonomous loop...${c.reset}`);
78
- console.log(`${c.dim}Tool: ${options.tool || 'claude'} | Iterations: ${options.iterations || 10}${c.reset}\n`);
663
+ const incompleteTasks = taskInfo.tasks.filter(t => t.status !== 'complete').length;
664
+ if (incompleteTasks === 0) {
665
+ console.log(`${c.green}All ${taskInfo.tasks.length} tasks already complete!${c.reset}`);
666
+ session.state.status = 'completed';
667
+ session.save();
668
+ return;
669
+ }
79
670
 
80
- const child = spawn('bash', [scriptPath, ...args], {
81
- stdio: 'inherit',
82
- cwd: process.cwd()
671
+ // Display startup
672
+ console.log(`
673
+ ${c.cyan}${c.bold}⚡ Bootspring Autonomous Loop${c.reset}
674
+ ${c.dim}Session: ${session.state.sessionId}${c.reset}
675
+
676
+ ${c.bold}Configuration${c.reset}
677
+ Tool: ${session.state.tool}
678
+ Source: ${taskInfo.source} (${taskInfo.name || taskInfo.path})
679
+ Tasks: ${incompleteTasks} pending of ${taskInfo.tasks.length} total
680
+ Max iterations: ${session.state.maxIterations}
681
+ Rate limit: ${LOOP_CONFIG.maxCallsPerHour}/hour
682
+ `);
683
+
684
+ session.log(`Loop started: ${incompleteTasks} tasks, max ${session.state.maxIterations} iterations`);
685
+ telemetry.track('loop:start', {
686
+ sessionId: session.state.sessionId,
687
+ source: session.state.source,
688
+ tasks: taskInfo.tasks.length,
689
+ incomplete: incompleteTasks
83
690
  });
84
691
 
85
- child.on('close', (code) => {
86
- process.exit(code);
692
+ // Main loop
693
+ while (session.state.iteration < session.state.maxIterations) {
694
+ // Refresh task list
695
+ const currentTasks = getTasks(session.state.source, session.state.sourcePath, projectRoot);
696
+ const remaining = currentTasks.tasks.filter(t => t.status !== 'complete').length;
697
+
698
+ if (remaining === 0) {
699
+ console.log(`\n${c.green}${c.bold}✓ All tasks complete!${c.reset}`);
700
+ session.state.status = 'completed';
701
+ session.save();
702
+ break;
703
+ }
704
+
705
+ // Run iteration
706
+ const result = await runIteration(session, currentTasks, options);
707
+
708
+ session.state.iteration++;
709
+ session.save();
710
+
711
+ // Handle result
712
+ if (result.status === 'all_complete') {
713
+ console.log(`\n${c.green}${c.bold}✓ Loop complete: ${result.reason || 'All tasks done'}${c.reset}`);
714
+ session.state.status = 'completed';
715
+ session.save();
716
+ break;
717
+ } else if (result.status === 'complete') {
718
+ console.log(`${c.green}✓${c.reset} Task completed`);
719
+ } else if (result.status === 'blocked') {
720
+ console.log(`${c.yellow}⚠${c.reset} Task blocked: ${result.reason || 'Unknown reason'}`);
721
+ } else if (result.status === 'error') {
722
+ console.log(`${c.red}✗${c.reset} Error: ${result.error || `Exit code ${result.exitCode}`}`);
723
+
724
+ if (session.state.consecutiveErrors >= LOOP_CONFIG.maxConsecutiveErrors) {
725
+ console.log(`\n${c.red}${c.bold}Circuit breaker triggered after ${LOOP_CONFIG.maxConsecutiveErrors} consecutive errors${c.reset}`);
726
+ session.state.status = 'failed';
727
+ session.save();
728
+ break;
729
+ }
730
+ }
731
+
732
+ // Delay between iterations
733
+ await sleep(LOOP_CONFIG.minDelayBetweenCalls);
734
+ }
735
+
736
+ // Final summary
737
+ console.log(`
738
+ ${c.cyan}${c.bold}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${c.reset}
739
+ ${c.bold}Loop Summary${c.reset}
740
+ Status: ${session.state.status}
741
+ Iterations: ${session.state.iteration}
742
+ Success rate: ${(session.metrics.completionRate * 100).toFixed(0)}%
743
+ Total time: ${formatDuration(session.metrics.totalDuration)}
744
+ `);
745
+
746
+ telemetry.track('loop:end', {
747
+ sessionId: session.state.sessionId,
748
+ status: session.state.status,
749
+ iterations: session.state.iteration,
750
+ successRate: session.metrics.completionRate
87
751
  });
752
+
753
+ // Show recent commits if any
754
+ try {
755
+ const commits = execSync('git log --oneline -n 5', {
756
+ cwd: projectRoot,
757
+ encoding: 'utf-8'
758
+ });
759
+ if (commits.trim()) {
760
+ console.log(`${c.bold}Recent Commits${c.reset}`);
761
+ console.log(commits.split('\n').map(l => ` ${l}`).join('\n'));
762
+ }
763
+ } catch {
764
+ // No git or no commits
765
+ }
766
+
767
+ session.log(`Loop ended: ${session.state.status}, ${session.state.iteration} iterations`);
768
+ }
769
+
770
+ /**
771
+ * Format duration
772
+ */
773
+ function formatDuration(ms) {
774
+ if (ms < 1000) return `${ms}ms`;
775
+ if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
776
+ if (ms < 3600000) return `${(ms / 60000).toFixed(1)}m`;
777
+ return `${(ms / 3600000).toFixed(1)}h`;
778
+ }
779
+
780
+ /**
781
+ * Show loop status/monitor
782
+ */
783
+ function showStatus(projectRoot, options = {}) {
784
+ const session = new LoopSession(projectRoot);
785
+
786
+ if (options.watch) {
787
+ // Live monitor mode
788
+ console.clear();
789
+ console.log(`${c.cyan}${c.bold}⚡ Bootspring Loop Monitor${c.reset}`);
790
+ console.log(`${c.dim}Press Ctrl+C to exit${c.reset}\n`);
791
+
792
+ const interval = setInterval(() => {
793
+ const s = new LoopSession(projectRoot);
794
+ console.clear();
795
+ displayStatus(s);
796
+ }, 1000);
797
+
798
+ process.on('SIGINT', () => {
799
+ clearInterval(interval);
800
+ process.exit(0);
801
+ });
802
+
803
+ displayStatus(session);
804
+ return;
805
+ }
806
+
807
+ displayStatus(session);
808
+ }
809
+
810
+ /**
811
+ * Display status helper
812
+ */
813
+ function displayStatus(session) {
814
+ const state = session.state;
815
+ const metrics = session.metrics;
816
+
817
+ const statusIcon = {
818
+ idle: `${c.dim}○${c.reset}`,
819
+ running: `${c.green}●${c.reset}`,
820
+ paused: `${c.yellow}◐${c.reset}`,
821
+ completed: `${c.green}✓${c.reset}`,
822
+ failed: `${c.red}✗${c.reset}`
823
+ }[state.status] || '?';
824
+
825
+ console.log(`
826
+ ${c.cyan}${c.bold}⚡ Loop Status${c.reset}
827
+
828
+ ${c.bold}Session${c.reset}
829
+ ID: ${c.dim}${state.sessionId}${c.reset}
830
+ Status: ${statusIcon} ${state.status}
831
+ Tool: ${state.tool}
832
+ Source: ${state.source}
833
+
834
+ ${c.bold}Progress${c.reset}
835
+ Iteration: ${state.iteration}/${state.maxIterations}
836
+ Completion indicators: ${state.completionIndicatorCount}
837
+ Exit signal: ${state.exitSignalReceived ? `${c.green}Yes${c.reset}` : `${c.dim}No${c.reset}`}
838
+ Consecutive errors: ${state.consecutiveErrors}
839
+
840
+ ${c.bold}Metrics${c.reset}
841
+ Total iterations: ${metrics.totalIterations}
842
+ Successful: ${c.green}${metrics.successfulTasks}${c.reset}
843
+ Failed: ${c.red}${metrics.failedTasks}${c.reset}
844
+ Blocked: ${c.yellow}${metrics.blockedTasks}${c.reset}
845
+ Success rate: ${(metrics.completionRate * 100).toFixed(0)}%
846
+ Avg task time: ${formatDuration(metrics.averageTaskDuration)}
847
+ `);
848
+
849
+ // Current task
850
+ if (state.currentTask) {
851
+ console.log(`${c.bold}Current Task${c.reset}`);
852
+ console.log(` ${state.currentTask.title}`);
853
+ console.log(` Started: ${state.currentTask.startedAt}`);
854
+ console.log('');
855
+ }
856
+
857
+ // Recent history
858
+ if (state.taskHistory.length > 0) {
859
+ console.log(`${c.bold}Recent Tasks${c.reset}`);
860
+ const recent = state.taskHistory.slice(-5);
861
+ for (const task of recent) {
862
+ const icon = task.status === 'complete' ? `${c.green}✓${c.reset}` :
863
+ task.status === 'error' ? `${c.red}✗${c.reset}` :
864
+ task.status === 'blocked' ? `${c.yellow}⚠${c.reset}` : '○';
865
+ console.log(` ${icon} ${task.title}`);
866
+ }
867
+ console.log('');
868
+ }
869
+
870
+ // Rate limit status
871
+ const callsThisHour = state.callsThisHour.filter(t => t > Date.now() - 3600000).length;
872
+ const rateColor = callsThisHour > 80 ? c.red : callsThisHour > 50 ? c.yellow : c.green;
873
+ console.log(`${c.bold}Rate Limit${c.reset}`);
874
+ console.log(` ${rateColor}${callsThisHour}${c.reset}/${LOOP_CONFIG.maxCallsPerHour} calls this hour`);
875
+ }
876
+
877
+ /**
878
+ * Pause the loop
879
+ */
880
+ function pauseLoop(projectRoot) {
881
+ const session = new LoopSession(projectRoot);
882
+
883
+ if (session.state.status !== 'running') {
884
+ console.log(`${c.yellow}Loop is not running${c.reset}`);
885
+ return;
886
+ }
887
+
888
+ session.state.status = 'paused';
889
+ session.save();
890
+ session.log('Loop paused by user');
891
+
892
+ console.log(`${c.green}Loop paused${c.reset}`);
893
+ console.log('Resume with: bootspring loop start --resume');
894
+ }
895
+
896
+ /**
897
+ * Reset the loop
898
+ */
899
+ function resetLoop(projectRoot) {
900
+ const session = new LoopSession(projectRoot);
901
+ session.reset();
902
+
903
+ console.log(`${c.green}Loop session reset${c.reset}`);
88
904
  }
89
905
 
90
906
  /**
@@ -140,7 +956,7 @@ function createPRD(name, options = {}) {
140
956
  /**
141
957
  * Show PRD status
142
958
  */
143
- function showStatus(prdPath) {
959
+ function showPRDStatus(prdPath) {
144
960
  const filePath = prdPath || path.join(prd.DEFAULT_PRD_DIR, prd.DEFAULT_PRD_FILE);
145
961
  const data = prd.loadPRD(filePath);
146
962
 
@@ -207,11 +1023,6 @@ function showStatus(prdPath) {
207
1023
  * Generate PRD skill for Claude Code
208
1024
  */
209
1025
  function generateSkill() {
210
- // Require MCP for skill generation
211
- if (!utils.requireMCP('Skill generation')) {
212
- return;
213
- }
214
-
215
1026
  const skillDir = path.join(__dirname, '..', '.claude-plugin', 'skills');
216
1027
 
217
1028
  if (!fs.existsSync(skillDir)) {
@@ -345,71 +1156,154 @@ bootspring loop prd <name> # Create new PRD
345
1156
  */
346
1157
  function showHelp() {
347
1158
  console.log(`
348
- ${c.bold}Bootspring Autonomous Loop${c.reset}
1159
+ ${c.cyan}${c.bold}Bootspring Autonomous Loop${c.reset}
1160
+ ${c.dim}Intelligent task execution with session continuity${c.reset}
349
1161
 
350
- ${c.cyan}Usage:${c.reset}
1162
+ ${c.bold}Usage:${c.reset}
351
1163
  bootspring loop <command> [options]
352
1164
 
353
- ${c.cyan}Commands:${c.reset}
354
- start [n] [--tool] Start loop (n iterations, default 10)
355
- status Show PRD progress
356
- prd <name> Create new PRD template
357
- prd --from <file> Create PRD from markdown
358
- skill Generate Claude Code plugin skills
359
-
360
- ${c.cyan}Options:${c.reset}
361
- --tool <claude|amp> AI tool to use (default: claude)
362
- --prd <path> Path to prd.json
363
-
364
- ${c.cyan}Examples:${c.reset}
1165
+ ${c.bold}Commands:${c.reset}
1166
+ ${c.cyan}start${c.reset} Start the autonomous loop
1167
+ ${c.cyan}status${c.reset} Show loop status and metrics
1168
+ ${c.cyan}pause${c.reset} Pause a running loop
1169
+ ${c.cyan}reset${c.reset} Reset loop session state
1170
+ ${c.cyan}prd <name>${c.reset} Create a new PRD template
1171
+ ${c.cyan}skill${c.reset} Generate Claude Code plugin skills
1172
+
1173
+ ${c.bold}Options:${c.reset}
1174
+ --iterations <n> Max iterations (default: 10)
1175
+ --tool <name> AI tool: claude or amp (default: auto-detect)
1176
+ --source <type> Task source: prd, todo, or plan (default: prd)
1177
+ --resume Resume a paused session
1178
+ --force Force restart even if session running
1179
+ --live Stream AI output in real-time
1180
+ --watch Live monitoring mode
1181
+ --verbose Show detailed output
1182
+
1183
+ ${c.bold}Task Sources:${c.reset}
1184
+ ${c.cyan}prd${c.reset} Tasks from tasks/prd.json
1185
+ ${c.cyan}todo${c.reset} Tasks from todo.md
1186
+ ${c.cyan}plan${c.reset} Tasks from planning/*.md
1187
+ ${c.cyan}build${c.reset} Tasks from autonomous build system
1188
+
1189
+ ${c.bold}Features:${c.reset}
1190
+ • Session continuity (--resume)
1191
+ • Rate limiting (${LOOP_CONFIG.maxCallsPerHour}/hour)
1192
+ • Circuit breaker (${LOOP_CONFIG.maxConsecutiveErrors} consecutive errors)
1193
+ • Dual-condition exit detection
1194
+ • Live monitoring (--watch)
1195
+
1196
+ ${c.bold}Examples:${c.reset}
365
1197
  bootspring loop prd user-auth
366
- bootspring loop start 20 --tool claude
367
- bootspring loop status
1198
+ bootspring loop start --iterations 20
1199
+ bootspring loop start --source todo
1200
+ bootspring loop start --resume
1201
+ bootspring loop status --watch
368
1202
 
369
- ${c.cyan}Workflow:${c.reset}
1203
+ ${c.bold}Workflow:${c.reset}
370
1204
  1. Create PRD: bootspring loop prd my-feature
371
1205
  2. Edit stories: Edit tasks/prd.json
372
1206
  3. Start loop: bootspring loop start
373
- 4. Monitor: bootspring loop status
1207
+ 4. Monitor: bootspring loop status --watch
374
1208
  `);
375
1209
  }
376
1210
 
1211
+ /**
1212
+ * Parse arguments
1213
+ */
1214
+ function parseArgs(args) {
1215
+ const result = {
1216
+ _: [],
1217
+ iterations: 10,
1218
+ tool: null,
1219
+ source: 'prd',
1220
+ sourcePath: null,
1221
+ resume: false,
1222
+ force: false,
1223
+ live: false,
1224
+ watch: false,
1225
+ verbose: false,
1226
+ from: null
1227
+ };
1228
+
1229
+ for (let i = 0; i < args.length; i++) {
1230
+ const arg = args[i];
1231
+
1232
+ if (arg === '--iterations' || arg === '-n') {
1233
+ result.iterations = parseInt(args[++i], 10) || 10;
1234
+ } else if (arg === '--tool') {
1235
+ result.tool = args[++i];
1236
+ } else if (arg === '--source') {
1237
+ result.source = args[++i];
1238
+ } else if (arg === '--path') {
1239
+ result.sourcePath = args[++i];
1240
+ } else if (arg === '--from') {
1241
+ result.from = args[++i];
1242
+ } else if (arg === '--resume') {
1243
+ result.resume = true;
1244
+ } else if (arg === '--force') {
1245
+ result.force = true;
1246
+ } else if (arg === '--live') {
1247
+ result.live = true;
1248
+ } else if (arg === '--watch') {
1249
+ result.watch = true;
1250
+ } else if (arg === '--verbose' || arg === '-v') {
1251
+ result.verbose = true;
1252
+ } else if (/^\d+$/.test(arg)) {
1253
+ result.iterations = parseInt(arg, 10);
1254
+ } else if (!arg.startsWith('-')) {
1255
+ result._.push(arg);
1256
+ }
1257
+ }
1258
+
1259
+ return result;
1260
+ }
1261
+
377
1262
  /**
378
1263
  * Main CLI handler
379
1264
  */
380
- function main(args) {
381
- const command = args[0] || 'help';
1265
+ async function run(args) {
1266
+ const parsed = parseArgs(args);
1267
+ const command = parsed._[0] || 'help';
1268
+
1269
+ const cfg = config.load();
1270
+ const projectRoot = cfg._projectRoot;
382
1271
 
383
1272
  switch (command) {
384
1273
  case 'start': {
385
- const tool = detectTool();
386
- if (!tool) {
387
- console.error(`${c.red}No AI tool found. Install claude or amp CLI.${c.reset}`);
388
- process.exit(1);
389
- }
390
-
391
- const iterations = args.find(a => /^\d+$/.test(a)) || '10';
392
- const toolArg = args.includes('--tool') ?
393
- args[args.indexOf('--tool') + 1] : tool;
394
- const prdArg = args.includes('--prd') ?
395
- args[args.indexOf('--prd') + 1] : null;
396
-
397
- runLoop({ iterations, tool: toolArg, prd: prdArg });
1274
+ await runLoop(projectRoot, {
1275
+ iterations: parsed.iterations,
1276
+ tool: parsed.tool,
1277
+ source: parsed.source,
1278
+ sourcePath: parsed.sourcePath,
1279
+ resume: parsed.resume,
1280
+ force: parsed.force,
1281
+ live: parsed.live,
1282
+ verbose: parsed.verbose
1283
+ });
398
1284
  break;
399
1285
  }
400
1286
 
401
1287
  case 'status':
402
- showStatus(args[1]);
1288
+ case 'monitor':
1289
+ showStatus(projectRoot, { watch: parsed.watch });
403
1290
  break;
404
1291
 
405
- case 'prd': {
406
- // Skip the 'prd' command itself, find the feature name
407
- const prdArgs = args.slice(1);
408
- const name = prdArgs.find(a => !a.startsWith('--'));
409
- const fromFile = prdArgs.includes('--from') ?
410
- prdArgs[prdArgs.indexOf('--from') + 1] : null;
1292
+ case 'pause':
1293
+ case 'stop':
1294
+ pauseLoop(projectRoot);
1295
+ break;
1296
+
1297
+ case 'reset':
1298
+ resetLoop(projectRoot);
1299
+ break;
411
1300
 
412
- createPRD(name, { from: fromFile });
1301
+ case 'prd': {
1302
+ const name = parsed._[1];
1303
+ createPRD(name, { from: parsed.from });
1304
+ if (!parsed.from) {
1305
+ showPRDStatus();
1306
+ }
413
1307
  break;
414
1308
  }
415
1309
 
@@ -426,7 +1320,16 @@ function main(args) {
426
1320
 
427
1321
  // CLI execution
428
1322
  if (require.main === module) {
429
- main(process.argv.slice(2));
1323
+ run(process.argv.slice(2));
430
1324
  }
431
1325
 
432
- module.exports = { run: main, main, runLoop, createPRD, showStatus };
1326
+ module.exports = {
1327
+ run,
1328
+ runLoop,
1329
+ LoopSession,
1330
+ createPRD,
1331
+ showStatus,
1332
+ showPRDStatus,
1333
+ getTasks,
1334
+ LOOP_CONFIG
1335
+ };