@longtable/cli 0.1.45 → 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/dist/cli.js CHANGED
@@ -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) {
@@ -1131,7 +1158,7 @@ function resolveMcpProviders(value) {
1131
1158
  function resolveMcpPackageSpec(args) {
1132
1159
  return typeof args.package === "string" && args.package.trim()
1133
1160
  ? args.package.trim()
1134
- : `@longtable/mcp@${LONGTABLE_MCP_PACKAGE_VERSION}`;
1161
+ : LONGTABLE_MCP_PACKAGE_SPEC;
1135
1162
  }
1136
1163
  function resolveCodexMcpConfigPath(args) {
1137
1164
  return resolve(normalizeUserPath(typeof args["codex-config"] === "string" && args["codex-config"].trim()
@@ -1162,6 +1189,12 @@ function renderCodexMcpBlock(serverName, command, mcpArgs) {
1162
1189
  `[mcp_servers.${serverName}]`,
1163
1190
  `command = ${escapeTomlString(command)}`,
1164
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
+ ]),
1165
1198
  LONGTABLE_MCP_MARKER_END
1166
1199
  ].join("\n");
1167
1200
  }
@@ -1185,10 +1218,55 @@ function codexMcpElicitationsAllowed(config) {
1185
1218
  function codexLongTableMcpConfigured(config) {
1186
1219
  return new RegExp(`\\[mcp_servers\\.${LONGTABLE_MCP_SERVER_NAME.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\]`).test(config);
1187
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
+ }
1188
1261
  function replaceMarkedCodexMcpBlock(existing, block, serverName) {
1189
- const markerPattern = new RegExp(`${LONGTABLE_MCP_MARKER_START.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s\\S]*?${LONGTABLE_MCP_MARKER_END.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\n?`, "m");
1190
- const serverPattern = new RegExp(`\\n?\\[mcp_servers\\.${serverName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\][\\s\\S]*?(?=\\n\\[|$)`, "m");
1191
- 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();
1192
1270
  return trimmed ? `${trimmed}\n\n${block}\n` : `${block}\n`;
1193
1271
  }
1194
1272
  async function writeCodexMcpConfig(path, block, serverName, options = {}) {
@@ -1467,6 +1545,8 @@ async function collectDoctorStatus(args) {
1467
1545
  const codexMcpConfig = existsSync(codexMcpConfigPath)
1468
1546
  ? await readFile(codexMcpConfigPath, "utf8")
1469
1547
  : "";
1548
+ const codexMcpPackageSpec = codexLongTableMcpPackageSpec(codexMcpConfig);
1549
+ const missingMcpTools = missingCodexLongTableMcpTools(codexMcpConfig);
1470
1550
  const codexHooksPath = resolveCodexHooksPath(args);
1471
1551
  const codexHooksContent = existsSync(codexHooksPath)
1472
1552
  ? await readFile(codexHooksPath, "utf8")
@@ -1502,6 +1582,10 @@ async function collectDoctorStatus(args) {
1502
1582
  mcpConfigPath: codexMcpConfigPath,
1503
1583
  mcpConfigExists: existsSync(codexMcpConfigPath),
1504
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)),
1505
1589
  mcpElicitationsAllowed: codexMcpElicitationsAllowed(codexMcpConfig),
1506
1590
  hooksPath: codexHooksPath,
1507
1591
  hooksExists: existsSync(codexHooksPath),
@@ -1547,6 +1631,11 @@ function renderDoctorStatus(status) {
1547
1631
  : []),
1548
1632
  `- MCP config: ${status.providers.codex.mcpConfigExists ? "present" : "missing"} (${status.providers.codex.mcpConfigPath})`,
1549
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"]),
1550
1639
  `- MCP elicitation approval: ${status.providers.codex.mcpElicitationsAllowed ? "allowed" : "not allowed"}`,
1551
1640
  `- Codex hooks file: ${status.providers.codex.hooksExists ? "present" : "missing"} (${status.providers.codex.hooksPath})`,
1552
1641
  `- codex_hooks feature: ${status.providers.codex.codexHooksEnabled ? "enabled" : "missing"}`,
@@ -1595,6 +1684,9 @@ function renderDoctorStatus(status) {
1595
1684
  const canFix = status.providers.codex.missingSkills.length > 0 ||
1596
1685
  status.providers.claude.missingSkills.length > 0 ||
1597
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 ||
1598
1690
  !status.providers.codex.codexHooksEnabled ||
1599
1691
  status.providers.codex.missingManagedHookEvents.length > 0 ||
1600
1692
  (status.setupExists &&
@@ -1605,6 +1697,11 @@ function renderDoctorStatus(status) {
1605
1697
  if (!status.providers.codex.codexHooksEnabled || status.providers.codex.missingManagedHookEvents.length > 0) {
1606
1698
  nextActions.push("longtable codex install-hooks");
1607
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
+ }
1608
1705
  if (!status.setupExists) {
1609
1706
  nextActions.push("longtable setup --provider codex");
1610
1707
  }
@@ -1644,7 +1741,7 @@ function renderRepairSummary(repair) {
1644
1741
  }
1645
1742
  }
1646
1743
  if (repair.writtenRuntimeConfigs.length > 0) {
1647
- lines.push("- wrote runtime configs:");
1744
+ lines.push("- wrote configs:");
1648
1745
  for (const target of repair.writtenRuntimeConfigs) {
1649
1746
  lines.push(` - ${target.provider}: ${target.path}`);
1650
1747
  }
@@ -1700,6 +1797,21 @@ async function repairDoctorStatus(args, status) {
1700
1797
  writtenRuntimeConfigs: [],
1701
1798
  skipped: []
1702
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
+ }
1703
1815
  if (status.providers.codex.missingSkills.length > 0) {
1704
1816
  repair.installedCodexSkills = (await installCodexSkills(roles, codexDir, skillSurface)).map((skill) => skill.name);
1705
1817
  }
@@ -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;
@@ -163,6 +163,43 @@ function renderResearchSpecificationSummary(specification, locale) {
163
163
  }
164
164
  return lines;
165
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
+ }
166
203
  function buildCurrentGuide(project, session, recentInvocations = [], pendingQuestions = [], pendingObligations = []) {
167
204
  const locale = normalizeLocale(session.locale ?? project.locale);
168
205
  const openQuestions = session.openQuestions && session.openQuestions.length > 0
@@ -191,6 +228,7 @@ function buildCurrentGuide(project, session, recentInvocations = [], pendingQues
191
228
  `- 다음 액션: ${nextAction}`,
192
229
  `- 관점: ${session.requestedPerspectives.length > 0 ? session.requestedPerspectives.join(", ") : "auto"}`,
193
230
  `- disagreement: ${session.disagreementPreference}`,
231
+ ...renderResearchSpecificationStatus(session, locale),
194
232
  "",
195
233
  "## 열린 질문",
196
234
  ...openQuestions.map((question) => `- ${question}`),
@@ -210,7 +248,7 @@ function buildCurrentGuide(project, session, recentInvocations = [], pendingQues
210
248
  "## 대기 중인 결정 질문",
211
249
  ...pendingQuestions.map((record) => {
212
250
  const options = formatQuestionOptionValues(record).join("/");
213
- return `- ${record.id}: ${record.prompt.question} (${options})`;
251
+ return `- ${record.id}: ${record.prompt.question}${formatQuestionMetadata(record)} (${options})`;
214
252
  }),
215
253
  "- 답변 기록: `longtable decide --question <id> --answer <value>`"
216
254
  ]
@@ -266,6 +304,7 @@ function buildCurrentGuide(project, session, recentInvocations = [], pendingQues
266
304
  `- Next action: ${nextAction}`,
267
305
  `- Perspectives: ${session.requestedPerspectives.length > 0 ? session.requestedPerspectives.join(", ") : "auto"}`,
268
306
  `- Disagreement: ${session.disagreementPreference}`,
307
+ ...renderResearchSpecificationStatus(session, locale),
269
308
  "",
270
309
  "## Open Questions",
271
310
  ...openQuestions.map((question) => `- ${question}`),
@@ -285,7 +324,7 @@ function buildCurrentGuide(project, session, recentInvocations = [], pendingQues
285
324
  "## Pending Decision Questions",
286
325
  ...pendingQuestions.map((record) => {
287
326
  const options = formatQuestionOptionValues(record).join("/");
288
- return `- ${record.id}: ${record.prompt.question} (${options})`;
327
+ return `- ${record.id}: ${record.prompt.question}${formatQuestionMetadata(record)} (${options})`;
289
328
  }),
290
329
  "- Record an answer: `longtable decide --question <id> --answer <value>`"
291
330
  ]
@@ -375,6 +414,13 @@ function formatQuestionOptionValues(record) {
375
414
  }
376
415
  return values;
377
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
+ }
378
424
  function summarizeWorkspaceInspection(context, state) {
379
425
  const questions = state.questionLog ?? [];
380
426
  const pendingQuestions = questions.filter((record) => record.status === "pending");
@@ -435,6 +481,8 @@ function summarizeWorkspaceInspection(context, state) {
435
481
  id: record.id,
436
482
  title: record.prompt.title,
437
483
  question: record.prompt.question,
484
+ ...(record.commitmentFamily ? { commitmentFamily: record.commitmentFamily } : {}),
485
+ ...(record.epistemicBasis ? { epistemicBasis: record.epistemicBasis } : {}),
438
486
  options: formatQuestionOptionValues(record),
439
487
  required: record.prompt.required
440
488
  })),
@@ -449,6 +497,8 @@ function summarizeWorkspaceInspection(context, state) {
449
497
  id: record.id,
450
498
  checkpointKey: record.checkpointKey,
451
499
  summary: record.summary,
500
+ ...(record.commitmentFamily ? { commitmentFamily: record.commitmentFamily } : {}),
501
+ ...(record.epistemicBasis ? { epistemicBasis: record.epistemicBasis } : {}),
452
502
  ...(record.selectedOption ? { selectedOption: record.selectedOption } : {}),
453
503
  timestamp: record.timestamp
454
504
  })),
@@ -495,9 +545,12 @@ function buildProjectAgentsMd(project, session) {
495
545
  "- Begin exploratory work with clarifying or tension questions before recommending a direction.",
496
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.",
497
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.",
498
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.",
499
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.",
500
- "- 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.",
501
554
  "- If you foreground role perspectives, disclose them with `LongTable consulted: ...`.",
502
555
  "- Keep one accountable synthesis, but do not hide meaningful disagreement.",
503
556
  ...(session.disagreementPreference === "always_visible"
@@ -1668,6 +1721,70 @@ export function generateQuestionOpportunities(prompt, options = {}) {
1668
1721
  };
1669
1722
  }
1670
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
+ }
1671
1788
  function hasFollowUpPrompt(record, prompt) {
1672
1789
  return record.prompt.rationale.includes(`${FOLLOW_UP_PROMPT_PREFIX} ${prompt}`);
1673
1790
  }
@@ -1705,31 +1822,43 @@ export async function createWorkspaceFollowUpQuestions(options) {
1705
1822
  if (specsToCreate.length === 0) {
1706
1823
  return { questions: pendingMatches, state, created: false, alreadyAnswered: false };
1707
1824
  }
1708
- const questions = specsToCreate.map((spec) => ({
1709
- id: createId("question_record"),
1710
- createdAt,
1711
- updatedAt: createdAt,
1712
- status: "pending",
1713
- prompt: {
1714
- id: createId("question_prompt"),
1715
- 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,
1716
1835
  title: spec.title,
1717
1836
  question: spec.question,
1718
- type: "single_choice",
1719
- options: spec.options,
1720
- allowOther: true,
1721
- otherLabel: "Other",
1722
- required: options.required ?? spec.required,
1723
- source: "runtime_guidance",
1724
- rationale: [
1725
- spec.whyNow,
1726
- `Question kind: ${spec.kind}`,
1727
- `Question confidence: ${spec.confidence}`,
1728
- `${FOLLOW_UP_PROMPT_PREFIX} ${options.prompt}`
1729
- ],
1730
- preferredSurfaces: preferredSurfaces
1731
- }
1732
- }));
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
+ });
1733
1862
  const updated = appendQuestionRecords(state, questions);
1734
1863
  await writeFile(options.context.stateFilePath, JSON.stringify(updated, null, 2), "utf8");
1735
1864
  await syncCurrentWorkspaceView(options.context);
@@ -1743,16 +1872,35 @@ export async function createWorkspaceQuestion(options) {
1743
1872
  });
1744
1873
  const checkpointKey = options.checkpointKey ?? trigger.signal.checkpointKey;
1745
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
+ });
1746
1893
  const question = {
1747
1894
  id: createId("question_record"),
1748
1895
  createdAt,
1749
1896
  updatedAt: createdAt,
1750
1897
  status: "pending",
1898
+ ...metadata,
1751
1899
  prompt: {
1752
1900
  id: createId("question_prompt"),
1753
1901
  checkpointKey,
1754
- title: options.title ?? questionTitleForCheckpoint(trigger.family, checkpointKey),
1755
- question: options.question ?? questionTextForCheckpoint(trigger.family, options.prompt, checkpointKey),
1902
+ title,
1903
+ question: questionText,
1756
1904
  type: "single_choice",
1757
1905
  options: options.questionOptions ?? optionsForCheckpointTrigger(trigger.family, checkpointKey),
1758
1906
  allowOther: true,
@@ -1760,12 +1908,7 @@ export async function createWorkspaceQuestion(options) {
1760
1908
  required: options.required ?? trigger.requiresQuestionBeforeClosure,
1761
1909
  source: "checkpoint",
1762
1910
  displayReason: options.displayReason ?? trigger.rationale[0],
1763
- rationale: [
1764
- ...trigger.rationale,
1765
- `Trigger family: ${trigger.family}.`,
1766
- `Trigger confidence: ${trigger.confidence}.`,
1767
- `Original prompt: ${options.prompt}`
1768
- ],
1911
+ rationale,
1769
1912
  preferredSurfaces: options.provider === "claude"
1770
1913
  ? ["native_structured", "numbered"]
1771
1914
  : ["mcp_elicitation", "numbered"]
@@ -1892,6 +2035,8 @@ export async function answerWorkspaceQuestion(options) {
1892
2035
  level: question.prompt.required ? "adaptive_required" : "recommended",
1893
2036
  mode: "commit",
1894
2037
  summary: `Answered ${question.prompt.title}: ${answer.selectedLabels.join(", ")}`,
2038
+ ...(question.commitmentFamily ? { commitmentFamily: question.commitmentFamily } : {}),
2039
+ ...(question.epistemicBasis ? { epistemicBasis: question.epistemicBasis } : {}),
1895
2040
  selectedOption: answer.selectedValues[0],
1896
2041
  ...(rationale ? { rationale } : {})
1897
2042
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@longtable/cli",
3
- "version": "0.1.45",
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.45",
33
- "@longtable/core": "0.1.45",
34
- "@longtable/memory": "0.1.45",
35
- "@longtable/provider-claude": "0.1.45",
36
- "@longtable/provider-codex": "0.1.45",
37
- "@longtable/setup": "0.1.45"
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",