test(pi): a Pi seat's managed guidance goes to its agent folder and the tracked AGENTS.md is never written

A Pi seat loads its agent folder's AGENTS.md before the repository's, so its
managed guidance can live there instead of in the person's tracked file. These
cells pin that: the launch chooses and records the file, every reader uses the
recorded one, rig down and an exact resume leave the tracked AGENTS.md
byte-identical, a linked agent file or an older snapshot keeps the repository
file, and OMP is unchanged.
This commit is contained in:
dev60-review committed 2026-11-03 04:54:27 +00:00
1 parent d7fc1fce0c
commit e91e308b32
4 files changed
+123 -1

No files matched your search

@@ -1171,6 +1171,16 @@ describe("node detail reports a guidance block's installed file only from eviden
declaredTarget: "CLAUDE.md", effectiveTarget: path.join(cwd, "AGENTS.md") });
});
it("follows a Pi seat's recorded guidance file in its agent folder, not the repository AGENTS.md (#875)", () => {
const agentFile = path.join(cwd, "pi-state", "dev-impl@test-rig", "agent", "AGENTS.md");
fs.mkdirSync(path.dirname(agentFile), { recursive: true });
fs.writeFileSync(agentFile, block("role"));
fs.writeFileSync(path.join(cwd, "AGENTS.md"), block("role")); // a stale repository copy isn't evidence for this seat
seedStartupContext(db, "node-1", { runtime: "pi", projectionEntries: [guidance("AGENTS.md")] });
db.prepare("UPDATE node_startup_context SET guidance_file = ? WHERE node_id = 'node-1'").run(agentFile);
expect(getNodeDetail(db, "rig-1", "dev.impl")!.installedResources[0]).toMatchObject({ targetPath: agentFile, effectiveTarget: agentFile });
});
it("claims nothing for a runtime with no known guidance file, a merge that writes nothing, or a half-written block", () => {
fs.writeFileSync(path.join(cwd, "AGENTS.md"), block("role"));
expect(installed("synth")[0]).toEqual({ id: "role", category: "guidance", targetPath: "", declaredTarget: "AGENTS.md" });
@@ -765,3 +765,57 @@ describe("durable catch-up cursor survives launch-attempt resets", () => {
}
});
});
// #875 direction 2: Pi loads its agent folder's AGENTS.md before the repository's (Quality's native run on Pi 1.0.1), so a
// Pi seat's managed guidance goes there and the person's tracked repository AGENTS.md is never written.
describe("a Pi seat's managed guidance goes to its own agent folder (#875)", () => {
const AGENT_FILE = nodePath.join(piSeatPaths(STATE_ROOT, SESSION).agentDir, "AGENTS.md");
const seat = { tmuxSession: SESSION, cwd: "/work" } as never;
it("chooses the seat's agent-folder AGENTS.md, and nothing without a session", () => {
const adapter = adapterWith(memFs(), mockTmux());
expect(adapter.guidanceFileFor(seat)).toEqual({ file: AGENT_FILE });
expect(adapter.guidanceFileFor({ tmuxSession: null, cwd: "/work" } as never)).toEqual({ file: null });
});
it.each([
["the agent AGENTS.md is a symlink", AGENT_FILE, { link: true, nlink: 1 }],
["the agent AGENTS.md is a hard link", AGENT_FILE, { link: false, nlink: 2 }],
["the agent folder is a symlink", piSeatPaths(STATE_ROOT, SESSION).agentDir, { link: true, nlink: 1 }],
])("keeps the repository file when %s, which other sessions could read, and says so", (_case, linked, info) => {
const fs = { ...memFs(), linkInfo: (p: string) => (p === linked ? info : null) };
const chosen = adapterWith(fs, mockTmux()).guidanceFileFor(seat);
expect(chosen.file).toBeNull();
expect(chosen.note).toMatch(/repository AGENTS\.md/);
});
it("merges startup guidance and projected guidance into the chosen file, creating its folder, and never the repository AGENTS.md", async () => {
const fs = memFs({ "/spec/SOP.md": "How work flows here.", "/spec/culture.md": "Team culture." });
const adapter = adapterWith(fs, mockTmux());
const binding = { tmuxSession: SESSION, cwd: "/work", guidanceFile: AGENT_FILE } as never;
const delivered = await adapter.deliverStartup([{ path: "SOP.md", absolutePath: "/spec/SOP.md", ownerRoot: "/spec",
deliveryHint: "guidance_merge", required: true, appliesOn: ["fresh_start"] }], binding);
expect(delivered.failed).toEqual([]);
const projected = await adapter.project({ runtime: "pi", cwd: "/work", entries: [{ effectiveId: "culture", category: "guidance",
mergeStrategy: "managed_block", absolutePath: "/spec/culture.md", classification: "safe_projection" }],
startup: { files: [], actions: [] }, conflicts: [], noOps: [], diagnostics: [] } as never, binding);
expect(projected.failed).toEqual([]);
expect(fs.files[AGENT_FILE]).toContain("How work flows here.");
expect(fs.files[AGENT_FILE]).toContain("Team culture.");
expect(fs.dirs.has(nodePath.dirname(AGENT_FILE))).toBe(true);
expect("/work/AGENTS.md" in fs.files).toBe(false);
});
it("a binding with no chosen file (a restore from an older snapshot) still merges into the repository AGENTS.md", async () => {
const fs = memFs({ "/spec/SOP.md": "How work flows here." });
await adapterWith(fs, mockTmux()).deliverStartup([{ path: "SOP.md", absolutePath: "/spec/SOP.md", ownerRoot: "/spec",
deliveryHint: "guidance_merge", required: true, appliesOn: ["fresh_start"] }], { tmuxSession: SESSION, cwd: "/work", guidanceFile: null } as never);
expect(fs.files["/work/AGENTS.md"]).toContain("How work flows here.");
expect(AGENT_FILE in fs.files).toBe(false);
});
it("an OMP seat keeps the repository file: nothing has shown OMP loads its agent folder's AGENTS.md", () => {
const omp = new OmpRuntimeAdapter({ tmux: mockTmux(), fsOps: memFs(), stateRoot: STATE_ROOT, runnerEntryPath: RUNNER });
expect(omp.guidanceFileFor(seat)).toEqual({ file: null });
});
});
@@ -72,6 +72,8 @@ interface SeatOptions {
autoGuidanceFile?: boolean;
/** The saved selection carries the seat-skills bootstrap file, as for every seat with skill storage. */
seatSkillsLine?: boolean;
/** #875: a Pi launch that chose its own agent folder's AGENTS.md, recorded with the startup context. */
piAgentFile?: boolean;
}
/** The resume type each runtime records for its native session. */
@@ -169,7 +171,12 @@ describe("a seat's guidance across rig down then rig up --existing (exact resume
const actions = [{ type: "send_text", value: "STARTUP-PROMPT-MUST-NOT-REPLAY", phase: "after_ready", appliesOn: ["fresh_start", "restore"], idempotent: true }];
db.prepare("INSERT INTO node_startup_context (node_id, projection_entries_json, resolved_files_json, startup_actions_json, runtime) VALUES (?, ?, ?, ?, ?)")
.run(node.id, JSON.stringify(entries), JSON.stringify(files), JSON.stringify(actions), runtime);
const guidanceFile = path.join(cwd, runtime === "claude-code" ? (opts.claudeManagedBlockFile ?? "CLAUDE.md") : "AGENTS.md");
const guidanceFile = opts.piAgentFile ? path.join(tmp, "pi-state", sessionName, "agent", "AGENTS.md")
: path.join(cwd, runtime === "claude-code" ? (opts.claudeManagedBlockFile ?? "CLAUDE.md") : "AGENTS.md");
if (opts.piAgentFile) {
fs.mkdirSync(path.dirname(guidanceFile), { recursive: true });
db.prepare("UPDATE node_startup_context SET guidance_file = ? WHERE node_id = ?").run(guidanceFile, node.id);
}
if (opts.userText) fs.writeFileSync(guidanceFile, opts.userText);
mergeManagedBlock(nodeFs, guidanceFile, "lead-role", "You lead this team.");
mergeManagedBlock(nodeFs, guidanceFile, "SOP.md", "How work flows here.");
@@ -290,6 +297,38 @@ describe("a seat's guidance across rig down then rig up --existing (exact resume
for (const other of ["CLAUDE.md", "CLAUDE.local.md"]) expect(fs.existsSync(path.join(cwd, other)), other).toBe(false);
});
// #875 direction 2: this Pi seat's launch put its blocks in its own agent folder's AGENTS.md. Down takes them out of that
// file, the exact resume puts them back there, and the person's tracked repository AGENTS.md is never written.
it("a Pi seat whose launch chose its agent folder keeps the tracked repository AGENTS.md byte-identical across down and the exact resume", async () => {
const tracked = "# Project guidance (tracked in git)\nRun the tests before pushing.\n";
const repoFile = path.join(cwd, "AGENTS.md");
fs.writeFileSync(repoFile, tracked);
const { rigId, guidanceFile } = launchedSeat({ podAware: true, runtime: "pi", piAgentFile: true, userText: "My own Pi notes\n" });
expect(guidanceFile).not.toBe(repoFile);
let afterDown: string | null = null;
const { result, adapter, fileAtHarnessStart } = await downThenUpExisting(rigId, guidanceFile, { podAware: true, runtime: "pi",
betweenDownAndUp: () => {
afterDown = fs.readFileSync(guidanceFile, "utf-8");
expect(fs.readFileSync(repoFile, "utf-8"), "the tracked AGENTS.md after rig down").toBe(tracked);
} });
expect(afterDown, "the agent AGENTS.md after rig down keeps only the person's lines").toBe("My own Pi notes\n");
expect(fileAtHarnessStart).toContain("My own Pi notes\n");
expect(fileAtHarnessStart).toContain(MANAGED_BLOCK_START("lead-role"));
expect(fileAtHarnessStart).toContain(MANAGED_BLOCK_START("SOP.md"));
expect(fs.readFileSync(repoFile, "utf-8"), "the tracked AGENTS.md after the exact resume").toBe(tracked);
expect(result.nodes[0]?.guidanceGaps).toBeUndefined();
// The restore hands the launch's recorded choice to the adapter, so a replayed start would use the same file.
expect((adapter.project.mock.calls[0] as unknown[] | undefined)?.[1]).toMatchObject({ guidanceFile });
});
it("a Pi seat restored from a snapshot with no recorded choice stays on the repository AGENTS.md (no silent repoint)", async () => {
const { rigId, guidanceFile } = launchedSeat({ podAware: true, runtime: "pi" });
expect(guidanceFile).toBe(path.join(cwd, "AGENTS.md"));
const { adapter, fileAtHarnessStart } = await downThenUpExisting(rigId, guidanceFile, { podAware: true, runtime: "pi" });
expect(fileAtHarnessStart).toContain(MANAGED_BLOCK_START("SOP.md"));
expect((adapter.project.mock.calls[0] as unknown[] | undefined)?.[1]).toMatchObject({ guidanceFile: null });
});
it("only a missing block is put back: an existing block keeps its content, and user text is preserved", async () => {
const { rigId, guidanceFile } = launchedSeat({ userText: "# My project notes\nKeep tests fast.\n" });
const { fileAtHarnessStart } = await downThenUpExisting(rigId, guidanceFile, {
@@ -776,6 +776,25 @@ describe("StartupOrchestrator", () => {
expect(read()).toEqual(before);
});
// #875 direction 2: a launch decides once where its managed guidance goes and records it; a restore passes the saved
// choice (null for an older snapshot), so the adapter isn't asked again and an old seat isn't silently repointed.
it("records the adapter's chosen guidance file at launch, and uses a restore's saved choice without asking again", async () => {
const seed = seedSession(); const orch = createOrchestrator();
const chosen = "/openrig-home/state/pi/r01-impl/agent/AGENTS.md";
const guidanceFileFor = vi.fn(() => ({ file: chosen }));
const adapter = mockAdapter({ runtime: "pi", guidanceFileFor } as never);
const recorded = () => (db.prepare("SELECT guidance_file FROM node_startup_context WHERE node_id=?").get(seed.nodeId) as { guidance_file: string | null }).guidance_file;
expect((await orch.startNode(makeInput(seed, { adapter }))).ok).toBe(true);
expect(vi.mocked(adapter.project).mock.calls[0]?.[1]).toMatchObject({ guidanceFile: chosen });
expect(vi.mocked(adapter.deliverStartup).mock.calls[0]?.[1]).toMatchObject({ guidanceFile: chosen });
expect(recorded()).toBe(chosen);
guidanceFileFor.mockClear();
const restored = await orch.startNode(makeInput(seed, { adapter, isRestore: true, binding: { ...makeBinding(), guidanceFile: null } }));
expect(restored.ok).toBe(true);
expect(guidanceFileFor).not.toHaveBeenCalled();
expect(recorded()).toBeNull();
});
it("exact resume retains configured fresh context while sending no replay", async () => {
const seed = seedSession(); const orch = createOrchestrator();
const action = makeAction({ type: "send_text", value: "configured context" });