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>
This commit is contained in:
2026-06-13 19:15:47 +00:00
commit 346ed31acb
19 changed files with 849 additions and 0 deletions

19
scripts/_resolve.sh Executable file
View File

@ -0,0 +1,19 @@
#!/usr/bin/env bash
# _resolve.sh — shared helper: given a fleet project name, echo "<location>\t<config>\t<harness>".
# Sourced by start/stop/update scripts. Reads the PO's fleet.toml (the only project↔location record).
set -euo pipefail
PO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
resolve_project() {
local name="$1"
python3 - "$PO_ROOT/fleet.toml" "$name" <<'PY'
import sys, tomllib
path, name = sys.argv[1], sys.argv[2]
with open(path, "rb") as f:
raw = tomllib.load(f)
for p in raw.get("project", []):
if p.get("name") == name:
print("\t".join([p.get("location", ""), p.get("config", "agents.toml"), p.get("harness", "")]))
sys.exit(0)
sys.exit(f"_resolve: no project named {name!r} in {path}")
PY
}

117
scripts/create-project.sh Executable file
View File

@ -0,0 +1,117 @@
#!/usr/bin/env bash
# create-project.sh — scaffold a NEW project that uses a harness.
#
# Produces a self-contained project repo: the chosen harness vendored as the `engine/` submodule at
# a pinned ref, plus a harness config scaffolded by the harness's own `init`. The project contains
# NO project-orchestrator / fleet metadata — knowledge is one-directional (PO → project). Registering
# the project in the PO's fleet.toml is a SEPARATE, PO-side step (use --register, or edit fleet.toml
# by hand); nothing about the fleet ever lands inside the project repo.
#
# Usage:
# scripts/create-project.sh <name> [options]
#
# Options:
# --dir <parent> parent directory to create the project under (default: ./projects)
# --engine-url <url> harness repo to vendor as engine/ (default: the agent-orchestrator repo)
# --ref <ref> harness ref to pin the submodule at (default: v0.1.0)
# --prefix <prefix> session_prefix to write into the project's config (default: <name>-)
# --register also append a [[project]] entry to this PO's fleet.toml (PO-side only)
# --no-commit leave the project tree uncommitted (default: make an initial commit)
#
# Drive it by hand afterwards, exactly like any project:
# cd <parent>/<name> && python3 engine/agents.py status
set -euo pipefail
die() { echo "create-project: $*" >&2; exit 1; }
[ $# -ge 1 ] || die "usage: create-project.sh <name> [--dir P] [--engine-url U] [--ref R] [--prefix X] [--register] [--no-commit]"
NAME="$1"; shift
[[ "$NAME" =~ ^[a-z0-9][a-z0-9-]*$ ]] || die "name must be kebab-case (got: $NAME)"
PO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
PARENT="$PO_ROOT/projects"
ENGINE_URL="https://git.autonomic.zone/recipe-maintainers/agent-orchestrator.git"
REF="v0.1.0"
PREFIX=""
REGISTER=0
COMMIT=1
while [ $# -gt 0 ]; do
case "$1" in
--dir) PARENT="$2"; shift 2;;
--engine-url) ENGINE_URL="$2"; shift 2;;
--ref) REF="$2"; shift 2;;
--prefix) PREFIX="$2"; shift 2;;
--register) REGISTER=1; shift;;
--no-commit) COMMIT=0; shift;;
*) die "unknown option: $1";;
esac
done
PREFIX="${PREFIX:-${NAME}-}"
command -v git >/dev/null || die "git not on PATH"
command -v python3 >/dev/null || die "python3 not on PATH"
DEST="$PARENT/$NAME"
[ -e "$DEST" ] && die "$DEST already exists — refusing to overwrite"
mkdir -p "$PARENT"
echo "create-project: scaffolding '$NAME' at $DEST (engine $ENGINE_URL @ $REF)"
git init -q -b main "$DEST"
cd "$DEST"
# 1) vendor the harness as a pinned submodule under engine/
git -c protocol.version=2 submodule add -q "$ENGINE_URL" engine
( cd engine && git fetch -q --tags origin && git checkout -q "$REF" )
git add .gitmodules engine
# 2) scaffold the harness config + prompts via the harness's OWN init (no PO/fleet metadata)
python3 engine/agents.py init . >/dev/null
# stamp the chosen session_prefix into the scaffolded config (keeps namespaces unique per project)
python3 - "$PREFIX" <<'PY'
import re, sys, pathlib
prefix = sys.argv[1]
p = pathlib.Path("agents.toml")
txt = p.read_text()
txt = re.sub(r'session_prefix\s*=\s*"[^"]*"', f'session_prefix = "{prefix}"', txt, count=1)
p.write_text(txt)
PY
# 3) ignore runtime state; the project knows nothing about any PO
cat > .gitignore <<'EOF'
# runtime state + logs (never committed)
.ao-state/
*.log
__pycache__/
*.pyc
result
EOF
git add agents.toml prompts .gitignore 2>/dev/null || true
if [ "$COMMIT" -eq 1 ]; then
git -c user.name="project-orchestrator" -c user.email="po@localhost" \
commit -q -m "init: scaffold $NAME (engine @ $REF)"
fi
echo "create-project: done — $DEST"
echo " engine pinned at: $(cd engine && git rev-parse HEAD) ($REF)"
echo " config: agents.toml (session_prefix = $PREFIX)"
echo " verify it: ( cd $DEST && python3 engine/agents.py status )"
if [ "$REGISTER" -eq 1 ]; then
echo "create-project: registering '$NAME' in $PO_ROOT/fleet.toml"
cat >> "$PO_ROOT/fleet.toml" <<EOF
[[project]]
name = "$NAME"
location = "$DEST"
harness = "agent-orchestrator"
ref = "$REF"
enabled = false
secrets = ".env"
config = "agents.toml"
notes = "Created by scripts/create-project.sh"
EOF
python3 "$PO_ROOT/scripts/fleet.py" --file "$PO_ROOT/fleet.toml" validate
fi

117
scripts/fleet.py Executable file
View File

@ -0,0 +1,117 @@
#!/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()

19
scripts/start-project.sh Executable file
View File

@ -0,0 +1,19 @@
#!/usr/bin/env bash
# start-project.sh <name> [agent...] — start a fleet project's agents via its harness.
#
# Resolves <name> in the PO's fleet.toml, then drives that project's harness. For an
# agent-orchestrator project that is `engine/agents.py up`. For an unfamiliar harness, READ that
# project's harness docs first — there is no rigid contract. This wrapper handles the common
# agent-orchestrator case; extend it (or act by hand) for other harnesses.
set -euo pipefail
[ $# -ge 1 ] || { echo "usage: start-project.sh <name> [agent...]" >&2; exit 1; }
NAME="$1"; shift || true
source "$(dirname "$0")/_resolve.sh"
IFS=$'\t' read -r LOCATION CONFIG HARNESS < <(resolve_project "$NAME")
[ -d "$LOCATION" ] || { echo "start-project: location not a local dir: $LOCATION (clone it first)" >&2; exit 1; }
case "$HARNESS" in
agent-orchestrator)
echo "start-project: $NAME → python3 engine/agents.py up $* (in $LOCATION)"
( cd "$LOCATION" && python3 engine/agents.py --config "$CONFIG" up "$@" );;
*) echo "start-project: harness '$HARNESS' is not agent-orchestrator — read its docs and drive it by hand." >&2; exit 2;;
esac

15
scripts/stop-project.sh Executable file
View File

@ -0,0 +1,15 @@
#!/usr/bin/env bash
# stop-project.sh <name> [agent...] — stop a fleet project's agents via its harness.
# Mirror of start-project.sh; for an agent-orchestrator project that is `engine/agents.py down`.
set -euo pipefail
[ $# -ge 1 ] || { echo "usage: stop-project.sh <name> [agent...]" >&2; exit 1; }
NAME="$1"; shift || true
source "$(dirname "$0")/_resolve.sh"
IFS=$'\t' read -r LOCATION CONFIG HARNESS < <(resolve_project "$NAME")
[ -d "$LOCATION" ] || { echo "stop-project: location not a local dir: $LOCATION" >&2; exit 1; }
case "$HARNESS" in
agent-orchestrator)
echo "stop-project: $NAME → python3 engine/agents.py down $* (in $LOCATION)"
( cd "$LOCATION" && python3 engine/agents.py --config "$CONFIG" down "$@" );;
*) echo "stop-project: harness '$HARNESS' is not agent-orchestrator — read its docs and drive it by hand." >&2; exit 2;;
esac

21
scripts/update-project.sh Executable file
View File

@ -0,0 +1,21 @@
#!/usr/bin/env bash
# update-project.sh <name> <new-ref> — bump a fleet project's harness submodule to a new ref.
#
# Updating the engine for a project = checkout a new ref of its `engine/` submodule + commit, IN
# THAT PROJECT'S REPO ONLY. It touches no other project (each pins its own copy). Afterwards, update
# the project's `ref` in this PO's fleet.toml so the registry stays accurate.
set -euo pipefail
[ $# -ge 2 ] || { echo "usage: update-project.sh <name> <new-ref>" >&2; exit 1; }
NAME="$1"; NEWREF="$2"
source "$(dirname "$0")/_resolve.sh"
IFS=$'\t' read -r LOCATION CONFIG HARNESS < <(resolve_project "$NAME")
[ -d "$LOCATION" ] || { echo "update-project: location not a local dir: $LOCATION" >&2; exit 1; }
[ "$HARNESS" = "agent-orchestrator" ] || { echo "update-project: harness '$HARNESS' not agent-orchestrator — update by hand per its docs." >&2; exit 2; }
echo "update-project: $NAME engine → $NEWREF (in $LOCATION)"
( cd "$LOCATION/engine" && git fetch -q --tags origin && git checkout -q "$NEWREF" )
( cd "$LOCATION" && git add engine \
&& git -c user.name="project-orchestrator" -c user.email="po@localhost" \
commit -q -m "chore: bump engine to $NEWREF" )
echo "update-project: project committed. Now update fleet.toml: set ref = \"$NEWREF\" for '$NAME'."
echo " (edit $(cd "$(dirname "$0")/.." && pwd)/fleet.toml, then: python3 scripts/fleet.py validate)"