@longtable/cli 0.1.58 → 0.1.60

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";
@@ -10,7 +10,7 @@ import { homedir } from "node:os";
10
10
  import { fileURLToPath } from "node:url";
11
11
  import { collectHardStopBlockers } from "@longtable/core";
12
12
  import { classifyCheckpointTrigger } from "@longtable/checkpoints";
13
- import { assessSearchSourceCapabilities, buildResearchSearchIntent, parsePublisherTarget, probePublisherAccess, publisherConfigs, runResearchSearch, SEARCH_SOURCES, summarizeConfiguredPublisherAccess } from "./search/index.js";
13
+ import { assessSearchSourceCapabilities, assessScholarResearchReadiness, buildResearchSearchIntent, buildScholarResearchSmokeFixture, parsePublisherTarget, probePublisherAccess, publisherConfigs, runResearchSearch, SEARCH_SOURCES, writeScholarResearchRunScaffold, summarizeConfiguredPublisherAccess } from "./search/index.js";
14
14
  import { buildProviderChoices, buildQuickSetupFlow, createPersistedSetupOutput, installRuntimeConfigFromStoredSetup, loadSetupOutput, renderInstallSummary, renderSetupSummary, resolveDefaultRuntimeConfigPath, resolveDefaultSetupPath, saveSetupOutput, saveSetupAndRuntimeConfig, serializeSetupOutput, writeRuntimeConfig } from "@longtable/setup";
15
15
  import { buildCodexSkillSpecs, buildCodexThinWrappedPrompt, installCodexSkills, listInstalledCodexSkills, renderQuestionRecordPrompt, removeCodexSkills, resolveCodexSkillsDir, runCodexThinWrapper } from "@longtable/provider-codex";
16
16
  import { buildClaudeSkillSpecs, installClaudeSkills, listInstalledClaudeSkills, renderQuestionRecordInput, removeClaudeSkills, resolveClaudeSkillsDir } from "@longtable/provider-claude";
@@ -154,10 +154,13 @@ function usage() {
154
154
  " longtable access doctor [--doi <doi>] [--publisher auto|elsevier|springer_nature|wiley|taylor_francis|all] [--json]",
155
155
  " longtable access probe --doi <doi> [--publisher auto|elsevier|springer_nature|wiley|taylor_francis] [--json]",
156
156
  " longtable search --query <text> [--intent literature|theory|measurement|citation|metadata|venue] [--field <text>] [--source all|crossref,arxiv,openalex,semantic_scholar,pubmed,eric,doaj,unpaywall] [--must <term[,term]>] [--exclude <term[,term]>] [--limit <n>] [--allow-partial] [--publisher-access] [--record] [--cwd <path>] [--json]",
157
+ " longtable scholar-research doctor [--json]",
158
+ " longtable scholar-research scaffold [--cwd <path>] [--run-id <id>] [--json]",
159
+ " longtable scholar-research smoke-fixture [--json]",
157
160
  " longtable sentinel --prompt <text> [--cwd <path>] [--json] [--record]",
158
161
  " longtable ask [--prompt <text>] [--print] [--json] [--setup <path>] [--cwd <path>]",
159
162
  " 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]",
163
+ " 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
164
  " longtable clear-question --question <id> --reason <text> [--cwd <path>] [--json]",
162
165
  " longtable panel [--prompt <text>] [--role <role[,role]>] [--mode review|critique|draft|commit] [--visibility synthesis_only|show_on_conflict|always_visible] [--provider codex|claude] [--native-workers|--native-subagents] [--wait [ms]] [--print] [--json] [--setup <path>] [--cwd <path>]",
163
166
  " longtable panel status --run <panel_run_id> [--wait [ms]] [--cwd <path>] [--json]",
@@ -166,7 +169,7 @@ function usage() {
166
169
  " longtable panel resume --run <panel_run_id> [--wait [ms]] [--cwd <path>] [--json]",
167
170
  " longtable panel record [--invocation <id>] --result-file <json> [--surface sequential_fallback|native_subagents|native_workers] [--cwd <path>] [--json]",
168
171
  " 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]",
172
+ " 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
173
  " 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
174
  " longtable codex persist-init [--answers-json <json> | --stdin | full setup flags] [--install-skills] [--install-prompts] [--json]",
172
175
  " longtable codex install-skills [--surface compact|full] [--dir <path>]",
@@ -201,7 +204,7 @@ function parseArgs(argv) {
201
204
  const values = {};
202
205
  let subcommand = maybeSubcommand;
203
206
  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", "prune-questions", "panel", "handoff", "decide", "sentinel", "access", "search", "spec"].includes(command);
207
+ 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", "scholar-research", "spec"].includes(command);
205
208
  let startIndex = 1;
206
209
  if (modeCommand) {
207
210
  subcommand = undefined;
@@ -210,7 +213,7 @@ function parseArgs(argv) {
210
213
  else if (command === "codex" || command === "claude" || command === "mcp") {
211
214
  startIndex = 2;
212
215
  }
213
- else if ((command === "access" || command === "search" || command === "spec" || command === "panel") && maybeSubcommand && !maybeSubcommand.startsWith("--")) {
216
+ else if ((command === "access" || command === "search" || command === "scholar-research" || command === "spec" || command === "panel") && maybeSubcommand && !maybeSubcommand.startsWith("--")) {
214
217
  subcommand = maybeSubcommand;
215
218
  startIndex = 2;
216
219
  }
@@ -299,6 +302,27 @@ function parseSkillSurface(args) {
299
302
  }
300
303
  throw new Error("Invalid --surface value. Use compact or full.");
301
304
  }
305
+ const QUESTION_SURFACE_VALUES = [
306
+ "native_structured",
307
+ "tmux_popup",
308
+ "mcp_elicitation",
309
+ "numbered",
310
+ "terminal_selector",
311
+ "web_form"
312
+ ];
313
+ function parseQuestionSurface(value) {
314
+ if (value === undefined || value === false) {
315
+ return undefined;
316
+ }
317
+ if (value === true) {
318
+ throw new Error(`Invalid --surface value. Use one of: ${QUESTION_SURFACE_VALUES.join(", ")}.`);
319
+ }
320
+ const normalized = value.trim();
321
+ if (QUESTION_SURFACE_VALUES.includes(normalized)) {
322
+ return normalized;
323
+ }
324
+ throw new Error(`Invalid --surface value. Use one of: ${QUESTION_SURFACE_VALUES.join(", ")}.`);
325
+ }
302
326
  function stripWrappingQuotes(value) {
303
327
  const trimmed = value.trim();
304
328
  if ((trimmed.startsWith("\"") && trimmed.endsWith("\"")) ||
@@ -970,15 +994,25 @@ async function collectProjectInterview(setup, args) {
970
994
  ]));
971
995
  console.log("");
972
996
  }
973
- const projectName = (typeof args.name === "string" && args.name.trim()) ||
997
+ const projectNameInput = (typeof args.name === "string" && args.name.trim()) ||
974
998
  (await promptText(renderQuestionHeader(1, 2, "Workspace", "What should this project be called?"), true));
999
+ if (!projectNameInput) {
1000
+ throw new Error("LongTable start needs a project name.");
1001
+ }
1002
+ const projectName = projectNameInput;
975
1003
  const suggestedParentDir = typeof args.path === "string" && args.path.trim()
976
1004
  ? normalizeUserPath(args.path.trim())
977
1005
  : homedir();
978
1006
  const suggestedPath = resolveInteractiveProjectPath(suggestedParentDir, projectName);
979
- const projectPath = (typeof args.path === "string" && args.path.trim()
1007
+ const projectPathInput = typeof args.path === "string" && args.path.trim()
980
1008
  ? normalizeUserPath(args.path.trim())
981
- : resolveInteractiveProjectPath((await promptText(renderQuestionHeader(2, 2, "Workspace", `Which parent directory should contain this project?\nLongTable will create this folder:\n${suggestedPath}`), true)), projectName));
1009
+ : await promptText(renderQuestionHeader(2, 2, "Workspace", `Which parent directory should contain this project?\nLongTable will create this folder:\n${suggestedPath}`), true);
1010
+ if (!projectPathInput) {
1011
+ throw new Error("LongTable start needs a project path.");
1012
+ }
1013
+ const projectPath = typeof args.path === "string" && args.path.trim()
1014
+ ? projectPathInput
1015
+ : resolveInteractiveProjectPath(projectPathInput, projectName);
982
1016
  const adaptive = skipResearchInterview
983
1017
  ? {}
984
1018
  : await collectAdaptiveStartInterview({
@@ -1616,6 +1650,7 @@ async function collectDoctorStatus(args) {
1616
1650
  return {
1617
1651
  setupPath,
1618
1652
  setupExists: existsSync(setupPath),
1653
+ scholarResearch: assessScholarResearchReadiness(env),
1619
1654
  providers: {
1620
1655
  codex: {
1621
1656
  command: "codex",
@@ -1679,6 +1714,8 @@ function renderDoctorStatus(status) {
1679
1714
  const lines = [
1680
1715
  "LongTable doctor",
1681
1716
  `- setup: ${status.setupExists ? "present" : "missing"} (${status.setupPath})`,
1717
+ `- scholar-research connectors: ${status.scholarResearch.connectors.filter((connector) => connector.status === "ready").length}/${status.scholarResearch.connectors.length} ready`,
1718
+ `- scholar-research safety: paywall bypass ${status.scholarResearch.safety.paywallBypassAllowed ? "allowed" : "disabled"}, institution login automation ${status.scholarResearch.safety.institutionLoginAutomationAllowed ? "allowed" : "disabled"}`,
1682
1719
  "",
1683
1720
  ...renderProviderDoctorBlock("Codex", status.providers.codex),
1684
1721
  `- legacy prompt files: ${status.providers.codex.legacyPromptFilesInstalled.length}`,
@@ -2062,12 +2099,14 @@ function buildRoleAuditEntry(provider, spec) {
2062
2099
  }
2063
2100
  function runRoleAudit() {
2064
2101
  const baseSkillNames = new Set([
2102
+ "critical-interview",
2065
2103
  "longtable",
2066
2104
  "longtable-start",
2067
2105
  "longtable-interview",
2068
2106
  "longtable-panel",
2069
2107
  "longtable-explore",
2070
- "longtable-review"
2108
+ "longtable-review",
2109
+ "scholar-research"
2071
2110
  ]);
2072
2111
  const roles = [
2073
2112
  ...buildCodexSkillSpecs(listRoleDefinitions(), "full")
@@ -2324,6 +2363,149 @@ function commandAvailable(command) {
2324
2363
  return false;
2325
2364
  }
2326
2365
  }
2366
+ function isRecord(value) {
2367
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
2368
+ }
2369
+ function stringArrayValue(value) {
2370
+ if (Array.isArray(value)) {
2371
+ return value.filter((entry) => typeof entry === "string" && entry.trim().length > 0);
2372
+ }
2373
+ return typeof value === "string" && value.trim().length > 0 ? [value] : [];
2374
+ }
2375
+ function stringValue(value) {
2376
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
2377
+ }
2378
+ function currentTmuxReturnPane() {
2379
+ const explicit = stringValue(env.OMX_QUESTION_RETURN_PANE);
2380
+ if (explicit) {
2381
+ return explicit;
2382
+ }
2383
+ return stringValue(env.TMUX_PANE);
2384
+ }
2385
+ function codexTmuxPopupAvailable() {
2386
+ return Boolean(currentTmuxReturnPane() && commandAvailable("omx"));
2387
+ }
2388
+ function buildOmxQuestionInput(record) {
2389
+ const options = record.prompt.options.map((option) => ({
2390
+ label: option.label,
2391
+ value: option.value,
2392
+ ...(option.description ? { description: option.description } : {})
2393
+ }));
2394
+ const freeText = record.prompt.type === "free_text";
2395
+ const type = record.prompt.type === "multi_choice" ? "multi-answerable" : "single-answerable";
2396
+ return {
2397
+ header: "LongTable",
2398
+ question: record.prompt.question,
2399
+ questions: [{
2400
+ id: record.id,
2401
+ question: record.prompt.question,
2402
+ options,
2403
+ allow_other: freeText ? true : record.prompt.allowOther,
2404
+ other_label: freeText ? "Answer" : record.prompt.otherLabel ?? "Other",
2405
+ type,
2406
+ multi_select: record.prompt.type === "multi_choice"
2407
+ }],
2408
+ options,
2409
+ allow_other: freeText ? true : record.prompt.allowOther,
2410
+ other_label: freeText ? "Answer" : record.prompt.otherLabel ?? "Other",
2411
+ type,
2412
+ multi_select: record.prompt.type === "multi_choice",
2413
+ source: "longtable",
2414
+ session_id: record.id
2415
+ };
2416
+ }
2417
+ function readOmxAnswerPayload(payload) {
2418
+ const root = isRecord(payload) ? payload : {};
2419
+ const answers = Array.isArray(root.answers) ? root.answers : [];
2420
+ for (const entry of answers) {
2421
+ if (!isRecord(entry)) {
2422
+ continue;
2423
+ }
2424
+ if (entry.answer !== undefined) {
2425
+ return entry.answer;
2426
+ }
2427
+ return entry;
2428
+ }
2429
+ if (root.answer !== undefined) {
2430
+ return root.answer;
2431
+ }
2432
+ return root;
2433
+ }
2434
+ function questionAnswerFromOmxPayload(record, payload) {
2435
+ const answer = readOmxAnswerPayload(payload);
2436
+ if (typeof answer === "string") {
2437
+ return answer;
2438
+ }
2439
+ if (Array.isArray(answer)) {
2440
+ return stringArrayValue(answer);
2441
+ }
2442
+ if (!isRecord(answer)) {
2443
+ throw new Error("OMX question returned an answer payload LongTable cannot parse.");
2444
+ }
2445
+ if (record.prompt.type === "free_text") {
2446
+ const freeText = [
2447
+ stringValue(answer.other_text),
2448
+ stringValue(answer.otherText),
2449
+ stringValue(answer.text),
2450
+ stringValue(answer.value),
2451
+ stringArrayValue(answer.selected_values).join("\n"),
2452
+ stringArrayValue(answer.selectedValues).join("\n")
2453
+ ].find((entry) => entry && entry.trim().length > 0);
2454
+ if (freeText) {
2455
+ return freeText;
2456
+ }
2457
+ }
2458
+ const selectedValues = [
2459
+ ...stringArrayValue(answer.selected_values),
2460
+ ...stringArrayValue(answer.selectedValues)
2461
+ ];
2462
+ const otherText = stringValue(answer.other_text) ?? stringValue(answer.otherText) ?? stringValue(answer.text);
2463
+ if (answer.kind === "other" && otherText) {
2464
+ return { selectedValues: ["other"], otherText };
2465
+ }
2466
+ if (selectedValues.length > 0) {
2467
+ return otherText
2468
+ ? { selectedValues: selectedValues.includes("other") ? selectedValues : [...selectedValues, "other"], otherText }
2469
+ : selectedValues;
2470
+ }
2471
+ const valueValues = stringArrayValue(answer.value);
2472
+ if (valueValues.length > 0) {
2473
+ return valueValues.length === 1 ? valueValues[0] : valueValues;
2474
+ }
2475
+ if (otherText) {
2476
+ return otherText;
2477
+ }
2478
+ throw new Error("OMX question did not include an answer value.");
2479
+ }
2480
+ function invokeOmxQuestionPopup(record) {
2481
+ const returnPane = currentTmuxReturnPane();
2482
+ if (!returnPane || !commandAvailable("omx")) {
2483
+ throw new Error("LongTable tmux_popup transport requires an attached tmux pane and the `omx` command.");
2484
+ }
2485
+ const child = spawnSync("omx", ["question", "--input", JSON.stringify(buildOmxQuestionInput(record)), "--json"], {
2486
+ cwd: cwd(),
2487
+ env: {
2488
+ ...env,
2489
+ OMX_QUESTION_RETURN_PANE: returnPane
2490
+ },
2491
+ encoding: "utf8",
2492
+ stdio: ["ignore", "pipe", "pipe"]
2493
+ });
2494
+ if (child.error) {
2495
+ throw child.error;
2496
+ }
2497
+ if (child.status !== 0) {
2498
+ const detail = [String(child.stderr ?? "").trim(), String(child.stdout ?? "").trim()].filter(Boolean).join("\n");
2499
+ throw new Error(`LongTable tmux_popup transport failed${detail ? `: ${detail}` : "."}`);
2500
+ }
2501
+ const raw = String(child.stdout ?? "").trim();
2502
+ try {
2503
+ return raw ? JSON.parse(raw) : {};
2504
+ }
2505
+ catch {
2506
+ throw new Error(`LongTable tmux_popup transport returned non-JSON output: ${raw.slice(0, 200)}`);
2507
+ }
2508
+ }
2327
2509
  function parseWaitMs(value) {
2328
2510
  if (value === undefined || value === false) {
2329
2511
  return undefined;
@@ -2665,6 +2847,9 @@ async function runPanelCommand(args) {
2665
2847
  console.log(renderPanelSummary(fallback.plan));
2666
2848
  console.log("");
2667
2849
  if (finalNativeRun) {
2850
+ if (!nativeRunContext) {
2851
+ throw new Error("Native panel worker run finished without a run context.");
2852
+ }
2668
2853
  console.log("LongTable native panel worker run created");
2669
2854
  console.log(`- run: ${finalNativeRun.id}`);
2670
2855
  console.log(`- status: ${finalNativeRun.status}`);
@@ -3291,6 +3476,52 @@ async function runSearch(subcommand, args) {
3291
3476
  }
3292
3477
  console.log(renderEvidenceRunSummary(run, recordedPath));
3293
3478
  }
3479
+ async function runScholarResearch(subcommand, args) {
3480
+ const json = args.json === true;
3481
+ if (subcommand === "doctor" || subcommand === "status" || !subcommand) {
3482
+ const readiness = assessScholarResearchReadiness(env);
3483
+ if (json) {
3484
+ console.log(JSON.stringify(readiness, null, 2));
3485
+ return;
3486
+ }
3487
+ console.log("LongTable scholar-research doctor");
3488
+ for (const connector of readiness.connectors) {
3489
+ const missing = connector.missingEnv.length > 0 ? ` (missing ${connector.missingEnv.join(", ")})` : "";
3490
+ console.log(`- ${connector.name}: ${connector.status}${missing}`);
3491
+ }
3492
+ console.log("- safety: paywall bypass disabled; institution-login automation disabled");
3493
+ return;
3494
+ }
3495
+ if (subcommand === "scaffold") {
3496
+ const workingDirectory = typeof args.cwd === "string" ? args.cwd : cwd();
3497
+ const runId = typeof args["run-id"] === "string" ? args["run-id"] : undefined;
3498
+ const scaffold = await writeScholarResearchRunScaffold({
3499
+ cwd: workingDirectory,
3500
+ ...(runId ? { runId } : {})
3501
+ });
3502
+ if (json) {
3503
+ console.log(JSON.stringify(scaffold, null, 2));
3504
+ return;
3505
+ }
3506
+ console.log("LongTable scholar-research scaffold");
3507
+ console.log(`- run: ${scaffold.runId}`);
3508
+ console.log(`- dir: ${scaffold.runDir}`);
3509
+ return;
3510
+ }
3511
+ if (subcommand === "smoke-fixture") {
3512
+ const fixture = buildScholarResearchSmokeFixture();
3513
+ if (json) {
3514
+ console.log(JSON.stringify({ fixture }, null, 2));
3515
+ return;
3516
+ }
3517
+ console.log("LongTable scholar-research smoke fixture");
3518
+ for (const item of fixture) {
3519
+ console.log(`- ${item.id}: ${item.category} - ${item.label}`);
3520
+ }
3521
+ return;
3522
+ }
3523
+ throw new Error(`Unknown scholar-research subcommand: ${subcommand}`);
3524
+ }
3294
3525
  async function requireWorkspaceContext(args) {
3295
3526
  const workingDirectory = typeof args.cwd === "string" ? args.cwd : cwd();
3296
3527
  const context = await loadProjectContextFromDirectory(workingDirectory);
@@ -3393,11 +3624,51 @@ async function runSpec(subcommand, args) {
3393
3624
  }
3394
3625
  throw new Error(`Unknown spec subcommand: ${command}`);
3395
3626
  }
3627
+ function questionUsesKorean(record) {
3628
+ return /[가-힣]/.test([
3629
+ record.prompt.title,
3630
+ record.prompt.question,
3631
+ record.prompt.displayReason ?? "",
3632
+ ...record.prompt.options.flatMap((option) => [option.label, option.description ?? ""]),
3633
+ record.prompt.otherLabel ?? ""
3634
+ ].join("\n"));
3635
+ }
3636
+ function renderQuestionDecisionCard(record) {
3637
+ const korean = questionUsesKorean(record);
3638
+ const lines = [
3639
+ korean ? "LongTable 결정 카드" : "LongTable Decision Card",
3640
+ `${korean ? "체크포인트" : "Checkpoint"}: ${record.prompt.title}`,
3641
+ `${korean ? "무엇이 걸렸나" : "What is blocked"}: ${record.prompt.displayReason ?? record.prompt.rationale[0] ?? record.prompt.title}`,
3642
+ `${korean ? "지금 결정할 것" : "Decision needed"}: ${record.prompt.question}`,
3643
+ korean ? "선택지:" : "Choices:"
3644
+ ];
3645
+ record.prompt.options.forEach((option, index) => {
3646
+ const recommended = option.recommended ? (korean ? " (추천)" : " (recommended)") : "";
3647
+ lines.push(`${index + 1}. ${option.label}${recommended}`);
3648
+ if (option.description) {
3649
+ lines.push(` ${option.description}`);
3650
+ }
3651
+ lines.push(` ${korean ? "기록값" : "Record value"}: ${option.value}`);
3652
+ });
3653
+ if (record.prompt.allowOther) {
3654
+ lines.push(`${record.prompt.options.length + 1}. ${record.prompt.otherLabel ?? (korean ? "직접 입력" : "Other")}`);
3655
+ lines.push(` ${korean ? "기록값" : "Record value"}: other`);
3656
+ }
3657
+ return lines.join("\n");
3658
+ }
3396
3659
  async function runQuestion(args) {
3397
3660
  const workingDirectory = typeof args.cwd === "string" ? args.cwd : cwd();
3398
- const prompt = await resolvePrompt(typeof args.prompt === "string" ? args.prompt : undefined);
3399
- if (!prompt) {
3400
- throw new Error("A decision context is required. Pass --prompt <text>.");
3661
+ const requestedSurface = parseQuestionSurface(args.surface);
3662
+ if (requestedSurface &&
3663
+ requestedSurface !== "tmux_popup" &&
3664
+ requestedSurface !== "terminal_selector" &&
3665
+ requestedSurface !== "numbered") {
3666
+ 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.");
3667
+ }
3668
+ const questionId = typeof args.question === "string" ? args.question.trim() : "";
3669
+ const prompt = questionId ? undefined : await resolvePrompt(typeof args.prompt === "string" ? args.prompt : undefined);
3670
+ if (!prompt && !questionId) {
3671
+ throw new Error("A decision context or pending question id is required. Pass --prompt <text> or --question <id>.");
3401
3672
  }
3402
3673
  const context = await loadProjectContextFromDirectory(workingDirectory);
3403
3674
  if (!context) {
@@ -3405,17 +3676,73 @@ async function runQuestion(args) {
3405
3676
  }
3406
3677
  const provider = args.provider === "claude" ? "claude" : args.provider === "codex" ? "codex" : undefined;
3407
3678
  const required = args.required === true ? true : args.advisory === true ? false : undefined;
3408
- const result = await createWorkspaceQuestion({
3409
- context,
3410
- prompt,
3411
- title: typeof args.title === "string" ? args.title : undefined,
3412
- question: typeof args.text === "string" ? args.text : undefined,
3413
- provider,
3414
- required
3415
- });
3679
+ let result;
3680
+ if (questionId) {
3681
+ const state = await loadWorkspaceState(context);
3682
+ const question = state.questionLog.find((record) => record.id === questionId && record.status === "pending");
3683
+ if (!question) {
3684
+ throw new Error(`No pending LongTable question found for ${questionId}.`);
3685
+ }
3686
+ result = { question, state };
3687
+ }
3688
+ else {
3689
+ result = await createWorkspaceQuestion({
3690
+ context,
3691
+ prompt: prompt ?? "",
3692
+ title: typeof args.title === "string" ? args.title : undefined,
3693
+ question: typeof args.text === "string" ? args.text : undefined,
3694
+ provider,
3695
+ required
3696
+ });
3697
+ }
3416
3698
  const transport = provider === "claude"
3417
3699
  ? renderQuestionRecordInput(result.question)
3418
3700
  : renderQuestionRecordPrompt(result.question);
3701
+ const shouldTryTmuxPopup = requestedSurface === "tmux_popup" ||
3702
+ (requestedSurface === undefined && provider === "codex" && codexTmuxPopupAvailable() && args.print !== true && args.json !== true);
3703
+ if (shouldTryTmuxPopup) {
3704
+ try {
3705
+ const popupPayload = invokeOmxQuestionPopup(result.question);
3706
+ const answer = questionAnswerFromOmxPayload(result.question, popupPayload);
3707
+ const decision = await answerWorkspaceQuestion({
3708
+ context,
3709
+ questionId: result.question.id,
3710
+ answer,
3711
+ provider,
3712
+ surface: "tmux_popup"
3713
+ });
3714
+ if (args.json === true) {
3715
+ console.log(JSON.stringify({
3716
+ question: decision.question,
3717
+ decision: decision.decision,
3718
+ transport: {
3719
+ surface: "tmux_popup",
3720
+ status: "accepted"
3721
+ },
3722
+ files: {
3723
+ state: context.stateFilePath,
3724
+ current: context.currentFilePath
3725
+ }
3726
+ }, null, 2));
3727
+ return;
3728
+ }
3729
+ console.log("LongTable checkpoint decision recorded");
3730
+ console.log(`- question: ${decision.question.id}`);
3731
+ console.log(`- decision: ${decision.decision.id}`);
3732
+ console.log(`- surface: tmux_popup`);
3733
+ console.log(`- answer: ${decision.decision.selectedOption ?? "recorded"}`);
3734
+ console.log(`- state: ${context.stateFilePath}`);
3735
+ console.log(`- current: ${context.currentFilePath}`);
3736
+ return;
3737
+ }
3738
+ catch (error) {
3739
+ if (requestedSurface === "tmux_popup") {
3740
+ throw error;
3741
+ }
3742
+ const message = error instanceof Error ? error.message : String(error);
3743
+ console.error(`LongTable tmux_popup transport unavailable; falling back. ${message}`);
3744
+ }
3745
+ }
3419
3746
  if (args.json === true) {
3420
3747
  console.log(JSON.stringify({
3421
3748
  question: result.question,
@@ -3437,7 +3764,7 @@ async function runQuestion(args) {
3437
3764
  }
3438
3765
  return;
3439
3766
  }
3440
- if (isInteractiveTerminal()) {
3767
+ if ((requestedSurface === undefined || requestedSurface === "terminal_selector") && isInteractiveTerminal()) {
3441
3768
  console.log(renderBrandBanner("LongTable", "Researcher Checkpoint"));
3442
3769
  console.log("");
3443
3770
  const answer = await promptChoice(renderQuestionHeader(1, 1, result.question.prompt.title, result.question.prompt.question), questionRecordToChoices(result.question));
@@ -3457,16 +3784,11 @@ async function runQuestion(args) {
3457
3784
  console.log(`- current: ${context.currentFilePath}`);
3458
3785
  return;
3459
3786
  }
3460
- const optionValues = [
3461
- ...result.question.prompt.options.map((option) => option.value),
3462
- ...(result.question.prompt.allowOther ? ["other"] : [])
3463
- ];
3464
3787
  console.log(result.question.prompt.required ? "LongTable required Researcher Checkpoint recorded" : "LongTable advisory Researcher Checkpoint recorded");
3465
3788
  console.log(`- question: ${result.question.id}`);
3466
3789
  console.log(`- checkpoint: ${result.question.prompt.checkpointKey ?? "manual"}`);
3467
- console.log(`- prompt: ${result.question.prompt.question}`);
3468
- console.log(`- options: ${optionValues.join("/")}`);
3469
- console.log(`- answer: longtable decide --question ${result.question.id} --answer <value>`);
3790
+ console.log(renderQuestionDecisionCard(result.question));
3791
+ console.log(`- answer: longtable decide --question ${result.question.id} --answer <value> --surface numbered`);
3470
3792
  console.log(`- current: ${context.currentFilePath}`);
3471
3793
  }
3472
3794
  async function runClearQuestion(args) {
@@ -3928,12 +4250,14 @@ async function runDecide(args) {
3928
4250
  throw new Error("No LongTable project workspace was found here. Run this inside a project or pass --cwd.");
3929
4251
  }
3930
4252
  const provider = args.provider === "claude" ? "claude" : args.provider === "codex" ? "codex" : undefined;
4253
+ const surface = parseQuestionSurface(args.surface);
3931
4254
  const result = await answerWorkspaceQuestion({
3932
4255
  context,
3933
4256
  questionId: typeof args.question === "string" ? args.question : undefined,
3934
4257
  answer,
3935
4258
  rationale: typeof args.rationale === "string" ? args.rationale : undefined,
3936
- provider
4259
+ provider,
4260
+ ...(surface ? { surface } : {})
3937
4261
  });
3938
4262
  if (args.json === true) {
3939
4263
  console.log(JSON.stringify({
@@ -4331,6 +4655,10 @@ async function main() {
4331
4655
  await runSearch(subcommand, values);
4332
4656
  return;
4333
4657
  }
4658
+ if (command === "scholar-research") {
4659
+ await runScholarResearch(subcommand, values);
4660
+ return;
4661
+ }
4334
4662
  if (command === "spec") {
4335
4663
  await runSpec(subcommand, values);
4336
4664
  return;
package/dist/debate.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { join } from "node:path";
2
- import { buildInvocationIntent, buildPanelPlan } from "./panel.js";
2
+ import { buildInvocationIntent, buildPanelDecisionContext, buildPanelPlan } from "./panel.js";
3
3
  import { getPersonaDefinition, parsePersonaKey } from "./personas.js";
4
4
  function nowIso() {
5
5
  return new Date().toISOString();
@@ -167,6 +167,10 @@ function buildSynthesis(plan, artifactPath, kind) {
167
167
  const labels = plan.members.map((member) => member.label);
168
168
  const highSensitivity = plan.checkpointSensitivity === "high";
169
169
  const runLabel = kind === "debate" ? "panel debate" : "panel review";
170
+ const decisionContext = buildPanelDecisionContext(plan.prompt);
171
+ const localizedRunLabel = decisionContext.language === "ko"
172
+ ? (kind === "debate" ? "패널 토론" : "패널 리뷰")
173
+ : runLabel;
170
174
  return {
171
175
  artifactPath,
172
176
  summary: `The ${runLabel} completed across ${labels.join(", ")}. It should slow closure by turning role disagreement into an explicit researcher decision.`,
@@ -187,13 +191,16 @@ function buildSynthesis(plan, artifactPath, kind) {
187
191
  "Choose whether the debate should affect the current artifact, the research design, or only the decision log."
188
192
  ],
189
193
  recommendedCheckpoint: highSensitivity
190
- ? `The ${runLabel} surfaced high-sensitivity disagreement. What should LongTable treat as the next human decision before closure?`
191
- : `The ${runLabel} surfaced role disagreement. Should LongTable revise, verify evidence, proceed, or keep the tension open?`
194
+ ? (decisionContext.language === "ko"
195
+ ? `${localizedRunLabel}에서 ${decisionContext.focus} 대한 높은 민감도의 불일치가 드러났습니다. ${decisionContext.decisionQuestion}`
196
+ : `The ${runLabel} surfaced high-sensitivity disagreement about ${decisionContext.focus}. ${decisionContext.decisionQuestion}`)
197
+ : decisionContext.decisionQuestion
192
198
  };
193
199
  }
194
200
  export function createTeamDebateQuestionRecord(run, provider) {
195
201
  const createdAt = nowIso();
196
202
  const isDebate = run.interactionDepth === "debated";
203
+ const decisionContext = buildPanelDecisionContext(run.prompt);
197
204
  return {
198
205
  id: createId("question_record"),
199
206
  createdAt,
@@ -202,51 +209,32 @@ export function createTeamDebateQuestionRecord(run, provider) {
202
209
  prompt: {
203
210
  id: createId("question_prompt"),
204
211
  checkpointKey: "panel_debate_next_decision",
205
- title: isDebate ? "Panel debate follow-up decision" : "Panel review follow-up decision",
212
+ title: decisionContext.language === "ko"
213
+ ? (isDebate ? "패널 토론 후속 결정" : "패널 리뷰 후속 결정")
214
+ : (isDebate ? "Panel debate follow-up decision" : "Panel review follow-up decision"),
206
215
  question: run.synthesis.recommendedCheckpoint,
207
216
  type: "single_choice",
208
- options: [
209
- {
210
- value: "revise",
211
- label: "Revise before proceeding",
212
- description: "Use the debate result to revise the claim, design, or draft first."
213
- },
214
- {
215
- value: "evidence",
216
- label: "Gather or verify evidence first",
217
- description: "Check source, data, or local artifact support before proceeding."
218
- },
219
- {
220
- value: "proceed",
221
- label: "Proceed with current direction",
222
- description: "Accept the risk profile and continue with the current direction."
223
- },
224
- {
225
- value: "defer",
226
- label: "Keep this open",
227
- description: "Do not commit yet; keep the debate issue visible as an open tension."
228
- }
229
- ],
217
+ options: decisionContext.options,
230
218
  allowOther: true,
231
- otherLabel: "Other decision",
219
+ otherLabel: decisionContext.otherLabel,
232
220
  required: run.roles.some((member) => {
233
221
  const key = parsePersonaKey(member.role);
234
222
  return key ? getPersonaDefinition(key).checkpointSensitivity === "high" : false;
235
223
  }),
236
224
  source: "runtime_guidance",
237
- displayReason: isDebate
238
- ? "Role rebuttals and convergence should connect to an explicit researcher decision."
239
- : "Cross-review created role disagreement that should connect to an explicit researcher decision.",
225
+ displayReason: decisionContext.displayReason,
240
226
  rationale: [
241
227
  "LongTable panel orchestration is a research harness surface, not a substitute for researcher judgment.",
242
228
  isDebate
243
229
  ? "The fixed debate rounds created disagreement that should connect to an explicit researcher decision."
244
230
  : "The cross-review round created disagreement that should connect to an explicit researcher decision.",
231
+ `Panel decision focus: ${decisionContext.focus}.`,
232
+ "Panel follow-up choices are compact by default; unlisted decisions should use Other.",
245
233
  `LongTable panel run: ${run.id}.`
246
234
  ],
247
235
  preferredSurfaces: provider === "claude"
248
236
  ? ["native_structured", "numbered"]
249
- : ["mcp_elicitation", "numbered"]
237
+ : ["tmux_popup", "mcp_elicitation", "numbered"]
250
238
  }
251
239
  };
252
240
  }
package/dist/index.d.ts CHANGED
@@ -6,3 +6,4 @@ export * from "./panel.js";
6
6
  export * from "./project-session.js";
7
7
  export * from "./question-obligations.js";
8
8
  export * from "./hard-stop.js";
9
+ export * from "./search/index.js";
package/dist/index.js CHANGED
@@ -6,3 +6,4 @@ export * from "./panel.js";
6
6
  export * from "./project-session.js";
7
7
  export * from "./question-obligations.js";
8
8
  export * from "./hard-stop.js";
9
+ export * from "./search/index.js";