Files
stack/packages/harness/src/pi-extension.mjs
T
jason.woltjeandClaude Opus 5.5 ce104b27cc bus, harness: PM task reads that fit a session (#1583)
Row 89, round 1. The broker's `tasks` read takes optional filter and
page arguments and returns a short form. The harness adds `list_tasks`
arguments (open tasks by default), a `get_task` tool, and compact JSON
replies capped below the 50000-character threshold where Claude Code
2.1.296 saves a tool result to a file outside the workspace, which was
the PM's fault in Jason's test. The bundle's tools line names the
harness's own tools.

Candidate manifest b8c5bb3a (agents/dewey/work/queue-89/), base
a3f6e9e6, no drift on the 12 paths. Builder Dewey. Approved by
Darkwing (comment 27940). Sage's gate on f04185fc plus the patch
2026-10-12T22:00:55Z-22:03:38Z: bus 108, harness 65, webui 86, cli
115, control-board 124, every scripts/test-*.sh green.

Live at the next bus restart (broker) and role session launch
(harness), both Mos's call (decision 109); Mos had the notice.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-12 17:04:15 -05:00

50 lines
2.0 KiB
JavaScript

// Pi extension for a managed session, loaded with `-e` by adapters/pi.
// Three jobs:
// - register the session's typed tools, which call the runner's tool socket;
// - gate every tool call with gate.mjs (S0 lines 1 and 2: a block or a
// throw stops the tool);
// - write the turn marker on agent_end, so the runner can tell a finished
// turn from a pi that exited 0 mid-turn (S0 line 4).
// Missing or unreadable configuration throws here, which fails pi's start
// (S0 line 3): nothing is defaulted.
import { readFileSync, writeFileSync } from "node:fs";
import { decide } from "./gate.mjs";
import { callTool, replyText } from "./tools.mjs";
function need(name) {
const value = process.env[name];
if (typeof value !== "string" || !value) throw new Error(`mosaic extension: ${name} is not set`);
return value;
}
export default function (pi) {
const policy = JSON.parse(readFileSync(need("MOSAIC_POLICY_FILE"), "utf8"));
const tools = JSON.parse(readFileSync(need("MOSAIC_TOOLS_FILE"), "utf8"));
const socket = need("MOSAIC_TOOL_SOCKET");
const marker = need("MOSAIC_TURN_MARKER");
if (policy.harness !== "pi" || !Array.isArray(policy.tools) || !Array.isArray(policy.typed)) throw new Error("mosaic extension: policy file is not a pi policy");
pi.on("tool_call", async (event) => {
const verdict = decide(policy, event.toolName, event.input);
return verdict.allow ? undefined : { block: true, reason: verdict.reason };
});
pi.on("agent_end", async () => {
writeFileSync(marker, `${new Date().toISOString()}\n`);
});
for (const t of tools) {
pi.registerTool({
name: t.name,
label: t.name,
description: t.description,
parameters: t.parameters,
async execute(_toolCallId, params) {
// A refusal, or a reply over REPLY_CAP, throws, which pi reports to the model as a tool error.
const result = await callTool(socket, t.name, params ?? {});
return { content: [{ type: "text", text: replyText(result) }], details: {} };
},
});
}
}