mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-07 05:56:21 -10:00
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:
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" });
|
||||
|
||||
Reference in new issue
Block a user