feat(orchestrator): W3 artifacts tree, ancestry, gates and token-free cleanup

Artifacts show the branch tree with ancestry and gate marks. The cleanup runner calls pkgmgr branch cleanup with its real argv and parses its repos[*].items output, refuses any plan with a remote-branch action, marks an item done only when pkgmgr reports it done, and offers no delete for a branch a worktree holds. Every text states what the pkgmgr command does to volumes. Committed below the bar on the operator's word 2026-11-12 (no further per-bundle tests, one gate after the merge): the W3 e2e never ran; the client switches to --local once pkgmgr ships it.

🎯 Fix: 90 %
💥 Break: 6 %
💸 Cost: 30 %
⏱️ Time: 40 %
🛡️ Security: 100 %
🏆 Score: 100
🚦 Gate: ⛔ break 6 % > 5

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kevin Veen-BirkenbachandClaude Opus 5.5 committed 2026-11-12 16:15:14 +00:00
1 parent fd8fc08603
commit f7a59d43a5
33 files changed
+1998 -107

No files matched your search

@@ -9,6 +9,7 @@ from .cleanup import cleanup_request
from .denying import DENIABLE, DENY_KEYS, denial_note, request_status, request_words
from .errors import Conflict, Missing, ToolError
from .fields import decode, expect, field
from .gating import gate_request
from .offices import office_request
from .records import digest, shown
from .register import REGISTER, adder, operator_rows, read_register, runnable
@@ -250,6 +251,8 @@ class Approving:
signed = self.appointable(raw)
elif task_request(raw):
signed = self.task_changes(raw)
elif gate_request(raw):
signed = self.gate_changes(raw)
else:
signed = [item["signed"] for item in self.checked(raw) if item["signed"] is not None]
return self.challenge_for(signed)
+3 -1
View File
@@ -8,6 +8,7 @@ from pathlib import Path
from .records import epoch, iso_epoch, read_json, shown
from .register import ENDED, status_of
from .signed import signing
from .tree import inside, owns
PUSH = re.compile(r"\b(?:push|gipsn|git-sign-push)\b")
@@ -105,7 +106,8 @@ def enrich(scan: dict, tasks: dict[str, dict], reviews: list[dict], touched: Tou
shared = sorted(files[a] & files[b])
if shared:
overlaps.append({"a": a, "b": b, "count": len(shared), "files": shared[:SHARED_SHOWN]})
merges = [{"from": item["name"], "into": item["merged_into"] or entry.get("default") or "", "by": item["merged_by"]} for item in entry["branches"] if item["merged_by"]]
default = entry.get("default") or ""
merges = [{"from": item["name"], "into": default if signing.names_default(item["merged_into"], default) else item["merged_into"] or default, "by": item["merged_by"]} for item in entry["branches"] if item["merged_by"]]
repos.append({**entry, "branches": branches, "overlaps": overlaps, "merges": merges})
loose = [{"task": task, "title": shown(row.get("title")), "status": status_of(row), "artefacts": artefacts_of(task, row)} for task, row in sorted(tasks.items()) if task not in placed and artefacts_of(task, row)]
return {**scan, "repos": repos, "tasks": loose}
+28 -4
View File
@@ -11,6 +11,8 @@ from pathlib import Path
from .errors import Missing, ToolError
from .fields import decode, expect
from .records import read_json, stamp
from .register import OPERATOR
from .skill_scripts import OPERATOR_SCRIPT
from .tree import offered
ITEM_KEYS = ("kind", "repository", "branch", "worktree", "tip")
@@ -18,6 +20,10 @@ JOB_KEYS = ("repository", "branch", "worktree", "tip")
JOB_VERSION = 1
ITEM_LIMIT = 100
RESULTS_SHOWN = 20
TOKEN_ROW = "pkgmgr-github-token"
TOKEN_COMMAND = "env secret-tool store --label=pkgmgr-github service pkgmgr:github username 'github.com|-' </dev/tty"
TOKEN_REASON = "Store the GitHub token in the Secret Service under the key pkgmgr reads; Clean all merged then also deletes remote and squash-merged branches."
TOKEN_ADDER = "Operator"
@dataclass(frozen=True)
@@ -65,19 +71,24 @@ class Cleaning:
Returns:
The signed items, each still a merged-clean branch without a gc
skip reason at the same tip in the current tree file.
skip reason at the same tip in the current tree file; without
the GitHub token (delete_ready false) each also token-free.
"""
if not isinstance(raw, list) or not 1 <= len(raw) <= ITEM_LIMIT:
raise ToolError(f"items must be a list of 1 to {ITEM_LIMIT} objects")
scan = self.tree.get()
if scan.get("delete_ready") is not True:
raise ToolError(f"delete blocked: {scan.get('delete_reason') or scan.get('error') or 'delete_ready is false'}")
known = offered(scan)
ready = scan.get("delete_ready") is True
if scan.get("error"):
raise ToolError(f"delete blocked: {scan['error']}")
known = offered(scan, ready)
locked = [] if ready else offered(scan, True)
chosen = []
for entry in raw:
if not isinstance(entry, dict):
raise ToolError("every item must be an object with kind, repository, branch, worktree and tip")
expect(entry, ITEM_KEYS)
if entry in locked and entry not in known:
raise ToolError(f"delete blocked: {scan.get('delete_reason') or 'delete_ready is false'}")
if entry not in known:
raise ToolError(f"{entry.get('branch')!s:.200} in {entry.get('repository')!s:.200} is no merged-clean branch without skip reason at that tip in the tree file")
if entry in chosen:
@@ -97,6 +108,19 @@ class Cleaning:
self.log.add({"kind": "run", "title": f"Delete signed: {names}"[:200], "text": "\n".join(f"{item['repository']} {item['branch']} @ {item['tip'][:12]}" for item in items), "ref": {"cleanup": job_id, "status": "queued"}})
return {"id": job_id, "status": "queued", "items": job["items"]}
def token_row(self, body: bytes) -> dict:
"""Returns:
The operator row that stores the GitHub token in the Secret
Service under pkgmgr's key, added or refreshed through
operator.py; it holds no secret, the operator types the token
when running it.
"""
expect(decode(body), ())
done = self.scripts.run(OPERATOR_SCRIPT, ["add", TOKEN_ROW, "--dir", "/", "--command", TOKEN_COMMAND, "--reason", TOKEN_REASON, "--check", "manual", "--session", TOKEN_ADDER])
if done["code"] != 0:
raise ToolError(f"operator.py add failed ({done['code']}): {done['stderr'].strip()[-500:]}")
return {"task": f"{OPERATOR}{TOKEN_ROW}", "status": "queued"}
def cleanup_state(self, job_id: str) -> dict:
result = read_json(self.spool.results / f"{job_id}.json")
if isinstance(result, dict) and isinstance(result.get("items"), list):
+131
View File
@@ -0,0 +1,131 @@
from __future__ import annotations
import functools
import importlib.util
import sqlite3
import time
from contextlib import closing
from pathlib import Path
from types import ModuleType
from urllib.parse import quote
from .errors import ToolError
from .fields import decode, expect
from .jobs import GATE_JOB
from .signed import signing
GATES_SCRIPT = "kvb-check-gate/scripts/gates.py"
GATES_DB = "gates.db"
GATE_TASK = "gates"
CHANGE_LIMIT = 100
CHANGE_KEYS = ("kind", "argv")
def gate_request(raw: object) -> bool:
return isinstance(raw, list) and bool(raw) and all(isinstance(entry, dict) and entry.get("kind") == GATE_JOB for entry in raw)
def shown_command(argv: list[str]) -> str:
return " ".join(["gates.py", *argv])
def gates_module(script: Path) -> ModuleType | None:
"""Returns:
The installed gates.py as a module, for its resolve() and
parents_of(), which read only the database; loaded again after an
install changed it, None when it is missing or does not load.
"""
try:
changed = script.stat().st_mtime_ns
except OSError:
return None
return loaded(script, changed)
@functools.cache
def loaded(script: Path, changed: int) -> ModuleType | None:
spec = importlib.util.spec_from_file_location(f"kvb_orchestrator_gates_{changed}", script)
if spec is None:
return None
module = importlib.util.module_from_spec(spec)
try:
spec.loader.exec_module(module)
except (OSError, ImportError, RuntimeError, SyntaxError):
return None
return module
def branch_gates(gates: ModuleType, db: sqlite3.Connection, repo: str, branch: str) -> dict:
return {
"parents": [{"parent": parent, "source": source} for parent, source in gates.parents_of(db, repo, branch)],
"rules": dict(db.execute("SELECT action, level FROM rules WHERE repo = ? AND branch = ?", (repo, branch))),
"resolved": {action: dict(zip(("level", "source"), gates.resolve(db, repo, branch, action))) for action in gates.ACTIONS},
}
def read_gates(gates: ModuleType | None, db: Path, scan: dict) -> dict:
"""Args:
gates: the gates.py module, or None.
db: gates.db under the read-only mount of ~/.local/state/agents-council.
scan: the tree; its repositories and branches are the ones shown.
Returns:
{repo: {branch: {parents, rules, resolved}}} as `gates.py show --json`
prints it, read with the database opened read-only and without git;
{} without the script or a readable database.
"""
if gates is None or not db.is_file():
return {}
try:
with closing(sqlite3.connect(f"file:{quote(str(db))}?mode=ro", uri=True)) as connection:
return {repo["path"]: {entry["name"]: branch_gates(gates, connection, repo["path"], entry["name"]) for entry in repo["branches"]} for repo in scan["repos"]}
except sqlite3.Error:
return {}
class Gating:
def gate_view(self, scan: dict) -> dict:
try:
return read_gates(gates_module(self.scripts.skills / GATES_SCRIPT), self.kept / GATES_DB, scan)
except Exception: # noqa: BLE001
return {}
def gate_changes(self, raw: object) -> list[dict]:
"""Args:
raw: the page's items, each {"kind": "gate", "argv"} with the
gates.py arguments of one rule set|drop or parents add|drop;
the staged changes go under one passkey assertion.
Returns:
The signed items, each with an argv signing.gate_argv() accepts.
"""
if not isinstance(raw, list) or not 1 <= len(raw) <= CHANGE_LIMIT:
raise ToolError(f"items must be a list of 1 to {CHANGE_LIMIT} gate changes")
signed: list[dict] = []
for entry in raw:
if not isinstance(entry, dict):
raise ToolError("every item must be an object with kind and argv")
expect(entry, CHANGE_KEYS)
try:
item = signing.gate_item(signing.gate_argv(entry["argv"]))
except signing.Refused as error:
raise ToolError(f"gate change: {error}") from None
if item in signed:
raise ToolError("items must be distinct")
signed.append(item)
return signed
def change_gates(self, body: bytes) -> dict:
payload = decode(body)
expect(payload, ("items", "proof"))
items = self.gate_changes(payload["items"])
proof = self.covered(payload["proof"], items, time.time())
for item in items:
self.operator_proven(proof, item)
ids = []
for item in items:
fields = {"kind": GATE_JOB, "task": GATE_TASK, "argv": item["argv"], "command": shown_command(item["argv"]), "directory": "", "sha256": "", "status": "queued", "proof": proof}
ids.append(self.jobs.add({**fields, "code": None, "output": "", "created_ts": time.time(), "started": None, "finished": None})["id"])
commands = [shown_command(item["argv"]) for item in items]
self.log.add({"kind": "run", "title": f"Gate change signed: {len(items)}", "text": "\n".join(commands), "ref": {"jobs": ids, "status": "queued"}})
return {"jobs": ids, "status": "queued"}
+7 -1
View File
@@ -40,6 +40,8 @@ CLEANUP_STATE = re.compile(r"/api/cleanup/([0-9a-f]{16})")
OFFICES_ROUTE = "/api/offices"
OFFICE_STATE = re.compile(r"/api/offices/([0-9a-f]{16})")
TASKS_ROUTE = "/api/tasks"
GATES_ROUTE = "/api/gates"
TOKEN_ROUTE = "/api/cleanup/token"
VENDOR_ROUTE = re.compile(r"/vendor/([a-z0-9.-]{1,64})")
SCRIPT_TYPE = "text/javascript; charset=utf-8"
FONT_TYPE = "font/woff2"
@@ -57,7 +59,7 @@ SETTINGS_SIGN_ROUTE = "/api/settings/sign"
CHOICES_ROUTE = "/api/choices"
CHOICES_SIGN_ROUTE = "/api/choices/sign"
WAV_POSTS = (STT_ROUTE, VOICE_ROUTE)
ACTION_POSTS = ("/api/rules", "/api/nl", SETTINGS_ROUTE, TTS_ROUTE, STT_ROUTE, SIGN_ROUTE, RULE_SIGN_ROUTE, CLEANUP_ROUTE, OFFICES_ROUTE, TASKS_ROUTE, VOICE_ROUTE, MESSAGE_SIGN_ROUTE, SETTINGS_SIGN_ROUTE, CHOICES_ROUTE, CHOICES_SIGN_ROUTE)
ACTION_POSTS = ("/api/rules", "/api/nl", SETTINGS_ROUTE, TTS_ROUTE, STT_ROUTE, SIGN_ROUTE, RULE_SIGN_ROUTE, CLEANUP_ROUTE, OFFICES_ROUTE, TASKS_ROUTE, VOICE_ROUTE, MESSAGE_SIGN_ROUTE, SETTINGS_SIGN_ROUTE, CHOICES_ROUTE, CHOICES_SIGN_ROUTE, GATES_ROUTE, TOKEN_ROUTE)
class Handler(Exchange):
@@ -168,6 +170,10 @@ class Handler(Exchange):
return lambda body: self.answer(lambda: hub.appoint(body), HTTPStatus.ACCEPTED)
if path == TASKS_ROUTE:
return lambda body: self.answer(lambda: hub.change_tasks(body), HTTPStatus.ACCEPTED)
if path == GATES_ROUTE:
return lambda body: self.answer(lambda: hub.change_gates(body), HTTPStatus.ACCEPTED)
if path == TOKEN_ROUTE:
return lambda body: self.answer(lambda: hub.token_row(body), HTTPStatus.CREATED)
if inbox:
return lambda body: self.answer(lambda: hub.mark(body), HTTPStatus.OK)
if undo:
+2 -1
View File
@@ -16,6 +16,7 @@ from .cleanup import Cleaning, Spool
from .denying import Denying
from .engines import Engines, Speech
from .events import Log, Watch
from .gating import Gating
from .icons import sheet
from .inbox import Inbox
from .jobs import Jobs
@@ -65,7 +66,7 @@ def environment(templates: Path) -> jinja2.Environment:
@dataclass(frozen=True)
class Hub(Mcp, Reviewing, Asking, Messaging, Choosing, Approving, Denying, Requesting, Cleaning, Offices, Tasking, Inbox, Rules, Jobs, Sources, Timeline, View, Language):
class Hub(Mcp, Reviewing, Asking, Messaging, Choosing, Approving, Denying, Requesting, Cleaning, Offices, Tasking, Gating, Inbox, Rules, Jobs, Sources, Timeline, View, Language):
council: Path
pages: Path
public_port: int
+7 -5
View File
@@ -12,15 +12,17 @@ JOB_OPEN = ("queued", "running")
RUN_OUTPUT = 64 << 10
CONSOLE = ("id", "status", "code", "output", "created_ts", "started", "finished")
TASK_JOB = "task"
GATE_JOB = "gate"
CHANGES = (TASK_JOB, GATE_JOB)
def runner_job(job: dict) -> dict:
"""Returns:
What the runner claims: an operator command, or a task change with
kind and argv; a runner that predates task changes verifies the
latter as an operator command and refuses it.
What the runner claims: an operator command, or a task or gate change
with kind and argv; a runner that predates such changes verifies them
as an operator command and refuses them.
"""
change = {"kind": TASK_JOB, "argv": job["argv"]} if job.get("kind") == TASK_JOB else {}
change = {"kind": job["kind"], "argv": job["argv"]} if job.get("kind") in CHANGES else {}
return {**{key: job[key] for key in ("id", "task", "command", "directory", "sha256")}, **change, "proof": job.get("proof")}
@@ -90,7 +92,7 @@ class Jobs:
job, changed = self.jobs.transition(job_id, "running", {"status": status, "code": code, "output": output, "finished": time.time()})
if not changed:
raise Conflict({"id": job_id, "status": job["status"]})
command_done = status == "done" and job.get("kind") != TASK_JOB
command_done = status == "done" and job.get("kind") not in CHANGES
runs = [self.scripts.run(OPERATOR_SCRIPT, ["done", job["task"].removeprefix(OPERATOR)])] if command_done else []
tail = "\n".join(output.splitlines()[-20:])
self.log.add({"kind": "run", "title": f"{status}: {job['command'][:200]}", "text": tail, "ref": {"job": job_id, "task": job["task"], "code": code, "status": status}})
@@ -17,6 +17,8 @@ ESTIMATE = "kvb-org-queue-estimate/scripts/estimate.py"
class Runner(Protocol):
skills: Path
def run(self, script: str, args: list[str]) -> dict: ...
+1 -1
View File
@@ -49,7 +49,7 @@ class Sources:
reviews = self.reviews.listed()
scan = assign(self.tree.get(), tasks, read_claims(self.council), reviews)
history = read_jsonl(self.council / "queue-history.jsonl")
return {**enrich(scan, tasks, reviews, self.touched, history, self.jobs.listed()), "cleanups": self.cleanup_results()}
return {**enrich(scan, tasks, reviews, self.touched, history, self.jobs.listed()), "cleanups": self.cleanup_results(), "gates": self.gate_view(scan)}
def schedules(self, claims: list[dict], now: float) -> list[dict]:
beats = {claim["session"]: claim["beat"] for claim in claims if claim["beat"] is not None}
+37 -14
View File
@@ -18,6 +18,8 @@ FLOW_SOURCE = re.compile(r"[a-z][a-z0-9-]{0,31}")
INVALID = "invalid"
# renaming it silently stops the tree timer: kvb-orchestrator-tree.service tests this file's age
TREE_HEARTBEAT = "tree-heartbeat"
GC_SKILL = "kvb-git-branch-gc"
GC_COMPOSE_DOWN = ("docker", "compose", "-p", "{project}", "down", "-v", "--remove-orphans")
def flow(item: dict) -> dict | None:
@@ -66,34 +68,47 @@ def worktree(item: dict) -> dict:
}
def held_by(worktree: str) -> str:
return f"held by worktree {worktree}: pkgmgr branch cleanup skips it, use branch gc ({GC_SKILL}, pkgmgr gc)"
def dry_run(item: dict) -> list[str]:
"""Returns:
The lines `pkgmgr branch cleanup --local` would act on for one tree
item, read from the gc plan the tree file carries; a remote branch
always stays.
What the runner's `pkgmgr branch cleanup` does for one tree item,
read from the gc plan the tree file carries: it deletes the local
branch and never touches a worktree, Compose stack or volume; a
remote branch stays, the runner refuses a plan that deletes one. A
branch held by a worktree it skips; its lines name what branch gc
would do instead, its `docker compose down -v` deleting the stack's
volumes.
"""
tip = (item["local_sha"] or item["head"] or "")[:12]
merged = f", merged by {item['merged_by']}" if item["merged_by"] else ""
lines = [f"{item['branch']} @ {tip}: {item['state']}{merged}"]
if item["worktree"]:
lines.append(held_by(item["worktree"]))
lines.append(f"branch gc would remove the worktree {item['worktree']} and the local branch {item['branch']}")
lines += [f"branch gc would run {' '.join(GC_COMPOSE_DOWN).format(project=name)}, which deletes the volumes of {name}" for name in item["compose_projects"]]
lines += [f"branch gc would remove the image {name}" for name in item["images"]]
return lines
for action in item["actions"]:
if action == "worktree":
lines.append(f"removes the worktree {item['worktree']}")
elif action == "local-branch":
if action == "local-branch":
lines.append(f"deletes the local branch {item['branch']}")
elif action.startswith("remote"):
lines.append(f"stays: {action} (local only)")
lines.append(f"stays: {action} (local only; the host refuses a plan that deletes it)")
else:
lines.append(action)
lines += [f"stops the Compose stack {name} without -v" for name in item["compose_projects"]]
lines += [f"removes the image {name}" for name in item["images"]]
return lines
def branch(item: dict, path: str) -> dict:
def branch(item: dict, path: str, default: object) -> dict:
remote = next(iter(item["remotes"].values()), "")
head = item["local_sha"] or item["head"] or remote
open_to_delete = item["state"] == "merged-clean" and not item["skip"] and bool(item["local_sha"])
held = held_by(item["worktree"]) if item["worktree"] and item["state"] == "merged-clean" else ""
open_to_delete = item["state"] == "merged-clean" and not item["skip"] and bool(item["local_sha"]) and not item["worktree"]
return {
"held": held,
"token_free": signing.token_free(item, default),
"name": item["branch"],
"head": head,
"upstream": item["upstream"] or "",
@@ -116,13 +131,21 @@ def repo(entry: dict) -> dict:
return {
**{key: value for key, value in entry.items() if key != "items"},
"name": "/".join(Path(entry["path"]).parts[-LABEL_PARTS:]),
"branches": [branch(item, entry["path"]) for item in items if item["branch"]],
"branches": [branch(item, entry["path"], entry.get("default")) for item in items if item["branch"]],
"detached": [worktree(item) for item in items if item["detached"]],
}
def offered(scan: dict) -> list[dict]:
return [entry["cleanup"] for repository in scan["repos"] for entry in repository["branches"] if entry["cleanup"] is not None]
def offered(scan: dict, ready: bool) -> list[dict]:
"""Args:
scan: the tree.
ready: whether pkgmgr holds the GitHub token.
Returns:
The cleanup items deletable now: every one with the token, without
it only those of token-free branches.
"""
return [entry["cleanup"] for repository in scan["repos"] for entry in repository["branches"] if entry["cleanup"] is not None and (ready or entry["token_free"])]
def read_tree(path: Path) -> dict: