@getpipher/armory-fleet 0.3.0 → 0.5.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/run-registry.ts +13 -1
- package/src/engine/spawnSubagent.ts +33 -16
- package/src/index.ts +155 -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 +251 -0
- package/src/panel/bg-runs-store.ts +36 -0
- package/src/panel/fleet-items.ts +36 -0
- package/src/panel/fleet-panel.ts +334 -18
- package/src/panel/rows.ts +90 -0
- package/src/runtime/async-runner.ts +123 -0
- package/src/runtime/concurrency-pool.ts +27 -0
- package/src/runtime/results-inbox.ts +45 -0
- package/src/runtime/resume.ts +48 -0
- package/src/runtime/run-journal.ts +61 -0
- package/src/scheduling/expressions.ts +60 -0
- package/src/scheduling/pid-lock.ts +43 -0
- package/src/scheduling/scheduler.ts +147 -0
- package/src/todo-sync/adapter.ts +6 -0
- package/src/todo-sync/port.ts +2 -0
- package/src/tools/fleet-results.ts +35 -0
- package/src/tools/subagent.ts +58 -0
- package/src/vendor/cron-parser/NOTICE.md +23 -0
- package/src/vendor/cron-parser/lib/date.js +79 -0
- package/src/vendor/cron-parser/lib/expression.js +614 -0
- package/src/vendor/cron-parser/lib/number.js +8 -0
- package/src/vendor/cron-parser/lib/parser.js +103 -0
- package/src/vendor/cron-parser/types.d.ts +12 -0
- package/src/worktree/diff-service.ts +40 -0
- package/src/worktree/worktree-service.ts +92 -0
package/src/panel/fleet-panel.ts
CHANGED
|
@@ -10,15 +10,20 @@ import {
|
|
|
10
10
|
type SelectItem,
|
|
11
11
|
} from "@earendil-works/pi-tui";
|
|
12
12
|
import type { AgentDef } from "../registry/frontmatter.ts";
|
|
13
|
-
import
|
|
14
|
-
import {
|
|
13
|
+
import { agentsRow, agentInfo, backendsRow, backendInfo, lifecycleRow, lifecyclePhaseTimeline, scheduleRow } from "./rows.ts";
|
|
14
|
+
import { buildFleetItems } from "./fleet-items.ts";
|
|
15
|
+
import type { Scheduler, Schedule } from "../scheduling/scheduler.ts";
|
|
16
|
+
import type { BgRunsStore } from "./bg-runs-store.ts";
|
|
15
17
|
import { spawnSubagent, type SpawnResult } from "../engine/spawnSubagent.ts";
|
|
16
18
|
import type { Backend, BackendRegistry } from "../backend/port.ts";
|
|
17
19
|
import type { RunRegistry } from "../engine/run-registry.ts";
|
|
18
20
|
import type { SingleSlotLock } from "../engine/concurrency-lock.ts";
|
|
19
21
|
import type { TodoSyncPort } from "../todo-sync/port.ts";
|
|
22
|
+
import type { LifecycleDef, LifecycleRunRecord, CheckpointDecision, PhaseRecord } from "../lifecycle/lifecycle-types.ts";
|
|
23
|
+
import type { LifecycleRunDeps, CheckpointFn } from "../lifecycle/run-lifecycle.ts";
|
|
24
|
+
import { runLifecycle } from "../lifecycle/run-lifecycle.ts";
|
|
20
25
|
|
|
21
|
-
type View = "fleet" | "agents" | "backends";
|
|
26
|
+
type View = "fleet" | "lifecycle" | "agents" | "backends" | "scheduled";
|
|
22
27
|
|
|
23
28
|
export interface FleetPanelDeps {
|
|
24
29
|
registry: Map<string, AgentDef>;
|
|
@@ -28,6 +33,14 @@ export interface FleetPanelDeps {
|
|
|
28
33
|
backendRegistry: BackendRegistry; // SPEC-3: replaces childFactory
|
|
29
34
|
parentModel: { provider: string; id: string };
|
|
30
35
|
parentCwd: string;
|
|
36
|
+
/** SPEC-4: lifecycle registry + active/recent run records + deps to drive checkpoints. */
|
|
37
|
+
lifecycleRegistry: Map<string, LifecycleDef>;
|
|
38
|
+
lifecycleRuns: Map<string, LifecycleRunRecord>;
|
|
39
|
+
lifecycleDeps: Omit<LifecycleRunDeps, "spawn">;
|
|
40
|
+
/** SPEC-5a: scheduler for the scheduled tab. Optional — panel degrades to an empty list when absent. */
|
|
41
|
+
scheduler?: Scheduler;
|
|
42
|
+
/** SPEC-5a: live bg run status rows for the fleet tab. Optional. */
|
|
43
|
+
bgRuns?: BgRunsStore;
|
|
31
44
|
}
|
|
32
45
|
|
|
33
46
|
export interface FleetPanelOpts {
|
|
@@ -50,6 +63,27 @@ export class FleetPanel extends Container {
|
|
|
50
63
|
private linkPhase: "task" | "link" = "task";
|
|
51
64
|
private infoAgent: AgentDef | null = null;
|
|
52
65
|
private selectedBackend: Backend | null = null; // SPEC-3: Backends view i:Info
|
|
66
|
+
private selectedLifecycle: LifecycleRunRecord | null = null; // SPEC-4: Lifecycle view i:Info
|
|
67
|
+
// SPEC-4: Run-lifecycle inline input state
|
|
68
|
+
private lcRunMode = false;
|
|
69
|
+
private lcTaskInput: Input | null = null;
|
|
70
|
+
private lcNameInput: Input | null = null;
|
|
71
|
+
private lcPhase: "task" | "name" = "task";
|
|
72
|
+
// SPEC-4: pending checkpoint (interactive Continue/Revise/Abort)
|
|
73
|
+
private pendingCheckpoint: { phase: PhaseRecord; resolve: (d: CheckpointDecision) => void } | null = null;
|
|
74
|
+
private lcReviseInput: Input | null = null;
|
|
75
|
+
private lcRevising = false;
|
|
76
|
+
// SPEC-5a: scheduled tab — add-schedule inline input state + selected schedule for i:Info
|
|
77
|
+
private schedRunMode = false;
|
|
78
|
+
private schedTaskInput: Input | null = null;
|
|
79
|
+
private schedExprInput: Input | null = null;
|
|
80
|
+
private schedNameInput: Input | null = null;
|
|
81
|
+
private schedPhase: "task" | "expr" | "name" = "task";
|
|
82
|
+
private selectedSchedule: Schedule | null = null;
|
|
83
|
+
/** SPEC-5a proper-fix: store-change subscriptions — fired by RunRegistry + BgRunsStore
|
|
84
|
+
* so the panel re-renders the moment a (fore- or back-ground) run mutates, without a keypress. */
|
|
85
|
+
private readonly unsubs: (() => void)[] = [];
|
|
86
|
+
private closed = false; // SPEC-5a proper-fix: guard against double-close calling onDone twice
|
|
53
87
|
|
|
54
88
|
constructor(opts: FleetPanelOpts) {
|
|
55
89
|
super();
|
|
@@ -63,15 +97,25 @@ export class FleetPanel extends Container {
|
|
|
63
97
|
this.addChild(new Spacer(1));
|
|
64
98
|
this.list = this.buildList();
|
|
65
99
|
this.renderShell();
|
|
100
|
+
|
|
101
|
+
// SPEC-5a proper-fix: subscribe to run-registry + bg-runs mutations → live refresh.
|
|
102
|
+
// Covers both the model-invoked foreground case (spawnSubagent updates runRegistry)
|
|
103
|
+
// and the async/bg case (onProgress mutates BgRunsStore while the parent is idle).
|
|
104
|
+
this.unsubs.push(this.deps.runRegistry.subscribe(() => this.refresh()));
|
|
105
|
+
if (this.deps.bgRuns) this.unsubs.push(this.deps.bgRuns.subscribe(() => this.refresh()));
|
|
66
106
|
}
|
|
67
107
|
|
|
68
108
|
private buildList(): SelectList {
|
|
69
109
|
const items: SelectItem[] =
|
|
70
110
|
this.view === "fleet"
|
|
71
|
-
?
|
|
72
|
-
: this.view === "
|
|
73
|
-
? [...this.deps.
|
|
74
|
-
: this.
|
|
111
|
+
? buildFleetItems({ runRegistry: this.deps.runRegistry, bgRuns: this.deps.bgRuns })
|
|
112
|
+
: this.view === "lifecycle"
|
|
113
|
+
? [...this.deps.lifecycleRuns.values()].map((l: LifecycleRunRecord) => ({ value: l.runId, label: lifecycleRow(l) }))
|
|
114
|
+
: this.view === "agents"
|
|
115
|
+
? [...this.deps.registry.values()].map((a: AgentDef) => ({ value: a.name, label: agentsRow(a) }))
|
|
116
|
+
: this.view === "scheduled"
|
|
117
|
+
? (this.deps.scheduler?.list() ?? []).map((s: Schedule) => ({ value: s.id, label: scheduleRow(s) }))
|
|
118
|
+
: this.deps.backendRegistry.list().map((b: Backend) => ({ value: b.id, label: backendsRow(b) }));
|
|
75
119
|
const fresh = new SelectList(items, 12, {
|
|
76
120
|
selectedPrefix: (s: string) => this.theme.fg("accent", s),
|
|
77
121
|
selectedText: (s: string) => this.theme.fg("accent", s),
|
|
@@ -80,16 +124,36 @@ export class FleetPanel extends Container {
|
|
|
80
124
|
noMatch: (s: string) => this.theme.fg("warning", s),
|
|
81
125
|
});
|
|
82
126
|
fresh.onSelect = (item: SelectItem) => this.onSelect(item.value);
|
|
83
|
-
fresh.onCancel = () => this.
|
|
127
|
+
fresh.onCancel = () => this.close();
|
|
84
128
|
return fresh;
|
|
85
129
|
}
|
|
86
130
|
|
|
131
|
+
/** SPEC-5a proper-fix: re-pull from the run-registry + bg-runs stores and re-render.
|
|
132
|
+
* Called by the store-change subscriptions (constructor) on every mutation, so the
|
|
133
|
+
* fleet tab reflects completion/status changes without a keypress. Safe to call
|
|
134
|
+
* mid-overlay: renderShell preserves the active overlay (input/info/checkpoint). */
|
|
135
|
+
private refresh(): void {
|
|
136
|
+
this.list = this.buildList();
|
|
137
|
+
this.renderShell();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** SPEC-5a proper-fix: tear down store subscriptions then close the panel.
|
|
141
|
+
* Every exit path routes here so listeners never leak past the panel's lifetime.
|
|
142
|
+
* Idempotent — safe to call multiple times (esc + q + pi teardown). */
|
|
143
|
+
private close(): void {
|
|
144
|
+
if (this.closed) return;
|
|
145
|
+
this.closed = true;
|
|
146
|
+
for (const u of this.unsubs) u();
|
|
147
|
+
this.unsubs.length = 0;
|
|
148
|
+
this.onDone();
|
|
149
|
+
}
|
|
150
|
+
|
|
87
151
|
private renderShell(): void {
|
|
88
152
|
const keep = this.children.slice(0, 2);
|
|
89
153
|
this.children.length = 0;
|
|
90
154
|
this.children.push(...keep);
|
|
91
155
|
const accent = (s: string): string => this.theme.fg("accent", s);
|
|
92
|
-
const tabs = (["fleet", "agents", "backends"] as View[])
|
|
156
|
+
const tabs = (["fleet", "lifecycle", "agents", "backends", "scheduled"] as View[])
|
|
93
157
|
.map((v) => (v === this.view ? this.theme.fg("accent", this.theme.bold(`[${v}]`)) : this.theme.fg("dim", v)))
|
|
94
158
|
.join(" ");
|
|
95
159
|
this.addChild(new Text(accent(this.theme.bold(" FLEET")) + " " + tabs, 0, 0));
|
|
@@ -112,19 +176,66 @@ export class FleetPanel extends Container {
|
|
|
112
176
|
this.addChild(new Text(this.theme.fg("text", line), 0, 0));
|
|
113
177
|
}
|
|
114
178
|
this.addChild(new Text(this.theme.fg("dim", " esc:Back"), 0, 0));
|
|
179
|
+
} else if (this.selectedLifecycle) {
|
|
180
|
+
// SPEC-4: i:Info detail pane (lifecycle view) — phase timeline
|
|
181
|
+
this.addChild(new Text(this.theme.fg("dim", " ── lifecycle phases ──"), 0, 0));
|
|
182
|
+
for (const line of lifecyclePhaseTimeline(this.selectedLifecycle).split("\n")) {
|
|
183
|
+
this.addChild(new Text(this.theme.fg("text", line), 0, 0));
|
|
184
|
+
}
|
|
185
|
+
this.addChild(new Text(this.theme.fg("dim", " esc:Back"), 0, 0));
|
|
186
|
+
} else if (this.selectedSchedule) {
|
|
187
|
+
// SPEC-5a: i:Info detail pane (scheduled view)
|
|
188
|
+
this.addChild(new Text(this.theme.fg("dim", " ── schedule info ──"), 0, 0));
|
|
189
|
+
const s = this.selectedSchedule;
|
|
190
|
+
for (const line of [
|
|
191
|
+
`id: ${s.id}`,
|
|
192
|
+
`expression: ${s.expression}`,
|
|
193
|
+
`lifecycle: ${s.lifecycle}`,
|
|
194
|
+
`task: "${s.task}"`,
|
|
195
|
+
`paused: ${s.paused}`,
|
|
196
|
+
`nextFire: ${s.nextFire?.toLocaleString() ?? "(none)"}`,
|
|
197
|
+
]) this.addChild(new Text(this.theme.fg("text", line), 0, 0));
|
|
198
|
+
this.addChild(new Text(this.theme.fg("dim", " esc:Back"), 0, 0));
|
|
199
|
+
} else if (this.schedRunMode && (this.schedTaskInput || this.schedExprInput || this.schedNameInput)) {
|
|
200
|
+
const prompt = this.schedPhase === "task" ? " task> " : this.schedPhase === "expr" ? " schedule (cron | interval | one-shot ISO)> " : " lifecycle (blank=default)> ";
|
|
201
|
+
this.addChild(new Text(this.theme.fg("accent", prompt), 0, 0));
|
|
202
|
+
this.addChild(this.schedPhase === "task" ? this.schedTaskInput! : this.schedPhase === "expr" ? this.schedExprInput! : this.schedNameInput!);
|
|
203
|
+
this.addChild(new Text(this.theme.fg("dim", " enter submit • esc cancel"), 0, 0));
|
|
204
|
+
} else if (this.lcRunMode && (this.lcTaskInput || this.lcNameInput)) {
|
|
205
|
+
const prompt = this.lcPhase === "task" ? " task> " : " lifecycle name (blank=default)> ";
|
|
206
|
+
this.addChild(new Text(this.theme.fg("accent", prompt), 0, 0));
|
|
207
|
+
this.addChild(this.lcPhase === "task" ? this.lcTaskInput! : this.lcNameInput!);
|
|
208
|
+
this.addChild(new Text(this.theme.fg("dim", " enter submit • esc cancel"), 0, 0));
|
|
209
|
+
} else if (this.pendingCheckpoint && !this.lcRevising) {
|
|
210
|
+
const pc = this.pendingCheckpoint;
|
|
211
|
+
this.addChild(new Text(this.theme.fg("dim", ` ── checkpoint: phase '${pc.phase.name}' (${pc.phase.status}) ──`), 0, 0));
|
|
212
|
+
this.addChild(new Text(this.theme.fg("text", ` ${pc.phase.summary.slice(0, 200)}`), 0, 0));
|
|
213
|
+
this.addChild(new Text(this.theme.fg("dim", " c:Continue v:Revise a:Abort"), 0, 0));
|
|
214
|
+
} else if (this.lcRevising && this.lcReviseInput) {
|
|
215
|
+
this.addChild(new Text(this.theme.fg("accent", " revise feedback> "), 0, 0));
|
|
216
|
+
this.addChild(this.lcReviseInput);
|
|
217
|
+
this.addChild(new Text(this.theme.fg("dim", " enter submit • esc cancel"), 0, 0));
|
|
115
218
|
} else {
|
|
116
219
|
this.addChild(this.list);
|
|
117
220
|
}
|
|
118
221
|
|
|
119
222
|
this.addChild(new Spacer(1));
|
|
120
223
|
const hint =
|
|
121
|
-
this.infoAgent || this.selectedBackend
|
|
224
|
+
this.infoAgent || this.selectedBackend || this.selectedLifecycle || this.selectedSchedule
|
|
122
225
|
? " esc:Back"
|
|
123
|
-
: this.
|
|
124
|
-
? "
|
|
125
|
-
: this.
|
|
126
|
-
? "
|
|
127
|
-
: "
|
|
226
|
+
: this.pendingCheckpoint
|
|
227
|
+
? " c:Continue v:Revise a:Abort"
|
|
228
|
+
: this.lcRevising
|
|
229
|
+
? " enter:Submit-feedback esc:Cancel"
|
|
230
|
+
: this.view === "fleet"
|
|
231
|
+
? " r:Run-new s:Stop o:Open-todo tab:Lifecycle q:Quit"
|
|
232
|
+
: this.view === "lifecycle"
|
|
233
|
+
? " r:Run-lifecycle i:Info tab:Agents q:Quit"
|
|
234
|
+
: this.view === "agents"
|
|
235
|
+
? " r:Run e:Edit i:Info d:Reload tab:Backends q:Quit"
|
|
236
|
+
: this.view === "scheduled"
|
|
237
|
+
? " a:Add p:Pause/resume d:Delete i:Info tab:Fleet q:Quit"
|
|
238
|
+
: " r:Refresh i:Info tab:Fleet q:Quit";
|
|
128
239
|
this.addChild(new Text(this.theme.fg("dim", hint), 0, 0));
|
|
129
240
|
this.addChild(new Spacer(1));
|
|
130
241
|
this.addChild(new DynamicBorder(accent));
|
|
@@ -190,8 +301,13 @@ export class FleetPanel extends Container {
|
|
|
190
301
|
}
|
|
191
302
|
|
|
192
303
|
private switchView(): void {
|
|
193
|
-
this.view = this.view === "fleet" ? "
|
|
304
|
+
this.view = this.view === "fleet" ? "lifecycle"
|
|
305
|
+
: this.view === "lifecycle" ? "agents"
|
|
306
|
+
: this.view === "agents" ? "backends"
|
|
307
|
+
: this.view === "backends" ? "scheduled" : "fleet";
|
|
194
308
|
this.selectedBackend = null;
|
|
309
|
+
this.selectedLifecycle = null;
|
|
310
|
+
this.selectedSchedule = null;
|
|
195
311
|
this.list = this.buildList();
|
|
196
312
|
this.renderShell();
|
|
197
313
|
}
|
|
@@ -205,15 +321,44 @@ export class FleetPanel extends Container {
|
|
|
205
321
|
if (matchesKey(data, "escape")) { this.selectedBackend = null; this.renderShell(); }
|
|
206
322
|
return;
|
|
207
323
|
}
|
|
324
|
+
if (this.selectedLifecycle) {
|
|
325
|
+
if (matchesKey(data, "escape")) { this.selectedLifecycle = null; this.renderShell(); }
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
if (this.selectedSchedule) {
|
|
329
|
+
if (matchesKey(data, "escape")) { this.selectedSchedule = null; this.renderShell(); }
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
if (this.schedRunMode && (this.schedTaskInput || this.schedExprInput || this.schedNameInput)) {
|
|
333
|
+
if (matchesKey(data, "escape")) { this.cancelScheduleAdd(); return; }
|
|
334
|
+
(this.schedPhase === "task" ? this.schedTaskInput! : this.schedPhase === "expr" ? this.schedExprInput! : this.schedNameInput!).handleInput(data);
|
|
335
|
+
this.invalidate();
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
if (this.lcRunMode && (this.lcTaskInput || this.lcNameInput)) {
|
|
339
|
+
if (matchesKey(data, "escape")) { this.cancelLifecycleRun(); return; }
|
|
340
|
+
(this.lcPhase === "task" ? this.lcTaskInput! : this.lcNameInput!).handleInput(data);
|
|
341
|
+
this.invalidate();
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
208
344
|
if (this.runMode && (this.taskInput || this.linkInput)) {
|
|
209
345
|
if (matchesKey(data, "escape")) { this.cancelRun(); return; }
|
|
210
346
|
(this.linkPhase === "task" ? this.taskInput! : this.linkInput!).handleInput(data);
|
|
211
347
|
this.invalidate();
|
|
212
348
|
return;
|
|
213
349
|
}
|
|
214
|
-
if (matchesKey(data, "escape")) {
|
|
350
|
+
if (matchesKey(data, "escape")) {
|
|
351
|
+
// SPEC-4: if a lifecycle checkpoint is pending, resolve it as abort so runLifecycle
|
|
352
|
+
// doesn't hang + the lifecycle TODO is reverted (not orphaned) when the panel closes.
|
|
353
|
+
if (this.pendingCheckpoint) {
|
|
354
|
+
this.pendingCheckpoint.resolve({ action: "abort" });
|
|
355
|
+
this.pendingCheckpoint = null;
|
|
356
|
+
}
|
|
357
|
+
this.close();
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
215
360
|
if (matchesKey(data, "tab")) { this.switchView(); return; }
|
|
216
|
-
if (matchesKey(data, "q")) { this.
|
|
361
|
+
if (matchesKey(data, "q")) { this.close(); return; }
|
|
217
362
|
if (matchesKey(data, "r") && this.view === "agents") {
|
|
218
363
|
const sel = this.list.getSelectedItem();
|
|
219
364
|
if (sel) this.startRun(sel.value);
|
|
@@ -234,9 +379,180 @@ export class FleetPanel extends Container {
|
|
|
234
379
|
this.renderShell();
|
|
235
380
|
return;
|
|
236
381
|
}
|
|
382
|
+
// SPEC-4: Lifecycle view — i:Info + r:Run-lifecycle
|
|
383
|
+
if (matchesKey(data, "i") && this.view === "lifecycle") {
|
|
384
|
+
const sel = this.list.getSelectedItem();
|
|
385
|
+
if (sel) { this.selectedLifecycle = this.deps.lifecycleRuns.get(sel.value) ?? null; this.renderShell(); }
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
if (matchesKey(data, "r") && this.view === "lifecycle") {
|
|
389
|
+
this.startLifecycleRun();
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
// SPEC-5a: scheduled view — a:Add p:Pause/resume d:Delete i:Info
|
|
393
|
+
if (this.view === "scheduled" && this.deps.scheduler) {
|
|
394
|
+
if (matchesKey(data, "a")) { this.startScheduleAdd(); return; }
|
|
395
|
+
if (matchesKey(data, "i")) {
|
|
396
|
+
const sel = this.list.getSelectedItem();
|
|
397
|
+
if (sel) { this.selectedSchedule = this.deps.scheduler.list().find((x) => x.id === sel.value) ?? null; this.renderShell(); }
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
if (matchesKey(data, "p")) {
|
|
401
|
+
const sel = this.list.getSelectedItem();
|
|
402
|
+
if (sel) {
|
|
403
|
+
const s = this.deps.scheduler.list().find((x) => x.id === sel.value);
|
|
404
|
+
if (s) { s.paused ? this.deps.scheduler.resume(sel.value) : this.deps.scheduler.pause(sel.value); this.list = this.buildList(); this.renderShell(); }
|
|
405
|
+
}
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
if (matchesKey(data, "d")) {
|
|
409
|
+
const sel = this.list.getSelectedItem();
|
|
410
|
+
if (sel) { this.deps.scheduler.delete(sel.value); this.list = this.buildList(); this.renderShell(); }
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
// SPEC-4: pending checkpoint keys (c/v/a)
|
|
415
|
+
if (this.pendingCheckpoint && !this.lcRevising) {
|
|
416
|
+
if (matchesKey(data, "c")) { this.pendingCheckpoint.resolve({ action: "continue" }); this.pendingCheckpoint = null; this.renderShell(); return; }
|
|
417
|
+
if (matchesKey(data, "a")) { this.pendingCheckpoint!.resolve({ action: "abort" }); this.pendingCheckpoint = null; this.renderShell(); return; }
|
|
418
|
+
if (matchesKey(data, "v")) {
|
|
419
|
+
this.lcRevising = true;
|
|
420
|
+
this.lcReviseInput = new Input();
|
|
421
|
+
this.lcReviseInput.onSubmit = (fb: string) => {
|
|
422
|
+
this.lcRevising = false;
|
|
423
|
+
this.lcReviseInput = null;
|
|
424
|
+
this.pendingCheckpoint!.resolve({ action: "revise", feedback: fb });
|
|
425
|
+
this.pendingCheckpoint = null;
|
|
426
|
+
this.renderShell();
|
|
427
|
+
};
|
|
428
|
+
this.lcReviseInput.onEscape = () => { this.lcRevising = false; this.lcReviseInput = null; this.renderShell(); };
|
|
429
|
+
this.renderShell();
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
if (this.lcRevising && this.lcReviseInput) {
|
|
434
|
+
if (matchesKey(data, "escape")) { this.lcRevising = false; this.lcReviseInput = null; this.renderShell(); return; }
|
|
435
|
+
this.lcReviseInput.handleInput(data);
|
|
436
|
+
this.invalidate();
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
237
439
|
this.list.handleInput(data);
|
|
238
440
|
this.invalidate();
|
|
239
441
|
}
|
|
442
|
+
|
|
443
|
+
/** SPEC-5a: open the Add-schedule inline inputs (task → expression → lifecycle name → register). */
|
|
444
|
+
private startScheduleAdd(): void {
|
|
445
|
+
this.schedPhase = "task";
|
|
446
|
+
this.schedTaskInput = new Input();
|
|
447
|
+
this.schedTaskInput.onSubmit = (task: string) => {
|
|
448
|
+
if (!task.trim()) { this.cancelScheduleAdd(); return; }
|
|
449
|
+
this.schedPhase = "expr";
|
|
450
|
+
this.schedExprInput = new Input();
|
|
451
|
+
this.schedExprInput.onSubmit = (expr: string) => {
|
|
452
|
+
if (!expr.trim()) { this.cancelScheduleAdd(); return; }
|
|
453
|
+
this.schedPhase = "name";
|
|
454
|
+
this.schedNameInput = new Input();
|
|
455
|
+
this.schedNameInput.onSubmit = (name: string) => {
|
|
456
|
+
const lcName = name.trim() || "default";
|
|
457
|
+
this.executeScheduleAdd(task.trim(), expr.trim(), lcName);
|
|
458
|
+
};
|
|
459
|
+
this.schedNameInput.onEscape = () => { this.executeScheduleAdd(task.trim(), expr.trim(), "default"); };
|
|
460
|
+
this.renderShell();
|
|
461
|
+
};
|
|
462
|
+
this.schedExprInput.onEscape = () => this.cancelScheduleAdd();
|
|
463
|
+
this.renderShell();
|
|
464
|
+
};
|
|
465
|
+
this.schedTaskInput.onEscape = () => this.cancelScheduleAdd();
|
|
466
|
+
this.schedRunMode = true;
|
|
467
|
+
this.renderShell();
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
private cancelScheduleAdd(): void {
|
|
471
|
+
this.schedRunMode = false;
|
|
472
|
+
this.schedTaskInput = null;
|
|
473
|
+
this.schedExprInput = null;
|
|
474
|
+
this.schedNameInput = null;
|
|
475
|
+
this.renderShell();
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
private executeScheduleAdd(task: string, expression: string, lifecycleName: string): void {
|
|
479
|
+
this.schedRunMode = false;
|
|
480
|
+
this.schedTaskInput = null;
|
|
481
|
+
this.schedExprInput = null;
|
|
482
|
+
this.schedNameInput = null;
|
|
483
|
+
if (!this.deps.scheduler) { this.onNotify("scheduling not configured", "error"); this.renderShell(); return; }
|
|
484
|
+
try {
|
|
485
|
+
const id = this.deps.scheduler.register({ task, expression, lifecycle: lifecycleName, auto: true });
|
|
486
|
+
this.list = this.buildList();
|
|
487
|
+
this.renderShell();
|
|
488
|
+
const entry = this.deps.scheduler.list().find((s) => s.id === id);
|
|
489
|
+
this.onNotify(`scheduled: ${id} · next fire: ${entry?.nextFire?.toLocaleString() ?? "(paused)"}`, "info");
|
|
490
|
+
} catch (e) {
|
|
491
|
+
this.onNotify(`schedule register failed: ${(e as Error).message}`, "error");
|
|
492
|
+
}
|
|
493
|
+
this.renderShell();
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/** SPEC-4: open the Run-lifecycle inline inputs (task → lifecycle name → start runLifecycle). */
|
|
497
|
+
private startLifecycleRun(): void {
|
|
498
|
+
this.lcPhase = "task";
|
|
499
|
+
this.lcTaskInput = new Input();
|
|
500
|
+
this.lcTaskInput.onSubmit = (task: string) => {
|
|
501
|
+
if (!task.trim()) { this.cancelLifecycleRun(); return; }
|
|
502
|
+
this.lcPhase = "name";
|
|
503
|
+
this.lcNameInput = new Input();
|
|
504
|
+
this.lcNameInput.onSubmit = (name: string) => {
|
|
505
|
+
const lcName = name.trim() || "default";
|
|
506
|
+
void this.executeLifecycleRun(task.trim(), lcName);
|
|
507
|
+
};
|
|
508
|
+
this.lcNameInput.onEscape = () => { void this.executeLifecycleRun(task.trim(), "default"); };
|
|
509
|
+
this.renderShell();
|
|
510
|
+
};
|
|
511
|
+
this.lcTaskInput.onEscape = () => this.cancelLifecycleRun();
|
|
512
|
+
this.lcRunMode = true;
|
|
513
|
+
this.renderShell();
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
private cancelLifecycleRun(): void {
|
|
517
|
+
this.lcRunMode = false;
|
|
518
|
+
this.lcTaskInput = null;
|
|
519
|
+
this.lcNameInput = null;
|
|
520
|
+
this.renderShell();
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
private async executeLifecycleRun(task: string, lifecycleName: string): Promise<void> {
|
|
524
|
+
this.lcRunMode = false;
|
|
525
|
+
this.lcTaskInput = null;
|
|
526
|
+
this.lcNameInput = null;
|
|
527
|
+
this.renderShell();
|
|
528
|
+
if (!this.deps.lifecycleRegistry.has(lifecycleName)) {
|
|
529
|
+
this.onNotify(`lifecycle '${lifecycleName}' not found; available: ${[...this.deps.lifecycleRegistry.keys()].sort().join(", ")}`, "error");
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
const onCheckpoint: CheckpointFn = (phase) => new Promise<CheckpointDecision>((resolve) => {
|
|
533
|
+
this.pendingCheckpoint = { phase, resolve };
|
|
534
|
+
this.renderShell();
|
|
535
|
+
});
|
|
536
|
+
const lifecycleFullDeps: LifecycleRunDeps = {
|
|
537
|
+
...this.deps.lifecycleDeps,
|
|
538
|
+
spawn: async (o) => {
|
|
539
|
+
const { spawnSubagent } = await import("../engine/spawnSubagent.ts");
|
|
540
|
+
return spawnSubagent({
|
|
541
|
+
agent: o.agent, task: o.task, lifecycleTodoId: o.lifecycleTodoId, model: o.model,
|
|
542
|
+
skillsOverride: o.skills, backendOverride: o.backend,
|
|
543
|
+
registry: this.deps.registry, todoSync: this.deps.todoSync, runRegistry: this.deps.runRegistry, lock: this.deps.lock,
|
|
544
|
+
backendRegistry: this.deps.backendRegistry, parentModel: this.deps.parentModel, parentCwd: this.deps.parentCwd,
|
|
545
|
+
});
|
|
546
|
+
},
|
|
547
|
+
};
|
|
548
|
+
const res = await runLifecycle(task, lifecycleName, { deps: lifecycleFullDeps, mode: "checkpointed", onCheckpoint });
|
|
549
|
+
this.pendingCheckpoint = null;
|
|
550
|
+
// record the run so the Lifecycle view shows it
|
|
551
|
+
this.deps.lifecycleRuns.set(res.runId, res);
|
|
552
|
+
this.list = this.buildList();
|
|
553
|
+
this.renderShell();
|
|
554
|
+
this.onNotify(`lifecycle ${res.status}: ${res.runId}${res.error ? " — " + res.error : ""}`, res.status === "completed" ? "info" : "warning");
|
|
555
|
+
}
|
|
240
556
|
}
|
|
241
557
|
|
|
242
558
|
/** Factory used by src/index.ts to open the panel via ctx.ui.custom. */
|
package/src/panel/rows.ts
CHANGED
|
@@ -82,3 +82,93 @@ 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
|
+
|
|
89
|
+
// SPEC-5a §11 — bg run row status (Q8=A). The fleet tab gains live status icons + phase progress
|
|
90
|
+
// for async/bg runs; foreground rows are unchanged.
|
|
91
|
+
export type BgStatus = "running" | "paused" | "completed" | "failed" | "queued";
|
|
92
|
+
|
|
93
|
+
export interface BgRunStatus {
|
|
94
|
+
runId: string;
|
|
95
|
+
lifecycle: string;
|
|
96
|
+
status: BgStatus;
|
|
97
|
+
phase: string;
|
|
98
|
+
phaseIndex: number;
|
|
99
|
+
phaseTotal: number;
|
|
100
|
+
mode: "auto" | "checkpointed";
|
|
101
|
+
backend: string;
|
|
102
|
+
task: string;
|
|
103
|
+
branch?: string;
|
|
104
|
+
elapsedMs?: number;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function bgStatusIcon(s: BgStatus): string {
|
|
108
|
+
switch (s) {
|
|
109
|
+
case "running": return "▶";
|
|
110
|
+
case "paused": return "⏸";
|
|
111
|
+
case "completed": return "✓";
|
|
112
|
+
case "failed": return "✗";
|
|
113
|
+
case "queued": return "⏳";
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function renderBgRow(r: BgRunStatus): string {
|
|
118
|
+
const icon = bgStatusIcon(r.status);
|
|
119
|
+
const phase = r.phase ? `●${r.phase} ${r.phaseIndex}/${r.phaseTotal}` : `${r.phaseIndex}/${r.phaseTotal}`;
|
|
120
|
+
const branch = r.branch ? ` ${r.branch}` : "";
|
|
121
|
+
const elapsed = r.elapsedMs ? ` ${fmtDuration(r.elapsedMs)}` : "";
|
|
122
|
+
const task = r.task.length > 30 ? r.task.slice(0, 29) + "…" : r.task;
|
|
123
|
+
return `${icon} ${r.runId} ${r.lifecycle} ${phase} ${r.mode}${elapsed} ${r.backend}${branch} "${task}"`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// SPEC-5a §11 — scheduled tab row rendering.
|
|
127
|
+
export interface ScheduleRow {
|
|
128
|
+
id: string;
|
|
129
|
+
expression: string;
|
|
130
|
+
lifecycle?: string;
|
|
131
|
+
task: string;
|
|
132
|
+
nextFire: Date | null;
|
|
133
|
+
paused: boolean;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function scheduleRow(s: ScheduleRow): string {
|
|
137
|
+
const icon = s.paused ? "⏸" : "▶";
|
|
138
|
+
const next = s.nextFire ? `next: ${s.nextFire.toLocaleString()}` : "paused";
|
|
139
|
+
const task = s.task.length > 24 ? s.task.slice(0, 23) + "…" : s.task;
|
|
140
|
+
const lc = s.lifecycle ?? "default";
|
|
141
|
+
return `${icon} ${s.expression} ${lc} "${task}" ${next} ${s.id}`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const LC_GLYPH: Record<LifecycleStatus, string> = {
|
|
145
|
+
running: "▶", checkpoint: "⏸", completed: "✓", failed: "✗", aborted: "✗",
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
export function lifecycleRow(r: LifecycleRunRecord): string {
|
|
149
|
+
const dur = r.endedAt ? fmtDuration(r.endedAt - r.startedAt) : "—";
|
|
150
|
+
const curIdx = r.phases.findIndex((p) => p.status === "running");
|
|
151
|
+
const cur = curIdx >= 0 ? r.phases[curIdx] : r.phases[r.phases.length - 1];
|
|
152
|
+
const curName = cur ? `●${cur.name}` : "—";
|
|
153
|
+
// N/M = current phase position / total (1-indexed); falls back to last phase when none running.
|
|
154
|
+
const counts = `${(curIdx >= 0 ? curIdx + 1 : r.phases.length)}/${r.phases.length}`;
|
|
155
|
+
return `${LC_GLYPH[r.status]} ${r.runId} ${r.lifecycleName} ${curName} ${counts} ${r.mode} ${dur} ${r.backend} "${r.task}"`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function lifecyclePhaseTimeline(r: LifecycleRunRecord): string {
|
|
159
|
+
const lines: string[] = [
|
|
160
|
+
`Lifecycle ${r.runId} — ${r.lifecycleName} — "${r.task}"`,
|
|
161
|
+
`Backend: ${r.backend} · Mode: ${r.mode} · Status: ${r.status}`,
|
|
162
|
+
"",
|
|
163
|
+
"Phases:",
|
|
164
|
+
];
|
|
165
|
+
for (const p of r.phases) {
|
|
166
|
+
const mark = p.reviseCount > 0 ? "[~]" : p.status === "completed" ? "[x]" : "[ ]";
|
|
167
|
+
const art = p.paths.length ? ` → ${p.paths.join(", ")}` : "";
|
|
168
|
+
lines.push(` ${mark} ${p.name} ${p.status}${art}${p.paths.length ? " [Open]" : ""}`);
|
|
169
|
+
}
|
|
170
|
+
if (r.status === "checkpoint") {
|
|
171
|
+
lines.push("", "── Checkpoint ──", "[Continue] [Revise] [Abort]");
|
|
172
|
+
}
|
|
173
|
+
return lines.join("\n");
|
|
174
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// src/runtime/async-runner.ts
|
|
2
|
+
// SPEC-5a §2/§6/§7/§8/§10 — the async/bg path. Layers ABOVE the unchanged runLifecycle:
|
|
3
|
+
// creates a worktree, journals events, drives runLifecycle with the worktree cwd, discovers
|
|
4
|
+
// artifacts via DiffService, commits on completion, pushes to the inbox, notifies.
|
|
5
|
+
import type { WorktreeService } from "../worktree/worktree-service.ts";
|
|
6
|
+
import type { DiffService } from "../worktree/diff-service.ts";
|
|
7
|
+
import type { RunJournal, JournalEvent } from "./run-journal.ts";
|
|
8
|
+
import type { ConcurrencyPool } from "./concurrency-pool.ts";
|
|
9
|
+
import type { ResultsInbox, RunResult } from "./results-inbox.ts";
|
|
10
|
+
import { execSync } from "node:child_process";
|
|
11
|
+
|
|
12
|
+
// Thin shape of the LifecycleRunResult we need (avoids importing the full type here — the
|
|
13
|
+
// real adapter in index.ts maps the full LifecycleRunResult to this shape).
|
|
14
|
+
export interface FakeLifecycleResult {
|
|
15
|
+
runId: string;
|
|
16
|
+
lifecycleName: string;
|
|
17
|
+
task: string;
|
|
18
|
+
status: "completed" | "failed" | "aborted";
|
|
19
|
+
phases: Array<{ name: string; status: string; summary: string; paths: string[]; reviseCount: number }>;
|
|
20
|
+
todoId: string | null;
|
|
21
|
+
error?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface RunLifecycleOpts {
|
|
25
|
+
runId: string;
|
|
26
|
+
worktreePath: string;
|
|
27
|
+
branch: string;
|
|
28
|
+
mode: "auto" | "checkpointed";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type RunLifecycleFn = (task: string, lifecycleName: string, opts: RunLifecycleOpts) => Promise<FakeLifecycleResult>;
|
|
32
|
+
|
|
33
|
+
export interface AsyncRunnerDeps {
|
|
34
|
+
worktree: WorktreeService;
|
|
35
|
+
diff: DiffService;
|
|
36
|
+
journal: RunJournal;
|
|
37
|
+
pool: ConcurrencyPool;
|
|
38
|
+
inbox: ResultsInbox;
|
|
39
|
+
runLifecycle: RunLifecycleFn;
|
|
40
|
+
notify: (msg: string, level?: "info" | "warning" | "error") => void;
|
|
41
|
+
genRunId: () => string;
|
|
42
|
+
/** SPEC-5a: called at each run/phase transition so the host (index.ts) can update the live bgRuns map. */
|
|
43
|
+
onProgress?: (runId: string, status: import("../panel/rows.ts").BgRunStatus) => void;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface RunBackgroundOpts {
|
|
47
|
+
deps: AsyncRunnerDeps;
|
|
48
|
+
lifecycle: string;
|
|
49
|
+
mode: "auto" | "checkpointed";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface RunBackgroundHandle {
|
|
53
|
+
runId: string;
|
|
54
|
+
status: "background";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function emitProgress(deps: AsyncRunnerDeps, runId: string, partial: Partial<import("../panel/rows.ts").BgRunStatus> & { status: import("../panel/rows.ts").BgStatus; phase: string; phaseIndex: number; phaseTotal: number }): void {
|
|
58
|
+
if (!deps.onProgress) return;
|
|
59
|
+
deps.onProgress(runId, {
|
|
60
|
+
runId,
|
|
61
|
+
lifecycle: "",
|
|
62
|
+
mode: "auto",
|
|
63
|
+
backend: "pi",
|
|
64
|
+
task: "",
|
|
65
|
+
...partial,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function sh(cmd: string, cwd: string): void {
|
|
70
|
+
execSync(cmd, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function runBackground(task: string, opts: RunBackgroundOpts): RunBackgroundHandle {
|
|
74
|
+
const { deps } = opts;
|
|
75
|
+
const runId = deps.genRunId();
|
|
76
|
+
const baseRef = "HEAD";
|
|
77
|
+
|
|
78
|
+
// Fire-and-forget: the pool gates concurrency; the journal records the run.
|
|
79
|
+
void deps.pool.withSlot(async () => {
|
|
80
|
+
let wt: { path: string; branch: string } | null = null;
|
|
81
|
+
try {
|
|
82
|
+
wt = deps.worktree.create(runId, baseRef);
|
|
83
|
+
const ev0: JournalEvent = { type: "run:started", runId, task, lifecycle: opts.lifecycle, worktree: { path: wt.path, branch: wt.branch }, mode: opts.mode, ts: Date.now() };
|
|
84
|
+
deps.journal.append(runId, ev0);
|
|
85
|
+
emitProgress(deps, runId, { status: "running", phase: "", phaseIndex: 0, phaseTotal: 0, lifecycle: opts.lifecycle, mode: opts.mode, task });
|
|
86
|
+
|
|
87
|
+
const res = await deps.runLifecycle(task, opts.lifecycle, { runId, worktreePath: wt.path, branch: wt.branch, mode: opts.mode });
|
|
88
|
+
|
|
89
|
+
if (res.status === "completed") {
|
|
90
|
+
// commit the worktree to the branch (lifecycle finish phase or single-delegate completion)
|
|
91
|
+
try { sh("git add -A && git commit -m 'fleet run complete'", wt.path); } catch { /* nothing to commit */ }
|
|
92
|
+
deps.journal.append(runId, { type: "run:completed", runId, branch: wt.branch, ts: Date.now() });
|
|
93
|
+
const total = res.phases.length;
|
|
94
|
+
const lastIdx = total; // completed = past the last phase
|
|
95
|
+
emitProgress(deps, runId, { status: "completed", phase: res.phases[total - 1]?.name ?? "finish", phaseIndex: lastIdx, phaseTotal: total, lifecycle: opts.lifecycle, mode: opts.mode, task, branch: wt.branch });
|
|
96
|
+
const lastPhase = res.phases[res.phases.length - 1];
|
|
97
|
+
const result: RunResult = {
|
|
98
|
+
runId, task, status: "completed",
|
|
99
|
+
summary: lastPhase?.summary ?? "",
|
|
100
|
+
paths: res.phases.flatMap((p) => p.paths),
|
|
101
|
+
branch: wt.branch, completedAt: Date.now(),
|
|
102
|
+
};
|
|
103
|
+
deps.inbox.push(result);
|
|
104
|
+
deps.notify(`fleet run ${runId} completed`, "info");
|
|
105
|
+
// SPEC-5a: the worktree dir is temporary scaffolding; remove it but keep the branch for merge/inspection.
|
|
106
|
+
deps.worktree.removeWorktree(runId);
|
|
107
|
+
} else {
|
|
108
|
+
deps.journal.append(runId, { type: "run:aborted", runId, reason: res.error ?? res.status, ts: Date.now() });
|
|
109
|
+
deps.worktree.remove(runId);
|
|
110
|
+
emitProgress(deps, runId, { status: "failed", phase: "", phaseIndex: 0, phaseTotal: res.phases.length, lifecycle: opts.lifecycle, mode: opts.mode, task });
|
|
111
|
+
deps.notify(`fleet run ${runId} ${res.status}: ${res.error ?? ""}`, "warning");
|
|
112
|
+
}
|
|
113
|
+
} catch (e) {
|
|
114
|
+
const msg = (e as Error).message;
|
|
115
|
+
deps.journal.append(runId, { type: "run:aborted", runId, reason: msg, ts: Date.now() });
|
|
116
|
+
if (wt) deps.worktree.remove(runId);
|
|
117
|
+
deps.notify(`fleet run ${runId} failed: ${msg}`, "error");
|
|
118
|
+
emitProgress(deps, runId, { status: "failed", phase: "", phaseIndex: 0, phaseTotal: 0, lifecycle: opts.lifecycle, mode: opts.mode, task });
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
return { runId, status: "background" };
|
|
123
|
+
}
|