@longtable/cli 0.1.11 → 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
@@ -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;
@@ -1536,6 +1537,172 @@ async function runQuestion(args) {
1536
1537
  console.log(`- answer: longtable decide --question ${result.question.id} --answer <value>`);
1537
1538
  console.log(`- current: ${context.currentFilePath}`);
1538
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
+ }
1539
1706
  async function runAsk(args) {
1540
1707
  const prompt = await resolvePrompt(typeof args.prompt === "string" ? args.prompt : undefined);
1541
1708
  if (!prompt) {
@@ -1549,6 +1716,9 @@ async function runAsk(args) {
1549
1716
  return;
1550
1717
  }
1551
1718
  const mode = inferred === "panel" ? "review" : inferred;
1719
+ if (await runAutomaticClarificationIfNeeded(effectivePrompt, args)) {
1720
+ return;
1721
+ }
1552
1722
  const delegatedArgs = {
1553
1723
  ...args,
1554
1724
  prompt: effectivePrompt
@@ -1883,6 +2053,10 @@ async function main() {
1883
2053
  await runAsk(values);
1884
2054
  return;
1885
2055
  }
2056
+ if (command === "clarify") {
2057
+ await runClarify(values);
2058
+ return;
2059
+ }
1886
2060
  if (command === "question") {
1887
2061
  await runQuestion(values);
1888
2062
  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 {
@@ -104,6 +104,18 @@ export declare function syncCurrentWorkspaceView(context: LongTableProjectContex
104
104
  export declare function appendInvocationRecordToWorkspace(context: LongTableProjectContext, invocation: InvocationRecord, questions?: QuestionRecord[]): Promise<ResearchState>;
105
105
  export declare function listBlockingWorkspaceQuestions(context: LongTableProjectContext): Promise<QuestionRecord[]>;
106
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
+ }>;
107
119
  export declare function createWorkspaceQuestion(options: {
108
120
  context: LongTableProjectContext;
109
121
  prompt: string;
@@ -121,6 +133,7 @@ export declare function answerWorkspaceQuestion(options: {
121
133
  answer: string;
122
134
  rationale?: string;
123
135
  provider?: "codex" | "claude";
136
+ surface?: QuestionSurface;
124
137
  }): Promise<{
125
138
  question: QuestionRecord;
126
139
  decision: DecisionRecord;
@@ -489,6 +489,133 @@ function optionsForCheckpointFamily(family) {
489
489
  { value: "defer", label: "Keep this open", description: "Do not commit yet; keep the issue visible as an open tension." }
490
490
  ];
491
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
+ }
492
619
  export async function createWorkspaceQuestion(options) {
493
620
  const state = await loadResearchState(options.context.stateFilePath);
494
621
  const trigger = classifyCheckpointTrigger(options.prompt, {
@@ -560,7 +687,7 @@ export async function answerWorkspaceQuestion(options) {
560
687
  ...(option || explicitOther ? {} : { otherText: options.answer }),
561
688
  ...(options.rationale ? { rationale: options.rationale } : {}),
562
689
  ...(options.provider ? { provider: options.provider } : {}),
563
- surface: options.provider === "claude" ? "native_structured" : "numbered"
690
+ surface: options.surface ?? (options.provider === "claude" ? "native_structured" : "numbered")
564
691
  };
565
692
  const timestamp = nowIso();
566
693
  const decision = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@longtable/cli",
3
- "version": "0.1.11",
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.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.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",