agent-relay 3.2.0 → 3.2.2

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 (79) hide show
  1. package/bin/agent-relay-broker-darwin-arm64 +0 -0
  2. package/bin/agent-relay-broker-darwin-x64 +0 -0
  3. package/bin/agent-relay-broker-linux-arm64 +0 -0
  4. package/bin/agent-relay-broker-linux-x64 +0 -0
  5. package/dist/index.cjs +1421 -246
  6. package/dist/src/cli/commands/core.d.ts +1 -0
  7. package/dist/src/cli/commands/core.d.ts.map +1 -1
  8. package/dist/src/cli/commands/core.js +18 -0
  9. package/dist/src/cli/commands/core.js.map +1 -1
  10. package/dist/src/cli/lib/broker-lifecycle.d.ts.map +1 -1
  11. package/dist/src/cli/lib/broker-lifecycle.js +16 -13
  12. package/dist/src/cli/lib/broker-lifecycle.js.map +1 -1
  13. package/dist/src/cli/relaycast-mcp.d.ts +4 -0
  14. package/dist/src/cli/relaycast-mcp.d.ts.map +1 -1
  15. package/dist/src/cli/relaycast-mcp.js +4 -4
  16. package/dist/src/cli/relaycast-mcp.js.map +1 -1
  17. package/package.json +8 -8
  18. package/packages/acp-bridge/package.json +2 -2
  19. package/packages/config/package.json +1 -1
  20. package/packages/hooks/package.json +4 -4
  21. package/packages/memory/package.json +2 -2
  22. package/packages/openclaw/README.md +2 -2
  23. package/packages/openclaw/dist/identity/files.js +2 -2
  24. package/packages/openclaw/dist/identity/files.js.map +1 -1
  25. package/packages/openclaw/dist/setup.js +2 -2
  26. package/packages/openclaw/package.json +2 -2
  27. package/packages/openclaw/skill/SKILL.md +8 -8
  28. package/packages/openclaw/src/identity/files.ts +2 -2
  29. package/packages/openclaw/src/setup.ts +2 -2
  30. package/packages/openclaw/templates/SOUL.md.template +2 -2
  31. package/packages/policy/package.json +2 -2
  32. package/packages/sdk/dist/__tests__/completion-pipeline.test.d.ts +14 -0
  33. package/packages/sdk/dist/__tests__/completion-pipeline.test.d.ts.map +1 -0
  34. package/packages/sdk/dist/__tests__/completion-pipeline.test.js +1476 -0
  35. package/packages/sdk/dist/__tests__/completion-pipeline.test.js.map +1 -0
  36. package/packages/sdk/dist/__tests__/e2e-owner-review.test.js +2 -2
  37. package/packages/sdk/dist/__tests__/e2e-owner-review.test.js.map +1 -1
  38. package/packages/sdk/dist/examples/example.js +1 -1
  39. package/packages/sdk/dist/examples/example.js.map +1 -1
  40. package/packages/sdk/dist/relay-adapter.js +4 -4
  41. package/packages/sdk/dist/relay-adapter.js.map +1 -1
  42. package/packages/sdk/dist/workflows/builder.d.ts +18 -3
  43. package/packages/sdk/dist/workflows/builder.d.ts.map +1 -1
  44. package/packages/sdk/dist/workflows/builder.js +24 -12
  45. package/packages/sdk/dist/workflows/builder.js.map +1 -1
  46. package/packages/sdk/dist/workflows/runner.d.ts +55 -2
  47. package/packages/sdk/dist/workflows/runner.d.ts.map +1 -1
  48. package/packages/sdk/dist/workflows/runner.js +1370 -108
  49. package/packages/sdk/dist/workflows/runner.js.map +1 -1
  50. package/packages/sdk/dist/workflows/trajectory.d.ts +6 -2
  51. package/packages/sdk/dist/workflows/trajectory.d.ts.map +1 -1
  52. package/packages/sdk/dist/workflows/trajectory.js +37 -2
  53. package/packages/sdk/dist/workflows/trajectory.js.map +1 -1
  54. package/packages/sdk/dist/workflows/types.d.ts +88 -0
  55. package/packages/sdk/dist/workflows/types.d.ts.map +1 -1
  56. package/packages/sdk/dist/workflows/types.js.map +1 -1
  57. package/packages/sdk/dist/workflows/validator.js +1 -1
  58. package/packages/sdk/dist/workflows/validator.js.map +1 -1
  59. package/packages/sdk/package.json +2 -2
  60. package/packages/sdk/src/__tests__/completion-pipeline.test.ts +1820 -0
  61. package/packages/sdk/src/__tests__/e2e-owner-review.test.ts +2 -2
  62. package/packages/sdk/src/__tests__/idle-nudge.test.ts +68 -0
  63. package/packages/sdk/src/__tests__/workflow-runner.test.ts +113 -4
  64. package/packages/sdk/src/examples/example.ts +1 -1
  65. package/packages/sdk/src/relay-adapter.ts +4 -4
  66. package/packages/sdk/src/workflows/README.md +43 -11
  67. package/packages/sdk/src/workflows/builder.ts +38 -11
  68. package/packages/sdk/src/workflows/runner.ts +1860 -127
  69. package/packages/sdk/src/workflows/schema.json +6 -0
  70. package/packages/sdk/src/workflows/trajectory.ts +52 -3
  71. package/packages/sdk/src/workflows/types.ts +149 -0
  72. package/packages/sdk/src/workflows/validator.ts +1 -1
  73. package/packages/sdk-py/pyproject.toml +1 -1
  74. package/packages/telemetry/package.json +1 -1
  75. package/packages/trajectory/package.json +2 -2
  76. package/packages/user-directory/package.json +2 -2
  77. package/packages/utils/package.json +2 -2
  78. package/relay-snippets/agent-relay-protocol.md +4 -4
  79. package/relay-snippets/agent-relay-snippet.md +9 -9
package/dist/index.cjs CHANGED
@@ -33227,15 +33227,15 @@ var ShadowManager = class {
33227
33227
  var WORKFLOW_BOOTSTRAP_TASK = "You are connected to Agent Relay. Do not reply to this message and wait for relay messages and respond using Relaycast MCP tools.";
33228
33228
  var WORKFLOW_CONVENTIONS = [
33229
33229
  "Messaging requirements:",
33230
- '- When you receive `Relay message from <sender> ...`, reply using `mcp__relaycast__dm_send(to: "<sender>", text: "...")`.',
33230
+ '- When you receive `Relay message from <sender> ...`, reply using `mcp__relaycast__message_dm_send(to: "<sender>", text: "...")`.',
33231
33231
  "- Send `ACK: ...` when you receive a task.",
33232
33232
  "- Send `DONE: ...` when the task is complete.",
33233
- "- Do not reply only in terminal text; send the response via mcp__relaycast__dm_send.",
33234
- "- Use mcp__relaycast__inbox_check() and mcp__relaycast__agent_list() when context is missing."
33233
+ "- Do not reply only in terminal text; send the response via mcp__relaycast__message_dm_send.",
33234
+ "- Use mcp__relaycast__message_inbox_check() and mcp__relaycast__agent_list() when context is missing."
33235
33235
  ].join("\n");
33236
33236
  function hasWorkflowConventions(task) {
33237
33237
  const lower = task.toLowerCase();
33238
- return lower.includes("mcp__relaycast__dm_send(") || lower.includes("relay_send(") || lower.includes("ack:") && lower.includes("done:");
33238
+ return lower.includes("mcp__relaycast__message_dm_send(") || lower.includes("relay_send(") || lower.includes("ack:") && lower.includes("done:");
33239
33239
  }
33240
33240
  function buildSpawnTask(task, includeWorkflowConventions) {
33241
33241
  const normalized = typeof task === "string" ? task.trim() : "";
@@ -33433,6 +33433,131 @@ var import_promises3 = require("node:fs/promises");
33433
33433
  var import_node_path8 = __toESM(require("node:path"), 1);
33434
33434
  var import_yaml2 = __toESM(require_dist(), 1);
33435
33435
 
33436
+ // packages/sdk/dist/spawn-from-env.js
33437
+ var BYPASS_FLAGS = {
33438
+ claude: { flag: "--dangerously-skip-permissions" },
33439
+ codex: {
33440
+ flag: "--dangerously-bypass-approvals-and-sandbox",
33441
+ aliases: ["--full-auto"]
33442
+ },
33443
+ gemini: {
33444
+ flag: "--yolo",
33445
+ aliases: ["-y"]
33446
+ }
33447
+ };
33448
+ function getBypassFlagConfig(cli) {
33449
+ const baseCli = cli.includes(":") ? cli.split(":")[0] : cli;
33450
+ return BYPASS_FLAGS[baseCli];
33451
+ }
33452
+ function parseSpawnEnv(env = process.env) {
33453
+ const AGENT_NAME = env.AGENT_NAME;
33454
+ const AGENT_CLI = env.AGENT_CLI;
33455
+ const RELAY_API_KEY = env.RELAY_API_KEY;
33456
+ const missing = [];
33457
+ if (!AGENT_NAME)
33458
+ missing.push("AGENT_NAME");
33459
+ if (!AGENT_CLI)
33460
+ missing.push("AGENT_CLI");
33461
+ if (!RELAY_API_KEY)
33462
+ missing.push("RELAY_API_KEY");
33463
+ if (missing.length > 0) {
33464
+ throw new Error(`[spawn-from-env] Missing required environment variables: ${missing.join(", ")}`);
33465
+ }
33466
+ return {
33467
+ AGENT_NAME,
33468
+ AGENT_CLI,
33469
+ RELAY_API_KEY,
33470
+ AGENT_TASK: env.AGENT_TASK || void 0,
33471
+ AGENT_ARGS: env.AGENT_ARGS || void 0,
33472
+ AGENT_CWD: env.AGENT_CWD || void 0,
33473
+ AGENT_CHANNELS: env.AGENT_CHANNELS || void 0,
33474
+ RELAY_BASE_URL: env.RELAY_BASE_URL || void 0,
33475
+ BROKER_BINARY_PATH: env.BROKER_BINARY_PATH || void 0,
33476
+ AGENT_MODEL: env.AGENT_MODEL || void 0,
33477
+ AGENT_DISABLE_DEFAULT_BYPASS: env.AGENT_DISABLE_DEFAULT_BYPASS || void 0
33478
+ };
33479
+ }
33480
+ function parseArgs(raw) {
33481
+ if (!raw)
33482
+ return [];
33483
+ const trimmed = raw.trim();
33484
+ if (trimmed.startsWith("[")) {
33485
+ try {
33486
+ const parsed = JSON.parse(trimmed);
33487
+ if (Array.isArray(parsed))
33488
+ return parsed.map(String);
33489
+ } catch {
33490
+ }
33491
+ }
33492
+ return trimmed.split(/\s+/).filter(Boolean);
33493
+ }
33494
+ function resolveSpawnPolicy(input) {
33495
+ const extraArgs = parseArgs(input.AGENT_ARGS);
33496
+ const channels = input.AGENT_CHANNELS ? input.AGENT_CHANNELS.split(",").map((c) => c.trim()).filter(Boolean) : ["general"];
33497
+ const disableBypass = input.AGENT_DISABLE_DEFAULT_BYPASS === "1";
33498
+ const bypassConfig = getBypassFlagConfig(input.AGENT_CLI);
33499
+ let bypassApplied = false;
33500
+ const args = [...extraArgs];
33501
+ const bypassValues = bypassConfig ? [bypassConfig.flag, ...bypassConfig.aliases ?? []] : [];
33502
+ const hasBypassAlready = bypassValues.some((value) => args.includes(value));
33503
+ if (bypassConfig && !disableBypass && !hasBypassAlready) {
33504
+ args.push(bypassConfig.flag);
33505
+ bypassApplied = true;
33506
+ }
33507
+ return {
33508
+ name: input.AGENT_NAME,
33509
+ cli: input.AGENT_CLI,
33510
+ args,
33511
+ channels,
33512
+ task: input.AGENT_TASK,
33513
+ cwd: input.AGENT_CWD,
33514
+ model: input.AGENT_MODEL,
33515
+ bypassApplied
33516
+ };
33517
+ }
33518
+ async function spawnFromEnv(options = {}) {
33519
+ const env = options.env ?? process.env;
33520
+ const parsed = parseSpawnEnv(env);
33521
+ const policy = resolveSpawnPolicy(parsed);
33522
+ console.log(`[spawn-from-env] Spawning agent: name=${policy.name} cli=${policy.cli} channels=${policy.channels.join(",")} bypass=${policy.bypassApplied}`);
33523
+ if (policy.task) {
33524
+ console.log(`[spawn-from-env] Task: ${policy.task.slice(0, 200)}${policy.task.length > 200 ? "..." : ""}`);
33525
+ }
33526
+ const relay = new AgentRelay({
33527
+ binaryPath: options.binaryPath ?? parsed.BROKER_BINARY_PATH,
33528
+ brokerName: options.brokerName ?? `broker-${policy.name}`,
33529
+ channels: policy.channels,
33530
+ cwd: policy.cwd ?? process.cwd(),
33531
+ env
33532
+ });
33533
+ relay.onAgentSpawned = (agent) => {
33534
+ console.log(`[spawn-from-env] Agent spawned: ${agent.name}`);
33535
+ };
33536
+ relay.onAgentReady = (agent) => {
33537
+ console.log(`[spawn-from-env] Agent ready: ${agent.name}`);
33538
+ };
33539
+ relay.onAgentExited = (agent) => {
33540
+ console.log(`[spawn-from-env] Agent exited: ${agent.name} code=${agent.exitCode ?? "none"} signal=${agent.exitSignal ?? "none"}`);
33541
+ };
33542
+ try {
33543
+ const agent = await relay.spawnPty({
33544
+ name: policy.name,
33545
+ cli: policy.cli,
33546
+ args: policy.args,
33547
+ channels: policy.channels,
33548
+ task: policy.task
33549
+ });
33550
+ const exitReason = await agent.waitForExit();
33551
+ console.log(`[spawn-from-env] Exit reason: ${exitReason}`);
33552
+ return { exitReason, exitCode: agent.exitCode };
33553
+ } catch (err) {
33554
+ console.error(`[spawn-from-env] Error:`, err);
33555
+ throw err;
33556
+ } finally {
33557
+ await relay.shutdown();
33558
+ }
33559
+ }
33560
+
33436
33561
  // packages/sdk/dist/workflows/custom-steps.js
33437
33562
  var import_node_fs2 = require("node:fs");
33438
33563
  var import_node_path6 = __toESM(require("node:path"), 1);
@@ -33906,15 +34031,34 @@ var WorkflowTrajectory = class {
33906
34031
  });
33907
34032
  await this.flush();
33908
34033
  }
34034
+ async stepCompletionDecision(stepName, decision) {
34035
+ if (!this.enabled || !this.trajectory)
34036
+ return;
34037
+ const modeLabel = decision.mode === "marker" ? "marker-based" : `${decision.mode}-based`;
34038
+ const reason = decision.reason ? ` \u2014 ${decision.reason}` : "";
34039
+ const evidence = this.formatCompletionEvidenceSummary(decision.evidence);
34040
+ const evidenceSuffix = evidence ? ` (${evidence})` : "";
34041
+ this.addEvent(decision.mode === "marker" ? "completion-marker" : "completion-evidence", `"${stepName}" ${modeLabel} completion${reason}${evidenceSuffix}`, "medium", {
34042
+ stepName,
34043
+ completionMode: decision.mode,
34044
+ reason: decision.reason,
34045
+ evidence: decision.evidence
34046
+ });
34047
+ await this.flush();
34048
+ }
33909
34049
  /** Record step completed — captures what was accomplished. */
33910
- async stepCompleted(step, output, attempt) {
34050
+ async stepCompleted(step, output, attempt, decision) {
33911
34051
  if (!this.enabled || !this.trajectory)
33912
34052
  return;
33913
34053
  const suffix = attempt > 1 ? ` (after ${attempt} attempts)` : "";
33914
34054
  const lines = output.split("\n").map((l) => l.trim()).filter(Boolean);
33915
34055
  const lastMeaningful = lines.at(-1) ?? "";
33916
34056
  const completion = lastMeaningful.length > 0 && lastMeaningful.length < 100 ? lastMeaningful : output.trim().slice(0, 120) || "(no output)";
33917
- this.addEvent("finding", `"${step.name}" completed${suffix} \u2192 ${completion}`, "medium");
34057
+ if (decision) {
34058
+ await this.stepCompletionDecision(step.name, decision);
34059
+ }
34060
+ const modeSuffix = decision ? ` [${decision.mode}]` : "";
34061
+ this.addEvent("finding", `"${step.name}" completed${suffix}${modeSuffix} \u2192 ${completion}`, "medium");
33918
34062
  await this.flush();
33919
34063
  }
33920
34064
  /** Record step failed — categorizes root cause for actionable diagnosis. */
@@ -34157,6 +34301,22 @@ var WorkflowTrajectory = class {
34157
34301
  event.raw = raw;
34158
34302
  chapter.events.push(event);
34159
34303
  }
34304
+ formatCompletionEvidenceSummary(evidence) {
34305
+ if (!evidence)
34306
+ return void 0;
34307
+ const parts = [];
34308
+ if (evidence.summary)
34309
+ parts.push(evidence.summary);
34310
+ if (evidence.signals?.length)
34311
+ parts.push(`signals=${evidence.signals.join(", ")}`);
34312
+ if (evidence.channelPosts?.length)
34313
+ parts.push(`channel=${evidence.channelPosts.join(" | ")}`);
34314
+ if (evidence.files?.length)
34315
+ parts.push(`files=${evidence.files.join(", ")}`);
34316
+ if (evidence.exitCode !== void 0)
34317
+ parts.push(`exit=${evidence.exitCode}`);
34318
+ return parts.length > 0 ? parts.join("; ") : void 0;
34319
+ }
34160
34320
  async flush() {
34161
34321
  if (!this.trajectory)
34162
34322
  return;
@@ -34196,6 +34356,14 @@ var SpawnExitError = class extends Error {
34196
34356
  this.exitSignal = exitSignal ?? void 0;
34197
34357
  }
34198
34358
  };
34359
+ var WorkflowCompletionError = class extends Error {
34360
+ completionReason;
34361
+ constructor(message, completionReason) {
34362
+ super(message);
34363
+ this.name = "WorkflowCompletionError";
34364
+ this.completionReason = completionReason;
34365
+ }
34366
+ };
34199
34367
  var _resolvedCursorCli;
34200
34368
  function resolveCursorCli() {
34201
34369
  if (_resolvedCursorCli !== void 0)
@@ -34259,8 +34427,16 @@ var WorkflowRunner = class _WorkflowRunner {
34259
34427
  lastActivity = /* @__PURE__ */ new Map();
34260
34428
  /** Runtime-name lookup for agents participating in supervised owner flows. */
34261
34429
  supervisedRuntimeAgents = /* @__PURE__ */ new Map();
34430
+ /** Runtime-name lookup for active step agents so channel messages can be attributed to a step. */
34431
+ runtimeStepAgents = /* @__PURE__ */ new Map();
34432
+ /** Per-step completion evidence collected across output, channel, files, and tool side-effects. */
34433
+ stepCompletionEvidence = /* @__PURE__ */ new Map();
34434
+ /** Expected owner/worker identities per step so coordination signals can be validated by sender. */
34435
+ stepSignalParticipants = /* @__PURE__ */ new Map();
34262
34436
  /** Resolved named paths from the top-level `paths` config, keyed by name → absolute directory. */
34263
34437
  resolvedPaths = /* @__PURE__ */ new Map();
34438
+ /** Tracks agent names currently assigned as reviewers (ref-counted to handle concurrent usage). */
34439
+ activeReviewers = /* @__PURE__ */ new Map();
34264
34440
  constructor(options = {}) {
34265
34441
  this.db = options.db ?? new InMemoryWorkflowDb();
34266
34442
  this.workspaceId = options.workspaceId ?? "local";
@@ -34339,6 +34515,423 @@ var WorkflowRunner = class _WorkflowRunner {
34339
34515
  }
34340
34516
  return resolved;
34341
34517
  }
34518
+ static EVIDENCE_IGNORED_DIRS = /* @__PURE__ */ new Set([
34519
+ ".git",
34520
+ ".agent-relay",
34521
+ ".trajectories",
34522
+ "node_modules"
34523
+ ]);
34524
+ getStepCompletionEvidence(stepName) {
34525
+ const record2 = this.stepCompletionEvidence.get(stepName);
34526
+ if (!record2)
34527
+ return void 0;
34528
+ const evidence = structuredClone(record2.evidence);
34529
+ return this.filterStepEvidenceBySignalProvenance(stepName, evidence);
34530
+ }
34531
+ getOrCreateStepEvidenceRecord(stepName) {
34532
+ const existing = this.stepCompletionEvidence.get(stepName);
34533
+ if (existing)
34534
+ return existing;
34535
+ const now = (/* @__PURE__ */ new Date()).toISOString();
34536
+ const record2 = {
34537
+ evidence: {
34538
+ stepName,
34539
+ lastUpdatedAt: now,
34540
+ roots: [],
34541
+ output: {
34542
+ stdout: "",
34543
+ stderr: "",
34544
+ combined: ""
34545
+ },
34546
+ channelPosts: [],
34547
+ files: [],
34548
+ process: {},
34549
+ toolSideEffects: [],
34550
+ coordinationSignals: []
34551
+ },
34552
+ baselineSnapshots: /* @__PURE__ */ new Map(),
34553
+ filesCaptured: false
34554
+ };
34555
+ this.stepCompletionEvidence.set(stepName, record2);
34556
+ return record2;
34557
+ }
34558
+ initializeStepSignalParticipants(stepName, ownerSender, workerSender) {
34559
+ this.stepSignalParticipants.set(stepName, {
34560
+ ownerSenders: /* @__PURE__ */ new Set(),
34561
+ workerSenders: /* @__PURE__ */ new Set()
34562
+ });
34563
+ this.rememberStepSignalSender(stepName, "owner", ownerSender);
34564
+ this.rememberStepSignalSender(stepName, "worker", workerSender);
34565
+ }
34566
+ rememberStepSignalSender(stepName, participant, ...senders) {
34567
+ const participants = this.stepSignalParticipants.get(stepName) ?? {
34568
+ ownerSenders: /* @__PURE__ */ new Set(),
34569
+ workerSenders: /* @__PURE__ */ new Set()
34570
+ };
34571
+ this.stepSignalParticipants.set(stepName, participants);
34572
+ const target = participant === "owner" ? participants.ownerSenders : participants.workerSenders;
34573
+ for (const sender of senders) {
34574
+ const trimmed = sender?.trim();
34575
+ if (trimmed)
34576
+ target.add(trimmed);
34577
+ }
34578
+ }
34579
+ resolveSignalParticipantKind(role) {
34580
+ const roleLC = role?.toLowerCase().trim();
34581
+ if (!roleLC)
34582
+ return void 0;
34583
+ if (/\b(owner|lead|supervisor)\b/.test(roleLC))
34584
+ return "owner";
34585
+ if (/\b(worker|specialist|engineer|implementer)\b/.test(roleLC))
34586
+ return "worker";
34587
+ return void 0;
34588
+ }
34589
+ isSignalFromExpectedSender(stepName, signal) {
34590
+ const expectedParticipant = signal.kind === "worker_done" ? "worker" : signal.kind === "lead_done" ? "owner" : void 0;
34591
+ if (!expectedParticipant)
34592
+ return true;
34593
+ const participants = this.stepSignalParticipants.get(stepName);
34594
+ if (!participants)
34595
+ return true;
34596
+ const allowedSenders = expectedParticipant === "owner" ? participants.ownerSenders : participants.workerSenders;
34597
+ if (allowedSenders.size === 0)
34598
+ return true;
34599
+ const sender = signal.sender ?? signal.actor;
34600
+ if (sender) {
34601
+ return allowedSenders.has(sender);
34602
+ }
34603
+ const observedParticipant = this.resolveSignalParticipantKind(signal.role);
34604
+ if (observedParticipant) {
34605
+ return observedParticipant === expectedParticipant;
34606
+ }
34607
+ return signal.source !== "channel";
34608
+ }
34609
+ filterStepEvidenceBySignalProvenance(stepName, evidence) {
34610
+ evidence.channelPosts = evidence.channelPosts.map((post) => {
34611
+ const signals = post.signals.filter((signal) => this.isSignalFromExpectedSender(stepName, signal));
34612
+ return {
34613
+ ...post,
34614
+ completionRelevant: signals.length > 0,
34615
+ signals
34616
+ };
34617
+ });
34618
+ evidence.coordinationSignals = evidence.coordinationSignals.filter((signal) => this.isSignalFromExpectedSender(stepName, signal));
34619
+ return evidence;
34620
+ }
34621
+ beginStepEvidence(stepName, roots, startedAt) {
34622
+ const record2 = this.getOrCreateStepEvidenceRecord(stepName);
34623
+ const evidence = record2.evidence;
34624
+ const now = startedAt ?? (/* @__PURE__ */ new Date()).toISOString();
34625
+ evidence.startedAt ??= now;
34626
+ evidence.status = "running";
34627
+ evidence.lastUpdatedAt = now;
34628
+ for (const root of this.uniqueEvidenceRoots(roots)) {
34629
+ if (!evidence.roots.includes(root)) {
34630
+ evidence.roots.push(root);
34631
+ }
34632
+ if (!record2.baselineSnapshots.has(root)) {
34633
+ record2.baselineSnapshots.set(root, this.captureFileSnapshot(root));
34634
+ }
34635
+ }
34636
+ }
34637
+ captureStepTerminalEvidence(stepName, output, process3, meta3) {
34638
+ const record2 = this.getOrCreateStepEvidenceRecord(stepName);
34639
+ const evidence = record2.evidence;
34640
+ const observedAt = (/* @__PURE__ */ new Date()).toISOString();
34641
+ const append = (current, next) => {
34642
+ if (!next)
34643
+ return current;
34644
+ return current ? `${current}
34645
+ ${next}` : next;
34646
+ };
34647
+ if (output.stdout) {
34648
+ evidence.output.stdout = append(evidence.output.stdout, output.stdout);
34649
+ for (const signal of this.extractCompletionSignals(output.stdout, "stdout", observedAt, meta3)) {
34650
+ evidence.coordinationSignals.push(signal);
34651
+ }
34652
+ }
34653
+ if (output.stderr) {
34654
+ evidence.output.stderr = append(evidence.output.stderr, output.stderr);
34655
+ for (const signal of this.extractCompletionSignals(output.stderr, "stderr", observedAt, meta3)) {
34656
+ evidence.coordinationSignals.push(signal);
34657
+ }
34658
+ }
34659
+ const combinedOutput = output.combined ?? [output.stdout, output.stderr].filter((value) => Boolean(value)).join("\n");
34660
+ if (combinedOutput) {
34661
+ evidence.output.combined = append(evidence.output.combined, combinedOutput);
34662
+ }
34663
+ if (process3) {
34664
+ if (process3.exitCode !== void 0) {
34665
+ evidence.process.exitCode = process3.exitCode;
34666
+ evidence.coordinationSignals.push({
34667
+ kind: "process_exit",
34668
+ source: "process",
34669
+ text: `Process exited with code ${process3.exitCode}`,
34670
+ observedAt,
34671
+ value: String(process3.exitCode)
34672
+ });
34673
+ }
34674
+ if (process3.exitSignal !== void 0) {
34675
+ evidence.process.exitSignal = process3.exitSignal;
34676
+ }
34677
+ }
34678
+ evidence.lastUpdatedAt = observedAt;
34679
+ }
34680
+ finalizeStepEvidence(stepName, status, completedAt, completionReason) {
34681
+ const record2 = this.stepCompletionEvidence.get(stepName);
34682
+ if (!record2)
34683
+ return;
34684
+ const evidence = record2.evidence;
34685
+ const observedAt = completedAt ?? (/* @__PURE__ */ new Date()).toISOString();
34686
+ evidence.status = status;
34687
+ if (status !== "running") {
34688
+ evidence.completedAt = observedAt;
34689
+ }
34690
+ evidence.lastUpdatedAt = observedAt;
34691
+ if (!record2.filesCaptured) {
34692
+ const existing = new Set(evidence.files.map((file2) => `${file2.kind}:${file2.path}`));
34693
+ for (const root of evidence.roots) {
34694
+ const before = record2.baselineSnapshots.get(root) ?? /* @__PURE__ */ new Map();
34695
+ const after = this.captureFileSnapshot(root);
34696
+ for (const change of this.diffFileSnapshots(before, after, root, observedAt)) {
34697
+ const key = `${change.kind}:${change.path}`;
34698
+ if (existing.has(key))
34699
+ continue;
34700
+ existing.add(key);
34701
+ evidence.files.push(change);
34702
+ }
34703
+ }
34704
+ record2.filesCaptured = true;
34705
+ }
34706
+ if (completionReason) {
34707
+ const decision = this.buildStepCompletionDecision(stepName, completionReason);
34708
+ if (decision) {
34709
+ void this.trajectory?.stepCompletionDecision(stepName, decision);
34710
+ }
34711
+ }
34712
+ }
34713
+ recordStepToolSideEffect(stepName, effect) {
34714
+ const record2 = this.getOrCreateStepEvidenceRecord(stepName);
34715
+ const observedAt = effect.observedAt ?? (/* @__PURE__ */ new Date()).toISOString();
34716
+ record2.evidence.toolSideEffects.push({
34717
+ ...effect,
34718
+ observedAt
34719
+ });
34720
+ record2.evidence.lastUpdatedAt = observedAt;
34721
+ }
34722
+ recordChannelEvidence(text, options = {}) {
34723
+ const stepName = options.stepName ?? this.inferStepNameFromChannelText(text) ?? (options.actor ? this.runtimeStepAgents.get(options.actor)?.stepName : void 0);
34724
+ if (!stepName)
34725
+ return;
34726
+ const record2 = this.getOrCreateStepEvidenceRecord(stepName);
34727
+ const postedAt = (/* @__PURE__ */ new Date()).toISOString();
34728
+ const sender = options.sender ?? options.actor;
34729
+ const signals = this.extractCompletionSignals(text, "channel", postedAt, {
34730
+ sender,
34731
+ actor: options.actor,
34732
+ role: options.role
34733
+ });
34734
+ const channelPost = {
34735
+ stepName,
34736
+ text,
34737
+ postedAt,
34738
+ origin: options.origin ?? "runner_post",
34739
+ completionRelevant: signals.length > 0,
34740
+ sender,
34741
+ actor: options.actor,
34742
+ role: options.role,
34743
+ target: options.target,
34744
+ signals
34745
+ };
34746
+ record2.evidence.channelPosts.push(channelPost);
34747
+ record2.evidence.coordinationSignals.push(...signals);
34748
+ record2.evidence.lastUpdatedAt = postedAt;
34749
+ }
34750
+ extractCompletionSignals(text, source, observedAt, meta3) {
34751
+ const signals = [];
34752
+ const seen = /* @__PURE__ */ new Set();
34753
+ const add = (kind, signalText, value) => {
34754
+ const trimmed = signalText.trim().slice(0, 280);
34755
+ if (!trimmed)
34756
+ return;
34757
+ const key = `${kind}:${trimmed}:${value ?? ""}`;
34758
+ if (seen.has(key))
34759
+ return;
34760
+ seen.add(key);
34761
+ signals.push({
34762
+ kind,
34763
+ source,
34764
+ text: trimmed,
34765
+ observedAt,
34766
+ sender: meta3?.sender,
34767
+ actor: meta3?.actor,
34768
+ role: meta3?.role,
34769
+ value
34770
+ });
34771
+ };
34772
+ for (const match of text.matchAll(/\bWORKER_DONE\b(?::\s*([^\n]+))?/gi)) {
34773
+ add("worker_done", match[0], match[1]?.trim());
34774
+ }
34775
+ for (const match of text.matchAll(/\bLEAD_DONE\b(?::\s*([^\n]+))?/gi)) {
34776
+ add("lead_done", match[0], match[1]?.trim());
34777
+ }
34778
+ for (const match of text.matchAll(/\bSTEP_COMPLETE:([A-Za-z0-9_.:-]+)/g)) {
34779
+ add("step_complete", match[0], match[1]);
34780
+ }
34781
+ for (const match of text.matchAll(/\bOWNER_DECISION:\s*(COMPLETE|INCOMPLETE_RETRY|INCOMPLETE_FAIL|NEEDS_CLARIFICATION)\b/gi)) {
34782
+ add("owner_decision", match[0], match[1].toUpperCase());
34783
+ }
34784
+ for (const match of text.matchAll(/\bREVIEW_DECISION:\s*(APPROVE|REJECT)\b/gi)) {
34785
+ add("review_decision", match[0], match[1].toUpperCase());
34786
+ }
34787
+ if (/\bverification gate observed\b|\bverification passed\b/i.test(text)) {
34788
+ add("verification_passed", this.firstMeaningfulLine(text) ?? text);
34789
+ }
34790
+ if (/\bverification failed\b/i.test(text)) {
34791
+ add("verification_failed", this.firstMeaningfulLine(text) ?? text);
34792
+ }
34793
+ if (/\b(summary|handoff|ready for review|ready for handoff|task complete|work complete|completed work|finished work)\b/i.test(text)) {
34794
+ add("task_summary", this.firstMeaningfulLine(text) ?? text);
34795
+ }
34796
+ return signals;
34797
+ }
34798
+ inferStepNameFromChannelText(text) {
34799
+ const bracketMatch = text.match(/^\*\*\[([^\]]+)\]/);
34800
+ if (bracketMatch?.[1])
34801
+ return bracketMatch[1];
34802
+ const markerMatch = text.match(/\bSTEP_COMPLETE:([A-Za-z0-9_.:-]+)/);
34803
+ if (markerMatch?.[1])
34804
+ return markerMatch[1];
34805
+ return void 0;
34806
+ }
34807
+ uniqueEvidenceRoots(roots) {
34808
+ return [...new Set(roots.filter((root) => Boolean(root)).map((root) => import_node_path8.default.resolve(root)))];
34809
+ }
34810
+ captureFileSnapshot(root) {
34811
+ const snapshot = /* @__PURE__ */ new Map();
34812
+ if (!(0, import_node_fs4.existsSync)(root))
34813
+ return snapshot;
34814
+ const visit = (currentPath) => {
34815
+ let entries;
34816
+ try {
34817
+ entries = (0, import_node_fs4.readdirSync)(currentPath, { withFileTypes: true });
34818
+ } catch {
34819
+ return;
34820
+ }
34821
+ for (const entry of entries) {
34822
+ if (entry.isDirectory() && _WorkflowRunner.EVIDENCE_IGNORED_DIRS.has(entry.name)) {
34823
+ continue;
34824
+ }
34825
+ const fullPath = import_node_path8.default.join(currentPath, entry.name);
34826
+ if (entry.isDirectory()) {
34827
+ visit(fullPath);
34828
+ continue;
34829
+ }
34830
+ try {
34831
+ const stats = (0, import_node_fs4.statSync)(fullPath);
34832
+ if (!stats.isFile())
34833
+ continue;
34834
+ snapshot.set(fullPath, { mtimeMs: stats.mtimeMs, size: stats.size });
34835
+ } catch {
34836
+ }
34837
+ }
34838
+ };
34839
+ try {
34840
+ const stats = (0, import_node_fs4.statSync)(root);
34841
+ if (stats.isFile()) {
34842
+ snapshot.set(root, { mtimeMs: stats.mtimeMs, size: stats.size });
34843
+ return snapshot;
34844
+ }
34845
+ } catch {
34846
+ return snapshot;
34847
+ }
34848
+ visit(root);
34849
+ return snapshot;
34850
+ }
34851
+ diffFileSnapshots(before, after, root, observedAt) {
34852
+ const allPaths = /* @__PURE__ */ new Set([...before.keys(), ...after.keys()]);
34853
+ const changes = [];
34854
+ for (const filePath of allPaths) {
34855
+ const prior = before.get(filePath);
34856
+ const next = after.get(filePath);
34857
+ let kind;
34858
+ if (!prior && next) {
34859
+ kind = "created";
34860
+ } else if (prior && !next) {
34861
+ kind = "deleted";
34862
+ } else if (prior && next && (prior.mtimeMs !== next.mtimeMs || prior.size !== next.size)) {
34863
+ kind = "modified";
34864
+ }
34865
+ if (!kind)
34866
+ continue;
34867
+ changes.push({
34868
+ path: this.normalizeEvidencePath(filePath),
34869
+ kind,
34870
+ observedAt,
34871
+ root
34872
+ });
34873
+ }
34874
+ return changes.sort((a, b) => a.path.localeCompare(b.path));
34875
+ }
34876
+ normalizeEvidencePath(filePath) {
34877
+ const relative = import_node_path8.default.relative(this.cwd, filePath);
34878
+ if (!relative || relative === "")
34879
+ return import_node_path8.default.basename(filePath);
34880
+ return relative.startsWith("..") ? filePath : relative;
34881
+ }
34882
+ buildStepCompletionDecision(stepName, completionReason) {
34883
+ let reason;
34884
+ let mode;
34885
+ switch (completionReason) {
34886
+ case "completed_verified":
34887
+ mode = "verification";
34888
+ reason = "Verification passed";
34889
+ break;
34890
+ case "completed_by_evidence":
34891
+ mode = "evidence";
34892
+ reason = "Completion inferred from collected evidence";
34893
+ break;
34894
+ case "completed_by_owner_decision": {
34895
+ const evidence = this.getStepCompletionEvidence(stepName);
34896
+ const markerObserved = evidence?.coordinationSignals.some((signal) => signal.kind === "step_complete");
34897
+ mode = markerObserved ? "marker" : "owner_decision";
34898
+ reason = markerObserved ? "Legacy STEP_COMPLETE marker observed" : "Owner approved completion";
34899
+ break;
34900
+ }
34901
+ default:
34902
+ return void 0;
34903
+ }
34904
+ return {
34905
+ mode,
34906
+ reason,
34907
+ evidence: this.buildTrajectoryCompletionEvidence(stepName)
34908
+ };
34909
+ }
34910
+ buildTrajectoryCompletionEvidence(stepName) {
34911
+ const evidence = this.getStepCompletionEvidence(stepName);
34912
+ if (!evidence)
34913
+ return void 0;
34914
+ const signals = evidence.coordinationSignals.slice(-6).map((signal) => signal.value ?? signal.text);
34915
+ const channelPosts = evidence.channelPosts.filter((post) => post.completionRelevant).slice(-3).map((post) => post.text.slice(0, 160));
34916
+ const files = evidence.files.slice(0, 6).map((file2) => `${file2.kind}:${file2.path}`);
34917
+ const summaryParts = [];
34918
+ if (signals.length > 0)
34919
+ summaryParts.push(`${signals.length} signal(s)`);
34920
+ if (channelPosts.length > 0)
34921
+ summaryParts.push(`${channelPosts.length} relevant channel post(s)`);
34922
+ if (files.length > 0)
34923
+ summaryParts.push(`${files.length} file change(s)`);
34924
+ if (evidence.process.exitCode !== void 0) {
34925
+ summaryParts.push(`exit=${evidence.process.exitCode}`);
34926
+ }
34927
+ return {
34928
+ summary: summaryParts.length > 0 ? summaryParts.join(", ") : void 0,
34929
+ signals: signals.length > 0 ? signals : void 0,
34930
+ channelPosts: channelPosts.length > 0 ? channelPosts : void 0,
34931
+ files: files.length > 0 ? files : void 0,
34932
+ exitCode: evidence.process.exitCode
34933
+ };
34934
+ }
34342
34935
  // ── Progress logging ────────────────────────────────────────────────────
34343
34936
  /** Log a progress message with elapsed time since run start. */
34344
34937
  log(msg) {
@@ -35064,9 +35657,11 @@ ${err.suggestion}`);
35064
35657
  if (state.row.status === "failed") {
35065
35658
  state.row.status = "pending";
35066
35659
  state.row.error = void 0;
35660
+ state.row.completionReason = void 0;
35067
35661
  await this.db.updateStep(state.row.id, {
35068
35662
  status: "pending",
35069
35663
  error: void 0,
35664
+ completionReason: void 0,
35070
35665
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
35071
35666
  });
35072
35667
  }
@@ -35085,6 +35680,8 @@ ${err.suggestion}`);
35085
35680
  this.currentConfig = config2;
35086
35681
  this.currentRunId = runId;
35087
35682
  this.runStartTime = Date.now();
35683
+ this.runtimeStepAgents.clear();
35684
+ this.stepCompletionEvidence.clear();
35088
35685
  this.log(`Starting workflow "${workflow2.name}" (${workflow2.steps.length} steps)`);
35089
35686
  this.trajectory = new WorkflowTrajectory(config2.trajectories, runId, this.cwd);
35090
35687
  try {
@@ -35188,8 +35785,24 @@ ${err.suggestion}`);
35188
35785
  const fromShort = msg.from.replace(/-[a-f0-9]{6,}$/, "");
35189
35786
  const toShort = msg.to.replace(/-[a-f0-9]{6,}$/, "");
35190
35787
  this.log(`[msg] ${fromShort} \u2192 ${toShort}: ${body}`);
35788
+ if (this.channel && (msg.to === this.channel || msg.to === `#${this.channel}`)) {
35789
+ const runtimeAgent = this.runtimeStepAgents.get(msg.from);
35790
+ this.recordChannelEvidence(msg.text, {
35791
+ sender: runtimeAgent?.logicalName ?? msg.from,
35792
+ actor: msg.from,
35793
+ role: runtimeAgent?.role,
35794
+ target: msg.to,
35795
+ origin: "relay_message",
35796
+ stepName: runtimeAgent?.stepName
35797
+ });
35798
+ }
35191
35799
  const supervision = this.supervisedRuntimeAgents.get(msg.from);
35192
35800
  if (supervision?.role === "owner") {
35801
+ this.recordStepToolSideEffect(supervision.stepName, {
35802
+ type: "owner_monitoring",
35803
+ detail: `Owner messaged ${msg.to}: ${msg.text.slice(0, 120)}`,
35804
+ raw: { to: msg.to, text: msg.text }
35805
+ });
35193
35806
  void this.trajectory?.ownerMonitoringEvent(supervision.stepName, supervision.logicalName, `Messaged ${msg.to}: ${msg.text.slice(0, 120)}`, { to: msg.to, text: msg.text });
35194
35807
  }
35195
35808
  };
@@ -35333,6 +35946,7 @@ ${err.suggestion}`);
35333
35946
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
35334
35947
  });
35335
35948
  this.emit({ type: "step:failed", runId, stepName, error: "Cancelled" });
35949
+ this.finalizeStepEvidence(stepName, "failed");
35336
35950
  }
35337
35951
  }
35338
35952
  this.emit({ type: "run:cancelled", runId });
@@ -35370,6 +35984,8 @@ ${err.suggestion}`);
35370
35984
  this.lastIdleLog.clear();
35371
35985
  this.lastActivity.clear();
35372
35986
  this.supervisedRuntimeAgents.clear();
35987
+ this.runtimeStepAgents.clear();
35988
+ this.activeReviewers.clear();
35373
35989
  this.log("Shutting down broker...");
35374
35990
  await this.relay?.shutdown();
35375
35991
  this.relay = void 0;
@@ -35457,7 +36073,8 @@ ${err.suggestion}`);
35457
36073
  status: state?.row.status === "completed" ? "completed" : "failed",
35458
36074
  attempts: (state?.row.retryCount ?? 0) + 1,
35459
36075
  output: state?.row.output,
35460
- verificationPassed: state?.row.status === "completed" && step.verification !== void 0
36076
+ verificationPassed: state?.row.status === "completed" && step.verification !== void 0,
36077
+ completionMode: state?.row.completionReason ? this.buildStepCompletionDecision(step.name, state.row.completionReason)?.mode : void 0
35461
36078
  });
35462
36079
  }
35463
36080
  }
@@ -35604,11 +36221,21 @@ ${trimmedOutput.slice(0, 200)}`);
35604
36221
  const maxRetries = step.retries ?? errorHandling?.maxRetries ?? 0;
35605
36222
  const retryDelay = errorHandling?.retryDelayMs ?? 1e3;
35606
36223
  let lastError;
36224
+ let lastCompletionReason;
36225
+ let lastExitCode;
36226
+ let lastExitSignal;
35607
36227
  for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
35608
36228
  this.checkAborted();
36229
+ lastExitCode = void 0;
36230
+ lastExitSignal = void 0;
35609
36231
  if (attempt > 0) {
35610
36232
  this.emit({ type: "step:retrying", runId, stepName: step.name, attempt });
35611
36233
  this.postToChannel(`**[${step.name}]** Retrying (attempt ${attempt + 1}/${maxRetries + 1})`);
36234
+ this.recordStepToolSideEffect(step.name, {
36235
+ type: "retry",
36236
+ detail: `Retrying attempt ${attempt + 1}/${maxRetries + 1}`,
36237
+ raw: { attempt, maxRetries }
36238
+ });
35612
36239
  state.row.retryCount = attempt;
35613
36240
  await this.db.updateStep(state.row.id, {
35614
36241
  retryCount: attempt,
@@ -35617,9 +36244,13 @@ ${trimmedOutput.slice(0, 200)}`);
35617
36244
  await this.delay(retryDelay);
35618
36245
  }
35619
36246
  state.row.status = "running";
36247
+ state.row.error = void 0;
36248
+ state.row.completionReason = void 0;
35620
36249
  state.row.startedAt = (/* @__PURE__ */ new Date()).toISOString();
35621
36250
  await this.db.updateStep(state.row.id, {
35622
36251
  status: "running",
36252
+ error: void 0,
36253
+ completionReason: void 0,
35623
36254
  startedAt: state.row.startedAt,
35624
36255
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
35625
36256
  });
@@ -35634,30 +36265,36 @@ ${trimmedOutput.slice(0, 200)}`);
35634
36265
  return value !== void 0 ? String(value) : _match;
35635
36266
  });
35636
36267
  const stepCwd = this.resolveStepWorkdir(step) ?? this.cwd;
36268
+ this.beginStepEvidence(step.name, [stepCwd], state.row.startedAt);
35637
36269
  try {
35638
36270
  if (this.executor?.executeDeterministicStep) {
35639
36271
  const result = await this.executor.executeDeterministicStep(step, resolvedCommand, stepCwd);
36272
+ lastExitCode = result.exitCode;
35640
36273
  const failOnError = step.failOnError !== false;
35641
36274
  if (failOnError && result.exitCode !== 0) {
35642
36275
  throw new Error(`Command failed with exit code ${result.exitCode}: ${result.output.slice(0, 500)}`);
35643
36276
  }
35644
36277
  const output2 = step.captureOutput !== false ? result.output : `Command completed (exit code ${result.exitCode})`;
35645
- if (step.verification) {
35646
- this.runVerification(step.verification, output2, step.name);
35647
- }
36278
+ this.captureStepTerminalEvidence(step.name, { stdout: result.output, combined: result.output }, { exitCode: result.exitCode });
36279
+ const verificationResult2 = step.verification ? this.runVerification(step.verification, output2, step.name) : void 0;
35648
36280
  state.row.status = "completed";
35649
36281
  state.row.output = output2;
36282
+ state.row.completionReason = verificationResult2?.completionReason;
35650
36283
  state.row.completedAt = (/* @__PURE__ */ new Date()).toISOString();
35651
36284
  await this.db.updateStep(state.row.id, {
35652
36285
  status: "completed",
35653
36286
  output: output2,
36287
+ completionReason: verificationResult2?.completionReason,
35654
36288
  completedAt: state.row.completedAt,
35655
36289
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
35656
36290
  });
35657
36291
  await this.persistStepOutput(runId, step.name, output2);
35658
36292
  this.emit({ type: "step:completed", runId, stepName: step.name, output: output2 });
36293
+ this.finalizeStepEvidence(step.name, "completed", state.row.completedAt, verificationResult2?.completionReason);
35659
36294
  return;
35660
36295
  }
36296
+ let commandStdout = "";
36297
+ let commandStderr = "";
35661
36298
  const output = await new Promise((resolve3, reject) => {
35662
36299
  const child = (0, import_node_child_process3.spawn)("sh", ["-c", resolvedCommand], {
35663
36300
  stdio: "pipe",
@@ -35690,7 +36327,7 @@ ${trimmedOutput.slice(0, 200)}`);
35690
36327
  child.stderr?.on("data", (chunk) => {
35691
36328
  stderrChunks.push(chunk.toString());
35692
36329
  });
35693
- child.on("close", (code) => {
36330
+ child.on("close", (code, signal) => {
35694
36331
  if (timer)
35695
36332
  clearTimeout(timer);
35696
36333
  if (abortHandler && abortSignal) {
@@ -35706,6 +36343,10 @@ ${trimmedOutput.slice(0, 200)}`);
35706
36343
  }
35707
36344
  const stdout = stdoutChunks.join("");
35708
36345
  const stderr = stderrChunks.join("");
36346
+ commandStdout = stdout;
36347
+ commandStderr = stderr;
36348
+ lastExitCode = code ?? void 0;
36349
+ lastExitSignal = signal ?? void 0;
35709
36350
  const failOnError = step.failOnError !== false;
35710
36351
  if (failOnError && code !== 0 && code !== null) {
35711
36352
  reject(new Error(`Command failed with exit code ${code}${stderr ? `: ${stderr.slice(0, 500)}` : ""}`));
@@ -35722,28 +36363,35 @@ ${trimmedOutput.slice(0, 200)}`);
35722
36363
  reject(new Error(`Failed to execute command: ${err.message}`));
35723
36364
  });
35724
36365
  });
35725
- if (step.verification) {
35726
- this.runVerification(step.verification, output, step.name);
35727
- }
36366
+ this.captureStepTerminalEvidence(step.name, {
36367
+ stdout: commandStdout || output,
36368
+ stderr: commandStderr,
36369
+ combined: [commandStdout || output, commandStderr].filter(Boolean).join("\n")
36370
+ }, { exitCode: lastExitCode, exitSignal: lastExitSignal });
36371
+ const verificationResult = step.verification ? this.runVerification(step.verification, output, step.name) : void 0;
35728
36372
  state.row.status = "completed";
35729
36373
  state.row.output = output;
36374
+ state.row.completionReason = verificationResult?.completionReason;
35730
36375
  state.row.completedAt = (/* @__PURE__ */ new Date()).toISOString();
35731
36376
  await this.db.updateStep(state.row.id, {
35732
36377
  status: "completed",
35733
36378
  output,
36379
+ completionReason: verificationResult?.completionReason,
35734
36380
  completedAt: state.row.completedAt,
35735
36381
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
35736
36382
  });
35737
36383
  await this.persistStepOutput(runId, step.name, output);
35738
36384
  this.emit({ type: "step:completed", runId, stepName: step.name, output });
36385
+ this.finalizeStepEvidence(step.name, "completed", state.row.completedAt, verificationResult?.completionReason);
35739
36386
  return;
35740
36387
  } catch (err) {
35741
36388
  lastError = err instanceof Error ? err.message : String(err);
36389
+ lastCompletionReason = err instanceof WorkflowCompletionError ? err.completionReason : void 0;
35742
36390
  }
35743
36391
  }
35744
36392
  const errorMsg = lastError ?? "Unknown error";
35745
36393
  this.postToChannel(`**[${step.name}]** Failed: ${errorMsg}`);
35746
- await this.markStepFailed(state, errorMsg, runId);
36394
+ await this.markStepFailed(state, errorMsg, runId, { exitCode: lastExitCode, exitSignal: lastExitSignal }, lastCompletionReason);
35747
36395
  throw new Error(`Step "${step.name}" failed: ${errorMsg}`);
35748
36396
  }
35749
36397
  /**
@@ -35755,11 +36403,17 @@ ${trimmedOutput.slice(0, 200)}`);
35755
36403
  const state = stepStates.get(step.name);
35756
36404
  if (!state)
35757
36405
  throw new Error(`Step state not found: ${step.name}`);
36406
+ let lastExitCode;
36407
+ let lastExitSignal;
35758
36408
  this.checkAborted();
35759
36409
  state.row.status = "running";
36410
+ state.row.error = void 0;
36411
+ state.row.completionReason = void 0;
35760
36412
  state.row.startedAt = (/* @__PURE__ */ new Date()).toISOString();
35761
36413
  await this.db.updateStep(state.row.id, {
35762
36414
  status: "running",
36415
+ error: void 0,
36416
+ completionReason: void 0,
35763
36417
  startedAt: state.row.startedAt,
35764
36418
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
35765
36419
  });
@@ -35771,6 +36425,7 @@ ${trimmedOutput.slice(0, 200)}`);
35771
36425
  const worktreePath = step.path ? this.interpolateStepTask(step.path, stepOutputContext) : import_node_path8.default.join(".worktrees", step.name);
35772
36426
  const createBranch = step.createBranch !== false;
35773
36427
  const stepCwd = this.resolveStepWorkdir(step) ?? this.cwd;
36428
+ this.beginStepEvidence(step.name, [stepCwd], state.row.startedAt);
35774
36429
  if (!branch) {
35775
36430
  const errorMsg = 'Worktree step missing required "branch" field';
35776
36431
  await this.markStepFailed(state, errorMsg, runId);
@@ -35802,6 +36457,10 @@ ${trimmedOutput.slice(0, 200)}`);
35802
36457
  await this.markStepFailed(state, errorMsg, runId);
35803
36458
  throw new Error(`Step "${step.name}" failed: ${errorMsg}`);
35804
36459
  }
36460
+ let commandStdout = "";
36461
+ let commandStderr = "";
36462
+ let commandExitCode;
36463
+ let commandExitSignal;
35805
36464
  const output = await new Promise((resolve3, reject) => {
35806
36465
  const child = (0, import_node_child_process3.spawn)("sh", ["-c", worktreeCmd], {
35807
36466
  stdio: "pipe",
@@ -35834,7 +36493,7 @@ ${trimmedOutput.slice(0, 200)}`);
35834
36493
  child.stderr?.on("data", (chunk) => {
35835
36494
  stderrChunks.push(chunk.toString());
35836
36495
  });
35837
- child.on("close", (code) => {
36496
+ child.on("close", (code, signal) => {
35838
36497
  if (timer)
35839
36498
  clearTimeout(timer);
35840
36499
  if (abortHandler && abortSignal) {
@@ -35848,7 +36507,13 @@ ${trimmedOutput.slice(0, 200)}`);
35848
36507
  reject(new Error(`Step "${step.name}" timed out (no step timeout set, check global swarm.timeoutMs)`));
35849
36508
  return;
35850
36509
  }
36510
+ commandStdout = stdoutChunks.join("");
35851
36511
  const stderr = stderrChunks.join("");
36512
+ commandStderr = stderr;
36513
+ commandExitCode = code ?? void 0;
36514
+ commandExitSignal = signal ?? void 0;
36515
+ lastExitCode = commandExitCode;
36516
+ lastExitSignal = commandExitSignal;
35852
36517
  if (code !== 0 && code !== null) {
35853
36518
  reject(new Error(`git worktree add failed with exit code ${code}${stderr ? `: ${stderr.slice(0, 500)}` : ""}`));
35854
36519
  return;
@@ -35864,6 +36529,11 @@ ${trimmedOutput.slice(0, 200)}`);
35864
36529
  reject(new Error(`Failed to execute git worktree command: ${err.message}`));
35865
36530
  });
35866
36531
  });
36532
+ this.captureStepTerminalEvidence(step.name, {
36533
+ stdout: commandStdout || output,
36534
+ stderr: commandStderr,
36535
+ combined: [commandStdout || output, commandStderr].filter(Boolean).join("\n")
36536
+ }, { exitCode: commandExitCode, exitSignal: commandExitSignal });
35867
36537
  state.row.status = "completed";
35868
36538
  state.row.output = output;
35869
36539
  state.row.completedAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -35877,10 +36547,19 @@ ${trimmedOutput.slice(0, 200)}`);
35877
36547
  this.emit({ type: "step:completed", runId, stepName: step.name, output });
35878
36548
  this.postToChannel(`**[${step.name}]** Worktree created at: ${output}
35879
36549
  Branch: ${branch}${!branchExists && createBranch ? " (created)" : ""}`);
36550
+ this.recordStepToolSideEffect(step.name, {
36551
+ type: "worktree_created",
36552
+ detail: `Worktree created at ${output}`,
36553
+ raw: { branch, createdBranch: !branchExists && createBranch }
36554
+ });
36555
+ this.finalizeStepEvidence(step.name, "completed", state.row.completedAt);
35880
36556
  } catch (err) {
35881
36557
  const errorMsg = err instanceof Error ? err.message : String(err);
35882
36558
  this.postToChannel(`**[${step.name}]** Failed: ${errorMsg}`);
35883
- await this.markStepFailed(state, errorMsg, runId);
36559
+ await this.markStepFailed(state, errorMsg, runId, {
36560
+ exitCode: lastExitCode,
36561
+ exitSignal: lastExitSignal
36562
+ });
35884
36563
  throw new Error(`Step "${step.name}" failed: ${errorMsg}`);
35885
36564
  }
35886
36565
  }
@@ -35901,8 +36580,11 @@ ${trimmedOutput.slice(0, 200)}`);
35901
36580
  }
35902
36581
  const specialistDef = _WorkflowRunner.resolveAgentDef(rawAgentDef);
35903
36582
  const usesOwnerFlow = specialistDef.interactive !== false;
35904
- const ownerDef = usesOwnerFlow ? this.resolveAutoStepOwner(specialistDef, agentMap) : specialistDef;
35905
- const reviewDef = usesOwnerFlow ? this.resolveAutoReviewAgent(ownerDef, agentMap) : void 0;
36583
+ const currentPattern = this.currentConfig?.swarm?.pattern ?? "";
36584
+ const isHubPattern = _WorkflowRunner.HUB_PATTERNS.has(currentPattern);
36585
+ const usesAutoHardening = usesOwnerFlow && isHubPattern && !this.isExplicitInteractiveWorker(specialistDef);
36586
+ const ownerDef = usesAutoHardening ? this.resolveAutoStepOwner(specialistDef, agentMap) : specialistDef;
36587
+ let reviewDef;
35906
36588
  const supervised = {
35907
36589
  specialist: specialistDef,
35908
36590
  owner: ownerDef,
@@ -35915,6 +36597,7 @@ ${trimmedOutput.slice(0, 200)}`);
35915
36597
  let lastError;
35916
36598
  let lastExitCode;
35917
36599
  let lastExitSignal;
36600
+ let lastCompletionReason;
35918
36601
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
35919
36602
  this.checkAborted();
35920
36603
  lastExitCode = void 0;
@@ -35922,6 +36605,11 @@ ${trimmedOutput.slice(0, 200)}`);
35922
36605
  if (attempt > 0) {
35923
36606
  this.emit({ type: "step:retrying", runId, stepName: step.name, attempt });
35924
36607
  this.postToChannel(`**[${step.name}]** Retrying (attempt ${attempt + 1}/${maxRetries + 1})`);
36608
+ this.recordStepToolSideEffect(step.name, {
36609
+ type: "retry",
36610
+ detail: `Retrying attempt ${attempt + 1}/${maxRetries + 1}`,
36611
+ raw: { attempt, maxRetries }
36612
+ });
35925
36613
  state.row.retryCount = attempt;
35926
36614
  await this.db.updateStep(state.row.id, {
35927
36615
  retryCount: attempt,
@@ -35932,14 +36620,19 @@ ${trimmedOutput.slice(0, 200)}`);
35932
36620
  }
35933
36621
  try {
35934
36622
  state.row.status = "running";
36623
+ state.row.error = void 0;
36624
+ state.row.completionReason = void 0;
35935
36625
  state.row.startedAt = (/* @__PURE__ */ new Date()).toISOString();
35936
36626
  await this.db.updateStep(state.row.id, {
35937
36627
  status: "running",
36628
+ error: void 0,
36629
+ completionReason: void 0,
35938
36630
  startedAt: state.row.startedAt,
35939
36631
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
35940
36632
  });
35941
36633
  this.emit({ type: "step:started", runId, stepName: step.name });
35942
- this.postToChannel(`**[${step.name}]** Started (owner: ${ownerDef.name}, specialist: ${specialistDef.name})`);
36634
+ this.log(`[${step.name}] Started (owner: ${ownerDef.name}, specialist: ${specialistDef.name})`);
36635
+ this.initializeStepSignalParticipants(step.name, ownerDef.name, specialistDef.name);
35943
36636
  await this.trajectory?.stepStarted(step, ownerDef.name, {
35944
36637
  role: usesDedicatedOwner ? "owner" : "specialist",
35945
36638
  owner: ownerDef.name,
@@ -35984,55 +36677,126 @@ ${resolvedTask}`;
35984
36677
  };
35985
36678
  const effectiveSpecialist = applyStepWorkdir(specialistDef);
35986
36679
  const effectiveOwner = applyStepWorkdir(ownerDef);
36680
+ const effectiveReviewer = reviewDef ? applyStepWorkdir(reviewDef) : void 0;
36681
+ this.beginStepEvidence(step.name, [
36682
+ this.resolveAgentCwd(effectiveSpecialist),
36683
+ this.resolveAgentCwd(effectiveOwner),
36684
+ effectiveReviewer ? this.resolveAgentCwd(effectiveReviewer) : void 0
36685
+ ], state.row.startedAt);
35987
36686
  let specialistOutput;
35988
36687
  let ownerOutput;
35989
36688
  let ownerElapsed;
36689
+ let completionReason;
35990
36690
  if (usesDedicatedOwner) {
35991
36691
  const result = await this.executeSupervisedAgentStep(step, { specialist: effectiveSpecialist, owner: effectiveOwner, reviewer: reviewDef }, resolvedTask, timeoutMs);
35992
36692
  specialistOutput = result.specialistOutput;
35993
36693
  ownerOutput = result.ownerOutput;
35994
36694
  ownerElapsed = result.ownerElapsed;
36695
+ completionReason = result.completionReason;
35995
36696
  } else {
35996
36697
  const ownerTask = this.injectStepOwnerContract(step, resolvedTask, effectiveOwner, effectiveSpecialist);
36698
+ const explicitInteractiveWorker = this.isExplicitInteractiveWorker(effectiveOwner);
36699
+ let explicitWorkerHandle;
36700
+ let explicitWorkerCompleted = false;
36701
+ let explicitWorkerOutput = "";
35997
36702
  this.log(`[${step.name}] Spawning owner "${effectiveOwner.name}" (cli: ${effectiveOwner.cli})${step.workdir ? ` [workdir: ${step.workdir}]` : ""}`);
35998
36703
  const resolvedStep = { ...step, task: ownerTask };
35999
36704
  const ownerStartTime = Date.now();
36000
- const spawnResult = this.executor ? await this.executor.executeAgentStep(resolvedStep, effectiveOwner, ownerTask, timeoutMs) : await this.spawnAndWait(effectiveOwner, resolvedStep, timeoutMs);
36705
+ const spawnResult = this.executor ? await this.executor.executeAgentStep(resolvedStep, effectiveOwner, ownerTask, timeoutMs) : await this.spawnAndWait(effectiveOwner, resolvedStep, timeoutMs, {
36706
+ evidenceStepName: step.name,
36707
+ evidenceRole: usesOwnerFlow ? "owner" : "specialist",
36708
+ preserveOnIdle: !isHubPattern || !this.isLeadLikeAgent(effectiveOwner) ? false : void 0,
36709
+ logicalName: effectiveOwner.name,
36710
+ onSpawned: explicitInteractiveWorker ? ({ agent }) => {
36711
+ explicitWorkerHandle = agent;
36712
+ } : void 0,
36713
+ onChunk: explicitInteractiveWorker ? ({ chunk }) => {
36714
+ explicitWorkerOutput += _WorkflowRunner.stripAnsi(chunk);
36715
+ if (!explicitWorkerCompleted && this.hasExplicitInteractiveWorkerCompletionEvidence(step, explicitWorkerOutput, ownerTask, resolvedTask)) {
36716
+ explicitWorkerCompleted = true;
36717
+ void explicitWorkerHandle?.release().catch(() => void 0);
36718
+ }
36719
+ } : void 0
36720
+ });
36001
36721
  const output = typeof spawnResult === "string" ? spawnResult : spawnResult.output;
36002
36722
  lastExitCode = typeof spawnResult === "string" ? void 0 : spawnResult.exitCode;
36003
36723
  lastExitSignal = typeof spawnResult === "string" ? void 0 : spawnResult.exitSignal;
36004
36724
  ownerElapsed = Date.now() - ownerStartTime;
36005
36725
  this.log(`[${step.name}] Owner "${effectiveOwner.name}" exited`);
36006
36726
  if (usesOwnerFlow) {
36007
- this.assertOwnerCompletionMarker(step, output, ownerTask);
36727
+ try {
36728
+ const completionDecision = this.resolveOwnerCompletionDecision(step, output, output, ownerTask, resolvedTask);
36729
+ completionReason = completionDecision.completionReason;
36730
+ } catch (error48) {
36731
+ const canUseVerificationFallback = !usesDedicatedOwner && step.verification && error48 instanceof WorkflowCompletionError && error48.completionReason === "failed_no_evidence";
36732
+ if (!canUseVerificationFallback) {
36733
+ throw error48;
36734
+ }
36735
+ }
36008
36736
  }
36009
36737
  specialistOutput = output;
36010
36738
  ownerOutput = output;
36011
36739
  }
36012
- if (step.verification) {
36013
- this.runVerification(step.verification, specialistOutput, step.name, effectiveOwner.interactive === false ? void 0 : resolvedTask);
36740
+ if (!usesOwnerFlow) {
36741
+ const explicitOwnerDecision = this.parseOwnerDecision(step, ownerOutput, false);
36742
+ if (explicitOwnerDecision?.decision === "INCOMPLETE_RETRY") {
36743
+ throw new WorkflowCompletionError(`Step "${step.name}" owner requested retry${explicitOwnerDecision.reason ? `: ${explicitOwnerDecision.reason}` : ""}`, "retry_requested_by_owner");
36744
+ }
36745
+ if (explicitOwnerDecision?.decision === "INCOMPLETE_FAIL") {
36746
+ throw new WorkflowCompletionError(`Step "${step.name}" owner marked the step incomplete${explicitOwnerDecision.reason ? `: ${explicitOwnerDecision.reason}` : ""}`, "failed_owner_decision");
36747
+ }
36748
+ if (explicitOwnerDecision?.decision === "NEEDS_CLARIFICATION") {
36749
+ throw new WorkflowCompletionError(`Step "${step.name}" owner requested clarification before completion${explicitOwnerDecision.reason ? `: ${explicitOwnerDecision.reason}` : ""}`, "retry_requested_by_owner");
36750
+ }
36751
+ }
36752
+ if (step.verification && (!usesOwnerFlow || !usesDedicatedOwner) && !completionReason) {
36753
+ const verificationResult = this.runVerification(step.verification, specialistOutput, step.name, effectiveOwner.interactive === false ? void 0 : resolvedTask);
36754
+ completionReason = verificationResult.completionReason;
36755
+ }
36756
+ if (completionReason === "retry_requested_by_owner") {
36757
+ throw new WorkflowCompletionError(`Step "${step.name}" owner requested another attempt`, "retry_requested_by_owner");
36758
+ }
36759
+ if (usesAutoHardening && usesDedicatedOwner && !reviewDef) {
36760
+ reviewDef = this.resolveAutoReviewAgent(ownerDef, agentMap);
36761
+ supervised.reviewer = reviewDef;
36014
36762
  }
36015
36763
  let combinedOutput = specialistOutput;
36016
36764
  if (usesOwnerFlow && reviewDef) {
36017
- const remainingMs = timeoutMs ? Math.max(0, timeoutMs - ownerElapsed) : void 0;
36018
- const reviewOutput = await this.runStepReviewGate(step, resolvedTask, specialistOutput, ownerOutput, ownerDef, reviewDef, remainingMs);
36019
- combinedOutput = this.combineStepAndReviewOutput(specialistOutput, reviewOutput);
36765
+ this.activeReviewers.set(reviewDef.name, (this.activeReviewers.get(reviewDef.name) ?? 0) + 1);
36766
+ try {
36767
+ const remainingMs = timeoutMs ? Math.max(0, timeoutMs - ownerElapsed) : void 0;
36768
+ const reviewOutput = await this.runStepReviewGate(step, resolvedTask, specialistOutput, ownerOutput, ownerDef, reviewDef, remainingMs);
36769
+ combinedOutput = this.combineStepAndReviewOutput(specialistOutput, reviewOutput);
36770
+ } finally {
36771
+ const count = (this.activeReviewers.get(reviewDef.name) ?? 1) - 1;
36772
+ if (count <= 0)
36773
+ this.activeReviewers.delete(reviewDef.name);
36774
+ else
36775
+ this.activeReviewers.set(reviewDef.name, count);
36776
+ }
36020
36777
  }
36021
36778
  state.row.status = "completed";
36022
36779
  state.row.output = combinedOutput;
36780
+ state.row.completionReason = completionReason;
36023
36781
  state.row.completedAt = (/* @__PURE__ */ new Date()).toISOString();
36024
36782
  await this.db.updateStep(state.row.id, {
36025
36783
  status: "completed",
36026
36784
  output: combinedOutput,
36785
+ completionReason,
36027
36786
  completedAt: state.row.completedAt,
36028
36787
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
36029
36788
  });
36030
36789
  await this.persistStepOutput(runId, step.name, combinedOutput);
36031
36790
  this.emit({ type: "step:completed", runId, stepName: step.name, output: combinedOutput, exitCode: lastExitCode, exitSignal: lastExitSignal });
36791
+ this.finalizeStepEvidence(step.name, "completed", state.row.completedAt, completionReason);
36032
36792
  await this.trajectory?.stepCompleted(step, combinedOutput, attempt + 1);
36033
36793
  return;
36034
36794
  } catch (err) {
36035
36795
  lastError = err instanceof Error ? err.message : String(err);
36796
+ lastCompletionReason = err instanceof WorkflowCompletionError ? err.completionReason : void 0;
36797
+ if (lastCompletionReason === "retry_requested_by_owner" && attempt >= maxRetries) {
36798
+ lastError = this.buildOwnerRetryBudgetExceededMessage(step.name, maxRetries, lastError);
36799
+ }
36036
36800
  if (err instanceof SpawnExitError) {
36037
36801
  lastExitCode = err.exitCode;
36038
36802
  lastExitSignal = err.exitSignal;
@@ -36054,9 +36818,19 @@ ${resolvedTask}`;
36054
36818
  await this.markStepFailed(state, lastError ?? "Unknown error", runId, {
36055
36819
  exitCode: lastExitCode,
36056
36820
  exitSignal: lastExitSignal
36057
- });
36821
+ }, lastCompletionReason);
36058
36822
  throw new Error(`Step "${step.name}" failed after ${maxRetries} retries: ${lastError ?? "Unknown error"}`);
36059
36823
  }
36824
+ buildOwnerRetryBudgetExceededMessage(stepName, maxRetries, ownerDecisionError) {
36825
+ const attempts = maxRetries + 1;
36826
+ const prefix = `Step "${stepName}" `;
36827
+ const normalizedDecision = ownerDecisionError?.startsWith(prefix) ? ownerDecisionError.slice(prefix.length).trim() : ownerDecisionError?.trim();
36828
+ const decisionSuffix = normalizedDecision ? ` Latest owner decision: ${normalizedDecision}` : "";
36829
+ if (maxRetries === 0) {
36830
+ return `Step "${stepName}" owner requested another attempt, but no retries are configured (maxRetries=0). Configure retries > 0 to allow OWNER_DECISION: INCOMPLETE_RETRY.` + decisionSuffix;
36831
+ }
36832
+ return `Step "${stepName}" owner requested another attempt after ${attempts} total attempts, but the retry budget is exhausted (maxRetries=${maxRetries}).` + decisionSuffix;
36833
+ }
36060
36834
  injectStepOwnerContract(step, resolvedTask, ownerDef, specialistDef) {
36061
36835
  if (ownerDef.interactive === false)
36062
36836
  return resolvedTask;
@@ -36068,12 +36842,18 @@ STEP OWNER CONTRACT:
36068
36842
  - You are the accountable owner for step "${step.name}".
36069
36843
  ` + (specialistNote ? `- ${specialistNote}
36070
36844
  ` : "") + `- If you delegate, you must still verify completion yourself.
36071
- - Before exiting, provide an explicit completion line: STEP_COMPLETE:${step.name}
36845
+ - Preferred final decision format:
36846
+ OWNER_DECISION: <one of COMPLETE, INCOMPLETE_RETRY, INCOMPLETE_FAIL, NEEDS_CLARIFICATION>
36847
+ REASON: <one sentence>
36848
+ - Legacy completion marker still supported: STEP_COMPLETE:${step.name}
36072
36849
  - Then self-terminate immediately with /exit.`;
36073
36850
  }
36074
36851
  buildOwnerSupervisorTask(step, originalTask, supervised, workerRuntimeName) {
36075
36852
  const verificationGuide = this.buildSupervisorVerificationGuide(step.verification);
36076
36853
  const channelLine = this.channel ? `#${this.channel}` : "(workflow channel unavailable)";
36854
+ const channelContract = this.channel ? `- Prefer Relaycast/group-chat handoff signals over terminal sentinels: wait for the worker to post \`WORKER_DONE: <brief summary>\` in ${channelLine}
36855
+ - When you have validated the handoff, post \`LEAD_DONE: <brief summary>\` to ${channelLine} before you exit
36856
+ ` : "";
36077
36857
  return `You are the step owner/supervisor for step "${step.name}".
36078
36858
 
36079
36859
  Worker: ${supervised.specialist.name} (runtime: ${workerRuntimeName}) on ${channelLine}
@@ -36085,9 +36865,23 @@ How to verify completion:
36085
36865
  - Watch ${channelLine} for the worker's progress messages and mirrored PTY output
36086
36866
  - Check file changes: run \`git diff --stat\` or inspect expected files directly
36087
36867
  - Ask the worker directly on ${channelLine} if you need a status update
36088
- ` + verificationGuide + `
36089
- When you're satisfied the work is done correctly:
36090
- Output exactly: STEP_COMPLETE:${step.name}`;
36868
+ ` + channelContract + verificationGuide + `
36869
+ When you have enough evidence, return:
36870
+ OWNER_DECISION: <one of COMPLETE, INCOMPLETE_RETRY, INCOMPLETE_FAIL, NEEDS_CLARIFICATION>
36871
+ REASON: <one sentence>
36872
+ Legacy completion marker still supported: STEP_COMPLETE:${step.name}`;
36873
+ }
36874
+ buildWorkerHandoffTask(step, originalTask, supervised) {
36875
+ if (!this.channel)
36876
+ return originalTask;
36877
+ return `${originalTask}
36878
+
36879
+ ---
36880
+ WORKER COMPLETION CONTRACT:
36881
+ - You are handing work off to owner "${supervised.owner.name}" for step "${step.name}".
36882
+ - When your work is ready for review, post to #${this.channel}: \`WORKER_DONE: <brief summary>\`
36883
+ - Do not rely on terminal output alone for handoff; use the workflow group chat signal above.
36884
+ - After posting your handoff signal, self-terminate with /exit unless the owner asks for follow-up.`;
36091
36885
  }
36092
36886
  buildSupervisorVerificationGuide(verification) {
36093
36887
  if (!verification)
@@ -36111,8 +36905,9 @@ Output exactly: STEP_COMPLETE:${step.name}`;
36111
36905
  }
36112
36906
  async executeSupervisedAgentStep(step, supervised, resolvedTask, timeoutMs) {
36113
36907
  if (this.executor) {
36908
+ const specialistTask2 = this.buildWorkerHandoffTask(step, resolvedTask, supervised);
36114
36909
  const supervisorTask2 = this.buildOwnerSupervisorTask(step, resolvedTask, supervised, supervised.specialist.name);
36115
- const specialistStep2 = { ...step, task: resolvedTask };
36910
+ const specialistStep2 = { ...step, task: specialistTask2 };
36116
36911
  const ownerStep2 = {
36117
36912
  ...step,
36118
36913
  name: `${step.name}-owner`,
@@ -36120,15 +36915,20 @@ Output exactly: STEP_COMPLETE:${step.name}`;
36120
36915
  task: supervisorTask2
36121
36916
  };
36122
36917
  this.log(`[${step.name}] Spawning specialist "${supervised.specialist.name}" and owner "${supervised.owner.name}"`);
36123
- const specialistPromise = this.executor.executeAgentStep(specialistStep2, supervised.specialist, resolvedTask, timeoutMs);
36918
+ const specialistPromise = this.executor.executeAgentStep(specialistStep2, supervised.specialist, specialistTask2, timeoutMs);
36124
36919
  const specialistSettled = specialistPromise.catch(() => void 0);
36125
36920
  try {
36126
36921
  const ownerStartTime2 = Date.now();
36127
36922
  const ownerOutput = await this.executor.executeAgentStep(ownerStep2, supervised.owner, supervisorTask2, timeoutMs);
36128
36923
  const ownerElapsed = Date.now() - ownerStartTime2;
36129
- this.assertOwnerCompletionMarker(step, ownerOutput, supervisorTask2);
36130
36924
  const specialistOutput = await specialistPromise;
36131
- return { specialistOutput, ownerOutput, ownerElapsed };
36925
+ const completionDecision = this.resolveOwnerCompletionDecision(step, ownerOutput, specialistOutput, supervisorTask2, resolvedTask);
36926
+ return {
36927
+ specialistOutput,
36928
+ ownerOutput,
36929
+ ownerElapsed,
36930
+ completionReason: completionDecision.completionReason
36931
+ };
36132
36932
  } catch (error48) {
36133
36933
  await specialistSettled;
36134
36934
  throw error48;
@@ -36144,10 +36944,14 @@ Output exactly: STEP_COMPLETE:${step.name}`;
36144
36944
  resolveWorkerSpawn = resolve3;
36145
36945
  rejectWorkerSpawn = reject;
36146
36946
  });
36147
- const specialistStep = { ...step, task: resolvedTask };
36947
+ const specialistTask = this.buildWorkerHandoffTask(step, resolvedTask, supervised);
36948
+ const specialistStep = { ...step, task: specialistTask };
36148
36949
  this.log(`[${step.name}] Spawning specialist "${supervised.specialist.name}" (cli: ${supervised.specialist.cli})`);
36149
36950
  const workerPromise = this.spawnAndWait(supervised.specialist, specialistStep, timeoutMs, {
36150
36951
  agentNameSuffix: "worker",
36952
+ evidenceStepName: step.name,
36953
+ evidenceRole: "worker",
36954
+ logicalName: supervised.specialist.name,
36151
36955
  onSpawned: ({ actualName, agent }) => {
36152
36956
  workerHandle = agent;
36153
36957
  workerRuntimeName = actualName;
@@ -36162,7 +36966,7 @@ Output exactly: STEP_COMPLETE:${step.name}`;
36162
36966
  }
36163
36967
  },
36164
36968
  onChunk: ({ agentName, chunk }) => {
36165
- this.forwardAgentChunkToChannel(step.name, "Worker", agentName, chunk);
36969
+ this.forwardAgentChunkToChannel(step.name, "Worker", agentName, chunk, supervised.specialist.name);
36166
36970
  }
36167
36971
  }).catch((error48) => {
36168
36972
  if (!workerSpawned) {
@@ -36174,13 +36978,23 @@ Output exactly: STEP_COMPLETE:${step.name}`;
36174
36978
  const workerSettled = workerPromise.catch(() => void 0);
36175
36979
  workerPromise.then((result) => {
36176
36980
  workerReleased = true;
36177
- this.postToChannel(`**[${step.name}]** Worker \`${workerRuntimeName}\` exited`);
36981
+ this.log(`[${step.name}] Worker ${workerRuntimeName} exited`);
36982
+ this.recordStepToolSideEffect(step.name, {
36983
+ type: "worker_exit",
36984
+ detail: `Worker ${workerRuntimeName} exited`,
36985
+ raw: { worker: workerRuntimeName, exitCode: result.exitCode, exitSignal: result.exitSignal }
36986
+ });
36178
36987
  if (step.verification?.type === "output_contains" && result.output.includes(step.verification.value)) {
36179
- this.postToChannel(`**[${step.name}]** Verification gate observed: output contains ${JSON.stringify(step.verification.value)}`);
36988
+ this.log(`[${step.name}] Verification gate observed: output contains ${JSON.stringify(step.verification.value)}`);
36180
36989
  }
36181
36990
  }).catch((error48) => {
36182
36991
  const message = error48 instanceof Error ? error48.message : String(error48);
36183
36992
  this.postToChannel(`**[${step.name}]** Worker \`${workerRuntimeName}\` exited with error: ${message}`);
36993
+ this.recordStepToolSideEffect(step.name, {
36994
+ type: "worker_error",
36995
+ detail: `Worker ${workerRuntimeName} exited with error: ${message}`,
36996
+ raw: { worker: workerRuntimeName, error: message }
36997
+ });
36184
36998
  });
36185
36999
  await workerReady;
36186
37000
  const supervisorTask = this.buildOwnerSupervisorTask(step, resolvedTask, supervised, workerRuntimeName);
@@ -36195,6 +37009,9 @@ Output exactly: STEP_COMPLETE:${step.name}`;
36195
37009
  try {
36196
37010
  const ownerResultObj = await this.spawnAndWait(supervised.owner, ownerStep, timeoutMs, {
36197
37011
  agentNameSuffix: "owner",
37012
+ evidenceStepName: step.name,
37013
+ evidenceRole: "owner",
37014
+ logicalName: supervised.owner.name,
36198
37015
  onSpawned: ({ actualName }) => {
36199
37016
  this.supervisedRuntimeAgents.set(actualName, {
36200
37017
  stepName: step.name,
@@ -36209,9 +37026,14 @@ Output exactly: STEP_COMPLETE:${step.name}`;
36209
37026
  const ownerElapsed = Date.now() - ownerStartTime;
36210
37027
  const ownerOutput = ownerResultObj.output;
36211
37028
  this.log(`[${step.name}] Owner "${supervised.owner.name}" exited`);
36212
- this.assertOwnerCompletionMarker(step, ownerOutput, supervisorTask);
36213
37029
  const specialistOutput = (await workerPromise).output;
36214
- return { specialistOutput, ownerOutput, ownerElapsed };
37030
+ const completionDecision = this.resolveOwnerCompletionDecision(step, ownerOutput, specialistOutput, supervisorTask, resolvedTask);
37031
+ return {
37032
+ specialistOutput,
37033
+ ownerOutput,
37034
+ ownerElapsed,
37035
+ completionReason: completionDecision.completionReason
37036
+ };
36215
37037
  } catch (error48) {
36216
37038
  const message = error48 instanceof Error ? error48.message : String(error48);
36217
37039
  if (!workerReleased && workerHandle) {
@@ -36224,10 +37046,16 @@ Output exactly: STEP_COMPLETE:${step.name}`;
36224
37046
  throw error48;
36225
37047
  }
36226
37048
  }
36227
- forwardAgentChunkToChannel(stepName, roleLabel, agentName, chunk) {
36228
- const lines = _WorkflowRunner.stripAnsi(chunk).split("\n").map((line) => line.trim()).filter(Boolean).slice(0, 3);
37049
+ forwardAgentChunkToChannel(stepName, roleLabel, agentName, chunk, sender) {
37050
+ const lines = _WorkflowRunner.scrubForChannel(chunk).split("\n").map((line) => line.trim()).filter(Boolean).slice(0, 3);
36229
37051
  for (const line of lines) {
36230
- this.postToChannel(`**[${stepName}]** ${roleLabel} \`${agentName}\`: ${line.slice(0, 280)}`);
37052
+ this.postToChannel(`**[${stepName}]** ${roleLabel} \`${agentName}\`: ${line.slice(0, 280)}`, {
37053
+ stepName,
37054
+ sender,
37055
+ actor: agentName,
37056
+ role: roleLabel,
37057
+ origin: "forwarded_chunk"
37058
+ });
36231
37059
  }
36232
37060
  }
36233
37061
  async recordOwnerMonitoringChunk(step, ownerDef, chunk) {
@@ -36242,6 +37070,11 @@ Output exactly: STEP_COMPLETE:${step.name}`;
36242
37070
  if (/STEP_COMPLETE:/i.test(stripped))
36243
37071
  details.push("Declared the step complete");
36244
37072
  for (const detail of details) {
37073
+ this.recordStepToolSideEffect(step.name, {
37074
+ type: "owner_monitoring",
37075
+ detail,
37076
+ raw: { output: stripped.slice(0, 240), owner: ownerDef.name }
37077
+ });
36245
37078
  await this.trajectory?.ownerMonitoringEvent(step.name, ownerDef.name, detail, {
36246
37079
  output: stripped.slice(0, 240)
36247
37080
  });
@@ -36280,6 +37113,7 @@ Output exactly: STEP_COMPLETE:${step.name}`;
36280
37113
  }
36281
37114
  resolveAutoReviewAgent(ownerDef, agentMap) {
36282
37115
  const allDefs = [...agentMap.values()].map((d) => _WorkflowRunner.resolveAgentDef(d));
37116
+ const eligible = (def) => def.name !== ownerDef.name && !this.isExplicitInteractiveWorker(def);
36283
37117
  const isReviewer = (def) => {
36284
37118
  const roleLC = def.role?.toLowerCase() ?? "";
36285
37119
  const nameLC = def.name.toLowerCase();
@@ -36298,28 +37132,190 @@ Output exactly: STEP_COMPLETE:${step.name}`;
36298
37132
  return 2;
36299
37133
  return isReviewer(def) ? 1 : 0;
36300
37134
  };
36301
- const dedicated = allDefs.filter((d) => d.name !== ownerDef.name && isReviewer(d)).sort((a, b) => reviewerPriority(b) - reviewerPriority(a) || a.name.localeCompare(b.name))[0];
36302
- if (dedicated)
36303
- return dedicated;
36304
- const alternate = allDefs.find((d) => d.name !== ownerDef.name && d.interactive !== false);
36305
- if (alternate)
36306
- return alternate;
36307
- return ownerDef;
37135
+ const notBusy = (def) => !this.activeReviewers.has(def.name);
37136
+ const dedicatedCandidates = allDefs.filter((d) => eligible(d) && isReviewer(d)).sort((a, b) => reviewerPriority(b) - reviewerPriority(a) || a.name.localeCompare(b.name));
37137
+ const dedicated = dedicatedCandidates.find(notBusy) ?? dedicatedCandidates[0];
37138
+ if (dedicated)
37139
+ return dedicated;
37140
+ const alternateCandidates = allDefs.filter((d) => eligible(d) && d.interactive !== false);
37141
+ const alternate = alternateCandidates.find(notBusy) ?? alternateCandidates[0];
37142
+ if (alternate)
37143
+ return alternate;
37144
+ return ownerDef;
37145
+ }
37146
+ isExplicitInteractiveWorker(agentDef) {
37147
+ return agentDef.preset === "worker" && agentDef.interactive !== false;
37148
+ }
37149
+ resolveOwnerCompletionDecision(step, ownerOutput, specialistOutput, injectedTaskText, verificationTaskText) {
37150
+ const hasMarker = this.hasOwnerCompletionMarker(step, ownerOutput, injectedTaskText);
37151
+ const explicitOwnerDecision = this.parseOwnerDecision(step, ownerOutput, false);
37152
+ if (explicitOwnerDecision?.decision === "INCOMPLETE_RETRY") {
37153
+ throw new WorkflowCompletionError(`Step "${step.name}" owner requested retry${explicitOwnerDecision.reason ? `: ${explicitOwnerDecision.reason}` : ""}`, "retry_requested_by_owner");
37154
+ }
37155
+ if (explicitOwnerDecision?.decision === "INCOMPLETE_FAIL") {
37156
+ throw new WorkflowCompletionError(`Step "${step.name}" owner marked the step incomplete${explicitOwnerDecision.reason ? `: ${explicitOwnerDecision.reason}` : ""}`, "failed_owner_decision");
37157
+ }
37158
+ if (explicitOwnerDecision?.decision === "NEEDS_CLARIFICATION") {
37159
+ throw new WorkflowCompletionError(`Step "${step.name}" owner requested clarification before completion${explicitOwnerDecision.reason ? `: ${explicitOwnerDecision.reason}` : ""}`, "retry_requested_by_owner");
37160
+ }
37161
+ const verificationResult = step.verification ? this.runVerification(step.verification, specialistOutput, step.name, verificationTaskText, {
37162
+ allowFailure: true,
37163
+ completionMarkerFound: hasMarker
37164
+ }) : { passed: false };
37165
+ if (verificationResult.error) {
37166
+ throw new WorkflowCompletionError(`Step "${step.name}" verification failed and no owner decision or evidence established completion: ${verificationResult.error}`, "failed_verification");
37167
+ }
37168
+ if (explicitOwnerDecision?.decision === "COMPLETE") {
37169
+ if (!hasMarker) {
37170
+ this.log(`[${step.name}] Structured OWNER_DECISION completed the step without legacy STEP_COMPLETE marker`);
37171
+ }
37172
+ return {
37173
+ completionReason: "completed_by_owner_decision",
37174
+ ownerDecision: explicitOwnerDecision.decision,
37175
+ reason: explicitOwnerDecision.reason
37176
+ };
37177
+ }
37178
+ if (verificationResult.passed) {
37179
+ return { completionReason: "completed_verified" };
37180
+ }
37181
+ const ownerDecision = this.parseOwnerDecision(step, ownerOutput, hasMarker);
37182
+ if (ownerDecision?.decision === "COMPLETE") {
37183
+ return {
37184
+ completionReason: "completed_by_owner_decision",
37185
+ ownerDecision: ownerDecision.decision,
37186
+ reason: ownerDecision.reason
37187
+ };
37188
+ }
37189
+ if (!explicitOwnerDecision) {
37190
+ const evidenceReason = this.judgeOwnerCompletionByEvidence(step.name, ownerOutput);
37191
+ if (evidenceReason) {
37192
+ if (!hasMarker) {
37193
+ this.log(`[${step.name}] Evidence-based completion resolved without legacy STEP_COMPLETE marker`);
37194
+ }
37195
+ return {
37196
+ completionReason: "completed_by_evidence",
37197
+ reason: evidenceReason
37198
+ };
37199
+ }
37200
+ }
37201
+ const processExitFallback = this.tryProcessExitFallback(step, specialistOutput, verificationTaskText, ownerOutput);
37202
+ if (processExitFallback) {
37203
+ this.log(`[${step.name}] Completion inferred from clean process exit (code 0)` + (step.verification ? " + verification passed" : "") + " \u2014 no coordination signal was required");
37204
+ return processExitFallback;
37205
+ }
37206
+ throw new WorkflowCompletionError(`Step "${step.name}" owner completion decision missing: no OWNER_DECISION, legacy STEP_COMPLETE marker, or evidence-backed completion signal`, "failed_no_evidence");
37207
+ }
37208
+ hasExplicitInteractiveWorkerCompletionEvidence(step, output, injectedTaskText, verificationTaskText) {
37209
+ try {
37210
+ this.resolveOwnerCompletionDecision(step, output, output, injectedTaskText, verificationTaskText);
37211
+ return true;
37212
+ } catch {
37213
+ return false;
37214
+ }
37215
+ }
37216
+ hasOwnerCompletionMarker(step, output, injectedTaskText) {
37217
+ const marker = `STEP_COMPLETE:${step.name}`;
37218
+ const taskHasMarker = injectedTaskText.includes(marker);
37219
+ const first = output.indexOf(marker);
37220
+ if (first === -1) {
37221
+ return false;
37222
+ }
37223
+ const outputLikelyContainsInjectedPrompt = output.includes("STEP OWNER CONTRACT") || output.includes("Preferred final decision format") || output.includes("Legacy completion marker still supported") || output.includes("Output exactly: STEP_COMPLETE:");
37224
+ if (taskHasMarker && outputLikelyContainsInjectedPrompt) {
37225
+ return output.includes(marker, first + marker.length);
37226
+ }
37227
+ return true;
37228
+ }
37229
+ parseOwnerDecision(step, ownerOutput, hasMarker) {
37230
+ const decisionPattern = /OWNER_DECISION:\s*(COMPLETE|INCOMPLETE_RETRY|INCOMPLETE_FAIL|NEEDS_CLARIFICATION)\b/gi;
37231
+ const decisionMatches = [...ownerOutput.matchAll(decisionPattern)];
37232
+ const outputLikelyContainsEchoedPrompt = ownerOutput.includes("STEP OWNER CONTRACT") || ownerOutput.includes("Preferred final decision format") || ownerOutput.includes("one of COMPLETE, INCOMPLETE_RETRY") || ownerOutput.includes("COMPLETE|INCOMPLETE_RETRY");
37233
+ if (decisionMatches.length === 0) {
37234
+ if (!hasMarker)
37235
+ return null;
37236
+ return {
37237
+ decision: "COMPLETE",
37238
+ reason: `Legacy completion marker observed: STEP_COMPLETE:${step.name}`
37239
+ };
37240
+ }
37241
+ const realMatches = outputLikelyContainsEchoedPrompt ? decisionMatches.filter((m) => {
37242
+ const lineStart = ownerOutput.lastIndexOf("\n", m.index) + 1;
37243
+ const lineEnd = ownerOutput.indexOf("\n", m.index);
37244
+ const line = ownerOutput.slice(lineStart, lineEnd === -1 ? void 0 : lineEnd);
37245
+ return !line.includes("COMPLETE|INCOMPLETE_RETRY");
37246
+ }) : decisionMatches;
37247
+ const decisionMatch = realMatches.length > 0 ? realMatches[realMatches.length - 1] : decisionMatches[decisionMatches.length - 1];
37248
+ const decision = decisionMatch?.[1]?.toUpperCase();
37249
+ if (decision !== "COMPLETE" && decision !== "INCOMPLETE_RETRY" && decision !== "INCOMPLETE_FAIL" && decision !== "NEEDS_CLARIFICATION") {
37250
+ return null;
37251
+ }
37252
+ const reasonPattern = /(?:^|\n)REASON:\s*(.+)/gi;
37253
+ const reasonMatches = [...ownerOutput.matchAll(reasonPattern)];
37254
+ const reasonMatch = outputLikelyContainsEchoedPrompt && reasonMatches.length > 1 ? reasonMatches[reasonMatches.length - 1] : reasonMatches[0];
37255
+ const reason = reasonMatch?.[1]?.trim();
37256
+ return {
37257
+ decision,
37258
+ reason: reason && reason !== "<one sentence>" ? reason : void 0
37259
+ };
36308
37260
  }
36309
- assertOwnerCompletionMarker(step, output, injectedTaskText) {
36310
- const marker = `STEP_COMPLETE:${step.name}`;
36311
- const taskHasMarker = injectedTaskText.includes(marker);
36312
- const first = output.indexOf(marker);
36313
- if (first === -1) {
36314
- throw new Error(`Step "${step.name}" owner completion marker missing: "${marker}"`);
37261
+ stripEchoedPromptLines(output, patterns) {
37262
+ return output.split("\n").map((line) => line.trim()).filter(Boolean).filter((line) => patterns.every((pattern) => !pattern.test(line))).join("\n");
37263
+ }
37264
+ firstMeaningfulLine(output) {
37265
+ return output.split("\n").map((line) => line.trim()).find(Boolean);
37266
+ }
37267
+ judgeOwnerCompletionByEvidence(stepName, ownerOutput) {
37268
+ if (/OWNER_DECISION:\s*(?:INCOMPLETE_RETRY|INCOMPLETE_FAIL|NEEDS_CLARIFICATION)\b/i.test(ownerOutput)) {
37269
+ return null;
36315
37270
  }
36316
- const outputLikelyContainsInjectedPrompt = output.includes("STEP OWNER CONTRACT") || output.includes("Output exactly: STEP_COMPLETE:");
36317
- if (taskHasMarker && outputLikelyContainsInjectedPrompt) {
36318
- const hasSecond = output.includes(marker, first + marker.length);
36319
- if (!hasSecond) {
36320
- throw new Error(`Step "${step.name}" owner completion marker missing in agent response: "${marker}"`);
36321
- }
37271
+ const sanitized = this.stripEchoedPromptLines(ownerOutput, [
37272
+ /^STEP OWNER CONTRACT:?$/i,
37273
+ /^Preferred final decision format:?$/i,
37274
+ /^OWNER_DECISION:\s*(?:COMPLETE\|INCOMPLETE_RETRY|<one of COMPLETE, INCOMPLETE_RETRY)/i,
37275
+ /^REASON:\s*<one sentence>$/i,
37276
+ /^Legacy completion marker still supported:/i,
37277
+ /^STEP_COMPLETE:/i
37278
+ ]);
37279
+ if (!sanitized)
37280
+ return null;
37281
+ const hasExplicitSelfRelease = /Calling\s+(?:[\w.-]+\.)?remove_agent\(\{[^<\n]*"reason":"task completed"/i.test(sanitized);
37282
+ const hasPositiveConclusion = /\b(complete(?:d)?|done|verified|looks correct|safe handoff|artifact verified)\b/i.test(sanitized) || /\bartifacts?\b.*\b(correct|verified|complete)\b/i.test(sanitized) || hasExplicitSelfRelease;
37283
+ const evidence = this.getStepCompletionEvidence(stepName);
37284
+ const hasValidatedCoordinationSignal = evidence?.coordinationSignals.some((signal) => signal.kind === "worker_done" || signal.kind === "lead_done" || signal.kind === "verification_passed" || signal.kind === "process_exit" && signal.value === "0") ?? false;
37285
+ const hasValidatedInspectionSignal = evidence?.toolSideEffects.some((effect) => effect.type === "owner_monitoring" && (/Checked git diff stats/i.test(effect.detail) || /Listed files for verification/i.test(effect.detail))) ?? false;
37286
+ const hasEvidenceSignal = hasValidatedCoordinationSignal || hasValidatedInspectionSignal;
37287
+ if (!hasPositiveConclusion || !hasEvidenceSignal) {
37288
+ return null;
37289
+ }
37290
+ return this.firstMeaningfulLine(sanitized) ?? "Evidence-backed completion";
37291
+ }
37292
+ /**
37293
+ * Process-exit fallback: when agent exits with code 0 but posts no coordination
37294
+ * signal, check if verification passes (or no verification is configured) and
37295
+ * infer completion. This is the key mechanism for reducing agent compliance
37296
+ * dependence — the runner trusts a clean exit + passing verification over
37297
+ * requiring exact signal text.
37298
+ */
37299
+ tryProcessExitFallback(step, specialistOutput, verificationTaskText, ownerOutput) {
37300
+ const gracePeriodMs = this.currentConfig?.swarm.completionGracePeriodMs ?? 5e3;
37301
+ if (gracePeriodMs === 0)
37302
+ return null;
37303
+ if (ownerOutput && /OWNER_DECISION:\s*(?:INCOMPLETE_RETRY|INCOMPLETE_FAIL|NEEDS_CLARIFICATION)\b/i.test(ownerOutput)) {
37304
+ return null;
36322
37305
  }
37306
+ const evidence = this.getStepCompletionEvidence(step.name);
37307
+ const hasCleanExit = evidence?.coordinationSignals.some((signal) => signal.kind === "process_exit" && signal.value === "0") ?? false;
37308
+ if (!hasCleanExit)
37309
+ return null;
37310
+ if (step.verification) {
37311
+ const verificationResult = this.runVerification(step.verification, specialistOutput, step.name, verificationTaskText, { allowFailure: true });
37312
+ if (!verificationResult.passed)
37313
+ return null;
37314
+ }
37315
+ return {
37316
+ completionReason: "completed_by_process_exit",
37317
+ reason: `Process exited with code 0${step.verification ? " and verification passed" : ""} \u2014 coordination signal not required`
37318
+ };
36323
37319
  }
36324
37320
  async runStepReviewGate(step, resolvedTask, specialistOutput, ownerOutput, ownerDef, reviewerDef, timeoutMs) {
36325
37321
  const reviewSnippetMax = 12e3;
@@ -36365,7 +37361,17 @@ Then output /exit.`;
36365
37361
  };
36366
37362
  await this.trajectory?.registerAgent(reviewerDef.name, "reviewer");
36367
37363
  this.postToChannel(`**[${step.name}]** Review started (reviewer: ${reviewerDef.name})`);
37364
+ this.recordStepToolSideEffect(step.name, {
37365
+ type: "review_started",
37366
+ detail: `Review started with ${reviewerDef.name}`,
37367
+ raw: { reviewer: reviewerDef.name }
37368
+ });
36368
37369
  const emitReviewCompleted = async (decision, reason) => {
37370
+ this.recordStepToolSideEffect(step.name, {
37371
+ type: "review_completed",
37372
+ detail: `Review ${decision} by ${reviewerDef.name}${reason ? `: ${reason}` : ""}`,
37373
+ raw: { reviewer: reviewerDef.name, decision, reason }
37374
+ });
36369
37375
  await this.trajectory?.reviewCompleted(step.name, reviewerDef.name, decision, reason);
36370
37376
  this.emit({
36371
37377
  type: "step:review-completed",
@@ -36409,6 +37415,9 @@ Then output /exit.`;
36409
37415
  };
36410
37416
  try {
36411
37417
  await this.spawnAndWait(reviewerDef, reviewStep, safetyTimeoutMs, {
37418
+ evidenceStepName: step.name,
37419
+ evidenceRole: "reviewer",
37420
+ logicalName: reviewerDef.name,
36412
37421
  onSpawned: ({ agent }) => {
36413
37422
  reviewerHandle = agent;
36414
37423
  },
@@ -36445,13 +37454,30 @@ Then output /exit.`;
36445
37454
  return reviewOutput;
36446
37455
  }
36447
37456
  parseReviewDecision(reviewOutput) {
37457
+ const strict = this.parseStrictReviewDecision(reviewOutput);
37458
+ if (strict) {
37459
+ return strict;
37460
+ }
37461
+ const tolerant = this.parseTolerantReviewDecision(reviewOutput);
37462
+ if (tolerant) {
37463
+ return tolerant;
37464
+ }
37465
+ return this.judgeReviewDecisionFromEvidence(reviewOutput);
37466
+ }
37467
+ parseStrictReviewDecision(reviewOutput) {
36448
37468
  const decisionPattern = /REVIEW_DECISION:\s*(APPROVE|REJECT)/gi;
36449
37469
  const decisionMatches = [...reviewOutput.matchAll(decisionPattern)];
36450
37470
  if (decisionMatches.length === 0) {
36451
37471
  return null;
36452
37472
  }
36453
37473
  const outputLikelyContainsEchoedPrompt = reviewOutput.includes("Return exactly") || reviewOutput.includes("REVIEW_DECISION: APPROVE or REJECT");
36454
- const decisionMatch = outputLikelyContainsEchoedPrompt && decisionMatches.length > 1 ? decisionMatches[decisionMatches.length - 1] : decisionMatches[0];
37474
+ const realReviewMatches = outputLikelyContainsEchoedPrompt ? decisionMatches.filter((m) => {
37475
+ const lineStart = reviewOutput.lastIndexOf("\n", m.index) + 1;
37476
+ const lineEnd = reviewOutput.indexOf("\n", m.index);
37477
+ const line = reviewOutput.slice(lineStart, lineEnd === -1 ? void 0 : lineEnd);
37478
+ return !line.includes("APPROVE or REJECT");
37479
+ }) : decisionMatches;
37480
+ const decisionMatch = realReviewMatches.length > 0 ? realReviewMatches[realReviewMatches.length - 1] : decisionMatches[decisionMatches.length - 1];
36455
37481
  const decision = decisionMatch?.[1]?.toUpperCase();
36456
37482
  if (decision !== "APPROVE" && decision !== "REJECT") {
36457
37483
  return null;
@@ -36465,6 +37491,80 @@ Then output /exit.`;
36465
37491
  reason: reason && reason !== "<one sentence>" ? reason : void 0
36466
37492
  };
36467
37493
  }
37494
+ parseTolerantReviewDecision(reviewOutput) {
37495
+ const sanitized = this.stripEchoedPromptLines(reviewOutput, [
37496
+ /^Return exactly:?$/i,
37497
+ /^REVIEW_DECISION:\s*APPROVE\s+or\s+REJECT$/i,
37498
+ /^REVIEW_REASON:\s*<one sentence>$/i
37499
+ ]);
37500
+ if (!sanitized) {
37501
+ return null;
37502
+ }
37503
+ const lines = sanitized.split("\n").map((line) => line.trim()).filter(Boolean);
37504
+ for (const line of lines) {
37505
+ const candidate = line.replace(/^REVIEW_DECISION:\s*/i, "").trim();
37506
+ const decision2 = this.normalizeReviewDecisionCandidate(candidate);
37507
+ if (decision2) {
37508
+ return {
37509
+ decision: decision2,
37510
+ reason: this.parseReviewReason(sanitized) ?? this.firstMeaningfulLine(sanitized)
37511
+ };
37512
+ }
37513
+ }
37514
+ const decision = this.normalizeReviewDecisionCandidate(lines.join(" "));
37515
+ if (!decision) {
37516
+ return null;
37517
+ }
37518
+ return {
37519
+ decision,
37520
+ reason: this.parseReviewReason(sanitized) ?? this.firstMeaningfulLine(sanitized)
37521
+ };
37522
+ }
37523
+ normalizeReviewDecisionCandidate(candidate) {
37524
+ const value = candidate.trim().toLowerCase();
37525
+ if (!value)
37526
+ return null;
37527
+ if (/^(approve|approved|complete|completed|pass|passed|accept|accepted|lgtm|ship it|looks good|looks fine)\b/i.test(value)) {
37528
+ return "approved";
37529
+ }
37530
+ if (/^(reject|rejected|retry|retry requested|fail|failed|incomplete|needs clarification|not complete|not ready|insufficient evidence)\b/i.test(value)) {
37531
+ return "rejected";
37532
+ }
37533
+ return null;
37534
+ }
37535
+ parseReviewReason(reviewOutput) {
37536
+ const reasonPattern = /REVIEW_REASON:\s*(.+)/gi;
37537
+ const reasonMatches = [...reviewOutput.matchAll(reasonPattern)];
37538
+ const outputLikelyContainsEchoedPrompt = reviewOutput.includes("Return exactly") || reviewOutput.includes("REVIEW_DECISION: APPROVE or REJECT");
37539
+ const reasonMatch = outputLikelyContainsEchoedPrompt && reasonMatches.length > 1 ? reasonMatches[reasonMatches.length - 1] : reasonMatches[0];
37540
+ const reason = reasonMatch?.[1]?.trim();
37541
+ return reason && reason !== "<one sentence>" ? reason : void 0;
37542
+ }
37543
+ judgeReviewDecisionFromEvidence(reviewOutput) {
37544
+ const sanitized = this.stripEchoedPromptLines(reviewOutput, [
37545
+ /^Return exactly:?$/i,
37546
+ /^REVIEW_DECISION:\s*APPROVE\s+or\s+REJECT$/i,
37547
+ /^REVIEW_REASON:\s*<one sentence>$/i
37548
+ ]);
37549
+ if (!sanitized) {
37550
+ return null;
37551
+ }
37552
+ const hasPositiveEvidence = /\b(approved?|complete(?:d)?|verified|looks good|looks fine|safe handoff|pass(?:ed)?)\b/i.test(sanitized);
37553
+ const hasNegativeEvidence = /\b(reject(?:ed)?|retry|fail(?:ed)?|incomplete|missing checks|insufficient evidence|not safe)\b/i.test(sanitized);
37554
+ if (hasNegativeEvidence) {
37555
+ return {
37556
+ decision: "rejected",
37557
+ reason: this.parseReviewReason(sanitized) ?? this.firstMeaningfulLine(sanitized)
37558
+ };
37559
+ }
37560
+ if (!hasPositiveEvidence) {
37561
+ return null;
37562
+ }
37563
+ return {
37564
+ decision: "approved",
37565
+ reason: this.parseReviewReason(sanitized) ?? this.firstMeaningfulLine(sanitized)
37566
+ };
37567
+ }
36468
37568
  combineStepAndReviewOutput(stepOutput, reviewOutput) {
36469
37569
  const primary = stepOutput.trimEnd();
36470
37570
  const review = reviewOutput.trim();
@@ -36532,7 +37632,7 @@ ${review}
36532
37632
  buildPresetInjection(preset) {
36533
37633
  switch (preset) {
36534
37634
  case "worker":
36535
- return "You are a non-interactive worker agent. Produce clean, structured output to stdout.\nDo NOT use mcp__relaycast__agent_add, add_agent, or any MCP tool to spawn sub-agents.\nDo NOT use mcp__relaycast__dm_send or any Relaycast messaging tools \u2014 you have no relay connection.\n\n";
37635
+ return "You are a non-interactive worker agent. Produce clean, structured output to stdout.\nDo NOT use mcp__relaycast__agent_add, add_agent, or any MCP tool to spawn sub-agents.\nDo NOT use mcp__relaycast__message_dm_send or any Relaycast messaging tools \u2014 you have no relay connection.\n\n";
36536
37636
  case "reviewer":
36537
37637
  return "You are a non-interactive reviewer agent. Read the specified files/artifacts and produce a clear verdict.\nDo NOT spawn sub-agents or use any Relaycast messaging tools.\n\n";
36538
37638
  case "analyst":
@@ -36671,10 +37771,18 @@ DO NOT:
36671
37771
  reject(new Error(`Failed to spawn ${cmd}: ${err.message}`));
36672
37772
  });
36673
37773
  });
37774
+ this.captureStepTerminalEvidence(step.name, {}, { exitCode, exitSignal });
36674
37775
  return { output, exitCode, exitSignal };
36675
37776
  } finally {
36676
- const combinedOutput = stdoutChunks.join("") + stderrChunks.join("");
37777
+ const stdout = stdoutChunks.join("");
37778
+ const stderr = stderrChunks.join("");
37779
+ const combinedOutput = stdout + stderr;
36677
37780
  this.lastFailedStepOutput.set(step.name, combinedOutput);
37781
+ this.captureStepTerminalEvidence(step.name, {
37782
+ stdout,
37783
+ stderr,
37784
+ combined: combinedOutput
37785
+ });
36678
37786
  stopHeartbeat?.();
36679
37787
  logStream.end();
36680
37788
  this.unregisterWorker(agentName);
@@ -36687,6 +37795,7 @@ DO NOT:
36687
37795
  if (!this.relay) {
36688
37796
  throw new Error("AgentRelay not initialized");
36689
37797
  }
37798
+ const evidenceStepName = options.evidenceStepName ?? step.name;
36690
37799
  const requestedName = `${step.name}${options.agentNameSuffix ? `-${options.agentNameSuffix}` : ""}-${(this.currentRunId ?? this.generateShortId()).slice(0, 8)}`;
36691
37800
  let agentName = requestedName;
36692
37801
  const role = agentDef.role?.toLowerCase() ?? "";
@@ -36714,11 +37823,17 @@ DO NOT:
36714
37823
  let ptyChunks = [];
36715
37824
  try {
36716
37825
  const agentCwd = this.resolveAgentCwd(agentDef);
37826
+ const interactiveSpawnPolicy = resolveSpawnPolicy({
37827
+ AGENT_NAME: agentName,
37828
+ AGENT_CLI: agentDef.cli,
37829
+ RELAY_API_KEY: this.relayApiKey ?? "workflow-runner",
37830
+ AGENT_CHANNELS: (agentChannels ?? []).join(",")
37831
+ });
36717
37832
  agent = await this.relay.spawnPty({
36718
37833
  name: agentName,
36719
37834
  cli: agentDef.cli,
36720
37835
  model: agentDef.constraints?.model,
36721
- args: [],
37836
+ args: interactiveSpawnPolicy.args,
36722
37837
  channels: agentChannels,
36723
37838
  task: taskWithExit,
36724
37839
  idleThresholdSecs: agentDef.constraints?.idleThresholdSecs,
@@ -36744,16 +37859,27 @@ DO NOT:
36744
37859
  const oldListener = this.ptyListeners.get(oldName);
36745
37860
  if (oldListener) {
36746
37861
  this.ptyListeners.delete(oldName);
36747
- this.ptyListeners.set(agent.name, (chunk) => {
37862
+ const resolvedAgentName = agent.name;
37863
+ this.ptyListeners.set(resolvedAgentName, (chunk) => {
36748
37864
  const stripped = _WorkflowRunner.stripAnsi(chunk);
36749
- this.ptyOutputBuffers.get(agent.name)?.push(stripped);
37865
+ this.ptyOutputBuffers.get(resolvedAgentName)?.push(stripped);
36750
37866
  newLogStream.write(chunk);
36751
- options.onChunk?.({ agentName: agent.name, chunk });
37867
+ options.onChunk?.({ agentName: resolvedAgentName, chunk });
36752
37868
  });
36753
37869
  }
36754
37870
  agentName = agent.name;
36755
37871
  }
36756
- await options.onSpawned?.({ requestedName, actualName: agent.name, agent });
37872
+ const liveAgent = agent;
37873
+ await options.onSpawned?.({ requestedName, actualName: liveAgent.name, agent: liveAgent });
37874
+ this.runtimeStepAgents.set(liveAgent.name, {
37875
+ stepName: evidenceStepName,
37876
+ role: options.evidenceRole ?? agentDef.role ?? "agent",
37877
+ logicalName: options.logicalName ?? agentDef.name
37878
+ });
37879
+ const signalParticipant = this.resolveSignalParticipantKind(options.evidenceRole ?? agentDef.role ?? "agent");
37880
+ if (signalParticipant) {
37881
+ this.rememberStepSignalSender(evidenceStepName, signalParticipant, liveAgent.name, options.logicalName ?? agentDef.name);
37882
+ }
36757
37883
  let workerPid;
36758
37884
  try {
36759
37885
  const rawAgents = await this.relay.listAgentsRaw();
@@ -36762,8 +37888,8 @@ DO NOT:
36762
37888
  }
36763
37889
  this.registerWorker(agentName, agentDef.cli, step.task ?? "", workerPid);
36764
37890
  if (this.relayApiKey) {
36765
- const agentClient = await this.registerRelaycastExternalAgent(agent.name, `Workflow agent for step "${step.name}" (${agentDef.cli})`).catch((err) => {
36766
- console.warn(`[WorkflowRunner] Failed to register ${agent.name} in Relaycast:`, err?.message ?? err);
37891
+ const agentClient = await this.registerRelaycastExternalAgent(liveAgent.name, `Workflow agent for step "${step.name}" (${agentDef.cli})`).catch((err) => {
37892
+ console.warn(`[WorkflowRunner] Failed to register ${liveAgent.name} in Relaycast:`, err?.message ?? err);
36767
37893
  return null;
36768
37894
  });
36769
37895
  if (agentClient) {
@@ -36775,21 +37901,23 @@ DO NOT:
36775
37901
  await channelAgent?.channels.invite(this.channel, agent.name).catch(() => {
36776
37902
  });
36777
37903
  }
36778
- this.postToChannel(`**[${step.name}]** Assigned to \`${agent.name}\``);
37904
+ this.log(`[${step.name}] Assigned to ${agent.name}`);
36779
37905
  this.activeAgentHandles.set(agentName, agent);
36780
- exitResult = await this.waitForExitWithIdleNudging(agent, agentDef, step, timeoutMs);
37906
+ exitResult = await this.waitForExitWithIdleNudging(agent, agentDef, step, timeoutMs, options.preserveOnIdle ?? this.shouldPreserveIdleSupervisor(agentDef, step, options.evidenceRole));
36781
37907
  stopHeartbeat?.();
36782
37908
  if (exitResult === "timeout") {
36783
- if (step.verification?.type === "file_exists") {
36784
- const verifyPath = import_node_path8.default.resolve(this.cwd, step.verification.value);
36785
- if ((0, import_node_fs4.existsSync)(verifyPath)) {
36786
- this.postToChannel(`**[${step.name}]** Agent idle after completing work \u2014 releasing`);
36787
- await agent.release();
36788
- } else {
37909
+ let timeoutRecovered = false;
37910
+ if (step.verification) {
37911
+ const ptyOutput = (this.ptyOutputBuffers.get(agentName) ?? []).join("");
37912
+ const verificationResult = this.runVerification(step.verification, ptyOutput, step.name, void 0, { allowFailure: true });
37913
+ if (verificationResult.passed) {
37914
+ this.log(`[${step.name}] Agent timed out but verification passed \u2014 treating as complete`);
37915
+ this.postToChannel(`**[${step.name}]** Agent idle after completing work \u2014 verification passed, releasing`);
36789
37916
  await agent.release();
36790
- throw new Error(`Step "${step.name}" timed out after ${timeoutMs ?? "unknown"}ms`);
37917
+ timeoutRecovered = true;
36791
37918
  }
36792
- } else {
37919
+ }
37920
+ if (!timeoutRecovered) {
36793
37921
  await agent.release();
36794
37922
  throw new Error(`Step "${step.name}" timed out after ${timeoutMs ?? "unknown"}ms`);
36795
37923
  }
@@ -36800,6 +37928,19 @@ DO NOT:
36800
37928
  } finally {
36801
37929
  ptyChunks = this.ptyOutputBuffers.get(agentName) ?? [];
36802
37930
  this.lastFailedStepOutput.set(step.name, ptyChunks.join(""));
37931
+ if (ptyChunks.length > 0 || agent?.exitCode !== void 0 || agent?.exitSignal !== void 0) {
37932
+ this.captureStepTerminalEvidence(evidenceStepName, {
37933
+ stdout: ptyChunks.length > 0 ? ptyChunks.join("") : void 0,
37934
+ combined: ptyChunks.length > 0 ? ptyChunks.join("") : void 0
37935
+ }, {
37936
+ exitCode: agent?.exitCode,
37937
+ exitSignal: agent?.exitSignal
37938
+ }, {
37939
+ sender: options.logicalName ?? agentDef.name,
37940
+ actor: agent?.name ?? agentName,
37941
+ role: options.evidenceRole ?? agentDef.role ?? "agent"
37942
+ });
37943
+ }
36803
37944
  stopHeartbeat?.();
36804
37945
  this.activeAgentHandles.delete(agentName);
36805
37946
  this.ptyOutputBuffers.delete(agentName);
@@ -36811,6 +37952,7 @@ DO NOT:
36811
37952
  }
36812
37953
  this.unregisterWorker(agentName);
36813
37954
  this.supervisedRuntimeAgents.delete(agentName);
37955
+ this.runtimeStepAgents.delete(agentName);
36814
37956
  }
36815
37957
  let output;
36816
37958
  if (ptyChunks.length > 0) {
@@ -36819,6 +37961,13 @@ DO NOT:
36819
37961
  const summaryPath = import_node_path8.default.join(this.summaryDir, `${step.name}.md`);
36820
37962
  output = (0, import_node_fs4.existsSync)(summaryPath) ? await (0, import_promises3.readFile)(summaryPath, "utf-8") : exitResult === "timeout" ? "Agent completed (released after idle timeout)" : exitResult === "released" ? "Agent completed (idle \u2014 treated as done)" : `Agent exited (${exitResult})`;
36821
37963
  }
37964
+ if (ptyChunks.length === 0) {
37965
+ this.captureStepTerminalEvidence(evidenceStepName, { stdout: output, combined: output }, { exitCode: agent?.exitCode, exitSignal: agent?.exitSignal }, {
37966
+ sender: options.logicalName ?? agentDef.name,
37967
+ actor: agent?.name ?? agentName,
37968
+ role: options.evidenceRole ?? agentDef.role ?? "agent"
37969
+ });
37970
+ }
36822
37971
  return {
36823
37972
  output,
36824
37973
  exitCode: agent?.exitCode,
@@ -36846,29 +37995,90 @@ DO NOT:
36846
37995
  "orchestrator",
36847
37996
  "auctioneer"
36848
37997
  ]);
37998
+ isLeadLikeAgent(agentDef, roleOverride) {
37999
+ if (agentDef.preset === "lead")
38000
+ return true;
38001
+ const role = (roleOverride ?? agentDef.role ?? "").toLowerCase();
38002
+ const nameLC = agentDef.name.toLowerCase();
38003
+ return [..._WorkflowRunner.HUB_ROLES].some((hubRole) => new RegExp(`\\b${hubRole}\\b`, "i").test(nameLC) || new RegExp(`\\b${hubRole}\\b`, "i").test(role));
38004
+ }
38005
+ shouldPreserveIdleSupervisor(agentDef, step, evidenceRole) {
38006
+ if (evidenceRole && /\bowner\b/i.test(evidenceRole)) {
38007
+ return true;
38008
+ }
38009
+ if (!this.isLeadLikeAgent(agentDef, evidenceRole)) {
38010
+ return false;
38011
+ }
38012
+ const task = step.task ?? "";
38013
+ return /\b(wait|waiting|monitor|supervis|check inbox|check.*channel|poll|DONE|_DONE|signal|handoff)\b/i.test(task);
38014
+ }
36849
38015
  /**
36850
38016
  * Wait for agent exit with idle detection and nudging.
36851
38017
  * If no idle nudge config is set, falls through to simple waitForExit.
36852
38018
  */
36853
- async waitForExitWithIdleNudging(agent, agentDef, step, timeoutMs) {
38019
+ async waitForExitWithIdleNudging(agent, agentDef, step, timeoutMs, preserveIdleSupervisor = false) {
36854
38020
  const nudgeConfig = this.currentConfig?.swarm.idleNudge;
36855
38021
  if (!nudgeConfig) {
36856
- const result = await Promise.race([
36857
- agent.waitForExit(timeoutMs).then((r) => ({ kind: "exit", result: r })),
36858
- agent.waitForIdle(timeoutMs).then((r) => ({ kind: "idle", result: r }))
36859
- ]);
36860
- if (result.kind === "idle" && result.result === "idle") {
36861
- this.log(`[${step.name}] Agent "${agent.name}" went idle \u2014 treating as complete`);
36862
- this.postToChannel(`**[${step.name}]** Agent \`${agent.name}\` idle \u2014 treating as complete`);
36863
- await agent.release();
36864
- return "released";
38022
+ if (preserveIdleSupervisor) {
38023
+ this.log(`[${step.name}] Supervising agent "${agent.name}" may idle while waiting \u2014 using exit-only completion`);
38024
+ return agent.waitForExit(timeoutMs);
38025
+ }
38026
+ const idleLoopStart = Date.now();
38027
+ while (true) {
38028
+ const elapsed = Date.now() - idleLoopStart;
38029
+ const remaining = timeoutMs != null ? Math.max(0, timeoutMs - elapsed) : void 0;
38030
+ if (remaining != null && remaining <= 0) {
38031
+ return "timeout";
38032
+ }
38033
+ const result = await Promise.race([
38034
+ agent.waitForExit(remaining).then((r) => ({ kind: "exit", result: r })),
38035
+ agent.waitForIdle(remaining).then((r) => ({ kind: "idle", result: r }))
38036
+ ]);
38037
+ if (result.kind === "idle" && result.result === "idle") {
38038
+ if (step.verification && step.verification.type === "output_contains") {
38039
+ const token = step.verification.value;
38040
+ const ptyOutput = (this.ptyOutputBuffers.get(agent.name) ?? []).join("");
38041
+ const taskText = step.task ?? "";
38042
+ const taskHasToken = taskText.includes(token);
38043
+ let verificationPassed = true;
38044
+ if (taskHasToken) {
38045
+ const first = ptyOutput.indexOf(token);
38046
+ verificationPassed = first !== -1 && ptyOutput.includes(token, first + token.length);
38047
+ } else {
38048
+ verificationPassed = ptyOutput.includes(token);
38049
+ }
38050
+ if (!verificationPassed) {
38051
+ this.log(`[${step.name}] Agent "${agent.name}" went idle but verification not yet passed \u2014 waiting for more output`);
38052
+ const idleGraceSecs = 15;
38053
+ const graceResult = await Promise.race([
38054
+ agent.waitForExit(idleGraceSecs * 1e3).then((r) => ({ kind: "exit", result: r })),
38055
+ agent.waitForIdle(idleGraceSecs * 1e3).then((r) => ({ kind: "idle", result: r }))
38056
+ ]);
38057
+ if (graceResult.kind === "idle" && graceResult.result === "idle") {
38058
+ continue;
38059
+ }
38060
+ if (graceResult.kind === "exit") {
38061
+ return graceResult.result;
38062
+ }
38063
+ this.log(`[${step.name}] Agent "${agent.name}" still idle after ${idleGraceSecs}s grace \u2014 releasing`);
38064
+ this.postToChannel(`**[${step.name}]** Agent \`${agent.name}\` idle \u2014 releasing (verification pending)`);
38065
+ await agent.release();
38066
+ return "released";
38067
+ }
38068
+ }
38069
+ this.log(`[${step.name}] Agent "${agent.name}" went idle \u2014 treating as complete`);
38070
+ this.postToChannel(`**[${step.name}]** Agent \`${agent.name}\` idle \u2014 treating as complete`);
38071
+ await agent.release();
38072
+ return "released";
38073
+ }
38074
+ return result.result;
36865
38075
  }
36866
- return result.result;
36867
38076
  }
36868
38077
  const nudgeAfterMs = nudgeConfig.nudgeAfterMs ?? 12e4;
36869
38078
  const escalateAfterMs = nudgeConfig.escalateAfterMs ?? 12e4;
36870
38079
  const maxNudges = nudgeConfig.maxNudges ?? 1;
36871
38080
  let nudgeCount = 0;
38081
+ let preservedSupervisorNoticeSent = false;
36872
38082
  const startTime = Date.now();
36873
38083
  while (true) {
36874
38084
  const elapsed = Date.now() - startTime;
@@ -36892,6 +38102,14 @@ DO NOT:
36892
38102
  this.emit({ type: "step:nudged", runId: this.currentRunId ?? "", stepName: step.name, nudgeCount });
36893
38103
  continue;
36894
38104
  }
38105
+ if (preserveIdleSupervisor) {
38106
+ if (!preservedSupervisorNoticeSent) {
38107
+ this.log(`[${step.name}] Supervising agent "${agent.name}" stayed idle after ${nudgeCount} nudge(s) \u2014 preserving until exit or timeout`);
38108
+ this.postToChannel(`**[${step.name}]** Supervising agent \`${agent.name}\` is waiting on handoff \u2014 keeping it alive until it exits or the step times out`);
38109
+ preservedSupervisorNoticeSent = true;
38110
+ }
38111
+ continue;
38112
+ }
36895
38113
  this.postToChannel(`**[${step.name}]** Agent \`${agent.name}\` still idle after ${nudgeCount} nudge(s) \u2014 force-releasing`);
36896
38114
  this.emit({ type: "step:force-released", runId: this.currentRunId ?? "", stepName: step.name });
36897
38115
  await agent.release();
@@ -36949,7 +38167,31 @@ DO NOT:
36949
38167
  return void 0;
36950
38168
  }
36951
38169
  // ── Verification ────────────────────────────────────────────────────────
36952
- runVerification(check2, output, stepName, injectedTaskText) {
38170
+ runVerification(check2, output, stepName, injectedTaskText, options) {
38171
+ const fail = (message) => {
38172
+ const observedAt2 = (/* @__PURE__ */ new Date()).toISOString();
38173
+ this.recordStepToolSideEffect(stepName, {
38174
+ type: "verification_observed",
38175
+ detail: message,
38176
+ observedAt: observedAt2,
38177
+ raw: { passed: false, type: check2.type, value: check2.value }
38178
+ });
38179
+ this.getOrCreateStepEvidenceRecord(stepName).evidence.coordinationSignals.push({
38180
+ kind: "verification_failed",
38181
+ source: "verification",
38182
+ text: message,
38183
+ observedAt: observedAt2,
38184
+ value: check2.value
38185
+ });
38186
+ if (options?.allowFailure) {
38187
+ return {
38188
+ passed: false,
38189
+ completionReason: "failed_verification",
38190
+ error: message
38191
+ };
38192
+ }
38193
+ throw new WorkflowCompletionError(message, "failed_verification");
38194
+ };
36953
38195
  switch (check2.type) {
36954
38196
  case "output_contains": {
36955
38197
  const token = check2.value;
@@ -36958,10 +38200,10 @@ DO NOT:
36958
38200
  const first = output.indexOf(token);
36959
38201
  const hasSecond = first !== -1 && output.includes(token, first + token.length);
36960
38202
  if (!hasSecond) {
36961
- throw new Error(`Verification failed for "${stepName}": output does not contain "${token}" (token found only in task injection \u2014 agent must output it explicitly)`);
38203
+ return fail(`Verification failed for "${stepName}": output does not contain "${token}" (token found only in task injection \u2014 agent must output it explicitly)`);
36962
38204
  }
36963
38205
  } else if (!output.includes(token)) {
36964
- throw new Error(`Verification failed for "${stepName}": output does not contain "${token}"`);
38206
+ return fail(`Verification failed for "${stepName}": output does not contain "${token}"`);
36965
38207
  }
36966
38208
  break;
36967
38209
  }
@@ -36969,12 +38211,34 @@ DO NOT:
36969
38211
  break;
36970
38212
  case "file_exists":
36971
38213
  if (!(0, import_node_fs4.existsSync)(import_node_path8.default.resolve(this.cwd, check2.value))) {
36972
- throw new Error(`Verification failed for "${stepName}": file "${check2.value}" does not exist`);
38214
+ return fail(`Verification failed for "${stepName}": file "${check2.value}" does not exist`);
36973
38215
  }
36974
38216
  break;
36975
38217
  case "custom":
36976
- break;
36977
- }
38218
+ return { passed: false };
38219
+ }
38220
+ if (options?.completionMarkerFound === false) {
38221
+ this.log(`[${stepName}] Verification passed without legacy STEP_COMPLETE marker; allowing completion`);
38222
+ }
38223
+ const successMessage = options?.completionMarkerFound === false ? `Verification passed without legacy STEP_COMPLETE marker` : `Verification passed`;
38224
+ const observedAt = (/* @__PURE__ */ new Date()).toISOString();
38225
+ this.recordStepToolSideEffect(stepName, {
38226
+ type: "verification_observed",
38227
+ detail: successMessage,
38228
+ observedAt,
38229
+ raw: { passed: true, type: check2.type, value: check2.value }
38230
+ });
38231
+ this.getOrCreateStepEvidenceRecord(stepName).evidence.coordinationSignals.push({
38232
+ kind: "verification_passed",
38233
+ source: "verification",
38234
+ text: successMessage,
38235
+ observedAt,
38236
+ value: check2.value
38237
+ });
38238
+ return {
38239
+ passed: true,
38240
+ completionReason: "completed_verified"
38241
+ };
36978
38242
  }
36979
38243
  // ── State helpers ─────────────────────────────────────────────────────
36980
38244
  async updateRunStatus(runId, status, error48) {
@@ -36990,13 +38254,16 @@ DO NOT:
36990
38254
  }
36991
38255
  await this.db.updateRun(runId, patch);
36992
38256
  }
36993
- async markStepFailed(state, error48, runId, exitInfo) {
38257
+ async markStepFailed(state, error48, runId, exitInfo, completionReason) {
38258
+ this.captureStepTerminalEvidence(state.row.stepName, {}, exitInfo);
36994
38259
  state.row.status = "failed";
36995
38260
  state.row.error = error48;
38261
+ state.row.completionReason = completionReason;
36996
38262
  state.row.completedAt = (/* @__PURE__ */ new Date()).toISOString();
36997
38263
  await this.db.updateStep(state.row.id, {
36998
38264
  status: "failed",
36999
38265
  error: error48,
38266
+ completionReason,
37000
38267
  completedAt: state.row.completedAt,
37001
38268
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
37002
38269
  });
@@ -37008,6 +38275,7 @@ DO NOT:
37008
38275
  exitCode: exitInfo?.exitCode,
37009
38276
  exitSignal: exitInfo?.exitSignal
37010
38277
  });
38278
+ this.finalizeStepEvidence(state.row.stepName, "failed", state.row.completedAt, completionReason);
37011
38279
  }
37012
38280
  async markDownstreamSkipped(failedStepName, allSteps, stepStates, runId) {
37013
38281
  const queue = [failedStepName];
@@ -37096,7 +38364,7 @@ DO NOT:
37096
38364
  RELAY SETUP \u2014 do this FIRST before any other relay tool:
37097
38365
  1. Call: register(name="${agentName}")
37098
38366
  This authenticates you in the Relaycast workspace.
37099
- ALL relay tools (mcp__relaycast__dm_send, mcp__relaycast__inbox_check, mcp__relaycast__message_post, etc.) require
38367
+ ALL relay tools (mcp__relaycast__message_dm_send, mcp__relaycast__message_inbox_check, mcp__relaycast__message_post, etc.) require
37100
38368
  registration first \u2014 they will fail with "Not registered" otherwise.
37101
38369
  2. Your agent name is "${agentName}" \u2014 use this exact name when registering.`;
37102
38370
  }
@@ -37105,7 +38373,7 @@ RELAY SETUP \u2014 do this FIRST before any other relay tool:
37105
38373
 
37106
38374
  ` : "";
37107
38375
  const subAgentOption = cli === "claude" ? "Option 2 \u2014 Use built-in sub-agents (Task tool) for research or scoped work:\n - Good for exploring code, reading files, or making targeted changes\n - Can run multiple sub-agents in parallel\n\n" : "";
37108
- return "---\nAUTONOMOUS DELEGATION \u2014 READ THIS BEFORE STARTING:\n" + timeoutNote + 'Before diving in, assess whether this task is too large or complex for a single agent. If it involves multiple independent subtasks, touches many files, or could take a long time, you should break it down and delegate to helper agents to avoid timeouts.\n\nOption 1 \u2014 Spawn relay agents (for real parallel coding work):\n - mcp__relaycast__agent_add(name="helper-1", cli="claude", task="Specific subtask description")\n - Coordinate via mcp__relaycast__dm_send(to="helper-1", text="...")\n - Check on them with mcp__relaycast__inbox_check()\n - Clean up when done: mcp__relaycast__agent_remove(name="helper-1")\n\n' + subAgentOption + `Guidelines:
38376
+ return "---\nAUTONOMOUS DELEGATION \u2014 READ THIS BEFORE STARTING:\n" + timeoutNote + 'Before diving in, assess whether this task is too large or complex for a single agent. If it involves multiple independent subtasks, touches many files, or could take a long time, you should break it down and delegate to helper agents to avoid timeouts.\n\nOption 1 \u2014 Spawn relay agents (for real parallel coding work):\n - mcp__relaycast__agent_add(name="helper-1", cli="claude", task="Specific subtask description")\n - Coordinate via mcp__relaycast__message_dm_send(to="helper-1", text="...")\n - Check on them with mcp__relaycast__message_inbox_check()\n - Clean up when done: mcp__relaycast__agent_remove(name="helper-1")\n\n' + subAgentOption + `Guidelines:
37109
38377
  - You are the lead \u2014 delegate but stay in control, track progress, integrate results
37110
38378
  - Give each helper a clear, self-contained task with enough context to work independently
37111
38379
  - For simple or quick work, just do it yourself \u2014 don't over-delegate
@@ -37114,9 +38382,23 @@ RELAY SETUP \u2014 do this FIRST before any other relay tool:
37114
38382
  "RELAY SETUP: First call register(name='<exact-agent-name>') before any other relay tool."`;
37115
38383
  }
37116
38384
  /** Post a message to the workflow channel. Fire-and-forget — never throws or blocks. */
37117
- postToChannel(text) {
38385
+ postToChannel(text, options = {}) {
37118
38386
  if (!this.relayApiKey || !this.channel)
37119
38387
  return;
38388
+ this.recordChannelEvidence(text, options);
38389
+ const stepName = options.stepName ?? this.inferStepNameFromChannelText(text);
38390
+ if (stepName) {
38391
+ this.recordStepToolSideEffect(stepName, {
38392
+ type: "post_channel_message",
38393
+ detail: text.slice(0, 240),
38394
+ raw: {
38395
+ actor: options.actor,
38396
+ role: options.role,
38397
+ target: options.target ?? this.channel,
38398
+ origin: options.origin ?? "runner_post"
38399
+ }
38400
+ });
38401
+ }
37120
38402
  this.ensureRelaycastRunnerAgent().then((agent) => agent.send(this.channel, text)).catch(() => {
37121
38403
  });
37122
38404
  }
@@ -37259,7 +38541,8 @@ ${excerpt}` : "";
37259
38541
  attempts: state.row.retryCount + 1,
37260
38542
  output: state.row.output,
37261
38543
  error: state.row.error,
37262
- verificationPassed: state.row.status === "completed" && stepsWithVerification.has(name)
38544
+ verificationPassed: state.row.status === "completed" && stepsWithVerification.has(name),
38545
+ completionMode: state.row.completionReason ? this.buildStepCompletionDecision(name, state.row.completionReason)?.mode : void 0
37263
38546
  });
37264
38547
  }
37265
38548
  return outcomes;
@@ -37367,16 +38650,22 @@ ${excerpt}` : "";
37367
38650
  }
37368
38651
  /** Persist step output to disk and post full output as a channel message. */
37369
38652
  async persistStepOutput(runId, stepName, output) {
38653
+ const outputPath = import_node_path8.default.join(this.getStepOutputDir(runId), `${stepName}.md`);
37370
38654
  try {
37371
38655
  const dir = this.getStepOutputDir(runId);
37372
38656
  (0, import_node_fs4.mkdirSync)(dir, { recursive: true });
37373
38657
  const cleaned = _WorkflowRunner.stripAnsi(output);
37374
- await (0, import_promises3.writeFile)(import_node_path8.default.join(dir, `${stepName}.md`), cleaned);
38658
+ await (0, import_promises3.writeFile)(outputPath, cleaned);
37375
38659
  } catch {
37376
38660
  }
38661
+ this.recordStepToolSideEffect(stepName, {
38662
+ type: "persist_step_output",
38663
+ detail: `Persisted step output to ${this.normalizeEvidencePath(outputPath)}`,
38664
+ raw: { path: outputPath }
38665
+ });
37377
38666
  const scrubbed = _WorkflowRunner.scrubForChannel(output);
37378
38667
  if (scrubbed.length === 0) {
37379
- this.postToChannel(`**[${stepName}]** Step completed \u2014 output written to disk`);
38668
+ this.postToChannel(`**[${stepName}]** Step completed \u2014 output written to disk`, { stepName });
37380
38669
  return;
37381
38670
  }
37382
38671
  const maxMsg = 2e3;
@@ -37384,7 +38673,7 @@ ${excerpt}` : "";
37384
38673
  this.postToChannel(`**[${stepName}] Output:**
37385
38674
  \`\`\`
37386
38675
  ${preview}
37387
- \`\`\``);
38676
+ \`\`\``, { stepName });
37388
38677
  }
37389
38678
  /** Load persisted step output from disk. */
37390
38679
  loadStepOutput(runId, stepName) {
@@ -37678,6 +38967,8 @@ var WorkflowBuilder = class {
37678
38967
  def.task = options.task;
37679
38968
  if (options.channels !== void 0)
37680
38969
  def.channels = options.channels;
38970
+ if (options.preset !== void 0)
38971
+ def.preset = options.preset;
37681
38972
  if (options.interactive !== void 0)
37682
38973
  def.interactive = options.interactive;
37683
38974
  if (options.model !== void 0 || options.maxTokens !== void 0 || options.timeoutMs !== void 0 || options.retries !== void 0 || options.idleThresholdSecs !== void 0) {
@@ -37696,21 +38987,29 @@ var WorkflowBuilder = class {
37696
38987
  this._agents.push(def);
37697
38988
  return this;
37698
38989
  }
37699
- /** Add a workflow step. */
38990
+ /** Add a workflow step (agent or deterministic). */
37700
38991
  step(name, options) {
37701
- const step = {
37702
- name,
37703
- agent: options.agent,
37704
- task: options.task
37705
- };
38992
+ const step = { name };
38993
+ if ("type" in options && options.type === "deterministic") {
38994
+ step.type = "deterministic";
38995
+ step.command = options.command;
38996
+ if (options.failOnError !== void 0)
38997
+ step.failOnError = options.failOnError;
38998
+ if (options.captureOutput !== void 0)
38999
+ step.captureOutput = options.captureOutput;
39000
+ } else {
39001
+ const agentOpts = options;
39002
+ step.agent = agentOpts.agent;
39003
+ step.task = agentOpts.task;
39004
+ if (agentOpts.verification !== void 0)
39005
+ step.verification = agentOpts.verification;
39006
+ if (agentOpts.retries !== void 0)
39007
+ step.retries = agentOpts.retries;
39008
+ }
37706
39009
  if (options.dependsOn !== void 0)
37707
39010
  step.dependsOn = options.dependsOn;
37708
- if (options.verification !== void 0)
37709
- step.verification = options.verification;
37710
39011
  if (options.timeoutMs !== void 0)
37711
39012
  step.timeoutMs = options.timeoutMs;
37712
- if (options.retries !== void 0)
37713
- step.retries = options.retries;
37714
39013
  this._steps.push(step);
37715
39014
  return this;
37716
39015
  }
@@ -37727,8 +39026,9 @@ var WorkflowBuilder = class {
37727
39026
  }
37728
39027
  /** Build and return the RelayYamlConfig object. */
37729
39028
  toConfig() {
37730
- if (this._agents.length === 0) {
37731
- throw new Error("Workflow must have at least one agent");
39029
+ const hasAgentSteps = this._steps.some((s) => s.type !== "deterministic" && s.type !== "worktree");
39030
+ if (hasAgentSteps && this._agents.length === 0) {
39031
+ throw new Error("Workflow must have at least one agent when using agent steps");
37732
39032
  }
37733
39033
  if (this._steps.length === 0) {
37734
39034
  throw new Error("Workflow must have at least one step");
@@ -39013,131 +40313,6 @@ var TemplateRegistry = class {
39013
40313
  }
39014
40314
  };
39015
40315
 
39016
- // packages/sdk/dist/spawn-from-env.js
39017
- var BYPASS_FLAGS = {
39018
- claude: { flag: "--dangerously-skip-permissions" },
39019
- codex: {
39020
- flag: "--dangerously-bypass-approvals-and-sandbox",
39021
- aliases: ["--full-auto"]
39022
- },
39023
- gemini: {
39024
- flag: "--yolo",
39025
- aliases: ["-y"]
39026
- }
39027
- };
39028
- function getBypassFlagConfig(cli) {
39029
- const baseCli = cli.includes(":") ? cli.split(":")[0] : cli;
39030
- return BYPASS_FLAGS[baseCli];
39031
- }
39032
- function parseSpawnEnv(env = process.env) {
39033
- const AGENT_NAME = env.AGENT_NAME;
39034
- const AGENT_CLI = env.AGENT_CLI;
39035
- const RELAY_API_KEY = env.RELAY_API_KEY;
39036
- const missing = [];
39037
- if (!AGENT_NAME)
39038
- missing.push("AGENT_NAME");
39039
- if (!AGENT_CLI)
39040
- missing.push("AGENT_CLI");
39041
- if (!RELAY_API_KEY)
39042
- missing.push("RELAY_API_KEY");
39043
- if (missing.length > 0) {
39044
- throw new Error(`[spawn-from-env] Missing required environment variables: ${missing.join(", ")}`);
39045
- }
39046
- return {
39047
- AGENT_NAME,
39048
- AGENT_CLI,
39049
- RELAY_API_KEY,
39050
- AGENT_TASK: env.AGENT_TASK || void 0,
39051
- AGENT_ARGS: env.AGENT_ARGS || void 0,
39052
- AGENT_CWD: env.AGENT_CWD || void 0,
39053
- AGENT_CHANNELS: env.AGENT_CHANNELS || void 0,
39054
- RELAY_BASE_URL: env.RELAY_BASE_URL || void 0,
39055
- BROKER_BINARY_PATH: env.BROKER_BINARY_PATH || void 0,
39056
- AGENT_MODEL: env.AGENT_MODEL || void 0,
39057
- AGENT_DISABLE_DEFAULT_BYPASS: env.AGENT_DISABLE_DEFAULT_BYPASS || void 0
39058
- };
39059
- }
39060
- function parseArgs(raw) {
39061
- if (!raw)
39062
- return [];
39063
- const trimmed = raw.trim();
39064
- if (trimmed.startsWith("[")) {
39065
- try {
39066
- const parsed = JSON.parse(trimmed);
39067
- if (Array.isArray(parsed))
39068
- return parsed.map(String);
39069
- } catch {
39070
- }
39071
- }
39072
- return trimmed.split(/\s+/).filter(Boolean);
39073
- }
39074
- function resolveSpawnPolicy(input) {
39075
- const extraArgs = parseArgs(input.AGENT_ARGS);
39076
- const channels = input.AGENT_CHANNELS ? input.AGENT_CHANNELS.split(",").map((c) => c.trim()).filter(Boolean) : ["general"];
39077
- const disableBypass = input.AGENT_DISABLE_DEFAULT_BYPASS === "1";
39078
- const bypassConfig = getBypassFlagConfig(input.AGENT_CLI);
39079
- let bypassApplied = false;
39080
- const args = [...extraArgs];
39081
- const bypassValues = bypassConfig ? [bypassConfig.flag, ...bypassConfig.aliases ?? []] : [];
39082
- const hasBypassAlready = bypassValues.some((value) => args.includes(value));
39083
- if (bypassConfig && !disableBypass && !hasBypassAlready) {
39084
- args.push(bypassConfig.flag);
39085
- bypassApplied = true;
39086
- }
39087
- return {
39088
- name: input.AGENT_NAME,
39089
- cli: input.AGENT_CLI,
39090
- args,
39091
- channels,
39092
- task: input.AGENT_TASK,
39093
- cwd: input.AGENT_CWD,
39094
- model: input.AGENT_MODEL,
39095
- bypassApplied
39096
- };
39097
- }
39098
- async function spawnFromEnv(options = {}) {
39099
- const env = options.env ?? process.env;
39100
- const parsed = parseSpawnEnv(env);
39101
- const policy = resolveSpawnPolicy(parsed);
39102
- console.log(`[spawn-from-env] Spawning agent: name=${policy.name} cli=${policy.cli} channels=${policy.channels.join(",")} bypass=${policy.bypassApplied}`);
39103
- if (policy.task) {
39104
- console.log(`[spawn-from-env] Task: ${policy.task.slice(0, 200)}${policy.task.length > 200 ? "..." : ""}`);
39105
- }
39106
- const relay = new AgentRelay({
39107
- binaryPath: options.binaryPath ?? parsed.BROKER_BINARY_PATH,
39108
- brokerName: options.brokerName ?? `broker-${policy.name}`,
39109
- channels: policy.channels,
39110
- cwd: policy.cwd ?? process.cwd(),
39111
- env
39112
- });
39113
- relay.onAgentSpawned = (agent) => {
39114
- console.log(`[spawn-from-env] Agent spawned: ${agent.name}`);
39115
- };
39116
- relay.onAgentReady = (agent) => {
39117
- console.log(`[spawn-from-env] Agent ready: ${agent.name}`);
39118
- };
39119
- relay.onAgentExited = (agent) => {
39120
- console.log(`[spawn-from-env] Agent exited: ${agent.name} code=${agent.exitCode ?? "none"} signal=${agent.exitSignal ?? "none"}`);
39121
- };
39122
- try {
39123
- const agent = await relay.spawnPty({
39124
- name: policy.name,
39125
- cli: policy.cli,
39126
- args: policy.args,
39127
- channels: policy.channels,
39128
- task: policy.task
39129
- });
39130
- const exitReason = await agent.waitForExit();
39131
- console.log(`[spawn-from-env] Exit reason: ${exitReason}`);
39132
- return { exitReason, exitCode: agent.exitCode };
39133
- } catch (err) {
39134
- console.error(`[spawn-from-env] Error:`, err);
39135
- throw err;
39136
- } finally {
39137
- await relay.shutdown();
39138
- }
39139
- }
39140
-
39141
40316
  // packages/utils/dist/name-generator.js
39142
40317
  var ADJECTIVES = [
39143
40318
  "Blue",