@longtable/cli 0.1.57 → 0.1.59
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/dist/cli.js +365 -73
- package/dist/debate.js +19 -31
- package/dist/longtable-codex-native-hook.js +36 -8
- package/dist/panel-runtime.d.ts +1 -0
- package/dist/panel-runtime.js +447 -88
- package/dist/panel.d.ts +13 -1
- package/dist/panel.js +343 -31
- package/dist/project-session.d.ts +0 -1
- package/dist/project-session.js +75 -116
- package/package.json +7 -7
package/dist/panel-runtime.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
-
import { chmod, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { appendFile, chmod, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
5
|
function nowIso() {
|
|
6
6
|
return new Date().toISOString();
|
|
@@ -26,6 +26,30 @@ function appendDiagnostic(existing, diagnostic) {
|
|
|
26
26
|
function panelRunsDirectory(workingDirectory) {
|
|
27
27
|
return join(workingDirectory, ".longtable", "panel-runs");
|
|
28
28
|
}
|
|
29
|
+
function safeName(value) {
|
|
30
|
+
return value.replace(/[^A-Za-z0-9._/-]/g, "-").replace(/\/+/g, "/").slice(0, 180);
|
|
31
|
+
}
|
|
32
|
+
function workerSafeName(value) {
|
|
33
|
+
return value.replace(/[^A-Za-z0-9._-]/g, "-").slice(0, 80);
|
|
34
|
+
}
|
|
35
|
+
function gitOutput(workingDirectory, args) {
|
|
36
|
+
return execFileSync("git", args, { cwd: workingDirectory, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
37
|
+
}
|
|
38
|
+
function currentGitCommit(workingDirectory) {
|
|
39
|
+
try {
|
|
40
|
+
return gitOutput(workingDirectory, ["rev-parse", "HEAD"]);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async function appendLifecycleEvent(run, event) {
|
|
47
|
+
if (!run.eventLogPath) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
await mkdir(dirname(run.eventLogPath), { recursive: true });
|
|
51
|
+
await appendFile(run.eventLogPath, `${JSON.stringify({ id: createId("event"), createdAt: nowIso(), ...event })}\n`, "utf8");
|
|
52
|
+
}
|
|
29
53
|
export function panelWorkerRunDirectory(workingDirectory, runId) {
|
|
30
54
|
return join(panelRunsDirectory(workingDirectory), runId);
|
|
31
55
|
}
|
|
@@ -38,16 +62,41 @@ async function writeJsonAtomic(path, value) {
|
|
|
38
62
|
await writeFile(tempPath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
39
63
|
await rename(tempPath, path);
|
|
40
64
|
}
|
|
41
|
-
function
|
|
65
|
+
function workerRuntimeDirectory(worker) {
|
|
66
|
+
return join(worker.worktreePath ?? dirname(worker.resultPath), ".longtable-worker");
|
|
67
|
+
}
|
|
68
|
+
function workerRuntimePaths(worktreePath) {
|
|
69
|
+
const workerDirectory = join(worktreePath, ".longtable-worker");
|
|
70
|
+
return {
|
|
71
|
+
taskPath: join(workerDirectory, "task.md"),
|
|
72
|
+
resultPath: join(workerDirectory, "result.json"),
|
|
73
|
+
logPath: join(workerDirectory, "worker.log"),
|
|
74
|
+
launcherPath: join(workerDirectory, "launch.sh"),
|
|
75
|
+
exitCodePath: join(workerDirectory, "result.exit.json"),
|
|
76
|
+
mailboxPath: join(workerDirectory, "mailbox.jsonl"),
|
|
77
|
+
taskStatePath: join(workerDirectory, "state.json")
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function shouldNormalizeWorkerRuntimePaths(run, worker) {
|
|
81
|
+
if (worker.status === "completed" || worker.status === "blocked" || worker.status === "running") {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
return run.status === "planned" ||
|
|
85
|
+
run.status === "failed" ||
|
|
86
|
+
run.status === "stopped" ||
|
|
87
|
+
run.status === "stop_requested" ||
|
|
88
|
+
run.status === "resumable";
|
|
89
|
+
}
|
|
90
|
+
function workerTaskPrompt(run, worker) {
|
|
42
91
|
return [
|
|
43
92
|
"LongTable native panel worker",
|
|
44
93
|
"",
|
|
45
94
|
`Role: ${worker.label} (${worker.role})`,
|
|
46
|
-
`Invocation: ${
|
|
47
|
-
`Panel plan: ${
|
|
95
|
+
`Invocation: ${run.invocationId}`,
|
|
96
|
+
`Panel plan: ${run.planId}`,
|
|
48
97
|
"",
|
|
49
98
|
"Instructions:",
|
|
50
|
-
"- Work
|
|
99
|
+
"- Work inside the assigned writable worker worktree; do not mutate the leader checkout directly.",
|
|
51
100
|
"- Do not expose or persist hidden reasoning, private tool traces, or chain-of-thought.",
|
|
52
101
|
"- Persist only the structured final role output to the result path below.",
|
|
53
102
|
"- Return JSON matching this shape:",
|
|
@@ -55,9 +104,12 @@ function workerTaskPrompt(fallback, worker) {
|
|
|
55
104
|
"",
|
|
56
105
|
`Result path: ${worker.resultPath}`,
|
|
57
106
|
`Log path: ${worker.logPath}`,
|
|
107
|
+
worker.worktreePath ? `Writable worker worktree: ${worker.worktreePath}` : "",
|
|
108
|
+
worker.mailboxPath ? `Worker mailbox: ${worker.mailboxPath}` : "",
|
|
109
|
+
worker.taskStatePath ? `Worker task lifecycle state: ${worker.taskStatePath}` : "",
|
|
58
110
|
"",
|
|
59
111
|
"Research object:",
|
|
60
|
-
|
|
112
|
+
run.prompt
|
|
61
113
|
].join("\n");
|
|
62
114
|
}
|
|
63
115
|
const PANEL_WORKER_OUTPUT_SCHEMA = {
|
|
@@ -80,18 +132,22 @@ function launcherScript(run, worker) {
|
|
|
80
132
|
const role = shellQuote(worker.role);
|
|
81
133
|
const label = shellQuote(worker.label);
|
|
82
134
|
const stdoutPath = `${worker.resultPath}.stdout`;
|
|
135
|
+
const worktreePath = worker.worktreePath ?? run.workingDirectory;
|
|
136
|
+
const commitPath = `${worker.resultPath}.commit`;
|
|
83
137
|
return [
|
|
84
138
|
"#!/usr/bin/env bash",
|
|
85
139
|
"set +e",
|
|
86
140
|
`printf 'started_at=%s\\n' "$(date -u +'%Y-%m-%dT%H:%M:%SZ')" > ${shellQuote(worker.logPath)}`,
|
|
141
|
+
`mkdir -p ${shellQuote(join(worktreePath, ".longtable-worker"))}`,
|
|
142
|
+
`printf '{"workerId":%s,"runId":%s,"startedAt":"%s"}\\n' ${shellQuote(JSON.stringify(worker.id))} ${shellQuote(JSON.stringify(run.id))} "$(date -u +'%Y-%m-%dT%H:%M:%SZ')" > ${shellQuote(join(worktreePath, ".longtable-worker", `${worker.id}.json`))}`,
|
|
87
143
|
`if [ -f ${shellQuote(run.stopFilePath)} ]; then`,
|
|
88
144
|
` printf 'stop_requested_before_launch\\n' >> ${shellQuote(worker.logPath)}`,
|
|
89
145
|
" exit 0",
|
|
90
146
|
"fi",
|
|
91
147
|
[
|
|
92
148
|
"codex exec",
|
|
93
|
-
"-s
|
|
94
|
-
`-C ${shellQuote(
|
|
149
|
+
"-s workspace-write",
|
|
150
|
+
`-C ${shellQuote(worktreePath)}`,
|
|
95
151
|
"--skip-git-repo-check",
|
|
96
152
|
`--output-schema ${shellQuote(run.outputSchemaPath)}`,
|
|
97
153
|
"-",
|
|
@@ -100,7 +156,7 @@ function launcherScript(run, worker) {
|
|
|
100
156
|
].join(" "),
|
|
101
157
|
"code=$?",
|
|
102
158
|
`if [ -s ${shellQuote(stdoutPath)} ]; then`,
|
|
103
|
-
` node -e 'const fs=require("fs"); const [stdoutPath,resultPath]=process.argv.slice(1); const parsed=JSON.parse(fs.readFileSync(stdoutPath,"utf8").trim()); fs.writeFileSync(resultPath, JSON.stringify(
|
|
159
|
+
` node -e 'const fs=require("fs"); const [stdoutPath,resultPath]=process.argv.slice(1); const parsed=JSON.parse(fs.readFileSync(stdoutPath,"utf8").trim()); const strings=(value)=>Array.isArray(value)?value.filter((entry)=>typeof entry==="string"):[]; const status=["completed","blocked","error"].includes(parsed.status)?parsed.status:"error"; const sanitized={role:typeof parsed.role==="string"?parsed.role:"",label:typeof parsed.label==="string"?parsed.label:"",status,summary:typeof parsed.summary==="string"?parsed.summary:"",claims:strings(parsed.claims),objections:strings(parsed.objections),openQuestions:strings(parsed.openQuestions),evidenceRefs:strings(parsed.evidenceRefs),error:typeof parsed.error==="string"?parsed.error:""}; fs.writeFileSync(resultPath, JSON.stringify(sanitized, null, 2)+"\\n");' ${shellQuote(stdoutPath)} ${shellQuote(worker.resultPath)}`,
|
|
104
160
|
" parse_code=$?",
|
|
105
161
|
` rm -f ${shellQuote(stdoutPath)}`,
|
|
106
162
|
` if [ "$parse_code" -ne 0 ]; then code=1; fi`,
|
|
@@ -110,6 +166,12 @@ function launcherScript(run, worker) {
|
|
|
110
166
|
`if [ "$code" -ne 0 ] && [ ! -s ${shellQuote(worker.resultPath)} ]; then`,
|
|
111
167
|
` node -e 'const fs=require("fs"); const [path,role,label,code]=process.argv.slice(1); fs.writeFileSync(path, JSON.stringify({role,label,status:"error",summary:"",claims:[],objections:[],openQuestions:[],evidenceRefs:[],error:\`codex exec exited ${"${code}"}\`}, null, 2)+"\\n");' ${shellQuote(worker.resultPath)} ${role} ${label} "$code"`,
|
|
112
168
|
"fi",
|
|
169
|
+
`if git -C ${shellQuote(worktreePath)} status --porcelain >/tmp/longtable-worker-status.$$ 2>/dev/null && [ -s /tmp/longtable-worker-status.$$ ]; then`,
|
|
170
|
+
` git -C ${shellQuote(worktreePath)} add -A >/dev/null 2>&1`,
|
|
171
|
+
` git -C ${shellQuote(worktreePath)} -c user.name='LongTable Panel Worker' -c user.email='longtable-panel-worker@example.invalid' commit -m ${shellQuote(`longtable panel worker ${worker.id}`)} >/dev/null 2>&1`,
|
|
172
|
+
"fi",
|
|
173
|
+
`rm -f /tmp/longtable-worker-status.$$`,
|
|
174
|
+
`git -C ${shellQuote(worktreePath)} rev-parse HEAD > ${shellQuote(commitPath)} 2>/dev/null`,
|
|
113
175
|
"exit $code",
|
|
114
176
|
""
|
|
115
177
|
].join("\n");
|
|
@@ -125,24 +187,32 @@ export async function createPanelWorkerRun(options) {
|
|
|
125
187
|
const resultDirectory = join(runDirectory, "results");
|
|
126
188
|
const logDirectory = join(runDirectory, "logs");
|
|
127
189
|
const launcherDirectory = join(runDirectory, "launchers");
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
await mkdir(
|
|
131
|
-
await mkdir(
|
|
190
|
+
const worktreeDirectory = join(runDirectory, "worktrees");
|
|
191
|
+
const mailboxDirectory = join(runDirectory, "mailbox");
|
|
192
|
+
await mkdir(runDirectory, { recursive: true });
|
|
193
|
+
await mkdir(worktreeDirectory, { recursive: true });
|
|
132
194
|
const runStatus = options.initialStatus ?? "planned";
|
|
133
195
|
const workers = options.fallback.plan.members.map((member, index) => {
|
|
134
196
|
const workerId = `worker-${index + 1}-${member.role}`;
|
|
197
|
+
const worktreePath = join(worktreeDirectory, workerSafeName(workerId));
|
|
198
|
+
const paths = workerRuntimePaths(worktreePath);
|
|
135
199
|
return {
|
|
136
200
|
id: workerId,
|
|
137
201
|
role: member.role,
|
|
138
202
|
label: member.label,
|
|
139
203
|
required: member.required,
|
|
140
204
|
status: plannedWorkerStatus(runStatus),
|
|
141
|
-
taskPath:
|
|
142
|
-
resultPath:
|
|
143
|
-
logPath:
|
|
144
|
-
launcherPath:
|
|
145
|
-
exitCodePath:
|
|
205
|
+
taskPath: paths.taskPath,
|
|
206
|
+
resultPath: paths.resultPath,
|
|
207
|
+
logPath: paths.logPath,
|
|
208
|
+
launcherPath: paths.launcherPath,
|
|
209
|
+
exitCodePath: paths.exitCodePath,
|
|
210
|
+
worktreePath,
|
|
211
|
+
worktreeBranch: safeName(`longtable/panel/${runId}/${workerId}`),
|
|
212
|
+
mailboxPath: paths.mailboxPath,
|
|
213
|
+
taskStatePath: paths.taskStatePath,
|
|
214
|
+
cleanupStatus: "not_started",
|
|
215
|
+
executionState: "not_started",
|
|
146
216
|
updatedAt: createdAt,
|
|
147
217
|
diagnostics: []
|
|
148
218
|
};
|
|
@@ -167,9 +237,14 @@ export async function createPanelWorkerRun(options) {
|
|
|
167
237
|
resultDirectory,
|
|
168
238
|
logDirectory,
|
|
169
239
|
launcherDirectory,
|
|
240
|
+
worktreeDirectory,
|
|
241
|
+
mailboxDirectory,
|
|
242
|
+
eventLogPath: join(runDirectory, "events.jsonl"),
|
|
170
243
|
outputSchemaPath: join(runDirectory, "panel-worker-output.schema.json"),
|
|
171
244
|
stopFilePath: join(runDirectory, "stop-requested"),
|
|
172
245
|
aggregateResultPath: join(runDirectory, "panel-result.json"),
|
|
246
|
+
bridgeStatus: runStatus === "planned" ? "not_requested" : "running",
|
|
247
|
+
sequentialFallbackAvailable: true,
|
|
173
248
|
workers,
|
|
174
249
|
diagnostics: options.diagnostics ?? []
|
|
175
250
|
};
|
|
@@ -179,7 +254,7 @@ export async function createPanelWorkerRun(options) {
|
|
|
179
254
|
note: "Durable worker state stores task/status/result metadata, not hidden reasoning or raw tool traces."
|
|
180
255
|
});
|
|
181
256
|
await writeJsonAtomic(run.outputSchemaPath, PANEL_WORKER_OUTPUT_SCHEMA);
|
|
182
|
-
await
|
|
257
|
+
await appendLifecycleEvent(run, { type: "run_created", message: "LongTable native worker bridge run created." });
|
|
183
258
|
await writePanelWorkerRun(run);
|
|
184
259
|
return run;
|
|
185
260
|
}
|
|
@@ -189,17 +264,38 @@ export async function readPanelWorkerRun(workingDirectory, runId) {
|
|
|
189
264
|
function normalizePanelWorkerRun(run) {
|
|
190
265
|
const launcherDirectory = run.launcherDirectory ?? join(run.runDirectory, "launchers");
|
|
191
266
|
const resultDirectory = run.resultDirectory ?? join(run.runDirectory, "results");
|
|
267
|
+
const worktreeDirectory = run.worktreeDirectory ?? join(run.runDirectory, "worktrees");
|
|
268
|
+
const mailboxDirectory = run.mailboxDirectory ?? join(run.runDirectory, "mailbox");
|
|
192
269
|
return {
|
|
193
270
|
...run,
|
|
194
271
|
launcherDirectory,
|
|
272
|
+
worktreeDirectory,
|
|
273
|
+
mailboxDirectory,
|
|
274
|
+
eventLogPath: run.eventLogPath ?? join(run.runDirectory, "events.jsonl"),
|
|
275
|
+
bridgeStatus: run.bridgeStatus ?? bridgeStatusFromRunStatus(run.status),
|
|
276
|
+
sequentialFallbackAvailable: run.sequentialFallbackAvailable ?? true,
|
|
195
277
|
outputSchemaPath: run.outputSchemaPath ?? join(run.runDirectory, "panel-worker-output.schema.json"),
|
|
196
278
|
stopFilePath: run.stopFilePath ?? join(run.runDirectory, "stop-requested"),
|
|
197
|
-
workers: run.workers.map((worker) =>
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
279
|
+
workers: run.workers.map((worker) => {
|
|
280
|
+
const worktreePath = worker.worktreePath ?? join(worktreeDirectory, workerSafeName(worker.id));
|
|
281
|
+
const runtimePaths = workerRuntimePaths(worktreePath);
|
|
282
|
+
const normalizePaths = shouldNormalizeWorkerRuntimePaths(run, worker);
|
|
283
|
+
return {
|
|
284
|
+
...worker,
|
|
285
|
+
taskPath: normalizePaths ? runtimePaths.taskPath : worker.taskPath,
|
|
286
|
+
resultPath: normalizePaths ? runtimePaths.resultPath : worker.resultPath,
|
|
287
|
+
logPath: normalizePaths ? runtimePaths.logPath : worker.logPath,
|
|
288
|
+
launcherPath: normalizePaths ? runtimePaths.launcherPath : worker.launcherPath ?? join(launcherDirectory, `${worker.id}.sh`),
|
|
289
|
+
exitCodePath: normalizePaths ? runtimePaths.exitCodePath : worker.exitCodePath ?? join(resultDirectory, `${worker.id}.exit.json`),
|
|
290
|
+
worktreePath,
|
|
291
|
+
worktreeBranch: worker.worktreeBranch ?? safeName(`longtable/panel/${run.id}/${worker.id}`),
|
|
292
|
+
mailboxPath: normalizePaths ? runtimePaths.mailboxPath : worker.mailboxPath ?? join(mailboxDirectory, `${worker.id}.jsonl`),
|
|
293
|
+
taskStatePath: normalizePaths ? runtimePaths.taskStatePath : worker.taskStatePath ?? join(run.taskDirectory, `${worker.id}.state.json`),
|
|
294
|
+
cleanupStatus: worker.cleanupStatus ?? "not_started",
|
|
295
|
+
executionState: worker.executionState ?? "not_started",
|
|
296
|
+
diagnostics: worker.diagnostics ?? []
|
|
297
|
+
};
|
|
298
|
+
})
|
|
203
299
|
};
|
|
204
300
|
}
|
|
205
301
|
export async function writePanelWorkerRun(run) {
|
|
@@ -207,6 +303,24 @@ export async function writePanelWorkerRun(run) {
|
|
|
207
303
|
...run,
|
|
208
304
|
updatedAt: nowIso()
|
|
209
305
|
});
|
|
306
|
+
await Promise.all(run.workers.map((worker) => worker.taskStatePath
|
|
307
|
+
&& existsSync(dirname(worker.taskStatePath))
|
|
308
|
+
&& (!worker.worktreePath || existsSync(worker.worktreePath))
|
|
309
|
+
? writeJsonAtomic(worker.taskStatePath, {
|
|
310
|
+
workerId: worker.id,
|
|
311
|
+
status: worker.status,
|
|
312
|
+
executionState: worker.executionState,
|
|
313
|
+
paneId: worker.paneId,
|
|
314
|
+
runtime: worker.runtime,
|
|
315
|
+
worktreePath: worker.worktreePath,
|
|
316
|
+
worktreeBranch: worker.worktreeBranch,
|
|
317
|
+
worktreeCommit: worker.worktreeCommit,
|
|
318
|
+
cleanupStatus: worker.cleanupStatus,
|
|
319
|
+
failureReason: worker.failureReason,
|
|
320
|
+
error: worker.error,
|
|
321
|
+
updatedAt: worker.updatedAt
|
|
322
|
+
})
|
|
323
|
+
: Promise.resolve()));
|
|
210
324
|
}
|
|
211
325
|
function parseWorkerResult(worker) {
|
|
212
326
|
if (!existsSync(worker.resultPath)) {
|
|
@@ -242,7 +356,7 @@ function statusFromWorkers(workers) {
|
|
|
242
356
|
return "running";
|
|
243
357
|
}
|
|
244
358
|
if (workers.some((worker) => worker.status === "failed")) {
|
|
245
|
-
return "
|
|
359
|
+
return "failed";
|
|
246
360
|
}
|
|
247
361
|
if (workers.some((worker) => worker.status === "blocked")) {
|
|
248
362
|
return "blocked";
|
|
@@ -255,6 +369,15 @@ function statusFromWorkers(workers) {
|
|
|
255
369
|
}
|
|
256
370
|
return "running";
|
|
257
371
|
}
|
|
372
|
+
function bridgeStatusFromRunStatus(status) {
|
|
373
|
+
return status === "planned" || status === "resumable" ? "not_requested" : status;
|
|
374
|
+
}
|
|
375
|
+
function bridgeStatusFromWorkers(status, workers) {
|
|
376
|
+
if (status === "failed" && workers.every((worker) => worker.executionState === "preflight_failed")) {
|
|
377
|
+
return "preflight_failed";
|
|
378
|
+
}
|
|
379
|
+
return bridgeStatusFromRunStatus(status);
|
|
380
|
+
}
|
|
258
381
|
function tmuxPaneAlive(paneId) {
|
|
259
382
|
if (!commandAvailable("tmux")) {
|
|
260
383
|
return false;
|
|
@@ -268,35 +391,169 @@ function tmuxPaneAlive(paneId) {
|
|
|
268
391
|
}
|
|
269
392
|
}
|
|
270
393
|
function terminalStatus(status) {
|
|
271
|
-
return status === "completed" || status === "blocked" || status === "stopped" || status === "degraded";
|
|
394
|
+
return status === "completed" || status === "blocked" || status === "stopped" || status === "degraded" || status === "failed";
|
|
272
395
|
}
|
|
273
|
-
function
|
|
274
|
-
const
|
|
396
|
+
function preflightNativeWorkerBridge(run) {
|
|
397
|
+
const failures = [
|
|
398
|
+
commandAvailable("tmux") ? null : "tmux:unavailable",
|
|
399
|
+
commandAvailable("codex") ? null : "codex:unavailable",
|
|
400
|
+
commandAvailable("git") ? null : "git:unavailable"
|
|
401
|
+
].filter((entry) => Boolean(entry));
|
|
402
|
+
if (!process.env.TMUX && !process.env.TMUX_PANE) {
|
|
403
|
+
failures.push("tmux:attached-pane-unavailable");
|
|
404
|
+
}
|
|
405
|
+
try {
|
|
406
|
+
execFileSync("tmux", ["display-message", "-p", "#{pane_id}"], { stdio: "ignore" });
|
|
407
|
+
}
|
|
408
|
+
catch {
|
|
409
|
+
failures.push("tmux:current-pane-unavailable");
|
|
410
|
+
}
|
|
275
411
|
try {
|
|
276
|
-
|
|
277
|
-
"new-window",
|
|
278
|
-
"-d",
|
|
279
|
-
"-P",
|
|
280
|
-
"-F",
|
|
281
|
-
"#{pane_id}",
|
|
282
|
-
"-n",
|
|
283
|
-
`lt-${worker.id.slice(0, 12)}`,
|
|
284
|
-
command
|
|
285
|
-
], { encoding: "utf8" }).trim();
|
|
412
|
+
gitOutput(run.workingDirectory, ["rev-parse", "--is-inside-work-tree"]);
|
|
286
413
|
}
|
|
287
414
|
catch {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
415
|
+
failures.push("git:working-directory-unavailable");
|
|
416
|
+
}
|
|
417
|
+
return failures;
|
|
418
|
+
}
|
|
419
|
+
function applyLeaderTrackedChanges(run, worker) {
|
|
420
|
+
if (!worker.worktreePath) {
|
|
421
|
+
return undefined;
|
|
422
|
+
}
|
|
423
|
+
const patch = execFileSync("git", ["diff", "--binary", "HEAD"], {
|
|
424
|
+
cwd: run.workingDirectory,
|
|
425
|
+
encoding: "buffer",
|
|
426
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
427
|
+
});
|
|
428
|
+
if (patch.length === 0) {
|
|
429
|
+
return undefined;
|
|
430
|
+
}
|
|
431
|
+
try {
|
|
432
|
+
execFileSync("git", ["apply", "--check", "--whitespace=nowarn", "-"], {
|
|
433
|
+
cwd: worker.worktreePath,
|
|
434
|
+
input: patch,
|
|
435
|
+
stdio: ["pipe", "ignore", "pipe"]
|
|
436
|
+
});
|
|
437
|
+
execFileSync("git", ["apply", "--whitespace=nowarn", "-"], {
|
|
438
|
+
cwd: worker.worktreePath,
|
|
439
|
+
input: patch,
|
|
440
|
+
stdio: ["pipe", "ignore", "pipe"]
|
|
441
|
+
});
|
|
442
|
+
return "Leader tracked workspace changes were applied to the worker worktree before launch.";
|
|
443
|
+
}
|
|
444
|
+
catch (error) {
|
|
445
|
+
try {
|
|
446
|
+
execFileSync("git", ["apply", "--reverse", "--check", "--whitespace=nowarn", "-"], {
|
|
447
|
+
cwd: worker.worktreePath,
|
|
448
|
+
input: patch,
|
|
449
|
+
stdio: ["pipe", "ignore", "pipe"]
|
|
450
|
+
});
|
|
451
|
+
return "Leader tracked workspace changes were already present in the worker worktree before launch.";
|
|
452
|
+
}
|
|
453
|
+
catch {
|
|
454
|
+
throw error;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
async function markBridgePreflightFailed(run, failures) {
|
|
459
|
+
const updatedAt = nowIso();
|
|
460
|
+
const reason = failures.join(", ");
|
|
461
|
+
const nextRun = {
|
|
462
|
+
...run,
|
|
463
|
+
status: "failed",
|
|
464
|
+
bridgeStatus: "preflight_failed",
|
|
465
|
+
bridgeFailureReason: reason,
|
|
466
|
+
updatedAt,
|
|
467
|
+
diagnostics: [...run.diagnostics, ...failures, "Native worker bridge preflight failed; sequential fallback was not executed implicitly."],
|
|
468
|
+
workers: run.workers.map((worker) => ({
|
|
469
|
+
...worker,
|
|
470
|
+
status: worker.status === "completed" ? worker.status : "failed",
|
|
471
|
+
executionState: "preflight_failed",
|
|
472
|
+
failureReason: reason,
|
|
473
|
+
error: reason,
|
|
474
|
+
updatedAt,
|
|
475
|
+
diagnostics: appendDiagnostic(worker.diagnostics, "Native worker bridge preflight failed before launch.")
|
|
476
|
+
}))
|
|
477
|
+
};
|
|
478
|
+
await appendLifecycleEvent(nextRun, { type: "preflight_failed", message: reason });
|
|
479
|
+
await writePanelWorkerRun(nextRun);
|
|
480
|
+
return nextRun;
|
|
481
|
+
}
|
|
482
|
+
async function provisionWorkerWorktree(run, worker) {
|
|
483
|
+
if (!worker.worktreePath || !worker.worktreeBranch) {
|
|
484
|
+
throw new Error(`missing worktree metadata for ${worker.id}`);
|
|
299
485
|
}
|
|
486
|
+
await mkdir(dirname(worker.worktreePath), { recursive: true });
|
|
487
|
+
if (!existsSync(worker.worktreePath)) {
|
|
488
|
+
try {
|
|
489
|
+
gitOutput(run.workingDirectory, ["worktree", "add", worker.worktreePath, "-b", worker.worktreeBranch, "HEAD"]);
|
|
490
|
+
}
|
|
491
|
+
catch (firstError) {
|
|
492
|
+
try {
|
|
493
|
+
gitOutput(run.workingDirectory, ["worktree", "add", worker.worktreePath, worker.worktreeBranch]);
|
|
494
|
+
}
|
|
495
|
+
catch {
|
|
496
|
+
throw firstError;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
const commit = currentGitCommit(worker.worktreePath);
|
|
501
|
+
const snapshotDiagnostic = applyLeaderTrackedChanges(run, worker);
|
|
502
|
+
await mkdir(workerRuntimeDirectory(worker), { recursive: true });
|
|
503
|
+
await writeFile(worker.mailboxPath ?? join(workerRuntimeDirectory(worker), "mailbox.jsonl"), "", { flag: "a" });
|
|
504
|
+
await appendLifecycleEvent(run, {
|
|
505
|
+
workerId: worker.id,
|
|
506
|
+
type: "worktree_provisioned",
|
|
507
|
+
message: `Worker worktree provisioned at ${worker.worktreePath}.`,
|
|
508
|
+
path: worker.worktreePath
|
|
509
|
+
});
|
|
510
|
+
return {
|
|
511
|
+
...worker,
|
|
512
|
+
worktreeCommit: commit,
|
|
513
|
+
cleanupStatus: "retained",
|
|
514
|
+
executionState: "provisioned",
|
|
515
|
+
diagnostics: snapshotDiagnostic
|
|
516
|
+
? appendDiagnostic(appendDiagnostic(worker.diagnostics, "Writable git worktree provisioned for LongTable native worker."), snapshotDiagnostic)
|
|
517
|
+
: appendDiagnostic(worker.diagnostics, "Writable git worktree provisioned for LongTable native worker.")
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
async function writeWorkerTask(run, worker) {
|
|
521
|
+
await mkdir(workerRuntimeDirectory(worker), { recursive: true });
|
|
522
|
+
await writeFile(worker.taskPath, workerTaskPrompt(run, worker), "utf8");
|
|
523
|
+
}
|
|
524
|
+
async function clearWorkerAttemptArtifacts(worker) {
|
|
525
|
+
const paths = [
|
|
526
|
+
worker.resultPath,
|
|
527
|
+
`${worker.resultPath}.stdout`,
|
|
528
|
+
`${worker.resultPath}.commit`,
|
|
529
|
+
worker.exitCodePath,
|
|
530
|
+
worker.logPath
|
|
531
|
+
].filter((path) => typeof path === "string" && path.length > 0);
|
|
532
|
+
await Promise.all(paths.map((path) => rm(path, { force: true })));
|
|
533
|
+
}
|
|
534
|
+
function launchWorkerPane(run, worker) {
|
|
535
|
+
const command = `cd ${shellQuote(worker.worktreePath ?? run.workingDirectory)} && bash ${shellQuote(worker.launcherPath)}`;
|
|
536
|
+
const args = [
|
|
537
|
+
"split-window",
|
|
538
|
+
"-d",
|
|
539
|
+
"-P",
|
|
540
|
+
"-F",
|
|
541
|
+
"#{pane_id}"
|
|
542
|
+
];
|
|
543
|
+
if (process.env.TMUX_PANE) {
|
|
544
|
+
args.push("-t", process.env.TMUX_PANE);
|
|
545
|
+
}
|
|
546
|
+
args.push(command);
|
|
547
|
+
const paneId = execFileSync("tmux", args, { encoding: "utf8" }).trim();
|
|
548
|
+
if (paneId) {
|
|
549
|
+
try {
|
|
550
|
+
execFileSync("tmux", ["set-option", "-p", "-t", paneId, "remain-on-exit", "on"], { stdio: "ignore" });
|
|
551
|
+
}
|
|
552
|
+
catch {
|
|
553
|
+
// Pane retention is best-effort on older tmux versions; the pane id is still recorded.
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
return paneId;
|
|
300
557
|
}
|
|
301
558
|
async function writeWorkerLauncher(run, worker) {
|
|
302
559
|
if (!worker.launcherPath) {
|
|
@@ -309,66 +566,81 @@ export async function launchPanelWorkerRun(run) {
|
|
|
309
566
|
if (terminalStatus(run.status) || run.status === "stop_requested") {
|
|
310
567
|
return run;
|
|
311
568
|
}
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
const unavailable = [
|
|
316
|
-
tmuxAvailable ? null : "tmux:unavailable",
|
|
317
|
-
codexAvailable ? null : "codex:unavailable"
|
|
318
|
-
].filter((entry) => Boolean(entry));
|
|
319
|
-
const nextRun = {
|
|
320
|
-
...run,
|
|
321
|
-
status: "degraded",
|
|
322
|
-
updatedAt: nowIso(),
|
|
323
|
-
diagnostics: [...run.diagnostics, ...unavailable, "Native worker launch degraded; use sequential_fallback or resume when local runtime is available."],
|
|
324
|
-
workers: run.workers.map((worker) => worker.status === "completed"
|
|
325
|
-
? worker
|
|
326
|
-
: {
|
|
327
|
-
...worker,
|
|
328
|
-
status: "pending",
|
|
329
|
-
updatedAt: nowIso(),
|
|
330
|
-
diagnostics: appendDiagnostic(worker.diagnostics, "Launch skipped because tmux or codex is unavailable.")
|
|
331
|
-
})
|
|
332
|
-
};
|
|
333
|
-
await writePanelWorkerRun(nextRun);
|
|
334
|
-
return nextRun;
|
|
569
|
+
const preflightFailures = preflightNativeWorkerBridge(run);
|
|
570
|
+
if (preflightFailures.length > 0) {
|
|
571
|
+
return markBridgePreflightFailed(run, preflightFailures);
|
|
335
572
|
}
|
|
336
573
|
const launchedAt = nowIso();
|
|
337
|
-
const workers =
|
|
574
|
+
const workers = [];
|
|
575
|
+
for (const worker of run.workers) {
|
|
338
576
|
if (worker.status !== "pending" && worker.status !== "stopped" && worker.status !== "failed") {
|
|
339
|
-
|
|
577
|
+
workers.push(worker);
|
|
578
|
+
continue;
|
|
340
579
|
}
|
|
341
|
-
const launchable = {
|
|
342
|
-
...worker,
|
|
343
|
-
launcherPath: worker.launcherPath ?? join(run.launcherDirectory, `${worker.id}.sh`),
|
|
344
|
-
exitCodePath: worker.exitCodePath ?? join(run.resultDirectory, `${worker.id}.exit.json`)
|
|
345
|
-
};
|
|
346
|
-
await writeWorkerLauncher(run, launchable);
|
|
347
580
|
try {
|
|
581
|
+
const provisioned = await provisionWorkerWorktree(run, {
|
|
582
|
+
...worker,
|
|
583
|
+
launcherPath: worker.launcherPath ?? join(run.launcherDirectory, `${worker.id}.sh`),
|
|
584
|
+
exitCodePath: worker.exitCodePath ?? join(run.resultDirectory, `${worker.id}.exit.json`),
|
|
585
|
+
executionState: "provisioning"
|
|
586
|
+
});
|
|
587
|
+
const launchable = {
|
|
588
|
+
...provisioned,
|
|
589
|
+
executionState: "launching"
|
|
590
|
+
};
|
|
591
|
+
await writeWorkerTask(run, launchable);
|
|
592
|
+
await writeWorkerLauncher(run, launchable);
|
|
348
593
|
const paneId = launchWorkerPane(run, launchable);
|
|
349
|
-
|
|
594
|
+
const launchedWorker = {
|
|
350
595
|
...launchable,
|
|
351
596
|
status: "running",
|
|
352
597
|
paneId: paneId || launchable.paneId,
|
|
598
|
+
runtime: {
|
|
599
|
+
transport: "tmux",
|
|
600
|
+
paneId: paneId || launchable.paneId,
|
|
601
|
+
paneTarget: process.env.TMUX_PANE,
|
|
602
|
+
splitCommand: "split-window",
|
|
603
|
+
retainPane: true,
|
|
604
|
+
launchedAt
|
|
605
|
+
},
|
|
606
|
+
executionState: "running",
|
|
353
607
|
startedAt: launchedAt,
|
|
354
608
|
updatedAt: launchedAt,
|
|
355
|
-
diagnostics: appendDiagnostic(launchable.diagnostics, "Launched
|
|
609
|
+
diagnostics: appendDiagnostic(launchable.diagnostics, "Launched in a retained current-window tmux split pane with Codex workspace-write scoped to the worker worktree.")
|
|
356
610
|
};
|
|
611
|
+
await appendLifecycleEvent(run, {
|
|
612
|
+
workerId: worker.id,
|
|
613
|
+
type: "worker_launched",
|
|
614
|
+
message: `Worker launched in tmux split pane ${paneId}.`,
|
|
615
|
+
path: launchable.worktreePath
|
|
616
|
+
});
|
|
617
|
+
workers.push(launchedWorker);
|
|
357
618
|
}
|
|
358
619
|
catch (error) {
|
|
359
|
-
|
|
360
|
-
...
|
|
620
|
+
const failedWorker = {
|
|
621
|
+
...worker,
|
|
361
622
|
status: "failed",
|
|
623
|
+
executionState: worker.worktreePath && existsSync(worker.worktreePath) ? "launch_failed" : "provision_failed",
|
|
624
|
+
failureReason: error instanceof Error ? error.message : String(error),
|
|
362
625
|
updatedAt: nowIso(),
|
|
363
626
|
error: error instanceof Error ? error.message : String(error),
|
|
364
|
-
diagnostics: appendDiagnostic(
|
|
627
|
+
diagnostics: appendDiagnostic(worker.diagnostics, "Native worker bridge provisioning or tmux launch failed before worker result was created.")
|
|
365
628
|
};
|
|
629
|
+
await appendLifecycleEvent(run, {
|
|
630
|
+
workerId: worker.id,
|
|
631
|
+
type: "worker_failed",
|
|
632
|
+
message: failedWorker.error ?? "worker launch failed",
|
|
633
|
+
path: worker.worktreePath
|
|
634
|
+
});
|
|
635
|
+
workers.push(failedWorker);
|
|
366
636
|
}
|
|
367
|
-
}
|
|
637
|
+
}
|
|
368
638
|
const nextRun = {
|
|
369
639
|
...run,
|
|
370
640
|
workers,
|
|
371
641
|
status: statusFromWorkers(workers),
|
|
642
|
+
bridgeStatus: bridgeStatusFromWorkers(statusFromWorkers(workers), workers),
|
|
643
|
+
bridgeFailureReason: workers.find((worker) => worker.status === "failed")?.error,
|
|
372
644
|
updatedAt: nowIso()
|
|
373
645
|
};
|
|
374
646
|
await writePanelWorkerRun(nextRun);
|
|
@@ -405,12 +677,21 @@ export async function refreshPanelWorkerRun(run) {
|
|
|
405
677
|
}
|
|
406
678
|
memberResults.push(result);
|
|
407
679
|
const nextStatus = result.status === "error" ? "failed" : result.status === "blocked" ? "blocked" : "completed";
|
|
680
|
+
const commitPath = `${worker.resultPath}.commit`;
|
|
681
|
+
const worktreeCommit = existsSync(commitPath)
|
|
682
|
+
? readFileSyncUtf8(commitPath).trim()
|
|
683
|
+
: worker.worktreePath
|
|
684
|
+
? currentGitCommit(worker.worktreePath)
|
|
685
|
+
: worker.worktreeCommit;
|
|
408
686
|
return {
|
|
409
687
|
...worker,
|
|
410
688
|
status: nextStatus,
|
|
689
|
+
executionState: nextStatus === "completed" || nextStatus === "blocked" ? "result_ready" : "launch_failed",
|
|
411
690
|
completedAt: nextStatus === "failed" ? worker.completedAt : updatedAt,
|
|
691
|
+
worktreeCommit,
|
|
412
692
|
updatedAt,
|
|
413
|
-
error: result.error
|
|
693
|
+
error: result.error,
|
|
694
|
+
failureReason: nextStatus === "failed" ? result.error : worker.failureReason
|
|
414
695
|
};
|
|
415
696
|
}
|
|
416
697
|
catch (error) {
|
|
@@ -427,6 +708,8 @@ export async function refreshPanelWorkerRun(run) {
|
|
|
427
708
|
...run,
|
|
428
709
|
workers,
|
|
429
710
|
status: statusFromWorkers(workers),
|
|
711
|
+
bridgeStatus: bridgeStatusFromWorkers(statusFromWorkers(workers), workers),
|
|
712
|
+
bridgeFailureReason: workers.find((worker) => worker.status === "failed")?.error,
|
|
430
713
|
updatedAt
|
|
431
714
|
};
|
|
432
715
|
if (nextRun.status === "completed" || nextRun.status === "blocked") {
|
|
@@ -457,6 +740,7 @@ export async function requestPanelWorkerStop(run) {
|
|
|
457
740
|
const nextRun = {
|
|
458
741
|
...run,
|
|
459
742
|
status: "stop_requested",
|
|
743
|
+
bridgeStatus: "stop_requested",
|
|
460
744
|
updatedAt,
|
|
461
745
|
workers: run.workers.map((worker) => {
|
|
462
746
|
if (worker.status === "completed") {
|
|
@@ -475,20 +759,32 @@ export async function requestPanelWorkerStop(run) {
|
|
|
475
759
|
return {
|
|
476
760
|
...worker,
|
|
477
761
|
status: stopped ? "stopped" : "stop_requested",
|
|
762
|
+
executionState: stopped ? "stopped" : "stopping",
|
|
763
|
+
runtime: worker.runtime
|
|
764
|
+
? { ...worker.runtime, stoppedAt: updatedAt }
|
|
765
|
+
: worker.paneId
|
|
766
|
+
? { transport: "tmux", paneId: worker.paneId, retainPane: true, stoppedAt: updatedAt }
|
|
767
|
+
: worker.runtime,
|
|
478
768
|
updatedAt,
|
|
479
769
|
diagnostics: appendDiagnostic(worker.diagnostics, "Stop requested through LongTable panel runtime.")
|
|
480
770
|
};
|
|
481
771
|
})
|
|
482
772
|
};
|
|
773
|
+
await appendLifecycleEvent(nextRun, { type: "stop_requested", message: "Stop requested through LongTable panel runtime." });
|
|
483
774
|
await writePanelWorkerRun(nextRun);
|
|
484
775
|
return nextRun;
|
|
485
776
|
}
|
|
486
777
|
export async function resumePanelWorkerRun(run) {
|
|
487
778
|
const updatedAt = nowIso();
|
|
488
779
|
await rm(run.stopFilePath, { force: true });
|
|
780
|
+
await Promise.all(run.workers
|
|
781
|
+
.filter((worker) => worker.status !== "completed")
|
|
782
|
+
.map((worker) => clearWorkerAttemptArtifacts(worker)));
|
|
489
783
|
const nextRun = {
|
|
490
784
|
...run,
|
|
491
785
|
status: "planned",
|
|
786
|
+
bridgeStatus: "not_requested",
|
|
787
|
+
bridgeFailureReason: undefined,
|
|
492
788
|
updatedAt,
|
|
493
789
|
workers: run.workers.map((worker) => worker.status === "completed"
|
|
494
790
|
? worker
|
|
@@ -497,10 +793,73 @@ export async function resumePanelWorkerRun(run) {
|
|
|
497
793
|
status: "pending",
|
|
498
794
|
paneId: undefined,
|
|
499
795
|
error: undefined,
|
|
796
|
+
failureReason: undefined,
|
|
797
|
+
executionState: "not_started",
|
|
500
798
|
updatedAt,
|
|
501
799
|
diagnostics: appendDiagnostic(worker.diagnostics, "Resume requested; worker is ready to be relaunched.")
|
|
502
800
|
})
|
|
503
801
|
};
|
|
802
|
+
await appendLifecycleEvent(nextRun, { type: "resume_requested", message: "Resume requested; incomplete workers are ready to relaunch." });
|
|
803
|
+
await writePanelWorkerRun(nextRun);
|
|
804
|
+
return nextRun;
|
|
805
|
+
}
|
|
806
|
+
export async function shutdownPanelWorkerRun(run) {
|
|
807
|
+
const updatedAt = nowIso();
|
|
808
|
+
await writeFile(run.stopFilePath, `${updatedAt}\n`, "utf8");
|
|
809
|
+
const workers = [];
|
|
810
|
+
for (const worker of run.workers) {
|
|
811
|
+
let cleanupStatus = worker.cleanupStatus ?? "not_started";
|
|
812
|
+
let cleanupError;
|
|
813
|
+
if (worker.paneId && commandAvailable("tmux")) {
|
|
814
|
+
try {
|
|
815
|
+
execFileSync("tmux", ["kill-pane", "-t", worker.paneId], { stdio: "ignore" });
|
|
816
|
+
}
|
|
817
|
+
catch {
|
|
818
|
+
// A missing pane is already shut down for LongTable lifecycle purposes.
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
if (worker.worktreePath && existsSync(worker.worktreePath) && commandAvailable("git")) {
|
|
822
|
+
try {
|
|
823
|
+
gitOutput(run.workingDirectory, ["worktree", "remove", "--force", worker.worktreePath]);
|
|
824
|
+
cleanupStatus = "removed";
|
|
825
|
+
}
|
|
826
|
+
catch (error) {
|
|
827
|
+
cleanupStatus = "failed";
|
|
828
|
+
cleanupError = error instanceof Error ? error.message : String(error);
|
|
829
|
+
await appendLifecycleEvent(run, {
|
|
830
|
+
workerId: worker.id,
|
|
831
|
+
type: "cleanup_failed",
|
|
832
|
+
message: cleanupError,
|
|
833
|
+
path: worker.worktreePath
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
workers.push({
|
|
838
|
+
...worker,
|
|
839
|
+
status: worker.status === "completed" ? "completed" : "stopped",
|
|
840
|
+
executionState: cleanupStatus === "failed" ? "cleanup_failed" : "shutdown",
|
|
841
|
+
shutdownRequestedAt: updatedAt,
|
|
842
|
+
cleanupStatus,
|
|
843
|
+
error: cleanupError ?? worker.error,
|
|
844
|
+
failureReason: cleanupError ?? worker.failureReason,
|
|
845
|
+
runtime: worker.runtime
|
|
846
|
+
? { ...worker.runtime, shutdownAt: updatedAt }
|
|
847
|
+
: worker.paneId
|
|
848
|
+
? { transport: "tmux", paneId: worker.paneId, retainPane: true, shutdownAt: updatedAt }
|
|
849
|
+
: worker.runtime,
|
|
850
|
+
updatedAt,
|
|
851
|
+
diagnostics: appendDiagnostic(worker.diagnostics, "Shutdown requested through LongTable panel runtime.")
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
const nextRun = {
|
|
855
|
+
...run,
|
|
856
|
+
status: workers.some((worker) => worker.cleanupStatus === "failed") ? "failed" : "stopped",
|
|
857
|
+
bridgeStatus: workers.some((worker) => worker.cleanupStatus === "failed") ? "failed" : "shutdown",
|
|
858
|
+
bridgeFailureReason: workers.find((worker) => worker.cleanupStatus === "failed")?.error,
|
|
859
|
+
workers,
|
|
860
|
+
updatedAt
|
|
861
|
+
};
|
|
862
|
+
await appendLifecycleEvent(nextRun, { type: "shutdown_requested", message: "Shutdown requested; panes killed and worker worktrees removed when possible." });
|
|
504
863
|
await writePanelWorkerRun(nextRun);
|
|
505
864
|
return nextRun;
|
|
506
865
|
}
|