mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-07 05:56:21 -10:00
feat(plugins): an openrig plugin for a person's own coding agent (#2647)
Adds packages/daemon/assets/plugins/openrig beside openrig-core, shipped in the npm package. Its Claude Code manifest runs rig mcp serve --front-door with OPENRIG_SESSION_NAME from the plugin's seat setting, the app's address on the person's team as a hosted member; there is no root .mcp.json and no hooks. A marketplace manifest named openrig-app installs it from the package already on the machine, and a Codex manifest connects it with one command. One new skill, working-with-an-openrig-team, describes the loop around the existing MCP tools; its home is the spec copy, and mirror-skills generates the canonical and plugin copies and routes it in the skills index. rig mcp serve --front-door gives the plugin's seat front-door instructions (the person's questions and words, with no member work-loop authority), and rig_whoami answers a declared hosted seat with no session when its rig exists; the default instruction modes and tool exposure are unchanged.
This commit is contained in:
1 parent
c26af88dc7
commit
772fa5c5fb
20 files changed
+726
-20
No files matched your search
@@ -28,6 +28,7 @@ export function mcpCommand(depsOverride?: StatusDeps): Command {
|
|||||||
.option("--task-destination <seat>", "With --http --grant-service: operator-configured local team seat for external requests that omit destination")
|
.option("--task-destination <seat>", "With --http --grant-service: operator-configured local team seat for external requests that omit destination")
|
||||||
.option("--oauth-resource <url>", "With --grant-service and --oauth-issuer: the public https URL clients reach this server as (its path ends in /mcp), for OAuth discovery")
|
.option("--oauth-resource <url>", "With --grant-service and --oauth-issuer: the public https URL clients reach this server as (its path ends in /mcp), for OAuth discovery")
|
||||||
.option("--oauth-issuer <url>", "With --oauth-resource: the OAuth authorization server that issues the keys --grant-service checks")
|
.option("--oauth-issuer <url>", "With --oauth-resource: the OAuth authorization server that issues the keys --grant-service checks")
|
||||||
|
.option("--front-door", "Serve a person's agent in their own app: it hands the team work and brings the team's questions to the person, instead of working its inbox as a member")
|
||||||
.addHelpText("after", `
|
.addHelpText("after", `
|
||||||
Both MCP protocol revisions are served, over stdio and --http alike: clients on 2026-07-28 (which may start with
|
Both MCP protocol revisions are served, over stdio and --http alike: clients on 2026-07-28 (which may start with
|
||||||
server/discover) and clients that open with 2025's initialize handshake.
|
server/discover) and clients that open with 2025's initialize handshake.
|
||||||
@@ -57,7 +58,7 @@ server that issues the keys --grant-service checks. GET /.well-known/oauth-prote
|
|||||||
with both, exactly as given, without a credential or a call to the grant service, and every 401 points there. Both must
|
with both, exactly as given, without a credential or a call to the grant service, and every 401 points there. Both must
|
||||||
be https with no userinfo, query or fragment, written as a URL writes them (a lowercase host, no default port). /mcp
|
be https with no userinfo, query or fragment, written as a URL writes them (a lowercase host, no default port). /mcp
|
||||||
stays the endpoint; a proxy in front forwards the metadata path unchanged.`)
|
stays the endpoint; a proxy in front forwards the metadata path unchanged.`)
|
||||||
.action(async (opts: { port?: string; http?: boolean; bind?: string; grantService?: string; grantServiceHeaderFile?: string; taskDestination?: string; oauthResource?: string; oauthIssuer?: string }) => {
|
.action(async (opts: { port?: string; http?: boolean; bind?: string; grantService?: string; grantServiceHeaderFile?: string; taskDestination?: string; oauthResource?: string; oauthIssuer?: string; frontDoor?: boolean }) => {
|
||||||
const deps = getDepsF();
|
const deps = getDepsF();
|
||||||
|
|
||||||
let daemonUrl: string;
|
let daemonUrl: string;
|
||||||
@@ -105,7 +106,7 @@ stays the endpoint; a proxy in front forwards the metadata path unchanged.`)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (opts.http) {
|
if (opts.http) {
|
||||||
await serveHttp(daemonUrl, opts.bind ?? "127.0.0.1:7437", opts.grantService, opts.grantServiceHeaderFile, oauth, opts.taskDestination);
|
await serveHttp(daemonUrl, opts.bind ?? "127.0.0.1:7437", opts.grantService, opts.grantServiceHeaderFile, oauth, opts.taskDestination, opts.frontDoor === true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const client = deps.clientFactory(daemonUrl);
|
const client = deps.clientFactory(daemonUrl);
|
||||||
@@ -115,7 +116,7 @@ stays the endpoint; a proxy in front forwards the metadata path unchanged.`)
|
|||||||
process.once("SIGINT", finish);
|
process.once("SIGINT", finish);
|
||||||
process.once("SIGTERM", finish);
|
process.once("SIGTERM", finish);
|
||||||
// Serves the protocol revision the client opens with: 2026-07-28 per request, or 2025's initialize handshake.
|
// Serves the protocol revision the client opens with: 2026-07-28 per request, or 2025's initialize handshake.
|
||||||
const served = serveStdio(() => createMcpServer(client), { onerror: (err) => console.error(`rig mcp serve: ${err.message}`) });
|
const served = serveStdio(() => createMcpServer(client, { frontDoor: opts.frontDoor === true }), { onerror: (err) => console.error(`rig mcp serve: ${err.message}`) });
|
||||||
try {
|
try {
|
||||||
// EOF is a normal stdio disconnect, not an unresolved top-level await.
|
// EOF is a normal stdio disconnect, not an unresolved top-level await.
|
||||||
if (process.stdin.readableEnded) finish();
|
if (process.stdin.readableEnded) finish();
|
||||||
@@ -182,7 +183,7 @@ stays the endpoint; a proxy in front forwards the metadata path unchanged.`)
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function serveHttp(daemonUrl: string, bind: string, grantService?: string, grantServiceHeaderFile?: string,
|
async function serveHttp(daemonUrl: string, bind: string, grantService?: string, grantServiceHeaderFile?: string,
|
||||||
oauth?: { resource: string; issuer: string }, taskDestination?: string): Promise<void> {
|
oauth?: { resource: string; issuer: string }, taskDestination?: string, frontDoor = false): Promise<void> {
|
||||||
const match = /^(\[[^\]]+\]|[^:]+):(\d+)$/.exec(bind);
|
const match = /^(\[[^\]]+\]|[^:]+):(\d+)$/.exec(bind);
|
||||||
if (!match) {
|
if (!match) {
|
||||||
console.error(`--bind takes host:port (for example 127.0.0.1:7437 or [::1]:7437), not ${bind}`);
|
console.error(`--bind takes host:port (for example 127.0.0.1:7437 or [::1]:7437), not ${bind}`);
|
||||||
@@ -194,7 +195,7 @@ async function serveHttp(daemonUrl: string, bind: string, grantService?: string,
|
|||||||
let served;
|
let served;
|
||||||
try {
|
try {
|
||||||
const header = grantServiceHeaderFile ? readGrantServiceHeader(grantServiceHeaderFile) : undefined;
|
const header = grantServiceHeaderFile ? readGrantServiceHeader(grantServiceHeaderFile) : undefined;
|
||||||
served = await startMcpHttpServer({ daemonUrl, host, port: Number(match[2]), tokenFile, taskDestination,
|
served = await startMcpHttpServer({ daemonUrl, host, port: Number(match[2]), tokenFile, taskDestination, frontDoor,
|
||||||
...(grantService ? { grantService: { url: grantService, ...(header ? { header } : {}) } } : {}), ...(oauth ? { oauth } : {}) });
|
...(grantService ? { grantService: { url: grantService, ...(header ? { header } : {}) } } : {}), ...(oauth ? { oauth } : {}) });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err instanceof McpHttpConfigError ? err.message : `Can't serve on ${bind}: ${(err as Error).message}`);
|
console.error(err instanceof McpHttpConfigError ? err.message : `Can't serve on ${bind}: ${(err as Error).message}`);
|
||||||
|
|||||||
@@ -312,6 +312,8 @@ export type McpHttpOptions = {
|
|||||||
grantService?: GrantServiceOptions;
|
grantService?: GrantServiceOptions;
|
||||||
/** Operator-configured receiver for external callers omitting a task destination; never an authority grant. */
|
/** Operator-configured receiver for external callers omitting a task destination; never an authority grant. */
|
||||||
taskDestination?: string;
|
taskDestination?: string;
|
||||||
|
/** rig mcp serve --front-door: a caller that isn't external gets the front-door instructions, not the member workflow. */
|
||||||
|
frontDoor?: boolean;
|
||||||
/** With grantService: OAuth discovery for clients that sign in with OAuth. The public resource this server is reached
|
/** With grantService: OAuth discovery for clients that sign in with OAuth. The public resource this server is reached
|
||||||
* as and the authorization server that issues its keys, both served exactly as given. */
|
* as and the authorization server that issues its keys, both served exactly as given. */
|
||||||
oauth?: { resource: string; issuer: string };
|
oauth?: { resource: string; issuer: string };
|
||||||
@@ -391,7 +393,7 @@ export async function startMcpHttpServer(opts: McpHttpOptions): Promise<{ url: s
|
|||||||
// Never a default seat: a request reaches the handler only with its caller decided.
|
// Never a default seat: a request reaches the handler only with its caller decided.
|
||||||
if (!caller) throw new Error("a request reached the MCP handler without its caller");
|
if (!caller) throw new Error("a request reached the MCP handler without its caller");
|
||||||
return createMcpServer(makeClient(opts.daemonUrl, caller.seat, caller.principal), { seat: caller.seat, access: caller.access,
|
return createMcpServer(makeClient(opts.daemonUrl, caller.seat, caller.principal), { seat: caller.seat, access: caller.access,
|
||||||
taskDestination: opts.taskDestination, presenceFile: () => presenceFile });
|
taskDestination: opts.taskDestination, presenceFile: () => presenceFile, frontDoor: opts.frontDoor === true });
|
||||||
});
|
});
|
||||||
const serveMcp = toNodeHandler(mcpHandler);
|
const serveMcp = toNodeHandler(mcpHandler);
|
||||||
const bearer = (req: http.IncomingMessage): string | undefined => /^Bearer\s+(\S+)\s*$/i.exec(req.headers.authorization ?? "")?.[1];
|
const bearer = (req: http.IncomingMessage): string | undefined => /^Bearer\s+(\S+)\s*$/i.exec(req.headers.authorization ?? "")?.[1];
|
||||||
|
|||||||
@@ -572,6 +572,17 @@ const SHARED_INSTRUCTIONS = [
|
|||||||
"A question reserved for a person is theirs to answer: don't answer it on their behalf. rig_queue_decide records a decision only inside the decision room of the mode they have on, beside their answers and never as one.",
|
"A question reserved for a person is theirs to answer: don't answer it on their behalf. rig_queue_decide records a decision only inside the decision room of the mode they have on, beside their answers and never as one.",
|
||||||
];
|
];
|
||||||
const INSTRUCTIONS = [...SHARED_INSTRUCTIONS, MEMBER_WORKFLOW].join("\n");
|
const INSTRUCTIONS = [...SHARED_INSTRUCTIONS, MEMBER_WORKFLOW].join("\n");
|
||||||
|
|
||||||
|
/** rig mcp serve --front-door: the seat is a person's agent in their own app, the front door to their team, not a member
|
||||||
|
* that works its inbox. The team's questions to it are the person's to answer. */
|
||||||
|
export const FRONT_DOOR_WORKFLOW_VERSION = "front-door/v1";
|
||||||
|
export const FRONT_DOOR_WORKFLOW = [
|
||||||
|
`Front door (${FRONT_DOOR_WORKFLOW_VERSION}). This seat is a person's agent in their own app: you hand the team their work and bring back what it produced, and you aren't one of its members.`,
|
||||||
|
"Items addressed to your seat (rig_queue_list with owned set) are usually the team's questions about work you handed it. They're the person's to answer, not yours: show each to the person with the team's draft, and record only what the person tells you.",
|
||||||
|
"To record an answer, claim the item (rig_queue_claim), then close it done with closure reason no-follow-on and the person's words in resolution, saying they're the person's (rig_queue_update). If the person isn't there, leave the item open and tell them it's waiting. Never choose an answer for them, and don't claim an item you can't close with their words.",
|
||||||
|
"Nothing wakes this conversation when the team finishes: check your work and your items when the person asks or comes back.",
|
||||||
|
].join(" ");
|
||||||
|
const FRONT_DOOR_INSTRUCTIONS = [...SHARED_INSTRUCTIONS, FRONT_DOOR_WORKFLOW].join("\n");
|
||||||
const EXTERNAL_INSTRUCTIONS = [
|
const EXTERNAL_INSTRUCTIONS = [
|
||||||
"This connection sends requests to an OpenRig team as the caller named by its grant. The daemon attributes requests to that caller. Use only the tools this connection offers.",
|
"This connection sends requests to an OpenRig team as the caller named by its grant. The daemon attributes requests to that caller. Use only the tools this connection offers.",
|
||||||
"To ask the connected team to do work, use rig_queue_create with the task in body. You may omit destination to use the receiver configured by the server operator. If no receiver is available, ask the operator to configure it; don't guess an address. An explicit destination still must be within this connection's grant.",
|
"To ask the connected team to do work, use rig_queue_create with the task in body. You may omit destination to use the receiver configured by the server operator. If no receiver is available, ask the operator to configure it; don't guess an address. An explicit destination still must be within this connection's grant.",
|
||||||
@@ -584,8 +595,8 @@ const EXTERNAL_INBOX_INSTRUCTIONS = [...SHARED_INSTRUCTIONS,
|
|||||||
"Items addressed to your seat are yours: rig_queue_list with owned set lists them. For each, claim it (rig_queue_claim), do it, then close it done with closure reason no-follow-on and the answer in resolution (rig_queue_update); the sender reads that answer on the same item. A message is an item too: read it, close it done, and reply, when it needs one, with a new item to its sender. Nothing promises to wake you, so check your items again.",
|
"Items addressed to your seat are yours: rig_queue_list with owned set lists them. For each, claim it (rig_queue_claim), do it, then close it done with closure reason no-follow-on and the answer in resolution (rig_queue_update); the sender reads that answer on the same item. A message is an item too: read it, close it done, and reply, when it needs one, with a new item to its sender. Nothing promises to wake you, so check your items again.",
|
||||||
].join("\n");
|
].join("\n");
|
||||||
|
|
||||||
function instructionsFor(access: McpAccess | undefined): string {
|
function instructionsFor(access: McpAccess | undefined, frontDoor = false): string {
|
||||||
if (!access?.external) return INSTRUCTIONS;
|
if (!access?.external) return frontDoor ? FRONT_DOOR_INSTRUCTIONS : INSTRUCTIONS;
|
||||||
const tools = externalTools(access.external.objects);
|
const tools = externalTools(access.external.objects);
|
||||||
return tools.has("rig_queue_claim") && tools.has("rig_queue_update") ? EXTERNAL_INBOX_INSTRUCTIONS : EXTERNAL_INSTRUCTIONS;
|
return tools.has("rig_queue_claim") && tools.has("rig_queue_update") ? EXTERNAL_INBOX_INSTRUCTIONS : EXTERNAL_INSTRUCTIONS;
|
||||||
}
|
}
|
||||||
@@ -656,11 +667,11 @@ function mapResult(
|
|||||||
* @param client - DaemonClient connected to the daemon
|
* @param client - DaemonClient connected to the daemon
|
||||||
* @returns McpServer instance (not yet connected to a transport)
|
* @returns McpServer instance (not yet connected to a transport)
|
||||||
*/
|
*/
|
||||||
export function createMcpServer(client: DaemonClient, options: { seat?: string; access?: McpAccess; taskDestination?: string; rostersFolder?: () => string; presenceFile?: () => string } = {}): McpServer {
|
export function createMcpServer(client: DaemonClient, options: { seat?: string; access?: McpAccess; taskDestination?: string; rostersFolder?: () => string; presenceFile?: () => string; frontDoor?: boolean } = {}): McpServer {
|
||||||
const server = new McpServer({
|
const server = new McpServer({
|
||||||
name: "openrig",
|
name: "openrig",
|
||||||
version: CLI_VERSION,
|
version: CLI_VERSION,
|
||||||
}, { instructions: instructionsFor(options.access) });
|
}, { instructions: instructionsFor(options.access, options.frontDoor === true) });
|
||||||
const { access } = options;
|
const { access } = options;
|
||||||
const rostersFolder = options.rostersFolder ?? (() => nodePath.join(new ConfigStore().get("workspace.root") as string, "rosters"));
|
const rostersFolder = options.rostersFolder ?? (() => nodePath.join(new ConfigStore().get("workspace.root") as string, "rosters"));
|
||||||
const presenceFile = options.presenceFile ?? (() => mcpPresenceFilePath());
|
const presenceFile = options.presenceFile ?? (() => mcpPresenceFilePath());
|
||||||
@@ -1541,6 +1552,17 @@ export function createMcpServer(client: DaemonClient, options: { seat?: string;
|
|||||||
message: "This seat has no session here: it's the seat this connection's grant names, such as a hosted member's. rig_team shows its rig and hosted members, and rig_queue_list with owned set lists its work." };
|
message: "This seat has no session here: it's the seat this connection's grant names, such as a hosted member's. rig_team shows its rig and hosted members, and rig_queue_list with owned set lists its work." };
|
||||||
return { content: [{ type: "text" as const, text: JSON.stringify(member) }], structuredContent: member };
|
return { content: [{ type: "text" as const, text: JSON.stringify(member) }], structuredContent: member };
|
||||||
}
|
}
|
||||||
|
// A front door (--front-door) usually has no session either: its seat is the address the person picked for their
|
||||||
|
// app, declared in OPENRIG_SESSION_NAME. Answer with it when its rig exists here, still marked declared.
|
||||||
|
if (res.status === 404 && !access && options.frontDoor === true) {
|
||||||
|
const at = seat.indexOf("@");
|
||||||
|
const rigName = at > 0 ? seat.slice(at + 1) : "";
|
||||||
|
if (rigName && (await resolveRigHandle(client, rigName, { strict: true })).kind === "resolved") {
|
||||||
|
const frontDoor = { ok: true, seat, rig: rigName, session: null, caller,
|
||||||
|
message: "This seat has no session here: it's the front-door address this server was configured with (OPENRIG_SESSION_NAME), declared, not verified. rig_team shows its rig and lead, and rig_queue_list with owned set lists the team's questions to it." };
|
||||||
|
return { content: [{ type: "text" as const, text: JSON.stringify(frontDoor) }], structuredContent: frontDoor };
|
||||||
|
}
|
||||||
|
}
|
||||||
if (res.status >= 400) return mapResult(res);
|
if (res.status >= 400) return mapResult(res);
|
||||||
return mapResult({ ...res, data: { ...(res.data as Record<string, unknown>), caller } }, undefined, { structured: true });
|
return mapResult({ ...res, data: { ...(res.data as Record<string, unknown>), caller } }, undefined, { structured: true });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import os from "node:os";
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
||||||
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
|
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
|
||||||
import { createMcpServer, MEMBER_WORKFLOW, MEMBER_WORKFLOW_VERSION, type McpAccess } from "../src/mcp-server.js";
|
import { createMcpServer, FRONT_DOOR_WORKFLOW, FRONT_DOOR_WORKFLOW_VERSION, MEMBER_WORKFLOW, MEMBER_WORKFLOW_VERSION, type McpAccess } from "../src/mcp-server.js";
|
||||||
import { DaemonClient, DaemonConnectionError, type DaemonResponse } from "../src/client.js";
|
import { DaemonClient, DaemonConnectionError, type DaemonResponse } from "../src/client.js";
|
||||||
|
|
||||||
function mockClient(overrides?: {
|
function mockClient(overrides?: {
|
||||||
@@ -24,9 +24,9 @@ describe("MCP Server", () => {
|
|||||||
let daemonClient: DaemonClient;
|
let daemonClient: DaemonClient;
|
||||||
let cleanup: () => Promise<void>;
|
let cleanup: () => Promise<void>;
|
||||||
|
|
||||||
async function setup(clientOverrides?: Parameters<typeof mockClient>[0]) {
|
async function setup(clientOverrides?: Parameters<typeof mockClient>[0], serverOptions?: Parameters<typeof createMcpServer>[1]) {
|
||||||
daemonClient = mockClient(clientOverrides);
|
daemonClient = mockClient(clientOverrides);
|
||||||
const server = createMcpServer(daemonClient);
|
const server = createMcpServer(daemonClient, serverOptions);
|
||||||
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
||||||
|
|
||||||
mcpClient = new Client({ name: "test-client", version: "1.0.0" });
|
mcpClient = new Client({ name: "test-client", version: "1.0.0" });
|
||||||
@@ -327,6 +327,45 @@ describe("MCP Server", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("a front door gets front-door instructions, not the member workflow, and its declared seat with no session reads as itself", async () => {
|
||||||
|
try {
|
||||||
|
vi.stubEnv("OPENRIG_SESSION_NAME", "claude-app@my-rig");
|
||||||
|
vi.stubEnv("RIGGED_SESSION_NAME", "");
|
||||||
|
vi.stubEnv("OPENRIG_TARGET_REPO", "");
|
||||||
|
vi.stubEnv("RIGGED_TARGET_REPO", "");
|
||||||
|
// A hosted address has no session, so whoami answers 404; the rig itself exists.
|
||||||
|
const getFn = vi.fn(async (p: string) => p.startsWith("/api/whoami") ? { status: 404, data: { error: "session not found" } }
|
||||||
|
: p.startsWith("/api/rigs/summary") ? { status: 200, data: [{ id: "rig-1", name: "my-rig", archivedAt: null }] } : { status: 404, data: {} });
|
||||||
|
await setup({ get: getFn }, { frontDoor: true });
|
||||||
|
const instructions = mcpClient.getInstructions() ?? "";
|
||||||
|
expect(FRONT_DOOR_WORKFLOW).toContain(`(${FRONT_DOOR_WORKFLOW_VERSION})`);
|
||||||
|
expect(instructions).toContain(FRONT_DOOR_WORKFLOW);
|
||||||
|
// The person's harm first: nothing tells this agent to work its inbox without asking.
|
||||||
|
expect(instructions).not.toContain(MEMBER_WORKFLOW);
|
||||||
|
expect(instructions).not.toContain("without asking whether you may");
|
||||||
|
for (const part of ["the person's to answer", "record only what the person tells you", "Never choose an answer for them"]) expect(FRONT_DOOR_WORKFLOW).toContain(part);
|
||||||
|
const front = await mcpClient.callTool({ name: "rig_whoami", arguments: {} });
|
||||||
|
expect(front.isError).toBeFalsy();
|
||||||
|
expect(front.structuredContent).toMatchObject({ ok: true, seat: "claude-app@my-rig", rig: "my-rig", session: null,
|
||||||
|
caller: { authenticated: false, identity: "declared" } });
|
||||||
|
await cleanup();
|
||||||
|
|
||||||
|
// Without --front-door, the same server is a member: the member workflow, and whoami's 404 as today.
|
||||||
|
await setup({ get: getFn });
|
||||||
|
expect(mcpClient.getInstructions() ?? "").toContain(MEMBER_WORKFLOW);
|
||||||
|
expect((await mcpClient.callTool({ name: "rig_whoami", arguments: {} })).isError).toBe(true);
|
||||||
|
await cleanup();
|
||||||
|
|
||||||
|
// A front door whose rig isn't here is a misconfiguration, still answered with the 404.
|
||||||
|
vi.stubEnv("OPENRIG_SESSION_NAME", "claude-app@no-such-rig");
|
||||||
|
await setup({ get: getFn }, { frontDoor: true });
|
||||||
|
expect((await mcpClient.callTool({ name: "rig_whoami", arguments: {} })).isError).toBe(true);
|
||||||
|
await cleanup();
|
||||||
|
} finally {
|
||||||
|
vi.unstubAllEnvs();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("the queue tools return structured content; every error a tool returns carries one shape and keeps its old text", async () => {
|
it("the queue tools return structured content; every error a tool returns carries one shape and keeps its old text", async () => {
|
||||||
// The queue's missing-item read answers with its code alone, as GET /api/queue/:qitemId does.
|
// The queue's missing-item read answers with its code alone, as GET /api/queue/:qitemId does.
|
||||||
const missing = { error: "qitem_not_found" };
|
const missing = { error: "qitem_not_found" };
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ These ship in the OpenRig repo and reach a seat when its profile selects them. T
|
|||||||
- **watchdog** — monitoring or recovering a seat (health, restore, stuck state).
|
- **watchdog** — monitoring or recovering a seat (health, restore, stuck state).
|
||||||
- **session-source-fork** — forking a session's source/context.
|
- **session-source-fork** — forking a session's source/context.
|
||||||
- **context-engineering** — designing context systems. Provisional and non-normative: current OpenRig skills, explicit rulings, and measured practice outrank it on any conflict.
|
- **context-engineering** — designing context systems. Provisional and non-normative: current OpenRig skills, explicit rulings, and measured practice outrank it on any conflict.
|
||||||
|
- **working-with-an-openrig-team** — a person's own agent in an app (Claude Code, Codex, Cursor, VS Code) works with their team through `rig mcp serve`: hand work over, follow it, answer the team's questions, read results. The `openrig` app plugin loads it; a seat helping someone connect an app can open it with `rig context get skills/core/working-with-an-openrig-team/SKILL.md`.
|
||||||
|
|
||||||
Pod handbooks (load when you're in that pod):
|
Pod handbooks (load when you're in that pod):
|
||||||
- **orchestration-team** — you're orchestrating a rig: dispatching, monitoring, keeping the loop moving.
|
- **orchestration-team** — you're orchestrating a rig: dispatching, monitoring, keeping the loop moving.
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"name": "openrig-app",
|
||||||
|
"description": "The OpenRig plugin, installed from your OpenRig package: work with your OpenRig team from this app.",
|
||||||
|
"owner": {
|
||||||
|
"name": "Mike Schwarz",
|
||||||
|
"url": "https://github.com/mvschwarz"
|
||||||
|
},
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "openrig",
|
||||||
|
"source": "./",
|
||||||
|
"description": "Work with your OpenRig team from your coding agent: hand it work, follow that work, answer its questions and read what it returns."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"name": "openrig",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Work with your OpenRig team from your coding agent: hand it work, follow that work, answer its questions and read what it returns.",
|
||||||
|
"author": {
|
||||||
|
"name": "Mike Schwarz",
|
||||||
|
"url": "https://github.com/mvschwarz"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/mvschwarz/openrig",
|
||||||
|
"repository": "github:mvschwarz/openrig",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"keywords": ["openrig", "agents", "team", "delegation", "mcp"],
|
||||||
|
"skills": "./skills",
|
||||||
|
"mcpServers": {
|
||||||
|
"openrig": {
|
||||||
|
"command": "rig",
|
||||||
|
"args": ["mcp", "serve", "--front-door"],
|
||||||
|
"env": {
|
||||||
|
"OPENRIG_SESSION_NAME": "${user_config.seat}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"userConfig": {
|
||||||
|
"seat": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "OpenRig seat",
|
||||||
|
"description": "This app's address on your OpenRig team, such as cursor@my-team. Your requests are recorded as coming from it, and the team's questions about them come back to it.",
|
||||||
|
"required": false,
|
||||||
|
"default": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "openrig",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Work with your OpenRig team from your coding agent: hand it work, follow that work, answer its questions and read what it returns.",
|
||||||
|
"author": {
|
||||||
|
"name": "Mike Schwarz",
|
||||||
|
"url": "https://github.com/mvschwarz"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/mvschwarz/openrig",
|
||||||
|
"repository": "github:mvschwarz/openrig",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"keywords": ["openrig", "agents", "team", "delegation", "mcp"],
|
||||||
|
"skills": "./skills",
|
||||||
|
"interface": {
|
||||||
|
"displayName": "OpenRig",
|
||||||
|
"shortDescription": "Work with your OpenRig team",
|
||||||
|
"longDescription": "Hand your OpenRig team work from this agent, follow it, answer the team's questions and read what it returns, through OpenRig's MCP server. Tools and guidance only: it adds no panel, and nothing wakes a chat when the team finishes.",
|
||||||
|
"developerName": "Mike Schwarz",
|
||||||
|
"category": "Productivity",
|
||||||
|
"capabilities": ["Read"],
|
||||||
|
"websiteURL": "https://github.com/mvschwarz/openrig"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,200 @@
|
|||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for describing the origin of the Work and
|
||||||
|
reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Support. While redistributing the Work or Derivative Works thereof,
|
||||||
|
You may choose to offer, and charge a fee for, acceptance of support,
|
||||||
|
warranty, indemnity, or other liability obligations and/or rights
|
||||||
|
consistent with this License. However, in accepting such obligations,
|
||||||
|
You may act only on Your own behalf and on Your sole responsibility,
|
||||||
|
not on behalf of any other Contributor, and only if You agree to
|
||||||
|
indemnify, defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or support.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright 2026 Mike Schwarz
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
# openrig
|
||||||
|
|
||||||
|
Work with your OpenRig team from the coding agent you already use. Your agent can hand the team work, follow that work,
|
||||||
|
bring you the team's questions and read what it returns, through OpenRig's MCP server.
|
||||||
|
|
||||||
|
This plugin is for a person's own agent in an app such as Claude Code or Codex. The agents on an OpenRig team load
|
||||||
|
`openrig-core` instead.
|
||||||
|
|
||||||
|
## What it ships
|
||||||
|
|
||||||
|
- **The MCP server entry**, in the Claude Code manifest only: `rig mcp serve --front-door`, acting as the seat you set in
|
||||||
|
the plugin's settings. Claude Code substitutes that seat into the server's environment. Codex doesn't, so the plugin
|
||||||
|
ships no shared MCP file, and Codex gets the server from one command (below).
|
||||||
|
- **`--front-door`** tells your agent the team's questions to its seat are yours to answer: it shows them to you and
|
||||||
|
records only what you say, instead of working them itself as a team member would.
|
||||||
|
- **One skill,** `working-with-an-openrig-team`: the loop around the tools. It covers handing work to the team's lead,
|
||||||
|
following it along its handoffs, answering the team's questions with your own words, reading the result and coming
|
||||||
|
back to it later.
|
||||||
|
- **No hooks.** Nothing runs on your agent's prompts or sessions.
|
||||||
|
|
||||||
|
## Before you install
|
||||||
|
|
||||||
|
1. OpenRig is installed and runs a team on this machine: `rig ps` lists it.
|
||||||
|
2. Pick an address for this app on that team, such as `cursor@my-team`. The queue takes work for it; nothing is
|
||||||
|
launched. To tell the team what it's for, add it to a roster as a hosted member (`docs/reference/hosted-members.md`).
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
The plugin ships inside the OpenRig package, so you install it from there. Its marketplace is named `openrig-app`, because
|
||||||
|
OpenRig already uses `openrig` for the marketplace its own Codex seats load `openrig-core` from.
|
||||||
|
|
||||||
|
The commands below use `$(npm root -g)`, npm's global folder. If you installed OpenRig with another prefix, use that
|
||||||
|
prefix's `lib/node_modules` instead.
|
||||||
|
|
||||||
|
**Claude Code**
|
||||||
|
|
||||||
|
```sh
|
||||||
|
claude plugin marketplace add "$(npm root -g)/@openrig/cli/daemon/assets/plugins/openrig"
|
||||||
|
claude plugin install openrig@openrig-app
|
||||||
|
```
|
||||||
|
|
||||||
|
Set the plugin's **OpenRig seat** to your address (`/plugin`, then configure): type the address into the row, then save.
|
||||||
|
Pressing Enter on the empty row closes it with no change. Then start a new session.
|
||||||
|
|
||||||
|
**Codex**
|
||||||
|
|
||||||
|
Install the plugin for its skill, then connect the MCP server with your address:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
codex plugin marketplace add "$(npm root -g)/@openrig/cli/daemon/assets/plugins/openrig"
|
||||||
|
codex plugin add openrig@openrig-app
|
||||||
|
codex mcp add openrig --env OPENRIG_SESSION_NAME=<address> -- rig mcp serve --front-door
|
||||||
|
```
|
||||||
|
|
||||||
|
- **A non-default OpenRig home:** Codex does not forward every shell environment variable to MCP servers by default. If
|
||||||
|
your team runs on a non-default OpenRig home, add `--env OPENRIG_HOME=<that home>` to the `codex mcp add` line.
|
||||||
|
- **Approvals:** Codex may ask you to approve tools according to its approval settings. Create, claim and update change
|
||||||
|
team state; tools that only read are marked read-only.
|
||||||
|
|
||||||
|
## What it doesn't do
|
||||||
|
|
||||||
|
- It adds no panel to the app, and nothing wakes your chat when the team finishes: ask your agent to check.
|
||||||
|
- Everyone using the app on this machine acts as the seat it names.
|
||||||
|
- It reaches a team on this machine.
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
---
|
||||||
|
name: working-with-an-openrig-team
|
||||||
|
description: >-
|
||||||
|
Use when you are a person's coding agent in an app such as Claude Code, Codex, Cursor or VS Code, connected through the
|
||||||
|
OpenRig MCP server, and the person wants their OpenRig team to do some work, wants to know how that work is going,
|
||||||
|
has a question from the team to answer, or wants to read what the team returned.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Working with an OpenRig team from your app
|
||||||
|
|
||||||
|
The person runs a team of agents with OpenRig: each agent has its own terminal, and they keep working after this chat ends.
|
||||||
|
You aren't one of those terminals. You're the person's front door to the team: you hand it work, follow that work, bring
|
||||||
|
the team's questions to the person, and bring back what it produced. The MCP server's own instructions say how its tools
|
||||||
|
behave; this skill is the loop around them.
|
||||||
|
|
||||||
|
## Check the connection first
|
||||||
|
|
||||||
|
- Call `rig_whoami`. It names the seat this connection acts as. Every item you create is recorded as coming from that seat,
|
||||||
|
and the team's questions about your work come back to it.
|
||||||
|
- If it has no seat, stop and help the person set one (the last section). Without one, your requests are unattributed and you
|
||||||
|
can't list your own work.
|
||||||
|
- Call `rig_team` with the rig's name: it shows the team's seats, what each is for, and its lead. Work goes to the lead
|
||||||
|
unless the person names a seat.
|
||||||
|
- If `lead` is null (its `leadNote` says why), and the team has exactly one seat, send work there. Otherwise, ask the person
|
||||||
|
which seat should take it.
|
||||||
|
|
||||||
|
## Hand the team work
|
||||||
|
|
||||||
|
- Use `rig_queue_create` with `destination` (the lead, or the seat the person chose) and `body`. Write the body for a
|
||||||
|
capable colleague: the goal, why it matters, what done looks like, and where to look. Add a one-line `summary`.
|
||||||
|
- Tell the person the `qitemId` it returns. It's how both of you find this work again.
|
||||||
|
- If the call times out, check `rig_queue_show` with that same `qitemId` before trying again. If you retry, pass the same id
|
||||||
|
as `qitemId`. Never retry with a new one: that creates the work twice.
|
||||||
|
|
||||||
|
## Follow it
|
||||||
|
|
||||||
|
- `rig_queue_show` with the `qitemId` gives its state, its current owner and its transitions.
|
||||||
|
- When a seat hands the work on, the item names the next one. Follow the chain to the newest item before you report.
|
||||||
|
- Report what the record says. An accepted or claimed item isn't finished work, and a quiet one isn't stuck: say what you
|
||||||
|
saw and when it last changed.
|
||||||
|
- Nothing wakes this chat when the team finishes. Check again when the person asks, or when they come back.
|
||||||
|
|
||||||
|
## The team's questions
|
||||||
|
|
||||||
|
- Questions about your work come to your seat: `rig_queue_list` with `owned` lists them.
|
||||||
|
- Show the person the question and the team's draft, in their own words where the item gives them. Then ask for an answer.
|
||||||
|
- Record only what the person actually tells you. Claim the item (`rig_queue_claim`), then close it with
|
||||||
|
`rig_queue_update`: `state` `done`, `closureReason` `no-follow-on`, and the person's answer in `resolution`, saying it's
|
||||||
|
theirs. Put only their words there: no notes, paths or summaries of your own. If the person isn't there to answer, leave
|
||||||
|
the item open and tell them it's waiting. Never choose an answer for them.
|
||||||
|
- Some questions go to the person directly, outside your seat. Those wait for them where they already answer the team,
|
||||||
|
such as the OpenRig TUI or Slack. You can mention that they're waiting; answering them isn't yours.
|
||||||
|
|
||||||
|
## Read the result
|
||||||
|
|
||||||
|
- When the item is `done`, its `resolution` is the answer, and its transitions hold the notes the team recorded.
|
||||||
|
- If it names an `evidenceRef`, read it with `rig_artifact_read`, passing the item's `qitemId`, not the path. The tool
|
||||||
|
reads the item's `evidenceRef` within the shared roots this connection may read. For another file in a shared root,
|
||||||
|
pass the root's name and `path`. If the connection can't read it, give the person the path.
|
||||||
|
- To ask for changes, create a new item with `revises` set to the earlier `qitemId`, so the revision stays with the
|
||||||
|
original work.
|
||||||
|
|
||||||
|
## Come back later
|
||||||
|
|
||||||
|
- `rig_queue_list` with `mine` lists what this seat sent or owns. Add `all` to include finished work, or `since` to see
|
||||||
|
what changed after a time.
|
||||||
|
- A new chat with the same seat reaches the same work. Items outlive conversations.
|
||||||
|
|
||||||
|
## Set it up (once, with the person)
|
||||||
|
|
||||||
|
1. OpenRig runs on this machine with a team: `rig ps` lists the person's rigs.
|
||||||
|
2. Pick an address for this app on one of those rigs, for example `cursor@my-team`. It's an address the queue takes items
|
||||||
|
for; nothing is launched for it. To tell the team what it's for, the person can add it to a roster as a hosted member
|
||||||
|
(`docs/reference/hosted-members.md` in the OpenRig package).
|
||||||
|
3. Connect the app to that address, then start a new session so the MCP server starts with it:
|
||||||
|
- **Claude Code:** set it as the plugin's OpenRig seat in the plugin's settings.
|
||||||
|
- **Codex:** run `codex mcp add openrig --env OPENRIG_SESSION_NAME=<address> -- rig mcp serve --front-door`. Codex
|
||||||
|
does not forward every shell environment variable to MCP servers by default, so if the team's OpenRig home isn't the
|
||||||
|
default, add `--env OPENRIG_HOME=<that home>` too.
|
||||||
|
- **Another app:** add an MCP server that runs `rig mcp serve --front-door` with `OPENRIG_SESSION_NAME` set to the
|
||||||
|
address.
|
||||||
|
|
||||||
|
## What this doesn't do
|
||||||
|
|
||||||
|
- It doesn't add a panel to the app, and nothing wakes this chat when the team finishes.
|
||||||
|
- Anyone using this app on this machine acts as the seat the plugin names.
|
||||||
|
- It reaches a team on this machine. A team hosted elsewhere needs that host's own connection.
|
||||||
@@ -87,6 +87,7 @@ These ship in the OpenRig repo and reach a seat when its profile selects them. T
|
|||||||
- **watchdog** — monitoring or recovering a seat (health, restore, stuck state).
|
- **watchdog** — monitoring or recovering a seat (health, restore, stuck state).
|
||||||
- **session-source-fork** — forking a session's source/context.
|
- **session-source-fork** — forking a session's source/context.
|
||||||
- **context-engineering** — designing context systems. Provisional and non-normative: current OpenRig skills, explicit rulings, and measured practice outrank it on any conflict.
|
- **context-engineering** — designing context systems. Provisional and non-normative: current OpenRig skills, explicit rulings, and measured practice outrank it on any conflict.
|
||||||
|
- **working-with-an-openrig-team** — a person's own agent in an app (Claude Code, Codex, Cursor, VS Code) works with their team through `rig mcp serve`: hand work over, follow it, answer the team's questions, read results. The `openrig` app plugin loads it; a seat helping someone connect an app can open it with `rig context get skills/core/working-with-an-openrig-team/SKILL.md`.
|
||||||
|
|
||||||
Pod handbooks (load when you're in that pod):
|
Pod handbooks (load when you're in that pod):
|
||||||
- **orchestration-team** — you're orchestrating a rig: dispatching, monitoring, keeping the loop moving.
|
- **orchestration-team** — you're orchestrating a rig: dispatching, monitoring, keeping the loop moving.
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
---
|
||||||
|
name: working-with-an-openrig-team
|
||||||
|
description: >-
|
||||||
|
Use when you are a person's coding agent in an app such as Claude Code, Codex, Cursor or VS Code, connected through the
|
||||||
|
OpenRig MCP server, and the person wants their OpenRig team to do some work, wants to know how that work is going,
|
||||||
|
has a question from the team to answer, or wants to read what the team returned.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Working with an OpenRig team from your app
|
||||||
|
|
||||||
|
The person runs a team of agents with OpenRig: each agent has its own terminal, and they keep working after this chat ends.
|
||||||
|
You aren't one of those terminals. You're the person's front door to the team: you hand it work, follow that work, bring
|
||||||
|
the team's questions to the person, and bring back what it produced. The MCP server's own instructions say how its tools
|
||||||
|
behave; this skill is the loop around them.
|
||||||
|
|
||||||
|
## Check the connection first
|
||||||
|
|
||||||
|
- Call `rig_whoami`. It names the seat this connection acts as. Every item you create is recorded as coming from that seat,
|
||||||
|
and the team's questions about your work come back to it.
|
||||||
|
- If it has no seat, stop and help the person set one (the last section). Without one, your requests are unattributed and you
|
||||||
|
can't list your own work.
|
||||||
|
- Call `rig_team` with the rig's name: it shows the team's seats, what each is for, and its lead. Work goes to the lead
|
||||||
|
unless the person names a seat.
|
||||||
|
- If `lead` is null (its `leadNote` says why), and the team has exactly one seat, send work there. Otherwise, ask the person
|
||||||
|
which seat should take it.
|
||||||
|
|
||||||
|
## Hand the team work
|
||||||
|
|
||||||
|
- Use `rig_queue_create` with `destination` (the lead, or the seat the person chose) and `body`. Write the body for a
|
||||||
|
capable colleague: the goal, why it matters, what done looks like, and where to look. Add a one-line `summary`.
|
||||||
|
- Tell the person the `qitemId` it returns. It's how both of you find this work again.
|
||||||
|
- If the call times out, check `rig_queue_show` with that same `qitemId` before trying again. If you retry, pass the same id
|
||||||
|
as `qitemId`. Never retry with a new one: that creates the work twice.
|
||||||
|
|
||||||
|
## Follow it
|
||||||
|
|
||||||
|
- `rig_queue_show` with the `qitemId` gives its state, its current owner and its transitions.
|
||||||
|
- When a seat hands the work on, the item names the next one. Follow the chain to the newest item before you report.
|
||||||
|
- Report what the record says. An accepted or claimed item isn't finished work, and a quiet one isn't stuck: say what you
|
||||||
|
saw and when it last changed.
|
||||||
|
- Nothing wakes this chat when the team finishes. Check again when the person asks, or when they come back.
|
||||||
|
|
||||||
|
## The team's questions
|
||||||
|
|
||||||
|
- Questions about your work come to your seat: `rig_queue_list` with `owned` lists them.
|
||||||
|
- Show the person the question and the team's draft, in their own words where the item gives them. Then ask for an answer.
|
||||||
|
- Record only what the person actually tells you. Claim the item (`rig_queue_claim`), then close it with
|
||||||
|
`rig_queue_update`: `state` `done`, `closureReason` `no-follow-on`, and the person's answer in `resolution`, saying it's
|
||||||
|
theirs. Put only their words there: no notes, paths or summaries of your own. If the person isn't there to answer, leave
|
||||||
|
the item open and tell them it's waiting. Never choose an answer for them.
|
||||||
|
- Some questions go to the person directly, outside your seat. Those wait for them where they already answer the team,
|
||||||
|
such as the OpenRig TUI or Slack. You can mention that they're waiting; answering them isn't yours.
|
||||||
|
|
||||||
|
## Read the result
|
||||||
|
|
||||||
|
- When the item is `done`, its `resolution` is the answer, and its transitions hold the notes the team recorded.
|
||||||
|
- If it names an `evidenceRef`, read it with `rig_artifact_read`, passing the item's `qitemId`, not the path. The tool
|
||||||
|
reads the item's `evidenceRef` within the shared roots this connection may read. For another file in a shared root,
|
||||||
|
pass the root's name and `path`. If the connection can't read it, give the person the path.
|
||||||
|
- To ask for changes, create a new item with `revises` set to the earlier `qitemId`, so the revision stays with the
|
||||||
|
original work.
|
||||||
|
|
||||||
|
## Come back later
|
||||||
|
|
||||||
|
- `rig_queue_list` with `mine` lists what this seat sent or owns. Add `all` to include finished work, or `since` to see
|
||||||
|
what changed after a time.
|
||||||
|
- A new chat with the same seat reaches the same work. Items outlive conversations.
|
||||||
|
|
||||||
|
## Set it up (once, with the person)
|
||||||
|
|
||||||
|
1. OpenRig runs on this machine with a team: `rig ps` lists the person's rigs.
|
||||||
|
2. Pick an address for this app on one of those rigs, for example `cursor@my-team`. It's an address the queue takes items
|
||||||
|
for; nothing is launched for it. To tell the team what it's for, the person can add it to a roster as a hosted member
|
||||||
|
(`docs/reference/hosted-members.md` in the OpenRig package).
|
||||||
|
3. Connect the app to that address, then start a new session so the MCP server starts with it:
|
||||||
|
- **Claude Code:** set it as the plugin's OpenRig seat in the plugin's settings.
|
||||||
|
- **Codex:** run `codex mcp add openrig --env OPENRIG_SESSION_NAME=<address> -- rig mcp serve --front-door`. Codex
|
||||||
|
does not forward every shell environment variable to MCP servers by default, so if the team's OpenRig home isn't the
|
||||||
|
default, add `--env OPENRIG_HOME=<that home>` too.
|
||||||
|
- **Another app:** add an MCP server that runs `rig mcp serve --front-door` with `OPENRIG_SESSION_NAME` set to the
|
||||||
|
address.
|
||||||
|
|
||||||
|
## What this doesn't do
|
||||||
|
|
||||||
|
- It doesn't add a panel to the app, and nothing wakes this chat when the team finishes.
|
||||||
|
- Anyone using this app on this machine acts as the seat the plugin names.
|
||||||
|
- It reaches a team on this machine. A team hosted elsewhere needs that host's own connection.
|
||||||
@@ -906,12 +906,13 @@ function validateAuthoringLayout(layout) {
|
|||||||
}
|
}
|
||||||
const edgeNames = Object.keys(layout.edges).sort();
|
const edgeNames = Object.keys(layout.edges).sort();
|
||||||
const requiredEdges = ["canonical", "plugin", "spec"];
|
const requiredEdges = ["canonical", "plugin", "spec"];
|
||||||
|
// A further edge is another shipped plugin's copy of skills authored on spec or plugin (the openrig bundle's).
|
||||||
if (
|
if (
|
||||||
edgeNames.length !== requiredEdges.length ||
|
requiredEdges.some((edge) => !edgeNames.includes(edge)) ||
|
||||||
edgeNames.some((edge, index) => edge !== requiredEdges[index])
|
edgeNames.some((edge) => !requiredEdges.includes(edge) && layout.edges[edge]?.layout !== "flat")
|
||||||
) {
|
) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Layout control must contain exactly canonical, plugin, and spec edges",
|
"Layout control must contain canonical, plugin, and spec edges; any other edge must be a flat plugin copy",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (!isRecord(layout.skills) || Object.keys(layout.skills).length === 0) {
|
if (!isRecord(layout.skills) || Object.keys(layout.skills).length === 0) {
|
||||||
|
|||||||
@@ -922,6 +922,31 @@ test("authoring regeneration writes each generated copy from the skill's one hom
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("a further flat edge is another plugin's copy, written from the skill's home; other extra edges and missing ones refuse", async () => {
|
||||||
|
const mirror = await import("./mirror-skills.mjs");
|
||||||
|
const root = mkdtempSync(join(tmpdir(), "openrig-authoring-bundle-"));
|
||||||
|
try {
|
||||||
|
const { repoRoot, inputs } = homeFixture(root);
|
||||||
|
const layout = {
|
||||||
|
...inputs.layout,
|
||||||
|
edges: { ...inputs.layout.edges, bundle: { path: "bundle", layout: "flat" } },
|
||||||
|
skills: { ...inputs.layout.skills, two: { edges: ["bundle", "canonical", "spec"], category: "core" } },
|
||||||
|
};
|
||||||
|
const first = await mirror.regeneratePublicSkills({ ...inputs, layout });
|
||||||
|
assert.deepEqual(first.undeclared, []);
|
||||||
|
assert.deepEqual(snapshotTree(join(repoRoot, "bundle/two")), snapshotTree(join(repoRoot, "spec/core/two")));
|
||||||
|
assert.equal(existsSync(join(repoRoot, "bundle/three")), false);
|
||||||
|
|
||||||
|
const nested = { ...layout, edges: { ...layout.edges, bundle: { path: "bundle", layout: "categorized" } } };
|
||||||
|
await assert.rejects(mirror.regeneratePublicSkills({ ...inputs, layout: nested }), /any other edge must be a flat plugin copy/);
|
||||||
|
const { plugin: _plugin, ...withoutPlugin } = inputs.layout.edges;
|
||||||
|
await assert.rejects(mirror.regeneratePublicSkills({ ...inputs, layout: { ...inputs.layout, edges: withoutPlugin } }),
|
||||||
|
/must contain canonical, plugin, and spec edges/);
|
||||||
|
} finally {
|
||||||
|
rmSync(root, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test("authoring regeneration repairs permission-only drift in generated copies from the home's modes", async () => {
|
test("authoring regeneration repairs permission-only drift in generated copies from the home's modes", async () => {
|
||||||
const mirror = await import("./mirror-skills.mjs");
|
const mirror = await import("./mirror-skills.mjs");
|
||||||
const root = mkdtempSync(join(tmpdir(), "openrig-authoring-modes-"));
|
const root = mkdtempSync(join(tmpdir(), "openrig-authoring-modes-"));
|
||||||
|
|||||||
@@ -16,7 +16,8 @@
|
|||||||
"refocusing",
|
"refocusing",
|
||||||
"delegating-work",
|
"delegating-work",
|
||||||
"context-engineering",
|
"context-engineering",
|
||||||
"openrig-software-factory"
|
"openrig-software-factory",
|
||||||
|
"working-with-an-openrig-team"
|
||||||
],
|
],
|
||||||
"ship_after_fix": [
|
"ship_after_fix": [
|
||||||
"openrig-user",
|
"openrig-user",
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"edges": {
|
"edges": {
|
||||||
|
"bundle": {
|
||||||
|
"working-with-an-openrig-team/SKILL.md": "4b3e6e8eecb710abdb95d604c419599566dbc77b019b49175e936e713b65cf6f"
|
||||||
|
},
|
||||||
"canonical": {
|
"canonical": {
|
||||||
"core/agent-starters/SKILL.md": "8e8961f44fccb812f39e01174a331dab24f153ceaba91925f1e42371836cdec0",
|
"core/agent-starters/SKILL.md": "8e8961f44fccb812f39e01174a331dab24f153ceaba91925f1e42371836cdec0",
|
||||||
"core/agent-startup-and-context-ingestion/SKILL.md": "347f28a9567b4b8d8669aff6946229f53144e0c139aca2cbe77e0eb93a87f1b1",
|
"core/agent-startup-and-context-ingestion/SKILL.md": "347f28a9567b4b8d8669aff6946229f53144e0c139aca2cbe77e0eb93a87f1b1",
|
||||||
@@ -10,7 +13,7 @@
|
|||||||
"core/openrig-architect/SKILL.md": "affa65df8087efca711e429e36e8756c711419dccc801d26d9e51a746b56391d",
|
"core/openrig-architect/SKILL.md": "affa65df8087efca711e429e36e8756c711419dccc801d26d9e51a746b56391d",
|
||||||
"core/openrig-cmux/SKILL.md": "73a86eac45d68b16a38fc0a4438713c634d86061073dac96a4d9e656ea5d34dc",
|
"core/openrig-cmux/SKILL.md": "73a86eac45d68b16a38fc0a4438713c634d86061073dac96a4d9e656ea5d34dc",
|
||||||
"core/openrig-herdr/SKILL.md": "cb39b51f954ca89e8d7ec0f8b3f326476d5c4dd3bc17dbd47e6379aa630e7b22",
|
"core/openrig-herdr/SKILL.md": "cb39b51f954ca89e8d7ec0f8b3f326476d5c4dd3bc17dbd47e6379aa630e7b22",
|
||||||
"core/openrig-skills/SKILL.md": "bdd6594ae1108455103239d39694c865cc40d3f7d9f24cd3566a18397df270da",
|
"core/openrig-skills/SKILL.md": "6d7bf933ac40531f11757a31a93d26dcbda617f1b85f48a21865b3a5c5e2ddaa",
|
||||||
"core/openrig-software-factory/SKILL.md": "7a57815f3c6865d5cc42c063cccdca998d6ee100a82812362d0c81f314465b77",
|
"core/openrig-software-factory/SKILL.md": "7a57815f3c6865d5cc42c063cccdca998d6ee100a82812362d0c81f314465b77",
|
||||||
"core/openrig-software-factory/references/worked-example.md": "64dec0e832692b655ffc1ca9f72e96f221f78b8bc97f73775198f188b502f147",
|
"core/openrig-software-factory/references/worked-example.md": "64dec0e832692b655ffc1ca9f72e96f221f78b8bc97f73775198f188b502f147",
|
||||||
"core/openrig-upgrade/SKILL.md": "bd1765d94e4d462b9c8b08198238c1239c70ff68228c3d575438a7bc9a326ac6",
|
"core/openrig-upgrade/SKILL.md": "bd1765d94e4d462b9c8b08198238c1239c70ff68228c3d575438a7bc9a326ac6",
|
||||||
@@ -24,6 +27,7 @@
|
|||||||
"core/specification-system/SKILL.md": "5226d6e8b1362df89ba3d5afd82e4b4add355870132ca4b140d7f9f4324ffdc1",
|
"core/specification-system/SKILL.md": "5226d6e8b1362df89ba3d5afd82e4b4add355870132ca4b140d7f9f4324ffdc1",
|
||||||
"core/topology-mutation-and-seat-management/SKILL.md": "527ca074432ce6513fb25cd27e8c709dabe0c91361b8999e09b272f3813abe62",
|
"core/topology-mutation-and-seat-management/SKILL.md": "527ca074432ce6513fb25cd27e8c709dabe0c91361b8999e09b272f3813abe62",
|
||||||
"core/watchdog/SKILL.md": "24b2e756e3e9d067fa93a4b5fa5f48a5cce511e8b1a5b360b1c39bc09bc95d82",
|
"core/watchdog/SKILL.md": "24b2e756e3e9d067fa93a4b5fa5f48a5cce511e8b1a5b360b1c39bc09bc95d82",
|
||||||
|
"core/working-with-an-openrig-team/SKILL.md": "4b3e6e8eecb710abdb95d604c419599566dbc77b019b49175e936e713b65cf6f",
|
||||||
"pm/backlog-capture/SKILL.md": "345c2b47fc85e48c15c0d42bbb48fb27dd3b0ceec36bf59dc6ed20daaea47a84",
|
"pm/backlog-capture/SKILL.md": "345c2b47fc85e48c15c0d42bbb48fb27dd3b0ceec36bf59dc6ed20daaea47a84",
|
||||||
"pm/context-builder/SKILL.md": "0134c9b0a9f196565109f7f51b6ec78d5c3e4b5982a0d7105a93b29952625b3c",
|
"pm/context-builder/SKILL.md": "0134c9b0a9f196565109f7f51b6ec78d5c3e4b5982a0d7105a93b29952625b3c",
|
||||||
"pm/exec-summary/SKILL.md": "ada0f42337920e7acdd94e24cad4f5c269b057e750f4a7fc627ea9efbc02fcb6",
|
"pm/exec-summary/SKILL.md": "ada0f42337920e7acdd94e24cad4f5c269b057e750f4a7fc627ea9efbc02fcb6",
|
||||||
@@ -88,7 +92,7 @@
|
|||||||
"openrig-operating-model/templates/PLAYBOOK.md": "ff6f9dc8caab06dc7b9ae21977310ddefa23e925453bd54629ce0a669403bfb0",
|
"openrig-operating-model/templates/PLAYBOOK.md": "ff6f9dc8caab06dc7b9ae21977310ddefa23e925453bd54629ce0a669403bfb0",
|
||||||
"openrig-operating-model/templates/SOP.md": "1eaf0daf4f9bbf0715e6a5aaf0f29f030af5fe8b8a10232488ee97c507afa6dc",
|
"openrig-operating-model/templates/SOP.md": "1eaf0daf4f9bbf0715e6a5aaf0f29f030af5fe8b8a10232488ee97c507afa6dc",
|
||||||
"openrig-operating-model/templates/SPEC.md": "f126536daa59cea3f1cfb0898d83c7cdf99f6392f6f155ea64fe55187dd0f5f1",
|
"openrig-operating-model/templates/SPEC.md": "f126536daa59cea3f1cfb0898d83c7cdf99f6392f6f155ea64fe55187dd0f5f1",
|
||||||
"openrig-skills/SKILL.md": "bdd6594ae1108455103239d39694c865cc40d3f7d9f24cd3566a18397df270da",
|
"openrig-skills/SKILL.md": "6d7bf933ac40531f11757a31a93d26dcbda617f1b85f48a21865b3a5c5e2ddaa",
|
||||||
"openrig-user/SKILL.md": "0776fe4e197d347614f72eec817f881debfcae372c4a1f912f2e0fbbf91c67da",
|
"openrig-user/SKILL.md": "0776fe4e197d347614f72eec817f881debfcae372c4a1f912f2e0fbbf91c67da",
|
||||||
"orienting-to-an-inherited-seat/CHANGELOG.md": "4d0970e75e5c15b00a48d6a7150376d646257777264fd1b9a191165a8d5a6e84",
|
"orienting-to-an-inherited-seat/CHANGELOG.md": "4d0970e75e5c15b00a48d6a7150376d646257777264fd1b9a191165a8d5a6e84",
|
||||||
"orienting-to-an-inherited-seat/SKILL.md": "252fed402f19027056bbba241df3305be88de3175b67dc8fe051143bb0d4412b",
|
"orienting-to-an-inherited-seat/SKILL.md": "252fed402f19027056bbba241df3305be88de3175b67dc8fe051143bb0d4412b",
|
||||||
@@ -114,7 +118,7 @@
|
|||||||
"core/openrig-architect/SKILL.md": "affa65df8087efca711e429e36e8756c711419dccc801d26d9e51a746b56391d",
|
"core/openrig-architect/SKILL.md": "affa65df8087efca711e429e36e8756c711419dccc801d26d9e51a746b56391d",
|
||||||
"core/openrig-cmux/SKILL.md": "73a86eac45d68b16a38fc0a4438713c634d86061073dac96a4d9e656ea5d34dc",
|
"core/openrig-cmux/SKILL.md": "73a86eac45d68b16a38fc0a4438713c634d86061073dac96a4d9e656ea5d34dc",
|
||||||
"core/openrig-herdr/SKILL.md": "cb39b51f954ca89e8d7ec0f8b3f326476d5c4dd3bc17dbd47e6379aa630e7b22",
|
"core/openrig-herdr/SKILL.md": "cb39b51f954ca89e8d7ec0f8b3f326476d5c4dd3bc17dbd47e6379aa630e7b22",
|
||||||
"core/openrig-skills/SKILL.md": "bdd6594ae1108455103239d39694c865cc40d3f7d9f24cd3566a18397df270da",
|
"core/openrig-skills/SKILL.md": "6d7bf933ac40531f11757a31a93d26dcbda617f1b85f48a21865b3a5c5e2ddaa",
|
||||||
"core/openrig-software-factory/SKILL.md": "7a57815f3c6865d5cc42c063cccdca998d6ee100a82812362d0c81f314465b77",
|
"core/openrig-software-factory/SKILL.md": "7a57815f3c6865d5cc42c063cccdca998d6ee100a82812362d0c81f314465b77",
|
||||||
"core/openrig-software-factory/references/worked-example.md": "64dec0e832692b655ffc1ca9f72e96f221f78b8bc97f73775198f188b502f147",
|
"core/openrig-software-factory/references/worked-example.md": "64dec0e832692b655ffc1ca9f72e96f221f78b8bc97f73775198f188b502f147",
|
||||||
"core/openrig-upgrade/SKILL.md": "bd1765d94e4d462b9c8b08198238c1239c70ff68228c3d575438a7bc9a326ac6",
|
"core/openrig-upgrade/SKILL.md": "bd1765d94e4d462b9c8b08198238c1239c70ff68228c3d575438a7bc9a326ac6",
|
||||||
@@ -128,6 +132,7 @@
|
|||||||
"core/specification-system/SKILL.md": "5226d6e8b1362df89ba3d5afd82e4b4add355870132ca4b140d7f9f4324ffdc1",
|
"core/specification-system/SKILL.md": "5226d6e8b1362df89ba3d5afd82e4b4add355870132ca4b140d7f9f4324ffdc1",
|
||||||
"core/topology-mutation-and-seat-management/SKILL.md": "527ca074432ce6513fb25cd27e8c709dabe0c91361b8999e09b272f3813abe62",
|
"core/topology-mutation-and-seat-management/SKILL.md": "527ca074432ce6513fb25cd27e8c709dabe0c91361b8999e09b272f3813abe62",
|
||||||
"core/watchdog/SKILL.md": "24b2e756e3e9d067fa93a4b5fa5f48a5cce511e8b1a5b360b1c39bc09bc95d82",
|
"core/watchdog/SKILL.md": "24b2e756e3e9d067fa93a4b5fa5f48a5cce511e8b1a5b360b1c39bc09bc95d82",
|
||||||
|
"core/working-with-an-openrig-team/SKILL.md": "4b3e6e8eecb710abdb95d604c419599566dbc77b019b49175e936e713b65cf6f",
|
||||||
"pm/backlog-capture/SKILL.md": "345c2b47fc85e48c15c0d42bbb48fb27dd3b0ceec36bf59dc6ed20daaea47a84",
|
"pm/backlog-capture/SKILL.md": "345c2b47fc85e48c15c0d42bbb48fb27dd3b0ceec36bf59dc6ed20daaea47a84",
|
||||||
"pm/context-builder/SKILL.md": "0134c9b0a9f196565109f7f51b6ec78d5c3e4b5982a0d7105a93b29952625b3c",
|
"pm/context-builder/SKILL.md": "0134c9b0a9f196565109f7f51b6ec78d5c3e4b5982a0d7105a93b29952625b3c",
|
||||||
"pm/exec-summary/SKILL.md": "ada0f42337920e7acdd94e24cad4f5c269b057e750f4a7fc627ea9efbc02fcb6",
|
"pm/exec-summary/SKILL.md": "ada0f42337920e7acdd94e24cad4f5c269b057e750f4a7fc627ea9efbc02fcb6",
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
{
|
{
|
||||||
"version": 0,
|
"version": 0,
|
||||||
"edges": {
|
"edges": {
|
||||||
|
"bundle": {
|
||||||
|
"path": "packages/daemon/assets/plugins/openrig/skills",
|
||||||
|
"layout": "flat"
|
||||||
|
},
|
||||||
"canonical": {
|
"canonical": {
|
||||||
"path": "skills/_canonical",
|
"path": "skills/_canonical",
|
||||||
"layout": "mirror-of-spec"
|
"layout": "mirror-of-spec"
|
||||||
@@ -363,6 +367,14 @@
|
|||||||
"spec"
|
"spec"
|
||||||
],
|
],
|
||||||
"category": "core"
|
"category": "core"
|
||||||
|
},
|
||||||
|
"working-with-an-openrig-team": {
|
||||||
|
"edges": [
|
||||||
|
"bundle",
|
||||||
|
"canonical",
|
||||||
|
"spec"
|
||||||
|
],
|
||||||
|
"category": "core"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -87,6 +87,7 @@ These ship in the OpenRig repo and reach a seat when its profile selects them. T
|
|||||||
- **watchdog** — monitoring or recovering a seat (health, restore, stuck state).
|
- **watchdog** — monitoring or recovering a seat (health, restore, stuck state).
|
||||||
- **session-source-fork** — forking a session's source/context.
|
- **session-source-fork** — forking a session's source/context.
|
||||||
- **context-engineering** — designing context systems. Provisional and non-normative: current OpenRig skills, explicit rulings, and measured practice outrank it on any conflict.
|
- **context-engineering** — designing context systems. Provisional and non-normative: current OpenRig skills, explicit rulings, and measured practice outrank it on any conflict.
|
||||||
|
- **working-with-an-openrig-team** — a person's own agent in an app (Claude Code, Codex, Cursor, VS Code) works with their team through `rig mcp serve`: hand work over, follow it, answer the team's questions, read results. The `openrig` app plugin loads it; a seat helping someone connect an app can open it with `rig context get skills/core/working-with-an-openrig-team/SKILL.md`.
|
||||||
|
|
||||||
Pod handbooks (load when you're in that pod):
|
Pod handbooks (load when you're in that pod):
|
||||||
- **orchestration-team** — you're orchestrating a rig: dispatching, monitoring, keeping the loop moving.
|
- **orchestration-team** — you're orchestrating a rig: dispatching, monitoring, keeping the loop moving.
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
---
|
||||||
|
name: working-with-an-openrig-team
|
||||||
|
description: >-
|
||||||
|
Use when you are a person's coding agent in an app such as Claude Code, Codex, Cursor or VS Code, connected through the
|
||||||
|
OpenRig MCP server, and the person wants their OpenRig team to do some work, wants to know how that work is going,
|
||||||
|
has a question from the team to answer, or wants to read what the team returned.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Working with an OpenRig team from your app
|
||||||
|
|
||||||
|
The person runs a team of agents with OpenRig: each agent has its own terminal, and they keep working after this chat ends.
|
||||||
|
You aren't one of those terminals. You're the person's front door to the team: you hand it work, follow that work, bring
|
||||||
|
the team's questions to the person, and bring back what it produced. The MCP server's own instructions say how its tools
|
||||||
|
behave; this skill is the loop around them.
|
||||||
|
|
||||||
|
## Check the connection first
|
||||||
|
|
||||||
|
- Call `rig_whoami`. It names the seat this connection acts as. Every item you create is recorded as coming from that seat,
|
||||||
|
and the team's questions about your work come back to it.
|
||||||
|
- If it has no seat, stop and help the person set one (the last section). Without one, your requests are unattributed and you
|
||||||
|
can't list your own work.
|
||||||
|
- Call `rig_team` with the rig's name: it shows the team's seats, what each is for, and its lead. Work goes to the lead
|
||||||
|
unless the person names a seat.
|
||||||
|
- If `lead` is null (its `leadNote` says why), and the team has exactly one seat, send work there. Otherwise, ask the person
|
||||||
|
which seat should take it.
|
||||||
|
|
||||||
|
## Hand the team work
|
||||||
|
|
||||||
|
- Use `rig_queue_create` with `destination` (the lead, or the seat the person chose) and `body`. Write the body for a
|
||||||
|
capable colleague: the goal, why it matters, what done looks like, and where to look. Add a one-line `summary`.
|
||||||
|
- Tell the person the `qitemId` it returns. It's how both of you find this work again.
|
||||||
|
- If the call times out, check `rig_queue_show` with that same `qitemId` before trying again. If you retry, pass the same id
|
||||||
|
as `qitemId`. Never retry with a new one: that creates the work twice.
|
||||||
|
|
||||||
|
## Follow it
|
||||||
|
|
||||||
|
- `rig_queue_show` with the `qitemId` gives its state, its current owner and its transitions.
|
||||||
|
- When a seat hands the work on, the item names the next one. Follow the chain to the newest item before you report.
|
||||||
|
- Report what the record says. An accepted or claimed item isn't finished work, and a quiet one isn't stuck: say what you
|
||||||
|
saw and when it last changed.
|
||||||
|
- Nothing wakes this chat when the team finishes. Check again when the person asks, or when they come back.
|
||||||
|
|
||||||
|
## The team's questions
|
||||||
|
|
||||||
|
- Questions about your work come to your seat: `rig_queue_list` with `owned` lists them.
|
||||||
|
- Show the person the question and the team's draft, in their own words where the item gives them. Then ask for an answer.
|
||||||
|
- Record only what the person actually tells you. Claim the item (`rig_queue_claim`), then close it with
|
||||||
|
`rig_queue_update`: `state` `done`, `closureReason` `no-follow-on`, and the person's answer in `resolution`, saying it's
|
||||||
|
theirs. Put only their words there: no notes, paths or summaries of your own. If the person isn't there to answer, leave
|
||||||
|
the item open and tell them it's waiting. Never choose an answer for them.
|
||||||
|
- Some questions go to the person directly, outside your seat. Those wait for them where they already answer the team,
|
||||||
|
such as the OpenRig TUI or Slack. You can mention that they're waiting; answering them isn't yours.
|
||||||
|
|
||||||
|
## Read the result
|
||||||
|
|
||||||
|
- When the item is `done`, its `resolution` is the answer, and its transitions hold the notes the team recorded.
|
||||||
|
- If it names an `evidenceRef`, read it with `rig_artifact_read`, passing the item's `qitemId`, not the path. The tool
|
||||||
|
reads the item's `evidenceRef` within the shared roots this connection may read. For another file in a shared root,
|
||||||
|
pass the root's name and `path`. If the connection can't read it, give the person the path.
|
||||||
|
- To ask for changes, create a new item with `revises` set to the earlier `qitemId`, so the revision stays with the
|
||||||
|
original work.
|
||||||
|
|
||||||
|
## Come back later
|
||||||
|
|
||||||
|
- `rig_queue_list` with `mine` lists what this seat sent or owns. Add `all` to include finished work, or `since` to see
|
||||||
|
what changed after a time.
|
||||||
|
- A new chat with the same seat reaches the same work. Items outlive conversations.
|
||||||
|
|
||||||
|
## Set it up (once, with the person)
|
||||||
|
|
||||||
|
1. OpenRig runs on this machine with a team: `rig ps` lists the person's rigs.
|
||||||
|
2. Pick an address for this app on one of those rigs, for example `cursor@my-team`. It's an address the queue takes items
|
||||||
|
for; nothing is launched for it. To tell the team what it's for, the person can add it to a roster as a hosted member
|
||||||
|
(`docs/reference/hosted-members.md` in the OpenRig package).
|
||||||
|
3. Connect the app to that address, then start a new session so the MCP server starts with it:
|
||||||
|
- **Claude Code:** set it as the plugin's OpenRig seat in the plugin's settings.
|
||||||
|
- **Codex:** run `codex mcp add openrig --env OPENRIG_SESSION_NAME=<address> -- rig mcp serve --front-door`. Codex
|
||||||
|
does not forward every shell environment variable to MCP servers by default, so if the team's OpenRig home isn't the
|
||||||
|
default, add `--env OPENRIG_HOME=<that home>` too.
|
||||||
|
- **Another app:** add an MCP server that runs `rig mcp serve --front-door` with `OPENRIG_SESSION_NAME` set to the
|
||||||
|
address.
|
||||||
|
|
||||||
|
## What this doesn't do
|
||||||
|
|
||||||
|
- It doesn't add a panel to the app, and nothing wakes this chat when the team finishes.
|
||||||
|
- Anyone using this app on this machine acts as the seat the plugin names.
|
||||||
|
- It reaches a team on this machine. A team hosted elsewhere needs that host's own connection.
|
||||||
Reference in new issue
Block a user