Collision Radar is a small, read-only CLI for a specific failure mode of parallel coding agents: a clean Git merge that joins incompatible in-flight changes.
It observes actual worktrees; it does not rely on agent reservations or task declarations.
A full project overview — problem, audience, market gap, novelty, architecture and workflow diagrams — is in docs/overview.html.
Project state and agent/developer handoff details live in CONTEXT.md.
- discovers Git worktrees using
git worktree list --porcelain -z; - inspects each worktree's changes, committed and uncommitted, against the merge base shared by all worktrees, or against a base you name;
- reports textual conflicts, including uncommitted changes, via
git merge-tree --write-treein a disposable Git object directory, snapshotting each worktree once per scan rather than once per pair, and running both snapshots and merges concurrently; - reports symbols both worktrees changed;
- reports a signature change/deletion in one worktree when another introduces calls to that function;
- reports a module or top-level name removed in one worktree when another introduces an import of it, naming where a renamed module moved to;
- supports Python, Go, and the JavaScript family (
.ts,.tsx,.mts,.cts,.js,.jsx,.mjs,.cjs), with explicit imports, CommonJSrequirebindings, direct named barrel re-exports in both module systems, and direct imported-class receiver resolution, reading only the modules a call target names; - emits a concise, conservative migration plan;
- folds the ordered findings into one merge order over the worktrees, reporting cycles rather than breaking them;
- serves that whole report to a phone on the same Wi-Fi, paired by a QR code printed in the terminal, and explains each finding in plain language using a local open-weights model when one is running.
A file that exists but cannot be parsed is treated as unknown rather than empty, so an agent typing mid-function never produces a finding claiming their code was deleted. The same applies to bytes: source is read as UTF-8 (a byte-order mark is fine), and a legacy-encoded or binary file is skipped rather than guessed at or allowed to abort the scan.
It is deliberately a risk detector, not a proof of a semantic bug. A directly typed value resolves method calls on it — verifier = Verifier() in Python, new Verifier() in TypeScript, v := auth.Verifier{} in Go — and reassignment clears that link. Dynamic dispatch, star imports, factories, generated code, and invalid intermediate syntax remain out of scope for this release.
Each language needs its toolchain, and a missing one means that language's findings are absent rather than an error:
| Language | Needs | Notes |
|---|---|---|
| Python | nothing | the standard-library ast module |
| JavaScript family | node, plus typescript 5.x |
resolved from Radar's own npm ci, else from the project being scanned; 7.x is the native port and ships none of the compiler API, so it is rejected rather than half-used |
| Go | a go toolchain on PATH |
a small go/ast analyzer, built once and cached |
One parser serves the whole JavaScript family, where the file extension only selects a dialect. Each language keeps one process for the whole run, including every watch poll. Git 2.38 or newer is required.
The phone dashboard and its QR code need nothing beyond Python: no dependency, no build step, no internet. Plain-language explanations are the one optional extra, and they need only a model server already running on the same machine.
pip install collision-radarPython and Go analysis work from that alone. For the JavaScript family the sidecar needs a TypeScript 5.x compiler: it uses the one the scanned project already depends on, or run npm ci in a checkout to give Radar its own.
python -m pip install -e .
npm ci
radar status --repo path\to\repository
radar status --repo path\to\repository --json
radar check --repo path\to\repository
radar watch --repo path\to\repository
radar serve --repo path\to\repository
python -m unittest discover -s tests -v
python examples/clean_merge_demo.pystatus reports; check gates; serve puts the report on your phone. All three print the same findings, but check exits 2 when a finding reaches its severity threshold, so it can fail a pre-merge hook or a CI job:
| Command | Exit 0 | Exit 1 | Exit 2 |
|---|---|---|---|
status, watch |
always | Git failed | never |
check |
nothing reached the threshold | Git failed | a finding reached the threshold |
serve |
stopped with Ctrl+C | the port or model endpoint was unusable | never |
Severities rank WARN < SERIALIZE < BREAK < CONFLICT. BREAK sits above SERIALIZE because a removed import target is missing after either merge, so no order repairs it. The threshold defaults to SERIALIZE — the risk this product exists to catch. Use --fail-on warn to gate on every signal, or --fail-on conflict to gate only on textual conflicts Git will surface anyway.
Wiring check into a hook or a CI job has two traps — a single worktree can never fail, and blocking on SERIALIZE stops the correct first merge. docs/gating.md covers both with working recipes.
--base <revision> measures every worktree against one revision instead of the shared ancestor of all worktree HEADs — --base main asks "what breaks when these land on the trunk?". Each worktree is compared from where it diverged from that base, so a branch that forked earlier is never blamed for the base's own commits. An unknown revision is a usage error rather than a silent fallback.
watch polls every two seconds and redraws the report in an interactive terminal. Use --interval 1 to adjust the cadence and --iterations 3 for a bounded demo or automation run.
Parallel agents are watched, not driven, and the person supervising them is usually not sitting at the terminal running them. radar serve puts the report where they are:
radar serve --repo path\to\repositoryThe terminal prints a URL and a QR code. Scan it with the phone's camera on the same Wi-Fi and the dashboard opens: the verdict and worst severity at the top, then the merge order, then every finding with its evidence and plan, then each worktree with its changed files and whether its work is committed. It re-polls every couple of seconds, so it works as a live watch view you can leave on a desk.
Collision Radar dashboard
http://192.168.1.42:8765
█████████████████████████████████
████ ▄▄▄▄▄ ██▄▀▀█ ▀▀▀█ ▄▄▄▄▄ ████
████ █ █ █ ▀ ▄ ▀█ █ █ █ ████
████ █▄▄▄█ █ ▄ ▀█▀ ▀▀█ █▄▄▄█ ████
████▄▄▄▄▄▄▄█ █ ▀▄█▄▀▄█▄▄▄▄▄▄▄████
...
Repository: .
Rescans at most every 2s, shared by every viewer.
Same Wi-Fi, no internet. Ctrl+C to stop.
- It never leaves your network. The server binds the LAN and refuses any client on a routable address; nothing in the codebase makes an outbound call. There is no POST route and no control on the page -
serveis exactly as read-only asstatus. - It is the same scan. Every number comes from the call
statusmakes, so the phone and the terminal cannot disagree. - One scan serves everyone. Two phones and a reload cost what one
watchpoll costs.--intervalsets the cadence.
--host and --port change where it binds (default: every interface, port 8765), --no-qr prints the URL alone, and --light-terminal inverts the code for a light terminal theme. A console whose code page cannot draw half-block characters is told to use the URL rather than shown a QR code that looks scannable and is not.
--explain adds a sentence to each finding saying what actually breaks and what to do about it:
radar serve --repo path\to\repository --explainIf a local model server is running it writes them. Radar talks to it over loopback only and refuses any endpoint that is not on this machine, because a finding names your branches, files and symbols. The wire format is Ollama's /api/generate, which llama.cpp's server and others also speak, so any quantized open-weights model works - Qwen, Phi, Llama, Mistral:
ollama pull qwen2.5-coder:1.5b
radar serve --repo . --explainUse --model and --model-endpoint, or RADAR_MODEL and RADAR_MODEL_ENDPOINT, to point somewhere else on the machine. No model is required. With none installed, or none reachable, every finding still gets a written explanation composed from the finding itself: the phrasing changes, the information does not. Nothing a model says can change a severity, an ordering, or whether a finding exists.
Run the local stdio server with:
python -m collision_radar.mcpIt exposes one read-only tool, collision_radar_status, with an optional repo path and base revision. The result includes a human-readable summary and the full JSON report in structuredContent.
Run python examples/clean_merge_demo.py for the core proof. Each scenario ends the same way: Git merges both branches without a conflict and the merged app fails exactly as Radar predicted.
- A changed signature and a new caller. Radar returns
SERIALIZEand orders the change before its caller; the merged app raisesTypeError. - A moved module and a new import. Radar returns
BREAKand names where the module went; the merged app raisesModuleNotFoundError. - One API change across Python, TypeScript, and Go. All three collisions are reported in a single scan, under one merge order. Skipped with a plain message when
nodeorgois not onPATH, so the demo never errors.
Each fixture uses a temporary directory and removes it after the run.
Git worktrees + dirty diffs
|
v
Git adapter ----> textual merge signal (including dirty worktrees)
|
v
AST diff ------> changed symbols, signatures, introduced calls and imports
|
v
pairwise rules -> findings + migration guidance
|
v
ordering ------> one merge order, or a reported cycle
|
v
CLI / JSON / MCP / phone dashboard (LAN only)
|
v
local open-weights model, when one is running -> plain-language wording
There is no watcher, database, code graph, or TUI in the MVP, and the only third-party parser is the TypeScript compiler that serves the JavaScript family. The MCP server is a minimal stdio adapter around the existing detector, and the dashboard is a standard-library HTTP server over the same report. The model, when present, only rephrases findings the detector already proved.
- Git worktree porcelain format is the stable discovery interface used here.
- Git merge-tree performs a real merge without changing the index or working tree; Radar invokes it against disposable snapshots to include dirty worktrees.
- MCP stdio transport uses newline-delimited JSON-RPC messages; the server exposes the documented
tools/listandtools/callflow. - ISO/IEC 18004 specifies QR symbol encoding. Radar implements the byte-mode, level-M subset rather than taking a dependency, and the test suite verifies it by decoding rather than by comparing cells.
- Ollama's generate API is the local-model wire format used by
--explain, chosen because llama.cpp's server and other local runtimes implement it too. - TypeScript's compiler API provides the AST source files used to parse TypeScript without brittle regex matching.
- Clash is the existing worktree merge-conflict tool. Radar intentionally does not duplicate its watcher/TUI scope; its MVP differentiates on signature-to-caller risk and a concrete coordination plan.
- SafeMerge and SAM motivate the boundary: semantic merge analysis is valuable but hard. Radar reports narrow, explainable risk signals rather than claiming general semantic-conflict detection.
A SERIALIZE finding already names which worktree must land first, so the report ends with a single order over the worktrees those findings name:
Merge order
1. agent-auth
2. agent-admin (after agent-auth)
Worktrees no ordering constraint names are left out rather than given a position they have not earned, and mutually blocking constraints are printed as an unorderable cycle instead of being broken arbitrarily. The same plan is in merge_order in --json and mergeOrder in the MCP structuredContent.
Nothing is outstanding: every rule is implemented in all three languages, proven end to end by the demo, and gated. CONTEXT.md lists the options that would most extend its reach — a fourth language, symbol-level rename detection, and an incremental watch.