@getpipher/armory-fleet 0.3.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/package.json +1 -1
- package/src/engine/spawnSubagent.ts +33 -16
- package/src/index.ts +88 -2
- 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 +178 -14
- package/src/panel/rows.ts +34 -0
- package/src/todo-sync/adapter.ts +6 -0
- package/src/todo-sync/port.ts +2 -0
- package/src/tools/subagent.ts +32 -0
package/src/panel/fleet-panel.ts
CHANGED
|
@@ -11,14 +11,17 @@ 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, backendsRow, backendInfo } from "./rows.ts";
|
|
14
|
+
import { fleetRow, agentsRow, agentInfo, backendsRow, backendInfo, lifecycleRow, lifecyclePhaseTimeline } from "./rows.ts";
|
|
15
15
|
import { spawnSubagent, type SpawnResult } from "../engine/spawnSubagent.ts";
|
|
16
16
|
import type { Backend, BackendRegistry } from "../backend/port.ts";
|
|
17
17
|
import type { RunRegistry } from "../engine/run-registry.ts";
|
|
18
18
|
import type { SingleSlotLock } from "../engine/concurrency-lock.ts";
|
|
19
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";
|
|
20
23
|
|
|
21
|
-
type View = "fleet" | "agents" | "backends";
|
|
24
|
+
type View = "fleet" | "lifecycle" | "agents" | "backends";
|
|
22
25
|
|
|
23
26
|
export interface FleetPanelDeps {
|
|
24
27
|
registry: Map<string, AgentDef>;
|
|
@@ -28,6 +31,10 @@ export interface FleetPanelDeps {
|
|
|
28
31
|
backendRegistry: BackendRegistry; // SPEC-3: replaces childFactory
|
|
29
32
|
parentModel: { provider: string; id: string };
|
|
30
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">;
|
|
31
38
|
}
|
|
32
39
|
|
|
33
40
|
export interface FleetPanelOpts {
|
|
@@ -50,6 +57,16 @@ export class FleetPanel extends Container {
|
|
|
50
57
|
private linkPhase: "task" | "link" = "task";
|
|
51
58
|
private infoAgent: AgentDef | null = null;
|
|
52
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;
|
|
53
70
|
|
|
54
71
|
constructor(opts: FleetPanelOpts) {
|
|
55
72
|
super();
|
|
@@ -69,9 +86,11 @@ export class FleetPanel extends Container {
|
|
|
69
86
|
const items: SelectItem[] =
|
|
70
87
|
this.view === "fleet"
|
|
71
88
|
? this.deps.runRegistry.list().map((r: RunRecord) => ({ value: r.runId, label: fleetRow(r) }))
|
|
72
|
-
: this.view === "
|
|
73
|
-
? [...this.deps.
|
|
74
|
-
: this.
|
|
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) }));
|
|
75
94
|
const fresh = new SelectList(items, 12, {
|
|
76
95
|
selectedPrefix: (s: string) => this.theme.fg("accent", s),
|
|
77
96
|
selectedText: (s: string) => this.theme.fg("accent", s),
|
|
@@ -89,7 +108,7 @@ export class FleetPanel extends Container {
|
|
|
89
108
|
this.children.length = 0;
|
|
90
109
|
this.children.push(...keep);
|
|
91
110
|
const accent = (s: string): string => this.theme.fg("accent", s);
|
|
92
|
-
const tabs = (["fleet", "agents", "backends"] as View[])
|
|
111
|
+
const tabs = (["fleet", "lifecycle", "agents", "backends"] as View[])
|
|
93
112
|
.map((v) => (v === this.view ? this.theme.fg("accent", this.theme.bold(`[${v}]`)) : this.theme.fg("dim", v)))
|
|
94
113
|
.join(" ");
|
|
95
114
|
this.addChild(new Text(accent(this.theme.bold(" FLEET")) + " " + tabs, 0, 0));
|
|
@@ -112,19 +131,46 @@ export class FleetPanel extends Container {
|
|
|
112
131
|
this.addChild(new Text(this.theme.fg("text", line), 0, 0));
|
|
113
132
|
}
|
|
114
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));
|
|
115
155
|
} else {
|
|
116
156
|
this.addChild(this.list);
|
|
117
157
|
}
|
|
118
158
|
|
|
119
159
|
this.addChild(new Spacer(1));
|
|
120
160
|
const hint =
|
|
121
|
-
this.infoAgent || this.selectedBackend
|
|
161
|
+
this.infoAgent || this.selectedBackend || this.selectedLifecycle
|
|
122
162
|
? " esc:Back"
|
|
123
|
-
: this.
|
|
124
|
-
? "
|
|
125
|
-
: this.
|
|
126
|
-
? "
|
|
127
|
-
: "
|
|
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";
|
|
128
174
|
this.addChild(new Text(this.theme.fg("dim", hint), 0, 0));
|
|
129
175
|
this.addChild(new Spacer(1));
|
|
130
176
|
this.addChild(new DynamicBorder(accent));
|
|
@@ -190,8 +236,11 @@ export class FleetPanel extends Container {
|
|
|
190
236
|
}
|
|
191
237
|
|
|
192
238
|
private switchView(): void {
|
|
193
|
-
this.view = this.view === "fleet" ? "
|
|
239
|
+
this.view = this.view === "fleet" ? "lifecycle"
|
|
240
|
+
: this.view === "lifecycle" ? "agents"
|
|
241
|
+
: this.view === "agents" ? "backends" : "fleet";
|
|
194
242
|
this.selectedBackend = null;
|
|
243
|
+
this.selectedLifecycle = null;
|
|
195
244
|
this.list = this.buildList();
|
|
196
245
|
this.renderShell();
|
|
197
246
|
}
|
|
@@ -205,13 +254,32 @@ export class FleetPanel extends Container {
|
|
|
205
254
|
if (matchesKey(data, "escape")) { this.selectedBackend = null; this.renderShell(); }
|
|
206
255
|
return;
|
|
207
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
|
+
}
|
|
208
267
|
if (this.runMode && (this.taskInput || this.linkInput)) {
|
|
209
268
|
if (matchesKey(data, "escape")) { this.cancelRun(); return; }
|
|
210
269
|
(this.linkPhase === "task" ? this.taskInput! : this.linkInput!).handleInput(data);
|
|
211
270
|
this.invalidate();
|
|
212
271
|
return;
|
|
213
272
|
}
|
|
214
|
-
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
|
+
}
|
|
215
283
|
if (matchesKey(data, "tab")) { this.switchView(); return; }
|
|
216
284
|
if (matchesKey(data, "q")) { this.onDone(); return; }
|
|
217
285
|
if (matchesKey(data, "r") && this.view === "agents") {
|
|
@@ -234,9 +302,105 @@ export class FleetPanel extends Container {
|
|
|
234
302
|
this.renderShell();
|
|
235
303
|
return;
|
|
236
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
|
+
}
|
|
237
340
|
this.list.handleInput(data);
|
|
238
341
|
this.invalidate();
|
|
239
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
|
+
}
|
|
240
404
|
}
|
|
241
405
|
|
|
242
406
|
/** Factory used by src/index.ts to open the panel via ctx.ui.custom. */
|
package/src/panel/rows.ts
CHANGED
|
@@ -82,3 +82,37 @@ export function backendInfo(b: Backend): string {
|
|
|
82
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
83
|
return lines.join("\n");
|
|
84
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
|
+
}
|
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
|
@@ -7,6 +7,8 @@ import type { SingleSlotLock } from "../engine/concurrency-lock.ts";
|
|
|
7
7
|
import type { SpawnResult } from "../engine/spawnSubagent.ts";
|
|
8
8
|
import { spawnSubagent } from "../engine/spawnSubagent.ts";
|
|
9
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";
|
|
10
12
|
|
|
11
13
|
export const subagentParams = Type.Object({
|
|
12
14
|
agent: Type.String({ description: "Agent name from the registry (builtin, project, or global)." }),
|
|
@@ -14,6 +16,8 @@ export const subagentParams = Type.Object({
|
|
|
14
16
|
todoId: Type.Optional(Type.String({ description: "Explicit link to an existing open/in_progress armory-todo todo. Omit to create a fleet task." })),
|
|
15
17
|
track: Type.Optional(Type.Boolean({ description: "Default true. Pass false only for throwaway lookups that don't represent real work." })),
|
|
16
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." })),
|
|
17
21
|
});
|
|
18
22
|
|
|
19
23
|
export type SubagentInput = Static<typeof subagentParams>;
|
|
@@ -26,6 +30,10 @@ export interface SubagentToolDeps {
|
|
|
26
30
|
backendRegistry: BackendRegistry; // SPEC-3: replaces childFactory
|
|
27
31
|
parentModel: { provider: string; id: string };
|
|
28
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">;
|
|
29
37
|
}
|
|
30
38
|
|
|
31
39
|
/** Build the pi.registerTool definition. Thin wrapper over spawnSubagent. */
|
|
@@ -42,6 +50,30 @@ export function createSubagentTool(deps: SubagentToolDeps) {
|
|
|
42
50
|
],
|
|
43
51
|
parameters: subagentParams,
|
|
44
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
|
+
}
|
|
45
77
|
const res: SpawnResult = await spawnSubagent({
|
|
46
78
|
agent: params.agent,
|
|
47
79
|
task: params.task,
|