bosun 0.42.2 → 0.42.4

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 (49) hide show
  1. package/.env.example +9 -0
  2. package/agent/agent-event-bus.mjs +10 -0
  3. package/agent/agent-supervisor.mjs +20 -0
  4. package/bosun-tui.mjs +107 -105
  5. package/cli.mjs +10 -0
  6. package/config/config.mjs +25 -0
  7. package/config/executor-config.mjs +124 -1
  8. package/infra/container-runner.mjs +565 -1
  9. package/infra/monitor.mjs +18 -0
  10. package/infra/tracing.mjs +544 -240
  11. package/infra/tui-bridge.mjs +13 -1
  12. package/kanban/kanban-adapter.mjs +128 -4
  13. package/lib/repo-map.mjs +114 -3
  14. package/package.json +11 -4
  15. package/server/ui-server.mjs +3 -0
  16. package/task/task-archiver.mjs +18 -6
  17. package/task/task-attachments.mjs +14 -10
  18. package/task/task-cli.mjs +24 -4
  19. package/task/task-executor.mjs +19 -0
  20. package/task/task-store.mjs +194 -37
  21. package/telegram/telegram-bot.mjs +4 -1
  22. package/tui/app.mjs +131 -171
  23. package/tui/components/status-header.mjs +178 -75
  24. package/tui/lib/header-config.mjs +68 -0
  25. package/tui/lib/ws-bridge.mjs +61 -9
  26. package/tui/screens/agents.mjs +127 -0
  27. package/tui/screens/tasks.mjs +1 -48
  28. package/ui/app.js +8 -5
  29. package/ui/components/kanban-board.js +65 -3
  30. package/ui/components/session-list.js +18 -32
  31. package/ui/demo-defaults.js +52 -2
  32. package/ui/modules/session-api.js +100 -0
  33. package/ui/modules/state.js +71 -15
  34. package/ui/tabs/workflows.js +25 -1
  35. package/ui/tui/App.js +298 -0
  36. package/ui/tui/TasksScreen.js +564 -0
  37. package/ui/tui/constants.js +55 -0
  38. package/ui/tui/tasks-screen-helpers.js +301 -0
  39. package/ui/tui/useTasks.js +61 -0
  40. package/ui/tui/useWebSocket.js +166 -0
  41. package/ui/tui/useWorkflows.js +30 -0
  42. package/workflow/workflow-engine.mjs +412 -7
  43. package/workflow/workflow-nodes.mjs +616 -75
  44. package/workflow-templates/agents.mjs +3 -0
  45. package/workflow-templates/planning.mjs +7 -0
  46. package/workflow-templates/sub-workflows.mjs +5 -0
  47. package/workflow-templates/task-execution.mjs +3 -0
  48. package/workspace/command-diagnostics.mjs +1 -1
  49. package/workspace/context-cache.mjs +182 -9
@@ -39,6 +39,7 @@ import { getToolsPromptBlock } from "../agent/agent-custom-tools.mjs";
39
39
  import { buildRelevantSkillsPromptBlock, emitSkillInvokeEvent, findRelevantSkills } from "../agent/bosun-skills.mjs";
40
40
  import { readBenchmarkModeState, taskMatchesBenchmarkMode } from "../bench/benchmark-mode.mjs";
41
41
  import { getSessionTracker } from "../infra/session-tracker.mjs";
42
+ import { runInIsolatedRunner } from "../infra/container-runner.mjs";
42
43
  import { recordWorktreeRecoveryEvent } from "../infra/worktree-recovery-state.mjs";
43
44
  import {
44
45
  appendKnowledgeEntry,
@@ -57,7 +58,7 @@ import { loadConfig } from "../config/config.mjs";
57
58
  import { fixGitConfigCorruption } from "../workspace/worktree-manager.mjs";
58
59
  import { clearBlockedWorktreeIdentity, normalizeBaseBranch } from "../git/git-safety.mjs";
59
60
  import { getBosunCoAuthorTrailer, shouldAddBosunCoAuthor } from "../git/git-commit-helpers.mjs";
60
- import { buildArchitectEditorFrame } from "../lib/repo-map.mjs";
61
+ import { buildArchitectEditorFrame, buildRepoTopologyContext, hasRepoMapContext } from "../lib/repo-map.mjs";
61
62
  import { getGitHubToken, invalidateTokenType } from "../github/github-auth-manager.mjs";
62
63
  import {
63
64
  CUSTOM_NODE_DIR_NAME,
@@ -1448,6 +1449,208 @@ async function compactWorkflowCommandResult({
1448
1449
  };
1449
1450
  }
1450
1451
 
1452
+ function isValidationSandboxFailureText(text) {
1453
+ return /(?:sandbox|operation not permitted|permission denied|access is denied|read-only file system|EPERM|EACCES|denied by policy|seccomp)/i.test(
1454
+ String(text || ""),
1455
+ );
1456
+ }
1457
+
1458
+ function isValidationBootstrapFailureText(text) {
1459
+ return /(?:\bENOENT\b|spawn\s+.+\s+ENOENT|not recognized as an internal or external command|is not recognized as a name of a cmdlet|command not found|executable file not found|no such file or directory|cannot find the file|failed to start|startup failure)/i.test(
1460
+ String(text || ""),
1461
+ );
1462
+ }
1463
+
1464
+ function buildValidationFailureDiagnostic({
1465
+ command = "",
1466
+ args = [],
1467
+ status = "error",
1468
+ exitCode = null,
1469
+ stderr = "",
1470
+ output = "",
1471
+ timeoutMs = null,
1472
+ blocked = false,
1473
+ failureDiagnostic = null,
1474
+ } = {}) {
1475
+ if (failureDiagnostic && typeof failureDiagnostic === "object") return failureDiagnostic;
1476
+ const normalizedStatus = String(status || "error").trim().toLowerCase();
1477
+ if (!blocked && normalizedStatus === "success" && Number(exitCode ?? 0) === 0) {
1478
+ return null;
1479
+ }
1480
+ const combinedText = [stderr, output]
1481
+ .map((value) => String(value || "").trim())
1482
+ .find(Boolean) || "";
1483
+
1484
+ let category = "command_failure";
1485
+ let retryable = false;
1486
+ let summary = `Validation command exited with code ${exitCode ?? "unknown"}.`;
1487
+
1488
+ if (blocked || normalizedStatus === "blocked") {
1489
+ category = "runner_unavailable";
1490
+ retryable = true;
1491
+ summary = "Isolated runner was unavailable before the validation command started.";
1492
+ } else if (normalizedStatus === "timeout" || /(?:timed out|ETIMEDOUT|SIGTERM)/i.test(combinedText)) {
1493
+ category = "timeout";
1494
+ retryable = true;
1495
+ const numericTimeoutMs = timeoutMs != null ? Number(timeoutMs) : NaN;
1496
+ if (Number.isFinite(numericTimeoutMs) && numericTimeoutMs > 0) {
1497
+ summary = `Validation timed out after ${numericTimeoutMs}ms.`;
1498
+ } else {
1499
+ summary = "Validation timed out after the configured timeout.";
1500
+ }
1501
+ } else if (isValidationSandboxFailureText(combinedText)) {
1502
+ category = "sandbox_error";
1503
+ retryable = false;
1504
+ summary = "Validation was blocked by sandbox or filesystem restrictions.";
1505
+ } else if (isValidationBootstrapFailureText(combinedText) || normalizedStatus === "error" && (exitCode == null || Number(exitCode) < 0)) {
1506
+ category = "bootstrap_failure";
1507
+ retryable = true;
1508
+ summary = "Validation could not start cleanly.";
1509
+ }
1510
+
1511
+ const detail = String(combinedText || "").trim().split(/\r?\n/).find(Boolean) || "";
1512
+ return {
1513
+ category,
1514
+ retryable,
1515
+ summary,
1516
+ detail,
1517
+ status: normalizedStatus,
1518
+ exitCode: exitCode ?? null,
1519
+ blocked: blocked === true,
1520
+ command,
1521
+ args: Array.isArray(args) ? [...args] : [],
1522
+ };
1523
+ }
1524
+
1525
+ function didValidationCommandPass(result = {}) {
1526
+ if (!result || result.blocked === true || result.failureDiagnostic) return false;
1527
+ const status = String(result.status || "success").trim().toLowerCase();
1528
+ if (status && status !== "success") return false;
1529
+ return Number(result.exitCode ?? 0) === 0;
1530
+ }
1531
+
1532
+ function buildValidationResult({
1533
+ passed,
1534
+ exitCode = null,
1535
+ blocked = false,
1536
+ compacted = {},
1537
+ extras = {},
1538
+ failureDiagnostic = null,
1539
+ } = {}) {
1540
+ return {
1541
+ passed,
1542
+ exitCode,
1543
+ blocked,
1544
+ ...(failureDiagnostic
1545
+ ? {
1546
+ failureKind: failureDiagnostic.category || null,
1547
+ retryable: failureDiagnostic.retryable === true,
1548
+ failureDiagnostic,
1549
+ }
1550
+ : {}),
1551
+ ...compacted,
1552
+ ...extras,
1553
+ };
1554
+ }
1555
+
1556
+ function buildIsolatedRunnerResultExtras(result, lane) {
1557
+ const artifacts = Array.isArray(result?.artifacts) ? result.artifacts : [];
1558
+ return {
1559
+ isolatedRunner: {
1560
+ lane: lane?.lane || "main",
1561
+ reason: lane?.reason || "unknown",
1562
+ provider: result?.provider || null,
1563
+ leaseId: result?.leaseId || null,
1564
+ artifactRoot: result?.artifactRoot || null,
1565
+ attempts: result?.attempts || 1,
1566
+ blocked: result?.blocked === true,
1567
+ failureDiagnostic: result?.failureDiagnostic || null,
1568
+ artifacts,
1569
+ },
1570
+ artifactRoot: result?.artifactRoot || null,
1571
+ artifacts,
1572
+ artifactPaths: artifacts.map((artifact) => artifact.path).filter(Boolean),
1573
+ artifactRetrieveCommands: artifacts
1574
+ .map((artifact) => artifact.retrieveCommand)
1575
+ .filter(Boolean),
1576
+ };
1577
+ }
1578
+
1579
+ function resolveWorkflowCommandLane({ nodeType, commandType, command, engine }) {
1580
+ const scheduler = engine?.services?.scheduler;
1581
+ if (scheduler && typeof scheduler.selectWorkflowLane === "function") {
1582
+ return scheduler.selectWorkflowLane({ nodeType, commandType, command });
1583
+ }
1584
+
1585
+ const normalizedNodeType = String(nodeType || "").trim();
1586
+ if (['validation.tests', 'validation.build', 'validation.lint'].includes(normalizedNodeType)) {
1587
+ return { lane: "isolated", reason: `workflow_node:${normalizedNodeType}`, heavy: true };
1588
+ }
1589
+
1590
+ const normalizedCommandType = String(commandType || "").trim();
1591
+ if (["test", "build", "qualityGate"].includes(normalizedCommandType)) {
1592
+ return { lane: "isolated", reason: `command_type:${normalizedCommandType}`, heavy: true };
1593
+ }
1594
+
1595
+ const rawCommand = String(command || "");
1596
+ if (/(?:\b(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?test\b|\b(?:npm|pnpm|yarn|bun)\s+run\s+build\b|\b(?:npm|pnpm|yarn|bun)\s+run\s+lint\b|\bpre-?push\b|\bgit\s+diff\b|\bvitest\b|\bjest\b|\btsc\b)/i.test(rawCommand)) {
1597
+ return { lane: "isolated", reason: "command_pattern:default", heavy: true };
1598
+ }
1599
+
1600
+ return { lane: "main", reason: "lightweight", heavy: false };
1601
+ }
1602
+
1603
+ async function maybeRunWorkflowCommandInIsolation({
1604
+ node,
1605
+ ctx,
1606
+ engine,
1607
+ nodeType,
1608
+ command,
1609
+ args = [],
1610
+ cwd,
1611
+ timeoutMs,
1612
+ env = {},
1613
+ commandType = "",
1614
+ sessionType = "flow",
1615
+ } = {}) {
1616
+ const lane = resolveWorkflowCommandLane({ nodeType, commandType, command, engine });
1617
+ if (lane?.lane !== "isolated") return null;
1618
+
1619
+ ctx.log(node.id, `Offloading ${commandType || nodeType} to isolated runner (${lane.reason})`);
1620
+ const runner = engine?.services?.isolatedRunner;
1621
+ const execute = typeof runner?.run === "function" ? runner.run.bind(runner) : runInIsolatedRunner;
1622
+ const isolated = await execute({
1623
+ command,
1624
+ args,
1625
+ cwd,
1626
+ timeoutMs,
1627
+ env,
1628
+ requestType: commandType || nodeType,
1629
+ taskId: `${node.id || nodeType || "validation"}`,
1630
+ metadata: {
1631
+ nodeId: node.id || null,
1632
+ nodeType,
1633
+ commandType,
1634
+ },
1635
+ });
1636
+ const compacted = await compactWorkflowCommandResult({
1637
+ command,
1638
+ args,
1639
+ output: isolated?.stdout || "",
1640
+ stderr: isolated?.stderr || isolated?.error || "",
1641
+ exitCode: isolated?.exitCode,
1642
+ durationMs: isolated?.duration,
1643
+ sessionType,
1644
+ });
1645
+
1646
+ return {
1647
+ lane,
1648
+ isolated,
1649
+ compacted,
1650
+ extras: buildIsolatedRunnerResultExtras(isolated, lane),
1651
+ };
1652
+ }
1653
+
1451
1654
  function resolveWorkflowNodeValue(value, ctx) {
1452
1655
  if (typeof value === "string") {
1453
1656
  const resolved = ctx.resolve(value);
@@ -1872,7 +2075,7 @@ registerBuiltinNodeType("trigger.manual", {
1872
2075
  type: "object",
1873
2076
  properties: {},
1874
2077
  },
1875
- async execute(node, ctx) {
2078
+ async execute(node, ctx, engine) {
1876
2079
  ctx.log(node.id, "Manual trigger fired");
1877
2080
  return { triggered: true, reason: "manual" };
1878
2081
  },
@@ -1892,7 +2095,7 @@ registerBuiltinNodeType("trigger.task_low", {
1892
2095
  countDraftTasks: { type: "boolean", default: false, description: "Also count draft tasks toward threshold" },
1893
2096
  },
1894
2097
  },
1895
- async execute(node, ctx) {
2098
+ async execute(node, ctx, engine) {
1896
2099
  const threshold = node.config?.threshold ?? 3;
1897
2100
  const status = node.config?.status ?? "todo";
1898
2101
  const countDrafts = node.config?.countDraftTasks === true;
@@ -1953,7 +2156,7 @@ registerBuiltinNodeType("trigger.schedule", {
1953
2156
  cron: { type: "string", description: "Cron expression (future support)" },
1954
2157
  },
1955
2158
  },
1956
- async execute(node, ctx) {
2159
+ async execute(node, ctx, engine) {
1957
2160
  const interval = node.config?.intervalMs ?? 3600000;
1958
2161
  const lastRun = ctx.data?._lastRunAt ?? 0;
1959
2162
  const elapsed = Date.now() - lastRun;
@@ -1972,7 +2175,7 @@ registerBuiltinNodeType("trigger.event", {
1972
2175
  filter: { type: "string", description: "Optional filter expression" },
1973
2176
  },
1974
2177
  },
1975
- async execute(node, ctx) {
2178
+ async execute(node, ctx, engine) {
1976
2179
  const expected = node.config?.eventType;
1977
2180
  const actual = ctx.data?.eventType || ctx.eventType;
1978
2181
  const triggered = expected === actual;
@@ -2018,7 +2221,7 @@ registerBuiltinNodeType("trigger.meeting.wake_phrase", {
2018
2221
  },
2019
2222
  },
2020
2223
  },
2021
- async execute(node, ctx) {
2224
+ async execute(node, ctx, engine) {
2022
2225
  const eventData = ctx.data && typeof ctx.data === "object" ? ctx.data : {};
2023
2226
  const resolveValue = (value) => (
2024
2227
  typeof ctx?.resolve === "function" ? ctx.resolve(value) : value
@@ -2156,7 +2359,7 @@ registerBuiltinNodeType("trigger.webhook", {
2156
2359
  method: { type: "string", default: "POST", enum: ["GET", "POST"] },
2157
2360
  },
2158
2361
  },
2159
- async execute(node, ctx) {
2362
+ async execute(node, ctx, engine) {
2160
2363
  return { triggered: true, payload: ctx.data?.webhookPayload || {} };
2161
2364
  },
2162
2365
  });
@@ -2175,7 +2378,7 @@ registerBuiltinNodeType("trigger.pr_event", {
2175
2378
  branchPattern: { type: "string", description: "Branch name regex filter" },
2176
2379
  },
2177
2380
  },
2178
- async execute(node, ctx) {
2381
+ async execute(node, ctx, engine) {
2179
2382
  const expectedEvents = [
2180
2383
  node.config?.event,
2181
2384
  ...(Array.isArray(node.config?.events) ? node.config.events : []),
@@ -2208,7 +2411,7 @@ registerBuiltinNodeType("trigger.task_assigned", {
2208
2411
  filter: { type: "string", description: "JS expression filter (e.g., \"task.tags?.includes('backend')\")" },
2209
2412
  },
2210
2413
  },
2211
- async execute(node, ctx) {
2414
+ async execute(node, ctx, engine) {
2212
2415
  const triggered = evaluateTaskAssignedTriggerConfig(node.config, ctx.data);
2213
2416
  return { triggered, task: ctx.data };
2214
2417
  },
@@ -2224,7 +2427,7 @@ registerBuiltinNodeType("trigger.anomaly", {
2224
2427
  agentFilter: { type: "string", description: "Regex to match agent ID or name" },
2225
2428
  },
2226
2429
  },
2227
- async execute(node, ctx) {
2430
+ async execute(node, ctx, engine) {
2228
2431
  const expected = node.config?.anomalyType;
2229
2432
  const actual = ctx.data?.anomalyType || ctx.data?.type;
2230
2433
  const typeMatch = !expected || expected === actual;
@@ -2264,7 +2467,7 @@ registerBuiltinNodeType("trigger.scheduled_once", {
2264
2467
  },
2265
2468
  required: ["runAt"],
2266
2469
  },
2267
- async execute(node, ctx) {
2470
+ async execute(node, ctx, engine) {
2268
2471
  const rawRunAt = ctx.resolve(node.config?.runAt || "");
2269
2472
  let runAtMs;
2270
2473
 
@@ -2315,7 +2518,7 @@ registerBuiltinNodeType("trigger.workflow_call", {
2315
2518
  },
2316
2519
  },
2317
2520
  },
2318
- async execute(node, ctx) {
2521
+ async execute(node, ctx, engine) {
2319
2522
  // Validate required inputs from _triggerVars or context data
2320
2523
  const inputDefs = node.config?.inputs || {};
2321
2524
  const callerVars = ctx.data?._triggerVars || ctx.data || {};
@@ -2365,7 +2568,7 @@ registerBuiltinNodeType("condition.expression", {
2365
2568
  },
2366
2569
  required: ["expression"],
2367
2570
  },
2368
- async execute(node, ctx) {
2571
+ async execute(node, ctx, engine) {
2369
2572
  const expr = node.config?.expression;
2370
2573
  if (!expr) throw new Error("Expression is required");
2371
2574
  try {
@@ -2391,7 +2594,7 @@ registerBuiltinNodeType("condition.task_has_tag", {
2391
2594
  },
2392
2595
  required: ["tag"],
2393
2596
  },
2394
- async execute(node, ctx) {
2597
+ async execute(node, ctx, engine) {
2395
2598
  const tag = node.config?.tag?.toLowerCase();
2396
2599
  const field = node.config?.field || "tags";
2397
2600
  let haystack = ctx.data?.task?.[field] || ctx.data?.[field] || "";
@@ -2412,7 +2615,7 @@ registerBuiltinNodeType("condition.file_exists", {
2412
2615
  },
2413
2616
  required: ["path"],
2414
2617
  },
2415
- async execute(node, ctx) {
2618
+ async execute(node, ctx, engine) {
2416
2619
  const filePath = ctx.resolve(node.config?.path || "");
2417
2620
  const exists = existsSync(filePath);
2418
2621
  ctx.log(node.id, `File check: "${filePath}" → ${exists}`);
@@ -2436,7 +2639,7 @@ registerBuiltinNodeType("condition.switch", {
2436
2639
  },
2437
2640
  required: [],
2438
2641
  },
2439
- async execute(node, ctx) {
2642
+ async execute(node, ctx, engine) {
2440
2643
  let value;
2441
2644
  const expr = node.config?.value || node.config?.expression || "";
2442
2645
  if (expr) {
@@ -2615,10 +2818,12 @@ registerBuiltinNodeType("action.run_agent", {
2615
2818
  );
2616
2819
  }
2617
2820
  let finalPrompt = prompt;
2821
+ const promptHasRepoMapContext = hasRepoMapContext(finalPrompt);
2618
2822
  const architectEditorFrame = buildArchitectEditorFrame({
2619
2823
  executionRole: ctx.resolve(node.config?.executionRole || ""),
2620
2824
  architectPlan,
2621
2825
  planSummary: architectPlan,
2826
+ includeRepoMap: !promptHasRepoMapContext,
2622
2827
  repoMap: node.config?.repoMap || ctx.data?.repoMap || null,
2623
2828
  repoMapFileLimit: node.config?.repoMapFileLimit,
2624
2829
  repoMapQuery: ctx.resolve(node.config?.repoMapQuery || ""),
@@ -3343,7 +3548,7 @@ registerBuiltinNodeType("action.run_command", {
3343
3548
  },
3344
3549
  required: ["command"],
3345
3550
  },
3346
- async execute(node, ctx) {
3551
+ async execute(node, ctx, engine) {
3347
3552
  const resolvedCommand = ctx.resolve(node.config?.command || "");
3348
3553
  const cwd = ctx.resolve(node.config?.cwd || ctx.data?.worktreePath || process.cwd());
3349
3554
  const commandType = typeof node.config?.commandType === "string" ? node.config.commandType.trim() : "";
@@ -3409,7 +3614,43 @@ registerBuiltinNodeType("action.run_command", {
3409
3614
  if (command !== autoResolvedCommand) {
3410
3615
  ctx.log(node.id, `Normalized legacy command for portability: ${command}`);
3411
3616
  }
3412
- ctx.log(node.id, `Running: ${usedArgv ? `${command} ${commandArgs.join(" ")}`.trim() : command}`);
3617
+ const displayCommand = usedArgv ? `${command} ${commandArgs.join(" ")}`.trim() : command;
3618
+ ctx.log(node.id, `Running: ${displayCommand}`);
3619
+ const isolatedRun = await maybeRunWorkflowCommandInIsolation({
3620
+ node,
3621
+ ctx,
3622
+ engine,
3623
+ nodeType: "action.run_command",
3624
+ command,
3625
+ args: commandArgs,
3626
+ cwd,
3627
+ timeoutMs: timeout,
3628
+ env: commandEnv,
3629
+ commandType,
3630
+ });
3631
+ if (isolatedRun) {
3632
+ if (shouldParseJson) {
3633
+ const parsedOutput = parseOutput(isolatedRun.isolated?.stdout || isolatedRun.compacted.output || "");
3634
+ return {
3635
+ success: isolatedRun.isolated?.blocked !== true && Number(isolatedRun.isolated?.exitCode ?? 0) === 0,
3636
+ exitCode: isolatedRun.isolated?.exitCode ?? null,
3637
+ output: parsedOutput,
3638
+ ...isolatedRun.extras,
3639
+ };
3640
+ }
3641
+ const result = {
3642
+ success: isolatedRun.isolated?.blocked !== true && Number(isolatedRun.isolated?.exitCode ?? 0) === 0,
3643
+ exitCode: isolatedRun.isolated?.exitCode ?? null,
3644
+ blocked: isolatedRun.isolated?.blocked === true,
3645
+ error: isolatedRun.isolated?.error || null,
3646
+ ...isolatedRun.compacted,
3647
+ ...isolatedRun.extras,
3648
+ };
3649
+ if (node.config?.failOnError && !result.success) {
3650
+ throw new Error(trimLogText(result.output || result.error || "command failed", 400));
3651
+ }
3652
+ return result;
3653
+ }
3413
3654
  const startedAt = Date.now();
3414
3655
  try {
3415
3656
  const output = usedArgv
@@ -4594,7 +4835,7 @@ registerBuiltinNodeType("action.git_operations", {
4594
4835
  },
4595
4836
  required: [],
4596
4837
  },
4597
- async execute(node, ctx) {
4838
+ async execute(node, ctx, engine) {
4598
4839
  const cwd = ctx.resolve(node.config?.cwd || ctx.data?.worktreePath || process.cwd());
4599
4840
  const resolveOpCommand = (opConfig = {}) => {
4600
4841
  const op = String(opConfig.op || opConfig.operation || "").trim();
@@ -4736,7 +4977,7 @@ registerBuiltinNodeType("action.create_pr", {
4736
4977
  },
4737
4978
  required: ["title"],
4738
4979
  },
4739
- async execute(node, ctx) {
4980
+ async execute(node, ctx, engine) {
4740
4981
  const title = ctx.resolve(node.config?.title || "");
4741
4982
  const body = ctx.resolve(node.config?.body || "");
4742
4983
  const baseInput = ctx.resolve(node.config?.base || node.config?.baseBranch || "main");
@@ -5079,7 +5320,7 @@ registerBuiltinNodeType("action.write_file", {
5079
5320
  },
5080
5321
  required: ["path", "content"],
5081
5322
  },
5082
- async execute(node, ctx) {
5323
+ async execute(node, ctx, engine) {
5083
5324
  const filePath = ctx.resolve(node.config?.path || "");
5084
5325
  const content = ctx.resolve(node.config?.content || "");
5085
5326
  if (node.config?.mkdir) {
@@ -5105,7 +5346,7 @@ registerBuiltinNodeType("action.read_file", {
5105
5346
  },
5106
5347
  required: ["path"],
5107
5348
  },
5108
- async execute(node, ctx) {
5349
+ async execute(node, ctx, engine) {
5109
5350
  const filePath = ctx.resolve(node.config?.path || "");
5110
5351
  if (!existsSync(filePath)) {
5111
5352
  return { success: false, error: `File not found: ${filePath}` };
@@ -5126,7 +5367,7 @@ registerBuiltinNodeType("action.set_variable", {
5126
5367
  },
5127
5368
  required: ["key"],
5128
5369
  },
5129
- async execute(node, ctx) {
5370
+ async execute(node, ctx, engine) {
5130
5371
  const key = node.config?.key;
5131
5372
  let value = node.config?.value || "";
5132
5373
  if (node.config?.isExpression) {
@@ -5161,7 +5402,7 @@ registerBuiltinNodeType("action.delay", {
5161
5402
  message: { type: "string", description: "Legacy alias for reason" },
5162
5403
  },
5163
5404
  },
5164
- async execute(node, ctx) {
5405
+ async execute(node, ctx, engine) {
5165
5406
  const baseMs = Number(
5166
5407
  node.config?.ms ??
5167
5408
  node.config?.delayMs ??
@@ -5218,7 +5459,7 @@ registerBuiltinNodeType("validation.screenshot", {
5218
5459
  },
5219
5460
  required: ["url"],
5220
5461
  },
5221
- async execute(node, ctx) {
5462
+ async execute(node, ctx, engine) {
5222
5463
  const url = ctx.resolve(node.config?.url || "http://localhost:3000");
5223
5464
  const outDir = ctx.resolve(node.config?.outputDir || ".bosun/evidence");
5224
5465
  const filename = ctx.resolve(node.config?.filename || `screenshot-${Date.now()}.png`);
@@ -5441,12 +5682,42 @@ registerBuiltinNodeType("validation.tests", {
5441
5682
  requiredPassRate: { type: "number", default: 1.0, description: "Minimum pass rate (0-1)" },
5442
5683
  },
5443
5684
  },
5444
- async execute(node, ctx) {
5685
+ async execute(node, ctx, engine) {
5445
5686
  const command = ctx.resolve(node.config?.command || "npm test");
5446
5687
  const cwd = ctx.resolve(node.config?.cwd || ctx.data?.worktreePath || process.cwd());
5447
5688
  const timeout = node.config?.timeoutMs || 600000;
5448
5689
 
5449
5690
  ctx.log(node.id, `Running tests: ${command}`);
5691
+ const isolatedRun = await maybeRunWorkflowCommandInIsolation({
5692
+ node,
5693
+ ctx,
5694
+ engine,
5695
+ nodeType: "validation.tests",
5696
+ command,
5697
+ cwd,
5698
+ timeoutMs: timeout,
5699
+ commandType: "test",
5700
+ });
5701
+ if (isolatedRun) {
5702
+ const failureDiagnostic = buildValidationFailureDiagnostic({
5703
+ command,
5704
+ status: isolatedRun.isolated?.status,
5705
+ exitCode: isolatedRun.isolated?.exitCode,
5706
+ stderr: isolatedRun.isolated?.stderr || isolatedRun.isolated?.error || "",
5707
+ output: isolatedRun.isolated?.stdout || "",
5708
+ timeoutMs: timeout,
5709
+ blocked: isolatedRun.isolated?.blocked === true,
5710
+ failureDiagnostic: isolatedRun.isolated?.failureDiagnostic,
5711
+ });
5712
+ return buildValidationResult({
5713
+ passed: didValidationCommandPass({ ...isolatedRun.isolated, failureDiagnostic }),
5714
+ exitCode: isolatedRun.isolated?.exitCode ?? null,
5715
+ blocked: isolatedRun.isolated?.blocked === true,
5716
+ compacted: isolatedRun.compacted,
5717
+ extras: isolatedRun.extras,
5718
+ failureDiagnostic,
5719
+ });
5720
+ }
5450
5721
  const startedAt = Date.now();
5451
5722
  try {
5452
5723
  const output = execSync(command, { cwd, timeout, encoding: "utf8", stdio: "pipe" });
@@ -5467,7 +5738,20 @@ registerBuiltinNodeType("validation.tests", {
5467
5738
  exitCode: err.status,
5468
5739
  durationMs: Date.now() - startedAt,
5469
5740
  });
5470
- return { passed: false, exitCode: err.status, ...compacted };
5741
+ const failureDiagnostic = buildValidationFailureDiagnostic({
5742
+ command,
5743
+ status: /(?:timed out|ETIMEDOUT|SIGTERM)/i.test(String(err?.message || "")) ? "timeout" : "error",
5744
+ exitCode: err.status,
5745
+ stderr: err.stderr?.toString() || err.message,
5746
+ output,
5747
+ timeoutMs: timeout,
5748
+ });
5749
+ return buildValidationResult({
5750
+ passed: false,
5751
+ exitCode: err.status,
5752
+ compacted,
5753
+ failureDiagnostic,
5754
+ });
5471
5755
  }
5472
5756
  },
5473
5757
  });
@@ -5483,7 +5767,7 @@ registerBuiltinNodeType("validation.build", {
5483
5767
  zeroWarnings: { type: "boolean", default: false, description: "Fail on warnings too" },
5484
5768
  },
5485
5769
  },
5486
- async execute(node, ctx) {
5770
+ async execute(node, ctx, engine) {
5487
5771
  const resolvedCommand = ctx.resolve(node.config?.command || "npm run build");
5488
5772
  const command = normalizeLegacyWorkflowCommand(resolvedCommand);
5489
5773
  const cwd = ctx.resolve(node.config?.cwd || ctx.data?.worktreePath || process.cwd());
@@ -5493,6 +5777,48 @@ registerBuiltinNodeType("validation.build", {
5493
5777
  ctx.log(node.id, `Normalized legacy command for portability: ${command}`);
5494
5778
  }
5495
5779
  ctx.log(node.id, `Building: ${command}`);
5780
+ const isolatedRun = await maybeRunWorkflowCommandInIsolation({
5781
+ node,
5782
+ ctx,
5783
+ engine,
5784
+ nodeType: "validation.build",
5785
+ command,
5786
+ cwd,
5787
+ timeoutMs: timeout,
5788
+ commandType: "build",
5789
+ });
5790
+ if (isolatedRun) {
5791
+ const combinedOutput = `${isolatedRun.isolated?.stdout || ""}\n${isolatedRun.isolated?.stderr || ""}`;
5792
+ const hasWarnings = /warning/i.test(combinedOutput);
5793
+ if (node.config?.zeroWarnings && hasWarnings) {
5794
+ return {
5795
+ passed: false,
5796
+ reason: "warnings_found",
5797
+ exitCode: isolatedRun.isolated?.exitCode ?? 0,
5798
+ blocked: isolatedRun.isolated?.blocked === true,
5799
+ ...isolatedRun.compacted,
5800
+ ...isolatedRun.extras,
5801
+ };
5802
+ }
5803
+ const failureDiagnostic = buildValidationFailureDiagnostic({
5804
+ command,
5805
+ status: isolatedRun.isolated?.status,
5806
+ exitCode: isolatedRun.isolated?.exitCode,
5807
+ stderr: isolatedRun.isolated?.stderr || isolatedRun.isolated?.error || "",
5808
+ output: isolatedRun.isolated?.stdout || "",
5809
+ timeoutMs: timeout,
5810
+ blocked: isolatedRun.isolated?.blocked === true,
5811
+ failureDiagnostic: isolatedRun.isolated?.failureDiagnostic,
5812
+ });
5813
+ return buildValidationResult({
5814
+ passed: didValidationCommandPass({ ...isolatedRun.isolated, failureDiagnostic }),
5815
+ exitCode: isolatedRun.isolated?.exitCode ?? null,
5816
+ blocked: isolatedRun.isolated?.blocked === true,
5817
+ compacted: isolatedRun.compacted,
5818
+ extras: isolatedRun.extras,
5819
+ failureDiagnostic,
5820
+ });
5821
+ }
5496
5822
  const startedAt = Date.now();
5497
5823
  try {
5498
5824
  const output = execSync(command, { cwd, timeout, encoding: "utf8", stdio: "pipe" });
@@ -5515,7 +5841,20 @@ registerBuiltinNodeType("validation.build", {
5515
5841
  exitCode: err.status,
5516
5842
  durationMs: Date.now() - startedAt,
5517
5843
  });
5518
- return { passed: false, exitCode: err.status, ...compacted };
5844
+ const failureDiagnostic = buildValidationFailureDiagnostic({
5845
+ command,
5846
+ status: /(?:timed out|ETIMEDOUT|SIGTERM)/i.test(String(err?.message || "")) ? "timeout" : "error",
5847
+ exitCode: err.status,
5848
+ stderr: err.stderr?.toString() || err.message,
5849
+ output: err.stdout?.toString() || "",
5850
+ timeoutMs: timeout,
5851
+ });
5852
+ return buildValidationResult({
5853
+ passed: false,
5854
+ exitCode: err.status,
5855
+ compacted,
5856
+ failureDiagnostic,
5857
+ });
5519
5858
  }
5520
5859
  },
5521
5860
  });
@@ -5530,15 +5869,46 @@ registerBuiltinNodeType("validation.lint", {
5530
5869
  timeoutMs: { type: "number", default: 120000 },
5531
5870
  },
5532
5871
  },
5533
- async execute(node, ctx) {
5872
+ async execute(node, ctx, engine) {
5534
5873
  const command = ctx.resolve(node.config?.command || "npm run lint");
5535
5874
  if (!command || !command.trim()) {
5536
5875
  return { passed: true, output: "no lint configured", skipped: true };
5537
5876
  }
5538
5877
  const cwd = ctx.resolve(node.config?.cwd || ctx.data?.worktreePath || process.cwd());
5878
+ const timeout = node.config?.timeoutMs || 120000;
5879
+ const isolatedRun = await maybeRunWorkflowCommandInIsolation({
5880
+ node,
5881
+ ctx,
5882
+ engine,
5883
+ nodeType: "validation.lint",
5884
+ command,
5885
+ cwd,
5886
+ timeoutMs: timeout,
5887
+ commandType: "qualityGate",
5888
+ });
5889
+ if (isolatedRun) {
5890
+ const failureDiagnostic = buildValidationFailureDiagnostic({
5891
+ command,
5892
+ status: isolatedRun.isolated?.status,
5893
+ exitCode: isolatedRun.isolated?.exitCode,
5894
+ stderr: isolatedRun.isolated?.stderr || isolatedRun.isolated?.error || "",
5895
+ output: isolatedRun.isolated?.stdout || "",
5896
+ timeoutMs: timeout,
5897
+ blocked: isolatedRun.isolated?.blocked === true,
5898
+ failureDiagnostic: isolatedRun.isolated?.failureDiagnostic,
5899
+ });
5900
+ return buildValidationResult({
5901
+ passed: didValidationCommandPass({ ...isolatedRun.isolated, failureDiagnostic }),
5902
+ exitCode: isolatedRun.isolated?.exitCode ?? null,
5903
+ blocked: isolatedRun.isolated?.blocked === true,
5904
+ compacted: isolatedRun.compacted,
5905
+ extras: isolatedRun.extras,
5906
+ failureDiagnostic,
5907
+ });
5908
+ }
5539
5909
  const startedAt = Date.now();
5540
5910
  try {
5541
- const output = execSync(command, { cwd, timeout: node.config?.timeoutMs || 120000, encoding: "utf8", stdio: "pipe" });
5911
+ const output = execSync(command, { cwd, timeout, encoding: "utf8", stdio: "pipe" });
5542
5912
  const compacted = await compactWorkflowCommandResult({
5543
5913
  command,
5544
5914
  output: output?.trim() || "",
@@ -5554,7 +5924,20 @@ registerBuiltinNodeType("validation.lint", {
5554
5924
  exitCode: err.status,
5555
5925
  durationMs: Date.now() - startedAt,
5556
5926
  });
5557
- return { passed: false, ...compacted };
5927
+ const failureDiagnostic = buildValidationFailureDiagnostic({
5928
+ command,
5929
+ status: /(?:timed out|ETIMEDOUT|SIGTERM)/i.test(String(err?.message || "")) ? "timeout" : "error",
5930
+ exitCode: err.status,
5931
+ stderr: err.stderr?.toString() || err.message,
5932
+ output: err.stdout?.toString() || "",
5933
+ timeoutMs: timeout,
5934
+ });
5935
+ return buildValidationResult({
5936
+ passed: false,
5937
+ exitCode: err.status,
5938
+ compacted,
5939
+ failureDiagnostic,
5940
+ });
5558
5941
  }
5559
5942
  },
5560
5943
  });
@@ -5572,7 +5955,7 @@ registerBuiltinNodeType("transform.json_parse", {
5572
5955
  field: { type: "string", description: "Field in source output containing JSON" },
5573
5956
  },
5574
5957
  },
5575
- async execute(node, ctx) {
5958
+ async execute(node, ctx, engine) {
5576
5959
  const sourceId = node.config?.input;
5577
5960
  const field = node.config?.field || "output";
5578
5961
  let raw = sourceId ? ctx.getNodeOutput(sourceId)?.[field] : ctx.resolve(node.config?.value || "");
@@ -5594,7 +5977,7 @@ registerBuiltinNodeType("transform.template", {
5594
5977
  },
5595
5978
  required: ["template"],
5596
5979
  },
5597
- async execute(node, ctx) {
5980
+ async execute(node, ctx, engine) {
5598
5981
  const result = ctx.resolve(node.config?.template || "");
5599
5982
  return { text: result };
5600
5983
  },
@@ -5608,7 +5991,7 @@ registerBuiltinNodeType("transform.aggregate", {
5608
5991
  sources: { type: "array", items: { type: "string" }, description: "Node IDs to aggregate" },
5609
5992
  },
5610
5993
  },
5611
- async execute(node, ctx) {
5994
+ async execute(node, ctx, engine) {
5612
5995
  const sources = node.config?.sources || [];
5613
5996
  const aggregated = {};
5614
5997
  for (const src of sources) {
@@ -5662,7 +6045,7 @@ registerBuiltinNodeType("transform.llm_parse", {
5662
6045
  },
5663
6046
  required: [],
5664
6047
  },
5665
- async execute(node, ctx) {
6048
+ async execute(node, ctx, engine) {
5666
6049
  // Resolve the input text
5667
6050
  let text = "";
5668
6051
  const inputRef = ctx.resolve(node.config?.input || "");
@@ -5739,7 +6122,7 @@ registerBuiltinNodeType("notify.log", {
5739
6122
  },
5740
6123
  required: ["message"],
5741
6124
  },
5742
- async execute(node, ctx) {
6125
+ async execute(node, ctx, engine) {
5743
6126
  const message = ctx.resolve(node.config?.message || "");
5744
6127
  const level = node.config?.level || "info";
5745
6128
  ctx.log(node.id, message, level);
@@ -5793,7 +6176,7 @@ registerBuiltinNodeType("notify.webhook_out", {
5793
6176
  },
5794
6177
  required: ["url"],
5795
6178
  },
5796
- async execute(node, ctx) {
6179
+ async execute(node, ctx, engine) {
5797
6180
  const url = ctx.resolve(node.config?.url || "");
5798
6181
  const method = node.config?.method || "POST";
5799
6182
  const body = node.config?.body ? JSON.stringify(node.config.body) : undefined;
@@ -5954,7 +6337,7 @@ registerBuiltinNodeType("agent.select_profile", {
5954
6337
  default: { type: "string", default: "general", description: "Default profile if no match" },
5955
6338
  },
5956
6339
  },
5957
- async execute(node, ctx) {
6340
+ async execute(node, ctx, engine) {
5958
6341
  const profiles = node.config?.profiles || {};
5959
6342
  const taskTitle = (ctx.data?.taskTitle || "").toLowerCase();
5960
6343
  const taskTags = (ctx.data?.taskTags || []).map((t) => t.toLowerCase());
@@ -7169,6 +7552,9 @@ registerBuiltinNodeType("agent.run_planner", {
7169
7552
  context: { type: "string", description: "Additional context for the planner" },
7170
7553
  prompt: { type: "string", description: "Optional explicit planner prompt override" },
7171
7554
  outputVariable: { type: "string", description: "Optional context key to store planner output text" },
7555
+ repoMap: { type: "object", description: "Optional explicit repo map context" },
7556
+ repoMapQuery: { type: "string", description: "Optional query used to select a compact repo topology" },
7557
+ repoMapFileLimit: { type: "number", default: 8, description: "Maximum repo-map files to include" },
7172
7558
  projectId: { type: "string" },
7173
7559
  dedup: { type: "boolean", default: true },
7174
7560
  timeoutMs: { type: "number", default: 960000, description: "Node timeout in ms (recommended >= agentTimeoutMs)" },
@@ -7184,6 +7570,7 @@ registerBuiltinNodeType("agent.run_planner", {
7184
7570
  const plannerFeedback = resolvePlannerFeedbackContext(ctx.data?._plannerFeedback);
7185
7571
  const explicitPrompt = ctx.resolve(node.config?.prompt || "");
7186
7572
  const outputVariable = ctx.resolve(node.config?.outputVariable || "");
7573
+ const repoMapQuery = ctx.resolve(node.config?.repoMapQuery || "");
7187
7574
  const configuredNodeTimeout = Number(ctx.resolve(node.config?.timeoutMs || node.config?.timeout || 0));
7188
7575
  const configuredAgentTimeout = Number(ctx.resolve(node.config?.agentTimeoutMs || 0));
7189
7576
 
@@ -7199,20 +7586,51 @@ registerBuiltinNodeType("agent.run_planner", {
7199
7586
  // This delegates to the existing planner prompt flow
7200
7587
  const agentPool = engine.services?.agentPool;
7201
7588
  const plannerPrompt = engine.services?.prompts?.planner;
7589
+ const basePrompt = explicitPrompt || plannerPrompt || "";
7590
+ const fullPromptForRepoMapCheck = [basePrompt, context, plannerFeedback].filter(Boolean).join("\n\n");
7591
+ const promptHasRepoMap = hasRepoMapContext(fullPromptForRepoMapCheck);
7592
+ const repoTopologyContext = (node.config?.repoMap || repoMapQuery)
7593
+ && !promptHasRepoMap
7594
+ ? buildRepoTopologyContext({
7595
+ repoMap: node.config?.repoMap || ctx.data?.repoMap || null,
7596
+ repoMapFileLimit: node.config?.repoMapFileLimit ?? 8,
7597
+ repoMapQuery,
7598
+ query: [context, explicitPrompt, plannerPrompt].filter(Boolean).join(" "),
7599
+ prompt: explicitPrompt || plannerPrompt || "",
7600
+ userMessage: context,
7601
+ taskTitle: ctx.data?.taskTitle || ctx.data?.task?.title || "",
7602
+ taskDescription:
7603
+ ctx.data?.taskDescription ||
7604
+ ctx.data?.task?.description ||
7605
+ ctx.data?.task?.body ||
7606
+ ctx.data?.taskDetail?.description ||
7607
+ ctx.data?.taskInfo?.description ||
7608
+ "",
7609
+ changedFiles:
7610
+ (Array.isArray(ctx.data?.changedFiles) ? ctx.data.changedFiles : null) ||
7611
+ (Array.isArray(ctx.data?.task?.changedFiles) ? ctx.data.task.changedFiles : null) ||
7612
+ [],
7613
+ cwd: process.cwd(),
7614
+ repoRoot: ctx.data?.repoRoot || process.cwd(),
7615
+ })
7616
+ : "";
7202
7617
  // Enforce strict output instructions to ensure the downstream materialize node
7203
7618
  // can parse the planner output. The planner prompt already defines the contract,
7204
7619
  // but we reinforce it here to prevent agents from wrapping output in prose.
7205
7620
  const outputEnforcement =
7206
7621
  `\n\n## CRITICAL OUTPUT REQUIREMENT\n` +
7207
7622
  `Generate exactly ${count} new tasks.\n` +
7208
- ((context || plannerFeedback)
7209
- ? `${[context, plannerFeedback ? `Planner feedback context:\n${plannerFeedback}` : ""].filter(Boolean).join("\n\n")}\n\n`
7623
+ ((context || plannerFeedback || repoTopologyContext)
7624
+ ? `${[
7625
+ context,
7626
+ plannerFeedback ? `Planner feedback context:\n${plannerFeedback}` : "",
7627
+ repoTopologyContext,
7628
+ ].filter(Boolean).join("\n\n")}\n\n`
7210
7629
  : "\n") +
7211
7630
  `Your response MUST be a single fenced JSON block with shape { "tasks": [...] }.\n` +
7212
7631
  `Do NOT include status updates, analysis notes, tool commentary, questions, or prose outside the JSON block.\n` +
7213
7632
  `Do NOT reference or use legacy ve-kanban integration commands or scripts.\n` +
7214
7633
  `The downstream system will parse your output as JSON — any extra text will cause task creation to fail.`;
7215
- const basePrompt = explicitPrompt || plannerPrompt || "";
7216
7634
  const promptText = basePrompt
7217
7635
  ? `${basePrompt}${outputEnforcement}`
7218
7636
  : "";
@@ -7312,7 +7730,7 @@ registerBuiltinNodeType("agent.evidence_collect", {
7312
7730
  types: { type: "array", items: { type: "string" }, default: ["png", "jpg", "json", "log", "txt"] },
7313
7731
  },
7314
7732
  },
7315
- async execute(node, ctx) {
7733
+ async execute(node, ctx, engine) {
7316
7734
  const dir = ctx.resolve(node.config?.evidenceDir || ".bosun/evidence");
7317
7735
  if (!existsSync(dir)) {
7318
7736
  mkdirSync(dir, { recursive: true });
@@ -7443,7 +7861,7 @@ registerBuiltinNodeType("flow.join", {
7443
7861
  },
7444
7862
  },
7445
7863
  },
7446
- async execute(node, ctx) {
7864
+ async execute(node, ctx, engine) {
7447
7865
  const mode = String(ctx.resolve(node.config?.mode || "all") || "all").toLowerCase();
7448
7866
  const includeSkipped = parseBooleanSetting(
7449
7867
  resolveWorkflowNodeValue(node.config?.includeSkipped ?? true, ctx),
@@ -7526,7 +7944,7 @@ registerBuiltinNodeType("flow.end", {
7526
7944
  },
7527
7945
  },
7528
7946
  },
7529
- async execute(node, ctx) {
7947
+ async execute(node, ctx, engine) {
7530
7948
  const rawStatus = String(ctx.resolve(node.config?.status || "completed") || "completed")
7531
7949
  .trim()
7532
7950
  .toLowerCase();
@@ -7620,6 +8038,54 @@ const UNIVERSAL_FLOW_NODE = {
7620
8038
  ...configuredInput,
7621
8039
  }, workflowId);
7622
8040
  const childRunOpts = makeChildWorkflowExecuteOptions(ctx, { childWorkflowId: workflowId });
8041
+ const trackedTaskId = String(
8042
+ ctx.data?.taskId ||
8043
+ ctx.data?.task?.id ||
8044
+ ctx.data?.taskDetail?.id ||
8045
+ ctx.data?.taskInfo?.id ||
8046
+ "",
8047
+ ).trim();
8048
+ const trackedTaskTitle = String(
8049
+ ctx.data?.taskTitle ||
8050
+ ctx.data?.task?.title ||
8051
+ ctx.data?.taskDetail?.title ||
8052
+ ctx.data?.taskInfo?.title ||
8053
+ trackedTaskId,
8054
+ ).trim();
8055
+ const tracker = trackedTaskId ? getSessionTracker() : null;
8056
+ if (tracker && trackedTaskId) {
8057
+ const existing = tracker.getSessionById(trackedTaskId);
8058
+ if (!existing) {
8059
+ tracker.createSession({
8060
+ id: trackedTaskId,
8061
+ type: "task",
8062
+ taskId: trackedTaskId,
8063
+ metadata: {
8064
+ title: trackedTaskTitle || trackedTaskId,
8065
+ workspaceId: String(ctx.data?.workspaceId || ctx.data?.activeWorkspace || "").trim() || undefined,
8066
+ workspaceDir: String(ctx.data?.worktreePath || ctx.data?.workspaceDir || "").trim() || undefined,
8067
+ branch:
8068
+ String(
8069
+ ctx.data?.branch ||
8070
+ ctx.data?.task?.branchName ||
8071
+ ctx.data?.taskDetail?.branchName ||
8072
+ ctx.data?.taskInfo?.branchName ||
8073
+ "",
8074
+ ).trim() || undefined,
8075
+ },
8076
+ });
8077
+ } else {
8078
+ tracker.updateSessionStatus(trackedTaskId, "active");
8079
+ if (trackedTaskTitle) tracker.renameSession(trackedTaskId, trackedTaskTitle);
8080
+ }
8081
+ tracker.recordEvent(trackedTaskId, {
8082
+ role: "system",
8083
+ type: "system",
8084
+ content: `Delegating to workflow "${workflowId}"`,
8085
+ timestamp: new Date().toISOString(),
8086
+ _sessionType: "task",
8087
+ });
8088
+ }
7623
8089
 
7624
8090
  if (mode === "dispatch") {
7625
8091
  ctx.log(node.id, `Dispatching universal workflow \"${workflowId}\"`);
@@ -7632,9 +8098,29 @@ const UNIVERSAL_FLOW_NODE = {
7632
8098
  dispatched
7633
8099
  .then((childCtx) => {
7634
8100
  const status = childCtx?.errors?.length ? "failed" : "completed";
8101
+ if (tracker && trackedTaskId) {
8102
+ tracker.updateSessionStatus(trackedTaskId, status);
8103
+ tracker.recordEvent(trackedTaskId, {
8104
+ role: status === "completed" ? "assistant" : "system",
8105
+ type: status === "completed" ? "agent_message" : "error",
8106
+ content: `Workflow "${workflowId}" ${status}`,
8107
+ timestamp: new Date().toISOString(),
8108
+ _sessionType: "task",
8109
+ });
8110
+ }
7635
8111
  ctx.log(node.id, `Dispatched universal workflow \"${workflowId}\" finished with status=${status}`);
7636
8112
  })
7637
8113
  .catch((err) => {
8114
+ if (tracker && trackedTaskId) {
8115
+ tracker.updateSessionStatus(trackedTaskId, "failed");
8116
+ tracker.recordEvent(trackedTaskId, {
8117
+ role: "system",
8118
+ type: "error",
8119
+ content: `Workflow "${workflowId}" failed: ${err.message}`,
8120
+ timestamp: new Date().toISOString(),
8121
+ _sessionType: "task",
8122
+ });
8123
+ }
7638
8124
  ctx.log(node.id, `Dispatched universal workflow \"${workflowId}\" failed: ${err.message}`, "error");
7639
8125
  });
7640
8126
 
@@ -7659,8 +8145,20 @@ const UNIVERSAL_FLOW_NODE = {
7659
8145
  workflowId,
7660
8146
  runId: childCtx?.id || null,
7661
8147
  status: errorCount > 0 ? "failed" : "completed",
8148
+ message: String(childCtx?.data?._workflowTerminalMessage || "").trim(),
8149
+ output: childCtx?.data?._workflowTerminalOutput,
7662
8150
  errorCount,
7663
8151
  };
8152
+ if (tracker && trackedTaskId) {
8153
+ tracker.updateSessionStatus(trackedTaskId, output.status);
8154
+ tracker.recordEvent(trackedTaskId, {
8155
+ role: output.status === "completed" ? "assistant" : "system",
8156
+ type: output.status === "completed" ? "agent_message" : "error",
8157
+ content: `Workflow "${workflowId}" ${output.status}${output.message ? `: ${output.message}` : ""}`,
8158
+ timestamp: new Date().toISOString(),
8159
+ _sessionType: "task",
8160
+ });
8161
+ }
7664
8162
  if (outputVariable) ctx.data[outputVariable] = output;
7665
8163
  return output;
7666
8164
  },
@@ -7975,12 +8473,30 @@ registerBuiltinNodeType("action.continue_session", {
7975
8473
  const prompt = ctx.resolve(node.config?.prompt || "Continue working on the current task.");
7976
8474
  const timeout = node.config?.timeoutMs || 1800000;
7977
8475
  const strategy = node.config?.strategy || "continue";
8476
+ const issueAdvisor =
8477
+ ctx.data?._issueAdvisor && typeof ctx.data._issueAdvisor === "object"
8478
+ ? ctx.data._issueAdvisor
8479
+ : null;
8480
+ const dagStateSummary =
8481
+ ctx.data?._plannerFeedback?.dagStateSummary && typeof ctx.data._plannerFeedback.dagStateSummary === "object"
8482
+ ? ctx.data._plannerFeedback.dagStateSummary
8483
+ : null;
8484
+ const continuationPrefix = issueAdvisor
8485
+ ? [
8486
+ "Issue-advisor continuation context:",
8487
+ `- Recommendation: ${issueAdvisor.recommendedAction || "continue"}`,
8488
+ issueAdvisor.summary ? `- Summary: ${issueAdvisor.summary}` : null,
8489
+ issueAdvisor.nextStepGuidance ? `- Guidance: ${issueAdvisor.nextStepGuidance}` : null,
8490
+ dagStateSummary?.counts ? `- DAG counts: completed=${Number(dagStateSummary.counts.completed ?? 0) || 0}, failed=${Number(dagStateSummary.counts.failed ?? 0) || 0}, pending=${Number(dagStateSummary.counts.pending ?? 0) || 0}` : null,
8491
+ ].filter(Boolean).join("\n") + "\n\n"
8492
+ : "";
8493
+ const enrichedPrompt = continuationPrefix ? `${continuationPrefix}${prompt}` : prompt;
7978
8494
 
7979
8495
  ctx.log(node.id, `Continuing session ${sessionId} (strategy: ${strategy})`);
7980
8496
 
7981
8497
  const agentPool = engine.services?.agentPool;
7982
8498
  if (agentPool?.continueSession) {
7983
- const result = await agentPool.continueSession(sessionId, prompt, { timeout, strategy });
8499
+ const result = await agentPool.continueSession(sessionId, enrichedPrompt, { timeout, strategy });
7984
8500
 
7985
8501
  // Propagate session ID for downstream chaining
7986
8502
  const threadId = result.threadId || sessionId;
@@ -7993,12 +8509,12 @@ registerBuiltinNodeType("action.continue_session", {
7993
8509
  // Fallback: use ephemeral thread with continuation context
7994
8510
  if (agentPool?.launchEphemeralThread) {
7995
8511
  const continuation = strategy === "retry"
7996
- ? `Start over on this task. Previous attempt failed.\n\n${prompt}`
8512
+ ? `Start over on this task. Previous attempt failed.\n\n${enrichedPrompt}`
7997
8513
  : strategy === "refine"
7998
- ? `Refine your previous work. Specifically:\n\n${prompt}`
8514
+ ? `Refine your previous work. Specifically:\n\n${enrichedPrompt}`
7999
8515
  : strategy === "finish_up"
8000
- ? `Wrap up the current task. Commit, push, and hand off PR lifecycle to Bosun. Ensure tests pass.\n\n${prompt}`
8001
- : `Continue where you left off.\n\n${prompt}`;
8516
+ ? `Wrap up the current task. Commit, push, and hand off PR lifecycle to Bosun. Ensure tests pass.\n\n${enrichedPrompt}`
8517
+ : `Continue where you left off.\n\n${enrichedPrompt}`;
8002
8518
 
8003
8519
  const result = await agentPool.launchEphemeralThread(continuation, ctx.data?.worktreePath || process.cwd(), timeout);
8004
8520
 
@@ -8089,7 +8605,7 @@ registerBuiltinNodeType("action.bosun_cli", {
8089
8605
  },
8090
8606
  required: ["subcommand"],
8091
8607
  },
8092
- async execute(node, ctx) {
8608
+ async execute(node, ctx, engine) {
8093
8609
  const sub = node.config?.subcommand || "";
8094
8610
  const args = ctx.resolve(node.config?.args || "");
8095
8611
  const cmd = `bosun ${sub} ${args}`.trim();
@@ -8218,7 +8734,7 @@ registerBuiltinNodeType("action.bosun_tool", {
8218
8734
  },
8219
8735
  required: ["toolId"],
8220
8736
  },
8221
- async execute(node, ctx) {
8737
+ async execute(node, ctx, engine) {
8222
8738
  const toolId = ctx.resolve(node.config?.toolId || "");
8223
8739
  if (!toolId) throw new Error("action.bosun_tool: 'toolId' is required");
8224
8740
 
@@ -8915,7 +9431,7 @@ registerBuiltinNodeType("action.handle_rate_limit", {
8915
9431
  strategy: { type: "string", enum: ["wait", "rotate", "skip"], default: "wait", description: "Rate limit strategy" },
8916
9432
  },
8917
9433
  },
8918
- async execute(node, ctx) {
9434
+ async execute(node, ctx, engine) {
8919
9435
  const attempt = ctx.data?._rateLimitAttempt || 0;
8920
9436
  const maxRetries = node.config?.maxRetries || 5;
8921
9437
  const strategy = node.config?.strategy || "wait";
@@ -9068,7 +9584,7 @@ registerBuiltinNodeType("action.refresh_worktree", {
9068
9584
  },
9069
9585
  required: ["operation"],
9070
9586
  },
9071
- async execute(node, ctx) {
9587
+ async execute(node, ctx, engine) {
9072
9588
  const op = node.config?.operation || "fetch";
9073
9589
  const cwd = ctx.resolve(node.config?.cwd || ctx.data?.worktreePath || process.cwd());
9074
9590
  const branch = ctx.resolve(node.config?.branch || "main");
@@ -9423,7 +9939,7 @@ registerBuiltinNodeType("action.mcp_tool_call", {
9423
9939
  },
9424
9940
  required: ["server", "tool"],
9425
9941
  },
9426
- async execute(node, ctx) {
9942
+ async execute(node, ctx, engine) {
9427
9943
  const serverId = ctx.resolve(node.config?.server || "");
9428
9944
  const toolName = ctx.resolve(node.config?.tool || "");
9429
9945
  const timeoutMs = node.config?.timeoutMs || 30000;
@@ -9518,7 +10034,7 @@ registerBuiltinNodeType("action.mcp_list_tools", {
9518
10034
  },
9519
10035
  required: ["server"],
9520
10036
  },
9521
- async execute(node, ctx) {
10037
+ async execute(node, ctx, engine) {
9522
10038
  const serverId = ctx.resolve(node.config?.server || "");
9523
10039
  const timeoutMs = node.config?.timeoutMs || 30000;
9524
10040
 
@@ -9645,7 +10161,7 @@ registerBuiltinNodeType("action.mcp_pipeline", {
9645
10161
  },
9646
10162
  required: ["steps"],
9647
10163
  },
9648
- async execute(node, ctx) {
10164
+ async execute(node, ctx, engine) {
9649
10165
  const adapter = await getMcpAdapter();
9650
10166
  const pipelineSpec = adapter.createPipelineSpec(node.config?.steps || []);
9651
10167
 
@@ -9850,7 +10366,7 @@ registerBuiltinNodeType("transform.mcp_extract", {
9850
10366
  },
9851
10367
  required: ["source", "fields"],
9852
10368
  },
9853
- async execute(node, ctx) {
10369
+ async execute(node, ctx, engine) {
9854
10370
  const sourceNodeId = ctx.resolve(node.config?.source || "");
9855
10371
  const sourceField = node.config?.sourceField || "data";
9856
10372
 
@@ -10942,6 +11458,8 @@ registerBuiltinNodeType("trigger.task_available", {
10942
11458
  return {
10943
11459
  triggered: true,
10944
11460
  tasks: toDispatch,
11461
+ task: primaryTask,
11462
+ taskTitle: primaryTask ? pickTaskString(primaryTask.title, primaryTask.task_title) : "",
10945
11463
  taskCount: toDispatch.length,
10946
11464
  availableSlots: remaining,
10947
11465
  selectedTaskId: primaryTask ? pickTaskString(primaryTask.id, primaryTask.task_id) : "",
@@ -10964,7 +11482,7 @@ registerBuiltinNodeType("condition.slot_available", {
10964
11482
  baseBranch: { type: "string", description: "Base branch to check against" },
10965
11483
  },
10966
11484
  },
10967
- async execute(node, ctx) {
11485
+ async execute(node, ctx, engine) {
10968
11486
  const maxParallel = node.config?.maxParallel ?? 3;
10969
11487
  const baseBranchLimit = node.config?.baseBranchLimit ?? 0;
10970
11488
  const activeSlotCount = ctx.data?.activeSlotCount ?? 0;
@@ -11002,7 +11520,7 @@ registerBuiltinNodeType("action.allocate_slot", {
11002
11520
  },
11003
11521
  required: ["taskId"],
11004
11522
  },
11005
- async execute(node, ctx) {
11523
+ async execute(node, ctx, engine) {
11006
11524
  const taskId = cfgOrCtx(node, ctx, "taskId");
11007
11525
  const taskTitle = cfgOrCtx(node, ctx, "taskTitle", "(untitled)");
11008
11526
  const branch = cfgOrCtx(node, ctx, "branch");
@@ -11056,7 +11574,7 @@ registerBuiltinNodeType("action.release_slot", {
11056
11574
  taskId: { type: "string", description: "Task ID whose slot to release" },
11057
11575
  },
11058
11576
  },
11059
- async execute(node, ctx) {
11577
+ async execute(node, ctx, engine) {
11060
11578
  const taskId = cfgOrCtx(node, ctx, "taskId");
11061
11579
  const slot = ctx.data?._allocatedSlot;
11062
11580
 
@@ -11100,7 +11618,7 @@ registerBuiltinNodeType("action.claim_task", {
11100
11618
  },
11101
11619
  required: ["taskId"],
11102
11620
  },
11103
- async execute(node, ctx) {
11621
+ async execute(node, ctx, engine) {
11104
11622
  const taskId = cfgOrCtx(node, ctx, "taskId");
11105
11623
  const taskTitle = cfgOrCtx(node, ctx, "taskTitle");
11106
11624
  const ttlMinutes = node.config?.ttlMinutes ?? 180;
@@ -11222,7 +11740,7 @@ registerBuiltinNodeType("action.release_claim", {
11222
11740
  instanceId: { type: "string", description: "Instance ID (auto-read from ctx)" },
11223
11741
  },
11224
11742
  },
11225
- async execute(node, ctx) {
11743
+ async execute(node, ctx, engine) {
11226
11744
  const taskId = cfgOrCtx(node, ctx, "taskId");
11227
11745
  const claimToken = cfgOrCtx(node, ctx, "claimToken") || ctx.data?._claimToken || "";
11228
11746
  const instanceId = cfgOrCtx(node, ctx, "instanceId") || ctx.data?._claimInstanceId || "";
@@ -11289,7 +11807,7 @@ registerBuiltinNodeType("action.resolve_executor", {
11289
11807
  modelOverride: { type: "string", description: "Force a specific model" },
11290
11808
  },
11291
11809
  },
11292
- async execute(node, ctx) {
11810
+ async execute(node, ctx, engine) {
11293
11811
  const defaultSdk = cfgOrCtx(node, ctx, "defaultSdk", "auto");
11294
11812
  const sdkOverride = cfgOrCtx(node, ctx, "sdkOverride");
11295
11813
  const modelOverride = cfgOrCtx(node, ctx, "modelOverride");
@@ -11530,7 +12048,7 @@ registerBuiltinNodeType("action.acquire_worktree", {
11530
12048
  },
11531
12049
  required: ["branch", "taskId"],
11532
12050
  },
11533
- async execute(node, ctx) {
12051
+ async execute(node, ctx, engine) {
11534
12052
  // Outer guard: ensure we ALWAYS return structured output with recoveryNote
11535
12053
  // so downstream {{acquire-worktree.recoveryNote}} templates never stay literal.
11536
12054
  let taskId, branch, repoRoot, baseBranch;
@@ -12067,7 +12585,7 @@ registerBuiltinNodeType("action.release_worktree", {
12067
12585
  removeTimeout: { type: "number", default: 30000, description: "Timeout for removal (ms)" },
12068
12586
  },
12069
12587
  },
12070
- async execute(node, ctx) {
12588
+ async execute(node, ctx, engine) {
12071
12589
  const worktreePath = cfgOrCtx(node, ctx, "worktreePath");
12072
12590
  const repoRoot = cfgOrCtx(node, ctx, "repoRoot") || process.cwd();
12073
12591
  const taskId = cfgOrCtx(node, ctx, "taskId");
@@ -12129,7 +12647,7 @@ registerBuiltinNodeType("action.recover_worktree", {
12129
12647
  removeTimeout: { type: "number", default: 30000, description: "Timeout for removal (ms)" },
12130
12648
  },
12131
12649
  },
12132
- async execute(node, ctx) {
12650
+ async execute(node, ctx, engine) {
12133
12651
  const worktreePath = cfgOrCtx(node, ctx, "worktreePath") || ctx.data?.worktreePath || "";
12134
12652
  const repoRoot = cfgOrCtx(node, ctx, "repoRoot") || process.cwd();
12135
12653
  const taskId = cfgOrCtx(node, ctx, "taskId") || ctx.data?.taskId || "";
@@ -12171,7 +12689,7 @@ registerBuiltinNodeType("action.sweep_task_worktrees", {
12171
12689
  timeout: { type: "number", default: 15000, description: "Timeout for git worktree prune (ms)" },
12172
12690
  },
12173
12691
  },
12174
- async execute(node, ctx) {
12692
+ async execute(node, ctx, engine) {
12175
12693
  const repoRoot = cfgOrCtx(node, ctx, "repoRoot") || process.cwd();
12176
12694
  const taskId = cfgOrCtx(node, ctx, "taskId") || ctx.data?.taskId || "";
12177
12695
  const timeout = Number(node.config?.timeout ?? 15000);
@@ -12208,7 +12726,7 @@ const readWorkflowContractHandler = {
12208
12726
  },
12209
12727
  },
12210
12728
  },
12211
- async execute(node, ctx) {
12729
+ async execute(node, ctx, engine) {
12212
12730
  const projectRoot = cfgOrCtx(node, ctx, "projectRoot")
12213
12731
  || cfgOrCtx(node, ctx, "worktreePath")
12214
12732
  || cfgOrCtx(node, ctx, "repoRoot")
@@ -12277,7 +12795,7 @@ const workflowContractValidationHandler = {
12277
12795
  },
12278
12796
  },
12279
12797
  },
12280
- async execute(node, ctx) {
12798
+ async execute(node, ctx, engine) {
12281
12799
  const projectRoot = cfgOrCtx(node, ctx, "projectRoot")
12282
12800
  || cfgOrCtx(node, ctx, "worktreePath")
12283
12801
  || cfgOrCtx(node, ctx, "repoRoot")
@@ -12379,7 +12897,7 @@ registerBuiltinNodeType("action.build_task_prompt", {
12379
12897
  },
12380
12898
  required: ["taskTitle"],
12381
12899
  },
12382
- async execute(node, ctx) {
12900
+ async execute(node, ctx, engine) {
12383
12901
  const taskId = cfgOrCtx(node, ctx, "taskId");
12384
12902
  const taskTitle = cfgOrCtx(node, ctx, "taskTitle");
12385
12903
  const taskDescription = cfgOrCtx(node, ctx, "taskDescription");
@@ -12396,6 +12914,14 @@ registerBuiltinNodeType("action.build_task_prompt", {
12396
12914
  const includeMemory = node.config?.includeMemory !== false;
12397
12915
  ctx.data._taskIncludeContext = includeComments;
12398
12916
  const customTemplate = cfgOrCtx(node, ctx, "promptTemplate");
12917
+ const workflowIssueAdvisor =
12918
+ ctx.data?._issueAdvisor && typeof ctx.data._issueAdvisor === "object"
12919
+ ? ctx.data._issueAdvisor
12920
+ : null;
12921
+ const workflowDagStateSummary =
12922
+ ctx.data?._plannerFeedback?.dagStateSummary && typeof ctx.data._plannerFeedback.dagStateSummary === "object"
12923
+ ? ctx.data._plannerFeedback.dagStateSummary
12924
+ : null;
12399
12925
  const taskPayload =
12400
12926
  ctx.data?.task && typeof ctx.data.task === "object"
12401
12927
  ? ctx.data.task
@@ -12716,6 +13242,18 @@ registerBuiltinNodeType("action.build_task_prompt", {
12716
13242
  userParts.push("");
12717
13243
  }
12718
13244
 
13245
+ if (workflowIssueAdvisor || workflowDagStateSummary) {
13246
+ userParts.push("## Workflow Continuation Context");
13247
+ if (workflowIssueAdvisor?.recommendedAction) userParts.push(`- **Issue Advisor Action:** ${workflowIssueAdvisor.recommendedAction}`);
13248
+ if (workflowIssueAdvisor?.summary) userParts.push(`- **Issue Advisor Summary:** ${workflowIssueAdvisor.summary}`);
13249
+ if (workflowIssueAdvisor?.nextStepGuidance) userParts.push(`- **Next-Step Guidance:** ${workflowIssueAdvisor.nextStepGuidance}`);
13250
+ if (workflowDagStateSummary?.counts) {
13251
+ userParts.push(`- **DAG Counts:** completed=${Number(workflowDagStateSummary.counts.completed ?? 0) || 0}, failed=${Number(workflowDagStateSummary.counts.failed ?? 0) || 0}, pending=${Number(workflowDagStateSummary.counts.pending ?? 0) || 0}`);
13252
+ }
13253
+ if (workflowDagStateSummary?.revisionCount !== undefined) userParts.push(`- **DAG Revisions:** ${workflowDagStateSummary.revisionCount}`);
13254
+ userParts.push("");
13255
+ }
13256
+
12719
13257
  if (includeComments) {
12720
13258
  const taskContextBlock = buildTaskContextBlock(taskPayload);
12721
13259
  if (taskContextBlock) {
@@ -13002,7 +13540,7 @@ registerBuiltinNodeType("action.persist_memory", {
13002
13540
  },
13003
13541
  required: ["content"],
13004
13542
  },
13005
- async execute(node, ctx) {
13543
+ async execute(node, ctx, engine) {
13006
13544
  const TASK_TEMPLATE_PLACEHOLDER_RE = /^\{\{\s*[\w.-]+\s*\}\}$/;
13007
13545
  const TASK_TEMPLATE_INLINE_PLACEHOLDER_RE = /\{\{\s*[\w.-]+\s*\}\}/g;
13008
13546
  const normalizeString = (value) => {
@@ -13198,7 +13736,7 @@ registerBuiltinNodeType("action.auto_commit_dirty", {
13198
13736
  },
13199
13737
  required: ["worktreePath"],
13200
13738
  },
13201
- async execute(node, ctx) {
13739
+ async execute(node, ctx, engine) {
13202
13740
  const worktreePath = cfgOrCtx(node, ctx, "worktreePath");
13203
13741
  const taskId = cfgOrCtx(node, ctx, "taskId") || ctx.data?.taskId || "unknown";
13204
13742
 
@@ -13270,7 +13808,7 @@ registerBuiltinNodeType("action.detect_new_commits", {
13270
13808
  baseBranch: { type: "string", description: "Base branch for diff stats" },
13271
13809
  },
13272
13810
  },
13273
- async execute(node, ctx) {
13811
+ async execute(node, ctx, engine) {
13274
13812
  const worktreePath = cfgOrCtx(node, ctx, "worktreePath");
13275
13813
  const baseBranch = cfgOrCtx(node, ctx, "baseBranch", "origin/main");
13276
13814
 
@@ -13407,7 +13945,7 @@ registerBuiltinNodeType("action.push_branch", {
13407
13945
  },
13408
13946
  required: ["worktreePath"],
13409
13947
  },
13410
- async execute(node, ctx) {
13948
+ async execute(node, ctx, engine) {
13411
13949
  const worktreePath = cfgOrCtx(node, ctx, "worktreePath");
13412
13950
  const branch = cfgOrCtx(node, ctx, "branch", "");
13413
13951
  const baseBranch = cfgOrCtx(node, ctx, "baseBranch", "origin/main");
@@ -14084,3 +14622,6 @@ export async function ensureWorkflowNodeTypesLoaded(options = {}) {
14084
14622
  }
14085
14623
 
14086
14624
 
14625
+
14626
+
14627
+