@longtable/cli 0.1.30 → 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.
@@ -1,6 +1,71 @@
1
1
  import type { DecisionRecord, InvocationRecord, ProviderKind, QuestionOption, 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
+ export type StartInterviewSignal = "phenomenon" | "audience" | "artifact" | "evidence" | "assumption" | "decision_risk" | "voice";
5
+ export interface StartInterviewTurn {
6
+ index: number;
7
+ question: string;
8
+ answer: string;
9
+ signal: StartInterviewSignal;
10
+ purpose: string;
11
+ }
12
+ export interface StartInterviewSession {
13
+ mode: "adaptive";
14
+ openingStyle: "scene_problem";
15
+ createdAt: string;
16
+ completedAt: string;
17
+ turnCount: number;
18
+ turns: StartInterviewTurn[];
19
+ inferredSignals: StartInterviewSignal[];
20
+ summary: string;
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
+ };
4
69
  export interface LongTableProjectRecord {
5
70
  schemaVersion: 1;
6
71
  product: "LongTable";
@@ -33,6 +98,8 @@ export interface LongTableSessionRecord {
33
98
  protectedDecision?: string;
34
99
  nextAction?: string;
35
100
  openQuestions?: string[];
101
+ startInterview?: StartInterviewSession;
102
+ firstResearchShape?: FirstResearchShape;
36
103
  requestedPerspectives: string[];
37
104
  disagreementPreference: ProjectDisagreementPreference;
38
105
  activeModes?: string[];
@@ -108,9 +175,43 @@ export interface LongTableWorkspaceInspection {
108
175
  suggestion?: string;
109
176
  }>;
110
177
  }
111
- export declare function loadWorkspaceState(context: LongTableProjectContext): Promise<ResearchState>;
178
+ export declare function loadWorkspaceState(context: LongTableProjectContext): Promise<LongTableWorkspaceState>;
112
179
  export declare function syncCurrentWorkspaceView(context: LongTableProjectContext): Promise<string>;
113
- 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
+ }>;
114
215
  export declare function listBlockingWorkspaceQuestions(context: LongTableProjectContext): Promise<QuestionRecord[]>;
115
216
  export declare function assertWorkspaceNotBlocked(context: LongTableProjectContext): Promise<void>;
116
217
  export declare function createWorkspaceFollowUpQuestions(options: {
@@ -159,6 +260,7 @@ export declare function createOrUpdateProjectWorkspace(options: {
159
260
  researchObject?: string;
160
261
  gapRisk?: string;
161
262
  protectedDecision?: string;
263
+ startInterview?: StartInterviewSession;
162
264
  requestedPerspectives: string[];
163
265
  disagreementPreference: ProjectDisagreementPreference;
164
266
  setup: SetupPersistedOutput;
@@ -54,28 +54,53 @@ 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
- ? `What would reduce the uncertainty around "${session.currentBlocker}" first?`
59
- : `What is the first concrete question that would move "${session.currentGoal}" forward?`;
61
+ ? `Where does "${session.currentBlocker}" show up most concretely in the scene, material, or evidence?`
62
+ : `What scene, case, text, data, or draft would make "${session.currentGoal}" easiest to inspect first?`;
60
63
  }
61
64
  function buildOpenQuestions(session) {
62
65
  const firstQuestion = buildFirstQuestion(session);
66
+ if (session.startInterview) {
67
+ return [
68
+ firstQuestion,
69
+ `What still feels hardest to name or make concrete in "${session.currentGoal}"?`
70
+ ];
71
+ }
63
72
  return session.currentBlocker
64
73
  ? [
65
74
  firstQuestion,
66
- `What evidence would let you decide whether "${session.currentBlocker}" is a knowledge gap, a coding rule gap, or a data gap?`
75
+ `What would give "${session.currentBlocker}" a usable first research handle without forcing a final research question yet?`
67
76
  ]
68
77
  : [
69
78
  firstQuestion,
70
- `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?`
71
80
  ];
72
81
  }
73
82
  function buildNextAction(session) {
83
+ if (session.firstResearchShape) {
84
+ return session.firstResearchShape.nextAction;
85
+ }
86
+ if (session.startInterview) {
87
+ return session.currentBlocker
88
+ ? `Begin from the start-interview brief, then make "${session.currentBlocker}" concrete with one scene, source, case, or dataset.`
89
+ : "Begin from the start-interview brief, then choose one concrete scene, source, case, or dataset to inspect first.";
90
+ }
74
91
  return session.currentBlocker
75
- ? `Open with the blocker, then ask LongTable to surface the first high-leverage uncertainty around "${session.currentBlocker}".`
92
+ ? `Open with the blocker, then make "${session.currentBlocker}" concrete with one scene, source, case, or dataset.`
76
93
  : "Open with your current goal in one sentence, then ask LongTable for the first concrete research move.";
77
94
  }
78
95
  function buildResumeHint(session) {
96
+ if (session.firstResearchShape) {
97
+ return `I want to continue from the First Research Shape: ${session.firstResearchShape.handle}.`;
98
+ }
99
+ if (session.startInterview) {
100
+ return session.currentBlocker
101
+ ? `I want to continue from the LongTable start interview. The first unresolved issue is ${session.currentBlocker}.`
102
+ : `I want to continue from the LongTable start interview for ${session.currentGoal}.`;
103
+ }
79
104
  return session.currentBlocker
80
105
  ? `I want to continue ${session.currentGoal}. The unresolved blocker is ${session.currentBlocker}.`
81
106
  : `I want to continue ${session.currentGoal}.`;
@@ -102,6 +127,8 @@ function buildCurrentGuide(project, session, recentInvocations = [], pendingQues
102
127
  ...(session.researchObject ? [`- 연구 객체: ${session.researchObject}`] : []),
103
128
  ...(session.gapRisk ? [`- 공백/암묵지 위험: ${session.gapRisk}`] : []),
104
129
  ...(session.protectedDecision ? [`- 보호할 결정: ${session.protectedDecision}`] : []),
130
+ ...(session.firstResearchShape ? [`- First Research Shape: ${session.firstResearchShape.handle}`] : []),
131
+ ...(session.startInterview ? [`- start interview: ${session.startInterview.summary}`] : []),
105
132
  `- 다음 액션: ${nextAction}`,
106
133
  `- 관점: ${session.requestedPerspectives.length > 0 ? session.requestedPerspectives.join(", ") : "auto"}`,
107
134
  `- disagreement: ${session.disagreementPreference}`,
@@ -132,10 +159,19 @@ function buildCurrentGuide(project, session, recentInvocations = [], pendingQues
132
159
  "",
133
160
  "## 다시 시작 문장",
134
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
+ : []),
135
171
  "",
136
172
  "## 빠른 시작",
137
173
  "- 이 디렉토리에서 `codex`를 엽니다.",
138
- `- 첫 메시지는 보통 \`${suggestedPrompt}\` 정도면 충분합니다.`,
174
+ `- 첫 메시지는 보통 \`${session.firstResearchShape ? suggestedPrompt : "$longtable-interview"}\` 정도면 충분합니다.`,
139
175
  "",
140
176
  "## 증거 규칙",
141
177
  "- 외부 사실이나 현재 정보는 source를 붙이거나 inference로 낮춥니다."
@@ -154,6 +190,8 @@ function buildCurrentGuide(project, session, recentInvocations = [], pendingQues
154
190
  ...(session.researchObject ? [`- Research object: ${session.researchObject}`] : []),
155
191
  ...(session.gapRisk ? [`- Gap/tacit risk: ${session.gapRisk}`] : []),
156
192
  ...(session.protectedDecision ? [`- Protected decision: ${session.protectedDecision}`] : []),
193
+ ...(session.firstResearchShape ? [`- First Research Shape: ${session.firstResearchShape.handle}`] : []),
194
+ ...(session.startInterview ? [`- Start interview: ${session.startInterview.summary}`] : []),
157
195
  `- Next action: ${nextAction}`,
158
196
  `- Perspectives: ${session.requestedPerspectives.length > 0 ? session.requestedPerspectives.join(", ") : "auto"}`,
159
197
  `- Disagreement: ${session.disagreementPreference}`,
@@ -184,10 +222,19 @@ function buildCurrentGuide(project, session, recentInvocations = [], pendingQues
184
222
  "",
185
223
  "## Restart Prompt",
186
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
+ : []),
187
234
  "",
188
235
  "## Quick Start",
189
236
  "- Open `codex` in this directory.",
190
- `- A good first message is usually \`${suggestedPrompt}\`.`,
237
+ `- A good first message is usually \`${session.firstResearchShape ? suggestedPrompt : "$longtable-interview"}\`.`,
191
238
  "",
192
239
  "## Evidence Rule",
193
240
  "- External or current claims should carry a source link or be labeled as inference."
@@ -202,6 +249,8 @@ async function loadResearchState(stateFilePath) {
202
249
  ...parsed,
203
250
  explicitState: parsed.explicitState ?? {},
204
251
  workingState: parsed.workingState ?? {},
252
+ hooks: parsed.hooks ?? [],
253
+ ...(parsed.firstResearchShape ? { firstResearchShape: parsed.firstResearchShape } : {}),
205
254
  inferredHypotheses: parsed.inferredHypotheses ?? [],
206
255
  openTensions: parsed.openTensions ?? [],
207
256
  decisionLog: parsed.decisionLog ?? [],
@@ -321,13 +370,16 @@ function buildProjectAgentsMd(project, session) {
321
370
  "- Treat `AGENTS.md` as runtime guidance, not as the researcher-facing resume artifact.",
322
371
  "",
323
372
  "## Invocation Rules",
373
+ "- If the user message starts with `$longtable-interview`, run the LongTable interview flow before generic research advice.",
324
374
  "- If the user message starts with `lt `, `longtable `, `long table `, or `롱테이블 ` followed by a directive and `:`, treat it as an explicit LongTable invocation.",
325
- "- 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.",
326
376
  "- For explicit LongTable invocations, do not begin by scanning the workspace. Use the current session files first and answer as LongTable immediately.",
327
377
  "- For general research requests in this workspace, prefer LongTable behavior before generic coding behavior.",
328
378
  "",
329
379
  "## Research Behavior",
330
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.",
331
383
  "- If you foreground role perspectives, disclose them with `LongTable consulted: ...`.",
332
384
  "- Keep one accountable synthesis, but do not hide meaningful disagreement.",
333
385
  ...(session.disagreementPreference === "always_visible"
@@ -344,6 +396,8 @@ function buildProjectAgentsMd(project, session) {
344
396
  ...(session.researchObject ? [`- Research object: ${session.researchObject}`] : []),
345
397
  ...(session.gapRisk ? [`- Gap/tacit risk: ${session.gapRisk}`] : []),
346
398
  ...(session.protectedDecision ? [`- Protected decision: ${session.protectedDecision}`] : []),
399
+ ...(session.firstResearchShape ? [`- First Research Shape: ${session.firstResearchShape.handle}`] : []),
400
+ ...(session.startInterview ? [`- Start interview summary: ${session.startInterview.summary}`] : []),
347
401
  `- Requested perspectives: ${session.requestedPerspectives.length > 0 ? session.requestedPerspectives.join(", ") : "auto"}`,
348
402
  `- Disagreement visibility: ${session.disagreementPreference}`,
349
403
  "- These instructions apply to this directory and its children."
@@ -368,8 +422,13 @@ function buildStateSeed(project, session, setup) {
368
422
  ...(session.nextAction ? { nextAction: session.nextAction } : {}),
369
423
  openQuestions: session.openQuestions ?? [],
370
424
  activeModes: session.activeModes ?? [],
425
+ ...(session.startInterview ? { startInterview: session.startInterview } : {}),
371
426
  ...(session.resumeHint ? { resumeHint: session.resumeHint } : {})
372
427
  };
428
+ if (session.firstResearchShape) {
429
+ state.firstResearchShape = session.firstResearchShape;
430
+ state.workingState.firstResearchShape = session.firstResearchShape;
431
+ }
373
432
  if (session.currentBlocker) {
374
433
  state.openTensions.push(session.currentBlocker);
375
434
  }
@@ -414,6 +473,25 @@ function buildStateSeed(project, session, setup) {
414
473
  importance: "high"
415
474
  });
416
475
  }
476
+ if (session.startInterview) {
477
+ state.narrativeTraces.push({
478
+ id: "project-session-start-interview",
479
+ timestamp: nowIso(),
480
+ source: "longtable-start",
481
+ traceType: "experience",
482
+ summary: session.startInterview.summary,
483
+ visibility: "explicit",
484
+ importance: "high"
485
+ });
486
+ if (session.startInterview.inferredSignals.length > 0) {
487
+ state.inferredHypotheses.push({
488
+ hypothesis: `Start interview suggests these early lenses: ${session.startInterview.inferredSignals.join(", ")}.`,
489
+ confidence: 0.65,
490
+ evidence: session.startInterview.turns.map((turn) => turn.answer).filter(Boolean),
491
+ status: "unconfirmed"
492
+ });
493
+ }
494
+ }
417
495
  return JSON.stringify(state, null, 2);
418
496
  }
419
497
  async function removeLegacyRootFiles(projectPath) {
@@ -438,6 +516,200 @@ export async function appendInvocationRecordToWorkspace(context, invocation, que
438
516
  function createId(prefix) {
439
517
  return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
440
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
+ }
441
713
  function findQuestionForDecision(state, questionId) {
442
714
  const pending = (state.questionLog ?? []).filter((record) => record.status === "pending");
443
715
  if (questionId) {
@@ -1008,6 +1280,7 @@ export async function createOrUpdateProjectWorkspace(options) {
1008
1280
  ...(options.researchObject ? { researchObject: options.researchObject } : {}),
1009
1281
  ...(options.gapRisk ? { gapRisk: options.gapRisk } : {}),
1010
1282
  ...(options.protectedDecision ? { protectedDecision: options.protectedDecision } : {}),
1283
+ ...(options.startInterview ? { startInterview: options.startInterview } : {}),
1011
1284
  nextAction: buildNextAction({
1012
1285
  schemaVersion: 1,
1013
1286
  id: sessionId,
@@ -1019,6 +1292,7 @@ export async function createOrUpdateProjectWorkspace(options) {
1019
1292
  ...(options.researchObject ? { researchObject: options.researchObject } : {}),
1020
1293
  ...(options.gapRisk ? { gapRisk: options.gapRisk } : {}),
1021
1294
  ...(options.protectedDecision ? { protectedDecision: options.protectedDecision } : {}),
1295
+ ...(options.startInterview ? { startInterview: options.startInterview } : {}),
1022
1296
  requestedPerspectives: options.requestedPerspectives,
1023
1297
  disagreementPreference: options.disagreementPreference
1024
1298
  }),
@@ -1033,6 +1307,7 @@ export async function createOrUpdateProjectWorkspace(options) {
1033
1307
  ...(options.researchObject ? { researchObject: options.researchObject } : {}),
1034
1308
  ...(options.gapRisk ? { gapRisk: options.gapRisk } : {}),
1035
1309
  ...(options.protectedDecision ? { protectedDecision: options.protectedDecision } : {}),
1310
+ ...(options.startInterview ? { startInterview: options.startInterview } : {}),
1036
1311
  requestedPerspectives: options.requestedPerspectives,
1037
1312
  disagreementPreference: options.disagreementPreference
1038
1313
  }),
@@ -1050,6 +1325,7 @@ export async function createOrUpdateProjectWorkspace(options) {
1050
1325
  ...(options.researchObject ? { researchObject: options.researchObject } : {}),
1051
1326
  ...(options.gapRisk ? { gapRisk: options.gapRisk } : {}),
1052
1327
  ...(options.protectedDecision ? { protectedDecision: options.protectedDecision } : {}),
1328
+ ...(options.startInterview ? { startInterview: options.startInterview } : {}),
1053
1329
  requestedPerspectives: options.requestedPerspectives,
1054
1330
  disagreementPreference: options.disagreementPreference
1055
1331
  }),
@@ -1123,13 +1399,19 @@ export async function inspectProjectWorkspace(startPath) {
1123
1399
  }
1124
1400
  export function renderProjectWorkspaceSummary(context) {
1125
1401
  return [
1126
- "┌──────────────────────────────────────────────┐",
1127
- "│ LongTable Project Workspace │",
1128
- "└──────────────────────────────────────────────┘",
1402
+ "┌─ LongTable Project Workspace ─────────────────────────┐",
1403
+ "└───────────────────────────────────────────────────────┘",
1129
1404
  `Project: ${context.project.projectName}`,
1130
1405
  `Path: ${context.project.projectPath}`,
1406
+ "",
1407
+ "┌─ Current Research Shape ──────────────────────────────┐",
1131
1408
  `Goal: ${context.session.currentGoal}`,
1132
1409
  ...(context.session.currentBlocker ? [`Blocker: ${context.session.currentBlocker}`] : []),
1410
+ ...(context.session.researchObject ? [`Working object: ${context.session.researchObject}`] : []),
1411
+ ...(context.session.firstResearchShape ? [`First Research Shape: ${context.session.firstResearchShape.handle}`] : []),
1412
+ ...(context.session.startInterview ? [`Start interview: ${context.session.startInterview.summary}`] : []),
1413
+ "└───────────────────────────────────────────────────────┘",
1414
+ "",
1133
1415
  `Perspectives: ${context.session.requestedPerspectives.length > 0 ? context.session.requestedPerspectives.join(", ") : "auto"}`,
1134
1416
  `Disagreement: ${context.session.disagreementPreference}`,
1135
1417
  "",
@@ -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
  },
@@ -0,0 +1,11 @@
1
+ import type { SetupChoice } from "@longtable/setup";
2
+ export interface PromptRenderer {
3
+ text(prompt: string, options?: {
4
+ required?: boolean;
5
+ placeholder?: string;
6
+ }): Promise<string | undefined>;
7
+ select(prompt: string, choices: SetupChoice[]): Promise<string>;
8
+ multiselect(prompt: string, choices: SetupChoice[]): Promise<string[]>;
9
+ note(message: string, title?: string): void;
10
+ }
11
+ export declare function createPromptRenderer(): PromptRenderer;