agentweaver 0.1.17 → 0.1.18
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 +104 -23
- package/dist/artifacts.js +41 -0
- package/dist/index.js +252 -27
- package/dist/interactive/controller.js +249 -13
- package/dist/interactive/ink/index.js +2 -2
- package/dist/interactive/state.js +1 -0
- package/dist/interactive/web/index.js +179 -0
- package/dist/interactive/web/protocol.js +154 -0
- package/dist/interactive/web/server.js +575 -0
- package/dist/interactive/web/static/app.js +709 -0
- package/dist/interactive/web/static/index.html +77 -0
- package/dist/interactive/web/static/styles.css +2 -0
- package/dist/interactive/web/static/styles.input.css +469 -0
- package/dist/pipeline/flow-catalog.js +4 -0
- package/dist/pipeline/flow-specs/auto-common-guided.json +313 -0
- package/dist/pipeline/flow-specs/auto-common.json +3 -1
- package/dist/pipeline/flow-specs/design-review/design-review-loop.json +2 -0
- package/dist/pipeline/flow-specs/design-review.json +2 -0
- package/dist/pipeline/flow-specs/implement.json +3 -1
- package/dist/pipeline/flow-specs/plan.json +4 -0
- package/dist/pipeline/flow-specs/playbook-init.json +199 -0
- package/dist/pipeline/flow-specs/review/review-fix.json +3 -1
- package/dist/pipeline/flow-specs/review/review-loop.json +4 -0
- package/dist/pipeline/flow-specs/review/review.json +2 -0
- package/dist/pipeline/node-registry.js +45 -0
- package/dist/pipeline/nodes/flow-run-node.js +13 -1
- package/dist/pipeline/nodes/playbook-ensure-node.js +115 -0
- package/dist/pipeline/nodes/playbook-inventory-node.js +51 -0
- package/dist/pipeline/nodes/playbook-questions-form-node.js +166 -0
- package/dist/pipeline/nodes/playbook-write-node.js +243 -0
- package/dist/pipeline/nodes/project-guidance-node.js +69 -0
- package/dist/pipeline/prompt-registry.js +4 -1
- package/dist/pipeline/prompt-runtime.js +6 -2
- package/dist/pipeline/spec-types.js +19 -0
- package/dist/pipeline/value-resolver.js +39 -1
- package/dist/playbook/practice-candidates.js +12 -0
- package/dist/playbook/repo-inventory.js +208 -0
- package/dist/prompts.js +31 -0
- package/dist/runtime/playbook.js +485 -0
- package/dist/runtime/project-guidance.js +339 -0
- package/dist/structured-artifact-schema-registry.js +8 -0
- package/dist/structured-artifact-schemas.json +235 -0
- package/dist/structured-artifacts.js +7 -1
- package/docs/declarative-workflows.md +565 -0
- package/docs/features.md +77 -0
- package/docs/playbook.md +327 -0
- package/package.json +8 -3
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, readFileSync, writeSync } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import process from "node:process";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
@@ -32,12 +32,14 @@ import { clearReadyToMergeFile } from "./runtime/ready-to-merge.js";
|
|
|
32
32
|
import { describeExecutionRouting, executorsForRoutingGroups, resolveExecutionRouting, } from "./runtime/execution-routing.js";
|
|
33
33
|
import { requestInteractiveExecutionRouting } from "./runtime/interactive-execution-routing.js";
|
|
34
34
|
import { createInteractiveSession } from "./interactive/create-interactive-session.js";
|
|
35
|
+
import { createWebInteractiveSession } from "./interactive/web/index.js";
|
|
35
36
|
import { bye, printError, printInfo, printPanel, printSummary, setFlowExecutionState, stripAnsi, } from "./tui.js";
|
|
36
37
|
import { requestUserInputInTerminal } from "./user-input.js";
|
|
37
38
|
import { runDoctorCommand } from "./doctor/index.js";
|
|
38
|
-
import {
|
|
39
|
+
import { requestJiraContext, resolveProjectScope, } from "./scope.js";
|
|
39
40
|
const COMMANDS = [
|
|
40
41
|
"auto-golang",
|
|
42
|
+
"auto-common-guided",
|
|
41
43
|
"auto-common",
|
|
42
44
|
"auto-simple",
|
|
43
45
|
"auto-status",
|
|
@@ -53,7 +55,9 @@ const COMMANDS = [
|
|
|
53
55
|
"mr-description",
|
|
54
56
|
"plan",
|
|
55
57
|
"plan-revise",
|
|
58
|
+
"playbook-init",
|
|
56
59
|
"task-describe",
|
|
60
|
+
"web",
|
|
57
61
|
"implement",
|
|
58
62
|
"review",
|
|
59
63
|
"review-fix",
|
|
@@ -61,8 +65,17 @@ const COMMANDS = [
|
|
|
61
65
|
"run-go-tests-loop",
|
|
62
66
|
"run-go-linter-loop",
|
|
63
67
|
];
|
|
68
|
+
const INTERACTIVE_SCOPE_WATCH_INTERVAL_MS = 1500;
|
|
69
|
+
const WEB_AUTH_USERNAME_ENV = "AGENTWEAVER_WEB_USERNAME";
|
|
70
|
+
const WEB_AUTH_PASSWORD_ENV = "AGENTWEAVER_WEB_PASSWORD";
|
|
64
71
|
const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
65
72
|
const PACKAGE_ROOT = path.resolve(MODULE_DIR, "..");
|
|
73
|
+
function writeStdoutSync(text) {
|
|
74
|
+
writeSync(process.stdout.fd, text);
|
|
75
|
+
}
|
|
76
|
+
function writeStderrSync(text) {
|
|
77
|
+
writeSync(process.stderr.fd, text);
|
|
78
|
+
}
|
|
66
79
|
function createRuntimeServices(signal) {
|
|
67
80
|
return {
|
|
68
81
|
resolveCmd,
|
|
@@ -71,6 +84,37 @@ function createRuntimeServices(signal) {
|
|
|
71
84
|
};
|
|
72
85
|
}
|
|
73
86
|
const runtimeServices = createRuntimeServices();
|
|
87
|
+
function isExternalWebHost(host) {
|
|
88
|
+
const normalized = (host?.trim() || "127.0.0.1").toLowerCase();
|
|
89
|
+
if (normalized === "127.0.0.1" || normalized === "::1" || normalized === "localhost") {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
const unbracketed = normalized.startsWith("[") && normalized.endsWith("]") ? normalized.slice(1, -1) : normalized;
|
|
93
|
+
if (unbracketed === "::1") {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
function resolveWebAuthConfig() {
|
|
99
|
+
const username = process.env[WEB_AUTH_USERNAME_ENV]?.trim() ?? "";
|
|
100
|
+
const password = process.env[WEB_AUTH_PASSWORD_ENV] ?? "";
|
|
101
|
+
const hasUsername = username.length > 0;
|
|
102
|
+
const hasPassword = password.length > 0;
|
|
103
|
+
if (hasUsername !== hasPassword) {
|
|
104
|
+
throw new TaskRunnerError(`Web UI auth requires both ${WEB_AUTH_USERNAME_ENV} and ${WEB_AUTH_PASSWORD_ENV}.`);
|
|
105
|
+
}
|
|
106
|
+
if (!hasUsername || !hasPassword) {
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
return { username, password };
|
|
110
|
+
}
|
|
111
|
+
function requireWebAuthForHost(host, auth) {
|
|
112
|
+
if (!isExternalWebHost(host) || auth) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
throw new TaskRunnerError(`External Web UI binding requires ${WEB_AUTH_USERNAME_ENV} and ${WEB_AUTH_PASSWORD_ENV}. ` +
|
|
116
|
+
"Use localhost for no-auth local access, or configure credentials before using --listen-all or --host with an external interface.");
|
|
117
|
+
}
|
|
74
118
|
function buildFailureOutputPreview(output) {
|
|
75
119
|
const normalized = stripAnsi(output).replace(/\r\n/g, "\n").trim();
|
|
76
120
|
if (!normalized) {
|
|
@@ -110,6 +154,7 @@ function usage() {
|
|
|
110
154
|
agentweaver
|
|
111
155
|
agentweaver <jira-browse-url|jira-issue-key>
|
|
112
156
|
agentweaver --force <jira-browse-url|jira-issue-key>
|
|
157
|
+
agentweaver web [--no-open] [--host <host>|--listen-all] [<jira-browse-url|jira-issue-key>]
|
|
113
158
|
agentweaver git-commit [--dry] [--verbose] [--prompt <text>] [--scope <name>] [<jira-browse-url|jira-issue-key>]
|
|
114
159
|
agentweaver gitlab-diff-review [--dry] [--verbose] [--prompt <text>] [--scope <name>]
|
|
115
160
|
agentweaver gitlab-review [--dry] [--verbose] [--prompt <text>] [--scope <name>]
|
|
@@ -121,6 +166,7 @@ function usage() {
|
|
|
121
166
|
agentweaver mr-description [--dry] [--verbose] [--prompt <text>] <jira-browse-url|jira-issue-key>
|
|
122
167
|
agentweaver plan [--dry] [--verbose] [--prompt <text>] [--md-lang <en|ru>] [<jira-browse-url|jira-issue-key>]
|
|
123
168
|
agentweaver plan-revise [--dry] [--verbose] [--prompt <text>] <jira-browse-url|jira-issue-key>
|
|
169
|
+
agentweaver playbook-init [--dry] [--verbose] [--prompt <text>] [--accept-playbook-draft] [--scope <name>]
|
|
124
170
|
agentweaver task-describe [--dry] [--verbose] [--prompt <text>] [<jira-browse-url|jira-issue-key>]
|
|
125
171
|
agentweaver implement [--dry] [--verbose] [--prompt <text>] [--scope <name>] [<jira-browse-url|jira-issue-key>]
|
|
126
172
|
agentweaver review [--dry] [--verbose] [--prompt <text>] [--scope <name>] [--blocking-severities <list>] [<jira-browse-url|jira-issue-key>]
|
|
@@ -131,6 +177,7 @@ function usage() {
|
|
|
131
177
|
agentweaver auto-golang [--dry] [--verbose] [--prompt <text>] [<jira-browse-url|jira-issue-key>]
|
|
132
178
|
agentweaver auto-golang [--dry] [--verbose] [--prompt <text>] --from <phase> [<jira-browse-url|jira-issue-key>]
|
|
133
179
|
agentweaver auto-golang --help-phases
|
|
180
|
+
agentweaver auto-common-guided [--dry] [--verbose] [--prompt <text>] [--md-lang <en|ru>] [--accept-playbook-draft] <jira-browse-url|jira-issue-key>
|
|
134
181
|
agentweaver auto-common [--dry] [--verbose] [--prompt <text>] [--md-lang <en|ru>] <jira-browse-url|jira-issue-key>
|
|
135
182
|
agentweaver auto-common --help-phases
|
|
136
183
|
agentweaver auto-simple [--dry] [--verbose] [--prompt <text>] [--md-lang <en|ru>] <jira-browse-url|jira-issue-key>
|
|
@@ -146,6 +193,9 @@ Interactive Mode:
|
|
|
146
193
|
Flags:
|
|
147
194
|
--version Show package version
|
|
148
195
|
--force In interactive mode, regenerate task summary in Jira-backed flows
|
|
196
|
+
--no-open Web command only: print the Web UI URL without opening a browser
|
|
197
|
+
--host Web command only: bind Web UI to this host (default: 127.0.0.1)
|
|
198
|
+
--listen-all Web command only: bind Web UI to 0.0.0.0
|
|
149
199
|
--dry Fetch Jira task, but print codex/opencode commands instead of executing them
|
|
150
200
|
--verbose Show live stdout/stderr of launched commands
|
|
151
201
|
--scope Explicit workflow scope name for non-Jira runs except instant-task
|
|
@@ -154,7 +204,8 @@ Flags:
|
|
|
154
204
|
--continue Continue a terminated iterative run when valid
|
|
155
205
|
--restart Archive the active attempt and start a fresh run
|
|
156
206
|
--blocking-severities Comma-separated severities that block merge and drive review-fix auto-selection
|
|
157
|
-
--md-lang Language for markdown
|
|
207
|
+
--md-lang Language for workflow markdown artifacts only: en (English) or ru (Russian, default)
|
|
208
|
+
--accept-playbook-draft Non-interactively accept generated playbook content for playbook-init or auto-common-guided missing-manifest runs
|
|
158
209
|
|
|
159
210
|
Required environment variables:
|
|
160
211
|
JIRA_API_KEY Jira API token used for Jira-backed flows (Bearer by default, or Basic with Jira Cloud)
|
|
@@ -170,9 +221,15 @@ Optional environment variables:
|
|
|
170
221
|
CODEX_MODEL
|
|
171
222
|
OPENCODE_BIN
|
|
172
223
|
OPENCODE_MODEL
|
|
224
|
+
AGENTWEAVER_WEB_NO_OPEN Set to 1 to disable browser auto-open for agentweaver web
|
|
225
|
+
${WEB_AUTH_USERNAME_ENV} Web UI Basic auth username; required for external Web UI binding
|
|
226
|
+
${WEB_AUTH_PASSWORD_ENV} Web UI Basic auth password; required for external Web UI binding
|
|
173
227
|
|
|
174
228
|
Notes:
|
|
175
229
|
- Jira-backed task flows will ask for Jira task via user-input when it is not passed as an argument. task-describe can also work from a manual task description without Jira.
|
|
230
|
+
- agentweaver web binds to 127.0.0.1 by default on an operating-system-assigned port and does not require auth unless Web UI credentials are configured.
|
|
231
|
+
- External Web UI binding through --listen-all, --host 0.0.0.0, --host ::, non-loopback IPs, or hostnames other than localhost requires ${WEB_AUTH_USERNAME_ENV} and ${WEB_AUTH_PASSWORD_ENV}.
|
|
232
|
+
- Web UI Basic auth over plain HTTP is suitable only on trusted networks; use TLS termination or a reverse proxy on untrusted networks.
|
|
176
233
|
- instant-task always uses the current branch-derived project scope and rejects explicit scope overrides or Jira arguments.
|
|
177
234
|
- All flow state and artifacts are stored in the current project scope by default.
|
|
178
235
|
- gitlab-review and gitlab-diff-review ask for GitLab merge request URL via user-input.
|
|
@@ -393,13 +450,13 @@ async function printAutoPhasesHelp() {
|
|
|
393
450
|
phaseLines.push("", "You can resume auto-golang from a phase with:", "agentweaver auto-golang --from <phase> <jira>", "or in interactive mode:", "/auto-golang --from <phase>");
|
|
394
451
|
printPanel("Auto-Golang Phases", phaseLines.join("\n"), "magenta");
|
|
395
452
|
}
|
|
396
|
-
async function autoCommonPhaseIds() {
|
|
397
|
-
return (await loadDeclarativeFlow({ source: "built-in", fileName
|
|
453
|
+
async function autoCommonPhaseIds(fileName = "auto-common.json") {
|
|
454
|
+
return (await loadDeclarativeFlow({ source: "built-in", fileName })).phases.map((phase) => phase.id);
|
|
398
455
|
}
|
|
399
|
-
async function printAutoCommonPhasesHelp() {
|
|
400
|
-
const phaseLines = [
|
|
401
|
-
phaseLines.push("",
|
|
402
|
-
printPanel("Auto-Common Phases", phaseLines.join("\n"), "magenta");
|
|
456
|
+
async function printAutoCommonPhasesHelp(command = "auto-common", fileName = "auto-common.json") {
|
|
457
|
+
const phaseLines = [`Available ${command} phases:`, "", ...(await autoCommonPhaseIds(fileName))];
|
|
458
|
+
phaseLines.push("", `You can run ${command} with:`, `agentweaver ${command} <jira>`);
|
|
459
|
+
printPanel(command === "auto-common-guided" ? "Auto-Common Guided Phases" : "Auto-Common Phases", phaseLines.join("\n"), "magenta");
|
|
403
460
|
}
|
|
404
461
|
async function autoSimplePhaseIds() {
|
|
405
462
|
return (await loadDeclarativeFlow({ source: "built-in", fileName: "auto-simple.json" })).phases.map((phase) => phase.id);
|
|
@@ -428,6 +485,7 @@ function buildBaseConfig(command, options = {}) {
|
|
|
428
485
|
dryRun: options.dryRun ?? false,
|
|
429
486
|
verbose: options.verbose ?? false,
|
|
430
487
|
...(options.doctorArgs !== undefined ? { doctorArgs: options.doctorArgs } : {}),
|
|
488
|
+
...(options.acceptPlaybookDraft !== undefined ? { acceptPlaybookDraft: options.acceptPlaybookDraft } : {}),
|
|
431
489
|
};
|
|
432
490
|
}
|
|
433
491
|
function commandRequiresTask(command) {
|
|
@@ -437,6 +495,7 @@ function commandRequiresTask(command) {
|
|
|
437
495
|
command === "design-review" ||
|
|
438
496
|
command === "mr-description" ||
|
|
439
497
|
command === "auto-golang" ||
|
|
498
|
+
command === "auto-common-guided" ||
|
|
440
499
|
command === "auto-common" ||
|
|
441
500
|
command === "auto-simple" ||
|
|
442
501
|
command === "auto-status" ||
|
|
@@ -448,6 +507,7 @@ function commandSupportsProjectScope(command) {
|
|
|
448
507
|
command === "gitlab-diff-review" ||
|
|
449
508
|
command === "gitlab-review" ||
|
|
450
509
|
command === "instant-task" ||
|
|
510
|
+
command === "playbook-init" ||
|
|
451
511
|
command === "task-describe" ||
|
|
452
512
|
command === "implement" ||
|
|
453
513
|
command === "review" ||
|
|
@@ -572,6 +632,8 @@ function autoFlowParams(config, forceRefreshSummary = false) {
|
|
|
572
632
|
reviewBlockingSeverities: config.reviewBlockingSeverities,
|
|
573
633
|
forceRefresh: forceRefreshSummary,
|
|
574
634
|
mdLang: config.mdLang,
|
|
635
|
+
acceptPlaybookDraft: config.command === "auto-common-guided" ? config.acceptPlaybookDraft === true : false,
|
|
636
|
+
launchMode: config.command === "auto-common-guided" ? config.autoFromPhase ?? "restart" : undefined,
|
|
575
637
|
runGoTestsScript: path.join(agentweaverHome(PACKAGE_ROOT), "run_go_tests.py"),
|
|
576
638
|
runGoLinterScript: path.join(agentweaverHome(PACKAGE_ROOT), "run_go_linter.py"),
|
|
577
639
|
runGoTestsIteration: nextArtifactIteration(config.taskKey, "run-go-tests-result", "json"),
|
|
@@ -849,6 +911,10 @@ function defaultDeclarativeFlowParams(config, forceRefreshSummary = false, overr
|
|
|
849
911
|
mdLang: config.mdLang,
|
|
850
912
|
llmExecutor: launchProfile.executor,
|
|
851
913
|
llmModel: launchProfile.model,
|
|
914
|
+
projectGuidanceFile: "not provided",
|
|
915
|
+
projectGuidanceJsonFile: "not provided",
|
|
916
|
+
repairProjectGuidanceFile: "not provided",
|
|
917
|
+
repairProjectGuidanceJsonFile: "not provided",
|
|
852
918
|
launchProfile,
|
|
853
919
|
executionRouting,
|
|
854
920
|
iteration,
|
|
@@ -1028,13 +1094,13 @@ async function executeCommand(baseConfig, runFollowupVerify = true, requestUserI
|
|
|
1028
1094
|
: {}, requestUserInput, setSummary, effectiveLaunchMode, runtime);
|
|
1029
1095
|
return false;
|
|
1030
1096
|
}
|
|
1031
|
-
if (config.command === "auto-common") {
|
|
1097
|
+
if (config.command === "auto-common" || config.command === "auto-common-guided") {
|
|
1032
1098
|
requireJiraConfig(config);
|
|
1033
1099
|
await checkAutoPrerequisites(config, launchProfile, executionRouting);
|
|
1034
1100
|
process.env.JIRA_BROWSE_URL = config.jiraBrowseUrl;
|
|
1035
1101
|
process.env.JIRA_API_URL = config.jiraApiUrl;
|
|
1036
1102
|
process.env.JIRA_TASK_FILE = config.jiraTaskFile;
|
|
1037
|
-
await runDeclarativeFlowBySpecFile("auto-common.json", config, autoFlowParams(config, forceRefreshSummary), flowOverrides, requestUserInput, setSummary, launchMode, runtime);
|
|
1103
|
+
await runDeclarativeFlowBySpecFile(config.command === "auto-common-guided" ? "auto-common-guided.json" : "auto-common.json", config, autoFlowParams(config, forceRefreshSummary), flowOverrides, requestUserInput, setSummary, launchMode, runtime);
|
|
1038
1104
|
return false;
|
|
1039
1105
|
}
|
|
1040
1106
|
if (config.command === "auto-simple") {
|
|
@@ -1135,6 +1201,14 @@ async function executeCommand(baseConfig, runFollowupVerify = true, requestUserI
|
|
|
1135
1201
|
}, flowOverrides, requestUserInput, setSummary, launchMode, runtime);
|
|
1136
1202
|
return false;
|
|
1137
1203
|
}
|
|
1204
|
+
if (config.command === "playbook-init") {
|
|
1205
|
+
await runDeclarativeFlowBySpecFile("playbook-init.json", config, {
|
|
1206
|
+
taskKey: config.taskKey,
|
|
1207
|
+
extraPrompt: config.extraPrompt,
|
|
1208
|
+
acceptPlaybookDraft: config.acceptPlaybookDraft === true,
|
|
1209
|
+
}, flowOverrides, requestUserInput, setSummary, launchMode, runtime);
|
|
1210
|
+
return false;
|
|
1211
|
+
}
|
|
1138
1212
|
if (config.command === "bug-analyze") {
|
|
1139
1213
|
requireJiraConfig(config);
|
|
1140
1214
|
if (config.verbose) {
|
|
@@ -1396,20 +1470,20 @@ async function executeCommand(baseConfig, runFollowupVerify = true, requestUserI
|
|
|
1396
1470
|
}
|
|
1397
1471
|
async function parseCliArgs(argv) {
|
|
1398
1472
|
if (argv.includes("--version") || argv.includes("-v")) {
|
|
1399
|
-
|
|
1473
|
+
writeStdoutSync(`${packageVersion()}\n`);
|
|
1400
1474
|
process.exit(0);
|
|
1401
1475
|
}
|
|
1402
1476
|
if (argv.includes("--help") || argv.includes("-h")) {
|
|
1403
|
-
|
|
1477
|
+
writeStdoutSync(`${usage()}\n`);
|
|
1404
1478
|
process.exit(0);
|
|
1405
1479
|
}
|
|
1406
1480
|
if (argv.length === 0) {
|
|
1407
|
-
|
|
1481
|
+
writeStderrSync(`${usage()}\n`);
|
|
1408
1482
|
process.exit(1);
|
|
1409
1483
|
}
|
|
1410
1484
|
const command = argv[0];
|
|
1411
1485
|
if (!COMMANDS.includes(command)) {
|
|
1412
|
-
|
|
1486
|
+
writeStderrSync(`${usage()}\n`);
|
|
1413
1487
|
process.exit(1);
|
|
1414
1488
|
}
|
|
1415
1489
|
let dry = false;
|
|
@@ -1422,6 +1496,9 @@ async function parseCliArgs(argv) {
|
|
|
1422
1496
|
let jiraRef;
|
|
1423
1497
|
let mdLang;
|
|
1424
1498
|
let launchMode;
|
|
1499
|
+
let acceptPlaybookDraft = false;
|
|
1500
|
+
let webNoOpen = process.env.AGENTWEAVER_WEB_NO_OPEN === "1";
|
|
1501
|
+
let webHost;
|
|
1425
1502
|
const doctorArgs = [];
|
|
1426
1503
|
for (let index = 1; index < argv.length; index += 1) {
|
|
1427
1504
|
const token = argv[index] ?? "";
|
|
@@ -1437,9 +1514,56 @@ async function parseCliArgs(argv) {
|
|
|
1437
1514
|
helpPhases = true;
|
|
1438
1515
|
continue;
|
|
1439
1516
|
}
|
|
1517
|
+
if (token === "--accept-playbook-draft") {
|
|
1518
|
+
acceptPlaybookDraft = true;
|
|
1519
|
+
continue;
|
|
1520
|
+
}
|
|
1521
|
+
if (token === "--no-open") {
|
|
1522
|
+
if (command !== "web") {
|
|
1523
|
+
writeStderrSync("Error: --no-open is only supported after the web command.\n");
|
|
1524
|
+
process.exit(1);
|
|
1525
|
+
}
|
|
1526
|
+
webNoOpen = true;
|
|
1527
|
+
continue;
|
|
1528
|
+
}
|
|
1529
|
+
if (token === "--listen-all") {
|
|
1530
|
+
if (command !== "web") {
|
|
1531
|
+
writeStderrSync("Error: --listen-all is only supported after the web command.\n");
|
|
1532
|
+
process.exit(1);
|
|
1533
|
+
}
|
|
1534
|
+
webHost = "0.0.0.0";
|
|
1535
|
+
continue;
|
|
1536
|
+
}
|
|
1537
|
+
if (token === "--host") {
|
|
1538
|
+
if (command !== "web") {
|
|
1539
|
+
writeStderrSync("Error: --host is only supported after the web command.\n");
|
|
1540
|
+
process.exit(1);
|
|
1541
|
+
}
|
|
1542
|
+
const hostValue = argv[index + 1]?.trim();
|
|
1543
|
+
if (!hostValue || hostValue.startsWith("-")) {
|
|
1544
|
+
writeStderrSync("Error: --host requires a host value.\n");
|
|
1545
|
+
process.exit(1);
|
|
1546
|
+
}
|
|
1547
|
+
webHost = hostValue;
|
|
1548
|
+
index += 1;
|
|
1549
|
+
continue;
|
|
1550
|
+
}
|
|
1551
|
+
if (token.startsWith("--host=")) {
|
|
1552
|
+
if (command !== "web") {
|
|
1553
|
+
writeStderrSync("Error: --host is only supported after the web command.\n");
|
|
1554
|
+
process.exit(1);
|
|
1555
|
+
}
|
|
1556
|
+
const hostValue = token.slice("--host=".length).trim();
|
|
1557
|
+
if (!hostValue) {
|
|
1558
|
+
writeStderrSync("Error: --host requires a host value.\n");
|
|
1559
|
+
process.exit(1);
|
|
1560
|
+
}
|
|
1561
|
+
webHost = hostValue;
|
|
1562
|
+
continue;
|
|
1563
|
+
}
|
|
1440
1564
|
if (token === "--resume" || token === "--continue" || token === "--restart") {
|
|
1441
1565
|
if (launchMode) {
|
|
1442
|
-
|
|
1566
|
+
writeStderrSync("Error: --resume, --continue, and --restart are mutually exclusive.\n");
|
|
1443
1567
|
process.exit(1);
|
|
1444
1568
|
}
|
|
1445
1569
|
launchMode = token.slice(2);
|
|
@@ -1475,7 +1599,7 @@ async function parseCliArgs(argv) {
|
|
|
1475
1599
|
mdLang = langValue;
|
|
1476
1600
|
}
|
|
1477
1601
|
else {
|
|
1478
|
-
|
|
1602
|
+
writeStderrSync("Error: --md-lang accepts only 'en' or 'ru' as values.\n");
|
|
1479
1603
|
process.exit(1);
|
|
1480
1604
|
}
|
|
1481
1605
|
index += 1;
|
|
@@ -1487,7 +1611,7 @@ async function parseCliArgs(argv) {
|
|
|
1487
1611
|
mdLang = langValue;
|
|
1488
1612
|
}
|
|
1489
1613
|
else {
|
|
1490
|
-
|
|
1614
|
+
writeStderrSync("Error: --md-lang accepts only 'en' or 'ru' as values.\n");
|
|
1491
1615
|
process.exit(1);
|
|
1492
1616
|
}
|
|
1493
1617
|
continue;
|
|
@@ -1503,8 +1627,8 @@ async function parseCliArgs(argv) {
|
|
|
1503
1627
|
await printAutoPhasesHelp();
|
|
1504
1628
|
process.exit(0);
|
|
1505
1629
|
}
|
|
1506
|
-
if (command === "auto-common" && helpPhases) {
|
|
1507
|
-
await printAutoCommonPhasesHelp();
|
|
1630
|
+
if ((command === "auto-common" || command === "auto-common-guided") && helpPhases) {
|
|
1631
|
+
await printAutoCommonPhasesHelp(command, command === "auto-common-guided" ? "auto-common-guided.json" : "auto-common.json");
|
|
1508
1632
|
process.exit(0);
|
|
1509
1633
|
}
|
|
1510
1634
|
if (command === "auto-simple" && helpPhases) {
|
|
@@ -1524,6 +1648,9 @@ async function parseCliArgs(argv) {
|
|
|
1524
1648
|
...(mdLang !== undefined ? { mdLang } : {}),
|
|
1525
1649
|
...(doctorArgs.length > 0 ? { doctorArgs } : {}),
|
|
1526
1650
|
...(launchMode !== undefined ? { launchMode } : {}),
|
|
1651
|
+
...(acceptPlaybookDraft ? { acceptPlaybookDraft } : {}),
|
|
1652
|
+
...(command === "web" ? { webNoOpen } : {}),
|
|
1653
|
+
...(command === "web" && webHost !== undefined ? { webHost } : {}),
|
|
1527
1654
|
};
|
|
1528
1655
|
}
|
|
1529
1656
|
function buildConfigFromArgs(args) {
|
|
@@ -1537,24 +1664,66 @@ function buildConfigFromArgs(args) {
|
|
|
1537
1664
|
dryRun: args.dry,
|
|
1538
1665
|
verbose: args.verbose,
|
|
1539
1666
|
...(args.doctorArgs !== undefined ? { doctorArgs: args.doctorArgs } : {}),
|
|
1667
|
+
...(args.acceptPlaybookDraft !== undefined ? { acceptPlaybookDraft: args.acceptPlaybookDraft } : {}),
|
|
1540
1668
|
});
|
|
1541
1669
|
}
|
|
1542
|
-
async function
|
|
1670
|
+
async function runInteractiveWithSessionFactory(createSession, jiraRef, forceRefresh = false, scopeName, installSignalCleanup = false) {
|
|
1543
1671
|
let currentScope = resolveProjectScope(scopeName, jiraRef);
|
|
1544
|
-
const gitBranchName = detectGitBranchName();
|
|
1545
1672
|
const flowCatalog = await loadInteractiveFlowCatalog(process.cwd());
|
|
1546
1673
|
let activeAbortController = null;
|
|
1547
1674
|
let activeFlowId = null;
|
|
1675
|
+
let pendingScopeSwitch = null;
|
|
1676
|
+
const autoScopeSwitchEnabled = !scopeName?.trim() && !jiraRef?.trim();
|
|
1677
|
+
let lastObservedGitScope = currentScope;
|
|
1678
|
+
let ui;
|
|
1548
1679
|
let exiting = false;
|
|
1549
|
-
const
|
|
1680
|
+
const applyScopeSwitch = (nextScope, reason) => {
|
|
1681
|
+
const previousScope = currentScope;
|
|
1682
|
+
currentScope = nextScope;
|
|
1683
|
+
ui.setScope(currentScope.scopeKey, currentScope.jiraIssueKey ?? null, currentScope.gitBranchName);
|
|
1684
|
+
syncInteractiveTaskSummary(ui, currentScope, forceRefresh);
|
|
1685
|
+
ui.appendLog(`[scope] ${reason}: ${previousScope.scopeKey} -> ${currentScope.scopeKey}`);
|
|
1686
|
+
};
|
|
1687
|
+
const handleObservedScope = (observedScope, reason) => {
|
|
1688
|
+
if (observedScope.scopeKey === currentScope.scopeKey
|
|
1689
|
+
&& observedScope.gitBranchName === currentScope.gitBranchName) {
|
|
1690
|
+
pendingScopeSwitch = null;
|
|
1691
|
+
return;
|
|
1692
|
+
}
|
|
1693
|
+
if (activeAbortController) {
|
|
1694
|
+
if (!pendingScopeSwitch
|
|
1695
|
+
|| pendingScopeSwitch.scopeKey !== observedScope.scopeKey
|
|
1696
|
+
|| pendingScopeSwitch.gitBranchName !== observedScope.gitBranchName) {
|
|
1697
|
+
pendingScopeSwitch = observedScope;
|
|
1698
|
+
ui.appendLog(`[scope] ${reason}: switch to ${observedScope.scopeKey} pending until current flow finishes`);
|
|
1699
|
+
}
|
|
1700
|
+
return;
|
|
1701
|
+
}
|
|
1702
|
+
pendingScopeSwitch = null;
|
|
1703
|
+
applyScopeSwitch(observedScope, reason);
|
|
1704
|
+
};
|
|
1705
|
+
const refreshScopeFromGit = (reason) => {
|
|
1706
|
+
if (!autoScopeSwitchEnabled) {
|
|
1707
|
+
return;
|
|
1708
|
+
}
|
|
1709
|
+
const observedScope = resolveProjectScope(null, null);
|
|
1710
|
+
if (observedScope.scopeKey === lastObservedGitScope.scopeKey
|
|
1711
|
+
&& observedScope.gitBranchName === lastObservedGitScope.gitBranchName) {
|
|
1712
|
+
return;
|
|
1713
|
+
}
|
|
1714
|
+
lastObservedGitScope = observedScope;
|
|
1715
|
+
handleObservedScope(observedScope, reason);
|
|
1716
|
+
};
|
|
1717
|
+
ui = createSession({
|
|
1550
1718
|
scopeKey: currentScope.scopeKey,
|
|
1551
1719
|
jiraIssueKey: currentScope.jiraIssueKey ?? null,
|
|
1552
1720
|
summaryText: "",
|
|
1553
1721
|
cwd: process.cwd(),
|
|
1554
|
-
gitBranchName,
|
|
1722
|
+
gitBranchName: currentScope.gitBranchName,
|
|
1555
1723
|
version: packageVersion(),
|
|
1556
1724
|
flows: interactiveFlowDefinitions(flowCatalog),
|
|
1557
1725
|
getRunConfirmation: async (flowId) => {
|
|
1726
|
+
refreshScopeFromGit("git scope refresh before launch confirmation");
|
|
1558
1727
|
const flowEntry = findCatalogEntry(flowId, flowCatalog);
|
|
1559
1728
|
if (!flowEntry) {
|
|
1560
1729
|
throw new TaskRunnerError(`Unknown flow: ${flowId}`);
|
|
@@ -1563,6 +1732,7 @@ async function runInteractive(jiraRef, forceRefresh = false, scopeName) {
|
|
|
1563
1732
|
return resumeLookup;
|
|
1564
1733
|
},
|
|
1565
1734
|
onRun: async (flowId, launchMode) => {
|
|
1735
|
+
refreshScopeFromGit("git scope refresh before flow launch");
|
|
1566
1736
|
const abortController = new AbortController();
|
|
1567
1737
|
activeAbortController = abortController;
|
|
1568
1738
|
activeFlowId = flowId;
|
|
@@ -1598,7 +1768,7 @@ async function runInteractive(jiraRef, forceRefresh = false, scopeName) {
|
|
|
1598
1768
|
const jiraContext = await requestJiraContext((form) => ui.requestUserInput(form));
|
|
1599
1769
|
currentScope = resolveProjectScope(null, jiraContext.jiraRef);
|
|
1600
1770
|
}
|
|
1601
|
-
ui.setScope(currentScope.scopeKey, currentScope.jiraIssueKey ?? null);
|
|
1771
|
+
ui.setScope(currentScope.scopeKey, currentScope.jiraIssueKey ?? null, currentScope.gitBranchName);
|
|
1602
1772
|
if (previousScopeKey !== currentScope.scopeKey || currentScope.jiraIssueKey) {
|
|
1603
1773
|
syncInteractiveTaskSummary(ui, currentScope, forceRefresh);
|
|
1604
1774
|
}
|
|
@@ -1640,6 +1810,11 @@ async function runInteractive(jiraRef, forceRefresh = false, scopeName) {
|
|
|
1640
1810
|
if (activeAbortController === abortController) {
|
|
1641
1811
|
activeAbortController = null;
|
|
1642
1812
|
activeFlowId = null;
|
|
1813
|
+
if (pendingScopeSwitch && !exiting) {
|
|
1814
|
+
const nextScope = pendingScopeSwitch;
|
|
1815
|
+
pendingScopeSwitch = null;
|
|
1816
|
+
applyScopeSwitch(nextScope, "git scope refresh after flow completion");
|
|
1817
|
+
}
|
|
1643
1818
|
}
|
|
1644
1819
|
}
|
|
1645
1820
|
},
|
|
@@ -1651,6 +1826,10 @@ async function runInteractive(jiraRef, forceRefresh = false, scopeName) {
|
|
|
1651
1826
|
activeAbortController.abort();
|
|
1652
1827
|
},
|
|
1653
1828
|
onExit: () => {
|
|
1829
|
+
if (activeAbortController) {
|
|
1830
|
+
ui.interruptActiveForm();
|
|
1831
|
+
activeAbortController.abort();
|
|
1832
|
+
}
|
|
1654
1833
|
exiting = true;
|
|
1655
1834
|
},
|
|
1656
1835
|
});
|
|
@@ -1661,13 +1840,45 @@ async function runInteractive(jiraRef, forceRefresh = false, scopeName) {
|
|
|
1661
1840
|
ui.appendLog("[scope] project scope active; task summary will appear after a Jira-backed flow runs");
|
|
1662
1841
|
}
|
|
1663
1842
|
syncInteractiveTaskSummary(ui, currentScope, forceRefresh);
|
|
1843
|
+
const scopeWatchInterval = autoScopeSwitchEnabled
|
|
1844
|
+
? setInterval(() => {
|
|
1845
|
+
if (!exiting) {
|
|
1846
|
+
refreshScopeFromGit("git branch changed");
|
|
1847
|
+
}
|
|
1848
|
+
}, INTERACTIVE_SCOPE_WATCH_INTERVAL_MS)
|
|
1849
|
+
: null;
|
|
1664
1850
|
return await new Promise((resolve, reject) => {
|
|
1851
|
+
let cleanupStarted = false;
|
|
1852
|
+
const requestExit = () => {
|
|
1853
|
+
if (activeAbortController) {
|
|
1854
|
+
ui.interruptActiveForm();
|
|
1855
|
+
activeAbortController.abort();
|
|
1856
|
+
}
|
|
1857
|
+
exiting = true;
|
|
1858
|
+
};
|
|
1859
|
+
const onSigint = () => requestExit();
|
|
1860
|
+
const onSigterm = () => requestExit();
|
|
1861
|
+
if (installSignalCleanup) {
|
|
1862
|
+
process.once("SIGINT", onSigint);
|
|
1863
|
+
process.once("SIGTERM", onSigterm);
|
|
1864
|
+
}
|
|
1665
1865
|
const interval = setInterval(() => {
|
|
1666
1866
|
if (!exiting) {
|
|
1667
1867
|
return;
|
|
1668
1868
|
}
|
|
1669
1869
|
clearInterval(interval);
|
|
1670
1870
|
try {
|
|
1871
|
+
if (cleanupStarted) {
|
|
1872
|
+
return;
|
|
1873
|
+
}
|
|
1874
|
+
cleanupStarted = true;
|
|
1875
|
+
if (scopeWatchInterval) {
|
|
1876
|
+
clearInterval(scopeWatchInterval);
|
|
1877
|
+
}
|
|
1878
|
+
if (installSignalCleanup) {
|
|
1879
|
+
process.off("SIGINT", onSigint);
|
|
1880
|
+
process.off("SIGTERM", onSigterm);
|
|
1881
|
+
}
|
|
1671
1882
|
ui.destroy();
|
|
1672
1883
|
bye();
|
|
1673
1884
|
resolve(0);
|
|
@@ -1678,6 +1889,12 @@ async function runInteractive(jiraRef, forceRefresh = false, scopeName) {
|
|
|
1678
1889
|
}, 100);
|
|
1679
1890
|
});
|
|
1680
1891
|
}
|
|
1892
|
+
async function runInteractive(jiraRef, forceRefresh = false, scopeName) {
|
|
1893
|
+
return await runInteractiveWithSessionFactory(createInteractiveSession, jiraRef, forceRefresh, scopeName);
|
|
1894
|
+
}
|
|
1895
|
+
async function runWebInteractive(jiraRef, forceRefresh = false, noOpen = false, host, auth) {
|
|
1896
|
+
return await runInteractiveWithSessionFactory((options) => createWebInteractiveSession(options, { noOpen, ...(host ? { host } : {}), ...(auth ? { auth } : {}), printInfo }), jiraRef, forceRefresh, null, true);
|
|
1897
|
+
}
|
|
1681
1898
|
export async function main(argv = process.argv.slice(2)) {
|
|
1682
1899
|
loadTieredEnv(process.cwd());
|
|
1683
1900
|
let forceRefresh = false;
|
|
@@ -1687,6 +1904,9 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
1687
1904
|
args.shift();
|
|
1688
1905
|
}
|
|
1689
1906
|
try {
|
|
1907
|
+
if (args[0] === "--no-open") {
|
|
1908
|
+
throw new TaskRunnerError("--no-open is only supported after the web command.");
|
|
1909
|
+
}
|
|
1690
1910
|
if (args.length === 0) {
|
|
1691
1911
|
return await runInteractive(undefined, forceRefresh);
|
|
1692
1912
|
}
|
|
@@ -1694,6 +1914,11 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
1694
1914
|
return await runInteractive(args[0] ?? "", forceRefresh);
|
|
1695
1915
|
}
|
|
1696
1916
|
const parsedArgs = await parseCliArgs(args);
|
|
1917
|
+
if (parsedArgs.command === "web") {
|
|
1918
|
+
const webAuth = resolveWebAuthConfig();
|
|
1919
|
+
requireWebAuthForHost(parsedArgs.webHost, webAuth);
|
|
1920
|
+
return await runWebInteractive(parsedArgs.jiraRef, forceRefresh, parsedArgs.webNoOpen === true, parsedArgs.webHost, webAuth);
|
|
1921
|
+
}
|
|
1697
1922
|
const commandCompleted = await executeCommand(buildConfigFromArgs(parsedArgs), true, requestUserInputInTerminal, undefined, undefined, false, parsedArgs.launchMode);
|
|
1698
1923
|
if (parsedArgs.command === "doctor") {
|
|
1699
1924
|
return commandCompleted ? 0 : 1;
|
|
@@ -1702,12 +1927,12 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
1702
1927
|
}
|
|
1703
1928
|
catch (error) {
|
|
1704
1929
|
if (error instanceof TaskRunnerError) {
|
|
1705
|
-
|
|
1930
|
+
writeStderrSync(`Error: ${error.message}\n`);
|
|
1706
1931
|
return 1;
|
|
1707
1932
|
}
|
|
1708
1933
|
const returnCode = Number(error.returnCode);
|
|
1709
1934
|
if (!Number.isNaN(returnCode)) {
|
|
1710
|
-
|
|
1935
|
+
writeStderrSync(`Error: ${formatProcessFailure(error)}\n`);
|
|
1711
1936
|
return returnCode || 1;
|
|
1712
1937
|
}
|
|
1713
1938
|
throw error;
|