@longtable/mcp 0.1.43 → 0.1.44
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/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/research-specification.d.ts +65 -0
- package/dist/research-specification.js +108 -0
- package/dist/server.js +421 -0
- package/package.json +7 -7
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { QuestionOption } from "@longtable/core";
|
|
2
|
+
export interface ResearchSpecification {
|
|
3
|
+
title: string;
|
|
4
|
+
status?: "draft" | "confirmed" | "deferred";
|
|
5
|
+
createdAt?: string;
|
|
6
|
+
updatedAt?: string;
|
|
7
|
+
sourceHookId?: string;
|
|
8
|
+
researchDirection: {
|
|
9
|
+
question?: string;
|
|
10
|
+
purpose: string;
|
|
11
|
+
scopeBoundary?: string;
|
|
12
|
+
inclusionCriteria?: string[];
|
|
13
|
+
exclusionCriteria?: string[];
|
|
14
|
+
};
|
|
15
|
+
constructOntology: {
|
|
16
|
+
coreConstructs: string[];
|
|
17
|
+
distinctions: string[];
|
|
18
|
+
termsToAvoidCollapsing?: string[];
|
|
19
|
+
};
|
|
20
|
+
theoryAndFraming: {
|
|
21
|
+
anchors: string[];
|
|
22
|
+
alternatives?: string[];
|
|
23
|
+
overreachRisks?: string[];
|
|
24
|
+
};
|
|
25
|
+
measurementCoding: {
|
|
26
|
+
variablesOrConstructs: string[];
|
|
27
|
+
evidenceTypes: string[];
|
|
28
|
+
codingRules: string[];
|
|
29
|
+
openStandards?: string[];
|
|
30
|
+
};
|
|
31
|
+
methodAnalysis: {
|
|
32
|
+
design?: string;
|
|
33
|
+
analysisOptions: string[];
|
|
34
|
+
dataSufficiencyCriteria?: string[];
|
|
35
|
+
unsettledChoices?: string[];
|
|
36
|
+
};
|
|
37
|
+
evidenceAccess: {
|
|
38
|
+
requiredSources?: string[];
|
|
39
|
+
accessRequirements?: string[];
|
|
40
|
+
evidenceStandards?: string[];
|
|
41
|
+
};
|
|
42
|
+
epistemicAlignment: {
|
|
43
|
+
researcherKnowledge?: string[];
|
|
44
|
+
projectStatePriority?: string[];
|
|
45
|
+
aiInferenceLimits?: string[];
|
|
46
|
+
conflictResolutionRule?: string;
|
|
47
|
+
};
|
|
48
|
+
protectedDecisions: string[];
|
|
49
|
+
openQuestions: string[];
|
|
50
|
+
nextActions: string[];
|
|
51
|
+
confidence: "low" | "medium" | "high";
|
|
52
|
+
confirmedAt?: string;
|
|
53
|
+
}
|
|
54
|
+
export interface ResearchSpecificationQuestionSpec {
|
|
55
|
+
prompt: string;
|
|
56
|
+
title: string;
|
|
57
|
+
question: string;
|
|
58
|
+
checkpointKey: string;
|
|
59
|
+
options: QuestionOption[];
|
|
60
|
+
displayReason: string;
|
|
61
|
+
}
|
|
62
|
+
export declare function renderResearchSpecificationPreview(specification: ResearchSpecification): string;
|
|
63
|
+
export declare function buildResearchSpecificationQuestion(specification: ResearchSpecification): ResearchSpecificationQuestionSpec;
|
|
64
|
+
export declare function researchSpecificationAnswerConfirms(answer: string): boolean;
|
|
65
|
+
export declare function researchSpecificationAnswerStatus(answer: string): "confirmed" | "active" | "deferred";
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
function compactList(values, limit = 3) {
|
|
2
|
+
const kept = (values ?? []).map((value) => value.trim()).filter(Boolean).slice(0, limit);
|
|
3
|
+
return kept.length > 0 ? kept.join("; ") : "not specified";
|
|
4
|
+
}
|
|
5
|
+
function usesKorean(specification) {
|
|
6
|
+
const values = [
|
|
7
|
+
specification.title,
|
|
8
|
+
specification.researchDirection.question,
|
|
9
|
+
specification.researchDirection.purpose,
|
|
10
|
+
specification.researchDirection.scopeBoundary,
|
|
11
|
+
...specification.constructOntology.coreConstructs,
|
|
12
|
+
...specification.constructOntology.distinctions,
|
|
13
|
+
...specification.theoryAndFraming.anchors,
|
|
14
|
+
...specification.measurementCoding.codingRules,
|
|
15
|
+
...specification.methodAnalysis.analysisOptions,
|
|
16
|
+
specification.epistemicAlignment.conflictResolutionRule,
|
|
17
|
+
...specification.protectedDecisions,
|
|
18
|
+
...specification.openQuestions,
|
|
19
|
+
...specification.nextActions
|
|
20
|
+
];
|
|
21
|
+
return values.some((value) => typeof value === "string" && /[ㄱ-ㅎㅏ-ㅣ가-힣]/.test(value));
|
|
22
|
+
}
|
|
23
|
+
export function renderResearchSpecificationPreview(specification) {
|
|
24
|
+
const korean = usesKorean(specification);
|
|
25
|
+
const lines = [
|
|
26
|
+
korean ? "Research Specification Preview" : "Research Specification Preview",
|
|
27
|
+
`${korean ? "제목" : "Title"}: ${specification.title}`,
|
|
28
|
+
`${korean ? "목적" : "Purpose"}: ${specification.researchDirection.purpose}`,
|
|
29
|
+
specification.researchDirection.scopeBoundary
|
|
30
|
+
? `${korean ? "범위" : "Scope"}: ${specification.researchDirection.scopeBoundary}`
|
|
31
|
+
: undefined,
|
|
32
|
+
`${korean ? "핵심 construct" : "Core constructs"}: ${compactList(specification.constructOntology.coreConstructs)}`,
|
|
33
|
+
`${korean ? "구분" : "Distinctions"}: ${compactList(specification.constructOntology.distinctions)}`,
|
|
34
|
+
`${korean ? "이론 앵커" : "Theory anchors"}: ${compactList(specification.theoryAndFraming.anchors)}`,
|
|
35
|
+
`${korean ? "코딩 규칙" : "Coding rules"}: ${compactList(specification.measurementCoding.codingRules)}`,
|
|
36
|
+
`${korean ? "분석 옵션" : "Analysis options"}: ${compactList(specification.methodAnalysis.analysisOptions)}`,
|
|
37
|
+
specification.epistemicAlignment.conflictResolutionRule
|
|
38
|
+
? `${korean ? "충돌 조정" : "Conflict rule"}: ${specification.epistemicAlignment.conflictResolutionRule}`
|
|
39
|
+
: undefined,
|
|
40
|
+
`${korean ? "열린 질문" : "Open questions"}: ${compactList(specification.openQuestions, 2)}`,
|
|
41
|
+
`${korean ? "다음 행동" : "Next actions"}: ${compactList(specification.nextActions, 2)}`,
|
|
42
|
+
`${korean ? "신뢰도" : "Confidence"}: ${specification.confidence}`
|
|
43
|
+
].filter(Boolean);
|
|
44
|
+
return lines.join("\n");
|
|
45
|
+
}
|
|
46
|
+
function baseOptions(specification) {
|
|
47
|
+
const korean = usesKorean(specification);
|
|
48
|
+
return [
|
|
49
|
+
{
|
|
50
|
+
value: "confirm_specification",
|
|
51
|
+
label: korean ? "저장/확정" : "Confirm and save",
|
|
52
|
+
description: korean
|
|
53
|
+
? "이 Research Specification을 현재 연구 명세로 저장하고 이어갑니다."
|
|
54
|
+
: "Save this Research Specification as the current research specification.",
|
|
55
|
+
recommended: specification.confidence !== "low"
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
value: "ask_one_more",
|
|
59
|
+
label: korean ? "한 질문 더" : "Ask one more question",
|
|
60
|
+
description: korean
|
|
61
|
+
? "저장하기 전에 가장 위험한 빈칸 하나를 더 묻습니다."
|
|
62
|
+
: "Ask one more high-risk clarification before saving it.",
|
|
63
|
+
recommended: specification.confidence === "low"
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
value: "revise_section",
|
|
67
|
+
label: korean ? "섹션 수정" : "Revise a section",
|
|
68
|
+
description: korean
|
|
69
|
+
? "범위, construct, 이론, 코딩, 방법, 접근, 정렬 중 특정 섹션을 수정합니다."
|
|
70
|
+
: "Revise one section such as scope, constructs, theory, coding, method, access, or alignment."
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
value: "keep_open",
|
|
74
|
+
label: korean ? "열어두기" : "Keep open",
|
|
75
|
+
description: korean
|
|
76
|
+
? "아직 이 명세를 확정하지 않고 draft로 둡니다."
|
|
77
|
+
: "Keep this specification as a draft instead of confirming it."
|
|
78
|
+
}
|
|
79
|
+
];
|
|
80
|
+
}
|
|
81
|
+
export function buildResearchSpecificationQuestion(specification) {
|
|
82
|
+
const korean = usesKorean(specification);
|
|
83
|
+
const preview = renderResearchSpecificationPreview(specification);
|
|
84
|
+
return {
|
|
85
|
+
prompt: preview,
|
|
86
|
+
title: korean ? "Research Specification 확인" : "Research Specification Confirmation",
|
|
87
|
+
question: korean
|
|
88
|
+
? "이 Research Specification을 어떻게 처리할까요?"
|
|
89
|
+
: "How should LongTable handle this Research Specification?",
|
|
90
|
+
checkpointKey: "research_specification_confirmation",
|
|
91
|
+
options: baseOptions(specification),
|
|
92
|
+
displayReason: korean
|
|
93
|
+
? `${preview}\n\n인터뷰가 단순한 방향 요약을 넘어 연구 명세를 만들 만큼 구체화되었습니다. 저장 전에 범위, construct, 이론, 코딩, 방법, 접근, 정렬을 명시적으로 확인해야 합니다.`
|
|
94
|
+
: `${preview}\n\nThe interview has moved beyond a short direction summary into a research specification. Scope, constructs, theory, coding, method, access, and epistemic alignment should be explicit before saving.`
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
export function researchSpecificationAnswerConfirms(answer) {
|
|
98
|
+
return answer === "confirm_specification";
|
|
99
|
+
}
|
|
100
|
+
export function researchSpecificationAnswerStatus(answer) {
|
|
101
|
+
if (researchSpecificationAnswerConfirms(answer)) {
|
|
102
|
+
return "confirmed";
|
|
103
|
+
}
|
|
104
|
+
if (answer === "keep_open") {
|
|
105
|
+
return "deferred";
|
|
106
|
+
}
|
|
107
|
+
return "active";
|
|
108
|
+
}
|
package/dist/server.js
CHANGED
|
@@ -13,6 +13,7 @@ import { renderQuestionRecordPrompt } from "@longtable/provider-codex";
|
|
|
13
13
|
import { loadSetupOutput } from "@longtable/setup";
|
|
14
14
|
import { answerWorkspaceQuestion, clearWorkspaceQuestion, createOrUpdateProjectWorkspace, createWorkspaceQuestion, inspectProjectWorkspace, loadProjectContextFromDirectory, loadWorkspaceState, 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
17
|
const SERVER_NAME = "longtable-state";
|
|
17
18
|
const require = createRequire(import.meta.url);
|
|
18
19
|
const SERVER_VERSION = String(require("../package.json").version ?? "0.0.0");
|
|
@@ -24,8 +25,11 @@ const TOOL_NAMES = [
|
|
|
24
25
|
"begin_interview",
|
|
25
26
|
"append_interview_turn",
|
|
26
27
|
"summarize_interview",
|
|
28
|
+
"summarize_research_specification",
|
|
29
|
+
"read_research_specification",
|
|
27
30
|
"cancel_interview",
|
|
28
31
|
"confirm_first_research_shape",
|
|
32
|
+
"confirm_research_specification",
|
|
29
33
|
"pending_questions",
|
|
30
34
|
"evaluate_checkpoint",
|
|
31
35
|
"create_question",
|
|
@@ -56,6 +60,59 @@ const firstResearchShapeSchema = z.object({
|
|
|
56
60
|
sourceHookId: z.string().optional(),
|
|
57
61
|
confirmedAt: z.string().optional()
|
|
58
62
|
});
|
|
63
|
+
const optionalStringArraySchema = z.array(z.string().min(1)).optional();
|
|
64
|
+
const researchSpecificationSchema = z.object({
|
|
65
|
+
title: z.string().min(1),
|
|
66
|
+
status: z.enum(["draft", "confirmed", "deferred"]).optional(),
|
|
67
|
+
createdAt: z.string().optional(),
|
|
68
|
+
updatedAt: z.string().optional(),
|
|
69
|
+
sourceHookId: z.string().optional(),
|
|
70
|
+
researchDirection: z.object({
|
|
71
|
+
question: z.string().optional(),
|
|
72
|
+
purpose: z.string().min(1),
|
|
73
|
+
scopeBoundary: z.string().optional(),
|
|
74
|
+
inclusionCriteria: optionalStringArraySchema,
|
|
75
|
+
exclusionCriteria: optionalStringArraySchema
|
|
76
|
+
}),
|
|
77
|
+
constructOntology: z.object({
|
|
78
|
+
coreConstructs: z.array(z.string().min(1)).default([]),
|
|
79
|
+
distinctions: z.array(z.string().min(1)).default([]),
|
|
80
|
+
termsToAvoidCollapsing: optionalStringArraySchema
|
|
81
|
+
}),
|
|
82
|
+
theoryAndFraming: z.object({
|
|
83
|
+
anchors: z.array(z.string().min(1)).default([]),
|
|
84
|
+
alternatives: optionalStringArraySchema,
|
|
85
|
+
overreachRisks: optionalStringArraySchema
|
|
86
|
+
}),
|
|
87
|
+
measurementCoding: z.object({
|
|
88
|
+
variablesOrConstructs: z.array(z.string().min(1)).default([]),
|
|
89
|
+
evidenceTypes: z.array(z.string().min(1)).default([]),
|
|
90
|
+
codingRules: z.array(z.string().min(1)).default([]),
|
|
91
|
+
openStandards: optionalStringArraySchema
|
|
92
|
+
}),
|
|
93
|
+
methodAnalysis: z.object({
|
|
94
|
+
design: z.string().optional(),
|
|
95
|
+
analysisOptions: z.array(z.string().min(1)).default([]),
|
|
96
|
+
dataSufficiencyCriteria: optionalStringArraySchema,
|
|
97
|
+
unsettledChoices: optionalStringArraySchema
|
|
98
|
+
}),
|
|
99
|
+
evidenceAccess: z.object({
|
|
100
|
+
requiredSources: optionalStringArraySchema,
|
|
101
|
+
accessRequirements: optionalStringArraySchema,
|
|
102
|
+
evidenceStandards: optionalStringArraySchema
|
|
103
|
+
}),
|
|
104
|
+
epistemicAlignment: z.object({
|
|
105
|
+
researcherKnowledge: optionalStringArraySchema,
|
|
106
|
+
projectStatePriority: optionalStringArraySchema,
|
|
107
|
+
aiInferenceLimits: optionalStringArraySchema,
|
|
108
|
+
conflictResolutionRule: z.string().optional()
|
|
109
|
+
}),
|
|
110
|
+
protectedDecisions: z.array(z.string().min(1)).default([]),
|
|
111
|
+
openQuestions: z.array(z.string().min(1)).default([]),
|
|
112
|
+
nextActions: z.array(z.string().min(1)).default([]),
|
|
113
|
+
confidence: z.enum(["low", "medium", "high"]).default("medium"),
|
|
114
|
+
confirmedAt: z.string().optional()
|
|
115
|
+
});
|
|
59
116
|
function textResult(structuredContent) {
|
|
60
117
|
return {
|
|
61
118
|
content: [
|
|
@@ -360,6 +417,119 @@ async function summarizeInterviewHook(context, options) {
|
|
|
360
417
|
await syncCurrentWorkspaceView(context);
|
|
361
418
|
return { hook, shape, state: updated, session };
|
|
362
419
|
}
|
|
420
|
+
function normalizeStringArray(values) {
|
|
421
|
+
return (values ?? []).map((value) => value.trim()).filter(Boolean);
|
|
422
|
+
}
|
|
423
|
+
function normalizeOptionalString(value) {
|
|
424
|
+
const trimmed = value?.trim();
|
|
425
|
+
return trimmed && trimmed.length > 0 ? trimmed : undefined;
|
|
426
|
+
}
|
|
427
|
+
function normalizeResearchSpecification(input, sourceHookId, timestamp) {
|
|
428
|
+
const title = input.title.trim();
|
|
429
|
+
const purpose = input.researchDirection.purpose.trim();
|
|
430
|
+
if (!title || !purpose) {
|
|
431
|
+
throw new Error("Research Specification title and researchDirection.purpose are required.");
|
|
432
|
+
}
|
|
433
|
+
return {
|
|
434
|
+
title,
|
|
435
|
+
status: input.confirmedAt ? "confirmed" : input.status ?? "draft",
|
|
436
|
+
createdAt: input.createdAt ?? timestamp,
|
|
437
|
+
updatedAt: timestamp,
|
|
438
|
+
...(sourceHookId ? { sourceHookId } : {}),
|
|
439
|
+
researchDirection: {
|
|
440
|
+
...(normalizeOptionalString(input.researchDirection.question) ? { question: normalizeOptionalString(input.researchDirection.question) } : {}),
|
|
441
|
+
purpose,
|
|
442
|
+
...(normalizeOptionalString(input.researchDirection.scopeBoundary) ? { scopeBoundary: normalizeOptionalString(input.researchDirection.scopeBoundary) } : {}),
|
|
443
|
+
inclusionCriteria: normalizeStringArray(input.researchDirection.inclusionCriteria),
|
|
444
|
+
exclusionCriteria: normalizeStringArray(input.researchDirection.exclusionCriteria)
|
|
445
|
+
},
|
|
446
|
+
constructOntology: {
|
|
447
|
+
coreConstructs: normalizeStringArray(input.constructOntology.coreConstructs),
|
|
448
|
+
distinctions: normalizeStringArray(input.constructOntology.distinctions),
|
|
449
|
+
termsToAvoidCollapsing: normalizeStringArray(input.constructOntology.termsToAvoidCollapsing)
|
|
450
|
+
},
|
|
451
|
+
theoryAndFraming: {
|
|
452
|
+
anchors: normalizeStringArray(input.theoryAndFraming.anchors),
|
|
453
|
+
alternatives: normalizeStringArray(input.theoryAndFraming.alternatives),
|
|
454
|
+
overreachRisks: normalizeStringArray(input.theoryAndFraming.overreachRisks)
|
|
455
|
+
},
|
|
456
|
+
measurementCoding: {
|
|
457
|
+
variablesOrConstructs: normalizeStringArray(input.measurementCoding.variablesOrConstructs),
|
|
458
|
+
evidenceTypes: normalizeStringArray(input.measurementCoding.evidenceTypes),
|
|
459
|
+
codingRules: normalizeStringArray(input.measurementCoding.codingRules),
|
|
460
|
+
openStandards: normalizeStringArray(input.measurementCoding.openStandards)
|
|
461
|
+
},
|
|
462
|
+
methodAnalysis: {
|
|
463
|
+
...(normalizeOptionalString(input.methodAnalysis.design) ? { design: normalizeOptionalString(input.methodAnalysis.design) } : {}),
|
|
464
|
+
analysisOptions: normalizeStringArray(input.methodAnalysis.analysisOptions),
|
|
465
|
+
dataSufficiencyCriteria: normalizeStringArray(input.methodAnalysis.dataSufficiencyCriteria),
|
|
466
|
+
unsettledChoices: normalizeStringArray(input.methodAnalysis.unsettledChoices)
|
|
467
|
+
},
|
|
468
|
+
evidenceAccess: {
|
|
469
|
+
requiredSources: normalizeStringArray(input.evidenceAccess.requiredSources),
|
|
470
|
+
accessRequirements: normalizeStringArray(input.evidenceAccess.accessRequirements),
|
|
471
|
+
evidenceStandards: normalizeStringArray(input.evidenceAccess.evidenceStandards)
|
|
472
|
+
},
|
|
473
|
+
epistemicAlignment: {
|
|
474
|
+
researcherKnowledge: normalizeStringArray(input.epistemicAlignment.researcherKnowledge),
|
|
475
|
+
projectStatePriority: normalizeStringArray(input.epistemicAlignment.projectStatePriority),
|
|
476
|
+
aiInferenceLimits: normalizeStringArray(input.epistemicAlignment.aiInferenceLimits),
|
|
477
|
+
...(normalizeOptionalString(input.epistemicAlignment.conflictResolutionRule)
|
|
478
|
+
? { conflictResolutionRule: normalizeOptionalString(input.epistemicAlignment.conflictResolutionRule) }
|
|
479
|
+
: {})
|
|
480
|
+
},
|
|
481
|
+
protectedDecisions: normalizeStringArray(input.protectedDecisions),
|
|
482
|
+
openQuestions: normalizeStringArray(input.openQuestions),
|
|
483
|
+
nextActions: normalizeStringArray(input.nextActions),
|
|
484
|
+
confidence: input.confidence,
|
|
485
|
+
...(input.confirmedAt ? { confirmedAt: input.confirmedAt } : {})
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
async function summarizeResearchSpecificationHook(context, options) {
|
|
489
|
+
const state = asInterviewState(await loadWorkspaceState(context));
|
|
490
|
+
const sourceHookId = options.hookId
|
|
491
|
+
?? options.specification.sourceHookId
|
|
492
|
+
?? state.firstResearchShape?.sourceHookId;
|
|
493
|
+
const existing = sourceHookId
|
|
494
|
+
? (state.hooks ?? []).find((hook) => isInterviewHookRun(hook) && hook.id === sourceHookId)
|
|
495
|
+
: activeInterviewHook(state);
|
|
496
|
+
const timestamp = new Date().toISOString();
|
|
497
|
+
const specification = normalizeResearchSpecification(options.specification, existing?.id ?? sourceHookId, timestamp);
|
|
498
|
+
const hook = existing
|
|
499
|
+
? {
|
|
500
|
+
...existing,
|
|
501
|
+
status: "ready_to_confirm",
|
|
502
|
+
updatedAt: timestamp,
|
|
503
|
+
researchSpecification: specification
|
|
504
|
+
}
|
|
505
|
+
: undefined;
|
|
506
|
+
const session = {
|
|
507
|
+
...context.session,
|
|
508
|
+
lastUpdatedAt: timestamp,
|
|
509
|
+
researchSpecification: specification,
|
|
510
|
+
resumeHint: `I want to continue from the Research Specification: ${specification.title}.`
|
|
511
|
+
};
|
|
512
|
+
context.session = session;
|
|
513
|
+
const updated = hook ? upsertInterviewHook(state, hook) : state;
|
|
514
|
+
updated.researchSpecification = specification;
|
|
515
|
+
updated.workingState = {
|
|
516
|
+
...updated.workingState,
|
|
517
|
+
researchSpecification: specification
|
|
518
|
+
};
|
|
519
|
+
updated.narrativeTraces.push({
|
|
520
|
+
id: createId("narrative_trace"),
|
|
521
|
+
timestamp,
|
|
522
|
+
source: "$longtable-interview",
|
|
523
|
+
traceType: "judgment",
|
|
524
|
+
summary: `Research Specification draft: ${specification.title}.`,
|
|
525
|
+
visibility: "explicit",
|
|
526
|
+
importance: specification.confidence
|
|
527
|
+
});
|
|
528
|
+
await writeFile(context.sessionFilePath, JSON.stringify(session, null, 2), "utf8");
|
|
529
|
+
await writeFile(context.stateFilePath, JSON.stringify(updated, null, 2), "utf8");
|
|
530
|
+
await syncCurrentWorkspaceView(context);
|
|
531
|
+
return { hook, specification, state: updated, session };
|
|
532
|
+
}
|
|
363
533
|
async function cancelInterviewHook(context, options) {
|
|
364
534
|
const state = asInterviewState(await loadWorkspaceState(context));
|
|
365
535
|
const existing = activeInterviewHook(state, options.hookId);
|
|
@@ -430,6 +600,7 @@ function compactInterviewHook(hook) {
|
|
|
430
600
|
provider: hook.provider,
|
|
431
601
|
turnCount: hook.turns?.length ?? 0,
|
|
432
602
|
firstResearchShape: hook.firstResearchShape?.handle,
|
|
603
|
+
researchSpecification: hook.researchSpecification?.title,
|
|
433
604
|
createdAt: hook.createdAt,
|
|
434
605
|
updatedAt: hook.updatedAt
|
|
435
606
|
};
|
|
@@ -591,6 +762,87 @@ async function markAlreadyConfirmedFirstResearchShape(context, shape) {
|
|
|
591
762
|
await syncCurrentWorkspaceView(context);
|
|
592
763
|
return { state: nextState, session, shape: confirmedShape };
|
|
593
764
|
}
|
|
765
|
+
async function markResearchSpecificationConfirmation(context, specification, answer, questionId, decisionId) {
|
|
766
|
+
const state = asInterviewState(await loadWorkspaceState(context));
|
|
767
|
+
const timestamp = new Date().toISOString();
|
|
768
|
+
const confirmedSpecification = researchSpecificationAnswerConfirms(answer)
|
|
769
|
+
? { ...specification, status: "confirmed", confirmedAt: timestamp, updatedAt: timestamp }
|
|
770
|
+
: {
|
|
771
|
+
...specification,
|
|
772
|
+
status: researchSpecificationAnswerStatus(answer) === "deferred" ? "deferred" : "draft",
|
|
773
|
+
updatedAt: timestamp
|
|
774
|
+
};
|
|
775
|
+
state.researchSpecification = confirmedSpecification;
|
|
776
|
+
state.workingState = {
|
|
777
|
+
...state.workingState,
|
|
778
|
+
researchSpecification: confirmedSpecification
|
|
779
|
+
};
|
|
780
|
+
state.hooks = (state.hooks ?? []).map((hook) => {
|
|
781
|
+
if (hook.id !== specification.sourceHookId || !isInterviewHookRun(hook)) {
|
|
782
|
+
return hook;
|
|
783
|
+
}
|
|
784
|
+
return {
|
|
785
|
+
...hook,
|
|
786
|
+
status: researchSpecificationAnswerStatus(answer),
|
|
787
|
+
updatedAt: timestamp,
|
|
788
|
+
researchSpecification: confirmedSpecification,
|
|
789
|
+
linkedQuestionRecordIds: questionId
|
|
790
|
+
? [...(hook.linkedQuestionRecordIds ?? []), questionId]
|
|
791
|
+
: hook.linkedQuestionRecordIds,
|
|
792
|
+
linkedDecisionRecordIds: decisionId
|
|
793
|
+
? [...(hook.linkedDecisionRecordIds ?? []), decisionId]
|
|
794
|
+
: hook.linkedDecisionRecordIds
|
|
795
|
+
};
|
|
796
|
+
});
|
|
797
|
+
const session = {
|
|
798
|
+
...context.session,
|
|
799
|
+
researchSpecification: confirmedSpecification,
|
|
800
|
+
lastUpdatedAt: timestamp
|
|
801
|
+
};
|
|
802
|
+
context.session = session;
|
|
803
|
+
await writeFile(context.sessionFilePath, JSON.stringify(session, null, 2), "utf8");
|
|
804
|
+
await writeFile(context.stateFilePath, JSON.stringify(state, null, 2), "utf8");
|
|
805
|
+
await syncCurrentWorkspaceView(context);
|
|
806
|
+
return { state, session, specification: confirmedSpecification };
|
|
807
|
+
}
|
|
808
|
+
async function markAlreadyConfirmedResearchSpecification(context, specification) {
|
|
809
|
+
const state = asInterviewState(await loadWorkspaceState(context));
|
|
810
|
+
const timestamp = new Date().toISOString();
|
|
811
|
+
const confirmedSpecification = specification.confirmedAt
|
|
812
|
+
? specification
|
|
813
|
+
: { ...specification, status: "confirmed", confirmedAt: timestamp, updatedAt: timestamp };
|
|
814
|
+
state.researchSpecification = confirmedSpecification;
|
|
815
|
+
state.workingState = {
|
|
816
|
+
...state.workingState,
|
|
817
|
+
researchSpecification: confirmedSpecification
|
|
818
|
+
};
|
|
819
|
+
state.hooks = (state.hooks ?? []).map((hook) => {
|
|
820
|
+
if (!isInterviewHookRun(hook)) {
|
|
821
|
+
return hook;
|
|
822
|
+
}
|
|
823
|
+
const matchesSource = specification.sourceHookId && hook.id === specification.sourceHookId;
|
|
824
|
+
const matchesTitle = !specification.sourceHookId && hook.researchSpecification?.title === specification.title;
|
|
825
|
+
if (!matchesSource && !matchesTitle) {
|
|
826
|
+
return hook;
|
|
827
|
+
}
|
|
828
|
+
return {
|
|
829
|
+
...hook,
|
|
830
|
+
status: "confirmed",
|
|
831
|
+
updatedAt: timestamp,
|
|
832
|
+
researchSpecification: confirmedSpecification
|
|
833
|
+
};
|
|
834
|
+
});
|
|
835
|
+
const session = {
|
|
836
|
+
...context.session,
|
|
837
|
+
researchSpecification: confirmedSpecification,
|
|
838
|
+
lastUpdatedAt: timestamp
|
|
839
|
+
};
|
|
840
|
+
context.session = session;
|
|
841
|
+
await writeFile(context.sessionFilePath, JSON.stringify(session, null, 2), "utf8");
|
|
842
|
+
await writeFile(context.stateFilePath, JSON.stringify(state, null, 2), "utf8");
|
|
843
|
+
await syncCurrentWorkspaceView(context);
|
|
844
|
+
return { state, session, specification: confirmedSpecification };
|
|
845
|
+
}
|
|
594
846
|
function statusForElicitationError(error) {
|
|
595
847
|
const message = error instanceof Error ? error.message : String(error);
|
|
596
848
|
if (/timed?\s*out|timeout/i.test(message)) {
|
|
@@ -823,6 +1075,63 @@ export function createLongTableMcpServer() {
|
|
|
823
1075
|
return errorResult(error instanceof Error ? error.message : String(error));
|
|
824
1076
|
}
|
|
825
1077
|
});
|
|
1078
|
+
server.registerTool("summarize_research_specification", {
|
|
1079
|
+
title: "Summarize LongTable Research Specification",
|
|
1080
|
+
description: "Store the fuller Research Specification after a First Research Shape has enough interview context to preserve scope, constructs, theory, coding, method, evidence/access, and epistemic alignment.",
|
|
1081
|
+
inputSchema: cwdSchema.extend({
|
|
1082
|
+
hookId: z.string().optional(),
|
|
1083
|
+
specification: researchSpecificationSchema
|
|
1084
|
+
})
|
|
1085
|
+
}, async ({ cwd: inputCwd, hookId, specification }) => {
|
|
1086
|
+
try {
|
|
1087
|
+
const context = await requireContext(inputCwd);
|
|
1088
|
+
const result = await summarizeResearchSpecificationHook(context, {
|
|
1089
|
+
hookId,
|
|
1090
|
+
specification: specification
|
|
1091
|
+
});
|
|
1092
|
+
return textResult({
|
|
1093
|
+
hook: compactInterviewHook(result.hook),
|
|
1094
|
+
specification: result.specification,
|
|
1095
|
+
preview: renderResearchSpecificationPreview(result.specification),
|
|
1096
|
+
session: {
|
|
1097
|
+
currentGoal: result.session.currentGoal,
|
|
1098
|
+
currentBlocker: result.session.currentBlocker,
|
|
1099
|
+
nextAction: result.session.nextAction,
|
|
1100
|
+
firstResearchShape: result.session.firstResearchShape,
|
|
1101
|
+
researchSpecification: result.session.researchSpecification
|
|
1102
|
+
}
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1105
|
+
catch (error) {
|
|
1106
|
+
return errorResult(error instanceof Error ? error.message : String(error));
|
|
1107
|
+
}
|
|
1108
|
+
});
|
|
1109
|
+
server.registerTool("read_research_specification", {
|
|
1110
|
+
title: "Read LongTable Research Specification",
|
|
1111
|
+
description: "Read the current Research Specification and render the researcher-facing preview.",
|
|
1112
|
+
inputSchema: cwdSchema
|
|
1113
|
+
}, async ({ cwd: inputCwd }) => {
|
|
1114
|
+
try {
|
|
1115
|
+
const context = await requireContext(inputCwd);
|
|
1116
|
+
const state = asInterviewState(await loadWorkspaceState(context));
|
|
1117
|
+
const session = context.session;
|
|
1118
|
+
const specification = state.researchSpecification ?? session.researchSpecification;
|
|
1119
|
+
if (!specification) {
|
|
1120
|
+
return textResult({
|
|
1121
|
+
found: false,
|
|
1122
|
+
message: "No Research Specification was found. Run summarize_research_specification first."
|
|
1123
|
+
});
|
|
1124
|
+
}
|
|
1125
|
+
return textResult({
|
|
1126
|
+
found: true,
|
|
1127
|
+
specification,
|
|
1128
|
+
preview: renderResearchSpecificationPreview(specification)
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
1131
|
+
catch (error) {
|
|
1132
|
+
return errorResult(error instanceof Error ? error.message : String(error));
|
|
1133
|
+
}
|
|
1134
|
+
});
|
|
826
1135
|
server.registerTool("cancel_interview", {
|
|
827
1136
|
title: "Cancel LongTable Interview",
|
|
828
1137
|
description: "Explicitly cancel the active $longtable-interview hook without confirming a First Research Shape.",
|
|
@@ -954,6 +1263,118 @@ export function createLongTableMcpServer() {
|
|
|
954
1263
|
return errorResult(error instanceof Error ? error.message : String(error));
|
|
955
1264
|
}
|
|
956
1265
|
});
|
|
1266
|
+
server.registerTool("confirm_research_specification", {
|
|
1267
|
+
title: "Confirm Research Specification",
|
|
1268
|
+
description: "Use MCP form elicitation to confirm, revise, defer, or request one more question for the full Research Specification.",
|
|
1269
|
+
inputSchema: cwdSchema.extend({
|
|
1270
|
+
specification: researchSpecificationSchema.optional(),
|
|
1271
|
+
provider: z.enum(["codex", "claude"]).default("codex"),
|
|
1272
|
+
fallbackOnly: z.boolean().default(false)
|
|
1273
|
+
})
|
|
1274
|
+
}, async ({ cwd: inputCwd, specification: inputSpecification, provider, fallbackOnly }) => {
|
|
1275
|
+
try {
|
|
1276
|
+
const context = await requireContext(inputCwd);
|
|
1277
|
+
const state = asInterviewState(await loadWorkspaceState(context));
|
|
1278
|
+
const session = context.session;
|
|
1279
|
+
const specification = inputSpecification
|
|
1280
|
+
?? state.researchSpecification
|
|
1281
|
+
?? session.researchSpecification;
|
|
1282
|
+
if (!specification) {
|
|
1283
|
+
return errorResult("No Research Specification was found to confirm. Run summarize_research_specification first.");
|
|
1284
|
+
}
|
|
1285
|
+
if (specification.confirmedAt) {
|
|
1286
|
+
const confirmation = await markAlreadyConfirmedResearchSpecification(context, specification);
|
|
1287
|
+
return textResult({
|
|
1288
|
+
specification: confirmation.specification,
|
|
1289
|
+
preview: renderResearchSpecificationPreview(confirmation.specification),
|
|
1290
|
+
elicitation: { attempted: false, reason: "already_confirmed" }
|
|
1291
|
+
});
|
|
1292
|
+
}
|
|
1293
|
+
const spec = buildResearchSpecificationQuestion(specification);
|
|
1294
|
+
const created = await createWorkspaceQuestion({
|
|
1295
|
+
context,
|
|
1296
|
+
prompt: spec.prompt,
|
|
1297
|
+
title: spec.title,
|
|
1298
|
+
question: spec.question,
|
|
1299
|
+
checkpointKey: spec.checkpointKey,
|
|
1300
|
+
questionOptions: spec.options,
|
|
1301
|
+
displayReason: spec.displayReason,
|
|
1302
|
+
provider: provider,
|
|
1303
|
+
required: true
|
|
1304
|
+
});
|
|
1305
|
+
const fallback = renderQuestionFallback(created.question, provider);
|
|
1306
|
+
if (fallbackOnly) {
|
|
1307
|
+
const marked = await markQuestionTransport(context, created.question.id, "fallback_rendered", "MCP elicitation skipped by fallbackOnly.");
|
|
1308
|
+
return textResult({
|
|
1309
|
+
question: marked ?? created.question,
|
|
1310
|
+
specification,
|
|
1311
|
+
preview: renderResearchSpecificationPreview(specification),
|
|
1312
|
+
elicitation: { attempted: false, reason: "fallbackOnly" },
|
|
1313
|
+
fallback
|
|
1314
|
+
});
|
|
1315
|
+
}
|
|
1316
|
+
try {
|
|
1317
|
+
await markQuestionTransport(context, created.question.id, "attempted");
|
|
1318
|
+
const elicited = await server.server.elicitInput(buildElicitationParams(created.question));
|
|
1319
|
+
const accepted = acceptedAnswer(elicited);
|
|
1320
|
+
if (!accepted) {
|
|
1321
|
+
const status = elicited.action === "decline" || elicited.action === "cancel"
|
|
1322
|
+
? "declined"
|
|
1323
|
+
: "fallback_rendered";
|
|
1324
|
+
const marked = await markQuestionTransport(context, created.question.id, status, `MCP elicitation returned action: ${elicited.action}.`);
|
|
1325
|
+
const cleared = status === "declined"
|
|
1326
|
+
? await clearWorkspaceQuestion({
|
|
1327
|
+
context,
|
|
1328
|
+
questionId: created.question.id,
|
|
1329
|
+
reason: `MCP elicitation returned action: ${elicited.action}; Research Specification confirmation was deferred.`
|
|
1330
|
+
})
|
|
1331
|
+
: undefined;
|
|
1332
|
+
return textResult({
|
|
1333
|
+
question: cleared?.question ?? marked ?? created.question,
|
|
1334
|
+
specification,
|
|
1335
|
+
preview: renderResearchSpecificationPreview(specification),
|
|
1336
|
+
elicitation: { attempted: true, action: elicited.action },
|
|
1337
|
+
fallback
|
|
1338
|
+
});
|
|
1339
|
+
}
|
|
1340
|
+
const decided = await answerWorkspaceQuestion({
|
|
1341
|
+
context,
|
|
1342
|
+
questionId: created.question.id,
|
|
1343
|
+
answer: accepted.answer,
|
|
1344
|
+
provider: provider,
|
|
1345
|
+
surface: "mcp_elicitation"
|
|
1346
|
+
});
|
|
1347
|
+
const marked = await markQuestionTransport(context, created.question.id, "accepted");
|
|
1348
|
+
const confirmation = await markResearchSpecificationConfirmation(context, specification, accepted.answer, created.question.id, decided.decision.id);
|
|
1349
|
+
return textResult({
|
|
1350
|
+
specification: confirmation.specification,
|
|
1351
|
+
preview: renderResearchSpecificationPreview(confirmation.specification),
|
|
1352
|
+
question: marked ? { ...decided.question, transportStatus: marked.transportStatus } : decided.question,
|
|
1353
|
+
decision: decided.decision,
|
|
1354
|
+
elicitation: { attempted: true, action: elicited.action }
|
|
1355
|
+
});
|
|
1356
|
+
}
|
|
1357
|
+
catch (elicitationError) {
|
|
1358
|
+
const status = statusForElicitationError(elicitationError);
|
|
1359
|
+
const message = elicitationError instanceof Error ? elicitationError.message : String(elicitationError);
|
|
1360
|
+
const marked = await markQuestionTransport(context, created.question.id, status, message);
|
|
1361
|
+
return textResult({
|
|
1362
|
+
question: marked ?? created.question,
|
|
1363
|
+
specification,
|
|
1364
|
+
preview: renderResearchSpecificationPreview(specification),
|
|
1365
|
+
elicitation: {
|
|
1366
|
+
attempted: true,
|
|
1367
|
+
supported: status !== "unsupported" ? undefined : false,
|
|
1368
|
+
error: message
|
|
1369
|
+
},
|
|
1370
|
+
fallback
|
|
1371
|
+
});
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
catch (error) {
|
|
1375
|
+
return errorResult(error instanceof Error ? error.message : String(error));
|
|
1376
|
+
}
|
|
1377
|
+
});
|
|
957
1378
|
server.registerTool("pending_questions", {
|
|
958
1379
|
title: "List Pending Researcher Checkpoints",
|
|
959
1380
|
description: "List pending LongTable QuestionRecords.",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@longtable/mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.44",
|
|
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.44",
|
|
30
|
+
"@longtable/cli": "0.1.44",
|
|
31
|
+
"@longtable/core": "0.1.44",
|
|
32
|
+
"@longtable/provider-claude": "0.1.44",
|
|
33
|
+
"@longtable/provider-codex": "0.1.44",
|
|
34
|
+
"@longtable/setup": "0.1.44",
|
|
35
35
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
36
36
|
"zod": "^4.0.0"
|
|
37
37
|
},
|