@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.
- package/README.md +107 -14
- package/bin/bootspring.js +166 -27
- package/cli/agent.js +189 -17
- package/cli/analyze.js +499 -0
- package/cli/audit.js +557 -0
- package/cli/auth.js +495 -38
- package/cli/billing.js +302 -0
- package/cli/build.js +695 -0
- package/cli/business.js +109 -26
- package/cli/checkpoint-utils.js +168 -0
- package/cli/checkpoint.js +639 -0
- package/cli/cloud-sync.js +447 -0
- package/cli/content.js +198 -0
- package/cli/context.js +1 -1
- package/cli/deploy.js +543 -0
- package/cli/fundraise.js +112 -50
- package/cli/github-cmd.js +435 -0
- package/cli/health.js +477 -0
- package/cli/init.js +84 -13
- package/cli/legal.js +107 -95
- package/cli/log.js +2 -2
- package/cli/loop.js +976 -73
- package/cli/manager.js +711 -0
- package/cli/metrics.js +480 -0
- package/cli/monitor.js +812 -0
- package/cli/onboard.js +521 -0
- package/cli/orchestrator.js +12 -24
- package/cli/prd.js +594 -0
- package/cli/preseed-start.js +1483 -0
- package/cli/preseed.js +2302 -0
- package/cli/project.js +436 -0
- package/cli/quality.js +233 -0
- package/cli/security.js +913 -0
- package/cli/seed.js +1441 -5
- package/cli/skill.js +273 -211
- package/cli/suggest.js +989 -0
- package/cli/switch.js +453 -0
- package/cli/visualize.js +527 -0
- package/cli/watch.js +769 -0
- package/cli/workspace.js +607 -0
- package/core/analyze-workflow.js +1134 -0
- package/core/api-client.js +535 -22
- package/core/audit-workflow.js +1350 -0
- package/core/build-orchestrator.js +480 -0
- package/core/build-state.js +577 -0
- package/core/checkpoint-engine.js +408 -0
- package/core/config.js +1109 -26
- package/core/context-loader.js +21 -1
- package/core/deploy-workflow.js +836 -0
- package/core/entitlements.js +93 -22
- package/core/github-sync.js +610 -0
- package/core/index.js +8 -1
- package/core/ingest.js +1111 -0
- package/core/metrics-engine.js +768 -0
- package/core/onboard-workflow.js +1007 -0
- package/core/preseed-workflow.js +934 -0
- package/core/preseed.js +1617 -0
- package/core/project-context.js +325 -0
- package/core/project-state.js +694 -0
- package/core/r2-sync.js +583 -0
- package/core/scaffold.js +525 -7
- package/core/session.js +258 -0
- package/core/task-extractor.js +758 -0
- package/core/telemetry.js +28 -6
- package/core/tier-enforcement.js +737 -0
- package/core/utils.js +38 -14
- package/generators/questionnaire.js +15 -12
- package/generators/sections/ai.js +7 -7
- package/generators/sections/content.js +300 -0
- package/generators/sections/index.js +3 -0
- package/generators/sections/plugins.js +7 -6
- package/generators/templates/build-planning.template.js +596 -0
- package/generators/templates/content.template.js +819 -0
- package/generators/templates/index.js +2 -1
- package/hooks/git-autopilot.js +1250 -0
- package/hooks/index.js +9 -0
- package/intelligence/agent-collab.js +2057 -0
- package/intelligence/auto-suggest.js +634 -0
- package/intelligence/content-gen.js +1589 -0
- package/intelligence/cross-project.js +1647 -0
- package/intelligence/index.js +184 -0
- package/intelligence/learning/insights.json +517 -7
- package/intelligence/learning/pattern-learner.js +1008 -14
- package/intelligence/memory/decision-tracker.js +1431 -31
- package/intelligence/memory/decisions.jsonl +0 -0
- package/intelligence/orchestrator.js +2896 -1
- package/intelligence/prd.js +92 -1
- package/intelligence/recommendation-weights.json +14 -2
- package/intelligence/recommendations.js +463 -9
- package/intelligence/workflow-composer.js +1451 -0
- package/marketplace/index.d.ts +324 -0
- package/marketplace/index.js +1921 -0
- package/mcp/contracts/mcp-contract.v1.json +342 -4
- package/mcp/registry.js +680 -3
- package/mcp/response-formatter.js +23 -0
- package/mcp/tools/assist-tool.js +78 -4
- package/mcp/tools/autopilot-tool.js +408 -0
- package/mcp/tools/content-tool.js +571 -0
- package/mcp/tools/dashboard-tool.js +251 -5
- package/mcp/tools/mvp-tool.js +344 -0
- package/mcp/tools/plugin-tool.js +23 -1
- package/mcp/tools/prd-tool.js +579 -0
- package/mcp/tools/seed-tool.js +447 -0
- package/mcp/tools/skill-tool.js +43 -14
- package/mcp/tools/suggest-tool.js +147 -0
- package/package.json +15 -6
- package/agents/README.md +0 -93
- package/agents/ai-integration-expert/context.md +0 -386
- package/agents/api-expert/context.md +0 -416
- package/agents/architecture-expert/context.md +0 -454
- package/agents/auth-expert/context.md +0 -399
- package/agents/backend-expert/context.md +0 -483
- package/agents/business-strategy-expert/context.md +0 -180
- package/agents/code-review-expert/context.md +0 -365
- package/agents/competitive-analysis-expert/context.md +0 -239
- package/agents/data-modeling-expert/context.md +0 -352
- package/agents/database-expert/context.md +0 -250
- package/agents/devops-expert/context.md +0 -446
- package/agents/email-expert/context.md +0 -379
- package/agents/financial-expert/context.md +0 -213
- package/agents/frontend-expert/context.md +0 -364
- package/agents/fundraising-expert/context.md +0 -257
- package/agents/growth-expert/context.md +0 -249
- package/agents/index.js +0 -140
- package/agents/investor-relations-expert/context.md +0 -266
- package/agents/legal-expert/context.md +0 -284
- package/agents/marketing-expert/context.md +0 -236
- package/agents/monitoring-expert/context.md +0 -362
- package/agents/operations-expert/context.md +0 -279
- package/agents/partnerships-expert/context.md +0 -286
- package/agents/payment-expert/context.md +0 -340
- package/agents/performance-expert/context.md +0 -377
- package/agents/private-equity-expert/context.md +0 -246
- package/agents/railway-expert/context.md +0 -284
- package/agents/research-expert/context.md +0 -245
- package/agents/sales-expert/context.md +0 -241
- package/agents/security-expert/context.md +0 -343
- package/agents/testing-expert/context.md +0 -414
- package/agents/ui-ux-expert/context.md +0 -448
- package/agents/vercel-expert/context.md +0 -426
- package/skills/index.js +0 -787
- package/skills/patterns/README.md +0 -163
- package/skills/patterns/ai/agents.md +0 -281
- package/skills/patterns/ai/claude.md +0 -138
- package/skills/patterns/ai/embeddings.md +0 -150
- package/skills/patterns/ai/rag.md +0 -266
- package/skills/patterns/ai/streaming.md +0 -170
- package/skills/patterns/ai/structured-output.md +0 -162
- package/skills/patterns/ai/tools.md +0 -154
- package/skills/patterns/analytics/tracking.md +0 -220
- package/skills/patterns/api/errors.md +0 -296
- package/skills/patterns/api/graphql.md +0 -440
- package/skills/patterns/api/middleware.md +0 -279
- package/skills/patterns/api/openapi.md +0 -285
- package/skills/patterns/api/rate-limiting.md +0 -231
- package/skills/patterns/api/route-handler.md +0 -217
- package/skills/patterns/api/server-action.md +0 -249
- package/skills/patterns/api/versioning.md +0 -443
- package/skills/patterns/api/webhooks.md +0 -247
- package/skills/patterns/auth/clerk.md +0 -132
- package/skills/patterns/auth/mfa.md +0 -313
- package/skills/patterns/auth/nextauth.md +0 -140
- package/skills/patterns/auth/oauth.md +0 -237
- package/skills/patterns/auth/rbac.md +0 -152
- package/skills/patterns/auth/session-management.md +0 -367
- package/skills/patterns/auth/session.md +0 -120
- package/skills/patterns/database/audit.md +0 -177
- package/skills/patterns/database/migrations.md +0 -177
- package/skills/patterns/database/pagination.md +0 -230
- package/skills/patterns/database/pooling.md +0 -357
- package/skills/patterns/database/prisma.md +0 -180
- package/skills/patterns/database/relations.md +0 -187
- package/skills/patterns/database/seeding.md +0 -246
- package/skills/patterns/database/soft-delete.md +0 -153
- package/skills/patterns/database/transactions.md +0 -162
- package/skills/patterns/deployment/ci-cd.md +0 -231
- package/skills/patterns/deployment/docker.md +0 -188
- package/skills/patterns/deployment/monitoring.md +0 -387
- package/skills/patterns/deployment/vercel.md +0 -160
- package/skills/patterns/email/resend.md +0 -143
- package/skills/patterns/email/templates.md +0 -245
- package/skills/patterns/email/transactional.md +0 -503
- package/skills/patterns/email/verification.md +0 -176
- package/skills/patterns/files/download.md +0 -243
- package/skills/patterns/files/upload.md +0 -239
- package/skills/patterns/i18n/nextintl.md +0 -188
- package/skills/patterns/logging/structured.md +0 -292
- package/skills/patterns/notifications/email-queue.md +0 -248
- package/skills/patterns/notifications/push.md +0 -279
- package/skills/patterns/payments/checkout.md +0 -303
- package/skills/patterns/payments/invoices.md +0 -287
- package/skills/patterns/payments/portal.md +0 -245
- package/skills/patterns/payments/stripe.md +0 -272
- package/skills/patterns/payments/subscriptions.md +0 -300
- package/skills/patterns/payments/usage.md +0 -279
- package/skills/patterns/performance/caching.md +0 -276
- package/skills/patterns/performance/code-splitting.md +0 -233
- package/skills/patterns/performance/edge.md +0 -254
- package/skills/patterns/performance/isr.md +0 -266
- package/skills/patterns/performance/lazy-loading.md +0 -281
- package/skills/patterns/realtime/sse.md +0 -327
- package/skills/patterns/realtime/websockets.md +0 -336
- package/skills/patterns/search/filtering.md +0 -329
- package/skills/patterns/search/fulltext.md +0 -260
- package/skills/patterns/security/audit-logging.md +0 -444
- package/skills/patterns/security/csrf.md +0 -234
- package/skills/patterns/security/headers.md +0 -252
- package/skills/patterns/security/sanitization.md +0 -258
- package/skills/patterns/security/secrets.md +0 -261
- package/skills/patterns/security/validation.md +0 -268
- package/skills/patterns/security/xss.md +0 -229
- package/skills/patterns/seo/metadata.md +0 -252
- package/skills/patterns/state/context.md +0 -349
- package/skills/patterns/state/react-query.md +0 -313
- package/skills/patterns/state/url-state.md +0 -482
- package/skills/patterns/state/zustand.md +0 -262
- package/skills/patterns/testing/api.md +0 -259
- package/skills/patterns/testing/component.md +0 -233
- package/skills/patterns/testing/coverage.md +0 -207
- package/skills/patterns/testing/fixtures.md +0 -225
- package/skills/patterns/testing/integration.md +0 -436
- package/skills/patterns/testing/mocking.md +0 -177
- package/skills/patterns/testing/playwright.md +0 -162
- package/skills/patterns/testing/snapshot.md +0 -175
- package/skills/patterns/testing/vitest.md +0 -307
- package/skills/patterns/ui/accordions.md +0 -395
- package/skills/patterns/ui/cards.md +0 -299
- package/skills/patterns/ui/dropdowns.md +0 -476
- package/skills/patterns/ui/empty-states.md +0 -320
- package/skills/patterns/ui/forms.md +0 -405
- package/skills/patterns/ui/inputs.md +0 -319
- package/skills/patterns/ui/layouts.md +0 -282
- package/skills/patterns/ui/loading.md +0 -291
- package/skills/patterns/ui/modals.md +0 -338
- package/skills/patterns/ui/navigation.md +0 -374
- package/skills/patterns/ui/tables.md +0 -407
- package/skills/patterns/ui/toasts.md +0 -300
- package/skills/patterns/ui/tooltips.md +0 -396
- package/skills/patterns/utils/dates.md +0 -435
- package/skills/patterns/utils/errors.md +0 -451
- package/skills/patterns/utils/formatting.md +0 -345
- package/skills/patterns/utils/validation.md +0 -434
- package/templates/bootspring.config.js +0 -83
- package/templates/business/business-model-canvas.md +0 -246
- package/templates/business/business-plan.md +0 -266
- package/templates/business/competitive-analysis.md +0 -312
- package/templates/fundraising/data-room-checklist.md +0 -300
- package/templates/fundraising/investor-research.md +0 -243
- package/templates/fundraising/pitch-deck-outline.md +0 -253
- package/templates/legal/gdpr-checklist.md +0 -339
- package/templates/legal/privacy-policy.md +0 -285
- package/templates/legal/terms-of-service.md +0 -222
- 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
|
-
*
|
|
7
|
-
* iteratively until all
|
|
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
|
-
*
|
|
306
|
+
* Get tasks from different sources
|
|
54
307
|
*/
|
|
55
|
-
function
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
494
|
+
parts.push('\nNow, begin work on the task above.');
|
|
62
495
|
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
'--tool', options.tool || 'claude'
|
|
71
|
-
];
|
|
654
|
+
// Get tasks
|
|
655
|
+
const taskInfo = getTasks(session.state.source, session.state.sourcePath, projectRoot);
|
|
72
656
|
|
|
73
|
-
if (
|
|
74
|
-
|
|
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
|
-
|
|
78
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
86
|
-
|
|
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
|
|
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.
|
|
1162
|
+
${c.bold}Usage:${c.reset}
|
|
351
1163
|
bootspring loop <command> [options]
|
|
352
1164
|
|
|
353
|
-
${c.
|
|
354
|
-
start
|
|
355
|
-
status
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
--
|
|
363
|
-
|
|
364
|
-
|
|
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
|
|
367
|
-
bootspring loop
|
|
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.
|
|
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
|
|
381
|
-
const
|
|
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
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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
|
-
|
|
1288
|
+
case 'monitor':
|
|
1289
|
+
showStatus(projectRoot, { watch: parsed.watch });
|
|
403
1290
|
break;
|
|
404
1291
|
|
|
405
|
-
case '
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
1292
|
+
case 'pause':
|
|
1293
|
+
case 'stop':
|
|
1294
|
+
pauseLoop(projectRoot);
|
|
1295
|
+
break;
|
|
1296
|
+
|
|
1297
|
+
case 'reset':
|
|
1298
|
+
resetLoop(projectRoot);
|
|
1299
|
+
break;
|
|
411
1300
|
|
|
412
|
-
|
|
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
|
-
|
|
1323
|
+
run(process.argv.slice(2));
|
|
430
1324
|
}
|
|
431
1325
|
|
|
432
|
-
module.exports = {
|
|
1326
|
+
module.exports = {
|
|
1327
|
+
run,
|
|
1328
|
+
runLoop,
|
|
1329
|
+
LoopSession,
|
|
1330
|
+
createPRD,
|
|
1331
|
+
showStatus,
|
|
1332
|
+
showPRDStatus,
|
|
1333
|
+
getTasks,
|
|
1334
|
+
LOOP_CONFIG
|
|
1335
|
+
};
|