@longtable/cli 0.1.31 → 0.1.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,9 +8,9 @@ Claude skills, and future MCP surfaces remain generated adapter artifacts.
8
8
 
9
9
  The basic contract is:
10
10
 
11
- 1. seed the researcher profile once
12
- 2. create a workspace for each project
13
- 3. continue the research conversation inside that workspace
11
+ 1. approve provider/runtime support once
12
+ 2. start each project inside the provider with `$longtable-interview`
13
+ 3. create or resume a workspace from that interview
14
14
  4. preserve decisions, tensions, and evidence as durable project state
15
15
 
16
16
  ## Install
@@ -26,15 +26,17 @@ config, hooks, or provider runtime files without explicit setup approval.
26
26
 
27
27
  ```bash
28
28
  longtable setup --provider codex
29
- longtable start
30
- cd "<project-path>"
29
+ cd "<research-folder>"
31
30
  codex
32
31
  ```
33
32
 
33
+ Then invoke `$longtable-interview` inside Codex.
34
+
34
35
  `longtable setup --provider codex` is the permission-first setup route. It asks
35
36
  where LongTable may install support, which runtime surfaces it may enable, how
36
- strongly it may interrupt research decisions, and whether to create a project
37
- workspace now. `longtable init` remains only as a deprecated compatibility alias.
37
+ strongly it may interrupt research decisions, and whether to show the
38
+ provider-native interview launch steps. `longtable init` remains only as a
39
+ deprecated compatibility alias.
38
40
 
39
41
  Return later:
40
42
 
@@ -44,7 +46,7 @@ longtable resume
44
46
  codex
45
47
  ```
46
48
 
47
- ## What `longtable start` Creates
49
+ ## What `$longtable-interview` Creates
48
50
 
49
51
  ```text
50
52
  <project>/
@@ -84,7 +86,6 @@ This is how LongTable avoids turning tacit knowledge into fake certainty.
84
86
 
85
87
  ```bash
86
88
  longtable setup
87
- longtable start
88
89
  longtable resume --cwd "<project-path>"
89
90
  longtable roles
90
91
  longtable ask --cwd "<project-path>" --prompt "..."
@@ -136,12 +137,14 @@ longtable claude install-skills
136
137
 
137
138
  Codex skills include `longtable`, `longtable-panel`, and generated role-specific
138
139
  skills such as `longtable-methods-critic`. If your Codex build exposes explicit
139
- skill shortcuts, `$longtable` is the manual entry. Do not depend on `/prompts`;
140
- current Codex builds may reject it.
140
+ skill shortcuts, `$longtable-interview` is the research-start entry and
141
+ `$longtable` is the general router. Do not depend on `/prompts`; current Codex
142
+ builds may reject it.
141
143
 
142
144
  Claude Code skills include `longtable`, `longtable-panel`, and generated
143
- role-specific skills such as `longtable-methods-critic`. They are adapter files
144
- generated from the LongTable role registry.
145
+ role-specific skills such as `longtable-methods-critic`. They also include
146
+ `longtable-interview` for the First Research Shape workflow. They are adapter
147
+ files generated from the LongTable role registry.
145
148
 
146
149
  ## Panel Orchestration
147
150
 
package/dist/cli.js CHANGED
@@ -43,7 +43,7 @@ const ANSI = {
43
43
  green: "\u001B[32m"
44
44
  };
45
45
  const LONGTABLE_MCP_SERVER_NAME = "longtable-state";
46
- const LONGTABLE_MCP_PACKAGE_VERSION = "0.1.31";
46
+ const LONGTABLE_MCP_PACKAGE_VERSION = "0.1.32";
47
47
  const LONGTABLE_MCP_MARKER_START = "# LongTable state MCP START";
48
48
  const LONGTABLE_MCP_MARKER_END = "# LongTable state MCP END";
49
49
  function style(text, prefix) {
@@ -68,6 +68,20 @@ function renderBrandBanner(title, subtitle) {
68
68
  }
69
69
  return lines.join("\n");
70
70
  }
71
+ function renderInterviewLaunchSteps(provider) {
72
+ const command = provider === "codex" ? "codex" : "claude";
73
+ return renderSectionCard("LongTable Interview", [
74
+ "Setup is permission and runtime calibration, not the research interview.",
75
+ "The first research conversation now happens inside the provider so the model can listen, reflect, and ask one natural-language follow-up at a time.",
76
+ "",
77
+ "Next:",
78
+ "1. cd \"<research-folder>\"",
79
+ `2. run \`${command}\``,
80
+ "3. invoke `$longtable-interview`",
81
+ "",
82
+ "The interview will create or resume `.longtable/`, build a First Research Shape, and use option UI only for the final confirmation."
83
+ ]);
84
+ }
71
85
  function renderProgressBar(current, total) {
72
86
  const width = 10;
73
87
  const filled = Math.max(1, Math.round((current / total) * width));
@@ -77,11 +91,11 @@ function usage() {
77
91
  return [
78
92
  "Usage:",
79
93
  " Run `longtable ...` in your terminal, not inside the Codex chat box.",
80
- " After `longtable start`, move into the created project directory and open `codex` there.",
94
+ " LongTable research starts inside Codex or Claude with `$longtable-interview` after setup.",
81
95
  "",
82
96
  " longtable setup [--provider codex|claude] [--install-scope user|project|none] [--surfaces cli_only|skills|skills_mcp|skills_mcp_sentinel] [--intervention advisory|balanced|strong] [--checkpoint-ui off|interactive|strong] [--workspace create|later] [--project-dir <path>] [--json] [--dir <path>] [--skills-dir <path>] [--runtime-path <file>] [--setup-path <file>]",
83
97
  " longtable init [deprecated alias for setup; full legacy flags still supported for automation]",
84
- " longtable start [--path <dir>] [--name <project>] [--goal <text>] [--blocker <text>] [--research-object research_question|theory_framework|measurement_instrument|study_design|analysis_plan|manuscript] [--gap-risk known_gap|suspected_tacit_assumptions|diagnose] [--protected-decision theory|measurement|method|evidence_citation|authorship_voice|submission_public_sharing] [--perspectives <role[,role]>] [--disagreement synthesis_only|show_on_conflict|always_visible] [--setup <path>] [--json]",
98
+ " longtable start [deprecated fallback] [--path <dir>] [--name <project>] [--goal <text>] [--blocker <text>] [--research-object research_question|theory_framework|measurement_instrument|study_design|analysis_plan|manuscript] [--gap-risk known_gap|suspected_tacit_assumptions|diagnose] [--protected-decision theory|measurement|method|evidence_citation|authorship_voice|submission_public_sharing] [--perspectives <role[,role]>] [--disagreement synthesis_only|show_on_conflict|always_visible] [--setup <path>] [--json] [--no-interview]",
85
99
  " longtable resume [--cwd <path>] [--json]",
86
100
  " longtable doctor [--cwd <path>] [--fix] [--json] [--codex-dir <path>] [--codex-config <path>] [--claude-dir <path>] [--codex-prompts-dir <path>] [--codex-runtime-path <file>] [--claude-runtime-path <file>]",
87
101
  " longtable status [--cwd <path>] [--fix] [--json] [--codex-dir <path>] [--codex-config <path>] [--claude-dir <path>] [--codex-prompts-dir <path>] [--codex-runtime-path <file>] [--claude-runtime-path <file>]",
@@ -114,9 +128,9 @@ function usage() {
114
128
  "",
115
129
  "Examples:",
116
130
  " longtable setup --provider codex",
117
- " longtable start",
118
- " longtable start --path ~/Research/My-Project --name \"AI Adoption Meta-Analysis\" --goal \"Narrow the review question\"",
119
- " cd \"<project-path>\" && codex",
131
+ " cd \"<research-folder>\" && codex",
132
+ " $longtable-interview",
133
+ " longtable start --no-interview --path ~/Research/My-Project --name \"AI Adoption Meta-Analysis\" --goal \"Narrow the review question\"",
120
134
  " longtable doctor",
121
135
  " longtable roles",
122
136
  " longtable ask --prompt \"연구를 시작하고 싶어. 지금 어디서부터 좁혀야 할지 모르겠어.\"",
@@ -447,13 +461,13 @@ function buildPermissionSetupChoices() {
447
461
  workspace: [
448
462
  {
449
463
  id: "create",
450
- label: "Yes, create one now",
451
- description: "Why: durable state needs .longtable/. What you get: decision log and CURRENT.md. Tradeoff: asks project-specific questions now."
464
+ label: "Show interview launch steps",
465
+ description: "Why: research should start inside the provider. What you get: setup finishes with Codex/Claude + $longtable-interview steps. Tradeoff: workspace creation waits for the in-provider interview."
452
466
  },
453
467
  {
454
468
  id: "later",
455
469
  label: "No, prepare runtime only",
456
- description: "Why: keeps setup short. What you get: runtime support without project state. Tradeoff: no durable research memory until `longtable start`."
470
+ description: "Why: keeps setup short. What you get: runtime support without project state. Tradeoff: no durable research memory until `$longtable-interview` creates or resumes a workspace."
457
471
  }
458
472
  ]
459
473
  };
@@ -575,6 +589,8 @@ async function runSetup(args) {
575
589
  interventionPosture: effectiveIntervention,
576
590
  checkpointUiMode: checkpointUi,
577
591
  workspaceCreationPreference: workspacePreference,
592
+ officialStartSurface: "$longtable-interview",
593
+ setupPosture: "permission_first",
578
594
  teamMode: "panel"
579
595
  };
580
596
  if (surfaces === "skills_mcp_sentinel") {
@@ -617,7 +633,12 @@ async function runSetup(args) {
617
633
  runtime: result,
618
634
  installedSkills: installedSkills.map((skill) => skill.name),
619
635
  mcpInstall,
620
- workspacePreference
636
+ workspacePreference,
637
+ nextStep: {
638
+ surface: "$longtable-interview",
639
+ command: provider === "codex" ? "codex" : "claude",
640
+ description: "Open the provider in the research folder and invoke `$longtable-interview`."
641
+ }
621
642
  }, null, 2));
622
643
  return;
623
644
  }
@@ -646,12 +667,11 @@ async function runSetup(args) {
646
667
  console.log("Background sentinel approval recorded.");
647
668
  console.log("Hook installation remains opt-in; LongTable will not install hooks without an explicit hook command.");
648
669
  }
670
+ console.log("");
671
+ console.log(renderInterviewLaunchSteps(provider));
649
672
  if (workspacePreference === "create") {
650
673
  console.log("");
651
- console.log("Project workspace requested. LongTable will now run `longtable start` with an adaptive start interview.");
652
- await runStart({
653
- setup: result.setupTarget.path
654
- });
674
+ console.log("Workspace launch requested. Open the provider in your research folder and run `$longtable-interview`; the interview will create `.longtable/` there.");
655
675
  }
656
676
  }
657
677
  function perspectiveChoices() {
@@ -849,6 +869,7 @@ function normalizePerspectiveList(value) {
849
869
  .filter(Boolean);
850
870
  }
851
871
  async function collectProjectInterview(setup, args) {
872
+ const skipResearchInterview = args["no-interview"] === true;
852
873
  const providedPerspectives = normalizePerspectiveList(typeof args.perspectives === "string" ? args.perspectives : undefined);
853
874
  const providedGoal = typeof args.goal === "string" && args.goal.trim() ? args.goal.trim() : undefined;
854
875
  const providedBlocker = typeof args.blocker === "string" && args.blocker.trim() ? args.blocker.trim() : undefined;
@@ -863,11 +884,11 @@ async function collectProjectInterview(setup, args) {
863
884
  : undefined;
864
885
  const needsInteractivePrompts = !(typeof args.name === "string" && args.name.trim()) ||
865
886
  !(typeof args.path === "string" && args.path.trim()) ||
866
- !providedGoal ||
867
- !providedBlocker ||
868
- !providedResearchObject ||
869
- !providedGapRisk ||
870
- !providedProtectedDecision;
887
+ (!skipResearchInterview && (!providedGoal ||
888
+ !providedBlocker ||
889
+ !providedResearchObject ||
890
+ !providedGapRisk ||
891
+ !providedProtectedDecision));
871
892
  if (needsInteractivePrompts) {
872
893
  console.log("");
873
894
  console.log(renderBrandBanner("LongTable", "Project workspace interview"));
@@ -887,15 +908,17 @@ async function collectProjectInterview(setup, args) {
887
908
  const projectPath = (typeof args.path === "string" && args.path.trim()
888
909
  ? normalizeUserPath(args.path.trim())
889
910
  : resolveInteractiveProjectPath((await promptText(renderQuestionHeader(2, 2, "Workspace", `Which parent directory should contain this project?\nLongTable will create this folder:\n${suggestedPath}`), true)), projectName));
890
- const adaptive = await collectAdaptiveStartInterview({
891
- currentGoal: providedGoal,
892
- currentBlocker: providedBlocker,
893
- needsResearchSeed: !providedGoal ||
894
- !providedBlocker ||
895
- !providedResearchObject ||
896
- !providedGapRisk ||
897
- !providedProtectedDecision
898
- });
911
+ const adaptive = skipResearchInterview
912
+ ? {}
913
+ : await collectAdaptiveStartInterview({
914
+ currentGoal: providedGoal,
915
+ currentBlocker: providedBlocker,
916
+ needsResearchSeed: !providedGoal ||
917
+ !providedBlocker ||
918
+ !providedResearchObject ||
919
+ !providedGapRisk ||
920
+ !providedProtectedDecision
921
+ });
899
922
  const currentGoal = providedGoal ?? adaptive.currentGoal;
900
923
  if (!currentGoal?.trim()) {
901
924
  throw new Error("LongTable start needs a current research goal or an opening interview answer.");
@@ -2773,6 +2796,26 @@ async function runRoles(args) {
2773
2796
  }
2774
2797
  }
2775
2798
  async function runStart(args) {
2799
+ const hasMinimalFallbackArgs = typeof args.name === "string" &&
2800
+ typeof args.path === "string" &&
2801
+ typeof args.goal === "string";
2802
+ const hasFallbackIntent = args["no-interview"] === true ||
2803
+ hasMinimalFallbackArgs;
2804
+ if (!hasFallbackIntent) {
2805
+ console.log(renderSectionCard("LongTable Start Has Moved", [
2806
+ "`longtable start` is now a fallback for automation and scripted workspace creation.",
2807
+ "The primary research-start experience is provider-native so LongTable can run a real interview instead of a terminal questionnaire.",
2808
+ "",
2809
+ "Use:",
2810
+ "1. longtable setup --provider codex",
2811
+ "2. cd \"<research-folder>\"",
2812
+ "3. codex",
2813
+ "4. $longtable-interview",
2814
+ "",
2815
+ "For automation, pass `--no-interview --json` with `--name`, `--path`, and `--goal`."
2816
+ ]));
2817
+ return;
2818
+ }
2776
2819
  const setupPath = typeof args.setup === "string" ? args.setup : undefined;
2777
2820
  const existingSetup = await loadOptionalSetup(setupPath);
2778
2821
  if (!existingSetup) {
@@ -19,6 +19,53 @@ export interface StartInterviewSession {
19
19
  inferredSignals: StartInterviewSignal[];
20
20
  summary: string;
21
21
  }
22
+ export type InterviewTurnQuality = "thin" | "usable" | "rich";
23
+ export type InterviewDepth = "gathering_context" | "forming_first_handle" | "ready_to_summarize";
24
+ export interface FirstResearchShape {
25
+ handle: string;
26
+ currentGoal: string;
27
+ currentBlocker?: string;
28
+ researchObject?: string;
29
+ gapRisk?: string;
30
+ protectedDecision?: string;
31
+ openQuestions: string[];
32
+ nextAction: string;
33
+ confidence: "low" | "medium" | "high";
34
+ sourceHookId?: string;
35
+ confirmedAt?: string;
36
+ }
37
+ export interface LongTableInterviewTurn {
38
+ id: string;
39
+ index: number;
40
+ createdAt: string;
41
+ question: string;
42
+ answer: string;
43
+ reflection?: string;
44
+ quality: InterviewTurnQuality;
45
+ needsFollowUp: boolean;
46
+ followUpQuestion?: string;
47
+ rationale?: string[];
48
+ }
49
+ export interface LongTableHookRun {
50
+ id: string;
51
+ kind: "longtable_interview" | "quality_probe" | "checkpoint" | "panel_decision";
52
+ status: "pending" | "active" | "ready_to_confirm" | "confirmed" | "deferred" | "cancelled";
53
+ createdAt: string;
54
+ updatedAt: string;
55
+ targetOutcome?: "first_research_handle" | string;
56
+ depth?: InterviewDepth;
57
+ provider?: ProviderKind;
58
+ turns?: LongTableInterviewTurn[];
59
+ firstResearchShape?: FirstResearchShape;
60
+ qualityNotes?: string[];
61
+ rationale?: string[];
62
+ linkedQuestionRecordIds?: string[];
63
+ linkedDecisionRecordIds?: string[];
64
+ }
65
+ export type LongTableWorkspaceState = ResearchState & {
66
+ hooks?: LongTableHookRun[];
67
+ firstResearchShape?: FirstResearchShape;
68
+ };
22
69
  export interface LongTableProjectRecord {
23
70
  schemaVersion: 1;
24
71
  product: "LongTable";
@@ -52,6 +99,7 @@ export interface LongTableSessionRecord {
52
99
  nextAction?: string;
53
100
  openQuestions?: string[];
54
101
  startInterview?: StartInterviewSession;
102
+ firstResearchShape?: FirstResearchShape;
55
103
  requestedPerspectives: string[];
56
104
  disagreementPreference: ProjectDisagreementPreference;
57
105
  activeModes?: string[];
@@ -127,9 +175,43 @@ export interface LongTableWorkspaceInspection {
127
175
  suggestion?: string;
128
176
  }>;
129
177
  }
130
- export declare function loadWorkspaceState(context: LongTableProjectContext): Promise<ResearchState>;
178
+ export declare function loadWorkspaceState(context: LongTableProjectContext): Promise<LongTableWorkspaceState>;
131
179
  export declare function syncCurrentWorkspaceView(context: LongTableProjectContext): Promise<string>;
132
- export declare function appendInvocationRecordToWorkspace(context: LongTableProjectContext, invocation: InvocationRecord, questions?: QuestionRecord[]): Promise<ResearchState>;
180
+ export declare function appendInvocationRecordToWorkspace(context: LongTableProjectContext, invocation: InvocationRecord, questions?: QuestionRecord[]): Promise<LongTableWorkspaceState>;
181
+ export declare function beginLongTableInterview(options: {
182
+ context: LongTableProjectContext;
183
+ provider?: ProviderKind;
184
+ openingQuestion?: string;
185
+ seedAnswer?: string;
186
+ }): Promise<{
187
+ hook: LongTableHookRun;
188
+ state: LongTableWorkspaceState;
189
+ }>;
190
+ export declare function appendLongTableInterviewTurn(options: {
191
+ context: LongTableProjectContext;
192
+ hookId?: string;
193
+ question: string;
194
+ answer: string;
195
+ reflection?: string;
196
+ quality?: InterviewTurnQuality;
197
+ needsFollowUp?: boolean;
198
+ followUpQuestion?: string;
199
+ rationale?: string[];
200
+ }): Promise<{
201
+ hook: LongTableHookRun;
202
+ turn: NonNullable<LongTableHookRun["turns"]>[number];
203
+ state: LongTableWorkspaceState;
204
+ }>;
205
+ export declare function summarizeLongTableInterview(options: {
206
+ context: LongTableProjectContext;
207
+ hookId?: string;
208
+ shape: FirstResearchShape;
209
+ }): Promise<{
210
+ hook: LongTableHookRun;
211
+ shape: FirstResearchShape;
212
+ state: LongTableWorkspaceState;
213
+ session: LongTableSessionRecord;
214
+ }>;
133
215
  export declare function listBlockingWorkspaceQuestions(context: LongTableProjectContext): Promise<QuestionRecord[]>;
134
216
  export declare function assertWorkspaceNotBlocked(context: LongTableProjectContext): Promise<void>;
135
217
  export declare function createWorkspaceFollowUpQuestions(options: {
@@ -54,6 +54,9 @@ function resolveUserLocale() {
54
54
  return normalizeLocale(Intl.DateTimeFormat().resolvedOptions().locale);
55
55
  }
56
56
  function buildFirstQuestion(session) {
57
+ if (session.firstResearchShape?.openQuestions?.[0]) {
58
+ return session.firstResearchShape.openQuestions[0];
59
+ }
57
60
  return session.currentBlocker
58
61
  ? `Where does "${session.currentBlocker}" show up most concretely in the scene, material, or evidence?`
59
62
  : `What scene, case, text, data, or draft would make "${session.currentGoal}" easiest to inspect first?`;
@@ -63,20 +66,23 @@ function buildOpenQuestions(session) {
63
66
  if (session.startInterview) {
64
67
  return [
65
68
  firstQuestion,
66
- `What would a reader need to understand differently if "${session.currentGoal}" becomes a strong research project?`
69
+ `What still feels hardest to name or make concrete in "${session.currentGoal}"?`
67
70
  ];
68
71
  }
69
72
  return session.currentBlocker
70
73
  ? [
71
74
  firstQuestion,
72
- `What would a reader need to understand differently if "${session.currentBlocker}" becomes a strong research problem?`
75
+ `What would give "${session.currentBlocker}" a usable first research handle without forcing a final research question yet?`
73
76
  ]
74
77
  : [
75
78
  firstQuestion,
76
- `What would count as a good first outcome for "${session.currentGoal}" in this session?`
79
+ `What would give this project a usable first research handle without pretending the question is settled?`
77
80
  ];
78
81
  }
79
82
  function buildNextAction(session) {
83
+ if (session.firstResearchShape) {
84
+ return session.firstResearchShape.nextAction;
85
+ }
80
86
  if (session.startInterview) {
81
87
  return session.currentBlocker
82
88
  ? `Begin from the start-interview brief, then make "${session.currentBlocker}" concrete with one scene, source, case, or dataset.`
@@ -87,6 +93,9 @@ function buildNextAction(session) {
87
93
  : "Open with your current goal in one sentence, then ask LongTable for the first concrete research move.";
88
94
  }
89
95
  function buildResumeHint(session) {
96
+ if (session.firstResearchShape) {
97
+ return `I want to continue from the First Research Shape: ${session.firstResearchShape.handle}.`;
98
+ }
90
99
  if (session.startInterview) {
91
100
  return session.currentBlocker
92
101
  ? `I want to continue from the LongTable start interview. The first unresolved issue is ${session.currentBlocker}.`
@@ -118,6 +127,7 @@ function buildCurrentGuide(project, session, recentInvocations = [], pendingQues
118
127
  ...(session.researchObject ? [`- 연구 객체: ${session.researchObject}`] : []),
119
128
  ...(session.gapRisk ? [`- 공백/암묵지 위험: ${session.gapRisk}`] : []),
120
129
  ...(session.protectedDecision ? [`- 보호할 결정: ${session.protectedDecision}`] : []),
130
+ ...(session.firstResearchShape ? [`- First Research Shape: ${session.firstResearchShape.handle}`] : []),
121
131
  ...(session.startInterview ? [`- start interview: ${session.startInterview.summary}`] : []),
122
132
  `- 다음 액션: ${nextAction}`,
123
133
  `- 관점: ${session.requestedPerspectives.length > 0 ? session.requestedPerspectives.join(", ") : "auto"}`,
@@ -149,10 +159,19 @@ function buildCurrentGuide(project, session, recentInvocations = [], pendingQues
149
159
  "",
150
160
  "## 다시 시작 문장",
151
161
  `- "${resumeHint}"`,
162
+ ...(session.firstResearchShape
163
+ ? [
164
+ "",
165
+ "## First Research Shape",
166
+ `- Handle: ${session.firstResearchShape.handle}`,
167
+ `- Confidence: ${session.firstResearchShape.confidence}`,
168
+ ...session.firstResearchShape.openQuestions.map((question) => `- Open question: ${question}`)
169
+ ]
170
+ : []),
152
171
  "",
153
172
  "## 빠른 시작",
154
173
  "- 이 디렉토리에서 `codex`를 엽니다.",
155
- `- 첫 메시지는 보통 \`${suggestedPrompt}\` 정도면 충분합니다.`,
174
+ `- 첫 메시지는 보통 \`${session.firstResearchShape ? suggestedPrompt : "$longtable-interview"}\` 정도면 충분합니다.`,
156
175
  "",
157
176
  "## 증거 규칙",
158
177
  "- 외부 사실이나 현재 정보는 source를 붙이거나 inference로 낮춥니다."
@@ -171,6 +190,7 @@ function buildCurrentGuide(project, session, recentInvocations = [], pendingQues
171
190
  ...(session.researchObject ? [`- Research object: ${session.researchObject}`] : []),
172
191
  ...(session.gapRisk ? [`- Gap/tacit risk: ${session.gapRisk}`] : []),
173
192
  ...(session.protectedDecision ? [`- Protected decision: ${session.protectedDecision}`] : []),
193
+ ...(session.firstResearchShape ? [`- First Research Shape: ${session.firstResearchShape.handle}`] : []),
174
194
  ...(session.startInterview ? [`- Start interview: ${session.startInterview.summary}`] : []),
175
195
  `- Next action: ${nextAction}`,
176
196
  `- Perspectives: ${session.requestedPerspectives.length > 0 ? session.requestedPerspectives.join(", ") : "auto"}`,
@@ -202,10 +222,19 @@ function buildCurrentGuide(project, session, recentInvocations = [], pendingQues
202
222
  "",
203
223
  "## Restart Prompt",
204
224
  `- "${resumeHint}"`,
225
+ ...(session.firstResearchShape
226
+ ? [
227
+ "",
228
+ "## First Research Shape",
229
+ `- Handle: ${session.firstResearchShape.handle}`,
230
+ `- Confidence: ${session.firstResearchShape.confidence}`,
231
+ ...session.firstResearchShape.openQuestions.map((question) => `- Open question: ${question}`)
232
+ ]
233
+ : []),
205
234
  "",
206
235
  "## Quick Start",
207
236
  "- Open `codex` in this directory.",
208
- `- A good first message is usually \`${suggestedPrompt}\`.`,
237
+ `- A good first message is usually \`${session.firstResearchShape ? suggestedPrompt : "$longtable-interview"}\`.`,
209
238
  "",
210
239
  "## Evidence Rule",
211
240
  "- External or current claims should carry a source link or be labeled as inference."
@@ -220,6 +249,8 @@ async function loadResearchState(stateFilePath) {
220
249
  ...parsed,
221
250
  explicitState: parsed.explicitState ?? {},
222
251
  workingState: parsed.workingState ?? {},
252
+ hooks: parsed.hooks ?? [],
253
+ ...(parsed.firstResearchShape ? { firstResearchShape: parsed.firstResearchShape } : {}),
223
254
  inferredHypotheses: parsed.inferredHypotheses ?? [],
224
255
  openTensions: parsed.openTensions ?? [],
225
256
  decisionLog: parsed.decisionLog ?? [],
@@ -339,13 +370,16 @@ function buildProjectAgentsMd(project, session) {
339
370
  "- Treat `AGENTS.md` as runtime guidance, not as the researcher-facing resume artifact.",
340
371
  "",
341
372
  "## Invocation Rules",
373
+ "- If the user message starts with `$longtable-interview`, run the LongTable interview flow before generic research advice.",
342
374
  "- If the user message starts with `lt `, `longtable `, `long table `, or `롱테이블 ` followed by a directive and `:`, treat it as an explicit LongTable invocation.",
343
- "- Supported explicit directives are: explore, review, critique, draft, commit, panel, status, editor, reviewer, methods, theory, measurement, ethics, voice, venue.",
375
+ "- Supported explicit directives are: interview, explore, review, critique, draft, commit, panel, status, editor, reviewer, methods, theory, measurement, ethics, voice, venue.",
344
376
  "- For explicit LongTable invocations, do not begin by scanning the workspace. Use the current session files first and answer as LongTable immediately.",
345
377
  "- For general research requests in this workspace, prefer LongTable behavior before generic coding behavior.",
346
378
  "",
347
379
  "## Research Behavior",
348
380
  "- Begin exploratory work with clarifying or tension questions before recommending a direction.",
381
+ "- 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.",
382
+ "- Use structured options only at the final First Research Shape confirmation or at true checkpoint boundaries.",
349
383
  "- If you foreground role perspectives, disclose them with `LongTable consulted: ...`.",
350
384
  "- Keep one accountable synthesis, but do not hide meaningful disagreement.",
351
385
  ...(session.disagreementPreference === "always_visible"
@@ -362,6 +396,7 @@ function buildProjectAgentsMd(project, session) {
362
396
  ...(session.researchObject ? [`- Research object: ${session.researchObject}`] : []),
363
397
  ...(session.gapRisk ? [`- Gap/tacit risk: ${session.gapRisk}`] : []),
364
398
  ...(session.protectedDecision ? [`- Protected decision: ${session.protectedDecision}`] : []),
399
+ ...(session.firstResearchShape ? [`- First Research Shape: ${session.firstResearchShape.handle}`] : []),
365
400
  ...(session.startInterview ? [`- Start interview summary: ${session.startInterview.summary}`] : []),
366
401
  `- Requested perspectives: ${session.requestedPerspectives.length > 0 ? session.requestedPerspectives.join(", ") : "auto"}`,
367
402
  `- Disagreement visibility: ${session.disagreementPreference}`,
@@ -390,6 +425,10 @@ function buildStateSeed(project, session, setup) {
390
425
  ...(session.startInterview ? { startInterview: session.startInterview } : {}),
391
426
  ...(session.resumeHint ? { resumeHint: session.resumeHint } : {})
392
427
  };
428
+ if (session.firstResearchShape) {
429
+ state.firstResearchShape = session.firstResearchShape;
430
+ state.workingState.firstResearchShape = session.firstResearchShape;
431
+ }
393
432
  if (session.currentBlocker) {
394
433
  state.openTensions.push(session.currentBlocker);
395
434
  }
@@ -477,6 +516,200 @@ export async function appendInvocationRecordToWorkspace(context, invocation, que
477
516
  function createId(prefix) {
478
517
  return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
479
518
  }
519
+ function normalizeInterviewQuality(answer, quality) {
520
+ if (quality) {
521
+ return quality;
522
+ }
523
+ const trimmed = answer.trim();
524
+ const wordCount = trimmed.split(/\s+/).filter(Boolean).length;
525
+ if (trimmed.length < 12 || wordCount < 3) {
526
+ return "thin";
527
+ }
528
+ if (trimmed.length > 80 || wordCount >= 12) {
529
+ return "rich";
530
+ }
531
+ return "usable";
532
+ }
533
+ function defaultFollowUpQuestion(answer) {
534
+ const trimmed = answer.trim();
535
+ if (trimmed.length < 12) {
536
+ return "Say one more sentence about where this problem appears or why it matters before LongTable tries to classify it.";
537
+ }
538
+ return "What concrete scene, case, material, text, dataset, or decision would make that problem easier to inspect first?";
539
+ }
540
+ function depthForInterview(turns = []) {
541
+ const usableTurns = turns.filter((turn) => turn.quality !== "thin").length;
542
+ if (usableTurns >= 3) {
543
+ return "ready_to_summarize";
544
+ }
545
+ if (usableTurns >= 1) {
546
+ return "forming_first_handle";
547
+ }
548
+ return "gathering_context";
549
+ }
550
+ function activeInterviewHook(state, hookId) {
551
+ const hooks = state.hooks ?? [];
552
+ if (hookId) {
553
+ return hooks.find((hook) => hook.id === hookId);
554
+ }
555
+ return [...hooks].reverse().find((hook) => hook.kind === "longtable_interview" &&
556
+ (hook.status === "pending" || hook.status === "active" || hook.status === "ready_to_confirm"));
557
+ }
558
+ function upsertHook(state, hook) {
559
+ const hooks = state.hooks ?? [];
560
+ const existingIndex = hooks.findIndex((candidate) => candidate.id === hook.id);
561
+ const nextHooks = existingIndex >= 0
562
+ ? hooks.map((candidate) => candidate.id === hook.id ? hook : candidate)
563
+ : [...hooks, hook];
564
+ return {
565
+ ...state,
566
+ hooks: nextHooks
567
+ };
568
+ }
569
+ export async function beginLongTableInterview(options) {
570
+ const state = await loadResearchState(options.context.stateFilePath);
571
+ const existing = activeInterviewHook(state);
572
+ if (existing) {
573
+ return { hook: existing, state };
574
+ }
575
+ const timestamp = nowIso();
576
+ const hook = {
577
+ id: createId("hook_interview"),
578
+ kind: "longtable_interview",
579
+ status: "active",
580
+ createdAt: timestamp,
581
+ updatedAt: timestamp,
582
+ targetOutcome: "first_research_handle",
583
+ depth: "gathering_context",
584
+ provider: options.provider,
585
+ turns: [],
586
+ qualityNotes: [],
587
+ rationale: [
588
+ "Official LongTable research start surface is provider-native `$longtable-interview`, not the CLI start questionnaire.",
589
+ "The hook keeps early research ambiguity open until a first research handle can be summarized."
590
+ ]
591
+ };
592
+ let updated = upsertHook(state, hook);
593
+ updated.workingState = {
594
+ ...updated.workingState,
595
+ activeInterviewHookId: hook.id,
596
+ interviewSurface: "$longtable-interview",
597
+ ...(options.openingQuestion ? { interviewOpeningQuestion: options.openingQuestion } : {}),
598
+ ...(options.seedAnswer ? { interviewSeedAnswer: options.seedAnswer } : {})
599
+ };
600
+ await writeFile(options.context.stateFilePath, JSON.stringify(updated, null, 2), "utf8");
601
+ await syncCurrentWorkspaceView(options.context);
602
+ return { hook, state: updated };
603
+ }
604
+ export async function appendLongTableInterviewTurn(options) {
605
+ const state = await loadResearchState(options.context.stateFilePath);
606
+ const existing = activeInterviewHook(state, options.hookId);
607
+ if (!existing) {
608
+ throw new Error("No active LongTable interview hook was found. Run begin_interview first.");
609
+ }
610
+ const quality = normalizeInterviewQuality(options.answer, options.quality);
611
+ const needsFollowUp = options.needsFollowUp ?? quality === "thin";
612
+ const followUpQuestion = needsFollowUp
613
+ ? options.followUpQuestion ?? defaultFollowUpQuestion(options.answer)
614
+ : options.followUpQuestion;
615
+ const timestamp = nowIso();
616
+ const turns = existing.turns ?? [];
617
+ const turn = {
618
+ id: createId("interview_turn"),
619
+ index: turns.length + 1,
620
+ createdAt: timestamp,
621
+ question: options.question.trim(),
622
+ answer: options.answer.trim(),
623
+ ...(options.reflection?.trim() ? { reflection: options.reflection.trim() } : {}),
624
+ quality,
625
+ needsFollowUp,
626
+ ...(followUpQuestion?.trim() ? { followUpQuestion: followUpQuestion.trim() } : {}),
627
+ ...(options.rationale && options.rationale.length > 0 ? { rationale: options.rationale } : {})
628
+ };
629
+ const nextTurns = [...turns, turn];
630
+ const depth = depthForInterview(nextTurns);
631
+ const hook = {
632
+ ...existing,
633
+ status: depth === "ready_to_summarize" ? "ready_to_confirm" : "active",
634
+ updatedAt: timestamp,
635
+ depth,
636
+ turns: nextTurns,
637
+ qualityNotes: [
638
+ ...(existing.qualityNotes ?? []),
639
+ ...(needsFollowUp ? [`Turn ${turn.index} needs follow-up: ${followUpQuestion}`] : [])
640
+ ]
641
+ };
642
+ const updated = upsertHook(state, hook);
643
+ await writeFile(options.context.stateFilePath, JSON.stringify(updated, null, 2), "utf8");
644
+ await syncCurrentWorkspaceView(options.context);
645
+ return { hook, turn, state: updated };
646
+ }
647
+ export async function summarizeLongTableInterview(options) {
648
+ const state = await loadResearchState(options.context.stateFilePath);
649
+ const existing = activeInterviewHook(state, options.hookId);
650
+ if (!existing) {
651
+ throw new Error("No active LongTable interview hook was found. Run begin_interview first.");
652
+ }
653
+ const timestamp = nowIso();
654
+ const shape = {
655
+ ...options.shape,
656
+ handle: options.shape.handle.trim(),
657
+ currentGoal: options.shape.currentGoal.trim(),
658
+ openQuestions: options.shape.openQuestions.map((question) => question.trim()).filter(Boolean),
659
+ nextAction: options.shape.nextAction.trim(),
660
+ sourceHookId: existing.id
661
+ };
662
+ const hook = {
663
+ ...existing,
664
+ status: "ready_to_confirm",
665
+ updatedAt: timestamp,
666
+ depth: "ready_to_summarize",
667
+ firstResearchShape: shape
668
+ };
669
+ const session = {
670
+ ...options.context.session,
671
+ lastUpdatedAt: timestamp,
672
+ currentGoal: shape.currentGoal,
673
+ ...(shape.currentBlocker ? { currentBlocker: shape.currentBlocker } : {}),
674
+ ...(shape.researchObject ? { researchObject: shape.researchObject } : {}),
675
+ ...(shape.gapRisk ? { gapRisk: shape.gapRisk } : {}),
676
+ ...(shape.protectedDecision ? { protectedDecision: shape.protectedDecision } : {}),
677
+ nextAction: shape.nextAction,
678
+ openQuestions: shape.openQuestions,
679
+ firstResearchShape: shape,
680
+ resumeHint: `I want to continue from the First Research Shape: ${shape.handle}.`
681
+ };
682
+ options.context.session = session;
683
+ let updated = upsertHook(state, hook);
684
+ updated.firstResearchShape = shape;
685
+ updated.workingState = {
686
+ ...updated.workingState,
687
+ currentGoal: shape.currentGoal,
688
+ ...(shape.currentBlocker ? { currentBlocker: shape.currentBlocker } : {}),
689
+ ...(shape.researchObject ? { researchObject: shape.researchObject } : {}),
690
+ ...(shape.gapRisk ? { gapRisk: shape.gapRisk } : {}),
691
+ ...(shape.protectedDecision ? { protectedDecision: shape.protectedDecision } : {}),
692
+ openQuestions: shape.openQuestions,
693
+ nextAction: shape.nextAction,
694
+ firstResearchShape: shape
695
+ };
696
+ if (shape.currentBlocker && !updated.openTensions.includes(shape.currentBlocker)) {
697
+ updated.openTensions.push(shape.currentBlocker);
698
+ }
699
+ updated.narrativeTraces.push({
700
+ id: createId("narrative_trace"),
701
+ timestamp,
702
+ source: "$longtable-interview",
703
+ traceType: "judgment",
704
+ summary: `First Research Shape: ${shape.handle}.`,
705
+ visibility: "explicit",
706
+ importance: shape.confidence
707
+ });
708
+ await writeFile(options.context.sessionFilePath, JSON.stringify(session, null, 2), "utf8");
709
+ await writeFile(options.context.stateFilePath, JSON.stringify(updated, null, 2), "utf8");
710
+ await syncCurrentWorkspaceView(options.context);
711
+ return { hook, shape, state: updated, session };
712
+ }
480
713
  function findQuestionForDecision(state, questionId) {
481
714
  const pending = (state.questionLog ?? []).filter((record) => record.status === "pending");
482
715
  if (questionId) {
@@ -1175,6 +1408,7 @@ export function renderProjectWorkspaceSummary(context) {
1175
1408
  `Goal: ${context.session.currentGoal}`,
1176
1409
  ...(context.session.currentBlocker ? [`Blocker: ${context.session.currentBlocker}`] : []),
1177
1410
  ...(context.session.researchObject ? [`Working object: ${context.session.researchObject}`] : []),
1411
+ ...(context.session.firstResearchShape ? [`First Research Shape: ${context.session.firstResearchShape.handle}`] : []),
1178
1412
  ...(context.session.startInterview ? [`Start interview: ${context.session.startInterview.summary}`] : []),
1179
1413
  "└───────────────────────────────────────────────────────┘",
1180
1414
  "",
@@ -29,18 +29,18 @@ function promptSpec() {
29
29
  argumentHint: "[project context or current uncertainty]",
30
30
  body: [
31
31
  "You are LongTable setup guidance inside Codex.",
32
- "Treat `longtable init` as deprecated. Prefer `longtable setup --provider codex` for runtime permissions, then `longtable start` for the project interview.",
32
+ "Treat `longtable init` as deprecated. Prefer `longtable setup --provider codex` for runtime permissions, then `$longtable-interview` inside Codex for the project interview.",
33
33
  "Ask exactly one setup question at a time.",
34
34
  "Use numbered choices and include a concise Why / What you get / Tradeoff for each option.",
35
35
  "Do not move to the next question until the researcher answers the current one.",
36
- "Runtime setup covers only: provider, install scope, runtime surfaces, intervention strength, and whether to create a workspace now.",
36
+ "Runtime setup covers only: provider, install scope, runtime surfaces, intervention strength, and whether to show interview launch steps.",
37
37
  "Do not ask for field, career stage, experience level, authorship signal, weakest domain, or panel preference during runtime setup.",
38
- "Project start covers: research object, gap/tacit risk, and protected decision area.",
38
+ "Project interview covers: the first research handle, early uncertainty, first inspectable material, and final structured confirmation.",
39
39
  "After collecting runtime answers, summarize the proposed setup and output the exact `longtable setup --provider codex ...` command.",
40
- "If the researcher is ready to create a project workspace, output the exact `longtable start ...` command separately.",
40
+ "If the researcher is ready to create a project workspace, tell them to open Codex in the research folder and invoke `$longtable-interview`.",
41
41
  "If the researcher asks you to stay inside Codex, keep the conversation in numbered form and do not prematurely close.",
42
42
  "Frame setup as permission and intervention calibration, not a researcher profile interview.",
43
- "Do not pretend setup is the full project-start interview. The project-start interview happens in `longtable start`.",
43
+ "Do not pretend setup is the full project-start interview. The project-start interview happens in `$longtable-interview`.",
44
44
  "Treat any slash-command arguments as context for why setup is being done now."
45
45
  ]
46
46
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@longtable/cli",
3
- "version": "0.1.31",
3
+ "version": "0.1.32",
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.31",
33
- "@longtable/core": "0.1.31",
34
- "@longtable/memory": "0.1.31",
35
- "@longtable/provider-claude": "0.1.31",
36
- "@longtable/provider-codex": "0.1.31",
37
- "@longtable/setup": "0.1.31"
32
+ "@longtable/checkpoints": "0.1.32",
33
+ "@longtable/core": "0.1.32",
34
+ "@longtable/memory": "0.1.32",
35
+ "@longtable/provider-claude": "0.1.32",
36
+ "@longtable/provider-codex": "0.1.32",
37
+ "@longtable/setup": "0.1.32"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@types/node": "^22.10.1",