agentweaver 0.1.0 → 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 +30 -5
- package/dist/artifacts.js +24 -2
- package/dist/executors/claude-executor.js +46 -0
- package/dist/executors/claude-summary-executor.js +31 -0
- package/dist/executors/codex-docker-executor.js +27 -0
- package/dist/executors/codex-local-executor.js +25 -0
- package/dist/executors/command-check-executor.js +14 -0
- package/dist/executors/configs/claude-config.js +12 -0
- package/dist/executors/configs/claude-summary-config.js +8 -0
- package/dist/executors/configs/codex-docker-config.js +10 -0
- package/dist/executors/configs/codex-local-config.js +8 -0
- package/dist/executors/configs/jira-fetch-config.js +4 -0
- package/dist/executors/configs/process-config.js +3 -0
- package/dist/executors/configs/verify-build-config.js +7 -0
- package/dist/executors/jira-fetch-executor.js +11 -0
- package/dist/executors/process-executor.js +21 -0
- package/dist/executors/types.js +1 -0
- package/dist/executors/verify-build-executor.js +22 -0
- package/dist/index.js +456 -699
- package/dist/interactive-ui.js +536 -182
- package/dist/jira.js +3 -1
- package/dist/pipeline/auto-flow.js +9 -0
- package/dist/pipeline/build-failure-summary.js +6 -0
- package/dist/pipeline/checks.js +15 -0
- package/dist/pipeline/context.js +19 -0
- package/dist/pipeline/declarative-flow-runner.js +246 -0
- package/dist/pipeline/declarative-flows.js +24 -0
- package/dist/pipeline/flow-runner.js +13 -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/flow-types.js +1 -0
- package/dist/pipeline/flows/implement-flow.js +47 -0
- package/dist/pipeline/flows/plan-flow.js +42 -0
- package/dist/pipeline/flows/preflight-flow.js +19 -0
- package/dist/pipeline/flows/review-fix-flow.js +62 -0
- package/dist/pipeline/flows/review-flow.js +124 -0
- package/dist/pipeline/flows/test-fix-flow.js +12 -0
- package/dist/pipeline/flows/test-flow.js +32 -0
- package/dist/pipeline/node-registry.js +71 -0
- package/dist/pipeline/node-runner.js +20 -0
- package/dist/pipeline/nodes/build-failure-summary-node.js +71 -0
- package/dist/pipeline/nodes/claude-prompt-node.js +54 -0
- package/dist/pipeline/nodes/claude-summary-node.js +38 -0
- package/dist/pipeline/nodes/codex-docker-prompt-node.js +32 -0
- package/dist/pipeline/nodes/codex-local-prompt-node.js +32 -0
- package/dist/pipeline/nodes/command-check-node.js +10 -0
- package/dist/pipeline/nodes/file-check-node.js +15 -0
- package/dist/pipeline/nodes/implement-codex-node.js +16 -0
- package/dist/pipeline/nodes/jira-fetch-node.js +25 -0
- package/dist/pipeline/nodes/plan-codex-node.js +32 -0
- package/dist/pipeline/nodes/review-claude-node.js +38 -0
- package/dist/pipeline/nodes/review-reply-codex-node.js +40 -0
- package/dist/pipeline/nodes/summary-file-load-node.js +16 -0
- package/dist/pipeline/nodes/task-summary-node.js +42 -0
- package/dist/pipeline/nodes/verify-build-node.js +14 -0
- package/dist/pipeline/prompt-registry.js +22 -0
- package/dist/pipeline/prompt-runtime.js +18 -0
- package/dist/pipeline/registry.js +23 -0
- 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/types.js +10 -0
- package/dist/pipeline/value-resolver.js +199 -0
- package/dist/prompts.js +1 -3
- package/dist/runtime/command-resolution.js +139 -0
- package/dist/runtime/docker-runtime.js +51 -0
- package/dist/runtime/process-runner.js +112 -0
- package/dist/tui.js +73 -0
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
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
|
-
import { spawn, spawnSync } from "node:child_process";
|
|
8
5
|
import { fileURLToPath } from "node:url";
|
|
9
|
-
import {
|
|
6
|
+
import { REVIEW_FILE_RE, REVIEW_REPLY_FILE_RE, autoStateFile, ensureTaskWorkspaceDir, jiraTaskFile, planArtifacts, readyToMergeFile, requireArtifacts, taskWorkspaceDir, taskSummaryFile, } from "./artifacts.js";
|
|
10
7
|
import { TaskRunnerError } from "./errors.js";
|
|
11
|
-
import { buildJiraApiUrl, buildJiraBrowseUrl, extractIssueKey,
|
|
12
|
-
import {
|
|
8
|
+
import { buildJiraApiUrl, buildJiraBrowseUrl, extractIssueKey, requireJiraTaskFile } from "./jira.js";
|
|
9
|
+
import { summarizeBuildFailure as summarizeBuildFailureViaPipeline } from "./pipeline/build-failure-summary.js";
|
|
10
|
+
import { createPipelineContext } from "./pipeline/context.js";
|
|
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";
|
|
14
|
+
import { runPreflightFlow } from "./pipeline/flows/preflight-flow.js";
|
|
15
|
+
import { resolveCmd, resolveDockerComposeCmd } from "./runtime/command-resolution.js";
|
|
16
|
+
import { defaultDockerComposeFile, dockerRuntimeEnv } from "./runtime/docker-runtime.js";
|
|
17
|
+
import { runCommand } from "./runtime/process-runner.js";
|
|
13
18
|
import { InteractiveUi } from "./interactive-ui.js";
|
|
14
|
-
import { bye,
|
|
19
|
+
import { bye, printError, printInfo, printPanel, printSummary, setFlowExecutionState } from "./tui.js";
|
|
15
20
|
const COMMANDS = [
|
|
16
21
|
"plan",
|
|
17
22
|
"implement",
|
|
@@ -24,14 +29,15 @@ const COMMANDS = [
|
|
|
24
29
|
"auto-status",
|
|
25
30
|
"auto-reset",
|
|
26
31
|
];
|
|
27
|
-
const
|
|
28
|
-
const DEFAULT_CLAUDE_REVIEW_MODEL = "opus";
|
|
29
|
-
const DEFAULT_CLAUDE_SUMMARY_MODEL = "haiku";
|
|
30
|
-
const HISTORY_FILE = path.join(os.homedir(), ".codex", "memories", "agentweaver-history");
|
|
31
|
-
const AUTO_STATE_SCHEMA_VERSION = 1;
|
|
32
|
-
const AUTO_MAX_REVIEW_ITERATIONS = 3;
|
|
32
|
+
const AUTO_STATE_SCHEMA_VERSION = 2;
|
|
33
33
|
const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
34
34
|
const PACKAGE_ROOT = path.resolve(MODULE_DIR, "..");
|
|
35
|
+
const runtimeServices = {
|
|
36
|
+
resolveCmd,
|
|
37
|
+
resolveDockerComposeCmd,
|
|
38
|
+
dockerRuntimeEnv: () => dockerRuntimeEnv(PACKAGE_ROOT),
|
|
39
|
+
runCommand,
|
|
40
|
+
};
|
|
35
41
|
function usage() {
|
|
36
42
|
return `Usage:
|
|
37
43
|
agentweaver <jira-browse-url|jira-issue-key>
|
|
@@ -50,10 +56,11 @@ function usage() {
|
|
|
50
56
|
agentweaver auto-reset <jira-browse-url|jira-issue-key>
|
|
51
57
|
|
|
52
58
|
Interactive Mode:
|
|
53
|
-
When started with only a Jira task, the script opens an interactive
|
|
54
|
-
|
|
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.
|
|
55
61
|
|
|
56
62
|
Flags:
|
|
63
|
+
--version Show package version
|
|
57
64
|
--force In interactive mode, force refresh Jira task and task summary
|
|
58
65
|
--dry Fetch Jira task, but print docker/codex/claude commands instead of executing them
|
|
59
66
|
--verbose Show live stdout/stderr of launched commands
|
|
@@ -69,8 +76,15 @@ Optional environment variables:
|
|
|
69
76
|
CODEX_BIN
|
|
70
77
|
CODEX_MODEL
|
|
71
78
|
CLAUDE_BIN
|
|
72
|
-
|
|
73
|
-
|
|
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;
|
|
74
88
|
}
|
|
75
89
|
function nowIso8601() {
|
|
76
90
|
return new Date().toISOString();
|
|
@@ -78,24 +92,14 @@ function nowIso8601() {
|
|
|
78
92
|
function normalizeAutoPhaseId(phaseId) {
|
|
79
93
|
return phaseId.trim().toLowerCase().replaceAll("-", "_");
|
|
80
94
|
}
|
|
81
|
-
function buildAutoSteps(
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
];
|
|
87
|
-
for (let iteration = 1; iteration <= maxReviewIterations; iteration += 1) {
|
|
88
|
-
steps.push({ id: `review_${iteration}`, command: "review", status: "pending", reviewIteration: iteration }, { id: `review_fix_${iteration}`, command: "review-fix", status: "pending", reviewIteration: iteration }, {
|
|
89
|
-
id: `test_after_review_fix_${iteration}`,
|
|
90
|
-
command: "test",
|
|
91
|
-
status: "pending",
|
|
92
|
-
reviewIteration: iteration,
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
return steps;
|
|
95
|
+
function buildAutoSteps() {
|
|
96
|
+
return loadAutoFlow().phases.map((phase) => ({
|
|
97
|
+
id: phase.id,
|
|
98
|
+
status: "pending",
|
|
99
|
+
}));
|
|
96
100
|
}
|
|
97
|
-
function autoPhaseIds(
|
|
98
|
-
return buildAutoSteps(
|
|
101
|
+
function autoPhaseIds() {
|
|
102
|
+
return buildAutoSteps().map((step) => step.id);
|
|
99
103
|
}
|
|
100
104
|
function validateAutoPhaseId(phaseId) {
|
|
101
105
|
const normalized = normalizeAutoPhaseId(phaseId);
|
|
@@ -104,19 +108,46 @@ function validateAutoPhaseId(phaseId) {
|
|
|
104
108
|
}
|
|
105
109
|
return normalized;
|
|
106
110
|
}
|
|
107
|
-
function autoStateFile(taskKey) {
|
|
108
|
-
return path.join(process.cwd(), `.agentweaver-state-${taskKey}.json`);
|
|
109
|
-
}
|
|
110
111
|
function createAutoPipelineState(config) {
|
|
112
|
+
const autoFlow = loadAutoFlow();
|
|
113
|
+
const maxReviewIterations = autoFlow.phases.filter((phase) => /^review_\d+$/.test(phase.id)).length;
|
|
111
114
|
return {
|
|
112
115
|
schemaVersion: AUTO_STATE_SCHEMA_VERSION,
|
|
113
116
|
issueKey: config.taskKey,
|
|
114
117
|
jiraRef: config.jiraRef,
|
|
115
118
|
status: "pending",
|
|
116
119
|
currentStep: null,
|
|
117
|
-
maxReviewIterations
|
|
120
|
+
maxReviewIterations,
|
|
118
121
|
updatedAt: nowIso8601(),
|
|
119
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
|
+
})),
|
|
120
151
|
};
|
|
121
152
|
}
|
|
122
153
|
function loadAutoPipelineState(config) {
|
|
@@ -138,11 +169,29 @@ function loadAutoPipelineState(config) {
|
|
|
138
169
|
if (state.schemaVersion !== AUTO_STATE_SCHEMA_VERSION) {
|
|
139
170
|
throw new TaskRunnerError(`Unsupported auto state schema in ${filePath}: ${state.schemaVersion}`);
|
|
140
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);
|
|
141
182
|
return state;
|
|
142
183
|
}
|
|
143
184
|
function saveAutoPipelineState(state) {
|
|
144
185
|
state.updatedAt = nowIso8601();
|
|
145
|
-
|
|
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);
|
|
146
195
|
}
|
|
147
196
|
function resetAutoPipelineState(config) {
|
|
148
197
|
const filePath = autoStateFile(config.taskKey);
|
|
@@ -155,28 +204,42 @@ function resetAutoPipelineState(config) {
|
|
|
155
204
|
function nextAutoStep(state) {
|
|
156
205
|
return state.steps.find((step) => ["running", "failed", "pending"].includes(step.status)) ?? null;
|
|
157
206
|
}
|
|
158
|
-
function
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
}
|
|
163
|
-
function skipAutoStepsAfterReadyToMerge(state, currentStepId) {
|
|
164
|
-
let seenCurrent = false;
|
|
165
|
-
for (const step of state.steps) {
|
|
166
|
-
if (!seenCurrent) {
|
|
167
|
-
seenCurrent = step.id === currentStepId;
|
|
168
|
-
continue;
|
|
169
|
-
}
|
|
170
|
-
if (step.status === "pending") {
|
|
171
|
-
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}`;
|
|
172
212
|
}
|
|
173
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;
|
|
174
236
|
}
|
|
175
237
|
function printAutoState(state) {
|
|
238
|
+
const currentStep = findCurrentExecutionStep(state) ?? state.currentStep ?? "-";
|
|
176
239
|
const lines = [
|
|
177
240
|
`Issue: ${state.issueKey}`,
|
|
178
|
-
`Status: ${state
|
|
179
|
-
`Current step: ${
|
|
241
|
+
`Status: ${deriveAutoPipelineStatus(state)}`,
|
|
242
|
+
`Current step: ${currentStep}`,
|
|
180
243
|
`Updated: ${state.updatedAt}`,
|
|
181
244
|
];
|
|
182
245
|
if (state.lastError) {
|
|
@@ -185,14 +248,42 @@ function printAutoState(state) {
|
|
|
185
248
|
lines.push("");
|
|
186
249
|
for (const step of state.steps) {
|
|
187
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"}`);
|
|
188
258
|
}
|
|
189
259
|
printPanel("Auto Status", lines.join("\n"), "cyan");
|
|
190
260
|
}
|
|
191
|
-
function
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
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
|
+
}
|
|
195
281
|
}
|
|
282
|
+
state.currentStep = findCurrentExecutionStep(state);
|
|
283
|
+
state.status = deriveAutoPipelineStatus(state);
|
|
284
|
+
}
|
|
285
|
+
function printAutoPhasesHelp() {
|
|
286
|
+
const phaseLines = ["Available auto phases:", "", ...autoPhaseIds()];
|
|
196
287
|
phaseLines.push("", "You can resume auto from a phase with:", "agentweaver auto --from <phase> <jira>", "or in interactive mode:", "/auto --from <phase>");
|
|
197
288
|
printPanel("Auto Phases", phaseLines.join("\n"), "magenta");
|
|
198
289
|
}
|
|
@@ -224,113 +315,13 @@ function loadEnvFile(envFilePath) {
|
|
|
224
315
|
process.env[key] = value;
|
|
225
316
|
}
|
|
226
317
|
}
|
|
227
|
-
function agentweaverHome() {
|
|
228
|
-
const configured = process.env.AGENTWEAVER_HOME?.trim();
|
|
229
|
-
if (configured) {
|
|
230
|
-
return path.resolve(configured);
|
|
231
|
-
}
|
|
232
|
-
return PACKAGE_ROOT;
|
|
233
|
-
}
|
|
234
|
-
function defaultDockerComposeFile() {
|
|
235
|
-
return path.join(agentweaverHome(), "docker-compose.yml");
|
|
236
|
-
}
|
|
237
|
-
function defaultCodexHomeDir() {
|
|
238
|
-
return path.join(agentweaverHome(), ".codex-home");
|
|
239
|
-
}
|
|
240
|
-
function ensureRuntimeBindPath(targetPath, isDir) {
|
|
241
|
-
mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
242
|
-
if (isDir) {
|
|
243
|
-
mkdirSync(targetPath, { recursive: true });
|
|
244
|
-
}
|
|
245
|
-
else if (!existsSync(targetPath)) {
|
|
246
|
-
writeFileSync(targetPath, "", "utf8");
|
|
247
|
-
}
|
|
248
|
-
return targetPath;
|
|
249
|
-
}
|
|
250
|
-
function defaultHostSshDir() {
|
|
251
|
-
const candidate = path.join(os.homedir(), ".ssh");
|
|
252
|
-
if (existsSync(candidate)) {
|
|
253
|
-
return candidate;
|
|
254
|
-
}
|
|
255
|
-
return ensureRuntimeBindPath(path.join(agentweaverHome(), ".runtime", "ssh"), true);
|
|
256
|
-
}
|
|
257
|
-
function defaultHostGitconfig() {
|
|
258
|
-
const candidate = path.join(os.homedir(), ".gitconfig");
|
|
259
|
-
if (existsSync(candidate)) {
|
|
260
|
-
return candidate;
|
|
261
|
-
}
|
|
262
|
-
return ensureRuntimeBindPath(path.join(agentweaverHome(), ".runtime", "gitconfig"), false);
|
|
263
|
-
}
|
|
264
|
-
function dockerRuntimeEnv() {
|
|
265
|
-
const env = { ...process.env };
|
|
266
|
-
env.AGENTWEAVER_HOME ??= agentweaverHome();
|
|
267
|
-
env.PROJECT_DIR ??= process.cwd();
|
|
268
|
-
env.CODEX_HOME_DIR ??= ensureRuntimeBindPath(defaultCodexHomeDir(), true);
|
|
269
|
-
env.HOST_SSH_DIR ??= defaultHostSshDir();
|
|
270
|
-
env.HOST_GITCONFIG ??= defaultHostGitconfig();
|
|
271
|
-
env.LOCAL_UID ??= typeof process.getuid === "function" ? String(process.getuid()) : "1000";
|
|
272
|
-
env.LOCAL_GID ??= typeof process.getgid === "function" ? String(process.getgid()) : "1000";
|
|
273
|
-
return env;
|
|
274
|
-
}
|
|
275
|
-
function commandExists(commandName) {
|
|
276
|
-
const result = spawnSync("bash", ["-lc", `command -v ${shellQuote(commandName)}`], { stdio: "ignore" });
|
|
277
|
-
return result.status === 0;
|
|
278
|
-
}
|
|
279
|
-
function shellQuote(value) {
|
|
280
|
-
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
281
|
-
}
|
|
282
|
-
function resolveCmd(commandName, envVarName) {
|
|
283
|
-
const configuredPath = process.env[envVarName];
|
|
284
|
-
if (configuredPath) {
|
|
285
|
-
accessSync(configuredPath, constants.X_OK);
|
|
286
|
-
return configuredPath;
|
|
287
|
-
}
|
|
288
|
-
const result = spawnSync("bash", ["-lc", `command -v ${shellQuote(commandName)}`], { encoding: "utf8" });
|
|
289
|
-
if (result.status === 0 && result.stdout.trim()) {
|
|
290
|
-
return result.stdout.trim().split(/\r?\n/)[0] ?? commandName;
|
|
291
|
-
}
|
|
292
|
-
throw new TaskRunnerError(`Missing required command: ${commandName}`);
|
|
293
|
-
}
|
|
294
|
-
function requireDockerCompose() {
|
|
295
|
-
if (!commandExists("docker")) {
|
|
296
|
-
throw new TaskRunnerError("Missing required command: docker");
|
|
297
|
-
}
|
|
298
|
-
const result = spawnSync("docker", ["compose", "version"], { stdio: "ignore" });
|
|
299
|
-
if (result.status !== 0) {
|
|
300
|
-
throw new TaskRunnerError("Missing required docker compose plugin");
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
function resolveDockerComposeCmd() {
|
|
304
|
-
const configured = process.env.DOCKER_COMPOSE_BIN?.trim() ?? "";
|
|
305
|
-
if (configured) {
|
|
306
|
-
const parts = splitArgs(configured);
|
|
307
|
-
if (parts.length === 0) {
|
|
308
|
-
throw new TaskRunnerError("DOCKER_COMPOSE_BIN is set but empty.");
|
|
309
|
-
}
|
|
310
|
-
const executable = parts[0] ?? "";
|
|
311
|
-
try {
|
|
312
|
-
if (path.isAbsolute(executable)) {
|
|
313
|
-
accessSync(executable, constants.X_OK);
|
|
314
|
-
return parts;
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
catch {
|
|
318
|
-
throw new TaskRunnerError(`Configured docker compose command is not executable: ${configured}`);
|
|
319
|
-
}
|
|
320
|
-
if (commandExists(executable)) {
|
|
321
|
-
return parts;
|
|
322
|
-
}
|
|
323
|
-
throw new TaskRunnerError(`Configured docker compose command is not executable: ${configured}`);
|
|
324
|
-
}
|
|
325
|
-
if (commandExists("docker-compose")) {
|
|
326
|
-
return ["docker-compose"];
|
|
327
|
-
}
|
|
328
|
-
requireDockerCompose();
|
|
329
|
-
return ["docker", "compose"];
|
|
330
|
-
}
|
|
331
318
|
function nextReviewIterationForTask(taskKey) {
|
|
332
319
|
let maxIndex = 0;
|
|
333
|
-
|
|
320
|
+
const workspaceDir = taskWorkspaceDir(taskKey);
|
|
321
|
+
if (!existsSync(workspaceDir)) {
|
|
322
|
+
return 1;
|
|
323
|
+
}
|
|
324
|
+
for (const entry of readdirSync(workspaceDir, { withFileTypes: true })) {
|
|
334
325
|
if (!entry.isFile()) {
|
|
335
326
|
continue;
|
|
336
327
|
}
|
|
@@ -343,7 +334,11 @@ function nextReviewIterationForTask(taskKey) {
|
|
|
343
334
|
}
|
|
344
335
|
function latestReviewReplyIteration(taskKey) {
|
|
345
336
|
let maxIndex = null;
|
|
346
|
-
|
|
337
|
+
const workspaceDir = taskWorkspaceDir(taskKey);
|
|
338
|
+
if (!existsSync(workspaceDir)) {
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
for (const entry of readdirSync(workspaceDir, { withFileTypes: true })) {
|
|
347
342
|
if (!entry.isFile()) {
|
|
348
343
|
continue;
|
|
349
344
|
}
|
|
@@ -355,112 +350,9 @@ function latestReviewReplyIteration(taskKey) {
|
|
|
355
350
|
}
|
|
356
351
|
return maxIndex;
|
|
357
352
|
}
|
|
358
|
-
function formatCommand(argv, env) {
|
|
359
|
-
const envParts = Object.entries(env ?? {})
|
|
360
|
-
.filter(([key, value]) => value !== undefined && process.env[key] !== value)
|
|
361
|
-
.map(([key, value]) => `${key}=${shellQuote(value ?? "")}`);
|
|
362
|
-
const command = argv.map(shellQuote).join(" ");
|
|
363
|
-
return envParts.length > 0 ? `${envParts.join(" ")} ${command}` : command;
|
|
364
|
-
}
|
|
365
|
-
function formatDuration(ms) {
|
|
366
|
-
const totalSeconds = Math.max(0, Math.floor(ms / 1000));
|
|
367
|
-
const minutes = String(Math.floor(totalSeconds / 60)).padStart(2, "0");
|
|
368
|
-
const seconds = String(totalSeconds % 60).padStart(2, "0");
|
|
369
|
-
return `${minutes}:${seconds}`;
|
|
370
|
-
}
|
|
371
|
-
async function runCommand(argv, options = {}) {
|
|
372
|
-
const { env, dryRun = false, verbose = false, label, printFailureOutput = true } = options;
|
|
373
|
-
const outputAdapter = getOutputAdapter();
|
|
374
|
-
if (dryRun) {
|
|
375
|
-
outputAdapter.writeStdout(`${formatCommand(argv, env)}\n`);
|
|
376
|
-
return "";
|
|
377
|
-
}
|
|
378
|
-
if (verbose && outputAdapter.supportsPassthrough) {
|
|
379
|
-
await new Promise((resolve, reject) => {
|
|
380
|
-
const child = spawn(argv[0] ?? "", argv.slice(1), {
|
|
381
|
-
stdio: "inherit",
|
|
382
|
-
env,
|
|
383
|
-
});
|
|
384
|
-
child.on("exit", (code) => (code === 0 ? resolve() : reject(new Error(String(code ?? 1)))));
|
|
385
|
-
child.on("error", reject);
|
|
386
|
-
}).catch((error) => {
|
|
387
|
-
const code = Number.parseInt(error.message, 10);
|
|
388
|
-
throw Object.assign(new Error(`Command failed with exit code ${Number.isNaN(code) ? 1 : code}`), {
|
|
389
|
-
returnCode: Number.isNaN(code) ? 1 : code,
|
|
390
|
-
output: "",
|
|
391
|
-
});
|
|
392
|
-
});
|
|
393
|
-
return "";
|
|
394
|
-
}
|
|
395
|
-
const startedAt = Date.now();
|
|
396
|
-
const statusLabel = label ?? path.basename(argv[0] ?? argv.join(" "));
|
|
397
|
-
const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
398
|
-
let frameIndex = 0;
|
|
399
|
-
let output = "";
|
|
400
|
-
const child = spawn(argv[0] ?? "", argv.slice(1), {
|
|
401
|
-
env,
|
|
402
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
403
|
-
});
|
|
404
|
-
child.stdout?.on("data", (chunk) => {
|
|
405
|
-
const text = String(chunk);
|
|
406
|
-
output += text;
|
|
407
|
-
if (!outputAdapter.supportsTransientStatus || verbose) {
|
|
408
|
-
outputAdapter.writeStdout(text);
|
|
409
|
-
}
|
|
410
|
-
});
|
|
411
|
-
child.stderr?.on("data", (chunk) => {
|
|
412
|
-
const text = String(chunk);
|
|
413
|
-
output += text;
|
|
414
|
-
if (!outputAdapter.supportsTransientStatus || verbose) {
|
|
415
|
-
outputAdapter.writeStderr(text);
|
|
416
|
-
}
|
|
417
|
-
});
|
|
418
|
-
if (!outputAdapter.supportsTransientStatus) {
|
|
419
|
-
outputAdapter.writeStdout(`Running ${statusLabel}\n`);
|
|
420
|
-
}
|
|
421
|
-
const timer = outputAdapter.supportsTransientStatus
|
|
422
|
-
? setInterval(() => {
|
|
423
|
-
const elapsed = formatDuration(Date.now() - startedAt);
|
|
424
|
-
process.stdout.write(`\r${frames[frameIndex]} ${statusLabel} ${dim(elapsed)}`);
|
|
425
|
-
frameIndex = (frameIndex + 1) % frames.length;
|
|
426
|
-
}, 200)
|
|
427
|
-
: null;
|
|
428
|
-
try {
|
|
429
|
-
const exitCode = await new Promise((resolve, reject) => {
|
|
430
|
-
child.on("error", reject);
|
|
431
|
-
child.on("exit", (code) => resolve(code ?? 1));
|
|
432
|
-
});
|
|
433
|
-
if (timer) {
|
|
434
|
-
clearInterval(timer);
|
|
435
|
-
process.stdout.write(`\r${" ".repeat(80)}\r${formatDone(formatDuration(Date.now() - startedAt))}\n`);
|
|
436
|
-
}
|
|
437
|
-
else {
|
|
438
|
-
outputAdapter.writeStdout(`Done ${formatDuration(Date.now() - startedAt)}\n`);
|
|
439
|
-
}
|
|
440
|
-
if (exitCode !== 0) {
|
|
441
|
-
if (output && printFailureOutput) {
|
|
442
|
-
if (outputAdapter.supportsTransientStatus) {
|
|
443
|
-
process.stderr.write(output);
|
|
444
|
-
if (!output.endsWith("\n")) {
|
|
445
|
-
process.stderr.write("\n");
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
throw Object.assign(new Error(`Command failed with exit code ${exitCode}`), {
|
|
450
|
-
returnCode: exitCode,
|
|
451
|
-
output,
|
|
452
|
-
});
|
|
453
|
-
}
|
|
454
|
-
return output;
|
|
455
|
-
}
|
|
456
|
-
finally {
|
|
457
|
-
if (timer) {
|
|
458
|
-
clearInterval(timer);
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
353
|
function buildConfig(command, jiraRef, options = {}) {
|
|
463
354
|
const jiraIssueKey = extractIssueKey(jiraRef);
|
|
355
|
+
ensureTaskWorkspaceDir(jiraIssueKey);
|
|
464
356
|
return {
|
|
465
357
|
command,
|
|
466
358
|
jiraRef,
|
|
@@ -469,52 +361,158 @@ function buildConfig(command, jiraRef, options = {}) {
|
|
|
469
361
|
autoFromPhase: options.autoFromPhase ? validateAutoPhaseId(options.autoFromPhase) : null,
|
|
470
362
|
dryRun: options.dryRun ?? false,
|
|
471
363
|
verbose: options.verbose ?? false,
|
|
472
|
-
dockerComposeFile: defaultDockerComposeFile(),
|
|
473
|
-
dockerComposeCmd: [],
|
|
474
|
-
codexCmd: process.env.CODEX_BIN ?? "codex",
|
|
475
|
-
claudeCmd: process.env.CLAUDE_BIN ?? "claude",
|
|
364
|
+
dockerComposeFile: defaultDockerComposeFile(PACKAGE_ROOT),
|
|
476
365
|
jiraIssueKey,
|
|
477
366
|
taskKey: jiraIssueKey,
|
|
478
367
|
jiraBrowseUrl: buildJiraBrowseUrl(jiraRef),
|
|
479
368
|
jiraApiUrl: buildJiraApiUrl(jiraRef),
|
|
480
|
-
jiraTaskFile:
|
|
369
|
+
jiraTaskFile: jiraTaskFile(jiraIssueKey),
|
|
481
370
|
};
|
|
482
371
|
}
|
|
483
372
|
function checkPrerequisites(config) {
|
|
484
|
-
let codexCmd = config.codexCmd;
|
|
485
|
-
let claudeCmd = config.claudeCmd;
|
|
486
|
-
let dockerComposeCmd = config.dockerComposeCmd;
|
|
487
373
|
if (config.command === "plan" || config.command === "review") {
|
|
488
|
-
|
|
374
|
+
resolveCmd("codex", "CODEX_BIN");
|
|
489
375
|
}
|
|
490
376
|
if (config.command === "review") {
|
|
491
|
-
|
|
377
|
+
resolveCmd("claude", "CLAUDE_BIN");
|
|
492
378
|
}
|
|
493
|
-
if (["implement", "review-fix", "test"
|
|
494
|
-
|
|
379
|
+
if (["implement", "review-fix", "test"].includes(config.command)) {
|
|
380
|
+
resolveDockerComposeCmd();
|
|
495
381
|
if (!existsSync(config.dockerComposeFile)) {
|
|
496
382
|
throw new TaskRunnerError(`docker-compose file not found: ${config.dockerComposeFile}`);
|
|
497
383
|
}
|
|
498
384
|
}
|
|
499
|
-
return { codexCmd, claudeCmd, dockerComposeCmd };
|
|
500
385
|
}
|
|
501
|
-
function
|
|
502
|
-
|
|
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}`);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
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
|
+
];
|
|
442
|
+
}
|
|
443
|
+
function publishFlowState(flowId, executionState) {
|
|
444
|
+
setFlowExecutionState(flowId, stripExecutionStatePayload(executionState));
|
|
503
445
|
}
|
|
504
|
-
function
|
|
505
|
-
|
|
506
|
-
|
|
446
|
+
async function runDeclarativeFlowBySpecFile(fileName, config, flowParams) {
|
|
447
|
+
const context = createPipelineContext({
|
|
448
|
+
issueKey: config.taskKey,
|
|
449
|
+
jiraRef: config.jiraRef,
|
|
450
|
+
dryRun: config.dryRun,
|
|
451
|
+
verbose: config.verbose,
|
|
452
|
+
runtime: runtimeServices,
|
|
453
|
+
});
|
|
454
|
+
const flow = loadDeclarativeFlow(fileName);
|
|
455
|
+
const executionState = {
|
|
456
|
+
flowKind: flow.kind,
|
|
457
|
+
flowVersion: flow.version,
|
|
458
|
+
terminated: false,
|
|
459
|
+
phases: [],
|
|
460
|
+
};
|
|
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
|
+
});
|
|
507
471
|
}
|
|
508
|
-
return `${basePrompt.trim()}\n${suffix}`;
|
|
509
472
|
}
|
|
510
|
-
function
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
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;
|
|
495
|
+
}
|
|
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";
|
|
505
|
+
}
|
|
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
|
+
}
|
|
513
|
+
}
|
|
514
|
+
throw error;
|
|
516
515
|
}
|
|
517
|
-
return buildPhaseConfig(baseConfig, step.command);
|
|
518
516
|
}
|
|
519
517
|
function rewindAutoPipelineState(state, phaseId) {
|
|
520
518
|
const targetPhaseId = validateAutoPhaseId(phaseId);
|
|
@@ -539,121 +537,21 @@ function rewindAutoPipelineState(state, phaseId) {
|
|
|
539
537
|
state.status = "pending";
|
|
540
538
|
state.currentStep = null;
|
|
541
539
|
state.lastError = null;
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
dockerEnv.CODEX_PROMPT = prompt;
|
|
546
|
-
dockerEnv.CODEX_EXEC_FLAGS = `--model ${codexModel()} --dangerously-bypass-approvals-and-sandbox`;
|
|
547
|
-
printInfo(labelText);
|
|
548
|
-
printPrompt("Codex", prompt);
|
|
549
|
-
await runCommand([...dockerComposeCmd, "-f", config.dockerComposeFile, "run", "--rm", "codex-exec"], {
|
|
550
|
-
env: dockerEnv,
|
|
551
|
-
dryRun: config.dryRun,
|
|
552
|
-
verbose: config.verbose,
|
|
553
|
-
label: `codex:${codexModel()}`,
|
|
554
|
-
});
|
|
555
|
-
}
|
|
556
|
-
async function runVerifyBuildInDocker(config, dockerComposeCmd, labelText) {
|
|
557
|
-
printInfo(labelText);
|
|
558
|
-
try {
|
|
559
|
-
await runCommand([...dockerComposeCmd, "-f", config.dockerComposeFile, "run", "--rm", "verify-build"], {
|
|
560
|
-
env: dockerRuntimeEnv(),
|
|
561
|
-
dryRun: config.dryRun,
|
|
562
|
-
verbose: false,
|
|
563
|
-
label: "verify-build",
|
|
564
|
-
printFailureOutput: false,
|
|
565
|
-
});
|
|
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);
|
|
566
543
|
}
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
printError(`Build verification failed with exit code ${returnCode}`);
|
|
570
|
-
if (!config.dryRun) {
|
|
571
|
-
printSummary("Build Failure Summary", await summarizeBuildFailure(String(error.output ?? "")));
|
|
572
|
-
}
|
|
573
|
-
throw error;
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
function codexModel() {
|
|
577
|
-
return process.env.CODEX_MODEL?.trim() || DEFAULT_CODEX_MODEL;
|
|
578
|
-
}
|
|
579
|
-
function claudeReviewModel() {
|
|
580
|
-
return process.env.CLAUDE_REVIEW_MODEL?.trim() || DEFAULT_CLAUDE_REVIEW_MODEL;
|
|
581
|
-
}
|
|
582
|
-
function claudeSummaryModel() {
|
|
583
|
-
return process.env.CLAUDE_SUMMARY_MODEL?.trim() || DEFAULT_CLAUDE_SUMMARY_MODEL;
|
|
584
|
-
}
|
|
585
|
-
function truncateText(text, maxChars = 12000) {
|
|
586
|
-
return text.length <= maxChars ? text.trim() : text.trim().slice(-maxChars);
|
|
587
|
-
}
|
|
588
|
-
function fallbackBuildFailureSummary(output) {
|
|
589
|
-
const lines = output
|
|
590
|
-
.split(/\r?\n/)
|
|
591
|
-
.map((line) => line.trim())
|
|
592
|
-
.filter(Boolean);
|
|
593
|
-
const tail = lines.length > 0 ? lines.slice(-8) : ["No build output captured."];
|
|
594
|
-
return `Не удалось получить summary через Claude.\n\nПоследние строки лога:\n${tail.join("\n")}`;
|
|
544
|
+
state.executionState.terminated = false;
|
|
545
|
+
delete state.executionState.terminationReason;
|
|
595
546
|
}
|
|
596
547
|
async function summarizeBuildFailure(output) {
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
let claudeCmd;
|
|
601
|
-
try {
|
|
602
|
-
claudeCmd = resolveCmd("claude", "CLAUDE_BIN");
|
|
603
|
-
}
|
|
604
|
-
catch {
|
|
605
|
-
return fallbackBuildFailureSummary(output);
|
|
606
|
-
}
|
|
607
|
-
const prompt = "Ниже лог упавшей build verification.\n" +
|
|
608
|
-
"Сделай краткое резюме на русском языке, без воды.\n" +
|
|
609
|
-
"Нужно обязательно выделить:\n" +
|
|
610
|
-
"1. Где именно упало.\n" +
|
|
611
|
-
"2. Главную причину падения.\n" +
|
|
612
|
-
"3. Что нужно исправить дальше, если это очевидно.\n" +
|
|
613
|
-
"Ответ дай максимум 5 короткими пунктами.\n\n" +
|
|
614
|
-
`Лог:\n${truncateText(output)}`;
|
|
615
|
-
printInfo(`Summarizing build failure with Claude (${claudeSummaryModel()})`);
|
|
616
|
-
try {
|
|
617
|
-
const summary = await runCommand([claudeCmd, "--model", claudeSummaryModel(), "-p", prompt], {
|
|
618
|
-
env: { ...process.env },
|
|
619
|
-
dryRun: false,
|
|
620
|
-
verbose: false,
|
|
621
|
-
label: `claude:${claudeSummaryModel()}`,
|
|
622
|
-
});
|
|
623
|
-
return summary.trim() || fallbackBuildFailureSummary(output);
|
|
624
|
-
}
|
|
625
|
-
catch {
|
|
626
|
-
return fallbackBuildFailureSummary(output);
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
async function runClaudeSummary(claudeCmd, outputFile, prompt, verbose = false) {
|
|
630
|
-
printInfo(`Preparing summary in ${outputFile}`);
|
|
631
|
-
printPrompt("Claude", prompt);
|
|
632
|
-
await runCommand([claudeCmd, "--model", claudeSummaryModel(), "-p", "--allowedTools=Read,Write,Edit", prompt], {
|
|
633
|
-
env: { ...process.env },
|
|
548
|
+
return summarizeBuildFailureViaPipeline(createPipelineContext({
|
|
549
|
+
issueKey: "build-failure-summary",
|
|
550
|
+
jiraRef: "build-failure-summary",
|
|
634
551
|
dryRun: false,
|
|
635
|
-
verbose,
|
|
636
|
-
|
|
637
|
-
});
|
|
638
|
-
requireArtifacts([outputFile], `Claude summary did not produce ${outputFile}.`);
|
|
639
|
-
return readFileSync(outputFile, "utf8").trim();
|
|
640
|
-
}
|
|
641
|
-
async function summarizeTask(jiraRef) {
|
|
642
|
-
const config = buildConfig("plan", jiraRef);
|
|
643
|
-
const claudeCmd = resolveCmd("claude", "CLAUDE_BIN");
|
|
644
|
-
await fetchJiraIssue(config.jiraApiUrl, config.jiraTaskFile);
|
|
645
|
-
const summaryPrompt = formatTemplate(TASK_SUMMARY_PROMPT_TEMPLATE, {
|
|
646
|
-
jira_task_file: config.jiraTaskFile,
|
|
647
|
-
task_summary_file: taskSummaryFile(config.taskKey),
|
|
648
|
-
});
|
|
649
|
-
const summaryText = await runClaudeSummary(claudeCmd, taskSummaryFile(config.taskKey), summaryPrompt);
|
|
650
|
-
return { issueKey: config.jiraIssueKey, summaryText };
|
|
651
|
-
}
|
|
652
|
-
function resolveTaskIdentity(jiraRef) {
|
|
653
|
-
const config = buildConfig("plan", jiraRef);
|
|
654
|
-
const summaryPath = taskSummaryFile(config.taskKey);
|
|
655
|
-
const summaryText = existsSync(summaryPath) ? readFileSync(summaryPath, "utf8").trim() : "";
|
|
656
|
-
return { issueKey: config.jiraIssueKey, summaryText };
|
|
552
|
+
verbose: false,
|
|
553
|
+
runtime: runtimeServices,
|
|
554
|
+
}), output);
|
|
657
555
|
}
|
|
658
556
|
async function executeCommand(config, runFollowupVerify = true) {
|
|
659
557
|
if (config.command === "auto") {
|
|
@@ -674,169 +572,127 @@ async function executeCommand(config, runFollowupVerify = true) {
|
|
|
674
572
|
printPanel("Auto Reset", removed ? `State file ${autoStateFile(config.taskKey)} removed.` : "No auto state file found.", "yellow");
|
|
675
573
|
return false;
|
|
676
574
|
}
|
|
677
|
-
|
|
575
|
+
checkPrerequisites(config);
|
|
678
576
|
process.env.JIRA_BROWSE_URL = config.jiraBrowseUrl;
|
|
679
577
|
process.env.JIRA_API_URL = config.jiraApiUrl;
|
|
680
578
|
process.env.JIRA_TASK_FILE = config.jiraTaskFile;
|
|
681
|
-
const planPrompt = formatPrompt(formatTemplate(PLAN_PROMPT_TEMPLATE, {
|
|
682
|
-
jira_task_file: config.jiraTaskFile,
|
|
683
|
-
design_file: designFile(config.taskKey),
|
|
684
|
-
plan_file: planFile(config.taskKey),
|
|
685
|
-
qa_file: qaFile(config.taskKey),
|
|
686
|
-
}), config.extraPrompt);
|
|
687
|
-
const implementPrompt = formatPrompt(formatTemplate(IMPLEMENT_PROMPT_TEMPLATE, {
|
|
688
|
-
design_file: designFile(config.taskKey),
|
|
689
|
-
plan_file: planFile(config.taskKey),
|
|
690
|
-
}), config.extraPrompt);
|
|
691
579
|
if (config.command === "plan") {
|
|
692
580
|
if (config.verbose) {
|
|
693
581
|
process.stdout.write(`Fetching Jira issue from browse URL: ${config.jiraBrowseUrl}\n`);
|
|
694
582
|
process.stdout.write(`Resolved Jira API URL: ${config.jiraApiUrl}\n`);
|
|
695
583
|
process.stdout.write(`Saving Jira issue JSON to: ${config.jiraTaskFile}\n`);
|
|
696
584
|
}
|
|
697
|
-
await
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
env: { ...process.env },
|
|
702
|
-
dryRun: config.dryRun,
|
|
703
|
-
verbose: config.verbose,
|
|
704
|
-
label: `codex:${codexModel()}`,
|
|
585
|
+
await runDeclarativeFlowBySpecFile("plan.json", config, {
|
|
586
|
+
jiraApiUrl: config.jiraApiUrl,
|
|
587
|
+
taskKey: config.taskKey,
|
|
588
|
+
extraPrompt: config.extraPrompt,
|
|
705
589
|
});
|
|
706
|
-
requireArtifacts(planArtifacts(config.taskKey), "Plan mode did not produce the required artifacts.");
|
|
707
590
|
return false;
|
|
708
591
|
}
|
|
709
592
|
if (config.command === "implement") {
|
|
710
593
|
requireJiraTaskFile(config.jiraTaskFile);
|
|
711
594
|
requireArtifacts(planArtifacts(config.taskKey), "Implement mode requires plan artifacts from the planning phase.");
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
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));
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
throw error;
|
|
715
612
|
}
|
|
716
613
|
return false;
|
|
717
614
|
}
|
|
718
615
|
if (config.command === "review") {
|
|
719
616
|
requireJiraTaskFile(config.jiraTaskFile);
|
|
720
|
-
requireArtifacts(planArtifacts(config.taskKey), "Review mode requires plan artifacts from the planning phase.");
|
|
721
617
|
const iteration = nextReviewIterationForTask(config.taskKey);
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
const claudePrompt = formatPrompt(formatTemplate(REVIEW_PROMPT_TEMPLATE, {
|
|
727
|
-
jira_task_file: config.jiraTaskFile,
|
|
728
|
-
design_file: designFile(config.taskKey),
|
|
729
|
-
plan_file: planFile(config.taskKey),
|
|
730
|
-
review_file: reviewFile,
|
|
731
|
-
}), config.extraPrompt);
|
|
732
|
-
const codexReplyPrompt = formatPrompt(formatTemplate(REVIEW_REPLY_PROMPT_TEMPLATE, {
|
|
733
|
-
review_file: reviewFile,
|
|
734
|
-
jira_task_file: config.jiraTaskFile,
|
|
735
|
-
design_file: designFile(config.taskKey),
|
|
736
|
-
plan_file: planFile(config.taskKey),
|
|
737
|
-
review_reply_file: reviewReplyFile,
|
|
738
|
-
}), config.extraPrompt);
|
|
739
|
-
printInfo(`Running Claude review mode (iteration ${iteration})`);
|
|
740
|
-
printPrompt("Claude", claudePrompt);
|
|
741
|
-
await runCommand([
|
|
742
|
-
claudeCmd,
|
|
743
|
-
"--model",
|
|
744
|
-
claudeReviewModel(),
|
|
745
|
-
"-p",
|
|
746
|
-
"--allowedTools=Read,Write,Edit",
|
|
747
|
-
"--output-format",
|
|
748
|
-
"stream-json",
|
|
749
|
-
"--verbose",
|
|
750
|
-
"--include-partial-messages",
|
|
751
|
-
claudePrompt,
|
|
752
|
-
], {
|
|
753
|
-
env: { ...process.env },
|
|
754
|
-
dryRun: config.dryRun,
|
|
755
|
-
verbose: config.verbose,
|
|
756
|
-
label: `claude:${claudeReviewModel()}`,
|
|
757
|
-
});
|
|
758
|
-
if (!config.dryRun) {
|
|
759
|
-
requireArtifacts([reviewFile], "Claude review did not produce the required review artifact.");
|
|
760
|
-
const reviewSummaryText = await runClaudeSummary(claudeCmd, reviewSummaryFile, formatTemplate(REVIEW_SUMMARY_PROMPT_TEMPLATE, {
|
|
761
|
-
review_file: reviewFile,
|
|
762
|
-
review_summary_file: reviewSummaryFile,
|
|
763
|
-
}), config.verbose);
|
|
764
|
-
printSummary("Claude Comments", reviewSummaryText);
|
|
765
|
-
}
|
|
766
|
-
printInfo(`Running Codex review reply mode (iteration ${iteration})`);
|
|
767
|
-
printPrompt("Codex", codexReplyPrompt);
|
|
768
|
-
await runCommand([codexCmd, "exec", "--model", codexModel(), "--full-auto", codexReplyPrompt], {
|
|
769
|
-
env: { ...process.env },
|
|
770
|
-
dryRun: config.dryRun,
|
|
771
|
-
verbose: config.verbose,
|
|
772
|
-
label: `codex:${codexModel()}`,
|
|
618
|
+
await runDeclarativeFlowBySpecFile("review.json", config, {
|
|
619
|
+
taskKey: config.taskKey,
|
|
620
|
+
iteration,
|
|
621
|
+
extraPrompt: config.extraPrompt,
|
|
773
622
|
});
|
|
774
|
-
|
|
775
|
-
if (!config.dryRun) {
|
|
776
|
-
requireArtifacts([reviewReplyFile], "Codex review reply did not produce the required review-reply artifact.");
|
|
777
|
-
const reviewReplySummaryText = await runClaudeSummary(claudeCmd, reviewReplySummaryFile, formatTemplate(REVIEW_REPLY_SUMMARY_PROMPT_TEMPLATE, {
|
|
778
|
-
review_reply_file: reviewReplyFile,
|
|
779
|
-
review_reply_summary_file: reviewReplySummaryFile,
|
|
780
|
-
}), config.verbose);
|
|
781
|
-
printSummary("Codex Reply Summary", reviewReplySummaryText);
|
|
782
|
-
if (existsSync(READY_TO_MERGE_FILE)) {
|
|
783
|
-
printPanel("Ready To Merge", "Изменения готовы к merge\nФайл ready-to-merge.md создан.", "green");
|
|
784
|
-
readyToMerge = true;
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
return readyToMerge;
|
|
623
|
+
return !config.dryRun && existsSync(readyToMergeFile(config.taskKey));
|
|
788
624
|
}
|
|
789
625
|
if (config.command === "review-fix") {
|
|
790
626
|
requireJiraTaskFile(config.jiraTaskFile);
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
items: config.reviewFixPoints ?? "",
|
|
801
|
-
review_fix_file: reviewFixFile,
|
|
802
|
-
}), config.extraPrompt);
|
|
803
|
-
await runCodexInDocker(config, dockerComposeCmd, reviewFixPrompt, `Running Codex review-fix mode in isolated Docker (iteration ${latestIteration})`);
|
|
804
|
-
if (!config.dryRun) {
|
|
805
|
-
requireArtifacts([reviewFixFile], "Review-fix mode did not produce the required review-fix artifact.");
|
|
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
|
+
});
|
|
806
636
|
}
|
|
807
|
-
|
|
808
|
-
|
|
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));
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
throw error;
|
|
809
646
|
}
|
|
810
647
|
return false;
|
|
811
648
|
}
|
|
812
649
|
if (config.command === "test") {
|
|
813
650
|
requireJiraTaskFile(config.jiraTaskFile);
|
|
814
|
-
|
|
815
|
-
|
|
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));
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
throw error;
|
|
666
|
+
}
|
|
816
667
|
return false;
|
|
817
668
|
}
|
|
818
669
|
if (config.command === "test-fix" || config.command === "test-linter-fix") {
|
|
819
670
|
requireJiraTaskFile(config.jiraTaskFile);
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
671
|
+
await runDeclarativeFlowBySpecFile(config.command === "test-fix" ? "test-fix.json" : "test-linter-fix.json", config, {
|
|
672
|
+
taskKey: config.taskKey,
|
|
673
|
+
extraPrompt: config.extraPrompt,
|
|
674
|
+
});
|
|
823
675
|
return false;
|
|
824
676
|
}
|
|
825
677
|
throw new TaskRunnerError(`Unsupported command: ${config.command}`);
|
|
826
678
|
}
|
|
827
679
|
async function runAutoPipelineDryRun(config) {
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
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;
|
|
695
|
+
}
|
|
840
696
|
}
|
|
841
697
|
}
|
|
842
698
|
async function runAutoPipeline(config) {
|
|
@@ -844,6 +700,10 @@ async function runAutoPipeline(config) {
|
|
|
844
700
|
await runAutoPipelineDryRun(config);
|
|
845
701
|
return;
|
|
846
702
|
}
|
|
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;
|
|
847
707
|
let state = loadAutoPipelineState(config) ?? createAutoPipelineState(config);
|
|
848
708
|
if (config.autoFromPhase) {
|
|
849
709
|
rewindAutoPipelineState(state, config.autoFromPhase);
|
|
@@ -857,17 +717,7 @@ async function runAutoPipeline(config) {
|
|
|
857
717
|
while (true) {
|
|
858
718
|
const step = nextAutoStep(state);
|
|
859
719
|
if (!step) {
|
|
860
|
-
|
|
861
|
-
state.status = "blocked";
|
|
862
|
-
}
|
|
863
|
-
else if (state.steps.some((candidate) => candidate.status === "skipped")) {
|
|
864
|
-
state.status = "completed";
|
|
865
|
-
}
|
|
866
|
-
else {
|
|
867
|
-
state.status = "max-iterations-reached";
|
|
868
|
-
}
|
|
869
|
-
state.currentStep = null;
|
|
870
|
-
saveAutoPipelineState(state);
|
|
720
|
+
syncAndSaveAutoPipelineState(state);
|
|
871
721
|
if (state.status === "completed") {
|
|
872
722
|
printPanel("Auto", "Auto pipeline finished", "green");
|
|
873
723
|
}
|
|
@@ -887,18 +737,14 @@ async function runAutoPipeline(config) {
|
|
|
887
737
|
saveAutoPipelineState(state);
|
|
888
738
|
try {
|
|
889
739
|
printInfo(`Running auto step: ${step.id}`);
|
|
890
|
-
const
|
|
891
|
-
step.status =
|
|
740
|
+
const status = await runAutoPhaseViaSpec(config, step.id, state.executionState, state);
|
|
741
|
+
step.status = status;
|
|
892
742
|
step.finishedAt = nowIso8601();
|
|
893
743
|
step.returnCode = 0;
|
|
894
|
-
if (
|
|
895
|
-
|
|
896
|
-
state.status = "completed";
|
|
897
|
-
state.currentStep = null;
|
|
898
|
-
saveAutoPipelineState(state);
|
|
899
|
-
printPanel("Auto", "Auto pipeline finished", "green");
|
|
900
|
-
return;
|
|
744
|
+
if (status === "skipped") {
|
|
745
|
+
step.note = "condition not met";
|
|
901
746
|
}
|
|
747
|
+
syncAndSaveAutoPipelineState(state);
|
|
902
748
|
}
|
|
903
749
|
catch (error) {
|
|
904
750
|
const returnCode = Number(error.returnCode ?? 1);
|
|
@@ -915,46 +761,18 @@ async function runAutoPipeline(config) {
|
|
|
915
761
|
saveAutoPipelineState(state);
|
|
916
762
|
throw error;
|
|
917
763
|
}
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
const result = [];
|
|
923
|
-
let current = "";
|
|
924
|
-
let quote = null;
|
|
925
|
-
for (let index = 0; index < input.length; index += 1) {
|
|
926
|
-
const char = input[index] ?? "";
|
|
927
|
-
if (quote) {
|
|
928
|
-
if (char === quote) {
|
|
929
|
-
quote = null;
|
|
930
|
-
}
|
|
931
|
-
else {
|
|
932
|
-
current += char;
|
|
933
|
-
}
|
|
934
|
-
continue;
|
|
935
|
-
}
|
|
936
|
-
if (char === "'" || char === '"') {
|
|
937
|
-
quote = char;
|
|
938
|
-
continue;
|
|
939
|
-
}
|
|
940
|
-
if (/\s/.test(char)) {
|
|
941
|
-
if (current) {
|
|
942
|
-
result.push(current);
|
|
943
|
-
current = "";
|
|
944
|
-
}
|
|
945
|
-
continue;
|
|
764
|
+
if (state.executionState.terminated) {
|
|
765
|
+
syncAndSaveAutoPipelineState(state);
|
|
766
|
+
printPanel("Auto", "Auto pipeline finished", "green");
|
|
767
|
+
return;
|
|
946
768
|
}
|
|
947
|
-
current += char;
|
|
948
769
|
}
|
|
949
|
-
if (quote) {
|
|
950
|
-
throw new TaskRunnerError("Cannot parse command: unterminated quote");
|
|
951
|
-
}
|
|
952
|
-
if (current) {
|
|
953
|
-
result.push(current);
|
|
954
|
-
}
|
|
955
|
-
return result;
|
|
956
770
|
}
|
|
957
771
|
function parseCliArgs(argv) {
|
|
772
|
+
if (argv.includes("--version") || argv.includes("-v")) {
|
|
773
|
+
process.stdout.write(`${packageVersion()}\n`);
|
|
774
|
+
process.exit(0);
|
|
775
|
+
}
|
|
958
776
|
if (argv.includes("--help") || argv.includes("-h")) {
|
|
959
777
|
process.stdout.write(`${usage()}\n`);
|
|
960
778
|
process.exit(0);
|
|
@@ -1026,147 +844,86 @@ function buildConfigFromArgs(args) {
|
|
|
1026
844
|
verbose: args.verbose,
|
|
1027
845
|
});
|
|
1028
846
|
}
|
|
1029
|
-
function interactiveHelp() {
|
|
1030
|
-
printPanel("Interactive Commands", [
|
|
1031
|
-
"/plan [extra prompt]",
|
|
1032
|
-
"/implement [extra prompt]",
|
|
1033
|
-
"/review [extra prompt]",
|
|
1034
|
-
"/review-fix [extra prompt]",
|
|
1035
|
-
"/test",
|
|
1036
|
-
"/test-fix [extra prompt]",
|
|
1037
|
-
"/test-linter-fix [extra prompt]",
|
|
1038
|
-
"/auto [extra prompt]",
|
|
1039
|
-
"/auto --from <phase> [extra prompt]",
|
|
1040
|
-
"/auto-status",
|
|
1041
|
-
"/auto-reset",
|
|
1042
|
-
"/help auto",
|
|
1043
|
-
"/help",
|
|
1044
|
-
"/exit",
|
|
1045
|
-
].join("\n"), "magenta");
|
|
1046
|
-
}
|
|
1047
|
-
function parseInteractiveCommand(line, jiraRef) {
|
|
1048
|
-
const parts = splitArgs(line);
|
|
1049
|
-
if (parts.length === 0) {
|
|
1050
|
-
return null;
|
|
1051
|
-
}
|
|
1052
|
-
const command = parts[0] ?? "";
|
|
1053
|
-
if (!command.startsWith("/")) {
|
|
1054
|
-
throw new TaskRunnerError("Interactive mode expects slash commands. Use /help.");
|
|
1055
|
-
}
|
|
1056
|
-
const commandName = command.slice(1);
|
|
1057
|
-
if (commandName === "help") {
|
|
1058
|
-
if (parts[1] === "auto" || parts[1] === "phases") {
|
|
1059
|
-
printAutoPhasesHelp();
|
|
1060
|
-
return null;
|
|
1061
|
-
}
|
|
1062
|
-
interactiveHelp();
|
|
1063
|
-
return null;
|
|
1064
|
-
}
|
|
1065
|
-
if (commandName === "exit" || commandName === "quit") {
|
|
1066
|
-
throw new EOFError();
|
|
1067
|
-
}
|
|
1068
|
-
if (!COMMANDS.includes(commandName)) {
|
|
1069
|
-
throw new TaskRunnerError(`Unknown command: ${command}`);
|
|
1070
|
-
}
|
|
1071
|
-
if (commandName === "auto") {
|
|
1072
|
-
let autoFromPhase;
|
|
1073
|
-
let extraParts = parts.slice(1);
|
|
1074
|
-
if (extraParts[0] === "--from") {
|
|
1075
|
-
if (!extraParts[1]) {
|
|
1076
|
-
throw new TaskRunnerError("auto --from requires a phase name. Use /help auto.");
|
|
1077
|
-
}
|
|
1078
|
-
autoFromPhase = extraParts[1];
|
|
1079
|
-
extraParts = extraParts.slice(2);
|
|
1080
|
-
}
|
|
1081
|
-
return buildConfig("auto", jiraRef, {
|
|
1082
|
-
extraPrompt: extraParts.join(" ") || null,
|
|
1083
|
-
...(autoFromPhase !== undefined ? { autoFromPhase } : {}),
|
|
1084
|
-
});
|
|
1085
|
-
}
|
|
1086
|
-
return buildConfig(commandName, jiraRef, {
|
|
1087
|
-
extraPrompt: parts.slice(1).join(" ") || null,
|
|
1088
|
-
});
|
|
1089
|
-
}
|
|
1090
|
-
class EOFError extends Error {
|
|
1091
|
-
}
|
|
1092
|
-
async function ensureHistoryFile() {
|
|
1093
|
-
mkdirSync(path.dirname(HISTORY_FILE), { recursive: true });
|
|
1094
|
-
if (!existsSync(HISTORY_FILE)) {
|
|
1095
|
-
writeFileSync(HISTORY_FILE, "", "utf8");
|
|
1096
|
-
}
|
|
1097
|
-
}
|
|
1098
847
|
async function runInteractive(jiraRef, forceRefresh = false) {
|
|
1099
848
|
const config = buildConfig("plan", jiraRef);
|
|
1100
849
|
const jiraTaskPath = config.jiraTaskFile;
|
|
1101
|
-
const taskIdentity = forceRefresh || !existsSync(jiraTaskPath) ? await summarizeTask(jiraRef) : resolveTaskIdentity(jiraRef);
|
|
1102
|
-
await ensureHistoryFile();
|
|
1103
|
-
const historyLines = (await readFile(HISTORY_FILE, "utf8"))
|
|
1104
|
-
.split(/\r?\n/)
|
|
1105
|
-
.filter(Boolean)
|
|
1106
|
-
.slice(-200);
|
|
1107
850
|
let exiting = false;
|
|
1108
|
-
const commandList = [
|
|
1109
|
-
"/plan",
|
|
1110
|
-
"/implement",
|
|
1111
|
-
"/review",
|
|
1112
|
-
"/review-fix",
|
|
1113
|
-
"/test",
|
|
1114
|
-
"/test-fix",
|
|
1115
|
-
"/test-linter-fix",
|
|
1116
|
-
"/auto",
|
|
1117
|
-
"/auto-status",
|
|
1118
|
-
"/auto-reset",
|
|
1119
|
-
"/help",
|
|
1120
|
-
"/exit",
|
|
1121
|
-
];
|
|
1122
851
|
const ui = new InteractiveUi({
|
|
1123
|
-
issueKey:
|
|
1124
|
-
summaryText:
|
|
852
|
+
issueKey: config.jiraIssueKey,
|
|
853
|
+
summaryText: "Starting interactive session...",
|
|
1125
854
|
cwd: process.cwd(),
|
|
1126
|
-
|
|
1127
|
-
|
|
855
|
+
flows: interactiveFlowDefinitions(),
|
|
856
|
+
onRun: async (flowId) => {
|
|
1128
857
|
try {
|
|
1129
|
-
|
|
1130
|
-
const command = parseInteractiveCommand(line, jiraRef);
|
|
1131
|
-
if (!command) {
|
|
1132
|
-
return;
|
|
1133
|
-
}
|
|
1134
|
-
ui.setBusy(true, command.command);
|
|
858
|
+
const command = buildConfig(flowId, jiraRef);
|
|
1135
859
|
await executeCommand(command);
|
|
1136
860
|
}
|
|
1137
861
|
catch (error) {
|
|
1138
|
-
if (error instanceof EOFError) {
|
|
1139
|
-
exiting = true;
|
|
1140
|
-
return;
|
|
1141
|
-
}
|
|
1142
862
|
if (error instanceof TaskRunnerError) {
|
|
863
|
+
ui.setFlowFailed(flowId);
|
|
1143
864
|
printError(error.message);
|
|
1144
865
|
return;
|
|
1145
866
|
}
|
|
1146
867
|
const returnCode = Number(error.returnCode);
|
|
1147
868
|
if (!Number.isNaN(returnCode)) {
|
|
869
|
+
ui.setFlowFailed(flowId);
|
|
1148
870
|
printError(`Command failed with exit code ${returnCode}`);
|
|
1149
871
|
return;
|
|
1150
872
|
}
|
|
1151
873
|
throw error;
|
|
1152
874
|
}
|
|
1153
|
-
finally {
|
|
1154
|
-
ui.setBusy(false);
|
|
1155
|
-
}
|
|
1156
875
|
},
|
|
1157
876
|
onExit: () => {
|
|
1158
877
|
exiting = true;
|
|
1159
878
|
},
|
|
1160
|
-
}
|
|
879
|
+
});
|
|
1161
880
|
ui.mount();
|
|
1162
|
-
printInfo(`Interactive mode for ${
|
|
1163
|
-
|
|
1164
|
-
|
|
881
|
+
printInfo(`Interactive mode for ${config.jiraIssueKey}`);
|
|
882
|
+
printInfo("Use h to see help.");
|
|
883
|
+
try {
|
|
884
|
+
ui.setBusy(true, "preflight");
|
|
885
|
+
const preflightState = await runPreflightFlow(createPipelineContext({
|
|
886
|
+
issueKey: config.taskKey,
|
|
887
|
+
jiraRef: config.jiraRef,
|
|
888
|
+
dryRun: false,
|
|
889
|
+
verbose: config.verbose,
|
|
890
|
+
runtime: runtimeServices,
|
|
891
|
+
setSummary: (markdown) => {
|
|
892
|
+
ui.setSummary(markdown);
|
|
893
|
+
},
|
|
894
|
+
}), {
|
|
895
|
+
jiraApiUrl: config.jiraApiUrl,
|
|
896
|
+
jiraTaskFile: config.jiraTaskFile,
|
|
897
|
+
taskKey: config.taskKey,
|
|
898
|
+
forceRefresh,
|
|
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
|
+
}
|
|
911
|
+
if (!existsSync(taskSummaryFile(config.taskKey))) {
|
|
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.");
|
|
914
|
+
}
|
|
1165
915
|
}
|
|
1166
|
-
|
|
1167
|
-
|
|
916
|
+
catch (error) {
|
|
917
|
+
if (error instanceof TaskRunnerError) {
|
|
918
|
+
printError(error.message);
|
|
919
|
+
}
|
|
920
|
+
else {
|
|
921
|
+
throw error;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
finally {
|
|
925
|
+
ui.setBusy(false);
|
|
1168
926
|
}
|
|
1169
|
-
printInfo("Use /help to see commands.");
|
|
1170
927
|
return await new Promise((resolve, reject) => {
|
|
1171
928
|
const interval = setInterval(() => {
|
|
1172
929
|
if (!exiting) {
|