diff --git a/CHANGELOG.md b/CHANGELOG.md index 32e386a0..d02eed26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Built-in `brigade doctor` bootstrap budget checks that fail hard when installed bootstrap files exceed conservative byte limits. +- Built-in `brigade doctor` memory-index checks that fail when `MEMORY.md` links to missing `memory/cards/*.md` files. - `brigade run ""`, 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:`). 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. diff --git a/README.md b/README.md index b81dd0e2..bbfb42fa 100644 --- a/README.md +++ b/README.md @@ -168,7 +168,7 @@ Use `--output-dir ` 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//`, 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. Bootstrap truncation is treated as a hard failure to prevent, not a cosmetic warning; move durable detail into `memory/cards/` and keep 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 verifies that `MEMORY.md` card links resolve under `memory/cards/`, so the bootstrap index cannot quietly point at missing durable memory. Bootstrap truncation is treated as a hard failure to prevent, not a cosmetic warning; move durable detail into `memory/cards/` and keep bootstrap files as slim indexes. Inspect local work sessions with `brigade work list`, `brigade work latest`, or `brigade work show `. Use `brigade work recap` for a compact summary of recent sessions, or add `--since YYYY-MM-DD` for a day-range recap. diff --git a/src/brigade/doctor.py b/src/brigade/doctor.py index 06d09a63..33c37afc 100644 --- a/src/brigade/doctor.py +++ b/src/brigade/doctor.py @@ -3,6 +3,7 @@ import json import os +import re import shutil import subprocess import sys @@ -64,6 +65,7 @@ def core_station_checks(ctx: DoctorContext) -> List[CheckResult]: def memory_station_checks(ctx: DoctorContext) -> List[CheckResult]: checks: List[CheckResult] = [] checks.extend(_check_handoff_inboxes(ctx.target, ctx.selection, ctx.harnesses)) + checks.extend(_check_memory_index(ctx.target)) checks.extend(_check_memory_care(ctx.target)) return checks @@ -197,7 +199,8 @@ def _check_handoff_inboxes( ) cards = target / "memory" / "cards" if cards.is_dir(): - results.append((OK, "memory: cards/", str(cards))) + card_count = len([path for path in cards.rglob("*.md") if path.is_file()]) + results.append((OK, "memory: cards/", f"{cards} ({card_count} card{'s' if card_count != 1 else ''})")) else: results.append( ( @@ -209,6 +212,36 @@ def _check_handoff_inboxes( return results +def _check_memory_index(target: Path) -> List[CheckResult]: + index = target / "MEMORY.md" + if not index.is_file(): + return [] + try: + text = index.read_text() + except OSError as exc: + return [(FAIL, "memory-index: MEMORY.md", f"unreadable: {exc}")] + + linked_cards = sorted( + { + match.group("path") + for match in re.finditer( + r"\[[^\]]+\]\((?Pmemory/cards/[^)#\s]+\.md)(?:#[^)]+)?\)", + text, + ) + } + ) + if not linked_cards: + return [(WARN, "memory-index: card links", "MEMORY.md links no memory cards")] + + missing = [path for path in linked_cards if not (target / path).is_file()] + if missing: + preview = ", ".join(missing[:5]) + if len(missing) > 5: + preview += f", ... {len(missing) - 5} more" + return [(FAIL, "memory-index: card links", f"{len(missing)} broken link{'s' if len(missing) != 1 else ''}: {preview}")] + return [(OK, "memory-index: card links", f"{len(linked_cards)} verified")] + + def _check_orphan_inboxes( target: Path, selected_harnesses: List[str] ) -> List[CheckResult]: diff --git a/tests/test_doctor.py b/tests/test_doctor.py index b4a3994b..88e6e8f8 100644 --- a/tests/test_doctor.py +++ b/tests/test_doctor.py @@ -113,6 +113,35 @@ def test_doctor_reports_memory_care_files(tmp_target: Path, capsys): assert "1 queued" in out +def test_doctor_verifies_memory_index_card_links(tmp_target: Path, capsys): + install_selection( + tmp_target, + Selection(depth="workspace", harnesses=["claude"], owner="claude", includes=[]), + ) + + rc = doctor_mod.run(target=tmp_target, harness="generic") + out = capsys.readouterr().out + assert rc == 0 + assert "memory-index: card links" in out + assert "verified" in out + + +def test_doctor_fails_broken_memory_index_card_link(tmp_target: Path, capsys): + install_selection( + tmp_target, + Selection(depth="workspace", harnesses=["claude"], owner="claude", includes=[]), + ) + memory = tmp_target / "MEMORY.md" + memory.write_text(memory.read_text() + "\n- [missing-card](memory/cards/missing-card.md)\n") + + rc = doctor_mod.run(target=tmp_target, harness="generic") + out = capsys.readouterr().out + assert rc == 1 + assert "memory-index: card links" in out + assert "broken link" in out + assert "memory/cards/missing-card.md" in out + + def test_doctor_openclaw_reports_cron_memory_jobs(tmp_target: Path, monkeypatch, capsys): install_selection( tmp_target,