#!/usr/bin/env python3 """Read-only Misaligned project dashboard (human text or JSON).""" from __future__ import annotations import argparse from datetime import datetime import json import subprocess import time from pathlib import Path import work_orders def run(cmd: list[str], cwd: Path, timeout: int = 8) -> tuple[int, str]: try: result = subprocess.run( cmd, cwd=cwd, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=timeout, check=False, ) return result.returncode, result.stdout.strip() except (OSError, subprocess.TimeoutExpired) as exc: return 127, str(exc) def primary_root(root: Path) -> Path: code, common = run( ["git", "rev-parse", "--path-format=absolute", "--git-common-dir"], root ) if code == 0 and common.endswith("/.git"): return Path(common[:-5]) if code == 0 and "/.git/worktrees/" in common: return Path(common.split("/.git/worktrees/", 1)[0]) return root def worktree_rows(root: Path) -> list[dict]: code, output = run(["git", "worktree", "list", "--porcelain"], root) if code != 0: return [{"error": output}] rows: list[dict] = [] current: dict[str, str] = {} for line in output.splitlines() + [""]: if not line: if current: path = Path(current["worktree"]) _, status = run(["git", "status", "--porcelain"], path) _, counts = run( ["git", "rev-list", "--left-right", "--count", "origin/main...HEAD"], path, ) ahead = behind = None if counts: parts = counts.split() if len(parts) == 2 and all(part.isdigit() for part in parts): behind, ahead = map(int, parts) _, timestamp = run(["git", "show", "-s", "--format=%ct", "HEAD"], path) age_hours = None if timestamp.isdigit(): age_hours = round((time.time() - int(timestamp)) / 3600, 1) rows.append( { "path": str(path), "branch": current.get("branch", "(detached)").removeprefix("refs/heads/"), "head": current.get("HEAD", "")[:12], "dirty": bool(status), "dirty_entries": status.splitlines(), "ahead": ahead, "behind": behind, "last_commit_age_hours": age_hours, } ) current = {} continue key, _, value = line.partition(" ") if key in {"worktree", "HEAD", "branch"}: current[key] = value return rows def key_value_files(directory: Path, suffix: str) -> list[dict]: rows: list[dict] = [] if not directory.is_dir(): return rows for path in sorted(directory.glob(f"*{suffix}")): values: dict[str, object] = { "file": str(path), "record_name": path.name.removesuffix(suffix), } try: lines = path.read_text(encoding="utf-8").splitlines() except OSError as exc: values["parse_error"] = str(exc) rows.append(values) continue for line in lines: key, sep, value = line.partition("=") if sep: if key == "key": values.setdefault("keys", []).append(value) else: values[key] = value rows.append(values) return rows def run_files(directory: Path) -> list[dict]: rows: list[dict] = [] if not directory.is_dir(): return rows for path in sorted(directory.glob("*/status.json")): try: row = json.loads(path.read_text(encoding="utf-8")) row["file"] = str(path) row["record_name"] = path.parent.name rows.append(row) except (OSError, json.JSONDecodeError) as exc: rows.append( { "file": str(path), "record_name": path.parent.name, "status": "invalid", "parse_error": str(exc), } ) return rows def parse_time(value: object) -> float | None: if not isinstance(value, str) or not value: return None try: return datetime.fromisoformat(value.replace("Z", "+00:00")).timestamp() except ValueError: return None def linked_worktree(task_id: str, worktrees: list[dict], explicit: object = None) -> dict | None: """Find the registered worktree carrying a task's durable state.""" if isinstance(explicit, str) and explicit: expected = str(Path(explicit).resolve()) return next( (row for row in worktrees if str(Path(row.get("path", "")).resolve()) == expected), None, ) for row in worktrees: branch = str(row.get("branch", "")) branch_tail = branch.rsplit("/", 1)[-1] name = Path(row.get("path", "")).name if ( name == task_id or branch == f"worktree-{task_id}" or branch_tail == task_id or branch_tail.startswith(f"{task_id}-") ): return row return None def consistency(payload: dict, now: float | None = None, stale_hours: float = 2.0) -> dict: """Validate machine-local project-operation records without mutating them. Recent broken links are errors: they make the live dispatch picture lie. Old running records and ordinary worktree dirt/age are warnings only; this checker reports ownership evidence and never guesses that it may delete it. """ now = time.time() if now is None else now stale_after = stale_hours * 3600 errors: list[str] = [] warnings: list[str] = [] worktrees = [row for row in payload["worktrees"] if "error" not in row] activities = payload["activities"] runs = payload["runs"] for row in payload["worktrees"]: if "error" in row: errors.append(f"worktree inventory unavailable: {row['error']}") def validate_ids(rows: list[dict], kind: str) -> dict[str, dict]: indexed: dict[str, dict] = {} for row in rows: record_name = str(row.get("record_name", "")) item_id = str(row.get("id", "")) if row.get("parse_error"): errors.append(f"{kind} {record_name or '?'} is invalid: {row['parse_error']}") continue if not item_id: errors.append(f"{kind} {record_name or '?'} has no id") continue if record_name and item_id != record_name: errors.append(f"{kind} file {record_name} declares id {item_id}") if item_id in indexed: errors.append(f"duplicate {kind} id {item_id}") indexed[item_id] = row return indexed activity_by_id = validate_ids(activities, "activity") run_by_id = validate_ids(runs, "run") active_statuses = {"claimed", "blocked", "checking", "landing"} run_statuses = {"running", "ok", "fail"} for item_id, activity in activity_by_id.items(): status = str(activity.get("status", "")) if status not in active_statuses | {"done", "abandoned"}: errors.append(f"activity {item_id} has invalid status {status or '?'}") continue if status not in active_statuses: continue started = parse_time(activity.get("started")) stale = started is not None and now - started > stale_after worktree = linked_worktree(item_id, worktrees) run = run_by_id.get(item_id) problems = [] if worktree is None: problems.append("has no linked registered worktree") # `worktree-new.sh` is also a supported low-level entry point and does # not open a heartbeat. Absence is therefore not contradictory. Once # an id has both records, however, active activity cannot point at a # completed/failed run. if run is not None and run.get("status") != "running": problems.append("has no running heartbeat") for problem in problems: (warnings if stale else errors).append(f"activity {item_id} {problem}") for item_id, run_row in run_by_id.items(): status = str(run_row.get("status", "")) if status not in run_statuses: errors.append(f"run {item_id} has invalid status {status or '?'}") continue updated = parse_time(run_row.get("updated_at")) if updated is None: errors.append(f"run {item_id} has invalid updated_at") continue if status != "running": continue stale = now - updated > stale_after worktree = linked_worktree(item_id, worktrees, run_row.get("worktree")) activity = activity_by_id.get(item_id) problems = [] if worktree is None: explicit = run_row.get("worktree") detail = f" ({explicit})" if explicit else "" problems.append(f"has no linked registered worktree{detail}") if activity is None or activity.get("status") not in active_statuses: problems.append("has no active activity record") if stale: warnings.append( f"run {item_id} heartbeat is stale by {(now - updated) / 3600:.1f}h" ) for problem in problems: (warnings if stale else errors).append(f"run {item_id} {problem}") return {"ok": not errors, "errors": errors, "warnings": warnings} def issues(root: Path, offline: bool) -> dict: if offline: return {"available": False, "reason": "offline", "items": []} code, output = run(["tang", "issue", "list"], root, timeout=6) if code != 0: return {"available": False, "reason": output or "tang unavailable", "items": []} items = [line.strip() for line in output.splitlines() if line.lstrip().startswith("#")] return {"available": True, "reason": None, "items": items} def freshness(root: Path, specs: list[work_orders.WorkOrder]) -> dict: errors = work_orders.validate(root, specs) roadmap = root / "wiki/process/ROADMAP.md" projected = work_orders.projected_roadmap(root, specs) work_index = not errors and roadmap.read_text(encoding="utf-8") == projected ledger_code, ledger_output = run(["bash", "tools/ledger_index.sh", "--check"], root) return { "work_orders": work_index, "work_order_errors": errors, "ledgers": ledger_code == 0, "ledger_detail": ledger_output, } def collect(root: Path, offline: bool) -> dict: root = root.resolve() primary = primary_root(root) specs = work_orders.load_specs(root) by_rel = {spec.rel: spec for spec in specs} unfinished = [spec for spec in specs if spec.status != "IMPLEMENTED" and spec.task] lanes = {name: [] for name in ("current", "held", "staged")} for spec in unfinished: row = spec.as_dict() row["active_blockers"] = work_orders.blocker_state(spec, by_rel) lanes[work_orders.lane(spec, by_rel)].append(row) for rows in lanes.values(): rows.sort(key=lambda row: (row["priority"] or 9999, row["path"])) next_lane = lanes["current"][0] if lanes["current"] else None payload = { "root": str(root), "primary_root": str(primary), "recommended_next": next_lane, "work_orders": lanes, # Storage keeps the historical path so older worktrees remain visible; # these records are advisory activity, not exclusive ownership. "activities": key_value_files(primary / ".agents/claims", ".claim"), "runs": run_files(primary / ".agents/runs"), "worktrees": worktree_rows(primary), "issues": issues(primary, offline), "freshness": freshness(root, specs), } payload["consistency"] = consistency(payload) return payload def human(payload: dict) -> str: lines = ["MISALIGNED PROJECT STATUS", ""] recommended = payload["recommended_next"] if recommended: lines.append( f"Next: [{recommended['priority']}] {recommended['task']} " f"({recommended['status']}, {recommended['class']})" ) else: lines.append("Next: no current-stage work order is dispatchable") for name, label in (("current", "Current lanes"), ("held", "Held"), ("staged", "Later stages")): rows = payload["work_orders"][name] lines.extend(["", f"{label} ({len(rows)}):"]) for row in rows: blocker = f"; waits on {', '.join(row['active_blockers'])}" if row["active_blockers"] else "" lines.append( f" {row['priority']:>3} {row['task']} [{row['status']}/{row['class']}]{blocker}" ) lines.extend(["", f"Activities ({len(payload['activities'])}):"]) if payload["activities"]: for activity in payload["activities"]: lines.append( f" {activity.get('id', '?')} {activity.get('status', '?')} " f"paths={','.join(activity.get('keys', []))}" ) else: lines.append(" none") lines.extend(["", f"Runs ({len(payload['runs'])}):"]) if payload["runs"]: for row in payload["runs"]: lines.append(f" {row.get('id', '?')} {row.get('status', '?')} phase={row.get('phase', '?')}") else: lines.append(" none") lines.extend(["", f"Worktrees ({len(payload['worktrees'])}):"]) for row in payload["worktrees"]: if "error" in row: lines.append(f" unavailable: {row['error']}") continue flags = [] if row["dirty"]: flags.append("dirty") if row["ahead"]: flags.append(f"ahead {row['ahead']}") if row["behind"]: flags.append(f"behind {row['behind']}") if row["last_commit_age_hours"] is not None: flags.append(f"age {row['last_commit_age_hours']}h") lines.append(f" {row['branch']} ({', '.join(flags) or 'clean'}) {row['path']}") issue = payload["issues"] lines.extend(["", f"Decision issues: {'available' if issue['available'] else 'unavailable'}"]) for item in issue["items"]: lines.append(f" {item}") if not issue["available"]: lines.append(f" {issue['reason']}") fresh = payload["freshness"] lines.extend( [ "", "Generated state:", f" work orders: {'fresh' if fresh['work_orders'] else 'STALE'}", f" ledgers: {'fresh' if fresh['ledgers'] else 'STALE'}", ] ) checked = payload["consistency"] lines.extend( [ "", f"Consistency: {'PASS' if checked['ok'] else 'FAIL'} " f"({len(checked['warnings'])} warnings)", ] ) for error in checked["errors"]: lines.append(f" ERROR: {error}") for warning in checked["warnings"]: lines.append(f" WARN: {warning}") return "\n".join(lines) def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--json", action="store_true") parser.add_argument( "--check", action="store_true", help="exit nonzero when project-operation records contradict each other", ) parser.add_argument("--offline", action="store_true", help="skip Tangled lookup") parser.add_argument("--root", type=Path, default=Path(__file__).resolve().parent.parent) args = parser.parse_args() payload = collect(args.root, args.offline) print(json.dumps(payload, indent=2, sort_keys=True) if args.json else human(payload)) return 0 if not args.check or payload["consistency"]["ok"] else 1 if __name__ == "__main__": raise SystemExit(main())