herdr-plugin-amq 0.1.7 → 0.1.9
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 +35 -265
- package/bin/herdr-amq.mjs +6 -0
- package/docs/architecture.md +50 -0
- package/docs/cli-and-workflows.md +72 -0
- package/docs/images/agmail-agent-activity.webp +0 -0
- package/docs/images/agmail-mobile-new-task.webp +0 -0
- package/docs/images/agmail-task-drawer.webp +0 -0
- package/docs/installation.md +70 -0
- package/docs/security-and-testing.md +33 -0
- package/docs/ui-screenshots.md +21 -0
- package/herdr-plugin.toml +7 -1
- package/package.json +11 -5
- package/skills/herdr-amq/SKILL.md +12 -6
- package/src/actions.mjs +149 -12
- package/src/board.mjs +144 -6
- package/src/bridge.mjs +249 -34
- package/src/config.mjs +36 -2
- package/src/fleet.mjs +282 -111
- package/src/herdr.mjs +68 -14
- package/src/index.mjs +1 -0
- package/src/protocol.mjs +315 -67
- package/src/runtime-models.mjs +118 -0
- package/src/server.mjs +81 -37
- package/src/store.mjs +219 -34
- package/src/templates.mjs +118 -0
- package/src/web/app.js +487 -105
- package/src/web/index.html +130 -11
- package/src/web/style.css +1022 -39
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "herdr-plugin-amq",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
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",
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"bin/",
|
|
12
12
|
"src/",
|
|
13
13
|
"skills/",
|
|
14
|
+
"docs/",
|
|
14
15
|
"herdr-plugin.toml",
|
|
15
16
|
"README.md",
|
|
16
17
|
"LICENSE"
|
|
@@ -19,9 +20,11 @@
|
|
|
19
20
|
"node": ">=18.0.0"
|
|
20
21
|
},
|
|
21
22
|
"scripts": {
|
|
22
|
-
"test": "node --test test/*.test.mjs",
|
|
23
|
-
"test:coverage": "mkdir -p coverage && node --test --experimental-test-coverage --test-reporter=spec --test-reporter=lcov --test-reporter-destination=stdout --test-reporter-destination=coverage/lcov.info test/*.test.mjs",
|
|
24
|
-
"
|
|
23
|
+
"test": "HERDR_DISABLE_PROMPT=1 node --test test/*.test.mjs",
|
|
24
|
+
"test:coverage": "mkdir -p coverage && HERDR_DISABLE_PROMPT=1 node --test --experimental-test-coverage --test-reporter=spec --test-reporter=lcov --test-reporter-destination=stdout --test-reporter-destination=coverage/lcov.info test/*.test.mjs",
|
|
25
|
+
"test:e2e": "HERDR_DISABLE_PROMPT=1 node --test test/e2e/*.test.mjs",
|
|
26
|
+
"test:visual": "npm run test:e2e",
|
|
27
|
+
"check": "node --check bin/herdr-amq.mjs src/*.mjs src/web/app.js test/*.test.mjs test/e2e/*.mjs",
|
|
25
28
|
"test:security": "node --test test/security.test.mjs",
|
|
26
29
|
"coverage:summary": "node tools/coverage-summary.mjs",
|
|
27
30
|
"status": "node bin/herdr-amq.mjs status",
|
|
@@ -46,5 +49,8 @@
|
|
|
46
49
|
"agmail"
|
|
47
50
|
],
|
|
48
51
|
"author": "cabra.lat",
|
|
49
|
-
"license": "MIT"
|
|
52
|
+
"license": "MIT",
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"playwright-core": "1.48.2"
|
|
55
|
+
}
|
|
50
56
|
}
|
|
@@ -12,18 +12,22 @@ metadata:
|
|
|
12
12
|
|
|
13
13
|
## Golden Rules for Agents
|
|
14
14
|
|
|
15
|
-
1. **Drain First**: Whenever awakened by a doorbell notification or starting a turn, drain your inbox
|
|
15
|
+
1. **Drain First**: Whenever awakened by a doorbell notification or starting a turn, drain your inbox and check assigned backlog tasks:
|
|
16
16
|
```bash
|
|
17
17
|
herdr-amq mail drain --me <handle> --include-body
|
|
18
|
+
herdr-amq task drain --me <handle>
|
|
18
19
|
```
|
|
19
|
-
|
|
20
|
+
Registration welcomes are status messages. Drain them, but do not reply unless the welcome explicitly requests action.
|
|
21
|
+
2. **Reply Only When Action Is Requested**: Reply on the same thread/ref chain when a message asks a question, requests action, or reports a blocker. Do not reply to terminal ACKs or informational status messages, which prevents ACK loops:
|
|
20
22
|
```bash
|
|
21
23
|
herdr-amq mail reply --me <handle> --id <msg_id> --body "..."
|
|
22
24
|
```
|
|
23
25
|
3. **Atomic Task Claiming**: Claim cards from the directory bus before modifying shared code:
|
|
24
26
|
```bash
|
|
25
|
-
herdr-amq task
|
|
27
|
+
herdr-amq task drain --me <handle>
|
|
26
28
|
herdr-amq task claim <task-id> --me <handle>
|
|
29
|
+
# Or auto-claim next in a single command:
|
|
30
|
+
herdr-amq task next --me <handle>
|
|
27
31
|
```
|
|
28
32
|
When finished, complete with proof:
|
|
29
33
|
```bash
|
|
@@ -50,11 +54,13 @@ metadata:
|
|
|
50
54
|
|
|
51
55
|
### Task Board & Bus (`herdr-amq task`)
|
|
52
56
|
|
|
53
|
-
Operates against decentralized card files in `.
|
|
57
|
+
Operates against decentralized card files in `.agent-mail/bus/{backlog,doing,blocked,done}/`:
|
|
54
58
|
|
|
55
59
|
| Command | Usage | Description |
|
|
56
60
|
|---|---|---|
|
|
57
61
|
| `list` | `herdr-amq task list [--json]` | List cards grouped by column |
|
|
62
|
+
| `drain` | `herdr-amq task drain --me <handle> [--claim]` | Drain assigned backlog cards with full description bodies, optionally claiming |
|
|
63
|
+
| `next` | `herdr-amq task next --me <handle>` | Shortcut to auto-claim and start next assigned backlog card |
|
|
58
64
|
| `claim` | `herdr-amq task claim <task-id> --me <handle>` | Atomically moves card to `doing/<task-id>.md` and updates assignee |
|
|
59
65
|
| `done` | `herdr-amq task done <task-id> --proof "<proof>"` | Moves card to `done/` with timestamp, proof, and duration |
|
|
60
66
|
| `block` | `herdr-amq task block <task-id> --reason "<reason>"` | Moves card to `blocked/` with blocker reason |
|
|
@@ -66,7 +72,7 @@ Unifies external tool agent briefs (`.opencode/agents`, `.agents`, `.pi/agents`,
|
|
|
66
72
|
| Command | Usage | Description |
|
|
67
73
|
|---|---|---|
|
|
68
74
|
| `fleet status` | `herdr-amq fleet status` | Discover personas across external tools and show live Herdr pane states |
|
|
69
|
-
| `fleet prepopulate` | `herdr-amq fleet prepopulate` | Ensure Maildirs, Git worktrees, and workspace trust exist
|
|
75
|
+
| `fleet prepopulate` | `herdr-amq fleet prepopulate` | Ensure Maildirs, Git worktrees, and workspace trust exist; welcome each newly registered agent once |
|
|
70
76
|
| `fleet up` | `herdr-amq fleet up [--kind agy\|opencode\|pi]` | Launch missing fleet agents into isolated Herdr tabs with auto-trust & clean PATH |
|
|
71
77
|
| `bootstrap` | `herdr-amq bootstrap [--kind agy]` | Instant cold-start: prepopulate + launch fleet + start bridge daemon + doorbell pass |
|
|
72
78
|
| `migrate` | `herdr-amq migrate [--dry-run]` | Migrate historical attachments into CAS blobs or pinned Git commits |
|
|
@@ -76,6 +82,6 @@ Unifies external tool agent briefs (`.opencode/agents`, `.agents`, `.pi/agents`,
|
|
|
76
82
|
| Command | Usage | Description |
|
|
77
83
|
|---|---|---|
|
|
78
84
|
| `status` | `herdr-amq status` | Show bridge daemon status, unread counts per handle, and paths |
|
|
79
|
-
| `doorbell` | `herdr-amq doorbell` | Run a single doorbell pass
|
|
85
|
+
| `doorbell` | `herdr-amq doorbell` | Run a single doorbell pass; operators may customize `.agent-mail/templates/doorbell.md` with scalar substitutions |
|
|
80
86
|
| `dashboard` | `herdr-amq dashboard [--port 8505]` | Launch web AGmail UI & REST API |
|
|
81
87
|
| `--skill` | `herdr-amq --skill [--install [dir]]` | Print or install this agentic skill definition |
|
package/src/actions.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
getStateDir,
|
|
8
8
|
getConfigDir,
|
|
9
9
|
getEventContext,
|
|
10
|
+
getPluginVersion,
|
|
10
11
|
} from "./config.mjs";
|
|
11
12
|
import {
|
|
12
13
|
isDaemonRunning,
|
|
@@ -20,6 +21,7 @@ import {
|
|
|
20
21
|
addBoardTask,
|
|
21
22
|
updateBoardTask,
|
|
22
23
|
deleteBoardTask,
|
|
24
|
+
drainTasks,
|
|
23
25
|
} from "./board.mjs";
|
|
24
26
|
import {
|
|
25
27
|
sendMaildirMessage,
|
|
@@ -31,6 +33,7 @@ import {
|
|
|
31
33
|
discoverFleetPersonas,
|
|
32
34
|
prepopulateFleet,
|
|
33
35
|
launchFleet,
|
|
36
|
+
stopFleet,
|
|
34
37
|
} from "./fleet.mjs";
|
|
35
38
|
import { getHerdrAgents } from "./herdr.mjs";
|
|
36
39
|
|
|
@@ -38,9 +41,11 @@ export function handleStatus() {
|
|
|
38
41
|
const amqRoot = findAmqRoot();
|
|
39
42
|
const pid = isDaemonRunning();
|
|
40
43
|
const handles = amqRoot ? getAgentHandles(amqRoot) : [];
|
|
44
|
+
const version = getPluginVersion();
|
|
41
45
|
|
|
42
|
-
console.log(
|
|
46
|
+
console.log(`\n📦 \x1b[1mHerdr AMQ Bridge Status\x1b[0m \x1b[2m(v${version})\x1b[0m`);
|
|
43
47
|
console.log("──────────────────────────────────────────────");
|
|
48
|
+
console.log(`Version: v${version}`);
|
|
44
49
|
console.log(`Daemon: ${pid ? `\x1b[32m● Running\x1b[0m (PID ${pid})` : "\x1b[33m○ Stopped\x1b[0m"}`);
|
|
45
50
|
console.log(`AMQ Root: ${amqRoot ? `\x1b[36m${amqRoot}\x1b[0m` : "\x1b[31mNot found\x1b[0m"}`);
|
|
46
51
|
console.log(`State Dir: ${getStateDir()}`);
|
|
@@ -109,17 +114,19 @@ export function handleDoorbell() {
|
|
|
109
114
|
process.exit(1);
|
|
110
115
|
}
|
|
111
116
|
|
|
112
|
-
|
|
113
|
-
|
|
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 });
|
|
114
120
|
|
|
115
121
|
if (!res.ok) {
|
|
116
122
|
console.error(`❌ Doorbell check failed: ${res.error}`);
|
|
117
123
|
return;
|
|
118
124
|
}
|
|
119
125
|
|
|
120
|
-
console.log(`Checked ${res.agentsChecked} agents. Doorbelled: ${res.doorbelled} message(s).`);
|
|
126
|
+
console.log(`Checked ${res.agentsChecked} agents. Doorbelled: ${res.doorbelled} message(s), ${res.doorbelledTasks || 0} task(s).`);
|
|
121
127
|
for (const r of res.results || []) {
|
|
122
|
-
|
|
128
|
+
const taskInfo = r.tasksCount ? `, ${r.tasksCount} task(s)` : "";
|
|
129
|
+
console.log(` - ${r.handle} (${r.status}): ${r.count} msg(s)${taskInfo} -> ${r.action}`);
|
|
123
130
|
}
|
|
124
131
|
}
|
|
125
132
|
|
|
@@ -140,7 +147,7 @@ export function handleAgentStatusChanged() {
|
|
|
140
147
|
if (status === "idle" || status === "done") {
|
|
141
148
|
const amqRoot = findAmqRoot();
|
|
142
149
|
if (amqRoot && handle) {
|
|
143
|
-
runDoorbellPass({ amqRoot, targetHandle: handle });
|
|
150
|
+
runDoorbellPass({ amqRoot, targetHandle: handle, allowPrompt: true, persistState: true });
|
|
144
151
|
}
|
|
145
152
|
}
|
|
146
153
|
}
|
|
@@ -380,12 +387,75 @@ export function handleTaskCommand(subcommand = "list", rawArgs = []) {
|
|
|
380
387
|
break;
|
|
381
388
|
}
|
|
382
389
|
|
|
390
|
+
case "drain": {
|
|
391
|
+
const claim = Boolean(flags.claim || flags.autoClaim);
|
|
392
|
+
const res = drainTasks(repoRoot, amqRoot, {
|
|
393
|
+
me,
|
|
394
|
+
claim,
|
|
395
|
+
notify: flags.notify !== "false",
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
if (flags.json) {
|
|
399
|
+
console.log(JSON.stringify(res, null, 2));
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
console.log(`\n📋 \x1b[1mTask Drain for ${me}\x1b[0m (${res.count} pending backlog task(s))`);
|
|
404
|
+
console.log("────────────────────────────────────────────────────────────────────────────");
|
|
405
|
+
|
|
406
|
+
if (res.activeTasks && res.activeTasks.length > 0) {
|
|
407
|
+
for (const at of res.activeTasks) {
|
|
408
|
+
console.log(`⚡ \x1b[33mActive task in progress (doing):\x1b[0m \x1b[1m${at.title}\x1b[0m (ID: ${at.id})`);
|
|
409
|
+
}
|
|
410
|
+
console.log("────────────────────────────────────────────────────────────────────────────");
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (res.tasks.length === 0) {
|
|
414
|
+
console.log(` (no pending backlog tasks assigned to ${me})`);
|
|
415
|
+
} else {
|
|
416
|
+
for (const t of res.tasks) {
|
|
417
|
+
const isClaimed = res.claimedTask && res.claimedTask.id === t.id;
|
|
418
|
+
const statusStr = isClaimed
|
|
419
|
+
? `\x1b[32m[CLAIMED -> in_progress]\x1b[0m`
|
|
420
|
+
: `\x1b[34m[backlog]\x1b[0m`;
|
|
421
|
+
|
|
422
|
+
console.log(`\n${statusStr} \x1b[1m${t.title}\x1b[0m (ID: \x1b[36m${t.id}\x1b[0m)`);
|
|
423
|
+
if (t.created) console.log(` Created: ${t.created}`);
|
|
424
|
+
if (t.description) {
|
|
425
|
+
console.log(` Details:`);
|
|
426
|
+
for (const line of t.description.split("\n")) {
|
|
427
|
+
console.log(` ${line}`);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
console.log("\n────────────────────────────────────────────────────────────────────────────");
|
|
433
|
+
if (res.claimedTask) {
|
|
434
|
+
console.log(`🚀 \x1b[32mAuto-claimed task ${res.claimedTask.id} into doing/\x1b[0m (status: in_progress)`);
|
|
435
|
+
console.log(`✉️ Notification dispatched to coordinator via AMQ.`);
|
|
436
|
+
} else {
|
|
437
|
+
console.log(`👉 \x1b[1mTo claim a task:\x1b[0m`);
|
|
438
|
+
console.log(` herdr-amq task claim ${res.tasks[0].id} --me ${me}`);
|
|
439
|
+
console.log(` or auto-claim next: herdr-amq task next --me ${me}`);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
console.log("────────────────────────────────────────────────────────────────────────────\n");
|
|
443
|
+
break;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
case "next": {
|
|
447
|
+
handleTaskCommand("drain", [...rawArgs, "--claim"]);
|
|
448
|
+
break;
|
|
449
|
+
}
|
|
450
|
+
|
|
383
451
|
default:
|
|
384
452
|
console.log(`\n📋 \x1b[1mAGboard Task Coordination CLI\x1b[0m`);
|
|
385
453
|
console.log("────────────────────────────────────────────────────────────────────────────");
|
|
386
454
|
console.log("Usage: herdr-amq task <subcommand> [options]");
|
|
387
455
|
console.log("\nCommands:");
|
|
388
456
|
console.log(" list [--owner <h>] [--status <s>] [--json] List all tasks");
|
|
457
|
+
console.log(" drain [--me <h>] [--claim] [--json] Drain backlog tasks with full descriptions");
|
|
458
|
+
console.log(" next [--me <h>] Auto-claim and start next backlog task");
|
|
389
459
|
console.log(" assign --to <h> --title <t> [--desc <d>] Assign a new task to an agent");
|
|
390
460
|
console.log(" claim <id> [--me <h>] Claim an existing task");
|
|
391
461
|
console.log(" done <id> [--me <h>] [--proof <evidence>] Complete a task with proof");
|
|
@@ -567,8 +637,15 @@ export function handleSkillCommand(args = []) {
|
|
|
567
637
|
destDir = path.resolve(process.cwd(), destDir);
|
|
568
638
|
}
|
|
569
639
|
|
|
640
|
+
let targetFile;
|
|
641
|
+
if (destDir.endsWith(".md")) {
|
|
642
|
+
targetFile = destDir;
|
|
643
|
+
destDir = path.dirname(destDir);
|
|
644
|
+
} else {
|
|
645
|
+
targetFile = path.join(destDir, "SKILL.md");
|
|
646
|
+
}
|
|
647
|
+
|
|
570
648
|
fs.mkdirSync(destDir, { recursive: true });
|
|
571
|
-
const targetFile = path.join(destDir, "SKILL.md");
|
|
572
649
|
fs.writeFileSync(targetFile, content, "utf-8");
|
|
573
650
|
console.log(`✅ Successfully installed herdr-amq skill to ${targetFile}`);
|
|
574
651
|
return targetFile;
|
|
@@ -654,12 +731,14 @@ Usage: herdr-amq fleet <command> [options]
|
|
|
654
731
|
Commands:
|
|
655
732
|
status, list Show discovered fleet personas, worktrees, and Herdr status
|
|
656
733
|
prepopulate Create AMQ maildirs and worktrees for all fleet personas
|
|
657
|
-
up Launch missing
|
|
734
|
+
up Launch missing agents and replace mismatched kinds
|
|
735
|
+
down Close fleet agent panes without removing worktrees
|
|
658
736
|
|
|
659
737
|
Options:
|
|
660
|
-
--kind <kind> Agent kind
|
|
738
|
+
--kind <kind> Agent kind (default: agy for up, required for down)
|
|
661
739
|
--agents <list> Comma-separated handles to target (default: all)
|
|
662
|
-
--
|
|
740
|
+
--no-replace Refuse to replace agents running as another kind
|
|
741
|
+
--dry-run Preview actions without changing panes
|
|
663
742
|
--help, -h Show this help message
|
|
664
743
|
`);
|
|
665
744
|
return;
|
|
@@ -675,7 +754,7 @@ Options:
|
|
|
675
754
|
for (const [handle, p] of personas.entries()) {
|
|
676
755
|
const live = liveMap.get(handle);
|
|
677
756
|
const liveBadge = live
|
|
678
|
-
? `\x1b[32m● ${live.agent_status} (${live.pane_id})\x1b[0m`
|
|
757
|
+
? `\x1b[32m● ${live.agent || "unknown"} ${live.agent_status} (${live.pane_id})\x1b[0m`
|
|
679
758
|
: `\x1b[90m○ offline\x1b[0m`;
|
|
680
759
|
const wtExists = fs.existsSync(path.join(repoRoot, ".worktrees", handle));
|
|
681
760
|
const wtBadge = wtExists ? "worktree: ok" : "\x1b[33mno worktree\x1b[0m";
|
|
@@ -696,24 +775,82 @@ Options:
|
|
|
696
775
|
return;
|
|
697
776
|
}
|
|
698
777
|
|
|
778
|
+
if (subcommand === "down") {
|
|
779
|
+
const kindIdx = rawArgs.indexOf("--kind");
|
|
780
|
+
const kind = kindIdx !== -1 && rawArgs[kindIdx + 1] ? rawArgs[kindIdx + 1] : null;
|
|
781
|
+
const agentsIdx = rawArgs.indexOf("--agents");
|
|
782
|
+
const agents = agentsIdx !== -1 && rawArgs[agentsIdx + 1] ? rawArgs[agentsIdx + 1] : null;
|
|
783
|
+
const dryRun = rawArgs.includes("--dry-run");
|
|
784
|
+
if (!kind) {
|
|
785
|
+
console.error("❌ fleet down requires --kind <agy|opencode|pi>");
|
|
786
|
+
process.exitCode = 1;
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
console.log(`\n🛑 \x1b[1mStopping Fleet via Herdr (kind: ${kind})\x1b[0m`);
|
|
791
|
+
console.log("──────────────────────────────────────────────");
|
|
792
|
+
if (dryRun) console.log("Mode: \x1b[33mDry Run (preview only)\x1b[0m\n");
|
|
793
|
+
|
|
794
|
+
const res = await stopFleet(amqRoot, repoRoot, { kind, agents, dryRun });
|
|
795
|
+
if (dryRun && res.wouldStop.length > 0) {
|
|
796
|
+
console.log(`\x1b[33m⚡ Would stop (${res.wouldStop.length}):\x1b[0m ${[...new Set(res.wouldStop.map((entry) => entry.handle))].join(", ")}`);
|
|
797
|
+
}
|
|
798
|
+
if (res.stopped.length > 0) {
|
|
799
|
+
console.log(`\x1b[32m✔ Stopped agents (${res.stopped.length}):\x1b[0m`);
|
|
800
|
+
for (const entry of res.stopped) {
|
|
801
|
+
console.log(` • ${entry.handle} -> pane ${entry.paneId} (${entry.kind})`);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
if (res.skipped.length > 0) {
|
|
805
|
+
console.log(`\x1b[33m↷ Skipped mismatched agents (${res.skipped.length}):\x1b[0m`);
|
|
806
|
+
for (const entry of res.skipped) {
|
|
807
|
+
console.log(` • ${entry.handle}: ${entry.reason}`);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
if (res.failed.length > 0) {
|
|
811
|
+
console.log(`\x1b[31m✖ Failed to stop:\x1b[0m`);
|
|
812
|
+
for (const entry of res.failed) {
|
|
813
|
+
console.log(` • ${entry.handle} (${entry.paneId}): ${entry.error}`);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
console.log("\n✅ Fleet stop pass complete. Worktrees and maildirs were preserved.\n");
|
|
817
|
+
return res;
|
|
818
|
+
}
|
|
819
|
+
|
|
699
820
|
if (subcommand === "up") {
|
|
700
821
|
const kindIdx = rawArgs.indexOf("--kind");
|
|
701
822
|
const kind = kindIdx !== -1 && rawArgs[kindIdx + 1] ? rawArgs[kindIdx + 1] : "agy";
|
|
702
823
|
const agentsIdx = rawArgs.indexOf("--agents");
|
|
703
824
|
const agents = agentsIdx !== -1 && rawArgs[agentsIdx + 1] ? rawArgs[agentsIdx + 1] : null;
|
|
704
825
|
const dryRun = rawArgs.includes("--dry-run");
|
|
826
|
+
const replace = !rawArgs.includes("--no-replace");
|
|
705
827
|
|
|
706
828
|
console.log(`\n🚀 \x1b[1mLaunching Fleet via Herdr (kind: ${kind})\x1b[0m`);
|
|
707
829
|
console.log("──────────────────────────────────────────────");
|
|
708
830
|
if (dryRun) console.log("Mode: \x1b[33mDry Run (preview only)\x1b[0m\n");
|
|
709
831
|
|
|
710
|
-
const res = await launchFleet(amqRoot, repoRoot, { kind, agents, dryRun });
|
|
832
|
+
const res = await launchFleet(amqRoot, repoRoot, { kind, agents, dryRun, replace });
|
|
711
833
|
if (res.alreadyRunning.length > 0) {
|
|
712
834
|
console.log(`\x1b[36m● Already running (${res.alreadyRunning.length}):\x1b[0m ${res.alreadyRunning.join(", ")}`);
|
|
713
835
|
}
|
|
836
|
+
if (res.replaced.length > 0) {
|
|
837
|
+
console.log(`\x1b[33m↻ Replaced mismatched agents (${res.replaced.length}):\x1b[0m`);
|
|
838
|
+
for (const entry of res.replaced) {
|
|
839
|
+
console.log(` • ${entry.handle}: ${entry.fromKinds.join(", ")} -> ${kind}`);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
if (dryRun && res.wouldReplace.length > 0) {
|
|
843
|
+
console.log(`\x1b[33m⚡ Would replace (${res.wouldReplace.length}):\x1b[0m ${res.wouldReplace.join(", ")}`);
|
|
844
|
+
}
|
|
714
845
|
if (dryRun && res.wouldLaunch.length > 0) {
|
|
715
846
|
console.log(`\x1b[33m⚡ Would launch into Herdr (${res.wouldLaunch.length}):\x1b[0m ${res.wouldLaunch.join(", ")}`);
|
|
716
847
|
}
|
|
848
|
+
if (res.blocked.length > 0) {
|
|
849
|
+
console.log(`\x1b[33m↷ Blocked mismatched agents (${res.blocked.length}):\x1b[0m`);
|
|
850
|
+
for (const entry of res.blocked) {
|
|
851
|
+
console.log(` • ${entry.handle}: ${entry.reason}`);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
717
854
|
if (res.launched.length > 0) {
|
|
718
855
|
console.log(`\x1b[32m✔ Launched agents (${res.launched.length}):\x1b[0m`);
|
|
719
856
|
for (const l of res.launched) {
|
package/src/board.mjs
CHANGED
|
@@ -29,22 +29,34 @@ export function findStatusFile(repoRoot) {
|
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* Locate the global bus directory for tasks.
|
|
32
|
-
* Defaults to .
|
|
32
|
+
* Defaults to .agent-mail/bus (lives alongside agent mailboxes and blobs).
|
|
33
|
+
* Falls back to legacy .opencode/bus if present.
|
|
33
34
|
*/
|
|
34
35
|
export function getBusDirectory(repoRoot, amqRoot) {
|
|
36
|
+
if (process.env.AMQ_BUS_DIR) {
|
|
37
|
+
return path.resolve(process.env.AMQ_BUS_DIR);
|
|
38
|
+
}
|
|
39
|
+
// 1. Primary: .agent-mail/bus if amqRoot is provided
|
|
40
|
+
if (amqRoot) {
|
|
41
|
+
const amqBus = path.join(amqRoot, "bus");
|
|
42
|
+
if (fs.existsSync(amqBus)) return amqBus;
|
|
43
|
+
}
|
|
44
|
+
// 2. Check repoRoot/.agent-mail/bus
|
|
35
45
|
if (repoRoot) {
|
|
46
|
+
const repoAmqBus = path.join(repoRoot, ".agent-mail", "bus");
|
|
47
|
+
if (fs.existsSync(repoAmqBus)) return repoAmqBus;
|
|
48
|
+
// 3. Fallback to legacy .opencode/bus if it exists
|
|
36
49
|
const opencodeBus = path.join(repoRoot, ".opencode", "bus");
|
|
37
|
-
if (fs.existsSync(opencodeBus))
|
|
38
|
-
return opencodeBus;
|
|
39
|
-
}
|
|
50
|
+
if (fs.existsSync(opencodeBus)) return opencodeBus;
|
|
40
51
|
}
|
|
52
|
+
// Default new creation target: .agent-mail/bus
|
|
41
53
|
if (amqRoot) {
|
|
42
54
|
return path.join(amqRoot, "bus");
|
|
43
55
|
}
|
|
44
56
|
if (repoRoot) {
|
|
45
|
-
return path.join(repoRoot, ".
|
|
57
|
+
return path.join(repoRoot, ".agent-mail", "bus");
|
|
46
58
|
}
|
|
47
|
-
return path.join(process.cwd(), ".
|
|
59
|
+
return path.join(process.cwd(), ".agent-mail", "bus");
|
|
48
60
|
}
|
|
49
61
|
|
|
50
62
|
export const STAGE_DIRS = {
|
|
@@ -758,3 +770,129 @@ export function deleteBoardTask(repoRoot, amqRoot, taskId) {
|
|
|
758
770
|
|
|
759
771
|
return { ok: true, taskId, deleted };
|
|
760
772
|
}
|
|
773
|
+
|
|
774
|
+
/**
|
|
775
|
+
* List pending backlog tasks assigned to a specific handle (or all backlog tasks if null/all).
|
|
776
|
+
*/
|
|
777
|
+
export function listBacklogTasks(repoRoot, amqRoot, handle = null) {
|
|
778
|
+
const busDir = getBusDirectory(repoRoot, amqRoot);
|
|
779
|
+
const backlogDir = path.join(busDir, "backlog");
|
|
780
|
+
if (!fs.existsSync(backlogDir)) return [];
|
|
781
|
+
|
|
782
|
+
try {
|
|
783
|
+
const files = fs.readdirSync(backlogDir).filter((f) => f.endsWith(".md") && !f.startsWith("."));
|
|
784
|
+
const tasks = [];
|
|
785
|
+
const target = handle && handle !== "all" ? canonicalizeOwner(handle) : null;
|
|
786
|
+
|
|
787
|
+
for (const f of files) {
|
|
788
|
+
const fullPath = path.join(backlogDir, f);
|
|
789
|
+
const parsed = parseTaskFile(fullPath, "backlog");
|
|
790
|
+
if (parsed) {
|
|
791
|
+
if (!target || parsed.owner === target) {
|
|
792
|
+
tasks.push(parsed);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
tasks.sort((a, b) => (a.created || "").localeCompare(b.created || ""));
|
|
797
|
+
return tasks;
|
|
798
|
+
} catch {
|
|
799
|
+
return [];
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* Drain tasks for an agent: returns all pending backlog tasks with full descriptions,
|
|
805
|
+
* optionally auto-claiming the first available task if claim: true.
|
|
806
|
+
*/
|
|
807
|
+
export function drainTasks(repoRoot, amqRoot, { me, claim = false, notify = true } = {}) {
|
|
808
|
+
const target = me ? canonicalizeOwner(me) : "coordinator";
|
|
809
|
+
const tasks = listBacklogTasks(repoRoot, amqRoot, target);
|
|
810
|
+
|
|
811
|
+
let claimedTask = null;
|
|
812
|
+
if (claim && tasks.length > 0) {
|
|
813
|
+
const toClaim = tasks[0];
|
|
814
|
+
const res = updateBoardTask(
|
|
815
|
+
repoRoot,
|
|
816
|
+
amqRoot,
|
|
817
|
+
toClaim.id,
|
|
818
|
+
{ status: "in_progress", owner: target },
|
|
819
|
+
{ from: target, notify }
|
|
820
|
+
);
|
|
821
|
+
if (res.ok) {
|
|
822
|
+
claimedTask = res.task;
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// Also discover any active tasks currently in doing/in_progress
|
|
827
|
+
const busDir = getBusDirectory(repoRoot, amqRoot);
|
|
828
|
+
const doingDir = path.join(busDir, resolveStageDir(busDir, "doing"));
|
|
829
|
+
const activeTasks = [];
|
|
830
|
+
if (fs.existsSync(doingDir)) {
|
|
831
|
+
try {
|
|
832
|
+
const files = fs.readdirSync(doingDir).filter((f) => f.endsWith(".md") && !f.startsWith("."));
|
|
833
|
+
for (const f of files) {
|
|
834
|
+
const fullPath = path.join(doingDir, f);
|
|
835
|
+
const parsed = parseTaskFile(fullPath, "in_progress");
|
|
836
|
+
if (parsed && parsed.owner === target) {
|
|
837
|
+
activeTasks.push(parsed);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
} catch {}
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
return {
|
|
844
|
+
ok: true,
|
|
845
|
+
owner: target,
|
|
846
|
+
count: tasks.length,
|
|
847
|
+
tasks,
|
|
848
|
+
activeTasks,
|
|
849
|
+
claimedTask,
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
/**
|
|
854
|
+
* Fast query of task numbers/statistics for an agent across stages:
|
|
855
|
+
* { backlog, doing, blocked, done, total }
|
|
856
|
+
*/
|
|
857
|
+
export function getAgentTaskStats(repoRoot, amqRoot, handle) {
|
|
858
|
+
const target = handle ? canonicalizeOwner(handle) : null;
|
|
859
|
+
const busDir = getBusDirectory(repoRoot, amqRoot);
|
|
860
|
+
const stages = [
|
|
861
|
+
{ dir: "backlog", key: "backlog" },
|
|
862
|
+
{ dir: "doing", key: "doing" },
|
|
863
|
+
{ dir: "in_progress", key: "doing" },
|
|
864
|
+
{ dir: "blocked", key: "blocked" },
|
|
865
|
+
{ dir: "done", key: "done" },
|
|
866
|
+
];
|
|
867
|
+
|
|
868
|
+
const stats = {
|
|
869
|
+
backlog: 0,
|
|
870
|
+
doing: 0,
|
|
871
|
+
blocked: 0,
|
|
872
|
+
done: 0,
|
|
873
|
+
total: 0,
|
|
874
|
+
};
|
|
875
|
+
|
|
876
|
+
const seenIds = new Set();
|
|
877
|
+
|
|
878
|
+
for (const { dir, key } of stages) {
|
|
879
|
+
const fullDir = path.join(busDir, dir);
|
|
880
|
+
if (!fs.existsSync(fullDir)) continue;
|
|
881
|
+
try {
|
|
882
|
+
const files = fs.readdirSync(fullDir).filter((f) => f.endsWith(".md") && !f.startsWith("."));
|
|
883
|
+
for (const f of files) {
|
|
884
|
+
const fullPath = path.join(fullDir, f);
|
|
885
|
+
const parsed = parseTaskFile(fullPath, key);
|
|
886
|
+
if (parsed && (!target || parsed.owner === target)) {
|
|
887
|
+
if (!seenIds.has(parsed.id)) {
|
|
888
|
+
seenIds.add(parsed.id);
|
|
889
|
+
stats[key]++;
|
|
890
|
+
stats.total++;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
} catch {}
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
return stats;
|
|
898
|
+
}
|