herdr-plugin-amq 0.1.9 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,72 +3,214 @@
3
3
  [![npm version](https://img.shields.io/npm/v/herdr-plugin-amq.svg)](https://www.npmjs.com/package/herdr-plugin-amq)
4
4
  [![CI](https://github.com/cabra-lat/herdr-plugin-amq/actions/workflows/ci.yml/badge.svg)](https://github.com/cabra-lat/herdr-plugin-amq/actions/workflows/ci.yml)
5
5
  [![Security](https://github.com/cabra-lat/herdr-plugin-amq/actions/workflows/security.yml/badge.svg)](https://github.com/cabra-lat/herdr-plugin-amq/actions/workflows/security.yml)
6
+ [![Tests](https://img.shields.io/badge/tests-142%20passing-brightgreen.svg)](https://github.com/cabra-lat/herdr-plugin-amq/actions/workflows/ci.yml)
7
+ [![Coverage](https://img.shields.io/badge/coverage-79.9%25-brightgreen.svg)](https://github.com/cabra-lat/herdr-plugin-amq/actions/workflows/ci.yml)
6
8
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
9
+ [![Proudly Vibe Coded - Plasma Mix](https://vibecoded.fyi/badges/flat/main/proudly-vibe-coded-plasma-mix.svg)](https://vibecoded.fyi/)
7
10
 
8
11
  > The asynchronous nervous system for autonomous AI agent swarms in [Herdr](https://herdr.dev/).
9
12
 
10
- Herdr AMQ combines native pure-JS Maildir messaging, lifecycle-aware doorbells, a decentralized task bus, immutable CAS evidence, and the local-first **AGmail** dashboard.
13
+ Herdr AMQ is a local coordination layer for turn-based coding agents. It combines native pure-JS Maildir messaging, lifecycle-aware doorbells, a decentralized task bus, immutable CAS evidence, and the local-first **AGmail** dashboard.
11
14
 
12
- ## Why it exists
15
+ The core problem is simple: an agent finishes a turn and goes idle, while a message or task waits in its queue. Herdr AMQ watches the queue, checks the agent's lifecycle, and rings the doorbell only when the agent can act.
13
16
 
14
- LLM agents are turn-based: when an agent finishes a response or tool sequence, it goes idle. A persistent inbox is useful, but it needs a doorbell. Herdr AMQ watches agent panes, wakes only idle or done agents with unread mail or assigned backlog work, and leaves working panes alone.
17
+ ## The origin & the problem
15
18
 
16
- The result is an asynchronous workflow:
19
+ > *"If you follow AI news, you have probably seen endless hype around 'multi-agent swarms' talking to each other... That is cute for a 30-second screen recording. In a real codebase with actual physics, compiler errors, and Git history, it is a complete disaster."*
20
+ > — Read the full story: [**My AI Agents Send Me Emails: Office Drama in a Godot Repo**](https://cabra.pw/my-ai-agents-send-me-emails.html)
17
21
 
18
- 1. A coordinator or human sends a message or creates a task.
19
- 2. The bridge detects the unread item and checks the Herdr pane state.
20
- 3. An idle agent receives a precise drain/claim prompt.
21
- 4. The agent works in its isolated worktree and replies on the original thread.
22
- 5. AGmail provides a human view of messages, activity, tasks, and evidence.
22
+ Synchronous chat rooms and blocking `wait` loops fall apart for turn-based coding agents:
23
23
 
24
- ## Documentation map
24
+ 1. **Context window bloat**: group chats flood agent context with irrelevant noise.
25
+ 2. **Turn-based nature of LLMs**: when an agent finishes its tool execution, it terminates its turn and goes to sleep. It cannot run a busy-wait loop.
26
+ 3. **Dead mailboxes without a doorbell**: an inbox directory is inert storage. If the agent is asleep, incoming messages sit unread forever.
25
27
 
26
- - [Architecture and live model reporting](docs/architecture.md)
27
- - [Installation and Herdr setup](docs/installation.md)
28
- - [CLI, fleet lifecycle, and templates](docs/cli-and-workflows.md)
29
- - [Security and testing](docs/security-and-testing.md)
30
- - [AGmail visual tour](docs/ui-screenshots.md)
28
+ ### The missing piece: the doorbell bridge
29
+
30
+ The bridge daemon continuously inspects agent inboxes. When an agent is `idle` or `done` in its Herdr terminal pane and has unread mail or an assigned backlog card, the bridge **rings the doorbell** via `herdr agent prompt`. The sleeping agent wakes up, drains its inbox, does the work, replies on-thread, and goes back to sleep.
31
+
32
+ ```mermaid
33
+ flowchart TD
34
+ AMQ[".agent-mail/ (Maildir + RFC 5322)<br/>Decoupled Markdown Transmissions"]
35
+ BUS[".agent-mail/bus/ (Task Cards)<br/>backlog/ → doing/ → blocked/ → done/"]
36
+ DAEMON["Bridge Daemon<br/>Watches mailboxes & checks Herdr agent states"]
37
+ H_BUSY["working → Leave alone (no spam)"]
38
+ H_BLOCKED["blocked → Alert coordinator / human"]
39
+ H_IDLE["idle / done → RING DOORBELL<br/>(herdr agent prompt)"]
40
+ AGENT["Awakened Agent<br/>1. drain inbox<br/>2. claim task & execute<br/>3. reply on-thread<br/>4. back to sleep"]
41
+ AGMAIL["AGmail Dashboard<br/>http://127.0.0.1:8505 (Strictly Local)"]
42
+
43
+ AMQ -->|New mail arrives| DAEMON
44
+ BUS -->|Assigned card waits| DAEMON
45
+ DAEMON --> H_BUSY
46
+ DAEMON --> H_BLOCKED
47
+ DAEMON --> H_IDLE
48
+ H_IDLE --> AGENT
49
+ AGENT -->|Sends mail + evidence| AMQ
50
+ AGENT -->|Claims / updates tasks| BUS
51
+ AMQ -.->|Monitored & inspected by| AGMAIL
52
+ BUS -.->|Rendered live in Kanban| AGMAIL
53
+ ```
54
+
55
+ ## Choose an install
56
+
57
+ ### Published CLI: fastest path
58
+
59
+ Install the executable directly from npm when you want the dashboard, task bus, or CLI without setting up a plugin checkout:
60
+
61
+ ```bash
62
+ npm install --global herdr-plugin-amq
63
+ herdr-amq status
64
+ herdr-amq dashboard
65
+ ```
66
+
67
+ You can also run a one-off command without a global install:
31
68
 
32
- ## Quick start
69
+ ```bash
70
+ npx --yes herdr-plugin-amq status
71
+ npx --yes herdr-plugin-amq dashboard
72
+ ```
73
+
74
+ The npm package is the CLI and dashboard entry point. It does not automatically register Herdr actions or panes; use the full plugin setup below when you want those integrations.
75
+
76
+ ### Full Herdr plugin
77
+
78
+ Link the plugin from a checkout to register its bridge actions, panes, and agent events:
33
79
 
34
80
  ```bash
81
+ git clone https://github.com/cabra-lat/herdr-plugin-amq.git herdr-plugin-amq
82
+ cd herdr-plugin-amq
35
83
  npm ci --ignore-scripts
36
84
  herdr plugin link .
85
+ herdr plugin action list --plugin cabra.amq
86
+ ```
87
+
88
+ From that checkout, start the local dashboard without installing a global command:
89
+
90
+ ```bash
91
+ node bin/herdr-amq.mjs dashboard
92
+ ```
93
+
94
+ To use the `herdr-amq` command in this source setup, run `npm link` once and then use the normal CLI commands. For a new swarm, bootstrap the queue, worktrees, bridge daemon, and first doorbell pass:
95
+
96
+ ```bash
97
+ npm link
37
98
  herdr-amq bootstrap --kind opencode
38
99
  herdr-amq dashboard
39
100
  ```
40
101
 
41
- The dashboard is local-only at `http://127.0.0.1:8505`. The CLI and agent protocol are documented in the [workflow guide](docs/cli-and-workflows.md).
102
+ Run commands from the project or workspace that owns your `.agent-mail` queue. If you already have a queue, skip `bootstrap`.
103
+
104
+ ## What you get
105
+
106
+ ### A doorbell for sleeping agents
107
+
108
+ The bridge watches Maildir messages and assigned backlog cards, then checks Herdr's pane state:
109
+
110
+ - `idle` or `done` with new work receives a precise drain and claim prompt.
111
+ - `working` panes are left alone so a prompt cannot interrupt an active turn.
112
+ - Delivered message and task IDs are recorded so the same event is not announced twice.
113
+ - Blocked agents raise an actionable alert for the coordinator or human operator.
42
114
 
43
- ## AGmail preview
115
+ ### Mail, tasks, and evidence that stay inspectable
116
+
117
+ Messages are RFC 5322 Markdown files in Maildir, with real `In-Reply-To`, `References`, and thread metadata. Task cards move through `backlog/`, `doing/`, `blocked/`, and `done/`. Attachments and verification evidence can be stored in the CAS blobstore or pinned to a Git object, so a handoff does not depend on terminal scrollback.
118
+
119
+ ### AGmail mission control
120
+
121
+ AGmail is a local webmail and Kanban interface for the swarm. It provides:
122
+
123
+ - Inbox, sent mail, starred mail, all-mail search, and threaded conversations.
124
+ - A responsive board with owners, stage controls, linked transmissions, and dispatch composer.
125
+ - Agent presence with pane state, unread counts, current activity, and the model reported by the live harness when available. `Working` means an active turn; `Idle` means the turn ended and the agent is ready for input.
126
+ - Human personas, including an explicit God Mode identity for sending as the operator without impersonating an agent.
127
+ - Responsive desktop, tablet, and mobile layouts with a pull-to-refresh guard and compact task actions.
128
+
129
+ ### Fleet lifecycle and worktree isolation
130
+
131
+ `bootstrap` and `fleet` commands discover supported agent personas, provision Maildirs, prepare isolated Git worktrees, start the bridge, and perform an initial doorbell pass. Agents can therefore resume from a clean turn without sharing a monolithic chat context.
132
+
133
+ ## How it works
134
+
135
+ 1. A coordinator or human creates a message or task.
136
+ 2. The bridge sees the unread Maildir item or assigned backlog card.
137
+ 3. Herdr reports whether the target agent is working, idle, done, or blocked.
138
+ 4. Only an actionable agent is prompted to drain and claim the work.
139
+ 5. The agent replies on the original thread and attaches evidence when needed.
140
+ 6. AGmail shows the message, task, status, and proof in one local view.
141
+
142
+ ## AGmail visual tour
143
+
144
+ The captures below come from the isolated browser fixture. They contain fixture data rather than a live mailbox.
145
+
146
+ ### Threaded mail and verification evidence
147
+
148
+ ![AGmail threaded mail with verification evidence](docs/images/agmail-mail-thread.webp)
149
+
150
+ AGmail keeps the latest message, its sender metadata, and quick-reply actions together while the thread remains navigable.
151
+
152
+ ### Live agent activity
44
153
 
45
154
  ![AGmail agent activity sheet](docs/images/agmail-agent-activity.webp)
46
155
 
47
- The activity card reports live harness state and the current model when Herdr/OpenCode exposes it. Profile configuration remains a fallback, with the source exposed in the API.
156
+ The activity sheet reports the current task, pane, unread count, and live harness model. If no live model or explicit profile model is configured, it shows `Not configured` instead of inventing a placeholder.
157
+
158
+ ### Task dossier and dispatch
48
159
 
49
160
  ![AGmail task dossier](docs/images/agmail-task-drawer.webp)
50
161
 
51
- The compact mobile task form keeps its owner warning and action row visible at narrow widths.
162
+ The task drawer keeps the board context, owner, stage controls, linked AMQ thread, transmissions, and dispatch composer in one place.
163
+
164
+ ### Human and agent personas
165
+
166
+ ![AGmail persona switcher](docs/images/agmail-persona-switcher.webp)
167
+
168
+ The persona switcher makes the active identity explicit. God Mode is the human operator; selecting an agent persona scopes the mailbox and compose identity to that agent.
169
+
170
+ ### Mobile swarm presence
171
+
172
+ ![AGmail mobile swarm presence](docs/images/agmail-mobile-presence.webp)
173
+
174
+ The mobile layout keeps the inbox, agent presence, and navigation usable on a narrow screen.
175
+
176
+ ### Compact task creation
52
177
 
53
178
  ![Compact AGmail New Task form](docs/images/agmail-mobile-new-task.webp)
54
179
 
180
+ At 320×568, owner guidance and the Cancel/Create Task actions remain visible without horizontal overflow.
181
+
182
+ ## Documentation
183
+
184
+ - [Architecture and live model reporting](docs/architecture.md)
185
+ - [Installation and Herdr setup](docs/installation.md)
186
+ - [CLI, fleet lifecycle, and templates](docs/cli-and-workflows.md)
187
+ - [Security and testing](docs/security-and-testing.md)
188
+ - [AGmail visual tour](docs/ui-screenshots.md)
189
+
55
190
  ## Requirements
56
191
 
57
- - Node.js >= 18
58
- - Herdr >= 0.7.0
59
- - Chrome/Chromium for browser journeys only
60
- - Zero npm runtime dependencies
192
+ - Node.js 18 or newer.
193
+ - Herdr 0.7.0 or newer for bridge actions, panes, and fleet lifecycle features.
194
+ - Chrome or Chromium only for the optional browser journeys.
195
+ - No npm runtime dependencies; `playwright-core` is development-only.
61
196
 
62
- ## Verification
197
+ ## Security note
198
+
199
+ AGmail and the AMQ bridge are local development tools. The server binds to loopback, validates `Host` headers, and rejects path traversal and credential access. Never expose the dashboard to a public network or untrusted LAN. Do not put secrets in messages, task descriptions, prompt templates, or screenshots. See [Security and testing](docs/security-and-testing.md).
200
+
201
+ ## Development and verification
202
+
203
+ Install the development dependencies from a checkout, then run the same gates used by CI:
63
204
 
64
205
  ```bash
206
+ npm ci --ignore-scripts
65
207
  npm test
66
208
  npm run test:e2e
67
209
  npm run check
68
210
  npm audit --audit-level=high
69
211
  ```
70
212
 
71
- The browser suite uses an isolated Maildir, board, and fake Herdr socket, so screenshots and tests never touch the live swarm.
213
+ The browser suite uses an isolated Maildir, board, and fake Herdr socket, so screenshots and tests never touch the live swarm. CI runs the test matrix on Ubuntu and macOS across Node 18, 20, and 22, plus a dedicated security audit workflow.
72
214
 
73
215
  ## License
74
216
 
@@ -12,6 +12,8 @@ Synchronous group chat does not fit autonomous coding sessions:
12
12
 
13
13
  AMQ provides persistent Maildir messages, a file-based task bus, and immutable CAS attachments. The bridge watches Herdr panes and prompts only agents that are `idle` or `done`. Working panes are left alone; blocked panes raise an actionable alert.
14
14
 
15
+ Herdr lifecycle semantics are explicit: `working` is an active turn, while `idle` and `done` are terminal turn states where a new prompt can be delivered. AGmail coalesces rapid status events before rendering so transient output or stale snapshots do not make the indicator flicker.
16
+
15
17
  ## Data flow
16
18
 
17
19
  ```mermaid
@@ -45,6 +47,6 @@ flowchart TD
45
47
 
46
48
  ## Live model reporting
47
49
 
48
- The profile model is configuration metadata, not proof of the model currently selected by a running harness. When Herdr exposes model fields, AGmail uses them first. For OpenCode panes, the Herdr `agent_session.value` is resolved against the local OpenCode session database and rendered as `provider/model (variant)`. The API also returns `modelSource` so the UI and operators can distinguish live harness data from a profile fallback.
50
+ The profile model is optional configuration metadata, not proof of the model currently selected by a running harness. When Herdr exposes model fields, AGmail uses them first. For OpenCode panes, the Herdr `agent_session.value` is resolved against the local OpenCode session database and rendered as `provider/model (variant)`. The API returns `modelSource` when a model is known; when neither live nor explicit profile metadata is available, the model remains `null` and the UI shows `Not configured`.
49
51
 
50
52
  The resolver is best-effort: unavailable Herdr/OpenCode data leaves the configured profile visible rather than inventing a model.
Binary file
@@ -9,17 +9,45 @@
9
9
 
10
10
  The runtime has no npm production dependencies. `playwright-core` is a development-only dependency for the browser suite.
11
11
 
12
+ ## Published CLI
13
+
14
+ Install the CLI and dashboard entry point from npm:
15
+
16
+ ```bash
17
+ npm install --global herdr-plugin-amq
18
+ herdr-amq status
19
+ herdr-amq dashboard
20
+ ```
21
+
22
+ For a one-off command, use `npx` instead of a global install:
23
+
24
+ ```bash
25
+ npx --yes herdr-plugin-amq status
26
+ npx --yes herdr-plugin-amq dashboard
27
+ ```
28
+
29
+ The npm package does not automatically register Herdr actions or panes. Use the checkout flow below when you want the full plugin integration.
30
+
12
31
  ## Link the plugin
13
32
 
14
33
  ```bash
15
34
  git clone https://github.com/cabra-lat/herdr-plugin-amq.git herdr-plugin-amq
16
35
  cd herdr-plugin-amq
36
+ npm ci --ignore-scripts
17
37
  herdr plugin link .
18
38
  herdr plugin list
19
39
  herdr plugin action list --plugin cabra.amq
20
40
  ```
21
41
 
22
- The dashboard and queue root are discovered from the current workspace. The web server binds to `127.0.0.1` and refuses foreign `Host` headers.
42
+ The dashboard and queue root are discovered from the current workspace. Run commands from the project or worktree that owns the queue. The web server binds to `127.0.0.1` and refuses foreign `Host` headers.
43
+
44
+ From the checkout, the dashboard can be started without a global CLI link:
45
+
46
+ ```bash
47
+ node bin/herdr-amq.mjs dashboard
48
+ ```
49
+
50
+ Run `npm link` if you want the `herdr-amq` command available globally while developing from this checkout. For a new swarm, `herdr-amq bootstrap --kind opencode` provisions the queue, worktrees, bridge daemon, and first doorbell pass.
23
51
 
24
52
  ## Herdr actions
25
53
 
@@ -2,9 +2,15 @@
2
2
 
3
3
  These captures come from the isolated browser fixture. They contain no live mailbox data and are checked by the desktop/mobile journey suite.
4
4
 
5
+ ## Threaded mail
6
+
7
+ The mail view keeps the latest message, sender metadata, thread navigation, and quick-reply actions together.
8
+
9
+ ![AGmail threaded mail with verification evidence](images/agmail-mail-thread.webp)
10
+
5
11
  ## Agent activity
6
12
 
7
- The activity sheet combines the live Herdr state, assigned task, unread count, pane ID, and the model reported by the running harness. The model is sourced from Herdr/OpenCode session metadata when available, rather than blindly displaying the profile fallback.
13
+ The activity sheet combines the live Herdr state, assigned task, unread count, pane ID, and the model reported by the running harness. The model is sourced from Herdr/OpenCode session metadata when available, rather than blindly displaying a placeholder; if no model is available, the sheet shows `Not configured`.
8
14
 
9
15
  ![AGmail agent activity sheet showing live task, pane, and harness model](images/agmail-agent-activity.webp)
10
16
 
@@ -14,6 +20,18 @@ The task drawer keeps the stage controls, owner, linked AMQ thread, transmission
14
20
 
15
21
  ![AGmail task dossier with linked transmissions and dispatch controls](images/agmail-task-drawer.webp)
16
22
 
23
+ ## Human and agent personas
24
+
25
+ The persona switcher makes the active identity explicit and scopes mailbox and compose identity to the selected human or agent persona.
26
+
27
+ ![AGmail persona switcher showing human and agent identities](images/agmail-persona-switcher.webp)
28
+
29
+ ## Mobile presence
30
+
31
+ The mobile layout keeps the inbox, agent presence, and navigation usable on a narrow screen.
32
+
33
+ ![AGmail mobile swarm presence](images/agmail-mobile-presence.webp)
34
+
17
35
  ## Compact New Task flow
18
36
 
19
37
  At 320×568, the owner tip and sticky action row remain visible. The form warns before assigning a card to an owner with claimed or blocked work.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "herdr-plugin-amq",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "description": "Herdr plugin for AMQ (Agent Message Queue) autonomous bridge, status monitoring, and AGmail dashboard",
5
5
  "type": "module",
6
6
  "main": "src/index.mjs",
@@ -45,8 +45,23 @@
45
45
  "amq",
46
46
  "multi-agent",
47
47
  "autonomous-agents",
48
+ "ai-agents",
49
+ "agent-swarm",
50
+ "agent-coordination",
51
+ "agent-orchestration",
52
+ "maildir",
53
+ "kanban",
54
+ "task-board",
55
+ "webmail",
56
+ "dashboard",
48
57
  "cas-blobstore",
49
- "agmail"
58
+ "agmail",
59
+ "git-worktree",
60
+ "local-first",
61
+ "opencode",
62
+ "claude-code",
63
+ "coding-agents",
64
+ "cli"
50
65
  ],
51
66
  "author": "cabra.lat",
52
67
  "license": "MIT",
package/src/actions.mjs CHANGED
@@ -115,8 +115,9 @@ export function handleDoorbell() {
115
115
  }
116
116
 
117
117
  const force = process.argv.includes("--force") || process.argv.includes("-f");
118
- console.log(`🔔 Checking AMQ inboxes at ${amqRoot}${force ? " (force=true)" : ""}...`);
119
- const res = runDoorbellPass({ amqRoot, force, allowPrompt: true, persistState: true });
118
+ const dryRun = process.argv.includes("--dry-run");
119
+ console.log(`🔔 Checking AMQ inboxes at ${amqRoot}${force ? " (force=true)" : ""}${dryRun ? " [dry-run: no prompts, heals, or state writes]" : ""}...`);
120
+ const res = runDoorbellPass({ amqRoot, force, dryRun, allowPrompt: !dryRun, persistState: !dryRun });
120
121
 
121
122
  if (!res.ok) {
122
123
  console.error(`❌ Doorbell check failed: ${res.error}`);
package/src/blobs.mjs CHANGED
@@ -34,6 +34,11 @@ export const MIME_TYPES = {
34
34
  ".zip": "application/zip",
35
35
  ".tar": "application/x-tar",
36
36
  ".gz": "application/gzip",
37
+
38
+ // Video (pilot: inline <video> playback, same-origin blob stream)
39
+ ".mp4": "video/mp4",
40
+ ".m4v": "video/mp4",
41
+ ".webm": "video/webm",
37
42
  };
38
43
 
39
44
  export function getMimeType(ext) {
@@ -104,6 +109,7 @@ export function storeBlob(input, amqRoot, originalName = "") {
104
109
 
105
110
  const sizeBytes = contentBuffer.length;
106
111
  const isImage = [".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".bmp"].includes(ext);
112
+ const isVideo = [".mp4", ".m4v", ".webm"].includes(ext);
107
113
  const isLog = [".log", ".txt", ".csv", ".json", ".out", ".diff", ".patch"].includes(ext);
108
114
 
109
115
  return {
@@ -114,6 +120,7 @@ export function storeBlob(input, amqRoot, originalName = "") {
114
120
  mime: getMimeType(ext),
115
121
  sizeBytes,
116
122
  isImage,
123
+ isVideo,
117
124
  isLog,
118
125
  exists: true,
119
126
  url: `/api/blob/${sha256}${ext ? `?ext=${encodeURIComponent(ext)}` : ""}`,
@@ -269,6 +276,7 @@ export function pinGitRef(repoRoot, relativePath, commit = "HEAD") {
269
276
 
270
277
  const ext = path.extname(cleanRel).toLowerCase();
271
278
  const isImage = [".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".bmp"].includes(ext);
279
+ const isVideo = [".mp4", ".m4v", ".webm"].includes(ext);
272
280
  const isLog = [".log", ".txt", ".csv", ".json", ".out", ".diff", ".patch"].includes(ext);
273
281
 
274
282
  const ref = {
@@ -282,6 +290,7 @@ export function pinGitRef(repoRoot, relativePath, commit = "HEAD") {
282
290
  mime: getMimeType(ext),
283
291
  sizeBytes,
284
292
  isImage,
293
+ isVideo,
285
294
  isLog,
286
295
  exists: true,
287
296
  url: `/api/git-file?commit=${commitSha}&path=${encodeURIComponent(cleanRel)}`,
package/src/bridge.mjs CHANGED
@@ -104,20 +104,35 @@ function getAgentStatus(handle) {
104
104
  }
105
105
  }
106
106
 
107
- function healAgentName(handle, dryRun = false) {
107
+ export function healAgentName(handle, dryRun = false, run = runHerdr) {
108
108
  try {
109
- const out = runHerdr(["pane", "list"]);
109
+ const out = run(["pane", "list"]);
110
110
  const panes = JSON.parse(out)?.result?.panes ?? [];
111
111
  const needle = `- ${handle} - `;
112
- const hit = panes.find((p) =>
112
+ let hit = panes.find((p) =>
113
113
  (p.terminal_title_stripped || p.terminal_title || "").includes(needle)
114
114
  );
115
+ if (!hit) {
116
+ // Fallback: the terminal title is often overwritten by the foreground
117
+ // program (e.g. "OpenCode"), while the tab label keeps the canonical
118
+ // handle. Match the tab label exactly, and heal only when that tab
119
+ // holds a single pane (multi-pane tabs are ambiguous — skip them).
120
+ try {
121
+ const tabsOut = run(["tab", "list"]);
122
+ const tabs = JSON.parse(tabsOut)?.result?.tabs ?? [];
123
+ const tab = tabs.find((t) => (t.label || "") === handle);
124
+ if (tab) {
125
+ const inTab = panes.filter((p) => p.tab_id === tab.tab_id);
126
+ if (inTab.length === 1) hit = inTab[0];
127
+ }
128
+ } catch {}
129
+ }
115
130
  if (!hit) return false;
116
131
  if (dryRun) {
117
132
  console.log(`[bridge] DRY: would heal name ${handle} <- pane ${hit.pane_id}`);
118
133
  return true;
119
134
  }
120
- runHerdr(["agent", "rename", hit.pane_id, handle]);
135
+ run(["agent", "rename", hit.pane_id, handle]);
121
136
  console.log(`[bridge] Healed pane ${hit.pane_id} -> renamed back to '${handle}'`);
122
137
  return true;
123
138
  } catch (err) {
package/src/fleet.mjs CHANGED
@@ -113,7 +113,7 @@ export function prepopulateFleet(amqRoot, repoRoot) {
113
113
  role: persona.role || persona.description,
114
114
  description: persona.description,
115
115
  prompt: persona.prompt,
116
- model: persona.model || "Gemini 3.8 Flash (High)",
116
+ model: persona.model || null,
117
117
  worktree: worktreeResult.ok ? worktreeResult.path : undefined,
118
118
  });
119
119
 
package/src/herdr.mjs CHANGED
@@ -120,7 +120,8 @@ export async function getHerdrStatusMap() {
120
120
 
121
121
  export function normalizeHerdrStatus(value) {
122
122
  const status = String(value || "").trim().toLowerCase();
123
- if (status === "online" || status === "active") return "idle";
123
+ if (status === "online") return "idle";
124
+ if (status === "active") return "working";
124
125
  if (["idle", "working", "blocked", "done", "error", "unknown"].includes(status)) return status;
125
126
  return "unknown";
126
127
  }
@@ -180,6 +181,18 @@ export function mapHerdrAgentActivity(agent, observedAt = new Date().toISOString
180
181
  };
181
182
  }
182
183
 
184
+ export function normalizeHerdrEvent(message) {
185
+ if (!message || typeof message !== "object") return null;
186
+ if (message.method && message.params && typeof message.params === "object") {
187
+ return { type: String(message.method), ...message.params };
188
+ }
189
+ if (message.event && message.data && typeof message.data === "object") {
190
+ const payload = message.data.pane && typeof message.data.pane === "object" ? message.data.pane : message.data;
191
+ return { ...payload, type: String(message.event).replace("_", ".") };
192
+ }
193
+ return null;
194
+ }
195
+
183
196
  // ─── Long-lived event subscription ──────────────────────────────────────────
184
197
 
185
198
  /**
@@ -200,7 +213,9 @@ export function subscribeHerdrEvents({ onEvent, onDisconnect, onConnect } = {})
200
213
  JSON.stringify({
201
214
  id: subId,
202
215
  method: "events.subscribe",
203
- params: {},
216
+ params: {
217
+ subscriptions: [{ type: "pane.updated" }],
218
+ },
204
219
  }) + "\n";
205
220
 
206
221
  let sock = null;
@@ -226,11 +241,9 @@ export function subscribeHerdrEvents({ onEvent, onDisconnect, onConnect } = {})
226
241
  if (!line.trim()) continue;
227
242
  try {
228
243
  const msg = JSON.parse(line);
229
- // Skip the ack for the subscribe call itself
230
244
  if (msg.id === subId && msg.result) continue;
231
- if (msg.method && msg.params && onEvent) {
232
- onEvent({ type: msg.method, ...msg.params });
233
- }
245
+ const event = normalizeHerdrEvent(msg);
246
+ if (event && onEvent) onEvent(event);
234
247
  } catch {}
235
248
  }
236
249
  });