@longtable/cli 0.1.56 → 0.1.58

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 CHANGED
@@ -18,7 +18,7 @@ import { installCodexPromptAliases, listInstalledCodexPromptAliases, removeCodex
18
18
  import { buildPersonaGuidance, parseInvocationDirective } from "./persona-router.js";
19
19
  import { PERSONA_DEFINITIONS, listRoleDefinitions } from "./personas.js";
20
20
  import { buildPanelFallback, renderPanelSummary } from "./panel.js";
21
- import { createPanelWorkerRun, launchPanelWorkerRun, panelWorkerRunPath, readPanelWorkerRun, refreshPanelWorkerRun, requestPanelWorkerStop, resumePanelWorkerRun, waitForPanelWorkerRun } from "./panel-runtime.js";
21
+ import { createPanelWorkerRun, launchPanelWorkerRun, panelWorkerRunPath, readPanelWorkerRun, refreshPanelWorkerRun, requestPanelWorkerStop, resumePanelWorkerRun, shutdownPanelWorkerRun, waitForPanelWorkerRun } from "./panel-runtime.js";
22
22
  import { LONGTABLE_MANAGED_HOOK_EVENTS, codexHooksEnabled, enableCodexHooksFeature, getMissingManagedCodexHookEvents, getMissingManagedCodexHookTrustState, mergeCodexHookTrustState, mergeManagedCodexHooksConfig, removeCodexHookTrustState, removeManagedCodexHooks } from "./codex-hooks.js";
23
23
  import { appendInvocationRecordToWorkspace, applyResearchSpecificationPatch, assertWorkspaceNotBlocked, answerWorkspaceQuestion, buildQuestionOpportunitySpecs, clearWorkspaceQuestion, createWorkspaceFollowUpQuestions, createWorkspaceQuestion, createOrUpdateProjectWorkspace, createWorkspaceHandoff, diffResearchSpecifications, inspectProjectWorkspace, loadWorkspaceState, loadProjectContextFromDirectory, findUnincorporatedResearchEvidence, proposeResearchSpecificationPatch, pruneWorkspaceQuestions, readResearchSpecificationHistory, recordPanelResultInWorkspace, repairWorkspaceStateConsistency, renderProjectWorkspaceSummary, syncCurrentWorkspaceView } from "./project-session.js";
24
24
  import { buildTeamDebate } from "./debate.js";
@@ -162,6 +162,7 @@ function usage() {
162
162
  " longtable panel [--prompt <text>] [--role <role[,role]>] [--mode review|critique|draft|commit] [--visibility synthesis_only|show_on_conflict|always_visible] [--provider codex|claude] [--native-workers|--native-subagents] [--wait [ms]] [--print] [--json] [--setup <path>] [--cwd <path>]",
163
163
  " longtable panel status --run <panel_run_id> [--wait [ms]] [--cwd <path>] [--json]",
164
164
  " longtable panel stop --run <panel_run_id> [--cwd <path>] [--json]",
165
+ " longtable panel shutdown --run <panel_run_id> [--cwd <path>] [--json]",
165
166
  " longtable panel resume --run <panel_run_id> [--wait [ms]] [--cwd <path>] [--json]",
166
167
  " longtable panel record [--invocation <id>] --result-file <json> [--surface sequential_fallback|native_subagents|native_workers] [--cwd <path>] [--json]",
167
168
  " longtable handoff [--cwd <path>] [--output <file>] [--print] [--json]",
@@ -2343,6 +2344,7 @@ function panelWorkerNextCommands(context, runId) {
2343
2344
  return {
2344
2345
  status: `longtable panel status ${cwdFlag} --run ${runId}`,
2345
2346
  stop: `longtable panel stop ${cwdFlag} --run ${runId}`,
2347
+ shutdown: `longtable panel shutdown ${cwdFlag} --run ${runId}`,
2346
2348
  resume: `longtable panel resume ${cwdFlag} --run ${runId}`
2347
2349
  };
2348
2350
  }
@@ -2372,6 +2374,32 @@ function summarizePanelRecordOutput(result) {
2372
2374
  evidenceRecordIds: result.evidenceRecords.map((record) => record.id)
2373
2375
  };
2374
2376
  }
2377
+ function summarizePanelWorkerExecution(run) {
2378
+ const bridgeStatus = run.bridgeStatus ?? run.status;
2379
+ const bridgeFailureReason = run.bridgeFailureReason ?? (bridgeStatus === "degraded"
2380
+ ? "native worker bridge unavailable before execution"
2381
+ : bridgeStatus === "failed" || bridgeStatus === "blocked"
2382
+ ? "native worker bridge did not complete all role passes"
2383
+ : undefined);
2384
+ return {
2385
+ requestedSurface: run.requestedSurface,
2386
+ fallbackSurface: run.fallbackSurface,
2387
+ bridgeStatus,
2388
+ bridgeFailureReason,
2389
+ sequentialFallbackExecuted: false,
2390
+ paneIds: run.workers.map((worker) => worker.paneId).filter((paneId) => typeof paneId === "string" && paneId.length > 0),
2391
+ workers: run.workers.map((worker) => ({
2392
+ id: worker.id,
2393
+ role: worker.role,
2394
+ label: worker.label,
2395
+ status: worker.status,
2396
+ paneId: worker.paneId,
2397
+ resultPath: worker.resultPath,
2398
+ diagnostics: worker.diagnostics
2399
+ })),
2400
+ diagnostics: run.diagnostics
2401
+ };
2402
+ }
2375
2403
  async function runPanelStatusCommand(args) {
2376
2404
  const context = await requireWorkspaceContext(args);
2377
2405
  const runId = requireRunId(args);
@@ -2381,7 +2409,12 @@ async function runPanelStatusCommand(args) {
2381
2409
  const recordedPanelResult = await recordTerminalNativeWorkerRun(context, refreshed);
2382
2410
  const nextCommands = panelWorkerNextCommands(context, refreshed.id);
2383
2411
  if (args.json === true) {
2384
- console.log(JSON.stringify({ ...refreshed, nextCommands, recordedPanelResult: summarizePanelRecordOutput(recordedPanelResult) }, null, 2));
2412
+ console.log(JSON.stringify({
2413
+ ...refreshed,
2414
+ execution: summarizePanelWorkerExecution(refreshed),
2415
+ nextCommands,
2416
+ recordedPanelResult: summarizePanelRecordOutput(recordedPanelResult)
2417
+ }, null, 2));
2385
2418
  return;
2386
2419
  }
2387
2420
  console.log("LongTable panel run status");
@@ -2391,6 +2424,7 @@ async function runPanelStatusCommand(args) {
2391
2424
  console.log(`- ${worker.label} (${worker.role}): ${worker.status}`);
2392
2425
  }
2393
2426
  console.log(`- stop: ${nextCommands.stop}`);
2427
+ console.log(`- shutdown: ${nextCommands.shutdown}`);
2394
2428
  console.log(`- resume: ${nextCommands.resume}`);
2395
2429
  if (recordedPanelResult) {
2396
2430
  console.log(`- recorded evidence: ${recordedPanelResult.evidenceRecords.length}`);
@@ -2411,13 +2445,36 @@ async function runPanelStopCommand(args) {
2411
2445
  for (const worker of stopped.workers) {
2412
2446
  console.log(`- ${worker.label} (${worker.role}): ${worker.status}`);
2413
2447
  }
2448
+ console.log(`- shutdown: ${nextCommands.shutdown}`);
2449
+ console.log(`- resume: ${nextCommands.resume}`);
2450
+ }
2451
+ async function runPanelShutdownCommand(args) {
2452
+ const context = await requireWorkspaceContext(args);
2453
+ const runId = requireRunId(args);
2454
+ const shutdown = await shutdownPanelWorkerRun(await readPanelWorkerRun(context.project.projectPath, runId));
2455
+ const nextCommands = panelWorkerNextCommands(context, shutdown.id);
2456
+ if (args.json === true) {
2457
+ console.log(JSON.stringify({
2458
+ ...shutdown,
2459
+ execution: summarizePanelWorkerExecution(shutdown),
2460
+ nextCommands
2461
+ }, null, 2));
2462
+ return;
2463
+ }
2464
+ console.log("LongTable panel run shutdown requested");
2465
+ console.log(`- run: ${shutdown.id}`);
2466
+ console.log(`- status: ${shutdown.status}`);
2467
+ for (const worker of shutdown.workers) {
2468
+ console.log(`- ${worker.label} (${worker.role}): ${worker.status}`);
2469
+ }
2470
+ console.log(`- status command: ${nextCommands.status}`);
2414
2471
  console.log(`- resume: ${nextCommands.resume}`);
2415
2472
  }
2416
2473
  async function runPanelResumeCommand(args) {
2417
2474
  const context = await requireWorkspaceContext(args);
2418
2475
  const runId = requireRunId(args);
2419
2476
  const waitMs = parseWaitMs(args.wait);
2420
- const { run } = await refreshPanelWorkerRun(await readPanelWorkerRun(context.project.projectPath, runId));
2477
+ const run = await readPanelWorkerRun(context.project.projectPath, runId);
2421
2478
  const resumed = run.status === "completed" ? run : await launchPanelWorkerRun(await resumePanelWorkerRun(run));
2422
2479
  const finalRun = waitMs ? await waitForPanelWorkerRun(resumed, waitMs) : resumed;
2423
2480
  const recordedPanelResult = await recordTerminalNativeWorkerRun(context, finalRun);
@@ -2587,10 +2644,17 @@ async function runPanelCommand(args) {
2587
2644
  nativeRunCreated: Boolean(finalNativeRun),
2588
2645
  waitMs,
2589
2646
  degradedReason: nativeWorkersRequested && !finalNativeRun
2590
- ? "native workers require an existing LongTable workspace; sequential fallback prompt returned"
2647
+ ? "native workers require an existing LongTable workspace; no native run was created and no sequential fallback was executed"
2591
2648
  : finalNativeRun?.status === "degraded"
2592
- ? "native workers require local tmux and codex commands; sequential fallback remains available"
2593
- : undefined
2649
+ ? "native worker bridge unavailable; sequential fallback remains available but was not executed by this native-workers request"
2650
+ : undefined,
2651
+ bridgeStatus: finalNativeRun?.bridgeStatus ?? finalNativeRun?.status,
2652
+ bridgeFailureReason: finalNativeRun?.bridgeFailureReason ?? (finalNativeRun?.status === "degraded"
2653
+ ? "native worker bridge unavailable before execution"
2654
+ : finalNativeRun?.status === "failed" || finalNativeRun?.status === "blocked"
2655
+ ? "native worker bridge did not complete all role passes"
2656
+ : undefined),
2657
+ sequentialFallbackExecuted: !nativeWorkersRequested
2594
2658
  },
2595
2659
  nativeRun: finalNativeRun,
2596
2660
  recordedPanelResult: summarizePanelRecordOutput(recordedPanelResult),
@@ -2608,17 +2672,26 @@ async function runPanelCommand(args) {
2608
2672
  const nextCommands = panelWorkerNextCommands(nativeRunContext, finalNativeRun.id);
2609
2673
  console.log(`- next status: ${nextCommands.status}`);
2610
2674
  console.log(`- stop: ${nextCommands.stop}`);
2675
+ console.log(`- shutdown: ${nextCommands.shutdown}`);
2611
2676
  console.log(`- resume: ${nextCommands.resume}`);
2612
2677
  if (recordedPanelResult) {
2613
2678
  console.log(`- recorded evidence: ${recordedPanelResult.evidenceRecords.length}`);
2614
2679
  }
2615
2680
  if (finalNativeRun.status === "degraded") {
2616
- console.log("- degraded: native workers are unavailable; use the sequential fallback prompt below.");
2681
+ console.log("- degraded: native workers are unavailable; sequential fallback is available but was not executed by this native-workers request.");
2617
2682
  console.log("");
2618
2683
  console.log(fallback.prompt);
2619
2684
  }
2620
2685
  return;
2621
2686
  }
2687
+ if (nativeWorkersRequested) {
2688
+ console.log("LongTable native panel worker run was not created");
2689
+ console.log("- status: failed");
2690
+ console.log("- reason: native workers require an existing LongTable workspace; sequential fallback was not executed by this native-workers request.");
2691
+ console.log("");
2692
+ console.log(fallback.prompt);
2693
+ exit(1);
2694
+ }
2622
2695
  const exitCode = await runCodexThinWrapper({
2623
2696
  prompt: fallback.prompt,
2624
2697
  mode,
@@ -4295,6 +4368,10 @@ async function main() {
4295
4368
  await runPanelStopCommand(values);
4296
4369
  return;
4297
4370
  }
4371
+ if (subcommand === "shutdown") {
4372
+ await runPanelShutdownCommand(values);
4373
+ return;
4374
+ }
4298
4375
  if (subcommand === "resume") {
4299
4376
  await runPanelResumeCommand(values);
4300
4377
  return;
@@ -17,4 +17,5 @@ export declare function refreshPanelWorkerRun(run: PanelWorkerRun): Promise<{
17
17
  }>;
18
18
  export declare function requestPanelWorkerStop(run: PanelWorkerRun): Promise<PanelWorkerRun>;
19
19
  export declare function resumePanelWorkerRun(run: PanelWorkerRun): Promise<PanelWorkerRun>;
20
+ export declare function shutdownPanelWorkerRun(run: PanelWorkerRun): Promise<PanelWorkerRun>;
20
21
  export declare function waitForPanelWorkerRun(run: PanelWorkerRun, timeoutMs: number): Promise<PanelWorkerRun>;
@@ -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 workerTaskPrompt(fallback, worker) {
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: ${fallback.invocationRecord.id}`,
47
- `Panel plan: ${fallback.plan.id}`,
95
+ `Invocation: ${run.invocationId}`,
96
+ `Panel plan: ${run.planId}`,
48
97
  "",
49
98
  "Instructions:",
50
- "- Work read-only unless the researcher explicitly asked for drafting.",
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
- fallback.plan.prompt
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 read-only",
94
- `-C ${shellQuote(run.workingDirectory)}`,
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(parsed, null, 2)+"\\n");' ${shellQuote(stdoutPath)} ${shellQuote(worker.resultPath)}`,
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
- await mkdir(taskDirectory, { recursive: true });
129
- await mkdir(resultDirectory, { recursive: true });
130
- await mkdir(logDirectory, { recursive: true });
131
- await mkdir(launcherDirectory, { recursive: true });
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: join(taskDirectory, `${workerId}.md`),
142
- resultPath: join(resultDirectory, `${workerId}.json`),
143
- logPath: join(logDirectory, `${workerId}.log`),
144
- launcherPath: join(launcherDirectory, `${workerId}.sh`),
145
- exitCodePath: join(resultDirectory, `${workerId}.exit.json`),
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 Promise.all(workers.map((worker) => writeFile(worker.taskPath, workerTaskPrompt(options.fallback, worker), "utf8")));
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
- ...worker,
199
- launcherPath: worker.launcherPath ?? join(launcherDirectory, `${worker.id}.sh`),
200
- exitCodePath: worker.exitCodePath ?? join(resultDirectory, `${worker.id}.exit.json`),
201
- diagnostics: worker.diagnostics ?? []
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 "resumable";
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 launchWorkerPane(run, worker) {
274
- const command = `bash ${shellQuote(worker.launcherPath)}`;
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
- return execFileSync("tmux", [
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
- const sessionName = `longtable-${run.id}-${worker.id}`.replace(/[^A-Za-z0-9_-]/g, "-").slice(0, 80);
289
- return execFileSync("tmux", [
290
- "new-session",
291
- "-d",
292
- "-P",
293
- "-F",
294
- "#{pane_id}",
295
- "-s",
296
- sessionName,
297
- command
298
- ], { encoding: "utf8" }).trim();
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 tmuxAvailable = commandAvailable("tmux");
313
- const codexAvailable = commandAvailable("codex");
314
- if (!tmuxAvailable || !codexAvailable) {
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 = await Promise.all(run.workers.map(async (worker) => {
574
+ const workers = [];
575
+ for (const worker of run.workers) {
338
576
  if (worker.status !== "pending" && worker.status !== "stopped" && worker.status !== "failed") {
339
- return worker;
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
- return {
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 with tmux/codex read-only native worker command; launcher persists the structured final output file.")
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
- return {
360
- ...launchable,
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(launchable.diagnostics, "tmux launch failed before worker result was created.")
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
  }
package/dist/panel.js CHANGED
@@ -85,11 +85,11 @@ export function buildPanelPlan(options) {
85
85
  rationale: [
86
86
  "Option A uses provider-neutral panel semantics before native provider orchestration.",
87
87
  preferredSurface === "native_workers"
88
- ? "LongTable-native workers may execute role passes through durable worker state; outputs must normalize back to this PanelResult."
88
+ ? "LongTable-native workers may execute role passes through durable worker state; outputs must normalize back to this PanelResult, and bridge failure must be reported separately from any later sequential fallback."
89
89
  : preferredSurface === "native_subagents"
90
90
  ? "Codex native subagents may execute the role passes when the current provider session exposes them; outputs must normalize back to this PanelResult."
91
91
  : "Sequential fallback is the stable execution path for both Claude Code and Codex.",
92
- "Sequential fallback remains the required degradation path.",
92
+ "Sequential fallback remains the ordinary degradation path, but it is not treated as executed merely because a native-worker bridge request failed.",
93
93
  roles.length === explicitRoles.length && explicitRoles.length > 0
94
94
  ? "The panel is constrained by explicitly requested roles."
95
95
  : "The panel combines default research-review roles with prompt-triggered roles."
@@ -194,7 +194,7 @@ export function createPlannedInvocationRecord(options) {
194
194
  panelPlan: options.plan,
195
195
  panelResult: options.result,
196
196
  degradationReason: options.plan.preferredSurface === "native_workers"
197
- ? "LongTable-native panel workers are optional; sequential_fallback is the required degradation path when local worker execution is unavailable."
197
+ ? "LongTable-native panel workers are optional; bridge failure is reported explicitly, and sequential_fallback remains available only when it is deliberately executed."
198
198
  : options.plan.preferredSurface === "native_subagents"
199
199
  ? "Codex native subagent execution is session-dependent; sequential_fallback is the required LongTable degradation path."
200
200
  : "Sequential fallback is the stable LongTable panel surface."
@@ -225,7 +225,7 @@ export function renderSequentialFallbackPrompt(plan) {
225
225
  : plan.preferredSurface === "native_workers"
226
226
  ? [
227
227
  "Preferred execution surface: LongTable-native panel workers when the local runtime supports them.",
228
- "Fallback: if native workers are unavailable or stopped, run the same role passes sequentially and disclose the fallback in the technical record."
228
+ "Fallback: if native workers are unavailable or stopped, report the bridge failure explicitly; run sequential fallback only as a deliberate fallback execution and disclose it in the technical record."
229
229
  ]
230
230
  : ["Execution surface: sequential_fallback"];
231
231
  return [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@longtable/cli",
3
- "version": "0.1.56",
3
+ "version": "0.1.58",
4
4
  "private": false,
5
5
  "description": "Researcher-facing LongTable CLI",
6
6
  "type": "module",
@@ -29,12 +29,12 @@
29
29
  },
30
30
  "dependencies": {
31
31
  "@clack/prompts": "^1.2.0",
32
- "@longtable/checkpoints": "0.1.56",
33
- "@longtable/core": "0.1.56",
34
- "@longtable/memory": "0.1.56",
35
- "@longtable/provider-claude": "0.1.56",
36
- "@longtable/provider-codex": "0.1.56",
37
- "@longtable/setup": "0.1.56"
32
+ "@longtable/checkpoints": "0.1.58",
33
+ "@longtable/core": "0.1.58",
34
+ "@longtable/memory": "0.1.58",
35
+ "@longtable/provider-claude": "0.1.58",
36
+ "@longtable/provider-codex": "0.1.58",
37
+ "@longtable/setup": "0.1.58"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@types/node": "^22.10.1",