@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 +16 -13
- package/dist/cli.js +265 -36
- package/dist/codex-hooks.d.ts +22 -0
- package/dist/codex-hooks.js +240 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/longtable-codex-native-hook.d.ts +4 -0
- package/dist/longtable-codex-native-hook.js +314 -0
- package/dist/project-session.d.ts +108 -3
- package/dist/project-session.js +420 -23
- package/dist/prompt-aliases.js +5 -5
- package/dist/question-obligations.d.ts +22 -0
- package/dist/question-obligations.js +112 -0
- package/package.json +7 -7
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
export const LONGTABLE_MANAGED_HOOK_EVENTS = [
|
|
3
|
+
"SessionStart",
|
|
4
|
+
"PreToolUse",
|
|
5
|
+
"PostToolUse",
|
|
6
|
+
"UserPromptSubmit",
|
|
7
|
+
"Stop"
|
|
8
|
+
];
|
|
9
|
+
function isPlainObject(value) {
|
|
10
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
11
|
+
}
|
|
12
|
+
function cloneJson(value) {
|
|
13
|
+
return structuredClone(value);
|
|
14
|
+
}
|
|
15
|
+
function buildCommandHook(command, options = {}) {
|
|
16
|
+
const hook = {
|
|
17
|
+
type: "command",
|
|
18
|
+
command,
|
|
19
|
+
...(options.statusMessage ? { statusMessage: options.statusMessage } : {}),
|
|
20
|
+
...(typeof options.timeout === "number" ? { timeout: options.timeout } : {})
|
|
21
|
+
};
|
|
22
|
+
return {
|
|
23
|
+
...(options.matcher ? { matcher: options.matcher } : {}),
|
|
24
|
+
hooks: [hook]
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export function buildManagedCodexHooksConfig(packageRoot) {
|
|
28
|
+
const hookScript = join(packageRoot, "dist", "longtable-codex-native-hook.js");
|
|
29
|
+
const command = `node "${hookScript}"`;
|
|
30
|
+
return {
|
|
31
|
+
hooks: {
|
|
32
|
+
SessionStart: [
|
|
33
|
+
buildCommandHook(command, {
|
|
34
|
+
matcher: "startup|resume"
|
|
35
|
+
})
|
|
36
|
+
],
|
|
37
|
+
PreToolUse: [
|
|
38
|
+
buildCommandHook(command, {
|
|
39
|
+
matcher: "Bash",
|
|
40
|
+
statusMessage: "Running LongTable checkpoint guard"
|
|
41
|
+
})
|
|
42
|
+
],
|
|
43
|
+
PostToolUse: [
|
|
44
|
+
buildCommandHook(command, {
|
|
45
|
+
matcher: "Bash",
|
|
46
|
+
statusMessage: "Reviewing LongTable post-tool state"
|
|
47
|
+
})
|
|
48
|
+
],
|
|
49
|
+
UserPromptSubmit: [
|
|
50
|
+
buildCommandHook(command, {
|
|
51
|
+
statusMessage: "Applying LongTable research context"
|
|
52
|
+
})
|
|
53
|
+
],
|
|
54
|
+
Stop: [
|
|
55
|
+
buildCommandHook(command, {
|
|
56
|
+
timeout: 30
|
|
57
|
+
})
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
export function parseCodexHooksConfig(content) {
|
|
63
|
+
try {
|
|
64
|
+
const parsed = JSON.parse(content);
|
|
65
|
+
if (!isPlainObject(parsed)) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
root: cloneJson(parsed),
|
|
70
|
+
hooks: isPlainObject(parsed.hooks) ? cloneJson(parsed.hooks) : {}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function isLongTableManagedHookCommand(command) {
|
|
78
|
+
return /(?:^|[\\/])longtable-codex-native-hook\.js(?:["'\s]|$)/.test(command);
|
|
79
|
+
}
|
|
80
|
+
function countManagedHooksInEntry(entry) {
|
|
81
|
+
if (!isPlainObject(entry) || !Array.isArray(entry.hooks)) {
|
|
82
|
+
return 0;
|
|
83
|
+
}
|
|
84
|
+
return entry.hooks.filter((hook) => isPlainObject(hook) &&
|
|
85
|
+
hook.type === "command" &&
|
|
86
|
+
typeof hook.command === "string" &&
|
|
87
|
+
isLongTableManagedHookCommand(hook.command)).length;
|
|
88
|
+
}
|
|
89
|
+
export function getMissingManagedCodexHookEvents(content) {
|
|
90
|
+
const parsed = parseCodexHooksConfig(content);
|
|
91
|
+
if (!parsed) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
return LONGTABLE_MANAGED_HOOK_EVENTS.filter((eventName) => {
|
|
95
|
+
const entries = Array.isArray(parsed.hooks[eventName]) ? parsed.hooks[eventName] : [];
|
|
96
|
+
return !entries.some((entry) => countManagedHooksInEntry(entry) > 0);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
function stripManagedHooksFromEntry(entry) {
|
|
100
|
+
if (!isPlainObject(entry) || !Array.isArray(entry.hooks)) {
|
|
101
|
+
return { entry: cloneJson(entry), removedCount: 0 };
|
|
102
|
+
}
|
|
103
|
+
const nextHooks = entry.hooks.filter((hook) => {
|
|
104
|
+
if (!isPlainObject(hook)) {
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
return !(hook.type === "command" &&
|
|
108
|
+
typeof hook.command === "string" &&
|
|
109
|
+
isLongTableManagedHookCommand(hook.command));
|
|
110
|
+
});
|
|
111
|
+
const removedCount = entry.hooks.length - nextHooks.length;
|
|
112
|
+
if (removedCount === 0) {
|
|
113
|
+
return { entry: cloneJson(entry), removedCount: 0 };
|
|
114
|
+
}
|
|
115
|
+
if (nextHooks.length === 0) {
|
|
116
|
+
return { entry: null, removedCount };
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
entry: {
|
|
120
|
+
...cloneJson(entry),
|
|
121
|
+
hooks: nextHooks
|
|
122
|
+
},
|
|
123
|
+
removedCount
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function serializeCodexHooksConfig(root) {
|
|
127
|
+
return JSON.stringify(root, null, 2) + "\n";
|
|
128
|
+
}
|
|
129
|
+
export function mergeManagedCodexHooksConfig(existingContent, packageRoot) {
|
|
130
|
+
const managedConfig = buildManagedCodexHooksConfig(packageRoot);
|
|
131
|
+
const parsed = typeof existingContent === "string"
|
|
132
|
+
? parseCodexHooksConfig(existingContent)
|
|
133
|
+
: null;
|
|
134
|
+
const nextRoot = parsed ? cloneJson(parsed.root) : {};
|
|
135
|
+
const nextHooks = parsed ? cloneJson(parsed.hooks) : {};
|
|
136
|
+
for (const eventName of LONGTABLE_MANAGED_HOOK_EVENTS) {
|
|
137
|
+
const existingEntries = Array.isArray(nextHooks[eventName]) ? nextHooks[eventName] : [];
|
|
138
|
+
const preservedEntries = [];
|
|
139
|
+
for (const entry of existingEntries) {
|
|
140
|
+
const stripped = stripManagedHooksFromEntry(entry);
|
|
141
|
+
if (stripped.entry !== null) {
|
|
142
|
+
preservedEntries.push(stripped.entry);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
nextHooks[eventName] = [
|
|
146
|
+
...preservedEntries,
|
|
147
|
+
...managedConfig.hooks[eventName].map((entry) => cloneJson(entry))
|
|
148
|
+
];
|
|
149
|
+
}
|
|
150
|
+
nextRoot.hooks = nextHooks;
|
|
151
|
+
return serializeCodexHooksConfig(nextRoot);
|
|
152
|
+
}
|
|
153
|
+
export function removeManagedCodexHooks(existingContent) {
|
|
154
|
+
const parsed = parseCodexHooksConfig(existingContent);
|
|
155
|
+
if (!parsed) {
|
|
156
|
+
return { nextContent: existingContent, removedCount: 0 };
|
|
157
|
+
}
|
|
158
|
+
const nextRoot = cloneJson(parsed.root);
|
|
159
|
+
const nextHooks = cloneJson(parsed.hooks);
|
|
160
|
+
let removedCount = 0;
|
|
161
|
+
for (const [eventName, rawEntries] of Object.entries(nextHooks)) {
|
|
162
|
+
if (!Array.isArray(rawEntries)) {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const preservedEntries = [];
|
|
166
|
+
for (const entry of rawEntries) {
|
|
167
|
+
const stripped = stripManagedHooksFromEntry(entry);
|
|
168
|
+
removedCount += stripped.removedCount;
|
|
169
|
+
if (stripped.entry !== null) {
|
|
170
|
+
preservedEntries.push(stripped.entry);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (preservedEntries.length > 0) {
|
|
174
|
+
nextHooks[eventName] = preservedEntries;
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
delete nextHooks[eventName];
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (removedCount === 0) {
|
|
181
|
+
return { nextContent: existingContent, removedCount: 0 };
|
|
182
|
+
}
|
|
183
|
+
if (Object.keys(nextHooks).length > 0) {
|
|
184
|
+
nextRoot.hooks = nextHooks;
|
|
185
|
+
return { nextContent: serializeCodexHooksConfig(nextRoot), removedCount };
|
|
186
|
+
}
|
|
187
|
+
delete nextRoot.hooks;
|
|
188
|
+
if (Object.keys(nextRoot).length > 0) {
|
|
189
|
+
return { nextContent: serializeCodexHooksConfig(nextRoot), removedCount };
|
|
190
|
+
}
|
|
191
|
+
return { nextContent: null, removedCount };
|
|
192
|
+
}
|
|
193
|
+
function findSectionBounds(lines, sectionHeader) {
|
|
194
|
+
const start = lines.findIndex((line) => line.trim() === sectionHeader);
|
|
195
|
+
if (start === -1) {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
let end = lines.length;
|
|
199
|
+
for (let index = start + 1; index < lines.length; index += 1) {
|
|
200
|
+
if (/^\s*\[[^\]]+\]\s*$/.test(lines[index])) {
|
|
201
|
+
end = index;
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return { start, end };
|
|
206
|
+
}
|
|
207
|
+
export function enableCodexHooksFeature(existing) {
|
|
208
|
+
const trimmed = existing.trimEnd();
|
|
209
|
+
const lines = trimmed ? trimmed.split(/\r?\n/) : [];
|
|
210
|
+
const section = findSectionBounds(lines, "[features]");
|
|
211
|
+
if (!section) {
|
|
212
|
+
return trimmed
|
|
213
|
+
? `${trimmed}\n\n[features]\ncodex_hooks = true\n`
|
|
214
|
+
: "[features]\ncodex_hooks = true\n";
|
|
215
|
+
}
|
|
216
|
+
const featureLines = lines.slice(section.start + 1, section.end);
|
|
217
|
+
const existingIndex = featureLines.findIndex((line) => /^\s*codex_hooks\s*=/.test(line));
|
|
218
|
+
if (existingIndex !== -1) {
|
|
219
|
+
featureLines[existingIndex] = "codex_hooks = true";
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
featureLines.push("codex_hooks = true");
|
|
223
|
+
}
|
|
224
|
+
const rebuilt = [
|
|
225
|
+
...lines.slice(0, section.start + 1),
|
|
226
|
+
...featureLines,
|
|
227
|
+
...lines.slice(section.end)
|
|
228
|
+
].join("\n");
|
|
229
|
+
return `${rebuilt.trimEnd()}\n`;
|
|
230
|
+
}
|
|
231
|
+
export function codexHooksEnabled(config) {
|
|
232
|
+
const lines = config.split(/\r?\n/);
|
|
233
|
+
const section = findSectionBounds(lines, "[features]");
|
|
234
|
+
if (!section) {
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
return lines
|
|
238
|
+
.slice(section.start + 1, section.end)
|
|
239
|
+
.some((line) => /^\s*codex_hooks\s*=\s*true\s*$/.test(line));
|
|
240
|
+
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
type CodexHookPayload = Record<string, unknown>;
|
|
2
|
+
export declare function dispatchCodexHook(payload: CodexHookPayload, cwdOverride?: string): Promise<Record<string, unknown> | null>;
|
|
3
|
+
export declare function isCodexNativeHookMainModule(moduleUrl: string, argv1: string | undefined): boolean;
|
|
4
|
+
export {};
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import { pathToFileURL } from "node:url";
|
|
2
|
+
import { loadProjectContextFromDirectory, loadWorkspaceState, pendingQuestionObligations } from "./index.js";
|
|
3
|
+
function safeString(value) {
|
|
4
|
+
return typeof value === "string" ? value : "";
|
|
5
|
+
}
|
|
6
|
+
function safeInteger(value) {
|
|
7
|
+
if (typeof value === "number" && Number.isInteger(value)) {
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
if (typeof value === "string" && /^\d+$/.test(value.trim())) {
|
|
11
|
+
return Number.parseInt(value.trim(), 10);
|
|
12
|
+
}
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
function readHookEventName(payload) {
|
|
16
|
+
const candidate = safeString(payload.hook_event_name ??
|
|
17
|
+
payload.hookEventName ??
|
|
18
|
+
payload.event ??
|
|
19
|
+
payload.name).trim();
|
|
20
|
+
if (candidate === "SessionStart" ||
|
|
21
|
+
candidate === "PreToolUse" ||
|
|
22
|
+
candidate === "PostToolUse" ||
|
|
23
|
+
candidate === "UserPromptSubmit" ||
|
|
24
|
+
candidate === "Stop") {
|
|
25
|
+
return candidate;
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
function readPromptText(payload) {
|
|
30
|
+
return safeString(payload.prompt ?? payload.user_prompt ?? payload.userPrompt).trim();
|
|
31
|
+
}
|
|
32
|
+
function readToolName(payload) {
|
|
33
|
+
return safeString(payload.tool_name ?? payload.toolName).trim();
|
|
34
|
+
}
|
|
35
|
+
function readCommandText(payload) {
|
|
36
|
+
const direct = safeString(payload.command ?? payload.input ?? payload.tool_input ?? payload.toolInput).trim();
|
|
37
|
+
if (direct) {
|
|
38
|
+
return direct;
|
|
39
|
+
}
|
|
40
|
+
const toolInput = payload.tool_input ?? payload.toolInput;
|
|
41
|
+
if (toolInput && typeof toolInput === "object") {
|
|
42
|
+
const objectInput = toolInput;
|
|
43
|
+
return safeString(objectInput.command ?? objectInput.input ?? objectInput.cmd).trim();
|
|
44
|
+
}
|
|
45
|
+
return "";
|
|
46
|
+
}
|
|
47
|
+
function readExitCode(payload) {
|
|
48
|
+
return safeInteger(payload.exit_code ?? payload.exitCode);
|
|
49
|
+
}
|
|
50
|
+
function readCombinedOutput(payload) {
|
|
51
|
+
return [
|
|
52
|
+
safeString(payload.stderr),
|
|
53
|
+
safeString(payload.stdout),
|
|
54
|
+
safeString(payload.output)
|
|
55
|
+
].filter(Boolean).join("\n").trim();
|
|
56
|
+
}
|
|
57
|
+
function formatQuestionOptions(question) {
|
|
58
|
+
const options = question.prompt.options.map((option) => option.value);
|
|
59
|
+
if (question.prompt.allowOther) {
|
|
60
|
+
options.push("other");
|
|
61
|
+
}
|
|
62
|
+
return options.join("/");
|
|
63
|
+
}
|
|
64
|
+
function pendingRequiredQuestions(state) {
|
|
65
|
+
return (state.questionLog ?? []).filter((question) => question.status === "pending" && question.prompt.required);
|
|
66
|
+
}
|
|
67
|
+
function pendingObligations(state) {
|
|
68
|
+
return pendingQuestionObligations(state);
|
|
69
|
+
}
|
|
70
|
+
function activeInterviewHook(state) {
|
|
71
|
+
return [...(state.hooks ?? [])].reverse().find((hook) => hook.kind === "longtable_interview" &&
|
|
72
|
+
(hook.status === "pending" || hook.status === "active" || hook.status === "ready_to_confirm"));
|
|
73
|
+
}
|
|
74
|
+
function buildAdditionalContextOutput(hookEventName, additionalContext) {
|
|
75
|
+
return {
|
|
76
|
+
hookSpecificOutput: {
|
|
77
|
+
hookEventName,
|
|
78
|
+
additionalContext
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function buildBlockOutput(hookEventName, reason, additionalContext) {
|
|
83
|
+
return {
|
|
84
|
+
hookSpecificOutput: {
|
|
85
|
+
hookEventName,
|
|
86
|
+
permissionDecision: "deny",
|
|
87
|
+
permissionDecisionReason: reason,
|
|
88
|
+
additionalContext
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function buildStopBlockOutput(reason) {
|
|
93
|
+
return {
|
|
94
|
+
decision: "block",
|
|
95
|
+
reason
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function looksLikeClosurePrompt(prompt) {
|
|
99
|
+
const normalized = prompt.trim();
|
|
100
|
+
if (!normalized) {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
return /\b(final|finalize|commit|ship|submit|revise|rewrite|draft|publish|implement|fix)\b/i.test(normalized)
|
|
104
|
+
|| /최종|확정|커밋|제출|수정|초안|구현|진행|고쳐/.test(normalized);
|
|
105
|
+
}
|
|
106
|
+
function isStateChangingBash(command) {
|
|
107
|
+
const normalized = command.trim();
|
|
108
|
+
if (!normalized) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
return /\b(git\s+commit|npm\s+version|mv|cp|rm|sed\s+-i|perl\s+-i|tee|touch|mkdir|rmdir|apply_patch|patch)\b/.test(normalized)
|
|
112
|
+
|| />\s*\S+/.test(normalized);
|
|
113
|
+
}
|
|
114
|
+
async function loadLongTableRuntime(startPath) {
|
|
115
|
+
const context = await loadProjectContextFromDirectory(startPath);
|
|
116
|
+
if (!context) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
const state = await loadWorkspaceState(context);
|
|
120
|
+
return { context, state };
|
|
121
|
+
}
|
|
122
|
+
function buildWorkspaceSummary(runtime) {
|
|
123
|
+
const { context, state } = runtime;
|
|
124
|
+
const lines = [
|
|
125
|
+
"LongTable workspace detected.",
|
|
126
|
+
`Current goal: ${context.session.currentGoal}.`,
|
|
127
|
+
context.session.currentBlocker ? `Current blocker: ${context.session.currentBlocker}.` : "",
|
|
128
|
+
context.session.protectedDecision ? `Protected decision: ${context.session.protectedDecision}.` : "",
|
|
129
|
+
state.firstResearchShape ? `First research shape: ${state.firstResearchShape.handle}.` : "",
|
|
130
|
+
context.session.nextAction ? `Next action: ${context.session.nextAction}.` : ""
|
|
131
|
+
].filter(Boolean);
|
|
132
|
+
return lines;
|
|
133
|
+
}
|
|
134
|
+
function buildPendingQuestionContext(question) {
|
|
135
|
+
return [
|
|
136
|
+
`Required Researcher Checkpoint is still pending: ${question.prompt.question}`,
|
|
137
|
+
`Options: ${formatQuestionOptions(question)}`,
|
|
138
|
+
`Record it with longtable decide --question ${question.id} --answer <value> if you are outside MCP elicitation.`
|
|
139
|
+
].join("\n");
|
|
140
|
+
}
|
|
141
|
+
function buildPendingObligationContext(obligation) {
|
|
142
|
+
return [
|
|
143
|
+
`Pending LongTable research obligation: ${obligation.prompt}`,
|
|
144
|
+
obligation.reason,
|
|
145
|
+
obligation.questionId
|
|
146
|
+
? `This obligation is linked to question ${obligation.questionId}; answer it before treating the direction as settled.`
|
|
147
|
+
: "Resume the LongTable interview and let it ask the next research-facing checkpoint before settling the direction."
|
|
148
|
+
].join("\n");
|
|
149
|
+
}
|
|
150
|
+
function buildActiveInterviewContext(hook) {
|
|
151
|
+
const turnCount = hook.turns?.length ?? 0;
|
|
152
|
+
return [
|
|
153
|
+
"A LongTable interview is currently active.",
|
|
154
|
+
`Interview status: ${hook.status}.`,
|
|
155
|
+
`Turns recorded: ${turnCount}.`,
|
|
156
|
+
"Do not finalize the research direction until the interview is either summarized into a First Research Shape or explicitly cleared."
|
|
157
|
+
].join("\n");
|
|
158
|
+
}
|
|
159
|
+
function sessionStartContext(runtime) {
|
|
160
|
+
const blockingQuestion = pendingRequiredQuestions(runtime.state)[0];
|
|
161
|
+
const blockingObligation = pendingObligations(runtime.state)[0];
|
|
162
|
+
const interview = activeInterviewHook(runtime.state);
|
|
163
|
+
const sections = [buildWorkspaceSummary(runtime).join("\n")];
|
|
164
|
+
if (blockingQuestion) {
|
|
165
|
+
sections.push(buildPendingQuestionContext(blockingQuestion));
|
|
166
|
+
}
|
|
167
|
+
else if (blockingObligation) {
|
|
168
|
+
sections.push(buildPendingObligationContext(blockingObligation));
|
|
169
|
+
}
|
|
170
|
+
else if (interview) {
|
|
171
|
+
sections.push(buildActiveInterviewContext(interview));
|
|
172
|
+
}
|
|
173
|
+
sections.push("Treat `.longtable/` state and `CURRENT.md` as the source of truth for this workspace.");
|
|
174
|
+
return sections.filter(Boolean).join("\n\n");
|
|
175
|
+
}
|
|
176
|
+
function userPromptSubmitContext(runtime, prompt) {
|
|
177
|
+
const blockingQuestion = pendingRequiredQuestions(runtime.state)[0];
|
|
178
|
+
if (blockingQuestion) {
|
|
179
|
+
return buildPendingQuestionContext(blockingQuestion);
|
|
180
|
+
}
|
|
181
|
+
const blockingObligation = pendingObligations(runtime.state)[0];
|
|
182
|
+
if (blockingObligation) {
|
|
183
|
+
return buildPendingObligationContext(blockingObligation);
|
|
184
|
+
}
|
|
185
|
+
const interview = activeInterviewHook(runtime.state);
|
|
186
|
+
if (interview) {
|
|
187
|
+
return buildActiveInterviewContext(interview);
|
|
188
|
+
}
|
|
189
|
+
if (runtime.context.session.protectedDecision && looksLikeClosurePrompt(prompt)) {
|
|
190
|
+
return [
|
|
191
|
+
`This workspace marks ${runtime.context.session.protectedDecision} as a protected decision.`,
|
|
192
|
+
"Before you settle it through drafting, revision, or closure, surface one researcher-facing checkpoint grounded in the current blocker or open questions."
|
|
193
|
+
].join("\n");
|
|
194
|
+
}
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
function preToolUseOutput(runtime, payload) {
|
|
198
|
+
if (readToolName(payload) !== "Bash") {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
const command = readCommandText(payload);
|
|
202
|
+
if (!isStateChangingBash(command)) {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
const blockingQuestion = pendingRequiredQuestions(runtime.state)[0];
|
|
206
|
+
if (blockingQuestion) {
|
|
207
|
+
return buildBlockOutput("PreToolUse", "A required LongTable checkpoint is still pending before a state-changing Bash command.", buildPendingQuestionContext(blockingQuestion));
|
|
208
|
+
}
|
|
209
|
+
const blockingObligation = pendingObligations(runtime.state)[0];
|
|
210
|
+
if (blockingObligation) {
|
|
211
|
+
return buildBlockOutput("PreToolUse", "A LongTable research obligation is still pending before a state-changing Bash command.", buildPendingObligationContext(blockingObligation));
|
|
212
|
+
}
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
function postToolUseOutput(runtime, payload) {
|
|
216
|
+
if (readToolName(payload) !== "Bash") {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
const command = readCommandText(payload);
|
|
220
|
+
const exitCode = readExitCode(payload);
|
|
221
|
+
const output = readCombinedOutput(payload);
|
|
222
|
+
const blockingQuestion = pendingRequiredQuestions(runtime.state)[0];
|
|
223
|
+
const blockingObligation = pendingObligations(runtime.state)[0];
|
|
224
|
+
if ((blockingQuestion || blockingObligation) && isStateChangingBash(command)) {
|
|
225
|
+
return buildBlockOutput("PostToolUse", "A state-changing Bash command completed while LongTable still had an unresolved checkpoint or obligation.", blockingQuestion
|
|
226
|
+
? buildPendingQuestionContext(blockingQuestion)
|
|
227
|
+
: buildPendingObligationContext(blockingObligation));
|
|
228
|
+
}
|
|
229
|
+
if (exitCode !== null && exitCode !== 0 && output) {
|
|
230
|
+
return buildBlockOutput("PostToolUse", "The Bash command returned a non-zero exit code and should be reviewed before LongTable continues.", "Review the command output and explain what failed before retrying or continuing.");
|
|
231
|
+
}
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
function stopOutput(runtime) {
|
|
235
|
+
const blockingQuestion = pendingRequiredQuestions(runtime.state)[0];
|
|
236
|
+
if (blockingQuestion) {
|
|
237
|
+
return buildStopBlockOutput("A required LongTable Researcher Checkpoint is still pending.");
|
|
238
|
+
}
|
|
239
|
+
const blockingObligation = pendingObligations(runtime.state)[0];
|
|
240
|
+
if (blockingObligation) {
|
|
241
|
+
return buildStopBlockOutput("A LongTable research obligation is still pending.");
|
|
242
|
+
}
|
|
243
|
+
const interview = activeInterviewHook(runtime.state);
|
|
244
|
+
if (interview) {
|
|
245
|
+
return buildStopBlockOutput("A LongTable interview is still active and should not be closed silently.");
|
|
246
|
+
}
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
export async function dispatchCodexHook(payload, cwdOverride) {
|
|
250
|
+
const hookEventName = readHookEventName(payload);
|
|
251
|
+
if (!hookEventName) {
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
const runtime = await loadLongTableRuntime(cwdOverride ?? process.cwd());
|
|
255
|
+
if (!runtime) {
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
if (hookEventName === "SessionStart") {
|
|
259
|
+
return buildAdditionalContextOutput(hookEventName, sessionStartContext(runtime));
|
|
260
|
+
}
|
|
261
|
+
if (hookEventName === "UserPromptSubmit") {
|
|
262
|
+
const additionalContext = userPromptSubmitContext(runtime, readPromptText(payload));
|
|
263
|
+
return additionalContext
|
|
264
|
+
? buildAdditionalContextOutput(hookEventName, additionalContext)
|
|
265
|
+
: null;
|
|
266
|
+
}
|
|
267
|
+
if (hookEventName === "PreToolUse") {
|
|
268
|
+
return preToolUseOutput(runtime, payload);
|
|
269
|
+
}
|
|
270
|
+
if (hookEventName === "PostToolUse") {
|
|
271
|
+
return postToolUseOutput(runtime, payload);
|
|
272
|
+
}
|
|
273
|
+
if (hookEventName === "Stop") {
|
|
274
|
+
return stopOutput(runtime);
|
|
275
|
+
}
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
async function readStdinJson() {
|
|
279
|
+
const chunks = [];
|
|
280
|
+
for await (const chunk of process.stdin) {
|
|
281
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
|
|
282
|
+
}
|
|
283
|
+
const raw = Buffer.concat(chunks).toString("utf8").trim();
|
|
284
|
+
if (!raw) {
|
|
285
|
+
return {};
|
|
286
|
+
}
|
|
287
|
+
try {
|
|
288
|
+
const parsed = JSON.parse(raw);
|
|
289
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
290
|
+
}
|
|
291
|
+
catch {
|
|
292
|
+
return {};
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
export function isCodexNativeHookMainModule(moduleUrl, argv1) {
|
|
296
|
+
if (!argv1) {
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
return moduleUrl === pathToFileURL(argv1).href;
|
|
300
|
+
}
|
|
301
|
+
async function main() {
|
|
302
|
+
const payload = await readStdinJson();
|
|
303
|
+
const output = await dispatchCodexHook(payload);
|
|
304
|
+
if (output) {
|
|
305
|
+
process.stdout.write(JSON.stringify(output));
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (isCodexNativeHookMainModule(import.meta.url, process.argv[1])) {
|
|
309
|
+
main().catch((error) => {
|
|
310
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
311
|
+
process.stderr.write(`${message}\n`);
|
|
312
|
+
process.exit(1);
|
|
313
|
+
});
|
|
314
|
+
}
|