@longtable/cli 0.1.57 → 0.1.59

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { existsSync, readFileSync, statSync } from "node:fs";
3
3
  import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
4
- import { execSync } from "node:child_process";
4
+ import { execSync, spawnSync } from "node:child_process";
5
5
  import { createInterface } from "node:readline/promises";
6
6
  import { createRequire } from "node:module";
7
7
  import { stdin as input, stdout as output, cwd, env, exit } from "node:process";
@@ -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";
@@ -157,16 +157,16 @@ function usage() {
157
157
  " longtable sentinel --prompt <text> [--cwd <path>] [--json] [--record]",
158
158
  " longtable ask [--prompt <text>] [--print] [--json] [--setup <path>] [--cwd <path>]",
159
159
  " longtable clarify --prompt <task-context> [--provider codex|claude] [--required|--advisory] [--print] [--cwd <path>] [--json] [--force]",
160
- " longtable question --prompt <decision-context> [--title <text>] [--text <question>] [--provider codex|claude] [--required|--advisory] [--print] [--cwd <path>] [--json]",
160
+ " longtable question (--prompt <decision-context> | --question <id>) [--title <text>] [--text <question>] [--provider codex|claude] [--required|--advisory] [--surface tmux_popup|terminal_selector|numbered] [--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]",
169
- " longtable decide [--question <id>] --answer <value-or-text> [--rationale <text>] [--provider codex|claude] [--cwd <path>] [--json]",
169
+ " longtable decide [--question <id>] --answer <value-or-text> [--rationale <text>] [--provider codex|claude] [--surface tmux_popup|mcp_elicitation|terminal_selector|numbered] [--cwd <path>] [--json]",
170
170
  " longtable explore|review|critique|draft|commit|submit [--prompt <text>] [--role <role[,role]>] [--panel] [--show-conflicts] [--show-deliberation] [--print] [--json] [--stage <stage>] [--setup <path>] [--cwd <path>]",
171
171
  " longtable codex persist-init [--answers-json <json> | --stdin | full setup flags] [--install-skills] [--install-prompts] [--json]",
172
172
  " longtable codex install-skills [--surface compact|full] [--dir <path>]",
@@ -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", "repair-state", "prune-questions", "panel", "handoff", "decide", "sentinel", "access", "search", "spec"].includes(command);
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;
@@ -299,6 +299,27 @@ function parseSkillSurface(args) {
299
299
  }
300
300
  throw new Error("Invalid --surface value. Use compact or full.");
301
301
  }
302
+ const QUESTION_SURFACE_VALUES = [
303
+ "native_structured",
304
+ "tmux_popup",
305
+ "mcp_elicitation",
306
+ "numbered",
307
+ "terminal_selector",
308
+ "web_form"
309
+ ];
310
+ function parseQuestionSurface(value) {
311
+ if (value === undefined || value === false) {
312
+ return undefined;
313
+ }
314
+ if (value === true) {
315
+ throw new Error(`Invalid --surface value. Use one of: ${QUESTION_SURFACE_VALUES.join(", ")}.`);
316
+ }
317
+ const normalized = value.trim();
318
+ if (QUESTION_SURFACE_VALUES.includes(normalized)) {
319
+ return normalized;
320
+ }
321
+ throw new Error(`Invalid --surface value. Use one of: ${QUESTION_SURFACE_VALUES.join(", ")}.`);
322
+ }
302
323
  function stripWrappingQuotes(value) {
303
324
  const trimmed = value.trim();
304
325
  if ((trimmed.startsWith("\"") && trimmed.endsWith("\"")) ||
@@ -1784,10 +1805,6 @@ function renderDoctorStatus(status) {
1784
1805
  if (!status.workspace.found) {
1785
1806
  nextActions.push("longtable start");
1786
1807
  }
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
1808
  nextActions.push(...status.hardStop.nextActions);
1792
1809
  const firstQuestion = status.workspace.pendingQuestions?.[0];
1793
1810
  if (firstQuestion && status.hardStop.nextActions.length === 0) {
@@ -2328,6 +2345,149 @@ function commandAvailable(command) {
2328
2345
  return false;
2329
2346
  }
2330
2347
  }
2348
+ function isRecord(value) {
2349
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
2350
+ }
2351
+ function stringArrayValue(value) {
2352
+ if (Array.isArray(value)) {
2353
+ return value.filter((entry) => typeof entry === "string" && entry.trim().length > 0);
2354
+ }
2355
+ return typeof value === "string" && value.trim().length > 0 ? [value] : [];
2356
+ }
2357
+ function stringValue(value) {
2358
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
2359
+ }
2360
+ function currentTmuxReturnPane() {
2361
+ const explicit = stringValue(env.OMX_QUESTION_RETURN_PANE);
2362
+ if (explicit) {
2363
+ return explicit;
2364
+ }
2365
+ return stringValue(env.TMUX_PANE);
2366
+ }
2367
+ function codexTmuxPopupAvailable() {
2368
+ return Boolean(currentTmuxReturnPane() && commandAvailable("omx"));
2369
+ }
2370
+ function buildOmxQuestionInput(record) {
2371
+ const options = record.prompt.options.map((option) => ({
2372
+ label: option.label,
2373
+ value: option.value,
2374
+ ...(option.description ? { description: option.description } : {})
2375
+ }));
2376
+ const freeText = record.prompt.type === "free_text";
2377
+ const type = record.prompt.type === "multi_choice" ? "multi-answerable" : "single-answerable";
2378
+ return {
2379
+ header: "LongTable",
2380
+ question: record.prompt.question,
2381
+ questions: [{
2382
+ id: record.id,
2383
+ question: record.prompt.question,
2384
+ options,
2385
+ allow_other: freeText ? true : record.prompt.allowOther,
2386
+ other_label: freeText ? "Answer" : record.prompt.otherLabel ?? "Other",
2387
+ type,
2388
+ multi_select: record.prompt.type === "multi_choice"
2389
+ }],
2390
+ options,
2391
+ allow_other: freeText ? true : record.prompt.allowOther,
2392
+ other_label: freeText ? "Answer" : record.prompt.otherLabel ?? "Other",
2393
+ type,
2394
+ multi_select: record.prompt.type === "multi_choice",
2395
+ source: "longtable",
2396
+ session_id: record.id
2397
+ };
2398
+ }
2399
+ function readOmxAnswerPayload(payload) {
2400
+ const root = isRecord(payload) ? payload : {};
2401
+ const answers = Array.isArray(root.answers) ? root.answers : [];
2402
+ for (const entry of answers) {
2403
+ if (!isRecord(entry)) {
2404
+ continue;
2405
+ }
2406
+ if (entry.answer !== undefined) {
2407
+ return entry.answer;
2408
+ }
2409
+ return entry;
2410
+ }
2411
+ if (root.answer !== undefined) {
2412
+ return root.answer;
2413
+ }
2414
+ return root;
2415
+ }
2416
+ function questionAnswerFromOmxPayload(record, payload) {
2417
+ const answer = readOmxAnswerPayload(payload);
2418
+ if (typeof answer === "string") {
2419
+ return answer;
2420
+ }
2421
+ if (Array.isArray(answer)) {
2422
+ return stringArrayValue(answer);
2423
+ }
2424
+ if (!isRecord(answer)) {
2425
+ throw new Error("OMX question returned an answer payload LongTable cannot parse.");
2426
+ }
2427
+ if (record.prompt.type === "free_text") {
2428
+ const freeText = [
2429
+ stringValue(answer.other_text),
2430
+ stringValue(answer.otherText),
2431
+ stringValue(answer.text),
2432
+ stringValue(answer.value),
2433
+ stringArrayValue(answer.selected_values).join("\n"),
2434
+ stringArrayValue(answer.selectedValues).join("\n")
2435
+ ].find((entry) => entry && entry.trim().length > 0);
2436
+ if (freeText) {
2437
+ return freeText;
2438
+ }
2439
+ }
2440
+ const selectedValues = [
2441
+ ...stringArrayValue(answer.selected_values),
2442
+ ...stringArrayValue(answer.selectedValues)
2443
+ ];
2444
+ const otherText = stringValue(answer.other_text) ?? stringValue(answer.otherText) ?? stringValue(answer.text);
2445
+ if (answer.kind === "other" && otherText) {
2446
+ return { selectedValues: ["other"], otherText };
2447
+ }
2448
+ if (selectedValues.length > 0) {
2449
+ return otherText
2450
+ ? { selectedValues: selectedValues.includes("other") ? selectedValues : [...selectedValues, "other"], otherText }
2451
+ : selectedValues;
2452
+ }
2453
+ const valueValues = stringArrayValue(answer.value);
2454
+ if (valueValues.length > 0) {
2455
+ return valueValues.length === 1 ? valueValues[0] : valueValues;
2456
+ }
2457
+ if (otherText) {
2458
+ return otherText;
2459
+ }
2460
+ throw new Error("OMX question did not include an answer value.");
2461
+ }
2462
+ function invokeOmxQuestionPopup(record) {
2463
+ const returnPane = currentTmuxReturnPane();
2464
+ if (!returnPane || !commandAvailable("omx")) {
2465
+ throw new Error("LongTable tmux_popup transport requires an attached tmux pane and the `omx` command.");
2466
+ }
2467
+ const child = spawnSync("omx", ["question", "--input", JSON.stringify(buildOmxQuestionInput(record)), "--json"], {
2468
+ cwd: cwd(),
2469
+ env: {
2470
+ ...env,
2471
+ OMX_QUESTION_RETURN_PANE: returnPane
2472
+ },
2473
+ encoding: "utf8",
2474
+ stdio: ["ignore", "pipe", "pipe"]
2475
+ });
2476
+ if (child.error) {
2477
+ throw child.error;
2478
+ }
2479
+ if (child.status !== 0) {
2480
+ const detail = [String(child.stderr ?? "").trim(), String(child.stdout ?? "").trim()].filter(Boolean).join("\n");
2481
+ throw new Error(`LongTable tmux_popup transport failed${detail ? `: ${detail}` : "."}`);
2482
+ }
2483
+ const raw = String(child.stdout ?? "").trim();
2484
+ try {
2485
+ return raw ? JSON.parse(raw) : {};
2486
+ }
2487
+ catch {
2488
+ throw new Error(`LongTable tmux_popup transport returned non-JSON output: ${raw.slice(0, 200)}`);
2489
+ }
2490
+ }
2331
2491
  function parseWaitMs(value) {
2332
2492
  if (value === undefined || value === false) {
2333
2493
  return undefined;
@@ -2348,6 +2508,7 @@ function panelWorkerNextCommands(context, runId) {
2348
2508
  return {
2349
2509
  status: `longtable panel status ${cwdFlag} --run ${runId}`,
2350
2510
  stop: `longtable panel stop ${cwdFlag} --run ${runId}`,
2511
+ shutdown: `longtable panel shutdown ${cwdFlag} --run ${runId}`,
2351
2512
  resume: `longtable panel resume ${cwdFlag} --run ${runId}`
2352
2513
  };
2353
2514
  }
@@ -2377,6 +2538,32 @@ function summarizePanelRecordOutput(result) {
2377
2538
  evidenceRecordIds: result.evidenceRecords.map((record) => record.id)
2378
2539
  };
2379
2540
  }
2541
+ function summarizePanelWorkerExecution(run) {
2542
+ const bridgeStatus = run.bridgeStatus ?? run.status;
2543
+ const bridgeFailureReason = run.bridgeFailureReason ?? (bridgeStatus === "degraded"
2544
+ ? "native worker bridge unavailable before execution"
2545
+ : bridgeStatus === "failed" || bridgeStatus === "blocked"
2546
+ ? "native worker bridge did not complete all role passes"
2547
+ : undefined);
2548
+ return {
2549
+ requestedSurface: run.requestedSurface,
2550
+ fallbackSurface: run.fallbackSurface,
2551
+ bridgeStatus,
2552
+ bridgeFailureReason,
2553
+ sequentialFallbackExecuted: false,
2554
+ paneIds: run.workers.map((worker) => worker.paneId).filter((paneId) => typeof paneId === "string" && paneId.length > 0),
2555
+ workers: run.workers.map((worker) => ({
2556
+ id: worker.id,
2557
+ role: worker.role,
2558
+ label: worker.label,
2559
+ status: worker.status,
2560
+ paneId: worker.paneId,
2561
+ resultPath: worker.resultPath,
2562
+ diagnostics: worker.diagnostics
2563
+ })),
2564
+ diagnostics: run.diagnostics
2565
+ };
2566
+ }
2380
2567
  async function runPanelStatusCommand(args) {
2381
2568
  const context = await requireWorkspaceContext(args);
2382
2569
  const runId = requireRunId(args);
@@ -2386,7 +2573,12 @@ async function runPanelStatusCommand(args) {
2386
2573
  const recordedPanelResult = await recordTerminalNativeWorkerRun(context, refreshed);
2387
2574
  const nextCommands = panelWorkerNextCommands(context, refreshed.id);
2388
2575
  if (args.json === true) {
2389
- console.log(JSON.stringify({ ...refreshed, nextCommands, recordedPanelResult: summarizePanelRecordOutput(recordedPanelResult) }, null, 2));
2576
+ console.log(JSON.stringify({
2577
+ ...refreshed,
2578
+ execution: summarizePanelWorkerExecution(refreshed),
2579
+ nextCommands,
2580
+ recordedPanelResult: summarizePanelRecordOutput(recordedPanelResult)
2581
+ }, null, 2));
2390
2582
  return;
2391
2583
  }
2392
2584
  console.log("LongTable panel run status");
@@ -2396,6 +2588,7 @@ async function runPanelStatusCommand(args) {
2396
2588
  console.log(`- ${worker.label} (${worker.role}): ${worker.status}`);
2397
2589
  }
2398
2590
  console.log(`- stop: ${nextCommands.stop}`);
2591
+ console.log(`- shutdown: ${nextCommands.shutdown}`);
2399
2592
  console.log(`- resume: ${nextCommands.resume}`);
2400
2593
  if (recordedPanelResult) {
2401
2594
  console.log(`- recorded evidence: ${recordedPanelResult.evidenceRecords.length}`);
@@ -2416,13 +2609,36 @@ async function runPanelStopCommand(args) {
2416
2609
  for (const worker of stopped.workers) {
2417
2610
  console.log(`- ${worker.label} (${worker.role}): ${worker.status}`);
2418
2611
  }
2612
+ console.log(`- shutdown: ${nextCommands.shutdown}`);
2613
+ console.log(`- resume: ${nextCommands.resume}`);
2614
+ }
2615
+ async function runPanelShutdownCommand(args) {
2616
+ const context = await requireWorkspaceContext(args);
2617
+ const runId = requireRunId(args);
2618
+ const shutdown = await shutdownPanelWorkerRun(await readPanelWorkerRun(context.project.projectPath, runId));
2619
+ const nextCommands = panelWorkerNextCommands(context, shutdown.id);
2620
+ if (args.json === true) {
2621
+ console.log(JSON.stringify({
2622
+ ...shutdown,
2623
+ execution: summarizePanelWorkerExecution(shutdown),
2624
+ nextCommands
2625
+ }, null, 2));
2626
+ return;
2627
+ }
2628
+ console.log("LongTable panel run shutdown requested");
2629
+ console.log(`- run: ${shutdown.id}`);
2630
+ console.log(`- status: ${shutdown.status}`);
2631
+ for (const worker of shutdown.workers) {
2632
+ console.log(`- ${worker.label} (${worker.role}): ${worker.status}`);
2633
+ }
2634
+ console.log(`- status command: ${nextCommands.status}`);
2419
2635
  console.log(`- resume: ${nextCommands.resume}`);
2420
2636
  }
2421
2637
  async function runPanelResumeCommand(args) {
2422
2638
  const context = await requireWorkspaceContext(args);
2423
2639
  const runId = requireRunId(args);
2424
2640
  const waitMs = parseWaitMs(args.wait);
2425
- const { run } = await refreshPanelWorkerRun(await readPanelWorkerRun(context.project.projectPath, runId));
2641
+ const run = await readPanelWorkerRun(context.project.projectPath, runId);
2426
2642
  const resumed = run.status === "completed" ? run : await launchPanelWorkerRun(await resumePanelWorkerRun(run));
2427
2643
  const finalRun = waitMs ? await waitForPanelWorkerRun(resumed, waitMs) : resumed;
2428
2644
  const recordedPanelResult = await recordTerminalNativeWorkerRun(context, finalRun);
@@ -2592,10 +2808,17 @@ async function runPanelCommand(args) {
2592
2808
  nativeRunCreated: Boolean(finalNativeRun),
2593
2809
  waitMs,
2594
2810
  degradedReason: nativeWorkersRequested && !finalNativeRun
2595
- ? "native workers require an existing LongTable workspace; sequential fallback prompt returned"
2811
+ ? "native workers require an existing LongTable workspace; no native run was created and no sequential fallback was executed"
2596
2812
  : finalNativeRun?.status === "degraded"
2597
- ? "native workers require local tmux and codex commands; sequential fallback remains available"
2598
- : undefined
2813
+ ? "native worker bridge unavailable; sequential fallback remains available but was not executed by this native-workers request"
2814
+ : undefined,
2815
+ bridgeStatus: finalNativeRun?.bridgeStatus ?? finalNativeRun?.status,
2816
+ bridgeFailureReason: finalNativeRun?.bridgeFailureReason ?? (finalNativeRun?.status === "degraded"
2817
+ ? "native worker bridge unavailable before execution"
2818
+ : finalNativeRun?.status === "failed" || finalNativeRun?.status === "blocked"
2819
+ ? "native worker bridge did not complete all role passes"
2820
+ : undefined),
2821
+ sequentialFallbackExecuted: !nativeWorkersRequested
2599
2822
  },
2600
2823
  nativeRun: finalNativeRun,
2601
2824
  recordedPanelResult: summarizePanelRecordOutput(recordedPanelResult),
@@ -2613,17 +2836,26 @@ async function runPanelCommand(args) {
2613
2836
  const nextCommands = panelWorkerNextCommands(nativeRunContext, finalNativeRun.id);
2614
2837
  console.log(`- next status: ${nextCommands.status}`);
2615
2838
  console.log(`- stop: ${nextCommands.stop}`);
2839
+ console.log(`- shutdown: ${nextCommands.shutdown}`);
2616
2840
  console.log(`- resume: ${nextCommands.resume}`);
2617
2841
  if (recordedPanelResult) {
2618
2842
  console.log(`- recorded evidence: ${recordedPanelResult.evidenceRecords.length}`);
2619
2843
  }
2620
2844
  if (finalNativeRun.status === "degraded") {
2621
- console.log("- degraded: native workers are unavailable; use the sequential fallback prompt below.");
2845
+ console.log("- degraded: native workers are unavailable; sequential fallback is available but was not executed by this native-workers request.");
2622
2846
  console.log("");
2623
2847
  console.log(fallback.prompt);
2624
2848
  }
2625
2849
  return;
2626
2850
  }
2851
+ if (nativeWorkersRequested) {
2852
+ console.log("LongTable native panel worker run was not created");
2853
+ console.log("- status: failed");
2854
+ console.log("- reason: native workers require an existing LongTable workspace; sequential fallback was not executed by this native-workers request.");
2855
+ console.log("");
2856
+ console.log(fallback.prompt);
2857
+ exit(1);
2858
+ }
2627
2859
  const exitCode = await runCodexThinWrapper({
2628
2860
  prompt: fallback.prompt,
2629
2861
  mode,
@@ -3325,11 +3557,51 @@ async function runSpec(subcommand, args) {
3325
3557
  }
3326
3558
  throw new Error(`Unknown spec subcommand: ${command}`);
3327
3559
  }
3560
+ function questionUsesKorean(record) {
3561
+ return /[가-힣]/.test([
3562
+ record.prompt.title,
3563
+ record.prompt.question,
3564
+ record.prompt.displayReason ?? "",
3565
+ ...record.prompt.options.flatMap((option) => [option.label, option.description ?? ""]),
3566
+ record.prompt.otherLabel ?? ""
3567
+ ].join("\n"));
3568
+ }
3569
+ function renderQuestionDecisionCard(record) {
3570
+ const korean = questionUsesKorean(record);
3571
+ const lines = [
3572
+ korean ? "LongTable 결정 카드" : "LongTable Decision Card",
3573
+ `${korean ? "체크포인트" : "Checkpoint"}: ${record.prompt.title}`,
3574
+ `${korean ? "무엇이 걸렸나" : "What is blocked"}: ${record.prompt.displayReason ?? record.prompt.rationale[0] ?? record.prompt.title}`,
3575
+ `${korean ? "지금 결정할 것" : "Decision needed"}: ${record.prompt.question}`,
3576
+ korean ? "선택지:" : "Choices:"
3577
+ ];
3578
+ record.prompt.options.forEach((option, index) => {
3579
+ const recommended = option.recommended ? (korean ? " (추천)" : " (recommended)") : "";
3580
+ lines.push(`${index + 1}. ${option.label}${recommended}`);
3581
+ if (option.description) {
3582
+ lines.push(` ${option.description}`);
3583
+ }
3584
+ lines.push(` ${korean ? "기록값" : "Record value"}: ${option.value}`);
3585
+ });
3586
+ if (record.prompt.allowOther) {
3587
+ lines.push(`${record.prompt.options.length + 1}. ${record.prompt.otherLabel ?? (korean ? "직접 입력" : "Other")}`);
3588
+ lines.push(` ${korean ? "기록값" : "Record value"}: other`);
3589
+ }
3590
+ return lines.join("\n");
3591
+ }
3328
3592
  async function runQuestion(args) {
3329
3593
  const workingDirectory = typeof args.cwd === "string" ? args.cwd : cwd();
3330
- const prompt = await resolvePrompt(typeof args.prompt === "string" ? args.prompt : undefined);
3331
- if (!prompt) {
3332
- throw new Error("A decision context is required. Pass --prompt <text>.");
3594
+ const requestedSurface = parseQuestionSurface(args.surface);
3595
+ if (requestedSurface &&
3596
+ requestedSurface !== "tmux_popup" &&
3597
+ requestedSurface !== "terminal_selector" &&
3598
+ requestedSurface !== "numbered") {
3599
+ throw new Error("The LongTable question CLI can render tmux_popup, terminal_selector, or numbered surfaces. Use the MCP or provider-native runtime for other surfaces.");
3600
+ }
3601
+ const questionId = typeof args.question === "string" ? args.question.trim() : "";
3602
+ const prompt = questionId ? undefined : await resolvePrompt(typeof args.prompt === "string" ? args.prompt : undefined);
3603
+ if (!prompt && !questionId) {
3604
+ throw new Error("A decision context or pending question id is required. Pass --prompt <text> or --question <id>.");
3333
3605
  }
3334
3606
  const context = await loadProjectContextFromDirectory(workingDirectory);
3335
3607
  if (!context) {
@@ -3337,17 +3609,73 @@ async function runQuestion(args) {
3337
3609
  }
3338
3610
  const provider = args.provider === "claude" ? "claude" : args.provider === "codex" ? "codex" : undefined;
3339
3611
  const required = args.required === true ? true : args.advisory === true ? false : undefined;
3340
- const result = await createWorkspaceQuestion({
3341
- context,
3342
- prompt,
3343
- title: typeof args.title === "string" ? args.title : undefined,
3344
- question: typeof args.text === "string" ? args.text : undefined,
3345
- provider,
3346
- required
3347
- });
3612
+ let result;
3613
+ if (questionId) {
3614
+ const state = await loadWorkspaceState(context);
3615
+ const question = state.questionLog.find((record) => record.id === questionId && record.status === "pending");
3616
+ if (!question) {
3617
+ throw new Error(`No pending LongTable question found for ${questionId}.`);
3618
+ }
3619
+ result = { question, state };
3620
+ }
3621
+ else {
3622
+ result = await createWorkspaceQuestion({
3623
+ context,
3624
+ prompt: prompt ?? "",
3625
+ title: typeof args.title === "string" ? args.title : undefined,
3626
+ question: typeof args.text === "string" ? args.text : undefined,
3627
+ provider,
3628
+ required
3629
+ });
3630
+ }
3348
3631
  const transport = provider === "claude"
3349
3632
  ? renderQuestionRecordInput(result.question)
3350
3633
  : renderQuestionRecordPrompt(result.question);
3634
+ const shouldTryTmuxPopup = requestedSurface === "tmux_popup" ||
3635
+ (requestedSurface === undefined && provider === "codex" && codexTmuxPopupAvailable() && args.print !== true && args.json !== true);
3636
+ if (shouldTryTmuxPopup) {
3637
+ try {
3638
+ const popupPayload = invokeOmxQuestionPopup(result.question);
3639
+ const answer = questionAnswerFromOmxPayload(result.question, popupPayload);
3640
+ const decision = await answerWorkspaceQuestion({
3641
+ context,
3642
+ questionId: result.question.id,
3643
+ answer,
3644
+ provider,
3645
+ surface: "tmux_popup"
3646
+ });
3647
+ if (args.json === true) {
3648
+ console.log(JSON.stringify({
3649
+ question: decision.question,
3650
+ decision: decision.decision,
3651
+ transport: {
3652
+ surface: "tmux_popup",
3653
+ status: "accepted"
3654
+ },
3655
+ files: {
3656
+ state: context.stateFilePath,
3657
+ current: context.currentFilePath
3658
+ }
3659
+ }, null, 2));
3660
+ return;
3661
+ }
3662
+ console.log("LongTable checkpoint decision recorded");
3663
+ console.log(`- question: ${decision.question.id}`);
3664
+ console.log(`- decision: ${decision.decision.id}`);
3665
+ console.log(`- surface: tmux_popup`);
3666
+ console.log(`- answer: ${decision.decision.selectedOption ?? "recorded"}`);
3667
+ console.log(`- state: ${context.stateFilePath}`);
3668
+ console.log(`- current: ${context.currentFilePath}`);
3669
+ return;
3670
+ }
3671
+ catch (error) {
3672
+ if (requestedSurface === "tmux_popup") {
3673
+ throw error;
3674
+ }
3675
+ const message = error instanceof Error ? error.message : String(error);
3676
+ console.error(`LongTable tmux_popup transport unavailable; falling back. ${message}`);
3677
+ }
3678
+ }
3351
3679
  if (args.json === true) {
3352
3680
  console.log(JSON.stringify({
3353
3681
  question: result.question,
@@ -3369,7 +3697,7 @@ async function runQuestion(args) {
3369
3697
  }
3370
3698
  return;
3371
3699
  }
3372
- if (isInteractiveTerminal()) {
3700
+ if ((requestedSurface === undefined || requestedSurface === "terminal_selector") && isInteractiveTerminal()) {
3373
3701
  console.log(renderBrandBanner("LongTable", "Researcher Checkpoint"));
3374
3702
  console.log("");
3375
3703
  const answer = await promptChoice(renderQuestionHeader(1, 1, result.question.prompt.title, result.question.prompt.question), questionRecordToChoices(result.question));
@@ -3389,16 +3717,11 @@ async function runQuestion(args) {
3389
3717
  console.log(`- current: ${context.currentFilePath}`);
3390
3718
  return;
3391
3719
  }
3392
- const optionValues = [
3393
- ...result.question.prompt.options.map((option) => option.value),
3394
- ...(result.question.prompt.allowOther ? ["other"] : [])
3395
- ];
3396
3720
  console.log(result.question.prompt.required ? "LongTable required Researcher Checkpoint recorded" : "LongTable advisory Researcher Checkpoint recorded");
3397
3721
  console.log(`- question: ${result.question.id}`);
3398
3722
  console.log(`- checkpoint: ${result.question.prompt.checkpointKey ?? "manual"}`);
3399
- console.log(`- prompt: ${result.question.prompt.question}`);
3400
- console.log(`- options: ${optionValues.join("/")}`);
3401
- console.log(`- answer: longtable decide --question ${result.question.id} --answer <value>`);
3723
+ console.log(renderQuestionDecisionCard(result.question));
3724
+ console.log(`- answer: longtable decide --question ${result.question.id} --answer <value> --surface numbered`);
3402
3725
  console.log(`- current: ${context.currentFilePath}`);
3403
3726
  }
3404
3727
  async function runClearQuestion(args) {
@@ -3436,39 +3759,6 @@ async function runClearQuestion(args) {
3436
3759
  console.log(`- state: ${context.stateFilePath}`);
3437
3760
  console.log(`- current: ${context.currentFilePath}`);
3438
3761
  }
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
3762
  async function runPruneQuestions(args) {
3473
3763
  const workingDirectory = typeof args.cwd === "string" ? args.cwd : cwd();
3474
3764
  const context = await loadProjectContextFromDirectory(workingDirectory);
@@ -3893,12 +4183,14 @@ async function runDecide(args) {
3893
4183
  throw new Error("No LongTable project workspace was found here. Run this inside a project or pass --cwd.");
3894
4184
  }
3895
4185
  const provider = args.provider === "claude" ? "claude" : args.provider === "codex" ? "codex" : undefined;
4186
+ const surface = parseQuestionSurface(args.surface);
3896
4187
  const result = await answerWorkspaceQuestion({
3897
4188
  context,
3898
4189
  questionId: typeof args.question === "string" ? args.question : undefined,
3899
4190
  answer,
3900
4191
  rationale: typeof args.rationale === "string" ? args.rationale : undefined,
3901
- provider
4192
+ provider,
4193
+ ...(surface ? { surface } : {})
3902
4194
  });
3903
4195
  if (args.json === true) {
3904
4196
  console.log(JSON.stringify({
@@ -4316,10 +4608,6 @@ async function main() {
4316
4608
  await runClearQuestion(values);
4317
4609
  return;
4318
4610
  }
4319
- if (command === "repair-state") {
4320
- await runRepairState(values);
4321
- return;
4322
- }
4323
4611
  if (command === "prune-questions") {
4324
4612
  await runPruneQuestions(values);
4325
4613
  return;
@@ -4337,6 +4625,10 @@ async function main() {
4337
4625
  await runPanelStopCommand(values);
4338
4626
  return;
4339
4627
  }
4628
+ if (subcommand === "shutdown") {
4629
+ await runPanelShutdownCommand(values);
4630
+ return;
4631
+ }
4340
4632
  if (subcommand === "resume") {
4341
4633
  await runPanelResumeCommand(values);
4342
4634
  return;