Skip to main content
Glama

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 low and re-seen critical stops hiding from a --severity critical query. Lowering it back is a deliberate update, 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_fix or not_a_bug does 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_resolve reports 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_claim gives 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 where will 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, recommended

pip 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 codebugs runs. Re-run pipx install --force ./codebugs to pick the changes up, or pass --editable at 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 fresh pipx install, typically the newest release each declared range admits; for pip into 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 the newest-sdk job in .github/workflows/ci.yml runs 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 init

This 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 init at 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. init refuses to do this unless you pass --force.

  • Git worktrees share the main repo's tracker. A worktree's .git is 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. init refuses 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 own git worktree list reports 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-dir repo whose git directory is itself named .git looks 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). Run init in 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 spawns

Resolution 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 it

where 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

summary, add, query, categories

reqs

Functional requirements (FR-N)

reqs_summary, reqs_add, reqs_verify, reqs_search_similar

blockers

"X is blocked by Y" dependency graph

blockers_add, blockers_check

sweep

Batch iteration with state machines

codesweep_create, codesweep_next, codesweep_mark

bench

Performance benchmark snapshots

codebench_import, codebench_query

merge

Parallel-agent merge serialization

codemerge_start, codemerge_claim

milestones

Releases, streams, capacity-aware pull

pull_next, milestone_status, milestone_close

provenance

Staleness vs git history, commit trailers

staleness_check

claims

Which agent holds a finding or requirement

claims_claim, claims_release, claims_who_holds

loc

Where in the code a finding is, across edits

anchor_resolve, anchor_recapture

similarity

Near-duplicate findings, as a dry run

similarity_check, similarity_report

relations

Typed, retractable links between findings

relations_relate, relations_query

grouping

Reads the axes the tracker stores but never exposed

grouping_citations, grouping_tags, grouping_filing

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

summary

Dashboard overview — start here for orientation

add

File an observation. Creates, bumps, reopens or refiles — read dedup_action to see which

batch_add

File several observations at once

update

Change status, severity, notes, tags or metadata (append_note adds, notes replaces)

query

Search/filter with pagination and group-by

get

Fetch one finding by id, with its full body and occurrence history

recent

Findings touched at or after a date — the one call for "what closed since"

stats

Cross-tabulated counts (severity x category/file/status)

categories

List existing categories — call before add for consistency

categories_normalize

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

reqs_summary

Requirements dashboard — start here

reqs_add

Add a requirement (FR-001, priority, status, test coverage)

reqs_update

Change status, description, priority, test coverage

reqs_query

Search/filter by status, priority, section, free text

reqs_get

Fetch a single requirement by ID with full body

reqs_stats

Cross-tabulated counts (status x priority)

reqs_verify

Automated checks: ghost test files, duplicate IDs, status contradictions

reqs_import

Import from REQUIREMENTS.md (parses markdown tables)

reqs_embed / reqs_batch_embed

Store embedding vectors

reqs_search_similar

Semantic search across requirements

reqs_embedding_stats

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.md

Blockers — "X is blocked by Y", with auto-unblock

MCP tools:

Tool

Purpose

blockers_add

Defer an item until another item resolves, a date passes, or a manual signal

blockers_query

List blockers filtered by item, dependency, trigger type

blockers_check

Find currently-actionable items (all blockers satisfied)

blockers_resolve

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

milestone_status

Rollup for one milestone (counts by status/size, branch-only, blocked, days to target)

milestone_list

List milestones, filter by kind / state

milestone_create

Create a release or stream

milestone_update

Mutate description, target_date, state

milestone_add_item

Attach a bug / requirement / external ref to a milestone

milestone_move_item

Move an item between milestones

milestone_set_status

Open / in_progress / done / dismissed / deferred

milestone_defer

Move to stream/maintenance with status='deferred'

milestone_close

Refuses if open / branch-only / blocked items remain (force overrides, except for streams)

milestone_audit_query

Full state-transition history

triage_inbox

Items waiting to be triaged

triage_dismiss

Reject a triage item; propagates to underlying entity

triage_promote

Move a triage item to a target milestone

pull_next

Atomically claim the next eligible item for the calling agent

release_item

Free agent capacity (status='done' or 'abandoned')

wip_status

Snapshot of agent_capacity per agent

mark_branch_only

Flag an item as living on a feature branch only

mark_integrated

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 work

  • stream/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.1

A 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

codesweep_create

Create a new sweep (optional lifecycle=[...], terminal_states=[...], transitions={...} for state machines)

codesweep_add

Add items. Atomic upsert: existing items bump recurrence_count, refresh last_seen, un-archive

codesweep_next

Next batch of unprocessed (non-terminal, non-archived) items

codesweep_mark

Transition state (legacy processed=True still works)

codesweep_status

Progress overview

codesweep_archive / codesweep_archive_items

Soft-delete

codesweep_list_items / codesweep_list

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-pass

With 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 30d

Bench — performance snapshots over time

MCP tools:

Tool

Purpose

codebench_import

Import benchmark results (file or inline)

codebench_query

Filter and trend metrics across runs

codebench_list

List recorded runs

codebench_delete

Remove a run

Merge — parallel-agent merge serialization

MCP tools:

Tool

Purpose

codemerge_start

Open a merge session

codemerge_claim

Claim files for the session (advisory file-level claims)

codemerge_check

Check for overlapping claims against main

codemerge_merge

Mark merge in progress (acquires the global merge lock with TTL)

codemerge_finish

Release the lock

codemerge_claims

List all files a session has claimed, in claim order

codemerge_sessions

List merge sessions with claim counts

codemerge_status

Dashboard: session counts by status, total active claims

codemerge_abandon

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

anchor_resolve

Where a finding's code is now — current, moved, moved_file, lost, ambiguous, or unknown with a reason

anchor_recapture

Rebuild stored anchors from the git object store. Dry run by default

similarity_check

Preview what the file-time annotator would stamp for an observation

similarity_report

Similarity families as auditable evidence — a scrub you read, never a merge it performs

relations_relate

Assert a typed relation (duplicate_of, follow_up_of, split_from, ...) between two findings

relations_query

List relations touching a finding, in both directions

relations_unrelate

Retract a relation. Tombstones it — the row and its history remain

grouping_citations

Connected components of the hand-written CB-id reference graph

grouping_tags

Tag pivots: counts, co-occurrence, near-duplicate taxonomy strings

grouping_filing

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:

  1. File the observation with add (or batch_add for several at once). Call categories first when you are unsure of the naming.

  2. Read what came back before doing anything else. dedup_action says whether this created a new card, bumped or reopened an existing one, or refiled one already dismissed. attention is 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.

  3. The code location is anchored at file time when the observation names one; anchor_resolve reports whether that anchor still points at live code.

  4. Close the card with update CB-N --status fixed once 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:

  1. Triage: AI calls triage_inbox → triage_dismiss non-bugs, triage_promote real items to release/1.1 (with linked_frs for the ones that need an FR row).

  2. Execution: Each parallel agent calls pull_next(agent_id=..., capacity=...) → claims the next eligible item.

  3. After landing: mark_integrated(item, commit) → release_item(item, status='done').

  4. 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

id

text

Auto-generated (CB-1, CB-2, ...) or user-provided

severity

text

critical, high, medium, low

category

text

User-defined (e.g. n_plus_one, missing_validation, security:xss)

file

text

File path relative to project root

status

text

open, in_progress, fixed, not_a_bug, wont_fix, stale

description

text

What's wrong

source

text

claude, ruff, human, mypy, ...

tags

json

Array of strings for ad-hoc grouping

meta

json

lines, module, rule_code, cwe_id, ...

reported_at_commit, reported_at_ref

text

Provenance for staleness checks

Requirements

Field

Type

Description

id

text

User-provided (FR-001, NFR-001, ...)

section, description, priority, status, source, test_coverage

text

per-row metadata

embedding

blob

Optional float32 vector for semantic search

tags, meta

json

Milestones

Table

Purpose

milestones

Slug (release/1.1, stream/triage), kind, state, target_date, description

milestone_items

(milestone_id, item_kind, item_ref) link, size, priority, status, acceptance, branch_only, done_commit

milestone_audit

Append-only log: actor, action, from_state → to_state, reason, timestamp

agent_capacity

Per-agent WIP (large_held, small_held, triage_held, last pull/release)

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

item_id, item_type

text

Blocked entity (e.g. CB-5 / finding)

blocked_by, blocked_by_type

text

Dependency (or null for date/manual triggers)

trigger_type

text

entity_resolved, date, manual

trigger_at

text

UTC datetime for date triggers

reason

text

Human explanation

Sweeps

Table

Purpose

codesweeps

sweep_id, name, description, lifecycle, terminal_states, transitions DAG

codesweep_items

(sweep_id, item) unique key; state, recurrence_count, first_seen, last_seen, archived_at

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     2

If 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 coverage

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-2

Then 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.py uses MCPServer, the class that replaced FastMCP in the 2.0 SDK, so an older mcp will 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 tools
addA

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 categories first 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 description does 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 the file argument above; a full "path/file.py:1850" token, whose path must name the same file as file or 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 as meta.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 lines argument above, and that is the spelling to use. meta.lines remains 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 file column)

  • 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 untouched

  • apply: Write the rebuilt anchors (default: report only)

  • force_tombstone: Overwrite a loc: null tombstone

  • include_unanchored: Also take rows carrying no loc key at all (the backfill population). Leaves tombstones alone; still a dry run unless apply is set

  • limit: 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 file column)

  • project_dir: The repository the anchors resolve against. Omitting it reports no_root rather 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 about

  • limit: 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. lines is per-member and works exactly as it does on add: 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 own file) 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's meta.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, because state names one state and processed names a class of them.

  • state: Explicit target state. Validated against the sweep's lifecycle and transitions DAG (if declared). Mutually exclusive with processed — 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_check uses 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). tag and meta:<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 carries population, ungrouped_rows, multi_group_rows and nonscalar_value_rows beside groups; the counts sum to the population only while the last three are 0. meta:<key> reads the AUTHORED top-level meta, like meta_key does, 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. OVERLAPS grouping_tags DELIBERATELY and differently: that tool is a tag census with pair co-occurrence over status/category only; 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/ids too (CB-158 — an id list used to raise any smaller limit to fit itself, so limit=0 came 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 an ids list 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. get resolves 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 deferred pseudo-status of query is NOT accepted here and is refused rather than ignored — use query for it.

  • limit: Max results (default 100). 0 means NO results. A negative value is an error (it used to mean "no limit"). The neighbouring query tool 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/ids too (CB-158 — an id list used to raise any smaller limit to fit itself, so limit=0 came 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 an ids list 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 update tool 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 reason naming 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 tag or meta:<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_rows and nonscalar_value_rows ride beside groups on 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

    A3.6/5.0

    Scored across 83 tools

    Disambiguation4/5

    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.

    Naming Consistency4/5

    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).

    Tool Count2/5

    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.

    Completeness4/5

    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

    ActivityMaintained
    ResponsivenessNo recent community issues

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      Not graded
      quality
      D
      maintenance
      Provides 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 npm
      Apache 2.0
    • A
      license
      A
      quality
      D
      maintenance
      Persistent 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.
      16
      235 npm
      5
      AGPL 3.0
    • A
      license
      Not graded
      quality
      A
      maintenance
      Provides persistent, searchable memory and knowledge capture for AI-assisted development, enabling agents to retain decisions, bugs, and patterns across sessions and projects.
      53 PyPI
      MIT
    • A
      license
      Not graded
      quality
      C
      maintenance
      Gives 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