@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 +1 -0
- package/dist/first-research-shape.js +59 -17
- package/dist/server.js +86 -4
- package/package.json +7 -7
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
shape.
|
|
89
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
30
|
-
"@longtable/cli": "0.1.
|
|
31
|
-
"@longtable/core": "0.1.
|
|
32
|
-
"@longtable/provider-claude": "0.1.
|
|
33
|
-
"@longtable/provider-codex": "0.1.
|
|
34
|
-
"@longtable/setup": "0.1.
|
|
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
|
},
|