|
2 | 2 | from __future__ import annotations |
3 | 3 |
|
4 | 4 | import ast |
| 5 | +import json |
| 6 | +import shutil |
| 7 | +import subprocess |
5 | 8 | import sys |
6 | 9 | from dataclasses import dataclass |
7 | 10 | from pathlib import Path |
| 11 | +from typing import Any |
8 | 12 |
|
9 | 13 | from . import aboyeur |
10 | 14 | from . import runs_cmd |
@@ -134,6 +138,131 @@ def load_config(target: Path) -> DogfoodConfig | None: |
134 | 138 | ) |
135 | 139 |
|
136 | 140 |
|
| 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 | + |
137 | 266 | def init( |
138 | 267 | *, |
139 | 268 | target: Path, |
|
0 commit comments