@longtable/cli 0.1.47 → 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 +155 -15
- package/dist/codex-hooks.d.ts +9 -2
- package/dist/codex-hooks.js +175 -7
- package/dist/longtable-codex-native-hook.js +36 -0
- package/dist/project-session.d.ts +66 -1
- package/dist/project-session.js +538 -12
- package/package.json +7 -7
package/dist/cli.js
CHANGED
|
@@ -17,8 +17,8 @@ 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";
|
|
21
|
-
import { appendInvocationRecordToWorkspace, assertWorkspaceNotBlocked, answerWorkspaceQuestion, buildQuestionOpportunitySpecs, clearWorkspaceQuestion, createWorkspaceFollowUpQuestions, createWorkspaceQuestion, createOrUpdateProjectWorkspace, inspectProjectWorkspace, loadWorkspaceState, loadProjectContextFromDirectory, pruneWorkspaceQuestions, repairWorkspaceStateConsistency, renderProjectWorkspaceSummary, syncCurrentWorkspaceView } from "./project-session.js";
|
|
20
|
+
import { LONGTABLE_MANAGED_HOOK_EVENTS, codexHooksEnabled, enableCodexHooksFeature, getMissingManagedCodexHookEvents, getMissingManagedCodexHookTrustState, mergeCodexHookTrustState, mergeManagedCodexHooksConfig, removeCodexHookTrustState, removeManagedCodexHooks } from "./codex-hooks.js";
|
|
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";
|
|
24
24
|
const VALID_MODES = new Set([
|
|
@@ -62,6 +62,11 @@ const LONGTABLE_MCP_MANAGED_TOOLS = [
|
|
|
62
62
|
"summarize_interview",
|
|
63
63
|
"summarize_research_specification",
|
|
64
64
|
"read_research_specification",
|
|
65
|
+
"propose_research_spec_patch",
|
|
66
|
+
"apply_research_spec_patch",
|
|
67
|
+
"diff_research_specification",
|
|
68
|
+
"read_research_spec_history",
|
|
69
|
+
"find_unincorporated_evidence",
|
|
65
70
|
"cancel_interview",
|
|
66
71
|
"confirm_first_research_shape",
|
|
67
72
|
"confirm_research_specification",
|
|
@@ -76,6 +81,11 @@ const LONGTABLE_MCP_MANAGED_TOOLS = [
|
|
|
76
81
|
const LONGTABLE_MCP_RESEARCH_SPECIFICATION_TOOLS = [
|
|
77
82
|
"summarize_research_specification",
|
|
78
83
|
"read_research_specification",
|
|
84
|
+
"propose_research_spec_patch",
|
|
85
|
+
"apply_research_spec_patch",
|
|
86
|
+
"diff_research_specification",
|
|
87
|
+
"read_research_spec_history",
|
|
88
|
+
"find_unincorporated_evidence",
|
|
79
89
|
"confirm_research_specification"
|
|
80
90
|
];
|
|
81
91
|
function style(text, prefix) {
|
|
@@ -132,6 +142,7 @@ function usage() {
|
|
|
132
142
|
" 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>]",
|
|
133
143
|
" 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>]",
|
|
134
144
|
" longtable audit [questions|roles] [--json]",
|
|
145
|
+
" longtable spec [read|history|diff|unincorporated|apply|propose] [--cwd <path>] [--json] [--spec-file <path>] [--patch-id <id>]",
|
|
135
146
|
" longtable roles [--json]",
|
|
136
147
|
" longtable show [--json] [--path <file>]",
|
|
137
148
|
" longtable install [--json] [--path <file>] [--runtime-path <file>]",
|
|
@@ -182,7 +193,7 @@ function parseArgs(argv) {
|
|
|
182
193
|
const values = {};
|
|
183
194
|
let subcommand = maybeSubcommand;
|
|
184
195
|
const modeCommand = command && VALID_MODES.has(command);
|
|
185
|
-
const directCommand = command && ["init", "setup", "start", "resume", "doctor", "status", "audit", "roles", "show", "install", "mcp", "codex", "claude", "ask", "clarify", "question", "clear-question", "prune-questions", "panel", "decide", "sentinel", "team", "access", "search"].includes(command);
|
|
196
|
+
const directCommand = command && ["init", "setup", "start", "resume", "doctor", "status", "audit", "roles", "show", "install", "mcp", "codex", "claude", "ask", "clarify", "question", "clear-question", "prune-questions", "panel", "decide", "sentinel", "team", "access", "search", "spec"].includes(command);
|
|
186
197
|
let startIndex = 1;
|
|
187
198
|
if (modeCommand) {
|
|
188
199
|
subcommand = undefined;
|
|
@@ -191,7 +202,7 @@ function parseArgs(argv) {
|
|
|
191
202
|
else if (command === "codex" || command === "claude" || command === "mcp") {
|
|
192
203
|
startIndex = 2;
|
|
193
204
|
}
|
|
194
|
-
else if ((command === "access" || command === "search") && maybeSubcommand && !maybeSubcommand.startsWith("--")) {
|
|
205
|
+
else if ((command === "access" || command === "search" || command === "spec") && maybeSubcommand && !maybeSubcommand.startsWith("--")) {
|
|
195
206
|
subcommand = maybeSubcommand;
|
|
196
207
|
startIndex = 2;
|
|
197
208
|
}
|
|
@@ -1314,8 +1325,9 @@ async function installCodexNativeHooks(args) {
|
|
|
1314
1325
|
const packageRoot = resolveCliPackageRoot();
|
|
1315
1326
|
const existingConfig = existsSync(configPath) ? await readFile(configPath, "utf8") : "";
|
|
1316
1327
|
const existingHooks = existsSync(hooksPath) ? await readFile(hooksPath, "utf8") : "";
|
|
1317
|
-
const nextConfig = enableCodexHooksFeature(existingConfig);
|
|
1318
1328
|
const nextHooks = mergeManagedCodexHooksConfig(existingHooks, packageRoot);
|
|
1329
|
+
const configWithHooksFeature = enableCodexHooksFeature(existingConfig, "hooks");
|
|
1330
|
+
const nextConfig = mergeCodexHookTrustState(configWithHooksFeature, hooksPath, nextHooks);
|
|
1319
1331
|
await mkdir(dirname(configPath), { recursive: true });
|
|
1320
1332
|
await mkdir(dirname(hooksPath), { recursive: true });
|
|
1321
1333
|
await writeFile(configPath, nextConfig, "utf8");
|
|
@@ -1325,14 +1337,21 @@ async function installCodexNativeHooks(args) {
|
|
|
1325
1337
|
hooksPath,
|
|
1326
1338
|
codexHooksEnabled: codexHooksEnabled(nextConfig),
|
|
1327
1339
|
managedEvents: [...LONGTABLE_MANAGED_HOOK_EVENTS],
|
|
1340
|
+
managedTrustEntries: getMissingManagedCodexHookTrustState("", hooksPath, nextHooks).length,
|
|
1328
1341
|
write: true
|
|
1329
1342
|
};
|
|
1330
1343
|
}
|
|
1331
1344
|
async function removeCodexNativeHooks(args) {
|
|
1332
1345
|
const configPath = resolveCodexMcpConfigPath(args);
|
|
1333
1346
|
const hooksPath = resolveCodexHooksPath(args);
|
|
1347
|
+
const configContent = existsSync(configPath) ? await readFile(configPath, "utf8") : "";
|
|
1334
1348
|
const existingHooks = existsSync(hooksPath) ? await readFile(hooksPath, "utf8") : "";
|
|
1349
|
+
const nextConfig = existingHooks
|
|
1350
|
+
? removeCodexHookTrustState(configContent, hooksPath, existingHooks)
|
|
1351
|
+
: configContent;
|
|
1335
1352
|
const removed = existingHooks ? removeManagedCodexHooks(existingHooks) : { nextContent: null, removedCount: 0 };
|
|
1353
|
+
await mkdir(dirname(configPath), { recursive: true });
|
|
1354
|
+
await writeFile(configPath, nextConfig, "utf8");
|
|
1336
1355
|
if (removed.nextContent === null) {
|
|
1337
1356
|
await rm(hooksPath, { force: true });
|
|
1338
1357
|
}
|
|
@@ -1340,12 +1359,12 @@ async function removeCodexNativeHooks(args) {
|
|
|
1340
1359
|
await mkdir(dirname(hooksPath), { recursive: true });
|
|
1341
1360
|
await writeFile(hooksPath, removed.nextContent, "utf8");
|
|
1342
1361
|
}
|
|
1343
|
-
const configContent = existsSync(configPath) ? await readFile(configPath, "utf8") : "";
|
|
1344
1362
|
return {
|
|
1345
1363
|
configPath,
|
|
1346
1364
|
hooksPath,
|
|
1347
|
-
codexHooksEnabled: codexHooksEnabled(
|
|
1365
|
+
codexHooksEnabled: codexHooksEnabled(nextConfig),
|
|
1348
1366
|
managedEvents: removed.removedCount > 0 ? [...LONGTABLE_MANAGED_HOOK_EVENTS] : [],
|
|
1367
|
+
managedTrustEntries: 0,
|
|
1349
1368
|
write: true
|
|
1350
1369
|
};
|
|
1351
1370
|
}
|
|
@@ -1354,8 +1373,9 @@ function renderCodexHookInstallSummary(result) {
|
|
|
1354
1373
|
"LongTable Codex hooks",
|
|
1355
1374
|
`- config: ${result.configPath}`,
|
|
1356
1375
|
`- hooks: ${result.hooksPath}`,
|
|
1357
|
-
`-
|
|
1358
|
-
`- 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}`
|
|
1359
1379
|
].join("\n");
|
|
1360
1380
|
}
|
|
1361
1381
|
function renderMcpInstallSummary(result) {
|
|
@@ -1554,6 +1574,9 @@ async function collectDoctorStatus(args) {
|
|
|
1554
1574
|
const missingManagedHookEvents = codexHooksContent
|
|
1555
1575
|
? (getMissingManagedCodexHookEvents(codexHooksContent) ?? [...LONGTABLE_MANAGED_HOOK_EVENTS])
|
|
1556
1576
|
: [...LONGTABLE_MANAGED_HOOK_EVENTS];
|
|
1577
|
+
const missingManagedHookTrustState = codexHooksContent
|
|
1578
|
+
? getMissingManagedCodexHookTrustState(codexMcpConfig, codexHooksPath, codexHooksContent)
|
|
1579
|
+
: [];
|
|
1557
1580
|
const expectedCodexSkills = buildCodexSkillSpecs(roles, skillSurface).map((skill) => skill.name);
|
|
1558
1581
|
const expectedClaudeSkills = buildClaudeSkillSpecs(roles, skillSurface).map((skill) => skill.name);
|
|
1559
1582
|
const [codexSkills, claudeSkills, codexAliases, workspace] = await Promise.all([
|
|
@@ -1590,7 +1613,8 @@ async function collectDoctorStatus(args) {
|
|
|
1590
1613
|
hooksPath: codexHooksPath,
|
|
1591
1614
|
hooksExists: existsSync(codexHooksPath),
|
|
1592
1615
|
codexHooksEnabled: codexHooksEnabled(codexMcpConfig),
|
|
1593
|
-
missingManagedHookEvents
|
|
1616
|
+
missingManagedHookEvents,
|
|
1617
|
+
missingManagedHookTrustState
|
|
1594
1618
|
},
|
|
1595
1619
|
claude: {
|
|
1596
1620
|
command: "claude",
|
|
@@ -1638,8 +1662,9 @@ function renderDoctorStatus(status) {
|
|
|
1638
1662
|
: ["- Research Specification MCP tools: complete"]),
|
|
1639
1663
|
`- MCP elicitation approval: ${status.providers.codex.mcpElicitationsAllowed ? "allowed" : "not allowed"}`,
|
|
1640
1664
|
`- Codex hooks file: ${status.providers.codex.hooksExists ? "present" : "missing"} (${status.providers.codex.hooksPath})`,
|
|
1641
|
-
`-
|
|
1665
|
+
`- hooks feature: ${status.providers.codex.codexHooksEnabled ? "enabled" : "missing"}`,
|
|
1642
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}`}`,
|
|
1643
1668
|
"",
|
|
1644
1669
|
...renderProviderDoctorBlock("Claude", status.providers.claude),
|
|
1645
1670
|
"",
|
|
@@ -1689,12 +1714,15 @@ function renderDoctorStatus(status) {
|
|
|
1689
1714
|
status.providers.codex.missingMcpTools.length > 0 ||
|
|
1690
1715
|
!status.providers.codex.codexHooksEnabled ||
|
|
1691
1716
|
status.providers.codex.missingManagedHookEvents.length > 0 ||
|
|
1717
|
+
status.providers.codex.missingManagedHookTrustState.length > 0 ||
|
|
1692
1718
|
(status.setupExists &&
|
|
1693
1719
|
(!status.providers.codex.runtimeExists || !status.providers.claude.runtimeExists));
|
|
1694
1720
|
if (canFix) {
|
|
1695
1721
|
nextActions.push("longtable doctor --fix");
|
|
1696
1722
|
}
|
|
1697
|
-
if (!status.providers.codex.codexHooksEnabled ||
|
|
1723
|
+
if (!status.providers.codex.codexHooksEnabled ||
|
|
1724
|
+
status.providers.codex.missingManagedHookEvents.length > 0 ||
|
|
1725
|
+
status.providers.codex.missingManagedHookTrustState.length > 0) {
|
|
1698
1726
|
nextActions.push("longtable codex install-hooks");
|
|
1699
1727
|
}
|
|
1700
1728
|
if (!status.providers.codex.longtableMcpConfigured ||
|
|
@@ -1821,7 +1849,9 @@ async function repairDoctorStatus(args, status) {
|
|
|
1821
1849
|
if (status.providers.codex.legacyPromptFilesInstalled.length > 0) {
|
|
1822
1850
|
repair.removedLegacyPromptFiles = await removeCodexPromptAliases(codexPromptsDir);
|
|
1823
1851
|
}
|
|
1824
|
-
if (!status.providers.codex.codexHooksEnabled ||
|
|
1852
|
+
if (!status.providers.codex.codexHooksEnabled ||
|
|
1853
|
+
status.providers.codex.missingManagedHookEvents.length > 0 ||
|
|
1854
|
+
status.providers.codex.missingManagedHookTrustState.length > 0) {
|
|
1825
1855
|
await installCodexNativeHooks(args);
|
|
1826
1856
|
repair.installedCodexHooks = true;
|
|
1827
1857
|
}
|
|
@@ -2851,6 +2881,108 @@ async function runSearch(subcommand, args) {
|
|
|
2851
2881
|
}
|
|
2852
2882
|
console.log(renderEvidenceRunSummary(run, recordedPath));
|
|
2853
2883
|
}
|
|
2884
|
+
async function requireWorkspaceContext(args) {
|
|
2885
|
+
const workingDirectory = typeof args.cwd === "string" ? args.cwd : cwd();
|
|
2886
|
+
const context = await loadProjectContextFromDirectory(workingDirectory);
|
|
2887
|
+
if (!context) {
|
|
2888
|
+
throw new Error("No LongTable workspace was found from the supplied cwd.");
|
|
2889
|
+
}
|
|
2890
|
+
return context;
|
|
2891
|
+
}
|
|
2892
|
+
async function readResearchSpecificationFile(path) {
|
|
2893
|
+
if (!path) {
|
|
2894
|
+
throw new Error("A Research Specification JSON file is required. Use --spec-file <path>.");
|
|
2895
|
+
}
|
|
2896
|
+
return JSON.parse(await readFile(resolve(path), "utf8"));
|
|
2897
|
+
}
|
|
2898
|
+
async function runSpec(subcommand, args) {
|
|
2899
|
+
const context = await requireWorkspaceContext(args);
|
|
2900
|
+
const command = subcommand ?? "read";
|
|
2901
|
+
if (command === "read" || command === "history") {
|
|
2902
|
+
const history = await readResearchSpecificationHistory(context);
|
|
2903
|
+
if (args.json === true) {
|
|
2904
|
+
console.log(JSON.stringify(history, null, 2));
|
|
2905
|
+
return;
|
|
2906
|
+
}
|
|
2907
|
+
console.log("LongTable Research Specification");
|
|
2908
|
+
console.log(`- title: ${history.specification?.title ?? "missing"}`);
|
|
2909
|
+
console.log(`- status: ${history.specification?.confirmedAt ? "confirmed" : history.specification?.status ?? "missing"}`);
|
|
2910
|
+
console.log(`- revisions: ${history.revisions.length}`);
|
|
2911
|
+
console.log(`- patches: ${history.patches.length}`);
|
|
2912
|
+
console.log(`- evidence records: ${history.evidenceRecords.length}`);
|
|
2913
|
+
for (const revision of history.revisions.slice(-5).reverse()) {
|
|
2914
|
+
console.log(`- v${revision.index}: ${revision.title} (${revision.changeSummary.slice(0, 2).join("; ")})`);
|
|
2915
|
+
}
|
|
2916
|
+
return;
|
|
2917
|
+
}
|
|
2918
|
+
if (command === "unincorporated") {
|
|
2919
|
+
const evidenceRecords = await findUnincorporatedResearchEvidence(context);
|
|
2920
|
+
if (args.json === true) {
|
|
2921
|
+
console.log(JSON.stringify({ evidenceRecords }, null, 2));
|
|
2922
|
+
return;
|
|
2923
|
+
}
|
|
2924
|
+
console.log("Unincorporated Research Evidence");
|
|
2925
|
+
for (const record of evidenceRecords.slice(-10).reverse()) {
|
|
2926
|
+
console.log(`- ${record.id} [${record.sourceKind}]: ${record.summary}`);
|
|
2927
|
+
}
|
|
2928
|
+
return;
|
|
2929
|
+
}
|
|
2930
|
+
if (command === "diff") {
|
|
2931
|
+
const specification = await readResearchSpecificationFile(typeof args["spec-file"] === "string" ? args["spec-file"] : undefined);
|
|
2932
|
+
const state = await loadWorkspaceState(context);
|
|
2933
|
+
const changes = diffResearchSpecifications(state.researchSpecification, specification);
|
|
2934
|
+
if (args.json === true) {
|
|
2935
|
+
console.log(JSON.stringify({ changes }, null, 2));
|
|
2936
|
+
return;
|
|
2937
|
+
}
|
|
2938
|
+
console.log("Research Specification Diff");
|
|
2939
|
+
for (const change of changes) {
|
|
2940
|
+
console.log(`- ${change.summary}`);
|
|
2941
|
+
}
|
|
2942
|
+
return;
|
|
2943
|
+
}
|
|
2944
|
+
if (command === "propose") {
|
|
2945
|
+
const specification = await readResearchSpecificationFile(typeof args["spec-file"] === "string" ? args["spec-file"] : undefined);
|
|
2946
|
+
const result = await proposeResearchSpecificationPatch({
|
|
2947
|
+
context,
|
|
2948
|
+
specification,
|
|
2949
|
+
source: "manual",
|
|
2950
|
+
rationale: typeof args.rationale === "string" ? args.rationale : undefined
|
|
2951
|
+
});
|
|
2952
|
+
if (args.json === true) {
|
|
2953
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2954
|
+
return;
|
|
2955
|
+
}
|
|
2956
|
+
console.log("Research Specification patch proposed");
|
|
2957
|
+
console.log(`- patch: ${result.patch.id}`);
|
|
2958
|
+
console.log(`- changes: ${result.changes.length}`);
|
|
2959
|
+
console.log(`- apply: longtable spec apply --patch-id ${result.patch.id}`);
|
|
2960
|
+
return;
|
|
2961
|
+
}
|
|
2962
|
+
if (command === "apply") {
|
|
2963
|
+
const specification = typeof args["spec-file"] === "string"
|
|
2964
|
+
? await readResearchSpecificationFile(args["spec-file"])
|
|
2965
|
+
: undefined;
|
|
2966
|
+
const result = await applyResearchSpecificationPatch({
|
|
2967
|
+
context,
|
|
2968
|
+
patchId: typeof args["patch-id"] === "string" ? args["patch-id"] : undefined,
|
|
2969
|
+
specification,
|
|
2970
|
+
source: "manual",
|
|
2971
|
+
rationale: typeof args.rationale === "string" ? args.rationale : undefined
|
|
2972
|
+
});
|
|
2973
|
+
if (args.json === true) {
|
|
2974
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2975
|
+
return;
|
|
2976
|
+
}
|
|
2977
|
+
console.log("Research Specification patch applied");
|
|
2978
|
+
console.log(`- revision: v${result.revision.index} (${result.revision.id})`);
|
|
2979
|
+
console.log(`- patch: ${result.patch.id}`);
|
|
2980
|
+
console.log(`- decision: ${result.decision?.id ?? result.patch.decisionRecordId ?? "existing/none"}`);
|
|
2981
|
+
console.log(`- current: ${context.currentFilePath}`);
|
|
2982
|
+
return;
|
|
2983
|
+
}
|
|
2984
|
+
throw new Error(`Unknown spec subcommand: ${command}`);
|
|
2985
|
+
}
|
|
2854
2986
|
async function runQuestion(args) {
|
|
2855
2987
|
const workingDirectory = typeof args.cwd === "string" ? args.cwd : cwd();
|
|
2856
2988
|
const prompt = await resolvePrompt(typeof args.prompt === "string" ? args.prompt : undefined);
|
|
@@ -3660,7 +3792,10 @@ async function runCodexSubcommand(subcommand, args) {
|
|
|
3660
3792
|
hooksExists: existsSync(hooksPath),
|
|
3661
3793
|
missingManagedHookEvents: hooksContent
|
|
3662
3794
|
? (getMissingManagedCodexHookEvents(hooksContent) ?? [...LONGTABLE_MANAGED_HOOK_EVENTS])
|
|
3663
|
-
: [...LONGTABLE_MANAGED_HOOK_EVENTS]
|
|
3795
|
+
: [...LONGTABLE_MANAGED_HOOK_EVENTS],
|
|
3796
|
+
missingManagedHookTrustState: hooksContent
|
|
3797
|
+
? getMissingManagedCodexHookTrustState(configContent, hooksPath, hooksContent)
|
|
3798
|
+
: []
|
|
3664
3799
|
};
|
|
3665
3800
|
if (args.json === true) {
|
|
3666
3801
|
console.log(JSON.stringify(status, null, 2));
|
|
@@ -3692,9 +3827,10 @@ async function runCodexSubcommand(subcommand, args) {
|
|
|
3692
3827
|
}
|
|
3693
3828
|
}
|
|
3694
3829
|
console.log(`- codex config: ${status.codexConfigPath}`);
|
|
3695
|
-
console.log(`-
|
|
3830
|
+
console.log(`- hooks feature: ${status.codexHooksEnabled ? "enabled" : "missing"}`);
|
|
3696
3831
|
console.log(`- hooks file: ${status.hooksExists ? "present" : "missing"} (${status.hooksPath})`);
|
|
3697
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}`}`);
|
|
3698
3834
|
return;
|
|
3699
3835
|
}
|
|
3700
3836
|
throw new Error("Unknown codex subcommand.");
|
|
@@ -3807,6 +3943,10 @@ async function main() {
|
|
|
3807
3943
|
await runSearch(subcommand, values);
|
|
3808
3944
|
return;
|
|
3809
3945
|
}
|
|
3946
|
+
if (command === "spec") {
|
|
3947
|
+
await runSpec(subcommand, values);
|
|
3948
|
+
return;
|
|
3949
|
+
}
|
|
3810
3950
|
if (command === "ask") {
|
|
3811
3951
|
await runAsk(values);
|
|
3812
3952
|
return;
|
package/dist/codex-hooks.d.ts
CHANGED
|
@@ -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 {};
|
package/dist/codex-hooks.js
CHANGED
|
@@ -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
|
-
|
|
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]\
|
|
214
|
-
:
|
|
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) =>
|
|
385
|
+
const existingIndex = featureLines.findIndex((line) => new RegExp(`^\\s*${flagName}\\s*=`).test(line));
|
|
218
386
|
if (existingIndex !== -1) {
|
|
219
|
-
featureLines[existingIndex] =
|
|
387
|
+
featureLines[existingIndex] = `${flagName} = true`;
|
|
220
388
|
}
|
|
221
389
|
else {
|
|
222
|
-
featureLines.push(
|
|
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
|
}
|