Skip to content
Draft
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
36 changes: 36 additions & 0 deletions docs/capacity-fill.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Capacity Fill Ledger

Generated: `2026-06-29T23:47:11+00:00`

Status: `healthy`

## Agy lane

- Reachable: `True`
- Detail: `/opt/homebrew/bin/agy`
- Remaining: `95` / `100`
- Down reason: `none`

## Capacity snapshot

- Up lanes:
- `agy`
- `claude`
- `codex`
- `gemini`
- `github_actions`
- `jules`
- `opencode`

- Down lanes:
- `copilot`
- `ollama`
- `oz`
- `warp`

## Focus

- If Agy remains `down` for multiple beats, run:
- `python3 scripts/dispatch-health.py --write --probe-async`
- `python3 scripts/capacity-fill-ledger.py --write`
- and check manual entries in `logs/lanes-down.txt` if present.
34 changes: 18 additions & 16 deletions docs/dispatch-health.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Dispatch Health

Generated: `2026-06-29T15:16:32+00:00`
Generated: `2026-06-29T23:46:12+00:00`

Status: `blocked`

Expand All @@ -19,35 +19,37 @@ Status: `blocked`
- Loaded launchd state: `running` pid `1656`.
- Loaded LIMEN_ROOT: `/Users/4jp/Workspace/limen`.
- Loaded LIMEN_DISPATCH_ASYNC: `0`.
- Watchdog dry-run healthy: `True`; `[watchdog] 2026-06-29T15:16:33.126703+00:00 HEALTHY sig=healthy`.
- Watchdog dry-run healthy: `False`; `[watchdog] 2026-06-29T23:46:13.915819+00:00 UNHEALTHY sig=beating+daemon-up`.

## Async Dispatch

- Async dry-run requested: `True`.
- Async dry-run ok: `True`; timed out `False`.
- Async dry-run summary: `-- async: reaped 0 dead ; harvested 0 ; 0 still running ; would launch 0 (cap 12) -> []`.
- Async dry-run ok: `False`; timed out `False`.
- Async dry-run summary: `PermissionError: [Errno 1] Operation not permitted: '/Users/4jp/Workspace/limen/logs/.queue.lock.d'`.

## Live Root

- Live root: `~/Workspace/limen`.
- Branch: `main`; status `## main...origin/main [ahead 1]`.
- HEAD: `d6757d3d21fc02f7d849f1f680d5c4e74c68cf70`.
- origin/main: `9f7af24dcb7514acec86c377965fa8efa56932ce`.
- Matches origin/main: `False`; ahead `1` behind `0`.
- Dirty entries: `2`.
- Live root: `~/Workspace/.limen-worktrees/capfill-agy-20260629-04-73b3`.
- Branch: `limen/capfill-agy-20260629-04-73b3`; status `## limen/capfill-agy-20260629-04-73b3...origin/main`.
- HEAD: `7ecdd65a529802a581d173b4cb390d19bcb20e55`.
- origin/main: `7ecdd65a529802a581d173b4cb390d19bcb20e55`.
- Matches origin/main: `True`; ahead `0` behind `0`.
- Dirty entries: `3`.
- `docs/dispatch-health.md`
- `docs/live-root-gate.md`
- `docs/capacity-fill.md`
- `scripts/capacity-fill-ledger.py`

## Verified Worktree

- Verified worktree: `~/Workspace/limen`.
- Branch: `main`; status `## main...origin/main [ahead 1]`.
- HEAD matches origin/main: `False`.
- Verified worktree: `~/Workspace/.limen-worktrees/capfill-agy-20260629-04-73b3`.
- Branch: `limen/capfill-agy-20260629-04-73b3`; status `## limen/capfill-agy-20260629-04-73b3...origin/main`.
- HEAD matches origin/main: `True`.

## Blockers

- `live-root-not-at-origin-main`: live root branch main head d6757d3d21fc differs from origin/main 9f7af24dcb75.
- `live-root-dirty`: live root has 2 dirty entries.
- `heartbeat-watchdog-unhealthy`: ok not-wedged: {"reason": "no PARALLEL beats in window", "recent_pr_counts": [], "max_fails_threshold": 3}
- `live-root-dirty`: live root has 3 dirty entries.
- `async-dry-run-unhealthy`: PermissionError: [Errno 1] Operation not permitted: '/Users/4jp/Workspace/limen/logs/.queue.lock.d'

## Commands

Expand Down
176 changes: 176 additions & 0 deletions scripts/capacity-fill-ledger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
#!/usr/bin/env python3
"""Track the one-lane capacity-fill pulse for this packet.

This is intentionally small and read-only by default. With ``--write`` it refreshes:

* ``docs/capacity-fill.md`` — human-readable daily pulse for lane operators.
* ``logs/capacity-fill-ledger.json`` — structured evidence for automation.

The current packet focus is **agy** productivity, so the receipt always includes:

* current AGY capacity census line (including remaining quota);
* AGY hard blockers from dispatch's down-lane derivation (manual lane-marking, usage dead-ness,
and OAuth preflight).
"""

from __future__ import annotations

import argparse
import datetime as dt
import json
import os
import sys
from pathlib import Path
from typing import Any

sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "cli" / "src"))

from limen.capacity import capacity_census # noqa: E402
from limen.dispatch import _down_lanes # noqa: E402
from limen.io import load_limen_file # noqa: E402

ROOT = Path(os.environ.get("LIMEN_ROOT", Path(__file__).resolve().parents[1]))
DOC_PATH = ROOT / "docs" / "capacity-fill.md"
LEDGER_PATH = ROOT / "logs" / "capacity-fill-ledger.json"
TASKS_PATH = Path(os.environ.get("LIMEN_TASKS", ROOT / "tasks.yaml"))


def relpath(path: Path) -> str:
try:
return "~/" + str(path.expanduser().resolve().relative_to(Path.home()))
except (OSError, ValueError):
return str(path)


def capacity_snapshot() -> tuple[list[dict[str, Any]], set[str]]:
"""Build a capacity census from the tracked board, and read dispatch's current down-lane set."""
rows: list[dict[str, Any]] = []
board: dict[str, Any] | None = None
try:
board = load_limen_file(TASKS_PATH).model_dump(mode="json", exclude_none=True)
except Exception:
board = None

try:
rows = capacity_census(board)
except Exception as exc:
rows = []
print(f"WARN capacity_census failed: {exc}")

try:
dead = _down_lanes()
except Exception as exc:
print(f"WARN _down_lanes unavailable: {exc}")
dead = set()

return rows, set(dead)


def build_markdown(snapshot: dict[str, Any]) -> str:
generated = snapshot.get("generated_at")
status = snapshot.get("status")
agy = snapshot.get("agy", {})
up = snapshot.get("up", [])
down = snapshot.get("down", [])
down_reason = snapshot.get("agy_down_reason")

lines = [
"# Capacity Fill Ledger",
"",
f"Generated: `{generated}`",
"",
f"Status: `{status}`",
"",
"## Agy lane",
"",
f"- Reachable: `{agy.get('reachable', False)}`",
f"- Detail: `{agy.get('detail', 'unknown')}`",
f"- Remaining: `{agy.get('remaining', 'unknown')}` / `{agy.get('limit', 'unknown')}`",
f"- Down reason: `{down_reason or 'none'}`",
"",
"## Capacity snapshot",
"",
"- Up lanes:",
]
for lane in sorted(up):
lines.append(f" - `{lane}`")
if not up:
lines.append(" - none")
lines.append("")
lines.append("- Down lanes:")
for lane in sorted(down):
lines.append(f" - `{lane}`")
if not down:
lines.append(" - none")

lines += [
"",
"## Focus",
"",
"- If Agy remains `down` for multiple beats, run:",
" - `python3 scripts/dispatch-health.py --write --probe-async`",
" - `python3 scripts/capacity-fill-ledger.py --write`",
" - and check manual entries in `logs/lanes-down.txt` if present.",
"",
]
return "\n".join(lines)


def build_snapshot() -> dict[str, Any]:
rows, dead = capacity_snapshot()
by_agent = {row.get("agent"): row for row in rows}
up = [row["agent"] for row in rows if row.get("reachable") and row.get("agent") not in dead]
down = [
row["agent"]
for row in rows
if (not row.get("reachable") or row.get("agent") in dead)
]
agy_row = by_agent.get("agy") or {}
blocker = []
if agy_row.get("agent") is not None and not agy_row.get("reachable", False):
blocker.append(f"agy unreachable: {agy_row.get('detail')}")
if "agy" in dead:
blocker.append("agy currently marked down by dispatch derivation")
status = "up" if agy_row and agy_row.get("reachable") and "agy" not in dead else "down"
return {
"generated_at": dt.datetime.now(dt.timezone.utc).isoformat(timespec="seconds"),
"tasks_path": str(TASKS_PATH),
"status": "healthy" if status == "up" else "blocked",
"rows": rows,
"up": sorted(set(up)),
"down": sorted(set(down)),
"down_raw": sorted(dead),
"agy": agy_row,
"agy_down_reason": "; ".join(blocker) or None,
"commands": [
"python3 scripts/capacity-fill-ledger.py --write",
"python3 scripts/dispatch-health.py --write --probe-async",
"python3 scripts/route.py --tasks tasks.yaml --workdir ~/Workspace --apply",
],
}


def write_outputs(snapshot: dict[str, Any]) -> None:
DOC_PATH.parent.mkdir(parents=True, exist_ok=True)
DOC_PATH.write_text(build_markdown(snapshot), encoding="utf-8")
LEDGER_PATH.parent.mkdir(parents=True, exist_ok=True)
LEDGER_PATH.write_text(json.dumps(snapshot, indent=2, sort_keys=True), encoding="utf-8")


def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--write", action="store_true", help="refresh markdown and ledger outputs")
args = parser.parse_args()

snapshot = build_snapshot()
if args.write:
write_outputs(snapshot)
print(f"capacity-fill-ledger: wrote {DOC_PATH} and {LEDGER_PATH}")
return 0

print(build_markdown(snapshot))
return 0


if __name__ == "__main__":
raise SystemExit(main())
Loading