Bifrost/tools/agent-transport.mjs
Meta Vens 868df35449 Bifrost suite: load @gamecult/cultcache-ts; skip Rust fixture tests with a stated reason
CultLib 8cb3b728 renamed only cultcache-ts to @gamecult/cultcache-ts; the
suite and three tools still required the bare name. Rename the require
string in tests/persona-feedback-cli, persona-discord-delivery,
persona-discord-crossing-rust-smoke, tools/persona-feedback (:114),
agent-transport and governance-threads. No fallback: a pre-8cb3b728 CultLib
fails loudly at import. operator-notification.mjs is untouched (retire cut).

The two Rust fixture tests skip, naming the missing producer (Epiphany
removed both fixture bins in 387afe49), and drop the hard-coded
CARGO_TARGET_DIR override. Builds.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 17:16:39 +02:00

895 lines
29 KiB
JavaScript

#!/usr/bin/env node
import { spawnSync } from "node:child_process";
import { createRequire } from "node:module";
import { randomUUID } from "node:crypto";
import { existsSync, readFileSync } from "node:fs";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import {
commandIdFromOptions,
crossingProvenanceFromOptions,
writeCrossingReceipt,
} from "./bifrost-crossing-documents.mjs";
const scriptDir = dirname(fileURLToPath(import.meta.url));
const repoRoot = resolve(scriptDir, "..");
const projectsRoot = resolve(repoRoot, "..");
const defaultStorePath = resolve(repoRoot, ".bifrost", "agent-transport.cc");
const bridgeCli = resolve(repoRoot, "tools", "bifrost-bridge.mjs");
const defaultPersonaName = "Bifrost";
const defaultPersonaAvatarUrl =
"https://raw.githubusercontent.com/GameCult/Bifrost/main/src/Bifrost.Web/wwwroot/img/bifrost-profile.png";
const cultCachePackagePath = resolveFirstExisting("CultCache TypeScript runtime", [
resolve(projectsRoot, "CultLib", "packages", "cultcache-ts", "package.json"),
resolve(projectsRoot, "CultCacheTS", "package.json"),
resolve(projectsRoot, "CultCacheTS", "node_modules", "cultcache-ts", "package.json"),
]);
const cultNetPackagePath = resolveFirstExisting("CultNet TypeScript runtime", [
resolve(projectsRoot, "CultLib", "packages", "cultnet-ts", "package.json"),
resolve(projectsRoot, "CultNetTS", "package.json"),
]);
const cultCacheRequire = createRequire(cultCachePackagePath);
const cultNetRequire = createRequire(cultNetPackagePath);
const {
CultCache,
SingleFileMessagePackBackingStore,
defineDocumentType,
} = cultCacheRequire("@gamecult/cultcache-ts");
const {
CultNetDocumentRegistry,
defineCultNetDocumentBinding,
} = cultNetRequire("cultnet-ts");
const { encode, decode } = cultNetRequire("@msgpack/msgpack");
function resolveFirstExisting(label, candidates) {
for (const candidate of candidates) {
if (existsSync(candidate)) {
return candidate;
}
}
throw new Error(`${label} is unavailable. Tried: ${candidates.join(", ")}`);
}
const documentType = "bifrost.agent-transport.update-request";
const schemaId = "bifrost.agent-transport.update-request.v0";
const validStatuses = new Set(["queued", "claimed", "completed", "cancelled"]);
const validCloseStatuses = new Set(["completed", "cancelled"]);
const updateRequestDefinition = defineDocumentType({
type: documentType,
schemaId,
schemaName: documentType,
schemaVersion: "bifrost.agent_transport.update_request.v0",
schema: {
parse: parseUpdateRequest,
},
name: "id",
indexes: {
targetRepoName: "targetRepoName",
targetAgentIdentity: "targetAgentIdentity",
status: "status",
},
members: [
{ slot: 1, memberName: "id", typeName: "string", isName: true },
{ slot: 2, memberName: "targetRepoName", typeName: "string", indexAlias: "targetRepoName" },
{ slot: 3, memberName: "targetRepositoryFullName", typeName: "string" },
{ slot: 4, memberName: "targetAgentIdentity", typeName: "string", indexAlias: "targetAgentIdentity" },
{ slot: 5, memberName: "title", typeName: "string" },
{ slot: 6, memberName: "requestMarkdown", typeName: "string" },
{ slot: 7, memberName: "priority", typeName: "number" },
{ slot: 8, memberName: "status", typeName: "string", indexAlias: "status" },
{ slot: 9, memberName: "sourceKind", typeName: "string" },
{ slot: 10, memberName: "sourceChannelId", typeName: "string" },
{ slot: 11, memberName: "sourceMessageIds", typeName: "string", isMany: true },
{ slot: 12, memberName: "sourcePacketPath", typeName: "string" },
{ slot: 13, memberName: "sourcePromptPath", typeName: "string" },
{ slot: 14, memberName: "createdByAgent", typeName: "string" },
{ slot: 15, memberName: "claimedByAgent", typeName: "string" },
{ slot: 16, memberName: "closeNote", typeName: "string" },
{ slot: 17, memberName: "createdAt", typeName: "string" },
{ slot: 18, memberName: "updatedAt", typeName: "string" },
{ slot: 19, memberName: "claimedAt", typeName: "string" },
{ slot: 20, memberName: "closedAt", typeName: "string" },
],
});
const cultNetRegistry = new CultNetDocumentRegistry([
defineCultNetDocumentBinding({
definition: updateRequestDefinition,
payloadSchemaVersion: "bifrost.agent_transport.update_request.v0",
}),
]);
async function main() {
loadBifrostLocalEnv();
const [command, ...rawArgs] = process.argv.slice(2);
const options = parseArgs(rawArgs);
if (!command || command === "help" || command === "--help" || command === "-h") {
printHelp();
return;
}
if (isMutatingCommand(command)) {
ensureTransportReceiptGate(options);
}
const storePath = resolveOptionPath(options.store ?? defaultStorePath);
const cache = await openCache(storePath);
switch (command) {
case "enqueue":
await enqueue(cache, options);
return;
case "list":
listRequests(cache, options);
return;
case "claim":
await claim(cache, options);
return;
case "release":
await releaseRequest(cache, options);
return;
case "close":
await closeRequest(cache, options);
return;
case "snapshot":
await writeSnapshot(cache, options);
return;
case "apply-snapshot":
await applySnapshot(cache, options);
return;
case "schema":
printJson({ documentType, schemaId });
return;
default:
throw new Error(`Unknown command "${command}". Run "node tools/agent-transport.mjs help".`);
}
}
function loadBifrostLocalEnv() {
if (process.env.BIFROST_SKIP_LOCAL_ENV === "true") {
return;
}
loadLocalEnv(resolve(repoRoot, ".env"));
loadLocalEnv(resolve(projectsRoot, "VoidBot", ".env"));
}
function isMutatingCommand(command) {
return command === "enqueue"
|| command === "claim"
|| command === "release"
|| command === "close"
|| command === "apply-snapshot";
}
function ensureTransportReceiptGate(options) {
if (wantsUnreceiptedActivity(options)) {
throw new Error(
"Bifrost request-lane mutations cannot use --allow-unreceipted-activity or BIFROST_ALLOW_UNRECEIPTED_ACTIVITY. " +
"Use the typed Bifrost CultCache/CultMesh request store instead of mutating off-book.",
);
}
if (hasRemovedHttpBridgeConfig()) {
throw new Error(
"BIFROST_BRIDGE_BASE_URL / BIFROST_BRIDGE_TOKEN were removed. " +
"Bifrost request-lane receipts are written to the typed CultCache/CultMesh store.",
);
}
}
function hasRemovedHttpBridgeConfig() {
return Boolean(optionalString(process.env.BIFROST_BRIDGE_BASE_URL) || optionalString(process.env.BIFROST_BRIDGE_TOKEN));
}
function wantsUnreceiptedActivity(options) {
return options["allow-unreceipted-activity"] === "true" || process.env.BIFROST_ALLOW_UNRECEIPTED_ACTIVITY === "true";
}
async function openCache(storePath) {
const cache = CultCache.builder()
.withDocumentType(updateRequestDefinition)
.withGenericStore(new SingleFileMessagePackBackingStore(storePath))
.build();
await cache.pullAllBackingStores();
return cache;
}
async function enqueue(cache, options) {
const now = new Date().toISOString();
const requestMarkdown = await readRequestMarkdown(options);
const request = parseUpdateRequest({
id: options.id ?? `req_${randomUUID()}`,
targetRepoName: requireOption(options, "repo"),
targetRepositoryFullName: optionalString(options["repo-full-name"]),
targetAgentIdentity: optionalString(options.agent),
title: requireOption(options, "title"),
requestMarkdown,
priority: parseInteger(options.priority ?? "50", "priority"),
status: "queued",
sourceKind: optionalString(options["source-kind"]) ?? "manual",
sourceChannelId: optionalString(options["source-channel-id"]),
sourceMessageIds: parseCsv(options["source-message-ids"]),
sourcePacketPath: optionalString(options["packet-path"]),
sourcePromptPath: optionalString(options["prompt-path"]),
createdByAgent: optionalString(options["created-by"]),
claimedByAgent: undefined,
closeNote: undefined,
createdAt: now,
updatedAt: now,
claimedAt: undefined,
closedAt: undefined,
});
await mirrorEnqueuedRequestOrThrow(request, options);
await cache.put(updateRequestDefinition, request.id, request);
await recordTransportReceiptOrThrow(request, {
activityKind: "Queued",
status: request.status,
actorName: request.createdByAgent ?? "system",
note: "Update request queued.",
}, options);
printJson(request);
}
async function mirrorEnqueuedRequestOrThrow(request, options) {
const channelId = resolveMirrorChannelId(options);
if (!channelId) {
if (allowsUnmirrored(options)) {
return;
}
throw new Error(
"Bifrost update requests require a Discord mirror. Set BIFROST_DISCORD_CHANNEL_ID, pass --mirror-channel-id, or use --allow-unmirrored true only for explicit fixtures.",
);
}
const mirrorContent =
await readOptionalTextOption(options, "mirror-content") ??
renderRequestMirrorFallback(request);
const personaName = optionalString(options["mirror-persona-name"])
?? optionalString(process.env.BIFROST_DISCORD_PERSONA_NAME)
?? defaultPersonaName;
const personaAvatarUrl =
optionalString(options["mirror-persona-avatar-url"]) ??
optionalString(process.env.BIFROST_DISCORD_PERSONA_AVATAR_URL) ??
optionalString(process.env.DISCORD_PERSONA_AVATAR_URL_BIFROST) ??
defaultPersonaAvatarUrl;
runNodeJson([
bridgeCli,
"discord-post",
"--channel-id", channelId,
"--content", mirrorContent,
"--persona-name", personaName,
"--source-kind", documentType,
"--source-id", request.id,
"--authority-ref", "bifrost_agent_transport_enqueue",
...optionalArg("--target-repository-full-name", request.targetRepositoryFullName ?? request.targetRepoName),
...optionalArg("--persona-avatar-url", personaAvatarUrl),
...optionalArg("--reply-to-message-id", options["mirror-reply-to-message-id"]),
...bridgeRecoveryArgs(options),
...(options["mirror-dry-run"] === "true" ? ["--dry-run", "true"] : []),
], repoRoot);
}
function resolveMirrorChannelId(options) {
return (
optionalString(options["mirror-channel-id"]) ??
optionalString(process.env.BIFROST_DISCORD_CHANNEL_ID) ??
optionalString(process.env.DISCORD_BIFROST_CHANNEL_ID)
);
}
function allowsUnmirrored(options) {
return options["allow-unmirrored"] === "true" || process.env.BIFROST_ALLOW_UNMIRRORED_GOVERNANCE === "true";
}
function bridgeRecoveryArgs(options) {
return [];
}
function renderRequestMirrorFallback(request) {
const actor = request.createdByAgent ? `${request.createdByAgent} queued` : "Queued";
return [
`Bifrost update request queued`,
"",
`**${request.targetRepoName}: ${request.title}**`,
`Request: \`${request.id}\``,
`Target: ${request.targetAgentIdentity ? `${request.targetAgentIdentity} / ` : ""}${request.targetRepoName}`,
`Status: queued`,
`Codex job: no - this request is waiting for the dispatcher`,
"",
`${actor} this work request.`,
"",
summarizeRequestMarkdown(request.requestMarkdown),
"",
`Next: a separate \`Bifrost Codex dispatch started\` receipt means a Codex turn actually began.`,
].join("\n");
}
function summarizeRequestMarkdown(markdown) {
const text = String(markdown);
const requestSection = text.match(/## Request\s+([\s\S]*?)(?:\n## |\n```|$)/i)?.[1]?.trim();
const firstUsefulBlock = (requestSection ?? text)
.split(/\n\s*\n/)
.map((block) => block.replace(/\s+/g, " ").trim())
.find((block) => block && !block.startsWith("#"));
return truncate(firstUsefulBlock ?? "A Bifrost update request was queued for Codex dispatch.", 360);
}
function truncate(value, maxLength) {
return value.length > maxLength ? `${value.slice(0, maxLength - 3)}...` : value;
}
function listRequests(cache, options) {
const requests = filteredRequests(cache, options)
.sort(compareRequests);
printJson(requests);
}
async function claim(cache, options) {
const repo = optionalString(options.repo);
if (!repo) {
throw new Error("claim requires --repo so an agent cannot grab work outside its jurisdiction.");
}
const agent = optionalString(options.agent);
const claimedBy = optionalString(options["claimed-by"]) ?? agent ?? "codex";
const request = filteredRequests(cache, {
...options,
status: "queued",
})
.filter((candidate) => equalsIgnoreCase(candidate.targetRepoName, repo))
.filter((candidate) => !agent || equalsIgnoreCase(candidate.targetAgentIdentity, agent) || !candidate.targetAgentIdentity)
.sort(compareRequests)[0];
if (!request) {
printJson(null);
return;
}
const now = new Date().toISOString();
const claimed = parseUpdateRequest({
...request,
status: "claimed",
claimedByAgent: claimedBy,
claimedAt: now,
updatedAt: now,
});
await cache.put(updateRequestDefinition, claimed.id, claimed);
await recordTransportReceiptOrThrow(claimed, {
activityKind: "Claimed",
status: claimed.status,
actorName: claimed.claimedByAgent ?? claimed.targetAgentIdentity ?? "codex",
note: `Request claimed for ${claimed.targetRepoName}.`,
}, options);
printJson(claimed);
}
async function closeRequest(cache, options) {
const id = requireOption(options, "id");
const status = requireOption(options, "status");
if (!validCloseStatuses.has(status)) {
throw new Error("--status for close must be completed or cancelled.");
}
const current = cache.get(updateRequestDefinition, id);
if (!current) {
throw new Error(`No update request found for id "${id}".`);
}
const now = new Date().toISOString();
const closed = parseUpdateRequest({
...current,
status,
closeNote: optionalString(options.note),
closedAt: now,
updatedAt: now,
});
await cache.put(updateRequestDefinition, closed.id, closed);
await recordTransportReceiptOrThrow(closed, {
activityKind: "Closed",
status: closed.status,
actorName: current.claimedByAgent ?? current.targetAgentIdentity ?? "system",
note: closed.closeNote || `Request closed as ${closed.status}.`,
}, options);
printJson(closed);
}
async function releaseRequest(cache, options) {
const id = requireOption(options, "id");
const current = cache.get(updateRequestDefinition, id);
if (!current) {
throw new Error(`No update request found for id "${id}".`);
}
if (current.status !== "claimed") {
throw new Error(`Only claimed requests can be released; "${id}" is ${current.status}.`);
}
const now = new Date().toISOString();
const released = parseUpdateRequest({
...current,
status: "queued",
claimedByAgent: undefined,
claimedAt: undefined,
closeNote: optionalString(options.note),
updatedAt: now,
});
await cache.put(updateRequestDefinition, released.id, released);
await recordTransportReceiptOrThrow(released, {
activityKind: "Released",
status: released.status,
actorName: current.claimedByAgent ?? current.targetAgentIdentity ?? "system",
note: released.closeNote || "Claim released back to the queue.",
}, options);
printJson(released);
}
async function writeSnapshot(cache, options) {
const message = cultNetRegistry.createRawSnapshotResponse(
cache,
options["message-id"] ?? `snapshot_${randomUUID()}`,
);
if (options.out) {
const outPath = resolveOptionPath(options.out);
await mkdir(dirname(outPath), { recursive: true });
await writeFile(outPath, encode(message));
printJson({
schemaVersion: message.schemaVersion,
messageId: message.messageId,
documentCount: message.documents.length,
out: outPath,
});
return;
}
printJson({
...message,
documents: message.documents.map((document) => ({
...document,
payload: Buffer.from(document.payload).toString("base64url"),
payloadEncoding: "messagepack+base64url",
})),
});
}
async function applySnapshot(cache, options) {
const inPath = resolveOptionPath(requireOption(options, "in"));
const beforeRequests = mapRequestsById(cache.getAll(updateRequestDefinition));
const message = decode(await readFile(inPath));
await cultNetRegistry.applyRawSnapshotResponse(cache, message);
const afterRequests = mapRequestsById(cache.getAll(updateRequestDefinition));
const receipts = deriveSnapshotReceipts(beforeRequests, afterRequests);
try {
for (const { request, receipt } of receipts) {
await recordTransportReceiptOrThrow(request, receipt, options);
}
} catch (error) {
await restoreRequests(cache, beforeRequests, afterRequests);
throw error;
}
printJson({
applied: true,
schemaVersion: message.schemaVersion,
messageId: message.messageId,
documentCount: message.documents?.length ?? 0,
receiptCount: receipts.length,
in: inPath,
});
}
function mapRequestsById(requests) {
return new Map(requests.map((request) => [request.id, request]));
}
function deriveSnapshotReceipts(beforeRequests, afterRequests) {
const receipts = [];
for (const [id, current] of afterRequests.entries()) {
const previous = beforeRequests.get(id);
if (previous && stableJson(previous) === stableJson(current)) {
continue;
}
receipts.push({
request: current,
receipt: buildSnapshotReceipt(previous, current),
});
}
return receipts;
}
function buildSnapshotReceipt(previous, current) {
if (!previous) {
return {
activityKind: activityKindForSnapshotState(current.status),
status: current.status,
actorName: receiptActorName(current),
note: `Snapshot import created request state as ${current.status}.`,
};
}
if (previous.status === "claimed" && current.status === "queued") {
return {
activityKind: "Released",
status: current.status,
actorName: previous.claimedByAgent ?? receiptActorName(current),
note: "Snapshot import released a claimed request back to the queue.",
};
}
if (previous.status !== "claimed" && current.status === "claimed") {
return {
activityKind: "Claimed",
status: current.status,
actorName: current.claimedByAgent ?? receiptActorName(current),
note: `Snapshot import claimed request for ${current.targetRepoName}.`,
};
}
if (current.status === "completed" || current.status === "cancelled") {
return {
activityKind: "Closed",
status: current.status,
actorName: current.claimedByAgent ?? previous.claimedByAgent ?? receiptActorName(current),
note: current.closeNote || `Snapshot import closed request as ${current.status}.`,
};
}
return {
activityKind: activityKindForSnapshotState(current.status),
status: current.status,
actorName: receiptActorName(current),
note: "Snapshot import refreshed request state.",
};
}
function activityKindForSnapshotState(status) {
return status === "claimed" ? "Claimed" : "Queued";
}
function receiptActorName(request) {
return request.claimedByAgent ?? request.createdByAgent ?? request.targetAgentIdentity ?? "system";
}
async function restoreRequests(cache, beforeRequests, afterRequests) {
for (const [id, request] of beforeRequests.entries()) {
await cache.put(updateRequestDefinition, id, request);
}
for (const id of afterRequests.keys()) {
if (!beforeRequests.has(id)) {
await cache.delete(updateRequestDefinition, id);
}
}
}
function stableJson(value) {
return JSON.stringify(value);
}
async function recordTransportReceiptOrThrow(request, receipt, options = {}) {
const commandId = commandIdFromOptions(options) || `agent-transport:${request.id}:${receipt.activityKind}`;
const provenance = crossingProvenanceFromOptions({
...options,
"cultmesh-command-id": commandId,
"source-kind": request.sourceKind || "bifrost.agent-transport.update-request",
"source-id": request.id,
"request-id": request.id,
"authority-ref": "bifrost_agent_transport",
identity: receipt.actorName,
});
const now = new Date().toISOString();
await writeCrossingReceipt({
receiptId: `crossing_${slugifyReceiptPart(commandId)}_${slugifyReceiptPart(receipt.activityKind)}`,
commandId,
crossingKind: "agent_transport.request",
status: request.status === "cancelled" ? "cancelled" : "completed",
ok: true,
requestedAt: request.createdAt || now,
startedAt: request.updatedAt || now,
completedAt: now,
actor: {
kind: "agent",
name: receipt.actorName || "system",
bifrostIdentity: receipt.actorName || "system",
},
source: provenance.source,
authority: provenance.authority,
epiphany: provenance.epiphany,
target: {
surface: "agent-transport",
locator: request.id,
repositoryFullName: request.targetRepositoryFullName || request.targetRepoName,
requestId: request.id,
status: request.status,
},
externalReceipt: {
id: request.id,
transport: "cultcache",
url: "",
},
surfaceReceipt: {
activityKind: receipt.activityKind,
note: receipt.note || "",
requestStatus: receipt.status,
},
}, options);
}
function filteredRequests(cache, options) {
return cache.getAll(updateRequestDefinition)
.filter((request) => !options.status || request.status === options.status)
.filter((request) => !options.repo || equalsIgnoreCase(request.targetRepoName, options.repo))
.filter((request) => !options.agent || equalsIgnoreCase(request.targetAgentIdentity, options.agent));
}
function compareRequests(left, right) {
if (right.priority !== left.priority) {
return right.priority - left.priority;
}
return left.createdAt.localeCompare(right.createdAt);
}
async function readRequestMarkdown(options) {
if (options["request-file"]) {
return readFile(resolveOptionPath(options["request-file"]), "utf8");
}
return requireOption(options, "request");
}
async function readOptionalTextOption(options, name) {
const file = optionalString(options[`${name}-file`]);
if (file) {
return readFile(resolveOptionPath(file), "utf8");
}
return optionalString(options[name]);
}
function parseUpdateRequest(input) {
if (!input || typeof input !== "object") {
throw new Error("Update request must be an object.");
}
const request = {
id: requireString(input.id, "id"),
targetRepoName: requireString(input.targetRepoName, "targetRepoName"),
targetRepositoryFullName: optionalString(input.targetRepositoryFullName),
targetAgentIdentity: optionalString(input.targetAgentIdentity),
title: requireString(input.title, "title"),
requestMarkdown: requireString(input.requestMarkdown, "requestMarkdown"),
priority: requireNumber(input.priority, "priority"),
status: requireString(input.status, "status"),
sourceKind: optionalString(input.sourceKind) ?? "manual",
sourceChannelId: optionalString(input.sourceChannelId),
sourceMessageIds: normalizeStringArray(input.sourceMessageIds, "sourceMessageIds"),
sourcePacketPath: optionalString(input.sourcePacketPath),
sourcePromptPath: optionalString(input.sourcePromptPath),
createdByAgent: optionalString(input.createdByAgent),
claimedByAgent: optionalString(input.claimedByAgent),
closeNote: optionalString(input.closeNote),
createdAt: requireString(input.createdAt, "createdAt"),
updatedAt: requireString(input.updatedAt, "updatedAt"),
claimedAt: optionalString(input.claimedAt),
closedAt: optionalString(input.closedAt),
};
if (!validStatuses.has(request.status)) {
throw new Error(`Update request status "${request.status}" is not valid.`);
}
return request;
}
function parseArgs(args) {
const options = {};
for (let index = 0; index < args.length; index += 1) {
const token = args[index];
if (!token.startsWith("--")) {
throw new Error(`Unexpected argument "${token}".`);
}
const key = token.slice(2);
const next = args[index + 1];
if (!next || next.startsWith("--")) {
options[key] = "true";
continue;
}
options[key] = next;
index += 1;
}
return options;
}
function requireOption(options, name) {
const value = options[name];
if (typeof value !== "string" || value.trim().length === 0) {
throw new Error(`Missing required option --${name}.`);
}
return value.trim();
}
function optionalString(value) {
if (typeof value !== "string") {
return undefined;
}
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : undefined;
}
function optionalArg(flag, value) {
const normalized = optionalString(value);
return normalized ? [flag, normalized] : [];
}
function requireString(value, field) {
const normalized = optionalString(value);
if (!normalized) {
throw new Error(`Update request field "${field}" must be a non-empty string.`);
}
return normalized;
}
function requireNumber(value, field) {
const number = typeof value === "number" ? value : Number(value);
if (!Number.isFinite(number)) {
throw new Error(`Update request field "${field}" must be a finite number.`);
}
return number;
}
function parseInteger(value, field) {
const number = Number(value);
if (!Number.isInteger(number)) {
throw new Error(`--${field} must be an integer.`);
}
return number;
}
function parseCsv(value) {
if (typeof value !== "string" || value.trim().length === 0) {
return [];
}
return value
.split(",")
.map((item) => item.trim())
.filter((item) => item.length > 0);
}
function normalizeStringArray(value, field) {
if (value === undefined || value === null) {
return [];
}
if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
throw new Error(`Update request field "${field}" must be an array of strings.`);
}
return value;
}
function equalsIgnoreCase(left, right) {
return typeof left === "string"
&& typeof right === "string"
&& left.toLowerCase() === right.toLowerCase();
}
function slugifyReceiptPart(value) {
return String(value)
.trim()
.toLowerCase()
.replace(/[^a-z0-9._:-]+/g, "-")
.replace(/^-+|-+$/g, "")
.slice(0, 120) || "receipt";
}
function resolveOptionPath(path) {
return resolve(process.cwd(), path);
}
function runNodeJson(args, cwd) {
const result = spawnSync(process.execPath, args, {
cwd,
encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"],
windowsHide: true,
timeout: 30000,
});
if (result.status !== 0) {
const reason = result.error?.message ?? result.stderr ?? result.stdout;
throw new Error(`node ${args.join(" ")} failed with ${result.status ?? "unknown"}:\n${reason}`);
}
return JSON.parse(result.stdout);
}
function loadLocalEnv(path) {
if (!existsSync(path)) {
return;
}
for (const line of readFileSync(path, "utf8").split(/\r?\n/)) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#")) {
continue;
}
const separator = trimmed.indexOf("=");
if (separator === -1) {
continue;
}
const key = trimmed.slice(0, separator).trim();
if (process.env[key]) {
continue;
}
let value = trimmed.slice(separator + 1).trim();
if (
value.length >= 2 &&
((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'")))
) {
value = value.slice(1, -1);
}
process.env[key] = value;
}
}
function printJson(value) {
process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
}
function printHelp() {
process.stdout.write(`Bifrost agent transport
Commands:
enqueue Store a queued update request in CultCache
list List requests, optionally filtered by --repo, --agent, --status
claim Claim the highest-priority queued request for --repo
close Mark a request completed or cancelled
release Return a claimed request to the queue after dispatch setup fails
snapshot Write or print a CultNet raw snapshot response
apply-snapshot Apply a CultNet raw snapshot response from --in
schema Print the document type and schema id
Common options:
--store <path> Override the .cc store path
Mirror options for enqueue:
--mirror-channel-id <id> Mirror queued requests to Discord; defaults to BIFROST_DISCORD_CHANNEL_ID
--mirror-persona-name <name> Render the mirror through this persona; defaults to Bifrost
--mirror-persona-avatar-url <url> Optional persona avatar for the mirror
--mirror-content <text> Optional custom mirror text
--mirror-content-file <path> Read custom mirror text from a file
--mirror-dry-run true Exercise mirror plumbing without posting to Discord
--allow-unmirrored true Fixture/debug escape hatch; production writes should not use this
Examples:
node tools/agent-transport.mjs enqueue --repo AetheriaLore --agent nibu --title "Wavecrafters" --request-file packet.md --priority 80
node tools/agent-transport.mjs claim --repo AetheriaLore --agent nibu --claimed-by nibu
node tools/agent-transport.mjs snapshot --out transport.msgpack
`);
}
main().catch((error) => {
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
process.exitCode = 1;
});