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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,17 @@ All notable changes to bicameral-mcp are tracked here. Format loosely follows

### Added

- **`bicameral-mcp branch-scan` CLI + opt-in pre-push git hook (#48).**
New console subcommand prints a terminal summary of drifted decisions
for HEAD; calls `link_commit` under the hood. Installed as a git
pre-push hook via `bicameral-mcp setup --with-push-hook`. Surfaces
drift warnings before `git push` completes, with a `Push anyway? [y/N]`
prompt when attached to a TTY. Non-blocking by default;
`BICAMERAL_PUSH_HOOK_BLOCK=1` forces hard-block on drift. Idempotent
install. Path C: skips silently when no `~/.bicameral/ledger.db`
exists. New module `cli/branch_scan.py`; new
`_install_git_pre_push_hook` in `setup_wizard.py`; new `--with-push-hook`
flag in `bicameral-mcp setup`. Issue #48.
- **GitHub Action — sticky PR-comment drift report (#49).** New advisory
workflow `.github/workflows/drift-report.yml` posts a sticky Markdown
comment on every PR open/synchronize with the drift state computed
Expand Down
177 changes: 177 additions & 0 deletions cli/branch_scan.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
"""Issue #48 — branch-scan CLI: terminal-output drift summary for the
pre-push git hook.

Wraps ``handlers.link_commit`` in a CLI surface that prints a
human-readable warning block and exits with a code the pre-push hook
can act on:

0 — no drift detected, or skipped (no ledger configured)
1 — drift detected AND user (TTY) declined the prompt
2 — drift detected AND ``BICAMERAL_PUSH_HOOK_BLOCK=1`` (hard-block)

Stderr carries the warning text so the user sees it before any
prompt; stdout is reserved for status messages the hook may want
to capture or filter.

Sibling of ``cli/drift_report.py`` (which renders Markdown for PR
sticky comments). The two are intentionally parallel — different
output formats, different exit-code semantics. Sharing a common
formatter would be premature abstraction with only two consumers.

Design rule: this module imports only from ``contracts`` and (via
the ``_compute_drift`` indirection) ``handlers.link_commit``. No
imports of GitHub API clients, no Markdown rendering. Pure terminal
output.
"""

from __future__ import annotations

import os
import sys
from typing import TYPE_CHECKING

if TYPE_CHECKING:
from contracts import LinkCommitResponse, PendingComplianceCheck

_HEADER_PREFIX = "[!] bicameral:"
_BLOCK_ENV = "BICAMERAL_PUSH_HOOK_BLOCK"

# Exit codes used by the pre-push hook
_EXIT_OK = 0
_EXIT_USER_DECLINED = 1 # set by the hook, not by main(); main returns _EXIT_BLOCK
_EXIT_BLOCK = 2


# ── Public entry (≤ 25 lines) ────────────────────────────────────────


def render_terminal_summary(
response: LinkCommitResponse | None,
) -> str:
"""Render a terminal-friendly summary of drift state.

``None`` ⇒ skip advisory (no ledger configured).
Empty pending + zero auto_resolved ⇒ empty string (caller skips).
Otherwise ⇒ multiline header + bulleted list of drifted decisions.
"""
if response is None:
return _render_skip_message()
pending = response.pending_compliance_checks
if not pending:
return ""
return _render_drift_block(pending)


# ── Helper renderers (each ≤ 20 lines) ───────────────────────────────


def _render_skip_message() -> str:
"""Body when no ledger is configured. ASCII only — no emojis —
so Windows terminals (cp1252) don't blow up on print()."""
return (
"bicameral: no ledger configured at ~/.bicameral/ledger.db; pre-push drift check skipped\n"
)


def _render_drift_block(
pending: list[PendingComplianceCheck],
) -> str:
"""Body for the has-drift case. Header line + one bullet per
decision with file:symbol locator."""
n = len(pending)
noun = "decision" if n == 1 else "decisions"
lines = [f"{_HEADER_PREFIX} {n} {noun} drifted in this push"]
for check in pending:
lines.append(_render_bullet(check))
return "\n".join(lines) + "\n"


def _render_bullet(check: PendingComplianceCheck) -> str:
"""Single bullet line: ' • <decision_id> — <file>:<symbol>'.
Decision description is omitted (often verbose); the locator is
what the user needs to navigate to the code."""
return f" • {check.decision_id} — {check.file_path}:{check.symbol}"


# ── CLI entry point (≤ 35 lines) ─────────────────────────────────────


def main(argv: list[str] | None = None) -> int:
"""CLI entry. Invoked by the pre-push hook as
``bicameral-mcp branch-scan`` (which dispatches via
``server:cli_main``) or directly via ``python -m cli.branch_scan``.

Returns the exit code described in the module docstring.
"""
response = _compute_drift()
summary = render_terminal_summary(response)
if summary:
print(summary, file=sys.stderr, end="")
if response is None or not response.pending_compliance_checks:
return _EXIT_OK
return _resolve_exit_code()


# ── Orchestration helpers (each ≤ 20 lines) ──────────────────────────


def _compute_drift() -> LinkCommitResponse | None:
"""Run ``handle_link_commit`` against HEAD and return its
response. Returns ``None`` if the ledger is not configured (no
``~/.bicameral/`` directory) OR the handler raises — graceful skip
matches the hook's non-blocking design.

Lazy-imports the handler so unit tests can patch this whole
function without paying the SurrealDB import cost.
"""
try:
return _invoke_link_commit()
except Exception: # noqa: BLE001 — graceful skip on any handler failure
return None


def _invoke_link_commit() -> LinkCommitResponse | None:
"""Synchronous wrapper that drives the async ``handle_link_commit``.
Builds a minimal context, calls the handler against HEAD, returns
the response."""
import asyncio
from pathlib import Path

if not (Path.home() / ".bicameral" / "ledger.db").exists():
return None
from context import BicameralContext
from handlers.link_commit import handle_link_commit

async def _run() -> LinkCommitResponse:
ctx = BicameralContext.from_env()
return await handle_link_commit(ctx, commit_hash="HEAD")

return asyncio.run(_run())


def _resolve_exit_code() -> int:
"""Decide exit code when drift IS present. Three branches:

- BICAMERAL_PUSH_HOOK_BLOCK=1 → 2 (hard-block, no prompt)
- non-TTY → 0 (advisory only; never block automation)
- TTY → 0 (let the hook script handle the prompt itself)

The hook script's prompt logic owns ``_EXIT_USER_DECLINED=1``;
main() never returns 1 directly. main()'s job is just: 0 = clean/safe
to push, 2 = blocked.
"""
if os.environ.get(_BLOCK_ENV, "") == "1":
return _EXIT_BLOCK
if not _stdin_is_tty():
return _EXIT_OK
return _EXIT_OK


def _stdin_is_tty() -> bool:
"""Indirection for testability — patchable from unit tests so
they don't have to mock ``sys.stdin.isatty`` directly."""
return sys.stdin.isatty()


if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))
112 changes: 110 additions & 2 deletions docs/META_LEDGER.md
Original file line number Diff line number Diff line change
Expand Up @@ -661,6 +661,114 @@ SHA256(content_hash + previous_hash) = **`567170e0f1dc008cd5663201d8b1582dbabb59
**Reality matches Promise.** Implementation conforms to the audited specification (`d846a4a`) with one documented plan deviation (training README scaffolding). Phase 1 (test corpus extension) and Phase 2 (skill rubric + training doc) sealed in sequence; 8/8 new tests + 40/40 regression green. Chain integrity intact. Next phase: `/qor-document` then open PR `feat/44-llm-drift-judge → BicameralAI/dev`.

---
*Chain integrity: VALID (16 entries)*
*Genesis: `29dfd085` → Phase 1+2 Seal: `509b411d` → Phase 3 Seal: `89cac7ff` → Phase 4 Audit v1 (VETO): `231fe5f1` → Phase 4 Audit v2 (PASS): `332c72b2` → Phase 4 Audit v3 (PASS, post-rebase): `21ac210f` → Phase 4 SEAL: `0ebcf69b` → #44 Audit (PASS, post-remediation): `536dd15f` → #44 SEAL: `567170e0`*

## Entry #17 — GATE TRIBUNAL: `plan-48-pre-push-drift-hook.md` (Issue #48)

**Phase**: GATE / qor-audit
**Date**: 2026-04-29
**Branch**: `feat/48-pre-push-drift-hook` (off `BicameralAI/dev` post-#113 sticky drift report, current tip `77b9ee3`)
**Subject**: Issue #48 — *Pre-push git hook: surface drift warnings before `git push`*
**Risk Grade**: L2 (new CLI subcommand surface; modifies setup_wizard + server.py; no MCP tool changes, no schema, no contracts)
**Change Class**: minor
**Verdict**: **PASS** (first-attempt — no remediation needed)

### Audit history

| v | Plan commit | Verdict | Findings |
|---|---|---|---|
| v1 | `79abcc2` | **PASS** | All standard passes clean. SG-PLAN-GROUNDING-DRIFT instance #4 prevented (plan author ran `ls -d */` before submission). Three non-blocking observations: O1 (cosmetic param-name nit), O2 (latent post-commit-hook bug — recommend separate issue), O3 (two-renderer non-duplication accepted). |

### Plan content hash

`sha256:96045a11fbd403ca0ef55b12d0c02b5dfbf5fc42ee31d3980ed87b0617b71807`

### Audit report content hash

`sha256:d9a003e44bf9ee52e1801ea61f5c6fbf68187389b86d82807ebcd96cce3e7b66`

### Previous chain hash

`567170e0f1dc008cd5663201d8b1582dbabb5904527acb31ed5ea869b1cd8877` (Entry #16, #44 SEAL on dev)

### Chain hash

`SHA256(plan_hash + audit_hash + prev_hash) =` **`bf890347b6aac9097f5468f577c5cf2e7581af57cc1dc776bda5baad498fb37c`**

### Decision

PASS first-attempt. Plan-author-level grounding mitigation confirmed working — no `pilot/mcp/skills/` references, no fictional paths, all module/file claims pre-verified via filesystem `ls`. Three phases (branch-scan CLI / setup-wizard hook install / docs) all gate-cleared for implementation.

### Audit recommendations

- **File separately**: latent bug in existing post-commit hook — `bicameral-mcp link_commit HEAD` is not a registered subcommand of `cli_main`. Hook silently no-ops under `|| true`. Out of scope for #48.

---

## Entry #18 — SUBSTANTIATION SEAL: `plan-48-pre-push-drift-hook.md` (Issue #48)

**Phase**: SUBSTANTIATE / qor-substantiate
**Date**: 2026-04-29
**Branch**: `feat/48-pre-push-drift-hook` (off `BicameralAI/dev` post-#113)
**Plan commit**: `79abcc2`; implementation latest commit on branch
**Risk Grade**: L2 (new CLI subcommand surface; modifies setup_wizard + server.py; no MCP tool changes, no schema, no contracts)
**Change Class**: minor

### Verification gates

| Step | Check | Result | Notes |
|---|---|---|---|
| Step 2 | PASS verdict in AUDIT_REPORT.md | ✅ | Entry #17 audit PASS at `bf890347` (first-attempt — no remediation cycle). |
| Step 2.5 | Version validation | ✅ | Source remains v0.16.0 (current dev tip from PR #107); no version bump in this PR per maintainer direction. |
| Step 3 | Reality vs Promise | ✅ | All 4 new files + 3 modified files exist. Zero plan deviations — implementation matches plan 1:1. |
| Step 3.5 | Backlog blockers | ✅ | No new blockers. |
| Step 4 | Test audit | ✅ | 27/28 in targeted sweep (11 new + 16 regression on PR #113 drift_report tests; 1 chmod test skipped on Windows). |
| Step 4 (artifacts) | console.log / debug | ✅ | Zero. The `print()` statements in `cli/branch_scan.py` are stderr/stdout CLI status output — intentional design. |
| Step 4.5 | Skill file integrity | N/A | No `skills/*/SKILL.md` files modified (no MCP tool changes). |
| Step 4.6 | Reliability sweep | ⚠️ skip | `qor/reliability/` capability shortfall. |
| Step 5 | Section 4 razor final | ✅ | `cli/branch_scan.py` 177 LOC (≤250); entry funcs ≤25 LOC; helpers ≤20 LOC; nesting ≤2; zero nested ternaries. |
| Step 6 | SYSTEM_STATE.md sync | ✅ | Updated with #48 inventory; #44 history preserved below. |
| Step 7 | Merkle seal | ✅ | Computed below. |
| Step 7.5 | Annotated tag | ⚠️ skip | Per maintainer direction, no version bump in this PR. |

### Architectural decisions sealed

Q1 (`cli/branch_scan.py` placement), Q2 (deliberate non-modeling on broken predecessor), Q3 (HEAD-only v1), Q4 (TTY/no-TTY/no-ledger graceful behaviors), Q5 (setup_wizard pattern mirroring) — all implemented exactly as specified. Zero design deviations during implementation.

### Plan deviations (none)

First implementation in this session with zero plan deviations. Plan was thorough enough that implementation was direct.

### Carried-forward observations

- **Audit's separate-issue recommendation**: latent bug in existing post-commit hook (`bicameral-mcp link_commit HEAD` not a registered subcommand). NOT addressed in this PR — separate workstream.
- **SG-PLAN-GROUNDING-DRIFT prevention**: this is the second consecutive plan in the session where author-time `ls -d */` mitigation worked (no instance #4). Issue #114 (CI lint) remains the durable countermeasure.

### Capability shortfalls (carried)

- `qor/scripts/` runtime helpers absent.
- `qor/reliability/` enforcement scripts absent.
- `agent-teams` capability not declared — sequential mode.
- `codex-plugin` capability not declared — solo audit mode.
- `AUDIT_REPORT.md` lives at `.agent/staging/` rather than `.failsafe/governance/`.

### Session content hash

SHA256 over 8 sorted-path files (plan + 1 new prod + 2 modified prod + 2 tests + 1 guide + SYSTEM_STATE.md) =
**`d943569a6fd566fcb9dfe61bce660100ca28e84671b4ca465cac02065ab15023`**

### Previous chain hash

`bf890347b6aac9097f5468f577c5cf2e7581af57cc1dc776bda5baad498fb37c` (Entry #17 audit PASS first-attempt)

### Merkle seal

SHA256(content_hash + previous_hash) = **`eacc6f89f707ce958fa2485177c9706808fdfeb32b8e4865aadc8bcda47cb645`**

### Decision

**Reality matches Promise.** Implementation conforms to the audit-PASSED specification (`79abcc2`) with **zero plan deviations**. Phase 0 (branch-scan CLI) + Phase 1 (setup_wizard hook install) + Phase 2 (CHANGELOG + user guide) sealed in sequence; 11/12 new tests + 16/16 regression green (1 Windows-only chmod skip). Chain integrity intact on this branch. Next phase: `/qor-document` then open PR `feat/48-pre-push-drift-hook → BicameralAI/dev`.

---
*Chain integrity: VALID (18 entries on this branch)*
*Genesis: `29dfd085` → Phase 1+2 Seal: `509b411d` → Phase 3 Seal: `89cac7ff` → Phase 4 Audit v1 (VETO): `231fe5f1` → Phase 4 Audit v2 (PASS): `332c72b2` → Phase 4 Audit v3 (PASS, post-rebase): `21ac210f` → Phase 4 SEAL: `0ebcf69b` → #44 Audit (PASS, post-remediation): `536dd15f` → #44 SEAL: `567170e0` → #48 Audit (PASS, first-attempt): `bf890347` → #48 SEAL: `eacc6f89`*
*Next required action: `/qor-document` then open PR to `BicameralAI/dev`*
74 changes: 74 additions & 0 deletions docs/SYSTEM_STATE.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,77 @@
# System State — post-#48-substantiation snapshot

**Generated**: 2026-04-29
**HEAD**: latest (Issue #48 sealed)
**Branch**: `feat/48-pre-push-drift-hook` (off `BicameralAI/dev` post-#113, current dev tip `77b9ee3`)
**Tracked PR**: will target `BicameralAI/dev` (Issue #48); aggregate `dev → main` PR is downstream
**Genesis hash**: `29dfd085...`
**#48 seal**: see Entry #18 (computed during this substantiation)

## #48 (pre-push drift hook + branch-scan CLI) implementation — 7 files, ~609 LOC, 11 new tests, 27/28 targeted regression

| Phase | Files | New tests | Notes |
|---|---|---|---|
| 0 — branch-scan CLI subcommand | 1 new prod + 1 new test + 1 modified | 7 | `cli/branch_scan.py` 177 LOC, server.py +14 LOC |
| 1 — setup_wizard pre-push hook | 1 modified + 1 new test | 5 (1 chmod skipped on Windows) | setup_wizard.py +50 LOC, --with-push-hook flag |
| 2 — Documentation | 2 modified/new | 0 | CHANGELOG [Unreleased] + 129-LOC user guide |

### Files in scope

**New** (4):
- `cli/branch_scan.py` (177 LOC) — terminal-output drift renderer + main() CLI
- `tests/test_branch_scan_cli.py` (144 LOC, 7 tests)
- `tests/test_setup_pre_push_hook.py` (92 LOC, 5 tests)
- `docs/guides/pre-push-drift-hook.md` (129 LOC) — user guide
- `plan-48-pre-push-drift-hook.md` (366 LOC) — plan, committed at `79abcc2`

**Modified** (3):
- `server.py` (+14 LOC, branch-scan subparser + --with-push-hook flag)
- `setup_wizard.py` (+50 LOC, _GIT_PRE_PUSH_HOOK + _install_git_pre_push_hook + run_setup kwarg + step 7b)
- `CHANGELOG.md` (Unreleased entry under Added)

### Plan deviations (none)

Implementation matches plan 1:1. All design decisions Q1–Q5 implemented exactly as specified.

### Architectural decisions retained from plan

- **Q1**: `cli/branch_scan.py` placement (mirrors `cli/classify.py` and `cli/drift_report.py` patterns).
- **Q2**: Deliberate non-modeling on possibly-broken post-commit-hook predecessor — `branch-scan` registered properly via `cli_main` subparser.
- **Q3**: HEAD-only v1 (no multi-commit-range walk); v2 tracked as future enhancement.
- **Q4**: TTY/no-TTY/no-ledger graceful behaviors — all three branches implemented per spec.
- **Q5**: setup_wizard pattern mirrors `_install_git_post_commit_hook` exactly (idempotent install, append-on-existing).

### Capability shortfalls (carried across phases)

- `qor/scripts/` runtime helpers absent — gate-chain artifacts not written.
- `qor/reliability/` enforcement scripts absent — Step 4.6 reliability sweep skipped.
- `agent-teams` capability not declared — sequential mode.
- `codex-plugin` capability not declared — solo audit mode.
- v1 audit was first plan in session where SG-PLAN-GROUNDING-DRIFT prevention worked at *author-time* rather than audit-time. Issue #114 (CI lint enforcement) remains the durable countermeasure.

### Test state (post-implementation)

- Targeted sweep: 27/28 (11 new + 16 regression on PR #113's drift_report tests; 1 chmod test skipped on Windows non-POSIX).
- All test functions ≤ 25 LOC.
- All test files ≤ 144 LOC.
- ruff check + format: clean.
- mypy on `cli/branch_scan.py`: no issues.
- End-to-end smoke confirmed: `python -m server branch-scan` → graceful skip → exit 0 (no ledger configured locally).

### Workflow security review

- Hook reads `/dev/tty` for the prompt; input matched against fixed regex (`[yY]|[yY][eE][sS]`); no shell expansion of user-controlled input.
- Hook calls `bicameral-mcp branch-scan` from `PATH` — same trust model as the existing post-commit hook.
- No `pull_request_target` triggers introduced.
- File mode `0o755` (executable, world-readable). No secrets in hook content.
- Behavior: hook short-circuits (`exit 0`) when no `.bicameral/` directory in repo.

### Audit's separate-issue recommendation (NOT addressed in this PR)

Latent bug in existing post-commit hook: `bicameral-mcp link_commit HEAD` is not a registered subcommand of `cli_main`. The `|| true` swallows the argparse error. Recommended title: *"post-commit hook command bicameral-mcp link_commit HEAD not a registered CLI subcommand — hook silently no-ops"*. Out of scope for #48; tracked separately.

---

# System State — post-#44-substantiation snapshot

**Generated**: 2026-04-29
Expand Down
Loading
Loading