Public: anyone can read this; the crew can change it.

The Claude Code and OpenCode plugins say what the crew is doing on an app and show a run's last lines, through MCP `runs` and `run_log` #7395

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

torque/forge:torque/7371-the-claude-code-and-opencode-plugins-say → main 1 commit fork 1 thread · 1 unresolved

  • Merged. torque/7371-the-claude-code-and-opencode-plugins-say was merged into main as 2e62cec.

Review

Sign in to review this change.
5 files changed +44 −8
plugins/claude-code/README.md +6 −1
@@ -4,7 +4,7 @@ Set `FORGE_URL` (the forge's address, no trailing slash) and `FORGE_TOKEN`
44 (an `rw` token; `ro` reads only; make one at `/settings/tokens` on the forge, which also shows this line filled in), then add this directory as a plugin
55 (`claude --plugin-dir plugins/claude-code`). It registers the forge's MCP
66 server and a `/forge` command: `tell`, `status`, `decide`, `plan`, `preview`,
7 `pulls`.
7 `pulls`, `runs`, `log`.
88
99 `/forge plan` shows the planner's proposed wave in three lines. `/forge plan approve`, `/forge plan redirect <words>` or `/forge plan later` answers it, after you confirm.
1010
@@ -12,4 +12,9 @@ server and a `/forge` command: `tell`, `status`, `decide`, `plan`, `preview`,
1212 and who is working at that URL. `/forge pulls <app>` lists the app's open
1313 pulls with their check.
1414
15 `/forge runs <app>` says what the crew is doing on the app: the running runs
16 and the last three finished, one line each (kind, issue or pull, state, how
17 long), through MCP `runs`. `/forge log <app> <run>` relays that run's last 40
18 lines verbatim, through MCP `run_log`.
19
1520 `test/plugins.test.ts` fails when this plugin names an MCP tool the forge does not list, so a rename in `src/http/mcp.ts` cannot leave it behind.
plugins/claude-code/commands/forge.md +4 −2
@@ -1,6 +1,6 @@
11 ---
2 description: Talk to your forge: tell, status, decide, plan, preview, pulls
3 argument-hint: tell <app> <words> | status | decide <approve|redirect|decline> [words] | plan [approve|redirect <words>|later] | preview <url> | pulls <app>
2 description: Talk to your forge: tell, status, decide, plan, preview, pulls, runs, log
3 argument-hint: tell <app> <words> | status | decide <approve|redirect|decline> [words] | plan [approve|redirect <words>|later] | preview <url> | pulls <app> | runs <app> | log <app> <run>
44 ---
55
66 Use the `forge` MCP server's tools for `$ARGUMENTS`.
@@ -11,5 +11,7 @@ Use the `forge` MCP server's tools for `$ARGUMENTS`.
1111 - `plan`: call `asks`, take the first row whose kind is `proposal` (none: say no wave waits). Show it in three lines: the wave's title, what it holds, why now. With `approve`, `redirect <words>` or `later`, ask the person to confirm, and only after they say yes call `asks_proposal` with `id` = that row's `proposal.id` (the proposal's own id, not the row's `id`, which starts `proposal:`), `verb` = the word, and `words` for redirect.
1212 - `preview <url>`: call `preview` with `url`; say in two lines the app, environment, pull, check and who is working.
1313 - `pulls <app>`: call `pulls` with `repo` = the app (owner/name); list the open pulls, one line each with its check.
14 - `runs <app>`: call `runs` with `repo` = the app (owner/name); say, one line each, the running runs and the last three finished ones: kind, issue or pull number, state, and how long (from `startedAt` to `endedAt`, or to now while running).

Pass limit = 50 so the last three finished runs are not pushed out of the default 20 by running ones.

15 - `log <app> <run>`: call `run_log` with `repo` = the app, `run` = the run id and `tail` = 40; relay the lines verbatim, in a code block.
1416
1517 Keep what you relay short. Never merge: a person lands changes.
plugins/opencode/README.md +6 −1
@@ -4,7 +4,7 @@ Copy `forge.ts` into `.opencode/plugin/` (or list this package under `plugin`
44 in `opencode.json`), and set `FORGE_URL` and `FORGE_TOKEN` (make the token at `/settings/tokens` on the forge, which shows the line with its own address filled in):
55 `mkdir -p .opencode/plugin && cp plugins/opencode/forge.ts .opencode/plugin/ && FORGE_URL=<url> FORGE_TOKEN=<token> opencode`. It registers the
66 forge's MCP server and the `forge` command: `tell`, `status`, `decide`, `plan`,
7 `preview`, `pulls`.
7 `preview`, `pulls`, `runs`, `log`.
88 `/forge plan` shows the planner's next wave (the first wave-proposal row of `asks`);
99 `/forge plan approve`, `redirect <words>` or `later` steers it through `asks_proposal`,
1010 applied only after you say yes. Only the owner may act on a proposal.
@@ -13,6 +13,11 @@ applied only after you say yes. Only the owner may act on a proposal.
1313 and who is working at that URL. `/forge pulls <app>` lists the app's open
1414 pulls with their check.
1515
16 `/forge runs <app>` says what the crew is doing on the app: the running runs
17 and the last three finished, one line each (kind, issue or pull, state, how
18 long), through MCP `runs`. `/forge log <app> <run>` relays that run's last 40
19 lines verbatim, through MCP `run_log`.
20
1621 Code Mode can also read `GET /contract/openapi.json`.
1722
1823 `test/plugins.test.ts` fails when this plugin names an MCP tool the forge does not list, so a rename in `src/http/mcp.ts` cannot leave it behind.
plugins/opencode/forge.ts +4 −2
@@ -1,6 +1,6 @@
11 /**
22 * forge for OpenCode: registers the forge's MCP server and a `forge`
3 * command (tell, status, decide, plan, preview, pulls). Set FORGE_URL and FORGE_TOKEN.
3 * command (tell, status, decide, plan, preview, pulls, runs, log). Set FORGE_URL and FORGE_TOKEN.
44 * No logic lives here: the tools are the forge's own (`docs/MCP.md`).
55 */
66 import type { Plugin } from "@opencode-ai/plugin";
@@ -12,6 +12,8 @@ const COMMAND = `Use the forge MCP tools for: $ARGUMENTS
1212 - plan [approve|redirect <words>|later]: call \`asks\`; show the first wave-proposal row (the planner's next wave) in a few lines. approve|redirect|later -> \`asks_proposal\` with that row's id and verb (redirect carries the words as \`words\`). Say what it will do and apply only after the person says yes.
1313 - preview <url>: call \`preview\` with url; say in two lines the app, environment, pull, check and who is working.
1414 - pulls <app>: call \`pulls\` with repo; list the open pulls, one line each with its check.
15 - runs <app>: call \`runs\` with repo; say, one line each, the running runs and the last three finished: kind, issue or pull number, state, how long (startedAt to endedAt, or to now).
16 - log <app> <run>: call \`run_log\` with repo, run (the run id) and \`tail\` 40; relay the lines verbatim.
1517 Never merge: a person lands changes.`;
1618
1719 export const Forge: Plugin = async () => ({
@@ -29,7 +31,7 @@ export const Forge: Plugin = async () => ({
2931 };
3032 config["command"] = {
3133 ...config["command"],
32 forge: { description: "Tell, status, decide, plan, preview or pulls on your forge", template: COMMAND },
34 forge: { description: "Tell, status, decide, plan, preview, pulls, runs or log on your forge", template: COMMAND },
3335 };
3436 },
3537 });
test/plugins.test.ts +24 −2
@@ -19,7 +19,7 @@ const opencodeText = (): string => {
1919 const backticked = (text: string): string[] => [...text.matchAll(/`([a-z][a-z0-9_]*)`/g)].map((m) => m[1]!);
2020
2121 /** Backticked words that are the server or an argument, not a tool. */
22 const NOT_TOOLS = new Set(["forge", "repo", "text", "context", "page", "run", "commit", "said", "action", "ship", "close", "status", "words", "id", "verb", "proposal", "approve", "later", "url"]);
22 const NOT_TOOLS = new Set(["forge", "repo", "text", "context", "page", "run", "commit", "said", "action", "ship", "close", "status", "words", "id", "verb", "proposal", "approve", "later", "url", "tail"]);
2323
2424 /** Tools the command text tells the agent to call: every backticked name that is not an argument. */
2525 const toolsNamed = (text: string): string[] => [...new Set(backticked(text))].filter((n) => !NOT_TOOLS.has(n));
@@ -37,6 +37,28 @@ for (const [label, text] of [
3737 });
3838 }
3939
40 describe("what the crew is doing on an app", () => {
41 for (const [label, text] of [
42 ["Claude Code", claudeText],
43 ["OpenCode", opencodeText],
44 ] as const) {
45 test(`${label} names runs and run_log, which the forge lists`, () => {
46 const named = toolsNamed(text());
47 for (const tool of ["runs", "run_log"]) {
48 expect(named).toContain(tool);
49 expect(tools.has(tool)).toBe(true);
50 }
51 expect(text()).toMatch(/log <app> <run>/);
52 expect(text()).toMatch(/`tail`\s*(=\s*)?40/);
53 expect(text()).toContain("verbatim");
54 });
55 }
56
57 test("a command that drops run_log is caught", () => {
58 expect(toolsNamed("- runs <app>: call `runs` with `repo`")).not.toContain("run_log");
59 });
60 });
61
4062 test("a renamed tool is caught", () => {
4163 expect(toolsNamed("call `tell_renamed` with `repo`").filter((n) => !tools.has(n))).toEqual(["tell_renamed"]);
4264 expect(backticked("`a_b` and `C`")).toEqual(["a_b"]);
@@ -98,7 +120,7 @@ const verbDiff = (a: string, b: string): { onlyA: string[]; onlyB: string[] } =>
98120
99121 describe("the two plugin commands offer the same verbs", () => {
100122 test("Claude Code and OpenCode have the same set", () => {
101 expect(verbsOf(claudeText())).toEqual(["decide", "plan", "preview", "pulls", "status", "tell"]);
123 expect(verbsOf(claudeText())).toEqual(["decide", "log", "plan", "preview", "pulls", "runs", "status", "tell"]);
102124 expect(verbDiff(claudeText(), opencodeText())).toEqual({ onlyA: [], onlyB: [] });
103125 });
104126

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs