@longtable/cli 0.1.15 → 0.1.17

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/dist/cli.js CHANGED
@@ -1,12 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import { existsSync, readFileSync, statSync } from "node:fs";
3
3
  import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
4
- import { execSync } from "node:child_process";
4
+ import { execFileSync, execSync } from "node:child_process";
5
5
  import { emitKeypressEvents } from "node:readline";
6
6
  import { createInterface } from "node:readline/promises";
7
7
  import { stdin as input, stdout as output, cwd, exit } from "node:process";
8
- import { dirname, resolve } from "node:path";
8
+ import { dirname, join, resolve } from "node:path";
9
9
  import { homedir } from "node:os";
10
+ import { classifyCheckpointTrigger } from "@longtable/checkpoints";
10
11
  import { buildProviderChoices, buildQuickSetupFlow, createPersistedSetupOutput, installRuntimeConfigFromStoredSetup, loadSetupOutput, renderInstallSummary, renderSetupSummary, resolveDefaultRuntimeConfigPath, resolveDefaultSetupPath, saveSetupAndRuntimeConfig, serializeSetupOutput, writeRuntimeConfig } from "@longtable/setup";
11
12
  import { buildCodexSkillSpecs, buildCodexThinWrappedPrompt, installCodexSkills, listInstalledCodexSkills, renderQuestionRecordPrompt, removeCodexSkills, resolveCodexSkillsDir, runCodexThinWrapper } from "@longtable/provider-codex";
12
13
  import { buildClaudeSkillSpecs, installClaudeSkills, listInstalledClaudeSkills, renderQuestionRecordInput, removeClaudeSkills, resolveClaudeSkillsDir } from "@longtable/provider-claude";
@@ -14,7 +15,8 @@ import { installCodexPromptAliases, listInstalledCodexPromptAliases, removeCodex
14
15
  import { buildPersonaGuidance, parseInvocationDirective } from "./persona-router.js";
15
16
  import { PERSONA_DEFINITIONS, listRoleDefinitions } from "./personas.js";
16
17
  import { buildPanelFallback, renderPanelSummary } from "./panel.js";
17
- import { appendInvocationRecordToWorkspace, assertWorkspaceNotBlocked, answerWorkspaceQuestion, createWorkspaceClarificationCard, createWorkspaceQuestion, createOrUpdateProjectWorkspace, inspectProjectWorkspace, loadProjectContextFromDirectory, renderProjectWorkspaceSummary, syncCurrentWorkspaceView } from "./project-session.js";
18
+ import { appendInvocationRecordToWorkspace, assertWorkspaceNotBlocked, answerWorkspaceQuestion, createWorkspaceClarificationCard, createWorkspaceQuestion, createOrUpdateProjectWorkspace, inspectProjectWorkspace, loadWorkspaceState, loadProjectContextFromDirectory, renderProjectWorkspaceSummary, syncCurrentWorkspaceView } from "./project-session.js";
19
+ import { buildTeamDebate, renderTeamDebateSummary } from "./debate.js";
18
20
  const VALID_MODES = new Set([
19
21
  "explore",
20
22
  "review",
@@ -40,7 +42,7 @@ const ANSI = {
40
42
  green: "\u001B[32m"
41
43
  };
42
44
  const LONGTABLE_MCP_SERVER_NAME = "longtable-state";
43
- const LONGTABLE_MCP_PACKAGE_VERSION = "0.1.15";
45
+ const LONGTABLE_MCP_PACKAGE_VERSION = "0.1.17";
44
46
  const LONGTABLE_MCP_MARKER_START = "# LongTable state MCP START";
45
47
  const LONGTABLE_MCP_MARKER_END = "# LongTable state MCP END";
46
48
  function style(text, prefix) {
@@ -76,7 +78,8 @@ function usage() {
76
78
  " Run `longtable ...` in your terminal, not inside the Codex chat box.",
77
79
  " After `longtable start`, move into the created project directory and open `codex` there.",
78
80
  "",
79
- " 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] [--agent-team native_when_available|sequential_panel_only] [--mcp skip|print_config|write_provider|write_all] [--json] [--no-install] [--install-skills] [--install-prompts]",
81
+ " 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-skills] [--install-prompts]",
82
+ " longtable setup [--provider codex|claude] [--json] [--dir <path>] [--skills-dir <path>] [--runtime-path <file>] [--setup-path <file>]",
80
83
  " longtable start [--path <dir>] [--name <project>] [--goal <text>] [--blocker <text>] [--perspectives <role[,role]>] [--disagreement synthesis_only|show_on_conflict|always_visible] [--setup <path>] [--json]",
81
84
  " longtable resume [--cwd <path>] [--json]",
82
85
  " longtable doctor [--cwd <path>] [--fix] [--json] [--codex-dir <path>] [--claude-dir <path>] [--codex-prompts-dir <path>] [--codex-runtime-path <file>] [--claude-runtime-path <file>]",
@@ -85,6 +88,9 @@ function usage() {
85
88
  " longtable show [--json] [--path <file>]",
86
89
  " longtable install [--json] [--path <file>] [--runtime-path <file>]",
87
90
  " longtable mcp install [--provider codex|claude|all] [--write] [--json] [--codex-config <path>] [--claude-settings <path>] [--package <spec>]",
91
+ " longtable hud [--watch] [--tmux] [--preset minimal|full] [--cwd <path>] [--json]",
92
+ " longtable sentinel --prompt <text> [--cwd <path>] [--json] [--record]",
93
+ " longtable team --prompt <text> [--role <role[,role]>] [--tmux] [--debate] [--rounds 5] [--cwd <path>] [--json]",
88
94
  " longtable ask [--prompt <text>] [--print] [--json] [--setup <path>] [--cwd <path>]",
89
95
  " longtable clarify --prompt <task-context> [--provider codex|claude] [--required|--advisory] [--print] [--cwd <path>] [--json] [--force]",
90
96
  " longtable question --prompt <decision-context> [--title <text>] [--text <question>] [--provider codex|claude] [--required|--advisory] [--print] [--cwd <path>] [--json]",
@@ -120,7 +126,7 @@ function parseArgs(argv) {
120
126
  const values = {};
121
127
  let subcommand = maybeSubcommand;
122
128
  const modeCommand = command && VALID_MODES.has(command);
123
- const directCommand = command && ["init", "start", "resume", "doctor", "status", "roles", "show", "install", "mcp", "codex", "claude", "ask", "clarify", "question", "panel", "decide"].includes(command);
129
+ const directCommand = command && ["init", "setup", "start", "resume", "doctor", "status", "roles", "show", "install", "mcp", "codex", "claude", "ask", "clarify", "question", "panel", "decide", "hud", "sentinel", "team"].includes(command);
124
130
  let startIndex = 1;
125
131
  if (modeCommand) {
126
132
  subcommand = undefined;
@@ -197,12 +203,9 @@ function questionSection(questionId) {
197
203
  if (questionId === "preferredCheckpointIntensity" || questionId === "preferredEntryMode") {
198
204
  return "Interaction style";
199
205
  }
200
- if (questionId === "weakestDomain" || questionId === "panelPreference" || questionId === "agentTeamPreference") {
206
+ if (questionId === "weakestDomain" || questionId === "panelPreference") {
201
207
  return "How LongTable should challenge you";
202
208
  }
203
- if (questionId === "mcpPreference") {
204
- return "Provider integration";
205
- }
206
209
  return "Authorship and voice";
207
210
  }
208
211
  function formatModeLabel(mode) {
@@ -499,12 +502,6 @@ function toSetupAnswers(args) {
499
502
  : undefined,
500
503
  panelPreference: typeof args["panel-preference"] === "string"
501
504
  ? String(args["panel-preference"])
502
- : undefined,
503
- agentTeamPreference: typeof args["agent-team"] === "string"
504
- ? String(args["agent-team"])
505
- : undefined,
506
- mcpPreference: typeof args.mcp === "string"
507
- ? String(args.mcp)
508
505
  : undefined
509
506
  };
510
507
  }
@@ -552,11 +549,6 @@ async function collectInteractiveAnswers(initialFlow) {
552
549
  answers.weakestDomain = value;
553
550
  if (question.id === "panelPreference")
554
551
  answers.panelPreference = value;
555
- if (question.id === "agentTeamPreference") {
556
- answers.agentTeamPreference = value;
557
- }
558
- if (question.id === "mcpPreference")
559
- answers.mcpPreference = value;
560
552
  }
561
553
  return {
562
554
  flow,
@@ -568,6 +560,172 @@ async function collectInteractiveAnswers(initialFlow) {
568
560
  rl.close();
569
561
  }
570
562
  }
563
+ function buildPermissionSetupChoices() {
564
+ return {
565
+ surfaces: [
566
+ {
567
+ id: "cli_only",
568
+ label: "CLI only",
569
+ description: "Why: least invasive. Tradeoff: no natural in-provider LongTable entrypoints."
570
+ },
571
+ {
572
+ id: "skills",
573
+ label: "Skills",
574
+ description: "Why: enables natural LongTable skill routing. Tradeoff: writes provider skill files."
575
+ },
576
+ {
577
+ id: "skills_mcp",
578
+ label: "Skills + MCP",
579
+ description: "Why: adds structured state access. Tradeoff: writes provider config for MCP transport."
580
+ },
581
+ {
582
+ id: "skills_mcp_sentinel",
583
+ label: "Skills + MCP + Sentinel",
584
+ description: "Why: prepares advisory gap/tacit monitoring. Tradeoff: LongTable may nudge research turns."
585
+ }
586
+ ],
587
+ intervention: [
588
+ {
589
+ id: "advisory",
590
+ label: "Advisory",
591
+ description: "Why: notices gaps without blocking. Tradeoff: you may still miss hard commitments."
592
+ },
593
+ {
594
+ id: "balanced",
595
+ label: "Balanced",
596
+ description: "Why: blocks clear theory, measurement, method, or evidence commitments. Tradeoff: occasional stops."
597
+ },
598
+ {
599
+ id: "strong",
600
+ label: "Strong",
601
+ description: "Why: maximizes judgment protection. Tradeoff: more interruption before closure."
602
+ }
603
+ ],
604
+ tmux: [
605
+ {
606
+ id: "standard",
607
+ label: "Standard chat",
608
+ description: "Why: portable default. Tradeoff: checkpoints and gaps are less persistently visible."
609
+ },
610
+ {
611
+ id: "hud",
612
+ label: "Research HUD",
613
+ description: "Why: keeps goals, blockers, and pending checkpoints visible. Requires tmux."
614
+ },
615
+ {
616
+ id: "console",
617
+ label: "Research console",
618
+ description: "Why: enables a richer tmux layout for HUD and team discussion. Requires tmux."
619
+ }
620
+ ],
621
+ team: [
622
+ {
623
+ id: "off",
624
+ label: "Off",
625
+ description: "Why: simplest. Tradeoff: panel disagreement stays inside one LongTable response."
626
+ },
627
+ {
628
+ id: "panel",
629
+ label: "Structured panel",
630
+ description: "Why: role disagreement is visible without tmux. Tradeoff: not parallel."
631
+ },
632
+ {
633
+ id: "tmux_team",
634
+ label: "Tmux team discussion",
635
+ description: "Why: opens role panes for parallel debate. Tradeoff: terminal complexity and cleanup."
636
+ }
637
+ ]
638
+ };
639
+ }
640
+ function checkpointIntensityFromIntervention(choice) {
641
+ if (choice === "strong")
642
+ return "high";
643
+ if (choice === "advisory")
644
+ return "low";
645
+ return "balanced";
646
+ }
647
+ async function runSetup(args) {
648
+ const json = args.json === true;
649
+ const rl = createInterface({ input, output });
650
+ try {
651
+ const provider = (typeof args.provider === "string"
652
+ ? (args.provider === "claude" ? "claude" : "codex")
653
+ : await promptChoice(rl, "Which provider should LongTable configure?", buildProviderChoices()));
654
+ const choices = buildPermissionSetupChoices();
655
+ const surfaces = await promptChoice(rl, [
656
+ "Which LongTable runtime surfaces should be enabled?",
657
+ "This is a permission choice because skills, MCP, and sentinel support write provider-facing runtime files."
658
+ ].join("\n"), choices.surfaces);
659
+ const intervention = await promptChoice(rl, "How strongly may LongTable interrupt research decisions?", choices.intervention);
660
+ const tmuxMode = await promptChoice(rl, "Should LongTable recommend a tmux-based research interface?", choices.tmux);
661
+ const teamMode = await promptChoice(rl, "Should LongTable enable agent/team discussion mode?", choices.team);
662
+ const outputValue = createPersistedSetupOutput({
663
+ field: "unspecified",
664
+ careerStage: "unspecified",
665
+ experienceLevel: "advanced",
666
+ preferredCheckpointIntensity: checkpointIntensityFromIntervention(intervention),
667
+ preferredEntryMode: "explore",
668
+ panelPreference: teamMode === "off" ? "show_on_conflict" : "always_visible"
669
+ }, provider, "quickstart");
670
+ outputValue.initialState.explicitState = {
671
+ ...outputValue.initialState.explicitState,
672
+ runtimeSurfaces: surfaces,
673
+ interventionPosture: intervention,
674
+ tmuxMode,
675
+ teamMode
676
+ };
677
+ if (surfaces === "skills_mcp_sentinel") {
678
+ outputValue.initialState.inferredHypotheses.push({
679
+ hypothesis: "Researcher approved advisory Gap/Tacit Sentinel setup.",
680
+ confidence: 0.95,
681
+ evidence: ["Selected Skills + MCP + Sentinel during permission-first setup."],
682
+ status: "confirmed"
683
+ });
684
+ }
685
+ const result = await saveSetupAndRuntimeConfig(outputValue, {
686
+ setupPath: typeof args["setup-path"] === "string" ? args["setup-path"] : undefined,
687
+ runtimePath: typeof args["runtime-path"] === "string" ? args["runtime-path"] : undefined
688
+ });
689
+ const installedSkills = surfaces === "cli_only"
690
+ ? []
691
+ : provider === "codex"
692
+ ? await installCodexSkills(listRoleDefinitions(), typeof args["skills-dir"] === "string" ? args["skills-dir"] : typeof args.dir === "string" ? args.dir : undefined)
693
+ : await installClaudeSkills(listRoleDefinitions(), typeof args["skills-dir"] === "string" ? args["skills-dir"] : typeof args.dir === "string" ? args.dir : undefined);
694
+ const mcpRequested = surfaces === "skills_mcp" || surfaces === "skills_mcp_sentinel";
695
+ if (mcpRequested && !json) {
696
+ console.log("");
697
+ console.log("MCP setup is approved. To write provider config now, run:");
698
+ console.log(`- longtable mcp install --provider ${provider} --write`);
699
+ }
700
+ if (json) {
701
+ console.log(JSON.stringify({
702
+ setup: outputValue,
703
+ runtime: result,
704
+ installedSkills: installedSkills.map((skill) => skill.name),
705
+ mcpRequested,
706
+ tmuxMode,
707
+ teamMode
708
+ }, null, 2));
709
+ return;
710
+ }
711
+ console.log("");
712
+ console.log(renderSetupSummary(outputValue));
713
+ console.log("");
714
+ console.log(renderInstallSummary(result));
715
+ console.log(`Installed skills: ${installedSkills.length}`);
716
+ if (tmuxMode !== "standard") {
717
+ console.log("");
718
+ console.log("Tmux recommendation:");
719
+ console.log("- macOS: brew install tmux");
720
+ console.log("- Ubuntu/Debian: sudo apt install tmux");
721
+ console.log("- Start HUD in an existing tmux session: longtable hud --tmux");
722
+ console.log("- Start a discussion team: longtable team --tmux --prompt \"...\"");
723
+ }
724
+ }
725
+ finally {
726
+ rl.close();
727
+ }
728
+ }
571
729
  function perspectiveChoices() {
572
730
  return PERSONA_DEFINITIONS.map((persona) => ({
573
731
  id: persona.key,
@@ -671,12 +829,6 @@ function normalizePersistAnswers(raw) {
671
829
  : {}),
672
830
  ...(raw.panelPreference
673
831
  ? { panelPreference: raw.panelPreference }
674
- : {}),
675
- ...(raw.agentTeamPreference
676
- ? { agentTeamPreference: raw.agentTeamPreference }
677
- : {}),
678
- ...(raw.mcpPreference
679
- ? { mcpPreference: raw.mcpPreference }
680
832
  : {})
681
833
  }
682
834
  };
@@ -705,15 +857,6 @@ async function readPersistAnswers(args) {
705
857
  }
706
858
  throw new Error("persist-init requires either --answers-json, --stdin, or the full set of setup flags.");
707
859
  }
708
- async function applySetupMcpPreference(provider, preference) {
709
- if (!preference || preference === "skip") {
710
- return undefined;
711
- }
712
- return installMcpConfig({
713
- provider: preference === "write_all" ? "all" : provider,
714
- write: preference === "write_provider" || preference === "write_all"
715
- });
716
- }
717
860
  async function runInit(args) {
718
861
  const json = args.json === true;
719
862
  const installRuntime = args["no-install"] !== true;
@@ -746,17 +889,15 @@ async function runInit(args) {
746
889
  if (provider === "claude" && installSkills) {
747
890
  installedSkills = await installClaudeSkills(listRoleDefinitions(), skillsDir);
748
891
  }
749
- const mcpInstall = await applySetupMcpPreference(provider, answers.mcpPreference);
750
892
  if (json) {
751
- if (installedPrompts.length === 0 && installedSkills.length === 0 && !mcpInstall) {
893
+ if (installedPrompts.length === 0 && installedSkills.length === 0) {
752
894
  console.log(serializeSetupOutput(outputValue));
753
895
  return;
754
896
  }
755
897
  console.log(JSON.stringify({
756
898
  setup: outputValue,
757
899
  installedPrompts: installedPrompts.map((prompt) => prompt.name),
758
- installedSkills: installedSkills.map((skill) => skill.name),
759
- mcpInstall
900
+ installedSkills: installedSkills.map((skill) => skill.name)
760
901
  }, null, 2));
761
902
  return;
762
903
  }
@@ -781,10 +922,6 @@ async function runInit(args) {
781
922
  }
782
923
  console.log(" Use these by naming LongTable naturally, e.g. `lt panel: ...`.");
783
924
  }
784
- if (mcpInstall) {
785
- console.log("");
786
- console.log(renderMcpInstallSummary(mcpInstall));
787
- }
788
925
  if (provider === "codex") {
789
926
  console.log("");
790
927
  console.log("Next step:");
@@ -918,43 +1055,40 @@ function renderMcpInstallSummary(result) {
918
1055
  }
919
1056
  return lines.join("\n").trimEnd();
920
1057
  }
921
- async function installMcpConfig(args) {
922
- const serverName = typeof args.name === "string" && args.name.trim()
923
- ? args.name.trim()
924
- : LONGTABLE_MCP_SERVER_NAME;
925
- const packageSpec = resolveMcpPackageSpec(args);
926
- const command = typeof args.command === "string" && args.command.trim() ? args.command.trim() : "npx";
927
- const mcpArgs = command === "npx" ? ["-y", packageSpec] : [packageSpec];
928
- const providers = resolveMcpProviders(args.provider);
929
- const write = args.write === true;
930
- const targets = [];
931
- for (const provider of providers) {
932
- if (provider === "codex") {
933
- const path = resolveCodexMcpConfigPath(args);
934
- const block = renderCodexMcpBlock(serverName, command, mcpArgs);
935
- const content = write ? await writeCodexMcpConfig(path, block, serverName) : block;
936
- targets.push({ provider, path, format: "toml", content });
937
- }
938
- if (provider === "claude") {
939
- const path = resolveClaudeMcpSettingsPath(args);
940
- const content = write
941
- ? await writeClaudeMcpSettings(path, serverName, command, mcpArgs)
942
- : renderClaudeMcpJson(serverName, command, mcpArgs);
943
- targets.push({ provider, path, format: "json", content });
944
- }
945
- }
946
- return {
947
- serverName,
948
- packageSpec,
949
- command,
950
- args: mcpArgs,
951
- write,
952
- targets
953
- };
954
- }
955
1058
  async function runMcpSubcommand(subcommand, args) {
956
1059
  if (!subcommand || subcommand === "install" || subcommand === "print-config") {
957
- const result = await installMcpConfig(args);
1060
+ const serverName = typeof args.name === "string" && args.name.trim()
1061
+ ? args.name.trim()
1062
+ : LONGTABLE_MCP_SERVER_NAME;
1063
+ const packageSpec = resolveMcpPackageSpec(args);
1064
+ const command = typeof args.command === "string" && args.command.trim() ? args.command.trim() : "npx";
1065
+ const mcpArgs = command === "npx" ? ["-y", packageSpec] : [packageSpec];
1066
+ const providers = resolveMcpProviders(args.provider);
1067
+ const write = args.write === true;
1068
+ const targets = [];
1069
+ for (const provider of providers) {
1070
+ if (provider === "codex") {
1071
+ const path = resolveCodexMcpConfigPath(args);
1072
+ const block = renderCodexMcpBlock(serverName, command, mcpArgs);
1073
+ const content = write ? await writeCodexMcpConfig(path, block, serverName) : block;
1074
+ targets.push({ provider, path, format: "toml", content });
1075
+ }
1076
+ if (provider === "claude") {
1077
+ const path = resolveClaudeMcpSettingsPath(args);
1078
+ const content = write
1079
+ ? await writeClaudeMcpSettings(path, serverName, command, mcpArgs)
1080
+ : renderClaudeMcpJson(serverName, command, mcpArgs);
1081
+ targets.push({ provider, path, format: "json", content });
1082
+ }
1083
+ }
1084
+ const result = {
1085
+ serverName,
1086
+ packageSpec,
1087
+ command,
1088
+ args: mcpArgs,
1089
+ write,
1090
+ targets
1091
+ };
958
1092
  if (args.json === true) {
959
1093
  console.log(JSON.stringify(result, null, 2));
960
1094
  return;
@@ -1428,9 +1562,6 @@ async function runModeCommand(mode, args) {
1428
1562
  }
1429
1563
  const setup = await loadOptionalSetup(typeof args.setup === "string" ? args.setup : undefined);
1430
1564
  const projectContext = await loadProjectContextFromDirectory(workingDirectory);
1431
- if (projectContext) {
1432
- await assertWorkspaceNotBlocked(projectContext);
1433
- }
1434
1565
  const projectAware = await buildProjectAwarePrompt(prompt, workingDirectory);
1435
1566
  const panelPreference = setup?.profileSeed.panelPreference;
1436
1567
  const panelRequested = args.panel === true ||
@@ -1487,8 +1618,7 @@ async function runPanelCommand(args) {
1487
1618
  mode,
1488
1619
  roleFlag: typeof args.role === "string" ? args.role : undefined,
1489
1620
  provider,
1490
- visibility,
1491
- agentTeamPreference: setup?.profileSeed.agentTeamPreference
1621
+ visibility
1492
1622
  });
1493
1623
  if (projectAware.projectContextFound) {
1494
1624
  const context = await loadProjectContextFromDirectory(workingDirectory);
@@ -1809,6 +1939,302 @@ async function runAsk(args) {
1809
1939
  }
1810
1940
  await runModeCommand(mode, delegatedArgs);
1811
1941
  }
1942
+ function localId(prefix) {
1943
+ return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
1944
+ }
1945
+ function shellEscape(value) {
1946
+ return `'${value.replaceAll("'", "'\\''")}'`;
1947
+ }
1948
+ async function writeJsonFile(path, value) {
1949
+ await writeFile(path, `${JSON.stringify(value, null, 2)}\n`, "utf8");
1950
+ }
1951
+ async function writeTeamDebateArtifacts(bundle, teamDir, prompt) {
1952
+ await mkdir(teamDir, { recursive: true });
1953
+ await writeFile(join(teamDir, "prompt.txt"), prompt, "utf8");
1954
+ await writeJsonFile(join(teamDir, "plan.json"), bundle.plan);
1955
+ await writeJsonFile(join(teamDir, "run.json"), bundle.run);
1956
+ for (const round of bundle.run.rounds) {
1957
+ await mkdir(round.artifactDir, { recursive: true });
1958
+ await writeJsonFile(join(round.artifactDir, "round.json"), round);
1959
+ for (const contribution of round.contributions) {
1960
+ await writeJsonFile(join(teamDir, contribution.artifactPath), contribution);
1961
+ }
1962
+ }
1963
+ await writeJsonFile(join(teamDir, "synthesis.json"), bundle.run.synthesis);
1964
+ await writeJsonFile(join(teamDir, "checkpoint.json"), bundle.questionRecord);
1965
+ await writeJsonFile(join(teamDir, "invocation.json"), bundle.invocationRecord);
1966
+ }
1967
+ function sentinelSummary(prompt, workingDirectory) {
1968
+ const trigger = classifyCheckpointTrigger(prompt, {
1969
+ fallbackMode: "explore",
1970
+ unresolvedTensions: []
1971
+ });
1972
+ const normalized = prompt.toLowerCase();
1973
+ const signals = [];
1974
+ if (/measure|measurement|scale|validity|reliability|측정|척도|타당도|신뢰도/.test(normalized)) {
1975
+ signals.push("measurement gap or commitment");
1976
+ }
1977
+ if (/theory|theoretical|framework|construct|이론|프레임워크|개념/.test(normalized)) {
1978
+ signals.push("theory or construct commitment");
1979
+ }
1980
+ if (/method|design|sample|participant|방법|설계|표본|참여자/.test(normalized)) {
1981
+ signals.push("method/design gap");
1982
+ }
1983
+ if (/citation|reference|source|evidence|doi|문헌|인용|근거|출처/.test(normalized)) {
1984
+ signals.push("evidence gap");
1985
+ }
1986
+ if (/voice|authorship|narrative|저자성|서사|문체|목소리/.test(normalized)) {
1987
+ signals.push("authorship or narrative-trace risk");
1988
+ }
1989
+ if (/assumption|implicit|tacit|암묵|전제|가정/.test(normalized)) {
1990
+ signals.push("tacit assumption risk");
1991
+ }
1992
+ return {
1993
+ cwd: workingDirectory,
1994
+ checkpoint: trigger.signal.checkpointKey,
1995
+ family: trigger.family,
1996
+ confidence: trigger.confidence,
1997
+ requiresQuestionBeforeClosure: trigger.requiresQuestionBeforeClosure,
1998
+ signals: signals.length > 0 ? signals : ["no specific gap/tacit signal beyond checkpoint classifier"],
1999
+ rationale: trigger.rationale
2000
+ };
2001
+ }
2002
+ async function runSentinel(args) {
2003
+ const workingDirectory = typeof args.cwd === "string" ? args.cwd : cwd();
2004
+ const prompt = await resolvePrompt(typeof args.prompt === "string" ? args.prompt : undefined);
2005
+ if (!prompt) {
2006
+ throw new Error("A prompt is required.");
2007
+ }
2008
+ const summary = sentinelSummary(prompt, workingDirectory);
2009
+ const context = await loadProjectContextFromDirectory(workingDirectory);
2010
+ if (args.record === true && context) {
2011
+ const state = await loadWorkspaceState(context);
2012
+ state.inferredHypotheses.push({
2013
+ hypothesis: `Sentinel detected: ${summary.signals.join(", ")}.`,
2014
+ confidence: summary.confidence === "high" ? 0.85 : summary.confidence === "medium" ? 0.65 : 0.4,
2015
+ evidence: [`Prompt: ${prompt}`],
2016
+ status: "unconfirmed"
2017
+ });
2018
+ if (summary.requiresQuestionBeforeClosure) {
2019
+ state.openTensions.push(`Pending sentinel risk: ${summary.checkpoint}`);
2020
+ }
2021
+ await writeFile(context.stateFilePath, JSON.stringify(state, null, 2), "utf8");
2022
+ await syncCurrentWorkspaceView(context);
2023
+ }
2024
+ if (args.json === true) {
2025
+ console.log(JSON.stringify(summary, null, 2));
2026
+ return;
2027
+ }
2028
+ console.log("LongTable Sentinel");
2029
+ console.log(`- checkpoint: ${summary.checkpoint}`);
2030
+ console.log(`- family: ${summary.family}`);
2031
+ console.log(`- confidence: ${summary.confidence}`);
2032
+ console.log(`- question before closure: ${summary.requiresQuestionBeforeClosure ? "yes" : "no"}`);
2033
+ console.log("- detected signals:");
2034
+ for (const signal of summary.signals) {
2035
+ console.log(` - ${signal}`);
2036
+ }
2037
+ if (args.record === true) {
2038
+ console.log(context ? `- recorded in: ${context.stateFilePath}` : "- record skipped: no LongTable workspace found");
2039
+ }
2040
+ }
2041
+ function renderHudText(inspection, preset) {
2042
+ if (!inspection.found) {
2043
+ return [
2044
+ "LongTable HUD",
2045
+ "- workspace: not found",
2046
+ "- run `longtable start` for durable research state"
2047
+ ].join("\n");
2048
+ }
2049
+ const lines = [
2050
+ "LongTable HUD",
2051
+ `- project: ${inspection.project?.name}`,
2052
+ `- goal: ${inspection.session?.currentGoal}`,
2053
+ ...(inspection.session?.currentBlocker ? [`- blocker: ${inspection.session.currentBlocker}`] : []),
2054
+ `- questions: ${inspection.counts?.pendingQuestions ?? 0} pending / ${inspection.counts?.questions ?? 0} total`,
2055
+ `- decisions: ${inspection.counts?.decisions ?? 0}`,
2056
+ `- invocations: ${inspection.counts?.invocations ?? 0}`
2057
+ ];
2058
+ if (preset !== "minimal") {
2059
+ lines.push("- pending checkpoints:");
2060
+ for (const question of inspection.pendingQuestions ?? []) {
2061
+ lines.push(` - ${question.required ? "required" : "advisory"}: ${question.question}`);
2062
+ }
2063
+ lines.push("- recent decisions:");
2064
+ for (const decision of inspection.recentDecisions ?? []) {
2065
+ lines.push(` - ${decision.summary}`);
2066
+ }
2067
+ }
2068
+ return lines.join("\n");
2069
+ }
2070
+ async function runHud(args) {
2071
+ const workingDirectory = typeof args.cwd === "string" ? args.cwd : cwd();
2072
+ const preset = typeof args.preset === "string" ? args.preset : "full";
2073
+ if (args.tmux === true) {
2074
+ if (!process.env.TMUX) {
2075
+ throw new Error("`longtable hud --tmux` must be run inside an existing tmux session.");
2076
+ }
2077
+ const launcher = process.argv[1] ?? "longtable";
2078
+ const command = `node ${shellEscape(launcher)} hud --watch --preset ${shellEscape(preset)} --cwd ${shellEscape(workingDirectory)}`;
2079
+ execFileSync("tmux", ["split-window", "-v", "-l", "10", command], { stdio: "inherit" });
2080
+ return;
2081
+ }
2082
+ while (true) {
2083
+ const inspection = await inspectProjectWorkspace(workingDirectory);
2084
+ if (args.json === true) {
2085
+ console.log(JSON.stringify(inspection, null, 2));
2086
+ return;
2087
+ }
2088
+ if (args.watch === true) {
2089
+ process.stdout.write("\u001Bc");
2090
+ }
2091
+ console.log(renderHudText(inspection, preset));
2092
+ if (args.watch !== true) {
2093
+ return;
2094
+ }
2095
+ await new Promise((resolvePromise) => setTimeout(resolvePromise, 1500));
2096
+ }
2097
+ }
2098
+ async function runTeam(args) {
2099
+ const workingDirectory = typeof args.cwd === "string" ? args.cwd : cwd();
2100
+ const prompt = await resolvePrompt(typeof args.prompt === "string" ? args.prompt : undefined);
2101
+ if (!prompt) {
2102
+ throw new Error("A prompt is required.");
2103
+ }
2104
+ const rounds = typeof args.rounds === "string" ? Number(args.rounds) : 5;
2105
+ if (!Number.isInteger(rounds) || rounds !== 5) {
2106
+ throw new Error("LongTable team debate v1 supports `--rounds 5` only.");
2107
+ }
2108
+ const setup = await loadOptionalSetup(typeof args.setup === "string" ? args.setup : undefined);
2109
+ const projectContext = await loadProjectContextFromDirectory(workingDirectory);
2110
+ if (projectContext) {
2111
+ await assertWorkspaceNotBlocked(projectContext);
2112
+ }
2113
+ const projectAware = await buildProjectAwarePrompt(prompt, workingDirectory);
2114
+ const fallback = buildPanelFallback({
2115
+ prompt,
2116
+ mode: "review",
2117
+ roleFlag: typeof args.role === "string" ? args.role : undefined,
2118
+ provider: setup?.providerSelection.provider,
2119
+ visibility: "always_visible"
2120
+ });
2121
+ const teamId = localId("team");
2122
+ const teamDir = join(workingDirectory, ".longtable", "team", teamId);
2123
+ if (args.debate === true) {
2124
+ const debate = buildTeamDebate({
2125
+ teamId,
2126
+ teamDir,
2127
+ prompt: projectAware.prompt,
2128
+ roleFlag: typeof args.role === "string" ? args.role : undefined,
2129
+ provider: setup?.providerSelection.provider,
2130
+ visibility: "always_visible",
2131
+ roundCount: rounds,
2132
+ tmux: args.tmux === true
2133
+ });
2134
+ await writeTeamDebateArtifacts(debate, teamDir, prompt);
2135
+ const canRecordWorkspace = projectAware.projectContextFound && projectContext && existsSync(projectContext.stateFilePath);
2136
+ if (canRecordWorkspace) {
2137
+ await appendInvocationRecordToWorkspace(projectContext, debate.invocationRecord, [debate.questionRecord]);
2138
+ }
2139
+ if (args.json === true) {
2140
+ console.log(JSON.stringify({
2141
+ teamId,
2142
+ teamDir,
2143
+ plan: debate.plan,
2144
+ run: debate.run,
2145
+ questionRecord: debate.questionRecord,
2146
+ invocationRecord: debate.invocationRecord,
2147
+ execution: {
2148
+ status: "completed",
2149
+ surface: debate.run.surface,
2150
+ projectContextFound: projectAware.projectContextFound,
2151
+ invocationLogged: canRecordWorkspace
2152
+ }
2153
+ }, null, 2));
2154
+ return;
2155
+ }
2156
+ if (args.tmux === true) {
2157
+ const sessionName = `longtable-${teamId.replaceAll("_", "-")}`;
2158
+ const shell = process.env.SHELL || "/bin/sh";
2159
+ const launcher = process.argv[1] ?? "longtable";
2160
+ const leaderCommand = [
2161
+ `echo ${shellEscape(`LongTable debate ${teamId}`)}`,
2162
+ `echo ${shellEscape(`Artifacts: ${teamDir}`)}`,
2163
+ `echo ${shellEscape("Fixed rounds are recorded. Role panes can add live review logs.")}`,
2164
+ `echo ${shellEscape(`Checkpoint: ${debate.questionRecord.id}`)}`,
2165
+ `exec ${shellEscape(shell)}`
2166
+ ].join("; ");
2167
+ execFileSync("tmux", ["new-session", "-d", "-s", sessionName, "-c", workingDirectory, leaderCommand], { stdio: "inherit" });
2168
+ for (const member of debate.plan.members) {
2169
+ const rolePrompt = [
2170
+ `LongTable autonomous debate role: ${member.label} (${member.role}).`,
2171
+ "Use the fixed debate artifacts as the shared record. Add live notes only; do not answer the researcher checkpoint.",
2172
+ `Artifacts: ${teamDir}`,
2173
+ "",
2174
+ projectAware.prompt
2175
+ ].join("\n");
2176
+ const logPath = join(teamDir, `${member.role}.debate.log`);
2177
+ const command = [
2178
+ `node ${shellEscape(launcher)} review --role ${shellEscape(member.role)} --prompt ${shellEscape(rolePrompt)} --cwd ${shellEscape(workingDirectory)} 2>&1 | tee ${shellEscape(logPath)}`,
2179
+ `echo ${shellEscape(`Debate role log written to ${logPath}`)}`,
2180
+ `exec ${shellEscape(shell)}`
2181
+ ].join("; ");
2182
+ execFileSync("tmux", ["split-window", "-t", sessionName, "-c", workingDirectory, command], { stdio: "inherit" });
2183
+ execFileSync("tmux", ["select-layout", "-t", sessionName, "tiled"], { stdio: "ignore" });
2184
+ }
2185
+ console.log(`LongTable tmux debate launched: ${sessionName}`);
2186
+ console.log(`Attach with: tmux attach -t ${sessionName}`);
2187
+ console.log(`Artifacts: ${teamDir}`);
2188
+ return;
2189
+ }
2190
+ console.log(renderTeamDebateSummary(debate.run));
2191
+ console.log(`- checkpoint: ${debate.questionRecord.id}`);
2192
+ return;
2193
+ }
2194
+ await mkdir(teamDir, { recursive: true });
2195
+ await writeFile(join(teamDir, "prompt.txt"), prompt, "utf8");
2196
+ await writeFile(join(teamDir, "plan.json"), JSON.stringify(fallback.plan, null, 2), "utf8");
2197
+ if (args.json === true) {
2198
+ console.log(JSON.stringify({ teamId, teamDir, plan: fallback.plan }, null, 2));
2199
+ return;
2200
+ }
2201
+ if (args.tmux !== true) {
2202
+ console.log(renderPanelSummary(fallback.plan));
2203
+ console.log("");
2204
+ console.log("Run with `--tmux` to launch role panes for parallel discussion.");
2205
+ return;
2206
+ }
2207
+ const sessionName = `longtable-${teamId.replaceAll("_", "-")}`;
2208
+ const shell = process.env.SHELL || "/bin/sh";
2209
+ const launcher = process.argv[1] ?? "longtable";
2210
+ const leaderCommand = [
2211
+ `echo ${shellEscape(`LongTable team ${teamId}`)}`,
2212
+ `echo ${shellEscape(`Logs: ${teamDir}`)}`,
2213
+ "echo 'Role panes are running. Review logs, then run:'",
2214
+ `echo ${shellEscape(`longtable panel --role ${fallback.plan.members.map((member) => member.role).join(",")} --prompt ${JSON.stringify(prompt)}`)}`,
2215
+ `exec ${shellEscape(shell)}`
2216
+ ].join("; ");
2217
+ execFileSync("tmux", ["new-session", "-d", "-s", sessionName, "-c", workingDirectory, leaderCommand], { stdio: "inherit" });
2218
+ for (const member of fallback.plan.members) {
2219
+ const rolePrompt = [
2220
+ `LongTable team discussion role: ${member.label} (${member.role}).`,
2221
+ "Give claims, objections, open questions, and evidence needs. Address likely disagreement with other roles.",
2222
+ "",
2223
+ prompt
2224
+ ].join("\n");
2225
+ const logPath = join(teamDir, `${member.role}.log`);
2226
+ const command = [
2227
+ `node ${shellEscape(launcher)} review --role ${shellEscape(member.role)} --prompt ${shellEscape(rolePrompt)} --cwd ${shellEscape(workingDirectory)} 2>&1 | tee ${shellEscape(logPath)}`,
2228
+ `echo ${shellEscape(`Role log written to ${logPath}`)}`,
2229
+ `exec ${shellEscape(shell)}`
2230
+ ].join("; ");
2231
+ execFileSync("tmux", ["split-window", "-t", sessionName, "-c", workingDirectory, command], { stdio: "inherit" });
2232
+ execFileSync("tmux", ["select-layout", "-t", sessionName, "tiled"], { stdio: "ignore" });
2233
+ }
2234
+ console.log(`LongTable tmux team launched: ${sessionName}`);
2235
+ console.log(`Attach with: tmux attach -t ${sessionName}`);
2236
+ console.log(`Logs: ${teamDir}`);
2237
+ }
1812
2238
  async function runDecide(args) {
1813
2239
  const workingDirectory = typeof args.cwd === "string" ? args.cwd : cwd();
1814
2240
  const answer = typeof args.answer === "string" ? args.answer.trim() : "";
@@ -2095,6 +2521,10 @@ async function main() {
2095
2521
  await runInit(values);
2096
2522
  return;
2097
2523
  }
2524
+ if (command === "setup") {
2525
+ await runSetup(values);
2526
+ return;
2527
+ }
2098
2528
  if (command === "start") {
2099
2529
  await runStart(values);
2100
2530
  return;
@@ -2139,6 +2569,18 @@ async function main() {
2139
2569
  await runPanelCommand(values);
2140
2570
  return;
2141
2571
  }
2572
+ if (command === "hud") {
2573
+ await runHud(values);
2574
+ return;
2575
+ }
2576
+ if (command === "sentinel") {
2577
+ await runSentinel(values);
2578
+ return;
2579
+ }
2580
+ if (command === "team") {
2581
+ await runTeam(values);
2582
+ return;
2583
+ }
2142
2584
  if (command === "decide") {
2143
2585
  await runDecide(values);
2144
2586
  return;