{
  "markdown": "# rebuild-dossier\n\n[![DOI](https://zenodo.org/badge/1308271634.svg)](https://doi.org/10.5281/zenodo.22036801)\n[![arXiv:2608.23616](https://img.shields.io/badge/arXiv-2608.23616-b31b1b.svg)](https://arxiv.org/abs/2608.23616)\n[![smithery badge](https://smithery.ai/badge/parkerscottfawcett/rebuild-dossier)](https://smithery.ai/servers/parkerscottfawcett/rebuild-dossier)\n[![M8ven Score](https://m8ven.ai/badge/mcp/businessfawcett-cloud-rebuild-dossier-17tqsh)](https://m8ven.ai/mcp/businessfawcett-cloud-rebuild-dossier-17tqsh)\n[![CI](https://github.com/Parker-Fawcett/rebuild-dossier/actions/workflows/ci.yml/badge.svg)](https://github.com/Parker-Fawcett/rebuild-dossier/actions/workflows/ci.yml)\n\nAn MCP server that reverse-engineers a trustworthy **rebuild spec** — a locked `CLAUDE.md`,\n`.claude/` config, and a mutation-tested test suite — out of an existing app, so any coding\nagent can rebuild it cleanly against that spec instead of guessing.\n\n**It does not rebuild the app.** It produces the spec, contracts, and tests a coding agent\nconsumes to do that separately. This boundary is deliberate — see [Why](#why) below.\n\n> **Validated against a real app.** The core loop (ingest, resolve cases, generate spec) has been\n> tested end-to-end against a real, messy repo with two independent fresh-agent handoffs on two\n> model tiers, plus a mutation-tested test suite.\n> [docs/v0-findings.md](docs/v0-findings.md) covers what worked, what broke, and what's still open.\n\n## Why\n\nPrior research ([AgentModernize, arXiv:2605.17535](https://arxiv.org/abs/2605.17535)) found\nthat a rebuild pipeline scores **0%** behavioral equivalence with no verified feedback loop,\nand only **9–19%** with a coarse one. The bet behind this tool: locking interface contracts\n*before* running tests, plus a strict one-test-at-a-time retry loop instead of batch\nregeneration, does meaningfully better.\n\nThe riskiest part of any such pipeline is silently validating a bug as intentional — four\nsources of evidence can quietly agree on the same mistake with nobody ever having said why.\nSo the single non-negotiable rule in this tool: **auto-resolving an ambiguity requires both\nsignal agreement *and* an affirmative signal that someone actually decided** (a stated\ncomment, a TODO admitting a bug, or a direct human answer). Silent agreement alone — code and\nobserved behavior simply matching, with no one ever having said why — always becomes a\nquestion, never an auto-resolution, no matter how high the apparent confidence.\n\n## How it works\n\nSix MCP tools, run from inside a normal Claude Code (or any MCP-compatible) session:\n\n| Tool | What it does |\n|---|---|\n| `ingest_repo(path)` | Static analysis only, no LLM call: routes, `package.json`, build config (via AST, never executed), existing tests, and structural-smell detectors (e.g. a client-side-only credential check with no server-side verification) that surface real ambiguity even when nobody ever commented on it. |\n| `crawl_site(url)` | Headless Playwright crawl of reachable routes, with progress notifications so long crawls don't get killed as unresponsive. |\n| `flag_known_bug(description)` | Free text, stored verbatim. Always overrides auto-resolve for anything it matches — the cheapest, most authoritative signal in the system. |\n| `get_case_queue()` / `resolve_case(id, decision)` | The ambiguity queue. Surfaces open questions via MCP elicitation when the client supports it; `resolve_case` is always available as a scripted fallback. |\n| `generate_spec()` | Only callable once the case queue is empty. Writes `CLAUDE.md`, `.claude/rules/`, `.claude/settings.json` (hooks that *mechanically* enforce the discipline — see below), `spec/contracts/*.md`, `tests/visible/` + `tests/held-out/`, and `kickoff-prompt.txt` to a clean sibling `<repo>-rebuild/` directory — never into the original repo. Runs a real mutation check before finalizing tests: deliberately breaks the original code and confirms each generated test actually catches it, downgrading any that don't. |\n\n`crawl_site` needs Chromium (`npx playwright install chromium`, step 2 below) — it isn't bundled with the server, including when installed via Smithery, so run it once first or the tool will fail.\n\n### Rails that are mechanically enforced, not just written down\n\nA comparison run across two model tiers found that a weaker model will happily read\n`CLAUDE.md`, understand \"only build what's currently failing, don't batch-regenerate,\" and\nthen quietly violate it anyway — because nothing *checked* it. Two rules in this tool are now\nenforced by real hooks, not prose, for exactly that reason:\n\n- **`spec/` is locked.** A `PreToolUse` hook blocks any edit under `spec/`.\n- **Contracts without tests don't get built ahead of schedule.** `generate_spec` writes\n  `spec/untested-contracts.json` (every route/contract with no covering test), and a second\n  `PreToolUse` hook blocks writes to anything on that list — the same enforcement shape as the\n  `spec/`-edit block, closing a gap that used to be advisory only.\n\nA `PostToolUse` hook runs the visible test suite after every edit.\n\n![rebuild-dossier demo](demo.gif)\n\n## Quick start\n\nAvailable on npm:\n\n```bash\nnpx rebuild-dossier@latest --help    # pull the MCP server (stdio), or:\nnpm install -g rebuild-dossier        # install the CLI globally\n```\n\nAlso available via Homebrew:\n\n```bash\nbrew install rebuild-dossier\n```\n\nRequires **Node 20.12+** (set in `package.json` `engines`). To run from source instead, clone the\nrepo, `npm install`, and use `npm start`.\n\nThen add it as an MCP server. In Claude Code, from the project you want to rebuild:\n\n```bash\nclaude mcp add rebuild-dossier -- npx -y rebuild-dossier@latest\n```\n\n(or add this to your `~/.claude.json` / project `.mcp.json`):\n\n```json\n{\n  \"mcpServers\": {\n    \"rebuild-dossier\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"rebuild-dossier@latest\"]\n    }\n  }\n}\n```\n\nThen in a session. The first call, `ingest_repo`, runs instantly with zero setup: static\nanalysis only, no browser, no Chromium, no LLM call. Run it on any app to confirm the server\nis alive before committing to the full workflow:\n\n```\ningest_repo({ path: \"/path/to/some-app\" })\nget_case_queue({ repoPath: \"/path/to/some-app\", interactive: true })\n# ...resolve whatever the queue surfaces...\ngenerate_spec({ repoPath: \"/path/to/some-app\" })\n```\n\nThis writes a clean `some-app-rebuild/` sibling directory. `cd` into it, start a **fresh**\nClaude Code session (nothing else should be in scope), and paste the contents of its\n`kickoff-prompt.txt`.\n\nIf this looks useful, a star helps other developers find it.\n\n## Operating guide\n\nThe full lifecycle, in order — each step's actual behavior, not just the call signature.\n\n### 1. Ingest the repo\n\n```\ningest_repo({ path: \"/absolute/path/to/some-app\" })\n```\n\nStatic analysis only — no LLM call, nothing executed. Parses `package.json`, route files\n(Express and Next.js App Router today — see [scope](#current-scope-and-whats-deliberately-not-built-yet)),\nbuild config (Tailwind/Vite/Next, via AST, never executed), existing tests, and scans for\ncomment/TODO signals plus structural smells (e.g. a hardcoded client-side credential check with\nno server-side verification — the kind of thing nobody ever comments on, which is exactly why\nit needs its own detector rather than relying on comments existing). Everything lands in\n`<repo>/.dossier/` — this tool's own scratch state, inside the *original* repo, never shared or\nuploaded anywhere. You'll get back a summary:\n\n```json\n{\n  \"routes\": 8,\n  \"existingTests\": 0,\n  \"signals\": 3,\n  \"buildConfig\": [\"tailwind\", \"next\"],\n  \"openCases\": 3,\n  \"savedTo\": \"/absolute/path/to/some-app/.dossier/evidence.json\"\n}\n```\n\n`openCases` here already reflects reconciliation — comment/TODO signals and structural smells\nthat didn't auto-resolve become case-queue entries automatically.\n\nIf `routes` comes back `0`, check for a `monorepoHint` field before assuming the app has none —\n`ingest_repo` needs to be pointed at the actual app directory, not a monorepo's root wrapper\n(a `package.json` with `apps/*`/`packages/*` next to it, common with Turborepo/Nx/workspace\nlayouts, including ones that never actually declare a `workspaces` field). The hint lists real\ncandidate directories found under `apps/`/`packages/` so you don't have to hunt for the real app\nyourself — re-run `ingest_repo` pointed at one of those instead.\n\nIf your client supports MCP elicitation, you can skip the manual re-run entirely: pass\n`interactive: true` and, when a monorepo root with candidates is detected, `ingest_repo` asks\nwhich one is the real app and ingests it directly — it never silently guesses on its own, the\nsame way `get_case_queue`'s interactive mode always asks rather than resolving anything without\nyou. Declining, an unsupported client, or an answer that isn't one of the real candidates all\nfall back to the plain hint above, unchanged.\n\n### 2. (Optional) Crawl the live site\n\n```\ncrawl_site({ url: \"http://localhost:3000\", repoPath: \"/absolute/path/to/some-app\" })\n```\n\nOnly useful if the app is actually running somewhere. Headless Playwright crawl of reachable\nroutes, emitting progress notifications periodically — long crawls get auto-backgrounded by\nmost MCP clients, and a silent multi-minute call risks being killed as unresponsive without them.\n\nThis step is the only one that needs a browser. `crawl_site` drives headless Chromium, which\nisn't bundled with the server (including via Smithery), so run `npx playwright install chromium`\nonce first or the tool will fail. Skip it and the rest of the workflow still works. `ingest_repo`,\n`resolve_case`, and `generate_spec` need no browser at all.\n\n### 3. (Optional, but do this before step 4) Flag anything you already know is broken\n\n```\nflag_known_bug({\n  repoPath: \"/absolute/path/to/some-app\",\n  description: \"The login gate secret check runs entirely client-side and is bypassable\"\n})\n```\n\nThe cheapest, most authoritative signal in the whole system — a direct human statement always\noutranks inference. It overrides auto-resolve for anything it matches, *even if* every other\nsignal quietly agrees the behavior looks intentional. Do this before resolving the queue, since\nit changes what shows up there (and can seed a case entirely on its own, with zero other\nevidence — see [docs/v0-findings.md](docs/v0-findings.md) for why that matters).\n\nMatching is plain token overlap against each open case's file path and claim text, not fuzzy or\nsemantic — so one bug description can match (and auto-resolve) more open cases than you intended\nif your codebase has several similarly-named components. In the validated example, one bug about\n\"the login gate\" matched and closed all three of Madeline's near-duplicate gate components in a\nsingle call, before any of them were reviewed individually. `resolve_case` overwrites a case's\ndecision regardless of its current status, so if that's not what you meant, call it directly on\nthe ones it swept up too broadly — don't assume every case it touched was actually the same\ndecision.\n\n### 4. Resolve the case queue\n\n```\nget_case_queue({ repoPath: \"/absolute/path/to/some-app\", interactive: true })\n```\n\n`interactive: true` walks each open case via MCP elicitation — a real interactive prompt in\nyour client, showing the evidence side by side, if your client supports it. If not (or you're\nscripting this), resolve cases one at a time instead:\n\n```\nresolve_case({ repoPath: \"/absolute/path/to/some-app\", id: \"case:...\", decision: \"intentional\", note: \"...\" })\n```\n\n**This step doesn't have a shortcut.** `generate_spec` refuses to run while any case is still\nopen, by design — there's no partial or in-progress spec to hand a rebuild agent with caveats;\nphases 1–2 are literally what produce `spec/` in the first place.\n\n### 5. Generate the spec\n\n```\ngenerate_spec({ repoPath: \"/absolute/path/to/some-app\" })\n```\n\nOnly callable once the queue is empty. Writes `CLAUDE.md`, `.claude/` (rules, hooks, a\nspec-auditor subagent, and a verify-against-spec skill — all derived from *this* project's actual\ncontracts and tests, not boilerplate), `spec/` (contracts, locked decisions,\n`test-dependencies.json`, `untested-contracts.json`), and `tests/` to a clean sibling\n`some-app-rebuild/` directory — never into the original repo. Two more `.claude/` artifacts are\ngenerated only when they'd earn their keep: a `test-verifier` subagent, only if there are\nheld-out tests to guard; a `parallel-test-fix` workflow, only if the generated tests split into\ntwo or more independent clusters (by shared route files) worth fixing concurrently. A small app\nwith a couple of tests covering the same routes — like the validated example above — gets\nneither; that's not a bug, it's the generator refusing to hand a rebuild agent tooling it has\nnothing real to do with. This step also runs a real mutation check: it deliberately breaks\nthe original code (flips a comparison, drops a null check, off-by-ones a loop bound) in a\nscratch copy and confirms each generated test actually catches it — anything that doesn't gets\nmoved to `tests/weak/` instead of shipped as if it were trustworthy. You'll get back:\n\n```json\n{\n  \"outputDir\": \"/absolute/path/to/some-app-rebuild\",\n  \"mutationsChecked\": 8,\n  \"weakTests\": [],\n  \"unrunnableTests\": []\n}\n```\n\nBoth `weakTests` and `unrunnableTests` land in the same `tests/weak/` directory instead of\n`tests/visible/`, but for different reasons worth telling apart: a weak test ran fine and just\nnever caught anything a mutation broke; an unrunnable test never passed even against the\noriginal, unmutated code (a broken import, a missing environment variable, infrastructure the\nbare repo doesn't have) — before this distinction existed, an unrunnable test looked\nindistinguishable from a 100%-effective one, since it \"fails\" identically whether or not the\ncode under test was mutated. Neither is an error — it's the tool telling you honestly that a\nspecific test didn't earn its place in `tests/visible/`, and why.\n\nIf every generated test lands in `tests/weak/` with `mutationsChecked: 0`, check for a `warning`\nfield before assuming something's structurally wrong — the far more common cause is that the\ntarget repo hasn't had `npm install` run in it, so the mutation-check scratch copy has none of\nthe target's own real dependencies (`next`, `@prisma/client`, whatever the app actually needs)\nand every generated test fails to even import them. `generate_spec` checks for this directly and\nsays so, rather than leaving you to debug a confusing all-unrunnable result.\n\n#### Optional: vision-assisted page-content classification\n\nFor a Next.js target, page routes get real Playwright-captured tests (a screenshot plus DOM-text\nassertions) alongside the API-route tests described above. Whether a piece of captured text gets\nan exact-match assertion (`static`) or a loose shape check (`dynamic`) is decided by a small\nregex classifier by default — reliable most of the time, but confirmed capable of getting it\nbackwards in both directions on a real app (a hardcoded dropdown legend read as live data; a\nlive, comma-formatted database count read as fixed).\n\nSetting **both** `GROQ_API_KEY` and `REBUILD_DOSSIER_ENABLE_VISION_CLASSIFICATION=1` before\ncalling `generate_spec` sends each captured page's screenshot and (secret-redacted) source code\nto a Groq vision model instead, which can see *where* a value actually comes from — a literal\narray in the source vs. a `fetch`/`useState` call — rather than only guessing from what the\nrendered string looks like. Both variables are required together on purpose: an ambient\n`GROQ_API_KEY` left over from some unrelated tool must never silently start sending this target\nrepo's code to a third party. Neither variable set (the default) means zero behavior change and\nzero network calls beyond what `generate_spec` already does.\n\nThis is real added cost, not free: one Groq API call per captured page, plus a deliberate ~20s\npacing delay between pages (Groq's free tier has a tight per-minute token budget, and firing\nrequests back to back exhausts it fast) — `generate_spec`'s own response states the exact added\ntime for that run. A page that can't be classified this way for any reason (rate limit, network\nissue, an invalid response) falls back to the regex classifier for that page only, reported in\n`pageVisionFallbacks` — never a silent gap or a failed run. Groq's free tier (no credit card\nrequired, at [console.groq.com](https://console.groq.com)) is enough to try this.\n\n### 6. Hand it off\n\n```bash\ncd /absolute/path/to/some-app-rebuild\nclaude   # or oh-my-pi, opencode — any coding agent, a genuinely fresh session\n```\n\nPaste the contents of `kickoff-prompt.txt` verbatim. Nothing else should be in that session's\ncontext — the directory is fully self-contained on purpose (see [How it works](#how-it-works)),\nso there's nothing else for a rebuild agent to read, drift toward, or edit in place instead of\nbuilding cleanly. Read [docs/v0-findings.md](docs/v0-findings.md) for what actually happens\nwhen you do this against a real app, including exactly where it got stuck.\n\n## Connecting from other tools (oh-my-pi, opencode, etc.)\n\nTwo ways to run this, both entirely local — there is no hosted/shared instance, and none is\nrequired:\n\n**stdio (default)** — each tool spawns its own copy of the server as a local subprocess. This\nis the standard way every MCP client (Claude Code, [oh-my-pi](https://github.com/can1357/oh-my-pi),\n[opencode](https://opencode.ai)) adds a local MCP server — point it at `npx tsx src/index.ts`\n(or a built `node dist/index.js`) from this repo's directory. No extra setup, no auth, nothing\nin this section applies.\n\n**HTTP (optional)** — one persistent server on `localhost` that multiple tools/sessions\nconnect to instead of each spawning their own. Useful if you want oh-my-pi and opencode (or\nseveral Claude Code sessions) sharing one running instance. Still fully local — `MCP_ALLOWED_HOSTS`\nonly needs to include the hostname you'll actually connect to (`localhost`), not a real domain,\nunless you deliberately choose to expose this beyond your own machine.\n\n```bash\nnpm run build\nPORT=8080 \\\nMCP_AUTH_TOKEN=$(openssl rand -hex 32) \\\nMCP_ALLOWED_HOSTS=localhost,127.0.0.1 \\\nREBUILD_DOSSIER_ALLOWED_PATHS=/absolute/path/to/your/projects \\\nnpm run start:http:prod\n```\n\nAll three env vars are required — the server refuses to start without them, on purpose:\n`MCP_AUTH_TOKEN` gates every `/mcp` request (bearer auth), `MCP_ALLOWED_HOSTS` guards against\nDNS-rebinding, and `REBUILD_DOSSIER_ALLOWED_PATHS` (comma-separated absolute directories) is\nthe only paths `ingest_repo`/`generate_spec`/etc. are allowed to touch — set it to whatever\nparent directory holds the repos you actually want to rebuild.\n\n**oh-my-pi** (`.omp/mcp.json` or `~/.omp/agent/mcp.json`):\n\n```json\n{\n  \"mcpServers\": {\n    \"rebuild-dossier\": {\n      \"type\": \"http\",\n      \"url\": \"http://localhost:8080/mcp\",\n      \"headers\": { \"Authorization\": \"Bearer ${REBUILD_DOSSIER_TOKEN}\" }\n    }\n  }\n}\n```\n\n**opencode** (`opencode.json`):\n\n```json\n{\n  \"$schema\": \"https://opencode.ai/config.json\",\n  \"mcp\": {\n    \"rebuild-dossier\": {\n      \"type\": \"remote\",\n      \"url\": \"http://localhost:8080/mcp\",\n      \"enabled\": true,\n      \"oauth\": false,\n      \"headers\": { \"Authorization\": \"Bearer {env:REBUILD_DOSSIER_TOKEN}\" }\n    }\n  }\n}\n```\n\n`oauth: false` disables opencode's automatic OAuth discovery on a `401` — this server only\nsupports the static bearer token above, not a real OAuth flow. Set the referenced env var\n(`REBUILD_DOSSIER_TOKEN` in both examples) to the same value as `MCP_AUTH_TOKEN` above.\n\n## Development\n\n```bash\nnpm test        # full suite\nnpm run typecheck\n```\n\nSmall, single-purpose functions; TDD throughout (tests are written before the implementation\nthey cover, including for the reconciliation logic itself — this is a tool that generates\ntests, so its own correctness matters as much as any feature).\n\n## Current scope, and what's deliberately not built yet\n\nv0 is scoped to prove the core loop, not to be feature-complete. Deliberately deferred, and\ntracked as real backlog rather than silently skipped:\n\n- Reconciliation on API-shaped ambiguity (a validation rule, an error-response shape) is still\n  genuinely untested — the one differently-shaped real app validated so far (catchandtrade)\n  happened to have zero comment/TODO signals to reconcile, so this specific question has no\n  answer yet either way. See [docs/v0-findings.md](docs/v0-findings.md).\n- Video/screen-recording ingestion and the video-LLM flagged-window review.\n- Original-CLAUDE.md / auto-memory as an evidence source.\n- Live Chrome MCP capture for auth-gated/multi-account flows a headless crawler can't reach.\n- Asset-manifest extraction (binary files copied byte-verbatim + a hash manifest, locked\n  contract tier) — real design exists, not yet built.\n- A mutator that no-ops a handler entirely (the current three — flip comparison, drop null\n  check, off-by-one — can't produce a \"this branch never ran\" mutant).\n\nSee [docs/v0-findings.md](docs/v0-findings.md) for the full, honest write-up: the real bugs\nfound and fixed during validation, the comparison across model tiers, and what's still open.\n\n## Contributing\n\nThanks for considering a contribution! This is an academic/research project — changes should\nalign with the design described in the [paper](https://arxiv.org/abs/2608.23616). See\n[CONTRIBUTING.md](CONTRIBUTING.md) for setup, testing, and PR guidelines. First-timers welcome —\nlook for issues tagged `good first issue` or open one and ask what's most useful.\n\nSee [How rebuild-dossier compares](docs/COMPARISONS.html) for how it differs from alternatives.\n\n## License\n\n[MIT](LICENSE)\n\n⭐ If rebuild-dossier helps you ship cleaner rebuilds, a star helps other developers find it.\n",
  "bytes": 21838,
  "sha": "136daef987c3e69172d04b8113afc4b9fb2a952002330e558ef3d23c1def20bf",
  "repo_slug": "parker-fawcett/rebuild-dossier",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_com_parkerfawcett_rebuild_dossier_fbbe6999/readme"
}