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/interactive-ui.js
CHANGED
|
@@ -6,6 +6,7 @@ export class InteractiveUi {
|
|
|
6
6
|
screen;
|
|
7
7
|
header;
|
|
8
8
|
summary;
|
|
9
|
+
status;
|
|
9
10
|
sidebar;
|
|
10
11
|
log;
|
|
11
12
|
input;
|
|
@@ -15,7 +16,13 @@ export class InteractiveUi {
|
|
|
15
16
|
historyIndex;
|
|
16
17
|
busy = false;
|
|
17
18
|
currentCommand = "idle";
|
|
19
|
+
summaryText = "";
|
|
18
20
|
focusedPane = "input";
|
|
21
|
+
currentNode = null;
|
|
22
|
+
currentExecutor = null;
|
|
23
|
+
spinnerFrame = 0;
|
|
24
|
+
spinnerTimer = null;
|
|
25
|
+
runningStartedAt = null;
|
|
19
26
|
constructor(options, history) {
|
|
20
27
|
this.options = options;
|
|
21
28
|
this.history = history;
|
|
@@ -49,7 +56,7 @@ export class InteractiveUi {
|
|
|
49
56
|
top: 3,
|
|
50
57
|
left: 0,
|
|
51
58
|
width: "28%",
|
|
52
|
-
|
|
59
|
+
bottom: 10,
|
|
53
60
|
tags: true,
|
|
54
61
|
label: " Commands ",
|
|
55
62
|
padding: {
|
|
@@ -71,7 +78,7 @@ export class InteractiveUi {
|
|
|
71
78
|
top: 3,
|
|
72
79
|
left: "28%",
|
|
73
80
|
width: "72%",
|
|
74
|
-
height:
|
|
81
|
+
height: 8,
|
|
75
82
|
tags: true,
|
|
76
83
|
label: " Task Summary ",
|
|
77
84
|
padding: {
|
|
@@ -88,12 +95,30 @@ export class InteractiveUi {
|
|
|
88
95
|
fg: "white",
|
|
89
96
|
},
|
|
90
97
|
});
|
|
98
|
+
this.status = blessed.box({
|
|
99
|
+
parent: this.screen,
|
|
100
|
+
bottom: 4,
|
|
101
|
+
left: 0,
|
|
102
|
+
width: "28%",
|
|
103
|
+
height: 6,
|
|
104
|
+
tags: true,
|
|
105
|
+
label: " Status ",
|
|
106
|
+
padding: {
|
|
107
|
+
left: 1,
|
|
108
|
+
right: 1,
|
|
109
|
+
},
|
|
110
|
+
border: "line",
|
|
111
|
+
style: {
|
|
112
|
+
border: { fg: "green" },
|
|
113
|
+
fg: "white",
|
|
114
|
+
},
|
|
115
|
+
});
|
|
91
116
|
this.log = blessed.log({
|
|
92
117
|
parent: this.screen,
|
|
93
|
-
top:
|
|
118
|
+
top: 11,
|
|
119
|
+
bottom: 4,
|
|
94
120
|
left: "28%",
|
|
95
121
|
width: "72%",
|
|
96
|
-
height: "100%-16",
|
|
97
122
|
tags: false,
|
|
98
123
|
label: " Activity ",
|
|
99
124
|
padding: {
|
|
@@ -347,12 +372,9 @@ export class InteractiveUi {
|
|
|
347
372
|
this.screen.render();
|
|
348
373
|
}
|
|
349
374
|
renderStaticContent() {
|
|
350
|
-
this.
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
? this.options.summaryText.trim()
|
|
354
|
-
: "Using existing Jira task file.\n\nUse /help to see commands.";
|
|
355
|
-
this.summary.setContent(renderMarkdownToTerminal(stripAnsi(summaryBody)));
|
|
375
|
+
this.summaryText = this.options.summaryText.trim();
|
|
376
|
+
this.updateHeader();
|
|
377
|
+
this.renderSummary();
|
|
356
378
|
this.sidebar.setContent([
|
|
357
379
|
this.options.commands.join("\n"),
|
|
358
380
|
"",
|
|
@@ -378,6 +400,16 @@ export class InteractiveUi {
|
|
|
378
400
|
].join("\n")));
|
|
379
401
|
this.footer.setContent(" Enter: run command | Tab: complete | Up/Down: history | ?: help | Ctrl+L: clear log | q: exit ");
|
|
380
402
|
}
|
|
403
|
+
updateHeader() {
|
|
404
|
+
this.header.setContent(`{bold}AgentWeaver{/bold} {green-fg}${this.options.issueKey}{/green-fg}\n` +
|
|
405
|
+
`cwd: ${this.options.cwd} current: ${this.currentCommand}`);
|
|
406
|
+
}
|
|
407
|
+
renderSummary() {
|
|
408
|
+
const summaryBody = this.summaryText
|
|
409
|
+
? this.summaryText
|
|
410
|
+
: "Task summary is not available yet.";
|
|
411
|
+
this.summary.setContent(renderMarkdownToTerminal(stripAnsi(summaryBody)));
|
|
412
|
+
}
|
|
381
413
|
createAdapter() {
|
|
382
414
|
return {
|
|
383
415
|
writeStdout: (text) => {
|
|
@@ -388,6 +420,12 @@ export class InteractiveUi {
|
|
|
388
420
|
},
|
|
389
421
|
supportsTransientStatus: false,
|
|
390
422
|
supportsPassthrough: false,
|
|
423
|
+
renderAuxiliaryOutput: false,
|
|
424
|
+
setExecutionState: (state) => {
|
|
425
|
+
this.currentNode = state.node;
|
|
426
|
+
this.currentExecutor = state.executor;
|
|
427
|
+
this.updateRunningPanel();
|
|
428
|
+
},
|
|
391
429
|
};
|
|
392
430
|
}
|
|
393
431
|
mount() {
|
|
@@ -395,18 +433,39 @@ export class InteractiveUi {
|
|
|
395
433
|
this.focusPane("input");
|
|
396
434
|
}
|
|
397
435
|
destroy() {
|
|
436
|
+
if (this.spinnerTimer) {
|
|
437
|
+
clearInterval(this.spinnerTimer);
|
|
438
|
+
this.spinnerTimer = null;
|
|
439
|
+
}
|
|
398
440
|
setOutputAdapter(null);
|
|
399
441
|
this.screen.destroy();
|
|
400
442
|
}
|
|
401
443
|
setBusy(busy, command) {
|
|
402
444
|
this.busy = busy;
|
|
403
445
|
this.currentCommand = command ?? (busy ? this.currentCommand : "idle");
|
|
446
|
+
if (busy && this.runningStartedAt === null) {
|
|
447
|
+
this.runningStartedAt = Date.now();
|
|
448
|
+
}
|
|
449
|
+
else if (!busy && this.currentNode === null && this.currentExecutor === null) {
|
|
450
|
+
this.runningStartedAt = null;
|
|
451
|
+
}
|
|
452
|
+
this.updateHeader();
|
|
404
453
|
this.header.setContent(`{bold}AgentWeaver{/bold} {green-fg}${this.options.issueKey}{/green-fg}\n` +
|
|
405
|
-
`cwd: ${this.options.cwd}
|
|
406
|
-
|
|
454
|
+
`cwd: ${this.options.cwd} current: ${this.currentCommand}${busy ? " {yellow-fg}[running]{/yellow-fg}" : ""}`);
|
|
455
|
+
this.updateRunningPanel();
|
|
407
456
|
this.input.setLabel(busy ? " command [busy] " : " command ");
|
|
408
457
|
this.screen.render();
|
|
409
458
|
}
|
|
459
|
+
setStatus(status) {
|
|
460
|
+
this.currentCommand = status;
|
|
461
|
+
this.updateHeader();
|
|
462
|
+
this.screen.render();
|
|
463
|
+
}
|
|
464
|
+
setSummary(markdown) {
|
|
465
|
+
this.summaryText = markdown.trim();
|
|
466
|
+
this.renderSummary();
|
|
467
|
+
this.screen.render();
|
|
468
|
+
}
|
|
410
469
|
appendLog(text) {
|
|
411
470
|
const normalized = text
|
|
412
471
|
.split("\n")
|
|
@@ -424,4 +483,42 @@ export class InteractiveUi {
|
|
|
424
483
|
this.log.setScrollPerc(100);
|
|
425
484
|
this.screen.render();
|
|
426
485
|
}
|
|
486
|
+
updateRunningPanel() {
|
|
487
|
+
const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
488
|
+
const running = this.busy || this.currentNode !== null || this.currentExecutor !== null;
|
|
489
|
+
if (running && this.spinnerTimer === null) {
|
|
490
|
+
if (this.runningStartedAt === null) {
|
|
491
|
+
this.runningStartedAt = Date.now();
|
|
492
|
+
}
|
|
493
|
+
this.spinnerTimer = setInterval(() => {
|
|
494
|
+
this.spinnerFrame = (this.spinnerFrame + 1) % frames.length;
|
|
495
|
+
this.updateRunningPanel();
|
|
496
|
+
this.screen.render();
|
|
497
|
+
}, 120);
|
|
498
|
+
}
|
|
499
|
+
else if (!running && this.spinnerTimer) {
|
|
500
|
+
clearInterval(this.spinnerTimer);
|
|
501
|
+
this.spinnerTimer = null;
|
|
502
|
+
this.spinnerFrame = 0;
|
|
503
|
+
this.runningStartedAt = null;
|
|
504
|
+
}
|
|
505
|
+
const spinner = running ? `{green-fg}${frames[this.spinnerFrame] ?? "•"}{/green-fg}` : "•";
|
|
506
|
+
const elapsed = this.formatElapsed(running ? Date.now() : null);
|
|
507
|
+
const nodeLine = `Node: ${this.currentNode ?? "-"}`;
|
|
508
|
+
const executorLine = `Executor: ${this.currentExecutor ?? "-"}`;
|
|
509
|
+
const stateLine = `State: ${running ? `${spinner} running` : "idle"}`;
|
|
510
|
+
const elapsedLine = `Time: ${elapsed}`;
|
|
511
|
+
this.status.setContent([stateLine, elapsedLine, nodeLine, executorLine].join("\n"));
|
|
512
|
+
this.screen.render();
|
|
513
|
+
}
|
|
514
|
+
formatElapsed(now) {
|
|
515
|
+
if (this.runningStartedAt === null || now === null) {
|
|
516
|
+
return "00:00:00";
|
|
517
|
+
}
|
|
518
|
+
const totalSeconds = Math.max(0, Math.floor((now - this.runningStartedAt) / 1000));
|
|
519
|
+
const hours = String(Math.floor(totalSeconds / 3600)).padStart(2, "0");
|
|
520
|
+
const minutes = String(Math.floor((totalSeconds % 3600) / 60)).padStart(2, "0");
|
|
521
|
+
const seconds = String(totalSeconds % 60).padStart(2, "0");
|
|
522
|
+
return `${hours}:${minutes}:${seconds}`;
|
|
523
|
+
}
|
|
427
524
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { runNode } from "./node-runner.js";
|
|
2
|
+
import { buildFailureSummaryNode } from "./nodes/build-failure-summary-node.js";
|
|
3
|
+
export async function summarizeBuildFailure(context, output) {
|
|
4
|
+
const result = await runNode(buildFailureSummaryNode, context, { output });
|
|
5
|
+
return result.value.summaryText;
|
|
6
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { requireArtifacts } from "../artifacts.js";
|
|
2
|
+
import { TaskRunnerError } from "../errors.js";
|
|
3
|
+
export function runNodeChecks(checks) {
|
|
4
|
+
for (const check of checks) {
|
|
5
|
+
if (check.kind === "require-artifacts") {
|
|
6
|
+
requireArtifacts(check.paths, check.message);
|
|
7
|
+
continue;
|
|
8
|
+
}
|
|
9
|
+
if (check.kind === "require-file") {
|
|
10
|
+
requireArtifacts([check.path], check.message);
|
|
11
|
+
continue;
|
|
12
|
+
}
|
|
13
|
+
throw new TaskRunnerError(`Unsupported node check: ${check.kind ?? "unknown"}`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
import { getOutputAdapter } from "../tui.js";
|
|
3
|
+
import { createExecutorRegistry } from "./registry.js";
|
|
4
|
+
export function createPipelineContext(input) {
|
|
5
|
+
return {
|
|
6
|
+
issueKey: input.issueKey,
|
|
7
|
+
jiraRef: input.jiraRef,
|
|
8
|
+
cwd: process.cwd(),
|
|
9
|
+
env: { ...process.env },
|
|
10
|
+
ui: getOutputAdapter(),
|
|
11
|
+
dryRun: input.dryRun,
|
|
12
|
+
verbose: input.verbose,
|
|
13
|
+
runtime: input.runtime,
|
|
14
|
+
executors: createExecutorRegistry(),
|
|
15
|
+
...(input.setSummary ? { setSummary: input.setSummary } : {}),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export async function runFlow(definition, context, params, options = {}) {
|
|
2
|
+
const steps = [];
|
|
3
|
+
for (const step of definition.steps) {
|
|
4
|
+
await options.onStepStart?.(step);
|
|
5
|
+
const result = await step.run(context, params);
|
|
6
|
+
await options.onStepComplete?.(step, result);
|
|
7
|
+
steps.push({
|
|
8
|
+
id: step.id,
|
|
9
|
+
result,
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
return { steps };
|
|
13
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { runFlow } from "../flow-runner.js";
|
|
2
|
+
import { runNode } from "../node-runner.js";
|
|
3
|
+
import { codexDockerPromptNode } from "../nodes/codex-docker-prompt-node.js";
|
|
4
|
+
import { verifyBuildNode } from "../nodes/verify-build-node.js";
|
|
5
|
+
export const implementFlowDefinition = {
|
|
6
|
+
kind: "implement-flow",
|
|
7
|
+
version: 1,
|
|
8
|
+
steps: [
|
|
9
|
+
{
|
|
10
|
+
id: "run_codex_implement",
|
|
11
|
+
async run(context, params) {
|
|
12
|
+
await runNode(codexDockerPromptNode, context, {
|
|
13
|
+
dockerComposeFile: params.dockerComposeFile,
|
|
14
|
+
prompt: params.prompt,
|
|
15
|
+
labelText: "Running Codex implementation mode in isolated Docker",
|
|
16
|
+
});
|
|
17
|
+
return { completed: true };
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: "verify_build_after_implement",
|
|
22
|
+
async run(context, params) {
|
|
23
|
+
if (!params.runFollowupVerify) {
|
|
24
|
+
return {
|
|
25
|
+
completed: true,
|
|
26
|
+
metadata: { skipped: true },
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
await runNode(verifyBuildNode, context, {
|
|
31
|
+
dockerComposeFile: params.dockerComposeFile,
|
|
32
|
+
labelText: "Running build verification in isolated Docker",
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
if (params.onVerifyBuildFailure) {
|
|
37
|
+
await params.onVerifyBuildFailure(String(error.output ?? ""));
|
|
38
|
+
}
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
return { completed: true };
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
};
|
|
46
|
+
export async function runImplementFlow(context, params) {
|
|
47
|
+
await runFlow(implementFlowDefinition, context, params);
|
|
48
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { designFile, planArtifacts, planFile, qaFile } from "../../artifacts.js";
|
|
2
|
+
import { PLAN_PROMPT_TEMPLATE, formatPrompt, formatTemplate } from "../../prompts.js";
|
|
3
|
+
import { runFlow } from "../flow-runner.js";
|
|
4
|
+
import { runNode } from "../node-runner.js";
|
|
5
|
+
import { jiraFetchNode } from "../nodes/jira-fetch-node.js";
|
|
6
|
+
import { planCodexNode } from "../nodes/plan-codex-node.js";
|
|
7
|
+
export const planFlowDefinition = {
|
|
8
|
+
kind: "plan-flow",
|
|
9
|
+
version: 1,
|
|
10
|
+
steps: [
|
|
11
|
+
{
|
|
12
|
+
id: "fetch_jira",
|
|
13
|
+
async run(context, params) {
|
|
14
|
+
await runNode(jiraFetchNode, context, {
|
|
15
|
+
jiraApiUrl: params.jiraApiUrl,
|
|
16
|
+
outputFile: params.jiraTaskFile,
|
|
17
|
+
});
|
|
18
|
+
return { completed: true };
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
id: "run_codex_plan",
|
|
23
|
+
async run(context, params) {
|
|
24
|
+
const prompt = formatPrompt(formatTemplate(PLAN_PROMPT_TEMPLATE, {
|
|
25
|
+
jira_task_file: params.jiraTaskFile,
|
|
26
|
+
design_file: designFile(params.taskKey),
|
|
27
|
+
plan_file: planFile(params.taskKey),
|
|
28
|
+
qa_file: qaFile(params.taskKey),
|
|
29
|
+
}), params.extraPrompt);
|
|
30
|
+
await runNode(planCodexNode, context, {
|
|
31
|
+
prompt,
|
|
32
|
+
command: params.codexCmd,
|
|
33
|
+
requiredArtifacts: planArtifacts(params.taskKey),
|
|
34
|
+
});
|
|
35
|
+
return { completed: true };
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
};
|
|
40
|
+
export async function runPlanFlow(context, params) {
|
|
41
|
+
await runFlow(planFlowDefinition, context, params);
|
|
42
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { taskSummaryFile } from "../../artifacts.js";
|
|
3
|
+
import { runFlow } from "../flow-runner.js";
|
|
4
|
+
import { runNode } from "../node-runner.js";
|
|
5
|
+
import { commandCheckNode } from "../nodes/command-check-node.js";
|
|
6
|
+
import { jiraFetchNode } from "../nodes/jira-fetch-node.js";
|
|
7
|
+
import { taskSummaryNode } from "../nodes/task-summary-node.js";
|
|
8
|
+
export const preflightFlowDefinition = {
|
|
9
|
+
kind: "interactive-preflight-flow",
|
|
10
|
+
version: 1,
|
|
11
|
+
steps: [
|
|
12
|
+
{
|
|
13
|
+
id: "check_commands",
|
|
14
|
+
async run(context) {
|
|
15
|
+
const result = await runNode(commandCheckNode, context, {
|
|
16
|
+
commands: [
|
|
17
|
+
{ commandName: "codex", envVarName: "CODEX_BIN" },
|
|
18
|
+
{ commandName: "claude", envVarName: "CLAUDE_BIN" },
|
|
19
|
+
],
|
|
20
|
+
});
|
|
21
|
+
return { completed: true, metadata: { resolved: result.value.resolved.length } };
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: "fetch_jira_if_needed",
|
|
26
|
+
async run(context, params) {
|
|
27
|
+
if (!params.forceRefresh && existsSync(params.jiraTaskFile)) {
|
|
28
|
+
return { completed: true, metadata: { skipped: true } };
|
|
29
|
+
}
|
|
30
|
+
await runNode(jiraFetchNode, context, {
|
|
31
|
+
jiraApiUrl: params.jiraApiUrl,
|
|
32
|
+
outputFile: params.jiraTaskFile,
|
|
33
|
+
});
|
|
34
|
+
return { completed: true };
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: "load_or_generate_task_summary",
|
|
39
|
+
async run(context, params) {
|
|
40
|
+
const summaryPath = taskSummaryFile(params.taskKey);
|
|
41
|
+
if (!params.forceRefresh && existsSync(params.jiraTaskFile) && existsSync(summaryPath)) {
|
|
42
|
+
context.setSummary?.(readFileSync(summaryPath, "utf8").trim());
|
|
43
|
+
return { completed: true, metadata: { source: "existing" } };
|
|
44
|
+
}
|
|
45
|
+
const claudeCmd = context.runtime.resolveCmd("claude", "CLAUDE_BIN");
|
|
46
|
+
await runNode(taskSummaryNode, context, {
|
|
47
|
+
jiraTaskFile: params.jiraTaskFile,
|
|
48
|
+
taskKey: params.taskKey,
|
|
49
|
+
claudeCmd,
|
|
50
|
+
verbose: context.verbose,
|
|
51
|
+
});
|
|
52
|
+
return { completed: true, metadata: { source: "generated" } };
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
};
|
|
57
|
+
export async function runPreflightFlow(context, params) {
|
|
58
|
+
await runFlow(preflightFlowDefinition, context, params);
|
|
59
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { artifactFile, planArtifacts, requireArtifacts } from "../../artifacts.js";
|
|
2
|
+
import { TaskRunnerError } from "../../errors.js";
|
|
3
|
+
import { REVIEW_FIX_PROMPT_TEMPLATE, formatPrompt, formatTemplate } from "../../prompts.js";
|
|
4
|
+
import { runFlow } from "../flow-runner.js";
|
|
5
|
+
import { runNode } from "../node-runner.js";
|
|
6
|
+
import { codexDockerPromptNode } from "../nodes/codex-docker-prompt-node.js";
|
|
7
|
+
import { verifyBuildNode } from "../nodes/verify-build-node.js";
|
|
8
|
+
export function createReviewFixFlowDefinition(iteration) {
|
|
9
|
+
return {
|
|
10
|
+
kind: "review-fix-flow",
|
|
11
|
+
version: 1,
|
|
12
|
+
steps: [
|
|
13
|
+
{
|
|
14
|
+
id: "run_codex_review_fix",
|
|
15
|
+
async run(stepContext, stepParams) {
|
|
16
|
+
const reviewReplyFile = artifactFile("review-reply", stepParams.taskKey, iteration);
|
|
17
|
+
const reviewFixFile = artifactFile("review-fix", stepParams.taskKey, iteration);
|
|
18
|
+
const prompt = formatPrompt(formatTemplate(REVIEW_FIX_PROMPT_TEMPLATE, {
|
|
19
|
+
review_reply_file: reviewReplyFile,
|
|
20
|
+
items: stepParams.reviewFixPoints ?? "",
|
|
21
|
+
review_fix_file: reviewFixFile,
|
|
22
|
+
}), stepParams.extraPrompt);
|
|
23
|
+
await runNode(codexDockerPromptNode, stepContext, {
|
|
24
|
+
dockerComposeFile: stepParams.dockerComposeFile,
|
|
25
|
+
prompt,
|
|
26
|
+
labelText: `Running Codex review-fix mode in isolated Docker (iteration ${iteration})`,
|
|
27
|
+
requiredArtifacts: stepContext.dryRun ? [] : [reviewFixFile],
|
|
28
|
+
missingArtifactsMessage: "Review-fix mode did not produce the required review-fix artifact.",
|
|
29
|
+
});
|
|
30
|
+
return { completed: true, metadata: { iteration } };
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: "verify_build_after_review_fix",
|
|
35
|
+
async run(stepContext, stepParams) {
|
|
36
|
+
if (!stepParams.runFollowupVerify) {
|
|
37
|
+
return { completed: true, metadata: { skipped: true } };
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
await runNode(verifyBuildNode, stepContext, {
|
|
41
|
+
dockerComposeFile: stepParams.dockerComposeFile,
|
|
42
|
+
labelText: "Running build verification in isolated Docker",
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
if (stepParams.onVerifyBuildFailure) {
|
|
47
|
+
await stepParams.onVerifyBuildFailure(String(error.output ?? ""));
|
|
48
|
+
}
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
return { completed: true };
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export async function runReviewFixFlow(context, params) {
|
|
58
|
+
requireArtifacts(planArtifacts(params.taskKey), "Review-fix mode requires plan artifacts from the planning phase.");
|
|
59
|
+
if (params.latestIteration === null) {
|
|
60
|
+
throw new TaskRunnerError(`Review-fix mode requires at least one review-reply-${params.taskKey}-N.md artifact.`);
|
|
61
|
+
}
|
|
62
|
+
await runFlow(createReviewFixFlowDefinition(params.latestIteration), context, params);
|
|
63
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
2
|
+
import { READY_TO_MERGE_FILE, REVIEW_REPLY_FILE_RE, artifactFile, planArtifacts, requireArtifacts, } from "../../artifacts.js";
|
|
3
|
+
import { REVIEW_REPLY_SUMMARY_PROMPT_TEMPLATE, REVIEW_SUMMARY_PROMPT_TEMPLATE, formatTemplate } from "../../prompts.js";
|
|
4
|
+
import { printPanel } from "../../tui.js";
|
|
5
|
+
import { runFlow } from "../flow-runner.js";
|
|
6
|
+
import { runNode } from "../node-runner.js";
|
|
7
|
+
import { claudeSummaryNode } from "../nodes/claude-summary-node.js";
|
|
8
|
+
import { reviewClaudeNode } from "../nodes/review-claude-node.js";
|
|
9
|
+
import { reviewReplyCodexNode } from "../nodes/review-reply-codex-node.js";
|
|
10
|
+
function nextReviewIterationForTask(taskKey) {
|
|
11
|
+
let maxIndex = 0;
|
|
12
|
+
for (const entry of readdirSync(process.cwd(), { withFileTypes: true })) {
|
|
13
|
+
if (!entry.isFile()) {
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
const match = REVIEW_REPLY_FILE_RE.exec(entry.name);
|
|
17
|
+
if (match && match[1] === taskKey) {
|
|
18
|
+
const current = Number.parseInt(match[2] ?? "0", 10);
|
|
19
|
+
maxIndex = Math.max(maxIndex, current);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return maxIndex + 1;
|
|
23
|
+
}
|
|
24
|
+
export function createReviewFlowDefinition(iteration) {
|
|
25
|
+
return {
|
|
26
|
+
kind: "review-flow",
|
|
27
|
+
version: 1,
|
|
28
|
+
steps: [
|
|
29
|
+
{
|
|
30
|
+
id: "run_claude_review",
|
|
31
|
+
async run(stepContext, stepParams) {
|
|
32
|
+
await runNode(reviewClaudeNode, stepContext, {
|
|
33
|
+
jiraTaskFile: stepParams.jiraTaskFile,
|
|
34
|
+
taskKey: stepParams.taskKey,
|
|
35
|
+
iteration,
|
|
36
|
+
claudeCmd: stepParams.claudeCmd,
|
|
37
|
+
...(stepParams.extraPrompt !== undefined ? { extraPrompt: stepParams.extraPrompt } : {}),
|
|
38
|
+
});
|
|
39
|
+
return { completed: true, metadata: { iteration } };
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: "summarize_review",
|
|
44
|
+
async run(stepContext, stepParams) {
|
|
45
|
+
const reviewFile = artifactFile("review", stepParams.taskKey, iteration);
|
|
46
|
+
const reviewSummaryFile = artifactFile("review-summary", stepParams.taskKey, iteration);
|
|
47
|
+
if (stepContext.dryRun) {
|
|
48
|
+
return { completed: true, metadata: { skipped: true } };
|
|
49
|
+
}
|
|
50
|
+
await runNode(claudeSummaryNode, stepContext, {
|
|
51
|
+
claudeCmd: stepParams.claudeCmd,
|
|
52
|
+
outputFile: reviewSummaryFile,
|
|
53
|
+
prompt: formatTemplate(REVIEW_SUMMARY_PROMPT_TEMPLATE, {
|
|
54
|
+
review_file: reviewFile,
|
|
55
|
+
review_summary_file: reviewSummaryFile,
|
|
56
|
+
}),
|
|
57
|
+
summaryTitle: "Claude Comments",
|
|
58
|
+
verbose: stepContext.verbose,
|
|
59
|
+
});
|
|
60
|
+
return { completed: true };
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: "run_codex_review_reply",
|
|
65
|
+
async run(stepContext, stepParams) {
|
|
66
|
+
await runNode(reviewReplyCodexNode, stepContext, {
|
|
67
|
+
jiraTaskFile: stepParams.jiraTaskFile,
|
|
68
|
+
taskKey: stepParams.taskKey,
|
|
69
|
+
iteration,
|
|
70
|
+
codexCmd: stepParams.codexCmd,
|
|
71
|
+
...(stepParams.extraPrompt !== undefined ? { extraPrompt: stepParams.extraPrompt } : {}),
|
|
72
|
+
});
|
|
73
|
+
return { completed: true };
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: "summarize_review_reply",
|
|
78
|
+
async run(stepContext, stepParams) {
|
|
79
|
+
const reviewReplyFile = artifactFile("review-reply", stepParams.taskKey, iteration);
|
|
80
|
+
const reviewReplySummaryFile = artifactFile("review-reply-summary", stepParams.taskKey, iteration);
|
|
81
|
+
if (stepContext.dryRun) {
|
|
82
|
+
return { completed: true, metadata: { skipped: true } };
|
|
83
|
+
}
|
|
84
|
+
await runNode(claudeSummaryNode, stepContext, {
|
|
85
|
+
claudeCmd: stepParams.claudeCmd,
|
|
86
|
+
outputFile: reviewReplySummaryFile,
|
|
87
|
+
prompt: formatTemplate(REVIEW_REPLY_SUMMARY_PROMPT_TEMPLATE, {
|
|
88
|
+
review_reply_file: reviewReplyFile,
|
|
89
|
+
review_reply_summary_file: reviewReplySummaryFile,
|
|
90
|
+
}),
|
|
91
|
+
summaryTitle: "Codex Reply Summary",
|
|
92
|
+
verbose: stepContext.verbose,
|
|
93
|
+
});
|
|
94
|
+
return { completed: true };
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
id: "check_ready_to_merge",
|
|
99
|
+
async run(stepContext) {
|
|
100
|
+
const readyToMerge = !stepContext.dryRun && existsSync(READY_TO_MERGE_FILE);
|
|
101
|
+
if (readyToMerge) {
|
|
102
|
+
printPanel("Ready To Merge", "Изменения готовы к merge\nФайл ready-to-merge.md создан.", "green");
|
|
103
|
+
}
|
|
104
|
+
return { completed: true, metadata: { readyToMerge } };
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
async function runReviewFlowInternal(context, params, iteration) {
|
|
111
|
+
requireArtifacts(planArtifacts(params.taskKey), "Review mode requires plan artifacts from the planning phase.");
|
|
112
|
+
const result = await runFlow(createReviewFlowDefinition(iteration), context, params);
|
|
113
|
+
const readyToMerge = result.steps.find((step) => step.id === "check_ready_to_merge")?.result.metadata?.readyToMerge === true;
|
|
114
|
+
return { readyToMerge };
|
|
115
|
+
}
|
|
116
|
+
export async function runReviewFlow(context, params) {
|
|
117
|
+
const iteration = nextReviewIterationForTask(params.taskKey);
|
|
118
|
+
const result = await runReviewFlowInternal(context, params, iteration);
|
|
119
|
+
return { readyToMerge: result.readyToMerge, iteration };
|
|
120
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { planArtifacts, requireArtifacts } from "../../artifacts.js";
|
|
2
|
+
import { TEST_FIX_PROMPT_TEMPLATE, TEST_LINTER_FIX_PROMPT_TEMPLATE, formatPrompt } from "../../prompts.js";
|
|
3
|
+
import { runNode } from "../node-runner.js";
|
|
4
|
+
import { codexDockerPromptNode } from "../nodes/codex-docker-prompt-node.js";
|
|
5
|
+
export async function runTestFixFlow(context, params) {
|
|
6
|
+
requireArtifacts(planArtifacts(params.taskKey), `${params.command} mode requires plan artifacts from the planning phase.`);
|
|
7
|
+
const prompt = formatPrompt(params.command === "test-fix" ? TEST_FIX_PROMPT_TEMPLATE : TEST_LINTER_FIX_PROMPT_TEMPLATE, params.extraPrompt);
|
|
8
|
+
await runNode(codexDockerPromptNode, context, {
|
|
9
|
+
dockerComposeFile: params.dockerComposeFile,
|
|
10
|
+
prompt,
|
|
11
|
+
labelText: `Running Codex ${params.command} mode in isolated Docker`,
|
|
12
|
+
});
|
|
13
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { planArtifacts, requireArtifacts } from "../../artifacts.js";
|
|
2
|
+
import { runFlow } from "../flow-runner.js";
|
|
3
|
+
import { runNode } from "../node-runner.js";
|
|
4
|
+
import { verifyBuildNode } from "../nodes/verify-build-node.js";
|
|
5
|
+
export const testFlowDefinition = {
|
|
6
|
+
kind: "test-flow",
|
|
7
|
+
version: 1,
|
|
8
|
+
steps: [
|
|
9
|
+
{
|
|
10
|
+
id: "verify_build",
|
|
11
|
+
async run(context, params) {
|
|
12
|
+
try {
|
|
13
|
+
await runNode(verifyBuildNode, context, {
|
|
14
|
+
dockerComposeFile: params.dockerComposeFile,
|
|
15
|
+
labelText: "Running build verification in isolated Docker",
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
if (params.onVerifyBuildFailure) {
|
|
20
|
+
await params.onVerifyBuildFailure(String(error.output ?? ""));
|
|
21
|
+
}
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
return { completed: true };
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
};
|
|
29
|
+
export async function runTestFlow(context, params) {
|
|
30
|
+
requireArtifacts(planArtifacts(params.taskKey), "Test mode requires plan artifacts from the planning phase.");
|
|
31
|
+
await runFlow(testFlowDefinition, context, params);
|
|
32
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { setCurrentNode } from "../tui.js";
|
|
2
|
+
import { runNodeChecks } from "./checks.js";
|
|
3
|
+
export async function runNode(node, context, params) {
|
|
4
|
+
setCurrentNode(node.kind);
|
|
5
|
+
try {
|
|
6
|
+
const result = await node.run(context, params);
|
|
7
|
+
const checks = node.checks?.(context, params, result) ?? [];
|
|
8
|
+
runNodeChecks(checks);
|
|
9
|
+
return result;
|
|
10
|
+
}
|
|
11
|
+
finally {
|
|
12
|
+
setCurrentNode(null);
|
|
13
|
+
}
|
|
14
|
+
}
|