Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Changelog

All notable changes to bicameral-mcp are tracked here. Format loosely follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## 0.4.5 — 2026-04-14

### Fixed

- **Ingest now stamps a baseline `content_hash` at HEAD for every grounded
region.** Previously `ingest_payload` only computed a hash when the caller
explicitly passed `commit_hash` in the payload, which the MCP `bicameral_ingest`
handler never did — so every bulk transcript ingest persisted empty hashes
and decisions were permanently stuck in `pending`. Now `ingest_payload`
resolves HEAD from `repo_path` when no commit_hash is supplied, computes a
baseline hash for every region, and derives the intent's initial status from
that hash. Freshly ingested decisions are born `reflected` when their
grounded code exists at HEAD.
- **Empty-hash regions from older ledgers are now self-healed.** `handle_link_commit`
runs a repo-scoped backfill sweep before the normal drift loop, walking any
code regions with an empty `content_hash` and handing them to
`HashDriftAnalyzer`, which adopts the current git state as the baseline and
flips the owning intents to `reflected`. No forced migration, no new tooling —
just call `bicameral_status` or any other handler that triggers a
`link_commit` and legacy ledgers heal themselves.
- **`HashDriftAnalyzer.analyze_region` self-heals missing baselines.** When
`stored_hash == ""` and the analyzer can compute a real hash at the requested
ref, it returns `reflected` with the new hash as the baseline instead of the
old `ungrounded` verdict. Used by both the new backfill path and the regular
drift sweep.

### Added

- Per-region status aggregation on intents: an intent with multiple code
regions now adopts the loudest status across them (drifted > reflected >
pending > ungrounded), so a single drifted region always raises an alarm
even if other regions still reflect.
- `SurrealDBLedgerAdapter.backfill_empty_hashes(repo_path, drift_analyzer=...)`
— public method to run the backfill sweep on demand. Idempotent and scoped
by repo, so multi-repo SurrealDB instances stay isolated.
- `ledger.queries.get_regions_without_hash(client, repo="")` — helper query
used by the backfill sweep to find legacy regions.
- New test module `tests/test_phase1_l1_wiring.py` with four regression
scenarios: ingest→reflected, edit→drifted, phantom range→not reflected,
and legacy empty-hash backfill→reflected.

### Migration

No manual action required. Existing ledgers backfill themselves on the next
`bicameral_status`, `bicameral_link_commit`, or any other tool that drives a
`link_commit`. Users whose files aren't touched by subsequent commits can
also simply re-run their original bulk ingest — v0.4.5 stamps hashes at ingest
time, so re-ingestion produces correct status for every grounded decision
without any further work.

---

## 0.4.4 — 2026-04-13

- Submodule bump for grounding reuse + coverage loop (Phase 3 of the
code-locator drift fix plan).

## 0.4.3 — 2026-04-12

- Few-shot `bicameral-ingest` skill update (`ff2eff7`).

## 0.4.2 — 2026-04-11

- Skills bundle + CLAUDE.md context files.

## 0.4.1 — 2026-04-10

- Ingest pipeline hardening: input contracts, payload normalization, freshness
guards.

## 0.4.0 — 2026-04-08

- Event-sourced collaboration + BicameralContext request-scoped snapshot
isolation.
189 changes: 103 additions & 86 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,22 @@
```
██████╗ ██╗ ██████╗ █████╗ ███╗ ███╗███████╗██████╗ █████╗ ██╗
██╔══██╗██║██╔════╝██╔══██╗████╗ ████║██╔════╝██╔══██╗██╔══██╗██║
██████╔╝██║██║ ███████║██╔████╔██║█████╗ ██████╔╝███████║██║
██╔══██╗██║██║ ██╔══██║██║╚██╔╝██║██╔══╝ ██╔══██╗██╔══██║██║
██████╔╝██║╚██████╗██║ ██║██║ ╚═╝ ██║███████╗██║ ██║██║ ██║███████╗
╚═════╝ ╚═╝ ╚═════╝╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝

┌────────────────┐ ┌────────────────┐
│ DECISIONS │ ◀── drift ──▶ │ CODE │
│ what was │ detection │ what actually │
│ said │ │ shipped │
└────────┬───────┘ └────────┬───────┘
│ ┌──────────┐ │
└──────────▶│ ledger │◀─────────────┘
└──────────┘
local · deterministic
```

# Bicameral MCP

[![PyPI version](https://img.shields.io/pypi/v/bicameral-mcp)](https://pypi.org/project/bicameral-mcp/)
Expand All @@ -14,15 +33,14 @@ Bicameral MCP is a local-first [Model Context Protocol](https://spec.modelcontex
## Table of Contents

- [The Problem](#the-problem)
- [Collaboration Modes](#collaboration-modes)
- [Tool Composition](#tool-composition)
- [How It Works](#how-it-works)
- [Architecture](#architecture)
- [Quickstart](#quickstart)
- [MCP Tools Reference](#mcp-tools-reference)
- [Tool Composition](#tool-composition)
- [Testing](#testing)
- [Collaboration Modes](#collaboration-modes)
- [Configuration](#configuration)
- [Roadmap](#roadmap)
- [Contributing](#contributing)
- [License](#license)

Expand All @@ -44,6 +62,88 @@ Bicameral's core value is **drift detection** -- knowing that a decision made th

---

## Collaboration Modes

Bicameral runs in one of two modes, set during `bicameral-mcp setup` or in `.bicameral/config.yaml`:

| | Solo (default) | Team |
|---|---|---|
| **Who** | Individual testing or evaluation | Any mix of roles -- devs, PMs, designers |
| **Data** | Local DB only | Local DB + git-committed event files |
| **Shared via** | Nothing -- fully isolated, zero impact on teammates | Normal `git push` / `git pull` |
| **Merge conflicts** | N/A | Zero -- per-user directories, append-only files |

**Solo mode** is ideal for trying Bicameral without affecting your team's workflow. All data stays in a gitignored local DB -- no event files, no commits, no side effects. Switch to team mode when you're ready to share.

**Team mode** enables cross-role collaboration through git. A PM ingests a PRD and sprint transcript; when a developer pulls, `bicameral.search` surfaces those decisions as coding context and `bicameral.status` shows what still needs implementation. The PM never touches the code; the developer never sits through the meeting. The decision graph is the handoff.

```
.bicameral/
├── events/ ← committed to git (shared decisions)
│ ├── pm@co.com/ ← PM's ingested PRDs and transcripts
│ └── dev@co.com/ ← developer's commit syncs
├── config.yaml ← committed (mode: solo | team)
└── local/ ← gitignored (materialized state)
```

**Switching modes:** Set `mode: team` or `mode: solo` in `.bicameral/config.yaml`. No data migration needed.

---

## Tool Composition

The nine tools compose into three workflows that follow the natural lifecycle of a decision — **captured in a meeting, pulled as context during coding, checked at review time.** Each workflow below uses the same running example (a checkout-flow sprint) so you can see a single decision move through the pipeline.

### 1. Ingestion — after a meeting

> **Scenario:** Your PM wraps a 30-minute sprint planning in `#product-planning`. The transcript contains three decisions. You paste it into Claude and say "ingest this."

```jsonc
// bicameral.ingest
{
"source": "slack",
"title": "Sprint 14 Planning — 2026-03-12",
"decisions": [
{ "title": "Apply 10% discount on orders over $100",
"description": "Marketing confirmed at offsite. No upper bound." },
{ "title": "Cache user sessions in Redis, not local memory",
"description": "Arch review: local memory breaks horizontal scaling." },
{ "title": "Rate-limit checkout to 100 req/min per user",
"description": "Legal/compliance ask. Not yet built." }
]
}
```

**Outcome.** The discount rule and the Redis session decision anchor to real symbols (`pricing/discount.py:DiscountService.calculate`, `auth/session_store.py:SessionStore.put`) and are born `reflected`. Auto-grounding can't find code for the rate-limit rule — because it hasn't been written yet — so it lands as `ungrounded`. The ledger now knows a decision exists with no corresponding code, and the next `bicameral.status` call will show exactly that.

---

### 2. Pre-flight — before writing new code

> **Scenario:** A dev picks up the ticket "add rate limiting to checkout." Before writing a single line, they ask Claude for context.

```jsonc
// bicameral.search
{ "query": "rate limit checkout", "max_results": 5 }
```

**Outcome.** Before writing any code, the dev sees the prior rate-limit decision *with its compliance rationale*, learns that it's still `ungrounded` (so they're the first implementer), and discovers an adjacent `pricing/discount.py:DiscountService.calculate` region their new code will need to coexist with. No re-litigating a decided rule, no Slack archaeology, no ambushing the PM in standup tomorrow.

---

### 3. Code review — before merging

> **Scenario:** Three weeks later, a different dev opens PR #241 with a 50-line diff touching `pricing/discount.py`. Reviewer asks Claude "any drift in this file?"

```jsonc
// bicameral.drift
{ "file_path": "pricing/discount.py", "use_working_tree": false }
```

**Outcome.** The reviewer learns that `DiscountService.calculate:42-67` has drifted from the Sprint 14 Planning decision — threshold raised $100 → $500, rate lowered 10% → 5%. Either the change is intentional, in which case a new decision must be ingested before merge, or it's accidental and gets reverted. The conversation happens at PR time, not three sprints later in an incident post-mortem.

---

## How It Works

### Status Derivation Model
Expand Down Expand Up @@ -406,49 +506,6 @@ No LLM provider credentials needed -- all retrieval is deterministic.

---

## Tool Composition

The nine tools are designed to compose into three primary workflows:

### Pre-flight (before coding)

```
bicameral.search("add rate limiting to checkout")
--> surfaces prior constraints, related decisions, and their code regions
--> auto-syncs ledger to HEAD before returning results
```

Use this to check for prior art and constraints before writing new code. Prevents CONSTRAINT_LOST and REPEATED_EXPLANATION.

### Code review (before merging)

```
bicameral.drift("payments/processor.py")
--> surfaces all decisions touching symbols in this file
--> flags any where the code has diverged from recorded intent

bicameral.status(filter="drifted")
--> full drift report across the entire codebase
```

Use this in pull request review to catch unintentional drift. The `use_working_tree` parameter controls whether comparison is against disk (pre-commit) or HEAD (PR review).

### Ingestion (after a meeting)

```
bicameral.ingest(payload)
--> extracts intents, auto-grounds to code symbols
--> advances source cursor for incremental sync

bicameral.link_commit("HEAD")
--> syncs latest commit state into the ledger

bicameral.status(since="2025-03-20")
--> shows what's reflected vs. pending since the meeting
```

---

## Testing

Bicameral has 42 test files organized into three phases, all using real adapters with `SURREAL_URL=memory://` (embedded, in-process SurrealDB -- no external services required).
Expand All @@ -470,34 +527,6 @@ Phase 3 tests produce JSON artifacts (`test-results/e2e/`) with full tool respon

---

## Collaboration Modes

Bicameral runs in one of two modes, set during `bicameral-mcp setup` or in `.bicameral/config.yaml`:

| | Solo (default) | Team |
|---|---|---|
| **Who** | Individual testing or evaluation | Any mix of roles -- devs, PMs, designers |
| **Data** | Local DB only | Local DB + git-committed event files |
| **Shared via** | Nothing -- fully isolated, zero impact on teammates | Normal `git push` / `git pull` |
| **Merge conflicts** | N/A | Zero -- per-user directories, append-only files |

**Solo mode** is ideal for trying Bicameral without affecting your team's workflow. All data stays in a gitignored local DB -- no event files, no commits, no side effects. Switch to team mode when you're ready to share.

**Team mode** enables cross-role collaboration through git. A PM ingests a PRD and sprint transcript; when a developer pulls, `bicameral.search` surfaces those decisions as coding context and `bicameral.status` shows what still needs implementation. The PM never touches the code; the developer never sits through the meeting. The decision graph is the handoff.

```
.bicameral/
├── events/ ← committed to git (shared decisions)
│ ├── pm@co.com/ ← PM's ingested PRDs and transcripts
│ └── dev@co.com/ ← developer's commit syncs
├── config.yaml ← committed (mode: solo | team)
└── local/ ← gitignored (materialized state)
```

**Switching modes:** Set `mode: team` or `mode: solo` in `.bicameral/config.yaml`. No data migration needed.

---

## Configuration

| Variable | Default | Description |
Expand All @@ -510,18 +539,6 @@ All data is stored locally. The embedded SurrealDB instance runs in-process -- n

---

## Roadmap

### CodeGenome Identity Layer

The current system grounds decisions via symbol names and file paths. This works well for stable codebases, but location-based anchoring breaks when code is renamed, moved, or heavily refactored.

The next major evolution -- **CodeGenome** -- replaces location-based anchoring with identity-based grounding: structural signatures and behavioral profiles that persist across renames, moves, and AI-driven rewrites. This resolves what we call the **Auto-Grounding Problem**: intent anchored to identity rather than location.

Where Bicameral today maps `intent --> symbol_name --> file:line`, CodeGenome will map `intent --> structural_identity --> any_location`, making the decision graph resilient to large-scale codebase reorganization.

---

## Contributing

Contributions are welcome. To get started:
Expand Down
11 changes: 11 additions & 0 deletions handlers/link_commit.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,17 @@ async def _reground_ungrounded(ctx) -> int:


async def handle_link_commit(ctx, commit_hash: str = "HEAD") -> LinkCommitResponse:
# Self-heal legacy regions with empty content_hash from pre-v0.4.5
# ingests. Scoped to ctx.repo_path so multi-repo SurrealDB instances
# stay isolated; no-op once every region in this repo has a baseline.
try:
if hasattr(ctx.ledger, "backfill_empty_hashes"):
await ctx.ledger.backfill_empty_hashes(
ctx.repo_path, drift_analyzer=ctx.drift_analyzer,
)
except Exception as exc:
logger.warning("[link_commit] backfill failed: %s", exc)

result = await ctx.ledger.ingest_commit(
commit_hash, ctx.repo_path, drift_analyzer=ctx.drift_analyzer,
)
Expand Down
Loading