@longtable/cli 0.1.30 → 0.1.32
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 +16 -13
- package/dist/cli.js +464 -493
- package/dist/project-session.d.ts +104 -2
- package/dist/project-session.js +293 -11
- package/dist/prompt-aliases.js +5 -5
- package/dist/prompt-renderer.d.ts +11 -0
- package/dist/prompt-renderer.js +130 -0
- package/dist/search/publisher-access.js +1 -1
- package/dist/search/sources.js +2 -2
- package/package.json +8 -7
package/dist/cli.js
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
3
3
|
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
4
4
|
import { execSync } from "node:child_process";
|
|
5
|
-
import { emitKeypressEvents } from "node:readline";
|
|
6
5
|
import { createInterface } from "node:readline/promises";
|
|
7
6
|
import { stdin as input, stdout as output, cwd, env, exit } from "node:process";
|
|
8
7
|
import { dirname, join, resolve } from "node:path";
|
|
@@ -18,6 +17,7 @@ import { PERSONA_DEFINITIONS, listRoleDefinitions } from "./personas.js";
|
|
|
18
17
|
import { buildPanelFallback, renderPanelSummary } from "./panel.js";
|
|
19
18
|
import { appendInvocationRecordToWorkspace, assertWorkspaceNotBlocked, answerWorkspaceQuestion, createWorkspaceFollowUpQuestions, createWorkspaceQuestion, createOrUpdateProjectWorkspace, inspectProjectWorkspace, loadWorkspaceState, loadProjectContextFromDirectory, renderProjectWorkspaceSummary, syncCurrentWorkspaceView } from "./project-session.js";
|
|
20
19
|
import { buildTeamDebate, buildTeamReview, renderTeamDebateSummary } from "./debate.js";
|
|
20
|
+
import { createPromptRenderer } from "./prompt-renderer.js";
|
|
21
21
|
const VALID_MODES = new Set([
|
|
22
22
|
"explore",
|
|
23
23
|
"review",
|
|
@@ -43,7 +43,7 @@ const ANSI = {
|
|
|
43
43
|
green: "\u001B[32m"
|
|
44
44
|
};
|
|
45
45
|
const LONGTABLE_MCP_SERVER_NAME = "longtable-state";
|
|
46
|
-
const LONGTABLE_MCP_PACKAGE_VERSION = "0.1.
|
|
46
|
+
const LONGTABLE_MCP_PACKAGE_VERSION = "0.1.32";
|
|
47
47
|
const LONGTABLE_MCP_MARKER_START = "# LongTable state MCP START";
|
|
48
48
|
const LONGTABLE_MCP_MARKER_END = "# LongTable state MCP END";
|
|
49
49
|
function style(text, prefix) {
|
|
@@ -68,6 +68,20 @@ function renderBrandBanner(title, subtitle) {
|
|
|
68
68
|
}
|
|
69
69
|
return lines.join("\n");
|
|
70
70
|
}
|
|
71
|
+
function renderInterviewLaunchSteps(provider) {
|
|
72
|
+
const command = provider === "codex" ? "codex" : "claude";
|
|
73
|
+
return renderSectionCard("LongTable Interview", [
|
|
74
|
+
"Setup is permission and runtime calibration, not the research interview.",
|
|
75
|
+
"The first research conversation now happens inside the provider so the model can listen, reflect, and ask one natural-language follow-up at a time.",
|
|
76
|
+
"",
|
|
77
|
+
"Next:",
|
|
78
|
+
"1. cd \"<research-folder>\"",
|
|
79
|
+
`2. run \`${command}\``,
|
|
80
|
+
"3. invoke `$longtable-interview`",
|
|
81
|
+
"",
|
|
82
|
+
"The interview will create or resume `.longtable/`, build a First Research Shape, and use option UI only for the final confirmation."
|
|
83
|
+
]);
|
|
84
|
+
}
|
|
71
85
|
function renderProgressBar(current, total) {
|
|
72
86
|
const width = 10;
|
|
73
87
|
const filled = Math.max(1, Math.round((current / total) * width));
|
|
@@ -77,11 +91,11 @@ function usage() {
|
|
|
77
91
|
return [
|
|
78
92
|
"Usage:",
|
|
79
93
|
" Run `longtable ...` in your terminal, not inside the Codex chat box.",
|
|
80
|
-
"
|
|
94
|
+
" LongTable research starts inside Codex or Claude with `$longtable-interview` after setup.",
|
|
81
95
|
"",
|
|
82
96
|
" longtable setup [--provider codex|claude] [--install-scope user|project|none] [--surfaces cli_only|skills|skills_mcp|skills_mcp_sentinel] [--intervention advisory|balanced|strong] [--checkpoint-ui off|interactive|strong] [--workspace create|later] [--project-dir <path>] [--json] [--dir <path>] [--skills-dir <path>] [--runtime-path <file>] [--setup-path <file>]",
|
|
83
97
|
" longtable init [deprecated alias for setup; full legacy flags still supported for automation]",
|
|
84
|
-
" longtable start [--path <dir>] [--name <project>] [--goal <text>] [--blocker <text>] [--research-object research_question|theory_framework|measurement_instrument|study_design|analysis_plan|manuscript] [--gap-risk known_gap|suspected_tacit_assumptions|diagnose] [--protected-decision theory|measurement|method|evidence_citation|authorship_voice|submission_public_sharing] [--perspectives <role[,role]>] [--disagreement synthesis_only|show_on_conflict|always_visible] [--setup <path>] [--json]",
|
|
98
|
+
" longtable start [deprecated fallback] [--path <dir>] [--name <project>] [--goal <text>] [--blocker <text>] [--research-object research_question|theory_framework|measurement_instrument|study_design|analysis_plan|manuscript] [--gap-risk known_gap|suspected_tacit_assumptions|diagnose] [--protected-decision theory|measurement|method|evidence_citation|authorship_voice|submission_public_sharing] [--perspectives <role[,role]>] [--disagreement synthesis_only|show_on_conflict|always_visible] [--setup <path>] [--json] [--no-interview]",
|
|
85
99
|
" longtable resume [--cwd <path>] [--json]",
|
|
86
100
|
" longtable doctor [--cwd <path>] [--fix] [--json] [--codex-dir <path>] [--codex-config <path>] [--claude-dir <path>] [--codex-prompts-dir <path>] [--codex-runtime-path <file>] [--claude-runtime-path <file>]",
|
|
87
101
|
" longtable status [--cwd <path>] [--fix] [--json] [--codex-dir <path>] [--codex-config <path>] [--claude-dir <path>] [--codex-prompts-dir <path>] [--codex-runtime-path <file>] [--claude-runtime-path <file>]",
|
|
@@ -114,9 +128,9 @@ function usage() {
|
|
|
114
128
|
"",
|
|
115
129
|
"Examples:",
|
|
116
130
|
" longtable setup --provider codex",
|
|
117
|
-
"
|
|
118
|
-
" longtable
|
|
119
|
-
"
|
|
131
|
+
" cd \"<research-folder>\" && codex",
|
|
132
|
+
" $longtable-interview",
|
|
133
|
+
" longtable start --no-interview --path ~/Research/My-Project --name \"AI Adoption Meta-Analysis\" --goal \"Narrow the review question\"",
|
|
120
134
|
" longtable doctor",
|
|
121
135
|
" longtable roles",
|
|
122
136
|
" longtable ask --prompt \"연구를 시작하고 싶어. 지금 어디서부터 좁혀야 할지 모르겠어.\"",
|
|
@@ -171,11 +185,6 @@ function parseArgs(argv) {
|
|
|
171
185
|
}
|
|
172
186
|
return { command, subcommand, values };
|
|
173
187
|
}
|
|
174
|
-
function renderChoices(choices) {
|
|
175
|
-
return choices
|
|
176
|
-
.map((choice, index) => `${index + 1}. ${choice.label} — ${choice.description}`)
|
|
177
|
-
.join("\n");
|
|
178
|
-
}
|
|
179
188
|
function buildSetupFlowChoices() {
|
|
180
189
|
return [
|
|
181
190
|
{
|
|
@@ -219,23 +228,6 @@ function questionSection(questionId) {
|
|
|
219
228
|
function formatModeLabel(mode) {
|
|
220
229
|
return `${mode[0].toUpperCase()}${mode.slice(1)}`;
|
|
221
230
|
}
|
|
222
|
-
function moveCursorUp(lines) {
|
|
223
|
-
return lines > 0 ? `\u001B[${lines}A` : "";
|
|
224
|
-
}
|
|
225
|
-
function clearLine() {
|
|
226
|
-
return "\u001B[2K\r";
|
|
227
|
-
}
|
|
228
|
-
function renderArrowMenu(prompt, choices, selectedIndex) {
|
|
229
|
-
const lines = [style(prompt, ANSI.bold), style("Use ↑/↓ and Enter.", ANSI.dim)];
|
|
230
|
-
for (let index = 0; index < choices.length; index += 1) {
|
|
231
|
-
const prefix = index === selectedIndex ? style(">", `${ANSI.bold}${ANSI.green}`) : " ";
|
|
232
|
-
lines.push(`${prefix} ${choices[index].label} - ${choices[index].description}`);
|
|
233
|
-
}
|
|
234
|
-
return lines.join("\n");
|
|
235
|
-
}
|
|
236
|
-
function countRenderedLines(text) {
|
|
237
|
-
return text.split("\n").length;
|
|
238
|
-
}
|
|
239
231
|
function stripWrappingQuotes(value) {
|
|
240
232
|
const trimmed = value.trim();
|
|
241
233
|
if ((trimmed.startsWith("\"") && trimmed.endsWith("\"")) ||
|
|
@@ -302,186 +294,14 @@ async function verifyWritableWorkspaceParent(projectPath) {
|
|
|
302
294
|
].join("\n"));
|
|
303
295
|
}
|
|
304
296
|
}
|
|
305
|
-
async function
|
|
306
|
-
|
|
307
|
-
const answer = await rl.question(`${prompt}\n${renderChoices(choices)}\nSelect one number: `);
|
|
308
|
-
const numeric = Number(answer.trim());
|
|
309
|
-
if (!Number.isInteger(numeric) || numeric < 1 || numeric > choices.length) {
|
|
310
|
-
console.log("Invalid selection. Enter one of the listed numbers.");
|
|
311
|
-
continue;
|
|
312
|
-
}
|
|
313
|
-
const choice = choices[numeric - 1];
|
|
314
|
-
if (choice.fallbackToText) {
|
|
315
|
-
const freeText = await rl.question("Type your custom value: ");
|
|
316
|
-
if (!freeText.trim()) {
|
|
317
|
-
console.log("Custom value cannot be empty.");
|
|
318
|
-
continue;
|
|
319
|
-
}
|
|
320
|
-
return freeText.trim();
|
|
321
|
-
}
|
|
322
|
-
return choice.id;
|
|
323
|
-
}
|
|
297
|
+
async function promptText(prompt, required) {
|
|
298
|
+
return createPromptRenderer().text(prompt, { required });
|
|
324
299
|
}
|
|
325
|
-
async function
|
|
326
|
-
|
|
327
|
-
const answer = (await rl.question(`${prompt}\n> `)).trim();
|
|
328
|
-
if (!required) {
|
|
329
|
-
return answer || undefined;
|
|
330
|
-
}
|
|
331
|
-
if (answer) {
|
|
332
|
-
return answer;
|
|
333
|
-
}
|
|
334
|
-
console.log("This answer cannot be empty.");
|
|
335
|
-
}
|
|
300
|
+
async function promptChoice(prompt, choices) {
|
|
301
|
+
return createPromptRenderer().select(prompt, choices);
|
|
336
302
|
}
|
|
337
|
-
async function
|
|
338
|
-
|
|
339
|
-
if (!stream.isTTY || !output.isTTY) {
|
|
340
|
-
return promptChoiceByNumber(rl, prompt, choices);
|
|
341
|
-
}
|
|
342
|
-
const previousRawMode = stream.isRaw;
|
|
343
|
-
let selectedIndex = 0;
|
|
344
|
-
let lastRenderLineCount = 0;
|
|
345
|
-
return await new Promise((resolve, reject) => {
|
|
346
|
-
function draw(first = false) {
|
|
347
|
-
const renderedText = renderArrowMenu(prompt, choices, selectedIndex);
|
|
348
|
-
const rendered = renderedText.split("\n");
|
|
349
|
-
if (!first && lastRenderLineCount > 0) {
|
|
350
|
-
output.write(moveCursorUp(lastRenderLineCount));
|
|
351
|
-
}
|
|
352
|
-
for (const line of rendered) {
|
|
353
|
-
output.write(clearLine());
|
|
354
|
-
output.write(`${line}\n`);
|
|
355
|
-
}
|
|
356
|
-
lastRenderLineCount = countRenderedLines(renderedText);
|
|
357
|
-
}
|
|
358
|
-
function cleanup() {
|
|
359
|
-
stream.off("keypress", onKeypress);
|
|
360
|
-
if (stream.isTTY) {
|
|
361
|
-
stream.setRawMode(previousRawMode ?? false);
|
|
362
|
-
}
|
|
363
|
-
output.write("\u001B[?25h");
|
|
364
|
-
}
|
|
365
|
-
async function handleChoice(choice) {
|
|
366
|
-
cleanup();
|
|
367
|
-
if (choice.fallbackToText) {
|
|
368
|
-
const freeText = await rl.question("Type your custom value: ");
|
|
369
|
-
if (!freeText.trim()) {
|
|
370
|
-
resolve(await promptChoiceWithArrows(rl, prompt, choices));
|
|
371
|
-
return;
|
|
372
|
-
}
|
|
373
|
-
resolve(freeText.trim());
|
|
374
|
-
return;
|
|
375
|
-
}
|
|
376
|
-
resolve(choice.id);
|
|
377
|
-
}
|
|
378
|
-
function onKeypress(_, key) {
|
|
379
|
-
if (key.ctrl && key.name === "c") {
|
|
380
|
-
cleanup();
|
|
381
|
-
reject(new Error("Setup cancelled."));
|
|
382
|
-
return;
|
|
383
|
-
}
|
|
384
|
-
if (key.name === "up") {
|
|
385
|
-
selectedIndex = selectedIndex === 0 ? choices.length - 1 : selectedIndex - 1;
|
|
386
|
-
draw();
|
|
387
|
-
return;
|
|
388
|
-
}
|
|
389
|
-
if (key.name === "down") {
|
|
390
|
-
selectedIndex = selectedIndex === choices.length - 1 ? 0 : selectedIndex + 1;
|
|
391
|
-
draw();
|
|
392
|
-
return;
|
|
393
|
-
}
|
|
394
|
-
if (key.name === "return") {
|
|
395
|
-
void handleChoice(choices[selectedIndex]);
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
emitKeypressEvents(stream);
|
|
399
|
-
stream.setRawMode(true);
|
|
400
|
-
output.write("\u001B[?25l");
|
|
401
|
-
draw(true);
|
|
402
|
-
stream.on("keypress", onKeypress);
|
|
403
|
-
});
|
|
404
|
-
}
|
|
405
|
-
async function promptChoice(rl, prompt, choices) {
|
|
406
|
-
return promptChoiceWithArrows(rl, prompt, choices);
|
|
407
|
-
}
|
|
408
|
-
async function promptMultiChoice(rl, prompt, choices) {
|
|
409
|
-
const stream = input;
|
|
410
|
-
if (!stream.isTTY || !output.isTTY) {
|
|
411
|
-
const answer = await rl.question(`${prompt}\nType comma-separated ids or leave blank for auto.\n> `);
|
|
412
|
-
return answer
|
|
413
|
-
.split(",")
|
|
414
|
-
.map((part) => part.trim())
|
|
415
|
-
.filter(Boolean);
|
|
416
|
-
}
|
|
417
|
-
const previousRawMode = stream.isRaw;
|
|
418
|
-
let selectedIndex = 0;
|
|
419
|
-
const selected = new Set();
|
|
420
|
-
let lastRenderLineCount = 0;
|
|
421
|
-
return await new Promise((resolvePromise, reject) => {
|
|
422
|
-
function draw(first = false) {
|
|
423
|
-
const lines = [prompt, "Use ↑/↓, Space to toggle, and Enter to confirm."];
|
|
424
|
-
for (let index = 0; index < choices.length; index += 1) {
|
|
425
|
-
const choice = choices[index];
|
|
426
|
-
const pointer = index === selectedIndex ? ">" : " ";
|
|
427
|
-
const marker = selected.has(choice.id) ? "[x]" : "[ ]";
|
|
428
|
-
lines.push(`${pointer} ${marker} ${choice.label} - ${choice.description}`);
|
|
429
|
-
}
|
|
430
|
-
const renderedText = lines.join("\n");
|
|
431
|
-
if (!first && lastRenderLineCount > 0) {
|
|
432
|
-
output.write(moveCursorUp(lastRenderLineCount));
|
|
433
|
-
}
|
|
434
|
-
for (const line of lines) {
|
|
435
|
-
output.write(clearLine());
|
|
436
|
-
output.write(`${line}\n`);
|
|
437
|
-
}
|
|
438
|
-
lastRenderLineCount = countRenderedLines(renderedText);
|
|
439
|
-
}
|
|
440
|
-
function cleanup() {
|
|
441
|
-
stream.off("keypress", onKeypress);
|
|
442
|
-
if (stream.isTTY) {
|
|
443
|
-
stream.setRawMode(previousRawMode ?? false);
|
|
444
|
-
}
|
|
445
|
-
output.write("\u001B[?25h");
|
|
446
|
-
}
|
|
447
|
-
function onKeypress(_, key) {
|
|
448
|
-
if (key.ctrl && key.name === "c") {
|
|
449
|
-
cleanup();
|
|
450
|
-
reject(new Error("Setup cancelled."));
|
|
451
|
-
return;
|
|
452
|
-
}
|
|
453
|
-
if (key.name === "up") {
|
|
454
|
-
selectedIndex = selectedIndex === 0 ? choices.length - 1 : selectedIndex - 1;
|
|
455
|
-
draw();
|
|
456
|
-
return;
|
|
457
|
-
}
|
|
458
|
-
if (key.name === "down") {
|
|
459
|
-
selectedIndex = selectedIndex === choices.length - 1 ? 0 : selectedIndex + 1;
|
|
460
|
-
draw();
|
|
461
|
-
return;
|
|
462
|
-
}
|
|
463
|
-
if (key.name === "space") {
|
|
464
|
-
const id = choices[selectedIndex].id;
|
|
465
|
-
if (selected.has(id)) {
|
|
466
|
-
selected.delete(id);
|
|
467
|
-
}
|
|
468
|
-
else {
|
|
469
|
-
selected.add(id);
|
|
470
|
-
}
|
|
471
|
-
draw();
|
|
472
|
-
return;
|
|
473
|
-
}
|
|
474
|
-
if (key.name === "return") {
|
|
475
|
-
cleanup();
|
|
476
|
-
resolvePromise([...selected]);
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
emitKeypressEvents(stream);
|
|
480
|
-
stream.setRawMode(true);
|
|
481
|
-
output.write("\u001B[?25l");
|
|
482
|
-
draw(true);
|
|
483
|
-
stream.on("keypress", onKeypress);
|
|
484
|
-
});
|
|
303
|
+
async function promptMultiChoice(prompt, choices) {
|
|
304
|
+
return createPromptRenderer().multiselect(prompt, choices);
|
|
485
305
|
}
|
|
486
306
|
function hasCompleteFlagInput(args) {
|
|
487
307
|
const required = ["provider", "career-stage", "experience", "checkpoint"];
|
|
@@ -516,58 +336,52 @@ function toSetupAnswers(args) {
|
|
|
516
336
|
};
|
|
517
337
|
}
|
|
518
338
|
async function collectInteractiveAnswers(initialFlow) {
|
|
519
|
-
const
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
const
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
if (!value) {
|
|
543
|
-
continue;
|
|
544
|
-
}
|
|
545
|
-
if (question.id === "careerStage")
|
|
546
|
-
answers.careerStage = value;
|
|
547
|
-
if (question.id === "experienceLevel")
|
|
548
|
-
answers.experienceLevel = value;
|
|
549
|
-
if (question.id === "preferredCheckpointIntensity") {
|
|
550
|
-
answers.preferredCheckpointIntensity = value;
|
|
551
|
-
}
|
|
552
|
-
if (question.id === "humanAuthorshipSignal" && value !== "other") {
|
|
553
|
-
answers.humanAuthorshipSignal = value;
|
|
554
|
-
}
|
|
555
|
-
if (question.id === "preferredEntryMode")
|
|
556
|
-
answers.preferredEntryMode = value;
|
|
557
|
-
if (question.id === "weakestDomain")
|
|
558
|
-
answers.weakestDomain = value;
|
|
559
|
-
if (question.id === "panelPreference")
|
|
560
|
-
answers.panelPreference = value;
|
|
339
|
+
const flow = initialFlow ??
|
|
340
|
+
(await promptChoice("How would you like to set up LongTable?", buildSetupFlowChoices()));
|
|
341
|
+
console.log("");
|
|
342
|
+
console.log(renderSetupHeader(flow));
|
|
343
|
+
console.log("");
|
|
344
|
+
const provider = await promptChoice("Which provider do you want to configure?", buildProviderChoices());
|
|
345
|
+
const answers = {
|
|
346
|
+
field: "unspecified",
|
|
347
|
+
currentProjectType: "unspecified research task"
|
|
348
|
+
};
|
|
349
|
+
const questions = buildQuickSetupFlow(flow);
|
|
350
|
+
for (let index = 0; index < questions.length; index += 1) {
|
|
351
|
+
const question = questions[index];
|
|
352
|
+
const prompt = renderQuestionHeader(index + 1, questions.length, questionSection(question.id), question.prompt);
|
|
353
|
+
let value;
|
|
354
|
+
if (question.kind === "text") {
|
|
355
|
+
value = await promptText(prompt, question.required);
|
|
356
|
+
}
|
|
357
|
+
else if (question.choices) {
|
|
358
|
+
value = await promptChoice(prompt, question.choices);
|
|
359
|
+
}
|
|
360
|
+
if (!value) {
|
|
361
|
+
continue;
|
|
561
362
|
}
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
answers
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
363
|
+
if (question.id === "careerStage")
|
|
364
|
+
answers.careerStage = value;
|
|
365
|
+
if (question.id === "experienceLevel")
|
|
366
|
+
answers.experienceLevel = value;
|
|
367
|
+
if (question.id === "preferredCheckpointIntensity") {
|
|
368
|
+
answers.preferredCheckpointIntensity = value;
|
|
369
|
+
}
|
|
370
|
+
if (question.id === "humanAuthorshipSignal" && value !== "other") {
|
|
371
|
+
answers.humanAuthorshipSignal = value;
|
|
372
|
+
}
|
|
373
|
+
if (question.id === "preferredEntryMode")
|
|
374
|
+
answers.preferredEntryMode = value;
|
|
375
|
+
if (question.id === "weakestDomain")
|
|
376
|
+
answers.weakestDomain = value;
|
|
377
|
+
if (question.id === "panelPreference")
|
|
378
|
+
answers.panelPreference = value;
|
|
570
379
|
}
|
|
380
|
+
return {
|
|
381
|
+
flow,
|
|
382
|
+
provider,
|
|
383
|
+
answers: answers
|
|
384
|
+
};
|
|
571
385
|
}
|
|
572
386
|
function buildPermissionSetupChoices() {
|
|
573
387
|
return {
|
|
@@ -647,13 +461,13 @@ function buildPermissionSetupChoices() {
|
|
|
647
461
|
workspace: [
|
|
648
462
|
{
|
|
649
463
|
id: "create",
|
|
650
|
-
label: "
|
|
651
|
-
description: "Why:
|
|
464
|
+
label: "Show interview launch steps",
|
|
465
|
+
description: "Why: research should start inside the provider. What you get: setup finishes with Codex/Claude + $longtable-interview steps. Tradeoff: workspace creation waits for the in-provider interview."
|
|
652
466
|
},
|
|
653
467
|
{
|
|
654
468
|
id: "later",
|
|
655
469
|
label: "No, prepare runtime only",
|
|
656
|
-
description: "Why: keeps setup short. What you get: runtime support without project state. Tradeoff: no durable research memory until `
|
|
470
|
+
description: "Why: keeps setup short. What you get: runtime support without project state. Tradeoff: no durable research memory until `$longtable-interview` creates or resumes a workspace."
|
|
657
471
|
}
|
|
658
472
|
]
|
|
659
473
|
};
|
|
@@ -735,129 +549,129 @@ function checkpointUiIntervention(intervention, checkpointUi) {
|
|
|
735
549
|
}
|
|
736
550
|
async function runSetup(args) {
|
|
737
551
|
const json = args.json === true;
|
|
738
|
-
const
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
552
|
+
const provider = (typeof args.provider === "string"
|
|
553
|
+
? (args.provider === "claude" ? "claude" : "codex")
|
|
554
|
+
: await promptChoice("Which provider should LongTable configure?", buildProviderChoices()));
|
|
555
|
+
const choices = buildPermissionSetupChoices();
|
|
556
|
+
const installScope = parseSetupInstallScope(args["install-scope"]) ?? await promptChoice("Where may LongTable install runtime support?", choices.installScope);
|
|
557
|
+
const surfaces = parseSetupSurface(args.surfaces) ?? await promptChoice([
|
|
558
|
+
"Which LongTable runtime surfaces should be enabled?",
|
|
559
|
+
"This is a permission choice because skills, MCP, and sentinel support write provider-facing runtime files."
|
|
560
|
+
].join("\n"), choices.surfaces);
|
|
561
|
+
const intervention = parseSetupIntervention(args.intervention) ?? await promptChoice("How strongly may LongTable interrupt research decisions?", choices.intervention);
|
|
562
|
+
const parsedCheckpointUi = parseSetupCheckpointUi(args["checkpoint-ui"]);
|
|
563
|
+
const checkpointUiEligible = provider === "codex" && installScope !== "none" && shouldInstallMcp(installScope, surfaces);
|
|
564
|
+
if (parsedCheckpointUi && checkpointUiRequiresMcp(parsedCheckpointUi) && !checkpointUiEligible) {
|
|
565
|
+
throw new Error("`--checkpoint-ui interactive|strong` requires Codex with an MCP runtime surface.");
|
|
566
|
+
}
|
|
567
|
+
const checkpointUi = parsedCheckpointUi ?? (checkpointUiEligible
|
|
568
|
+
? await promptChoice([
|
|
569
|
+
"Should Codex use UI Researcher Checkpoints when MCP elicitation is available?",
|
|
570
|
+
"This writes Codex MCP elicitation approval only when you choose an interactive mode."
|
|
571
|
+
].join("\n"), choices.checkpointUi)
|
|
572
|
+
: "off");
|
|
573
|
+
const workspacePreference = parseSetupWorkspace(args.workspace) ?? await promptChoice("Should LongTable create a project workspace now?", choices.workspace);
|
|
574
|
+
const projectRoot = setupProjectRoot(args);
|
|
575
|
+
const effectiveIntervention = checkpointUiIntervention(intervention, checkpointUi);
|
|
576
|
+
const outputValue = createPersistedSetupOutput({
|
|
577
|
+
field: "unspecified",
|
|
578
|
+
careerStage: "unspecified",
|
|
579
|
+
experienceLevel: "advanced",
|
|
580
|
+
preferredCheckpointIntensity: checkpointIntensityFromIntervention(effectiveIntervention),
|
|
581
|
+
checkpointUiMode: checkpointUi,
|
|
582
|
+
preferredEntryMode: "explore",
|
|
583
|
+
panelPreference: "show_on_conflict"
|
|
584
|
+
}, provider, "quickstart");
|
|
585
|
+
outputValue.initialState.explicitState = {
|
|
586
|
+
...outputValue.initialState.explicitState,
|
|
587
|
+
installScope,
|
|
588
|
+
runtimeSurfaces: surfaces,
|
|
589
|
+
interventionPosture: effectiveIntervention,
|
|
590
|
+
checkpointUiMode: checkpointUi,
|
|
591
|
+
workspaceCreationPreference: workspacePreference,
|
|
592
|
+
officialStartSurface: "$longtable-interview",
|
|
593
|
+
setupPosture: "permission_first",
|
|
594
|
+
teamMode: "panel"
|
|
595
|
+
};
|
|
596
|
+
if (surfaces === "skills_mcp_sentinel") {
|
|
597
|
+
outputValue.initialState.inferredHypotheses.push({
|
|
598
|
+
hypothesis: "Researcher approved advisory Gap/Tacit Sentinel setup.",
|
|
599
|
+
confidence: 0.95,
|
|
600
|
+
evidence: ["Selected Skills + MCP + Sentinel during permission-first setup."],
|
|
601
|
+
status: "confirmed"
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
const setupPath = setupPathForScope(installScope, args, projectRoot);
|
|
605
|
+
const runtimePath = runtimePathForScope(provider, installScope, args, projectRoot);
|
|
606
|
+
const result = installScope === "none"
|
|
607
|
+
? {
|
|
608
|
+
provider,
|
|
609
|
+
setupTarget: await saveSetupOutput(outputValue, setupPath)
|
|
789
610
|
}
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
611
|
+
: await saveSetupAndRuntimeConfig(outputValue, {
|
|
612
|
+
setupPath,
|
|
613
|
+
runtimePath
|
|
614
|
+
});
|
|
615
|
+
const scopedInstallDir = setupInstallDir(provider, installScope, typeof args["skills-dir"] === "string" ? args["skills-dir"] : typeof args.dir === "string" ? args.dir : undefined, projectRoot);
|
|
616
|
+
const installedSkills = !shouldInstallSkills(installScope, surfaces)
|
|
617
|
+
? []
|
|
618
|
+
: provider === "codex"
|
|
619
|
+
? await installCodexSkills(listRoleDefinitions(), scopedInstallDir)
|
|
620
|
+
: await installClaudeSkills(listRoleDefinitions(), scopedInstallDir);
|
|
621
|
+
let mcpInstall;
|
|
622
|
+
if (shouldInstallMcp(installScope, surfaces)) {
|
|
623
|
+
mcpInstall = await installMcpForSetup(provider, {
|
|
624
|
+
...mcpArgsForScope(provider, installScope, args, projectRoot),
|
|
625
|
+
...(provider === "codex" && checkpointUiRequiresMcp(checkpointUi)
|
|
626
|
+
? { "checkpoint-ui": checkpointUi }
|
|
627
|
+
: {})
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
if (json) {
|
|
631
|
+
console.log(JSON.stringify({
|
|
632
|
+
setup: outputValue,
|
|
633
|
+
runtime: result,
|
|
634
|
+
installedSkills: installedSkills.map((skill) => skill.name),
|
|
635
|
+
mcpInstall,
|
|
636
|
+
workspacePreference,
|
|
637
|
+
nextStep: {
|
|
638
|
+
surface: "$longtable-interview",
|
|
639
|
+
command: provider === "codex" ? "codex" : "claude",
|
|
640
|
+
description: "Open the provider in the research folder and invoke `$longtable-interview`."
|
|
796
641
|
}
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
: {})
|
|
814
|
-
});
|
|
815
|
-
}
|
|
816
|
-
if (json) {
|
|
817
|
-
console.log(JSON.stringify({
|
|
818
|
-
setup: outputValue,
|
|
819
|
-
runtime: result,
|
|
820
|
-
installedSkills: installedSkills.map((skill) => skill.name),
|
|
821
|
-
mcpInstall,
|
|
822
|
-
workspacePreference
|
|
823
|
-
}, null, 2));
|
|
824
|
-
return;
|
|
825
|
-
}
|
|
826
|
-
console.log("");
|
|
827
|
-
console.log(renderSetupSummary(outputValue));
|
|
642
|
+
}, null, 2));
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
console.log("");
|
|
646
|
+
console.log(renderSetupSummary(outputValue));
|
|
647
|
+
console.log("");
|
|
648
|
+
if ("runtimeTarget" in result) {
|
|
649
|
+
console.log(renderInstallSummary(result));
|
|
650
|
+
}
|
|
651
|
+
else {
|
|
652
|
+
console.log("LongTable setup summary");
|
|
653
|
+
console.log(`setup path: ${result.setupTarget.path}`);
|
|
654
|
+
console.log("provider files: not installed by researcher choice");
|
|
655
|
+
}
|
|
656
|
+
console.log(`Installed skills: ${installedSkills.length}`);
|
|
657
|
+
if (mcpInstall) {
|
|
828
658
|
console.log("");
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
}
|
|
832
|
-
else {
|
|
833
|
-
console.log("LongTable setup summary");
|
|
834
|
-
console.log(`setup path: ${result.setupTarget.path}`);
|
|
835
|
-
console.log("provider files: not installed by researcher choice");
|
|
836
|
-
}
|
|
837
|
-
console.log(`Installed skills: ${installedSkills.length}`);
|
|
838
|
-
if (mcpInstall) {
|
|
839
|
-
console.log("");
|
|
840
|
-
console.log(renderMcpInstallSummary(mcpInstall));
|
|
841
|
-
if (provider === "codex" && checkpointUiRequiresMcp(checkpointUi)) {
|
|
842
|
-
console.log("");
|
|
843
|
-
console.log("Restart Codex after this config change, then run `longtable doctor` in the project workspace.");
|
|
844
|
-
}
|
|
845
|
-
}
|
|
846
|
-
if (surfaces === "skills_mcp_sentinel") {
|
|
659
|
+
console.log(renderMcpInstallSummary(mcpInstall));
|
|
660
|
+
if (provider === "codex" && checkpointUiRequiresMcp(checkpointUi)) {
|
|
847
661
|
console.log("");
|
|
848
|
-
console.log("
|
|
849
|
-
console.log("Hook installation remains opt-in; LongTable will not install hooks without an explicit hook command.");
|
|
850
|
-
}
|
|
851
|
-
if (workspacePreference === "create") {
|
|
852
|
-
console.log("");
|
|
853
|
-
console.log("Project workspace requested. LongTable will now run `longtable start` with research-object and gap-risk prompts.");
|
|
854
|
-
await runStart({
|
|
855
|
-
setup: result.setupTarget.path
|
|
856
|
-
});
|
|
662
|
+
console.log("Restart Codex after this config change, then run `longtable doctor` in the project workspace.");
|
|
857
663
|
}
|
|
858
664
|
}
|
|
859
|
-
|
|
860
|
-
|
|
665
|
+
if (surfaces === "skills_mcp_sentinel") {
|
|
666
|
+
console.log("");
|
|
667
|
+
console.log("Background sentinel approval recorded.");
|
|
668
|
+
console.log("Hook installation remains opt-in; LongTable will not install hooks without an explicit hook command.");
|
|
669
|
+
}
|
|
670
|
+
console.log("");
|
|
671
|
+
console.log(renderInterviewLaunchSteps(provider));
|
|
672
|
+
if (workspacePreference === "create") {
|
|
673
|
+
console.log("");
|
|
674
|
+
console.log("Workspace launch requested. Open the provider in your research folder and run `$longtable-interview`; the interview will create `.longtable/` there.");
|
|
861
675
|
}
|
|
862
676
|
}
|
|
863
677
|
function perspectiveChoices() {
|
|
@@ -894,6 +708,157 @@ function protectedDecisionChoices() {
|
|
|
894
708
|
{ id: "submission_public_sharing", label: "Submission/public sharing", description: "Do not let public-facing commitments settle quietly." }
|
|
895
709
|
];
|
|
896
710
|
}
|
|
711
|
+
function compactAnswer(value, maxLength = 180) {
|
|
712
|
+
const firstLine = value
|
|
713
|
+
.split(/\r?\n/)
|
|
714
|
+
.map((line) => line.trim())
|
|
715
|
+
.find(Boolean) ?? "";
|
|
716
|
+
return firstLine.length > maxLength ? `${firstLine.slice(0, maxLength - 1)}…` : firstLine;
|
|
717
|
+
}
|
|
718
|
+
function answerIncludes(answer, patterns) {
|
|
719
|
+
return patterns.some((pattern) => pattern.test(answer));
|
|
720
|
+
}
|
|
721
|
+
function classifyStartInterviewSignal(answer) {
|
|
722
|
+
const normalized = answer.toLowerCase();
|
|
723
|
+
if (answerIncludes(normalized, [/\bvoice\b/, /\bauthor(ship)?\b/, /\bwriting\b/, /저자|목소리|문체|글쓰기/])) {
|
|
724
|
+
return "voice";
|
|
725
|
+
}
|
|
726
|
+
if (answerIncludes(normalized, [/\breader\b/, /\breviewer\b/, /\bvenue\b/, /\bjournal\b/, /\baudience\b/, /독자|심사자|저널|학회|대상/])) {
|
|
727
|
+
return "audience";
|
|
728
|
+
}
|
|
729
|
+
if (answerIncludes(normalized, [/\bpaper\b/, /\bproposal\b/, /\bmanuscript\b/, /\bdraft\b/, /\bdeliverable\b/, /논문|제안서|원고|초안|산출물/])) {
|
|
730
|
+
return "artifact";
|
|
731
|
+
}
|
|
732
|
+
if (answerIncludes(normalized, [/\bevidence\b/, /\bdata\b/, /\bsource\b/, /\bcitation\b/, /\bliterature\b/, /\bmeasure\b/, /\bscale\b/, /\binstrument\b/, /근거|자료|데이터|문헌|인용|측정|척도|도구/])) {
|
|
733
|
+
return "evidence";
|
|
734
|
+
}
|
|
735
|
+
if (answerIncludes(normalized, [/\bassum/, /\btacit\b/, /\bimplicit\b/, /\bpremise\b/, /전제|암묵|가정|숨겨진/])) {
|
|
736
|
+
return "assumption";
|
|
737
|
+
}
|
|
738
|
+
if (answerIncludes(normalized, [/\bdecid/, /\bchoose\b/, /\bcommit\b/, /\bsettle\b/, /\block\b/, /결정|선택|확정|고정/])) {
|
|
739
|
+
return "decision_risk";
|
|
740
|
+
}
|
|
741
|
+
return "phenomenon";
|
|
742
|
+
}
|
|
743
|
+
function inferResearchObjectFromAnswers(answers) {
|
|
744
|
+
const joined = answers.join("\n").toLowerCase();
|
|
745
|
+
if (answerIncludes(joined, [/\bmeasure\b/, /\bscale\b/, /\binstrument\b/, /\bvariable\b/, /\bconstruct\b/, /측정|척도|변수|구성개념|도구/])) {
|
|
746
|
+
return "measurement_instrument";
|
|
747
|
+
}
|
|
748
|
+
if (answerIncludes(joined, [/\bmethod\b/, /\bdesign\b/, /\bsample\b/, /\binterview\b/, /\bexperiment\b/, /\bparticipant\b/, /방법|설계|표본|인터뷰|실험|참여자/])) {
|
|
749
|
+
return "study_design";
|
|
750
|
+
}
|
|
751
|
+
if (answerIncludes(joined, [/\btheory\b/, /\bframework\b/, /\bmodel\b/, /\bconcept\b/, /이론|프레임워크|모형|개념/])) {
|
|
752
|
+
return "theory_framework";
|
|
753
|
+
}
|
|
754
|
+
if (answerIncludes(joined, [/\banalysis\b/, /\bmodel\b/, /\bcoding\b/, /\bstatistic\b/, /분석|모델|코딩|통계/])) {
|
|
755
|
+
return "analysis_plan";
|
|
756
|
+
}
|
|
757
|
+
if (answerIncludes(joined, [/\bpaper\b/, /\bmanuscript\b/, /\bdraft\b/, /\bwriting\b/, /논문|원고|초안|글쓰기/])) {
|
|
758
|
+
return "manuscript";
|
|
759
|
+
}
|
|
760
|
+
return "research_question";
|
|
761
|
+
}
|
|
762
|
+
function inferGapRiskFromSignals(signals) {
|
|
763
|
+
if (signals.includes("assumption") || signals.includes("decision_risk")) {
|
|
764
|
+
return "suspected_tacit_assumptions";
|
|
765
|
+
}
|
|
766
|
+
if (signals.includes("evidence")) {
|
|
767
|
+
return "known_gap";
|
|
768
|
+
}
|
|
769
|
+
return "diagnose";
|
|
770
|
+
}
|
|
771
|
+
function inferProtectedDecisionFromAnswers(answers) {
|
|
772
|
+
const joined = answers.join("\n").toLowerCase();
|
|
773
|
+
if (answerIncludes(joined, [/\bmeasure\b/, /\bscale\b/, /\binstrument\b/, /\bvariable\b/, /측정|척도|변수|도구/])) {
|
|
774
|
+
return "measurement";
|
|
775
|
+
}
|
|
776
|
+
if (answerIncludes(joined, [/\bmethod\b/, /\bdesign\b/, /\bsample\b/, /\binterview\b/, /\bexperiment\b/, /방법|설계|표본|인터뷰|실험/])) {
|
|
777
|
+
return "method";
|
|
778
|
+
}
|
|
779
|
+
if (answerIncludes(joined, [/\bevidence\b/, /\bsource\b/, /\bcitation\b/, /\bliterature\b/, /근거|출처|인용|문헌/])) {
|
|
780
|
+
return "evidence_citation";
|
|
781
|
+
}
|
|
782
|
+
if (answerIncludes(joined, [/\bvoice\b/, /\bauthor(ship)?\b/, /\bwriting\b/, /저자|목소리|문체|글쓰기/])) {
|
|
783
|
+
return "authorship_voice";
|
|
784
|
+
}
|
|
785
|
+
if (answerIncludes(joined, [/\bsubmit\b/, /\bpublication\b/, /\bjournal\b/, /\bvenue\b/, /투고|출판|저널|학회|공개/])) {
|
|
786
|
+
return "submission_public_sharing";
|
|
787
|
+
}
|
|
788
|
+
if (answerIncludes(joined, [/\btheory\b/, /\bframework\b/, /\bconstruct\b/, /이론|프레임워크|구성개념/])) {
|
|
789
|
+
return "theory";
|
|
790
|
+
}
|
|
791
|
+
return undefined;
|
|
792
|
+
}
|
|
793
|
+
function uniqueSignals(turns) {
|
|
794
|
+
return [...new Set(turns.map((turn) => turn.signal))];
|
|
795
|
+
}
|
|
796
|
+
function renderStartInterviewPrompt(turn, total, question, context) {
|
|
797
|
+
return [
|
|
798
|
+
`LongTable Start Interview Turn ${turn}/${total}`,
|
|
799
|
+
context ? `Context: ${context}` : undefined,
|
|
800
|
+
question
|
|
801
|
+
].filter(Boolean).join("\n");
|
|
802
|
+
}
|
|
803
|
+
async function collectAdaptiveStartInterview(args) {
|
|
804
|
+
if (!args.needsResearchSeed) {
|
|
805
|
+
return {};
|
|
806
|
+
}
|
|
807
|
+
const createdAt = new Date().toISOString();
|
|
808
|
+
const turns = [];
|
|
809
|
+
async function ask(question, purpose, context) {
|
|
810
|
+
const answer = await promptText(renderStartInterviewPrompt(turns.length + 1, 5, question, context), true);
|
|
811
|
+
if (!answer?.trim()) {
|
|
812
|
+
return undefined;
|
|
813
|
+
}
|
|
814
|
+
turns.push({
|
|
815
|
+
index: turns.length + 1,
|
|
816
|
+
question,
|
|
817
|
+
answer: answer.trim(),
|
|
818
|
+
signal: classifyStartInterviewSignal(answer),
|
|
819
|
+
purpose
|
|
820
|
+
});
|
|
821
|
+
return answer.trim();
|
|
822
|
+
}
|
|
823
|
+
let currentGoal = args.currentGoal;
|
|
824
|
+
let currentBlocker = args.currentBlocker;
|
|
825
|
+
if (!currentGoal) {
|
|
826
|
+
const opening = await ask("What scene, problem, or moment made you want to start this research?", "Open from the researcher's lived entry point instead of a taxonomy.");
|
|
827
|
+
currentGoal = opening ? compactAnswer(opening) : undefined;
|
|
828
|
+
}
|
|
829
|
+
const openingContext = currentGoal ? compactAnswer(currentGoal, 120) : undefined;
|
|
830
|
+
if (!currentBlocker) {
|
|
831
|
+
const blocker = await ask("In that scene, what still feels least explained or hardest to justify?", "Name the first uncertainty without asking the researcher to classify it as theory, method, or measurement.", openingContext);
|
|
832
|
+
currentBlocker = blocker ? compactAnswer(blocker) : undefined;
|
|
833
|
+
}
|
|
834
|
+
const readerContext = currentBlocker ? compactAnswer(currentBlocker, 120) : openingContext;
|
|
835
|
+
await ask("If this research succeeds, what should a reader or reviewer understand differently?", "Locate the audience-facing contribution before proposing a direction.", readerContext);
|
|
836
|
+
await ask("What material would you inspect first to make this research concrete: a case, dataset, text, instrument, draft, or literature trail?", "Convert the opening story into a first inspectable research move.", readerContext);
|
|
837
|
+
const answers = turns.map((turn) => turn.answer);
|
|
838
|
+
const inferredSignals = uniqueSignals(turns);
|
|
839
|
+
const summary = [
|
|
840
|
+
currentGoal ? `Opening: ${compactAnswer(currentGoal, 120)}.` : "",
|
|
841
|
+
currentBlocker ? `First uncertainty: ${compactAnswer(currentBlocker, 120)}.` : "",
|
|
842
|
+
inferredSignals.length > 0 ? `Early lenses: ${inferredSignals.join(", ")}.` : ""
|
|
843
|
+
].filter(Boolean).join(" ");
|
|
844
|
+
return {
|
|
845
|
+
currentGoal,
|
|
846
|
+
currentBlocker,
|
|
847
|
+
startInterview: {
|
|
848
|
+
mode: "adaptive",
|
|
849
|
+
openingStyle: "scene_problem",
|
|
850
|
+
createdAt,
|
|
851
|
+
completedAt: new Date().toISOString(),
|
|
852
|
+
turnCount: turns.length,
|
|
853
|
+
turns,
|
|
854
|
+
inferredSignals,
|
|
855
|
+
summary: summary || "Adaptive start interview completed."
|
|
856
|
+
},
|
|
857
|
+
inferredResearchObject: inferResearchObjectFromAnswers(answers),
|
|
858
|
+
inferredGapRisk: inferGapRiskFromSignals(inferredSignals),
|
|
859
|
+
inferredProtectedDecision: inferProtectedDecisionFromAnswers(answers)
|
|
860
|
+
};
|
|
861
|
+
}
|
|
897
862
|
function normalizePerspectiveList(value) {
|
|
898
863
|
if (!value?.trim()) {
|
|
899
864
|
return [];
|
|
@@ -904,82 +869,79 @@ function normalizePerspectiveList(value) {
|
|
|
904
869
|
.filter(Boolean);
|
|
905
870
|
}
|
|
906
871
|
async function collectProjectInterview(setup, args) {
|
|
872
|
+
const skipResearchInterview = args["no-interview"] === true;
|
|
873
|
+
const providedPerspectives = normalizePerspectiveList(typeof args.perspectives === "string" ? args.perspectives : undefined);
|
|
874
|
+
const providedGoal = typeof args.goal === "string" && args.goal.trim() ? args.goal.trim() : undefined;
|
|
875
|
+
const providedBlocker = typeof args.blocker === "string" && args.blocker.trim() ? args.blocker.trim() : undefined;
|
|
876
|
+
const providedResearchObject = typeof args["research-object"] === "string" && args["research-object"].trim()
|
|
877
|
+
? args["research-object"].trim()
|
|
878
|
+
: undefined;
|
|
879
|
+
const providedGapRisk = typeof args["gap-risk"] === "string" && args["gap-risk"].trim()
|
|
880
|
+
? args["gap-risk"].trim()
|
|
881
|
+
: undefined;
|
|
882
|
+
const providedProtectedDecision = typeof args["protected-decision"] === "string" && args["protected-decision"].trim()
|
|
883
|
+
? args["protected-decision"].trim()
|
|
884
|
+
: undefined;
|
|
907
885
|
const needsInteractivePrompts = !(typeof args.name === "string" && args.name.trim()) ||
|
|
908
886
|
!(typeof args.path === "string" && args.path.trim()) ||
|
|
909
|
-
!
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
"We will create a project workspace and a session memory seed for today's work.",
|
|
924
|
-
"At the end, LongTable will tell you exactly which directory to open in Codex."
|
|
925
|
-
]));
|
|
926
|
-
console.log("");
|
|
927
|
-
}
|
|
928
|
-
const projectName = (typeof args.name === "string" && args.name.trim()) ||
|
|
929
|
-
(await promptText(rl, renderQuestionHeader(1, 9, "Project interview", "What should this project be called?"), true));
|
|
930
|
-
const suggestedParentDir = typeof args.path === "string" && args.path.trim()
|
|
931
|
-
? normalizeUserPath(args.path.trim())
|
|
932
|
-
: homedir();
|
|
933
|
-
const suggestedPath = resolveInteractiveProjectPath(suggestedParentDir, projectName);
|
|
934
|
-
const projectPath = (typeof args.path === "string" && args.path.trim()
|
|
935
|
-
? normalizeUserPath(args.path.trim())
|
|
936
|
-
: resolveInteractiveProjectPath((await promptText(rl, renderQuestionHeader(2, 9, "Project interview", `Which parent directory should contain this project?\nLongTable will create this folder:\n${suggestedPath}`), true)), projectName));
|
|
937
|
-
const currentGoal = (typeof args.goal === "string" && args.goal.trim()) ||
|
|
938
|
-
(await promptText(rl, renderQuestionHeader(3, 9, "Current session", "What are you trying to accomplish in this session?"), true));
|
|
939
|
-
const currentBlocker = (typeof args.blocker === "string" && args.blocker.trim()) ||
|
|
940
|
-
(await promptText(rl, renderQuestionHeader(4, 9, "Current session", "What is the main blocker or uncertainty right now?"), false));
|
|
941
|
-
const researchObject = (typeof args["research-object"] === "string" && args["research-object"].trim()) ||
|
|
942
|
-
await promptChoice(rl, renderQuestionHeader(5, 9, "Research object", "What kind of research object are we protecting right now?"), researchObjectChoices());
|
|
943
|
-
const gapRisk = (typeof args["gap-risk"] === "string" && args["gap-risk"].trim()) ||
|
|
944
|
-
await promptChoice(rl, renderQuestionHeader(6, 9, "Gap/tacit risk", "What is the most likely gap risk at the start of this workspace?"), gapRiskChoices());
|
|
945
|
-
const protectedDecision = (typeof args["protected-decision"] === "string" && args["protected-decision"].trim()) ||
|
|
946
|
-
await promptChoice(rl, renderQuestionHeader(7, 9, "Protected decision", "Which decision should LongTable not let you settle quietly?"), protectedDecisionChoices());
|
|
947
|
-
const requestedPerspectives = normalizePerspectiveList(typeof args.perspectives === "string" ? args.perspectives : undefined).length > 0
|
|
948
|
-
? normalizePerspectiveList(typeof args.perspectives === "string" ? args.perspectives : undefined)
|
|
949
|
-
: await promptMultiChoice(rl, renderQuestionHeader(8, 9, "Perspectives", "Which perspectives do you already know you want at the table? Leave everything unchecked for auto."), perspectiveChoices());
|
|
950
|
-
const disagreementPreference = (typeof args.disagreement === "string" && args.disagreement.trim()) ||
|
|
951
|
-
(await promptChoice(rl, renderQuestionHeader(9, 9, "Disagreement", "How visible should disagreement between perspectives be in this project by default?"), [
|
|
952
|
-
{
|
|
953
|
-
id: "synthesis_only",
|
|
954
|
-
label: "Synthesis only",
|
|
955
|
-
description: "Show one LongTable answer unless I ask for more."
|
|
956
|
-
},
|
|
957
|
-
{
|
|
958
|
-
id: "show_on_conflict",
|
|
959
|
-
label: "Show on conflict",
|
|
960
|
-
description: "Surface disagreement when the perspectives materially diverge."
|
|
961
|
-
},
|
|
962
|
-
{
|
|
963
|
-
id: "always_visible",
|
|
964
|
-
label: "Always visible",
|
|
965
|
-
description: "Keep panel opinions visible by default."
|
|
966
|
-
}
|
|
967
|
-
]));
|
|
968
|
-
return {
|
|
969
|
-
projectName: projectName.trim(),
|
|
970
|
-
projectPath: projectPath.trim(),
|
|
971
|
-
currentGoal: currentGoal.trim(),
|
|
972
|
-
...(currentBlocker?.trim() ? { currentBlocker: currentBlocker.trim() } : {}),
|
|
973
|
-
researchObject: researchObject.trim(),
|
|
974
|
-
gapRisk: gapRisk.trim(),
|
|
975
|
-
protectedDecision: protectedDecision.trim(),
|
|
976
|
-
requestedPerspectives,
|
|
977
|
-
disagreementPreference: disagreementPreference
|
|
978
|
-
};
|
|
979
|
-
}
|
|
980
|
-
finally {
|
|
981
|
-
rl.close();
|
|
887
|
+
(!skipResearchInterview && (!providedGoal ||
|
|
888
|
+
!providedBlocker ||
|
|
889
|
+
!providedResearchObject ||
|
|
890
|
+
!providedGapRisk ||
|
|
891
|
+
!providedProtectedDecision));
|
|
892
|
+
if (needsInteractivePrompts) {
|
|
893
|
+
console.log("");
|
|
894
|
+
console.log(renderBrandBanner("LongTable", "Project workspace interview"));
|
|
895
|
+
console.log("");
|
|
896
|
+
console.log(renderSectionCard("LongTable Project Start", [
|
|
897
|
+
"LongTable will create a workspace and seed today's research memory.",
|
|
898
|
+
"The start interview begins from the scene or problem, then LongTable quietly infers the research shape."
|
|
899
|
+
]));
|
|
900
|
+
console.log("");
|
|
982
901
|
}
|
|
902
|
+
const projectName = (typeof args.name === "string" && args.name.trim()) ||
|
|
903
|
+
(await promptText(renderQuestionHeader(1, 2, "Workspace", "What should this project be called?"), true));
|
|
904
|
+
const suggestedParentDir = typeof args.path === "string" && args.path.trim()
|
|
905
|
+
? normalizeUserPath(args.path.trim())
|
|
906
|
+
: homedir();
|
|
907
|
+
const suggestedPath = resolveInteractiveProjectPath(suggestedParentDir, projectName);
|
|
908
|
+
const projectPath = (typeof args.path === "string" && args.path.trim()
|
|
909
|
+
? normalizeUserPath(args.path.trim())
|
|
910
|
+
: resolveInteractiveProjectPath((await promptText(renderQuestionHeader(2, 2, "Workspace", `Which parent directory should contain this project?\nLongTable will create this folder:\n${suggestedPath}`), true)), projectName));
|
|
911
|
+
const adaptive = skipResearchInterview
|
|
912
|
+
? {}
|
|
913
|
+
: await collectAdaptiveStartInterview({
|
|
914
|
+
currentGoal: providedGoal,
|
|
915
|
+
currentBlocker: providedBlocker,
|
|
916
|
+
needsResearchSeed: !providedGoal ||
|
|
917
|
+
!providedBlocker ||
|
|
918
|
+
!providedResearchObject ||
|
|
919
|
+
!providedGapRisk ||
|
|
920
|
+
!providedProtectedDecision
|
|
921
|
+
});
|
|
922
|
+
const currentGoal = providedGoal ?? adaptive.currentGoal;
|
|
923
|
+
if (!currentGoal?.trim()) {
|
|
924
|
+
throw new Error("LongTable start needs a current research goal or an opening interview answer.");
|
|
925
|
+
}
|
|
926
|
+
const currentBlocker = providedBlocker ?? adaptive.currentBlocker;
|
|
927
|
+
const researchObject = providedResearchObject ?? adaptive.inferredResearchObject;
|
|
928
|
+
const gapRisk = providedGapRisk ?? adaptive.inferredGapRisk;
|
|
929
|
+
const protectedDecision = providedProtectedDecision ?? adaptive.inferredProtectedDecision;
|
|
930
|
+
const disagreementPreference = (typeof args.disagreement === "string" && args.disagreement.trim()
|
|
931
|
+
? args.disagreement.trim()
|
|
932
|
+
: setup.profileSeed.panelPreference ?? "show_on_conflict");
|
|
933
|
+
return {
|
|
934
|
+
projectName: projectName.trim(),
|
|
935
|
+
projectPath: projectPath.trim(),
|
|
936
|
+
currentGoal: currentGoal.trim(),
|
|
937
|
+
...(currentBlocker?.trim() ? { currentBlocker: currentBlocker.trim() } : {}),
|
|
938
|
+
...(researchObject?.trim() ? { researchObject: researchObject.trim() } : {}),
|
|
939
|
+
...(gapRisk?.trim() ? { gapRisk: gapRisk.trim() } : {}),
|
|
940
|
+
...(protectedDecision?.trim() ? { protectedDecision: protectedDecision.trim() } : {}),
|
|
941
|
+
...(adaptive.startInterview ? { startInterview: adaptive.startInterview } : {}),
|
|
942
|
+
requestedPerspectives: providedPerspectives,
|
|
943
|
+
disagreementPreference
|
|
944
|
+
};
|
|
983
945
|
}
|
|
984
946
|
function normalizePersistAnswers(raw) {
|
|
985
947
|
return {
|
|
@@ -2338,30 +2300,24 @@ async function runQuestion(args) {
|
|
|
2338
2300
|
return;
|
|
2339
2301
|
}
|
|
2340
2302
|
if (isInteractiveTerminal()) {
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
console.log(`- current: ${context.currentFilePath}`);
|
|
2360
|
-
return;
|
|
2361
|
-
}
|
|
2362
|
-
finally {
|
|
2363
|
-
rl.close();
|
|
2364
|
-
}
|
|
2303
|
+
console.log(renderBrandBanner("LongTable", "Researcher Checkpoint"));
|
|
2304
|
+
console.log("");
|
|
2305
|
+
const answer = await promptChoice(renderQuestionHeader(1, 1, result.question.prompt.title, result.question.prompt.question), questionRecordToChoices(result.question));
|
|
2306
|
+
const decision = await answerWorkspaceQuestion({
|
|
2307
|
+
context,
|
|
2308
|
+
questionId: result.question.id,
|
|
2309
|
+
answer,
|
|
2310
|
+
provider,
|
|
2311
|
+
surface: "terminal_selector"
|
|
2312
|
+
});
|
|
2313
|
+
console.log("");
|
|
2314
|
+
console.log("LongTable checkpoint decision recorded");
|
|
2315
|
+
console.log(`- question: ${decision.question.id}`);
|
|
2316
|
+
console.log(`- decision: ${decision.decision.id}`);
|
|
2317
|
+
console.log(`- answer: ${decision.decision.selectedOption ?? answer}`);
|
|
2318
|
+
console.log(`- state: ${context.stateFilePath}`);
|
|
2319
|
+
console.log(`- current: ${context.currentFilePath}`);
|
|
2320
|
+
return;
|
|
2365
2321
|
}
|
|
2366
2322
|
const optionValues = [
|
|
2367
2323
|
...result.question.prompt.options.map((option) => option.value),
|
|
@@ -2452,25 +2408,19 @@ async function answerFollowUpQuestionsInTerminal(context, questions, provider) {
|
|
|
2452
2408
|
if (questions.length === 0) {
|
|
2453
2409
|
return;
|
|
2454
2410
|
}
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
surface: "terminal_selector"
|
|
2469
|
-
});
|
|
2470
|
-
}
|
|
2471
|
-
}
|
|
2472
|
-
finally {
|
|
2473
|
-
rl.close();
|
|
2411
|
+
console.log(renderBrandBanner("LongTable", "Follow-up Questions"));
|
|
2412
|
+
console.log("");
|
|
2413
|
+
for (let index = 0; index < questions.length; index += 1) {
|
|
2414
|
+
const question = questions[index];
|
|
2415
|
+
const prompt = renderQuestionHeader(index + 1, questions.length, question.prompt.title, question.prompt.question);
|
|
2416
|
+
const answer = await promptChoice(prompt, questionRecordToChoices(question));
|
|
2417
|
+
await answerWorkspaceQuestion({
|
|
2418
|
+
context,
|
|
2419
|
+
questionId: question.id,
|
|
2420
|
+
answer,
|
|
2421
|
+
provider,
|
|
2422
|
+
surface: "terminal_selector"
|
|
2423
|
+
});
|
|
2474
2424
|
}
|
|
2475
2425
|
}
|
|
2476
2426
|
async function runClarify(args) {
|
|
@@ -2846,6 +2796,26 @@ async function runRoles(args) {
|
|
|
2846
2796
|
}
|
|
2847
2797
|
}
|
|
2848
2798
|
async function runStart(args) {
|
|
2799
|
+
const hasMinimalFallbackArgs = typeof args.name === "string" &&
|
|
2800
|
+
typeof args.path === "string" &&
|
|
2801
|
+
typeof args.goal === "string";
|
|
2802
|
+
const hasFallbackIntent = args["no-interview"] === true ||
|
|
2803
|
+
hasMinimalFallbackArgs;
|
|
2804
|
+
if (!hasFallbackIntent) {
|
|
2805
|
+
console.log(renderSectionCard("LongTable Start Has Moved", [
|
|
2806
|
+
"`longtable start` is now a fallback for automation and scripted workspace creation.",
|
|
2807
|
+
"The primary research-start experience is provider-native so LongTable can run a real interview instead of a terminal questionnaire.",
|
|
2808
|
+
"",
|
|
2809
|
+
"Use:",
|
|
2810
|
+
"1. longtable setup --provider codex",
|
|
2811
|
+
"2. cd \"<research-folder>\"",
|
|
2812
|
+
"3. codex",
|
|
2813
|
+
"4. $longtable-interview",
|
|
2814
|
+
"",
|
|
2815
|
+
"For automation, pass `--no-interview --json` with `--name`, `--path`, and `--goal`."
|
|
2816
|
+
]));
|
|
2817
|
+
return;
|
|
2818
|
+
}
|
|
2849
2819
|
const setupPath = typeof args.setup === "string" ? args.setup : undefined;
|
|
2850
2820
|
const existingSetup = await loadOptionalSetup(setupPath);
|
|
2851
2821
|
if (!existingSetup) {
|
|
@@ -2861,6 +2831,7 @@ async function runStart(args) {
|
|
|
2861
2831
|
researchObject: interview.researchObject,
|
|
2862
2832
|
gapRisk: interview.gapRisk,
|
|
2863
2833
|
protectedDecision: interview.protectedDecision,
|
|
2834
|
+
startInterview: interview.startInterview,
|
|
2864
2835
|
requestedPerspectives: interview.requestedPerspectives,
|
|
2865
2836
|
disagreementPreference: interview.disagreementPreference,
|
|
2866
2837
|
setup: existingSetup
|