Skip to content

Releases: marcusrbrown/opencode-copilot-delegate

v0.12.0

13 May 05:26
7f84434

Choose a tag to compare

Minor Changes

  • 2667712: Add copilot_resume tool for resuming prior Copilot sessions by ID, name, or prefix.

    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.
    • All path inputs (cwd, addDirs) are validated against allowed roots before spawn; argv-injection-shaped values are rejected.

    The tool catalog grows from 3 to 4. Resume + new prompt (fork) is not part of this release.

  • a02c3e7: Extract the post-completion pipeline (SpawnCopilotResultTaskState back-patch + completion notification) into a shared attachCompletionPipeline helper in src/runtime/notify.ts. copilot_delegate now delegates its void task.completionPromise.then(...) block to this helper; the upcoming copilot_resume tool will reuse it without duplicating the load-bearing field sync.

    Notification headers are now origin-aware: tasks created with origin: 'spawn' (today's only call site) keep the existing [COPILOT DELEGATION COMPLETED] header. Tasks with origin: 'resume' will surface [COPILOT RESUME COMPLETED] so users can distinguish a resumed Copilot session from a fresh delegation. The connect header is wired for forward compatibility but no connect-origin tasks are constructed in this slice.

  • 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.

    Also documents the divergence rationale: this plugin keeps the plugInOnce singleton pattern (returns empty hooks on duplicate invocations) even though Systematic's PR #352 replaced that pattern with per-load registration. The constraint inverts here because this plugin's doInit binds a TCP port and writes a PID file — running doInit twice in the same process would race on those exclusive resources. src/runtime/plugin-singleton.ts and src/lib/rpc-cleanup.ts carry top-of-file JSDoc explaining the divergence with cross-references to marcusrbrown/systematic#352.

    No user-visible behavior change.

  • 66f97f4: Add origin discriminator ('spawn' | 'resume' | 'connect') to TaskState, the OutputEnvelope returned by copilot_output, and the EnvelopeInput builder. Capture the upstream Copilot session UUID from the JSONL result event and surface it as copilot_session_id on the envelope (omitted when the subprocess never emitted a result event).

    Existing copilot_delegate calls receive origin: 'spawn' automatically and continue to behave the same way. The new fields are the substrate the upcoming copilot_resume tool builds on; they do not change today's tool surface.

  • 82a1026: Make the TUI plugin survive OpenCode's api.command.register migration.

    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.

v0.11.0

05 May 06:06
8b5f8ef

Choose a tag to compare

Minor Changes

  • 31dfb28: Fix host-side double registration of copilot_delegate, copilot_output, and copilot_cancel when the plugin is listed in both a user-level and project-level opencode.json.

    Previously, the per-process register-once guard returned the cached real hooks to every duplicate factory invocation in the same PID. The OpenCode host iterates each plugin source's returned hook surface and registers every tool entry it finds, even when two sources return the same JS reference. That meant each tool appeared twice in the LLM-visible tool catalog under dual-source configurations.

    The guard now returns empty hooks ({}) on duplicate invocations so the host has nothing to register a second time. The first invocation still runs doInit once and receives the real hooks; subsequent invocations in the same PID receive {} and emit a one-time warning. Heavy initialization (agent discovery, orphan reaping, RPC server startup) still runs at most once per process.

    Single-source configurations are unaffected.

Patch Changes

  • e019e86: Stabilize the cancel-process-tree test against CI runner load by polling process.kill(pid, 0) with a 2s deadline instead of asserting synchronously. Test-only change; no plugin behavior or consumer surface affected.

v0.10.1

02 May 09:03
9f0b6f2

Choose a tag to compare

Patch Changes

  • 98a827e: Fix the /copilot-status TUI freeze caused by re-entrant dialog close handling when pressing Escape.

v0.10.0

02 May 05:31
4127f79

Choose a tag to compare

Minor Changes

  • e84f127: Prepare the package for an optional Copilot status TUI by adding server and TUI plugin metadata plus dedicated package exports.

v0.9.0

01 May 01:00
790998e

Choose a tag to compare

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.

v0.8.0

30 Apr 09:30
640ff22

Choose a tag to compare

Minor Changes

  • 0c21f43: Make the plugin factory idempotent across multiple OpenCode config sources within the same process. When ~/.config/opencode/opencode.json AND a project-level opencode.json (or any other combination of config sources) both list opencode-copilot-delegate, OpenCode previously invoked the factory once per source — each invocation evaluating the plugin module fresh, running orphan reaping, and registering its own copy of the three tools. The result was duplicated tools in the catalog, duplicated init side effects (PID-file mkdir, reapOrphans), and per-invocation closure state that could diverge across registrations.

    The factory now resolves at most once per process via a globalThis symbol singleton (Symbol.for('opencode-copilot-delegate.singleton.v1')). Subsequent invocations within the same PID return the cached hooks promise, skip the heavy init, and emit a single one-shot warning (via console.warn AND client.app.log under service=copilot-delegate) so duplicate-config situations remain observable in logs. Across distinct OpenCode processes the singleton is fresh — each process initializes normally.

    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.

    Also export ReapOptions from src/runtime/orphan-reaper.ts (consolidates ReapDeps + reap-specific opts via interface extension), and consolidate reapOneFile to a single opts-bag parameter. cleanupAfterReap now routes its truncate-on-current and unlink-on-foreign paths through the new helpers so any future change that runs reap concurrent with task spawns is automatically race-safe.

    No user-visible behavior change in default usage; the bump reflects the new exported surface.

  • 42c5239: Add timedOut: boolean to ReapResult so consumers can distinguish a successful no-op reap (nothing to do) from a timeout-aborted reap (gave up; orphans may remain). The flag is false on every success path and true only when the overall reapOrphans timeout fires.

    When timedOut is true, the count fields are zero placeholders, not partial-progress accounting — in-flight workers may have already invoked killProcessTree or scanned files before the abort signal landed, but those side effects are not reflected in the returned counts. Treat a timed-out result as "no reliable count signal" and warn or retry.

    Note for TypeScript consumers: ReapResult is exported from the package's runtime types and gains a required field. Any caller constructing a ReapResult literal will need to add timedOut explicitly. Internal callers in this repo are already updated.

  • 3491e63: Tighten the setStatus lifecycle helper to forbid terminal → non-terminal transitions. Once a task reaches complete, failed, or cancelled, every subsequent setStatus call is a no-op. Previously the helper short-circuited only when both old and new status were terminal, leaving an unintended resurrection path that no caller exercises but the contract permitted. This narrows the public API contract; no caller behavior changes.

v0.7.0

29 Apr 17:50
caf4c90

Choose a tag to compare

Minor Changes

  • 3f8b78e: Surface per-parameter .describe() text to the host runtime by patching each tool arg schema with a _zod.toJSONSchema override.

    OpenCode's tool catalog renders plugin schemas via the host's bundled zod, which lives in a different module instance from the plugin's zod and cannot see the plugin-side .describe() metadata registry. The override delegates serialization back to the plugin-local zod so descriptions survive intact, mirroring the pattern shipped by @cortexkit/opencode-magic-context and @cortexkit/aft-opencode.

    Also pins zod as a direct dependency (^4.3.0) with a matching overrides entry to keep this repo's dependency tree on a single zod version, resolving a TS2883 unportable-inferred-type error from two zod trees coexisting at build time. The overrides field is local to this repo's installs only; npm ignores it for downstream consumers, so external plugin authors importing this package may still see a different transitive zod from their OpenCode host. Also reverts the prior schema-chain reorder since the override makes ordering irrelevant.

v0.6.0

29 Apr 07:46
8317352

Choose a tag to compare

Minor Changes

  • e0480fe: Surface per-parameter descriptions to host LLMs and drop the unused @opencode-ai/sdk peer dependency.

    OpenCode's tool-list endpoint serializes plugin schemas with Zod's toJSONSchema(..., { io: 'input' }) mode, which unwraps .optional() wrappers and drops any .describe() text attached to the wrapper. The 5 optional parameters across copilot_delegate and copilot_output chained .optional().describe(...) and lost their descriptions in the rendered tool catalog. Reordering to .describe(...).optional() places the description on the leaf type so it survives the unwrap.

    Also drops @opencode-ai/sdk from peerDependencies and the build externals — the package was never imported from any source file, so the peer requirement was dead config that forced consumers to install an unused dependency.

v0.5.0

29 Apr 06:08
7bcaeeb

Choose a tag to compare

Minor Changes

  • e940a28: Drop BUILTIN_AGENTS and enrich tool schema descriptions

    BUILTIN_AGENTS removal. The plugin previously advertised six "built-in" agent names — default, explore, task, general-purpose, code-review, research — inherited from VS Code Copilot Chat / OpenCode conventions. The standalone @github/copilot CLI v1.0.36 ships zero of these; passing any of them as --agent <name> causes Copilot CLI to fail at spawn time with Error: No such agent: <name>, available:. The constant has been removed. discoverAgents now returns user agents (filtered by repo override) followed by repo agents; Agent.source is 'user' | 'repo'. buildDescription handles the empty-discovery case by emitting an actionable hint pointing at ~/.copilot/agents and .github/agents.

    Tool schema enrichment. The three exposed tools — copilot_delegate, copilot_output, copilot_cancel — now ship multi-paragraph tool descriptions covering what they do, when to use them, lifecycle, common pitfalls, and return-value shape. Per-parameter .describe() text spells out type, default, when-required, example values, and constraints. Examples follow the patterns Magic Context's ctx_* tools established. The new descriptions surface in OpenCode's tool registry exposure to LLMs and via mise run opencode:doctor --only tools.

    No runtime behavior change beyond the agent discovery fix. Tool argument shapes are unchanged; existing callers continue to work without modification.

v0.4.0

29 Apr 05:26
9201df9

Choose a tag to compare

Minor Changes

  • 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).

    All new parameters are additive optionals; existing callers continue to work without modification. Behavior at default settings is unchanged for normal-load operation, but the new overall reap timeout introduces a conditional control-flow branch (warn + empty return) that activates only under pathological conditions where a reap exceeds the 15s default budget.