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