@longtable/cli 0.1.48 → 0.1.49

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
@@ -17,7 +17,7 @@ import { installCodexPromptAliases, listInstalledCodexPromptAliases, removeCodex
17
17
  import { buildPersonaGuidance, parseInvocationDirective } from "./persona-router.js";
18
18
  import { PERSONA_DEFINITIONS, listRoleDefinitions } from "./personas.js";
19
19
  import { buildPanelFallback, renderPanelSummary } from "./panel.js";
20
- import { LONGTABLE_MANAGED_HOOK_EVENTS, codexHooksEnabled, enableCodexHooksFeature, getMissingManagedCodexHookEvents, mergeManagedCodexHooksConfig, removeManagedCodexHooks } from "./codex-hooks.js";
20
+ import { LONGTABLE_MANAGED_HOOK_EVENTS, codexHooksEnabled, enableCodexHooksFeature, getMissingManagedCodexHookEvents, getMissingManagedCodexHookTrustState, mergeCodexHookTrustState, mergeManagedCodexHooksConfig, removeCodexHookTrustState, removeManagedCodexHooks } from "./codex-hooks.js";
21
21
  import { appendInvocationRecordToWorkspace, applyResearchSpecificationPatch, assertWorkspaceNotBlocked, answerWorkspaceQuestion, buildQuestionOpportunitySpecs, clearWorkspaceQuestion, createWorkspaceFollowUpQuestions, createWorkspaceQuestion, createOrUpdateProjectWorkspace, diffResearchSpecifications, inspectProjectWorkspace, loadWorkspaceState, loadProjectContextFromDirectory, findUnincorporatedResearchEvidence, proposeResearchSpecificationPatch, pruneWorkspaceQuestions, readResearchSpecificationHistory, repairWorkspaceStateConsistency, renderProjectWorkspaceSummary, syncCurrentWorkspaceView } from "./project-session.js";
22
22
  import { buildTeamDebate, buildTeamReview, renderTeamDebateSummary } from "./debate.js";
23
23
  import { createPromptRenderer } from "./prompt-renderer.js";
@@ -1325,8 +1325,9 @@ async function installCodexNativeHooks(args) {
1325
1325
  const packageRoot = resolveCliPackageRoot();
1326
1326
  const existingConfig = existsSync(configPath) ? await readFile(configPath, "utf8") : "";
1327
1327
  const existingHooks = existsSync(hooksPath) ? await readFile(hooksPath, "utf8") : "";
1328
- const nextConfig = enableCodexHooksFeature(existingConfig);
1329
1328
  const nextHooks = mergeManagedCodexHooksConfig(existingHooks, packageRoot);
1329
+ const configWithHooksFeature = enableCodexHooksFeature(existingConfig, "hooks");
1330
+ const nextConfig = mergeCodexHookTrustState(configWithHooksFeature, hooksPath, nextHooks);
1330
1331
  await mkdir(dirname(configPath), { recursive: true });
1331
1332
  await mkdir(dirname(hooksPath), { recursive: true });
1332
1333
  await writeFile(configPath, nextConfig, "utf8");
@@ -1336,14 +1337,21 @@ async function installCodexNativeHooks(args) {
1336
1337
  hooksPath,
1337
1338
  codexHooksEnabled: codexHooksEnabled(nextConfig),
1338
1339
  managedEvents: [...LONGTABLE_MANAGED_HOOK_EVENTS],
1340
+ managedTrustEntries: getMissingManagedCodexHookTrustState("", hooksPath, nextHooks).length,
1339
1341
  write: true
1340
1342
  };
1341
1343
  }
1342
1344
  async function removeCodexNativeHooks(args) {
1343
1345
  const configPath = resolveCodexMcpConfigPath(args);
1344
1346
  const hooksPath = resolveCodexHooksPath(args);
1347
+ const configContent = existsSync(configPath) ? await readFile(configPath, "utf8") : "";
1345
1348
  const existingHooks = existsSync(hooksPath) ? await readFile(hooksPath, "utf8") : "";
1349
+ const nextConfig = existingHooks
1350
+ ? removeCodexHookTrustState(configContent, hooksPath, existingHooks)
1351
+ : configContent;
1346
1352
  const removed = existingHooks ? removeManagedCodexHooks(existingHooks) : { nextContent: null, removedCount: 0 };
1353
+ await mkdir(dirname(configPath), { recursive: true });
1354
+ await writeFile(configPath, nextConfig, "utf8");
1347
1355
  if (removed.nextContent === null) {
1348
1356
  await rm(hooksPath, { force: true });
1349
1357
  }
@@ -1351,12 +1359,12 @@ async function removeCodexNativeHooks(args) {
1351
1359
  await mkdir(dirname(hooksPath), { recursive: true });
1352
1360
  await writeFile(hooksPath, removed.nextContent, "utf8");
1353
1361
  }
1354
- const configContent = existsSync(configPath) ? await readFile(configPath, "utf8") : "";
1355
1362
  return {
1356
1363
  configPath,
1357
1364
  hooksPath,
1358
- codexHooksEnabled: codexHooksEnabled(configContent),
1365
+ codexHooksEnabled: codexHooksEnabled(nextConfig),
1359
1366
  managedEvents: removed.removedCount > 0 ? [...LONGTABLE_MANAGED_HOOK_EVENTS] : [],
1367
+ managedTrustEntries: 0,
1360
1368
  write: true
1361
1369
  };
1362
1370
  }
@@ -1365,8 +1373,9 @@ function renderCodexHookInstallSummary(result) {
1365
1373
  "LongTable Codex hooks",
1366
1374
  `- config: ${result.configPath}`,
1367
1375
  `- hooks: ${result.hooksPath}`,
1368
- `- codex_hooks feature: ${result.codexHooksEnabled ? "enabled" : "missing"}`,
1369
- `- managed events: ${result.managedEvents.length > 0 ? result.managedEvents.join(", ") : "none"}`
1376
+ `- hooks feature: ${result.codexHooksEnabled ? "enabled" : "missing"}`,
1377
+ `- managed events: ${result.managedEvents.length > 0 ? result.managedEvents.join(", ") : "none"}`,
1378
+ `- managed trust entries: ${result.managedTrustEntries}`
1370
1379
  ].join("\n");
1371
1380
  }
1372
1381
  function renderMcpInstallSummary(result) {
@@ -1565,6 +1574,9 @@ async function collectDoctorStatus(args) {
1565
1574
  const missingManagedHookEvents = codexHooksContent
1566
1575
  ? (getMissingManagedCodexHookEvents(codexHooksContent) ?? [...LONGTABLE_MANAGED_HOOK_EVENTS])
1567
1576
  : [...LONGTABLE_MANAGED_HOOK_EVENTS];
1577
+ const missingManagedHookTrustState = codexHooksContent
1578
+ ? getMissingManagedCodexHookTrustState(codexMcpConfig, codexHooksPath, codexHooksContent)
1579
+ : [];
1568
1580
  const expectedCodexSkills = buildCodexSkillSpecs(roles, skillSurface).map((skill) => skill.name);
1569
1581
  const expectedClaudeSkills = buildClaudeSkillSpecs(roles, skillSurface).map((skill) => skill.name);
1570
1582
  const [codexSkills, claudeSkills, codexAliases, workspace] = await Promise.all([
@@ -1601,7 +1613,8 @@ async function collectDoctorStatus(args) {
1601
1613
  hooksPath: codexHooksPath,
1602
1614
  hooksExists: existsSync(codexHooksPath),
1603
1615
  codexHooksEnabled: codexHooksEnabled(codexMcpConfig),
1604
- missingManagedHookEvents
1616
+ missingManagedHookEvents,
1617
+ missingManagedHookTrustState
1605
1618
  },
1606
1619
  claude: {
1607
1620
  command: "claude",
@@ -1649,8 +1662,9 @@ function renderDoctorStatus(status) {
1649
1662
  : ["- Research Specification MCP tools: complete"]),
1650
1663
  `- MCP elicitation approval: ${status.providers.codex.mcpElicitationsAllowed ? "allowed" : "not allowed"}`,
1651
1664
  `- Codex hooks file: ${status.providers.codex.hooksExists ? "present" : "missing"} (${status.providers.codex.hooksPath})`,
1652
- `- codex_hooks feature: ${status.providers.codex.codexHooksEnabled ? "enabled" : "missing"}`,
1665
+ `- hooks feature: ${status.providers.codex.codexHooksEnabled ? "enabled" : "missing"}`,
1653
1666
  `- managed hook coverage: ${status.providers.codex.missingManagedHookEvents.length === 0 ? "complete" : `missing ${status.providers.codex.missingManagedHookEvents.join(", ")}`}`,
1667
+ `- managed hook trust: ${status.providers.codex.missingManagedHookTrustState.length === 0 ? "current" : `missing/stale ${status.providers.codex.missingManagedHookTrustState.length}`}`,
1654
1668
  "",
1655
1669
  ...renderProviderDoctorBlock("Claude", status.providers.claude),
1656
1670
  "",
@@ -1700,12 +1714,15 @@ function renderDoctorStatus(status) {
1700
1714
  status.providers.codex.missingMcpTools.length > 0 ||
1701
1715
  !status.providers.codex.codexHooksEnabled ||
1702
1716
  status.providers.codex.missingManagedHookEvents.length > 0 ||
1717
+ status.providers.codex.missingManagedHookTrustState.length > 0 ||
1703
1718
  (status.setupExists &&
1704
1719
  (!status.providers.codex.runtimeExists || !status.providers.claude.runtimeExists));
1705
1720
  if (canFix) {
1706
1721
  nextActions.push("longtable doctor --fix");
1707
1722
  }
1708
- if (!status.providers.codex.codexHooksEnabled || status.providers.codex.missingManagedHookEvents.length > 0) {
1723
+ if (!status.providers.codex.codexHooksEnabled ||
1724
+ status.providers.codex.missingManagedHookEvents.length > 0 ||
1725
+ status.providers.codex.missingManagedHookTrustState.length > 0) {
1709
1726
  nextActions.push("longtable codex install-hooks");
1710
1727
  }
1711
1728
  if (!status.providers.codex.longtableMcpConfigured ||
@@ -1832,7 +1849,9 @@ async function repairDoctorStatus(args, status) {
1832
1849
  if (status.providers.codex.legacyPromptFilesInstalled.length > 0) {
1833
1850
  repair.removedLegacyPromptFiles = await removeCodexPromptAliases(codexPromptsDir);
1834
1851
  }
1835
- if (!status.providers.codex.codexHooksEnabled || status.providers.codex.missingManagedHookEvents.length > 0) {
1852
+ if (!status.providers.codex.codexHooksEnabled ||
1853
+ status.providers.codex.missingManagedHookEvents.length > 0 ||
1854
+ status.providers.codex.missingManagedHookTrustState.length > 0) {
1836
1855
  await installCodexNativeHooks(args);
1837
1856
  repair.installedCodexHooks = true;
1838
1857
  }
@@ -3773,7 +3792,10 @@ async function runCodexSubcommand(subcommand, args) {
3773
3792
  hooksExists: existsSync(hooksPath),
3774
3793
  missingManagedHookEvents: hooksContent
3775
3794
  ? (getMissingManagedCodexHookEvents(hooksContent) ?? [...LONGTABLE_MANAGED_HOOK_EVENTS])
3776
- : [...LONGTABLE_MANAGED_HOOK_EVENTS]
3795
+ : [...LONGTABLE_MANAGED_HOOK_EVENTS],
3796
+ missingManagedHookTrustState: hooksContent
3797
+ ? getMissingManagedCodexHookTrustState(configContent, hooksPath, hooksContent)
3798
+ : []
3777
3799
  };
3778
3800
  if (args.json === true) {
3779
3801
  console.log(JSON.stringify(status, null, 2));
@@ -3805,9 +3827,10 @@ async function runCodexSubcommand(subcommand, args) {
3805
3827
  }
3806
3828
  }
3807
3829
  console.log(`- codex config: ${status.codexConfigPath}`);
3808
- console.log(`- codex_hooks feature: ${status.codexHooksEnabled ? "enabled" : "missing"}`);
3830
+ console.log(`- hooks feature: ${status.codexHooksEnabled ? "enabled" : "missing"}`);
3809
3831
  console.log(`- hooks file: ${status.hooksExists ? "present" : "missing"} (${status.hooksPath})`);
3810
3832
  console.log(`- managed hook coverage: ${status.missingManagedHookEvents.length === 0 ? "complete" : `missing ${status.missingManagedHookEvents.join(", ")}`}`);
3833
+ console.log(`- managed hook trust: ${status.missingManagedHookTrustState.length === 0 ? "current" : `missing/stale ${status.missingManagedHookTrustState.length}`}`);
3811
3834
  return;
3812
3835
  }
3813
3836
  throw new Error("Unknown codex subcommand.");
@@ -1,4 +1,4 @@
1
- export declare const LONGTABLE_MANAGED_HOOK_EVENTS: readonly ["SessionStart", "PreToolUse", "PostToolUse", "UserPromptSubmit", "Stop"];
1
+ export declare const LONGTABLE_MANAGED_HOOK_EVENTS: readonly ["SessionStart", "PreToolUse", "PostToolUse", "UserPromptSubmit", "PreCompact", "PostCompact", "Stop"];
2
2
  type ManagedHookEventName = (typeof LONGTABLE_MANAGED_HOOK_EVENTS)[number];
3
3
  type JsonObject = Record<string, unknown>;
4
4
  export interface ManagedCodexHooksConfig {
@@ -12,11 +12,18 @@ export interface RemoveManagedCodexHooksResult {
12
12
  nextContent: string | null;
13
13
  removedCount: number;
14
14
  }
15
+ export interface CodexHookTrustStateEntry {
16
+ trusted_hash: string;
17
+ }
15
18
  export declare function buildManagedCodexHooksConfig(packageRoot: string): ManagedCodexHooksConfig;
16
19
  export declare function parseCodexHooksConfig(content: string): ParsedCodexHooksConfig | null;
17
20
  export declare function getMissingManagedCodexHookEvents(content: string): ManagedHookEventName[] | null;
21
+ export declare function buildManagedCodexHookTrustState(hooksPath: string, hooksContent: string): Record<string, CodexHookTrustStateEntry>;
22
+ export declare function mergeCodexHookTrustState(config: string, hooksPath: string, hooksContent: string): string;
23
+ export declare function removeCodexHookTrustState(config: string, hooksPath: string, hooksContent: string): string;
24
+ export declare function getMissingManagedCodexHookTrustState(config: string, hooksPath: string, hooksContent: string): string[];
18
25
  export declare function mergeManagedCodexHooksConfig(existingContent: string | null | undefined, packageRoot: string): string;
19
26
  export declare function removeManagedCodexHooks(existingContent: string): RemoveManagedCodexHooksResult;
20
- export declare function enableCodexHooksFeature(existing: string): string;
27
+ export declare function enableCodexHooksFeature(existing: string, featureFlag?: string): string;
21
28
  export declare function codexHooksEnabled(config: string): boolean;
22
29
  export {};
@@ -1,11 +1,23 @@
1
+ import { createHash } from "node:crypto";
1
2
  import { join } from "node:path";
2
3
  export const LONGTABLE_MANAGED_HOOK_EVENTS = [
3
4
  "SessionStart",
4
5
  "PreToolUse",
5
6
  "PostToolUse",
6
7
  "UserPromptSubmit",
8
+ "PreCompact",
9
+ "PostCompact",
7
10
  "Stop"
8
11
  ];
12
+ const CODEX_HOOK_EVENT_LABELS = {
13
+ SessionStart: "session_start",
14
+ PreToolUse: "pre_tool_use",
15
+ PostToolUse: "post_tool_use",
16
+ UserPromptSubmit: "user_prompt_submit",
17
+ PreCompact: "pre_compact",
18
+ PostCompact: "post_compact",
19
+ Stop: "stop"
20
+ };
9
21
  function isPlainObject(value) {
10
22
  return typeof value === "object" && value !== null && !Array.isArray(value);
11
23
  }
@@ -51,6 +63,12 @@ export function buildManagedCodexHooksConfig(packageRoot) {
51
63
  statusMessage: "Applying LongTable research context"
52
64
  })
53
65
  ],
66
+ PreCompact: [
67
+ buildCommandHook(command)
68
+ ],
69
+ PostCompact: [
70
+ buildCommandHook(command)
71
+ ],
54
72
  Stop: [
55
73
  buildCommandHook(command, {
56
74
  timeout: 30
@@ -126,6 +144,152 @@ function stripManagedHooksFromEntry(entry) {
126
144
  function serializeCodexHooksConfig(root) {
127
145
  return JSON.stringify(root, null, 2) + "\n";
128
146
  }
147
+ function canonicalJson(value) {
148
+ if (Array.isArray(value)) {
149
+ return value.map((item) => canonicalJson(item));
150
+ }
151
+ if (isPlainObject(value)) {
152
+ return Object.fromEntries(Object.keys(value)
153
+ .sort()
154
+ .map((key) => [key, canonicalJson(value[key])]));
155
+ }
156
+ return value;
157
+ }
158
+ function versionForCodexTomlIdentity(value) {
159
+ const serialized = JSON.stringify(canonicalJson(value));
160
+ return `sha256:${createHash("sha256").update(serialized).digest("hex")}`;
161
+ }
162
+ function normalizedCommandHookIdentity(eventName, entry, hook) {
163
+ return {
164
+ event_name: CODEX_HOOK_EVENT_LABELS[eventName],
165
+ ...(typeof entry.matcher === "string" ? { matcher: entry.matcher } : {}),
166
+ hooks: [
167
+ {
168
+ type: "command",
169
+ command: hook.command,
170
+ timeout: Math.max(1, typeof hook.timeout === "number" ? hook.timeout : 600),
171
+ async: false,
172
+ ...(typeof hook.statusMessage === "string" ? { statusMessage: hook.statusMessage } : {})
173
+ }
174
+ ]
175
+ };
176
+ }
177
+ function managedHookStateKey(hooksPath, eventName, groupIndex, handlerIndex) {
178
+ return `${hooksPath}:${CODEX_HOOK_EVENT_LABELS[eventName]}:${groupIndex}:${handlerIndex}`;
179
+ }
180
+ export function buildManagedCodexHookTrustState(hooksPath, hooksContent) {
181
+ const parsed = parseCodexHooksConfig(hooksContent);
182
+ if (!parsed) {
183
+ return {};
184
+ }
185
+ const state = {};
186
+ for (const eventName of LONGTABLE_MANAGED_HOOK_EVENTS) {
187
+ const entries = Array.isArray(parsed.hooks[eventName]) ? parsed.hooks[eventName] : [];
188
+ entries.forEach((entry, groupIndex) => {
189
+ if (!isPlainObject(entry) || !Array.isArray(entry.hooks)) {
190
+ return;
191
+ }
192
+ entry.hooks.forEach((hook, handlerIndex) => {
193
+ if (!isPlainObject(hook) ||
194
+ hook.type !== "command" ||
195
+ typeof hook.command !== "string" ||
196
+ !isLongTableManagedHookCommand(hook.command)) {
197
+ return;
198
+ }
199
+ state[managedHookStateKey(hooksPath, eventName, groupIndex, handlerIndex)] = {
200
+ trusted_hash: versionForCodexTomlIdentity(normalizedCommandHookIdentity(eventName, entry, hook))
201
+ };
202
+ });
203
+ });
204
+ }
205
+ return state;
206
+ }
207
+ function escapeTomlBasicString(value) {
208
+ return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
209
+ }
210
+ function unescapeTomlBasicString(value) {
211
+ return value.replace(/\\(["\\])/g, "$1");
212
+ }
213
+ function readHooksStateTableKey(line) {
214
+ const match = line.trim().match(/^\[hooks\.state\."((?:\\.|[^"\\])*)"\]$/);
215
+ return match ? unescapeTomlBasicString(match[1]) : null;
216
+ }
217
+ function removeHookStateTables(config, shouldRemove) {
218
+ const lines = config.split(/\r?\n/);
219
+ const kept = [];
220
+ for (let index = 0; index < lines.length;) {
221
+ const key = readHooksStateTableKey(lines[index]);
222
+ if (!key || !shouldRemove(key)) {
223
+ kept.push(lines[index]);
224
+ index += 1;
225
+ continue;
226
+ }
227
+ index += 1;
228
+ while (index < lines.length && !/^\s*\[/.test(lines[index])) {
229
+ index += 1;
230
+ }
231
+ }
232
+ return `${kept.join("\n").trimEnd()}\n`;
233
+ }
234
+ function readHookStateTrustedHashes(config) {
235
+ const lines = config.split(/\r?\n/);
236
+ const hashes = new Map();
237
+ for (let index = 0; index < lines.length;) {
238
+ const key = readHooksStateTableKey(lines[index]);
239
+ if (!key) {
240
+ index += 1;
241
+ continue;
242
+ }
243
+ index += 1;
244
+ while (index < lines.length && !/^\s*\[/.test(lines[index])) {
245
+ const match = lines[index].trim().match(/^trusted_hash\s*=\s*"([^"]+)"$/);
246
+ if (match) {
247
+ hashes.set(key, match[1]);
248
+ }
249
+ index += 1;
250
+ }
251
+ }
252
+ return hashes;
253
+ }
254
+ function renderCodexHookTrustToml(state) {
255
+ return Object.entries(state)
256
+ .sort(([left], [right]) => left.localeCompare(right))
257
+ .flatMap(([key, entry]) => [
258
+ `[hooks.state."${escapeTomlBasicString(key)}"]`,
259
+ `trusted_hash = "${escapeTomlBasicString(entry.trusted_hash)}"`,
260
+ ""
261
+ ])
262
+ .join("\n")
263
+ .trimEnd();
264
+ }
265
+ export function mergeCodexHookTrustState(config, hooksPath, hooksContent) {
266
+ const state = buildManagedCodexHookTrustState(hooksPath, hooksContent);
267
+ const keys = new Set(Object.keys(state));
268
+ if (keys.size === 0) {
269
+ return config.trimEnd() ? `${config.trimEnd()}\n` : "";
270
+ }
271
+ const withoutCurrent = removeHookStateTables(config, (key) => keys.has(key)).trimEnd();
272
+ const trustToml = renderCodexHookTrustToml(state);
273
+ return withoutCurrent ? `${withoutCurrent}\n\n${trustToml}\n` : `${trustToml}\n`;
274
+ }
275
+ export function removeCodexHookTrustState(config, hooksPath, hooksContent) {
276
+ const keys = new Set(Object.keys(buildManagedCodexHookTrustState(hooksPath, hooksContent)));
277
+ if (keys.size === 0) {
278
+ return config.trimEnd() ? `${config.trimEnd()}\n` : "";
279
+ }
280
+ return removeHookStateTables(config, (key) => keys.has(key));
281
+ }
282
+ export function getMissingManagedCodexHookTrustState(config, hooksPath, hooksContent) {
283
+ const expected = buildManagedCodexHookTrustState(hooksPath, hooksContent);
284
+ const hashes = readHookStateTrustedHashes(config);
285
+ const missing = [];
286
+ for (const [key, entry] of Object.entries(expected)) {
287
+ if (hashes.get(key) !== entry.trusted_hash) {
288
+ missing.push(key);
289
+ }
290
+ }
291
+ return missing;
292
+ }
129
293
  export function mergeManagedCodexHooksConfig(existingContent, packageRoot) {
130
294
  const managedConfig = buildManagedCodexHooksConfig(packageRoot);
131
295
  const parsed = typeof existingContent === "string"
@@ -204,22 +368,26 @@ function findSectionBounds(lines, sectionHeader) {
204
368
  }
205
369
  return { start, end };
206
370
  }
207
- export function enableCodexHooksFeature(existing) {
371
+ function normalizeCodexHookFeatureFlag(value) {
372
+ return value === "codex_hooks" ? "codex_hooks" : "hooks";
373
+ }
374
+ export function enableCodexHooksFeature(existing, featureFlag) {
375
+ const flagName = normalizeCodexHookFeatureFlag(featureFlag);
208
376
  const trimmed = existing.trimEnd();
209
377
  const lines = trimmed ? trimmed.split(/\r?\n/) : [];
210
378
  const section = findSectionBounds(lines, "[features]");
211
379
  if (!section) {
212
380
  return trimmed
213
- ? `${trimmed}\n\n[features]\ncodex_hooks = true\n`
214
- : "[features]\ncodex_hooks = true\n";
381
+ ? `${trimmed}\n\n[features]\n${flagName} = true\n`
382
+ : `[features]\n${flagName} = true\n`;
215
383
  }
216
384
  const featureLines = lines.slice(section.start + 1, section.end);
217
- const existingIndex = featureLines.findIndex((line) => /^\s*codex_hooks\s*=/.test(line));
385
+ const existingIndex = featureLines.findIndex((line) => new RegExp(`^\\s*${flagName}\\s*=`).test(line));
218
386
  if (existingIndex !== -1) {
219
- featureLines[existingIndex] = "codex_hooks = true";
387
+ featureLines[existingIndex] = `${flagName} = true`;
220
388
  }
221
389
  else {
222
- featureLines.push("codex_hooks = true");
390
+ featureLines.push(`${flagName} = true`);
223
391
  }
224
392
  const rebuilt = [
225
393
  ...lines.slice(0, section.start + 1),
@@ -236,5 +404,5 @@ export function codexHooksEnabled(config) {
236
404
  }
237
405
  return lines
238
406
  .slice(section.start + 1, section.end)
239
- .some((line) => /^\s*codex_hooks\s*=\s*true\s*$/.test(line));
407
+ .some((line) => /^\s*(?:hooks|codex_hooks)\s*=\s*true\s*$/.test(line));
240
408
  }
@@ -21,6 +21,8 @@ function readHookEventName(payload) {
21
21
  candidate === "PreToolUse" ||
22
22
  candidate === "PostToolUse" ||
23
23
  candidate === "UserPromptSubmit" ||
24
+ candidate === "PreCompact" ||
25
+ candidate === "PostCompact" ||
24
26
  candidate === "Stop") {
25
27
  return candidate;
26
28
  }
@@ -329,6 +331,31 @@ function sessionStartContext(runtime) {
329
331
  sections.push("Treat `.longtable/` state and `CURRENT.md` as the source of truth for this workspace.");
330
332
  return sections.filter(Boolean).join("\n\n");
331
333
  }
334
+ function postCompactContext(runtime) {
335
+ const blockingQuestion = pendingRequiredQuestions(runtime.state)[0];
336
+ const blockingObligation = pendingObligations(runtime.state)[0];
337
+ const interview = activeInterviewHook(runtime.state);
338
+ if (!blockingQuestion && !blockingObligation && !interview) {
339
+ return null;
340
+ }
341
+ const sections = [buildWorkspaceSummary(runtime, "compact").join("\n")];
342
+ if (interview) {
343
+ sections.push(buildActiveInterviewContext(interview));
344
+ if (blockingQuestion) {
345
+ sections.push(buildSeparatePendingQuestionNotice(blockingQuestion));
346
+ }
347
+ else if (blockingObligation) {
348
+ sections.push(buildSeparatePendingObligationNotice(blockingObligation));
349
+ }
350
+ }
351
+ else if (blockingQuestion) {
352
+ sections.push(buildPendingQuestionContext(blockingQuestion));
353
+ }
354
+ else if (blockingObligation) {
355
+ sections.push(buildPendingObligationContext(blockingObligation));
356
+ }
357
+ return sections.filter(Boolean).join("\n\n");
358
+ }
332
359
  async function userPromptSubmitContext(runtime, prompt) {
333
360
  const blockingQuestion = pendingRequiredQuestions(runtime.state)[0];
334
361
  const blockingObligation = pendingObligations(runtime.state)[0];
@@ -451,6 +478,15 @@ export async function dispatchCodexHook(payload, cwdOverride) {
451
478
  if (hookEventName === "PostToolUse") {
452
479
  return postToolUseOutput(runtime, payload);
453
480
  }
481
+ if (hookEventName === "PreCompact") {
482
+ return null;
483
+ }
484
+ if (hookEventName === "PostCompact") {
485
+ const additionalContext = postCompactContext(runtime);
486
+ return additionalContext
487
+ ? buildAdditionalContextOutput(hookEventName, additionalContext)
488
+ : null;
489
+ }
454
490
  if (hookEventName === "Stop") {
455
491
  return stopOutput(runtime);
456
492
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@longtable/cli",
3
- "version": "0.1.48",
3
+ "version": "0.1.49",
4
4
  "private": false,
5
5
  "description": "Researcher-facing LongTable CLI",
6
6
  "type": "module",
@@ -29,12 +29,12 @@
29
29
  },
30
30
  "dependencies": {
31
31
  "@clack/prompts": "^1.2.0",
32
- "@longtable/checkpoints": "0.1.48",
33
- "@longtable/core": "0.1.48",
34
- "@longtable/memory": "0.1.48",
35
- "@longtable/provider-claude": "0.1.48",
36
- "@longtable/provider-codex": "0.1.48",
37
- "@longtable/setup": "0.1.48"
32
+ "@longtable/checkpoints": "0.1.49",
33
+ "@longtable/core": "0.1.49",
34
+ "@longtable/memory": "0.1.49",
35
+ "@longtable/provider-claude": "0.1.49",
36
+ "@longtable/provider-codex": "0.1.49",
37
+ "@longtable/setup": "0.1.49"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@types/node": "^22.10.1",