Skip to content

Commit 37dc233

Browse files
authored
Merge pull request #12 from escoffier-labs/feat/brigade-dogfood-status
feat: report brigade dogfood status
2 parents fd4e448 + a2cdb64 commit 37dc233

5 files changed

Lines changed: 188 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1313
- `brigade roster init` and `brigade roster doctor` to scaffold a Codex/Ollama starter roster and validate roster syntax plus installed CLI availability.
1414
- `brigade dogfood` for a built-in Codex-only, prompt-level read-only, inspected run with artifacts and optional handoff.
1515
- `brigade dogfood init` to persist machine-local dogfood defaults in gitignored `.brigade/dogfood.toml`, enabling a one-command daily `brigade dogfood` path.
16+
- `brigade dogfood status` to report local dogfood readiness, effective paths, CLI availability, ignore coverage, sandbox mode, and latest run.
1617
- `brigade run --show-plan` and `--verbose` visibility modes, plus defensive runtime enforcement of roster `allow_models`.
1718
- `brigade run --inspect` to print a readable artifact summary immediately after a run completes.
1819
- `brigade run --cwd`, `--output-dir`, and default `.brigade/runs/<id>` artifacts for dogfooding auditable runs.

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,13 +130,14 @@ brigade run "review this repo" --handoff
130130
brigade run "review this repo" --read-only
131131
brigade run "review this repo" --read-only --inspect
132132
brigade dogfood init --target /path/to/repo
133+
brigade dogfood status
133134
brigade dogfood
134135
brigade dogfood --target /path/to/repo
135136
```
136137

137138
`--dry-run` prints the planned assignments as JSON and stops before worker dispatch. `--show-plan` prints assignments before a normal run. `--verbose` prints the plan, worker statuses, and synthesis status. `--cwd` sets the working directory for the agent CLI calls and defaults to the current directory. `--handoff` writes a Memory Handoff for a successful non-dry run. `--inspect` prints the same readable artifact summary as `brigade runs show` after the run completes. `--read-only` tells the orchestrator and workers to inspect and recommend only, without modifying files or external state. For `codex` agents, Brigade also passes `codex exec --sandbox read-only`; other adapters receive the prompt policy only. The `cli` values are adapters for installed command-line tools: `codex`, `claude`, and `ollama:<model>`. Pick the ones you already use. Brigade shells out to those tools and keeps no provider keys. `brigade roster doctor` validates the roster syntax and reports which CLIs are present on `PATH`.
138139

139-
`brigade dogfood` is the shortcut for using Brigade on itself or another trusted repo. It uses a built-in Codex-only roster, runs with prompt-level read-only instructions, shows the plan, writes normal run artifacts, writes a Memory Handoff by default, and prints the artifact summary afterward. Run `brigade dogfood init --target /path/to/repo` once to write local defaults to `.brigade/dogfood.toml`; that file is gitignored because it captures machine-local paths and preferences. After that, `brigade dogfood` from the repo is the one-command daily path, and `brigade dogfood "review today's changes"` overrides only the task. Dogfood defaults to a 600 second per-agent timeout because full repo review can exceed short smoke-test limits. By default it passes Codex's `danger-full-access` sandbox setting for trusted-workspace use so repo inspection works on hosts where native read-only sandboxing blocks shell inspection. Use `--no-handoff` or `--no-inspect` to turn off those last two steps. Use `--native-read-only-sandbox` when the host supports Codex's native read-only sandbox and you want that additional enforcement.
140+
`brigade dogfood` is the shortcut for using Brigade on itself or another trusted repo. It uses a built-in Codex-only roster, runs with prompt-level read-only instructions, shows the plan, writes normal run artifacts, writes a Memory Handoff by default, and prints the artifact summary afterward. Run `brigade dogfood init --target /path/to/repo` once to write local defaults to `.brigade/dogfood.toml`; that file is gitignored because it captures machine-local paths and preferences. After that, `brigade dogfood` from the repo is the one-command daily path, and `brigade dogfood "review today's changes"` overrides only the task. Use `brigade dogfood status` to inspect the effective target, artifact paths, handoff path, sandbox mode, CLI availability, ignore rules, and latest run without starting a new orchestration. Dogfood defaults to a 600 second per-agent timeout because full repo review can exceed short smoke-test limits. By default it passes Codex's `danger-full-access` sandbox setting for trusted-workspace use so repo inspection works on hosts where native read-only sandboxing blocks shell inspection. Use `--no-handoff` or `--no-inspect` to turn off those last two steps. Use `--native-read-only-sandbox` when the host supports Codex's native read-only sandbox and you want that additional enforcement.
140141

141142
CLI runs write artifacts by default under `.brigade/runs/<id>` below `--cwd`; dogfood runs use `.brigade/runs/<id>` below the configured target:
142143

src/brigade/cli.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,11 @@ def main(argv=None) -> int:
334334
native_read_only_sandbox=args.native_read_only_sandbox,
335335
timeout_seconds=args.timeout_seconds,
336336
)
337+
if dogfood_args and dogfood_args[0] == "status":
338+
if len(dogfood_args) > 1:
339+
print("error: dogfood status does not accept a task argument", file=sys.stderr)
340+
return 2
341+
return dogfood_cmd.status(target=args.target)
337342
task = " ".join(dogfood_args) if dogfood_args else None
338343
return dogfood_cmd.run(
339344
task,

src/brigade/dogfood_cmd.py

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,13 @@
22
from __future__ import annotations
33

44
import ast
5+
import json
6+
import shutil
7+
import subprocess
58
import sys
69
from dataclasses import dataclass
710
from pathlib import Path
11+
from typing import Any
812

913
from . import aboyeur
1014
from . import runs_cmd
@@ -134,6 +138,131 @@ def load_config(target: Path) -> DogfoodConfig | None:
134138
)
135139

136140

141+
def _check_git_ignored(repo: Path, path: Path) -> str:
142+
try:
143+
relative = path.expanduser().resolve().relative_to(repo)
144+
except ValueError:
145+
return "outside-target"
146+
try:
147+
result = subprocess.run(
148+
["git", "-C", str(repo), "check-ignore", "-q", str(relative)],
149+
check=False,
150+
stdout=subprocess.DEVNULL,
151+
stderr=subprocess.DEVNULL,
152+
)
153+
except OSError:
154+
return "unknown"
155+
if result.returncode == 0:
156+
return "yes"
157+
if result.returncode == 1:
158+
return "no"
159+
return "unknown"
160+
161+
162+
def _latest_run(runs_dir: Path) -> tuple[Path, dict[str, Any]] | None:
163+
if not runs_dir.is_dir():
164+
return None
165+
latest: tuple[Path, dict[str, Any]] | None = None
166+
latest_key = ""
167+
for child in runs_dir.iterdir():
168+
if not child.is_dir():
169+
continue
170+
try:
171+
payload = json.loads((child / "run.json").read_text())
172+
except (OSError, json.JSONDecodeError):
173+
continue
174+
if not isinstance(payload, dict):
175+
continue
176+
key = str(payload.get("started_at") or child.name)
177+
if latest is None or key > latest_key:
178+
latest = (child, payload)
179+
latest_key = key
180+
return latest
181+
182+
183+
def _setting_line(label: str, value: object) -> None:
184+
print(f"{label}: {value}")
185+
186+
187+
def status(*, target: Path) -> int:
188+
target = target.expanduser().resolve()
189+
if not target.is_dir():
190+
print(f"error: --target is not a directory: {target}", file=sys.stderr)
191+
return 2
192+
193+
path = config_path(target)
194+
try:
195+
cfg = load_config(target)
196+
except ValueError as exc:
197+
print(f"error: invalid dogfood config: {exc}", file=sys.stderr)
198+
return 2
199+
200+
effective_target = cfg.target if cfg and cfg.target is not None else target
201+
effective_target = effective_target.expanduser().resolve()
202+
artifacts_dir = cfg.artifacts_dir if cfg and cfg.artifacts_dir is not None else effective_target / ".brigade" / "runs"
203+
handoff = cfg.handoff if cfg else True
204+
handoff_inbox = (
205+
cfg.handoff_inbox
206+
if cfg and cfg.handoff_inbox is not None
207+
else effective_target / ".claude" / "memory-handoffs"
208+
)
209+
inspect = cfg.inspect if cfg else True
210+
native = cfg.native_read_only_sandbox if cfg else False
211+
timeout = cfg.timeout_seconds if cfg else DEFAULT_TIMEOUT_SECONDS
212+
213+
codex_path = shutil.which("codex")
214+
brigade_path = shutil.which("brigade")
215+
blockers: list[str] = []
216+
warnings: list[str] = []
217+
if cfg is None:
218+
warnings.append(f"config missing: run `brigade dogfood init --target {target}`")
219+
if not effective_target.is_dir():
220+
blockers.append(f"configured target is not a directory: {effective_target}")
221+
if codex_path is None:
222+
blockers.append("codex CLI not found on PATH")
223+
if brigade_path is None:
224+
warnings.append("brigade CLI not found on PATH; use the venv command or install the package")
225+
226+
ready = not blockers
227+
print(f"dogfood: {'ready' if ready else 'not ready'}")
228+
_setting_line("config", path if path.exists() else f"{path} (missing)")
229+
_setting_line("target", effective_target)
230+
_setting_line("artifacts_dir", artifacts_dir)
231+
_setting_line("handoff", "enabled" if handoff else "disabled")
232+
if handoff:
233+
_setting_line("handoff_inbox", handoff_inbox)
234+
_setting_line("inspect", "enabled" if inspect else "disabled")
235+
_setting_line(
236+
"sandbox",
237+
"native read-only" if native else "prompt read-only + trusted-workspace execution",
238+
)
239+
_setting_line("timeout_seconds", f"{timeout:g}")
240+
_setting_line("codex", codex_path or "missing")
241+
_setting_line("brigade", brigade_path or "missing")
242+
_setting_line("config_ignored", _check_git_ignored(effective_target, path))
243+
_setting_line("artifacts_ignored", _check_git_ignored(effective_target, artifacts_dir))
244+
latest = _latest_run(artifacts_dir)
245+
if latest is not None:
246+
latest_path, latest_meta = latest
247+
task = " ".join(str(latest_meta.get("task") or "").split())
248+
if len(task) > 80:
249+
task = task[:77].rstrip() + "..."
250+
_setting_line(
251+
"latest_run",
252+
f"{latest_meta.get('started_at', latest_path.name)} [{latest_meta.get('status', 'unknown')}] {latest_path}",
253+
)
254+
if task:
255+
_setting_line("latest_task", task)
256+
else:
257+
_setting_line("latest_run", "none")
258+
259+
for warning in warnings:
260+
print(f"warning: {warning}", file=sys.stderr)
261+
for blocker in blockers:
262+
print(f"error: {blocker}", file=sys.stderr)
263+
return 0 if ready else 1
264+
265+
137266
def init(
138267
*,
139268
target: Path,

tests/test_dogfood_cmd.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,44 @@ def fake_run(
173173
assert "inspect" not in seen
174174

175175

176+
def test_dogfood_status_reports_config_and_latest_run(tmp_path, monkeypatch, capsys):
177+
dogfood_cmd.init(target=tmp_path, timeout_seconds=33)
178+
run_dir = tmp_path / ".brigade" / "runs" / "20260526-120000-test"
179+
run_dir.mkdir(parents=True)
180+
(run_dir / "run.json").write_text(
181+
'{"started_at":"2026-05-26T12:00:00Z","status":"ok","task":"review the repo"}'
182+
)
183+
184+
monkeypatch.setattr(dogfood_cmd.shutil, "which", lambda name: f"/usr/bin/{name}")
185+
monkeypatch.setattr(dogfood_cmd, "_check_git_ignored", lambda repo, path: "yes")
186+
187+
assert dogfood_cmd.status(target=tmp_path) == 0
188+
captured = capsys.readouterr()
189+
assert "dogfood: ready" in captured.out
190+
assert f"config: {tmp_path / '.brigade' / 'dogfood.toml'}" in captured.out
191+
assert f"target: {tmp_path.resolve()}" in captured.out
192+
assert "artifacts_ignored: yes" in captured.out
193+
assert "codex: /usr/bin/codex" in captured.out
194+
assert "brigade: /usr/bin/brigade" in captured.out
195+
assert "timeout_seconds: 33" in captured.out
196+
assert "latest_run: 2026-05-26T12:00:00Z [ok]" in captured.out
197+
assert "latest_task: review the repo" in captured.out
198+
199+
200+
def test_dogfood_status_reports_missing_codex(tmp_path, monkeypatch, capsys):
201+
monkeypatch.setattr(dogfood_cmd.shutil, "which", lambda name: None)
202+
monkeypatch.setattr(dogfood_cmd, "_check_git_ignored", lambda repo, path: "no")
203+
204+
assert dogfood_cmd.status(target=tmp_path) == 1
205+
captured = capsys.readouterr()
206+
assert "dogfood: not ready" in captured.out
207+
assert "config:" in captured.out
208+
assert "(missing)" in captured.out
209+
assert "codex: missing" in captured.out
210+
assert "warning: config missing" in captured.err
211+
assert "error: codex CLI not found on PATH" in captured.err
212+
213+
176214
def test_dogfood_rejects_missing_target(tmp_path, capsys):
177215
assert dogfood_cmd.run(None, target=tmp_path / "missing") == 2
178216
assert "--target is not a directory" in capsys.readouterr().err
@@ -265,3 +303,16 @@ def fake_init(**kwargs):
265303
"native_read_only_sandbox": True,
266304
"timeout_seconds": 12.0,
267305
}
306+
307+
308+
def test_dogfood_cli_status(tmp_path, monkeypatch):
309+
seen = {}
310+
311+
def fake_status(**kwargs):
312+
seen.update(kwargs)
313+
return 0
314+
315+
monkeypatch.setattr(dogfood_cmd, "status", fake_status)
316+
317+
assert cli.main(["dogfood", "status", "--target", str(tmp_path)]) == 0
318+
assert seen == {"target": tmp_path}

0 commit comments

Comments
 (0)