feat: v0.5.3 — gap attribution, auto link_commit sync, skill aggression + README overhaul#37
Conversation
…on + README overhaul - gap answer attribution: agent_session/manual spans now backfill git email as speaker instead of inheriting document source - PostToolUse hook: setup_wizard installs bicameral.link_commit auto-sync after git commits/merges/pulls into user repos - preflight lazy HEAD catch-up: syncs ledger before search when HEAD has advanced since last link_commit - ingest/preflight skills: much more aggressive trigger conditions; preflight fires on any implementation question - README: OSS overhaul — spec-alignment narrative, what-setup-installs table with removal guide, real dashboard screenshot, corrected local dev path - bump RECOMMENDED_VERSION to 0.5.3 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
📝 WalkthroughWalkthroughThe PR introduces a live decision dashboard UI with real-time updates, adds "gap" status to history decisions, implements dashboard notifications across multiple handlers (ingest, link_commit, preflight), adds Claude Code hook installation, updates the version to 0.5.3, and substantially rewrites the README with refreshed tool lists and configuration documentation. Changes
Sequence Diagram(s)sequenceDiagram
participant Client as Client/User
participant Server as MCP Server
participant DashSvc as Dashboard Service
participant EventStream as EventSource Stream
rect rgba(100, 150, 200, 0.5)
Note over DashSvc,EventStream: Dashboard Startup
Server->>DashSvc: get_dashboard_server() on first tool call
DashSvc->>DashSvc: Start HTTP sidecar on free port
DashSvc-->>Server: Return URL & port
end
rect rgba(150, 200, 100, 0.5)
Note over Client,DashSvc: Live Dashboard Updates
Client->>DashSvc: GET /history (fetch decision ledger)
DashSvc-->>Client: Render dashboard with features & decisions
Client->>EventStream: Open EventSource('/events')
end
rect rgba(200, 150, 100, 0.5)
Note over Server,DashSvc: Handler Notifications
Server->>Server: handle_ingest() or handle_link_commit()
Server->>DashSvc: notify_dashboard(ctx)
DashSvc->>DashSvc: Append event to stream
end
rect rgba(200, 100, 150, 0.5)
Note over EventStream,Client: Real-time Sync
DashSvc->>EventStream: POST new JSON event
EventStream->>Client: Deliver via SSE
Client->>Client: Render updated row/feature
end
sequenceDiagram
participant Tool as bicameral.preflight Tool
participant Handler as handle_preflight()
participant Git as Git Repo
participant LinkCommit as handle_link_commit()
participant Search as Search/Brief Chain
rect rgba(180, 150, 200, 0.5)
Note over Tool,Handler: Preflight Initialization
Tool->>Handler: Invoke preflight handler
Handler->>Git: Read current HEAD SHA
end
rect rgba(150, 200, 180, 0.5)
Note over Handler,LinkCommit: Lazy HEAD Catch-up (if needed)
alt HEAD differs from last_sync_sha
Handler->>LinkCommit: await handle_link_commit(ctx, "HEAD")
LinkCommit->>Handler: Update ledger state
else No change
Handler->>Handler: Skip sync (same HEAD)
end
end
rect rgba(200, 180, 150, 0.5)
Note over Handler,Search: Main Preflight Flow
Handler->>Search: Proceed with search/brief chaining
Search-->>Handler: Return compliance insights
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes The PR introduces a new dashboard feature with corresponding HTML/server integration, extends decision contracts with new status semantics, modifies multiple handlers with a dashboard notification pattern, implements speaker backfill logic, adds Claude Code hook installation, and substantially rewrites documentation. The changes are heterogeneous—spanning UI, contracts, multiple handler modifications, and setup logic—each requiring separate reasoning about correctness, interaction, and integration. Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 10
🧹 Nitpick comments (4)
handlers/link_commit.py (1)
254-258: Optional: log the swallowed dashboard-notify failure at debug.The silent
except Exception: passmakes this path invisible whendashboard.serveris broken or missing — a handy breadcrumb for diagnosing a dashboard that's stopped updating. Keeps behavior identical (still non-blocking).♻️ Proposed tweak
try: from dashboard.server import notify_dashboard await notify_dashboard(ctx) - except Exception: - pass + except Exception as exc: + logger.debug("[link_commit] dashboard notify failed: %s", exc)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@handlers/link_commit.py` around lines 254 - 258, Replace the silent "except Exception: pass" that swallows notify_dashboard errors with a debug-level log that records the exception but keeps the path non-blocking; specifically, in the try/except around from dashboard.server import notify_dashboard and await notify_dashboard(ctx) catch Exception as e and call a debug logger (e.g., logging.getLogger(__name__).debug or an existing module logger) to log a short message like "notify_dashboard failed" including the exception (use exc_info=True or include e) so failures are visible at debug level without changing behavior.handlers/update.py (1)
116-121: Consider logging the hook-reinstall failure at debug.A silent
except Exception: passhere means a user who upgrades and expects the new PostToolUse hook gets no signal when_install_claude_hooksthrows (e.g. permissions on.claude/settings.json, non-git directory). A one-linelogger.debugpreserves fail-open behavior while leaving a breadcrumb for support.♻️ Proposed tweak
try: from setup_wizard import _install_claude_hooks _install_claude_hooks(Path(repo_path)) - except Exception: - pass + except Exception as exc: + logger.debug("[update] hook reinstall failed: %s", exc)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@handlers/update.py` around lines 116 - 121, Replace the silent swallow of exceptions when calling _install_claude_hooks(Path(repo_path)) with a debug log that records the exception and context; keep the try/except to preserve fail-open behavior but in the except block call logger.debug with a clear message (e.g., "failed to reinstall Claude hooks for repo {repo_path}") and include the exception info (or use logger.debug(..., exc_info=True)) so failures in _install_claude_hooks are recorded without changing behavior.handlers/ingest.py (1)
331-345: Private helper imported across packages — promote_get_git_emailor wrap it.
_get_git_emailis imported fromevents.writer(leading underscore = module-private) and used in bothhandlers/ingest.pyandadapters/ledger.py, making it a de-facto public contract. Keep the API honest by renaming toget_git_email(removing the underscore) or exposing a thin public wrapper in a shared module. No functional concern with the caching sentinel—usingNoneto mean "not fetched yet" works correctly even when_get_git_emailreturns""or"unknown"(cache populates, retry is skipped).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@handlers/ingest.py` around lines 331 - 345, The code imports the module-private helper _get_git_email from events.writer and uses it in multiple places; make its access explicit by either renaming/_promoting it to a public function get_git_email in events.writer or by adding a thin public wrapper (e.g., get_git_email) in a shared module and update callers; replace imports of events.writer._get_git_email in handlers.ingest (the loop that sets span["speakers"]) and adapters.ledger with the public get_git_email, keep the existing caching logic (_git_email_cache) unchanged, and ensure callers import the new public symbol instead of the underscored one.handlers/preflight.py (1)
204-216: Promote_read_current_head_shaor reuseresolve_head— cross-module private import requires clarification.
_read_current_head_shais imported fromhandlers.link_commitdespite its leading underscore, which signals module-private. Now that preflight relies on it, it's a de facto cross-handler contract that should be explicit. The function is equivalent toresolve_headfromledger.status(both usegit rev-parse), so you could either:
- Drop the underscore from
_read_current_head_sha(public export fromhandlers/link_commit.py), or- Switch to
resolve_headfromledger.status(which already owns HEAD resolution logic).Secondary concern:
git rev-parse HEADnow runs three times per lazy sync:
- Here in preflight (line 210)
- Inside
_sync_cache_lookup(line 148 of handlers/link_commit.py)- Inside
_store_sync_cache(line 176 of handlers/link_commit.py)Pass the already-read
live_headSHA intohandle_link_committo collapse this to one subprocess call.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@handlers/preflight.py` around lines 204 - 216, Preflight imports the module-private _read_current_head_sha from handlers.link_commit and re-runs git rev-parse multiple times; make the contract explicit and avoid duplicate subprocess calls by either (A) promoting _read_current_head_sha to a public export (rename to read_current_head_sha) in handlers.link_commit and keep using it, or (B) switch preflight to call resolve_head from ledger.status instead; in either case modify handle_link_commit to accept an optional head_sha parameter and pass the already-read live_head from preflight into handle_link_commit so _sync_cache_lookup and _store_sync_cache can use the supplied head and skip their own git rev-parse calls.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@assets/dashboard.html`:
- Around line 413-418: Replace the clickable divs used for expand/collapse with
real buttons (e.g., change the container with id pattern d${idx} and the similar
feature containers at the other block to use <button> instead of <div>) so
keyboard users can focus and activate them; keep the existing onclick handler
toggleDec(this) and preserve inner structure (dec-hdr, d-chev, d-text,
renderBadge). Also add/reset styling for .dec, .dec-hdr and .feature-hdr to
remove default button chrome (border: 0; background: transparent; width: 100%;
font: inherit; text-align: left) so appearance remains identical while gaining
keyboard/focus semantics. Ensure both the decision block (id d${idx}) and the
feature block counterparts are updated.
- Around line 435-438: The hasPriority function currently checks for 'drifted',
'ungrounded', and 'discovered' statuses but omits 'gap', so features with only
gap decisions aren't treated as priority; modify the hasPriority implementation
(function hasPriority) to also return true when the decisions set contains 'gap'
(i.e., include s.has('gap') in the condition or add 'gap' to the list of
priority statuses) so gap decisions cause the feature to be treated and sorted
as a priority.
- Around line 7-29: Remove the external Google Fonts link element and ensure the
dashboard uses local/system fonts or bundled assets: delete the <link
href="https://fonts.googleapis.com/..."> tag, update the --mono and --sans CSS
variables in the :root block to rely on system font stacks (or reference locally
bundled font files via `@font-face` if you include them in assets), and if you
bundle fonts add `@font-face` rules that point to local files instead of remote
URLs so the dashboard remains fully local-first.
In `@contracts.py`:
- Around line 569-571: The comment on the HistoryEntry fields is incorrect for
the "discovered" status: update the comment for the fulfillment field on the
HistoryEntry (fulfillment: HistoryFulfillment | None) to state that fulfillment
may be present for "discovered" entries (i.e., fulfillment is None only for
ungrounded and gap statuses), and clarify that sources remains empty for
discovered/gap; adjust the inline comment that currently reads "None when
ungrounded/gap/discovered" to something like "None when ungrounded/gap
(discovered may have fulfillment)" to match HistoryFulfillment and dashboard
behavior.
In `@handlers/update.py`:
- Around line 108-114: The SKILL.md copy uses skill_md.read_text() and (dst_dir
/ "SKILL.md").write_text() which default to the platform encoding and can
corrupt non-ASCII content on Windows; update the copy logic to explicitly
open/read and write using UTF-8 (i.e., use read_text(encoding="utf-8") and
write_text(..., encoding="utf-8")) for the skill_md variable and the destination
SKILL.md write to ensure consistent Unicode handling during the SKILL.md copy in
handlers/update.py.
In `@README.md`:
- Around line 212-231: The tools list in the README is out of sync with the
server's registered tools: update the "13 tools across three categories" section
to match the actual registered set (EXPECTED_TOOL_NAMES) and show the correct
count (14); remove the nonexistent entries (bicameral.search, bicameral.brief,
bicameral.drift) and replace them with the exact tool names from
EXPECTED_TOOL_NAMES (or copy the canonical list produced by the server
registration function, e.g., the code that builds/registers the tools), ensuring
the table entries and the header count reflect the registered tool names and
purposes.
In `@server.py`:
- Around line 694-699: serve_stdio() currently awaits dashboard_srv.start which
blocks MCP startup; instead kick off the dashboard startup as best-effort
background work: call get_dashboard_server() in serve_stdio and schedule
dashboard_srv.start(ctx_factory=BicameralContext.from_env) via an
asyncio.create_task (or equivalent background runner) so it does not block stdio
startup, and ensure the task catches and logs exceptions (do not let failures
propagate). Reference serve_stdio, get_dashboard_server, dashboard_srv.start,
and BicameralContext.from_env when making the change.
In `@setup_wizard.py`:
- Around line 286-294: The current _BICAMERAL_HOOK_COMMAND only checks
startswith against ops and misses cases like chained commands, prefixed cd, or
flags (e.g., git -C). Update the _BICAMERAL_HOOK_COMMAND logic to detect git
write-ops anywhere in the command string by matching a broader pattern (e.g.,
look for a word-boundary "git" followed later by subcommands commit, merge,
pull, or "rebase --continue" using a regex or token search), then print the
bicameral sync message when that pattern is found; modify the literal assigned
to _BICAMERAL_HOOK_COMMAND so the inner Python checks use this broader matcher
instead of the current ops tuple and startswith checks.
In `@skills/bicameral-ingest/SKILL.md`:
- Around line 215-233: Update the payload examples so every decision includes
feature_group: add "feature_group" to each object in the natural format
decisions array (decisions: [{ "description": "...", "feature_group": "Account
Status", ... }]) and to each mapping object in the internal format mappings
array (mappings: [{ "intent": "...", "feature_group": "Account Status", ... }]);
ensure no example omits feature_group anywhere it currently appears (including
the other noted example ranges) so readers copying examples won't trigger the "5
decisions, 5 features" dashboard split.
- Around line 197-228: The markdown fenced code blocks in SKILL.md are missing
language identifiers causing MD040; update the three shown blocks by adding
```text for the user prompt block (the one starting with "⚠ I'm not sure how to
categorize this decision"), and add ```jsonc (or ```json) for the two payload
examples that show decisions and mappings (the blocks containing decisions:
[...] and mappings: [...]) so the prompt block uses "text" and the payload
blocks use "jsonc"/"json".
---
Nitpick comments:
In `@handlers/ingest.py`:
- Around line 331-345: The code imports the module-private helper _get_git_email
from events.writer and uses it in multiple places; make its access explicit by
either renaming/_promoting it to a public function get_git_email in
events.writer or by adding a thin public wrapper (e.g., get_git_email) in a
shared module and update callers; replace imports of
events.writer._get_git_email in handlers.ingest (the loop that sets
span["speakers"]) and adapters.ledger with the public get_git_email, keep the
existing caching logic (_git_email_cache) unchanged, and ensure callers import
the new public symbol instead of the underscored one.
In `@handlers/link_commit.py`:
- Around line 254-258: Replace the silent "except Exception: pass" that swallows
notify_dashboard errors with a debug-level log that records the exception but
keeps the path non-blocking; specifically, in the try/except around from
dashboard.server import notify_dashboard and await notify_dashboard(ctx) catch
Exception as e and call a debug logger (e.g., logging.getLogger(__name__).debug
or an existing module logger) to log a short message like "notify_dashboard
failed" including the exception (use exc_info=True or include e) so failures are
visible at debug level without changing behavior.
In `@handlers/preflight.py`:
- Around line 204-216: Preflight imports the module-private
_read_current_head_sha from handlers.link_commit and re-runs git rev-parse
multiple times; make the contract explicit and avoid duplicate subprocess calls
by either (A) promoting _read_current_head_sha to a public export (rename to
read_current_head_sha) in handlers.link_commit and keep using it, or (B) switch
preflight to call resolve_head from ledger.status instead; in either case modify
handle_link_commit to accept an optional head_sha parameter and pass the
already-read live_head from preflight into handle_link_commit so
_sync_cache_lookup and _store_sync_cache can use the supplied head and skip
their own git rev-parse calls.
In `@handlers/update.py`:
- Around line 116-121: Replace the silent swallow of exceptions when calling
_install_claude_hooks(Path(repo_path)) with a debug log that records the
exception and context; keep the try/except to preserve fail-open behavior but in
the except block call logger.debug with a clear message (e.g., "failed to
reinstall Claude hooks for repo {repo_path}") and include the exception info (or
use logger.debug(..., exc_info=True)) so failures in _install_claude_hooks are
recorded without changing behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: ab13c157-b9b9-40e3-a043-ae78037b496c
⛔ Files ignored due to path filters (1)
assets/dashboard-preview.pngis excluded by!**/*.png
📒 Files selected for processing (15)
.gitignoreREADME.mdRECOMMENDED_VERSIONassets/dashboard.htmlcontracts.pyhandlers/history.pyhandlers/ingest.pyhandlers/link_commit.pyhandlers/preflight.pyhandlers/update.pypyproject.tomlserver.pysetup_wizard.pyskills/bicameral-ingest/SKILL.mdskills/bicameral-preflight/SKILL.md
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet"> | ||
| <style> | ||
| :root { | ||
| --bg: #fbfaf7; | ||
| --bg-soft: #f5f3ed; | ||
| --bg-card: #ffffff; | ||
| --border: #e7e3d8; | ||
| --border-soft: #efece3; | ||
| --text: #2a2825; | ||
| --text-dim: #75716a; | ||
| --text-bright: #1a1816; | ||
| --accent: #3a5af6; | ||
| --accent-soft: #eef0ff; | ||
| --green: #15803d; | ||
| --green-soft: #ecf7ee; | ||
| --amber: #b45309; | ||
| --amber-soft: #fdf4e3; | ||
| --red: #b91c1c; | ||
| --red-soft: #fdf0ef; | ||
| --purple: #7c3aed; | ||
| --purple-soft: #f3eeff; | ||
| --mono: "JetBrains Mono", "SF Mono", "Fira Code", Menlo, Monaco, Consolas, monospace; | ||
| --sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; |
There was a problem hiding this comment.
Avoid external font requests in the local dashboard.
Line 7 loads Google Fonts from the browser, which weakens the “local-first / all data stays local” posture and makes the dashboard depend on network access. Prefer system fonts or bundled assets.
Suggested local-first adjustment
-<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">- --mono: "JetBrains Mono", "SF Mono", "Fira Code", Menlo, Monaco, Consolas, monospace;
- --sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
+ --mono: "SF Mono", "Fira Code", Menlo, Monaco, Consolas, monospace;
+ --sans: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet"> | |
| <style> | |
| :root { | |
| --bg: #fbfaf7; | |
| --bg-soft: #f5f3ed; | |
| --bg-card: #ffffff; | |
| --border: #e7e3d8; | |
| --border-soft: #efece3; | |
| --text: #2a2825; | |
| --text-dim: #75716a; | |
| --text-bright: #1a1816; | |
| --accent: #3a5af6; | |
| --accent-soft: #eef0ff; | |
| --green: #15803d; | |
| --green-soft: #ecf7ee; | |
| --amber: #b45309; | |
| --amber-soft: #fdf4e3; | |
| --red: #b91c1c; | |
| --red-soft: #fdf0ef; | |
| --purple: #7c3aed; | |
| --purple-soft: #f3eeff; | |
| --mono: "JetBrains Mono", "SF Mono", "Fira Code", Menlo, Monaco, Consolas, monospace; | |
| --sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; | |
| <style> | |
| :root { | |
| --bg: `#fbfaf7`; | |
| --bg-soft: `#f5f3ed`; | |
| --bg-card: `#ffffff`; | |
| --border: `#e7e3d8`; | |
| --border-soft: `#efece3`; | |
| --text: `#2a2825`; | |
| --text-dim: `#75716a`; | |
| --text-bright: `#1a1816`; | |
| --accent: `#3a5af6`; | |
| --accent-soft: `#eef0ff`; | |
| --green: `#15803d`; | |
| --green-soft: `#ecf7ee`; | |
| --amber: `#b45309`; | |
| --amber-soft: `#fdf4e3`; | |
| --red: `#b91c1c`; | |
| --red-soft: `#fdf0ef`; | |
| --purple: `#7c3aed`; | |
| --purple-soft: `#f3eeff`; | |
| --mono: "SF Mono", "Fira Code", Menlo, Monaco, Consolas, monospace; | |
| --sans: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@assets/dashboard.html` around lines 7 - 29, Remove the external Google Fonts
link element and ensure the dashboard uses local/system fonts or bundled assets:
delete the <link href="https://fonts.googleapis.com/..."> tag, update the --mono
and --sans CSS variables in the :root block to rely on system font stacks (or
reference locally bundled font files via `@font-face` if you include them in
assets), and if you bundle fonts add `@font-face` rules that point to local files
instead of remote URLs so the dashboard remains fully local-first.
| <div class="dec" id="d${idx}" onclick="toggleDec(this)"> | ||
| <div class="dec-hdr"> | ||
| <span class="d-chev">▶</span> | ||
| <span class="d-text">${esc('"' + d.summary + '"')}</span> | ||
| ${renderBadge(d)} | ||
| </div> |
There was a problem hiding this comment.
Make expand/collapse controls keyboard-accessible.
The dashboard’s interactive rows are clickable divs only, so keyboard users cannot expand features or decisions. Use real buttons or add equivalent keyboard/focus semantics.
Suggested direction
-<div class="feature-hdr" onclick="toggleFeature('ft${fi}')">
+<button type="button" class="feature-hdr" onclick="toggleFeature('ft${fi}')">
<span class="f-chev">${open ? '▼' : '▶'}</span>
<span class="f-name">${esc(f.name.toUpperCase())}</span>
<span class="f-rule"></span>
<span class="f-counts">${renderCounts(c)}</span>
-</div>
+</button>-<div class="dec" id="d${idx}" onclick="toggleDec(this)">
- <div class="dec-hdr">
+<div class="dec" id="d${idx}">
+ <button type="button" class="dec-hdr" onclick="toggleDec(this.closest('.dec'))">
<span class="d-chev">▶</span>
<span class="d-text">${esc('"' + d.summary + '"')}</span>
${renderBadge(d)}
- </div>
+ </button>You’ll also need to reset button styling for .feature-hdr / .dec-hdr (border: 0, background: transparent, width: 100%, font: inherit, text-align: left).
Also applies to: 446-452
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@assets/dashboard.html` around lines 413 - 418, Replace the clickable divs
used for expand/collapse with real buttons (e.g., change the container with id
pattern d${idx} and the similar feature containers at the other block to use
<button> instead of <div>) so keyboard users can focus and activate them; keep
the existing onclick handler toggleDec(this) and preserve inner structure
(dec-hdr, d-chev, d-text, renderBadge). Also add/reset styling for .dec,
.dec-hdr and .feature-hdr to remove default button chrome (border: 0;
background: transparent; width: 100%; font: inherit; text-align: left) so
appearance remains identical while gaining keyboard/focus semantics. Ensure both
the decision block (id d${idx}) and the feature block counterparts are updated.
| function hasPriority(f) { | ||
| const s = new Set(f.decisions.map(d => d.status)); | ||
| return s.has('drifted') || s.has('ungrounded') || s.has('discovered'); | ||
| } |
There was a problem hiding this comment.
Treat gap as a priority status.
Gap decisions are actionable “known unknowns”, but hasPriority() omits them, so gap-only features stay collapsed and sort below priority sections.
Suggested fix
function hasPriority(f) {
const s = new Set(f.decisions.map(d => d.status));
- return s.has('drifted') || s.has('ungrounded') || s.has('discovered');
+ return s.has('drifted') || s.has('gap') || s.has('ungrounded') || s.has('discovered');
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function hasPriority(f) { | |
| const s = new Set(f.decisions.map(d => d.status)); | |
| return s.has('drifted') || s.has('ungrounded') || s.has('discovered'); | |
| } | |
| function hasPriority(f) { | |
| const s = new Set(f.decisions.map(d => d.status)); | |
| return s.has('drifted') || s.has('gap') || s.has('ungrounded') || s.has('discovered'); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@assets/dashboard.html` around lines 435 - 438, The hasPriority function
currently checks for 'drifted', 'ungrounded', and 'discovered' statuses but
omits 'gap', so features with only gap decisions aren't treated as priority;
modify the hasPriority implementation (function hasPriority) to also return true
when the decisions set contains 'gap' (i.e., include s.has('gap') in the
condition or add 'gap' to the list of priority statuses) so gap decisions cause
the feature to be treated and sorted as a priority.
| status: Literal["reflected", "drifted", "ungrounded", "superseded", "discovered", "gap"] | ||
| sources: list[HistorySource] # 1+ input spans; empty for discovered/gap | ||
| fulfillment: HistoryFulfillment | None = None # None when ungrounded/gap/discovered |
There was a problem hiding this comment.
Fix the discovered fulfillment contract comment.
discovered means code exists without a meeting/source claim, so it can still have fulfillment. The current comment says fulfillment is None for discovered, which conflicts with the dashboard’s “✓ in code” handling.
Suggested clarification
- sources: list[HistorySource] # 1+ input spans; empty for discovered/gap
- fulfillment: HistoryFulfillment | None = None # None when ungrounded/gap/discovered
+ sources: list[HistorySource] # 1+ input spans; empty for discovered/gap
+ fulfillment: HistoryFulfillment | None = None # None when ungrounded/gap; discovered may still have code fulfillment📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| status: Literal["reflected", "drifted", "ungrounded", "superseded", "discovered", "gap"] | |
| sources: list[HistorySource] # 1+ input spans; empty for discovered/gap | |
| fulfillment: HistoryFulfillment | None = None # None when ungrounded/gap/discovered | |
| status: Literal["reflected", "drifted", "ungrounded", "superseded", "discovered", "gap"] | |
| sources: list[HistorySource] # 1+ input spans; empty for discovered/gap | |
| fulfillment: HistoryFulfillment | None = None # None when ungrounded/gap; discovered may still have code fulfillment |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@contracts.py` around lines 569 - 571, The comment on the HistoryEntry fields
is incorrect for the "discovered" status: update the comment for the fulfillment
field on the HistoryEntry (fulfillment: HistoryFulfillment | None) to state that
fulfillment may be present for "discovered" entries (i.e., fulfillment is None
only for ungrounded and gap statuses), and clarify that sources remains empty
for discovered/gap; adjust the inline comment that currently reads "None when
ungrounded/gap/discovered" to something like "None when ungrounded/gap
(discovered may have fulfillment)" to match HistoryFulfillment and dashboard
behavior.
| skill_md = skill_dir / "SKILL.md" | ||
| if not skill_md.exists(): | ||
| continue | ||
| dst_dir = skills_dst / skill_dir.name | ||
| dst_dir.mkdir(parents=True, exist_ok=True) | ||
| (dst_dir / "SKILL.md").write_text(skill_md.read_text()) | ||
| count += 1 |
There was a problem hiding this comment.
Specify encoding="utf-8" on SKILL.md copy — avoids Windows locale corruption.
read_text() / write_text() fall back to the platform's locale encoding (cp1252 on most Windows installs). Any non-ASCII content in a shipped SKILL.md (smart quotes, em-dashes, emoji in headings, etc.) will raise UnicodeDecodeError or silently mojibake when a Windows user upgrades via bicameral.update. Since the whole block is wrapped in a catch-all except Exception, the failure would surface only as skills_updated=0.
🛡️ Proposed fix
- (dst_dir / "SKILL.md").write_text(skill_md.read_text())
+ (dst_dir / "SKILL.md").write_text(
+ skill_md.read_text(encoding="utf-8"),
+ encoding="utf-8",
+ )📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| skill_md = skill_dir / "SKILL.md" | |
| if not skill_md.exists(): | |
| continue | |
| dst_dir = skills_dst / skill_dir.name | |
| dst_dir.mkdir(parents=True, exist_ok=True) | |
| (dst_dir / "SKILL.md").write_text(skill_md.read_text()) | |
| count += 1 | |
| skill_md = skill_dir / "SKILL.md" | |
| if not skill_md.exists(): | |
| continue | |
| dst_dir = skills_dst / skill_dir.name | |
| dst_dir.mkdir(parents=True, exist_ok=True) | |
| (dst_dir / "SKILL.md").write_text( | |
| skill_md.read_text(encoding="utf-8"), | |
| encoding="utf-8", | |
| ) | |
| count += 1 |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@handlers/update.py` around lines 108 - 114, The SKILL.md copy uses
skill_md.read_text() and (dst_dir / "SKILL.md").write_text() which default to
the platform encoding and can corrupt non-ASCII content on Windows; update the
copy logic to explicitly open/read and write using UTF-8 (i.e., use
read_text(encoding="utf-8") and write_text(..., encoding="utf-8")) for the
skill_md variable and the destination SKILL.md write to ensure consistent
Unicode handling during the SKILL.md copy in handlers/update.py.
| <details> | ||
| <summary><strong>Full tool input schemas</strong></summary> | ||
|
|
||
| #### bicameral.status | ||
|
|
||
| ```json | ||
| { | ||
| "type": "object", | ||
| "properties": { | ||
| "filter": { | ||
| "type": "string", | ||
| "enum": ["all", "drifted", "pending", "reflected", "ungrounded"], | ||
| "default": "all", | ||
| "description": "Filter decisions by status" | ||
| }, | ||
| "since": { | ||
| "type": "string", | ||
| "description": "ISO date — only decisions ingested after this date" | ||
| }, | ||
| "ref": { | ||
| "type": "string", | ||
| "default": "HEAD", | ||
| "description": "Git ref to evaluate against" | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| #### bicameral.search | ||
|
|
||
| ```json | ||
| { | ||
| "type": "object", | ||
| "properties": { | ||
| "query": { | ||
| "type": "string", | ||
| "description": "Natural language description — e.g. 'add retry with backoff'" | ||
| }, | ||
| "max_results": { | ||
| "type": "integer", | ||
| "default": 10 | ||
| }, | ||
| "min_confidence": { | ||
| "type": "number", | ||
| "default": 0.5, | ||
| "description": "Minimum BM25 confidence score (0-1)" | ||
| } | ||
| }, | ||
| "required": ["query"] | ||
| } | ||
| ``` | ||
|
|
||
| #### bicameral.drift | ||
|
|
||
| ```json | ||
| { | ||
| "type": "object", | ||
| "properties": { | ||
| "file_path": { | ||
| "type": "string", | ||
| "description": "File path relative to repo root" | ||
| }, | ||
| "use_working_tree": { | ||
| "type": "boolean", | ||
| "default": true, | ||
| "description": "True = compare against disk (pre-commit), False = compare against HEAD" | ||
| } | ||
| }, | ||
| "required": ["file_path"] | ||
| } | ||
| ``` | ||
|
|
||
| #### bicameral.link_commit | ||
|
|
||
| ```json | ||
| { | ||
| "type": "object", | ||
| "properties": { | ||
| "commit_hash": { | ||
| "type": "string", | ||
| "default": "HEAD", | ||
| "description": "Git commit hash or ref to sync (default: HEAD)" | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
| <summary><strong>13 tools across three categories</strong></summary> | ||
|
|
||
| #### bicameral.ingest | ||
|
|
||
| ```json | ||
| { | ||
| "type": "object", | ||
| "properties": { | ||
| "payload": { | ||
| "type": "object", | ||
| "description": "Normalized ingest payload matching the internal code-locator handoff shape" | ||
| }, | ||
| "source_scope": { | ||
| "type": "string", | ||
| "default": "default", | ||
| "description": "Source stream identifier, e.g. Slack channel or Notion database" | ||
| }, | ||
| "cursor": { | ||
| "type": "string", | ||
| "description": "Optional upstream checkpoint (timestamp, event id, updated_at)" | ||
| } | ||
| }, | ||
| "required": ["payload"] | ||
| } | ||
| ``` | ||
|
|
||
| #### validate_symbols | ||
|
|
||
| ```json | ||
| { | ||
| "type": "object", | ||
| "properties": { | ||
| "candidates": { | ||
| "type": "array", | ||
| "items": { "type": "string" }, | ||
| "description": "Symbol name hypotheses to validate (e.g. ['CheckoutController', 'processOrder'])" | ||
| } | ||
| }, | ||
| "required": ["candidates"] | ||
| } | ||
| ``` | ||
| ### Decision Ledger | ||
|
|
||
| #### search_code | ||
|
|
||
| ```json | ||
| { | ||
| "type": "object", | ||
| "properties": { | ||
| "query": { | ||
| "type": "string", | ||
| "description": "Text search query (e.g. 'checkout rate limit middleware')" | ||
| }, | ||
| "symbol_ids": { | ||
| "type": "array", | ||
| "items": { "type": "integer" }, | ||
| "description": "Symbol IDs from validate_symbols to use as graph traversal seeds" | ||
| } | ||
| }, | ||
| "required": ["query"] | ||
| } | ||
| ``` | ||
|
|
||
| #### get_neighbors | ||
|
|
||
| ```json | ||
| { | ||
| "type": "object", | ||
| "properties": { | ||
| "symbol_id": { | ||
| "type": "integer", | ||
| "description": "Symbol ID from validate_symbols results" | ||
| } | ||
| }, | ||
| "required": ["symbol_id"] | ||
| } | ||
| ``` | ||
| | Tool | Purpose | | ||
| |---|---| | ||
| | `bicameral.ingest` | Ingest a transcript, PRD, or Slack export into the ledger | | ||
| | `bicameral.preflight` | Pre-flight: surface prior decisions and drift before coding | | ||
| | `bicameral.search` | Search past decisions by topic | | ||
| | `bicameral.brief` | Full brief for a feature area (decisions, drift, divergences, gaps) | | ||
| | `bicameral.history` | Read-only snapshot of all decisions grouped by feature | | ||
| | `bicameral.link_commit` | Sync a commit — update content hashes, re-evaluate drift | | ||
| | `bicameral.drift` | Detect drift for decisions touching a specific file | | ||
| | `bicameral.judge_gaps` | Run the business-requirement gap rubric on a topic | | ||
| | `bicameral.resolve_compliance` | Write back caller-LLM compliance verdicts (compliant/drifted/not_relevant) | | ||
| | `bicameral.ratify` | Record product sign-off on a decision | | ||
| | `bicameral.update` | Check for and apply recommended version updates | | ||
| | `bicameral.reset` | Wipe the ledger for the current repo (dry-run by default) | | ||
| | `bicameral.dashboard` | Start the local dashboard server and return its URL | |
There was a problem hiding this comment.
Align the tools reference with the registered MCP tools.
The server registers 14 tools, but this section says 13 and lists bicameral.search, bicameral.brief, and bicameral.drift, which are not in EXPECTED_TOOL_NAMES. Users will try to call nonexistent tools.
Suggested README correction
-<summary><strong>13 tools across three categories</strong></summary>
+<summary><strong>14 tools across two categories</strong></summary> | `bicameral.ingest` | Ingest a transcript, PRD, or Slack export into the ledger |
| `bicameral.preflight` | Pre-flight: surface prior decisions and drift before coding |
-| `bicameral.search` | Search past decisions by topic |
-| `bicameral.brief` | Full brief for a feature area (decisions, drift, divergences, gaps) |
| `bicameral.history` | Read-only snapshot of all decisions grouped by feature |
| `bicameral.link_commit` | Sync a commit — update content hashes, re-evaluate drift |
-| `bicameral.drift` | Detect drift for decisions touching a specific file |
| `bicameral.judge_gaps` | Run the business-requirement gap rubric on a topic |
| `bicameral.resolve_compliance` | Write back caller-LLM compliance verdicts (compliant/drifted/not_relevant) |
| `bicameral.ratify` | Record product sign-off on a decision |🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@README.md` around lines 212 - 231, The tools list in the README is out of
sync with the server's registered tools: update the "13 tools across three
categories" section to match the actual registered set (EXPECTED_TOOL_NAMES) and
show the correct count (14); remove the nonexistent entries (bicameral.search,
bicameral.brief, bicameral.drift) and replace them with the exact tool names
from EXPECTED_TOOL_NAMES (or copy the canonical list produced by the server
registration function, e.g., the code that builds/registers the tools), ensuring
the table entries and the header count reflect the registered tool names and
purposes.
| async def serve_stdio() -> None: | ||
| # Start the live dashboard HTTP sidecar in the background. | ||
| # It binds to a free port and stays running for the session. | ||
| dashboard_srv = get_dashboard_server() | ||
| await dashboard_srv.start(ctx_factory=BicameralContext.from_env) | ||
|
|
There was a problem hiding this comment.
Don’t let dashboard startup block the MCP server.
The dashboard is optional, but serve_stdio() now awaits it before starting stdio. Any sidecar failure would prevent every MCP tool from coming up. Make this best-effort or rely on lazy startup via bicameral.dashboard.
Suggested best-effort guard
async def serve_stdio() -> None:
# Start the live dashboard HTTP sidecar in the background.
# It binds to a free port and stays running for the session.
- dashboard_srv = get_dashboard_server()
- await dashboard_srv.start(ctx_factory=BicameralContext.from_env)
+ try:
+ dashboard_srv = get_dashboard_server()
+ await dashboard_srv.start(ctx_factory=BicameralContext.from_env)
+ except Exception as exc:
+ print(f"[dashboard] failed to start sidecar: {exc}", file=sys.stderr)
async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@server.py` around lines 694 - 699, serve_stdio() currently awaits
dashboard_srv.start which blocks MCP startup; instead kick off the dashboard
startup as best-effort background work: call get_dashboard_server() in
serve_stdio and schedule
dashboard_srv.start(ctx_factory=BicameralContext.from_env) via an
asyncio.create_task (or equivalent background runner) so it does not block stdio
startup, and ensure the task catches and logs exceptions (do not let failures
propagate). Reference serve_stdio, get_dashboard_server, dashboard_srv.start,
and BicameralContext.from_env when making the change.
| _BICAMERAL_HOOK_COMMAND = ( | ||
| "python3 -c \"" | ||
| "import json,sys; " | ||
| "d=json.load(sys.stdin); " | ||
| "c=d.get('tool_input',{}).get('command','').lstrip(); " | ||
| "ops=('git commit','git merge ','git pull','git rebase --continue'); " | ||
| "[print('bicameral: git write-op detected — call bicameral.link_commit" | ||
| "(commit_hash=\\'HEAD\\') now to sync the decision ledger') " | ||
| "for _ in [1] if any(c.startswith(op) for op in ops)]\"" |
There was a problem hiding this comment.
Detect git write-ops beyond the first shell token.
The hook only matches commands that start with git commit, git merge, etc. Common forms like git add . && git commit -m ..., cd app && git pull, or git -C repo commit won’t print the sync instruction, so HEAD can advance without the intended hook signal.
Suggested broader matcher
_BICAMERAL_HOOK_COMMAND = (
"python3 -c \""
- "import json,sys; "
+ "import json,re,sys; "
"d=json.load(sys.stdin); "
- "c=d.get('tool_input',{}).get('command','').lstrip(); "
- "ops=('git commit','git merge ','git pull','git rebase --continue'); "
+ "c=d.get('tool_input',{}).get('command',''); "
+ "pat=r'(^|[;&|]\\s*)(?:\\w+=\\S+\\s+)*git(?:\\s+-C\\s+\\S+)?\\s+(commit\\b|merge\\b|pull\\b|rebase\\s+--continue\\b)'; "
"[print('bicameral: git write-op detected — call bicameral.link_commit"
"(commit_hash=\\'HEAD\\') now to sync the decision ledger') "
- "for _ in [1] if any(c.startswith(op) for op in ops)]\""
+ "for _ in [1] if re.search(pat,c)]\""
)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@setup_wizard.py` around lines 286 - 294, The current _BICAMERAL_HOOK_COMMAND
only checks startswith against ops and misses cases like chained commands,
prefixed cd, or flags (e.g., git -C). Update the _BICAMERAL_HOOK_COMMAND logic
to detect git write-ops anywhere in the command string by matching a broader
pattern (e.g., look for a word-boundary "git" followed later by subcommands
commit, merge, pull, or "rebase --continue" using a regex or token search), then
print the bicameral sync message when that pattern is found; modify the literal
assigned to _BICAMERAL_HOOK_COMMAND so the inner Python checks use this broader
matcher instead of the current ops tuple and startswith checks.
| ``` | ||
| ⚠ I'm not sure how to categorize this decision: | ||
| "<decision text>" | ||
|
|
||
| Proposed group for the rest: "<dominant group>" | ||
|
|
||
| Options: | ||
| a) "<existing group A>" (existing) | ||
| b) "<existing group B>" (existing) | ||
| c) "<proposed new group>" (new) | ||
| a) Use "<dominant group>" anyway | ||
| b) "<existing group B>" (existing) | ||
| c) "<proposed different group>" (new) | ||
| d) Enter a different group name | ||
|
|
||
| Which feature does this belong to? | ||
| ``` | ||
|
|
||
| Wait for the user's response. Do not proceed with the ingest call | ||
| until every decision has a confirmed group. | ||
| Wait for the user's response. Do not ask for every decision — only | ||
| the ones that genuinely don't fit the dominant group. | ||
|
|
||
| 4. **Pass `feature_group`** on each decision in the ingest payload. | ||
| 5. **Pass `feature_group`** on each decision in the ingest payload. | ||
| For the internal format, add `feature_group` at the mapping level. | ||
| For the natural format, add it on each decision object: | ||
| ``` | ||
| decisions: [ | ||
| { "description": "...", "feature_group": "Checkout Flow", ... } | ||
| { "description": "...", "feature_group": "Account Status", ... } | ||
| ] | ||
| ``` | ||
| For the internal format: | ||
| ``` | ||
| mappings: [ | ||
| { "intent": "...", "feature_group": "Account Status", ... } | ||
| ] | ||
| ``` |
There was a problem hiding this comment.
Add languages to the changed fenced blocks.
markdownlint flags these fences as MD040. Use text for the prompt block and json/jsonc for payload snippets.
Suggested lint fix
- ```
+ ```text
⚠ I'm not sure how to categorize this decision:
"<decision text>"- ```
+ ```jsonc
decisions: [
{ "description": "...", "feature_group": "Account Status", ... }
]- ```
+ ```jsonc
mappings: [
{ "intent": "...", "feature_group": "Account Status", ... }
]🧰 Tools
🪛 markdownlint-cli2 (0.22.0)
[warning] 197-197: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
[warning] 218-218: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
[warning] 224-224: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@skills/bicameral-ingest/SKILL.md` around lines 197 - 228, The markdown fenced
code blocks in SKILL.md are missing language identifiers causing MD040; update
the three shown blocks by adding ```text for the user prompt block (the one
starting with "⚠ I'm not sure how to categorize this decision"), and add
```jsonc (or ```json) for the two payload examples that show decisions and
mappings (the blocks containing decisions: [...] and mappings: [...]) so the
prompt block uses "text" and the payload blocks use "jsonc"/"json".
| 5. **Pass `feature_group`** on each decision in the ingest payload. | ||
| For the internal format, add `feature_group` at the mapping level. | ||
| For the natural format, add it on each decision object: | ||
| ``` | ||
| decisions: [ | ||
| { "description": "...", "feature_group": "Checkout Flow", ... } | ||
| { "description": "...", "feature_group": "Account Status", ... } | ||
| ] | ||
| ``` | ||
| For the internal format: | ||
| ``` | ||
| mappings: [ | ||
| { "intent": "...", "feature_group": "Account Status", ... } | ||
| ] | ||
| ``` | ||
|
|
||
| **Never omit `feature_group`.** An unset `feature_group` causes the | ||
| decision to fall back to `source_ref` grouping — every decision ends | ||
| up in its own feature row in the dashboard. This is the primary cause | ||
| of the "5 decisions, 5 features" problem. |
There was a problem hiding this comment.
Update the payload examples to include feature_group.
The new guidance says “Never omit feature_group”, but both the internal and natural examples still omit it. Agents copying these examples will recreate the “5 decisions, 5 features” dashboard split.
Suggested example fixes
code_regions: [
{ file_path: "src/lib/session.ts", symbol: "SessionCache",
start_line: 42, end_line: 89, type: "class" },
{ file_path: "src/lib/redis.ts", symbol: "RedisClient",
start_line: 1, end_line: 34, type: "class" }
],
- search_hint: "SessionCache RedisClient session-cache horizontal scaling"
+ search_hint: "SessionCache RedisClient session-cache horizontal scaling",
+ feature_group: "Session Storage" {
description: "Cache user sessions in Redis for horizontal scaling",
id: "sprint-14-planning#session-cache", # optional stable id
- search_hint: "SessionCache RedisClient session cache horizontal scaling"
+ search_hint: "SessionCache RedisClient session cache horizontal scaling",
+ feature_group: "Session Storage"
},
{
description: "Apply 10% discount on orders ≥ $100",
- search_hint: "calculateDiscount order_total applyDiscount PricingService"
+ search_hint: "calculateDiscount order_total applyDiscount PricingService",
+ feature_group: "Pricing"
}Also applies to: 318-339, 354-363
🧰 Tools
🪛 markdownlint-cli2 (0.22.0)
[warning] 218-218: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
[warning] 224-224: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@skills/bicameral-ingest/SKILL.md` around lines 215 - 233, Update the payload
examples so every decision includes feature_group: add "feature_group" to each
object in the natural format decisions array (decisions: [{ "description":
"...", "feature_group": "Account Status", ... }]) and to each mapping object in
the internal format mappings array (mappings: [{ "intent": "...",
"feature_group": "Account Status", ... }]); ensure no example omits
feature_group anywhere it currently appears (including the other noted example
ranges) so readers copying examples won't trigger the "5 decisions, 5 features"
dashboard split.
Three-round audit cycle (VETO → VETO → PASS) for closing v0 release blockers issues #160 (materializer event_type mismatch) and #161 (channel_allowlist not populated). META_LEDGER entries #37-#41 capture: round-1 VETO (infrastructure- mismatch — pull_team_server_events had zero production callers), round-2 VETO (specification-drift — sketch passed wrapped adapter without unwrap; would echo events O(N²) cross-dev), round-3 PASS, IMPLEMENT, SUBSTANTIATION. SHADOW_GENOME #7 heuristic catalog grew 4→6 across this branch: - Heuristic 5 (upstream-consumer) — Entry #37 - Heuristic 6 (wrapper-side-effect) — Entry #38 The catalog is the productive deposit beyond the code; each heuristic is a durable detection pattern reusable in future audits. SYSTEM_STATE.md adds the v0 release-blockers section: end-to-end ingest pipeline now functional (Slack OAuth → workspace row → YAML allowlist sync → channel_allowlist → Slack worker polls → heuristic+ LLM extraction → team_event → /events HTTP → per-dev consumer pulls → bridges to IngestPayload → per-dev local ledger). Merkle seal: SHA256(content_hash + previous_hash) = 7cc405fc8d39f468d502da669982c88321ce3a84bb571d28e0b14be86ab56bdd (content_hash 14e387b1..., previous_hash b3700366... = Priority C v1.1 SEAL at Entry #36). Closes #160, closes #161. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three-round audit cycle (VETO → VETO → PASS) for closing v0 release blockers issues #160 (materializer event_type mismatch) and #161 (channel_allowlist not populated). META_LEDGER entries #37-#41 capture: round-1 VETO (infrastructure- mismatch — pull_team_server_events had zero production callers), round-2 VETO (specification-drift — sketch passed wrapped adapter without unwrap; would echo events O(N²) cross-dev), round-3 PASS, IMPLEMENT, SUBSTANTIATION. SHADOW_GENOME #7 heuristic catalog grew 4→6 across this branch: - Heuristic 5 (upstream-consumer) — Entry #37 - Heuristic 6 (wrapper-side-effect) — Entry #38 The catalog is the productive deposit beyond the code; each heuristic is a durable detection pattern reusable in future audits. SYSTEM_STATE.md adds the v0 release-blockers section: end-to-end ingest pipeline now functional (Slack OAuth → workspace row → YAML allowlist sync → channel_allowlist → Slack worker polls → heuristic+ LLM extraction → team_event → /events HTTP → per-dev consumer pulls → bridges to IngestPayload → per-dev local ledger). Merkle seal: SHA256(content_hash + previous_hash) = 7cc405fc8d39f468d502da669982c88321ce3a84bb571d28e0b14be86ab56bdd (content_hash 14e387b1..., previous_hash b3700366... = Priority C v1.1 SEAL at Entry #36). Closes #160, closes #161. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Test plan
bicameral-mcp setupinto.claude/settings.jsonbicameral.preflightlazy-syncs when HEAD has advanced (commit outside Claude Code → next preflight catches up)🤖 Generated with Claude Code
Summary by CodeRabbit
Release Notes v0.5.3
New Features
Improvements
Documentation