agent-relay 3.2.0 → 3.2.1

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 (48) 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 +1307 -204
  6. package/dist/src/cli/relaycast-mcp.d.ts +4 -0
  7. package/dist/src/cli/relaycast-mcp.d.ts.map +1 -1
  8. package/dist/src/cli/relaycast-mcp.js +4 -4
  9. package/dist/src/cli/relaycast-mcp.js.map +1 -1
  10. package/package.json +8 -8
  11. package/packages/acp-bridge/package.json +2 -2
  12. package/packages/config/package.json +1 -1
  13. package/packages/hooks/package.json +4 -4
  14. package/packages/memory/package.json +2 -2
  15. package/packages/openclaw/package.json +2 -2
  16. package/packages/policy/package.json +2 -2
  17. package/packages/sdk/dist/__tests__/completion-pipeline.test.d.ts +14 -0
  18. package/packages/sdk/dist/__tests__/completion-pipeline.test.d.ts.map +1 -0
  19. package/packages/sdk/dist/__tests__/completion-pipeline.test.js +1476 -0
  20. package/packages/sdk/dist/__tests__/completion-pipeline.test.js.map +1 -0
  21. package/packages/sdk/dist/__tests__/e2e-owner-review.test.js +2 -2
  22. package/packages/sdk/dist/__tests__/e2e-owner-review.test.js.map +1 -1
  23. package/packages/sdk/dist/workflows/runner.d.ts +53 -2
  24. package/packages/sdk/dist/workflows/runner.d.ts.map +1 -1
  25. package/packages/sdk/dist/workflows/runner.js +1269 -86
  26. package/packages/sdk/dist/workflows/runner.js.map +1 -1
  27. package/packages/sdk/dist/workflows/trajectory.d.ts +6 -2
  28. package/packages/sdk/dist/workflows/trajectory.d.ts.map +1 -1
  29. package/packages/sdk/dist/workflows/trajectory.js +37 -2
  30. package/packages/sdk/dist/workflows/trajectory.js.map +1 -1
  31. package/packages/sdk/dist/workflows/types.d.ts +88 -0
  32. package/packages/sdk/dist/workflows/types.d.ts.map +1 -1
  33. package/packages/sdk/dist/workflows/types.js.map +1 -1
  34. package/packages/sdk/package.json +2 -2
  35. package/packages/sdk/src/__tests__/completion-pipeline.test.ts +1820 -0
  36. package/packages/sdk/src/__tests__/e2e-owner-review.test.ts +2 -2
  37. package/packages/sdk/src/__tests__/idle-nudge.test.ts +68 -0
  38. package/packages/sdk/src/__tests__/workflow-runner.test.ts +113 -4
  39. package/packages/sdk/src/workflows/README.md +43 -11
  40. package/packages/sdk/src/workflows/runner.ts +1751 -94
  41. package/packages/sdk/src/workflows/schema.json +6 -0
  42. package/packages/sdk/src/workflows/trajectory.ts +52 -3
  43. package/packages/sdk/src/workflows/types.ts +149 -0
  44. package/packages/sdk-py/pyproject.toml +1 -1
  45. package/packages/telemetry/package.json +1 -1
  46. package/packages/trajectory/package.json +2 -2
  47. package/packages/user-directory/package.json +2 -2
  48. package/packages/utils/package.json +2 -2
package/dist/index.cjs CHANGED
@@ -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,6 +34427,12 @@ 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();
34264
34438
  constructor(options = {}) {
@@ -34339,6 +34513,423 @@ var WorkflowRunner = class _WorkflowRunner {
34339
34513
  }
34340
34514
  return resolved;
34341
34515
  }
34516
+ static EVIDENCE_IGNORED_DIRS = /* @__PURE__ */ new Set([
34517
+ ".git",
34518
+ ".agent-relay",
34519
+ ".trajectories",
34520
+ "node_modules"
34521
+ ]);
34522
+ getStepCompletionEvidence(stepName) {
34523
+ const record2 = this.stepCompletionEvidence.get(stepName);
34524
+ if (!record2)
34525
+ return void 0;
34526
+ const evidence = structuredClone(record2.evidence);
34527
+ return this.filterStepEvidenceBySignalProvenance(stepName, evidence);
34528
+ }
34529
+ getOrCreateStepEvidenceRecord(stepName) {
34530
+ const existing = this.stepCompletionEvidence.get(stepName);
34531
+ if (existing)
34532
+ return existing;
34533
+ const now = (/* @__PURE__ */ new Date()).toISOString();
34534
+ const record2 = {
34535
+ evidence: {
34536
+ stepName,
34537
+ lastUpdatedAt: now,
34538
+ roots: [],
34539
+ output: {
34540
+ stdout: "",
34541
+ stderr: "",
34542
+ combined: ""
34543
+ },
34544
+ channelPosts: [],
34545
+ files: [],
34546
+ process: {},
34547
+ toolSideEffects: [],
34548
+ coordinationSignals: []
34549
+ },
34550
+ baselineSnapshots: /* @__PURE__ */ new Map(),
34551
+ filesCaptured: false
34552
+ };
34553
+ this.stepCompletionEvidence.set(stepName, record2);
34554
+ return record2;
34555
+ }
34556
+ initializeStepSignalParticipants(stepName, ownerSender, workerSender) {
34557
+ this.stepSignalParticipants.set(stepName, {
34558
+ ownerSenders: /* @__PURE__ */ new Set(),
34559
+ workerSenders: /* @__PURE__ */ new Set()
34560
+ });
34561
+ this.rememberStepSignalSender(stepName, "owner", ownerSender);
34562
+ this.rememberStepSignalSender(stepName, "worker", workerSender);
34563
+ }
34564
+ rememberStepSignalSender(stepName, participant, ...senders) {
34565
+ const participants = this.stepSignalParticipants.get(stepName) ?? {
34566
+ ownerSenders: /* @__PURE__ */ new Set(),
34567
+ workerSenders: /* @__PURE__ */ new Set()
34568
+ };
34569
+ this.stepSignalParticipants.set(stepName, participants);
34570
+ const target = participant === "owner" ? participants.ownerSenders : participants.workerSenders;
34571
+ for (const sender of senders) {
34572
+ const trimmed = sender?.trim();
34573
+ if (trimmed)
34574
+ target.add(trimmed);
34575
+ }
34576
+ }
34577
+ resolveSignalParticipantKind(role) {
34578
+ const roleLC = role?.toLowerCase().trim();
34579
+ if (!roleLC)
34580
+ return void 0;
34581
+ if (/\b(owner|lead|supervisor)\b/.test(roleLC))
34582
+ return "owner";
34583
+ if (/\b(worker|specialist|engineer|implementer)\b/.test(roleLC))
34584
+ return "worker";
34585
+ return void 0;
34586
+ }
34587
+ isSignalFromExpectedSender(stepName, signal) {
34588
+ const expectedParticipant = signal.kind === "worker_done" ? "worker" : signal.kind === "lead_done" ? "owner" : void 0;
34589
+ if (!expectedParticipant)
34590
+ return true;
34591
+ const participants = this.stepSignalParticipants.get(stepName);
34592
+ if (!participants)
34593
+ return true;
34594
+ const allowedSenders = expectedParticipant === "owner" ? participants.ownerSenders : participants.workerSenders;
34595
+ if (allowedSenders.size === 0)
34596
+ return true;
34597
+ const sender = signal.sender ?? signal.actor;
34598
+ if (sender) {
34599
+ return allowedSenders.has(sender);
34600
+ }
34601
+ const observedParticipant = this.resolveSignalParticipantKind(signal.role);
34602
+ if (observedParticipant) {
34603
+ return observedParticipant === expectedParticipant;
34604
+ }
34605
+ return signal.source !== "channel";
34606
+ }
34607
+ filterStepEvidenceBySignalProvenance(stepName, evidence) {
34608
+ evidence.channelPosts = evidence.channelPosts.map((post) => {
34609
+ const signals = post.signals.filter((signal) => this.isSignalFromExpectedSender(stepName, signal));
34610
+ return {
34611
+ ...post,
34612
+ completionRelevant: signals.length > 0,
34613
+ signals
34614
+ };
34615
+ });
34616
+ evidence.coordinationSignals = evidence.coordinationSignals.filter((signal) => this.isSignalFromExpectedSender(stepName, signal));
34617
+ return evidence;
34618
+ }
34619
+ beginStepEvidence(stepName, roots, startedAt) {
34620
+ const record2 = this.getOrCreateStepEvidenceRecord(stepName);
34621
+ const evidence = record2.evidence;
34622
+ const now = startedAt ?? (/* @__PURE__ */ new Date()).toISOString();
34623
+ evidence.startedAt ??= now;
34624
+ evidence.status = "running";
34625
+ evidence.lastUpdatedAt = now;
34626
+ for (const root of this.uniqueEvidenceRoots(roots)) {
34627
+ if (!evidence.roots.includes(root)) {
34628
+ evidence.roots.push(root);
34629
+ }
34630
+ if (!record2.baselineSnapshots.has(root)) {
34631
+ record2.baselineSnapshots.set(root, this.captureFileSnapshot(root));
34632
+ }
34633
+ }
34634
+ }
34635
+ captureStepTerminalEvidence(stepName, output, process3, meta3) {
34636
+ const record2 = this.getOrCreateStepEvidenceRecord(stepName);
34637
+ const evidence = record2.evidence;
34638
+ const observedAt = (/* @__PURE__ */ new Date()).toISOString();
34639
+ const append = (current, next) => {
34640
+ if (!next)
34641
+ return current;
34642
+ return current ? `${current}
34643
+ ${next}` : next;
34644
+ };
34645
+ if (output.stdout) {
34646
+ evidence.output.stdout = append(evidence.output.stdout, output.stdout);
34647
+ for (const signal of this.extractCompletionSignals(output.stdout, "stdout", observedAt, meta3)) {
34648
+ evidence.coordinationSignals.push(signal);
34649
+ }
34650
+ }
34651
+ if (output.stderr) {
34652
+ evidence.output.stderr = append(evidence.output.stderr, output.stderr);
34653
+ for (const signal of this.extractCompletionSignals(output.stderr, "stderr", observedAt, meta3)) {
34654
+ evidence.coordinationSignals.push(signal);
34655
+ }
34656
+ }
34657
+ const combinedOutput = output.combined ?? [output.stdout, output.stderr].filter((value) => Boolean(value)).join("\n");
34658
+ if (combinedOutput) {
34659
+ evidence.output.combined = append(evidence.output.combined, combinedOutput);
34660
+ }
34661
+ if (process3) {
34662
+ if (process3.exitCode !== void 0) {
34663
+ evidence.process.exitCode = process3.exitCode;
34664
+ evidence.coordinationSignals.push({
34665
+ kind: "process_exit",
34666
+ source: "process",
34667
+ text: `Process exited with code ${process3.exitCode}`,
34668
+ observedAt,
34669
+ value: String(process3.exitCode)
34670
+ });
34671
+ }
34672
+ if (process3.exitSignal !== void 0) {
34673
+ evidence.process.exitSignal = process3.exitSignal;
34674
+ }
34675
+ }
34676
+ evidence.lastUpdatedAt = observedAt;
34677
+ }
34678
+ finalizeStepEvidence(stepName, status, completedAt, completionReason) {
34679
+ const record2 = this.stepCompletionEvidence.get(stepName);
34680
+ if (!record2)
34681
+ return;
34682
+ const evidence = record2.evidence;
34683
+ const observedAt = completedAt ?? (/* @__PURE__ */ new Date()).toISOString();
34684
+ evidence.status = status;
34685
+ if (status !== "running") {
34686
+ evidence.completedAt = observedAt;
34687
+ }
34688
+ evidence.lastUpdatedAt = observedAt;
34689
+ if (!record2.filesCaptured) {
34690
+ const existing = new Set(evidence.files.map((file2) => `${file2.kind}:${file2.path}`));
34691
+ for (const root of evidence.roots) {
34692
+ const before = record2.baselineSnapshots.get(root) ?? /* @__PURE__ */ new Map();
34693
+ const after = this.captureFileSnapshot(root);
34694
+ for (const change of this.diffFileSnapshots(before, after, root, observedAt)) {
34695
+ const key = `${change.kind}:${change.path}`;
34696
+ if (existing.has(key))
34697
+ continue;
34698
+ existing.add(key);
34699
+ evidence.files.push(change);
34700
+ }
34701
+ }
34702
+ record2.filesCaptured = true;
34703
+ }
34704
+ if (completionReason) {
34705
+ const decision = this.buildStepCompletionDecision(stepName, completionReason);
34706
+ if (decision) {
34707
+ void this.trajectory?.stepCompletionDecision(stepName, decision);
34708
+ }
34709
+ }
34710
+ }
34711
+ recordStepToolSideEffect(stepName, effect) {
34712
+ const record2 = this.getOrCreateStepEvidenceRecord(stepName);
34713
+ const observedAt = effect.observedAt ?? (/* @__PURE__ */ new Date()).toISOString();
34714
+ record2.evidence.toolSideEffects.push({
34715
+ ...effect,
34716
+ observedAt
34717
+ });
34718
+ record2.evidence.lastUpdatedAt = observedAt;
34719
+ }
34720
+ recordChannelEvidence(text, options = {}) {
34721
+ const stepName = options.stepName ?? this.inferStepNameFromChannelText(text) ?? (options.actor ? this.runtimeStepAgents.get(options.actor)?.stepName : void 0);
34722
+ if (!stepName)
34723
+ return;
34724
+ const record2 = this.getOrCreateStepEvidenceRecord(stepName);
34725
+ const postedAt = (/* @__PURE__ */ new Date()).toISOString();
34726
+ const sender = options.sender ?? options.actor;
34727
+ const signals = this.extractCompletionSignals(text, "channel", postedAt, {
34728
+ sender,
34729
+ actor: options.actor,
34730
+ role: options.role
34731
+ });
34732
+ const channelPost = {
34733
+ stepName,
34734
+ text,
34735
+ postedAt,
34736
+ origin: options.origin ?? "runner_post",
34737
+ completionRelevant: signals.length > 0,
34738
+ sender,
34739
+ actor: options.actor,
34740
+ role: options.role,
34741
+ target: options.target,
34742
+ signals
34743
+ };
34744
+ record2.evidence.channelPosts.push(channelPost);
34745
+ record2.evidence.coordinationSignals.push(...signals);
34746
+ record2.evidence.lastUpdatedAt = postedAt;
34747
+ }
34748
+ extractCompletionSignals(text, source, observedAt, meta3) {
34749
+ const signals = [];
34750
+ const seen = /* @__PURE__ */ new Set();
34751
+ const add = (kind, signalText, value) => {
34752
+ const trimmed = signalText.trim().slice(0, 280);
34753
+ if (!trimmed)
34754
+ return;
34755
+ const key = `${kind}:${trimmed}:${value ?? ""}`;
34756
+ if (seen.has(key))
34757
+ return;
34758
+ seen.add(key);
34759
+ signals.push({
34760
+ kind,
34761
+ source,
34762
+ text: trimmed,
34763
+ observedAt,
34764
+ sender: meta3?.sender,
34765
+ actor: meta3?.actor,
34766
+ role: meta3?.role,
34767
+ value
34768
+ });
34769
+ };
34770
+ for (const match of text.matchAll(/\bWORKER_DONE\b(?::\s*([^\n]+))?/gi)) {
34771
+ add("worker_done", match[0], match[1]?.trim());
34772
+ }
34773
+ for (const match of text.matchAll(/\bLEAD_DONE\b(?::\s*([^\n]+))?/gi)) {
34774
+ add("lead_done", match[0], match[1]?.trim());
34775
+ }
34776
+ for (const match of text.matchAll(/\bSTEP_COMPLETE:([A-Za-z0-9_.:-]+)/g)) {
34777
+ add("step_complete", match[0], match[1]);
34778
+ }
34779
+ for (const match of text.matchAll(/\bOWNER_DECISION:\s*(COMPLETE|INCOMPLETE_RETRY|INCOMPLETE_FAIL|NEEDS_CLARIFICATION)\b/gi)) {
34780
+ add("owner_decision", match[0], match[1].toUpperCase());
34781
+ }
34782
+ for (const match of text.matchAll(/\bREVIEW_DECISION:\s*(APPROVE|REJECT)\b/gi)) {
34783
+ add("review_decision", match[0], match[1].toUpperCase());
34784
+ }
34785
+ if (/\bverification gate observed\b|\bverification passed\b/i.test(text)) {
34786
+ add("verification_passed", this.firstMeaningfulLine(text) ?? text);
34787
+ }
34788
+ if (/\bverification failed\b/i.test(text)) {
34789
+ add("verification_failed", this.firstMeaningfulLine(text) ?? text);
34790
+ }
34791
+ if (/\b(summary|handoff|ready for review|ready for handoff|task complete|work complete|completed work|finished work)\b/i.test(text)) {
34792
+ add("task_summary", this.firstMeaningfulLine(text) ?? text);
34793
+ }
34794
+ return signals;
34795
+ }
34796
+ inferStepNameFromChannelText(text) {
34797
+ const bracketMatch = text.match(/^\*\*\[([^\]]+)\]/);
34798
+ if (bracketMatch?.[1])
34799
+ return bracketMatch[1];
34800
+ const markerMatch = text.match(/\bSTEP_COMPLETE:([A-Za-z0-9_.:-]+)/);
34801
+ if (markerMatch?.[1])
34802
+ return markerMatch[1];
34803
+ return void 0;
34804
+ }
34805
+ uniqueEvidenceRoots(roots) {
34806
+ return [...new Set(roots.filter((root) => Boolean(root)).map((root) => import_node_path8.default.resolve(root)))];
34807
+ }
34808
+ captureFileSnapshot(root) {
34809
+ const snapshot = /* @__PURE__ */ new Map();
34810
+ if (!(0, import_node_fs4.existsSync)(root))
34811
+ return snapshot;
34812
+ const visit = (currentPath) => {
34813
+ let entries;
34814
+ try {
34815
+ entries = (0, import_node_fs4.readdirSync)(currentPath, { withFileTypes: true });
34816
+ } catch {
34817
+ return;
34818
+ }
34819
+ for (const entry of entries) {
34820
+ if (entry.isDirectory() && _WorkflowRunner.EVIDENCE_IGNORED_DIRS.has(entry.name)) {
34821
+ continue;
34822
+ }
34823
+ const fullPath = import_node_path8.default.join(currentPath, entry.name);
34824
+ if (entry.isDirectory()) {
34825
+ visit(fullPath);
34826
+ continue;
34827
+ }
34828
+ try {
34829
+ const stats = (0, import_node_fs4.statSync)(fullPath);
34830
+ if (!stats.isFile())
34831
+ continue;
34832
+ snapshot.set(fullPath, { mtimeMs: stats.mtimeMs, size: stats.size });
34833
+ } catch {
34834
+ }
34835
+ }
34836
+ };
34837
+ try {
34838
+ const stats = (0, import_node_fs4.statSync)(root);
34839
+ if (stats.isFile()) {
34840
+ snapshot.set(root, { mtimeMs: stats.mtimeMs, size: stats.size });
34841
+ return snapshot;
34842
+ }
34843
+ } catch {
34844
+ return snapshot;
34845
+ }
34846
+ visit(root);
34847
+ return snapshot;
34848
+ }
34849
+ diffFileSnapshots(before, after, root, observedAt) {
34850
+ const allPaths = /* @__PURE__ */ new Set([...before.keys(), ...after.keys()]);
34851
+ const changes = [];
34852
+ for (const filePath of allPaths) {
34853
+ const prior = before.get(filePath);
34854
+ const next = after.get(filePath);
34855
+ let kind;
34856
+ if (!prior && next) {
34857
+ kind = "created";
34858
+ } else if (prior && !next) {
34859
+ kind = "deleted";
34860
+ } else if (prior && next && (prior.mtimeMs !== next.mtimeMs || prior.size !== next.size)) {
34861
+ kind = "modified";
34862
+ }
34863
+ if (!kind)
34864
+ continue;
34865
+ changes.push({
34866
+ path: this.normalizeEvidencePath(filePath),
34867
+ kind,
34868
+ observedAt,
34869
+ root
34870
+ });
34871
+ }
34872
+ return changes.sort((a, b) => a.path.localeCompare(b.path));
34873
+ }
34874
+ normalizeEvidencePath(filePath) {
34875
+ const relative = import_node_path8.default.relative(this.cwd, filePath);
34876
+ if (!relative || relative === "")
34877
+ return import_node_path8.default.basename(filePath);
34878
+ return relative.startsWith("..") ? filePath : relative;
34879
+ }
34880
+ buildStepCompletionDecision(stepName, completionReason) {
34881
+ let reason;
34882
+ let mode;
34883
+ switch (completionReason) {
34884
+ case "completed_verified":
34885
+ mode = "verification";
34886
+ reason = "Verification passed";
34887
+ break;
34888
+ case "completed_by_evidence":
34889
+ mode = "evidence";
34890
+ reason = "Completion inferred from collected evidence";
34891
+ break;
34892
+ case "completed_by_owner_decision": {
34893
+ const evidence = this.getStepCompletionEvidence(stepName);
34894
+ const markerObserved = evidence?.coordinationSignals.some((signal) => signal.kind === "step_complete");
34895
+ mode = markerObserved ? "marker" : "owner_decision";
34896
+ reason = markerObserved ? "Legacy STEP_COMPLETE marker observed" : "Owner approved completion";
34897
+ break;
34898
+ }
34899
+ default:
34900
+ return void 0;
34901
+ }
34902
+ return {
34903
+ mode,
34904
+ reason,
34905
+ evidence: this.buildTrajectoryCompletionEvidence(stepName)
34906
+ };
34907
+ }
34908
+ buildTrajectoryCompletionEvidence(stepName) {
34909
+ const evidence = this.getStepCompletionEvidence(stepName);
34910
+ if (!evidence)
34911
+ return void 0;
34912
+ const signals = evidence.coordinationSignals.slice(-6).map((signal) => signal.value ?? signal.text);
34913
+ const channelPosts = evidence.channelPosts.filter((post) => post.completionRelevant).slice(-3).map((post) => post.text.slice(0, 160));
34914
+ const files = evidence.files.slice(0, 6).map((file2) => `${file2.kind}:${file2.path}`);
34915
+ const summaryParts = [];
34916
+ if (signals.length > 0)
34917
+ summaryParts.push(`${signals.length} signal(s)`);
34918
+ if (channelPosts.length > 0)
34919
+ summaryParts.push(`${channelPosts.length} relevant channel post(s)`);
34920
+ if (files.length > 0)
34921
+ summaryParts.push(`${files.length} file change(s)`);
34922
+ if (evidence.process.exitCode !== void 0) {
34923
+ summaryParts.push(`exit=${evidence.process.exitCode}`);
34924
+ }
34925
+ return {
34926
+ summary: summaryParts.length > 0 ? summaryParts.join(", ") : void 0,
34927
+ signals: signals.length > 0 ? signals : void 0,
34928
+ channelPosts: channelPosts.length > 0 ? channelPosts : void 0,
34929
+ files: files.length > 0 ? files : void 0,
34930
+ exitCode: evidence.process.exitCode
34931
+ };
34932
+ }
34342
34933
  // ── Progress logging ────────────────────────────────────────────────────
34343
34934
  /** Log a progress message with elapsed time since run start. */
34344
34935
  log(msg) {
@@ -35064,9 +35655,11 @@ ${err.suggestion}`);
35064
35655
  if (state.row.status === "failed") {
35065
35656
  state.row.status = "pending";
35066
35657
  state.row.error = void 0;
35658
+ state.row.completionReason = void 0;
35067
35659
  await this.db.updateStep(state.row.id, {
35068
35660
  status: "pending",
35069
35661
  error: void 0,
35662
+ completionReason: void 0,
35070
35663
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
35071
35664
  });
35072
35665
  }
@@ -35085,6 +35678,8 @@ ${err.suggestion}`);
35085
35678
  this.currentConfig = config2;
35086
35679
  this.currentRunId = runId;
35087
35680
  this.runStartTime = Date.now();
35681
+ this.runtimeStepAgents.clear();
35682
+ this.stepCompletionEvidence.clear();
35088
35683
  this.log(`Starting workflow "${workflow2.name}" (${workflow2.steps.length} steps)`);
35089
35684
  this.trajectory = new WorkflowTrajectory(config2.trajectories, runId, this.cwd);
35090
35685
  try {
@@ -35188,8 +35783,24 @@ ${err.suggestion}`);
35188
35783
  const fromShort = msg.from.replace(/-[a-f0-9]{6,}$/, "");
35189
35784
  const toShort = msg.to.replace(/-[a-f0-9]{6,}$/, "");
35190
35785
  this.log(`[msg] ${fromShort} \u2192 ${toShort}: ${body}`);
35786
+ if (this.channel && (msg.to === this.channel || msg.to === `#${this.channel}`)) {
35787
+ const runtimeAgent = this.runtimeStepAgents.get(msg.from);
35788
+ this.recordChannelEvidence(msg.text, {
35789
+ sender: runtimeAgent?.logicalName ?? msg.from,
35790
+ actor: msg.from,
35791
+ role: runtimeAgent?.role,
35792
+ target: msg.to,
35793
+ origin: "relay_message",
35794
+ stepName: runtimeAgent?.stepName
35795
+ });
35796
+ }
35191
35797
  const supervision = this.supervisedRuntimeAgents.get(msg.from);
35192
35798
  if (supervision?.role === "owner") {
35799
+ this.recordStepToolSideEffect(supervision.stepName, {
35800
+ type: "owner_monitoring",
35801
+ detail: `Owner messaged ${msg.to}: ${msg.text.slice(0, 120)}`,
35802
+ raw: { to: msg.to, text: msg.text }
35803
+ });
35193
35804
  void this.trajectory?.ownerMonitoringEvent(supervision.stepName, supervision.logicalName, `Messaged ${msg.to}: ${msg.text.slice(0, 120)}`, { to: msg.to, text: msg.text });
35194
35805
  }
35195
35806
  };
@@ -35333,6 +35944,7 @@ ${err.suggestion}`);
35333
35944
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
35334
35945
  });
35335
35946
  this.emit({ type: "step:failed", runId, stepName, error: "Cancelled" });
35947
+ this.finalizeStepEvidence(stepName, "failed");
35336
35948
  }
35337
35949
  }
35338
35950
  this.emit({ type: "run:cancelled", runId });
@@ -35370,6 +35982,7 @@ ${err.suggestion}`);
35370
35982
  this.lastIdleLog.clear();
35371
35983
  this.lastActivity.clear();
35372
35984
  this.supervisedRuntimeAgents.clear();
35985
+ this.runtimeStepAgents.clear();
35373
35986
  this.log("Shutting down broker...");
35374
35987
  await this.relay?.shutdown();
35375
35988
  this.relay = void 0;
@@ -35457,7 +36070,8 @@ ${err.suggestion}`);
35457
36070
  status: state?.row.status === "completed" ? "completed" : "failed",
35458
36071
  attempts: (state?.row.retryCount ?? 0) + 1,
35459
36072
  output: state?.row.output,
35460
- verificationPassed: state?.row.status === "completed" && step.verification !== void 0
36073
+ verificationPassed: state?.row.status === "completed" && step.verification !== void 0,
36074
+ completionMode: state?.row.completionReason ? this.buildStepCompletionDecision(step.name, state.row.completionReason)?.mode : void 0
35461
36075
  });
35462
36076
  }
35463
36077
  }
@@ -35604,11 +36218,21 @@ ${trimmedOutput.slice(0, 200)}`);
35604
36218
  const maxRetries = step.retries ?? errorHandling?.maxRetries ?? 0;
35605
36219
  const retryDelay = errorHandling?.retryDelayMs ?? 1e3;
35606
36220
  let lastError;
36221
+ let lastCompletionReason;
36222
+ let lastExitCode;
36223
+ let lastExitSignal;
35607
36224
  for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
35608
36225
  this.checkAborted();
36226
+ lastExitCode = void 0;
36227
+ lastExitSignal = void 0;
35609
36228
  if (attempt > 0) {
35610
36229
  this.emit({ type: "step:retrying", runId, stepName: step.name, attempt });
35611
36230
  this.postToChannel(`**[${step.name}]** Retrying (attempt ${attempt + 1}/${maxRetries + 1})`);
36231
+ this.recordStepToolSideEffect(step.name, {
36232
+ type: "retry",
36233
+ detail: `Retrying attempt ${attempt + 1}/${maxRetries + 1}`,
36234
+ raw: { attempt, maxRetries }
36235
+ });
35612
36236
  state.row.retryCount = attempt;
35613
36237
  await this.db.updateStep(state.row.id, {
35614
36238
  retryCount: attempt,
@@ -35617,9 +36241,13 @@ ${trimmedOutput.slice(0, 200)}`);
35617
36241
  await this.delay(retryDelay);
35618
36242
  }
35619
36243
  state.row.status = "running";
36244
+ state.row.error = void 0;
36245
+ state.row.completionReason = void 0;
35620
36246
  state.row.startedAt = (/* @__PURE__ */ new Date()).toISOString();
35621
36247
  await this.db.updateStep(state.row.id, {
35622
36248
  status: "running",
36249
+ error: void 0,
36250
+ completionReason: void 0,
35623
36251
  startedAt: state.row.startedAt,
35624
36252
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
35625
36253
  });
@@ -35634,30 +36262,36 @@ ${trimmedOutput.slice(0, 200)}`);
35634
36262
  return value !== void 0 ? String(value) : _match;
35635
36263
  });
35636
36264
  const stepCwd = this.resolveStepWorkdir(step) ?? this.cwd;
36265
+ this.beginStepEvidence(step.name, [stepCwd], state.row.startedAt);
35637
36266
  try {
35638
36267
  if (this.executor?.executeDeterministicStep) {
35639
36268
  const result = await this.executor.executeDeterministicStep(step, resolvedCommand, stepCwd);
36269
+ lastExitCode = result.exitCode;
35640
36270
  const failOnError = step.failOnError !== false;
35641
36271
  if (failOnError && result.exitCode !== 0) {
35642
36272
  throw new Error(`Command failed with exit code ${result.exitCode}: ${result.output.slice(0, 500)}`);
35643
36273
  }
35644
36274
  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
- }
36275
+ this.captureStepTerminalEvidence(step.name, { stdout: result.output, combined: result.output }, { exitCode: result.exitCode });
36276
+ const verificationResult2 = step.verification ? this.runVerification(step.verification, output2, step.name) : void 0;
35648
36277
  state.row.status = "completed";
35649
36278
  state.row.output = output2;
36279
+ state.row.completionReason = verificationResult2?.completionReason;
35650
36280
  state.row.completedAt = (/* @__PURE__ */ new Date()).toISOString();
35651
36281
  await this.db.updateStep(state.row.id, {
35652
36282
  status: "completed",
35653
36283
  output: output2,
36284
+ completionReason: verificationResult2?.completionReason,
35654
36285
  completedAt: state.row.completedAt,
35655
36286
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
35656
36287
  });
35657
36288
  await this.persistStepOutput(runId, step.name, output2);
35658
36289
  this.emit({ type: "step:completed", runId, stepName: step.name, output: output2 });
36290
+ this.finalizeStepEvidence(step.name, "completed", state.row.completedAt, verificationResult2?.completionReason);
35659
36291
  return;
35660
36292
  }
36293
+ let commandStdout = "";
36294
+ let commandStderr = "";
35661
36295
  const output = await new Promise((resolve3, reject) => {
35662
36296
  const child = (0, import_node_child_process3.spawn)("sh", ["-c", resolvedCommand], {
35663
36297
  stdio: "pipe",
@@ -35690,7 +36324,7 @@ ${trimmedOutput.slice(0, 200)}`);
35690
36324
  child.stderr?.on("data", (chunk) => {
35691
36325
  stderrChunks.push(chunk.toString());
35692
36326
  });
35693
- child.on("close", (code) => {
36327
+ child.on("close", (code, signal) => {
35694
36328
  if (timer)
35695
36329
  clearTimeout(timer);
35696
36330
  if (abortHandler && abortSignal) {
@@ -35706,6 +36340,10 @@ ${trimmedOutput.slice(0, 200)}`);
35706
36340
  }
35707
36341
  const stdout = stdoutChunks.join("");
35708
36342
  const stderr = stderrChunks.join("");
36343
+ commandStdout = stdout;
36344
+ commandStderr = stderr;
36345
+ lastExitCode = code ?? void 0;
36346
+ lastExitSignal = signal ?? void 0;
35709
36347
  const failOnError = step.failOnError !== false;
35710
36348
  if (failOnError && code !== 0 && code !== null) {
35711
36349
  reject(new Error(`Command failed with exit code ${code}${stderr ? `: ${stderr.slice(0, 500)}` : ""}`));
@@ -35722,28 +36360,35 @@ ${trimmedOutput.slice(0, 200)}`);
35722
36360
  reject(new Error(`Failed to execute command: ${err.message}`));
35723
36361
  });
35724
36362
  });
35725
- if (step.verification) {
35726
- this.runVerification(step.verification, output, step.name);
35727
- }
36363
+ this.captureStepTerminalEvidence(step.name, {
36364
+ stdout: commandStdout || output,
36365
+ stderr: commandStderr,
36366
+ combined: [commandStdout || output, commandStderr].filter(Boolean).join("\n")
36367
+ }, { exitCode: lastExitCode, exitSignal: lastExitSignal });
36368
+ const verificationResult = step.verification ? this.runVerification(step.verification, output, step.name) : void 0;
35728
36369
  state.row.status = "completed";
35729
36370
  state.row.output = output;
36371
+ state.row.completionReason = verificationResult?.completionReason;
35730
36372
  state.row.completedAt = (/* @__PURE__ */ new Date()).toISOString();
35731
36373
  await this.db.updateStep(state.row.id, {
35732
36374
  status: "completed",
35733
36375
  output,
36376
+ completionReason: verificationResult?.completionReason,
35734
36377
  completedAt: state.row.completedAt,
35735
36378
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
35736
36379
  });
35737
36380
  await this.persistStepOutput(runId, step.name, output);
35738
36381
  this.emit({ type: "step:completed", runId, stepName: step.name, output });
36382
+ this.finalizeStepEvidence(step.name, "completed", state.row.completedAt, verificationResult?.completionReason);
35739
36383
  return;
35740
36384
  } catch (err) {
35741
36385
  lastError = err instanceof Error ? err.message : String(err);
36386
+ lastCompletionReason = err instanceof WorkflowCompletionError ? err.completionReason : void 0;
35742
36387
  }
35743
36388
  }
35744
36389
  const errorMsg = lastError ?? "Unknown error";
35745
36390
  this.postToChannel(`**[${step.name}]** Failed: ${errorMsg}`);
35746
- await this.markStepFailed(state, errorMsg, runId);
36391
+ await this.markStepFailed(state, errorMsg, runId, { exitCode: lastExitCode, exitSignal: lastExitSignal }, lastCompletionReason);
35747
36392
  throw new Error(`Step "${step.name}" failed: ${errorMsg}`);
35748
36393
  }
35749
36394
  /**
@@ -35755,11 +36400,17 @@ ${trimmedOutput.slice(0, 200)}`);
35755
36400
  const state = stepStates.get(step.name);
35756
36401
  if (!state)
35757
36402
  throw new Error(`Step state not found: ${step.name}`);
36403
+ let lastExitCode;
36404
+ let lastExitSignal;
35758
36405
  this.checkAborted();
35759
36406
  state.row.status = "running";
36407
+ state.row.error = void 0;
36408
+ state.row.completionReason = void 0;
35760
36409
  state.row.startedAt = (/* @__PURE__ */ new Date()).toISOString();
35761
36410
  await this.db.updateStep(state.row.id, {
35762
36411
  status: "running",
36412
+ error: void 0,
36413
+ completionReason: void 0,
35763
36414
  startedAt: state.row.startedAt,
35764
36415
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
35765
36416
  });
@@ -35771,6 +36422,7 @@ ${trimmedOutput.slice(0, 200)}`);
35771
36422
  const worktreePath = step.path ? this.interpolateStepTask(step.path, stepOutputContext) : import_node_path8.default.join(".worktrees", step.name);
35772
36423
  const createBranch = step.createBranch !== false;
35773
36424
  const stepCwd = this.resolveStepWorkdir(step) ?? this.cwd;
36425
+ this.beginStepEvidence(step.name, [stepCwd], state.row.startedAt);
35774
36426
  if (!branch) {
35775
36427
  const errorMsg = 'Worktree step missing required "branch" field';
35776
36428
  await this.markStepFailed(state, errorMsg, runId);
@@ -35802,6 +36454,10 @@ ${trimmedOutput.slice(0, 200)}`);
35802
36454
  await this.markStepFailed(state, errorMsg, runId);
35803
36455
  throw new Error(`Step "${step.name}" failed: ${errorMsg}`);
35804
36456
  }
36457
+ let commandStdout = "";
36458
+ let commandStderr = "";
36459
+ let commandExitCode;
36460
+ let commandExitSignal;
35805
36461
  const output = await new Promise((resolve3, reject) => {
35806
36462
  const child = (0, import_node_child_process3.spawn)("sh", ["-c", worktreeCmd], {
35807
36463
  stdio: "pipe",
@@ -35834,7 +36490,7 @@ ${trimmedOutput.slice(0, 200)}`);
35834
36490
  child.stderr?.on("data", (chunk) => {
35835
36491
  stderrChunks.push(chunk.toString());
35836
36492
  });
35837
- child.on("close", (code) => {
36493
+ child.on("close", (code, signal) => {
35838
36494
  if (timer)
35839
36495
  clearTimeout(timer);
35840
36496
  if (abortHandler && abortSignal) {
@@ -35848,7 +36504,13 @@ ${trimmedOutput.slice(0, 200)}`);
35848
36504
  reject(new Error(`Step "${step.name}" timed out (no step timeout set, check global swarm.timeoutMs)`));
35849
36505
  return;
35850
36506
  }
36507
+ commandStdout = stdoutChunks.join("");
35851
36508
  const stderr = stderrChunks.join("");
36509
+ commandStderr = stderr;
36510
+ commandExitCode = code ?? void 0;
36511
+ commandExitSignal = signal ?? void 0;
36512
+ lastExitCode = commandExitCode;
36513
+ lastExitSignal = commandExitSignal;
35852
36514
  if (code !== 0 && code !== null) {
35853
36515
  reject(new Error(`git worktree add failed with exit code ${code}${stderr ? `: ${stderr.slice(0, 500)}` : ""}`));
35854
36516
  return;
@@ -35864,6 +36526,11 @@ ${trimmedOutput.slice(0, 200)}`);
35864
36526
  reject(new Error(`Failed to execute git worktree command: ${err.message}`));
35865
36527
  });
35866
36528
  });
36529
+ this.captureStepTerminalEvidence(step.name, {
36530
+ stdout: commandStdout || output,
36531
+ stderr: commandStderr,
36532
+ combined: [commandStdout || output, commandStderr].filter(Boolean).join("\n")
36533
+ }, { exitCode: commandExitCode, exitSignal: commandExitSignal });
35867
36534
  state.row.status = "completed";
35868
36535
  state.row.output = output;
35869
36536
  state.row.completedAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -35877,10 +36544,19 @@ ${trimmedOutput.slice(0, 200)}`);
35877
36544
  this.emit({ type: "step:completed", runId, stepName: step.name, output });
35878
36545
  this.postToChannel(`**[${step.name}]** Worktree created at: ${output}
35879
36546
  Branch: ${branch}${!branchExists && createBranch ? " (created)" : ""}`);
36547
+ this.recordStepToolSideEffect(step.name, {
36548
+ type: "worktree_created",
36549
+ detail: `Worktree created at ${output}`,
36550
+ raw: { branch, createdBranch: !branchExists && createBranch }
36551
+ });
36552
+ this.finalizeStepEvidence(step.name, "completed", state.row.completedAt);
35880
36553
  } catch (err) {
35881
36554
  const errorMsg = err instanceof Error ? err.message : String(err);
35882
36555
  this.postToChannel(`**[${step.name}]** Failed: ${errorMsg}`);
35883
- await this.markStepFailed(state, errorMsg, runId);
36556
+ await this.markStepFailed(state, errorMsg, runId, {
36557
+ exitCode: lastExitCode,
36558
+ exitSignal: lastExitSignal
36559
+ });
35884
36560
  throw new Error(`Step "${step.name}" failed: ${errorMsg}`);
35885
36561
  }
35886
36562
  }
@@ -35901,8 +36577,9 @@ ${trimmedOutput.slice(0, 200)}`);
35901
36577
  }
35902
36578
  const specialistDef = _WorkflowRunner.resolveAgentDef(rawAgentDef);
35903
36579
  const usesOwnerFlow = specialistDef.interactive !== false;
35904
- const ownerDef = usesOwnerFlow ? this.resolveAutoStepOwner(specialistDef, agentMap) : specialistDef;
35905
- const reviewDef = usesOwnerFlow ? this.resolveAutoReviewAgent(ownerDef, agentMap) : void 0;
36580
+ const usesAutoHardening = usesOwnerFlow && !this.isExplicitInteractiveWorker(specialistDef);
36581
+ const ownerDef = usesAutoHardening ? this.resolveAutoStepOwner(specialistDef, agentMap) : specialistDef;
36582
+ const reviewDef = usesAutoHardening ? this.resolveAutoReviewAgent(ownerDef, agentMap) : void 0;
35906
36583
  const supervised = {
35907
36584
  specialist: specialistDef,
35908
36585
  owner: ownerDef,
@@ -35915,6 +36592,7 @@ ${trimmedOutput.slice(0, 200)}`);
35915
36592
  let lastError;
35916
36593
  let lastExitCode;
35917
36594
  let lastExitSignal;
36595
+ let lastCompletionReason;
35918
36596
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
35919
36597
  this.checkAborted();
35920
36598
  lastExitCode = void 0;
@@ -35922,6 +36600,11 @@ ${trimmedOutput.slice(0, 200)}`);
35922
36600
  if (attempt > 0) {
35923
36601
  this.emit({ type: "step:retrying", runId, stepName: step.name, attempt });
35924
36602
  this.postToChannel(`**[${step.name}]** Retrying (attempt ${attempt + 1}/${maxRetries + 1})`);
36603
+ this.recordStepToolSideEffect(step.name, {
36604
+ type: "retry",
36605
+ detail: `Retrying attempt ${attempt + 1}/${maxRetries + 1}`,
36606
+ raw: { attempt, maxRetries }
36607
+ });
35925
36608
  state.row.retryCount = attempt;
35926
36609
  await this.db.updateStep(state.row.id, {
35927
36610
  retryCount: attempt,
@@ -35932,14 +36615,19 @@ ${trimmedOutput.slice(0, 200)}`);
35932
36615
  }
35933
36616
  try {
35934
36617
  state.row.status = "running";
36618
+ state.row.error = void 0;
36619
+ state.row.completionReason = void 0;
35935
36620
  state.row.startedAt = (/* @__PURE__ */ new Date()).toISOString();
35936
36621
  await this.db.updateStep(state.row.id, {
35937
36622
  status: "running",
36623
+ error: void 0,
36624
+ completionReason: void 0,
35938
36625
  startedAt: state.row.startedAt,
35939
36626
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
35940
36627
  });
35941
36628
  this.emit({ type: "step:started", runId, stepName: step.name });
35942
- this.postToChannel(`**[${step.name}]** Started (owner: ${ownerDef.name}, specialist: ${specialistDef.name})`);
36629
+ this.log(`[${step.name}] Started (owner: ${ownerDef.name}, specialist: ${specialistDef.name})`);
36630
+ this.initializeStepSignalParticipants(step.name, ownerDef.name, specialistDef.name);
35943
36631
  await this.trajectory?.stepStarted(step, ownerDef.name, {
35944
36632
  role: usesDedicatedOwner ? "owner" : "specialist",
35945
36633
  owner: ownerDef.name,
@@ -35984,33 +36672,83 @@ ${resolvedTask}`;
35984
36672
  };
35985
36673
  const effectiveSpecialist = applyStepWorkdir(specialistDef);
35986
36674
  const effectiveOwner = applyStepWorkdir(ownerDef);
36675
+ const effectiveReviewer = reviewDef ? applyStepWorkdir(reviewDef) : void 0;
36676
+ this.beginStepEvidence(step.name, [
36677
+ this.resolveAgentCwd(effectiveSpecialist),
36678
+ this.resolveAgentCwd(effectiveOwner),
36679
+ effectiveReviewer ? this.resolveAgentCwd(effectiveReviewer) : void 0
36680
+ ], state.row.startedAt);
35987
36681
  let specialistOutput;
35988
36682
  let ownerOutput;
35989
36683
  let ownerElapsed;
36684
+ let completionReason;
35990
36685
  if (usesDedicatedOwner) {
35991
36686
  const result = await this.executeSupervisedAgentStep(step, { specialist: effectiveSpecialist, owner: effectiveOwner, reviewer: reviewDef }, resolvedTask, timeoutMs);
35992
36687
  specialistOutput = result.specialistOutput;
35993
36688
  ownerOutput = result.ownerOutput;
35994
36689
  ownerElapsed = result.ownerElapsed;
36690
+ completionReason = result.completionReason;
35995
36691
  } else {
35996
36692
  const ownerTask = this.injectStepOwnerContract(step, resolvedTask, effectiveOwner, effectiveSpecialist);
36693
+ const explicitInteractiveWorker = this.isExplicitInteractiveWorker(effectiveOwner);
36694
+ let explicitWorkerHandle;
36695
+ let explicitWorkerCompleted = false;
36696
+ let explicitWorkerOutput = "";
35997
36697
  this.log(`[${step.name}] Spawning owner "${effectiveOwner.name}" (cli: ${effectiveOwner.cli})${step.workdir ? ` [workdir: ${step.workdir}]` : ""}`);
35998
36698
  const resolvedStep = { ...step, task: ownerTask };
35999
36699
  const ownerStartTime = Date.now();
36000
- const spawnResult = this.executor ? await this.executor.executeAgentStep(resolvedStep, effectiveOwner, ownerTask, timeoutMs) : await this.spawnAndWait(effectiveOwner, resolvedStep, timeoutMs);
36700
+ const spawnResult = this.executor ? await this.executor.executeAgentStep(resolvedStep, effectiveOwner, ownerTask, timeoutMs) : await this.spawnAndWait(effectiveOwner, resolvedStep, timeoutMs, {
36701
+ evidenceStepName: step.name,
36702
+ evidenceRole: usesOwnerFlow ? "owner" : "specialist",
36703
+ logicalName: effectiveOwner.name,
36704
+ onSpawned: explicitInteractiveWorker ? ({ agent }) => {
36705
+ explicitWorkerHandle = agent;
36706
+ } : void 0,
36707
+ onChunk: explicitInteractiveWorker ? ({ chunk }) => {
36708
+ explicitWorkerOutput += _WorkflowRunner.stripAnsi(chunk);
36709
+ if (!explicitWorkerCompleted && this.hasExplicitInteractiveWorkerCompletionEvidence(step, explicitWorkerOutput, ownerTask, resolvedTask)) {
36710
+ explicitWorkerCompleted = true;
36711
+ void explicitWorkerHandle?.release().catch(() => void 0);
36712
+ }
36713
+ } : void 0
36714
+ });
36001
36715
  const output = typeof spawnResult === "string" ? spawnResult : spawnResult.output;
36002
36716
  lastExitCode = typeof spawnResult === "string" ? void 0 : spawnResult.exitCode;
36003
36717
  lastExitSignal = typeof spawnResult === "string" ? void 0 : spawnResult.exitSignal;
36004
36718
  ownerElapsed = Date.now() - ownerStartTime;
36005
36719
  this.log(`[${step.name}] Owner "${effectiveOwner.name}" exited`);
36006
36720
  if (usesOwnerFlow) {
36007
- this.assertOwnerCompletionMarker(step, output, ownerTask);
36721
+ try {
36722
+ const completionDecision = this.resolveOwnerCompletionDecision(step, output, output, ownerTask, resolvedTask);
36723
+ completionReason = completionDecision.completionReason;
36724
+ } catch (error48) {
36725
+ const canUseVerificationFallback = !usesDedicatedOwner && step.verification && error48 instanceof WorkflowCompletionError && error48.completionReason === "failed_no_evidence";
36726
+ if (!canUseVerificationFallback) {
36727
+ throw error48;
36728
+ }
36729
+ }
36008
36730
  }
36009
36731
  specialistOutput = output;
36010
36732
  ownerOutput = output;
36011
36733
  }
36012
- if (step.verification) {
36013
- this.runVerification(step.verification, specialistOutput, step.name, effectiveOwner.interactive === false ? void 0 : resolvedTask);
36734
+ if (!usesOwnerFlow) {
36735
+ const explicitOwnerDecision = this.parseOwnerDecision(step, ownerOutput, false);
36736
+ if (explicitOwnerDecision?.decision === "INCOMPLETE_RETRY") {
36737
+ throw new WorkflowCompletionError(`Step "${step.name}" owner requested retry${explicitOwnerDecision.reason ? `: ${explicitOwnerDecision.reason}` : ""}`, "retry_requested_by_owner");
36738
+ }
36739
+ if (explicitOwnerDecision?.decision === "INCOMPLETE_FAIL") {
36740
+ throw new WorkflowCompletionError(`Step "${step.name}" owner marked the step incomplete${explicitOwnerDecision.reason ? `: ${explicitOwnerDecision.reason}` : ""}`, "failed_owner_decision");
36741
+ }
36742
+ if (explicitOwnerDecision?.decision === "NEEDS_CLARIFICATION") {
36743
+ throw new WorkflowCompletionError(`Step "${step.name}" owner requested clarification before completion${explicitOwnerDecision.reason ? `: ${explicitOwnerDecision.reason}` : ""}`, "retry_requested_by_owner");
36744
+ }
36745
+ }
36746
+ if (step.verification && (!usesOwnerFlow || !usesDedicatedOwner) && !completionReason) {
36747
+ const verificationResult = this.runVerification(step.verification, specialistOutput, step.name, effectiveOwner.interactive === false ? void 0 : resolvedTask);
36748
+ completionReason = verificationResult.completionReason;
36749
+ }
36750
+ if (completionReason === "retry_requested_by_owner") {
36751
+ throw new WorkflowCompletionError(`Step "${step.name}" owner requested another attempt`, "retry_requested_by_owner");
36014
36752
  }
36015
36753
  let combinedOutput = specialistOutput;
36016
36754
  if (usesOwnerFlow && reviewDef) {
@@ -36020,19 +36758,26 @@ ${resolvedTask}`;
36020
36758
  }
36021
36759
  state.row.status = "completed";
36022
36760
  state.row.output = combinedOutput;
36761
+ state.row.completionReason = completionReason;
36023
36762
  state.row.completedAt = (/* @__PURE__ */ new Date()).toISOString();
36024
36763
  await this.db.updateStep(state.row.id, {
36025
36764
  status: "completed",
36026
36765
  output: combinedOutput,
36766
+ completionReason,
36027
36767
  completedAt: state.row.completedAt,
36028
36768
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
36029
36769
  });
36030
36770
  await this.persistStepOutput(runId, step.name, combinedOutput);
36031
36771
  this.emit({ type: "step:completed", runId, stepName: step.name, output: combinedOutput, exitCode: lastExitCode, exitSignal: lastExitSignal });
36772
+ this.finalizeStepEvidence(step.name, "completed", state.row.completedAt, completionReason);
36032
36773
  await this.trajectory?.stepCompleted(step, combinedOutput, attempt + 1);
36033
36774
  return;
36034
36775
  } catch (err) {
36035
36776
  lastError = err instanceof Error ? err.message : String(err);
36777
+ lastCompletionReason = err instanceof WorkflowCompletionError ? err.completionReason : void 0;
36778
+ if (lastCompletionReason === "retry_requested_by_owner" && attempt >= maxRetries) {
36779
+ lastError = this.buildOwnerRetryBudgetExceededMessage(step.name, maxRetries, lastError);
36780
+ }
36036
36781
  if (err instanceof SpawnExitError) {
36037
36782
  lastExitCode = err.exitCode;
36038
36783
  lastExitSignal = err.exitSignal;
@@ -36054,9 +36799,19 @@ ${resolvedTask}`;
36054
36799
  await this.markStepFailed(state, lastError ?? "Unknown error", runId, {
36055
36800
  exitCode: lastExitCode,
36056
36801
  exitSignal: lastExitSignal
36057
- });
36802
+ }, lastCompletionReason);
36058
36803
  throw new Error(`Step "${step.name}" failed after ${maxRetries} retries: ${lastError ?? "Unknown error"}`);
36059
36804
  }
36805
+ buildOwnerRetryBudgetExceededMessage(stepName, maxRetries, ownerDecisionError) {
36806
+ const attempts = maxRetries + 1;
36807
+ const prefix = `Step "${stepName}" `;
36808
+ const normalizedDecision = ownerDecisionError?.startsWith(prefix) ? ownerDecisionError.slice(prefix.length).trim() : ownerDecisionError?.trim();
36809
+ const decisionSuffix = normalizedDecision ? ` Latest owner decision: ${normalizedDecision}` : "";
36810
+ if (maxRetries === 0) {
36811
+ return `Step "${stepName}" owner requested another attempt, but no retries are configured (maxRetries=0). Configure retries > 0 to allow OWNER_DECISION: INCOMPLETE_RETRY.` + decisionSuffix;
36812
+ }
36813
+ return `Step "${stepName}" owner requested another attempt after ${attempts} total attempts, but the retry budget is exhausted (maxRetries=${maxRetries}).` + decisionSuffix;
36814
+ }
36060
36815
  injectStepOwnerContract(step, resolvedTask, ownerDef, specialistDef) {
36061
36816
  if (ownerDef.interactive === false)
36062
36817
  return resolvedTask;
@@ -36068,12 +36823,18 @@ STEP OWNER CONTRACT:
36068
36823
  - You are the accountable owner for step "${step.name}".
36069
36824
  ` + (specialistNote ? `- ${specialistNote}
36070
36825
  ` : "") + `- If you delegate, you must still verify completion yourself.
36071
- - Before exiting, provide an explicit completion line: STEP_COMPLETE:${step.name}
36826
+ - Preferred final decision format:
36827
+ OWNER_DECISION: <one of COMPLETE, INCOMPLETE_RETRY, INCOMPLETE_FAIL, NEEDS_CLARIFICATION>
36828
+ REASON: <one sentence>
36829
+ - Legacy completion marker still supported: STEP_COMPLETE:${step.name}
36072
36830
  - Then self-terminate immediately with /exit.`;
36073
36831
  }
36074
36832
  buildOwnerSupervisorTask(step, originalTask, supervised, workerRuntimeName) {
36075
36833
  const verificationGuide = this.buildSupervisorVerificationGuide(step.verification);
36076
36834
  const channelLine = this.channel ? `#${this.channel}` : "(workflow channel unavailable)";
36835
+ 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}
36836
+ - When you have validated the handoff, post \`LEAD_DONE: <brief summary>\` to ${channelLine} before you exit
36837
+ ` : "";
36077
36838
  return `You are the step owner/supervisor for step "${step.name}".
36078
36839
 
36079
36840
  Worker: ${supervised.specialist.name} (runtime: ${workerRuntimeName}) on ${channelLine}
@@ -36085,9 +36846,23 @@ How to verify completion:
36085
36846
  - Watch ${channelLine} for the worker's progress messages and mirrored PTY output
36086
36847
  - Check file changes: run \`git diff --stat\` or inspect expected files directly
36087
36848
  - 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}`;
36849
+ ` + channelContract + verificationGuide + `
36850
+ When you have enough evidence, return:
36851
+ OWNER_DECISION: <one of COMPLETE, INCOMPLETE_RETRY, INCOMPLETE_FAIL, NEEDS_CLARIFICATION>
36852
+ REASON: <one sentence>
36853
+ Legacy completion marker still supported: STEP_COMPLETE:${step.name}`;
36854
+ }
36855
+ buildWorkerHandoffTask(step, originalTask, supervised) {
36856
+ if (!this.channel)
36857
+ return originalTask;
36858
+ return `${originalTask}
36859
+
36860
+ ---
36861
+ WORKER COMPLETION CONTRACT:
36862
+ - You are handing work off to owner "${supervised.owner.name}" for step "${step.name}".
36863
+ - When your work is ready for review, post to #${this.channel}: \`WORKER_DONE: <brief summary>\`
36864
+ - Do not rely on terminal output alone for handoff; use the workflow group chat signal above.
36865
+ - After posting your handoff signal, self-terminate with /exit unless the owner asks for follow-up.`;
36091
36866
  }
36092
36867
  buildSupervisorVerificationGuide(verification) {
36093
36868
  if (!verification)
@@ -36111,8 +36886,9 @@ Output exactly: STEP_COMPLETE:${step.name}`;
36111
36886
  }
36112
36887
  async executeSupervisedAgentStep(step, supervised, resolvedTask, timeoutMs) {
36113
36888
  if (this.executor) {
36889
+ const specialistTask2 = this.buildWorkerHandoffTask(step, resolvedTask, supervised);
36114
36890
  const supervisorTask2 = this.buildOwnerSupervisorTask(step, resolvedTask, supervised, supervised.specialist.name);
36115
- const specialistStep2 = { ...step, task: resolvedTask };
36891
+ const specialistStep2 = { ...step, task: specialistTask2 };
36116
36892
  const ownerStep2 = {
36117
36893
  ...step,
36118
36894
  name: `${step.name}-owner`,
@@ -36120,15 +36896,20 @@ Output exactly: STEP_COMPLETE:${step.name}`;
36120
36896
  task: supervisorTask2
36121
36897
  };
36122
36898
  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);
36899
+ const specialistPromise = this.executor.executeAgentStep(specialistStep2, supervised.specialist, specialistTask2, timeoutMs);
36124
36900
  const specialistSettled = specialistPromise.catch(() => void 0);
36125
36901
  try {
36126
36902
  const ownerStartTime2 = Date.now();
36127
36903
  const ownerOutput = await this.executor.executeAgentStep(ownerStep2, supervised.owner, supervisorTask2, timeoutMs);
36128
36904
  const ownerElapsed = Date.now() - ownerStartTime2;
36129
- this.assertOwnerCompletionMarker(step, ownerOutput, supervisorTask2);
36130
36905
  const specialistOutput = await specialistPromise;
36131
- return { specialistOutput, ownerOutput, ownerElapsed };
36906
+ const completionDecision = this.resolveOwnerCompletionDecision(step, ownerOutput, specialistOutput, supervisorTask2, resolvedTask);
36907
+ return {
36908
+ specialistOutput,
36909
+ ownerOutput,
36910
+ ownerElapsed,
36911
+ completionReason: completionDecision.completionReason
36912
+ };
36132
36913
  } catch (error48) {
36133
36914
  await specialistSettled;
36134
36915
  throw error48;
@@ -36144,10 +36925,14 @@ Output exactly: STEP_COMPLETE:${step.name}`;
36144
36925
  resolveWorkerSpawn = resolve3;
36145
36926
  rejectWorkerSpawn = reject;
36146
36927
  });
36147
- const specialistStep = { ...step, task: resolvedTask };
36928
+ const specialistTask = this.buildWorkerHandoffTask(step, resolvedTask, supervised);
36929
+ const specialistStep = { ...step, task: specialistTask };
36148
36930
  this.log(`[${step.name}] Spawning specialist "${supervised.specialist.name}" (cli: ${supervised.specialist.cli})`);
36149
36931
  const workerPromise = this.spawnAndWait(supervised.specialist, specialistStep, timeoutMs, {
36150
36932
  agentNameSuffix: "worker",
36933
+ evidenceStepName: step.name,
36934
+ evidenceRole: "worker",
36935
+ logicalName: supervised.specialist.name,
36151
36936
  onSpawned: ({ actualName, agent }) => {
36152
36937
  workerHandle = agent;
36153
36938
  workerRuntimeName = actualName;
@@ -36162,7 +36947,7 @@ Output exactly: STEP_COMPLETE:${step.name}`;
36162
36947
  }
36163
36948
  },
36164
36949
  onChunk: ({ agentName, chunk }) => {
36165
- this.forwardAgentChunkToChannel(step.name, "Worker", agentName, chunk);
36950
+ this.forwardAgentChunkToChannel(step.name, "Worker", agentName, chunk, supervised.specialist.name);
36166
36951
  }
36167
36952
  }).catch((error48) => {
36168
36953
  if (!workerSpawned) {
@@ -36174,13 +36959,23 @@ Output exactly: STEP_COMPLETE:${step.name}`;
36174
36959
  const workerSettled = workerPromise.catch(() => void 0);
36175
36960
  workerPromise.then((result) => {
36176
36961
  workerReleased = true;
36177
- this.postToChannel(`**[${step.name}]** Worker \`${workerRuntimeName}\` exited`);
36962
+ this.log(`[${step.name}] Worker ${workerRuntimeName} exited`);
36963
+ this.recordStepToolSideEffect(step.name, {
36964
+ type: "worker_exit",
36965
+ detail: `Worker ${workerRuntimeName} exited`,
36966
+ raw: { worker: workerRuntimeName, exitCode: result.exitCode, exitSignal: result.exitSignal }
36967
+ });
36178
36968
  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)}`);
36969
+ this.log(`[${step.name}] Verification gate observed: output contains ${JSON.stringify(step.verification.value)}`);
36180
36970
  }
36181
36971
  }).catch((error48) => {
36182
36972
  const message = error48 instanceof Error ? error48.message : String(error48);
36183
36973
  this.postToChannel(`**[${step.name}]** Worker \`${workerRuntimeName}\` exited with error: ${message}`);
36974
+ this.recordStepToolSideEffect(step.name, {
36975
+ type: "worker_error",
36976
+ detail: `Worker ${workerRuntimeName} exited with error: ${message}`,
36977
+ raw: { worker: workerRuntimeName, error: message }
36978
+ });
36184
36979
  });
36185
36980
  await workerReady;
36186
36981
  const supervisorTask = this.buildOwnerSupervisorTask(step, resolvedTask, supervised, workerRuntimeName);
@@ -36195,6 +36990,9 @@ Output exactly: STEP_COMPLETE:${step.name}`;
36195
36990
  try {
36196
36991
  const ownerResultObj = await this.spawnAndWait(supervised.owner, ownerStep, timeoutMs, {
36197
36992
  agentNameSuffix: "owner",
36993
+ evidenceStepName: step.name,
36994
+ evidenceRole: "owner",
36995
+ logicalName: supervised.owner.name,
36198
36996
  onSpawned: ({ actualName }) => {
36199
36997
  this.supervisedRuntimeAgents.set(actualName, {
36200
36998
  stepName: step.name,
@@ -36209,9 +37007,14 @@ Output exactly: STEP_COMPLETE:${step.name}`;
36209
37007
  const ownerElapsed = Date.now() - ownerStartTime;
36210
37008
  const ownerOutput = ownerResultObj.output;
36211
37009
  this.log(`[${step.name}] Owner "${supervised.owner.name}" exited`);
36212
- this.assertOwnerCompletionMarker(step, ownerOutput, supervisorTask);
36213
37010
  const specialistOutput = (await workerPromise).output;
36214
- return { specialistOutput, ownerOutput, ownerElapsed };
37011
+ const completionDecision = this.resolveOwnerCompletionDecision(step, ownerOutput, specialistOutput, supervisorTask, resolvedTask);
37012
+ return {
37013
+ specialistOutput,
37014
+ ownerOutput,
37015
+ ownerElapsed,
37016
+ completionReason: completionDecision.completionReason
37017
+ };
36215
37018
  } catch (error48) {
36216
37019
  const message = error48 instanceof Error ? error48.message : String(error48);
36217
37020
  if (!workerReleased && workerHandle) {
@@ -36224,10 +37027,16 @@ Output exactly: STEP_COMPLETE:${step.name}`;
36224
37027
  throw error48;
36225
37028
  }
36226
37029
  }
36227
- forwardAgentChunkToChannel(stepName, roleLabel, agentName, chunk) {
36228
- const lines = _WorkflowRunner.stripAnsi(chunk).split("\n").map((line) => line.trim()).filter(Boolean).slice(0, 3);
37030
+ forwardAgentChunkToChannel(stepName, roleLabel, agentName, chunk, sender) {
37031
+ const lines = _WorkflowRunner.scrubForChannel(chunk).split("\n").map((line) => line.trim()).filter(Boolean).slice(0, 3);
36229
37032
  for (const line of lines) {
36230
- this.postToChannel(`**[${stepName}]** ${roleLabel} \`${agentName}\`: ${line.slice(0, 280)}`);
37033
+ this.postToChannel(`**[${stepName}]** ${roleLabel} \`${agentName}\`: ${line.slice(0, 280)}`, {
37034
+ stepName,
37035
+ sender,
37036
+ actor: agentName,
37037
+ role: roleLabel,
37038
+ origin: "forwarded_chunk"
37039
+ });
36231
37040
  }
36232
37041
  }
36233
37042
  async recordOwnerMonitoringChunk(step, ownerDef, chunk) {
@@ -36242,6 +37051,11 @@ Output exactly: STEP_COMPLETE:${step.name}`;
36242
37051
  if (/STEP_COMPLETE:/i.test(stripped))
36243
37052
  details.push("Declared the step complete");
36244
37053
  for (const detail of details) {
37054
+ this.recordStepToolSideEffect(step.name, {
37055
+ type: "owner_monitoring",
37056
+ detail,
37057
+ raw: { output: stripped.slice(0, 240), owner: ownerDef.name }
37058
+ });
36245
37059
  await this.trajectory?.ownerMonitoringEvent(step.name, ownerDef.name, detail, {
36246
37060
  output: stripped.slice(0, 240)
36247
37061
  });
@@ -36280,6 +37094,7 @@ Output exactly: STEP_COMPLETE:${step.name}`;
36280
37094
  }
36281
37095
  resolveAutoReviewAgent(ownerDef, agentMap) {
36282
37096
  const allDefs = [...agentMap.values()].map((d) => _WorkflowRunner.resolveAgentDef(d));
37097
+ const eligible = (def) => def.name !== ownerDef.name && !this.isExplicitInteractiveWorker(def);
36283
37098
  const isReviewer = (def) => {
36284
37099
  const roleLC = def.role?.toLowerCase() ?? "";
36285
37100
  const nameLC = def.name.toLowerCase();
@@ -36298,28 +37113,187 @@ Output exactly: STEP_COMPLETE:${step.name}`;
36298
37113
  return 2;
36299
37114
  return isReviewer(def) ? 1 : 0;
36300
37115
  };
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];
37116
+ const dedicated = allDefs.filter((d) => eligible(d) && isReviewer(d)).sort((a, b) => reviewerPriority(b) - reviewerPriority(a) || a.name.localeCompare(b.name))[0];
36302
37117
  if (dedicated)
36303
37118
  return dedicated;
36304
- const alternate = allDefs.find((d) => d.name !== ownerDef.name && d.interactive !== false);
37119
+ const alternate = allDefs.find((d) => eligible(d) && d.interactive !== false);
36305
37120
  if (alternate)
36306
37121
  return alternate;
36307
37122
  return ownerDef;
36308
37123
  }
36309
- assertOwnerCompletionMarker(step, output, injectedTaskText) {
37124
+ isExplicitInteractiveWorker(agentDef) {
37125
+ return agentDef.preset === "worker" && agentDef.interactive !== false;
37126
+ }
37127
+ resolveOwnerCompletionDecision(step, ownerOutput, specialistOutput, injectedTaskText, verificationTaskText) {
37128
+ const hasMarker = this.hasOwnerCompletionMarker(step, ownerOutput, injectedTaskText);
37129
+ const explicitOwnerDecision = this.parseOwnerDecision(step, ownerOutput, false);
37130
+ if (explicitOwnerDecision?.decision === "INCOMPLETE_RETRY") {
37131
+ throw new WorkflowCompletionError(`Step "${step.name}" owner requested retry${explicitOwnerDecision.reason ? `: ${explicitOwnerDecision.reason}` : ""}`, "retry_requested_by_owner");
37132
+ }
37133
+ if (explicitOwnerDecision?.decision === "INCOMPLETE_FAIL") {
37134
+ throw new WorkflowCompletionError(`Step "${step.name}" owner marked the step incomplete${explicitOwnerDecision.reason ? `: ${explicitOwnerDecision.reason}` : ""}`, "failed_owner_decision");
37135
+ }
37136
+ if (explicitOwnerDecision?.decision === "NEEDS_CLARIFICATION") {
37137
+ throw new WorkflowCompletionError(`Step "${step.name}" owner requested clarification before completion${explicitOwnerDecision.reason ? `: ${explicitOwnerDecision.reason}` : ""}`, "retry_requested_by_owner");
37138
+ }
37139
+ const verificationResult = step.verification ? this.runVerification(step.verification, specialistOutput, step.name, verificationTaskText, {
37140
+ allowFailure: true,
37141
+ completionMarkerFound: hasMarker
37142
+ }) : { passed: false };
37143
+ if (verificationResult.error) {
37144
+ throw new WorkflowCompletionError(`Step "${step.name}" verification failed and no owner decision or evidence established completion: ${verificationResult.error}`, "failed_verification");
37145
+ }
37146
+ if (explicitOwnerDecision?.decision === "COMPLETE") {
37147
+ if (!hasMarker) {
37148
+ this.log(`[${step.name}] Structured OWNER_DECISION completed the step without legacy STEP_COMPLETE marker`);
37149
+ }
37150
+ return {
37151
+ completionReason: "completed_by_owner_decision",
37152
+ ownerDecision: explicitOwnerDecision.decision,
37153
+ reason: explicitOwnerDecision.reason
37154
+ };
37155
+ }
37156
+ if (verificationResult.passed) {
37157
+ return { completionReason: "completed_verified" };
37158
+ }
37159
+ const ownerDecision = this.parseOwnerDecision(step, ownerOutput, hasMarker);
37160
+ if (ownerDecision?.decision === "COMPLETE") {
37161
+ return {
37162
+ completionReason: "completed_by_owner_decision",
37163
+ ownerDecision: ownerDecision.decision,
37164
+ reason: ownerDecision.reason
37165
+ };
37166
+ }
37167
+ if (!explicitOwnerDecision) {
37168
+ const evidenceReason = this.judgeOwnerCompletionByEvidence(step.name, ownerOutput);
37169
+ if (evidenceReason) {
37170
+ if (!hasMarker) {
37171
+ this.log(`[${step.name}] Evidence-based completion resolved without legacy STEP_COMPLETE marker`);
37172
+ }
37173
+ return {
37174
+ completionReason: "completed_by_evidence",
37175
+ reason: evidenceReason
37176
+ };
37177
+ }
37178
+ }
37179
+ const processExitFallback = this.tryProcessExitFallback(step, specialistOutput, verificationTaskText, ownerOutput);
37180
+ if (processExitFallback) {
37181
+ this.log(`[${step.name}] Completion inferred from clean process exit (code 0)` + (step.verification ? " + verification passed" : "") + " \u2014 no coordination signal was required");
37182
+ return processExitFallback;
37183
+ }
37184
+ 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");
37185
+ }
37186
+ hasExplicitInteractiveWorkerCompletionEvidence(step, output, injectedTaskText, verificationTaskText) {
37187
+ try {
37188
+ this.resolveOwnerCompletionDecision(step, output, output, injectedTaskText, verificationTaskText);
37189
+ return true;
37190
+ } catch {
37191
+ return false;
37192
+ }
37193
+ }
37194
+ hasOwnerCompletionMarker(step, output, injectedTaskText) {
36310
37195
  const marker = `STEP_COMPLETE:${step.name}`;
36311
37196
  const taskHasMarker = injectedTaskText.includes(marker);
36312
37197
  const first = output.indexOf(marker);
36313
37198
  if (first === -1) {
36314
- throw new Error(`Step "${step.name}" owner completion marker missing: "${marker}"`);
37199
+ return false;
36315
37200
  }
36316
- const outputLikelyContainsInjectedPrompt = output.includes("STEP OWNER CONTRACT") || output.includes("Output exactly: STEP_COMPLETE:");
37201
+ 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:");
36317
37202
  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
- }
37203
+ return output.includes(marker, first + marker.length);
37204
+ }
37205
+ return true;
37206
+ }
37207
+ parseOwnerDecision(step, ownerOutput, hasMarker) {
37208
+ const decisionPattern = /OWNER_DECISION:\s*(COMPLETE|INCOMPLETE_RETRY|INCOMPLETE_FAIL|NEEDS_CLARIFICATION)\b/gi;
37209
+ const decisionMatches = [...ownerOutput.matchAll(decisionPattern)];
37210
+ 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");
37211
+ if (decisionMatches.length === 0) {
37212
+ if (!hasMarker)
37213
+ return null;
37214
+ return {
37215
+ decision: "COMPLETE",
37216
+ reason: `Legacy completion marker observed: STEP_COMPLETE:${step.name}`
37217
+ };
37218
+ }
37219
+ const realMatches = outputLikelyContainsEchoedPrompt ? decisionMatches.filter((m) => {
37220
+ const lineStart = ownerOutput.lastIndexOf("\n", m.index) + 1;
37221
+ const lineEnd = ownerOutput.indexOf("\n", m.index);
37222
+ const line = ownerOutput.slice(lineStart, lineEnd === -1 ? void 0 : lineEnd);
37223
+ return !line.includes("COMPLETE|INCOMPLETE_RETRY");
37224
+ }) : decisionMatches;
37225
+ const decisionMatch = realMatches.length > 0 ? realMatches[realMatches.length - 1] : decisionMatches[decisionMatches.length - 1];
37226
+ const decision = decisionMatch?.[1]?.toUpperCase();
37227
+ if (decision !== "COMPLETE" && decision !== "INCOMPLETE_RETRY" && decision !== "INCOMPLETE_FAIL" && decision !== "NEEDS_CLARIFICATION") {
37228
+ return null;
37229
+ }
37230
+ const reasonPattern = /(?:^|\n)REASON:\s*(.+)/gi;
37231
+ const reasonMatches = [...ownerOutput.matchAll(reasonPattern)];
37232
+ const reasonMatch = outputLikelyContainsEchoedPrompt && reasonMatches.length > 1 ? reasonMatches[reasonMatches.length - 1] : reasonMatches[0];
37233
+ const reason = reasonMatch?.[1]?.trim();
37234
+ return {
37235
+ decision,
37236
+ reason: reason && reason !== "<one sentence>" ? reason : void 0
37237
+ };
37238
+ }
37239
+ stripEchoedPromptLines(output, patterns) {
37240
+ return output.split("\n").map((line) => line.trim()).filter(Boolean).filter((line) => patterns.every((pattern) => !pattern.test(line))).join("\n");
37241
+ }
37242
+ firstMeaningfulLine(output) {
37243
+ return output.split("\n").map((line) => line.trim()).find(Boolean);
37244
+ }
37245
+ judgeOwnerCompletionByEvidence(stepName, ownerOutput) {
37246
+ if (/OWNER_DECISION:\s*(?:INCOMPLETE_RETRY|INCOMPLETE_FAIL|NEEDS_CLARIFICATION)\b/i.test(ownerOutput)) {
37247
+ return null;
36322
37248
  }
37249
+ const sanitized = this.stripEchoedPromptLines(ownerOutput, [
37250
+ /^STEP OWNER CONTRACT:?$/i,
37251
+ /^Preferred final decision format:?$/i,
37252
+ /^OWNER_DECISION:\s*(?:COMPLETE\|INCOMPLETE_RETRY|<one of COMPLETE, INCOMPLETE_RETRY)/i,
37253
+ /^REASON:\s*<one sentence>$/i,
37254
+ /^Legacy completion marker still supported:/i,
37255
+ /^STEP_COMPLETE:/i
37256
+ ]);
37257
+ if (!sanitized)
37258
+ return null;
37259
+ const hasExplicitSelfRelease = /Calling\s+(?:[\w.-]+\.)?remove_agent\(\{[^<\n]*"reason":"task completed"/i.test(sanitized);
37260
+ 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;
37261
+ const evidence = this.getStepCompletionEvidence(stepName);
37262
+ 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;
37263
+ 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;
37264
+ const hasEvidenceSignal = hasValidatedCoordinationSignal || hasValidatedInspectionSignal;
37265
+ if (!hasPositiveConclusion || !hasEvidenceSignal) {
37266
+ return null;
37267
+ }
37268
+ return this.firstMeaningfulLine(sanitized) ?? "Evidence-backed completion";
37269
+ }
37270
+ /**
37271
+ * Process-exit fallback: when agent exits with code 0 but posts no coordination
37272
+ * signal, check if verification passes (or no verification is configured) and
37273
+ * infer completion. This is the key mechanism for reducing agent compliance
37274
+ * dependence — the runner trusts a clean exit + passing verification over
37275
+ * requiring exact signal text.
37276
+ */
37277
+ tryProcessExitFallback(step, specialistOutput, verificationTaskText, ownerOutput) {
37278
+ const gracePeriodMs = this.currentConfig?.swarm.completionGracePeriodMs ?? 5e3;
37279
+ if (gracePeriodMs === 0)
37280
+ return null;
37281
+ if (ownerOutput && /OWNER_DECISION:\s*(?:INCOMPLETE_RETRY|INCOMPLETE_FAIL|NEEDS_CLARIFICATION)\b/i.test(ownerOutput)) {
37282
+ return null;
37283
+ }
37284
+ const evidence = this.getStepCompletionEvidence(step.name);
37285
+ const hasCleanExit = evidence?.coordinationSignals.some((signal) => signal.kind === "process_exit" && signal.value === "0") ?? false;
37286
+ if (!hasCleanExit)
37287
+ return null;
37288
+ if (step.verification) {
37289
+ const verificationResult = this.runVerification(step.verification, specialistOutput, step.name, verificationTaskText, { allowFailure: true });
37290
+ if (!verificationResult.passed)
37291
+ return null;
37292
+ }
37293
+ return {
37294
+ completionReason: "completed_by_process_exit",
37295
+ reason: `Process exited with code 0${step.verification ? " and verification passed" : ""} \u2014 coordination signal not required`
37296
+ };
36323
37297
  }
36324
37298
  async runStepReviewGate(step, resolvedTask, specialistOutput, ownerOutput, ownerDef, reviewerDef, timeoutMs) {
36325
37299
  const reviewSnippetMax = 12e3;
@@ -36365,7 +37339,17 @@ Then output /exit.`;
36365
37339
  };
36366
37340
  await this.trajectory?.registerAgent(reviewerDef.name, "reviewer");
36367
37341
  this.postToChannel(`**[${step.name}]** Review started (reviewer: ${reviewerDef.name})`);
37342
+ this.recordStepToolSideEffect(step.name, {
37343
+ type: "review_started",
37344
+ detail: `Review started with ${reviewerDef.name}`,
37345
+ raw: { reviewer: reviewerDef.name }
37346
+ });
36368
37347
  const emitReviewCompleted = async (decision, reason) => {
37348
+ this.recordStepToolSideEffect(step.name, {
37349
+ type: "review_completed",
37350
+ detail: `Review ${decision} by ${reviewerDef.name}${reason ? `: ${reason}` : ""}`,
37351
+ raw: { reviewer: reviewerDef.name, decision, reason }
37352
+ });
36369
37353
  await this.trajectory?.reviewCompleted(step.name, reviewerDef.name, decision, reason);
36370
37354
  this.emit({
36371
37355
  type: "step:review-completed",
@@ -36409,6 +37393,9 @@ Then output /exit.`;
36409
37393
  };
36410
37394
  try {
36411
37395
  await this.spawnAndWait(reviewerDef, reviewStep, safetyTimeoutMs, {
37396
+ evidenceStepName: step.name,
37397
+ evidenceRole: "reviewer",
37398
+ logicalName: reviewerDef.name,
36412
37399
  onSpawned: ({ agent }) => {
36413
37400
  reviewerHandle = agent;
36414
37401
  },
@@ -36445,13 +37432,30 @@ Then output /exit.`;
36445
37432
  return reviewOutput;
36446
37433
  }
36447
37434
  parseReviewDecision(reviewOutput) {
37435
+ const strict = this.parseStrictReviewDecision(reviewOutput);
37436
+ if (strict) {
37437
+ return strict;
37438
+ }
37439
+ const tolerant = this.parseTolerantReviewDecision(reviewOutput);
37440
+ if (tolerant) {
37441
+ return tolerant;
37442
+ }
37443
+ return this.judgeReviewDecisionFromEvidence(reviewOutput);
37444
+ }
37445
+ parseStrictReviewDecision(reviewOutput) {
36448
37446
  const decisionPattern = /REVIEW_DECISION:\s*(APPROVE|REJECT)/gi;
36449
37447
  const decisionMatches = [...reviewOutput.matchAll(decisionPattern)];
36450
37448
  if (decisionMatches.length === 0) {
36451
37449
  return null;
36452
37450
  }
36453
37451
  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];
37452
+ const realReviewMatches = outputLikelyContainsEchoedPrompt ? decisionMatches.filter((m) => {
37453
+ const lineStart = reviewOutput.lastIndexOf("\n", m.index) + 1;
37454
+ const lineEnd = reviewOutput.indexOf("\n", m.index);
37455
+ const line = reviewOutput.slice(lineStart, lineEnd === -1 ? void 0 : lineEnd);
37456
+ return !line.includes("APPROVE or REJECT");
37457
+ }) : decisionMatches;
37458
+ const decisionMatch = realReviewMatches.length > 0 ? realReviewMatches[realReviewMatches.length - 1] : decisionMatches[decisionMatches.length - 1];
36455
37459
  const decision = decisionMatch?.[1]?.toUpperCase();
36456
37460
  if (decision !== "APPROVE" && decision !== "REJECT") {
36457
37461
  return null;
@@ -36465,6 +37469,80 @@ Then output /exit.`;
36465
37469
  reason: reason && reason !== "<one sentence>" ? reason : void 0
36466
37470
  };
36467
37471
  }
37472
+ parseTolerantReviewDecision(reviewOutput) {
37473
+ const sanitized = this.stripEchoedPromptLines(reviewOutput, [
37474
+ /^Return exactly:?$/i,
37475
+ /^REVIEW_DECISION:\s*APPROVE\s+or\s+REJECT$/i,
37476
+ /^REVIEW_REASON:\s*<one sentence>$/i
37477
+ ]);
37478
+ if (!sanitized) {
37479
+ return null;
37480
+ }
37481
+ const lines = sanitized.split("\n").map((line) => line.trim()).filter(Boolean);
37482
+ for (const line of lines) {
37483
+ const candidate = line.replace(/^REVIEW_DECISION:\s*/i, "").trim();
37484
+ const decision2 = this.normalizeReviewDecisionCandidate(candidate);
37485
+ if (decision2) {
37486
+ return {
37487
+ decision: decision2,
37488
+ reason: this.parseReviewReason(sanitized) ?? this.firstMeaningfulLine(sanitized)
37489
+ };
37490
+ }
37491
+ }
37492
+ const decision = this.normalizeReviewDecisionCandidate(lines.join(" "));
37493
+ if (!decision) {
37494
+ return null;
37495
+ }
37496
+ return {
37497
+ decision,
37498
+ reason: this.parseReviewReason(sanitized) ?? this.firstMeaningfulLine(sanitized)
37499
+ };
37500
+ }
37501
+ normalizeReviewDecisionCandidate(candidate) {
37502
+ const value = candidate.trim().toLowerCase();
37503
+ if (!value)
37504
+ return null;
37505
+ if (/^(approve|approved|complete|completed|pass|passed|accept|accepted|lgtm|ship it|looks good|looks fine)\b/i.test(value)) {
37506
+ return "approved";
37507
+ }
37508
+ if (/^(reject|rejected|retry|retry requested|fail|failed|incomplete|needs clarification|not complete|not ready|insufficient evidence)\b/i.test(value)) {
37509
+ return "rejected";
37510
+ }
37511
+ return null;
37512
+ }
37513
+ parseReviewReason(reviewOutput) {
37514
+ const reasonPattern = /REVIEW_REASON:\s*(.+)/gi;
37515
+ const reasonMatches = [...reviewOutput.matchAll(reasonPattern)];
37516
+ const outputLikelyContainsEchoedPrompt = reviewOutput.includes("Return exactly") || reviewOutput.includes("REVIEW_DECISION: APPROVE or REJECT");
37517
+ const reasonMatch = outputLikelyContainsEchoedPrompt && reasonMatches.length > 1 ? reasonMatches[reasonMatches.length - 1] : reasonMatches[0];
37518
+ const reason = reasonMatch?.[1]?.trim();
37519
+ return reason && reason !== "<one sentence>" ? reason : void 0;
37520
+ }
37521
+ judgeReviewDecisionFromEvidence(reviewOutput) {
37522
+ const sanitized = this.stripEchoedPromptLines(reviewOutput, [
37523
+ /^Return exactly:?$/i,
37524
+ /^REVIEW_DECISION:\s*APPROVE\s+or\s+REJECT$/i,
37525
+ /^REVIEW_REASON:\s*<one sentence>$/i
37526
+ ]);
37527
+ if (!sanitized) {
37528
+ return null;
37529
+ }
37530
+ const hasPositiveEvidence = /\b(approved?|complete(?:d)?|verified|looks good|looks fine|safe handoff|pass(?:ed)?)\b/i.test(sanitized);
37531
+ const hasNegativeEvidence = /\b(reject(?:ed)?|retry|fail(?:ed)?|incomplete|missing checks|insufficient evidence|not safe)\b/i.test(sanitized);
37532
+ if (hasNegativeEvidence) {
37533
+ return {
37534
+ decision: "rejected",
37535
+ reason: this.parseReviewReason(sanitized) ?? this.firstMeaningfulLine(sanitized)
37536
+ };
37537
+ }
37538
+ if (!hasPositiveEvidence) {
37539
+ return null;
37540
+ }
37541
+ return {
37542
+ decision: "approved",
37543
+ reason: this.parseReviewReason(sanitized) ?? this.firstMeaningfulLine(sanitized)
37544
+ };
37545
+ }
36468
37546
  combineStepAndReviewOutput(stepOutput, reviewOutput) {
36469
37547
  const primary = stepOutput.trimEnd();
36470
37548
  const review = reviewOutput.trim();
@@ -36671,10 +37749,18 @@ DO NOT:
36671
37749
  reject(new Error(`Failed to spawn ${cmd}: ${err.message}`));
36672
37750
  });
36673
37751
  });
37752
+ this.captureStepTerminalEvidence(step.name, {}, { exitCode, exitSignal });
36674
37753
  return { output, exitCode, exitSignal };
36675
37754
  } finally {
36676
- const combinedOutput = stdoutChunks.join("") + stderrChunks.join("");
37755
+ const stdout = stdoutChunks.join("");
37756
+ const stderr = stderrChunks.join("");
37757
+ const combinedOutput = stdout + stderr;
36677
37758
  this.lastFailedStepOutput.set(step.name, combinedOutput);
37759
+ this.captureStepTerminalEvidence(step.name, {
37760
+ stdout,
37761
+ stderr,
37762
+ combined: combinedOutput
37763
+ });
36678
37764
  stopHeartbeat?.();
36679
37765
  logStream.end();
36680
37766
  this.unregisterWorker(agentName);
@@ -36687,6 +37773,7 @@ DO NOT:
36687
37773
  if (!this.relay) {
36688
37774
  throw new Error("AgentRelay not initialized");
36689
37775
  }
37776
+ const evidenceStepName = options.evidenceStepName ?? step.name;
36690
37777
  const requestedName = `${step.name}${options.agentNameSuffix ? `-${options.agentNameSuffix}` : ""}-${(this.currentRunId ?? this.generateShortId()).slice(0, 8)}`;
36691
37778
  let agentName = requestedName;
36692
37779
  const role = agentDef.role?.toLowerCase() ?? "";
@@ -36714,11 +37801,17 @@ DO NOT:
36714
37801
  let ptyChunks = [];
36715
37802
  try {
36716
37803
  const agentCwd = this.resolveAgentCwd(agentDef);
37804
+ const interactiveSpawnPolicy = resolveSpawnPolicy({
37805
+ AGENT_NAME: agentName,
37806
+ AGENT_CLI: agentDef.cli,
37807
+ RELAY_API_KEY: this.relayApiKey ?? "workflow-runner",
37808
+ AGENT_CHANNELS: (agentChannels ?? []).join(",")
37809
+ });
36717
37810
  agent = await this.relay.spawnPty({
36718
37811
  name: agentName,
36719
37812
  cli: agentDef.cli,
36720
37813
  model: agentDef.constraints?.model,
36721
- args: [],
37814
+ args: interactiveSpawnPolicy.args,
36722
37815
  channels: agentChannels,
36723
37816
  task: taskWithExit,
36724
37817
  idleThresholdSecs: agentDef.constraints?.idleThresholdSecs,
@@ -36744,16 +37837,27 @@ DO NOT:
36744
37837
  const oldListener = this.ptyListeners.get(oldName);
36745
37838
  if (oldListener) {
36746
37839
  this.ptyListeners.delete(oldName);
36747
- this.ptyListeners.set(agent.name, (chunk) => {
37840
+ const resolvedAgentName = agent.name;
37841
+ this.ptyListeners.set(resolvedAgentName, (chunk) => {
36748
37842
  const stripped = _WorkflowRunner.stripAnsi(chunk);
36749
- this.ptyOutputBuffers.get(agent.name)?.push(stripped);
37843
+ this.ptyOutputBuffers.get(resolvedAgentName)?.push(stripped);
36750
37844
  newLogStream.write(chunk);
36751
- options.onChunk?.({ agentName: agent.name, chunk });
37845
+ options.onChunk?.({ agentName: resolvedAgentName, chunk });
36752
37846
  });
36753
37847
  }
36754
37848
  agentName = agent.name;
36755
37849
  }
36756
- await options.onSpawned?.({ requestedName, actualName: agent.name, agent });
37850
+ const liveAgent = agent;
37851
+ await options.onSpawned?.({ requestedName, actualName: liveAgent.name, agent: liveAgent });
37852
+ this.runtimeStepAgents.set(liveAgent.name, {
37853
+ stepName: evidenceStepName,
37854
+ role: options.evidenceRole ?? agentDef.role ?? "agent",
37855
+ logicalName: options.logicalName ?? agentDef.name
37856
+ });
37857
+ const signalParticipant = this.resolveSignalParticipantKind(options.evidenceRole ?? agentDef.role ?? "agent");
37858
+ if (signalParticipant) {
37859
+ this.rememberStepSignalSender(evidenceStepName, signalParticipant, liveAgent.name, options.logicalName ?? agentDef.name);
37860
+ }
36757
37861
  let workerPid;
36758
37862
  try {
36759
37863
  const rawAgents = await this.relay.listAgentsRaw();
@@ -36762,8 +37866,8 @@ DO NOT:
36762
37866
  }
36763
37867
  this.registerWorker(agentName, agentDef.cli, step.task ?? "", workerPid);
36764
37868
  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);
37869
+ const agentClient = await this.registerRelaycastExternalAgent(liveAgent.name, `Workflow agent for step "${step.name}" (${agentDef.cli})`).catch((err) => {
37870
+ console.warn(`[WorkflowRunner] Failed to register ${liveAgent.name} in Relaycast:`, err?.message ?? err);
36767
37871
  return null;
36768
37872
  });
36769
37873
  if (agentClient) {
@@ -36775,21 +37879,23 @@ DO NOT:
36775
37879
  await channelAgent?.channels.invite(this.channel, agent.name).catch(() => {
36776
37880
  });
36777
37881
  }
36778
- this.postToChannel(`**[${step.name}]** Assigned to \`${agent.name}\``);
37882
+ this.log(`[${step.name}] Assigned to ${agent.name}`);
36779
37883
  this.activeAgentHandles.set(agentName, agent);
36780
- exitResult = await this.waitForExitWithIdleNudging(agent, agentDef, step, timeoutMs);
37884
+ exitResult = await this.waitForExitWithIdleNudging(agent, agentDef, step, timeoutMs, options.preserveOnIdle ?? this.shouldPreserveIdleSupervisor(agentDef, step, options.evidenceRole));
36781
37885
  stopHeartbeat?.();
36782
37886
  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 {
37887
+ let timeoutRecovered = false;
37888
+ if (step.verification) {
37889
+ const ptyOutput = (this.ptyOutputBuffers.get(agentName) ?? []).join("");
37890
+ const verificationResult = this.runVerification(step.verification, ptyOutput, step.name, void 0, { allowFailure: true });
37891
+ if (verificationResult.passed) {
37892
+ this.log(`[${step.name}] Agent timed out but verification passed \u2014 treating as complete`);
37893
+ this.postToChannel(`**[${step.name}]** Agent idle after completing work \u2014 verification passed, releasing`);
36789
37894
  await agent.release();
36790
- throw new Error(`Step "${step.name}" timed out after ${timeoutMs ?? "unknown"}ms`);
37895
+ timeoutRecovered = true;
36791
37896
  }
36792
- } else {
37897
+ }
37898
+ if (!timeoutRecovered) {
36793
37899
  await agent.release();
36794
37900
  throw new Error(`Step "${step.name}" timed out after ${timeoutMs ?? "unknown"}ms`);
36795
37901
  }
@@ -36800,6 +37906,19 @@ DO NOT:
36800
37906
  } finally {
36801
37907
  ptyChunks = this.ptyOutputBuffers.get(agentName) ?? [];
36802
37908
  this.lastFailedStepOutput.set(step.name, ptyChunks.join(""));
37909
+ if (ptyChunks.length > 0 || agent?.exitCode !== void 0 || agent?.exitSignal !== void 0) {
37910
+ this.captureStepTerminalEvidence(evidenceStepName, {
37911
+ stdout: ptyChunks.length > 0 ? ptyChunks.join("") : void 0,
37912
+ combined: ptyChunks.length > 0 ? ptyChunks.join("") : void 0
37913
+ }, {
37914
+ exitCode: agent?.exitCode,
37915
+ exitSignal: agent?.exitSignal
37916
+ }, {
37917
+ sender: options.logicalName ?? agentDef.name,
37918
+ actor: agent?.name ?? agentName,
37919
+ role: options.evidenceRole ?? agentDef.role ?? "agent"
37920
+ });
37921
+ }
36803
37922
  stopHeartbeat?.();
36804
37923
  this.activeAgentHandles.delete(agentName);
36805
37924
  this.ptyOutputBuffers.delete(agentName);
@@ -36811,6 +37930,7 @@ DO NOT:
36811
37930
  }
36812
37931
  this.unregisterWorker(agentName);
36813
37932
  this.supervisedRuntimeAgents.delete(agentName);
37933
+ this.runtimeStepAgents.delete(agentName);
36814
37934
  }
36815
37935
  let output;
36816
37936
  if (ptyChunks.length > 0) {
@@ -36819,6 +37939,13 @@ DO NOT:
36819
37939
  const summaryPath = import_node_path8.default.join(this.summaryDir, `${step.name}.md`);
36820
37940
  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
37941
  }
37942
+ if (ptyChunks.length === 0) {
37943
+ this.captureStepTerminalEvidence(evidenceStepName, { stdout: output, combined: output }, { exitCode: agent?.exitCode, exitSignal: agent?.exitSignal }, {
37944
+ sender: options.logicalName ?? agentDef.name,
37945
+ actor: agent?.name ?? agentName,
37946
+ role: options.evidenceRole ?? agentDef.role ?? "agent"
37947
+ });
37948
+ }
36822
37949
  return {
36823
37950
  output,
36824
37951
  exitCode: agent?.exitCode,
@@ -36846,13 +37973,34 @@ DO NOT:
36846
37973
  "orchestrator",
36847
37974
  "auctioneer"
36848
37975
  ]);
37976
+ isLeadLikeAgent(agentDef, roleOverride) {
37977
+ if (agentDef.preset === "lead")
37978
+ return true;
37979
+ const role = (roleOverride ?? agentDef.role ?? "").toLowerCase();
37980
+ const nameLC = agentDef.name.toLowerCase();
37981
+ return [..._WorkflowRunner.HUB_ROLES].some((hubRole) => new RegExp(`\\b${hubRole}\\b`, "i").test(nameLC) || new RegExp(`\\b${hubRole}\\b`, "i").test(role));
37982
+ }
37983
+ shouldPreserveIdleSupervisor(agentDef, step, evidenceRole) {
37984
+ if (evidenceRole && /\bowner\b/i.test(evidenceRole)) {
37985
+ return true;
37986
+ }
37987
+ if (!this.isLeadLikeAgent(agentDef, evidenceRole)) {
37988
+ return false;
37989
+ }
37990
+ const task = step.task ?? "";
37991
+ return /\b(wait|waiting|monitor|supervis|check inbox|check.*channel|poll|DONE|_DONE|signal|handoff)\b/i.test(task);
37992
+ }
36849
37993
  /**
36850
37994
  * Wait for agent exit with idle detection and nudging.
36851
37995
  * If no idle nudge config is set, falls through to simple waitForExit.
36852
37996
  */
36853
- async waitForExitWithIdleNudging(agent, agentDef, step, timeoutMs) {
37997
+ async waitForExitWithIdleNudging(agent, agentDef, step, timeoutMs, preserveIdleSupervisor = false) {
36854
37998
  const nudgeConfig = this.currentConfig?.swarm.idleNudge;
36855
37999
  if (!nudgeConfig) {
38000
+ if (preserveIdleSupervisor) {
38001
+ this.log(`[${step.name}] Supervising agent "${agent.name}" may idle while waiting \u2014 using exit-only completion`);
38002
+ return agent.waitForExit(timeoutMs);
38003
+ }
36856
38004
  const result = await Promise.race([
36857
38005
  agent.waitForExit(timeoutMs).then((r) => ({ kind: "exit", result: r })),
36858
38006
  agent.waitForIdle(timeoutMs).then((r) => ({ kind: "idle", result: r }))
@@ -36869,6 +38017,7 @@ DO NOT:
36869
38017
  const escalateAfterMs = nudgeConfig.escalateAfterMs ?? 12e4;
36870
38018
  const maxNudges = nudgeConfig.maxNudges ?? 1;
36871
38019
  let nudgeCount = 0;
38020
+ let preservedSupervisorNoticeSent = false;
36872
38021
  const startTime = Date.now();
36873
38022
  while (true) {
36874
38023
  const elapsed = Date.now() - startTime;
@@ -36892,6 +38041,14 @@ DO NOT:
36892
38041
  this.emit({ type: "step:nudged", runId: this.currentRunId ?? "", stepName: step.name, nudgeCount });
36893
38042
  continue;
36894
38043
  }
38044
+ if (preserveIdleSupervisor) {
38045
+ if (!preservedSupervisorNoticeSent) {
38046
+ this.log(`[${step.name}] Supervising agent "${agent.name}" stayed idle after ${nudgeCount} nudge(s) \u2014 preserving until exit or timeout`);
38047
+ this.postToChannel(`**[${step.name}]** Supervising agent \`${agent.name}\` is waiting on handoff \u2014 keeping it alive until it exits or the step times out`);
38048
+ preservedSupervisorNoticeSent = true;
38049
+ }
38050
+ continue;
38051
+ }
36895
38052
  this.postToChannel(`**[${step.name}]** Agent \`${agent.name}\` still idle after ${nudgeCount} nudge(s) \u2014 force-releasing`);
36896
38053
  this.emit({ type: "step:force-released", runId: this.currentRunId ?? "", stepName: step.name });
36897
38054
  await agent.release();
@@ -36949,7 +38106,31 @@ DO NOT:
36949
38106
  return void 0;
36950
38107
  }
36951
38108
  // ── Verification ────────────────────────────────────────────────────────
36952
- runVerification(check2, output, stepName, injectedTaskText) {
38109
+ runVerification(check2, output, stepName, injectedTaskText, options) {
38110
+ const fail = (message) => {
38111
+ const observedAt2 = (/* @__PURE__ */ new Date()).toISOString();
38112
+ this.recordStepToolSideEffect(stepName, {
38113
+ type: "verification_observed",
38114
+ detail: message,
38115
+ observedAt: observedAt2,
38116
+ raw: { passed: false, type: check2.type, value: check2.value }
38117
+ });
38118
+ this.getOrCreateStepEvidenceRecord(stepName).evidence.coordinationSignals.push({
38119
+ kind: "verification_failed",
38120
+ source: "verification",
38121
+ text: message,
38122
+ observedAt: observedAt2,
38123
+ value: check2.value
38124
+ });
38125
+ if (options?.allowFailure) {
38126
+ return {
38127
+ passed: false,
38128
+ completionReason: "failed_verification",
38129
+ error: message
38130
+ };
38131
+ }
38132
+ throw new WorkflowCompletionError(message, "failed_verification");
38133
+ };
36953
38134
  switch (check2.type) {
36954
38135
  case "output_contains": {
36955
38136
  const token = check2.value;
@@ -36958,10 +38139,10 @@ DO NOT:
36958
38139
  const first = output.indexOf(token);
36959
38140
  const hasSecond = first !== -1 && output.includes(token, first + token.length);
36960
38141
  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)`);
38142
+ return fail(`Verification failed for "${stepName}": output does not contain "${token}" (token found only in task injection \u2014 agent must output it explicitly)`);
36962
38143
  }
36963
38144
  } else if (!output.includes(token)) {
36964
- throw new Error(`Verification failed for "${stepName}": output does not contain "${token}"`);
38145
+ return fail(`Verification failed for "${stepName}": output does not contain "${token}"`);
36965
38146
  }
36966
38147
  break;
36967
38148
  }
@@ -36969,12 +38150,34 @@ DO NOT:
36969
38150
  break;
36970
38151
  case "file_exists":
36971
38152
  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`);
38153
+ return fail(`Verification failed for "${stepName}": file "${check2.value}" does not exist`);
36973
38154
  }
36974
38155
  break;
36975
38156
  case "custom":
36976
- break;
36977
- }
38157
+ return { passed: false };
38158
+ }
38159
+ if (options?.completionMarkerFound === false) {
38160
+ this.log(`[${stepName}] Verification passed without legacy STEP_COMPLETE marker; allowing completion`);
38161
+ }
38162
+ const successMessage = options?.completionMarkerFound === false ? `Verification passed without legacy STEP_COMPLETE marker` : `Verification passed`;
38163
+ const observedAt = (/* @__PURE__ */ new Date()).toISOString();
38164
+ this.recordStepToolSideEffect(stepName, {
38165
+ type: "verification_observed",
38166
+ detail: successMessage,
38167
+ observedAt,
38168
+ raw: { passed: true, type: check2.type, value: check2.value }
38169
+ });
38170
+ this.getOrCreateStepEvidenceRecord(stepName).evidence.coordinationSignals.push({
38171
+ kind: "verification_passed",
38172
+ source: "verification",
38173
+ text: successMessage,
38174
+ observedAt,
38175
+ value: check2.value
38176
+ });
38177
+ return {
38178
+ passed: true,
38179
+ completionReason: "completed_verified"
38180
+ };
36978
38181
  }
36979
38182
  // ── State helpers ─────────────────────────────────────────────────────
36980
38183
  async updateRunStatus(runId, status, error48) {
@@ -36990,13 +38193,16 @@ DO NOT:
36990
38193
  }
36991
38194
  await this.db.updateRun(runId, patch);
36992
38195
  }
36993
- async markStepFailed(state, error48, runId, exitInfo) {
38196
+ async markStepFailed(state, error48, runId, exitInfo, completionReason) {
38197
+ this.captureStepTerminalEvidence(state.row.stepName, {}, exitInfo);
36994
38198
  state.row.status = "failed";
36995
38199
  state.row.error = error48;
38200
+ state.row.completionReason = completionReason;
36996
38201
  state.row.completedAt = (/* @__PURE__ */ new Date()).toISOString();
36997
38202
  await this.db.updateStep(state.row.id, {
36998
38203
  status: "failed",
36999
38204
  error: error48,
38205
+ completionReason,
37000
38206
  completedAt: state.row.completedAt,
37001
38207
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
37002
38208
  });
@@ -37008,6 +38214,7 @@ DO NOT:
37008
38214
  exitCode: exitInfo?.exitCode,
37009
38215
  exitSignal: exitInfo?.exitSignal
37010
38216
  });
38217
+ this.finalizeStepEvidence(state.row.stepName, "failed", state.row.completedAt, completionReason);
37011
38218
  }
37012
38219
  async markDownstreamSkipped(failedStepName, allSteps, stepStates, runId) {
37013
38220
  const queue = [failedStepName];
@@ -37114,9 +38321,23 @@ RELAY SETUP \u2014 do this FIRST before any other relay tool:
37114
38321
  "RELAY SETUP: First call register(name='<exact-agent-name>') before any other relay tool."`;
37115
38322
  }
37116
38323
  /** Post a message to the workflow channel. Fire-and-forget — never throws or blocks. */
37117
- postToChannel(text) {
38324
+ postToChannel(text, options = {}) {
37118
38325
  if (!this.relayApiKey || !this.channel)
37119
38326
  return;
38327
+ this.recordChannelEvidence(text, options);
38328
+ const stepName = options.stepName ?? this.inferStepNameFromChannelText(text);
38329
+ if (stepName) {
38330
+ this.recordStepToolSideEffect(stepName, {
38331
+ type: "post_channel_message",
38332
+ detail: text.slice(0, 240),
38333
+ raw: {
38334
+ actor: options.actor,
38335
+ role: options.role,
38336
+ target: options.target ?? this.channel,
38337
+ origin: options.origin ?? "runner_post"
38338
+ }
38339
+ });
38340
+ }
37120
38341
  this.ensureRelaycastRunnerAgent().then((agent) => agent.send(this.channel, text)).catch(() => {
37121
38342
  });
37122
38343
  }
@@ -37259,7 +38480,8 @@ ${excerpt}` : "";
37259
38480
  attempts: state.row.retryCount + 1,
37260
38481
  output: state.row.output,
37261
38482
  error: state.row.error,
37262
- verificationPassed: state.row.status === "completed" && stepsWithVerification.has(name)
38483
+ verificationPassed: state.row.status === "completed" && stepsWithVerification.has(name),
38484
+ completionMode: state.row.completionReason ? this.buildStepCompletionDecision(name, state.row.completionReason)?.mode : void 0
37263
38485
  });
37264
38486
  }
37265
38487
  return outcomes;
@@ -37367,16 +38589,22 @@ ${excerpt}` : "";
37367
38589
  }
37368
38590
  /** Persist step output to disk and post full output as a channel message. */
37369
38591
  async persistStepOutput(runId, stepName, output) {
38592
+ const outputPath = import_node_path8.default.join(this.getStepOutputDir(runId), `${stepName}.md`);
37370
38593
  try {
37371
38594
  const dir = this.getStepOutputDir(runId);
37372
38595
  (0, import_node_fs4.mkdirSync)(dir, { recursive: true });
37373
38596
  const cleaned = _WorkflowRunner.stripAnsi(output);
37374
- await (0, import_promises3.writeFile)(import_node_path8.default.join(dir, `${stepName}.md`), cleaned);
38597
+ await (0, import_promises3.writeFile)(outputPath, cleaned);
37375
38598
  } catch {
37376
38599
  }
38600
+ this.recordStepToolSideEffect(stepName, {
38601
+ type: "persist_step_output",
38602
+ detail: `Persisted step output to ${this.normalizeEvidencePath(outputPath)}`,
38603
+ raw: { path: outputPath }
38604
+ });
37377
38605
  const scrubbed = _WorkflowRunner.scrubForChannel(output);
37378
38606
  if (scrubbed.length === 0) {
37379
- this.postToChannel(`**[${stepName}]** Step completed \u2014 output written to disk`);
38607
+ this.postToChannel(`**[${stepName}]** Step completed \u2014 output written to disk`, { stepName });
37380
38608
  return;
37381
38609
  }
37382
38610
  const maxMsg = 2e3;
@@ -37384,7 +38612,7 @@ ${excerpt}` : "";
37384
38612
  this.postToChannel(`**[${stepName}] Output:**
37385
38613
  \`\`\`
37386
38614
  ${preview}
37387
- \`\`\``);
38615
+ \`\`\``, { stepName });
37388
38616
  }
37389
38617
  /** Load persisted step output from disk. */
37390
38618
  loadStepOutput(runId, stepName) {
@@ -39013,131 +40241,6 @@ var TemplateRegistry = class {
39013
40241
  }
39014
40242
  };
39015
40243
 
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
40244
  // packages/utils/dist/name-generator.js
39142
40245
  var ADJECTIVES = [
39143
40246
  "Blue",