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.
- package/bin/agent-relay-broker-darwin-arm64 +0 -0
- package/bin/agent-relay-broker-darwin-x64 +0 -0
- package/bin/agent-relay-broker-linux-arm64 +0 -0
- package/bin/agent-relay-broker-linux-x64 +0 -0
- package/dist/index.cjs +1307 -204
- package/dist/src/cli/relaycast-mcp.d.ts +4 -0
- package/dist/src/cli/relaycast-mcp.d.ts.map +1 -1
- package/dist/src/cli/relaycast-mcp.js +4 -4
- package/dist/src/cli/relaycast-mcp.js.map +1 -1
- package/package.json +8 -8
- package/packages/acp-bridge/package.json +2 -2
- package/packages/config/package.json +1 -1
- package/packages/hooks/package.json +4 -4
- package/packages/memory/package.json +2 -2
- package/packages/openclaw/package.json +2 -2
- package/packages/policy/package.json +2 -2
- package/packages/sdk/dist/__tests__/completion-pipeline.test.d.ts +14 -0
- package/packages/sdk/dist/__tests__/completion-pipeline.test.d.ts.map +1 -0
- package/packages/sdk/dist/__tests__/completion-pipeline.test.js +1476 -0
- package/packages/sdk/dist/__tests__/completion-pipeline.test.js.map +1 -0
- package/packages/sdk/dist/__tests__/e2e-owner-review.test.js +2 -2
- package/packages/sdk/dist/__tests__/e2e-owner-review.test.js.map +1 -1
- package/packages/sdk/dist/workflows/runner.d.ts +53 -2
- package/packages/sdk/dist/workflows/runner.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/runner.js +1269 -86
- package/packages/sdk/dist/workflows/runner.js.map +1 -1
- package/packages/sdk/dist/workflows/trajectory.d.ts +6 -2
- package/packages/sdk/dist/workflows/trajectory.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/trajectory.js +37 -2
- package/packages/sdk/dist/workflows/trajectory.js.map +1 -1
- package/packages/sdk/dist/workflows/types.d.ts +88 -0
- package/packages/sdk/dist/workflows/types.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/types.js.map +1 -1
- package/packages/sdk/package.json +2 -2
- package/packages/sdk/src/__tests__/completion-pipeline.test.ts +1820 -0
- package/packages/sdk/src/__tests__/e2e-owner-review.test.ts +2 -2
- package/packages/sdk/src/__tests__/idle-nudge.test.ts +68 -0
- package/packages/sdk/src/__tests__/workflow-runner.test.ts +113 -4
- package/packages/sdk/src/workflows/README.md +43 -11
- package/packages/sdk/src/workflows/runner.ts +1751 -94
- package/packages/sdk/src/workflows/schema.json +6 -0
- package/packages/sdk/src/workflows/trajectory.ts +52 -3
- package/packages/sdk/src/workflows/types.ts +149 -0
- package/packages/sdk-py/pyproject.toml +1 -1
- package/packages/telemetry/package.json +1 -1
- package/packages/trajectory/package.json +2 -2
- package/packages/user-directory/package.json +2 -2
- 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
|
-
|
|
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
|
-
|
|
35646
|
-
|
|
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
|
-
|
|
35726
|
-
|
|
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
|
|
35905
|
-
const
|
|
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.
|
|
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
|
-
|
|
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 (
|
|
36013
|
-
this.
|
|
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
|
-
-
|
|
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
|
|
36090
|
-
|
|
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:
|
|
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,
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
36319
|
-
|
|
36320
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
37840
|
+
const resolvedAgentName = agent.name;
|
|
37841
|
+
this.ptyListeners.set(resolvedAgentName, (chunk) => {
|
|
36748
37842
|
const stripped = _WorkflowRunner.stripAnsi(chunk);
|
|
36749
|
-
this.ptyOutputBuffers.get(
|
|
37843
|
+
this.ptyOutputBuffers.get(resolvedAgentName)?.push(stripped);
|
|
36750
37844
|
newLogStream.write(chunk);
|
|
36751
|
-
options.onChunk?.({ agentName:
|
|
37845
|
+
options.onChunk?.({ agentName: resolvedAgentName, chunk });
|
|
36752
37846
|
});
|
|
36753
37847
|
}
|
|
36754
37848
|
agentName = agent.name;
|
|
36755
37849
|
}
|
|
36756
|
-
|
|
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(
|
|
36766
|
-
console.warn(`[WorkflowRunner] Failed to register ${
|
|
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.
|
|
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
|
-
|
|
36784
|
-
|
|
36785
|
-
|
|
36786
|
-
|
|
36787
|
-
|
|
36788
|
-
|
|
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
|
-
|
|
37895
|
+
timeoutRecovered = true;
|
|
36791
37896
|
}
|
|
36792
|
-
}
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
38153
|
+
return fail(`Verification failed for "${stepName}": file "${check2.value}" does not exist`);
|
|
36973
38154
|
}
|
|
36974
38155
|
break;
|
|
36975
38156
|
case "custom":
|
|
36976
|
-
|
|
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)(
|
|
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",
|