agentweaver 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -0
- package/dist/executors/claude-executor.js +36 -0
- package/dist/executors/claude-summary-executor.js +31 -0
- package/dist/executors/codex-docker-executor.js +27 -0
- package/dist/executors/codex-local-executor.js +25 -0
- package/dist/executors/command-check-executor.js +14 -0
- package/dist/executors/configs/claude-config.js +11 -0
- package/dist/executors/configs/claude-summary-config.js +8 -0
- package/dist/executors/configs/codex-docker-config.js +10 -0
- package/dist/executors/configs/codex-local-config.js +8 -0
- package/dist/executors/configs/jira-fetch-config.js +4 -0
- package/dist/executors/configs/process-config.js +3 -0
- package/dist/executors/configs/verify-build-config.js +7 -0
- package/dist/executors/jira-fetch-executor.js +11 -0
- package/dist/executors/process-executor.js +21 -0
- package/dist/executors/types.js +1 -0
- package/dist/executors/verify-build-executor.js +22 -0
- package/dist/index.js +270 -450
- package/dist/interactive-ui.js +109 -12
- package/dist/pipeline/build-failure-summary.js +6 -0
- package/dist/pipeline/checks.js +15 -0
- package/dist/pipeline/context.js +17 -0
- package/dist/pipeline/flow-runner.js +13 -0
- package/dist/pipeline/flow-types.js +1 -0
- package/dist/pipeline/flows/implement-flow.js +48 -0
- package/dist/pipeline/flows/plan-flow.js +42 -0
- package/dist/pipeline/flows/preflight-flow.js +59 -0
- package/dist/pipeline/flows/review-fix-flow.js +63 -0
- package/dist/pipeline/flows/review-flow.js +120 -0
- package/dist/pipeline/flows/test-fix-flow.js +13 -0
- package/dist/pipeline/flows/test-flow.js +32 -0
- package/dist/pipeline/node-runner.js +14 -0
- package/dist/pipeline/nodes/build-failure-summary-node.js +71 -0
- package/dist/pipeline/nodes/claude-summary-node.js +32 -0
- package/dist/pipeline/nodes/codex-docker-prompt-node.js +31 -0
- package/dist/pipeline/nodes/command-check-node.js +10 -0
- package/dist/pipeline/nodes/implement-codex-node.js +16 -0
- package/dist/pipeline/nodes/jira-fetch-node.js +25 -0
- package/dist/pipeline/nodes/plan-codex-node.js +32 -0
- package/dist/pipeline/nodes/review-claude-node.js +38 -0
- package/dist/pipeline/nodes/review-reply-codex-node.js +40 -0
- package/dist/pipeline/nodes/task-summary-node.js +36 -0
- package/dist/pipeline/nodes/verify-build-node.js +14 -0
- package/dist/pipeline/registry.js +25 -0
- package/dist/pipeline/types.js +10 -0
- package/dist/runtime/command-resolution.js +139 -0
- package/dist/runtime/docker-runtime.js +51 -0
- package/dist/runtime/process-runner.js +111 -0
- package/dist/tui.js +34 -0
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -1,17 +1,29 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { appendFile, readFile } from "node:fs/promises";
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import process from "node:process";
|
|
7
|
-
import { spawn, spawnSync } from "node:child_process";
|
|
8
7
|
import { fileURLToPath } from "node:url";
|
|
9
|
-
import {
|
|
8
|
+
import { REVIEW_FILE_RE, REVIEW_REPLY_FILE_RE, designFile, planArtifacts, planFile, requireArtifacts, taskSummaryFile, } from "./artifacts.js";
|
|
10
9
|
import { TaskRunnerError } from "./errors.js";
|
|
11
|
-
import { buildJiraApiUrl, buildJiraBrowseUrl, extractIssueKey,
|
|
12
|
-
import { AUTO_REVIEW_FIX_EXTRA_PROMPT, IMPLEMENT_PROMPT_TEMPLATE,
|
|
10
|
+
import { buildJiraApiUrl, buildJiraBrowseUrl, extractIssueKey, requireJiraTaskFile } from "./jira.js";
|
|
11
|
+
import { AUTO_REVIEW_FIX_EXTRA_PROMPT, IMPLEMENT_PROMPT_TEMPLATE, formatPrompt, formatTemplate, } from "./prompts.js";
|
|
12
|
+
import { summarizeBuildFailure as summarizeBuildFailureViaPipeline } from "./pipeline/build-failure-summary.js";
|
|
13
|
+
import { createPipelineContext } from "./pipeline/context.js";
|
|
14
|
+
import { runFlow } from "./pipeline/flow-runner.js";
|
|
15
|
+
import { implementFlowDefinition, runImplementFlow } from "./pipeline/flows/implement-flow.js";
|
|
16
|
+
import { planFlowDefinition, runPlanFlow } from "./pipeline/flows/plan-flow.js";
|
|
17
|
+
import { runPreflightFlow } from "./pipeline/flows/preflight-flow.js";
|
|
18
|
+
import { createReviewFixFlowDefinition, runReviewFixFlow } from "./pipeline/flows/review-fix-flow.js";
|
|
19
|
+
import { createReviewFlowDefinition, runReviewFlow } from "./pipeline/flows/review-flow.js";
|
|
20
|
+
import { runTestFixFlow } from "./pipeline/flows/test-fix-flow.js";
|
|
21
|
+
import { runTestFlow, testFlowDefinition } from "./pipeline/flows/test-flow.js";
|
|
22
|
+
import { resolveCmd, resolveDockerComposeCmd } from "./runtime/command-resolution.js";
|
|
23
|
+
import { defaultDockerComposeFile, dockerRuntimeEnv } from "./runtime/docker-runtime.js";
|
|
24
|
+
import { runCommand } from "./runtime/process-runner.js";
|
|
13
25
|
import { InteractiveUi } from "./interactive-ui.js";
|
|
14
|
-
import { bye,
|
|
26
|
+
import { bye, printError, printInfo, printPanel, printSummary } from "./tui.js";
|
|
15
27
|
const COMMANDS = [
|
|
16
28
|
"plan",
|
|
17
29
|
"implement",
|
|
@@ -26,12 +38,17 @@ const COMMANDS = [
|
|
|
26
38
|
];
|
|
27
39
|
const DEFAULT_CODEX_MODEL = "gpt-5.4";
|
|
28
40
|
const DEFAULT_CLAUDE_REVIEW_MODEL = "opus";
|
|
29
|
-
const DEFAULT_CLAUDE_SUMMARY_MODEL = "haiku";
|
|
30
41
|
const HISTORY_FILE = path.join(os.homedir(), ".codex", "memories", "agentweaver-history");
|
|
31
42
|
const AUTO_STATE_SCHEMA_VERSION = 1;
|
|
32
43
|
const AUTO_MAX_REVIEW_ITERATIONS = 3;
|
|
33
44
|
const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
34
45
|
const PACKAGE_ROOT = path.resolve(MODULE_DIR, "..");
|
|
46
|
+
const runtimeServices = {
|
|
47
|
+
resolveCmd,
|
|
48
|
+
resolveDockerComposeCmd,
|
|
49
|
+
dockerRuntimeEnv: () => dockerRuntimeEnv(PACKAGE_ROOT),
|
|
50
|
+
runCommand,
|
|
51
|
+
};
|
|
35
52
|
function usage() {
|
|
36
53
|
return `Usage:
|
|
37
54
|
agentweaver <jira-browse-url|jira-issue-key>
|
|
@@ -224,110 +241,6 @@ function loadEnvFile(envFilePath) {
|
|
|
224
241
|
process.env[key] = value;
|
|
225
242
|
}
|
|
226
243
|
}
|
|
227
|
-
function agentweaverHome() {
|
|
228
|
-
const configured = process.env.AGENTWEAVER_HOME?.trim();
|
|
229
|
-
if (configured) {
|
|
230
|
-
return path.resolve(configured);
|
|
231
|
-
}
|
|
232
|
-
return PACKAGE_ROOT;
|
|
233
|
-
}
|
|
234
|
-
function defaultDockerComposeFile() {
|
|
235
|
-
return path.join(agentweaverHome(), "docker-compose.yml");
|
|
236
|
-
}
|
|
237
|
-
function defaultCodexHomeDir() {
|
|
238
|
-
return path.join(agentweaverHome(), ".codex-home");
|
|
239
|
-
}
|
|
240
|
-
function ensureRuntimeBindPath(targetPath, isDir) {
|
|
241
|
-
mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
242
|
-
if (isDir) {
|
|
243
|
-
mkdirSync(targetPath, { recursive: true });
|
|
244
|
-
}
|
|
245
|
-
else if (!existsSync(targetPath)) {
|
|
246
|
-
writeFileSync(targetPath, "", "utf8");
|
|
247
|
-
}
|
|
248
|
-
return targetPath;
|
|
249
|
-
}
|
|
250
|
-
function defaultHostSshDir() {
|
|
251
|
-
const candidate = path.join(os.homedir(), ".ssh");
|
|
252
|
-
if (existsSync(candidate)) {
|
|
253
|
-
return candidate;
|
|
254
|
-
}
|
|
255
|
-
return ensureRuntimeBindPath(path.join(agentweaverHome(), ".runtime", "ssh"), true);
|
|
256
|
-
}
|
|
257
|
-
function defaultHostGitconfig() {
|
|
258
|
-
const candidate = path.join(os.homedir(), ".gitconfig");
|
|
259
|
-
if (existsSync(candidate)) {
|
|
260
|
-
return candidate;
|
|
261
|
-
}
|
|
262
|
-
return ensureRuntimeBindPath(path.join(agentweaverHome(), ".runtime", "gitconfig"), false);
|
|
263
|
-
}
|
|
264
|
-
function dockerRuntimeEnv() {
|
|
265
|
-
const env = { ...process.env };
|
|
266
|
-
env.AGENTWEAVER_HOME ??= agentweaverHome();
|
|
267
|
-
env.PROJECT_DIR ??= process.cwd();
|
|
268
|
-
env.CODEX_HOME_DIR ??= ensureRuntimeBindPath(defaultCodexHomeDir(), true);
|
|
269
|
-
env.HOST_SSH_DIR ??= defaultHostSshDir();
|
|
270
|
-
env.HOST_GITCONFIG ??= defaultHostGitconfig();
|
|
271
|
-
env.LOCAL_UID ??= typeof process.getuid === "function" ? String(process.getuid()) : "1000";
|
|
272
|
-
env.LOCAL_GID ??= typeof process.getgid === "function" ? String(process.getgid()) : "1000";
|
|
273
|
-
return env;
|
|
274
|
-
}
|
|
275
|
-
function commandExists(commandName) {
|
|
276
|
-
const result = spawnSync("bash", ["-lc", `command -v ${shellQuote(commandName)}`], { stdio: "ignore" });
|
|
277
|
-
return result.status === 0;
|
|
278
|
-
}
|
|
279
|
-
function shellQuote(value) {
|
|
280
|
-
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
281
|
-
}
|
|
282
|
-
function resolveCmd(commandName, envVarName) {
|
|
283
|
-
const configuredPath = process.env[envVarName];
|
|
284
|
-
if (configuredPath) {
|
|
285
|
-
accessSync(configuredPath, constants.X_OK);
|
|
286
|
-
return configuredPath;
|
|
287
|
-
}
|
|
288
|
-
const result = spawnSync("bash", ["-lc", `command -v ${shellQuote(commandName)}`], { encoding: "utf8" });
|
|
289
|
-
if (result.status === 0 && result.stdout.trim()) {
|
|
290
|
-
return result.stdout.trim().split(/\r?\n/)[0] ?? commandName;
|
|
291
|
-
}
|
|
292
|
-
throw new TaskRunnerError(`Missing required command: ${commandName}`);
|
|
293
|
-
}
|
|
294
|
-
function requireDockerCompose() {
|
|
295
|
-
if (!commandExists("docker")) {
|
|
296
|
-
throw new TaskRunnerError("Missing required command: docker");
|
|
297
|
-
}
|
|
298
|
-
const result = spawnSync("docker", ["compose", "version"], { stdio: "ignore" });
|
|
299
|
-
if (result.status !== 0) {
|
|
300
|
-
throw new TaskRunnerError("Missing required docker compose plugin");
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
function resolveDockerComposeCmd() {
|
|
304
|
-
const configured = process.env.DOCKER_COMPOSE_BIN?.trim() ?? "";
|
|
305
|
-
if (configured) {
|
|
306
|
-
const parts = splitArgs(configured);
|
|
307
|
-
if (parts.length === 0) {
|
|
308
|
-
throw new TaskRunnerError("DOCKER_COMPOSE_BIN is set but empty.");
|
|
309
|
-
}
|
|
310
|
-
const executable = parts[0] ?? "";
|
|
311
|
-
try {
|
|
312
|
-
if (path.isAbsolute(executable)) {
|
|
313
|
-
accessSync(executable, constants.X_OK);
|
|
314
|
-
return parts;
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
catch {
|
|
318
|
-
throw new TaskRunnerError(`Configured docker compose command is not executable: ${configured}`);
|
|
319
|
-
}
|
|
320
|
-
if (commandExists(executable)) {
|
|
321
|
-
return parts;
|
|
322
|
-
}
|
|
323
|
-
throw new TaskRunnerError(`Configured docker compose command is not executable: ${configured}`);
|
|
324
|
-
}
|
|
325
|
-
if (commandExists("docker-compose")) {
|
|
326
|
-
return ["docker-compose"];
|
|
327
|
-
}
|
|
328
|
-
requireDockerCompose();
|
|
329
|
-
return ["docker", "compose"];
|
|
330
|
-
}
|
|
331
244
|
function nextReviewIterationForTask(taskKey) {
|
|
332
245
|
let maxIndex = 0;
|
|
333
246
|
for (const entry of readdirSync(process.cwd(), { withFileTypes: true })) {
|
|
@@ -355,110 +268,6 @@ function latestReviewReplyIteration(taskKey) {
|
|
|
355
268
|
}
|
|
356
269
|
return maxIndex;
|
|
357
270
|
}
|
|
358
|
-
function formatCommand(argv, env) {
|
|
359
|
-
const envParts = Object.entries(env ?? {})
|
|
360
|
-
.filter(([key, value]) => value !== undefined && process.env[key] !== value)
|
|
361
|
-
.map(([key, value]) => `${key}=${shellQuote(value ?? "")}`);
|
|
362
|
-
const command = argv.map(shellQuote).join(" ");
|
|
363
|
-
return envParts.length > 0 ? `${envParts.join(" ")} ${command}` : command;
|
|
364
|
-
}
|
|
365
|
-
function formatDuration(ms) {
|
|
366
|
-
const totalSeconds = Math.max(0, Math.floor(ms / 1000));
|
|
367
|
-
const minutes = String(Math.floor(totalSeconds / 60)).padStart(2, "0");
|
|
368
|
-
const seconds = String(totalSeconds % 60).padStart(2, "0");
|
|
369
|
-
return `${minutes}:${seconds}`;
|
|
370
|
-
}
|
|
371
|
-
async function runCommand(argv, options = {}) {
|
|
372
|
-
const { env, dryRun = false, verbose = false, label, printFailureOutput = true } = options;
|
|
373
|
-
const outputAdapter = getOutputAdapter();
|
|
374
|
-
if (dryRun) {
|
|
375
|
-
outputAdapter.writeStdout(`${formatCommand(argv, env)}\n`);
|
|
376
|
-
return "";
|
|
377
|
-
}
|
|
378
|
-
if (verbose && outputAdapter.supportsPassthrough) {
|
|
379
|
-
await new Promise((resolve, reject) => {
|
|
380
|
-
const child = spawn(argv[0] ?? "", argv.slice(1), {
|
|
381
|
-
stdio: "inherit",
|
|
382
|
-
env,
|
|
383
|
-
});
|
|
384
|
-
child.on("exit", (code) => (code === 0 ? resolve() : reject(new Error(String(code ?? 1)))));
|
|
385
|
-
child.on("error", reject);
|
|
386
|
-
}).catch((error) => {
|
|
387
|
-
const code = Number.parseInt(error.message, 10);
|
|
388
|
-
throw Object.assign(new Error(`Command failed with exit code ${Number.isNaN(code) ? 1 : code}`), {
|
|
389
|
-
returnCode: Number.isNaN(code) ? 1 : code,
|
|
390
|
-
output: "",
|
|
391
|
-
});
|
|
392
|
-
});
|
|
393
|
-
return "";
|
|
394
|
-
}
|
|
395
|
-
const startedAt = Date.now();
|
|
396
|
-
const statusLabel = label ?? path.basename(argv[0] ?? argv.join(" "));
|
|
397
|
-
const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
398
|
-
let frameIndex = 0;
|
|
399
|
-
let output = "";
|
|
400
|
-
const child = spawn(argv[0] ?? "", argv.slice(1), {
|
|
401
|
-
env,
|
|
402
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
403
|
-
});
|
|
404
|
-
child.stdout?.on("data", (chunk) => {
|
|
405
|
-
const text = String(chunk);
|
|
406
|
-
output += text;
|
|
407
|
-
if (!outputAdapter.supportsTransientStatus || verbose) {
|
|
408
|
-
outputAdapter.writeStdout(text);
|
|
409
|
-
}
|
|
410
|
-
});
|
|
411
|
-
child.stderr?.on("data", (chunk) => {
|
|
412
|
-
const text = String(chunk);
|
|
413
|
-
output += text;
|
|
414
|
-
if (!outputAdapter.supportsTransientStatus || verbose) {
|
|
415
|
-
outputAdapter.writeStderr(text);
|
|
416
|
-
}
|
|
417
|
-
});
|
|
418
|
-
if (!outputAdapter.supportsTransientStatus) {
|
|
419
|
-
outputAdapter.writeStdout(`Running ${statusLabel}\n`);
|
|
420
|
-
}
|
|
421
|
-
const timer = outputAdapter.supportsTransientStatus
|
|
422
|
-
? setInterval(() => {
|
|
423
|
-
const elapsed = formatDuration(Date.now() - startedAt);
|
|
424
|
-
process.stdout.write(`\r${frames[frameIndex]} ${statusLabel} ${dim(elapsed)}`);
|
|
425
|
-
frameIndex = (frameIndex + 1) % frames.length;
|
|
426
|
-
}, 200)
|
|
427
|
-
: null;
|
|
428
|
-
try {
|
|
429
|
-
const exitCode = await new Promise((resolve, reject) => {
|
|
430
|
-
child.on("error", reject);
|
|
431
|
-
child.on("exit", (code) => resolve(code ?? 1));
|
|
432
|
-
});
|
|
433
|
-
if (timer) {
|
|
434
|
-
clearInterval(timer);
|
|
435
|
-
process.stdout.write(`\r${" ".repeat(80)}\r${formatDone(formatDuration(Date.now() - startedAt))}\n`);
|
|
436
|
-
}
|
|
437
|
-
else {
|
|
438
|
-
outputAdapter.writeStdout(`Done ${formatDuration(Date.now() - startedAt)}\n`);
|
|
439
|
-
}
|
|
440
|
-
if (exitCode !== 0) {
|
|
441
|
-
if (output && printFailureOutput) {
|
|
442
|
-
if (outputAdapter.supportsTransientStatus) {
|
|
443
|
-
process.stderr.write(output);
|
|
444
|
-
if (!output.endsWith("\n")) {
|
|
445
|
-
process.stderr.write("\n");
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
throw Object.assign(new Error(`Command failed with exit code ${exitCode}`), {
|
|
450
|
-
returnCode: exitCode,
|
|
451
|
-
output,
|
|
452
|
-
});
|
|
453
|
-
}
|
|
454
|
-
return output;
|
|
455
|
-
}
|
|
456
|
-
finally {
|
|
457
|
-
if (timer) {
|
|
458
|
-
clearInterval(timer);
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
271
|
function buildConfig(command, jiraRef, options = {}) {
|
|
463
272
|
const jiraIssueKey = extractIssueKey(jiraRef);
|
|
464
273
|
return {
|
|
@@ -469,8 +278,7 @@ function buildConfig(command, jiraRef, options = {}) {
|
|
|
469
278
|
autoFromPhase: options.autoFromPhase ? validateAutoPhaseId(options.autoFromPhase) : null,
|
|
470
279
|
dryRun: options.dryRun ?? false,
|
|
471
280
|
verbose: options.verbose ?? false,
|
|
472
|
-
dockerComposeFile: defaultDockerComposeFile(),
|
|
473
|
-
dockerComposeCmd: [],
|
|
281
|
+
dockerComposeFile: defaultDockerComposeFile(PACKAGE_ROOT),
|
|
474
282
|
codexCmd: process.env.CODEX_BIN ?? "codex",
|
|
475
283
|
claudeCmd: process.env.CLAUDE_BIN ?? "claude",
|
|
476
284
|
jiraIssueKey,
|
|
@@ -483,7 +291,6 @@ function buildConfig(command, jiraRef, options = {}) {
|
|
|
483
291
|
function checkPrerequisites(config) {
|
|
484
292
|
let codexCmd = config.codexCmd;
|
|
485
293
|
let claudeCmd = config.claudeCmd;
|
|
486
|
-
let dockerComposeCmd = config.dockerComposeCmd;
|
|
487
294
|
if (config.command === "plan" || config.command === "review") {
|
|
488
295
|
codexCmd = resolveCmd("codex", "CODEX_BIN");
|
|
489
296
|
}
|
|
@@ -491,12 +298,12 @@ function checkPrerequisites(config) {
|
|
|
491
298
|
claudeCmd = resolveCmd("claude", "CLAUDE_BIN");
|
|
492
299
|
}
|
|
493
300
|
if (["implement", "review-fix", "test", "test-fix", "test-linter-fix"].includes(config.command)) {
|
|
494
|
-
|
|
301
|
+
resolveDockerComposeCmd();
|
|
495
302
|
if (!existsSync(config.dockerComposeFile)) {
|
|
496
303
|
throw new TaskRunnerError(`docker-compose file not found: ${config.dockerComposeFile}`);
|
|
497
304
|
}
|
|
498
305
|
}
|
|
499
|
-
return { codexCmd, claudeCmd
|
|
306
|
+
return { codexCmd, claudeCmd };
|
|
500
307
|
}
|
|
501
308
|
function buildPhaseConfig(baseConfig, command) {
|
|
502
309
|
return { ...baseConfig, command };
|
|
@@ -516,6 +323,99 @@ function configForAutoStep(baseConfig, step) {
|
|
|
516
323
|
}
|
|
517
324
|
return buildPhaseConfig(baseConfig, step.command);
|
|
518
325
|
}
|
|
326
|
+
async function runAutoStepViaFlow(config, step, codexCmd, claudeCmd, state) {
|
|
327
|
+
const context = createPipelineContext({
|
|
328
|
+
issueKey: config.taskKey,
|
|
329
|
+
jiraRef: config.jiraRef,
|
|
330
|
+
dryRun: config.dryRun,
|
|
331
|
+
verbose: config.verbose,
|
|
332
|
+
runtime: runtimeServices,
|
|
333
|
+
});
|
|
334
|
+
const onStepStart = async (flowStep) => {
|
|
335
|
+
state.currentStep = `${step.id}:${flowStep.id}`;
|
|
336
|
+
saveAutoPipelineState(state);
|
|
337
|
+
};
|
|
338
|
+
if (step.command === "plan") {
|
|
339
|
+
await runFlow(planFlowDefinition, context, {
|
|
340
|
+
jiraApiUrl: config.jiraApiUrl,
|
|
341
|
+
jiraTaskFile: config.jiraTaskFile,
|
|
342
|
+
taskKey: config.taskKey,
|
|
343
|
+
codexCmd,
|
|
344
|
+
...(config.extraPrompt !== undefined ? { extraPrompt: config.extraPrompt } : {}),
|
|
345
|
+
}, { onStepStart });
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
if (step.command === "implement") {
|
|
349
|
+
const implementPrompt = formatPrompt(formatTemplate(IMPLEMENT_PROMPT_TEMPLATE, {
|
|
350
|
+
design_file: designFile(config.taskKey),
|
|
351
|
+
plan_file: planFile(config.taskKey),
|
|
352
|
+
}), config.extraPrompt);
|
|
353
|
+
await runFlow(implementFlowDefinition, context, {
|
|
354
|
+
dockerComposeFile: config.dockerComposeFile,
|
|
355
|
+
prompt: implementPrompt,
|
|
356
|
+
runFollowupVerify: false,
|
|
357
|
+
...(!config.dryRun
|
|
358
|
+
? {
|
|
359
|
+
onVerifyBuildFailure: async (output) => {
|
|
360
|
+
printError("Build verification failed");
|
|
361
|
+
printSummary("Build Failure Summary", await summarizeBuildFailure(output));
|
|
362
|
+
},
|
|
363
|
+
}
|
|
364
|
+
: {}),
|
|
365
|
+
}, { onStepStart });
|
|
366
|
+
return false;
|
|
367
|
+
}
|
|
368
|
+
if (step.command === "test") {
|
|
369
|
+
await runFlow(testFlowDefinition, context, {
|
|
370
|
+
taskKey: config.taskKey,
|
|
371
|
+
dockerComposeFile: config.dockerComposeFile,
|
|
372
|
+
...(!config.dryRun
|
|
373
|
+
? {
|
|
374
|
+
onVerifyBuildFailure: async (output) => {
|
|
375
|
+
printError("Build verification failed");
|
|
376
|
+
printSummary("Build Failure Summary", await summarizeBuildFailure(output));
|
|
377
|
+
},
|
|
378
|
+
}
|
|
379
|
+
: {}),
|
|
380
|
+
}, { onStepStart });
|
|
381
|
+
return false;
|
|
382
|
+
}
|
|
383
|
+
if (step.command === "review") {
|
|
384
|
+
const iteration = step.reviewIteration ?? nextReviewIterationForTask(config.taskKey);
|
|
385
|
+
const result = await runFlow(createReviewFlowDefinition(iteration), context, {
|
|
386
|
+
jiraTaskFile: config.jiraTaskFile,
|
|
387
|
+
taskKey: config.taskKey,
|
|
388
|
+
claudeCmd,
|
|
389
|
+
codexCmd,
|
|
390
|
+
...(config.extraPrompt !== undefined ? { extraPrompt: config.extraPrompt } : {}),
|
|
391
|
+
}, { onStepStart });
|
|
392
|
+
return result.steps.find((candidate) => candidate.id === "check_ready_to_merge")?.result.metadata?.readyToMerge === true;
|
|
393
|
+
}
|
|
394
|
+
if (step.command === "review-fix") {
|
|
395
|
+
const latestIteration = step.reviewIteration ?? latestReviewReplyIteration(config.taskKey);
|
|
396
|
+
if (latestIteration === null) {
|
|
397
|
+
throw new TaskRunnerError(`Review-fix mode requires at least one review-reply-${config.taskKey}-N.md artifact.`);
|
|
398
|
+
}
|
|
399
|
+
await runFlow(createReviewFixFlowDefinition(latestIteration), context, {
|
|
400
|
+
taskKey: config.taskKey,
|
|
401
|
+
dockerComposeFile: config.dockerComposeFile,
|
|
402
|
+
latestIteration,
|
|
403
|
+
...(config.reviewFixPoints !== undefined ? { reviewFixPoints: config.reviewFixPoints } : {}),
|
|
404
|
+
...(config.extraPrompt !== undefined ? { extraPrompt: config.extraPrompt } : {}),
|
|
405
|
+
runFollowupVerify: false,
|
|
406
|
+
...(!config.dryRun
|
|
407
|
+
? {
|
|
408
|
+
onVerifyBuildFailure: async (output) => {
|
|
409
|
+
printError("Build verification failed");
|
|
410
|
+
printSummary("Build Failure Summary", await summarizeBuildFailure(output));
|
|
411
|
+
},
|
|
412
|
+
}
|
|
413
|
+
: {}),
|
|
414
|
+
}, { onStepStart });
|
|
415
|
+
return false;
|
|
416
|
+
}
|
|
417
|
+
throw new TaskRunnerError(`Unsupported auto step command: ${step.command}`);
|
|
418
|
+
}
|
|
519
419
|
function rewindAutoPipelineState(state, phaseId) {
|
|
520
420
|
const targetPhaseId = validateAutoPhaseId(phaseId);
|
|
521
421
|
let phaseSeen = false;
|
|
@@ -540,120 +440,20 @@ function rewindAutoPipelineState(state, phaseId) {
|
|
|
540
440
|
state.currentStep = null;
|
|
541
441
|
state.lastError = null;
|
|
542
442
|
}
|
|
543
|
-
async function runCodexInDocker(config, dockerComposeCmd, prompt, labelText) {
|
|
544
|
-
const dockerEnv = dockerRuntimeEnv();
|
|
545
|
-
dockerEnv.CODEX_PROMPT = prompt;
|
|
546
|
-
dockerEnv.CODEX_EXEC_FLAGS = `--model ${codexModel()} --dangerously-bypass-approvals-and-sandbox`;
|
|
547
|
-
printInfo(labelText);
|
|
548
|
-
printPrompt("Codex", prompt);
|
|
549
|
-
await runCommand([...dockerComposeCmd, "-f", config.dockerComposeFile, "run", "--rm", "codex-exec"], {
|
|
550
|
-
env: dockerEnv,
|
|
551
|
-
dryRun: config.dryRun,
|
|
552
|
-
verbose: config.verbose,
|
|
553
|
-
label: `codex:${codexModel()}`,
|
|
554
|
-
});
|
|
555
|
-
}
|
|
556
|
-
async function runVerifyBuildInDocker(config, dockerComposeCmd, labelText) {
|
|
557
|
-
printInfo(labelText);
|
|
558
|
-
try {
|
|
559
|
-
await runCommand([...dockerComposeCmd, "-f", config.dockerComposeFile, "run", "--rm", "verify-build"], {
|
|
560
|
-
env: dockerRuntimeEnv(),
|
|
561
|
-
dryRun: config.dryRun,
|
|
562
|
-
verbose: false,
|
|
563
|
-
label: "verify-build",
|
|
564
|
-
printFailureOutput: false,
|
|
565
|
-
});
|
|
566
|
-
}
|
|
567
|
-
catch (error) {
|
|
568
|
-
const returnCode = Number(error.returnCode ?? 1);
|
|
569
|
-
printError(`Build verification failed with exit code ${returnCode}`);
|
|
570
|
-
if (!config.dryRun) {
|
|
571
|
-
printSummary("Build Failure Summary", await summarizeBuildFailure(String(error.output ?? "")));
|
|
572
|
-
}
|
|
573
|
-
throw error;
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
443
|
function codexModel() {
|
|
577
444
|
return process.env.CODEX_MODEL?.trim() || DEFAULT_CODEX_MODEL;
|
|
578
445
|
}
|
|
579
446
|
function claudeReviewModel() {
|
|
580
447
|
return process.env.CLAUDE_REVIEW_MODEL?.trim() || DEFAULT_CLAUDE_REVIEW_MODEL;
|
|
581
448
|
}
|
|
582
|
-
function claudeSummaryModel() {
|
|
583
|
-
return process.env.CLAUDE_SUMMARY_MODEL?.trim() || DEFAULT_CLAUDE_SUMMARY_MODEL;
|
|
584
|
-
}
|
|
585
|
-
function truncateText(text, maxChars = 12000) {
|
|
586
|
-
return text.length <= maxChars ? text.trim() : text.trim().slice(-maxChars);
|
|
587
|
-
}
|
|
588
|
-
function fallbackBuildFailureSummary(output) {
|
|
589
|
-
const lines = output
|
|
590
|
-
.split(/\r?\n/)
|
|
591
|
-
.map((line) => line.trim())
|
|
592
|
-
.filter(Boolean);
|
|
593
|
-
const tail = lines.length > 0 ? lines.slice(-8) : ["No build output captured."];
|
|
594
|
-
return `Не удалось получить summary через Claude.\n\nПоследние строки лога:\n${tail.join("\n")}`;
|
|
595
|
-
}
|
|
596
449
|
async function summarizeBuildFailure(output) {
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
let claudeCmd;
|
|
601
|
-
try {
|
|
602
|
-
claudeCmd = resolveCmd("claude", "CLAUDE_BIN");
|
|
603
|
-
}
|
|
604
|
-
catch {
|
|
605
|
-
return fallbackBuildFailureSummary(output);
|
|
606
|
-
}
|
|
607
|
-
const prompt = "Ниже лог упавшей build verification.\n" +
|
|
608
|
-
"Сделай краткое резюме на русском языке, без воды.\n" +
|
|
609
|
-
"Нужно обязательно выделить:\n" +
|
|
610
|
-
"1. Где именно упало.\n" +
|
|
611
|
-
"2. Главную причину падения.\n" +
|
|
612
|
-
"3. Что нужно исправить дальше, если это очевидно.\n" +
|
|
613
|
-
"Ответ дай максимум 5 короткими пунктами.\n\n" +
|
|
614
|
-
`Лог:\n${truncateText(output)}`;
|
|
615
|
-
printInfo(`Summarizing build failure with Claude (${claudeSummaryModel()})`);
|
|
616
|
-
try {
|
|
617
|
-
const summary = await runCommand([claudeCmd, "--model", claudeSummaryModel(), "-p", prompt], {
|
|
618
|
-
env: { ...process.env },
|
|
619
|
-
dryRun: false,
|
|
620
|
-
verbose: false,
|
|
621
|
-
label: `claude:${claudeSummaryModel()}`,
|
|
622
|
-
});
|
|
623
|
-
return summary.trim() || fallbackBuildFailureSummary(output);
|
|
624
|
-
}
|
|
625
|
-
catch {
|
|
626
|
-
return fallbackBuildFailureSummary(output);
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
async function runClaudeSummary(claudeCmd, outputFile, prompt, verbose = false) {
|
|
630
|
-
printInfo(`Preparing summary in ${outputFile}`);
|
|
631
|
-
printPrompt("Claude", prompt);
|
|
632
|
-
await runCommand([claudeCmd, "--model", claudeSummaryModel(), "-p", "--allowedTools=Read,Write,Edit", prompt], {
|
|
633
|
-
env: { ...process.env },
|
|
450
|
+
return summarizeBuildFailureViaPipeline(createPipelineContext({
|
|
451
|
+
issueKey: "build-failure-summary",
|
|
452
|
+
jiraRef: "build-failure-summary",
|
|
634
453
|
dryRun: false,
|
|
635
|
-
verbose,
|
|
636
|
-
|
|
637
|
-
});
|
|
638
|
-
requireArtifacts([outputFile], `Claude summary did not produce ${outputFile}.`);
|
|
639
|
-
return readFileSync(outputFile, "utf8").trim();
|
|
640
|
-
}
|
|
641
|
-
async function summarizeTask(jiraRef) {
|
|
642
|
-
const config = buildConfig("plan", jiraRef);
|
|
643
|
-
const claudeCmd = resolveCmd("claude", "CLAUDE_BIN");
|
|
644
|
-
await fetchJiraIssue(config.jiraApiUrl, config.jiraTaskFile);
|
|
645
|
-
const summaryPrompt = formatTemplate(TASK_SUMMARY_PROMPT_TEMPLATE, {
|
|
646
|
-
jira_task_file: config.jiraTaskFile,
|
|
647
|
-
task_summary_file: taskSummaryFile(config.taskKey),
|
|
648
|
-
});
|
|
649
|
-
const summaryText = await runClaudeSummary(claudeCmd, taskSummaryFile(config.taskKey), summaryPrompt);
|
|
650
|
-
return { issueKey: config.jiraIssueKey, summaryText };
|
|
651
|
-
}
|
|
652
|
-
function resolveTaskIdentity(jiraRef) {
|
|
653
|
-
const config = buildConfig("plan", jiraRef);
|
|
654
|
-
const summaryPath = taskSummaryFile(config.taskKey);
|
|
655
|
-
const summaryText = existsSync(summaryPath) ? readFileSync(summaryPath, "utf8").trim() : "";
|
|
656
|
-
return { issueKey: config.jiraIssueKey, summaryText };
|
|
454
|
+
verbose: false,
|
|
455
|
+
runtime: runtimeServices,
|
|
456
|
+
}), output);
|
|
657
457
|
}
|
|
658
458
|
async function executeCommand(config, runFollowupVerify = true) {
|
|
659
459
|
if (config.command === "auto") {
|
|
@@ -674,16 +474,10 @@ async function executeCommand(config, runFollowupVerify = true) {
|
|
|
674
474
|
printPanel("Auto Reset", removed ? `State file ${autoStateFile(config.taskKey)} removed.` : "No auto state file found.", "yellow");
|
|
675
475
|
return false;
|
|
676
476
|
}
|
|
677
|
-
const { codexCmd, claudeCmd
|
|
477
|
+
const { codexCmd, claudeCmd } = checkPrerequisites(config);
|
|
678
478
|
process.env.JIRA_BROWSE_URL = config.jiraBrowseUrl;
|
|
679
479
|
process.env.JIRA_API_URL = config.jiraApiUrl;
|
|
680
480
|
process.env.JIRA_TASK_FILE = config.jiraTaskFile;
|
|
681
|
-
const planPrompt = formatPrompt(formatTemplate(PLAN_PROMPT_TEMPLATE, {
|
|
682
|
-
jira_task_file: config.jiraTaskFile,
|
|
683
|
-
design_file: designFile(config.taskKey),
|
|
684
|
-
plan_file: planFile(config.taskKey),
|
|
685
|
-
qa_file: qaFile(config.taskKey),
|
|
686
|
-
}), config.extraPrompt);
|
|
687
481
|
const implementPrompt = formatPrompt(formatTemplate(IMPLEMENT_PROMPT_TEMPLATE, {
|
|
688
482
|
design_file: designFile(config.taskKey),
|
|
689
483
|
plan_file: planFile(config.taskKey),
|
|
@@ -694,149 +488,149 @@ async function executeCommand(config, runFollowupVerify = true) {
|
|
|
694
488
|
process.stdout.write(`Resolved Jira API URL: ${config.jiraApiUrl}\n`);
|
|
695
489
|
process.stdout.write(`Saving Jira issue JSON to: ${config.jiraTaskFile}\n`);
|
|
696
490
|
}
|
|
697
|
-
await
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
await runCommand([codexCmd, "exec", "--model", codexModel(), "--full-auto", planPrompt], {
|
|
701
|
-
env: { ...process.env },
|
|
491
|
+
await runPlanFlow(createPipelineContext({
|
|
492
|
+
issueKey: config.taskKey,
|
|
493
|
+
jiraRef: config.jiraRef,
|
|
702
494
|
dryRun: config.dryRun,
|
|
703
495
|
verbose: config.verbose,
|
|
704
|
-
|
|
496
|
+
runtime: runtimeServices,
|
|
497
|
+
}), {
|
|
498
|
+
jiraApiUrl: config.jiraApiUrl,
|
|
499
|
+
jiraTaskFile: config.jiraTaskFile,
|
|
500
|
+
taskKey: config.taskKey,
|
|
501
|
+
codexCmd,
|
|
502
|
+
...(config.extraPrompt !== undefined ? { extraPrompt: config.extraPrompt } : {}),
|
|
705
503
|
});
|
|
706
|
-
requireArtifacts(planArtifacts(config.taskKey), "Plan mode did not produce the required artifacts.");
|
|
707
504
|
return false;
|
|
708
505
|
}
|
|
709
506
|
if (config.command === "implement") {
|
|
710
507
|
requireJiraTaskFile(config.jiraTaskFile);
|
|
711
508
|
requireArtifacts(planArtifacts(config.taskKey), "Implement mode requires plan artifacts from the planning phase.");
|
|
712
|
-
await
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
509
|
+
await runImplementFlow(createPipelineContext({
|
|
510
|
+
issueKey: config.taskKey,
|
|
511
|
+
jiraRef: config.jiraRef,
|
|
512
|
+
dryRun: config.dryRun,
|
|
513
|
+
verbose: config.verbose,
|
|
514
|
+
runtime: runtimeServices,
|
|
515
|
+
}), {
|
|
516
|
+
dockerComposeFile: config.dockerComposeFile,
|
|
517
|
+
prompt: implementPrompt,
|
|
518
|
+
runFollowupVerify,
|
|
519
|
+
...(!config.dryRun
|
|
520
|
+
? {
|
|
521
|
+
onVerifyBuildFailure: async (output) => {
|
|
522
|
+
printError("Build verification failed");
|
|
523
|
+
printSummary("Build Failure Summary", await summarizeBuildFailure(output));
|
|
524
|
+
},
|
|
525
|
+
}
|
|
526
|
+
: {}),
|
|
527
|
+
});
|
|
716
528
|
return false;
|
|
717
529
|
}
|
|
718
530
|
if (config.command === "review") {
|
|
719
531
|
requireJiraTaskFile(config.jiraTaskFile);
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
const reviewReplyFile = artifactFile("review-reply", config.taskKey, iteration);
|
|
724
|
-
const reviewSummaryFile = artifactFile("review-summary", config.taskKey, iteration);
|
|
725
|
-
const reviewReplySummaryFile = artifactFile("review-reply-summary", config.taskKey, iteration);
|
|
726
|
-
const claudePrompt = formatPrompt(formatTemplate(REVIEW_PROMPT_TEMPLATE, {
|
|
727
|
-
jira_task_file: config.jiraTaskFile,
|
|
728
|
-
design_file: designFile(config.taskKey),
|
|
729
|
-
plan_file: planFile(config.taskKey),
|
|
730
|
-
review_file: reviewFile,
|
|
731
|
-
}), config.extraPrompt);
|
|
732
|
-
const codexReplyPrompt = formatPrompt(formatTemplate(REVIEW_REPLY_PROMPT_TEMPLATE, {
|
|
733
|
-
review_file: reviewFile,
|
|
734
|
-
jira_task_file: config.jiraTaskFile,
|
|
735
|
-
design_file: designFile(config.taskKey),
|
|
736
|
-
plan_file: planFile(config.taskKey),
|
|
737
|
-
review_reply_file: reviewReplyFile,
|
|
738
|
-
}), config.extraPrompt);
|
|
739
|
-
printInfo(`Running Claude review mode (iteration ${iteration})`);
|
|
740
|
-
printPrompt("Claude", claudePrompt);
|
|
741
|
-
await runCommand([
|
|
742
|
-
claudeCmd,
|
|
743
|
-
"--model",
|
|
744
|
-
claudeReviewModel(),
|
|
745
|
-
"-p",
|
|
746
|
-
"--allowedTools=Read,Write,Edit",
|
|
747
|
-
"--output-format",
|
|
748
|
-
"stream-json",
|
|
749
|
-
"--verbose",
|
|
750
|
-
"--include-partial-messages",
|
|
751
|
-
claudePrompt,
|
|
752
|
-
], {
|
|
753
|
-
env: { ...process.env },
|
|
754
|
-
dryRun: config.dryRun,
|
|
755
|
-
verbose: config.verbose,
|
|
756
|
-
label: `claude:${claudeReviewModel()}`,
|
|
757
|
-
});
|
|
758
|
-
if (!config.dryRun) {
|
|
759
|
-
requireArtifacts([reviewFile], "Claude review did not produce the required review artifact.");
|
|
760
|
-
const reviewSummaryText = await runClaudeSummary(claudeCmd, reviewSummaryFile, formatTemplate(REVIEW_SUMMARY_PROMPT_TEMPLATE, {
|
|
761
|
-
review_file: reviewFile,
|
|
762
|
-
review_summary_file: reviewSummaryFile,
|
|
763
|
-
}), config.verbose);
|
|
764
|
-
printSummary("Claude Comments", reviewSummaryText);
|
|
765
|
-
}
|
|
766
|
-
printInfo(`Running Codex review reply mode (iteration ${iteration})`);
|
|
767
|
-
printPrompt("Codex", codexReplyPrompt);
|
|
768
|
-
await runCommand([codexCmd, "exec", "--model", codexModel(), "--full-auto", codexReplyPrompt], {
|
|
769
|
-
env: { ...process.env },
|
|
532
|
+
const result = await runReviewFlow(createPipelineContext({
|
|
533
|
+
issueKey: config.taskKey,
|
|
534
|
+
jiraRef: config.jiraRef,
|
|
770
535
|
dryRun: config.dryRun,
|
|
771
536
|
verbose: config.verbose,
|
|
772
|
-
|
|
537
|
+
runtime: runtimeServices,
|
|
538
|
+
}), {
|
|
539
|
+
jiraTaskFile: config.jiraTaskFile,
|
|
540
|
+
taskKey: config.taskKey,
|
|
541
|
+
claudeCmd,
|
|
542
|
+
codexCmd,
|
|
543
|
+
...(config.extraPrompt !== undefined ? { extraPrompt: config.extraPrompt } : {}),
|
|
773
544
|
});
|
|
774
|
-
|
|
775
|
-
if (!config.dryRun) {
|
|
776
|
-
requireArtifacts([reviewReplyFile], "Codex review reply did not produce the required review-reply artifact.");
|
|
777
|
-
const reviewReplySummaryText = await runClaudeSummary(claudeCmd, reviewReplySummaryFile, formatTemplate(REVIEW_REPLY_SUMMARY_PROMPT_TEMPLATE, {
|
|
778
|
-
review_reply_file: reviewReplyFile,
|
|
779
|
-
review_reply_summary_file: reviewReplySummaryFile,
|
|
780
|
-
}), config.verbose);
|
|
781
|
-
printSummary("Codex Reply Summary", reviewReplySummaryText);
|
|
782
|
-
if (existsSync(READY_TO_MERGE_FILE)) {
|
|
783
|
-
printPanel("Ready To Merge", "Изменения готовы к merge\nФайл ready-to-merge.md создан.", "green");
|
|
784
|
-
readyToMerge = true;
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
return readyToMerge;
|
|
545
|
+
return result.readyToMerge;
|
|
788
546
|
}
|
|
789
547
|
if (config.command === "review-fix") {
|
|
790
548
|
requireJiraTaskFile(config.jiraTaskFile);
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
549
|
+
await runReviewFixFlow(createPipelineContext({
|
|
550
|
+
issueKey: config.taskKey,
|
|
551
|
+
jiraRef: config.jiraRef,
|
|
552
|
+
dryRun: config.dryRun,
|
|
553
|
+
verbose: config.verbose,
|
|
554
|
+
runtime: runtimeServices,
|
|
555
|
+
}), {
|
|
556
|
+
taskKey: config.taskKey,
|
|
557
|
+
dockerComposeFile: config.dockerComposeFile,
|
|
558
|
+
latestIteration: latestReviewReplyIteration(config.taskKey),
|
|
559
|
+
...(config.extraPrompt !== undefined ? { extraPrompt: config.extraPrompt } : {}),
|
|
560
|
+
...(config.reviewFixPoints !== undefined ? { reviewFixPoints: config.reviewFixPoints } : {}),
|
|
561
|
+
runFollowupVerify,
|
|
562
|
+
...(!config.dryRun
|
|
563
|
+
? {
|
|
564
|
+
onVerifyBuildFailure: async (output) => {
|
|
565
|
+
printError("Build verification failed");
|
|
566
|
+
printSummary("Build Failure Summary", await summarizeBuildFailure(output));
|
|
567
|
+
},
|
|
568
|
+
}
|
|
569
|
+
: {}),
|
|
570
|
+
});
|
|
810
571
|
return false;
|
|
811
572
|
}
|
|
812
573
|
if (config.command === "test") {
|
|
813
574
|
requireJiraTaskFile(config.jiraTaskFile);
|
|
814
|
-
|
|
815
|
-
|
|
575
|
+
await runTestFlow(createPipelineContext({
|
|
576
|
+
issueKey: config.taskKey,
|
|
577
|
+
jiraRef: config.jiraRef,
|
|
578
|
+
dryRun: config.dryRun,
|
|
579
|
+
verbose: config.verbose,
|
|
580
|
+
runtime: runtimeServices,
|
|
581
|
+
}), {
|
|
582
|
+
taskKey: config.taskKey,
|
|
583
|
+
dockerComposeFile: config.dockerComposeFile,
|
|
584
|
+
...(!config.dryRun
|
|
585
|
+
? {
|
|
586
|
+
onVerifyBuildFailure: async (output) => {
|
|
587
|
+
printError("Build verification failed");
|
|
588
|
+
printSummary("Build Failure Summary", await summarizeBuildFailure(output));
|
|
589
|
+
},
|
|
590
|
+
}
|
|
591
|
+
: {}),
|
|
592
|
+
});
|
|
816
593
|
return false;
|
|
817
594
|
}
|
|
818
595
|
if (config.command === "test-fix" || config.command === "test-linter-fix") {
|
|
819
596
|
requireJiraTaskFile(config.jiraTaskFile);
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
597
|
+
await runTestFixFlow(createPipelineContext({
|
|
598
|
+
issueKey: config.taskKey,
|
|
599
|
+
jiraRef: config.jiraRef,
|
|
600
|
+
dryRun: config.dryRun,
|
|
601
|
+
verbose: config.verbose,
|
|
602
|
+
runtime: runtimeServices,
|
|
603
|
+
}), {
|
|
604
|
+
command: config.command,
|
|
605
|
+
taskKey: config.taskKey,
|
|
606
|
+
dockerComposeFile: config.dockerComposeFile,
|
|
607
|
+
...(config.extraPrompt !== undefined ? { extraPrompt: config.extraPrompt } : {}),
|
|
608
|
+
});
|
|
823
609
|
return false;
|
|
824
610
|
}
|
|
825
611
|
throw new TaskRunnerError(`Unsupported command: ${config.command}`);
|
|
826
612
|
}
|
|
827
613
|
async function runAutoPipelineDryRun(config) {
|
|
614
|
+
const { codexCmd, claudeCmd } = checkPrerequisites(config);
|
|
828
615
|
printInfo("Dry-run auto pipeline: plan -> implement -> test -> review/review-fix/test");
|
|
829
|
-
|
|
830
|
-
await
|
|
831
|
-
await
|
|
616
|
+
const dryState = createAutoPipelineState(config);
|
|
617
|
+
await runAutoStepViaFlow(buildPhaseConfig(config, "plan"), dryState.steps[0], codexCmd, claudeCmd, dryState);
|
|
618
|
+
await runAutoStepViaFlow(buildPhaseConfig(config, "implement"), dryState.steps[1], codexCmd, claudeCmd, dryState);
|
|
619
|
+
await runAutoStepViaFlow(buildPhaseConfig(config, "test"), dryState.steps[2], codexCmd, claudeCmd, dryState);
|
|
832
620
|
for (let iteration = 1; iteration <= AUTO_MAX_REVIEW_ITERATIONS; iteration += 1) {
|
|
833
621
|
printInfo(`Dry-run auto review iteration ${iteration}/${AUTO_MAX_REVIEW_ITERATIONS}`);
|
|
834
|
-
|
|
835
|
-
|
|
622
|
+
const reviewStep = dryState.steps.find((step) => step.id === `review_${iteration}`);
|
|
623
|
+
const reviewFixStep = dryState.steps.find((step) => step.id === `review_fix_${iteration}`);
|
|
624
|
+
const testStep = dryState.steps.find((step) => step.id === `test_after_review_fix_${iteration}`);
|
|
625
|
+
if (!reviewStep || !reviewFixStep || !testStep) {
|
|
626
|
+
throw new TaskRunnerError(`Missing auto dry-run steps for iteration ${iteration}`);
|
|
627
|
+
}
|
|
628
|
+
await runAutoStepViaFlow(buildPhaseConfig(config, "review"), reviewStep, codexCmd, claudeCmd, dryState);
|
|
629
|
+
await runAutoStepViaFlow({
|
|
836
630
|
...buildPhaseConfig(config, "review-fix"),
|
|
837
631
|
extraPrompt: appendPromptText(config.extraPrompt, AUTO_REVIEW_FIX_EXTRA_PROMPT),
|
|
838
|
-
},
|
|
839
|
-
await
|
|
632
|
+
}, reviewFixStep, codexCmd, claudeCmd, dryState);
|
|
633
|
+
await runAutoStepViaFlow(buildPhaseConfig(config, "test"), testStep, codexCmd, claudeCmd, dryState);
|
|
840
634
|
}
|
|
841
635
|
}
|
|
842
636
|
async function runAutoPipeline(config) {
|
|
@@ -844,6 +638,7 @@ async function runAutoPipeline(config) {
|
|
|
844
638
|
await runAutoPipelineDryRun(config);
|
|
845
639
|
return;
|
|
846
640
|
}
|
|
641
|
+
const { codexCmd, claudeCmd } = checkPrerequisites(config);
|
|
847
642
|
let state = loadAutoPipelineState(config) ?? createAutoPipelineState(config);
|
|
848
643
|
if (config.autoFromPhase) {
|
|
849
644
|
rewindAutoPipelineState(state, config.autoFromPhase);
|
|
@@ -887,7 +682,7 @@ async function runAutoPipeline(config) {
|
|
|
887
682
|
saveAutoPipelineState(state);
|
|
888
683
|
try {
|
|
889
684
|
printInfo(`Running auto step: ${step.id}`);
|
|
890
|
-
const readyToMerge = await
|
|
685
|
+
const readyToMerge = await runAutoStepViaFlow(configForAutoStep(config, step), step, codexCmd, claudeCmd, state);
|
|
891
686
|
step.status = "done";
|
|
892
687
|
step.finishedAt = nowIso8601();
|
|
893
688
|
step.returnCode = 0;
|
|
@@ -1098,7 +893,6 @@ async function ensureHistoryFile() {
|
|
|
1098
893
|
async function runInteractive(jiraRef, forceRefresh = false) {
|
|
1099
894
|
const config = buildConfig("plan", jiraRef);
|
|
1100
895
|
const jiraTaskPath = config.jiraTaskFile;
|
|
1101
|
-
const taskIdentity = forceRefresh || !existsSync(jiraTaskPath) ? await summarizeTask(jiraRef) : resolveTaskIdentity(jiraRef);
|
|
1102
896
|
await ensureHistoryFile();
|
|
1103
897
|
const historyLines = (await readFile(HISTORY_FILE, "utf8"))
|
|
1104
898
|
.split(/\r?\n/)
|
|
@@ -1120,8 +914,8 @@ async function runInteractive(jiraRef, forceRefresh = false) {
|
|
|
1120
914
|
"/exit",
|
|
1121
915
|
];
|
|
1122
916
|
const ui = new InteractiveUi({
|
|
1123
|
-
issueKey:
|
|
1124
|
-
summaryText:
|
|
917
|
+
issueKey: config.jiraIssueKey,
|
|
918
|
+
summaryText: "Starting interactive session...",
|
|
1125
919
|
cwd: process.cwd(),
|
|
1126
920
|
commands: commandList,
|
|
1127
921
|
onSubmit: async (line) => {
|
|
@@ -1159,14 +953,40 @@ async function runInteractive(jiraRef, forceRefresh = false) {
|
|
|
1159
953
|
},
|
|
1160
954
|
}, historyLines);
|
|
1161
955
|
ui.mount();
|
|
1162
|
-
printInfo(`Interactive mode for ${
|
|
1163
|
-
|
|
1164
|
-
|
|
956
|
+
printInfo(`Interactive mode for ${config.jiraIssueKey}`);
|
|
957
|
+
printInfo("Use /help to see commands.");
|
|
958
|
+
try {
|
|
959
|
+
ui.setBusy(true, "preflight");
|
|
960
|
+
await runPreflightFlow(createPipelineContext({
|
|
961
|
+
issueKey: config.taskKey,
|
|
962
|
+
jiraRef: config.jiraRef,
|
|
963
|
+
dryRun: false,
|
|
964
|
+
verbose: config.verbose,
|
|
965
|
+
runtime: runtimeServices,
|
|
966
|
+
setSummary: (markdown) => {
|
|
967
|
+
ui.setSummary(markdown);
|
|
968
|
+
},
|
|
969
|
+
}), {
|
|
970
|
+
jiraApiUrl: config.jiraApiUrl,
|
|
971
|
+
jiraTaskFile: config.jiraTaskFile,
|
|
972
|
+
taskKey: config.taskKey,
|
|
973
|
+
forceRefresh,
|
|
974
|
+
});
|
|
975
|
+
if (!existsSync(taskSummaryFile(config.taskKey))) {
|
|
976
|
+
ui.setSummary("Task summary is not available yet. Run `/plan` or refresh Jira data.");
|
|
977
|
+
}
|
|
1165
978
|
}
|
|
1166
|
-
|
|
1167
|
-
|
|
979
|
+
catch (error) {
|
|
980
|
+
if (error instanceof TaskRunnerError) {
|
|
981
|
+
printError(error.message);
|
|
982
|
+
}
|
|
983
|
+
else {
|
|
984
|
+
throw error;
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
finally {
|
|
988
|
+
ui.setBusy(false);
|
|
1168
989
|
}
|
|
1169
|
-
printInfo("Use /help to see commands.");
|
|
1170
990
|
return await new Promise((resolve, reject) => {
|
|
1171
991
|
const interval = setInterval(() => {
|
|
1172
992
|
if (!exiting) {
|