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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Built-in `brigade doctor` bootstrap budget checks that fail hard when installed bootstrap files exceed conservative byte limits.
- Built-in `brigade doctor` memory-card budget checks that fail when `memory/cards/*.md` cards become too large.
- Built-in `brigade doctor` memory-index checks that fail when `MEMORY.md` links to missing `memory/cards/*.md` files.
- `brigade doctor` memory-care freshness checks for stale decay scans, plus hard failures for corrupt scan or refresh-queue JSON.
- `brigade run "<task>"`, a bounded aboyeur flow that asks one rostered orchestrator to plan assignments, dispatches worker CLIs in parallel, then asks the orchestrator to synthesize the final answer.
- `.brigade/roster.toml` loading for cross-model agent rosters using the user's installed CLIs (`codex`, `claude`, or `ollama:<model>`). Claude is optional, not required.
- `brigade roster init` and `brigade roster doctor` to scaffold a Codex/Ollama starter roster and validate roster syntax plus installed CLI availability.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ Use `--output-dir <path>` to pick the artifact directory, or `--no-artifacts` fo

Use `brigade work status` as the quick daily dashboard for a repo. It reports the current branch, dirty files, dogfood readiness, configured dogfood paths, latest dogfood run, and extracted next step without starting a new orchestration. Use `brigade work doctor` to check whether the repo is ready for the daily loop: dogfood config, Codex CLI, local artifact paths, handoff inbox, ignore coverage, and latest run context. Use `brigade work resume` when returning to a repo; it shows the active or latest work session, latest dogfood run, extracted next step, and the suggested command to continue. `brigade work run` is the one-command daily loop: it starts a work session, runs `brigade dogfood`, ends the session, writes a work-session Memory Handoff by default, and prints a compact recap. Pass a task to override the default next-slice review, `--title` to name the session, `--no-handoff` to skip the work handoff, or `--dogfood-handoff` to also let the underlying dogfood run write its own handoff. `brigade work start "title"` opens a local work session under `.brigade/work/<id>/`, records the starting git and dogfood context, and writes `start.md`. `brigade work note "checkpoint"` appends a timestamped note to the active session without ending it. `brigade work end --note "what happened"` closes the active session, records ending context, and writes `end.md`. Add `--handoff` to also write a Memory Handoff for the closed session; it defaults to the configured dogfood handoff inbox or `.codex/memory-handoffs`.

Memory and bootstrap readiness are part of the same operating-system health story. `brigade doctor` checks installed bootstrap files against built-in hard byte budgets so overgrown files fail before agents load truncated context. It also checks `memory/cards/*.md` budgets and verifies that `MEMORY.md` card links resolve under `memory/cards/`, so the bootstrap index cannot quietly point at missing or overgrown durable memory. Bootstrap truncation is treated as a hard failure to prevent, not a cosmetic warning; keep cards atomic and bootstrap files as slim indexes.
Memory and bootstrap readiness are part of the same operating-system health story. `brigade doctor` checks installed bootstrap files against built-in hard byte budgets so overgrown files fail before agents load truncated context. It also checks `memory/cards/*.md` budgets, verifies that `MEMORY.md` card links resolve under `memory/cards/`, and reports memory-care freshness from `memory/cards/decay/scan-latest.json`. Missing memory-care decay state is advisory for fresh installs, but corrupt scan or refresh-queue JSON fails once the loop is wired. Bootstrap truncation is treated as a hard failure to prevent, not a cosmetic warning; keep cards atomic and bootstrap files as slim indexes.

Inspect local work sessions with `brigade work list`, `brigade work latest`, or `brigade work show <session-id-or-path>`. Use `brigade work recap` for a compact summary of recent sessions, or add `--since YYYY-MM-DD` for a day-range recap.

Expand Down
77 changes: 62 additions & 15 deletions src/brigade/doctor.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import shutil
import subprocess
import sys
from datetime import date, datetime
from pathlib import Path
from typing import Callable, List, Tuple

Expand All @@ -29,6 +30,7 @@
"HEARTBEAT.md": 5_000,
}
MEMORY_CARD_BUDGET_BYTES = 8_000
MEMORY_CARE_SCAN_STALE_DAYS = 7

from .station import DoctorContext

Expand Down Expand Up @@ -331,36 +333,81 @@ def _check_memory_care(target: Path) -> List[CheckResult]:
detail = str(scan)
try:
data = json.loads(scan.read_text())
scan_date = data.get("scan_date")
counts = data.get("counts", {})
if scan_date:
detail = f"{scan} (scan_date={scan_date}, stale={counts.get('stale', 'unknown')})"
if not isinstance(data, dict):
results.append((FAIL, "memory-care: scan-latest", f"expected JSON object: {scan}"))
else:
scan_date = data.get("scan_date")
counts = data.get("counts", {})
if not isinstance(counts, dict):
counts = {}
if scan_date:
detail = f"{scan} (scan_date={scan_date}, stale={counts.get('stale', 'unknown')})"
results.append((OK, "memory-care: scan-latest", detail))
results.append(_check_memory_care_scan_freshness(scan, scan_date))
except json.JSONDecodeError:
detail = f"invalid JSON: {scan}"
results.append((WARN, "memory-care: scan-latest", detail))
else:
results.append((OK, "memory-care: scan-latest", detail))
results.append((FAIL, "memory-care: scan-latest", f"invalid JSON: {scan}"))
else:
results.append((WARN, "memory-care: scan-latest", f"missing at {scan}"))

if queue.is_file():
detail = str(queue)
try:
data = json.loads(queue.read_text())
cards = data.get("cards", [])
if isinstance(cards, list):
detail = f"{queue} ({len(cards)} queued)"
if not isinstance(data, dict):
results.append((FAIL, "memory-care: refresh-queue", f"expected JSON object: {queue}"))
else:
cards = data.get("cards", [])
if not isinstance(cards, list):
results.append((FAIL, "memory-care: refresh-queue", f"`cards` must be a list: {queue}"))
else:
detail = f"{queue} ({len(cards)} queued)"
results.append((OK, "memory-care: refresh-queue", detail))
except json.JSONDecodeError:
detail = f"invalid JSON: {queue}"
results.append((WARN, "memory-care: refresh-queue", detail))
else:
results.append((OK, "memory-care: refresh-queue", detail))
results.append((FAIL, "memory-care: refresh-queue", f"invalid JSON: {queue}"))
else:
results.append((WARN, "memory-care: refresh-queue", f"missing at {queue}"))

return results


def _check_memory_care_scan_freshness(scan: Path, scan_date: object) -> CheckResult:
if not scan_date:
return (WARN, "memory-care: scan freshness", f"scan_date missing in {scan}")
parsed = _parse_memory_care_scan_date(scan_date)
if parsed is None:
return (WARN, "memory-care: scan freshness", f"unparseable scan_date={scan_date!r} in {scan}")
age_days = (_memory_care_today() - parsed).days
if age_days < 0:
return (WARN, "memory-care: scan freshness", f"scan_date is in the future: {scan_date}")
if age_days > MEMORY_CARE_SCAN_STALE_DAYS:
return (
WARN,
"memory-care: scan freshness",
f"last scan {age_days} days ago; run memory-care scanner",
)
return (OK, "memory-care: scan freshness", f"last scan {age_days} days ago")


def _parse_memory_care_scan_date(value: object) -> date | None:
if not isinstance(value, str):
return None
text = value.strip()
if not text:
return None
try:
return datetime.fromisoformat(text.replace("Z", "+00:00")).date()
except ValueError:
pass
try:
return date.fromisoformat(text[:10])
except ValueError:
return None


def _memory_care_today() -> date:
return date.today()


def _check_publish_gate(target: Path) -> List[CheckResult]:
results: List[CheckResult] = []
hook = target / "hooks" / "pre-push"
Expand Down
46 changes: 45 additions & 1 deletion tests/test_doctor.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Tests for brigade doctor."""
from __future__ import annotations

from datetime import date
from pathlib import Path
import json

Expand Down Expand Up @@ -92,7 +93,8 @@ def test_doctor_hermes_flags_experimental(tmp_target: Path, capsys):
assert rc == 0


def test_doctor_reports_memory_care_files(tmp_target: Path, capsys):
def test_doctor_reports_memory_care_files(tmp_target: Path, monkeypatch, capsys):
monkeypatch.setattr(doctor_mod, "_memory_care_today", lambda: date(2026, 5, 14))
install_selection(
tmp_target,
Selection(depth="workspace", harnesses=["claude"], owner="claude", includes=[]),
Expand All @@ -109,10 +111,52 @@ def test_doctor_reports_memory_care_files(tmp_target: Path, capsys):
assert rc == 0
assert "memory-care: scan-latest" in out
assert "stale=2" in out
assert "memory-care: scan freshness" in out
assert "last scan 1 days ago" in out
assert "memory-care: refresh-queue" in out
assert "1 queued" in out


def test_doctor_warns_when_memory_care_scan_is_stale(tmp_target: Path, monkeypatch, capsys):
monkeypatch.setattr(doctor_mod, "_memory_care_today", lambda: date(2026, 5, 26))
install_selection(
tmp_target,
Selection(depth="workspace", harnesses=["claude"], owner="claude", includes=[]),
)
decay = tmp_target / "memory" / "cards" / "decay"
decay.mkdir(exist_ok=True)
(decay / "scan-latest.json").write_text(
json.dumps({"scan_date": "2026-05-01", "counts": {"stale": 4}})
)
(decay / "refresh-queue.json").write_text(json.dumps({"cards": []}))

rc = doctor_mod.run(target=tmp_target, harness="generic")
out = capsys.readouterr().out
assert rc == 0
assert "memory-care: scan freshness" in out
assert "last scan 25 days ago" in out
assert "run memory-care scanner" in out


def test_doctor_fails_when_memory_care_state_is_invalid(tmp_target: Path, capsys):
install_selection(
tmp_target,
Selection(depth="workspace", harnesses=["claude"], owner="claude", includes=[]),
)
decay = tmp_target / "memory" / "cards" / "decay"
decay.mkdir(exist_ok=True)
(decay / "scan-latest.json").write_text("{not-json")
(decay / "refresh-queue.json").write_text(json.dumps({"cards": "not-a-list"}))

rc = doctor_mod.run(target=tmp_target, harness="generic")
out = capsys.readouterr().out
assert rc == 1
assert "memory-care: scan-latest" in out
assert "invalid JSON" in out
assert "memory-care: refresh-queue" in out
assert "`cards` must be a list" in out


def test_doctor_verifies_memory_index_card_links(tmp_target: Path, capsys):
install_selection(
tmp_target,
Expand Down
Loading