@getpipher/armory-fleet 0.2.0 → 0.4.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/agents/general-purpose-cc.md +11 -0
- package/package.json +1 -1
- package/src/backend/claude-detector.ts +75 -0
- package/src/backend/claude-events.ts +39 -0
- package/src/backend/claude-factory.ts +50 -0
- package/src/backend/claude-session.ts +75 -0
- package/src/backend/hook-parity.ts +19 -0
- package/src/backend/port.ts +5 -0
- package/src/backend/registry.ts +36 -0
- package/src/backend/resume-store.ts +44 -0
- package/src/engine/run-registry.ts +4 -0
- package/src/engine/spawnSubagent.ts +46 -17
- package/src/index.ts +141 -7
- package/src/lifecycle/artifacts-parser.ts +57 -0
- package/src/lifecycle/default.ts +74 -0
- package/src/lifecycle/lifecycle-todo.ts +84 -0
- package/src/lifecycle/lifecycle-types.ts +66 -0
- package/src/lifecycle/port.ts +7 -0
- package/src/lifecycle/prompt-template.ts +40 -0
- package/src/lifecycle/registry.ts +169 -0
- package/src/lifecycle/run-lifecycle.ts +235 -0
- package/src/panel/fleet-panel.ts +205 -13
- package/src/panel/rows.ts +67 -2
- package/src/registry/frontmatter.ts +13 -0
- package/src/todo-sync/adapter.ts +6 -0
- package/src/todo-sync/port.ts +2 -0
- package/src/tools/subagent.ts +36 -3
package/src/panel/fleet-panel.ts
CHANGED
|
@@ -11,22 +11,30 @@ import {
|
|
|
11
11
|
} from "@earendil-works/pi-tui";
|
|
12
12
|
import type { AgentDef } from "../registry/frontmatter.ts";
|
|
13
13
|
import type { RunRecord } from "../engine/run-registry.ts";
|
|
14
|
-
import { fleetRow, agentsRow, agentInfo } from "./rows.ts";
|
|
15
|
-
import { spawnSubagent, type
|
|
14
|
+
import { fleetRow, agentsRow, agentInfo, backendsRow, backendInfo, lifecycleRow, lifecyclePhaseTimeline } from "./rows.ts";
|
|
15
|
+
import { spawnSubagent, type SpawnResult } from "../engine/spawnSubagent.ts";
|
|
16
|
+
import type { Backend, BackendRegistry } from "../backend/port.ts";
|
|
16
17
|
import type { RunRegistry } from "../engine/run-registry.ts";
|
|
17
18
|
import type { SingleSlotLock } from "../engine/concurrency-lock.ts";
|
|
18
19
|
import type { TodoSyncPort } from "../todo-sync/port.ts";
|
|
20
|
+
import type { LifecycleDef, LifecycleRunRecord, CheckpointDecision, PhaseRecord } from "../lifecycle/lifecycle-types.ts";
|
|
21
|
+
import type { LifecycleRunDeps, CheckpointFn } from "../lifecycle/run-lifecycle.ts";
|
|
22
|
+
import { runLifecycle } from "../lifecycle/run-lifecycle.ts";
|
|
19
23
|
|
|
20
|
-
type View = "fleet" | "agents";
|
|
24
|
+
type View = "fleet" | "lifecycle" | "agents" | "backends";
|
|
21
25
|
|
|
22
26
|
export interface FleetPanelDeps {
|
|
23
27
|
registry: Map<string, AgentDef>;
|
|
24
28
|
runRegistry: RunRegistry;
|
|
25
29
|
lock: SingleSlotLock;
|
|
26
30
|
todoSync: TodoSyncPort;
|
|
27
|
-
|
|
31
|
+
backendRegistry: BackendRegistry; // SPEC-3: replaces childFactory
|
|
28
32
|
parentModel: { provider: string; id: string };
|
|
29
33
|
parentCwd: string;
|
|
34
|
+
/** SPEC-4: lifecycle registry + active/recent run records + deps to drive checkpoints. */
|
|
35
|
+
lifecycleRegistry: Map<string, LifecycleDef>;
|
|
36
|
+
lifecycleRuns: Map<string, LifecycleRunRecord>;
|
|
37
|
+
lifecycleDeps: Omit<LifecycleRunDeps, "spawn">;
|
|
30
38
|
}
|
|
31
39
|
|
|
32
40
|
export interface FleetPanelOpts {
|
|
@@ -48,6 +56,17 @@ export class FleetPanel extends Container {
|
|
|
48
56
|
private linkInput: Input | null = null;
|
|
49
57
|
private linkPhase: "task" | "link" = "task";
|
|
50
58
|
private infoAgent: AgentDef | null = null;
|
|
59
|
+
private selectedBackend: Backend | null = null; // SPEC-3: Backends view i:Info
|
|
60
|
+
private selectedLifecycle: LifecycleRunRecord | null = null; // SPEC-4: Lifecycle view i:Info
|
|
61
|
+
// SPEC-4: Run-lifecycle inline input state
|
|
62
|
+
private lcRunMode = false;
|
|
63
|
+
private lcTaskInput: Input | null = null;
|
|
64
|
+
private lcNameInput: Input | null = null;
|
|
65
|
+
private lcPhase: "task" | "name" = "task";
|
|
66
|
+
// SPEC-4: pending checkpoint (interactive Continue/Revise/Abort)
|
|
67
|
+
private pendingCheckpoint: { phase: PhaseRecord; resolve: (d: CheckpointDecision) => void } | null = null;
|
|
68
|
+
private lcReviseInput: Input | null = null;
|
|
69
|
+
private lcRevising = false;
|
|
51
70
|
|
|
52
71
|
constructor(opts: FleetPanelOpts) {
|
|
53
72
|
super();
|
|
@@ -67,7 +86,11 @@ export class FleetPanel extends Container {
|
|
|
67
86
|
const items: SelectItem[] =
|
|
68
87
|
this.view === "fleet"
|
|
69
88
|
? this.deps.runRegistry.list().map((r: RunRecord) => ({ value: r.runId, label: fleetRow(r) }))
|
|
70
|
-
:
|
|
89
|
+
: this.view === "lifecycle"
|
|
90
|
+
? [...this.deps.lifecycleRuns.values()].map((l: LifecycleRunRecord) => ({ value: l.runId, label: lifecycleRow(l) }))
|
|
91
|
+
: this.view === "agents"
|
|
92
|
+
? [...this.deps.registry.values()].map((a: AgentDef) => ({ value: a.name, label: agentsRow(a) }))
|
|
93
|
+
: this.deps.backendRegistry.list().map((b: Backend) => ({ value: b.id, label: backendsRow(b) }));
|
|
71
94
|
const fresh = new SelectList(items, 12, {
|
|
72
95
|
selectedPrefix: (s: string) => this.theme.fg("accent", s),
|
|
73
96
|
selectedText: (s: string) => this.theme.fg("accent", s),
|
|
@@ -85,7 +108,7 @@ export class FleetPanel extends Container {
|
|
|
85
108
|
this.children.length = 0;
|
|
86
109
|
this.children.push(...keep);
|
|
87
110
|
const accent = (s: string): string => this.theme.fg("accent", s);
|
|
88
|
-
const tabs = (["fleet", "agents"] as View[])
|
|
111
|
+
const tabs = (["fleet", "lifecycle", "agents", "backends"] as View[])
|
|
89
112
|
.map((v) => (v === this.view ? this.theme.fg("accent", this.theme.bold(`[${v}]`)) : this.theme.fg("dim", v)))
|
|
90
113
|
.join(" ");
|
|
91
114
|
this.addChild(new Text(accent(this.theme.bold(" FLEET")) + " " + tabs, 0, 0));
|
|
@@ -101,17 +124,53 @@ export class FleetPanel extends Container {
|
|
|
101
124
|
for (const line of agentInfo(this.infoAgent).split("\n")) {
|
|
102
125
|
this.addChild(new Text(this.theme.fg("text", line), 0, 0));
|
|
103
126
|
}
|
|
127
|
+
} else if (this.selectedBackend) {
|
|
128
|
+
// SPEC-3: i:Info detail pane (backends view)
|
|
129
|
+
this.addChild(new Text(this.theme.fg("dim", " ── backend info ──"), 0, 0));
|
|
130
|
+
for (const line of backendInfo(this.selectedBackend).split("\n")) {
|
|
131
|
+
this.addChild(new Text(this.theme.fg("text", line), 0, 0));
|
|
132
|
+
}
|
|
133
|
+
this.addChild(new Text(this.theme.fg("dim", " esc:Back"), 0, 0));
|
|
134
|
+
} else if (this.selectedLifecycle) {
|
|
135
|
+
// SPEC-4: i:Info detail pane (lifecycle view) — phase timeline
|
|
136
|
+
this.addChild(new Text(this.theme.fg("dim", " ── lifecycle phases ──"), 0, 0));
|
|
137
|
+
for (const line of lifecyclePhaseTimeline(this.selectedLifecycle).split("\n")) {
|
|
138
|
+
this.addChild(new Text(this.theme.fg("text", line), 0, 0));
|
|
139
|
+
}
|
|
140
|
+
this.addChild(new Text(this.theme.fg("dim", " esc:Back"), 0, 0));
|
|
141
|
+
} else if (this.lcRunMode && (this.lcTaskInput || this.lcNameInput)) {
|
|
142
|
+
const prompt = this.lcPhase === "task" ? " task> " : " lifecycle name (blank=default)> ";
|
|
143
|
+
this.addChild(new Text(this.theme.fg("accent", prompt), 0, 0));
|
|
144
|
+
this.addChild(this.lcPhase === "task" ? this.lcTaskInput! : this.lcNameInput!);
|
|
145
|
+
this.addChild(new Text(this.theme.fg("dim", " enter submit • esc cancel"), 0, 0));
|
|
146
|
+
} else if (this.pendingCheckpoint && !this.lcRevising) {
|
|
147
|
+
const pc = this.pendingCheckpoint;
|
|
148
|
+
this.addChild(new Text(this.theme.fg("dim", ` ── checkpoint: phase '${pc.phase.name}' (${pc.phase.status}) ──`), 0, 0));
|
|
149
|
+
this.addChild(new Text(this.theme.fg("text", ` ${pc.phase.summary.slice(0, 200)}`), 0, 0));
|
|
150
|
+
this.addChild(new Text(this.theme.fg("dim", " c:Continue v:Revise a:Abort"), 0, 0));
|
|
151
|
+
} else if (this.lcRevising && this.lcReviseInput) {
|
|
152
|
+
this.addChild(new Text(this.theme.fg("accent", " revise feedback> "), 0, 0));
|
|
153
|
+
this.addChild(this.lcReviseInput);
|
|
154
|
+
this.addChild(new Text(this.theme.fg("dim", " enter submit • esc cancel"), 0, 0));
|
|
104
155
|
} else {
|
|
105
156
|
this.addChild(this.list);
|
|
106
157
|
}
|
|
107
158
|
|
|
108
159
|
this.addChild(new Spacer(1));
|
|
109
160
|
const hint =
|
|
110
|
-
this.infoAgent
|
|
161
|
+
this.infoAgent || this.selectedBackend || this.selectedLifecycle
|
|
111
162
|
? " esc:Back"
|
|
112
|
-
: this.
|
|
113
|
-
? "
|
|
114
|
-
:
|
|
163
|
+
: this.pendingCheckpoint
|
|
164
|
+
? " c:Continue v:Revise a:Abort"
|
|
165
|
+
: this.lcRevising
|
|
166
|
+
? " enter:Submit-feedback esc:Cancel"
|
|
167
|
+
: this.view === "fleet"
|
|
168
|
+
? " r:Run-new s:Stop o:Open-todo tab:Lifecycle q:Quit"
|
|
169
|
+
: this.view === "lifecycle"
|
|
170
|
+
? " r:Run-lifecycle i:Info tab:Agents q:Quit"
|
|
171
|
+
: this.view === "agents"
|
|
172
|
+
? " r:Run e:Edit i:Info d:Reload tab:Backends q:Quit"
|
|
173
|
+
: " r:Refresh i:Info tab:Fleet q:Quit";
|
|
115
174
|
this.addChild(new Text(this.theme.fg("dim", hint), 0, 0));
|
|
116
175
|
this.addChild(new Spacer(1));
|
|
117
176
|
this.addChild(new DynamicBorder(accent));
|
|
@@ -151,7 +210,7 @@ export class FleetPanel extends Container {
|
|
|
151
210
|
agent, task, todoId, track: true,
|
|
152
211
|
registry: this.deps.registry, todoSync: this.deps.todoSync,
|
|
153
212
|
runRegistry: this.deps.runRegistry, lock: this.deps.lock,
|
|
154
|
-
|
|
213
|
+
backendRegistry: this.deps.backendRegistry,
|
|
155
214
|
parentModel: this.deps.parentModel, parentCwd: this.deps.parentCwd,
|
|
156
215
|
// live Fleet row during the run (SPEC-1 §4c) — re-render on each turn_end
|
|
157
216
|
onEvent: (e) => {
|
|
@@ -177,7 +236,11 @@ export class FleetPanel extends Container {
|
|
|
177
236
|
}
|
|
178
237
|
|
|
179
238
|
private switchView(): void {
|
|
180
|
-
this.view = this.view === "fleet" ? "
|
|
239
|
+
this.view = this.view === "fleet" ? "lifecycle"
|
|
240
|
+
: this.view === "lifecycle" ? "agents"
|
|
241
|
+
: this.view === "agents" ? "backends" : "fleet";
|
|
242
|
+
this.selectedBackend = null;
|
|
243
|
+
this.selectedLifecycle = null;
|
|
181
244
|
this.list = this.buildList();
|
|
182
245
|
this.renderShell();
|
|
183
246
|
}
|
|
@@ -187,13 +250,36 @@ export class FleetPanel extends Container {
|
|
|
187
250
|
if (matchesKey(data, "escape")) { this.infoAgent = null; this.renderShell(); }
|
|
188
251
|
return;
|
|
189
252
|
}
|
|
253
|
+
if (this.selectedBackend) {
|
|
254
|
+
if (matchesKey(data, "escape")) { this.selectedBackend = null; this.renderShell(); }
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
if (this.selectedLifecycle) {
|
|
258
|
+
if (matchesKey(data, "escape")) { this.selectedLifecycle = null; this.renderShell(); }
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
if (this.lcRunMode && (this.lcTaskInput || this.lcNameInput)) {
|
|
262
|
+
if (matchesKey(data, "escape")) { this.cancelLifecycleRun(); return; }
|
|
263
|
+
(this.lcPhase === "task" ? this.lcTaskInput! : this.lcNameInput!).handleInput(data);
|
|
264
|
+
this.invalidate();
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
190
267
|
if (this.runMode && (this.taskInput || this.linkInput)) {
|
|
191
268
|
if (matchesKey(data, "escape")) { this.cancelRun(); return; }
|
|
192
269
|
(this.linkPhase === "task" ? this.taskInput! : this.linkInput!).handleInput(data);
|
|
193
270
|
this.invalidate();
|
|
194
271
|
return;
|
|
195
272
|
}
|
|
196
|
-
if (matchesKey(data, "escape")) {
|
|
273
|
+
if (matchesKey(data, "escape")) {
|
|
274
|
+
// SPEC-4: if a lifecycle checkpoint is pending, resolve it as abort so runLifecycle
|
|
275
|
+
// doesn't hang + the lifecycle TODO is reverted (not orphaned) when the panel closes.
|
|
276
|
+
if (this.pendingCheckpoint) {
|
|
277
|
+
this.pendingCheckpoint.resolve({ action: "abort" });
|
|
278
|
+
this.pendingCheckpoint = null;
|
|
279
|
+
}
|
|
280
|
+
this.onDone();
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
197
283
|
if (matchesKey(data, "tab")) { this.switchView(); return; }
|
|
198
284
|
if (matchesKey(data, "q")) { this.onDone(); return; }
|
|
199
285
|
if (matchesKey(data, "r") && this.view === "agents") {
|
|
@@ -206,9 +292,115 @@ export class FleetPanel extends Container {
|
|
|
206
292
|
if (sel) { this.infoAgent = this.deps.registry.get(sel.value) ?? null; this.renderShell(); }
|
|
207
293
|
return;
|
|
208
294
|
}
|
|
295
|
+
if (matchesKey(data, "i") && this.view === "backends") {
|
|
296
|
+
const sel = this.list.getSelectedItem();
|
|
297
|
+
if (sel) { this.selectedBackend = this.deps.backendRegistry.list().find((x) => x.id === sel.value) ?? null; this.renderShell(); }
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
if (matchesKey(data, "r") && this.view === "backends") {
|
|
301
|
+
this.onNotify("Backends reflect init-time detection; restart pi to re-detect.", "info");
|
|
302
|
+
this.renderShell();
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
// SPEC-4: Lifecycle view — i:Info + r:Run-lifecycle
|
|
306
|
+
if (matchesKey(data, "i") && this.view === "lifecycle") {
|
|
307
|
+
const sel = this.list.getSelectedItem();
|
|
308
|
+
if (sel) { this.selectedLifecycle = this.deps.lifecycleRuns.get(sel.value) ?? null; this.renderShell(); }
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
if (matchesKey(data, "r") && this.view === "lifecycle") {
|
|
312
|
+
this.startLifecycleRun();
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
// SPEC-4: pending checkpoint keys (c/v/a)
|
|
316
|
+
if (this.pendingCheckpoint && !this.lcRevising) {
|
|
317
|
+
if (matchesKey(data, "c")) { this.pendingCheckpoint.resolve({ action: "continue" }); this.pendingCheckpoint = null; this.renderShell(); return; }
|
|
318
|
+
if (matchesKey(data, "a")) { this.pendingCheckpoint!.resolve({ action: "abort" }); this.pendingCheckpoint = null; this.renderShell(); return; }
|
|
319
|
+
if (matchesKey(data, "v")) {
|
|
320
|
+
this.lcRevising = true;
|
|
321
|
+
this.lcReviseInput = new Input();
|
|
322
|
+
this.lcReviseInput.onSubmit = (fb: string) => {
|
|
323
|
+
this.lcRevising = false;
|
|
324
|
+
this.lcReviseInput = null;
|
|
325
|
+
this.pendingCheckpoint!.resolve({ action: "revise", feedback: fb });
|
|
326
|
+
this.pendingCheckpoint = null;
|
|
327
|
+
this.renderShell();
|
|
328
|
+
};
|
|
329
|
+
this.lcReviseInput.onEscape = () => { this.lcRevising = false; this.lcReviseInput = null; this.renderShell(); };
|
|
330
|
+
this.renderShell();
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if (this.lcRevising && this.lcReviseInput) {
|
|
335
|
+
if (matchesKey(data, "escape")) { this.lcRevising = false; this.lcReviseInput = null; this.renderShell(); return; }
|
|
336
|
+
this.lcReviseInput.handleInput(data);
|
|
337
|
+
this.invalidate();
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
209
340
|
this.list.handleInput(data);
|
|
210
341
|
this.invalidate();
|
|
211
342
|
}
|
|
343
|
+
|
|
344
|
+
/** SPEC-4: open the Run-lifecycle inline inputs (task → lifecycle name → start runLifecycle). */
|
|
345
|
+
private startLifecycleRun(): void {
|
|
346
|
+
this.lcPhase = "task";
|
|
347
|
+
this.lcTaskInput = new Input();
|
|
348
|
+
this.lcTaskInput.onSubmit = (task: string) => {
|
|
349
|
+
if (!task.trim()) { this.cancelLifecycleRun(); return; }
|
|
350
|
+
this.lcPhase = "name";
|
|
351
|
+
this.lcNameInput = new Input();
|
|
352
|
+
this.lcNameInput.onSubmit = (name: string) => {
|
|
353
|
+
const lcName = name.trim() || "default";
|
|
354
|
+
void this.executeLifecycleRun(task.trim(), lcName);
|
|
355
|
+
};
|
|
356
|
+
this.lcNameInput.onEscape = () => { void this.executeLifecycleRun(task.trim(), "default"); };
|
|
357
|
+
this.renderShell();
|
|
358
|
+
};
|
|
359
|
+
this.lcTaskInput.onEscape = () => this.cancelLifecycleRun();
|
|
360
|
+
this.lcRunMode = true;
|
|
361
|
+
this.renderShell();
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
private cancelLifecycleRun(): void {
|
|
365
|
+
this.lcRunMode = false;
|
|
366
|
+
this.lcTaskInput = null;
|
|
367
|
+
this.lcNameInput = null;
|
|
368
|
+
this.renderShell();
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
private async executeLifecycleRun(task: string, lifecycleName: string): Promise<void> {
|
|
372
|
+
this.lcRunMode = false;
|
|
373
|
+
this.lcTaskInput = null;
|
|
374
|
+
this.lcNameInput = null;
|
|
375
|
+
this.renderShell();
|
|
376
|
+
if (!this.deps.lifecycleRegistry.has(lifecycleName)) {
|
|
377
|
+
this.onNotify(`lifecycle '${lifecycleName}' not found; available: ${[...this.deps.lifecycleRegistry.keys()].sort().join(", ")}`, "error");
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
const onCheckpoint: CheckpointFn = (phase) => new Promise<CheckpointDecision>((resolve) => {
|
|
381
|
+
this.pendingCheckpoint = { phase, resolve };
|
|
382
|
+
this.renderShell();
|
|
383
|
+
});
|
|
384
|
+
const lifecycleFullDeps: LifecycleRunDeps = {
|
|
385
|
+
...this.deps.lifecycleDeps,
|
|
386
|
+
spawn: async (o) => {
|
|
387
|
+
const { spawnSubagent } = await import("../engine/spawnSubagent.ts");
|
|
388
|
+
return spawnSubagent({
|
|
389
|
+
agent: o.agent, task: o.task, lifecycleTodoId: o.lifecycleTodoId, model: o.model,
|
|
390
|
+
skillsOverride: o.skills, backendOverride: o.backend,
|
|
391
|
+
registry: this.deps.registry, todoSync: this.deps.todoSync, runRegistry: this.deps.runRegistry, lock: this.deps.lock,
|
|
392
|
+
backendRegistry: this.deps.backendRegistry, parentModel: this.deps.parentModel, parentCwd: this.deps.parentCwd,
|
|
393
|
+
});
|
|
394
|
+
},
|
|
395
|
+
};
|
|
396
|
+
const res = await runLifecycle(task, lifecycleName, { deps: lifecycleFullDeps, mode: "checkpointed", onCheckpoint });
|
|
397
|
+
this.pendingCheckpoint = null;
|
|
398
|
+
// record the run so the Lifecycle view shows it
|
|
399
|
+
this.deps.lifecycleRuns.set(res.runId, res);
|
|
400
|
+
this.list = this.buildList();
|
|
401
|
+
this.renderShell();
|
|
402
|
+
this.onNotify(`lifecycle ${res.status}: ${res.runId}${res.error ? " — " + res.error : ""}`, res.status === "completed" ? "info" : "warning");
|
|
403
|
+
}
|
|
212
404
|
}
|
|
213
405
|
|
|
214
406
|
/** Factory used by src/index.ts to open the panel via ctx.ui.custom. */
|
package/src/panel/rows.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import type { AgentDef } from "../registry/frontmatter.ts";
|
|
3
3
|
import type { FleetRunStatus } from "../todo-sync/port.ts";
|
|
4
4
|
import type { RunRecord } from "../engine/run-registry.ts";
|
|
5
|
+
import type { Backend, BackendHookParity } from "../backend/port.ts";
|
|
5
6
|
|
|
6
7
|
export function fmtDuration(ms: number): string {
|
|
7
8
|
const s = Math.floor(ms / 1000);
|
|
@@ -30,7 +31,7 @@ export function agentsRow(agent: AgentDef): string {
|
|
|
30
31
|
const chip = `armory:[t${agent.todoSync ? "✓" : "✗"} m${agent.memoryHydrate ? "✓" : "✗"} v${agent.vision ? "✓" : "✗"}]`;
|
|
31
32
|
const skills = agent.skills?.length ? ` skills: ${agent.skills.join(",")}` : "";
|
|
32
33
|
const tools = agent.tools?.length ? ` tools: ${agent.tools.join(",")}` : "";
|
|
33
|
-
return `${agent.name} [${agent.source}] ${model}${tools}${skills} ${chip}`;
|
|
34
|
+
return `${agent.name} [${agent.backend}] [${agent.source}] ${model}${tools}${skills} ${chip}`;
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
export function agentInfo(agent: AgentDef): string {
|
|
@@ -50,4 +51,68 @@ export function agentInfo(agent: AgentDef): string {
|
|
|
50
51
|
agent.rolePrompt.trim(),
|
|
51
52
|
];
|
|
52
53
|
return lines.join("\n");
|
|
53
|
-
}
|
|
54
|
+
}
|
|
55
|
+
function chipStr(p: BackendHookParity): string {
|
|
56
|
+
return `t${p.todo} m${p.memory} v${p.vision}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function backendsRow(b: Backend): string {
|
|
60
|
+
const avail = b.available() ? "✓" : "✗";
|
|
61
|
+
const vi = b.versionInfo();
|
|
62
|
+
const version = vi?.version ? vi.version : "—";
|
|
63
|
+
const schema = vi ? (vi.schemaOk ? "✓" : "✗") : "—";
|
|
64
|
+
const note = vi && !vi.schemaOk && vi.note ? ` ${vi.note}` : "";
|
|
65
|
+
return `${b.id} ${avail} ${version} schema:${schema} armory:[${chipStr(b.hookParity)}]${note}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function backendInfo(b: Backend): string {
|
|
69
|
+
const vi = b.versionInfo();
|
|
70
|
+
const lines = [
|
|
71
|
+
`id: ${b.id}`,
|
|
72
|
+
`available: ${b.available() ? "✓" : "✗"}`,
|
|
73
|
+
`version: ${vi?.version ?? "—"}`,
|
|
74
|
+
`schemaOk: ${vi ? vi.schemaOk : "—"}`,
|
|
75
|
+
];
|
|
76
|
+
if (vi?.note) lines.push(`note: ${vi.note}`);
|
|
77
|
+
lines.push("flagSupport:");
|
|
78
|
+
for (const [flag, ok] of Object.entries(vi?.flagSupport ?? {})) lines.push(` ${flag}: ${ok ? "✓" : "✗"}`);
|
|
79
|
+
lines.push("hookParity:");
|
|
80
|
+
lines.push(` todo: ${b.hookParity.todo} (excluded via ${b.id === "pi" ? "excludeTools+noExtensions" : "--disallowed-tools/prompt-nudge"})`);
|
|
81
|
+
lines.push(` memory: ${b.hookParity.memory} (${b.id === "pi" ? "CustomResourceLoader systemPromptOverride" : "--append-system-prompt"})`);
|
|
82
|
+
lines.push(` vision: ${b.hookParity.vision} (${b.hookParity.vision === "✓" ? "describe_image fallback injected" : "pass-through only; no describe_image fallback — customTools not injectable into claude -p"})`);
|
|
83
|
+
return lines.join("\n");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
import type { LifecycleRunRecord, LifecycleStatus } from "../lifecycle/lifecycle-types.ts";
|
|
87
|
+
|
|
88
|
+
const LC_GLYPH: Record<LifecycleStatus, string> = {
|
|
89
|
+
running: "▶", checkpoint: "⏸", completed: "✓", failed: "✗", aborted: "✗",
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export function lifecycleRow(r: LifecycleRunRecord): string {
|
|
93
|
+
const dur = r.endedAt ? fmtDuration(r.endedAt - r.startedAt) : "—";
|
|
94
|
+
const curIdx = r.phases.findIndex((p) => p.status === "running");
|
|
95
|
+
const cur = curIdx >= 0 ? r.phases[curIdx] : r.phases[r.phases.length - 1];
|
|
96
|
+
const curName = cur ? `●${cur.name}` : "—";
|
|
97
|
+
// N/M = current phase position / total (1-indexed); falls back to last phase when none running.
|
|
98
|
+
const counts = `${(curIdx >= 0 ? curIdx + 1 : r.phases.length)}/${r.phases.length}`;
|
|
99
|
+
return `${LC_GLYPH[r.status]} ${r.runId} ${r.lifecycleName} ${curName} ${counts} ${r.mode} ${dur} ${r.backend} "${r.task}"`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function lifecyclePhaseTimeline(r: LifecycleRunRecord): string {
|
|
103
|
+
const lines: string[] = [
|
|
104
|
+
`Lifecycle ${r.runId} — ${r.lifecycleName} — "${r.task}"`,
|
|
105
|
+
`Backend: ${r.backend} · Mode: ${r.mode} · Status: ${r.status}`,
|
|
106
|
+
"",
|
|
107
|
+
"Phases:",
|
|
108
|
+
];
|
|
109
|
+
for (const p of r.phases) {
|
|
110
|
+
const mark = p.reviseCount > 0 ? "[~]" : p.status === "completed" ? "[x]" : "[ ]";
|
|
111
|
+
const art = p.paths.length ? ` → ${p.paths.join(", ")}` : "";
|
|
112
|
+
lines.push(` ${mark} ${p.name} ${p.status}${art}${p.paths.length ? " [Open]" : ""}`);
|
|
113
|
+
}
|
|
114
|
+
if (r.status === "checkpoint") {
|
|
115
|
+
lines.push("", "── Checkpoint ──", "[Continue] [Revise] [Abort]");
|
|
116
|
+
}
|
|
117
|
+
return lines.join("\n");
|
|
118
|
+
}
|
|
@@ -16,6 +16,10 @@ export interface AgentDef {
|
|
|
16
16
|
todoSync: boolean;
|
|
17
17
|
memoryHydrate: boolean;
|
|
18
18
|
vision: boolean;
|
|
19
|
+
/** Cross-harness backend routing (SPEC-3). Invalid value → FrontmatterError. */
|
|
20
|
+
backend: "pi" | "claude";
|
|
21
|
+
/** Stable id for backend-native resume (SPEC-3). Defaults to name. */
|
|
22
|
+
sessionKey: string;
|
|
19
23
|
source: AgentSource;
|
|
20
24
|
filePath: string;
|
|
21
25
|
}
|
|
@@ -52,6 +56,13 @@ export function parseAgentFile(content: string, filePath: string, source: AgentS
|
|
|
52
56
|
const memoryHydrate = raw.memoryHydrate === undefined ? true : Boolean(raw.memoryHydrate);
|
|
53
57
|
const vision = raw.vision === undefined ? true : Boolean(raw.vision);
|
|
54
58
|
|
|
59
|
+
const rawBackend = typeof raw.backend === "string" ? raw.backend.trim() : "pi";
|
|
60
|
+
if (rawBackend !== "pi" && rawBackend !== "claude") {
|
|
61
|
+
throw new FrontmatterError(`${filePath}: invalid backend '${rawBackend}' (must be 'pi' | 'claude')`);
|
|
62
|
+
}
|
|
63
|
+
const backend = rawBackend as "pi" | "claude";
|
|
64
|
+
const sessionKey = typeof raw.sessionKey === "string" && raw.sessionKey.trim() ? raw.sessionKey.trim() : name;
|
|
65
|
+
|
|
55
66
|
return {
|
|
56
67
|
name,
|
|
57
68
|
description,
|
|
@@ -63,6 +74,8 @@ export function parseAgentFile(content: string, filePath: string, source: AgentS
|
|
|
63
74
|
todoSync,
|
|
64
75
|
memoryHydrate,
|
|
65
76
|
vision,
|
|
77
|
+
backend,
|
|
78
|
+
sessionKey,
|
|
66
79
|
source,
|
|
67
80
|
filePath,
|
|
68
81
|
};
|
package/src/todo-sync/adapter.ts
CHANGED
|
@@ -81,4 +81,10 @@ export class ArmoryTodoAdapter implements TodoSyncPort {
|
|
|
81
81
|
}
|
|
82
82
|
appendNote(todoId, `fleet-run reverted: ${reason}`);
|
|
83
83
|
}
|
|
84
|
+
|
|
85
|
+
async updateLifecycleProgress(todoId: string, progressBlock: string): Promise<void> {
|
|
86
|
+
if (!todoId) return;
|
|
87
|
+
// single-writer: replace notes wholesale with the progress block (the lifecycle owns it)
|
|
88
|
+
updateTodo(todoId, { notes: progressBlock });
|
|
89
|
+
}
|
|
84
90
|
}
|
package/src/todo-sync/port.ts
CHANGED
|
@@ -38,4 +38,6 @@ export interface TodoSyncPort {
|
|
|
38
38
|
markRunTodoDone(todoId: string | null, priorStatus: string | undefined, result: string): Promise<void>;
|
|
39
39
|
/** After a failed/aborted run: fleet-created -> open; linked -> restore prior. + reason note. */
|
|
40
40
|
markRunTodoReverted(todoId: string | null, priorStatus: string | undefined, reason: string): Promise<void>;
|
|
41
|
+
/** SPEC-4: replace a lifecycle todo's notes with the phase-progress block (single source of truth). */
|
|
42
|
+
updateLifecycleProgress(todoId: string, progressBlock: string): Promise<void>;
|
|
41
43
|
}
|
package/src/tools/subagent.ts
CHANGED
|
@@ -4,8 +4,11 @@ import type { AgentDef } from "../registry/frontmatter.ts";
|
|
|
4
4
|
import type { TodoSyncPort } from "../todo-sync/port.ts";
|
|
5
5
|
import type { RunRegistry } from "../engine/run-registry.ts";
|
|
6
6
|
import type { SingleSlotLock } from "../engine/concurrency-lock.ts";
|
|
7
|
-
import type {
|
|
7
|
+
import type { SpawnResult } from "../engine/spawnSubagent.ts";
|
|
8
8
|
import { spawnSubagent } from "../engine/spawnSubagent.ts";
|
|
9
|
+
import type { BackendRegistry } from "../backend/port.ts";
|
|
10
|
+
import type { LifecycleRunDeps } from "../lifecycle/run-lifecycle.ts";
|
|
11
|
+
import type { LifecycleDef } from "../lifecycle/lifecycle-types.ts";
|
|
9
12
|
|
|
10
13
|
export const subagentParams = Type.Object({
|
|
11
14
|
agent: Type.String({ description: "Agent name from the registry (builtin, project, or global)." }),
|
|
@@ -13,6 +16,8 @@ export const subagentParams = Type.Object({
|
|
|
13
16
|
todoId: Type.Optional(Type.String({ description: "Explicit link to an existing open/in_progress armory-todo todo. Omit to create a fleet task." })),
|
|
14
17
|
track: Type.Optional(Type.Boolean({ description: "Default true. Pass false only for throwaway lookups that don't represent real work." })),
|
|
15
18
|
model: Type.Optional(Type.String({ description: 'Override the agent model, e.g. "anthropic/claude-sonnet-4".' })),
|
|
19
|
+
lifecycle: Type.Optional(Type.String({ description: "Run a multi-phase superpowers lifecycle by name (e.g. 'default') instead of a single delegate. Tool-driven lifecycles run end-to-end (auto) — checkpoints are a /fleet panel feature." })),
|
|
20
|
+
auto: Type.Optional(Type.Boolean({ description: "Only relevant with `lifecycle`. Tool-driven is always auto; this flag is forward-compat. Panel-driven uses --auto on /fleet-implement." })),
|
|
16
21
|
});
|
|
17
22
|
|
|
18
23
|
export type SubagentInput = Static<typeof subagentParams>;
|
|
@@ -22,9 +27,13 @@ export interface SubagentToolDeps {
|
|
|
22
27
|
runRegistry: RunRegistry;
|
|
23
28
|
lock: SingleSlotLock;
|
|
24
29
|
todoSync: TodoSyncPort;
|
|
25
|
-
|
|
30
|
+
backendRegistry: BackendRegistry; // SPEC-3: replaces childFactory
|
|
26
31
|
parentModel: { provider: string; id: string };
|
|
27
32
|
parentCwd: string;
|
|
33
|
+
/** SPEC-4: lifecycle registry + spawn adapter (tool-driven = auto). */
|
|
34
|
+
lifecycleRegistry: Map<string, LifecycleDef>;
|
|
35
|
+
lifecycleRuns: Map<string, import("../lifecycle/lifecycle-types.ts").LifecycleRunRecord>;
|
|
36
|
+
lifecycleDeps: Omit<LifecycleRunDeps, "spawn">;
|
|
28
37
|
}
|
|
29
38
|
|
|
30
39
|
/** Build the pi.registerTool definition. Thin wrapper over spawnSubagent. */
|
|
@@ -41,6 +50,30 @@ export function createSubagentTool(deps: SubagentToolDeps) {
|
|
|
41
50
|
],
|
|
42
51
|
parameters: subagentParams,
|
|
43
52
|
async execute(_toolCallId: string, params: SubagentInput, signal: AbortSignal, _onUpdate: unknown, ctx: any) {
|
|
53
|
+
if (params.lifecycle) {
|
|
54
|
+
const { runLifecycle } = await import("../lifecycle/run-lifecycle.ts");
|
|
55
|
+
const lifecycleFullDeps: LifecycleRunDeps = {
|
|
56
|
+
...deps.lifecycleDeps,
|
|
57
|
+
spawn: async (o) => spawnSubagent({
|
|
58
|
+
agent: o.agent, task: o.task, lifecycleTodoId: o.lifecycleTodoId, model: o.model,
|
|
59
|
+
skillsOverride: o.skills, backendOverride: o.backend,
|
|
60
|
+
registry: deps.registry, todoSync: deps.todoSync, runRegistry: deps.runRegistry, lock: deps.lock,
|
|
61
|
+
backendRegistry: deps.backendRegistry, parentModel: deps.parentModel, parentCwd: deps.parentCwd, signal,
|
|
62
|
+
}),
|
|
63
|
+
};
|
|
64
|
+
const res = await runLifecycle(params.task, params.lifecycle, {
|
|
65
|
+
deps: lifecycleFullDeps, mode: "auto",
|
|
66
|
+
onCheckpoint: async (phase) => phase.status === "failed" ? { action: "abort" } : { action: "continue" },
|
|
67
|
+
});
|
|
68
|
+
const isError = res.status === "failed" || res.status === "aborted";
|
|
69
|
+
const summary = `lifecycle ${res.lifecycleName}: ${res.status} (${res.phases.length} phases)\n` +
|
|
70
|
+
res.phases.map((p) => ` ${p.name}: ${p.status}${p.paths.length ? " → " + p.paths.join(", ") : ""}`).join("\n");
|
|
71
|
+
return {
|
|
72
|
+
content: [{ type: "text" as const, text: isError ? (res.error ?? res.status) : summary }],
|
|
73
|
+
details: { runId: res.runId, todoId: res.todoId, lifecycle: res.lifecycleName, status: res.status, phases: res.phases.length },
|
|
74
|
+
isError,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
44
77
|
const res: SpawnResult = await spawnSubagent({
|
|
45
78
|
agent: params.agent,
|
|
46
79
|
task: params.task,
|
|
@@ -51,7 +84,7 @@ export function createSubagentTool(deps: SubagentToolDeps) {
|
|
|
51
84
|
todoSync: deps.todoSync,
|
|
52
85
|
runRegistry: deps.runRegistry,
|
|
53
86
|
lock: deps.lock,
|
|
54
|
-
|
|
87
|
+
backendRegistry: deps.backendRegistry,
|
|
55
88
|
parentModel: deps.parentModel,
|
|
56
89
|
parentCwd: deps.parentCwd,
|
|
57
90
|
signal,
|