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

Both plugins gain `/forge preview <url>` and `/forge pulls <app>`: where am I, and what is open #6107

Merged opened this

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

Merged

scout/forge:scout/6089-both-plugins-gain-forge-preview-url-and → main 6 commits fork 1 thread · 1 unresolved

  • Merged. scout/6089-both-plugins-gain-forge-preview-url-and was merged into main as a48ee29.

Review

Sign in to review this change.
6 files changed +26 −10
docs/MCP.md +2 −1
@@ -331,4 +331,5 @@ answers
331331 from `context.page`, which is any http(s) address appended as context, not
332332 linked as the origin.
333333 - **Plugins.** `plugins/opencode` and `plugins/claude-code` register this
334 server and add a `forge` command (tell, status, decide). They hold no logic.
334 server and add a `forge` command (tell, status, decide, plan, preview,
335 pulls). They hold no logic.
plugins/claude-code/README.md +6 −1
@@ -3,8 +3,13 @@
33 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
6 server and a `/forge` command: `tell`, `status`, `decide`, `plan`.
6 server and a `/forge` command: `tell`, `status`, `decide`, `plan`, `preview`,
7 `pulls`.
78
89 `/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.
910
11 `/forge preview <url>` says in two lines the app, environment, pull, check
12 and who is working at that URL. `/forge pulls <app>` lists the app's open
13 pulls with their check.
14
1015 `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
3 argument-hint: tell <app> <words> | status | decide <approve|redirect|decline> [words] | plan [approve|redirect <words>|later]
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>
44 ---
55
66 Use the `forge` MCP server's tools for `$ARGUMENTS`.
@@ -9,5 +9,7 @@ Use the `forge` MCP server's tools for `$ARGUMENTS`.
99 - `status`: call `now` and `asks`; say in three sentences what is running and what waits on the person.
1010 - `decide approve|redirect|decline`: call `asks`, take the first row; approve → `asks_act` with action `ship`, decline → `asks_act` with action `close`, redirect → `say` with the person's words. Plan first (`dryRun` defaults to true), show what would change, and apply (`dryRun: false`) only after the person says yes.
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.
12 - `preview <url>`: call `preview` with `url`; say in two lines the app, environment, pull, check and who is working.
13 - `pulls <app>`: call `pulls` with `repo` = the app (owner/name); list the open pulls, one line each with its check.
1214
1315 Keep what you relay short. Never merge: a person lands changes.
plugins/opencode/README.md +7 −1
@@ -3,10 +3,16 @@
33 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
6 forge's MCP server and the `forge` command: `tell`, `status`, `decide`, `plan`.
6 forge's MCP server and the `forge` command: `tell`, `status`, `decide`, `plan`,
7 `preview`, `pulls`.
78 `/forge plan` shows the planner's next wave (the first wave-proposal row of `asks`);
89 `/forge plan approve`, `redirect <words>` or `later` steers it through `asks_proposal`,
910 applied only after you say yes. Only the owner may act on a proposal.
outdated

The Code Mode sentence on the next line now reads as part of this paragraph. Put a blank line after this one or move it above.

11
12 `/forge preview <url>` says in two lines the app, environment, pull, check
13 and who is working at that URL. `/forge pulls <app>` lists the app's open
14 pulls with their check.
15
1016 Code Mode can also read `GET /contract/openapi.json`.
1117
1218 `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). Set FORGE_URL and FORGE_TOKEN.
3 * command (tell, status, decide, plan, preview, pulls). 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";
@@ -10,6 +10,8 @@ const COMMAND = `Use the forge MCP tools for: $ARGUMENTS
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.
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.
13 - preview <url>: call \`preview\` with url; say in two lines the app, environment, pull, check and who is working.
14 - pulls <app>: call \`pulls\` with repo; list the open pulls, one line each with its check.
1315 Never merge: a person lands changes.`;
1416
1517 export const Forge: Plugin = async () => ({
@@ -27,7 +29,7 @@ export const Forge: Plugin = async () => ({
2729 };
2830 config["command"] = {
2931 ...config["command"],
30 forge: { description: "Tell, status, decide or plan on your forge", template: COMMAND },
32 forge: { description: "Tell, status, decide, plan, preview or pulls on your forge", template: COMMAND },
3133 };
3234 },
3335 });
test/plugins.test.ts +3 −3
@@ -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"]);
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"]);
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));
@@ -32,7 +32,7 @@ for (const [label, text] of [
3232 ] as const) {
3333 test(`${label} names only tools the forge has`, () => {
3434 const named = toolsNamed(text());
35 for (const core of ["tell", "now", "asks", "asks_act", "say"]) expect(named).toContain(core);
35 for (const core of ["tell", "now", "asks", "asks_act", "say", "preview", "pulls"]) expect(named).toContain(core);
3636 expect(named.filter((n) => !tools.has(n))).toEqual([]);
3737 });
3838 }
@@ -98,7 +98,7 @@ const verbDiff = (a: string, b: string): { onlyA: string[]; onlyB: string[] } =>
9898
9999 describe("the two plugin commands offer the same verbs", () => {
100100 test("Claude Code and OpenCode have the same set", () => {
101 expect(verbsOf(claudeText())).toEqual(["decide", "plan", "status", "tell"]);
101 expect(verbsOf(claudeText())).toEqual(["decide", "plan", "preview", "pulls", "status", "tell"]);
102102 expect(verbDiff(claudeText(), opencodeText())).toEqual({ onlyA: [], onlyB: [] });
103103 });
104104

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs