agent-relay 3.1.15 → 3.1.17

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 (63) hide show
  1. package/dist/index.cjs +683 -35
  2. package/dist/src/cli/commands/core.d.ts +2 -0
  3. package/dist/src/cli/commands/core.d.ts.map +1 -1
  4. package/dist/src/cli/commands/core.js +1 -0
  5. package/dist/src/cli/commands/core.js.map +1 -1
  6. package/dist/src/cli/lib/broker-lifecycle.d.ts.map +1 -1
  7. package/dist/src/cli/lib/broker-lifecycle.js +19 -2
  8. package/dist/src/cli/lib/broker-lifecycle.js.map +1 -1
  9. package/package.json +8 -8
  10. package/packages/acp-bridge/package.json +2 -2
  11. package/packages/config/package.json +1 -1
  12. package/packages/hooks/package.json +4 -4
  13. package/packages/memory/package.json +2 -2
  14. package/packages/openclaw/README.md +4 -7
  15. package/packages/openclaw/dist/setup.d.ts.map +1 -1
  16. package/packages/openclaw/dist/setup.js +10 -3
  17. package/packages/openclaw/dist/setup.js.map +1 -1
  18. package/packages/openclaw/package.json +2 -2
  19. package/packages/openclaw/skill/SKILL.md +27 -5
  20. package/packages/openclaw/src/setup.ts +10 -3
  21. package/packages/policy/package.json +2 -2
  22. package/packages/sdk/dist/__tests__/e2e-owner-review.test.d.ts +16 -0
  23. package/packages/sdk/dist/__tests__/e2e-owner-review.test.d.ts.map +1 -0
  24. package/packages/sdk/dist/__tests__/e2e-owner-review.test.js +640 -0
  25. package/packages/sdk/dist/__tests__/e2e-owner-review.test.js.map +1 -0
  26. package/packages/sdk/dist/client.d.ts +1 -0
  27. package/packages/sdk/dist/client.d.ts.map +1 -1
  28. package/packages/sdk/dist/client.js +3 -0
  29. package/packages/sdk/dist/client.js.map +1 -1
  30. package/packages/sdk/dist/workflows/cli.js +10 -0
  31. package/packages/sdk/dist/workflows/cli.js.map +1 -1
  32. package/packages/sdk/dist/workflows/runner.d.ts +51 -0
  33. package/packages/sdk/dist/workflows/runner.d.ts.map +1 -1
  34. package/packages/sdk/dist/workflows/runner.js +655 -33
  35. package/packages/sdk/dist/workflows/runner.js.map +1 -1
  36. package/packages/sdk/dist/workflows/trajectory.d.ts +22 -1
  37. package/packages/sdk/dist/workflows/trajectory.d.ts.map +1 -1
  38. package/packages/sdk/dist/workflows/trajectory.js +55 -8
  39. package/packages/sdk/dist/workflows/trajectory.js.map +1 -1
  40. package/packages/sdk/dist/workflows/types.d.ts +26 -0
  41. package/packages/sdk/dist/workflows/types.d.ts.map +1 -1
  42. package/packages/sdk/dist/workflows/types.js.map +1 -1
  43. package/packages/sdk/dist/workflows/validator.d.ts.map +1 -1
  44. package/packages/sdk/dist/workflows/validator.js +29 -0
  45. package/packages/sdk/dist/workflows/validator.js.map +1 -1
  46. package/packages/sdk/package.json +2 -2
  47. package/packages/sdk/src/__tests__/e2e-owner-review.test.ts +778 -0
  48. package/packages/sdk/src/__tests__/workflow-runner.test.ts +484 -9
  49. package/packages/sdk/src/client.ts +4 -0
  50. package/packages/sdk/src/workflows/README.md +11 -0
  51. package/packages/sdk/src/workflows/cli.ts +10 -0
  52. package/packages/sdk/src/workflows/runner.ts +854 -34
  53. package/packages/sdk/src/workflows/schema.json +219 -40
  54. package/packages/sdk/src/workflows/trajectory.ts +89 -8
  55. package/packages/sdk/src/workflows/types.ts +29 -0
  56. package/packages/sdk/src/workflows/validator.ts +29 -0
  57. package/packages/sdk-py/pyproject.toml +1 -1
  58. package/packages/sdk-py/src/agent_relay/__init__.py +2 -0
  59. package/packages/sdk-py/src/agent_relay/types.py +33 -0
  60. package/packages/telemetry/package.json +1 -1
  61. package/packages/trajectory/package.json +2 -2
  62. package/packages/user-directory/package.json +2 -2
  63. package/packages/utils/package.json +2 -2
package/dist/index.cjs CHANGED
@@ -9052,6 +9052,9 @@ var AgentRelayClient = class _AgentRelayClient {
9052
9052
  this.stderrListeners.delete(listener);
9053
9053
  };
9054
9054
  }
9055
+ get brokerPid() {
9056
+ return this.child?.pid;
9057
+ }
9055
9058
  async start() {
9056
9059
  if (this.child) {
9057
9060
  return;
@@ -47269,18 +47272,66 @@ var WorkflowTrajectory = class {
47269
47272
  }
47270
47273
  // ── Step events ────────────────────────────────────────────────────────────
47271
47274
  /** Record step started — captures intent, not just assignment. */
47272
- async stepStarted(step, agent) {
47275
+ async stepStarted(step, agent, participants) {
47276
+ if (!this.enabled || !this.trajectory)
47277
+ return;
47278
+ await this.registerAgent(agent, participants?.role ?? step.agent ?? "deterministic");
47279
+ if (participants?.owner && participants.owner !== agent) {
47280
+ await this.registerAgent(participants.owner, "owner");
47281
+ }
47282
+ if (participants?.specialist) {
47283
+ await this.registerAgent(participants.specialist, "specialist");
47284
+ }
47285
+ if (participants?.reviewer) {
47286
+ await this.registerAgent(participants.reviewer, "reviewer");
47287
+ }
47288
+ const intent = step.task ? step.task.trim().split(/\n|\.(?=\s)/)[0].trim().slice(0, 120) : `${step.type ?? "deterministic"} step`;
47289
+ this.addEvent("note", `"${step.name}": ${intent}`, void 0, { agent });
47290
+ await this.flush();
47291
+ }
47292
+ async registerAgent(name, role) {
47273
47293
  if (!this.enabled || !this.trajectory)
47274
47294
  return;
47275
- if (!this.trajectory.agents.some((a) => a.name === agent)) {
47295
+ if (!this.trajectory.agents.some((a) => a.name === name)) {
47276
47296
  this.trajectory.agents.push({
47277
- name: agent,
47278
- role: step.agent ?? "deterministic",
47297
+ name,
47298
+ role,
47279
47299
  joinedAt: (/* @__PURE__ */ new Date()).toISOString()
47280
47300
  });
47301
+ await this.flush();
47281
47302
  }
47282
- const intent = step.task ? step.task.trim().split(/\n|\.(?=\s)/)[0].trim().slice(0, 120) : `${step.type ?? "deterministic"} step`;
47283
- this.addEvent("note", `"${step.name}": ${intent}`, void 0, { agent });
47303
+ }
47304
+ async stepSupervisionAssigned(step, supervised) {
47305
+ if (!this.enabled || !this.trajectory)
47306
+ return;
47307
+ await this.registerAgent(supervised.owner.name, "owner");
47308
+ await this.registerAgent(supervised.specialist.name, "specialist");
47309
+ if (supervised.reviewer?.name) {
47310
+ await this.registerAgent(supervised.reviewer.name, "reviewer");
47311
+ }
47312
+ const reviewerNote = supervised.reviewer?.name ? `, reviewer=${supervised.reviewer.name}` : "";
47313
+ this.addEvent("decision", `"${step.name}" supervision assigned \u2192 owner=${supervised.owner.name}, specialist=${supervised.specialist.name}${reviewerNote}`, "medium", {
47314
+ owner: supervised.owner.name,
47315
+ specialist: supervised.specialist.name,
47316
+ reviewer: supervised.reviewer?.name
47317
+ });
47318
+ await this.flush();
47319
+ }
47320
+ async ownerMonitoringEvent(stepName, owner, detail, raw) {
47321
+ if (!this.enabled || !this.trajectory)
47322
+ return;
47323
+ this.addEvent("note", `"${stepName}" owner ${owner}: ${detail}`, "medium", raw ? { owner, ...raw } : { owner });
47324
+ await this.flush();
47325
+ }
47326
+ async reviewCompleted(stepName, reviewerName, decision, reason) {
47327
+ if (!this.enabled || !this.trajectory)
47328
+ return;
47329
+ this.addEvent("review-completed", `"${stepName}" review ${decision} by ${reviewerName}`, "medium", {
47330
+ stepName,
47331
+ reviewer: reviewerName,
47332
+ decision,
47333
+ reason
47334
+ });
47284
47335
  await this.flush();
47285
47336
  }
47286
47337
  /** Record step completed — captures what was accomplished. */
@@ -47610,6 +47661,10 @@ var WorkflowRunner = class _WorkflowRunner {
47610
47661
  lastIdleLog = /* @__PURE__ */ new Map();
47611
47662
  /** Tracks last logged activity type per agent to avoid duplicate status lines. */
47612
47663
  lastActivity = /* @__PURE__ */ new Map();
47664
+ /** Runtime-name lookup for agents participating in supervised owner flows. */
47665
+ supervisedRuntimeAgents = /* @__PURE__ */ new Map();
47666
+ /** Resolved named paths from the top-level `paths` config, keyed by name → absolute directory. */
47667
+ resolvedPaths = /* @__PURE__ */ new Map();
47613
47668
  constructor(options = {}) {
47614
47669
  this.db = options.db ?? new InMemoryWorkflowDb();
47615
47670
  this.workspaceId = options.workspaceId ?? "local";
@@ -47619,6 +47674,75 @@ var WorkflowRunner = class _WorkflowRunner {
47619
47674
  this.workersPath = import_node_path8.default.join(this.cwd, ".agent-relay", "team", "workers.json");
47620
47675
  this.executor = options.executor;
47621
47676
  }
47677
+ // ── Path resolution ─────────────────────────────────────────────────────
47678
+ /** Expand environment variables like $HOME or $VAR in a path string. */
47679
+ static resolveEnvVars(p) {
47680
+ return p.replace(/\$([A-Za-z_][A-Za-z0-9_]*)/g, (_match, varName) => {
47681
+ return process.env[varName] ?? _match;
47682
+ });
47683
+ }
47684
+ /**
47685
+ * Resolve and validate the top-level `paths` definitions from the config.
47686
+ * Returns a map of name → absolute directory path.
47687
+ * Throws if a required path does not exist.
47688
+ */
47689
+ resolvePathDefinitions(pathDefs, baseCwd) {
47690
+ const resolved = /* @__PURE__ */ new Map();
47691
+ const errors = [];
47692
+ const warnings = [];
47693
+ if (!pathDefs || pathDefs.length === 0)
47694
+ return { resolved, errors, warnings };
47695
+ const seenNames = /* @__PURE__ */ new Set();
47696
+ for (const pd of pathDefs) {
47697
+ if (seenNames.has(pd.name)) {
47698
+ errors.push(`Duplicate path name "${pd.name}"`);
47699
+ continue;
47700
+ }
47701
+ seenNames.add(pd.name);
47702
+ const expanded = _WorkflowRunner.resolveEnvVars(pd.path);
47703
+ const abs = import_node_path8.default.resolve(baseCwd, expanded);
47704
+ resolved.set(pd.name, abs);
47705
+ const isRequired = pd.required !== false;
47706
+ if (!(0, import_node_fs4.existsSync)(abs)) {
47707
+ if (isRequired) {
47708
+ errors.push(`Path "${pd.name}" resolves to "${abs}" which does not exist (required)`);
47709
+ } else {
47710
+ warnings.push(`Path "${pd.name}" resolves to "${abs}" which does not exist (optional)`);
47711
+ }
47712
+ }
47713
+ }
47714
+ return { resolved, errors, warnings };
47715
+ }
47716
+ /**
47717
+ * Resolve an agent's effective working directory, considering `workdir` (named path reference)
47718
+ * and `cwd` (explicit path). `workdir` takes precedence when both are set.
47719
+ */
47720
+ resolveAgentCwd(agent) {
47721
+ if (agent.workdir) {
47722
+ const resolved = this.resolvedPaths.get(agent.workdir);
47723
+ if (!resolved) {
47724
+ throw new Error(`Agent "${agent.name}" references workdir "${agent.workdir}" which is not defined in paths`);
47725
+ }
47726
+ return resolved;
47727
+ }
47728
+ if (agent.cwd) {
47729
+ return import_node_path8.default.resolve(this.cwd, agent.cwd);
47730
+ }
47731
+ return this.cwd;
47732
+ }
47733
+ /**
47734
+ * Resolve a step's working directory from its `workdir` field (named path reference).
47735
+ * Returns undefined if no workdir is set.
47736
+ */
47737
+ resolveStepWorkdir(step) {
47738
+ if (!step.workdir)
47739
+ return void 0;
47740
+ const resolved = this.resolvedPaths.get(step.workdir);
47741
+ if (!resolved) {
47742
+ throw new Error(`Step "${step.name}" references workdir "${step.workdir}" which is not defined in paths`);
47743
+ }
47744
+ return resolved;
47745
+ }
47622
47746
  // ── Progress logging ────────────────────────────────────────────────────
47623
47747
  /** Log a progress message with elapsed time since run start. */
47624
47748
  log(msg) {
@@ -47852,6 +47976,15 @@ var WorkflowRunner = class _WorkflowRunner {
47852
47976
  estimatedWaves: 0
47853
47977
  };
47854
47978
  }
47979
+ const pathResult = this.resolvePathDefinitions(resolved.paths, this.cwd);
47980
+ errors.push(...pathResult.errors);
47981
+ warnings.push(...pathResult.warnings);
47982
+ const dryRunPaths = pathResult.resolved;
47983
+ for (const agent of resolved.agents) {
47984
+ if (agent.workdir && !dryRunPaths.has(agent.workdir)) {
47985
+ errors.push(`Agent "${agent.name}" references workdir "${agent.workdir}" which is not defined in paths`);
47986
+ }
47987
+ }
47855
47988
  const workflows = resolved.workflows ?? [];
47856
47989
  const workflow2 = workflowName ? workflows.find((w) => w.name === workflowName) : workflows[0];
47857
47990
  if (!workflow2) {
@@ -47909,6 +48042,11 @@ ${err.suggestion}`);
47909
48042
  stepAgentCounts.set(step.agent, (stepAgentCounts.get(step.agent) ?? 0) + 1);
47910
48043
  }
47911
48044
  }
48045
+ for (const step of resolvedSteps) {
48046
+ if (step.workdir && !dryRunPaths.has(step.workdir)) {
48047
+ errors.push(`Step "${step.name}" references workdir "${step.workdir}" which is not defined in paths`);
48048
+ }
48049
+ }
47912
48050
  for (const agent of resolved.agents) {
47913
48051
  if (agent.cwd) {
47914
48052
  const resolvedCwd = import_node_path8.default.resolve(this.cwd, agent.cwd);
@@ -47989,7 +48127,7 @@ ${err.suggestion}`);
47989
48127
  name: a.name,
47990
48128
  cli: a.cli,
47991
48129
  role: a.role,
47992
- cwd: a.cwd,
48130
+ cwd: a.workdir ? dryRunPaths.get(a.workdir) : a.cwd,
47993
48131
  stepCount: stepAgentCounts.get(a.name) ?? 0
47994
48132
  }));
47995
48133
  const waves = [];
@@ -48235,6 +48373,17 @@ ${err.suggestion}`);
48235
48373
  /** Execute a named workflow from a validated config. */
48236
48374
  async execute(config3, workflowName, vars) {
48237
48375
  const resolved = vars ? this.resolveVariables(config3, vars) : config3;
48376
+ const pathResult = this.resolvePathDefinitions(resolved.paths, this.cwd);
48377
+ if (pathResult.errors.length > 0) {
48378
+ throw new Error(`Path validation failed:
48379
+ ${pathResult.errors.join("\n ")}`);
48380
+ }
48381
+ this.resolvedPaths = pathResult.resolved;
48382
+ if (this.resolvedPaths.size > 0) {
48383
+ for (const [name, abs] of this.resolvedPaths) {
48384
+ console.log(`[workflow] path "${name}" \u2192 ${abs}`);
48385
+ }
48386
+ }
48238
48387
  const workflows = resolved.workflows ?? [];
48239
48388
  const workflow2 = workflowName ? workflows.find((w) => w.name === workflowName) : workflows[0];
48240
48389
  if (!workflow2) {
@@ -48294,6 +48443,12 @@ ${err.suggestion}`);
48294
48443
  throw new Error(`Run "${runId}" is in status "${run.status}" and cannot be resumed`);
48295
48444
  }
48296
48445
  const config3 = vars ? this.resolveVariables(run.config, vars) : run.config;
48446
+ const pathResult = this.resolvePathDefinitions(config3.paths, this.cwd);
48447
+ if (pathResult.errors.length > 0) {
48448
+ throw new Error(`Path validation failed:
48449
+ ${pathResult.errors.join("\n ")}`);
48450
+ }
48451
+ this.resolvedPaths = pathResult.resolved;
48297
48452
  const workflows = config3.workflows ?? [];
48298
48453
  const workflow2 = workflows.find((w) => w.name === run.workflowName);
48299
48454
  if (!workflow2) {
@@ -48418,6 +48573,10 @@ ${err.suggestion}`);
48418
48573
  const fromShort = msg.from.replace(/-[a-f0-9]{6,}$/, "");
48419
48574
  const toShort = msg.to.replace(/-[a-f0-9]{6,}$/, "");
48420
48575
  this.log(`[msg] ${fromShort} \u2192 ${toShort}: ${body}`);
48576
+ const supervision = this.supervisedRuntimeAgents.get(msg.from);
48577
+ if (supervision?.role === "owner") {
48578
+ void this.trajectory?.ownerMonitoringEvent(supervision.stepName, supervision.logicalName, `Messaged ${msg.to}: ${msg.text.slice(0, 120)}`, { to: msg.to, text: msg.text });
48579
+ }
48421
48580
  };
48422
48581
  this.relay.onAgentSpawned = (agent) => {
48423
48582
  if (!this.activeAgentHandles.has(agent.name)) {
@@ -48520,6 +48679,7 @@ ${err.suggestion}`);
48520
48679
  }
48521
48680
  this.lastIdleLog.clear();
48522
48681
  this.lastActivity.clear();
48682
+ this.supervisedRuntimeAgents.clear();
48523
48683
  this.log("Shutting down broker...");
48524
48684
  await this.relay?.shutdown();
48525
48685
  this.relay = void 0;
@@ -48769,9 +48929,10 @@ ${trimmedOutput.slice(0, 200)}`);
48769
48929
  const value = this.resolveDotPath(key, stepOutputContext);
48770
48930
  return value !== void 0 ? String(value) : _match;
48771
48931
  });
48932
+ const stepCwd = this.resolveStepWorkdir(step) ?? this.cwd;
48772
48933
  try {
48773
48934
  if (this.executor?.executeDeterministicStep) {
48774
- const result = await this.executor.executeDeterministicStep(step, resolvedCommand, this.cwd);
48935
+ const result = await this.executor.executeDeterministicStep(step, resolvedCommand, stepCwd);
48775
48936
  const failOnError = step.failOnError !== false;
48776
48937
  if (failOnError && result.exitCode !== 0) {
48777
48938
  throw new Error(`Command failed with exit code ${result.exitCode}: ${result.output.slice(0, 500)}`);
@@ -48793,7 +48954,7 @@ ${trimmedOutput.slice(0, 200)}`);
48793
48954
  const output = await new Promise((resolve3, reject) => {
48794
48955
  const child = (0, import_node_child_process3.spawn)("sh", ["-c", resolvedCommand], {
48795
48956
  stdio: "pipe",
48796
- cwd: this.cwd,
48957
+ cwd: stepCwd,
48797
48958
  env: { ...process.env }
48798
48959
  });
48799
48960
  const stdoutChunks = [];
@@ -48896,19 +49057,20 @@ ${trimmedOutput.slice(0, 200)}`);
48896
49057
  const baseBranch = step.baseBranch ? this.interpolateStepTask(step.baseBranch, stepOutputContext) : "HEAD";
48897
49058
  const worktreePath = step.path ? this.interpolateStepTask(step.path, stepOutputContext) : import_node_path8.default.join(".worktrees", step.name);
48898
49059
  const createBranch = step.createBranch !== false;
49060
+ const stepCwd = this.resolveStepWorkdir(step) ?? this.cwd;
48899
49061
  if (!branch) {
48900
49062
  const errorMsg = 'Worktree step missing required "branch" field';
48901
49063
  await this.markStepFailed(state, errorMsg, runId);
48902
49064
  throw new Error(`Step "${step.name}" failed: ${errorMsg}`);
48903
49065
  }
48904
49066
  try {
48905
- const absoluteWorktreePath = import_node_path8.default.resolve(this.cwd, worktreePath);
49067
+ const absoluteWorktreePath = import_node_path8.default.resolve(stepCwd, worktreePath);
48906
49068
  const checkBranchCmd = `git rev-parse --verify --quiet ${branch} 2>/dev/null`;
48907
49069
  let branchExists = false;
48908
49070
  await new Promise((resolve3) => {
48909
49071
  const checkChild = (0, import_node_child_process3.spawn)("sh", ["-c", checkBranchCmd], {
48910
49072
  stdio: "pipe",
48911
- cwd: this.cwd,
49073
+ cwd: stepCwd,
48912
49074
  env: { ...process.env }
48913
49075
  });
48914
49076
  checkChild.on("close", (code) => {
@@ -48930,7 +49092,7 @@ ${trimmedOutput.slice(0, 200)}`);
48930
49092
  const output = await new Promise((resolve3, reject) => {
48931
49093
  const child = (0, import_node_child_process3.spawn)("sh", ["-c", worktreeCmd], {
48932
49094
  stdio: "pipe",
48933
- cwd: this.cwd,
49095
+ cwd: stepCwd,
48934
49096
  env: { ...process.env }
48935
49097
  });
48936
49098
  const stdoutChunks = [];
@@ -49024,10 +49186,19 @@ ${trimmedOutput.slice(0, 200)}`);
49024
49186
  if (!rawAgentDef) {
49025
49187
  throw new Error(`Agent "${agentName}" not found in config`);
49026
49188
  }
49027
- const agentDef = _WorkflowRunner.resolveAgentDef(rawAgentDef);
49028
- const maxRetries = step.retries ?? agentDef.constraints?.retries ?? errorHandling?.maxRetries ?? 0;
49189
+ const specialistDef = _WorkflowRunner.resolveAgentDef(rawAgentDef);
49190
+ const usesOwnerFlow = specialistDef.interactive !== false;
49191
+ const ownerDef = usesOwnerFlow ? this.resolveAutoStepOwner(specialistDef, agentMap) : specialistDef;
49192
+ const reviewDef = usesOwnerFlow ? this.resolveAutoReviewAgent(ownerDef, agentMap) : void 0;
49193
+ const supervised = {
49194
+ specialist: specialistDef,
49195
+ owner: ownerDef,
49196
+ reviewer: reviewDef
49197
+ };
49198
+ const usesDedicatedOwner = usesOwnerFlow && ownerDef.name !== specialistDef.name;
49199
+ const maxRetries = step.retries ?? ownerDef.constraints?.retries ?? specialistDef.constraints?.retries ?? errorHandling?.maxRetries ?? 0;
49029
49200
  const retryDelay = errorHandling?.retryDelayMs ?? 1e3;
49030
- const timeoutMs = step.timeoutMs ?? agentDef.constraints?.timeoutMs ?? this.currentConfig?.swarm?.timeoutMs;
49201
+ const timeoutMs = step.timeoutMs ?? ownerDef.constraints?.timeoutMs ?? specialistDef.constraints?.timeoutMs ?? this.currentConfig?.swarm?.timeoutMs;
49031
49202
  let lastError;
49032
49203
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
49033
49204
  this.checkAborted();
@@ -49051,41 +49222,95 @@ ${trimmedOutput.slice(0, 200)}`);
49051
49222
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
49052
49223
  });
49053
49224
  this.emit({ type: "step:started", runId, stepName: step.name });
49054
- this.postToChannel(`**[${step.name}]** Started (agent: ${agentDef.name})`);
49055
- await this.trajectory?.stepStarted(step, agentDef.name);
49225
+ this.postToChannel(`**[${step.name}]** Started (owner: ${ownerDef.name}, specialist: ${specialistDef.name})`);
49226
+ await this.trajectory?.stepStarted(step, ownerDef.name, {
49227
+ role: usesDedicatedOwner ? "owner" : "specialist",
49228
+ owner: ownerDef.name,
49229
+ specialist: specialistDef.name,
49230
+ reviewer: reviewDef?.name
49231
+ });
49232
+ if (usesDedicatedOwner) {
49233
+ await this.trajectory?.stepSupervisionAssigned(step, supervised);
49234
+ }
49235
+ this.emit({
49236
+ type: "step:owner-assigned",
49237
+ runId,
49238
+ stepName: step.name,
49239
+ ownerName: ownerDef.name,
49240
+ specialistName: specialistDef.name
49241
+ });
49056
49242
  const stepOutputContext = this.buildStepOutputContext(stepStates, runId);
49057
49243
  let resolvedTask = this.interpolateStepTask(step.task ?? "", stepOutputContext);
49058
- if (agentDef.interactive !== false) {
49244
+ if (specialistDef.interactive !== false || ownerDef.interactive !== false) {
49059
49245
  const nonInteractiveInfo = this.buildNonInteractiveAwareness(agentMap, stepStates);
49060
49246
  if (nonInteractiveInfo) {
49061
49247
  resolvedTask += nonInteractiveInfo;
49062
49248
  }
49063
49249
  }
49064
- this.log(`[${step.name}] Spawning agent "${agentDef.name}" (cli: ${agentDef.cli})`);
49065
- const resolvedStep = { ...step, task: resolvedTask };
49066
- const output = this.executor ? await this.executor.executeAgentStep(resolvedStep, agentDef, resolvedTask, timeoutMs) : await this.spawnAndWait(agentDef, resolvedStep, timeoutMs);
49067
- this.log(`[${step.name}] Agent "${agentDef.name}" exited`);
49250
+ const applyStepWorkdir = (def) => {
49251
+ if (step.workdir) {
49252
+ const stepWorkdir = this.resolveStepWorkdir(step);
49253
+ if (stepWorkdir) {
49254
+ return { ...def, cwd: stepWorkdir, workdir: void 0 };
49255
+ }
49256
+ }
49257
+ return def;
49258
+ };
49259
+ const effectiveSpecialist = applyStepWorkdir(specialistDef);
49260
+ const effectiveOwner = applyStepWorkdir(ownerDef);
49261
+ let specialistOutput;
49262
+ let ownerOutput;
49263
+ let ownerElapsed;
49264
+ if (usesDedicatedOwner) {
49265
+ const result = await this.executeSupervisedAgentStep(step, { specialist: effectiveSpecialist, owner: effectiveOwner, reviewer: reviewDef }, resolvedTask, timeoutMs);
49266
+ specialistOutput = result.specialistOutput;
49267
+ ownerOutput = result.ownerOutput;
49268
+ ownerElapsed = result.ownerElapsed;
49269
+ } else {
49270
+ const ownerTask = this.injectStepOwnerContract(step, resolvedTask, effectiveOwner, effectiveSpecialist);
49271
+ this.log(`[${step.name}] Spawning owner "${effectiveOwner.name}" (cli: ${effectiveOwner.cli})${step.workdir ? ` [workdir: ${step.workdir}]` : ""}`);
49272
+ const resolvedStep = { ...step, task: ownerTask };
49273
+ const ownerStartTime = Date.now();
49274
+ const output = this.executor ? await this.executor.executeAgentStep(resolvedStep, effectiveOwner, ownerTask, timeoutMs) : await this.spawnAndWait(effectiveOwner, resolvedStep, timeoutMs);
49275
+ ownerElapsed = Date.now() - ownerStartTime;
49276
+ this.log(`[${step.name}] Owner "${effectiveOwner.name}" exited`);
49277
+ if (usesOwnerFlow) {
49278
+ this.assertOwnerCompletionMarker(step, output, ownerTask);
49279
+ }
49280
+ specialistOutput = output;
49281
+ ownerOutput = output;
49282
+ }
49068
49283
  if (step.verification) {
49069
- this.runVerification(step.verification, output, step.name, resolvedTask);
49284
+ this.runVerification(step.verification, specialistOutput, step.name, resolvedTask);
49285
+ }
49286
+ let combinedOutput = specialistOutput;
49287
+ if (usesOwnerFlow && reviewDef) {
49288
+ const remainingMs = timeoutMs ? Math.max(0, timeoutMs - ownerElapsed) : void 0;
49289
+ const reviewOutput = await this.runStepReviewGate(step, resolvedTask, specialistOutput, ownerOutput, ownerDef, reviewDef, remainingMs);
49290
+ combinedOutput = this.combineStepAndReviewOutput(specialistOutput, reviewOutput);
49070
49291
  }
49071
49292
  state.row.status = "completed";
49072
- state.row.output = output;
49293
+ state.row.output = combinedOutput;
49073
49294
  state.row.completedAt = (/* @__PURE__ */ new Date()).toISOString();
49074
49295
  await this.db.updateStep(state.row.id, {
49075
49296
  status: "completed",
49076
- output,
49297
+ output: combinedOutput,
49077
49298
  completedAt: state.row.completedAt,
49078
49299
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
49079
49300
  });
49080
- await this.persistStepOutput(runId, step.name, output);
49081
- this.emit({ type: "step:completed", runId, stepName: step.name, output });
49082
- await this.trajectory?.stepCompleted(step, output, attempt + 1);
49301
+ await this.persistStepOutput(runId, step.name, combinedOutput);
49302
+ this.emit({ type: "step:completed", runId, stepName: step.name, output: combinedOutput });
49303
+ await this.trajectory?.stepCompleted(step, combinedOutput, attempt + 1);
49083
49304
  return;
49084
49305
  } catch (err) {
49085
49306
  lastError = err instanceof Error ? err.message : String(err);
49307
+ const ownerTimedOut = usesDedicatedOwner ? /\bowner timed out\b/i.test(lastError) : /\btimed out\b/i.test(lastError) && !lastError.includes(`${step.name}-review`);
49308
+ if (ownerTimedOut) {
49309
+ this.emit({ type: "step:owner-timeout", runId, stepName: step.name, ownerName: ownerDef.name });
49310
+ }
49086
49311
  }
49087
49312
  }
49088
- const nonInteractive = agentDef.interactive === false || ["worker", "reviewer", "analyst"].includes(agentDef.preset ?? "");
49313
+ const nonInteractive = ownerDef.interactive === false || ["worker", "reviewer", "analyst"].includes(ownerDef.preset ?? "");
49089
49314
  const verificationValue = typeof step.verification === "object" && "value" in step.verification ? String(step.verification.value) : void 0;
49090
49315
  await this.trajectory?.stepFailed(step, lastError ?? "Unknown error", maxRetries + 1, maxRetries, {
49091
49316
  agent: agentName,
@@ -49096,6 +49321,423 @@ ${trimmedOutput.slice(0, 200)}`);
49096
49321
  await this.markStepFailed(state, lastError ?? "Unknown error", runId);
49097
49322
  throw new Error(`Step "${step.name}" failed after ${maxRetries} retries: ${lastError ?? "Unknown error"}`);
49098
49323
  }
49324
+ injectStepOwnerContract(step, resolvedTask, ownerDef, specialistDef) {
49325
+ if (ownerDef.interactive === false)
49326
+ return resolvedTask;
49327
+ const specialistNote = ownerDef.name === specialistDef.name ? "" : `Specialist intended for this step: "${specialistDef.name}" (${specialistDef.role ?? specialistDef.cli}).`;
49328
+ return resolvedTask + `
49329
+
49330
+ ---
49331
+ STEP OWNER CONTRACT:
49332
+ - You are the accountable owner for step "${step.name}".
49333
+ ` + (specialistNote ? `- ${specialistNote}
49334
+ ` : "") + `- If you delegate, you must still verify completion yourself.
49335
+ - Before exiting, provide an explicit completion line: STEP_COMPLETE:${step.name}
49336
+ - Then self-terminate immediately with /exit.`;
49337
+ }
49338
+ buildOwnerSupervisorTask(step, originalTask, supervised, workerRuntimeName) {
49339
+ const verificationGuide = this.buildSupervisorVerificationGuide(step.verification);
49340
+ const channelLine = this.channel ? `#${this.channel}` : "(workflow channel unavailable)";
49341
+ return `You are the step owner/supervisor for step "${step.name}".
49342
+
49343
+ Worker: ${supervised.specialist.name} (runtime: ${workerRuntimeName}) on ${channelLine}
49344
+ Task: ${originalTask}
49345
+
49346
+ Your job: Monitor the worker and determine when the task is complete.
49347
+
49348
+ How to verify completion:
49349
+ - Watch ${channelLine} for the worker's progress messages and mirrored PTY output
49350
+ - Check file changes: run \`git diff --stat\` or inspect expected files directly
49351
+ - Ask the worker directly on ${channelLine} if you need a status update
49352
+ ` + verificationGuide + `
49353
+ When you're satisfied the work is done correctly:
49354
+ Output exactly: STEP_COMPLETE:${step.name}`;
49355
+ }
49356
+ buildSupervisorVerificationGuide(verification) {
49357
+ if (!verification)
49358
+ return "";
49359
+ switch (verification.type) {
49360
+ case "output_contains":
49361
+ return `- Verification gate: confirm the worker output contains ${JSON.stringify(verification.value)}
49362
+ `;
49363
+ case "file_exists":
49364
+ return `- Verification gate: confirm the file exists at ${JSON.stringify(verification.value)}
49365
+ `;
49366
+ case "exit_code":
49367
+ return `- Verification gate: confirm the worker exits with code ${JSON.stringify(verification.value)}
49368
+ `;
49369
+ case "custom":
49370
+ return `- Verification gate: apply the custom verification rule ${JSON.stringify(verification.value)}
49371
+ `;
49372
+ default:
49373
+ return "";
49374
+ }
49375
+ }
49376
+ async executeSupervisedAgentStep(step, supervised, resolvedTask, timeoutMs) {
49377
+ if (this.executor) {
49378
+ const supervisorTask2 = this.buildOwnerSupervisorTask(step, resolvedTask, supervised, supervised.specialist.name);
49379
+ const specialistStep2 = { ...step, task: resolvedTask };
49380
+ const ownerStep2 = {
49381
+ ...step,
49382
+ name: `${step.name}-owner`,
49383
+ agent: supervised.owner.name,
49384
+ task: supervisorTask2
49385
+ };
49386
+ this.log(`[${step.name}] Spawning specialist "${supervised.specialist.name}" and owner "${supervised.owner.name}"`);
49387
+ const specialistPromise = this.executor.executeAgentStep(specialistStep2, supervised.specialist, resolvedTask, timeoutMs);
49388
+ const ownerStartTime2 = Date.now();
49389
+ const ownerOutput = await this.executor.executeAgentStep(ownerStep2, supervised.owner, supervisorTask2, timeoutMs);
49390
+ const ownerElapsed = Date.now() - ownerStartTime2;
49391
+ this.assertOwnerCompletionMarker(step, ownerOutput, supervisorTask2);
49392
+ const specialistOutput = await specialistPromise;
49393
+ return { specialistOutput, ownerOutput, ownerElapsed };
49394
+ }
49395
+ let workerHandle;
49396
+ let workerRuntimeName = supervised.specialist.name;
49397
+ let workerSpawned = false;
49398
+ let workerReleased = false;
49399
+ let resolveWorkerSpawn;
49400
+ let rejectWorkerSpawn;
49401
+ const workerReady = new Promise((resolve3, reject) => {
49402
+ resolveWorkerSpawn = resolve3;
49403
+ rejectWorkerSpawn = reject;
49404
+ });
49405
+ const specialistStep = { ...step, task: resolvedTask };
49406
+ this.log(`[${step.name}] Spawning specialist "${supervised.specialist.name}" (cli: ${supervised.specialist.cli})`);
49407
+ const workerPromise = this.spawnAndWait(supervised.specialist, specialistStep, timeoutMs, {
49408
+ agentNameSuffix: "worker",
49409
+ onSpawned: ({ actualName, agent }) => {
49410
+ workerHandle = agent;
49411
+ workerRuntimeName = actualName;
49412
+ this.supervisedRuntimeAgents.set(actualName, {
49413
+ stepName: step.name,
49414
+ role: "specialist",
49415
+ logicalName: supervised.specialist.name
49416
+ });
49417
+ if (!workerSpawned) {
49418
+ workerSpawned = true;
49419
+ resolveWorkerSpawn();
49420
+ }
49421
+ },
49422
+ onChunk: ({ agentName, chunk }) => {
49423
+ this.forwardAgentChunkToChannel(step.name, "Worker", agentName, chunk);
49424
+ }
49425
+ }).catch((error95) => {
49426
+ if (!workerSpawned) {
49427
+ workerSpawned = true;
49428
+ rejectWorkerSpawn(error95);
49429
+ }
49430
+ throw error95;
49431
+ });
49432
+ const workerSettled = workerPromise.catch(() => void 0);
49433
+ workerPromise.then((output) => {
49434
+ workerReleased = true;
49435
+ this.postToChannel(`**[${step.name}]** Worker \`${workerRuntimeName}\` exited`);
49436
+ if (step.verification?.type === "output_contains" && output.includes(step.verification.value)) {
49437
+ this.postToChannel(`**[${step.name}]** Verification gate observed: output contains ${JSON.stringify(step.verification.value)}`);
49438
+ }
49439
+ }).catch((error95) => {
49440
+ const message = error95 instanceof Error ? error95.message : String(error95);
49441
+ this.postToChannel(`**[${step.name}]** Worker \`${workerRuntimeName}\` exited with error: ${message}`);
49442
+ });
49443
+ await workerReady;
49444
+ const supervisorTask = this.buildOwnerSupervisorTask(step, resolvedTask, supervised, workerRuntimeName);
49445
+ const ownerStep = {
49446
+ ...step,
49447
+ name: `${step.name}-owner`,
49448
+ agent: supervised.owner.name,
49449
+ task: supervisorTask
49450
+ };
49451
+ this.log(`[${step.name}] Spawning owner "${supervised.owner.name}" (cli: ${supervised.owner.cli})`);
49452
+ const ownerStartTime = Date.now();
49453
+ try {
49454
+ const ownerOutput = await this.spawnAndWait(supervised.owner, ownerStep, timeoutMs, {
49455
+ agentNameSuffix: "owner",
49456
+ onSpawned: ({ actualName }) => {
49457
+ this.supervisedRuntimeAgents.set(actualName, {
49458
+ stepName: step.name,
49459
+ role: "owner",
49460
+ logicalName: supervised.owner.name
49461
+ });
49462
+ },
49463
+ onChunk: ({ chunk }) => {
49464
+ void this.recordOwnerMonitoringChunk(step, supervised.owner, chunk);
49465
+ }
49466
+ });
49467
+ const ownerElapsed = Date.now() - ownerStartTime;
49468
+ this.log(`[${step.name}] Owner "${supervised.owner.name}" exited`);
49469
+ this.assertOwnerCompletionMarker(step, ownerOutput, supervisorTask);
49470
+ const specialistOutput = await workerPromise;
49471
+ return { specialistOutput, ownerOutput, ownerElapsed };
49472
+ } catch (error95) {
49473
+ const message = error95 instanceof Error ? error95.message : String(error95);
49474
+ if (!workerReleased && workerHandle) {
49475
+ await workerHandle.release().catch(() => void 0);
49476
+ }
49477
+ await workerSettled;
49478
+ if (/\btimed out\b/i.test(message)) {
49479
+ throw new Error(`Step "${step.name}" owner timed out after ${timeoutMs ?? "unknown"}ms`);
49480
+ }
49481
+ throw error95;
49482
+ }
49483
+ }
49484
+ forwardAgentChunkToChannel(stepName, roleLabel, agentName, chunk) {
49485
+ const lines = _WorkflowRunner.stripAnsi(chunk).split("\n").map((line) => line.trim()).filter(Boolean).slice(0, 3);
49486
+ for (const line of lines) {
49487
+ this.postToChannel(`**[${stepName}]** ${roleLabel} \`${agentName}\`: ${line.slice(0, 280)}`);
49488
+ }
49489
+ }
49490
+ async recordOwnerMonitoringChunk(step, ownerDef, chunk) {
49491
+ const stripped = _WorkflowRunner.stripAnsi(chunk);
49492
+ const details = [];
49493
+ if (/git diff --stat/i.test(stripped))
49494
+ details.push("Checked git diff stats");
49495
+ if (/\bls -la\b/i.test(stripped))
49496
+ details.push("Listed files for verification");
49497
+ if (/status update\?/i.test(stripped))
49498
+ details.push("Asked the worker for a status update");
49499
+ if (/STEP_COMPLETE:/i.test(stripped))
49500
+ details.push("Declared the step complete");
49501
+ for (const detail of details) {
49502
+ await this.trajectory?.ownerMonitoringEvent(step.name, ownerDef.name, detail, {
49503
+ output: stripped.slice(0, 240)
49504
+ });
49505
+ }
49506
+ }
49507
+ resolveAutoStepOwner(specialistDef, agentMap) {
49508
+ if (specialistDef.interactive === false)
49509
+ return specialistDef;
49510
+ const allDefs = [...agentMap.values()].map((d) => _WorkflowRunner.resolveAgentDef(d));
49511
+ const candidates = allDefs.filter((d) => d.interactive !== false);
49512
+ const matchesHubRole = (text) => [..._WorkflowRunner.HUB_ROLES].some((r) => new RegExp(`\\b${r}\\b`, "i").test(text));
49513
+ const ownerish = (def) => {
49514
+ const nameLC = def.name.toLowerCase();
49515
+ const roleLC = def.role?.toLowerCase() ?? "";
49516
+ return matchesHubRole(nameLC) || matchesHubRole(roleLC);
49517
+ };
49518
+ const ownerPriority = (def) => {
49519
+ const roleLC = def.role?.toLowerCase() ?? "";
49520
+ const nameLC = def.name.toLowerCase();
49521
+ if (/\blead\b/.test(roleLC) || /\blead\b/.test(nameLC))
49522
+ return 6;
49523
+ if (/\bcoordinator\b/.test(roleLC) || /\bcoordinator\b/.test(nameLC))
49524
+ return 5;
49525
+ if (/\bsupervisor\b/.test(roleLC) || /\bsupervisor\b/.test(nameLC))
49526
+ return 4;
49527
+ if (/\borchestrator\b/.test(roleLC) || /\borchestrator\b/.test(nameLC))
49528
+ return 3;
49529
+ if (/\bhub\b/.test(roleLC) || /\bhub\b/.test(nameLC))
49530
+ return 2;
49531
+ return ownerish(def) ? 1 : 0;
49532
+ };
49533
+ const dedicatedOwner = candidates.filter((d) => d.name !== specialistDef.name && ownerish(d)).sort((a, b) => ownerPriority(b) - ownerPriority(a) || a.name.localeCompare(b.name))[0];
49534
+ if (dedicatedOwner)
49535
+ return dedicatedOwner;
49536
+ return specialistDef;
49537
+ }
49538
+ resolveAutoReviewAgent(ownerDef, agentMap) {
49539
+ const allDefs = [...agentMap.values()].map((d) => _WorkflowRunner.resolveAgentDef(d));
49540
+ const isReviewer = (def) => {
49541
+ const roleLC = def.role?.toLowerCase() ?? "";
49542
+ const nameLC = def.name.toLowerCase();
49543
+ return def.preset === "reviewer" || roleLC.includes("review") || roleLC.includes("critic") || roleLC.includes("verifier") || roleLC.includes("qa") || nameLC.includes("review");
49544
+ };
49545
+ const reviewerPriority = (def) => {
49546
+ if (def.preset === "reviewer")
49547
+ return 5;
49548
+ const roleLC = def.role?.toLowerCase() ?? "";
49549
+ const nameLC = def.name.toLowerCase();
49550
+ if (roleLC.includes("review") || nameLC.includes("review"))
49551
+ return 4;
49552
+ if (roleLC.includes("verifier") || roleLC.includes("qa"))
49553
+ return 3;
49554
+ if (roleLC.includes("critic"))
49555
+ return 2;
49556
+ return isReviewer(def) ? 1 : 0;
49557
+ };
49558
+ const dedicated = allDefs.filter((d) => d.name !== ownerDef.name && isReviewer(d)).sort((a, b) => reviewerPriority(b) - reviewerPriority(a) || a.name.localeCompare(b.name))[0];
49559
+ if (dedicated)
49560
+ return dedicated;
49561
+ const alternate = allDefs.find((d) => d.name !== ownerDef.name && d.interactive !== false);
49562
+ if (alternate)
49563
+ return alternate;
49564
+ return ownerDef;
49565
+ }
49566
+ assertOwnerCompletionMarker(step, output, injectedTaskText) {
49567
+ const marker = `STEP_COMPLETE:${step.name}`;
49568
+ const taskHasMarker = injectedTaskText.includes(marker);
49569
+ const first = output.indexOf(marker);
49570
+ if (first === -1) {
49571
+ throw new Error(`Step "${step.name}" owner completion marker missing: "${marker}"`);
49572
+ }
49573
+ const outputLikelyContainsInjectedPrompt = output.includes("STEP OWNER CONTRACT") || output.includes("Output exactly: STEP_COMPLETE:");
49574
+ if (taskHasMarker && outputLikelyContainsInjectedPrompt) {
49575
+ const hasSecond = output.includes(marker, first + marker.length);
49576
+ if (!hasSecond) {
49577
+ throw new Error(`Step "${step.name}" owner completion marker missing in agent response: "${marker}"`);
49578
+ }
49579
+ }
49580
+ }
49581
+ async runStepReviewGate(step, resolvedTask, specialistOutput, ownerOutput, ownerDef, reviewerDef, timeoutMs) {
49582
+ const reviewSnippetMax = 12e3;
49583
+ let specialistSnippet = specialistOutput;
49584
+ if (specialistOutput.length > reviewSnippetMax) {
49585
+ const head = Math.floor(reviewSnippetMax / 2);
49586
+ const tail = reviewSnippetMax - head;
49587
+ const omitted = specialistOutput.length - head - tail;
49588
+ specialistSnippet = `${specialistOutput.slice(0, head)}
49589
+ ...[truncated ${omitted} chars for review]...
49590
+ ${specialistOutput.slice(specialistOutput.length - tail)}`;
49591
+ }
49592
+ let ownerSnippet = ownerOutput;
49593
+ if (ownerOutput.length > reviewSnippetMax) {
49594
+ const head = Math.floor(reviewSnippetMax / 2);
49595
+ const tail = reviewSnippetMax - head;
49596
+ const omitted = ownerOutput.length - head - tail;
49597
+ ownerSnippet = `${ownerOutput.slice(0, head)}
49598
+ ...[truncated ${omitted} chars for review]...
49599
+ ${ownerOutput.slice(ownerOutput.length - tail)}`;
49600
+ }
49601
+ const reviewTask = `Review workflow step "${step.name}" for completion and safe handoff.
49602
+ Step owner: ${ownerDef.name}
49603
+ Original objective:
49604
+ ${resolvedTask}
49605
+
49606
+ Specialist output:
49607
+ ${specialistSnippet}
49608
+
49609
+ Owner verification notes:
49610
+ ${ownerSnippet}
49611
+
49612
+ Return exactly:
49613
+ REVIEW_DECISION: APPROVE or REJECT
49614
+ REVIEW_REASON: <one sentence>
49615
+ Then output /exit.`;
49616
+ const safetyTimeoutMs = timeoutMs ?? 6e5;
49617
+ const reviewStep = {
49618
+ name: `${step.name}-review`,
49619
+ type: "agent",
49620
+ agent: reviewerDef.name,
49621
+ task: reviewTask
49622
+ };
49623
+ await this.trajectory?.registerAgent(reviewerDef.name, "reviewer");
49624
+ this.postToChannel(`**[${step.name}]** Review started (reviewer: ${reviewerDef.name})`);
49625
+ const emitReviewCompleted = async (decision, reason) => {
49626
+ await this.trajectory?.reviewCompleted(step.name, reviewerDef.name, decision, reason);
49627
+ this.emit({
49628
+ type: "step:review-completed",
49629
+ runId: this.currentRunId ?? "",
49630
+ stepName: step.name,
49631
+ reviewerName: reviewerDef.name,
49632
+ decision
49633
+ });
49634
+ };
49635
+ if (this.executor) {
49636
+ const reviewOutput2 = await this.executor.executeAgentStep(reviewStep, reviewerDef, reviewTask, safetyTimeoutMs);
49637
+ const parsed = this.parseReviewDecision(reviewOutput2);
49638
+ if (!parsed) {
49639
+ throw new Error(`Step "${step.name}" review response malformed from "${reviewerDef.name}" (missing REVIEW_DECISION)`);
49640
+ }
49641
+ await emitReviewCompleted(parsed.decision, parsed.reason);
49642
+ if (parsed.decision === "rejected") {
49643
+ throw new Error(`Step "${step.name}" review rejected by "${reviewerDef.name}"`);
49644
+ }
49645
+ this.postToChannel(`**[${step.name}]** Review approved by \`${reviewerDef.name}\``);
49646
+ return reviewOutput2;
49647
+ }
49648
+ let reviewerHandle;
49649
+ let reviewerReleased = false;
49650
+ let reviewOutput = "";
49651
+ let completedReview;
49652
+ let reviewCompletionPromise;
49653
+ const reviewCompletionStarted = { value: false };
49654
+ const startReviewCompletion = (parsed) => {
49655
+ if (reviewCompletionStarted.value)
49656
+ return;
49657
+ reviewCompletionStarted.value = true;
49658
+ completedReview = parsed;
49659
+ reviewCompletionPromise = (async () => {
49660
+ await emitReviewCompleted(parsed.decision, parsed.reason);
49661
+ if (reviewerHandle && !reviewerReleased) {
49662
+ reviewerReleased = true;
49663
+ await reviewerHandle.release().catch(() => void 0);
49664
+ }
49665
+ })();
49666
+ };
49667
+ try {
49668
+ reviewOutput = await this.spawnAndWait(reviewerDef, reviewStep, safetyTimeoutMs, {
49669
+ onSpawned: ({ agent }) => {
49670
+ reviewerHandle = agent;
49671
+ },
49672
+ onChunk: ({ chunk }) => {
49673
+ const nextOutput = reviewOutput + _WorkflowRunner.stripAnsi(chunk);
49674
+ reviewOutput = nextOutput;
49675
+ const parsed = this.parseReviewDecision(nextOutput);
49676
+ if (parsed) {
49677
+ startReviewCompletion(parsed);
49678
+ }
49679
+ }
49680
+ });
49681
+ await reviewCompletionPromise;
49682
+ } catch (error95) {
49683
+ const message = error95 instanceof Error ? error95.message : String(error95);
49684
+ if (/\btimed out\b/i.test(message)) {
49685
+ this.log(`[${step.name}] Review safety backstop timeout fired after ${safetyTimeoutMs}ms`);
49686
+ throw new Error(`Step "${step.name}" review safety backstop timed out after ${safetyTimeoutMs}ms`);
49687
+ }
49688
+ throw error95;
49689
+ }
49690
+ if (!completedReview) {
49691
+ const parsed = this.parseReviewDecision(reviewOutput);
49692
+ if (!parsed) {
49693
+ throw new Error(`Step "${step.name}" review response malformed from "${reviewerDef.name}" (missing REVIEW_DECISION)`);
49694
+ }
49695
+ completedReview = parsed;
49696
+ await emitReviewCompleted(parsed.decision, parsed.reason);
49697
+ }
49698
+ if (completedReview.decision === "rejected") {
49699
+ throw new Error(`Step "${step.name}" review rejected by "${reviewerDef.name}"`);
49700
+ }
49701
+ this.postToChannel(`**[${step.name}]** Review approved by \`${reviewerDef.name}\``);
49702
+ return reviewOutput;
49703
+ }
49704
+ parseReviewDecision(reviewOutput) {
49705
+ const decisionPattern = /REVIEW_DECISION:\s*(APPROVE|REJECT)/gi;
49706
+ const decisionMatches = [...reviewOutput.matchAll(decisionPattern)];
49707
+ if (decisionMatches.length === 0) {
49708
+ return null;
49709
+ }
49710
+ const outputLikelyContainsEchoedPrompt = reviewOutput.includes("Return exactly") || reviewOutput.includes("REVIEW_DECISION: APPROVE or REJECT");
49711
+ const decisionMatch = outputLikelyContainsEchoedPrompt && decisionMatches.length > 1 ? decisionMatches[decisionMatches.length - 1] : decisionMatches[0];
49712
+ const decision = decisionMatch?.[1]?.toUpperCase();
49713
+ if (decision !== "APPROVE" && decision !== "REJECT") {
49714
+ return null;
49715
+ }
49716
+ const reasonPattern = /REVIEW_REASON:\s*(.+)/gi;
49717
+ const reasonMatches = [...reviewOutput.matchAll(reasonPattern)];
49718
+ const reasonMatch = outputLikelyContainsEchoedPrompt && reasonMatches.length > 1 ? reasonMatches[reasonMatches.length - 1] : reasonMatches[0];
49719
+ const reason = reasonMatch?.[1]?.trim();
49720
+ return {
49721
+ decision: decision === "APPROVE" ? "approved" : "rejected",
49722
+ reason: reason && reason !== "<one sentence>" ? reason : void 0
49723
+ };
49724
+ }
49725
+ combineStepAndReviewOutput(stepOutput, reviewOutput) {
49726
+ const primary = stepOutput.trimEnd();
49727
+ const review = reviewOutput.trim();
49728
+ if (!review)
49729
+ return primary;
49730
+ if (!primary)
49731
+ return `REVIEW_OUTPUT
49732
+ ${review}
49733
+ `;
49734
+ return `${primary}
49735
+
49736
+ ---
49737
+ REVIEW_OUTPUT
49738
+ ${review}
49739
+ `;
49740
+ }
49099
49741
  /**
49100
49742
  * Build the CLI command and arguments for a non-interactive agent execution.
49101
49743
  * Each CLI has a specific flag for one-shot prompt mode.
@@ -49206,7 +49848,7 @@ DO NOT:
49206
49848
  const output = await new Promise((resolve3, reject) => {
49207
49849
  const child = (0, import_node_child_process3.spawn)(cmd, args, {
49208
49850
  stdio: ["ignore", "pipe", "pipe"],
49209
- cwd: agentDef.cwd ? import_node_path8.default.resolve(this.cwd, agentDef.cwd) : this.cwd,
49851
+ cwd: this.resolveAgentCwd(agentDef),
49210
49852
  env: this.getRelayEnv() ?? { ...process.env }
49211
49853
  });
49212
49854
  this.registerWorker(agentName, agentDef.cli, step.task ?? "", child.pid, false);
@@ -49289,17 +49931,18 @@ DO NOT:
49289
49931
  this.unregisterWorker(agentName);
49290
49932
  }
49291
49933
  }
49292
- async spawnAndWait(agentDef, step, timeoutMs) {
49934
+ async spawnAndWait(agentDef, step, timeoutMs, options = {}) {
49293
49935
  if (agentDef.interactive === false) {
49294
49936
  return this.execNonInteractive(agentDef, step, timeoutMs);
49295
49937
  }
49296
49938
  if (!this.relay) {
49297
49939
  throw new Error("AgentRelay not initialized");
49298
49940
  }
49299
- let agentName = `${step.name}-${(this.currentRunId ?? this.generateShortId()).slice(0, 8)}`;
49941
+ const requestedName = `${step.name}${options.agentNameSuffix ? `-${options.agentNameSuffix}` : ""}-${(this.currentRunId ?? this.generateShortId()).slice(0, 8)}`;
49942
+ let agentName = requestedName;
49300
49943
  const role = agentDef.role?.toLowerCase() ?? "";
49301
49944
  const nameLC = agentDef.name.toLowerCase();
49302
- const isHub = _WorkflowRunner.HUB_ROLES.has(nameLC) || [..._WorkflowRunner.HUB_ROLES].some((r) => role.includes(r));
49945
+ const isHub = _WorkflowRunner.HUB_ROLES.has(nameLC) || [..._WorkflowRunner.HUB_ROLES].some((r) => new RegExp(`\\b${r}\\b`).test(role));
49303
49946
  const pattern = this.currentConfig?.swarm.pattern;
49304
49947
  const isHubPattern = pattern && _WorkflowRunner.HUB_PATTERNS.has(pattern);
49305
49948
  const delegationGuidance = isHub || !isHubPattern ? this.buildDelegationGuidance(agentDef.cli, timeoutMs) : "";
@@ -49313,6 +49956,7 @@ DO NOT:
49313
49956
  const stripped = _WorkflowRunner.stripAnsi(chunk);
49314
49957
  this.ptyOutputBuffers.get(agentName)?.push(stripped);
49315
49958
  logStream.write(chunk);
49959
+ options.onChunk?.({ agentName, chunk });
49316
49960
  });
49317
49961
  const agentChannels = this.channel ? [this.channel] : agentDef.channels;
49318
49962
  let agent;
@@ -49320,6 +49964,7 @@ DO NOT:
49320
49964
  let stopHeartbeat;
49321
49965
  let ptyChunks = [];
49322
49966
  try {
49967
+ const agentCwd = this.resolveAgentCwd(agentDef);
49323
49968
  agent = await this.relay.spawnPty({
49324
49969
  name: agentName,
49325
49970
  cli: agentDef.cli,
@@ -49328,7 +49973,7 @@ DO NOT:
49328
49973
  channels: agentChannels,
49329
49974
  task: taskWithExit,
49330
49975
  idleThresholdSecs: agentDef.constraints?.idleThresholdSecs,
49331
- cwd: agentDef.cwd ? import_node_path8.default.resolve(this.cwd, agentDef.cwd) : void 0
49976
+ cwd: agentCwd !== this.cwd ? agentCwd : void 0
49332
49977
  });
49333
49978
  if (agent.name !== agentName) {
49334
49979
  const oldName = agentName;
@@ -49354,10 +49999,12 @@ DO NOT:
49354
49999
  const stripped = _WorkflowRunner.stripAnsi(chunk);
49355
50000
  this.ptyOutputBuffers.get(agent.name)?.push(stripped);
49356
50001
  newLogStream.write(chunk);
50002
+ options.onChunk?.({ agentName: agent.name, chunk });
49357
50003
  });
49358
50004
  }
49359
50005
  agentName = agent.name;
49360
50006
  }
50007
+ await options.onSpawned?.({ requestedName, actualName: agent.name, agent });
49361
50008
  let workerPid;
49362
50009
  try {
49363
50010
  const rawAgents = await this.relay.listAgentsRaw();
@@ -49410,6 +50057,7 @@ DO NOT:
49410
50057
  this.ptyLogStreams.delete(agentName);
49411
50058
  }
49412
50059
  this.unregisterWorker(agentName);
50060
+ this.supervisedRuntimeAgents.delete(agentName);
49413
50061
  }
49414
50062
  let output;
49415
50063
  if (ptyChunks.length > 0) {
@@ -49535,7 +50183,7 @@ DO NOT:
49535
50183
  continue;
49536
50184
  const role = agentDef.role?.toLowerCase() ?? "";
49537
50185
  const nameLC = agentDef.name.toLowerCase();
49538
- if (_WorkflowRunner.HUB_ROLES.has(nameLC) || [..._WorkflowRunner.HUB_ROLES].some((r) => role.includes(r))) {
50186
+ if (_WorkflowRunner.HUB_ROLES.has(nameLC) || [..._WorkflowRunner.HUB_ROLES].some((r) => new RegExp(`\\b${r}\\b`).test(role))) {
49539
50187
  const handle = this.activeAgentHandles.get(agentDef.name);
49540
50188
  if (handle)
49541
50189
  return handle;