@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 +58 -45
- package/dist/cli.js +250 -180
- package/dist/project-session.d.ts +47 -0
- package/dist/project-session.js +248 -0
- package/dist/prompt-aliases.js +4 -3
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -2,14 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
Long Table의 researcher-facing CLI입니다.
|
|
4
4
|
|
|
5
|
-
이
|
|
5
|
+
이 패키지의 핵심은 명령을 많이 외우게 하는 것이 아니라, 아래 두 단계를 명확하게 만드는 것입니다.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
1. `longtable init`
|
|
8
|
+
2. `longtable start`
|
|
9
|
+
|
|
10
|
+
중요한 점:
|
|
10
11
|
|
|
11
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
- 현재 주제
|
|
31
|
-
- 현재 막힘
|
|
32
|
-
- 선호하는 첫 진입 모드
|
|
33
|
-
- 가장 많이 도전받고 싶은 영역
|
|
34
|
-
- 역할 간 의견 충돌 가시성
|
|
28
|
+
여기서는 연구자 프로필과 기본 선호를 묻습니다.
|
|
35
29
|
|
|
36
|
-
|
|
30
|
+
- 연구 분야
|
|
31
|
+
- 연구자 역할
|
|
32
|
+
- challenge 강도
|
|
33
|
+
- 저자성/서사 관련 기본값
|
|
34
|
+
- Long Table이 보통 어디서부터 시작해야 하는지
|
|
37
35
|
|
|
38
|
-
|
|
36
|
+
## 2. Project start
|
|
39
37
|
|
|
40
38
|
```bash
|
|
41
39
|
longtable start
|
|
42
40
|
```
|
|
43
41
|
|
|
44
|
-
|
|
45
|
-
오늘 필요한 작업을 먼저 고르고, 그 다음 자연어로 상황을 말하면 됩니다.
|
|
42
|
+
여기서는 실제 작업공간을 만듭니다.
|
|
46
43
|
|
|
47
|
-
|
|
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
|
|
63
|
+
longtable init --flow interview
|
|
64
|
+
longtable start
|
|
65
|
+
cd "<project-path>"
|
|
66
|
+
codex
|
|
51
67
|
```
|
|
52
68
|
|
|
53
|
-
|
|
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 "
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
94
|
-
- 그래서 기본 사용자 경로는 `/prompts:...`가 아니라 `longtable start`와 `longtable ask`입니다.
|
|
108
|
+
기본 경로는 여전히:
|
|
95
109
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
`Diverga`는 아직 일부 하위 패키지와 runtime path에서만 남아 있는 호환성 레이어입니다.
|
|
110
|
+
- `longtable init`
|
|
111
|
+
- `longtable start`
|
|
112
|
+
- 프로젝트 디렉토리에서 `codex`
|
|
100
113
|
|
|
101
|
-
|
|
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
|
|
32
|
-
" longtable start
|
|
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,
|
|
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
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
{
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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", "
|
|
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:
|
|
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 =
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
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
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
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
|
+
}
|
package/dist/prompt-aliases.js
CHANGED
|
@@ -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,
|
|
37
|
-
"Interview also covers:
|
|
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,
|
|
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
|
+
"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.
|
|
30
|
+
"@diverga/setup": "0.1.4"
|
|
30
31
|
},
|
|
31
32
|
"devDependencies": {
|
|
32
33
|
"@types/node": "^22.10.1",
|