@longtable/cli 0.1.56 → 0.1.57

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
@@ -159,6 +159,7 @@ function usage() {
159
159
  " longtable clarify --prompt <task-context> [--provider codex|claude] [--required|--advisory] [--print] [--cwd <path>] [--json] [--force]",
160
160
  " longtable question --prompt <decision-context> [--title <text>] [--text <question>] [--provider codex|claude] [--required|--advisory] [--print] [--cwd <path>] [--json]",
161
161
  " longtable clear-question --question <id> --reason <text> [--cwd <path>] [--json]",
162
+ " longtable repair-state [--cwd <path>] [--dry-run] [--json]",
162
163
  " longtable panel [--prompt <text>] [--role <role[,role]>] [--mode review|critique|draft|commit] [--visibility synthesis_only|show_on_conflict|always_visible] [--provider codex|claude] [--native-workers|--native-subagents] [--wait [ms]] [--print] [--json] [--setup <path>] [--cwd <path>]",
163
164
  " longtable panel status --run <panel_run_id> [--wait [ms]] [--cwd <path>] [--json]",
164
165
  " longtable panel stop --run <panel_run_id> [--cwd <path>] [--json]",
@@ -200,7 +201,7 @@ function parseArgs(argv) {
200
201
  const values = {};
201
202
  let subcommand = maybeSubcommand;
202
203
  const modeCommand = command && VALID_MODES.has(command);
203
- const directCommand = command && ["init", "setup", "start", "resume", "doctor", "status", "audit", "roles", "show", "install", "mcp", "codex", "claude", "ask", "clarify", "question", "clear-question", "prune-questions", "panel", "handoff", "decide", "sentinel", "access", "search", "spec"].includes(command);
204
+ const directCommand = command && ["init", "setup", "start", "resume", "doctor", "status", "audit", "roles", "show", "install", "mcp", "codex", "claude", "ask", "clarify", "question", "clear-question", "repair-state", "prune-questions", "panel", "handoff", "decide", "sentinel", "access", "search", "spec"].includes(command);
204
205
  let startIndex = 1;
205
206
  if (modeCommand) {
206
207
  subcommand = undefined;
@@ -1783,6 +1784,10 @@ function renderDoctorStatus(status) {
1783
1784
  if (!status.workspace.found) {
1784
1785
  nextActions.push("longtable start");
1785
1786
  }
1787
+ if ((status.workspace.answerWarnings ?? []).some((warning) => warning.issue.includes("legacy answer shape"))) {
1788
+ const root = status.workspace.rootPath ? ` --cwd "${status.workspace.rootPath}"` : "";
1789
+ nextActions.push(`longtable repair-state${root}`);
1790
+ }
1786
1791
  nextActions.push(...status.hardStop.nextActions);
1787
1792
  const firstQuestion = status.workspace.pendingQuestions?.[0];
1788
1793
  if (firstQuestion && status.hardStop.nextActions.length === 0) {
@@ -3431,6 +3436,39 @@ async function runClearQuestion(args) {
3431
3436
  console.log(`- state: ${context.stateFilePath}`);
3432
3437
  console.log(`- current: ${context.currentFilePath}`);
3433
3438
  }
3439
+ async function runRepairState(args) {
3440
+ const workingDirectory = typeof args.cwd === "string" ? args.cwd : cwd();
3441
+ const context = await loadProjectContextFromDirectory(workingDirectory);
3442
+ if (!context) {
3443
+ throw new Error("No LongTable project workspace was found here. Run this inside a project or pass --cwd.");
3444
+ }
3445
+ const result = await repairWorkspaceStateConsistency({
3446
+ context,
3447
+ dryRun: args["dry-run"] === true
3448
+ });
3449
+ if (args.json === true) {
3450
+ console.log(JSON.stringify({
3451
+ dryRun: args["dry-run"] === true,
3452
+ repaired: result.repaired,
3453
+ files: {
3454
+ state: context.stateFilePath,
3455
+ current: context.currentFilePath
3456
+ }
3457
+ }, null, 2));
3458
+ return;
3459
+ }
3460
+ console.log(args["dry-run"] === true ? "LongTable state repair preview" : "LongTable state repaired");
3461
+ if (result.repaired.length === 0) {
3462
+ console.log("- no repairs needed");
3463
+ }
3464
+ else {
3465
+ for (const item of result.repaired) {
3466
+ console.log(`- ${item}`);
3467
+ }
3468
+ }
3469
+ console.log(`- state: ${context.stateFilePath}`);
3470
+ console.log(`- current: ${context.currentFilePath}`);
3471
+ }
3434
3472
  async function runPruneQuestions(args) {
3435
3473
  const workingDirectory = typeof args.cwd === "string" ? args.cwd : cwd();
3436
3474
  const context = await loadProjectContextFromDirectory(workingDirectory);
@@ -4278,6 +4316,10 @@ async function main() {
4278
4316
  await runClearQuestion(values);
4279
4317
  return;
4280
4318
  }
4319
+ if (command === "repair-state") {
4320
+ await runRepairState(values);
4321
+ return;
4322
+ }
4281
4323
  if (command === "prune-questions") {
4282
4324
  await runPruneQuestions(values);
4283
4325
  return;
@@ -490,6 +490,7 @@ export declare function pruneWorkspaceQuestions(options: {
490
490
  }>;
491
491
  export declare function repairWorkspaceStateConsistency(options: {
492
492
  context: LongTableProjectContext;
493
+ dryRun?: boolean;
493
494
  }): Promise<{
494
495
  state: ResearchState;
495
496
  repaired: string[];
@@ -497,6 +497,98 @@ function formatQuestionMetadata(record) {
497
497
  ].filter(Boolean);
498
498
  return parts.length > 0 ? ` [${parts.join("; ")}]` : "";
499
499
  }
500
+ const QUESTION_SURFACES = new Set([
501
+ "native_structured",
502
+ "mcp_elicitation",
503
+ "numbered",
504
+ "terminal_selector",
505
+ "web_form"
506
+ ]);
507
+ function asStringArray(value) {
508
+ if (Array.isArray(value)) {
509
+ return value.filter((entry) => typeof entry === "string");
510
+ }
511
+ return typeof value === "string" ? [value] : [];
512
+ }
513
+ function legacyQuestionAnswerRecord(record) {
514
+ return asRecord(record.answer);
515
+ }
516
+ function labelForQuestionAnswerValue(record, value) {
517
+ const option = record.prompt.options.find((candidate) => candidate.value === value || candidate.label === value);
518
+ if (option) {
519
+ return option.label;
520
+ }
521
+ if (value === "other") {
522
+ return record.prompt.otherLabel ?? "Other";
523
+ }
524
+ return value;
525
+ }
526
+ function selectedValuesForQuestion(record) {
527
+ const answer = legacyQuestionAnswerRecord(record);
528
+ if (!answer) {
529
+ return [];
530
+ }
531
+ const directValues = asStringArray(answer.selectedValues);
532
+ if (directValues.length > 0) {
533
+ return directValues;
534
+ }
535
+ const legacyValues = [
536
+ ...asStringArray(answer.selectedValue),
537
+ ...asStringArray(answer.selected),
538
+ ...asStringArray(answer.selectedOption),
539
+ ...asStringArray(answer.answer),
540
+ ...asStringArray(answer.value)
541
+ ];
542
+ return uniqueStrings(legacyValues);
543
+ }
544
+ function selectedLabelsForQuestion(record, selectedValues) {
545
+ const answer = legacyQuestionAnswerRecord(record);
546
+ const directLabels = asStringArray(answer?.selectedLabels);
547
+ if (directLabels.length > 0) {
548
+ return directLabels;
549
+ }
550
+ return selectedValues.map((value) => labelForQuestionAnswerValue(record, value));
551
+ }
552
+ function legacyAnswerShapeWarnings(questions) {
553
+ return questions.flatMap((record) => {
554
+ if (record.status !== "answered" || !legacyQuestionAnswerRecord(record)) {
555
+ return [];
556
+ }
557
+ const selectedValues = asStringArray(legacyQuestionAnswerRecord(record)?.selectedValues);
558
+ const selectedLabels = asStringArray(legacyQuestionAnswerRecord(record)?.selectedLabels);
559
+ if (selectedValues.length > 0 && selectedLabels.length > 0) {
560
+ return [];
561
+ }
562
+ return [{
563
+ questionId: record.id,
564
+ ...(record.decisionRecordId ? { decisionRecordId: record.decisionRecordId } : {}),
565
+ issue: "Answered question uses a legacy answer shape that is missing selectedValues or selectedLabels.",
566
+ suggestion: "Run `longtable repair-state --cwd <project-path>` to normalize the answer without changing the recorded selection."
567
+ }];
568
+ });
569
+ }
570
+ function numericOtherAnswerWarnings(questions) {
571
+ return questions.flatMap((record) => {
572
+ if (record.status !== "answered" || !selectedValuesForQuestion(record).includes("other")) {
573
+ return [];
574
+ }
575
+ const answer = legacyQuestionAnswerRecord(record);
576
+ const raw = typeof answer?.otherText === "string"
577
+ ? answer.otherText
578
+ : selectedLabelsForQuestion(record, selectedValuesForQuestion(record))[0] ?? "";
579
+ if (!/^\d+$/.test(raw.trim())) {
580
+ return [];
581
+ }
582
+ const index = Number(raw.trim()) - 1;
583
+ const option = record.prompt.options[index];
584
+ return [{
585
+ questionId: record.id,
586
+ ...(record.decisionRecordId ? { decisionRecordId: record.decisionRecordId } : {}),
587
+ issue: `Numeric answer "${raw.trim()}" was stored as other text.`,
588
+ ...(option ? { suggestion: `Use "${option.value}" (${option.label}) for this checkpoint option.` } : {})
589
+ }];
590
+ });
591
+ }
500
592
  function compactLine(value, limit = 160) {
501
593
  const compacted = value.replace(/\s+/g, " ").trim();
502
594
  return compacted.length > limit ? `${compacted.slice(0, limit - 1)}…` : compacted;
@@ -854,22 +946,10 @@ function summarizeWorkspaceInspection(context, state) {
854
946
  ...(record.selectedOption ? { selectedOption: record.selectedOption } : {}),
855
947
  timestamp: record.timestamp
856
948
  })),
857
- answerWarnings: questions
858
- .filter((record) => record.status === "answered" && record.answer?.selectedValues.includes("other"))
859
- .flatMap((record) => {
860
- const raw = record.answer?.otherText ?? record.answer?.selectedLabels[0] ?? "";
861
- if (!/^\d+$/.test(raw.trim())) {
862
- return [];
863
- }
864
- const index = Number(raw.trim()) - 1;
865
- const option = record.prompt.options[index];
866
- return [{
867
- questionId: record.id,
868
- ...(record.decisionRecordId ? { decisionRecordId: record.decisionRecordId } : {}),
869
- issue: `Numeric answer "${raw.trim()}" was stored as other text.`,
870
- ...(option ? { suggestion: `Use "${option.value}" (${option.label}) for this checkpoint option.` } : {})
871
- }];
872
- })
949
+ answerWarnings: [
950
+ ...(legacyAnswerShapeWarnings(questions) ?? []),
951
+ ...(numericOtherAnswerWarnings(questions) ?? [])
952
+ ]
873
953
  };
874
954
  }
875
955
  function buildProjectAgentsMd(project, session) {
@@ -3242,7 +3322,51 @@ export async function repairWorkspaceStateConsistency(options) {
3242
3322
  })
3243
3323
  };
3244
3324
  }
3245
- if (repaired.length > 0) {
3325
+ const timestamp = nowIso();
3326
+ const repairedQuestionLog = (updated.questionLog ?? []).map((record) => {
3327
+ if (record.status !== "answered") {
3328
+ return record;
3329
+ }
3330
+ const answer = legacyQuestionAnswerRecord(record);
3331
+ if (!answer) {
3332
+ return record;
3333
+ }
3334
+ const selectedValues = selectedValuesForQuestion(record);
3335
+ if (selectedValues.length === 0) {
3336
+ return record;
3337
+ }
3338
+ const selectedLabels = selectedLabelsForQuestion(record, selectedValues);
3339
+ const needsRepair = asStringArray(answer.selectedValues).length === 0 ||
3340
+ asStringArray(answer.selectedLabels).length === 0 ||
3341
+ typeof answer.promptId !== "string" ||
3342
+ !QUESTION_SURFACES.has(answer.surface);
3343
+ if (!needsRepair) {
3344
+ return record;
3345
+ }
3346
+ repaired.push(`normalized legacy answer shape for question ${record.id}`);
3347
+ const normalizedAnswer = {
3348
+ ...answer,
3349
+ promptId: typeof answer.promptId === "string" ? answer.promptId : record.prompt.id,
3350
+ selectedValues,
3351
+ selectedLabels,
3352
+ ...(typeof answer.otherText === "string" && answer.otherText.trim() ? { otherText: answer.otherText } : {}),
3353
+ ...(typeof answer.rationale === "string" && answer.rationale.trim() ? { rationale: answer.rationale } : {}),
3354
+ ...(answer.provider === "codex" || answer.provider === "claude" ? { provider: answer.provider } : {}),
3355
+ surface: QUESTION_SURFACES.has(answer.surface) ? answer.surface : "numbered"
3356
+ };
3357
+ return {
3358
+ ...record,
3359
+ updatedAt: timestamp,
3360
+ answer: normalizedAnswer
3361
+ };
3362
+ });
3363
+ if (repairedQuestionLog.some((record, index) => record !== (updated.questionLog ?? [])[index])) {
3364
+ updated = {
3365
+ ...updated,
3366
+ questionLog: repairedQuestionLog
3367
+ };
3368
+ }
3369
+ if (repaired.length > 0 && !options.dryRun) {
3246
3370
  await writeFile(options.context.stateFilePath, JSON.stringify(updated, null, 2), "utf8");
3247
3371
  await syncCurrentWorkspaceView(options.context);
3248
3372
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@longtable/cli",
3
- "version": "0.1.56",
3
+ "version": "0.1.57",
4
4
  "private": false,
5
5
  "description": "Researcher-facing LongTable CLI",
6
6
  "type": "module",
@@ -29,12 +29,12 @@
29
29
  },
30
30
  "dependencies": {
31
31
  "@clack/prompts": "^1.2.0",
32
- "@longtable/checkpoints": "0.1.56",
33
- "@longtable/core": "0.1.56",
34
- "@longtable/memory": "0.1.56",
35
- "@longtable/provider-claude": "0.1.56",
36
- "@longtable/provider-codex": "0.1.56",
37
- "@longtable/setup": "0.1.56"
32
+ "@longtable/checkpoints": "0.1.57",
33
+ "@longtable/core": "0.1.57",
34
+ "@longtable/memory": "0.1.57",
35
+ "@longtable/provider-claude": "0.1.57",
36
+ "@longtable/provider-codex": "0.1.57",
37
+ "@longtable/setup": "0.1.57"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@types/node": "^22.10.1",