Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions .claude/skills/opencode-plugin-idioms/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
name: opencode-plugin-idioms
description: Event hook payload shapes, session lifecycle timing, toast/TUI-route render boundaries, and child-session tagging conventions for OpenCode server (@opencode-ai/plugin) and TUI (@opencode-ai/plugin/tui) plugins, grounded in the vendored OpenCode source at references/opencode.
type: prompt
whenToUse: Load before modifying src/server.ts, src/tui.tsx, src/tui/board/board.tsx, src/tui/board/commands.tsx, src/tui/board/store.tsx, src/server/intake.ts, or src/server/validator/spawn.ts — or when debugging a missing toast, a session-creation race, a permission/gate ordering bug, or a plugin-spawned child session re-triggering its own handler.
whenToUse: Load before modifying src/server.ts, src/tui.tsx, src/tui/board/board.tsx, src/tui/board/commands.tsx, src/tui/board/store.tsx, src/tui/updates.ts, src/tui/update-manager.ts, src/server/intake.ts, or src/server/validator/spawn.ts — or when debugging a missing toast, a session-creation race, a permission/gate ordering bug, or a plugin-spawned child session re-triggering its own handler.
---

Version check: repo pins `@opencode-ai/plugin@1.17.13`; vendored copy at `references/opencode/packages/plugin/package.json` should match — verify there, never from memory.
Version check: repo pins `@opencode-ai/plugin@1.17.18`; vendored copy at `references/opencode/packages/plugin/package.json` should match — verify there, never from memory.

## 1. Event hook: what fires, exact payload, and ordering guarantees

Expand Down Expand Up @@ -140,6 +140,10 @@ The shared `App()` render tree switches on route type and renders a plugin's cus

When `route.data.type === "plugin"` (this repo's `api.route.navigate(ROUTE)` in `src/tui.tsx:29`), neither `<Switch>` `<Match>` fires, so `<Home/>`/`<Session/>` (and their embedded `<Toast/>`) don't mount. **A toast call made while the kagan board is the active route is not dropped by a race or clobbered visually — there is simply no `<Toast/>` component mounted to render it.** It is silently swallowed until the user navigates back to `home` or a `session` route. Use the board's own notice overlay for board-route feedback.

Automatic update status deliberately uses a separate dual surface: `api.ui.toast` once when the
current route is `home` or `session`, and persistent footer state on the Kagan route. It never enters
`store.notify`; operational task warnings retain the board Notice queue.

## 4. Spawning child/helper sessions without recursive triggering

`SessionInfo` has first-class `parentID?: SessionID` and free-form `metadata?: Record<string, unknown>` — `references/opencode/packages/schema/src/v1/session.ts:550,559`. There is **no built-in "internal/helper session" flag** anywhere in the schema or in any bundled OpenCode plugin (`references/opencode/packages/opencode/src/plugin/*` only contains auth-provider plugins; none spawn sessions). The tagging convention is project-invented, not an OpenCode-documented idiom — state that plainly rather than citing it as upstream guidance.
Expand Down
14 changes: 14 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
version: 2
updates:
- package-ecosystem: bun
directory: /
schedule:
interval: daily
groups:
opencode:
patterns:
- "@opencode-ai/plugin"
- "@opencode-ai/sdk"
update-types:
- minor
- patch
2 changes: 1 addition & 1 deletion .specs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ plugin today.
## Files and authority order

1. [`requirements.md`](./kagan-supervision-board/requirements.md) — what must be true. EARS-style
acceptance criteria with stable numbers (R1–R17); code and tests trace to these. When documents
acceptance criteria with stable numbers (R1–R18); code and tests trace to these. When documents
disagree, this one wins.
2. [`design.md`](./kagan-supervision-board/design.md) — how it's built: architecture constraints,
the metadata model, and the key flows, each traced to the requirements it satisfies.
Expand Down
38 changes: 30 additions & 8 deletions .specs/kagan-supervision-board/design.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Design — Kagan Supervision Board

Technical design backing [requirements.md](./requirements.md). Traces each subsystem to the
requirements it satisfies (R1–R17).
requirements it satisfies (R1–R18).

## Overview

Expand All @@ -13,12 +13,13 @@ Kagan ships as two OpenCode plugin surfaces from one package:
state of its own; everything authoritative lives in session metadata.
- **TUI plugin** (`src/tui.tsx`, default export `{ id, tui }`) — renders the board route, the
create-task dialog, and the triage/merge dialogs; owns user-initiated actions (create, move,
triage, approve, send-back) via a Solid store.
triage, approve, send-back) via a Solid store and owns automatic update checking, preparation,
promotion, and feedback.

Both receive the plugin `options` object (OpenCode config). The option reference in
[`docs/reference/configuration.md`](../../docs/reference/configuration.md) is canonical.

## Architecture constraints (verified against @opencode-ai/{plugin,sdk} 1.17.13)
## Architecture constraints (verified against @opencode-ai/{plugin,sdk} 1.17.18)

These constraints shape the design and must hold for it to be correct:

Expand All @@ -38,8 +39,14 @@ These constraints shape the design and must hold for it to be correct:
`info.id`); `session.idle` carries `properties.sessionID`.
- **`promptAsync`** starts an agent turn and returns immediately; used for every agent-starting
prompt (intake, validator, auto-start, send-back).
- **Toasts are invisible while the board route is active**, so all board feedback uses the board's
own `Notice` overlay via the store, never `api.ui.toast`.
- **Toasts are invisible while the board route is active**, so task and board-action feedback uses
the board's own `Notice` overlay. Automatic update feedback is the deliberate exception: one host
toast on home/session routes plus persistent board footer state, never the Notice queue (R3.8,
R18.5).
- **Plugin activation ends at restart.** `api.plugins.add` can resolve, compatibility-check, import,
and validate an exact package during the current TUI process, but server hooks already loaded by
OpenCode cannot be replaced safely. Compatible npm updates therefore promote only during
`api.lifecycle.onDispose`; restart loads the promoted wrapper (R18).
- **The host dialog stack already centers each dialog element** in a full-screen overlay, so the
create-task dialog renders bare content and calls `dialog.setSize`, rather than wrapping itself in
another overlay.
Expand Down Expand Up @@ -78,12 +85,14 @@ same-session writes so concurrent event handlers cannot clobber each other.
| `server/intake.ts` | `spawnIntake` read-only child | R4 |
| `server/validator/` | `spawnValidator` read-only child, diff+context prompt, validator model rotation | R9 |
| `checks/runner.ts` | Setup/check command runner with timeout, skipped/ran step evidence, and output tail | R9.11–15, R17.2–3 |
| `tui.tsx` | TUI composition, routes, subscriptions, automatic-update orchestration, and route-aware update toast | R3, R18.3–5, R18.9 |
| `tui/session/`, `tui/tasks/` | TUI data ops: list/create, serialized metadata patching, send-back, merge, triage, approval, retry | R1, R3.2, R11, R12, R17.8 |
| `tui/dialogs/create-task.tsx` | Custom OpenTUI create dialog, including configured/custom task scope selection | R1.1–1.4, R1.9–10 |
| `tui/board/store.tsx` | Solid board store: grouping, ordering, selection, move gating, refresh, notices | R3, R7, R17.4–5 |
| `tui/board/store.tsx` | Solid board store: grouping, ordering, selection, move gating, refresh, notices, and update status | R3, R7, R17.4–5, R18 |
| `tui/board/commands.tsx` | Key bindings and dialog flows: create, move, triage, approve/merge, send-back, retry, task details view | R5.2, R10, R11, R12, R17.3, R17.7 |
| `tui/board/board.tsx` / `column.tsx` / `card.tsx` | Board layout, column headers with cap, cards with task number and badges; board footer shows version and update banner | R3, R3.7–8, R7.4 |
| `tui/updates.ts` | npm dist-tag lookup with TTL cache; semver comparison for update banner | R3.8 |
| `tui/board/board.tsx` / `column.tsx` / `card.tsx` | Board layout, column headers with cap, cards with task number and badges; board footer shows version and persistent update status | R3, R3.7–8, R7.4, R18 |
| `tui/updates.ts` | one-hour npm latest/manifest cache and `engines.opencode` classification | R3.8, R18.1, R18.4, R18.6–7 |
| `tui/update-manager.ts` | exact-release preparation, hostile-path validation, disposal promotion/restore, and successful-load cleanup | R18.2–3, R18.6–9 |
| `tui/format.ts` | Card badges, age/diff/subtask formatting | R3.6 |
| `tui/dialogs/task-details.tsx` | Read-only task details view from live session metadata and diff stats | R17.7 |
| `tui/dialogs/onboarding.tsx` | First-run board tour and opt-out persistence | R17.6 |
Expand Down Expand Up @@ -205,6 +214,19 @@ agent as a failed tool call carrying the thrown message; a generic OpenCode sess
task, role, nor parent back-pointer) is left untouched, and non-`bash` tool calls and non-push bash
commands are ignored.

**Automatic update (R18).** Only TUI instances loaded from bare `@kagan-sh/kagan` or explicit
`@latest` resolve npm `latest`; exact pins and file installs return before network access. A newer
clean release is classified only after its manifest supplies a valid `engines.opencode` range.
Compatible latest is prepared exactly through `api.plugins.add`, which independently performs the
host compatibility check and imports the package without activating a duplicate `kagan` plugin id.
The manager proves that the current and prepared targets are non-symlinked
`opencode/packages/@kagan-sh/kagan@…/node_modules/@kagan-sh/kagan` wrappers before writing one
sibling marker. Its disposal callback renames current to one backup, promotes prepared to
`kagan@latest`, and restores current if promotion fails. The next successful load of the marker's
version removes only that validated backup and marker. Ready/blocked status is a dedicated store
signal: home/session routes receive one host toast, while the board renders persistent footer text
and never consumes Notice capacity.

## Configuration

The canonical option list is [`docs/reference/configuration.md`](../../docs/reference/configuration.md).
Expand Down
34 changes: 31 additions & 3 deletions .specs/kagan-supervision-board/requirements.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,8 @@ status cues, so that I can see at a glance what needs my attention.
6. WHILE a Backlog task is intake-ready, its card SHALL display a distinct border color and an
`intake ok` badge indicating it is eligible to move to In Progress.
7. THE board footer SHALL display the plugin name and version.
8. WHERE the published npm `latest` dist-tag is a newer clean release (`x.y.z`) than the
installed plugin version, the board footer SHALL append an update-available indicator naming
that version.
8. WHERE automatic update status is ready or blocked, the board footer SHALL persist the prepared
version or the OpenCode compatibility requirement respectively (R18).
9. WHILE a task is in Review and not yet approved, the board SHALL sort its card ahead of other
cards in that column.
10. WHILE a Backlog task's intake outcome is `failed`, the card SHALL display a distinct failed
Expand Down Expand Up @@ -460,3 +459,32 @@ so that failures, handoffs, and supervision evidence are visible instead of hidd
child sessions instead of the session default.
13. WHERE the plugin options set `validatorModels`, Kagan SHALL rotate reviewer models per the
configuration reference when spawning validator sessions.

---

## Requirement 18 — Automatic npm updates

**User Story:** As an npm user, I want Kagan to prepare compatible releases automatically and tell
me when OpenCode blocks one, so that updating requires only a restart and no compatibility judgment.

#### Acceptance Criteria

1. WHERE Kagan was loaded from bare `@kagan-sh/kagan` or explicit `@latest`, Kagan SHALL check npm
`latest` no more than once per successful one-hour cache window.
2. WHEN npm `latest` is newer and its `engines.opencode` range accepts the running OpenCode version,
Kagan SHALL prepare that exact release through the TUI plugin API without changing plugin config.
3. WHEN a compatible release has been prepared THEN Kagan SHALL keep the running wrapper unchanged
until TUI disposal, promote the prepared wrapper during disposal, and require an OpenCode restart
to activate it.
4. WHEN npm `latest` requires a different OpenCode version THEN Kagan SHALL leave the current
wrapper unchanged and SHALL persistently name the required OpenCode range on the board.
5. WHERE automatic update status is ready or blocked on a home or session route, Kagan SHALL show
one host toast; update status SHALL NOT enter the board Notice queue.
6. IF Kagan was loaded from an exact npm pin or a local/file source THEN Kagan SHALL NOT query npm,
prepare a release, or mutate its wrapper.
7. IF a registry request, manifest validation, download, import, or cache-path validation fails THEN
Kagan SHALL remain quiet and SHALL leave the current wrapper unchanged.
8. WHEN promotion of the prepared wrapper fails after the current wrapper was moved to backup THEN
Kagan SHALL restore the current wrapper immediately.
9. WHEN the prepared version loads successfully after restart THEN Kagan SHALL remove its validated
backup and marker without deleting any broader OpenCode cache path.
6 changes: 6 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,12 @@ spec authority and read order. `src/domain/task/metadata.ts` is the authoritativ
board that does not repaint. The architecture guard enforces this.
- Use `TuiPluginApi` for renderer dimensions, keyboard input, and keymap layers; do not import
OpenTUI/Solid context hooks for those surfaces.
- Automatic updates are TUI-only. `src/tui/updates.ts` checks npm `latest` for bare/`@latest`
installs, prepares compatible exact releases through `api.plugins.add`, promotes only on
`api.lifecycle.onDispose`, and toasts only on home/session routes. Never add a server update hook
or touch exact pins and file installs. `src/tui/update-manager.ts` treats cache paths as hostile:
it accepts only non-symlinked `@kagan-sh/kagan` wrappers and removes only its own marker and single
backup; never broaden that deletion.

## External APIs

Expand Down
4 changes: 3 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ bun run plugin:install
```

This installs your local build into OpenCode. Open a project in OpenCode and launch the board to
see your changes. Run `bun run plugin:reset` to undo it.
see your changes. Local/file installs, including `plugin:install:prod`, intentionally skip automatic
update checks; only a published bare/`@latest` npm install exercises that path. Run
`bun run plugin:reset` to undo the install.

## The one command that has to pass

Expand Down
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ The agent never touches your checkout. It works on a `kagan/<slug>` branch in it

## Install

You need [OpenCode](https://opencode.ai/) **1.17.13** or newer (below **1.18.0** — see `engines.opencode` in `package.json`).
You need [OpenCode](https://opencode.ai/) **1.17.13** or newer (below **1.18.0** — see
`engines.opencode` in `package.json`).

From npm:

Expand All @@ -35,7 +36,11 @@ Or add a local clone to both OpenCode config files:

Open the board with `/kagan` from the command palette, the `kagan` palette command, or `<leader>k` (the leader key defaults to `ctrl+x`).

OpenCode caches the version it first installs and never re-checks, so the board footer flags when a newer release is out — see [Updating](https://docs.kagan.sh/quickstart/#updating) to move to it.
For bare `@kagan-sh/kagan` and explicit `@latest` installs, Kagan prepares compatible `latest`
releases automatically. Ready or blocked updates appear once as a host toast on home/session routes
and persist in the board footer. Restart OpenCode when an update is ready; if `latest` needs a newer
OpenCode, Kagan names the required range. Exact version pins and local installs remain unchanged. See
[Updating](https://docs.kagan.sh/quickstart/#updating).

Pass options by using the array-of-array form, or open `/kagan-settings` from the project — see the [configuration reference](https://docs.kagan.sh/reference/configuration/).

Expand Down
Loading