@getpipher/armory-fleet 0.5.2 → 0.6.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 +2 -2
- package/src/engine/run-registry.ts +4 -0
- package/src/engine/spawnSubagent.ts +36 -1
- package/src/index.ts +13 -1
- package/src/panel/fleet-panel.ts +161 -9
- package/src/panel/runs-index.ts +8 -0
- package/src/panel/runs-rows.ts +29 -0
- package/src/runtime/reconcile.ts +29 -0
- package/src/runtime/run-log.ts +124 -0
- package/src/tools/subagent.ts +4 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getpipher/armory-fleet",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "The armory suite's subagent orchestrator for the pi coding agent — a cross-harness, superpowers-native fleet where every agent is armory-native from birth.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -60,6 +60,6 @@
|
|
|
60
60
|
},
|
|
61
61
|
"scripts": {
|
|
62
62
|
"typecheck": "tsc --noEmit",
|
|
63
|
-
"test:run": "node --import tsx --test test/*.test.mts"
|
|
63
|
+
"test:run": "node --import tsx --test --test-timeout=30000 test/*.test.mts"
|
|
64
64
|
}
|
|
65
65
|
}
|
|
@@ -16,6 +16,10 @@ export interface RunRecord {
|
|
|
16
16
|
backendSessionId?: string | null;
|
|
17
17
|
/** The sessionKey whose resume this run belongs to (SPEC-3). */
|
|
18
18
|
sessionKey?: string | null;
|
|
19
|
+
/** SPEC-5b-1: runId this run resumed from (rehydrated the prior sessionKey + a follow-up). */
|
|
20
|
+
resumedFrom?: string;
|
|
21
|
+
/** SPEC-5b-1: runId this run forked from (fresh re-run with same agent+task). */
|
|
22
|
+
forkedFrom?: string;
|
|
19
23
|
}
|
|
20
24
|
|
|
21
25
|
/** runId format: fl-<base36 ms>-<6 random> (SPEC-1 §5.1). */
|
|
@@ -7,6 +7,8 @@ import type { BackendRegistry } from "../backend/port.ts";
|
|
|
7
7
|
import { genRunId, RunRegistry } from "./run-registry.ts";
|
|
8
8
|
import { createTurnBudget, DEFAULT_MAX_TURNS } from "./turn-budget.ts";
|
|
9
9
|
import type { SingleSlotLock } from "./concurrency-lock.ts";
|
|
10
|
+
import type { RunLog } from "../runtime/run-log.ts";
|
|
11
|
+
import { buildToolEvent } from "../runtime/run-log.ts";
|
|
10
12
|
|
|
11
13
|
const PI_DEFAULT_TOOLS = ["read", "bash", "edit", "write"];
|
|
12
14
|
|
|
@@ -81,6 +83,13 @@ export interface SpawnOptions {
|
|
|
81
83
|
* (Q4=C) instead of the agent's `backend`. */
|
|
82
84
|
skillsOverride?: string[];
|
|
83
85
|
backendOverride?: "pi" | "claude";
|
|
86
|
+
/** SPEC-5b-1: per-run conversation journal. When set, spawnSubagent writes run:meta →
|
|
87
|
+
* message/tool → run:ended events. Absent = no journal (unit tests stay clean). */
|
|
88
|
+
runLog?: RunLog;
|
|
89
|
+
/** SPEC-5b-1: when set, the new run is a resume of this prior runId (written to run:ended + RunRecord.resumedFrom). */
|
|
90
|
+
resumeLink?: string;
|
|
91
|
+
/** SPEC-5b-1: when set, the new run is a fork of this prior runId (written to run:ended + RunRecord.forkedFrom). */
|
|
92
|
+
forkLink?: string;
|
|
84
93
|
}
|
|
85
94
|
|
|
86
95
|
export interface SpawnResult {
|
|
@@ -145,6 +154,9 @@ export async function spawnSubagent(opts: SpawnOptions): Promise<SpawnResult> {
|
|
|
145
154
|
runId, agent: agentDef.name, model, task: opts.task, track,
|
|
146
155
|
todoId: null, status: "running", startedAt,
|
|
147
156
|
});
|
|
157
|
+
try {
|
|
158
|
+
opts.runLog?.append(runId, { type: "run:meta", runId, agent: agentDef.name, model, task: opts.task, startedAt, track, todoId: null });
|
|
159
|
+
} catch { /* best-effort: journal is the index, not the product */ }
|
|
148
160
|
|
|
149
161
|
// todo-sync (before) — only when both caller tracks AND agent allows todoSync
|
|
150
162
|
let priorStatus: string | undefined;
|
|
@@ -179,6 +191,7 @@ export async function spawnSubagent(opts: SpawnOptions): Promise<SpawnResult> {
|
|
|
179
191
|
let finalText = "";
|
|
180
192
|
let tokenTotal = 0;
|
|
181
193
|
let aborted = false;
|
|
194
|
+
let turnIdx = -1;
|
|
182
195
|
|
|
183
196
|
const onSignalAbort = (): void => { aborted = true; void session.abort(); };
|
|
184
197
|
opts.signal?.addEventListener("abort", onSignalAbort);
|
|
@@ -186,6 +199,11 @@ export async function spawnSubagent(opts: SpawnOptions): Promise<SpawnResult> {
|
|
|
186
199
|
const unsub = session.subscribe((e) => {
|
|
187
200
|
if (e.type === "session_init" && e.backendSessionId) {
|
|
188
201
|
opts.runRegistry.update(runId, { backendSessionId: e.backendSessionId, sessionKey: agentDef.sessionKey });
|
|
202
|
+
try {
|
|
203
|
+
opts.runLog?.append(runId, { type: "run:meta", runId, agent: agentDef.name, model, task: opts.task, startedAt, track, todoId, backendSessionId: e.backendSessionId, sessionKey: agentDef.sessionKey });
|
|
204
|
+
} catch { /* best-effort */ }
|
|
205
|
+
} else if (e.type === "turn_start") {
|
|
206
|
+
turnIdx++;
|
|
189
207
|
} else if (e.type === "turn_end") {
|
|
190
208
|
if (budget.consume()) void session.abort();
|
|
191
209
|
} else if (e.type === "message_end" && e.message?.role === "assistant") {
|
|
@@ -193,6 +211,13 @@ export async function spawnSubagent(opts: SpawnOptions): Promise<SpawnResult> {
|
|
|
193
211
|
if (text) finalText = text;
|
|
194
212
|
const total = e.message.usage?.cost?.total;
|
|
195
213
|
if (typeof total === "number") tokenTotal += total;
|
|
214
|
+
try {
|
|
215
|
+
opts.runLog?.append(runId, { type: "message", role: "assistant", text, usage: { total }, turnIndex: turnIdx });
|
|
216
|
+
} catch { /* best-effort */ }
|
|
217
|
+
} else if (e.type === "tool_execution_end") {
|
|
218
|
+
try {
|
|
219
|
+
opts.runLog?.append(runId, buildToolEvent((e as any).toolName, (e as any).args, (e as any).result, (e as any).isError ?? false, turnIdx));
|
|
220
|
+
} catch { /* best-effort */ }
|
|
196
221
|
}
|
|
197
222
|
opts.onEvent?.(e);
|
|
198
223
|
});
|
|
@@ -242,7 +267,17 @@ async function finishRun(
|
|
|
242
267
|
error: string | undefined, agentName: string, model: string, tokenTotal = 0,
|
|
243
268
|
): Promise<SpawnResult> {
|
|
244
269
|
const endedAt = Date.now();
|
|
245
|
-
opts.runRegistry.update(runId, {
|
|
270
|
+
opts.runRegistry.update(runId, {
|
|
271
|
+
status, endedAt, resultSummary: finalText.slice(0, 120),
|
|
272
|
+
resumedFrom: opts.resumeLink, forkedFrom: opts.forkLink,
|
|
273
|
+
});
|
|
274
|
+
try {
|
|
275
|
+
opts.runLog?.append(runId, {
|
|
276
|
+
type: "run:ended", runId, status, endedAt,
|
|
277
|
+
resultSummary: finalText.slice(0, 120), tokenTotal,
|
|
278
|
+
resumedFrom: opts.resumeLink, forkedFrom: opts.forkLink,
|
|
279
|
+
});
|
|
280
|
+
} catch { /* best-effort: journal is the index, not the product */ }
|
|
246
281
|
// SPEC-4: lifecycle phase children skip the per-run todo reconciliation — the lifecycle
|
|
247
282
|
// engine owns the lifecycle todo's status + progress block (Q7=C).
|
|
248
283
|
if (!opts.lifecycleTodoId) {
|
package/src/index.ts
CHANGED
|
@@ -37,6 +37,8 @@ import { ConcurrencyPool } from "./runtime/concurrency-pool.ts";
|
|
|
37
37
|
import { ResultsInbox } from "./runtime/results-inbox.ts";
|
|
38
38
|
import { runBackground, type AsyncRunnerDeps } from "./runtime/async-runner.ts";
|
|
39
39
|
import { scanResumeCandidates } from "./runtime/resume.ts";
|
|
40
|
+
import { RunLog } from "./runtime/run-log.ts";
|
|
41
|
+
import { reconcileRuns } from "./runtime/reconcile.ts";
|
|
40
42
|
import { Scheduler } from "./scheduling/scheduler.ts";
|
|
41
43
|
import { createFleetResultsTool } from "./tools/fleet-results.ts";
|
|
42
44
|
import { BgRunsStore } from "./panel/bg-runs-store.ts";
|
|
@@ -186,7 +188,7 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
186
188
|
agent: o.agent, task: o.task, lifecycleTodoId: o.lifecycleTodoId, model: o.model,
|
|
187
189
|
skillsOverride: o.skills, backendOverride: o.backend,
|
|
188
190
|
registry: deps.registry, todoSync: deps.todoSync, runRegistry: deps.runRegistry, lock: bgLock,
|
|
189
|
-
backendRegistry: deps.backendRegistry, parentModel: deps.parentModel, parentCwd: opts.worktreePath, // child runs in the worktree
|
|
191
|
+
backendRegistry: deps.backendRegistry, parentModel: deps.parentModel, parentCwd: opts.worktreePath, runLog: deps.runLog, // child runs in the worktree
|
|
190
192
|
}),
|
|
191
193
|
};
|
|
192
194
|
const res = await runLifecycle(task, lifecycleName, { deps: lifecycleFullDeps, mode: opts.mode, worktreePath: opts.worktreePath, baseRef: "HEAD", onCheckpoint: async (p) => p.status === "failed" ? { action: "abort" } : { action: "continue" } });
|
|
@@ -196,6 +198,9 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
196
198
|
// At init they're undefined — the subagent tool's `if (!deps.asyncRunner)` guard returns an
|
|
197
199
|
// actionable "not configured" error if called before session_start (can't happen in practice).
|
|
198
200
|
deps.bgRuns = bgRuns;
|
|
201
|
+
// SPEC-5b-1: RunLog is constructed per-session (needs the cwd) in session_start; the
|
|
202
|
+
// shared `deps` reference is mutated there so the subagent tool + panel pick it up live.
|
|
203
|
+
deps.runLog = undefined as RunLog | undefined;
|
|
199
204
|
|
|
200
205
|
const refresh = (ctx: { cwd: string; ui: { notify: (m: string, t?: "info" | "warning" | "error") => void } }): void => {
|
|
201
206
|
const r = discoverAgents({
|
|
@@ -232,6 +237,13 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
232
237
|
deps.parentCwd = ctx.cwd;
|
|
233
238
|
// SPEC-5a: build the per-session async runner + scheduler, start firing, scan for interrupted runs.
|
|
234
239
|
const dir = fleetDir(ctx.cwd);
|
|
240
|
+
// SPEC-5b-1: per-session RunLog at .pi/fleet/conversations/ (separate from the
|
|
241
|
+
// SPEC-5a phase journal at .pi/fleet/runs/ — different granularity, no filename collision).
|
|
242
|
+
deps.runLog = new RunLog(join(dir, "conversations"));
|
|
243
|
+
const reconciled = reconcileRuns(deps.runLog);
|
|
244
|
+
if (reconciled.length > 0) {
|
|
245
|
+
ctx.ui.notify(`reconciled ${reconciled.length} interrupted fleet run${reconciled.length > 1 ? "s" : ""} (marked aborted)`, "info");
|
|
246
|
+
}
|
|
235
247
|
deps.asyncRunner = {
|
|
236
248
|
worktree: new WorktreeService({ rootDir: ctx.cwd }),
|
|
237
249
|
diff: new DiffService(),
|
package/src/panel/fleet-panel.ts
CHANGED
|
@@ -12,6 +12,9 @@ import {
|
|
|
12
12
|
import type { AgentDef } from "../registry/frontmatter.ts";
|
|
13
13
|
import { agentsRow, agentInfo, backendsRow, backendInfo, lifecycleRow, lifecyclePhaseTimeline, scheduleRow } from "./rows.ts";
|
|
14
14
|
import { buildFleetItems } from "./fleet-items.ts";
|
|
15
|
+
import { runsRow, runTimelineRow } from "./runs-rows.ts";
|
|
16
|
+
import { buildRunsIndex } from "./runs-index.ts";
|
|
17
|
+
import type { RunLog, RunMeta, RunLogEvent } from "../runtime/run-log.ts";
|
|
15
18
|
import type { Scheduler, Schedule } from "../scheduling/scheduler.ts";
|
|
16
19
|
import type { BgRunsStore } from "./bg-runs-store.ts";
|
|
17
20
|
import { spawnSubagent, type SpawnResult } from "../engine/spawnSubagent.ts";
|
|
@@ -23,7 +26,7 @@ import type { LifecycleDef, LifecycleRunRecord, CheckpointDecision, PhaseRecord
|
|
|
23
26
|
import type { LifecycleRunDeps, CheckpointFn } from "../lifecycle/run-lifecycle.ts";
|
|
24
27
|
import { runLifecycle } from "../lifecycle/run-lifecycle.ts";
|
|
25
28
|
|
|
26
|
-
type View = "fleet" | "lifecycle" | "agents" | "backends" | "scheduled";
|
|
29
|
+
type View = "fleet" | "lifecycle" | "runs" | "agents" | "backends" | "scheduled";
|
|
27
30
|
|
|
28
31
|
export interface FleetPanelDeps {
|
|
29
32
|
registry: Map<string, AgentDef>;
|
|
@@ -41,6 +44,8 @@ export interface FleetPanelDeps {
|
|
|
41
44
|
scheduler?: Scheduler;
|
|
42
45
|
/** SPEC-5a: live bg run status rows for the fleet tab. Optional. */
|
|
43
46
|
bgRuns?: BgRunsStore;
|
|
47
|
+
/** SPEC-5b-1: durable per-run conversation log. Optional — Runs tab degrades to empty when absent. */
|
|
48
|
+
runLog?: RunLog;
|
|
44
49
|
}
|
|
45
50
|
|
|
46
51
|
export interface FleetPanelOpts {
|
|
@@ -80,6 +85,11 @@ export class FleetPanel extends Container {
|
|
|
80
85
|
private schedNameInput: Input | null = null;
|
|
81
86
|
private schedPhase: "task" | "expr" | "name" = "task";
|
|
82
87
|
private selectedSchedule: Schedule | null = null;
|
|
88
|
+
// SPEC-5b-1: Runs tab — replay overlay state + resume/fork input state
|
|
89
|
+
private selectedRun: RunMeta | null = null;
|
|
90
|
+
private runTimeline: RunLogEvent[] | null = null;
|
|
91
|
+
private resumeInput: Input | null = null;
|
|
92
|
+
private resumeMode = false;
|
|
83
93
|
/** SPEC-5a proper-fix: store-change subscriptions — fired by RunRegistry + BgRunsStore
|
|
84
94
|
* so the panel re-renders the moment a (fore- or back-ground) run mutates, without a keypress. */
|
|
85
95
|
private readonly unsubs: (() => void)[] = [];
|
|
@@ -111,8 +121,10 @@ export class FleetPanel extends Container {
|
|
|
111
121
|
? buildFleetItems({ runRegistry: this.deps.runRegistry, bgRuns: this.deps.bgRuns })
|
|
112
122
|
: this.view === "lifecycle"
|
|
113
123
|
? [...this.deps.lifecycleRuns.values()].map((l: LifecycleRunRecord) => ({ value: l.runId, label: lifecycleRow(l) }))
|
|
114
|
-
: this.view === "
|
|
115
|
-
?
|
|
124
|
+
: this.view === "runs"
|
|
125
|
+
? buildRunsIndex(this.deps.runLog?.dir ?? "").map((r: RunMeta) => ({ value: r.runId, label: runsRow(r) }))
|
|
126
|
+
: this.view === "agents"
|
|
127
|
+
? [...this.deps.registry.values()].map((a: AgentDef) => ({ value: a.name, label: agentsRow(a) }))
|
|
116
128
|
: this.view === "scheduled"
|
|
117
129
|
? (this.deps.scheduler?.list() ?? []).map((s: Schedule) => ({ value: s.id, label: scheduleRow(s) }))
|
|
118
130
|
: this.deps.backendRegistry.list().map((b: Backend) => ({ value: b.id, label: backendsRow(b) }));
|
|
@@ -153,7 +165,7 @@ export class FleetPanel extends Container {
|
|
|
153
165
|
this.children.length = 0;
|
|
154
166
|
this.children.push(...keep);
|
|
155
167
|
const accent = (s: string): string => this.theme.fg("accent", s);
|
|
156
|
-
const tabs = (["fleet", "lifecycle", "agents", "backends", "scheduled"] as View[])
|
|
168
|
+
const tabs = (["fleet", "lifecycle", "runs", "agents", "backends", "scheduled"] as View[])
|
|
157
169
|
.map((v) => (v === this.view ? this.theme.fg("accent", this.theme.bold(`[${v}]`)) : this.theme.fg("dim", v)))
|
|
158
170
|
.join(" ");
|
|
159
171
|
this.addChild(new Text(accent(this.theme.bold(" FLEET")) + " " + tabs, 0, 0));
|
|
@@ -196,6 +208,33 @@ export class FleetPanel extends Container {
|
|
|
196
208
|
`nextFire: ${s.nextFire?.toLocaleString() ?? "(none)"}`,
|
|
197
209
|
]) this.addChild(new Text(this.theme.fg("text", line), 0, 0));
|
|
198
210
|
this.addChild(new Text(this.theme.fg("dim", " esc:Back"), 0, 0));
|
|
211
|
+
} else if (this.selectedRun) {
|
|
212
|
+
// SPEC-5b-1: Runs tab — per-turn timeline replay (read-only); enter reserved for 5b-3 overlay.
|
|
213
|
+
this.addChild(new Text(this.theme.fg("dim", ` ── run ${this.selectedRun.runId} — timeline ──`), 0, 0));
|
|
214
|
+
const events = (this.runTimeline ?? []).filter((e) => e.type === "message" || e.type === "tool") as Array<RunLogEvent>;
|
|
215
|
+
if (events.length === 0) {
|
|
216
|
+
this.addChild(new Text(this.theme.fg("dim", " (no conversation events)"), 0, 0));
|
|
217
|
+
} else {
|
|
218
|
+
const tl = new SelectList(
|
|
219
|
+
events.map((e) => ({ value: "", label: runTimelineRow(e as never) })),
|
|
220
|
+
Math.min(events.length, 12),
|
|
221
|
+
{
|
|
222
|
+
selectedPrefix: (s: string) => this.theme.fg("accent", s),
|
|
223
|
+
selectedText: (s: string) => this.theme.fg("accent", s),
|
|
224
|
+
description: (s: string) => this.theme.fg("muted", s),
|
|
225
|
+
scrollInfo: (s: string) => this.theme.fg("dim", s),
|
|
226
|
+
noMatch: (s: string) => this.theme.fg("warning", s),
|
|
227
|
+
},
|
|
228
|
+
);
|
|
229
|
+
tl.onCancel = () => { this.selectedRun = null; this.runTimeline = null; this.renderShell(); };
|
|
230
|
+
this.addChild(tl);
|
|
231
|
+
}
|
|
232
|
+
this.addChild(new Text(this.theme.fg("dim", " enter: (5b-3 full message) esc:Back"), 0, 0));
|
|
233
|
+
} else if (this.resumeMode && this.resumeInput) {
|
|
234
|
+
// SPEC-5b-1: Runs tab — resume follow-up input.
|
|
235
|
+
this.addChild(new Text(this.theme.fg("accent", " follow-up> "), 0, 0));
|
|
236
|
+
this.addChild(this.resumeInput);
|
|
237
|
+
this.addChild(new Text(this.theme.fg("dim", " enter submit • esc cancel"), 0, 0));
|
|
199
238
|
} else if (this.schedRunMode && (this.schedTaskInput || this.schedExprInput || this.schedNameInput)) {
|
|
200
239
|
const prompt = this.schedPhase === "task" ? " task> " : this.schedPhase === "expr" ? " schedule (cron | interval | one-shot ISO)> " : " lifecycle (blank=default)> ";
|
|
201
240
|
this.addChild(new Text(this.theme.fg("accent", prompt), 0, 0));
|
|
@@ -221,8 +260,8 @@ export class FleetPanel extends Container {
|
|
|
221
260
|
|
|
222
261
|
this.addChild(new Spacer(1));
|
|
223
262
|
const hint =
|
|
224
|
-
this.infoAgent || this.selectedBackend || this.selectedLifecycle || this.selectedSchedule
|
|
225
|
-
? " esc:Back"
|
|
263
|
+
this.infoAgent || this.selectedBackend || this.selectedLifecycle || this.selectedSchedule || this.selectedRun
|
|
264
|
+
? (this.selectedRun ? " enter:(5b-3) esc:Back" : " esc:Back")
|
|
226
265
|
: this.pendingCheckpoint
|
|
227
266
|
? " c:Continue v:Revise a:Abort"
|
|
228
267
|
: this.lcRevising
|
|
@@ -230,8 +269,10 @@ export class FleetPanel extends Container {
|
|
|
230
269
|
: this.view === "fleet"
|
|
231
270
|
? " r:Run-new s:Stop o:Open-todo tab:Lifecycle q:Quit"
|
|
232
271
|
: this.view === "lifecycle"
|
|
233
|
-
? " r:Run-lifecycle i:Info tab:
|
|
234
|
-
: this.view === "
|
|
272
|
+
? " r:Run-lifecycle i:Info tab:Runs q:Quit"
|
|
273
|
+
: this.view === "runs"
|
|
274
|
+
? " enter:Replay r:Resume f:Fork tab:Agents q:Quit"
|
|
275
|
+
: this.view === "agents"
|
|
235
276
|
? " r:Run e:Edit i:Info d:Reload tab:Backends q:Quit"
|
|
236
277
|
: this.view === "scheduled"
|
|
237
278
|
? " a:Add p:Pause/resume d:Delete i:Info tab:Fleet q:Quit"
|
|
@@ -277,6 +318,7 @@ export class FleetPanel extends Container {
|
|
|
277
318
|
runRegistry: this.deps.runRegistry, lock: this.deps.lock,
|
|
278
319
|
backendRegistry: this.deps.backendRegistry,
|
|
279
320
|
parentModel: this.deps.parentModel, parentCwd: this.deps.parentCwd,
|
|
321
|
+
runLog: this.deps.runLog,
|
|
280
322
|
// live Fleet row during the run (SPEC-1 §4c) — re-render on each turn_end
|
|
281
323
|
onEvent: (e) => {
|
|
282
324
|
if (e.type === "turn_end") {
|
|
@@ -302,12 +344,15 @@ export class FleetPanel extends Container {
|
|
|
302
344
|
|
|
303
345
|
private switchView(): void {
|
|
304
346
|
this.view = this.view === "fleet" ? "lifecycle"
|
|
305
|
-
: this.view === "lifecycle" ? "
|
|
347
|
+
: this.view === "lifecycle" ? "runs"
|
|
348
|
+
: this.view === "runs" ? "agents"
|
|
306
349
|
: this.view === "agents" ? "backends"
|
|
307
350
|
: this.view === "backends" ? "scheduled" : "fleet";
|
|
308
351
|
this.selectedBackend = null;
|
|
309
352
|
this.selectedLifecycle = null;
|
|
310
353
|
this.selectedSchedule = null;
|
|
354
|
+
this.selectedRun = null;
|
|
355
|
+
this.runTimeline = null;
|
|
311
356
|
this.list = this.buildList();
|
|
312
357
|
this.renderShell();
|
|
313
358
|
}
|
|
@@ -329,6 +374,17 @@ export class FleetPanel extends Container {
|
|
|
329
374
|
if (matchesKey(data, "escape")) { this.selectedSchedule = null; this.renderShell(); }
|
|
330
375
|
return;
|
|
331
376
|
}
|
|
377
|
+
if (this.selectedRun) {
|
|
378
|
+
// SPEC-5b-1: Runs timeline replay overlay — esc back; enter is a 5b-3 placeholder.
|
|
379
|
+
if (matchesKey(data, "escape")) { this.selectedRun = null; this.runTimeline = null; this.renderShell(); }
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
if (this.resumeMode && this.resumeInput) {
|
|
383
|
+
if (matchesKey(data, "escape")) { this.cancelResume(); return; }
|
|
384
|
+
this.resumeInput.handleInput(data);
|
|
385
|
+
this.invalidate();
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
332
388
|
if (this.schedRunMode && (this.schedTaskInput || this.schedExprInput || this.schedNameInput)) {
|
|
333
389
|
if (matchesKey(data, "escape")) { this.cancelScheduleAdd(); return; }
|
|
334
390
|
(this.schedPhase === "task" ? this.schedTaskInput! : this.schedPhase === "expr" ? this.schedExprInput! : this.schedNameInput!).handleInput(data);
|
|
@@ -379,6 +435,20 @@ export class FleetPanel extends Container {
|
|
|
379
435
|
this.renderShell();
|
|
380
436
|
return;
|
|
381
437
|
}
|
|
438
|
+
// SPEC-5b-1: Runs view — enter/i:Replay R:Resume F:Fork
|
|
439
|
+
if (this.view === "runs" && this.deps.runLog) {
|
|
440
|
+
if (matchesKey(data, "enter") || matchesKey(data, "i")) {
|
|
441
|
+
const sel = this.list.getSelectedItem();
|
|
442
|
+
if (sel) {
|
|
443
|
+
this.selectedRun = buildRunsIndex(this.deps.runLog.dir).find((r) => r.runId === sel.value) ?? null;
|
|
444
|
+
this.runTimeline = this.deps.runLog.replay(sel.value);
|
|
445
|
+
this.renderShell();
|
|
446
|
+
}
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
if (matchesKey(data, "r")) { this.startResume(); return; }
|
|
450
|
+
if (matchesKey(data, "f")) { this.startFork(); return; }
|
|
451
|
+
}
|
|
382
452
|
// SPEC-4: Lifecycle view — i:Info + r:Run-lifecycle
|
|
383
453
|
if (matchesKey(data, "i") && this.view === "lifecycle") {
|
|
384
454
|
const sel = this.list.getSelectedItem();
|
|
@@ -493,6 +563,87 @@ export class FleetPanel extends Container {
|
|
|
493
563
|
this.renderShell();
|
|
494
564
|
}
|
|
495
565
|
|
|
566
|
+
/** SPEC-5b-1: Resume — rehydrate the prior session (same agent sessionKey) + a follow-up. */
|
|
567
|
+
private startResume(): void {
|
|
568
|
+
const sel = this.list.getSelectedItem();
|
|
569
|
+
if (!sel) return;
|
|
570
|
+
const run = buildRunsIndex(this.deps.runLog!.dir).find((r) => r.runId === sel.value);
|
|
571
|
+
if (!run) return;
|
|
572
|
+
if (!run.backendSessionId) { this.onNotify("no resumable session for this run", "warning"); return; }
|
|
573
|
+
if (run.status === "running") { this.onNotify("run still running; stop it first", "warning"); return; }
|
|
574
|
+
this.resumeInput = new Input();
|
|
575
|
+
this.resumeInput.onSubmit = (followUp: string) => {
|
|
576
|
+
if (!followUp.trim()) { this.cancelResume(); return; }
|
|
577
|
+
void this.executeResume(run, followUp.trim());
|
|
578
|
+
};
|
|
579
|
+
this.resumeInput.onEscape = () => this.cancelResume();
|
|
580
|
+
this.resumeMode = true;
|
|
581
|
+
this.renderShell();
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
private cancelResume(): void {
|
|
585
|
+
this.resumeMode = false;
|
|
586
|
+
this.resumeInput = null;
|
|
587
|
+
this.renderShell();
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
private async executeResume(prior: RunMeta, followUp: string): Promise<void> {
|
|
591
|
+
this.resumeMode = false;
|
|
592
|
+
this.resumeInput = null;
|
|
593
|
+
this.renderShell();
|
|
594
|
+
const res: SpawnResult = await spawnSubagent({
|
|
595
|
+
agent: prior.agent, task: followUp, track: true, resumeLink: prior.runId, runLog: this.deps.runLog,
|
|
596
|
+
registry: this.deps.registry, todoSync: this.deps.todoSync, runRegistry: this.deps.runRegistry,
|
|
597
|
+
lock: this.deps.lock, backendRegistry: this.deps.backendRegistry,
|
|
598
|
+
parentModel: this.deps.parentModel, parentCwd: this.deps.parentCwd,
|
|
599
|
+
onEvent: (e) => { if (e.type === "turn_end") { this.list = this.buildList(); this.renderShell(); } },
|
|
600
|
+
});
|
|
601
|
+
this.list = this.buildList();
|
|
602
|
+
this.renderShell();
|
|
603
|
+
this.onNotify(`resume ${res.status}: ${res.runId}${res.error ? " — " + res.error : ""}`, res.status === "completed" ? "info" : "warning");
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/** SPEC-5b-1: Fork — fresh re-run with the same agent + task (no session rehydration). */
|
|
607
|
+
private startFork(): void {
|
|
608
|
+
const sel = this.list.getSelectedItem();
|
|
609
|
+
if (!sel) return;
|
|
610
|
+
const run = buildRunsIndex(this.deps.runLog!.dir).find((r) => r.runId === sel.value);
|
|
611
|
+
if (!run) return;
|
|
612
|
+
if (run.status === "running") { this.onNotify("run still running; stop it first", "warning"); return; }
|
|
613
|
+
this.linkPhase = "task";
|
|
614
|
+
this.taskInput = new Input();
|
|
615
|
+
this.taskInput.onSubmit = (task: string) => {
|
|
616
|
+
const finalTask = task.trim() || run.task;
|
|
617
|
+
this.linkPhase = "link";
|
|
618
|
+
this.linkInput = new Input();
|
|
619
|
+
this.linkInput.onSubmit = (todoIdRaw: string) => {
|
|
620
|
+
void this.executeFork(run.agent, finalTask, todoIdRaw.trim() || undefined, run.runId);
|
|
621
|
+
};
|
|
622
|
+
this.linkInput.onEscape = () => { void this.executeFork(run.agent, finalTask, undefined, run.runId); };
|
|
623
|
+
this.renderShell();
|
|
624
|
+
};
|
|
625
|
+
this.taskInput.onEscape = () => this.cancelRun();
|
|
626
|
+
this.runMode = true;
|
|
627
|
+
this.renderShell();
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
private async executeFork(agent: string, task: string, todoId: string | undefined, priorRunId: string): Promise<void> {
|
|
631
|
+
this.runMode = false;
|
|
632
|
+
this.taskInput = null;
|
|
633
|
+
this.linkInput = null;
|
|
634
|
+
this.renderShell();
|
|
635
|
+
const res: SpawnResult = await spawnSubagent({
|
|
636
|
+
agent, task, todoId, track: true, forkLink: priorRunId, runLog: this.deps.runLog,
|
|
637
|
+
registry: this.deps.registry, todoSync: this.deps.todoSync, runRegistry: this.deps.runRegistry,
|
|
638
|
+
lock: this.deps.lock, backendRegistry: this.deps.backendRegistry,
|
|
639
|
+
parentModel: this.deps.parentModel, parentCwd: this.deps.parentCwd,
|
|
640
|
+
onEvent: (e) => { if (e.type === "turn_end") { this.list = this.buildList(); this.renderShell(); } },
|
|
641
|
+
});
|
|
642
|
+
this.list = this.buildList();
|
|
643
|
+
this.renderShell();
|
|
644
|
+
this.onNotify(`fork ${res.status}: ${res.runId}${res.error ? " — " + res.error : ""}`, res.status === "completed" ? "info" : "warning");
|
|
645
|
+
}
|
|
646
|
+
|
|
496
647
|
/** SPEC-4: open the Run-lifecycle inline inputs (task → lifecycle name → start runLifecycle). */
|
|
497
648
|
private startLifecycleRun(): void {
|
|
498
649
|
this.lcPhase = "task";
|
|
@@ -542,6 +693,7 @@ export class FleetPanel extends Container {
|
|
|
542
693
|
skillsOverride: o.skills, backendOverride: o.backend,
|
|
543
694
|
registry: this.deps.registry, todoSync: this.deps.todoSync, runRegistry: this.deps.runRegistry, lock: this.deps.lock,
|
|
544
695
|
backendRegistry: this.deps.backendRegistry, parentModel: this.deps.parentModel, parentCwd: this.deps.parentCwd,
|
|
696
|
+
runLog: this.deps.runLog,
|
|
545
697
|
});
|
|
546
698
|
},
|
|
547
699
|
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// src/panel/runs-index.ts
|
|
2
|
+
// SPEC-5b-1 — pure scan of the conversations dir → newest-first run list. Pure so the
|
|
3
|
+
// Runs tab + unit tests share one path (mirrors buildFleetItems from v0.5.0).
|
|
4
|
+
import { RunLog, type RunMeta } from "../runtime/run-log.ts";
|
|
5
|
+
|
|
6
|
+
export function buildRunsIndex(logDir: string): RunMeta[] {
|
|
7
|
+
return new RunLog(logDir).scanMeta().sort((a, b) => b.startedAt - a.startedAt);
|
|
8
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// src/panel/runs-rows.ts
|
|
2
|
+
// SPEC-5b-1 — pure renderers for the Runs tab + per-turn timeline. Reuses the glyph
|
|
3
|
+
// language (▶ ✓ ✗) so the Runs tab is visually consistent with Fleet/Lifecycle.
|
|
4
|
+
import { fmtDuration } from "./rows.ts";
|
|
5
|
+
import type { RunMeta, MessageEvent, ToolEvent } from "../runtime/run-log.ts";
|
|
6
|
+
|
|
7
|
+
const STATUS_GLYPH: Record<RunMeta["status"], string> = {
|
|
8
|
+
running: "▶", completed: "✓", failed: "✗", aborted: "✗",
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function runsRow(r: RunMeta): string {
|
|
12
|
+
const dur = r.endedAt ? fmtDuration(r.endedAt - r.startedAt) : "—";
|
|
13
|
+
const tok = r.tokenTotal > 0 ? ` ${r.tokenTotal} tok` : "";
|
|
14
|
+
const summary = r.resultSummary ? ` "${r.resultSummary}"` : "";
|
|
15
|
+
const prov = r.resumedFrom ? ` ← resumed:${r.resumedFrom}` : r.forkedFrom ? ` ← forked:${r.forkedFrom}` : "";
|
|
16
|
+
return `${STATUS_GLYPH[r.status]} ${r.runId} ${r.agent} ${r.status} ${dur}${tok}${summary}${prov}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function runTimelineRow(e: MessageEvent | ToolEvent): string {
|
|
20
|
+
const turn = Math.max(0, e.turnIndex);
|
|
21
|
+
if (e.type === "message") {
|
|
22
|
+
const text = e.text.length > 80 ? e.text.slice(0, 79) + "…" : e.text;
|
|
23
|
+
const tok = e.usage?.total != null ? ` ${e.usage.total} tok` : "";
|
|
24
|
+
return `[a] "${text}"${tok} ·t${turn}`;
|
|
25
|
+
}
|
|
26
|
+
const glyph = e.isError ? "✗" : "✓";
|
|
27
|
+
const err = e.isError ? ` "${e.result}"` : "";
|
|
28
|
+
return `[t] ${e.toolName} ${e.args} ${glyph}${err} ·t${turn}`;
|
|
29
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// src/runtime/reconcile.ts
|
|
2
|
+
// SPEC-5b-1 — on pi boot, mark orphan RunLog runs (run:meta with no run:ended whose process
|
|
3
|
+
// is gone) as aborted so the Runs tab doesn't show stale "running" rows across restarts.
|
|
4
|
+
// Foreground orphans; bg/lifecycle orphans are already handled by scanResumeCandidates (SPEC-5a).
|
|
5
|
+
import type { RunLog } from "./run-log.ts";
|
|
6
|
+
|
|
7
|
+
export interface ReconcileOpts {
|
|
8
|
+
/** Orphans whose startedAt is older than (now - graceMs) are marked aborted. Default 60000. */
|
|
9
|
+
graceMs?: number;
|
|
10
|
+
/** Test injection. Default Date.now(). */
|
|
11
|
+
now?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Returns the runIds it marked aborted. Idempotent: a run already ended is skipped. */
|
|
15
|
+
export function reconcileRuns(log: RunLog, opts: ReconcileOpts = {}): string[] {
|
|
16
|
+
const grace = opts.graceMs ?? 60_000;
|
|
17
|
+
const now = opts.now ?? Date.now();
|
|
18
|
+
const aborted: string[] = [];
|
|
19
|
+
for (const meta of log.scanMeta()) {
|
|
20
|
+
if (meta.status !== "running") continue;
|
|
21
|
+
if (now - meta.startedAt <= grace) continue;
|
|
22
|
+
log.append(meta.runId, {
|
|
23
|
+
type: "run:ended", runId: meta.runId, status: "aborted",
|
|
24
|
+
endedAt: now, resultSummary: "process-gone", tokenTotal: meta.tokenTotal,
|
|
25
|
+
});
|
|
26
|
+
aborted.push(meta.runId);
|
|
27
|
+
}
|
|
28
|
+
return aborted;
|
|
29
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// src/runtime/run-log.ts
|
|
2
|
+
// SPEC-5b-1 — self-describing per-run conversation log. Append-only JSONL (crash-safe:
|
|
3
|
+
// partial last line discarded). One file per run in .pi/fleet/conversations/<runId>.jsonl.
|
|
4
|
+
// The Runs tab rebuilds the run list across restarts via scanMeta().
|
|
5
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
|
|
8
|
+
export type FleetRunStatus = "running" | "completed" | "failed" | "aborted";
|
|
9
|
+
|
|
10
|
+
export interface RunMetaEvent {
|
|
11
|
+
type: "run:meta"; runId: string; agent: string; model: string; task: string;
|
|
12
|
+
startedAt: number; track: boolean; todoId: string | null;
|
|
13
|
+
backendSessionId?: string; sessionKey?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface MessageEvent {
|
|
16
|
+
type: "message"; role: string; text: string;
|
|
17
|
+
usage?: { total?: number }; turnIndex: number;
|
|
18
|
+
}
|
|
19
|
+
export interface ToolEvent {
|
|
20
|
+
type: "tool"; toolName: string; args: string; result: string; isError: boolean; turnIndex: number;
|
|
21
|
+
}
|
|
22
|
+
export interface RunEndedEvent {
|
|
23
|
+
type: "run:ended"; runId: string; status: FleetRunStatus; endedAt: number;
|
|
24
|
+
resultSummary?: string; tokenTotal: number; resumedFrom?: string; forkedFrom?: string;
|
|
25
|
+
}
|
|
26
|
+
export type RunLogEvent = RunMetaEvent | MessageEvent | ToolEvent | RunEndedEvent;
|
|
27
|
+
|
|
28
|
+
/** Resolved view of a run for the Runs tab (scanMeta result). */
|
|
29
|
+
export interface RunMeta {
|
|
30
|
+
runId: string; agent: string; model: string; task: string;
|
|
31
|
+
startedAt: number; track: boolean; todoId: string | null;
|
|
32
|
+
backendSessionId?: string; sessionKey?: string;
|
|
33
|
+
status: FleetRunStatus; endedAt?: number; resultSummary?: string; tokenTotal: number;
|
|
34
|
+
resumedFrom?: string; forkedFrom?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const ARGS_LIMIT = 200;
|
|
38
|
+
const RESULT_LIMIT = 500;
|
|
39
|
+
|
|
40
|
+
/** Truncate to `n` chars with a single `…` ellipsis. Non-string → "". */
|
|
41
|
+
export function excerpt(str: string, n: number): string {
|
|
42
|
+
if (typeof str !== "string" || str.length === 0) return "";
|
|
43
|
+
if (str.length <= n) return str;
|
|
44
|
+
return str.slice(0, Math.max(0, n - 1)) + "…";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export class RunLog {
|
|
48
|
+
/** Public so buildRunsIndex + FleetPanel can read `runLog.dir` for the scan. */
|
|
49
|
+
constructor(readonly dir: string) {}
|
|
50
|
+
|
|
51
|
+
private file(runId: string): string { return join(this.dir, `${runId}.jsonl`); }
|
|
52
|
+
|
|
53
|
+
append(runId: string, event: RunLogEvent): void {
|
|
54
|
+
try {
|
|
55
|
+
mkdirSync(this.dir, { recursive: true });
|
|
56
|
+
appendFileSync(this.file(runId), JSON.stringify(event) + "\n", "utf8");
|
|
57
|
+
} catch {
|
|
58
|
+
// best-effort: the run is the product; the journal is the index. Never fail the run.
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
replay(runId: string): RunLogEvent[] {
|
|
63
|
+
const f = this.file(runId);
|
|
64
|
+
if (!existsSync(f)) return [];
|
|
65
|
+
const events: RunLogEvent[] = [];
|
|
66
|
+
for (const line of readFileSync(f, "utf8").split("\n")) {
|
|
67
|
+
if (!line) continue;
|
|
68
|
+
try { events.push(JSON.parse(line) as RunLogEvent); } catch { /* partial last line */ }
|
|
69
|
+
}
|
|
70
|
+
return events;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Rebuild the durable run list across restarts. Reads each file's run:meta events
|
|
74
|
+
* (latest binding wins for backendSessionId/sessionKey; first wins for startedAt) + the
|
|
75
|
+
* last run:ended (if present). */
|
|
76
|
+
scanMeta(): RunMeta[] {
|
|
77
|
+
if (!existsSync(this.dir)) return [];
|
|
78
|
+
const out: RunMeta[] = [];
|
|
79
|
+
for (const f of readdirSync(this.dir)) {
|
|
80
|
+
if (!f.endsWith(".jsonl")) continue;
|
|
81
|
+
const runId = f.slice(0, -".jsonl".length);
|
|
82
|
+
const events = this.replay(runId);
|
|
83
|
+
let meta: RunMeta | null = null;
|
|
84
|
+
let ended: RunEndedEvent | null = null;
|
|
85
|
+
for (const e of events) {
|
|
86
|
+
if (e.type === "run:meta") {
|
|
87
|
+
if (!meta) {
|
|
88
|
+
meta = { runId: e.runId, agent: e.agent, model: e.model, task: e.task, startedAt: e.startedAt,
|
|
89
|
+
track: e.track, todoId: e.todoId, backendSessionId: e.backendSessionId, sessionKey: e.sessionKey,
|
|
90
|
+
status: "running", tokenTotal: 0 };
|
|
91
|
+
} else {
|
|
92
|
+
// latest binding wins
|
|
93
|
+
if (e.backendSessionId) meta.backendSessionId = e.backendSessionId;
|
|
94
|
+
if (e.sessionKey) meta.sessionKey = e.sessionKey;
|
|
95
|
+
}
|
|
96
|
+
} else if (e.type === "run:ended") {
|
|
97
|
+
ended = e; // keep the last one
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (!meta) continue;
|
|
101
|
+
if (ended) {
|
|
102
|
+
meta.status = ended.status; meta.endedAt = ended.endedAt;
|
|
103
|
+
meta.resultSummary = ended.resultSummary; meta.tokenTotal = ended.tokenTotal;
|
|
104
|
+
meta.resumedFrom = ended.resumedFrom; meta.forkedFrom = ended.forkedFrom;
|
|
105
|
+
}
|
|
106
|
+
out.push(meta);
|
|
107
|
+
}
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Build a tool event with the excerpt policy: errors-in-full, non-errors excerpted. */
|
|
113
|
+
export function buildToolEvent(
|
|
114
|
+
toolName: string, args: unknown, result: unknown, isError: boolean, turnIndex: number,
|
|
115
|
+
): ToolEvent {
|
|
116
|
+
const argsStr = typeof args === "string" ? args : JSON.stringify(args);
|
|
117
|
+
const resultStr = typeof result === "string" ? result : JSON.stringify(result);
|
|
118
|
+
return {
|
|
119
|
+
type: "tool", toolName,
|
|
120
|
+
args: excerpt(argsStr, ARGS_LIMIT),
|
|
121
|
+
result: isError ? resultStr : excerpt(resultStr, RESULT_LIMIT),
|
|
122
|
+
isError, turnIndex,
|
|
123
|
+
};
|
|
124
|
+
}
|
package/src/tools/subagent.ts
CHANGED
|
@@ -45,6 +45,8 @@ export interface SubagentToolDeps {
|
|
|
45
45
|
scheduler?: Scheduler;
|
|
46
46
|
/** SPEC-5a: live bg run status rows for the /fleet panel. Optional. */
|
|
47
47
|
bgRuns?: import("../panel/bg-runs-store.ts").BgRunsStore;
|
|
48
|
+
/** SPEC-5b-1: durable per-run conversation log. Optional — Runs tab + journaling disabled when absent. */
|
|
49
|
+
runLog?: import("../runtime/run-log.ts").RunLog;
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
/** Build the pi.registerTool definition. Thin wrapper over spawnSubagent. */
|
|
@@ -84,7 +86,7 @@ export function createSubagentTool(deps: SubagentToolDeps) {
|
|
|
84
86
|
agent: o.agent, task: o.task, lifecycleTodoId: o.lifecycleTodoId, model: o.model,
|
|
85
87
|
skillsOverride: o.skills, backendOverride: o.backend,
|
|
86
88
|
registry: deps.registry, todoSync: deps.todoSync, runRegistry: deps.runRegistry, lock: deps.lock,
|
|
87
|
-
backendRegistry: deps.backendRegistry, parentModel: deps.parentModel, parentCwd: deps.parentCwd, signal,
|
|
89
|
+
backendRegistry: deps.backendRegistry, parentModel: deps.parentModel, parentCwd: deps.parentCwd, runLog: deps.runLog, signal,
|
|
88
90
|
}),
|
|
89
91
|
};
|
|
90
92
|
const res = await runLifecycle(params.task, params.lifecycle, {
|
|
@@ -113,6 +115,7 @@ export function createSubagentTool(deps: SubagentToolDeps) {
|
|
|
113
115
|
backendRegistry: deps.backendRegistry,
|
|
114
116
|
parentModel: deps.parentModel,
|
|
115
117
|
parentCwd: deps.parentCwd,
|
|
118
|
+
runLog: deps.runLog,
|
|
116
119
|
signal,
|
|
117
120
|
onEvent: (e) => {
|
|
118
121
|
if (ctx?.ui?.setWidget && e.type === "turn_end") {
|