| 1 | { |
| 2 | "description": "Agents vectors: shared remote Claude Code sessions fronted by an Eleven group (docs/superpowers/specs/2026-08-05-eleven-claude-agents-design.md). The workstation bridge posts durable session.* events; every member's fold converges on the same fleet board. Payloads aren't sender-signed, so the fold re-validates everything regardless of what the bridge checked. Source of truth: web/static/apps/agents.js.", |
| 3 | "app": "agents", |
| 4 | "source": "web/static/apps/agents.js", |
| 5 | "state_shape": { "sessions": [{ "id": "string (^[A-Za-z0-9_-]{1,64}$)", "name": "string (≤80, from the wire field `session` — `name` is transport-injected)", "flavor": "\"scoped\" | \"plain\" (anything else folds to scoped)", "root": "string (≤300)", "worktree": "string (≤300)", "by": "string (≤80, who asked)", "ts": "number (epoch ms)", "state": "running | idle | waiting | dead | ended", "activity": "string (≤200)", "statusTs": "number", "prompts": "number", "turns": "number", "result": "string (≤500)", "approvals": [{ "rid": "string", "tool": "string (≤120)", "digest": "string (≤200)", "state": "pending | allowed | denied", "by": "string (decider, from transport name)" }], "endedAt": "number", "reason": "string (≤120)" }] }, |
| 6 | "notes": [ |
| 7 | "The session's human name rides the wire as `session`, never `name` — `name`/`ts`/`uid` are transport-injected (emitter identity), so the bridge cannot use them for payload.", |
| 8 | "session.spawn is idempotent by id and never replaces: an existing id is a no-op (a re-spawn is a new id). Max 50 sessions per thread, oldest ended evicted first.", |
| 9 | "session.status on an unknown session, with an unknown state, or after session.end is a no-op — ended is terminal; a late status frame can't resurrect a session.", |
| 10 | "session.approval: the bridge posts the pending request; a member's decision folds onto it. First decision wins — a decided approval never flips, and a decision for an unposted rid is a no-op.", |
| 11 | "session.end drops pending approvals (they die with the session) and is the bridge's cue to remove a scoped session's worktree (close-session-closes-worktree, decided 2026-08-28; committed branches survive).", |
| 12 | "session.tool / session.stream are ephemeral (relay-only) types claimed with no-op folds — a hostile stored copy folds harmlessly.", |
| 13 | "Per the accepted design, session.prompt IS the member's chat message (one event, two projections); board state keeps only a count." |
| 14 | ], |
| 15 | "vectors": [ |
| 16 | { |
| 17 | "name": "spawn", |
| 18 | "description": "The bridge announces a new scoped session; every canonical field materialises.", |
| 19 | "initial_state": { "sessions": [] }, |
| 20 | "event": { |
| 21 | "type": "session.spawn", |
| 22 | "id": "s1", |
| 23 | "session": "fix-login", |
| 24 | "flavor": "scoped", |
| 25 | "root": "~/github.com/elevenmessenger/messenger", |
| 26 | "worktree": "wt-fix-login", |
| 27 | "by": "Paul", |
| 28 | "name": "Claude", |
| 29 | "ts": 1700000000000 |
| 30 | }, |
| 31 | "expected_state_update": { |
| 32 | "sessions": [ |
| 33 | { "id": "s1", "name": "fix-login", "flavor": "scoped", "root": "~/github.com/elevenmessenger/messenger", "worktree": "wt-fix-login", "by": "Paul", "ts": 1700000000000, "state": "running", "activity": "", "statusTs": 1700000000000, "prompts": 0, "turns": 0, "result": "", "approvals": [], "endedAt": 0, "reason": "" } |
| 34 | ] |
| 35 | }, |
| 36 | "expected_group_chat_projection": { "text": "started session “fix-login” on ~/github.com/elevenmessenger/messenger", "action": true } |
| 37 | }, |
| 38 | { |
| 39 | "name": "spawn_duplicate_id_noop", |
| 40 | "description": "A second spawn with an existing id never replaces — history can't be rewritten by a later event.", |
| 41 | "initial_state": { |
| 42 | "sessions": [ |
| 43 | { "id": "s1", "name": "fix-login", "flavor": "scoped", "root": "r", "worktree": "", "by": "Paul", "ts": 1700000000000, "state": "running", "activity": "", "statusTs": 1700000000000, "prompts": 0, "turns": 0, "result": "", "approvals": [], "endedAt": 0, "reason": "" } |
| 44 | ] |
| 45 | }, |
| 46 | "event": { "type": "session.spawn", "id": "s1", "session": "impostor", "flavor": "plain", "name": "Mallory", "ts": 1700000009000 }, |
| 47 | "expected_state_update": { |
| 48 | "sessions": [ |
| 49 | { "id": "s1", "name": "fix-login", "flavor": "scoped", "root": "r", "worktree": "", "by": "Paul", "ts": 1700000000000, "state": "running", "activity": "", "statusTs": 1700000000000, "prompts": 0, "turns": 0, "result": "", "approvals": [], "endedAt": 0, "reason": "" } |
| 50 | ] |
| 51 | } |
| 52 | }, |
| 53 | { |
| 54 | "name": "spawn_bad_flavor_folds_scoped", |
| 55 | "description": "A hostile or future flavor value folds to the safe reading: scoped.", |
| 56 | "initial_state": { "sessions": [] }, |
| 57 | "event": { "type": "session.spawn", "id": "s2", "session": "ops", "flavor": "root-of-everything", "name": "Claude", "ts": 1700000001000 }, |
| 58 | "expected_state_update": { |
| 59 | "sessions": [ |
| 60 | { "id": "s2", "name": "ops", "flavor": "scoped", "root": "", "worktree": "", "by": "", "ts": 1700000001000, "state": "running", "activity": "", "statusTs": 1700000001000, "prompts": 0, "turns": 0, "result": "", "approvals": [], "endedAt": 0, "reason": "" } |
| 61 | ] |
| 62 | } |
| 63 | }, |
| 64 | { |
| 65 | "name": "spawn_missing_id_noop", |
| 66 | "description": "An event without a usable id is hopeless and folds to a no-op.", |
| 67 | "initial_state": { "sessions": [] }, |
| 68 | "event": { "type": "session.spawn", "session": "ghost", "name": "Mallory", "ts": 1700000002000 }, |
| 69 | "expected_state_update": { "sessions": [] } |
| 70 | }, |
| 71 | { |
| 72 | "name": "status_updates_board", |
| 73 | "description": "session.status is board truth: state + the one-line activity + its timestamp.", |
| 74 | "initial_state": { |
| 75 | "sessions": [ |
| 76 | { "id": "s1", "name": "fix-login", "flavor": "scoped", "root": "r", "worktree": "", "by": "Paul", "ts": 1700000000000, "state": "running", "activity": "", "statusTs": 1700000000000, "prompts": 0, "turns": 0, "result": "", "approvals": [], "endedAt": 0, "reason": "" } |
| 77 | ] |
| 78 | }, |
| 79 | "event": { "type": "session.status", "id": "s1", "state": "waiting", "activity": "go test ./internal/bot", "name": "Claude", "ts": 1700000003000 }, |
| 80 | "expected_state_update": { |
| 81 | "sessions": [ |
| 82 | { "id": "s1", "name": "fix-login", "flavor": "scoped", "root": "r", "worktree": "", "by": "Paul", "ts": 1700000000000, "state": "waiting", "activity": "go test ./internal/bot", "statusTs": 1700000003000, "prompts": 0, "turns": 0, "result": "", "approvals": [], "endedAt": 0, "reason": "" } |
| 83 | ] |
| 84 | }, |
| 85 | "expected_group_chat_projection": null |
| 86 | }, |
| 87 | { |
| 88 | "name": "status_unknown_session_or_state_noop", |
| 89 | "description": "A status for an unknown session id folds to a no-op (and an unknown state value would too).", |
| 90 | "initial_state": { "sessions": [] }, |
| 91 | "event": { "type": "session.status", "id": "s9", "state": "running", "activity": "x", "name": "Claude", "ts": 1700000003000 }, |
| 92 | "expected_state_update": { "sessions": [] } |
| 93 | }, |
| 94 | { |
| 95 | "name": "prompt_counts_and_projects", |
| 96 | "description": "A member steers the session: the prompt IS their chat message (action-line projection for now); the board counts it.", |
| 97 | "initial_state": { |
| 98 | "sessions": [ |
| 99 | { "id": "s1", "name": "fix-login", "flavor": "scoped", "root": "r", "worktree": "", "by": "Paul", "ts": 1700000000000, "state": "running", "activity": "", "statusTs": 1700000000000, "prompts": 0, "turns": 0, "result": "", "approvals": [], "endedAt": 0, "reason": "" } |
| 100 | ] |
| 101 | }, |
| 102 | "event": { "type": "session.prompt", "id": "s1", "session": "fix-login", "text": "fix the flaky reminder test", "name": "Paul", "uid": "u_paul", "ts": 1700000004000 }, |
| 103 | "expected_state_update": { |
| 104 | "sessions": [ |
| 105 | { "id": "s1", "name": "fix-login", "flavor": "scoped", "root": "r", "worktree": "", "by": "Paul", "ts": 1700000000000, "state": "running", "activity": "", "statusTs": 1700000000000, "prompts": 1, "turns": 0, "result": "", "approvals": [], "endedAt": 0, "reason": "" } |
| 106 | ] |
| 107 | }, |
| 108 | "expected_group_chat_projection": { "text": "asked fix-login: “fix the flaky reminder test”", "action": true } |
| 109 | }, |
| 110 | { |
| 111 | "name": "turn_counts_and_projects", |
| 112 | "description": "The assistant's completed turn: a bubble in the app's voice, tagged with the session; the board counts it.", |
| 113 | "initial_state": { |
| 114 | "sessions": [ |
| 115 | { "id": "s1", "name": "fix-login", "flavor": "scoped", "root": "r", "worktree": "", "by": "Paul", "ts": 1700000000000, "state": "running", "activity": "", "statusTs": 1700000000000, "prompts": 1, "turns": 0, "result": "", "approvals": [], "endedAt": 0, "reason": "" } |
| 116 | ] |
| 117 | }, |
| 118 | "event": { "type": "session.turn", "id": "s1", "session": "fix-login", "text": "Found it — the ticker fires before the store hydrates.", "name": "Claude", "ts": 1700000005000 }, |
| 119 | "expected_state_update": { |
| 120 | "sessions": [ |
| 121 | { "id": "s1", "name": "fix-login", "flavor": "scoped", "root": "r", "worktree": "", "by": "Paul", "ts": 1700000000000, "state": "running", "activity": "", "statusTs": 1700000000000, "prompts": 1, "turns": 1, "result": "", "approvals": [], "endedAt": 0, "reason": "" } |
| 122 | ] |
| 123 | }, |
| 124 | "expected_group_chat_projection": { "text": "[fix-login] Found it — the ticker fires before the store hydrates.", "action": false } |
| 125 | }, |
| 126 | { |
| 127 | "name": "approval_first_decision_wins", |
| 128 | "description": "The bridge posts a pending approval; Ana allows it; Bob's later deny is a no-op — first decision wins, attributed by the transport name.", |
| 129 | "event_sequence": [ |
| 130 | { "type": "session.spawn", "id": "s1", "session": "fix-login", "flavor": "scoped", "root": "r", "by": "Paul", "name": "Claude", "ts": 1700000000000 }, |
| 131 | { "type": "session.approval", "id": "s1", "rid": "a1", "session": "fix-login", "tool": "git push", "digest": "origin main", "state": "pending", "name": "Claude", "ts": 1700000006000 }, |
| 132 | { "type": "session.approval", "id": "s1", "rid": "a1", "session": "fix-login", "tool": "git push", "state": "allowed", "name": "Ana", "uid": "u_ana", "ts": 1700000007000 }, |
| 133 | { "type": "session.approval", "id": "s1", "rid": "a1", "session": "fix-login", "tool": "git push", "state": "denied", "name": "Bob", "uid": "u_bob", "ts": 1700000008000 } |
| 134 | ], |
| 135 | "expected_state": { |
| 136 | "sessions": [ |
| 137 | { "id": "s1", "name": "fix-login", "flavor": "scoped", "root": "r", "worktree": "", "by": "Paul", "ts": 1700000000000, "state": "running", "activity": "", "statusTs": 1700000000000, "prompts": 0, "turns": 0, "result": "", "approvals": [{ "rid": "a1", "tool": "git push", "digest": "origin main", "state": "allowed", "by": "Ana" }], "endedAt": 0, "reason": "" } |
| 138 | ] |
| 139 | } |
| 140 | }, |
| 141 | { |
| 142 | "name": "approval_decision_without_pending_noop", |
| 143 | "description": "A decision for a request the log never saw folds to a no-op — approvals exist only once the bridge posts them.", |
| 144 | "initial_state": { |
| 145 | "sessions": [ |
| 146 | { "id": "s1", "name": "fix-login", "flavor": "scoped", "root": "r", "worktree": "", "by": "Paul", "ts": 1700000000000, "state": "running", "activity": "", "statusTs": 1700000000000, "prompts": 0, "turns": 0, "result": "", "approvals": [], "endedAt": 0, "reason": "" } |
| 147 | ] |
| 148 | }, |
| 149 | "event": { "type": "session.approval", "id": "s1", "rid": "a9", "state": "allowed", "name": "Mallory", "ts": 1700000008000 }, |
| 150 | "expected_state_update": { |
| 151 | "sessions": [ |
| 152 | { "id": "s1", "name": "fix-login", "flavor": "scoped", "root": "r", "worktree": "", "by": "Paul", "ts": 1700000000000, "state": "running", "activity": "", "statusTs": 1700000000000, "prompts": 0, "turns": 0, "result": "", "approvals": [], "endedAt": 0, "reason": "" } |
| 153 | ] |
| 154 | } |
| 155 | }, |
| 156 | { |
| 157 | "name": "end_is_terminal", |
| 158 | "description": "Closing a session: pending approvals die with it, and a late status frame can't resurrect it.", |
| 159 | "event_sequence": [ |
| 160 | { "type": "session.spawn", "id": "s1", "session": "fix-login", "flavor": "scoped", "root": "r", "by": "Paul", "name": "Claude", "ts": 1700000000000 }, |
| 161 | { "type": "session.approval", "id": "s1", "rid": "a1", "session": "fix-login", "tool": "git push", "state": "pending", "name": "Claude", "ts": 1700000006000 }, |
| 162 | { "type": "session.end", "id": "s1", "session": "fix-login", "reason": "closed", "name": "Paul", "uid": "u_paul", "ts": 1700000009000 }, |
| 163 | { "type": "session.status", "id": "s1", "state": "running", "activity": "zombie", "name": "Claude", "ts": 1700000010000 } |
| 164 | ], |
| 165 | "expected_state": { |
| 166 | "sessions": [ |
| 167 | { "id": "s1", "name": "fix-login", "flavor": "scoped", "root": "r", "worktree": "", "by": "Paul", "ts": 1700000000000, "state": "ended", "activity": "", "statusTs": 1700000000000, "prompts": 0, "turns": 0, "result": "", "approvals": [], "endedAt": 1700000009000, "reason": "closed" } |
| 168 | ] |
| 169 | }, |
| 170 | "expected_group_chat_projection_of": { "type": "session.end", "id": "s1", "session": "fix-login", "reason": "closed", "name": "Paul", "uid": "u_paul", "ts": 1700000009000 }, |
| 171 | "expected_group_chat_projection": { "text": "closed session “fix-login”", "action": true } |
| 172 | }, |
| 173 | { |
| 174 | "name": "result_keeps_latest_summary", |
| 175 | "description": "A milestone summary lands on the board (latest wins) and speaks as a bubble.", |
| 176 | "initial_state": { |
| 177 | "sessions": [ |
| 178 | { "id": "s1", "name": "fix-login", "flavor": "scoped", "root": "r", "worktree": "", "by": "Paul", "ts": 1700000000000, "state": "running", "activity": "", "statusTs": 1700000000000, "prompts": 1, "turns": 2, "result": "", "approvals": [], "endedAt": 0, "reason": "" } |
| 179 | ] |
| 180 | }, |
| 181 | "event": { "type": "session.result", "id": "s1", "session": "fix-login", "summary": "2 files changed, go test green", "name": "Claude", "ts": 1700000011000 }, |
| 182 | "expected_state_update": { |
| 183 | "sessions": [ |
| 184 | { "id": "s1", "name": "fix-login", "flavor": "scoped", "root": "r", "worktree": "", "by": "Paul", "ts": 1700000000000, "state": "running", "activity": "", "statusTs": 1700000000000, "prompts": 1, "turns": 2, "result": "2 files changed, go test green", "approvals": [], "endedAt": 0, "reason": "" } |
| 185 | ] |
| 186 | }, |
| 187 | "expected_group_chat_projection": { "text": "[fix-login] ✅ 2 files changed, go test green", "action": false } |
| 188 | }, |
| 189 | { |
| 190 | "name": "ephemeral_types_fold_to_noop", |
| 191 | "description": "A hostile client storing a session.stream frame as a real message folds harmlessly — the type is claimed with a no-op fold.", |
| 192 | "initial_state": { "sessions": [] }, |
| 193 | "event": { "type": "session.stream", "id": "s1", "text": "partial…", "name": "Mallory", "ts": 1700000012000 }, |
| 194 | "expected_state_update": { "sessions": [] } |
| 195 | } |
| 196 | ] |
| 197 | } |
| 198 | |