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.
Files changed (50) hide show
  1. package/README.md +24 -0
  2. package/dist/executors/claude-executor.js +36 -0
  3. package/dist/executors/claude-summary-executor.js +31 -0
  4. package/dist/executors/codex-docker-executor.js +27 -0
  5. package/dist/executors/codex-local-executor.js +25 -0
  6. package/dist/executors/command-check-executor.js +14 -0
  7. package/dist/executors/configs/claude-config.js +11 -0
  8. package/dist/executors/configs/claude-summary-config.js +8 -0
  9. package/dist/executors/configs/codex-docker-config.js +10 -0
  10. package/dist/executors/configs/codex-local-config.js +8 -0
  11. package/dist/executors/configs/jira-fetch-config.js +4 -0
  12. package/dist/executors/configs/process-config.js +3 -0
  13. package/dist/executors/configs/verify-build-config.js +7 -0
  14. package/dist/executors/jira-fetch-executor.js +11 -0
  15. package/dist/executors/process-executor.js +21 -0
  16. package/dist/executors/types.js +1 -0
  17. package/dist/executors/verify-build-executor.js +22 -0
  18. package/dist/index.js +270 -450
  19. package/dist/interactive-ui.js +109 -12
  20. package/dist/pipeline/build-failure-summary.js +6 -0
  21. package/dist/pipeline/checks.js +15 -0
  22. package/dist/pipeline/context.js +17 -0
  23. package/dist/pipeline/flow-runner.js +13 -0
  24. package/dist/pipeline/flow-types.js +1 -0
  25. package/dist/pipeline/flows/implement-flow.js +48 -0
  26. package/dist/pipeline/flows/plan-flow.js +42 -0
  27. package/dist/pipeline/flows/preflight-flow.js +59 -0
  28. package/dist/pipeline/flows/review-fix-flow.js +63 -0
  29. package/dist/pipeline/flows/review-flow.js +120 -0
  30. package/dist/pipeline/flows/test-fix-flow.js +13 -0
  31. package/dist/pipeline/flows/test-flow.js +32 -0
  32. package/dist/pipeline/node-runner.js +14 -0
  33. package/dist/pipeline/nodes/build-failure-summary-node.js +71 -0
  34. package/dist/pipeline/nodes/claude-summary-node.js +32 -0
  35. package/dist/pipeline/nodes/codex-docker-prompt-node.js +31 -0
  36. package/dist/pipeline/nodes/command-check-node.js +10 -0
  37. package/dist/pipeline/nodes/implement-codex-node.js +16 -0
  38. package/dist/pipeline/nodes/jira-fetch-node.js +25 -0
  39. package/dist/pipeline/nodes/plan-codex-node.js +32 -0
  40. package/dist/pipeline/nodes/review-claude-node.js +38 -0
  41. package/dist/pipeline/nodes/review-reply-codex-node.js +40 -0
  42. package/dist/pipeline/nodes/task-summary-node.js +36 -0
  43. package/dist/pipeline/nodes/verify-build-node.js +14 -0
  44. package/dist/pipeline/registry.js +25 -0
  45. package/dist/pipeline/types.js +10 -0
  46. package/dist/runtime/command-resolution.js +139 -0
  47. package/dist/runtime/docker-runtime.js +51 -0
  48. package/dist/runtime/process-runner.js +111 -0
  49. package/dist/tui.js +34 -0
  50. package/package.json +2 -1
@@ -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
- height: "100%-7",
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: 9,
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: 12,
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.header.setContent(`{bold}AgentWeaver{/bold} {green-fg}${this.options.issueKey}{/green-fg}\n` +
351
- `cwd: ${this.options.cwd} current: ${this.currentCommand}`);
352
- const summaryBody = this.options.summaryText.trim()
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}\n` +
406
- `current: ${this.currentCommand}${busy ? " {yellow-fg}[running]{/yellow-fg}" : ""}`);
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
+ }