Homelab MCP starter
by sailfact
README.md
# dspec
**DSpark-inspired speculative draft-then-verify for Claude Code.** A cheap draft model attempts a task, an independent confidence gate scores it, weak drafts are discarded before the target model ever sees them, and the target model verifies-and-patches only the drafts that survive — spending expensive tokens only on the parts that actually diverge.
---
## Read this first: what dspec is *not*
**dspec is a DSpark-*inspired* workflow optimization, not a reimplementation of speculative decoding, and it is not lossless.**
Real [DSpark](https://arxiv.org/) verifies draft tokens against the target model's probability distribution via rejection sampling, which guarantees output identical to target-only decoding. That requires logit access *inside* the inference engine. dspec runs at the API/CLI layer, where no logits exist. Here the target model **judges** a finished draft against a rubric; it does **not** verify it against logits.
Concretely:
- Output is **approximate-to-target**, not distribution-exact.
- The result is **not guaranteed identical** to what the target model would have produced on its own.
- The documentation, this README included, will never claim otherwise.
What you get instead is a *measurable bet*: when the gate is well-calibrated, mechanical work is drafted cheaply and rubber-stamped, while genuinely hard work falls through to normal full-quality execution. The telemetry (`/spec-stats`) tells you whether that bet is paying off. If the gate can't separate good drafts from bad, the whole scheme is noise — and the calibration metric is designed to make that failure obvious rather than silent.
---
## How it works
```
task ──▶ draft model (cheap) ──▶ confidence gate (independent) ──▶ threshold
│
confidence ≥ threshold │ confidence < threshold
▼ │ ▼
target model verifies │ discard draft;
& patches only the delta │ target does the
│ task normally
└──────────────┴──▶ record outcome + telemetry
```
The pipeline maps onto DSpark's structure as follows:
| DSpark concept | dspec implementation |
|---|---|
| Draft model | `claude -p --model haiku` subprocess producing a full candidate deliverable |
| Trained per-token confidence head | A second, independent `haiku` call scoring the whole draft 0–100 against a rubric |
| Discard low-confidence drafts pre-verification | Server-side threshold comparison; discarded drafts never reach the target model |
| Target verification (rejection sampling) | Target model verify-and-patch prompt: accept verbatim unless demonstrably wrong |
| Acceptance rate / accepted length | JSONL telemetry: accepted / patched / rejected / discarded + gate calibration |
| Fall back to normal decoding | Target model does the task itself whenever speculation is discarded, errors, or times out |
**Fail-open is a hard rule.** Any error — CLI missing, timeout, non-zero exit, unparseable gate output — short-circuits to `decision: "discard"` with the error recorded, and the task falls through to normal execution. Degradation means losing the *speedup*, never the *answer*. The server never blocks your task.
---
## Requirements
- **Rust** (2021 edition) and `cargo` — to build the MCP server.
- The **`claude` CLI**, installed and authenticated. dspec shells out to `claude -p` subprocesses, so it rides your existing Claude Code auth; there are no separate API keys to configure.
- **Claude Code**, to load the plugin.
---
## Install
dspec ships as source; you build the server once, then load the directory as a local plugin. Prebuilt binaries and build-on-install hooks are out of scope for v1.
**1. Build the server:**
```bash
cd dspec/server
cargo build --release
# binary lands at dspec/server/target/release/dspec-server
```
The plugin manifest points at `${CLAUDE_PLUGIN_ROOT}/server/target/release/dspec-server`, so the release binary must exist before the plugin will start.
**2. Install as a local plugin.**
The local-plugin loading mechanism has changed across Claude Code releases, so don't guess it — check the current help:
```bash
claude plugin --help
claude plugin validate . # run from the dspec/ plugin root
```
Install `dspec/` as a local plugin per whatever your version documents, then restart Claude Code.
**3. Verify:**
- `/spec` appears in the slash-command list.
- `/mcp` shows the `dspec` server connected with three tools: `draft_task`, `record_outcome`, `spec_stats`.
---
## Usage
### `/spec <task>`
Runs the full speculative pipeline on a task.
```
/spec write a conventional commit message for: renamed telemetry field ts to ts_ms across the dspec server
```
What happens:
1. The command assembles **minimal** context (a hard budget of ~200 lines — context transfer is the hidden cost of speculation).
2. `draft_task` drafts and gates the work server-side.
3. If the decision is **discard**, the target model just does the task normally at full quality and records the outcome as `discarded`.
4. If the decision is **verify**, the target model applies *verify-and-patch discipline*: accept the draft verbatim unless a span is demonstrably wrong, patch only the divergent spans, and never restyle acceptable content.
5. A one-line status is always printed:
```
spec: <decision> conf=<confidence> outcome=<outcome> draft=<draft_ms>ms gate=<gate_ms>ms
```
### `/spec-eval [tasks | clean]`
Batch-runs the calibration eval in [`eval/`](eval/) through the `/spec`
pipeline: `/spec-eval` runs every task, `/spec-eval 1`, `/spec-eval 3 2`, and
`/spec-eval 3-7` run a selection (in the order given), and `/spec-eval clean`
wipes the telemetry log (`events.jsonl` and live logs in `DSPEC_DATA_DIR`) so a
run starts from zero drafts. Each task is graded against its file's grading
notes and recorded via `record_outcome`, then the run ends with a per-task
table and the `/spec-stats` calibration verdict.
### `/spec-stats`
Reports the telemetry conversationally: total drafts, per-outcome counts, verify-path acceptance rate, mean patch ratio, mean draft/gate latency, and — most importantly — **gate calibration**: the mean gate confidence of drafts that ended accepted/patched versus those that ended rejected. If those two numbers aren't separated by a comfortable margin, the gate isn't predictive and the threshold is meaningless.
---
## The MCP tools
The server exposes three tools; the slash commands orchestrate them, but they're documented here for completeness.
- **`draft_task(task, context?)`** — the core pipeline. Returns JSON: `draft_id`, `decision` (`verify`/`discard`), `confidence`, `reasons`, `draft`, `draft_ms`, `gate_ms`, `error`.
- **`record_outcome(draft_id, outcome, patch_ratio?)`** — appends the final outcome for a speculation.
- **`spec_stats()`** — aggregates the telemetry, joining draft events to outcome events by `draft_id`.
### Outcome vocabulary
Exactly four values, no others:
| Outcome | Meaning |
|---|---|
| `accepted` | Draft used verbatim |
| `patched` | Draft used with targeted edits; include `patch_ratio` (0.0–1.0, fraction of the draft changed) |
| `rejected` | Target regenerated the work from scratch |
| `discarded` | Gate or an error killed the draft; the target did the task normally |
---
## Configuration
All configuration is via environment variables set in the plugin manifest's MCP server entry, each with a default:
| Variable | Default | Meaning |
|---|---|---|
| `DSPEC_DRAFT_MODEL` | `haiku` | Model alias passed to `claude --model` for drafting |
| `DSPEC_GATE_MODEL` | `haiku` | Model for the gate pass |
| `DSPEC_THRESHOLD` | `60` | Minimum confidence (inclusive) to reach verification |
| `DSPEC_TIMEOUT_SECS` | `120` | Per-subprocess timeout, in seconds |
| `DSPEC_DATA_DIR` | `~/.dspec` | Telemetry location |
| `DSPEC_CLAUDE_BIN` | `claude` | CLI binary; overridden in tests to a mock script |
Unparseable numeric values (e.g. a non-numeric `DSPEC_THRESHOLD`) silently fall back to the default rather than failing.
---
## Telemetry
One JSON object per line, appended to `<DSPEC_DATA_DIR>/events.jsonl` (default `~/.dspec/events.jsonl`). Two event shapes — `draft` and `outcome` — joined by `id`. `spec_stats` reads this file; corrupt lines are skipped rather than causing stats to fail, and telemetry write failures are logged to stderr and swallowed so stats can degrade without ever taking down a task.
---
## Development & testing
```bash
cd server
cargo test -- --test-threads=1
```
The `--test-threads=1` flag matters: a few tests in the `claude_cli` and `server` suites mutate process-level environment variables (`MOCK_MODE`) to drive the mock `claude` fixture, so they must not run concurrently. The subprocess wrapper uses `kill_on_drop(true)`, so a timed-out speculation cannot leak a running `claude` process.
The test suite covers config defaults/overrides, gate JSON extraction (clean, prose-wrapped, garbage, out-of-range), telemetry append/join/calibration math, the CLI wrapper against a mock shell fixture (success / non-zero exit / timeout), the prompts, and the fail-open pipeline paths.
---
## Calibration eval
Gate calibration is the make-or-break metric, so dspec ships a **self-contained**
eval for it in [`eval/`](eval/). It runs a spread of tasks — from mechanical
(should gate high, end accepted/patched) to novel design (should gate low, end
discarded/rejected) — and checks that `mean_confidence_good` separates cleanly
from `mean_confidence_bad` by **≥10 points**.
Every task under [`eval/tasks/`](eval/tasks/) embeds its full `/spec` prompt *and
all input material* (the source to document, the JSON to convert, the diff, the
README to summarize, the function to test), so a run is reproducible on any
checkout and doesn't drift as the code changes. See [`eval/README.md`](eval/README.md)
for how to run it and [`eval/RESULTS.md`](eval/RESULTS.md) for the results
template. If calibration fails, iterate the gate rubric wording in `prompts.rs`
(a single constant, no structural change) and re-run.
---
## License
_TBD — add your license of choice._This server cannot be deployed
Maintenance
ActivityStale
ResponsivenessNo issues