@longtable/cli 0.1.3 → 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 +57 -45
- package/dist/cli.js +224 -171
- 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
|
@@ -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,74 @@ 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
|
+
|
|
57
|
+
을 수행합니다.
|
|
58
|
+
|
|
59
|
+
## Recommended flow
|
|
48
60
|
|
|
49
61
|
```bash
|
|
50
|
-
longtable
|
|
62
|
+
longtable init --flow interview
|
|
63
|
+
longtable start
|
|
64
|
+
cd "<project-path>"
|
|
65
|
+
codex
|
|
51
66
|
```
|
|
52
67
|
|
|
53
|
-
|
|
68
|
+
이게 현재 가장 신뢰할 수 있는 Long Table 사용 경로입니다.
|
|
69
|
+
|
|
70
|
+
## After the project starts
|
|
71
|
+
|
|
72
|
+
프로젝트 디렉토리 안에서는 보통 그냥 `codex`를 열고 자연어로 연구를 시작하면 됩니다.
|
|
73
|
+
|
|
74
|
+
CLI를 계속 쓰고 싶다면 아래 명령은 보조 경로입니다.
|
|
75
|
+
|
|
76
|
+
## Advanced commands
|
|
54
77
|
|
|
55
78
|
```bash
|
|
56
|
-
longtable start
|
|
57
79
|
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
|
|
80
|
+
longtable ask --cwd "<project-path>" --prompt "연구 질문을 어디서부터 좁혀야 할지 모르겠어."
|
|
81
|
+
longtable review --cwd "<project-path>" --prompt "방법론적으로 어디가 취약한지 말해줘." --role methods_critic
|
|
63
82
|
```
|
|
64
83
|
|
|
65
|
-
##
|
|
84
|
+
## Roles
|
|
66
85
|
|
|
67
86
|
```bash
|
|
68
87
|
longtable roles
|
|
69
88
|
```
|
|
70
89
|
|
|
71
|
-
Long Table이 어떤 관점을 호출할 수 있는지 짧게 보여줍니다.
|
|
72
|
-
|
|
73
90
|
예:
|
|
74
91
|
|
|
75
92
|
- `editor`
|
|
@@ -77,25 +94,20 @@ Long Table이 어떤 관점을 호출할 수 있는지 짧게 보여줍니다.
|
|
|
77
94
|
- `methods_critic`
|
|
78
95
|
- `voice_keeper`
|
|
79
96
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
## Codex integration
|
|
83
|
-
|
|
84
|
-
Long Table은 Codex prompt file 설치도 지원합니다.
|
|
97
|
+
## Codex prompt-file integration
|
|
85
98
|
|
|
86
99
|
```bash
|
|
87
100
|
longtable codex install-prompts
|
|
88
101
|
longtable codex status
|
|
89
102
|
```
|
|
90
103
|
|
|
91
|
-
|
|
104
|
+
이 경로는 현재 실험적입니다.
|
|
105
|
+
사용자에게 약속하는 주 경로는 아닙니다.
|
|
92
106
|
|
|
93
|
-
|
|
94
|
-
- 그래서 기본 사용자 경로는 `/prompts:...`가 아니라 `longtable start`와 `longtable ask`입니다.
|
|
107
|
+
기본 경로는 여전히:
|
|
95
108
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
`Diverga`는 아직 일부 하위 패키지와 runtime path에서만 남아 있는 호환성 레이어입니다.
|
|
109
|
+
- `longtable init`
|
|
110
|
+
- `longtable start`
|
|
111
|
+
- 프로젝트 디렉토리에서 `codex`
|
|
100
112
|
|
|
101
|
-
|
|
113
|
+
입니다.
|
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",
|
|
@@ -28,8 +30,11 @@ const VALID_STAGES = new Set([
|
|
|
28
30
|
function usage() {
|
|
29
31
|
return [
|
|
30
32
|
"Usage:",
|
|
31
|
-
" longtable
|
|
32
|
-
" longtable start
|
|
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]",
|
|
33
38
|
" longtable roles [--json]",
|
|
34
39
|
" longtable show [--json] [--path <file>]",
|
|
35
40
|
" longtable install [--json] [--path <file>] [--runtime-path <file>]",
|
|
@@ -43,10 +48,10 @@ function usage() {
|
|
|
43
48
|
"Examples:",
|
|
44
49
|
" longtable init --flow interview --install-prompts",
|
|
45
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",
|
|
46
53
|
" longtable roles",
|
|
47
54
|
" longtable ask --prompt \"연구를 시작하고 싶어. 지금 어디서부터 좁혀야 할지 모르겠어.\"",
|
|
48
|
-
" longtable review --prompt \"Review this claim critically.\" --panel --show-conflicts",
|
|
49
|
-
" longtable review --role editor --prompt \"BJET 편집자 관점에서 봐줘.\"",
|
|
50
55
|
" printf '{\"provider\":\"codex\",...}' | longtable codex persist-init --stdin --install-prompts",
|
|
51
56
|
" longtable codex install-prompts"
|
|
52
57
|
].join("\n");
|
|
@@ -115,7 +120,7 @@ function buildSetupFlowChoices() {
|
|
|
115
120
|
function renderSetupHeader(flow) {
|
|
116
121
|
const title = flow === "interview" ? "Long Table Setup Interview" : "Long Table Quickstart";
|
|
117
122
|
const subtitle = flow === "interview"
|
|
118
|
-
? "We will ask about your research persona,
|
|
123
|
+
? "We will ask about your research persona, challenge preferences, and authorship defaults."
|
|
119
124
|
: "We will capture the minimum profile needed to start using Long Table.";
|
|
120
125
|
return [
|
|
121
126
|
"┌──────────────────────────────────────────────┐",
|
|
@@ -131,9 +136,6 @@ function questionSection(questionId) {
|
|
|
131
136
|
if (questionId === "field" || questionId === "careerStage" || questionId === "experienceLevel") {
|
|
132
137
|
return "Researcher profile";
|
|
133
138
|
}
|
|
134
|
-
if (questionId === "currentProjectType" || questionId === "currentResearchTopic" || questionId === "currentBlocker") {
|
|
135
|
-
return "Current work";
|
|
136
|
-
}
|
|
137
139
|
if (questionId === "preferredCheckpointIntensity" || questionId === "preferredEntryMode") {
|
|
138
140
|
return "Interaction style";
|
|
139
141
|
}
|
|
@@ -260,54 +262,84 @@ async function promptChoiceWithArrows(rl, prompt, choices) {
|
|
|
260
262
|
async function promptChoice(rl, prompt, choices) {
|
|
261
263
|
return promptChoiceWithArrows(rl, prompt, choices);
|
|
262
264
|
}
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
+
}
|
|
306
333
|
}
|
|
307
|
-
|
|
334
|
+
emitKeypressEvents(stream);
|
|
335
|
+
stream.setRawMode(true);
|
|
336
|
+
output.write("\u001B[?25l");
|
|
337
|
+
draw(true);
|
|
338
|
+
stream.on("keypress", onKeypress);
|
|
339
|
+
});
|
|
308
340
|
}
|
|
309
341
|
function hasCompleteFlagInput(args) {
|
|
310
|
-
const required = ["provider", "field", "career-stage", "experience", "
|
|
342
|
+
const required = ["provider", "field", "career-stage", "experience", "checkpoint"];
|
|
311
343
|
return required.every((key) => typeof args[key] === "string" && String(args[key]).trim().length > 0);
|
|
312
344
|
}
|
|
313
345
|
function resolveSetupFlow(args) {
|
|
@@ -318,13 +350,13 @@ function toSetupAnswers(args) {
|
|
|
318
350
|
field: String(args.field),
|
|
319
351
|
careerStage: String(args["career-stage"]),
|
|
320
352
|
experienceLevel: String(args.experience),
|
|
321
|
-
currentProjectType:
|
|
353
|
+
currentProjectType: typeof args["project-type"] === "string" && args["project-type"].trim().length > 0
|
|
354
|
+
? String(args["project-type"])
|
|
355
|
+
: "unspecified research task",
|
|
322
356
|
preferredCheckpointIntensity: String(args.checkpoint),
|
|
323
357
|
humanAuthorshipSignal: typeof args["authorship-signal"] === "string" && args["authorship-signal"].trim().length > 0
|
|
324
358
|
? args["authorship-signal"].trim()
|
|
325
359
|
: 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
360
|
preferredEntryMode: typeof args["entry-mode"] === "string" && VALID_MODES.has(String(args["entry-mode"]))
|
|
329
361
|
? String(args["entry-mode"])
|
|
330
362
|
: undefined,
|
|
@@ -336,15 +368,18 @@ function toSetupAnswers(args) {
|
|
|
336
368
|
: undefined
|
|
337
369
|
};
|
|
338
370
|
}
|
|
339
|
-
async function collectInteractiveAnswers() {
|
|
371
|
+
async function collectInteractiveAnswers(initialFlow) {
|
|
340
372
|
const rl = createInterface({ input, output });
|
|
341
373
|
try {
|
|
342
|
-
const flow =
|
|
374
|
+
const flow = initialFlow ??
|
|
375
|
+
(await promptChoice(rl, "How would you like to set up Long Table?", buildSetupFlowChoices()));
|
|
343
376
|
console.log("");
|
|
344
377
|
console.log(renderSetupHeader(flow));
|
|
345
378
|
console.log("");
|
|
346
379
|
const provider = await promptChoice(rl, "Which provider do you want to configure?", buildProviderChoices());
|
|
347
|
-
const answers = {
|
|
380
|
+
const answers = {
|
|
381
|
+
currentProjectType: "unspecified research task"
|
|
382
|
+
};
|
|
348
383
|
const questions = buildQuickSetupFlow(flow);
|
|
349
384
|
for (let index = 0; index < questions.length; index += 1) {
|
|
350
385
|
const question = questions[index];
|
|
@@ -365,18 +400,12 @@ async function collectInteractiveAnswers() {
|
|
|
365
400
|
answers.careerStage = value;
|
|
366
401
|
if (question.id === "experienceLevel")
|
|
367
402
|
answers.experienceLevel = value;
|
|
368
|
-
if (question.id === "currentProjectType")
|
|
369
|
-
answers.currentProjectType = value;
|
|
370
403
|
if (question.id === "preferredCheckpointIntensity") {
|
|
371
404
|
answers.preferredCheckpointIntensity = value;
|
|
372
405
|
}
|
|
373
406
|
if (question.id === "humanAuthorshipSignal" && value !== "other") {
|
|
374
407
|
answers.humanAuthorshipSignal = value;
|
|
375
408
|
}
|
|
376
|
-
if (question.id === "currentResearchTopic")
|
|
377
|
-
answers.currentResearchTopic = value;
|
|
378
|
-
if (question.id === "currentBlocker")
|
|
379
|
-
answers.currentBlocker = value;
|
|
380
409
|
if (question.id === "preferredEntryMode")
|
|
381
410
|
answers.preferredEntryMode = value;
|
|
382
411
|
if (question.id === "weakestDomain")
|
|
@@ -394,6 +423,84 @@ async function collectInteractiveAnswers() {
|
|
|
394
423
|
rl.close();
|
|
395
424
|
}
|
|
396
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
|
+
}
|
|
397
504
|
function normalizePersistAnswers(raw) {
|
|
398
505
|
return {
|
|
399
506
|
flow: raw.flow === "interview" ? "interview" : "quickstart",
|
|
@@ -402,17 +509,11 @@ function normalizePersistAnswers(raw) {
|
|
|
402
509
|
field: raw.field,
|
|
403
510
|
careerStage: raw.careerStage,
|
|
404
511
|
experienceLevel: raw.experienceLevel,
|
|
405
|
-
currentProjectType:
|
|
512
|
+
currentProjectType: "unspecified research task",
|
|
406
513
|
preferredCheckpointIntensity: raw.preferredCheckpointIntensity,
|
|
407
514
|
...(raw.humanAuthorshipSignal?.trim()
|
|
408
515
|
? { humanAuthorshipSignal: raw.humanAuthorshipSignal.trim() }
|
|
409
516
|
: {}),
|
|
410
|
-
...(raw.currentResearchTopic?.trim()
|
|
411
|
-
? { currentResearchTopic: raw.currentResearchTopic.trim() }
|
|
412
|
-
: {}),
|
|
413
|
-
...(raw.currentBlocker?.trim()
|
|
414
|
-
? { currentBlocker: raw.currentBlocker.trim() }
|
|
415
|
-
: {}),
|
|
416
517
|
...(raw.preferredEntryMode
|
|
417
518
|
? { preferredEntryMode: raw.preferredEntryMode }
|
|
418
519
|
: {}),
|
|
@@ -462,7 +563,7 @@ async function runInit(args) {
|
|
|
462
563
|
provider: String(args.provider) === "claude" ? "claude" : "codex",
|
|
463
564
|
answers: toSetupAnswers(args)
|
|
464
565
|
}
|
|
465
|
-
: await collectInteractiveAnswers();
|
|
566
|
+
: await collectInteractiveAnswers(typeof args.flow === "string" ? resolveSetupFlow(args) : undefined);
|
|
466
567
|
const outputValue = createPersistedSetupOutput(answers, provider, flow);
|
|
467
568
|
const result = await saveSetupAndRuntimeConfig(outputValue, {
|
|
468
569
|
setupPath: customPath,
|
|
@@ -495,15 +596,7 @@ async function runInit(args) {
|
|
|
495
596
|
console.log("- Start here: `longtable start`.");
|
|
496
597
|
console.log("- If you want a direct natural-language entry: `longtable ask --prompt \"...\"`.");
|
|
497
598
|
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
|
-
}
|
|
599
|
+
console.log("- Suggested next action: create a project workspace and let Long Table interview the current session.");
|
|
507
600
|
}
|
|
508
601
|
}
|
|
509
602
|
async function runShow(args) {
|
|
@@ -561,15 +654,7 @@ async function runCodexPersistInit(args) {
|
|
|
561
654
|
console.log("- Start here: `longtable start`.");
|
|
562
655
|
console.log("- If you want a direct natural-language entry: `longtable ask --prompt \"...\"`.");
|
|
563
656
|
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
|
-
}
|
|
657
|
+
console.log("- Suggested next action: create a project workspace and let Long Table interview the current session.");
|
|
573
658
|
}
|
|
574
659
|
}
|
|
575
660
|
async function resolvePrompt(prompt) {
|
|
@@ -638,7 +723,25 @@ async function loadOptionalSetup(path) {
|
|
|
638
723
|
return null;
|
|
639
724
|
}
|
|
640
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
|
+
}
|
|
641
743
|
async function runModeCommand(mode, args) {
|
|
744
|
+
const workingDirectory = typeof args.cwd === "string" ? args.cwd : cwd();
|
|
642
745
|
const prompt = await resolvePrompt(typeof args.prompt === "string" ? args.prompt : undefined);
|
|
643
746
|
if (!prompt) {
|
|
644
747
|
throw new Error("A prompt is required.");
|
|
@@ -648,13 +751,14 @@ async function runModeCommand(mode, args) {
|
|
|
648
751
|
throw new Error(`Invalid stage: ${stage}`);
|
|
649
752
|
}
|
|
650
753
|
const setup = await loadOptionalSetup(typeof args.setup === "string" ? args.setup : undefined);
|
|
754
|
+
const projectAware = await buildProjectAwarePrompt(prompt, workingDirectory);
|
|
651
755
|
const panelPreference = setup?.profileSeed.panelPreference;
|
|
652
756
|
const panelRequested = args.panel === true ||
|
|
653
757
|
panelPreference === "always_visible" ||
|
|
654
758
|
(panelPreference === "show_on_conflict" && args["show-conflicts"] === true);
|
|
655
759
|
const { guidedPrompt } = buildPersonaGuidance({
|
|
656
760
|
mode,
|
|
657
|
-
prompt,
|
|
761
|
+
prompt: projectAware.prompt,
|
|
658
762
|
roleFlag: typeof args.role === "string" ? args.role : undefined,
|
|
659
763
|
panel: panelRequested,
|
|
660
764
|
showConflicts: args["show-conflicts"] === true,
|
|
@@ -666,7 +770,7 @@ async function runModeCommand(mode, args) {
|
|
|
666
770
|
mode,
|
|
667
771
|
researchStage: stage,
|
|
668
772
|
setupPath: typeof args.setup === "string" ? args.setup : undefined,
|
|
669
|
-
workingDirectory
|
|
773
|
+
workingDirectory
|
|
670
774
|
});
|
|
671
775
|
console.log(wrapped.wrappedPrompt);
|
|
672
776
|
return;
|
|
@@ -676,7 +780,7 @@ async function runModeCommand(mode, args) {
|
|
|
676
780
|
mode,
|
|
677
781
|
researchStage: stage,
|
|
678
782
|
setupPath: typeof args.setup === "string" ? args.setup : undefined,
|
|
679
|
-
workingDirectory
|
|
783
|
+
workingDirectory,
|
|
680
784
|
json: args.json === true
|
|
681
785
|
});
|
|
682
786
|
exit(exitCode);
|
|
@@ -728,85 +832,34 @@ async function runStart(args) {
|
|
|
728
832
|
const setupPath = typeof args.setup === "string" ? args.setup : undefined;
|
|
729
833
|
const existingSetup = await loadOptionalSetup(setupPath);
|
|
730
834
|
if (!existingSetup) {
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
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));
|
|
747
854
|
return;
|
|
748
855
|
}
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
}
|
|
757
|
-
if (action === "roles") {
|
|
758
|
-
rl.close();
|
|
759
|
-
await runRoles(args);
|
|
760
|
-
return;
|
|
761
|
-
}
|
|
762
|
-
const defaultPrompt = existingSetup.profileSeed.currentBlocker ??
|
|
763
|
-
existingSetup.profileSeed.currentResearchTopic ??
|
|
764
|
-
"";
|
|
765
|
-
const prompt = (await rl.question(defaultPrompt
|
|
766
|
-
? `What are you working on today?\nPress Enter to use: ${defaultPrompt}\n> `
|
|
767
|
-
: "What are you working on today?\n> ")).trim() || defaultPrompt;
|
|
768
|
-
rl.close();
|
|
769
|
-
if (!prompt) {
|
|
770
|
-
throw new Error("A prompt is required.");
|
|
771
|
-
}
|
|
772
|
-
if (action === "editor") {
|
|
773
|
-
await runModeCommand("review", {
|
|
774
|
-
...args,
|
|
775
|
-
prompt,
|
|
776
|
-
role: "editor",
|
|
777
|
-
...(setupPath ? { setup: setupPath } : {})
|
|
778
|
-
});
|
|
779
|
-
return;
|
|
780
|
-
}
|
|
781
|
-
if (action === "methods") {
|
|
782
|
-
await runModeCommand("review", {
|
|
783
|
-
...args,
|
|
784
|
-
prompt,
|
|
785
|
-
role: "methods_critic",
|
|
786
|
-
...(setupPath ? { setup: setupPath } : {})
|
|
787
|
-
});
|
|
788
|
-
return;
|
|
789
|
-
}
|
|
790
|
-
if (action === "panel") {
|
|
791
|
-
await runModeCommand("review", {
|
|
792
|
-
...args,
|
|
793
|
-
prompt,
|
|
794
|
-
panel: true,
|
|
795
|
-
"show-conflicts": true,
|
|
796
|
-
...(setupPath ? { setup: setupPath } : {})
|
|
797
|
-
});
|
|
798
|
-
return;
|
|
799
|
-
}
|
|
800
|
-
const mode = action;
|
|
801
|
-
await runModeCommand(mode, {
|
|
802
|
-
...args,
|
|
803
|
-
prompt,
|
|
804
|
-
...(setupPath ? { setup: setupPath } : {})
|
|
805
|
-
});
|
|
806
|
-
}
|
|
807
|
-
finally {
|
|
808
|
-
rl.close();
|
|
809
|
-
}
|
|
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`");
|
|
810
863
|
}
|
|
811
864
|
async function runCodexSubcommand(subcommand, args) {
|
|
812
865
|
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,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",
|