{
  "markdown": "# Ducklab\n\nA self-hosted harness that runs a project's full development cycle with\nseveral LLMs in fixed roles, under test gates that only you sign.\n\n**In one block:** self-hosted development harness (Go engine + CLI + desktop,\nLinux first) · brief → requirements → spec → plan → build → review → release ·\nverdicts are exit codes, never model opinions · local models first (llama.cpp,\nvLLM) beside any OpenAI-compatible or Anthropic endpoint · operable by humans\nor by other agents over **MCP** with recorded, attributed decisions ·\nApache-2.0 · develops itself (the run records in `.ducklab/` are the\nreceipts). Agents: start at [`AGENTS.md`](AGENTS.md) and [`llms.txt`](llms.txt).\n\nYou give it a brief. It writes requirements, a spec and a plan; builds tasks\nwith one model or several arguing; runs your project's real test gate; and\nstops for you before anything is committed. Every model call is logged. No\nmodel ever decides a verdict.\n\n<p align=\"center\">\n  <img src=\"docs/screenshots/council.gif\" alt=\"A live council intake: the architect streams a requirements draft, the reviewer approves, and the run stops at a human gate\" width=\"840\">\n  <br><em>A real council intake, recorded live and sped up: the architect streams the draft, a different model reviews it, the budget ticks in cents — and the run stops at <strong>your</strong> gate. Total cost of what you just watched: $0.07.</em>\n</p>\n\nIt was built for local models first. Two of the seats that built most of it\nare a vLLM box on the LAN and a llama.cpp server on localhost, both priced\nat zero; hosted models sit beside them in the same roster, measured by the\nsame evidence.\n\n## Why this exists\n\nMost agentic coding tools assume one strong model and trust it. Ducklab\nassumes **several cheap models and trusts none of them**:\n\n- **The gate decides, never a model.** A verdict is a command's exit code.\n  A test-first run measures a green **baseline** before any test is written,\n  the red **over the new test** after, and every accept **reproduces the gate\n  from a clean checkout of the committed sha** — nothing lands that did not\n  reproduce, and an accept whose reproduction fails takes its own commit back.\n- **Decorrelation everywhere.** A different model reviews; a reviewer never\n  learns who wrote the code (absent from the payload, not hidden in the UI);\n  tournament judges choose blind; council critics read the draft, not each\n  other.\n- **Work is a contract.** A task's deliverables are the implementer's\n  numbered checklist; it reports on each by number, the reviewer checks each\n  against the diff, and an undelivered item summons the **rubber duck** — an\n  advisor seat that wakes only on measured distress (brake refusals, failure\n  streaks, red gates) and answers `none`, a note that sends the implementer\n  straight back to work, or `stop`.\n- **Seats are chosen on evidence.** Every duckling carries a scorecard —\n  in-seat pass rate from your own runs, cost per run, coding index — and the\n  roster board suggests seats from it, with the ranking criteria yours to\n  reorder. Suggestions are rare and justified: pass rates rank by their\n  Wilson lower bound, three runs minimum, locals never win on a $0 price.\n- **Nothing is unbounded.** Turns, tokens, cost, wallclock, tool output,\n  shell commands — every ceiling visible and liftable mid-run, on the record.\n- **Your documentation is not bounded by the model's window.** Attach a wiki\n  to a stage and a big seat reads it whole; a small seat gets each document\n  digested to fit, the full text one `ref_read` call away, and the gate\n  names any document nobody opened. A 32k local model can be briefed by a\n  quarter-million characters of reference material — the harness carries the\n  working memory.\n\n<p align=\"center\">\n  <img src=\"docs/screenshots/runs.png\" alt=\"The run record: 358 runs with verdicts, costs, and reproduced-green accepts\" width=\"900\">\n  <br><em>The record does not round up: every run with its verdict, its cost, and whether its accept <strong>reproduced green from a clean checkout</strong>.</em>\n</p>\n\nDucklab is developed inside ducklab. The plan, the bugs, the releases and\nthe accepted tasks went through its own loop, driven by the same local and\nhosted models it measures; recent features (per-run worktrees, the\nmerge-proof accept, the acceptance receipts, the governance write guard)\nwere built by the duck and gated by a person. To check the claim yourself:\n\n```bash\ngit clone https://github.com/jrullan/ducklab && cd ducklab\ngo build -o ducklab-cli ./cmd/ducklab\nfor r in .ducklab/runs/*/receipt.json; do ./ducklab-cli proof verify \"$r\"; done\n```\n\nReceipts ship with every accept since v0.7.0: the committed sha, the gate\ncommand, its exit code, and the clean-checkout reproduction verdict —\nfacts a third party re-derives, never assessments.\n\n## Status\n\nv0.7.0 plus the phase-3 work now on main: every build and test run\nexecutes in its own git worktree (your checkout is never touched),\nacceptance rebases the run branch, re-runs the gate on the rebased commit\nand merges fast-forward only, and an operator can re-close a finished run\nas `landed` when its work reached main outside the engine. Before that:\nseven stages, five modes, the roster board with measured scorecards,\nreference documents with automatic digestion, skills managed from the\ndesktop, a seated consultant chat (vision verified before images are\nsent), bug reports with screenshot evidence, adopt surveys with a\ndeterministic coverage check, provider-aware queueing that states why a\nrun waits, escalation suggestions when a seat measurably hits its\nceiling, acceptance receipts (`ducklab proof verify`), releases,\nautopilot, a CLI, a desktop app, and an MCP server — in the\n[official MCP registry](https://registry.modelcontextprotocol.io) as\n`io.github.jrullan/ducklab` — so another model can operate the loop with\nrecorded, attributed decisions.\n\n[`docs/status.md`](docs/status.md) tracks all acceptance criteria and does\nnot round up. Where code and spec differ, the difference is recorded in\n[`docs/decisions/`](docs/decisions/).\n\n## Install\n\nNeeds Go 1.25+, Node 22+ for the desktop, and git.\n\n### Linux\n\nThe CLI and engine are pure Go. The desktop is a Wails v3 app and needs the\nGTK/WebKit development packages:\n\n```bash\nsudo apt install libgtk-3-dev libwebkit2gtk-4.1-dev   # Debian/Ubuntu names\nmake desktop && make install\n```\n\nOn Ubuntu 24.04+ the desktop also needs an AppArmor profile — see\n[decision 0003](docs/decisions/0003-apparmor-userns.md) and\n`packaging/apparmor/`.\n\n### macOS\n\n```bash\nxcode-select --install    # the desktop build links against WebKit\nbrew install go node\nmake desktop && make install\n```\n\nHonesty note: ducklab is developed and exercised daily on Linux. The CLI and\nengine compile-check for `darwin/arm64` on every `make cross`, but no desktop\nbuild has been verified on a Mac yet — the first person to try it is the\ntest, and `make install` gives you the CLI and engine either way. Please\nreport whatever breaks.\n\n### Both\n\n`make install` installs to `~/.local/bin` — make sure it is on your `PATH`.\nIt warns when the desktop binary predates `frontend/src`, because it will\nhappily install a stale one.\n\n## Frontend development without the desktop\n\nTo exercise the frontend in a browser, run the engine and Vite in separate\nterminals, then open the browser with its connection details. The fake engine is\nthe quickest option; the same flow can use a real engine with its opt-in CORS\nflag:\n\n```bash\n# Fast, scripted data (recommended for UI work)\ngo run ./cmd/fake-engine --port 8787 --token fake-token\nnpm run dev --prefix frontend\n# open http://localhost:5173/?engine=http://127.0.0.1:8787&token=fake-token\n\n# Or use real engine data (development only; keep the origin explicit)\ngo run ./cmd/ducklab-engine --allow-origin http://localhost:5173\nnpm run dev --prefix frontend\n# open http://localhost:5173/?engine=http://127.0.0.1:<engine-port>&token=<engine-token>\n```\n\nThe real engine remains same-origin restricted by default. `--allow-origin`\nenables exactly one browser origin and is intended for local frontend development\nand visual audits; it does not change authentication or the loopback bind. Without\nthis flag, a browser's cross-origin failure can look like a dead session.\n\nThe `engine` and `token` query parameters are available only in Vite dev\nbuilds. They can also be supplied as `VITE_DUCKLAB_ENGINE` and\n`VITE_DUCKLAB_TOKEN` environment variables. The desktop shell continues to use\nits injected `window.ducklab` connection.\n\n## Three binaries\n\n| | What it is |\n|---|---|\n| `ducklab-engine` | The daemon. Owns every run. Binds 127.0.0.1 only, bearer token rotated each start. |\n| `ducklab` | The CLI client. Holds no state; it asks the engine. |\n| `ducklab-desktop` | The desktop app. Also a client, also holds no state. Starts (or adopts) the engine itself. |\n\nProvider keys come from the engine's environment at call time — export them\nbefore it starts, or launch the desktop through a wrapper that loads them\nfrom your keyring. The app tells you when the engine it adopted is missing a\nkey this app has, with the restart button beside the words.\n\n## A cycle, end to end\n\nFrom the desktop: **Projects → New project**, then **Cycle → Draft it**. From\na terminal:\n\n```bash\ncd ~/dev/myproject\ngit init                                    # ducklab needs a git repo\nducklab project init --name MyProject       # auto-starts the engine if none is running\n\nducklab intake --from brief.txt             # brief        → requirements\nducklab intent                              # your briefs, verbatim, and what each one changed\nducklab spec                                # requirements → spec\nducklab plan                                # spec         → milestones and tasks\n\nducklab run T-001                           # build it\nducklab run accept r-20260729-...           # commit it\n\nducklab review T-001                        # read the commit\nducklab release plan --bump minor           # what shipped\n```\n\nYour words are part of the record: every brief is kept verbatim as an\n`INT-nnn` entry before any model reads it, and the requirements it added or\nchanged point back to it — so a requirement can always answer \"who asked for\nthis, and in what words\".\n\nEach stage writes a `.proposed` file first and waits for you. `accept`\npromotes it; `reject` restores exactly what the run wrote and nothing else;\n\"request changes\" sends any draft — spec, plan, release notes — back with\nyour note. Nothing is committed without you (or without the autonomy level\nyou explicitly granted).\n\n**Reference documents** ride any stage: `--ref ~/wiki/product/` (or the\nattach door in the desktop) loads files or whole directories as background\nfor the architect — grounded by two rules the prompt states outright: the\napproved requirements own the scope, and where a reference and the code\ndisagree, the code is the truth. When the corpus outgrows the seat's\ncontext, each document is digested once (cached by content hash), the full\ntext stays reachable through the `ref_read` tool, and the proposal card\nlists any document no seat ever opened.\n\n**Adopting an existing codebase** works the same way: intake reads the code\nand writes as-built requirements, the spec marks its sections `as-built`, and\nthe plan stays deliberately empty — new work then enters through bug reports\nand plan amendments, which is how ducklab itself is developed.\n\nYour project declares its own truth in `.ducklab/project.toml`: the gate\n(`[verify]` — with `link_deps` and `setup` for what a clean checkout needs),\nhow the app launches (`[run]` with a preflight), and how the project's own\nbinaries are rebuilt (`[install]`) so the whole loop runs without leaving\nducklab.\n\nGate and shell process trees always receive `DUCKLAB_RUN_ID` and `DUCKLAB_PROJECT_ID`. For example, excercise-tracker can use `DATABASE_URL=test_db_${DUCKLAB_RUN_ID}` in `[verify].tests`, and a compose preflight can use `${DUCKLAB_PROJECT_ID}` as its per-run project name. Ducklab guarantees identity only; provisioning and teardown remain the project's.\n\n## Adding a model\n\n```bash\nducklab provider set openrouter --url https://openrouter.ai/api/v1 \\\n                                --key-env OPENROUTER_API_KEY\nducklab duckling set pato-sonnet --provider openrouter \\\n                                 --model anthropic/claude-sonnet-4.5 \\\n                                 --roles reviewer,judge --context 200000 \\\n                                 --cost-in 3.0 --cost-out 15.0\nducklab duckling test pato-sonnet --prompt \"say OK\"\n```\n\n`--key-env` is the **name** of an environment variable, never a key. No key\nis written to config, sent over the API, or kept in shell history.\n\n<p align=\"center\">\n  <img src=\"docs/screenshots/roster.png\" alt=\"The roster board: flock with scorecards, seats per mode, engine suggestions\" width=\"900\">\n  <br><em>Seat suggestions come with their evidence: pass rates from your own runs, cost per run, coding index. You decide.</em>\n</p>\n\nThe desktop's **Roster** view is where seats are assigned: drag from the\nFlock onto a mode's seat, globally or per project, with each duckling's\nevidence on the card and the engine's suggestions beside the seats. Coding /\nintelligence / agentic indices come from OpenRouter's benchmarks endpoint\nwhen a duckling lives there; your own runs supply the rest.\n\n### The fleet that built this repo\n\nThis is not a recommendation list. It is this repository's own run record\n(454 recorded runs, ~2,300 seat assignments as of 2026-08-24), so you can\nsee what actually held which seat. Any OpenAI-compatible endpoint slots in\nthe same way.\n\n| Duckling | Model | Served by | Seats held | What the record says |\n|---|---|---|---|---|\n| `beelink-local` | Qwen3.6-35B-A3B (Q4 GGUF) | llama.cpp (Vulkan) on a Ryzen AI Max 395, on-desk | 465 (the most-seated duckling in this repo) | judge, scribe, reviewer. Free. |\n| `luna` | gpt-5.6-luna | OpenRouter | 455 | implementer workhorse: 77% measured pass rate at ~$0.02/run. |\n| `atom-local` | Qwen3.8-27B | vLLM on a DGX Spark on the LAN | 352 | architect and scribe; it wrote the release notes. Free. |\n| `k3` | Kimi K3 | OpenRouter | 348 | triage, architecture drafts, question advisor. |\n| `terra` | gpt-5.6-terra | OpenRouter | 303 | the heavier implementer, ~$0.28/run. |\n| `glm52` | GLM-5.2 | OpenRouter | 160 | the reviewer seat: 81% measured over 261 reviews. |\n| `qwen38-max` | Qwen3.8-Max | OpenRouter | 140 | the advisor (the rubber duck). 88% measured. |\n| `pato-sonnet` | Claude Sonnet 4.5 | OpenRouter | 7 | the expensive seat, used when cheaper ones measurably hit a ceiling. |\n\nTwo notes for accuracy. First, \"built with local models\" here means the\nlocal seats held judgment and documentation roles (judge, reviewer,\nscribe, architect) while cheap hosted models did most of the typing;\nabout a third of all seat assignments ran on hardware in this room.\nSecond, the pass rates above are measured on my runs (`ducklab duckling\nscorecard`, Wilson lower bound). Yours will differ, and that is the\npoint: the roster works from your record, not from a leaderboard.\n\n<p align=\"center\">\n  <img src=\"docs/screenshots/council-run.png\" alt=\"A council run mid-flight on ducklab's own spec\" width=\"900\">\n  <br><em>The same machinery on real work: a council revising ducklab's own spec, 4.5M tokens in, paused once on a budget it asked to lift.</em>\n</p>\n\n## The five modes\n\n`ducklab run T-001 --mode <mode>`\n\n| Mode | What it does |\n|---|---|\n| `solo` | One duckling. The yardstick everything else is measured against. |\n| `pair` | Implementer and reviewer, decorrelated. Between them the advisor — the rubber duck. |\n| `tournament` | Contestants build the same task in isolated worktrees; a judge picks, blind. |\n| `split` | An architect decomposes; subtasks run in parallel; integration is file copies, no model involved. |\n| `council` | Several models on one document, for intake, spec, plan and review. One drafts, the others critique blind, the first revises. |\n\n## What it will not do\n\nInvariants, enforced in code:\n\n- A model never decides a verdict. A gate is a command's exit code.\n- A green candidate is applied byte-for-byte; nothing is re-generated\n  after it passed.\n- A reviewer never learns who wrote the code.\n- Nothing lands that did not reproduce from a clean checkout of the\n  commit being merged.\n- A reject undoes what the run wrote, and nobody else's work.\n- Every budget (tokens, cost, turns, wallclock) has a ceiling you can see.\n- Secrets never touch project state.\n- The engine is loopback-only. There is no remote mode.\n\n## Skills\n\nA skill is a directory with a `SKILL.md` — under `.ducklab/skills/` for one\nproject, or in the machine-wide skills directory to serve every project\n(project shadows global on a name collision). The documentation-only form\nhas no script and is the default: a recipe a model reads and follows. The\narchitect reads survey guides before an adopt (`skill_list` is in its\nprompt), the consultant reads them in chat, and only the implementer can\n`skill_run` an executable one.\n\nSkills are administered from the desktop (**gear → Skills**): list with\nscope badges and validation problems, read, edit the whole `SKILL.md`,\nrun with arguments, delete. A skill a duckling writes during a run shows\nthere greyed `pending acceptance` until its run is accepted — proposing a\nskill goes through the same gate as proposing code.\n\n```bash\nducklab skill new house-style\nducklab skill run changelog-entry --arg summary=\"...\"\n```\n\n## The consultant\n\nEvery project seats a **consultant** (a Common seat on the roster board):\nthe model behind the \"chat about this\" doors and the free-form chat in the\nguide rail. It reads the code, the runs, the boards and the skills — never\nwrites — and takes **images**: paste a screenshot of a broken view and ask.\nVision is verified, not assumed: a declared-vision seat is probed with a\nreal image request once, and a text-only seat refuses the paste with words\ninstead of hallucinating an answer.\n\n<p align=\"center\">\n  <img src=\"docs/screenshots/consultant-chat.png\" alt=\"The consultant chat: luna answering 'convince me to use Ducklab'\" width=\"900\">\n  <br><em>The seated consultant answering a question about the repo it reads.</em>\n</p>\n\n## Operating ducklab from another model\n\n`ducklab mcp serve` exposes the whole loop over stdio as an MCP server: an\nexternal model reads each result, decides gates (with a required, recorded\nreason — decisions land as `approved_by: mcp:<client>`, never as \"human\"),\nanswers questions, files bugs, amends plans and starts work. The engine's\n`next` lists are the law: an operator cannot take an action a person could\nnot.\n\n## Contributing\n\nSee [CONTRIBUTING.md](CONTRIBUTING.md) — how to build, how the tests guard\nthe architecture, how work flows through ducklab's own loop, and where to\nstart. The short version:\n\n```bash\nmake            # vet, test, build the frontend\ngo test ./...   # 39 packages\ncd frontend && npx vitest run\n```\n\n**License:** [Apache-2.0](LICENSE). Contributions are accepted under the\nsame terms (§5 of the license — no CLA). The Ducklab name and the duck are\nthe maintainer's (§6).\n\n## Specification\n\nThe code implements a written specification, in this repo:\n[`docs/spec/`](docs/spec/) (00-VISION through 08-DESKTOP-UI) is the\n**normative** layer — vision, invariants, protocol contracts, acceptance\ncriteria. What the system IS today lives in `.ducklab/docs/` — the as-built\nrequirements, spec and plan the loop itself maintains, each version signed\nat a human gate. Where the two differ deliberately, the difference is\nrecorded in [`docs/decisions/`](docs/decisions/); the diff between them is\nthe roadmap, and the alignment stage computes it.\n",
  "bytes": 19734,
  "sha": "ccec65c23598b3544cbdb4f6933b2a0cf68e77f2d0cb4f627aae9fd549bb0ab0",
  "repo_slug": "jrullan/ducklab",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_jrullan_ducklab_8e49bcf5/readme"
}