@longtable/mcp 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/README.md CHANGED
@@ -15,7 +15,7 @@ longtable-state
15
15
  Run:
16
16
 
17
17
  ```bash
18
- npx -y @longtable/mcp@0.1.41
18
+ npx -y @longtable/mcp@0.1.47
19
19
  ```
20
20
 
21
21
  Self-test:
@@ -62,4 +62,5 @@ export interface ResearchSpecificationQuestionSpec {
62
62
  export declare function renderResearchSpecificationPreview(specification: ResearchSpecification): string;
63
63
  export declare function buildResearchSpecificationQuestion(specification: ResearchSpecification): ResearchSpecificationQuestionSpec;
64
64
  export declare function researchSpecificationAnswerConfirms(answer: string): boolean;
65
+ export declare function researchSpecificationAnswerNeedsFollowUp(answer: string): boolean;
65
66
  export declare function researchSpecificationAnswerStatus(answer: string): "confirmed" | "active" | "deferred";
@@ -116,6 +116,9 @@ export function buildResearchSpecificationQuestion(specification) {
116
116
  export function researchSpecificationAnswerConfirms(answer) {
117
117
  return answer === "confirm_specification";
118
118
  }
119
+ export function researchSpecificationAnswerNeedsFollowUp(answer) {
120
+ return answer === "ask_one_more" || answer === "revise_section";
121
+ }
119
122
  export function researchSpecificationAnswerStatus(answer) {
120
123
  if (researchSpecificationAnswerConfirms(answer)) {
121
124
  return "confirmed";
package/dist/server.js CHANGED
@@ -11,9 +11,9 @@ import { classifyCheckpointTrigger } from "@longtable/checkpoints";
11
11
  import { renderQuestionRecordInput } from "@longtable/provider-claude";
12
12
  import { renderQuestionRecordPrompt } from "@longtable/provider-codex";
13
13
  import { loadSetupOutput } from "@longtable/setup";
14
- import { answerWorkspaceQuestion, clearWorkspaceQuestion, createOrUpdateProjectWorkspace, createWorkspaceQuestion, inspectProjectWorkspace, loadProjectContextFromDirectory, loadWorkspaceState, syncCurrentWorkspaceView } from "@longtable/cli";
14
+ import { answerWorkspaceQuestion, applyResearchSpecificationAuditUpdate, applyResearchSpecificationPatch, clearWorkspaceQuestion, createOrUpdateProjectWorkspace, createWorkspaceQuestion, diffResearchSpecifications, findUnincorporatedResearchEvidence, inspectProjectWorkspace, loadProjectContextFromDirectory, loadWorkspaceState, proposeResearchSpecificationPatch, readResearchSpecificationHistory, syncCurrentWorkspaceView } from "@longtable/cli";
15
15
  import { buildFirstResearchShapeQuestion, firstResearchShapeAnswerConfirms, firstResearchShapeAnswerStatus } from "./first-research-shape.js";
16
- import { buildResearchSpecificationQuestion, renderResearchSpecificationPreview, researchSpecificationAnswerConfirms, researchSpecificationAnswerStatus } from "./research-specification.js";
16
+ import { buildResearchSpecificationQuestion, renderResearchSpecificationPreview, researchSpecificationAnswerConfirms, researchSpecificationAnswerNeedsFollowUp, researchSpecificationAnswerStatus } from "./research-specification.js";
17
17
  const SERVER_NAME = "longtable-state";
18
18
  const require = createRequire(import.meta.url);
19
19
  const SERVER_VERSION = String(require("../package.json").version ?? "0.0.0");
@@ -27,6 +27,11 @@ const TOOL_NAMES = [
27
27
  "summarize_interview",
28
28
  "summarize_research_specification",
29
29
  "read_research_specification",
30
+ "propose_research_spec_patch",
31
+ "apply_research_spec_patch",
32
+ "diff_research_specification",
33
+ "read_research_spec_history",
34
+ "find_unincorporated_evidence",
30
35
  "cancel_interview",
31
36
  "confirm_first_research_shape",
32
37
  "confirm_research_specification",
@@ -47,6 +52,22 @@ const questionOptionSchema = z.object({
47
52
  description: z.string().optional(),
48
53
  recommended: z.boolean().optional()
49
54
  });
55
+ const commitmentFamilySchema = z.enum([
56
+ "scope",
57
+ "construct",
58
+ "coding",
59
+ "method",
60
+ "evidence",
61
+ "epistemic_authority",
62
+ "product_policy"
63
+ ]);
64
+ const epistemicBasisSchema = z.enum([
65
+ "researcher_knowledge",
66
+ "project_state",
67
+ "external_evidence",
68
+ "ai_inference",
69
+ "mixed"
70
+ ]);
50
71
  const firstResearchShapeSchema = z.object({
51
72
  handle: z.string().min(1),
52
73
  currentGoal: z.string().min(1),
@@ -67,6 +88,9 @@ const researchSpecificationSchema = z.object({
67
88
  createdAt: z.string().optional(),
68
89
  updatedAt: z.string().optional(),
69
90
  sourceHookId: z.string().optional(),
91
+ latestRevisionId: z.string().optional(),
92
+ sourceEvidenceIds: z.array(z.string()).optional(),
93
+ sectionEvidence: z.record(z.string(), z.array(z.string())).optional(),
70
94
  researchDirection: z.object({
71
95
  question: z.string().optional(),
72
96
  purpose: z.string().min(1),
@@ -113,6 +137,15 @@ const researchSpecificationSchema = z.object({
113
137
  confidence: z.enum(["low", "medium", "high"]).default("medium"),
114
138
  confirmedAt: z.string().optional()
115
139
  });
140
+ const researchSpecificationPatchSourceSchema = z.enum([
141
+ "interview",
142
+ "panel",
143
+ "critic",
144
+ "reviewer",
145
+ "decision",
146
+ "manual",
147
+ "system"
148
+ ]);
116
149
  function textResult(structuredContent) {
117
150
  return {
118
151
  content: [
@@ -228,6 +261,57 @@ function resolveFirstResearchShapeObligation(state, options) {
228
261
  })
229
262
  };
230
263
  }
264
+ function researchSpecificationFollowUpReason(answer) {
265
+ if (answer === "ask_one_more") {
266
+ return "The researcher chose one more question before saving; after that answer, LongTable must update or read the Research Specification and return to confirmation.";
267
+ }
268
+ if (answer === "revise_section") {
269
+ return "The researcher chose section revision; after the section is revised, LongTable must return to the Research Specification Preview for confirmation.";
270
+ }
271
+ return "Research Specification confirmation remains open.";
272
+ }
273
+ function ensureResearchSpecificationConfirmationObligation(state, specification, options) {
274
+ const existing = (state.questionObligations ?? []).find((obligation) => obligation.kind === "research_specification_confirmation" &&
275
+ obligation.status === "pending" &&
276
+ ((specification.sourceHookId && obligation.sourceHookId === specification.sourceHookId) ||
277
+ (!specification.sourceHookId && obligation.prompt.includes(specification.title))));
278
+ const timestamp = new Date().toISOString();
279
+ const next = {
280
+ ...(existing ?? {
281
+ id: createId("question_obligation"),
282
+ kind: "research_specification_confirmation",
283
+ status: "pending",
284
+ createdAt: timestamp
285
+ }),
286
+ updatedAt: timestamp,
287
+ prompt: `Return to Research Specification Preview before ending the interview: ${specification.title}`,
288
+ reason: researchSpecificationFollowUpReason(options.answer),
289
+ ...(specification.sourceHookId ? { sourceHookId: specification.sourceHookId } : {}),
290
+ ...(options.questionId ? { questionId: options.questionId } : existing?.questionId ? { questionId: existing.questionId } : {}),
291
+ ...(options.decisionId ? { decisionId: options.decisionId } : existing?.decisionId ? { decisionId: existing.decisionId } : {})
292
+ };
293
+ return upsertQuestionObligation(state, next);
294
+ }
295
+ function resolveResearchSpecificationConfirmationObligation(state, specification, options = {}) {
296
+ return {
297
+ ...state,
298
+ questionObligations: (state.questionObligations ?? []).map((obligation) => {
299
+ const matches = obligation.kind === "research_specification_confirmation" && ((specification.sourceHookId && obligation.sourceHookId === specification.sourceHookId) ||
300
+ (options.questionId && obligation.questionId === options.questionId) ||
301
+ (!specification.sourceHookId && obligation.prompt.includes(specification.title)));
302
+ if (!matches || obligation.status !== "pending") {
303
+ return obligation;
304
+ }
305
+ return {
306
+ ...obligation,
307
+ status: options.status ?? "satisfied",
308
+ updatedAt: new Date().toISOString(),
309
+ ...(options.questionId ? { questionId: options.questionId } : obligation.questionId ? { questionId: obligation.questionId } : {}),
310
+ ...(options.decisionId ? { decisionId: options.decisionId } : obligation.decisionId ? { decisionId: obligation.decisionId } : {})
311
+ };
312
+ })
313
+ };
314
+ }
231
315
  function interviewDepth(turns) {
232
316
  if (turns.some((turn) => turn.readyToSummarize === true && turn.quality !== "thin")) {
233
317
  return "ready_to_summarize";
@@ -772,11 +856,6 @@ async function markResearchSpecificationConfirmation(context, specification, ans
772
856
  status: researchSpecificationAnswerStatus(answer) === "deferred" ? "deferred" : "draft",
773
857
  updatedAt: timestamp
774
858
  };
775
- state.researchSpecification = confirmedSpecification;
776
- state.workingState = {
777
- ...state.workingState,
778
- researchSpecification: confirmedSpecification
779
- };
780
859
  state.hooks = (state.hooks ?? []).map((hook) => {
781
860
  if (hook.id !== specification.sourceHookId || !isInterviewHookRun(hook)) {
782
861
  return hook;
@@ -794,6 +873,37 @@ async function markResearchSpecificationConfirmation(context, specification, ans
794
873
  : hook.linkedDecisionRecordIds
795
874
  };
796
875
  });
876
+ const sourceEvidenceIds = (state.evidenceRecords ?? [])
877
+ .filter((record) => record.sourceHookId && record.sourceHookId === specification.sourceHookId)
878
+ .map((record) => record.id);
879
+ const audited = applyResearchSpecificationAuditUpdate(state, {
880
+ specification: confirmedSpecification,
881
+ timestamp,
882
+ source: "decision",
883
+ title: `Research Specification confirmation: ${confirmedSpecification.title}`,
884
+ rationale: `Research Specification confirmation answer: ${answer}`,
885
+ sourceEvidenceIds,
886
+ questionRecordId: questionId,
887
+ decisionRecordId: decisionId,
888
+ createDecisionRecord: false
889
+ });
890
+ const nextState = researchSpecificationAnswerConfirms(answer)
891
+ ? resolveResearchSpecificationConfirmationObligation(audited.state, confirmedSpecification, {
892
+ questionId,
893
+ decisionId,
894
+ status: "satisfied"
895
+ })
896
+ : researchSpecificationAnswerNeedsFollowUp(answer)
897
+ ? ensureResearchSpecificationConfirmationObligation(audited.state, confirmedSpecification, {
898
+ answer,
899
+ questionId,
900
+ decisionId
901
+ })
902
+ : resolveResearchSpecificationConfirmationObligation(audited.state, confirmedSpecification, {
903
+ questionId,
904
+ decisionId,
905
+ status: "cleared"
906
+ });
797
907
  const session = {
798
908
  ...context.session,
799
909
  researchSpecification: confirmedSpecification,
@@ -801,9 +911,9 @@ async function markResearchSpecificationConfirmation(context, specification, ans
801
911
  };
802
912
  context.session = session;
803
913
  await writeFile(context.sessionFilePath, JSON.stringify(session, null, 2), "utf8");
804
- await writeFile(context.stateFilePath, JSON.stringify(state, null, 2), "utf8");
914
+ await writeFile(context.stateFilePath, JSON.stringify(nextState, null, 2), "utf8");
805
915
  await syncCurrentWorkspaceView(context);
806
- return { state, session, specification: confirmedSpecification };
916
+ return { state: nextState, session, specification: confirmedSpecification };
807
917
  }
808
918
  async function markAlreadyConfirmedResearchSpecification(context, specification) {
809
919
  const state = asInterviewState(await loadWorkspaceState(context));
@@ -832,6 +942,9 @@ async function markAlreadyConfirmedResearchSpecification(context, specification)
832
942
  researchSpecification: confirmedSpecification
833
943
  };
834
944
  });
945
+ const nextState = resolveResearchSpecificationConfirmationObligation(state, confirmedSpecification, {
946
+ status: "satisfied"
947
+ });
835
948
  const session = {
836
949
  ...context.session,
837
950
  researchSpecification: confirmedSpecification,
@@ -839,9 +952,9 @@ async function markAlreadyConfirmedResearchSpecification(context, specification)
839
952
  };
840
953
  context.session = session;
841
954
  await writeFile(context.sessionFilePath, JSON.stringify(session, null, 2), "utf8");
842
- await writeFile(context.stateFilePath, JSON.stringify(state, null, 2), "utf8");
955
+ await writeFile(context.stateFilePath, JSON.stringify(nextState, null, 2), "utf8");
843
956
  await syncCurrentWorkspaceView(context);
844
- return { state, session, specification: confirmedSpecification };
957
+ return { state: nextState, session, specification: confirmedSpecification };
845
958
  }
846
959
  function statusForElicitationError(error) {
847
960
  const message = error instanceof Error ? error.message : String(error);
@@ -1132,6 +1245,126 @@ export function createLongTableMcpServer() {
1132
1245
  return errorResult(error instanceof Error ? error.message : String(error));
1133
1246
  }
1134
1247
  });
1248
+ server.registerTool("propose_research_spec_patch", {
1249
+ title: "Propose Research Specification Patch",
1250
+ description: "Store a reviewable Research Specification patch without applying it.",
1251
+ inputSchema: cwdSchema.extend({
1252
+ specification: researchSpecificationSchema,
1253
+ source: researchSpecificationPatchSourceSchema.default("manual"),
1254
+ rationale: z.string().optional(),
1255
+ sourceEvidenceIds: z.array(z.string()).optional()
1256
+ })
1257
+ }, async ({ cwd: inputCwd, specification, source, rationale, sourceEvidenceIds }) => {
1258
+ try {
1259
+ const context = await requireContext(inputCwd);
1260
+ const result = await proposeResearchSpecificationPatch({
1261
+ context,
1262
+ specification: specification,
1263
+ source,
1264
+ rationale,
1265
+ sourceEvidenceIds
1266
+ });
1267
+ return textResult({
1268
+ patch: result.patch,
1269
+ changes: result.changes,
1270
+ nextAction: `apply_research_spec_patch patchId=${result.patch.id}`
1271
+ });
1272
+ }
1273
+ catch (error) {
1274
+ return errorResult(error instanceof Error ? error.message : String(error));
1275
+ }
1276
+ });
1277
+ server.registerTool("apply_research_spec_patch", {
1278
+ title: "Apply Research Specification Patch",
1279
+ description: "Automatically apply a proposed or inline Research Specification update and record a revision.",
1280
+ inputSchema: cwdSchema.extend({
1281
+ patchId: z.string().optional(),
1282
+ specification: researchSpecificationSchema.optional(),
1283
+ source: researchSpecificationPatchSourceSchema.default("manual"),
1284
+ rationale: z.string().optional(),
1285
+ sourceEvidenceIds: z.array(z.string()).optional(),
1286
+ questionRecordId: z.string().optional(),
1287
+ decisionRecordId: z.string().optional()
1288
+ })
1289
+ }, async ({ cwd: inputCwd, patchId, specification, source, rationale, sourceEvidenceIds, questionRecordId, decisionRecordId }) => {
1290
+ try {
1291
+ const context = await requireContext(inputCwd);
1292
+ const result = await applyResearchSpecificationPatch({
1293
+ context,
1294
+ patchId,
1295
+ specification: specification,
1296
+ source,
1297
+ rationale,
1298
+ sourceEvidenceIds,
1299
+ questionRecordId,
1300
+ decisionRecordId
1301
+ });
1302
+ return textResult({
1303
+ patch: result.patch,
1304
+ revision: result.revision,
1305
+ specification: result.specification,
1306
+ decision: result.decision,
1307
+ session: {
1308
+ currentGoal: result.session.currentGoal,
1309
+ researchSpecification: result.session.researchSpecification
1310
+ }
1311
+ });
1312
+ }
1313
+ catch (error) {
1314
+ return errorResult(error instanceof Error ? error.message : String(error));
1315
+ }
1316
+ });
1317
+ server.registerTool("diff_research_specification", {
1318
+ title: "Diff Research Specification",
1319
+ description: "Compare an inline Research Specification against the current workspace specification without writing state.",
1320
+ inputSchema: cwdSchema.extend({
1321
+ specification: researchSpecificationSchema
1322
+ }),
1323
+ annotations: { readOnlyHint: true }
1324
+ }, async ({ cwd: inputCwd, specification }) => {
1325
+ try {
1326
+ const context = await requireContext(inputCwd);
1327
+ const state = asInterviewState(await loadWorkspaceState(context));
1328
+ const current = state.researchSpecification ?? context.session.researchSpecification;
1329
+ return textResult({
1330
+ current,
1331
+ changes: diffResearchSpecifications(current, specification)
1332
+ });
1333
+ }
1334
+ catch (error) {
1335
+ return errorResult(error instanceof Error ? error.message : String(error));
1336
+ }
1337
+ });
1338
+ server.registerTool("read_research_spec_history", {
1339
+ title: "Read Research Specification History",
1340
+ description: "Read specification revisions, patches, and evidence records for audit or resume.",
1341
+ inputSchema: cwdSchema,
1342
+ annotations: { readOnlyHint: true }
1343
+ }, async ({ cwd: inputCwd }) => {
1344
+ try {
1345
+ const context = await requireContext(inputCwd);
1346
+ return textResult(await readResearchSpecificationHistory(context));
1347
+ }
1348
+ catch (error) {
1349
+ return errorResult(error instanceof Error ? error.message : String(error));
1350
+ }
1351
+ });
1352
+ server.registerTool("find_unincorporated_evidence", {
1353
+ title: "Find Unincorporated Research Evidence",
1354
+ description: "List interview, panel, critic, reviewer, or invocation evidence not yet incorporated into a Research Specification revision.",
1355
+ inputSchema: cwdSchema,
1356
+ annotations: { readOnlyHint: true }
1357
+ }, async ({ cwd: inputCwd }) => {
1358
+ try {
1359
+ const context = await requireContext(inputCwd);
1360
+ return textResult({
1361
+ evidenceRecords: await findUnincorporatedResearchEvidence(context)
1362
+ });
1363
+ }
1364
+ catch (error) {
1365
+ return errorResult(error instanceof Error ? error.message : String(error));
1366
+ }
1367
+ });
1135
1368
  server.registerTool("cancel_interview", {
1136
1369
  title: "Cancel LongTable Interview",
1137
1370
  description: "Explicitly cancel the active $longtable-interview hook without confirming a First Research Shape.",
@@ -1428,9 +1661,11 @@ export function createLongTableMcpServer() {
1428
1661
  options: z.array(questionOptionSchema).optional(),
1429
1662
  displayReason: z.string().optional(),
1430
1663
  provider: z.enum(["codex", "claude"]).optional(),
1431
- required: z.boolean().optional()
1664
+ required: z.boolean().optional(),
1665
+ commitmentFamily: commitmentFamilySchema.optional(),
1666
+ epistemicBasis: epistemicBasisSchema.optional()
1432
1667
  })
1433
- }, async ({ cwd: inputCwd, prompt, title, question, checkpointKey, options, displayReason, provider, required }) => {
1668
+ }, async ({ cwd: inputCwd, prompt, title, question, checkpointKey, options, displayReason, provider, required, commitmentFamily, epistemicBasis }) => {
1434
1669
  try {
1435
1670
  const context = await requireContext(inputCwd);
1436
1671
  const result = await createWorkspaceQuestion({
@@ -1442,7 +1677,9 @@ export function createLongTableMcpServer() {
1442
1677
  questionOptions: options,
1443
1678
  displayReason,
1444
1679
  provider,
1445
- required
1680
+ required,
1681
+ commitmentFamily: commitmentFamily,
1682
+ epistemicBasis: epistemicBasis
1446
1683
  });
1447
1684
  return textResult({
1448
1685
  question: result.question,
@@ -1465,9 +1702,11 @@ export function createLongTableMcpServer() {
1465
1702
  displayReason: z.string().optional(),
1466
1703
  provider: z.enum(["codex", "claude"]).default("codex"),
1467
1704
  required: z.boolean().optional(),
1705
+ commitmentFamily: commitmentFamilySchema.optional(),
1706
+ epistemicBasis: epistemicBasisSchema.optional(),
1468
1707
  fallbackOnly: z.boolean().default(false).describe("Create and render the checkpoint without calling MCP elicitation.")
1469
1708
  })
1470
- }, async ({ cwd: inputCwd, prompt, title, question, checkpointKey, options, displayReason, provider, required, fallbackOnly }) => {
1709
+ }, async ({ cwd: inputCwd, prompt, title, question, checkpointKey, options, displayReason, provider, required, commitmentFamily, epistemicBasis, fallbackOnly }) => {
1471
1710
  try {
1472
1711
  const context = await requireContext(inputCwd);
1473
1712
  const created = await createWorkspaceQuestion({
@@ -1479,7 +1718,9 @@ export function createLongTableMcpServer() {
1479
1718
  questionOptions: options,
1480
1719
  displayReason,
1481
1720
  provider,
1482
- required
1721
+ required,
1722
+ commitmentFamily: commitmentFamily,
1723
+ epistemicBasis: epistemicBasis
1483
1724
  });
1484
1725
  const fallback = renderQuestionFallback(created.question, provider);
1485
1726
  if (fallbackOnly) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@longtable/mcp",
3
- "version": "0.1.45",
3
+ "version": "0.1.48",
4
4
  "private": false,
5
5
  "description": "LongTable MCP transport for workspace state and Researcher Checkpoints",
6
6
  "type": "module",
@@ -26,12 +26,12 @@
26
26
  "self-test": "node ./dist/server.js --self-test"
27
27
  },
28
28
  "dependencies": {
29
- "@longtable/checkpoints": "0.1.45",
30
- "@longtable/cli": "0.1.45",
31
- "@longtable/core": "0.1.45",
32
- "@longtable/provider-claude": "0.1.45",
33
- "@longtable/provider-codex": "0.1.45",
34
- "@longtable/setup": "0.1.45",
29
+ "@longtable/checkpoints": "0.1.48",
30
+ "@longtable/cli": "0.1.48",
31
+ "@longtable/core": "0.1.48",
32
+ "@longtable/provider-claude": "0.1.48",
33
+ "@longtable/provider-codex": "0.1.48",
34
+ "@longtable/setup": "0.1.48",
35
35
  "@modelcontextprotocol/sdk": "^1.29.0",
36
36
  "zod": "^4.0.0"
37
37
  },