@longtable/cli 0.1.2 → 0.1.4
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 +65 -59
- package/dist/cli.js +283 -54
- package/dist/project-session.d.ts +47 -0
- package/dist/project-session.js +207 -0
- package/dist/prompt-aliases.js +4 -3
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,107 +1,113 @@
|
|
|
1
1
|
# @longtable/cli
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Long Table의 researcher-facing CLI입니다.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
이 패키지의 핵심은 명령을 많이 외우게 하는 것이 아니라, 아래 두 단계를 명확하게 만드는 것입니다.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
1. `longtable init`
|
|
8
|
+
2. `longtable start`
|
|
9
|
+
|
|
10
|
+
중요한 점:
|
|
8
11
|
|
|
9
|
-
|
|
12
|
+
- 이 두 명령은 **터미널에서 실행하는 셸 명령**입니다.
|
|
13
|
+
- Codex 채팅창에 `longtable start`를 입력하는 것이 아닙니다.
|
|
14
|
+
- `longtable start`로 프로젝트 작업공간을 만든 뒤, 그 디렉토리에서 `codex`를 여는 것이 기본 경로입니다.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
10
17
|
|
|
11
18
|
```bash
|
|
12
19
|
npm install -g @longtable/cli
|
|
13
20
|
```
|
|
14
21
|
|
|
15
|
-
|
|
22
|
+
## 1. Global setup
|
|
16
23
|
|
|
17
24
|
```bash
|
|
18
|
-
|
|
19
|
-
npm run build
|
|
20
|
-
node packages/longtable/dist/cli.js --help
|
|
25
|
+
longtable init --flow interview
|
|
21
26
|
```
|
|
22
27
|
|
|
23
|
-
|
|
28
|
+
여기서는 연구자 프로필과 기본 선호를 묻습니다.
|
|
24
29
|
|
|
25
|
-
|
|
30
|
+
- 연구 분야
|
|
31
|
+
- 연구자 역할
|
|
32
|
+
- challenge 강도
|
|
33
|
+
- 저자성/서사 관련 기본값
|
|
34
|
+
- Long Table이 보통 어디서부터 시작해야 하는지
|
|
35
|
+
|
|
36
|
+
## 2. Project start
|
|
26
37
|
|
|
27
38
|
```bash
|
|
28
|
-
longtable
|
|
39
|
+
longtable start
|
|
29
40
|
```
|
|
30
41
|
|
|
31
|
-
|
|
42
|
+
여기서는 실제 작업공간을 만듭니다.
|
|
43
|
+
|
|
44
|
+
- 프로젝트 이름
|
|
45
|
+
- 프로젝트 디렉토리 위치
|
|
46
|
+
- 현재 세션 목표
|
|
47
|
+
- 현재 blocker
|
|
48
|
+
- 필요한 관점
|
|
49
|
+
- disagreement 가시성
|
|
32
50
|
|
|
33
|
-
|
|
34
|
-
- `--flow interview`
|
|
51
|
+
그리고 Long Table은:
|
|
35
52
|
|
|
36
|
-
|
|
53
|
+
- 프로젝트 디렉토리 생성
|
|
54
|
+
- `.longtable/` 메모리 파일 생성
|
|
55
|
+
- 프로젝트용 `AGENTS.md` 생성
|
|
37
56
|
|
|
38
|
-
|
|
57
|
+
을 수행합니다.
|
|
58
|
+
|
|
59
|
+
## Recommended flow
|
|
39
60
|
|
|
40
61
|
```bash
|
|
41
|
-
longtable
|
|
42
|
-
longtable
|
|
43
|
-
|
|
44
|
-
|
|
62
|
+
longtable init --flow interview
|
|
63
|
+
longtable start
|
|
64
|
+
cd "<project-path>"
|
|
65
|
+
codex
|
|
45
66
|
```
|
|
46
67
|
|
|
47
|
-
|
|
68
|
+
이게 현재 가장 신뢰할 수 있는 Long Table 사용 경로입니다.
|
|
48
69
|
|
|
49
|
-
|
|
50
|
-
- `/prompts:longtable-init`
|
|
51
|
-
- `/prompts:longtable-explore`
|
|
52
|
-
- `/prompts:longtable-review`
|
|
53
|
-
- `/prompts:longtable-critique`
|
|
54
|
-
- `/prompts:longtable-draft`
|
|
55
|
-
- `/prompts:longtable-commit`
|
|
56
|
-
- `/prompts:longtable-status`
|
|
70
|
+
## After the project starts
|
|
57
71
|
|
|
58
|
-
|
|
72
|
+
프로젝트 디렉토리 안에서는 보통 그냥 `codex`를 열고 자연어로 연구를 시작하면 됩니다.
|
|
59
73
|
|
|
60
|
-
|
|
61
|
-
longtable ask --prompt "Help me open this research problem carefully."
|
|
62
|
-
longtable init
|
|
63
|
-
longtable show --json
|
|
64
|
-
longtable install --json
|
|
65
|
-
longtable explore --prompt "Help me stay exploratory."
|
|
66
|
-
longtable review --prompt "Review this claim critically."
|
|
67
|
-
longtable commit --prompt "Help me make this decision carefully."
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
## Codex overlay
|
|
74
|
+
CLI를 계속 쓰고 싶다면 아래 명령은 보조 경로입니다.
|
|
71
75
|
|
|
72
|
-
|
|
76
|
+
## Advanced commands
|
|
73
77
|
|
|
74
78
|
```bash
|
|
75
|
-
longtable
|
|
79
|
+
longtable roles
|
|
80
|
+
longtable ask --cwd "<project-path>" --prompt "연구 질문을 어디서부터 좁혀야 할지 모르겠어."
|
|
81
|
+
longtable review --cwd "<project-path>" --prompt "방법론적으로 어디가 취약한지 말해줘." --role methods_critic
|
|
76
82
|
```
|
|
77
83
|
|
|
78
|
-
|
|
84
|
+
## Roles
|
|
79
85
|
|
|
80
86
|
```bash
|
|
81
|
-
longtable
|
|
87
|
+
longtable roles
|
|
82
88
|
```
|
|
83
89
|
|
|
84
|
-
|
|
90
|
+
예:
|
|
85
91
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
92
|
+
- `editor`
|
|
93
|
+
- `reviewer`
|
|
94
|
+
- `methods_critic`
|
|
95
|
+
- `voice_keeper`
|
|
89
96
|
|
|
90
|
-
|
|
97
|
+
## Codex prompt-file integration
|
|
91
98
|
|
|
92
99
|
```bash
|
|
100
|
+
longtable codex install-prompts
|
|
93
101
|
longtable codex status
|
|
94
102
|
```
|
|
95
103
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
```bash
|
|
99
|
-
longtable codex remove-prompts
|
|
100
|
-
```
|
|
104
|
+
이 경로는 현재 실험적입니다.
|
|
105
|
+
사용자에게 약속하는 주 경로는 아닙니다.
|
|
101
106
|
|
|
102
|
-
|
|
107
|
+
기본 경로는 여전히:
|
|
103
108
|
|
|
104
|
-
|
|
105
|
-
|
|
109
|
+
- `longtable init`
|
|
110
|
+
- `longtable start`
|
|
111
|
+
- 프로젝트 디렉토리에서 `codex`
|
|
106
112
|
|
|
107
|
-
|
|
113
|
+
입니다.
|
package/dist/cli.js
CHANGED
|
@@ -3,10 +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";
|
|
11
|
+
import { PERSONA_DEFINITIONS } from "./personas.js";
|
|
12
|
+
import { createOrUpdateProjectWorkspace, loadProjectContextFromDirectory, renderProjectWorkspaceSummary } from "./project-session.js";
|
|
10
13
|
const VALID_MODES = new Set([
|
|
11
14
|
"explore",
|
|
12
15
|
"review",
|
|
@@ -27,7 +30,12 @@ const VALID_STAGES = new Set([
|
|
|
27
30
|
function usage() {
|
|
28
31
|
return [
|
|
29
32
|
"Usage:",
|
|
30
|
-
" longtable
|
|
33
|
+
" Run `longtable ...` in your terminal, not inside the Codex chat box.",
|
|
34
|
+
" After `longtable start`, move into the created project directory and open `codex` there.",
|
|
35
|
+
"",
|
|
36
|
+
" 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]",
|
|
37
|
+
" longtable start [--path <dir>] [--name <project>] [--goal <text>] [--blocker <text>] [--perspectives <role[,role]>] [--disagreement synthesis_only|show_on_conflict|always_visible] [--setup <path>] [--json]",
|
|
38
|
+
" longtable roles [--json]",
|
|
31
39
|
" longtable show [--json] [--path <file>]",
|
|
32
40
|
" longtable install [--json] [--path <file>] [--runtime-path <file>]",
|
|
33
41
|
" longtable ask [--prompt <text>] [--print] [--json] [--setup <path>] [--cwd <path>]",
|
|
@@ -39,9 +47,11 @@ function usage() {
|
|
|
39
47
|
"",
|
|
40
48
|
"Examples:",
|
|
41
49
|
" longtable init --flow interview --install-prompts",
|
|
50
|
+
" longtable start",
|
|
51
|
+
" longtable start --path ~/Research/My-Project --name \"AI Adoption Meta-Analysis\" --goal \"Narrow the review question\"",
|
|
52
|
+
" cd \"<project-path>\" && codex",
|
|
53
|
+
" longtable roles",
|
|
42
54
|
" longtable ask --prompt \"연구를 시작하고 싶어. 지금 어디서부터 좁혀야 할지 모르겠어.\"",
|
|
43
|
-
" longtable review --prompt \"Review this claim critically.\" --panel --show-conflicts",
|
|
44
|
-
" longtable review --role editor --prompt \"BJET 편집자 관점에서 봐줘.\"",
|
|
45
55
|
" printf '{\"provider\":\"codex\",...}' | longtable codex persist-init --stdin --install-prompts",
|
|
46
56
|
" longtable codex install-prompts"
|
|
47
57
|
].join("\n");
|
|
@@ -51,7 +61,7 @@ function parseArgs(argv) {
|
|
|
51
61
|
const values = {};
|
|
52
62
|
let subcommand = maybeSubcommand;
|
|
53
63
|
const modeCommand = command && VALID_MODES.has(command);
|
|
54
|
-
const directCommand = command && ["init", "show", "install", "codex", "ask"].includes(command);
|
|
64
|
+
const directCommand = command && ["init", "start", "roles", "show", "install", "codex", "ask"].includes(command);
|
|
55
65
|
let startIndex = 1;
|
|
56
66
|
if (modeCommand) {
|
|
57
67
|
subcommand = undefined;
|
|
@@ -110,7 +120,7 @@ function buildSetupFlowChoices() {
|
|
|
110
120
|
function renderSetupHeader(flow) {
|
|
111
121
|
const title = flow === "interview" ? "Long Table Setup Interview" : "Long Table Quickstart";
|
|
112
122
|
const subtitle = flow === "interview"
|
|
113
|
-
? "We will ask about your research persona,
|
|
123
|
+
? "We will ask about your research persona, challenge preferences, and authorship defaults."
|
|
114
124
|
: "We will capture the minimum profile needed to start using Long Table.";
|
|
115
125
|
return [
|
|
116
126
|
"┌──────────────────────────────────────────────┐",
|
|
@@ -126,9 +136,6 @@ function questionSection(questionId) {
|
|
|
126
136
|
if (questionId === "field" || questionId === "careerStage" || questionId === "experienceLevel") {
|
|
127
137
|
return "Researcher profile";
|
|
128
138
|
}
|
|
129
|
-
if (questionId === "currentProjectType" || questionId === "currentResearchTopic" || questionId === "currentBlocker") {
|
|
130
|
-
return "Current work";
|
|
131
|
-
}
|
|
132
139
|
if (questionId === "preferredCheckpointIntensity" || questionId === "preferredEntryMode") {
|
|
133
140
|
return "Interaction style";
|
|
134
141
|
}
|
|
@@ -137,6 +144,9 @@ function questionSection(questionId) {
|
|
|
137
144
|
}
|
|
138
145
|
return "Authorship and voice";
|
|
139
146
|
}
|
|
147
|
+
function formatModeLabel(mode) {
|
|
148
|
+
return `${mode[0].toUpperCase()}${mode.slice(1)}`;
|
|
149
|
+
}
|
|
140
150
|
function moveCursorUp(lines) {
|
|
141
151
|
return lines > 0 ? `\u001B[${lines}A` : "";
|
|
142
152
|
}
|
|
@@ -252,8 +262,84 @@ async function promptChoiceWithArrows(rl, prompt, choices) {
|
|
|
252
262
|
async function promptChoice(rl, prompt, choices) {
|
|
253
263
|
return promptChoiceWithArrows(rl, prompt, choices);
|
|
254
264
|
}
|
|
265
|
+
async function promptMultiChoice(rl, prompt, choices) {
|
|
266
|
+
const stream = input;
|
|
267
|
+
if (!stream.isTTY || !output.isTTY) {
|
|
268
|
+
const answer = await rl.question(`${prompt}\nType comma-separated ids or leave blank for auto.\n> `);
|
|
269
|
+
return answer
|
|
270
|
+
.split(",")
|
|
271
|
+
.map((part) => part.trim())
|
|
272
|
+
.filter(Boolean);
|
|
273
|
+
}
|
|
274
|
+
const previousRawMode = stream.isRaw;
|
|
275
|
+
let selectedIndex = 0;
|
|
276
|
+
const selected = new Set();
|
|
277
|
+
const renderedLineCount = choices.length + 2;
|
|
278
|
+
return await new Promise((resolvePromise, reject) => {
|
|
279
|
+
function draw(first = false) {
|
|
280
|
+
if (!first) {
|
|
281
|
+
output.write(moveCursorUp(renderedLineCount));
|
|
282
|
+
}
|
|
283
|
+
const lines = [prompt, "Use ↑/↓, Space to toggle, and Enter to confirm."];
|
|
284
|
+
for (let index = 0; index < choices.length; index += 1) {
|
|
285
|
+
const choice = choices[index];
|
|
286
|
+
const pointer = index === selectedIndex ? ">" : " ";
|
|
287
|
+
const marker = selected.has(choice.id) ? "[x]" : "[ ]";
|
|
288
|
+
lines.push(`${pointer} ${marker} ${choice.label} - ${choice.description}`);
|
|
289
|
+
}
|
|
290
|
+
for (const line of lines) {
|
|
291
|
+
output.write(clearLine());
|
|
292
|
+
output.write(`${line}\n`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
function cleanup() {
|
|
296
|
+
stream.off("keypress", onKeypress);
|
|
297
|
+
if (stream.isTTY) {
|
|
298
|
+
stream.setRawMode(previousRawMode ?? false);
|
|
299
|
+
}
|
|
300
|
+
output.write("\u001B[?25h");
|
|
301
|
+
}
|
|
302
|
+
function onKeypress(_, key) {
|
|
303
|
+
if (key.ctrl && key.name === "c") {
|
|
304
|
+
cleanup();
|
|
305
|
+
reject(new Error("Setup cancelled."));
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
if (key.name === "up") {
|
|
309
|
+
selectedIndex = selectedIndex === 0 ? choices.length - 1 : selectedIndex - 1;
|
|
310
|
+
draw();
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
if (key.name === "down") {
|
|
314
|
+
selectedIndex = selectedIndex === choices.length - 1 ? 0 : selectedIndex + 1;
|
|
315
|
+
draw();
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
if (key.name === "space") {
|
|
319
|
+
const id = choices[selectedIndex].id;
|
|
320
|
+
if (selected.has(id)) {
|
|
321
|
+
selected.delete(id);
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
selected.add(id);
|
|
325
|
+
}
|
|
326
|
+
draw();
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
if (key.name === "return") {
|
|
330
|
+
cleanup();
|
|
331
|
+
resolvePromise([...selected]);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
emitKeypressEvents(stream);
|
|
335
|
+
stream.setRawMode(true);
|
|
336
|
+
output.write("\u001B[?25l");
|
|
337
|
+
draw(true);
|
|
338
|
+
stream.on("keypress", onKeypress);
|
|
339
|
+
});
|
|
340
|
+
}
|
|
255
341
|
function hasCompleteFlagInput(args) {
|
|
256
|
-
const required = ["provider", "field", "career-stage", "experience", "
|
|
342
|
+
const required = ["provider", "field", "career-stage", "experience", "checkpoint"];
|
|
257
343
|
return required.every((key) => typeof args[key] === "string" && String(args[key]).trim().length > 0);
|
|
258
344
|
}
|
|
259
345
|
function resolveSetupFlow(args) {
|
|
@@ -264,13 +350,13 @@ function toSetupAnswers(args) {
|
|
|
264
350
|
field: String(args.field),
|
|
265
351
|
careerStage: String(args["career-stage"]),
|
|
266
352
|
experienceLevel: String(args.experience),
|
|
267
|
-
currentProjectType:
|
|
353
|
+
currentProjectType: typeof args["project-type"] === "string" && args["project-type"].trim().length > 0
|
|
354
|
+
? String(args["project-type"])
|
|
355
|
+
: "unspecified research task",
|
|
268
356
|
preferredCheckpointIntensity: String(args.checkpoint),
|
|
269
357
|
humanAuthorshipSignal: typeof args["authorship-signal"] === "string" && args["authorship-signal"].trim().length > 0
|
|
270
358
|
? args["authorship-signal"].trim()
|
|
271
359
|
: undefined,
|
|
272
|
-
currentResearchTopic: typeof args.topic === "string" && args.topic.trim().length > 0 ? args.topic.trim() : undefined,
|
|
273
|
-
currentBlocker: typeof args.blocker === "string" && args.blocker.trim().length > 0 ? args.blocker.trim() : undefined,
|
|
274
360
|
preferredEntryMode: typeof args["entry-mode"] === "string" && VALID_MODES.has(String(args["entry-mode"]))
|
|
275
361
|
? String(args["entry-mode"])
|
|
276
362
|
: undefined,
|
|
@@ -282,15 +368,18 @@ function toSetupAnswers(args) {
|
|
|
282
368
|
: undefined
|
|
283
369
|
};
|
|
284
370
|
}
|
|
285
|
-
async function collectInteractiveAnswers() {
|
|
371
|
+
async function collectInteractiveAnswers(initialFlow) {
|
|
286
372
|
const rl = createInterface({ input, output });
|
|
287
373
|
try {
|
|
288
|
-
const flow =
|
|
374
|
+
const flow = initialFlow ??
|
|
375
|
+
(await promptChoice(rl, "How would you like to set up Long Table?", buildSetupFlowChoices()));
|
|
289
376
|
console.log("");
|
|
290
377
|
console.log(renderSetupHeader(flow));
|
|
291
378
|
console.log("");
|
|
292
379
|
const provider = await promptChoice(rl, "Which provider do you want to configure?", buildProviderChoices());
|
|
293
|
-
const answers = {
|
|
380
|
+
const answers = {
|
|
381
|
+
currentProjectType: "unspecified research task"
|
|
382
|
+
};
|
|
294
383
|
const questions = buildQuickSetupFlow(flow);
|
|
295
384
|
for (let index = 0; index < questions.length; index += 1) {
|
|
296
385
|
const question = questions[index];
|
|
@@ -311,18 +400,12 @@ async function collectInteractiveAnswers() {
|
|
|
311
400
|
answers.careerStage = value;
|
|
312
401
|
if (question.id === "experienceLevel")
|
|
313
402
|
answers.experienceLevel = value;
|
|
314
|
-
if (question.id === "currentProjectType")
|
|
315
|
-
answers.currentProjectType = value;
|
|
316
403
|
if (question.id === "preferredCheckpointIntensity") {
|
|
317
404
|
answers.preferredCheckpointIntensity = value;
|
|
318
405
|
}
|
|
319
406
|
if (question.id === "humanAuthorshipSignal" && value !== "other") {
|
|
320
407
|
answers.humanAuthorshipSignal = value;
|
|
321
408
|
}
|
|
322
|
-
if (question.id === "currentResearchTopic")
|
|
323
|
-
answers.currentResearchTopic = value;
|
|
324
|
-
if (question.id === "currentBlocker")
|
|
325
|
-
answers.currentBlocker = value;
|
|
326
409
|
if (question.id === "preferredEntryMode")
|
|
327
410
|
answers.preferredEntryMode = value;
|
|
328
411
|
if (question.id === "weakestDomain")
|
|
@@ -340,6 +423,84 @@ async function collectInteractiveAnswers() {
|
|
|
340
423
|
rl.close();
|
|
341
424
|
}
|
|
342
425
|
}
|
|
426
|
+
function perspectiveChoices() {
|
|
427
|
+
return PERSONA_DEFINITIONS.map((persona) => ({
|
|
428
|
+
id: persona.key,
|
|
429
|
+
label: persona.label,
|
|
430
|
+
description: persona.shortDescription
|
|
431
|
+
}));
|
|
432
|
+
}
|
|
433
|
+
function normalizePerspectiveList(value) {
|
|
434
|
+
if (!value?.trim()) {
|
|
435
|
+
return [];
|
|
436
|
+
}
|
|
437
|
+
return value
|
|
438
|
+
.split(",")
|
|
439
|
+
.map((part) => part.trim())
|
|
440
|
+
.filter(Boolean);
|
|
441
|
+
}
|
|
442
|
+
async function collectProjectInterview(setup, args) {
|
|
443
|
+
const needsInteractivePrompts = !(typeof args.name === "string" && args.name.trim()) ||
|
|
444
|
+
!(typeof args.path === "string" && args.path.trim()) ||
|
|
445
|
+
!(typeof args.goal === "string" && args.goal.trim()) ||
|
|
446
|
+
typeof args.blocker !== "string" ||
|
|
447
|
+
normalizePerspectiveList(typeof args.perspectives === "string" ? args.perspectives : undefined).length === 0 ||
|
|
448
|
+
!(typeof args.disagreement === "string" && args.disagreement.trim());
|
|
449
|
+
const rl = createInterface({ input, output });
|
|
450
|
+
try {
|
|
451
|
+
if (needsInteractivePrompts) {
|
|
452
|
+
console.log("");
|
|
453
|
+
console.log("┌──────────────────────────────────────────────┐");
|
|
454
|
+
console.log("│ Long Table Project Start │");
|
|
455
|
+
console.log("└──────────────────────────────────────────────┘");
|
|
456
|
+
console.log("We will create a project workspace and a session memory seed for today's work.");
|
|
457
|
+
console.log("");
|
|
458
|
+
}
|
|
459
|
+
const projectName = (typeof args.name === "string" && args.name.trim()) ||
|
|
460
|
+
(await promptText(rl, renderQuestionHeader(1, 6, "Project interview", "What should this project be called?"), true));
|
|
461
|
+
const suggestedPath = typeof args.path === "string" && args.path.trim()
|
|
462
|
+
? args.path.trim()
|
|
463
|
+
: resolve(projectName.replace(/\s+/g, "-"));
|
|
464
|
+
const projectPath = (typeof args.path === "string" && args.path.trim()) ||
|
|
465
|
+
(await promptText(rl, renderQuestionHeader(2, 6, "Project interview", `Where should this project live on your machine?\nSuggested path: ${suggestedPath}`), true));
|
|
466
|
+
const currentGoal = (typeof args.goal === "string" && args.goal.trim()) ||
|
|
467
|
+
(await promptText(rl, renderQuestionHeader(3, 6, "Current session", "What are you trying to accomplish in this session?"), true));
|
|
468
|
+
const currentBlocker = (typeof args.blocker === "string" && args.blocker.trim()) ||
|
|
469
|
+
(await promptText(rl, renderQuestionHeader(4, 6, "Current session", "What is the main blocker or uncertainty right now?"), false));
|
|
470
|
+
const requestedPerspectives = normalizePerspectiveList(typeof args.perspectives === "string" ? args.perspectives : undefined).length > 0
|
|
471
|
+
? normalizePerspectiveList(typeof args.perspectives === "string" ? args.perspectives : undefined)
|
|
472
|
+
: await promptMultiChoice(rl, renderQuestionHeader(5, 6, "Perspectives", "Which perspectives do you already know you want at the table? Leave everything unchecked for auto."), perspectiveChoices());
|
|
473
|
+
const disagreementPreference = (typeof args.disagreement === "string" && args.disagreement.trim()) ||
|
|
474
|
+
(await promptChoice(rl, renderQuestionHeader(6, 6, "Disagreement", "How visible should disagreement between perspectives be in this project by default?"), [
|
|
475
|
+
{
|
|
476
|
+
id: "synthesis_only",
|
|
477
|
+
label: "Synthesis only",
|
|
478
|
+
description: "Show one Long Table answer unless I ask for more."
|
|
479
|
+
},
|
|
480
|
+
{
|
|
481
|
+
id: "show_on_conflict",
|
|
482
|
+
label: "Show on conflict",
|
|
483
|
+
description: "Surface disagreement when the perspectives materially diverge."
|
|
484
|
+
},
|
|
485
|
+
{
|
|
486
|
+
id: "always_visible",
|
|
487
|
+
label: "Always visible",
|
|
488
|
+
description: "Keep panel opinions visible by default."
|
|
489
|
+
}
|
|
490
|
+
]));
|
|
491
|
+
return {
|
|
492
|
+
projectName: projectName.trim(),
|
|
493
|
+
projectPath: projectPath.trim(),
|
|
494
|
+
currentGoal: currentGoal.trim(),
|
|
495
|
+
...(currentBlocker?.trim() ? { currentBlocker: currentBlocker.trim() } : {}),
|
|
496
|
+
requestedPerspectives,
|
|
497
|
+
disagreementPreference: disagreementPreference
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
finally {
|
|
501
|
+
rl.close();
|
|
502
|
+
}
|
|
503
|
+
}
|
|
343
504
|
function normalizePersistAnswers(raw) {
|
|
344
505
|
return {
|
|
345
506
|
flow: raw.flow === "interview" ? "interview" : "quickstart",
|
|
@@ -348,17 +509,11 @@ function normalizePersistAnswers(raw) {
|
|
|
348
509
|
field: raw.field,
|
|
349
510
|
careerStage: raw.careerStage,
|
|
350
511
|
experienceLevel: raw.experienceLevel,
|
|
351
|
-
currentProjectType:
|
|
512
|
+
currentProjectType: "unspecified research task",
|
|
352
513
|
preferredCheckpointIntensity: raw.preferredCheckpointIntensity,
|
|
353
514
|
...(raw.humanAuthorshipSignal?.trim()
|
|
354
515
|
? { humanAuthorshipSignal: raw.humanAuthorshipSignal.trim() }
|
|
355
516
|
: {}),
|
|
356
|
-
...(raw.currentResearchTopic?.trim()
|
|
357
|
-
? { currentResearchTopic: raw.currentResearchTopic.trim() }
|
|
358
|
-
: {}),
|
|
359
|
-
...(raw.currentBlocker?.trim()
|
|
360
|
-
? { currentBlocker: raw.currentBlocker.trim() }
|
|
361
|
-
: {}),
|
|
362
517
|
...(raw.preferredEntryMode
|
|
363
518
|
? { preferredEntryMode: raw.preferredEntryMode }
|
|
364
519
|
: {}),
|
|
@@ -408,7 +563,7 @@ async function runInit(args) {
|
|
|
408
563
|
provider: String(args.provider) === "claude" ? "claude" : "codex",
|
|
409
564
|
answers: toSetupAnswers(args)
|
|
410
565
|
}
|
|
411
|
-
: await collectInteractiveAnswers();
|
|
566
|
+
: await collectInteractiveAnswers(typeof args.flow === "string" ? resolveSetupFlow(args) : undefined);
|
|
412
567
|
const outputValue = createPersistedSetupOutput(answers, provider, flow);
|
|
413
568
|
const result = await saveSetupAndRuntimeConfig(outputValue, {
|
|
414
569
|
setupPath: customPath,
|
|
@@ -429,24 +584,19 @@ async function runInit(args) {
|
|
|
429
584
|
}
|
|
430
585
|
if (installedPrompts.length > 0) {
|
|
431
586
|
console.log("");
|
|
432
|
-
console.log("Installed Codex prompt
|
|
587
|
+
console.log("Installed Codex prompt files:");
|
|
433
588
|
for (const prompt of installedPrompts) {
|
|
434
589
|
console.log(`- /prompts:${prompt.name}`);
|
|
435
590
|
}
|
|
591
|
+
console.log(" Note: whether Codex exposes these as slash commands depends on your Codex build.");
|
|
436
592
|
}
|
|
437
593
|
if (provider === "codex") {
|
|
438
594
|
console.log("");
|
|
439
595
|
console.log("Next step:");
|
|
440
|
-
console.log("-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
else if (answers.currentResearchTopic) {
|
|
445
|
-
console.log(`- Suggested first message: Help me open the problem around ${answers.currentResearchTopic}.`);
|
|
446
|
-
}
|
|
447
|
-
else {
|
|
448
|
-
console.log("- Suggested first message: I want to start my research and need help opening the problem.");
|
|
449
|
-
}
|
|
596
|
+
console.log("- Start here: `longtable start`.");
|
|
597
|
+
console.log("- If you want a direct natural-language entry: `longtable ask --prompt \"...\"`.");
|
|
598
|
+
console.log("- Codex prompt files are available as an experimental integration, not the primary path.");
|
|
599
|
+
console.log("- Suggested next action: create a project workspace and let Long Table interview the current session.");
|
|
450
600
|
}
|
|
451
601
|
}
|
|
452
602
|
async function runShow(args) {
|
|
@@ -492,24 +642,19 @@ async function runCodexPersistInit(args) {
|
|
|
492
642
|
console.log(renderInstallSummary(result));
|
|
493
643
|
if (installedPrompts.length > 0) {
|
|
494
644
|
console.log("");
|
|
495
|
-
console.log("Installed Codex prompt
|
|
645
|
+
console.log("Installed Codex prompt files:");
|
|
496
646
|
for (const prompt of installedPrompts) {
|
|
497
647
|
console.log(`- /prompts:${prompt.name}`);
|
|
498
648
|
}
|
|
649
|
+
console.log(" Note: whether Codex exposes these as slash commands depends on your Codex build.");
|
|
499
650
|
}
|
|
500
651
|
if (provider === "codex") {
|
|
501
652
|
console.log("");
|
|
502
653
|
console.log("Next step:");
|
|
503
|
-
console.log("- Start
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
else if (answers.currentResearchTopic) {
|
|
508
|
-
console.log(`- Suggested first message: Help me think through ${answers.currentResearchTopic}.`);
|
|
509
|
-
}
|
|
510
|
-
else {
|
|
511
|
-
console.log("- Suggested first message: I want to start my research and I am not sure where to open the problem.");
|
|
512
|
-
}
|
|
654
|
+
console.log("- Start here: `longtable start`.");
|
|
655
|
+
console.log("- If you want a direct natural-language entry: `longtable ask --prompt \"...\"`.");
|
|
656
|
+
console.log("- Codex prompt files are available as an experimental integration, not the primary path.");
|
|
657
|
+
console.log("- Suggested next action: create a project workspace and let Long Table interview the current session.");
|
|
513
658
|
}
|
|
514
659
|
}
|
|
515
660
|
async function resolvePrompt(prompt) {
|
|
@@ -578,7 +723,25 @@ async function loadOptionalSetup(path) {
|
|
|
578
723
|
return null;
|
|
579
724
|
}
|
|
580
725
|
}
|
|
726
|
+
async function buildProjectAwarePrompt(prompt, workingDirectory) {
|
|
727
|
+
const context = await loadProjectContextFromDirectory(workingDirectory);
|
|
728
|
+
if (!context) {
|
|
729
|
+
return { prompt, projectContextFound: false };
|
|
730
|
+
}
|
|
731
|
+
const lines = [
|
|
732
|
+
"Long Table project context",
|
|
733
|
+
`project: ${context.project.projectName}`,
|
|
734
|
+
`current session goal: ${context.session.currentGoal}`,
|
|
735
|
+
...(context.session.currentBlocker ? [`current blocker: ${context.session.currentBlocker}`] : []),
|
|
736
|
+
`requested perspectives: ${context.session.requestedPerspectives.length > 0 ? context.session.requestedPerspectives.join(", ") : "auto"}`,
|
|
737
|
+
`disagreement preference: ${context.session.disagreementPreference}`,
|
|
738
|
+
"",
|
|
739
|
+
prompt
|
|
740
|
+
];
|
|
741
|
+
return { prompt: lines.join("\n"), projectContextFound: true };
|
|
742
|
+
}
|
|
581
743
|
async function runModeCommand(mode, args) {
|
|
744
|
+
const workingDirectory = typeof args.cwd === "string" ? args.cwd : cwd();
|
|
582
745
|
const prompt = await resolvePrompt(typeof args.prompt === "string" ? args.prompt : undefined);
|
|
583
746
|
if (!prompt) {
|
|
584
747
|
throw new Error("A prompt is required.");
|
|
@@ -588,13 +751,14 @@ async function runModeCommand(mode, args) {
|
|
|
588
751
|
throw new Error(`Invalid stage: ${stage}`);
|
|
589
752
|
}
|
|
590
753
|
const setup = await loadOptionalSetup(typeof args.setup === "string" ? args.setup : undefined);
|
|
754
|
+
const projectAware = await buildProjectAwarePrompt(prompt, workingDirectory);
|
|
591
755
|
const panelPreference = setup?.profileSeed.panelPreference;
|
|
592
756
|
const panelRequested = args.panel === true ||
|
|
593
757
|
panelPreference === "always_visible" ||
|
|
594
758
|
(panelPreference === "show_on_conflict" && args["show-conflicts"] === true);
|
|
595
759
|
const { guidedPrompt } = buildPersonaGuidance({
|
|
596
760
|
mode,
|
|
597
|
-
prompt,
|
|
761
|
+
prompt: projectAware.prompt,
|
|
598
762
|
roleFlag: typeof args.role === "string" ? args.role : undefined,
|
|
599
763
|
panel: panelRequested,
|
|
600
764
|
showConflicts: args["show-conflicts"] === true,
|
|
@@ -606,7 +770,7 @@ async function runModeCommand(mode, args) {
|
|
|
606
770
|
mode,
|
|
607
771
|
researchStage: stage,
|
|
608
772
|
setupPath: typeof args.setup === "string" ? args.setup : undefined,
|
|
609
|
-
workingDirectory
|
|
773
|
+
workingDirectory
|
|
610
774
|
});
|
|
611
775
|
console.log(wrapped.wrappedPrompt);
|
|
612
776
|
return;
|
|
@@ -616,7 +780,7 @@ async function runModeCommand(mode, args) {
|
|
|
616
780
|
mode,
|
|
617
781
|
researchStage: stage,
|
|
618
782
|
setupPath: typeof args.setup === "string" ? args.setup : undefined,
|
|
619
|
-
workingDirectory
|
|
783
|
+
workingDirectory,
|
|
620
784
|
json: args.json === true
|
|
621
785
|
});
|
|
622
786
|
exit(exitCode);
|
|
@@ -642,11 +806,67 @@ async function runAsk(args) {
|
|
|
642
806
|
}
|
|
643
807
|
await runModeCommand(mode, delegatedArgs);
|
|
644
808
|
}
|
|
809
|
+
async function runRoles(args) {
|
|
810
|
+
const payload = PERSONA_DEFINITIONS.map((persona) => ({
|
|
811
|
+
key: persona.key,
|
|
812
|
+
label: persona.label,
|
|
813
|
+
description: persona.shortDescription,
|
|
814
|
+
triggerMode: persona.triggerMode,
|
|
815
|
+
exampleTriggers: persona.synonyms.slice(0, 4)
|
|
816
|
+
}));
|
|
817
|
+
if (args.json === true) {
|
|
818
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
console.log("Long Table roles");
|
|
822
|
+
console.log("These are perspectives Long Table can consult when relevant.");
|
|
823
|
+
console.log("");
|
|
824
|
+
for (const persona of payload) {
|
|
825
|
+
console.log(`- ${persona.label} (${persona.key})`);
|
|
826
|
+
console.log(` ${persona.description}`);
|
|
827
|
+
console.log(` Trigger: ${persona.triggerMode === "auto-callable" ? "auto-callable when your language strongly implies it" : "explicit request only"}`);
|
|
828
|
+
console.log(` Examples: ${persona.exampleTriggers.join(", ")}`);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
async function runStart(args) {
|
|
832
|
+
const setupPath = typeof args.setup === "string" ? args.setup : undefined;
|
|
833
|
+
const existingSetup = await loadOptionalSetup(setupPath);
|
|
834
|
+
if (!existingSetup) {
|
|
835
|
+
throw new Error("Long Table global setup is missing. Run `longtable init --flow interview` first.");
|
|
836
|
+
}
|
|
837
|
+
const interview = await collectProjectInterview(existingSetup, args);
|
|
838
|
+
const context = await createOrUpdateProjectWorkspace({
|
|
839
|
+
projectName: interview.projectName,
|
|
840
|
+
projectPath: interview.projectPath,
|
|
841
|
+
currentGoal: interview.currentGoal,
|
|
842
|
+
currentBlocker: interview.currentBlocker,
|
|
843
|
+
requestedPerspectives: interview.requestedPerspectives,
|
|
844
|
+
disagreementPreference: interview.disagreementPreference,
|
|
845
|
+
setup: existingSetup
|
|
846
|
+
});
|
|
847
|
+
if (args.json === true) {
|
|
848
|
+
console.log(JSON.stringify({
|
|
849
|
+
project: context.project,
|
|
850
|
+
session: context.session,
|
|
851
|
+
projectFilePath: context.projectFilePath,
|
|
852
|
+
sessionFilePath: context.sessionFilePath
|
|
853
|
+
}, null, 2));
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
console.log(renderProjectWorkspaceSummary(context));
|
|
857
|
+
console.log("");
|
|
858
|
+
console.log("Next step:");
|
|
859
|
+
console.log(`- cd "${context.project.projectPath}"`);
|
|
860
|
+
console.log("- start Codex in that directory if you want a plain Codex session that inherits Long Table project instructions");
|
|
861
|
+
console.log(`- or run: longtable ask --cwd "${context.project.projectPath}" --prompt "${context.session.currentGoal.replaceAll("\"", "\\\"")}"`);
|
|
862
|
+
console.log("- the workspace now includes `.longtable/` memory files and a project-scoped `AGENTS.md`");
|
|
863
|
+
}
|
|
645
864
|
async function runCodexSubcommand(subcommand, args) {
|
|
646
865
|
const customDir = typeof args.dir === "string" ? args.dir : undefined;
|
|
647
866
|
if (subcommand === "install-prompts") {
|
|
648
867
|
const installed = await installCodexPromptAliases(customDir);
|
|
649
868
|
console.log(`Installed ${installed.length} Long Table prompt aliases in ${resolveCodexPromptsDir(customDir)}`);
|
|
869
|
+
console.log("Note: prompt-file discovery depends on the Codex build. Treat this as an experimental integration.");
|
|
650
870
|
for (const prompt of installed) {
|
|
651
871
|
console.log(`- /prompts:${prompt.name}`);
|
|
652
872
|
}
|
|
@@ -681,6 +901,7 @@ async function runCodexSubcommand(subcommand, args) {
|
|
|
681
901
|
console.log(`- setup: ${status.setupExists ? "present" : "missing"} (${setupPath})`);
|
|
682
902
|
console.log(`- codex runtime artifact: ${status.runtimeExists ? "present" : "missing"} (${runtimePath})`);
|
|
683
903
|
console.log(`- prompt aliases dir: ${status.promptsDir}`);
|
|
904
|
+
console.log("- prompt-file integration: experimental (your Codex build may not expose these as slash commands)");
|
|
684
905
|
if (aliases.length === 0) {
|
|
685
906
|
console.log("- prompt aliases: none");
|
|
686
907
|
}
|
|
@@ -705,6 +926,14 @@ async function main() {
|
|
|
705
926
|
await runInit(values);
|
|
706
927
|
return;
|
|
707
928
|
}
|
|
929
|
+
if (command === "start") {
|
|
930
|
+
await runStart(values);
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
if (command === "roles") {
|
|
934
|
+
await runRoles(values);
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
708
937
|
if (command === "show") {
|
|
709
938
|
await runShow(values);
|
|
710
939
|
return;
|
|
@@ -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,207 @@
|
|
|
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 buildProjectAgentsMd(project, session) {
|
|
46
|
+
return [
|
|
47
|
+
"# AGENTS.md",
|
|
48
|
+
"",
|
|
49
|
+
"This directory is a Long Table research workspace.",
|
|
50
|
+
"",
|
|
51
|
+
"## Purpose",
|
|
52
|
+
`- Project name: ${project.projectName}`,
|
|
53
|
+
`- Current goal: ${session.currentGoal}`,
|
|
54
|
+
...(session.currentBlocker ? [`- Current blocker: ${session.currentBlocker}`] : []),
|
|
55
|
+
`- Requested perspectives: ${session.requestedPerspectives.length > 0 ? session.requestedPerspectives.join(", ") : "auto"}`,
|
|
56
|
+
`- Disagreement visibility: ${session.disagreementPreference}`,
|
|
57
|
+
"",
|
|
58
|
+
"## Research-facing behavior",
|
|
59
|
+
"- Treat researcher interaction as the primary task.",
|
|
60
|
+
"- Begin exploratory work with clarifying or tension questions before recommending a direction.",
|
|
61
|
+
"- If you foreground role perspectives, disclose them with `Long Table consulted: ...`.",
|
|
62
|
+
"- Keep one accountable synthesis, but do not hide meaningful disagreement.",
|
|
63
|
+
"- Do not expose internal tool logs, file-search traces, or process commentary in the researcher-facing answer.",
|
|
64
|
+
"",
|
|
65
|
+
"## Session memory",
|
|
66
|
+
"- Read `.longtable/current-session.json` before giving substantial guidance.",
|
|
67
|
+
"- Use `.longtable/project.json` as the project-level context.",
|
|
68
|
+
"- Prefer the current goal and blocker over generic assumptions.",
|
|
69
|
+
"",
|
|
70
|
+
"## Scope",
|
|
71
|
+
"- These instructions apply to this directory and its children."
|
|
72
|
+
].join("\n");
|
|
73
|
+
}
|
|
74
|
+
function buildStateSeed(project, session, setup) {
|
|
75
|
+
const state = createEmptyResearchState();
|
|
76
|
+
state.explicitState = {
|
|
77
|
+
field: setup.profileSeed.field,
|
|
78
|
+
careerStage: setup.profileSeed.careerStage,
|
|
79
|
+
experienceLevel: setup.profileSeed.experienceLevel,
|
|
80
|
+
projectName: project.projectName,
|
|
81
|
+
currentGoal: session.currentGoal,
|
|
82
|
+
disagreementPreference: session.disagreementPreference,
|
|
83
|
+
requestedPerspectives: session.requestedPerspectives
|
|
84
|
+
};
|
|
85
|
+
if (session.currentBlocker) {
|
|
86
|
+
state.explicitState.currentBlocker = session.currentBlocker;
|
|
87
|
+
state.openTensions.push(session.currentBlocker);
|
|
88
|
+
}
|
|
89
|
+
if (setup.profileSeed.humanAuthorshipSignal) {
|
|
90
|
+
state.explicitState.humanAuthorshipSignal = setup.profileSeed.humanAuthorshipSignal;
|
|
91
|
+
}
|
|
92
|
+
state.narrativeTraces.push({
|
|
93
|
+
id: "project-session-goal",
|
|
94
|
+
timestamp: nowIso(),
|
|
95
|
+
source: "longtable-start",
|
|
96
|
+
traceType: "judgment",
|
|
97
|
+
summary: `Current session goal: ${session.currentGoal}.`,
|
|
98
|
+
visibility: "explicit",
|
|
99
|
+
importance: "high"
|
|
100
|
+
});
|
|
101
|
+
if (session.currentBlocker) {
|
|
102
|
+
state.narrativeTraces.push({
|
|
103
|
+
id: "project-session-blocker",
|
|
104
|
+
timestamp: nowIso(),
|
|
105
|
+
source: "longtable-start",
|
|
106
|
+
traceType: "tension",
|
|
107
|
+
summary: `Current session blocker: ${session.currentBlocker}.`,
|
|
108
|
+
visibility: "explicit",
|
|
109
|
+
importance: "high"
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
return JSON.stringify(state, null, 2);
|
|
113
|
+
}
|
|
114
|
+
export async function createOrUpdateProjectWorkspace(options) {
|
|
115
|
+
const projectPath = resolve(options.projectPath);
|
|
116
|
+
const metaDir = resolveMetaDir(projectPath);
|
|
117
|
+
const sessionsDir = join(metaDir, "sessions");
|
|
118
|
+
const projectFilePath = join(metaDir, "project.json");
|
|
119
|
+
const sessionFilePath = join(metaDir, "current-session.json");
|
|
120
|
+
const sessionId = slugify(`${options.projectName}-${Date.now()}`);
|
|
121
|
+
await mkdir(projectPath, { recursive: true });
|
|
122
|
+
await mkdir(metaDir, { recursive: true });
|
|
123
|
+
await mkdir(sessionsDir, { recursive: true });
|
|
124
|
+
const project = existsSync(projectFilePath)
|
|
125
|
+
? JSON.parse(await readFile(projectFilePath, "utf8"))
|
|
126
|
+
: {
|
|
127
|
+
schemaVersion: 1,
|
|
128
|
+
product: "Long Table",
|
|
129
|
+
projectName: options.projectName,
|
|
130
|
+
projectPath,
|
|
131
|
+
createdAt: nowIso(),
|
|
132
|
+
globalSetupSummary: {
|
|
133
|
+
field: options.setup.profileSeed.field,
|
|
134
|
+
careerStage: options.setup.profileSeed.careerStage,
|
|
135
|
+
experienceLevel: options.setup.profileSeed.experienceLevel,
|
|
136
|
+
checkpointIntensity: options.setup.profileSeed.preferredCheckpointIntensity,
|
|
137
|
+
...(options.setup.profileSeed.humanAuthorshipSignal
|
|
138
|
+
? { humanAuthorshipSignal: options.setup.profileSeed.humanAuthorshipSignal }
|
|
139
|
+
: {}),
|
|
140
|
+
...(options.setup.profileSeed.weakestDomain
|
|
141
|
+
? { weakestDomain: options.setup.profileSeed.weakestDomain }
|
|
142
|
+
: {}),
|
|
143
|
+
...(options.setup.profileSeed.panelPreference
|
|
144
|
+
? { defaultPanelPreference: options.setup.profileSeed.panelPreference }
|
|
145
|
+
: {})
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
const session = {
|
|
149
|
+
schemaVersion: 1,
|
|
150
|
+
id: sessionId,
|
|
151
|
+
createdAt: nowIso(),
|
|
152
|
+
projectName: project.projectName,
|
|
153
|
+
projectPath,
|
|
154
|
+
currentGoal: options.currentGoal,
|
|
155
|
+
...(options.currentBlocker ? { currentBlocker: options.currentBlocker } : {}),
|
|
156
|
+
requestedPerspectives: options.requestedPerspectives,
|
|
157
|
+
disagreementPreference: options.disagreementPreference
|
|
158
|
+
};
|
|
159
|
+
await writeFile(projectFilePath, JSON.stringify(project, null, 2), "utf8");
|
|
160
|
+
await writeFile(sessionFilePath, JSON.stringify(session, null, 2), "utf8");
|
|
161
|
+
await writeFile(join(sessionsDir, `${sessionId}.json`), JSON.stringify(session, null, 2), "utf8");
|
|
162
|
+
await writeFile(join(metaDir, "state.json"), buildStateSeed(project, session, options.setup), "utf8");
|
|
163
|
+
await writeFile(join(projectPath, "LONGTABLE.md"), buildWorkspaceGuide(project, session), "utf8");
|
|
164
|
+
await writeFile(join(projectPath, "AGENTS.md"), buildProjectAgentsMd(project, session), "utf8");
|
|
165
|
+
return {
|
|
166
|
+
project,
|
|
167
|
+
session,
|
|
168
|
+
projectFilePath,
|
|
169
|
+
sessionFilePath,
|
|
170
|
+
metaDir
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
export async function loadProjectContextFromDirectory(startPath) {
|
|
174
|
+
let current = resolve(startPath);
|
|
175
|
+
while (true) {
|
|
176
|
+
const metaDir = resolveMetaDir(current);
|
|
177
|
+
const projectFilePath = join(metaDir, "project.json");
|
|
178
|
+
const sessionFilePath = join(metaDir, "current-session.json");
|
|
179
|
+
if (existsSync(projectFilePath) && existsSync(sessionFilePath)) {
|
|
180
|
+
return {
|
|
181
|
+
project: JSON.parse(await readFile(projectFilePath, "utf8")),
|
|
182
|
+
session: JSON.parse(await readFile(sessionFilePath, "utf8")),
|
|
183
|
+
projectFilePath,
|
|
184
|
+
sessionFilePath,
|
|
185
|
+
metaDir
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
const parent = dirname(current);
|
|
189
|
+
if (parent === current) {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
current = parent;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
export function renderProjectWorkspaceSummary(context) {
|
|
196
|
+
return [
|
|
197
|
+
"Long Table project workspace",
|
|
198
|
+
`project: ${context.project.projectName}`,
|
|
199
|
+
`path: ${context.project.projectPath}`,
|
|
200
|
+
`goal: ${context.session.currentGoal}`,
|
|
201
|
+
...(context.session.currentBlocker ? [`blocker: ${context.session.currentBlocker}`] : []),
|
|
202
|
+
`perspectives: ${context.session.requestedPerspectives.length > 0 ? context.session.requestedPerspectives.join(", ") : "auto"}`,
|
|
203
|
+
`disagreement: ${context.session.disagreementPreference}`,
|
|
204
|
+
`project file: ${context.projectFilePath}`,
|
|
205
|
+
`session file: ${context.sessionFilePath}`
|
|
206
|
+
].join("\n");
|
|
207
|
+
}
|
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.4",
|
|
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",
|