Skip to content

Commit 913e12c

Browse files
authored
Merge pull request #21 from escoffier-labs/feat/provider-neutral-handoffs
fix: default dogfood handoffs to codex
2 parents bffb7b3 + a2dd783 commit 913e12c

8 files changed

Lines changed: 50 additions & 15 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3737
- `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.
3838

3939
### Changed
40+
- Dogfood handoff defaults now use `.codex/memory-handoffs/` for new Codex-driven local configs while preserving explicit configured inbox paths such as `.claude/memory-handoffs/`.
4041
- Dogfood runs now default to a 600 second per-agent timeout for practical daily repo reviews.
4142
- The managed gitignore block now treats `.brigade/dogfood.toml` and `.brigade/runs/` as local state.
4243
- Live smoke docs now keep Codex agent execution in a trusted repo cwd while writing temporary roster, artifacts, and handoff output under `/tmp`.

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ The cookbook explains the why. This package gives you the kitchen.
3535

3636
- sanitized bootstrap files for agent behavior, safety, tools, identity, and memory
3737
- a canonical memory layout where one configured owner holds durable knowledge
38-
- a shared `.claude/memory-handoffs/` inbox for Claude Code, Codex, and other side harnesses
38+
- writer-specific Memory Handoff inboxes, such as `.claude/memory-handoffs/` for Claude Code and `.codex/memory-handoffs/` for Codex
3939
- starter memory cards and routing rules
4040
- multi-workspace handoff patterns for people administering more than one agent setup
4141
- memory-care staleness checks so durable cards do not quietly rot
@@ -146,7 +146,7 @@ brigade work recap --since 2026-05-26
146146

147147
`--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`.
148148

149-
`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. Use `brigade dogfood latest` to show the latest configured dogfood run, and `brigade dogfood next` to print the latest extracted next step. Dogfood writes a `summary.md` beside each run's JSON artifacts with the final answer and extracted next step when present. 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.
149+
`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. New dogfood configs default the handoff inbox to `.codex/memory-handoffs/` under the target because the dogfood roster is Codex-driven; pass `--handoff-inbox` if your memory owner ingests a different path, including existing `.claude/memory-handoffs/` setups. 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. Use `brigade dogfood latest` to show the latest configured dogfood run, and `brigade dogfood next` to print the latest extracted next step. Dogfood writes a `summary.md` beside each run's JSON artifacts with the final answer and extracted next step when present. 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.
150150

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

@@ -175,7 +175,7 @@ brigade runs latest --cwd /path/to/repo
175175
brigade runs show .brigade/runs/<run-id>
176176
```
177177

178-
Use `--handoff` to bridge a completed run back into the memory system. By default it writes a reviewable handoff to `.claude/memory-handoffs/` under `--cwd`; override with `--handoff-inbox <path>`. The handoff targets `.learnings/LEARNINGS.md` as a `no-card` document update, so the normal `brigade ingest` route can review or ingest it. If handoff writing fails after synthesis, Brigade still prints the final answer and keeps the final artifacts, but exits nonzero and marks `run.json` as `handoff-failed`. `--handoff` is not allowed with `--dry-run` because dry runs have no final answer.
178+
Use `--handoff` to bridge a completed run back into the memory system. By default it writes a reviewable handoff to `.claude/memory-handoffs/` under `--cwd`; override with `--handoff-inbox <path>` for Codex, OpenCode, GPT, Hermes, OpenClaw, or any other non-Claude writer inbox. The handoff targets `.learnings/LEARNINGS.md` as a `no-card` document update, so the normal `brigade ingest` route can review or ingest it. If handoff writing fails after synthesis, Brigade still prints the final answer and keeps the final artifacts, but exits nonzero and marks `run.json` as `handoff-failed`. `--handoff` is not allowed with `--dry-run` because dry runs have no final answer.
179179

180180
Live smoke test, using a temporary Codex-only roster:
181181

src/brigade/cli.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ def _build_parser() -> argparse.ArgumentParser:
9595
"--handoff-inbox",
9696
type=Path,
9797
default=None,
98-
help="Memory Handoff inbox. Defaults to .claude/memory-handoffs under --target.",
98+
help="Memory Handoff inbox. Defaults to .codex/memory-handoffs under the effective target.",
9999
)
100100
p_dogfood.add_argument("--force", action="store_true", help="Overwrite an existing dogfood config during init.")
101101
p_dogfood.add_argument("--no-handoff", action="store_true", help="Do not write a Memory Handoff.")
@@ -158,7 +158,7 @@ def _build_parser() -> argparse.ArgumentParser:
158158
"--handoff-inbox",
159159
type=Path,
160160
default=None,
161-
help="Memory Handoff inbox. Defaults to configured dogfood inbox or .claude/memory-handoffs.",
161+
help="Memory Handoff inbox. Defaults to configured dogfood inbox or .codex/memory-handoffs.",
162162
)
163163

164164
# run
@@ -285,7 +285,7 @@ def _build_parser() -> argparse.ArgumentParser:
285285
)
286286

287287
# ingest
288-
p_ing = sub.add_parser("ingest", help="Process .claude/memory-handoffs/*.md into canonical memory.")
288+
p_ing = sub.add_parser("ingest", help="Process writer memory-handoff inboxes into canonical memory.")
289289
p_ing.add_argument("--target", "-t", type=Path, default=Path("."))
290290
p_ing.add_argument("--dry-run", action="store_true")
291291
p_ing.add_argument(

src/brigade/dogfood_cmd.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
DEFAULT_TASK = "Review this repo and recommend the next implementation slice."
1818
DEFAULT_TIMEOUT_SECONDS = 600.0
1919
CONFIG_REL_PATH = ".brigade/dogfood.toml"
20+
DEFAULT_HANDOFF_INBOX_REL_PATH = ".codex/memory-handoffs"
2021
NEXT_LABELS = (
2122
"next practical slice",
2223
"smallest follow-up slice",
@@ -124,6 +125,10 @@ def config_path(target: Path) -> Path:
124125
return target / CONFIG_REL_PATH
125126

126127

128+
def default_handoff_inbox(target: Path) -> Path:
129+
return target / DEFAULT_HANDOFF_INBOX_REL_PATH
130+
131+
127132
def load_config(target: Path) -> DogfoodConfig | None:
128133
target = target.expanduser().resolve()
129134
path = config_path(target)
@@ -314,7 +319,7 @@ def status(*, target: Path) -> int:
314319
handoff_inbox = (
315320
cfg.handoff_inbox
316321
if cfg and cfg.handoff_inbox is not None
317-
else effective_target / ".claude" / "memory-handoffs"
322+
else default_handoff_inbox(effective_target)
318323
)
319324
inspect = cfg.inspect if cfg else True
320325
native = cfg.native_read_only_sandbox if cfg else False
@@ -403,7 +408,7 @@ def init(
403408

404409
chosen_artifacts_dir = artifacts_dir.expanduser() if artifacts_dir is not None else target / ".brigade" / "runs"
405410
chosen_handoff_inbox = (
406-
handoff_inbox.expanduser() if handoff_inbox is not None else target / ".claude" / "memory-handoffs"
411+
handoff_inbox.expanduser() if handoff_inbox is not None else default_handoff_inbox(target)
407412
)
408413
payload = {
409414
"target": str(target),
@@ -473,7 +478,7 @@ def run(
473478
if handoff_inbox is not None
474479
else cfg.handoff_inbox
475480
if cfg and cfg.handoff_inbox is not None
476-
else effective_target / ".claude" / "memory-handoffs"
481+
else default_handoff_inbox(effective_target)
477482
)
478483

479484
rc = aboyeur.run(

src/brigade/ingest.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
"""`brigade ingest` - route .claude/memory-handoffs/*.md into canonical memory.
1+
"""`brigade ingest` - route writer memory handoffs into canonical memory.
22
33
Conservative by design:
44
- auto-promote handoffs with safe card filenames + YAML frontmatter
55
- append-only routing for TOOLS.md, USER.md, rules/*.md, .learnings/*.md
66
- everything ambiguous lands in memory/handoff-inbox/ for manual review
7-
- processed files move to .claude/memory-handoffs/processed/
7+
- processed files move to the writer inbox's processed/ directory
88
"""
99
from __future__ import annotations
1010

src/brigade/work_cmd.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,11 @@ def _dogfood_snapshot(target: Path) -> dict[str, Any]:
8484
"config": str(dogfood_cmd.config_path(target)),
8585
"target": str(effective_target),
8686
"artifacts_dir": str(artifacts_dir),
87-
"handoff_inbox": str(cfg.handoff_inbox) if cfg and cfg.handoff_inbox is not None else None,
87+
"handoff_inbox": str(
88+
cfg.handoff_inbox
89+
if cfg and cfg.handoff_inbox is not None
90+
else dogfood_cmd.default_handoff_inbox(effective_target)
91+
),
8892
}
8993
if latest is None:
9094
snapshot["latest_run"] = None
@@ -284,7 +288,7 @@ def _handoff_inbox(target: Path, payload: dict[str, Any], override: Path | None)
284288
configured = dogfood.get("handoff_inbox")
285289
if isinstance(configured, str) and configured:
286290
return Path(configured).expanduser()
287-
return target / ".claude" / "memory-handoffs"
291+
return dogfood_cmd.default_handoff_inbox(target)
288292

289293

290294
def _write_work_handoff(target: Path, session_dir: Path, payload: dict[str, Any], inbox: Path) -> Path:
@@ -666,7 +670,7 @@ def status(*, target: Path, limit: int = 12) -> int:
666670
print(f"dogfood_artifacts: {artifacts_dir}")
667671
print(f"codex: {codex_path or 'missing'}")
668672
if cfg and cfg.handoff:
669-
handoff_inbox = cfg.handoff_inbox or effective_target / ".claude" / "memory-handoffs"
673+
handoff_inbox = cfg.handoff_inbox or dogfood_cmd.default_handoff_inbox(effective_target)
670674
print(f"handoff_inbox: {handoff_inbox}")
671675

672676
latest = dogfood_cmd._latest_run(artifacts_dir)

tests/test_dogfood_cmd.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ def fake_show(run_dir):
5151
assert seen["show_plan"] is True
5252
assert seen["cwd"] == tmp_path.resolve()
5353
assert seen["output_dir"].parent == tmp_path / ".brigade" / "runs"
54-
assert seen["handoff_inbox"] == tmp_path / ".claude" / "memory-handoffs"
54+
assert seen["handoff_inbox"] == tmp_path / ".codex" / "memory-handoffs"
5555
assert seen["read_only"] is True
5656
assert seen["sandbox_read_only"] is None
5757
assert seen["sandbox"] == "danger-full-access"
@@ -169,6 +169,14 @@ def test_dogfood_init_writes_local_config(tmp_path, capsys):
169169
assert f"wrote {config}" in capsys.readouterr().out
170170

171171

172+
def test_dogfood_init_defaults_handoff_inbox_to_codex_writer(tmp_path):
173+
assert dogfood_cmd.init(target=tmp_path) == 0
174+
175+
loaded = dogfood_cmd.load_config(tmp_path)
176+
assert loaded is not None
177+
assert loaded.handoff_inbox == tmp_path / ".codex" / "memory-handoffs"
178+
179+
172180
def test_dogfood_init_refuses_existing_without_force(tmp_path, capsys):
173181
assert dogfood_cmd.init(target=tmp_path) == 0
174182
assert dogfood_cmd.init(target=tmp_path) == 2

tests/test_work_cmd.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,23 @@ def test_work_end_can_write_handoff(tmp_path, monkeypatch, capsys):
152152
assert payload["handoff"] == str(handoffs[0])
153153

154154

155+
def test_work_end_defaults_handoff_to_codex_inbox(tmp_path, monkeypatch):
156+
_init_git_repo(tmp_path)
157+
times = iter(
158+
[
159+
datetime(2026, 5, 26, 12, 0, 0, tzinfo=timezone.utc),
160+
datetime(2026, 5, 26, 13, 0, 0, tzinfo=timezone.utc),
161+
]
162+
)
163+
monkeypatch.setattr(work_cmd, "_now", lambda: next(times))
164+
assert work_cmd.start(target=tmp_path, title="Build Work Loop") == 0
165+
166+
assert work_cmd.end(target=tmp_path, note="done for now", handoff=True) == 0
167+
session_dir = tmp_path / ".brigade" / "work" / "20260526-120000-build-work-loop"
168+
payload = json.loads((session_dir / "session.json").read_text())
169+
assert payload["handoff"].startswith(str(tmp_path / ".codex" / "memory-handoffs"))
170+
171+
155172
def test_work_end_reports_no_active_session(tmp_path, capsys):
156173
_init_git_repo(tmp_path)
157174

0 commit comments

Comments
 (0)