agentweaver 0.1.17 → 0.1.19

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