OpenCode harness extension and the dld-core library - #43
Merged
Conversation
DL-016: V2 combined plugin (server + CLI), shared state layer DL-017: Spike scope — validate state sharing, loop dispatch, exec, status, widget DL-018: Full implementation — complete UI surface after the spike Generated-By: pi 0.84.2 Generated-By: pi 0.84.2 Co-Authored-By: Kimi K3 <noreply@pi.dev>
The spike proves the core mechanism: an OpenCode V2 server plugin subscribes to session.execution.succeeded, reads the active run from .dld/runs/ via the shared bash scripts, and dispatches continuation prompts through ctx.session.prompt. The state layer is identical to the Pi extension. Key findings: - Loop dispatch works: session.execution.succeeded → ctx.session.prompt - Workspace root comes from session.location.directory, not process.cwd() - The event fires with two shapes; filter on data?.sessionID - execSync replaces pi.exec; Bun.$ was not needed - ctx.storage is server-only; the CLI plugin reads .dld/runs/ from disk - The plugin loads from .opencode/plugins/ with @opencode-ai/plugin@beta The TUI plugin (tui.tsx) implements status line, widget, and board but is untested — it requires an interactive TUI session. npm's @beta tag is stale (September 2025); bun installs the real beta-18269. The sandbox npm proxy blocks packages published after 2026-07-31. Generated-By: pi 0.84.2 Generated-By: pi 0.84.2 Co-Authored-By: Kimi K3 <noreply@pi.dev>
Gives the OpenCode spike real types and lets symlinked plugin files resolve the package from dld-kit's node_modules. Generated-By: pi 0.84.2 Generated-By: pi 0.84.2 Co-Authored-By: Kimi K3 <noreply@pi.dev>
The spike's command surface covered status, pause, resume, and stop but not start — the first command a user tries. Worse, the 'Unknown subcommand' synthetic message became agent context, and the agent started exploring the filesystem to diagnose the failure. The start flow mirrors the Pi extension: parseStartArgs handles range, dash, and positional forms; guard-preconditions refuses early; create-run plus add-item builds the run; a session.prompt kicks off item 1 immediately. Plugin messages are now prefixed '[dld-run plugin]' and phrased so the agent's correct action is to relay them verbatim, not debug them. DL-019 amends DL-017's spike scope. Co-Authored-By: Kimi K3 <noreply@pi.dev> Generated-By: pi 0.84.2
The first tui.tsx guessed the API surface. Real-world testing found: - ctx.ui.slot, not ctx.slot; render returns JSX, not strings - Keymap layers use Solid's useContext and must live inside a component — the board command mounts via an app-slot component, the pattern OpenCode's own storybook plugin uses - Slot renders are not reactive to filesystem reads; a shared createSignal refreshed by fs.watch on .dld/runs/ drives re-renders. The agent mutates state by shelling out to scripts mid-turn, so no OpenCode event can drive this — the filesystem is the only place every writer is visible. A 10s interval backs the watcher up. - JSX needs @jsxImportSource @opentui/solid or bun emits react imports that don't resolve outside React projects - solid-js and @opentui/solid are peer deps of @opencode-ai/plugin; the install script installs them alongside Verified live in llm-eval-service: status line updates during a run, the loop dispatched both items, and the run completed. Co-Authored-By: Kimi K3 <noreply@pi.dev> Generated-By: pi 0.84.2
The first live test surfaced dispatch spam: a session resuming a run with an in-flight item re-dispatched the identical prompt on every execution completion. The Opus 5 review found the root causes: - Dispatch guard: lastDispatch tracks which item each session was told to work; a turn ending mid-item no longer re-delivers it - Error mapping mirrors Pi: exit 2 pauses, other non-zero skips, exit-0-empty completes — a script failure can no longer mark a run complete - Paused runs resumable via list fallback; resume re-runs guard-preconditions (DL-004) - execFileSync replaces execSync: argv arrays, no shell, no injection through slugs or titles, paths with spaces work - TUI reactivity: signal reads moved inside JSX expressions — Solid untracks component bodies, which is why the sidebar went stale despite the fs.watch signal - Pause interrupts the in-flight turn (DL-014), stop writes stopped, transitions append events, partial start blocks the run Still unported, recorded in DL-020 as follow-up: the four-part completion transaction and maxMinutes enforcement. Item completion currently relies on the agent following the skill. Accept DL-020. Co-Authored-By: Kimi K3 <noreply@pi.dev> Generated-By: pi 0.84.2
… accept DL-021 The spike shipped without DL-003's completion transaction — item completion relied on the agent grading its own homework. The loop now runs the transaction on session.execution.succeeded, before dispatch: - verifying item with new evidence → verify-item.sh - pass + review enabled → nag the agent once per item (synthetic) - pass + review disabled → accept → repin → append-event, each step checked, a failure aborts the rest - fail, attempts < 2 → back to implementing, dispatch guard cleared so the retry is delivered - fail, attempts >= 2 → block-item, run paused, failure surfaced Completion takes priority over dispatch, and the run status is re-read after the transaction so a blocked-item pause doesn't fall through to dispatching on stale state. maxMinutes enforcement remains the one unported bound — separate follow-up. Co-Authored-By: Kimi K3 <noreply@pi.dev> Generated-By: pi 0.84.2
Deletions: collapsed the three-way --decisions branch, removed the dead onCleanup try/catch (Solid warns, never throws), made blockedQuestions optional to match the guards, removed the dead scriptPath import in the Pi run-state.ts, extracted itemIcon from two duplicated ternary chains, padded range slugs (dl-014-022). Correctness: verify-item.sh gets 300s timeout and 16MB buffer with SIGTERM mapped to code 3 (infrastructure, not a verification failure — the transaction surfaces it without counting an attempt); review is required and fails toward more review; the item claim and the partial-start rollback are checked, with a loud message when the rollback itself fails; past-tense and statusMap narrowing cleaned up; tui.tsx uses execFileSync like the server. Co-Authored-By: Kimi K3 <noreply@pi.dev> Generated-By: pi 0.84.2
docs/plan/typescript-core.md outlines the two-move path: dld-core as a shared TS module with a function-shaped API (strangler-fig over the bash scripts), then the state layer rewritten as TS and dld-kit published as an npm package with a global CLI, following OpenSpec's model. This is a direction document, not a decision — each step gets its own decision record as we implement it. Co-Authored-By: Kimi K3 <noreply@pi.dev> Generated-By: pi 0.84.2
Function-shaped API, no script paths or exit codes in the surface. Reads are pure; mutations delegate to the bash scripts behind typed results; exec is injectable so pi keeps its async sandbox and OpenCode uses child processes directly. Path resolution is existence-checked. This is Move 1 of docs/plan/typescript-core.md. Co-Authored-By: Kimi K3 <noreply@pi.dev> Generated-By: pi 0.84.2
…-022 dld-core is a TypeScript-native library with a function-shaped API: no script paths, no argv, no exit codes in the surface. - run-api.ts: typed mutations (createRun, addItem, setRunStatus, setItemStatus, repinItem, verifyItem, blockItem, nextItem, activeRun, resumableRun, guardPreconditions, appendRunEvent) over an injectable exec. verifyItem returns pass/fail/infrastructure — a timeout is not a test failure. This is the API the future npm package implements; the bash scripts are the implementation behind it for now. - parse-start-args.ts: the third copy, now the only one, with tests. Range slugs are padded (dl-014-022) — the docstring always claimed this; the code finally agrees. - run-state.ts, render.ts, paths.ts: moved from extensions/dld-run. packageRoot walks up until it finds the scripts directory instead of assuming a depth. The OpenCode plugin deletes ~350 lines of duplicated logic and every unjustified cast; the Pi extension's parser and readers redirect. The widget window now anchors on the current item on both harnesses. All suites green: 104 bun, 305 bats, tsc, tessl lint. Co-Authored-By: Kimi K3 <noreply@pi.dev> Generated-By: pi 0.84.2
…h prompt Co-Authored-By: Kimi K3 <noreply@pi.dev> Generated-By: pi 0.84.2
…ccept DL-023 A real run wedged after item 1: the agent implemented the decision, committed, and asked 'Proceed?' — but never ran the state scripts, so the item stayed implementing and the dispatch guard suppressed re-delivery forever. Two fixes, one decision: 1. Bounded re-delivery: the first suppression re-delivers the item once (the turn may have ended for reasons unrelated to the work); the second surfaces a wedge message and the loop stays quiet. Never two re-deliveries in a row — the anti-spam property holds. Resume clears the budgets so a resumed run starts fresh. 2. Inline protocol: the dispatch prompt carries the state-machine mechanics (set-item-status verifying, add-evidence, never self-accept, block-item for blockers, end the turn without asking) so a run works in projects without the dld-run skill loaded. The skill owns rationale and review; the dispatch needs only the mechanics that advance state. Co-Authored-By: Kimi K3 <noreply@pi.dev> Generated-By: pi 0.84.2
Two CI failures on the OpenCode branch: 1. npm ci failed with ERESOLVE: @opentui/solid@0.5.8 peers solid-js 1.9.12 exactly, and package.json allowed ^1.9.15. Pinned to 1.9.12. 2. The DL-023 commit accidentally deleted the tracked package-lock.json (an rm during a local proxy workaround, swept into git add -A). The repo has used bun.lock since the extension landed; CI now matches local development with bun install --frozen-lockfile instead of npm ci. Co-Authored-By: Kimi K3 <noreply@pi.dev> Generated-By: pi 0.84.2
bun 1.2.23 can't parse lockfileVersion 2. Local development is on 1.4.1; CI matches. Co-Authored-By: Kimi K3 <noreply@pi.dev> Generated-By: pi 0.84.2
1.4.1 doesn't exist as a stable — 1.4.0 is the last non-canary. Co-Authored-By: Kimi K3 <noreply@pi.dev> Generated-By: pi 0.84.2
- typescript ^5.6.0 → ^7.0.2 (the native-compiler line; 5.9.3 was what the old pin resolved to) - target ES2022 → ES2024, lib ES2023 → ES2024 (ES2025 lib isn't in TS 7's lib set yet) - erasableSyntaxOnly: bans enums, namespaces, and parameter properties — everything that requires code generation, which is exactly what breaks source-only loaders (jiti, bun). It immediately caught a real violation: LoopController's constructor parameter property, now an explicit field assignment. All suites green: 104 bun, 305 bats, tsc 7.0.2. Co-Authored-By: Kimi K3 <noreply@pi.dev> Generated-By: pi 0.84.2
jimutt
marked this pull request as ready for review
August 27, 2026 08:47
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
The OpenCode V2 harness integration for dld-run: a server plugin driving the continuation loop and a TUI plugin rendering the status line, sidebar widget, and board overlay. Plus
extensions/dld-core— a TypeScript-native library both harness extensions now share.Why
Validates that the dld-run design is harness-agnostic: the same state layer, scripts, and run contract work on a second harness with a different extension model (client/server, SolidJS TUI). Live-tested end-to-end: full run lifecycle with the four-part completion transaction enforced by the plugin.
Contents
Notable
@opencode-ai/plugin@0.0.0-beta-18269, pinned and documented) — the V2 plugin API is explicitly unstableopencode2 plugin add, synthetic-messageresume: falsecheck