agentweaver 0.1.2 → 0.1.4
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 +58 -23
- package/dist/artifacts.js +58 -2
- package/dist/executors/claude-executor.js +12 -2
- package/dist/executors/claude-summary-executor.js +1 -1
- package/dist/executors/codex-docker-executor.js +1 -1
- package/dist/executors/codex-local-executor.js +1 -1
- package/dist/executors/configs/claude-config.js +2 -1
- package/dist/executors/verify-build-executor.js +110 -9
- package/dist/index.js +466 -452
- package/dist/interactive-ui.js +538 -194
- package/dist/jira.js +3 -1
- package/dist/pipeline/auto-flow.js +9 -0
- package/dist/pipeline/checks.js +5 -0
- package/dist/pipeline/context.js +2 -0
- package/dist/pipeline/declarative-flow-runner.js +262 -0
- package/dist/pipeline/declarative-flows.js +24 -0
- package/dist/pipeline/flow-specs/auto.json +485 -0
- package/dist/pipeline/flow-specs/bug-analyze.json +140 -0
- package/dist/pipeline/flow-specs/bug-fix.json +44 -0
- package/dist/pipeline/flow-specs/implement.json +47 -0
- package/dist/pipeline/flow-specs/mr-description.json +61 -0
- package/dist/pipeline/flow-specs/plan.json +88 -0
- package/dist/pipeline/flow-specs/preflight.json +174 -0
- package/dist/pipeline/flow-specs/review-fix.json +76 -0
- package/dist/pipeline/flow-specs/review.json +233 -0
- package/dist/pipeline/flow-specs/run-linter-loop.json +149 -0
- package/dist/pipeline/flow-specs/run-tests-loop.json +149 -0
- package/dist/pipeline/flow-specs/task-describe.json +61 -0
- package/dist/pipeline/flow-specs/test-fix.json +24 -0
- package/dist/pipeline/flow-specs/test-linter-fix.json +24 -0
- package/dist/pipeline/flow-specs/test.json +19 -0
- package/dist/pipeline/flows/implement-flow.js +3 -4
- package/dist/pipeline/flows/preflight-flow.js +17 -57
- package/dist/pipeline/flows/review-fix-flow.js +3 -4
- package/dist/pipeline/flows/review-flow.js +8 -4
- package/dist/pipeline/flows/test-fix-flow.js +3 -4
- package/dist/pipeline/node-registry.js +74 -0
- package/dist/pipeline/node-runner.js +9 -3
- package/dist/pipeline/nodes/build-failure-summary-node.js +4 -4
- package/dist/pipeline/nodes/claude-prompt-node.js +54 -0
- package/dist/pipeline/nodes/claude-summary-node.js +12 -6
- package/dist/pipeline/nodes/codex-docker-prompt-node.js +1 -0
- package/dist/pipeline/nodes/codex-local-prompt-node.js +32 -0
- package/dist/pipeline/nodes/file-check-node.js +15 -0
- package/dist/pipeline/nodes/flow-run-node.js +40 -0
- package/dist/pipeline/nodes/summary-file-load-node.js +16 -0
- package/dist/pipeline/nodes/task-summary-node.js +12 -6
- package/dist/pipeline/nodes/verify-build-node.js +1 -0
- package/dist/pipeline/prompt-registry.js +27 -0
- package/dist/pipeline/prompt-runtime.js +18 -0
- package/dist/pipeline/registry.js +0 -2
- package/dist/pipeline/spec-compiler.js +213 -0
- package/dist/pipeline/spec-loader.js +14 -0
- package/dist/pipeline/spec-types.js +1 -0
- package/dist/pipeline/spec-validator.js +302 -0
- package/dist/pipeline/value-resolver.js +217 -0
- package/dist/prompts.js +22 -3
- package/dist/runtime/process-runner.js +24 -23
- package/dist/structured-artifacts.js +178 -0
- package/dist/tui.js +39 -0
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,46 +1,42 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { existsSync,
|
|
3
|
-
import { appendFile, readFile } from "node:fs/promises";
|
|
4
|
-
import os from "node:os";
|
|
2
|
+
import { existsSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
5
3
|
import path from "node:path";
|
|
6
4
|
import process from "node:process";
|
|
7
5
|
import { fileURLToPath } from "node:url";
|
|
8
|
-
import { REVIEW_FILE_RE, REVIEW_REPLY_FILE_RE,
|
|
6
|
+
import { REVIEW_FILE_RE, REVIEW_REPLY_FILE_RE, autoStateFile, bugAnalyzeArtifacts, bugAnalyzeJsonFile, bugFixDesignJsonFile, bugFixPlanJsonFile, ensureTaskWorkspaceDir, jiraTaskFile, planArtifacts, readyToMergeFile, requireArtifacts, taskWorkspaceDir, taskSummaryFile, } from "./artifacts.js";
|
|
9
7
|
import { TaskRunnerError } from "./errors.js";
|
|
10
8
|
import { buildJiraApiUrl, buildJiraBrowseUrl, extractIssueKey, requireJiraTaskFile } from "./jira.js";
|
|
11
|
-
import {
|
|
9
|
+
import { validateStructuredArtifacts } from "./structured-artifacts.js";
|
|
12
10
|
import { summarizeBuildFailure as summarizeBuildFailureViaPipeline } from "./pipeline/build-failure-summary.js";
|
|
13
11
|
import { createPipelineContext } from "./pipeline/context.js";
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
12
|
+
import { loadAutoFlow } from "./pipeline/auto-flow.js";
|
|
13
|
+
import { loadDeclarativeFlow } from "./pipeline/declarative-flows.js";
|
|
14
|
+
import { findPhaseById, runExpandedPhase } from "./pipeline/declarative-flow-runner.js";
|
|
17
15
|
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
16
|
import { resolveCmd, resolveDockerComposeCmd } from "./runtime/command-resolution.js";
|
|
23
17
|
import { defaultDockerComposeFile, dockerRuntimeEnv } from "./runtime/docker-runtime.js";
|
|
24
18
|
import { runCommand } from "./runtime/process-runner.js";
|
|
25
19
|
import { InteractiveUi } from "./interactive-ui.js";
|
|
26
|
-
import { bye, printError, printInfo, printPanel, printSummary } from "./tui.js";
|
|
20
|
+
import { bye, printError, printInfo, printPanel, printSummary, setFlowExecutionState } from "./tui.js";
|
|
27
21
|
const COMMANDS = [
|
|
22
|
+
"bug-analyze",
|
|
23
|
+
"bug-fix",
|
|
24
|
+
"mr-description",
|
|
28
25
|
"plan",
|
|
26
|
+
"task-describe",
|
|
29
27
|
"implement",
|
|
30
28
|
"review",
|
|
31
29
|
"review-fix",
|
|
32
30
|
"test",
|
|
33
31
|
"test-fix",
|
|
34
32
|
"test-linter-fix",
|
|
33
|
+
"run-tests-loop",
|
|
34
|
+
"run-linter-loop",
|
|
35
35
|
"auto",
|
|
36
36
|
"auto-status",
|
|
37
37
|
"auto-reset",
|
|
38
38
|
];
|
|
39
|
-
const
|
|
40
|
-
const DEFAULT_CLAUDE_REVIEW_MODEL = "opus";
|
|
41
|
-
const HISTORY_FILE = path.join(os.homedir(), ".codex", "memories", "agentweaver-history");
|
|
42
|
-
const AUTO_STATE_SCHEMA_VERSION = 1;
|
|
43
|
-
const AUTO_MAX_REVIEW_ITERATIONS = 3;
|
|
39
|
+
const AUTO_STATE_SCHEMA_VERSION = 3;
|
|
44
40
|
const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
45
41
|
const PACKAGE_ROOT = path.resolve(MODULE_DIR, "..");
|
|
46
42
|
const runtimeServices = {
|
|
@@ -53,13 +49,19 @@ function usage() {
|
|
|
53
49
|
return `Usage:
|
|
54
50
|
agentweaver <jira-browse-url|jira-issue-key>
|
|
55
51
|
agentweaver --force <jira-browse-url|jira-issue-key>
|
|
52
|
+
agentweaver bug-analyze [--dry] [--verbose] [--prompt <text>] <jira-browse-url|jira-issue-key>
|
|
53
|
+
agentweaver bug-fix [--dry] [--verbose] [--prompt <text>] <jira-browse-url|jira-issue-key>
|
|
54
|
+
agentweaver mr-description [--dry] [--verbose] [--prompt <text>] <jira-browse-url|jira-issue-key>
|
|
56
55
|
agentweaver plan [--dry] [--verbose] [--prompt <text>] <jira-browse-url|jira-issue-key>
|
|
56
|
+
agentweaver task-describe [--dry] [--verbose] [--prompt <text>] <jira-browse-url|jira-issue-key>
|
|
57
57
|
agentweaver implement [--dry] [--verbose] [--prompt <text>] <jira-browse-url|jira-issue-key>
|
|
58
58
|
agentweaver review [--dry] [--verbose] [--prompt <text>] <jira-browse-url|jira-issue-key>
|
|
59
59
|
agentweaver review-fix [--dry] [--verbose] [--prompt <text>] <jira-browse-url|jira-issue-key>
|
|
60
60
|
agentweaver test [--dry] [--verbose] <jira-browse-url|jira-issue-key>
|
|
61
61
|
agentweaver test-fix [--dry] [--verbose] [--prompt <text>] <jira-browse-url|jira-issue-key>
|
|
62
62
|
agentweaver test-linter-fix [--dry] [--verbose] [--prompt <text>] <jira-browse-url|jira-issue-key>
|
|
63
|
+
agentweaver run-tests-loop [--dry] [--verbose] [--prompt <text>] <jira-browse-url|jira-issue-key>
|
|
64
|
+
agentweaver run-linter-loop [--dry] [--verbose] [--prompt <text>] <jira-browse-url|jira-issue-key>
|
|
63
65
|
agentweaver auto [--dry] [--verbose] [--prompt <text>] <jira-browse-url|jira-issue-key>
|
|
64
66
|
agentweaver auto [--dry] [--verbose] [--prompt <text>] --from <phase> <jira-browse-url|jira-issue-key>
|
|
65
67
|
agentweaver auto --help-phases
|
|
@@ -67,17 +69,18 @@ function usage() {
|
|
|
67
69
|
agentweaver auto-reset <jira-browse-url|jira-issue-key>
|
|
68
70
|
|
|
69
71
|
Interactive Mode:
|
|
70
|
-
When started with only a Jira task, the script opens an interactive
|
|
71
|
-
|
|
72
|
+
When started with only a Jira task, the script opens an interactive UI.
|
|
73
|
+
Use Up/Down to select a flow, Enter to confirm launch, h for help, q to exit.
|
|
72
74
|
|
|
73
75
|
Flags:
|
|
76
|
+
--version Show package version
|
|
74
77
|
--force In interactive mode, force refresh Jira task and task summary
|
|
75
78
|
--dry Fetch Jira task, but print docker/codex/claude commands instead of executing them
|
|
76
79
|
--verbose Show live stdout/stderr of launched commands
|
|
77
80
|
--prompt Extra prompt text appended to the base prompt
|
|
78
81
|
|
|
79
82
|
Required environment variables:
|
|
80
|
-
JIRA_API_KEY Jira API key used in Authorization: Bearer <token> for
|
|
83
|
+
JIRA_API_KEY Jira API key used in Authorization: Bearer <token> for Jira-backed flows
|
|
81
84
|
|
|
82
85
|
Optional environment variables:
|
|
83
86
|
JIRA_BASE_URL
|
|
@@ -86,8 +89,15 @@ Optional environment variables:
|
|
|
86
89
|
CODEX_BIN
|
|
87
90
|
CODEX_MODEL
|
|
88
91
|
CLAUDE_BIN
|
|
89
|
-
|
|
90
|
-
|
|
92
|
+
CLAUDE_MODEL`;
|
|
93
|
+
}
|
|
94
|
+
function packageVersion() {
|
|
95
|
+
const packageJsonPath = path.join(PACKAGE_ROOT, "package.json");
|
|
96
|
+
const raw = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
97
|
+
if (typeof raw.version !== "string" || !raw.version.trim()) {
|
|
98
|
+
throw new TaskRunnerError(`Package version is missing in ${packageJsonPath}`);
|
|
99
|
+
}
|
|
100
|
+
return raw.version;
|
|
91
101
|
}
|
|
92
102
|
function nowIso8601() {
|
|
93
103
|
return new Date().toISOString();
|
|
@@ -95,24 +105,14 @@ function nowIso8601() {
|
|
|
95
105
|
function normalizeAutoPhaseId(phaseId) {
|
|
96
106
|
return phaseId.trim().toLowerCase().replaceAll("-", "_");
|
|
97
107
|
}
|
|
98
|
-
function buildAutoSteps(
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
];
|
|
104
|
-
for (let iteration = 1; iteration <= maxReviewIterations; iteration += 1) {
|
|
105
|
-
steps.push({ id: `review_${iteration}`, command: "review", status: "pending", reviewIteration: iteration }, { id: `review_fix_${iteration}`, command: "review-fix", status: "pending", reviewIteration: iteration }, {
|
|
106
|
-
id: `test_after_review_fix_${iteration}`,
|
|
107
|
-
command: "test",
|
|
108
|
-
status: "pending",
|
|
109
|
-
reviewIteration: iteration,
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
return steps;
|
|
108
|
+
function buildAutoSteps() {
|
|
109
|
+
return loadAutoFlow().phases.map((phase) => ({
|
|
110
|
+
id: phase.id,
|
|
111
|
+
status: "pending",
|
|
112
|
+
}));
|
|
113
113
|
}
|
|
114
|
-
function autoPhaseIds(
|
|
115
|
-
return buildAutoSteps(
|
|
114
|
+
function autoPhaseIds() {
|
|
115
|
+
return buildAutoSteps().map((step) => step.id);
|
|
116
116
|
}
|
|
117
117
|
function validateAutoPhaseId(phaseId) {
|
|
118
118
|
const normalized = normalizeAutoPhaseId(phaseId);
|
|
@@ -121,19 +121,46 @@ function validateAutoPhaseId(phaseId) {
|
|
|
121
121
|
}
|
|
122
122
|
return normalized;
|
|
123
123
|
}
|
|
124
|
-
function autoStateFile(taskKey) {
|
|
125
|
-
return path.join(process.cwd(), `.agentweaver-state-${taskKey}.json`);
|
|
126
|
-
}
|
|
127
124
|
function createAutoPipelineState(config) {
|
|
125
|
+
const autoFlow = loadAutoFlow();
|
|
126
|
+
const maxReviewIterations = autoFlow.phases.filter((phase) => /^review_\d+$/.test(phase.id)).length;
|
|
128
127
|
return {
|
|
129
128
|
schemaVersion: AUTO_STATE_SCHEMA_VERSION,
|
|
130
129
|
issueKey: config.taskKey,
|
|
131
130
|
jiraRef: config.jiraRef,
|
|
132
131
|
status: "pending",
|
|
133
132
|
currentStep: null,
|
|
134
|
-
maxReviewIterations
|
|
133
|
+
maxReviewIterations,
|
|
135
134
|
updatedAt: nowIso8601(),
|
|
136
135
|
steps: buildAutoSteps(),
|
|
136
|
+
executionState: {
|
|
137
|
+
flowKind: autoFlow.kind,
|
|
138
|
+
flowVersion: autoFlow.version,
|
|
139
|
+
terminated: false,
|
|
140
|
+
phases: [],
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function stripExecutionStatePayload(executionState) {
|
|
145
|
+
return {
|
|
146
|
+
flowKind: executionState.flowKind,
|
|
147
|
+
flowVersion: executionState.flowVersion,
|
|
148
|
+
terminated: executionState.terminated,
|
|
149
|
+
...(executionState.terminationReason ? { terminationReason: executionState.terminationReason } : {}),
|
|
150
|
+
phases: executionState.phases.map((phase) => ({
|
|
151
|
+
id: phase.id,
|
|
152
|
+
status: phase.status,
|
|
153
|
+
repeatVars: { ...phase.repeatVars },
|
|
154
|
+
...(phase.startedAt ? { startedAt: phase.startedAt } : {}),
|
|
155
|
+
...(phase.finishedAt ? { finishedAt: phase.finishedAt } : {}),
|
|
156
|
+
steps: phase.steps.map((step) => ({
|
|
157
|
+
id: step.id,
|
|
158
|
+
status: step.status,
|
|
159
|
+
...(step.startedAt ? { startedAt: step.startedAt } : {}),
|
|
160
|
+
...(step.finishedAt ? { finishedAt: step.finishedAt } : {}),
|
|
161
|
+
...(step.stopFlow !== undefined ? { stopFlow: step.stopFlow } : {}),
|
|
162
|
+
})),
|
|
163
|
+
})),
|
|
137
164
|
};
|
|
138
165
|
}
|
|
139
166
|
function loadAutoPipelineState(config) {
|
|
@@ -155,11 +182,29 @@ function loadAutoPipelineState(config) {
|
|
|
155
182
|
if (state.schemaVersion !== AUTO_STATE_SCHEMA_VERSION) {
|
|
156
183
|
throw new TaskRunnerError(`Unsupported auto state schema in ${filePath}: ${state.schemaVersion}`);
|
|
157
184
|
}
|
|
185
|
+
if (!state.executionState) {
|
|
186
|
+
const autoFlow = loadAutoFlow();
|
|
187
|
+
state.executionState = {
|
|
188
|
+
flowKind: autoFlow.kind,
|
|
189
|
+
flowVersion: autoFlow.version,
|
|
190
|
+
terminated: false,
|
|
191
|
+
phases: [],
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
syncAutoStepsFromExecutionState(state);
|
|
158
195
|
return state;
|
|
159
196
|
}
|
|
160
197
|
function saveAutoPipelineState(state) {
|
|
161
198
|
state.updatedAt = nowIso8601();
|
|
162
|
-
|
|
199
|
+
ensureTaskWorkspaceDir(state.issueKey);
|
|
200
|
+
writeFileSync(autoStateFile(state.issueKey), `${JSON.stringify({
|
|
201
|
+
...state,
|
|
202
|
+
executionState: stripExecutionStatePayload(state.executionState),
|
|
203
|
+
}, null, 2)}\n`, "utf8");
|
|
204
|
+
}
|
|
205
|
+
function syncAndSaveAutoPipelineState(state) {
|
|
206
|
+
syncAutoStepsFromExecutionState(state);
|
|
207
|
+
saveAutoPipelineState(state);
|
|
163
208
|
}
|
|
164
209
|
function resetAutoPipelineState(config) {
|
|
165
210
|
const filePath = autoStateFile(config.taskKey);
|
|
@@ -172,28 +217,42 @@ function resetAutoPipelineState(config) {
|
|
|
172
217
|
function nextAutoStep(state) {
|
|
173
218
|
return state.steps.find((step) => ["running", "failed", "pending"].includes(step.status)) ?? null;
|
|
174
219
|
}
|
|
175
|
-
function
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
}
|
|
180
|
-
function skipAutoStepsAfterReadyToMerge(state, currentStepId) {
|
|
181
|
-
let seenCurrent = false;
|
|
182
|
-
for (const step of state.steps) {
|
|
183
|
-
if (!seenCurrent) {
|
|
184
|
-
seenCurrent = step.id === currentStepId;
|
|
185
|
-
continue;
|
|
186
|
-
}
|
|
187
|
-
if (step.status === "pending") {
|
|
188
|
-
markAutoStepSkipped(step, "ready-to-merge detected");
|
|
220
|
+
function findCurrentExecutionStep(state) {
|
|
221
|
+
for (const phase of state.executionState.phases) {
|
|
222
|
+
const runningStep = phase.steps.find((step) => step.status === "running");
|
|
223
|
+
if (runningStep) {
|
|
224
|
+
return `${phase.id}:${runningStep.id}`;
|
|
189
225
|
}
|
|
190
226
|
}
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
function deriveAutoPipelineStatus(state) {
|
|
230
|
+
if (state.lastError || state.steps.some((candidate) => candidate.status === "failed")) {
|
|
231
|
+
return "blocked";
|
|
232
|
+
}
|
|
233
|
+
if (state.executionState.terminated) {
|
|
234
|
+
return "completed";
|
|
235
|
+
}
|
|
236
|
+
if (state.steps.some((candidate) => candidate.status === "running")) {
|
|
237
|
+
return "running";
|
|
238
|
+
}
|
|
239
|
+
if (state.steps.some((candidate) => candidate.status === "pending")) {
|
|
240
|
+
return "pending";
|
|
241
|
+
}
|
|
242
|
+
if (state.steps.some((candidate) => candidate.status === "skipped")) {
|
|
243
|
+
return "completed";
|
|
244
|
+
}
|
|
245
|
+
if (state.steps.every((candidate) => candidate.status === "done")) {
|
|
246
|
+
return "completed";
|
|
247
|
+
}
|
|
248
|
+
return state.status;
|
|
191
249
|
}
|
|
192
250
|
function printAutoState(state) {
|
|
251
|
+
const currentStep = findCurrentExecutionStep(state) ?? state.currentStep ?? "-";
|
|
193
252
|
const lines = [
|
|
194
253
|
`Issue: ${state.issueKey}`,
|
|
195
|
-
`Status: ${state
|
|
196
|
-
`Current step: ${
|
|
254
|
+
`Status: ${deriveAutoPipelineStatus(state)}`,
|
|
255
|
+
`Current step: ${currentStep}`,
|
|
197
256
|
`Updated: ${state.updatedAt}`,
|
|
198
257
|
];
|
|
199
258
|
if (state.lastError) {
|
|
@@ -202,14 +261,42 @@ function printAutoState(state) {
|
|
|
202
261
|
lines.push("");
|
|
203
262
|
for (const step of state.steps) {
|
|
204
263
|
lines.push(`[${step.status}] ${step.id}${step.note ? ` (${step.note})` : ""}`);
|
|
264
|
+
const phaseState = state.executionState.phases.find((candidate) => candidate.id === step.id);
|
|
265
|
+
for (const childStep of phaseState?.steps ?? []) {
|
|
266
|
+
lines.push(` - [${childStep.status}] ${childStep.id}`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (state.executionState.terminated) {
|
|
270
|
+
lines.push("", `Execution terminated: ${state.executionState.terminationReason ?? "yes"}`);
|
|
205
271
|
}
|
|
206
272
|
printPanel("Auto Status", lines.join("\n"), "cyan");
|
|
207
273
|
}
|
|
208
|
-
function
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
274
|
+
function syncAutoStepsFromExecutionState(state) {
|
|
275
|
+
for (const step of state.steps) {
|
|
276
|
+
const phaseState = state.executionState.phases.find((candidate) => candidate.id === step.id);
|
|
277
|
+
if (!phaseState) {
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
step.status = phaseState.status;
|
|
281
|
+
step.startedAt = phaseState.startedAt ?? null;
|
|
282
|
+
step.finishedAt = phaseState.finishedAt ?? null;
|
|
283
|
+
step.note = null;
|
|
284
|
+
if (phaseState.status === "skipped") {
|
|
285
|
+
step.note = "condition not met";
|
|
286
|
+
step.returnCode ??= 0;
|
|
287
|
+
}
|
|
288
|
+
else if (phaseState.status === "done") {
|
|
289
|
+
step.returnCode ??= 0;
|
|
290
|
+
if (state.executionState.terminated && state.executionState.terminationReason?.startsWith(`Stopped by ${step.id}:`)) {
|
|
291
|
+
step.note = "stop condition met";
|
|
292
|
+
}
|
|
293
|
+
}
|
|
212
294
|
}
|
|
295
|
+
state.currentStep = findCurrentExecutionStep(state);
|
|
296
|
+
state.status = deriveAutoPipelineStatus(state);
|
|
297
|
+
}
|
|
298
|
+
function printAutoPhasesHelp() {
|
|
299
|
+
const phaseLines = ["Available auto phases:", "", ...autoPhaseIds()];
|
|
213
300
|
phaseLines.push("", "You can resume auto from a phase with:", "agentweaver auto --from <phase> <jira>", "or in interactive mode:", "/auto --from <phase>");
|
|
214
301
|
printPanel("Auto Phases", phaseLines.join("\n"), "magenta");
|
|
215
302
|
}
|
|
@@ -243,7 +330,11 @@ function loadEnvFile(envFilePath) {
|
|
|
243
330
|
}
|
|
244
331
|
function nextReviewIterationForTask(taskKey) {
|
|
245
332
|
let maxIndex = 0;
|
|
246
|
-
|
|
333
|
+
const workspaceDir = taskWorkspaceDir(taskKey);
|
|
334
|
+
if (!existsSync(workspaceDir)) {
|
|
335
|
+
return 1;
|
|
336
|
+
}
|
|
337
|
+
for (const entry of readdirSync(workspaceDir, { withFileTypes: true })) {
|
|
247
338
|
if (!entry.isFile()) {
|
|
248
339
|
continue;
|
|
249
340
|
}
|
|
@@ -256,7 +347,11 @@ function nextReviewIterationForTask(taskKey) {
|
|
|
256
347
|
}
|
|
257
348
|
function latestReviewReplyIteration(taskKey) {
|
|
258
349
|
let maxIndex = null;
|
|
259
|
-
|
|
350
|
+
const workspaceDir = taskWorkspaceDir(taskKey);
|
|
351
|
+
if (!existsSync(workspaceDir)) {
|
|
352
|
+
return null;
|
|
353
|
+
}
|
|
354
|
+
for (const entry of readdirSync(workspaceDir, { withFileTypes: true })) {
|
|
260
355
|
if (!entry.isFile()) {
|
|
261
356
|
continue;
|
|
262
357
|
}
|
|
@@ -270,6 +365,7 @@ function latestReviewReplyIteration(taskKey) {
|
|
|
270
365
|
}
|
|
271
366
|
function buildConfig(command, jiraRef, options = {}) {
|
|
272
367
|
const jiraIssueKey = extractIssueKey(jiraRef);
|
|
368
|
+
ensureTaskWorkspaceDir(jiraIssueKey);
|
|
273
369
|
return {
|
|
274
370
|
command,
|
|
275
371
|
jiraRef,
|
|
@@ -279,51 +375,101 @@ function buildConfig(command, jiraRef, options = {}) {
|
|
|
279
375
|
dryRun: options.dryRun ?? false,
|
|
280
376
|
verbose: options.verbose ?? false,
|
|
281
377
|
dockerComposeFile: defaultDockerComposeFile(PACKAGE_ROOT),
|
|
282
|
-
codexCmd: process.env.CODEX_BIN ?? "codex",
|
|
283
|
-
claudeCmd: process.env.CLAUDE_BIN ?? "claude",
|
|
284
378
|
jiraIssueKey,
|
|
285
379
|
taskKey: jiraIssueKey,
|
|
286
380
|
jiraBrowseUrl: buildJiraBrowseUrl(jiraRef),
|
|
287
381
|
jiraApiUrl: buildJiraApiUrl(jiraRef),
|
|
288
|
-
jiraTaskFile:
|
|
382
|
+
jiraTaskFile: jiraTaskFile(jiraIssueKey),
|
|
289
383
|
};
|
|
290
384
|
}
|
|
291
385
|
function checkPrerequisites(config) {
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
386
|
+
if (config.command === "bug-analyze" ||
|
|
387
|
+
config.command === "bug-fix" ||
|
|
388
|
+
config.command === "mr-description" ||
|
|
389
|
+
config.command === "plan" ||
|
|
390
|
+
config.command === "task-describe" ||
|
|
391
|
+
config.command === "review" ||
|
|
392
|
+
config.command === "run-tests-loop" ||
|
|
393
|
+
config.command === "run-linter-loop") {
|
|
394
|
+
resolveCmd("codex", "CODEX_BIN");
|
|
296
395
|
}
|
|
297
396
|
if (config.command === "review") {
|
|
298
|
-
|
|
397
|
+
resolveCmd("claude", "CLAUDE_BIN");
|
|
299
398
|
}
|
|
300
|
-
if (["implement", "review-fix", "test", "
|
|
399
|
+
if (["implement", "review-fix", "test", "run-tests-loop", "run-linter-loop"].includes(config.command)) {
|
|
301
400
|
resolveDockerComposeCmd();
|
|
302
401
|
if (!existsSync(config.dockerComposeFile)) {
|
|
303
402
|
throw new TaskRunnerError(`docker-compose file not found: ${config.dockerComposeFile}`);
|
|
304
403
|
}
|
|
305
404
|
}
|
|
306
|
-
return { codexCmd, claudeCmd };
|
|
307
405
|
}
|
|
308
|
-
function
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
if (!
|
|
313
|
-
|
|
406
|
+
function checkAutoPrerequisites(config) {
|
|
407
|
+
resolveCmd("codex", "CODEX_BIN");
|
|
408
|
+
resolveCmd("claude", "CLAUDE_BIN");
|
|
409
|
+
resolveDockerComposeCmd();
|
|
410
|
+
if (!existsSync(config.dockerComposeFile)) {
|
|
411
|
+
throw new TaskRunnerError(`docker-compose file not found: ${config.dockerComposeFile}`);
|
|
314
412
|
}
|
|
315
|
-
return `${basePrompt.trim()}\n${suffix}`;
|
|
316
413
|
}
|
|
317
|
-
function
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
414
|
+
function autoFlowParams(config) {
|
|
415
|
+
return {
|
|
416
|
+
jiraApiUrl: config.jiraApiUrl,
|
|
417
|
+
taskKey: config.taskKey,
|
|
418
|
+
dockerComposeFile: config.dockerComposeFile,
|
|
419
|
+
extraPrompt: config.extraPrompt,
|
|
420
|
+
reviewFixPoints: config.reviewFixPoints,
|
|
421
|
+
};
|
|
325
422
|
}
|
|
326
|
-
|
|
423
|
+
function declarativeFlowDefinition(id, label, fileName) {
|
|
424
|
+
const flow = loadDeclarativeFlow(fileName);
|
|
425
|
+
return {
|
|
426
|
+
id,
|
|
427
|
+
label,
|
|
428
|
+
phases: flow.phases.map((phase) => ({
|
|
429
|
+
id: phase.id,
|
|
430
|
+
repeatVars: Object.fromEntries(Object.entries(phase.repeatVars).map(([key, value]) => [key, value])),
|
|
431
|
+
steps: phase.steps.map((step) => ({
|
|
432
|
+
id: step.id,
|
|
433
|
+
})),
|
|
434
|
+
})),
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
function autoFlowDefinition() {
|
|
438
|
+
const flow = loadAutoFlow();
|
|
439
|
+
return {
|
|
440
|
+
id: "auto",
|
|
441
|
+
label: "auto",
|
|
442
|
+
phases: flow.phases.map((phase) => ({
|
|
443
|
+
id: phase.id,
|
|
444
|
+
repeatVars: Object.fromEntries(Object.entries(phase.repeatVars).map(([key, value]) => [key, value])),
|
|
445
|
+
steps: phase.steps.map((step) => ({
|
|
446
|
+
id: step.id,
|
|
447
|
+
})),
|
|
448
|
+
})),
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
function interactiveFlowDefinitions() {
|
|
452
|
+
return [
|
|
453
|
+
autoFlowDefinition(),
|
|
454
|
+
declarativeFlowDefinition("bug-analyze", "bug-analyze", "bug-analyze.json"),
|
|
455
|
+
declarativeFlowDefinition("bug-fix", "bug-fix", "bug-fix.json"),
|
|
456
|
+
declarativeFlowDefinition("mr-description", "mr-description", "mr-description.json"),
|
|
457
|
+
declarativeFlowDefinition("plan", "plan", "plan.json"),
|
|
458
|
+
declarativeFlowDefinition("task-describe", "task-describe", "task-describe.json"),
|
|
459
|
+
declarativeFlowDefinition("implement", "implement", "implement.json"),
|
|
460
|
+
declarativeFlowDefinition("review", "review", "review.json"),
|
|
461
|
+
declarativeFlowDefinition("review-fix", "review-fix", "review-fix.json"),
|
|
462
|
+
declarativeFlowDefinition("test", "test", "test.json"),
|
|
463
|
+
declarativeFlowDefinition("test-fix", "test-fix", "test-fix.json"),
|
|
464
|
+
declarativeFlowDefinition("test-linter-fix", "test-linter-fix", "test-linter-fix.json"),
|
|
465
|
+
declarativeFlowDefinition("run-tests-loop", "run-tests-loop", "run-tests-loop.json"),
|
|
466
|
+
declarativeFlowDefinition("run-linter-loop", "run-linter-loop", "run-linter-loop.json"),
|
|
467
|
+
];
|
|
468
|
+
}
|
|
469
|
+
function publishFlowState(flowId, executionState) {
|
|
470
|
+
setFlowExecutionState(flowId, stripExecutionStatePayload(executionState));
|
|
471
|
+
}
|
|
472
|
+
async function runDeclarativeFlowBySpecFile(fileName, config, flowParams) {
|
|
327
473
|
const context = createPipelineContext({
|
|
328
474
|
issueKey: config.taskKey,
|
|
329
475
|
jiraRef: config.jiraRef,
|
|
@@ -331,90 +477,68 @@ async function runAutoStepViaFlow(config, step, codexCmd, claudeCmd, state) {
|
|
|
331
477
|
verbose: config.verbose,
|
|
332
478
|
runtime: runtimeServices,
|
|
333
479
|
});
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
|
|
480
|
+
const flow = loadDeclarativeFlow(fileName);
|
|
481
|
+
const executionState = {
|
|
482
|
+
flowKind: flow.kind,
|
|
483
|
+
flowVersion: flow.version,
|
|
484
|
+
terminated: false,
|
|
485
|
+
phases: [],
|
|
337
486
|
};
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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;
|
|
487
|
+
publishFlowState(config.command, executionState);
|
|
488
|
+
for (const phase of flow.phases) {
|
|
489
|
+
await runExpandedPhase(phase, context, flowParams, flow.constants, {
|
|
490
|
+
executionState,
|
|
491
|
+
flowKind: flow.kind,
|
|
492
|
+
flowVersion: flow.version,
|
|
493
|
+
onStateChange: async (state) => {
|
|
494
|
+
publishFlowState(config.command, state);
|
|
495
|
+
},
|
|
496
|
+
});
|
|
367
497
|
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
498
|
+
}
|
|
499
|
+
async function runAutoPhaseViaSpec(config, phaseId, executionState, state) {
|
|
500
|
+
const context = createPipelineContext({
|
|
501
|
+
issueKey: config.taskKey,
|
|
502
|
+
jiraRef: config.jiraRef,
|
|
503
|
+
dryRun: config.dryRun,
|
|
504
|
+
verbose: config.verbose,
|
|
505
|
+
runtime: runtimeServices,
|
|
506
|
+
});
|
|
507
|
+
const autoFlow = loadAutoFlow();
|
|
508
|
+
const phase = findPhaseById(autoFlow.phases, phaseId);
|
|
509
|
+
publishFlowState("auto", executionState);
|
|
510
|
+
try {
|
|
511
|
+
const result = await runExpandedPhase(phase, context, autoFlowParams(config), autoFlow.constants, {
|
|
512
|
+
executionState,
|
|
513
|
+
flowKind: autoFlow.kind,
|
|
514
|
+
flowVersion: autoFlow.version,
|
|
515
|
+
onStateChange: async (state) => {
|
|
516
|
+
publishFlowState("auto", state);
|
|
517
|
+
},
|
|
518
|
+
onStepStart: async (_phase, step) => {
|
|
519
|
+
if (!state) {
|
|
520
|
+
return;
|
|
378
521
|
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
522
|
+
state.currentStep = `${phaseId}:${step.id}`;
|
|
523
|
+
saveAutoPipelineState(state);
|
|
524
|
+
},
|
|
525
|
+
});
|
|
526
|
+
if (state) {
|
|
527
|
+
state.executionState = result.executionState;
|
|
528
|
+
syncAndSaveAutoPipelineState(state);
|
|
529
|
+
}
|
|
530
|
+
return result.status === "skipped" ? "skipped" : "done";
|
|
382
531
|
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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.`);
|
|
532
|
+
catch (error) {
|
|
533
|
+
if (!config.dryRun) {
|
|
534
|
+
const output = String(error.output ?? "");
|
|
535
|
+
if (output.trim()) {
|
|
536
|
+
printError("Build verification failed");
|
|
537
|
+
printSummary("Build Failure Summary", await summarizeBuildFailure(output));
|
|
538
|
+
}
|
|
398
539
|
}
|
|
399
|
-
|
|
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;
|
|
540
|
+
throw error;
|
|
416
541
|
}
|
|
417
|
-
throw new TaskRunnerError(`Unsupported auto step command: ${step.command}`);
|
|
418
542
|
}
|
|
419
543
|
function rewindAutoPipelineState(state, phaseId) {
|
|
420
544
|
const targetPhaseId = validateAutoPhaseId(phaseId);
|
|
@@ -439,12 +563,12 @@ function rewindAutoPipelineState(state, phaseId) {
|
|
|
439
563
|
state.status = "pending";
|
|
440
564
|
state.currentStep = null;
|
|
441
565
|
state.lastError = null;
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
|
|
566
|
+
const targetIndex = state.executionState.phases.findIndex((phase) => phase.id === targetPhaseId);
|
|
567
|
+
if (targetIndex >= 0) {
|
|
568
|
+
state.executionState.phases = state.executionState.phases.slice(0, targetIndex);
|
|
569
|
+
}
|
|
570
|
+
state.executionState.terminated = false;
|
|
571
|
+
delete state.executionState.terminationReason;
|
|
448
572
|
}
|
|
449
573
|
async function summarizeBuildFailure(output) {
|
|
450
574
|
return summarizeBuildFailureViaPipeline(createPipelineContext({
|
|
@@ -474,163 +598,178 @@ async function executeCommand(config, runFollowupVerify = true) {
|
|
|
474
598
|
printPanel("Auto Reset", removed ? `State file ${autoStateFile(config.taskKey)} removed.` : "No auto state file found.", "yellow");
|
|
475
599
|
return false;
|
|
476
600
|
}
|
|
477
|
-
|
|
601
|
+
checkPrerequisites(config);
|
|
478
602
|
process.env.JIRA_BROWSE_URL = config.jiraBrowseUrl;
|
|
479
603
|
process.env.JIRA_API_URL = config.jiraApiUrl;
|
|
480
604
|
process.env.JIRA_TASK_FILE = config.jiraTaskFile;
|
|
481
|
-
const implementPrompt = formatPrompt(formatTemplate(IMPLEMENT_PROMPT_TEMPLATE, {
|
|
482
|
-
design_file: designFile(config.taskKey),
|
|
483
|
-
plan_file: planFile(config.taskKey),
|
|
484
|
-
}), config.extraPrompt);
|
|
485
605
|
if (config.command === "plan") {
|
|
486
606
|
if (config.verbose) {
|
|
487
607
|
process.stdout.write(`Fetching Jira issue from browse URL: ${config.jiraBrowseUrl}\n`);
|
|
488
608
|
process.stdout.write(`Resolved Jira API URL: ${config.jiraApiUrl}\n`);
|
|
489
609
|
process.stdout.write(`Saving Jira issue JSON to: ${config.jiraTaskFile}\n`);
|
|
490
610
|
}
|
|
491
|
-
await
|
|
492
|
-
issueKey: config.taskKey,
|
|
493
|
-
jiraRef: config.jiraRef,
|
|
494
|
-
dryRun: config.dryRun,
|
|
495
|
-
verbose: config.verbose,
|
|
496
|
-
runtime: runtimeServices,
|
|
497
|
-
}), {
|
|
611
|
+
await runDeclarativeFlowBySpecFile("plan.json", config, {
|
|
498
612
|
jiraApiUrl: config.jiraApiUrl,
|
|
499
|
-
jiraTaskFile: config.jiraTaskFile,
|
|
500
613
|
taskKey: config.taskKey,
|
|
501
|
-
|
|
502
|
-
|
|
614
|
+
extraPrompt: config.extraPrompt,
|
|
615
|
+
});
|
|
616
|
+
return false;
|
|
617
|
+
}
|
|
618
|
+
if (config.command === "bug-analyze") {
|
|
619
|
+
if (config.verbose) {
|
|
620
|
+
process.stdout.write(`Fetching Jira issue from browse URL: ${config.jiraBrowseUrl}\n`);
|
|
621
|
+
process.stdout.write(`Resolved Jira API URL: ${config.jiraApiUrl}\n`);
|
|
622
|
+
process.stdout.write(`Saving Jira issue JSON to: ${config.jiraTaskFile}\n`);
|
|
623
|
+
}
|
|
624
|
+
await runDeclarativeFlowBySpecFile("bug-analyze.json", config, {
|
|
625
|
+
jiraApiUrl: config.jiraApiUrl,
|
|
626
|
+
taskKey: config.taskKey,
|
|
627
|
+
extraPrompt: config.extraPrompt,
|
|
628
|
+
});
|
|
629
|
+
return false;
|
|
630
|
+
}
|
|
631
|
+
if (config.command === "bug-fix") {
|
|
632
|
+
requireJiraTaskFile(config.jiraTaskFile);
|
|
633
|
+
requireArtifacts(bugAnalyzeArtifacts(config.taskKey), "Bug-fix mode requires bug-analyze artifacts from the bug analysis phase.");
|
|
634
|
+
validateStructuredArtifacts([
|
|
635
|
+
{ path: bugAnalyzeJsonFile(config.taskKey), schemaId: "bug-analysis/v1" },
|
|
636
|
+
{ path: bugFixDesignJsonFile(config.taskKey), schemaId: "bug-fix-design/v1" },
|
|
637
|
+
{ path: bugFixPlanJsonFile(config.taskKey), schemaId: "bug-fix-plan/v1" },
|
|
638
|
+
], "Bug-fix mode requires valid structured artifacts from the bug analysis phase.");
|
|
639
|
+
await runDeclarativeFlowBySpecFile("bug-fix.json", config, {
|
|
640
|
+
taskKey: config.taskKey,
|
|
641
|
+
extraPrompt: config.extraPrompt,
|
|
642
|
+
});
|
|
643
|
+
return false;
|
|
644
|
+
}
|
|
645
|
+
if (config.command === "mr-description") {
|
|
646
|
+
requireJiraTaskFile(config.jiraTaskFile);
|
|
647
|
+
await runDeclarativeFlowBySpecFile("mr-description.json", config, {
|
|
648
|
+
taskKey: config.taskKey,
|
|
649
|
+
extraPrompt: config.extraPrompt,
|
|
650
|
+
});
|
|
651
|
+
return false;
|
|
652
|
+
}
|
|
653
|
+
if (config.command === "task-describe") {
|
|
654
|
+
requireJiraTaskFile(config.jiraTaskFile);
|
|
655
|
+
await runDeclarativeFlowBySpecFile("task-describe.json", config, {
|
|
656
|
+
taskKey: config.taskKey,
|
|
657
|
+
extraPrompt: config.extraPrompt,
|
|
503
658
|
});
|
|
504
659
|
return false;
|
|
505
660
|
}
|
|
506
661
|
if (config.command === "implement") {
|
|
507
662
|
requireJiraTaskFile(config.jiraTaskFile);
|
|
508
663
|
requireArtifacts(planArtifacts(config.taskKey), "Implement mode requires plan artifacts from the planning phase.");
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
printSummary("Build Failure Summary", await summarizeBuildFailure(output));
|
|
524
|
-
},
|
|
664
|
+
try {
|
|
665
|
+
await runDeclarativeFlowBySpecFile("implement.json", config, {
|
|
666
|
+
taskKey: config.taskKey,
|
|
667
|
+
dockerComposeFile: config.dockerComposeFile,
|
|
668
|
+
extraPrompt: config.extraPrompt,
|
|
669
|
+
runFollowupVerify,
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
catch (error) {
|
|
673
|
+
if (!config.dryRun) {
|
|
674
|
+
const output = String(error.output ?? "");
|
|
675
|
+
if (output.trim()) {
|
|
676
|
+
printError("Build verification failed");
|
|
677
|
+
printSummary("Build Failure Summary", await summarizeBuildFailure(output));
|
|
525
678
|
}
|
|
526
|
-
|
|
527
|
-
|
|
679
|
+
}
|
|
680
|
+
throw error;
|
|
681
|
+
}
|
|
528
682
|
return false;
|
|
529
683
|
}
|
|
530
684
|
if (config.command === "review") {
|
|
531
685
|
requireJiraTaskFile(config.jiraTaskFile);
|
|
532
|
-
const
|
|
533
|
-
|
|
534
|
-
jiraRef: config.jiraRef,
|
|
535
|
-
dryRun: config.dryRun,
|
|
536
|
-
verbose: config.verbose,
|
|
537
|
-
runtime: runtimeServices,
|
|
538
|
-
}), {
|
|
539
|
-
jiraTaskFile: config.jiraTaskFile,
|
|
686
|
+
const iteration = nextReviewIterationForTask(config.taskKey);
|
|
687
|
+
await runDeclarativeFlowBySpecFile("review.json", config, {
|
|
540
688
|
taskKey: config.taskKey,
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
...(config.extraPrompt !== undefined ? { extraPrompt: config.extraPrompt } : {}),
|
|
689
|
+
iteration,
|
|
690
|
+
extraPrompt: config.extraPrompt,
|
|
544
691
|
});
|
|
545
|
-
return
|
|
692
|
+
return !config.dryRun && existsSync(readyToMergeFile(config.taskKey));
|
|
546
693
|
}
|
|
547
694
|
if (config.command === "review-fix") {
|
|
548
695
|
requireJiraTaskFile(config.jiraTaskFile);
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
printError("Build verification failed");
|
|
566
|
-
printSummary("Build Failure Summary", await summarizeBuildFailure(output));
|
|
567
|
-
},
|
|
696
|
+
try {
|
|
697
|
+
await runDeclarativeFlowBySpecFile("review-fix.json", config, {
|
|
698
|
+
taskKey: config.taskKey,
|
|
699
|
+
dockerComposeFile: config.dockerComposeFile,
|
|
700
|
+
latestIteration: latestReviewReplyIteration(config.taskKey),
|
|
701
|
+
runFollowupVerify,
|
|
702
|
+
extraPrompt: config.extraPrompt,
|
|
703
|
+
reviewFixPoints: config.reviewFixPoints,
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
catch (error) {
|
|
707
|
+
if (!config.dryRun) {
|
|
708
|
+
const output = String(error.output ?? "");
|
|
709
|
+
if (output.trim()) {
|
|
710
|
+
printError("Build verification failed");
|
|
711
|
+
printSummary("Build Failure Summary", await summarizeBuildFailure(output));
|
|
568
712
|
}
|
|
569
|
-
|
|
570
|
-
|
|
713
|
+
}
|
|
714
|
+
throw error;
|
|
715
|
+
}
|
|
571
716
|
return false;
|
|
572
717
|
}
|
|
573
718
|
if (config.command === "test") {
|
|
574
719
|
requireJiraTaskFile(config.jiraTaskFile);
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
printError("Build verification failed");
|
|
588
|
-
printSummary("Build Failure Summary", await summarizeBuildFailure(output));
|
|
589
|
-
},
|
|
720
|
+
try {
|
|
721
|
+
await runDeclarativeFlowBySpecFile("test.json", config, {
|
|
722
|
+
taskKey: config.taskKey,
|
|
723
|
+
dockerComposeFile: config.dockerComposeFile,
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
catch (error) {
|
|
727
|
+
if (!config.dryRun) {
|
|
728
|
+
const output = String(error.output ?? "");
|
|
729
|
+
if (output.trim()) {
|
|
730
|
+
printError("Build verification failed");
|
|
731
|
+
printSummary("Build Failure Summary", await summarizeBuildFailure(output));
|
|
590
732
|
}
|
|
591
|
-
|
|
592
|
-
|
|
733
|
+
}
|
|
734
|
+
throw error;
|
|
735
|
+
}
|
|
593
736
|
return false;
|
|
594
737
|
}
|
|
595
738
|
if (config.command === "test-fix" || config.command === "test-linter-fix") {
|
|
596
739
|
requireJiraTaskFile(config.jiraTaskFile);
|
|
597
|
-
await
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
740
|
+
await runDeclarativeFlowBySpecFile(config.command === "test-fix" ? "test-fix.json" : "test-linter-fix.json", config, {
|
|
741
|
+
taskKey: config.taskKey,
|
|
742
|
+
extraPrompt: config.extraPrompt,
|
|
743
|
+
});
|
|
744
|
+
return false;
|
|
745
|
+
}
|
|
746
|
+
if (config.command === "run-tests-loop" || config.command === "run-linter-loop") {
|
|
747
|
+
await runDeclarativeFlowBySpecFile(config.command === "run-tests-loop" ? "run-tests-loop.json" : "run-linter-loop.json", config, {
|
|
605
748
|
taskKey: config.taskKey,
|
|
606
749
|
dockerComposeFile: config.dockerComposeFile,
|
|
607
|
-
|
|
750
|
+
extraPrompt: config.extraPrompt,
|
|
608
751
|
});
|
|
609
752
|
return false;
|
|
610
753
|
}
|
|
611
754
|
throw new TaskRunnerError(`Unsupported command: ${config.command}`);
|
|
612
755
|
}
|
|
613
756
|
async function runAutoPipelineDryRun(config) {
|
|
614
|
-
|
|
615
|
-
printInfo("Dry-run auto pipeline
|
|
616
|
-
const
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
757
|
+
checkAutoPrerequisites(config);
|
|
758
|
+
printInfo("Dry-run auto pipeline from declarative spec");
|
|
759
|
+
const autoFlow = loadAutoFlow();
|
|
760
|
+
const executionState = {
|
|
761
|
+
flowKind: autoFlow.kind,
|
|
762
|
+
flowVersion: autoFlow.version,
|
|
763
|
+
terminated: false,
|
|
764
|
+
phases: [],
|
|
765
|
+
};
|
|
766
|
+
publishFlowState("auto", executionState);
|
|
767
|
+
for (const phase of autoFlow.phases) {
|
|
768
|
+
printInfo(`Dry-run auto phase: ${phase.id}`);
|
|
769
|
+
await runAutoPhaseViaSpec(config, phase.id, executionState);
|
|
770
|
+
if (executionState.terminated) {
|
|
771
|
+
break;
|
|
627
772
|
}
|
|
628
|
-
await runAutoStepViaFlow(buildPhaseConfig(config, "review"), reviewStep, codexCmd, claudeCmd, dryState);
|
|
629
|
-
await runAutoStepViaFlow({
|
|
630
|
-
...buildPhaseConfig(config, "review-fix"),
|
|
631
|
-
extraPrompt: appendPromptText(config.extraPrompt, AUTO_REVIEW_FIX_EXTRA_PROMPT),
|
|
632
|
-
}, reviewFixStep, codexCmd, claudeCmd, dryState);
|
|
633
|
-
await runAutoStepViaFlow(buildPhaseConfig(config, "test"), testStep, codexCmd, claudeCmd, dryState);
|
|
634
773
|
}
|
|
635
774
|
}
|
|
636
775
|
async function runAutoPipeline(config) {
|
|
@@ -638,7 +777,10 @@ async function runAutoPipeline(config) {
|
|
|
638
777
|
await runAutoPipelineDryRun(config);
|
|
639
778
|
return;
|
|
640
779
|
}
|
|
641
|
-
|
|
780
|
+
checkAutoPrerequisites(config);
|
|
781
|
+
process.env.JIRA_BROWSE_URL = config.jiraBrowseUrl;
|
|
782
|
+
process.env.JIRA_API_URL = config.jiraApiUrl;
|
|
783
|
+
process.env.JIRA_TASK_FILE = config.jiraTaskFile;
|
|
642
784
|
let state = loadAutoPipelineState(config) ?? createAutoPipelineState(config);
|
|
643
785
|
if (config.autoFromPhase) {
|
|
644
786
|
rewindAutoPipelineState(state, config.autoFromPhase);
|
|
@@ -652,17 +794,7 @@ async function runAutoPipeline(config) {
|
|
|
652
794
|
while (true) {
|
|
653
795
|
const step = nextAutoStep(state);
|
|
654
796
|
if (!step) {
|
|
655
|
-
|
|
656
|
-
state.status = "blocked";
|
|
657
|
-
}
|
|
658
|
-
else if (state.steps.some((candidate) => candidate.status === "skipped")) {
|
|
659
|
-
state.status = "completed";
|
|
660
|
-
}
|
|
661
|
-
else {
|
|
662
|
-
state.status = "max-iterations-reached";
|
|
663
|
-
}
|
|
664
|
-
state.currentStep = null;
|
|
665
|
-
saveAutoPipelineState(state);
|
|
797
|
+
syncAndSaveAutoPipelineState(state);
|
|
666
798
|
if (state.status === "completed") {
|
|
667
799
|
printPanel("Auto", "Auto pipeline finished", "green");
|
|
668
800
|
}
|
|
@@ -682,18 +814,14 @@ async function runAutoPipeline(config) {
|
|
|
682
814
|
saveAutoPipelineState(state);
|
|
683
815
|
try {
|
|
684
816
|
printInfo(`Running auto step: ${step.id}`);
|
|
685
|
-
const
|
|
686
|
-
step.status =
|
|
817
|
+
const status = await runAutoPhaseViaSpec(config, step.id, state.executionState, state);
|
|
818
|
+
step.status = status;
|
|
687
819
|
step.finishedAt = nowIso8601();
|
|
688
820
|
step.returnCode = 0;
|
|
689
|
-
if (
|
|
690
|
-
|
|
691
|
-
state.status = "completed";
|
|
692
|
-
state.currentStep = null;
|
|
693
|
-
saveAutoPipelineState(state);
|
|
694
|
-
printPanel("Auto", "Auto pipeline finished", "green");
|
|
695
|
-
return;
|
|
821
|
+
if (status === "skipped") {
|
|
822
|
+
step.note = "condition not met";
|
|
696
823
|
}
|
|
824
|
+
syncAndSaveAutoPipelineState(state);
|
|
697
825
|
}
|
|
698
826
|
catch (error) {
|
|
699
827
|
const returnCode = Number(error.returnCode ?? 1);
|
|
@@ -710,46 +838,18 @@ async function runAutoPipeline(config) {
|
|
|
710
838
|
saveAutoPipelineState(state);
|
|
711
839
|
throw error;
|
|
712
840
|
}
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
const result = [];
|
|
718
|
-
let current = "";
|
|
719
|
-
let quote = null;
|
|
720
|
-
for (let index = 0; index < input.length; index += 1) {
|
|
721
|
-
const char = input[index] ?? "";
|
|
722
|
-
if (quote) {
|
|
723
|
-
if (char === quote) {
|
|
724
|
-
quote = null;
|
|
725
|
-
}
|
|
726
|
-
else {
|
|
727
|
-
current += char;
|
|
728
|
-
}
|
|
729
|
-
continue;
|
|
730
|
-
}
|
|
731
|
-
if (char === "'" || char === '"') {
|
|
732
|
-
quote = char;
|
|
733
|
-
continue;
|
|
734
|
-
}
|
|
735
|
-
if (/\s/.test(char)) {
|
|
736
|
-
if (current) {
|
|
737
|
-
result.push(current);
|
|
738
|
-
current = "";
|
|
739
|
-
}
|
|
740
|
-
continue;
|
|
841
|
+
if (state.executionState.terminated) {
|
|
842
|
+
syncAndSaveAutoPipelineState(state);
|
|
843
|
+
printPanel("Auto", "Auto pipeline finished", "green");
|
|
844
|
+
return;
|
|
741
845
|
}
|
|
742
|
-
current += char;
|
|
743
|
-
}
|
|
744
|
-
if (quote) {
|
|
745
|
-
throw new TaskRunnerError("Cannot parse command: unterminated quote");
|
|
746
|
-
}
|
|
747
|
-
if (current) {
|
|
748
|
-
result.push(current);
|
|
749
846
|
}
|
|
750
|
-
return result;
|
|
751
847
|
}
|
|
752
848
|
function parseCliArgs(argv) {
|
|
849
|
+
if (argv.includes("--version") || argv.includes("-v")) {
|
|
850
|
+
process.stdout.write(`${packageVersion()}\n`);
|
|
851
|
+
process.exit(0);
|
|
852
|
+
}
|
|
753
853
|
if (argv.includes("--help") || argv.includes("-h")) {
|
|
754
854
|
process.stdout.write(`${usage()}\n`);
|
|
755
855
|
process.exit(0);
|
|
@@ -821,143 +921,45 @@ function buildConfigFromArgs(args) {
|
|
|
821
921
|
verbose: args.verbose,
|
|
822
922
|
});
|
|
823
923
|
}
|
|
824
|
-
function interactiveHelp() {
|
|
825
|
-
printPanel("Interactive Commands", [
|
|
826
|
-
"/plan [extra prompt]",
|
|
827
|
-
"/implement [extra prompt]",
|
|
828
|
-
"/review [extra prompt]",
|
|
829
|
-
"/review-fix [extra prompt]",
|
|
830
|
-
"/test",
|
|
831
|
-
"/test-fix [extra prompt]",
|
|
832
|
-
"/test-linter-fix [extra prompt]",
|
|
833
|
-
"/auto [extra prompt]",
|
|
834
|
-
"/auto --from <phase> [extra prompt]",
|
|
835
|
-
"/auto-status",
|
|
836
|
-
"/auto-reset",
|
|
837
|
-
"/help auto",
|
|
838
|
-
"/help",
|
|
839
|
-
"/exit",
|
|
840
|
-
].join("\n"), "magenta");
|
|
841
|
-
}
|
|
842
|
-
function parseInteractiveCommand(line, jiraRef) {
|
|
843
|
-
const parts = splitArgs(line);
|
|
844
|
-
if (parts.length === 0) {
|
|
845
|
-
return null;
|
|
846
|
-
}
|
|
847
|
-
const command = parts[0] ?? "";
|
|
848
|
-
if (!command.startsWith("/")) {
|
|
849
|
-
throw new TaskRunnerError("Interactive mode expects slash commands. Use /help.");
|
|
850
|
-
}
|
|
851
|
-
const commandName = command.slice(1);
|
|
852
|
-
if (commandName === "help") {
|
|
853
|
-
if (parts[1] === "auto" || parts[1] === "phases") {
|
|
854
|
-
printAutoPhasesHelp();
|
|
855
|
-
return null;
|
|
856
|
-
}
|
|
857
|
-
interactiveHelp();
|
|
858
|
-
return null;
|
|
859
|
-
}
|
|
860
|
-
if (commandName === "exit" || commandName === "quit") {
|
|
861
|
-
throw new EOFError();
|
|
862
|
-
}
|
|
863
|
-
if (!COMMANDS.includes(commandName)) {
|
|
864
|
-
throw new TaskRunnerError(`Unknown command: ${command}`);
|
|
865
|
-
}
|
|
866
|
-
if (commandName === "auto") {
|
|
867
|
-
let autoFromPhase;
|
|
868
|
-
let extraParts = parts.slice(1);
|
|
869
|
-
if (extraParts[0] === "--from") {
|
|
870
|
-
if (!extraParts[1]) {
|
|
871
|
-
throw new TaskRunnerError("auto --from requires a phase name. Use /help auto.");
|
|
872
|
-
}
|
|
873
|
-
autoFromPhase = extraParts[1];
|
|
874
|
-
extraParts = extraParts.slice(2);
|
|
875
|
-
}
|
|
876
|
-
return buildConfig("auto", jiraRef, {
|
|
877
|
-
extraPrompt: extraParts.join(" ") || null,
|
|
878
|
-
...(autoFromPhase !== undefined ? { autoFromPhase } : {}),
|
|
879
|
-
});
|
|
880
|
-
}
|
|
881
|
-
return buildConfig(commandName, jiraRef, {
|
|
882
|
-
extraPrompt: parts.slice(1).join(" ") || null,
|
|
883
|
-
});
|
|
884
|
-
}
|
|
885
|
-
class EOFError extends Error {
|
|
886
|
-
}
|
|
887
|
-
async function ensureHistoryFile() {
|
|
888
|
-
mkdirSync(path.dirname(HISTORY_FILE), { recursive: true });
|
|
889
|
-
if (!existsSync(HISTORY_FILE)) {
|
|
890
|
-
writeFileSync(HISTORY_FILE, "", "utf8");
|
|
891
|
-
}
|
|
892
|
-
}
|
|
893
924
|
async function runInteractive(jiraRef, forceRefresh = false) {
|
|
894
925
|
const config = buildConfig("plan", jiraRef);
|
|
895
926
|
const jiraTaskPath = config.jiraTaskFile;
|
|
896
|
-
await ensureHistoryFile();
|
|
897
|
-
const historyLines = (await readFile(HISTORY_FILE, "utf8"))
|
|
898
|
-
.split(/\r?\n/)
|
|
899
|
-
.filter(Boolean)
|
|
900
|
-
.slice(-200);
|
|
901
927
|
let exiting = false;
|
|
902
|
-
const commandList = [
|
|
903
|
-
"/plan",
|
|
904
|
-
"/implement",
|
|
905
|
-
"/review",
|
|
906
|
-
"/review-fix",
|
|
907
|
-
"/test",
|
|
908
|
-
"/test-fix",
|
|
909
|
-
"/test-linter-fix",
|
|
910
|
-
"/auto",
|
|
911
|
-
"/auto-status",
|
|
912
|
-
"/auto-reset",
|
|
913
|
-
"/help",
|
|
914
|
-
"/exit",
|
|
915
|
-
];
|
|
916
928
|
const ui = new InteractiveUi({
|
|
917
929
|
issueKey: config.jiraIssueKey,
|
|
918
930
|
summaryText: "Starting interactive session...",
|
|
919
931
|
cwd: process.cwd(),
|
|
920
|
-
|
|
921
|
-
|
|
932
|
+
flows: interactiveFlowDefinitions(),
|
|
933
|
+
onRun: async (flowId) => {
|
|
922
934
|
try {
|
|
923
|
-
|
|
924
|
-
const command = parseInteractiveCommand(line, jiraRef);
|
|
925
|
-
if (!command) {
|
|
926
|
-
return;
|
|
927
|
-
}
|
|
928
|
-
ui.setBusy(true, command.command);
|
|
935
|
+
const command = buildConfig(flowId, jiraRef);
|
|
929
936
|
await executeCommand(command);
|
|
930
937
|
}
|
|
931
938
|
catch (error) {
|
|
932
|
-
if (error instanceof EOFError) {
|
|
933
|
-
exiting = true;
|
|
934
|
-
return;
|
|
935
|
-
}
|
|
936
939
|
if (error instanceof TaskRunnerError) {
|
|
940
|
+
ui.setFlowFailed(flowId);
|
|
937
941
|
printError(error.message);
|
|
938
942
|
return;
|
|
939
943
|
}
|
|
940
944
|
const returnCode = Number(error.returnCode);
|
|
941
945
|
if (!Number.isNaN(returnCode)) {
|
|
946
|
+
ui.setFlowFailed(flowId);
|
|
942
947
|
printError(`Command failed with exit code ${returnCode}`);
|
|
943
948
|
return;
|
|
944
949
|
}
|
|
945
950
|
throw error;
|
|
946
951
|
}
|
|
947
|
-
finally {
|
|
948
|
-
ui.setBusy(false);
|
|
949
|
-
}
|
|
950
952
|
},
|
|
951
953
|
onExit: () => {
|
|
952
954
|
exiting = true;
|
|
953
955
|
},
|
|
954
|
-
}
|
|
956
|
+
});
|
|
955
957
|
ui.mount();
|
|
956
958
|
printInfo(`Interactive mode for ${config.jiraIssueKey}`);
|
|
957
|
-
printInfo("Use
|
|
959
|
+
printInfo("Use h to see help.");
|
|
958
960
|
try {
|
|
959
961
|
ui.setBusy(true, "preflight");
|
|
960
|
-
await runPreflightFlow(createPipelineContext({
|
|
962
|
+
const preflightState = await runPreflightFlow(createPipelineContext({
|
|
961
963
|
issueKey: config.taskKey,
|
|
962
964
|
jiraRef: config.jiraRef,
|
|
963
965
|
dryRun: false,
|
|
@@ -972,8 +974,20 @@ async function runInteractive(jiraRef, forceRefresh = false) {
|
|
|
972
974
|
taskKey: config.taskKey,
|
|
973
975
|
forceRefresh,
|
|
974
976
|
});
|
|
977
|
+
const preflightPhase = preflightState.phases.find((phase) => phase.id === "preflight");
|
|
978
|
+
if (preflightPhase) {
|
|
979
|
+
ui.appendLog("[preflight] completed");
|
|
980
|
+
for (const step of preflightPhase.steps) {
|
|
981
|
+
ui.appendLog(`[preflight] ${step.id}: ${step.status}`);
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
if (!existsSync(jiraTaskPath)) {
|
|
985
|
+
throw new TaskRunnerError(`Preflight finished without Jira task file: ${jiraTaskPath}\n` +
|
|
986
|
+
"Jira fetch did not complete successfully. Check JIRA_API_KEY and Jira connectivity.");
|
|
987
|
+
}
|
|
975
988
|
if (!existsSync(taskSummaryFile(config.taskKey))) {
|
|
976
|
-
ui.
|
|
989
|
+
ui.appendLog("[preflight] task summary file was not created");
|
|
990
|
+
ui.setSummary("Task summary is not available yet. Select and run `plan` or refresh Jira data.");
|
|
977
991
|
}
|
|
978
992
|
}
|
|
979
993
|
catch (error) {
|