codebugs
This server is an MCP interface to codebugs, a SQLite-backed tracker for code findings, requirements, releases, and agent coordination, where each defect has a stable identity and duplicate observations bump the same card.
Findings management: file observations with deduplication (create/bump/reopen/refile dismissed cards), batch add, update status/severity/notes/tags/meta, query/filter/group, fetch by ID, recent activity, stats, and category listing/normalization.
Requirements tracking: add/update/query/get/stats/summary/verify/import requirements, store embeddings, batch embed, semantic similarity search, and embedding coverage stats.
Blockers: defer items until a dependency resolves, a date passes, or a manual signal; check actionable items; query and resolve/cancel blockers.
Milestones & releases: create releases/streams, attach/move items, set statuses, triage inbox/dismiss/promote, capacity-aware atomic
pull_next, release items, branch-only/integrated flags, audit logs, close gates with force override, defer items, and reconcile stale stream items.Sweeps: create batch-iteration sweeps with custom lifecycles/transitions, add items with recurrence-aware upserts, fetch next batches, mark states, archive items/sweeps, and list status/items.
Performance benchmarks: import CSV/JSON benchmark results, query/pivot by run or row, list runs, and delete runs.
Parallel-agent merge coordination: start/claim/check/merge/finish/abandon merge sessions, list sessions/claims, and status dashboards.
Claims: claim/release findings or requirements to prevent parallel-agent collisions, see who holds an entity, and list live claims.
Location anchoring: resolve where a finding's code is now (
current,moved,lost, etc.) and recapture anchors from git history, with dry-run defaults.Similarity & grouping: preview near-duplicate annotations, generate similarity families as auditable dry runs, relate findings with typed retractable relations, and inspect citation/tag/filing groupings.
Provenance/staleness: compare findings against git history to detect modified, renamed, or deleted files.
Dashboard & orientation:
summary,reqs_summary,milestone_status,wip_status, and other overview tools for quick context recovery.
Provides integration with Git repositories for anchoring code findings to source locations, resolving moved code across edits, checking file staleness against commit history, and coordinating branch merges through merge sessions with file claims and a merge lock.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@codebugslog a critical bug in src/utils.py"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
codebugs
A code-finding, requirements, and release tracker for AI assistants — one where a finding has an identity. SQLite-backed, exposed via an MCP server and a CLI.
Most trackers treat every report as a new row, so the second agent to notice the same bug files it again, and the queue fills with copies of one defect. codebugs treats a finding as a defect, and each report of it as one observation of that defect:
$ codebugs add -s high -c n_plus_one -f src/api.py -d "Query in loop at line 42" --new-category
Added: CB-1
$ codebugs add -s high -c n_plus_one -f src/api.py -d "Query in loop at line 42"
Bumped: CB-1 (occurrence 2)
$ codebugs update CB-1 --status fixed
Updated: CB-1 (status=fixed, severity=high)
$ codebugs add -s high -c n_plus_one -f src/api.py -d "Query in loop at line 42"
Reopened as regression: CB-1 (occurrence 3)One card, three observations, and a regression recorded on the row it belongs to. Filing an observation again is normal and useful, not noise.
What makes this different
Deduplication is the point, not a side effect — and the rest of the design falls out of that one decision.
Filing the same finding twice does not create two cards. The second report bumps the first: its occurrence count goes up, and its severity rises if this sighting was worse than the last. Severity only ever escalates under observation, so a card filed
lowand re-seencriticalstops hiding from a--severity criticalquery. Lowering it back is a deliberateupdate, never an accident of the last report.A card that was fixed and comes back is a regression, not a duplicate. Re-filing reopens the same card and records the regression on it, so one defect's whole history stays on one row.
A decision stays decided. Re-filing something already dismissed as
wont_fixornot_a_bugdoes not quietly reopen the argument. It files a new card pointing back at the dismissal, so the recurrence is visible and the original ruling survives.The place in the code outlives the edit. When a report names a line range, the location is anchored at filing time from git. After the file is edited around it,
anchor_resolvereports where that code went —moved, with the new line numbers — instead of pointing at whatever now occupies the old line.Parallel agents don't collide.
claims_claimgives one agent a card and refuses the second, naming who holds it and from which repo, so two agents cannot silently fix the same thing. Closing a card releases the claim in the same transaction.Findings can be related and grouped. Cards link to each other, and a similarity report proposes families of near-duplicate findings as a dry run you inspect before merging anything.
It tells you when it could not look. A tracker it cannot read, a file it cannot stat, an anchor whose card never named a code span — each of these comes back as undetermined, with the reason, rather than as a confident wrong answer.
codebugs wherewill tell you which tracker a command is actually bound to and which channel decided that, because a binding you cannot see is a binding you cannot debug.
Underneath that, it is durable memory across sessions: findings survive the conversation that produced them, requirements are checked against the code that claims to implement them, blocked work resurfaces when its dependency resolves, and a release knows what is still stranded on a branch.
codebugs is one SQLite database (.codebugs/findings.db). Modules are self-registering, and the running server reports its own tool catalogue — the module table below is the set of them.
Related MCP server: iranti
Install
codebugs is not published to PyPI. There is nothing under that name in the public index, so
pipx install codebugs and pip install codebugs cannot resolve — they fail before they start.
Install it from a clone instead, by path:
git clone https://github.com/faxik/codebugs.git
pipx install ./codebugs # global install, recommendedpip install ./codebugs works too, into whatever environment is active.
Two consequences of installing by path, both worth knowing before the first surprise:
The installed copy is a snapshot, not a link. Editing the clone afterwards does not change what
codebugsruns. Re-runpipx install --force ./codebugsto pick the changes up, or pass--editableat install time if you want the clone itself to be what runs.The installer resolves dependencies on its own and does not read this repository's
uv.lock. So the versions you end up running are whatever your installer resolved — for a freshpipxinstall, typically the newest release each declared range admits; forpipinto an environment that already has a satisfying version, whatever was already there — and neither is necessarily the combination the test suite ran against. That gap is real and has bitten once already, which is why thenewest-sdkjob in.github/workflows/ci.ymlruns the whole suite against the newest versions the ranges admit. Note what that buys and what it does not: a break on the versions you would install becomes VISIBLE there instead of passing silently on the locked ones — it is not a promise that the newest combination currently works.
Setup
Create the tracker
Run this once per project, in the project root:
codebugs initThis creates .codebugs/findings.db. init is the only command that creates the .codebugs/ directory — every other command discovers an existing one by walking up from the current directory (unless you point it somewhere explicitly, see below), and refuses with an actionable error if there is none. That refusal is deliberate: silently creating an empty database is how findings go missing.
There is one deliberate exception, and it is worth stating precisely because it looks like the rule being broken. The upward walk treats an existing .codebugs/ directory as the opt-in, so if that directory is there but holds no findings.db, the next command creates the database inside it rather than refusing. The common way to end up in that state is an interrupted init — the directory is created before the database — and self-healing on the next command is more useful there than demanding a second init.
A tracker you name explicitly is held to the stricter rule. --repo, --tracker-root and $CODEBUGS_ROOT must resolve to a directory that actually contains findings.db; a .codebugs/ without one is refused, and the message names which channel pointed there. The difference is about evidence: standing inside a directory says something about where you are, while a named path is an assertion that can be mistyped, or exported into a shell days ago and inherited by an unrelated process. That is exactly where a silent empty tracker does the most damage.
Two consequences worth knowing:
Run
initat the project root, not in a subdirectory. Discovery binds to the nearest.codebugs/, so a nested tracker hides the project's real one from everything beneath it.initrefuses to do this unless you pass--force.Git worktrees share the main repo's tracker. A worktree's
.gitis a file pointing at the main repo, which discovery follows — so findings filed from a worktree land in the project's database, not in a throwaway that dies with the worktree.initrefuses to run inside a worktree for the same reason; run it in the main checkout.Two layouts are the exception: if the main repo is bare or was created with
--separate-git-dir, git records no path back to a main checkout — its owngit worktree listreports the git directory instead. Discovery usually refuses those with an explicit error rather than guessing. There is one case it cannot detect: a--separate-git-dirrepo whose git directory is itself named.gitlooks exactly like a normal checkout, so discovery binds to the directory holding the git dir instead of the real checkout, silently. Nothing local can distinguish the two — git reports that directory as a valid work tree as well — so the remedy is to state the root explicitly (below). Runinitin the main checkout before creating worktrees.
Pointing codebugs at a specific tracker
Discovery is a heuristic, so it has an override:
codebugs --tracker-root /path/to/project query # this invocation only
export CODEBUGS_ROOT=/path/to/project # this shell and anything it spawnsResolution order, most specific first: a command's own explicit path argument (--repo, where a command has one) → --tracker-root → CODEBUGS_ROOT → walking up from the current directory. A per-command argument outranks a declaration because it names one operation's target, while a declaration is process-wide.
codebugs where # show the current binding and which channel decided itwhere is a diagnostic, not a precedence level: it prints the resolved root, the database path, and the channel — the fastest way to check that a command is about to read the tracker you think it is.
One thing is true of every declared root, for every command except init: a root that contains no .codebugs/ is a hard error, never a new tracker. The value may be a stale export inherited from another shell, and silently creating an empty database there is how findings go missing.
init is the exception, and it treats the two channels differently, because they carry different evidence. --tracker-root DIR init creates the tracker in DIR — the flag is typed on the command line being run, so it is an assertion about this invocation, exactly like a path argument. CODEBUGS_ROOT is ignored by init, which creates where you are standing, and warns that later commands will read somewhere else: an environment variable exported days ago and inherited by an unrelated process must never conjure a tracker in a directory you are not in. Whenever the tracker init created is not the one the next command would read, it says so on stderr, since otherwise it would report success for a dead end.
CODEBUGS_ROOT is inherited by every subprocess, so export it only when you mean "this shell works on that project". For one-off use across projects, prefer --tracker-root.
Claude Code (MCP)
Add to ~/.claude.json (global) or .mcp.json (per-project):
{
"mcpServers": {
"codebugs": {
"command": "codebugs-mcp"
}
}
}The database lives at .codebugs/findings.db, discovered by walking up from the server's working directory — each project gets its own. Run codebugs init in the project first (see above), or every tool call will fail with "no .codebugs/ found".
The server connects lazily, per tool call, so it starts successfully even when no tracker is reachable. At startup it writes a diagnostic to stderr — which MCP clients log — if discovery failed, or if a root was declared rather than discovered; on the ordinary path it says nothing. It never refuses to start: a project directory that appears later must still work.
To pin one server to one tracker instead of deriving it from the working directory:
{
"mcpServers": {
"codebugs": {
"command": "codebugs-mcp",
"args": ["--tracker-root", "/path/to/project"]
}
}
}Only do this when you want that server bound to a single project — the default cwd-derived behavior is what lets one registration serve many. Add .codebugs/ to your .gitignore.
Running Modules Independently
Use --mode to load only the tools you need:
{
"mcpServers": {
"codebugs": {
"command": "codebugs-mcp",
"args": ["--mode", "findings"]
}
}
}Any module name from the module table below is a valid mode, and all — the default — loads everything. The CLI takes the same flag: codebugs --mode findings summary.
One asymmetry is worth knowing before you rely on it: usage is a CLI-only mode. It registers a command but no MCP tools, so codebugs --mode usage usage works, while codebugs-mcp --mode usage does not start at all — the server does not accept that value, and refuses it with argument --mode: invalid choice: 'usage' and exit code 2. It is not a server that runs with an empty catalogue; there is no server. The two lists of accepted modes are compared with each other, and with the module table below, by the test suite — so a mode that appears on one surface and not the other turns a test red rather than surprising you here.
Other MCP Clients
Any MCP-compatible client can connect to codebugs-mcp via stdio transport.
The modules
Every name in this table is a valid --mode value.
Module | Domain | Headline tools |
findings | Bugs, tech-debt, review findings |
|
reqs | Functional requirements (FR-N) |
|
blockers | "X is blocked by Y" dependency graph |
|
sweep | Batch iteration with state machines |
|
bench | Performance benchmark snapshots |
|
merge | Parallel-agent merge serialization |
|
milestones | Releases, streams, capacity-aware pull |
|
provenance | Staleness vs git history, commit trailers |
|
claims | Which agent holds a finding or requirement |
|
loc | Where in the code a finding is, across edits |
|
similarity | Near-duplicate findings, as a dry run |
|
relations | Typed, retractable links between findings |
|
grouping | Reads the axes the tracker stores but never exposed |
|
usage | Tool-call counters (CLI only — no MCP tools) | — |
Modules are self-registering — adding a new one is local to its own file. See docs/superpowers/specs/ for the architecture history.
Not every MCP tool has a CLI verb. The milestones module is where the gap is widest: milestone_create, milestone_update, milestone_add_item, milestone_move_item, milestone_set_status, milestone_defer, milestone_close, triage_dismiss, triage_promote, pull_next and release_item are reachable through MCP only. From a terminal you can inspect a release, but you cannot create one or pull work from it.
Quick tour
Findings — log it, never re-discover it
MCP tools:
Tool | Purpose |
| Dashboard overview — start here for orientation |
| File an observation. Creates, bumps, reopens or refiles — read |
| File several observations at once |
| Change status, severity, notes, tags or metadata ( |
| Search/filter with pagination and group-by |
| Fetch one finding by id, with its full body and occurrence history |
| Findings touched at or after a date — the one call for "what closed since" |
| Cross-tabulated counts (severity x category/file/status) |
| List existing categories — call before |
| Fold twin category spellings together. Dry run by default |
staleness_check lives in the provenance module rather than here, and anchor_resolve in loc; both are listed in the module table. The tools behind the opening section — anchors, similarity, relations and grouping — have their own entry under Identity, location and grouping below.
CLI:
codebugs add -s high -c n_plus_one -f src/api.py -d "Query in loop at line 42" --new-category
codebugs summary
codebugs query --status open --severity critical
codebugs update CB-1 --status fixed --append-note "Fixed in PR #42"
codebugs categories--new-category is needed the first time a category is used, and only then. A category the tracker has never seen is refused, naming the closest existing spellings, because the common way a category set fragments is a typo — n-plus-one beside n_plus_one — and a fragmented category set is what makes categories stop revealing patterns. Once n_plus_one exists, later findings use it without the flag. Spelling is normalized on the way in, so hyphens, spaces and case do not mint twins.
--append-note adds to a finding's notes; --notes replaces them wholesale. Prefer --append-note when recording investigation history — --notes will discard whatever was there, which is usually not what you want on a finding others have been working.
When a new finding is added, the milestones auto-router automatically attaches it to stream/triage (or stream/security when severity=critical and category starts with security:). The finding and its triage entry land in the same transaction.
Requirements — verify what shipped, surface contradictions
MCP tools:
Tool | Purpose |
| Requirements dashboard — start here |
| Add a requirement (FR-001, priority, status, test coverage) |
| Change status, description, priority, test coverage |
| Search/filter by status, priority, section, free text |
| Fetch a single requirement by ID with full body |
| Cross-tabulated counts (status x priority) |
| Automated checks: ghost test files, duplicate IDs, status contradictions |
| Import from REQUIREMENTS.md (parses markdown tables) |
| Store embedding vectors |
| Semantic search across requirements |
| Report on embedding coverage |
CLI:
codebugs reqs-import REQUIREMENTS.md
codebugs reqs-summary
codebugs reqs-verify
codebugs reqs-query --status Implemented --priority Must
codebugs reqs-update FR-090 --status Superseded --notes "Replaced by vault architecture"
codebugs reqs-export REQUIREMENTS.mdBlockers — "X is blocked by Y", with auto-unblock
MCP tools:
Tool | Purpose |
| Defer an item until another item resolves, a date passes, or a manual signal |
| List blockers filtered by item, dependency, trigger type |
| Find currently-actionable items (all blockers satisfied) |
| Cancel or manually resolve a blocker |
Triggers come in three flavors: entity_resolved (waits for another finding/requirement to reach a terminal state), date (unblocks on a specific datetime), and manual (operator signal). When you mark a finding fixed, every blocker that was waiting on it auto-unblocks and surfaces in the next blockers_check.
Milestones — release containers + standing streams + capacity-aware pull
MCP tools:
Tool | Purpose |
| Rollup for one milestone (counts by status/size, branch-only, blocked, days to target) |
| List milestones, filter by kind / state |
| Create a release or stream |
| Mutate |
| Attach a bug / requirement / external ref to a milestone |
| Move an item between milestones |
| Open / in_progress / done / dismissed / deferred |
| Move to |
| Refuses if open / branch-only / blocked items remain (force overrides, except for streams) |
| Full state-transition history |
| Items waiting to be triaged |
| Reject a triage item; propagates to underlying entity |
| Move a triage item to a target milestone |
| Atomically claim the next eligible item for the calling agent |
| Free agent capacity ( |
| Snapshot of |
| Flag an item as living on a feature branch only |
| Mark merged-to-main with commit SHA; clears branch_only |
Four seed milestones are created automatically:
stream/triage— inbox for unsorted findings (default destination)stream/maintenance— deferred / boy-scout workstream/security— urgent fixes (preempts release work)release/1.1— first post-1.0 release
pull_next priority order: stream/security > release/* (earliest target_date first) > stream/triage > stream/maintenance. Within a milestone: priority ASC, then created_at ASC.
Eligibility: item is open, no active blockers (skipped for item_kind='external'), acceptance required for size='large', and a large bug in a release milestone must declare linked_frs whose ids resolve to rows in requirements. Concurrent calls from multiple agents are atomic — claims are serialized via BEGIN IMMEDIATE.
CLI:
codebugs milestone-list
codebugs milestone-status release/1.1
codebugs triage-inbox
codebugs wip-status
codebugs milestone-audit --milestone release/1.1A typical autonomous-agent loop:
# 1. Agent claims the next eligible item.
item = pull_next(agent_id="agent-A", capacity={"large": 1, "small": 2, "triage": 5})
# 2. (Optional) flag a feature branch.
mark_branch_only(item_ref=item["item_ref"], branch_name="feat/CB-1234")
# 3. After integration, mark it done with the commit SHA.
mark_integrated(item_ref=item["item_ref"], commit="abc123…")
# 4. Free the agent's capacity slot.
release_item(item_ref=item["item_ref"], status="done")Closing a release runs the close-gate: unfinished, branch-only, and blocker-gated items refuse to let the milestone ship. force=True (with a logged reason) overrides — but stream/* milestones cannot be closed, even with force.
Sweeps — batch iteration with recurrence-aware lifecycles
MCP tools:
Tool | Purpose |
| Create a new sweep (optional |
| Add items. Atomic upsert: existing items bump |
| Next batch of unprocessed (non-terminal, non-archived) items |
| Transition state (legacy |
| Progress overview |
| Soft-delete |
| Inspection |
codebugs sweep-create --name lint-pass --batch-size 5
codebugs sweep-add lint-pass src/*.py --tags critical
codebugs sweep-next lint-pass
codebugs sweep-mark lint-pass src/api.py
codebugs sweep-status lint-passWith a custom lifecycle (e.g. for retro findings):
codebugs sweep-create --name retro-findings \
--lifecycle DETECTED,CONFIRMED,ESCALATED,RESOLVED,DROPPED \
--terminal-states RESOLVED,DROPPED
codebugs sweep-add retro-findings finding-2026-04-todo-bypassed --tags silent_abandonment
codebugs sweep-mark retro-findings finding-2026-04-todo-bypassed --state CONFIRMED
codebugs sweep-archive-items retro-findings --state RESOLVED --older-than 30dBench — performance snapshots over time
MCP tools:
Tool | Purpose |
| Import benchmark results (file or inline) |
| Filter and trend metrics across runs |
| List recorded runs |
| Remove a run |
Merge — parallel-agent merge serialization
MCP tools:
Tool | Purpose |
| Open a merge session |
| Claim files for the session (advisory file-level claims) |
| Check for overlapping claims against |
| Mark merge in progress (acquires the global merge lock with TTL) |
| Release the lock |
| List all files a session has claimed, in claim order |
| List merge sessions with claim counts |
| Dashboard: session counts by status, total active claims |
| Close a session for good, so its files stop blocking everyone else |
Identity, location and grouping
The opening section's claims about identity, location and grouping rest on loc, similarity, relations and grouping, and none of them had an entry here before.
Tool | Purpose |
| Where a finding's code is now — |
| Rebuild stored anchors from the git object store. Dry run by default |
| Preview what the file-time annotator would stamp for an observation |
| Similarity families as auditable evidence — a scrub you read, never a merge it performs |
| Assert a typed relation ( |
| List relations touching a finding, in both directions |
| Retract a relation. Tombstones it — the row and its history remain |
| Connected components of the hand-written CB-id reference graph |
| Tag pivots: counts, co-occurrence, near-duplicate taxonomy strings |
| Split lineages and shared filing events |
An anchor needs the filing to name a code span. The location is captured from meta.line / meta.lines (and a few equivalent spellings) at the moment the finding is filed. A report that names only a file has nothing to anchor, and anchor_resolve says so — unknown, reason no_grammar — instead of guessing a line. Most findings in practice name no span, so this is the ordinary case, not an error:
$ codebugs anchor-resolve --finding-id CB-2 --repo . --json
...
"anchor": {
"status": "moved",
"path": "src_api.py",
"line": 25,
"end": 27,
"channel": "git",
"reason": null,
"survived": "3/3",
...That card was filed against lines 5–7. Twenty lines were then inserted above it. The stored line numbers are stale; the anchor is not.
The ... are elision marks rather than output: this is one entry lifted out of a larger JSON document, and the anchor object carries one further key below survived — resolved_against, the root, commit and on-disk file the answer was computed against. Nothing is ever omitted because it was absent. Every key is present on every answer, so "reason": null here means there is nothing to explain, because there is an answer — never this field is missing.
How It Works
The Problem
AI code review sessions produce findings that get lost. Multiple agents working in parallel double-claim work. Requirements files drift. Releases lose track of what's in them.
The Solution
codebugs stores everything in one local SQLite database. AI assistants write findings, requirements, and milestone items as they discover them, then query the database in future sessions for instant context recovery. Concurrent agents coordinate via the same database — no race conditions, atomic claims.
Token savings: a summary call returns one small structured overview — counts by severity, the top categories, the hottest files — instead of the file reading and conversation replay it would otherwise take to re-establish the same picture.
Typical Workflows
Code review loop. This is the same loop the MCP server tells every client that connects, so the two cannot drift apart:
File the observation with
add(orbatch_addfor several at once). Callcategoriesfirst when you are unsure of the naming.Read what came back before doing anything else.
dedup_actionsays whether this created a new card, bumped or reopened an existing one, or refiled one already dismissed.attentionis the server's own flag when your observation raised the card's severity or diverged from its stored category — a card you thought you were filing fresh but which the tracker already knew about, differently.The code location is anchored at file time when the observation names one;
anchor_resolvereports whether that anchor still points at live code.Close the card with
update CB-N --status fixedonce it is actually fixed.
Working alongside other agents on the same tracker? Claim the card with claims_claim before starting and release it with claims_release when done, or two agents can end up fixing the same thing. Closing the card releases the claim on its own.
Each add also auto-routes the finding to stream/triage, and over time categories reveals systemic issues — "12 tz_naive_datetime fixed across 9 files → time for a lint rule."
Requirements (reqs_add, reqs_query, ...) are a separate, authored entity next to findings, and they have no deduplication. Do not file a requirement through add, or a defect through reqs_add.
Release loop:
Triage: AI calls
triage_inbox→triage_dismissnon-bugs,triage_promotereal items torelease/1.1(withlinked_frsfor the ones that need an FR row).Execution: Each parallel agent calls
pull_next(agent_id=..., capacity=...)→ claims the next eligible item.After landing:
mark_integrated(item, commit)→release_item(item, status='done').Close:
milestone_close("release/1.1"). Refuses if anything is stranded on a branch; lists the offenders with the branch name.
Schema (highlights)
All tables share .codebugs/findings.db with flexible JSON columns. Schemas are additive — every module owns its tables, declares dependencies, and migrates additively.
Findings
Field | Type | Description |
| text | Auto-generated ( |
| text |
|
| text | User-defined (e.g. |
| text | File path relative to project root |
| text |
|
| text | What's wrong |
| text |
|
| json | Array of strings for ad-hoc grouping |
| json |
|
| text | Provenance for staleness checks |
Requirements
Field | Type | Description |
| text | User-provided ( |
| text | per-row metadata |
| blob | Optional float32 vector for semantic search |
| json |
Milestones
Table | Purpose |
| Slug ( |
|
|
| Append-only log: actor, action, from_state → to_state, reason, timestamp |
| Per-agent WIP ( |
Item kinds are bug (validated against findings), requirement (validated against requirements), or external (free-form, blockers skipped). The (milestone_id, item_kind, item_ref) unique constraint prevents double-attach.
Blockers
Field | Type | Description |
| text | Blocked entity (e.g. |
| text | Dependency (or null for date/manual triggers) |
| text |
|
| text | UTC datetime for date triggers |
| text | Human explanation |
Sweeps
Table | Purpose |
|
|
|
|
Killer features
Pattern detection over time
$ codebugs categories
category total open fixed
------------------------ ----- ---- -----
tz_naive_datetime 15 3 12
n_plus_one 8 2 6
missing_input_validation 6 4 2If you keep fixing the same category → time for a lint rule. codebugs turns reactive bug-fixing into proactive prevention.
This is the view the category gate above protects. Had half those findings been filed as tz_naive_dt, the table would show two rows of 8 and 7 instead of one row of 15, and there would be no pattern to see. Normalization handles the punctuation-and-case twins on its own; the gate is what catches a genuinely different name for the same thing.
Requirements verification
reqs_verify catches documentation rot before it ships:
$ codebugs reqs-verify
Verified 3 requirements.
4 issue(s) found:
check sev id message
------ ------ ------ -----------------------------------------------------------
ids medium -- Numbering gaps (5+): FR-007..FR-089, FR-091..FR-349
status medium FR-006 Must-priority requirement implemented without test coverage
status high FR-090 Description mentions 'superseded' but status is 'planned'
status medium FR-350 Must-priority requirement implemented without test coverageSemantic requirements search
Store embeddings (caller generates vectors via any embedding API) and find related requirements semantically:
reqs_embed(req_id="FR-001", embedding=[0.1, 0.2, ...])
reqs_search_similar(query_embedding=[...], limit=5, min_similarity=0.3)Float32 BLOB storage in SQLite; brute-force cosine similarity — fast for thousands of requirements.
Close-gate enforcement
milestone_close won't let you ship a release with work stranded on a branch. First, what the release looks like:
$ codebugs milestone-status release/1.1
release/1.1 (release, state=open)
target: 2026-09-15 (19 days)
First post-1.0 feature release. Target date set later.
Items: 3 total (3 open/in_progress, 0 done)
By status:
open 3
By size:
small 3
Branch-only: CB-1
Blocked: CB-2Then closing it. milestone_close is one of the milestone tools with no CLI verb — this is the error the MCP tool returns, raised as a ValueError from the domain function, on a single line:
cannot close release/1.1: unfinished items (3): CB-1, CB-2, CB-3; branch-only items (1): CB-1@feat/CB-1; items with active blockers (1): CB-2 (use force=True with reason to override)force=True with a logged reason overrides that. Streams do not have an override — milestone_close(id="stream/triage", force=True, reason="x") still refuses with streams cannot be closed (milestone=stream/triage), because they are permanent buckets rather than things that ship.
Requirements
Python 3.11+
One runtime dependency:
mcp>=2.0.0,<3, for the server. The 2.0 floor is not cosmetic —server.pyusesMCPServer, the class that replacedFastMCPin the 2.0 SDK, so an oldermcpwill not start.SQLite (bundled with Python)
Development
# Run tests
uv run --extra dev python -m pytest tests/ -q
# Lint — this is the gate
uv run --extra dev ruff check src/ tests/pytest and ruff live in the dev extra, which uv run does not install by default, so --extra dev is not optional in a fresh clone.
ruff format is deliberately not run over this tree. Much of the existing code does not conform to it, so ruff format src/ tests/ would rewrite most of the repository in one commit. ruff check is the gate; formatting is left alone on purpose.
See CLAUDE.md for architectural rules and conventions.
License
MIT
Available Tools
83 toolsaddA
Record a code finding observation (deduplicated by fingerprint).
If the fingerprint matches a live finding, that finding's occurrence count
is bumped and IT is returned (was_new: false, dedup_action: "bumped");
a match on a fixed finding reopens it as a regression ("reopened"); a
match on a wont_fix/not_a_bug finding creates a new row linked via
meta.recurrence_of. Without a fingerprint a conservative server-side one
is derived from category, file and the normalized description.
dedup_action has exactly four values — "created", "bumped", "reopened"
and "recurrence_of_closed" — and the fourth is the one to read carefully:
a recurrence of a DISMISSED twin files a NEW row and therefore reports
was_new: true, so a client that tells create from match by gating on
was_new == false misses the event entirely. The twin's id is always in
meta.recurrence_of, and meta.similar_to usually carries its status
alongside; on the paths where it does not — a caller-supplied fingerprint
whose text does not resemble the twin, or a normalized description under
the similarity minimum — the twin's status is NOT in this response at
all, and costs one get.
attention is a top-level list, ALWAYS present and often empty: an empty
list means "evaluated, nothing serious fired", which is a different fact
from an absent channel. Two record forms exist, and a list may carry both
(severity first, category second; each form at most once).
{signal: severity_escalated, from, to} says THIS observation raised the
finding's stored severity. It appears only where a stored severity was
raised — the bumped and reopened branches — and severity is monotonic
under observation, so there is no de-escalation record to expect.
{signal: category_divergence, observed, stored} says this observation
does not NAME the matched finding's category. It appears on every branch
that HAS a matched row: bumped, reopened, and the recurrence branch,
where the comparison is against the DISMISSED TWIN rather than the new
row. Both sides are normalized, so a difference of spelling
(Process Improvement vs process-improvement) is deliberately not a
signal while a difference of name is; a stored category that is not text
is skipped rather than raising. A newly created finding matched nothing,
so it emits neither record.
stripped_meta_keys is a top-level list, ALWAYS present and often
empty, following the same discipline as attention: [] means
"checked, nothing to strip", never "no such channel". A meta key
that is identity machinery OUTPUT (e.g. occurrences, recurrence_of,
category_minted) is stripped from what gets stored rather than
refused, so a caller that copies a fetched card's meta forward
(get -> modify -> add) can tell, from this response alone, which
of its own keys silently did not land. resolver_errors is the one
exception: it reports a FAILURE state, not machinery input, so it is
REFUSED outright rather than stripped, on this path exactly as on
update's meta_update. This is the ADD-side contract only — CSV
import strips the same dynamic reserved union but silently, with no
equivalent response key (a decided, separate contract, CB-51), and
update's meta_update still refuses every reserved key rather than
stripping any of them.
stripped_description_tail is a top-level boolean, ALWAYS present and
usually False, following that same discipline: False means "checked,
nothing to cut", never "no such channel". Some filing agents leak a
slice of their own tool call into the end of description; when the
text after a </description> marker is nothing but envelope lines, that
tail is CUT rather than refused — the finding is real and only its tail
is junk — and cut BEFORE the fingerprint is derived, so a tailed and a
clean report of one defect collapse onto one card instead of two. Prose
that merely quotes the marker is not cut. True means the text stored
is not byte-for-byte the text you passed.
Args:
severity: critical, high, medium, or low (case-insensitive, no aliases)
category: Finding category (e.g. tz_naive_datetime, n_plus_one, missing_validation). Call
categoriesfirst to reuse existing category names. Spelling is normalized (casefold, hyphen/whitespace -> "_"); a category this tracker does not already hold is REFUSED with a hint unless new_category=true — but only when the observation would CREATE a row: a fingerprint match on a known live or fixed finding is recorded regardless, with the observed category kept in the occurrence ring.file: File path relative to project root
description: What's wrong
lines: WHERE IN THE CODE this finding is, and the only input that gives the card a durable ANCHOR. An anchor stores the surrounding source text and the commit it was read at, so the card still points at the right code after the file is edited and the line numbers move; a path written in
descriptiondoes not, and is never read as a location. WITHOUT THIS THE CARD HAS NO ANCHOR — nothing else in this call supplies one. Four spellings, all accepted: a bare line number ("1850") or range ("1850-1870"), which are read against thefileargument above; a full "path/file.py:1850" token, whose path must name the same file asfileor the anchor is refused rather than pointed at another file's line numbers; and a list ("[1850, 1870]"), which is N SEPARATE lines and never a range. Pass it whenever the finding is about a place in the code. Omit it — do not invent one — when the finding is about a process, a decision or a whole file: a made-up anchor is worse than none. Same field asmeta.lines; supplying both with different values is refused.source: First reporter of this defect (default: claude). Frozen at first report by design (BT-4): a re-observation keeps the original; newest sources live in the occurrence ring (meta.occurrences[*].source) — and an imported observation's ring source can be a peer tracker's.
tags: Optional tags for grouping
meta: Optional JSON metadata for anything this call has no argument for (module, rule_code, and so on). The code location is NOT one of those: it has its own
linesargument above, and that is the spelling to use.meta.linesremains the same field and still works, so the two must not disagree — passing both with different values is refused rather than one silently winning. Top-level meta is the row's AUTHORED state, observation-frozen (BT-4): a re-observation's meta lands only as per-occurrence evidence in meta.occurrences[*].meta. Promoting specific keys into the row is a future allowlist by measured demand, not a general merge.reported_at_commit: Git SHA when finding was created (auto-detected from HEAD if omitted)
reported_at_ref: Version/tag label (e.g. "v2.1.0"), always caller-supplied. Observation-frozen: a bump never updates it (per-occurrence refs stay in the ring as evidence) — but manually mutable BY DESIGN via update(reported_at_ref=), since a release is tagged after filing.
fingerprint: Stable identity token for this defect, computed from the INVARIANT part of the observation (normalized error signature + failing test + anchor file — no timestamps, SHAs, run ids). Same defect → same fingerprint. The
auto:prefix is reserved for server-derived values.new_category: Explicit permission to MINT a category the tracker does not hold yet (CB-60). Minting is stamped as meta.category_minted for later counting. Existing categories never need this.
Inputs: file (required), category (required), severity (required), description (required), +8 more. Returns: object
anchor_recaptureA
Rebuild stored location anchors from the git object store. DRY RUN by default.
The sanctioned repair path: meta.loc is writable through
update_finding(meta_update=) and NOTHING validates it there, so a
hand-assembled object is accepted at the write and read back as
unknown(invalid_anchor). This verb builds the object itself, from the
same capture the file-time resolver uses.
Four behaviours are specified rather than incidental. A FAILED capture
never replaces a valid stored anchor (outcome kept) — the refusal is
usually about the environment, and the anchor it would destroy is still
good in a clone that has the history. The loc: null tombstone ("do not
recapture") is left alone unless force_tombstone says otherwise. The
git work runs outside any transaction, and only the version check and
the write share one — so a row whose anchor changed while the capture
ran is reported stale and left to the other writer.
include_unanchored widens the POPULATION to rows that never carried an
anchor at all — every finding filed before the capture seam landed, since
capture runs only when a genuine new finding is filed. They report
would_backfill/backfilled, never folded into would_update/updated:
"acquired an anchor for the first time" is the number this exists to
produce. It is NOT force_tombstone (a loc: null tombstone is a key
that is present and null, and this flag never touches it) and it is NOT a
fingerprint backfill — nothing here reads or writes that column.
Args:
finding_id: Repair one finding instead of a population
status: Status filter; "all" widens to every status (default: open)
category: Restrict to one category
file: Restrict to one file (the finding's
filecolumn)project_dir: The repository to capture from. Omitting it makes every capture refuse with
no_root, which by the rule above leaves every valid anchor untouchedapply: Write the rebuilt anchors (default: report only)
force_tombstone: Overwrite a
loc: nulltombstoneinclude_unanchored: Also take rows carrying no
lockey at all (the backfill population). Leaves tombstones alone; still a dry run unlessapplyis setlimit: Maximum findings examined (default 10000). 0 means NO findings are examined; a negative value is an error (it used to mean "no limit").
Inputs: file, apply, limit, status, +5 more. Returns: object
anchor_resolveA
Resolve stored location anchors to their current lines on HEAD.
Each anchored finding gets a record: status (current / moved /
moved_file / lost / ambiguous / unknown), the coordinate, the channel
that produced it ("git" for reverse blame, "content" for the secondary
text channel), a reason token when there is no answer, and
survived as "/" when part of a span outlived the rest.
moved_file is a status of its own, not moved with a different path:
the code left the file the finding names, and a consumer must see that
rather than receive a line number in a file it never asked about.
THE SUMMARY'S DENOMINATOR IS anchored, NOT total. anchored counts
the rows that CARRY an anchor (a persisted refusal and the tombstone
included); rows filed before anchors existed carry none and are counted
in without_anchor instead. So a moved_file share is
summary["moved_file"] / anchored; computing it against total is a
share of a population the number does not describe.
Args:
finding_id: Resolve one finding instead of a population
status: Status filter; "all" widens to every status (default: open)
category: Restrict to one category
file: Restrict to one file (the finding's
filecolumn)project_dir: The repository the anchors resolve against. Omitting it reports
no_rootrather than reading whatever tree the server process happens to stand in — a long-lived server's cwd has nothing to do with the tracker a call is aboutlimit: Maximum findings examined (default 10000). 0 means NO findings are examined; a negative value is an error (it used to mean "no limit").
Inputs: file, limit, status, category, +2 more. Returns: object
batch_addA
Record multiple finding observations at once (deduplicated by fingerprint).
Members are deduplicated exactly like add — including against each other,
so two members sharing a fingerprint yield one insert plus one bump. One
result per input, in input order. Unknown member keys are refused.
Each result carries the same discriminators add returns, and the same
four dedup_action values: "created", "bumped", "reopened" and
"recurrence_of_closed", the last of which reports was_new: true because
it files a NEW row linked to a dismissed twin via meta.recurrence_of.
Each result also carries its OWN attention list — always present, often
empty, never shared between members. Two record forms exist, in this
order and at most once each:
{signal: severity_escalated, from, to} on the bumped and reopened
branches, meaning that member's observation raised the stored finding's
severity; and {signal: category_divergence, observed, stored} on every
branch with a matched row (bumped, reopened, and the recurrence
branch, where the comparison is against the dismissed twin), meaning that
member does not NAME the matched finding's category. Both category sides
are normalized, so a difference of spelling is not a signal; a stored
category that is not text is skipped rather than raising.
Each result also carries its OWN stripped_meta_keys list — always
present, often empty, never shared between members — following the
same discipline: a meta key that is identity machinery OUTPUT (e.g.
occurrences, recurrence_of, category_minted) is stripped from
what gets stored rather than refused, and reported here so a caller
forwarding a fetched card's meta can tell which of its own keys
silently did not land. resolver_errors is refused outright instead
(a FAILURE state, not machinery input), on this path exactly as on
add.
Each result likewise carries its OWN stripped_description_tail
boolean — always present, usually False, meaning "checked, nothing to
cut" rather than "no such channel". A leaked tool-call tail on that
member's description (envelope lines and nothing else after a
</description> marker) is CUT rather than refused, before the
fingerprint is derived so a tailed and a clean report of one defect
collapse onto one card; prose merely quoting the marker is left alone.
True means that member's stored text is not the text you passed.
Args:
findings: List of finding objects, each with keys: severity, category, file, description, and optionally: lines, source, tags, meta, reported_at_commit, reported_at_ref, fingerprint.
linesis per-member and works exactly as it does onadd: it is WHERE IN THE CODE that member is, and the only input that gives its card a durable ANCHOR surviving later edits to the file. A bare number ("1850"), a range ("1850-1870"), a full "path/file.py:1850" token (whose path must name that member's ownfile) or a list of lines. Omit it on a member that describes no place in the code rather than inventing one. It is the same field as that member'smeta.lines, and the two are refused when they disagree.reported_at_commit: Default commit SHA for all findings (auto-detected if omitted). Per-finding values override this.
reported_at_ref: Default version label for all findings. Per-finding values override this.
new_category: Batch-wide permission to MINT categories the tracker does not hold yet (CB-60); the first member introducing a category is stamped meta.category_minted.
Inputs: findings (required), new_category, reported_at_ref, reported_at_commit. Returns: result
blockers_addA
Defer an item by adding a blocker.
Args:
item_id: The blocked entity (e.g. "CB-5", "FR-012")
reason: Why it's blocked
blocked_by: Dependency entity (e.g. "CB-3"). Required for entity_resolved triggers.
trigger_type: entity_resolved, date, or manual. Defaults to entity_resolved if blocked_by provided, manual otherwise.
trigger_at: Date/datetime for date triggers (e.g. "2026-04-10"). Normalized to UTC.
Inputs: reason (required), item_id (required), blocked_by, trigger_at, +1 more. Returns: object
blockers_checkA
Scan for currently actionable items — items whose blockers are all satisfied.
Returns actionable items (all blockers met), partially unblocked items (some blockers met), and overdue date triggers.
Inputs: no named parameters. Returns: object
blockers_queryA
List blockers with filters. Each result includes computed satisfaction state.
Args:
item_id: Filter by blocked item (e.g. "CB-5")
blocked_by: Filter by dependency ("what does CB-3 unblock?")
trigger_type: Filter by trigger type (entity_resolved, date, manual)
active_only: Only unsatisfied, uncancelled blockers (default: true)
Inputs: item_id, blocked_by, active_only, trigger_type. Returns: object
blockers_resolveA
Cancel or manually resolve a blocker.
Args:
blocker_id: The blocker row ID
action: 'cancel' (any trigger type) or 'resolve' (manual triggers only)
Inputs: action (required), blocker_id (required). Returns: object
categoriesA
List all existing categories with counts. Call this before adding findings to reuse consistent category names.
Inputs: no named parameters. Returns: result
categories_normalizeA
Rename stored categories and re-key their fingerprints (CB-61).
TWO MODES, and the second is a working mode rather than a side effect.
Without fold_map this folds every stored SPELLING to its canonical
form, for rows filed before write-time canonicalization existed, whose
stored auto:v1 fingerprint still carries the old spelling and therefore
forks identity when the same defect is reported again. With a fold_map
it MERGES CATEGORY NAMES: any stored name may be renamed to any canonical
target, and the two need not be spellings of each other. That second mode
is how a tracker's rare category names are collapsed into its common ones.
DRY RUN BY DEFAULT — without apply=true nothing is written and the
report tells you exactly what would change. A key that matches no stored
category is accepted and renames nothing, and unmatched_fold_keys names
every one of them, so a typo on the left-hand side is stated rather than
left to be spotted as a pair missing from the from -> to table. A typo
in a TARGET is refused instead — see new_category. Matching is exact
against the stored spelling, so a canonical key does not reach a stored
Process Improvement and is reported unmatched. Take an export-csv
backup before applying; restore-csv puts findings back verbatim into an
EMPTY tracker, but milestone items and audit history are not in a CSV
export and are not restored.
Each renamed row's fingerprint is handled by kind: a NULL or a
caller-SUPPLIED fingerprint is left byte-identical, an auto:v1 one is
re-derived with the new category after its stored inputs are verified to
reproduce the stored hash. A row that fails that round trip is skipped
WHOLE and reported under unverifiable. The occurrence ring
(meta.occurrences) is never rewritten.
If the fold would put two LIVE findings on one fingerprint, the run
writes NOTHING and reports the colliding pair by id — merging two cards
is a decision, not a migration step. Any OTHER identity merge — two closed
cards, or a closed card and a live one — is legal, so it is reported under
merged_identities rather than refused: the run proceeds, and you are told
which cards this fold fused. Both unmatched_fold_keys and
merged_identities are always present; [] means "checked, none".
Args:
fold_map: Optional {stored category name: canonical target name} map, as an object or a JSON string. The key is matched exactly against the stored value and may be any name the table holds. Every target must already be canonical (casefold, hyphen/whitespace -> "_"). Omit it to fold every stored spelling to its own normalized form;
{}is an explicit no-op.apply: Write the changes. Default false (report only).
new_category: Permission to fold INTO a category this tracker does not hold yet, for the whole map at once. Without it such a target is refused, naming the nearest existing categories — an operation meant to REDUCE the number of category names must not invent one by typo. The refusal stops at the first bad target.
Inputs: apply, fold_map, new_category. Returns: object
claims_claimA
Claim a finding or requirement so parallel agents do not collide.
Args:
entity_id: CB-N, FR-N or NFR-N
holder: who is claiming — a branch name, agent id, or person
holder_kind: branch | agent | human
holder_repo: absolute path of the repo owning the branch, if any
note: free-text reason, kept on renewal unless replaced
project: also move a finding to in_progress (requirements never project)
allow_terminal: claim even if the entity is already resolved
Returns:
outcome: claimed | already_mine | held_by_other | entity_terminal | undetermined. On held_by_other the holder fields name the INCUMBENT. On undetermined, re-issue the identical call — it converges on already_mine.
Inputs: holder (required), entity_id (required), note, project, +3 more. Returns: object
claims_held_byB
Everything a given holder currently holds.
Inputs: holder (required). Returns: object
claims_listA
List live claims, optionally filtered by kind, holder or holder kind.
Args:
kind: Entity kind filter (e.g. "finding"), or omit for every kind.
holder: Holder name filter, or omit for every holder.
holder_kind: Holder kind filter (e.g. "branch"), or omit for every kind.
limit: Max rows (default 200). 0 means NO rows; a negative value is an error (it used to mean "no limit").
Inputs: kind, limit, holder, holder_kind. Returns: object
claims_releaseA
Release a claim. Authorized on the full (holder, holder_kind, holder_repo) triple — pass exactly what you claimed with.
Returns:
outcome: released | not_yours | not_claimed | undetermined. A projected status is restored only if it still holds the projected value, so finished work is never resurrected.
Inputs: holder (required), entity_id (required), reason, holder_kind, +2 more. Returns: object
claims_who_holdsB
Who currently holds this entity, if anyone.
Inputs: entity_id (required). Returns: object
codebench_deleteA
Delete a single run or all runs for a benchmark.
Args:
run_id: Delete a specific run (e.g. "BE-1")
benchmark: Delete all runs for a benchmark name
Inputs: run_id, benchmark. Returns: object
codebench_importA
Import benchmark results from CSV or JSON.
CSV convention: first column is the row label, remaining columns are metric names with finite numeric values.
JSON convention: array of objects, first key is the row label, rest are metric keys with finite numeric values.
Each (row label, metric) pair may appear only once per import, and NaN/Infinity are refused: a non-finite measurement is not one.
Args:
benchmark: Benchmark name (e.g. "search-perf")
csv_data: CSV string (header + data rows). Provide csv_data OR json_data.
json_data: JSON array string. Provide csv_data OR json_data.
date: Run date (default: today, ISO format YYYY-MM-DD)
tags: Optional tags (e.g. ["nightly", "v2.1"])
meta: Optional metadata (e.g. {"git_sha": "abc123", "ci_url": "..."})
Inputs: benchmark (required), date, meta, tags, +2 more. Returns: object
codebench_listA
List benchmarks or runs.
Without benchmark: lists all benchmark names with run counts. With benchmark: lists runs for that benchmark.
Args:
benchmark: If provided, list runs for this benchmark
last_n: Limit to last N runs. Requires benchmark — supplying last_n without one is an ERROR, because benchmark names have no runs to limit and the argument would otherwise be silently discarded. 0 means NO runs; omit it for no limit. A negative value is an error.
Inputs: last_n, benchmark. Returns: object
codebench_queryA
Query and pivot benchmark results.
group_by="row": original table shape (row_labels as rows, metrics as columns). Returns one table per run.
group_by="run": trend view (runs as rows, metrics as columns). Returns one table per row_label.
Args:
benchmark: Benchmark name to query
runs: Specific run IDs (default: all matching)
date_from: Start date filter (inclusive, YYYY-MM-DD)
date_to: End date filter (inclusive, YYYY-MM-DD)
metrics: Which metrics to include (default: all)
rows: Which row_labels to include (default: all)
group_by: Pivot axis — "row" or "run"
last_n: Limit to last N runs by date. 0 means NO runs; omit it for no limit. A negative value is an error (it used to mean "no limit").
format: Output — "json" or "csv"
Inputs: benchmark (required), rows, runs, format, +5 more. Returns: object
codemerge_abandonA
Close a session for good, so its files stop blocking everyone else.
This is the way OUT of a session that will not be merged under its own lock — including the case an agent hits routinely: the branch was integrated by some other route (a merge harness holding its own lock), so codemerge_merge refuses with reason='main_moved' and the session is stranded in 'active', which codemerge_finish will not accept. Until it is abandoned, its claimed files are reported as conflicts to every later session, with no expiry — so closing it is what keeps codemerge_check worth consulting.
What it does, stated exactly: the session's claim rows are NOT deleted, they stop being reported, because the conflict query selects on session status. The merge lock is released only if this session holds it.
Re-issuing it is safe: a second call on an already-abandoned session changes nothing but the timestamps. A 'done' session is REFUSED — that would erase the record of a merge that succeeded — and an unknown session_id is an error.
Args:
session_id: The merge session ID. Whatever session is NAMED is the one closed; nothing ties it to the caller, so passing a stale or mistyped id closes someone else's work.
Inputs: session_id (required). Returns: object
codemerge_checkA
Check for overlapping file claims with other sessions.
Returns whether the session is clean to proceed, lists any conflicts, and records the current main HEAD for CAS comparison at merge time.
Args:
session_id: The merge session ID
main_changed_files: Files changed on main since base (optional, for overlap check)
Inputs: session_id (required), main_changed_files. Returns: object
codemerge_claimB
Claim a file as being modified by this session.
Args:
session_id: The merge session ID
file_path: File path being modified (relative to repo root)
Inputs: file_path (required), session_id (required). Returns: object
codemerge_claimsA
List all files a session has claimed, in claim order.
Args:
session_id: The merge session ID.
Inputs: session_id (required). Returns: result
codemerge_finishA
Finish a merge session and release the lock.
Call this after codemerge_merge() returned proceed=true; the session must
be in 'merging' state or this refuses in BOTH directions of success.
Args:
session_id: The merge session ID
success: True if the merge succeeded (status→done). False if the git merge/cherry-pick failed (status→active): the lock is released and the session stays alive so it can try again. False does NOT close the session — use codemerge_abandon for that.
Inputs: session_id (required), success. Returns: object
codemerge_mergeA
Acquire the merge lock and proceed with merging.
Uses compare-and-swap on main HEAD to prevent races. If main has moved since check, returns proceed=False with reason='main_moved'. If another session holds the lock, returns proceed=False with reason='lock_held'.
Args:
session_id: The merge session ID
expected_main_head: The main HEAD SHA recorded during codemerge_check
Inputs: session_id (required), expected_main_head (required). Returns: object
codemerge_sessionsA
List merge sessions with claim counts.
Args:
status: Filter by status ('active', 'merging', 'done', 'abandoned'). Omit for all sessions.
Inputs: status. Returns: result
codemerge_startA
Start a new merge session for a branch.
Args:
session_id: Unique identifier for this merge session
branch: Git branch name being merged
description: Human-readable description of the work
base_commit: Git commit SHA this branch diverged from
repo_root: Repo root path (default: cwd)
allow_restart: If True, reuse this session_id when its previous session is finished — 'abandoned' or 'done'. Restarting DELETES that session's file claims, so re-claim anything you still need. It does NOT restart a live one: starting over an 'active' or 'merging' session is an error whether or not this is set.
Inputs: branch (required), session_id (required), repo_root, base_commit, +2 more. Returns: object
codemerge_statusA
Dashboard summary: session counts by status, total active claims, and who (if anyone) holds the merge lock.
Inputs: no named parameters. Returns: object
codesweep_addA
Add items to a sweep. Atomic upsert: existing items have their
recurrence_count bumped instead of being silently skipped, their
last_seen updated, and their archive flag cleared (R5: re-detected
archived items un-archive automatically).
Args:
sweep_ref: Sweep ID (SW-N) or name
items: Item identifiers to add
tags: Optional tags applied to this batch (overwrite on bump)
Returns: {sweep_id, added, recurrence_bumped, duplicates_skipped (alias)}
Inputs: items (required), sweep_ref (required), tags. Returns: object
codesweep_archiveA
Archive a sweep. Archived sweeps are excluded from codesweep_list by default.
For entry-level archive, use codesweep_archive_items.
Args:
sweep_ref: Sweep ID (SW-N) or name
Inputs: sweep_ref (required). Returns: object
codesweep_archive_itemsA
Selectively archive entries within a sweep (soft-delete).
Archived entries are excluded from codesweep_next, codesweep_status
totals, and default codesweep_list_items. They remain matchable by
codesweep_add for recurrence detection — re-adding un-archives them
with recurrence_count carried forward (R5 invariant).
At least one filter is required.
Args:
sweep_ref: Sweep ID (SW-N) or name
items: Specific item identifiers to archive. An EXPLICITLY EMPTY list selects nothing; passing it together with where_status or older_than is an ERROR, because those filters would then be silently ignored. Omit items entirely to archive by filter.
where_status: Archive entries currently in this state
older_than: Duration spec — '30d', '2w', '6m', '1y'. Compares against the entry's last activity timestamp.
reason: Free-form reason recorded on each archived entry
Inputs: sweep_ref (required), items, reason, older_than, +1 more. Returns: object
codesweep_createA
Create a new sweep for batch iteration over items.
Args:
name: Optional human-readable name (must be unique)
description: What this sweep is for
default_batch_size: Default items per batch (default: 10)
lifecycle: Ordered list of allowed states (default ["pending","done"]). For retro-style workflows: ["DETECTED","CONFIRMED","ESCALATED", "POSTPONED","RESOLVED","DROPPED"].
terminal_states: States that count as "processed" (default ["done"]).
transitions: Optional dict[state, list[allowed_next_state]] for DAG-constrained lifecycles. None = unconstrained transitions.
Inputs: name, lifecycle, description, transitions, +2 more. Returns: object
codesweep_listB
List all sweeps with summary counts.
Args:
include_archived: Include archived sweeps (default: false)
Inputs: include_archived. Returns: object
codesweep_list_itemsA
List items in a sweep with optional filters.
Args:
sweep_ref: Sweep ID (SW-N) or name
state: Filter to a specific state
tag: Filter to items having this tag
include_archived: Include archived entries alongside live ones
archived_only: Show only archived entries. Mutually exclusive with include_archived: passing both as true is an error (it used to mean archived-only, returning FEWER entries than include_archived alone).
limit: Max number of entries to return. 0 means NO entries; omit it for no limit. A negative value is an error (it used to mean "no limit").
Inputs: sweep_ref (required), tag, limit, state, +2 more. Returns: object
codesweep_markA
Mark items by state transition.
Args:
sweep_ref: Sweep ID (SW-N) or name
items: Item identifiers to mark
processed: Legacy mode — True maps to first terminal state, False to first non-terminal state. Omit it entirely (the default) to get the same effect as True. MUTUALLY EXCLUSIVE with
state: sending both is an error, including when the two happen to agree, becausestatenames one state andprocessednames a class of them.state: Explicit target state. Validated against the sweep's
lifecycleandtransitionsDAG (if declared). Mutually exclusive withprocessed— send one or the other, never both.
Inputs: items (required), sweep_ref (required), state, processed. Returns: object
codesweep_nextA
Get next batch of unprocessed (non-terminal, non-archived) items in insertion order.
Args:
sweep_ref: Sweep ID (SW-N) or name
limit: Batch size (overrides sweep default). 0 means NO items; omit it to use the sweep's own default batch size. A negative value is an error (it used to mean "no limit").
tags: Filter to items matching any of these tags
Inputs: sweep_ref (required), tags, limit. Returns: object
codesweep_statusA
Sweep overview — total/processed/remaining/archived counts, per-tag and
per-state breakdowns. Archived entries are excluded from total/processed/
remaining and reported separately as archived.
Args:
sweep_ref: Sweep ID (SW-N) or name
Inputs: sweep_ref (required). Returns: object
getA
Fetch a single finding by ID with full body (description, severity, status, tags, meta, timestamps, commit refs).
The result carries an anchor summary saying where this card's code is
NOW: state tells a card with no anchor apart from one whose anchor was
retracted by hand and from one where capture looked and had nothing to
grab, and loc_status/moved_file/path report the resolution against
HEAD when it ran.
Raises a not-found error if the ID does not exist. For lenient batch
lookup that silently drops missing IDs, use query(ids=[...]).
Args:
finding_id: The finding ID (e.g. CB-1383)
resolve_anchors: Resolve the anchor against the repository (default ON — the cost is bounded by one card). Pass False for a read that must not spawn a process: no git available, no repository, or a caller that only wants to know whether an anchor exists at all.
Inputs: finding_id (required), resolve_anchors. Returns: object
grouping_citationsA
Connected components of the hand-written CB-id reference graph.
READ-ONLY, and an ANNOTATION of what people already wrote — no link here is inferred. Every edge carries the field it came from and the quoted context of its first mention. A node whose degree exceeds hub_degree is a landmark many work units point AT, not a member of one: it does not transmit connectivity and is reported as an ANCHOR with its citers, so the components either side of it stay separate. References to ids outside the population are COUNTED as dangling, never dropped.
A pair you have declared DIFFERENT — relations_relate(a, "distinct_from", b) — stops being joined BY THAT REFERENCE, in either order; retracting the declaration brings the grouping back. The citation itself still appears, with its quoted context, in suppressed_edges: your declaration corrects the conclusion this tool draws from the reference, not the fact that somebody wrote it. Read still_grouped on each entry before trusting the separation — dropping one reference does not cut a graph, so if a third card cites both they are in one component regardless, and that flag (counted by still_grouped_total) is how the report tells you your declaration lost rather than leaving you to notice. This is the ONLY place distinct_from suppresses: grouping_filing's declared lineage (split_from / split_children) is untouched, because overriding one declaration with another is a different question from overriding a guess.
Args:
status: Narrow/widen the population (default: live statuses; "all")
category: Restrict to one category
hub_degree: Degree above which a node becomes an anchor (default 3, chosen on the outcome — see DEFAULT_HUB_DEGREE); None disables hub splitting and returns the raw components
component_limit: Max components returned (totals stay visible)
member_limit: Max members per component (edges follow the page)
anchor_limit: Max anchors returned (default 25)
orphan_limit: Max orphan ids returned (default 50)
Inputs: status, category, hub_degree, anchor_limit, +3 more. Returns: object
grouping_filingA
Split lineages and shared filing events (sprint / plan).
READ-ONLY. LINEAGE IS TRAVERSED, NOT GROUPED: A → B → C is one lineage
with depths, and its links resolve against EVERY card in the tracker,
not just the population, so a fixed middle card does not sever the
chain. A lineage surfaces when at least one member is in the population;
a lineage value naming no card is reported unresolved, not dropped.
Filing events are grouped by exact value within the population.
Args:
status: Narrow/widen the population (default: live statuses; "all")
category: Restrict to one category
lineage_limit: Max lineages returned (totals stay visible)
event_limit: Max filing events returned (totals stay visible)
Inputs: status, category, event_limit, lineage_limit. Returns: object
grouping_tagsA
Tag pivots: counts, co-occurrence, and near-duplicate taxonomy strings.
READ-ONLY. Co-occurrence carries Jaccard beside the raw count, because
on a corpus with one 390-card tag the raw count ranks that tag's pairs
first no matter how weak the association is. variants spans tags AND
categories in one namespace: the taxonomy drift is not confined to one
column (process_improvement / process-improvement).
Args:
status: Narrow/widen the population (default: live statuses; "all")
category: Restrict to one category
min_pair_count: Drop tag pairs co-occurring fewer times (default 2)
tag_limit: Max tags returned (totals stay visible)
pair_limit: Max pairs returned (default 50; None for all)
Inputs: status, category, tag_limit, pair_limit, +1 more. Returns: object
mark_branch_onlyA
Flag an item as living on a feature branch (not yet integrated). Called by worktree-setup.sh when a branch is created.
Args:
item_ref: The item id (e.g. CB-5).
branch_name: Git branch holding the work.
Inputs: item_ref (required), branch_name (required). Returns: object
mark_integratedA
Mark an item as merged to main. Sets done_commit, status='done', clears branch_only. Called by worktree-finish.sh.
Args:
item_ref: The item id (e.g. CB-5).
commit: Commit SHA where the work landed on main.
Inputs: commit (required), item_ref (required). Returns: object
milestone_add_itemA
Attach an item (bug / requirement / external) to a milestone.
Args:
milestone_id: Target milestone slug.
item_kind: 'bug' (CB-N), 'requirement' (FR-N), or 'external'.
item_ref: The id of the underlying entity (must exist for bug/req).
size: 'large' (worktree+sprint), 'small' (1-2h), 'triage' (minutes).
priority: Lower = higher priority. Default 100.
acceptance: Markdown acceptance criteria. Required for size='large'.
linked_frs: Optional list of FR ids to link (used by pull_next eligibility).
Inputs: item_ref (required), item_kind (required), milestone_id (required), size, +3 more. Returns: object
milestone_audit_queryA
Audit log query with filters. Returns most-recent rows first.
Args:
milestone_id: Filter by milestone slug.
item_ref: Filter by item id.
actor: Filter by actor.
since: ISO datetime — only rows at or after this time.
limit: Max rows (default 200). 0 means NO rows; a negative value is an error (it used to mean "no limit").
Inputs: actor, limit, since, item_ref, +1 more. Returns: result
milestone_closeA
Close a release milestone. Refuses if items are unfinished, on a branch, or have unresolved blockers. Streams cannot be closed.
Args:
id: Milestone slug (must be kind='release').
force: Override the close-gate (still won't close streams). Audit-logged.
reason: Audit reason for the close.
Inputs: id (required), force, reason. Returns: object
milestone_createA
Create a new milestone.
Args:
id: Slug identifier, e.g. 'release/1.2' or 'stream/security'.
kind: 'release' or 'stream'. Streams never close.
description: Short charter for the milestone.
target_date: ISO date (e.g. '2026-06-30'). Optional, releases only.
Inputs: id (required), kind (required), description (required), target_date. Returns: object
milestone_deferC
Move an item to stream/maintenance (or another milestone) and mark it deferred.
Args:
item_ref: The item to defer.
to_milestone: Destination (default 'stream/maintenance').
reason: Optional audit reason.
Inputs: item_ref (required), reason, to_milestone. Returns: object
milestone_listA
List milestones with optional filters.
Args:
kind: 'release' or 'stream'.
state: 'open' / 'closing' / 'shipped' / 'archived'.
Inputs: kind, state. Returns: result
milestone_move_itemB
Move an item to a different milestone.
Args:
item_ref: The item to move (e.g. CB-5).
to_milestone: Destination milestone slug.
reason: One-line audit reason.
Inputs: item_ref (required), to_milestone (required), reason. Returns: object
milestone_reconcileA
One-time repair (CB-107) for stream items whose source finding or requirement resolved before the status-change hook existed (CB-26). The hook keeps new resolutions in sync; this is the retroactive fix for rows it never saw.
DRY RUN BY DEFAULT — without apply=true nothing is written, and the response still lists every candidate transition it WOULD make. This is a bulk mutation, and CLAUDE.md is explicit that a repair tool which writes by default is how it becomes an accident, so the dry-run default is load-bearing and this wrapper refuses to weaken it.
Args:
apply: Must be a literal JSON boolean. Defaults to false (dry run). Rejected outright for any other JSON type (a string like "false", a number, null) rather than coerced by truthiness — an MCP client sends JSON over the wire, and Python's bool("0") and bool("false") are both True, which would silently turn a client's intended dry run into a write (CB-82's class of bug). CB-151: strict typing refuses this at the pydantic boundary before the tool body runs, closing the one hole the previous union annotation + isinstance check left open (1.0/0.0 coerced to a real bool before isinstance could see it).
Inputs: apply. Returns: object
milestone_set_statusB
Set an item's status. Records done_commit if status is terminal.
Args:
item_ref: The item id (e.g. CB-5).
status: open / in_progress / done / deferred / dismissed.
commit: SHA where the work landed on main (recorded for terminal status).
reason: Optional audit reason.
Inputs: status (required), item_ref (required), commit, reason. Returns: object
milestone_statusA
Detailed rollup for one milestone: item counts by status / size, blockers, branch-only items, days to target.
Args:
id: Milestone slug.
Inputs: id (required). Returns: object
milestone_updateA
Update mutable fields of a milestone. id and kind are immutable.
Args:
id: Milestone slug.
description: New description (or None to skip).
target_date: New ISO target date (or None to skip).
state: New state (open / closing / shipped / archived).
Inputs: id (required), state, description, target_date. Returns: object
pull_nextA
Claim the next eligible item for the calling agent. Returns the item dict or None if nothing eligible.
Priority: stream/security > release/* (earliest target_date) > stream/triage > stream/maintenance.
Args:
agent_id: Stable id for the calling agent. Used as actor in audit.
capacity: Dict like {'large':1,'small':2,'triage':5}. Defaults to those values if not provided.
Inputs: agent_id (required), capacity. Returns: result
queryA
Search and filter findings. Returns structured results.
Supports lookup by ID via id= (single) or ids= (batch). Missing IDs
are silently absent from the result so the caller can diff. For a strict
single-ID fetch that errors on miss, use get instead.
Args:
id: Fetch a single finding by exact ID (e.g. CB-1383)
ids: Fetch multiple findings by ID list; missing IDs are skipped
status: Filter by status (open, in_progress, fixed, not_a_bug, wont_fix, stale, deferred). Aliases accepted. Use 'deferred' to find items with active blockers.
severity: Filter by severity (critical, high, medium, low)
category: Filter by exact category
file: Filter by file path (substring match)
source: Filter by source (claude, ruff, human, etc.). Compares the FIRST reporter — the column is frozen at first report (BT-4); later observations' sources live only in the occurrence ring (meta.occurrences[*].source), and an imported observation's ring source can be a peer tracker's.
tag: Filter by tag (finds findings containing this tag)
meta_key: Filter by metadata key existence. Reads the row's AUTHORED top-level meta (the column), never the occurrence ring.
meta_value: Filter by metadata value (requires meta_key; same authored top-level meta as meta_key — ring meta is not consulted)
commit: Matches the first-report column OR any occurrence in the ring (prefix match, hex validated) — "what was observed on this commit" (CB-128).
staleness_checkuses the NEWEST ring entry instead: a different question.ref: Filter by reported_at_ref (exact match, never prefix) — matches the first-observed or manually assigned release ref (BT-4); per-occurrence refs in the ring are not consulted.
fingerprint: Filter by identity fingerprint (exact match)
group_by: Group results by: severity, category, status, file, source, tag, meta: (source groups count FIRST reporters — the column is frozen at first report).
tagandmeta:<key>do NOT partition the population: a card with two tags is counted under both, and a card carrying no value on the axis is in no group at all. The response therefore always carriespopulation,ungrouped_rows,multi_group_rowsandnonscalar_value_rowsbesidegroups; the counts sum to the population only while the last three are 0.meta:<key>reads the AUTHORED top-level meta, likemeta_keydoes, and a key holding.,[,]or"is REFUSED — SQLite cannot tell such a name from a path (CB-167). A key that is absent, JSON null, or holds an object/array is ungrouped rather than invented. OVERLAPSgrouping_tagsDELIBERATELY and differently: that tool is a tag census with pair co-occurrence overstatus/categoryonly; this is a distribution that composes with every filter on this tool.limit: Max results. A limit you PASS is always honoured: 0 means NO results, and it means that with
id/idstoo (CB-158 — an id list used to raise any smaller limit to fit itself, solimit=0came back full). A negative value is an error (it used to mean "no limit"). Omit it and the page size is 100, widened to fit anidslist so a batch lookup returns every id it asked for.offset: Pagination offset
resolve_anchors: Resolve each result's location anchor against the repository HEAD, so a card whose code moved reports its new path. OFF by default because it costs 2-4 git calls per ANCHORED row and this is the primary read path; the cheap half — whether a card carries an anchor at all, and the refusal token when capture found nothing to grab — is in every result either way.
getresolves one card by default.
Inputs: id, ids, ref, tag, +13 more. Returns: object
recentA
Findings TOUCHED at or after a date — the one call for "what closed since".
WHAT THIS MEASURES: updated_at, the time of the LAST WRITE to the row,
and not the moment the finding was closed. There is no close timestamp
anywhere in the schema. A status change moves updated_at, and so do a
re-tag, an AUTHORED meta patch, a severity re-triage, an append_note,
and a DEDUPLICATED OBSERVATION — a repeat report bumps the occurrence
count and stamps updated_at while the status stays exactly where it was.
AUTHORED is doing work in that sentence since CB-230: the tracker's own
housekeeping (refreshing a card's code anchor) writes meta WITHOUT
stamping, so a maintenance pass no longer floods this reader with every
card it touched.
So recent(since=..., status="fixed") means "cards that are fixed NOW and
were touched since that date", NOT "cards closed since that date". The
error is ONE-SIDED: false positives are possible, misses are not, because
closing a card always writes updated_at — guaranteed rather than merely
true, since housekeeping is refused outright when it carries a status.
Rows come back newest touch first, with rowid breaking the whole-second
ties updated_at produces, so a paged walk is stable.
Args:
since: Lower bound on
updated_at, INCLUSIVE. 'YYYY-MM-DD' or 'YYYY-MM-DDTHH:MM:SSZ'. REQUIRED — an unparseable value is refused rather than defaulted, because a silently widened window answers a question nobody asked.status: Filter by status (open, in_progress, fixed, not_a_bug, wont_fix, stale). Aliases accepted. Omit for every status. The
deferredpseudo-status ofqueryis NOT accepted here and is refused rather than ignored — usequeryfor it.limit: Max results (default 100). 0 means NO results. A negative value is an error (it used to mean "no limit"). The neighbouring
querytool answers the same argument the same way.offset: Pagination offset
Inputs: since (required), limit, offset, status. Returns: object
relations_queryA
List relations touching a finding, in both directions.
Args:
entity_id: Finding to look up (e.g. "CB-5"). Omit to list all.
rel: Filter by relation.
rel="distinct_from"alone lists every live suppression, which is worth reviewing periodically.include_retracted: Include tombstoned edges (default: false)
Inputs: rel, entity_id, include_retracted. Returns: object
relations_relateA
Assert a typed relation between two findings.
Args:
src_id: Source finding (e.g. "CB-5"). For duplicate_of this is the LOSER — the card that dies.
rel: duplicate_of, split_from, follow_up_of, found_during, distinct_from, or related_to.
dst_id: Target finding. For duplicate_of this is the SURVIVOR.
source: Who is asserting this (e.g. "owner", "goldset-2026-08-17").
note: Optional reasoning.
Inputs: rel (required), dst_id (required), source (required), src_id (required), +1 more. Returns: object
relations_unrelateA
Retract a relation. Tombstones it — the row and its history remain.
Args:
src_id: Source finding
rel: The relation to retract
dst_id: Target finding
retracted_by: Who is retracting it
reason: Why
Inputs: rel (required), dst_id (required), src_id (required), retracted_by (required), +1 more. Returns: object
release_itemA
Free agent capacity for an item.
Args:
item_ref: The item id (e.g. CB-5).
status: 'done' (terminal) or 'abandoned' (returns item to 'open').
commit: SHA where the work landed (recorded if status='done').
Inputs: item_ref (required), commit, status. Returns: object
reqs_addB
Add a requirement.
Args:
req_id: Requirement ID (e.g. FR-001)
description: What the system shall do
section: Section name (e.g. "1.10 Document Sorting")
priority: must, should, or could
status: planned, partial, implemented, verified, superseded, obsolete
source: Where this requirement came from (e.g. Take 26, NEW)
test_coverage: Test file name(s)
tags: Optional tags
meta: Optional metadata
Inputs: req_id (required), description (required), meta, tags, +5 more. Returns: object
reqs_batch_embedA
Store embeddings for multiple requirements at once.
Same preconditions as reqs_embed: you compute the vectors yourself and pass finished numbers, the requirement text never reaches this tool, and codebugs stores them locally and sends them nowhere.
Every vector in one call must have the same number of components as every other vector in the call AND as the vectors already stored in this tracker; empty vectors, non-numbers, NaN and infinity are refused. The self-consistency rule is a separate one: in an empty tracker there is nothing to compare against, so without it a single call could create the mixed state the rules exist to prevent.
Args:
embeddings: Dict mapping requirement ID to float vector
Inputs: embeddings (required). Returns: object
reqs_embedA
Store an embedding vector for a requirement.
YOU compute the embedding, in your own process, and pass the finished numbers here. This tool never receives the requirement's text. codebugs stores the vector in its own local SQLite file and sends it nowhere. (Scope, stated precisely rather than loudly, because a promise wider than its check is worse than no promise. The route above is the claim. A test enforces two narrower things beside it: this package's own source imports none of the socket-opening modules that test lists, and it imports nothing at all from outside the package and the standard library without a declared, reasoned entry — so a network client nobody anticipated is still refused. Neither says "codebugs cannot reach the network": the MCP transport your client is talking over is a separate layer, and it is not covered.)
Because there is no embedding provider inside codebugs, nothing here knows the "right" dimensionality — it is whatever the first stored vector had. So the vector is refused if it is empty, contains a non-number, contains NaN or infinity, or has a different number of components than the vectors already stored in this tracker. Each of those would otherwise break reqs_search_similar: a mismatched width makes it unable to score the other rows, and a NaN makes a row drop out of every result with no error at all.
Once a tracker holds vectors of one width you cannot switch embedding model: there is no clear-and-re-embed operation in this package. reqs_embedding_stats reports which widths are actually present.
Args:
req_id: Requirement ID
embedding: Float vector. Any dimensionality, but the SAME one for every requirement in a given tracker.
Inputs: req_id (required), embedding (required). Returns: object
reqs_embedding_statsA
Report on embedding coverage --- how many requirements have embeddings.
This tool takes no input at all, so it is not a privacy surface and carries no precondition block of its own; that is said explicitly rather than left as an omission a reader has to interpret.
Beyond coverage it reports dimensions --- which vector widths this
tracker actually holds, and how many rows each --- plus mixed, true
when there is more than one. That is the channel for noticing a tracker
that received vectors from two different embedding models:
reqs_search_similar silently excludes rows of a width other than your
query's, and being able to see the split here is what keeps that from
looking like "nothing is similar". Both keys are always present; an
empty dimensions list means no vectors are stored, never that the
check did not run.
Inputs: no named parameters. Returns: object
reqs_getA
Fetch a single requirement by ID with full body.
Raises a not-found error if the ID does not exist. For lenient batch
lookup that silently drops missing IDs, use reqs_query(ids=[...]).
Args:
req_id: The requirement ID (e.g. FR-001)
Inputs: req_id (required). Returns: object
reqs_importA
Import requirements from a REQUIREMENTS.md file.
Parses markdown tables with columns: | ID | Requirement | Priority | Status | Source | Test Coverage |
Uses INSERT OR REPLACE, so re-importing updates existing entries.
Args:
markdown_path: Path to the REQUIREMENTS.md file
Inputs: markdown_path (required). Returns: object
reqs_queryA
Search and filter requirements.
Supports lookup by ID via id= (single) or ids= (batch). Missing IDs
are silently absent from the result. For a strict single-ID fetch that
errors on miss, use reqs_get.
Args:
id: Fetch a single requirement by exact ID (e.g. FR-001)
ids: Fetch multiple requirements by ID list; missing IDs are skipped
status: Filter by status (planned, partial, implemented, verified, superseded, obsolete, deferred). Use 'deferred' to find requirements with active blockers.
priority: Filter by priority (must, should, could)
section: Filter by section (substring match)
search: Search in description and ID
source: Filter by source (substring match)
tag: Filter by tag
group_by: Group by: section, status, priority, source
limit: Max results. A limit you PASS is always honoured: 0 means NO results, and it means that with
id/idstoo (CB-158 — an id list used to raise any smaller limit to fit itself, solimit=0came back full). A negative value is an error (it used to mean "no limit"). Omit it and the page size is 100, widened to fit anidslist so a batch lookup returns every id it asked for.offset: Pagination offset
Inputs: id, ids, tag, limit, +7 more. Returns: object
reqs_search_similarA
Find requirements semantically similar to a query.
Pass a query embedding (from the same model used to embed requirements). You compute it yourself; no text is sent anywhere by this tool, and the query vector is not stored.
Requirements whose stored vector has a different number of components than your query are EXCLUDED from the search rather than compared, so one foreign vector can no longer make the whole search fail. That also means they are invisible here: if you get fewer results than you expect, call reqs_embedding_stats, which reports which widths this tracker holds. A query vector that is empty or contains NaN or infinity is refused, because it would match nothing and return an empty list indistinguishable from an empty tracker.
Returns requirements ranked by cosine similarity.
Args:
query_embedding: Query vector
limit: Max results (default 10)
min_similarity: Minimum cosine similarity (default 0.3)
status: Optional status filter
Inputs: query_embedding (required), limit, status, min_similarity. Returns: result
reqs_statsA
Aggregated requirement counts by status x priority.
Args:
group_by: Group by: status, priority, section, source
Inputs: group_by. Returns: object
reqs_summaryA
Dashboard overview --- status breakdown, priority split, section progress, requirements without tests, deferred counts. Start here.
Inputs: no named parameters. Returns: object
reqs_updateA
Update a requirement's status, description, or metadata.
Args:
req_id: Requirement ID (e.g. FR-001)
status: New status: planned, partial, implemented, verified, superseded, obsolete
description: Updated description
priority: Updated priority: must, should, could
section: Updated section name
test_coverage: Updated test file reference
notes: Notes (stored in meta.notes). REPLACES the stored notes wholesale. If meta_update also carries a "notes" key, the meta_update value is the one that lands — see meta_update.
tags: Replace tags
meta_update: Merge metadata keys. notes and meta_update compose over ONE dict: notes replaces first, meta_update merges LAST. So passing both notes= and meta_update={"notes": ...} in a single call is neither an error nor a refusal — meta_update wins the collision, on every key it names. That precedence is deliberate: meta_update names the storage key directly, so it is the repair path for a key no other argument can write. Unlike the findings
updatetool there is no append_note here, so there is no third writer.
Inputs: req_id (required), tags, notes, status, +5 more. Returns: object
reqs_verifyA
Verify requirements for issues.
Runs automated checks to find problems:
tests: do referenced test files actually exist?
ids: duplicate IDs, numbering gaps
status: contradictions (description says superseded but status says planned)
Args:
checks: List of checks to run (default: all). Options: tests, ids, status
project_dir: Project root for test file verification (default: cwd)
Inputs: checks, project_dir. Returns: object
similarity_checkA
Preview what the file-time annotator would stamp for an observation.
Applies EXACTLY the resolver's policy: same candidate pool (live + dismissed, same category, newest 500), same minimum-text-length gate, same scoring. Advisory only — nothing is written.
Args:
description: The observation's description text
category: Category to search within (annotation never crosses it)
meta: Optional observation meta (volatile values are stripped from the text before scoring, same as fingerprint normalization)
threshold: Minimum similarity in [0, 1] (default 0.7, calibrated)
limit: Max matches returned (default 5)
Inputs: category (required), description (required), meta, limit, +1 more. Returns: object
similarity_reportA
Offline grouping scrub: similarity families as auditable evidence.
Families are connected components with min_pair_score and edge lists — the dry run for any future backfill/merge (the blocked backfill card); no merge is performed or implied. Default population is LIVE rows; pass status= to widen (the sentinel "all" means every status). Wall-clock is quadratic per category block (~115k pair comparisons on a 3k-row tracker); prefer the CLI for very large trackers.
Args:
threshold: Minimum similarity in [0, 1] (default 0.7, calibrated)
category: Restrict to one category
status: Widen/narrow the population (default: live statuses; "all")
family_limit: Max families returned (totals stay visible)
member_limit: Max members per family (totals stay visible)
Inputs: status, category, threshold, family_limit, +1 more. Returns: object
staleness_checkA
Check if findings are stale by comparing against git history.
Staleness is checked against each finding's NEWEST observation: a
deduplicated re-observation records its commit in the occurrence ring,
and that commit — not the frozen first-report reported_at_commit —
is what the file is compared from; findings with no ring fall back to
the first report. Each result carries checked_commit, the commit the
verdict was computed against.
Returns file_status for each finding:
current: file unchanged since finding was reported
modified: file changed but still exists
renamed: file was renamed/moved
deleted: file no longer exists
unknown: can't determine, with a
reasonnaming which question could not be answered — no provenance data, an unreachable commit, a path outside this repository's worktree, a path the reported commit never contained (a glob, free text, or a file added later), an empty or malformed value, a path that is neither a file nor a directory, or a git/stat call that failed
Args:
finding_id: Check a single finding (e.g. CB-1)
status: Filter by finding status (default: open)
category: Filter by category
file: Filter by file path (substring match)
resolve_anchors: Also resolve each finding's location anchor against HEAD, so a record says where the reported LINES are now and not only what became of the file. OFF by default: this query permits ten thousand rows and already spends git per file. Every record carries the cheap half of the summary either way — whether the card has an anchor at all.
Inputs: file, status, category, finding_id, +1 more. Returns: object
statsA
Aggregated cross-tabulated counts.
Args:
group_by: Group by: severity, category, status, file, source, tag, meta: (source buckets count FIRST reporters — the column is frozen at first report, BT-4). With
tagormeta:<key>the rows do NOT partition: a card with two tags is cross-tabulated under both of them, so the totals exceed the number of findings.population,ungrouped_rows,multi_group_rowsandnonscalar_value_rowsride besidegroupson every axis and are what make that readable. A meta key holding.,[,]or"is refused (CB-167).
Inputs: group_by. Returns: object
summaryA
Dashboard overview — open/resolved counts, severity breakdown, top categories, hottest files, deferred counts. Start here for orientation.
Inputs: no named parameters. Returns: object
triage_dismissA
Mark a triage item as dismissed. Propagates to the underlying entity: bug → finding 'not_a_bug'; requirement → requirement 'obsolete'; external → no propagation.
Args:
bug_id: The item id (e.g. CB-5).
reason: Required one-line audit reason.
Inputs: bug_id (required), reason (required). Returns: object
triage_inboxA
List open items in stream/triage, oldest first.
Args:
limit: Max rows (default 50).
Inputs: limit. Returns: result
triage_promoteA
Move a triage item to a target milestone.
Args:
bug_id: The item id (e.g. CB-5).
to_milestone: Destination milestone slug.
size: 'large' / 'small' / 'triage'. Default 'small'.
acceptance: Required for size='large'.
priority: Lower = higher priority. Default 100.
linked_frs: FR ids linked to this item (required for size='large' bugs in release milestones to be pull-eligible).
Inputs: bug_id (required), to_milestone (required), size, priority, +2 more. Returns: object
updateA
Update a finding's status, severity, notes, tags, or metadata.
Args:
finding_id: The finding ID (e.g. CB-1)
status: New status: open, in_progress, fixed, not_a_bug, wont_fix, stale. Aliases accepted: done/resolved/implemented/closed → fixed, wontfix → wont_fix, invalid → not_a_bug, active/working/in-progress → in_progress
severity: Re-triage the finding: critical, high, medium, or low. Case-insensitive, but no aliases — unlike status, "crit" and "P0" are refused.
notes: REPLACES the notes wholesale, discarding whatever was there. To add to an existing record without destroying it, use append_note instead. If meta_update also carries a "notes" key, the meta_update value is the one that lands — see meta_update for why that is deliberate.
append_note: Appends a newline-joined line, preserving the prior notes. This is the safe way to add evidence to a long-lived card.
tags: Replace tags list
meta_update: Merge additional metadata keys. The three meta-writing arguments compose over ONE dict, in this order: notes replaces, append_note then extends that replacement, and meta_update merges LAST. So passing both notes= and meta_update={"notes": ...} in a single call is neither an error nor a refusal — meta_update wins the collision, on every key it names. That precedence is deliberate rather than incidental: meta_update names the storage key directly, which makes it the repair path for keys no other argument can reach (similar_to, category_minted, fingerprint_refusals), and a stamp no argument could overwrite would be an unrepairable one.
reported_at_ref: Update version/tag label (e.g. "v2.1.0"). This is the SANCTIONED manual mutation of an observation-frozen column (BT-4): observations never move it, this call does — a release is tagged after filing.
Inputs: finding_id (required), tags, notes, status, +4 more. Returns: object
wip_statusA
Snapshot of agent_capacity. agent_id=None returns all agents.
Args:
agent_id: Filter to one agent (None = all).
Inputs: agent_id. Returns: result
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
TDQS
Scored across 83 tools
Domain prefixes (reqs_, milestone_, blockers_, codemerge_, codebench_, codesweep_, grouping_, relations_, anchor_) make most tools' purposes clear at a glance, and overlapping-sounding pairs like codebench_query vs reqs_query are cleanly separated by prefix. A few genuinely confusable pairs remain: milestone_defer and blockers_add are two different mechanisms for deferring an item; mark_integrated and milestone_set_status both record completion; and claims_claim vs codemerge_claim use the same verb for two different claim systems.
The dominant pattern is consistent verb_noun within prefixed domains (milestone_create, milestone_move_item, codesweep_archive_items, relations_unrelate), and the code* family is uniformly codemerge_/codebench_/codesweep_. The main flaw is a two-tier system: core finding tools are unprefixed (add, get, query, update, stats, summary) while every other domain is prefixed, and there are verb/noun collisions such as claims_claim, codemerge_claim (verb) vs codemerge_claims (noun).
83 tools is more than three times the 25+ 'too many' threshold, creating a heavy tool-selection surface for any agent even with good organization. The server bundles nine distinct subdomains (findings, requirements, milestones, blockers, claims, triage, merge sessions, benchmarks, sweeps) that would typically be split across separate servers. Each tool does earn its place within its own subdomain, so the count is excessive rather than chaotic.
Every subdomain has near-complete lifecycle coverage: findings and requirements have create/read/update/analytics plus verification and embedding; milestones, blockers, claims, codemerge, and sweeps all have full workflows including audit and repair paths. The absence of hard delete is a deliberate audit-trail design, with status-based retirement covering removal. The one notable gap is that categories_normalize references export-csv/restore-csv backup tools that are not exposed in the MCP surface.
Maintenance
Related MCP Connectors
Give your AI agent a persistent map of your project's structure, dependencies, and bugs.
Persistent engineering memory designed to follow developers across coding agents.
Project memory for coding agents: requirements, decisions, code graph and delivery telemetry.
Shared memory for coding agents. Stop re-explaining your codebase every session.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceProvides AI coding assistants with persistent project memory to retain architectural decisions, code patterns, and domain knowledge across sessions. It stores data locally in a SQLite database, allowing agents to remember, recall, and manage project-specific context using full-text search.4 npmApache 2.0
- AlicenseAqualityDmaintenancePersistent shared memory for AI coding agents. Stores facts as entity/key/value triples with hybrid semantic search, task checkpoints, and conflict resolution — shared across Claude Code, Codex CLI, and GitHub Copilot.16235 npm5AGPL 3.0
- AlicenseNot gradedqualityAmaintenanceProvides persistent, searchable memory and knowledge capture for AI-assisted development, enabling agents to retain decisions, bugs, and patterns across sessions and projects.53 PyPIMIT
- AlicenseNot gradedqualityCmaintenanceGives AI coding agents persistent, branch-aware memory and a dependency-tracked task graph by storing decisions, lessons, and tasks as plain JSON and Markdown committed directly into the repository. Agents can record and fuzzy-search past decisions, dump instant project context, and create, claim, complete, and query tasks whose completion automatically unblocks downstream work.MIT