Files
autonomic-bot 346ed31acb feat: project-orchestrator — engine@v0.1.0 submodule, PO config, fleet.toml registry, mgmt scripts, docs, Nix
The PO is itself a project using the agent-orchestrator harness (engine/ submodule pinned at
v0.1.0). Adds: agents.toml (one persistent fleet-management agent) + prompts/; fleet.toml (the
sole project<->harness<->ref registry) + docs/fleet-registry.md; scripts/ (fleet.py +
create/start/stop/update-project.sh); docs/manage-projects.md + docs/bootstrap.md; flake.nix/.lock
devShell (python311+tmux+git); README.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 19:15:47 +00:00

118 lines
4.0 KiB
Python
Executable File

#!/usr/bin/env python3
"""fleet.py — read, validate, and report the PO fleet registry (fleet.toml).
This is the PO's view of the fleet. It NEVER touches a project's repo and carries no project-side
state — the registry here is the single source of truth for project ↔ harness ↔ ref ↔ location.
Usage:
python3 scripts/fleet.py list # one line per project (also validates the file)
python3 scripts/fleet.py status # same, plus a one-line summary count
python3 scripts/fleet.py validate # parse + schema-check only; exit 1 on any error
python3 scripts/fleet.py get <name> # dump one project's full entry
python3 scripts/fleet.py --file PATH ... # use a non-default registry (default: ./fleet.toml)
Needs only the Python stdlib (tomllib → python >= 3.11).
"""
import sys
import tomllib
from pathlib import Path
REQUIRED = ["name", "location", "harness", "ref", "enabled", "secrets"]
OPTIONAL = ["config", "notes"]
def _registry_path(argv):
path = Path("fleet.toml")
out = []
i = 0
while i < len(argv):
if argv[i] == "--file" and i + 1 < len(argv):
path = Path(argv[i + 1]); i += 2; continue
out.append(argv[i]); i += 1
return path, out
def load(path):
if not path.exists():
sys.exit(f"fleet: registry not found: {path}")
with open(path, "rb") as f:
raw = tomllib.load(f)
return raw
def validate(raw, path):
errors = []
projects = raw.get("project", [])
if not isinstance(projects, list):
errors.append("[[project]] must be an array of tables")
projects = []
seen = set()
for i, p in enumerate(projects):
tag = p.get("name", f"#{i}")
for k in REQUIRED:
if k not in p:
errors.append(f"project {tag}: missing required field '{k}'")
if not isinstance(p.get("enabled", False), bool):
errors.append(f"project {tag}: 'enabled' must be a boolean")
name = p.get("name")
if name in seen:
errors.append(f"duplicate project name: {name}")
if name:
seen.add(name)
for k in p:
if k not in REQUIRED + OPTIONAL:
errors.append(f"project {tag}: unknown field '{k}'")
return projects, errors
def cmd_list(projects):
if not projects:
print("(no projects registered)")
return
w = max(len(p.get("name", "?")) for p in projects)
for p in projects:
flag = "enabled " if p.get("enabled") else "disabled"
print(f" {p.get('name','?'):<{w}} [{flag}] {p.get('harness','?')}@{p.get('ref','?')}"
f" {p.get('location','?')}")
def main():
path, argv = _registry_path(sys.argv[1:])
cmd = argv[0] if argv else "list"
raw = load(path)
projects, errors = validate(raw, path)
if cmd == "validate":
if errors:
for e in errors:
print(f" ERROR: {e}")
sys.exit(1)
print(f"fleet: OK — {len(projects)} project(s), schema v{raw.get('fleet', {}).get('version', '?')}")
return
# for list/status/get we still surface errors but don't hard-fail the read
for e in errors:
print(f" ERROR: {e}", file=sys.stderr)
if cmd == "list":
cmd_list(projects)
elif cmd == "status":
cmd_list(projects)
en = sum(1 for p in projects if p.get("enabled"))
print(f"\n total={len(projects)} enabled={en} disabled={len(projects) - en}"
f" (registry schema v{raw.get('fleet', {}).get('version', '?')})")
elif cmd == "get" and len(argv) > 1:
target = argv[1]
for p in projects:
if p.get("name") == target:
for k in REQUIRED + OPTIONAL:
if k in p:
print(f" {k:<9} = {p[k]!r}")
return
sys.exit(f"fleet: no project named {target!r}")
else:
sys.exit(__doc__)
if errors:
sys.exit(1)
if __name__ == "__main__":
main()