diff --git a/packages/forgekeeper/README.md b/packages/forgekeeper/README.md index aa00dd5..43afc6e 100644 --- a/packages/forgekeeper/README.md +++ b/packages/forgekeeper/README.md @@ -296,8 +296,9 @@ title prefix. It doesn't poll or send review notifications, and the CLI shows no ## RPC -The server registers an [RPC](https://opencode.ai/v2/docs/build/plugins/rpc) that the CLI -uses, defined in `src/rpc.ts`: +The server registers an [RPC](https://opencode.ai/v2/docs/build/plugins/rpc) with the ID +`opencode-forgekeeper`, defined in `src/rpc.ts`. The CLI uses it, and other plugins can call +`target` and listen for `targetChanged`: - `target({ sessionID })` returns `{ url, issueUrl?, targets }` for explicit targets, and `{}` in branch mode. `targets` lists every target as `{ url, issueUrl? }`. `url` and @@ -318,18 +319,25 @@ plugin instance watches its own sessions. ## Migrating from opencode-forge-session-title -Replace `opencode-forge-session-title` with `opencode-forgekeeper` in `opencode.json`. Never -load both: the server keeps `opencode-forge-session-title`'s plugin and RPC IDs, so the two -would collide. - -| Entry point | Plugin ID | Stored state | -| ----------- | ------------------------------ | ---------------------------------------------------------------------------------------- | -| Server | `opencode-forge-session-title` | Session targets, owned title prefixes, human-review baselines, and pending notifications | -| CLI | `pedropombeiro.forgekeeper` | None | - -Because the IDs match, stored targets carry over, and callers of the `target` RPC keep working. -The server reads a session's single stored `target` as a list of one, and writes `targets` from -the next change on. +Replace `opencode-forge-session-title` with `opencode-forgekeeper` in `opencode.json`. Upgrading +from `opencode-forgekeeper` 1.x works the same way, because 1.x used the same IDs as +`opencode-forge-session-title`. + +| Entry point | Plugin ID | Stored state | +| ----------- | --------------------------- | ---------------------------------------------------------------------------------------- | +| Server | `opencode-forgekeeper` | Session targets, owned title prefixes, human-review baselines, and pending notifications | +| CLI | `pedropombeiro.forgekeeper` | None | + +OpenCode scopes plugin storage by plugin ID, so nothing stored under the former ID, +`opencode-forge-session-title`, carries over: + +- Sessions start in branch-based naming. Ask the agent to set their targets again. +- A title that kept a single-reference prefix from before, such as `[!456] Review`, can get a + second prefix, such as `[#12, !45] [!456] Review`. Remove the older one by hand. Older prefixes + with two references, such as `[#12, !45]`, are replaced automatically. +- The first look at each PR/MR records a new review baseline, so comments posted before then + aren't announced. +- Callers of the RPC need the new ID, `opencode-forgekeeper`. With default options, the new footer and review notifications start right away, and notifications resume the session. Set `reviewStatus` to `false` to keep only titles, or diff --git a/packages/forgekeeper/src/index.ts b/packages/forgekeeper/src/index.ts index 06fa602..42c3859 100644 --- a/packages/forgekeeper/src/index.ts +++ b/packages/forgekeeper/src/index.ts @@ -1,11 +1,8 @@ -// Merges opencode-forge-session-title and forge-review-status. This server -// entry point owns each session's targets: the `set_session_target` tool, the -// agent guidance, and the title prefix. It also watches the PRs/MRs of the -// sessions that CLIs show and tells sessions about their reviews. -// opencode.json loads this directory through a path entry; OpenCode then loads -// tui.tsx, which renders the status that this entry point serves, in the CLI. -// -// The ID is opencode-forge-session-title's, so stored targets carry over. +// This server entry point owns each session's targets: the +// `set_session_target` tool, the agent guidance, and the title prefix. It +// also watches the PRs/MRs of the sessions that CLIs show and tells sessions +// about their reviews. OpenCode loads the package's CLI entry point, tui.tsx, +// which renders the status that this entry point serves. import type { Plugin } from '@opencode/plugin'; import { exec, execWithTimeout } from './exec'; import { Forges, hostsFrom } from './forges'; @@ -14,7 +11,7 @@ import { createTitleLookups } from './lookups'; import { setupServer } from './server'; export default { - id: 'opencode-forge-session-title', + id: 'opencode-forgekeeper', async setup(ctx) { const hosts = hostsFrom((ctx.options ?? {}) as Record); // Status polling and feedback fetches share one limit across sessions, diff --git a/packages/forgekeeper/src/rpc.ts b/packages/forgekeeper/src/rpc.ts index 210eb4d..d364d63 100644 --- a/packages/forgekeeper/src/rpc.ts +++ b/packages/forgekeeper/src/rpc.ts @@ -1,7 +1,6 @@ // Contract between the server, which stores each session's targets and -// watches their PRs/MRs, and the CLI, which renders their status. The ID is -// the one that opencode-forge-session-title registered, so callers written -// for that package keep working. +// watches their PRs/MRs, and the CLI, which renders their status. Other +// plugins can call `target` and listen for `targetChanged` too. // // Status keys are session IDs, or "" for the checkout outside a session. A // key is relative to the location the call or event names, because each @@ -36,7 +35,7 @@ const status = { } as const; export const ForgeSessionRpc = { - id: 'opencode-forge-session-title', + id: 'opencode-forgekeeper', methods: { // The session's explicit targets, or `{}` in automatic branch mode. target: { diff --git a/packages/forgekeeper/src/server.test.ts b/packages/forgekeeper/src/server.test.ts index 250f62a..9516d17 100644 --- a/packages/forgekeeper/src/server.test.ts +++ b/packages/forgekeeper/src/server.test.ts @@ -462,9 +462,9 @@ describe('session targets', () => { await reloaded.cleanup(); }); - test('reads a single target stored before sessions could have several', async () => { + test('keeps the inferred issue of a stored target when adding another', async () => { const storage = new Map([ - ['sessions/one', { target: { url: mr, branchIssue: '123' }, prefix: '[#123, !45]' }], + ['sessions/one', { targets: [{ url: mr, branchIssue: '123' }], prefix: '[#123, !45]' }], ]); const app = await harness({ storage, sourceBranches: new Map([['456', '999-other']]) }); expect(await app.target()).toEqual({ url: mr, targets: [{ url: mr }] }); diff --git a/packages/forgekeeper/src/server.ts b/packages/forgekeeper/src/server.ts index bdd32f3..19301a1 100644 --- a/packages/forgekeeper/src/server.ts +++ b/packages/forgekeeper/src/server.ts @@ -22,7 +22,7 @@ const SKIP_BRANCHES = new Set(['master', 'main', 'HEAD', 'develop']); // Stored per session. `prefix` is the title prefix this plugin last wrote, so // it can be replaced without touching text the user added. Before sessions // could have several targets, the state held a single `target`. -type StoredState = { target?: Target; targets?: Target[]; prefix?: string }; +type StoredState = { targets?: Target[]; prefix?: string }; type SessionState = { targets: Target[]; prefix?: string }; const GUIDANCE = [ @@ -97,11 +97,7 @@ export async function setupServer( const started = revision(sessionID); const stored = ((await ctx.storage.get(`sessions/${sessionID}`)) as StoredState | undefined) ?? {}; - const targets = Array.isArray(stored.targets) - ? stored.targets - : stored.target - ? [stored.target] - : []; + const targets = Array.isArray(stored.targets) ? stored.targets : []; if (revision(sessionID) === started) known.set(sessionID, targets); return { targets, ...(stored.prefix ? { prefix: stored.prefix } : {}) }; } diff --git a/packages/forgekeeper/src/status-server.ts b/packages/forgekeeper/src/status-server.ts index aa2614e..dbff743 100644 --- a/packages/forgekeeper/src/status-server.ts +++ b/packages/forgekeeper/src/status-server.ts @@ -63,9 +63,6 @@ const pendingPrefix = (sessionID: string) => `${PENDING_PREFIX}${encodeURICompon const pendingKey = (sessionID: string, url: string) => `${pendingPrefix(sessionID)}${encodeURIComponent(url)}`; const feedbackRecordKey = (key: string) => `${FEEDBACK_PREFIX}${encodeURIComponent(key)}`; -// Whole logs from before records had their own keys. They're removed, so the -// first look at each PR/MR records a new baseline instead of replaying. -const LEGACY_KEYS = ['status/humanReviewFeedback', 'status/automatedReviewPending']; // The title without the prefix this plugin wrote. export function userTitle( @@ -297,10 +294,9 @@ export async function setupStatus( }); invalidate = (sessionID) => service.invalidate(sessionID); - // Housekeeping, on the write queue so it doesn't hold up setup: removes the - // former whole logs, and feedback records not seen for a long time. + // Housekeeping, on the write queue so it doesn't hold up setup: removes + // feedback records not seen for a long time. write(async () => { - for (const key of LEGACY_KEYS) await ctx.storage.remove(key); const at = (deps.now ?? Date.now)(); for (const { key, value } of await scanAll(FEEDBACK_PREFIX)) { if (isExpired(value as FeedbackRecord, at)) await ctx.storage.remove(key);