Skip to content

Commit 81301e8

Browse files
authored
Merge pull request #27 from escoffier-labs/feat/memory-card-budget-doctor
feat: fail oversized memory cards
2 parents 584e9a2 + 1629d31 commit 81301e8

4 files changed

Lines changed: 81 additions & 1 deletion

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-card budget checks that fail when `memory/cards/*.md` cards become too large.
1213
- Built-in `brigade doctor` memory-index checks that fail when `MEMORY.md` links to missing `memory/cards/*.md` files.
1314
- `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.
1415
- `.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.

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. 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.
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 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.
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: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"IDENTITY.md": 4_000,
2929
"HEARTBEAT.md": 5_000,
3030
}
31+
MEMORY_CARD_BUDGET_BYTES = 8_000
3132

3233
from .station import DoctorContext
3334

@@ -65,6 +66,7 @@ def core_station_checks(ctx: DoctorContext) -> List[CheckResult]:
6566
def memory_station_checks(ctx: DoctorContext) -> List[CheckResult]:
6667
checks: List[CheckResult] = []
6768
checks.extend(_check_handoff_inboxes(ctx.target, ctx.selection, ctx.harnesses))
69+
checks.extend(_check_memory_cards(ctx.target))
6870
checks.extend(_check_memory_index(ctx.target))
6971
checks.extend(_check_memory_care(ctx.target))
7072
return checks
@@ -242,6 +244,51 @@ def _check_memory_index(target: Path) -> List[CheckResult]:
242244
return [(OK, "memory-index: card links", f"{len(linked_cards)} verified")]
243245

244246

247+
def _check_memory_cards(target: Path) -> List[CheckResult]:
248+
cards = target / "memory" / "cards"
249+
if not cards.is_dir():
250+
return []
251+
252+
results: List[CheckResult] = []
253+
oversized: list[str] = []
254+
empty: list[str] = []
255+
for path in sorted(cards.rglob("*.md")):
256+
if not path.is_file():
257+
continue
258+
rel = path.relative_to(target)
259+
try:
260+
size = path.stat().st_size
261+
except OSError as exc:
262+
results.append((FAIL, f"memory-card: {rel}", f"unreadable: {exc}"))
263+
continue
264+
if size == 0:
265+
empty.append(str(rel))
266+
if size > MEMORY_CARD_BUDGET_BYTES:
267+
oversized.append(f"{rel} ({size}/{MEMORY_CARD_BUDGET_BYTES} bytes)")
268+
269+
if empty:
270+
preview = ", ".join(empty[:5])
271+
if len(empty) > 5:
272+
preview += f", ... {len(empty) - 5} more"
273+
results.append((WARN, "memory-card: empty", f"{len(empty)} empty card{'s' if len(empty) != 1 else ''}: {preview}"))
274+
275+
if oversized:
276+
preview = ", ".join(oversized[:5])
277+
if len(oversized) > 5:
278+
preview += f", ... {len(oversized) - 5} more"
279+
results.append(
280+
(
281+
FAIL,
282+
"memory-card: budget",
283+
f"{len(oversized)} over hard limit; split cards into atomic topics: {preview}",
284+
)
285+
)
286+
else:
287+
count = len([path for path in cards.rglob("*.md") if path.is_file()])
288+
results.append((OK, "memory-card: budget", f"{count} card{'s' if count != 1 else ''} <= {MEMORY_CARD_BUDGET_BYTES} bytes"))
289+
return results
290+
291+
245292
def _check_orphan_inboxes(
246293
target: Path, selected_harnesses: List[str]
247294
) -> List[CheckResult]:

tests/test_doctor.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,38 @@ def test_doctor_fails_broken_memory_index_card_link(tmp_target: Path, capsys):
142142
assert "memory/cards/missing-card.md" in out
143143

144144

145+
def test_doctor_fails_when_memory_card_exceeds_budget(tmp_target: Path, capsys):
146+
install_selection(
147+
tmp_target,
148+
Selection(depth="workspace", harnesses=["claude"], owner="claude", includes=[]),
149+
)
150+
limit = doctor_mod.MEMORY_CARD_BUDGET_BYTES
151+
oversized = tmp_target / "memory" / "cards" / "oversized.md"
152+
oversized.write_text("x" * (limit + 1))
153+
154+
rc = doctor_mod.run(target=tmp_target, harness="generic")
155+
out = capsys.readouterr().out
156+
assert rc == 1
157+
assert "memory-card: budget" in out
158+
assert "over hard limit" in out
159+
assert "memory/cards/oversized.md" in out
160+
161+
162+
def test_doctor_warns_when_memory_card_is_empty(tmp_target: Path, capsys):
163+
install_selection(
164+
tmp_target,
165+
Selection(depth="workspace", harnesses=["claude"], owner="claude", includes=[]),
166+
)
167+
empty = tmp_target / "memory" / "cards" / "empty.md"
168+
empty.write_text("")
169+
170+
rc = doctor_mod.run(target=tmp_target, harness="generic")
171+
out = capsys.readouterr().out
172+
assert rc == 0
173+
assert "memory-card: empty" in out
174+
assert "memory/cards/empty.md" in out
175+
176+
145177
def test_doctor_openclaw_reports_cron_memory_jobs(tmp_target: Path, monkeypatch, capsys):
146178
install_selection(
147179
tmp_target,

0 commit comments

Comments
 (0)