@longtable/cli 0.1.54 → 0.1.56
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 +326 -20
- package/dist/panel-runtime.d.ts +20 -0
- package/dist/panel-runtime.js +525 -0
- package/dist/panel.d.ts +4 -1
- package/dist/panel.js +34 -7
- package/dist/project-session.d.ts +35 -1
- package/dist/project-session.js +372 -2
- package/package.json +7 -7
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { chmod, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
function nowIso() {
|
|
6
|
+
return new Date().toISOString();
|
|
7
|
+
}
|
|
8
|
+
function createId(prefix) {
|
|
9
|
+
return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
10
|
+
}
|
|
11
|
+
function commandAvailable(command) {
|
|
12
|
+
try {
|
|
13
|
+
execFileSync("sh", ["-lc", `command -v ${command}`], { stdio: "ignore" });
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function shellQuote(value) {
|
|
21
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
22
|
+
}
|
|
23
|
+
function appendDiagnostic(existing, diagnostic) {
|
|
24
|
+
return existing.includes(diagnostic) ? existing : [...existing, diagnostic];
|
|
25
|
+
}
|
|
26
|
+
function panelRunsDirectory(workingDirectory) {
|
|
27
|
+
return join(workingDirectory, ".longtable", "panel-runs");
|
|
28
|
+
}
|
|
29
|
+
export function panelWorkerRunDirectory(workingDirectory, runId) {
|
|
30
|
+
return join(panelRunsDirectory(workingDirectory), runId);
|
|
31
|
+
}
|
|
32
|
+
export function panelWorkerRunPath(workingDirectory, runId) {
|
|
33
|
+
return join(panelWorkerRunDirectory(workingDirectory, runId), "run.json");
|
|
34
|
+
}
|
|
35
|
+
async function writeJsonAtomic(path, value) {
|
|
36
|
+
await mkdir(dirname(path), { recursive: true });
|
|
37
|
+
const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
38
|
+
await writeFile(tempPath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
39
|
+
await rename(tempPath, path);
|
|
40
|
+
}
|
|
41
|
+
function workerTaskPrompt(fallback, worker) {
|
|
42
|
+
return [
|
|
43
|
+
"LongTable native panel worker",
|
|
44
|
+
"",
|
|
45
|
+
`Role: ${worker.label} (${worker.role})`,
|
|
46
|
+
`Invocation: ${fallback.invocationRecord.id}`,
|
|
47
|
+
`Panel plan: ${fallback.plan.id}`,
|
|
48
|
+
"",
|
|
49
|
+
"Instructions:",
|
|
50
|
+
"- Work read-only unless the researcher explicitly asked for drafting.",
|
|
51
|
+
"- Do not expose or persist hidden reasoning, private tool traces, or chain-of-thought.",
|
|
52
|
+
"- Persist only the structured final role output to the result path below.",
|
|
53
|
+
"- Return JSON matching this shape:",
|
|
54
|
+
" {\"role\":\"...\",\"label\":\"...\",\"status\":\"completed\",\"summary\":\"...\",\"claims\":[],\"objections\":[],\"openQuestions\":[],\"evidenceRefs\":[],\"error\":\"\"}",
|
|
55
|
+
"",
|
|
56
|
+
`Result path: ${worker.resultPath}`,
|
|
57
|
+
`Log path: ${worker.logPath}`,
|
|
58
|
+
"",
|
|
59
|
+
"Research object:",
|
|
60
|
+
fallback.plan.prompt
|
|
61
|
+
].join("\n");
|
|
62
|
+
}
|
|
63
|
+
const PANEL_WORKER_OUTPUT_SCHEMA = {
|
|
64
|
+
type: "object",
|
|
65
|
+
additionalProperties: false,
|
|
66
|
+
required: ["role", "label", "status", "summary", "claims", "objections", "openQuestions", "evidenceRefs", "error"],
|
|
67
|
+
properties: {
|
|
68
|
+
role: { type: "string" },
|
|
69
|
+
label: { type: "string" },
|
|
70
|
+
status: { enum: ["completed", "blocked", "error"] },
|
|
71
|
+
summary: { type: "string" },
|
|
72
|
+
claims: { type: "array", items: { type: "string" } },
|
|
73
|
+
objections: { type: "array", items: { type: "string" } },
|
|
74
|
+
openQuestions: { type: "array", items: { type: "string" } },
|
|
75
|
+
evidenceRefs: { type: "array", items: { type: "string" } },
|
|
76
|
+
error: { type: "string" }
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
function launcherScript(run, worker) {
|
|
80
|
+
const role = shellQuote(worker.role);
|
|
81
|
+
const label = shellQuote(worker.label);
|
|
82
|
+
const stdoutPath = `${worker.resultPath}.stdout`;
|
|
83
|
+
return [
|
|
84
|
+
"#!/usr/bin/env bash",
|
|
85
|
+
"set +e",
|
|
86
|
+
`printf 'started_at=%s\\n' "$(date -u +'%Y-%m-%dT%H:%M:%SZ')" > ${shellQuote(worker.logPath)}`,
|
|
87
|
+
`if [ -f ${shellQuote(run.stopFilePath)} ]; then`,
|
|
88
|
+
` printf 'stop_requested_before_launch\\n' >> ${shellQuote(worker.logPath)}`,
|
|
89
|
+
" exit 0",
|
|
90
|
+
"fi",
|
|
91
|
+
[
|
|
92
|
+
"codex exec",
|
|
93
|
+
"-s read-only",
|
|
94
|
+
`-C ${shellQuote(run.workingDirectory)}`,
|
|
95
|
+
"--skip-git-repo-check",
|
|
96
|
+
`--output-schema ${shellQuote(run.outputSchemaPath)}`,
|
|
97
|
+
"-",
|
|
98
|
+
`< ${shellQuote(worker.taskPath)}`,
|
|
99
|
+
`> ${shellQuote(stdoutPath)} 2>/dev/null`
|
|
100
|
+
].join(" "),
|
|
101
|
+
"code=$?",
|
|
102
|
+
`if [ -s ${shellQuote(stdoutPath)} ]; then`,
|
|
103
|
+
` node -e 'const fs=require("fs"); const [stdoutPath,resultPath]=process.argv.slice(1); const parsed=JSON.parse(fs.readFileSync(stdoutPath,"utf8").trim()); fs.writeFileSync(resultPath, JSON.stringify(parsed, null, 2)+"\\n");' ${shellQuote(stdoutPath)} ${shellQuote(worker.resultPath)}`,
|
|
104
|
+
" parse_code=$?",
|
|
105
|
+
` rm -f ${shellQuote(stdoutPath)}`,
|
|
106
|
+
` if [ "$parse_code" -ne 0 ]; then code=1; fi`,
|
|
107
|
+
"fi",
|
|
108
|
+
`printf '{"exitCode":%s,"completedAt":"%s"}\\n' "$code" "$(date -u +'%Y-%m-%dT%H:%M:%SZ')" > ${shellQuote(worker.exitCodePath ?? `${worker.resultPath}.exit.json`)}`,
|
|
109
|
+
`printf 'exit_code=%s\\n' "$code" >> ${shellQuote(worker.logPath)}`,
|
|
110
|
+
`if [ "$code" -ne 0 ] && [ ! -s ${shellQuote(worker.resultPath)} ]; then`,
|
|
111
|
+
` 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
|
+
"fi",
|
|
113
|
+
"exit $code",
|
|
114
|
+
""
|
|
115
|
+
].join("\n");
|
|
116
|
+
}
|
|
117
|
+
function plannedWorkerStatus(runStatus) {
|
|
118
|
+
return runStatus === "planned" ? "pending" : "running";
|
|
119
|
+
}
|
|
120
|
+
export async function createPanelWorkerRun(options) {
|
|
121
|
+
const createdAt = nowIso();
|
|
122
|
+
const runId = createId("panel_run");
|
|
123
|
+
const runDirectory = panelWorkerRunDirectory(options.workingDirectory, runId);
|
|
124
|
+
const taskDirectory = join(runDirectory, "tasks");
|
|
125
|
+
const resultDirectory = join(runDirectory, "results");
|
|
126
|
+
const logDirectory = join(runDirectory, "logs");
|
|
127
|
+
const launcherDirectory = join(runDirectory, "launchers");
|
|
128
|
+
await mkdir(taskDirectory, { recursive: true });
|
|
129
|
+
await mkdir(resultDirectory, { recursive: true });
|
|
130
|
+
await mkdir(logDirectory, { recursive: true });
|
|
131
|
+
await mkdir(launcherDirectory, { recursive: true });
|
|
132
|
+
const runStatus = options.initialStatus ?? "planned";
|
|
133
|
+
const workers = options.fallback.plan.members.map((member, index) => {
|
|
134
|
+
const workerId = `worker-${index + 1}-${member.role}`;
|
|
135
|
+
return {
|
|
136
|
+
id: workerId,
|
|
137
|
+
role: member.role,
|
|
138
|
+
label: member.label,
|
|
139
|
+
required: member.required,
|
|
140
|
+
status: plannedWorkerStatus(runStatus),
|
|
141
|
+
taskPath: join(taskDirectory, `${workerId}.md`),
|
|
142
|
+
resultPath: join(resultDirectory, `${workerId}.json`),
|
|
143
|
+
logPath: join(logDirectory, `${workerId}.log`),
|
|
144
|
+
launcherPath: join(launcherDirectory, `${workerId}.sh`),
|
|
145
|
+
exitCodePath: join(resultDirectory, `${workerId}.exit.json`),
|
|
146
|
+
updatedAt: createdAt,
|
|
147
|
+
diagnostics: []
|
|
148
|
+
};
|
|
149
|
+
});
|
|
150
|
+
const run = {
|
|
151
|
+
schemaVersion: 1,
|
|
152
|
+
id: runId,
|
|
153
|
+
createdAt,
|
|
154
|
+
updatedAt: createdAt,
|
|
155
|
+
invocationId: options.fallback.invocationRecord.id,
|
|
156
|
+
planId: options.fallback.plan.id,
|
|
157
|
+
provider: options.fallback.invocationRecord.provider,
|
|
158
|
+
prompt: options.fallback.plan.prompt,
|
|
159
|
+
mode: options.fallback.plan.mode,
|
|
160
|
+
visibility: options.fallback.plan.visibility,
|
|
161
|
+
requestedSurface: "native_workers",
|
|
162
|
+
fallbackSurface: "sequential_fallback",
|
|
163
|
+
status: runStatus,
|
|
164
|
+
workingDirectory: options.workingDirectory,
|
|
165
|
+
runDirectory,
|
|
166
|
+
taskDirectory,
|
|
167
|
+
resultDirectory,
|
|
168
|
+
logDirectory,
|
|
169
|
+
launcherDirectory,
|
|
170
|
+
outputSchemaPath: join(runDirectory, "panel-worker-output.schema.json"),
|
|
171
|
+
stopFilePath: join(runDirectory, "stop-requested"),
|
|
172
|
+
aggregateResultPath: join(runDirectory, "panel-result.json"),
|
|
173
|
+
workers,
|
|
174
|
+
diagnostics: options.diagnostics ?? []
|
|
175
|
+
};
|
|
176
|
+
await writeJsonAtomic(join(runDirectory, "schema.json"), {
|
|
177
|
+
schemaVersion: 1,
|
|
178
|
+
kind: "longtable.panelWorkerRun",
|
|
179
|
+
note: "Durable worker state stores task/status/result metadata, not hidden reasoning or raw tool traces."
|
|
180
|
+
});
|
|
181
|
+
await writeJsonAtomic(run.outputSchemaPath, PANEL_WORKER_OUTPUT_SCHEMA);
|
|
182
|
+
await Promise.all(workers.map((worker) => writeFile(worker.taskPath, workerTaskPrompt(options.fallback, worker), "utf8")));
|
|
183
|
+
await writePanelWorkerRun(run);
|
|
184
|
+
return run;
|
|
185
|
+
}
|
|
186
|
+
export async function readPanelWorkerRun(workingDirectory, runId) {
|
|
187
|
+
return normalizePanelWorkerRun(JSON.parse(await readFile(panelWorkerRunPath(workingDirectory, runId), "utf8")));
|
|
188
|
+
}
|
|
189
|
+
function normalizePanelWorkerRun(run) {
|
|
190
|
+
const launcherDirectory = run.launcherDirectory ?? join(run.runDirectory, "launchers");
|
|
191
|
+
const resultDirectory = run.resultDirectory ?? join(run.runDirectory, "results");
|
|
192
|
+
return {
|
|
193
|
+
...run,
|
|
194
|
+
launcherDirectory,
|
|
195
|
+
outputSchemaPath: run.outputSchemaPath ?? join(run.runDirectory, "panel-worker-output.schema.json"),
|
|
196
|
+
stopFilePath: run.stopFilePath ?? join(run.runDirectory, "stop-requested"),
|
|
197
|
+
workers: run.workers.map((worker) => ({
|
|
198
|
+
...worker,
|
|
199
|
+
launcherPath: worker.launcherPath ?? join(launcherDirectory, `${worker.id}.sh`),
|
|
200
|
+
exitCodePath: worker.exitCodePath ?? join(resultDirectory, `${worker.id}.exit.json`),
|
|
201
|
+
diagnostics: worker.diagnostics ?? []
|
|
202
|
+
}))
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
export async function writePanelWorkerRun(run) {
|
|
206
|
+
await writeJsonAtomic(join(run.runDirectory, "run.json"), {
|
|
207
|
+
...run,
|
|
208
|
+
updatedAt: nowIso()
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
function parseWorkerResult(worker) {
|
|
212
|
+
if (!existsSync(worker.resultPath)) {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
const parsed = JSON.parse(readFileSyncUtf8(worker.resultPath));
|
|
216
|
+
const roleMismatch = typeof parsed.role === "string" && parsed.role !== worker.role;
|
|
217
|
+
const labelMismatch = typeof parsed.label === "string" && parsed.label !== worker.label;
|
|
218
|
+
const identityError = [
|
|
219
|
+
roleMismatch ? `role mismatch: expected ${worker.role}, received ${parsed.role}` : "",
|
|
220
|
+
labelMismatch ? `label mismatch: expected ${worker.label}, received ${parsed.label}` : ""
|
|
221
|
+
].filter(Boolean).join("; ");
|
|
222
|
+
return {
|
|
223
|
+
role: worker.role,
|
|
224
|
+
label: worker.label,
|
|
225
|
+
status: identityError ? "error" : parsed.status ?? "completed",
|
|
226
|
+
summary: parsed.summary,
|
|
227
|
+
claims: parsed.claims,
|
|
228
|
+
objections: parsed.objections,
|
|
229
|
+
openQuestions: parsed.openQuestions,
|
|
230
|
+
evidenceRefs: parsed.evidenceRefs,
|
|
231
|
+
error: identityError || parsed.error
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
function readFileSyncUtf8(path) {
|
|
235
|
+
return readFileSync(path, "utf8");
|
|
236
|
+
}
|
|
237
|
+
function statusFromWorkers(workers) {
|
|
238
|
+
if (workers.every((worker) => worker.status === "completed")) {
|
|
239
|
+
return "completed";
|
|
240
|
+
}
|
|
241
|
+
if (workers.some((worker) => worker.status === "running")) {
|
|
242
|
+
return "running";
|
|
243
|
+
}
|
|
244
|
+
if (workers.some((worker) => worker.status === "failed")) {
|
|
245
|
+
return "resumable";
|
|
246
|
+
}
|
|
247
|
+
if (workers.some((worker) => worker.status === "blocked")) {
|
|
248
|
+
return "blocked";
|
|
249
|
+
}
|
|
250
|
+
if (workers.some((worker) => worker.status === "stop_requested")) {
|
|
251
|
+
return "stop_requested";
|
|
252
|
+
}
|
|
253
|
+
if (workers.every((worker) => worker.status === "stopped" || worker.status === "completed")) {
|
|
254
|
+
return "stopped";
|
|
255
|
+
}
|
|
256
|
+
return "running";
|
|
257
|
+
}
|
|
258
|
+
function tmuxPaneAlive(paneId) {
|
|
259
|
+
if (!commandAvailable("tmux")) {
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
try {
|
|
263
|
+
execFileSync("tmux", ["display-message", "-p", "-t", paneId, "#{pane_id}"], { stdio: "ignore" });
|
|
264
|
+
return true;
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
function terminalStatus(status) {
|
|
271
|
+
return status === "completed" || status === "blocked" || status === "stopped" || status === "degraded";
|
|
272
|
+
}
|
|
273
|
+
function launchWorkerPane(run, worker) {
|
|
274
|
+
const command = `bash ${shellQuote(worker.launcherPath)}`;
|
|
275
|
+
try {
|
|
276
|
+
return execFileSync("tmux", [
|
|
277
|
+
"new-window",
|
|
278
|
+
"-d",
|
|
279
|
+
"-P",
|
|
280
|
+
"-F",
|
|
281
|
+
"#{pane_id}",
|
|
282
|
+
"-n",
|
|
283
|
+
`lt-${worker.id.slice(0, 12)}`,
|
|
284
|
+
command
|
|
285
|
+
], { encoding: "utf8" }).trim();
|
|
286
|
+
}
|
|
287
|
+
catch {
|
|
288
|
+
const sessionName = `longtable-${run.id}-${worker.id}`.replace(/[^A-Za-z0-9_-]/g, "-").slice(0, 80);
|
|
289
|
+
return execFileSync("tmux", [
|
|
290
|
+
"new-session",
|
|
291
|
+
"-d",
|
|
292
|
+
"-P",
|
|
293
|
+
"-F",
|
|
294
|
+
"#{pane_id}",
|
|
295
|
+
"-s",
|
|
296
|
+
sessionName,
|
|
297
|
+
command
|
|
298
|
+
], { encoding: "utf8" }).trim();
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
async function writeWorkerLauncher(run, worker) {
|
|
302
|
+
if (!worker.launcherPath) {
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
await writeFile(worker.launcherPath, launcherScript(run, worker), "utf8");
|
|
306
|
+
await chmod(worker.launcherPath, 0o755);
|
|
307
|
+
}
|
|
308
|
+
export async function launchPanelWorkerRun(run) {
|
|
309
|
+
if (terminalStatus(run.status) || run.status === "stop_requested") {
|
|
310
|
+
return run;
|
|
311
|
+
}
|
|
312
|
+
const tmuxAvailable = commandAvailable("tmux");
|
|
313
|
+
const codexAvailable = commandAvailable("codex");
|
|
314
|
+
if (!tmuxAvailable || !codexAvailable) {
|
|
315
|
+
const unavailable = [
|
|
316
|
+
tmuxAvailable ? null : "tmux:unavailable",
|
|
317
|
+
codexAvailable ? null : "codex:unavailable"
|
|
318
|
+
].filter((entry) => Boolean(entry));
|
|
319
|
+
const nextRun = {
|
|
320
|
+
...run,
|
|
321
|
+
status: "degraded",
|
|
322
|
+
updatedAt: nowIso(),
|
|
323
|
+
diagnostics: [...run.diagnostics, ...unavailable, "Native worker launch degraded; use sequential_fallback or resume when local runtime is available."],
|
|
324
|
+
workers: run.workers.map((worker) => worker.status === "completed"
|
|
325
|
+
? worker
|
|
326
|
+
: {
|
|
327
|
+
...worker,
|
|
328
|
+
status: "pending",
|
|
329
|
+
updatedAt: nowIso(),
|
|
330
|
+
diagnostics: appendDiagnostic(worker.diagnostics, "Launch skipped because tmux or codex is unavailable.")
|
|
331
|
+
})
|
|
332
|
+
};
|
|
333
|
+
await writePanelWorkerRun(nextRun);
|
|
334
|
+
return nextRun;
|
|
335
|
+
}
|
|
336
|
+
const launchedAt = nowIso();
|
|
337
|
+
const workers = await Promise.all(run.workers.map(async (worker) => {
|
|
338
|
+
if (worker.status !== "pending" && worker.status !== "stopped" && worker.status !== "failed") {
|
|
339
|
+
return worker;
|
|
340
|
+
}
|
|
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
|
+
try {
|
|
348
|
+
const paneId = launchWorkerPane(run, launchable);
|
|
349
|
+
return {
|
|
350
|
+
...launchable,
|
|
351
|
+
status: "running",
|
|
352
|
+
paneId: paneId || launchable.paneId,
|
|
353
|
+
startedAt: launchedAt,
|
|
354
|
+
updatedAt: launchedAt,
|
|
355
|
+
diagnostics: appendDiagnostic(launchable.diagnostics, "Launched with tmux/codex read-only native worker command; launcher persists the structured final output file.")
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
catch (error) {
|
|
359
|
+
return {
|
|
360
|
+
...launchable,
|
|
361
|
+
status: "failed",
|
|
362
|
+
updatedAt: nowIso(),
|
|
363
|
+
error: error instanceof Error ? error.message : String(error),
|
|
364
|
+
diagnostics: appendDiagnostic(launchable.diagnostics, "tmux launch failed before worker result was created.")
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
}));
|
|
368
|
+
const nextRun = {
|
|
369
|
+
...run,
|
|
370
|
+
workers,
|
|
371
|
+
status: statusFromWorkers(workers),
|
|
372
|
+
updatedAt: nowIso()
|
|
373
|
+
};
|
|
374
|
+
await writePanelWorkerRun(nextRun);
|
|
375
|
+
return nextRun;
|
|
376
|
+
}
|
|
377
|
+
export async function refreshPanelWorkerRun(run) {
|
|
378
|
+
const updatedAt = nowIso();
|
|
379
|
+
const memberResults = [];
|
|
380
|
+
const workers = run.workers.map((worker) => {
|
|
381
|
+
try {
|
|
382
|
+
const result = parseWorkerResult(worker);
|
|
383
|
+
if (!result) {
|
|
384
|
+
if (worker.status === "stop_requested") {
|
|
385
|
+
const stopped = !worker.paneId || !tmuxPaneAlive(worker.paneId);
|
|
386
|
+
return stopped
|
|
387
|
+
? {
|
|
388
|
+
...worker,
|
|
389
|
+
status: "stopped",
|
|
390
|
+
updatedAt,
|
|
391
|
+
diagnostics: appendDiagnostic(worker.diagnostics, "Stop-requested worker reconciled after pane exit.")
|
|
392
|
+
}
|
|
393
|
+
: worker;
|
|
394
|
+
}
|
|
395
|
+
if (worker.status === "running" && worker.paneId && !tmuxPaneAlive(worker.paneId)) {
|
|
396
|
+
return {
|
|
397
|
+
...worker,
|
|
398
|
+
status: "failed",
|
|
399
|
+
updatedAt,
|
|
400
|
+
error: "Worker pane is no longer active and no result file was produced.",
|
|
401
|
+
diagnostics: appendDiagnostic(worker.diagnostics, "Stale tmux pane detected without a structured result file.")
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
return worker;
|
|
405
|
+
}
|
|
406
|
+
memberResults.push(result);
|
|
407
|
+
const nextStatus = result.status === "error" ? "failed" : result.status === "blocked" ? "blocked" : "completed";
|
|
408
|
+
return {
|
|
409
|
+
...worker,
|
|
410
|
+
status: nextStatus,
|
|
411
|
+
completedAt: nextStatus === "failed" ? worker.completedAt : updatedAt,
|
|
412
|
+
updatedAt,
|
|
413
|
+
error: result.error
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
catch (error) {
|
|
417
|
+
return {
|
|
418
|
+
...worker,
|
|
419
|
+
status: "failed",
|
|
420
|
+
updatedAt,
|
|
421
|
+
error: error instanceof Error ? error.message : String(error),
|
|
422
|
+
diagnostics: [...worker.diagnostics, "Worker result file could not be parsed."]
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
const nextRun = {
|
|
427
|
+
...run,
|
|
428
|
+
workers,
|
|
429
|
+
status: statusFromWorkers(workers),
|
|
430
|
+
updatedAt
|
|
431
|
+
};
|
|
432
|
+
if (nextRun.status === "completed" || nextRun.status === "blocked") {
|
|
433
|
+
const aggregate = {
|
|
434
|
+
id: createId("panel_result"),
|
|
435
|
+
planId: run.planId,
|
|
436
|
+
createdAt: updatedAt,
|
|
437
|
+
updatedAt,
|
|
438
|
+
provider: run.provider,
|
|
439
|
+
surface: "native_workers",
|
|
440
|
+
status: nextRun.status,
|
|
441
|
+
interactionDepth: "independent",
|
|
442
|
+
memberResults,
|
|
443
|
+
linkedQuestionRecordIds: [],
|
|
444
|
+
linkedDecisionRecordIds: []
|
|
445
|
+
};
|
|
446
|
+
await writeJsonAtomic(nextRun.aggregateResultPath, aggregate);
|
|
447
|
+
}
|
|
448
|
+
await writePanelWorkerRun(nextRun);
|
|
449
|
+
return { run: nextRun, memberResults };
|
|
450
|
+
}
|
|
451
|
+
export async function requestPanelWorkerStop(run) {
|
|
452
|
+
if (run.status === "completed") {
|
|
453
|
+
return run;
|
|
454
|
+
}
|
|
455
|
+
const updatedAt = nowIso();
|
|
456
|
+
await writeFile(run.stopFilePath, `${updatedAt}\n`, "utf8");
|
|
457
|
+
const nextRun = {
|
|
458
|
+
...run,
|
|
459
|
+
status: "stop_requested",
|
|
460
|
+
updatedAt,
|
|
461
|
+
workers: run.workers.map((worker) => {
|
|
462
|
+
if (worker.status === "completed") {
|
|
463
|
+
return worker;
|
|
464
|
+
}
|
|
465
|
+
let stopped = !worker.paneId;
|
|
466
|
+
if (worker.paneId && commandAvailable("tmux")) {
|
|
467
|
+
try {
|
|
468
|
+
execFileSync("tmux", ["kill-pane", "-t", worker.paneId], { stdio: "ignore" });
|
|
469
|
+
stopped = true;
|
|
470
|
+
}
|
|
471
|
+
catch {
|
|
472
|
+
stopped = true;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
return {
|
|
476
|
+
...worker,
|
|
477
|
+
status: stopped ? "stopped" : "stop_requested",
|
|
478
|
+
updatedAt,
|
|
479
|
+
diagnostics: appendDiagnostic(worker.diagnostics, "Stop requested through LongTable panel runtime.")
|
|
480
|
+
};
|
|
481
|
+
})
|
|
482
|
+
};
|
|
483
|
+
await writePanelWorkerRun(nextRun);
|
|
484
|
+
return nextRun;
|
|
485
|
+
}
|
|
486
|
+
export async function resumePanelWorkerRun(run) {
|
|
487
|
+
const updatedAt = nowIso();
|
|
488
|
+
await rm(run.stopFilePath, { force: true });
|
|
489
|
+
const nextRun = {
|
|
490
|
+
...run,
|
|
491
|
+
status: "planned",
|
|
492
|
+
updatedAt,
|
|
493
|
+
workers: run.workers.map((worker) => worker.status === "completed"
|
|
494
|
+
? worker
|
|
495
|
+
: {
|
|
496
|
+
...worker,
|
|
497
|
+
status: "pending",
|
|
498
|
+
paneId: undefined,
|
|
499
|
+
error: undefined,
|
|
500
|
+
updatedAt,
|
|
501
|
+
diagnostics: appendDiagnostic(worker.diagnostics, "Resume requested; worker is ready to be relaunched.")
|
|
502
|
+
})
|
|
503
|
+
};
|
|
504
|
+
await writePanelWorkerRun(nextRun);
|
|
505
|
+
return nextRun;
|
|
506
|
+
}
|
|
507
|
+
export async function waitForPanelWorkerRun(run, timeoutMs) {
|
|
508
|
+
const deadline = Date.now() + timeoutMs;
|
|
509
|
+
let current = run;
|
|
510
|
+
while (Date.now() <= deadline) {
|
|
511
|
+
const refreshed = await refreshPanelWorkerRun(current);
|
|
512
|
+
current = refreshed.run;
|
|
513
|
+
if (terminalStatus(current.status) || current.status === "resumable") {
|
|
514
|
+
return current;
|
|
515
|
+
}
|
|
516
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
517
|
+
}
|
|
518
|
+
const timedOut = {
|
|
519
|
+
...current,
|
|
520
|
+
updatedAt: nowIso(),
|
|
521
|
+
diagnostics: appendDiagnostic(current.diagnostics, `wait-timeout:${timeoutMs}ms`)
|
|
522
|
+
};
|
|
523
|
+
await writePanelWorkerRun(timedOut);
|
|
524
|
+
return timedOut;
|
|
525
|
+
}
|
package/dist/panel.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CheckpointSensitivity, InteractionMode, InvocationIntent, InvocationRecord, PanelPlan, PanelResult, PanelVisibility, QuestionRecord, ProviderKind, RoleKey } from "@longtable/core";
|
|
1
|
+
import type { CheckpointSensitivity, InteractionMode, InvocationIntent, InvocationRecord, PanelPlan, PanelResult, PanelVisibility, QuestionRecord, ProviderKind, InvocationSurface, RoleKey } from "@longtable/core";
|
|
2
2
|
import { type CanonicalPersona } from "./personas.js";
|
|
3
3
|
export interface BuildPanelPlanOptions {
|
|
4
4
|
prompt: string;
|
|
@@ -7,6 +7,8 @@ export interface BuildPanelPlanOptions {
|
|
|
7
7
|
roles?: CanonicalPersona[];
|
|
8
8
|
provider?: ProviderKind;
|
|
9
9
|
visibility?: PanelVisibility;
|
|
10
|
+
nativeSubagents?: boolean;
|
|
11
|
+
nativeWorkers?: boolean;
|
|
10
12
|
}
|
|
11
13
|
export interface PanelFallback {
|
|
12
14
|
intent: InvocationIntent;
|
|
@@ -24,6 +26,7 @@ export declare function buildInvocationIntent(options: {
|
|
|
24
26
|
provider?: ProviderKind;
|
|
25
27
|
visibility?: PanelVisibility;
|
|
26
28
|
checkpointSensitivity?: CheckpointSensitivity;
|
|
29
|
+
requestedSurface?: InvocationSurface;
|
|
27
30
|
rationale?: string[];
|
|
28
31
|
}): InvocationIntent;
|
|
29
32
|
export declare function createPlannedPanelQuestionRecord(plan: PanelPlan, provider?: ProviderKind): QuestionRecord;
|
package/dist/panel.js
CHANGED
|
@@ -67,6 +67,11 @@ export function buildPanelPlan(options) {
|
|
|
67
67
|
const routedRoles = routePersonas(options.prompt).consultedRoles;
|
|
68
68
|
const roles = resolvePanelRoles(options);
|
|
69
69
|
const createdAt = nowIso();
|
|
70
|
+
const preferredSurface = options.nativeWorkers && options.provider === "codex"
|
|
71
|
+
? "native_workers"
|
|
72
|
+
: options.nativeSubagents && options.provider === "codex"
|
|
73
|
+
? "native_subagents"
|
|
74
|
+
: "sequential_fallback";
|
|
70
75
|
return {
|
|
71
76
|
id: createId("panel_plan"),
|
|
72
77
|
createdAt,
|
|
@@ -74,12 +79,17 @@ export function buildPanelPlan(options) {
|
|
|
74
79
|
prompt: options.prompt,
|
|
75
80
|
members: roles.map((role) => memberForRole(role, explicitRoles, routedRoles)),
|
|
76
81
|
visibility: options.visibility ?? "always_visible",
|
|
77
|
-
preferredSurface
|
|
82
|
+
preferredSurface,
|
|
78
83
|
fallbackSurface: "sequential_fallback",
|
|
79
84
|
checkpointSensitivity: highestSensitivity(roles),
|
|
80
85
|
rationale: [
|
|
81
86
|
"Option A uses provider-neutral panel semantics before native provider orchestration.",
|
|
82
|
-
|
|
87
|
+
preferredSurface === "native_workers"
|
|
88
|
+
? "LongTable-native workers may execute role passes through durable worker state; outputs must normalize back to this PanelResult."
|
|
89
|
+
: preferredSurface === "native_subagents"
|
|
90
|
+
? "Codex native subagents may execute the role passes when the current provider session exposes them; outputs must normalize back to this PanelResult."
|
|
91
|
+
: "Sequential fallback is the stable execution path for both Claude Code and Codex.",
|
|
92
|
+
"Sequential fallback remains the required degradation path.",
|
|
83
93
|
roles.length === explicitRoles.length && explicitRoles.length > 0
|
|
84
94
|
? "The panel is constrained by explicitly requested roles."
|
|
85
95
|
: "The panel combines default research-review roles with prompt-triggered roles."
|
|
@@ -94,7 +104,7 @@ export function buildInvocationIntent(options) {
|
|
|
94
104
|
prompt: options.prompt,
|
|
95
105
|
roles: options.roles,
|
|
96
106
|
provider: options.provider,
|
|
97
|
-
requestedSurface: "sequential_fallback",
|
|
107
|
+
requestedSurface: options.requestedSurface ?? "sequential_fallback",
|
|
98
108
|
visibility: options.visibility ?? "always_visible",
|
|
99
109
|
checkpointSensitivity: options.checkpointSensitivity ?? "medium",
|
|
100
110
|
rationale: options.rationale ?? ["Panel invocation requested."]
|
|
@@ -158,7 +168,7 @@ export function createPlannedPanelResult(plan, provider, linkedQuestionRecordIds
|
|
|
158
168
|
createdAt,
|
|
159
169
|
updatedAt: createdAt,
|
|
160
170
|
provider,
|
|
161
|
-
surface:
|
|
171
|
+
surface: plan.preferredSurface,
|
|
162
172
|
status: "planned",
|
|
163
173
|
interactionDepth: "independent",
|
|
164
174
|
memberResults: plan.members.map((member) => ({
|
|
@@ -179,11 +189,15 @@ export function createPlannedInvocationRecord(options) {
|
|
|
179
189
|
intent: options.intent,
|
|
180
190
|
status: "planned",
|
|
181
191
|
provider: options.provider,
|
|
182
|
-
surface:
|
|
192
|
+
surface: options.plan.preferredSurface,
|
|
183
193
|
interactionDepth: "independent",
|
|
184
194
|
panelPlan: options.plan,
|
|
185
195
|
panelResult: options.result,
|
|
186
|
-
degradationReason:
|
|
196
|
+
degradationReason: options.plan.preferredSurface === "native_workers"
|
|
197
|
+
? "LongTable-native panel workers are optional; sequential_fallback is the required degradation path when local worker execution is unavailable."
|
|
198
|
+
: options.plan.preferredSurface === "native_subagents"
|
|
199
|
+
? "Codex native subagent execution is session-dependent; sequential_fallback is the required LongTable degradation path."
|
|
200
|
+
: "Sequential fallback is the stable LongTable panel surface."
|
|
187
201
|
};
|
|
188
202
|
}
|
|
189
203
|
function roleInstruction(member) {
|
|
@@ -203,9 +217,20 @@ function languageNote(language) {
|
|
|
203
217
|
}
|
|
204
218
|
export function renderSequentialFallbackPrompt(plan) {
|
|
205
219
|
const language = detectOutputLanguage(plan.prompt);
|
|
220
|
+
const nativeSubagentNote = plan.preferredSurface === "native_subagents"
|
|
221
|
+
? [
|
|
222
|
+
"Preferred execution surface: native_subagents when the current Codex session exposes provider-native subagents.",
|
|
223
|
+
"Fallback: if native subagents are unavailable, run the same role passes sequentially and disclose the fallback in the technical record."
|
|
224
|
+
]
|
|
225
|
+
: plan.preferredSurface === "native_workers"
|
|
226
|
+
? [
|
|
227
|
+
"Preferred execution surface: LongTable-native panel workers when the local runtime supports them.",
|
|
228
|
+
"Fallback: if native workers are unavailable or stopped, run the same role passes sequentially and disclose the fallback in the technical record."
|
|
229
|
+
]
|
|
230
|
+
: ["Execution surface: sequential_fallback"];
|
|
206
231
|
return [
|
|
207
232
|
"LongTable mode: Panel",
|
|
208
|
-
|
|
233
|
+
...nativeSubagentNote,
|
|
209
234
|
"",
|
|
210
235
|
"Run this as a structured panel review. Treat each role as an independent pass, then synthesize.",
|
|
211
236
|
"Do not expose hidden reasoning, private tool traces, or provider chain-of-thought.",
|
|
@@ -221,6 +246,7 @@ export function renderSequentialFallbackPrompt(plan) {
|
|
|
221
246
|
"3. Conflict summary",
|
|
222
247
|
"4. Decision prompt for the researcher",
|
|
223
248
|
"5. Technical record: roles consulted, execution surface, fallback/native mode, and source/file references used",
|
|
249
|
+
"6. Persistence hint: if this is a real panel result, record structured outputs with `longtable panel record --invocation <id> --result-file <json>` before generating `longtable handoff`.",
|
|
224
250
|
"",
|
|
225
251
|
"Research object:",
|
|
226
252
|
plan.prompt
|
|
@@ -235,6 +261,7 @@ export function buildPanelFallback(options) {
|
|
|
235
261
|
provider: options.provider,
|
|
236
262
|
visibility: plan.visibility,
|
|
237
263
|
checkpointSensitivity: plan.checkpointSensitivity,
|
|
264
|
+
requestedSurface: plan.preferredSurface,
|
|
238
265
|
rationale: plan.rationale
|
|
239
266
|
});
|
|
240
267
|
const questionRecord = createPlannedPanelQuestionRecord(plan, options.provider);
|