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: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `brigade work bootstrap` to initialize and verify the dogfood-backed daily work loop in one command.
- `brigade work brief` and `brigade work brief --json` as a start-of-day entrypoint with git state, latest sessions, latest dogfood run, resolved next task, and suggested command.
- `brigade work tasks` plus `brigade work task add/show/done` to manage a gitignored local task ledger under `.brigade/work/tasks.json`.
- `brigade work run --queue-next` to queue the successful run's extracted next step, with duplicate pending task protection.
- `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.
Expand All @@ -57,6 +58,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Dogfood runs now default to a 600 second per-agent timeout for practical daily repo reviews.
- Dogfood next-step extraction now handles markdown `## Next` sections and can fall back to `summary.md` when `final.txt` does not contain a next-step label.
- `brigade work run` now consumes the oldest pending ledger task before falling back to the latest extracted dogfood next step, and marks consumed tasks done after successful runs.
- `brigade work task add --from-next` now reuses an equivalent pending task instead of adding duplicates.
- The managed gitignore block now treats `.brigade/dogfood.toml` and `.brigade/runs/` as local state.
- Live smoke docs now keep Codex agent execution in a trusted repo cwd while writing temporary roster, artifacts, and handoff output under `/tmp`.
- Handoff write failures now preserve final run artifacts, print the final answer, return nonzero, and mark `run.json` as `handoff-failed`.
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ brigade work task add "build the next slice"
brigade work task add --from-next
brigade work task done <task-id>
brigade work run
brigade work run --queue-next
brigade work run "review today's changes"
brigade work start "next slice"
brigade work note "wired parser and tests"
Expand Down Expand Up @@ -177,7 +178,7 @@ 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 bootstrap` once in a repo to initialize the daily loop: it writes or verifies `.brigade/dogfood.toml`, creates local run and work artifact directories, creates the configured handoff inbox, updates the managed `.gitignore` block, and reports whether the repo is ready to run. Use `brigade work brief` as the normal start-of-day command. It shows branch and dirty-file state, active and latest work sessions, pending local tasks, latest dogfood run, extracted next task, recent sessions, and the command to continue; add `--json` for wrappers. Use `brigade work tasks` to list the local task ledger stored in `.brigade/work/tasks.json`, `brigade work task add "..."` to queue work manually, `brigade work task add --from-next` to promote the latest extracted dogfood next step, and `brigade work task done <task-id>` to close queued work. 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, or `brigade work next --json` for wrappers that want the active session, dogfood snapshot, resolved task, and suggested command as structured output. `brigade work run` is the one-command daily loop: it starts a work session, consumes the oldest pending ledger task when no task is passed, otherwise uses the latest extracted dogfood next step, runs `brigade dogfood`, marks consumed ledger tasks done after successful runs, 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`.
Use `brigade work bootstrap` once in a repo to initialize the daily loop: it writes or verifies `.brigade/dogfood.toml`, creates local run and work artifact directories, creates the configured handoff inbox, updates the managed `.gitignore` block, and reports whether the repo is ready to run. Use `brigade work brief` as the normal start-of-day command. It shows branch and dirty-file state, active and latest work sessions, pending local tasks, latest dogfood run, extracted next task, recent sessions, and the command to continue; add `--json` for wrappers. Use `brigade work tasks` to list the local task ledger stored in `.brigade/work/tasks.json`, `brigade work task add "..."` to queue work manually, `brigade work task add --from-next` to promote the latest extracted dogfood next step, and `brigade work task done <task-id>` to close queued work. 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, or `brigade work next --json` for wrappers that want the active session, dogfood snapshot, resolved task, and suggested command as structured output. `brigade work run` is the one-command daily loop: it starts a work session, consumes the oldest pending ledger task when no task is passed, otherwise uses the latest extracted dogfood next step, runs `brigade dogfood`, marks consumed ledger tasks done after successful runs, ends the session, writes a work-session Memory Handoff by default, and prints a compact recap. Add `--queue-next` to queue the successful run's extracted next step for the next session; duplicate pending task text is reused instead of added again. 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`. 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.

Expand Down
2 changes: 2 additions & 0 deletions src/brigade/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ def _build_parser() -> argparse.ArgumentParser:
)
p_work_run.add_argument("--timeout-seconds", type=float, default=DEFAULT_TIMEOUT_SECONDS, help="Per-agent timeout.")
p_work_run.add_argument("--recap-limit", type=int, default=1, help="Maximum sessions to include in the final recap.")
p_work_run.add_argument("--queue-next", action="store_true", help="Queue the extracted next step after a successful run.")
p_work_start = work_sub.add_parser("start", help="Start a local Brigade work session.")
p_work_start.add_argument("title", nargs="*", help="Optional session title.")
p_work_start.add_argument("--target", "-t", type=Path, default=Path("."), help="Repo or workspace for the session.")
Expand Down Expand Up @@ -531,6 +532,7 @@ def main(argv=None) -> int:
native_read_only_sandbox=args.native_read_only_sandbox,
timeout_seconds=args.timeout_seconds,
recap_limit=args.recap_limit,
queue_next=args.queue_next,
)
if args.work_command == "start":
title = " ".join(args.title) if args.title else None
Expand Down
121 changes: 111 additions & 10 deletions src/brigade/work_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,10 @@ def _task_sort_key(task: dict[str, Any]) -> str:
return str(task.get("created_at") or task.get("id") or "")


def _task_text_key(text: str) -> str:
return " ".join(text.casefold().split())


def _pending_tasks(target: Path) -> list[dict[str, Any]]:
ledger = _read_task_ledger(target)
tasks = [
Expand All @@ -172,6 +176,16 @@ def _pending_tasks(target: Path) -> list[dict[str, Any]]:
return tasks


def _find_pending_task_by_text(target: Path, text: str) -> dict[str, Any] | None:
wanted = _task_text_key(text)
if not wanted:
return None
for task in _pending_tasks(target):
if _task_text_key(str(task.get("text") or "")) == wanted:
return task
return None


def _find_task(target: Path, task_id: str) -> tuple[dict[str, Any] | None, dict[str, Any]]:
ledger = _read_task_ledger(target)
matches: list[dict[str, Any]] = []
Expand All @@ -187,25 +201,78 @@ def _find_task(target: Path, task_id: str) -> tuple[dict[str, Any] | None, dict[
return None, ledger


def _make_task(text: str, *, source: str = "manual") -> dict[str, Any]:
def _make_task(text: str, *, source: str = "manual", metadata: dict[str, Any] | None = None) -> dict[str, Any]:
now = _now()
created = now.isoformat()
return {
task = {
"id": f"{now.strftime('%Y%m%d-%H%M%S')}-{_slug(text)}-{uuid4().hex[:6]}",
"text": text,
"status": "pending",
"source": source,
"created_at": created,
"updated_at": created,
}
if metadata:
task["metadata"] = metadata
return task


def _add_task(target: Path, text: str, *, source: str = "manual") -> dict[str, Any]:
def _add_task(
target: Path,
text: str,
*,
source: str = "manual",
metadata: dict[str, Any] | None = None,
) -> tuple[dict[str, Any], bool]:
ledger = _read_task_ledger(target)
task = _make_task(text, source=source)
existing = _find_pending_task_by_text(target, text)
if existing is not None:
return existing, False
task = _make_task(text, source=source, metadata=metadata)
ledger["tasks"].append(task)
_write_task_ledger(target, ledger)
return task
return task, True


def _latest_run_next_metadata(target: Path) -> tuple[str | None, dict[str, Any]]:
dogfood = _dogfood_snapshot(target)
next_step = dogfood.get("next") if isinstance(dogfood.get("next"), str) else None
latest = dogfood.get("latest_run") if isinstance(dogfood.get("latest_run"), dict) else None
metadata: dict[str, Any] = {
"dogfood_next_source": dogfood.get("next_source"),
}
if isinstance(latest, dict):
metadata.update(
{
"run_path": latest.get("path"),
"run_started_at": latest.get("started_at"),
"run_status": latest.get("status"),
"run_task": latest.get("task"),
}
)
return next_step.strip() if next_step and next_step.strip() else None, metadata


def _queue_latest_next(
target: Path,
*,
session_dir: Path | None = None,
session_title: str | None = None,
) -> tuple[dict[str, Any] | None, bool, str | None]:
next_step, metadata = _latest_run_next_metadata(target)
if not next_step:
return None, False, "no extracted next step is available"
if session_dir is not None:
metadata["session_path"] = str(session_dir)
if session_title:
metadata["session_title"] = session_title
task, created = _add_task(
target,
next_step,
source="latest_dogfood_run",
metadata=metadata,
)
return task, created, None


def _read_session(path: Path) -> dict[str, Any] | None:
Expand Down Expand Up @@ -560,6 +627,16 @@ def _active_session_info(target: Path) -> dict[str, Any] | None:
}


def _active_session_dir(target: Path) -> Path | None:
current = _current_path(target)
if not current.exists():
return None
session_id = current.read_text().strip()
if not session_id:
return None
return _work_root(target) / session_id


def _next_payload(target: Path) -> dict[str, Any]:
target = target.expanduser().resolve()
active = _active_session_info(target)
Expand Down Expand Up @@ -1087,6 +1164,11 @@ def tasks(*, target: Path, all_tasks: bool = False, json_output: bool = False) -
print(f"- {task.get('id')} [{status_text}] {_short(str(task.get('text', '')))}")
if task.get("source"):
print(f" source: {task['source']}")
metadata = task.get("metadata") if isinstance(task.get("metadata"), dict) else {}
if metadata.get("run_path"):
print(f" run: {metadata['run_path']}")
if metadata.get("session_path"):
print(f" session: {metadata['session_path']}")
if task.get("completed_at"):
print(f" completed_at: {task['completed_at']}")
return 0
Expand All @@ -1103,19 +1185,21 @@ def task_add(*, target: Path, text: str | None = None, from_next: bool = False)
task_text = (text or "").strip()
source = "manual"
if from_next:
dogfood = _dogfood_snapshot(target)
next_step = dogfood.get("next") if isinstance(dogfood.get("next"), str) else None
if not next_step or not next_step.strip():
next_step, metadata = _latest_run_next_metadata(target)
if not next_step:
print("error: no extracted next step is available", file=sys.stderr)
return 1
task_text = next_step.strip()
task_text = next_step
source = "latest_dogfood_run"
else:
metadata = None
if not task_text:
print("error: task text is required", file=sys.stderr)
return 2
task = _add_task(target, task_text, source=source)
task, created = _add_task(target, task_text, source=source, metadata=metadata)
print(f"task: {task['id']}")
print(f"status: {task['status']}")
print(f"created: {created}")
print(f"text: {task['text']}")
return 0

Expand All @@ -1134,6 +1218,11 @@ def task_show(*, target: Path, task_id: str) -> int:
print(f"source: {task.get('source', '')}")
print(f"created_at: {task.get('created_at', '')}")
print(f"updated_at: {task.get('updated_at', '')}")
metadata = task.get("metadata") if isinstance(task.get("metadata"), dict) else {}
if metadata:
print("metadata:")
for key in sorted(metadata):
print(f" {key}: {metadata[key]}")
if task.get("completed_at"):
print(f"completed_at: {task['completed_at']}")
print(f"text: {task.get('text', '')}")
Expand Down Expand Up @@ -1341,6 +1430,7 @@ def run(
native_read_only_sandbox: bool = False,
timeout_seconds: float = dogfood_cmd.DEFAULT_TIMEOUT_SECONDS,
recap_limit: int = 1,
queue_next: bool = False,
) -> int:
if recap_limit < 1:
print("error: --recap-limit must be a positive integer", file=sys.stderr)
Expand All @@ -1358,6 +1448,7 @@ def run(
start_rc = start(target=target, title=session_title)
if start_rc != 0:
return start_rc
session_dir = _active_session_dir(target)

dogfood_rc = 1
try:
Expand Down Expand Up @@ -1386,6 +1477,16 @@ def run(
task["completed_at"] = now
task["completed_session_title"] = session_title
_write_task_ledger(target, ledger)
if dogfood_rc == 0 and queue_next:
queued_task, created, reason = _queue_latest_next(
target,
session_dir=session_dir,
session_title=session_title,
)
if queued_task is None:
print(f"queued_next: skipped ({reason})")
else:
print(f"queued_next: {queued_task.get('id')} ({'created' if created else 'existing'})")
recap(target=target, limit=recap_limit)
return dogfood_rc

Expand Down
Loading
Loading