@longtable/cli 0.1.45 → 0.1.47
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 +118 -6
- package/dist/project-session.d.ts +7 -1
- package/dist/project-session.js +179 -34
- package/package.json +7 -7
package/dist/cli.js
CHANGED
|
@@ -49,8 +49,35 @@ 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
|
+
"cancel_interview",
|
|
66
|
+
"confirm_first_research_shape",
|
|
67
|
+
"confirm_research_specification",
|
|
68
|
+
"pending_questions",
|
|
69
|
+
"evaluate_checkpoint",
|
|
70
|
+
"create_question",
|
|
71
|
+
"elicit_question",
|
|
72
|
+
"render_question",
|
|
73
|
+
"append_decision",
|
|
74
|
+
"regenerate_current"
|
|
75
|
+
];
|
|
76
|
+
const LONGTABLE_MCP_RESEARCH_SPECIFICATION_TOOLS = [
|
|
77
|
+
"summarize_research_specification",
|
|
78
|
+
"read_research_specification",
|
|
79
|
+
"confirm_research_specification"
|
|
80
|
+
];
|
|
54
81
|
function style(text, prefix) {
|
|
55
82
|
return `${prefix}${text}${ANSI.reset}`;
|
|
56
83
|
}
|
|
@@ -84,7 +111,7 @@ function renderInterviewLaunchSteps(provider) {
|
|
|
84
111
|
`2. run \`${command}\``,
|
|
85
112
|
"3. invoke `$longtable-interview`",
|
|
86
113
|
"",
|
|
87
|
-
"The interview will create or resume `.longtable/`,
|
|
114
|
+
"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
115
|
]);
|
|
89
116
|
}
|
|
90
117
|
function renderProgressBar(current, total) {
|
|
@@ -1131,7 +1158,7 @@ function resolveMcpProviders(value) {
|
|
|
1131
1158
|
function resolveMcpPackageSpec(args) {
|
|
1132
1159
|
return typeof args.package === "string" && args.package.trim()
|
|
1133
1160
|
? args.package.trim()
|
|
1134
|
-
:
|
|
1161
|
+
: LONGTABLE_MCP_PACKAGE_SPEC;
|
|
1135
1162
|
}
|
|
1136
1163
|
function resolveCodexMcpConfigPath(args) {
|
|
1137
1164
|
return resolve(normalizeUserPath(typeof args["codex-config"] === "string" && args["codex-config"].trim()
|
|
@@ -1162,6 +1189,12 @@ function renderCodexMcpBlock(serverName, command, mcpArgs) {
|
|
|
1162
1189
|
`[mcp_servers.${serverName}]`,
|
|
1163
1190
|
`command = ${escapeTomlString(command)}`,
|
|
1164
1191
|
`args = [${mcpArgs.map((arg) => escapeTomlString(arg)).join(", ")}]`,
|
|
1192
|
+
"",
|
|
1193
|
+
...LONGTABLE_MCP_MANAGED_TOOLS.flatMap((tool) => [
|
|
1194
|
+
`[mcp_servers.${serverName}.tools.${tool}]`,
|
|
1195
|
+
"approval_mode = \"approve\"",
|
|
1196
|
+
""
|
|
1197
|
+
]),
|
|
1165
1198
|
LONGTABLE_MCP_MARKER_END
|
|
1166
1199
|
].join("\n");
|
|
1167
1200
|
}
|
|
@@ -1185,10 +1218,55 @@ function codexMcpElicitationsAllowed(config) {
|
|
|
1185
1218
|
function codexLongTableMcpConfigured(config) {
|
|
1186
1219
|
return new RegExp(`\\[mcp_servers\\.${LONGTABLE_MCP_SERVER_NAME.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\]`).test(config);
|
|
1187
1220
|
}
|
|
1221
|
+
function codexLongTableMcpPackageSpec(config) {
|
|
1222
|
+
const serverName = LONGTABLE_MCP_SERVER_NAME.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1223
|
+
const match = new RegExp(`\\[mcp_servers\\.${serverName}\\][\\s\\S]*?(?=\\n\\[|$)`).exec(config);
|
|
1224
|
+
if (!match) {
|
|
1225
|
+
return undefined;
|
|
1226
|
+
}
|
|
1227
|
+
const packageMatch = /@longtable\/mcp@[A-Za-z0-9._~+:-]+/.exec(match[0]);
|
|
1228
|
+
return packageMatch?.[0];
|
|
1229
|
+
}
|
|
1230
|
+
function codexLongTableMcpToolConfigured(config, tool) {
|
|
1231
|
+
const serverName = LONGTABLE_MCP_SERVER_NAME.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1232
|
+
const escapedTool = tool.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1233
|
+
return new RegExp(`\\[mcp_servers\\.${serverName}\\.tools\\.${escapedTool}\\]`).test(config);
|
|
1234
|
+
}
|
|
1235
|
+
function missingCodexLongTableMcpTools(config) {
|
|
1236
|
+
if (!codexLongTableMcpConfigured(config)) {
|
|
1237
|
+
return [...LONGTABLE_MCP_MANAGED_TOOLS];
|
|
1238
|
+
}
|
|
1239
|
+
return LONGTABLE_MCP_MANAGED_TOOLS.filter((tool) => !codexLongTableMcpToolConfigured(config, tool));
|
|
1240
|
+
}
|
|
1241
|
+
function preserveNonLongTableSectionsFromMarkedBlock(block, serverName) {
|
|
1242
|
+
const body = block
|
|
1243
|
+
.replace(LONGTABLE_MCP_MARKER_START, "")
|
|
1244
|
+
.replace(LONGTABLE_MCP_MARKER_END, "")
|
|
1245
|
+
.trim();
|
|
1246
|
+
if (!body) {
|
|
1247
|
+
return "";
|
|
1248
|
+
}
|
|
1249
|
+
const sections = body.split(/(?=^\[[^\]]+\])/m);
|
|
1250
|
+
const serverHeader = `[mcp_servers.${serverName}]`;
|
|
1251
|
+
const toolPrefix = `[mcp_servers.${serverName}.tools.`;
|
|
1252
|
+
return sections
|
|
1253
|
+
.map((section) => section.trim())
|
|
1254
|
+
.filter(Boolean)
|
|
1255
|
+
.filter((section) => {
|
|
1256
|
+
const header = section.split(/\r?\n/, 1)[0]?.trim() ?? "";
|
|
1257
|
+
return header !== serverHeader && !header.startsWith(toolPrefix);
|
|
1258
|
+
})
|
|
1259
|
+
.join("\n\n");
|
|
1260
|
+
}
|
|
1188
1261
|
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
|
|
1190
|
-
const serverPattern = new RegExp(`\\n?\\[mcp_servers\\.${serverName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\][\\s\\S]*?(?=\\n\\[|$)
|
|
1191
|
-
const
|
|
1262
|
+
const markerPattern = new RegExp(`${LONGTABLE_MCP_MARKER_START.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s\\S]*?${LONGTABLE_MCP_MARKER_END.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\n?`);
|
|
1263
|
+
const serverPattern = new RegExp(`\\n?\\[mcp_servers\\.${serverName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\][\\s\\S]*?(?=\\n\\[|$)`);
|
|
1264
|
+
const toolPattern = new RegExp(`\\n?\\[mcp_servers\\.${serverName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\.tools\\.[^\\]]+\\][\\s\\S]*?(?=\\n\\[|$)`, "g");
|
|
1265
|
+
const withoutMarked = existing.replace(markerPattern, (matched) => {
|
|
1266
|
+
const preserved = preserveNonLongTableSectionsFromMarkedBlock(matched, serverName);
|
|
1267
|
+
return preserved ? `${preserved}\n\n` : "";
|
|
1268
|
+
});
|
|
1269
|
+
const trimmed = withoutMarked.replace(toolPattern, "").replace(serverPattern, "").trimEnd();
|
|
1192
1270
|
return trimmed ? `${trimmed}\n\n${block}\n` : `${block}\n`;
|
|
1193
1271
|
}
|
|
1194
1272
|
async function writeCodexMcpConfig(path, block, serverName, options = {}) {
|
|
@@ -1467,6 +1545,8 @@ async function collectDoctorStatus(args) {
|
|
|
1467
1545
|
const codexMcpConfig = existsSync(codexMcpConfigPath)
|
|
1468
1546
|
? await readFile(codexMcpConfigPath, "utf8")
|
|
1469
1547
|
: "";
|
|
1548
|
+
const codexMcpPackageSpec = codexLongTableMcpPackageSpec(codexMcpConfig);
|
|
1549
|
+
const missingMcpTools = missingCodexLongTableMcpTools(codexMcpConfig);
|
|
1470
1550
|
const codexHooksPath = resolveCodexHooksPath(args);
|
|
1471
1551
|
const codexHooksContent = existsSync(codexHooksPath)
|
|
1472
1552
|
? await readFile(codexHooksPath, "utf8")
|
|
@@ -1502,6 +1582,10 @@ async function collectDoctorStatus(args) {
|
|
|
1502
1582
|
mcpConfigPath: codexMcpConfigPath,
|
|
1503
1583
|
mcpConfigExists: existsSync(codexMcpConfigPath),
|
|
1504
1584
|
longtableMcpConfigured: codexLongTableMcpConfigured(codexMcpConfig),
|
|
1585
|
+
...(codexMcpPackageSpec ? { mcpPackageSpec: codexMcpPackageSpec } : {}),
|
|
1586
|
+
expectedMcpPackageSpec: LONGTABLE_MCP_PACKAGE_SPEC,
|
|
1587
|
+
missingMcpTools,
|
|
1588
|
+
missingResearchSpecificationMcpTools: missingMcpTools.filter((tool) => LONGTABLE_MCP_RESEARCH_SPECIFICATION_TOOLS.includes(tool)),
|
|
1505
1589
|
mcpElicitationsAllowed: codexMcpElicitationsAllowed(codexMcpConfig),
|
|
1506
1590
|
hooksPath: codexHooksPath,
|
|
1507
1591
|
hooksExists: existsSync(codexHooksPath),
|
|
@@ -1547,6 +1631,11 @@ function renderDoctorStatus(status) {
|
|
|
1547
1631
|
: []),
|
|
1548
1632
|
`- MCP config: ${status.providers.codex.mcpConfigExists ? "present" : "missing"} (${status.providers.codex.mcpConfigPath})`,
|
|
1549
1633
|
`- LongTable MCP: ${status.providers.codex.longtableMcpConfigured ? "configured" : "missing"}`,
|
|
1634
|
+
`- MCP package: ${status.providers.codex.mcpPackageSpec ?? "unknown"}${status.providers.codex.mcpPackageSpec === status.providers.codex.expectedMcpPackageSpec ? "" : ` (expected ${status.providers.codex.expectedMcpPackageSpec})`}`,
|
|
1635
|
+
`- MCP managed tools: ${LONGTABLE_MCP_MANAGED_TOOLS.length - status.providers.codex.missingMcpTools.length}/${LONGTABLE_MCP_MANAGED_TOOLS.length} configured`,
|
|
1636
|
+
...(status.providers.codex.missingResearchSpecificationMcpTools.length > 0
|
|
1637
|
+
? [`- Research Specification MCP tools: missing ${status.providers.codex.missingResearchSpecificationMcpTools.join(", ")}`]
|
|
1638
|
+
: ["- Research Specification MCP tools: complete"]),
|
|
1550
1639
|
`- MCP elicitation approval: ${status.providers.codex.mcpElicitationsAllowed ? "allowed" : "not allowed"}`,
|
|
1551
1640
|
`- Codex hooks file: ${status.providers.codex.hooksExists ? "present" : "missing"} (${status.providers.codex.hooksPath})`,
|
|
1552
1641
|
`- codex_hooks feature: ${status.providers.codex.codexHooksEnabled ? "enabled" : "missing"}`,
|
|
@@ -1595,6 +1684,9 @@ function renderDoctorStatus(status) {
|
|
|
1595
1684
|
const canFix = status.providers.codex.missingSkills.length > 0 ||
|
|
1596
1685
|
status.providers.claude.missingSkills.length > 0 ||
|
|
1597
1686
|
status.providers.codex.legacyPromptFilesInstalled.length > 0 ||
|
|
1687
|
+
!status.providers.codex.longtableMcpConfigured ||
|
|
1688
|
+
status.providers.codex.mcpPackageSpec !== status.providers.codex.expectedMcpPackageSpec ||
|
|
1689
|
+
status.providers.codex.missingMcpTools.length > 0 ||
|
|
1598
1690
|
!status.providers.codex.codexHooksEnabled ||
|
|
1599
1691
|
status.providers.codex.missingManagedHookEvents.length > 0 ||
|
|
1600
1692
|
(status.setupExists &&
|
|
@@ -1605,6 +1697,11 @@ function renderDoctorStatus(status) {
|
|
|
1605
1697
|
if (!status.providers.codex.codexHooksEnabled || status.providers.codex.missingManagedHookEvents.length > 0) {
|
|
1606
1698
|
nextActions.push("longtable codex install-hooks");
|
|
1607
1699
|
}
|
|
1700
|
+
if (!status.providers.codex.longtableMcpConfigured ||
|
|
1701
|
+
status.providers.codex.mcpPackageSpec !== status.providers.codex.expectedMcpPackageSpec ||
|
|
1702
|
+
status.providers.codex.missingMcpTools.length > 0) {
|
|
1703
|
+
nextActions.push("longtable mcp install --provider codex --write");
|
|
1704
|
+
}
|
|
1608
1705
|
if (!status.setupExists) {
|
|
1609
1706
|
nextActions.push("longtable setup --provider codex");
|
|
1610
1707
|
}
|
|
@@ -1644,7 +1741,7 @@ function renderRepairSummary(repair) {
|
|
|
1644
1741
|
}
|
|
1645
1742
|
}
|
|
1646
1743
|
if (repair.writtenRuntimeConfigs.length > 0) {
|
|
1647
|
-
lines.push("- wrote
|
|
1744
|
+
lines.push("- wrote configs:");
|
|
1648
1745
|
for (const target of repair.writtenRuntimeConfigs) {
|
|
1649
1746
|
lines.push(` - ${target.provider}: ${target.path}`);
|
|
1650
1747
|
}
|
|
@@ -1700,6 +1797,21 @@ async function repairDoctorStatus(args, status) {
|
|
|
1700
1797
|
writtenRuntimeConfigs: [],
|
|
1701
1798
|
skipped: []
|
|
1702
1799
|
};
|
|
1800
|
+
const mcpRepairNeeded = !status.providers.codex.longtableMcpConfigured ||
|
|
1801
|
+
status.providers.codex.mcpPackageSpec !== status.providers.codex.expectedMcpPackageSpec ||
|
|
1802
|
+
status.providers.codex.missingMcpTools.length > 0;
|
|
1803
|
+
if (mcpRepairNeeded) {
|
|
1804
|
+
const mcpConfigPath = resolveDoctorCodexMcpConfigPath(args);
|
|
1805
|
+
const block = renderCodexMcpBlock(LONGTABLE_MCP_SERVER_NAME, "npx", ["-y", LONGTABLE_MCP_PACKAGE_SPEC]);
|
|
1806
|
+
await writeCodexMcpConfig(mcpConfigPath, block, LONGTABLE_MCP_SERVER_NAME, {
|
|
1807
|
+
enableElicitations: status.providers.codex.mcpElicitationsAllowed
|
|
1808
|
+
});
|
|
1809
|
+
repair.writtenRuntimeConfigs.push({
|
|
1810
|
+
provider: "codex",
|
|
1811
|
+
path: mcpConfigPath,
|
|
1812
|
+
format: "toml"
|
|
1813
|
+
});
|
|
1814
|
+
}
|
|
1703
1815
|
if (status.providers.codex.missingSkills.length > 0) {
|
|
1704
1816
|
repair.installedCodexSkills = (await installCodexSkills(roles, codexDir, skillSurface)).map((skill) => skill.name);
|
|
1705
1817
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { DecisionRecord, InvocationRecord, LongTableQuestionObligation, ProviderKind, QuestionOption, QuestionGenerationResult, QuestionOpportunity, QuestionSurface, QuestionRecord, ResearchState } from "@longtable/core";
|
|
1
|
+
import type { DecisionRecord, InvocationRecord, LongTableQuestionObligation, ProviderKind, QuestionOption, QuestionCommitmentFamily, QuestionEpistemicBasis, QuestionGenerationResult, QuestionOpportunity, QuestionSurface, QuestionRecord, 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";
|
|
@@ -221,6 +221,8 @@ export interface LongTableWorkspaceInspection {
|
|
|
221
221
|
id: string;
|
|
222
222
|
title: string;
|
|
223
223
|
question: string;
|
|
224
|
+
commitmentFamily?: QuestionCommitmentFamily;
|
|
225
|
+
epistemicBasis?: QuestionEpistemicBasis;
|
|
224
226
|
options: string[];
|
|
225
227
|
required: boolean;
|
|
226
228
|
}>;
|
|
@@ -235,6 +237,8 @@ export interface LongTableWorkspaceInspection {
|
|
|
235
237
|
id: string;
|
|
236
238
|
checkpointKey: string;
|
|
237
239
|
summary: string;
|
|
240
|
+
commitmentFamily?: QuestionCommitmentFamily;
|
|
241
|
+
epistemicBasis?: QuestionEpistemicBasis;
|
|
238
242
|
selectedOption?: string;
|
|
239
243
|
timestamp: string;
|
|
240
244
|
}>;
|
|
@@ -329,6 +333,8 @@ export declare function createWorkspaceQuestion(options: {
|
|
|
329
333
|
displayReason?: string;
|
|
330
334
|
provider?: ProviderKind;
|
|
331
335
|
required?: boolean;
|
|
336
|
+
commitmentFamily?: QuestionCommitmentFamily;
|
|
337
|
+
epistemicBasis?: QuestionEpistemicBasis;
|
|
332
338
|
}): Promise<{
|
|
333
339
|
question: QuestionRecord;
|
|
334
340
|
state: ResearchState;
|
package/dist/project-session.js
CHANGED
|
@@ -163,6 +163,43 @@ function renderResearchSpecificationSummary(specification, locale) {
|
|
|
163
163
|
}
|
|
164
164
|
return lines;
|
|
165
165
|
}
|
|
166
|
+
function renderResearchSpecificationStatus(session, locale) {
|
|
167
|
+
if (!session.firstResearchShape && !session.researchSpecification) {
|
|
168
|
+
return [];
|
|
169
|
+
}
|
|
170
|
+
const korean = locale === "ko";
|
|
171
|
+
if (!session.researchSpecification) {
|
|
172
|
+
return [
|
|
173
|
+
"",
|
|
174
|
+
korean ? "## Research Specification 상태" : "## Research Specification Status",
|
|
175
|
+
korean
|
|
176
|
+
? "- 상태: First Research Shape는 있지만 Research Specification은 아직 없습니다."
|
|
177
|
+
: "- Status: First Research Shape exists, but Research Specification is missing.",
|
|
178
|
+
korean
|
|
179
|
+
? "- 의미: First Research Shape는 짧은 핸들/재개 인덱스이며, 인터뷰 종료나 연구 명세 확정이 아닙니다."
|
|
180
|
+
: "- Meaning: First Research Shape is a short handle/resume index, not interview closure or a confirmed research specification.",
|
|
181
|
+
korean
|
|
182
|
+
? "- 다음 프로토콜: 충분한 내용이 있으면 `summarize_research_specification`으로 preview를 만들고 `confirm_research_specification`으로 저장/한 질문 더/섹션 수정/열어두기를 확인합니다."
|
|
183
|
+
: "- Next protocol: when enough detail exists, run `summarize_research_specification` to create the preview, then `confirm_research_specification` to confirm, ask one more question, revise a section, or keep it open."
|
|
184
|
+
];
|
|
185
|
+
}
|
|
186
|
+
const status = session.researchSpecification.confirmedAt
|
|
187
|
+
? "confirmed"
|
|
188
|
+
: session.researchSpecification.status ?? "draft";
|
|
189
|
+
if (status === "confirmed") {
|
|
190
|
+
return [];
|
|
191
|
+
}
|
|
192
|
+
return [
|
|
193
|
+
"",
|
|
194
|
+
korean ? "## Research Specification 상태" : "## Research Specification Status",
|
|
195
|
+
korean
|
|
196
|
+
? `- 상태: ${status}. Research Specification은 저장되어 있지만 아직 확정된 종료 지점이 아닙니다.`
|
|
197
|
+
: `- Status: ${status}. Research Specification exists, but it is not a confirmed closure point yet.`,
|
|
198
|
+
korean
|
|
199
|
+
? "- 다음 프로토콜: 명세를 업데이트한 뒤 `confirm_research_specification`으로 다시 preview 확인을 받아야 합니다."
|
|
200
|
+
: "- Next protocol: update the specification, then return to `confirm_research_specification` for another preview confirmation."
|
|
201
|
+
];
|
|
202
|
+
}
|
|
166
203
|
function buildCurrentGuide(project, session, recentInvocations = [], pendingQuestions = [], pendingObligations = []) {
|
|
167
204
|
const locale = normalizeLocale(session.locale ?? project.locale);
|
|
168
205
|
const openQuestions = session.openQuestions && session.openQuestions.length > 0
|
|
@@ -191,6 +228,7 @@ function buildCurrentGuide(project, session, recentInvocations = [], pendingQues
|
|
|
191
228
|
`- 다음 액션: ${nextAction}`,
|
|
192
229
|
`- 관점: ${session.requestedPerspectives.length > 0 ? session.requestedPerspectives.join(", ") : "auto"}`,
|
|
193
230
|
`- disagreement: ${session.disagreementPreference}`,
|
|
231
|
+
...renderResearchSpecificationStatus(session, locale),
|
|
194
232
|
"",
|
|
195
233
|
"## 열린 질문",
|
|
196
234
|
...openQuestions.map((question) => `- ${question}`),
|
|
@@ -210,7 +248,7 @@ function buildCurrentGuide(project, session, recentInvocations = [], pendingQues
|
|
|
210
248
|
"## 대기 중인 결정 질문",
|
|
211
249
|
...pendingQuestions.map((record) => {
|
|
212
250
|
const options = formatQuestionOptionValues(record).join("/");
|
|
213
|
-
return `- ${record.id}: ${record.prompt.question} (${options})`;
|
|
251
|
+
return `- ${record.id}: ${record.prompt.question}${formatQuestionMetadata(record)} (${options})`;
|
|
214
252
|
}),
|
|
215
253
|
"- 답변 기록: `longtable decide --question <id> --answer <value>`"
|
|
216
254
|
]
|
|
@@ -266,6 +304,7 @@ function buildCurrentGuide(project, session, recentInvocations = [], pendingQues
|
|
|
266
304
|
`- Next action: ${nextAction}`,
|
|
267
305
|
`- Perspectives: ${session.requestedPerspectives.length > 0 ? session.requestedPerspectives.join(", ") : "auto"}`,
|
|
268
306
|
`- Disagreement: ${session.disagreementPreference}`,
|
|
307
|
+
...renderResearchSpecificationStatus(session, locale),
|
|
269
308
|
"",
|
|
270
309
|
"## Open Questions",
|
|
271
310
|
...openQuestions.map((question) => `- ${question}`),
|
|
@@ -285,7 +324,7 @@ function buildCurrentGuide(project, session, recentInvocations = [], pendingQues
|
|
|
285
324
|
"## Pending Decision Questions",
|
|
286
325
|
...pendingQuestions.map((record) => {
|
|
287
326
|
const options = formatQuestionOptionValues(record).join("/");
|
|
288
|
-
return `- ${record.id}: ${record.prompt.question} (${options})`;
|
|
327
|
+
return `- ${record.id}: ${record.prompt.question}${formatQuestionMetadata(record)} (${options})`;
|
|
289
328
|
}),
|
|
290
329
|
"- Record an answer: `longtable decide --question <id> --answer <value>`"
|
|
291
330
|
]
|
|
@@ -375,6 +414,13 @@ function formatQuestionOptionValues(record) {
|
|
|
375
414
|
}
|
|
376
415
|
return values;
|
|
377
416
|
}
|
|
417
|
+
function formatQuestionMetadata(record) {
|
|
418
|
+
const parts = [
|
|
419
|
+
record.commitmentFamily ? `commitment: ${record.commitmentFamily}` : "",
|
|
420
|
+
record.epistemicBasis ? `basis: ${record.epistemicBasis}` : ""
|
|
421
|
+
].filter(Boolean);
|
|
422
|
+
return parts.length > 0 ? ` [${parts.join("; ")}]` : "";
|
|
423
|
+
}
|
|
378
424
|
function summarizeWorkspaceInspection(context, state) {
|
|
379
425
|
const questions = state.questionLog ?? [];
|
|
380
426
|
const pendingQuestions = questions.filter((record) => record.status === "pending");
|
|
@@ -435,6 +481,8 @@ function summarizeWorkspaceInspection(context, state) {
|
|
|
435
481
|
id: record.id,
|
|
436
482
|
title: record.prompt.title,
|
|
437
483
|
question: record.prompt.question,
|
|
484
|
+
...(record.commitmentFamily ? { commitmentFamily: record.commitmentFamily } : {}),
|
|
485
|
+
...(record.epistemicBasis ? { epistemicBasis: record.epistemicBasis } : {}),
|
|
438
486
|
options: formatQuestionOptionValues(record),
|
|
439
487
|
required: record.prompt.required
|
|
440
488
|
})),
|
|
@@ -449,6 +497,8 @@ function summarizeWorkspaceInspection(context, state) {
|
|
|
449
497
|
id: record.id,
|
|
450
498
|
checkpointKey: record.checkpointKey,
|
|
451
499
|
summary: record.summary,
|
|
500
|
+
...(record.commitmentFamily ? { commitmentFamily: record.commitmentFamily } : {}),
|
|
501
|
+
...(record.epistemicBasis ? { epistemicBasis: record.epistemicBasis } : {}),
|
|
452
502
|
...(record.selectedOption ? { selectedOption: record.selectedOption } : {}),
|
|
453
503
|
timestamp: record.timestamp
|
|
454
504
|
})),
|
|
@@ -495,9 +545,12 @@ function buildProjectAgentsMd(project, session) {
|
|
|
495
545
|
"- Begin exploratory work with clarifying or tension questions before recommending a direction.",
|
|
496
546
|
"- For `$longtable-interview`, ask one natural-language question at a time, reflect with `LongTable hears: ...`, record turns when MCP is available, and avoid early reader/reviewer or theory/method/measurement classification.",
|
|
497
547
|
"- Do not summarize `$longtable-interview` because a fixed number of turns has passed; wait for content-based readiness around research object, focal uncertainty, boundary, evidence/material, protected decision, and next action.",
|
|
548
|
+
"- First Research Shape is a short handle/resume index, not the default closure point.",
|
|
498
549
|
"- After the First Research Shape, create a Research Specification when the interview has enough detail to preserve scope, construct ontology, theory framing, coding rules, method options, evidence/access requirements, epistemic alignment, protected decisions, open questions, and next actions.",
|
|
550
|
+
"- If a confirmed First Research Shape exists without a Research Specification, continue directly into the next Research Specification question instead of asking shape-level continue/revise/restart questions.",
|
|
551
|
+
"- If the researcher chooses `ask_one_more` or `revise_section` at Research Specification confirmation, answer that gap and return to the Research Specification Preview before ending the interview.",
|
|
499
552
|
"- Do not let unrelated pending Researcher Checkpoints interrupt `$longtable-interview`; mention them only as separate unresolved checkpoints unless the researcher is confirming, saving, or recording a research decision.",
|
|
500
|
-
"- Use structured options
|
|
553
|
+
"- Use structured options at the final Research Specification confirmation, at explicit short-handle stop points, or at true checkpoint boundaries.",
|
|
501
554
|
"- If you foreground role perspectives, disclose them with `LongTable consulted: ...`.",
|
|
502
555
|
"- Keep one accountable synthesis, but do not hide meaningful disagreement.",
|
|
503
556
|
...(session.disagreementPreference === "always_visible"
|
|
@@ -1668,6 +1721,70 @@ export function generateQuestionOpportunities(prompt, options = {}) {
|
|
|
1668
1721
|
};
|
|
1669
1722
|
}
|
|
1670
1723
|
const FOLLOW_UP_PROMPT_PREFIX = "Follow-up prompt:";
|
|
1724
|
+
function compactMetadataText(parts) {
|
|
1725
|
+
return parts
|
|
1726
|
+
.flatMap((part) => Array.isArray(part) ? part : [part])
|
|
1727
|
+
.filter((part) => Boolean(part && part.trim()))
|
|
1728
|
+
.join(" ")
|
|
1729
|
+
.replace(/\s+/g, " ")
|
|
1730
|
+
.trim()
|
|
1731
|
+
.toLowerCase();
|
|
1732
|
+
}
|
|
1733
|
+
function textMatchesAny(text, patterns) {
|
|
1734
|
+
return patterns.some((pattern) => pattern.test(text));
|
|
1735
|
+
}
|
|
1736
|
+
const COMMITMENT_FAMILY_BY_CHECKPOINT = [
|
|
1737
|
+
[/product|meta_decision/, "product_policy"],
|
|
1738
|
+
[/research_question|research_direction|scope|boundary|inclusion|exclusion/, "scope"],
|
|
1739
|
+
[/theory|construct|conceptual/, "construct"],
|
|
1740
|
+
[/measurement|coding|codebook|extraction/, "coding"],
|
|
1741
|
+
[/method|analysis|panel_disagreement|team_debate|review/, "method"],
|
|
1742
|
+
[/evidence|scholarly_access|source_authority/, "evidence"],
|
|
1743
|
+
[/knowledge_gap|tacit_assumption|epistemic/, "epistemic_authority"]
|
|
1744
|
+
];
|
|
1745
|
+
function inferCommitmentFamily(input) {
|
|
1746
|
+
const checkpointKey = (input.checkpointKey ?? "").toLowerCase();
|
|
1747
|
+
const matched = COMMITMENT_FAMILY_BY_CHECKPOINT.find(([pattern]) => pattern.test(checkpointKey));
|
|
1748
|
+
if (matched)
|
|
1749
|
+
return matched[1];
|
|
1750
|
+
if (input.triggerFamily === "meta_decision")
|
|
1751
|
+
return "product_policy";
|
|
1752
|
+
if (input.triggerFamily === "evidence")
|
|
1753
|
+
return "evidence";
|
|
1754
|
+
const text = compactMetadataText([input.title, input.question, input.prompt, input.rationale]);
|
|
1755
|
+
if (textMatchesAny(text, [/checkpoint policy/, /hook ux/, /product language/, /\breadme\b/, /제품 언어|체크포인트 정책|훅|리드미/])) {
|
|
1756
|
+
return "product_policy";
|
|
1757
|
+
}
|
|
1758
|
+
return undefined;
|
|
1759
|
+
}
|
|
1760
|
+
function inferEpistemicBasis(input) {
|
|
1761
|
+
const text = compactMetadataText([input.title, input.question, input.prompt, input.rationale]);
|
|
1762
|
+
const bases = [];
|
|
1763
|
+
if (textMatchesAny(text, [/\bresearcher\b/, /\bhuman\b/, /\byour judgment\b/, /\byour knowledge\b/, /연구자|인간|사람|너의\s*판단|당신의\s*판단|내\s*지식|사용자/])) {
|
|
1764
|
+
bases.push("researcher_knowledge");
|
|
1765
|
+
}
|
|
1766
|
+
if (textMatchesAny(text, [/\bproject state\b/, /\bworkspace\b/, /\bcurrent\.md\b/, /\.longtable\b/, /\bstate\.json\b/, /\bdataset\b/, /\bcodebook\b/, /\bcoding sheet\b/, /프로젝트\s*상태|워크스페이스|데이터셋|코드북|코딩\s*시트/])) {
|
|
1767
|
+
bases.push("project_state");
|
|
1768
|
+
}
|
|
1769
|
+
if (textMatchesAny(text, [/\bexternal evidence\b/, /\bliterature\b/, /\bpaper\b/, /\bpdf\b/, /\bsource\b/, /\bcitation\b/, /\breference\b/, /\bfull[- ]?text\b/, /외부\s*근거|문헌|논문|원문|전문|출처|인용|레퍼런스/])) {
|
|
1770
|
+
bases.push("external_evidence");
|
|
1771
|
+
}
|
|
1772
|
+
if (textMatchesAny(text, [/\bcodex\b/, /\bllm\b/, /\blanguage model\b/, /\bmodel judgment\b/, /\bai inference\b/, /\bassistant judgment\b/, /코덱스|언어\s*모델|모델\s*판단|AI\s*추론|LLM/])) {
|
|
1773
|
+
bases.push("ai_inference");
|
|
1774
|
+
}
|
|
1775
|
+
const unique = [...new Set(bases)];
|
|
1776
|
+
if (unique.length > 1)
|
|
1777
|
+
return "mixed";
|
|
1778
|
+
return unique[0];
|
|
1779
|
+
}
|
|
1780
|
+
function resolveQuestionRecordMetadata(input) {
|
|
1781
|
+
const commitmentFamily = input.commitmentFamily ?? inferCommitmentFamily(input);
|
|
1782
|
+
const epistemicBasis = input.epistemicBasis ?? inferEpistemicBasis(input);
|
|
1783
|
+
return {
|
|
1784
|
+
...(commitmentFamily ? { commitmentFamily } : {}),
|
|
1785
|
+
...(epistemicBasis ? { epistemicBasis } : {})
|
|
1786
|
+
};
|
|
1787
|
+
}
|
|
1671
1788
|
function hasFollowUpPrompt(record, prompt) {
|
|
1672
1789
|
return record.prompt.rationale.includes(`${FOLLOW_UP_PROMPT_PREFIX} ${prompt}`);
|
|
1673
1790
|
}
|
|
@@ -1705,31 +1822,43 @@ export async function createWorkspaceFollowUpQuestions(options) {
|
|
|
1705
1822
|
if (specsToCreate.length === 0) {
|
|
1706
1823
|
return { questions: pendingMatches, state, created: false, alreadyAnswered: false };
|
|
1707
1824
|
}
|
|
1708
|
-
const questions = specsToCreate.map((spec) =>
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1825
|
+
const questions = specsToCreate.map((spec) => {
|
|
1826
|
+
const checkpointKey = `follow_up_${spec.key}`;
|
|
1827
|
+
const rationale = [
|
|
1828
|
+
spec.whyNow,
|
|
1829
|
+
`Question kind: ${spec.kind}`,
|
|
1830
|
+
`Question confidence: ${spec.confidence}`,
|
|
1831
|
+
`${FOLLOW_UP_PROMPT_PREFIX} ${options.prompt}`
|
|
1832
|
+
];
|
|
1833
|
+
const metadata = resolveQuestionRecordMetadata({
|
|
1834
|
+
checkpointKey,
|
|
1716
1835
|
title: spec.title,
|
|
1717
1836
|
question: spec.question,
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1837
|
+
prompt: options.prompt,
|
|
1838
|
+
rationale
|
|
1839
|
+
});
|
|
1840
|
+
return {
|
|
1841
|
+
id: createId("question_record"),
|
|
1842
|
+
createdAt,
|
|
1843
|
+
updatedAt: createdAt,
|
|
1844
|
+
status: "pending",
|
|
1845
|
+
...metadata,
|
|
1846
|
+
prompt: {
|
|
1847
|
+
id: createId("question_prompt"),
|
|
1848
|
+
checkpointKey,
|
|
1849
|
+
title: spec.title,
|
|
1850
|
+
question: spec.question,
|
|
1851
|
+
type: "single_choice",
|
|
1852
|
+
options: spec.options,
|
|
1853
|
+
allowOther: true,
|
|
1854
|
+
otherLabel: "Other",
|
|
1855
|
+
required: options.required ?? spec.required,
|
|
1856
|
+
source: "runtime_guidance",
|
|
1857
|
+
rationale,
|
|
1858
|
+
preferredSurfaces: preferredSurfaces
|
|
1859
|
+
}
|
|
1860
|
+
};
|
|
1861
|
+
});
|
|
1733
1862
|
const updated = appendQuestionRecords(state, questions);
|
|
1734
1863
|
await writeFile(options.context.stateFilePath, JSON.stringify(updated, null, 2), "utf8");
|
|
1735
1864
|
await syncCurrentWorkspaceView(options.context);
|
|
@@ -1743,16 +1872,35 @@ export async function createWorkspaceQuestion(options) {
|
|
|
1743
1872
|
});
|
|
1744
1873
|
const checkpointKey = options.checkpointKey ?? trigger.signal.checkpointKey;
|
|
1745
1874
|
const createdAt = nowIso();
|
|
1875
|
+
const title = options.title ?? questionTitleForCheckpoint(trigger.family, checkpointKey);
|
|
1876
|
+
const questionText = options.question ?? questionTextForCheckpoint(trigger.family, options.prompt, checkpointKey);
|
|
1877
|
+
const rationale = [
|
|
1878
|
+
...trigger.rationale,
|
|
1879
|
+
`Trigger family: ${trigger.family}.`,
|
|
1880
|
+
`Trigger confidence: ${trigger.confidence}.`,
|
|
1881
|
+
`Original prompt: ${options.prompt}`
|
|
1882
|
+
];
|
|
1883
|
+
const metadata = resolveQuestionRecordMetadata({
|
|
1884
|
+
checkpointKey,
|
|
1885
|
+
triggerFamily: trigger.family,
|
|
1886
|
+
title,
|
|
1887
|
+
question: questionText,
|
|
1888
|
+
prompt: options.prompt,
|
|
1889
|
+
rationale,
|
|
1890
|
+
commitmentFamily: options.commitmentFamily,
|
|
1891
|
+
epistemicBasis: options.epistemicBasis
|
|
1892
|
+
});
|
|
1746
1893
|
const question = {
|
|
1747
1894
|
id: createId("question_record"),
|
|
1748
1895
|
createdAt,
|
|
1749
1896
|
updatedAt: createdAt,
|
|
1750
1897
|
status: "pending",
|
|
1898
|
+
...metadata,
|
|
1751
1899
|
prompt: {
|
|
1752
1900
|
id: createId("question_prompt"),
|
|
1753
1901
|
checkpointKey,
|
|
1754
|
-
title
|
|
1755
|
-
question:
|
|
1902
|
+
title,
|
|
1903
|
+
question: questionText,
|
|
1756
1904
|
type: "single_choice",
|
|
1757
1905
|
options: options.questionOptions ?? optionsForCheckpointTrigger(trigger.family, checkpointKey),
|
|
1758
1906
|
allowOther: true,
|
|
@@ -1760,12 +1908,7 @@ export async function createWorkspaceQuestion(options) {
|
|
|
1760
1908
|
required: options.required ?? trigger.requiresQuestionBeforeClosure,
|
|
1761
1909
|
source: "checkpoint",
|
|
1762
1910
|
displayReason: options.displayReason ?? trigger.rationale[0],
|
|
1763
|
-
rationale
|
|
1764
|
-
...trigger.rationale,
|
|
1765
|
-
`Trigger family: ${trigger.family}.`,
|
|
1766
|
-
`Trigger confidence: ${trigger.confidence}.`,
|
|
1767
|
-
`Original prompt: ${options.prompt}`
|
|
1768
|
-
],
|
|
1911
|
+
rationale,
|
|
1769
1912
|
preferredSurfaces: options.provider === "claude"
|
|
1770
1913
|
? ["native_structured", "numbered"]
|
|
1771
1914
|
: ["mcp_elicitation", "numbered"]
|
|
@@ -1892,6 +2035,8 @@ export async function answerWorkspaceQuestion(options) {
|
|
|
1892
2035
|
level: question.prompt.required ? "adaptive_required" : "recommended",
|
|
1893
2036
|
mode: "commit",
|
|
1894
2037
|
summary: `Answered ${question.prompt.title}: ${answer.selectedLabels.join(", ")}`,
|
|
2038
|
+
...(question.commitmentFamily ? { commitmentFamily: question.commitmentFamily } : {}),
|
|
2039
|
+
...(question.epistemicBasis ? { epistemicBasis: question.epistemicBasis } : {}),
|
|
1895
2040
|
selectedOption: answer.selectedValues[0],
|
|
1896
2041
|
...(rationale ? { rationale } : {})
|
|
1897
2042
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@longtable/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.47",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Researcher-facing LongTable CLI",
|
|
6
6
|
"type": "module",
|
|
@@ -29,12 +29,12 @@
|
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@clack/prompts": "^1.2.0",
|
|
32
|
-
"@longtable/checkpoints": "0.1.
|
|
33
|
-
"@longtable/core": "0.1.
|
|
34
|
-
"@longtable/memory": "0.1.
|
|
35
|
-
"@longtable/provider-claude": "0.1.
|
|
36
|
-
"@longtable/provider-codex": "0.1.
|
|
37
|
-
"@longtable/setup": "0.1.
|
|
32
|
+
"@longtable/checkpoints": "0.1.47",
|
|
33
|
+
"@longtable/core": "0.1.47",
|
|
34
|
+
"@longtable/memory": "0.1.47",
|
|
35
|
+
"@longtable/provider-claude": "0.1.47",
|
|
36
|
+
"@longtable/provider-codex": "0.1.47",
|
|
37
|
+
"@longtable/setup": "0.1.47"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@types/node": "^22.10.1",
|