@getpipher/armory-fleet 0.11.1 → 0.12.1

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.
Files changed (40) hide show
  1. package/README.md +371 -14
  2. package/package.json +1 -1
  3. package/src/engine/concurrency-lock.ts +20 -15
  4. package/src/engine/spawnSubagent.ts +57 -4
  5. package/src/index.ts +114 -7
  6. package/src/panel/fleet-panel.ts +177 -11
  7. package/src/panel/fleet-widget.ts +5 -1
  8. package/src/runtime/reconcile.ts +32 -11
  9. package/src/todo-sync/adapter.ts +22 -3
  10. package/src/tools/fleet.ts +179 -0
  11. package/src/workflows/builtin/adversarial-review.js +19 -0
  12. package/src/workflows/builtin/code-review.js +13 -0
  13. package/src/workflows/builtin/codebase-audit.js +16 -0
  14. package/src/workflows/builtin/deep-research.js +12 -0
  15. package/src/workflows/builtin/multi-perspective.js +17 -0
  16. package/src/workflows/helpers/checkpoint.ts +15 -0
  17. package/src/workflows/helpers/completeness-check.ts +18 -0
  18. package/src/workflows/helpers/gate.ts +22 -0
  19. package/src/workflows/helpers/index.ts +8 -0
  20. package/src/workflows/helpers/judge-panel.ts +33 -0
  21. package/src/workflows/helpers/loop-until-dry.ts +21 -0
  22. package/src/workflows/helpers/retry.ts +17 -0
  23. package/src/workflows/helpers/types.ts +19 -0
  24. package/src/workflows/helpers/verify.ts +27 -0
  25. package/src/workflows/journal.ts +76 -0
  26. package/src/workflows/keyword.ts +22 -0
  27. package/src/workflows/panel/workflows-items.ts +150 -0
  28. package/src/workflows/panel/workflows-rows.ts +3 -0
  29. package/src/workflows/panel-host.ts +179 -0
  30. package/src/workflows/registry.ts +68 -0
  31. package/src/workflows/runner.ts +507 -0
  32. package/src/workflows/runtime/adapters.ts +182 -0
  33. package/src/workflows/runtime/controller.ts +493 -0
  34. package/src/workflows/runtime/hydrate.ts +116 -0
  35. package/src/workflows/runtime/pause-gate.ts +41 -0
  36. package/src/workflows/runtime/run-store.ts +31 -0
  37. package/src/workflows/runtime/save.ts +111 -0
  38. package/src/workflows/runtime/types.ts +78 -0
  39. package/src/workflows/source.ts +156 -0
  40. package/src/workflows/vm-realm.ts +106 -0
package/README.md CHANGED
@@ -1,31 +1,388 @@
1
+ <div align="center">
2
+
3
+ <img src="assets/armory-fleet-logo.png" alt="@getpipher/armory-fleet" width="220" height="220" />
4
+
1
5
  # @getpipher/armory-fleet
2
6
 
3
- > The armory suite's subagent orchestrator for the [pi coding agent](https://github.com/earendil-works/pi-coding-agent) — a cross-harness, superpowers-native fleet where every agent is **armory-native from birth**.
7
+ **The armory suite's subagent orchestrator for the [pi coding agent](https://github.com/earendil-works/pi-coding-agent) — a cross-harness, superpowers-native fleet where every agent is armory-native from birth.**
8
+
9
+ [![npm version](https://img.shields.io/npm/v/@getpipher/armory-fleet?color=cb3837&logo=npm&label=npm)](https://www.npmjs.com/package/@getpipher/armory-fleet)
10
+ [![npm downloads](https://img.shields.io/npm/dm/@getpipher/armory-fleet?color=cb3837&logo=npm)](https://www.npmjs.com/package/@getpipher/armory-fleet)
11
+ [![pi compatibility](https://img.shields.io/badge/pi-%5E0.81.1-6f42c1?logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHRleHQgeD0iNCIgeT0iMTgiIGZvbnQtc2l6ZT0iMTYiIGZpbGw9IiNmZmYiPuKCrTwvdGV4dD48L3N2Zz4=)](https://github.com/earendil-works/pi-coding-agent)
12
+ [![license](https://img.shields.io/github/license/getpipher/armory-fleet?color=blue)](./LICENSE)
13
+ [![tests](https://img.shields.io/badge/tests-593%20passing-21c463?logo=jest)](#testing)
14
+ [![release](https://img.shields.io/github/v/release/getpipher/armory-fleet?color=success&label=latest%20release)](https://github.com/getpipher/armory-fleet/releases)
15
+ [![platform](https://img.shields.io/badge/platform-macOS%20%7C%20Linux%20%7C%20WSL-lightgrey)](#compatibility)
4
16
 
5
- `@getpipher/armory-fleet` brings Claude-Code-style autonomous sub-agents to pi, built so that the armory ecosystem ([armory-todo](https://github.com/getpipher/armory-todo), [armory-memory](https://github.com/getpipher/armory-memory), [vision](https://github.com/getpipher/vision), [cursor](https://github.com/getpipher/cursor)) is the *default substrate* agents run on — not a bolt-on.
17
+ [Why](#why) · [Features](#features-at-a-glance) · [Quick start](#quick-start) · [Architecture](#architecture) · [The fleet panel](#the-fleet-panel) · [Workflows-as-code](#workflows-as-code) · [Cost-aware tiers](#cost-aware-tiers) · [Roadmap](#roadmap) · [Ecosystem](#ecosystem)
18
+
19
+ </div>
20
+
21
+ ---
6
22
 
7
23
  ## Why
8
24
 
9
- The pi-subagent ecosystem is crowded (nicobailon/pi-subagents at 2688⭐, tintinweb at 702⭐, QuintinShaw at 287⭐, and more). Three structural gaps remain open:
25
+ The pi-subagent ecosystem is crowded — `nicobailon/pi-subagents` (2688⭐), `tintinweb/pi-subagents` (702⭐), `QuintinShaw/pi-dynamic-workflows` (287⭐), `kky42/pi-flow` (66⭐), `teelicht/pi-superagents` (54⭐). Three structural gaps remain open, and `armory-fleet` owns all three — then reaches parity on the rest to be the best pi-subagent package in the ecosystem.
26
+
27
+ | Gap | Status quo | armory-fleet |
28
+ |---|---|---|
29
+ | **Armory-native integration** | No external package can integrate with the armory suite; they don't own it. | Agents that sync to [armory-todo](https://github.com/getpipher/armory-todo), hydrate from [armory-memory](https://github.com/getpipher/armory-memory), see via [vision](https://github.com/getpipher/vision), and edit via [cursor](https://github.com/getpipher/cursor) **by default** — uncopyable. |
30
+ | **Cross-harness peers** | Only one early attempt runs Claude Code + Pi as peer backends, foreground-only. | First-class dual-arsenal topology — pi and Claude Code spawn as sibling backends from one fleet. |
31
+ | **Superpowers-native lifecycle** | Only one attempt wraps the superpowers skill pipeline, synchronous-only. | The full superpowers lifecycle (brainstorm → plan → implement → review → finish) with checkpoints, quality gates, and lifecycle hooks baked in. |
32
+
33
+ Beyond those: a fleet TUI, cron/interval scheduling, git worktree isolation, cost accounting, quality gates, workflows-as-code, and a journaled event-bus — all in one package.
34
+
35
+ > **Vision, spine, and the 7-SPEC roadmap** live in [`PRD.md`](./PRD.md). The landscape deep-read (11+ packages mapped, 5 contenders deep-read) is in [`research/`](./research).
36
+
37
+ ---
38
+
39
+ ## Features at a glance
40
+
41
+ | Capability | What you get |
42
+ |---|---|
43
+ | 🧬 **Armory-native agents** | Every child syncs to `armory-todo`, hydrates from `armory-memory`, sees via `vision`, edits via `cursor` — by default, from birth. No bolt-on. |
44
+ | 🏛️ **Cross-harness backends** | Spawn pi **and** Claude Code sessions as peer backends from one fleet. Auto-detect Claude; hook-parity keeps both on equal footing. |
45
+ | 🦸 **Superpowers lifecycle** | `brainstorm → plan → implement → review → finish`, checkpoint-driven, skill-loaded per phase. `/fleet-implement <task>` runs the whole pipeline. |
46
+ | 🎚️ **Cost-aware tiers** | `economy` / `standard` / `frontier` model tiers with cost caps + context floors. Route cheap work to cheap models, escalate when it matters. Live cost $ + context % per run. |
47
+ | 🚦 **Quality gates** | `verification-before-completion`, `completeness-check`, `gate`, `verify` — built-in. Register your own. Composite helpers: `judgePanel`, `loopUntilDry`, `retry`. |
48
+ | 🧩 **Workflows-as-code** | Author multi-phase workflows in a JS DSL with `agent()`, `pipeline()`, `phase()`, `checkpoint()`. 5 builtins ship: adversarial-review, code-review, codebase-audit, deep-research, multi-perspective. Journaled + resumable. |
49
+ | 🖥️ **Fleet TUI** | `/fleet` opens an interactive panel: Runs, Tiers, Lifecycle, Workflows, Conversation viewer. Live widget, mid-run Steer/Stop, edit-resume, save-as. |
50
+ | ⏱️ **Scheduling** | Cron expressions, intervals, one-shot ISO datetimes. PID-locked scheduler, session-scoped (no catch-up). Background runs on isolated git worktrees. |
51
+ | 🔒 **Worktree isolation** | Background runs get isolated git worktrees (in-place fallback for non-git cwds). Foreground runs share the session cwd. |
52
+ | 📒 **RunLog + journaling** | Every run is journaled; interrupted workflows recover on restart. A results inbox lets the model pull completed background runs. |
53
+ | 🔄 **Edit-and-resume** | Re-run a workflow by replaying the unchanged prefix and re-running only the edited suffix. |
54
+ | 📡 **Vision built-in** | `describe_image` tool is wired into child sessions — agents can see screenshots and diagrams without leaving the fleet. |
55
+
56
+ ---
57
+
58
+ ## Quick start
59
+
60
+ ### Install
61
+
62
+ armory-fleet is a [pi extension](https://pi-coding-agent.dev/docs/extensions) — it loads inside pi, no build step.
63
+
64
+ ```bash
65
+ # 1. Add to your pi packages (~/local-dev/arsenal or your package dir)
66
+ pnpm add @getpipher/armory-fleet
67
+
68
+ # 2. Register in ~/.pi/agent/settings.json
69
+ ```
70
+
71
+ ```jsonc
72
+ // ~/.pi/agent/settings.json
73
+ {
74
+ "packages": [
75
+ "@getpipher/armory-fleet@0.12.0"
76
+ // + its armory siblings: armory-todo, armory-memory, vision, cursor
77
+ ]
78
+ }
79
+ ```
80
+
81
+ ```bash
82
+ # 3. Reload pi (/reload) and open the panel
83
+ pi
84
+ # inside pi → /fleet
85
+ ```
86
+
87
+ ### Your first subagent (model-callable tool)
88
+
89
+ The `subagent` tool is what the model calls to delegate a focused task. Every child is armory-native by default.
90
+
91
+ ```ts
92
+ // the agent calls this — not you
93
+ subagent({
94
+ agent: "general-purpose",
95
+ task: "Audit src/auth/ for token-handling bugs; report findings.",
96
+ // optional: model, lifecycle, todoId, background, isolation, schedule, maxTurns
97
+ });
98
+ ```
99
+
100
+ | Param | Effect |
101
+ |---|---|
102
+ | `agent` | Agent definition to spawn (from `agents/` or discovered). |
103
+ | `task` | The prompt handed to the child. |
104
+ | `model` | Override the session model. **Tip:** omit to inherit the session model, or use `Ollama/...` when the session is on Ollama — don't cross providers. |
105
+ | `lifecycle` | Run the task through a superpowers lifecycle (e.g. `default`) instead of a single delegate. |
106
+ | `todoId` | Link the run to an existing armory-todo entry. |
107
+ | `track` | Default `true` (syncs to armory-todo). Pass `false` only for throwaway lookups. |
108
+ | `background` | Fire without awaiting — run goes to the async pool on an isolated git worktree. |
109
+ | `isolation` | `worktree` (default for bg in a git repo) · `none` (in-place) · `auto`. |
110
+ | `schedule` | Cron (`0 9 * * 1-5`), interval (`30m`), or one-shot ISO datetime. Session-scoped, no catch-up. |
111
+ | `maxTurns` | Per-run turn budget (default 20). Raise for complex multi-step tasks. |
112
+
113
+ ### Your first workflow
114
+
115
+ Workflows are plain JS files evaluated in a sandboxed vm realm. The orchestration primitives — `agent`, `parallel`, `pipeline`, `phase`, `gate`, `judgePanel`, `loopUntilDry`, `retry`, `checkpoint`, `verify`, `workflow`, `log` — are **injected globals** (no imports). The only thing you `export` is `meta`.
116
+
117
+ ```js
118
+ // ship-feature.js — drop into a workflows/ dir discovered by WorkflowRegistry
119
+ export const meta = {
120
+ name: 'ship-feature',
121
+ description: 'Plan → implement → 3 parallel review angles with a gate',
122
+ phases: [{ title: 'Plan' }, { title: 'Implement' }, { title: 'Review' }],
123
+ }
124
+
125
+ phase('Plan')
126
+ const plan = await agent('Plan this feature: ' + args.task, { tier: 'low' })
127
+
128
+ phase('Implement')
129
+ const impl = await agent(`Implement the plan:\n${plan}`, { tier: 'medium' })
130
+
131
+ phase('Review')
132
+ const angles = ['security', 'performance', 'correctness']
133
+ const reviews = await parallel(
134
+ angles.map((a) => () => agent(`Review the implementation for ${a} issues.`, { tier: 'low' })),
135
+ )
136
+
137
+ // gate: revise the synthesis until it passes a validator
138
+ const synthesis = await gate(
139
+ async (_feedback, n) => n === 0
140
+ ? agent(`Synthesize ${reviews.length} reviews.`, { tier: 'low' })
141
+ : agent('Revise synthesis per feedback.', { tier: 'low' }),
142
+ (v) => typeof v === 'string' && v.length > 200 ? { ok: true } : { ok: false, feedback: 'more detail' },
143
+ { attempts: 3 },
144
+ )
145
+
146
+ return { plan, impl, reviews, synthesis }
147
+ ```
148
+
149
+ Open `/fleet → Workflows`, pick `ship-feature`, run it. The panel shows live phase progress; mid-run you can Steer (inject a message) or Stop. The realm also exposes `args`, `cwd`, and a `budget` object (`{ total, spent(), remaining() }`) so workflows can self-limit.
150
+
151
+ ### Your first lifecycle run
152
+
153
+ ```text
154
+ /fleet-implement Refactor the auth module to use the new session API --auto
155
+ ```
156
+
157
+ Runs `brainstorm → plan → implement → review → finish` autonomously. Drop `--auto` for checkpointed mode (pauses at each checkpoint; continue/revise/abort from `/fleet → Lifecycle`).
158
+
159
+ ---
160
+
161
+ ## Architecture
162
+
163
+ ```
164
+ ┌─────────────────────────────────────────────┐
165
+ │ pi host session │
166
+ │ (loads @getpipher/armory-fleet extension) │
167
+ └───────────────────────┬─────────────────────┘
168
+
169
+ ┌───────────────────────────────────────┼───────────────────────────────────────┐
170
+ ▼ ▼ ▼
171
+ subagent tool fleet tool /fleet panel
172
+ (model-callable) (workflow runner) (FleetView TUI)
173
+ │ │ │
174
+ ▼ ▼ ▼
175
+ createAgentSession() WorkflowController Runs · Tiers · Lifecycle
176
+ (pi SDK child) + ConcurrencyPool Workflows · Conversation
177
+ │ + adapters + live widget
178
+ ├─→ armory-todo sync + journal/resume
179
+ ├─→ armory-memory hydrate │
180
+ ├─→ vision (describe_image) ▼
181
+ ├─→ lifecycle + gates backend registry
182
+ └─→ tier routing (pi | Claude Code)
183
+ ```
184
+
185
+ ### Core engine
186
+
187
+ - **Engine primitive:** `createAgentSession()` from the pi SDK — child Pi sessions, in-memory or file-backed `SessionManager`, `ResourceLoader`. Each child is wrapped to emit `session_init` on subscribe so the fleet can track it from the first event.
188
+ - **Child loader:** `buildChildLoader()` threads armory-todo, armory-memory, and vision into every child's resource + tool set — armory-native from birth, cwd-agnostic.
189
+ - **Concurrency:** a single-slot lock for foreground runs + a `ConcurrencyPool` for parallel workflow branches.
190
+ - **Turn budget:** `engine/turn-budget.ts` caps each child's run; the `subagent` tool surfaces exhaustion as a structured status (not a silent truncation).
191
+
192
+ ### Armory integration (the uncopyable layer)
193
+
194
+ | Sibling | What the fleet wires in | Where |
195
+ |---|---|---|
196
+ | [armory-todo](https://github.com/getpipher/armory-todo) | Every run syncs to the cross-session TODO store. Pass `todoId` to link. | `src/todo-sync/` |
197
+ | [armory-memory](https://github.com/getpipher/armory-memory) | Children hydrate project memory on spawn. Shared port, cwd-agnostic. | `src/memory-hydrate/` |
198
+ | [vision](https://github.com/getpipher/vision) | `describe_image` tool is wired into child sessions. | `src/vision/` |
199
+ | [cursor](https://github.com/getpipher/cursor) | Children edit through the cursor extension when present. | (via child loader) |
200
+
201
+ ### Cross-harness backends
202
+
203
+ `src/backend/` ships a backend registry with **pi** (default) and **Claude Code** as peer backends. `detectClaude()` auto-discovers Claude; `PI_HOOK_PARITY` / `CLAUDE_HOOK_PARITY` tables keep both backends on equal footing. `hook-parity.ts` normalizes lifecycle/event hooks across harnesses. A `ResumeStore` persists backend session IDs so cross-harness runs can resume.
204
+
205
+ ### Superpowers lifecycle
10
206
 
11
- 1. **Armory-native integration** no external package can integrate with the armory suite; they don't own it. Agents that sync to armory-todo, hydrate from armory-memory, see via vision, and edit via cursor *by default* are uncopyable.
12
- 2. **Cross-harness peers** — only one attempt (kky42/pi-flow, 66⭐) runs Claude Code + Pi as peer subagent backends, and it's early. A dual-arsenal topology deserves a first-class implementation.
13
- 3. **Superpowers-native lifecycle** — only one attempt (teelicht/pi-superagents, 54⭐) wraps the superpowers skill pipeline, and it's synchronous-only.
207
+ The default lifecycle (`src/lifecycle/default.ts`) is the superpowers-native 5-phase pipeline:
14
208
 
15
- `armory-fleet` owns all three, then reaches parity on the rest (fleet TUI, scheduling, worktree isolation, cost accounting, quality patterns, workflows-as-code, event-bus/RPC) — to be the best pi-subagent package in the ecosystem.
209
+ | Phase | Skills loaded | Checkpoint? | Gates |
210
+ |---|---|---|---|
211
+ | `brainstorm` | `brainstorming` | ✅ | — |
212
+ | `plan` | `writing-plans` | ✅ | `completenessCheck` |
213
+ | `implement` | `executing-plans`, `test-driven-development`, `verification-before-completion` | ❌ | `verification-before-completion`, `completenessCheck`, `gate` |
214
+ | `review` | `requesting-code-review`, `receiving-code-review` | ✅ | — |
215
+ | `finish` | `finishing-a-development-branch` | — | — |
16
216
 
17
- ## Status
217
+ Custom lifecycles: drop a YAML file in your `lifecycles/` dir, register via `discoverLifecycles()`. Gates are registered on a `GateRegistry` (`fleet-register-gate` command for runtime extensibility).
18
218
 
19
- 🚧 **Pre-implementation.** The master PRD + 7-SPEC roadmap are written and self-reviewed; SPEC-1 (core engine + armory-todo sync) is the next brainstorm cycle.
219
+ ### Quality gates
20
220
 
21
- - **Master PRD:** [`PRD.md`](./PRD.md)
22
- - **Landscape research:** [`research/`](./research) (11+ packages mapped, 5 contenders deep-read)
23
- - **Roadmap:** PRD → SPEC-1 (core+todo-sync) SPEC-2 (deep armory) → SPEC-3 (cross-harness) → SPEC-4 (superpowers lifecycle) → SPEC-5a (ops) → SPEC-5b (fleet TUI) → SPEC-6 (power-user → v1.0)
221
+ Built-in (`src/lifecycle/gates/`): `verification-before-completion`, `completeness-check`, `gate`, `verify`.
222
+
223
+ Composite helpers (`src/workflows/helpers/`) usable from any workflow:
224
+
225
+ | Helper | What it does |
226
+ |---|---|
227
+ | `judgePanel` | Run N judge agents; majority/weighted verdict. |
228
+ | `loopUntilDry` | Re-run an agent until a dry-run gate passes. |
229
+ | `retry` | Retry an agent with backoff on failure. |
230
+ | `checkpoint` | Pause a workflow for human review. |
231
+ | `completeness-check` / `gate` / `verify` | Gate wrappers for workflow use. |
232
+
233
+ ### Cost-aware tiers
234
+
235
+ `src/tiers/` ships three built-in tiers:
236
+
237
+ | Tier | Models | Cost cap | Context floor |
238
+ |---|---|---|---|
239
+ | `economy` | `Ollama/minimax-m3:cloud` | — | — |
240
+ | `standard` | `Ollama/glm-5.2:cloud`, `Ollama/minimax-m3:cloud` | — | — |
241
+ | `frontier` | `anthropic/claude-sonnet-4`, `Ollama/glm-5.2:cloud` | $5 | 200k ctx |
242
+
243
+ Live cost $ and context % are tracked per run and surfaced in the Tiers view. Override per-run with `model`, or let the tier registry route based on the task class.
244
+
245
+ ### Operational runtime
246
+
247
+ `src/runtime/` — the async/scheduling spine:
248
+
249
+ - `async-runner.ts` — background dispatch (fire-and-forget).
250
+ - `run-journal.ts` + `run-log.ts` — durable run records; `reconcile.ts` reattaches orphaned runs on restart.
251
+ - `concurrency-pool.ts` — bounded parallel branches.
252
+ - `results-inbox.ts` — the model pulls completed background runs via the `fleet_results` tool.
253
+ - `resume.ts` — scan for resumable runs + workflows.
254
+
255
+ ### Scheduling + worktree
256
+
257
+ `src/scheduling/` — cron expressions (`expressions.ts`), a `Scheduler` with PID-locking (`pid-lock.ts`), session-scoped (no catch-up). `src/worktree/` — `WorktreeService` for isolated bg-run worktrees + `DiffService` for reviewable diffs.
258
+
259
+ ---
260
+
261
+ ## The fleet panel
262
+
263
+ `/fleet` opens an interactive TUI panel (TUI-only; in non-interactive modes use the `subagent` tool).
264
+
265
+ | View | What it shows |
266
+ |---|---|
267
+ | **Runs** | Running + recent subagents; status, cost, context %, agent, model. Action submenu: Steer, Stop, View conversation. |
268
+ | **Tiers** | Per-tier model lists, cost caps, context floors. Configure routing. |
269
+ | **Lifecycle** | Active lifecycle runs; Continue/Revise/Abort at checkpoints. |
270
+ | **Workflows** | Registered workflows + live runs. Run, edit-resume, save-as, view result, checkpoint. |
271
+ | **Conversation** | The full message timeline for any selected run. |
272
+
273
+ A live `FleetWidget` can render in the pi footer/overlay for at-a-glance fleet status while you work.
274
+
275
+ ### Slash commands
276
+
277
+ | Command | Purpose |
278
+ |---|---|
279
+ | `/fleet` | Open the interactive fleet panel (TUI). |
280
+ | `/fleet-implement <task> [--lifecycle <name>] [--auto]` | Run a task through the superpowers lifecycle. |
281
+ | `/fleet-register-gate` | Register a custom gate on the fleet gate registry (extensibility). |
282
+
283
+ ### Model-callable tools
284
+
285
+ | Tool | Purpose |
286
+ |---|---|
287
+ | `subagent` | Delegate a focused task to a child agent (sync foreground or async background). |
288
+ | `fleet` | Run + control fleet workflows (JS orchestration: `agent`, `pipeline`, `phase`, checkpoints). |
289
+ | `fleet_results` | Pull completed background run results from the inbox. |
290
+
291
+ ---
292
+
293
+ ## Workflows-as-code
294
+
295
+ Workflows are authored in a JS DSL (`src/workflows/source.ts` parses; `vm-realm.ts` evaluates). 5 builtins ship in `src/workflows/builtin/`:
296
+
297
+ | Workflow | Description | Phases |
298
+ |---|---|---|
299
+ | `adversarial-review` | Red-team + blue-team review with judge panel | Attack → Defend → Judge |
300
+ | `code-review` | 7 parallel review angles plus verification | Review → Verify |
301
+ | `codebase-audit` | File-tree scan with completeness check | Scan → Audit |
302
+ | `deep-research` | 3-round discovery loop with de-duplication | Discover → Synthesize |
303
+ | `multi-perspective` | 4 personas review the same artifact | Review → Merge |
304
+
305
+ Every workflow run is **journaled** (`workflows/journal.ts`) and **resumable**. `edit-resume` replays the unchanged prefix from cache and re-runs only the edited suffix. `runtime/controller.ts` orchestrates; `runtime/pause-gate.ts` handles checkpoints; `runtime/adapters.ts` binds the controller to the fleet's spawn + accounting.
306
+
307
+ ---
308
+
309
+ ## Roadmap
310
+
311
+ armory-fleet follows a PRD → SPEC-N (brainstorm → spec → plan → implementation) pipeline. **16/16 phases done through v0.12.0.**
312
+
313
+ | SPEC | Headline | Status | Artifact |
314
+ |---|---|---|---|
315
+ | PRD | Master PRD | ✅ done | `PRD.md` |
316
+ | RESEARCH | Landscape research (11+ packages, 5 deep-reads) | ✅ done | `research/` |
317
+ | SPEC-1 | Core engine + armory-todo sync | ✅ done | PR #1 · `547319b` |
318
+ | SPEC-2 | Deep armory integration (memory/vision/cursor) | ✅ done · @0.2.0 | PR #2 · `c6e727c` |
319
+ | SPEC-3 | Cross-harness peers (pi + Claude Code) | ✅ done · @0.3.0 | PR #4 · `5bb75fb` |
320
+ | SPEC-4 | Superpowers-native lifecycle | ✅ done · @0.4.0 | PR #5 · `67ff9b4` |
321
+ | SPEC-5a | Operational runtime (async/scheduling/worktree) | ✅ done · @0.5.2 | PR #6 · `52e3477` |
322
+ | SPEC-5b-1 | RunLog seam + Runs view | ✅ done · @0.6.0 | PR #7 · `54b1b10` |
323
+ | SPEC-5b-2 | Live widget + FleetView + Q9 | ✅ done · @0.7.0 | PR #8 · `9266a7` |
324
+ | SPEC-5b-3 | Conversation viewer + timeline fix | ✅ done · @0.8.0 | PR #9 · `adc0034` |
325
+ | SPEC-5b-4 | Mid-run steering (Steer) + Stop | ✅ done · @0.9.1 | PR #10 + #11 + #12 |
326
+ | SPEC-6-1 | Cost-aware tiers + cost $ + context % + Tiers view | ✅ done · @0.10.x | PR #15/#16/#17 |
327
+ | SPEC-6-2 | Quality gates + lifecycle hooks | ✅ done · @0.11.0 | PR #18 · `cda5e2b` |
328
+ | v0.11.1 | bg dispatch isolation split (non-git cwd fix) | ✅ done · @0.11.1 | PR #19 · `51956e0` |
329
+ | **SPEC-6-3** | **Workflows-as-code (release-gate completion)** | ✅ done · @0.12.0 | PR #21 · `9986ad1` |
330
+ | SPEC-6-4 | Event-bus RPC + live conversation viewer → v1.0 | 🚧 next | — |
331
+
332
+ See the [full release history](https://github.com/getpipher/armory-fleet/releases) and the [PRD](./PRD.md) §8 for the roadmap rationale.
333
+
334
+ ---
335
+
336
+ ## Ecosystem
337
+
338
+ armory-fleet is the orchestrator in the [getpipher](https://github.com/getpipher) armory suite — the default substrate it runs agents on:
339
+
340
+ | Package | Role |
341
+ |---|---|
342
+ | [armory-todo](https://github.com/getpipher/armory-todo) | Global cross-session TODO store (the fleet syncs every run to it). |
343
+ | [armory-memory](https://github.com/getpipher/armory-memory) | Project memory hydration for child agents. |
344
+ | [vision](https://github.com/getpipher/vision) | The `describe_image` tool, wired into fleet children. |
345
+ | [cursor](https://github.com/getpipher/cursor) | Custom editor component for the pi TUI. |
346
+
347
+ ---
348
+
349
+ ## Conventions
350
+
351
+ - **No build step** — extensions ship raw `.ts` via tsx at pi runtime. `pnpm typecheck` + `pnpm test:run` before release.
352
+ - **Tests** — `node:test` via tsx in `test/*.test.mts`, importing from `../src/...`. 593 passing.
353
+ - **Publish** — CI on `v*` tags using the getpipher `NPM_TOKEN` org secret (`release.yml` mirrors armory-todo: idempotent npm publish + GitHub Release).
354
+ - **Interactive-first UX** — every capability lands as a `/fleet` panel tab/view + action submenu first, then the model-callable tool action.
355
+
356
+ <a id="testing"></a>
357
+ ### Verify locally
358
+
359
+ ```bash
360
+ pnpm install
361
+ pnpm typecheck
362
+ pnpm test:run --test-timeout=30000 # 593/593
363
+ ```
364
+
365
+ ### Release-gate smoke (mandatory before any release)
366
+
367
+ ```bash
368
+ pi --no-extensions -e ./src/index.ts --no-session --approve
369
+ # inside: /fleet → Workflows → verify the 5 builtins render + a workflow runs end-to-end
370
+ ```
371
+
372
+ ---
24
373
 
25
374
  ## Compatibility
26
375
 
27
- pi `^0.81.1`.
376
+ - pi `^0.81.1`
377
+ - Node `>=22` (tsx runtime)
378
+ - Platform: macOS, Linux, WSL
28
379
 
29
380
  ## License
30
381
 
31
- MIT — see [LICENSE](./LICENSE).
382
+ MIT — see [LICENSE](./LICENSE). © RECTOR ([@rz1989s](https://github.com/rz1989s)).
383
+
384
+ <div align="center">
385
+
386
+ Built with Ihsan · Maintained by [RECTOR](https://github.com/rz1989s) · [getpipher](https://github.com/getpipher)
387
+
388
+ </div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getpipher/armory-fleet",
3
- "version": "0.11.1",
3
+ "version": "0.12.1",
4
4
  "private": false,
5
5
  "description": "The armory suite's subagent orchestrator for the pi coding agent — a cross-harness, superpowers-native fleet where every agent is armory-native from birth.",
6
6
  "license": "MIT",
@@ -6,19 +6,24 @@ export interface SingleSlotLock {
6
6
  current(): string | null;
7
7
  }
8
8
 
9
+ export class SingleSlotLockImpl implements SingleSlotLock {
10
+ private holding: string | null = null;
11
+ tryAcquire(id: string): boolean {
12
+ if (this.holding !== null) return false;
13
+ this.holding = id;
14
+ return true;
15
+ }
16
+ release(): void {
17
+ this.holding = null;
18
+ }
19
+ current(): string | null {
20
+ return this.holding;
21
+ }
22
+ }
23
+
24
+ /** Alias so tests can `new SingleSlotLock()`. */
25
+ export const SingleSlotLock = SingleSlotLockImpl;
26
+
9
27
  export function createSingleSlotLock(): SingleSlotLock {
10
- let holding: string | null = null;
11
- return {
12
- tryAcquire(id): boolean {
13
- if (holding !== null) return false;
14
- holding = id;
15
- return true;
16
- },
17
- release(): void {
18
- holding = null;
19
- },
20
- current(): string | null {
21
- return holding;
22
- },
23
- };
24
- }
28
+ return new SingleSlotLock();
29
+ }
@@ -140,6 +140,8 @@ export interface SpawnOptions {
140
140
  tierRegistry?: TierRegistry;
141
141
  /** SPEC-6-1: model catalog for contextFloor filtering. Optional — absent means no catalog filtering. */
142
142
  modelRegistry?: ModelRegistryLike;
143
+ /** SPEC-6-3: workflow adapter tier override — replaces agent.tier before model resolution. */
144
+ tierOverride?: string;
143
145
  }
144
146
 
145
147
  export interface SpawnResult {
@@ -195,8 +197,12 @@ export async function spawnSubagent(opts: SpawnOptions): Promise<SpawnResult> {
195
197
  const childAgent = opts.skillsOverride ? { ...agentDef, skills: opts.skillsOverride } : agentDef;
196
198
 
197
199
  // SPEC-6-1: resolve model via tier registry (Q4 precedence + Q5 contextFloor/catalog filter).
200
+ // SPEC-6-3: workflow tierOverride replaces agent.tier before resolution.
201
+ const effectiveAgent = opts.tierOverride
202
+ ? { ...agentDef, tier: opts.tierOverride }
203
+ : agentDef;
198
204
  const resolved = resolveAgentModel(
199
- agentDef, opts.model, opts.parentModel,
205
+ effectiveAgent, opts.model, opts.parentModel,
200
206
  opts.tierRegistry ?? new TierRegistry({ tiers: [], agents: new Map() }),
201
207
  opts.modelRegistry ?? { find: () => undefined },
202
208
  );
@@ -276,6 +282,10 @@ export async function spawnSubagent(opts: SpawnOptions): Promise<SpawnResult> {
276
282
  let costTotal = 0;
277
283
  let contextTokens = 0;
278
284
  let turnIdx = -1;
285
+ // #26/#22: declared before subscribe() because some child sessions emit events
286
+ // synchronously inside subscribe() (temporal-dead-zone guard).
287
+ let modelError: string | undefined; // model-call failure surfaced via stopReason "error"
288
+ let sawAssistantMessage = false; // #22: did the child emit any assistant message_end at all?
279
289
 
280
290
  const onSignalAbort = (): void => { aborted = true; void session.abort(); };
281
291
  opts.signal?.addEventListener("abort", onSignalAbort);
@@ -291,8 +301,18 @@ export async function spawnSubagent(opts: SpawnOptions): Promise<SpawnResult> {
291
301
  } else if (e.type === "turn_end") {
292
302
  if (budget.consume()) void session.abort();
293
303
  } else if (e.type === "message_end" && e.message?.role === "assistant") {
304
+ sawAssistantMessage = true;
294
305
  const text = e.message.content?.map((c) => (c.type === "text" ? c.text ?? "" : "")).join("") ?? "";
295
- if (text) finalText = text;
306
+ // #26/#22: a model-call failure (401, provider down, rate limit) surfaces as
307
+ // stopReason "error". The SDK retries internally; if it still ends with an error
308
+ // stopReason, capture it so the run is marked failed (not completed-with-empty) —
309
+ // the controller gets an actionable error instead of "(no tool output)".
310
+ const stopReason = (e.message as { stopReason?: string }).stopReason;
311
+ if (stopReason === "error") {
312
+ modelError = text || `model call ended with stopReason 'error' (provider/auth failure or rate limit) for model '${model}'`;
313
+ } else {
314
+ if (text) finalText = text;
315
+ }
296
316
  // SPEC-5b-2 (Q9): accumulate REAL tokens (input+output+cacheRead+cacheWrite), not cost.total (dollars).
297
317
  const u = e.message.usage;
298
318
  const turnTokens = (u?.input ?? 0) + (u?.output ?? 0) + (u?.cacheRead ?? 0) + (u?.cacheWrite ?? 0);
@@ -337,11 +357,44 @@ export async function spawnSubagent(opts: SpawnOptions): Promise<SpawnResult> {
337
357
  status = "aborted";
338
358
  error = tier?.costCap && costTotal > tier.costCap ? `budget_exceeded (cost $${costTotal.toFixed(4)} > cap $${tier.costCap})` : "aborted by user";
339
359
  } else if (budget.count() >= maxTurns) {
360
+ // #25: surface a coherent partial, not a mid-sentence 200-char cut. The controller reads
361
+ // `res.error` (the tool surfaces error, not finalText, for failed runs), so the partial must
362
+ // live here. 4000 chars (~600 tokens) is enough for any structured summary the model emitted;
363
+ // a truncation marker names the run log for the full output. (The wind-down nudge — injecting
364
+ // a "you have ~N turns left, emit your partial now" message before the hard cut — is a
365
+ // future enhancement tracked in #25; it needs mid-loop injection semantics.)
366
+ const PARTIAL_WINDOW = 4000;
367
+ const partial = finalText.length > PARTIAL_WINDOW
368
+ ? finalText.slice(0, PARTIAL_WINDOW) + "\n…(partial truncated — see run log for full output)"
369
+ : finalText;
340
370
  status = "failed";
341
- error = `hit turn budget (${maxTurns}) mid-task; partial result: ${finalText.slice(0, 200)}`;
371
+ error = `hit turn budget (${maxTurns}) mid-task; partial result:\n${partial}`;
342
372
  } else if (runError) {
343
373
  status = "failed";
344
374
  error = runError;
375
+ } else if (modelError) {
376
+ // #26: a 401/provider/rate-limit failure that the SDK surfaced via stopReason "error"
377
+ // after exhausting retries. Without this, the run fell through to `completed` with an
378
+ // empty finalText — the controller saw "(no tool output)" and couldn't tell a broken
379
+ // model from a no-op run.
380
+ //
381
+ // Precedence note (PR #30 review): a late error-stop overrides a prior successful turn.
382
+ // If turn 1 set finalText (valid output) and turn 2 hit stopReason "error", the run is
383
+ // marked failed with the error — the run IS incomplete, and the error is more actionable
384
+ // to the controller than a partial result. finalText is preserved (not cleared) so
385
+ // finishRun + the run log still carry the partial; only the surfaced status is failed.
386
+ // Gating this on `!finalText` (only fail if no prior output) is a future design call, not
387
+ // this fix — the current "last error wins" is the defensible default.
388
+ status = "failed";
389
+ error = modelError;
390
+ } else if (!sawAssistantMessage) {
391
+ // #22: prompt() resolved cleanly but the child produced NO assistant message_end at all.
392
+ // A real agent loop always emits at least one assistant message; zero means a silent
393
+ // failure (provider hung, empty response, premature exit). Treat as a structured
394
+ // EMPTY_RESULT so orchestration can escalate models or retry, rather than silently
395
+ // succeeding with empty output the controller can't distinguish from a no-op.
396
+ status = "failed";
397
+ error = `EMPTY_RESULT: child session produced no assistant output for model '${model}' (possible provider/auth failure, empty response, or premature exit)`;
345
398
  } else {
346
399
  status = "completed";
347
400
  }
@@ -410,4 +463,4 @@ async function finishRun(
410
463
  status, finalText, runId, todoId, agent: agentName, model,
411
464
  durationMs: endedAt - startedAt, tokenTotal, costTotal, contextTokens, error,
412
465
  };
413
- }
466
+ }