diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index b7d91bc..f7d0a42 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -3,11 +3,11 @@ ## Verification -- [ ] `bun test` -- [ ] `bun run typecheck` -- [ ] `bun run build` +- Commands run: +- Manual checks: -## Notes +## Impact -- Hard gates affected: -- Manual OpenCode integration: +- User-visible behavior: +- Compatibility, privacy, or packaging: +- Screenshots for picker UI changes: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ce44597..5f384b7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -79,6 +79,7 @@ jobs: - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: ${{ matrix.node }} + package-manager-cache: false - run: bun install --frozen-lockfile - run: bun run build - name: Exercise exact npm tarball as a Node consumer @@ -97,6 +98,7 @@ jobs: - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 22 + package-manager-cache: false - name: Install dependencies and tested OpenCode runtime run: | bun install --frozen-lockfile @@ -106,7 +108,7 @@ jobs: OPENCODE_BIN: ${{ runner.temp }}/opencode-runtime/node_modules/.bin/opencode picker-build: - name: Picker build + name: Picker build (Linux x64) runs-on: ubuntu-22.04 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -122,6 +124,7 @@ jobs: - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 22 + package-manager-cache: false - name: Install pinned Rust toolchain run: rustup toolchain install 1.97.1 --profile minimal --no-self-update - run: bun install --frozen-lockfile @@ -130,6 +133,11 @@ jobs: - name: Exercise rendered picker theme and layout run: bun run test:picker:rendered - run: bun run build:picker + - name: Verify native Linux x64 picker + run: bun run check:picker-host + env: + MODEL_DISPATCH_EXPECTED_PICKER_PLATFORM: linux + MODEL_DISPATCH_EXPECTED_PICKER_ARCH: x64 - run: cargo test --manifest-path picker/src-tauri/Cargo.toml --locked - run: bun run build - name: Install tested OpenCode runtime @@ -170,6 +178,7 @@ jobs: - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 22 + package-manager-cache: false - name: Install pinned Rust toolchain run: rustup toolchain install 1.97.1 --profile minimal --no-self-update - name: Verify native ARM64 toolchain @@ -184,6 +193,11 @@ jobs: - run: bun install --frozen-lockfile working-directory: picker - run: bun run build:picker + - name: Verify native Linux ARM64 picker + run: bun run check:picker-host + env: + MODEL_DISPATCH_EXPECTED_PICKER_PLATFORM: linux + MODEL_DISPATCH_EXPECTED_PICKER_ARCH: arm64 - name: Verify native ARM64 picker architecture and linkage shell: bash run: | @@ -218,6 +232,55 @@ jobs: WEBKIT_DISABLE_COMPOSITING_MODE: "1" WEBKIT_DISABLE_DMABUF_RENDERER: "1" + picker-build-nonlinux: + name: Picker build (${{ matrix.label }}) + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + include: + - label: macOS ARM64 + platform: macos + arch: arm64 + runner: macos-15 + - label: Windows x64 + platform: windows + arch: x64 + runner: windows-2025 + - label: Windows ARM64 + platform: windows + arch: arm64 + runner: windows-11-arm + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 + with: + bun-version: 1.3.14 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 22 + package-manager-cache: false + - name: Install pinned Rust toolchain + run: rustup toolchain install 1.97.1 --profile minimal --no-self-update + - run: bun install --frozen-lockfile + - run: bun install --frozen-lockfile + working-directory: picker + - run: bun run build:picker + - name: Verify native picker host and architecture + run: bun run check:picker-host + env: + MODEL_DISPATCH_EXPECTED_PICKER_PLATFORM: ${{ matrix.platform }} + MODEL_DISPATCH_EXPECTED_PICKER_ARCH: ${{ matrix.arch }} + - run: cargo test --manifest-path picker/src-tauri/Cargo.toml --locked + - name: Exercise Apple notarization log policy + if: matrix.platform == 'macos' + shell: bash + run: bash scripts/test-apple-notary-log-validator.sh + - name: Exercise native picker ready protocol + run: bun run test:picker-ready + packaging: name: Packaging checks runs-on: ubuntu-latest @@ -231,6 +294,7 @@ jobs: - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 22 + package-manager-cache: false - run: bun install --frozen-lockfile - run: bun run check:release-version - run: bun run build @@ -241,4 +305,6 @@ jobs: set -euo pipefail pkg="$(npm pack --silent)" tmp="$(mktemp -d)" - npm install --ignore-scripts --prefix "$tmp" "./$pkg" + peer_version="$(node -p 'require("./package.json").devDependencies["@opencode-ai/plugin"]')" + npm install --ignore-scripts --no-audit --no-fund --package-lock=false \ + --prefix "$tmp" "@opencode-ai/plugin@$peer_version" "./$pkg" diff --git a/.github/workflows/compatibility.yml b/.github/workflows/compatibility.yml new file mode 100644 index 0000000..763e26e --- /dev/null +++ b/.github/workflows/compatibility.yml @@ -0,0 +1,98 @@ +name: OpenCode compatibility + +on: + schedule: + - cron: "17 2 * * *" + workflow_dispatch: + push: + branches: [main] + paths: + - ".github/workflows/compatibility.yml" + - "package.json" + - "bun.lock" + - "src/**" + - "scripts/resolve-opencode-compatibility.ts" + - "scripts/test-opencode-server.ts" + pull_request: + paths: + - ".github/workflows/compatibility.yml" + - "package.json" + - "bun.lock" + - "src/**" + - "scripts/resolve-opencode-compatibility.ts" + - "scripts/test-opencode-server.ts" + +permissions: + contents: read + +concurrency: + group: opencode-compatibility-${{ github.ref }} + cancel-in-progress: true + +jobs: + discover: + name: Resolve supported OpenCode targets + runs-on: ubuntu-22.04 + outputs: + targets: ${{ steps.targets.outputs.targets }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 + with: + bun-version: 1.3.14 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 22 + package-manager-cache: false + - name: Resolve current and five prior minor lines + id: targets + shell: bash + run: | + set -euo pipefail + targets="$(bun scripts/resolve-opencode-compatibility.ts)" + echo "targets=$targets" >> "$GITHUB_OUTPUT" + echo "Testing OpenCode targets: \`$targets\`" >> "$GITHUB_STEP_SUMMARY" + + compatibility: + name: OpenCode ${{ matrix.target.version }} (${{ matrix.target.line }}) + needs: discover + runs-on: ubuntu-22.04 + timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + target: ${{ fromJSON(needs.discover.outputs.targets) }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 + with: + bun-version: 1.3.14 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 22 + package-manager-cache: false + - run: bun install --frozen-lockfile + - name: Install exact OpenCode target + run: | + npm install \ + --prefix "$RUNNER_TEMP/opencode-runtime" \ + --package-lock=false \ + "opencode-ai@${{ matrix.target.version }}" + - name: Exercise real dispatch integration + run: bun run test:opencode + env: + OPENCODE_BIN: ${{ runner.temp }}/opencode-runtime/node_modules/.bin/opencode + OPENCODE_TEST_VERSION: ${{ matrix.target.version }} + - name: Record compatibility result + if: always() + run: | + plugin_version="$(node -p "require('./package.json').version")" + { + echo "| OpenCode | opencode-model-dispatch | Target | Result |" + echo "| --- | --- | --- | --- |" + echo "| \`${{ matrix.target.version }}\` | \`$plugin_version\` | ${{ matrix.target.line }}, ${{ matrix.target.role }} | **${{ job.status }}** |" + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 1a16281..37badf2 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -28,6 +28,7 @@ jobs: - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 22 + package-manager-cache: false - name: Verify exact release SHA passed push CI run: bun run check:release-ci env: @@ -131,6 +132,7 @@ jobs: - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 22 + package-manager-cache: false - name: Install pinned Rust toolchain run: rustup toolchain install 1.97.1 --profile minimal --no-self-update - run: bun install --frozen-lockfile @@ -224,6 +226,7 @@ jobs: - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 22 + package-manager-cache: false - run: bun install --frozen-lockfile - name: Download exact first-party picker artifact uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 @@ -276,6 +279,7 @@ jobs: name: Sign and notarize macOS picker needs: picker-unsigned runs-on: macos-15 + environment: release-signing-macos permissions: actions: read steps: @@ -324,6 +328,8 @@ jobs: api_key="$RUNNER_TEMP/notary-api-key.p8" archive="$RUNNER_TEMP/picker-macos-arm64.zip" notary_result="$RUNNER_TEMP/notary-result.json" + notary_log="$RUNNER_TEMP/notary-log.json" + notary_validator="$RUNNER_TEMP/validate-notary-log.swift" umask 077 printf '%s' "$APPLE_CERTIFICATE" | /usr/bin/base64 -D > "$certificate" printf '%s' "$APPLE_API_PRIVATE_KEY" | /usr/bin/base64 -D > "$api_key" @@ -355,25 +361,89 @@ jobs: --wait \ --output-format json > "$notary_result" /bin/cat "$notary_result" + notary_id="$(/usr/bin/plutil -extract id raw -o - "$notary_result")" + if ! [[ "$notary_id" =~ ^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$ ]]; then + echo "Apple notarization returned an invalid submission ID" >&2 + exit 1 + fi notary_status="$(/usr/bin/plutil -extract status raw -o - "$notary_result")" + /usr/bin/xcrun notarytool log "$notary_id" \ + --key "$api_key" \ + --key-id "$APPLE_API_KEY_ID" \ + --issuer "$APPLE_API_ISSUER_ID" \ + "$notary_log" + /bin/cat "$notary_log" if [ "$notary_status" != "Accepted" ]; then echo "Apple notarization finished with status: $notary_status" >&2 exit 1 fi + /bin/cat > "$notary_validator" <<'SWIFT' + import Foundation + + func fail(_ message: String) -> Never { + FileHandle.standardError.write(Data((message + "\n").utf8)) + exit(1) + } + + guard CommandLine.arguments.count == 2 else { + fail("Apple notarization log validator requires one path") + } + + do { + let data = try Data( + contentsOf: URL(fileURLWithPath: CommandLine.arguments[1]) + ) + guard let root = try JSONSerialization.jsonObject(with: data) + as? [String: Any], + root.keys.contains("issues") + else { + fail("Apple notarization log must be a JSON object with issues") + } + let issues = root["issues"]! + if issues is NSNull { + exit(0) + } + guard let issueList = issues as? [Any], issueList.isEmpty else { + fail("Apple notarization log contains issues or warnings") + } + } catch { + fail("Apple notarization log is invalid JSON: \(error)") + } + SWIFT + /usr/bin/xcrun swift "$notary_validator" "$notary_log" /usr/bin/codesign --verify --strict --verbose=2 "$picker" + assessment_passed=false + for attempt in {1..12}; do + if /usr/sbin/spctl --assess --type exec --verbose=4 "$picker"; then + assessment_passed=true + break + fi + if [ "$attempt" -lt 12 ]; then + /bin/sleep 10 + fi + done + if [ "$assessment_passed" != "true" ]; then + echo "Gatekeeper assessment did not accept the notarized picker" >&2 + exit 1 + fi - name: Remove Apple signing credentials if: always() shell: /bin/bash --noprofile --norc -e -o pipefail {0} run: | + cleanup_status=0 keychain="$RUNNER_TEMP/model-dispatch.keychain-db" if [ -e "$keychain" ]; then - /usr/bin/security delete-keychain "$keychain" + /usr/bin/security delete-keychain "$keychain" || cleanup_status=$? fi /bin/rm -f \ "$RUNNER_TEMP/developer-id.p12" \ "$RUNNER_TEMP/notary-api-key.p8" \ "$RUNNER_TEMP/picker-macos-arm64.zip" \ - "$RUNNER_TEMP/notary-result.json" + "$RUNNER_TEMP/notary-result.json" \ + "$RUNNER_TEMP/notary-log.json" \ + "$RUNNER_TEMP/validate-notary-log.swift" \ + || cleanup_status=$? + exit "$cleanup_status" - name: Retain canonical signed macOS picker if: success() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -387,6 +457,7 @@ jobs: name: Sign Windows picker ${{ matrix.arch }} needs: picker-unsigned runs-on: windows-2025 + environment: release-signing-windows permissions: actions: read strategy: @@ -794,6 +865,7 @@ jobs: - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 22.14.0 + package-manager-cache: false - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: release-picker-assets @@ -917,6 +989,7 @@ jobs: - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 22.14.0 + package-manager-cache: false - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: release-npm-package @@ -1056,6 +1129,7 @@ jobs: - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 22.14.0 + package-manager-cache: false - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: release-npm-package @@ -1155,6 +1229,7 @@ jobs: - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 22.14.0 + package-manager-cache: false - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: release-npm-package @@ -1259,6 +1334,7 @@ jobs: - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 22.14.0 + package-manager-cache: false - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: release-npm-package diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..5f72f25 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,133 @@ +# Repository Guidelines + +This file is the short, operational guide for writing code in this repository. +Human contributors should start with `CONTRIBUTING.md`; architectural context +lives in `docs/architecture.md`. + +## Repository Map + +- `src/` contains the OpenCode plugin and its runtime boundaries. +- `picker/src/` contains the Svelte picker UI, pure state reducers, protocol + validation, and bundled OpenCode theme support. +- `picker/src-tauri/` contains the small native Tauri host. +- `test/` contains plugin, protocol, packaging, and integration tests. +- `scripts/` contains build, packaging, smoke-test, and release tooling. +- `docs/` contains living architecture, development, and release guidance. + +## General Style + +- Use Bun for installs, scripts, and tests. Keep Node.js 18 compatibility for + the published ESM package. +- Keep TypeScript strict. Prefer precise types and narrow `unknown` at + boundaries; do not introduce `any`. +- Prefer `const`, early returns, and a readable happy path. Avoid `else` after a + branch that already returns. +- Keep logic together until extracting a helper names a real concept, isolates + a complex boundary, or enables reuse. Do not create single-use abstractions + for simple expressions. +- Keep helpers near the code they support and below the primary exported + operation when practical. +- Use comments for constraints, security properties, and surprising behavior, + not for restating the code. +- Follow the surrounding file's naming and formatting. Repository source uses + no semicolons and double-quoted strings. +- Include `.js` in relative imports emitted by the published plugin. Tests may + use extensionless imports as supported by Bun. + +## Runtime Boundaries + +- Treat OpenCode, settings files, environment variables, picker messages, and + process output as untrusted input. Validate shape, size, identity, and + lifecycle before use. +- Bound input before parsing or allocating from it. Preserve the existing + fail-closed behavior for invalid settings, catalogs, RPC lines, and + non-loopback persistence endpoints. +- Keep explicit user cancellation separate from technical failure. Cancellation + starts no task; technical failure warns and lets OpenCode use its configured + fallback model. +- Do not send task prompts, task descriptions, file contents, model responses, + or prompt-derived text to the picker or plugin logs. +- Do not add a hosted backend, analytics, telemetry, updater, network listener, + or direct provider connection. +- Use the OpenCode SDK as the source of truth for providers, models, agents, + sessions, and messages. Direct OpenCode file reads are limited to the + documented local theme behavior. + +## Plugin Code + +- Keep OpenCode's built-in `task` tool responsible for permissions, child + sessions, execution, cancellation, output, and agent identity. +- Preserve `task` arguments. Apply a selected model only through the correlated + child `chat.message` hook and session model persistence. +- Preserve FIFO serialization for concurrent calls with the same parent and + agent unless a newer supported OpenCode API provides exact pre-message call + correlation. +- Keep batching isolated by parent session. One batch failure must settle every + waiter and must not strand later batches. +- Dependency injection in public constructors and plugin factories is + preferred over global mutation so behavior remains testable. + +## Picker Code + +- Keep selection, effort, setup, and request validation logic in pure TypeScript + modules where possible. Svelte components should primarily render state and + translate user actions. +- Keep the UI compact, keyboard-first, and aligned with OpenCode theme tokens. + Escape cancels; Enter submits only a valid selection. +- Preserve visible focus, hover, selected, disabled, and error states. Do not + rely on color alone. +- Keep the `ready` → `start` → `started` → `submit`/`cancel` NDJSON-RPC + lifecycle compatible on both sides whenever the protocol changes. +- Changes to bundled OpenCode theme sources require a source/license review and + regenerated third-party notices where applicable. + +## Tests + +- Add or update a focused regression test for every behavior change. +- Test public behavior and real boundaries rather than copying implementation + logic into the test. +- Prefer injected fakes over global mocks. Use real OpenCode, native picker, and + installed-package tests when changing the integration contract. +- Keep fixtures free of real prompts, secrets, home paths, and client data. +- Run the smallest relevant check while iterating, then the required area + checks before handing off a change. + +Common checks: + +```sh +bun test +bun run check:coverage +bun run typecheck +bun run build +bun run --cwd picker build +``` + +Picker or protocol changes also require: + +```sh +bun run test:picker:rendered +bun run test:picker-ready +bun run test:gui:auto +``` + +Packaging or OpenCode integration changes also require: + +```sh +bun run check:packaging +bun run test:package +bun run test:opencode +``` + +## Documentation + +- Update `README.md` for user-visible behavior or configuration. +- Update `docs/architecture.md` when a durable boundary or design choice + changes. Keep it current; do not add an ADR. +- Update `docs/development.md` when the development workflow or required checks + change. +- Update `docs/compatibility.md` when the OpenCode engine range, pinned SDK + contract, or nightly compatibility process changes. +- Update `docs/releasing.md` and the manual gate only when release operations + change. +- Use GitHub issues and pull requests for work tracking. Do not add issue + backlogs or implementation-status documents to the repository. diff --git a/CHANGELOG.md b/CHANGELOG.md index b6fc30c..57c8642 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,9 @@ All notable changes to this project will be documented here. the variants advertised by each model. - Add signed/notarized release gates, dependency auditing, third-party notices, and public-repository security metadata. +- Add a nightly OpenCode compatibility matrix covering the current minor and + five prior lines, with an exact append-only version history and archived + last-tested guarantees. - Match the active local OpenCode theme, acknowledge rendered native startup, and keep failed picker decisions retryable. - Keep the child-session persistence transport on loopback even across diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a177f7e..59233ce 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,42 +1,81 @@ # Contributing -Thanks for your interest in `opencode-model-dispatch`. +Thanks for helping improve `opencode-model-dispatch`. -## Project State +Before changing code, read the current design in `docs/architecture.md` and the +repository rules in `AGENTS.md`. The full local workflow and test matrix are in +`docs/development.md`. -This project is preparing its initial `0.1.x` release line. Architecture -decisions and the product constraints remain part of the public contract. -Start with: +## Set Up -- `docs/adr/` -- `PRODUCT.md` -- `docs/manual-integration-gate.md` +The repository uses Bun `1.3.14`. -## Development Rules +```sh +bun install --frozen-lockfile +bun install --cwd picker --frozen-lockfile +bun test +bun run typecheck +``` + +Rust and the platform Tauri prerequisites are needed only for native picker +work. Run `bun run doctor:picker` to check them. + +## What Makes a Good Change + +- Keep the change small enough to explain and review as one unit. +- Preserve the privacy, fallback, and OpenCode ownership boundaries in + `docs/architecture.md`. +- Add a focused regression test for behavior changes. +- Update user documentation and the living architecture in the same pull + request when their contracts change. +- Use GitHub issues for work tracking. Do not add issue briefs, implementation + plans, or ADRs to the repository. + +For a large feature or a change to OpenCode hook timing, picker IPC, native +packaging, privacy, or fallback behavior, discuss the design before investing +in the implementation. -- Complete hard spikes before feature implementation. -- Use test-first development for production behavior. -- Keep picker payloads and logs free of prompts, descriptions, user text, and - sensitive data. -- Do not add private OpenCode config reads except the documented theme exception. +## Verify -## Commands +Every code change should pass: ```sh -bun install bun test +bun run check:coverage bun run typecheck bun run build +``` + +Also run the relevant area checks: + +```sh +# Picker UI or native protocol +bun run --cwd picker build +bun run test:picker:rendered +bun run build:picker +bun run test:picker-ready +bun run test:gui:auto + +# Package or OpenCode integration +bun run check:packaging +bun run check:notices bun run test:package bun run test:opencode ``` +The manual visible GUI check, installed native package checks, and release gate +are required when the corresponding production path changes. See +`docs/development.md` for what each command proves. + ## Pull Requests -Before opening a PR: +In the pull request: + +- explain the problem and why this change solves it; +- list the commands or manual checks you ran; +- call out user-visible behavior, compatibility, privacy, or packaging impact; +- include before/after screenshots for picker UI changes; +- keep generated files and dependency notices synchronized. -- Ensure docs are updated for behavior changes. -- Run the relevant tests. -- Note any hard-gate result or manual OpenCode integration result. -- Run the native ready/GUI gates when changing the picker or process protocol. -- Keep PRs focused on one change or decision. +Short, concrete descriptions are easier to review than a transcript of the +implementation process. diff --git a/DESIGN.md b/DESIGN.md deleted file mode 100644 index 44c9251..0000000 --- a/DESIGN.md +++ /dev/null @@ -1,41 +0,0 @@ -# opencode-model-dispatch Design Context - -## Visual Direction - -The picker runs as the plugin's own UI, but should visually match OpenCode as closely as possible. It should resemble an OpenCode modal or command palette: compact, token-driven, keyboard-first, and focused on rows of actionable choices. - -The visual test is simple: even though it is a separate plugin UI, it should look like a close OpenCode companion surface rather than a generic browser-based app. - -## Theme - -Use bundled OpenCode theme tokens and resolved OpenCode CSS variables as the source of truth. Default to the active runtime theme hint when available, then URL overrides for previews, then fixture defaults. - -Prefer the host theme over custom color invention. Any custom color should be a subtle state treatment derived from OpenCode tokens, not a separate brand palette. - -## Typography - -- Base text: compact product UI scale around 12px to 13px. -- Row titles: slightly stronger, around 13px to 14px. -- Metadata and helper text: 11px to 12px, muted. -- Avoid large marketing-style page headings in the picker. -- Use short labels and OpenCode-like terminology. Avoid explanatory prose when a command label or concise helper line is enough. - -## Layout - -- Picker window should be compact enough to feel modal-like, approximately 680px wide by 500px tall in preview. -- Use tight row spacing, hairline borders, and restrained surfaces. -- Prefer a command-palette structure: small title area, dense list, concise footer actions. -- Settings can remain structurally similar but should use smaller fonts and sizing so it fits a smaller window comfortably. -- Avoid dashboard composition, marketing sections, card grids, and roomy form layouts. - -## Interaction - -- Escape cancels. -- Enter submits when the selection is valid. -- Focus, hover, disabled, and selected states must be visible and OpenCode-like. -- Native browser controls are acceptable only if styled to fit the surrounding OpenCode surface. -- Keyboard flow should be as important as pointer flow. Tab order, selected-row state, and submit/cancel actions should be predictable without reading instructions. - -## Motion - -Keep motion minimal and functional. Avoid decorative animation. diff --git a/PRODUCT.md b/PRODUCT.md deleted file mode 100644 index f3b3611..0000000 --- a/PRODUCT.md +++ /dev/null @@ -1,45 +0,0 @@ -# opencode-model-dispatch Product Context - -## Register - -product - -## Purpose - -`opencode-model-dispatch` is an OpenCode plugin for model routing with its own UI surface. It intercepts OpenCode subagent task dispatches, batches near-simultaneous calls, and lets the user choose one model for all tasks or a different model per task before execution starts. - -Success means the separate plugin UI looks and behaves very close to OpenCode itself: fast, keyboard-first, compact, and invisible once the routing decision is made. - -## Users - -Developers using OpenCode with multiple configured models and subagents. They are already in a focused coding session and need to make a fast routing decision without losing context or switching mental modes. - -They expect the plugin's own UI to mirror OpenCode's interaction model, theme, density, and terminology rather than behaving like a separate browser app. - -## Brand Personality - -OpenCode-aligned, precise, quiet. The product should feel like a sharp developer tool: direct when it needs input, restrained everywhere else. - -## Principles - -- Treat OpenCode as the visual and interaction reference. The plugin runs its own UI, but should closely mirror OpenCode's language, density, theme tokens, and command-oriented behavior. -- Optimize for fast model selection, keyboard confidence, and clear cancellation. -- Keep effort optional: default to `Auto`, and show only the effort choices - supported by the selected model. -- Avoid exposing task prompts or prompt-derived content in UI or logs. -- Keep the dispatch control plane local-only: no hosted service, analytics, - telemetry endpoint, updater, or external UI assets. -- Treat the picker as a transient command surface, not a dashboard. -- Keep settings secondary and compact; the model picker is the primary release-critical surface. - -## Anti-References - -- SaaS cards, hero copy, marketing language, oversized headings. -- Bright decorative gradients or glass effects. -- Spacious web-form layouts that feel disconnected from OpenCode. -- UI that requires reading long explanatory copy before choosing models. -- Any styling that makes the plugin's own UI look like a generic webview instead of a close OpenCode companion surface. - -## Accessibility & Inclusion - -Support keyboard-only operation as a first-class path. Keep focus states visible, preserve clear cancel/submit affordances, and avoid relying on color alone to communicate selection or disabled states. diff --git a/README.md b/README.md index cef7e69..59a243e 100644 --- a/README.md +++ b/README.md @@ -8,15 +8,15 @@ by, or endorsed by OpenCode or Anomaly. ## Status -Version `0.1.0` is the initial supported release line. The implementation, npm -packaging, real OpenCode dispatch path, and native picker protocol are -automated release gates. Every publication remains fail-closed until the TUI -and Desktop checklist in `docs/manual-integration-gate.md` is completed with -release evidence for that source revision. +Version `0.1.0` is the initial supported release line. Package installation, +the real OpenCode dispatch path, native picker IPC, and release artifacts are +automated gates. TUI and Desktop behavior is verified manually for the exact +release candidate. ## Install -The package targets OpenCode `>=1.18.7 <2`. Add it to OpenCode configuration: +The package supports OpenCode `>=1.18.7 <2`. Add it to your OpenCode +configuration: ```jsonc { @@ -25,126 +25,144 @@ The package targets OpenCode `>=1.18.7 <2`. Add it to OpenCode configuration: } ``` +See the +[OpenCode compatibility matrix](docs/compatibility.md#compatibility-matrix) +for the version guarantee and the +[rolling nightly verification](docs/compatibility.md#rolling-verification-window) +across the current supported OpenCode minor and its five predecessors, +including across a reviewed major-version boundary. Newer plugin versions can +be tried on older OpenCode versions, but only the latest +`opencode-model-dispatch` release recorded for that exact version is +guaranteed. + OpenCode installs configured npm plugins with Bun. No Rust or Tauri toolchain is required, -and no separate `npm install` is needed. The npm tarball contains the plugin -JavaScript and its supported native picker binaries; install lifecycle scripts -are not used. +and no separate `npm install` is needed. The package contains the plugin and +its supported native picker binaries and uses no install lifecycle scripts. -Bundled picker targets: +| Operating system | CPU | Status | +| --- | --- | --- | +| Linux glibc, Ubuntu 22.04 baseline | x64 | Supported | +| Linux glibc, Ubuntu 22.04 baseline | ARM64 | Supported | +| macOS | Apple Silicon | Supported | +| Windows 10/11 | x64 | Supported | +| Windows 11 | ARM64 | Supported | -| Operating system | CPU | Asset | Status | -| --- | --- | --- | --- | -| Linux (glibc; Ubuntu 22.04 build baseline) | x64 | `picker-linux-x64` | Supported | -| Linux (glibc; Ubuntu 22.04 build baseline) | ARM64 | `picker-linux-arm64` | Supported | -| macOS | Apple Silicon (ARM64) | `picker-macos-arm64` | Supported | -| Windows 10/11 | x64 | `picker-windows-x64.exe` | Supported | -| Windows 11 | ARM64 | `picker-windows-arm64.exe` | Supported | +Linux requires the WebKitGTK 4.1 and GTK runtime libraries used by Tauri. +Alpine/musl is not supported. Windows uses WebView2 and macOS uses system +WebKit. -Other targets fail with a clear error. Set `OPENCODE_MODEL_DISPATCH_PICKER` to -an absolute compatible picker path to use a custom build. +Set `OPENCODE_MODEL_DISPATCH_PICKER` to an absolute compatible picker path to +use an operator-provided build. Other unsupported targets fail with a clear +error. The expected picker release asset name is -`picker-${platform}-${arch}${ext}`. The same binaries are attached to: +`picker-${platform}-${arch}${ext}`. The same binary is attached to: `https://github.com/Lauritz-Timm/opencode-model-dispatch/releases/download/v${version}/picker-${platform}-${arch}${ext}` -The Linux artifacts are dynamically linked and require a glibc-based -distribution with the WebKitGTK 4.1/GTK runtime libraries used by Tauri; they -are not Alpine/musl binaries. They are built and tested on Ubuntu 22.04. -Windows uses WebView2, and macOS uses the system WebKit runtime. Release binaries are -Developer ID signed and notarized on macOS and Authenticode signed on Windows -before they are uploaded. - The package is ESM-only and supports Node.js 18 or newer. The installed `opencode-model-dispatch-picker` executable is a low-level NDJSON-RPC bridge -for OpenCode/package diagnostics, not a standalone interactive CLI; normal use -is through the plugin entry in OpenCode configuration. +for package diagnostics; normal use is through OpenCode. ## Configuration Dispatch is opt-in. First-run setup and the `configure_model_dispatch` tool -write: - -- global settings to `~/.config/opencode/model-dispatch.json`; -- optional project dispatch settings to `.opencode/model-dispatch.json`; -- optional project config exclusion to `.gitignore`. - -The settings UI controls dispatch enablement, the batching window, picker -timeout, global privacy-safe logging, global/project scope, and reset. -Settings files larger than 64 KiB fail closed, and repository-controlled -project settings symlinks are not followed. Setup also refuses to load or -rewrite a project `.gitignore` larger than 1 MiB. -By default the picker reads the active theme ID through OpenCode's local -client and follows the local system light/dark preference. Global -`appearance.theme_id` and `appearance.color_scheme` settings, or the +manage: + +- global settings in `~/.config/opencode/model-dispatch.json`; +- optional project dispatch settings in `.opencode/model-dispatch.json`; +- optional exclusion of project settings through `.gitignore`. + +The settings UI controls dispatch enablement, batching, picker startup timeout, +privacy-safe logging, global/project scope, appearance overrides, and reset. + +Settings files larger than 64 KiB fail closed. Project settings symlinks are +not followed, and setup refuses to load or rewrite a project `.gitignore` +larger than 1 MiB. + +By default, the picker follows the active local OpenCode theme and the system +light/dark preference. Global `appearance.theme_id` and +`appearance.color_scheme` settings can override them. The environment variables `OPENCODE_MODEL_DISPATCH_THEME_ID` and -`OPENCODE_MODEL_DISPATCH_COLOR_SCHEME` environment variables, can explicitly -override that behavior. +`OPENCODE_MODEL_DISPATCH_COLOR_SCHEME` take final precedence. + +Read more about the +[configuration trust boundary](docs/architecture.md#separate-global-trust-from-project-configuration) +or follow the checklist for +[adding or changing a setting](docs/development.md#add-or-change-a-setting). ## Setup +To get started: + 1. Add the package to OpenCode's plugin list. -2. Restart OpenCode and complete the first-run picker. +2. Restart OpenCode and complete first-run setup. 3. Enable model dispatch. 4. Start one or more built-in `task` calls. -5. Choose a model per row, or use **Apply to all**. Leave **Effort** on - **Auto** for the simplest provider-default behavior, or choose one of the - exact effort variants advertised by that model. +5. Choose a model for each row or use **Apply to all**. + +Leave **Effort** on **Auto** to use the provider default, or choose one of the +exact effort variants advertised by the selected model. -Cancel during first-run disables dispatch and snoozes setup for 24 hours. -Cancel from the configuration tool leaves existing settings unchanged. +Cancelling first-run setup leaves dispatch disabled and snoozes setup for 24 +hours. Cancelling the configuration tool leaves existing settings unchanged. ## Behavior - Calls are batched per parent session during the configured debounce window. -- Models come from OpenCode's configured provider catalog; disabled, - deprecated, and OpenCode `-nano` entries hidden by OpenCode are excluded. -- Agent defaults take precedence over the current parent model for preselection. -- Effort defaults to **Auto**. Auto preserves a compatible existing effort - when the model stays the same; changing models lets the new provider choose - its default. Explicit effort choices are limited to variants advertised by - the selected model and are persisted with the child model. +- Models come from OpenCode's configured provider catalog. Disabled, + deprecated, and hidden `-nano` entries are excluded. +- An agent model default takes precedence over the current parent model for + preselection. +- **Auto** preserves a compatible effort when the model stays the same. + Changing models lets the new provider choose its default. - The original agent identity and task arguments stay unchanged. -- The selected model is applied to the child session's `chat.message` hook, - persisted as the child session's current model for subsequent turns, and - reported in task metadata. -- Parallel calls to the same parent agent are serialized until their child - message is correlated, preventing model selections from swapping. -- Technical picker failures show a warning and use OpenCode's configured - fallback model. Explicit picker cancellation starts no calls in that batch. +- The selected model is applied to the child message, persisted for later child + turns, and reported in task metadata. +- Parallel calls to the same parent agent are serialized until each child + message is correlated, preventing selections from swapping. +- A technical picker failure warns and uses OpenCode's configured fallback + model. +- Explicit cancellation starts no calls in the affected batch. + +See the complete [runtime flow](docs/architecture.md#runtime-flow) and the +[design choices behind it](docs/architecture.md#design-choices). ## Privacy -Task prompts, task descriptions, file contents, and model responses are not +Task prompts, task descriptions, file contents, and model responses are never sent to the picker or written to plugin logs. Picker rows contain only call -identity, agent name, model catalog data, and model preselection. Operational -logging can include event names, call IDs, model IDs, counts, platform, and -failure categories, and can be disabled globally. +identity, agent name, model catalog data, and preselection. + +Operational logs may include event names, call IDs, model IDs, counts, +platform, and failure categories. Logging can be disabled globally. -The dispatch layer is local-only: it has no hosted backend, analytics, +The dispatch layer is local-only. It has no hosted backend, analytics, telemetry endpoint, updater, or direct model-provider connection. Picker -traffic uses local process stdio and Tauri IPC, settings stay in local files, -and the plugin talks only through the OpenCode client supplied by the host. Its -additional child-session persistence client is created only for a loopback -OpenCode server URL, rejects redirects, and validates the endpoint again for -every request. -OpenCode itself still sends a task to the configured model provider; choose a -local OpenCode provider when the model inference must also remain on-device. +traffic uses process stdio and Tauri IPC; settings remain local files; and the +plugin uses the OpenCode client supplied by the host. + +The extra client used to persist the child model permits loopback OpenCode +server URLs only, rejects redirects, and revalidates every endpoint. OpenCode +still sends tasks to the configured model provider; choose a local OpenCode provider +when inference must remain on-device. + +Read the full [privacy boundary](docs/architecture.md#keep-the-control-plane-private) +and [security policy](SECURITY.md#privacy-expectations). ## Troubleshooting - Run `configure_model_dispatch` if dispatch is inactive. -- Verify that your OS/CPU appears in the support table. -- On Linux, install the distribution packages providing WebKitGTK 4.1 and GTK - if the native picker does not start. -- Set `OPENCODE_MODEL_DISPATCH_PICKER` to the absolute path of an - operator-trusted local native binary. -- Run `bun run doctor:picker` in a development checkout to diagnose the Rust, - Tauri, Node/Bun, and WebKit build prerequisites. -- A technical picker failure deliberately falls back instead of blocking the - task forever. -- Picker startup uses the configured timeout. Once its UI has started, an - abandoned picker is terminated after 10 minutes. +- Confirm that your OS and CPU are supported. +- On Linux, install your distribution's WebKitGTK 4.1 and GTK runtime packages + if the picker does not start. +- Run `bun run doctor:picker` from a development checkout to diagnose native + build prerequisites. +- Set `OPENCODE_MODEL_DISPATCH_PICKER` to an absolute, trusted native binary + when testing a custom picker build. +- A technical failure deliberately falls back instead of blocking a task. +- Picker startup uses the configured timeout. Once started, an abandoned picker + is terminated after 10 minutes. ## Development @@ -155,207 +173,31 @@ bun test bun run check:coverage bun run typecheck bun run build -bun run --cwd picker build -bun run check:packaging -bun run check:notices -bun run test:package -bun run test:opencode -bun run check:release-source ``` -`bun run test:package` packs the currently staged npm payload, installs it without -lifecycle scripts, verifies both Bun and Node ESM imports, and exercises the -installed plugin contract. The publish job performs this check again after all -five native picker assets are staged, then retains and tests that exact -tarball for publication. - -The tagged publish workflow runs `bun run check:release-ci` with its automatic -`github.token`, scoped to `actions: read` and `contents: read`, before release -validation. That fail-closed check queries only `ci.yml` push runs for the exact -tagged SHA and requires the newest matching run to have completed successfully. -It never receives a repository-administration or settings token. - -`THIRD_PARTY_NOTICES.md` is shipped in the npm tarball. It contains reviewed -notices for the bundled OpenCode SDK/theme sources, Svelte, and Tauri -JavaScript API, plus the complete locked Rust crate graph for every release -target. After dependency changes, run -`cargo install --locked cargo-about --version 0.9.1 --features cli`, then -`bun run notices:generate`, and commit both generated notice files. -`bun run check:notices` regenerates them offline and fails on drift. - -`bun run test:opencode` starts a real OpenCode 1.18.7 server, an isolated local -OpenAI-compatible test provider, and a deterministic picker. It drives a -built-in `task` call through the plugin, verifies the chosen model and effort -reach the child request, and checks the resulting session metadata and durable -model-switch event. It then emits two concurrent built-in `task` calls for the -same agent, assigns them different models in picker FIFO order, and follows -both real child sessions and provider requests to prove the selections did not -swap. No external model provider is contacted. Set `OPENCODE_BIN` when OpenCode -is not on `PATH`. - -On Linux, `bun run test:package:native:opencode` closes the distribution loop: -it packs and installs the package in an isolated consumer, serves the exact -tarball and its locked dependency graph from an ephemeral loopback-only npm -registry, and makes real OpenCode resolve the documented -`plugin: ["opencode-model-dispatch"]` configuration. It then launches the -installed bundled Tauri picker without a path override, chooses a different -model and explicit effort in the real window, and verifies the resulting child -request and session metadata. CI runs it under X11/Xvfb; the test provider, -registry, and all dispatch control traffic remain local. - -For native work: +See the [development guide](docs/development.md) for the repository map, +coding workflow, [common change checklists](docs/development.md#common-changes), +native picker setup, and full test matrix. The current system design and its +constraints are documented in [architecture](docs/architecture.md), and the +[compatibility policy](docs/compatibility.md) explains the moving OpenCode +version gate. -```sh -bun run doctor:picker -bun run dev:picker:tauri -bun run build:picker -bun run test:picker-ready -bun run test:gui:auto -bun run test:gui -``` +## Contributing -`test:picker-ready` is the non-interactive native handshake gate used on every -runnable release target. It waits for the webview to acknowledge that it hydrated -the real start request, rather than accepting process startup alone. On Linux, -`test:gui:auto` drives the real Tauri window's -explicit-effort and submit controls under X11/Xvfb and verifies the production -payload; the installed-package integration additionally changes the selected -model. Native Linux ARM64 runs the same GUI and OpenCode/TUI matrix as x64. -macOS and both Windows architectures run the exact installed-tarball native -handshake. `test:gui` remains the visible cross-platform check: it opens the real -picker with two fixture rows so a release operator can inspect model and effort -selection before clicking **Start tasks**. - -## Release - -The tag must be exactly `v`. The publish workflow: - -1. validates a clean tracked source tree, tag/main ancestry, changelog, - synchronized versions, tests, coverage, packaging, a real OpenCode child - dispatch, and recorded TUI/Desktop evidence; -2. builds, Rust-tests, and ready-smokes unsigned Linux x64/ARM64, macOS ARM64, - and Windows x64/ARM64 pickers, including real Linux GUI and OpenCode/TUI - submits, without exposing signing credentials to checked-out source or - package-manager code; -3. downloads the exact macOS and Windows unsigned artifacts into fresh jobs - that do not check out the repository, Developer ID signs and notarizes - macOS, Authenticode signs both Windows architectures, cleans up credentials, - then uploads only the canonical signed artifacts. It validates all native - binary formats, packs one exact npm tarball, and drives that same tarball through real - OpenCode and its installed Linux picker, plus Bun-installed native - handshake smokes on Linux ARM64, macOS, and both Windows architectures, - before a registry-only validation - job routes it to either the protected first-publish bootstrap or a minimal - token-free trusted-publishing job; -4. publishes the prevalidated draft GitHub release only after npm succeeds, - with `LICENSE`, `THIRD_PARTY_NOTICES.md`, and SHA-256 checksums alongside - the native binaries. npm readback must match the tested tarball's SHA-512 - integrity, and `npm audit signatures` must cryptographically verify a SLSA - provenance attestation bound to the release commit, tag, repository, and - `publish.yml`. - -If a run is retried after the draft release has been staged, the draft's -verified, commit-bound SLSA-attested picker assets remain canonical. The -workflow reuses them for the npm tarball and final release instead of replacing -timestamp-signed macOS or Windows binaries with different rebuilds. Edited or -substituted draft assets fail attestation verification. Do not delete or edit a -staged draft while recovering a release. - -The package name is new, so bootstrap its first publish with a short-lived npm -granular access token. Give **Packages and scopes** `Read and write` access to -**All packages** (the new package cannot be selected yet), enable **Bypass -two-factor authentication**, choose the shortest practical expiration, and, -after completing the repository protection setup below, create a protected -GitHub environment named `npm-bootstrap` and store the token there as -`NPM_BOOTSTRAP_TOKEN`. Require a maintainer approval for that environment. -The workflow exposes that secret only to the isolated bootstrap job, and that -job runs only when the package name itself returns npm `E404`; a missing -version of an established package always uses token-free trusted publishing. -The bootstrap job also receives GitHub OIDC solely to create npm provenance -for the first publish; it does not check out or execute repository code. - -Immediately after the first version exists, configure its npm trusted -publisher for GitHub repository `Lauritz-Timm/opencode-model-dispatch`, -workflow filename `publish.yml`, no environment, and allowed action -`npm publish`. Revoke the bootstrap token, delete `NPM_BOOTSTRAP_TOKEN`, and -configure the package to require two-factor authentication and disallow token -publishing. Subsequent publishes use only the workflow's OIDC identity; no npm -repository secret is needed. - -Before adding any publishing or signing secrets, enable GitHub immutable -releases, private vulnerability reporting, Dependabot security updates, and -strict required CI plus deletion/force-push protection on `main`. Main may use -an active branch ruleset or classic branch protection. Add an active tag -ruleset whose include pattern is exactly `refs/tags/v*`, with no matching -exclusions, and restrict creation, update, and deletion. Because the creation -rule permits only bypass actors to create matching tags, add only the -repository owner user as an always-allowed bypass actor; do not grant a broad -repository role bypass. Configure and review these protections before storing -any release secret, including `NPM_BOOTSTRAP_TOKEN` and the Apple and Windows -signing secrets. A tag-triggered workflow starts immediately, so repository -protection is a mandatory local pre-tag gate rather than a check that receives -an administration token after tagged source has started running. - -Complete `docs/manual-integration-gate.md` from the intended source commit and -record its full `git rev-parse HEAD`. Then commit only the completed gate -document on a dedicated branch based on that commit, open a pull request, and -merge it through protected `main`; do not push it directly to `main`. Confirm -the merged commit differs from the tested source only by the gate document and -wait until the CI workflow's push run for that exact merged SHA is completed -successfully. Only then create `v` at that merged commit and -push the tag. If `main` moved in the meantime, rerun the gate from the new -intended source. The publish workflow requires the tagged commit to equal the -current `origin/main`, rejects a tag containing any other change after the -tested source, and fails if exact-SHA CI is still queued or incomplete. - -Run `bun run release:preflight` without privileged tokens, then run -`bun run check:public-repo` as a separate fail-closed gate for the intended -release SHA before tagging. -The source preflight intentionally fails on any modified or untracked source -file, but it does not require cross-platform binaries that cannot be produced -from one clean developer checkout. The tagged workflow builds and signs those -artifacts on their native runners, then `check:release-package` validates their -formats and modes and an npm dry run proves that all five are present. Use -`bun run release:artifact-preflight` only when all five generated -`bin/picker-*` assets have deliberately been staged for inspection. -The public check fails closed unless the repository and exact release SHA meet -the settings above; missing description/topics and an unused enabled wiki are -reported as polish warnings. Live settings verification uses the -`GITHUB_REPOSITORY_SETTINGS_TOKEN` environment variable only in the standalone -local gate process. GitHub deliberately omits a ruleset's bypass actors unless -the caller has write access to that ruleset, so this audit cannot use an -Administration read-only token. Create a short-lived fine-grained personal -access token scoped only to this repository with repository Administration set -to **Read and write**, but do so only after the owner-only release-tag ruleset -has been configured and reviewed. Use a separate read-only `GITHUB_TOKEN` for -the exact-SHA Actions lookup. Pass both tokens only to the separate -`bun run check:public-repo` process, not to `release:preflight` or the parent -shell environment, and revoke the administration token before pushing the -release tag. Never add it as an Actions secret or reuse it as a publishing or -signing credential. The publish workflow intentionally contains no -repository-administration token. - -Required release secrets are: - -- `NPM_BOOTSTRAP_TOKEN` for the first publish only, configured with the bootstrap - permissions above; -- `APPLE_CERTIFICATE` (base64 P12), `APPLE_CERTIFICATE_PASSWORD`, - `APPLE_SIGNING_IDENTITY`, `APPLE_KEYCHAIN_PASSWORD`, - `APPLE_API_PRIVATE_KEY` (base64 P8), `APPLE_API_KEY_ID`, and - `APPLE_API_ISSUER_ID`; -- `WINDOWS_CERTIFICATE` (base64 PFX) and - `WINDOWS_CERTIFICATE_PASSWORD`. +See [CONTRIBUTING.md](CONTRIBUTING.md). -## Contributing +## Releasing -See `CONTRIBUTING.md`. +Maintainers should follow [the release guide](docs/releasing.md) and the exact +release-candidate evidence checklist in +[the manual integration gate](docs/manual-integration-gate.md). ## Security -See `SECURITY.md`. +See [SECURITY.md](SECURITY.md). ## License -MIT. See `THIRD_PARTY_NOTICES.md` for the bundled OpenCode SDK/theme sources, -Svelte and Tauri JavaScript runtime, and native picker's complete Rust -dependency notices. +MIT. See [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md) for the bundled +OpenCode SDK and theme sources, Svelte and Tauri JavaScript runtime, and the +native picker's complete Rust dependency notices. diff --git a/docs/adr/0001-plugin-first-task-interception.md b/docs/adr/0001-plugin-first-task-interception.md deleted file mode 100644 index fa8d486..0000000 --- a/docs/adr/0001-plugin-first-task-interception.md +++ /dev/null @@ -1,37 +0,0 @@ -# ADR 0001: Plugin-First Task Interception - -Status: accepted. The original subagent-type mutation mechanism is superseded -by ADR 0009. - -## Context - -The desired behavior is automatic model selection whenever an agent dispatches a -subagent. Agents should keep calling OpenCode's built-in `task` tool. A custom -`dispatch_agent` tool would require steering the model to use a different tool, -and the OpenCode `question` tool does not provide the desired custom picker UI. - -OpenCode plugins can hook `tool.execute.before` and mutate tool arguments before -execution. - -## Decision - -Implement model dispatch as a standalone plugin that intercepts built-in `task` -calls through `tool.execute.before`. - -The plugin leaves `task` unchanged when dispatch is disabled. When enabled, it -pauses intercepted task calls, collects model selections, and then lets the -built-in `task` implementation continue without changing the requested agent -or task arguments. ADR 0009 records how the selected model is correlated with -and applied to the child message after OpenCode creates the child session. - -## Consequences - -Agents keep using the normal subagent flow. - -The plugin does not need to reimplement task output, child session creation, -permissions, foreground/background behavior, or metadata if the interception path -works. - -The design depends on `tool.execute.before` being able to wait for user input -before built-in `task` runs, plus `chat.message` exposing the child session in -time to apply the correlated model override. diff --git a/docs/adr/0002-tauri-overlay-picker.md b/docs/adr/0002-tauri-overlay-picker.md deleted file mode 100644 index 61b93d1..0000000 --- a/docs/adr/0002-tauri-overlay-picker.md +++ /dev/null @@ -1,30 +0,0 @@ -# ADR 0002: Bundled Tauri Overlay Picker - -## Context - -The picker should feel visually aligned with OpenCode and appear above the -current OpenCode surface. It should not use OpenCode's `question` tool, and it -does not need to be rendered inside OpenCode's native TUI/Desktop UI. - -The plugin can start external processes. A browser popup would be easier but -would feel less integrated. Native OpenCode UI would require OpenCode core -changes. - -## Decision - -Bundle a Tauri mini-window picker with the plugin. - -The picker will be a focused always-on-top external overlay, packaged as -prebuilt binaries per platform. It will use OpenCode-like dark/light styling, -best-effort theme loading, compact window chrome, keyboard navigation, apply-to-all, -and per-task model selection. - -## Consequences - -The plugin can ship the desired custom UI without OpenCode core UI changes. - -Release packaging becomes more complex because platform binaries must be built -and distributed. - -The picker is not truly native OpenCode UI, so exact theme and window integration -are best-effort rather than guaranteed. diff --git a/docs/adr/0003-batched-task-selection.md b/docs/adr/0003-batched-task-selection.md deleted file mode 100644 index 618b097..0000000 --- a/docs/adr/0003-batched-task-selection.md +++ /dev/null @@ -1,27 +0,0 @@ -# ADR 0003: Debounced Batch Selection - -## Context - -Agents can issue multiple `task` calls in the same assistant turn or nearly at -the same time. Those calls should be shown in one picker so the user can choose -one model for all or assign models per task. If the agent waits for one subagent -to finish before starting the next, there is no reliable way for the plugin to -know future calls are coming. - -## Decision - -Batch task calls that arrive within a 500 ms debounce window for the same parent -session. - -The picker will show one apply-to-all control and one row per task. Parallel -calls continue with their original concurrency after the user confirms. -Sequential calls that happen after a prior task finishes become separate picker -batches. - -## Consequences - -Parallel subagent dispatches get a single model-selection interaction. - -Single task calls wait up to 500 ms before the picker opens. - -The design does not attempt to predict future sequential task calls. diff --git a/docs/adr/0004-shadow-agent-model-override.md b/docs/adr/0004-shadow-agent-model-override.md deleted file mode 100644 index 0a9a462..0000000 --- a/docs/adr/0004-shadow-agent-model-override.md +++ /dev/null @@ -1,34 +0,0 @@ -# ADR 0004: Shadow Agents for Model Override - -Status: superseded by [ADR 0009](./0009-child-message-model-override.md). - -## Context - -Built-in `task` does not expose a public `model` argument. It chooses the child -model from the resolved subagent's configured model and otherwise falls back to -the parent/current message model. - -OpenCode agent config can set an agent `model`, and agent config can override -the returned agent `name`. This suggests a plugin-only path: route the task to a -temporary agent definition that has the selected model while preserving the -original agent name. - -## Decision - -Use ephemeral per-call shadow agents as the model override mechanism. - -For each selected task, the plugin creates or exposes a shadow agent with an -internal lookup key, copies the latest original agent definition, sets the -selected model, preserves the original agent `name`, and rewrites -`subagent_type` to the shadow lookup key before built-in `task` runs. - -## Consequences - -The built-in `task` tool can remain authoritative for execution. - -The implementation must prove that a shadow agent can be registered before -`agent.get()` runs and that child session history/UI shows the original agent -name, not the shadow key. - -If either invariant fails, the plugin cannot safely use this path and must wait -for upstream OpenCode support for a public per-call task model override. diff --git a/docs/adr/0005-picker-stdio-json-rpc.md b/docs/adr/0005-picker-stdio-json-rpc.md deleted file mode 100644 index b093e7b..0000000 --- a/docs/adr/0005-picker-stdio-json-rpc.md +++ /dev/null @@ -1,37 +0,0 @@ -# ADR 0005: Persistent Stdio JSON-RPC for Picker IPC - -## Context - -The picker needs a startup handshake, one structured request, and an explicit -submit-or-cancel result. Keeping stdin open also lets the picker terminate when -its owning OpenCode/plugin process disappears. - -The plugin starts the picker process, so stdio is available without opening a -localhost port or adding cross-platform named pipe complexity. - -## Decision - -Use NDJSON JSON-RPC notifications over the picker process stdin/stdout. - -Messages are one JSON object per line. V1 uses `ready` from picker to plugin, -`start` from plugin to picker, `started` from picker to plugin after the -webview has accepted and hydrated that request, and one terminal `submit` or -`cancel` from picker to plugin. The startup timeout remains active until -`started`, so a blank or rejected picker request fails into the technical -fallback instead of leaving a stuck window. After `started`, a separate -10-minute decision timeout bounds an abandoned or unresponsive picker. Both -directions cap a single RPC line at 4 MiB, and every technical transport failure -terminates the child process. The complete model catalog is captured -immediately before launch; v1 has no live model-refresh or validation RPC. - -## Consequences - -The picker can exchange its complete request and decision without a local -server or exposing a network listener. - -The plugin must handle parse errors, oversized input, process crashes, startup -and decision timeouts, lost stdio, invalid submit payloads, and observed -cancellation distinctly. - -The IPC layer is more complex than one-shot process execution but remains -portable across supported platforms. diff --git a/docs/adr/0006-sdk-model-and-agent-metadata.md b/docs/adr/0006-sdk-model-and-agent-metadata.md deleted file mode 100644 index cb08207..0000000 --- a/docs/adr/0006-sdk-model-and-agent-metadata.md +++ /dev/null @@ -1,35 +0,0 @@ -# ADR 0006: SDK-Owned Model and Agent Metadata - -## Context - -OpenCode already has configured providers, visible models, current/default -models, and agent-specific model defaults. Duplicating that model list in plugin -settings would create drift and extra configuration burden. - -The plugin should use stable SDK/API for model and agent data. Theme matching is -the only accepted exception where direct OpenCode theme file reads are allowed. - -## Decision - -Use OpenCode SDK/API as the source of truth for model and agent metadata. - -The picker model catalog must come from the same enabled/visible model list that -OpenCode's model picker uses. Agent defaults come from `client.app.agents()`. -Parent/current model preselection comes from the latest assistant message in the -parent session. - -Reasoning effort is represented by OpenCode's provider-specific model -`variants`. The picker exposes only variants advertised for the selected model. -It labels the absence of an explicit variant as `Auto`, allowing OpenCode and -the provider to retain their normal default. - -## Consequences - -The picker reflects the user's existing OpenCode model setup without a separate -plugin shortlist. - -Implementation must stop if stable SDK/API cannot provide the enabled/visible -model list. - -Hidden configured/current preselected models can be shown only on their task row -and are not added to apply-to-all. diff --git a/docs/adr/0007-settings-setup-and-privacy.md b/docs/adr/0007-settings-setup-and-privacy.md deleted file mode 100644 index 28912f8..0000000 --- a/docs/adr/0007-settings-setup-and-privacy.md +++ /dev/null @@ -1,29 +0,0 @@ -# ADR 0007: Settings, Setup, and Privacy - -## Context - -The plugin should be opt-in, configurable globally and per project, and safe for -privacy-sensitive users. Logging is useful for maintenance, but users must be -able to disable all plugin logging. - -First-run setup should happen through the same Tauri shell so both TUI and -Desktop users get a consistent configuration experience. - -## Decision - -Use global and project settings files with deep merge for `dispatch` settings. - -Privacy/logging settings are global only. `privacy.logging_enabled: false` -disables all plugin logging. First-run setup opens at plugin load when no config -exists, unless snoozed. If setup is cancelled, dispatch remains disabled and -setup is snoozed globally for 24 hours. - -## Consequences - -The plugin is inactive until explicitly enabled. - -Project dispatch behavior can be configured without allowing project config to -override user privacy. - -Setup can create project config and optionally add `.opencode/model-dispatch.json` -to `.gitignore`, but it must clearly inform the user what it changed. diff --git a/docs/adr/0008-shadow-agent-lifecycle.md b/docs/adr/0008-shadow-agent-lifecycle.md deleted file mode 100644 index d65643b..0000000 --- a/docs/adr/0008-shadow-agent-lifecycle.md +++ /dev/null @@ -1,32 +0,0 @@ -# ADR 0008: Shadow Agent Lifecycle and Cleanup - -Status: superseded by [ADR 0009](./0009-child-message-model-override.md). No -shadow agents are created by the released design. - -## Context - -Shadow agents may be referenced by child sessions after a task completes. If the -plugin deletes a shadow agent immediately, session history may later reference a -missing agent definition. Keeping shadow agents forever would leak internal -state. - -OpenCode session updates include archive state through `time.archived`. - -## Decision - -Keep mapped shadow agents until their child session is archived. - -The plugin stores shadow state in a global plugin cache. It tracks -`callID -> shadowKey` before task execution and `shadowKey -> childSessionID` -after task metadata exposes the child session. Dispose removes only orphaned -shadows with no child session id. Session archive and startup GC remove mapped -shadows whose child sessions are archived or missing. - -## Consequences - -Active session history can continue resolving shadow-backed agents. - -The plugin needs reliable mapping from task call to child session id. - -Startup and dispose cleanup are required to handle crashes, failed tasks, and -orphaned shadow entries. diff --git a/docs/adr/0009-child-message-model-override.md b/docs/adr/0009-child-message-model-override.md deleted file mode 100644 index 9e13b50..0000000 --- a/docs/adr/0009-child-message-model-override.md +++ /dev/null @@ -1,49 +0,0 @@ -# ADR 0009: Child Message Model Override - -## Context - -The original shadow-agent design attempted to add an agent from -`tool.execute.before`. OpenCode invokes a plugin's `config` hook once during -startup and then builds its agent registry from that configuration. A shadow -created later cannot be resolved by the built-in `task` tool. - -OpenCode creates the child session before it saves the child user message. The -documented `chat.message` hook receives that message, including its model, before -OpenCode persists or executes it. - -## Decision - -Keep `subagent_type` unchanged and queue each selected model by parent session -and original agent. When `chat.message` receives a child session, resolve its -parent through the SDK and apply the queued model to the child user message. -OpenCode 1.18.7 does not expose the originating task `callID` on that hook or -on the child session, so concurrent intercepted calls to the same agent are -released in FIFO order: the next built-in task does not start until the current -child message has consumed its selection. -Persist that selection through OpenCode's session model endpoint so later turns -use the same model, and update the built-in task result metadata after the -message has consumed the correlated selection. - -An explicit provider-advertised effort variant is applied and persisted with -the model. `Auto` omits the variant: it preserves the existing variant when the -model is unchanged and otherwise lets the newly selected model use its normal -default. - -## Consequences - -- The built-in `task` implementation remains responsible for permissions, - session creation, execution, cancellation, and output. -- Child history retains the original agent identity without temporary config. -- Subsequent child turns keep the selected model instead of reverting to the - parent's original model. -- Explicit effort follows the selected model, while `Auto` avoids imposing a - plugin-specific default. -- No persistent shadow-agent cache or cleanup is required. -- The integration is pinned and tested against the supported OpenCode plugin - and SDK contract. A change to child-message hook timing is a compatibility - break and must fail the live release gate. -- The FIFO gate prevents intercepted same-agent task calls from swapping - selections. OpenCode does not currently provide enough pre-prompt identity to - distinguish an unrelated same-parent/same-agent child created outside that - queue; exact correlation requires an upstream child `parentCallID` (or an - equivalent pre-prompt hook field). diff --git a/docs/adr/README.md b/docs/adr/README.md deleted file mode 100644 index cba57a2..0000000 --- a/docs/adr/README.md +++ /dev/null @@ -1,14 +0,0 @@ -# Architecture Decision Records - -This directory contains short ADRs for accepted architectural decisions. - -Each ADR uses only: - -- Context -- Decision -- Consequences - -ADRs 0004 and 0008 are retained for history and explicitly superseded by ADR -0009, which also supersedes ADR 0001's original subagent-type mutation -mechanism. ADRs 0001 and 0005 otherwise record the current interception and -picker IPC boundaries. diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..1631d83 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,211 @@ +# Architecture + +`opencode-model-dispatch` is an OpenCode plugin that pauses built-in subagent +dispatches long enough for the user to choose a model. It preserves OpenCode's +normal task lifecycle and adds one local decision surface: a bundled native +picker. + +This is a living description of the current design. Update it when the design +changes; historical proposals belong in Git history, not in separate decision +records. + +## Goals + +- Let users choose a model and optional provider-advertised effort for each + built-in `task` call. +- Batch calls started close together into one quick, keyboard-first decision. +- Preserve the requested agent, task arguments, OpenCode permissions, child + session behavior, and task output. +- Keep prompts and workspace content outside the picker and operational logs. +- Ship a native picker without requiring users to install Rust or Tauri. + +## Non-goals + +- Replacing OpenCode's `task` tool or implementing a separate agent runner. +- Maintaining a second provider or model configuration. +- Sending inference traffic, telemetry, analytics, or update requests. +- Predicting future sequential task calls. +- Reproducing OpenCode's UI exactly across every host and platform. + +## Runtime Flow + +```text +OpenCode task call + │ + ▼ +tool.execute.before ── disabled ───────────────► built-in task + │ enabled + ▼ +session-scoped debounce batch + │ + ├── OpenCode SDK: models, agents, parent model + ▼ +bundled picker process ◄── NDJSON-RPC ──► Tauri + Svelte UI + │ + ├── cancel ─────────► no calls start + ├── technical error ► warning + OpenCode fallback + ▼ +selected model queued by parent session and agent + │ + ▼ +built-in task creates child session + │ + ▼ +chat.message applies and persists selected model + │ + ▼ +tool.execute.after reports model in task metadata +``` + +## Components + +### Plugin runtime + +`src/index.ts` composes the OpenCode hooks and owns the end-to-end dispatch +flow. It deliberately leaves the original `task` arguments unchanged. + +`src/batcher.ts` groups calls by parent session during a configurable debounce +window. Batches for one session are dispatched in order; different sessions +remain independent. + +`src/model-catalog.ts` shapes the enabled OpenCode provider catalog for the +picker. Agent defaults take precedence over the latest parent model for row +preselection. A hidden configured model may remain visible on its affected row, +but is not added to the apply-to-all catalog. + +`src/picker-process.ts` and `src/picker-rpc.ts` resolve the native binary, +manage its lifecycle, and enforce the picker protocol and size limits. + +`src/settings.ts` and `src/setup.ts` read, validate, merge, and write global and +project configuration. Invalid or unsafe configuration returns defaults with a +warning. + +`src/loopback-transport.ts` protects the extra client used to persist a selected +child-session model. It permits loopback HTTP only, rejects redirects, and +revalidates endpoints. + +### Native picker + +`picker/src-tauri/` is a small native host. It bridges process stdio to the +webview and owns native window behavior. + +`picker/src/` contains the Svelte UI. Selection and setup behavior is kept in +pure reducers and validators so it can be tested independently from rendering. +The picker receives model metadata and call identity, never task prompts or +descriptions. + +The theme resolver uses bundled OpenCode theme sources. Runtime theme hints +take precedence, followed by explicit preview parameters and fixture defaults. + +### Build and release tooling + +`scripts/` builds the ESM package and native picker, verifies the npm payload, +tests an installed package, exercises a real local OpenCode server, and checks +release invariants. The npm package contains prebuilt picker binaries and uses +no install lifecycle scripts. + +## Design Choices + +### Intercept the built-in task + +The plugin waits in `tool.execute.before` instead of adding a custom dispatch +tool. Agents continue using OpenCode's normal tool, so OpenCode remains +responsible for permissions, child creation, execution, cancellation, and +output. + +### Override the child message, not the agent + +OpenCode resolves its agent registry during startup, so a temporary agent +created when a task starts cannot reliably affect that task. The selected model +is queued instead and applied when the child session's first `chat.message` +hook runs. It is then persisted for later turns and copied to task metadata. + +OpenCode does not currently expose the originating task call ID before that +child message. Calls with the same parent and agent are therefore released in +FIFO order so selections cannot swap. This serialization should remain until a +supported API provides exact correlation at the same point in the lifecycle. + +### Debounce parallel work + +Calls arriving within the configured window, `500 ms` by default, share one +picker. Calls that occur after an earlier task finishes form a new batch. This +gives parallel dispatches one interaction without trying to predict later work. + +### Use an external Tauri picker + +A bundled Tauri mini-window provides a focused native overlay for both the TUI +and Desktop without requiring changes to OpenCode core. The tradeoff is a +cross-platform build and signing matrix plus best-effort, rather than exact, +host integration. + +### Keep IPC local and process-owned + +The plugin and picker exchange newline-delimited JSON-RPC over stdio. The +lifecycle is: + +1. Picker sends `ready`. +2. Plugin sends one bounded `start` request. +3. Picker sends `started` after the webview has hydrated the request. +4. Picker sends exactly one terminal `submit` or `cancel`. + +Each line is capped at 4 MiB. Startup remains timed until `started`; an +abandoned picker is terminated after the separate decision timeout. Transport +errors terminate the child process. No network listener is opened. + +### Follow OpenCode's model catalog + +Provider, model, variant, agent, and parent-session data comes from the +OpenCode SDK. The plugin does not maintain a separate allowlist. Effort values +are limited to variants advertised by the selected model. `Auto` omits an +explicit variant, preserving a compatible existing value only when the model +does not change. + +### Fail safely + +User cancellation and technical failure are distinct: + +- Cancellation starts no task in the affected batch. +- A technical picker or transport failure warns the user and leaves the task + unchanged so OpenCode chooses its configured fallback model. + +Invalid settings, duplicate call identity, oversized messages, unadvertised +variants, and unsafe persistence endpoints also fail into bounded, explicit +behavior rather than being accepted optimistically. + +### Separate global trust from project configuration + +Dispatch settings can be overridden per project. Privacy and appearance remain +global so repository-controlled files cannot enable logging or control local +theme behavior. Project settings symlinks are rejected, configuration files +are size-bounded, and setup handles `.gitignore` explicitly. + +### Keep the control plane private + +The picker sees call IDs, agent names, model metadata, and preselection only. +Logs contain bounded operational identifiers and failure categories. The +plugin has no hosted service, analytics, updater, or direct provider +connection. OpenCode's own provider requests remain outside this boundary. + +## Picker Design + +The picker should look like a close OpenCode companion surface: compact, +token-driven, keyboard-first, and quiet. + +- Prefer OpenCode theme tokens over a custom palette. +- Use a dense command-palette layout, restrained surfaces, and short labels. +- Keep base text around 12–13 px, titles around 13–14 px, and helper text + around 11–12 px. +- Make focus, hover, selection, disabled state, and errors visible. +- Escape cancels. Enter submits only when every row has a valid selection. +- Keep settings secondary to model selection. +- Avoid dashboard layouts, marketing copy, decorative gradients, large + headings, and decorative motion. +- Support keyboard-only use and never rely on color alone. + +## Changing the Design + +Before changing a boundary above, describe the current limitation and the new +invariant in the pull request. Update this document in the same change and add +tests at the narrowest useful level. Changes to OpenCode hook timing, picker +IPC, privacy boundaries, native packaging, or fallback semantics also require +the relevant installed-package or real OpenCode integration check. diff --git a/docs/compatibility.md b/docs/compatibility.md new file mode 100644 index 0000000..e0eacf5 --- /dev/null +++ b/docs/compatibility.md @@ -0,0 +1,121 @@ +# OpenCode Compatibility + +This document records the strongest compatibility claim the project makes. +`package.json` defines where the plugin can be installed and tried; the matrix +below is the historical record of exact version combinations that passed the +real integration suite. + +## Compatibility Matrix + +| OpenCode version | Latest `opencode-model-dispatch` release tested | Verification | Status | +| --- | --- | --- | --- | +| `1.18.7` | `0.1.0` | Real dispatch and same-agent FIFO | Rolling minimum | +| `1.18.9` | `0.1.0` | Real dispatch and same-agent FIFO | Archived | +| `1.18.10` | `0.1.0` | Real dispatch and same-agent FIFO | Rolling minor latest | + +Every exact OpenCode version that passes against a release is kept in this +table. Rows are never removed merely because the rolling window moves. When a +runtime version is no longer tested, its status changes to **Archived** and its +latest tested plugin release stays unchanged. If that exact runtime is tested +successfully against a later plugin release, update the existing row. + +Newer plugin versions may still work on an archived OpenCode version and users +are welcome to try them. The guarantee for that OpenCode version stops at the +plugin release recorded in the matrix. An OpenCode version absent from the +table has no tested compatibility guarantee. + +## Rolling Verification Window + +The +[OpenCode compatibility workflow](../.github/workflows/compatibility.yml) +runs every night and can also be started manually. It covers the current +supported OpenCode minor line and the five previous lines. For each line it +installs the latest stable patch and runs the real integration suite. + +While the declared minimum belongs to the rolling window, its exact version is +also tested. For example, the `1.18.x` line currently tests both the exact +minimum `1.18.7` and the latest published `1.18.x` patch. If those are the same +release, the workflow runs it only once. + +The resolver: + +1. reads `engines.opencode` from `package.json`; +2. fetches the published `opencode-ai` version list from npm; +3. groups matching stable releases into `major.minor.x` lines; +4. keeps the current line plus its five predecessors and selects the latest + patch in each; +5. adds the exact declared minimum while its line remains active. + +The implementation is in +[`scripts/resolve-opencode-compatibility.ts`](../scripts/resolve-opencode-compatibility.ts). +It accepts only the explicit `>=x.y.z ` is useful while iterating. Run the complete plugin suite +before handing off a runtime change. + +### Picker UI or protocol + +```sh +bun run --cwd picker build +bun run test:picker:rendered +bun run build:picker +bun run test:picker-ready +bun run test:gui:auto +``` + +`test:picker:rendered` covers computed themes, layout, and keyboard behavior. +`test:picker-ready` proves that the native webview hydrated the real start +request. `test:gui:auto` drives the production Tauri UI on Linux. Run +`bun run test:gui` as the final visible inspection when native interaction or +layout changes. + +### OpenCode integration or packaging + +```sh +bun run check:packaging +bun run check:notices +bun run test:package +bun run test:opencode +``` + +`test:package` packs the staged npm payload, installs it without lifecycle +scripts, and verifies Bun and Node ESM consumers. + +`test:opencode` starts a pinned real OpenCode server and a local deterministic +provider. It verifies selected model and effort persistence, task metadata, and +same-agent FIFO correlation without contacting an external model provider. Set +`OPENCODE_BIN` and the matching exact `OPENCODE_TEST_VERSION` to test a +specific executable. See the +[compatibility policy](compatibility.md#rolling-verification-window) for the +automated rolling matrix. + +On Linux, `bun run test:package:native:opencode` additionally installs the +tarball through an isolated loopback npm registry and drives its bundled native +picker through a real task. + +## Common Changes + +Use these checklists to find the complete change surface before editing. They +are starting points, not substitutes for reading the nearby implementation and +tests. + +### Add or Change a Setting + +1. Define the type, default, bounds, decoding, and merge behavior in + `src/settings.ts`. +2. Update read/write decisions in `src/setup.ts`. +3. Update the setup state in `picker/src/setup-reducer.ts` and its controls in + `picker/src/App.svelte`. +4. Preserve the trust boundary: project files may override dispatch behavior, + while privacy and appearance remain global. +5. Add focused coverage in `test/settings.test.ts`, `test/setup.test.ts`, and + picker reducer or UI contract tests. +6. Update the README configuration section and `docs/architecture.md` if the + trust model or defaults change. + +Run: + +```sh +bun test test/settings.test.ts test/setup.test.ts test/picker-ui.test.ts +bun run typecheck +``` + +### Change the Picker Protocol + +1. Update plugin-side message validation in `src/picker-rpc.ts` and process + lifecycle handling in `src/picker-process.ts`. +2. Update picker-side shapes and validation in `picker/src/protocol.ts`, + `picker/src/runtime-rpc.ts`, and `picker/src/runtime-request.ts`. +3. Update the Tauri bridge when framing, lifecycle, or native close behavior + changes. +4. Keep every message bounded and preserve the + `ready` → `start` → `started` → `submit`/`cancel` lifecycle unless the + architecture changes deliberately. +5. Update protocol, runtime, process, rendered UI, and native ready tests on + both sides of the boundary. + +Run: + +```sh +bun test test/picker-rpc.test.ts test/picker-process.test.ts test/picker-runtime-smoke.test.ts +bun run test:picker-ready +``` + +### Update the OpenCode SDK Contract + +1. Update the pinned plugin and SDK versions together in `package.json` and + regenerate `bun.lock`. +2. Review every adapter in `src/index.ts`, `src/model-catalog.ts`, and + `src/opencode-capabilities.ts` against the supported SDK response shapes and + hook timing. +3. Keep OpenCode as the source of truth. Do not replace an unavailable SDK + field with an undocumented config-file read. +4. Update capability, catalog, hook, declaration, installed-package, and real + OpenCode tests. +5. Change the documented OpenCode engine range only after the complete + integration path passes for that range. +6. For a release, update every exact OpenCode version that passed the rolling + workflow to the new plugin version and append newly tested runtime versions. +7. When an OpenCode minor becomes more than five lines older than the current + minor, keep all of its exact-version rows and mark them archived. + +Run: + +```sh +bun test test/opencode-capabilities.test.ts test/model-catalog.test.ts test/plugin-hooks.test.ts +bun run test:package +bun run test:opencode +``` + +### Update Bundled OpenCode Themes + +1. Record the new upstream source commit in + `picker/src/opencode-themes/README.md`. +2. Update the theme snapshot and resolver together; do not mix files from + different upstream revisions. +3. Review source attribution, licenses, and `third-party/components.json`. +4. Regenerate third-party notices. +5. Verify light, dark, explicit, system, unknown-theme, and computed-token + behavior. + +Run: + +```sh +bun run notices:generate +bun run check:notices +bun run test:picker:rendered +``` + +### Add a Supported Platform + +1. Add one canonical platform/architecture mapping in + `src/picker-targets.ts`. +2. Update native target resolution, build tooling, the package launcher, + release workflows, and artifact validation. +3. Follow the existing `picker-${platform}-${arch}${ext}` asset contract. +4. Add the platform to the README support table and document its runtime + requirements. +5. Add architecture, binary-format, ready-handshake, installed-package, and + release-matrix coverage. Every supported OS/architecture pair must have its + own named CI check, and a platform is not supported until CI runs its + production artifact natively. + +Run: + +```sh +bun test test/picker-targets.test.ts test/packaging.test.ts +bun run check:packaging +bun run test:package +``` + +### Change Privacy-sensitive Logging + +1. Treat every new log field as public operational metadata. +2. Keep prompts, descriptions, user text, file content, model responses, and + prompt-derived values out of log inputs as well as serialized output. +3. Preserve complete suppression when `privacy.logging_enabled` is false while + retaining local user-facing warnings. +4. Update `test/logging.test.ts`, `SECURITY.md`, the README privacy section, and + the architecture privacy boundary when the observable contract changes. + +Run: + +```sh +bun test test/logging.test.ts test/local-runtime.test.ts +bun run typecheck +``` + +## Generated and Third-party Files + +Do not hand-edit `THIRD_PARTY_NOTICES.md` or +`third-party/RUST_THIRD_PARTY_LICENSES.md`. After dependency or bundled-source +changes, install the pinned generator and regenerate: + +```sh +cargo install --locked cargo-about --version 0.9.1 --features cli +bun run notices:generate +bun run check:notices +``` + +The files under `picker/src/opencode-themes/` are adapted from OpenCode. Keep +their source attribution current and review license/notices whenever the +snapshot changes. + +Native picker binaries and build directories are generated artifacts. Do not +commit a local build unless the release workflow explicitly calls for staged +artifact inspection. + +## Documentation + +- `README.md`: installation, behavior, configuration, and troubleshooting. +- `CONTRIBUTING.md`: contributor entry point and pull request expectations. +- `AGENTS.md`: concise coding rules used by humans and coding agents. +- `docs/architecture.md`: current system design and durable choices. +- `docs/compatibility.md`: OpenCode support matrix and nightly verification. +- `docs/development.md`: local workflow and verification. +- `docs/releasing.md`: maintainer-only release process. +- `docs/manual-integration-gate.md`: evidence template for a release candidate. diff --git a/docs/issues/0001-hard-gates-opencode-sdk-and-shadow-agent-feasibility.md b/docs/issues/0001-hard-gates-opencode-sdk-and-shadow-agent-feasibility.md deleted file mode 100644 index 6f413ea..0000000 --- a/docs/issues/0001-hard-gates-opencode-sdk-and-shadow-agent-feasibility.md +++ /dev/null @@ -1,19 +0,0 @@ -# 0001 Hard Gates: OpenCode SDK And Shadow-Agent Feasibility - -Parent: [`PRODUCT.md`](../../PRODUCT.md) - -Status: historical; the shadow-agent approach was superseded by ADR 0009. - -What to build: Prove the critical OpenCode integration assumptions before dependent implementation proceeds: enabled/visible model discovery through stable SDK/API, agent model metadata, parent/current model discovery, shadow-agent display-name invariants, per-call shadow registration timing, task hook mutation safety, and child-session mapping viability. - -Acceptance criteria: -- `probeVisibleModels(client)` accepts provider/model responses with enabled/visible information. -- The probe rejects responses where model visibility cannot be determined. -- Provider/model order from OpenCode is preserved. -- Shadow-agent creation preserves original agent `name` while using an internal lookup key. -- Shadow-agent definitions copy permissions, prompt, mode, tools/options, description, and selected model from the latest source agent. -- Generated shadow keys are unique per call and safe for config keys. -- A local OpenCode spike confirms per-call shadow agents can be exposed before built-in `task` resolves `agent.get()`. -- If any hard gate fails, implementation stops and the requirement for upstream API support is documented. - -Blocked by: - diff --git a/docs/issues/0002-settings-merge-and-privacy-defaults.md b/docs/issues/0002-settings-merge-and-privacy-defaults.md deleted file mode 100644 index 92b161e..0000000 --- a/docs/issues/0002-settings-merge-and-privacy-defaults.md +++ /dev/null @@ -1,16 +0,0 @@ -# 0002 Settings Merge And Privacy Defaults - -Parent: [`PRODUCT.md`](../../PRODUCT.md) - -What to build: Implement the settings layer for global and project `model-dispatch.json` files, including default values, deep merge behavior, corrupt-config handling, and global-only privacy settings. - -Acceptance criteria: -- Reads global and project `model-dispatch.json` settings. -- Deep merges `dispatch` settings. -- Keeps `privacy.logging_enabled` global-only. -- Defaults to `dispatch.enabled: false`, `batch_ms: 500`, `picker_timeout_ms: 20000`, and `technical_failure: "default_model"`. -- Supports `setup.snoozed_until` and 24-hour setup snooze. -- Corrupt settings return defaults plus a warning. -- Tests cover settings reads, writes, decoding, merging, defaults, and corrupt input. - -Blocked by: - diff --git a/docs/issues/0003-privacy-safe-logging.md b/docs/issues/0003-privacy-safe-logging.md deleted file mode 100644 index 236aa3c..0000000 --- a/docs/issues/0003-privacy-safe-logging.md +++ /dev/null @@ -1,15 +0,0 @@ -# 0003 Privacy-Safe Logging - -Parent: [`PRODUCT.md`](../../PRODUCT.md) - -What to build: Implement structured plugin logging that records useful operational telemetry while excluding prompts, descriptions, user text, and other sensitive task details, with complete suppression when privacy logging is disabled. - -Acceptance criteria: -- Success logs include only non-sensitive plugin telemetry. -- Failure logs include code, reason category, batch/call/session ids, platform, picker version, and IPC/process status. -- Prompts, descriptions, and user text are never logged. -- `privacy.logging_enabled: false` suppresses all plugin logs. -- User-facing warnings/errors are still returned when logging is disabled. -- Supports `MODEL_DISPATCH_CANCELLED` and `MODEL_DISPATCH_PICKER_FAILED` codes. - -Blocked by: 0002 diff --git a/docs/issues/0004-model-catalog-and-preselection.md b/docs/issues/0004-model-catalog-and-preselection.md deleted file mode 100644 index b245084..0000000 --- a/docs/issues/0004-model-catalog-and-preselection.md +++ /dev/null @@ -1,19 +0,0 @@ -# 0004 Model Catalog And Preselection - -Parent: [`PRODUCT.md`](../../PRODUCT.md) - -What to build: Build the provider-grouped picker catalog from visible OpenCode models and derive per-task preselection from agent defaults or parent/current session model metadata. - -Acceptance criteria: -- Builds provider-grouped picker catalog from visible SDK models. -- Preserves OpenCode provider/model order. -- Reads agent defaults from `client.app.agents()` model metadata. -- Finds parent/current model from latest assistant message via `client.session.messages()`. -- Preselects agent model first, then parent/current model. -- Includes only provider-advertised effort variants and preserves a supported - configured variant in row preselection. -- Shows hidden configured/current preselect only on that row, not in apply-to-all. -- Provider icon resolves best-effort from metadata, bundled map, then initial. -- Tests cover catalog shaping, preselection, hidden model handling, and icon fallback. - -Blocked by: 0001 diff --git a/docs/issues/0005-picker-json-rpc-protocol-contract.md b/docs/issues/0005-picker-json-rpc-protocol-contract.md deleted file mode 100644 index f98de95..0000000 --- a/docs/issues/0005-picker-json-rpc-protocol-contract.md +++ /dev/null @@ -1,20 +0,0 @@ -# 0005 Picker JSON-RPC Protocol Contract - -Parent: [`PRODUCT.md`](../../PRODUCT.md) - -Status: implemented with the catalog-at-launch v1 protocol. Earlier live -validation/refresh method names are superseded by ADR 0005. - -What to build: Implement the stdin/stdout NDJSON JSON-RPC protocol shared by -the plugin and picker, including framing, parse failures, lifecycle, and v1 -method names. - -Acceptance criteria: -- Encodes and decodes one JSON-RPC notification per line. -- Handles parse errors as technical failures with debug reason. -- Supports `ready`, `start`, `started`, `submit`, and `cancel`. -- Treats parent stdin EOF, process exit, start-write failure, and lost stdio as - explicit lifecycle outcomes. -- Contract tests verify framing compatibility with a picker-like harness. - -Blocked by: - diff --git a/docs/issues/0006-picker-process-manager.md b/docs/issues/0006-picker-process-manager.md deleted file mode 100644 index 758594a..0000000 --- a/docs/issues/0006-picker-process-manager.md +++ /dev/null @@ -1,23 +0,0 @@ -# 0006 Picker Process Manager - -Parent: [`PRODUCT.md`](../../PRODUCT.md) - -What to build: Implement the plugin-side picker process manager that launches -the platform picker binary, connects JSON-RPC stdio, waits for the -`ready`/`start`/`started` handshake, and classifies cancel versus technical -failure. - -Acceptance criteria: -- Starts the platform picker binary and waits for `ready`, then sends `start` - and waits for `started`. -- Resolves bundled picker binary path per platform. -- Applies startup/connect timeout default `20000`. -- Treats missing binary, timeout, crash, lost stdio, and invalid payload as technical failures. -- Treats observed cancel as cancel even if the process exits afterward. -- Applies a separate 10-minute decision timeout after `started`. -- Bounds inbound and outbound JSON-RPC lines at 4 MiB and terminates the child - process on every technical transport failure. -- Tests cover successful launch, both timeouts, missing binary, crash, invalid - or oversized payload, lost stdio, process termination, and cancel precedence. - -Blocked by: 0005 diff --git a/docs/issues/0007-batch-coordinator.md b/docs/issues/0007-batch-coordinator.md deleted file mode 100644 index 6030a57..0000000 --- a/docs/issues/0007-batch-coordinator.md +++ /dev/null @@ -1,15 +0,0 @@ -# 0007 Batch Coordinator - -Parent: [`PRODUCT.md`](../../PRODUCT.md) - -What to build: Implement the session-scoped task batching coordinator that groups near-simultaneous task calls, resolves each waiter independently, and preserves fallback/cancel semantics. - -Acceptance criteria: -- Groups task calls by session and debounce window using default `500 ms`. -- Resolves all waiters in a batch with selections. -- Cancels all waiters on picker cancel. -- On technical failure, marks fallback and leaves original args unchanged. -- Preserves original concurrency by resolving all waiters independently. -- Tests cover grouping, independent resolution, cancel, technical failure, and session isolation. - -Blocked by: - diff --git a/docs/issues/0008-shadow-agent-creation.md b/docs/issues/0008-shadow-agent-creation.md deleted file mode 100644 index 0c12ed0..0000000 --- a/docs/issues/0008-shadow-agent-creation.md +++ /dev/null @@ -1,17 +0,0 @@ -# 0008 Shadow-Agent Creation - -Parent: [`PRODUCT.md`](../../PRODUCT.md) - -Status: superseded by ADR 0009; no shadow agents are created. - -What to build: Implement the pure shadow-agent creation helpers that generate internal lookup keys and config injection shapes for selected per-task models while preserving original agent identity. - -Acceptance criteria: -- Creates a shadow definition from the current agent and selected model. -- Uses an internal lookup key while preserving original `name`. -- Copies permissions, prompt, mode, tools/options, description, and relevant metadata from the latest source agent. -- Generated keys are unique per call and safe for config keys. -- Exposes config injection shape needed by plugin hook wiring. -- Tests cover key generation, copied fields, selected model override, and display-name preservation. - -Blocked by: 0001 diff --git a/docs/issues/0009-shadow-agent-cache-and-cleanup.md b/docs/issues/0009-shadow-agent-cache-and-cleanup.md deleted file mode 100644 index ef3d164..0000000 --- a/docs/issues/0009-shadow-agent-cache-and-cleanup.md +++ /dev/null @@ -1,18 +0,0 @@ -# 0009 Shadow-Agent Cache And Cleanup - -Parent: [`PRODUCT.md`](../../PRODUCT.md) - -Status: superseded by ADR 0009; no shadow-agent cache exists. - -What to build: Implement persistent shadow-agent lifecycle state and cleanup rules so shadow agents remain available for active child sessions and are removed when orphaned, archived, missing, or stale. - -Acceptance criteria: -- Stores `callID -> shadowKey` before task execution. -- Updates `shadowKey -> childSessionID` from task metadata after task execution. -- Dispose removes only orphaned shadows without child session ids. -- `session.updated` with `time.archived` removes mapped shadow. -- Startup garbage collection removes stale orphans and mapped shadows whose child session is archived or missing. -- Stores cache under the user's OpenCode/plugin cache area, not the project repo. -- Tests cover call mapping, child-session mapping, dispose cleanup, archive cleanup, and startup GC. - -Blocked by: 0008 diff --git a/docs/issues/0010-plugin-hook-wiring-disabled-and-single-task-success.md b/docs/issues/0010-plugin-hook-wiring-disabled-and-single-task-success.md deleted file mode 100644 index 6dfd4c8..0000000 --- a/docs/issues/0010-plugin-hook-wiring-disabled-and-single-task-success.md +++ /dev/null @@ -1,18 +0,0 @@ -# 0010 Plugin Hook Wiring: Disabled And Single-Task Success - -Parent: [`PRODUCT.md`](../../PRODUCT.md) - -Status: implemented using child-message model override and session-model -persistence; the shadow-agent details below are historical. - -What to build: Wire the plugin hooks for the disabled path and the first successful single-task dispatch path by composing settings, model catalog, picker process, shadow-agent creation, and task argument rewriting. - -Acceptance criteria: -- Plugin returns hooks: `config`, `event`, `dispose`, `tool.execute.before`, `tool.execute.after`, and custom tool registration placeholder if needed. -- Disabled dispatch leaves task args unchanged. -- Enabled dispatch waits for a single picker result. -- Successful selection creates a shadow agent and rewrites only `output.args.subagent_type` to the shadow key. -- Built-in task execution remains responsible for child session creation, permissions, metadata, and output. -- Tests cover disabled behavior and single-task success with real-shape OpenCode SDK fakes. - -Blocked by: 0002, 0004, 0006, 0008 diff --git a/docs/issues/0011-plugin-hook-wiring-cancel-and-technical-failure.md b/docs/issues/0011-plugin-hook-wiring-cancel-and-technical-failure.md deleted file mode 100644 index bfcba6d..0000000 --- a/docs/issues/0011-plugin-hook-wiring-cancel-and-technical-failure.md +++ /dev/null @@ -1,15 +0,0 @@ -# 0011 Plugin Hook Wiring: Cancel And Technical Failure - -Parent: [`PRODUCT.md`](../../PRODUCT.md) - -What to build: Complete plugin hook failure semantics for user cancel and technical picker failure, including privacy-safe logging, warning paths, and preservation of original task args on fallback. - -Acceptance criteria: -- Cancel throws `Error("Model selection cancelled")`. -- Cancel starts no subagents for the affected batch. -- Technical failure leaves args unchanged and emits a user-facing warning path. -- Technical failure uses OpenCode's normal configured/default/current model behavior. -- Logs use `MODEL_DISPATCH_CANCELLED` and `MODEL_DISPATCH_PICKER_FAILED` without sensitive user text. -- Tests cover cancel, technical failure, logging enabled, logging disabled, and unchanged args on fallback. - -Blocked by: 0003, 0006, 0007, 0010 diff --git a/docs/issues/0012-plugin-hook-wiring-batched-task-dispatch.md b/docs/issues/0012-plugin-hook-wiring-batched-task-dispatch.md deleted file mode 100644 index 9e7efca..0000000 --- a/docs/issues/0012-plugin-hook-wiring-batched-task-dispatch.md +++ /dev/null @@ -1,18 +0,0 @@ -# 0012 Plugin Hook Wiring: Batched Task Dispatch - -Parent: [`PRODUCT.md`](../../PRODUCT.md) - -Status: implemented using correlated child-message overrides rather than -per-call shadow agents. - -What to build: Wire batched task dispatch into the plugin hooks so multiple task calls in the same session/debounce window share one picker decision while each call continues independently after selection. - -Acceptance criteria: -- Enabled dispatch groups task calls by session and batch window. -- One picker request represents all calls in the batch. -- Per-row selections create per-call shadow agents. -- Apply-to-all selection applies the selected model to every task row. -- All waiters resolve independently and preserve original concurrency. -- Tests cover multiple parallel calls, per-row selections, apply-to-all, and separate sessions/batches. - -Blocked by: 0007, 0010 diff --git a/docs/issues/0013-setup-and-configure-ui-backend.md b/docs/issues/0013-setup-and-configure-ui-backend.md deleted file mode 100644 index a8cc44a..0000000 --- a/docs/issues/0013-setup-and-configure-ui-backend.md +++ /dev/null @@ -1,20 +0,0 @@ -# 0013 Setup And Configure UI Backend - -Parent: [`PRODUCT.md`](../../PRODUCT.md) - -What to build: Implement the backend flow for first-run setup and the user-requested `configure_model_dispatch` tool, reusing picker protocol/setup mode to read, write, snooze, reset, and scope settings. - -Acceptance criteria: -- No config at plugin load opens setup unless snoozed. -- Setup cancel disables dispatch and snoozes for 24 hours. -- Setup snooze is written globally. -- Privacy/logging writes global only. -- Dispatch behavior writes global or project config based on user choice. -- Project config creates `.opencode/model-dispatch.json`. -- Git repo path offers checked-by-default gitignore option. -- If selected, `.opencode/model-dispatch.json` is added to `.gitignore` and the user is informed. -- Non-git project writes config and reports no gitignore update needed. -- Reset to defaults works. -- Tests cover first-run, snooze, global/project writes, gitignore behavior, non-git behavior, and reset. - -Blocked by: 0002, 0005 diff --git a/docs/issues/0014-tauri-picker-app-model-selection-mode.md b/docs/issues/0014-tauri-picker-app-model-selection-mode.md deleted file mode 100644 index 3f495e7..0000000 --- a/docs/issues/0014-tauri-picker-app-model-selection-mode.md +++ /dev/null @@ -1,29 +0,0 @@ -# 0014 Tauri Picker App: Model Selection Mode - -Parent: [`PRODUCT.md`](../../PRODUCT.md) - -Status: implemented with catalog-at-launch IPC and the current keyboard -interaction. - -What to build: Build the Tauri picker app's model selection mode, including OpenCode-like theming, adaptive always-on-top window behavior, task rows, apply-to-all, dropdown search, validation state, submit/cancel, and keyboard interaction. - -Acceptance criteria: -- Adds `picker/` Tauri app for model selection mode. -- Uses OpenCode-like dark/light theme tokens and best-effort theme resolver. -- Implements adaptive always-on-top focused window behavior. -- Shows apply-to-all first, then one row per task. -- Task rows show the agent type; prompts and task descriptions are not sent to - the picker. -- Dropdown groups models by provider and supports search. -- Apply-to-all applies the selected model to all rows and keeps focus on apply-to-all. -- Each selected model has an effort control whose default is `Auto`; it lists - only that model's advertised variants and clears incompatible effort on a - model change. -- Submit is disabled until every row has a model. -- Dropdown keyboard behavior supports arrows, Home, End, Enter, and Escape; - global Enter submits a valid model view and Escape cancels. -- Unit tests cover rows, preselection, apply-to-all, dropdown search, submit - state, and keyboard commands. -- Contract test exchanges NDJSON JSON-RPC with the plugin test harness. - -Blocked by: 0005 diff --git a/docs/issues/0015-tauri-picker-app-setup-config-mode.md b/docs/issues/0015-tauri-picker-app-setup-config-mode.md deleted file mode 100644 index a149049..0000000 --- a/docs/issues/0015-tauri-picker-app-setup-config-mode.md +++ /dev/null @@ -1,17 +0,0 @@ -# 0015 Tauri Picker App: Setup/Config Mode - -Parent: [`PRODUCT.md`](../../PRODUCT.md) - -What to build: Extend the Tauri picker app with setup/configuration mode so first-run setup and the `configure_model_dispatch` tool can edit v1 settings, choose config scope, manage privacy, snooze/cancel, and reset defaults. - -Acceptance criteria: -- Implements setup/config mode in the same Tauri shell as picker mode. -- Shows controls for enable dispatch, batch window, startup/connect timeout, technical failure behavior, logging/privacy, and config scope. -- Explains global-only privacy/logging behavior. -- Supports setup cancel and snooze flow. -- Supports reset to defaults. -- Supports project config gitignore choice and warning text. -- Uses the shared JSON-RPC protocol for setup/config messages. -- Tests cover setup/config UI state, validation, reset, cancel/snooze, scope selection, and protocol exchange. - -Blocked by: 0013, 0014 diff --git a/docs/issues/0016-packaging-and-release-assets.md b/docs/issues/0016-packaging-and-release-assets.md deleted file mode 100644 index 28d46ea..0000000 --- a/docs/issues/0016-packaging-and-release-assets.md +++ /dev/null @@ -1,16 +0,0 @@ -# 0016 Packaging And Release Assets - -Parent: [`PRODUCT.md`](../../PRODUCT.md) - -What to build: Package the plugin and picker for release so published npm installs do not require a Rust/Tauri toolchain and CI validates plugin tests, typecheck, picker build, packaging, and release asset behavior. - -Acceptance criteria: -- Package platform picker binaries or document and implement release asset download path. -- npm install for published builds does not require Rust/Tauri toolchain. -- CI runs plugin tests, typecheck, picker build, and packaging checks. -- Tagged release workflow publishes the plugin and platform picker assets. -- README documents install, configuration, setup, privacy, and troubleshooting. -- Clean install test from packed tarball succeeds. -- Picker binary launch smoke test succeeds. - -Blocked by: 0014, 0015 diff --git a/docs/issues/0017-manual-opencode-integration-gate.md b/docs/issues/0017-manual-opencode-integration-gate.md deleted file mode 100644 index 4c4c01c..0000000 --- a/docs/issues/0017-manual-opencode-integration-gate.md +++ /dev/null @@ -1,25 +0,0 @@ -# 0017 Manual OpenCode Integration Gate - -Parent: [`PRODUCT.md`](../../PRODUCT.md) - -What to build: Run the final manual integration gate against a local OpenCode instance in a scratch project, validating the complete plugin, picker, setup, dispatch, batching, failure, display-name, model-persistence, signing, and publishing behavior before release. - -Acceptance criteria: -- Local OpenCode starts with the plugin installed in a scratch project. -- First-run setup opens at plugin load. -- Dispatch remains disabled if setup is cancelled and snoozed. -- Enabling dispatch works. -- One built-in `task` opens the picker and selection overrides the model. -- Multiple parallel `task` calls batch into one picker. -- Apply-to-all and per-row selections both work. -- Child sessions show original agent names in TUI/Desktop history. -- Technical picker failure falls back to built-in task default/current model with warning. -- Explicit cancel starts no subagents. -- Subsequent child turns and task metadata retain the selected model. -- Native close behaves as explicit cancel. -- TUI/Desktop evidence is recorded for the exact release-candidate commit; - signing and checksums remain automated workflow gates, while repository - security is verified by the separate local pre-tag gate. -- Every hard gate passes; a failed check blocks release until fixed and rerun. - -Blocked by: 0011, 0012, 0016 diff --git a/docs/issues/README.md b/docs/issues/README.md deleted file mode 100644 index 194c7f8..0000000 --- a/docs/issues/README.md +++ /dev/null @@ -1,12 +0,0 @@ -# Historical implementation briefs - -These numbered files are archived implementation briefs, not the current -product or architecture specification. The repository README, `PRODUCT.md`, -accepted ADRs, tests, and `docs/manual-integration-gate.md` are authoritative. - -- `0001` and `0008`–`0010` describe a shadow-agent design superseded by ADR - 0009. -- `0012` was implemented with child-message model override and session-model - persistence instead of per-call shadow agents. -- `0002`–`0007`, `0011`, and `0013`–`0016` are implemented. -- `0017` remains the live, fail-closed release gate. diff --git a/docs/releasing.md b/docs/releasing.md new file mode 100644 index 0000000..20e44a1 --- /dev/null +++ b/docs/releasing.md @@ -0,0 +1,163 @@ +# Releasing + +This document is for maintainers. The release process is deliberately +fail-closed because one npm package contains the plugin and prebuilt native +picker binaries for every supported platform. + +## Before the Release Candidate + +- Update `CHANGELOG.md`. +- Keep `package.json`, `picker/package.json`, + `picker/src-tauri/tauri.conf.json`, `picker/src-tauri/Cargo.toml`, and the + picker package entry in `picker/src-tauri/Cargo.lock` synchronized. +- After the rolling OpenCode compatibility workflow passes, update every exact + runtime version it tested in `docs/compatibility.md` to the release version + and append newly tested versions. Preserve older rows and archive versions + leaving the rolling window with their last verified plugin release. +- Regenerate and verify third-party notices after dependency changes. +- Confirm the required repository protections before storing or using signing + or publishing credentials. + +After the intended source candidate is merged to `main` and equals +`origin/main`, run the automated candidate preflight without privileged +tokens: + +```sh +bun run release:candidate-preflight +``` + +Run the public repository check as a separate process: + +```sh +bun run check:public-repo +``` + +The source preflight requires a clean tracked tree and does not require all +cross-platform binaries in a developer checkout. Use +`bun run release:artifact-preflight` only when all five generated picker assets +have deliberately been staged for inspection. + +## Manual Integration Evidence + +Complete `docs/manual-integration-gate.md` from the exact intended source +commit. Record its full `git rev-parse HEAD`, the tested tarball, and the TUI +and Desktop results. + +Commit only the completed evidence file on a branch based on that source +commit. Merge it through protected `main`, confirm that the merged commit +differs from the tested source only by the evidence file, and wait for CI on +the exact merged SHA. If `main` moves first, rerun the gate from the new +candidate. + +From that clean merged SHA, run the final local pre-tag preflight: + +```sh +bun run release:preflight +``` + +Rerun `bun run check:public-repo` from the same exact merged SHA so its +repository-settings and successful push-CI evidence apply to the commit that +will be tagged. Revoke the short-lived administration token after this final +check and before pushing the tag. + +## Tagging + +The release tag must be exactly `v` and point at the current +`origin/main`. Create and push it only after the manual evidence merge and its +exact-SHA CI run succeed. + +The tagged workflow verifies source state and ancestry before it: + +1. Builds and Rust-tests Linux x64/ARM64, macOS ARM64, and Windows x64/ARM64 + picker artifacts. +2. Exercises the real picker protocol and installed npm package on runnable + targets. +3. Signs and notarizes macOS and Authenticode-signs Windows artifacts in fresh + jobs that do not check out repository source. +4. Packs one exact npm tarball from the validated artifacts. +5. Publishes npm, verifies registry integrity and provenance, then publishes + the staged GitHub release with checksums and notices. + +A retried workflow reuses the verified, commit-bound assets in its staged draft +release. Do not edit or delete that draft while recovering a failed release; +substituted assets fail attestation verification. + +## Repository Protection + +Before adding any release credential, enable: + +- immutable GitHub releases; +- private vulnerability reporting; +- Dependabot security updates; +- required CI and deletion/force-push protection on `main`; +- an active `refs/tags/v*` ruleset that restricts create, update, and delete. + +Only the repository owner should be an always-allowed bypass actor for release +tag creation. Do not grant a broad repository role bypass. + +Repository protection is a mandatory local pre-tag gate. The tagged workflow +contains no repository-administration token. + +`check:public-repo` uses `GITHUB_REPOSITORY_SETTINGS_TOKEN` only in the local, +standalone gate. Use a short-lived fine-grained token scoped to this repository +with Administration read/write because GitHub omits ruleset bypass actors from +less privileged responses. Use a separate read-only `GITHUB_TOKEN` for the +exact-SHA Actions lookup. Do not pass either token to `release:preflight`, store +the administration token in Actions, or reuse it for publishing or signing. +Revoke it before pushing the release tag. + +## First npm Publish + +Because npm trusted publishing cannot be configured before the package exists, +the first release uses a short-lived granular access token: + +1. Set package scope to `All packages` because the new package cannot be + selected before its first publish. Grant `Read and write`, select the + shortest practical expiry, and enable the required 2FA bypass for + automation. +2. Store it as `NPM_BOOTSTRAP_TOKEN` in a protected `npm-bootstrap` GitHub + environment with maintainer approval. +3. Push the release tag. The workflow exposes the token only to the isolated + bootstrap job and only when the package name itself returns npm `E404`. +4. Configure npm trusted publishing for + `Lauritz-Timm/opencode-model-dispatch`, workflow `publish.yml`, with no + environment, and select the allowed action `npm publish`. +5. Revoke the bootstrap token, delete the Actions secret, require 2FA, and + disallow token publishing. + +Subsequent releases use GitHub OIDC trusted publishing and require no npm +repository secret. + +## Signing Secrets + +Store the Apple credentials only as environment secrets in the protected +`release-signing-macos` GitHub environment: + +- `APPLE_CERTIFICATE` (base64 P12) +- `APPLE_CERTIFICATE_PASSWORD` +- `APPLE_SIGNING_IDENTITY` +- `APPLE_KEYCHAIN_PASSWORD` +- `APPLE_API_PRIVATE_KEY` (base64 P8) +- `APPLE_API_KEY_ID` +- `APPLE_API_ISSUER_ID` + +Store the Windows credentials only as environment secrets in the protected +`release-signing-windows` GitHub environment: + +- `WINDOWS_CERTIFICATE` (base64 PFX) +- `WINDOWS_CERTIFICATE_PASSWORD` + +Require maintainer approval on both signing environments. Do not also define +these credentials as repository-level secrets. + +`NPM_BOOTSTRAP_TOKEN` exists for the first publish only. + +## Recovery + +- A failed local or manual gate requires a fix and a fresh run from the new + source commit. +- A tag must not move. Fix the cause and create the next version if publication + has crossed an irreversible registry boundary. +- Do not replace staged signed assets during a retry. +- Treat npm integrity, signature, provenance, or source-ancestry mismatches as + release blockers, not warnings. diff --git a/package.json b/package.json index 9578fd7..d13c091 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "dev:picker:tauri": "bun run scripts/dev-picker-tauri.ts", "preview:picker": "bun run dev:picker", "doctor:picker": "bun run scripts/check-picker-dev.ts", + "check:picker-host": "bun run scripts/check-built-picker.ts", "check:packaging": "bun run scripts/check-packaging.ts", "check:release-ci": "bun run scripts/check-release-ci.ts", "check:release-source": "bun run scripts/check-release-source.ts", @@ -73,7 +74,8 @@ "test:picker-ready": "bun run scripts/smoke-picker-ready.ts", "test:gui": "bun run build:picker && bun run scripts/smoke-native-picker.ts", "test:gui:auto": "bun run build:picker && bun run scripts/smoke-native-picker-auto.ts", - "release:preflight": "bun run check:release-source && bun run check:release-version && bun run check:notices && bun run typecheck && bun run check:coverage && cargo test --manifest-path picker/src-tauri/Cargo.toml --locked && bun run check:packaging && bun run test:package && bun run check:manual-gate", + "release:candidate-preflight": "bun run check:release-source && bun run check:release-version && bun run check:notices && bun run typecheck && bun run check:coverage && cargo test --manifest-path picker/src-tauri/Cargo.toml --locked && bun run check:packaging && bun run test:package", + "release:preflight": "bun run release:candidate-preflight && bun run check:manual-gate", "release:artifact-preflight": "bun run check:notices && bun run check:release-package && bun run test:package", "coverage": "bun test --coverage --coverage-reporter=lcov", "check:coverage": "bun run coverage && bun run scripts/check-coverage.ts", diff --git a/picker/src/App.svelte b/picker/src/App.svelte index eb42346..f67ac3b 100644 --- a/picker/src/App.svelte +++ b/picker/src/App.svelte @@ -28,6 +28,7 @@ type PickerSetupInput, } from "./runtime-request" import { createTauriPickerRuntimeAdapter, type PickerRuntimeAdapter } from "./runtime-rpc" + import CompactSelect from "./CompactSelect.svelte" import EffortSelect from "./EffortSelect.svelte" import ModelSelect from "./ModelSelect.svelte" import NumberRow from "./NumberRow.svelte" @@ -38,6 +39,10 @@ const maxPickerTimeoutMs = 600_000 const systemThemeMedia = "(prefers-color-scheme: light)" const decisionFailureMessage = "The picker could not complete the action. Please try again." + const scopeOptions = [ + { value: "global", label: "Global" }, + { value: "project", label: "This project" }, + ] const params = typeof window === "undefined" ? new URLSearchParams() : new URLSearchParams(window.location.search) let runtimeRequest = getPickerRuntimeRequest() const isPreviewWindow = isDevPreview && params.get("preview") === "1" @@ -272,6 +277,12 @@ } } + function setSetupScope(value: string) { + if (value !== "global" && value !== "project") return + setupScope = value + handleScopeChange() + } + function handleKeydown(event: KeyboardEvent) { if (event.key === "Escape") { event.preventDefault() @@ -487,16 +498,20 @@

Model Dispatch Settings

-