Skip to content

feat: v0.5.3 — gap attribution, auto link_commit sync, skill aggression + README overhaul#37

Merged
jinhongkuan merged 1 commit into
mainfrom
jin/v0.5.0-decision-tier
Apr 21, 2026
Merged

feat: v0.5.3 — gap attribution, auto link_commit sync, skill aggression + README overhaul#37
jinhongkuan merged 1 commit into
mainfrom
jin/v0.5.0-decision-tier

Conversation

@jinhongkuan

@jinhongkuan jinhongkuan commented Apr 21, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Gap answer attribution: agent_session/manual ingests backfill the answerer's git email as speaker instead of inheriting the document source
  • Auto link_commit sync: setup_wizard installs a PostToolUse hook into user repos (fires after git commits/merges/pulls); preflight also lazy-syncs HEAD before each search
  • Skill aggression: ingest and preflight SKILL.md rewrites with much broader trigger conditions — preflight fires on any "how should I implement X" question
  • README overhaul: ASCII banner, spec-alignment narrative, what-setup-installs table with removal guide, real dashboard screenshot, corrected local dev path

Test plan

  • Gap answers (agent_session/manual source_type) show git email as speaker in history
  • PostToolUse hook installed correctly by bicameral-mcp setup into .claude/settings.json
  • bicameral.preflight lazy-syncs when HEAD has advanced (commit outside Claude Code → next preflight catches up)
  • Skills trigger more aggressively in Claude Code sessions
  • README renders correctly on GitHub (banner, screenshot, tables)

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes v0.5.3

  • New Features

    • Decision Dashboard UI with live updates and status tracking
    • New "gap" decision status for tracking open questions and missing decisions
  • Improvements

    • Automatic ledger synchronization during preflight checks for better accuracy
    • Enhanced skill management during system updates
    • Dashboard notifications after data ingestion and commits
    • Better classification of action items and open questions
  • Documentation

    • Redesigned README with improved workflow and UI focus
    • Updated tool references and configuration options

…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>
@coderabbitai

coderabbitai Bot commented Apr 21, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

The 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

Cohort / File(s) Summary
Version & Manifest Updates
.gitignore, RECOMMENDED_VERSION, pyproject.toml
Added .gstack/ to gitignore; bumped version from 0.5.1 to 0.5.3 across manifest files.
Dashboard UI & Response Contract
assets/dashboard.html, contracts.py
Added self-contained HTML dashboard with live EventSource updates, collapsible features, and decision ledger rendering. Added DashboardResponse model with url, status, and port fields.
Decision History & Status
contracts.py, handlers/history.py
Extended HistoryDecision.status to include "gap" status. Updated _decision_status_for_history() to detect [Open Question] prefix and map to gap status. Adjusted feature prioritization to surface features with gaps alongside ungrounded decisions.
Handler Dashboard Notifications
handlers/ingest.py, handlers/link_commit.py, handlers/preflight.py
Added async dashboard notifications post-execution in ingest and link_commit handlers. Implemented lazy HEAD catch-up in preflight before search/brief chaining, with error handling to prevent preflight failure from sync issues.
Ingest & Speaker Backfill
handlers/ingest.py
Removed action item ledger mappings generation; added session-origin speaker backfill for agent_session and manual spans; improved response handling with dashboard notification.
Update & Skills Reinstallation
handlers/update.py
Added _reinstall_skills() helper to copy skill SKILL.md files and re-run Claude hooks installation. Extended handle_update() signature to accept repo_path and report count of reinstalled skills in response.
Claude Code Hook Installation
setup_wizard.py
Added _install_claude_hooks() to merge PostToolUse Bash hook into .claude/settings.json for detecting git write operations. Updated Claude Code slash commands list (added /bicameral:preflight, /bicameral:history, /bicameral:dashboard, /bicameral:reset; removed search/drift/status).
MCP Server & Dashboard Integration
server.py
Registered new bicameral.dashboard tool to start/query dashboard server. Modified serve_stdio() to start dashboard HTTP sidecar at process launch. Enhanced bicameral.history and bicameral.ingest responses with contextual _guidance messages. Extended bicameral.update to pass repo_path to handler.
Documentation & Skill Guides
README.md, skills/bicameral-ingest/SKILL.md, skills/bicameral-preflight/SKILL.md
Completely rewrote README with UI/workflow focus, updated tool list (13 tools including preflight, brief, history, judge_gaps, resolve_compliance, ratify, update, dashboard), added environment variables, and updated development instructions. Updated skill docs: ingest now emphasizes decisions from varied sources with feature-group assignment strategies; preflight expanded auto-fire coverage to broader code-change verbs and tightened skip rules.

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

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

🐰 A dashboard sparkles, gaps now glow,
Handlers whisper as decisions flow!
Skills reinstall, preflight hops with grace,
v0.5.3 pirouettes through the codebase! ✨🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 68.42% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title comprehensively summarizes the main changes: v0.5.3 release with gap attribution, auto sync, skill updates, and README overhaul—all key themes present in the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch jin/v0.5.0-decision-tier

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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: pass makes this path invisible when dashboard.server is 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: pass here means a user who upgrades and expects the new PostToolUse hook gets no signal when _install_claude_hooks throws (e.g. permissions on .claude/settings.json, non-git directory). A one-line logger.debug preserves 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_email or wrap it.

_get_git_email is imported from events.writer (leading underscore = module-private) and used in both handlers/ingest.py and adapters/ledger.py, making it a de-facto public contract. Keep the API honest by renaming to get_git_email (removing the underscore) or exposing a thin public wrapper in a shared module. No functional concern with the caching sentinel—using None to mean "not fetched yet" works correctly even when _get_git_email returns "" 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_sha or reuse resolve_head — cross-module private import requires clarification.

_read_current_head_sha is imported from handlers.link_commit despite 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 to resolve_head from ledger.status (both use git rev-parse), so you could either:

  • Drop the underscore from _read_current_head_sha (public export from handlers/link_commit.py), or
  • Switch to resolve_head from ledger.status (which already owns HEAD resolution logic).

Secondary concern: git rev-parse HEAD now runs three times per lazy sync:

  1. Here in preflight (line 210)
  2. Inside _sync_cache_lookup (line 148 of handlers/link_commit.py)
  3. Inside _store_sync_cache (line 176 of handlers/link_commit.py)

Pass the already-read live_head SHA into handle_link_commit to 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

📥 Commits

Reviewing files that changed from the base of the PR and between 0248112 and 3792a6a.

⛔ Files ignored due to path filters (1)
  • assets/dashboard-preview.png is excluded by !**/*.png
📒 Files selected for processing (15)
  • .gitignore
  • README.md
  • RECOMMENDED_VERSION
  • assets/dashboard.html
  • contracts.py
  • handlers/history.py
  • handlers/ingest.py
  • handlers/link_commit.py
  • handlers/preflight.py
  • handlers/update.py
  • pyproject.toml
  • server.py
  • setup_wizard.py
  • skills/bicameral-ingest/SKILL.md
  • skills/bicameral-preflight/SKILL.md

Comment thread assets/dashboard.html
Comment on lines +7 to +29
<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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Suggested change
<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.

Comment thread assets/dashboard.html
Comment on lines +413 to +418
<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>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Comment thread assets/dashboard.html
Comment on lines +435 to +438
function hasPriority(f) {
const s = new Set(f.decisions.map(d => d.status));
return s.has('drifted') || s.has('ungrounded') || s.has('discovered');
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Suggested change
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.

Comment thread contracts.py
Comment on lines +569 to +571
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
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.

Comment thread handlers/update.py
Comment on lines +108 to +114
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
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.

Comment thread README.md
Comment on lines 212 to +231
<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 |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Comment thread server.py
Comment on lines 694 to +699
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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Comment thread setup_wizard.py
Comment on lines +286 to +294
_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)]\""

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Comment on lines 197 to +228
```
⚠ 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", ... }
]
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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

Comment on lines +215 to +233
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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

@jinhongkuan jinhongkuan merged commit a7dca02 into main Apr 21, 2026
2 checks passed
@jinhongkuan jinhongkuan deleted the jin/v0.5.0-decision-tier branch April 21, 2026 20:57
Knapp-Kevin added a commit that referenced this pull request May 3, 2026
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>
Knapp-Kevin added a commit that referenced this pull request May 3, 2026
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>
@coderabbitai coderabbitai Bot mentioned this pull request May 3, 2026
15 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant