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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,12 @@ bun run build # Bundle + declaration emit

## Module Boundaries

| Module | Owns | Must Not |
|--------|------|----------|
| `src/tools/` | Tool definitions, schema, execute functions | Import from other tools |
| `src/runtime/` | Subprocess management, task registry, JSONL parsing, notifications | Reference OpenCode plugin/tool APIs directly |
| `src/discovery/` | Agent file discovery, description builder | Depend on runtime or tools |
| `src/index.ts` | Plugin entrypoint, wiring | Contain business logic |
| Module | Owns | Must Not |
| ---------------- | ------------------------------------------------------------------ | -------------------------------------------- |
| `src/tools/` | Tool definitions, schema, execute functions | Import from other tools |
| `src/runtime/` | Subprocess management, task registry, JSONL parsing, notifications | Reference OpenCode plugin/tool APIs directly |
| `src/discovery/` | Agent file discovery, description builder | Depend on runtime or tools |
| `src/index.ts` | Plugin entrypoint, wiring | Contain business logic |

## Persisting Architectural Memory

Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/fro-bot.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review, review_requested]
schedule:
- cron: '0 16 * * *' # Daily at 16:00 UTC
- cron: "0 16 * * *" # Daily at 16:00 UTC
workflow_dispatch:
inputs:
prompt:
Expand Down Expand Up @@ -207,7 +207,7 @@ jobs:
- name: Run Fro Bot
uses: fro-bot/agent@0f9f8fb198283a12e527311d285b1a422d8076d5 # v0.130.4
env:
OPENCODE_PROMPT_ARTIFACT: 'true'
OPENCODE_PROMPT_ARTIFACT: "true"
PROMPT: >-
${{
(github.event_name == 'workflow_dispatch' && (github.event.inputs.prompt || ''))
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ jobs:
GITHUB_TOKEN: ${{ steps.get-app-token.outputs.token }}
with:
setupGitUser: false
commit: 'chore(changesets): version packages'
title: 'chore(🦋📦): version packages'
commit: "chore(changesets): version packages"
title: "chore(🦋📦): version packages"
version: bun run version-changesets
publish: bun run publish-changesets
6 changes: 3 additions & 3 deletions .github/workflows/update-repo-settings.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ on:
push:
branches: [main]
paths:
- '.github/settings.yml'
- '.github/workflows/update-repo-settings.yaml'
- ".github/settings.yml"
- ".github/workflows/update-repo-settings.yaml"
schedule:
- cron: '55 2 * * *'
- cron: "55 2 * * *"
workflow_dispatch:

permissions:
Expand Down
11 changes: 0 additions & 11 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
The new tool wraps `copilot --resume=<target>` and integrates fully with the existing task lifecycle — output, cancel, and completion notifications all work the same way as delegated tasks.

Key behaviors:

- UUID targets are validated against the local Copilot session store before spawning; missing sessions return a structured error without touching the CLI.
- When a prior plugin task's session ID matches the target, its workspace paths (`--add-dir` set) are reused automatically when the caller omits `addDirs`.
- CLI no-match errors (`Error: No session, task, or name matched '...'`) are normalized to a clean `Session not found` response.
Expand All @@ -30,7 +29,6 @@
- 217a69e: Harden the plugin entry's public surface and Node-loadability so the build artifact survives the same class of regression that bit Systematic in v2.5.0 and v2.12.1.

OpenCode's plugin loader treats every named export from a plugin entry point as a separate plugin factory and invokes it with `undefined` input. That contract has bitten upstream plugins twice with hours of downtime each; this PR institutionalizes the fix in three coordinated changes:

- **Helper moved out of the entry point.** `wireRpcServerCleanup` now lives in `src/lib/rpc-cleanup.ts`. The plugin entry exports only `default` and re-imports the helper internally. No behavior change — the helper's identity-once semantics and its single caller are byte-identical.
- **Build target switched to Node for the plugin entry.** `scripts/build.ts` now builds `src/index.ts` with `target: 'node'` so `dist/index.js` loads under plain Node ESM. The TUI entry stays on `target: 'bun'` because `@opentui/solid` is Bun-specific. The Node-loadable build is the prerequisite for the new CI gate (next bullet).
- **CI gate asserts the export shape on every PR.** A new step in `.github/workflows/ci.yaml` between `Build` and `Unit tests` runs `node --input-type=module -e "import('./dist/index.js').then(m => …)"` and exits non-zero if the entry exposes any export other than `default` or if `default` is not a function. The local test surface gets a matching assertion in `tests/package-exports.test.ts`. The gate's failure message references the v2.5.0/v2.12.1 regression class so future contributors can find the rationale.
Expand All @@ -48,13 +46,11 @@
OpenCode 1.14.42 removed `api.command.register` in favor of the new keymap engine. 1.14.44+ restored `api.command.register` as a deprecated shim that translates to `api.keymap.registerLayer` internally. The TUI plugin's `/copilot-status` command was unconditionally calling `api.command.register`, which would silently disappear on OpenCode versions where the call path went away.

The TUI entry now runtime-feature-detects:

- **OpenCode 1.14.44+** (canonical): registers via `api.keymap.registerLayer({ commands: [{ namespace: 'palette', name: 'copilot-status', title: 'Copilot Status', category: 'Copilot', run() }], bindings: [] })`. Mirrors the dual-path pattern Magic Context established in commit `5fe1c4f`.
- **OpenCode 1.14.41** (the prior pinned version): falls back to `api.command.register(...)` with the original command shape, so the slash menu continues to surface `/copilot-status` exactly as before.
- **Neither API present** (defensive): logs a warning and continues to load the plugin without the slash command. The status modal remains available via direct API consumers.

Other surfaces:

- `devDependencies['@opencode-ai/plugin']` moves from `1.14.41` to `1.14.48` so tests run against the canonical keymap API.
- `peerDependencies['@opencode-ai/plugin']` narrows from `>=1.14.0` to `>=1.14.41` to align advertised compatibility with what's actually tested.

Expand Down Expand Up @@ -91,14 +87,12 @@
### Minor Changes

- 80b549e: Improve runtime observability and fix a silent failure in completion notifications.

- `killProcessTree` now classifies fkill failures by probing the process group with `process.kill(-pid, 0)`. ESRCH is reported as an already-gone group and the failure is suppressed; alive or unknown states preserve the original throw with a more accurate warning. The probe targets `-pid` (the process group) rather than the leader so children leaking after a leader exit are no longer misclassified as benign.
- `notifyCompletion`'s fallback `client.app.log` call now uses the structured SDK shape (`{ body: { service, level, message } }`) and is wrapped in `try/catch` so synchronous SDK throws can no longer escape the documented "never throws" contract.
- All runtime warnings now share the `[copilot-delegate]` prefix across `kill-tree`, `orphan-reaper`, `pid-file`, `task-registry`, and `task-status`, making operator log filtering predictable.
- Internal contract documentation: `setStatus` and `writeChains` carry JSDoc covering terminal-state transitions and the per-file serialize chain.

- 4f2e8b5: Harden PID file and orphan-reaper state path handling against same-user symlink attacks.

- Use no-follow PID file opens for read and truncate paths so symlinked `.pids` files are rejected instead of read or truncated.
- Reject symlinked PID file parent directories before orphan reaping, cleanup, and plugin init state-directory creation so `orphans/` scans and cleanup do not follow attacker-controlled links.

Expand All @@ -113,7 +107,6 @@
No configuration change is required for users with a single config source. Users with duplicate registrations will see one warning per process and a single set of tools in the catalog.

- 1fc343d: Tighten orphan-reaper internals and add two new exported helpers from `src/runtime/pid-file.ts`:

- `truncatePidFile(filePath)` — truncate under the per-file `serializeWrite` lock; ENOENT (file or parent missing) silently swallowed.
- `unlinkPidFile(filePath)` — unlink under the per-file `serializeWrite` lock; ENOENT silently swallowed.

Expand Down Expand Up @@ -168,7 +161,6 @@
- f3f21c7: Configurable orphan-reaper timeouts and parser extraction

Three medium-priority improvements to the plugin-init orphan-subprocess reaper, all from the runtime hardening backlog:

- **Configurable per-probe `ps` timeout with degradation warning**: `getPidIdentity` and the underlying `runPs` helper now accept an optional `timeoutMs` parameter (default 1000). On loaded systems where legitimate `ps` responses can exceed 1s, callers can pass a longer timeout to avoid identity-gate misses that leave orphans alive. When the timeout fires, `runPs` emits a `console.warn` so operators can detect probe degradation and tune the budget up.
- **Overall `reapOrphans` timeout with cooperative cancellation**: `reapOrphans` now accepts an optional `reapTimeoutMs` parameter (default 15000). When the timeout fires, the reap aborts via `AbortSignal`: in-flight workers cooperate by skipping their next mutating step (kill, truncate, unlink) so no dangerous side effects can occur after `reapOrphans` returns. The call resolves with an empty result and emits a `console.warn`. Prevents pathological cases (NFS `readdir` hang, all probes timing out simultaneously) from blocking plugin init indefinitely, and prevents lingering reap workers from wiping current-instance pid file entries that live tasks have appended after the timeout fired.
- **Parser extraction**: the comm/lstart parsing logic is lifted out of `getPidIdentity` into a new exported `parsePsIdentity(raw)` pure function with full edge-case coverage (empty input, single-token input, leading-whitespace input, the 15/16-char `comm` boundary, and multi-whitespace separator).
Expand All @@ -182,7 +174,6 @@
- 7dfb029: Eliminate head-of-line blocking and halve fork/exec cost in the orphan reaper

Two performance improvements to the plugin-init orphan-subprocess reaper, both surfaced in the `v0.1.x / v0.2.x runtime hardening follow-ups` tracking issue:

- **Streaming worker pool replaces chunked `Promise.all`**: under loaded conditions a single slow `ps` invocation (300-500ms is realistic) used to idle four sibling workers in the same chunk, blowing the soft init budget by 5-10x. The reaper now spawns up to `MAX_CONCURRENT_PROBES = 5` workers that drain a shared queue independently — a slow probe blocks only its own worker.
- **Combined `ps -p <pid> -o comm=,lstart=` query**: the two separate `comm` and `lstart` lookups (per entry, plus per task spawn) consolidate into a single `getPidIdentity(pid)` call. Halves fork/exec cost on the reap hot path, makes the concurrency-cap math match reality (5 workers ⇒ at most 5 concurrent `ps` subprocesses, not 10), and gets an atomic kernel snapshot of both identity legs.

Expand Down Expand Up @@ -214,15 +205,13 @@
Tests run against `opencode/minimax-m2.5` by default (override with `OPENCODE_TEST_MODEL`). The describe block is gated on either `GH_TOKEN` or `COPILOT_PAT` being set; when neither is present, the suite skips cleanly so `bun test` stays green on dev machines without a Copilot PAT. Per-test isolation uses `OPENCODE_CONFIG_DIR` pointed at an empty temp dir to skip the developer's globally-configured plugins, and `OPENCODE_CONFIG_CONTENT` to register the plugin's own `dist/index.js` as a `file://` plugin in each test session.

- 79d1f09: Add JSONL parser and envelope builder for Copilot CLI output processing

- `parseJsonlLine()` normalizes Copilot CLI JSONL events into typed `ParsedEvent` objects with defensive handling of malformed input
- `buildEnvelope()` folds parsed events into the structured `copilot_output` response shape with graceful degradation for missing fields
- Live JSONL fixtures captured from `copilot` CLI v1.0.34 for regression testing

- de20762: Document the upstream `ps`/prompt-visibility caveat, the absence of a subprocess lifetime cap, and the `0.x` versioning policy in the README. Correct the notification-injection description to match the implementation (`client.session.prompt` with `noReply`, not `promptAsync`).
- a23e15f: Add automated release pipeline. A new `Release` workflow runs after the `CI` workflow succeeds on `main`, opens a "Version Packages" pull request via `changesets/action` when changesets are pending, and publishes to npm via OIDC trusted publishing once that PR merges. Adds `@changesets/cli` as a development dependency and the `version-changesets` and `publish-changesets` scripts to drive the version-bump and publish steps.
- 6fd4c6d: Add subprocess wrapper, task registry, notification injection, and agent discovery

- Subprocess spawning with line-buffered JSONL parsing, auth token env precedence, and abort-based cancellation via fkill process group kills
- In-memory task registry with cleanup-all lifecycle
- Notification injection using noReply semantics with per-session in-flight counters
Expand Down
30 changes: 15 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,31 +59,31 @@ The plugin logs the resolved auth source (not the token value) at delegation sta

### `copilot_delegate`

| Arg | Type | Description |
|-----|------|-------------|
| `prompt` | `string` | Required. The prompt to send to Copilot. |
| `agent` | `string?` | Optional. Copilot agent name (see tool description for available agents). |
| `model` | `string?` | Optional. Model override (e.g. `claude-haiku-4.5`). |
| `add_dir` | `string[]?` | Optional. Additional directories to allow Copilot to read. |
| `allow_tool` | `string[]?` | Optional. Tool patterns to allow. |
| `deny_tool` | `string[]?` | Optional. Tool patterns to deny. |
| Arg | Type | Description |
| ------------ | ----------- | ------------------------------------------------------------------------- |
| `prompt` | `string` | Required. The prompt to send to Copilot. |
| `agent` | `string?` | Optional. Copilot agent name (see tool description for available agents). |
| `model` | `string?` | Optional. Model override (e.g. `claude-haiku-4.5`). |
| `add_dir` | `string[]?` | Optional. Additional directories to allow Copilot to read. |
| `allow_tool` | `string[]?` | Optional. Tool patterns to allow. |
| `deny_tool` | `string[]?` | Optional. Tool patterns to deny. |

Returns `{ task_id: string }` — a `cpl_`-prefixed UUID.

### `copilot_output`

| Arg | Type | Description |
|-----|------|-------------|
| `task_id` | `string` | Required. Task ID from `copilot_delegate`. |
| `block` | `boolean?` | Optional. Wait for completion before returning. Default `false`. |
| `timeout_ms` | `number?` | Optional. Max wait ms when `block: true`. Default `30000`, max `120000`. |
| Arg | Type | Description |
| ------------ | ---------- | ------------------------------------------------------------------------ |
| `task_id` | `string` | Required. Task ID from `copilot_delegate`. |
| `block` | `boolean?` | Optional. Wait for completion before returning. Default `false`. |
| `timeout_ms` | `number?` | Optional. Max wait ms when `block: true`. Default `30000`, max `120000`. |

Returns a structured envelope with `status`, `final_message`, `tokens`, `tool_calls_summary`, and more.

### `copilot_cancel`

| Arg | Type | Description |
|-----|------|-------------|
| Arg | Type | Description |
| --------- | -------- | ---------------------------- |
| `task_id` | `string` | Required. Task ID to cancel. |

Returns `{ cancelled: boolean; was_running: boolean }`.
Expand Down
Loading
Loading