mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-07 05:56:21 -10:00
fix(cli): the ACP launcher warns about another CLI version and runs it
- When module resolution finds an @openrig/cli other than the version @openrig/acp pins (only in a broken or pruned install), the launcher names the version it found, where it found it and the pinned version on stderr, then runs it as usual. A newer CLI very likely works, and one that doesn't fails visibly, so refusing protected nothing. - The guide and the launcher's README give the full registration command, with the two options it needs. - A reopened session replays its questions in the order they were asked. Cells now cover a question still open and one closed without an answer.
This commit is contained in:
1 parent
e691399f34
commit
a31cb3cb7a
6 files changed
+49
-16
No files matched your search
@@ -221,7 +221,8 @@ app that starts the agent from the ACP registry passes no arguments, so that's h
|
||||
that the daemon answers and that you resolve to an address; until then, signing in and new sessions fail with the
|
||||
reason and the fix. The agent talks only to the daemon on this computer, so start it first (`rig daemon start`).
|
||||
The team's questions reach you only as a registered person: an `<id>@external` address needs
|
||||
`rig gateway human add <id>` first. Until then, the app says so at your first prompt, and your work is still sent.
|
||||
`rig gateway human add <id> --display-name "<your name>" --delivery-class <A|B|C|D>` first. Until then, the app says
|
||||
so at your first prompt, and your work is still sent.
|
||||
|
||||
**In Zed**, add the agent to `settings.json`:
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ OpenRig as an agent in apps that speak the [Agent Client Protocol](https://agent
|
||||
Zed and JetBrains AI Assistant. Your prompt goes to your OpenRig team as a queue row. The app shows who has the work
|
||||
and their notes as it moves, any question the team asks you as a choice, and the team's result as the reply.
|
||||
|
||||
This package has one command, `openrig-acp`. It starts `rig acp serve` from the exact `@openrig/cli` version it
|
||||
depends on, and stops with a message if it finds another, so ACP apps and the ACP registry can launch it with `npx`.
|
||||
This package has one command, `openrig-acp`. It starts `rig acp serve` from the `@openrig/cli` version it depends on,
|
||||
and warns on stderr if it finds another, so ACP apps and the ACP registry can launch it with `npx`.
|
||||
|
||||
## What you need
|
||||
|
||||
@@ -13,7 +13,8 @@ depends on, and stops with a message if it finds another, so ACP apps and the AC
|
||||
- The seat that receives your team's new work, set once with `rig config set acp.to <member@rig>`, or passed as
|
||||
`--to <member@rig>`.
|
||||
- You, as a person OpenRig knows: the default person (`rig gateway default <id>`), or `--as <address>`. The team's
|
||||
questions reach an `<id>@external` address only once it's registered (`rig gateway human add <id>`).
|
||||
questions reach an `<id>@external` address only once it's registered:
|
||||
`rig gateway human add <id> --display-name "<your name>" --delivery-class <A|B|C|D>`.
|
||||
|
||||
The app's sign-in step, "OpenRig on this machine", checks that the daemon answers and that it can tell who you are.
|
||||
This agent talks only to the OpenRig daemon on this computer.
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#!/usr/bin/env node
|
||||
// OpenRig as an agent in ACP apps. The ACP registry starts an agent with `npx <package>@<version>`, and npx needs a
|
||||
// package with one executable: @openrig/cli has two. This one starts `rig acp serve` from the exact @openrig/cli this
|
||||
// package depends on (never a `rig` found elsewhere), with the app's stdin and stdout, arguments, environment and
|
||||
// working directory, and exits as it exits. Only ACP messages may reach stdout, so anything this file says goes to
|
||||
// package with one executable: @openrig/cli has two. This one starts `rig acp serve` from the @openrig/cli this package
|
||||
// depends on, as Node resolves it (never a `rig` on the PATH), with the app's stdin and stdout, arguments, environment
|
||||
// and working directory, and exits as it exits. Only ACP messages may reach stdout, so anything this file says goes to
|
||||
// stderr.
|
||||
import { spawn } from "node:child_process";
|
||||
import { readFileSync } from "node:fs";
|
||||
@@ -13,11 +13,11 @@ let rig;
|
||||
try {
|
||||
const manifestPath = createRequire(import.meta.url).resolve("@openrig/cli/package.json");
|
||||
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
||||
// Module resolution can reach a different @openrig/cli further up when this package's own copy is missing.
|
||||
// Module resolution can reach a different @openrig/cli further up when this package's own copy is missing. That copy
|
||||
// most likely still works, and one that doesn't fails visibly, so say which version runs and run it.
|
||||
const wanted = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).dependencies["@openrig/cli"];
|
||||
if (manifest.version !== wanted) {
|
||||
console.error(`openrig-acp: found @openrig/cli ${manifest.version} at ${dirname(manifestPath)}, but this package runs exactly ${wanted}. Reinstall @openrig/acp.`);
|
||||
process.exit(1);
|
||||
console.error(`openrig-acp: found @openrig/cli ${manifest.version} at ${dirname(manifestPath)}, not the ${wanted} this package pins; running it anyway. Reinstall @openrig/acp to get ${wanted}.`);
|
||||
}
|
||||
rig = join(dirname(manifestPath), manifest.bin.rig);
|
||||
} catch (error) {
|
||||
|
||||
@@ -350,7 +350,7 @@ export function serveAcpAgent(peer: JsonRpcPeer, options: AcpAgentOptions): void
|
||||
const roots = await sessionRoots(team, sessionId);
|
||||
if (roots.length === 0) throw new AcpError(INVALID_PARAMS, `No session ${sessionId} from ${team.caller} on this machine`);
|
||||
const session: Session = { id: sessionId, cwd: String(p.cwd ?? ""), sent: [] };
|
||||
const asks = await team.queue.sessionAsks(team.caller, sessionId);
|
||||
const asks = (await team.queue.sessionAsks(team.caller, sessionId)).sort((a, b) => a.tsCreated.localeCompare(b.tsCreated));
|
||||
// Replay: each prompt as the caller wrote it, then its work as it stands now, the questions the team asked during
|
||||
// it with what's recorded, and the reply. Nothing is asked again here; an open question comes up in the next turn.
|
||||
for (const [i, row] of roots.entries()) {
|
||||
|
||||
@@ -134,12 +134,13 @@ describe("openrig-acp hands everything to rig acp serve", () => {
|
||||
expect(app.err()).toMatch(/^openrig-acp: @openrig\/cli isn't installed beside this package/);
|
||||
});
|
||||
|
||||
it("an @openrig/cli of another version is refused on stderr, with nothing on stdout and exit 1", async () => {
|
||||
const app = run(install(true, "0.0.0"), []);
|
||||
it("an @openrig/cli of another version is named on stderr and run anyway, with the arguments and exit code passed", async () => {
|
||||
const root = install(true, "0.0.0");
|
||||
const app = run(root, ["--to", "lead@team"], { FAKE_EXIT: "3" });
|
||||
app.child.stdin.end();
|
||||
expect(await app.done).toEqual({ code: 1, signal: null });
|
||||
expect(app.out()).toBe("");
|
||||
expect(await app.done).toEqual({ code: 3, signal: null });
|
||||
expect(JSON.parse(app.out().trim().split("\n")[0]!).argv).toEqual(["acp", "serve", "--to", "lead@team"]);
|
||||
expect(app.err()).toMatch(/^openrig-acp: found @openrig\/cli 0\.0\.0 at /);
|
||||
expect(app.err()).toContain(`but this package runs exactly ${cli.version}. Reinstall @openrig/acp.`);
|
||||
expect(app.err()).toContain(`not the ${cli.version} this package pins; running it anyway. Reinstall @openrig/acp to get ${cli.version}.`);
|
||||
});
|
||||
});
|
||||
@@ -541,6 +541,36 @@ describe("rig acp serve", () => {
|
||||
expect(asked).toEqual([]);
|
||||
});
|
||||
|
||||
it("a reopened session shows, in the order asked, a question closed unanswered as closed and one still open as pending", async () => {
|
||||
const first = connect({}, "asker8@team");
|
||||
const sessionId = await open(first.client);
|
||||
const turn = first.client.request("session/prompt", prompt(sessionId, "Plan the release"));
|
||||
const task = await until(() => rowsTo("asker8@team")[0], "the work row");
|
||||
await move("asker8@team", `/api/queue/${task.qitem_id}/claim`, {});
|
||||
await move("asker8@team", `/api/queue/${task.qitem_id}/update`, { state: "done", closureReason: "no-follow-on", resolution: "Planned." });
|
||||
await turn;
|
||||
// Asked after the turn ended, so no live turn puts them to the app.
|
||||
const choices = [{ id: "y", label: "Yes" }, { id: "n", label: "No" }];
|
||||
await move("asker8@team", "/api/queue/create", { qitemId: "qitem-ask-closed-unanswered", destinationSession: CALLER, body: "Freeze on Friday?", summary: "Freeze?", nudge: false,
|
||||
tags: [`acp-session:${sessionId}`], humanQuestions: [{ id: "q1", question: "Freeze on Friday?", options: choices }] });
|
||||
await move(CALLER, "/api/queue/qitem-ask-closed-unanswered/claim", {});
|
||||
await move(CALLER, "/api/queue/qitem-ask-closed-unanswered/update", { state: "done", closureReason: "no-follow-on", resolution: "Moot now." });
|
||||
await move("asker8@team", "/api/queue/create", { qitemId: "qitem-ask-still-open", destinationSession: CALLER, body: "Tag it 1.0?", summary: "Tag?", nudge: false,
|
||||
tags: [`acp-session:${sessionId}`], humanQuestions: [{ id: "q1", question: "Tag it 1.0?", options: choices }] });
|
||||
|
||||
const second = connect({}, "asker8@team");
|
||||
const asked: unknown[] = [];
|
||||
second.client.onRequest("session/request_permission", async (params) => { asked.push(params); return { outcome: { outcome: "cancelled" } }; });
|
||||
await second.client.request("initialize", { protocolVersion: 1, clientCapabilities: {} });
|
||||
expect(await second.client.request("session/load", { sessionId, cwd: "/tmp", mcpServers: [] })).toEqual({});
|
||||
const questions = second.updates.filter((u) => u.sessionUpdate === "tool_call" && String(u.toolCallId).includes("#"));
|
||||
expect(questions.map((u) => [u.toolCallId, u.status, (u.content as Array<{ content: { text: string } }>).at(-1)!.content.text])).toEqual([
|
||||
["qitem-ask-closed-unanswered#q1", "completed", "Not answered: qitem-ask-closed-unanswered was closed (done)."],
|
||||
["qitem-ask-still-open#q1", "pending", "Not answered yet. It's still open in the OpenRig TUI and Slack."],
|
||||
]);
|
||||
expect(asked).toEqual([]);
|
||||
});
|
||||
|
||||
it("a connection's notice is said once in each session, before its first prompt's work", async () => {
|
||||
const team: TeamConnection = { queue: daemonTeamQueue(new DaemonClient(url(), { sessionName: CALLER })), caller: CALLER, receiver: "noticed@team", notice: "Your team can't ask you questions here yet." };
|
||||
const { client, updates } = connect({ connect: async () => team });
|
||||
|
||||
Reference in new issue
Block a user