@longtable/cli 0.1.5 → 0.1.7
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 +21 -0
- package/dist/cli.js +143 -21
- package/dist/persona-router.d.ts +9 -0
- package/dist/persona-router.js +72 -4
- package/dist/project-session.js +73 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -54,6 +54,8 @@ longtable start
|
|
|
54
54
|
- `.longtable/` 메모리 파일 생성
|
|
55
55
|
- 프로젝트용 `AGENTS.md` 생성
|
|
56
56
|
- `START-HERE.md` 생성
|
|
57
|
+
- `NEXT-STEPS.md` 생성
|
|
58
|
+
- `SESSION-SNAPSHOT.md` 생성
|
|
57
59
|
|
|
58
60
|
을 수행합니다.
|
|
59
61
|
|
|
@@ -68,16 +70,35 @@ codex
|
|
|
68
70
|
|
|
69
71
|
이게 현재 가장 신뢰할 수 있는 Long Table 사용 경로입니다.
|
|
70
72
|
|
|
73
|
+
돌아와서 다시 이어갈 때는:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
cd "<project-path>"
|
|
77
|
+
longtable resume
|
|
78
|
+
codex
|
|
79
|
+
```
|
|
80
|
+
|
|
71
81
|
## After the project starts
|
|
72
82
|
|
|
73
83
|
프로젝트 디렉토리 안에서는 보통 그냥 `codex`를 열고 자연어로 연구를 시작하면 됩니다.
|
|
74
84
|
|
|
85
|
+
Codex 안에서 더 명시적으로 부르고 싶다면, 아래처럼 짧은 문법도 사용할 수 있습니다.
|
|
86
|
+
|
|
87
|
+
```text
|
|
88
|
+
lt explore: 연구 질문을 어디서부터 좁혀야 할지 모르겠어.
|
|
89
|
+
lt review: 이 주장 어디가 약한지 봐줘.
|
|
90
|
+
lt panel: 이 방향에 대한 의견 충돌도 같이 보여줘.
|
|
91
|
+
lt editor: BJET 편집자 관점에서 봐줘.
|
|
92
|
+
lt methods: 방법론적으로 어디가 취약한지 말해줘.
|
|
93
|
+
```
|
|
94
|
+
|
|
75
95
|
CLI를 계속 쓰고 싶다면 아래 명령은 보조 경로입니다.
|
|
76
96
|
|
|
77
97
|
## Advanced commands
|
|
78
98
|
|
|
79
99
|
```bash
|
|
80
100
|
longtable roles
|
|
101
|
+
longtable resume --cwd "<project-path>"
|
|
81
102
|
longtable ask --cwd "<project-path>" --prompt "연구 질문을 어디서부터 좁혀야 할지 모르겠어."
|
|
82
103
|
longtable review --cwd "<project-path>" --prompt "방법론적으로 어디가 취약한지 말해줘." --role methods_critic
|
|
83
104
|
```
|
package/dist/cli.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, readFileSync, statSync } 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
6
|
import { resolve } from "node:path";
|
|
7
|
+
import { homedir } from "node:os";
|
|
7
8
|
import { buildProviderChoices, buildQuickSetupFlow, createPersistedSetupOutput, installRuntimeConfigFromStoredSetup, loadSetupOutput, renderInstallSummary, renderSetupSummary, resolveDefaultRuntimeConfigPath, resolveDefaultSetupPath, saveSetupAndRuntimeConfig, serializeSetupOutput } from "@diverga/setup";
|
|
8
9
|
import { buildCodexThinWrappedPrompt, runCodexThinWrapper } from "@diverga/provider-codex";
|
|
9
10
|
import { installCodexPromptAliases, listInstalledCodexPromptAliases, removeCodexPromptAliases, resolveCodexPromptsDir } from "./prompt-aliases.js";
|
|
10
|
-
import { buildPersonaGuidance } from "./persona-router.js";
|
|
11
|
+
import { buildPersonaGuidance, parseInvocationDirective } from "./persona-router.js";
|
|
11
12
|
import { PERSONA_DEFINITIONS } from "./personas.js";
|
|
12
13
|
import { createOrUpdateProjectWorkspace, loadProjectContextFromDirectory, renderProjectWorkspaceSummary } from "./project-session.js";
|
|
13
14
|
const VALID_MODES = new Set([
|
|
@@ -45,6 +46,22 @@ function renderSectionCard(title, body) {
|
|
|
45
46
|
...body
|
|
46
47
|
].join("\n");
|
|
47
48
|
}
|
|
49
|
+
function renderBrandBanner(title, subtitle) {
|
|
50
|
+
const lines = [
|
|
51
|
+
style("╭──────────────────────────────────────────────╮", ANSI.cyan),
|
|
52
|
+
style(`│ ${title.padEnd(44, " ")}│`, `${ANSI.bold}${ANSI.cyan}`),
|
|
53
|
+
style("╰──────────────────────────────────────────────╯", ANSI.cyan)
|
|
54
|
+
];
|
|
55
|
+
if (subtitle) {
|
|
56
|
+
lines.push(style(subtitle, ANSI.dim));
|
|
57
|
+
}
|
|
58
|
+
return lines.join("\n");
|
|
59
|
+
}
|
|
60
|
+
function renderProgressBar(current, total) {
|
|
61
|
+
const width = 10;
|
|
62
|
+
const filled = Math.max(1, Math.round((current / total) * width));
|
|
63
|
+
return `${"█".repeat(filled)}${"·".repeat(Math.max(0, width - filled))}`;
|
|
64
|
+
}
|
|
48
65
|
function usage() {
|
|
49
66
|
return [
|
|
50
67
|
"Usage:",
|
|
@@ -53,6 +70,7 @@ function usage() {
|
|
|
53
70
|
"",
|
|
54
71
|
" longtable init [--flow quickstart|interview] [--provider codex|claude] [--field <field>] [--career-stage <stage>] [--experience novice|intermediate|advanced] [--checkpoint low|balanced|high] [--authorship-signal <text>] [--entry-mode explore|review|critique|draft|commit] [--weakest-domain theory|methodology|measurement|analysis|writing] [--panel-preference synthesis_only|show_on_conflict|always_visible] [--json] [--no-install] [--install-prompts]",
|
|
55
72
|
" longtable start [--path <dir>] [--name <project>] [--goal <text>] [--blocker <text>] [--perspectives <role[,role]>] [--disagreement synthesis_only|show_on_conflict|always_visible] [--setup <path>] [--json]",
|
|
73
|
+
" longtable resume [--cwd <path>] [--json]",
|
|
56
74
|
" longtable roles [--json]",
|
|
57
75
|
" longtable show [--json] [--path <file>]",
|
|
58
76
|
" longtable install [--json] [--path <file>] [--runtime-path <file>]",
|
|
@@ -79,7 +97,7 @@ function parseArgs(argv) {
|
|
|
79
97
|
const values = {};
|
|
80
98
|
let subcommand = maybeSubcommand;
|
|
81
99
|
const modeCommand = command && VALID_MODES.has(command);
|
|
82
|
-
const directCommand = command && ["init", "start", "roles", "show", "install", "codex", "ask"].includes(command);
|
|
100
|
+
const directCommand = command && ["init", "start", "resume", "roles", "show", "install", "codex", "ask"].includes(command);
|
|
83
101
|
let startIndex = 1;
|
|
84
102
|
if (modeCommand) {
|
|
85
103
|
subcommand = undefined;
|
|
@@ -140,10 +158,14 @@ function renderSetupHeader(flow) {
|
|
|
140
158
|
const subtitle = flow === "interview"
|
|
141
159
|
? "We will ask about your research persona, challenge preferences, and authorship defaults."
|
|
142
160
|
: "We will capture the minimum profile needed to start using Long Table.";
|
|
143
|
-
return renderSectionCard(title, [subtitle]);
|
|
161
|
+
return [renderBrandBanner("Long Table", "Research workspace setup"), "", renderSectionCard(title, [subtitle])].join("\n");
|
|
144
162
|
}
|
|
145
163
|
function renderQuestionHeader(index, total, section, prompt) {
|
|
146
|
-
return [
|
|
164
|
+
return [
|
|
165
|
+
"",
|
|
166
|
+
style(`[${index}/${total}] ${section} ${renderProgressBar(index, total)}`, `${ANSI.bold}${ANSI.cyan}`),
|
|
167
|
+
prompt
|
|
168
|
+
].join("\n");
|
|
147
169
|
}
|
|
148
170
|
function questionSection(questionId) {
|
|
149
171
|
if (questionId === "field" || questionId === "careerStage" || questionId === "experienceLevel") {
|
|
@@ -174,6 +196,52 @@ function renderArrowMenu(prompt, choices, selectedIndex) {
|
|
|
174
196
|
}
|
|
175
197
|
return lines.join("\n");
|
|
176
198
|
}
|
|
199
|
+
function countRenderedLines(text) {
|
|
200
|
+
return text.split("\n").length;
|
|
201
|
+
}
|
|
202
|
+
function stripWrappingQuotes(value) {
|
|
203
|
+
const trimmed = value.trim();
|
|
204
|
+
if ((trimmed.startsWith("\"") && trimmed.endsWith("\"")) ||
|
|
205
|
+
(trimmed.startsWith("'") && trimmed.endsWith("'"))) {
|
|
206
|
+
return trimmed.slice(1, -1).trim();
|
|
207
|
+
}
|
|
208
|
+
return trimmed;
|
|
209
|
+
}
|
|
210
|
+
function expandHomePath(value) {
|
|
211
|
+
if (value === "~") {
|
|
212
|
+
return homedir();
|
|
213
|
+
}
|
|
214
|
+
if (value.startsWith("~/")) {
|
|
215
|
+
return resolve(homedir(), value.slice(2));
|
|
216
|
+
}
|
|
217
|
+
return value;
|
|
218
|
+
}
|
|
219
|
+
function normalizeUserPath(value) {
|
|
220
|
+
return expandHomePath(stripWrappingQuotes(value));
|
|
221
|
+
}
|
|
222
|
+
function projectFolderSlug(projectName) {
|
|
223
|
+
return projectName
|
|
224
|
+
.trim()
|
|
225
|
+
.replace(/[^\w가-힣]+/g, "-")
|
|
226
|
+
.replace(/-+/g, "-")
|
|
227
|
+
.replace(/^-+|-+$/g, "");
|
|
228
|
+
}
|
|
229
|
+
function resolveInteractiveProjectPath(parentOrPath, projectName) {
|
|
230
|
+
const normalized = normalizeUserPath(parentOrPath);
|
|
231
|
+
const folderName = projectFolderSlug(projectName);
|
|
232
|
+
if (!normalized) {
|
|
233
|
+
return resolve(folderName);
|
|
234
|
+
}
|
|
235
|
+
try {
|
|
236
|
+
if (existsSync(normalized) && statSync(normalized).isDirectory()) {
|
|
237
|
+
return resolve(normalized, folderName);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
return resolve(normalized);
|
|
242
|
+
}
|
|
243
|
+
return resolve(normalized);
|
|
244
|
+
}
|
|
177
245
|
async function promptChoiceByNumber(rl, prompt, choices) {
|
|
178
246
|
while (true) {
|
|
179
247
|
const answer = await rl.question(`${prompt}\n${renderChoices(choices)}\nSelect one number: `);
|
|
@@ -213,17 +281,19 @@ async function promptChoiceWithArrows(rl, prompt, choices) {
|
|
|
213
281
|
}
|
|
214
282
|
const previousRawMode = stream.isRaw;
|
|
215
283
|
let selectedIndex = 0;
|
|
216
|
-
|
|
284
|
+
let lastRenderLineCount = 0;
|
|
217
285
|
return await new Promise((resolve, reject) => {
|
|
218
286
|
function draw(first = false) {
|
|
219
|
-
|
|
220
|
-
|
|
287
|
+
const renderedText = renderArrowMenu(prompt, choices, selectedIndex);
|
|
288
|
+
const rendered = renderedText.split("\n");
|
|
289
|
+
if (!first && lastRenderLineCount > 0) {
|
|
290
|
+
output.write(moveCursorUp(lastRenderLineCount));
|
|
221
291
|
}
|
|
222
|
-
const rendered = renderArrowMenu(prompt, choices, selectedIndex).split("\n");
|
|
223
292
|
for (const line of rendered) {
|
|
224
293
|
output.write(clearLine());
|
|
225
294
|
output.write(`${line}\n`);
|
|
226
295
|
}
|
|
296
|
+
lastRenderLineCount = countRenderedLines(renderedText);
|
|
227
297
|
}
|
|
228
298
|
function cleanup() {
|
|
229
299
|
stream.off("keypress", onKeypress);
|
|
@@ -287,12 +357,9 @@ async function promptMultiChoice(rl, prompt, choices) {
|
|
|
287
357
|
const previousRawMode = stream.isRaw;
|
|
288
358
|
let selectedIndex = 0;
|
|
289
359
|
const selected = new Set();
|
|
290
|
-
|
|
360
|
+
let lastRenderLineCount = 0;
|
|
291
361
|
return await new Promise((resolvePromise, reject) => {
|
|
292
362
|
function draw(first = false) {
|
|
293
|
-
if (!first) {
|
|
294
|
-
output.write(moveCursorUp(renderedLineCount));
|
|
295
|
-
}
|
|
296
363
|
const lines = [prompt, "Use ↑/↓, Space to toggle, and Enter to confirm."];
|
|
297
364
|
for (let index = 0; index < choices.length; index += 1) {
|
|
298
365
|
const choice = choices[index];
|
|
@@ -300,10 +367,15 @@ async function promptMultiChoice(rl, prompt, choices) {
|
|
|
300
367
|
const marker = selected.has(choice.id) ? "[x]" : "[ ]";
|
|
301
368
|
lines.push(`${pointer} ${marker} ${choice.label} - ${choice.description}`);
|
|
302
369
|
}
|
|
370
|
+
const renderedText = lines.join("\n");
|
|
371
|
+
if (!first && lastRenderLineCount > 0) {
|
|
372
|
+
output.write(moveCursorUp(lastRenderLineCount));
|
|
373
|
+
}
|
|
303
374
|
for (const line of lines) {
|
|
304
375
|
output.write(clearLine());
|
|
305
376
|
output.write(`${line}\n`);
|
|
306
377
|
}
|
|
378
|
+
lastRenderLineCount = countRenderedLines(renderedText);
|
|
307
379
|
}
|
|
308
380
|
function cleanup() {
|
|
309
381
|
stream.off("keypress", onKeypress);
|
|
@@ -462,6 +534,8 @@ async function collectProjectInterview(setup, args) {
|
|
|
462
534
|
const rl = createInterface({ input, output });
|
|
463
535
|
try {
|
|
464
536
|
if (needsInteractivePrompts) {
|
|
537
|
+
console.log("");
|
|
538
|
+
console.log(renderBrandBanner("Long Table", "Project workspace interview"));
|
|
465
539
|
console.log("");
|
|
466
540
|
console.log(renderSectionCard("Long Table Project Start", [
|
|
467
541
|
"We will create a project workspace and a session memory seed for today's work.",
|
|
@@ -471,11 +545,13 @@ async function collectProjectInterview(setup, args) {
|
|
|
471
545
|
}
|
|
472
546
|
const projectName = (typeof args.name === "string" && args.name.trim()) ||
|
|
473
547
|
(await promptText(rl, renderQuestionHeader(1, 6, "Project interview", "What should this project be called?"), true));
|
|
474
|
-
const
|
|
475
|
-
? args.path.trim()
|
|
476
|
-
:
|
|
477
|
-
const
|
|
478
|
-
|
|
548
|
+
const suggestedParentDir = typeof args.path === "string" && args.path.trim()
|
|
549
|
+
? normalizeUserPath(args.path.trim())
|
|
550
|
+
: homedir();
|
|
551
|
+
const suggestedPath = resolveInteractiveProjectPath(suggestedParentDir, projectName);
|
|
552
|
+
const projectPath = (typeof args.path === "string" && args.path.trim()
|
|
553
|
+
? normalizeUserPath(args.path.trim())
|
|
554
|
+
: resolveInteractiveProjectPath((await promptText(rl, renderQuestionHeader(2, 6, "Project interview", `Which parent directory should contain this project?\nLong Table will create this folder:\n${suggestedPath}`), true)), projectName));
|
|
479
555
|
const currentGoal = (typeof args.goal === "string" && args.goal.trim()) ||
|
|
480
556
|
(await promptText(rl, renderQuestionHeader(3, 6, "Current session", "What are you trying to accomplish in this session?"), true));
|
|
481
557
|
const currentBlocker = (typeof args.blocker === "string" && args.blocker.trim()) ||
|
|
@@ -803,7 +879,9 @@ async function runAsk(args) {
|
|
|
803
879
|
if (!prompt) {
|
|
804
880
|
throw new Error("A prompt is required.");
|
|
805
881
|
}
|
|
806
|
-
const
|
|
882
|
+
const directive = parseInvocationDirective(prompt);
|
|
883
|
+
const effectivePrompt = directive.cleanedPrompt;
|
|
884
|
+
const inferred = directive.mode ?? inferModeFromPrompt(effectivePrompt);
|
|
807
885
|
if (inferred === "status") {
|
|
808
886
|
await runCodexSubcommand("status", args);
|
|
809
887
|
return;
|
|
@@ -811,9 +889,12 @@ async function runAsk(args) {
|
|
|
811
889
|
const mode = inferred === "panel" ? "review" : inferred;
|
|
812
890
|
const delegatedArgs = {
|
|
813
891
|
...args,
|
|
814
|
-
prompt
|
|
892
|
+
prompt: effectivePrompt
|
|
815
893
|
};
|
|
816
|
-
if (
|
|
894
|
+
if (directive.roles.length > 0 && typeof delegatedArgs.role !== "string") {
|
|
895
|
+
delegatedArgs.role = directive.roles.join(",");
|
|
896
|
+
}
|
|
897
|
+
if ((inferred === "panel" || directive.panel) && delegatedArgs.panel !== true) {
|
|
817
898
|
delegatedArgs.panel = true;
|
|
818
899
|
delegatedArgs["show-conflicts"] = true;
|
|
819
900
|
}
|
|
@@ -833,6 +914,7 @@ async function runRoles(args) {
|
|
|
833
914
|
}
|
|
834
915
|
console.log("Long Table roles");
|
|
835
916
|
console.log("These are perspectives Long Table can consult when relevant.");
|
|
917
|
+
console.log("Inside Codex, explicit forms like `lt editor: ...` and `lt methods: ...` are stronger than plain natural language.");
|
|
836
918
|
console.log("");
|
|
837
919
|
for (const persona of payload) {
|
|
838
920
|
console.log(`- ${persona.label} (${persona.key})`);
|
|
@@ -872,12 +954,48 @@ async function runStart(args) {
|
|
|
872
954
|
`1. cd "${context.project.projectPath}"`,
|
|
873
955
|
"2. run `codex` in that directory",
|
|
874
956
|
"3. begin with your current goal in natural language",
|
|
957
|
+
"4. if you return later, read `START-HERE.md`, `NEXT-STEPS.md`, or run `longtable resume`",
|
|
875
958
|
"",
|
|
876
959
|
`Suggested first message: ${context.session.currentBlocker ? `"I want to work on ${context.session.currentGoal}. My current blocker is ${context.session.currentBlocker}."` : `"I want to work on ${context.session.currentGoal}."`}`,
|
|
877
960
|
"",
|
|
878
961
|
`Optional CLI path: longtable ask --cwd "${context.project.projectPath}" --prompt "${context.session.currentGoal.replaceAll("\"", "\\\"")}"`
|
|
879
962
|
]));
|
|
880
963
|
}
|
|
964
|
+
async function runResume(args) {
|
|
965
|
+
const workingDirectory = typeof args.cwd === "string" ? args.cwd : cwd();
|
|
966
|
+
const context = await loadProjectContextFromDirectory(workingDirectory);
|
|
967
|
+
if (!context) {
|
|
968
|
+
throw new Error("No Long Table project workspace was found here. Run `longtable start` first or pass --cwd.");
|
|
969
|
+
}
|
|
970
|
+
const payload = {
|
|
971
|
+
project: context.project,
|
|
972
|
+
session: context.session,
|
|
973
|
+
files: {
|
|
974
|
+
startHere: resolve(context.project.projectPath, "START-HERE.md"),
|
|
975
|
+
nextSteps: resolve(context.project.projectPath, "NEXT-STEPS.md"),
|
|
976
|
+
sessionSnapshot: resolve(context.project.projectPath, "SESSION-SNAPSHOT.md")
|
|
977
|
+
}
|
|
978
|
+
};
|
|
979
|
+
if (args.json === true) {
|
|
980
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
console.log(renderSectionCard("Long Table Resume", [
|
|
984
|
+
`Project: ${context.project.projectName}`,
|
|
985
|
+
`Path: ${context.project.projectPath}`,
|
|
986
|
+
`Current goal: ${context.session.currentGoal}`,
|
|
987
|
+
...(context.session.currentBlocker ? [`Current blocker: ${context.session.currentBlocker}`] : []),
|
|
988
|
+
`Requested perspectives: ${context.session.requestedPerspectives.length > 0 ? context.session.requestedPerspectives.join(", ") : "auto"}`,
|
|
989
|
+
`Disagreement: ${context.session.disagreementPreference}`,
|
|
990
|
+
"",
|
|
991
|
+
"Resume files:",
|
|
992
|
+
`- ${payload.files.startHere}`,
|
|
993
|
+
`- ${payload.files.nextSteps}`,
|
|
994
|
+
`- ${payload.files.sessionSnapshot}`,
|
|
995
|
+
"",
|
|
996
|
+
`Suggested restart message: ${context.session.currentBlocker ? `"I want to continue ${context.session.currentGoal}. The unresolved blocker is ${context.session.currentBlocker}."` : `"I want to continue ${context.session.currentGoal}."`}`
|
|
997
|
+
]));
|
|
998
|
+
}
|
|
881
999
|
async function runCodexSubcommand(subcommand, args) {
|
|
882
1000
|
const customDir = typeof args.dir === "string" ? args.dir : undefined;
|
|
883
1001
|
if (subcommand === "install-prompts") {
|
|
@@ -947,6 +1065,10 @@ async function main() {
|
|
|
947
1065
|
await runStart(values);
|
|
948
1066
|
return;
|
|
949
1067
|
}
|
|
1068
|
+
if (command === "resume") {
|
|
1069
|
+
await runResume(values);
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
950
1072
|
if (command === "roles") {
|
|
951
1073
|
await runRoles(values);
|
|
952
1074
|
return;
|
package/dist/persona-router.d.ts
CHANGED
|
@@ -8,8 +8,17 @@ export interface PersonaRoutingResult {
|
|
|
8
8
|
consultedRoles: CanonicalPersona[];
|
|
9
9
|
ambiguousSignal: string | null;
|
|
10
10
|
}
|
|
11
|
+
export interface LongTableInvocationDirective {
|
|
12
|
+
explicit: boolean;
|
|
13
|
+
cleanedPrompt: string;
|
|
14
|
+
mode?: InteractionMode | "panel" | "status";
|
|
15
|
+
roles: CanonicalPersona[];
|
|
16
|
+
panel: boolean;
|
|
17
|
+
showConflicts: boolean;
|
|
18
|
+
}
|
|
11
19
|
export declare function detectOutputLanguage(input: string): OutputLanguage;
|
|
12
20
|
export declare function parseRoleFlag(value?: string): CanonicalPersona[];
|
|
21
|
+
export declare function parseInvocationDirective(prompt: string): LongTableInvocationDirective;
|
|
13
22
|
export declare function routePersonas(prompt: string, explicitRoleFlag?: string): PersonaRoutingResult;
|
|
14
23
|
export declare function renderDisclosure(roles: CanonicalPersona[], language: OutputLanguage): string | null;
|
|
15
24
|
export declare function buildPersonaGuidance(options: {
|
package/dist/persona-router.js
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
import { getPersonaDefinition, parsePersonaKey, PERSONA_DEFINITIONS } from "./personas.js";
|
|
2
2
|
const AUTO_CALL_LIMIT = 3;
|
|
3
|
+
const INVOCATION_PREFIX = /^(?:lt|longtable|long table|롱테이블)\s+/i;
|
|
4
|
+
const DIRECTIVE_MAP = [
|
|
5
|
+
{ key: "explore", mode: "explore" },
|
|
6
|
+
{ key: "review", mode: "review" },
|
|
7
|
+
{ key: "critique", mode: "critique" },
|
|
8
|
+
{ key: "draft", mode: "draft" },
|
|
9
|
+
{ key: "commit", mode: "commit" },
|
|
10
|
+
{ key: "panel", mode: "panel", panel: true, showConflicts: true },
|
|
11
|
+
{ key: "status", mode: "status" },
|
|
12
|
+
{ key: "editor", mode: "review", roles: ["editor"] },
|
|
13
|
+
{ key: "reviewer", mode: "review", roles: ["reviewer"] },
|
|
14
|
+
{ key: "methods", mode: "review", roles: ["methods_critic"] },
|
|
15
|
+
{ key: "method", mode: "review", roles: ["methods_critic"] },
|
|
16
|
+
{ key: "theory", mode: "review", roles: ["theory_critic"] },
|
|
17
|
+
{ key: "measurement", mode: "review", roles: ["measurement_auditor"] },
|
|
18
|
+
{ key: "ethics", mode: "review", roles: ["ethics_reviewer"] },
|
|
19
|
+
{ key: "voice", mode: "review", roles: ["voice_keeper"] },
|
|
20
|
+
{ key: "venue", mode: "review", roles: ["venue_strategist"] }
|
|
21
|
+
];
|
|
3
22
|
function unique(items) {
|
|
4
23
|
return [...new Set(items)];
|
|
5
24
|
}
|
|
@@ -15,6 +34,49 @@ export function parseRoleFlag(value) {
|
|
|
15
34
|
.map((part) => parsePersonaKey(part))
|
|
16
35
|
.filter((part) => part !== null));
|
|
17
36
|
}
|
|
37
|
+
export function parseInvocationDirective(prompt) {
|
|
38
|
+
const trimmed = prompt.trim();
|
|
39
|
+
if (!INVOCATION_PREFIX.test(trimmed)) {
|
|
40
|
+
return {
|
|
41
|
+
explicit: false,
|
|
42
|
+
cleanedPrompt: prompt,
|
|
43
|
+
roles: [],
|
|
44
|
+
panel: false,
|
|
45
|
+
showConflicts: false
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
const withoutPrefix = trimmed.replace(INVOCATION_PREFIX, "");
|
|
49
|
+
const colonIndex = withoutPrefix.indexOf(":");
|
|
50
|
+
if (colonIndex === -1) {
|
|
51
|
+
return {
|
|
52
|
+
explicit: false,
|
|
53
|
+
cleanedPrompt: prompt,
|
|
54
|
+
roles: [],
|
|
55
|
+
panel: false,
|
|
56
|
+
showConflicts: false
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
const directiveKey = withoutPrefix.slice(0, colonIndex).trim().toLowerCase();
|
|
60
|
+
const body = withoutPrefix.slice(colonIndex + 1).trim();
|
|
61
|
+
const directive = DIRECTIVE_MAP.find((entry) => entry.key === directiveKey);
|
|
62
|
+
if (!directive) {
|
|
63
|
+
return {
|
|
64
|
+
explicit: false,
|
|
65
|
+
cleanedPrompt: body || prompt,
|
|
66
|
+
roles: [],
|
|
67
|
+
panel: false,
|
|
68
|
+
showConflicts: false
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
explicit: true,
|
|
73
|
+
cleanedPrompt: body || prompt,
|
|
74
|
+
mode: directive.mode,
|
|
75
|
+
roles: directive.roles ?? [],
|
|
76
|
+
panel: directive.panel === true,
|
|
77
|
+
showConflicts: directive.showConflicts === true
|
|
78
|
+
};
|
|
79
|
+
}
|
|
18
80
|
export function routePersonas(prompt, explicitRoleFlag) {
|
|
19
81
|
const normalizedPrompt = prompt.toLowerCase();
|
|
20
82
|
const explicitRoles = parseRoleFlag(explicitRoleFlag);
|
|
@@ -60,7 +122,10 @@ export function renderDisclosure(roles, language) {
|
|
|
60
122
|
: `Long Table consulted: ${labels.join(", ")}`;
|
|
61
123
|
}
|
|
62
124
|
export function buildPersonaGuidance(options) {
|
|
63
|
-
const
|
|
125
|
+
const directive = parseInvocationDirective(options.prompt);
|
|
126
|
+
const effectivePrompt = directive.cleanedPrompt;
|
|
127
|
+
const mergedRoleFlag = [options.roleFlag, directive.roles.join(",")].filter(Boolean).join(",");
|
|
128
|
+
const routing = routePersonas(effectivePrompt, mergedRoleFlag || undefined);
|
|
64
129
|
const disclosure = renderDisclosure(routing.consultedRoles, routing.outputLanguage);
|
|
65
130
|
const lines = [];
|
|
66
131
|
lines.push(routing.outputLanguage === "ko"
|
|
@@ -74,12 +139,12 @@ export function buildPersonaGuidance(options) {
|
|
|
74
139
|
? "Ambiguity note: 편집자 관점인지 리뷰어 관점인지 애매합니다. 먼저 둘 중 무엇을 우선할지 짧게 확인하세요."
|
|
75
140
|
: "Ambiguity note: it is unclear whether the user wants an editor view or reviewer view. Ask briefly before closing.");
|
|
76
141
|
}
|
|
77
|
-
if (options.panel) {
|
|
142
|
+
if (options.panel || directive.panel) {
|
|
78
143
|
lines.push(routing.outputLanguage === "ko"
|
|
79
144
|
? "Return format: 1) Long Table synthesis 2) panel opinions by role 3) decision prompt to the researcher."
|
|
80
145
|
: "Return format: 1) Long Table synthesis 2) panel opinions by role 3) decision prompt to the researcher.");
|
|
81
146
|
}
|
|
82
|
-
if (options.showConflicts) {
|
|
147
|
+
if (options.showConflicts || directive.showConflicts) {
|
|
83
148
|
lines.push(routing.outputLanguage === "ko"
|
|
84
149
|
? "If roles disagree, show the conflict explicitly instead of forcing one answer."
|
|
85
150
|
: "If roles disagree, show the conflict explicitly instead of forcing one answer.");
|
|
@@ -89,10 +154,13 @@ export function buildPersonaGuidance(options) {
|
|
|
89
154
|
? "Include a short deliberation trace showing why the roles diverged."
|
|
90
155
|
: "Include a short deliberation trace showing why the roles diverged.");
|
|
91
156
|
}
|
|
157
|
+
lines.push(routing.outputLanguage === "ko"
|
|
158
|
+
? "For factual, current, or external claims, attach source links or local file references when possible. If you cannot source a statement, label it as inference or estimate."
|
|
159
|
+
: "For factual, current, or external claims, attach source links or local file references when possible. If you cannot source a statement, label it as inference or estimate.");
|
|
92
160
|
lines.push(routing.outputLanguage === "ko"
|
|
93
161
|
? "Do not show internal file-search logs, tool traces, or process commentary in the researcher-facing answer."
|
|
94
162
|
: "Do not show internal file-search logs, tool traces, or process commentary in the researcher-facing answer.");
|
|
95
|
-
lines.push(
|
|
163
|
+
lines.push(effectivePrompt.trim());
|
|
96
164
|
return {
|
|
97
165
|
guidedPrompt: lines.join("\n\n"),
|
|
98
166
|
routing
|
package/dist/project-session.js
CHANGED
|
@@ -34,6 +34,8 @@ function buildWorkspaceGuide(project, session) {
|
|
|
34
34
|
"- Ask clarifying or tension questions before closing too early.",
|
|
35
35
|
"- If you foreground specific roles, disclose them with `Long Table consulted: ...`.",
|
|
36
36
|
"- Keep one accountable synthesis, but keep disagreement visible by default when it matters.",
|
|
37
|
+
"- For factual, current, or external claims, attach source links or local file references whenever possible.",
|
|
38
|
+
"- If you cannot source a statement, label it as an inference or estimate.",
|
|
37
39
|
"- Do not expose internal tool logs or process commentary in researcher-facing answers.",
|
|
38
40
|
"",
|
|
39
41
|
"## Session files",
|
|
@@ -54,12 +56,22 @@ function buildStartHereGuide(project, session) {
|
|
|
54
56
|
"1. Open Codex in this directory.",
|
|
55
57
|
"2. Start with your current goal, not a shell command.",
|
|
56
58
|
"3. Let Long Table keep disagreement visible when it matters.",
|
|
59
|
+
"4. If you ask for factual or current external information, expect sources or an explicit inference label.",
|
|
57
60
|
"",
|
|
58
61
|
"## Suggested first message",
|
|
59
62
|
session.currentBlocker
|
|
60
63
|
? `"I want to work on ${session.currentGoal}. My current blocker is: ${session.currentBlocker}."`
|
|
61
64
|
: `"I want to work on ${session.currentGoal}."`,
|
|
62
65
|
"",
|
|
66
|
+
"## Explicit Long Table invocation inside Codex",
|
|
67
|
+
"- `lt explore: <question>`",
|
|
68
|
+
"- `lt review: <claim or plan>`",
|
|
69
|
+
"- `lt panel: <claim or plan>`",
|
|
70
|
+
"- `lt editor: <draft or positioning question>`",
|
|
71
|
+
"- `lt reviewer: <claim or section>`",
|
|
72
|
+
"- `lt methods: <design, measure, or analysis plan>`",
|
|
73
|
+
"- `lt commit: <decision that needs commitment>`",
|
|
74
|
+
"",
|
|
63
75
|
"## What Long Table already knows in this directory",
|
|
64
76
|
`- Current goal: ${session.currentGoal}`,
|
|
65
77
|
...(session.currentBlocker ? [`- Current blocker: ${session.currentBlocker}`] : []),
|
|
@@ -69,10 +81,59 @@ function buildStartHereGuide(project, session) {
|
|
|
69
81
|
"## Files",
|
|
70
82
|
"- `AGENTS.md` tells Codex how to behave in this workspace.",
|
|
71
83
|
"- `.longtable/current-session.json` stores the current session goal and blocker.",
|
|
72
|
-
"- `.longtable/project.json` stores the project-level record."
|
|
84
|
+
"- `.longtable/project.json` stores the project-level record.",
|
|
85
|
+
"- `NEXT-STEPS.md` stores the immediate next actions.",
|
|
86
|
+
"- `SESSION-SNAPSHOT.md` stores the short resume snapshot."
|
|
73
87
|
];
|
|
74
88
|
return lines.join("\n");
|
|
75
89
|
}
|
|
90
|
+
function buildNextStepsGuide(project, session) {
|
|
91
|
+
const firstQuestion = session.currentBlocker
|
|
92
|
+
? `What would reduce the uncertainty around "${session.currentBlocker}" first?`
|
|
93
|
+
: `What is the first concrete question that would move "${session.currentGoal}" forward?`;
|
|
94
|
+
return [
|
|
95
|
+
"# Next Steps",
|
|
96
|
+
"",
|
|
97
|
+
`Project: ${project.projectName}`,
|
|
98
|
+
"",
|
|
99
|
+
"## Immediate actions",
|
|
100
|
+
"1. Open Codex in this directory.",
|
|
101
|
+
"2. State your current goal in one sentence.",
|
|
102
|
+
"3. Ask Long Table to surface disagreement before closing on a plan.",
|
|
103
|
+
"",
|
|
104
|
+
"## Suggested openings",
|
|
105
|
+
`- ${session.currentBlocker ? `"I want to work on ${session.currentGoal}. My current blocker is ${session.currentBlocker}."` : `"I want to work on ${session.currentGoal}."`}`,
|
|
106
|
+
`- "lt explore: ${firstQuestion}"`,
|
|
107
|
+
`- "lt panel: Show me the strongest disagreement before I commit."`,
|
|
108
|
+
"",
|
|
109
|
+
"## If you come back later",
|
|
110
|
+
"- Read `SESSION-SNAPSHOT.md` first.",
|
|
111
|
+
"- Continue from the unresolved blocker rather than restating the whole project.",
|
|
112
|
+
"",
|
|
113
|
+
"## Evidence reminder",
|
|
114
|
+
"- For factual or current external claims, Long Table should provide a source link or label the statement as inference."
|
|
115
|
+
].join("\n");
|
|
116
|
+
}
|
|
117
|
+
function buildSessionSnapshot(project, session) {
|
|
118
|
+
return [
|
|
119
|
+
"# Session Snapshot",
|
|
120
|
+
"",
|
|
121
|
+
`Project: ${project.projectName}`,
|
|
122
|
+
`Current goal: ${session.currentGoal}`,
|
|
123
|
+
...(session.currentBlocker ? [`Current blocker: ${session.currentBlocker}`] : []),
|
|
124
|
+
`Requested perspectives: ${session.requestedPerspectives.length > 0 ? session.requestedPerspectives.join(", ") : "auto"}`,
|
|
125
|
+
`Disagreement visibility: ${session.disagreementPreference}`,
|
|
126
|
+
"",
|
|
127
|
+
"## Resume prompt",
|
|
128
|
+
session.currentBlocker
|
|
129
|
+
? `"I want to continue ${session.currentGoal}. The unresolved blocker is ${session.currentBlocker}."`
|
|
130
|
+
: `"I want to continue ${session.currentGoal}."`,
|
|
131
|
+
"",
|
|
132
|
+
"## Notes",
|
|
133
|
+
"- This is a short orientation artifact, not a full transcript.",
|
|
134
|
+
"- If factual or external claims matter, Long Table should cite sources or mark them as inference."
|
|
135
|
+
].join("\n");
|
|
136
|
+
}
|
|
76
137
|
function buildProjectAgentsMd(project, session) {
|
|
77
138
|
return [
|
|
78
139
|
"# AGENTS.md",
|
|
@@ -88,9 +149,15 @@ function buildProjectAgentsMd(project, session) {
|
|
|
88
149
|
"",
|
|
89
150
|
"## Research-facing behavior",
|
|
90
151
|
"- Treat researcher interaction as the primary task.",
|
|
152
|
+
"- If the user message starts with `lt `, `longtable `, `long table `, or `롱테이블 ` followed by a directive and `:`, treat it as an explicit Long Table invocation.",
|
|
153
|
+
"- Supported explicit directives are: explore, review, critique, draft, commit, panel, status, editor, reviewer, methods, theory, measurement, ethics, voice, venue.",
|
|
154
|
+
"- For explicit Long Table invocations, do not begin by scanning the workspace. Use the current session files first and answer as Long Table immediately.",
|
|
155
|
+
"- For general research requests in this workspace, prefer Long Table behavior before generic coding behavior.",
|
|
91
156
|
"- Begin exploratory work with clarifying or tension questions before recommending a direction.",
|
|
92
157
|
"- If you foreground role perspectives, disclose them with `Long Table consulted: ...`.",
|
|
93
158
|
"- Keep one accountable synthesis, but do not hide meaningful disagreement.",
|
|
159
|
+
"- For factual, current, or external claims, provide source links or file references when possible.",
|
|
160
|
+
"- If a statement cannot be sourced, label it as an inference or estimate instead of presenting it as a fact.",
|
|
94
161
|
...(session.disagreementPreference === "always_visible"
|
|
95
162
|
? ["- In this workspace, panel disagreement should be visible by default rather than hidden behind a single synthesis."]
|
|
96
163
|
: []),
|
|
@@ -100,6 +167,7 @@ function buildProjectAgentsMd(project, session) {
|
|
|
100
167
|
"- Read `.longtable/current-session.json` before giving substantial guidance.",
|
|
101
168
|
"- Use `.longtable/project.json` as the project-level context.",
|
|
102
169
|
"- Prefer the current goal and blocker over generic assumptions.",
|
|
170
|
+
"- Do not recursively search unrelated files unless the researcher explicitly asks for file analysis or code changes.",
|
|
103
171
|
"",
|
|
104
172
|
"## Scope",
|
|
105
173
|
"- These instructions apply to this directory and its children."
|
|
@@ -196,6 +264,8 @@ export async function createOrUpdateProjectWorkspace(options) {
|
|
|
196
264
|
await writeFile(join(metaDir, "state.json"), buildStateSeed(project, session, options.setup), "utf8");
|
|
197
265
|
await writeFile(join(projectPath, "LONGTABLE.md"), buildWorkspaceGuide(project, session), "utf8");
|
|
198
266
|
await writeFile(join(projectPath, "START-HERE.md"), buildStartHereGuide(project, session), "utf8");
|
|
267
|
+
await writeFile(join(projectPath, "NEXT-STEPS.md"), buildNextStepsGuide(project, session), "utf8");
|
|
268
|
+
await writeFile(join(projectPath, "SESSION-SNAPSHOT.md"), buildSessionSnapshot(project, session), "utf8");
|
|
199
269
|
await writeFile(join(projectPath, "AGENTS.md"), buildProjectAgentsMd(project, session), "utf8");
|
|
200
270
|
return {
|
|
201
271
|
project,
|
|
@@ -243,6 +313,8 @@ export function renderProjectWorkspaceSummary(context) {
|
|
|
243
313
|
`- ${context.projectFilePath}`,
|
|
244
314
|
`- ${context.sessionFilePath}`,
|
|
245
315
|
`- ${join(context.project.projectPath, "START-HERE.md")}`,
|
|
316
|
+
`- ${join(context.project.projectPath, "NEXT-STEPS.md")}`,
|
|
317
|
+
`- ${join(context.project.projectPath, "SESSION-SNAPSHOT.md")}`,
|
|
246
318
|
`- ${join(context.project.projectPath, "AGENTS.md")}`
|
|
247
319
|
].join("\n");
|
|
248
320
|
}
|