feat: implement core/team_runner.py and cli/agency.py

- core/blackboard.py: add t3_task_lists table, extend event kinds to
  include full visibility vocabulary (gate_pending, gate_approved,
  gate_rejected, gate_paused, gate_resumed, path_amendment, log), and
  add query methods (get_events, get_latest_gate_event, get_t3_task_lists,
  all_t3_committed, get_briefs, get_workstreams, etc.)

- core/team_runner.py: full run lifecycle orchestrator
  - Loads team.yaml + role_registry.yaml, instantiates all four adapter types
  - T1 Plan (two-phase: plan + accept), T2 Lead/Specialist/Synthesis,
    T3 Squad Lead with mesh draft/commit cycle and mesh-timeout escalation,
    T4 swarm+pipeline with dep-ordering, T5 fan-out + joint verdict
  - Async (asyncio.gather) for parallel workstream and T4/T5 fan-out
  - Gate logic: gate_pending → notify → poll blackboard → gate_approved/rejected
  - Path amendment monitor (background task)
  - EscalationHandler integrated into _dispatch_with_retry
  - T3 mesh timeout → T2 re-scope escalation
  - Terminal failure → notify + run status=failed
  - VCS branch creation + PR at T1 Accept phase
  - Runtime selection: coding_agent runtime for preferred_runtime="coding_agent",
    tier_runtime_map overrides, fallback to default runtime

- cli/agency.py: agency CLI with subcommands
  - run: start pipeline, prints run_id + watch/inspect hints
  - watch: tails blackboard events live with ANSI-coloured output
  - inspect: run tree, --tier filter, --brief detail view
  - approve: write gate_approved directly to blackboard (universal gate path)
  - reject: write gate_rejected with --reason
  - pause: write gate_paused signal
  - resume: write gate_resumed signal

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-01 16:40:43 -04:00
parent 641f122cdb
commit 8994f87a43
4 changed files with 2382 additions and 73 deletions

View File

@@ -85,18 +85,37 @@ CREATE TABLE IF NOT EXISTS events (
event_id TEXT PRIMARY KEY,
run_id TEXT NOT NULL,
brief_id TEXT, -- NULL for run-level events
kind TEXT NOT NULL, -- spawned|completed|failed|escalated|retried
kind TEXT NOT NULL, -- see _EVENT_KINDS
detail TEXT, -- JSON
created_at TEXT NOT NULL,
FOREIGN KEY (run_id) REFERENCES runs(run_id)
);
CREATE TABLE IF NOT EXISTS t3_task_lists (
entry_id TEXT PRIMARY KEY,
run_id TEXT NOT NULL,
workstream_id TEXT NOT NULL,
t3_agent_id TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'draft', -- draft|committed
tasks TEXT NOT NULL DEFAULT '[]', -- JSON array of T4 task descriptors
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
FOREIGN KEY (run_id) REFERENCES runs(run_id)
);
"""
# Valid status values per table — used for input validation.
_RUN_STATUSES = {"pending", "active", "review", "done", "failed"}
_WS_STATUSES = {"pending", "active", "blocked", "done", "failed"}
_BRIEF_STATUSES = {"pending", "active", "done", "failed"}
_EVENT_KINDS = {"spawned", "completed", "failed", "escalated", "retried"}
_EVENT_KINDS = {
# Lifecycle
"spawned", "completed", "failed", "escalated", "retried",
# Visibility / gates
"gate_pending", "gate_approved", "gate_rejected", "gate_paused", "gate_resumed",
# Amendments / informational
"path_amendment", "log",
}
# ---------------------------------------------------------------------------
@@ -360,6 +379,194 @@ class Blackboard:
# Cleanup
# ------------------------------------------------------------------
# ------------------------------------------------------------------
# Event queries
# ------------------------------------------------------------------
def get_events(
self,
kinds: Optional[list[str]] = None,
after_iso: Optional[str] = None,
brief_id: Optional[str] = None,
limit: int = 100,
) -> list[dict[str, Any]]:
"""
Query events for this run.
Parameters
----------
kinds : Filter by event kinds (OR). None = all kinds.
after_iso : Only return events created after this ISO-8601 timestamp.
brief_id : Filter by brief_id. None = all briefs.
limit : Maximum rows to return (most recent first).
"""
conditions = ["run_id = ?"]
params: list[Any] = [self.run_id]
if kinds:
placeholders = ",".join("?" * len(kinds))
conditions.append(f"kind IN ({placeholders})")
params.extend(kinds)
if after_iso:
conditions.append("created_at > ?")
params.append(after_iso)
if brief_id:
conditions.append("brief_id = ?")
params.append(brief_id)
where = " AND ".join(conditions)
rows = self._execute(
f"SELECT * FROM events WHERE {where} ORDER BY created_at DESC LIMIT ?",
(*params, limit),
).fetchall()
return [dict(r) for r in rows]
def get_latest_gate_event(
self, gate_name: str, after_iso: Optional[str] = None
) -> Optional[dict[str, Any]]:
"""
Return the most recent gate_approved or gate_rejected event for
*gate_name* written after *after_iso*.
The event detail JSON is expected to contain a ``"gate"`` field
matching *gate_name*. Falls back to returning any gate resolution
event if none carry an explicit gate field (for CLI-written events
that omit it).
"""
events = self.get_events(
kinds=["gate_approved", "gate_rejected"],
after_iso=after_iso,
limit=20,
)
# Prefer events whose detail.gate matches
for ev in events:
try:
detail = json.loads(ev.get("detail") or "{}")
if detail.get("gate") == gate_name or not detail.get("gate"):
return ev
except (json.JSONDecodeError, TypeError):
return ev
return None
def get_all_events(self, limit: int = 500) -> list[dict[str, Any]]:
"""Return all events for this run, oldest first."""
rows = self._execute(
"SELECT * FROM events WHERE run_id=? ORDER BY created_at ASC LIMIT ?",
(self.run_id, limit),
).fetchall()
return [dict(r) for r in rows]
# ------------------------------------------------------------------
# T3 task lists
# ------------------------------------------------------------------
def create_t3_draft(
self,
*,
workstream_id: str,
t3_agent_id: str,
) -> str:
"""Insert a draft t3_task_list entry. Returns entry_id."""
entry_id = _new_uuid()
now = _now_iso()
self._execute(
"INSERT OR IGNORE INTO t3_task_lists "
"(entry_id, run_id, workstream_id, t3_agent_id, status, tasks, created_at, updated_at) "
"VALUES (?, ?, ?, ?, 'draft', '[]', ?, ?)",
(entry_id, self.run_id, workstream_id, t3_agent_id, now, now),
commit=True,
)
return entry_id
def commit_t3_task_list(
self,
*,
workstream_id: str,
t3_agent_id: str,
tasks: list[Any],
) -> None:
"""Update a t3_task_list entry to committed with the final task list."""
now = _now_iso()
tasks_json = json.dumps(tasks)
self._execute(
"UPDATE t3_task_lists SET status='committed', tasks=?, updated_at=? "
"WHERE run_id=? AND workstream_id=? AND t3_agent_id=?",
(tasks_json, now, self.run_id, workstream_id, t3_agent_id),
commit=True,
)
def get_t3_task_lists(self, workstream_id: str) -> list[dict[str, Any]]:
"""Return all t3_task_list entries for a workstream."""
rows = self._execute(
"SELECT * FROM t3_task_lists WHERE run_id=? AND workstream_id=? ORDER BY created_at ASC",
(self.run_id, workstream_id),
).fetchall()
result = []
for r in rows:
d = dict(r)
try:
d["tasks"] = json.loads(d.get("tasks") or "[]")
except (json.JSONDecodeError, TypeError):
d["tasks"] = []
result.append(d)
return result
def all_t3_committed(self, workstream_id: str) -> bool:
"""Return True if all t3_task_list entries for the workstream are committed."""
rows = self._execute(
"SELECT status FROM t3_task_lists WHERE run_id=? AND workstream_id=?",
(self.run_id, workstream_id),
).fetchall()
if not rows:
return False
return all(r["status"] == "committed" for r in rows)
# ------------------------------------------------------------------
# Briefs query
# ------------------------------------------------------------------
def get_briefs(
self,
*,
status: Optional[str] = None,
tier: Optional[int] = None,
workstream_id: Optional[str] = None,
) -> list[dict[str, Any]]:
"""Query briefs with optional filters."""
conditions = ["run_id = ?"]
params: list[Any] = [self.run_id]
if status:
conditions.append("status = ?")
params.append(status)
if tier is not None:
conditions.append("tier = ?")
params.append(tier)
if workstream_id:
conditions.append("workstream_id = ?")
params.append(workstream_id)
where = " AND ".join(conditions)
rows = self._execute(
f"SELECT * FROM briefs WHERE {where} ORDER BY created_at ASC",
tuple(params),
).fetchall()
return [dict(r) for r in rows]
def get_workstreams(self) -> list[dict[str, Any]]:
"""Return all workstreams for this run."""
rows = self._execute(
"SELECT * FROM workstreams WHERE run_id=? ORDER BY created_at ASC",
(self.run_id,),
).fetchall()
return [dict(r) for r in rows]
# ------------------------------------------------------------------
# Cleanup
# ------------------------------------------------------------------
def close(self) -> None:
"""Close the database connection gracefully."""
with self._lock:

File diff suppressed because it is too large Load Diff