golem-cc 2.1.2 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/golem/build.md +18 -0
- package/.claude/commands/golem/config.md +39 -0
- package/.claude/commands/golem/continue.md +73 -0
- package/.claude/commands/golem/doctor.md +46 -0
- package/.claude/commands/golem/document.md +138 -0
- package/.claude/commands/golem/help.md +58 -0
- package/.claude/commands/golem/pause.md +130 -0
- package/.claude/commands/golem/plan.md +111 -0
- package/.claude/commands/golem/review.md +166 -0
- package/.claude/commands/golem/security.md +186 -0
- package/.claude/commands/golem/simplify.md +76 -0
- package/.claude/commands/golem/spec.md +105 -0
- package/.claude/commands/golem/status.md +33 -0
- package/.golem/agents/code-simplifier.md +54 -0
- package/.golem/agents/review-architecture.md +59 -0
- package/.golem/agents/review-logic.md +50 -0
- package/.golem/agents/review-security.md +50 -0
- package/.golem/agents/review-style.md +48 -0
- package/.golem/agents/review-tests.md +48 -0
- package/.golem/agents/spec-builder.md +60 -0
- package/.golem/bin/golem.mjs +270 -0
- package/.golem/lib/build.mjs +557 -0
- package/.golem/lib/claude.mjs +95 -0
- package/.golem/lib/config.mjs +421 -0
- package/.golem/lib/display.mjs +191 -0
- package/.golem/lib/doctor.mjs +197 -0
- package/.golem/lib/document.mjs +792 -0
- package/.golem/lib/gates.mjs +78 -0
- package/.golem/lib/init.mjs +166 -0
- package/.golem/lib/output.mjs +40 -0
- package/.golem/lib/ratelimit.mjs +86 -0
- package/.golem/lib/security.mjs +603 -0
- package/.golem/lib/simplify.mjs +101 -0
- package/.golem/lib/tui.mjs +368 -0
- package/.golem/lib/usage.mjs +119 -0
- package/.golem/lib/worktree.mjs +509 -0
- package/.golem/prompts/build.md +23 -0
- package/.golem/prompts/document-inline.md +66 -0
- package/.golem/prompts/document-markdown.md +80 -0
- package/.golem/prompts/simplify.md +35 -0
- package/README.md +141 -142
- package/bin/golem-shim.mjs +36 -0
- package/bin/install.mjs +193 -0
- package/package.json +27 -32
- package/.env.example +0 -17
- package/bin/golem +0 -1040
- package/commands/golem/build.md +0 -235
- package/commands/golem/config.md +0 -55
- package/commands/golem/doctor.md +0 -137
- package/commands/golem/help.md +0 -212
- package/commands/golem/plan.md +0 -214
- package/commands/golem/review.md +0 -376
- package/commands/golem/security.md +0 -204
- package/commands/golem/simplify.md +0 -94
- package/commands/golem/spec.md +0 -226
- package/commands/golem/status.md +0 -60
- package/dist/api/freshworks.d.ts +0 -61
- package/dist/api/freshworks.d.ts.map +0 -1
- package/dist/api/freshworks.js +0 -119
- package/dist/api/freshworks.js.map +0 -1
- package/dist/api/gitea.d.ts +0 -96
- package/dist/api/gitea.d.ts.map +0 -1
- package/dist/api/gitea.js +0 -154
- package/dist/api/gitea.js.map +0 -1
- package/dist/cli/index.d.ts +0 -9
- package/dist/cli/index.d.ts.map +0 -1
- package/dist/cli/index.js +0 -352
- package/dist/cli/index.js.map +0 -1
- package/dist/sync/ticket-sync.d.ts +0 -53
- package/dist/sync/ticket-sync.d.ts.map +0 -1
- package/dist/sync/ticket-sync.js +0 -226
- package/dist/sync/ticket-sync.js.map +0 -1
- package/dist/types.d.ts +0 -125
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -5
- package/dist/types.js.map +0 -1
- package/dist/worktree/manager.d.ts +0 -54
- package/dist/worktree/manager.d.ts.map +0 -1
- package/dist/worktree/manager.js +0 -190
- package/dist/worktree/manager.js.map +0 -1
- package/golem/agents/code-simplifier.md +0 -81
- package/golem/agents/spec-builder.md +0 -90
- package/golem/prompts/PROMPT_build.md +0 -71
- package/golem/prompts/PROMPT_plan.md +0 -102
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
import { readFile, writeFile, readdir } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { execSync } from 'node:child_process';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { header, success, fail, warn, info, spinner, table } from './output.mjs';
|
|
6
|
+
import { loadConfig } from './config.mjs';
|
|
7
|
+
import { runClaude, checkClaudeCli } from './claude.mjs';
|
|
8
|
+
import { attachDisplay } from './display.mjs';
|
|
9
|
+
import { createTui } from './tui.mjs';
|
|
10
|
+
import { getUsageData } from './usage.mjs';
|
|
11
|
+
import { runGates } from './gates.mjs';
|
|
12
|
+
import { runSecurityScan } from './security.mjs';
|
|
13
|
+
import { fetchUsage, formatResetTime } from './ratelimit.mjs';
|
|
14
|
+
import { runDocument } from './document.mjs';
|
|
15
|
+
|
|
16
|
+
const PLAN_PATH = join(process.cwd(), '.golem/IMPLEMENTATION_PLAN.md');
|
|
17
|
+
|
|
18
|
+
export function parseNextTask(planContent) {
|
|
19
|
+
const lines = planContent.split('\n');
|
|
20
|
+
let currentStage = '';
|
|
21
|
+
let stageCommit = '';
|
|
22
|
+
|
|
23
|
+
for (let i = 0; i < lines.length; i++) {
|
|
24
|
+
const line = lines[i];
|
|
25
|
+
|
|
26
|
+
if (/^## Stage \d+/.test(line)) {
|
|
27
|
+
currentStage = line.replace(/^## /, '');
|
|
28
|
+
}
|
|
29
|
+
if (/^Commit:/.test(line)) {
|
|
30
|
+
stageCommit = line.replace(/^Commit:\s*/, '');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const match = line.match(/^### \[ \] (\d+\.\d+)\.\s+(.*)/);
|
|
34
|
+
if (match) {
|
|
35
|
+
const taskId = match[1];
|
|
36
|
+
const taskTitle = match[2];
|
|
37
|
+
|
|
38
|
+
let details = '';
|
|
39
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
40
|
+
if (/^###\s|^##\s|^---/.test(lines[j])) break;
|
|
41
|
+
details += lines[j] + '\n';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const modelMatch = details.match(/^Model:\s*(\S+)/m);
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
id: taskId,
|
|
48
|
+
title: taskTitle,
|
|
49
|
+
stage: currentStage,
|
|
50
|
+
commit: stageCommit,
|
|
51
|
+
details: details.trim(),
|
|
52
|
+
model: modelMatch ? modelMatch[1] : null,
|
|
53
|
+
lineIndex: i,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function countTasks(planContent) {
|
|
61
|
+
const total = (planContent.match(/^### \[[ x]\]/gm) || []).length;
|
|
62
|
+
const done = (planContent.match(/^### \[x\]/gm) || []).length;
|
|
63
|
+
return { total, done };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function markTaskComplete(planContent, lineIndex) {
|
|
67
|
+
const lines = planContent.split('\n');
|
|
68
|
+
lines[lineIndex] = lines[lineIndex].replace('### [ ]', '### [x]');
|
|
69
|
+
return lines.join('\n');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function buildPrompt(task, config, retryContext) {
|
|
73
|
+
let prompt;
|
|
74
|
+
try {
|
|
75
|
+
prompt = await readFile(join(process.cwd(), '.golem/prompts/build.md'), 'utf-8');
|
|
76
|
+
} catch {
|
|
77
|
+
prompt = 'You are an autonomous coding agent. Complete the task below.';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const taskBlock = `
|
|
81
|
+
## Current Task: ${task.id}. ${task.title}
|
|
82
|
+
|
|
83
|
+
Stage: ${task.stage}
|
|
84
|
+
|
|
85
|
+
${task.details}
|
|
86
|
+
`;
|
|
87
|
+
|
|
88
|
+
return `${prompt}\n${taskBlock}${retryContext
|
|
89
|
+
? formatGateErrors(retryContext.gateResults, retryContext.attempt, retryContext.maxRetries)
|
|
90
|
+
: ''}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function formatGateErrors(gateResult, attempt, maxRetries) {
|
|
94
|
+
let ctx = `\n## Previous Attempt Failed (attempt ${attempt} of ${maxRetries})\n\nThe following quality gates failed:\n`;
|
|
95
|
+
for (const r of gateResult.results) {
|
|
96
|
+
if (r.skipped) continue;
|
|
97
|
+
if (!r.passed) {
|
|
98
|
+
ctx += `\n### ${r.name} (exit code ${r.exitCode})\n\`\`\`\n${r.output}\n\`\`\`\n`;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
ctx += '\nFix the issues above and try again. Do not repeat the same mistakes.\n';
|
|
102
|
+
return ctx;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function logGateResults(tui, gateResults) {
|
|
106
|
+
const maxNameLen = Math.max(...gateResults.map(r => r.name.length));
|
|
107
|
+
|
|
108
|
+
for (const r of gateResults) {
|
|
109
|
+
const dots = '.'.repeat(Math.max(1, maxNameLen - r.name.length + 3));
|
|
110
|
+
const durationStr = chalk.dim(` (${(r.duration / 1000).toFixed(1)}s)`);
|
|
111
|
+
let status;
|
|
112
|
+
if (r.skipped) {
|
|
113
|
+
status = chalk.dim('skipped');
|
|
114
|
+
} else if (r.passed) {
|
|
115
|
+
status = chalk.green('PASS') + durationStr;
|
|
116
|
+
} else {
|
|
117
|
+
status = chalk.red('FAIL') + durationStr;
|
|
118
|
+
}
|
|
119
|
+
tui.appendLog(`${chalk.white('[Gate]')} ${r.name} ${chalk.dim(dots)} ${status}`);
|
|
120
|
+
|
|
121
|
+
if (!r.passed && !r.skipped && r.output) {
|
|
122
|
+
for (const line of r.output.split('\n').filter(l => l.trim()).slice(0, 5)) {
|
|
123
|
+
tui.appendLog(chalk.dim(` ${line}`));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const SKIP_SIMPLIFY = [
|
|
130
|
+
/\.test\.\w+$/, /\.spec\.\w+$/, /\.config\.\w+$/, /\.d\.ts$/,
|
|
131
|
+
/lock\.\w+$/, /\.lock$/, /\.generated\./, /node_modules\//, /dist\//, /\.min\.\w+$/,
|
|
132
|
+
/^\.golem\//, /^\.claude\//,
|
|
133
|
+
];
|
|
134
|
+
|
|
135
|
+
function getChangedFiles() {
|
|
136
|
+
try {
|
|
137
|
+
return execSync('git diff --name-only HEAD', gitOpts())
|
|
138
|
+
.toString().trim().split('\n').filter(Boolean);
|
|
139
|
+
} catch {
|
|
140
|
+
return execSync('git status --porcelain', gitOpts())
|
|
141
|
+
.toString().trim().split('\n')
|
|
142
|
+
.map(l => l.slice(3).trim()).filter(Boolean);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function runSimplifyPass(files, config, tui) {
|
|
147
|
+
const targets = files.filter(f => !SKIP_SIMPLIFY.some(p => p.test(f)));
|
|
148
|
+
if (targets.length === 0) return;
|
|
149
|
+
|
|
150
|
+
tui.appendLog(chalk.cyan(`[Simplify] ${targets.length} file(s)`));
|
|
151
|
+
|
|
152
|
+
let prompt;
|
|
153
|
+
try {
|
|
154
|
+
prompt = await readFile(join(process.cwd(), '.golem/prompts/simplify.md'), 'utf-8');
|
|
155
|
+
} catch {
|
|
156
|
+
prompt = 'Simplify the given files. Run tests after each change. Revert on failure.';
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const fileList = targets.map(f => `- ${f}`).join('\n');
|
|
160
|
+
const fullPrompt = prompt.replace('{{FILES}}', fileList) + '\n\n## Test Command\n\n`node --test`\n';
|
|
161
|
+
|
|
162
|
+
const emitter = runClaude(fullPrompt, { model: 'sonnet' });
|
|
163
|
+
attachDisplay(emitter, { tui });
|
|
164
|
+
|
|
165
|
+
await new Promise((resolve) => {
|
|
166
|
+
emitter.on('close', resolve);
|
|
167
|
+
emitter.on('error', resolve);
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function runSecurityCheck(tui) {
|
|
172
|
+
tui.appendLog(chalk.cyan('[Security] Running scan...'));
|
|
173
|
+
const { verdict, findings } = await runSecurityScan();
|
|
174
|
+
|
|
175
|
+
const critical = findings.filter(f => f.severity === 'CRITICAL');
|
|
176
|
+
const high = findings.filter(f => f.severity === 'HIGH');
|
|
177
|
+
|
|
178
|
+
if (verdict === 'PASS') {
|
|
179
|
+
tui.appendLog(chalk.green('[Security] PASS — no issues'));
|
|
180
|
+
} else if (critical.length > 0 || high.length > 0) {
|
|
181
|
+
tui.appendLog(chalk.red(`[Security] FAIL — ${critical.length} critical, ${high.length} high`));
|
|
182
|
+
for (const f of [...critical, ...high].slice(0, 5)) {
|
|
183
|
+
tui.appendLog(chalk.dim(` ${f.severity}: ${f.message.slice(0, 60)} (${f.file})`));
|
|
184
|
+
}
|
|
185
|
+
} else {
|
|
186
|
+
tui.appendLog(chalk.yellow(`[Security] PARTIAL — ${findings.length} finding(s), none critical`));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return { verdict, findings, critical, high };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function gitOpts() {
|
|
193
|
+
return { cwd: process.cwd(), stdio: 'pipe' };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function gitSnapshot() {
|
|
197
|
+
return execSync('git rev-parse HEAD', gitOpts()).toString().trim();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function gitRestore(sha) {
|
|
201
|
+
execSync('git checkout .', gitOpts());
|
|
202
|
+
execSync('git clean -fd', gitOpts());
|
|
203
|
+
execSync(`git reset --hard ${sha}`, gitOpts());
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function gitHasChanges() {
|
|
207
|
+
return execSync('git status --porcelain', gitOpts()).toString().length > 0;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function gitDiffStats() {
|
|
211
|
+
try {
|
|
212
|
+
const out = execSync('git diff --cached --stat', gitOpts()).toString();
|
|
213
|
+
const lines = out.trim().split('\n');
|
|
214
|
+
const summary = lines[lines.length - 1] || '';
|
|
215
|
+
const extract = (pattern) => {
|
|
216
|
+
const match = summary.match(pattern);
|
|
217
|
+
return match ? parseInt(match[1]) : 0;
|
|
218
|
+
};
|
|
219
|
+
return {
|
|
220
|
+
files: extract(/(\d+) files? changed/),
|
|
221
|
+
ins: extract(/(\d+) insertions?/),
|
|
222
|
+
del: extract(/(\d+) deletions?/),
|
|
223
|
+
};
|
|
224
|
+
} catch {
|
|
225
|
+
return { files: 0, ins: 0, del: 0 };
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function gitCommit(message) {
|
|
230
|
+
try {
|
|
231
|
+
execSync('git add -A', gitOpts());
|
|
232
|
+
const stats = gitDiffStats();
|
|
233
|
+
execSync(`git commit -m "${message}"`, gitOpts());
|
|
234
|
+
const hash = execSync('git rev-parse --short HEAD', gitOpts()).toString().trim();
|
|
235
|
+
return { hash, ...stats };
|
|
236
|
+
} catch {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export async function runBuild(opts = {}) {
|
|
242
|
+
if (!checkClaudeCli()) {
|
|
243
|
+
fail('Claude CLI not found. Install it from https://docs.anthropic.com/en/docs/claude-code');
|
|
244
|
+
process.exit(1);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
try {
|
|
248
|
+
const specsDir = join(process.cwd(), '.golem/specs');
|
|
249
|
+
const specs = (await readdir(specsDir)).filter(f => f.endsWith('.md'));
|
|
250
|
+
if (specs.length === 0) {
|
|
251
|
+
warn('No spec files found in .golem/specs/. Run /golem:spec first to create specs.');
|
|
252
|
+
}
|
|
253
|
+
} catch {
|
|
254
|
+
warn('No .golem/specs/ directory found. Run golem init first.');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const config = await loadConfig();
|
|
258
|
+
const maxRetries = opts.retry === false ? 1 : (opts.retries || config.maxRetries || 3);
|
|
259
|
+
const maxTasks = opts.tasks || Infinity;
|
|
260
|
+
let tasksProcessed = 0;
|
|
261
|
+
let claudeCalls = 0;
|
|
262
|
+
let stopping = false;
|
|
263
|
+
let buildSucceeded = false;
|
|
264
|
+
const summaryRows = [];
|
|
265
|
+
let tui = null;
|
|
266
|
+
|
|
267
|
+
const onSigint = () => {
|
|
268
|
+
if (stopping) {
|
|
269
|
+
if (tui) tui.destroy();
|
|
270
|
+
warn('Force quitting...');
|
|
271
|
+
process.exit(1);
|
|
272
|
+
}
|
|
273
|
+
stopping = true;
|
|
274
|
+
if (tui) {
|
|
275
|
+
tui.appendLog('\nStopping after current task... (Ctrl+C again to force quit)');
|
|
276
|
+
} else {
|
|
277
|
+
warn('Stopping after current task... (Ctrl+C again to force quit)');
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
process.on('SIGINT', onSigint);
|
|
281
|
+
|
|
282
|
+
if (opts.dryRun) {
|
|
283
|
+
header('Golem Build Loop');
|
|
284
|
+
|
|
285
|
+
let plan;
|
|
286
|
+
try {
|
|
287
|
+
plan = await readFile(PLAN_PATH, 'utf-8');
|
|
288
|
+
} catch {
|
|
289
|
+
fail('No implementation plan found. Run /golem:plan first.');
|
|
290
|
+
process.exit(1);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const { total, done } = countTasks(plan);
|
|
294
|
+
const task = parseNextTask(plan);
|
|
295
|
+
|
|
296
|
+
if (!task) {
|
|
297
|
+
success(`All ${total} tasks complete!`);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
header(`Task ${task.id}: ${task.title}`);
|
|
302
|
+
info(`Progress: ${done}/${total} tasks (${Math.round(done / total * 100)}%)`);
|
|
303
|
+
info('Dry run — showing next task:');
|
|
304
|
+
console.log();
|
|
305
|
+
console.log(task.details);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
let usageData;
|
|
310
|
+
try {
|
|
311
|
+
usageData = await getUsageData();
|
|
312
|
+
} catch {
|
|
313
|
+
usageData = null;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
let plan;
|
|
317
|
+
try {
|
|
318
|
+
plan = await readFile(PLAN_PATH, 'utf-8');
|
|
319
|
+
} catch {
|
|
320
|
+
fail('No implementation plan found. Run /golem:plan first.');
|
|
321
|
+
process.exit(1);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const initialCounts = countTasks(plan);
|
|
325
|
+
const firstTask = parseNextTask(plan);
|
|
326
|
+
|
|
327
|
+
if (!firstTask) {
|
|
328
|
+
success(`All ${initialCounts.total} tasks complete!`);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
tui = createTui();
|
|
333
|
+
tui.init({
|
|
334
|
+
taskId: firstTask.id,
|
|
335
|
+
taskTitle: firstTask.title,
|
|
336
|
+
stage: firstTask.stage,
|
|
337
|
+
done: initialCounts.done,
|
|
338
|
+
total: initialCounts.total,
|
|
339
|
+
}, usageData);
|
|
340
|
+
|
|
341
|
+
try {
|
|
342
|
+
while (tasksProcessed < maxTasks && !stopping) {
|
|
343
|
+
plan = await readFile(PLAN_PATH, 'utf-8');
|
|
344
|
+
const { total, done } = countTasks(plan);
|
|
345
|
+
const task = parseNextTask(plan);
|
|
346
|
+
|
|
347
|
+
if (!task) {
|
|
348
|
+
tui.appendLog(`\nAll ${total} tasks complete!`);
|
|
349
|
+
buildSucceeded = true;
|
|
350
|
+
break;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const snapshot = gitSnapshot();
|
|
354
|
+
tasksProcessed++;
|
|
355
|
+
|
|
356
|
+
tui.updateTask({
|
|
357
|
+
taskId: task.id,
|
|
358
|
+
taskTitle: task.title,
|
|
359
|
+
stage: task.stage,
|
|
360
|
+
done,
|
|
361
|
+
total,
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
const taskModel = task.model || config.model;
|
|
365
|
+
tui.appendLog(`\n--- Task ${tasksProcessed} — ${task.id}: ${task.title} ---`);
|
|
366
|
+
tui.appendLog(chalk.dim(`[Model] ${taskModel} (fallback: sonnet)`));
|
|
367
|
+
tui.setTaskModel(taskModel);
|
|
368
|
+
|
|
369
|
+
// Pre-task rate limit check (informational — fallback is automatic)
|
|
370
|
+
const usage = await fetchUsage();
|
|
371
|
+
if (usage) {
|
|
372
|
+
tui.updateRateLimit(usage);
|
|
373
|
+
|
|
374
|
+
if (usage.fiveHour.utilization >= config.rateLimitWarnThreshold) {
|
|
375
|
+
const resetTime = formatResetTime(usage.fiveHour.resetsAt);
|
|
376
|
+
tui.appendLog(chalk.yellow(`[Rate Limit] 5h at ${Math.round(usage.fiveHour.utilization)}% — resets ${resetTime}, will fallback to sonnet`));
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
let passed = false;
|
|
381
|
+
let lastGateOutput = null;
|
|
382
|
+
let lastDisplay = null;
|
|
383
|
+
const attemptHistory = [];
|
|
384
|
+
|
|
385
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
386
|
+
if (stopping) break;
|
|
387
|
+
|
|
388
|
+
tui.resetTimer();
|
|
389
|
+
tui.updateTask({ attempt, maxAttempts: maxRetries });
|
|
390
|
+
|
|
391
|
+
if (attempt > 1) {
|
|
392
|
+
gitRestore(snapshot);
|
|
393
|
+
tui.appendLog(chalk.dim(`\n── Retrying task ${task.id} (attempt ${attempt}/${maxRetries}) ──`));
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const retryContext = (attempt > 1 && lastGateOutput)
|
|
397
|
+
? { attempt: attempt - 1, maxRetries, gateResults: lastGateOutput }
|
|
398
|
+
: null;
|
|
399
|
+
const prompt = await buildPrompt(task, config, retryContext);
|
|
400
|
+
|
|
401
|
+
claudeCalls++;
|
|
402
|
+
tui.updateTask({ claudeCalls });
|
|
403
|
+
const fallback = taskModel === 'sonnet' ? undefined : 'sonnet';
|
|
404
|
+
const emitter = runClaude(prompt, { model: taskModel, fallbackModel: fallback });
|
|
405
|
+
lastDisplay = attachDisplay(emitter, { tui });
|
|
406
|
+
|
|
407
|
+
const result = await new Promise((resolve) => {
|
|
408
|
+
let resultEvent = null;
|
|
409
|
+
emitter.on('result', (event) => { resultEvent = event; });
|
|
410
|
+
emitter.on('close', () => { resolve(resultEvent); });
|
|
411
|
+
emitter.on('error', (err) => {
|
|
412
|
+
tui.appendLog(`Error: ${err.message}`);
|
|
413
|
+
resolve(null);
|
|
414
|
+
});
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
if (!result || result.is_error) {
|
|
418
|
+
tui.appendLog(`Task ${task.id} — Claude error (attempt ${attempt}/${maxRetries})`);
|
|
419
|
+
break;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (opts.skipGates) {
|
|
423
|
+
passed = true;
|
|
424
|
+
break;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const gateResult = await runGates(config);
|
|
428
|
+
if (gateResult.results.length > 0) {
|
|
429
|
+
logGateResults(tui, gateResult.results);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (gateResult.passed) {
|
|
433
|
+
passed = true;
|
|
434
|
+
break;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
lastGateOutput = gateResult;
|
|
438
|
+
attemptHistory.push({
|
|
439
|
+
attempt,
|
|
440
|
+
failedGate: gateResult.failedGate,
|
|
441
|
+
summary: `${gateResult.failedGate} failed`,
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const elapsed = (tui.getCumulativeElapsed() / 1000).toFixed(1);
|
|
446
|
+
|
|
447
|
+
if (passed) {
|
|
448
|
+
const finalAttempt = attemptHistory.length + 1;
|
|
449
|
+
if (finalAttempt > 1) {
|
|
450
|
+
tui.appendLog(chalk.green(`Task ${task.id} PASSED on attempt ${finalAttempt}/${maxRetries}`));
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (config.simplifyOnBuild && !opts.skipGates) {
|
|
454
|
+
const changedFiles = getChangedFiles();
|
|
455
|
+
if (changedFiles.length > 0) {
|
|
456
|
+
claudeCalls++;
|
|
457
|
+
tui.updateTask({ claudeCalls });
|
|
458
|
+
const preSimplifySnapshot = gitSnapshot();
|
|
459
|
+
await runSimplifyPass(changedFiles, config, tui);
|
|
460
|
+
|
|
461
|
+
const postSimplifyGates = await runGates(config);
|
|
462
|
+
if (!postSimplifyGates.passed) {
|
|
463
|
+
tui.appendLog(chalk.yellow('[Simplify] Broke tests — reverting simplification'));
|
|
464
|
+
gitRestore(preSimplifySnapshot);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (!opts.skipGates) {
|
|
470
|
+
const sec = await runSecurityCheck(tui);
|
|
471
|
+
if (sec.critical.length > 0 || sec.high.length > 0) {
|
|
472
|
+
tui.appendLog(chalk.red(`[Security] Blocking commit — ${sec.critical.length + sec.high.length} critical/high issue(s)`));
|
|
473
|
+
gitRestore(snapshot);
|
|
474
|
+
summaryRows.push([task.id, task.title.slice(0, 35), `${elapsed}s`, 'SECURITY', '-']);
|
|
475
|
+
buildSucceeded = false;
|
|
476
|
+
break; // Hard stop
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
plan = await readFile(PLAN_PATH, 'utf-8');
|
|
481
|
+
const updatedPlan = markTaskComplete(plan, task.lineIndex);
|
|
482
|
+
await writeFile(PLAN_PATH, updatedPlan);
|
|
483
|
+
tui.appendLog(`Task ${task.id} marked complete`);
|
|
484
|
+
|
|
485
|
+
let commitInfo = null;
|
|
486
|
+
if (config.autoCommit) {
|
|
487
|
+
const commitMsg = task.commit || `feat: complete task ${task.id} — ${task.title}`;
|
|
488
|
+
commitInfo = gitCommit(commitMsg);
|
|
489
|
+
if (commitInfo) {
|
|
490
|
+
tui.appendLog(`Committed: ${commitInfo.hash} ${commitMsg}`);
|
|
491
|
+
} else {
|
|
492
|
+
tui.appendLog('No changes to commit');
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const changes = commitInfo
|
|
497
|
+
? `${commitInfo.files}f +${commitInfo.ins} -${commitInfo.del}`
|
|
498
|
+
: lastDisplay?.stats.filesModified.length
|
|
499
|
+
? `${lastDisplay.stats.filesModified.length} files`
|
|
500
|
+
: '-';
|
|
501
|
+
const commit = commitInfo ? commitInfo.hash : '-';
|
|
502
|
+
|
|
503
|
+
summaryRows.push([
|
|
504
|
+
task.id,
|
|
505
|
+
task.title.slice(0, 35),
|
|
506
|
+
`${elapsed}s`,
|
|
507
|
+
changes,
|
|
508
|
+
commit,
|
|
509
|
+
]);
|
|
510
|
+
|
|
511
|
+
try {
|
|
512
|
+
usageData = await getUsageData();
|
|
513
|
+
tui.updateUsage(usageData);
|
|
514
|
+
} catch {}
|
|
515
|
+
|
|
516
|
+
const newCounts = countTasks(await readFile(PLAN_PATH, 'utf-8'));
|
|
517
|
+
tui.updateTask({ done: newCounts.done });
|
|
518
|
+
buildSucceeded = true;
|
|
519
|
+
} else {
|
|
520
|
+
gitRestore(snapshot);
|
|
521
|
+
tui.appendLog(chalk.red(`\nTask ${task.id} FAILED after ${maxRetries} attempts`));
|
|
522
|
+
for (const h of attemptHistory) {
|
|
523
|
+
tui.appendLog(chalk.dim(` Attempt ${h.attempt}: ${h.summary}`));
|
|
524
|
+
}
|
|
525
|
+
summaryRows.push([
|
|
526
|
+
task.id,
|
|
527
|
+
task.title.slice(0, 35),
|
|
528
|
+
`${elapsed}s`,
|
|
529
|
+
'FAILED',
|
|
530
|
+
'-',
|
|
531
|
+
]);
|
|
532
|
+
buildSucceeded = false;
|
|
533
|
+
break; // Hard stop
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Post-build documentation pass
|
|
538
|
+
if (buildSucceeded && config.documentOnBuild) {
|
|
539
|
+
tui.appendLog(chalk.cyan('\n[Document] Running post-build documentation pass...'));
|
|
540
|
+
try {
|
|
541
|
+
await runDocument({ model: config.model });
|
|
542
|
+
tui.appendLog(chalk.green('[Document] Documentation pass complete'));
|
|
543
|
+
} catch (err) {
|
|
544
|
+
tui.appendLog(chalk.yellow(`[Document] Documentation pass failed: ${err.message}`));
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
} finally {
|
|
548
|
+
if (tui) tui.destroy();
|
|
549
|
+
process.removeListener('SIGINT', onSigint);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
if (summaryRows.length > 0) {
|
|
553
|
+
console.log();
|
|
554
|
+
header('Build Summary');
|
|
555
|
+
table(['Task', 'Title', 'Time', 'Changes', 'Commit'], summaryRows);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { spawn, execSync } from 'node:child_process';
|
|
2
|
+
import { EventEmitter } from 'node:events';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Check if the claude CLI is installed and accessible.
|
|
6
|
+
* Returns the version string if found, null otherwise.
|
|
7
|
+
*/
|
|
8
|
+
export function checkClaudeCli() {
|
|
9
|
+
try {
|
|
10
|
+
return execSync('claude --version', { stdio: 'pipe', encoding: 'utf-8' }).trim();
|
|
11
|
+
} catch {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Run claude -p with stream-json output.
|
|
18
|
+
* Returns an EventEmitter that emits:
|
|
19
|
+
* 'init' - { session_id, model, tools }
|
|
20
|
+
* 'assistant' - { message } (content blocks: text, tool_use, tool_result)
|
|
21
|
+
* 'result' - { result, duration_ms, total_cost_usd, usage, is_error }
|
|
22
|
+
* 'error' - Error
|
|
23
|
+
* 'close' - { code }
|
|
24
|
+
*/
|
|
25
|
+
export function runClaude(prompt, opts = {}) {
|
|
26
|
+
const emitter = new EventEmitter();
|
|
27
|
+
const args = ['-p', '--output-format', 'stream-json'];
|
|
28
|
+
|
|
29
|
+
if (opts.model) {
|
|
30
|
+
args.push('--model', opts.model);
|
|
31
|
+
}
|
|
32
|
+
if (opts.fallbackModel) {
|
|
33
|
+
args.push('--fallback-model', opts.fallbackModel);
|
|
34
|
+
}
|
|
35
|
+
if (opts.allowedTools) {
|
|
36
|
+
for (const tool of opts.allowedTools) {
|
|
37
|
+
args.push('--allowedTools', tool);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const proc = spawn('claude', args, {
|
|
42
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
43
|
+
cwd: opts.cwd || process.cwd(),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
let stderrBuf = '';
|
|
47
|
+
|
|
48
|
+
proc.stdout.on('data', (chunk) => {
|
|
49
|
+
const lines = chunk.toString().split('\n').filter(Boolean);
|
|
50
|
+
for (const line of lines) {
|
|
51
|
+
try {
|
|
52
|
+
const event = JSON.parse(line);
|
|
53
|
+
switch (event.type) {
|
|
54
|
+
case 'system':
|
|
55
|
+
emitter.emit('init', event);
|
|
56
|
+
break;
|
|
57
|
+
case 'assistant':
|
|
58
|
+
emitter.emit('assistant', event);
|
|
59
|
+
break;
|
|
60
|
+
case 'result':
|
|
61
|
+
emitter.emit('result', event);
|
|
62
|
+
break;
|
|
63
|
+
default:
|
|
64
|
+
emitter.emit(event.type, event);
|
|
65
|
+
}
|
|
66
|
+
} catch {
|
|
67
|
+
// partial JSON line, ignore
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
proc.stderr.on('data', (chunk) => {
|
|
73
|
+
stderrBuf += chunk.toString();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
proc.on('close', (code) => {
|
|
77
|
+
if (code !== 0 && stderrBuf) {
|
|
78
|
+
emitter.emit('error', new Error(stderrBuf.trim()));
|
|
79
|
+
}
|
|
80
|
+
emitter.emit('close', { code });
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
proc.on('error', (err) => {
|
|
84
|
+
emitter.emit('error', err);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Write prompt to stdin and close
|
|
88
|
+
proc.stdin.write(prompt);
|
|
89
|
+
proc.stdin.end();
|
|
90
|
+
|
|
91
|
+
// Expose process for kill/signal handling
|
|
92
|
+
emitter.process = proc;
|
|
93
|
+
|
|
94
|
+
return emitter;
|
|
95
|
+
}
|