@longtable/mcp 0.1.41 → 0.1.42

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
@@ -39,6 +39,7 @@ Provider guidance should use interview tools for `$longtable-interview`:
39
39
  - `begin_interview`
40
40
  - `append_interview_turn`
41
41
  - `summarize_interview`
42
+ - `cancel_interview`
42
43
  - `confirm_first_research_shape`
43
44
 
44
45
  For later Researcher Checkpoints, provider guidance should use
@@ -1,4 +1,7 @@
1
1
  function questionTitle(shape) {
2
+ if (usesKorean(shape)) {
3
+ return "First Research Shape 확인";
4
+ }
2
5
  if (shape.protectedDecision) {
3
6
  return "Protected Research Decision";
4
7
  }
@@ -7,7 +10,22 @@ function questionTitle(shape) {
7
10
  }
8
11
  return "Emerging Research Direction";
9
12
  }
13
+ function usesKorean(shape) {
14
+ return [
15
+ shape.handle,
16
+ shape.currentGoal,
17
+ shape.currentBlocker,
18
+ shape.researchObject,
19
+ shape.gapRisk,
20
+ shape.protectedDecision,
21
+ ...shape.openQuestions,
22
+ shape.nextAction
23
+ ].some((value) => typeof value === "string" && /[ㄱ-ㅎㅏ-ㅣ가-힣]/.test(value));
24
+ }
10
25
  function questionText(shape) {
26
+ if (usesKorean(shape)) {
27
+ return "이 First Research Shape를 어떻게 처리할까요?";
28
+ }
11
29
  if (shape.protectedDecision) {
12
30
  return "Before LongTable moves forward, what protected research decision should stay explicit?";
13
31
  }
@@ -20,6 +38,18 @@ function questionText(shape) {
20
38
  return "How should LongTable carry this emerging study forward?";
21
39
  }
22
40
  function displayReason(shape) {
41
+ if (usesKorean(shape)) {
42
+ if (shape.protectedDecision) {
43
+ return `인터뷰에서 보호해야 할 연구 판단이 드러났습니다: ${shape.protectedDecision}. LongTable은 이 판단이 조용히 확정되지 않게 해야 합니다.`;
44
+ }
45
+ if (shape.currentBlocker) {
46
+ return `주요 blocker가 아직 열려 있습니다: ${shape.currentBlocker}. 다음 행동은 이 불확실성을 명시적으로 다뤄야 합니다.`;
47
+ }
48
+ if (shape.openQuestions.length > 0) {
49
+ return "인터뷰에서 열린 연구 질문이 드러났습니다. 가장 중요한 질문을 보존하는 다음 행동을 선택해야 합니다.";
50
+ }
51
+ return "LongTable은 provisional research direction을 제안할 만큼의 맥락을 얻었지만, 다음 연구 행동은 명시적으로 선택되어야 합니다.";
52
+ }
23
53
  if (shape.protectedDecision) {
24
54
  return `The interview surfaced a protected decision: ${shape.protectedDecision}. LongTable should not let it settle silently.`;
25
55
  }
@@ -46,47 +76,59 @@ function recommendedValue(shape) {
46
76
  function baseOptions(shape) {
47
77
  const recommended = recommendedValue(shape);
48
78
  const options = [];
79
+ const korean = usesKorean(shape);
49
80
  if (shape.protectedDecision) {
50
81
  options.push({
51
82
  value: "protect_decision",
52
- label: "Keep the protected decision open",
53
- description: `Treat this as the guarded judgment while the broader direction stays provisional: ${shape.protectedDecision}`,
83
+ label: korean ? "보호 결정 열어두기" : "Keep the protected decision open",
84
+ description: korean
85
+ ? `전체 방향은 provisional로 두고, 이 판단을 보호된 결정으로 유지합니다: ${shape.protectedDecision}`
86
+ : `Treat this as the guarded judgment while the broader direction stays provisional: ${shape.protectedDecision}`,
54
87
  recommended: recommended === "protect_decision"
55
88
  });
56
89
  }
57
90
  options.push({
58
91
  value: "stabilize_shape",
59
- label: "Use this as the provisional direction",
60
- description: "Carry this research shape forward as the current working direction.",
92
+ label: korean ? "저장/확정" : "Use this as the provisional direction",
93
+ description: korean
94
+ ? "이 research shape를 현재 작업 방향으로 저장하고 이어갑니다."
95
+ : "Carry this research shape forward as the current working direction.",
61
96
  recommended: recommended === "stabilize_shape"
62
97
  }, {
63
98
  value: "gather_context",
64
- label: "Gather one more concrete case",
65
- description: "Ask for one more scene, source, dataset, or example before stabilizing it.",
99
+ label: korean ? "한 질문 더" : "Gather one more concrete case",
100
+ description: korean
101
+ ? "저장하기 전에 한 가지 장면, 출처, 데이터셋, 사례, 또는 결정 맥락을 더 묻습니다."
102
+ : "Ask for one more scene, source, dataset, or example before stabilizing it.",
66
103
  recommended: recommended === "gather_context"
67
104
  }, {
68
105
  value: "revise_shape",
69
- label: "Revise the emerging shape",
70
- description: "Change the handle, blocker, or next action before LongTable treats it as usable."
106
+ label: korean ? "수정" : "Revise the emerging shape",
107
+ description: korean
108
+ ? "LongTable이 이를 usable한 방향으로 다루기 전에 handle, blocker, next action 등을 수정합니다."
109
+ : "Change the handle, blocker, or next action before LongTable treats it as usable."
71
110
  });
72
111
  if (!shape.protectedDecision) {
73
112
  options.push({
74
113
  value: "keep_open",
75
- label: "Keep the study open longer",
76
- description: "Do not stabilize this direction yet."
114
+ label: korean ? "열어두기" : "Keep the study open longer",
115
+ description: korean ? "아직 이 방향을 확정하지 않습니다." : "Do not stabilize this direction yet."
77
116
  });
78
117
  }
79
118
  return options;
80
119
  }
81
120
  export function buildFirstResearchShapeQuestion(shape) {
121
+ const korean = usesKorean(shape);
82
122
  const summaryLines = [
83
- "LongTable has enough context to propose a provisional research direction.",
84
- `Handle: ${shape.handle}`,
85
- `Goal: ${shape.currentGoal}`,
86
- shape.currentBlocker ? `Blocker: ${shape.currentBlocker}` : undefined,
87
- shape.protectedDecision ? `Protected decision: ${shape.protectedDecision}` : undefined,
88
- shape.openQuestions.length > 0 ? `Open question: ${shape.openQuestions[0]}` : undefined,
89
- `Next action: ${shape.nextAction}`
123
+ korean
124
+ ? "LongTable이 provisional research direction을 제안할 만큼의 맥락을 얻었습니다."
125
+ : "LongTable has enough context to propose a provisional research direction.",
126
+ `${korean ? "핸들" : "Handle"}: ${shape.handle}`,
127
+ `${korean ? "목표" : "Goal"}: ${shape.currentGoal}`,
128
+ shape.currentBlocker ? `${korean ? "막힌 지점" : "Blocker"}: ${shape.currentBlocker}` : undefined,
129
+ shape.protectedDecision ? `${korean ? "보호할 결정" : "Protected decision"}: ${shape.protectedDecision}` : undefined,
130
+ shape.openQuestions.length > 0 ? `${korean ? "열린 질문" : "Open question"}: ${shape.openQuestions[0]}` : undefined,
131
+ `${korean ? "다음 행동" : "Next action"}: ${shape.nextAction}`
90
132
  ].filter(Boolean);
91
133
  return {
92
134
  prompt: summaryLines.join("\n"),
package/dist/server.js CHANGED
@@ -24,6 +24,7 @@ const TOOL_NAMES = [
24
24
  "begin_interview",
25
25
  "append_interview_turn",
26
26
  "summarize_interview",
27
+ "cancel_interview",
27
28
  "confirm_first_research_shape",
28
29
  "pending_questions",
29
30
  "evaluate_checkpoint",
@@ -171,9 +172,10 @@ function resolveFirstResearchShapeObligation(state, options) {
171
172
  };
172
173
  }
173
174
  function interviewDepth(turns) {
174
- const usableTurns = turns.filter((turn) => turn.quality !== "thin").length;
175
- if (usableTurns >= 3)
175
+ if (turns.some((turn) => turn.readyToSummarize === true && turn.quality !== "thin")) {
176
176
  return "ready_to_summarize";
177
+ }
178
+ const usableTurns = turns.filter((turn) => turn.quality !== "thin").length;
177
179
  if (usableTurns >= 1)
178
180
  return "forming_first_handle";
179
181
  return "gathering_context";
@@ -252,6 +254,10 @@ async function appendInterviewTurn(context, options) {
252
254
  const followUpQuestion = needsFollowUp
253
255
  ? options.followUpQuestion ?? defaultFollowUpQuestion(options.answer)
254
256
  : options.followUpQuestion;
257
+ const readyToSummarize = options.readyToSummarize === true && quality !== "thin";
258
+ const readinessRationale = options.readinessRationale
259
+ ?.map((rationale) => rationale.trim())
260
+ .filter(Boolean);
255
261
  const timestamp = new Date().toISOString();
256
262
  const turn = {
257
263
  id: createId("interview_turn"),
@@ -263,6 +269,8 @@ async function appendInterviewTurn(context, options) {
263
269
  quality,
264
270
  needsFollowUp,
265
271
  ...(followUpQuestion?.trim() ? { followUpQuestion: followUpQuestion.trim() } : {}),
272
+ ...(readyToSummarize ? { readyToSummarize } : {}),
273
+ ...(readinessRationale && readinessRationale.length > 0 ? { readinessRationale } : {}),
266
274
  ...(options.rationale && options.rationale.length > 0 ? { rationale: options.rationale } : {})
267
275
  };
268
276
  const turns = [...existing.turns, turn];
@@ -275,7 +283,10 @@ async function appendInterviewTurn(context, options) {
275
283
  turns,
276
284
  qualityNotes: [
277
285
  ...existing.qualityNotes,
278
- ...(needsFollowUp ? [`Turn ${turn.index} needs follow-up: ${followUpQuestion}`] : [])
286
+ ...(needsFollowUp ? [`Turn ${turn.index} needs follow-up: ${followUpQuestion}`] : []),
287
+ ...(readyToSummarize
288
+ ? [`Turn ${turn.index} marked ready to summarize: ${(readinessRationale ?? ["content-based readiness signal"]).join("; ")}`]
289
+ : [])
279
290
  ]
280
291
  };
281
292
  const updated = upsertInterviewHook(state, hook);
@@ -349,6 +360,53 @@ async function summarizeInterviewHook(context, options) {
349
360
  await syncCurrentWorkspaceView(context);
350
361
  return { hook, shape, state: updated, session };
351
362
  }
363
+ async function cancelInterviewHook(context, options) {
364
+ const state = asInterviewState(await loadWorkspaceState(context));
365
+ const existing = activeInterviewHook(state, options.hookId);
366
+ if (!existing) {
367
+ throw new Error("No active LongTable interview hook was found to cancel.");
368
+ }
369
+ const timestamp = new Date().toISOString();
370
+ const hook = {
371
+ ...existing,
372
+ status: "cancelled",
373
+ updatedAt: timestamp,
374
+ rationale: [
375
+ ...existing.rationale,
376
+ options.reason?.trim()
377
+ ? `Interview cancelled explicitly: ${options.reason.trim()}`
378
+ : "Interview cancelled explicitly by the researcher."
379
+ ]
380
+ };
381
+ const updated = upsertInterviewHook(state, hook);
382
+ const workingState = {
383
+ ...(updated.workingState ?? {})
384
+ };
385
+ if (workingState.activeInterviewHookId === existing.id) {
386
+ delete workingState.activeInterviewHookId;
387
+ delete workingState.interviewSurface;
388
+ delete workingState.interviewOpeningQuestion;
389
+ delete workingState.interviewSeedAnswer;
390
+ }
391
+ updated.workingState = workingState;
392
+ updated.narrativeTraces = [
393
+ ...(updated.narrativeTraces ?? []),
394
+ {
395
+ id: createId("narrative_trace"),
396
+ timestamp,
397
+ source: "$longtable-interview",
398
+ traceType: "judgment",
399
+ summary: options.reason?.trim()
400
+ ? `LongTable interview cancelled: ${options.reason.trim()}.`
401
+ : "LongTable interview cancelled before First Research Shape confirmation.",
402
+ visibility: "explicit",
403
+ importance: "low"
404
+ }
405
+ ];
406
+ await writeFile(context.stateFilePath, JSON.stringify(updated, null, 2), "utf8");
407
+ await syncCurrentWorkspaceView(context);
408
+ return { hook, state: updated };
409
+ }
352
410
  function findQuestion(records, questionId) {
353
411
  if (questionId) {
354
412
  return records.find((record) => record.id === questionId) ?? null;
@@ -711,9 +769,11 @@ export function createLongTableMcpServer() {
711
769
  quality: z.enum(["thin", "usable", "rich"]).optional(),
712
770
  needsFollowUp: z.boolean().optional(),
713
771
  followUpQuestion: z.string().optional(),
772
+ readyToSummarize: z.boolean().optional().describe("Set true only when the interview has content-based closure readiness; never infer this from turn count alone."),
773
+ readinessRationale: z.array(z.string()).optional().describe("Brief evidence that the interview has enough context for a First Research Shape."),
714
774
  rationale: z.array(z.string()).optional()
715
775
  })
716
- }, async ({ cwd: inputCwd, hookId, question, answer, reflection, quality, needsFollowUp, followUpQuestion, rationale }) => {
776
+ }, async ({ cwd: inputCwd, hookId, question, answer, reflection, quality, needsFollowUp, followUpQuestion, readyToSummarize, readinessRationale, rationale }) => {
717
777
  try {
718
778
  const context = await requireContext(inputCwd);
719
779
  const result = await appendInterviewTurn(context, {
@@ -724,6 +784,8 @@ export function createLongTableMcpServer() {
724
784
  quality: quality,
725
785
  needsFollowUp,
726
786
  followUpQuestion,
787
+ readyToSummarize,
788
+ readinessRationale,
727
789
  rationale
728
790
  });
729
791
  return textResult(result);
@@ -761,6 +823,26 @@ export function createLongTableMcpServer() {
761
823
  return errorResult(error instanceof Error ? error.message : String(error));
762
824
  }
763
825
  });
826
+ server.registerTool("cancel_interview", {
827
+ title: "Cancel LongTable Interview",
828
+ description: "Explicitly cancel the active $longtable-interview hook without confirming a First Research Shape.",
829
+ inputSchema: cwdSchema.extend({
830
+ hookId: z.string().optional(),
831
+ reason: z.string().optional()
832
+ })
833
+ }, async ({ cwd: inputCwd, hookId, reason }) => {
834
+ try {
835
+ const context = await requireContext(inputCwd);
836
+ const result = await cancelInterviewHook(context, { hookId, reason });
837
+ return textResult({
838
+ hook: compactInterviewHook(result.hook),
839
+ cancelled: true
840
+ });
841
+ }
842
+ catch (error) {
843
+ return errorResult(error instanceof Error ? error.message : String(error));
844
+ }
845
+ });
764
846
  server.registerTool("confirm_first_research_shape", {
765
847
  title: "Confirm First Research Shape",
766
848
  description: "Use MCP form elicitation to confirm, revise, defer, or request more context for the First Research Shape.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@longtable/mcp",
3
- "version": "0.1.41",
3
+ "version": "0.1.42",
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.41",
30
- "@longtable/cli": "0.1.41",
31
- "@longtable/core": "0.1.41",
32
- "@longtable/provider-claude": "0.1.41",
33
- "@longtable/provider-codex": "0.1.41",
34
- "@longtable/setup": "0.1.41",
29
+ "@longtable/checkpoints": "0.1.42",
30
+ "@longtable/cli": "0.1.42",
31
+ "@longtable/core": "0.1.42",
32
+ "@longtable/provider-claude": "0.1.42",
33
+ "@longtable/provider-codex": "0.1.42",
34
+ "@longtable/setup": "0.1.42",
35
35
  "@modelcontextprotocol/sdk": "^1.29.0",
36
36
  "zod": "^4.0.0"
37
37
  },