agent-horde 0.1.6 → 0.2.0
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 +22 -3
- package/docs/REFERENCE.md +86 -11
- package/package.json +1 -1
- package/src/args.ts +107 -47
- package/src/dashboard-data.ts +480 -49
- package/src/dashboard-html.ts +379 -92
- package/src/dashboard.ts +205 -96
- package/src/ledger/files.ts +6 -1
- package/src/member.ts +47 -0
- package/src/prompt.ts +29 -5
- package/src/run/command.ts +68 -37
- package/src/run/completions.ts +26 -14
- package/src/run/controls.ts +74 -15
- package/src/run/finalize.ts +24 -14
- package/src/run/launch.ts +179 -34
- package/src/run/setup.ts +35 -25
- package/src/run/supervise.ts +3 -1
- package/src/run/swarm-finalize.ts +121 -0
- package/src/run/swarm-report.ts +152 -0
- package/src/run/swarm.ts +156 -0
- package/src/run/synthesis.ts +141 -20
- package/src/run/types.ts +64 -5
- package/src/run/ui.ts +73 -28
- package/src/run-graph.ts +7 -7
- package/src/swarm-graph.ts +127 -0
- package/src/tool/actions.ts +61 -32
- package/src/tool/register.ts +3 -1
- package/src/widget.ts +80 -8
package/README.md
CHANGED
|
@@ -50,7 +50,9 @@ found there as its brief. Unrelated files cost members context and invite them t
|
|
|
50
50
|
from earlier runs persist in `.horde/` and will steer members; start with a fresh one when the
|
|
51
51
|
task changes.
|
|
52
52
|
|
|
53
|
-
A horde is at most twelve members
|
|
53
|
+
A horde is at most twelve members. For more hands, run a swarm: `3/12` launches three hordes of
|
|
54
|
+
twelve on disjoint workstreams of one task, each with its own ledgers, and merges their
|
|
55
|
+
deliverables at the end.
|
|
54
56
|
|
|
55
57
|
**Tips.**
|
|
56
58
|
|
|
@@ -88,7 +90,7 @@ pi install npm:agent-horde
|
|
|
88
90
|
Or from the repository, pinned to a tag:
|
|
89
91
|
|
|
90
92
|
```
|
|
91
|
-
pi install git:github.com/eliyastein/agent-horde@v0.
|
|
93
|
+
pi install git:github.com/eliyastein/agent-horde@v0.2.0
|
|
92
94
|
```
|
|
93
95
|
|
|
94
96
|
To hack on it, clone and install in place. pi loads the files from the clone, so edits take effect
|
|
@@ -135,14 +137,31 @@ extend, stress-test, or deepen the work.
|
|
|
135
137
|
/agent-horde --dashboard --until-stop 8 find every way an unauthenticated request reaches the database
|
|
136
138
|
```
|
|
137
139
|
|
|
140
|
+
A count of `H/M` runs a swarm: H hordes of M members on one task, at most four hordes. A planner
|
|
141
|
+
helper first splits the task into H disjoint workstreams with acceptance criteria and each horde
|
|
142
|
+
works only its slice, with its own ledgers under `.horde/swarms/<id>/hN/`; if the planner cannot
|
|
143
|
+
produce H distinct workstreams the launch stops and says so. `plan 3/12 a; b; c; d; e; f` skips
|
|
144
|
+
the planner and deals the layers round-robin to the hordes. Members read the other hordes' facts,
|
|
145
|
+
rule-outs and resources through the `horde` tool but write only to their own horde. The hordes
|
|
146
|
+
start a few seconds apart, run the normal lifecycle independently, and when the last one has
|
|
147
|
+
finished a synthesis helper merges their deliverables into the swarm's `DELIVERABLE.md`, each
|
|
148
|
+
claim attributed to its horde. `--until-stop` applies to every horde.
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
/agent-horde --dashboard=1234 3/12 audit the service for authorization bugs
|
|
152
|
+
```
|
|
153
|
+
|
|
138
154
|
| command | effect |
|
|
139
155
|
|---|---|
|
|
140
|
-
| `/agent-horde status` | the active run and each member |
|
|
156
|
+
| `/agent-horde status` | the active run and each member; one block per horde in a swarm |
|
|
141
157
|
| `/agent-horde stop-clean [postmortem]` | members wrap up, then the run finalizes; this is how an `--until-stop` run ends |
|
|
142
158
|
| `/agent-horde stop-hard` | terminate members now |
|
|
143
159
|
| `/agent-horde pause`, `resume` | suspend and resume member processes |
|
|
144
160
|
| `/agent-horde help` | every flag and target |
|
|
145
161
|
|
|
162
|
+
Every control takes a target: `member-3`, `h2` for one horde of a swarm, `h2:member-3` for one
|
|
163
|
+
member of one horde, or `all`; a bare `member-3` in a swarm names that member in every horde.
|
|
164
|
+
|
|
146
165
|
When the run ends, the report is injected into your conversation. Everything else stays under
|
|
147
166
|
`.horde/`; add it to your `.gitignore`.
|
|
148
167
|
|
package/docs/REFERENCE.md
CHANGED
|
@@ -16,8 +16,10 @@ at one problem beat a single linear one: research, audits, design exploration, R
|
|
|
16
16
|
/agent-horde help
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
`
|
|
20
|
-
member
|
|
19
|
+
`count` is `M` members (1 to 12) or `H/M`: H hordes of M members, H at most 4; a plain `M` is a
|
|
20
|
+
swarm of one. `target` is `all` (default), `member-N`, `N`, `hN` (one horde of a swarm),
|
|
21
|
+
`hN:member-M`, `hN:M`, or a comma or space separated list; a bare member names that member in
|
|
22
|
+
every horde. Subcommands and targets tab-complete.
|
|
21
23
|
|
|
22
24
|
The command returns immediately. A widget above the editor shows each member's status, current
|
|
23
25
|
tool, elapsed time, turns, and resets. Members inherit the parent session's model and thinking
|
|
@@ -76,9 +78,19 @@ Every page has a JSON twin under `/api/<same path>`. The footer of every page na
|
|
|
76
78
|
agent-horde version from its package.json and, when it runs from a git checkout, the commit it is
|
|
77
79
|
at (`-dirty` when the tree has uncommitted changes), linked to the commit on GitHub.
|
|
78
80
|
|
|
81
|
+
Every page belongs to one scope, fixed by its URL: the **project** (`/` and the bare ledger paths,
|
|
82
|
+
the ledgers single runs share), one **run** (`/runs/<id>/...`), or one **swarm**
|
|
83
|
+
(`/swarms/<id>/...`). Under the `runs` link a scope line names the scope, and a page row offers that
|
|
84
|
+
scope's pages (overview, the six ledgers, collaboration, graph; the project has the ledgers only),
|
|
85
|
+
with the current page marked. Inside a swarm the scope line adds horde tabs, `all | h1 | h2 ...`,
|
|
86
|
+
that keep the page type: from h2's findings, h3 opens h3's findings and `all` the swarm's. A horde's
|
|
87
|
+
pages are its run's pages, `/runs/<swarm-id>-h2/...`. No link falls back to another run. While a swarm
|
|
88
|
+
is running or finishing, project pages show a pointer to the same page in the swarm, since its
|
|
89
|
+
hordes write their own ledgers and leave the project ones untouched.
|
|
90
|
+
|
|
79
91
|
| page | shows |
|
|
80
92
|
|------|-------|
|
|
81
|
-
| `/` | every run: active
|
|
93
|
+
| `/` | every swarm and run: state (active, finalizing, pending, finished, not started), start, count, model, outcome, prompt, files present |
|
|
82
94
|
| `/runs/<id>` | prompt, outcome, canary, clean-stop countdown, plan layers, members table, run files |
|
|
83
95
|
| `/runs/<id>/members/<m>` | live activity, last and final text, log tail (`?tail=<n>`), transcript link |
|
|
84
96
|
| `/runs/<id>/members/<m>/log` | the member log; `tail=0` is the whole file (1 MiB cap) |
|
|
@@ -88,16 +100,27 @@ at (`-dirty` when the tree has uncommitted changes), linked to the commit on Git
|
|
|
88
100
|
| `/runs/<id>/collaboration` | messages, review scope, artifact acceptance, resources, event audit |
|
|
89
101
|
| `/runs/<id>/graph` | the run as a swimlane: one lane per member plus the parent, phase bands, every event a link to its ledger entry (`?w=<px>`, `?from=<s>&to=<s>`); the axis is real time until the last member exits, then the synthesis phases share a compressed tail |
|
|
90
102
|
| `/runs/<id>/graph.mmd` | the same run as a Mermaid flowchart with absolute dashboard links (text/plain) |
|
|
91
|
-
| `/ideas`,
|
|
103
|
+
| `/runs/<id>/ideas`, `board`, `findings`, `claims`, `facts`, `resources` | the run's ledgers: a horde's own, or for a single run the project ledgers with board and findings narrowed to the run and facts split into this run and carried; `?view=clusters`, `?owner=<m>`, `?order=oldest` |
|
|
104
|
+
| `/ideas`, `/board`, `/findings`, `/claims`, `/facts`, `/resources` | the project ledgers across every single run; `/resources` also lists who cited each resource in a live single run, as REPORT.md counts reuse |
|
|
105
|
+
| `/swarms/<id>` | one row per horde (state, outcome, members, findings, accepted artifacts, cost, workstream), the partition, the merge state, swarm files |
|
|
106
|
+
| `/swarms/<id>/ideas`, `board`, `findings`, `claims`, `facts`, `resources`, `collaboration` | every horde's ledger, or its acceptance, requests and reviews, on one page under a per-horde heading; ids repeat per horde, and each horde's event audit stays on its run page |
|
|
107
|
+
| `/swarms/<id>/graph` | one lane per horde with its phase marks, and the merge step |
|
|
108
|
+
| `/swarms/<id>/files/<NAME>` | `SWARM`, `REPORT`, `DELIVERABLE`; 404 until written |
|
|
92
109
|
| `/export` | confirms the export and starts the download of `/export.zip` (`?back=<path>` names the page to return to); no JSON twin |
|
|
93
110
|
| `/export.zip` | the whole dashboard as static HTML (`application/zip`, streamed, no JSON twin); see below |
|
|
94
111
|
|
|
112
|
+
A page auto-refreshes while what it shows can change: a run's pages while the run is live or
|
|
113
|
+
finalizing, a swarm's while the swarm is in memory, the project ledgers while a single run is
|
|
114
|
+
live, and the index always. A swarm and its hordes show as `pending` from the moment their directories
|
|
115
|
+
are allocated until the hordes have a TASK.md, which covers the minutes the planner takes, and as
|
|
116
|
+
`not started` when the planner could not partition the task.
|
|
117
|
+
|
|
95
118
|
The download icon in the top-right corner of every page opens a warning that the export may
|
|
96
119
|
leak sensitive data from this machine. Its download link goes to `/export`, which closes the
|
|
97
120
|
warning, explains the wait, and starts the download of `/export.zip`. The zip holds one
|
|
98
121
|
folder, `horde-dashboard-<timestamp>/`, whose `index.html` opens the dashboard from disk: every
|
|
99
122
|
run, member, transcript page and entry, log, run document, collaboration page, graph (each width,
|
|
100
|
-
plus the Mermaid file), and the ledgers
|
|
123
|
+
plus the Mermaid file), and the ledgers of every scope, with links rewritten relative to
|
|
101
124
|
each other. Exported pages do not refresh, expandable entries use the browser's own disclosure,
|
|
102
125
|
finding bodies are shown inline, transcripts are paged fifty entries at a time, and the export
|
|
103
126
|
control itself is absent. Pages that fail to load are exported as their error page, so no link
|
|
@@ -134,11 +157,17 @@ the session files, so they show end cause, turns, and usage but no activity.
|
|
|
134
157
|
SUMMARY.md REPORT.md per-member status and usage; the reconciled run report
|
|
135
158
|
ANSWER.md DELIVERABLE.md POSTMORTEM.md solved answer; synthesis output; reviewer critique
|
|
136
159
|
GRAPH.md Mermaid graph of the run, written last at finalize
|
|
160
|
+
swarms/<swarm-id>/
|
|
161
|
+
SWARM.md SWARM.json prompt, partition, horde list; the swarm's timeline for the dashboard
|
|
162
|
+
planner-sessions/ deliverable-sessions/ planner and merge helper sessions
|
|
163
|
+
REPORT.md DELIVERABLE.md per-horde totals and file list; the merged deliverable
|
|
164
|
+
h1/ h2/ ... one complete horde directory per horde: its own ledgers,
|
|
165
|
+
.lock and runs/<swarm-id>-hN/ with everything a run has
|
|
137
166
|
```
|
|
138
167
|
|
|
139
168
|
Each run appends a `## Run <id>` heading to BOARD.md and FINDINGS.md that cites `TASK.md` by
|
|
140
|
-
sha256. `runs/`
|
|
141
|
-
the ledgers out of git too.
|
|
169
|
+
sha256. `runs/` and `swarms/` are gitignored inside `.horde/`; add `.horde/` to the project
|
|
170
|
+
`.gitignore` to keep the ledgers out of git too.
|
|
142
171
|
|
|
143
172
|
## The `horde` tool
|
|
144
173
|
|
|
@@ -263,6 +292,46 @@ Crash and stall resets apply as usual. `stop-clean` and `stop-hard` end the run
|
|
|
263
292
|
`stop-clean` produces the report, deliverable, and postmortem; the last banked answer stands unless
|
|
264
293
|
a later proposal was validated too.
|
|
265
294
|
|
|
295
|
+
## Horde of hordes
|
|
296
|
+
|
|
297
|
+
`H/M` launches a swarm: H concurrent hordes of M members on one task, H at most 4. A plain `M` is
|
|
298
|
+
a swarm of one and keeps the layout above; a larger swarm gets `.horde/swarms/<swarm-id>/` with one
|
|
299
|
+
complete horde directory per horde (`h1/`, `h2/`, ...), each with its own ledgers, lock and
|
|
300
|
+
`runs/<swarm-id>-hN/`. Hordes therefore never share a lock, a dedup pass or a coverage watermark,
|
|
301
|
+
and run ids stay unique across the swarm.
|
|
302
|
+
|
|
303
|
+
**Partition.** A plain prompt is split before launch by a read-only planner helper (`pi -p`, the
|
|
304
|
+
parent's model, `PI_HORDE_PLANNER_MS` timeout) into exactly H disjoint workstreams with one
|
|
305
|
+
acceptance criterion each, recorded in `SWARM.md` and in each horde's `TASK.md`; every member of a
|
|
306
|
+
horde is told its workstream and to work only on it. If the planner cannot produce H distinct
|
|
307
|
+
workstreams the launch stops with that message, the swarm directory keeps the planner session and
|
|
308
|
+
a `SWARM.md` saying why, and the slot stays free. `plan H/M a; b; c; ...` skips the planner: the
|
|
309
|
+
layers are dealt round-robin to the hordes (layer 1 to h1, layer 2 to h2, ...) and each horde runs
|
|
310
|
+
its share as its own plan, so there must be at least H layers.
|
|
311
|
+
|
|
312
|
+
**Sharing.** Shared read, local write. Each member's system prompt lists the sibling horde
|
|
313
|
+
directories; `horde facts` and `horde resources` append every sibling's entries tagged `[hN]`, and
|
|
314
|
+
`horde list` appends their done and ruled-out ideas, so a fact, a rule-out or a lab another horde
|
|
315
|
+
recorded is not re-derived. Open sibling ideas are not shown and nothing is written to a sibling:
|
|
316
|
+
the lock, dedup and coverage rules keep their per-horde scope. Members reuse a sibling's resource
|
|
317
|
+
by its path, not its R#.
|
|
318
|
+
|
|
319
|
+
**Lifecycle.** The hordes start `PI_HORDE_SWARM_STAGGER_MS` (default 5000) apart so H canaries do
|
|
320
|
+
not hit the provider at once; each then runs the lifecycle above unchanged, with its own canary,
|
|
321
|
+
resets, repair, acceptance, deliverable and post-mortem. `--until-stop` applies to every horde.
|
|
322
|
+
Controls take horde targets: `stop-clean h2` winds one horde down, `pause h2:member-3` one member,
|
|
323
|
+
`stop-hard` every horde, and `status` prints one block per horde. The swarm slot frees once every
|
|
324
|
+
horde has written its REPORT.md; after the last horde's finalize a synthesis helper reads the
|
|
325
|
+
hordes' DELIVERABLE.md, REPORT.md and ACCEPTANCE.md and writes the swarm `DELIVERABLE.md`, keeping
|
|
326
|
+
each claim attributed to its horde and to that horde's acceptance label, and the swarm `REPORT.md`
|
|
327
|
+
with per-horde members, turns, tokens, cost, findings and outcome. One swarm report is injected into
|
|
328
|
+
the parent conversation instead of one per horde. No horde reviews another's work; a verification
|
|
329
|
+
pass over the merged deliverable is not built.
|
|
330
|
+
|
|
331
|
+
**Dashboard and widget.** Each swarm has its own scope in the dashboard, with horde tabs and
|
|
332
|
+
swarm-wide ledger, collaboration and graph pages; see [Dashboard](#dashboard). The widget shows one
|
|
333
|
+
line per horde.
|
|
334
|
+
|
|
266
335
|
## Finding metadata
|
|
267
336
|
|
|
268
337
|
`importance` (critical, high, medium, low, info), `confidence` (high, medium, low), and
|
|
@@ -295,6 +364,8 @@ carries a one-line diagnosis and, when generated, the post-mortem text.
|
|
|
295
364
|
| `PI_HORDE_CANARY_MS` | 60000 | time for member-1 to produce output before a retry |
|
|
296
365
|
| `PI_HORDE_STALL_MS` | 1800000 | reset a member with no ledger write for this long; `0` disables |
|
|
297
366
|
| `PI_HORDE_RESET_CAP` | 2 | resets per member |
|
|
367
|
+
| `PI_HORDE_SWARM_STAGGER_MS` | 5000 | pause between one horde's launch and the next in a swarm |
|
|
368
|
+
| `PI_HORDE_PLANNER_MS` | 300000 | planner helper timeout when a swarm partitions a plain prompt |
|
|
298
369
|
| `PI_HORDE_ACTIVE_GRACE_MS` | 15000 | one clean-stop extension for a member still producing output |
|
|
299
370
|
| `PI_HORDE_SOLVED_CONFIRM_MS` | 120000 | wait for a second member to validate a solve |
|
|
300
371
|
| `PI_HORDE_SOLVED_STOP_MS` | 30000 | extra wind-down window for a solve landing at the deadline |
|
|
@@ -337,6 +408,7 @@ build-info.ts the running package's version and checkout commit, for the
|
|
|
337
408
|
collaboration-report.ts acceptance checks, frozen snapshots, collaboration metrics
|
|
338
409
|
run-graph.ts events.jsonl projected into lanes, phases, nodes, and edges with dashboard links
|
|
339
410
|
run-graph-svg.ts the swimlane SVG and its legend; run-graph-mermaid.ts the Mermaid export and GRAPH.md
|
|
411
|
+
swarm-graph.ts a swarm's hordes as lanes of lifecycle marks, plus the merge step
|
|
340
412
|
|
|
341
413
|
ledger/ one module per .horde/ document, all writes under the .horde lock
|
|
342
414
|
files.ts paths, init, atomic writes, shared entry formatting
|
|
@@ -361,10 +433,13 @@ tool/ the member-side `horde` tool
|
|
|
361
433
|
resource-contract.ts resource generations, readiness, leases, and port coordination
|
|
362
434
|
|
|
363
435
|
run/ the parent-side run lifecycle, sharing one HordeState
|
|
364
|
-
types.ts HordeRun, HordeState, ReportDetails
|
|
365
|
-
command.ts /agent-horde dispatch
|
|
436
|
+
types.ts HordeRun, Swarm, HordeState, ReportDetails
|
|
437
|
+
command.ts /agent-horde dispatch, horde-prefixed targets, session shutdown
|
|
366
438
|
setup.ts run directory, TASK.md, roles, plan seeding
|
|
367
|
-
|
|
439
|
+
swarm.ts swarm directories and ids, dealing plan layers, planner output parsing
|
|
440
|
+
swarm-finalize.ts the swarm slot, the merge step, SWARM.json
|
|
441
|
+
swarm-report.ts per-horde totals: the swarm REPORT.md and the injected message
|
|
442
|
+
launch.ts dashboard, deadline, partition, staggered canary preflights, fan-out
|
|
368
443
|
supervise.ts member spawn, reset on exit, stall detection
|
|
369
444
|
schedule.ts artifact review assignment and superseded-dependency handoffs
|
|
370
445
|
repair.ts bounded post-discovery reviews, owner repair, and recheck attempts
|
|
@@ -372,7 +447,7 @@ run/ the parent-side run lifecycle, sharing one HordeState
|
|
|
372
447
|
controls.ts stop-hard, pause and resume, clean stop and its deadline
|
|
373
448
|
finalize.ts reconcile, cleanups, SUMMARY.md and REPORT.md, report message, GRAPH.md
|
|
374
449
|
lifecycle.ts parent lifecycle events in events.jsonl
|
|
375
|
-
synthesis.ts deliverable
|
|
450
|
+
synthesis.ts deliverable, post-mortem, swarm merge and planner helper agents
|
|
376
451
|
diagnosis.ts one-line run diagnosis
|
|
377
452
|
ui.ts widget mount, status line, report renderer, notifications
|
|
378
453
|
completions.ts command argument completions
|
package/package.json
CHANGED
package/src/args.ts
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
export const MAX_MEMBERS = 12;
|
|
2
2
|
|
|
3
|
+
/** Hordes per swarm; `H/M` launches H hordes of M members. */
|
|
4
|
+
export const MAX_HORDES = 4;
|
|
5
|
+
|
|
3
6
|
export const USAGE = [
|
|
4
7
|
"usage: /agent-horde [--dashboard[=<port>]] [--no-verifier] [--until-stop] <count> <prompt>",
|
|
5
|
-
" spawn <count> members with the same prompt",
|
|
8
|
+
" spawn <count> members with the same prompt;",
|
|
9
|
+
" <count> may be H/M: H hordes of M members, the",
|
|
10
|
+
" task partitioned between the hordes by a planner",
|
|
6
11
|
" /agent-horde status [target] show the active run",
|
|
7
12
|
" /agent-horde stop-hard [target] terminate members now (SIGTERM, then SIGKILL)",
|
|
8
13
|
" /agent-horde stop-clean [target] let members wrap up, then reconcile their ideas",
|
|
@@ -11,11 +16,13 @@ export const USAGE = [
|
|
|
11
16
|
" /agent-horde pause [target] suspend members (SIGSTOP)",
|
|
12
17
|
" /agent-horde resume [target] resume suspended members (SIGCONT)",
|
|
13
18
|
` /agent-horde plan [--dashboard] <count> <a; b; ...>`,
|
|
14
|
-
" partition members across workstreams",
|
|
19
|
+
" partition members across workstreams; with H/M",
|
|
20
|
+
" the layers go round-robin to the hordes",
|
|
15
21
|
" /agent-horde help show this text",
|
|
16
22
|
"",
|
|
17
|
-
"target is 'all' (default), 'member-N', 'N',
|
|
18
|
-
|
|
23
|
+
"target is 'all' (default), 'member-N', 'N', 'hN' (one horde), 'hN:member-M', 'hN:M', or a",
|
|
24
|
+
"comma/space separated list; a bare member names that member in every horde.",
|
|
25
|
+
`count is a whole number from 1 to ${MAX_MEMBERS}, or H/M with H from 1 to ${MAX_HORDES}`,
|
|
19
26
|
"--dashboard serves a read-only localhost dashboard (default port 7433, or",
|
|
20
27
|
"PI_HORDE_DASHBOARD_PORT); --dashboard=<port> picks the port.",
|
|
21
28
|
"--no-verifier skips the default verifier role that runs of 3 or more members assign",
|
|
@@ -25,10 +32,18 @@ export const USAGE = [
|
|
|
25
32
|
"session told to add new ideas.",
|
|
26
33
|
].join("\n");
|
|
27
34
|
|
|
28
|
-
/**
|
|
35
|
+
/** One token of a control target: a horde, a member in every horde, or a member of one horde. */
|
|
36
|
+
export interface TargetItem {
|
|
37
|
+
/** 1-based horde index (`h2`); absent for every horde. */
|
|
38
|
+
horde?: number;
|
|
39
|
+
/** `member-N`; absent for every member of the horde. */
|
|
40
|
+
member?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Which hordes and members a control command applies to. */
|
|
29
44
|
export type HordeTarget =
|
|
30
45
|
| { kind: "all" }
|
|
31
|
-
| { kind: "
|
|
46
|
+
| { kind: "selection"; items: TargetItem[] };
|
|
32
47
|
|
|
33
48
|
type HordeStopMode = "hard" | "clean";
|
|
34
49
|
|
|
@@ -46,6 +61,9 @@ type HordeArgs =
|
|
|
46
61
|
| { kind: "resume"; target: HordeTarget }
|
|
47
62
|
| {
|
|
48
63
|
kind: "run";
|
|
64
|
+
/** Concurrent hordes; `12` is one horde, `3/12` three. */
|
|
65
|
+
hordes: number;
|
|
66
|
+
/** Members per horde. */
|
|
49
67
|
count: number;
|
|
50
68
|
prompt: string;
|
|
51
69
|
plan?: string[] | undefined;
|
|
@@ -58,22 +76,51 @@ type HordeArgs =
|
|
|
58
76
|
|
|
59
77
|
export type HordeRunArgs = Extract<HordeArgs, { kind: "run" }>;
|
|
60
78
|
|
|
79
|
+
const HORDE_TOKEN = /^h(\d+)$/i;
|
|
80
|
+
const MEMBER_TOKEN = /^(?:member-)?(\d+)$/i;
|
|
81
|
+
|
|
82
|
+
function memberName(token: string): string | undefined {
|
|
83
|
+
const match = MEMBER_TOKEN.exec(token);
|
|
84
|
+
if (!match) return undefined;
|
|
85
|
+
const index = Number(match[1]);
|
|
86
|
+
return index >= 1 && index <= MAX_MEMBERS ? `member-${index}` : undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function hordeIndex(token: string): number | undefined {
|
|
90
|
+
const match = HORDE_TOKEN.exec(token);
|
|
91
|
+
if (!match) return undefined;
|
|
92
|
+
const index = Number(match[1]);
|
|
93
|
+
return index >= 1 && index <= MAX_HORDES ? index : undefined;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** `h2`, `h2:member-3`, `h2:3`, `member-3`, or `3`; undefined for anything else. */
|
|
97
|
+
function parseTargetItem(token: string): TargetItem | undefined {
|
|
98
|
+
const colon = token.indexOf(":");
|
|
99
|
+
if (colon === -1) {
|
|
100
|
+
const horde = hordeIndex(token);
|
|
101
|
+
if (horde !== undefined) return { horde };
|
|
102
|
+
const member = memberName(token);
|
|
103
|
+
return member === undefined ? undefined : { member };
|
|
104
|
+
}
|
|
105
|
+
const horde = hordeIndex(token.slice(0, colon));
|
|
106
|
+
const member = memberName(token.slice(colon + 1));
|
|
107
|
+
return horde === undefined || member === undefined ? undefined : { horde, member };
|
|
108
|
+
}
|
|
109
|
+
|
|
61
110
|
function parseTargets(raw: string): HordeTarget | undefined {
|
|
62
111
|
const trimmed = raw.trim();
|
|
63
112
|
if (trimmed === "") return { kind: "all" };
|
|
64
|
-
const
|
|
113
|
+
const items: TargetItem[] = [];
|
|
65
114
|
for (const token of trimmed.split(/[\s,]+/).filter(Boolean)) {
|
|
66
115
|
if (token.toLowerCase() === "all") return { kind: "all" };
|
|
67
|
-
const
|
|
68
|
-
if (!
|
|
69
|
-
|
|
70
|
-
if (index < 1 || index > MAX_MEMBERS) return undefined;
|
|
71
|
-
names.push(`member-${index}`);
|
|
116
|
+
const item = parseTargetItem(token);
|
|
117
|
+
if (!item) return undefined;
|
|
118
|
+
items.push(item);
|
|
72
119
|
}
|
|
73
|
-
return
|
|
120
|
+
return items.length > 0 ? { kind: "selection", items } : { kind: "all" };
|
|
74
121
|
}
|
|
75
122
|
|
|
76
|
-
const TARGET_ERROR = "invalid target; use all, member-N, or
|
|
123
|
+
const TARGET_ERROR = "invalid target; use all, member-N, N, hN, or hN:member-M";
|
|
77
124
|
|
|
78
125
|
function parseStopRest(
|
|
79
126
|
rest: string,
|
|
@@ -166,24 +213,31 @@ function mergeLaunchFlags(leading: LaunchFlags, trailing: LaunchFlags): LaunchFl
|
|
|
166
213
|
};
|
|
167
214
|
}
|
|
168
215
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
216
|
+
const COUNT_ERROR =
|
|
217
|
+
`count must be a whole number from 1 to ${MAX_MEMBERS}, or H/M with H from 1 to ${MAX_HORDES} hordes`;
|
|
218
|
+
|
|
219
|
+
interface CountedRun {
|
|
220
|
+
hordes: number;
|
|
221
|
+
count: number;
|
|
222
|
+
rest: string;
|
|
223
|
+
flags: LaunchFlags;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Parse `<count> [flags] <tail>`, where count is `M` or `H/M`. */
|
|
227
|
+
function parseCountedRun(text: string, leading: LaunchFlags): CountedRun | { error: string } {
|
|
228
|
+
const match = /^(?:(\d+)\/)?(\d+)(?:\s+([\s\S]*))?$/.exec(text.trim());
|
|
229
|
+
if (!match) return { error: COUNT_ERROR };
|
|
230
|
+
const hordes = match[1] === undefined ? 1 : Number(match[1]);
|
|
231
|
+
const count = Number(match[2]);
|
|
232
|
+
if (count < 1 || count > MAX_MEMBERS || hordes < 1 || hordes > MAX_HORDES) return { error: COUNT_ERROR };
|
|
233
|
+
const flags = mergeLaunchFlags(leading, takeLaunchFlags(match[3] ?? ""));
|
|
181
234
|
if (flags.error) return { error: flags.error };
|
|
182
|
-
return { count, rest: flags.rest, flags };
|
|
235
|
+
return { hordes, count, rest: flags.rest, flags };
|
|
183
236
|
}
|
|
184
237
|
|
|
185
|
-
function runArgs(
|
|
186
|
-
const
|
|
238
|
+
function runArgs(parsed: CountedRun, prompt: string, plan?: string[]): HordeRunArgs {
|
|
239
|
+
const { hordes, count, flags } = parsed;
|
|
240
|
+
const args: HordeRunArgs = { kind: "run", hordes, count, prompt };
|
|
187
241
|
if (plan) args.plan = plan;
|
|
188
242
|
if (flags.dashboard) args.dashboard = flags.dashboard;
|
|
189
243
|
if (flags.noVerifier) args.noVerifier = true;
|
|
@@ -191,6 +245,26 @@ function runArgs(count: number, prompt: string, flags: LaunchFlags, plan?: strin
|
|
|
191
245
|
return args;
|
|
192
246
|
}
|
|
193
247
|
|
|
248
|
+
function parsePlan(rest: string, leading: LaunchFlags): HordeArgs {
|
|
249
|
+
const afterPlan = takeLaunchFlags(rest);
|
|
250
|
+
if (afterPlan.error) return { kind: "usage", error: afterPlan.error };
|
|
251
|
+
const parsed = parseCountedRun(afterPlan.rest, mergeLaunchFlags(leading, afterPlan));
|
|
252
|
+
if ("error" in parsed) {
|
|
253
|
+
return { kind: "usage", error: "plan usage: /agent-horde plan <count> <layer1; layer2; ...>" };
|
|
254
|
+
}
|
|
255
|
+
const layers = parsed.rest
|
|
256
|
+
.split(/[;\n]/)
|
|
257
|
+
.map((layer) => layer.trim())
|
|
258
|
+
.filter(Boolean);
|
|
259
|
+
if (layers.length < 2) {
|
|
260
|
+
return { kind: "usage", error: "plan needs at least 2 layers separated by ';'" };
|
|
261
|
+
}
|
|
262
|
+
if (layers.length < parsed.hordes) {
|
|
263
|
+
return { kind: "usage", error: `plan with ${parsed.hordes} hordes needs at least ${parsed.hordes} layers` };
|
|
264
|
+
}
|
|
265
|
+
return runArgs(parsed, layers.join("; "), layers);
|
|
266
|
+
}
|
|
267
|
+
|
|
194
268
|
export function parseHordeArgs(raw: string): HordeArgs {
|
|
195
269
|
const leading = takeLaunchFlags(raw);
|
|
196
270
|
if (leading.error) return { kind: "usage", error: leading.error };
|
|
@@ -203,7 +277,7 @@ export function parseHordeArgs(raw: string): HordeArgs {
|
|
|
203
277
|
const space = trimmed.search(/\s/);
|
|
204
278
|
const head = (space === -1 ? trimmed : trimmed.slice(0, space)).toLowerCase();
|
|
205
279
|
const rest = space === -1 ? "" : trimmed.slice(space + 1);
|
|
206
|
-
if (hasFlags && head !== "plan" && !/^\d
|
|
280
|
+
if (hasFlags && head !== "plan" && !/^\d+(?:\/\d+)?$/.test(head)) {
|
|
207
281
|
return { kind: "usage", error: "launch flags only apply when starting a run" };
|
|
208
282
|
}
|
|
209
283
|
|
|
@@ -232,22 +306,8 @@ export function parseHordeArgs(raw: string): HordeArgs {
|
|
|
232
306
|
const target = parseTargets(rest);
|
|
233
307
|
return target ? { kind: "pause", target } : { kind: "usage", error: TARGET_ERROR };
|
|
234
308
|
}
|
|
235
|
-
case "plan":
|
|
236
|
-
|
|
237
|
-
if (afterPlan.error) return { kind: "usage", error: afterPlan.error };
|
|
238
|
-
const parsed = parseCountedRun(afterPlan.rest, mergeLaunchFlags(leading, afterPlan));
|
|
239
|
-
if ("error" in parsed) {
|
|
240
|
-
return { kind: "usage", error: "plan usage: /agent-horde plan <count> <layer1; layer2; ...>" };
|
|
241
|
-
}
|
|
242
|
-
const layers = parsed.rest
|
|
243
|
-
.split(/[;\n]/)
|
|
244
|
-
.map((layer) => layer.trim())
|
|
245
|
-
.filter(Boolean);
|
|
246
|
-
if (layers.length < 2) {
|
|
247
|
-
return { kind: "usage", error: "plan needs at least 2 layers separated by ';'" };
|
|
248
|
-
}
|
|
249
|
-
return runArgs(parsed.count, layers.join("; "), parsed.flags, layers);
|
|
250
|
-
}
|
|
309
|
+
case "plan":
|
|
310
|
+
return parsePlan(rest, leading);
|
|
251
311
|
case "resume": {
|
|
252
312
|
const target = parseTargets(rest);
|
|
253
313
|
return target ? { kind: "resume", target } : { kind: "usage", error: TARGET_ERROR };
|
|
@@ -257,5 +317,5 @@ export function parseHordeArgs(raw: string): HordeArgs {
|
|
|
257
317
|
const parsed = parseCountedRun(trimmed, leading);
|
|
258
318
|
if ("error" in parsed) return { kind: "usage", error: parsed.error };
|
|
259
319
|
if (parsed.rest === "") return { kind: "usage", error: "prompt is required" };
|
|
260
|
-
return runArgs(parsed
|
|
320
|
+
return runArgs(parsed, parsed.rest);
|
|
261
321
|
}
|