@ai-sdlc/orchestrator 0.5.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.
Files changed (47) hide show
  1. package/dist/action-enforcement.d.ts +26 -0
  2. package/dist/action-enforcement.js +70 -0
  3. package/dist/admission-score.d.ts +58 -0
  4. package/dist/admission-score.js +164 -0
  5. package/dist/cycle-utils.d.ts +51 -0
  6. package/dist/cycle-utils.js +77 -0
  7. package/dist/defaults.d.ts +5 -0
  8. package/dist/defaults.js +5 -0
  9. package/dist/execute.js +121 -26
  10. package/dist/fix-ci.js +32 -2
  11. package/dist/fix-review.d.ts +66 -0
  12. package/dist/fix-review.js +441 -0
  13. package/dist/index.d.ts +10 -2
  14. package/dist/index.js +13 -1
  15. package/dist/pipeline-cycle-detector.d.ts +70 -0
  16. package/dist/pipeline-cycle-detector.js +111 -0
  17. package/dist/priority.d.ts +2 -76
  18. package/dist/review.d.ts +31 -0
  19. package/dist/review.js +74 -0
  20. package/dist/runners/claude-code.js +314 -32
  21. package/dist/runners/index.d.ts +2 -1
  22. package/dist/runners/index.js +1 -0
  23. package/dist/runners/review-agent.d.ts +47 -0
  24. package/dist/runners/review-agent.js +220 -0
  25. package/dist/runners/security-triage.js +4 -0
  26. package/dist/runners/types.d.ts +19 -0
  27. package/dist/state/index.d.ts +1 -1
  28. package/dist/state/schema.d.ts +2 -1
  29. package/dist/state/schema.js +54 -1
  30. package/dist/state/store.d.ts +17 -1
  31. package/dist/state/store.js +122 -0
  32. package/dist/state/types.d.ts +35 -0
  33. package/dist/workflow-patterns/artifact-writer.d.ts +16 -0
  34. package/dist/workflow-patterns/artifact-writer.js +34 -0
  35. package/dist/workflow-patterns/classifiers.d.ts +10 -0
  36. package/dist/workflow-patterns/classifiers.js +72 -0
  37. package/dist/workflow-patterns/detector.d.ts +27 -0
  38. package/dist/workflow-patterns/detector.js +186 -0
  39. package/dist/workflow-patterns/index.d.ts +8 -0
  40. package/dist/workflow-patterns/index.js +7 -0
  41. package/dist/workflow-patterns/proposal-generator.d.ts +15 -0
  42. package/dist/workflow-patterns/proposal-generator.js +183 -0
  43. package/dist/workflow-patterns/telemetry-ingest.d.ts +27 -0
  44. package/dist/workflow-patterns/telemetry-ingest.js +103 -0
  45. package/dist/workflow-patterns/types.d.ts +61 -0
  46. package/dist/workflow-patterns/types.js +11 -0
  47. package/package.json +2 -2
@@ -7,9 +7,31 @@ 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
36
  `You are fixing issue ${/^\d+$/.test(ctx.issueId) ? '#' : ''}${ctx.issueId}: ${ctx.issueTitle}`,
15
37
  '',
@@ -23,29 +45,25 @@ 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
- if (lintCmd && fmtCmd) {
27
- lines.push(`${++step}. After making ANY code changes, always run \`${lintCmd}\` and \`${fmtCmd}\` to catch issues before committing.`);
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
- if (lintCmd && fmtCmd) {
41
- lines.push(`${++step}. After making code changes, run \`${lintCmd}\` and \`${fmtCmd}\` to ensure CI will pass.`);
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'}`);
@@ -76,16 +94,161 @@ 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 claudeArgs = ['-p', '--model', model, '--allowedTools', tools];
85
- // When running inside an OpenShell sandbox, prefix with sandbox connect
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';
86
249
  let cmd;
87
250
  let args;
88
- if (opts?.sandboxId) {
251
+ if (useOpenShell) {
89
252
  cmd = 'openshell';
90
253
  args = ['sandbox', 'connect', opts.sandboxId, '--', 'claude', ...claudeArgs];
91
254
  }
@@ -93,27 +256,114 @@ function runClaude(prompt, workDir, opts) {
93
256
  cmd = 'claude';
94
257
  args = claudeArgs;
95
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`);
96
264
  const child = spawn(cmd, args, {
97
265
  cwd: workDir,
98
266
  stdio: ['pipe', 'pipe', 'pipe'],
99
267
  env: { ...process.env },
100
268
  timeout: timeoutMs,
101
269
  });
102
- const chunks = [];
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
+ });
103
333
  const errChunks = [];
104
- child.stdout.on('data', (data) => chunks.push(data));
105
- child.stderr.on('data', (data) => errChunks.push(data));
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);
106
345
  child.on('close', (code) => {
107
- const stdout = Buffer.concat(chunks).toString('utf-8');
346
+ clearInterval(heartbeat);
347
+ const elapsed = Math.round((Date.now() - startTime) / 1000);
108
348
  const stderr = Buffer.concat(errChunks).toString('utf-8');
349
+ process.stderr.write(`${logPrefix} exited: code=${code} elapsed=${elapsed}s\n`);
109
350
  if (code === 0) {
110
- resolve({ stdout, stderr, model });
351
+ resolve({
352
+ stdout: resultText ?? '',
353
+ stderr,
354
+ model: resultTokenUsage?.model ?? model,
355
+ costUsd: resultCost,
356
+ });
111
357
  }
112
358
  else {
113
- reject(new Error(`claude exited with code ${code}: ${stderr || stdout}`));
359
+ reject(new Error(`claude exited with code ${code}: ${stderr || resultText || ''}`));
114
360
  }
115
361
  });
116
- child.on('error', reject);
362
+ child.on('error', (err) => {
363
+ clearInterval(heartbeat);
364
+ process.stderr.write(`${logPrefix} spawn error: ${err.message}\n`);
365
+ reject(err);
366
+ });
117
367
  // Send prompt via stdin and close it
118
368
  child.stdin.write(prompt);
119
369
  child.stdin.end();
@@ -176,26 +426,49 @@ export class ClaudeCodeRunner {
176
426
  async run(ctx) {
177
427
  const prompt = buildPrompt(ctx);
178
428
  try {
179
- // Invoke Claude Code CLI in print mode, sending prompt via stdin
429
+ // Invoke Claude Code CLI with stream-json for real-time progress
180
430
  const result = await runClaude(prompt, ctx.workDir, {
181
431
  allowedTools: ctx.allowedTools,
182
432
  timeoutMs: ctx.timeoutMs,
183
433
  model: ctx.model,
184
434
  sandboxId: ctx.sandboxId,
435
+ onProgress: ctx.onProgress,
185
436
  });
186
- // Parse token usage from stderr
437
+ // Token usage from stream-json result event (falls back to stderr parsing)
187
438
  const tokenUsage = parseTokenUsage(result.stderr, result.model);
188
- // 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)
189
441
  const diffOutput = await gitExec(ctx.workDir, ['diff', '--name-only']);
190
442
  const untrackedOutput = await gitExec(ctx.workDir, [
191
443
  'ls-files',
192
444
  '--others',
193
445
  '--exclude-standard',
194
446
  ]);
195
- const filesChanged = [
447
+ const uncommittedFiles = [
196
448
  ...diffOutput.split('\n').filter(Boolean),
197
449
  ...untrackedOutput.split('\n').filter(Boolean),
198
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;
199
472
  if (filesChanged.length === 0) {
200
473
  return {
201
474
  success: false,
@@ -205,6 +478,15 @@ export class ClaudeCodeRunner {
205
478
  tokenUsage,
206
479
  };
207
480
  }
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
+ }
208
490
  // Stage, lint/format, and commit
209
491
  await gitExec(ctx.workDir, ['add', '-A']);
210
492
  // Run lint and format before committing to avoid pre-commit hook failures
@@ -1,4 +1,4 @@
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';
@@ -6,4 +6,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
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';
9
10
  //# sourceMappingURL=index.d.ts.map
@@ -5,4 +5,5 @@ export { CursorRunner } from './cursor.js';
5
5
  export { CodexRunner } from './codex.js';
6
6
  export { RunnerRegistry, createRunnerRegistry } from './runner-registry.js';
7
7
  export { SecurityTriageRunner, TRIAGE_SYSTEM_PROMPT, } from './security-triage.js';
8
+ export { ReviewAgentRunner, REVIEW_PROMPTS, } from './review-agent.js';
8
9
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,47 @@
1
+ /**
2
+ * PR Review agent runner — analyzes pull request diffs for testing coverage,
3
+ * code quality, and security issues. Read-only: never modifies files.
4
+ *
5
+ * Uses the Anthropic Messages API directly (not Claude Code CLI)
6
+ * to produce a structured review verdict. Follows the SecurityTriageRunner
7
+ * pattern exactly.
8
+ */
9
+ import type { AgentRunner, AgentContext, AgentResult } from './types.js';
10
+ export type ReviewType = 'testing' | 'critic' | 'security';
11
+ export interface ReviewFinding {
12
+ severity: 'critical' | 'major' | 'minor' | 'suggestion';
13
+ file?: string;
14
+ line?: number;
15
+ message: string;
16
+ }
17
+ export interface ReviewVerdict {
18
+ type: ReviewType;
19
+ approved: boolean;
20
+ findings: ReviewFinding[];
21
+ summary: string;
22
+ }
23
+ export interface ReviewAgentConfig {
24
+ /** Anthropic API URL. Defaults to https://api.anthropic.com/v1/messages */
25
+ apiUrl?: string;
26
+ /** Anthropic API key. Defaults to ANTHROPIC_API_KEY env var. */
27
+ apiKey?: string;
28
+ /** Model to use. Defaults to claude-sonnet-4-5. */
29
+ model?: string;
30
+ /** Request timeout in ms. Defaults to 120_000. */
31
+ timeoutMs?: number;
32
+ /** Which review perspective to use. */
33
+ reviewType: ReviewType;
34
+ /** Project-specific review policy to prepend to the system prompt (calibration context). */
35
+ reviewPolicy?: string;
36
+ }
37
+ declare const REVIEW_PROMPTS: Record<ReviewType, string>;
38
+ export declare class ReviewAgentRunner implements AgentRunner {
39
+ private config;
40
+ constructor(config: ReviewAgentConfig);
41
+ get reviewType(): ReviewType;
42
+ run(ctx: AgentContext): Promise<AgentResult>;
43
+ private callAPI;
44
+ parseVerdict(text: string): ReviewVerdict;
45
+ }
46
+ export { REVIEW_PROMPTS };
47
+ //# sourceMappingURL=review-agent.d.ts.map