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

The OpenCode plugin's forge command gains `plan`: show the planner's wave and nod or redirect it #5347

Merged opened this

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

Merged

scout/forge:scout/5322-the-opencode-plugin-s-forge-command-gain → main 4 commits fork 2 threads · 2 unresolved

  • Merged. scout/5322-the-opencode-plugin-s-forge-command-gain was merged into main as cbd75ef.

Review

Sign in to review this change.
3 files changed +43 −4
plugins/opencode/README.md +4 −1
@@ -2,7 +2,10 @@
22
33 Copy `forge.ts` into `.opencode/plugin/` (or list this package under `plugin`
44 in `opencode.json`), and set `FORGE_URL` and `FORGE_TOKEN`. It registers the
5 forge's MCP server and the `forge` command: `tell`, `status`, `decide`.
5 forge's MCP server and the `forge` command: `tell`, `status`, `decide`, `plan`.
6 `/forge plan` shows the planner's next wave (the first wave-proposal row of `asks`);
7 `/forge plan approve`, `redirect <words>` or `later` steers it through `asks_proposal`,
8 applied only after you say yes. Only the owner may act on a proposal.
69 Code Mode can also read `GET /contract/openapi.json`.
710
811 `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 +3 −2
@@ -1,6 +1,6 @@
11 /**
22 * forge for OpenCode: registers the forge's MCP server and a `forge`
3 * command (tell, status, decide). Set FORGE_URL and FORGE_TOKEN.
3 * command (tell, status, decide, plan). 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";
@@ -9,6 +9,7 @@ const COMMAND = `Use the forge MCP tools for: $ARGUMENTS
99 - tell <app> <words>: call \`tell\` with repo and text; pass context { page, run, commit, said }.
1010 - status: call \`now\` and \`asks\`; say in three sentences what runs and what waits on the person.
1111 - decide approve|redirect|decline: call \`asks\`; approve -> \`asks_act\` ship, decline -> \`asks_act\` close, redirect -> \`say\`. Plan first, apply (dryRun false) only after the person says yes.
12 - 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.
outdated

Bare backticks inside the template literal end the string early and cause a syntax error. Escape them as \` like the lines above.

1213 Never merge: a person lands changes.`;
1314
1415 export const Forge: Plugin = async () => ({
@@ -26,7 +27,7 @@ export const Forge: Plugin = async () => ({
2627 };
2728 config["command"] = {
2829 ...config["command"],
29 forge: { description: "Tell, status or decide on your forge", template: COMMAND },
30 forge: { description: "Tell, status, decide or plan on your forge", template: COMMAND },
3031 };
3132 },
3233 });
test/plugins.test.ts +36 −1
@@ -1,5 +1,5 @@
11 /** The thin plugins name only MCP tools the forge lists, so a rename there fails here. */
2 import { expect, test } from "bun:test";
2 import { describe, expect, test } from "bun:test";
33 import { readFileSync } from "node:fs";
44 import { toolNames } from "../src/http/mcp.ts";
55
@@ -50,3 +50,38 @@ test("the Claude Code command names plan's tools, and the forge lists them", ()
5050 }
5151 expect(claudeText()).toContain("`proposal.id`");
5252 });
53
54 describe("the OpenCode plan command", () => {
55 test("steers the planner's wave through asks_proposal, after a yes", () => {
56 const text = opencodeText();
57 expect(text).toContain("plan [approve|redirect <words>|later]");
58 expect(toolsNamed(text)).toContain("asks_proposal");
59 expect(tools.has("asks_proposal")).toBe(true);
60 expect(text).toMatch(/only after the person says yes/);
61 });
62 });
63
64 describe("the OpenCode plugin module", () => {
65 test("loads and registers a forge command whose template has plan", async () => {
66 const saved = { url: process.env["FORGE_URL"], token: process.env["FORGE_TOKEN"] };
67 process.env["FORGE_URL"] = "https://forge.example";
68 process.env["FORGE_TOKEN"] = "t";
69 try {
70 // A computed specifier: the plugin's type-only import of OpenCode's package is not a dependency here, so tsc must not follow it.
71 const file = "../plugins/opencode/forge.ts";
72 const { Forge } = await import(file);
73 const config: Record<string, any> = {};
74 const hooks: any = await (Forge as any)({});
75 await hooks.config(config);
76 const template: string = config["command"].forge.template;
77 expect(template).toContain("plan [approve|redirect <words>|later]");
78 expect(template).toContain("`asks_proposal`");
79 expect(template).toEndWith("a person lands changes.");
80 } finally {
81 for (const [k, v] of [["FORGE_URL", saved.url], ["FORGE_TOKEN", saved.token]] as const) {
82 if (v === undefined) delete process.env[k];
83 else process.env[k] = v;
84 }
85 }
86 });
87 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs