@longtable/cli 0.1.11 → 0.1.13

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
@@ -14,7 +14,7 @@ import { installCodexPromptAliases, listInstalledCodexPromptAliases, removeCodex
14
14
  import { buildPersonaGuidance, parseInvocationDirective } from "./persona-router.js";
15
15
  import { PERSONA_DEFINITIONS, listRoleDefinitions } from "./personas.js";
16
16
  import { buildPanelFallback, renderPanelSummary } from "./panel.js";
17
- import { appendInvocationRecordToWorkspace, assertWorkspaceNotBlocked, answerWorkspaceQuestion, createWorkspaceQuestion, createOrUpdateProjectWorkspace, inspectProjectWorkspace, loadProjectContextFromDirectory, renderProjectWorkspaceSummary, syncCurrentWorkspaceView } from "./project-session.js";
17
+ import { appendInvocationRecordToWorkspace, assertWorkspaceNotBlocked, answerWorkspaceQuestion, createWorkspaceClarificationCard, createWorkspaceQuestion, createOrUpdateProjectWorkspace, inspectProjectWorkspace, loadProjectContextFromDirectory, renderProjectWorkspaceSummary, syncCurrentWorkspaceView } from "./project-session.js";
18
18
  const VALID_MODES = new Set([
19
19
  "explore",
20
20
  "review",
@@ -86,6 +86,7 @@ function usage() {
86
86
  " longtable install [--json] [--path <file>] [--runtime-path <file>]",
87
87
  " longtable mcp install [--provider codex|claude|all] [--write] [--json] [--codex-config <path>] [--claude-settings <path>] [--package <spec>]",
88
88
  " longtable ask [--prompt <text>] [--print] [--json] [--setup <path>] [--cwd <path>]",
89
+ " longtable clarify --prompt <task-context> [--provider codex|claude] [--required|--advisory] [--print] [--cwd <path>] [--json] [--force]",
89
90
  " longtable question --prompt <decision-context> [--title <text>] [--text <question>] [--provider codex|claude] [--required|--advisory] [--print] [--cwd <path>] [--json]",
90
91
  " longtable panel [--prompt <text>] [--role <role[,role]>] [--mode review|critique|draft|commit] [--visibility synthesis_only|show_on_conflict|always_visible] [--print] [--json] [--setup <path>] [--cwd <path>]",
91
92
  " longtable decide [--question <id>] --answer <value-or-text> [--rationale <text>] [--provider codex|claude] [--cwd <path>] [--json]",
@@ -119,7 +120,7 @@ function parseArgs(argv) {
119
120
  const values = {};
120
121
  let subcommand = maybeSubcommand;
121
122
  const modeCommand = command && VALID_MODES.has(command);
122
- const directCommand = command && ["init", "start", "resume", "doctor", "status", "roles", "show", "install", "mcp", "codex", "claude", "ask", "question", "panel", "decide"].includes(command);
123
+ const directCommand = command && ["init", "start", "resume", "doctor", "status", "roles", "show", "install", "mcp", "codex", "claude", "ask", "clarify", "question", "panel", "decide"].includes(command);
123
124
  let startIndex = 1;
124
125
  if (modeCommand) {
125
126
  subcommand = undefined;
@@ -1074,6 +1075,15 @@ function renderDoctorStatus(status) {
1074
1075
  lines.push(` - ${question.id}: ${question.question} (${question.options.join("/")})`);
1075
1076
  }
1076
1077
  }
1078
+ if ((workspace.answerWarnings ?? []).length > 0) {
1079
+ lines.push("- answer warnings:");
1080
+ for (const warning of workspace.answerWarnings ?? []) {
1081
+ lines.push(` - ${warning.questionId}: ${warning.issue}`);
1082
+ if (warning.suggestion) {
1083
+ lines.push(` ${warning.suggestion}`);
1084
+ }
1085
+ }
1086
+ }
1077
1087
  }
1078
1088
  const nextActions = [];
1079
1089
  const canFix = status.providers.codex.missingSkills.length > 0 ||
@@ -1524,6 +1534,32 @@ async function runQuestion(args) {
1524
1534
  }
1525
1535
  return;
1526
1536
  }
1537
+ if (isInteractiveTerminal()) {
1538
+ const rl = createInterface({ input, output });
1539
+ try {
1540
+ console.log(renderBrandBanner("LongTable", "Researcher Checkpoint"));
1541
+ console.log("");
1542
+ const answer = await promptChoice(rl, renderQuestionHeader(1, 1, result.question.prompt.title, result.question.prompt.question), questionRecordToChoices(result.question));
1543
+ const decision = await answerWorkspaceQuestion({
1544
+ context,
1545
+ questionId: result.question.id,
1546
+ answer,
1547
+ provider,
1548
+ surface: "terminal_selector"
1549
+ });
1550
+ console.log("");
1551
+ console.log("LongTable checkpoint decision recorded");
1552
+ console.log(`- question: ${decision.question.id}`);
1553
+ console.log(`- decision: ${decision.decision.id}`);
1554
+ console.log(`- answer: ${decision.decision.selectedOption ?? answer}`);
1555
+ console.log(`- state: ${context.stateFilePath}`);
1556
+ console.log(`- current: ${context.currentFilePath}`);
1557
+ return;
1558
+ }
1559
+ finally {
1560
+ rl.close();
1561
+ }
1562
+ }
1527
1563
  const optionValues = [
1528
1564
  ...result.question.prompt.options.map((option) => option.value),
1529
1565
  ...(result.question.prompt.allowOther ? ["other"] : [])
@@ -1536,6 +1572,172 @@ async function runQuestion(args) {
1536
1572
  console.log(`- answer: longtable decide --question ${result.question.id} --answer <value>`);
1537
1573
  console.log(`- current: ${context.currentFilePath}`);
1538
1574
  }
1575
+ function isInteractiveTerminal() {
1576
+ return Boolean(input.isTTY && output.isTTY);
1577
+ }
1578
+ function questionRecordToChoices(record) {
1579
+ return [
1580
+ ...record.prompt.options.map((option) => ({
1581
+ id: option.value,
1582
+ label: option.recommended ? `${option.label} (Recommended)` : option.label,
1583
+ description: option.description ?? "Select this option."
1584
+ })),
1585
+ ...(record.prompt.allowOther
1586
+ ? [{
1587
+ id: "other",
1588
+ label: record.prompt.otherLabel ?? "Other",
1589
+ description: "Type a custom answer.",
1590
+ fallbackToText: true
1591
+ }]
1592
+ : [])
1593
+ ];
1594
+ }
1595
+ function renderClarificationCard(questions) {
1596
+ if (questions.length === 0) {
1597
+ return "No new clarification questions are pending for this prompt.";
1598
+ }
1599
+ const width = 44;
1600
+ const boxLine = (text = "") => `│ ${text.padEnd(width, " ")} │`;
1601
+ const wrap = (text) => {
1602
+ const words = text.split(/\s+/).filter(Boolean);
1603
+ const wrapped = [];
1604
+ let line = "";
1605
+ for (const word of words) {
1606
+ if (!line) {
1607
+ line = word;
1608
+ continue;
1609
+ }
1610
+ if (`${line} ${word}`.length > width) {
1611
+ wrapped.push(line);
1612
+ line = word;
1613
+ continue;
1614
+ }
1615
+ line = `${line} ${word}`;
1616
+ }
1617
+ if (line) {
1618
+ wrapped.push(line);
1619
+ }
1620
+ return wrapped.length > 0 ? wrapped : [""];
1621
+ };
1622
+ const lines = [
1623
+ "I want to make sure I handle this in the way you actually want, so here are the choices LongTable should not infer silently:",
1624
+ "",
1625
+ "┌──────────────────────────────────────────────┐"
1626
+ ];
1627
+ for (const question of questions) {
1628
+ lines.push(boxLine(question.prompt.title));
1629
+ for (const line of wrap(question.prompt.question)) {
1630
+ lines.push(boxLine(line));
1631
+ }
1632
+ for (const option of question.prompt.options) {
1633
+ const suffix = option.recommended ? " (Recommended)" : "";
1634
+ for (const line of wrap(`- ${option.label}${suffix}`)) {
1635
+ lines.push(boxLine(line));
1636
+ }
1637
+ }
1638
+ if (question.prompt.allowOther) {
1639
+ lines.push(boxLine(`- ${question.prompt.otherLabel ?? "Other"}`));
1640
+ }
1641
+ lines.push(boxLine());
1642
+ }
1643
+ lines.push("└──────────────────────────────────────────────┘");
1644
+ lines.push("");
1645
+ lines.push("Answer in a terminal with `longtable clarify --prompt ...`, or record choices with `longtable decide --question <id> --answer <value>`.");
1646
+ return lines.join("\n");
1647
+ }
1648
+ async function answerClarificationCardInTerminal(context, questions, provider) {
1649
+ if (questions.length === 0) {
1650
+ return;
1651
+ }
1652
+ const rl = createInterface({ input, output });
1653
+ try {
1654
+ console.log(renderBrandBanner("LongTable", "Clarification Card"));
1655
+ console.log("");
1656
+ for (let index = 0; index < questions.length; index += 1) {
1657
+ const question = questions[index];
1658
+ const prompt = renderQuestionHeader(index + 1, questions.length, question.prompt.title, question.prompt.question);
1659
+ const answer = await promptChoice(rl, prompt, questionRecordToChoices(question));
1660
+ await answerWorkspaceQuestion({
1661
+ context,
1662
+ questionId: question.id,
1663
+ answer,
1664
+ provider,
1665
+ surface: "terminal_selector"
1666
+ });
1667
+ }
1668
+ }
1669
+ finally {
1670
+ rl.close();
1671
+ }
1672
+ }
1673
+ async function runClarify(args) {
1674
+ const workingDirectory = typeof args.cwd === "string" ? args.cwd : cwd();
1675
+ const prompt = await resolvePrompt(typeof args.prompt === "string" ? args.prompt : undefined);
1676
+ if (!prompt) {
1677
+ throw new Error("A task context is required. Pass --prompt <text>.");
1678
+ }
1679
+ const context = await loadProjectContextFromDirectory(workingDirectory);
1680
+ if (!context) {
1681
+ throw new Error("No LongTable project workspace was found here. Run this inside a project or pass --cwd.");
1682
+ }
1683
+ const provider = args.provider === "claude" ? "claude" : args.provider === "codex" ? "codex" : undefined;
1684
+ const required = args.required === true ? true : args.advisory === true ? false : undefined;
1685
+ const result = await createWorkspaceClarificationCard({
1686
+ context,
1687
+ prompt,
1688
+ provider,
1689
+ required,
1690
+ force: args.force === true
1691
+ });
1692
+ if (args.json === true) {
1693
+ console.log(JSON.stringify({
1694
+ questions: result.questions,
1695
+ created: result.created,
1696
+ alreadyAnswered: result.alreadyAnswered,
1697
+ files: {
1698
+ state: context.stateFilePath,
1699
+ current: context.currentFilePath
1700
+ }
1701
+ }, null, 2));
1702
+ return;
1703
+ }
1704
+ if (args.print === true || !isInteractiveTerminal()) {
1705
+ console.log(renderClarificationCard(result.questions));
1706
+ return;
1707
+ }
1708
+ await answerClarificationCardInTerminal(context, result.questions, provider);
1709
+ console.log("");
1710
+ console.log("LongTable clarification decisions recorded");
1711
+ console.log(`- answered: ${result.questions.length}`);
1712
+ console.log(`- state: ${context.stateFilePath}`);
1713
+ console.log(`- current: ${context.currentFilePath}`);
1714
+ }
1715
+ async function runAutomaticClarificationIfNeeded(prompt, args) {
1716
+ if (args["no-clarify"] === true || args.print === true || args.json === true) {
1717
+ return false;
1718
+ }
1719
+ const workingDirectory = typeof args.cwd === "string" ? args.cwd : cwd();
1720
+ const context = await loadProjectContextFromDirectory(workingDirectory);
1721
+ if (!context) {
1722
+ return false;
1723
+ }
1724
+ const provider = args.provider === "claude" ? "claude" : args.provider === "codex" ? "codex" : undefined;
1725
+ const result = await createWorkspaceClarificationCard({
1726
+ context,
1727
+ prompt,
1728
+ provider,
1729
+ required: true
1730
+ });
1731
+ if (result.questions.length === 0) {
1732
+ return false;
1733
+ }
1734
+ if (!isInteractiveTerminal()) {
1735
+ console.log(renderClarificationCard(result.questions));
1736
+ return true;
1737
+ }
1738
+ await answerClarificationCardInTerminal(context, result.questions, provider);
1739
+ return false;
1740
+ }
1539
1741
  async function runAsk(args) {
1540
1742
  const prompt = await resolvePrompt(typeof args.prompt === "string" ? args.prompt : undefined);
1541
1743
  if (!prompt) {
@@ -1549,6 +1751,9 @@ async function runAsk(args) {
1549
1751
  return;
1550
1752
  }
1551
1753
  const mode = inferred === "panel" ? "review" : inferred;
1754
+ if (await runAutomaticClarificationIfNeeded(effectivePrompt, args)) {
1755
+ return;
1756
+ }
1552
1757
  const delegatedArgs = {
1553
1758
  ...args,
1554
1759
  prompt: effectivePrompt
@@ -1883,6 +2088,10 @@ async function main() {
1883
2088
  await runAsk(values);
1884
2089
  return;
1885
2090
  }
2091
+ if (command === "clarify") {
2092
+ await runClarify(values);
2093
+ return;
2094
+ }
1886
2095
  if (command === "question") {
1887
2096
  await runQuestion(values);
1888
2097
  return;
@@ -1,4 +1,4 @@
1
- import type { DecisionRecord, InvocationRecord, ProviderKind, QuestionRecord, ResearchState } from "@longtable/core";
1
+ import type { DecisionRecord, InvocationRecord, ProviderKind, 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 interface LongTableProjectRecord {
@@ -98,12 +98,30 @@ export interface LongTableWorkspaceInspection {
98
98
  selectedOption?: string;
99
99
  timestamp: string;
100
100
  }>;
101
+ answerWarnings?: Array<{
102
+ questionId: string;
103
+ decisionRecordId?: string;
104
+ issue: string;
105
+ suggestion?: string;
106
+ }>;
101
107
  }
102
108
  export declare function loadWorkspaceState(context: LongTableProjectContext): Promise<ResearchState>;
103
109
  export declare function syncCurrentWorkspaceView(context: LongTableProjectContext): Promise<string>;
104
110
  export declare function appendInvocationRecordToWorkspace(context: LongTableProjectContext, invocation: InvocationRecord, questions?: QuestionRecord[]): Promise<ResearchState>;
105
111
  export declare function listBlockingWorkspaceQuestions(context: LongTableProjectContext): Promise<QuestionRecord[]>;
106
112
  export declare function assertWorkspaceNotBlocked(context: LongTableProjectContext): Promise<void>;
113
+ export declare function createWorkspaceClarificationCard(options: {
114
+ context: LongTableProjectContext;
115
+ prompt: string;
116
+ provider?: ProviderKind;
117
+ required?: boolean;
118
+ force?: boolean;
119
+ }): Promise<{
120
+ questions: QuestionRecord[];
121
+ state: ResearchState;
122
+ created: boolean;
123
+ alreadyAnswered: boolean;
124
+ }>;
107
125
  export declare function createWorkspaceQuestion(options: {
108
126
  context: LongTableProjectContext;
109
127
  prompt: string;
@@ -121,6 +139,7 @@ export declare function answerWorkspaceQuestion(options: {
121
139
  answer: string;
122
140
  rationale?: string;
123
141
  provider?: "codex" | "claude";
142
+ surface?: QuestionSurface;
124
143
  }): Promise<{
125
144
  question: QuestionRecord;
126
145
  decision: DecisionRecord;
@@ -280,7 +280,23 @@ function summarizeWorkspaceInspection(context, state) {
280
280
  summary: record.summary,
281
281
  ...(record.selectedOption ? { selectedOption: record.selectedOption } : {}),
282
282
  timestamp: record.timestamp
283
- }))
283
+ })),
284
+ answerWarnings: questions
285
+ .filter((record) => record.status === "answered" && record.answer?.selectedValues.includes("other"))
286
+ .flatMap((record) => {
287
+ const raw = record.answer?.otherText ?? record.answer?.selectedLabels[0] ?? "";
288
+ if (!/^\d+$/.test(raw.trim())) {
289
+ return [];
290
+ }
291
+ const index = Number(raw.trim()) - 1;
292
+ const option = record.prompt.options[index];
293
+ return [{
294
+ questionId: record.id,
295
+ ...(record.decisionRecordId ? { decisionRecordId: record.decisionRecordId } : {}),
296
+ issue: `Numeric answer "${raw.trim()}" was stored as other text.`,
297
+ ...(option ? { suggestion: `Use "${option.value}" (${option.label}) for this checkpoint option.` } : {})
298
+ }];
299
+ })
284
300
  };
285
301
  }
286
302
  function buildProjectAgentsMd(project, session) {
@@ -457,7 +473,7 @@ function questionTextForCheckpoint(family, prompt) {
457
473
  return `What should LongTable decide before proceeding with: ${prompt}`;
458
474
  }
459
475
  }
460
- function optionsForCheckpointFamily(family) {
476
+ function optionsForCheckpointTrigger(family, checkpointKey) {
461
477
  if (family === "evidence") {
462
478
  return [
463
479
  { value: "verify", label: "Verify evidence first", description: "Check whether the source supports the specific claim." },
@@ -482,6 +498,72 @@ function optionsForCheckpointFamily(family) {
482
498
  { value: "defer", label: "Do not submit yet", description: "Keep the submission decision open." }
483
499
  ];
484
500
  }
501
+ if (family === "authorship") {
502
+ return [
503
+ { value: "preserve_voice", label: "Preserve the researcher's voice", description: "Keep the current authorship trace visible before rewriting or smoothing." },
504
+ { value: "revise_with_trace", label: "Revise with an explicit authorship trace", description: "Change the text, but record what came from the researcher." },
505
+ { value: "ask_researcher", label: "Ask the researcher for wording first", description: "Do not infer the intended voice or narrative stance." },
506
+ { value: "defer", label: "Keep authorship open", description: "Do not settle the voice or authorship decision yet." }
507
+ ];
508
+ }
509
+ if (family === "exploration") {
510
+ return [
511
+ { value: "surface_tensions", label: "Surface tensions first", description: "Ask what is unresolved before narrowing the project." },
512
+ { value: "narrow_scope", label: "Narrow the research scope", description: "Move toward a smaller question while keeping the choice visible." },
513
+ { value: "gather_context", label: "Gather context before narrowing", description: "Check materials, constraints, or evidence before choosing a direction." },
514
+ { value: "defer", label: "Keep exploration open", description: "Do not collapse the problem space yet." }
515
+ ];
516
+ }
517
+ if (family === "review") {
518
+ return [
519
+ { value: "revise", label: "Revise before accepting the review", description: "Change the claim, design, or draft before treating the critique as resolved." },
520
+ { value: "evidence", label: "Check evidence for the objection", description: "Verify whether the review concern is actually supported." },
521
+ { value: "proceed", label: "Proceed while logging the risk", description: "Accept the objection profile and continue with the decision recorded." },
522
+ { value: "defer", label: "Keep the objection open", description: "Do not convert the review into closure yet." }
523
+ ];
524
+ }
525
+ if (family === "commitment") {
526
+ if (checkpointKey === "research_question_freeze") {
527
+ return [
528
+ { value: "revise", label: "Revise the research question", description: "Change the framing before treating the question as settled." },
529
+ { value: "scope", label: "Choose the scope boundary", description: "Commit only the boundary, not the full study design." },
530
+ { value: "evidence", label: "Gather support before freezing", description: "Check literature, feasibility, or data fit before locking the question." },
531
+ { value: "defer", label: "Keep the question open", description: "Do not freeze the research question yet." }
532
+ ];
533
+ }
534
+ if (checkpointKey === "theory_selection") {
535
+ return [
536
+ { value: "revise", label: "Revise the theory anchor", description: "Change the conceptual frame before treating it as settled." },
537
+ { value: "compare", label: "Compare candidate theories first", description: "Keep alternatives visible before choosing one anchor." },
538
+ { value: "evidence", label: "Check construct fit first", description: "Verify that the theory supports the constructs and claims." },
539
+ { value: "defer", label: "Keep theory selection open", description: "Do not commit to a theory anchor yet." }
540
+ ];
541
+ }
542
+ if (checkpointKey === "method_design_commitment") {
543
+ return [
544
+ { value: "revise", label: "Revise the study design", description: "Change method, sample, or design before treating it as settled." },
545
+ { value: "ethics", label: "Check participant and ethics implications", description: "Pause for consent, representation, or trust concerns." },
546
+ { value: "evidence", label: "Check feasibility and evidence first", description: "Verify that the method can support the intended claims." },
547
+ { value: "defer", label: "Keep method design open", description: "Do not commit the design yet." }
548
+ ];
549
+ }
550
+ if (checkpointKey === "measurement_validity") {
551
+ return [
552
+ { value: "revise", label: "Revise the measurement plan", description: "Change scales, constructs, or instruments before treating them as settled." },
553
+ { value: "evidence", label: "Verify construct validity first", description: "Check whether the instrument supports the construct." },
554
+ { value: "pilot", label: "Pilot or inspect the measure", description: "Gather local evidence before committing the measurement." },
555
+ { value: "defer", label: "Keep measurement open", description: "Do not settle the measurement plan yet." }
556
+ ];
557
+ }
558
+ if (checkpointKey === "analysis_plan") {
559
+ return [
560
+ { value: "revise", label: "Revise the analysis plan", description: "Change model, coding, or inference strategy before committing." },
561
+ { value: "assumptions", label: "Check assumptions first", description: "Inspect data, model assumptions, or coding validity before closure." },
562
+ { value: "evidence", label: "Verify analysis fit", description: "Confirm the analysis can answer the research question." },
563
+ { value: "defer", label: "Keep analysis open", description: "Do not commit the analysis plan yet." }
564
+ ];
565
+ }
566
+ }
485
567
  return [
486
568
  { value: "revise", label: "Revise before proceeding", description: "Change the framing, design, or draft before treating this as settled." },
487
569
  { value: "evidence", label: "Gather or verify evidence first", description: "Do not proceed until the relevant evidence is checked." },
@@ -489,6 +571,133 @@ function optionsForCheckpointFamily(family) {
489
571
  { value: "defer", label: "Keep this open", description: "Do not commit yet; keep the issue visible as an open tension." }
490
572
  ];
491
573
  }
574
+ function includesAny(prompt, patterns) {
575
+ return patterns.some((pattern) => pattern.test(prompt));
576
+ }
577
+ function clarificationOptions(first, second, third, fourth) {
578
+ return [first, second, third, ...(fourth ? [fourth] : [])];
579
+ }
580
+ function buildClarificationQuestionSpecs(prompt) {
581
+ const normalized = prompt.toLowerCase();
582
+ const specs = [];
583
+ function push(spec) {
584
+ if (!specs.some((candidate) => candidate.key === spec.key)) {
585
+ specs.push(spec);
586
+ }
587
+ }
588
+ if (includesAny(normalized, [/\brubrics?\b/, /루브릭|채점기준/])) {
589
+ push({
590
+ key: "rubric_update_basis",
591
+ title: "Rubric update basis",
592
+ question: "How should LongTable use the available materials to update the rubric?",
593
+ whyNow: "Rubric updates can silently change grading criteria if LongTable guesses the calibration basis.",
594
+ options: clarificationOptions({ value: "calibrate_to_exemplars", label: "Calibrate criteria to exemplars", description: "Use strong submissions to refine what each criterion means.", recommended: true }, { value: "polish_existing", label: "Polish existing rubric only", description: "Keep criteria stable and improve wording or consistency." }, { value: "rewrite_structure", label: "Restructure the rubric", description: "Change categories or levels where the materials suggest a better structure." })
595
+ });
596
+ }
597
+ if (includesAny(normalized, [/\bexemplar\b/, /\bbest submission\b/, /\bselected submission\b/, /\bTA\b/i, /우수\s*답안|예시|선정|조교/])) {
598
+ push({
599
+ key: "exemplar_use",
600
+ title: "Exemplar use",
601
+ question: "How should LongTable use selected exemplars or TA guidance?",
602
+ whyNow: "Exemplars can either calibrate criteria privately or become visible evidence inside the output.",
603
+ options: clarificationOptions({ value: "calibrate_only", label: "Use as private calibration", description: "Adjust criteria using exemplars without quoting them.", recommended: true }, { value: "include_deidentified_excerpts", label: "Include de-identified excerpts", description: "Add short anonymized examples where they clarify quality." }, { value: "separate_notes", label: "Keep examples in separate notes", description: "Use exemplars outside the main artifact." })
604
+ });
605
+ }
606
+ if (includesAny(normalized, [/\binstruction/, /\bguidance\b/, /\bsource\b/, /\bfile\b/, /\bdocx?\b/, /지침|가이드|문서|파일|자료/])) {
607
+ push({
608
+ key: "source_authority",
609
+ title: "Source authority",
610
+ question: "If sources conflict or leave gaps, which source should LongTable privilege?",
611
+ whyNow: "Without an authority rule, LongTable may resolve conflicts by convenience rather than researcher intent.",
612
+ options: clarificationOptions({ value: "explicit_user_instruction", label: "Your explicit instruction", description: "Use the researcher's current instruction as the highest authority.", recommended: true }, { value: "project_files", label: "Project files", description: "Treat supplied files or existing artifacts as authoritative." }, { value: "external_guidance", label: "TA or external guidance", description: "Prioritize instructor, TA, venue, or policy guidance." })
613
+ });
614
+ }
615
+ if (includesAny(normalized, [/\bdeliver\b/, /\boutput\b/, /\btracked?[- ]?change/, /\bdocx?\b/, /\bmarkdown\b/, /\btable\b/, /전달|산출물|결과물|수정\s*표시|트랙|형식|포맷/])) {
616
+ push({
617
+ key: "delivery_format",
618
+ title: "Delivery format",
619
+ question: "How should LongTable deliver the clarified output?",
620
+ whyNow: "Format and change-tracking choices affect whether the result is usable for review or handoff.",
621
+ options: clarificationOptions({ value: "tracked_changes", label: "Tracked-change artifact", description: "Produce a reviewable changed version where possible.", recommended: true }, { value: "clean_final", label: "Clean final artifact", description: "Deliver the final version without change markup." }, { value: "summary_plus_artifact", label: "Summary plus artifact", description: "Include a concise change summary with the output." })
622
+ });
623
+ }
624
+ if (includesAny(normalized, [/\bupdate\b/, /\bchange\b/, /\bedit\b/, /\bfix\b/, /\bimplement\b/, /\bbuild\b/, /\bcreate\b/, /업데이트|수정|변경|구현|만들|고쳐/])) {
625
+ push({
626
+ key: "autonomy_boundary",
627
+ title: "Autonomy boundary",
628
+ question: "How much should LongTable do before checking back with you?",
629
+ whyNow: "Execution requests can move from advice to authorship or artifact ownership unless the boundary is explicit.",
630
+ options: clarificationOptions({ value: "ask_then_act", label: "Clarify first, then act", description: "Ask needed questions before changing the artifact.", recommended: true }, { value: "act_with_defaults", label: "Act with visible defaults", description: "Proceed using recommended defaults and record them." }, { value: "recommend_only", label: "Recommend only", description: "Describe changes but do not alter artifacts." })
631
+ });
632
+ }
633
+ if (includesAny(normalized, [/\bperformance\b/, /\btest\b/, /\bevaluate\b/, /\bcheck\b/, /\bbenchmark\b/, /성능|테스트|평가|체크|검증/])) {
634
+ push({
635
+ key: "evaluation_target",
636
+ title: "Evaluation target",
637
+ question: "What should LongTable treat as the main performance target?",
638
+ whyNow: "Performance checks can optimize for UX, correctness, trigger sensitivity, or delivery reliability.",
639
+ options: clarificationOptions({ value: "question_sensitivity", label: "Question sensitivity", description: "Check whether LongTable asks at the right knowledge-gap moments.", recommended: true }, { value: "renderer_convenience", label: "Renderer convenience", description: "Check whether the most convenient question UI is used." }, { value: "state_reliability", label: "State reliability", description: "Check whether questions and answers persist correctly." })
640
+ });
641
+ }
642
+ if (specs.length === 0) {
643
+ push({
644
+ key: "general_missing_context",
645
+ title: "Missing context",
646
+ question: "What should LongTable clarify before proceeding?",
647
+ whyNow: "The request can be answered in multiple ways, and choosing silently would hide a researcher judgment.",
648
+ options: clarificationOptions({ value: "scope", label: "Clarify scope first", description: "Ask what is included and excluded before acting.", recommended: true }, { value: "criteria", label: "Clarify success criteria", description: "Ask what would count as a good result." }, { value: "proceed", label: "Proceed with visible assumptions", description: "Continue, but make assumptions explicit." })
649
+ });
650
+ }
651
+ return specs;
652
+ }
653
+ const CLARIFICATION_PROMPT_PREFIX = "Clarification prompt:";
654
+ function hasClarificationPrompt(record, prompt) {
655
+ return record.prompt.rationale.includes(`${CLARIFICATION_PROMPT_PREFIX} ${prompt}`);
656
+ }
657
+ export async function createWorkspaceClarificationCard(options) {
658
+ const state = await loadResearchState(options.context.stateFilePath);
659
+ if (!options.force) {
660
+ const existing = (state.questionLog ?? []).filter((record) => hasClarificationPrompt(record, options.prompt));
661
+ const pending = existing.filter((record) => record.status === "pending");
662
+ if (pending.length > 0) {
663
+ return { questions: pending, state, created: false, alreadyAnswered: false };
664
+ }
665
+ if (existing.some((record) => record.status === "answered")) {
666
+ return { questions: [], state, created: false, alreadyAnswered: true };
667
+ }
668
+ }
669
+ const createdAt = nowIso();
670
+ const preferredSurfaces = options.provider === "claude"
671
+ ? ["native_structured", "terminal_selector", "numbered"]
672
+ : ["terminal_selector", "numbered", "native_structured"];
673
+ const questions = buildClarificationQuestionSpecs(options.prompt).map((spec) => ({
674
+ id: createId("question_record"),
675
+ createdAt,
676
+ updatedAt: createdAt,
677
+ status: "pending",
678
+ prompt: {
679
+ id: createId("question_prompt"),
680
+ checkpointKey: `clarification_${spec.key}`,
681
+ title: spec.title,
682
+ question: spec.question,
683
+ type: "single_choice",
684
+ options: spec.options,
685
+ allowOther: true,
686
+ otherLabel: "Other",
687
+ required: options.required ?? true,
688
+ source: "runtime_guidance",
689
+ rationale: [
690
+ spec.whyNow,
691
+ `${CLARIFICATION_PROMPT_PREFIX} ${options.prompt}`
692
+ ],
693
+ preferredSurfaces: preferredSurfaces
694
+ }
695
+ }));
696
+ const updated = appendQuestionRecords(state, questions);
697
+ await writeFile(options.context.stateFilePath, JSON.stringify(updated, null, 2), "utf8");
698
+ await syncCurrentWorkspaceView(options.context);
699
+ return { questions, state: updated, created: true, alreadyAnswered: false };
700
+ }
492
701
  export async function createWorkspaceQuestion(options) {
493
702
  const state = await loadResearchState(options.context.stateFilePath);
494
703
  const trigger = classifyCheckpointTrigger(options.prompt, {
@@ -507,7 +716,7 @@ export async function createWorkspaceQuestion(options) {
507
716
  title: options.title ?? questionTitleForCheckpoint(trigger.family),
508
717
  question: options.question ?? questionTextForCheckpoint(trigger.family, options.prompt),
509
718
  type: "single_choice",
510
- options: optionsForCheckpointFamily(trigger.family),
719
+ options: optionsForCheckpointTrigger(trigger.family, trigger.signal.checkpointKey),
511
720
  allowOther: true,
512
721
  otherLabel: "Other decision",
513
722
  required: options.required ?? trigger.requiresQuestionBeforeClosure,
@@ -545,22 +754,95 @@ function updateInvocationWithDecision(invocation, questionId, decisionId) {
545
754
  }
546
755
  };
547
756
  }
757
+ function normalizeAnswerToken(value) {
758
+ return value.trim().replace(/\s+/g, " ").toLowerCase();
759
+ }
760
+ function optionAnswerCandidates(option) {
761
+ return [
762
+ option.value,
763
+ option.label,
764
+ ...(option.description
765
+ ? [
766
+ `${option.label} - ${option.description}`,
767
+ `${option.label} — ${option.description}`
768
+ ]
769
+ : [])
770
+ ].map(normalizeAnswerToken);
771
+ }
772
+ function splitAnswerAndRationale(rawAnswer) {
773
+ const [firstLine = "", ...restLines] = rawAnswer.trim().split(/\r?\n/);
774
+ const rationale = restLines.join("\n").trim();
775
+ return {
776
+ selection: firstLine.trim(),
777
+ ...(rationale ? { rationale } : {})
778
+ };
779
+ }
780
+ function normalizeQuestionAnswerSelection(question, rawAnswer) {
781
+ const trimmed = rawAnswer.trim();
782
+ const { selection, rationale } = splitAnswerAndRationale(trimmed);
783
+ const numeric = Number(selection);
784
+ if (/^\d+$/.test(selection) && Number.isInteger(numeric)) {
785
+ const option = question.prompt.options[numeric - 1];
786
+ if (option) {
787
+ return {
788
+ selectedValue: option.value,
789
+ selectedLabel: option.label,
790
+ ...(rationale ? { inlineRationale: rationale } : {})
791
+ };
792
+ }
793
+ if (question.prompt.allowOther && numeric === question.prompt.options.length + 1) {
794
+ return {
795
+ selectedValue: "other",
796
+ selectedLabel: question.prompt.otherLabel ?? "Other",
797
+ ...(rationale ? { inlineRationale: rationale } : {})
798
+ };
799
+ }
800
+ throw new Error(`Answer ${selection} is outside the available LongTable question options.`);
801
+ }
802
+ const normalizedSelection = normalizeAnswerToken(selection);
803
+ const option = question.prompt.options.find((candidate) => optionAnswerCandidates(candidate).includes(normalizedSelection));
804
+ if (option) {
805
+ return {
806
+ selectedValue: option.value,
807
+ selectedLabel: option.label,
808
+ ...(rationale ? { inlineRationale: rationale } : {})
809
+ };
810
+ }
811
+ if (normalizedSelection === "other" && question.prompt.allowOther) {
812
+ return {
813
+ selectedValue: "other",
814
+ selectedLabel: question.prompt.otherLabel ?? "Other",
815
+ ...(rationale ? { inlineRationale: rationale } : {})
816
+ };
817
+ }
818
+ if (question.prompt.allowOther) {
819
+ return {
820
+ selectedValue: "other",
821
+ selectedLabel: selection,
822
+ otherText: trimmed,
823
+ ...(rationale ? { inlineRationale: rationale } : {})
824
+ };
825
+ }
826
+ throw new Error(`Answer "${selection}" does not match a LongTable question option.`);
827
+ }
548
828
  export async function answerWorkspaceQuestion(options) {
549
829
  const state = await loadResearchState(options.context.stateFilePath);
550
830
  const question = findQuestionForDecision(state, options.questionId);
551
831
  if (!question) {
552
832
  throw new Error(options.questionId ? `No pending LongTable question found for ${options.questionId}.` : "No pending LongTable question was found.");
553
833
  }
554
- const option = question.prompt.options.find((candidate) => candidate.value === options.answer);
555
- const explicitOther = options.answer === "other" && question.prompt.allowOther;
834
+ const normalized = normalizeQuestionAnswerSelection(question, options.answer);
835
+ const rationale = [normalized.inlineRationale, options.rationale]
836
+ .filter((entry) => Boolean(entry && entry.trim()))
837
+ .join("\n");
556
838
  const answer = {
557
839
  promptId: question.prompt.id,
558
- selectedValues: [option?.value ?? "other"],
559
- selectedLabels: [option?.label ?? (explicitOther ? question.prompt.otherLabel ?? "Other" : options.answer)],
560
- ...(option || explicitOther ? {} : { otherText: options.answer }),
561
- ...(options.rationale ? { rationale: options.rationale } : {}),
840
+ selectedValues: [normalized.selectedValue],
841
+ selectedLabels: [normalized.selectedLabel],
842
+ ...(normalized.otherText ? { otherText: normalized.otherText } : {}),
843
+ ...(rationale ? { rationale } : {}),
562
844
  ...(options.provider ? { provider: options.provider } : {}),
563
- surface: options.provider === "claude" ? "native_structured" : "numbered"
845
+ surface: options.surface ?? (options.provider === "claude" ? "native_structured" : "numbered")
564
846
  };
565
847
  const timestamp = nowIso();
566
848
  const decision = {
@@ -571,7 +853,7 @@ export async function answerWorkspaceQuestion(options) {
571
853
  mode: "commit",
572
854
  summary: `Answered ${question.prompt.title}: ${answer.selectedLabels.join(", ")}`,
573
855
  selectedOption: answer.selectedValues[0],
574
- ...(options.rationale ? { rationale: options.rationale } : {})
856
+ ...(rationale ? { rationale } : {})
575
857
  };
576
858
  const answeredQuestion = {
577
859
  ...question,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@longtable/cli",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "private": false,
5
5
  "description": "Researcher-facing LongTable CLI",
6
6
  "type": "module",
@@ -28,12 +28,12 @@
28
28
  "typecheck": "tsc -p tsconfig.json --noEmit"
29
29
  },
30
30
  "dependencies": {
31
- "@longtable/checkpoints": "0.1.11",
32
- "@longtable/core": "0.1.11",
33
- "@longtable/memory": "0.1.11",
34
- "@longtable/provider-claude": "0.1.11",
35
- "@longtable/provider-codex": "0.1.11",
36
- "@longtable/setup": "0.1.11"
31
+ "@longtable/checkpoints": "0.1.13",
32
+ "@longtable/core": "0.1.13",
33
+ "@longtable/memory": "0.1.13",
34
+ "@longtable/provider-claude": "0.1.13",
35
+ "@longtable/provider-codex": "0.1.13",
36
+ "@longtable/setup": "0.1.13"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@types/node": "^22.10.1",