@longtable/cli 0.1.40 → 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/dist/cli.js +4 -1
- package/dist/longtable-codex-native-hook.js +83 -16
- package/dist/project-session.d.ts +4 -0
- package/dist/project-session.js +24 -4
- package/package.json +7 -7
package/dist/cli.js
CHANGED
|
@@ -3,6 +3,7 @@ import { existsSync, readFileSync, statSync } from "node:fs";
|
|
|
3
3
|
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
4
4
|
import { execSync } from "node:child_process";
|
|
5
5
|
import { createInterface } from "node:readline/promises";
|
|
6
|
+
import { createRequire } from "node:module";
|
|
6
7
|
import { stdin as input, stdout as output, cwd, env, exit } from "node:process";
|
|
7
8
|
import { dirname, join, resolve } from "node:path";
|
|
8
9
|
import { homedir } from "node:os";
|
|
@@ -44,8 +45,10 @@ const ANSI = {
|
|
|
44
45
|
cyan: "\u001B[36m",
|
|
45
46
|
green: "\u001B[32m"
|
|
46
47
|
};
|
|
48
|
+
const require = createRequire(import.meta.url);
|
|
49
|
+
const LONGTABLE_PACKAGE_VERSION = String(require("../package.json").version ?? "0.0.0");
|
|
47
50
|
const LONGTABLE_MCP_SERVER_NAME = "longtable-state";
|
|
48
|
-
const LONGTABLE_MCP_PACKAGE_VERSION =
|
|
51
|
+
const LONGTABLE_MCP_PACKAGE_VERSION = LONGTABLE_PACKAGE_VERSION;
|
|
49
52
|
const LONGTABLE_MCP_MARKER_START = "# LongTable state MCP START";
|
|
50
53
|
const LONGTABLE_MCP_MARKER_END = "# LongTable state MCP END";
|
|
51
54
|
function style(text, prefix) {
|
|
@@ -129,6 +129,33 @@ function looksLikeResearchDomainPrompt(prompt) {
|
|
|
129
129
|
function looksLikeResearchCommitmentPrompt(prompt) {
|
|
130
130
|
return looksLikeResearchDomainPrompt(prompt) && looksLikeClosurePrompt(prompt);
|
|
131
131
|
}
|
|
132
|
+
function looksLikeExplicitInterviewPrompt(prompt) {
|
|
133
|
+
const normalized = prompt.trim();
|
|
134
|
+
if (!normalized) {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
if (/\$longtable-interview\b/i.test(normalized)) {
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
if (looksLikeLongTableProductOrToolingPrompt(normalized)) {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
return /\bLongTable\b.*\binterview\b/i.test(normalized)
|
|
144
|
+
|| /\bfirst research shape\b/i.test(normalized)
|
|
145
|
+
|| /롱테이블.*인터뷰|LongTable.*인터뷰|First Research Shape/i.test(normalized);
|
|
146
|
+
}
|
|
147
|
+
function looksLikeResearchStateConfirmationPrompt(prompt) {
|
|
148
|
+
if (looksLikeLongTableProductOrToolingPrompt(prompt) && !looksLikeExplicitInterviewPrompt(prompt)) {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
return looksLikeResearchCommitmentPrompt(prompt)
|
|
152
|
+
|| (looksLikeExplicitInterviewPrompt(prompt) && looksLikeClosurePrompt(prompt))
|
|
153
|
+
|| /\b(confirm|summarize|save|store|record)\b.*\b(first research shape|research direction|research shape)\b/i.test(prompt)
|
|
154
|
+
|| /(First Research Shape|연구\s*방향|연구\s*형태).*(확정|저장|기록|요약)/.test(prompt);
|
|
155
|
+
}
|
|
156
|
+
function shouldSurfaceInterviewContext(prompt) {
|
|
157
|
+
return looksLikeExplicitInterviewPrompt(prompt) || looksLikeResearchStateConfirmationPrompt(prompt);
|
|
158
|
+
}
|
|
132
159
|
function buildResponseOnlyAdvisoryQuestions(prompt) {
|
|
133
160
|
if (looksLikeLongTableProductOrToolingPrompt(prompt)) {
|
|
134
161
|
return [];
|
|
@@ -167,6 +194,14 @@ function isStateChangingBash(command) {
|
|
|
167
194
|
return /\b(git\s+commit|npm\s+version|mv|cp|rm|sed\s+-i|perl\s+-i|tee|touch|mkdir|rmdir|apply_patch|patch)\b/.test(normalized)
|
|
168
195
|
|| />\s*\S+/.test(normalized);
|
|
169
196
|
}
|
|
197
|
+
function mutatesLongTableResearchState(command) {
|
|
198
|
+
const normalized = command.trim();
|
|
199
|
+
if (!normalized) {
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
return /\.longtable(?:\/|\b)|\bCURRENT\.md\b/.test(normalized)
|
|
203
|
+
|| /\blongtable\s+(?:start|question|clear-question|prune-questions|ask|clarify|panel|team)\b/.test(normalized);
|
|
204
|
+
}
|
|
170
205
|
async function loadLongTableRuntime(startPath) {
|
|
171
206
|
const context = await loadProjectContextFromDirectory(startPath);
|
|
172
207
|
if (!context) {
|
|
@@ -206,6 +241,13 @@ function buildPendingQuestionContext(question) {
|
|
|
206
241
|
"Do not choose or record an answer unless the researcher explicitly provides the selection."
|
|
207
242
|
].join("\n");
|
|
208
243
|
}
|
|
244
|
+
function buildSeparatePendingQuestionNotice(question) {
|
|
245
|
+
return [
|
|
246
|
+
`Separate unresolved Researcher Checkpoint: ${question.prompt.title}.`,
|
|
247
|
+
`Question: ${question.prompt.question}`,
|
|
248
|
+
"This is not part of the active interview. Keep it visible, but do not answer or record it unless the researcher explicitly provides the selection."
|
|
249
|
+
].join("\n");
|
|
250
|
+
}
|
|
209
251
|
function buildGeneratedQuestionsContext(questions, created) {
|
|
210
252
|
const lines = [
|
|
211
253
|
created
|
|
@@ -239,6 +281,12 @@ function buildPendingObligationContext(obligation) {
|
|
|
239
281
|
: "Resume the LongTable interview and let it ask the next research-facing checkpoint before settling the direction."
|
|
240
282
|
].join("\n");
|
|
241
283
|
}
|
|
284
|
+
function buildSeparatePendingObligationNotice(obligation) {
|
|
285
|
+
return [
|
|
286
|
+
`Separate unresolved LongTable obligation: ${obligation.prompt}`,
|
|
287
|
+
"This is not part of the active interview. Keep it visible only when the researcher is settling or saving the research direction."
|
|
288
|
+
].join("\n");
|
|
289
|
+
}
|
|
242
290
|
function buildActiveInterviewContext(hook) {
|
|
243
291
|
const turnCount = hook.turns?.length ?? 0;
|
|
244
292
|
return [
|
|
@@ -254,30 +302,48 @@ function sessionStartContext(runtime) {
|
|
|
254
302
|
const interview = activeInterviewHook(runtime.state);
|
|
255
303
|
const needsDetailedSummary = Boolean(blockingQuestion || blockingObligation);
|
|
256
304
|
const sections = [buildWorkspaceSummary(runtime, needsDetailedSummary ? "full" : "compact").join("\n")];
|
|
257
|
-
if (
|
|
305
|
+
if (interview) {
|
|
306
|
+
sections.push(buildActiveInterviewContext(interview));
|
|
307
|
+
if (blockingQuestion) {
|
|
308
|
+
sections.push(buildSeparatePendingQuestionNotice(blockingQuestion));
|
|
309
|
+
}
|
|
310
|
+
else if (blockingObligation) {
|
|
311
|
+
sections.push(buildSeparatePendingObligationNotice(blockingObligation));
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
else if (blockingQuestion) {
|
|
258
315
|
sections.push(buildPendingQuestionContext(blockingQuestion));
|
|
259
316
|
}
|
|
260
317
|
else if (blockingObligation) {
|
|
261
318
|
sections.push(buildPendingObligationContext(blockingObligation));
|
|
262
319
|
}
|
|
263
|
-
else if (interview) {
|
|
264
|
-
sections.push(buildActiveInterviewContext(interview));
|
|
265
|
-
}
|
|
266
320
|
sections.push("Treat `.longtable/` state and `CURRENT.md` as the source of truth for this workspace.");
|
|
267
321
|
return sections.filter(Boolean).join("\n\n");
|
|
268
322
|
}
|
|
269
323
|
async function userPromptSubmitContext(runtime, prompt) {
|
|
270
324
|
const blockingQuestion = pendingRequiredQuestions(runtime.state)[0];
|
|
271
|
-
|
|
325
|
+
const blockingObligation = pendingObligations(runtime.state)[0];
|
|
326
|
+
const interview = activeInterviewHook(runtime.state);
|
|
327
|
+
const shouldSurfaceInterview = shouldSurfaceInterviewContext(prompt);
|
|
328
|
+
const shouldSurfaceBlockingState = looksLikeResearchStateConfirmationPrompt(prompt);
|
|
329
|
+
if (interview && shouldSurfaceInterview) {
|
|
330
|
+
const sections = [buildActiveInterviewContext(interview)];
|
|
331
|
+
if (blockingQuestion) {
|
|
332
|
+
sections.push(buildSeparatePendingQuestionNotice(blockingQuestion));
|
|
333
|
+
}
|
|
334
|
+
else if (blockingObligation) {
|
|
335
|
+
sections.push(buildSeparatePendingObligationNotice(blockingObligation));
|
|
336
|
+
}
|
|
337
|
+
return sections.join("\n\n");
|
|
338
|
+
}
|
|
339
|
+
if (blockingQuestion && shouldSurfaceBlockingState) {
|
|
272
340
|
return buildPendingQuestionContext(blockingQuestion);
|
|
273
341
|
}
|
|
274
|
-
|
|
275
|
-
if (blockingObligation) {
|
|
342
|
+
if (blockingObligation && shouldSurfaceBlockingState) {
|
|
276
343
|
return buildPendingObligationContext(blockingObligation);
|
|
277
344
|
}
|
|
278
|
-
const interview = activeInterviewHook(runtime.state);
|
|
279
345
|
if (interview) {
|
|
280
|
-
return
|
|
346
|
+
return null;
|
|
281
347
|
}
|
|
282
348
|
const generatedQuestions = [];
|
|
283
349
|
let createdQuestions = false;
|
|
@@ -319,16 +385,17 @@ function preToolUseOutput(runtime, payload) {
|
|
|
319
385
|
return null;
|
|
320
386
|
}
|
|
321
387
|
const command = readCommandText(payload);
|
|
322
|
-
|
|
388
|
+
const stateChangingCommand = isStateChangingBash(command) || mutatesLongTableResearchState(command);
|
|
389
|
+
if (!stateChangingCommand) {
|
|
323
390
|
return null;
|
|
324
391
|
}
|
|
325
392
|
const blockingQuestion = pendingRequiredQuestions(runtime.state)[0];
|
|
326
|
-
if (blockingQuestion) {
|
|
327
|
-
return buildBlockOutput("PreToolUse", "A required LongTable checkpoint is still pending before a state
|
|
393
|
+
if (blockingQuestion && mutatesLongTableResearchState(command)) {
|
|
394
|
+
return buildBlockOutput("PreToolUse", "A required LongTable checkpoint is still pending before a research-state Bash command.", buildPendingQuestionContext(blockingQuestion));
|
|
328
395
|
}
|
|
329
396
|
const blockingObligation = pendingObligations(runtime.state)[0];
|
|
330
|
-
if (blockingObligation) {
|
|
331
|
-
return buildBlockOutput("PreToolUse", "A LongTable research obligation is still pending before a state
|
|
397
|
+
if (blockingObligation && mutatesLongTableResearchState(command)) {
|
|
398
|
+
return buildBlockOutput("PreToolUse", "A LongTable research obligation is still pending before a research-state Bash command.", buildPendingObligationContext(blockingObligation));
|
|
332
399
|
}
|
|
333
400
|
return null;
|
|
334
401
|
}
|
|
@@ -341,8 +408,8 @@ function postToolUseOutput(runtime, payload) {
|
|
|
341
408
|
const output = readCombinedOutput(payload);
|
|
342
409
|
const blockingQuestion = pendingRequiredQuestions(runtime.state)[0];
|
|
343
410
|
const blockingObligation = pendingObligations(runtime.state)[0];
|
|
344
|
-
if ((blockingQuestion || blockingObligation) &&
|
|
345
|
-
return buildBlockOutput("PostToolUse", "A state
|
|
411
|
+
if ((blockingQuestion || blockingObligation) && mutatesLongTableResearchState(command)) {
|
|
412
|
+
return buildBlockOutput("PostToolUse", "A research-state Bash command completed while LongTable still had an unresolved checkpoint or obligation.", blockingQuestion
|
|
346
413
|
? buildPendingQuestionContext(blockingQuestion)
|
|
347
414
|
: buildPendingObligationContext(blockingObligation));
|
|
348
415
|
}
|
|
@@ -44,6 +44,8 @@ export interface LongTableInterviewTurn {
|
|
|
44
44
|
quality: InterviewTurnQuality;
|
|
45
45
|
needsFollowUp: boolean;
|
|
46
46
|
followUpQuestion?: string;
|
|
47
|
+
readyToSummarize?: boolean;
|
|
48
|
+
readinessRationale?: string[];
|
|
47
49
|
rationale?: string[];
|
|
48
50
|
}
|
|
49
51
|
export interface LongTableHookRun {
|
|
@@ -204,6 +206,8 @@ export declare function appendLongTableInterviewTurn(options: {
|
|
|
204
206
|
quality?: InterviewTurnQuality;
|
|
205
207
|
needsFollowUp?: boolean;
|
|
206
208
|
followUpQuestion?: string;
|
|
209
|
+
readyToSummarize?: boolean;
|
|
210
|
+
readinessRationale?: string[];
|
|
207
211
|
rationale?: string[];
|
|
208
212
|
}): Promise<{
|
|
209
213
|
hook: LongTableHookRun;
|
package/dist/project-session.js
CHANGED
|
@@ -420,7 +420,9 @@ function buildProjectAgentsMd(project, session) {
|
|
|
420
420
|
"",
|
|
421
421
|
"## Research Behavior",
|
|
422
422
|
"- Begin exploratory work with clarifying or tension questions before recommending a direction.",
|
|
423
|
-
"- For `$longtable-interview`, ask one natural-language question at a time, reflect with `LongTable hears: ...`, and avoid early reader/reviewer or theory/method/measurement classification.",
|
|
423
|
+
"- 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.",
|
|
424
|
+
"- 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.",
|
|
425
|
+
"- 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.",
|
|
424
426
|
"- Use structured options only at the final First Research Shape confirmation or at true checkpoint boundaries.",
|
|
425
427
|
"- If you foreground role perspectives, disclose them with `LongTable consulted: ...`.",
|
|
426
428
|
"- Keep one accountable synthesis, but do not hide meaningful disagreement.",
|
|
@@ -580,10 +582,10 @@ function defaultFollowUpQuestion(answer) {
|
|
|
580
582
|
return "What concrete scene, case, material, text, dataset, or decision would make that problem easier to inspect first?";
|
|
581
583
|
}
|
|
582
584
|
function depthForInterview(turns = []) {
|
|
583
|
-
|
|
584
|
-
if (usableTurns >= 3) {
|
|
585
|
+
if (turns.some((turn) => turn.readyToSummarize === true && turn.quality !== "thin")) {
|
|
585
586
|
return "ready_to_summarize";
|
|
586
587
|
}
|
|
588
|
+
const usableTurns = turns.filter((turn) => turn.quality !== "thin").length;
|
|
587
589
|
if (usableTurns >= 1) {
|
|
588
590
|
return "forming_first_handle";
|
|
589
591
|
}
|
|
@@ -614,6 +616,15 @@ export async function beginLongTableInterview(options) {
|
|
|
614
616
|
if (existing) {
|
|
615
617
|
return { hook: existing, state };
|
|
616
618
|
}
|
|
619
|
+
const confirmedShape = state.firstResearchShape?.confirmedAt ? state.firstResearchShape : undefined;
|
|
620
|
+
if (confirmedShape) {
|
|
621
|
+
const confirmedHook = [...(state.hooks ?? [])].reverse().find((hook) => hook.kind === "longtable_interview" &&
|
|
622
|
+
hook.status === "confirmed" &&
|
|
623
|
+
hook.firstResearchShape?.handle === confirmedShape.handle);
|
|
624
|
+
if (confirmedHook) {
|
|
625
|
+
return { hook: confirmedHook, state };
|
|
626
|
+
}
|
|
627
|
+
}
|
|
617
628
|
const timestamp = nowIso();
|
|
618
629
|
const hook = {
|
|
619
630
|
id: createId("hook_interview"),
|
|
@@ -654,6 +665,10 @@ export async function appendLongTableInterviewTurn(options) {
|
|
|
654
665
|
const followUpQuestion = needsFollowUp
|
|
655
666
|
? options.followUpQuestion ?? defaultFollowUpQuestion(options.answer)
|
|
656
667
|
: options.followUpQuestion;
|
|
668
|
+
const readyToSummarize = options.readyToSummarize === true && quality !== "thin";
|
|
669
|
+
const readinessRationale = options.readinessRationale
|
|
670
|
+
?.map((rationale) => rationale.trim())
|
|
671
|
+
.filter(Boolean);
|
|
657
672
|
const timestamp = nowIso();
|
|
658
673
|
const turns = existing.turns ?? [];
|
|
659
674
|
const turn = {
|
|
@@ -666,6 +681,8 @@ export async function appendLongTableInterviewTurn(options) {
|
|
|
666
681
|
quality,
|
|
667
682
|
needsFollowUp,
|
|
668
683
|
...(followUpQuestion?.trim() ? { followUpQuestion: followUpQuestion.trim() } : {}),
|
|
684
|
+
...(readyToSummarize ? { readyToSummarize } : {}),
|
|
685
|
+
...(readinessRationale && readinessRationale.length > 0 ? { readinessRationale } : {}),
|
|
669
686
|
...(options.rationale && options.rationale.length > 0 ? { rationale: options.rationale } : {})
|
|
670
687
|
};
|
|
671
688
|
const nextTurns = [...turns, turn];
|
|
@@ -678,7 +695,10 @@ export async function appendLongTableInterviewTurn(options) {
|
|
|
678
695
|
turns: nextTurns,
|
|
679
696
|
qualityNotes: [
|
|
680
697
|
...(existing.qualityNotes ?? []),
|
|
681
|
-
...(needsFollowUp ? [`Turn ${turn.index} needs follow-up: ${followUpQuestion}`] : [])
|
|
698
|
+
...(needsFollowUp ? [`Turn ${turn.index} needs follow-up: ${followUpQuestion}`] : []),
|
|
699
|
+
...(readyToSummarize
|
|
700
|
+
? [`Turn ${turn.index} marked ready to summarize: ${(readinessRationale ?? ["content-based readiness signal"]).join("; ")}`]
|
|
701
|
+
: [])
|
|
682
702
|
]
|
|
683
703
|
};
|
|
684
704
|
const updated = upsertHook(state, hook);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@longtable/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.42",
|
|
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.42",
|
|
33
|
+
"@longtable/core": "0.1.42",
|
|
34
|
+
"@longtable/memory": "0.1.42",
|
|
35
|
+
"@longtable/provider-claude": "0.1.42",
|
|
36
|
+
"@longtable/provider-codex": "0.1.42",
|
|
37
|
+
"@longtable/setup": "0.1.42"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@types/node": "^22.10.1",
|