@longtable/cli 0.1.3 → 0.1.5

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
@@ -2,14 +2,16 @@
2
2
 
3
3
  Long Table의 researcher-facing CLI입니다.
4
4
 
5
- 패키지는 연구자가 아래처럼 시작할 있게 만드는 것이 목표입니다.
5
+ 패키지의 핵심은 명령을 많이 외우게 하는 것이 아니라, 아래 두 단계를 명확하게 만드는 것입니다.
6
6
 
7
- - `longtable init`
8
- - `longtable start`
9
- - `longtable ask --prompt "..."`
7
+ 1. `longtable init`
8
+ 2. `longtable start`
9
+
10
+ 중요한 점:
10
11
 
11
- 내부 명령이나 legacy package 이름을 필요 없이,
12
- 연구자가 `오늘 무엇이 필요한가`에서 바로 출발할 있게 하는 표면입니다.
12
+ - 명령은 **터미널에서 실행하는 명령**입니다.
13
+ - Codex 채팅창에 `longtable start`를 입력하는 것이 아닙니다.
14
+ - `longtable start`로 프로젝트 작업공간을 만든 뒤, 그 디렉토리에서 `codex`를 여는 것이 기본 경로입니다.
13
15
 
14
16
  ## Install
15
17
 
@@ -17,59 +19,75 @@ Long Table의 researcher-facing CLI입니다.
17
19
  npm install -g @longtable/cli
18
20
  ```
19
21
 
20
- ## Recommended flow
21
-
22
- ### 1. Setup once
22
+ ## 1. Global setup
23
23
 
24
24
  ```bash
25
25
  longtable init --flow interview
26
26
  ```
27
27
 
28
- 인터뷰 setup은 연구자의
29
-
30
- - 현재 주제
31
- - 현재 막힘
32
- - 선호하는 첫 진입 모드
33
- - 가장 많이 도전받고 싶은 영역
34
- - 역할 간 의견 충돌 가시성
28
+ 여기서는 연구자 프로필과 기본 선호를 묻습니다.
35
29
 
36
- 까지 저장합니다.
30
+ - 연구 분야
31
+ - 연구자 역할
32
+ - challenge 강도
33
+ - 저자성/서사 관련 기본값
34
+ - Long Table이 보통 어디서부터 시작해야 하는지
37
35
 
38
- ### 2. Start from today’s work
36
+ ## 2. Project start
39
37
 
40
38
  ```bash
41
39
  longtable start
42
40
  ```
43
41
 
44
- `longtable start`는 가장 단순한 진입점입니다.
45
- 오늘 필요한 작업을 먼저 고르고, 그 다음 자연어로 상황을 말하면 됩니다.
42
+ 여기서는 실제 작업공간을 만듭니다.
46
43
 
47
- ### 3. Or jump straight in
44
+ - 프로젝트 이름
45
+ - 프로젝트 디렉토리 위치
46
+ - 현재 세션 목표
47
+ - 현재 blocker
48
+ - 필요한 관점
49
+ - disagreement 가시성
50
+
51
+ 그리고 Long Table은:
52
+
53
+ - 프로젝트 디렉토리 생성
54
+ - `.longtable/` 메모리 파일 생성
55
+ - 프로젝트용 `AGENTS.md` 생성
56
+ - `START-HERE.md` 생성
57
+
58
+ 을 수행합니다.
59
+
60
+ ## Recommended flow
48
61
 
49
62
  ```bash
50
- longtable ask --prompt "연구를 시작하고 싶은데 어디서부터 좁혀야 할지 모르겠어."
63
+ longtable init --flow interview
64
+ longtable start
65
+ cd "<project-path>"
66
+ codex
51
67
  ```
52
68
 
53
- ## Useful commands
69
+ 이게 현재 가장 신뢰할 수 있는 Long Table 사용 경로입니다.
70
+
71
+ ## After the project starts
72
+
73
+ 프로젝트 디렉토리 안에서는 보통 그냥 `codex`를 열고 자연어로 연구를 시작하면 됩니다.
74
+
75
+ CLI를 계속 쓰고 싶다면 아래 명령은 보조 경로입니다.
76
+
77
+ ## Advanced commands
54
78
 
55
79
  ```bash
56
- longtable start
57
80
  longtable roles
58
- longtable ask --prompt "연구 아이디어를 어디서부터 열어야 할지 모르겠어."
59
- longtable review --prompt "BJET 편집자 관점에서 봐줘." --role editor
60
- longtable review --prompt "방법론적으로 어디가 취약한지 말해줘." --role methods_critic
61
- longtable review --prompt "의견 충돌까지 보여줘." --panel --show-conflicts
62
- longtable show --json
81
+ longtable ask --cwd "<project-path>" --prompt "연구 질문을 어디서부터 좁혀야 할지 모르겠어."
82
+ longtable review --cwd "<project-path>" --prompt "방법론적으로 어디가 취약한지 말해줘." --role methods_critic
63
83
  ```
64
84
 
65
- ## What `roles` means
85
+ ## Roles
66
86
 
67
87
  ```bash
68
88
  longtable roles
69
89
  ```
70
90
 
71
- Long Table이 어떤 관점을 호출할 수 있는지 짧게 보여줍니다.
72
-
73
91
  예:
74
92
 
75
93
  - `editor`
@@ -77,25 +95,20 @@ Long Table이 어떤 관점을 호출할 수 있는지 짧게 보여줍니다.
77
95
  - `methods_critic`
78
96
  - `voice_keeper`
79
97
 
80
- 기본값은 하나의 Long Table synthesis이지만, 필요하면 panel과 conflict도 드러낼 수 있습니다.
81
-
82
- ## Codex integration
83
-
84
- Long Table은 Codex prompt file 설치도 지원합니다.
98
+ ## Codex prompt-file integration
85
99
 
86
100
  ```bash
87
101
  longtable codex install-prompts
88
102
  longtable codex status
89
103
  ```
90
104
 
91
- 하지만 이 경로는 현재 `실험적`입니다.
105
+ 이 경로는 현재 실험적입니다.
106
+ 사용자에게 약속하는 주 경로는 아닙니다.
92
107
 
93
- - Codex 버전/빌드에 따라 prompt file이 slash command로 바로 노출되지 않을 수 있습니다.
94
- - 그래서 기본 사용자 경로는 `/prompts:...`가 아니라 `longtable start`와 `longtable ask`입니다.
108
+ 기본 경로는 여전히:
95
109
 
96
- ## Why this package exists
97
-
98
- Long Table은 제품명입니다.
99
- `Diverga`는 아직 일부 하위 패키지와 runtime path에서만 남아 있는 호환성 레이어입니다.
110
+ - `longtable init`
111
+ - `longtable start`
112
+ - 프로젝트 디렉토리에서 `codex`
100
113
 
101
- 이 패키지는 연구자가 `longtable`만 알면 되게 하기 위해 존재합니다.
114
+ 입니다.
package/dist/cli.js CHANGED
@@ -3,11 +3,13 @@ import { existsSync, readFileSync } from "node:fs";
3
3
  import { emitKeypressEvents } from "node:readline";
4
4
  import { createInterface } from "node:readline/promises";
5
5
  import { stdin as input, stdout as output, cwd, exit } from "node:process";
6
+ import { resolve } from "node:path";
6
7
  import { buildProviderChoices, buildQuickSetupFlow, createPersistedSetupOutput, installRuntimeConfigFromStoredSetup, loadSetupOutput, renderInstallSummary, renderSetupSummary, resolveDefaultRuntimeConfigPath, resolveDefaultSetupPath, saveSetupAndRuntimeConfig, serializeSetupOutput } from "@diverga/setup";
7
8
  import { buildCodexThinWrappedPrompt, runCodexThinWrapper } from "@diverga/provider-codex";
8
9
  import { installCodexPromptAliases, listInstalledCodexPromptAliases, removeCodexPromptAliases, resolveCodexPromptsDir } from "./prompt-aliases.js";
9
10
  import { buildPersonaGuidance } from "./persona-router.js";
10
11
  import { PERSONA_DEFINITIONS } from "./personas.js";
12
+ import { createOrUpdateProjectWorkspace, loadProjectContextFromDirectory, renderProjectWorkspaceSummary } from "./project-session.js";
11
13
  const VALID_MODES = new Set([
12
14
  "explore",
13
15
  "review",
@@ -25,11 +27,32 @@ const VALID_STAGES = new Set([
25
27
  "writing",
26
28
  "submission"
27
29
  ]);
30
+ const ANSI = {
31
+ reset: "\u001B[0m",
32
+ bold: "\u001B[1m",
33
+ dim: "\u001B[2m",
34
+ cyan: "\u001B[36m",
35
+ green: "\u001B[32m"
36
+ };
37
+ function style(text, prefix) {
38
+ return `${prefix}${text}${ANSI.reset}`;
39
+ }
40
+ function renderSectionCard(title, body) {
41
+ return [
42
+ "┌──────────────────────────────────────────────┐",
43
+ `│ ${title.padEnd(44, " ")}│`,
44
+ "└──────────────────────────────────────────────┘",
45
+ ...body
46
+ ].join("\n");
47
+ }
28
48
  function usage() {
29
49
  return [
30
50
  "Usage:",
31
- " longtable init [--flow quickstart|interview] [--provider codex|claude] [--field <field>] [--career-stage <stage>] [--experience novice|intermediate|advanced] [--project-type <type>] [--checkpoint low|balanced|high] [--authorship-signal <text>] [--topic <text>] [--blocker <text>] [--entry-mode explore|review|critique|draft|commit] [--weakest-domain theory|methodology|measurement|analysis|writing] [--panel-preference synthesis_only|show_on_conflict|always_visible] [--json] [--no-install] [--install-prompts]",
32
- " longtable start [--prompt <text>] [--setup <path>] [--cwd <path>] [--print] [--json]",
51
+ " Run `longtable ...` in your terminal, not inside the Codex chat box.",
52
+ " After `longtable start`, move into the created project directory and open `codex` there.",
53
+ "",
54
+ " longtable init [--flow quickstart|interview] [--provider codex|claude] [--field <field>] [--career-stage <stage>] [--experience novice|intermediate|advanced] [--checkpoint low|balanced|high] [--authorship-signal <text>] [--entry-mode explore|review|critique|draft|commit] [--weakest-domain theory|methodology|measurement|analysis|writing] [--panel-preference synthesis_only|show_on_conflict|always_visible] [--json] [--no-install] [--install-prompts]",
55
+ " longtable start [--path <dir>] [--name <project>] [--goal <text>] [--blocker <text>] [--perspectives <role[,role]>] [--disagreement synthesis_only|show_on_conflict|always_visible] [--setup <path>] [--json]",
33
56
  " longtable roles [--json]",
34
57
  " longtable show [--json] [--path <file>]",
35
58
  " longtable install [--json] [--path <file>] [--runtime-path <file>]",
@@ -43,10 +66,10 @@ function usage() {
43
66
  "Examples:",
44
67
  " longtable init --flow interview --install-prompts",
45
68
  " longtable start",
69
+ " longtable start --path ~/Research/My-Project --name \"AI Adoption Meta-Analysis\" --goal \"Narrow the review question\"",
70
+ " cd \"<project-path>\" && codex",
46
71
  " longtable roles",
47
72
  " longtable ask --prompt \"연구를 시작하고 싶어. 지금 어디서부터 좁혀야 할지 모르겠어.\"",
48
- " longtable review --prompt \"Review this claim critically.\" --panel --show-conflicts",
49
- " longtable review --role editor --prompt \"BJET 편집자 관점에서 봐줘.\"",
50
73
  " printf '{\"provider\":\"codex\",...}' | longtable codex persist-init --stdin --install-prompts",
51
74
  " longtable codex install-prompts"
52
75
  ].join("\n");
@@ -115,25 +138,17 @@ function buildSetupFlowChoices() {
115
138
  function renderSetupHeader(flow) {
116
139
  const title = flow === "interview" ? "Long Table Setup Interview" : "Long Table Quickstart";
117
140
  const subtitle = flow === "interview"
118
- ? "We will ask about your research persona, current topic, blocker, and preferred kind of challenge."
141
+ ? "We will ask about your research persona, challenge preferences, and authorship defaults."
119
142
  : "We will capture the minimum profile needed to start using Long Table.";
120
- return [
121
- "┌──────────────────────────────────────────────┐",
122
- `│ ${title.padEnd(44, " ")}│`,
123
- "└──────────────────────────────────────────────┘",
124
- subtitle
125
- ].join("\n");
143
+ return renderSectionCard(title, [subtitle]);
126
144
  }
127
145
  function renderQuestionHeader(index, total, section, prompt) {
128
- return [``, `[${index}/${total}] ${section}`, prompt].join("\n");
146
+ return [``, style(`[${index}/${total}] ${section}`, `${ANSI.bold}${ANSI.cyan}`), prompt].join("\n");
129
147
  }
130
148
  function questionSection(questionId) {
131
149
  if (questionId === "field" || questionId === "careerStage" || questionId === "experienceLevel") {
132
150
  return "Researcher profile";
133
151
  }
134
- if (questionId === "currentProjectType" || questionId === "currentResearchTopic" || questionId === "currentBlocker") {
135
- return "Current work";
136
- }
137
152
  if (questionId === "preferredCheckpointIntensity" || questionId === "preferredEntryMode") {
138
153
  return "Interaction style";
139
154
  }
@@ -152,9 +167,9 @@ function clearLine() {
152
167
  return "\u001B[2K\r";
153
168
  }
154
169
  function renderArrowMenu(prompt, choices, selectedIndex) {
155
- const lines = [prompt, "Use ↑/↓ and Enter."];
170
+ const lines = [style(prompt, ANSI.bold), style("Use ↑/↓ and Enter.", ANSI.dim)];
156
171
  for (let index = 0; index < choices.length; index += 1) {
157
- const prefix = index === selectedIndex ? ">" : " ";
172
+ const prefix = index === selectedIndex ? style(">", `${ANSI.bold}${ANSI.green}`) : " ";
158
173
  lines.push(`${prefix} ${choices[index].label} - ${choices[index].description}`);
159
174
  }
160
175
  return lines.join("\n");
@@ -260,54 +275,84 @@ async function promptChoiceWithArrows(rl, prompt, choices) {
260
275
  async function promptChoice(rl, prompt, choices) {
261
276
  return promptChoiceWithArrows(rl, prompt, choices);
262
277
  }
263
- async function promptStartAction(rl, blocker) {
264
- return (await promptChoice(rl, blocker?.trim()
265
- ? `What do you want to do with this first?\nCurrent blocker: ${blocker.trim()}`
266
- : "What do you want Long Table to help you do first?", [
267
- {
268
- id: "explore",
269
- label: "Open the problem",
270
- description: "Start with questions, tensions, and a clearer research direction."
271
- },
272
- {
273
- id: "review",
274
- label: "Review something",
275
- description: "Critically inspect a claim, outline, or draft."
276
- },
277
- {
278
- id: "critique",
279
- label: "Challenge me hard",
280
- description: "Push back, stress-test assumptions, and surface hidden risks."
281
- },
282
- {
283
- id: "editor",
284
- label: "Journal editor view",
285
- description: "See how an editor might judge framing, fit, and contribution."
286
- },
287
- {
288
- id: "methods",
289
- label: "Methods check",
290
- description: "Focus on design fit, defensibility, and methodological weakness."
291
- },
292
- {
293
- id: "panel",
294
- label: "Show disagreement",
295
- description: "Let multiple roles disagree instead of forcing one answer."
296
- },
297
- {
298
- id: "status",
299
- label: "Check my setup",
300
- description: "See what Long Table knows and what is installed."
301
- },
302
- {
303
- id: "roles",
304
- label: "What roles exist?",
305
- description: "See what perspectives Long Table can bring into the conversation."
278
+ async function promptMultiChoice(rl, prompt, choices) {
279
+ const stream = input;
280
+ if (!stream.isTTY || !output.isTTY) {
281
+ const answer = await rl.question(`${prompt}\nType comma-separated ids or leave blank for auto.\n> `);
282
+ return answer
283
+ .split(",")
284
+ .map((part) => part.trim())
285
+ .filter(Boolean);
286
+ }
287
+ const previousRawMode = stream.isRaw;
288
+ let selectedIndex = 0;
289
+ const selected = new Set();
290
+ const renderedLineCount = choices.length + 2;
291
+ return await new Promise((resolvePromise, reject) => {
292
+ function draw(first = false) {
293
+ if (!first) {
294
+ output.write(moveCursorUp(renderedLineCount));
295
+ }
296
+ const lines = [prompt, "Use ↑/↓, Space to toggle, and Enter to confirm."];
297
+ for (let index = 0; index < choices.length; index += 1) {
298
+ const choice = choices[index];
299
+ const pointer = index === selectedIndex ? ">" : " ";
300
+ const marker = selected.has(choice.id) ? "[x]" : "[ ]";
301
+ lines.push(`${pointer} ${marker} ${choice.label} - ${choice.description}`);
302
+ }
303
+ for (const line of lines) {
304
+ output.write(clearLine());
305
+ output.write(`${line}\n`);
306
+ }
306
307
  }
307
- ]));
308
+ function cleanup() {
309
+ stream.off("keypress", onKeypress);
310
+ if (stream.isTTY) {
311
+ stream.setRawMode(previousRawMode ?? false);
312
+ }
313
+ output.write("\u001B[?25h");
314
+ }
315
+ function onKeypress(_, key) {
316
+ if (key.ctrl && key.name === "c") {
317
+ cleanup();
318
+ reject(new Error("Setup cancelled."));
319
+ return;
320
+ }
321
+ if (key.name === "up") {
322
+ selectedIndex = selectedIndex === 0 ? choices.length - 1 : selectedIndex - 1;
323
+ draw();
324
+ return;
325
+ }
326
+ if (key.name === "down") {
327
+ selectedIndex = selectedIndex === choices.length - 1 ? 0 : selectedIndex + 1;
328
+ draw();
329
+ return;
330
+ }
331
+ if (key.name === "space") {
332
+ const id = choices[selectedIndex].id;
333
+ if (selected.has(id)) {
334
+ selected.delete(id);
335
+ }
336
+ else {
337
+ selected.add(id);
338
+ }
339
+ draw();
340
+ return;
341
+ }
342
+ if (key.name === "return") {
343
+ cleanup();
344
+ resolvePromise([...selected]);
345
+ }
346
+ }
347
+ emitKeypressEvents(stream);
348
+ stream.setRawMode(true);
349
+ output.write("\u001B[?25l");
350
+ draw(true);
351
+ stream.on("keypress", onKeypress);
352
+ });
308
353
  }
309
354
  function hasCompleteFlagInput(args) {
310
- const required = ["provider", "field", "career-stage", "experience", "project-type", "checkpoint"];
355
+ const required = ["provider", "field", "career-stage", "experience", "checkpoint"];
311
356
  return required.every((key) => typeof args[key] === "string" && String(args[key]).trim().length > 0);
312
357
  }
313
358
  function resolveSetupFlow(args) {
@@ -318,13 +363,13 @@ function toSetupAnswers(args) {
318
363
  field: String(args.field),
319
364
  careerStage: String(args["career-stage"]),
320
365
  experienceLevel: String(args.experience),
321
- currentProjectType: String(args["project-type"]),
366
+ currentProjectType: typeof args["project-type"] === "string" && args["project-type"].trim().length > 0
367
+ ? String(args["project-type"])
368
+ : "unspecified research task",
322
369
  preferredCheckpointIntensity: String(args.checkpoint),
323
370
  humanAuthorshipSignal: typeof args["authorship-signal"] === "string" && args["authorship-signal"].trim().length > 0
324
371
  ? args["authorship-signal"].trim()
325
372
  : undefined,
326
- currentResearchTopic: typeof args.topic === "string" && args.topic.trim().length > 0 ? args.topic.trim() : undefined,
327
- currentBlocker: typeof args.blocker === "string" && args.blocker.trim().length > 0 ? args.blocker.trim() : undefined,
328
373
  preferredEntryMode: typeof args["entry-mode"] === "string" && VALID_MODES.has(String(args["entry-mode"]))
329
374
  ? String(args["entry-mode"])
330
375
  : undefined,
@@ -336,15 +381,18 @@ function toSetupAnswers(args) {
336
381
  : undefined
337
382
  };
338
383
  }
339
- async function collectInteractiveAnswers() {
384
+ async function collectInteractiveAnswers(initialFlow) {
340
385
  const rl = createInterface({ input, output });
341
386
  try {
342
- const flow = (await promptChoice(rl, "How would you like to set up Long Table?", buildSetupFlowChoices()));
387
+ const flow = initialFlow ??
388
+ (await promptChoice(rl, "How would you like to set up Long Table?", buildSetupFlowChoices()));
343
389
  console.log("");
344
390
  console.log(renderSetupHeader(flow));
345
391
  console.log("");
346
392
  const provider = await promptChoice(rl, "Which provider do you want to configure?", buildProviderChoices());
347
- const answers = {};
393
+ const answers = {
394
+ currentProjectType: "unspecified research task"
395
+ };
348
396
  const questions = buildQuickSetupFlow(flow);
349
397
  for (let index = 0; index < questions.length; index += 1) {
350
398
  const question = questions[index];
@@ -365,18 +413,12 @@ async function collectInteractiveAnswers() {
365
413
  answers.careerStage = value;
366
414
  if (question.id === "experienceLevel")
367
415
  answers.experienceLevel = value;
368
- if (question.id === "currentProjectType")
369
- answers.currentProjectType = value;
370
416
  if (question.id === "preferredCheckpointIntensity") {
371
417
  answers.preferredCheckpointIntensity = value;
372
418
  }
373
419
  if (question.id === "humanAuthorshipSignal" && value !== "other") {
374
420
  answers.humanAuthorshipSignal = value;
375
421
  }
376
- if (question.id === "currentResearchTopic")
377
- answers.currentResearchTopic = value;
378
- if (question.id === "currentBlocker")
379
- answers.currentBlocker = value;
380
422
  if (question.id === "preferredEntryMode")
381
423
  answers.preferredEntryMode = value;
382
424
  if (question.id === "weakestDomain")
@@ -394,6 +436,84 @@ async function collectInteractiveAnswers() {
394
436
  rl.close();
395
437
  }
396
438
  }
439
+ function perspectiveChoices() {
440
+ return PERSONA_DEFINITIONS.map((persona) => ({
441
+ id: persona.key,
442
+ label: persona.label,
443
+ description: persona.shortDescription
444
+ }));
445
+ }
446
+ function normalizePerspectiveList(value) {
447
+ if (!value?.trim()) {
448
+ return [];
449
+ }
450
+ return value
451
+ .split(",")
452
+ .map((part) => part.trim())
453
+ .filter(Boolean);
454
+ }
455
+ async function collectProjectInterview(setup, args) {
456
+ const needsInteractivePrompts = !(typeof args.name === "string" && args.name.trim()) ||
457
+ !(typeof args.path === "string" && args.path.trim()) ||
458
+ !(typeof args.goal === "string" && args.goal.trim()) ||
459
+ typeof args.blocker !== "string" ||
460
+ normalizePerspectiveList(typeof args.perspectives === "string" ? args.perspectives : undefined).length === 0 ||
461
+ !(typeof args.disagreement === "string" && args.disagreement.trim());
462
+ const rl = createInterface({ input, output });
463
+ try {
464
+ if (needsInteractivePrompts) {
465
+ console.log("");
466
+ console.log(renderSectionCard("Long Table Project Start", [
467
+ "We will create a project workspace and a session memory seed for today's work.",
468
+ "At the end, Long Table will tell you exactly which directory to open in Codex."
469
+ ]));
470
+ console.log("");
471
+ }
472
+ const projectName = (typeof args.name === "string" && args.name.trim()) ||
473
+ (await promptText(rl, renderQuestionHeader(1, 6, "Project interview", "What should this project be called?"), true));
474
+ const suggestedPath = typeof args.path === "string" && args.path.trim()
475
+ ? args.path.trim()
476
+ : resolve(projectName.replace(/\s+/g, "-"));
477
+ const projectPath = (typeof args.path === "string" && args.path.trim()) ||
478
+ (await promptText(rl, renderQuestionHeader(2, 6, "Project interview", `Where should this project live on your machine?\nSuggested path: ${suggestedPath}`), true));
479
+ const currentGoal = (typeof args.goal === "string" && args.goal.trim()) ||
480
+ (await promptText(rl, renderQuestionHeader(3, 6, "Current session", "What are you trying to accomplish in this session?"), true));
481
+ const currentBlocker = (typeof args.blocker === "string" && args.blocker.trim()) ||
482
+ (await promptText(rl, renderQuestionHeader(4, 6, "Current session", "What is the main blocker or uncertainty right now?"), false));
483
+ const requestedPerspectives = normalizePerspectiveList(typeof args.perspectives === "string" ? args.perspectives : undefined).length > 0
484
+ ? normalizePerspectiveList(typeof args.perspectives === "string" ? args.perspectives : undefined)
485
+ : await promptMultiChoice(rl, renderQuestionHeader(5, 6, "Perspectives", "Which perspectives do you already know you want at the table? Leave everything unchecked for auto."), perspectiveChoices());
486
+ const disagreementPreference = (typeof args.disagreement === "string" && args.disagreement.trim()) ||
487
+ (await promptChoice(rl, renderQuestionHeader(6, 6, "Disagreement", "How visible should disagreement between perspectives be in this project by default?"), [
488
+ {
489
+ id: "synthesis_only",
490
+ label: "Synthesis only",
491
+ description: "Show one Long Table answer unless I ask for more."
492
+ },
493
+ {
494
+ id: "show_on_conflict",
495
+ label: "Show on conflict",
496
+ description: "Surface disagreement when the perspectives materially diverge."
497
+ },
498
+ {
499
+ id: "always_visible",
500
+ label: "Always visible",
501
+ description: "Keep panel opinions visible by default."
502
+ }
503
+ ]));
504
+ return {
505
+ projectName: projectName.trim(),
506
+ projectPath: projectPath.trim(),
507
+ currentGoal: currentGoal.trim(),
508
+ ...(currentBlocker?.trim() ? { currentBlocker: currentBlocker.trim() } : {}),
509
+ requestedPerspectives,
510
+ disagreementPreference: disagreementPreference
511
+ };
512
+ }
513
+ finally {
514
+ rl.close();
515
+ }
516
+ }
397
517
  function normalizePersistAnswers(raw) {
398
518
  return {
399
519
  flow: raw.flow === "interview" ? "interview" : "quickstart",
@@ -402,17 +522,11 @@ function normalizePersistAnswers(raw) {
402
522
  field: raw.field,
403
523
  careerStage: raw.careerStage,
404
524
  experienceLevel: raw.experienceLevel,
405
- currentProjectType: raw.currentProjectType,
525
+ currentProjectType: "unspecified research task",
406
526
  preferredCheckpointIntensity: raw.preferredCheckpointIntensity,
407
527
  ...(raw.humanAuthorshipSignal?.trim()
408
528
  ? { humanAuthorshipSignal: raw.humanAuthorshipSignal.trim() }
409
529
  : {}),
410
- ...(raw.currentResearchTopic?.trim()
411
- ? { currentResearchTopic: raw.currentResearchTopic.trim() }
412
- : {}),
413
- ...(raw.currentBlocker?.trim()
414
- ? { currentBlocker: raw.currentBlocker.trim() }
415
- : {}),
416
530
  ...(raw.preferredEntryMode
417
531
  ? { preferredEntryMode: raw.preferredEntryMode }
418
532
  : {}),
@@ -462,7 +576,7 @@ async function runInit(args) {
462
576
  provider: String(args.provider) === "claude" ? "claude" : "codex",
463
577
  answers: toSetupAnswers(args)
464
578
  }
465
- : await collectInteractiveAnswers();
579
+ : await collectInteractiveAnswers(typeof args.flow === "string" ? resolveSetupFlow(args) : undefined);
466
580
  const outputValue = createPersistedSetupOutput(answers, provider, flow);
467
581
  const result = await saveSetupAndRuntimeConfig(outputValue, {
468
582
  setupPath: customPath,
@@ -495,15 +609,7 @@ async function runInit(args) {
495
609
  console.log("- Start here: `longtable start`.");
496
610
  console.log("- If you want a direct natural-language entry: `longtable ask --prompt \"...\"`.");
497
611
  console.log("- Codex prompt files are available as an experimental integration, not the primary path.");
498
- if (answers.currentBlocker) {
499
- console.log(`- Suggested first message: ${answers.currentBlocker}`);
500
- }
501
- else if (answers.currentResearchTopic) {
502
- console.log(`- Suggested first message: Help me open the problem around ${answers.currentResearchTopic}.`);
503
- }
504
- else {
505
- console.log("- Suggested first message: I want to start my research and need help opening the problem.");
506
- }
612
+ console.log("- Suggested next action: create a project workspace and let Long Table interview the current session.");
507
613
  }
508
614
  }
509
615
  async function runShow(args) {
@@ -561,15 +667,7 @@ async function runCodexPersistInit(args) {
561
667
  console.log("- Start here: `longtable start`.");
562
668
  console.log("- If you want a direct natural-language entry: `longtable ask --prompt \"...\"`.");
563
669
  console.log("- Codex prompt files are available as an experimental integration, not the primary path.");
564
- if (answers.currentBlocker) {
565
- console.log(`- Suggested first message: ${answers.currentBlocker}`);
566
- }
567
- else if (answers.currentResearchTopic) {
568
- console.log(`- Suggested first message: Help me think through ${answers.currentResearchTopic}.`);
569
- }
570
- else {
571
- console.log("- Suggested first message: I want to start my research and I am not sure where to open the problem.");
572
- }
670
+ console.log("- Suggested next action: create a project workspace and let Long Table interview the current session.");
573
671
  }
574
672
  }
575
673
  async function resolvePrompt(prompt) {
@@ -638,7 +736,25 @@ async function loadOptionalSetup(path) {
638
736
  return null;
639
737
  }
640
738
  }
739
+ async function buildProjectAwarePrompt(prompt, workingDirectory) {
740
+ const context = await loadProjectContextFromDirectory(workingDirectory);
741
+ if (!context) {
742
+ return { prompt, projectContextFound: false };
743
+ }
744
+ const lines = [
745
+ "Long Table project context",
746
+ `project: ${context.project.projectName}`,
747
+ `current session goal: ${context.session.currentGoal}`,
748
+ ...(context.session.currentBlocker ? [`current blocker: ${context.session.currentBlocker}`] : []),
749
+ `requested perspectives: ${context.session.requestedPerspectives.length > 0 ? context.session.requestedPerspectives.join(", ") : "auto"}`,
750
+ `disagreement preference: ${context.session.disagreementPreference}`,
751
+ "",
752
+ prompt
753
+ ];
754
+ return { prompt: lines.join("\n"), projectContextFound: true };
755
+ }
641
756
  async function runModeCommand(mode, args) {
757
+ const workingDirectory = typeof args.cwd === "string" ? args.cwd : cwd();
642
758
  const prompt = await resolvePrompt(typeof args.prompt === "string" ? args.prompt : undefined);
643
759
  if (!prompt) {
644
760
  throw new Error("A prompt is required.");
@@ -648,13 +764,14 @@ async function runModeCommand(mode, args) {
648
764
  throw new Error(`Invalid stage: ${stage}`);
649
765
  }
650
766
  const setup = await loadOptionalSetup(typeof args.setup === "string" ? args.setup : undefined);
767
+ const projectAware = await buildProjectAwarePrompt(prompt, workingDirectory);
651
768
  const panelPreference = setup?.profileSeed.panelPreference;
652
769
  const panelRequested = args.panel === true ||
653
770
  panelPreference === "always_visible" ||
654
771
  (panelPreference === "show_on_conflict" && args["show-conflicts"] === true);
655
772
  const { guidedPrompt } = buildPersonaGuidance({
656
773
  mode,
657
- prompt,
774
+ prompt: projectAware.prompt,
658
775
  roleFlag: typeof args.role === "string" ? args.role : undefined,
659
776
  panel: panelRequested,
660
777
  showConflicts: args["show-conflicts"] === true,
@@ -666,7 +783,7 @@ async function runModeCommand(mode, args) {
666
783
  mode,
667
784
  researchStage: stage,
668
785
  setupPath: typeof args.setup === "string" ? args.setup : undefined,
669
- workingDirectory: typeof args.cwd === "string" ? args.cwd : cwd()
786
+ workingDirectory
670
787
  });
671
788
  console.log(wrapped.wrappedPrompt);
672
789
  return;
@@ -676,7 +793,7 @@ async function runModeCommand(mode, args) {
676
793
  mode,
677
794
  researchStage: stage,
678
795
  setupPath: typeof args.setup === "string" ? args.setup : undefined,
679
- workingDirectory: typeof args.cwd === "string" ? args.cwd : cwd(),
796
+ workingDirectory,
680
797
  json: args.json === true
681
798
  });
682
799
  exit(exitCode);
@@ -728,85 +845,38 @@ async function runStart(args) {
728
845
  const setupPath = typeof args.setup === "string" ? args.setup : undefined;
729
846
  const existingSetup = await loadOptionalSetup(setupPath);
730
847
  if (!existingSetup) {
731
- console.log("Long Table is not set up yet. Starting the interview flow first.");
732
- console.log("");
733
- await runInit({
734
- ...args,
735
- flow: typeof args.flow === "string" ? args.flow : "interview",
736
- "install-prompts": true
737
- });
738
- return;
739
- }
740
- const directPrompt = typeof args.prompt === "string" ? args.prompt.trim() : "";
741
- if (directPrompt) {
742
- await runAsk({
743
- ...args,
744
- prompt: directPrompt,
745
- ...(setupPath ? { setup: setupPath } : {})
746
- });
848
+ throw new Error("Long Table global setup is missing. Run `longtable init --flow interview` first.");
849
+ }
850
+ const interview = await collectProjectInterview(existingSetup, args);
851
+ const context = await createOrUpdateProjectWorkspace({
852
+ projectName: interview.projectName,
853
+ projectPath: interview.projectPath,
854
+ currentGoal: interview.currentGoal,
855
+ currentBlocker: interview.currentBlocker,
856
+ requestedPerspectives: interview.requestedPerspectives,
857
+ disagreementPreference: interview.disagreementPreference,
858
+ setup: existingSetup
859
+ });
860
+ if (args.json === true) {
861
+ console.log(JSON.stringify({
862
+ project: context.project,
863
+ session: context.session,
864
+ projectFilePath: context.projectFilePath,
865
+ sessionFilePath: context.sessionFilePath
866
+ }, null, 2));
747
867
  return;
748
868
  }
749
- const rl = createInterface({ input, output });
750
- try {
751
- const action = await promptStartAction(rl, existingSetup.profileSeed.currentBlocker ?? existingSetup.profileSeed.currentResearchTopic);
752
- if (action === "status") {
753
- rl.close();
754
- await runCodexSubcommand("status", args);
755
- return;
756
- }
757
- if (action === "roles") {
758
- rl.close();
759
- await runRoles(args);
760
- return;
761
- }
762
- const defaultPrompt = existingSetup.profileSeed.currentBlocker ??
763
- existingSetup.profileSeed.currentResearchTopic ??
764
- "";
765
- const prompt = (await rl.question(defaultPrompt
766
- ? `What are you working on today?\nPress Enter to use: ${defaultPrompt}\n> `
767
- : "What are you working on today?\n> ")).trim() || defaultPrompt;
768
- rl.close();
769
- if (!prompt) {
770
- throw new Error("A prompt is required.");
771
- }
772
- if (action === "editor") {
773
- await runModeCommand("review", {
774
- ...args,
775
- prompt,
776
- role: "editor",
777
- ...(setupPath ? { setup: setupPath } : {})
778
- });
779
- return;
780
- }
781
- if (action === "methods") {
782
- await runModeCommand("review", {
783
- ...args,
784
- prompt,
785
- role: "methods_critic",
786
- ...(setupPath ? { setup: setupPath } : {})
787
- });
788
- return;
789
- }
790
- if (action === "panel") {
791
- await runModeCommand("review", {
792
- ...args,
793
- prompt,
794
- panel: true,
795
- "show-conflicts": true,
796
- ...(setupPath ? { setup: setupPath } : {})
797
- });
798
- return;
799
- }
800
- const mode = action;
801
- await runModeCommand(mode, {
802
- ...args,
803
- prompt,
804
- ...(setupPath ? { setup: setupPath } : {})
805
- });
806
- }
807
- finally {
808
- rl.close();
809
- }
869
+ console.log(renderProjectWorkspaceSummary(context));
870
+ console.log("");
871
+ console.log(renderSectionCard("Next Step", [
872
+ `1. cd "${context.project.projectPath}"`,
873
+ "2. run `codex` in that directory",
874
+ "3. begin with your current goal in natural language",
875
+ "",
876
+ `Suggested first message: ${context.session.currentBlocker ? `"I want to work on ${context.session.currentGoal}. My current blocker is ${context.session.currentBlocker}."` : `"I want to work on ${context.session.currentGoal}."`}`,
877
+ "",
878
+ `Optional CLI path: longtable ask --cwd "${context.project.projectPath}" --prompt "${context.session.currentGoal.replaceAll("\"", "\\\"")}"`
879
+ ]));
810
880
  }
811
881
  async function runCodexSubcommand(subcommand, args) {
812
882
  const customDir = typeof args.dir === "string" ? args.dir : undefined;
@@ -0,0 +1,47 @@
1
+ import type { SetupPersistedOutput } from "@diverga/setup";
2
+ export type ProjectDisagreementPreference = "synthesis_only" | "show_on_conflict" | "always_visible";
3
+ export interface LongTableProjectRecord {
4
+ schemaVersion: 1;
5
+ product: "Long Table";
6
+ projectName: string;
7
+ projectPath: string;
8
+ createdAt: string;
9
+ globalSetupSummary: {
10
+ field: string;
11
+ careerStage: string;
12
+ experienceLevel: string;
13
+ checkpointIntensity: string;
14
+ humanAuthorshipSignal?: string;
15
+ weakestDomain?: string;
16
+ defaultPanelPreference?: ProjectDisagreementPreference;
17
+ };
18
+ }
19
+ export interface LongTableSessionRecord {
20
+ schemaVersion: 1;
21
+ id: string;
22
+ createdAt: string;
23
+ projectName: string;
24
+ projectPath: string;
25
+ currentGoal: string;
26
+ currentBlocker?: string;
27
+ requestedPerspectives: string[];
28
+ disagreementPreference: ProjectDisagreementPreference;
29
+ }
30
+ export interface LongTableProjectContext {
31
+ project: LongTableProjectRecord;
32
+ session: LongTableSessionRecord;
33
+ projectFilePath: string;
34
+ sessionFilePath: string;
35
+ metaDir: string;
36
+ }
37
+ export declare function createOrUpdateProjectWorkspace(options: {
38
+ projectName: string;
39
+ projectPath: string;
40
+ currentGoal: string;
41
+ currentBlocker?: string;
42
+ requestedPerspectives: string[];
43
+ disagreementPreference: ProjectDisagreementPreference;
44
+ setup: SetupPersistedOutput;
45
+ }): Promise<LongTableProjectContext>;
46
+ export declare function loadProjectContextFromDirectory(startPath: string): Promise<LongTableProjectContext | null>;
47
+ export declare function renderProjectWorkspaceSummary(context: LongTableProjectContext): string;
@@ -0,0 +1,248 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { existsSync } from "node:fs";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { createEmptyResearchState } from "@diverga/memory";
5
+ function nowIso() {
6
+ return new Date().toISOString();
7
+ }
8
+ function slugify(input) {
9
+ return input
10
+ .trim()
11
+ .toLowerCase()
12
+ .replace(/[^a-z0-9가-힣]+/g, "-")
13
+ .replace(/^-+|-+$/g, "")
14
+ .slice(0, 80);
15
+ }
16
+ function resolveMetaDir(projectPath) {
17
+ return join(projectPath, ".longtable");
18
+ }
19
+ function buildWorkspaceGuide(project, session) {
20
+ const lines = [
21
+ "# Long Table Workspace",
22
+ "",
23
+ "This directory is a Long Table research workspace.",
24
+ "",
25
+ "## Current project",
26
+ `- Project: ${project.projectName}`,
27
+ `- Goal right now: ${session.currentGoal}`,
28
+ ...(session.currentBlocker ? [`- Current blocker: ${session.currentBlocker}`] : []),
29
+ `- Disagreement visibility: ${session.disagreementPreference}`,
30
+ `- Requested perspectives: ${session.requestedPerspectives.length > 0 ? session.requestedPerspectives.join(", ") : "auto"}`,
31
+ "",
32
+ "## How Codex should behave here",
33
+ "- Treat this as a researcher-facing Long Table session, not a generic coding task.",
34
+ "- Ask clarifying or tension questions before closing too early.",
35
+ "- If you foreground specific roles, disclose them with `Long Table consulted: ...`.",
36
+ "- Keep one accountable synthesis, but keep disagreement visible by default when it matters.",
37
+ "- Do not expose internal tool logs or process commentary in researcher-facing answers.",
38
+ "",
39
+ "## Session files",
40
+ "- `.longtable/project.json` contains project-level metadata.",
41
+ "- `.longtable/current-session.json` contains the current session goal and blocker."
42
+ ];
43
+ return lines.join("\n");
44
+ }
45
+ function buildStartHereGuide(project, session) {
46
+ const lines = [
47
+ "# Start Here",
48
+ "",
49
+ `Project: ${project.projectName}`,
50
+ "",
51
+ "This workspace was created by Long Table for a single research project.",
52
+ "",
53
+ "## What to do now",
54
+ "1. Open Codex in this directory.",
55
+ "2. Start with your current goal, not a shell command.",
56
+ "3. Let Long Table keep disagreement visible when it matters.",
57
+ "",
58
+ "## Suggested first message",
59
+ session.currentBlocker
60
+ ? `"I want to work on ${session.currentGoal}. My current blocker is: ${session.currentBlocker}."`
61
+ : `"I want to work on ${session.currentGoal}."`,
62
+ "",
63
+ "## What Long Table already knows in this directory",
64
+ `- Current goal: ${session.currentGoal}`,
65
+ ...(session.currentBlocker ? [`- Current blocker: ${session.currentBlocker}`] : []),
66
+ `- Requested perspectives: ${session.requestedPerspectives.length > 0 ? session.requestedPerspectives.join(", ") : "auto"}`,
67
+ `- Disagreement visibility: ${session.disagreementPreference}`,
68
+ "",
69
+ "## Files",
70
+ "- `AGENTS.md` tells Codex how to behave in this workspace.",
71
+ "- `.longtable/current-session.json` stores the current session goal and blocker.",
72
+ "- `.longtable/project.json` stores the project-level record."
73
+ ];
74
+ return lines.join("\n");
75
+ }
76
+ function buildProjectAgentsMd(project, session) {
77
+ return [
78
+ "# AGENTS.md",
79
+ "",
80
+ "This directory is a Long Table research workspace.",
81
+ "",
82
+ "## Purpose",
83
+ `- Project name: ${project.projectName}`,
84
+ `- Current goal: ${session.currentGoal}`,
85
+ ...(session.currentBlocker ? [`- Current blocker: ${session.currentBlocker}`] : []),
86
+ `- Requested perspectives: ${session.requestedPerspectives.length > 0 ? session.requestedPerspectives.join(", ") : "auto"}`,
87
+ `- Disagreement visibility: ${session.disagreementPreference}`,
88
+ "",
89
+ "## Research-facing behavior",
90
+ "- Treat researcher interaction as the primary task.",
91
+ "- Begin exploratory work with clarifying or tension questions before recommending a direction.",
92
+ "- If you foreground role perspectives, disclose them with `Long Table consulted: ...`.",
93
+ "- Keep one accountable synthesis, but do not hide meaningful disagreement.",
94
+ ...(session.disagreementPreference === "always_visible"
95
+ ? ["- In this workspace, panel disagreement should be visible by default rather than hidden behind a single synthesis."]
96
+ : []),
97
+ "- Do not expose internal tool logs, file-search traces, or process commentary in the researcher-facing answer.",
98
+ "",
99
+ "## Session memory",
100
+ "- Read `.longtable/current-session.json` before giving substantial guidance.",
101
+ "- Use `.longtable/project.json` as the project-level context.",
102
+ "- Prefer the current goal and blocker over generic assumptions.",
103
+ "",
104
+ "## Scope",
105
+ "- These instructions apply to this directory and its children."
106
+ ].join("\n");
107
+ }
108
+ function buildStateSeed(project, session, setup) {
109
+ const state = createEmptyResearchState();
110
+ state.explicitState = {
111
+ field: setup.profileSeed.field,
112
+ careerStage: setup.profileSeed.careerStage,
113
+ experienceLevel: setup.profileSeed.experienceLevel,
114
+ projectName: project.projectName,
115
+ currentGoal: session.currentGoal,
116
+ disagreementPreference: session.disagreementPreference,
117
+ requestedPerspectives: session.requestedPerspectives
118
+ };
119
+ if (session.currentBlocker) {
120
+ state.explicitState.currentBlocker = session.currentBlocker;
121
+ state.openTensions.push(session.currentBlocker);
122
+ }
123
+ if (setup.profileSeed.humanAuthorshipSignal) {
124
+ state.explicitState.humanAuthorshipSignal = setup.profileSeed.humanAuthorshipSignal;
125
+ }
126
+ state.narrativeTraces.push({
127
+ id: "project-session-goal",
128
+ timestamp: nowIso(),
129
+ source: "longtable-start",
130
+ traceType: "judgment",
131
+ summary: `Current session goal: ${session.currentGoal}.`,
132
+ visibility: "explicit",
133
+ importance: "high"
134
+ });
135
+ if (session.currentBlocker) {
136
+ state.narrativeTraces.push({
137
+ id: "project-session-blocker",
138
+ timestamp: nowIso(),
139
+ source: "longtable-start",
140
+ traceType: "tension",
141
+ summary: `Current session blocker: ${session.currentBlocker}.`,
142
+ visibility: "explicit",
143
+ importance: "high"
144
+ });
145
+ }
146
+ return JSON.stringify(state, null, 2);
147
+ }
148
+ export async function createOrUpdateProjectWorkspace(options) {
149
+ const projectPath = resolve(options.projectPath);
150
+ const metaDir = resolveMetaDir(projectPath);
151
+ const sessionsDir = join(metaDir, "sessions");
152
+ const projectFilePath = join(metaDir, "project.json");
153
+ const sessionFilePath = join(metaDir, "current-session.json");
154
+ const sessionId = slugify(`${options.projectName}-${Date.now()}`);
155
+ await mkdir(projectPath, { recursive: true });
156
+ await mkdir(metaDir, { recursive: true });
157
+ await mkdir(sessionsDir, { recursive: true });
158
+ const project = existsSync(projectFilePath)
159
+ ? JSON.parse(await readFile(projectFilePath, "utf8"))
160
+ : {
161
+ schemaVersion: 1,
162
+ product: "Long Table",
163
+ projectName: options.projectName,
164
+ projectPath,
165
+ createdAt: nowIso(),
166
+ globalSetupSummary: {
167
+ field: options.setup.profileSeed.field,
168
+ careerStage: options.setup.profileSeed.careerStage,
169
+ experienceLevel: options.setup.profileSeed.experienceLevel,
170
+ checkpointIntensity: options.setup.profileSeed.preferredCheckpointIntensity,
171
+ ...(options.setup.profileSeed.humanAuthorshipSignal
172
+ ? { humanAuthorshipSignal: options.setup.profileSeed.humanAuthorshipSignal }
173
+ : {}),
174
+ ...(options.setup.profileSeed.weakestDomain
175
+ ? { weakestDomain: options.setup.profileSeed.weakestDomain }
176
+ : {}),
177
+ ...(options.setup.profileSeed.panelPreference
178
+ ? { defaultPanelPreference: options.setup.profileSeed.panelPreference }
179
+ : {})
180
+ }
181
+ };
182
+ const session = {
183
+ schemaVersion: 1,
184
+ id: sessionId,
185
+ createdAt: nowIso(),
186
+ projectName: project.projectName,
187
+ projectPath,
188
+ currentGoal: options.currentGoal,
189
+ ...(options.currentBlocker ? { currentBlocker: options.currentBlocker } : {}),
190
+ requestedPerspectives: options.requestedPerspectives,
191
+ disagreementPreference: options.disagreementPreference
192
+ };
193
+ await writeFile(projectFilePath, JSON.stringify(project, null, 2), "utf8");
194
+ await writeFile(sessionFilePath, JSON.stringify(session, null, 2), "utf8");
195
+ await writeFile(join(sessionsDir, `${sessionId}.json`), JSON.stringify(session, null, 2), "utf8");
196
+ await writeFile(join(metaDir, "state.json"), buildStateSeed(project, session, options.setup), "utf8");
197
+ await writeFile(join(projectPath, "LONGTABLE.md"), buildWorkspaceGuide(project, session), "utf8");
198
+ await writeFile(join(projectPath, "START-HERE.md"), buildStartHereGuide(project, session), "utf8");
199
+ await writeFile(join(projectPath, "AGENTS.md"), buildProjectAgentsMd(project, session), "utf8");
200
+ return {
201
+ project,
202
+ session,
203
+ projectFilePath,
204
+ sessionFilePath,
205
+ metaDir
206
+ };
207
+ }
208
+ export async function loadProjectContextFromDirectory(startPath) {
209
+ let current = resolve(startPath);
210
+ while (true) {
211
+ const metaDir = resolveMetaDir(current);
212
+ const projectFilePath = join(metaDir, "project.json");
213
+ const sessionFilePath = join(metaDir, "current-session.json");
214
+ if (existsSync(projectFilePath) && existsSync(sessionFilePath)) {
215
+ return {
216
+ project: JSON.parse(await readFile(projectFilePath, "utf8")),
217
+ session: JSON.parse(await readFile(sessionFilePath, "utf8")),
218
+ projectFilePath,
219
+ sessionFilePath,
220
+ metaDir
221
+ };
222
+ }
223
+ const parent = dirname(current);
224
+ if (parent === current) {
225
+ return null;
226
+ }
227
+ current = parent;
228
+ }
229
+ }
230
+ export function renderProjectWorkspaceSummary(context) {
231
+ return [
232
+ "┌──────────────────────────────────────────────┐",
233
+ "│ Long Table Project Workspace │",
234
+ "└──────────────────────────────────────────────┘",
235
+ `Project: ${context.project.projectName}`,
236
+ `Path: ${context.project.projectPath}`,
237
+ `Goal: ${context.session.currentGoal}`,
238
+ ...(context.session.currentBlocker ? [`Blocker: ${context.session.currentBlocker}`] : []),
239
+ `Perspectives: ${context.session.requestedPerspectives.length > 0 ? context.session.requestedPerspectives.join(", ") : "auto"}`,
240
+ `Disagreement: ${context.session.disagreementPreference}`,
241
+ "",
242
+ "Created files:",
243
+ `- ${context.projectFilePath}`,
244
+ `- ${context.sessionFilePath}`,
245
+ `- ${join(context.project.projectPath, "START-HERE.md")}`,
246
+ `- ${join(context.project.projectPath, "AGENTS.md")}`
247
+ ].join("\n");
248
+ }
@@ -33,12 +33,13 @@ function promptSpec() {
33
33
  "Ask exactly one setup question at a time.",
34
34
  "Use numbered choices when possible and include a 'None of the above' option when needed.",
35
35
  "Do not move to the next question until the researcher answers the current one.",
36
- "Quickstart covers: provider, field, career stage, experience level, current project type, checkpoint intensity, and human authorship signal.",
37
- "Interview also covers: current research topic, current blocker, preferred entry mode, weakest domain, and panel visibility preference.",
38
- "After collecting all answers, summarize the proposed setup and then output both: 1) the exact `longtable codex persist-init ... --install-prompts` command and 2) a strict JSON object with keys provider, flow, field, careerStage, experienceLevel, currentProjectType, preferredCheckpointIntensity, and optional humanAuthorshipSignal, currentResearchTopic, currentBlocker, preferredEntryMode, weakestDomain, panelPreference.",
36
+ "Quickstart covers: provider, field, career stage, experience level, checkpoint intensity, and human authorship signal.",
37
+ "Interview also covers: preferred entry mode, weakest domain, and panel visibility preference.",
38
+ "After collecting all answers, summarize the proposed setup and then output both: 1) the exact `longtable codex persist-init ... --install-prompts` command and 2) a strict JSON object with keys provider, flow, field, careerStage, experienceLevel, preferredCheckpointIntensity, and optional humanAuthorshipSignal, preferredEntryMode, weakestDomain, panelPreference.",
39
39
  "If the user prefers paste-based setup, tell them they can pipe the JSON into `longtable codex persist-init --stdin --install-prompts`.",
40
40
  "If the researcher asks you to stay inside Codex, keep the conversation in numbered form and do not prematurely close.",
41
41
  "Frame the setup like a short researcher interview, not a bare config form.",
42
+ "Do not pretend that this is the full project-start interview. The real project-start interview happens in `longtable start`.",
42
43
  "Treat any slash-command arguments as context for why setup is being done now."
43
44
  ]
44
45
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@longtable/cli",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "private": false,
5
5
  "description": "Researcher-facing Long Table CLI on top of the legacy Diverga package surface",
6
6
  "type": "module",
@@ -25,8 +25,9 @@
25
25
  "typecheck": "tsc -p tsconfig.json --noEmit"
26
26
  },
27
27
  "dependencies": {
28
+ "@diverga/memory": "0.1.0",
28
29
  "@diverga/provider-codex": "0.1.1",
29
- "@diverga/setup": "0.1.3"
30
+ "@diverga/setup": "0.1.4"
30
31
  },
31
32
  "devDependencies": {
32
33
  "@types/node": "^22.10.1",