Files
stack/packages/plugins/harness/pi/index.mjs
T
jason.woltje c216f47dbb plugins, cli, bus, business, webui: harness, model and thinking from the instance vars, a fallback route, and a harness plug-in kind (row 65, #1558)
Adds the harness plug-in kind (interface 1). packages/plugins/harness/pi
and claude-code hold what core branched on: the adapter, thinking levels,
preflight, version probe, sandbox mounts, builtins line, typed tool names,
gate timeout, bundle files, adapter env, turn end and session-files path.
Core reaches a module only through the loader.

A session's route is the instance's harness, model and thinking vars, or
the three fallback. vars; a missing one refuses no-route, and the system
file's execution keys are never read for a session. The launcher checks
the primary route, then the fallback: harness-off, harness-refused,
thinking-unsupported, then preflight. A failed preflight falls back when
a fallback is set, and any refusal of the fallback names the primary's
failure too. Account checks run
only when the route's module counts an account. The manifest records
route, routeReason and harnessVersion, replacing piVersion and
claudeVersion. pi gets --thinking, Claude Code --effort, both from
MOSAIC_THINKING.

The business file refuses a partial fallback. routes and routes.set are
human-only bus verbs outside ORGANIZATION_KEYS; checkRoutes is shared by
routes.set, instances.set and limits.set and checks only changed routes.
The chief of staff changes an instance's model, and the assistant's whole
vars (Q169), through limits.set, so its route write runs the same checks.
A model outside launch.models refuses unknown-model, primary or fallback,
and routes.set passes that code through.
A new run binds only on a switched-on harness module name and records
its transcript path, which the usage reader uses. Settings gains a
Routing table with six selects per instance and loses the Role models
fieldset; it posts changed rows to /api/routes. webui/src/bus.mjs is
unchanged.

Until Jason's `mosaic plugins set` adds "harness": ["claude-code"], a new
launch refuses harness-off.
2026-10-19 11:27:19 -05:00

66 lines
3.3 KiB
JavaScript

// The "pi" harness plug-in (row 65, REQ-PLUG-2, Q156): a managed session on the pinned
// @earendil-works/pi-coding-agent CLI, the repository's node_modules/.bin/pi. The tool gate is the
// Pi extension, which stays in packages/harness (a module can't change that boundary). The
// launcher, the bundle and the usage reader call these on the host; the runner gets the answers
// as data in session.json. packages/plugins/README.md lists the interface.
import { existsSync, readFileSync, realpathSync } from "node:fs";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
export const interfaceVersion = 1;
const REPO = fileURLToPath(new URL("../../../../", import.meta.url));
const PACKAGE = "@earendil-works/pi-coding-agent";
const BIN = join(REPO, "node_modules", ".bin", "pi");
const EXTENSION = fileURLToPath(new URL("../../../harness/src/pi-extension.mjs", import.meta.url));
// `pi --thinking <level>` on 0.85.1 (probed 2026-10-19, agents/sage/work/queue-65/BUILD.md).
const LEVELS = Object.freeze(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
// docs/plans/2026-10-04_slice-1.md, "What S6 can rely on", lines 1-5.
const RELIES_ON = Object.freeze(["S0-1 tool_call block", "S0-2 a throw blocks", "S0-3 a missing -e refuses to start",
"S0-4 external wall clock plus agent_end check", "S0-5 bash is limited by the tool limit only"]);
export const adapter = () => join(REPO, "adapters", "pi", "adapter.sh");
export const levels = () => LEVELS;
// The session runs pi from the repository's node_modules, which the sandbox mounts.
export function preflight() {
if (!existsSync(BIN)) return { code: "harness-binary-missing", message: `pi isn't installed at ${BIN}; run npm ci in the repository` };
return null;
}
// The exact pin from the repository's package.json.
export function version() {
try {
const pkg = JSON.parse(readFileSync(join(REPO, "package.json"), "utf8"));
return pkg.dependencies?.[PACKAGE] ?? pkg.devDependencies?.[PACKAGE] ?? null;
} catch {
return null;
}
}
// Pi sessions run on a provider key, never the Claude account limit.
export const countsAccount = () => false;
export const mounts = () => [{ src: realpathSync(join(REPO, "node_modules")), dest: join(REPO, "node_modules"), why: "pi and its packages, on the session's PATH" }];
// Pi has no web tool of its own, so a granted role under harness-builtin is refused.
export const builtinWeb = () => false;
export const builtins = ({ tools }) => [...tools];
export const typedName = (name) => name;
export const reliesOn = () => RELIES_ON;
export const gateTimeout = () => null;
export const bundle = () => ({ files: {}, digests: [], sources: { piExtension: EXTENSION } });
// The tool limit is the built-ins plus the typed tools, which the extension serves.
export function env({ policy, tools, session }) {
return {
MOSAIC_TOOLS: [...policy.tools, ...tools.map((t) => t.name)].join(","),
MOSAIC_EXTENSIONS: EXTENSION,
PI_PROVIDER: session.provider ?? "",
PI_MODEL: session.model,
MOSAIC_THINKING: session.thinking,
};
}
// The extension writes the turn marker at agent_end; an exit without it is a failed turn.
export const turnEnd = () => "marker";
export const transcript = ({ runDir, store = null }) => (store ? store.session : join(runDir, "session"));