| 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": "..."})
|
| 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"
|
| 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.
|
| codebench_deleteA | Delete a single run or all runs for a benchmark. Args: |
| 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.
|
| 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)
|
| 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. |
| blockers_resolveA | Cancel or manually resolve a blocker. Args: |
| 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: |
| claims_releaseA | Release a claim. Authorized on the full (holder, holder_kind, holder_repo)
triple — pass exactly what you claimed with. Returns: |
| claims_who_holdsB | Who currently holds this entity, if anyone. |
| claims_held_byB | Everything a given holder currently holds. |
| 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").
|
| 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.
|
| 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.
|
| 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.
|
| 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.
|
| 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
|
| 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.
|
| 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).
|
| summaryA | Dashboard overview — open/resolved counts, severity breakdown,
top categories, hottest files, deferred counts. Start here for orientation. |
| categoriesA | List all existing categories with counts.
Call this before adding findings to reuse consistent category names. |
| 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.
|
| 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.
|
| relations_unrelateA | Retract a relation. Tombstones it — the row and its history remain. Args: |
| 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)
|
| 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)
|
| 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)
|
| 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)
|
| 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.
|
| 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").
|
| 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").
|
| 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.
|
| codemerge_claimB | Claim a file as being modified by this session. Args: |
| 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)
|
| 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: |
| 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.
|
| 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: |
| codemerge_sessionsA | List merge sessions with claim counts. Args: |
| codemerge_statusA | Dashboard summary: session counts by status, total active claims,
and who (if anyone) holds the merge lock. |
| codemerge_claimsA | List all files a session has claimed, in claim order. Args: |
| 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.
|
| 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).
|
| milestone_listA | List milestones with optional filters. Args: |
| milestone_statusA | Detailed rollup for one milestone: item counts by status / size,
blockers, branch-only items, days to target. Args: |
| 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).
|
| 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.
|
| 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.
|
| 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").
|
| triage_inboxA | List open items in stream/triage, oldest first. Args: |
| 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: |
| 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).
|
| 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.
|
| 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').
|
| wip_statusA | Snapshot of agent_capacity. agent_id=None returns all agents. Args: |
| 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).
|
| 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: |
| mark_integratedA | Mark an item as merged to main. Sets done_commit, status='done',
clears branch_only. Called by worktree-finish.sh. Args: |
| 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.
|
| 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.
|
| 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
|
| 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.
|
| 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
|
| 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: |
| reqs_statsA | Aggregated requirement counts by status x priority. Args: |
| reqs_summaryA | Dashboard overview --- status breakdown, priority split,
section progress, requirements without tests, deferred counts. Start here. |
| 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)
|
| 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: |
| 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: |
| 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: |
| 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
|
| 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. |
| 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)
|
| 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)
|
| 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.
|
| 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)} |
| 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
|
| 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.
|
| 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: |
| codesweep_archiveA | Archive a sweep. Archived sweeps are excluded from codesweep_list by default. For entry-level archive, use codesweep_archive_items. Args: |
| 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
|
| 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").
|
| codesweep_listB | List all sweeps with summary counts. Args: |