@longtable/cli 0.1.10 → 0.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { existsSync, readFileSync, statSync } from "node:fs";
3
- import { mkdtemp, rm } from "node:fs/promises";
3
+ import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
4
4
  import { execSync } from "node:child_process";
5
5
  import { emitKeypressEvents } from "node:readline";
6
6
  import { createInterface } from "node:readline/promises";
@@ -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",
@@ -39,6 +39,10 @@ const ANSI = {
39
39
  cyan: "\u001B[36m",
40
40
  green: "\u001B[32m"
41
41
  };
42
+ const LONGTABLE_MCP_SERVER_NAME = "longtable-state";
43
+ const LONGTABLE_MCP_PACKAGE_VERSION = "0.1.11";
44
+ const LONGTABLE_MCP_MARKER_START = "# LongTable state MCP START";
45
+ const LONGTABLE_MCP_MARKER_END = "# LongTable state MCP END";
42
46
  function style(text, prefix) {
43
47
  return `${prefix}${text}${ANSI.reset}`;
44
48
  }
@@ -80,7 +84,9 @@ function usage() {
80
84
  " longtable roles [--json]",
81
85
  " longtable show [--json] [--path <file>]",
82
86
  " longtable install [--json] [--path <file>] [--runtime-path <file>]",
87
+ " longtable mcp install [--provider codex|claude|all] [--write] [--json] [--codex-config <path>] [--claude-settings <path>] [--package <spec>]",
83
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]",
84
90
  " longtable question --prompt <decision-context> [--title <text>] [--text <question>] [--provider codex|claude] [--required|--advisory] [--print] [--cwd <path>] [--json]",
85
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>]",
86
92
  " longtable decide [--question <id>] --answer <value-or-text> [--rationale <text>] [--provider codex|claude] [--cwd <path>] [--json]",
@@ -94,6 +100,7 @@ function usage() {
94
100
  " longtable claude install-skills [--dir <path>]",
95
101
  " longtable claude remove-skills [--dir <path>]",
96
102
  " longtable claude status [--dir <path>] [--json]",
103
+ " longtable mcp install --provider all",
97
104
  "",
98
105
  "Examples:",
99
106
  " longtable init --flow interview --provider codex --install-skills",
@@ -113,13 +120,13 @@ function parseArgs(argv) {
113
120
  const values = {};
114
121
  let subcommand = maybeSubcommand;
115
122
  const modeCommand = command && VALID_MODES.has(command);
116
- const directCommand = command && ["init", "start", "resume", "doctor", "status", "roles", "show", "install", "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);
117
124
  let startIndex = 1;
118
125
  if (modeCommand) {
119
126
  subcommand = undefined;
120
127
  startIndex = 1;
121
128
  }
122
- else if (command === "codex" || command === "claude") {
129
+ else if (command === "codex" || command === "claude" || command === "mcp") {
123
130
  startIndex = 2;
124
131
  }
125
132
  else if (directCommand) {
@@ -778,6 +785,147 @@ async function runInstall(args) {
778
785
  }
779
786
  console.log(renderInstallSummary(result));
780
787
  }
788
+ function resolveMcpProviders(value) {
789
+ if (value === "codex" || value === "claude") {
790
+ return [value];
791
+ }
792
+ return ["codex", "claude"];
793
+ }
794
+ function resolveMcpPackageSpec(args) {
795
+ return typeof args.package === "string" && args.package.trim()
796
+ ? args.package.trim()
797
+ : `@longtable/mcp@${LONGTABLE_MCP_PACKAGE_VERSION}`;
798
+ }
799
+ function resolveCodexMcpConfigPath(args) {
800
+ return resolve(normalizeUserPath(typeof args["codex-config"] === "string" && args["codex-config"].trim()
801
+ ? args["codex-config"].trim()
802
+ : "~/.codex/config.toml"));
803
+ }
804
+ function resolveClaudeMcpSettingsPath(args) {
805
+ return resolve(normalizeUserPath(typeof args["claude-settings"] === "string" && args["claude-settings"].trim()
806
+ ? args["claude-settings"].trim()
807
+ : "~/.claude/settings.json"));
808
+ }
809
+ function escapeTomlString(value) {
810
+ return JSON.stringify(value);
811
+ }
812
+ function renderCodexMcpBlock(serverName, command, mcpArgs) {
813
+ return [
814
+ LONGTABLE_MCP_MARKER_START,
815
+ `[mcp_servers.${serverName}]`,
816
+ `command = ${escapeTomlString(command)}`,
817
+ `args = [${mcpArgs.map((arg) => escapeTomlString(arg)).join(", ")}]`,
818
+ LONGTABLE_MCP_MARKER_END
819
+ ].join("\n");
820
+ }
821
+ function replaceMarkedCodexMcpBlock(existing, block, serverName) {
822
+ const markerPattern = new RegExp(`${LONGTABLE_MCP_MARKER_START.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s\\S]*?${LONGTABLE_MCP_MARKER_END.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\n?`, "m");
823
+ const serverPattern = new RegExp(`\\n?\\[mcp_servers\\.${serverName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\][\\s\\S]*?(?=\\n\\[|$)`, "m");
824
+ const trimmed = existing.replace(markerPattern, "").replace(serverPattern, "").trimEnd();
825
+ return trimmed ? `${trimmed}\n\n${block}\n` : `${block}\n`;
826
+ }
827
+ async function writeCodexMcpConfig(path, block, serverName) {
828
+ const existing = existsSync(path) ? await readFile(path, "utf8") : "";
829
+ const updated = replaceMarkedCodexMcpBlock(existing, block, serverName);
830
+ await mkdir(dirname(path), { recursive: true });
831
+ await writeFile(path, updated, "utf8");
832
+ return updated;
833
+ }
834
+ function renderClaudeMcpJson(serverName, command, mcpArgs) {
835
+ return JSON.stringify({
836
+ mcpServers: {
837
+ [serverName]: {
838
+ command,
839
+ args: mcpArgs
840
+ }
841
+ }
842
+ }, null, 2);
843
+ }
844
+ async function writeClaudeMcpSettings(path, serverName, command, mcpArgs) {
845
+ let settings = {};
846
+ if (existsSync(path)) {
847
+ const raw = await readFile(path, "utf8");
848
+ settings = raw.trim() ? JSON.parse(raw) : {};
849
+ }
850
+ const existingServers = typeof settings.mcpServers === "object" && settings.mcpServers !== null && !Array.isArray(settings.mcpServers)
851
+ ? settings.mcpServers
852
+ : {};
853
+ settings.mcpServers = {
854
+ ...existingServers,
855
+ [serverName]: {
856
+ command,
857
+ args: mcpArgs
858
+ }
859
+ };
860
+ const updated = JSON.stringify(settings, null, 2);
861
+ await mkdir(dirname(path), { recursive: true });
862
+ await writeFile(path, `${updated}\n`, "utf8");
863
+ return `${updated}\n`;
864
+ }
865
+ function renderMcpInstallSummary(result) {
866
+ const lines = [
867
+ "LongTable MCP transport",
868
+ `- server: ${result.serverName}`,
869
+ `- package: ${result.packageSpec}`,
870
+ `- command: ${result.command} ${result.args.join(" ")}`,
871
+ `- mode: ${result.write ? "wrote provider config" : "printed config only"}`,
872
+ ""
873
+ ];
874
+ for (const target of result.targets) {
875
+ lines.push(`${target.provider} (${target.path})`);
876
+ lines.push("```" + target.format);
877
+ lines.push(target.content.trimEnd());
878
+ lines.push("```");
879
+ lines.push("");
880
+ }
881
+ if (!result.write) {
882
+ lines.push("Run again with `--write` to update these provider config files.");
883
+ }
884
+ return lines.join("\n").trimEnd();
885
+ }
886
+ async function runMcpSubcommand(subcommand, args) {
887
+ if (!subcommand || subcommand === "install" || subcommand === "print-config") {
888
+ const serverName = typeof args.name === "string" && args.name.trim()
889
+ ? args.name.trim()
890
+ : LONGTABLE_MCP_SERVER_NAME;
891
+ const packageSpec = resolveMcpPackageSpec(args);
892
+ const command = typeof args.command === "string" && args.command.trim() ? args.command.trim() : "npx";
893
+ const mcpArgs = command === "npx" ? ["-y", packageSpec] : [packageSpec];
894
+ const providers = resolveMcpProviders(args.provider);
895
+ const write = args.write === true;
896
+ const targets = [];
897
+ for (const provider of providers) {
898
+ if (provider === "codex") {
899
+ const path = resolveCodexMcpConfigPath(args);
900
+ const block = renderCodexMcpBlock(serverName, command, mcpArgs);
901
+ const content = write ? await writeCodexMcpConfig(path, block, serverName) : block;
902
+ targets.push({ provider, path, format: "toml", content });
903
+ }
904
+ if (provider === "claude") {
905
+ const path = resolveClaudeMcpSettingsPath(args);
906
+ const content = write
907
+ ? await writeClaudeMcpSettings(path, serverName, command, mcpArgs)
908
+ : renderClaudeMcpJson(serverName, command, mcpArgs);
909
+ targets.push({ provider, path, format: "json", content });
910
+ }
911
+ }
912
+ const result = {
913
+ serverName,
914
+ packageSpec,
915
+ command,
916
+ args: mcpArgs,
917
+ write,
918
+ targets
919
+ };
920
+ if (args.json === true) {
921
+ console.log(JSON.stringify(result, null, 2));
922
+ return;
923
+ }
924
+ console.log(renderMcpInstallSummary(result));
925
+ return;
926
+ }
927
+ throw new Error("Unknown mcp subcommand.");
928
+ }
781
929
  function commandOnPath(command) {
782
930
  try {
783
931
  execSync(`command -v ${command}`, { stdio: "ignore" });
@@ -1389,6 +1537,172 @@ async function runQuestion(args) {
1389
1537
  console.log(`- answer: longtable decide --question ${result.question.id} --answer <value>`);
1390
1538
  console.log(`- current: ${context.currentFilePath}`);
1391
1539
  }
1540
+ function isInteractiveTerminal() {
1541
+ return Boolean(input.isTTY && output.isTTY);
1542
+ }
1543
+ function questionRecordToChoices(record) {
1544
+ return [
1545
+ ...record.prompt.options.map((option) => ({
1546
+ id: option.value,
1547
+ label: option.recommended ? `${option.label} (Recommended)` : option.label,
1548
+ description: option.description ?? "Select this option."
1549
+ })),
1550
+ ...(record.prompt.allowOther
1551
+ ? [{
1552
+ id: "other",
1553
+ label: record.prompt.otherLabel ?? "Other",
1554
+ description: "Type a custom answer.",
1555
+ fallbackToText: true
1556
+ }]
1557
+ : [])
1558
+ ];
1559
+ }
1560
+ function renderClarificationCard(questions) {
1561
+ if (questions.length === 0) {
1562
+ return "No new clarification questions are pending for this prompt.";
1563
+ }
1564
+ const width = 44;
1565
+ const boxLine = (text = "") => `│ ${text.padEnd(width, " ")} │`;
1566
+ const wrap = (text) => {
1567
+ const words = text.split(/\s+/).filter(Boolean);
1568
+ const wrapped = [];
1569
+ let line = "";
1570
+ for (const word of words) {
1571
+ if (!line) {
1572
+ line = word;
1573
+ continue;
1574
+ }
1575
+ if (`${line} ${word}`.length > width) {
1576
+ wrapped.push(line);
1577
+ line = word;
1578
+ continue;
1579
+ }
1580
+ line = `${line} ${word}`;
1581
+ }
1582
+ if (line) {
1583
+ wrapped.push(line);
1584
+ }
1585
+ return wrapped.length > 0 ? wrapped : [""];
1586
+ };
1587
+ const lines = [
1588
+ "I want to make sure I handle this in the way you actually want, so here are the choices LongTable should not infer silently:",
1589
+ "",
1590
+ "┌──────────────────────────────────────────────┐"
1591
+ ];
1592
+ for (const question of questions) {
1593
+ lines.push(boxLine(question.prompt.title));
1594
+ for (const line of wrap(question.prompt.question)) {
1595
+ lines.push(boxLine(line));
1596
+ }
1597
+ for (const option of question.prompt.options) {
1598
+ const suffix = option.recommended ? " (Recommended)" : "";
1599
+ for (const line of wrap(`- ${option.label}${suffix}`)) {
1600
+ lines.push(boxLine(line));
1601
+ }
1602
+ }
1603
+ if (question.prompt.allowOther) {
1604
+ lines.push(boxLine(`- ${question.prompt.otherLabel ?? "Other"}`));
1605
+ }
1606
+ lines.push(boxLine());
1607
+ }
1608
+ lines.push("└──────────────────────────────────────────────┘");
1609
+ lines.push("");
1610
+ lines.push("Answer in a terminal with `longtable clarify --prompt ...`, or record choices with `longtable decide --question <id> --answer <value>`.");
1611
+ return lines.join("\n");
1612
+ }
1613
+ async function answerClarificationCardInTerminal(context, questions, provider) {
1614
+ if (questions.length === 0) {
1615
+ return;
1616
+ }
1617
+ const rl = createInterface({ input, output });
1618
+ try {
1619
+ console.log(renderBrandBanner("LongTable", "Clarification Card"));
1620
+ console.log("");
1621
+ for (let index = 0; index < questions.length; index += 1) {
1622
+ const question = questions[index];
1623
+ const prompt = renderQuestionHeader(index + 1, questions.length, question.prompt.title, question.prompt.question);
1624
+ const answer = await promptChoice(rl, prompt, questionRecordToChoices(question));
1625
+ await answerWorkspaceQuestion({
1626
+ context,
1627
+ questionId: question.id,
1628
+ answer,
1629
+ provider,
1630
+ surface: "terminal_selector"
1631
+ });
1632
+ }
1633
+ }
1634
+ finally {
1635
+ rl.close();
1636
+ }
1637
+ }
1638
+ async function runClarify(args) {
1639
+ const workingDirectory = typeof args.cwd === "string" ? args.cwd : cwd();
1640
+ const prompt = await resolvePrompt(typeof args.prompt === "string" ? args.prompt : undefined);
1641
+ if (!prompt) {
1642
+ throw new Error("A task context is required. Pass --prompt <text>.");
1643
+ }
1644
+ const context = await loadProjectContextFromDirectory(workingDirectory);
1645
+ if (!context) {
1646
+ throw new Error("No LongTable project workspace was found here. Run this inside a project or pass --cwd.");
1647
+ }
1648
+ const provider = args.provider === "claude" ? "claude" : args.provider === "codex" ? "codex" : undefined;
1649
+ const required = args.required === true ? true : args.advisory === true ? false : undefined;
1650
+ const result = await createWorkspaceClarificationCard({
1651
+ context,
1652
+ prompt,
1653
+ provider,
1654
+ required,
1655
+ force: args.force === true
1656
+ });
1657
+ if (args.json === true) {
1658
+ console.log(JSON.stringify({
1659
+ questions: result.questions,
1660
+ created: result.created,
1661
+ alreadyAnswered: result.alreadyAnswered,
1662
+ files: {
1663
+ state: context.stateFilePath,
1664
+ current: context.currentFilePath
1665
+ }
1666
+ }, null, 2));
1667
+ return;
1668
+ }
1669
+ if (args.print === true || !isInteractiveTerminal()) {
1670
+ console.log(renderClarificationCard(result.questions));
1671
+ return;
1672
+ }
1673
+ await answerClarificationCardInTerminal(context, result.questions, provider);
1674
+ console.log("");
1675
+ console.log("LongTable clarification decisions recorded");
1676
+ console.log(`- answered: ${result.questions.length}`);
1677
+ console.log(`- state: ${context.stateFilePath}`);
1678
+ console.log(`- current: ${context.currentFilePath}`);
1679
+ }
1680
+ async function runAutomaticClarificationIfNeeded(prompt, args) {
1681
+ if (args["no-clarify"] === true || args.print === true || args.json === true) {
1682
+ return false;
1683
+ }
1684
+ const workingDirectory = typeof args.cwd === "string" ? args.cwd : cwd();
1685
+ const context = await loadProjectContextFromDirectory(workingDirectory);
1686
+ if (!context) {
1687
+ return false;
1688
+ }
1689
+ const provider = args.provider === "claude" ? "claude" : args.provider === "codex" ? "codex" : undefined;
1690
+ const result = await createWorkspaceClarificationCard({
1691
+ context,
1692
+ prompt,
1693
+ provider,
1694
+ required: true
1695
+ });
1696
+ if (result.questions.length === 0) {
1697
+ return false;
1698
+ }
1699
+ if (!isInteractiveTerminal()) {
1700
+ console.log(renderClarificationCard(result.questions));
1701
+ return true;
1702
+ }
1703
+ await answerClarificationCardInTerminal(context, result.questions, provider);
1704
+ return false;
1705
+ }
1392
1706
  async function runAsk(args) {
1393
1707
  const prompt = await resolvePrompt(typeof args.prompt === "string" ? args.prompt : undefined);
1394
1708
  if (!prompt) {
@@ -1402,6 +1716,9 @@ async function runAsk(args) {
1402
1716
  return;
1403
1717
  }
1404
1718
  const mode = inferred === "panel" ? "review" : inferred;
1719
+ if (await runAutomaticClarificationIfNeeded(effectivePrompt, args)) {
1720
+ return;
1721
+ }
1405
1722
  const delegatedArgs = {
1406
1723
  ...args,
1407
1724
  prompt: effectivePrompt
@@ -1728,10 +2045,18 @@ async function main() {
1728
2045
  await runInstall(values);
1729
2046
  return;
1730
2047
  }
2048
+ if (command === "mcp") {
2049
+ await runMcpSubcommand(subcommand, values);
2050
+ return;
2051
+ }
1731
2052
  if (command === "ask") {
1732
2053
  await runAsk(values);
1733
2054
  return;
1734
2055
  }
2056
+ if (command === "clarify") {
2057
+ await runClarify(values);
2058
+ return;
2059
+ }
1735
2060
  if (command === "question") {
1736
2061
  await runQuestion(values);
1737
2062
  return;
package/dist/index.d.ts CHANGED
@@ -2,3 +2,4 @@ export * from "./prompt-aliases.js";
2
2
  export * from "./personas.js";
3
3
  export * from "./persona-router.js";
4
4
  export * from "./panel.js";
5
+ export * from "./project-session.js";
package/dist/index.js CHANGED
@@ -2,3 +2,4 @@ export * from "./prompt-aliases.js";
2
2
  export * from "./personas.js";
3
3
  export * from "./persona-router.js";
4
4
  export * from "./panel.js";
5
+ export * from "./project-session.js";
@@ -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 {
@@ -99,10 +99,23 @@ export interface LongTableWorkspaceInspection {
99
99
  timestamp: string;
100
100
  }>;
101
101
  }
102
+ export declare function loadWorkspaceState(context: LongTableProjectContext): Promise<ResearchState>;
102
103
  export declare function syncCurrentWorkspaceView(context: LongTableProjectContext): Promise<string>;
103
104
  export declare function appendInvocationRecordToWorkspace(context: LongTableProjectContext, invocation: InvocationRecord, questions?: QuestionRecord[]): Promise<ResearchState>;
104
105
  export declare function listBlockingWorkspaceQuestions(context: LongTableProjectContext): Promise<QuestionRecord[]>;
105
106
  export declare function assertWorkspaceNotBlocked(context: LongTableProjectContext): Promise<void>;
107
+ export declare function createWorkspaceClarificationCard(options: {
108
+ context: LongTableProjectContext;
109
+ prompt: string;
110
+ provider?: ProviderKind;
111
+ required?: boolean;
112
+ force?: boolean;
113
+ }): Promise<{
114
+ questions: QuestionRecord[];
115
+ state: ResearchState;
116
+ created: boolean;
117
+ alreadyAnswered: boolean;
118
+ }>;
106
119
  export declare function createWorkspaceQuestion(options: {
107
120
  context: LongTableProjectContext;
108
121
  prompt: string;
@@ -120,6 +133,7 @@ export declare function answerWorkspaceQuestion(options: {
120
133
  answer: string;
121
134
  rationale?: string;
122
135
  provider?: "codex" | "claude";
136
+ surface?: QuestionSurface;
123
137
  }): Promise<{
124
138
  question: QuestionRecord;
125
139
  decision: DecisionRecord;
@@ -205,6 +205,9 @@ async function loadResearchState(stateFilePath) {
205
205
  ...(parsed.studyContract ? { studyContract: parsed.studyContract } : {})
206
206
  };
207
207
  }
208
+ export async function loadWorkspaceState(context) {
209
+ return loadResearchState(context.stateFilePath);
210
+ }
208
211
  function recentInvocationRecords(state, limit = 3) {
209
212
  return (state.invocationLog ?? []).slice(-limit).reverse();
210
213
  }
@@ -486,6 +489,133 @@ function optionsForCheckpointFamily(family) {
486
489
  { value: "defer", label: "Keep this open", description: "Do not commit yet; keep the issue visible as an open tension." }
487
490
  ];
488
491
  }
492
+ function includesAny(prompt, patterns) {
493
+ return patterns.some((pattern) => pattern.test(prompt));
494
+ }
495
+ function clarificationOptions(first, second, third, fourth) {
496
+ return [first, second, third, ...(fourth ? [fourth] : [])];
497
+ }
498
+ function buildClarificationQuestionSpecs(prompt) {
499
+ const normalized = prompt.toLowerCase();
500
+ const specs = [];
501
+ function push(spec) {
502
+ if (!specs.some((candidate) => candidate.key === spec.key)) {
503
+ specs.push(spec);
504
+ }
505
+ }
506
+ if (includesAny(normalized, [/\brubrics?\b/, /루브릭|채점기준/])) {
507
+ push({
508
+ key: "rubric_update_basis",
509
+ title: "Rubric update basis",
510
+ question: "How should LongTable use the available materials to update the rubric?",
511
+ whyNow: "Rubric updates can silently change grading criteria if LongTable guesses the calibration basis.",
512
+ 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." })
513
+ });
514
+ }
515
+ if (includesAny(normalized, [/\bexemplar\b/, /\bbest submission\b/, /\bselected submission\b/, /\bTA\b/i, /우수\s*답안|예시|선정|조교/])) {
516
+ push({
517
+ key: "exemplar_use",
518
+ title: "Exemplar use",
519
+ question: "How should LongTable use selected exemplars or TA guidance?",
520
+ whyNow: "Exemplars can either calibrate criteria privately or become visible evidence inside the output.",
521
+ 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." })
522
+ });
523
+ }
524
+ if (includesAny(normalized, [/\binstruction/, /\bguidance\b/, /\bsource\b/, /\bfile\b/, /\bdocx?\b/, /지침|가이드|문서|파일|자료/])) {
525
+ push({
526
+ key: "source_authority",
527
+ title: "Source authority",
528
+ question: "If sources conflict or leave gaps, which source should LongTable privilege?",
529
+ whyNow: "Without an authority rule, LongTable may resolve conflicts by convenience rather than researcher intent.",
530
+ 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." })
531
+ });
532
+ }
533
+ if (includesAny(normalized, [/\bdeliver\b/, /\boutput\b/, /\btracked?[- ]?change/, /\bdocx?\b/, /\bmarkdown\b/, /\btable\b/, /전달|산출물|결과물|수정\s*표시|트랙|형식|포맷/])) {
534
+ push({
535
+ key: "delivery_format",
536
+ title: "Delivery format",
537
+ question: "How should LongTable deliver the clarified output?",
538
+ whyNow: "Format and change-tracking choices affect whether the result is usable for review or handoff.",
539
+ 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." })
540
+ });
541
+ }
542
+ if (includesAny(normalized, [/\bupdate\b/, /\bchange\b/, /\bedit\b/, /\bfix\b/, /\bimplement\b/, /\bbuild\b/, /\bcreate\b/, /업데이트|수정|변경|구현|만들|고쳐/])) {
543
+ push({
544
+ key: "autonomy_boundary",
545
+ title: "Autonomy boundary",
546
+ question: "How much should LongTable do before checking back with you?",
547
+ whyNow: "Execution requests can move from advice to authorship or artifact ownership unless the boundary is explicit.",
548
+ 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." })
549
+ });
550
+ }
551
+ if (includesAny(normalized, [/\bperformance\b/, /\btest\b/, /\bevaluate\b/, /\bcheck\b/, /\bbenchmark\b/, /성능|테스트|평가|체크|검증/])) {
552
+ push({
553
+ key: "evaluation_target",
554
+ title: "Evaluation target",
555
+ question: "What should LongTable treat as the main performance target?",
556
+ whyNow: "Performance checks can optimize for UX, correctness, trigger sensitivity, or delivery reliability.",
557
+ 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." })
558
+ });
559
+ }
560
+ if (specs.length === 0) {
561
+ push({
562
+ key: "general_missing_context",
563
+ title: "Missing context",
564
+ question: "What should LongTable clarify before proceeding?",
565
+ whyNow: "The request can be answered in multiple ways, and choosing silently would hide a researcher judgment.",
566
+ 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." })
567
+ });
568
+ }
569
+ return specs;
570
+ }
571
+ const CLARIFICATION_PROMPT_PREFIX = "Clarification prompt:";
572
+ function hasClarificationPrompt(record, prompt) {
573
+ return record.prompt.rationale.includes(`${CLARIFICATION_PROMPT_PREFIX} ${prompt}`);
574
+ }
575
+ export async function createWorkspaceClarificationCard(options) {
576
+ const state = await loadResearchState(options.context.stateFilePath);
577
+ if (!options.force) {
578
+ const existing = (state.questionLog ?? []).filter((record) => hasClarificationPrompt(record, options.prompt));
579
+ const pending = existing.filter((record) => record.status === "pending");
580
+ if (pending.length > 0) {
581
+ return { questions: pending, state, created: false, alreadyAnswered: false };
582
+ }
583
+ if (existing.some((record) => record.status === "answered")) {
584
+ return { questions: [], state, created: false, alreadyAnswered: true };
585
+ }
586
+ }
587
+ const createdAt = nowIso();
588
+ const preferredSurfaces = options.provider === "claude"
589
+ ? ["native_structured", "terminal_selector", "numbered"]
590
+ : ["terminal_selector", "numbered", "native_structured"];
591
+ const questions = buildClarificationQuestionSpecs(options.prompt).map((spec) => ({
592
+ id: createId("question_record"),
593
+ createdAt,
594
+ updatedAt: createdAt,
595
+ status: "pending",
596
+ prompt: {
597
+ id: createId("question_prompt"),
598
+ checkpointKey: `clarification_${spec.key}`,
599
+ title: spec.title,
600
+ question: spec.question,
601
+ type: "single_choice",
602
+ options: spec.options,
603
+ allowOther: true,
604
+ otherLabel: "Other",
605
+ required: options.required ?? true,
606
+ source: "runtime_guidance",
607
+ rationale: [
608
+ spec.whyNow,
609
+ `${CLARIFICATION_PROMPT_PREFIX} ${options.prompt}`
610
+ ],
611
+ preferredSurfaces: preferredSurfaces
612
+ }
613
+ }));
614
+ const updated = appendQuestionRecords(state, questions);
615
+ await writeFile(options.context.stateFilePath, JSON.stringify(updated, null, 2), "utf8");
616
+ await syncCurrentWorkspaceView(options.context);
617
+ return { questions, state: updated, created: true, alreadyAnswered: false };
618
+ }
489
619
  export async function createWorkspaceQuestion(options) {
490
620
  const state = await loadResearchState(options.context.stateFilePath);
491
621
  const trigger = classifyCheckpointTrigger(options.prompt, {
@@ -557,7 +687,7 @@ export async function answerWorkspaceQuestion(options) {
557
687
  ...(option || explicitOther ? {} : { otherText: options.answer }),
558
688
  ...(options.rationale ? { rationale: options.rationale } : {}),
559
689
  ...(options.provider ? { provider: options.provider } : {}),
560
- surface: options.provider === "claude" ? "native_structured" : "numbered"
690
+ surface: options.surface ?? (options.provider === "claude" ? "native_structured" : "numbered")
561
691
  };
562
692
  const timestamp = nowIso();
563
693
  const decision = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@longtable/cli",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
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.10",
32
- "@longtable/core": "0.1.10",
33
- "@longtable/memory": "0.1.10",
34
- "@longtable/provider-claude": "0.1.10",
35
- "@longtable/provider-codex": "0.1.10",
36
- "@longtable/setup": "0.1.10"
31
+ "@longtable/checkpoints": "0.1.12",
32
+ "@longtable/core": "0.1.12",
33
+ "@longtable/memory": "0.1.12",
34
+ "@longtable/provider-claude": "0.1.12",
35
+ "@longtable/provider-codex": "0.1.12",
36
+ "@longtable/setup": "0.1.12"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@types/node": "^22.10.1",