@ai-sdlc/orchestrator 0.4.0 → 0.6.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/dist/action-enforcement.d.ts +26 -0
- package/dist/action-enforcement.js +70 -0
- package/dist/adapters.d.ts +18 -3
- package/dist/adapters.js +92 -2
- package/dist/admission-score.d.ts +58 -0
- package/dist/admission-score.js +164 -0
- package/dist/cli/commands/init.js +4 -8
- package/dist/cli/commands/run.js +2 -2
- package/dist/config.d.ts +3 -0
- package/dist/config.js +14 -5
- package/dist/cycle-utils.d.ts +51 -0
- package/dist/cycle-utils.js +77 -0
- package/dist/defaults.d.ts +5 -0
- package/dist/defaults.js +5 -0
- package/dist/execute.d.ts +5 -2
- package/dist/execute.js +212 -62
- package/dist/fix-ci.js +45 -13
- package/dist/fix-review.d.ts +66 -0
- package/dist/fix-review.js +441 -0
- package/dist/index.d.ts +14 -4
- package/dist/index.js +18 -3
- package/dist/orchestrator.d.ts +1 -1
- package/dist/orchestrator.js +31 -9
- package/dist/pipeline-cycle-detector.d.ts +70 -0
- package/dist/pipeline-cycle-detector.js +111 -0
- package/dist/plugin.d.ts +9 -3
- package/dist/priority.d.ts +28 -0
- package/dist/priority.js +230 -0
- package/dist/review.d.ts +31 -0
- package/dist/review.js +74 -0
- package/dist/runners/claude-code.js +367 -35
- package/dist/runners/codex.js +15 -4
- package/dist/runners/copilot.js +15 -4
- package/dist/runners/cursor.js +15 -4
- package/dist/runners/generic-llm.js +1 -1
- package/dist/runners/index.d.ts +3 -1
- package/dist/runners/index.js +2 -0
- package/dist/runners/review-agent.d.ts +47 -0
- package/dist/runners/review-agent.js +220 -0
- package/dist/runners/security-triage.d.ts +43 -0
- package/dist/runners/security-triage.js +158 -0
- package/dist/runners/types.d.ts +24 -1
- package/dist/security.d.ts +8 -3
- package/dist/security.js +13 -2
- package/dist/shared.d.ts +17 -0
- package/dist/shared.js +27 -0
- package/dist/state/index.d.ts +1 -1
- package/dist/state/schema.d.ts +4 -1
- package/dist/state/schema.js +89 -1
- package/dist/state/store.d.ts +31 -1
- package/dist/state/store.js +208 -13
- package/dist/state/types.d.ts +52 -0
- package/dist/triage.d.ts +36 -0
- package/dist/triage.js +133 -0
- package/dist/types.d.ts +1 -1
- package/dist/watch.d.ts +6 -2
- package/dist/watch.js +34 -6
- package/dist/workflow-patterns/artifact-writer.d.ts +16 -0
- package/dist/workflow-patterns/artifact-writer.js +34 -0
- package/dist/workflow-patterns/classifiers.d.ts +10 -0
- package/dist/workflow-patterns/classifiers.js +72 -0
- package/dist/workflow-patterns/detector.d.ts +27 -0
- package/dist/workflow-patterns/detector.js +186 -0
- package/dist/workflow-patterns/index.d.ts +8 -0
- package/dist/workflow-patterns/index.js +7 -0
- package/dist/workflow-patterns/proposal-generator.d.ts +15 -0
- package/dist/workflow-patterns/proposal-generator.js +183 -0
- package/dist/workflow-patterns/telemetry-ingest.d.ts +27 -0
- package/dist/workflow-patterns/telemetry-ingest.js +103 -0
- package/dist/workflow-patterns/types.d.ts +61 -0
- package/dist/workflow-patterns/types.js +11 -0
- package/package.json +4 -2
|
@@ -7,11 +7,33 @@ import { promisify } from 'node:util';
|
|
|
7
7
|
import { DEFAULT_MODEL, DEFAULT_ALLOWED_TOOLS, DEFAULT_RUNNER_TIMEOUT_MS, DEFAULT_LINT_COMMAND, DEFAULT_FORMAT_COMMAND, DEFAULT_COMMIT_MESSAGE_TEMPLATE, DEFAULT_COMMIT_CO_AUTHOR, } from '../defaults.js';
|
|
8
8
|
import { formatContextForPrompt } from '../analysis/context-builder.js';
|
|
9
9
|
const execFileAsync = promisify(execFile);
|
|
10
|
+
/**
|
|
11
|
+
* Build verification step instructions (lint, format, typecheck).
|
|
12
|
+
* Returns an array of numbered step strings starting from the given step number.
|
|
13
|
+
*/
|
|
14
|
+
function buildVerificationSteps(startStep, lintCmd, fmtCmd, typecheckCmd) {
|
|
15
|
+
let step = startStep;
|
|
16
|
+
const lines = [];
|
|
17
|
+
if (lintCmd && fmtCmd) {
|
|
18
|
+
lines.push(`${++step}. After making code changes, run \`${lintCmd}\` and \`${fmtCmd}\` to catch issues before committing.`);
|
|
19
|
+
}
|
|
20
|
+
else if (lintCmd) {
|
|
21
|
+
lines.push(`${++step}. After making code changes, run \`${lintCmd}\` to catch issues before committing.`);
|
|
22
|
+
}
|
|
23
|
+
else if (fmtCmd) {
|
|
24
|
+
lines.push(`${++step}. After making code changes, run \`${fmtCmd}\` to catch issues before committing.`);
|
|
25
|
+
}
|
|
26
|
+
if (typecheckCmd) {
|
|
27
|
+
lines.push(`${++step}. IMPORTANT: Run \`${typecheckCmd}\` to verify there are no TypeScript errors. The pre-commit hook will reject your commit if there are type errors. Fix ALL type errors before committing.`);
|
|
28
|
+
}
|
|
29
|
+
return { lines, nextStep: step };
|
|
30
|
+
}
|
|
10
31
|
export function buildPrompt(ctx) {
|
|
11
32
|
const lintCmd = ctx.lintCommand ?? DEFAULT_LINT_COMMAND;
|
|
12
33
|
const fmtCmd = ctx.formatCommand ?? DEFAULT_FORMAT_COMMAND;
|
|
34
|
+
const typecheckCmd = ctx.typecheckCommand;
|
|
13
35
|
const lines = [
|
|
14
|
-
`You are fixing issue
|
|
36
|
+
`You are fixing issue ${/^\d+$/.test(ctx.issueId) ? '#' : ''}${ctx.issueId}: ${ctx.issueTitle}`,
|
|
15
37
|
'',
|
|
16
38
|
'## Issue Description',
|
|
17
39
|
ctx.issueBody,
|
|
@@ -23,35 +45,31 @@ export function buildPrompt(ctx) {
|
|
|
23
45
|
if (fmtCmd) {
|
|
24
46
|
lines.push(`${++step}. If the failure is a formatting/prettier error, run \`${fmtCmd}\` to auto-fix it.`);
|
|
25
47
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
else if (lintCmd) {
|
|
30
|
-
lines.push(`${++step}. After making ANY code changes, always run \`${lintCmd}\` to catch issues before committing.`);
|
|
31
|
-
}
|
|
32
|
-
else if (fmtCmd) {
|
|
33
|
-
lines.push(`${++step}. After making ANY code changes, always run \`${fmtCmd}\` to catch issues before committing.`);
|
|
34
|
-
}
|
|
48
|
+
const ciVerify = buildVerificationSteps(step, lintCmd, fmtCmd, typecheckCmd);
|
|
49
|
+
lines.push(...ciVerify.lines);
|
|
50
|
+
step = ciVerify.nextStep;
|
|
35
51
|
lines.push(`${++step}. Write or update tests if needed to cover your fix.`, `${++step}. NEVER modify files matching the blocked paths below — violations will be automatically detected and the change will be rejected.`, `${++step}. Keep your changes to at most ${ctx.constraints.maxFilesPerChange} files.`);
|
|
36
52
|
}
|
|
53
|
+
else if (ctx.reviewFindings) {
|
|
54
|
+
let step = 0;
|
|
55
|
+
lines.push('## Review Findings', '', ctx.reviewFindings, '', '## Instructions', `${++step}. Read the review findings above carefully.`, `${++step}. Read the relevant source files to understand the context.`, `${++step}. Address all the review findings by making necessary code changes.`, `${++step}. Write or update tests if requested by the reviewers.`);
|
|
56
|
+
const reviewVerify = buildVerificationSteps(step, lintCmd, fmtCmd, typecheckCmd);
|
|
57
|
+
lines.push(...reviewVerify.lines);
|
|
58
|
+
step = reviewVerify.nextStep;
|
|
59
|
+
lines.push(`${++step}. NEVER modify files matching the blocked paths below — violations will be automatically detected and the change will be rejected.`, `${++step}. Keep your changes to at most ${ctx.constraints.maxFilesPerChange} files.`);
|
|
60
|
+
}
|
|
37
61
|
else {
|
|
38
62
|
let step = 0;
|
|
39
63
|
lines.push('## Instructions', `${++step}. Read the relevant source files to understand the codebase.`, `${++step}. Implement the fix or feature described in the issue.`, `${++step}. Write or update tests to cover your changes.`);
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
else if (lintCmd) {
|
|
44
|
-
lines.push(`${++step}. After making code changes, run \`${lintCmd}\` to ensure CI will pass.`);
|
|
45
|
-
}
|
|
46
|
-
else if (fmtCmd) {
|
|
47
|
-
lines.push(`${++step}. After making code changes, run \`${fmtCmd}\` to ensure CI will pass.`);
|
|
48
|
-
}
|
|
64
|
+
const defaultVerify = buildVerificationSteps(step, lintCmd, fmtCmd, typecheckCmd);
|
|
65
|
+
lines.push(...defaultVerify.lines);
|
|
66
|
+
step = defaultVerify.nextStep;
|
|
49
67
|
lines.push(`${++step}. NEVER modify files matching the blocked paths below — violations will be automatically detected and the change will be rejected.`, `${++step}. Keep your changes to at most ${ctx.constraints.maxFilesPerChange} files.`);
|
|
50
68
|
}
|
|
51
69
|
lines.push('', '## Constraints (enforced — violations will be automatically rejected)', `- Maximum files to change: ${ctx.constraints.maxFilesPerChange}`, `- Tests required: ${ctx.constraints.requireTests}`, `- Blocked paths (NEVER modify — changes will be rejected): ${ctx.constraints.blockedPaths.join(', ') || 'none'}`);
|
|
52
70
|
// Append relevant episodic memory if available
|
|
53
71
|
if (ctx.memory) {
|
|
54
|
-
const episodes = ctx.memory.episodic.search(`issue-${ctx.
|
|
72
|
+
const episodes = ctx.memory.episodic.search(`issue-${ctx.issueId}`);
|
|
55
73
|
if (episodes.length > 0) {
|
|
56
74
|
lines.push('', '## Previous Context');
|
|
57
75
|
for (const ep of episodes.slice(0, 5)) {
|
|
@@ -76,32 +94,276 @@ async function gitExec(workDir, args) {
|
|
|
76
94
|
const { stdout } = await execFileAsync('git', args, { cwd: workDir });
|
|
77
95
|
return stdout.trim();
|
|
78
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* Format a stream-json event into a concise, human-readable log line.
|
|
99
|
+
*/
|
|
100
|
+
function formatEventForLog(line) {
|
|
101
|
+
let parsed;
|
|
102
|
+
try {
|
|
103
|
+
parsed = JSON.parse(line);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
const type = parsed.type;
|
|
109
|
+
if (type === 'system') {
|
|
110
|
+
return `init session=${parsed.session_id?.slice(0, 8)}`;
|
|
111
|
+
}
|
|
112
|
+
if (type === 'assistant') {
|
|
113
|
+
const message = parsed.message;
|
|
114
|
+
const content = message?.content;
|
|
115
|
+
if (!content?.length)
|
|
116
|
+
return null;
|
|
117
|
+
const parts = [];
|
|
118
|
+
for (const block of content) {
|
|
119
|
+
if (block.type === 'thinking') {
|
|
120
|
+
const text = String(block.thinking ?? '');
|
|
121
|
+
parts.push(`thinking: ${text.slice(0, 120)}${text.length > 120 ? '...' : ''}`);
|
|
122
|
+
}
|
|
123
|
+
else if (block.type === 'text') {
|
|
124
|
+
const text = String(block.text ?? '');
|
|
125
|
+
parts.push(`text: ${text.slice(0, 120)}${text.length > 120 ? '...' : ''}`);
|
|
126
|
+
}
|
|
127
|
+
else if (block.type === 'tool_use') {
|
|
128
|
+
const name = String(block.name ?? '');
|
|
129
|
+
const input = (block.input ?? {});
|
|
130
|
+
const file = extractFilePath(input);
|
|
131
|
+
parts.push(file ? `${name}: ${file}` : name);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return parts.join(' | ');
|
|
135
|
+
}
|
|
136
|
+
if (type === 'user') {
|
|
137
|
+
const message = parsed.message;
|
|
138
|
+
const content = message?.content;
|
|
139
|
+
if (!content?.length)
|
|
140
|
+
return null;
|
|
141
|
+
const block = content[0];
|
|
142
|
+
if (block.type === 'tool_result') {
|
|
143
|
+
const text = String(block.content ?? '');
|
|
144
|
+
const truncated = text.slice(0, 80);
|
|
145
|
+
return `result: ${truncated}${text.length > 80 ? '...' : ''}`;
|
|
146
|
+
}
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
if (type === 'result') {
|
|
150
|
+
const cost = parsed.total_cost_usd;
|
|
151
|
+
const turns = parsed.num_turns;
|
|
152
|
+
const duration = parsed.duration_ms;
|
|
153
|
+
return `done — ${turns ?? '?'} turns, ${duration ? Math.round(duration / 1000) + 's' : '?'}, $${cost?.toFixed(4) ?? '?'}`;
|
|
154
|
+
}
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Extract a file path from a tool_use input object.
|
|
159
|
+
*/
|
|
160
|
+
function extractFilePath(input) {
|
|
161
|
+
return ((typeof input.file_path === 'string' ? input.file_path : undefined) ??
|
|
162
|
+
(typeof input.path === 'string' ? input.path : undefined) ??
|
|
163
|
+
(typeof input.pattern === 'string' ? input.pattern : undefined) ??
|
|
164
|
+
(typeof input.command === 'string' ? input.command.slice(0, 80) : undefined));
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Parse a single NDJSON line from Claude Code stream-json output
|
|
168
|
+
* and emit a progress event if applicable.
|
|
169
|
+
*/
|
|
170
|
+
function parseStreamEvent(line, onProgress) {
|
|
171
|
+
let parsed;
|
|
172
|
+
try {
|
|
173
|
+
parsed = JSON.parse(line);
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
178
|
+
const type = parsed.type;
|
|
179
|
+
if (type === 'assistant') {
|
|
180
|
+
const message = parsed.message;
|
|
181
|
+
const content = message?.content;
|
|
182
|
+
if (!content)
|
|
183
|
+
return undefined;
|
|
184
|
+
for (const block of content) {
|
|
185
|
+
if (block.type === 'tool_use') {
|
|
186
|
+
const toolName = String(block.name ?? '');
|
|
187
|
+
const input = (block.input ?? {});
|
|
188
|
+
const filePath = extractFilePath(input);
|
|
189
|
+
onProgress({
|
|
190
|
+
type: 'tool_start',
|
|
191
|
+
tool: toolName,
|
|
192
|
+
file: filePath,
|
|
193
|
+
message: filePath ? `${toolName}: ${filePath}` : toolName,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
else if (block.type === 'text') {
|
|
197
|
+
const text = String(block.text ?? '');
|
|
198
|
+
if (text.length > 0) {
|
|
199
|
+
onProgress({
|
|
200
|
+
type: 'text',
|
|
201
|
+
message: text.slice(0, 200),
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (type === 'result') {
|
|
208
|
+
const resultText = parsed.result;
|
|
209
|
+
const costUsd = parsed.total_cost_usd;
|
|
210
|
+
// Extract token usage from result event
|
|
211
|
+
const modelUsage = parsed.modelUsage;
|
|
212
|
+
let tokenUsage;
|
|
213
|
+
if (modelUsage) {
|
|
214
|
+
const firstModel = Object.keys(modelUsage)[0];
|
|
215
|
+
if (firstModel) {
|
|
216
|
+
const usage = modelUsage[firstModel];
|
|
217
|
+
tokenUsage = {
|
|
218
|
+
inputTokens: usage.inputTokens ?? 0,
|
|
219
|
+
outputTokens: usage.outputTokens ?? 0,
|
|
220
|
+
cacheReadTokens: usage.cacheReadInputTokens ?? undefined,
|
|
221
|
+
model: firstModel,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (costUsd !== undefined) {
|
|
226
|
+
onProgress({ type: 'cost', costUsd, message: `Total cost: $${costUsd.toFixed(4)}` });
|
|
227
|
+
}
|
|
228
|
+
return { resultText: resultText ?? undefined, costUsd, tokenUsage };
|
|
229
|
+
}
|
|
230
|
+
return undefined;
|
|
231
|
+
}
|
|
79
232
|
function runClaude(prompt, workDir, opts) {
|
|
80
233
|
const tools = opts?.allowedTools?.join(',') ?? DEFAULT_ALLOWED_TOOLS;
|
|
81
234
|
const timeoutMs = opts?.timeoutMs ?? DEFAULT_RUNNER_TIMEOUT_MS;
|
|
82
235
|
return new Promise((resolve, reject) => {
|
|
83
236
|
const model = opts?.model ?? process.env.AI_SDLC_MODEL ?? DEFAULT_MODEL;
|
|
84
|
-
const
|
|
237
|
+
const claudeArgs = [
|
|
238
|
+
'-p',
|
|
239
|
+
'--output-format',
|
|
240
|
+
'stream-json',
|
|
241
|
+
'--verbose',
|
|
242
|
+
'--model',
|
|
243
|
+
model,
|
|
244
|
+
'--allowedTools',
|
|
245
|
+
tools,
|
|
246
|
+
];
|
|
247
|
+
// When running inside an OpenShell sandbox, prefix with sandbox connect.
|
|
248
|
+
const useOpenShell = opts?.sandboxId && process.env.AI_SDLC_SANDBOX_PROVIDER === 'openshell';
|
|
249
|
+
let cmd;
|
|
250
|
+
let args;
|
|
251
|
+
if (useOpenShell) {
|
|
252
|
+
cmd = 'openshell';
|
|
253
|
+
args = ['sandbox', 'connect', opts.sandboxId, '--', 'claude', ...claudeArgs];
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
cmd = 'claude';
|
|
257
|
+
args = claudeArgs;
|
|
258
|
+
}
|
|
259
|
+
const startTime = Date.now();
|
|
260
|
+
const logPrefix = `[ai-sdlc:runner]`;
|
|
261
|
+
process.stderr.write(`${logPrefix} spawning: ${cmd} ${args.join(' ')}\n`);
|
|
262
|
+
process.stderr.write(`${logPrefix} workDir: ${workDir}\n`);
|
|
263
|
+
process.stderr.write(`${logPrefix} timeout: ${timeoutMs}ms\n`);
|
|
264
|
+
const child = spawn(cmd, args, {
|
|
85
265
|
cwd: workDir,
|
|
86
266
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
87
267
|
env: { ...process.env },
|
|
88
268
|
timeout: timeoutMs,
|
|
89
269
|
});
|
|
90
|
-
|
|
270
|
+
process.stderr.write(`${logPrefix} pid: ${child.pid}\n`);
|
|
271
|
+
let lastActivity = Date.now();
|
|
272
|
+
let resultText;
|
|
273
|
+
let resultCost;
|
|
274
|
+
let resultTokenUsage;
|
|
275
|
+
// Buffer for incomplete NDJSON lines
|
|
276
|
+
let lineBuffer = '';
|
|
277
|
+
const onProgress = opts?.onProgress;
|
|
278
|
+
child.stdout.on('data', (data) => {
|
|
279
|
+
lastActivity = Date.now();
|
|
280
|
+
// Parse NDJSON lines from stream-json output
|
|
281
|
+
lineBuffer += data.toString('utf-8');
|
|
282
|
+
const lines = lineBuffer.split('\n');
|
|
283
|
+
// Keep the last (possibly incomplete) line in the buffer
|
|
284
|
+
lineBuffer = lines.pop() ?? '';
|
|
285
|
+
for (const line of lines) {
|
|
286
|
+
const trimmed = line.trim();
|
|
287
|
+
if (!trimmed)
|
|
288
|
+
continue;
|
|
289
|
+
// Log formatted events to stderr for CI visibility
|
|
290
|
+
const formatted = formatEventForLog(trimmed);
|
|
291
|
+
if (formatted) {
|
|
292
|
+
process.stderr.write(`${logPrefix} ${formatted}\n`);
|
|
293
|
+
}
|
|
294
|
+
if (onProgress) {
|
|
295
|
+
const result = parseStreamEvent(trimmed, onProgress);
|
|
296
|
+
if (result) {
|
|
297
|
+
if (result.resultText !== undefined)
|
|
298
|
+
resultText = result.resultText;
|
|
299
|
+
if (result.costUsd !== undefined)
|
|
300
|
+
resultCost = result.costUsd;
|
|
301
|
+
if (result.tokenUsage)
|
|
302
|
+
resultTokenUsage = result.tokenUsage;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
// Still parse result event even without progress callback
|
|
307
|
+
try {
|
|
308
|
+
const parsed = JSON.parse(trimmed);
|
|
309
|
+
if (parsed.type === 'result') {
|
|
310
|
+
resultText = parsed.result;
|
|
311
|
+
resultCost = parsed.total_cost_usd;
|
|
312
|
+
const modelUsage = parsed.modelUsage;
|
|
313
|
+
if (modelUsage) {
|
|
314
|
+
const firstModel = Object.keys(modelUsage)[0];
|
|
315
|
+
if (firstModel) {
|
|
316
|
+
const usage = modelUsage[firstModel];
|
|
317
|
+
resultTokenUsage = {
|
|
318
|
+
inputTokens: usage.inputTokens ?? 0,
|
|
319
|
+
outputTokens: usage.outputTokens ?? 0,
|
|
320
|
+
cacheReadTokens: usage.cacheReadInputTokens ?? undefined,
|
|
321
|
+
model: firstModel,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
// Not JSON — ignore
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
});
|
|
91
333
|
const errChunks = [];
|
|
92
|
-
child.
|
|
93
|
-
|
|
334
|
+
child.stderr.on('data', (data) => {
|
|
335
|
+
errChunks.push(data);
|
|
336
|
+
lastActivity = Date.now();
|
|
337
|
+
process.stderr.write(data);
|
|
338
|
+
});
|
|
339
|
+
// Heartbeat: log progress every 30s so CI knows the process is alive
|
|
340
|
+
const heartbeat = setInterval(() => {
|
|
341
|
+
const elapsed = Math.round((Date.now() - startTime) / 1000);
|
|
342
|
+
const idle = Math.round((Date.now() - lastActivity) / 1000);
|
|
343
|
+
process.stderr.write(`${logPrefix} heartbeat: ${elapsed}s elapsed, ${idle}s idle\n`);
|
|
344
|
+
}, 30_000);
|
|
94
345
|
child.on('close', (code) => {
|
|
95
|
-
|
|
346
|
+
clearInterval(heartbeat);
|
|
347
|
+
const elapsed = Math.round((Date.now() - startTime) / 1000);
|
|
96
348
|
const stderr = Buffer.concat(errChunks).toString('utf-8');
|
|
349
|
+
process.stderr.write(`${logPrefix} exited: code=${code} elapsed=${elapsed}s\n`);
|
|
97
350
|
if (code === 0) {
|
|
98
|
-
resolve({
|
|
351
|
+
resolve({
|
|
352
|
+
stdout: resultText ?? '',
|
|
353
|
+
stderr,
|
|
354
|
+
model: resultTokenUsage?.model ?? model,
|
|
355
|
+
costUsd: resultCost,
|
|
356
|
+
});
|
|
99
357
|
}
|
|
100
358
|
else {
|
|
101
|
-
reject(new Error(`claude exited with code ${code}: ${stderr ||
|
|
359
|
+
reject(new Error(`claude exited with code ${code}: ${stderr || resultText || ''}`));
|
|
102
360
|
}
|
|
103
361
|
});
|
|
104
|
-
child.on('error',
|
|
362
|
+
child.on('error', (err) => {
|
|
363
|
+
clearInterval(heartbeat);
|
|
364
|
+
process.stderr.write(`${logPrefix} spawn error: ${err.message}\n`);
|
|
365
|
+
reject(err);
|
|
366
|
+
});
|
|
105
367
|
// Send prompt via stdin and close it
|
|
106
368
|
child.stdin.write(prompt);
|
|
107
369
|
child.stdin.end();
|
|
@@ -137,29 +399,76 @@ export function parseTokenUsage(stderr, model) {
|
|
|
137
399
|
}
|
|
138
400
|
return undefined;
|
|
139
401
|
}
|
|
402
|
+
/**
|
|
403
|
+
* Run lint --fix and format commands (best-effort) so pre-commit hooks pass.
|
|
404
|
+
*/
|
|
405
|
+
async function runAutoFix(workDir, lintCmd, fmtCmd) {
|
|
406
|
+
if (fmtCmd) {
|
|
407
|
+
try {
|
|
408
|
+
const [bin, ...args] = fmtCmd.split(' ');
|
|
409
|
+
await execFileAsync(bin, args, { cwd: workDir });
|
|
410
|
+
}
|
|
411
|
+
catch {
|
|
412
|
+
// Format failures are non-fatal — the commit hook will catch remaining issues
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
if (lintCmd) {
|
|
416
|
+
try {
|
|
417
|
+
const [bin, ...args] = lintCmd.split(' ');
|
|
418
|
+
await execFileAsync(bin, args, { cwd: workDir });
|
|
419
|
+
}
|
|
420
|
+
catch {
|
|
421
|
+
// Lint --fix failures are non-fatal
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
140
425
|
export class ClaudeCodeRunner {
|
|
141
426
|
async run(ctx) {
|
|
142
427
|
const prompt = buildPrompt(ctx);
|
|
143
428
|
try {
|
|
144
|
-
// Invoke Claude Code CLI
|
|
429
|
+
// Invoke Claude Code CLI with stream-json for real-time progress
|
|
145
430
|
const result = await runClaude(prompt, ctx.workDir, {
|
|
146
431
|
allowedTools: ctx.allowedTools,
|
|
147
432
|
timeoutMs: ctx.timeoutMs,
|
|
148
433
|
model: ctx.model,
|
|
434
|
+
sandboxId: ctx.sandboxId,
|
|
435
|
+
onProgress: ctx.onProgress,
|
|
149
436
|
});
|
|
150
|
-
//
|
|
437
|
+
// Token usage from stream-json result event (falls back to stderr parsing)
|
|
151
438
|
const tokenUsage = parseTokenUsage(result.stderr, result.model);
|
|
152
|
-
// Collect changed files
|
|
439
|
+
// Collect changed files — check both uncommitted and already-committed changes
|
|
440
|
+
// The agent may have committed on its own (Claude Code can run git commit)
|
|
153
441
|
const diffOutput = await gitExec(ctx.workDir, ['diff', '--name-only']);
|
|
154
442
|
const untrackedOutput = await gitExec(ctx.workDir, [
|
|
155
443
|
'ls-files',
|
|
156
444
|
'--others',
|
|
157
445
|
'--exclude-standard',
|
|
158
446
|
]);
|
|
159
|
-
const
|
|
447
|
+
const uncommittedFiles = [
|
|
160
448
|
...diffOutput.split('\n').filter(Boolean),
|
|
161
449
|
...untrackedOutput.split('\n').filter(Boolean),
|
|
162
450
|
];
|
|
451
|
+
// Check if agent already committed (and possibly pushed) —
|
|
452
|
+
// compare against the merge base with the target branch (main)
|
|
453
|
+
let committedFiles = [];
|
|
454
|
+
let agentAlreadyCommitted = false;
|
|
455
|
+
try {
|
|
456
|
+
// Find the merge base with main to see all new commits on this branch
|
|
457
|
+
const mergeBase = (await gitExec(ctx.workDir, ['merge-base', 'HEAD', 'origin/main'])).trim();
|
|
458
|
+
if (mergeBase) {
|
|
459
|
+
const commitDiff = await gitExec(ctx.workDir, [
|
|
460
|
+
'diff',
|
|
461
|
+
'--name-only',
|
|
462
|
+
`${mergeBase}..HEAD`,
|
|
463
|
+
]);
|
|
464
|
+
committedFiles = commitDiff.split('\n').filter(Boolean);
|
|
465
|
+
agentAlreadyCommitted = committedFiles.length > 0 && uncommittedFiles.length === 0;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
catch {
|
|
469
|
+
// merge-base may fail if main doesn't exist locally — that's fine
|
|
470
|
+
}
|
|
471
|
+
const filesChanged = agentAlreadyCommitted ? committedFiles : uncommittedFiles;
|
|
163
472
|
if (filesChanged.length === 0) {
|
|
164
473
|
return {
|
|
165
474
|
success: false,
|
|
@@ -169,14 +478,37 @@ export class ClaudeCodeRunner {
|
|
|
169
478
|
tokenUsage,
|
|
170
479
|
};
|
|
171
480
|
}
|
|
172
|
-
//
|
|
481
|
+
// If the agent already committed, skip our commit step
|
|
482
|
+
if (agentAlreadyCommitted) {
|
|
483
|
+
return {
|
|
484
|
+
success: true,
|
|
485
|
+
filesChanged,
|
|
486
|
+
summary: result.stdout.slice(0, 2000),
|
|
487
|
+
tokenUsage,
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
// Stage, lint/format, and commit
|
|
491
|
+
await gitExec(ctx.workDir, ['add', '-A']);
|
|
492
|
+
// Run lint and format before committing to avoid pre-commit hook failures
|
|
493
|
+
const lintCmd = ctx.lintCommand ?? DEFAULT_LINT_COMMAND;
|
|
494
|
+
const fmtCmd = ctx.formatCommand ?? DEFAULT_FORMAT_COMMAND;
|
|
495
|
+
await runAutoFix(ctx.workDir, lintCmd, fmtCmd);
|
|
496
|
+
// Re-stage after auto-fix may have modified files
|
|
173
497
|
await gitExec(ctx.workDir, ['add', '-A']);
|
|
174
498
|
const tmpl = ctx.commitMessageTemplate ?? DEFAULT_COMMIT_MESSAGE_TEMPLATE;
|
|
175
499
|
const coAuthor = ctx.commitCoAuthor ?? DEFAULT_COMMIT_CO_AUTHOR;
|
|
176
500
|
const commitMsg = tmpl
|
|
177
|
-
.replace(/\{issueNumber\}/g,
|
|
501
|
+
.replace(/\{issueNumber\}/g, ctx.issueId)
|
|
178
502
|
.replace(/\{issueTitle\}/g, ctx.issueTitle);
|
|
179
|
-
|
|
503
|
+
try {
|
|
504
|
+
await gitExec(ctx.workDir, ['commit', '-m', `${commitMsg}\n\nCo-Authored-By: ${coAuthor}`]);
|
|
505
|
+
}
|
|
506
|
+
catch {
|
|
507
|
+
// Pre-commit hook may have auto-fixed files — re-stage and retry once
|
|
508
|
+
await runAutoFix(ctx.workDir, lintCmd, fmtCmd);
|
|
509
|
+
await gitExec(ctx.workDir, ['add', '-A']);
|
|
510
|
+
await gitExec(ctx.workDir, ['commit', '-m', `${commitMsg}\n\nCo-Authored-By: ${coAuthor}`]);
|
|
511
|
+
}
|
|
180
512
|
return {
|
|
181
513
|
success: true,
|
|
182
514
|
filesChanged,
|
package/dist/runners/codex.js
CHANGED
|
@@ -73,12 +73,23 @@ export class CodexRunner {
|
|
|
73
73
|
const timeoutMs = ctx.timeoutMs ?? DEFAULT_RUNNER_TIMEOUT_MS;
|
|
74
74
|
const model = DEFAULT_CODEX_MODEL ?? 'codex-default';
|
|
75
75
|
try {
|
|
76
|
-
const
|
|
76
|
+
const codexArgs = ['exec', '-', '--full-auto', '--json'];
|
|
77
77
|
if (DEFAULT_CODEX_MODEL) {
|
|
78
|
-
|
|
78
|
+
codexArgs.push('-m', DEFAULT_CODEX_MODEL);
|
|
79
|
+
}
|
|
80
|
+
// When running inside an OpenShell sandbox, prefix with sandbox connect
|
|
81
|
+
let cmd;
|
|
82
|
+
let args;
|
|
83
|
+
if (ctx.sandboxId) {
|
|
84
|
+
cmd = 'openshell';
|
|
85
|
+
args = ['sandbox', 'connect', ctx.sandboxId, '--', 'codex', ...codexArgs];
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
cmd = 'codex';
|
|
89
|
+
args = codexArgs;
|
|
79
90
|
}
|
|
80
91
|
const { stdout, stderr } = await new Promise((resolve, reject) => {
|
|
81
|
-
const child = spawn(
|
|
92
|
+
const child = spawn(cmd, args, {
|
|
82
93
|
cwd: ctx.workDir,
|
|
83
94
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
84
95
|
env: { ...process.env },
|
|
@@ -129,7 +140,7 @@ export class CodexRunner {
|
|
|
129
140
|
const tmpl = ctx.commitMessageTemplate ?? DEFAULT_COMMIT_MESSAGE_TEMPLATE;
|
|
130
141
|
const coAuthor = ctx.commitCoAuthor ?? DEFAULT_COMMIT_CO_AUTHOR;
|
|
131
142
|
const commitMsg = tmpl
|
|
132
|
-
.replace(/\{issueNumber\}/g,
|
|
143
|
+
.replace(/\{issueNumber\}/g, ctx.issueId)
|
|
133
144
|
.replace(/\{issueTitle\}/g, ctx.issueTitle);
|
|
134
145
|
await gitExec(ctx.workDir, ['commit', '-m', `${commitMsg}\n\nCo-Authored-By: ${coAuthor}`]);
|
|
135
146
|
return {
|
package/dist/runners/copilot.js
CHANGED
|
@@ -42,12 +42,23 @@ export class CopilotRunner {
|
|
|
42
42
|
const timeoutMs = ctx.timeoutMs ?? DEFAULT_RUNNER_TIMEOUT_MS;
|
|
43
43
|
const model = DEFAULT_COPILOT_MODEL ?? 'copilot-default';
|
|
44
44
|
try {
|
|
45
|
-
const
|
|
45
|
+
const copilotArgs = ['-p', prompt, '--yolo'];
|
|
46
46
|
if (DEFAULT_COPILOT_MODEL) {
|
|
47
|
-
|
|
47
|
+
copilotArgs.push('--model', DEFAULT_COPILOT_MODEL);
|
|
48
|
+
}
|
|
49
|
+
// When running inside an OpenShell sandbox, prefix with sandbox connect
|
|
50
|
+
let cmd;
|
|
51
|
+
let args;
|
|
52
|
+
if (ctx.sandboxId) {
|
|
53
|
+
cmd = 'openshell';
|
|
54
|
+
args = ['sandbox', 'connect', ctx.sandboxId, '--', 'copilot', ...copilotArgs];
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
cmd = 'copilot';
|
|
58
|
+
args = copilotArgs;
|
|
48
59
|
}
|
|
49
60
|
const { stdout, stderr } = await new Promise((resolve, reject) => {
|
|
50
|
-
const child = spawn(
|
|
61
|
+
const child = spawn(cmd, args, {
|
|
51
62
|
cwd: ctx.workDir,
|
|
52
63
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
53
64
|
env: { ...process.env },
|
|
@@ -95,7 +106,7 @@ export class CopilotRunner {
|
|
|
95
106
|
const tmpl = ctx.commitMessageTemplate ?? DEFAULT_COMMIT_MESSAGE_TEMPLATE;
|
|
96
107
|
const coAuthor = ctx.commitCoAuthor ?? DEFAULT_COMMIT_CO_AUTHOR;
|
|
97
108
|
const commitMsg = tmpl
|
|
98
|
-
.replace(/\{issueNumber\}/g,
|
|
109
|
+
.replace(/\{issueNumber\}/g, ctx.issueId)
|
|
99
110
|
.replace(/\{issueTitle\}/g, ctx.issueTitle);
|
|
100
111
|
await gitExec(ctx.workDir, ['commit', '-m', `${commitMsg}\n\nCo-Authored-By: ${coAuthor}`]);
|
|
101
112
|
return {
|
package/dist/runners/cursor.js
CHANGED
|
@@ -62,12 +62,23 @@ export class CursorRunner {
|
|
|
62
62
|
const timeoutMs = ctx.timeoutMs ?? DEFAULT_RUNNER_TIMEOUT_MS;
|
|
63
63
|
const model = DEFAULT_CURSOR_MODEL ?? 'cursor-default';
|
|
64
64
|
try {
|
|
65
|
-
const
|
|
65
|
+
const cursorArgs = ['--print', prompt, '--force', '--output-format=stream-json'];
|
|
66
66
|
if (DEFAULT_CURSOR_MODEL) {
|
|
67
|
-
|
|
67
|
+
cursorArgs.push('-m', DEFAULT_CURSOR_MODEL);
|
|
68
|
+
}
|
|
69
|
+
// When running inside an OpenShell sandbox, prefix with sandbox connect
|
|
70
|
+
let cmd;
|
|
71
|
+
let args;
|
|
72
|
+
if (ctx.sandboxId) {
|
|
73
|
+
cmd = 'openshell';
|
|
74
|
+
args = ['sandbox', 'connect', ctx.sandboxId, '--', 'cursor-agent', ...cursorArgs];
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
cmd = 'cursor-agent';
|
|
78
|
+
args = cursorArgs;
|
|
68
79
|
}
|
|
69
80
|
const { stdout, stderr } = await new Promise((resolve, reject) => {
|
|
70
|
-
const child = spawn(
|
|
81
|
+
const child = spawn(cmd, args, {
|
|
71
82
|
cwd: ctx.workDir,
|
|
72
83
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
73
84
|
env: { ...process.env },
|
|
@@ -116,7 +127,7 @@ export class CursorRunner {
|
|
|
116
127
|
const tmpl = ctx.commitMessageTemplate ?? DEFAULT_COMMIT_MESSAGE_TEMPLATE;
|
|
117
128
|
const coAuthor = ctx.commitCoAuthor ?? DEFAULT_COMMIT_CO_AUTHOR;
|
|
118
129
|
const commitMsg = tmpl
|
|
119
|
-
.replace(/\{issueNumber\}/g,
|
|
130
|
+
.replace(/\{issueNumber\}/g, ctx.issueId)
|
|
120
131
|
.replace(/\{issueTitle\}/g, ctx.issueTitle);
|
|
121
132
|
await gitExec(ctx.workDir, ['commit', '-m', `${commitMsg}\n\nCo-Authored-By: ${coAuthor}`]);
|
|
122
133
|
return {
|
package/dist/runners/index.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
export type { AgentRunner, AgentContext, AgentResult, TokenUsage } from './types.js';
|
|
1
|
+
export type { AgentRunner, AgentContext, AgentResult, AgentProgressEvent, TokenUsage, } from './types.js';
|
|
2
2
|
export { ClaudeCodeRunner, GitHubActionsRunner } from './claude-code.js';
|
|
3
3
|
export { GenericLLMRunner, type GenericLLMConfig, type ChatCompletionResponse, } from './generic-llm.js';
|
|
4
4
|
export { CopilotRunner } from './copilot.js';
|
|
5
5
|
export { CursorRunner } from './cursor.js';
|
|
6
6
|
export { CodexRunner } from './codex.js';
|
|
7
7
|
export { RunnerRegistry, createRunnerRegistry, type RegisteredRunner } from './runner-registry.js';
|
|
8
|
+
export { SecurityTriageRunner, type SecurityTriageConfig, type TriageVerdict, TRIAGE_SYSTEM_PROMPT, } from './security-triage.js';
|
|
9
|
+
export { ReviewAgentRunner, REVIEW_PROMPTS, type ReviewAgentConfig, type ReviewType, type ReviewFinding, type ReviewVerdict, } from './review-agent.js';
|
|
8
10
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/runners/index.js
CHANGED
|
@@ -4,4 +4,6 @@ export { CopilotRunner } from './copilot.js';
|
|
|
4
4
|
export { CursorRunner } from './cursor.js';
|
|
5
5
|
export { CodexRunner } from './codex.js';
|
|
6
6
|
export { RunnerRegistry, createRunnerRegistry } from './runner-registry.js';
|
|
7
|
+
export { SecurityTriageRunner, TRIAGE_SYSTEM_PROMPT, } from './security-triage.js';
|
|
8
|
+
export { ReviewAgentRunner, REVIEW_PROMPTS, } from './review-agent.js';
|
|
7
9
|
//# sourceMappingURL=index.js.map
|