@longtable/cli 0.1.12 → 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
@@ -1075,6 +1075,15 @@ function renderDoctorStatus(status) {
1075
1075
  lines.push(` - ${question.id}: ${question.question} (${question.options.join("/")})`);
1076
1076
  }
1077
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
+ }
1078
1087
  }
1079
1088
  const nextActions = [];
1080
1089
  const canFix = status.providers.codex.missingSkills.length > 0 ||
@@ -1525,6 +1534,32 @@ async function runQuestion(args) {
1525
1534
  }
1526
1535
  return;
1527
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
+ }
1528
1563
  const optionValues = [
1529
1564
  ...result.question.prompt.options.map((option) => option.value),
1530
1565
  ...(result.question.prompt.allowOther ? ["other"] : [])
@@ -98,6 +98,12 @@ 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>;
@@ -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." },
@@ -634,7 +716,7 @@ export async function createWorkspaceQuestion(options) {
634
716
  title: options.title ?? questionTitleForCheckpoint(trigger.family),
635
717
  question: options.question ?? questionTextForCheckpoint(trigger.family, options.prompt),
636
718
  type: "single_choice",
637
- options: optionsForCheckpointFamily(trigger.family),
719
+ options: optionsForCheckpointTrigger(trigger.family, trigger.signal.checkpointKey),
638
720
  allowOther: true,
639
721
  otherLabel: "Other decision",
640
722
  required: options.required ?? trigger.requiresQuestionBeforeClosure,
@@ -672,20 +754,93 @@ function updateInvocationWithDecision(invocation, questionId, decisionId) {
672
754
  }
673
755
  };
674
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
+ }
675
828
  export async function answerWorkspaceQuestion(options) {
676
829
  const state = await loadResearchState(options.context.stateFilePath);
677
830
  const question = findQuestionForDecision(state, options.questionId);
678
831
  if (!question) {
679
832
  throw new Error(options.questionId ? `No pending LongTable question found for ${options.questionId}.` : "No pending LongTable question was found.");
680
833
  }
681
- const option = question.prompt.options.find((candidate) => candidate.value === options.answer);
682
- 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");
683
838
  const answer = {
684
839
  promptId: question.prompt.id,
685
- selectedValues: [option?.value ?? "other"],
686
- selectedLabels: [option?.label ?? (explicitOther ? question.prompt.otherLabel ?? "Other" : options.answer)],
687
- ...(option || explicitOther ? {} : { otherText: options.answer }),
688
- ...(options.rationale ? { rationale: options.rationale } : {}),
840
+ selectedValues: [normalized.selectedValue],
841
+ selectedLabels: [normalized.selectedLabel],
842
+ ...(normalized.otherText ? { otherText: normalized.otherText } : {}),
843
+ ...(rationale ? { rationale } : {}),
689
844
  ...(options.provider ? { provider: options.provider } : {}),
690
845
  surface: options.surface ?? (options.provider === "claude" ? "native_structured" : "numbered")
691
846
  };
@@ -698,7 +853,7 @@ export async function answerWorkspaceQuestion(options) {
698
853
  mode: "commit",
699
854
  summary: `Answered ${question.prompt.title}: ${answer.selectedLabels.join(", ")}`,
700
855
  selectedOption: answer.selectedValues[0],
701
- ...(options.rationale ? { rationale: options.rationale } : {})
856
+ ...(rationale ? { rationale } : {})
702
857
  };
703
858
  const answeredQuestion = {
704
859
  ...question,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@longtable/cli",
3
- "version": "0.1.12",
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.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"
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",