Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,8 @@ The `graph-execution` preset treats `TaskGraph` as the concrete execute-phase ru

Phase presets serialize as a versioned `schemaVersion: 1` catalog. Legacy arrays migrate explicitly; unsupported versions, malformed catalogs, and gate evaluation with missing top-level required evidence refs or artifact ids fail closed.

Agent execution stays adapter-neutral. The `AgentAdapterContract` describes required launch/send/capture/health/readiness/report/substrate behavior for Pi, Codex, and Claude Code compatible workers without coupling the domain layer to any one CLI. Readiness requires a nonce-equivalent proof, worker reports require `taskId`, `status`, `evidenceRefs`, and `summary`, and health can be supported or best-effort but not absent.

The domain boundary is explicit: `Decomposer` creates the concrete graph, `Scheduler` computes ready tasks, `WorkerRuntime` dispatches and heartbeats work, `Verifier` validates evidence refs, and `GateEngine` decides transitions.

## Workflow Map
Expand Down
96 changes: 96 additions & 0 deletions src/domain/agent-adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
export type AgentCapability = "interactive-shell" | "workspace" | "process" | "heartbeat" | "structured-report";
export type AdapterOperationSupport = "supported" | "best-effort" | "unsupported";
export type ExecutionSubstrateKind = "tmux" | "worktree" | "process";
export type WorkerReportFormat = "json" | "markdown" | "text";

export interface AgentAdapterContract {
id: string;
displayName: string;
capabilities: AgentCapability[];
launch: AdapterOperationSupport;
send: AdapterOperationSupport;
capture: AdapterOperationSupport;
interrupt: AdapterOperationSupport;
health: AdapterOperationSupport;
readiness: {
requiresNonce: boolean;
timeoutSeconds: number;
};
report: {
format: WorkerReportFormat;
requiredFields: string[];
};
substrates: ExecutionSubstrateKind[];
compatibilityNotes: string[];
}

export interface AgentAdapterValidationResult {
ok: boolean;
issues: string[];
}

export function validateAgentAdapterContract(contract: AgentAdapterContract): AgentAdapterValidationResult {
const issues: string[] = [];
if (!contract.id.trim()) issues.push("adapter id is required");
if (!contract.displayName.trim()) issues.push("adapter displayName is required");
if (contract.capabilities.length === 0) issues.push("adapter requires at least one capability");
if (contract.substrates.length === 0) issues.push("adapter requires at least one execution substrate");
if (contract.launch === "unsupported" && contract.send === "unsupported") issues.push("adapter must support launch or send");
if (contract.capture === "unsupported" && contract.report.format !== "json") issues.push("adapter without capture must provide JSON reports");
if (contract.health === "unsupported") issues.push("adapter health behavior must be supported or best-effort");
if (contract.readiness.timeoutSeconds <= 0 || !Number.isFinite(contract.readiness.timeoutSeconds)) issues.push("readiness timeout must be positive");
if (!contract.readiness.requiresNonce) issues.push("readiness must require a nonce or equivalent proof");
for (const field of ["taskId", "status", "evidenceRefs", "summary"]) {
if (!contract.report.requiredFields.includes(field)) issues.push(`worker report requires ${field}`);
}
const compatibilityText = [contract.id, contract.displayName, ...contract.compatibilityNotes].join("\n");
if (!/codex/i.test(compatibilityText)) issues.push("Codex compatibility note is required");
if (!/\bpi\b/i.test(compatibilityText)) issues.push("Pi compatibility note is required");
if (!/claude/i.test(compatibilityText)) issues.push("Claude Code compatibility note is required");
return { ok: issues.length === 0, issues };
}

export const DEFAULT_AGENT_ADAPTER_MATRIX: readonly AgentAdapterContract[] = [
{
id: "codex",
displayName: "Codex CLI",
capabilities: ["interactive-shell", "workspace", "process", "heartbeat", "structured-report"],
launch: "supported",
send: "supported",
capture: "supported",
interrupt: "best-effort",
health: "best-effort",
readiness: { requiresNonce: true, timeoutSeconds: 60 },
report: { format: "json", requiredFields: ["taskId", "status", "evidenceRefs", "summary"] },
substrates: ["tmux", "worktree", "process"],
compatibilityNotes: ["Codex can run through tmux/process substrate with workspace isolation.", "Pi compatibility uses the same structured report fields.", "Claude Code compatibility uses the same readiness nonce contract."],
},
{
id: "pi",
displayName: "Pi",
capabilities: ["interactive-shell", "workspace", "heartbeat", "structured-report"],
launch: "supported",
send: "supported",
capture: "supported",
interrupt: "best-effort",
health: "supported",
readiness: { requiresNonce: true, timeoutSeconds: 60 },
report: { format: "json", requiredFields: ["taskId", "status", "evidenceRefs", "summary"] },
substrates: ["tmux", "worktree", "process"],
compatibilityNotes: ["Pi is the native interactive shell substrate.", "Codex-compatible runs use the same launch/send/capture model.", "Claude Code can share the process/worktree substrate."],
},
{
id: "claude-code",
displayName: "Claude Code",
capabilities: ["interactive-shell", "workspace", "process", "heartbeat", "structured-report"],
launch: "supported",
send: "supported",
capture: "supported",
interrupt: "best-effort",
health: "best-effort",
readiness: { requiresNonce: true, timeoutSeconds: 60 },
report: { format: "json", requiredFields: ["taskId", "status", "evidenceRefs", "summary"] },
substrates: ["tmux", "worktree", "process"],
compatibilityNotes: ["Claude Code can run as a process-backed worker.", "Pi compatibility uses the shared substrate contract.", "Codex compatibility uses the same structured report fields."],
},
];
47 changes: 47 additions & 0 deletions test/agent-adapter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import * as assert from "node:assert/strict";
import { test } from "node:test";
import { DEFAULT_AGENT_ADAPTER_MATRIX, validateAgentAdapterContract } from "../src/domain/agent-adapter.js";

test("default agent adapter matrix captures Codex, Pi, and Claude Code substrate contracts", () => {
assert.deepEqual(
DEFAULT_AGENT_ADAPTER_MATRIX.map((adapter) => adapter.id),
["codex", "pi", "claude-code"],
);

for (const adapter of DEFAULT_AGENT_ADAPTER_MATRIX) {
assert.deepEqual(validateAgentAdapterContract(adapter), { ok: true, issues: [] });
assert.equal(adapter.readiness.requiresNonce, true);
assert.ok(adapter.report.requiredFields.includes("taskId"));
assert.ok(adapter.report.requiredFields.includes("status"));
assert.ok(adapter.report.requiredFields.includes("evidenceRefs"));
assert.ok(adapter.report.requiredFields.includes("summary"));
}
});

test("agent adapter validation fails closed for missing readiness, health, report, and compatibility guarantees", () => {
const result = validateAgentAdapterContract({
id: "bad",
displayName: "Bad",
capabilities: [],
launch: "unsupported",
send: "unsupported",
capture: "unsupported",
interrupt: "unsupported",
health: "unsupported",
readiness: { requiresNonce: false, timeoutSeconds: 0 },
report: { format: "text", requiredFields: [] },
substrates: [],
compatibilityNotes: [],
});

assert.equal(result.ok, false);
assert.ok(result.issues.includes("adapter requires at least one capability"));
assert.ok(result.issues.includes("adapter requires at least one execution substrate"));
assert.ok(result.issues.includes("adapter must support launch or send"));
assert.ok(result.issues.includes("adapter health behavior must be supported or best-effort"));
assert.ok(result.issues.includes("readiness must require a nonce or equivalent proof"));
assert.ok(result.issues.includes("worker report requires evidenceRefs"));
assert.ok(result.issues.includes("Codex compatibility note is required"));
assert.ok(result.issues.includes("Pi compatibility note is required"));
assert.ok(result.issues.includes("Claude Code compatibility note is required"));
});
Loading