@ob1-sg/horizon 0.1.10 → 0.1.12
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/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +43 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +10 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +293 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +635 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/__tests__/attachment-downloader.test.d.ts +2 -0
- package/dist/lib/__tests__/attachment-downloader.test.d.ts.map +1 -0
- package/dist/lib/__tests__/attachment-downloader.test.js +163 -0
- package/dist/lib/__tests__/attachment-downloader.test.js.map +1 -0
- package/dist/lib/__tests__/cli-detection.test.d.ts +2 -0
- package/dist/lib/__tests__/cli-detection.test.d.ts.map +1 -0
- package/dist/lib/__tests__/cli-detection.test.js +119 -0
- package/dist/lib/__tests__/cli-detection.test.js.map +1 -0
- package/dist/lib/__tests__/config.test.d.ts +2 -0
- package/dist/lib/__tests__/config.test.d.ts.map +1 -0
- package/dist/lib/__tests__/config.test.js +291 -0
- package/dist/lib/__tests__/config.test.js.map +1 -0
- package/dist/lib/__tests__/gcp.test.d.ts +2 -0
- package/dist/lib/__tests__/gcp.test.d.ts.map +1 -0
- package/dist/lib/__tests__/gcp.test.js +104 -0
- package/dist/lib/__tests__/gcp.test.js.map +1 -0
- package/dist/lib/__tests__/git.test.d.ts +2 -0
- package/dist/lib/__tests__/git.test.d.ts.map +1 -0
- package/dist/lib/__tests__/git.test.js +62 -0
- package/dist/lib/__tests__/git.test.js.map +1 -0
- package/dist/lib/__tests__/linear-quick-check.test.d.ts +2 -0
- package/dist/lib/__tests__/linear-quick-check.test.d.ts.map +1 -0
- package/dist/lib/__tests__/linear-quick-check.test.js +152 -0
- package/dist/lib/__tests__/linear-quick-check.test.js.map +1 -0
- package/dist/lib/__tests__/loop-instance-name.test.d.ts +2 -0
- package/dist/lib/__tests__/loop-instance-name.test.d.ts.map +1 -0
- package/dist/lib/__tests__/loop-instance-name.test.js +90 -0
- package/dist/lib/__tests__/loop-instance-name.test.js.map +1 -0
- package/dist/lib/__tests__/output-logger.test.d.ts +2 -0
- package/dist/lib/__tests__/output-logger.test.d.ts.map +1 -0
- package/dist/lib/__tests__/output-logger.test.js +136 -0
- package/dist/lib/__tests__/output-logger.test.js.map +1 -0
- package/dist/lib/__tests__/prompts.test.d.ts +2 -0
- package/dist/lib/__tests__/prompts.test.d.ts.map +1 -0
- package/dist/lib/__tests__/prompts.test.js +70 -0
- package/dist/lib/__tests__/prompts.test.js.map +1 -0
- package/dist/lib/__tests__/provider.test.d.ts +2 -0
- package/dist/lib/__tests__/provider.test.d.ts.map +1 -0
- package/dist/lib/__tests__/provider.test.js +89 -0
- package/dist/lib/__tests__/provider.test.js.map +1 -0
- package/dist/lib/__tests__/rate-limit.test.d.ts +2 -0
- package/dist/lib/__tests__/rate-limit.test.d.ts.map +1 -0
- package/dist/lib/__tests__/rate-limit.test.js +275 -0
- package/dist/lib/__tests__/rate-limit.test.js.map +1 -0
- package/dist/lib/__tests__/readline.test.d.ts +2 -0
- package/dist/lib/__tests__/readline.test.d.ts.map +1 -0
- package/dist/lib/__tests__/readline.test.js +55 -0
- package/dist/lib/__tests__/readline.test.js.map +1 -0
- package/dist/lib/__tests__/stats-logger.test.d.ts +2 -0
- package/dist/lib/__tests__/stats-logger.test.d.ts.map +1 -0
- package/dist/lib/__tests__/stats-logger.test.js +297 -0
- package/dist/lib/__tests__/stats-logger.test.js.map +1 -0
- package/dist/lib/__tests__/update-checker.test.d.ts +2 -0
- package/dist/lib/__tests__/update-checker.test.d.ts.map +1 -0
- package/dist/lib/__tests__/update-checker.test.js +141 -0
- package/dist/lib/__tests__/update-checker.test.js.map +1 -0
- package/dist/lib/__tests__/version.test.d.ts +2 -0
- package/dist/lib/__tests__/version.test.d.ts.map +1 -0
- package/dist/lib/__tests__/version.test.js +51 -0
- package/dist/lib/__tests__/version.test.js.map +1 -0
- package/dist/lib/attachment-downloader.d.ts +26 -0
- package/dist/lib/attachment-downloader.d.ts.map +1 -0
- package/dist/lib/attachment-downloader.js +259 -0
- package/dist/lib/attachment-downloader.js.map +1 -0
- package/dist/lib/claude.d.ts +6 -0
- package/dist/lib/claude.d.ts.map +1 -0
- package/dist/lib/claude.js +459 -0
- package/dist/lib/claude.js.map +1 -0
- package/dist/lib/cli-detection.d.ts +25 -0
- package/dist/lib/cli-detection.d.ts.map +1 -0
- package/dist/lib/cli-detection.js +53 -0
- package/dist/lib/cli-detection.js.map +1 -0
- package/dist/lib/codex.d.ts +4 -0
- package/dist/lib/codex.d.ts.map +1 -0
- package/dist/lib/codex.js +320 -0
- package/dist/lib/codex.js.map +1 -0
- package/dist/lib/gcp.d.ts +21 -0
- package/dist/lib/gcp.d.ts.map +1 -0
- package/dist/lib/gcp.js +96 -0
- package/dist/lib/gcp.js.map +1 -0
- package/dist/lib/git.d.ts +3 -0
- package/dist/lib/git.d.ts.map +1 -0
- package/dist/lib/git.js +24 -0
- package/dist/lib/git.js.map +1 -0
- package/dist/lib/init-project.d.ts +13 -0
- package/dist/lib/init-project.d.ts.map +1 -0
- package/dist/lib/init-project.js +420 -0
- package/dist/lib/init-project.js.map +1 -0
- package/dist/lib/linear-api.d.ts +32 -0
- package/dist/lib/linear-api.d.ts.map +1 -0
- package/dist/lib/linear-api.js +267 -0
- package/dist/lib/linear-api.js.map +1 -0
- package/dist/lib/linear-quick-check.d.ts +13 -0
- package/dist/lib/linear-quick-check.d.ts.map +1 -0
- package/dist/lib/linear-quick-check.js +61 -0
- package/dist/lib/linear-quick-check.js.map +1 -0
- package/dist/lib/loop-instance-name.d.ts +29 -0
- package/dist/lib/loop-instance-name.d.ts.map +1 -0
- package/dist/lib/loop-instance-name.js +105 -0
- package/dist/lib/loop-instance-name.js.map +1 -0
- package/dist/lib/output-logger.d.ts +23 -0
- package/dist/lib/output-logger.d.ts.map +1 -0
- package/dist/lib/output-logger.js +104 -0
- package/dist/lib/output-logger.js.map +1 -0
- package/dist/lib/prompts.d.ts +17 -0
- package/dist/lib/prompts.d.ts.map +1 -0
- package/dist/lib/prompts.js +65 -0
- package/dist/lib/prompts.js.map +1 -0
- package/dist/lib/provider.d.ts +32 -0
- package/dist/lib/provider.d.ts.map +1 -0
- package/dist/lib/provider.js +27 -0
- package/dist/lib/provider.js.map +1 -0
- package/dist/lib/rate-limit.d.ts +14 -0
- package/dist/lib/rate-limit.d.ts.map +1 -0
- package/dist/lib/rate-limit.js +154 -0
- package/dist/lib/rate-limit.js.map +1 -0
- package/dist/lib/readline.d.ts +4 -0
- package/dist/lib/readline.d.ts.map +1 -0
- package/dist/lib/readline.js +39 -0
- package/dist/lib/readline.js.map +1 -0
- package/dist/lib/setup.d.ts +126 -0
- package/dist/lib/setup.d.ts.map +1 -0
- package/dist/lib/setup.js +482 -0
- package/dist/lib/setup.js.map +1 -0
- package/dist/lib/stats-logger.d.ts +92 -0
- package/dist/lib/stats-logger.d.ts.map +1 -0
- package/dist/lib/stats-logger.js +258 -0
- package/dist/lib/stats-logger.js.map +1 -0
- package/dist/lib/ui.d.ts +38 -0
- package/dist/lib/ui.d.ts.map +1 -0
- package/dist/lib/ui.js +69 -0
- package/dist/lib/ui.js.map +1 -0
- package/dist/lib/update-checker.d.ts +17 -0
- package/dist/lib/update-checker.d.ts.map +1 -0
- package/dist/lib/update-checker.js +138 -0
- package/dist/lib/update-checker.js.map +1 -0
- package/dist/lib/version.d.ts +10 -0
- package/dist/lib/version.d.ts.map +1 -0
- package/dist/lib/version.js +37 -0
- package/dist/lib/version.js.map +1 -0
- package/dist/types.d.ts +92 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/package.json +5 -2
package/dist/index.js
ADDED
|
@@ -0,0 +1,635 @@
|
|
|
1
|
+
import { getConfig, isGitRepository } from './config.js';
|
|
2
|
+
import { sleep, executeWithRateLimitRetry } from './lib/rate-limit.js';
|
|
3
|
+
import { loadPrompt } from './lib/prompts.js';
|
|
4
|
+
import { getCurrentBranch } from './lib/git.js';
|
|
5
|
+
import { generatePodName } from './lib/loop-instance-name.js';
|
|
6
|
+
import { initLoopLogger, getCurrentOutputDir } from './lib/output-logger.js';
|
|
7
|
+
import { initLoopStats, logAgentStats, finalizeLoopStats } from './lib/stats-logger.js';
|
|
8
|
+
import { createProvider } from './lib/provider.js';
|
|
9
|
+
import { extractLinearUrls, downloadIssueAttachments } from './lib/attachment-downloader.js';
|
|
10
|
+
import { createLinearClientWithSignedUrls, getIssueDescription } from './lib/linear-api.js';
|
|
11
|
+
import { isRunningOnGcp, stopGcpInstance } from './lib/gcp.js';
|
|
12
|
+
import { checkForUncompletedTickets } from './lib/linear-quick-check.js';
|
|
13
|
+
import { getVersion } from './lib/version.js';
|
|
14
|
+
import { checkForUpdates, displayUpdateNotification } from './lib/update-checker.js';
|
|
15
|
+
import { box, BOLD, CYAN, YELLOW, RESET } from './lib/ui.js';
|
|
16
|
+
import { ensureHorizonDir, ensureHorizonDocsDir, ensureGitignore, loadExistingConfig, saveEnvConfig, saveMcpConfig, copyPromptsToProject, checkAndDisplayCliAvailability, autoSelectProvider, validateLinearKey, fetchLinearTeams, checkLinearStatuses, createLinearStatuses, checkCodexLinearMcp, createPromptInterface, promptSecret, promptSelect, } from './lib/setup.js';
|
|
17
|
+
// Import providers to register them
|
|
18
|
+
import './lib/claude.js';
|
|
19
|
+
import './lib/codex.js';
|
|
20
|
+
/**
|
|
21
|
+
* Extracts the issue identifier (e.g., RSK-39) from Agent 1's output.
|
|
22
|
+
* Agent 1 outputs this in the format "**Identifier**: RSK-39" or similar.
|
|
23
|
+
*/
|
|
24
|
+
function extractIssueIdentifier(agent1Output) {
|
|
25
|
+
// Try various patterns Agent 1 might use
|
|
26
|
+
const patterns = [
|
|
27
|
+
/\*\*Identifier\*\*:\s*([A-Z]+-\d+)/i,
|
|
28
|
+
/\*\*Issue Identifier\*\*:\s*([A-Z]+-\d+)/i,
|
|
29
|
+
/Issue ID[^:]*:\s*[^\n]*\n[^*]*\*\*Identifier\*\*:\s*([A-Z]+-\d+)/i,
|
|
30
|
+
/Identifier:\s*([A-Z]+-\d+)/i,
|
|
31
|
+
/Branch:\s*horizon\/([A-Z]+-\d+)/i,
|
|
32
|
+
/\b([A-Z]+-\d+)\b/, // Fallback: any ticket-like pattern
|
|
33
|
+
];
|
|
34
|
+
for (const pattern of patterns) {
|
|
35
|
+
const match = agent1Output.match(pattern);
|
|
36
|
+
if (match) {
|
|
37
|
+
return match[1];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
async function runLoop(podName, iteration) {
|
|
43
|
+
const config = getConfig();
|
|
44
|
+
// Initialize output logger for this loop iteration
|
|
45
|
+
// Pod name persists across all loops in this Horizon session
|
|
46
|
+
initLoopLogger(podName, iteration);
|
|
47
|
+
// Initialize stats tracking for this loop iteration
|
|
48
|
+
initLoopStats(podName, iteration);
|
|
49
|
+
console.log('\n' + box([` Log Directory: ${getCurrentOutputDir()} `], { title: `Loop ${iteration}`, color: CYAN, width: 60 }));
|
|
50
|
+
const loopStart = Date.now();
|
|
51
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
52
|
+
// AGENT 1: Linear Reader (uses configured provider)
|
|
53
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
54
|
+
console.log(`\n${BOLD}▸ Agent 1: Linear Reader${RESET}`);
|
|
55
|
+
const agent1Provider = createProvider(config.provider);
|
|
56
|
+
const agent1BasePrompt = await loadPrompt('agent1-linear-reader');
|
|
57
|
+
const agent1Prompt = `## Agent Instance
|
|
58
|
+
|
|
59
|
+
You are part of pod: **${podName}** / Loop ${iteration} / Agent 1 (Linear Reader)
|
|
60
|
+
|
|
61
|
+
This identifier format is: Pod Name / Loop Number / Agent Number (Role).
|
|
62
|
+
- **Pod Name**: ${podName} - persists for this entire Horizon session
|
|
63
|
+
- **Loop Number**: ${iteration} - increments each time Horizon processes a new ticket
|
|
64
|
+
- **Agent**: Agent 1 (Linear Reader) - your role in this loop
|
|
65
|
+
|
|
66
|
+
## Linear Team Configuration
|
|
67
|
+
|
|
68
|
+
**Team Key**: ${config.linearTeamId}
|
|
69
|
+
|
|
70
|
+
Use this team key for all Linear MCP tool calls (list_issues, list_issue_statuses, update_issue, create_comment, etc.). Do NOT use any other team key.
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
${agent1BasePrompt}`;
|
|
75
|
+
// Select model based on provider
|
|
76
|
+
const agent1Model = config.provider === 'codex' ? config.codexModel : 'opus';
|
|
77
|
+
// Rate limit retry config - uses configured max retries
|
|
78
|
+
const retryConfig = { maxRetries: config.rateLimitMaxRetries };
|
|
79
|
+
// Agent 1 with rate limit retry
|
|
80
|
+
const agent1Result = await executeWithRateLimitRetry(() => agent1Provider.spawn({
|
|
81
|
+
prompt: agent1Prompt,
|
|
82
|
+
model: agent1Model,
|
|
83
|
+
allowedTools: ['mcp__linear__*'],
|
|
84
|
+
reasoningEffort: config.provider === 'codex' ? config.codexAgentReasoning.agent1 : undefined,
|
|
85
|
+
}, 1), retryConfig, 'Agent 1 (Linear Reader)');
|
|
86
|
+
// If still rate limited after max retries, skip this iteration
|
|
87
|
+
if (agent1Result.rateLimited) {
|
|
88
|
+
console.log('Agent 1 still rate limited after max retries. Skipping iteration.');
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
// Log Agent 1 stats
|
|
92
|
+
await logAgentStats(1, config.provider, agent1Model, {
|
|
93
|
+
tokenUsage: agent1Result.tokenUsage,
|
|
94
|
+
cost: agent1Result.cost,
|
|
95
|
+
costEstimated: agent1Result.costEstimated,
|
|
96
|
+
duration: agent1Result.duration,
|
|
97
|
+
exitCode: agent1Result.exitCode,
|
|
98
|
+
rateLimited: agent1Result.rateLimited,
|
|
99
|
+
output: agent1Result.output,
|
|
100
|
+
});
|
|
101
|
+
// Extract the text output from agent 1
|
|
102
|
+
const agent1Output = agent1Result.finalOutput;
|
|
103
|
+
// Check if there's no work (look for the signal in the output)
|
|
104
|
+
if (agent1Output.includes('no_work: true') || agent1Output.includes('NO_WORK')) {
|
|
105
|
+
// Extract reason from agent output (format: "NO_WORK\n\nReason: ...")
|
|
106
|
+
const reasonMatch = agent1Output.match(/Reason:\s*([\s\S]*)/i);
|
|
107
|
+
const reason = reasonMatch?.[1]?.trim() || 'No actionable issues found.';
|
|
108
|
+
// Wrap reason text to fit box width, then display
|
|
109
|
+
const boxWidth = 64;
|
|
110
|
+
const innerWidth = boxWidth - 4; // borders + padding
|
|
111
|
+
const words = reason.split(/\s+/);
|
|
112
|
+
const wrappedLines = [];
|
|
113
|
+
let currentLine = '';
|
|
114
|
+
for (const word of words) {
|
|
115
|
+
if (currentLine && (currentLine.length + 1 + word.length) > innerWidth) {
|
|
116
|
+
wrappedLines.push(` ${currentLine}`);
|
|
117
|
+
currentLine = word;
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
currentLine = currentLine ? `${currentLine} ${word}` : word;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (currentLine)
|
|
124
|
+
wrappedLines.push(` ${currentLine}`);
|
|
125
|
+
console.log('\n' + box([` ${YELLOW}Create tickets in Linear to give Horizon work.${RESET}`, '', ...wrappedLines], { title: 'No Work', color: YELLOW, width: boxWidth }));
|
|
126
|
+
// If GCP auto-stop is enabled, check if we're on GCP and stop the instance
|
|
127
|
+
if (config.gcpAutoStop) {
|
|
128
|
+
console.log('GCP auto-stop is enabled. Checking if running on GCP...');
|
|
129
|
+
const onGcp = await isRunningOnGcp();
|
|
130
|
+
if (onGcp) {
|
|
131
|
+
console.log('Running on GCP VM. Initiating instance stop...');
|
|
132
|
+
const stopped = await stopGcpInstance();
|
|
133
|
+
if (stopped) {
|
|
134
|
+
console.log('Instance stop command issued. VM will shut down shortly.');
|
|
135
|
+
// Give the stop command time to take effect
|
|
136
|
+
await sleep(10000);
|
|
137
|
+
process.exit(0);
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
console.log('Failed to stop GCP instance. Falling back to quick check.');
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
console.log('Not running on GCP VM. Falling back to pulse check.');
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// Two-tier polling: pulse check loop with fallback
|
|
148
|
+
const fullCheckIntervalMs = config.fullCheckIntervalMinutes * 60 * 1000;
|
|
149
|
+
const pulseCheckIntervalMs = config.quickCheckIntervalMinutes * 60 * 1000;
|
|
150
|
+
let lastFullCheck = Date.now(); // Agent 1 just ran
|
|
151
|
+
while (true) {
|
|
152
|
+
console.log(`\n[Pulse Check] Sleeping ${config.quickCheckIntervalMinutes} minutes...`);
|
|
153
|
+
await sleep(pulseCheckIntervalMs);
|
|
154
|
+
// Check if fallback is due
|
|
155
|
+
const timeSinceFullCheck = Date.now() - lastFullCheck;
|
|
156
|
+
const fallbackDue = timeSinceFullCheck >= fullCheckIntervalMs;
|
|
157
|
+
if (fallbackDue) {
|
|
158
|
+
console.log('[Pulse Check] Full check interval reached. Running full Agent 1 check.');
|
|
159
|
+
return; // Exit to let main loop run Agent 1
|
|
160
|
+
}
|
|
161
|
+
// Perform pulse check
|
|
162
|
+
if (!config.linearApiKey || !config.linearTeamId) {
|
|
163
|
+
console.log('[Pulse Check] Missing Linear credentials. Falling back to Agent 1.');
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
console.log('[Pulse Check] Checking for ready-to-work tickets...');
|
|
167
|
+
const result = await checkForUncompletedTickets(config.linearApiKey, config.linearTeamId);
|
|
168
|
+
if (result.error) {
|
|
169
|
+
console.log(`[Pulse Check] Error: ${result.error}. Falling back to Agent 1.`);
|
|
170
|
+
return; // Error - let Agent 1 handle it
|
|
171
|
+
}
|
|
172
|
+
// Display status category stats
|
|
173
|
+
const { statusCounts } = result;
|
|
174
|
+
const statsLine = `[Pulse Check] Status: ${statusCounts.completed} done, ${statusCounts.started} in progress, ${statusCounts.unstarted} todo, ${statusCounts.backlog} backlog, ${statusCounts.canceled} canceled`;
|
|
175
|
+
console.log(statsLine);
|
|
176
|
+
if (result.hasWork) {
|
|
177
|
+
console.log(`[Pulse Check] Found ${result.ticketCount} ready-to-work ticket(s). Initiating next loop.`);
|
|
178
|
+
return; // Work found - run Agent 1
|
|
179
|
+
}
|
|
180
|
+
// No work found - continue pulse check loop
|
|
181
|
+
const minutesUntilFallback = Math.round((fullCheckIntervalMs - timeSinceFullCheck) / 60000);
|
|
182
|
+
console.log(`[Pulse Check] No ready-to-work tickets. Checking again in ${config.quickCheckIntervalMinutes} minutes (full check in ${minutesUntilFallback} minutes).`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
186
|
+
// ATTACHMENT DOWNLOAD: Download any attached images/files from the Linear issue
|
|
187
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
188
|
+
let attachmentPaths = [];
|
|
189
|
+
const issueIdentifier = extractIssueIdentifier(agent1Output);
|
|
190
|
+
if (issueIdentifier && config.linearApiKey) {
|
|
191
|
+
console.log('\nDownloading attachments from Linear...');
|
|
192
|
+
try {
|
|
193
|
+
// Fetch fresh description from Linear API with signed URLs
|
|
194
|
+
// The public-file-urls-expire-in header makes Linear return pre-signed URLs (1 hour expiry)
|
|
195
|
+
const linearClient = createLinearClientWithSignedUrls(config.linearApiKey, 3600);
|
|
196
|
+
const freshDescription = await getIssueDescription(linearClient, issueIdentifier);
|
|
197
|
+
if (freshDescription) {
|
|
198
|
+
// Extract URLs from the fresh description (has valid signatures)
|
|
199
|
+
const attachments = extractLinearUrls(freshDescription);
|
|
200
|
+
if (attachments.length > 0) {
|
|
201
|
+
const result = await downloadIssueAttachments(config.linearApiKey, issueIdentifier, attachments);
|
|
202
|
+
attachmentPaths = result.attachments.map(a => a.localPath);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (attachmentPaths.length > 0) {
|
|
206
|
+
console.log(`Downloaded ${attachmentPaths.length} attachment(s)`);
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
console.log('No attachments found in issue');
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
console.log('Failed to download attachments (continuing without them):', error);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
217
|
+
// AGENT 2: Worker (uses configured provider - Claude or Codex)
|
|
218
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
219
|
+
console.log(`\n${BOLD}▸ Agent 2: Worker${RESET}`);
|
|
220
|
+
// Use configured provider for Agent 2
|
|
221
|
+
const agent2Provider = createProvider(config.provider);
|
|
222
|
+
// Build attachment context section if we have downloaded files
|
|
223
|
+
const attachmentSection = attachmentPaths.length > 0
|
|
224
|
+
? `
|
|
225
|
+
|
|
226
|
+
## Downloaded Attachments
|
|
227
|
+
|
|
228
|
+
The following files have been downloaded locally from the Linear issue. You can read/view these files using the Read tool:
|
|
229
|
+
|
|
230
|
+
${attachmentPaths.map(p => `- ${p}`).join('\n')}
|
|
231
|
+
|
|
232
|
+
`
|
|
233
|
+
: '';
|
|
234
|
+
// Build worker prompt by combining the base prompt with agent 1's output
|
|
235
|
+
const workerBasePrompt = await loadPrompt('agent2-worker');
|
|
236
|
+
const workerPrompt = `## Agent Instance
|
|
237
|
+
|
|
238
|
+
You are part of pod: **${podName}** / Loop ${iteration} / Agent 2 (Worker)
|
|
239
|
+
|
|
240
|
+
This identifier format is: Pod Name / Loop Number / Agent Number (Role).
|
|
241
|
+
- **Pod Name**: ${podName} - persists for this entire Horizon session
|
|
242
|
+
- **Loop Number**: ${iteration} - increments each time Horizon processes a new ticket
|
|
243
|
+
- **Agent**: Agent 2 (Worker) - your role in this loop
|
|
244
|
+
|
|
245
|
+
## Linear Team Configuration
|
|
246
|
+
|
|
247
|
+
**Team Key**: ${config.linearTeamId}
|
|
248
|
+
|
|
249
|
+
This team key is used for all Linear MCP tool calls. While you don't make Linear calls directly, this context may be passed to other agents.
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## Context from Linear (gathered by Agent 1)
|
|
254
|
+
|
|
255
|
+
${agent1Output}
|
|
256
|
+
${attachmentSection}
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
${workerBasePrompt}`;
|
|
260
|
+
// Agent 2 with rate limit retry
|
|
261
|
+
let agent2Result;
|
|
262
|
+
if (config.provider === 'codex') {
|
|
263
|
+
agent2Result = await executeWithRateLimitRetry(() => agent2Provider.spawn({
|
|
264
|
+
prompt: workerPrompt,
|
|
265
|
+
model: config.codexModel,
|
|
266
|
+
reasoningEffort: config.codexReasoningEffort,
|
|
267
|
+
}, 2), retryConfig, 'Agent 2 (Worker)');
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
agent2Result = await executeWithRateLimitRetry(() => agent2Provider.spawn({
|
|
271
|
+
prompt: workerPrompt,
|
|
272
|
+
model: config.claudeModel,
|
|
273
|
+
}, 2), retryConfig, 'Agent 2 (Worker)');
|
|
274
|
+
}
|
|
275
|
+
// Note: Agent 2 rate limits are logged but we continue to Agent 3 regardless
|
|
276
|
+
if (agent2Result.rateLimited) {
|
|
277
|
+
console.log('Agent 2 still rate limited after max retries. Continuing to Agent 3 to log status.');
|
|
278
|
+
}
|
|
279
|
+
// Log Agent 2 stats
|
|
280
|
+
const agent2Model = config.provider === 'codex' ? config.codexModel : config.claudeModel;
|
|
281
|
+
await logAgentStats(2, config.provider, agent2Model, {
|
|
282
|
+
tokenUsage: agent2Result.tokenUsage,
|
|
283
|
+
cost: agent2Result.cost,
|
|
284
|
+
costEstimated: agent2Result.costEstimated,
|
|
285
|
+
duration: agent2Result.duration,
|
|
286
|
+
exitCode: agent2Result.exitCode,
|
|
287
|
+
rateLimited: agent2Result.rateLimited,
|
|
288
|
+
output: agent2Result.output,
|
|
289
|
+
});
|
|
290
|
+
const agent2Output = agent2Result.finalOutput;
|
|
291
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
292
|
+
// AGENT 3: Linear Writer (uses configured provider)
|
|
293
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
294
|
+
console.log(`\n${BOLD}▸ Agent 3: Linear Writer${RESET}`);
|
|
295
|
+
const agent3Provider = createProvider(config.provider);
|
|
296
|
+
const writerBasePrompt = await loadPrompt('agent3-linear-writer');
|
|
297
|
+
// Format cost strings with estimated marker for Codex
|
|
298
|
+
const agent1CostStr = agent1Result.costEstimated
|
|
299
|
+
? `~$${agent1Result.cost.toFixed(4)} (estimated)`
|
|
300
|
+
: `$${agent1Result.cost.toFixed(4)}`;
|
|
301
|
+
const agent2CostStr = agent2Result.costEstimated
|
|
302
|
+
? `~$${agent2Result.cost.toFixed(4)} (estimated)`
|
|
303
|
+
: `$${agent2Result.cost.toFixed(4)}`;
|
|
304
|
+
const writerPrompt = `## Agent Instance
|
|
305
|
+
|
|
306
|
+
You are part of pod: **${podName}** / Loop ${iteration} / Agent 3 (Linear Writer)
|
|
307
|
+
|
|
308
|
+
This identifier format is: Pod Name / Loop Number / Agent Number (Role).
|
|
309
|
+
- **Pod Name**: ${podName} - persists for this entire Horizon session
|
|
310
|
+
- **Loop Number**: ${iteration} - increments each time Horizon processes a new ticket
|
|
311
|
+
- **Agent**: Agent 3 (Linear Writer) - your role in this loop
|
|
312
|
+
|
|
313
|
+
**IMPORTANT**: Include the pod name (${podName}) in all comments you post to Linear so that when multiple pods work in parallel, we can identify which one made which comment.
|
|
314
|
+
|
|
315
|
+
## Linear Team Configuration
|
|
316
|
+
|
|
317
|
+
**Team Key**: ${config.linearTeamId}
|
|
318
|
+
|
|
319
|
+
Use this team key for all Linear MCP tool calls (update_issue, create_comment, create_issue for sub-issues, etc.). Do NOT use any other team key.
|
|
320
|
+
|
|
321
|
+
---
|
|
322
|
+
|
|
323
|
+
## Context from Agent 1 (Linear issue details)
|
|
324
|
+
|
|
325
|
+
${agent1Output}
|
|
326
|
+
|
|
327
|
+
---
|
|
328
|
+
|
|
329
|
+
## Results from Agent 2 (Work performed)
|
|
330
|
+
|
|
331
|
+
${agent2Output}
|
|
332
|
+
|
|
333
|
+
---
|
|
334
|
+
|
|
335
|
+
## Session Stats
|
|
336
|
+
|
|
337
|
+
- Pod: ${podName}
|
|
338
|
+
- Loop: ${iteration}
|
|
339
|
+
|
|
340
|
+
### Agent 1 (Linear Reader)
|
|
341
|
+
- Provider: ${config.provider}
|
|
342
|
+
- Model: ${agent1Model}
|
|
343
|
+
- Cost: ${agent1CostStr}
|
|
344
|
+
- Duration: ${Math.round(agent1Result.duration / 1000)}s
|
|
345
|
+
- Tokens: in=${agent1Result.tokenUsage.input.toLocaleString()} out=${agent1Result.tokenUsage.output.toLocaleString()} cached=${agent1Result.tokenUsage.cached.toLocaleString()}
|
|
346
|
+
|
|
347
|
+
### Agent 2 (Worker)
|
|
348
|
+
- Provider: ${config.provider}
|
|
349
|
+
- Model: ${agent2Model}
|
|
350
|
+
- Cost: ${agent2CostStr}
|
|
351
|
+
- Duration: ${Math.round(agent2Result.duration / 1000)}s
|
|
352
|
+
- Tokens: in=${agent2Result.tokenUsage.input.toLocaleString()} out=${agent2Result.tokenUsage.output.toLocaleString()} cached=${agent2Result.tokenUsage.cached.toLocaleString()}
|
|
353
|
+
- Exit code: ${agent2Result.exitCode}
|
|
354
|
+
- Rate limited: ${agent2Result.rateLimited}
|
|
355
|
+
|
|
356
|
+
### Loop Totals (Agent 1 + Agent 2)
|
|
357
|
+
- Total Cost: $${(agent1Result.cost + agent2Result.cost).toFixed(4)}${(agent1Result.costEstimated || agent2Result.costEstimated) ? ' (includes estimate)' : ''}
|
|
358
|
+
- Total Duration: ${Math.round((agent1Result.duration + agent2Result.duration) / 1000)}s
|
|
359
|
+
- Total Tokens: in=${(agent1Result.tokenUsage.input + agent2Result.tokenUsage.input).toLocaleString()} out=${(agent1Result.tokenUsage.output + agent2Result.tokenUsage.output).toLocaleString()} cached=${(agent1Result.tokenUsage.cached + agent2Result.tokenUsage.cached).toLocaleString()}
|
|
360
|
+
|
|
361
|
+
---
|
|
362
|
+
|
|
363
|
+
${writerBasePrompt}`;
|
|
364
|
+
// Agent 3 with rate limit retry
|
|
365
|
+
const agent3Model = config.provider === 'codex' ? config.codexModel : 'sonnet';
|
|
366
|
+
const agent3Result = await executeWithRateLimitRetry(() => agent3Provider.spawn({
|
|
367
|
+
prompt: writerPrompt,
|
|
368
|
+
model: agent3Model,
|
|
369
|
+
allowedTools: ['mcp__linear__*'],
|
|
370
|
+
reasoningEffort: config.provider === 'codex' ? config.codexAgentReasoning.agent3 : undefined,
|
|
371
|
+
}, 3), retryConfig, 'Agent 3 (Linear Writer)');
|
|
372
|
+
if (agent3Result.rateLimited) {
|
|
373
|
+
console.log('Agent 3 still rate limited after max retries.');
|
|
374
|
+
}
|
|
375
|
+
// Log Agent 3 stats
|
|
376
|
+
await logAgentStats(3, config.provider, agent3Model, {
|
|
377
|
+
tokenUsage: agent3Result.tokenUsage,
|
|
378
|
+
cost: agent3Result.cost,
|
|
379
|
+
costEstimated: agent3Result.costEstimated,
|
|
380
|
+
duration: agent3Result.duration,
|
|
381
|
+
exitCode: agent3Result.exitCode,
|
|
382
|
+
rateLimited: agent3Result.rateLimited,
|
|
383
|
+
output: agent3Result.output,
|
|
384
|
+
});
|
|
385
|
+
// Finalize loop stats
|
|
386
|
+
await finalizeLoopStats();
|
|
387
|
+
// Loop stats
|
|
388
|
+
const duration = Math.round((Date.now() - loopStart) / 1000);
|
|
389
|
+
const minutes = Math.floor(duration / 60);
|
|
390
|
+
const seconds = duration % 60;
|
|
391
|
+
console.log(`\nLoop ${iteration} complete in ${minutes}m ${seconds}s`);
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Displays the Horizon startup banner with ASCII art.
|
|
395
|
+
* Futuristic/sci-fi themed, ~10 lines.
|
|
396
|
+
*/
|
|
397
|
+
function displayBanner() {
|
|
398
|
+
const version = getVersion();
|
|
399
|
+
const banner = `
|
|
400
|
+
██╗ ██╗ ██████╗ ██████╗ ██╗███████╗ ██████╗ ███╗ ██╗
|
|
401
|
+
██║ ██║██╔═══██╗██╔══██╗██║╚══███╔╝██╔═══██╗████╗ ██║
|
|
402
|
+
███████║██║ ██║██████╔╝██║ ███╔╝ ██║ ██║██╔██╗ ██║
|
|
403
|
+
██╔══██║██║ ██║██╔══██╗██║ ███╔╝ ██║ ██║██║╚██╗██║
|
|
404
|
+
██║ ██║╚██████╔╝██║ ██║██║███████╗╚██████╔╝██║ ╚████║
|
|
405
|
+
╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝╚══════╝ ╚═════╝ ╚═╝ ╚═══╝
|
|
406
|
+
|
|
407
|
+
⚡ Complete Linear Tickets with Long-Running Agents ⚡ v${version}
|
|
408
|
+
════════════════════════════════════════════════════════════════`;
|
|
409
|
+
console.log(banner);
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Displays a safety warning about AI coding agents.
|
|
413
|
+
*/
|
|
414
|
+
function displaySafetyWarning() {
|
|
415
|
+
console.log('\n' + box([
|
|
416
|
+
' Horizon uses AI coding agents (Claude Code, Codex) that are ',
|
|
417
|
+
' granted permissions to read, write, and execute code. ',
|
|
418
|
+
' ',
|
|
419
|
+
' * Coding agents may make mistakes ',
|
|
420
|
+
' * They may take actions that could be harmful to your system ',
|
|
421
|
+
' * Always review changes before merging to production ',
|
|
422
|
+
' ',
|
|
423
|
+
' We recommend running Horizon in a sandboxed environment or ',
|
|
424
|
+
' virtual machine for added safety. ',
|
|
425
|
+
], { title: 'Safety Warning', width: 65 }) + '\n');
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Runs minimal first-run setup if credentials are missing.
|
|
429
|
+
* Prompts only for essentials (API key, team), uses defaults for everything else.
|
|
430
|
+
* Returns true if setup completed successfully, false if user cancelled.
|
|
431
|
+
*/
|
|
432
|
+
async function runMinimalSetup(cliAvailability) {
|
|
433
|
+
console.log('\n⚠️ Linear credentials not configured.\n');
|
|
434
|
+
const rl = createPromptInterface();
|
|
435
|
+
try {
|
|
436
|
+
// Load any existing config
|
|
437
|
+
const existingConfig = loadExistingConfig();
|
|
438
|
+
// ─── API Key ───
|
|
439
|
+
console.log('To get your API key:');
|
|
440
|
+
console.log(' 1. Go to: https://linear.app/settings/account/security/api-keys/new');
|
|
441
|
+
console.log(' 2. Enter a label (e.g., "Horizon")');
|
|
442
|
+
console.log(' 3. Click "Create key"');
|
|
443
|
+
console.log('');
|
|
444
|
+
let apiKey = await promptSecret(rl, 'Enter your Linear API key');
|
|
445
|
+
if (!apiKey) {
|
|
446
|
+
console.log('\nLinear API key is required.');
|
|
447
|
+
return false;
|
|
448
|
+
}
|
|
449
|
+
// Validate API key
|
|
450
|
+
console.log('Validating...');
|
|
451
|
+
const isValid = await validateLinearKey(apiKey);
|
|
452
|
+
if (!isValid) {
|
|
453
|
+
console.log('Invalid API key. Please check and try again.');
|
|
454
|
+
return false;
|
|
455
|
+
}
|
|
456
|
+
console.log('✓ Valid\n');
|
|
457
|
+
// ─── Team Selection ───
|
|
458
|
+
console.log('Fetching teams...');
|
|
459
|
+
const teams = await fetchLinearTeams(apiKey);
|
|
460
|
+
if (teams.length === 0) {
|
|
461
|
+
console.log('No teams found in your Linear workspace.');
|
|
462
|
+
return false;
|
|
463
|
+
}
|
|
464
|
+
let teamKey;
|
|
465
|
+
if (teams.length === 1) {
|
|
466
|
+
teamKey = teams[0].key;
|
|
467
|
+
console.log(`Found team: ${teams[0].name} (${teams[0].key})`);
|
|
468
|
+
console.log('Auto-selecting as it\'s the only team.\n');
|
|
469
|
+
}
|
|
470
|
+
else {
|
|
471
|
+
console.log('Select team:');
|
|
472
|
+
const teamOptions = teams.map((team) => ({
|
|
473
|
+
value: team.key,
|
|
474
|
+
label: `${team.name} (${team.key})`,
|
|
475
|
+
}));
|
|
476
|
+
teamKey = await promptSelect(rl, teamOptions, 0);
|
|
477
|
+
const selectedTeam = teams.find((t) => t.key === teamKey);
|
|
478
|
+
console.log(`Selected: ${selectedTeam.name}\n`);
|
|
479
|
+
}
|
|
480
|
+
// Auto-select provider based on CLI availability
|
|
481
|
+
const provider = autoSelectProvider(cliAvailability);
|
|
482
|
+
console.log(`Using provider: ${provider}\n`);
|
|
483
|
+
// ─── Merge Mode ───
|
|
484
|
+
console.log('When work completes:');
|
|
485
|
+
const mergeModeOptions = [
|
|
486
|
+
{ value: 'auto', label: 'auto', description: 'Agent decides: merge directly or create PR (recommended)' },
|
|
487
|
+
{ value: 'merge', label: 'merge', description: 'Merge directly to main' },
|
|
488
|
+
{ value: 'pr', label: 'pr', description: 'Create PR for review' },
|
|
489
|
+
];
|
|
490
|
+
const mergeMode = await promptSelect(rl, mergeModeOptions, 0);
|
|
491
|
+
// Save configuration
|
|
492
|
+
const newConfig = {
|
|
493
|
+
linearApiKey: apiKey,
|
|
494
|
+
linearTeamKey: teamKey,
|
|
495
|
+
provider,
|
|
496
|
+
claudeModel: existingConfig.claudeModel || 'opus',
|
|
497
|
+
codexModel: existingConfig.codexModel || 'gpt-5.2',
|
|
498
|
+
codexReasoningEffort: existingConfig.codexReasoningEffort || 'high',
|
|
499
|
+
maxIterations: existingConfig.maxIterations ?? 0,
|
|
500
|
+
mergeMode,
|
|
501
|
+
};
|
|
502
|
+
saveEnvConfig(newConfig);
|
|
503
|
+
saveMcpConfig(apiKey);
|
|
504
|
+
ensureGitignore();
|
|
505
|
+
// Update process.env so config picks up new values
|
|
506
|
+
process.env.LINEAR_API_KEY = apiKey;
|
|
507
|
+
process.env.LINEAR_TEAM_KEY = teamKey;
|
|
508
|
+
process.env.HORIZON_PROVIDER = provider;
|
|
509
|
+
process.env.HORIZON_MERGE_MODE = mergeMode;
|
|
510
|
+
console.log('\n✓ Configuration saved!\n');
|
|
511
|
+
return true;
|
|
512
|
+
}
|
|
513
|
+
finally {
|
|
514
|
+
rl.close();
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
export async function main() {
|
|
518
|
+
displayBanner();
|
|
519
|
+
displaySafetyWarning();
|
|
520
|
+
// Check for updates (non-blocking, cached for 24 hours)
|
|
521
|
+
const updateResult = await checkForUpdates();
|
|
522
|
+
displayUpdateNotification(updateResult);
|
|
523
|
+
// Check for git repository first - Horizon requires git
|
|
524
|
+
if (!isGitRepository()) {
|
|
525
|
+
console.log('\n❌ Error: Not a git repository');
|
|
526
|
+
console.log('');
|
|
527
|
+
console.log(' Horizon requires a git repository to operate. It uses git for:');
|
|
528
|
+
console.log(' - Creating feature branches');
|
|
529
|
+
console.log(' - Committing changes');
|
|
530
|
+
console.log(' - Creating pull requests');
|
|
531
|
+
console.log('');
|
|
532
|
+
console.log(' To initialize a git repository, run:');
|
|
533
|
+
console.log(' git init');
|
|
534
|
+
console.log('');
|
|
535
|
+
process.exit(1);
|
|
536
|
+
}
|
|
537
|
+
// Ensure directories exist (silent)
|
|
538
|
+
ensureHorizonDir();
|
|
539
|
+
ensureHorizonDocsDir();
|
|
540
|
+
ensureGitignore();
|
|
541
|
+
// Check for coding CLI availability - at least one must be installed
|
|
542
|
+
const cliAvailability = checkAndDisplayCliAvailability();
|
|
543
|
+
if (!cliAvailability) {
|
|
544
|
+
// Error message already displayed by checkAndDisplayCliAvailability
|
|
545
|
+
process.exit(1);
|
|
546
|
+
}
|
|
547
|
+
// Sync prompts from package to .horizon/prompts/ (ensures they're always up-to-date)
|
|
548
|
+
const promptSync = copyPromptsToProject();
|
|
549
|
+
let config = getConfig();
|
|
550
|
+
// Check if Linear credentials are configured
|
|
551
|
+
if (!config.linearApiKey || !config.linearTeamId) {
|
|
552
|
+
const setupSuccess = await runMinimalSetup(cliAvailability);
|
|
553
|
+
if (!setupSuccess) {
|
|
554
|
+
console.log('\nCannot start Horizon without Linear configuration.');
|
|
555
|
+
console.log('Run `horizon config` for full configuration options.\n');
|
|
556
|
+
process.exit(1);
|
|
557
|
+
}
|
|
558
|
+
// Reload config after setup (rebuild from updated process.env)
|
|
559
|
+
config = getConfig(true);
|
|
560
|
+
}
|
|
561
|
+
// Check if ∞ statuses exist in Linear, create if needed
|
|
562
|
+
const statusesExist = await checkLinearStatuses(config.linearApiKey, config.linearTeamId);
|
|
563
|
+
if (!statusesExist) {
|
|
564
|
+
const result = await createLinearStatuses(config.linearApiKey, config.linearTeamId);
|
|
565
|
+
if (!result.success) {
|
|
566
|
+
console.log('\n❌ Failed to create ∞ statuses. Please check Linear permissions.');
|
|
567
|
+
if (result.errors.length > 0) {
|
|
568
|
+
result.errors.forEach((err) => console.log(` - ${err}`));
|
|
569
|
+
}
|
|
570
|
+
process.exit(1);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
// Check Codex Linear MCP if using Codex provider
|
|
574
|
+
if (config.provider === 'codex') {
|
|
575
|
+
const hasLinearMcp = checkCodexLinearMcp();
|
|
576
|
+
if (!hasLinearMcp) {
|
|
577
|
+
console.log('\n⚠️ Linear MCP not configured for Codex.');
|
|
578
|
+
console.log(' Run: horizon config');
|
|
579
|
+
process.exit(1);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
// Generate pod name once at startup - persists for entire Horizon session
|
|
583
|
+
const podName = generatePodName();
|
|
584
|
+
// Build provider display string
|
|
585
|
+
const providerDisplay = config.provider === 'codex'
|
|
586
|
+
? `Codex (${config.codexModel})`
|
|
587
|
+
: `Claude (${config.claudeModel.charAt(0).toUpperCase() + config.claudeModel.slice(1)})`;
|
|
588
|
+
// Build CLI availability display
|
|
589
|
+
const cliParts = [];
|
|
590
|
+
if (cliAvailability.claude)
|
|
591
|
+
cliParts.push('Claude ✓');
|
|
592
|
+
if (cliAvailability.codex)
|
|
593
|
+
cliParts.push('Codex ✓');
|
|
594
|
+
// Build init box lines
|
|
595
|
+
const detectionLines = [
|
|
596
|
+
` CLIs Available ${cliParts.join(' ')} `,
|
|
597
|
+
` Agent Prompts ${promptSync.copied} files synced `,
|
|
598
|
+
];
|
|
599
|
+
const configLines = [
|
|
600
|
+
` Working Directory ${config.workingDirectory} `,
|
|
601
|
+
` Agent Branch ${getCurrentBranch()} `,
|
|
602
|
+
` Provider ${providerDisplay} `,
|
|
603
|
+
` Merge Mode ${promptSync.mergeMode} `,
|
|
604
|
+
` Linear Team ${config.linearTeamId} ∞ prefixed statuses `,
|
|
605
|
+
` Current Pod ${podName} `,
|
|
606
|
+
];
|
|
607
|
+
if (config.maxIterations > 0) {
|
|
608
|
+
configLines.push(` Max Iterations ${config.maxIterations} `);
|
|
609
|
+
}
|
|
610
|
+
if (config.gcpAutoStop) {
|
|
611
|
+
configLines.push(` GCP Auto-Stop enabled `);
|
|
612
|
+
}
|
|
613
|
+
console.log('\n' + box([...detectionLines, ...configLines], { title: 'Horizon', width: 60, dividerAfter: [detectionLines.length - 1] }));
|
|
614
|
+
let iteration = 0;
|
|
615
|
+
while (config.maxIterations === 0 || iteration < config.maxIterations) {
|
|
616
|
+
try {
|
|
617
|
+
await runLoop(podName, iteration);
|
|
618
|
+
iteration++;
|
|
619
|
+
}
|
|
620
|
+
catch (error) {
|
|
621
|
+
console.error(`\nLoop ${iteration} error:`, error);
|
|
622
|
+
console.log(`Sleeping ${config.errorSleepMinutes} minute(s) before retry...`);
|
|
623
|
+
await sleep(config.errorSleepMinutes * 60 * 1000);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
if (config.maxIterations > 0) {
|
|
627
|
+
console.log(`\nReached max iterations: ${config.maxIterations}`);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
// Run main when executed directly
|
|
631
|
+
// When used as CLI via cli.ts, main() is imported and called there
|
|
632
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
633
|
+
main().catch(console.error);
|
|
634
|
+
}
|
|
635
|
+
//# sourceMappingURL=index.js.map
|