cli, seat, harness: a project clone in each role workspace (#1585)

Row 91, round 3. Darkwing built it, Dewey approved it (comment 28150) on
manifest ca570607 (8 paths, base 2b2a9e14). At each launch the host
makes or updates a git clone of the project in the role's workspace,
and the session prompt names each clone. Round 3 closes Filbert's round
2 escape: a clone with a submodule is kept as it is, and no host git
recurses into one (decision 130 thread). Live at the next bus restart;
the prompt lines appear only when the launcher passes checkouts.
Sage gate on fc6d5822 plus the patch, 14:18:25Z to 14:22:27Z: bus 115,
business 60, cli 131, control-board 124, conversation 182, discord 178,
harness 67, ledger 78, mosaic 69, queue 150, runs 43, seat 89, tasks 89,
webui 104, all fail 0. Every scripts/test-*.sh exited 0; test-release
ran 14 cases.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-13 09:23:10 -05:00
co-authored by Claude Opus 5.5
parent fc6d58220e
commit d240645df0
8 changed files with 782 additions and 10 deletions
+11 -1
View File
@@ -55,6 +55,13 @@ export function sessionModel(resolved) {
return { harness, provider: v["execution.provider"] ?? null, model, thinking: v.thinking ?? null };
}
// Row 91 (#1585): the project clones the host made or kept in the workspace.
function checkoutLines(checkouts) {
if (!checkouts.length) return [];
const each = checkouts.map((c) => (c.outcome === "kept" ? `${c.project} at ${c.path}, not updated at this launch (${c.reason})` : `${c.project} at ${c.path}, on branch ${c.branch}`));
return [`- Project clones: ${each.join("; ")}. Each is a git clone of the project. The host fetches from its origin at each launch; the origin is outside this sandbox.`];
}
function prompt({ contract, resolved, session, tools, untyped }) {
const lines = [
contract.trimEnd(),
@@ -64,6 +71,7 @@ function prompt({ contract, resolved, session, tools, untyped }) {
`- Business: ${resolved.business}. Role instance: ${resolved.instance} (definition ${resolved.definition}). Run: ${session.run}.`,
`- Launched by: ${session.launchedBy}. Harness: ${session.harness}, model ${session.model}.`,
`- Workspace: ${session.workspace}. File tools work inside it only.`,
...checkoutLines(session.checkouts ?? []),
// Row 89 (#1583): the names the harness gives its built-ins, as the manifest lists them.
`- Built-in tools: ${(session.harness === "pi" ? resolved.limits.tools : claudeBuiltins({ tools: resolved.limits.tools })).join(", ") || "none"}.`,
`- Bus tools: ${tools.map((t) => t.name).join(", ")}. The broker decides every call; a refusal names its reason.`,
@@ -83,7 +91,9 @@ function prompt({ contract, resolved, session, tools, untyped }) {
// arbiter true when the business names this instance as an arbiter
// tracker true when the broker has task verbs for the business
// credentials the broker's credentialStatus(...) for the instance (metadata only)
// session { run, launchedBy, workspace, toolSocket, turnMarker }
// session { run, launchedBy, workspace, toolSocket, turnMarker, checkouts }
// checkouts is ensureCheckouts(...) output (packages/seat/src/checkout.mjs),
// named in the prompt; none when absent
// piVersion the pinned pi version, for the manifest
// claudeVersion the host's `claude --version`, for the manifest
// Returns { manifest, files } with absolute paths.
+17
View File
@@ -103,6 +103,23 @@ test("the prompt's built-in tools line is the harness's own names, as the manife
}
});
// Row 91 (#1585): the prompt names each project clone, and why a kept one wasn't updated.
test("the prompt names the project clones in the workspace, and none when there are none", (t) => {
const line = (b) => readFileSync(b.files.prompt, "utf8").split("\n").filter((l) => l.startsWith("- Project clones: "));
const plain = build(t, resolvedFor(t, "pm"));
assert.deepEqual(line(plain), []);
const dir = scratch(t);
const session = (checkouts) => ({ run: "run-1", launchedBy: "pm", workspace: join(dir, "ws"), toolSocket: join(dir, "tools.sock"), turnMarker: join(dir, "turn.done"), checkouts });
const two = build(t, resolvedFor(t, "pm"), { session: session([
{ project: "stack", path: join(dir, "ws", "stack"), outcome: "updated", branch: "main" },
{ project: "site", path: join(dir, "ws", "site"), outcome: "kept", reason: "dirty" },
]) });
assert.deepEqual(line(two), [`- Project clones: stack at ${join(dir, "ws", "stack")}, on branch main; site at ${join(dir, "ws", "site")}, not updated at this launch (dirty). Each is a git clone of the project. The host fetches from its origin at each launch; the origin is outside this sandbox.`]);
const prompt = readFileSync(two.files.prompt, "utf8");
assert.ok(prompt.indexOf("- Workspace: ") < prompt.indexOf("- Project clones: "));
assert.deepEqual(line(build(t, resolvedFor(t, "pm"), { session: session([]) })), []);
});
test("a bundle is written once: an existing file refuses", (t) => {
const dir = scratch(t);
mkdirSync(join(dir, "bundle"));