@longtable/cli 0.1.18 → 0.1.20

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 CHANGED
@@ -27,16 +27,15 @@ approval.
27
27
 
28
28
  ```bash
29
29
  longtable setup --provider codex
30
- longtable init --flow interview
31
30
  longtable start
32
31
  cd "<project-path>"
33
32
  codex
34
33
  ```
35
34
 
36
35
  `longtable setup --provider codex` is the permission-first setup route. It asks
37
- which runtime surfaces LongTable may enable and explains why each choice matters:
38
- CLI only, skills, skills + MCP, skills + MCP + sentinel, intervention posture,
39
- tmux HUD/console, and team discussion mode.
36
+ where LongTable may install support, which runtime surfaces it may enable, how
37
+ strongly it may interrupt research decisions, and whether to create a project
38
+ workspace now. `longtable init` remains only as a deprecated compatibility alias.
40
39
 
41
40
  Return later:
42
41
 
@@ -85,7 +84,7 @@ This is how LongTable avoids turning tacit knowledge into fake certainty.
85
84
  ## Commands
86
85
 
87
86
  ```bash
88
- longtable init
87
+ longtable setup
89
88
  longtable start
90
89
  longtable resume --cwd "<project-path>"
91
90
  longtable roles
package/dist/cli.js CHANGED
@@ -8,7 +8,7 @@ import { stdin as input, stdout as output, cwd, exit } from "node:process";
8
8
  import { dirname, join, resolve } from "node:path";
9
9
  import { homedir } from "node:os";
10
10
  import { classifyCheckpointTrigger } from "@longtable/checkpoints";
11
- import { buildProviderChoices, buildQuickSetupFlow, createPersistedSetupOutput, installRuntimeConfigFromStoredSetup, loadSetupOutput, renderInstallSummary, renderSetupSummary, resolveDefaultRuntimeConfigPath, resolveDefaultSetupPath, saveSetupAndRuntimeConfig, serializeSetupOutput, writeRuntimeConfig } from "@longtable/setup";
11
+ import { buildProviderChoices, buildQuickSetupFlow, createPersistedSetupOutput, installRuntimeConfigFromStoredSetup, loadSetupOutput, renderInstallSummary, renderSetupSummary, resolveDefaultRuntimeConfigPath, resolveDefaultSetupPath, saveSetupOutput, saveSetupAndRuntimeConfig, serializeSetupOutput, writeRuntimeConfig } from "@longtable/setup";
12
12
  import { buildCodexSkillSpecs, buildCodexThinWrappedPrompt, installCodexSkills, listInstalledCodexSkills, renderQuestionRecordPrompt, removeCodexSkills, resolveCodexSkillsDir, runCodexThinWrapper } from "@longtable/provider-codex";
13
13
  import { buildClaudeSkillSpecs, installClaudeSkills, listInstalledClaudeSkills, renderQuestionRecordInput, removeClaudeSkills, resolveClaudeSkillsDir } from "@longtable/provider-claude";
14
14
  import { installCodexPromptAliases, listInstalledCodexPromptAliases, removeCodexPromptAliases, resolveCodexPromptsDir } from "./prompt-aliases.js";
@@ -42,7 +42,7 @@ const ANSI = {
42
42
  green: "\u001B[32m"
43
43
  };
44
44
  const LONGTABLE_MCP_SERVER_NAME = "longtable-state";
45
- const LONGTABLE_MCP_PACKAGE_VERSION = "0.1.18";
45
+ const LONGTABLE_MCP_PACKAGE_VERSION = "0.1.20";
46
46
  const LONGTABLE_MCP_MARKER_START = "# LongTable state MCP START";
47
47
  const LONGTABLE_MCP_MARKER_END = "# LongTable state MCP END";
48
48
  function style(text, prefix) {
@@ -78,9 +78,9 @@ function usage() {
78
78
  " Run `longtable ...` in your terminal, not inside the Codex chat box.",
79
79
  " After `longtable start`, move into the created project directory and open `codex` there.",
80
80
  "",
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>]",
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
+ " longtable setup [--provider codex|claude] [--install-scope user|project|none] [--surfaces cli_only|skills|skills_mcp|skills_mcp_sentinel] [--intervention advisory|balanced|strong] [--workspace create|later] [--project-dir <path>] [--json] [--dir <path>] [--skills-dir <path>] [--runtime-path <file>] [--setup-path <file>]",
82
+ " longtable init [deprecated alias for setup; full legacy flags still supported for automation]",
83
+ " 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]",
84
84
  " longtable resume [--cwd <path>] [--json]",
85
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>]",
86
86
  " longtable status [--cwd <path>] [--fix] [--json] [--codex-dir <path>] [--claude-dir <path>] [--codex-prompts-dir <path>] [--codex-runtime-path <file>] [--claude-runtime-path <file>]",
@@ -109,7 +109,7 @@ function usage() {
109
109
  " longtable mcp install --provider all",
110
110
  "",
111
111
  "Examples:",
112
- " longtable init --flow interview --provider codex --install-skills",
112
+ " longtable setup --provider codex",
113
113
  " longtable start",
114
114
  " longtable start --path ~/Research/My-Project --name \"AI Adoption Meta-Analysis\" --goal \"Narrow the review question\"",
115
115
  " cd \"<project-path>\" && codex",
@@ -197,7 +197,7 @@ function renderQuestionHeader(index, total, section, prompt) {
197
197
  ].join("\n");
198
198
  }
199
199
  function questionSection(questionId) {
200
- if (questionId === "field" || questionId === "careerStage" || questionId === "experienceLevel") {
200
+ if (questionId === "careerStage" || questionId === "experienceLevel") {
201
201
  return "Researcher profile";
202
202
  }
203
203
  if (questionId === "preferredCheckpointIntensity" || questionId === "preferredEntryMode") {
@@ -476,7 +476,7 @@ async function promptMultiChoice(rl, prompt, choices) {
476
476
  });
477
477
  }
478
478
  function hasCompleteFlagInput(args) {
479
- const required = ["provider", "field", "career-stage", "experience", "checkpoint"];
479
+ const required = ["provider", "career-stage", "experience", "checkpoint"];
480
480
  return required.every((key) => typeof args[key] === "string" && String(args[key]).trim().length > 0);
481
481
  }
482
482
  function resolveSetupFlow(args) {
@@ -484,7 +484,9 @@ function resolveSetupFlow(args) {
484
484
  }
485
485
  function toSetupAnswers(args) {
486
486
  return {
487
- field: String(args.field),
487
+ field: typeof args.field === "string" && args.field.trim().length > 0
488
+ ? String(args.field)
489
+ : "unspecified",
488
490
  careerStage: String(args["career-stage"]),
489
491
  experienceLevel: String(args.experience),
490
492
  currentProjectType: typeof args["project-type"] === "string" && args["project-type"].trim().length > 0
@@ -515,6 +517,7 @@ async function collectInteractiveAnswers(initialFlow) {
515
517
  console.log("");
516
518
  const provider = await promptChoice(rl, "Which provider do you want to configure?", buildProviderChoices());
517
519
  const answers = {
520
+ field: "unspecified",
518
521
  currentProjectType: "unspecified research task"
519
522
  };
520
523
  const questions = buildQuickSetupFlow(flow);
@@ -531,8 +534,6 @@ async function collectInteractiveAnswers(initialFlow) {
531
534
  if (!value) {
532
535
  continue;
533
536
  }
534
- if (question.id === "field")
535
- answers.field = value;
536
537
  if (question.id === "careerStage")
537
538
  answers.careerStage = value;
538
539
  if (question.id === "experienceLevel")
@@ -562,77 +563,72 @@ async function collectInteractiveAnswers(initialFlow) {
562
563
  }
563
564
  function buildPermissionSetupChoices() {
564
565
  return {
566
+ installScope: [
567
+ {
568
+ id: "user",
569
+ label: "User-level provider config",
570
+ description: "Why: available across projects. What you get: writes to ~/.codex or ~/.claude. Tradeoff: broader machine-level change."
571
+ },
572
+ {
573
+ id: "project",
574
+ label: "Current project only",
575
+ description: "Why: keeps LongTable local to this repository. What you get: project-scoped runtime files when supported. Tradeoff: not available elsewhere."
576
+ },
577
+ {
578
+ id: "none",
579
+ label: "Do not install provider files",
580
+ description: "Why: safest permission boundary. What you get: CLI setup record only. Tradeoff: no provider-native skills or MCP config."
581
+ }
582
+ ],
565
583
  surfaces: [
566
584
  {
567
585
  id: "cli_only",
568
586
  label: "CLI only",
569
- description: "Why: least invasive. Tradeoff: no natural in-provider LongTable entrypoints."
587
+ description: "Why: least invasive. What you get: setup record and CLI commands. Tradeoff: no natural in-provider LongTable entrypoints."
570
588
  },
571
589
  {
572
590
  id: "skills",
573
591
  label: "Skills",
574
- description: "Why: enables natural LongTable skill routing. Tradeoff: writes provider skill files."
592
+ description: "Why: enables natural LongTable skill routing. What you get: provider skills. Tradeoff: writes provider skill files."
575
593
  },
576
594
  {
577
595
  id: "skills_mcp",
578
596
  label: "Skills + MCP",
579
- description: "Why: adds structured state access. Tradeoff: writes provider config for MCP transport."
597
+ description: "Why: adds structured state access. What you get: skills and MCP config. Tradeoff: writes provider config for MCP transport."
580
598
  },
581
599
  {
582
600
  id: "skills_mcp_sentinel",
583
601
  label: "Skills + MCP + Sentinel",
584
- description: "Why: prepares advisory gap/tacit monitoring. Tradeoff: LongTable may nudge research turns."
602
+ description: "Why: prepares advisory gap/tacit monitoring. What you get: skills, MCP, and sentinel approval. Tradeoff: LongTable may nudge research turns."
585
603
  }
586
604
  ],
587
605
  intervention: [
588
606
  {
589
607
  id: "advisory",
590
608
  label: "Advisory",
591
- description: "Why: notices gaps without blocking. Tradeoff: you may still miss hard commitments."
609
+ description: "Why: notices gaps without blocking. What you get: light nudges. Tradeoff: you may still miss hard commitments."
592
610
  },
593
611
  {
594
612
  id: "balanced",
595
613
  label: "Balanced",
596
- description: "Why: blocks clear theory, measurement, method, or evidence commitments. Tradeoff: occasional stops."
614
+ description: "Why: blocks clear theory, measurement, method, or evidence commitments. What you get: recommended checkpoints. Tradeoff: occasional stops."
597
615
  },
598
616
  {
599
617
  id: "strong",
600
618
  label: "Strong",
601
- description: "Why: maximizes judgment protection. Tradeoff: more interruption before closure."
619
+ description: "Why: maximizes judgment protection. What you get: stricter checkpoints. Tradeoff: more interruption before closure."
602
620
  }
603
621
  ],
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
- },
622
+ workspace: [
627
623
  {
628
- id: "panel",
629
- label: "Structured panel",
630
- description: "Why: role disagreement is visible without tmux. Tradeoff: not parallel."
624
+ id: "create",
625
+ label: "Yes, create one now",
626
+ description: "Why: durable state needs .longtable/. What you get: decision log and CURRENT.md. Tradeoff: asks project-specific questions now."
631
627
  },
632
628
  {
633
- id: "tmux_team",
634
- label: "Tmux team discussion",
635
- description: "Why: opens role panes for parallel debate. Tradeoff: terminal complexity and cleanup."
629
+ id: "later",
630
+ label: "No, prepare runtime only",
631
+ description: "Why: keeps setup short. What you get: runtime support without project state. Tradeoff: no durable research memory until `longtable start`."
636
632
  }
637
633
  ]
638
634
  };
@@ -644,6 +640,65 @@ function checkpointIntensityFromIntervention(choice) {
644
640
  return "low";
645
641
  return "balanced";
646
642
  }
643
+ function shouldInstallSkills(scope, surfaces) {
644
+ return scope !== "none" && surfaces !== "cli_only";
645
+ }
646
+ function shouldInstallMcp(scope, surfaces) {
647
+ return scope !== "none" && (surfaces === "skills_mcp" || surfaces === "skills_mcp_sentinel");
648
+ }
649
+ function setupProjectRoot(args) {
650
+ return resolve(normalizeUserPath(typeof args["project-dir"] === "string" && args["project-dir"].trim()
651
+ ? args["project-dir"].trim()
652
+ : cwd()));
653
+ }
654
+ function setupInstallDir(provider, scope, customDir, projectRoot) {
655
+ if (customDir)
656
+ return customDir;
657
+ if (scope === "project")
658
+ return join(projectRoot, provider === "codex" ? ".codex" : ".claude", "skills");
659
+ return undefined;
660
+ }
661
+ function setupPathForScope(scope, args, projectRoot) {
662
+ if (typeof args["setup-path"] === "string")
663
+ return args["setup-path"];
664
+ if (scope === "project")
665
+ return join(projectRoot, ".longtable", "setup.json");
666
+ return undefined;
667
+ }
668
+ function runtimePathForScope(provider, scope, args, projectRoot) {
669
+ if (typeof args["runtime-path"] === "string")
670
+ return args["runtime-path"];
671
+ if (scope !== "project")
672
+ return undefined;
673
+ return join(projectRoot, ".longtable", provider === "codex" ? "codex-runtime.toml" : "claude-runtime.json");
674
+ }
675
+ function mcpArgsForScope(provider, scope, args, projectRoot) {
676
+ if (scope !== "project")
677
+ return args;
678
+ return {
679
+ ...args,
680
+ ...(provider === "codex" && typeof args["codex-config"] !== "string"
681
+ ? { "codex-config": join(projectRoot, ".codex", "config.toml") }
682
+ : {}),
683
+ ...(provider === "claude" && typeof args["claude-settings"] !== "string"
684
+ ? { "claude-settings": join(projectRoot, ".claude", "settings.json") }
685
+ : {})
686
+ };
687
+ }
688
+ function parseSetupInstallScope(value) {
689
+ return value === "user" || value === "project" || value === "none" ? value : undefined;
690
+ }
691
+ function parseSetupSurface(value) {
692
+ return value === "cli_only" || value === "skills" || value === "skills_mcp" || value === "skills_mcp_sentinel"
693
+ ? value
694
+ : undefined;
695
+ }
696
+ function parseSetupIntervention(value) {
697
+ return value === "advisory" || value === "balanced" || value === "strong" ? value : undefined;
698
+ }
699
+ function parseSetupWorkspace(value) {
700
+ return value === "create" || value === "later" ? value : undefined;
701
+ }
647
702
  async function runSetup(args) {
648
703
  const json = args.json === true;
649
704
  const rl = createInterface({ input, output });
@@ -652,27 +707,30 @@ async function runSetup(args) {
652
707
  ? (args.provider === "claude" ? "claude" : "codex")
653
708
  : await promptChoice(rl, "Which provider should LongTable configure?", buildProviderChoices()));
654
709
  const choices = buildPermissionSetupChoices();
655
- const surfaces = await promptChoice(rl, [
710
+ const installScope = parseSetupInstallScope(args["install-scope"]) ?? await promptChoice(rl, "Where may LongTable install runtime support?", choices.installScope);
711
+ const surfaces = parseSetupSurface(args.surfaces) ?? await promptChoice(rl, [
656
712
  "Which LongTable runtime surfaces should be enabled?",
657
713
  "This is a permission choice because skills, MCP, and sentinel support write provider-facing runtime files."
658
714
  ].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);
715
+ const intervention = parseSetupIntervention(args.intervention) ?? await promptChoice(rl, "How strongly may LongTable interrupt research decisions?", choices.intervention);
716
+ const workspacePreference = parseSetupWorkspace(args.workspace) ?? await promptChoice(rl, "Should LongTable create a project workspace now?", choices.workspace);
717
+ const projectRoot = setupProjectRoot(args);
662
718
  const outputValue = createPersistedSetupOutput({
663
719
  field: "unspecified",
664
720
  careerStage: "unspecified",
665
721
  experienceLevel: "advanced",
666
722
  preferredCheckpointIntensity: checkpointIntensityFromIntervention(intervention),
667
723
  preferredEntryMode: "explore",
668
- panelPreference: teamMode === "off" ? "show_on_conflict" : "always_visible"
724
+ panelPreference: "show_on_conflict"
669
725
  }, provider, "quickstart");
670
726
  outputValue.initialState.explicitState = {
671
727
  ...outputValue.initialState.explicitState,
728
+ installScope,
672
729
  runtimeSurfaces: surfaces,
673
730
  interventionPosture: intervention,
674
- tmuxMode,
675
- teamMode
731
+ workspaceCreationPreference: workspacePreference,
732
+ tmuxMode: "standard",
733
+ teamMode: "panel"
676
734
  };
677
735
  if (surfaces === "skills_mcp_sentinel") {
678
736
  outputValue.initialState.inferredHypotheses.push({
@@ -682,44 +740,64 @@ async function runSetup(args) {
682
740
  status: "confirmed"
683
741
  });
684
742
  }
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"
743
+ const setupPath = setupPathForScope(installScope, args, projectRoot);
744
+ const runtimePath = runtimePathForScope(provider, installScope, args, projectRoot);
745
+ const result = installScope === "none"
746
+ ? {
747
+ provider,
748
+ setupTarget: await saveSetupOutput(outputValue, setupPath)
749
+ }
750
+ : await saveSetupAndRuntimeConfig(outputValue, {
751
+ setupPath,
752
+ runtimePath
753
+ });
754
+ const scopedInstallDir = setupInstallDir(provider, installScope, typeof args["skills-dir"] === "string" ? args["skills-dir"] : typeof args.dir === "string" ? args.dir : undefined, projectRoot);
755
+ const installedSkills = !shouldInstallSkills(installScope, surfaces)
690
756
  ? []
691
757
  : 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`);
758
+ ? await installCodexSkills(listRoleDefinitions(), scopedInstallDir)
759
+ : await installClaudeSkills(listRoleDefinitions(), scopedInstallDir);
760
+ let mcpInstall;
761
+ if (shouldInstallMcp(installScope, surfaces)) {
762
+ mcpInstall = await installMcpForSetup(provider, mcpArgsForScope(provider, installScope, args, projectRoot));
699
763
  }
700
764
  if (json) {
701
765
  console.log(JSON.stringify({
702
766
  setup: outputValue,
703
767
  runtime: result,
704
768
  installedSkills: installedSkills.map((skill) => skill.name),
705
- mcpRequested,
706
- tmuxMode,
707
- teamMode
769
+ mcpInstall,
770
+ workspacePreference
708
771
  }, null, 2));
709
772
  return;
710
773
  }
711
774
  console.log("");
712
775
  console.log(renderSetupSummary(outputValue));
713
776
  console.log("");
714
- console.log(renderInstallSummary(result));
777
+ if ("runtimeTarget" in result) {
778
+ console.log(renderInstallSummary(result));
779
+ }
780
+ else {
781
+ console.log("LongTable setup summary");
782
+ console.log(`setup path: ${result.setupTarget.path}`);
783
+ console.log("provider files: not installed by researcher choice");
784
+ }
715
785
  console.log(`Installed skills: ${installedSkills.length}`);
716
- if (tmuxMode !== "standard") {
786
+ if (mcpInstall) {
787
+ console.log("");
788
+ console.log(renderMcpInstallSummary(mcpInstall));
789
+ }
790
+ if (surfaces === "skills_mcp_sentinel") {
717
791
  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 \"...\"");
792
+ console.log("Background sentinel approval recorded.");
793
+ console.log("Hook installation remains opt-in; LongTable will not install hooks without an explicit hook command.");
794
+ }
795
+ if (workspacePreference === "create") {
796
+ console.log("");
797
+ console.log("Project workspace requested. LongTable will now run `longtable start` with research-object and gap-risk prompts.");
798
+ await runStart({
799
+ setup: result.setupTarget.path
800
+ });
723
801
  }
724
802
  }
725
803
  finally {
@@ -733,6 +811,33 @@ function perspectiveChoices() {
733
811
  description: persona.shortDescription
734
812
  }));
735
813
  }
814
+ function researchObjectChoices() {
815
+ return [
816
+ { id: "research_question", label: "Research question", description: "The main question, problem, or contribution boundary." },
817
+ { id: "theory_framework", label: "Theory framework", description: "Constructs, theory choice, or conceptual model." },
818
+ { id: "measurement_instrument", label: "Measurement/instrument", description: "Variables, scales, instruments, or operationalization." },
819
+ { id: "study_design", label: "Study design", description: "Methods, sample, intervention, or data collection design." },
820
+ { id: "analysis_plan", label: "Analysis plan", description: "Analytic strategy, models, coding, or interpretation plan." },
821
+ { id: "manuscript", label: "Manuscript", description: "Drafting, revision, voice, evidence, or submission writing." }
822
+ ];
823
+ }
824
+ function gapRiskChoices() {
825
+ return [
826
+ { id: "known_gap", label: "I know the gap", description: "The blocker is explicit and can be tracked directly." },
827
+ { id: "suspected_tacit_assumptions", label: "I suspect tacit assumptions", description: "There may be hidden commitments or unstated premises." },
828
+ { id: "diagnose", label: "Ask LongTable to diagnose it", description: "Let LongTable classify likely gaps during the session." }
829
+ ];
830
+ }
831
+ function protectedDecisionChoices() {
832
+ return [
833
+ { id: "theory", label: "Theory", description: "Do not let theory or construct choices settle quietly." },
834
+ { id: "measurement", label: "Measurement", description: "Do not let variables, scales, or instruments settle quietly." },
835
+ { id: "method", label: "Method", description: "Do not let design, sampling, or procedure choices settle quietly." },
836
+ { id: "evidence_citation", label: "Evidence/citation", description: "Do not let unsupported source or citation choices settle quietly." },
837
+ { id: "authorship_voice", label: "Authorship/voice", description: "Do not let writing voice or authorial judgment disappear quietly." },
838
+ { id: "submission_public_sharing", label: "Submission/public sharing", description: "Do not let public-facing commitments settle quietly." }
839
+ ];
840
+ }
736
841
  function normalizePerspectiveList(value) {
737
842
  if (!value?.trim()) {
738
843
  return [];
@@ -747,6 +852,9 @@ async function collectProjectInterview(setup, args) {
747
852
  !(typeof args.path === "string" && args.path.trim()) ||
748
853
  !(typeof args.goal === "string" && args.goal.trim()) ||
749
854
  typeof args.blocker !== "string" ||
855
+ !(typeof args["research-object"] === "string" && args["research-object"].trim()) ||
856
+ !(typeof args["gap-risk"] === "string" && args["gap-risk"].trim()) ||
857
+ !(typeof args["protected-decision"] === "string" && args["protected-decision"].trim()) ||
750
858
  normalizePerspectiveList(typeof args.perspectives === "string" ? args.perspectives : undefined).length === 0 ||
751
859
  !(typeof args.disagreement === "string" && args.disagreement.trim());
752
860
  const rl = createInterface({ input, output });
@@ -762,23 +870,29 @@ async function collectProjectInterview(setup, args) {
762
870
  console.log("");
763
871
  }
764
872
  const projectName = (typeof args.name === "string" && args.name.trim()) ||
765
- (await promptText(rl, renderQuestionHeader(1, 6, "Project interview", "What should this project be called?"), true));
873
+ (await promptText(rl, renderQuestionHeader(1, 9, "Project interview", "What should this project be called?"), true));
766
874
  const suggestedParentDir = typeof args.path === "string" && args.path.trim()
767
875
  ? normalizeUserPath(args.path.trim())
768
876
  : homedir();
769
877
  const suggestedPath = resolveInteractiveProjectPath(suggestedParentDir, projectName);
770
878
  const projectPath = (typeof args.path === "string" && args.path.trim()
771
879
  ? normalizeUserPath(args.path.trim())
772
- : resolveInteractiveProjectPath((await promptText(rl, renderQuestionHeader(2, 6, "Project interview", `Which parent directory should contain this project?\nLongTable will create this folder:\n${suggestedPath}`), true)), projectName));
880
+ : 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));
773
881
  const currentGoal = (typeof args.goal === "string" && args.goal.trim()) ||
774
- (await promptText(rl, renderQuestionHeader(3, 6, "Current session", "What are you trying to accomplish in this session?"), true));
882
+ (await promptText(rl, renderQuestionHeader(3, 9, "Current session", "What are you trying to accomplish in this session?"), true));
775
883
  const currentBlocker = (typeof args.blocker === "string" && args.blocker.trim()) ||
776
- (await promptText(rl, renderQuestionHeader(4, 6, "Current session", "What is the main blocker or uncertainty right now?"), false));
884
+ (await promptText(rl, renderQuestionHeader(4, 9, "Current session", "What is the main blocker or uncertainty right now?"), false));
885
+ const researchObject = (typeof args["research-object"] === "string" && args["research-object"].trim()) ||
886
+ await promptChoice(rl, renderQuestionHeader(5, 9, "Research object", "What kind of research object are we protecting right now?"), researchObjectChoices());
887
+ const gapRisk = (typeof args["gap-risk"] === "string" && args["gap-risk"].trim()) ||
888
+ await promptChoice(rl, renderQuestionHeader(6, 9, "Gap/tacit risk", "What is the most likely gap risk at the start of this workspace?"), gapRiskChoices());
889
+ const protectedDecision = (typeof args["protected-decision"] === "string" && args["protected-decision"].trim()) ||
890
+ await promptChoice(rl, renderQuestionHeader(7, 9, "Protected decision", "Which decision should LongTable not let you settle quietly?"), protectedDecisionChoices());
777
891
  const requestedPerspectives = normalizePerspectiveList(typeof args.perspectives === "string" ? args.perspectives : undefined).length > 0
778
892
  ? normalizePerspectiveList(typeof args.perspectives === "string" ? args.perspectives : undefined)
779
- : await promptMultiChoice(rl, renderQuestionHeader(5, 6, "Perspectives", "Which perspectives do you already know you want at the table? Leave everything unchecked for auto."), perspectiveChoices());
893
+ : await promptMultiChoice(rl, renderQuestionHeader(8, 9, "Perspectives", "Which perspectives do you already know you want at the table? Leave everything unchecked for auto."), perspectiveChoices());
780
894
  const disagreementPreference = (typeof args.disagreement === "string" && args.disagreement.trim()) ||
781
- (await promptChoice(rl, renderQuestionHeader(6, 6, "Disagreement", "How visible should disagreement between perspectives be in this project by default?"), [
895
+ (await promptChoice(rl, renderQuestionHeader(9, 9, "Disagreement", "How visible should disagreement between perspectives be in this project by default?"), [
782
896
  {
783
897
  id: "synthesis_only",
784
898
  label: "Synthesis only",
@@ -800,6 +914,9 @@ async function collectProjectInterview(setup, args) {
800
914
  projectPath: projectPath.trim(),
801
915
  currentGoal: currentGoal.trim(),
802
916
  ...(currentBlocker?.trim() ? { currentBlocker: currentBlocker.trim() } : {}),
917
+ researchObject: researchObject.trim(),
918
+ gapRisk: gapRisk.trim(),
919
+ protectedDecision: protectedDecision.trim(),
803
920
  requestedPerspectives,
804
921
  disagreementPreference: disagreementPreference
805
922
  };
@@ -813,7 +930,7 @@ function normalizePersistAnswers(raw) {
813
930
  flow: raw.flow === "interview" ? "interview" : "quickstart",
814
931
  provider: raw.provider === "claude" ? "claude" : "codex",
815
932
  answers: {
816
- field: raw.field,
933
+ field: raw.field?.trim() ? raw.field.trim() : "unspecified",
817
934
  careerStage: raw.careerStage,
818
935
  experienceLevel: raw.experienceLevel,
819
936
  currentProjectType: "unspecified research task",
@@ -858,6 +975,11 @@ async function readPersistAnswers(args) {
858
975
  throw new Error("persist-init requires either --answers-json, --stdin, or the full set of setup flags.");
859
976
  }
860
977
  async function runInit(args) {
978
+ if (!hasCompleteFlagInput(args)) {
979
+ console.error("`longtable init` is deprecated. Use `longtable setup` for permission-first runtime setup.");
980
+ await runSetup(args);
981
+ return;
982
+ }
861
983
  const json = args.json === true;
862
984
  const installRuntime = args["no-install"] !== true;
863
985
  const installPrompts = args["install-prompts"] === true;
@@ -1055,6 +1177,34 @@ function renderMcpInstallSummary(result) {
1055
1177
  }
1056
1178
  return lines.join("\n").trimEnd();
1057
1179
  }
1180
+ async function installMcpForSetup(provider, args) {
1181
+ const serverName = typeof args.name === "string" && args.name.trim()
1182
+ ? args.name.trim()
1183
+ : LONGTABLE_MCP_SERVER_NAME;
1184
+ const packageSpec = resolveMcpPackageSpec(args);
1185
+ const command = typeof args.command === "string" && args.command.trim() ? args.command.trim() : "npx";
1186
+ const mcpArgs = command === "npx" ? ["-y", packageSpec] : [packageSpec];
1187
+ const targets = [];
1188
+ if (provider === "codex") {
1189
+ const path = resolveCodexMcpConfigPath(args);
1190
+ const block = renderCodexMcpBlock(serverName, command, mcpArgs);
1191
+ const content = await writeCodexMcpConfig(path, block, serverName);
1192
+ targets.push({ provider, path, format: "toml", content });
1193
+ }
1194
+ if (provider === "claude") {
1195
+ const path = resolveClaudeMcpSettingsPath(args);
1196
+ const content = await writeClaudeMcpSettings(path, serverName, command, mcpArgs);
1197
+ targets.push({ provider, path, format: "json", content });
1198
+ }
1199
+ return {
1200
+ serverName,
1201
+ packageSpec,
1202
+ command,
1203
+ args: mcpArgs,
1204
+ write: true,
1205
+ targets
1206
+ };
1207
+ }
1058
1208
  async function runMcpSubcommand(subcommand, args) {
1059
1209
  if (!subcommand || subcommand === "install" || subcommand === "print-config") {
1060
1210
  const serverName = typeof args.name === "string" && args.name.trim()
@@ -1267,7 +1417,7 @@ function renderDoctorStatus(status) {
1267
1417
  nextActions.push("longtable doctor --fix");
1268
1418
  }
1269
1419
  if (!status.setupExists) {
1270
- nextActions.push("longtable init --flow interview --provider codex --install-skills");
1420
+ nextActions.push("longtable setup --provider codex");
1271
1421
  }
1272
1422
  if (!status.workspace.found) {
1273
1423
  nextActions.push("longtable start");
@@ -1359,7 +1509,7 @@ async function repairDoctorStatus(args, status) {
1359
1509
  repair.removedLegacyPromptFiles = await removeCodexPromptAliases(codexPromptsDir);
1360
1510
  }
1361
1511
  if (!status.setupExists) {
1362
- repair.skipped.push("runtime configs require a researcher setup; run `longtable init --flow interview --provider codex` first");
1512
+ repair.skipped.push("runtime configs require setup approval; run `longtable setup --provider codex` first");
1363
1513
  return repair;
1364
1514
  }
1365
1515
  const setup = await loadSetupOutput(setupOverride);
@@ -2303,7 +2453,7 @@ async function runStart(args) {
2303
2453
  const setupPath = typeof args.setup === "string" ? args.setup : undefined;
2304
2454
  const existingSetup = await loadOptionalSetup(setupPath);
2305
2455
  if (!existingSetup) {
2306
- throw new Error("LongTable global setup is missing. Run `longtable init --flow interview` first.");
2456
+ throw new Error("LongTable setup is missing. Run `longtable setup --provider codex` first.");
2307
2457
  }
2308
2458
  const interview = await collectProjectInterview(existingSetup, args);
2309
2459
  await verifyWritableWorkspaceParent(interview.projectPath);
@@ -2312,6 +2462,9 @@ async function runStart(args) {
2312
2462
  projectPath: interview.projectPath,
2313
2463
  currentGoal: interview.currentGoal,
2314
2464
  currentBlocker: interview.currentBlocker,
2465
+ researchObject: interview.researchObject,
2466
+ gapRisk: interview.gapRisk,
2467
+ protectedDecision: interview.protectedDecision,
2315
2468
  requestedPerspectives: interview.requestedPerspectives,
2316
2469
  disagreementPreference: interview.disagreementPreference,
2317
2470
  setup: existingSetup
@@ -28,6 +28,9 @@ export interface LongTableSessionRecord {
28
28
  projectPath: string;
29
29
  currentGoal: string;
30
30
  currentBlocker?: string;
31
+ researchObject?: string;
32
+ gapRisk?: string;
33
+ protectedDecision?: string;
31
34
  nextAction?: string;
32
35
  openQuestions?: string[];
33
36
  requestedPerspectives: string[];
@@ -150,6 +153,9 @@ export declare function createOrUpdateProjectWorkspace(options: {
150
153
  projectPath: string;
151
154
  currentGoal: string;
152
155
  currentBlocker?: string;
156
+ researchObject?: string;
157
+ gapRisk?: string;
158
+ protectedDecision?: string;
153
159
  requestedPerspectives: string[];
154
160
  disagreementPreference: ProjectDisagreementPreference;
155
161
  setup: SetupPersistedOutput;
@@ -99,6 +99,9 @@ function buildCurrentGuide(project, session, recentInvocations = [], pendingQues
99
99
  "## 지금 초점",
100
100
  `- 현재 목표: ${session.currentGoal}`,
101
101
  ...(session.currentBlocker ? [`- 현재 blocker: ${session.currentBlocker}`] : []),
102
+ ...(session.researchObject ? [`- 연구 객체: ${session.researchObject}`] : []),
103
+ ...(session.gapRisk ? [`- 공백/암묵지 위험: ${session.gapRisk}`] : []),
104
+ ...(session.protectedDecision ? [`- 보호할 결정: ${session.protectedDecision}`] : []),
102
105
  `- 다음 액션: ${nextAction}`,
103
106
  `- 관점: ${session.requestedPerspectives.length > 0 ? session.requestedPerspectives.join(", ") : "auto"}`,
104
107
  `- disagreement: ${session.disagreementPreference}`,
@@ -148,6 +151,9 @@ function buildCurrentGuide(project, session, recentInvocations = [], pendingQues
148
151
  "## Focus Now",
149
152
  `- Current goal: ${session.currentGoal}`,
150
153
  ...(session.currentBlocker ? [`- Current blocker: ${session.currentBlocker}`] : []),
154
+ ...(session.researchObject ? [`- Research object: ${session.researchObject}`] : []),
155
+ ...(session.gapRisk ? [`- Gap/tacit risk: ${session.gapRisk}`] : []),
156
+ ...(session.protectedDecision ? [`- Protected decision: ${session.protectedDecision}`] : []),
151
157
  `- Next action: ${nextAction}`,
152
158
  `- Perspectives: ${session.requestedPerspectives.length > 0 ? session.requestedPerspectives.join(", ") : "auto"}`,
153
159
  `- Disagreement: ${session.disagreementPreference}`,
@@ -335,6 +341,9 @@ function buildProjectAgentsMd(project, session) {
335
341
  `- Project: ${project.projectName}`,
336
342
  `- Current goal: ${session.currentGoal}`,
337
343
  ...(session.currentBlocker ? [`- Current blocker: ${session.currentBlocker}`] : []),
344
+ ...(session.researchObject ? [`- Research object: ${session.researchObject}`] : []),
345
+ ...(session.gapRisk ? [`- Gap/tacit risk: ${session.gapRisk}`] : []),
346
+ ...(session.protectedDecision ? [`- Protected decision: ${session.protectedDecision}`] : []),
338
347
  `- Requested perspectives: ${session.requestedPerspectives.length > 0 ? session.requestedPerspectives.join(", ") : "auto"}`,
339
348
  `- Disagreement visibility: ${session.disagreementPreference}`,
340
349
  "- These instructions apply to this directory and its children."
@@ -343,9 +352,9 @@ function buildProjectAgentsMd(project, session) {
343
352
  function buildStateSeed(project, session, setup) {
344
353
  const state = createEmptyResearchState();
345
354
  state.explicitState = {
346
- field: setup.profileSeed.field,
347
- careerStage: setup.profileSeed.careerStage,
348
- experienceLevel: setup.profileSeed.experienceLevel,
355
+ field: setup.profileSeed.field ?? "unspecified",
356
+ careerStage: setup.profileSeed.careerStage ?? "unspecified",
357
+ experienceLevel: setup.profileSeed.experienceLevel ?? "advanced",
349
358
  projectName: project.projectName,
350
359
  disagreementPreference: session.disagreementPreference,
351
360
  requestedPerspectives: session.requestedPerspectives
@@ -353,6 +362,9 @@ function buildStateSeed(project, session, setup) {
353
362
  state.workingState = {
354
363
  currentGoal: session.currentGoal,
355
364
  ...(session.currentBlocker ? { currentBlocker: session.currentBlocker } : {}),
365
+ ...(session.researchObject ? { researchObject: session.researchObject } : {}),
366
+ ...(session.gapRisk ? { gapRisk: session.gapRisk } : {}),
367
+ ...(session.protectedDecision ? { protectedDecision: session.protectedDecision } : {}),
356
368
  ...(session.nextAction ? { nextAction: session.nextAction } : {}),
357
369
  openQuestions: session.openQuestions ?? [],
358
370
  activeModes: session.activeModes ?? [],
@@ -361,6 +373,9 @@ function buildStateSeed(project, session, setup) {
361
373
  if (session.currentBlocker) {
362
374
  state.openTensions.push(session.currentBlocker);
363
375
  }
376
+ if (session.gapRisk) {
377
+ state.openTensions.push(`Gap/tacit risk: ${session.gapRisk}`);
378
+ }
364
379
  if (setup.profileSeed.humanAuthorshipSignal) {
365
380
  state.explicitState.humanAuthorshipSignal = setup.profileSeed.humanAuthorshipSignal;
366
381
  }
@@ -384,6 +399,21 @@ function buildStateSeed(project, session, setup) {
384
399
  importance: "high"
385
400
  });
386
401
  }
402
+ if (session.researchObject || session.gapRisk || session.protectedDecision) {
403
+ state.narrativeTraces.push({
404
+ id: "project-session-risk-profile",
405
+ timestamp: nowIso(),
406
+ source: "longtable-start",
407
+ traceType: "tension",
408
+ summary: [
409
+ session.researchObject ? `Research object: ${session.researchObject}.` : "",
410
+ session.gapRisk ? `Gap/tacit risk: ${session.gapRisk}.` : "",
411
+ session.protectedDecision ? `Protected decision: ${session.protectedDecision}.` : ""
412
+ ].filter(Boolean).join(" "),
413
+ visibility: "explicit",
414
+ importance: "high"
415
+ });
416
+ }
387
417
  return JSON.stringify(state, null, 2);
388
418
  }
389
419
  async function removeLegacyRootFiles(projectPath) {
@@ -907,10 +937,10 @@ export async function createOrUpdateProjectWorkspace(options) {
907
937
  contractVersion: "workspace-v2",
908
938
  locale,
909
939
  globalSetupSummary: {
910
- field: options.setup.profileSeed.field,
911
- careerStage: options.setup.profileSeed.careerStage,
912
- experienceLevel: options.setup.profileSeed.experienceLevel,
913
- checkpointIntensity: options.setup.profileSeed.preferredCheckpointIntensity,
940
+ field: options.setup.profileSeed.field ?? "unspecified",
941
+ careerStage: options.setup.profileSeed.careerStage ?? "unspecified",
942
+ experienceLevel: options.setup.profileSeed.experienceLevel ?? "advanced",
943
+ checkpointIntensity: options.setup.profileSeed.preferredCheckpointIntensity ?? "balanced",
914
944
  ...(options.setup.profileSeed.humanAuthorshipSignal
915
945
  ? { humanAuthorshipSignal: options.setup.profileSeed.humanAuthorshipSignal }
916
946
  : {}),
@@ -931,6 +961,9 @@ export async function createOrUpdateProjectWorkspace(options) {
931
961
  projectPath,
932
962
  currentGoal: options.currentGoal,
933
963
  ...(options.currentBlocker ? { currentBlocker: options.currentBlocker } : {}),
964
+ ...(options.researchObject ? { researchObject: options.researchObject } : {}),
965
+ ...(options.gapRisk ? { gapRisk: options.gapRisk } : {}),
966
+ ...(options.protectedDecision ? { protectedDecision: options.protectedDecision } : {}),
934
967
  nextAction: buildNextAction({
935
968
  schemaVersion: 1,
936
969
  id: sessionId,
@@ -939,6 +972,9 @@ export async function createOrUpdateProjectWorkspace(options) {
939
972
  projectPath,
940
973
  currentGoal: options.currentGoal,
941
974
  ...(options.currentBlocker ? { currentBlocker: options.currentBlocker } : {}),
975
+ ...(options.researchObject ? { researchObject: options.researchObject } : {}),
976
+ ...(options.gapRisk ? { gapRisk: options.gapRisk } : {}),
977
+ ...(options.protectedDecision ? { protectedDecision: options.protectedDecision } : {}),
942
978
  requestedPerspectives: options.requestedPerspectives,
943
979
  disagreementPreference: options.disagreementPreference
944
980
  }),
@@ -950,6 +986,9 @@ export async function createOrUpdateProjectWorkspace(options) {
950
986
  projectPath,
951
987
  currentGoal: options.currentGoal,
952
988
  ...(options.currentBlocker ? { currentBlocker: options.currentBlocker } : {}),
989
+ ...(options.researchObject ? { researchObject: options.researchObject } : {}),
990
+ ...(options.gapRisk ? { gapRisk: options.gapRisk } : {}),
991
+ ...(options.protectedDecision ? { protectedDecision: options.protectedDecision } : {}),
953
992
  requestedPerspectives: options.requestedPerspectives,
954
993
  disagreementPreference: options.disagreementPreference
955
994
  }),
@@ -964,6 +1003,9 @@ export async function createOrUpdateProjectWorkspace(options) {
964
1003
  projectPath,
965
1004
  currentGoal: options.currentGoal,
966
1005
  ...(options.currentBlocker ? { currentBlocker: options.currentBlocker } : {}),
1006
+ ...(options.researchObject ? { researchObject: options.researchObject } : {}),
1007
+ ...(options.gapRisk ? { gapRisk: options.gapRisk } : {}),
1008
+ ...(options.protectedDecision ? { protectedDecision: options.protectedDecision } : {}),
967
1009
  requestedPerspectives: options.requestedPerspectives,
968
1010
  disagreementPreference: options.disagreementPreference
969
1011
  }),
@@ -33,9 +33,9 @@ function promptSpec() {
33
33
  "Ask exactly one setup question at a time.",
34
34
  "Use numbered choices when possible and include a 'None of the above' option when needed.",
35
35
  "Do not move to the next question until the researcher answers the current one.",
36
- "Quickstart covers: provider, field, career stage, experience level, checkpoint intensity, and human authorship signal.",
36
+ "Quickstart covers: provider, career stage, experience level, checkpoint intensity, and human authorship signal. Do not ask for research field during setup unless the researcher volunteers it.",
37
37
  "Interview also covers: preferred entry mode, weakest domain, and panel visibility preference.",
38
- "After collecting all answers, summarize the proposed setup and then output both: 1) the exact `longtable codex persist-init ... --install-skills` command and 2) a strict JSON object with keys provider, flow, field, careerStage, experienceLevel, preferredCheckpointIntensity, and optional humanAuthorshipSignal, preferredEntryMode, weakestDomain, panelPreference.",
38
+ "After collecting all answers, summarize the proposed setup and then output both: 1) the exact `longtable codex persist-init ... --install-skills` command and 2) a strict JSON object with keys provider, flow, careerStage, experienceLevel, preferredCheckpointIntensity, and optional field, humanAuthorshipSignal, preferredEntryMode, weakestDomain, panelPreference.",
39
39
  "If the user prefers paste-based setup, tell them they can pipe the JSON into `longtable codex persist-init --stdin --install-skills`.",
40
40
  "If the researcher asks you to stay inside Codex, keep the conversation in numbered form and do not prematurely close.",
41
41
  "Frame the setup like a short researcher interview, not a bare config form.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@longtable/cli",
3
- "version": "0.1.18",
3
+ "version": "0.1.20",
4
4
  "private": false,
5
5
  "description": "Researcher-facing LongTable CLI",
6
6
  "type": "module",
@@ -28,12 +28,12 @@
28
28
  "typecheck": "tsc -p tsconfig.json --noEmit"
29
29
  },
30
30
  "dependencies": {
31
- "@longtable/checkpoints": "0.1.18",
32
- "@longtable/core": "0.1.18",
33
- "@longtable/memory": "0.1.18",
34
- "@longtable/provider-claude": "0.1.18",
35
- "@longtable/provider-codex": "0.1.18",
36
- "@longtable/setup": "0.1.18"
31
+ "@longtable/checkpoints": "0.1.20",
32
+ "@longtable/core": "0.1.20",
33
+ "@longtable/memory": "0.1.20",
34
+ "@longtable/provider-claude": "0.1.20",
35
+ "@longtable/provider-codex": "0.1.20",
36
+ "@longtable/setup": "0.1.20"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@types/node": "^22.10.1",