@longtable/cli 0.1.31 → 0.1.33

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
@@ -8,9 +8,9 @@ Claude skills, and future MCP surfaces remain generated adapter artifacts.
8
8
 
9
9
  The basic contract is:
10
10
 
11
- 1. seed the researcher profile once
12
- 2. create a workspace for each project
13
- 3. continue the research conversation inside that workspace
11
+ 1. approve provider/runtime support once
12
+ 2. start each project inside the provider with `$longtable-interview`
13
+ 3. create or resume a workspace from that interview
14
14
  4. preserve decisions, tensions, and evidence as durable project state
15
15
 
16
16
  ## Install
@@ -26,15 +26,17 @@ config, hooks, or provider runtime files without explicit setup approval.
26
26
 
27
27
  ```bash
28
28
  longtable setup --provider codex
29
- longtable start
30
- cd "<project-path>"
29
+ cd "<research-folder>"
31
30
  codex
32
31
  ```
33
32
 
33
+ Then invoke `$longtable-interview` inside Codex.
34
+
34
35
  `longtable setup --provider codex` is the permission-first setup route. It asks
35
36
  where LongTable may install support, which runtime surfaces it may enable, how
36
- strongly it may interrupt research decisions, and whether to create a project
37
- workspace now. `longtable init` remains only as a deprecated compatibility alias.
37
+ strongly it may interrupt research decisions, and whether to show the
38
+ provider-native interview launch steps. `longtable init` remains only as a
39
+ deprecated compatibility alias.
38
40
 
39
41
  Return later:
40
42
 
@@ -44,7 +46,7 @@ longtable resume
44
46
  codex
45
47
  ```
46
48
 
47
- ## What `longtable start` Creates
49
+ ## What `$longtable-interview` Creates
48
50
 
49
51
  ```text
50
52
  <project>/
@@ -84,7 +86,6 @@ This is how LongTable avoids turning tacit knowledge into fake certainty.
84
86
 
85
87
  ```bash
86
88
  longtable setup
87
- longtable start
88
89
  longtable resume --cwd "<project-path>"
89
90
  longtable roles
90
91
  longtable ask --cwd "<project-path>" --prompt "..."
@@ -136,12 +137,14 @@ longtable claude install-skills
136
137
 
137
138
  Codex skills include `longtable`, `longtable-panel`, and generated role-specific
138
139
  skills such as `longtable-methods-critic`. If your Codex build exposes explicit
139
- skill shortcuts, `$longtable` is the manual entry. Do not depend on `/prompts`;
140
- current Codex builds may reject it.
140
+ skill shortcuts, `$longtable-interview` is the research-start entry and
141
+ `$longtable` is the general router. Do not depend on `/prompts`; current Codex
142
+ builds may reject it.
141
143
 
142
144
  Claude Code skills include `longtable`, `longtable-panel`, and generated
143
- role-specific skills such as `longtable-methods-critic`. They are adapter files
144
- generated from the LongTable role registry.
145
+ role-specific skills such as `longtable-methods-critic`. They also include
146
+ `longtable-interview` for the First Research Shape workflow. They are adapter
147
+ files generated from the LongTable role registry.
145
148
 
146
149
  ## Panel Orchestration
147
150
 
package/dist/cli.js CHANGED
@@ -6,6 +6,7 @@ import { createInterface } from "node:readline/promises";
6
6
  import { stdin as input, stdout as output, cwd, env, exit } from "node:process";
7
7
  import { dirname, join, resolve } from "node:path";
8
8
  import { homedir } from "node:os";
9
+ import { fileURLToPath } from "node:url";
9
10
  import { classifyCheckpointTrigger } from "@longtable/checkpoints";
10
11
  import { assessSearchSourceCapabilities, buildResearchSearchIntent, buildSearchCapabilitySnapshot, parsePublisherTarget, probePublisherAccess, publisherConfigs, runResearchSearch, searchCapabilitySnapshotPath, summarizeConfiguredPublisherAccess } from "./search/index.js";
11
12
  import { buildProviderChoices, buildQuickSetupFlow, createPersistedSetupOutput, installRuntimeConfigFromStoredSetup, loadSetupOutput, renderInstallSummary, renderSetupSummary, resolveDefaultRuntimeConfigPath, resolveDefaultSetupPath, saveSetupOutput, saveSetupAndRuntimeConfig, serializeSetupOutput, writeRuntimeConfig } from "@longtable/setup";
@@ -15,7 +16,8 @@ import { installCodexPromptAliases, listInstalledCodexPromptAliases, removeCodex
15
16
  import { buildPersonaGuidance, parseInvocationDirective } from "./persona-router.js";
16
17
  import { PERSONA_DEFINITIONS, listRoleDefinitions } from "./personas.js";
17
18
  import { buildPanelFallback, renderPanelSummary } from "./panel.js";
18
- import { appendInvocationRecordToWorkspace, assertWorkspaceNotBlocked, answerWorkspaceQuestion, createWorkspaceFollowUpQuestions, createWorkspaceQuestion, createOrUpdateProjectWorkspace, inspectProjectWorkspace, loadWorkspaceState, loadProjectContextFromDirectory, renderProjectWorkspaceSummary, syncCurrentWorkspaceView } from "./project-session.js";
19
+ import { LONGTABLE_MANAGED_HOOK_EVENTS, codexHooksEnabled, enableCodexHooksFeature, getMissingManagedCodexHookEvents, mergeManagedCodexHooksConfig, removeManagedCodexHooks } from "./codex-hooks.js";
20
+ import { appendInvocationRecordToWorkspace, assertWorkspaceNotBlocked, answerWorkspaceQuestion, clearWorkspaceQuestion, createWorkspaceFollowUpQuestions, createWorkspaceQuestion, createOrUpdateProjectWorkspace, inspectProjectWorkspace, loadWorkspaceState, loadProjectContextFromDirectory, repairWorkspaceStateConsistency, renderProjectWorkspaceSummary, syncCurrentWorkspaceView } from "./project-session.js";
19
21
  import { buildTeamDebate, buildTeamReview, renderTeamDebateSummary } from "./debate.js";
20
22
  import { createPromptRenderer } from "./prompt-renderer.js";
21
23
  const VALID_MODES = new Set([
@@ -43,7 +45,7 @@ const ANSI = {
43
45
  green: "\u001B[32m"
44
46
  };
45
47
  const LONGTABLE_MCP_SERVER_NAME = "longtable-state";
46
- const LONGTABLE_MCP_PACKAGE_VERSION = "0.1.31";
48
+ const LONGTABLE_MCP_PACKAGE_VERSION = "0.1.33";
47
49
  const LONGTABLE_MCP_MARKER_START = "# LongTable state MCP START";
48
50
  const LONGTABLE_MCP_MARKER_END = "# LongTable state MCP END";
49
51
  function style(text, prefix) {
@@ -68,6 +70,20 @@ function renderBrandBanner(title, subtitle) {
68
70
  }
69
71
  return lines.join("\n");
70
72
  }
73
+ function renderInterviewLaunchSteps(provider) {
74
+ const command = provider === "codex" ? "codex" : "claude";
75
+ return renderSectionCard("LongTable Interview", [
76
+ "Setup is permission and runtime calibration, not the research interview.",
77
+ "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.",
78
+ "",
79
+ "Next:",
80
+ "1. cd \"<research-folder>\"",
81
+ `2. run \`${command}\``,
82
+ "3. invoke `$longtable-interview`",
83
+ "",
84
+ "The interview will create or resume `.longtable/`, build a First Research Shape, and use option UI only for the final confirmation."
85
+ ]);
86
+ }
71
87
  function renderProgressBar(current, total) {
72
88
  const width = 10;
73
89
  const filled = Math.max(1, Math.round((current / total) * width));
@@ -77,14 +93,14 @@ function usage() {
77
93
  return [
78
94
  "Usage:",
79
95
  " Run `longtable ...` in your terminal, not inside the Codex chat box.",
80
- " After `longtable start`, move into the created project directory and open `codex` there.",
96
+ " LongTable research starts inside Codex or Claude with `$longtable-interview` after setup.",
81
97
  "",
82
98
  " 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
99
  " 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]",
100
+ " 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
101
  " longtable resume [--cwd <path>] [--json]",
86
- " 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
- " 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>]",
102
+ " longtable doctor [--cwd <path>] [--fix] [--json] [--codex-dir <path>] [--codex-config <path>] [--hooks-path <path>] [--claude-dir <path>] [--codex-prompts-dir <path>] [--codex-runtime-path <file>] [--claude-runtime-path <file>]",
103
+ " longtable status [--cwd <path>] [--fix] [--json] [--codex-dir <path>] [--codex-config <path>] [--hooks-path <path>] [--claude-dir <path>] [--codex-prompts-dir <path>] [--codex-runtime-path <file>] [--claude-runtime-path <file>]",
88
104
  " longtable roles [--json]",
89
105
  " longtable show [--json] [--path <file>]",
90
106
  " longtable install [--json] [--path <file>] [--runtime-path <file>]",
@@ -98,6 +114,7 @@ function usage() {
98
114
  " longtable ask [--prompt <text>] [--print] [--json] [--setup <path>] [--cwd <path>]",
99
115
  " longtable clarify --prompt <task-context> [--provider codex|claude] [--required|--advisory] [--print] [--cwd <path>] [--json] [--force]",
100
116
  " longtable question --prompt <decision-context> [--title <text>] [--text <question>] [--provider codex|claude] [--required|--advisory] [--print] [--cwd <path>] [--json]",
117
+ " longtable clear-question --question <id> --reason <text> [--cwd <path>] [--json]",
101
118
  " longtable panel [--prompt <text>] [--role <role[,role]>] [--mode review|critique|draft|commit] [--visibility synthesis_only|show_on_conflict|always_visible] [--print] [--json] [--setup <path>] [--cwd <path>]",
102
119
  " longtable decide [--question <id>] --answer <value-or-text> [--rationale <text>] [--provider codex|claude] [--cwd <path>] [--json]",
103
120
  " longtable explore|review|critique|draft|commit|submit [--prompt <text>] [--role <role[,role]>] [--panel] [--show-conflicts] [--show-deliberation] [--print] [--json] [--stage <stage>] [--setup <path>] [--cwd <path>]",
@@ -106,7 +123,9 @@ function usage() {
106
123
  " longtable codex remove-skills [--dir <path>]",
107
124
  " longtable codex install-prompts [--dir <path>]",
108
125
  " longtable codex remove-prompts [--dir <path>]",
109
- " longtable codex status [--dir <path>] [--json]",
126
+ " longtable codex install-hooks [--codex-config <path>] [--hooks-path <path>] [--json]",
127
+ " longtable codex remove-hooks [--codex-config <path>] [--hooks-path <path>] [--json]",
128
+ " longtable codex status [--dir <path>] [--codex-config <path>] [--hooks-path <path>] [--json]",
110
129
  " longtable claude install-skills [--dir <path>]",
111
130
  " longtable claude remove-skills [--dir <path>]",
112
131
  " longtable claude status [--dir <path>] [--json]",
@@ -114,9 +133,9 @@ function usage() {
114
133
  "",
115
134
  "Examples:",
116
135
  " longtable setup --provider codex",
117
- " longtable start",
118
- " longtable start --path ~/Research/My-Project --name \"AI Adoption Meta-Analysis\" --goal \"Narrow the review question\"",
119
- " cd \"<project-path>\" && codex",
136
+ " cd \"<research-folder>\" && codex",
137
+ " $longtable-interview",
138
+ " longtable start --no-interview --path ~/Research/My-Project --name \"AI Adoption Meta-Analysis\" --goal \"Narrow the review question\"",
120
139
  " longtable doctor",
121
140
  " longtable roles",
122
141
  " longtable ask --prompt \"연구를 시작하고 싶어. 지금 어디서부터 좁혀야 할지 모르겠어.\"",
@@ -130,7 +149,7 @@ function parseArgs(argv) {
130
149
  const values = {};
131
150
  let subcommand = maybeSubcommand;
132
151
  const modeCommand = command && VALID_MODES.has(command);
133
- const directCommand = command && ["init", "setup", "start", "resume", "doctor", "status", "roles", "show", "install", "mcp", "codex", "claude", "ask", "clarify", "question", "panel", "decide", "sentinel", "team", "search"].includes(command);
152
+ const directCommand = command && ["init", "setup", "start", "resume", "doctor", "status", "roles", "show", "install", "mcp", "codex", "claude", "ask", "clarify", "question", "clear-question", "panel", "decide", "sentinel", "team", "search"].includes(command);
134
153
  let startIndex = 1;
135
154
  if (modeCommand) {
136
155
  subcommand = undefined;
@@ -447,13 +466,13 @@ function buildPermissionSetupChoices() {
447
466
  workspace: [
448
467
  {
449
468
  id: "create",
450
- label: "Yes, create one now",
451
- description: "Why: durable state needs .longtable/. What you get: decision log and CURRENT.md. Tradeoff: asks project-specific questions now."
469
+ label: "Show interview launch steps",
470
+ 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."
452
471
  },
453
472
  {
454
473
  id: "later",
455
474
  label: "No, prepare runtime only",
456
- description: "Why: keeps setup short. What you get: runtime support without project state. Tradeoff: no durable research memory until `longtable start`."
475
+ 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."
457
476
  }
458
477
  ]
459
478
  };
@@ -575,6 +594,8 @@ async function runSetup(args) {
575
594
  interventionPosture: effectiveIntervention,
576
595
  checkpointUiMode: checkpointUi,
577
596
  workspaceCreationPreference: workspacePreference,
597
+ officialStartSurface: "$longtable-interview",
598
+ setupPosture: "permission_first",
578
599
  teamMode: "panel"
579
600
  };
580
601
  if (surfaces === "skills_mcp_sentinel") {
@@ -617,7 +638,12 @@ async function runSetup(args) {
617
638
  runtime: result,
618
639
  installedSkills: installedSkills.map((skill) => skill.name),
619
640
  mcpInstall,
620
- workspacePreference
641
+ workspacePreference,
642
+ nextStep: {
643
+ surface: "$longtable-interview",
644
+ command: provider === "codex" ? "codex" : "claude",
645
+ description: "Open the provider in the research folder and invoke `$longtable-interview`."
646
+ }
621
647
  }, null, 2));
622
648
  return;
623
649
  }
@@ -646,12 +672,11 @@ async function runSetup(args) {
646
672
  console.log("Background sentinel approval recorded.");
647
673
  console.log("Hook installation remains opt-in; LongTable will not install hooks without an explicit hook command.");
648
674
  }
675
+ console.log("");
676
+ console.log(renderInterviewLaunchSteps(provider));
649
677
  if (workspacePreference === "create") {
650
678
  console.log("");
651
- console.log("Project workspace requested. LongTable will now run `longtable start` with an adaptive start interview.");
652
- await runStart({
653
- setup: result.setupTarget.path
654
- });
679
+ console.log("Workspace launch requested. Open the provider in your research folder and run `$longtable-interview`; the interview will create `.longtable/` there.");
655
680
  }
656
681
  }
657
682
  function perspectiveChoices() {
@@ -849,6 +874,7 @@ function normalizePerspectiveList(value) {
849
874
  .filter(Boolean);
850
875
  }
851
876
  async function collectProjectInterview(setup, args) {
877
+ const skipResearchInterview = args["no-interview"] === true;
852
878
  const providedPerspectives = normalizePerspectiveList(typeof args.perspectives === "string" ? args.perspectives : undefined);
853
879
  const providedGoal = typeof args.goal === "string" && args.goal.trim() ? args.goal.trim() : undefined;
854
880
  const providedBlocker = typeof args.blocker === "string" && args.blocker.trim() ? args.blocker.trim() : undefined;
@@ -863,11 +889,11 @@ async function collectProjectInterview(setup, args) {
863
889
  : undefined;
864
890
  const needsInteractivePrompts = !(typeof args.name === "string" && args.name.trim()) ||
865
891
  !(typeof args.path === "string" && args.path.trim()) ||
866
- !providedGoal ||
867
- !providedBlocker ||
868
- !providedResearchObject ||
869
- !providedGapRisk ||
870
- !providedProtectedDecision;
892
+ (!skipResearchInterview && (!providedGoal ||
893
+ !providedBlocker ||
894
+ !providedResearchObject ||
895
+ !providedGapRisk ||
896
+ !providedProtectedDecision));
871
897
  if (needsInteractivePrompts) {
872
898
  console.log("");
873
899
  console.log(renderBrandBanner("LongTable", "Project workspace interview"));
@@ -887,15 +913,17 @@ async function collectProjectInterview(setup, args) {
887
913
  const projectPath = (typeof args.path === "string" && args.path.trim()
888
914
  ? normalizeUserPath(args.path.trim())
889
915
  : resolveInteractiveProjectPath((await promptText(renderQuestionHeader(2, 2, "Workspace", `Which parent directory should contain this project?\nLongTable will create this folder:\n${suggestedPath}`), true)), projectName));
890
- const adaptive = await collectAdaptiveStartInterview({
891
- currentGoal: providedGoal,
892
- currentBlocker: providedBlocker,
893
- needsResearchSeed: !providedGoal ||
894
- !providedBlocker ||
895
- !providedResearchObject ||
896
- !providedGapRisk ||
897
- !providedProtectedDecision
898
- });
916
+ const adaptive = skipResearchInterview
917
+ ? {}
918
+ : await collectAdaptiveStartInterview({
919
+ currentGoal: providedGoal,
920
+ currentBlocker: providedBlocker,
921
+ needsResearchSeed: !providedGoal ||
922
+ !providedBlocker ||
923
+ !providedResearchObject ||
924
+ !providedGapRisk ||
925
+ !providedProtectedDecision
926
+ });
899
927
  const currentGoal = providedGoal ?? adaptive.currentGoal;
900
928
  if (!currentGoal?.trim()) {
901
929
  throw new Error("LongTable start needs a current research goal or an opening interview answer.");
@@ -1090,6 +1118,16 @@ function resolveCodexMcpConfigPath(args) {
1090
1118
  ? args["codex-config"].trim()
1091
1119
  : "~/.codex/config.toml"));
1092
1120
  }
1121
+ function resolveCodexHooksPath(args) {
1122
+ if (typeof args["hooks-path"] === "string" && args["hooks-path"].trim()) {
1123
+ return resolve(normalizeUserPath(args["hooks-path"]));
1124
+ }
1125
+ const configPath = resolveCodexMcpConfigPath(args);
1126
+ return resolve(dirname(configPath), "hooks.json");
1127
+ }
1128
+ function resolveCliPackageRoot() {
1129
+ return resolve(fileURLToPath(new URL("..", import.meta.url)));
1130
+ }
1093
1131
  function resolveClaudeMcpSettingsPath(args) {
1094
1132
  return resolve(normalizeUserPath(typeof args["claude-settings"] === "string" && args["claude-settings"].trim()
1095
1133
  ? args["claude-settings"].trim()
@@ -1172,6 +1210,56 @@ async function writeClaudeMcpSettings(path, serverName, command, mcpArgs) {
1172
1210
  await writeFile(path, `${updated}\n`, "utf8");
1173
1211
  return `${updated}\n`;
1174
1212
  }
1213
+ async function installCodexNativeHooks(args) {
1214
+ const configPath = resolveCodexMcpConfigPath(args);
1215
+ const hooksPath = resolveCodexHooksPath(args);
1216
+ const packageRoot = resolveCliPackageRoot();
1217
+ const existingConfig = existsSync(configPath) ? await readFile(configPath, "utf8") : "";
1218
+ const existingHooks = existsSync(hooksPath) ? await readFile(hooksPath, "utf8") : "";
1219
+ const nextConfig = enableCodexHooksFeature(existingConfig);
1220
+ const nextHooks = mergeManagedCodexHooksConfig(existingHooks, packageRoot);
1221
+ await mkdir(dirname(configPath), { recursive: true });
1222
+ await mkdir(dirname(hooksPath), { recursive: true });
1223
+ await writeFile(configPath, nextConfig, "utf8");
1224
+ await writeFile(hooksPath, nextHooks, "utf8");
1225
+ return {
1226
+ configPath,
1227
+ hooksPath,
1228
+ codexHooksEnabled: codexHooksEnabled(nextConfig),
1229
+ managedEvents: [...LONGTABLE_MANAGED_HOOK_EVENTS],
1230
+ write: true
1231
+ };
1232
+ }
1233
+ async function removeCodexNativeHooks(args) {
1234
+ const configPath = resolveCodexMcpConfigPath(args);
1235
+ const hooksPath = resolveCodexHooksPath(args);
1236
+ const existingHooks = existsSync(hooksPath) ? await readFile(hooksPath, "utf8") : "";
1237
+ const removed = existingHooks ? removeManagedCodexHooks(existingHooks) : { nextContent: null, removedCount: 0 };
1238
+ if (removed.nextContent === null) {
1239
+ await rm(hooksPath, { force: true });
1240
+ }
1241
+ else {
1242
+ await mkdir(dirname(hooksPath), { recursive: true });
1243
+ await writeFile(hooksPath, removed.nextContent, "utf8");
1244
+ }
1245
+ const configContent = existsSync(configPath) ? await readFile(configPath, "utf8") : "";
1246
+ return {
1247
+ configPath,
1248
+ hooksPath,
1249
+ codexHooksEnabled: codexHooksEnabled(configContent),
1250
+ managedEvents: removed.removedCount > 0 ? [...LONGTABLE_MANAGED_HOOK_EVENTS] : [],
1251
+ write: true
1252
+ };
1253
+ }
1254
+ function renderCodexHookInstallSummary(result) {
1255
+ return [
1256
+ "LongTable Codex hooks",
1257
+ `- config: ${result.configPath}`,
1258
+ `- hooks: ${result.hooksPath}`,
1259
+ `- codex_hooks feature: ${result.codexHooksEnabled ? "enabled" : "missing"}`,
1260
+ `- managed events: ${result.managedEvents.length > 0 ? result.managedEvents.join(", ") : "none"}`
1261
+ ].join("\n");
1262
+ }
1175
1263
  function renderMcpInstallSummary(result) {
1176
1264
  const lines = [
1177
1265
  "LongTable MCP transport",
@@ -1358,6 +1446,13 @@ async function collectDoctorStatus(args) {
1358
1446
  const codexMcpConfig = existsSync(codexMcpConfigPath)
1359
1447
  ? await readFile(codexMcpConfigPath, "utf8")
1360
1448
  : "";
1449
+ const codexHooksPath = resolveCodexHooksPath(args);
1450
+ const codexHooksContent = existsSync(codexHooksPath)
1451
+ ? await readFile(codexHooksPath, "utf8")
1452
+ : "";
1453
+ const missingManagedHookEvents = codexHooksContent
1454
+ ? (getMissingManagedCodexHookEvents(codexHooksContent) ?? [...LONGTABLE_MANAGED_HOOK_EVENTS])
1455
+ : [...LONGTABLE_MANAGED_HOOK_EVENTS];
1361
1456
  const expectedCodexSkills = buildCodexSkillSpecs(roles).map((skill) => skill.name);
1362
1457
  const expectedClaudeSkills = buildClaudeSkillSpecs(roles).map((skill) => skill.name);
1363
1458
  const [codexSkills, claudeSkills, codexAliases, workspace] = await Promise.all([
@@ -1386,7 +1481,11 @@ async function collectDoctorStatus(args) {
1386
1481
  mcpConfigPath: codexMcpConfigPath,
1387
1482
  mcpConfigExists: existsSync(codexMcpConfigPath),
1388
1483
  longtableMcpConfigured: codexLongTableMcpConfigured(codexMcpConfig),
1389
- mcpElicitationsAllowed: codexMcpElicitationsAllowed(codexMcpConfig)
1484
+ mcpElicitationsAllowed: codexMcpElicitationsAllowed(codexMcpConfig),
1485
+ hooksPath: codexHooksPath,
1486
+ hooksExists: existsSync(codexHooksPath),
1487
+ codexHooksEnabled: codexHooksEnabled(codexMcpConfig),
1488
+ missingManagedHookEvents
1390
1489
  },
1391
1490
  claude: {
1392
1491
  command: "claude",
@@ -1428,6 +1527,9 @@ function renderDoctorStatus(status) {
1428
1527
  `- MCP config: ${status.providers.codex.mcpConfigExists ? "present" : "missing"} (${status.providers.codex.mcpConfigPath})`,
1429
1528
  `- LongTable MCP: ${status.providers.codex.longtableMcpConfigured ? "configured" : "missing"}`,
1430
1529
  `- MCP elicitation approval: ${status.providers.codex.mcpElicitationsAllowed ? "allowed" : "not allowed"}`,
1530
+ `- Codex hooks file: ${status.providers.codex.hooksExists ? "present" : "missing"} (${status.providers.codex.hooksPath})`,
1531
+ `- codex_hooks feature: ${status.providers.codex.codexHooksEnabled ? "enabled" : "missing"}`,
1532
+ `- managed hook coverage: ${status.providers.codex.missingManagedHookEvents.length === 0 ? "complete" : `missing ${status.providers.codex.missingManagedHookEvents.join(", ")}`}`,
1431
1533
  "",
1432
1534
  ...renderProviderDoctorBlock("Claude", status.providers.claude),
1433
1535
  "",
@@ -1438,7 +1540,7 @@ function renderDoctorStatus(status) {
1438
1540
  }
1439
1541
  else {
1440
1542
  const workspace = status.workspace;
1441
- lines.push(`- project: ${workspace.project?.name ?? "unknown"}`, `- root: ${workspace.rootPath ?? "unknown"}`, `- goal: ${workspace.session?.currentGoal ?? "unknown"}`, `- invocations: ${workspace.counts?.invocations ?? 0}`, `- questions: ${workspace.counts?.questions ?? 0} (${workspace.counts?.pendingQuestions ?? 0} pending, ${workspace.counts?.answeredQuestions ?? 0} answered)`, `- decisions: ${workspace.counts?.decisions ?? 0}`);
1543
+ lines.push(`- project: ${workspace.project?.name ?? "unknown"}`, `- root: ${workspace.rootPath ?? "unknown"}`, `- goal: ${workspace.session?.currentGoal ?? "unknown"}`, `- invocations: ${workspace.counts?.invocations ?? 0}`, `- questions: ${workspace.counts?.questions ?? 0} (${workspace.counts?.pendingQuestions ?? 0} pending, ${workspace.counts?.answeredQuestions ?? 0} answered)`, `- obligations: ${workspace.counts?.pendingObligations ?? 0} pending`, `- decisions: ${workspace.counts?.decisions ?? 0}`);
1442
1544
  if ((workspace.recentInvocations ?? []).length > 0) {
1443
1545
  lines.push("- recent invocations:");
1444
1546
  for (const invocation of workspace.recentInvocations ?? []) {
@@ -1452,6 +1554,12 @@ function renderDoctorStatus(status) {
1452
1554
  lines.push(` - ${question.id}: ${question.question} (${question.options.join("/")})`);
1453
1555
  }
1454
1556
  }
1557
+ if ((workspace.pendingObligations ?? []).length > 0) {
1558
+ lines.push("- pending obligations:");
1559
+ for (const obligation of workspace.pendingObligations ?? []) {
1560
+ lines.push(` - ${obligation.id}: ${obligation.prompt}`);
1561
+ }
1562
+ }
1455
1563
  if ((workspace.answerWarnings ?? []).length > 0) {
1456
1564
  lines.push("- answer warnings:");
1457
1565
  for (const warning of workspace.answerWarnings ?? []) {
@@ -1466,11 +1574,16 @@ function renderDoctorStatus(status) {
1466
1574
  const canFix = status.providers.codex.missingSkills.length > 0 ||
1467
1575
  status.providers.claude.missingSkills.length > 0 ||
1468
1576
  status.providers.codex.legacyPromptFilesInstalled.length > 0 ||
1577
+ !status.providers.codex.codexHooksEnabled ||
1578
+ status.providers.codex.missingManagedHookEvents.length > 0 ||
1469
1579
  (status.setupExists &&
1470
1580
  (!status.providers.codex.runtimeExists || !status.providers.claude.runtimeExists));
1471
1581
  if (canFix) {
1472
1582
  nextActions.push("longtable doctor --fix");
1473
1583
  }
1584
+ if (!status.providers.codex.codexHooksEnabled || status.providers.codex.missingManagedHookEvents.length > 0) {
1585
+ nextActions.push("longtable codex install-hooks");
1586
+ }
1474
1587
  if (!status.setupExists) {
1475
1588
  nextActions.push("longtable setup --provider codex");
1476
1589
  }
@@ -1500,6 +1613,15 @@ function renderRepairSummary(repair) {
1500
1613
  if (repair.removedLegacyPromptFiles.length > 0) {
1501
1614
  lines.push(`- removed legacy prompt files: ${repair.removedLegacyPromptFiles.length}`);
1502
1615
  }
1616
+ if (repair.installedCodexHooks) {
1617
+ lines.push("- installed Codex native hooks");
1618
+ }
1619
+ if ((repair.repairedWorkspaceState ?? []).length > 0) {
1620
+ lines.push("- repaired workspace state:");
1621
+ for (const item of repair.repairedWorkspaceState ?? []) {
1622
+ lines.push(` - ${item}`);
1623
+ }
1624
+ }
1503
1625
  if (repair.writtenRuntimeConfigs.length > 0) {
1504
1626
  lines.push("- wrote runtime configs:");
1505
1627
  for (const target of repair.writtenRuntimeConfigs) {
@@ -1551,6 +1673,8 @@ async function repairDoctorStatus(args, status) {
1551
1673
  installedCodexSkills: [],
1552
1674
  installedClaudeSkills: [],
1553
1675
  removedLegacyPromptFiles: [],
1676
+ installedCodexHooks: false,
1677
+ repairedWorkspaceState: [],
1554
1678
  writtenRuntimeConfigs: [],
1555
1679
  skipped: []
1556
1680
  };
@@ -1563,8 +1687,16 @@ async function repairDoctorStatus(args, status) {
1563
1687
  if (status.providers.codex.legacyPromptFilesInstalled.length > 0) {
1564
1688
  repair.removedLegacyPromptFiles = await removeCodexPromptAliases(codexPromptsDir);
1565
1689
  }
1690
+ if (!status.providers.codex.codexHooksEnabled || status.providers.codex.missingManagedHookEvents.length > 0) {
1691
+ await installCodexNativeHooks(args);
1692
+ repair.installedCodexHooks = true;
1693
+ }
1566
1694
  if (!status.setupExists) {
1567
1695
  repair.skipped.push("runtime configs require setup approval; run `longtable setup --provider codex` first");
1696
+ const workspaceContext = await loadProjectContextFromDirectory(typeof args.cwd === "string" ? args.cwd : cwd());
1697
+ if (workspaceContext) {
1698
+ repair.repairedWorkspaceState = (await repairWorkspaceStateConsistency({ context: workspaceContext })).repaired;
1699
+ }
1568
1700
  return repair;
1569
1701
  }
1570
1702
  const setup = await loadSetupOutput(setupOverride);
@@ -1584,6 +1716,10 @@ async function repairDoctorStatus(args, status) {
1584
1716
  format: target.format
1585
1717
  });
1586
1718
  }
1719
+ const workspaceContext = await loadProjectContextFromDirectory(typeof args.cwd === "string" ? args.cwd : cwd());
1720
+ if (workspaceContext) {
1721
+ repair.repairedWorkspaceState = (await repairWorkspaceStateConsistency({ context: workspaceContext })).repaired;
1722
+ }
1587
1723
  return repair;
1588
1724
  }
1589
1725
  async function runDoctor(args) {
@@ -2308,6 +2444,41 @@ async function runQuestion(args) {
2308
2444
  console.log(`- answer: longtable decide --question ${result.question.id} --answer <value>`);
2309
2445
  console.log(`- current: ${context.currentFilePath}`);
2310
2446
  }
2447
+ async function runClearQuestion(args) {
2448
+ const workingDirectory = typeof args.cwd === "string" ? args.cwd : cwd();
2449
+ const questionId = typeof args.question === "string" ? args.question.trim() : "";
2450
+ const reason = typeof args.reason === "string" ? args.reason.trim() : "";
2451
+ if (!questionId) {
2452
+ throw new Error("`clear-question` requires --question <id>.");
2453
+ }
2454
+ if (!reason) {
2455
+ throw new Error("`clear-question` requires --reason <text>.");
2456
+ }
2457
+ const context = await loadProjectContextFromDirectory(workingDirectory);
2458
+ if (!context) {
2459
+ throw new Error("No LongTable project workspace was found here. Run this inside a project or pass --cwd.");
2460
+ }
2461
+ const result = await clearWorkspaceQuestion({
2462
+ context,
2463
+ questionId,
2464
+ reason
2465
+ });
2466
+ if (args.json === true) {
2467
+ console.log(JSON.stringify({
2468
+ question: result.question,
2469
+ files: {
2470
+ state: context.stateFilePath,
2471
+ current: context.currentFilePath
2472
+ }
2473
+ }, null, 2));
2474
+ return;
2475
+ }
2476
+ console.log("LongTable question cleared");
2477
+ console.log(`- question: ${result.question.id}`);
2478
+ console.log(`- reason: ${result.question.clearedReason ?? reason}`);
2479
+ console.log(`- state: ${context.stateFilePath}`);
2480
+ console.log(`- current: ${context.currentFilePath}`);
2481
+ }
2311
2482
  function isInteractiveTerminal() {
2312
2483
  return Boolean(input.isTTY && output.isTTY);
2313
2484
  }
@@ -2773,6 +2944,26 @@ async function runRoles(args) {
2773
2944
  }
2774
2945
  }
2775
2946
  async function runStart(args) {
2947
+ const hasMinimalFallbackArgs = typeof args.name === "string" &&
2948
+ typeof args.path === "string" &&
2949
+ typeof args.goal === "string";
2950
+ const hasFallbackIntent = args["no-interview"] === true ||
2951
+ hasMinimalFallbackArgs;
2952
+ if (!hasFallbackIntent) {
2953
+ console.log(renderSectionCard("LongTable Start Has Moved", [
2954
+ "`longtable start` is now a fallback for automation and scripted workspace creation.",
2955
+ "The primary research-start experience is provider-native so LongTable can run a real interview instead of a terminal questionnaire.",
2956
+ "",
2957
+ "Use:",
2958
+ "1. longtable setup --provider codex",
2959
+ "2. cd \"<research-folder>\"",
2960
+ "3. codex",
2961
+ "4. $longtable-interview",
2962
+ "",
2963
+ "For automation, pass `--no-interview --json` with `--name`, `--path`, and `--goal`."
2964
+ ]));
2965
+ return;
2966
+ }
2776
2967
  const setupPath = typeof args.setup === "string" ? args.setup : undefined;
2777
2968
  const existingSetup = await loadOptionalSetup(setupPath);
2778
2969
  if (!existingSetup) {
@@ -2890,11 +3081,34 @@ async function runCodexSubcommand(subcommand, args) {
2890
3081
  console.log(`Removed ${removed.length} legacy LongTable prompt files from ${resolveCodexPromptsDir(customDir)}`);
2891
3082
  return;
2892
3083
  }
3084
+ if (subcommand === "install-hooks") {
3085
+ const result = await installCodexNativeHooks(args);
3086
+ if (args.json === true) {
3087
+ console.log(JSON.stringify(result, null, 2));
3088
+ return;
3089
+ }
3090
+ console.log(renderCodexHookInstallSummary(result));
3091
+ console.log("Restart Codex so the native hook config is reloaded.");
3092
+ return;
3093
+ }
3094
+ if (subcommand === "remove-hooks") {
3095
+ const result = await removeCodexNativeHooks(args);
3096
+ if (args.json === true) {
3097
+ console.log(JSON.stringify(result, null, 2));
3098
+ return;
3099
+ }
3100
+ console.log(renderCodexHookInstallSummary(result));
3101
+ return;
3102
+ }
2893
3103
  if (subcommand === "status") {
2894
3104
  const aliases = await listInstalledCodexPromptAliases(customDir);
2895
3105
  const skills = await listInstalledCodexSkills(roles, customDir);
2896
3106
  const setupPath = resolveDefaultSetupPath(typeof args.path === "string" ? args.path : undefined).path;
2897
3107
  const runtimePath = resolveDefaultRuntimeConfigPath("codex", typeof args["runtime-path"] === "string" ? args["runtime-path"] : undefined).path;
3108
+ const configPath = resolveCodexMcpConfigPath(args);
3109
+ const configContent = existsSync(configPath) ? await readFile(configPath, "utf8") : "";
3110
+ const hooksPath = resolveCodexHooksPath(args);
3111
+ const hooksContent = existsSync(hooksPath) ? await readFile(hooksPath, "utf8") : "";
2898
3112
  const status = {
2899
3113
  setupPath,
2900
3114
  setupExists: existsSync(setupPath),
@@ -2903,7 +3117,14 @@ async function runCodexSubcommand(subcommand, args) {
2903
3117
  skillsDir: resolveCodexSkillsDir(customDir),
2904
3118
  skillsInstalled: skills.map((skill) => skill.name),
2905
3119
  promptsDir: resolveCodexPromptsDir(customDir),
2906
- legacyPromptFilesInstalled: aliases.map((alias) => alias.name)
3120
+ legacyPromptFilesInstalled: aliases.map((alias) => alias.name),
3121
+ codexConfigPath: configPath,
3122
+ codexHooksEnabled: codexHooksEnabled(configContent),
3123
+ hooksPath,
3124
+ hooksExists: existsSync(hooksPath),
3125
+ missingManagedHookEvents: hooksContent
3126
+ ? (getMissingManagedCodexHookEvents(hooksContent) ?? [...LONGTABLE_MANAGED_HOOK_EVENTS])
3127
+ : [...LONGTABLE_MANAGED_HOOK_EVENTS]
2907
3128
  };
2908
3129
  if (args.json === true) {
2909
3130
  console.log(JSON.stringify(status, null, 2));
@@ -2933,6 +3154,10 @@ async function runCodexSubcommand(subcommand, args) {
2933
3154
  console.log(` - ${alias.name}`);
2934
3155
  }
2935
3156
  }
3157
+ console.log(`- codex config: ${status.codexConfigPath}`);
3158
+ console.log(`- codex_hooks feature: ${status.codexHooksEnabled ? "enabled" : "missing"}`);
3159
+ console.log(`- hooks file: ${status.hooksExists ? "present" : "missing"} (${status.hooksPath})`);
3160
+ console.log(`- managed hook coverage: ${status.missingManagedHookEvents.length === 0 ? "complete" : `missing ${status.missingManagedHookEvents.join(", ")}`}`);
2936
3161
  return;
2937
3162
  }
2938
3163
  throw new Error("Unknown codex subcommand.");
@@ -3046,6 +3271,10 @@ async function main() {
3046
3271
  await runQuestion(values);
3047
3272
  return;
3048
3273
  }
3274
+ if (command === "clear-question") {
3275
+ await runClearQuestion(values);
3276
+ return;
3277
+ }
3049
3278
  if (command === "panel") {
3050
3279
  await runPanelCommand(values);
3051
3280
  return;
@@ -0,0 +1,22 @@
1
+ export declare const LONGTABLE_MANAGED_HOOK_EVENTS: readonly ["SessionStart", "PreToolUse", "PostToolUse", "UserPromptSubmit", "Stop"];
2
+ type ManagedHookEventName = (typeof LONGTABLE_MANAGED_HOOK_EVENTS)[number];
3
+ type JsonObject = Record<string, unknown>;
4
+ export interface ManagedCodexHooksConfig {
5
+ hooks: Record<ManagedHookEventName, Array<Record<string, unknown>>>;
6
+ }
7
+ interface ParsedCodexHooksConfig {
8
+ root: JsonObject;
9
+ hooks: JsonObject;
10
+ }
11
+ export interface RemoveManagedCodexHooksResult {
12
+ nextContent: string | null;
13
+ removedCount: number;
14
+ }
15
+ export declare function buildManagedCodexHooksConfig(packageRoot: string): ManagedCodexHooksConfig;
16
+ export declare function parseCodexHooksConfig(content: string): ParsedCodexHooksConfig | null;
17
+ export declare function getMissingManagedCodexHookEvents(content: string): ManagedHookEventName[] | null;
18
+ export declare function mergeManagedCodexHooksConfig(existingContent: string | null | undefined, packageRoot: string): string;
19
+ export declare function removeManagedCodexHooks(existingContent: string): RemoveManagedCodexHooksResult;
20
+ export declare function enableCodexHooksFeature(existing: string): string;
21
+ export declare function codexHooksEnabled(config: string): boolean;
22
+ export {};