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