@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 +84 -7
- package/dist/panel-runtime.d.ts +1 -0
- package/dist/panel-runtime.js +447 -88
- package/dist/panel.js +4 -4
- package/package.json +7 -7
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({
|
|
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
|
|
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
|
|
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
|
|
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;
|
|
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;
|
package/dist/panel-runtime.d.ts
CHANGED
|
@@ -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>;
|
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
|
}
|
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
|
|
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;
|
|
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,
|
|
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.
|
|
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.
|
|
33
|
-
"@longtable/core": "0.1.
|
|
34
|
-
"@longtable/memory": "0.1.
|
|
35
|
-
"@longtable/provider-claude": "0.1.
|
|
36
|
-
"@longtable/provider-codex": "0.1.
|
|
37
|
-
"@longtable/setup": "0.1.
|
|
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",
|