@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 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 = "0.1.39";
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 (blockingQuestion) {
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
- if (blockingQuestion) {
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
- const blockingObligation = pendingObligations(runtime.state)[0];
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 buildActiveInterviewContext(interview);
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
- if (!isStateChangingBash(command)) {
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-changing Bash command.", buildPendingQuestionContext(blockingQuestion));
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-changing Bash command.", buildPendingObligationContext(blockingObligation));
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) && isStateChangingBash(command)) {
345
- return buildBlockOutput("PostToolUse", "A state-changing Bash command completed while LongTable still had an unresolved checkpoint or obligation.", blockingQuestion
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;
@@ -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
- const usableTurns = turns.filter((turn) => turn.quality !== "thin").length;
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.40",
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.40",
33
- "@longtable/core": "0.1.40",
34
- "@longtable/memory": "0.1.40",
35
- "@longtable/provider-claude": "0.1.40",
36
- "@longtable/provider-codex": "0.1.40",
37
- "@longtable/setup": "0.1.40"
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",