Slice 1 S6. packages/harness runs a role session under the pi or claude
adapter behind a tool gate, and packages/seat holds the session and its
process. The bus host gains a launch socket: `mosaic bus start
<business> --pm <harness>:<model>` runs the PM without a window, the PM
launches role sessions through the broker within the business file's
`launch.max`, and every launch, refusal and end is a launch-log line and
a broker event. `mosaic talk`, `mosaic launches off|on|list` and
`mosaic stop <run>` are new CLI verbs. packages/bus gains the IPC ops
identity, authorize, refuse, endLaunch and credentialStatus, plus
Broker.endLaunch (decision 77); no socket verb, event kind or schema
changed. The session environment is an allowlist, and the runner exits
before claiming if a founder credential reaches it.
Filbert built it (agents/filbert/work/s6). Darkwing reviewed on #1523:
round 1 changes (26991/26995), round 2 changes (27004/27010), round 3
changes (27012/27032), round 4 approve (27033/27034) on manifest
08a78972, 42 paths.
Sage's gate on 64b0f9f1 (12:32Z), sequential and teed: node suites
business 60, bus 74, cli 83, control-board 124, conversation 182,
discord 178, harness 56, ledger 78, mosaic 69, queue 149, runs 41,
seat 27, tasks 51, webui 39, every one 0 failed; test-auth 15,
test-conductor 17, test-config 24, test-discord 66,
test-extension-package 18, test-foundation 44, test-queue 27,
test-release 14, test-task 98 and 26 without Docker, all 0 failed.
64b0f9f1..ee7f909b touches none of the 42 paths. It landed after the
Q14 hold ended at 2026-10-11T17:19:23.857Z. The recorded PM-to-coder
run on a scratch data root is still owed before row 41 moves to done.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
106 lines
5.6 KiB
JavaScript
106 lines
5.6 KiB
JavaScript
// Shared fixtures: resolved role instances from the business package's own
|
|
// test business, scratch directories, a fake tool socket and a scripted
|
|
// stand-in for the Anthropic Messages API (row S0's probe mock, reduced).
|
|
// Nothing here reads the real configuration or calls a real model.
|
|
|
|
import { createServer as createHttp } from "node:http";
|
|
import { createServer } from "node:net";
|
|
import { mkdtempSync, realpathSync, rmSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { resolveInstance, validateBusinessDocument } from "../../business/src/index.mjs";
|
|
import { businessDoc, REPO_ROLES, systemFor } from "../../business/tests/helpers.mjs";
|
|
|
|
export { REPO } from "../../business/tests/helpers.mjs";
|
|
|
|
export function scratch(t, prefix = "mosaic-harness-") {
|
|
const dir = realpathSync(mkdtempSync(join(tmpdir(), prefix)));
|
|
t.after(() => rmSync(dir, { recursive: true, force: true }));
|
|
return dir;
|
|
}
|
|
|
|
// resolved("pm" | "coder" | ..., { mutate }) in a scratch root.
|
|
export function resolvedFor(t, instance, mutate) {
|
|
const root = scratch(t, "mosaic-harness-biz-");
|
|
const doc = businessDoc(root);
|
|
mutate?.(doc);
|
|
const business = validateBusinessDocument(doc, join(root, "businesses", "acme.json"), { rolesDir: REPO_ROLES });
|
|
return resolveInstance({ system: systemFor(root), business, instance });
|
|
}
|
|
|
|
// A tool socket that answers with `handler(tool, args)`; a throw becomes a refusal.
|
|
export async function fakeToolSocket(t, dir, handler) {
|
|
const path = join(dir, "tools.sock");
|
|
const calls = [];
|
|
const server = createServer((s) => {
|
|
let input = "";
|
|
s.on("data", async (b) => {
|
|
input += b;
|
|
if (!input.includes("\n")) return;
|
|
const { tool, args } = JSON.parse(input);
|
|
calls.push({ tool, args });
|
|
try {
|
|
s.end(JSON.stringify({ ok: true, result: await handler(tool, args) }) + "\n");
|
|
} catch (e) {
|
|
s.end(JSON.stringify({ ok: false, error: e.message }) + "\n");
|
|
}
|
|
});
|
|
});
|
|
await new Promise((r) => server.listen(path, r));
|
|
t.after(() => server.close());
|
|
return { path, calls };
|
|
}
|
|
|
|
const textOf = (c) => (typeof c === "string" ? c : Array.isArray(c) ? c.map((b) => b.text ?? "").join("") : "");
|
|
function toolResults(messages) {
|
|
const out = [];
|
|
for (const m of messages ?? []) {
|
|
if (m.role !== "user" || !Array.isArray(m.content)) continue;
|
|
for (const b of m.content) if (b.type === "tool_result") out.push({ id: b.tool_use_id, isError: b.is_error ?? false, content: textOf(b.content) });
|
|
}
|
|
return out;
|
|
}
|
|
|
|
// A scripted Anthropic Messages API. Each request that offers tools gets
|
|
// the next scripted tool call; once every call has a result it answers
|
|
// `done(results)` as text. Returns { url, requests, close }.
|
|
export async function mockAnthropic(t, { script = [], done = (r) => `DONE ${JSON.stringify(r)}` } = {}) {
|
|
const requests = [];
|
|
const server = createHttp((req, res) => {
|
|
let raw = "";
|
|
req.on("data", (c) => (raw += c));
|
|
req.on("end", () => {
|
|
if (req.method !== "POST" || !/\/messages(\?|$)/.test(req.url)) {
|
|
res.writeHead(req.url.includes("count_tokens") ? 200 : 404, { "content-type": "application/json" });
|
|
return res.end(JSON.stringify(req.url.includes("count_tokens") ? { input_tokens: 10 } : { type: "error", error: { type: "not_found_error", message: "mock" } }));
|
|
}
|
|
const body = JSON.parse(raw || "{}");
|
|
const tools = Array.isArray(body.tools) ? body.tools.map((x) => x.name) : [];
|
|
const results = toolResults(body.messages);
|
|
const step = tools.length ? script[results.length] : null;
|
|
const r = step ? { kind: "tool", id: `toolu_${results.length + 1}`, name: step.name, input: step.input } : { kind: "text", text: tools.length ? done(results) : "mock" };
|
|
requests.push({ tools, results, system: body.system, answer: r, raw });
|
|
const content = r.kind === "tool" ? { type: "tool_use", id: r.id, name: r.name, input: {} } : { type: "text", text: "" };
|
|
const delta = r.kind === "tool" ? { type: "input_json_delta", partial_json: JSON.stringify(r.input) } : { type: "text_delta", text: r.text };
|
|
const stop = r.kind === "tool" ? "tool_use" : "end_turn";
|
|
if (!body.stream) {
|
|
res.writeHead(200, { "content-type": "application/json" });
|
|
const c = r.kind === "tool" ? { type: "tool_use", id: r.id, name: r.name, input: r.input } : { type: "text", text: r.text };
|
|
return res.end(JSON.stringify({ id: "msg_mock", type: "message", role: "assistant", model: body.model, content: [c], stop_reason: stop, stop_sequence: null, usage: { input_tokens: 10, output_tokens: 5 } }));
|
|
}
|
|
const send = (event, data) => res.write(`event: ${event}\ndata: ${JSON.stringify({ type: event, ...data })}\n\n`);
|
|
res.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-cache" });
|
|
send("message_start", { message: { id: "msg_mock", type: "message", role: "assistant", model: body.model, content: [], stop_reason: null, stop_sequence: null, usage: { input_tokens: 10, output_tokens: 1, cache_creation_input_tokens: 0, cache_read_input_tokens: 0 } } });
|
|
send("content_block_start", { index: 0, content_block: content });
|
|
send("content_block_delta", { index: 0, delta });
|
|
send("content_block_stop", { index: 0 });
|
|
send("message_delta", { delta: { stop_reason: stop, stop_sequence: null }, usage: { output_tokens: 5 } });
|
|
send("message_stop", {});
|
|
res.end();
|
|
});
|
|
});
|
|
await new Promise((r) => server.listen(0, "127.0.0.1", r));
|
|
t.after(() => server.close());
|
|
return { url: `http://127.0.0.1:${server.address().port}`, requests };
|
|
}
|