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.
Files changed (47) hide show
  1. package/README.md +104 -23
  2. package/dist/artifacts.js +41 -0
  3. package/dist/index.js +252 -27
  4. package/dist/interactive/controller.js +249 -13
  5. package/dist/interactive/ink/index.js +2 -2
  6. package/dist/interactive/state.js +1 -0
  7. package/dist/interactive/web/index.js +179 -0
  8. package/dist/interactive/web/protocol.js +154 -0
  9. package/dist/interactive/web/server.js +575 -0
  10. package/dist/interactive/web/static/app.js +709 -0
  11. package/dist/interactive/web/static/index.html +77 -0
  12. package/dist/interactive/web/static/styles.css +2 -0
  13. package/dist/interactive/web/static/styles.input.css +469 -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/playbook.js +485 -0
  40. package/dist/runtime/project-guidance.js +339 -0
  41. package/dist/structured-artifact-schema-registry.js +8 -0
  42. package/dist/structured-artifact-schemas.json +235 -0
  43. package/dist/structured-artifacts.js +7 -1
  44. package/docs/declarative-workflows.md +565 -0
  45. package/docs/features.md +77 -0
  46. package/docs/playbook.md +327 -0
  47. 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 { detectGitBranchName, requestJiraContext, resolveProjectScope, } from "./scope.js";
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 output files: en (English) or ru (Russian, default)
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: "auto-common.json" })).phases.map((phase) => phase.id);
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 = ["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");
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
- process.stdout.write(`${packageVersion()}\n`);
1473
+ writeStdoutSync(`${packageVersion()}\n`);
1400
1474
  process.exit(0);
1401
1475
  }
1402
1476
  if (argv.includes("--help") || argv.includes("-h")) {
1403
- process.stdout.write(`${usage()}\n`);
1477
+ writeStdoutSync(`${usage()}\n`);
1404
1478
  process.exit(0);
1405
1479
  }
1406
1480
  if (argv.length === 0) {
1407
- process.stderr.write(`${usage()}\n`);
1481
+ writeStderrSync(`${usage()}\n`);
1408
1482
  process.exit(1);
1409
1483
  }
1410
1484
  const command = argv[0];
1411
1485
  if (!COMMANDS.includes(command)) {
1412
- process.stderr.write(`${usage()}\n`);
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
- process.stderr.write("Error: --resume, --continue, and --restart are mutually exclusive.\n");
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
- process.stderr.write("Error: --md-lang accepts only 'en' or 'ru' as values.\n");
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
- process.stderr.write("Error: --md-lang accepts only 'en' or 'ru' as values.\n");
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 runInteractive(jiraRef, forceRefresh = false, scopeName) {
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 ui = createInteractiveSession({
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
- printError(error.message);
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
- printError(formatProcessFailure(error));
1935
+ writeStderrSync(`Error: ${formatProcessFailure(error)}\n`);
1711
1936
  return returnCode || 1;
1712
1937
  }
1713
1938
  throw error;