@longtable/cli 0.1.57 → 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 +85 -50
- package/dist/panel-runtime.d.ts +1 -0
- package/dist/panel-runtime.js +447 -88
- package/dist/panel.js +4 -4
- package/dist/project-session.d.ts +0 -1
- package/dist/project-session.js +17 -141
- 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";
|
|
@@ -159,10 +159,10 @@ function usage() {
|
|
|
159
159
|
" longtable clarify --prompt <task-context> [--provider codex|claude] [--required|--advisory] [--print] [--cwd <path>] [--json] [--force]",
|
|
160
160
|
" longtable question --prompt <decision-context> [--title <text>] [--text <question>] [--provider codex|claude] [--required|--advisory] [--print] [--cwd <path>] [--json]",
|
|
161
161
|
" longtable clear-question --question <id> --reason <text> [--cwd <path>] [--json]",
|
|
162
|
-
" longtable repair-state [--cwd <path>] [--dry-run] [--json]",
|
|
163
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>]",
|
|
164
163
|
" longtable panel status --run <panel_run_id> [--wait [ms]] [--cwd <path>] [--json]",
|
|
165
164
|
" longtable panel stop --run <panel_run_id> [--cwd <path>] [--json]",
|
|
165
|
+
" longtable panel shutdown --run <panel_run_id> [--cwd <path>] [--json]",
|
|
166
166
|
" longtable panel resume --run <panel_run_id> [--wait [ms]] [--cwd <path>] [--json]",
|
|
167
167
|
" longtable panel record [--invocation <id>] --result-file <json> [--surface sequential_fallback|native_subagents|native_workers] [--cwd <path>] [--json]",
|
|
168
168
|
" longtable handoff [--cwd <path>] [--output <file>] [--print] [--json]",
|
|
@@ -201,7 +201,7 @@ function parseArgs(argv) {
|
|
|
201
201
|
const values = {};
|
|
202
202
|
let subcommand = maybeSubcommand;
|
|
203
203
|
const modeCommand = command && VALID_MODES.has(command);
|
|
204
|
-
const directCommand = command && ["init", "setup", "start", "resume", "doctor", "status", "audit", "roles", "show", "install", "mcp", "codex", "claude", "ask", "clarify", "question", "clear-question", "
|
|
204
|
+
const directCommand = command && ["init", "setup", "start", "resume", "doctor", "status", "audit", "roles", "show", "install", "mcp", "codex", "claude", "ask", "clarify", "question", "clear-question", "prune-questions", "panel", "handoff", "decide", "sentinel", "access", "search", "spec"].includes(command);
|
|
205
205
|
let startIndex = 1;
|
|
206
206
|
if (modeCommand) {
|
|
207
207
|
subcommand = undefined;
|
|
@@ -1784,10 +1784,6 @@ function renderDoctorStatus(status) {
|
|
|
1784
1784
|
if (!status.workspace.found) {
|
|
1785
1785
|
nextActions.push("longtable start");
|
|
1786
1786
|
}
|
|
1787
|
-
if ((status.workspace.answerWarnings ?? []).some((warning) => warning.issue.includes("legacy answer shape"))) {
|
|
1788
|
-
const root = status.workspace.rootPath ? ` --cwd "${status.workspace.rootPath}"` : "";
|
|
1789
|
-
nextActions.push(`longtable repair-state${root}`);
|
|
1790
|
-
}
|
|
1791
1787
|
nextActions.push(...status.hardStop.nextActions);
|
|
1792
1788
|
const firstQuestion = status.workspace.pendingQuestions?.[0];
|
|
1793
1789
|
if (firstQuestion && status.hardStop.nextActions.length === 0) {
|
|
@@ -2348,6 +2344,7 @@ function panelWorkerNextCommands(context, runId) {
|
|
|
2348
2344
|
return {
|
|
2349
2345
|
status: `longtable panel status ${cwdFlag} --run ${runId}`,
|
|
2350
2346
|
stop: `longtable panel stop ${cwdFlag} --run ${runId}`,
|
|
2347
|
+
shutdown: `longtable panel shutdown ${cwdFlag} --run ${runId}`,
|
|
2351
2348
|
resume: `longtable panel resume ${cwdFlag} --run ${runId}`
|
|
2352
2349
|
};
|
|
2353
2350
|
}
|
|
@@ -2377,6 +2374,32 @@ function summarizePanelRecordOutput(result) {
|
|
|
2377
2374
|
evidenceRecordIds: result.evidenceRecords.map((record) => record.id)
|
|
2378
2375
|
};
|
|
2379
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
|
+
}
|
|
2380
2403
|
async function runPanelStatusCommand(args) {
|
|
2381
2404
|
const context = await requireWorkspaceContext(args);
|
|
2382
2405
|
const runId = requireRunId(args);
|
|
@@ -2386,7 +2409,12 @@ async function runPanelStatusCommand(args) {
|
|
|
2386
2409
|
const recordedPanelResult = await recordTerminalNativeWorkerRun(context, refreshed);
|
|
2387
2410
|
const nextCommands = panelWorkerNextCommands(context, refreshed.id);
|
|
2388
2411
|
if (args.json === true) {
|
|
2389
|
-
console.log(JSON.stringify({
|
|
2412
|
+
console.log(JSON.stringify({
|
|
2413
|
+
...refreshed,
|
|
2414
|
+
execution: summarizePanelWorkerExecution(refreshed),
|
|
2415
|
+
nextCommands,
|
|
2416
|
+
recordedPanelResult: summarizePanelRecordOutput(recordedPanelResult)
|
|
2417
|
+
}, null, 2));
|
|
2390
2418
|
return;
|
|
2391
2419
|
}
|
|
2392
2420
|
console.log("LongTable panel run status");
|
|
@@ -2396,6 +2424,7 @@ async function runPanelStatusCommand(args) {
|
|
|
2396
2424
|
console.log(`- ${worker.label} (${worker.role}): ${worker.status}`);
|
|
2397
2425
|
}
|
|
2398
2426
|
console.log(`- stop: ${nextCommands.stop}`);
|
|
2427
|
+
console.log(`- shutdown: ${nextCommands.shutdown}`);
|
|
2399
2428
|
console.log(`- resume: ${nextCommands.resume}`);
|
|
2400
2429
|
if (recordedPanelResult) {
|
|
2401
2430
|
console.log(`- recorded evidence: ${recordedPanelResult.evidenceRecords.length}`);
|
|
@@ -2416,13 +2445,36 @@ async function runPanelStopCommand(args) {
|
|
|
2416
2445
|
for (const worker of stopped.workers) {
|
|
2417
2446
|
console.log(`- ${worker.label} (${worker.role}): ${worker.status}`);
|
|
2418
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}`);
|
|
2419
2471
|
console.log(`- resume: ${nextCommands.resume}`);
|
|
2420
2472
|
}
|
|
2421
2473
|
async function runPanelResumeCommand(args) {
|
|
2422
2474
|
const context = await requireWorkspaceContext(args);
|
|
2423
2475
|
const runId = requireRunId(args);
|
|
2424
2476
|
const waitMs = parseWaitMs(args.wait);
|
|
2425
|
-
const
|
|
2477
|
+
const run = await readPanelWorkerRun(context.project.projectPath, runId);
|
|
2426
2478
|
const resumed = run.status === "completed" ? run : await launchPanelWorkerRun(await resumePanelWorkerRun(run));
|
|
2427
2479
|
const finalRun = waitMs ? await waitForPanelWorkerRun(resumed, waitMs) : resumed;
|
|
2428
2480
|
const recordedPanelResult = await recordTerminalNativeWorkerRun(context, finalRun);
|
|
@@ -2592,10 +2644,17 @@ async function runPanelCommand(args) {
|
|
|
2592
2644
|
nativeRunCreated: Boolean(finalNativeRun),
|
|
2593
2645
|
waitMs,
|
|
2594
2646
|
degradedReason: nativeWorkersRequested && !finalNativeRun
|
|
2595
|
-
? "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"
|
|
2596
2648
|
: finalNativeRun?.status === "degraded"
|
|
2597
|
-
? "native
|
|
2598
|
-
: 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
|
|
2599
2658
|
},
|
|
2600
2659
|
nativeRun: finalNativeRun,
|
|
2601
2660
|
recordedPanelResult: summarizePanelRecordOutput(recordedPanelResult),
|
|
@@ -2613,17 +2672,26 @@ async function runPanelCommand(args) {
|
|
|
2613
2672
|
const nextCommands = panelWorkerNextCommands(nativeRunContext, finalNativeRun.id);
|
|
2614
2673
|
console.log(`- next status: ${nextCommands.status}`);
|
|
2615
2674
|
console.log(`- stop: ${nextCommands.stop}`);
|
|
2675
|
+
console.log(`- shutdown: ${nextCommands.shutdown}`);
|
|
2616
2676
|
console.log(`- resume: ${nextCommands.resume}`);
|
|
2617
2677
|
if (recordedPanelResult) {
|
|
2618
2678
|
console.log(`- recorded evidence: ${recordedPanelResult.evidenceRecords.length}`);
|
|
2619
2679
|
}
|
|
2620
2680
|
if (finalNativeRun.status === "degraded") {
|
|
2621
|
-
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.");
|
|
2622
2682
|
console.log("");
|
|
2623
2683
|
console.log(fallback.prompt);
|
|
2624
2684
|
}
|
|
2625
2685
|
return;
|
|
2626
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
|
+
}
|
|
2627
2695
|
const exitCode = await runCodexThinWrapper({
|
|
2628
2696
|
prompt: fallback.prompt,
|
|
2629
2697
|
mode,
|
|
@@ -3436,39 +3504,6 @@ async function runClearQuestion(args) {
|
|
|
3436
3504
|
console.log(`- state: ${context.stateFilePath}`);
|
|
3437
3505
|
console.log(`- current: ${context.currentFilePath}`);
|
|
3438
3506
|
}
|
|
3439
|
-
async function runRepairState(args) {
|
|
3440
|
-
const workingDirectory = typeof args.cwd === "string" ? args.cwd : cwd();
|
|
3441
|
-
const context = await loadProjectContextFromDirectory(workingDirectory);
|
|
3442
|
-
if (!context) {
|
|
3443
|
-
throw new Error("No LongTable project workspace was found here. Run this inside a project or pass --cwd.");
|
|
3444
|
-
}
|
|
3445
|
-
const result = await repairWorkspaceStateConsistency({
|
|
3446
|
-
context,
|
|
3447
|
-
dryRun: args["dry-run"] === true
|
|
3448
|
-
});
|
|
3449
|
-
if (args.json === true) {
|
|
3450
|
-
console.log(JSON.stringify({
|
|
3451
|
-
dryRun: args["dry-run"] === true,
|
|
3452
|
-
repaired: result.repaired,
|
|
3453
|
-
files: {
|
|
3454
|
-
state: context.stateFilePath,
|
|
3455
|
-
current: context.currentFilePath
|
|
3456
|
-
}
|
|
3457
|
-
}, null, 2));
|
|
3458
|
-
return;
|
|
3459
|
-
}
|
|
3460
|
-
console.log(args["dry-run"] === true ? "LongTable state repair preview" : "LongTable state repaired");
|
|
3461
|
-
if (result.repaired.length === 0) {
|
|
3462
|
-
console.log("- no repairs needed");
|
|
3463
|
-
}
|
|
3464
|
-
else {
|
|
3465
|
-
for (const item of result.repaired) {
|
|
3466
|
-
console.log(`- ${item}`);
|
|
3467
|
-
}
|
|
3468
|
-
}
|
|
3469
|
-
console.log(`- state: ${context.stateFilePath}`);
|
|
3470
|
-
console.log(`- current: ${context.currentFilePath}`);
|
|
3471
|
-
}
|
|
3472
3507
|
async function runPruneQuestions(args) {
|
|
3473
3508
|
const workingDirectory = typeof args.cwd === "string" ? args.cwd : cwd();
|
|
3474
3509
|
const context = await loadProjectContextFromDirectory(workingDirectory);
|
|
@@ -4316,10 +4351,6 @@ async function main() {
|
|
|
4316
4351
|
await runClearQuestion(values);
|
|
4317
4352
|
return;
|
|
4318
4353
|
}
|
|
4319
|
-
if (command === "repair-state") {
|
|
4320
|
-
await runRepairState(values);
|
|
4321
|
-
return;
|
|
4322
|
-
}
|
|
4323
4354
|
if (command === "prune-questions") {
|
|
4324
4355
|
await runPruneQuestions(values);
|
|
4325
4356
|
return;
|
|
@@ -4337,6 +4368,10 @@ async function main() {
|
|
|
4337
4368
|
await runPanelStopCommand(values);
|
|
4338
4369
|
return;
|
|
4339
4370
|
}
|
|
4371
|
+
if (subcommand === "shutdown") {
|
|
4372
|
+
await runPanelShutdownCommand(values);
|
|
4373
|
+
return;
|
|
4374
|
+
}
|
|
4340
4375
|
if (subcommand === "resume") {
|
|
4341
4376
|
await runPanelResumeCommand(values);
|
|
4342
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 [
|
|
@@ -490,7 +490,6 @@ export declare function pruneWorkspaceQuestions(options: {
|
|
|
490
490
|
}>;
|
|
491
491
|
export declare function repairWorkspaceStateConsistency(options: {
|
|
492
492
|
context: LongTableProjectContext;
|
|
493
|
-
dryRun?: boolean;
|
|
494
493
|
}): Promise<{
|
|
495
494
|
state: ResearchState;
|
|
496
495
|
repaired: string[];
|
package/dist/project-session.js
CHANGED
|
@@ -497,98 +497,6 @@ function formatQuestionMetadata(record) {
|
|
|
497
497
|
].filter(Boolean);
|
|
498
498
|
return parts.length > 0 ? ` [${parts.join("; ")}]` : "";
|
|
499
499
|
}
|
|
500
|
-
const QUESTION_SURFACES = new Set([
|
|
501
|
-
"native_structured",
|
|
502
|
-
"mcp_elicitation",
|
|
503
|
-
"numbered",
|
|
504
|
-
"terminal_selector",
|
|
505
|
-
"web_form"
|
|
506
|
-
]);
|
|
507
|
-
function asStringArray(value) {
|
|
508
|
-
if (Array.isArray(value)) {
|
|
509
|
-
return value.filter((entry) => typeof entry === "string");
|
|
510
|
-
}
|
|
511
|
-
return typeof value === "string" ? [value] : [];
|
|
512
|
-
}
|
|
513
|
-
function legacyQuestionAnswerRecord(record) {
|
|
514
|
-
return asRecord(record.answer);
|
|
515
|
-
}
|
|
516
|
-
function labelForQuestionAnswerValue(record, value) {
|
|
517
|
-
const option = record.prompt.options.find((candidate) => candidate.value === value || candidate.label === value);
|
|
518
|
-
if (option) {
|
|
519
|
-
return option.label;
|
|
520
|
-
}
|
|
521
|
-
if (value === "other") {
|
|
522
|
-
return record.prompt.otherLabel ?? "Other";
|
|
523
|
-
}
|
|
524
|
-
return value;
|
|
525
|
-
}
|
|
526
|
-
function selectedValuesForQuestion(record) {
|
|
527
|
-
const answer = legacyQuestionAnswerRecord(record);
|
|
528
|
-
if (!answer) {
|
|
529
|
-
return [];
|
|
530
|
-
}
|
|
531
|
-
const directValues = asStringArray(answer.selectedValues);
|
|
532
|
-
if (directValues.length > 0) {
|
|
533
|
-
return directValues;
|
|
534
|
-
}
|
|
535
|
-
const legacyValues = [
|
|
536
|
-
...asStringArray(answer.selectedValue),
|
|
537
|
-
...asStringArray(answer.selected),
|
|
538
|
-
...asStringArray(answer.selectedOption),
|
|
539
|
-
...asStringArray(answer.answer),
|
|
540
|
-
...asStringArray(answer.value)
|
|
541
|
-
];
|
|
542
|
-
return uniqueStrings(legacyValues);
|
|
543
|
-
}
|
|
544
|
-
function selectedLabelsForQuestion(record, selectedValues) {
|
|
545
|
-
const answer = legacyQuestionAnswerRecord(record);
|
|
546
|
-
const directLabels = asStringArray(answer?.selectedLabels);
|
|
547
|
-
if (directLabels.length > 0) {
|
|
548
|
-
return directLabels;
|
|
549
|
-
}
|
|
550
|
-
return selectedValues.map((value) => labelForQuestionAnswerValue(record, value));
|
|
551
|
-
}
|
|
552
|
-
function legacyAnswerShapeWarnings(questions) {
|
|
553
|
-
return questions.flatMap((record) => {
|
|
554
|
-
if (record.status !== "answered" || !legacyQuestionAnswerRecord(record)) {
|
|
555
|
-
return [];
|
|
556
|
-
}
|
|
557
|
-
const selectedValues = asStringArray(legacyQuestionAnswerRecord(record)?.selectedValues);
|
|
558
|
-
const selectedLabels = asStringArray(legacyQuestionAnswerRecord(record)?.selectedLabels);
|
|
559
|
-
if (selectedValues.length > 0 && selectedLabels.length > 0) {
|
|
560
|
-
return [];
|
|
561
|
-
}
|
|
562
|
-
return [{
|
|
563
|
-
questionId: record.id,
|
|
564
|
-
...(record.decisionRecordId ? { decisionRecordId: record.decisionRecordId } : {}),
|
|
565
|
-
issue: "Answered question uses a legacy answer shape that is missing selectedValues or selectedLabels.",
|
|
566
|
-
suggestion: "Run `longtable repair-state --cwd <project-path>` to normalize the answer without changing the recorded selection."
|
|
567
|
-
}];
|
|
568
|
-
});
|
|
569
|
-
}
|
|
570
|
-
function numericOtherAnswerWarnings(questions) {
|
|
571
|
-
return questions.flatMap((record) => {
|
|
572
|
-
if (record.status !== "answered" || !selectedValuesForQuestion(record).includes("other")) {
|
|
573
|
-
return [];
|
|
574
|
-
}
|
|
575
|
-
const answer = legacyQuestionAnswerRecord(record);
|
|
576
|
-
const raw = typeof answer?.otherText === "string"
|
|
577
|
-
? answer.otherText
|
|
578
|
-
: selectedLabelsForQuestion(record, selectedValuesForQuestion(record))[0] ?? "";
|
|
579
|
-
if (!/^\d+$/.test(raw.trim())) {
|
|
580
|
-
return [];
|
|
581
|
-
}
|
|
582
|
-
const index = Number(raw.trim()) - 1;
|
|
583
|
-
const option = record.prompt.options[index];
|
|
584
|
-
return [{
|
|
585
|
-
questionId: record.id,
|
|
586
|
-
...(record.decisionRecordId ? { decisionRecordId: record.decisionRecordId } : {}),
|
|
587
|
-
issue: `Numeric answer "${raw.trim()}" was stored as other text.`,
|
|
588
|
-
...(option ? { suggestion: `Use "${option.value}" (${option.label}) for this checkpoint option.` } : {})
|
|
589
|
-
}];
|
|
590
|
-
});
|
|
591
|
-
}
|
|
592
500
|
function compactLine(value, limit = 160) {
|
|
593
501
|
const compacted = value.replace(/\s+/g, " ").trim();
|
|
594
502
|
return compacted.length > limit ? `${compacted.slice(0, limit - 1)}…` : compacted;
|
|
@@ -946,10 +854,22 @@ function summarizeWorkspaceInspection(context, state) {
|
|
|
946
854
|
...(record.selectedOption ? { selectedOption: record.selectedOption } : {}),
|
|
947
855
|
timestamp: record.timestamp
|
|
948
856
|
})),
|
|
949
|
-
answerWarnings:
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
857
|
+
answerWarnings: questions
|
|
858
|
+
.filter((record) => record.status === "answered" && record.answer?.selectedValues.includes("other"))
|
|
859
|
+
.flatMap((record) => {
|
|
860
|
+
const raw = record.answer?.otherText ?? record.answer?.selectedLabels[0] ?? "";
|
|
861
|
+
if (!/^\d+$/.test(raw.trim())) {
|
|
862
|
+
return [];
|
|
863
|
+
}
|
|
864
|
+
const index = Number(raw.trim()) - 1;
|
|
865
|
+
const option = record.prompt.options[index];
|
|
866
|
+
return [{
|
|
867
|
+
questionId: record.id,
|
|
868
|
+
...(record.decisionRecordId ? { decisionRecordId: record.decisionRecordId } : {}),
|
|
869
|
+
issue: `Numeric answer "${raw.trim()}" was stored as other text.`,
|
|
870
|
+
...(option ? { suggestion: `Use "${option.value}" (${option.label}) for this checkpoint option.` } : {})
|
|
871
|
+
}];
|
|
872
|
+
})
|
|
953
873
|
};
|
|
954
874
|
}
|
|
955
875
|
function buildProjectAgentsMd(project, session) {
|
|
@@ -3322,51 +3242,7 @@ export async function repairWorkspaceStateConsistency(options) {
|
|
|
3322
3242
|
})
|
|
3323
3243
|
};
|
|
3324
3244
|
}
|
|
3325
|
-
|
|
3326
|
-
const repairedQuestionLog = (updated.questionLog ?? []).map((record) => {
|
|
3327
|
-
if (record.status !== "answered") {
|
|
3328
|
-
return record;
|
|
3329
|
-
}
|
|
3330
|
-
const answer = legacyQuestionAnswerRecord(record);
|
|
3331
|
-
if (!answer) {
|
|
3332
|
-
return record;
|
|
3333
|
-
}
|
|
3334
|
-
const selectedValues = selectedValuesForQuestion(record);
|
|
3335
|
-
if (selectedValues.length === 0) {
|
|
3336
|
-
return record;
|
|
3337
|
-
}
|
|
3338
|
-
const selectedLabels = selectedLabelsForQuestion(record, selectedValues);
|
|
3339
|
-
const needsRepair = asStringArray(answer.selectedValues).length === 0 ||
|
|
3340
|
-
asStringArray(answer.selectedLabels).length === 0 ||
|
|
3341
|
-
typeof answer.promptId !== "string" ||
|
|
3342
|
-
!QUESTION_SURFACES.has(answer.surface);
|
|
3343
|
-
if (!needsRepair) {
|
|
3344
|
-
return record;
|
|
3345
|
-
}
|
|
3346
|
-
repaired.push(`normalized legacy answer shape for question ${record.id}`);
|
|
3347
|
-
const normalizedAnswer = {
|
|
3348
|
-
...answer,
|
|
3349
|
-
promptId: typeof answer.promptId === "string" ? answer.promptId : record.prompt.id,
|
|
3350
|
-
selectedValues,
|
|
3351
|
-
selectedLabels,
|
|
3352
|
-
...(typeof answer.otherText === "string" && answer.otherText.trim() ? { otherText: answer.otherText } : {}),
|
|
3353
|
-
...(typeof answer.rationale === "string" && answer.rationale.trim() ? { rationale: answer.rationale } : {}),
|
|
3354
|
-
...(answer.provider === "codex" || answer.provider === "claude" ? { provider: answer.provider } : {}),
|
|
3355
|
-
surface: QUESTION_SURFACES.has(answer.surface) ? answer.surface : "numbered"
|
|
3356
|
-
};
|
|
3357
|
-
return {
|
|
3358
|
-
...record,
|
|
3359
|
-
updatedAt: timestamp,
|
|
3360
|
-
answer: normalizedAnswer
|
|
3361
|
-
};
|
|
3362
|
-
});
|
|
3363
|
-
if (repairedQuestionLog.some((record, index) => record !== (updated.questionLog ?? [])[index])) {
|
|
3364
|
-
updated = {
|
|
3365
|
-
...updated,
|
|
3366
|
-
questionLog: repairedQuestionLog
|
|
3367
|
-
};
|
|
3368
|
-
}
|
|
3369
|
-
if (repaired.length > 0 && !options.dryRun) {
|
|
3245
|
+
if (repaired.length > 0) {
|
|
3370
3246
|
await writeFile(options.context.stateFilePath, JSON.stringify(updated, null, 2), "utf8");
|
|
3371
3247
|
await syncCurrentWorkspaceView(options.context);
|
|
3372
3248
|
}
|
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",
|