@longtable/cli 0.1.43 → 0.1.44

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
@@ -86,9 +86,18 @@ summary without starting a provider session.
86
86
  - `CURRENT.md`: human-facing current view regenerated from state
87
87
  - `.longtable/project.json`: stable project identity
88
88
  - `.longtable/current-session.json`: current session cursor
89
- - `.longtable/state.json`: layered memory state
89
+ - `.longtable/state.json`: layered memory state, including First Research
90
+ Shape and Research Specification when the interview has produced them
90
91
  - `.longtable/sessions/`: historical snapshots
91
92
 
93
+ `$longtable-interview` first stabilizes a short First Research Shape. When the
94
+ conversation is substantive enough, it should also preserve a Research
95
+ Specification covering scope, construct ontology, theory framing,
96
+ measurement/coding, method options, evidence/access requirements, epistemic
97
+ alignment, protected decisions, open questions, and next actions. `CURRENT.md`
98
+ renders that specification so later agents do not need to reconstruct the full
99
+ interview from memory.
100
+
92
101
  ## Why This Shape
93
102
 
94
103
  The CLI tries to keep the root simple for novice researchers while preserving enough structure for power users and downstream tooling.
@@ -140,6 +149,25 @@ That setup writes the MCP configuration and Codex elicitation approval needed
140
149
  for form-style checkpoint prompts. Without it, LongTable keeps the same
141
150
  `QuestionRecord` pending and falls back to numbered text.
142
151
 
152
+ ## Runtime Boundary
153
+
154
+ LongTable is not a replacement wrapper for Codex. Markdown docs and generated
155
+ skills are soft policy; hooks, MCP elicitation, CLI gates, and `.longtable/`
156
+ state are the enforcement layers.
157
+
158
+ LongTable should ask and stop before acting when the next step would change or
159
+ settle one of four high-risk research commitments:
160
+
161
+ 1. Research question or scope
162
+ 2. Theory frame or construct map
163
+ 3. Measurement, coding, or extraction standard
164
+ 4. Method design or analysis strategy
165
+
166
+ Low-risk reversible work should continue with visible assumptions instead of a
167
+ hook interruption. If human knowledge, AI inference, and durable project state
168
+ conflict, LongTable should prefer the most explicit durable state; if that state
169
+ is not explicit enough, it should ask the researcher for clarity.
170
+
143
171
  Explicit short forms are available when needed:
144
172
 
145
173
  ```text
@@ -34,6 +34,58 @@ export interface FirstResearchShape {
34
34
  sourceHookId?: string;
35
35
  confirmedAt?: string;
36
36
  }
37
+ export interface ResearchSpecification {
38
+ title: string;
39
+ status?: "draft" | "confirmed" | "deferred";
40
+ createdAt?: string;
41
+ updatedAt?: string;
42
+ sourceHookId?: string;
43
+ researchDirection: {
44
+ question?: string;
45
+ purpose: string;
46
+ scopeBoundary?: string;
47
+ inclusionCriteria?: string[];
48
+ exclusionCriteria?: string[];
49
+ };
50
+ constructOntology: {
51
+ coreConstructs: string[];
52
+ distinctions: string[];
53
+ termsToAvoidCollapsing?: string[];
54
+ };
55
+ theoryAndFraming: {
56
+ anchors: string[];
57
+ alternatives?: string[];
58
+ overreachRisks?: string[];
59
+ };
60
+ measurementCoding: {
61
+ variablesOrConstructs: string[];
62
+ evidenceTypes: string[];
63
+ codingRules: string[];
64
+ openStandards?: string[];
65
+ };
66
+ methodAnalysis: {
67
+ design?: string;
68
+ analysisOptions: string[];
69
+ dataSufficiencyCriteria?: string[];
70
+ unsettledChoices?: string[];
71
+ };
72
+ evidenceAccess: {
73
+ requiredSources?: string[];
74
+ accessRequirements?: string[];
75
+ evidenceStandards?: string[];
76
+ };
77
+ epistemicAlignment: {
78
+ researcherKnowledge?: string[];
79
+ projectStatePriority?: string[];
80
+ aiInferenceLimits?: string[];
81
+ conflictResolutionRule?: string;
82
+ };
83
+ protectedDecisions: string[];
84
+ openQuestions: string[];
85
+ nextActions: string[];
86
+ confidence: "low" | "medium" | "high";
87
+ confirmedAt?: string;
88
+ }
37
89
  export interface LongTableInterviewTurn {
38
90
  id: string;
39
91
  index: number;
@@ -59,6 +111,7 @@ export interface LongTableHookRun {
59
111
  provider?: ProviderKind;
60
112
  turns?: LongTableInterviewTurn[];
61
113
  firstResearchShape?: FirstResearchShape;
114
+ researchSpecification?: ResearchSpecification;
62
115
  qualityNotes?: string[];
63
116
  rationale?: string[];
64
117
  linkedQuestionRecordIds?: string[];
@@ -67,6 +120,7 @@ export interface LongTableHookRun {
67
120
  export type LongTableWorkspaceState = ResearchState & {
68
121
  hooks?: LongTableHookRun[];
69
122
  firstResearchShape?: FirstResearchShape;
123
+ researchSpecification?: ResearchSpecification;
70
124
  };
71
125
  export interface LongTableProjectRecord {
72
126
  schemaVersion: 1;
@@ -102,6 +156,7 @@ export interface LongTableSessionRecord {
102
156
  openQuestions?: string[];
103
157
  startInterview?: StartInterviewSession;
104
158
  firstResearchShape?: FirstResearchShape;
159
+ researchSpecification?: ResearchSpecification;
105
160
  requestedPerspectives: string[];
106
161
  disagreementPreference: ProjectDisagreementPreference;
107
162
  activeModes?: string[];
@@ -132,6 +187,11 @@ export interface LongTableWorkspaceInspection {
132
187
  currentBlocker?: string;
133
188
  requestedPerspectives: string[];
134
189
  disagreementPreference: ProjectDisagreementPreference;
190
+ researchSpecification?: {
191
+ title: string;
192
+ status: "draft" | "confirmed" | "deferred";
193
+ confidence: "low" | "medium" | "high";
194
+ };
135
195
  };
136
196
  files?: {
137
197
  project: string;
@@ -224,6 +284,16 @@ export declare function summarizeLongTableInterview(options: {
224
284
  state: LongTableWorkspaceState;
225
285
  session: LongTableSessionRecord;
226
286
  }>;
287
+ export declare function summarizeLongTableResearchSpecification(options: {
288
+ context: LongTableProjectContext;
289
+ hookId?: string;
290
+ specification: ResearchSpecification;
291
+ }): Promise<{
292
+ hook?: LongTableHookRun;
293
+ specification: ResearchSpecification;
294
+ state: LongTableWorkspaceState;
295
+ session: LongTableSessionRecord;
296
+ }>;
227
297
  export declare function listBlockingWorkspaceQuestions(context: LongTableProjectContext): Promise<QuestionRecord[]>;
228
298
  export declare function listBlockingWorkspaceObligations(context: LongTableProjectContext): Promise<LongTableQuestionObligation[]>;
229
299
  export declare function assertWorkspaceNotBlocked(context: LongTableProjectContext): Promise<void>;
@@ -94,6 +94,9 @@ function buildNextAction(session) {
94
94
  : "Open with your current goal in one sentence, then ask LongTable for the first concrete research move.";
95
95
  }
96
96
  function buildResumeHint(session) {
97
+ if (session.researchSpecification) {
98
+ return `I want to continue from the Research Specification: ${session.researchSpecification.title}.`;
99
+ }
97
100
  if (session.firstResearchShape) {
98
101
  return `I want to continue from the First Research Shape: ${session.firstResearchShape.handle}.`;
99
102
  }
@@ -106,6 +109,51 @@ function buildResumeHint(session) {
106
109
  ? `I want to continue ${session.currentGoal}. The unresolved blocker is ${session.currentBlocker}.`
107
110
  : `I want to continue ${session.currentGoal}.`;
108
111
  }
112
+ function renderResearchSpecificationSummary(specification, locale) {
113
+ const korean = locale === "ko";
114
+ const lines = [
115
+ "",
116
+ korean ? "## Research Specification" : "## Research Specification",
117
+ `- ${korean ? "제목" : "Title"}: ${specification.title}`,
118
+ `- ${korean ? "상태" : "Status"}: ${specification.confirmedAt ? "confirmed" : specification.status ?? "draft"}`,
119
+ `- ${korean ? "신뢰도" : "Confidence"}: ${specification.confidence}`
120
+ ];
121
+ if (specification.researchDirection.question) {
122
+ lines.push(`- ${korean ? "연구 질문" : "Question"}: ${specification.researchDirection.question}`);
123
+ }
124
+ lines.push(`- ${korean ? "목적" : "Purpose"}: ${specification.researchDirection.purpose}`);
125
+ if (specification.researchDirection.scopeBoundary) {
126
+ lines.push(`- ${korean ? "범위 경계" : "Scope boundary"}: ${specification.researchDirection.scopeBoundary}`);
127
+ }
128
+ if (specification.constructOntology.coreConstructs.length > 0) {
129
+ lines.push(`- ${korean ? "핵심 construct" : "Core constructs"}: ${specification.constructOntology.coreConstructs.join("; ")}`);
130
+ }
131
+ if (specification.constructOntology.distinctions.length > 0) {
132
+ lines.push(`- ${korean ? "구분해야 할 차이" : "Key distinctions"}: ${specification.constructOntology.distinctions.join("; ")}`);
133
+ }
134
+ if (specification.theoryAndFraming.anchors.length > 0) {
135
+ lines.push(`- ${korean ? "이론 앵커" : "Theory anchors"}: ${specification.theoryAndFraming.anchors.join("; ")}`);
136
+ }
137
+ if (specification.measurementCoding.codingRules.length > 0) {
138
+ lines.push(`- ${korean ? "코딩 규칙" : "Coding rules"}: ${specification.measurementCoding.codingRules.join("; ")}`);
139
+ }
140
+ if (specification.methodAnalysis.analysisOptions.length > 0) {
141
+ lines.push(`- ${korean ? "분석 옵션" : "Analysis options"}: ${specification.methodAnalysis.analysisOptions.join("; ")}`);
142
+ }
143
+ if (specification.epistemicAlignment.conflictResolutionRule) {
144
+ lines.push(`- ${korean ? "충돌 조정 규칙" : "Conflict rule"}: ${specification.epistemicAlignment.conflictResolutionRule}`);
145
+ }
146
+ if (specification.protectedDecisions.length > 0) {
147
+ lines.push(...specification.protectedDecisions.map((decision) => `- ${korean ? "보호할 결정" : "Protected decision"}: ${decision}`));
148
+ }
149
+ if (specification.openQuestions.length > 0) {
150
+ lines.push(...specification.openQuestions.map((question) => `- ${korean ? "열린 질문" : "Open question"}: ${question}`));
151
+ }
152
+ if (specification.nextActions.length > 0) {
153
+ lines.push(...specification.nextActions.map((action) => `- ${korean ? "다음 행동" : "Next action"}: ${action}`));
154
+ }
155
+ return lines;
156
+ }
109
157
  function buildCurrentGuide(project, session, recentInvocations = [], pendingQuestions = [], pendingObligations = []) {
110
158
  const locale = normalizeLocale(session.locale ?? project.locale);
111
159
  const openQuestions = session.openQuestions && session.openQuestions.length > 0
@@ -129,6 +177,7 @@ function buildCurrentGuide(project, session, recentInvocations = [], pendingQues
129
177
  ...(session.gapRisk ? [`- 공백/암묵지 위험: ${session.gapRisk}`] : []),
130
178
  ...(session.protectedDecision ? [`- 보호할 결정: ${session.protectedDecision}`] : []),
131
179
  ...(session.firstResearchShape ? [`- First Research Shape: ${session.firstResearchShape.handle}`] : []),
180
+ ...(session.researchSpecification ? [`- Research Specification: ${session.researchSpecification.title}`] : []),
132
181
  ...(session.startInterview ? [`- start interview: ${session.startInterview.summary}`] : []),
133
182
  `- 다음 액션: ${nextAction}`,
134
183
  `- 관점: ${session.requestedPerspectives.length > 0 ? session.requestedPerspectives.join(", ") : "auto"}`,
@@ -179,6 +228,7 @@ function buildCurrentGuide(project, session, recentInvocations = [], pendingQues
179
228
  ...session.firstResearchShape.openQuestions.map((question) => `- Open question: ${question}`)
180
229
  ]
181
230
  : []),
231
+ ...(session.researchSpecification ? renderResearchSpecificationSummary(session.researchSpecification, locale) : []),
182
232
  "",
183
233
  "## 빠른 시작",
184
234
  "- 이 디렉토리에서 `codex`를 엽니다.",
@@ -202,6 +252,7 @@ function buildCurrentGuide(project, session, recentInvocations = [], pendingQues
202
252
  ...(session.gapRisk ? [`- Gap/tacit risk: ${session.gapRisk}`] : []),
203
253
  ...(session.protectedDecision ? [`- Protected decision: ${session.protectedDecision}`] : []),
204
254
  ...(session.firstResearchShape ? [`- First Research Shape: ${session.firstResearchShape.handle}`] : []),
255
+ ...(session.researchSpecification ? [`- Research Specification: ${session.researchSpecification.title}`] : []),
205
256
  ...(session.startInterview ? [`- Start interview: ${session.startInterview.summary}`] : []),
206
257
  `- Next action: ${nextAction}`,
207
258
  `- Perspectives: ${session.requestedPerspectives.length > 0 ? session.requestedPerspectives.join(", ") : "auto"}`,
@@ -252,6 +303,7 @@ function buildCurrentGuide(project, session, recentInvocations = [], pendingQues
252
303
  ...session.firstResearchShape.openQuestions.map((question) => `- Open question: ${question}`)
253
304
  ]
254
305
  : []),
306
+ ...(session.researchSpecification ? renderResearchSpecificationSummary(session.researchSpecification, locale) : []),
255
307
  "",
256
308
  "## Quick Start",
257
309
  "- Open `codex` in this directory.",
@@ -272,6 +324,7 @@ async function loadResearchState(stateFilePath) {
272
324
  workingState: parsed.workingState ?? {},
273
325
  hooks: parsed.hooks ?? [],
274
326
  ...(parsed.firstResearchShape ? { firstResearchShape: parsed.firstResearchShape } : {}),
327
+ ...(parsed.researchSpecification ? { researchSpecification: parsed.researchSpecification } : {}),
275
328
  questionObligations: parsed.questionObligations ?? [],
276
329
  inferredHypotheses: parsed.inferredHypotheses ?? [],
277
330
  openTensions: parsed.openTensions ?? [],
@@ -332,7 +385,18 @@ function summarizeWorkspaceInspection(context, state) {
332
385
  currentGoal: context.session.currentGoal,
333
386
  ...(context.session.currentBlocker ? { currentBlocker: context.session.currentBlocker } : {}),
334
387
  requestedPerspectives: context.session.requestedPerspectives,
335
- disagreementPreference: context.session.disagreementPreference
388
+ disagreementPreference: context.session.disagreementPreference,
389
+ ...(context.session.researchSpecification
390
+ ? {
391
+ researchSpecification: {
392
+ title: context.session.researchSpecification.title,
393
+ status: context.session.researchSpecification.confirmedAt
394
+ ? "confirmed"
395
+ : context.session.researchSpecification.status ?? "draft",
396
+ confidence: context.session.researchSpecification.confidence
397
+ }
398
+ }
399
+ : {})
336
400
  },
337
401
  files: {
338
402
  project: context.projectFilePath,
@@ -422,6 +486,7 @@ function buildProjectAgentsMd(project, session) {
422
486
  "- Begin exploratory work with clarifying or tension questions before recommending a direction.",
423
487
  "- 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
488
  "- 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.",
489
+ "- After the First Research Shape, create a Research Specification when the interview has enough detail to preserve scope, construct ontology, theory framing, coding rules, method options, evidence/access requirements, epistemic alignment, protected decisions, open questions, and next actions.",
425
490
  "- 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.",
426
491
  "- Use structured options only at the final First Research Shape confirmation or at true checkpoint boundaries.",
427
492
  "- If you foreground role perspectives, disclose them with `LongTable consulted: ...`.",
@@ -441,6 +506,7 @@ function buildProjectAgentsMd(project, session) {
441
506
  ...(session.gapRisk ? [`- Gap/tacit risk: ${session.gapRisk}`] : []),
442
507
  ...(session.protectedDecision ? [`- Protected decision: ${session.protectedDecision}`] : []),
443
508
  ...(session.firstResearchShape ? [`- First Research Shape: ${session.firstResearchShape.handle}`] : []),
509
+ ...(session.researchSpecification ? [`- Research Specification: ${session.researchSpecification.title}`] : []),
444
510
  ...(session.startInterview ? [`- Start interview summary: ${session.startInterview.summary}`] : []),
445
511
  `- Requested perspectives: ${session.requestedPerspectives.length > 0 ? session.requestedPerspectives.join(", ") : "auto"}`,
446
512
  `- Disagreement visibility: ${session.disagreementPreference}`,
@@ -473,6 +539,10 @@ function buildStateSeed(project, session, setup) {
473
539
  state.firstResearchShape = session.firstResearchShape;
474
540
  state.workingState.firstResearchShape = session.firstResearchShape;
475
541
  }
542
+ if (session.researchSpecification) {
543
+ state.researchSpecification = session.researchSpecification;
544
+ state.workingState.researchSpecification = session.researchSpecification;
545
+ }
476
546
  if (session.currentBlocker) {
477
547
  state.openTensions.push(session.currentBlocker);
478
548
  }
@@ -543,7 +613,16 @@ async function removeLegacyRootFiles(projectPath) {
543
613
  }
544
614
  export async function syncCurrentWorkspaceView(context) {
545
615
  const state = await loadResearchState(context.stateFilePath);
546
- const body = buildCurrentGuide(context.project, context.session, recentInvocationRecords(state), recentPendingQuestions(state), recentPendingObligations(state));
616
+ const session = {
617
+ ...context.session,
618
+ ...(context.session.firstResearchShape ?? state.firstResearchShape
619
+ ? { firstResearchShape: context.session.firstResearchShape ?? state.firstResearchShape }
620
+ : {}),
621
+ ...(context.session.researchSpecification ?? state.researchSpecification
622
+ ? { researchSpecification: context.session.researchSpecification ?? state.researchSpecification }
623
+ : {})
624
+ };
625
+ const body = buildCurrentGuide(context.project, session, recentInvocationRecords(state), recentPendingQuestions(state), recentPendingObligations(state));
547
626
  await writeFile(context.currentFilePath, body, "utf8");
548
627
  return context.currentFilePath;
549
628
  }
@@ -772,6 +851,122 @@ export async function summarizeLongTableInterview(options) {
772
851
  await syncCurrentWorkspaceView(options.context);
773
852
  return { hook, shape, state: updated, session };
774
853
  }
854
+ function normalizeStringArray(values) {
855
+ return (values ?? []).map((value) => value.trim()).filter(Boolean);
856
+ }
857
+ function normalizeOptionalString(value) {
858
+ const trimmed = value?.trim();
859
+ return trimmed && trimmed.length > 0 ? trimmed : undefined;
860
+ }
861
+ function normalizeResearchSpecification(input, sourceHookId, timestamp) {
862
+ const title = input.title.trim();
863
+ if (!title) {
864
+ throw new Error("Research Specification title is required.");
865
+ }
866
+ const purpose = input.researchDirection.purpose.trim();
867
+ if (!purpose) {
868
+ throw new Error("Research Specification researchDirection.purpose is required.");
869
+ }
870
+ return {
871
+ title,
872
+ status: input.confirmedAt ? "confirmed" : input.status ?? "draft",
873
+ createdAt: input.createdAt ?? timestamp,
874
+ updatedAt: timestamp,
875
+ ...(sourceHookId ? { sourceHookId } : {}),
876
+ researchDirection: {
877
+ ...(normalizeOptionalString(input.researchDirection.question) ? { question: normalizeOptionalString(input.researchDirection.question) } : {}),
878
+ purpose,
879
+ ...(normalizeOptionalString(input.researchDirection.scopeBoundary) ? { scopeBoundary: normalizeOptionalString(input.researchDirection.scopeBoundary) } : {}),
880
+ inclusionCriteria: normalizeStringArray(input.researchDirection.inclusionCriteria),
881
+ exclusionCriteria: normalizeStringArray(input.researchDirection.exclusionCriteria)
882
+ },
883
+ constructOntology: {
884
+ coreConstructs: normalizeStringArray(input.constructOntology.coreConstructs),
885
+ distinctions: normalizeStringArray(input.constructOntology.distinctions),
886
+ termsToAvoidCollapsing: normalizeStringArray(input.constructOntology.termsToAvoidCollapsing)
887
+ },
888
+ theoryAndFraming: {
889
+ anchors: normalizeStringArray(input.theoryAndFraming.anchors),
890
+ alternatives: normalizeStringArray(input.theoryAndFraming.alternatives),
891
+ overreachRisks: normalizeStringArray(input.theoryAndFraming.overreachRisks)
892
+ },
893
+ measurementCoding: {
894
+ variablesOrConstructs: normalizeStringArray(input.measurementCoding.variablesOrConstructs),
895
+ evidenceTypes: normalizeStringArray(input.measurementCoding.evidenceTypes),
896
+ codingRules: normalizeStringArray(input.measurementCoding.codingRules),
897
+ openStandards: normalizeStringArray(input.measurementCoding.openStandards)
898
+ },
899
+ methodAnalysis: {
900
+ ...(normalizeOptionalString(input.methodAnalysis.design) ? { design: normalizeOptionalString(input.methodAnalysis.design) } : {}),
901
+ analysisOptions: normalizeStringArray(input.methodAnalysis.analysisOptions),
902
+ dataSufficiencyCriteria: normalizeStringArray(input.methodAnalysis.dataSufficiencyCriteria),
903
+ unsettledChoices: normalizeStringArray(input.methodAnalysis.unsettledChoices)
904
+ },
905
+ evidenceAccess: {
906
+ requiredSources: normalizeStringArray(input.evidenceAccess.requiredSources),
907
+ accessRequirements: normalizeStringArray(input.evidenceAccess.accessRequirements),
908
+ evidenceStandards: normalizeStringArray(input.evidenceAccess.evidenceStandards)
909
+ },
910
+ epistemicAlignment: {
911
+ researcherKnowledge: normalizeStringArray(input.epistemicAlignment.researcherKnowledge),
912
+ projectStatePriority: normalizeStringArray(input.epistemicAlignment.projectStatePriority),
913
+ aiInferenceLimits: normalizeStringArray(input.epistemicAlignment.aiInferenceLimits),
914
+ ...(normalizeOptionalString(input.epistemicAlignment.conflictResolutionRule)
915
+ ? { conflictResolutionRule: normalizeOptionalString(input.epistemicAlignment.conflictResolutionRule) }
916
+ : {})
917
+ },
918
+ protectedDecisions: normalizeStringArray(input.protectedDecisions),
919
+ openQuestions: normalizeStringArray(input.openQuestions),
920
+ nextActions: normalizeStringArray(input.nextActions),
921
+ confidence: input.confidence,
922
+ ...(input.confirmedAt ? { confirmedAt: input.confirmedAt } : {})
923
+ };
924
+ }
925
+ export async function summarizeLongTableResearchSpecification(options) {
926
+ const state = await loadResearchState(options.context.stateFilePath);
927
+ const sourceHookId = options.hookId
928
+ ?? options.specification.sourceHookId
929
+ ?? state.firstResearchShape?.sourceHookId;
930
+ const existing = sourceHookId
931
+ ? (state.hooks ?? []).find((hook) => hook.id === sourceHookId)
932
+ : activeInterviewHook(state);
933
+ const timestamp = nowIso();
934
+ const specification = normalizeResearchSpecification(options.specification, existing?.id ?? sourceHookId, timestamp);
935
+ const hook = existing
936
+ ? {
937
+ ...existing,
938
+ status: "ready_to_confirm",
939
+ updatedAt: timestamp,
940
+ researchSpecification: specification
941
+ }
942
+ : undefined;
943
+ const session = {
944
+ ...options.context.session,
945
+ lastUpdatedAt: timestamp,
946
+ researchSpecification: specification,
947
+ resumeHint: `I want to continue from the Research Specification: ${specification.title}.`
948
+ };
949
+ options.context.session = session;
950
+ let updated = hook ? upsertHook(state, hook) : state;
951
+ updated.researchSpecification = specification;
952
+ updated.workingState = {
953
+ ...updated.workingState,
954
+ researchSpecification: specification
955
+ };
956
+ updated.narrativeTraces.push({
957
+ id: createId("narrative_trace"),
958
+ timestamp,
959
+ source: "$longtable-interview",
960
+ traceType: "judgment",
961
+ summary: `Research Specification draft: ${specification.title}.`,
962
+ visibility: "explicit",
963
+ importance: specification.confidence
964
+ });
965
+ await writeFile(options.context.sessionFilePath, JSON.stringify(session, null, 2), "utf8");
966
+ await writeFile(options.context.stateFilePath, JSON.stringify(updated, null, 2), "utf8");
967
+ await syncCurrentWorkspaceView(options.context);
968
+ return { hook, specification, state: updated, session };
969
+ }
775
970
  function findQuestionForDecision(state, questionId) {
776
971
  const pending = (state.questionLog ?? []).filter((record) => record.status === "pending");
777
972
  if (questionId) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@longtable/cli",
3
- "version": "0.1.43",
3
+ "version": "0.1.44",
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.43",
33
- "@longtable/core": "0.1.43",
34
- "@longtable/memory": "0.1.43",
35
- "@longtable/provider-claude": "0.1.43",
36
- "@longtable/provider-codex": "0.1.43",
37
- "@longtable/setup": "0.1.43"
32
+ "@longtable/checkpoints": "0.1.44",
33
+ "@longtable/core": "0.1.44",
34
+ "@longtable/memory": "0.1.44",
35
+ "@longtable/provider-claude": "0.1.44",
36
+ "@longtable/provider-codex": "0.1.44",
37
+ "@longtable/setup": "0.1.44"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@types/node": "^22.10.1",