@longtable/cli 0.1.44 → 0.1.47

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/README.md CHANGED
@@ -269,16 +269,17 @@ deduplicates, ranks, and labels results as evidence cards. Some sources work
269
269
  without keys, some require a contact email, and some need API keys for reliable
270
270
  use.
271
271
 
272
- Publisher access is configured separately through environment variables and
273
- DOI probes. `longtable search setup` checks Elsevier, Springer Nature, Wiley,
274
- and Taylor & Francis credentials or TDM tokens without storing secrets.
272
+ Scholarly access is configured separately through `longtable access setup`.
273
+ It records readiness for metadata, OA full text, institutional access,
274
+ publisher API/TDM credentials, and manual PDFs without storing secrets.
275
+ Publisher probes cover Elsevier, Springer Nature, Wiley, and Taylor & Francis.
275
276
 
276
277
  Citation support should be checked explicitly. A reference can be useful as
277
278
  background while still failing to support the specific claim attached to it.
278
279
 
279
280
  ```bash
280
- longtable search setup
281
- longtable search probe --doi "10.1016/example" --publisher elsevier
281
+ longtable access setup
282
+ longtable access probe --doi "10.1016/example" --publisher elsevier
282
283
  longtable search --query "trust calibration measurement" --intent measurement
283
284
  longtable search --query "trust calibration measurement" --publisher-access --json
284
285
  longtable search --query "trust calibration citation support" --intent citation --record
package/dist/cli.js CHANGED
@@ -9,7 +9,7 @@ import { dirname, join, resolve } from "node:path";
9
9
  import { homedir } from "node:os";
10
10
  import { fileURLToPath } from "node:url";
11
11
  import { classifyCheckpointTrigger } from "@longtable/checkpoints";
12
- import { assessSearchSourceCapabilities, buildResearchSearchIntent, buildSearchCapabilitySnapshot, parsePublisherTarget, probePublisherAccess, publisherConfigs, runResearchSearch, searchCapabilitySnapshotPath, summarizeConfiguredPublisherAccess } from "./search/index.js";
12
+ import { assessSearchSourceCapabilities, buildResearchSearchIntent, parsePublisherTarget, probePublisherAccess, publisherConfigs, runResearchSearch, SEARCH_SOURCES, summarizeConfiguredPublisherAccess } from "./search/index.js";
13
13
  import { buildProviderChoices, buildQuickSetupFlow, createPersistedSetupOutput, installRuntimeConfigFromStoredSetup, loadSetupOutput, renderInstallSummary, renderSetupSummary, resolveDefaultRuntimeConfigPath, resolveDefaultSetupPath, saveSetupOutput, saveSetupAndRuntimeConfig, serializeSetupOutput, writeRuntimeConfig } from "@longtable/setup";
14
14
  import { buildCodexSkillSpecs, buildCodexThinWrappedPrompt, installCodexSkills, listInstalledCodexSkills, renderQuestionRecordPrompt, removeCodexSkills, resolveCodexSkillsDir, runCodexThinWrapper } from "@longtable/provider-codex";
15
15
  import { buildClaudeSkillSpecs, installClaudeSkills, listInstalledClaudeSkills, renderQuestionRecordInput, removeClaudeSkills, resolveClaudeSkillsDir } from "@longtable/provider-claude";
@@ -49,8 +49,35 @@ const require = createRequire(import.meta.url);
49
49
  const LONGTABLE_PACKAGE_VERSION = String(require("../package.json").version ?? "0.0.0");
50
50
  const LONGTABLE_MCP_SERVER_NAME = "longtable-state";
51
51
  const LONGTABLE_MCP_PACKAGE_VERSION = LONGTABLE_PACKAGE_VERSION;
52
+ const LONGTABLE_MCP_PACKAGE_SPEC = `@longtable/mcp@${LONGTABLE_MCP_PACKAGE_VERSION}`;
52
53
  const LONGTABLE_MCP_MARKER_START = "# LongTable state MCP START";
53
54
  const LONGTABLE_MCP_MARKER_END = "# LongTable state MCP END";
55
+ const LONGTABLE_MCP_MANAGED_TOOLS = [
56
+ "read_project",
57
+ "read_session",
58
+ "inspect_workspace",
59
+ "create_workspace",
60
+ "begin_interview",
61
+ "append_interview_turn",
62
+ "summarize_interview",
63
+ "summarize_research_specification",
64
+ "read_research_specification",
65
+ "cancel_interview",
66
+ "confirm_first_research_shape",
67
+ "confirm_research_specification",
68
+ "pending_questions",
69
+ "evaluate_checkpoint",
70
+ "create_question",
71
+ "elicit_question",
72
+ "render_question",
73
+ "append_decision",
74
+ "regenerate_current"
75
+ ];
76
+ const LONGTABLE_MCP_RESEARCH_SPECIFICATION_TOOLS = [
77
+ "summarize_research_specification",
78
+ "read_research_specification",
79
+ "confirm_research_specification"
80
+ ];
54
81
  function style(text, prefix) {
55
82
  return `${prefix}${text}${ANSI.reset}`;
56
83
  }
@@ -84,7 +111,7 @@ function renderInterviewLaunchSteps(provider) {
84
111
  `2. run \`${command}\``,
85
112
  "3. invoke `$longtable-interview`",
86
113
  "",
87
- "The interview will create or resume `.longtable/`, build a First Research Shape, and use option UI only for the final confirmation."
114
+ "The interview will create or resume `.longtable/`, may store a short First Research Shape handle, and uses option UI for the final Research Specification confirmation."
88
115
  ]);
89
116
  }
90
117
  function renderProgressBar(current, total) {
@@ -109,10 +136,11 @@ function usage() {
109
136
  " longtable show [--json] [--path <file>]",
110
137
  " longtable install [--json] [--path <file>] [--runtime-path <file>]",
111
138
  " longtable mcp install [--provider codex|claude|all] [--write] [--checkpoint-ui off|interactive|strong] [--json] [--codex-config <path>] [--claude-settings <path>] [--package <spec>]",
139
+ " longtable access setup [--doi <doi>] [--json]",
140
+ " longtable access status [--json]",
141
+ " longtable access doctor [--doi <doi>] [--publisher auto|elsevier|springer_nature|wiley|taylor_francis|all] [--json]",
142
+ " longtable access probe --doi <doi> [--publisher auto|elsevier|springer_nature|wiley|taylor_francis] [--json]",
112
143
  " 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]",
113
- " longtable search setup [--doi <doi>] [--json]",
114
- " longtable search doctor [--doi <doi>] [--publisher auto|elsevier|springer_nature|wiley|taylor_francis|all] [--json]",
115
- " longtable search probe --doi <doi> [--publisher auto|elsevier|springer_nature|wiley|taylor_francis] [--json]",
116
144
  " longtable sentinel --prompt <text> [--cwd <path>] [--json] [--record]",
117
145
  " longtable team --prompt <text> [--role <role[,role]>] [--debate] [--rounds 3|5] [--cwd <path>] [--json]",
118
146
  " longtable ask [--prompt <text>] [--print] [--json] [--setup <path>] [--cwd <path>]",
@@ -154,7 +182,7 @@ function parseArgs(argv) {
154
182
  const values = {};
155
183
  let subcommand = maybeSubcommand;
156
184
  const modeCommand = command && VALID_MODES.has(command);
157
- const directCommand = command && ["init", "setup", "start", "resume", "doctor", "status", "audit", "roles", "show", "install", "mcp", "codex", "claude", "ask", "clarify", "question", "clear-question", "prune-questions", "panel", "decide", "sentinel", "team", "search"].includes(command);
185
+ const directCommand = command && ["init", "setup", "start", "resume", "doctor", "status", "audit", "roles", "show", "install", "mcp", "codex", "claude", "ask", "clarify", "question", "clear-question", "prune-questions", "panel", "decide", "sentinel", "team", "access", "search"].includes(command);
158
186
  let startIndex = 1;
159
187
  if (modeCommand) {
160
188
  subcommand = undefined;
@@ -163,7 +191,7 @@ function parseArgs(argv) {
163
191
  else if (command === "codex" || command === "claude" || command === "mcp") {
164
192
  startIndex = 2;
165
193
  }
166
- else if (command === "search" && maybeSubcommand && !maybeSubcommand.startsWith("--")) {
194
+ else if ((command === "access" || command === "search") && maybeSubcommand && !maybeSubcommand.startsWith("--")) {
167
195
  subcommand = maybeSubcommand;
168
196
  startIndex = 2;
169
197
  }
@@ -1130,7 +1158,7 @@ function resolveMcpProviders(value) {
1130
1158
  function resolveMcpPackageSpec(args) {
1131
1159
  return typeof args.package === "string" && args.package.trim()
1132
1160
  ? args.package.trim()
1133
- : `@longtable/mcp@${LONGTABLE_MCP_PACKAGE_VERSION}`;
1161
+ : LONGTABLE_MCP_PACKAGE_SPEC;
1134
1162
  }
1135
1163
  function resolveCodexMcpConfigPath(args) {
1136
1164
  return resolve(normalizeUserPath(typeof args["codex-config"] === "string" && args["codex-config"].trim()
@@ -1161,6 +1189,12 @@ function renderCodexMcpBlock(serverName, command, mcpArgs) {
1161
1189
  `[mcp_servers.${serverName}]`,
1162
1190
  `command = ${escapeTomlString(command)}`,
1163
1191
  `args = [${mcpArgs.map((arg) => escapeTomlString(arg)).join(", ")}]`,
1192
+ "",
1193
+ ...LONGTABLE_MCP_MANAGED_TOOLS.flatMap((tool) => [
1194
+ `[mcp_servers.${serverName}.tools.${tool}]`,
1195
+ "approval_mode = \"approve\"",
1196
+ ""
1197
+ ]),
1164
1198
  LONGTABLE_MCP_MARKER_END
1165
1199
  ].join("\n");
1166
1200
  }
@@ -1184,10 +1218,55 @@ function codexMcpElicitationsAllowed(config) {
1184
1218
  function codexLongTableMcpConfigured(config) {
1185
1219
  return new RegExp(`\\[mcp_servers\\.${LONGTABLE_MCP_SERVER_NAME.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\]`).test(config);
1186
1220
  }
1221
+ function codexLongTableMcpPackageSpec(config) {
1222
+ const serverName = LONGTABLE_MCP_SERVER_NAME.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1223
+ const match = new RegExp(`\\[mcp_servers\\.${serverName}\\][\\s\\S]*?(?=\\n\\[|$)`).exec(config);
1224
+ if (!match) {
1225
+ return undefined;
1226
+ }
1227
+ const packageMatch = /@longtable\/mcp@[A-Za-z0-9._~+:-]+/.exec(match[0]);
1228
+ return packageMatch?.[0];
1229
+ }
1230
+ function codexLongTableMcpToolConfigured(config, tool) {
1231
+ const serverName = LONGTABLE_MCP_SERVER_NAME.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1232
+ const escapedTool = tool.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1233
+ return new RegExp(`\\[mcp_servers\\.${serverName}\\.tools\\.${escapedTool}\\]`).test(config);
1234
+ }
1235
+ function missingCodexLongTableMcpTools(config) {
1236
+ if (!codexLongTableMcpConfigured(config)) {
1237
+ return [...LONGTABLE_MCP_MANAGED_TOOLS];
1238
+ }
1239
+ return LONGTABLE_MCP_MANAGED_TOOLS.filter((tool) => !codexLongTableMcpToolConfigured(config, tool));
1240
+ }
1241
+ function preserveNonLongTableSectionsFromMarkedBlock(block, serverName) {
1242
+ const body = block
1243
+ .replace(LONGTABLE_MCP_MARKER_START, "")
1244
+ .replace(LONGTABLE_MCP_MARKER_END, "")
1245
+ .trim();
1246
+ if (!body) {
1247
+ return "";
1248
+ }
1249
+ const sections = body.split(/(?=^\[[^\]]+\])/m);
1250
+ const serverHeader = `[mcp_servers.${serverName}]`;
1251
+ const toolPrefix = `[mcp_servers.${serverName}.tools.`;
1252
+ return sections
1253
+ .map((section) => section.trim())
1254
+ .filter(Boolean)
1255
+ .filter((section) => {
1256
+ const header = section.split(/\r?\n/, 1)[0]?.trim() ?? "";
1257
+ return header !== serverHeader && !header.startsWith(toolPrefix);
1258
+ })
1259
+ .join("\n\n");
1260
+ }
1187
1261
  function replaceMarkedCodexMcpBlock(existing, block, serverName) {
1188
- const markerPattern = new RegExp(`${LONGTABLE_MCP_MARKER_START.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s\\S]*?${LONGTABLE_MCP_MARKER_END.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\n?`, "m");
1189
- const serverPattern = new RegExp(`\\n?\\[mcp_servers\\.${serverName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\][\\s\\S]*?(?=\\n\\[|$)`, "m");
1190
- const trimmed = existing.replace(markerPattern, "").replace(serverPattern, "").trimEnd();
1262
+ const markerPattern = new RegExp(`${LONGTABLE_MCP_MARKER_START.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s\\S]*?${LONGTABLE_MCP_MARKER_END.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\n?`);
1263
+ const serverPattern = new RegExp(`\\n?\\[mcp_servers\\.${serverName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\][\\s\\S]*?(?=\\n\\[|$)`);
1264
+ const toolPattern = new RegExp(`\\n?\\[mcp_servers\\.${serverName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\.tools\\.[^\\]]+\\][\\s\\S]*?(?=\\n\\[|$)`, "g");
1265
+ const withoutMarked = existing.replace(markerPattern, (matched) => {
1266
+ const preserved = preserveNonLongTableSectionsFromMarkedBlock(matched, serverName);
1267
+ return preserved ? `${preserved}\n\n` : "";
1268
+ });
1269
+ const trimmed = withoutMarked.replace(toolPattern, "").replace(serverPattern, "").trimEnd();
1191
1270
  return trimmed ? `${trimmed}\n\n${block}\n` : `${block}\n`;
1192
1271
  }
1193
1272
  async function writeCodexMcpConfig(path, block, serverName, options = {}) {
@@ -1466,6 +1545,8 @@ async function collectDoctorStatus(args) {
1466
1545
  const codexMcpConfig = existsSync(codexMcpConfigPath)
1467
1546
  ? await readFile(codexMcpConfigPath, "utf8")
1468
1547
  : "";
1548
+ const codexMcpPackageSpec = codexLongTableMcpPackageSpec(codexMcpConfig);
1549
+ const missingMcpTools = missingCodexLongTableMcpTools(codexMcpConfig);
1469
1550
  const codexHooksPath = resolveCodexHooksPath(args);
1470
1551
  const codexHooksContent = existsSync(codexHooksPath)
1471
1552
  ? await readFile(codexHooksPath, "utf8")
@@ -1501,6 +1582,10 @@ async function collectDoctorStatus(args) {
1501
1582
  mcpConfigPath: codexMcpConfigPath,
1502
1583
  mcpConfigExists: existsSync(codexMcpConfigPath),
1503
1584
  longtableMcpConfigured: codexLongTableMcpConfigured(codexMcpConfig),
1585
+ ...(codexMcpPackageSpec ? { mcpPackageSpec: codexMcpPackageSpec } : {}),
1586
+ expectedMcpPackageSpec: LONGTABLE_MCP_PACKAGE_SPEC,
1587
+ missingMcpTools,
1588
+ missingResearchSpecificationMcpTools: missingMcpTools.filter((tool) => LONGTABLE_MCP_RESEARCH_SPECIFICATION_TOOLS.includes(tool)),
1504
1589
  mcpElicitationsAllowed: codexMcpElicitationsAllowed(codexMcpConfig),
1505
1590
  hooksPath: codexHooksPath,
1506
1591
  hooksExists: existsSync(codexHooksPath),
@@ -1546,6 +1631,11 @@ function renderDoctorStatus(status) {
1546
1631
  : []),
1547
1632
  `- MCP config: ${status.providers.codex.mcpConfigExists ? "present" : "missing"} (${status.providers.codex.mcpConfigPath})`,
1548
1633
  `- LongTable MCP: ${status.providers.codex.longtableMcpConfigured ? "configured" : "missing"}`,
1634
+ `- MCP package: ${status.providers.codex.mcpPackageSpec ?? "unknown"}${status.providers.codex.mcpPackageSpec === status.providers.codex.expectedMcpPackageSpec ? "" : ` (expected ${status.providers.codex.expectedMcpPackageSpec})`}`,
1635
+ `- MCP managed tools: ${LONGTABLE_MCP_MANAGED_TOOLS.length - status.providers.codex.missingMcpTools.length}/${LONGTABLE_MCP_MANAGED_TOOLS.length} configured`,
1636
+ ...(status.providers.codex.missingResearchSpecificationMcpTools.length > 0
1637
+ ? [`- Research Specification MCP tools: missing ${status.providers.codex.missingResearchSpecificationMcpTools.join(", ")}`]
1638
+ : ["- Research Specification MCP tools: complete"]),
1549
1639
  `- MCP elicitation approval: ${status.providers.codex.mcpElicitationsAllowed ? "allowed" : "not allowed"}`,
1550
1640
  `- Codex hooks file: ${status.providers.codex.hooksExists ? "present" : "missing"} (${status.providers.codex.hooksPath})`,
1551
1641
  `- codex_hooks feature: ${status.providers.codex.codexHooksEnabled ? "enabled" : "missing"}`,
@@ -1594,6 +1684,9 @@ function renderDoctorStatus(status) {
1594
1684
  const canFix = status.providers.codex.missingSkills.length > 0 ||
1595
1685
  status.providers.claude.missingSkills.length > 0 ||
1596
1686
  status.providers.codex.legacyPromptFilesInstalled.length > 0 ||
1687
+ !status.providers.codex.longtableMcpConfigured ||
1688
+ status.providers.codex.mcpPackageSpec !== status.providers.codex.expectedMcpPackageSpec ||
1689
+ status.providers.codex.missingMcpTools.length > 0 ||
1597
1690
  !status.providers.codex.codexHooksEnabled ||
1598
1691
  status.providers.codex.missingManagedHookEvents.length > 0 ||
1599
1692
  (status.setupExists &&
@@ -1604,6 +1697,11 @@ function renderDoctorStatus(status) {
1604
1697
  if (!status.providers.codex.codexHooksEnabled || status.providers.codex.missingManagedHookEvents.length > 0) {
1605
1698
  nextActions.push("longtable codex install-hooks");
1606
1699
  }
1700
+ if (!status.providers.codex.longtableMcpConfigured ||
1701
+ status.providers.codex.mcpPackageSpec !== status.providers.codex.expectedMcpPackageSpec ||
1702
+ status.providers.codex.missingMcpTools.length > 0) {
1703
+ nextActions.push("longtable mcp install --provider codex --write");
1704
+ }
1607
1705
  if (!status.setupExists) {
1608
1706
  nextActions.push("longtable setup --provider codex");
1609
1707
  }
@@ -1643,7 +1741,7 @@ function renderRepairSummary(repair) {
1643
1741
  }
1644
1742
  }
1645
1743
  if (repair.writtenRuntimeConfigs.length > 0) {
1646
- lines.push("- wrote runtime configs:");
1744
+ lines.push("- wrote configs:");
1647
1745
  for (const target of repair.writtenRuntimeConfigs) {
1648
1746
  lines.push(` - ${target.provider}: ${target.path}`);
1649
1747
  }
@@ -1699,6 +1797,21 @@ async function repairDoctorStatus(args, status) {
1699
1797
  writtenRuntimeConfigs: [],
1700
1798
  skipped: []
1701
1799
  };
1800
+ const mcpRepairNeeded = !status.providers.codex.longtableMcpConfigured ||
1801
+ status.providers.codex.mcpPackageSpec !== status.providers.codex.expectedMcpPackageSpec ||
1802
+ status.providers.codex.missingMcpTools.length > 0;
1803
+ if (mcpRepairNeeded) {
1804
+ const mcpConfigPath = resolveDoctorCodexMcpConfigPath(args);
1805
+ const block = renderCodexMcpBlock(LONGTABLE_MCP_SERVER_NAME, "npx", ["-y", LONGTABLE_MCP_PACKAGE_SPEC]);
1806
+ await writeCodexMcpConfig(mcpConfigPath, block, LONGTABLE_MCP_SERVER_NAME, {
1807
+ enableElicitations: status.providers.codex.mcpElicitationsAllowed
1808
+ });
1809
+ repair.writtenRuntimeConfigs.push({
1810
+ provider: "codex",
1811
+ path: mcpConfigPath,
1812
+ format: "toml"
1813
+ });
1814
+ }
1702
1815
  if (status.providers.codex.missingSkills.length > 0) {
1703
1816
  repair.installedCodexSkills = (await installCodexSkills(roles, codexDir, skillSurface)).map((skill) => skill.name);
1704
1817
  }
@@ -1789,6 +1902,11 @@ const QUESTION_AUDIT_FIXTURES = [
1789
1902
  prompt: "Protected decision closure pressure: measurement. User prompt: Implement the plan.",
1790
1903
  expectedKinds: ["research_commitment"]
1791
1904
  },
1905
+ {
1906
+ id: "scholarly_access_policy",
1907
+ prompt: "메타분석 논문들의 PDF와 full text를 수집해서 원문 기반으로 코딩해줘.",
1908
+ expectedKinds: ["evidence_risk"]
1909
+ },
1792
1910
  {
1793
1911
  id: "low_stakes_copyedit",
1794
1912
  prompt: "문장 끝 공백만 정리해줘.",
@@ -2347,6 +2465,86 @@ async function recordEvidenceRun(run, workingDirectory) {
2347
2465
  await syncCurrentWorkspaceView(context);
2348
2466
  return evidencePath;
2349
2467
  }
2468
+ function nowIso() {
2469
+ return new Date().toISOString();
2470
+ }
2471
+ function uniqueAccessRoutes(routes) {
2472
+ return [...new Set(routes)];
2473
+ }
2474
+ function accessReadinessPath(home = homedir()) {
2475
+ return join(home, ".longtable", "access-readiness.json");
2476
+ }
2477
+ function readAccessReadinessProfile() {
2478
+ const path = accessReadinessPath();
2479
+ if (!existsSync(path)) {
2480
+ return undefined;
2481
+ }
2482
+ try {
2483
+ return JSON.parse(readFileSync(path, "utf8"));
2484
+ }
2485
+ catch {
2486
+ return undefined;
2487
+ }
2488
+ }
2489
+ function hasPublisherCredentialSignal(records) {
2490
+ return records.some((record) => record.presentEnv.length > 0 || record.credentialStatus !== "missing");
2491
+ }
2492
+ function readinessPublisherRecord(record) {
2493
+ const safeRecord = { ...record };
2494
+ delete safeRecord.evidenceSnippet;
2495
+ return safeRecord;
2496
+ }
2497
+ function inferAccessRoutes(records) {
2498
+ const routes = ["metadata"];
2499
+ if (hasPublisherCredentialSignal(records)) {
2500
+ routes.push("publisher_tdm");
2501
+ }
2502
+ return routes;
2503
+ }
2504
+ function buildAccessReadinessProfile(options) {
2505
+ const publisherRecords = options.publisherRecords ?? summarizeConfiguredPublisherAccess(env);
2506
+ const routes = uniqueAccessRoutes(options.routes?.length ? options.routes : inferAccessRoutes(publisherRecords));
2507
+ const disabled = options.readiness === "disabled";
2508
+ const institutionalMode = options.institutionalAccessMode;
2509
+ const institutionalAccess = routes.includes("institutional")
2510
+ ? {
2511
+ available: true,
2512
+ mode: institutionalMode ?? "unknown",
2513
+ verified: false,
2514
+ note: "LongTable records institutional access readiness only. The researcher must complete VPN/proxy/library login directly."
2515
+ }
2516
+ : undefined;
2517
+ return {
2518
+ version: 1,
2519
+ updatedAt: nowIso(),
2520
+ readiness: options.readiness,
2521
+ metadataSources: [...SEARCH_SOURCES],
2522
+ routes: disabled ? [] : routes,
2523
+ ...(institutionalAccess ? { institutionalAccess } : {}),
2524
+ publisherTdm: disabled
2525
+ ? "deferred"
2526
+ : routes.includes("publisher_tdm")
2527
+ ? hasPublisherCredentialSignal(publisherRecords) ? "configured" : "unknown"
2528
+ : "not_configured",
2529
+ oaOnly: !disabled && routes.includes("oa_full_text") && !routes.includes("institutional") && !routes.includes("publisher_tdm"),
2530
+ manualPdfAllowed: !disabled && routes.includes("manual_pdf"),
2531
+ storesSecrets: false,
2532
+ requiresCheckpointBeforeSearch: options.readiness === "deferred",
2533
+ requiresCheckpointBeforeFullText: options.readiness !== "disabled",
2534
+ ...(disabled ? {} : {
2535
+ publisherAccess: {
2536
+ contactEmailPresent: Boolean(env.LONGTABLE_CONTACT_EMAIL?.trim()),
2537
+ records: publisherRecords.map(readinessPublisherRecord)
2538
+ }
2539
+ })
2540
+ };
2541
+ }
2542
+ async function saveAccessReadinessProfile(profile) {
2543
+ const profilePath = accessReadinessPath();
2544
+ await mkdir(dirname(profilePath), { recursive: true });
2545
+ await writeJsonFile(profilePath, profile);
2546
+ return profilePath;
2547
+ }
2350
2548
  function renderPublisherAccessRecord(record) {
2351
2549
  const envSummary = record.missingEnv.length > 0
2352
2550
  ? `missing ${record.missingEnv.join(", ")}`
@@ -2381,12 +2579,6 @@ function renderPublisherAccessRecords(title, records, capabilityPath) {
2381
2579
  }
2382
2580
  return lines.join("\n");
2383
2581
  }
2384
- async function saveSearchCapabilityRecords(records) {
2385
- const snapshotPath = searchCapabilitySnapshotPath();
2386
- await mkdir(dirname(snapshotPath), { recursive: true });
2387
- await writeJsonFile(snapshotPath, buildSearchCapabilitySnapshot(records, env));
2388
- return snapshotPath;
2389
- }
2390
2582
  async function probeAllPublishers(doi) {
2391
2583
  const records = [];
2392
2584
  for (const publisher of publisherConfigs()) {
@@ -2398,9 +2590,48 @@ async function probeAllPublishers(doi) {
2398
2590
  }
2399
2591
  return records;
2400
2592
  }
2401
- async function runSearchProbe(args) {
2593
+ function renderAccessReadinessProfile(profile, profilePath = accessReadinessPath()) {
2594
+ const lines = [
2595
+ "LongTable Scholarly Access Readiness",
2596
+ `- profile: ${profilePath}`,
2597
+ `- readiness: ${profile.readiness}`,
2598
+ `- routes: ${profile.routes.length > 0 ? profile.routes.join(", ") : "none"}`,
2599
+ `- metadata sources: ${profile.metadataSources.join(", ")}`,
2600
+ `- OA-only: ${profile.oaOnly ? "yes" : "no"}`,
2601
+ `- manual PDF allowed: ${profile.manualPdfAllowed ? "yes" : "no"}`,
2602
+ `- publisher/TDM: ${profile.publisherTdm}`,
2603
+ `- stores secrets: ${profile.storesSecrets ? "yes" : "no"}`,
2604
+ `- checkpoint before search: ${profile.requiresCheckpointBeforeSearch ? "yes" : "no"}`,
2605
+ `- checkpoint before full text: ${profile.requiresCheckpointBeforeFullText ? "yes" : "no"}`
2606
+ ];
2607
+ if (profile.institutionalAccess) {
2608
+ lines.push(`- institutional access: ${profile.institutionalAccess.mode}; verified: no`);
2609
+ lines.push(` note: ${profile.institutionalAccess.note}`);
2610
+ }
2611
+ if (profile.publisherAccess) {
2612
+ lines.push(`- contact email: ${profile.publisherAccess.contactEmailPresent ? "present" : "missing"}`);
2613
+ lines.push(`- publisher adapters: ${profile.publisherAccess.records.length}`);
2614
+ }
2615
+ return lines.join("\n");
2616
+ }
2617
+ function renderAccessDoctor(profile, records, profilePath) {
2618
+ const metadataCapabilities = assessSearchSourceCapabilities([...SEARCH_SOURCES], env);
2619
+ const lines = [
2620
+ "LongTable Scholarly Access Doctor",
2621
+ `- readiness profile: ${profile ? "present" : "missing"} (${profilePath})`,
2622
+ `- metadata sources: ${metadataCapabilities.map((capability) => `${capability.source}:${capability.enabled ? "available" : "needs_config"}`).join(", ")}`,
2623
+ "- institutional access: LongTable cannot verify VPN/proxy/SSO until the researcher logs in.",
2624
+ "- secrets: LongTable stores env var names and capability status only, never credential values.",
2625
+ renderPublisherAccessRecords("Publisher API/TDM adapters", records)
2626
+ ];
2627
+ if (!profile) {
2628
+ lines.push("- next: run `longtable access setup` before PDF collection or full-text extraction.");
2629
+ }
2630
+ return lines.join("\n");
2631
+ }
2632
+ async function runAccessProbe(args) {
2402
2633
  if (typeof args.doi !== "string" || !args.doi.trim()) {
2403
- throw new Error("`longtable search probe` requires --doi <doi>.");
2634
+ throw new Error("`longtable access probe` requires --doi <doi>.");
2404
2635
  }
2405
2636
  const publisher = parsePublisherTarget(args.publisher);
2406
2637
  const record = await probePublisherAccess({
@@ -2416,7 +2647,7 @@ async function runSearchProbe(args) {
2416
2647
  }
2417
2648
  return [record];
2418
2649
  }
2419
- async function runSearchDoctor(args) {
2650
+ async function collectAccessDoctorRecords(args) {
2420
2651
  let records;
2421
2652
  if (typeof args.doi === "string" && args.doi.trim()) {
2422
2653
  if (args.publisher === "all") {
@@ -2434,104 +2665,148 @@ async function runSearchDoctor(args) {
2434
2665
  else {
2435
2666
  records = summarizeConfiguredPublisherAccess(env);
2436
2667
  }
2437
- const snapshotPath = searchCapabilitySnapshotPath();
2438
- const snapshotExists = existsSync(snapshotPath);
2668
+ return records;
2669
+ }
2670
+ async function runAccessDoctor(args) {
2671
+ const records = await collectAccessDoctorRecords(args);
2672
+ const profilePath = accessReadinessPath();
2673
+ const profile = readAccessReadinessProfile();
2439
2674
  if (args.json === true) {
2440
2675
  console.log(JSON.stringify({
2441
- capabilityFile: snapshotPath,
2442
- capabilityFileExists: snapshotExists,
2676
+ readinessFile: profilePath,
2677
+ readinessFileExists: Boolean(profile),
2678
+ readiness: profile,
2679
+ metadataSources: assessSearchSourceCapabilities([...SEARCH_SOURCES], env),
2443
2680
  records
2444
2681
  }, null, 2));
2445
2682
  }
2446
2683
  else {
2447
- console.log(renderPublisherAccessRecords("LongTable Search Publisher Access Doctor", records, snapshotPath));
2448
- if (!snapshotExists) {
2449
- console.log("- saved capabilities: none yet; run `longtable search setup` to record non-secret capability status.");
2450
- }
2684
+ console.log(renderAccessDoctor(profile, records, profilePath));
2451
2685
  }
2452
2686
  return records;
2453
2687
  }
2454
- async function promptPublisherDoi(rl, label, defaultDoi) {
2455
- const prompt = defaultDoi
2456
- ? `${label} test DOI [${defaultDoi}, Enter to reuse, skip to skip]: `
2457
- : `${label} test DOI (Enter to skip): `;
2458
- const answer = (await rl.question(prompt)).trim();
2459
- if (!answer && defaultDoi) {
2460
- return defaultDoi;
2461
- }
2462
- if (!answer || /^skip$/i.test(answer)) {
2463
- return undefined;
2688
+ async function publisherRecordsForAccessSetup(args, routes) {
2689
+ const defaultDoi = typeof args.doi === "string" ? args.doi : undefined;
2690
+ if (!routes.includes("publisher_tdm")) {
2691
+ return summarizeConfiguredPublisherAccess(env);
2464
2692
  }
2465
- return answer;
2466
- }
2467
- async function runInteractiveSearchSetup(defaultDoi) {
2468
- const rl = createInterface({ input, output });
2469
- const records = [];
2470
- try {
2471
- console.log("LongTable publisher access setup");
2472
- console.log("LongTable does not store API keys or TDM tokens. It reads environment variables and records only non-secret capability results.");
2473
- console.log("");
2474
- for (const publisher of publisherConfigs()) {
2475
- console.log(`${publisher.label}`);
2476
- console.log(` required env: ${publisher.requiredEnv.join(", ")}`);
2477
- if (publisher.optionalEnv.length > 0) {
2478
- console.log(` optional env: ${publisher.optionalEnv.join(", ")}`);
2479
- }
2480
- console.log(` ${publisher.setupHint}`);
2481
- const doi = await promptPublisherDoi(rl, publisher.label, defaultDoi);
2482
- if (doi) {
2483
- records.push(await probePublisherAccess({
2484
- doi,
2485
- publisher: publisher.publisher,
2486
- env
2487
- }));
2488
- }
2489
- else {
2490
- const summary = summarizeConfiguredPublisherAccess(env)
2491
- .find((record) => record.publisher === publisher.publisher);
2492
- if (summary) {
2493
- records.push(summary);
2494
- }
2495
- }
2496
- console.log(renderPublisherAccessRecord(records[records.length - 1]));
2497
- console.log("");
2498
- }
2693
+ if (defaultDoi) {
2694
+ return probeAllPublishers(defaultDoi);
2499
2695
  }
2500
- finally {
2501
- rl.close();
2696
+ if (input.isTTY && output.isTTY && args.json !== true) {
2697
+ const doi = await promptText("Optional DOI for publisher/TDM probing. Leave blank to record env-var readiness only.", false);
2698
+ return doi
2699
+ ? await probeAllPublishers(doi)
2700
+ : summarizeConfiguredPublisherAccess(env);
2502
2701
  }
2503
- return records;
2702
+ return summarizeConfiguredPublisherAccess(env);
2504
2703
  }
2505
- async function runSearchSetup(args) {
2506
- const defaultDoi = typeof args.doi === "string" ? args.doi : undefined;
2507
- const records = input.isTTY && output.isTTY && args.json !== true
2508
- ? await runInteractiveSearchSetup(defaultDoi)
2509
- : defaultDoi
2510
- ? await probeAllPublishers(defaultDoi)
2511
- : summarizeConfiguredPublisherAccess(env);
2512
- const snapshotPath = await saveSearchCapabilityRecords(records);
2704
+ async function runInteractiveAccessSetup(args) {
2705
+ const readiness = await promptChoice("Scholarly Access Readiness\n\nWill this machine/account use scholarly search or full-text access?", [
2706
+ { id: "configured", label: "Configure now", description: "Record access capability without storing secrets." },
2707
+ { id: "deferred", label: "Ask later", description: "Defer setup and require an access checkpoint before search or extraction." },
2708
+ { id: "disabled", label: "Do not use", description: "This project will use metadata/manual notes without scholarly full-text access." }
2709
+ ]);
2710
+ if (readiness !== "configured") {
2711
+ return buildAccessReadinessProfile({ readiness, routes: [] });
2712
+ }
2713
+ const routeSelections = await promptMultiChoice("Select every scholarly access route that is available or intended. Secrets are never stored.", [
2714
+ { id: "metadata", label: "Open metadata", description: "Crossref, OpenAlex, Semantic Scholar, PubMed, ERIC, DOAJ, Unpaywall." },
2715
+ { id: "oa_full_text", label: "OA full text", description: "Use open-access PDF/full-text when legally available." },
2716
+ { id: "institutional", label: "Institutional access", description: "VPN, library proxy, or browser SSO handled by the researcher." },
2717
+ { id: "publisher_tdm", label: "Publisher API/TDM", description: "Use configured publisher API/TDM environment variables." },
2718
+ { id: "manual_pdf", label: "Manual PDFs", description: "Researcher supplies PDFs; LongTable organizes/probes allowed extraction." },
2719
+ { id: "unknown", label: "Unknown", description: "Keep access uncertain and require a checkpoint before full-text work." }
2720
+ ]);
2721
+ const routes = uniqueAccessRoutes(routeSelections.length > 0 ? routeSelections : ["metadata"]);
2722
+ const institutionalAccessMode = routes.includes("institutional")
2723
+ ? await promptChoice("How will institutional access be completed? The researcher handles login/MFA directly.", [
2724
+ { id: "vpn", label: "VPN", description: "Researcher connects through school or institutional VPN." },
2725
+ { id: "library_proxy", label: "Library proxy", description: "Researcher uses proxy links or library resolver." },
2726
+ { id: "browser_sso", label: "Browser SSO", description: "Researcher logs into library/publisher SSO in the browser." },
2727
+ { id: "unknown", label: "Unknown", description: "The route exists but is not yet specified." }
2728
+ ])
2729
+ : undefined;
2730
+ const publisherRecords = await publisherRecordsForAccessSetup(args, routes);
2731
+ return buildAccessReadinessProfile({
2732
+ readiness,
2733
+ routes,
2734
+ institutionalAccessMode,
2735
+ publisherRecords
2736
+ });
2737
+ }
2738
+ async function runAccessSetup(args) {
2739
+ const records = typeof args.doi === "string" && args.doi.trim()
2740
+ ? await probeAllPublishers(args.doi)
2741
+ : summarizeConfiguredPublisherAccess(env);
2742
+ const profile = input.isTTY && output.isTTY && args.json !== true
2743
+ ? await runInteractiveAccessSetup(args)
2744
+ : buildAccessReadinessProfile({
2745
+ readiness: "configured",
2746
+ routes: inferAccessRoutes(records),
2747
+ publisherRecords: records
2748
+ });
2749
+ const profilePath = await saveAccessReadinessProfile(profile);
2513
2750
  if (args.json === true) {
2514
2751
  console.log(JSON.stringify({
2515
- capabilityFile: snapshotPath,
2516
- snapshot: buildSearchCapabilitySnapshot(records, env)
2752
+ readinessFile: profilePath,
2753
+ profile
2517
2754
  }, null, 2));
2518
2755
  return;
2519
2756
  }
2520
- console.log(renderPublisherAccessRecords("LongTable Search Publisher Access Setup", records, snapshotPath));
2757
+ console.log(renderAccessReadinessProfile(profile, profilePath));
2521
2758
  }
2522
- async function runSearch(subcommand, args) {
2523
- if (subcommand === "probe") {
2524
- await runSearchProbe(args);
2759
+ async function runAccessStatus(args) {
2760
+ const profilePath = accessReadinessPath();
2761
+ const profile = readAccessReadinessProfile();
2762
+ if (args.json === true) {
2763
+ console.log(JSON.stringify({
2764
+ readinessFile: profilePath,
2765
+ readinessFileExists: Boolean(profile),
2766
+ readiness: profile
2767
+ }, null, 2));
2525
2768
  return;
2526
2769
  }
2527
- if (subcommand === "doctor" || subcommand === "status") {
2528
- await runSearchDoctor(args);
2770
+ if (!profile) {
2771
+ console.log([
2772
+ "LongTable Scholarly Access Readiness",
2773
+ `- profile: missing (${profilePath})`,
2774
+ "- next: run `longtable access setup` before PDF collection or full-text extraction."
2775
+ ].join("\n"));
2529
2776
  return;
2530
2777
  }
2778
+ console.log(renderAccessReadinessProfile(profile, profilePath));
2779
+ }
2780
+ async function runAccess(subcommand, args) {
2531
2781
  if (subcommand === "setup") {
2532
- await runSearchSetup(args);
2782
+ await runAccessSetup(args);
2783
+ return;
2784
+ }
2785
+ if (subcommand === "status") {
2786
+ await runAccessStatus(args);
2533
2787
  return;
2534
2788
  }
2789
+ if (subcommand === "doctor") {
2790
+ await runAccessDoctor(args);
2791
+ return;
2792
+ }
2793
+ if (subcommand === "probe") {
2794
+ await runAccessProbe(args);
2795
+ return;
2796
+ }
2797
+ if (!subcommand) {
2798
+ await runAccessStatus(args);
2799
+ return;
2800
+ }
2801
+ throw new Error(`Unknown access subcommand: ${subcommand}`);
2802
+ }
2803
+ function movedSearchAccessCommand(subcommand) {
2804
+ return new Error(`\`longtable search ${subcommand}\` has moved. Use \`longtable access ${subcommand}\`.`);
2805
+ }
2806
+ async function runSearch(subcommand, args) {
2807
+ if (subcommand === "setup" || subcommand === "doctor" || subcommand === "status" || subcommand === "probe") {
2808
+ throw movedSearchAccessCommand(subcommand);
2809
+ }
2535
2810
  if (subcommand) {
2536
2811
  throw new Error(`Unknown search subcommand: ${subcommand}`);
2537
2812
  }
@@ -3524,6 +3799,10 @@ async function main() {
3524
3799
  await runMcpSubcommand(subcommand, values);
3525
3800
  return;
3526
3801
  }
3802
+ if (command === "access") {
3803
+ await runAccess(subcommand, values);
3804
+ return;
3805
+ }
3527
3806
  if (command === "search") {
3528
3807
  await runSearch(subcommand, values);
3529
3808
  return;
@@ -131,6 +131,11 @@ function looksLikeResearchCommitmentPrompt(prompt) {
131
131
  /\b(change|revise|update|replace|reframe|modify|alter)\b/i.test(prompt) ||
132
132
  /바꾸|변경|수정|교체|전환|재설정/.test(prompt));
133
133
  }
134
+ function looksLikeAccessSensitiveResearchAction(prompt) {
135
+ const normalized = prompt.trim();
136
+ return /\b(pdf|full[- ]?text|tdm|publisher api|institutional access|library login|vpn|proxy|subscription|paper collection|source collection|corpus|download)\b/i.test(normalized)
137
+ || /PDF|원문|전문|기관\s*구독|기관구독|구독|VPN|프록시|도서관|라이브러리|TDM|논문\s*수집|문헌\s*수집|코퍼스|다운로드/.test(normalized);
138
+ }
134
139
  function looksLikeQuestionGenerationPrompt(prompt) {
135
140
  return /\b(needed questions?|necessary questions?|question generation|clarifying questions?|ask questions?)\b/i.test(prompt)
136
141
  || /필요한\s*질문|질문을\s*(모두|많이|생성)|질문\s*생성|물어봐|질문해/.test(prompt);
@@ -184,11 +189,12 @@ function shouldSurfaceInterviewContext(prompt) {
184
189
  return looksLikeExplicitInterviewPrompt(prompt) || looksLikeResearchStateConfirmationPrompt(prompt);
185
190
  }
186
191
  function shouldCreateRequiredQuestionsForPrompt(prompt) {
187
- return !looksLikeLongTableProductOrToolingPrompt(prompt) && looksLikeResearchCommitmentPrompt(prompt);
192
+ return !looksLikeLongTableProductOrToolingPrompt(prompt) &&
193
+ (looksLikeResearchCommitmentPrompt(prompt) || looksLikeAccessSensitiveResearchAction(prompt));
188
194
  }
189
195
  function shouldApplyProtectedDecisionClosure(runtime, prompt) {
190
196
  return Boolean(runtime.context.session.protectedDecision) &&
191
- shouldCreateRequiredQuestionsForPrompt(prompt) &&
197
+ looksLikeResearchCommitmentPrompt(prompt) &&
192
198
  !looksLikeQuestionGenerationPrompt(prompt) &&
193
199
  !looksLikeMultiCommitmentChangePrompt(prompt);
194
200
  }
@@ -1,4 +1,4 @@
1
- import type { DecisionRecord, InvocationRecord, LongTableQuestionObligation, ProviderKind, QuestionOption, QuestionGenerationResult, QuestionOpportunity, QuestionSurface, QuestionRecord, ResearchState } from "@longtable/core";
1
+ import type { DecisionRecord, InvocationRecord, LongTableQuestionObligation, ProviderKind, QuestionOption, QuestionCommitmentFamily, QuestionEpistemicBasis, QuestionGenerationResult, QuestionOpportunity, QuestionSurface, QuestionRecord, ResearchState } from "@longtable/core";
2
2
  import type { SetupPersistedOutput } from "@longtable/setup";
3
3
  export type ProjectDisagreementPreference = "synthesis_only" | "show_on_conflict" | "always_visible";
4
4
  export type StartInterviewSignal = "phenomenon" | "audience" | "artifact" | "evidence" | "assumption" | "decision_risk" | "voice";
@@ -221,6 +221,8 @@ export interface LongTableWorkspaceInspection {
221
221
  id: string;
222
222
  title: string;
223
223
  question: string;
224
+ commitmentFamily?: QuestionCommitmentFamily;
225
+ epistemicBasis?: QuestionEpistemicBasis;
224
226
  options: string[];
225
227
  required: boolean;
226
228
  }>;
@@ -235,6 +237,8 @@ export interface LongTableWorkspaceInspection {
235
237
  id: string;
236
238
  checkpointKey: string;
237
239
  summary: string;
240
+ commitmentFamily?: QuestionCommitmentFamily;
241
+ epistemicBasis?: QuestionEpistemicBasis;
238
242
  selectedOption?: string;
239
243
  timestamp: string;
240
244
  }>;
@@ -329,6 +333,8 @@ export declare function createWorkspaceQuestion(options: {
329
333
  displayReason?: string;
330
334
  provider?: ProviderKind;
331
335
  required?: boolean;
336
+ commitmentFamily?: QuestionCommitmentFamily;
337
+ epistemicBasis?: QuestionEpistemicBasis;
332
338
  }): Promise<{
333
339
  question: QuestionRecord;
334
340
  state: ResearchState;
@@ -140,6 +140,15 @@ function renderResearchSpecificationSummary(specification, locale) {
140
140
  if (specification.methodAnalysis.analysisOptions.length > 0) {
141
141
  lines.push(`- ${korean ? "분석 옵션" : "Analysis options"}: ${specification.methodAnalysis.analysisOptions.join("; ")}`);
142
142
  }
143
+ if (specification.evidenceAccess.requiredSources?.length) {
144
+ lines.push(`- ${korean ? "필요 근거원" : "Required sources"}: ${specification.evidenceAccess.requiredSources.join("; ")}`);
145
+ }
146
+ if (specification.evidenceAccess.accessRequirements?.length) {
147
+ lines.push(`- ${korean ? "Corpus and Access Plan" : "Corpus and Access Plan"}: ${specification.evidenceAccess.accessRequirements.join("; ")}`);
148
+ }
149
+ if (specification.evidenceAccess.evidenceStandards?.length) {
150
+ lines.push(`- ${korean ? "근거 기준" : "Evidence standards"}: ${specification.evidenceAccess.evidenceStandards.join("; ")}`);
151
+ }
143
152
  if (specification.epistemicAlignment.conflictResolutionRule) {
144
153
  lines.push(`- ${korean ? "충돌 조정 규칙" : "Conflict rule"}: ${specification.epistemicAlignment.conflictResolutionRule}`);
145
154
  }
@@ -154,6 +163,43 @@ function renderResearchSpecificationSummary(specification, locale) {
154
163
  }
155
164
  return lines;
156
165
  }
166
+ function renderResearchSpecificationStatus(session, locale) {
167
+ if (!session.firstResearchShape && !session.researchSpecification) {
168
+ return [];
169
+ }
170
+ const korean = locale === "ko";
171
+ if (!session.researchSpecification) {
172
+ return [
173
+ "",
174
+ korean ? "## Research Specification 상태" : "## Research Specification Status",
175
+ korean
176
+ ? "- 상태: First Research Shape는 있지만 Research Specification은 아직 없습니다."
177
+ : "- Status: First Research Shape exists, but Research Specification is missing.",
178
+ korean
179
+ ? "- 의미: First Research Shape는 짧은 핸들/재개 인덱스이며, 인터뷰 종료나 연구 명세 확정이 아닙니다."
180
+ : "- Meaning: First Research Shape is a short handle/resume index, not interview closure or a confirmed research specification.",
181
+ korean
182
+ ? "- 다음 프로토콜: 충분한 내용이 있으면 `summarize_research_specification`으로 preview를 만들고 `confirm_research_specification`으로 저장/한 질문 더/섹션 수정/열어두기를 확인합니다."
183
+ : "- Next protocol: when enough detail exists, run `summarize_research_specification` to create the preview, then `confirm_research_specification` to confirm, ask one more question, revise a section, or keep it open."
184
+ ];
185
+ }
186
+ const status = session.researchSpecification.confirmedAt
187
+ ? "confirmed"
188
+ : session.researchSpecification.status ?? "draft";
189
+ if (status === "confirmed") {
190
+ return [];
191
+ }
192
+ return [
193
+ "",
194
+ korean ? "## Research Specification 상태" : "## Research Specification Status",
195
+ korean
196
+ ? `- 상태: ${status}. Research Specification은 저장되어 있지만 아직 확정된 종료 지점이 아닙니다.`
197
+ : `- Status: ${status}. Research Specification exists, but it is not a confirmed closure point yet.`,
198
+ korean
199
+ ? "- 다음 프로토콜: 명세를 업데이트한 뒤 `confirm_research_specification`으로 다시 preview 확인을 받아야 합니다."
200
+ : "- Next protocol: update the specification, then return to `confirm_research_specification` for another preview confirmation."
201
+ ];
202
+ }
157
203
  function buildCurrentGuide(project, session, recentInvocations = [], pendingQuestions = [], pendingObligations = []) {
158
204
  const locale = normalizeLocale(session.locale ?? project.locale);
159
205
  const openQuestions = session.openQuestions && session.openQuestions.length > 0
@@ -182,6 +228,7 @@ function buildCurrentGuide(project, session, recentInvocations = [], pendingQues
182
228
  `- 다음 액션: ${nextAction}`,
183
229
  `- 관점: ${session.requestedPerspectives.length > 0 ? session.requestedPerspectives.join(", ") : "auto"}`,
184
230
  `- disagreement: ${session.disagreementPreference}`,
231
+ ...renderResearchSpecificationStatus(session, locale),
185
232
  "",
186
233
  "## 열린 질문",
187
234
  ...openQuestions.map((question) => `- ${question}`),
@@ -201,7 +248,7 @@ function buildCurrentGuide(project, session, recentInvocations = [], pendingQues
201
248
  "## 대기 중인 결정 질문",
202
249
  ...pendingQuestions.map((record) => {
203
250
  const options = formatQuestionOptionValues(record).join("/");
204
- return `- ${record.id}: ${record.prompt.question} (${options})`;
251
+ return `- ${record.id}: ${record.prompt.question}${formatQuestionMetadata(record)} (${options})`;
205
252
  }),
206
253
  "- 답변 기록: `longtable decide --question <id> --answer <value>`"
207
254
  ]
@@ -257,6 +304,7 @@ function buildCurrentGuide(project, session, recentInvocations = [], pendingQues
257
304
  `- Next action: ${nextAction}`,
258
305
  `- Perspectives: ${session.requestedPerspectives.length > 0 ? session.requestedPerspectives.join(", ") : "auto"}`,
259
306
  `- Disagreement: ${session.disagreementPreference}`,
307
+ ...renderResearchSpecificationStatus(session, locale),
260
308
  "",
261
309
  "## Open Questions",
262
310
  ...openQuestions.map((question) => `- ${question}`),
@@ -276,7 +324,7 @@ function buildCurrentGuide(project, session, recentInvocations = [], pendingQues
276
324
  "## Pending Decision Questions",
277
325
  ...pendingQuestions.map((record) => {
278
326
  const options = formatQuestionOptionValues(record).join("/");
279
- return `- ${record.id}: ${record.prompt.question} (${options})`;
327
+ return `- ${record.id}: ${record.prompt.question}${formatQuestionMetadata(record)} (${options})`;
280
328
  }),
281
329
  "- Record an answer: `longtable decide --question <id> --answer <value>`"
282
330
  ]
@@ -366,6 +414,13 @@ function formatQuestionOptionValues(record) {
366
414
  }
367
415
  return values;
368
416
  }
417
+ function formatQuestionMetadata(record) {
418
+ const parts = [
419
+ record.commitmentFamily ? `commitment: ${record.commitmentFamily}` : "",
420
+ record.epistemicBasis ? `basis: ${record.epistemicBasis}` : ""
421
+ ].filter(Boolean);
422
+ return parts.length > 0 ? ` [${parts.join("; ")}]` : "";
423
+ }
369
424
  function summarizeWorkspaceInspection(context, state) {
370
425
  const questions = state.questionLog ?? [];
371
426
  const pendingQuestions = questions.filter((record) => record.status === "pending");
@@ -426,6 +481,8 @@ function summarizeWorkspaceInspection(context, state) {
426
481
  id: record.id,
427
482
  title: record.prompt.title,
428
483
  question: record.prompt.question,
484
+ ...(record.commitmentFamily ? { commitmentFamily: record.commitmentFamily } : {}),
485
+ ...(record.epistemicBasis ? { epistemicBasis: record.epistemicBasis } : {}),
429
486
  options: formatQuestionOptionValues(record),
430
487
  required: record.prompt.required
431
488
  })),
@@ -440,6 +497,8 @@ function summarizeWorkspaceInspection(context, state) {
440
497
  id: record.id,
441
498
  checkpointKey: record.checkpointKey,
442
499
  summary: record.summary,
500
+ ...(record.commitmentFamily ? { commitmentFamily: record.commitmentFamily } : {}),
501
+ ...(record.epistemicBasis ? { epistemicBasis: record.epistemicBasis } : {}),
443
502
  ...(record.selectedOption ? { selectedOption: record.selectedOption } : {}),
444
503
  timestamp: record.timestamp
445
504
  })),
@@ -486,9 +545,12 @@ function buildProjectAgentsMd(project, session) {
486
545
  "- Begin exploratory work with clarifying or tension questions before recommending a direction.",
487
546
  "- For `$longtable-interview`, ask one natural-language question at a time, reflect with `LongTable hears: ...`, record turns when MCP is available, and avoid early reader/reviewer or theory/method/measurement classification.",
488
547
  "- Do not summarize `$longtable-interview` because a fixed number of turns has passed; wait for content-based readiness around research object, focal uncertainty, boundary, evidence/material, protected decision, and next action.",
548
+ "- First Research Shape is a short handle/resume index, not the default closure point.",
489
549
  "- After the First Research Shape, create a Research Specification when the interview has enough detail to preserve scope, construct ontology, theory framing, coding rules, method options, evidence/access requirements, epistemic alignment, protected decisions, open questions, and next actions.",
550
+ "- If a confirmed First Research Shape exists without a Research Specification, continue directly into the next Research Specification question instead of asking shape-level continue/revise/restart questions.",
551
+ "- If the researcher chooses `ask_one_more` or `revise_section` at Research Specification confirmation, answer that gap and return to the Research Specification Preview before ending the interview.",
490
552
  "- Do not let unrelated pending Researcher Checkpoints interrupt `$longtable-interview`; mention them only as separate unresolved checkpoints unless the researcher is confirming, saving, or recording a research decision.",
491
- "- Use structured options only at the final First Research Shape confirmation or at true checkpoint boundaries.",
553
+ "- Use structured options at the final Research Specification confirmation, at explicit short-handle stop points, or at true checkpoint boundaries.",
492
554
  "- If you foreground role perspectives, disclose them with `LongTable consulted: ...`.",
493
555
  "- Keep one accountable synthesis, but do not hide meaningful disagreement.",
494
556
  ...(session.disagreementPreference === "always_visible"
@@ -1219,8 +1281,8 @@ function questionPriority(spec) {
1219
1281
  const requiredWeight = spec.kind === "research_commitment" || spec.required ? 20 : 0;
1220
1282
  return (byKey[spec.key] ?? 0) + confidenceWeight + requiredWeight;
1221
1283
  }
1222
- function followUpQuestionOptions(first, second, third, fourth) {
1223
- return [first, second, third, ...(fourth ? [fourth] : [])];
1284
+ function followUpQuestionOptions(first, second, third, ...rest) {
1285
+ return [first, second, third, ...rest];
1224
1286
  }
1225
1287
  export function buildQuestionOpportunitySpecs(prompt, options = {}) {
1226
1288
  const normalized = prompt.toLowerCase();
@@ -1285,8 +1347,26 @@ export function buildQuestionOpportunitySpecs(prompt, options = {}) {
1285
1347
  /\brandom[- ]?effects\b/i,
1286
1348
  /분석\s*계획|분석\s*방법|메타\s*분석|분석\s*(?:모형|모델)|통계\s*(?:모형|모델)|구조\s*방정식|경로\s*모형|조절효과|랜덤\s*효과/
1287
1349
  ]);
1350
+ const accessCue = includesAny(normalized, [
1351
+ /\b(pdf|full[- ]?text|tdm|publisher api|institutional access|library login|vpn|proxy|subscription|paper collection|source collection|corpus|download)\b/i,
1352
+ /PDF|원문|전문|기관\s*구독|기관구독|구독|VPN|프록시|도서관|라이브러리|TDM|논문\s*수집|문헌\s*수집|코퍼스|다운로드/
1353
+ ]);
1288
1354
  const decisionFamilyCount = [scopeCue, theoryCue, measurementCodingCue, methodCue, analysisCue]
1289
1355
  .filter(Boolean).length;
1356
+ if (accessCue) {
1357
+ push({
1358
+ key: "scholarly_access_policy",
1359
+ kind: "evidence_risk",
1360
+ title: "Scholarly access policy",
1361
+ question: "What scholarly access route should LongTable use before collecting PDFs, full text, or subscription-only evidence?",
1362
+ whyNow: "Full-text access decisions can change the corpus, inclusion bias, reproducibility, and TDM permission boundary.",
1363
+ options: followUpQuestionOptions({ value: "oa_only", label: "OA-only", description: "Use only open-access PDF or full text.", recommended: true }, { value: "institutional_access", label: "Institutional access", description: "Include VPN/proxy/library-login access after the researcher completes login." }, { value: "publisher_tdm", label: "Publisher API/TDM", description: "Use configured publisher API/TDM credentials and record entitlement checks." }, { value: "manual_pdf", label: "Manual PDFs", description: "Use PDFs supplied by the researcher and record provenance." }, { value: "metadata_only", label: "Metadata only", description: "Do not collect full text yet." }),
1364
+ confidence: "high",
1365
+ autoEligible: true,
1366
+ required: true,
1367
+ cues: ["scholarly_access", "full_text", "corpus"]
1368
+ });
1369
+ }
1290
1370
  if (decisionActionCue && decisionFamilyCount >= 2) {
1291
1371
  push({
1292
1372
  key: "research_direction_change_commitment",
@@ -1619,7 +1699,7 @@ export function buildQuestionOpportunitySpecs(prompt, options = {}) {
1619
1699
  }
1620
1700
  let selected = options.autoOnly === true ? specs.filter((spec) => spec.autoEligible) : specs;
1621
1701
  if (options.requiredOnly === true) {
1622
- selected = selected.filter((spec) => spec.kind === "research_commitment");
1702
+ selected = selected.filter((spec) => spec.kind === "research_commitment" || spec.required);
1623
1703
  }
1624
1704
  if (normalized.includes("protected decision closure pressure")) {
1625
1705
  selected = selected.filter((spec) => spec.key === "protected_decision_closure");
@@ -1641,6 +1721,70 @@ export function generateQuestionOpportunities(prompt, options = {}) {
1641
1721
  };
1642
1722
  }
1643
1723
  const FOLLOW_UP_PROMPT_PREFIX = "Follow-up prompt:";
1724
+ function compactMetadataText(parts) {
1725
+ return parts
1726
+ .flatMap((part) => Array.isArray(part) ? part : [part])
1727
+ .filter((part) => Boolean(part && part.trim()))
1728
+ .join(" ")
1729
+ .replace(/\s+/g, " ")
1730
+ .trim()
1731
+ .toLowerCase();
1732
+ }
1733
+ function textMatchesAny(text, patterns) {
1734
+ return patterns.some((pattern) => pattern.test(text));
1735
+ }
1736
+ const COMMITMENT_FAMILY_BY_CHECKPOINT = [
1737
+ [/product|meta_decision/, "product_policy"],
1738
+ [/research_question|research_direction|scope|boundary|inclusion|exclusion/, "scope"],
1739
+ [/theory|construct|conceptual/, "construct"],
1740
+ [/measurement|coding|codebook|extraction/, "coding"],
1741
+ [/method|analysis|panel_disagreement|team_debate|review/, "method"],
1742
+ [/evidence|scholarly_access|source_authority/, "evidence"],
1743
+ [/knowledge_gap|tacit_assumption|epistemic/, "epistemic_authority"]
1744
+ ];
1745
+ function inferCommitmentFamily(input) {
1746
+ const checkpointKey = (input.checkpointKey ?? "").toLowerCase();
1747
+ const matched = COMMITMENT_FAMILY_BY_CHECKPOINT.find(([pattern]) => pattern.test(checkpointKey));
1748
+ if (matched)
1749
+ return matched[1];
1750
+ if (input.triggerFamily === "meta_decision")
1751
+ return "product_policy";
1752
+ if (input.triggerFamily === "evidence")
1753
+ return "evidence";
1754
+ const text = compactMetadataText([input.title, input.question, input.prompt, input.rationale]);
1755
+ if (textMatchesAny(text, [/checkpoint policy/, /hook ux/, /product language/, /\breadme\b/, /제품 언어|체크포인트 정책|훅|리드미/])) {
1756
+ return "product_policy";
1757
+ }
1758
+ return undefined;
1759
+ }
1760
+ function inferEpistemicBasis(input) {
1761
+ const text = compactMetadataText([input.title, input.question, input.prompt, input.rationale]);
1762
+ const bases = [];
1763
+ if (textMatchesAny(text, [/\bresearcher\b/, /\bhuman\b/, /\byour judgment\b/, /\byour knowledge\b/, /연구자|인간|사람|너의\s*판단|당신의\s*판단|내\s*지식|사용자/])) {
1764
+ bases.push("researcher_knowledge");
1765
+ }
1766
+ if (textMatchesAny(text, [/\bproject state\b/, /\bworkspace\b/, /\bcurrent\.md\b/, /\.longtable\b/, /\bstate\.json\b/, /\bdataset\b/, /\bcodebook\b/, /\bcoding sheet\b/, /프로젝트\s*상태|워크스페이스|데이터셋|코드북|코딩\s*시트/])) {
1767
+ bases.push("project_state");
1768
+ }
1769
+ if (textMatchesAny(text, [/\bexternal evidence\b/, /\bliterature\b/, /\bpaper\b/, /\bpdf\b/, /\bsource\b/, /\bcitation\b/, /\breference\b/, /\bfull[- ]?text\b/, /외부\s*근거|문헌|논문|원문|전문|출처|인용|레퍼런스/])) {
1770
+ bases.push("external_evidence");
1771
+ }
1772
+ if (textMatchesAny(text, [/\bcodex\b/, /\bllm\b/, /\blanguage model\b/, /\bmodel judgment\b/, /\bai inference\b/, /\bassistant judgment\b/, /코덱스|언어\s*모델|모델\s*판단|AI\s*추론|LLM/])) {
1773
+ bases.push("ai_inference");
1774
+ }
1775
+ const unique = [...new Set(bases)];
1776
+ if (unique.length > 1)
1777
+ return "mixed";
1778
+ return unique[0];
1779
+ }
1780
+ function resolveQuestionRecordMetadata(input) {
1781
+ const commitmentFamily = input.commitmentFamily ?? inferCommitmentFamily(input);
1782
+ const epistemicBasis = input.epistemicBasis ?? inferEpistemicBasis(input);
1783
+ return {
1784
+ ...(commitmentFamily ? { commitmentFamily } : {}),
1785
+ ...(epistemicBasis ? { epistemicBasis } : {})
1786
+ };
1787
+ }
1644
1788
  function hasFollowUpPrompt(record, prompt) {
1645
1789
  return record.prompt.rationale.includes(`${FOLLOW_UP_PROMPT_PREFIX} ${prompt}`);
1646
1790
  }
@@ -1678,31 +1822,43 @@ export async function createWorkspaceFollowUpQuestions(options) {
1678
1822
  if (specsToCreate.length === 0) {
1679
1823
  return { questions: pendingMatches, state, created: false, alreadyAnswered: false };
1680
1824
  }
1681
- const questions = specsToCreate.map((spec) => ({
1682
- id: createId("question_record"),
1683
- createdAt,
1684
- updatedAt: createdAt,
1685
- status: "pending",
1686
- prompt: {
1687
- id: createId("question_prompt"),
1688
- checkpointKey: `follow_up_${spec.key}`,
1825
+ const questions = specsToCreate.map((spec) => {
1826
+ const checkpointKey = `follow_up_${spec.key}`;
1827
+ const rationale = [
1828
+ spec.whyNow,
1829
+ `Question kind: ${spec.kind}`,
1830
+ `Question confidence: ${spec.confidence}`,
1831
+ `${FOLLOW_UP_PROMPT_PREFIX} ${options.prompt}`
1832
+ ];
1833
+ const metadata = resolveQuestionRecordMetadata({
1834
+ checkpointKey,
1689
1835
  title: spec.title,
1690
1836
  question: spec.question,
1691
- type: "single_choice",
1692
- options: spec.options,
1693
- allowOther: true,
1694
- otherLabel: "Other",
1695
- required: options.required ?? spec.required,
1696
- source: "runtime_guidance",
1697
- rationale: [
1698
- spec.whyNow,
1699
- `Question kind: ${spec.kind}`,
1700
- `Question confidence: ${spec.confidence}`,
1701
- `${FOLLOW_UP_PROMPT_PREFIX} ${options.prompt}`
1702
- ],
1703
- preferredSurfaces: preferredSurfaces
1704
- }
1705
- }));
1837
+ prompt: options.prompt,
1838
+ rationale
1839
+ });
1840
+ return {
1841
+ id: createId("question_record"),
1842
+ createdAt,
1843
+ updatedAt: createdAt,
1844
+ status: "pending",
1845
+ ...metadata,
1846
+ prompt: {
1847
+ id: createId("question_prompt"),
1848
+ checkpointKey,
1849
+ title: spec.title,
1850
+ question: spec.question,
1851
+ type: "single_choice",
1852
+ options: spec.options,
1853
+ allowOther: true,
1854
+ otherLabel: "Other",
1855
+ required: options.required ?? spec.required,
1856
+ source: "runtime_guidance",
1857
+ rationale,
1858
+ preferredSurfaces: preferredSurfaces
1859
+ }
1860
+ };
1861
+ });
1706
1862
  const updated = appendQuestionRecords(state, questions);
1707
1863
  await writeFile(options.context.stateFilePath, JSON.stringify(updated, null, 2), "utf8");
1708
1864
  await syncCurrentWorkspaceView(options.context);
@@ -1716,16 +1872,35 @@ export async function createWorkspaceQuestion(options) {
1716
1872
  });
1717
1873
  const checkpointKey = options.checkpointKey ?? trigger.signal.checkpointKey;
1718
1874
  const createdAt = nowIso();
1875
+ const title = options.title ?? questionTitleForCheckpoint(trigger.family, checkpointKey);
1876
+ const questionText = options.question ?? questionTextForCheckpoint(trigger.family, options.prompt, checkpointKey);
1877
+ const rationale = [
1878
+ ...trigger.rationale,
1879
+ `Trigger family: ${trigger.family}.`,
1880
+ `Trigger confidence: ${trigger.confidence}.`,
1881
+ `Original prompt: ${options.prompt}`
1882
+ ];
1883
+ const metadata = resolveQuestionRecordMetadata({
1884
+ checkpointKey,
1885
+ triggerFamily: trigger.family,
1886
+ title,
1887
+ question: questionText,
1888
+ prompt: options.prompt,
1889
+ rationale,
1890
+ commitmentFamily: options.commitmentFamily,
1891
+ epistemicBasis: options.epistemicBasis
1892
+ });
1719
1893
  const question = {
1720
1894
  id: createId("question_record"),
1721
1895
  createdAt,
1722
1896
  updatedAt: createdAt,
1723
1897
  status: "pending",
1898
+ ...metadata,
1724
1899
  prompt: {
1725
1900
  id: createId("question_prompt"),
1726
1901
  checkpointKey,
1727
- title: options.title ?? questionTitleForCheckpoint(trigger.family, checkpointKey),
1728
- question: options.question ?? questionTextForCheckpoint(trigger.family, options.prompt, checkpointKey),
1902
+ title,
1903
+ question: questionText,
1729
1904
  type: "single_choice",
1730
1905
  options: options.questionOptions ?? optionsForCheckpointTrigger(trigger.family, checkpointKey),
1731
1906
  allowOther: true,
@@ -1733,12 +1908,7 @@ export async function createWorkspaceQuestion(options) {
1733
1908
  required: options.required ?? trigger.requiresQuestionBeforeClosure,
1734
1909
  source: "checkpoint",
1735
1910
  displayReason: options.displayReason ?? trigger.rationale[0],
1736
- rationale: [
1737
- ...trigger.rationale,
1738
- `Trigger family: ${trigger.family}.`,
1739
- `Trigger confidence: ${trigger.confidence}.`,
1740
- `Original prompt: ${options.prompt}`
1741
- ],
1911
+ rationale,
1742
1912
  preferredSurfaces: options.provider === "claude"
1743
1913
  ? ["native_structured", "numbered"]
1744
1914
  : ["mcp_elicitation", "numbered"]
@@ -1865,6 +2035,8 @@ export async function answerWorkspaceQuestion(options) {
1865
2035
  level: question.prompt.required ? "adaptive_required" : "recommended",
1866
2036
  mode: "commit",
1867
2037
  summary: `Answered ${question.prompt.title}: ${answer.selectedLabels.join(", ")}`,
2038
+ ...(question.commitmentFamily ? { commitmentFamily: question.commitmentFamily } : {}),
2039
+ ...(question.epistemicBasis ? { epistemicBasis: question.epistemicBasis } : {}),
1868
2040
  selectedOption: answer.selectedValues[0],
1869
2041
  ...(rationale ? { rationale } : {})
1870
2042
  };
@@ -1,4 +1,4 @@
1
- import { type CrossrefTdmDiscovery, type EvidenceCard, type Publisher, type PublisherAccessRecord, type PublisherProbeInput, type PublisherProbeTarget, type SearchCapabilitySnapshot, type SearchFetch } from "./types.js";
1
+ import { type CrossrefTdmDiscovery, type EvidenceCard, type Publisher, type PublisherAccessRecord, type PublisherProbeInput, type PublisherProbeTarget, type SearchFetch } from "./types.js";
2
2
  interface PublisherConfig {
3
3
  publisher: Publisher;
4
4
  label: string;
@@ -12,8 +12,6 @@ export declare function discoverCrossrefTdm(doi: string, env?: Record<string, st
12
12
  export declare function publisherConfigs(): PublisherConfig[];
13
13
  export declare function probePublisherAccess(input: PublisherProbeInput): Promise<PublisherAccessRecord>;
14
14
  export declare function summarizeConfiguredPublisherAccess(env?: Record<string, string | undefined>): PublisherAccessRecord[];
15
- export declare function buildSearchCapabilitySnapshot(records: PublisherAccessRecord[], env?: Record<string, string | undefined>): SearchCapabilitySnapshot;
16
- export declare function searchCapabilitySnapshotPath(home?: string): string;
17
15
  export declare function enrichCardsWithPublisherAccess(input: {
18
16
  cards: EvidenceCard[];
19
17
  env?: Record<string, string | undefined>;
@@ -1,5 +1,3 @@
1
- import { join } from "node:path";
2
- import { homedir } from "node:os";
3
1
  import { PUBLISHERS } from "./types.js";
4
2
  const PUBLISHER_CONFIGS = {
5
3
  elsevier: {
@@ -505,17 +503,6 @@ export function summarizeConfiguredPublisherAccess(env = process.env) {
505
503
  });
506
504
  });
507
505
  }
508
- export function buildSearchCapabilitySnapshot(records, env = process.env) {
509
- return {
510
- version: 1,
511
- updatedAt: now(),
512
- contactEmailPresent: Boolean(env.LONGTABLE_CONTACT_EMAIL?.trim()),
513
- records
514
- };
515
- }
516
- export function searchCapabilitySnapshotPath(home = homedir()) {
517
- return join(home, ".longtable", "search-capabilities.json");
518
- }
519
506
  function bestAccessStatus(record) {
520
507
  if (record.entitlementStatus === "licensed_full_text_available" && record.collectionDepth === "licensed_snippet") {
521
508
  return "licensed_full_text_checked";
@@ -173,12 +173,6 @@ export interface PublisherAccessRecord {
173
173
  evidenceSnippet?: string;
174
174
  crossref?: CrossrefTdmDiscovery;
175
175
  }
176
- export interface SearchCapabilitySnapshot {
177
- version: 1;
178
- updatedAt: string;
179
- contactEmailPresent: boolean;
180
- records: PublisherAccessRecord[];
181
- }
182
176
  export interface PublisherProbeInput {
183
177
  doi: string;
184
178
  publisher?: PublisherProbeTarget;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@longtable/cli",
3
- "version": "0.1.44",
3
+ "version": "0.1.47",
4
4
  "private": false,
5
5
  "description": "Researcher-facing LongTable CLI",
6
6
  "type": "module",
@@ -29,12 +29,12 @@
29
29
  },
30
30
  "dependencies": {
31
31
  "@clack/prompts": "^1.2.0",
32
- "@longtable/checkpoints": "0.1.44",
33
- "@longtable/core": "0.1.44",
34
- "@longtable/memory": "0.1.44",
35
- "@longtable/provider-claude": "0.1.44",
36
- "@longtable/provider-codex": "0.1.44",
37
- "@longtable/setup": "0.1.44"
32
+ "@longtable/checkpoints": "0.1.47",
33
+ "@longtable/core": "0.1.47",
34
+ "@longtable/memory": "0.1.47",
35
+ "@longtable/provider-claude": "0.1.47",
36
+ "@longtable/provider-codex": "0.1.47",
37
+ "@longtable/setup": "0.1.47"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@types/node": "^22.10.1",