#!/usr/bin/env python3 """Fleet progression evaluator for the Paperclips-inspired infrastructure epic. Refs: timmy-home #547 """ from __future__ import annotations import argparse import json import os from pathlib import Path from urllib import request from typing import Any DEFAULT_BASE_URL = "https://forge.alexanderwhitestone.com/api/v1" DEFAULT_OWNER = "Timmy_Foundation" DEFAULT_REPO = "timmy-home" DEFAULT_TOKEN_FILE = Path.home() / ".config" / "gitea" / "token" DEFAULT_SPEC_FILE = Path(__file__).resolve().parent.parent / "configs" / "fleet_progression.json" DEFAULT_REPO_ROOT = Path(__file__).resolve().parent.parent DEFAULT_RESOURCES = { "uptime_percent_30d": 0.0, "capacity_utilization": 0.0, "innovation": 0.0, "all_models_local": False, "sovereign_stable_days": 0, "human_free_days": 0, } class GiteaClient: def __init__(self, token: str, owner: str = DEFAULT_OWNER, repo: str = DEFAULT_REPO, base_url: str = DEFAULT_BASE_URL): self.token = token self.owner = owner self.repo = repo self.base_url = base_url.rstrip("/") def get_issue(self, issue_number: int): headers = {"Authorization": f"token {self.token}"} req = request.Request( f"{self.base_url}/repos/{self.owner}/{self.repo}/issues/{issue_number}", headers=headers, ) with request.urlopen(req, timeout=30) as resp: return json.loads(resp.read().decode()) def load_spec(path: Path | None = None): target = path or DEFAULT_SPEC_FILE return json.loads(target.read_text()) def load_issue_states(spec: dict[str, Any], token_file: Path = DEFAULT_TOKEN_FILE): if not token_file.exists(): raise FileNotFoundError(f"Token file not found: {token_file}") token = token_file.read_text().strip() client = GiteaClient(token=token) issue_states = {} for phase in spec["phases"]: issue = client.get_issue(phase["issue_number"]) issue_states[phase["issue_number"]] = issue["state"] return issue_states def _evaluate_rule(rule: dict[str, Any], issue_states: dict[int, str], resources: dict[str, Any]): rule_type = rule["type"] rule_id = rule["id"] if rule_type == "always": return {"rule": rule_id, "passed": True, "actual": True, "expected": True} if rule_type == "issue_closed": issue_number = int(rule["issue"]) actual = str(issue_states.get(issue_number, "open")) return { "rule": rule_id, "passed": actual == "closed", "actual": actual, "expected": "closed", } if rule_type == "resource_gte": resource = rule["resource"] actual = resources.get(resource, 0) expected = rule["value"] return { "rule": rule_id, "passed": actual >= expected, "actual": actual, "expected": f">={expected}", } if rule_type == "resource_gt": resource = rule["resource"] actual = resources.get(resource, 0) expected = rule["value"] return { "rule": rule_id, "passed": actual > expected, "actual": actual, "expected": f">{expected}", } if rule_type == "resource_true": resource = rule["resource"] actual = bool(resources.get(resource, False)) return { "rule": rule_id, "passed": actual is True, "actual": actual, "expected": True, } raise ValueError(f"Unsupported rule type: {rule_type}") def _collect_repo_evidence(phase: dict[str, Any], repo_root: Path): present = [] missing = [] for entry in phase.get("repo_evidence", []): path = entry["path"] description = entry.get("description", "") label = f"`{path}` — {description}" if description else f"`{path}`" if (repo_root / path).exists(): present.append(label) else: missing.append(label) return present, missing def _phase_status_label(phase_result: dict[str, Any]) -> str: if phase_result["completed"]: return "COMPLETE" if phase_result["available_to_work"]: return "ACTIVE" return "LOCKED" def evaluate_progression( spec: dict[str, Any], issue_states: dict[int, str], resources: dict[str, Any] | None = None, repo_root: Path | None = None, ): merged_resources = {**DEFAULT_RESOURCES, **(resources or {})} repo_root = repo_root or DEFAULT_REPO_ROOT phase_results = [] for phase in spec["phases"]: issue_number = phase["issue_number"] issue_state = str(issue_states.get(issue_number, "open")) completed = issue_state == "closed" rule_results = [ _evaluate_rule(rule, issue_states, merged_resources) for rule in phase.get("unlock_rules", []) ] blocking = [item for item in rule_results if not item["passed"]] unlocked = not blocking repo_evidence_present, repo_evidence_missing = _collect_repo_evidence(phase, repo_root) phase_result = { "number": phase["number"], "issue_number": issue_number, "issue_state": issue_state, "key": phase["key"], "name": phase["name"], "summary": phase["summary"], "completed": completed, "unlocked": unlocked, "available_to_work": unlocked and not completed, "passed_requirements": [item for item in rule_results if item["passed"]], "blocking_requirements": blocking, "repo_evidence_present": repo_evidence_present, "repo_evidence_missing": repo_evidence_missing, } phase_result["status"] = _phase_status_label(phase_result) phase_results.append(phase_result) unlocked_phases = [phase for phase in phase_results if phase["unlocked"]] current_phase = unlocked_phases[-1] if unlocked_phases else phase_results[0] next_locked_phase = next((phase for phase in phase_results if not phase["unlocked"]), None) epic_complete = all(phase["completed"] for phase in phase_results) and phase_results[-1]["unlocked"] return { "epic_issue": spec["epic_issue"], "epic_title": spec["epic_title"], "resources": merged_resources, "issue_states": {str(k): v for k, v in issue_states.items()}, "phases": phase_results, "current_phase": current_phase, "next_locked_phase": next_locked_phase, "epic_complete": epic_complete, } def render_markdown(result: dict[str, Any]) -> str: current_phase = result["current_phase"] next_locked_phase = result["next_locked_phase"] resources = result["resources"] lines = [ f"# [FLEET-EPIC] {result['epic_title']}", "", "This report grounds the fleet epic in executable state: live issue gates, current resource inputs, and repo evidence for each phase.", "", "## Current Phase", "", f"- Current unlocked phase: {current_phase['number']} — {current_phase['name']}", f"- Current phase status: {current_phase['status']}", f"- Epic complete: {'yes' if result['epic_complete'] else 'no'}", ] if next_locked_phase: lines.append(f"- Next locked phase: {next_locked_phase['number']} — {next_locked_phase['name']}") else: lines.append("- Next locked phase: none") lines.extend([ "", "## Resource Snapshot", "", f"- Uptime (30d): {resources['uptime_percent_30d']}", f"- Capacity utilization: {resources['capacity_utilization']}", f"- Innovation: {resources['innovation']}", f"- All models local: {resources['all_models_local']}", f"- Sovereign stable days: {resources['sovereign_stable_days']}", f"- Human-free days: {resources['human_free_days']}", "", "## Phase Matrix", "", ]) for phase in result["phases"]: lines.extend([ f"### Phase {phase['number']} — {phase['name']}", "", f"- Issue: #{phase['issue_number']} ({phase['issue_state']})", f"- Status: {phase['status']}", f"- Summary: {phase['summary']}", ]) if phase["repo_evidence_present"]: lines.append("- Repo evidence present:") lines.extend(f" - {item}" for item in phase["repo_evidence_present"]) if phase["repo_evidence_missing"]: lines.append("- Repo evidence missing:") lines.extend(f" - {item}" for item in phase["repo_evidence_missing"]) if phase["blocking_requirements"]: lines.append("- Blockers:") for blocker in phase["blocking_requirements"]: lines.append( f" - blocked by `{blocker['rule']}`: actual={blocker['actual']} expected={blocker['expected']}" ) else: lines.append("- Blockers: none") lines.append("") lines.extend([ "## Why This Epic Remains Open", "", "- The progression manifest and evaluator exist, but multiple child phases are still open or only partially implemented.", "- Several child lanes already have active PRs; this report is the parent-level grounding slice that keeps the epic honest without duplicating those lanes.", "- This epic only closes when the child phase gates are actually satisfied in code and in live operation.", ]) return "\n".join(lines).rstrip() + "\n" def parse_args(): parser = argparse.ArgumentParser(description="Evaluate current fleet progression against the Paperclips-inspired epic.") parser.add_argument("--spec-file", type=Path, default=DEFAULT_SPEC_FILE) parser.add_argument("--token-file", type=Path, default=DEFAULT_TOKEN_FILE) parser.add_argument("--issue-state-file", type=Path, help="Optional JSON file of issue_number -> state overrides") parser.add_argument("--resource-file", type=Path, help="Optional JSON file with resource values") parser.add_argument("--uptime-percent-30d", type=float) parser.add_argument("--capacity-utilization", type=float) parser.add_argument("--innovation", type=float) parser.add_argument("--all-models-local", action="store_true") parser.add_argument("--sovereign-stable-days", type=int) parser.add_argument("--human-free-days", type=int) parser.add_argument("--json", action="store_true") parser.add_argument("--markdown", action="store_true", help="Render a markdown report instead of the terse CLI summary") parser.add_argument("--output", type=Path, help="Optional file path for markdown output") return parser.parse_args() def _load_resources(args): resources = dict(DEFAULT_RESOURCES) if args.resource_file: resources.update(json.loads(args.resource_file.read_text())) overrides = { "uptime_percent_30d": args.uptime_percent_30d, "capacity_utilization": args.capacity_utilization, "innovation": args.innovation, "sovereign_stable_days": args.sovereign_stable_days, "human_free_days": args.human_free_days, } for key, value in overrides.items(): if value is not None: resources[key] = value if args.all_models_local: resources["all_models_local"] = True return resources def _load_issue_states(args, spec): if args.issue_state_file: raw = json.loads(args.issue_state_file.read_text()) return {int(k): v for k, v in raw.items()} return load_issue_states(spec, token_file=args.token_file) def _render_cli_summary(result: dict[str, Any]) -> str: lines = [ "--- Fleet Progression Evaluator ---", f"Epic #{result['epic_issue']}: {result['epic_title']}", f"Current phase: {result['current_phase']['number']} — {result['current_phase']['name']}", f"Epic complete: {result['epic_complete']}", ] if result["next_locked_phase"]: lines.append( f"Next locked phase: {result['next_locked_phase']['number']} — {result['next_locked_phase']['name']}" ) lines.append("") for phase in result["phases"]: lines.append(f"Phase {phase['number']} [{phase['status']}] {phase['name']}") if phase["blocking_requirements"]: for blocker in phase["blocking_requirements"]: lines.append( f" - blocked by {blocker['rule']}: actual={blocker['actual']} expected={blocker['expected']}" ) return "\n".join(lines) def main(): args = parse_args() spec = load_spec(args.spec_file) issue_states = _load_issue_states(args, spec) resources = _load_resources(args) result = evaluate_progression(spec, issue_states, resources, repo_root=DEFAULT_REPO_ROOT) if args.json: rendered = json.dumps(result, indent=2) elif args.markdown or args.output: rendered = render_markdown(result) else: rendered = _render_cli_summary(result) if args.output: args.output.parent.mkdir(parents=True, exist_ok=True) args.output.write_text(rendered, encoding="utf-8") print(f"Fleet progression report written to {args.output}") else: print(rendered) if __name__ == "__main__": main()