Skip to content

Commit 7294e4b

Browse files
authored
Merge pull request #29 from escoffier-labs/feat/daily-work-next-loop
feat: drive daily work from next task
2 parents bc638a7 + 8bccccd commit 7294e4b

12 files changed

Lines changed: 254 additions & 7 deletions

File tree

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ htmlcov/
3131
.DS_Store
3232
Thumbs.db
3333

34-
# claude / agent local state (side-harness only; solo-mise block below
34+
# claude / agent local state (side-harness only; brigade block below
3535
# handles the handoff path with TEMPLATE.md kept tracked)
3636
.codex/
3737
.openclaw/

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3838
- `brigade work recap` to summarize recent or date-filtered work sessions.
3939
- `brigade work run` to start a work session, run dogfood, close the session, write a work handoff, and print a recap in one command.
4040
- `brigade work resume` to show the active or latest work session, latest dogfood run, extracted next step, and suggested command.
41+
- `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.
4142
- `brigade work note` to append timestamped checkpoints to the active work session without ending it.
4243
- `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.
44+
- Workspace installs now include `.brigade/memory-care.example.json` as a scanner wiring contract for memory-care decay output.
4345
- Roster-level and per-agent `timeout_seconds` controls for bounded CLI calls.
4446
- `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.
4547

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ brigade dogfood --target /path/to/repo
137137
brigade work status
138138
brigade work doctor
139139
brigade work resume
140+
brigade work next
140141
brigade work run
141142
brigade work run "review today's changes"
142143
brigade work start "next slice"
@@ -166,9 +167,9 @@ CLI runs write artifacts by default under `.brigade/runs/<id>` below `--cwd`; do
166167

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

169-
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`.
170+
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`.
170171

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, 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.
172+
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.
172173

173174
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.
174175

src/brigade/cli.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,8 @@ def _build_parser() -> argparse.ArgumentParser:
118118
p_work_doctor.add_argument("--target", "-t", type=Path, default=Path("."), help="Repo or workspace to inspect.")
119119
p_work_resume = work_sub.add_parser("resume", help="Show the current work handoff point and next command.")
120120
p_work_resume.add_argument("--target", "-t", type=Path, default=Path("."), help="Repo or workspace to inspect.")
121+
p_work_next = work_sub.add_parser("next", help="Show the next daily work task and suggested command.")
122+
p_work_next.add_argument("--target", "-t", type=Path, default=Path("."), help="Repo or workspace to inspect.")
121123
p_work_list = work_sub.add_parser("list", help="List recent Brigade work sessions.")
122124
p_work_list.add_argument("--target", "-t", type=Path, default=Path("."), help="Repo or workspace to inspect.")
123125
p_work_list.add_argument("--limit", type=int, default=10, help="Maximum sessions to show.")
@@ -443,6 +445,8 @@ def main(argv=None) -> int:
443445
return work_cmd.doctor(target=args.target)
444446
if args.work_command == "resume":
445447
return work_cmd.resume(target=args.target)
448+
if args.work_command == "next":
449+
return work_cmd.next(target=args.target)
446450
if args.work_command == "list":
447451
return work_cmd.list_sessions(target=args.target, limit=args.limit)
448452
if args.work_command == "latest":

src/brigade/install.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626

2727
GITIGNORE_BEGIN = "# >>> brigade gitignore block >>>"
2828
GITIGNORE_END = "# <<< brigade gitignore block <<<"
29+
LEGACY_GITIGNORE_BEGIN = "# >>> solo-mise gitignore block >>>"
30+
LEGACY_GITIGNORE_END = "# <<< solo-mise gitignore block <<<"
2931

3032
# Writer harness -> inbox-dir prefix. Only writer harnesses have an inbox.
3133
_WRITER_INBOX = {
@@ -78,9 +80,15 @@ def apply_gitignore(target: Path, selection: Selection) -> str:
7880
gi.write_text(block)
7981
return "created"
8082
existing = gi.read_text()
81-
if GITIGNORE_BEGIN in existing and GITIGNORE_END in existing:
82-
prefix, _, rest = existing.partition(GITIGNORE_BEGIN)
83-
_, _, suffix = rest.partition(GITIGNORE_END)
83+
markers = (
84+
(GITIGNORE_BEGIN, GITIGNORE_END),
85+
(LEGACY_GITIGNORE_BEGIN, LEGACY_GITIGNORE_END),
86+
)
87+
for begin, end in markers:
88+
if begin not in existing or end not in existing:
89+
continue
90+
prefix, _, rest = existing.partition(begin)
91+
_, _, suffix = rest.partition(end)
8492
# Strip a trailing newline from prefix and a leading newline from suffix to avoid drift.
8593
new_text = prefix.rstrip("\n") + ("\n\n" if prefix.strip() else "") + block + suffix.lstrip("\n")
8694
gi.write_text(new_text)

src/brigade/templates/depth/workspace.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
{"src": "memory/cards/pipeline-standups.md", "dst": "memory/cards/pipeline-standups.md"},
2121
{"src": "memory/cards/obsidian-notes.md", "dst": "memory/cards/obsidian-notes.md"},
2222
{"src": "memory/cards/backup-restic.md", "dst": "memory/cards/backup-restic.md"},
23+
{"src": "memory/memory-care.example.json", "dst": ".brigade/memory-care.example.json"},
2324
{"src": "skills/note/SKILL.md", "dst": "skills/note/SKILL.md"},
2425
{"src": "scripts/backup-restic.sh", "dst": "scripts/backup-restic.sh", "mode": "0755"}
2526
],

src/brigade/templates/memory/cards/memory-care-staleness.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,39 @@ Store scan state under `memory/cards/decay/`:
3333

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

36+
`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.
37+
38+
Minimal `scan-latest.json` shape:
39+
40+
```json
41+
{
42+
"scan_date": "2026-05-26",
43+
"counts": {
44+
"total": 12,
45+
"fresh": 9,
46+
"aging": 2,
47+
"stale": 1,
48+
"critical": 0
49+
},
50+
"refresh_queue_size": 1
51+
}
52+
```
53+
54+
Minimal `refresh-queue.json` shape:
55+
56+
```json
57+
{
58+
"cards": [
59+
{
60+
"file": "memory/cards/example.md",
61+
"reason": "source-of-truth changed"
62+
}
63+
]
64+
}
65+
```
66+
67+
`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.
68+
3669
## Safe Refresh Rules
3770

3871
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.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"_brigade_version": "0.1.0",
3+
"_description": "Example config for a local memory-care scanner. Brigade does not run this scanner for you; it validates the output with `brigade doctor`.",
4+
"cards_glob": "memory/cards/*.md",
5+
"decay_dir": "memory/cards/decay",
6+
"scan_output": "memory/cards/decay/scan-latest.json",
7+
"refresh_queue": "memory/cards/decay/refresh-queue.json",
8+
"stale_after_days": 7,
9+
"schedule": "quiet-hours",
10+
"doctor_command": "brigade doctor --target ."
11+
}

src/brigade/work_cmd.py

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,22 @@ def _next_step(snapshot: dict[str, Any]) -> str | None:
211211
return None
212212

213213

214+
def _resolve_next_task(target: Path) -> dict[str, Any]:
215+
dogfood = _dogfood_snapshot(target)
216+
next_step = dogfood.get("next") if isinstance(dogfood.get("next"), str) else None
217+
if next_step and next_step.strip():
218+
return {
219+
"task": next_step.strip(),
220+
"source": "latest_dogfood_run",
221+
"dogfood": dogfood,
222+
}
223+
return {
224+
"task": dogfood_cmd.DEFAULT_TASK,
225+
"source": "default_review",
226+
"dogfood": dogfood,
227+
}
228+
229+
214230
def _display_session(path: Path, payload: dict[str, Any]) -> None:
215231
print(f"session: {path}")
216232
print(f"id: {payload.get('id', path.name)}")
@@ -732,6 +748,58 @@ def resume(*, target: Path) -> int:
732748
return 0
733749

734750

751+
def next(*, target: Path) -> int:
752+
target = target.expanduser().resolve()
753+
if not target.is_dir():
754+
print(f"error: --target is not a directory: {target}", file=sys.stderr)
755+
return 2
756+
757+
print(f"work next: {target}")
758+
root = _work_root(target)
759+
current = _current_path(target)
760+
active_payload: dict[str, Any] | None = None
761+
if current.exists():
762+
active_dir = root / current.read_text().strip()
763+
active_payload = _read_session(active_dir)
764+
if active_payload is None:
765+
print(f"active_session: invalid ({active_dir})")
766+
else:
767+
print(f"active_session: {active_dir}")
768+
print(f"active_session_status: {active_payload.get('status', 'unknown')}")
769+
if active_payload.get("title"):
770+
print(f"active_session_title: {_short(str(active_payload['title']))}")
771+
else:
772+
print("active_session: none")
773+
774+
resolved = _resolve_next_task(target)
775+
dogfood = resolved["dogfood"]
776+
print(f"dogfood_ready: {dogfood.get('ready')}")
777+
if dogfood.get("error"):
778+
print(f"dogfood_error: {dogfood['error']}")
779+
latest_run = dogfood.get("latest_run")
780+
if isinstance(latest_run, dict):
781+
print(
782+
"latest_run: "
783+
f"{latest_run.get('started_at', '')} "
784+
f"[{latest_run.get('status', 'unknown')}] {latest_run.get('path')}"
785+
)
786+
if latest_run.get("task"):
787+
print(f"latest_task: {_short(str(latest_run['task']))}")
788+
else:
789+
print("latest_run: none")
790+
791+
task = str(resolved["task"])
792+
print(f"next_source: {resolved['source']}")
793+
print(f"next: {_short(task)}")
794+
if active_payload is not None:
795+
print('suggested_command: brigade work end --note "..." --handoff')
796+
elif resolved["source"] == "latest_dogfood_run":
797+
print("suggested_command: brigade work run")
798+
else:
799+
print("suggested_command: brigade work run")
800+
return 0
801+
802+
735803
def run(
736804
task: str | None,
737805
*,
@@ -755,7 +823,8 @@ def run(
755823
print(f"error: --target is not a directory: {target}", file=sys.stderr)
756824
return 2
757825

758-
task_text = task or dogfood_cmd.DEFAULT_TASK
826+
resolved = _resolve_next_task(target)
827+
task_text = task or str(resolved["task"])
759828
session_title = title or task_text
760829
start_rc = start(target=target, title=session_title)
761830
if start_rc != 0:

tests/test_gitignore.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,39 @@ def test_init_idempotent_replaces_block(tmp_target: Path):
6363
assert second.count(install_mod.GITIGNORE_BEGIN) == 1
6464

6565

66+
def test_init_replaces_legacy_solo_mise_gitignore_block(tmp_target: Path):
67+
tmp_target.mkdir()
68+
(tmp_target / ".gitignore").write_text(
69+
"\n".join(
70+
[
71+
"# user rules",
72+
"*.log",
73+
"",
74+
install_mod.LEGACY_GITIGNORE_BEGIN,
75+
"GARBAGE_LINE",
76+
install_mod.LEGACY_GITIGNORE_END,
77+
"",
78+
"# after block",
79+
".local-cache/",
80+
"",
81+
]
82+
)
83+
)
84+
85+
rc = install_selection(tmp_target, _repo_selection())
86+
87+
assert rc == 0
88+
gi = _read_gi(tmp_target)
89+
assert "# user rules" in gi
90+
assert ".local-cache/" in gi
91+
assert install_mod.GITIGNORE_BEGIN in gi
92+
assert install_mod.GITIGNORE_END in gi
93+
assert install_mod.LEGACY_GITIGNORE_BEGIN not in gi
94+
assert install_mod.LEGACY_GITIGNORE_END not in gi
95+
assert "GARBAGE_LINE" not in gi
96+
assert gi.count(install_mod.GITIGNORE_BEGIN) == 1
97+
98+
6699
def test_init_preserves_user_edits_outside_block(tmp_target: Path):
67100
tmp_target.mkdir()
68101
pre = "node_modules/\n# user rules\n*.swp\n"

0 commit comments

Comments
 (0)