Skip to content

Commit 152a5ce

Browse files
committed
feat: validate memory index links
1 parent 07a647e commit 152a5ce

4 files changed

Lines changed: 65 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111
- Built-in `brigade doctor` bootstrap budget checks that fail hard when installed bootstrap files exceed conservative byte limits.
12+
- Built-in `brigade doctor` memory-index checks that fail when `MEMORY.md` links to missing `memory/cards/*.md` files.
1213
- `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.
1314
- `.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.
1415
- `brigade roster init` and `brigade roster doctor` to scaffold a Codex/Ollama starter roster and validate roster syntax plus installed CLI availability.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ Use `--output-dir <path>` to pick the artifact directory, or `--no-artifacts` fo
168168

169169
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`.
170170

171-
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.
171+
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.
172172

173173
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.
174174

src/brigade/doctor.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import json
55
import os
6+
import re
67
import shutil
78
import subprocess
89
import sys
@@ -64,6 +65,7 @@ def core_station_checks(ctx: DoctorContext) -> List[CheckResult]:
6465
def memory_station_checks(ctx: DoctorContext) -> List[CheckResult]:
6566
checks: List[CheckResult] = []
6667
checks.extend(_check_handoff_inboxes(ctx.target, ctx.selection, ctx.harnesses))
68+
checks.extend(_check_memory_index(ctx.target))
6769
checks.extend(_check_memory_care(ctx.target))
6870
return checks
6971

@@ -197,7 +199,8 @@ def _check_handoff_inboxes(
197199
)
198200
cards = target / "memory" / "cards"
199201
if cards.is_dir():
200-
results.append((OK, "memory: cards/", str(cards)))
202+
card_count = len([path for path in cards.rglob("*.md") if path.is_file()])
203+
results.append((OK, "memory: cards/", f"{cards} ({card_count} card{'s' if card_count != 1 else ''})"))
201204
else:
202205
results.append(
203206
(
@@ -209,6 +212,36 @@ def _check_handoff_inboxes(
209212
return results
210213

211214

215+
def _check_memory_index(target: Path) -> List[CheckResult]:
216+
index = target / "MEMORY.md"
217+
if not index.is_file():
218+
return []
219+
try:
220+
text = index.read_text()
221+
except OSError as exc:
222+
return [(FAIL, "memory-index: MEMORY.md", f"unreadable: {exc}")]
223+
224+
linked_cards = sorted(
225+
{
226+
match.group("path")
227+
for match in re.finditer(
228+
r"\[[^\]]+\]\((?P<path>memory/cards/[^)#\s]+\.md)(?:#[^)]+)?\)",
229+
text,
230+
)
231+
}
232+
)
233+
if not linked_cards:
234+
return [(WARN, "memory-index: card links", "MEMORY.md links no memory cards")]
235+
236+
missing = [path for path in linked_cards if not (target / path).is_file()]
237+
if missing:
238+
preview = ", ".join(missing[:5])
239+
if len(missing) > 5:
240+
preview += f", ... {len(missing) - 5} more"
241+
return [(FAIL, "memory-index: card links", f"{len(missing)} broken link{'s' if len(missing) != 1 else ''}: {preview}")]
242+
return [(OK, "memory-index: card links", f"{len(linked_cards)} verified")]
243+
244+
212245
def _check_orphan_inboxes(
213246
target: Path, selected_harnesses: List[str]
214247
) -> List[CheckResult]:

tests/test_doctor.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,35 @@ def test_doctor_reports_memory_care_files(tmp_target: Path, capsys):
113113
assert "1 queued" in out
114114

115115

116+
def test_doctor_verifies_memory_index_card_links(tmp_target: Path, capsys):
117+
install_selection(
118+
tmp_target,
119+
Selection(depth="workspace", harnesses=["claude"], owner="claude", includes=[]),
120+
)
121+
122+
rc = doctor_mod.run(target=tmp_target, harness="generic")
123+
out = capsys.readouterr().out
124+
assert rc == 0
125+
assert "memory-index: card links" in out
126+
assert "verified" in out
127+
128+
129+
def test_doctor_fails_broken_memory_index_card_link(tmp_target: Path, capsys):
130+
install_selection(
131+
tmp_target,
132+
Selection(depth="workspace", harnesses=["claude"], owner="claude", includes=[]),
133+
)
134+
memory = tmp_target / "MEMORY.md"
135+
memory.write_text(memory.read_text() + "\n- [missing-card](memory/cards/missing-card.md)\n")
136+
137+
rc = doctor_mod.run(target=tmp_target, harness="generic")
138+
out = capsys.readouterr().out
139+
assert rc == 1
140+
assert "memory-index: card links" in out
141+
assert "broken link" in out
142+
assert "memory/cards/missing-card.md" in out
143+
144+
116145
def test_doctor_openclaw_reports_cron_memory_jobs(tmp_target: Path, monkeypatch, capsys):
117146
install_selection(
118147
tmp_target,

0 commit comments

Comments
 (0)