agentweaver 0.1.16 → 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 (74) hide show
  1. package/README.md +148 -27
  2. package/dist/artifacts.js +114 -3
  3. package/dist/doctor/checks/executors.js +2 -2
  4. package/dist/flow-state.js +138 -1
  5. package/dist/index.js +421 -82
  6. package/dist/interactive/controller.js +305 -36
  7. package/dist/interactive/ink/index.js +24 -3
  8. package/dist/interactive/state.js +1 -0
  9. package/dist/interactive/tree.js +2 -2
  10. package/dist/interactive/web/index.js +179 -0
  11. package/dist/interactive/web/protocol.js +154 -0
  12. package/dist/interactive/web/server.js +575 -0
  13. package/dist/interactive/web/static/app.js +709 -0
  14. package/dist/interactive/web/static/index.html +77 -0
  15. package/dist/interactive/web/static/styles.css +2 -0
  16. package/dist/interactive/web/static/styles.input.css +469 -0
  17. package/dist/pipeline/auto-flow.js +9 -6
  18. package/dist/pipeline/context.js +6 -5
  19. package/dist/pipeline/declarative-flows.js +39 -20
  20. package/dist/pipeline/flow-catalog.js +40 -14
  21. package/dist/pipeline/flow-specs/auto-common-guided.json +313 -0
  22. package/dist/pipeline/flow-specs/auto-common.json +4 -1
  23. package/dist/pipeline/flow-specs/auto-golang.json +27 -1
  24. package/dist/pipeline/flow-specs/design-review/design-review-loop.json +15 -1
  25. package/dist/pipeline/flow-specs/design-review.json +2 -0
  26. package/dist/pipeline/flow-specs/implement.json +3 -1
  27. package/dist/pipeline/flow-specs/plan.json +8 -2
  28. package/dist/pipeline/flow-specs/playbook-init.json +199 -0
  29. package/dist/pipeline/flow-specs/review/review-fix.json +3 -1
  30. package/dist/pipeline/flow-specs/review/review-loop.json +4 -0
  31. package/dist/pipeline/flow-specs/review/review.json +2 -0
  32. package/dist/pipeline/launch-profile-config.js +30 -18
  33. package/dist/pipeline/node-contract.js +1 -0
  34. package/dist/pipeline/node-registry.js +119 -5
  35. package/dist/pipeline/nodes/flow-run-node.js +200 -173
  36. package/dist/pipeline/nodes/llm-prompt-node.js +15 -33
  37. package/dist/pipeline/nodes/playbook-ensure-node.js +115 -0
  38. package/dist/pipeline/nodes/playbook-inventory-node.js +51 -0
  39. package/dist/pipeline/nodes/playbook-questions-form-node.js +166 -0
  40. package/dist/pipeline/nodes/playbook-write-node.js +243 -0
  41. package/dist/pipeline/nodes/project-guidance-node.js +69 -0
  42. package/dist/pipeline/plugin-loader.js +389 -0
  43. package/dist/pipeline/plugin-types.js +1 -0
  44. package/dist/pipeline/prompt-registry.js +4 -1
  45. package/dist/pipeline/prompt-runtime.js +6 -2
  46. package/dist/pipeline/registry.js +71 -4
  47. package/dist/pipeline/spec-compiler.js +1 -0
  48. package/dist/pipeline/spec-loader.js +14 -0
  49. package/dist/pipeline/spec-types.js +19 -0
  50. package/dist/pipeline/spec-validator.js +6 -0
  51. package/dist/pipeline/value-resolver.js +41 -2
  52. package/dist/playbook/practice-candidates.js +12 -0
  53. package/dist/playbook/repo-inventory.js +208 -0
  54. package/dist/plugin-sdk.js +1 -0
  55. package/dist/prompts.js +31 -0
  56. package/dist/runtime/artifact-registry.js +3 -0
  57. package/dist/runtime/execution-routing.js +25 -19
  58. package/dist/runtime/interactive-execution-routing.js +66 -57
  59. package/dist/runtime/playbook.js +485 -0
  60. package/dist/runtime/project-guidance.js +339 -0
  61. package/dist/structured-artifact-schema-registry.js +8 -0
  62. package/dist/structured-artifact-schemas.json +235 -0
  63. package/dist/structured-artifacts.js +7 -1
  64. package/docs/declarative-workflows.md +565 -0
  65. package/docs/example/.flows/examples/claude-example.json +50 -0
  66. package/docs/example/.plugins/claude-example-plugin/index.js +149 -0
  67. package/docs/example/.plugins/claude-example-plugin/plugin.json +8 -0
  68. package/docs/examples/.flows/claude-example.json +50 -0
  69. package/docs/examples/.plugins/claude-example-plugin/index.js +149 -0
  70. package/docs/examples/.plugins/claude-example-plugin/plugin.json +8 -0
  71. package/docs/features.md +77 -0
  72. package/docs/playbook.md +327 -0
  73. package/docs/plugin-sdk.md +731 -0
  74. package/package.json +13 -4
package/dist/index.js CHANGED
@@ -1,11 +1,11 @@
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";
6
- import { bugAnalyzeArtifacts, bugAnalyzeJsonFile, bugFixDesignJsonFile, bugFixPlanJsonFile, designReviewFile, designReviewJsonFile, gitlabDiffFile, gitlabDiffJsonFile, ensureScopeWorkspaceDir, gitlabReviewFile, gitlabReviewJsonFile, instantTaskInputJsonFile, latestArtifactIteration, nextArtifactIteration, readyToMergeFile, requireArtifacts, reviewAssessmentFile, reviewAssessmentJsonFile, reviewFile, reviewFixSelectionJsonFile, reviewJsonFile, scopeWorkspaceDir, flowStateFile, taskSummaryFile, } from "./artifacts.js";
6
+ import { archiveActiveAttempt, bugAnalyzeArtifacts, bugAnalyzeJsonFile, bugFixDesignJsonFile, bugFixPlanJsonFile, designReviewFile, designReviewJsonFile, gitlabDiffFile, gitlabDiffJsonFile, ensureScopeWorkspaceDir, gitlabReviewFile, gitlabReviewJsonFile, instantTaskInputJsonFile, latestArtifactIteration, nextArtifactIteration, readyToMergeFile, requireArtifacts, reviewAssessmentFile, reviewAssessmentJsonFile, reviewFile, reviewFixSelectionJsonFile, reviewJsonFile, scopeWorkspaceDir, flowStateFile, taskSummaryFile, } from "./artifacts.js";
7
7
  import { FlowInterruptedError, TaskRunnerError } from "./errors.js";
8
- import { createFlowRunState, hasResumableFlowState, loadFlowRunState, prepareFlowStateForResume, resetFlowRunState, rewindFlowRunStateToPhase, saveFlowRunState, stripExecutionStatePayload, } from "./flow-state.js";
8
+ import { createFlowRunState, classifyFlowLaunchAvailability, loadFlowRunState, prepareFlowStateForContinue, prepareFlowStateForResume, resetFlowRunState, rewindFlowRunStateToPhase, saveFlowRunState, stripExecutionStatePayload, } from "./flow-state.js";
9
9
  import { requireJiraTaskFile } from "./jira.js";
10
10
  import { validateStructuredArtifacts } from "./structured-artifacts.js";
11
11
  import { AGENTWEAVER_REVIEW_BLOCKING_SEVERITIES_ENV, parseReviewSeverityCsv, resolveReviewBlockingSeveritiesFromEnv, } from "./review-severity.js";
@@ -15,6 +15,7 @@ import { createPipelineContext } from "./pipeline/context.js";
15
15
  import { collectFlowRoutingGroups, loadDeclarativeFlow } from "./pipeline/declarative-flows.js";
16
16
  import { runExpandedPhase } from "./pipeline/declarative-flow-runner.js";
17
17
  import { builtInCommandFlowFile, findCatalogEntry, flowRoutingGroups, isBuiltInCommandFlowId, loadInteractiveFlowCatalog, toDeclarativeFlowRef, } from "./pipeline/flow-catalog.js";
18
+ import { createPipelineRegistryContext } from "./pipeline/plugin-loader.js";
18
19
  import { DEFAULT_LAUNCH_PROFILE, } from "./pipeline/launch-profile-config.js";
19
20
  import { withCanonicalReviewLoopParams } from "./pipeline/review-iteration.js";
20
21
  import { evaluateCondition, resolveValue } from "./pipeline/value-resolver.js";
@@ -31,12 +32,14 @@ import { clearReadyToMergeFile } from "./runtime/ready-to-merge.js";
31
32
  import { describeExecutionRouting, executorsForRoutingGroups, resolveExecutionRouting, } from "./runtime/execution-routing.js";
32
33
  import { requestInteractiveExecutionRouting } from "./runtime/interactive-execution-routing.js";
33
34
  import { createInteractiveSession } from "./interactive/create-interactive-session.js";
35
+ import { createWebInteractiveSession } from "./interactive/web/index.js";
34
36
  import { bye, printError, printInfo, printPanel, printSummary, setFlowExecutionState, stripAnsi, } from "./tui.js";
35
37
  import { requestUserInputInTerminal } from "./user-input.js";
36
38
  import { runDoctorCommand } from "./doctor/index.js";
37
- import { detectGitBranchName, requestJiraContext, resolveProjectScope, } from "./scope.js";
39
+ import { requestJiraContext, resolveProjectScope, } from "./scope.js";
38
40
  const COMMANDS = [
39
41
  "auto-golang",
42
+ "auto-common-guided",
40
43
  "auto-common",
41
44
  "auto-simple",
42
45
  "auto-status",
@@ -52,7 +55,9 @@ const COMMANDS = [
52
55
  "mr-description",
53
56
  "plan",
54
57
  "plan-revise",
58
+ "playbook-init",
55
59
  "task-describe",
60
+ "web",
56
61
  "implement",
57
62
  "review",
58
63
  "review-fix",
@@ -60,8 +65,17 @@ const COMMANDS = [
60
65
  "run-go-tests-loop",
61
66
  "run-go-linter-loop",
62
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";
63
71
  const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
64
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
+ }
65
79
  function createRuntimeServices(signal) {
66
80
  return {
67
81
  resolveCmd,
@@ -70,6 +84,37 @@ function createRuntimeServices(signal) {
70
84
  };
71
85
  }
72
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
+ }
73
118
  function buildFailureOutputPreview(output) {
74
119
  const normalized = stripAnsi(output).replace(/\r\n/g, "\n").trim();
75
120
  if (!normalized) {
@@ -109,6 +154,7 @@ function usage() {
109
154
  agentweaver
110
155
  agentweaver <jira-browse-url|jira-issue-key>
111
156
  agentweaver --force <jira-browse-url|jira-issue-key>
157
+ agentweaver web [--no-open] [--host <host>|--listen-all] [<jira-browse-url|jira-issue-key>]
112
158
  agentweaver git-commit [--dry] [--verbose] [--prompt <text>] [--scope <name>] [<jira-browse-url|jira-issue-key>]
113
159
  agentweaver gitlab-diff-review [--dry] [--verbose] [--prompt <text>] [--scope <name>]
114
160
  agentweaver gitlab-review [--dry] [--verbose] [--prompt <text>] [--scope <name>]
@@ -120,6 +166,7 @@ function usage() {
120
166
  agentweaver mr-description [--dry] [--verbose] [--prompt <text>] <jira-browse-url|jira-issue-key>
121
167
  agentweaver plan [--dry] [--verbose] [--prompt <text>] [--md-lang <en|ru>] [<jira-browse-url|jira-issue-key>]
122
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>]
123
170
  agentweaver task-describe [--dry] [--verbose] [--prompt <text>] [<jira-browse-url|jira-issue-key>]
124
171
  agentweaver implement [--dry] [--verbose] [--prompt <text>] [--scope <name>] [<jira-browse-url|jira-issue-key>]
125
172
  agentweaver review [--dry] [--verbose] [--prompt <text>] [--scope <name>] [--blocking-severities <list>] [<jira-browse-url|jira-issue-key>]
@@ -130,6 +177,7 @@ function usage() {
130
177
  agentweaver auto-golang [--dry] [--verbose] [--prompt <text>] [<jira-browse-url|jira-issue-key>]
131
178
  agentweaver auto-golang [--dry] [--verbose] [--prompt <text>] --from <phase> [<jira-browse-url|jira-issue-key>]
132
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>
133
181
  agentweaver auto-common [--dry] [--verbose] [--prompt <text>] [--md-lang <en|ru>] <jira-browse-url|jira-issue-key>
134
182
  agentweaver auto-common --help-phases
135
183
  agentweaver auto-simple [--dry] [--verbose] [--prompt <text>] [--md-lang <en|ru>] <jira-browse-url|jira-issue-key>
@@ -145,12 +193,19 @@ Interactive Mode:
145
193
  Flags:
146
194
  --version Show package version
147
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
148
199
  --dry Fetch Jira task, but print codex/opencode commands instead of executing them
149
200
  --verbose Show live stdout/stderr of launched commands
150
201
  --scope Explicit workflow scope name for non-Jira runs except instant-task
151
202
  --prompt Extra prompt text appended to the base prompt
203
+ --resume Resume an interrupted run when valid
204
+ --continue Continue a terminated iterative run when valid
205
+ --restart Archive the active attempt and start a fresh run
152
206
  --blocking-severities Comma-separated severities that block merge and drive review-fix auto-selection
153
- --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
154
209
 
155
210
  Required environment variables:
156
211
  JIRA_API_KEY Jira API token used for Jira-backed flows (Bearer by default, or Basic with Jira Cloud)
@@ -166,9 +221,15 @@ Optional environment variables:
166
221
  CODEX_MODEL
167
222
  OPENCODE_BIN
168
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
169
227
 
170
228
  Notes:
171
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.
172
233
  - instant-task always uses the current branch-derived project scope and rejects explicit scope overrides or Jira arguments.
173
234
  - All flow state and artifacts are stored in the current project scope by default.
174
235
  - gitlab-review and gitlab-diff-review ask for GitLab merge request URL via user-input.
@@ -186,12 +247,12 @@ function packageVersion() {
186
247
  function normalizeAutoPhaseId(phaseId) {
187
248
  return phaseId.trim().toLowerCase().replaceAll("-", "_");
188
249
  }
189
- function autoPhaseIds() {
190
- return loadDeclarativeFlow({ source: "built-in", fileName: "auto-golang.json" }).phases.map((phase) => phase.id);
250
+ async function autoPhaseIds() {
251
+ return (await loadDeclarativeFlow({ source: "built-in", fileName: "auto-golang.json" })).phases.map((phase) => phase.id);
191
252
  }
192
- function validateAutoPhaseId(phaseId) {
253
+ async function validateAutoPhaseId(phaseId) {
193
254
  const normalized = normalizeAutoPhaseId(phaseId);
194
- if (!autoPhaseIds().includes(normalized)) {
255
+ if (!(await autoPhaseIds()).includes(normalized)) {
195
256
  throw new TaskRunnerError(`Unknown auto-golang phase: ${phaseId}\nUse 'agentweaver auto-golang --help-phases' or '/help auto-golang' to list valid phases.`);
196
257
  }
197
258
  return normalized;
@@ -215,6 +276,17 @@ function buildFlowResumeDetails(state) {
215
276
  }
216
277
  return lines.join("\n");
217
278
  }
279
+ function buildFlowContinueDetails(state) {
280
+ const lines = [
281
+ "Continuable loop boundary found.",
282
+ `Updated: ${state.updatedAt}`,
283
+ ];
284
+ if (state.continuation?.stopPhaseId && state.continuation?.stopStepId) {
285
+ lines.push(`Stopped at: ${state.continuation.stopPhaseId}:${state.continuation.stopStepId}`);
286
+ }
287
+ lines.push("Continue will preserve existing artifacts and start the next iteration from active inputs.");
288
+ return lines.join("\n");
289
+ }
218
290
  function buildResolverContext(pipelineContext, flowParams, flowConstants, repeatVars, executionState) {
219
291
  return {
220
292
  flowParams,
@@ -288,7 +360,7 @@ function validateDeclarativePhaseResumeState(phase, phaseState, pipelineContext,
288
360
  }
289
361
  }
290
362
  }
291
- function validateDeclarativeFlowResumeState(flowEntry, config, state, executionRouting, runtime = runtimeServices) {
363
+ async function validateDeclarativeFlowResumeState(flowEntry, config, state, executionRouting, runtime = runtimeServices) {
292
364
  if (state.flowId === "auto-common") {
293
365
  const persistedPhaseIds = state.executionState.phases.map((p) => p.id);
294
366
  const hasLegacyPlanningGatePhases = persistedPhaseIds.some((id) => ["design_review", "verdict", "plan_revision", "design_review_repeat", "verdict_repeat"].includes(id));
@@ -308,7 +380,7 @@ function validateDeclarativeFlowResumeState(flowEntry, config, state, executionR
308
380
  if (flowRequiresTaskScope(flowEntry) && !config.jiraRef) {
309
381
  throw new TaskRunnerError("Resume is impossible because Jira context is missing for this flow state. Use restart.");
310
382
  }
311
- const pipelineContext = createPipelineContext({
383
+ const pipelineContext = await createPipelineContext({
312
384
  issueKey: config.taskKey,
313
385
  jiraRef: config.jiraRef,
314
386
  dryRun: config.dryRun,
@@ -338,51 +410,59 @@ function buildInteractiveBaseConfig(flowId, scope) {
338
410
  ...(flowId !== "instant-task" && scope.jiraRef ? { jiraRef: scope.jiraRef } : {}),
339
411
  });
340
412
  }
341
- function lookupInteractiveFlowResume(flowEntry, currentScope) {
413
+ async function lookupInteractiveFlowResume(flowEntry, currentScope) {
342
414
  const directState = loadFlowRunState(currentScope.scopeKey, flowEntry.id);
343
- if (directState && hasResumableFlowState(directState)) {
415
+ const availability = classifyFlowLaunchAvailability(directState);
416
+ if (directState && availability.resume.available) {
344
417
  try {
345
418
  const effectiveScope = scopeWithRestoredJiraContext(currentScope, directState);
346
419
  const baseConfig = buildInteractiveBaseConfig(flowEntry.id, effectiveScope);
347
420
  const config = buildRuntimeConfig(baseConfig, effectiveScope);
348
- validateDeclarativeFlowResumeState(flowEntry, config, directState, directState.executionRouting);
421
+ await validateDeclarativeFlowResumeState(flowEntry, config, directState, directState.executionRouting);
349
422
  return {
350
- resumeAvailable: true,
351
- hasExistingState: true,
423
+ ...availability,
352
424
  details: buildFlowResumeDetails(directState),
353
425
  };
354
426
  }
355
427
  catch (error) {
356
428
  return {
357
- resumeAvailable: false,
358
- hasExistingState: true,
429
+ ...availability,
430
+ resume: {
431
+ available: false,
432
+ reason: error.message,
433
+ },
359
434
  details: `Interrupted run found, but resume is unavailable.\n${error.message}`,
360
435
  };
361
436
  }
362
437
  }
438
+ if (directState && availability.continue.available) {
439
+ return {
440
+ ...availability,
441
+ details: buildFlowContinueDetails(directState),
442
+ };
443
+ }
363
444
  return {
364
- resumeAvailable: false,
365
- hasExistingState: Boolean(directState),
445
+ ...availability,
366
446
  };
367
447
  }
368
- function printAutoPhasesHelp() {
369
- const phaseLines = ["Available auto-golang phases:", "", ...autoPhaseIds()];
448
+ async function printAutoPhasesHelp() {
449
+ const phaseLines = ["Available auto-golang phases:", "", ...(await autoPhaseIds())];
370
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>");
371
451
  printPanel("Auto-Golang Phases", phaseLines.join("\n"), "magenta");
372
452
  }
373
- function autoCommonPhaseIds() {
374
- return 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);
375
455
  }
376
- function printAutoCommonPhasesHelp() {
377
- const phaseLines = ["Available auto-common phases:", "", ...autoCommonPhaseIds()];
378
- phaseLines.push("", "You can run auto-common with:", "agentweaver auto-common <jira>");
379
- 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");
380
460
  }
381
- function autoSimplePhaseIds() {
382
- return loadDeclarativeFlow({ source: "built-in", fileName: "auto-simple.json" }).phases.map((phase) => phase.id);
461
+ async function autoSimplePhaseIds() {
462
+ return (await loadDeclarativeFlow({ source: "built-in", fileName: "auto-simple.json" })).phases.map((phase) => phase.id);
383
463
  }
384
- function printAutoSimplePhasesHelp() {
385
- const phaseLines = ["Available auto-simple phases:", "", ...autoSimplePhaseIds()];
464
+ async function printAutoSimplePhasesHelp() {
465
+ const phaseLines = ["Available auto-simple phases:", "", ...(await autoSimplePhaseIds())];
386
466
  phaseLines.push("", "You can run auto-simple with:", "agentweaver auto-simple <jira>");
387
467
  printPanel("Auto-Simple Phases", phaseLines.join("\n"), "magenta");
388
468
  }
@@ -400,11 +480,12 @@ function buildBaseConfig(command, options = {}) {
400
480
  reviewFixPoints: options.reviewFixPoints ?? null,
401
481
  reviewBlockingSeverities: options.reviewBlockingSeverities ?? resolveReviewBlockingSeveritiesFromEnv(),
402
482
  extraPrompt: options.extraPrompt ?? null,
403
- autoFromPhase: options.autoFromPhase ? validateAutoPhaseId(options.autoFromPhase) : null,
483
+ autoFromPhase: options.autoFromPhase ?? null,
404
484
  mdLang: options.mdLang ?? null,
405
485
  dryRun: options.dryRun ?? false,
406
486
  verbose: options.verbose ?? false,
407
487
  ...(options.doctorArgs !== undefined ? { doctorArgs: options.doctorArgs } : {}),
488
+ ...(options.acceptPlaybookDraft !== undefined ? { acceptPlaybookDraft: options.acceptPlaybookDraft } : {}),
408
489
  };
409
490
  }
410
491
  function commandRequiresTask(command) {
@@ -414,6 +495,7 @@ function commandRequiresTask(command) {
414
495
  command === "design-review" ||
415
496
  command === "mr-description" ||
416
497
  command === "auto-golang" ||
498
+ command === "auto-common-guided" ||
417
499
  command === "auto-common" ||
418
500
  command === "auto-simple" ||
419
501
  command === "auto-status" ||
@@ -425,6 +507,7 @@ function commandSupportsProjectScope(command) {
425
507
  command === "gitlab-diff-review" ||
426
508
  command === "gitlab-review" ||
427
509
  command === "instant-task" ||
510
+ command === "playbook-init" ||
428
511
  command === "task-describe" ||
429
512
  command === "implement" ||
430
513
  command === "review" ||
@@ -498,29 +581,42 @@ function routingForPrerequisites(launchProfile, executionRouting) {
498
581
  function flowSpecFileForPrerequisiteChecks(command) {
499
582
  return isBuiltInCommandFlowId(command) ? builtInCommandFlowFile(command) : null;
500
583
  }
501
- function commandRoutingGroupsForPrerequisiteChecks(command, cwd) {
584
+ async function commandRoutingGroupsForPrerequisiteChecks(command, cwd) {
502
585
  const fileName = flowSpecFileForPrerequisiteChecks(command);
503
586
  if (!fileName) {
504
587
  return [];
505
588
  }
506
- return collectFlowRoutingGroups(loadDeclarativeFlow({ source: "built-in", fileName }), cwd);
589
+ return collectFlowRoutingGroups(await loadDeclarativeFlow({ source: "built-in", fileName }), cwd);
507
590
  }
508
- function resolveExecutorPrerequisite(executor) {
591
+ function resolveExecutorPrerequisite(executor, registryContext) {
509
592
  if (executor === "codex") {
510
593
  resolveCmd("codex", "CODEX_BIN");
511
594
  return;
512
595
  }
513
- resolveCmd("opencode", "OPENCODE_BIN");
596
+ if (executor === "opencode") {
597
+ resolveCmd("opencode", "OPENCODE_BIN");
598
+ return;
599
+ }
600
+ const definition = registryContext.executors.get(executor);
601
+ const config = definition.defaultConfig;
602
+ if (config
603
+ && typeof config === "object"
604
+ && !Array.isArray(config)
605
+ && typeof config.defaultCommand === "string"
606
+ && typeof config.commandEnvVar === "string") {
607
+ resolveCmd(config.defaultCommand, config.commandEnvVar);
608
+ }
514
609
  }
515
- function checkPrerequisites(config, launchProfile, executionRouting) {
610
+ async function checkPrerequisites(config, launchProfile, executionRouting) {
611
+ const registryContext = await createPipelineRegistryContext(process.cwd());
516
612
  const routing = routingForPrerequisites(launchProfile, executionRouting);
517
- const groups = commandRoutingGroupsForPrerequisiteChecks(config.command, process.cwd());
613
+ const groups = await commandRoutingGroupsForPrerequisiteChecks(config.command, process.cwd());
518
614
  for (const executor of executorsForRoutingGroups(routing, groups)) {
519
- resolveExecutorPrerequisite(executor);
615
+ resolveExecutorPrerequisite(executor, registryContext);
520
616
  }
521
617
  }
522
- function checkAutoPrerequisites(config, launchProfile, executionRouting) {
523
- checkPrerequisites(config, launchProfile, executionRouting);
618
+ async function checkAutoPrerequisites(config, launchProfile, executionRouting) {
619
+ await checkPrerequisites(config, launchProfile, executionRouting);
524
620
  }
525
621
  function autoFlowParams(config, forceRefreshSummary = false) {
526
622
  return {
@@ -536,6 +632,8 @@ function autoFlowParams(config, forceRefreshSummary = false) {
536
632
  reviewBlockingSeverities: config.reviewBlockingSeverities,
537
633
  forceRefresh: forceRefreshSummary,
538
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,
539
637
  runGoTestsScript: path.join(agentweaverHome(PACKAGE_ROOT), "run_go_tests.py"),
540
638
  runGoLinterScript: path.join(agentweaverHome(PACKAGE_ROOT), "run_go_linter.py"),
541
639
  runGoTestsIteration: nextArtifactIteration(config.taskKey, "run-go-tests-result", "json"),
@@ -616,7 +714,7 @@ function interactiveFlowDefinition(entry) {
616
714
  description: flow.description ?? "No description available for this flow.",
617
715
  source: entry.source,
618
716
  treePath: [...entry.treePath],
619
- ...(entry.source === "project-local" ? { sourcePath: entry.absolutePath } : {}),
717
+ ...(entry.source !== "built-in" ? { sourcePath: entry.absolutePath } : {}),
620
718
  phases: flow.phases.map((phase) => ({
621
719
  id: phase.id,
622
720
  repeatVars: Object.fromEntries(Object.entries(phase.repeatVars).map(([key, value]) => [key, value])),
@@ -666,7 +764,7 @@ function findCurrentFlowExecutionStep(state) {
666
764
  return null;
667
765
  }
668
766
  async function runDeclarativeFlowByRef(flowId, flowRef, config, flowParams, overrides = {}, requestUserInput = requestUserInputInTerminal, setSummary, launchMode = "restart", runtime = runtimeServices) {
669
- const context = createPipelineContext({
767
+ const context = await createPipelineContext({
670
768
  issueKey: config.taskKey,
671
769
  jiraRef: config.jiraRef,
672
770
  dryRun: config.dryRun,
@@ -677,7 +775,7 @@ async function runDeclarativeFlowByRef(flowId, flowRef, config, flowParams, over
677
775
  requestUserInput,
678
776
  ...(overrides.executionRouting ? { executionRouting: overrides.executionRouting } : {}),
679
777
  });
680
- const flow = loadDeclarativeFlow(flowRef);
778
+ const flow = await loadDeclarativeFlow(flowRef);
681
779
  const initialExecutionState = {
682
780
  flowKind: flow.kind,
683
781
  flowVersion: flow.version,
@@ -685,9 +783,12 @@ async function runDeclarativeFlowByRef(flowId, flowRef, config, flowParams, over
685
783
  terminationOutcome: "success",
686
784
  phases: [],
687
785
  };
688
- let persistedState = launchMode === "resume" ? loadFlowRunState(config.scope.scopeKey, flowId) : null;
786
+ const existingStateForRestart = launchMode === "restart" ? loadFlowRunState(config.scope.scopeKey, flowId) : null;
787
+ let persistedState = launchMode === "resume" || launchMode === "continue"
788
+ ? loadFlowRunState(config.scope.scopeKey, flowId)
789
+ : null;
689
790
  if (persistedState && launchMode === "resume") {
690
- validateDeclarativeFlowResumeState({
791
+ await validateDeclarativeFlowResumeState({
691
792
  id: flowId,
692
793
  source: flow.source,
693
794
  fileName: flow.fileName,
@@ -700,7 +801,13 @@ async function runDeclarativeFlowByRef(flowId, flowRef, config, flowParams, over
700
801
  } }) : undefined), runtime);
701
802
  persistedState = prepareFlowStateForResume(persistedState);
702
803
  }
804
+ else if (persistedState && launchMode === "continue") {
805
+ persistedState = prepareFlowStateForContinue(persistedState, flow.phases);
806
+ }
703
807
  else if (launchMode === "restart") {
808
+ if (existingStateForRestart) {
809
+ archiveActiveAttempt(config.scope.scopeKey);
810
+ }
704
811
  resetFlowRunState(config.scope.scopeKey, flowId);
705
812
  }
706
813
  const executionState = persistedState?.executionState ?? initialExecutionState;
@@ -772,7 +879,7 @@ async function runDeclarativeFlowBySpecFile(fileName, config, flowParams, overri
772
879
  ...defaultDeclarativeFlowParams(config, false, overrides),
773
880
  ...flowParams,
774
881
  };
775
- await runDeclarativeFlowByRef(config.command, { source: "built-in", fileName }, config, withCanonicalReviewLoopParams(loadDeclarativeFlow({ source: "built-in", fileName }).kind, mergedFlowParams), overrides, requestUserInput, setSummary, launchMode, runtime);
882
+ await runDeclarativeFlowByRef(config.command, { source: "built-in", fileName }, config, withCanonicalReviewLoopParams((await loadDeclarativeFlow({ source: "built-in", fileName })).kind, mergedFlowParams), overrides, requestUserInput, setSummary, launchMode, runtime);
776
883
  }
777
884
  function defaultDeclarativeFlowParams(config, forceRefreshSummary = false, overrides = {}) {
778
885
  const iteration = nextReviewIterationForTask(config.taskKey);
@@ -804,10 +911,15 @@ function defaultDeclarativeFlowParams(config, forceRefreshSummary = false, overr
804
911
  mdLang: config.mdLang,
805
912
  llmExecutor: launchProfile.executor,
806
913
  llmModel: launchProfile.model,
914
+ projectGuidanceFile: "not provided",
915
+ projectGuidanceJsonFile: "not provided",
916
+ repairProjectGuidanceFile: "not provided",
917
+ repairProjectGuidanceJsonFile: "not provided",
807
918
  launchProfile,
808
919
  executionRouting,
809
920
  iteration,
810
921
  baseIteration: iteration,
922
+ designReviewBaseIteration: nextDesignReviewIterationForTask(config.taskKey),
811
923
  latestIteration,
812
924
  taskContextIteration: latestTaskContext ?? nextArtifactIteration(config.taskKey, "task-context", "json"),
813
925
  taskSummaryIteration: nextArtifactIteration(config.taskKey, "task"),
@@ -818,6 +930,60 @@ function defaultDeclarativeFlowParams(config, forceRefreshSummary = false, overr
818
930
  forceRefresh: forceRefreshSummary,
819
931
  };
820
932
  }
933
+ function countAvailableNonRestartActions(availability) {
934
+ return Number(availability.resume.available) + Number(availability.continue.available);
935
+ }
936
+ async function chooseLaunchMode(flowId, scopeKey, explicitLaunchMode, requestUserInput) {
937
+ const state = loadFlowRunState(scopeKey, flowId);
938
+ const availability = classifyFlowLaunchAvailability(state);
939
+ if (explicitLaunchMode) {
940
+ const selectedAvailability = availability[explicitLaunchMode];
941
+ if (!selectedAvailability.available) {
942
+ throw new TaskRunnerError(`${explicitLaunchMode.charAt(0).toUpperCase()}${explicitLaunchMode.slice(1)} is not available for '${flowId}'. ${selectedAvailability.reason}`);
943
+ }
944
+ return explicitLaunchMode;
945
+ }
946
+ if (!availability.hasExistingState) {
947
+ return "restart";
948
+ }
949
+ const availableNonRestart = countAvailableNonRestartActions(availability);
950
+ if (availableNonRestart === 0) {
951
+ return "restart";
952
+ }
953
+ const interactive = requestUserInput !== requestUserInputInTerminal || (process.stdin.isTTY && process.stdout.isTTY);
954
+ if (!interactive) {
955
+ throw new TaskRunnerError(`Multiple actions are valid for '${flowId}'. Re-run with one of: --resume, --continue, --restart.`);
956
+ }
957
+ const result = await requestUserInput({
958
+ formId: `launch-mode-${flowId}`,
959
+ title: "Launch Action",
960
+ description: `Select how to start '${flowId}'.`,
961
+ submitLabel: "Start",
962
+ fields: [
963
+ {
964
+ id: "launchMode",
965
+ type: "single-select",
966
+ label: "Action",
967
+ required: true,
968
+ default: availability.continue.available ? "continue" : availability.resume.available ? "resume" : "restart",
969
+ options: [
970
+ ...(availability.resume.available
971
+ ? [{ value: "resume", label: "Resume", description: availability.resume.reason }]
972
+ : []),
973
+ ...(availability.continue.available
974
+ ? [{ value: "continue", label: "Continue", description: availability.continue.reason }]
975
+ : []),
976
+ { value: "restart", label: "Restart", description: availability.restart.reason },
977
+ ],
978
+ },
979
+ ],
980
+ });
981
+ const selected = result.values.launchMode;
982
+ if (selected !== "resume" && selected !== "continue" && selected !== "restart") {
983
+ throw new TaskRunnerError(`Invalid launch action selected for '${flowId}'.`);
984
+ }
985
+ return selected;
986
+ }
821
987
  const TASK_SCOPE_PARAM_REFS = new Set(["params.jiraApiUrl", "params.jiraBrowseUrl", "params.jiraTaskFile"]);
822
988
  function valueReferencesTaskScopeParams(value) {
823
989
  if (Array.isArray(value)) {
@@ -840,7 +1006,7 @@ function flowRequiresTaskScope(entry) {
840
1006
  return valueReferencesTaskScopeParams(entry.flow.phases);
841
1007
  }
842
1008
  async function summarizeBuildFailure(output) {
843
- return summarizeBuildFailureViaPipeline(createPipelineContext({
1009
+ return summarizeBuildFailureViaPipeline(await createPipelineContext({
844
1010
  issueKey: "build-failure-summary",
845
1011
  jiraRef: "build-failure-summary",
846
1012
  dryRun: false,
@@ -855,7 +1021,7 @@ function requireJiraConfig(config) {
855
1021
  throw new TaskRunnerError(`Command '${config.command}' requires Jira context in the current project scope.`);
856
1022
  }
857
1023
  }
858
- async function executeCommand(baseConfig, runFollowupVerify = true, requestUserInput = requestUserInputInTerminal, resolvedScope, setSummary, forceRefreshSummary = false, launchMode = "restart", launchProfile, executionRouting, selectedRoutingPreset, runtime = runtimeServices) {
1024
+ async function executeCommand(baseConfig, runFollowupVerify = true, requestUserInput = requestUserInputInTerminal, resolvedScope, setSummary, forceRefreshSummary = false, explicitLaunchMode, launchProfile, executionRouting, selectedRoutingPreset, runtime = runtimeServices) {
859
1025
  if (baseConfig.command === "doctor") {
860
1026
  const exitCode = await runDoctorCommand(baseConfig.doctorArgs ?? []);
861
1027
  return exitCode === 0;
@@ -870,8 +1036,11 @@ async function executeCommand(baseConfig, runFollowupVerify = true, requestUserI
870
1036
  : launchProfile
871
1037
  ? { launchProfile }
872
1038
  : {};
1039
+ const launchMode = config.command === "auto-status" || config.command === "auto-reset"
1040
+ ? "restart"
1041
+ : await chooseLaunchMode(config.command, config.scope.scopeKey, explicitLaunchMode, requestUserInput);
873
1042
  if (config.command === "instant-task") {
874
- checkPrerequisites(config, launchProfile, executionRouting);
1043
+ await checkPrerequisites(config, launchProfile, executionRouting);
875
1044
  const hasPersistedInstantTaskState = loadFlowRunState(config.scope.scopeKey, "instant-task") !== null;
876
1045
  const repromptInstantTaskInput = launchMode === "restart"
877
1046
  && hasPersistedInstantTaskState
@@ -900,7 +1069,8 @@ async function executeCommand(baseConfig, runFollowupVerify = true, requestUserI
900
1069
  let effectiveLaunchProfile = launchProfile;
901
1070
  let effectiveExecutionRouting = executionRouting;
902
1071
  if (config.autoFromPhase) {
903
- const flow = loadDeclarativeFlow({ source: "built-in", fileName: "auto-golang.json" });
1072
+ config.autoFromPhase = await validateAutoPhaseId(config.autoFromPhase);
1073
+ const flow = await loadDeclarativeFlow({ source: "built-in", fileName: "auto-golang.json" });
904
1074
  const persistedState = loadFlowRunState(config.scope.scopeKey, "auto-golang");
905
1075
  if (!persistedState) {
906
1076
  throw new TaskRunnerError(`Cannot restart auto-golang from phase '${config.autoFromPhase}' because persisted flow state was not found.`);
@@ -912,7 +1082,7 @@ async function executeCommand(baseConfig, runFollowupVerify = true, requestUserI
912
1082
  effectiveExecutionRouting ??= persistedState.executionRouting;
913
1083
  printPanel("Auto-Golang Resume", `Auto-golang pipeline will continue from phase: ${config.autoFromPhase}`, "yellow");
914
1084
  }
915
- checkAutoPrerequisites(config, effectiveLaunchProfile, effectiveExecutionRouting);
1085
+ await checkAutoPrerequisites(config, effectiveLaunchProfile, effectiveExecutionRouting);
916
1086
  await runDeclarativeFlowBySpecFile("auto-golang.json", config, autoFlowParams(config, forceRefreshSummary), effectiveExecutionRouting
917
1087
  ? {
918
1088
  launchProfile: effectiveExecutionRouting.defaultRoute,
@@ -924,18 +1094,18 @@ async function executeCommand(baseConfig, runFollowupVerify = true, requestUserI
924
1094
  : {}, requestUserInput, setSummary, effectiveLaunchMode, runtime);
925
1095
  return false;
926
1096
  }
927
- if (config.command === "auto-common") {
1097
+ if (config.command === "auto-common" || config.command === "auto-common-guided") {
928
1098
  requireJiraConfig(config);
929
- checkAutoPrerequisites(config, launchProfile, executionRouting);
1099
+ await checkAutoPrerequisites(config, launchProfile, executionRouting);
930
1100
  process.env.JIRA_BROWSE_URL = config.jiraBrowseUrl;
931
1101
  process.env.JIRA_API_URL = config.jiraApiUrl;
932
1102
  process.env.JIRA_TASK_FILE = config.jiraTaskFile;
933
- 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);
934
1104
  return false;
935
1105
  }
936
1106
  if (config.command === "auto-simple") {
937
1107
  requireJiraConfig(config);
938
- checkAutoPrerequisites(config, launchProfile, executionRouting);
1108
+ await checkAutoPrerequisites(config, launchProfile, executionRouting);
939
1109
  process.env.JIRA_BROWSE_URL = config.jiraBrowseUrl;
940
1110
  process.env.JIRA_API_URL = config.jiraApiUrl;
941
1111
  process.env.JIRA_TASK_FILE = config.jiraTaskFile;
@@ -949,7 +1119,7 @@ async function executeCommand(baseConfig, runFollowupVerify = true, requestUserI
949
1119
  return false;
950
1120
  }
951
1121
  const currentStep = findCurrentFlowExecutionStep(state) ?? state.currentStep ?? "-";
952
- const phaseOrder = loadDeclarativeFlow({ source: "built-in", fileName: "auto-golang.json" }).phases;
1122
+ const phaseOrder = (await loadDeclarativeFlow({ source: "built-in", fileName: "auto-golang.json" })).phases;
953
1123
  const lines = [
954
1124
  `Issue: ${config.taskKey}`,
955
1125
  `Status: ${state.status}`,
@@ -986,7 +1156,7 @@ async function executeCommand(baseConfig, runFollowupVerify = true, requestUserI
986
1156
  printPanel("Auto-Golang Reset", removed ? `State file ${flowStateFile(config.scope.scopeKey, "auto-golang")} removed.` : "No flow state file found.", "yellow");
987
1157
  return false;
988
1158
  }
989
- checkPrerequisites(config, launchProfile, executionRouting);
1159
+ await checkPrerequisites(config, launchProfile, executionRouting);
990
1160
  if (config.jiraBrowseUrl && config.jiraApiUrl && config.jiraTaskFile) {
991
1161
  process.env.JIRA_BROWSE_URL = config.jiraBrowseUrl ?? "";
992
1162
  process.env.JIRA_API_URL = config.jiraApiUrl ?? "";
@@ -1031,6 +1201,14 @@ async function executeCommand(baseConfig, runFollowupVerify = true, requestUserI
1031
1201
  }, flowOverrides, requestUserInput, setSummary, launchMode, runtime);
1032
1202
  return false;
1033
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
+ }
1034
1212
  if (config.command === "bug-analyze") {
1035
1213
  requireJiraConfig(config);
1036
1214
  if (config.verbose) {
@@ -1290,22 +1468,22 @@ async function executeCommand(baseConfig, runFollowupVerify = true, requestUserI
1290
1468
  }
1291
1469
  throw new TaskRunnerError(`Unsupported command: ${config.command}`);
1292
1470
  }
1293
- function parseCliArgs(argv) {
1471
+ async function parseCliArgs(argv) {
1294
1472
  if (argv.includes("--version") || argv.includes("-v")) {
1295
- process.stdout.write(`${packageVersion()}\n`);
1473
+ writeStdoutSync(`${packageVersion()}\n`);
1296
1474
  process.exit(0);
1297
1475
  }
1298
1476
  if (argv.includes("--help") || argv.includes("-h")) {
1299
- process.stdout.write(`${usage()}\n`);
1477
+ writeStdoutSync(`${usage()}\n`);
1300
1478
  process.exit(0);
1301
1479
  }
1302
1480
  if (argv.length === 0) {
1303
- process.stderr.write(`${usage()}\n`);
1481
+ writeStderrSync(`${usage()}\n`);
1304
1482
  process.exit(1);
1305
1483
  }
1306
1484
  const command = argv[0];
1307
1485
  if (!COMMANDS.includes(command)) {
1308
- process.stderr.write(`${usage()}\n`);
1486
+ writeStderrSync(`${usage()}\n`);
1309
1487
  process.exit(1);
1310
1488
  }
1311
1489
  let dry = false;
@@ -1317,6 +1495,10 @@ function parseCliArgs(argv) {
1317
1495
  let helpPhases = false;
1318
1496
  let jiraRef;
1319
1497
  let mdLang;
1498
+ let launchMode;
1499
+ let acceptPlaybookDraft = false;
1500
+ let webNoOpen = process.env.AGENTWEAVER_WEB_NO_OPEN === "1";
1501
+ let webHost;
1320
1502
  const doctorArgs = [];
1321
1503
  for (let index = 1; index < argv.length; index += 1) {
1322
1504
  const token = argv[index] ?? "";
@@ -1332,6 +1514,61 @@ function parseCliArgs(argv) {
1332
1514
  helpPhases = true;
1333
1515
  continue;
1334
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
+ }
1564
+ if (token === "--resume" || token === "--continue" || token === "--restart") {
1565
+ if (launchMode) {
1566
+ writeStderrSync("Error: --resume, --continue, and --restart are mutually exclusive.\n");
1567
+ process.exit(1);
1568
+ }
1569
+ launchMode = token.slice(2);
1570
+ continue;
1571
+ }
1335
1572
  if (token === "--prompt") {
1336
1573
  prompt = argv[index + 1];
1337
1574
  index += 1;
@@ -1362,7 +1599,7 @@ function parseCliArgs(argv) {
1362
1599
  mdLang = langValue;
1363
1600
  }
1364
1601
  else {
1365
- 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");
1366
1603
  process.exit(1);
1367
1604
  }
1368
1605
  index += 1;
@@ -1374,7 +1611,7 @@ function parseCliArgs(argv) {
1374
1611
  mdLang = langValue;
1375
1612
  }
1376
1613
  else {
1377
- 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");
1378
1615
  process.exit(1);
1379
1616
  }
1380
1617
  continue;
@@ -1387,15 +1624,15 @@ function parseCliArgs(argv) {
1387
1624
  }
1388
1625
  }
1389
1626
  if (command === "auto-golang" && helpPhases) {
1390
- printAutoPhasesHelp();
1627
+ await printAutoPhasesHelp();
1391
1628
  process.exit(0);
1392
1629
  }
1393
- if (command === "auto-common" && helpPhases) {
1394
- 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");
1395
1632
  process.exit(0);
1396
1633
  }
1397
1634
  if (command === "auto-simple" && helpPhases) {
1398
- printAutoSimplePhasesHelp();
1635
+ await printAutoSimplePhasesHelp();
1399
1636
  process.exit(0);
1400
1637
  }
1401
1638
  return {
@@ -1410,6 +1647,10 @@ function parseCliArgs(argv) {
1410
1647
  ...(autoFromPhase !== undefined ? { autoFromPhase } : {}),
1411
1648
  ...(mdLang !== undefined ? { mdLang } : {}),
1412
1649
  ...(doctorArgs.length > 0 ? { doctorArgs } : {}),
1650
+ ...(launchMode !== undefined ? { launchMode } : {}),
1651
+ ...(acceptPlaybookDraft ? { acceptPlaybookDraft } : {}),
1652
+ ...(command === "web" ? { webNoOpen } : {}),
1653
+ ...(command === "web" && webHost !== undefined ? { webHost } : {}),
1413
1654
  };
1414
1655
  }
1415
1656
  function buildConfigFromArgs(args) {
@@ -1423,32 +1664,75 @@ function buildConfigFromArgs(args) {
1423
1664
  dryRun: args.dry,
1424
1665
  verbose: args.verbose,
1425
1666
  ...(args.doctorArgs !== undefined ? { doctorArgs: args.doctorArgs } : {}),
1667
+ ...(args.acceptPlaybookDraft !== undefined ? { acceptPlaybookDraft: args.acceptPlaybookDraft } : {}),
1426
1668
  });
1427
1669
  }
1428
- async function runInteractive(jiraRef, forceRefresh = false, scopeName) {
1670
+ async function runInteractiveWithSessionFactory(createSession, jiraRef, forceRefresh = false, scopeName, installSignalCleanup = false) {
1429
1671
  let currentScope = resolveProjectScope(scopeName, jiraRef);
1430
- const gitBranchName = detectGitBranchName();
1431
- const flowCatalog = loadInteractiveFlowCatalog(process.cwd());
1672
+ const flowCatalog = await loadInteractiveFlowCatalog(process.cwd());
1432
1673
  let activeAbortController = null;
1433
1674
  let activeFlowId = null;
1675
+ let pendingScopeSwitch = null;
1676
+ const autoScopeSwitchEnabled = !scopeName?.trim() && !jiraRef?.trim();
1677
+ let lastObservedGitScope = currentScope;
1678
+ let ui;
1434
1679
  let exiting = false;
1435
- 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({
1436
1718
  scopeKey: currentScope.scopeKey,
1437
1719
  jiraIssueKey: currentScope.jiraIssueKey ?? null,
1438
1720
  summaryText: "",
1439
1721
  cwd: process.cwd(),
1440
- gitBranchName,
1722
+ gitBranchName: currentScope.gitBranchName,
1441
1723
  version: packageVersion(),
1442
1724
  flows: interactiveFlowDefinitions(flowCatalog),
1443
1725
  getRunConfirmation: async (flowId) => {
1726
+ refreshScopeFromGit("git scope refresh before launch confirmation");
1444
1727
  const flowEntry = findCatalogEntry(flowId, flowCatalog);
1445
1728
  if (!flowEntry) {
1446
1729
  throw new TaskRunnerError(`Unknown flow: ${flowId}`);
1447
1730
  }
1448
- const resumeLookup = lookupInteractiveFlowResume(flowEntry, currentScope);
1731
+ const resumeLookup = await lookupInteractiveFlowResume(flowEntry, currentScope);
1449
1732
  return resumeLookup;
1450
1733
  },
1451
1734
  onRun: async (flowId, launchMode) => {
1735
+ refreshScopeFromGit("git scope refresh before flow launch");
1452
1736
  const abortController = new AbortController();
1453
1737
  activeAbortController = abortController;
1454
1738
  activeFlowId = flowId;
@@ -1457,7 +1741,7 @@ async function runInteractive(jiraRef, forceRefresh = false, scopeName) {
1457
1741
  if (!flowEntry) {
1458
1742
  throw new TaskRunnerError(`Unknown flow: ${flowId}`);
1459
1743
  }
1460
- const routingGroups = flowRoutingGroups(flowEntry, process.cwd());
1744
+ const routingGroups = await flowRoutingGroups(flowEntry, process.cwd());
1461
1745
  const resumeState = launchMode === "resume" ? loadFlowRunState(currentScope.scopeKey, flowId) : null;
1462
1746
  if (resumeState) {
1463
1747
  currentScope = scopeWithRestoredJiraContext(currentScope, resumeState);
@@ -1484,7 +1768,7 @@ async function runInteractive(jiraRef, forceRefresh = false, scopeName) {
1484
1768
  const jiraContext = await requestJiraContext((form) => ui.requestUserInput(form));
1485
1769
  currentScope = resolveProjectScope(null, jiraContext.jiraRef);
1486
1770
  }
1487
- ui.setScope(currentScope.scopeKey, currentScope.jiraIssueKey ?? null);
1771
+ ui.setScope(currentScope.scopeKey, currentScope.jiraIssueKey ?? null, currentScope.gitBranchName);
1488
1772
  if (previousScopeKey !== currentScope.scopeKey || currentScope.jiraIssueKey) {
1489
1773
  syncInteractiveTaskSummary(ui, currentScope, forceRefresh);
1490
1774
  }
@@ -1526,6 +1810,11 @@ async function runInteractive(jiraRef, forceRefresh = false, scopeName) {
1526
1810
  if (activeAbortController === abortController) {
1527
1811
  activeAbortController = null;
1528
1812
  activeFlowId = null;
1813
+ if (pendingScopeSwitch && !exiting) {
1814
+ const nextScope = pendingScopeSwitch;
1815
+ pendingScopeSwitch = null;
1816
+ applyScopeSwitch(nextScope, "git scope refresh after flow completion");
1817
+ }
1529
1818
  }
1530
1819
  }
1531
1820
  },
@@ -1537,6 +1826,10 @@ async function runInteractive(jiraRef, forceRefresh = false, scopeName) {
1537
1826
  activeAbortController.abort();
1538
1827
  },
1539
1828
  onExit: () => {
1829
+ if (activeAbortController) {
1830
+ ui.interruptActiveForm();
1831
+ activeAbortController.abort();
1832
+ }
1540
1833
  exiting = true;
1541
1834
  },
1542
1835
  });
@@ -1547,13 +1840,45 @@ async function runInteractive(jiraRef, forceRefresh = false, scopeName) {
1547
1840
  ui.appendLog("[scope] project scope active; task summary will appear after a Jira-backed flow runs");
1548
1841
  }
1549
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;
1550
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
+ }
1551
1865
  const interval = setInterval(() => {
1552
1866
  if (!exiting) {
1553
1867
  return;
1554
1868
  }
1555
1869
  clearInterval(interval);
1556
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
+ }
1557
1882
  ui.destroy();
1558
1883
  bye();
1559
1884
  resolve(0);
@@ -1564,6 +1889,12 @@ async function runInteractive(jiraRef, forceRefresh = false, scopeName) {
1564
1889
  }, 100);
1565
1890
  });
1566
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
+ }
1567
1898
  export async function main(argv = process.argv.slice(2)) {
1568
1899
  loadTieredEnv(process.cwd());
1569
1900
  let forceRefresh = false;
@@ -1573,14 +1904,22 @@ export async function main(argv = process.argv.slice(2)) {
1573
1904
  args.shift();
1574
1905
  }
1575
1906
  try {
1907
+ if (args[0] === "--no-open") {
1908
+ throw new TaskRunnerError("--no-open is only supported after the web command.");
1909
+ }
1576
1910
  if (args.length === 0) {
1577
1911
  return await runInteractive(undefined, forceRefresh);
1578
1912
  }
1579
1913
  if (args.length === 1 && !args[0]?.startsWith("-") && !COMMANDS.includes(args[0])) {
1580
1914
  return await runInteractive(args[0] ?? "", forceRefresh);
1581
1915
  }
1582
- const parsedArgs = parseCliArgs(args);
1583
- const commandCompleted = await executeCommand(buildConfigFromArgs(parsedArgs));
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
+ }
1922
+ const commandCompleted = await executeCommand(buildConfigFromArgs(parsedArgs), true, requestUserInputInTerminal, undefined, undefined, false, parsedArgs.launchMode);
1584
1923
  if (parsedArgs.command === "doctor") {
1585
1924
  return commandCompleted ? 0 : 1;
1586
1925
  }
@@ -1588,12 +1927,12 @@ export async function main(argv = process.argv.slice(2)) {
1588
1927
  }
1589
1928
  catch (error) {
1590
1929
  if (error instanceof TaskRunnerError) {
1591
- printError(error.message);
1930
+ writeStderrSync(`Error: ${error.message}\n`);
1592
1931
  return 1;
1593
1932
  }
1594
1933
  const returnCode = Number(error.returnCode);
1595
1934
  if (!Number.isNaN(returnCode)) {
1596
- printError(formatProcessFailure(error));
1935
+ writeStderrSync(`Error: ${formatProcessFailure(error)}\n`);
1597
1936
  return returnCode || 1;
1598
1937
  }
1599
1938
  throw error;