agentweaver 0.1.5 → 0.1.7
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/Dockerfile.codex +56 -0
- package/README.md +38 -15
- package/dist/artifacts.js +38 -8
- package/dist/executors/configs/fetch-gitlab-review-config.js +3 -0
- package/dist/executors/fetch-gitlab-review-executor.js +25 -0
- package/dist/flow-state.js +134 -0
- package/dist/gitlab.js +153 -0
- package/dist/index.js +397 -250
- package/dist/interactive-ui.js +170 -42
- package/dist/pipeline/declarative-flow-runner.js +28 -0
- package/dist/pipeline/flow-specs/auto.json +530 -392
- package/dist/pipeline/flow-specs/bug-analyze.json +149 -0
- package/dist/pipeline/flow-specs/gitlab-review.json +347 -0
- package/dist/pipeline/flow-specs/implement.json +0 -9
- package/dist/pipeline/flow-specs/plan.json +133 -0
- package/dist/pipeline/flow-specs/review-fix.json +2 -11
- package/dist/pipeline/flow-specs/review-project.json +243 -0
- package/dist/pipeline/flow-specs/run-go-linter-loop.json +155 -0
- package/dist/pipeline/flow-specs/run-go-tests-loop.json +155 -0
- package/dist/pipeline/flow-specs/run-linter-loop.json +17 -11
- package/dist/pipeline/flow-specs/run-tests-loop.json +17 -11
- package/dist/pipeline/flow-specs/task-describe.json +25 -0
- package/dist/pipeline/node-registry.js +28 -1
- package/dist/pipeline/nodes/fetch-gitlab-review-node.js +34 -0
- package/dist/pipeline/nodes/gitlab-review-artifacts-node.js +105 -0
- package/dist/pipeline/nodes/jira-issue-check-node.js +53 -0
- package/dist/pipeline/nodes/local-script-check-node.js +81 -0
- package/dist/pipeline/nodes/review-findings-form-node.js +14 -14
- package/dist/pipeline/prompt-registry.js +5 -5
- package/dist/pipeline/registry.js +2 -0
- package/dist/pipeline/value-resolver.js +7 -1
- package/dist/prompts.js +11 -4
- package/dist/scope.js +118 -0
- package/dist/structured-artifacts.js +33 -0
- package/docker-compose.yml +445 -0
- package/package.json +8 -3
- package/run_go_coverage.sh +113 -0
- package/run_go_linter.sh +89 -0
- package/run_go_tests.sh +83 -0
- package/verify_build.sh +105 -0
- package/dist/executors/claude-summary-executor.js +0 -31
- package/dist/executors/configs/claude-summary-config.js +0 -8
- package/dist/pipeline/flow-runner.js +0 -13
- package/dist/pipeline/flow-specs/test-fix.json +0 -24
- package/dist/pipeline/flow-specs/test-linter-fix.json +0 -24
- package/dist/pipeline/flow-specs/test.json +0 -19
- package/dist/pipeline/flow-types.js +0 -1
- package/dist/pipeline/flows/implement-flow.js +0 -47
- package/dist/pipeline/flows/plan-flow.js +0 -42
- package/dist/pipeline/flows/review-fix-flow.js +0 -62
- package/dist/pipeline/flows/review-flow.js +0 -124
- package/dist/pipeline/flows/test-fix-flow.js +0 -12
- package/dist/pipeline/flows/test-flow.js +0 -32
- package/dist/pipeline/nodes/claude-summary-node.js +0 -38
- package/dist/pipeline/nodes/implement-codex-node.js +0 -16
- package/dist/pipeline/nodes/task-summary-node.js +0 -42
package/dist/index.js
CHANGED
|
@@ -3,36 +3,35 @@ import { existsSync, readdirSync, readFileSync, rmSync, writeFileSync } from "no
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import process from "node:process";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
import { REVIEW_FILE_RE, REVIEW_REPLY_FILE_RE, autoStateFile, bugAnalyzeArtifacts, bugAnalyzeJsonFile, bugFixDesignJsonFile, bugFixPlanJsonFile, designJsonFile,
|
|
6
|
+
import { REVIEW_FILE_RE, REVIEW_REPLY_FILE_RE, autoStateFile, bugAnalyzeArtifacts, bugAnalyzeJsonFile, bugFixDesignJsonFile, bugFixPlanJsonFile, designJsonFile, ensureScopeWorkspaceDir, gitlabReviewFile, gitlabReviewJsonFile, planJsonFile, planArtifacts, qaJsonFile, readyToMergeFile, requireArtifacts, reviewReplyJsonFile, reviewFixSelectionJsonFile, reviewJsonFile, scopeWorkspaceDir, taskSummaryFile, } from "./artifacts.js";
|
|
7
7
|
import { TaskRunnerError } from "./errors.js";
|
|
8
|
-
import {
|
|
8
|
+
import { createFlowRunState, hasResumableFlowState, loadFlowRunState, prepareFlowStateForResume, resetFlowRunState, saveFlowRunState, stripExecutionStatePayload, } from "./flow-state.js";
|
|
9
|
+
import { requireJiraTaskFile } from "./jira.js";
|
|
9
10
|
import { validateStructuredArtifacts } from "./structured-artifacts.js";
|
|
10
11
|
import { summarizeBuildFailure as summarizeBuildFailureViaPipeline } from "./pipeline/build-failure-summary.js";
|
|
11
12
|
import { createPipelineContext } from "./pipeline/context.js";
|
|
12
13
|
import { loadAutoFlow } from "./pipeline/auto-flow.js";
|
|
13
14
|
import { loadDeclarativeFlow } from "./pipeline/declarative-flows.js";
|
|
14
15
|
import { findPhaseById, runExpandedPhase } from "./pipeline/declarative-flow-runner.js";
|
|
15
|
-
import { runPreflightFlow } from "./pipeline/flows/preflight-flow.js";
|
|
16
16
|
import { resolveCmd, resolveDockerComposeCmd } from "./runtime/command-resolution.js";
|
|
17
|
-
import { defaultDockerComposeFile, dockerRuntimeEnv } from "./runtime/docker-runtime.js";
|
|
17
|
+
import { agentweaverHome, defaultDockerComposeFile, dockerRuntimeEnv } from "./runtime/docker-runtime.js";
|
|
18
18
|
import { runCommand } from "./runtime/process-runner.js";
|
|
19
19
|
import { InteractiveUi } from "./interactive-ui.js";
|
|
20
20
|
import { bye, printError, printInfo, printPanel, printSummary, setFlowExecutionState, stripAnsi, } from "./tui.js";
|
|
21
21
|
import { requestUserInputInTerminal } from "./user-input.js";
|
|
22
|
+
import { detectGitBranchName, requestTaskScope, resolveProjectScope, resolveTaskScope, } from "./scope.js";
|
|
22
23
|
const COMMANDS = [
|
|
23
24
|
"bug-analyze",
|
|
24
25
|
"bug-fix",
|
|
26
|
+
"gitlab-review",
|
|
25
27
|
"mr-description",
|
|
26
28
|
"plan",
|
|
27
29
|
"task-describe",
|
|
28
30
|
"implement",
|
|
29
31
|
"review",
|
|
30
32
|
"review-fix",
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"test-linter-fix",
|
|
34
|
-
"run-tests-loop",
|
|
35
|
-
"run-linter-loop",
|
|
33
|
+
"run-go-tests-loop",
|
|
34
|
+
"run-go-linter-loop",
|
|
36
35
|
"auto",
|
|
37
36
|
"auto-status",
|
|
38
37
|
"auto-reset",
|
|
@@ -79,36 +78,37 @@ function formatProcessFailure(error) {
|
|
|
79
78
|
}
|
|
80
79
|
function usage() {
|
|
81
80
|
return `Usage:
|
|
81
|
+
agentweaver
|
|
82
82
|
agentweaver <jira-browse-url|jira-issue-key>
|
|
83
83
|
agentweaver --force <jira-browse-url|jira-issue-key>
|
|
84
|
+
agentweaver gitlab-review [--dry] [--verbose] [--prompt <text>] [--scope <name>] <jira-browse-url|jira-issue-key>
|
|
84
85
|
agentweaver bug-analyze [--dry] [--verbose] [--prompt <text>] <jira-browse-url|jira-issue-key>
|
|
85
86
|
agentweaver bug-fix [--dry] [--verbose] [--prompt <text>] <jira-browse-url|jira-issue-key>
|
|
86
87
|
agentweaver mr-description [--dry] [--verbose] [--prompt <text>] <jira-browse-url|jira-issue-key>
|
|
87
|
-
agentweaver plan [--dry] [--verbose] [--prompt <text>] <jira-browse-url|jira-issue-key>
|
|
88
|
+
agentweaver plan [--dry] [--verbose] [--prompt <text>] [<jira-browse-url|jira-issue-key>]
|
|
88
89
|
agentweaver task-describe [--dry] [--verbose] [--prompt <text>] <jira-browse-url|jira-issue-key>
|
|
89
|
-
agentweaver implement [--dry] [--verbose] [--prompt <text>] <jira-browse-url|jira-issue-key>
|
|
90
|
-
agentweaver review [--dry] [--verbose] [--prompt <text>] <jira-browse-url|jira-issue-key>
|
|
91
|
-
agentweaver review-fix [--dry] [--verbose] [--prompt <text>] <jira-browse-url|jira-issue-key>
|
|
92
|
-
agentweaver
|
|
93
|
-
agentweaver
|
|
94
|
-
agentweaver
|
|
95
|
-
agentweaver
|
|
96
|
-
agentweaver run-linter-loop [--dry] [--verbose] [--prompt <text>] <jira-browse-url|jira-issue-key>
|
|
97
|
-
agentweaver auto [--dry] [--verbose] [--prompt <text>] <jira-browse-url|jira-issue-key>
|
|
98
|
-
agentweaver auto [--dry] [--verbose] [--prompt <text>] --from <phase> <jira-browse-url|jira-issue-key>
|
|
90
|
+
agentweaver implement [--dry] [--verbose] [--prompt <text>] [--scope <name>] [<jira-browse-url|jira-issue-key>]
|
|
91
|
+
agentweaver review [--dry] [--verbose] [--prompt <text>] [--scope <name>] [<jira-browse-url|jira-issue-key>]
|
|
92
|
+
agentweaver review-fix [--dry] [--verbose] [--prompt <text>] [--scope <name>] [<jira-browse-url|jira-issue-key>]
|
|
93
|
+
agentweaver run-go-tests-loop [--dry] [--verbose] [--prompt <text>] [--scope <name>] [<jira-browse-url|jira-issue-key>]
|
|
94
|
+
agentweaver run-go-linter-loop [--dry] [--verbose] [--prompt <text>] [--scope <name>] [<jira-browse-url|jira-issue-key>]
|
|
95
|
+
agentweaver auto [--dry] [--verbose] [--prompt <text>] [<jira-browse-url|jira-issue-key>]
|
|
96
|
+
agentweaver auto [--dry] [--verbose] [--prompt <text>] --from <phase> [<jira-browse-url|jira-issue-key>]
|
|
99
97
|
agentweaver auto --help-phases
|
|
100
|
-
agentweaver auto-status <jira-browse-url|jira-issue-key>
|
|
101
|
-
agentweaver auto-reset <jira-browse-url|jira-issue-key>
|
|
98
|
+
agentweaver auto-status [<jira-browse-url|jira-issue-key>]
|
|
99
|
+
agentweaver auto-reset [<jira-browse-url|jira-issue-key>]
|
|
102
100
|
|
|
103
101
|
Interactive Mode:
|
|
104
|
-
When started
|
|
102
|
+
When started without a command, the script opens an interactive UI.
|
|
103
|
+
If a Jira task is provided, interactive mode starts in task scope; otherwise it starts in project scope.
|
|
105
104
|
Use Up/Down to select a flow, Enter to confirm launch, h for help, q to exit.
|
|
106
105
|
|
|
107
106
|
Flags:
|
|
108
107
|
--version Show package version
|
|
109
|
-
--force In interactive mode,
|
|
108
|
+
--force In interactive mode, regenerate task summary in Jira-backed flows
|
|
110
109
|
--dry Fetch Jira task, but print docker/codex/claude commands instead of executing them
|
|
111
110
|
--verbose Show live stdout/stderr of launched commands
|
|
111
|
+
--scope Explicit workflow scope name for non-Jira runs
|
|
112
112
|
--prompt Extra prompt text appended to the base prompt
|
|
113
113
|
|
|
114
114
|
Required environment variables:
|
|
@@ -116,12 +116,17 @@ Required environment variables:
|
|
|
116
116
|
|
|
117
117
|
Optional environment variables:
|
|
118
118
|
JIRA_BASE_URL
|
|
119
|
+
GITLAB_TOKEN
|
|
119
120
|
AGENTWEAVER_HOME
|
|
120
121
|
DOCKER_COMPOSE_BIN
|
|
121
122
|
CODEX_BIN
|
|
122
123
|
CODEX_MODEL
|
|
123
124
|
CLAUDE_BIN
|
|
124
|
-
CLAUDE_MODEL
|
|
125
|
+
CLAUDE_MODEL
|
|
126
|
+
|
|
127
|
+
Notes:
|
|
128
|
+
- Task-only flows will ask for Jira task via user-input when it is not passed as an argument.
|
|
129
|
+
- Scope-flexible flows use the current git branch by default when Jira task is not provided.`;
|
|
125
130
|
}
|
|
126
131
|
function packageVersion() {
|
|
127
132
|
const packageJsonPath = path.join(PACKAGE_ROOT, "package.json");
|
|
@@ -173,28 +178,6 @@ function createAutoPipelineState(config) {
|
|
|
173
178
|
},
|
|
174
179
|
};
|
|
175
180
|
}
|
|
176
|
-
function stripExecutionStatePayload(executionState) {
|
|
177
|
-
return {
|
|
178
|
-
flowKind: executionState.flowKind,
|
|
179
|
-
flowVersion: executionState.flowVersion,
|
|
180
|
-
terminated: executionState.terminated,
|
|
181
|
-
...(executionState.terminationReason ? { terminationReason: executionState.terminationReason } : {}),
|
|
182
|
-
phases: executionState.phases.map((phase) => ({
|
|
183
|
-
id: phase.id,
|
|
184
|
-
status: phase.status,
|
|
185
|
-
repeatVars: { ...phase.repeatVars },
|
|
186
|
-
...(phase.startedAt ? { startedAt: phase.startedAt } : {}),
|
|
187
|
-
...(phase.finishedAt ? { finishedAt: phase.finishedAt } : {}),
|
|
188
|
-
steps: phase.steps.map((step) => ({
|
|
189
|
-
id: step.id,
|
|
190
|
-
status: step.status,
|
|
191
|
-
...(step.startedAt ? { startedAt: step.startedAt } : {}),
|
|
192
|
-
...(step.finishedAt ? { finishedAt: step.finishedAt } : {}),
|
|
193
|
-
...(step.stopFlow !== undefined ? { stopFlow: step.stopFlow } : {}),
|
|
194
|
-
})),
|
|
195
|
-
})),
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
181
|
function loadAutoPipelineState(config) {
|
|
199
182
|
const filePath = autoStateFile(config.taskKey);
|
|
200
183
|
if (!existsSync(filePath)) {
|
|
@@ -228,7 +211,7 @@ function loadAutoPipelineState(config) {
|
|
|
228
211
|
}
|
|
229
212
|
function saveAutoPipelineState(state) {
|
|
230
213
|
state.updatedAt = nowIso8601();
|
|
231
|
-
|
|
214
|
+
ensureScopeWorkspaceDir(state.issueKey);
|
|
232
215
|
writeFileSync(autoStateFile(state.issueKey), `${JSON.stringify({
|
|
233
216
|
...state,
|
|
234
217
|
executionState: stripExecutionStatePayload(state.executionState),
|
|
@@ -327,6 +310,30 @@ function syncAutoStepsFromExecutionState(state) {
|
|
|
327
310
|
state.currentStep = findCurrentExecutionStep(state);
|
|
328
311
|
state.status = deriveAutoPipelineStatus(state);
|
|
329
312
|
}
|
|
313
|
+
function buildAutoResumeDetails(state) {
|
|
314
|
+
const currentStep = findCurrentExecutionStep(state) ?? state.currentStep ?? "-";
|
|
315
|
+
const lines = [
|
|
316
|
+
"Interrupted auto run found.",
|
|
317
|
+
`Current step: ${currentStep}`,
|
|
318
|
+
`Updated: ${state.updatedAt}`,
|
|
319
|
+
];
|
|
320
|
+
if (state.lastError) {
|
|
321
|
+
lines.push(`Last error: ${state.lastError.message ?? "-"} (exit ${state.lastError.returnCode ?? "-"})`);
|
|
322
|
+
}
|
|
323
|
+
return lines.join("\n");
|
|
324
|
+
}
|
|
325
|
+
function buildFlowResumeDetails(state) {
|
|
326
|
+
const currentStep = findCurrentFlowExecutionStep(state) ?? state.currentStep ?? "-";
|
|
327
|
+
const lines = [
|
|
328
|
+
"Interrupted run found.",
|
|
329
|
+
`Current step: ${currentStep}`,
|
|
330
|
+
`Updated: ${state.updatedAt}`,
|
|
331
|
+
];
|
|
332
|
+
if (state.lastError) {
|
|
333
|
+
lines.push(`Last error: ${state.lastError.message ?? "-"} (exit ${state.lastError.returnCode ?? "-"})`);
|
|
334
|
+
}
|
|
335
|
+
return lines.join("\n");
|
|
336
|
+
}
|
|
330
337
|
function printAutoPhasesHelp() {
|
|
331
338
|
const phaseLines = ["Available auto phases:", "", ...autoPhaseIds()];
|
|
332
339
|
phaseLines.push("", "You can resume auto from a phase with:", "agentweaver auto --from <phase> <jira>", "or in interactive mode:", "/auto --from <phase>");
|
|
@@ -362,7 +369,7 @@ function loadEnvFile(envFilePath) {
|
|
|
362
369
|
}
|
|
363
370
|
function nextReviewIterationForTask(taskKey) {
|
|
364
371
|
let maxIndex = 0;
|
|
365
|
-
const workspaceDir =
|
|
372
|
+
const workspaceDir = scopeWorkspaceDir(taskKey);
|
|
366
373
|
if (!existsSync(workspaceDir)) {
|
|
367
374
|
return 1;
|
|
368
375
|
}
|
|
@@ -379,7 +386,7 @@ function nextReviewIterationForTask(taskKey) {
|
|
|
379
386
|
}
|
|
380
387
|
function latestReviewReplyIteration(taskKey) {
|
|
381
388
|
let maxIndex = null;
|
|
382
|
-
const workspaceDir =
|
|
389
|
+
const workspaceDir = scopeWorkspaceDir(taskKey);
|
|
383
390
|
if (!existsSync(workspaceDir)) {
|
|
384
391
|
return null;
|
|
385
392
|
}
|
|
@@ -395,23 +402,81 @@ function latestReviewReplyIteration(taskKey) {
|
|
|
395
402
|
}
|
|
396
403
|
return maxIndex;
|
|
397
404
|
}
|
|
398
|
-
function
|
|
399
|
-
const
|
|
400
|
-
ensureTaskWorkspaceDir(jiraIssueKey);
|
|
405
|
+
function buildBaseConfig(command, options = {}) {
|
|
406
|
+
const homeDir = agentweaverHome(PACKAGE_ROOT);
|
|
401
407
|
return {
|
|
402
408
|
command,
|
|
403
|
-
jiraRef,
|
|
409
|
+
jiraRef: options.jiraRef ?? null,
|
|
410
|
+
scopeName: options.scopeName ?? null,
|
|
404
411
|
reviewFixPoints: options.reviewFixPoints ?? null,
|
|
405
412
|
extraPrompt: options.extraPrompt ?? null,
|
|
406
413
|
autoFromPhase: options.autoFromPhase ? validateAutoPhaseId(options.autoFromPhase) : null,
|
|
407
414
|
dryRun: options.dryRun ?? false,
|
|
408
415
|
verbose: options.verbose ?? false,
|
|
409
416
|
dockerComposeFile: defaultDockerComposeFile(PACKAGE_ROOT),
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
417
|
+
runGoTestsScript: path.join(homeDir, "run_go_tests.sh"),
|
|
418
|
+
runGoLinterScript: path.join(homeDir, "run_go_linter.sh"),
|
|
419
|
+
runGoCoverageScript: path.join(homeDir, "run_go_coverage.sh"),
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
function commandRequiresTask(command) {
|
|
423
|
+
return (command === "plan" ||
|
|
424
|
+
command === "bug-analyze" ||
|
|
425
|
+
command === "bug-fix" ||
|
|
426
|
+
command === "gitlab-review" ||
|
|
427
|
+
command === "mr-description" ||
|
|
428
|
+
command === "task-describe" ||
|
|
429
|
+
command === "auto" ||
|
|
430
|
+
command === "auto-status" ||
|
|
431
|
+
command === "auto-reset");
|
|
432
|
+
}
|
|
433
|
+
function commandSupportsProjectScope(command) {
|
|
434
|
+
return (command === "implement" ||
|
|
435
|
+
command === "review" ||
|
|
436
|
+
command === "review-fix" ||
|
|
437
|
+
command === "run-go-tests-loop" ||
|
|
438
|
+
command === "run-go-linter-loop");
|
|
439
|
+
}
|
|
440
|
+
async function resolveScopeForCommand(config, requestUserInput) {
|
|
441
|
+
if (config.jiraRef?.trim()) {
|
|
442
|
+
return resolveTaskScope(config.jiraRef, config.scopeName);
|
|
443
|
+
}
|
|
444
|
+
if (commandRequiresTask(config.command)) {
|
|
445
|
+
try {
|
|
446
|
+
const taskScope = await requestTaskScope(requestUserInput);
|
|
447
|
+
return config.scopeName ? resolveTaskScope(taskScope.jiraRef, config.scopeName) : taskScope;
|
|
448
|
+
}
|
|
449
|
+
catch (error) {
|
|
450
|
+
if (error instanceof TaskRunnerError && error.message.includes("no TTY is available")) {
|
|
451
|
+
throw new TaskRunnerError(`Command '${config.command}' requires a Jira task.\n` +
|
|
452
|
+
"Pass Jira issue key / browse URL as an argument, or run the command in an interactive terminal.");
|
|
453
|
+
}
|
|
454
|
+
throw error;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
if (commandSupportsProjectScope(config.command)) {
|
|
458
|
+
return resolveProjectScope(config.scopeName);
|
|
459
|
+
}
|
|
460
|
+
throw new TaskRunnerError(`Unsupported scope policy for command: ${config.command}`);
|
|
461
|
+
}
|
|
462
|
+
function buildRuntimeConfig(baseConfig, scope) {
|
|
463
|
+
ensureScopeWorkspaceDir(scope.scopeKey);
|
|
464
|
+
if (scope.scopeType === "task") {
|
|
465
|
+
return {
|
|
466
|
+
...baseConfig,
|
|
467
|
+
scope,
|
|
468
|
+
taskKey: scope.scopeKey,
|
|
469
|
+
jiraRef: scope.jiraRef,
|
|
470
|
+
jiraBrowseUrl: scope.jiraBrowseUrl,
|
|
471
|
+
jiraApiUrl: scope.jiraApiUrl,
|
|
472
|
+
jiraTaskFile: scope.jiraTaskFile,
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
return {
|
|
476
|
+
...baseConfig,
|
|
477
|
+
scope,
|
|
478
|
+
taskKey: scope.scopeKey,
|
|
479
|
+
jiraRef: scope.scopeKey,
|
|
415
480
|
};
|
|
416
481
|
}
|
|
417
482
|
function checkPrerequisites(config) {
|
|
@@ -421,40 +486,35 @@ function checkPrerequisites(config) {
|
|
|
421
486
|
config.command === "plan" ||
|
|
422
487
|
config.command === "task-describe" ||
|
|
423
488
|
config.command === "review" ||
|
|
424
|
-
config.command === "run-tests-loop" ||
|
|
425
|
-
config.command === "run-linter-loop") {
|
|
489
|
+
config.command === "run-go-tests-loop" ||
|
|
490
|
+
config.command === "run-go-linter-loop") {
|
|
426
491
|
resolveCmd("codex", "CODEX_BIN");
|
|
427
492
|
}
|
|
428
493
|
if (config.command === "review") {
|
|
429
494
|
resolveCmd("claude", "CLAUDE_BIN");
|
|
430
495
|
}
|
|
431
|
-
if (["implement", "review-fix", "test", "run-tests-loop", "run-linter-loop"].includes(config.command)) {
|
|
432
|
-
resolveDockerComposeCmd();
|
|
433
|
-
if (!existsSync(config.dockerComposeFile)) {
|
|
434
|
-
throw new TaskRunnerError(`docker-compose file not found: ${config.dockerComposeFile}`);
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
496
|
}
|
|
438
497
|
function checkAutoPrerequisites(config) {
|
|
439
498
|
resolveCmd("codex", "CODEX_BIN");
|
|
440
499
|
resolveCmd("claude", "CLAUDE_BIN");
|
|
441
|
-
resolveDockerComposeCmd();
|
|
442
|
-
if (!existsSync(config.dockerComposeFile)) {
|
|
443
|
-
throw new TaskRunnerError(`docker-compose file not found: ${config.dockerComposeFile}`);
|
|
444
|
-
}
|
|
445
500
|
}
|
|
446
|
-
function autoFlowParams(config) {
|
|
501
|
+
function autoFlowParams(config, forceRefreshSummary = false) {
|
|
447
502
|
return {
|
|
448
503
|
jiraApiUrl: config.jiraApiUrl,
|
|
449
504
|
taskKey: config.taskKey,
|
|
450
505
|
dockerComposeFile: config.dockerComposeFile,
|
|
506
|
+
runGoTestsScript: config.runGoTestsScript,
|
|
507
|
+
runGoLinterScript: config.runGoLinterScript,
|
|
508
|
+
runGoCoverageScript: config.runGoCoverageScript,
|
|
451
509
|
extraPrompt: config.extraPrompt,
|
|
452
510
|
reviewFixPoints: config.reviewFixPoints,
|
|
511
|
+
forceRefresh: forceRefreshSummary,
|
|
453
512
|
};
|
|
454
513
|
}
|
|
455
514
|
const FLOW_DESCRIPTIONS = {
|
|
456
515
|
auto: "Полный пайплайн задачи: планирование, реализация, проверки, ревью, ответы на ревью и повторные итерации до готовности к merge.",
|
|
457
516
|
"bug-analyze": "Анализирует баг по Jira и создаёт структурированные артефакты: гипотезу причины, дизайн исправления и план работ.",
|
|
517
|
+
"gitlab-review": "Запрашивает GitLab MR URL через user-input, загружает комментарии код-ревью по API и сохраняет markdown плюс structured JSON artifact.",
|
|
458
518
|
"bug-fix": "Берёт результаты bug-analyze как source of truth и реализует исправление бага в коде.",
|
|
459
519
|
"mr-description": "Готовит краткое intent-описание для merge request на основе задачи и текущих изменений.",
|
|
460
520
|
plan: "Загружает задачу из Jira и создаёт дизайн, план реализации и QA-план в structured JSON и markdown.",
|
|
@@ -462,11 +522,8 @@ const FLOW_DESCRIPTIONS = {
|
|
|
462
522
|
implement: "Реализует задачу по утверждённым design/plan артефактам и при необходимости запускает post-verify сборки.",
|
|
463
523
|
review: "Запускает Claude-код-ревью текущих изменений, валидирует structured findings, затем готовит ответ на замечания через Codex.",
|
|
464
524
|
"review-fix": "Исправляет замечания после review-reply, обновляет код и прогоняет обязательные проверки после правок.",
|
|
465
|
-
|
|
466
|
-
"
|
|
467
|
-
"test-linter-fix": "Прогоняет линтер и генерацию, затем исправляет замечания для чистого прогона.",
|
|
468
|
-
"run-tests-loop": "Циклически запускает `./run_tests.sh`, анализирует последнюю ошибку и правит код до успешного прохождения или исчерпания попыток.",
|
|
469
|
-
"run-linter-loop": "Циклически запускает `./run_linter.sh`, исправляет проблемы линтера или генерации и повторяет попытки до успеха.",
|
|
525
|
+
"run-go-tests-loop": "Циклически запускает `./run_go_tests.sh` локально, анализирует последнюю ошибку и правит код до успешного прохождения или исчерпания попыток.",
|
|
526
|
+
"run-go-linter-loop": "Циклически запускает `./run_go_linter.sh` локально, исправляет проблемы линтера или генерации и повторяет попытки до успеха.",
|
|
470
527
|
};
|
|
471
528
|
function flowDescription(id) {
|
|
472
529
|
return FLOW_DESCRIPTIONS[id] ?? "Описание для этого flow пока не задано.";
|
|
@@ -506,64 +563,139 @@ function interactiveFlowDefinitions() {
|
|
|
506
563
|
autoFlowDefinition(),
|
|
507
564
|
declarativeFlowDefinition("bug-analyze", "bug-analyze", "bug-analyze.json"),
|
|
508
565
|
declarativeFlowDefinition("bug-fix", "bug-fix", "bug-fix.json"),
|
|
566
|
+
declarativeFlowDefinition("gitlab-review", "gitlab-review", "gitlab-review.json"),
|
|
509
567
|
declarativeFlowDefinition("mr-description", "mr-description", "mr-description.json"),
|
|
510
568
|
declarativeFlowDefinition("plan", "plan", "plan.json"),
|
|
511
569
|
declarativeFlowDefinition("task-describe", "task-describe", "task-describe.json"),
|
|
512
570
|
declarativeFlowDefinition("implement", "implement", "implement.json"),
|
|
513
571
|
declarativeFlowDefinition("review", "review", "review.json"),
|
|
514
572
|
declarativeFlowDefinition("review-fix", "review-fix", "review-fix.json"),
|
|
515
|
-
declarativeFlowDefinition("
|
|
516
|
-
declarativeFlowDefinition("
|
|
517
|
-
declarativeFlowDefinition("test-linter-fix", "test-linter-fix", "test-linter-fix.json"),
|
|
518
|
-
declarativeFlowDefinition("run-tests-loop", "run-tests-loop", "run-tests-loop.json"),
|
|
519
|
-
declarativeFlowDefinition("run-linter-loop", "run-linter-loop", "run-linter-loop.json"),
|
|
573
|
+
declarativeFlowDefinition("run-go-tests-loop", "run-go-tests-loop", "run-go-tests-loop.json"),
|
|
574
|
+
declarativeFlowDefinition("run-go-linter-loop", "run-go-linter-loop", "run-go-linter-loop.json"),
|
|
520
575
|
];
|
|
521
576
|
}
|
|
522
577
|
function publishFlowState(flowId, executionState) {
|
|
523
578
|
setFlowExecutionState(flowId, stripExecutionStatePayload(executionState));
|
|
524
579
|
}
|
|
525
|
-
|
|
580
|
+
function loadTaskSummaryMarkdown(taskKey) {
|
|
581
|
+
const summaryPath = taskSummaryFile(taskKey);
|
|
582
|
+
if (!existsSync(summaryPath)) {
|
|
583
|
+
return null;
|
|
584
|
+
}
|
|
585
|
+
const markdown = readFileSync(summaryPath, "utf8").trim();
|
|
586
|
+
return markdown.length > 0 ? markdown : null;
|
|
587
|
+
}
|
|
588
|
+
function syncInteractiveTaskSummary(ui, scope, forceRefresh = false) {
|
|
589
|
+
if (scope.scopeType !== "task" || forceRefresh) {
|
|
590
|
+
ui.clearSummary();
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
const summaryMarkdown = loadTaskSummaryMarkdown(scope.scopeKey);
|
|
594
|
+
if (summaryMarkdown) {
|
|
595
|
+
ui.setSummary(summaryMarkdown);
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
ui.clearSummary();
|
|
599
|
+
}
|
|
600
|
+
function findCurrentFlowExecutionStep(state) {
|
|
601
|
+
for (const phase of state.executionState.phases) {
|
|
602
|
+
const runningStep = phase.steps.find((step) => step.status === "running");
|
|
603
|
+
if (runningStep) {
|
|
604
|
+
return `${phase.id}:${runningStep.id}`;
|
|
605
|
+
}
|
|
606
|
+
const pendingStep = phase.steps.find((step) => step.status === "pending");
|
|
607
|
+
if (pendingStep && phase.steps.some((step) => step.status === "done" || step.status === "skipped")) {
|
|
608
|
+
return `${phase.id}:${pendingStep.id}`;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
return null;
|
|
612
|
+
}
|
|
613
|
+
async function runDeclarativeFlowBySpecFile(fileName, config, flowParams, requestUserInput = requestUserInputInTerminal, setSummary, launchMode = "restart") {
|
|
526
614
|
const context = createPipelineContext({
|
|
527
615
|
issueKey: config.taskKey,
|
|
528
616
|
jiraRef: config.jiraRef,
|
|
529
617
|
dryRun: config.dryRun,
|
|
530
618
|
verbose: config.verbose,
|
|
531
619
|
runtime: runtimeServices,
|
|
620
|
+
...(setSummary ? { setSummary } : {}),
|
|
532
621
|
requestUserInput,
|
|
533
622
|
});
|
|
534
623
|
const flow = loadDeclarativeFlow(fileName);
|
|
535
|
-
const
|
|
624
|
+
const initialExecutionState = {
|
|
536
625
|
flowKind: flow.kind,
|
|
537
626
|
flowVersion: flow.version,
|
|
538
627
|
terminated: false,
|
|
539
628
|
phases: [],
|
|
540
629
|
};
|
|
630
|
+
let persistedState = launchMode === "resume" ? loadFlowRunState(config.scope.scopeKey, config.command) : null;
|
|
631
|
+
if (persistedState && launchMode === "resume") {
|
|
632
|
+
persistedState = prepareFlowStateForResume(persistedState);
|
|
633
|
+
}
|
|
634
|
+
else if (launchMode === "restart") {
|
|
635
|
+
resetFlowRunState(config.scope.scopeKey, config.command);
|
|
636
|
+
}
|
|
637
|
+
const executionState = persistedState?.executionState ?? initialExecutionState;
|
|
638
|
+
const state = persistedState ?? createFlowRunState(config.scope.scopeKey, config.command, executionState);
|
|
639
|
+
state.status = "running";
|
|
640
|
+
state.lastError = null;
|
|
641
|
+
state.currentStep = findCurrentFlowExecutionStep(state);
|
|
642
|
+
state.executionState = executionState;
|
|
643
|
+
saveFlowRunState(state);
|
|
541
644
|
publishFlowState(config.command, executionState);
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
645
|
+
try {
|
|
646
|
+
for (const phase of flow.phases) {
|
|
647
|
+
await runExpandedPhase(phase, context, flowParams, flow.constants, {
|
|
648
|
+
executionState,
|
|
649
|
+
flowKind: flow.kind,
|
|
650
|
+
flowVersion: flow.version,
|
|
651
|
+
onStateChange: async (nextExecutionState) => {
|
|
652
|
+
state.executionState = nextExecutionState;
|
|
653
|
+
state.currentStep = findCurrentFlowExecutionStep(state);
|
|
654
|
+
saveFlowRunState(state);
|
|
655
|
+
publishFlowState(config.command, nextExecutionState);
|
|
656
|
+
},
|
|
657
|
+
onStepStart: async (currentPhase, step) => {
|
|
658
|
+
state.currentStep = `${currentPhase.id}:${step.id}`;
|
|
659
|
+
saveFlowRunState(state);
|
|
660
|
+
},
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
state.status = "completed";
|
|
664
|
+
state.currentStep = null;
|
|
665
|
+
state.lastError = null;
|
|
666
|
+
state.executionState = executionState;
|
|
667
|
+
saveFlowRunState(state);
|
|
668
|
+
}
|
|
669
|
+
catch (error) {
|
|
670
|
+
state.status = "blocked";
|
|
671
|
+
state.currentStep = findCurrentFlowExecutionStep(state);
|
|
672
|
+
state.lastError = {
|
|
673
|
+
returnCode: Number(error.returnCode ?? 1),
|
|
674
|
+
message: error.message || "command failed",
|
|
675
|
+
};
|
|
676
|
+
if (state.currentStep) {
|
|
677
|
+
state.lastError.step = state.currentStep;
|
|
678
|
+
}
|
|
679
|
+
state.executionState = executionState;
|
|
680
|
+
saveFlowRunState(state);
|
|
681
|
+
throw error;
|
|
551
682
|
}
|
|
552
683
|
}
|
|
553
|
-
async function runAutoPhaseViaSpec(config, phaseId, executionState, state) {
|
|
684
|
+
async function runAutoPhaseViaSpec(config, phaseId, executionState, state, setSummary, forceRefreshSummary = false) {
|
|
554
685
|
const context = createPipelineContext({
|
|
555
686
|
issueKey: config.taskKey,
|
|
556
687
|
jiraRef: config.jiraRef,
|
|
557
688
|
dryRun: config.dryRun,
|
|
558
689
|
verbose: config.verbose,
|
|
559
690
|
runtime: runtimeServices,
|
|
691
|
+
...(setSummary ? { setSummary } : {}),
|
|
560
692
|
requestUserInput: requestUserInputInTerminal,
|
|
561
693
|
});
|
|
562
694
|
const autoFlow = loadAutoFlow();
|
|
563
695
|
const phase = findPhaseById(autoFlow.phases, phaseId);
|
|
564
696
|
publishFlowState("auto", executionState);
|
|
565
697
|
try {
|
|
566
|
-
const result = await runExpandedPhase(phase, context, autoFlowParams(config), autoFlow.constants, {
|
|
698
|
+
const result = await runExpandedPhase(phase, context, autoFlowParams(config, forceRefreshSummary), autoFlow.constants, {
|
|
567
699
|
executionState,
|
|
568
700
|
flowKind: autoFlow.kind,
|
|
569
701
|
flowVersion: autoFlow.version,
|
|
@@ -635,9 +767,18 @@ async function summarizeBuildFailure(output) {
|
|
|
635
767
|
requestUserInput: requestUserInputInTerminal,
|
|
636
768
|
}), output);
|
|
637
769
|
}
|
|
638
|
-
|
|
770
|
+
function requireTaskScopeConfig(config) {
|
|
771
|
+
if (config.scope.scopeType !== "task" || !config.jiraBrowseUrl || !config.jiraApiUrl || !config.jiraTaskFile) {
|
|
772
|
+
throw new TaskRunnerError(`Command '${config.command}' requires Jira task scope.`);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
async function executeCommand(baseConfig, runFollowupVerify = true, requestUserInput = requestUserInputInTerminal, resolvedScope, setSummary, forceRefreshSummary = false, launchMode = "restart") {
|
|
776
|
+
const config = buildRuntimeConfig(baseConfig, resolvedScope ?? (await resolveScopeForCommand(baseConfig, requestUserInput)));
|
|
639
777
|
if (config.command === "auto") {
|
|
640
|
-
|
|
778
|
+
if (launchMode === "restart") {
|
|
779
|
+
resetAutoPipelineState(config);
|
|
780
|
+
}
|
|
781
|
+
await runAutoPipeline(config, setSummary, forceRefreshSummary);
|
|
641
782
|
return false;
|
|
642
783
|
}
|
|
643
784
|
if (config.command === "auto-status") {
|
|
@@ -655,10 +796,18 @@ async function executeCommand(config, runFollowupVerify = true, requestUserInput
|
|
|
655
796
|
return false;
|
|
656
797
|
}
|
|
657
798
|
checkPrerequisites(config);
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
799
|
+
if (config.scope.scopeType === "task") {
|
|
800
|
+
process.env.JIRA_BROWSE_URL = config.jiraBrowseUrl ?? "";
|
|
801
|
+
process.env.JIRA_API_URL = config.jiraApiUrl ?? "";
|
|
802
|
+
process.env.JIRA_TASK_FILE = config.jiraTaskFile ?? "";
|
|
803
|
+
}
|
|
804
|
+
else {
|
|
805
|
+
delete process.env.JIRA_BROWSE_URL;
|
|
806
|
+
delete process.env.JIRA_API_URL;
|
|
807
|
+
delete process.env.JIRA_TASK_FILE;
|
|
808
|
+
}
|
|
661
809
|
if (config.command === "plan") {
|
|
810
|
+
requireTaskScopeConfig(config);
|
|
662
811
|
if (config.verbose) {
|
|
663
812
|
process.stdout.write(`Fetching Jira issue from browse URL: ${config.jiraBrowseUrl}\n`);
|
|
664
813
|
process.stdout.write(`Resolved Jira API URL: ${config.jiraApiUrl}\n`);
|
|
@@ -668,10 +817,12 @@ async function executeCommand(config, runFollowupVerify = true, requestUserInput
|
|
|
668
817
|
jiraApiUrl: config.jiraApiUrl,
|
|
669
818
|
taskKey: config.taskKey,
|
|
670
819
|
extraPrompt: config.extraPrompt,
|
|
671
|
-
|
|
820
|
+
forceRefresh: forceRefreshSummary,
|
|
821
|
+
}, requestUserInput, setSummary, launchMode);
|
|
672
822
|
return false;
|
|
673
823
|
}
|
|
674
824
|
if (config.command === "bug-analyze") {
|
|
825
|
+
requireTaskScopeConfig(config);
|
|
675
826
|
if (config.verbose) {
|
|
676
827
|
process.stdout.write(`Fetching Jira issue from browse URL: ${config.jiraBrowseUrl}\n`);
|
|
677
828
|
process.stdout.write(`Resolved Jira API URL: ${config.jiraApiUrl}\n`);
|
|
@@ -681,10 +832,30 @@ async function executeCommand(config, runFollowupVerify = true, requestUserInput
|
|
|
681
832
|
jiraApiUrl: config.jiraApiUrl,
|
|
682
833
|
taskKey: config.taskKey,
|
|
683
834
|
extraPrompt: config.extraPrompt,
|
|
684
|
-
|
|
835
|
+
forceRefresh: forceRefreshSummary,
|
|
836
|
+
}, requestUserInput, setSummary, launchMode);
|
|
837
|
+
return false;
|
|
838
|
+
}
|
|
839
|
+
if (config.command === "gitlab-review") {
|
|
840
|
+
requireTaskScopeConfig(config);
|
|
841
|
+
requireJiraTaskFile(config.jiraTaskFile);
|
|
842
|
+
validateStructuredArtifacts([
|
|
843
|
+
{ path: designJsonFile(config.taskKey), schemaId: "implementation-design/v1" },
|
|
844
|
+
{ path: planJsonFile(config.taskKey), schemaId: "implementation-plan/v1" },
|
|
845
|
+
], "GitLab-review mode requires valid structured plan artifacts from the planning phase.");
|
|
846
|
+
const iteration = nextReviewIterationForTask(config.taskKey);
|
|
847
|
+
await runDeclarativeFlowBySpecFile("gitlab-review.json", config, {
|
|
848
|
+
taskKey: config.taskKey,
|
|
849
|
+
iteration,
|
|
850
|
+
extraPrompt: config.extraPrompt,
|
|
851
|
+
}, requestUserInput, undefined, launchMode);
|
|
852
|
+
if (!config.dryRun) {
|
|
853
|
+
printSummary("GitLab Review", `Artifacts:\n${gitlabReviewFile(config.taskKey)}\n${gitlabReviewJsonFile(config.taskKey)}`);
|
|
854
|
+
}
|
|
685
855
|
return false;
|
|
686
856
|
}
|
|
687
857
|
if (config.command === "bug-fix") {
|
|
858
|
+
requireTaskScopeConfig(config);
|
|
688
859
|
requireJiraTaskFile(config.jiraTaskFile);
|
|
689
860
|
requireArtifacts(bugAnalyzeArtifacts(config.taskKey), "Bug-fix mode requires bug-analyze artifacts from the bug analysis phase.");
|
|
690
861
|
validateStructuredArtifacts([
|
|
@@ -695,69 +866,64 @@ async function executeCommand(config, runFollowupVerify = true, requestUserInput
|
|
|
695
866
|
await runDeclarativeFlowBySpecFile("bug-fix.json", config, {
|
|
696
867
|
taskKey: config.taskKey,
|
|
697
868
|
extraPrompt: config.extraPrompt,
|
|
698
|
-
}, requestUserInput);
|
|
869
|
+
}, requestUserInput, undefined, launchMode);
|
|
699
870
|
return false;
|
|
700
871
|
}
|
|
701
872
|
if (config.command === "mr-description") {
|
|
873
|
+
requireTaskScopeConfig(config);
|
|
702
874
|
requireJiraTaskFile(config.jiraTaskFile);
|
|
703
875
|
await runDeclarativeFlowBySpecFile("mr-description.json", config, {
|
|
704
876
|
taskKey: config.taskKey,
|
|
705
877
|
extraPrompt: config.extraPrompt,
|
|
706
|
-
}, requestUserInput);
|
|
878
|
+
}, requestUserInput, undefined, launchMode);
|
|
707
879
|
return false;
|
|
708
880
|
}
|
|
709
881
|
if (config.command === "task-describe") {
|
|
710
|
-
|
|
882
|
+
requireTaskScopeConfig(config);
|
|
711
883
|
await runDeclarativeFlowBySpecFile("task-describe.json", config, {
|
|
884
|
+
jiraApiUrl: config.jiraApiUrl,
|
|
712
885
|
taskKey: config.taskKey,
|
|
713
886
|
extraPrompt: config.extraPrompt,
|
|
714
|
-
}, requestUserInput);
|
|
887
|
+
}, requestUserInput, undefined, launchMode);
|
|
715
888
|
return false;
|
|
716
889
|
}
|
|
717
890
|
if (config.command === "implement") {
|
|
718
|
-
requireJiraTaskFile(config.jiraTaskFile);
|
|
719
891
|
requireArtifacts(planArtifacts(config.taskKey), "Implement mode requires plan artifacts from the planning phase.");
|
|
720
892
|
validateStructuredArtifacts([
|
|
721
893
|
{ path: designJsonFile(config.taskKey), schemaId: "implementation-design/v1" },
|
|
722
894
|
{ path: planJsonFile(config.taskKey), schemaId: "implementation-plan/v1" },
|
|
723
895
|
{ path: qaJsonFile(config.taskKey), schemaId: "qa-plan/v1" },
|
|
724
896
|
], "Implement mode requires valid structured plan artifacts from the planning phase.");
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
extraPrompt: config.extraPrompt,
|
|
730
|
-
runFollowupVerify,
|
|
731
|
-
}, requestUserInput);
|
|
732
|
-
}
|
|
733
|
-
catch (error) {
|
|
734
|
-
if (!config.dryRun) {
|
|
735
|
-
const output = String(error.output ?? "");
|
|
736
|
-
if (output.trim()) {
|
|
737
|
-
printError("Build verification failed");
|
|
738
|
-
printSummary("Build Failure Summary", await summarizeBuildFailure(output));
|
|
739
|
-
}
|
|
740
|
-
}
|
|
741
|
-
throw error;
|
|
742
|
-
}
|
|
897
|
+
await runDeclarativeFlowBySpecFile("implement.json", config, {
|
|
898
|
+
taskKey: config.taskKey,
|
|
899
|
+
extraPrompt: config.extraPrompt,
|
|
900
|
+
}, requestUserInput, undefined, launchMode);
|
|
743
901
|
return false;
|
|
744
902
|
}
|
|
745
903
|
if (config.command === "review") {
|
|
746
|
-
requireJiraTaskFile(config.jiraTaskFile);
|
|
747
|
-
validateStructuredArtifacts([
|
|
748
|
-
{ path: designJsonFile(config.taskKey), schemaId: "implementation-design/v1" },
|
|
749
|
-
{ path: planJsonFile(config.taskKey), schemaId: "implementation-plan/v1" },
|
|
750
|
-
], "Review mode requires valid structured plan artifacts from the planning phase.");
|
|
751
904
|
const iteration = nextReviewIterationForTask(config.taskKey);
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
905
|
+
if (config.scope.scopeType === "task") {
|
|
906
|
+
requireTaskScopeConfig(config);
|
|
907
|
+
validateStructuredArtifacts([
|
|
908
|
+
{ path: designJsonFile(config.taskKey), schemaId: "implementation-design/v1" },
|
|
909
|
+
{ path: planJsonFile(config.taskKey), schemaId: "implementation-plan/v1" },
|
|
910
|
+
], "Review mode requires valid structured plan artifacts from the planning phase.");
|
|
911
|
+
await runDeclarativeFlowBySpecFile("review.json", config, {
|
|
912
|
+
taskKey: config.taskKey,
|
|
913
|
+
iteration,
|
|
914
|
+
extraPrompt: config.extraPrompt,
|
|
915
|
+
}, requestUserInput, undefined, launchMode);
|
|
916
|
+
}
|
|
917
|
+
else {
|
|
918
|
+
await runDeclarativeFlowBySpecFile("review-project.json", config, {
|
|
919
|
+
taskKey: config.taskKey,
|
|
920
|
+
iteration,
|
|
921
|
+
extraPrompt: config.extraPrompt,
|
|
922
|
+
}, requestUserInput, undefined, launchMode);
|
|
923
|
+
}
|
|
757
924
|
return !config.dryRun && existsSync(readyToMergeFile(config.taskKey));
|
|
758
925
|
}
|
|
759
926
|
if (config.command === "review-fix") {
|
|
760
|
-
requireJiraTaskFile(config.jiraTaskFile);
|
|
761
927
|
const latestIteration = latestReviewReplyIteration(config.taskKey);
|
|
762
928
|
if (latestIteration === null) {
|
|
763
929
|
throw new TaskRunnerError("Review-fix mode requires at least one review-reply artifact.");
|
|
@@ -766,68 +932,27 @@ async function executeCommand(config, runFollowupVerify = true, requestUserInput
|
|
|
766
932
|
{ path: reviewJsonFile(config.taskKey, latestIteration), schemaId: "review-findings/v1" },
|
|
767
933
|
{ path: reviewReplyJsonFile(config.taskKey, latestIteration), schemaId: "review-reply/v1" },
|
|
768
934
|
], "Review-fix mode requires valid structured review artifacts.");
|
|
769
|
-
|
|
770
|
-
await runDeclarativeFlowBySpecFile("review-fix.json", config, {
|
|
771
|
-
taskKey: config.taskKey,
|
|
772
|
-
dockerComposeFile: config.dockerComposeFile,
|
|
773
|
-
latestIteration,
|
|
774
|
-
reviewFixSelectionJsonFile: reviewFixSelectionJsonFile(config.taskKey, latestIteration),
|
|
775
|
-
runFollowupVerify,
|
|
776
|
-
extraPrompt: config.extraPrompt,
|
|
777
|
-
reviewFixPoints: config.reviewFixPoints,
|
|
778
|
-
}, requestUserInput);
|
|
779
|
-
}
|
|
780
|
-
catch (error) {
|
|
781
|
-
if (!config.dryRun) {
|
|
782
|
-
const output = String(error.output ?? "");
|
|
783
|
-
if (output.trim()) {
|
|
784
|
-
printError("Build verification failed");
|
|
785
|
-
printSummary("Build Failure Summary", await summarizeBuildFailure(output));
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
throw error;
|
|
789
|
-
}
|
|
790
|
-
return false;
|
|
791
|
-
}
|
|
792
|
-
if (config.command === "test") {
|
|
793
|
-
requireJiraTaskFile(config.jiraTaskFile);
|
|
794
|
-
try {
|
|
795
|
-
await runDeclarativeFlowBySpecFile("test.json", config, {
|
|
796
|
-
taskKey: config.taskKey,
|
|
797
|
-
dockerComposeFile: config.dockerComposeFile,
|
|
798
|
-
}, requestUserInput);
|
|
799
|
-
}
|
|
800
|
-
catch (error) {
|
|
801
|
-
if (!config.dryRun) {
|
|
802
|
-
const output = String(error.output ?? "");
|
|
803
|
-
if (output.trim()) {
|
|
804
|
-
printError("Build verification failed");
|
|
805
|
-
printSummary("Build Failure Summary", await summarizeBuildFailure(output));
|
|
806
|
-
}
|
|
807
|
-
}
|
|
808
|
-
throw error;
|
|
809
|
-
}
|
|
810
|
-
return false;
|
|
811
|
-
}
|
|
812
|
-
if (config.command === "test-fix" || config.command === "test-linter-fix") {
|
|
813
|
-
requireJiraTaskFile(config.jiraTaskFile);
|
|
814
|
-
await runDeclarativeFlowBySpecFile(config.command === "test-fix" ? "test-fix.json" : "test-linter-fix.json", config, {
|
|
935
|
+
await runDeclarativeFlowBySpecFile("review-fix.json", config, {
|
|
815
936
|
taskKey: config.taskKey,
|
|
937
|
+
latestIteration,
|
|
938
|
+
reviewFixSelectionJsonFile: reviewFixSelectionJsonFile(config.taskKey, latestIteration),
|
|
816
939
|
extraPrompt: config.extraPrompt,
|
|
817
|
-
|
|
940
|
+
reviewFixPoints: config.reviewFixPoints,
|
|
941
|
+
}, requestUserInput, undefined, launchMode);
|
|
818
942
|
return false;
|
|
819
943
|
}
|
|
820
|
-
if (config.command === "run-tests-loop" || config.command === "run-linter-loop") {
|
|
821
|
-
await runDeclarativeFlowBySpecFile(config.command === "run-tests-loop" ? "run-tests-loop.json" : "run-linter-loop.json", config, {
|
|
944
|
+
if (config.command === "run-go-tests-loop" || config.command === "run-go-linter-loop") {
|
|
945
|
+
await runDeclarativeFlowBySpecFile(config.command === "run-go-tests-loop" ? "run-go-tests-loop.json" : "run-go-linter-loop.json", config, {
|
|
822
946
|
taskKey: config.taskKey,
|
|
823
|
-
|
|
947
|
+
runGoTestsScript: config.runGoTestsScript,
|
|
948
|
+
runGoLinterScript: config.runGoLinterScript,
|
|
824
949
|
extraPrompt: config.extraPrompt,
|
|
825
|
-
}, requestUserInput);
|
|
950
|
+
}, requestUserInput, undefined, launchMode);
|
|
826
951
|
return false;
|
|
827
952
|
}
|
|
828
953
|
throw new TaskRunnerError(`Unsupported command: ${config.command}`);
|
|
829
954
|
}
|
|
830
|
-
async function runAutoPipelineDryRun(config) {
|
|
955
|
+
async function runAutoPipelineDryRun(config, setSummary, forceRefreshSummary = false) {
|
|
831
956
|
checkAutoPrerequisites(config);
|
|
832
957
|
printInfo("Dry-run auto pipeline from declarative spec");
|
|
833
958
|
const autoFlow = loadAutoFlow();
|
|
@@ -840,15 +965,16 @@ async function runAutoPipelineDryRun(config) {
|
|
|
840
965
|
publishFlowState("auto", executionState);
|
|
841
966
|
for (const phase of autoFlow.phases) {
|
|
842
967
|
printInfo(`Dry-run auto phase: ${phase.id}`);
|
|
843
|
-
await runAutoPhaseViaSpec(config, phase.id, executionState);
|
|
968
|
+
await runAutoPhaseViaSpec(config, phase.id, executionState, undefined, setSummary, forceRefreshSummary);
|
|
844
969
|
if (executionState.terminated) {
|
|
845
970
|
break;
|
|
846
971
|
}
|
|
847
972
|
}
|
|
848
973
|
}
|
|
849
|
-
async function runAutoPipeline(config) {
|
|
974
|
+
async function runAutoPipeline(config, setSummary, forceRefreshSummary = false) {
|
|
975
|
+
requireTaskScopeConfig(config);
|
|
850
976
|
if (config.dryRun) {
|
|
851
|
-
await runAutoPipelineDryRun(config);
|
|
977
|
+
await runAutoPipelineDryRun(config, setSummary, forceRefreshSummary);
|
|
852
978
|
return;
|
|
853
979
|
}
|
|
854
980
|
checkAutoPrerequisites(config);
|
|
@@ -888,7 +1014,7 @@ async function runAutoPipeline(config) {
|
|
|
888
1014
|
saveAutoPipelineState(state);
|
|
889
1015
|
try {
|
|
890
1016
|
printInfo(`Running auto step: ${step.id}`);
|
|
891
|
-
const status = await runAutoPhaseViaSpec(config, step.id, state.executionState, state);
|
|
1017
|
+
const status = await runAutoPhaseViaSpec(config, step.id, state.executionState, state, setSummary, forceRefreshSummary);
|
|
892
1018
|
step.status = status;
|
|
893
1019
|
step.finishedAt = nowIso8601();
|
|
894
1020
|
step.returnCode = 0;
|
|
@@ -941,6 +1067,7 @@ function parseCliArgs(argv) {
|
|
|
941
1067
|
let verbose = false;
|
|
942
1068
|
let prompt;
|
|
943
1069
|
let autoFromPhase;
|
|
1070
|
+
let scopeName;
|
|
944
1071
|
let helpPhases = false;
|
|
945
1072
|
let jiraRef;
|
|
946
1073
|
for (let index = 1; index < argv.length; index += 1) {
|
|
@@ -962,6 +1089,11 @@ function parseCliArgs(argv) {
|
|
|
962
1089
|
index += 1;
|
|
963
1090
|
continue;
|
|
964
1091
|
}
|
|
1092
|
+
if (token === "--scope") {
|
|
1093
|
+
scopeName = argv[index + 1];
|
|
1094
|
+
index += 1;
|
|
1095
|
+
continue;
|
|
1096
|
+
}
|
|
965
1097
|
if (token === "--from") {
|
|
966
1098
|
autoFromPhase = argv[index + 1];
|
|
967
1099
|
index += 1;
|
|
@@ -973,41 +1105,92 @@ function parseCliArgs(argv) {
|
|
|
973
1105
|
printAutoPhasesHelp();
|
|
974
1106
|
process.exit(0);
|
|
975
1107
|
}
|
|
976
|
-
if (!jiraRef) {
|
|
977
|
-
process.stderr.write(`${usage()}\n`);
|
|
978
|
-
process.exit(1);
|
|
979
|
-
}
|
|
980
1108
|
return {
|
|
981
1109
|
command: command,
|
|
982
|
-
jiraRef,
|
|
983
1110
|
dry,
|
|
984
1111
|
verbose,
|
|
985
1112
|
helpPhases,
|
|
1113
|
+
...(jiraRef !== undefined ? { jiraRef } : {}),
|
|
1114
|
+
...(scopeName !== undefined ? { scopeName } : {}),
|
|
986
1115
|
...(prompt !== undefined ? { prompt } : {}),
|
|
987
1116
|
...(autoFromPhase !== undefined ? { autoFromPhase } : {}),
|
|
988
1117
|
};
|
|
989
1118
|
}
|
|
990
1119
|
function buildConfigFromArgs(args) {
|
|
991
|
-
return
|
|
1120
|
+
return buildBaseConfig(args.command, {
|
|
1121
|
+
...(args.jiraRef !== undefined ? { jiraRef: args.jiraRef } : {}),
|
|
1122
|
+
...(args.scopeName !== undefined ? { scopeName: args.scopeName } : {}),
|
|
992
1123
|
...(args.prompt !== undefined ? { extraPrompt: args.prompt } : {}),
|
|
993
1124
|
...(args.autoFromPhase !== undefined ? { autoFromPhase: args.autoFromPhase } : {}),
|
|
994
1125
|
dryRun: args.dry,
|
|
995
1126
|
verbose: args.verbose,
|
|
996
1127
|
});
|
|
997
1128
|
}
|
|
998
|
-
async function runInteractive(jiraRef, forceRefresh = false) {
|
|
999
|
-
|
|
1000
|
-
const
|
|
1129
|
+
async function runInteractive(jiraRef, forceRefresh = false, scopeName) {
|
|
1130
|
+
let currentScope = jiraRef?.trim() ? resolveTaskScope(jiraRef, scopeName) : resolveProjectScope(scopeName);
|
|
1131
|
+
const currentIssueKey = currentScope.scopeKey;
|
|
1132
|
+
const gitBranchName = detectGitBranchName();
|
|
1001
1133
|
let exiting = false;
|
|
1002
1134
|
const ui = new InteractiveUi({
|
|
1003
|
-
issueKey:
|
|
1004
|
-
summaryText: "
|
|
1135
|
+
issueKey: currentIssueKey,
|
|
1136
|
+
summaryText: "",
|
|
1005
1137
|
cwd: process.cwd(),
|
|
1138
|
+
gitBranchName,
|
|
1006
1139
|
flows: interactiveFlowDefinitions(),
|
|
1007
|
-
|
|
1140
|
+
getRunConfirmation: async (flowId) => {
|
|
1141
|
+
if (flowId === "auto") {
|
|
1142
|
+
if (currentScope.scopeType !== "task") {
|
|
1143
|
+
return { resumeAvailable: false, hasExistingState: false };
|
|
1144
|
+
}
|
|
1145
|
+
const baseConfig = buildBaseConfig("auto", {
|
|
1146
|
+
jiraRef: currentScope.jiraRef,
|
|
1147
|
+
scopeName: currentScope.scopeKey !== currentScope.jiraIssueKey ? currentScope.scopeKey : null,
|
|
1148
|
+
});
|
|
1149
|
+
const state = loadAutoPipelineState(buildRuntimeConfig(baseConfig, currentScope));
|
|
1150
|
+
if (!state) {
|
|
1151
|
+
return { resumeAvailable: false, hasExistingState: false };
|
|
1152
|
+
}
|
|
1153
|
+
const status = deriveAutoPipelineStatus(state);
|
|
1154
|
+
if (status === "completed") {
|
|
1155
|
+
return { resumeAvailable: false, hasExistingState: true };
|
|
1156
|
+
}
|
|
1157
|
+
return {
|
|
1158
|
+
resumeAvailable: true,
|
|
1159
|
+
hasExistingState: true,
|
|
1160
|
+
details: buildAutoResumeDetails(state),
|
|
1161
|
+
};
|
|
1162
|
+
}
|
|
1163
|
+
if (commandRequiresTask(flowId) && currentScope.scopeType !== "task") {
|
|
1164
|
+
return { resumeAvailable: false, hasExistingState: false };
|
|
1165
|
+
}
|
|
1166
|
+
const state = loadFlowRunState(currentScope.scopeKey, flowId);
|
|
1167
|
+
if (!state || !hasResumableFlowState(state)) {
|
|
1168
|
+
return { resumeAvailable: false, hasExistingState: Boolean(state) };
|
|
1169
|
+
}
|
|
1170
|
+
return {
|
|
1171
|
+
resumeAvailable: true,
|
|
1172
|
+
hasExistingState: true,
|
|
1173
|
+
details: buildFlowResumeDetails(state),
|
|
1174
|
+
};
|
|
1175
|
+
},
|
|
1176
|
+
onRun: async (flowId, launchMode) => {
|
|
1008
1177
|
try {
|
|
1009
|
-
const
|
|
1010
|
-
|
|
1178
|
+
const previousScopeType = currentScope.scopeType;
|
|
1179
|
+
const previousScopeKey = currentScope.scopeKey;
|
|
1180
|
+
const baseConfig = buildBaseConfig(flowId, {
|
|
1181
|
+
...(currentScope.scopeType === "task" ? { jiraRef: currentScope.jiraRef } : {}),
|
|
1182
|
+
...(currentScope.scopeType === "task" && currentScope.scopeKey !== currentScope.jiraIssueKey
|
|
1183
|
+
? { scopeName: currentScope.scopeKey }
|
|
1184
|
+
: {}),
|
|
1185
|
+
...(currentScope.scopeType === "project" ? { scopeName: currentScope.scopeKey } : {}),
|
|
1186
|
+
});
|
|
1187
|
+
const nextScope = await resolveScopeForCommand(baseConfig, (form) => ui.requestUserInput(form));
|
|
1188
|
+
currentScope = nextScope;
|
|
1189
|
+
ui.setIssueKey(currentScope.scopeKey);
|
|
1190
|
+
if (currentScope.scopeType === "task" && (previousScopeType !== "task" || previousScopeKey !== currentScope.scopeKey)) {
|
|
1191
|
+
syncInteractiveTaskSummary(ui, currentScope, forceRefresh);
|
|
1192
|
+
}
|
|
1193
|
+
await executeCommand(baseConfig, true, (form) => ui.requestUserInput(form), currentScope, (markdown) => ui.setSummary(markdown), forceRefresh, launchMode);
|
|
1011
1194
|
}
|
|
1012
1195
|
catch (error) {
|
|
1013
1196
|
if (error instanceof TaskRunnerError) {
|
|
@@ -1029,52 +1212,13 @@ async function runInteractive(jiraRef, forceRefresh = false) {
|
|
|
1029
1212
|
},
|
|
1030
1213
|
});
|
|
1031
1214
|
ui.mount();
|
|
1032
|
-
printInfo(`Interactive mode for ${
|
|
1215
|
+
printInfo(`Interactive mode for ${currentScope.scopeKey}`);
|
|
1033
1216
|
printInfo("Use h to see help.");
|
|
1034
|
-
|
|
1035
|
-
ui.
|
|
1036
|
-
const preflightState = await runPreflightFlow(createPipelineContext({
|
|
1037
|
-
issueKey: config.taskKey,
|
|
1038
|
-
jiraRef: config.jiraRef,
|
|
1039
|
-
dryRun: false,
|
|
1040
|
-
verbose: config.verbose,
|
|
1041
|
-
runtime: runtimeServices,
|
|
1042
|
-
setSummary: (markdown) => {
|
|
1043
|
-
ui.setSummary(markdown);
|
|
1044
|
-
},
|
|
1045
|
-
requestUserInput: (form) => ui.requestUserInput(form),
|
|
1046
|
-
}), {
|
|
1047
|
-
jiraApiUrl: config.jiraApiUrl,
|
|
1048
|
-
jiraTaskFile: config.jiraTaskFile,
|
|
1049
|
-
taskKey: config.taskKey,
|
|
1050
|
-
forceRefresh,
|
|
1051
|
-
});
|
|
1052
|
-
const preflightPhase = preflightState.phases.find((phase) => phase.id === "preflight");
|
|
1053
|
-
if (preflightPhase) {
|
|
1054
|
-
ui.appendLog("[preflight] completed");
|
|
1055
|
-
for (const step of preflightPhase.steps) {
|
|
1056
|
-
ui.appendLog(`[preflight] ${step.id}: ${step.status}`);
|
|
1057
|
-
}
|
|
1058
|
-
}
|
|
1059
|
-
if (!existsSync(jiraTaskPath)) {
|
|
1060
|
-
throw new TaskRunnerError(`Preflight finished without Jira task file: ${jiraTaskPath}\n` +
|
|
1061
|
-
"Jira fetch did not complete successfully. Check JIRA_API_KEY and Jira connectivity.");
|
|
1062
|
-
}
|
|
1063
|
-
if (!existsSync(taskSummaryFile(config.taskKey))) {
|
|
1064
|
-
ui.appendLog("[preflight] task summary file was not created");
|
|
1065
|
-
ui.setSummary("Task summary is not available yet. Select and run `plan` or refresh Jira data.");
|
|
1066
|
-
}
|
|
1217
|
+
if (currentScope.scopeType !== "task") {
|
|
1218
|
+
ui.appendLog("[scope] project scope active; task summary will appear after a Jira-backed flow runs");
|
|
1067
1219
|
}
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
printError(error.message);
|
|
1071
|
-
}
|
|
1072
|
-
else {
|
|
1073
|
-
throw error;
|
|
1074
|
-
}
|
|
1075
|
-
}
|
|
1076
|
-
finally {
|
|
1077
|
-
ui.setBusy(false);
|
|
1220
|
+
if (currentScope.scopeType === "task") {
|
|
1221
|
+
syncInteractiveTaskSummary(ui, currentScope, forceRefresh);
|
|
1078
1222
|
}
|
|
1079
1223
|
return await new Promise((resolve, reject) => {
|
|
1080
1224
|
const interval = setInterval(() => {
|
|
@@ -1102,6 +1246,9 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
1102
1246
|
args.shift();
|
|
1103
1247
|
}
|
|
1104
1248
|
try {
|
|
1249
|
+
if (args.length === 0) {
|
|
1250
|
+
return await runInteractive(undefined, forceRefresh);
|
|
1251
|
+
}
|
|
1105
1252
|
if (args.length === 1 && !args[0]?.startsWith("-") && !COMMANDS.includes(args[0])) {
|
|
1106
1253
|
return await runInteractive(args[0] ?? "", forceRefresh);
|
|
1107
1254
|
}
|