@longtable/cli 0.1.45 → 0.1.48

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
@@ -18,7 +18,7 @@ import { buildPersonaGuidance, parseInvocationDirective } from "./persona-router
18
18
  import { PERSONA_DEFINITIONS, listRoleDefinitions } from "./personas.js";
19
19
  import { buildPanelFallback, renderPanelSummary } from "./panel.js";
20
20
  import { LONGTABLE_MANAGED_HOOK_EVENTS, codexHooksEnabled, enableCodexHooksFeature, getMissingManagedCodexHookEvents, mergeManagedCodexHooksConfig, removeManagedCodexHooks } from "./codex-hooks.js";
21
- import { appendInvocationRecordToWorkspace, assertWorkspaceNotBlocked, answerWorkspaceQuestion, buildQuestionOpportunitySpecs, clearWorkspaceQuestion, createWorkspaceFollowUpQuestions, createWorkspaceQuestion, createOrUpdateProjectWorkspace, inspectProjectWorkspace, loadWorkspaceState, loadProjectContextFromDirectory, pruneWorkspaceQuestions, repairWorkspaceStateConsistency, renderProjectWorkspaceSummary, syncCurrentWorkspaceView } from "./project-session.js";
21
+ import { appendInvocationRecordToWorkspace, applyResearchSpecificationPatch, assertWorkspaceNotBlocked, answerWorkspaceQuestion, buildQuestionOpportunitySpecs, clearWorkspaceQuestion, createWorkspaceFollowUpQuestions, createWorkspaceQuestion, createOrUpdateProjectWorkspace, diffResearchSpecifications, inspectProjectWorkspace, loadWorkspaceState, loadProjectContextFromDirectory, findUnincorporatedResearchEvidence, proposeResearchSpecificationPatch, pruneWorkspaceQuestions, readResearchSpecificationHistory, repairWorkspaceStateConsistency, renderProjectWorkspaceSummary, syncCurrentWorkspaceView } from "./project-session.js";
22
22
  import { buildTeamDebate, buildTeamReview, renderTeamDebateSummary } from "./debate.js";
23
23
  import { createPromptRenderer } from "./prompt-renderer.js";
24
24
  const VALID_MODES = new Set([
@@ -49,8 +49,45 @@ const require = createRequire(import.meta.url);
49
49
  const LONGTABLE_PACKAGE_VERSION = String(require("../package.json").version ?? "0.0.0");
50
50
  const LONGTABLE_MCP_SERVER_NAME = "longtable-state";
51
51
  const LONGTABLE_MCP_PACKAGE_VERSION = LONGTABLE_PACKAGE_VERSION;
52
+ const LONGTABLE_MCP_PACKAGE_SPEC = `@longtable/mcp@${LONGTABLE_MCP_PACKAGE_VERSION}`;
52
53
  const LONGTABLE_MCP_MARKER_START = "# LongTable state MCP START";
53
54
  const LONGTABLE_MCP_MARKER_END = "# LongTable state MCP END";
55
+ const LONGTABLE_MCP_MANAGED_TOOLS = [
56
+ "read_project",
57
+ "read_session",
58
+ "inspect_workspace",
59
+ "create_workspace",
60
+ "begin_interview",
61
+ "append_interview_turn",
62
+ "summarize_interview",
63
+ "summarize_research_specification",
64
+ "read_research_specification",
65
+ "propose_research_spec_patch",
66
+ "apply_research_spec_patch",
67
+ "diff_research_specification",
68
+ "read_research_spec_history",
69
+ "find_unincorporated_evidence",
70
+ "cancel_interview",
71
+ "confirm_first_research_shape",
72
+ "confirm_research_specification",
73
+ "pending_questions",
74
+ "evaluate_checkpoint",
75
+ "create_question",
76
+ "elicit_question",
77
+ "render_question",
78
+ "append_decision",
79
+ "regenerate_current"
80
+ ];
81
+ const LONGTABLE_MCP_RESEARCH_SPECIFICATION_TOOLS = [
82
+ "summarize_research_specification",
83
+ "read_research_specification",
84
+ "propose_research_spec_patch",
85
+ "apply_research_spec_patch",
86
+ "diff_research_specification",
87
+ "read_research_spec_history",
88
+ "find_unincorporated_evidence",
89
+ "confirm_research_specification"
90
+ ];
54
91
  function style(text, prefix) {
55
92
  return `${prefix}${text}${ANSI.reset}`;
56
93
  }
@@ -84,7 +121,7 @@ function renderInterviewLaunchSteps(provider) {
84
121
  `2. run \`${command}\``,
85
122
  "3. invoke `$longtable-interview`",
86
123
  "",
87
- "The interview will create or resume `.longtable/`, build a First Research Shape, and use option UI only for the final confirmation."
124
+ "The interview will create or resume `.longtable/`, may store a short First Research Shape handle, and uses option UI for the final Research Specification confirmation."
88
125
  ]);
89
126
  }
90
127
  function renderProgressBar(current, total) {
@@ -105,6 +142,7 @@ function usage() {
105
142
  " longtable doctor [--cwd <path>] [--fix] [--json] [--codex-dir <path>] [--codex-config <path>] [--hooks-path <path>] [--claude-dir <path>] [--codex-prompts-dir <path>] [--codex-runtime-path <file>] [--claude-runtime-path <file>]",
106
143
  " longtable status [--cwd <path>] [--fix] [--json] [--codex-dir <path>] [--codex-config <path>] [--hooks-path <path>] [--claude-dir <path>] [--codex-prompts-dir <path>] [--codex-runtime-path <file>] [--claude-runtime-path <file>]",
107
144
  " longtable audit [questions|roles] [--json]",
145
+ " longtable spec [read|history|diff|unincorporated|apply|propose] [--cwd <path>] [--json] [--spec-file <path>] [--patch-id <id>]",
108
146
  " longtable roles [--json]",
109
147
  " longtable show [--json] [--path <file>]",
110
148
  " longtable install [--json] [--path <file>] [--runtime-path <file>]",
@@ -155,7 +193,7 @@ function parseArgs(argv) {
155
193
  const values = {};
156
194
  let subcommand = maybeSubcommand;
157
195
  const modeCommand = command && VALID_MODES.has(command);
158
- const directCommand = command && ["init", "setup", "start", "resume", "doctor", "status", "audit", "roles", "show", "install", "mcp", "codex", "claude", "ask", "clarify", "question", "clear-question", "prune-questions", "panel", "decide", "sentinel", "team", "access", "search"].includes(command);
196
+ const directCommand = command && ["init", "setup", "start", "resume", "doctor", "status", "audit", "roles", "show", "install", "mcp", "codex", "claude", "ask", "clarify", "question", "clear-question", "prune-questions", "panel", "decide", "sentinel", "team", "access", "search", "spec"].includes(command);
159
197
  let startIndex = 1;
160
198
  if (modeCommand) {
161
199
  subcommand = undefined;
@@ -164,7 +202,7 @@ function parseArgs(argv) {
164
202
  else if (command === "codex" || command === "claude" || command === "mcp") {
165
203
  startIndex = 2;
166
204
  }
167
- else if ((command === "access" || command === "search") && maybeSubcommand && !maybeSubcommand.startsWith("--")) {
205
+ else if ((command === "access" || command === "search" || command === "spec") && maybeSubcommand && !maybeSubcommand.startsWith("--")) {
168
206
  subcommand = maybeSubcommand;
169
207
  startIndex = 2;
170
208
  }
@@ -1131,7 +1169,7 @@ function resolveMcpProviders(value) {
1131
1169
  function resolveMcpPackageSpec(args) {
1132
1170
  return typeof args.package === "string" && args.package.trim()
1133
1171
  ? args.package.trim()
1134
- : `@longtable/mcp@${LONGTABLE_MCP_PACKAGE_VERSION}`;
1172
+ : LONGTABLE_MCP_PACKAGE_SPEC;
1135
1173
  }
1136
1174
  function resolveCodexMcpConfigPath(args) {
1137
1175
  return resolve(normalizeUserPath(typeof args["codex-config"] === "string" && args["codex-config"].trim()
@@ -1162,6 +1200,12 @@ function renderCodexMcpBlock(serverName, command, mcpArgs) {
1162
1200
  `[mcp_servers.${serverName}]`,
1163
1201
  `command = ${escapeTomlString(command)}`,
1164
1202
  `args = [${mcpArgs.map((arg) => escapeTomlString(arg)).join(", ")}]`,
1203
+ "",
1204
+ ...LONGTABLE_MCP_MANAGED_TOOLS.flatMap((tool) => [
1205
+ `[mcp_servers.${serverName}.tools.${tool}]`,
1206
+ "approval_mode = \"approve\"",
1207
+ ""
1208
+ ]),
1165
1209
  LONGTABLE_MCP_MARKER_END
1166
1210
  ].join("\n");
1167
1211
  }
@@ -1185,10 +1229,55 @@ function codexMcpElicitationsAllowed(config) {
1185
1229
  function codexLongTableMcpConfigured(config) {
1186
1230
  return new RegExp(`\\[mcp_servers\\.${LONGTABLE_MCP_SERVER_NAME.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\]`).test(config);
1187
1231
  }
1232
+ function codexLongTableMcpPackageSpec(config) {
1233
+ const serverName = LONGTABLE_MCP_SERVER_NAME.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1234
+ const match = new RegExp(`\\[mcp_servers\\.${serverName}\\][\\s\\S]*?(?=\\n\\[|$)`).exec(config);
1235
+ if (!match) {
1236
+ return undefined;
1237
+ }
1238
+ const packageMatch = /@longtable\/mcp@[A-Za-z0-9._~+:-]+/.exec(match[0]);
1239
+ return packageMatch?.[0];
1240
+ }
1241
+ function codexLongTableMcpToolConfigured(config, tool) {
1242
+ const serverName = LONGTABLE_MCP_SERVER_NAME.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1243
+ const escapedTool = tool.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1244
+ return new RegExp(`\\[mcp_servers\\.${serverName}\\.tools\\.${escapedTool}\\]`).test(config);
1245
+ }
1246
+ function missingCodexLongTableMcpTools(config) {
1247
+ if (!codexLongTableMcpConfigured(config)) {
1248
+ return [...LONGTABLE_MCP_MANAGED_TOOLS];
1249
+ }
1250
+ return LONGTABLE_MCP_MANAGED_TOOLS.filter((tool) => !codexLongTableMcpToolConfigured(config, tool));
1251
+ }
1252
+ function preserveNonLongTableSectionsFromMarkedBlock(block, serverName) {
1253
+ const body = block
1254
+ .replace(LONGTABLE_MCP_MARKER_START, "")
1255
+ .replace(LONGTABLE_MCP_MARKER_END, "")
1256
+ .trim();
1257
+ if (!body) {
1258
+ return "";
1259
+ }
1260
+ const sections = body.split(/(?=^\[[^\]]+\])/m);
1261
+ const serverHeader = `[mcp_servers.${serverName}]`;
1262
+ const toolPrefix = `[mcp_servers.${serverName}.tools.`;
1263
+ return sections
1264
+ .map((section) => section.trim())
1265
+ .filter(Boolean)
1266
+ .filter((section) => {
1267
+ const header = section.split(/\r?\n/, 1)[0]?.trim() ?? "";
1268
+ return header !== serverHeader && !header.startsWith(toolPrefix);
1269
+ })
1270
+ .join("\n\n");
1271
+ }
1188
1272
  function replaceMarkedCodexMcpBlock(existing, block, serverName) {
1189
- const markerPattern = new RegExp(`${LONGTABLE_MCP_MARKER_START.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s\\S]*?${LONGTABLE_MCP_MARKER_END.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\n?`, "m");
1190
- const serverPattern = new RegExp(`\\n?\\[mcp_servers\\.${serverName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\][\\s\\S]*?(?=\\n\\[|$)`, "m");
1191
- const trimmed = existing.replace(markerPattern, "").replace(serverPattern, "").trimEnd();
1273
+ const markerPattern = new RegExp(`${LONGTABLE_MCP_MARKER_START.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s\\S]*?${LONGTABLE_MCP_MARKER_END.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\n?`);
1274
+ const serverPattern = new RegExp(`\\n?\\[mcp_servers\\.${serverName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\][\\s\\S]*?(?=\\n\\[|$)`);
1275
+ const toolPattern = new RegExp(`\\n?\\[mcp_servers\\.${serverName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\.tools\\.[^\\]]+\\][\\s\\S]*?(?=\\n\\[|$)`, "g");
1276
+ const withoutMarked = existing.replace(markerPattern, (matched) => {
1277
+ const preserved = preserveNonLongTableSectionsFromMarkedBlock(matched, serverName);
1278
+ return preserved ? `${preserved}\n\n` : "";
1279
+ });
1280
+ const trimmed = withoutMarked.replace(toolPattern, "").replace(serverPattern, "").trimEnd();
1192
1281
  return trimmed ? `${trimmed}\n\n${block}\n` : `${block}\n`;
1193
1282
  }
1194
1283
  async function writeCodexMcpConfig(path, block, serverName, options = {}) {
@@ -1467,6 +1556,8 @@ async function collectDoctorStatus(args) {
1467
1556
  const codexMcpConfig = existsSync(codexMcpConfigPath)
1468
1557
  ? await readFile(codexMcpConfigPath, "utf8")
1469
1558
  : "";
1559
+ const codexMcpPackageSpec = codexLongTableMcpPackageSpec(codexMcpConfig);
1560
+ const missingMcpTools = missingCodexLongTableMcpTools(codexMcpConfig);
1470
1561
  const codexHooksPath = resolveCodexHooksPath(args);
1471
1562
  const codexHooksContent = existsSync(codexHooksPath)
1472
1563
  ? await readFile(codexHooksPath, "utf8")
@@ -1502,6 +1593,10 @@ async function collectDoctorStatus(args) {
1502
1593
  mcpConfigPath: codexMcpConfigPath,
1503
1594
  mcpConfigExists: existsSync(codexMcpConfigPath),
1504
1595
  longtableMcpConfigured: codexLongTableMcpConfigured(codexMcpConfig),
1596
+ ...(codexMcpPackageSpec ? { mcpPackageSpec: codexMcpPackageSpec } : {}),
1597
+ expectedMcpPackageSpec: LONGTABLE_MCP_PACKAGE_SPEC,
1598
+ missingMcpTools,
1599
+ missingResearchSpecificationMcpTools: missingMcpTools.filter((tool) => LONGTABLE_MCP_RESEARCH_SPECIFICATION_TOOLS.includes(tool)),
1505
1600
  mcpElicitationsAllowed: codexMcpElicitationsAllowed(codexMcpConfig),
1506
1601
  hooksPath: codexHooksPath,
1507
1602
  hooksExists: existsSync(codexHooksPath),
@@ -1547,6 +1642,11 @@ function renderDoctorStatus(status) {
1547
1642
  : []),
1548
1643
  `- MCP config: ${status.providers.codex.mcpConfigExists ? "present" : "missing"} (${status.providers.codex.mcpConfigPath})`,
1549
1644
  `- LongTable MCP: ${status.providers.codex.longtableMcpConfigured ? "configured" : "missing"}`,
1645
+ `- MCP package: ${status.providers.codex.mcpPackageSpec ?? "unknown"}${status.providers.codex.mcpPackageSpec === status.providers.codex.expectedMcpPackageSpec ? "" : ` (expected ${status.providers.codex.expectedMcpPackageSpec})`}`,
1646
+ `- MCP managed tools: ${LONGTABLE_MCP_MANAGED_TOOLS.length - status.providers.codex.missingMcpTools.length}/${LONGTABLE_MCP_MANAGED_TOOLS.length} configured`,
1647
+ ...(status.providers.codex.missingResearchSpecificationMcpTools.length > 0
1648
+ ? [`- Research Specification MCP tools: missing ${status.providers.codex.missingResearchSpecificationMcpTools.join(", ")}`]
1649
+ : ["- Research Specification MCP tools: complete"]),
1550
1650
  `- MCP elicitation approval: ${status.providers.codex.mcpElicitationsAllowed ? "allowed" : "not allowed"}`,
1551
1651
  `- Codex hooks file: ${status.providers.codex.hooksExists ? "present" : "missing"} (${status.providers.codex.hooksPath})`,
1552
1652
  `- codex_hooks feature: ${status.providers.codex.codexHooksEnabled ? "enabled" : "missing"}`,
@@ -1595,6 +1695,9 @@ function renderDoctorStatus(status) {
1595
1695
  const canFix = status.providers.codex.missingSkills.length > 0 ||
1596
1696
  status.providers.claude.missingSkills.length > 0 ||
1597
1697
  status.providers.codex.legacyPromptFilesInstalled.length > 0 ||
1698
+ !status.providers.codex.longtableMcpConfigured ||
1699
+ status.providers.codex.mcpPackageSpec !== status.providers.codex.expectedMcpPackageSpec ||
1700
+ status.providers.codex.missingMcpTools.length > 0 ||
1598
1701
  !status.providers.codex.codexHooksEnabled ||
1599
1702
  status.providers.codex.missingManagedHookEvents.length > 0 ||
1600
1703
  (status.setupExists &&
@@ -1605,6 +1708,11 @@ function renderDoctorStatus(status) {
1605
1708
  if (!status.providers.codex.codexHooksEnabled || status.providers.codex.missingManagedHookEvents.length > 0) {
1606
1709
  nextActions.push("longtable codex install-hooks");
1607
1710
  }
1711
+ if (!status.providers.codex.longtableMcpConfigured ||
1712
+ status.providers.codex.mcpPackageSpec !== status.providers.codex.expectedMcpPackageSpec ||
1713
+ status.providers.codex.missingMcpTools.length > 0) {
1714
+ nextActions.push("longtable mcp install --provider codex --write");
1715
+ }
1608
1716
  if (!status.setupExists) {
1609
1717
  nextActions.push("longtable setup --provider codex");
1610
1718
  }
@@ -1644,7 +1752,7 @@ function renderRepairSummary(repair) {
1644
1752
  }
1645
1753
  }
1646
1754
  if (repair.writtenRuntimeConfigs.length > 0) {
1647
- lines.push("- wrote runtime configs:");
1755
+ lines.push("- wrote configs:");
1648
1756
  for (const target of repair.writtenRuntimeConfigs) {
1649
1757
  lines.push(` - ${target.provider}: ${target.path}`);
1650
1758
  }
@@ -1700,6 +1808,21 @@ async function repairDoctorStatus(args, status) {
1700
1808
  writtenRuntimeConfigs: [],
1701
1809
  skipped: []
1702
1810
  };
1811
+ const mcpRepairNeeded = !status.providers.codex.longtableMcpConfigured ||
1812
+ status.providers.codex.mcpPackageSpec !== status.providers.codex.expectedMcpPackageSpec ||
1813
+ status.providers.codex.missingMcpTools.length > 0;
1814
+ if (mcpRepairNeeded) {
1815
+ const mcpConfigPath = resolveDoctorCodexMcpConfigPath(args);
1816
+ const block = renderCodexMcpBlock(LONGTABLE_MCP_SERVER_NAME, "npx", ["-y", LONGTABLE_MCP_PACKAGE_SPEC]);
1817
+ await writeCodexMcpConfig(mcpConfigPath, block, LONGTABLE_MCP_SERVER_NAME, {
1818
+ enableElicitations: status.providers.codex.mcpElicitationsAllowed
1819
+ });
1820
+ repair.writtenRuntimeConfigs.push({
1821
+ provider: "codex",
1822
+ path: mcpConfigPath,
1823
+ format: "toml"
1824
+ });
1825
+ }
1703
1826
  if (status.providers.codex.missingSkills.length > 0) {
1704
1827
  repair.installedCodexSkills = (await installCodexSkills(roles, codexDir, skillSurface)).map((skill) => skill.name);
1705
1828
  }
@@ -2739,6 +2862,108 @@ async function runSearch(subcommand, args) {
2739
2862
  }
2740
2863
  console.log(renderEvidenceRunSummary(run, recordedPath));
2741
2864
  }
2865
+ async function requireWorkspaceContext(args) {
2866
+ const workingDirectory = typeof args.cwd === "string" ? args.cwd : cwd();
2867
+ const context = await loadProjectContextFromDirectory(workingDirectory);
2868
+ if (!context) {
2869
+ throw new Error("No LongTable workspace was found from the supplied cwd.");
2870
+ }
2871
+ return context;
2872
+ }
2873
+ async function readResearchSpecificationFile(path) {
2874
+ if (!path) {
2875
+ throw new Error("A Research Specification JSON file is required. Use --spec-file <path>.");
2876
+ }
2877
+ return JSON.parse(await readFile(resolve(path), "utf8"));
2878
+ }
2879
+ async function runSpec(subcommand, args) {
2880
+ const context = await requireWorkspaceContext(args);
2881
+ const command = subcommand ?? "read";
2882
+ if (command === "read" || command === "history") {
2883
+ const history = await readResearchSpecificationHistory(context);
2884
+ if (args.json === true) {
2885
+ console.log(JSON.stringify(history, null, 2));
2886
+ return;
2887
+ }
2888
+ console.log("LongTable Research Specification");
2889
+ console.log(`- title: ${history.specification?.title ?? "missing"}`);
2890
+ console.log(`- status: ${history.specification?.confirmedAt ? "confirmed" : history.specification?.status ?? "missing"}`);
2891
+ console.log(`- revisions: ${history.revisions.length}`);
2892
+ console.log(`- patches: ${history.patches.length}`);
2893
+ console.log(`- evidence records: ${history.evidenceRecords.length}`);
2894
+ for (const revision of history.revisions.slice(-5).reverse()) {
2895
+ console.log(`- v${revision.index}: ${revision.title} (${revision.changeSummary.slice(0, 2).join("; ")})`);
2896
+ }
2897
+ return;
2898
+ }
2899
+ if (command === "unincorporated") {
2900
+ const evidenceRecords = await findUnincorporatedResearchEvidence(context);
2901
+ if (args.json === true) {
2902
+ console.log(JSON.stringify({ evidenceRecords }, null, 2));
2903
+ return;
2904
+ }
2905
+ console.log("Unincorporated Research Evidence");
2906
+ for (const record of evidenceRecords.slice(-10).reverse()) {
2907
+ console.log(`- ${record.id} [${record.sourceKind}]: ${record.summary}`);
2908
+ }
2909
+ return;
2910
+ }
2911
+ if (command === "diff") {
2912
+ const specification = await readResearchSpecificationFile(typeof args["spec-file"] === "string" ? args["spec-file"] : undefined);
2913
+ const state = await loadWorkspaceState(context);
2914
+ const changes = diffResearchSpecifications(state.researchSpecification, specification);
2915
+ if (args.json === true) {
2916
+ console.log(JSON.stringify({ changes }, null, 2));
2917
+ return;
2918
+ }
2919
+ console.log("Research Specification Diff");
2920
+ for (const change of changes) {
2921
+ console.log(`- ${change.summary}`);
2922
+ }
2923
+ return;
2924
+ }
2925
+ if (command === "propose") {
2926
+ const specification = await readResearchSpecificationFile(typeof args["spec-file"] === "string" ? args["spec-file"] : undefined);
2927
+ const result = await proposeResearchSpecificationPatch({
2928
+ context,
2929
+ specification,
2930
+ source: "manual",
2931
+ rationale: typeof args.rationale === "string" ? args.rationale : undefined
2932
+ });
2933
+ if (args.json === true) {
2934
+ console.log(JSON.stringify(result, null, 2));
2935
+ return;
2936
+ }
2937
+ console.log("Research Specification patch proposed");
2938
+ console.log(`- patch: ${result.patch.id}`);
2939
+ console.log(`- changes: ${result.changes.length}`);
2940
+ console.log(`- apply: longtable spec apply --patch-id ${result.patch.id}`);
2941
+ return;
2942
+ }
2943
+ if (command === "apply") {
2944
+ const specification = typeof args["spec-file"] === "string"
2945
+ ? await readResearchSpecificationFile(args["spec-file"])
2946
+ : undefined;
2947
+ const result = await applyResearchSpecificationPatch({
2948
+ context,
2949
+ patchId: typeof args["patch-id"] === "string" ? args["patch-id"] : undefined,
2950
+ specification,
2951
+ source: "manual",
2952
+ rationale: typeof args.rationale === "string" ? args.rationale : undefined
2953
+ });
2954
+ if (args.json === true) {
2955
+ console.log(JSON.stringify(result, null, 2));
2956
+ return;
2957
+ }
2958
+ console.log("Research Specification patch applied");
2959
+ console.log(`- revision: v${result.revision.index} (${result.revision.id})`);
2960
+ console.log(`- patch: ${result.patch.id}`);
2961
+ console.log(`- decision: ${result.decision?.id ?? result.patch.decisionRecordId ?? "existing/none"}`);
2962
+ console.log(`- current: ${context.currentFilePath}`);
2963
+ return;
2964
+ }
2965
+ throw new Error(`Unknown spec subcommand: ${command}`);
2966
+ }
2742
2967
  async function runQuestion(args) {
2743
2968
  const workingDirectory = typeof args.cwd === "string" ? args.cwd : cwd();
2744
2969
  const prompt = await resolvePrompt(typeof args.prompt === "string" ? args.prompt : undefined);
@@ -3695,6 +3920,10 @@ async function main() {
3695
3920
  await runSearch(subcommand, values);
3696
3921
  return;
3697
3922
  }
3923
+ if (command === "spec") {
3924
+ await runSpec(subcommand, values);
3925
+ return;
3926
+ }
3698
3927
  if (command === "ask") {
3699
3928
  await runAsk(values);
3700
3929
  return;
@@ -1,4 +1,4 @@
1
- import type { DecisionRecord, InvocationRecord, LongTableQuestionObligation, ProviderKind, QuestionOption, QuestionGenerationResult, QuestionOpportunity, QuestionSurface, QuestionRecord, ResearchState } from "@longtable/core";
1
+ import type { DecisionRecord, EvidenceRecord, InvocationRecord, LongTableQuestionObligation, ProviderKind, QuestionOption, QuestionCommitmentFamily, QuestionEpistemicBasis, QuestionGenerationResult, QuestionOpportunity, QuestionSurface, QuestionRecord, ResearchSpecificationChange, ResearchSpecificationPatch, ResearchSpecificationPatchSource, ResearchSpecificationRevision, 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 type StartInterviewSignal = "phenomenon" | "audience" | "artifact" | "evidence" | "assumption" | "decision_risk" | "voice";
@@ -40,6 +40,9 @@ export interface ResearchSpecification {
40
40
  createdAt?: string;
41
41
  updatedAt?: string;
42
42
  sourceHookId?: string;
43
+ latestRevisionId?: string;
44
+ sourceEvidenceIds?: string[];
45
+ sectionEvidence?: Record<string, string[]>;
43
46
  researchDirection: {
44
47
  question?: string;
45
48
  purpose: string;
@@ -121,6 +124,10 @@ export type LongTableWorkspaceState = ResearchState & {
121
124
  hooks?: LongTableHookRun[];
122
125
  firstResearchShape?: FirstResearchShape;
123
126
  researchSpecification?: ResearchSpecification;
127
+ interviewTurns?: LongTableInterviewTurn[];
128
+ evidenceRecords?: EvidenceRecord[];
129
+ specPatches?: ResearchSpecificationPatch[];
130
+ specRevisions?: ResearchSpecificationRevision[];
124
131
  };
125
132
  export interface LongTableProjectRecord {
126
133
  schemaVersion: 1;
@@ -206,6 +213,10 @@ export interface LongTableWorkspaceInspection {
206
213
  pendingObligations: number;
207
214
  answeredQuestions: number;
208
215
  decisions: number;
216
+ interviewTurns?: number;
217
+ evidenceRecords?: number;
218
+ specPatches?: number;
219
+ specRevisions?: number;
209
220
  };
210
221
  recentInvocations?: Array<{
211
222
  id: string;
@@ -221,6 +232,8 @@ export interface LongTableWorkspaceInspection {
221
232
  id: string;
222
233
  title: string;
223
234
  question: string;
235
+ commitmentFamily?: QuestionCommitmentFamily;
236
+ epistemicBasis?: QuestionEpistemicBasis;
224
237
  options: string[];
225
238
  required: boolean;
226
239
  }>;
@@ -235,6 +248,8 @@ export interface LongTableWorkspaceInspection {
235
248
  id: string;
236
249
  checkpointKey: string;
237
250
  summary: string;
251
+ commitmentFamily?: QuestionCommitmentFamily;
252
+ epistemicBasis?: QuestionEpistemicBasis;
238
253
  selectedOption?: string;
239
254
  timestamp: string;
240
255
  }>;
@@ -246,6 +261,25 @@ export interface LongTableWorkspaceInspection {
246
261
  }>;
247
262
  }
248
263
  export declare function loadWorkspaceState(context: LongTableProjectContext): Promise<LongTableWorkspaceState>;
264
+ export declare function diffResearchSpecifications(before: ResearchSpecification | undefined, after: ResearchSpecification): ResearchSpecificationChange[];
265
+ export declare function applyResearchSpecificationAuditUpdate(state: LongTableWorkspaceState, options: {
266
+ specification: ResearchSpecification;
267
+ timestamp: string;
268
+ source: ResearchSpecificationPatchSource;
269
+ title?: string;
270
+ rationale?: string;
271
+ sourceEvidenceIds?: string[];
272
+ patch?: ResearchSpecificationPatch;
273
+ questionRecordId?: string;
274
+ decisionRecordId?: string;
275
+ createDecisionRecord?: boolean;
276
+ }): {
277
+ state: LongTableWorkspaceState;
278
+ specification: ResearchSpecification;
279
+ patch: ResearchSpecificationPatch;
280
+ revision: ResearchSpecificationRevision;
281
+ decision?: DecisionRecord;
282
+ };
249
283
  export declare function syncCurrentWorkspaceView(context: LongTableProjectContext): Promise<string>;
250
284
  export declare function appendInvocationRecordToWorkspace(context: LongTableProjectContext, invocation: InvocationRecord, questions?: QuestionRecord[]): Promise<LongTableWorkspaceState>;
251
285
  export declare function beginLongTableInterview(options: {
@@ -294,6 +328,41 @@ export declare function summarizeLongTableResearchSpecification(options: {
294
328
  state: LongTableWorkspaceState;
295
329
  session: LongTableSessionRecord;
296
330
  }>;
331
+ export declare function proposeResearchSpecificationPatch(options: {
332
+ context: LongTableProjectContext;
333
+ specification: ResearchSpecification;
334
+ source?: ResearchSpecificationPatchSource;
335
+ rationale?: string;
336
+ sourceEvidenceIds?: string[];
337
+ }): Promise<{
338
+ patch: ResearchSpecificationPatch;
339
+ changes: ResearchSpecificationChange[];
340
+ state: LongTableWorkspaceState;
341
+ }>;
342
+ export declare function applyResearchSpecificationPatch(options: {
343
+ context: LongTableProjectContext;
344
+ patchId?: string;
345
+ specification?: ResearchSpecification;
346
+ source?: ResearchSpecificationPatchSource;
347
+ rationale?: string;
348
+ sourceEvidenceIds?: string[];
349
+ questionRecordId?: string;
350
+ decisionRecordId?: string;
351
+ }): Promise<{
352
+ patch: ResearchSpecificationPatch;
353
+ revision: ResearchSpecificationRevision;
354
+ specification: ResearchSpecification;
355
+ state: LongTableWorkspaceState;
356
+ session: LongTableSessionRecord;
357
+ decision?: DecisionRecord;
358
+ }>;
359
+ export declare function readResearchSpecificationHistory(context: LongTableProjectContext): Promise<{
360
+ specification?: ResearchSpecification;
361
+ revisions: ResearchSpecificationRevision[];
362
+ patches: ResearchSpecificationPatch[];
363
+ evidenceRecords: EvidenceRecord[];
364
+ }>;
365
+ export declare function findUnincorporatedResearchEvidence(context: LongTableProjectContext): Promise<EvidenceRecord[]>;
297
366
  export declare function listBlockingWorkspaceQuestions(context: LongTableProjectContext): Promise<QuestionRecord[]>;
298
367
  export declare function listBlockingWorkspaceObligations(context: LongTableProjectContext): Promise<LongTableQuestionObligation[]>;
299
368
  export declare function assertWorkspaceNotBlocked(context: LongTableProjectContext): Promise<void>;
@@ -329,6 +398,8 @@ export declare function createWorkspaceQuestion(options: {
329
398
  displayReason?: string;
330
399
  provider?: ProviderKind;
331
400
  required?: boolean;
401
+ commitmentFamily?: QuestionCommitmentFamily;
402
+ epistemicBasis?: QuestionEpistemicBasis;
332
403
  }): Promise<{
333
404
  question: QuestionRecord;
334
405
  state: ResearchState;