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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ htmlcov/
.DS_Store
Thumbs.db

# claude / agent local state (side-harness only; solo-mise block below
# claude / agent local state (side-harness only; brigade block below
# handles the handoff path with TEMPLATE.md kept tracked)
.codex/
.openclaw/
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `brigade work recap` to summarize recent or date-filtered work sessions.
- `brigade work run` to start a work session, run dogfood, close the session, write a work handoff, and print a recap in one command.
- `brigade work resume` to show the active or latest work session, latest dogfood run, extracted next step, and suggested command.
- `brigade work next` to resolve the next daily task without inspecting artifacts, plus `brigade work run` now uses the latest extracted next step when no task is passed.
- `brigade work note` to append timestamped checkpoints to the active work session without ending it.
- `brigade work doctor` to check dogfood config, Codex availability, local artifact paths, handoff inbox, ignore coverage, and latest run context for the daily work loop.
- Workspace installs now include `.brigade/memory-care.example.json` as a scanner wiring contract for memory-care decay output.
- Roster-level and per-agent `timeout_seconds` controls for bounded CLI calls.
- `brigade run --read-only` prompt policy for planning and review runs that should inspect and recommend only, with native `codex exec --sandbox read-only` enforcement for Codex agents.

Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ brigade dogfood --target /path/to/repo
brigade work status
brigade work doctor
brigade work resume
brigade work next
brigade work run
brigade work run "review today's changes"
brigade work start "next slice"
Expand Down Expand Up @@ -166,9 +167,9 @@ CLI runs write artifacts by default under `.brigade/runs/<id>` below `--cwd`; do

Use `--output-dir <path>` to pick the artifact directory, or `--no-artifacts` for a throwaway run.

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`.
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. Use `brigade work next` when you only need the task Brigade would run next. `brigade work run` is the one-command daily loop: it starts a work session, uses the latest extracted dogfood next step when no task is provided, 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 resolved next step, `--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, 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.
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`. Workspace installs include `.brigade/memory-care.example.json` as a scanner wiring contract for whatever scheduler or memory owner produces the decay files. 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
4 changes: 4 additions & 0 deletions src/brigade/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ def _build_parser() -> argparse.ArgumentParser:
p_work_doctor.add_argument("--target", "-t", type=Path, default=Path("."), help="Repo or workspace to inspect.")
p_work_resume = work_sub.add_parser("resume", help="Show the current work handoff point and next command.")
p_work_resume.add_argument("--target", "-t", type=Path, default=Path("."), help="Repo or workspace to inspect.")
p_work_next = work_sub.add_parser("next", help="Show the next daily work task and suggested command.")
p_work_next.add_argument("--target", "-t", type=Path, default=Path("."), help="Repo or workspace to inspect.")
p_work_list = work_sub.add_parser("list", help="List recent Brigade work sessions.")
p_work_list.add_argument("--target", "-t", type=Path, default=Path("."), help="Repo or workspace to inspect.")
p_work_list.add_argument("--limit", type=int, default=10, help="Maximum sessions to show.")
Expand Down Expand Up @@ -443,6 +445,8 @@ def main(argv=None) -> int:
return work_cmd.doctor(target=args.target)
if args.work_command == "resume":
return work_cmd.resume(target=args.target)
if args.work_command == "next":
return work_cmd.next(target=args.target)
if args.work_command == "list":
return work_cmd.list_sessions(target=args.target, limit=args.limit)
if args.work_command == "latest":
Expand Down
14 changes: 11 additions & 3 deletions src/brigade/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@

GITIGNORE_BEGIN = "# >>> brigade gitignore block >>>"
GITIGNORE_END = "# <<< brigade gitignore block <<<"
LEGACY_GITIGNORE_BEGIN = "# >>> solo-mise gitignore block >>>"
LEGACY_GITIGNORE_END = "# <<< solo-mise gitignore block <<<"

# Writer harness -> inbox-dir prefix. Only writer harnesses have an inbox.
_WRITER_INBOX = {
Expand Down Expand Up @@ -78,9 +80,15 @@ def apply_gitignore(target: Path, selection: Selection) -> str:
gi.write_text(block)
return "created"
existing = gi.read_text()
if GITIGNORE_BEGIN in existing and GITIGNORE_END in existing:
prefix, _, rest = existing.partition(GITIGNORE_BEGIN)
_, _, suffix = rest.partition(GITIGNORE_END)
markers = (
(GITIGNORE_BEGIN, GITIGNORE_END),
(LEGACY_GITIGNORE_BEGIN, LEGACY_GITIGNORE_END),
)
for begin, end in markers:
if begin not in existing or end not in existing:
continue
prefix, _, rest = existing.partition(begin)
_, _, suffix = rest.partition(end)
# Strip a trailing newline from prefix and a leading newline from suffix to avoid drift.
new_text = prefix.rstrip("\n") + ("\n\n" if prefix.strip() else "") + block + suffix.lstrip("\n")
gi.write_text(new_text)
Expand Down
1 change: 1 addition & 0 deletions src/brigade/templates/depth/workspace.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
{"src": "memory/cards/pipeline-standups.md", "dst": "memory/cards/pipeline-standups.md"},
{"src": "memory/cards/obsidian-notes.md", "dst": "memory/cards/obsidian-notes.md"},
{"src": "memory/cards/backup-restic.md", "dst": "memory/cards/backup-restic.md"},
{"src": "memory/memory-care.example.json", "dst": ".brigade/memory-care.example.json"},
{"src": "skills/note/SKILL.md", "dst": "skills/note/SKILL.md"},
{"src": "scripts/backup-restic.sh", "dst": "scripts/backup-restic.sh", "mode": "0755"}
],
Expand Down
33 changes: 33 additions & 0 deletions src/brigade/templates/memory/cards/memory-care-staleness.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,39 @@ Store scan state under `memory/cards/decay/`:

The scanner should report at least total cards, fresh count, aging count, stale count, critical count, and refresh queue size.

`brigade init --depth workspace` also installs `.brigade/memory-care.example.json` as a local wiring contract for whatever scanner you use. Adapt that file for your scheduler or memory owner, but keep the output paths stable unless you also update the doctor integration.

Minimal `scan-latest.json` shape:

```json
{
"scan_date": "2026-05-26",
"counts": {
"total": 12,
"fresh": 9,
"aging": 2,
"stale": 1,
"critical": 0
},
"refresh_queue_size": 1
}
```

Minimal `refresh-queue.json` shape:

```json
{
"cards": [
{
"file": "memory/cards/example.md",
"reason": "source-of-truth changed"
}
]
}
```

`brigade doctor` treats missing decay output as advisory, stale scans older than 7 days as a warning, and corrupt scan or queue JSON as a failure.

## Safe Refresh Rules

Only auto-refresh cards when current facts are grounded in local source-of-truth files read during the run. Good sources include `TOOLS.md`, `MEMORY.md`, recent `memory/YYYY-MM-DD.md`, local project docs, local scripts, and repo health reports.
Expand Down
11 changes: 11 additions & 0 deletions src/brigade/templates/memory/memory-care.example.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"_brigade_version": "0.1.0",
"_description": "Example config for a local memory-care scanner. Brigade does not run this scanner for you; it validates the output with `brigade doctor`.",
"cards_glob": "memory/cards/*.md",
"decay_dir": "memory/cards/decay",
"scan_output": "memory/cards/decay/scan-latest.json",
"refresh_queue": "memory/cards/decay/refresh-queue.json",
"stale_after_days": 7,
"schedule": "quiet-hours",
"doctor_command": "brigade doctor --target ."
}
71 changes: 70 additions & 1 deletion src/brigade/work_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,22 @@ def _next_step(snapshot: dict[str, Any]) -> str | None:
return None


def _resolve_next_task(target: Path) -> dict[str, Any]:
dogfood = _dogfood_snapshot(target)
next_step = dogfood.get("next") if isinstance(dogfood.get("next"), str) else None
if next_step and next_step.strip():
return {
"task": next_step.strip(),
"source": "latest_dogfood_run",
"dogfood": dogfood,
}
return {
"task": dogfood_cmd.DEFAULT_TASK,
"source": "default_review",
"dogfood": dogfood,
}


def _display_session(path: Path, payload: dict[str, Any]) -> None:
print(f"session: {path}")
print(f"id: {payload.get('id', path.name)}")
Expand Down Expand Up @@ -732,6 +748,58 @@ def resume(*, target: Path) -> int:
return 0


def next(*, target: Path) -> int:
target = target.expanduser().resolve()
if not target.is_dir():
print(f"error: --target is not a directory: {target}", file=sys.stderr)
return 2

print(f"work next: {target}")
root = _work_root(target)
current = _current_path(target)
active_payload: dict[str, Any] | None = None
if current.exists():
active_dir = root / current.read_text().strip()
active_payload = _read_session(active_dir)
if active_payload is None:
print(f"active_session: invalid ({active_dir})")
else:
print(f"active_session: {active_dir}")
print(f"active_session_status: {active_payload.get('status', 'unknown')}")
if active_payload.get("title"):
print(f"active_session_title: {_short(str(active_payload['title']))}")
else:
print("active_session: none")

resolved = _resolve_next_task(target)
dogfood = resolved["dogfood"]
print(f"dogfood_ready: {dogfood.get('ready')}")
if dogfood.get("error"):
print(f"dogfood_error: {dogfood['error']}")
latest_run = dogfood.get("latest_run")
if isinstance(latest_run, dict):
print(
"latest_run: "
f"{latest_run.get('started_at', '')} "
f"[{latest_run.get('status', 'unknown')}] {latest_run.get('path')}"
)
if latest_run.get("task"):
print(f"latest_task: {_short(str(latest_run['task']))}")
else:
print("latest_run: none")

task = str(resolved["task"])
print(f"next_source: {resolved['source']}")
print(f"next: {_short(task)}")
if active_payload is not None:
print('suggested_command: brigade work end --note "..." --handoff')
elif resolved["source"] == "latest_dogfood_run":
print("suggested_command: brigade work run")
else:
print("suggested_command: brigade work run")
return 0


def run(
task: str | None,
*,
Expand All @@ -755,7 +823,8 @@ def run(
print(f"error: --target is not a directory: {target}", file=sys.stderr)
return 2

task_text = task or dogfood_cmd.DEFAULT_TASK
resolved = _resolve_next_task(target)
task_text = task or str(resolved["task"])
session_title = title or task_text
start_rc = start(target=target, title=session_title)
if start_rc != 0:
Expand Down
33 changes: 33 additions & 0 deletions tests/test_gitignore.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,39 @@ def test_init_idempotent_replaces_block(tmp_target: Path):
assert second.count(install_mod.GITIGNORE_BEGIN) == 1


def test_init_replaces_legacy_solo_mise_gitignore_block(tmp_target: Path):
tmp_target.mkdir()
(tmp_target / ".gitignore").write_text(
"\n".join(
[
"# user rules",
"*.log",
"",
install_mod.LEGACY_GITIGNORE_BEGIN,
"GARBAGE_LINE",
install_mod.LEGACY_GITIGNORE_END,
"",
"# after block",
".local-cache/",
"",
]
)
)

rc = install_selection(tmp_target, _repo_selection())

assert rc == 0
gi = _read_gi(tmp_target)
assert "# user rules" in gi
assert ".local-cache/" in gi
assert install_mod.GITIGNORE_BEGIN in gi
assert install_mod.GITIGNORE_END in gi
assert install_mod.LEGACY_GITIGNORE_BEGIN not in gi
assert install_mod.LEGACY_GITIGNORE_END not in gi
assert "GARBAGE_LINE" not in gi
assert gi.count(install_mod.GITIGNORE_BEGIN) == 1


def test_init_preserves_user_edits_outside_block(tmp_target: Path):
tmp_target.mkdir()
pre = "node_modules/\n# user rules\n*.swp\n"
Expand Down
1 change: 1 addition & 0 deletions tests/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ def test_workspace_install_includes_memory_cards(tmp_target: Path):
assert (tmp_target / "memory" / "cards" / card).is_file(), f"missing card {card}"
assert (tmp_target / "memory" / "handoff-inbox").is_dir()
assert (tmp_target / ".claude" / "memory-handoffs" / "processed").is_dir()
assert (tmp_target / ".brigade" / "memory-care.example.json").is_file()
# skill + script land at the right paths, executable bit on the script
assert (tmp_target / "skills" / "note" / "SKILL.md").is_file()
backup = tmp_target / "scripts" / "backup-restic.sh"
Expand Down
Loading
Loading