# 001 - Agent Orchestrator ## User Story As the operator of many parallel agents, I want one simple Agent Orchestrator surface and a radio that only interrupts for urgent calls, so that agents spend few tokens on talk, urgent news reaches them at once, and I can drill from three views down to any agent, branch or artefact. ## Decisions (operator-confirmed 2026-11-06; do not re-litigate) 1. **One document**: radio model, surface, pkgmgr branch tree and views live in this one requirement as phases. Rejected: one document per feature (the operator wants the whole picture in one place). 2. **Location**: `docs/requirements/NNN-slug.md`, numbered per repository. Rejected: a file next to the code (no index, no numbering). 3. **Radio classes**: `post`, `call`, `newsletter`, `broadcast`, `mayday`, `pan-pan`, `security`. Rejected: `broadcast` only as the umbrella of the three alarms (the operator wants an immediate call to all without alarm). 4. **Mailbox**: `post` (one agent) and `newsletter` (all agents) are delivered only when the agent ends its turn and goes idle, batched; an agent already idle is not woken and sees its mailbox at the end of its next turn. Rejected: next prompt, next tool step (interrupts work), batched wake-ups (one turn per wake). 5. **Immediate classes**: `call`, `broadcast`, `pan-pan`, `mayday` and `security` reach the agent at its next tool step and wake an idle agent. 6. **Call receipt**: the caller gets a delivered/read receipt and does not wait; an answer is a call of its own. Rejected: the caller blocks until the answer (costs the caller), delivery without feedback. 7. **Mayday pause**: while a mayday holds, a PreToolUse hook blocks every writing action until `radio.py clear`; reading, the radio and teardown stay allowed. The sender and every agent that joins with `radio.py assist` to help keep working. Rejected: an instruction only (not enforced), heavy jobs only (today's behaviour, too weak), a full blockade that also stops the helpers. 8. **Radio and post**: two channels. `radio.py send` is the radio and always immediate (`call`, `broadcast`, `pan-pan`, `mayday`, `security`); `radio.py post` is mail and always delayed (`post` with `--to`, `newsletter` without). `send direct` stays as an alias so running peers keep working: with `--wake` it becomes `call`, without it `post`. Rows stored before the change carry no version mark and are read as before: `direct` as `post`, `broadcast` as `newsletter`. Rejected: `send broadcast` as an alias of `newsletter` (makes the immediate broadcast unreachable). 9. **Name**: kvb-hub is renamed in full, with a one-time migration: visible "Agent Orchestrator", technical `kvb-orchestrator` / `kvb_orchestrator` (directory, module, container, systemd unit, MCP server, environment `KVB_ORCHESTRATOR_*`, make targets). Rejected: visible name only (two names for one thing). 10. **Navigation**: Stream (sub-items Inbox and Regeln) · Agents (ex Graph) · Artifacts (ex Tree). Agents and Artifacts each offer Orbit, Chord, Swimlane, Tree and 3D. 11. **Header**: title · ⌘K · ⚙ only. The status pills become navigation badges (Inbox: waiting items, Agents: active agents, Artifacts: runner jobs); Mayday shows only as a red banner with "aufheben" while active; the background shows day and night. Rejected: one status dot with a popover (an extra click), a status block in a side bar, a "Lage" screen (effort), no header at all. 12. **Tools**: Steuerung (Funk an alle, Mayday aufheben, Priorität, Tag/Nacht) moves to the Agents toolbar, Regeln under Inbox, Himmel, Musik, Leitstand and Benachrichtigungen to ⚙ "Ambiente"; ⌘K reaches every one. Rejected: one ⋯ menu (hides instead of ordering). 13. **Stream**: four category chips Für dich · Funk · Arbeit · Transkripte; one search line with `agent:`, `art:`, `seit:`; sub-filters, saved views and a custom range under "Filter ▾"; the time bar becomes "24 h ▾" with the replay inside; transcripts are off by default. Rejected: a self-grouping stream, a Kibana-like query language (effort), preset chips only. 14. **3D**: `3d-force-graph` 1.80.1 on `three` 0.186.1, vendored as in meta.infinito.nexus; the Canvas depth axes Zeit and Spawn-Tiefe become 3D options. Rejected: keeping the Orbit-only Canvas depth. 15. **Artifacts scope**: git (repositories → branches → worktrees with their agents and reviews) plus the kvb-org-rasci register artefacts (images, volumes, logs) as leaves of the task or worktree that owns them. Rejected: git only. 16. **Artifacts views**: Orbit: repository sun, branch planets (size = commits ahead), worktree moons, agent satellites. Chord: branch overlap (same files touched, merges) as conflict risk. Swimlane: one lane per branch with commits, reviews, gates and pushes. Tree: the pkgmgr branch tree. 17. **Tree data**: the scan moves from `hub/kvb_hub/tree.py` into pkgmgr as `pkgmgr branch tree --format json|yaml --output FILE`, rewritten by a systemd user timer every minute and after every pkgmgr branch action; the orchestrator only reads the file, no git runs in its container. Rejected: the hub keeps scanning (pkgmgr is not the single source, git runs in the container), a live websocket daemon (effort). Hardened through kvb-think-dialectic before it is built. 18. **Order**: 1 radio → 2 surface and rename → 3 pkgmgr branch tree → 4 views and 3D; one bundle per phase, each change set through kvb-code-refactor. 19. **Icons over text**: every HTML surface a skill renders (the orchestrator, the review pages, the meditation page) shows Bootstrap Icons instead of text labels wherever an icon carries the meaning, each with a tooltip and an `aria-label`; terminal output and `SKILL.md` keep emoji. Rejected: Font Awesome Free (icons under CC BY 4.0, core loads it from a kit CDN), emoji in HTML (look differs per platform). 20. **Branch state**: `pkgmgr branch tree` covers every repository pkgmgr manages and gives each branch and worktree the classification of `pkgmgr branch gc` (one implementation, no second copy): merged and by what, clean or uncommitted changes, unpushed commits, gone upstream, locked, stale, protected and the gc skip reason. The Artifacts Tree shows it as icons. Phase 3 builds the data, phase 4 the icons. 21. **Delete from the tree**: merged, orphaned branches are deleted by click in the Artifacts Tree (phase 4), per item and as "Alle gemergten löschen": - **Credentials**: only while KeePassXC is unlocked; it hands pkgmgr the GitHub token through the Secret Service and the SSH key through its SSH agent, and no secret passes the orchestrator container. Rejected: the master password typed into the dialog (secret crosses container and browser), a token-only, local-only delete. - **Scope**: exactly what `pkgmgr branch gc --apply` deletes for a `merged-clean` item (worktree, local and remote branch, compose stack, images); a gc skip reason locks the button. Rejected: local only. - **Confirmation**: the click shows the gc dry-run lines of the chosen items, and "Löschen" needs the operator signature (`/api/sign`). Rejected: a second click without signature, one click without preview. - **Execution**: the orchestrator writes a signed job into a host directory; a systemd user path unit runs `pkgmgr branch gc --apply` for exactly those items and writes the result back. Rejected: an `operator.py` row (not direct). 22. **Organisation update**: a new skill `kvb-org-update` tells every agent and client which skills changed and how to use them, as one `newsletter` per update; phase 1 announces the radio change through it. ## Approach ```mermaid flowchart TD subgraph P["🧭 0 · Preparation"] P1["📋 Write requirement"] --> P2{"✅ Operator approves?"} P2 -- "✏️ corrections" --> P1 end P2 -- "🤖 yes" --> R1 subgraph R["📻 1 · Radio model"] R1["🔴 Red tests: classes, mailbox, receipt, mayday block"] --> R2["🛠️ radio.py, Stop mailbox hook, PreToolUse mayday hook, SKILL.md"] R2 --> R3{"🧪 Tests and make test green?"} R3 -- "🔴 no: fix" --> R2 R3 -- "🟢 yes" --> R4["🧹 kvb-code-refactor"] R4 --> RF{"💬 Operator feedback?"} RF -- "✏️ changes" --> R2 end RF -- "👍 commit word" --> S0 subgraph S["🧭 2 · Surface and rename"] S0["🔀 Rebase feature/kvb-hub onto origin/master"] --> S1["🔴 Red tests: nav, header, badges, stream, migration"] S1 --> S2["🛠️ Nav, header, badges, banner, stream, inbox, tools, rename + migration"] S2 --> S3{"🧪 Tests green?"} S3 -- "🔴 no: fix" --> S2 S3 -- "🟢 yes" --> S4["📸 Screenshots 1920 / 1366 / 390 px"] S4 --> SF{"💬 Operator feedback?"} SF -- "✏️ changes" --> S2 end SF -- "👍 commit word" --> T0 subgraph T["🌳 3 · pkgmgr branch tree"] T0["⚖️ kvb-think-dialectic on the data path"] --> T1["🔴 Red tests on fixture repositories"] T1 --> T2["🛠️ pkgmgr branch tree with gc states, timer, orchestrator reads the file"] T2 --> T3{"🧪 Tests green, no git in the container?"} T3 -- "🔴 no: fix" --> T2 T3 -- "🟢 yes" --> TF{"💬 Operator feedback?"} TF -- "✏️ changes" --> T2 end TF -- "👍 commit word" --> V1 subgraph V["🪐 4 · Agents and Artifacts views"] V1["🔴 Red tests: five views per tab, artefact leaves, delete job"] --> V2["🛠️ Orbit, Chord, Swimlane, Tree with state icons and delete, 3D (3d-force-graph), host gc unit"] V2 --> V3{"🧪 Tests green?"} V3 -- "🔴 no: fix" --> V2 V3 -- "🟢 yes" --> V4["📸 Screenshots of every view"] V4 --> VF{"💬 Operator feedback?"} VF -- "✏️ changes" --> V2 end VF -- "👍 fits" --> D1["🏁 Full gate, commit with operator OK, push hand-over"] ``` ## Acceptance Criteria ### Radio model - [ ] `radio.py send` accepts exactly `call`, `broadcast`, `pan-pan`, `mayday`, `security` and the alias `direct`; `post`, `newsletter` and any other class exit 2. - [ ] `radio.py post --to X` stores `post`; `radio.py post` without `--to` stores `newsletter`. - [ ] `call` and `send direct` require `--to`; `broadcast`, `pan-pan`, `mayday` and `security` exit 2 with `--to`. - [ ] `send direct --wake` is stored as `call`, `send direct` as `post`. - [ ] New rows carry a version mark; rows without it are read as before: `direct` as `post` (as `call` when it woke), `broadcast` as `newsletter`. - [ ] The UserPromptSubmit hook (`radio.py hook`) and the PostToolBatch hook (`radio.py tool`) never print a `post` or `newsletter` row. - [ ] A new Stop hook `radio.py mailbox`, registered in `scripts/enable-plugins.js`, prints the unseen `post` and `newsletter` rows of the session at most 10 at a time with a "N more" line, marks them seen and exits 2; with nothing unseen it exits 0 and prints nothing. - [ ] `radio.py listen` wakes an idle session only for `call`, `broadcast`, `pan-pan`, `mayday` and `security`, never for `post` or `newsletter`. - [ ] `radio.py tool` delivers `call` (to its addressee), `broadcast`, `pan-pan`, `mayday` and `security` at the next tool step. - [ ] When a call reaches the callee's context, a receipt row (callee, call time, read time) is stored; the caller's mailbox shows "CALL read