This repository has been archived on 2026-08-04. You can view files and clone it, but cannot push or open issues or pull requests.
orchestrator/agent-team/README.md
Adam Moussa 61ab1f0cd5 fix(agent-team): dispatch via GitHub App so P3 reaches CI (run_id resolves)
The P3 dispatcher's default seams shell out to gh/git, but the R720 box has
no gh and a read-only PAT with no Actions scope — so dispatch_apply_verify
returned no run_id and every task parked at verify ("dispatch unresolved").

Add a GitHub-App auth path: the box mints short-lived (~1h) installation
access tokens from the App private key and uses them for the three dispatch
seams, removing the gh dependency.

- agent_team/github_app.py (new): mint_installation_token (RS256 App JWT,
  iss=app_id, iat backdated 60s, exp 9 min; POST /access_tokens) + a lazy
  TokenProvider that caches and re-mints near expiry. Secret-safe: the JWT
  and token are never logged, never in an exception message, never persisted.
- dispatcher.py: app_branch_pusher / app_workflow_dispatcher / app_run_locator
  (additive; gh/git _default_* left untouched). Push auth rides a host-scoped
  http.extraHeader via GIT_CONFIG_* env (token never in argv/ps); the REST
  run locator maps id->databaseId / created_at->createdAt into select_run_id
  and surfaces 4xx promptly instead of silently exhausting the poll window.
- coordinator.py: default_dispatch_node_factory binds the App seams when
  AGENT_TEAM_GH_APP_ID / _INSTALLATION_ID / _PRIVATE_KEY are all set; partial
  or unreadable config logs one warning and falls back to gh-default (never
  raises at serve-start).
- requirements.txt: pin PyJWT, cryptography, requests (App seams + CI fetcher).
- DEPLOY-R720.md / README.md: App dispatch config, permission/scope audit,
  env-precedence check, key rotation/revocation + incident response.

Tests: +18 (test_github_app.py new; dispatcher/coordinator additions) covering
JWT claims, cache/re-mint, token-scrub-on-error, REST field mapping + run-name
correlation, and the partial-env inert fallback. Full suite 1523 passing.
2026-06-24 17:07:01 -04:00

223 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# agent-team — R720 Plane-2 SDLC pipeline
The durable, human-gated agentic SDLC pipeline for the R720 (`sh-secrev` VM),
design: `../docs/r720-agent-team-design.md`. A task flows INTAKE → CLARIFY (the
first human gate) → PLAN → REVIEW, and (P3) → BUILD → DISPATCH → VERIFY → draft
PR. Every stage is durable and resumable (LangGraph + a SQLite checkpointer);
both human gates suspend on `interrupt()` and resume on a real answer.
There are now **two human gates**: the **clarifier** (CLARIFY asks question-sets
until confident) and the **plan-decision gate** (the dead-end when PLAN ⇄ REVIEW
cannot auto-converge). When the review loop hits its revision cap (or the planner
salvages only a partial plan), the pipeline no longer terminally PARKs — it
suspends on a resumable `interrupt()` and the coordinator posts the plan +
reviewer findings to Slack `#agent-team`, threaded under the task root, for the
owner to decide.
```
INTAKE → CLARIFY (Claude, human gate) → PLAN (Claude) → REVIEW (GPT-4.1)
▲ │
└── loop-back ───┤
approve → BUILD/END
review-cap / partial plan
→ PLAN-DECISION GATE (human)
approve → BUILD/END
request changes → PLAN
abandon → FAILED
(P3 box path — gated behind the C1 re-review):
approve → BUILD (DeepSeek) → DISPATCH (push branch, trigger CI,
capture run_id, suspend) → [CI-watcher resumes on terminal
conclusion] → VERIFY (ci_gate) → draft PR
```
> **Status (2026-06-23).** P1 (human gate) + P2 (planner + adversarial review
> loop) + the live runtime (coordinator, Slack/GitHub/Claude-Code transports,
> intake) + P3 (build/dispatch/verify subgraph) + P4 (more transports +
> GitHub-issue intake) are **built, reviewed, and merged to main**.
> **The CI apply/verify workflow is LIVE + provisioned** (the `agent-apply`
> environment, the `AGENT_APPLY_APP_*` secrets, and dispatched runs all exist as
> of 2026-06-22). **The box-side integration that drives it** (run_id capture,
> the BUILD → DISPATCH → VERIFY reorder, async CI-watch, and the fail-safe bound
> `serve` default) is built on this branch but **gated behind the C1 re-review**
> — the `/sh-security-review` + mandatory GPT-4.1 cross-review of the new
> CI-trigger boundary — before it ships to the box. See the project memory
> `project_r720_agent_team` and §7 of the design (and `docs/P3-PHASE0-DESIGN.md`)
> for the phased rollout.
## Layout
```
agent-team/
run-team.py # operator CLI: init-db, list/show/answer/expire/
# force-resume (ledger), start (intake), serve (daemon),
# intake-github
agent_team/ # importable package (snake_case)
coordinator.py # the live runtime keystone: invoker→graph→ResumeWorker;
# start_task / submit_answer / drain / tick / recover / serve
graph.py # LangGraph wiring: P1 (intake→clarify→plan) + opt-in P2
# review loop + opt-in P3 build/verify subgraph
invoker.py # §3.1 real Claude path (subscription-OAuth / API / Bedrock)
invoker_multi.py # WS1 in-process non-Claude invokers (GPT-4.1 / DeepSeek /
# Gemini via the orchestrator's models.py); bind_multi_invoker()
api.py # WS1 FastAPI HTTP API (bearer auth, 127.0.0.1:8765) — SEPARATE
# opt-in process (api.serve()), NOT started by the coordinator
dashboard.py # read-only LAN status dashboard (FastAPI, 0.0.0.0:8770):
# /api/state /api/topology /api/task/{id} + serves web/dist SPA
topology.py # pipeline map derived from the compiled LangGraph
# (get_graph() + NODE_META sidecar) — new agents appear auto
status_page.py # read-only DATA LAYER for /api/state (build_snapshot /
# snapshot_to_dict); HTML rendering retired in the makeover
billing.py # §3.1 claude_invoke billing-mode seam
ci_gate.py # §3.3.2 pure-code authenticated-Checks PASS/FAIL gate
task_model.py / state_store.py
db/{schema.py,schema.sql} # SQLite ledger DDL + BEGIN IMMEDIATE compare-and-set
db/transitions.py # task_transitions recorder (per-task pipeline history,
# idempotent + fail-soft; written by instrumented graph nodes)
ledger.py / responder.py / resume_worker.py / deadline_timer.py / recovery.py
operator_cli.py
nodes/ # pipeline stages + their model bindings
clarifier.py + clarifier_llm.py # human gate (Claude)
planner.py # plan (Claude); per-call max_turns=4 +
# classified retry-once (reliability fix)
review_loop.py + review_loop_llm.py # adversarial review (GPT-4.1 via orchestrator)
builders.py + builders_llm.py # candidate diff (DeepSeek) — INERT, proposes only
verifier.py + verifier_llm.py # ci_gate sole PASS authority; LLM = fix-proposer;
# binds expected_run_id per-task from state["run_id"]
build_verify_subgraph.py # P3 BUILD→DISPATCH→VERIFY topology + the tick()-driven
# CI-watcher that resumes a suspended task on the
# dispatched run's terminal conclusion (or parks on timeout)
handbook.py # WS5 load_handbook_conventions (handbook seam,
# fail-safe → "" if dir missing); planner context
dispatch_invoker.py # P3 DISPATCH node — pushes the per-dispatch head branch,
# triggers CI, and captures run_id + dispatched_at into
# state via a correlation-tagged poll (fails closed on an
# unfound run). Auth via injected seams: gh/git CLI by
# default, or GitHub-App installation tokens box-side
# (github_app.py) when AGENT_TEAM_GH_APP_* is set
github_app.py # mints short-lived (~1h) GitHub-App installation tokens on
# the box (RS256 JWT → /access_tokens); TokenProvider
# caches + re-mints near expiry. Powers the App dispatch
# seams (dispatcher.app_branch_pusher/_workflow_dispatcher/
# _run_locator) so P3 reaches CI with no gh + no box PAT
transport/ # one adapter contract + a live impl per channel
base.py # Transport ABC + QuestionSet / NormalizedAnswer
slack_adapter.py + slack_live.py + slack_listener.py # Block Kit + Socket Mode + /new-task
github_adapter.py + github_live.py + github_intake.py # issue-comment + issue intake
claude_code_adapter.py + claude_code_live.py # file-drop responder
web/ # React/Vite/TypeScript status-dashboard SPA (React Flow map,
# task list, click-through task history); built to web/dist
scripts/ # deploy-r720-ws-rollout.sh — attended WS0–WS5 UPDATE of the box
ci/ # §3.3.2 split-job CI apply/verify workflow (LIVE since 2026-06-22)
systemd/ # agent-team-coordinator.service + agent-team-status.service
DEPLOY-R720.md # provisioning runbook (snapshot-first, rsync, tokens, demo)
tests/ # pytest, one module per source module + sim harness
```
The Claude Code plugin lives in a sibling top-level dir, `../sea-haven-claude-plugin/`
(CLAUDE.md, settings.template.json, `hooks/user_prompt_submit.py`): a
`UserPromptSubmit` hook that forwards `/delegate <task>` prompts from Claude Code
to the HTTP API's `POST /tasks` (env `AGENT_TEAM_API_URL` / `AGENT_TEAM_API_TOKEN`).
The top directory is kebab-case (`agent-team/`); the importable package is
snake_case (`agent_team/`), per the engineering handbook.
## Key design points
- **Durable human gates (§3.3.1).** The `pending_questions` ledger is the single
source of truth for the question lifecycle, with a `kind` discriminator
(`clarify` | `plan_decision`) marking which gate a row belongs to (schema v4,
idempotent additive migration). Every race (duplicate answers, transport
redelivery, answer-vs-timeout) resolves via one atomic compare-and-set against
`status`, inside a `BEGIN IMMEDIATE` transaction — first-answer-wins
(`rowcount == 1`), late/duplicate ignored. **Single-open-gate invariant:** a
thread holds at most one open question at a time (the clarifier row is answered
before the plan stage runs), so clarifier and plan-decision gates can never be
open simultaneously for one thread. The LangGraph `SqliteSaver` checkpointer
shares the same DB file.
- **Plan-review decision gate.** When PLAN ⇄ REVIEW cannot auto-converge
(review-revision cap) or only a partial plan is salvaged, the coordinator posts
the plan (`_summarize_plan`) + reviewer findings (`_summarize_blocker`) to Slack
`#agent-team` and the task owner decides via three verbs — **Approve** (settle
the plan → BUILD), **Request changes** (loop back to the planner with the notes
folded into review feedback), **Abandon** (FAILED). The decision arrives via
Block Kit buttons, a notes modal, or a free-text thread reply; **free-text
prose that isn't a recognized approve/abandon verb defaults to request-changes**
(carrying the full reply as the notes) so a change request can never be
misread as an accidental approve or abandon. Bounded by `MAX_PLAN_GATE_VISITS`
(= 3) so the human loop always terminates.
- **Planner reliability.** The planner's single-shot Claude call runs with
`max_turns=4` (tools stay disabled) so it has room to finish emitting its JSON
rather than exhausting the default 1-turn budget mid-reply, plus a classified
retry-once: a *transient* failure (turn-cap exhaustion or an empty reply) is
retried exactly once; a *deterministic* failure (malformed JSON, missing
phases) fails fast.
- **Fail-safe model seams.** Every node treats model output as untrusted and
fails SAFE: garbage never clears the 98% clarifier gate, never auto-approves a
plan, never fabricates a build success, and the verifier's `ci_gate` is the
**sole** PASS authority (the LLM is structurally a fix-proposer only).
- **Inbound auth (§3.3.1).** The Slack Socket Mode listener authorizes the
**sender** against an owner allowlist (`AGENT_TEAM_SLACK_OWNER_IDS`,
fail-closed) on top of the open-status CAS anti-replay.
- **Billing seam (§3.1).** Claude runs under subscription OAuth on the box;
GPT-4.1 (review) and DeepSeek (builders) route through the local orchestrator
`run.py`. Switching Claude billing is a config flip.
## Status dashboard (WebUI)
A read-only LAN dashboard (FastAPI, `0.0.0.0:8770`, no auth, `mode=ro` ledger
opens) for watching the pipeline. Served by `agent_team.dashboard` (systemd unit
`agent-team-status.service`):
- **Live pipeline map** — a React Flow graph **auto-laid-out from the real
LangGraph** (`topology.py` introspects `compiled.get_graph()` + a `NODE_META`
display sidecar). Adding an agent node in `graph.py` makes it appear on the map
with no manual coordinates; nodes group into **trees** (processes) branching off
`intake`. Node color = live state; loop-back edges (review→plan, verify→build)
render dashed.
- **Click-through task history** — selecting a task opens a timeline of its journey
through each node (entry/exit timestamps, per-node duration, per-stage cost, Q&A,
verdicts, plan), backed by the `task_transitions` ledger (schema v3) written by
the coordinator's instrumented graph nodes (`db/transitions.py`, fail-soft).
Endpoints: `GET /api/state` (live overview + per-node state), `GET /api/topology`
(map nodes/edges/trees), `GET /api/task/{thread_id}` (one task's history;
`thread_id` is validated `^[A-Za-z0-9_-]{1,64}$`). The legacy stdlib HTML page was
retired; `status_page.py` remains as the `/api/state` data layer.
**Build (on the Mac — the box Node is too old for Vite 5+):**
```
cd agent-team/web
npm ci && npm run build # -> web/dist (gitignored), rsynced to the VM
npm test # Vitest + React Testing Library
```
In dev, `npm run dev` proxies `/api` to a locally running dashboard
(`AGENT_TEAM_DASH`, default `http://127.0.0.1:8770`).
## WS0–WS5 rollout glossary
The "WS-rollout" (workstreams 0–5) layered HTTP/integration surfaces onto the
P1–P4 pipeline. What is **live** vs **inert** after the rollout:
| WS | What it adds | Live? |
|---|---|---|
| WS1 | `invoker_multi.py` (in-process GPT-4.1 / DeepSeek / Gemini via the orchestrator's `models.py`) + `api.py` (FastAPI HTTP API, bearer auth via `AGENT_TEAM_API_TOKEN`, binds `127.0.0.1:8765`, `/docs`+`/openapi` disabled, concurrency-capped) | `bind_multi_invoker()` wired in `run-team.py` `_cmd_serve` (LIVE); the **HTTP API is a separate opt-in process** (`api.serve()`), NOT started by the coordinator |
| WS5 | `nodes/handbook.py` `load_handbook_conventions` (reads `SEA_HAVEN_HANDBOOK_DIR` or `~/.sea-haven/engineering-handbook`, fail-safe → `""`); `retriever.py` `save_memory` writes to a `_box-drafts/` review queue | LIVE — the planner prompt receives the handbook via the `context_provider` seam in `run-team.py` `_build_coordinator` |
| WS2/WS0/WS4 | Slack `/new-task` slash command (AUTHZ-01 owner-allowlist gated) → `Coordinator.set_new_task_callback`; the `sea-haven-claude-plugin/` (CLAUDE.md, settings, `/delegate` `UserPromptSubmit` hook) | LIVE (`/new-task` wired in `serve`); the plugin/HTTP-API path is opt-in |
| WS3 / P3 | `nodes/dispatch_invoker.py` (DISPATCH LangGraph node) + the BUILD → DISPATCH → VERIFY reorder, the `tick()`-driven CI-watcher, per-task `run_id` plumbing, and the bound `serve` default | **Built on `feat/agent-team-p3-box-integration`, gated behind the C1 re-review** before it ships to the box. The `agent-apply` GitHub Environment human-approval gate is KEPT. Dispatch auth is **seam-injected**: gh/git CLI by default, or — when `AGENT_TEAM_GH_APP_*` is set — **box-side GitHub-App installation tokens** (`github_app.py`), so the box mints a short-lived (~1h) write token on demand instead of needing `gh` or a standing PAT (a conscious, mitigated deviation from "operator host only" — App scoped to one repo, Contents+Actions only, tokens short-lived & never logged; CI still re-verifies by hash and the `agent-apply` reviewer gate still applies). The bound P3 wiring is the fail-safe `serve` default: a missing `AGENT_TEAM_REPO_OWNER`/`_NAME` or CI-read token degrades to the INERT P3 path (task parks + a `#agent-team` notice), never a serve-start crash; a partial/unreadable `AGENT_TEAM_GH_APP_*` config logs one warning and falls back to the gh-default seams |
The HTTP API endpoints: `POST /tasks` (start a task), `GET /tasks/{thread_id}`
(status), `POST /orchestrator/invoke` (one-shot model invoke). See
`DEPLOY-R720.md` for the WS-rollout deploy (`scripts/deploy-r720-ws-rollout.sh`).
## Running the tests
```
cd agent-team
python3 -m pytest -q # conftest puts the package on sys.path; no install needed
```
## Deploy
Deploy-gated. See `DEPLOY-R720.md` for the provisioning runbook (VM snapshot
first, rsync, venv deps, `~/secrev.env` tokens, `init-db`, systemd, and the live
P1 exit-criteria demo). Secrets are never committed.