feat(omo-claude): vendor rules component with CC patches
model/turn_id optional (SessionStart keeps model), CLAUDE_PLUGIN_ROOT/DATA env fallback, .claude-plugin manifest path, PostToolUse matcher Write|Edit|MultiEdit, OMO_CLAUDE_RULES_* env aliases, bundled-rules vendored. Injects w/o turn_id (QA). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,241 @@
|
||||
import { configFromEnvironment } from "./config.js";
|
||||
import { createHookDebugTimer } from "./debug-log.js";
|
||||
import { fingerprintDynamicTargets } from "./dynamic-target-fingerprints.js";
|
||||
import { formatAdditionalContextOutput } from "./hook-output.js";
|
||||
import { displayPath, uniqueStrings } from "./path-utils.js";
|
||||
import {
|
||||
clearSessionState,
|
||||
hasPostCompactPending,
|
||||
hydrateEngineState,
|
||||
isPostCompactPending,
|
||||
markSessionCompacted,
|
||||
persistEngineState,
|
||||
sessionCachePath,
|
||||
} from "./persistent-cache.js";
|
||||
import { withPostCompactBudget } from "./post-compact-budget.js";
|
||||
import { createRulesEngine } from "./rules-engine-factory.js";
|
||||
import { extractCodexToolPaths } from "./tool-paths.js";
|
||||
import { filterRulesAlreadyInTranscript } from "./transcript-rule-filter.js";
|
||||
import type { TranscriptSearchOptions } from "./transcript-search.js";
|
||||
|
||||
export type CodexSessionStartInput = {
|
||||
session_id: string;
|
||||
transcript_path: string | null;
|
||||
cwd: string;
|
||||
hook_event_name: "SessionStart";
|
||||
model: string;
|
||||
permission_mode: string;
|
||||
source: "startup" | "resume" | "clear";
|
||||
};
|
||||
|
||||
export type CodexUserPromptSubmitInput = {
|
||||
session_id: string;
|
||||
turn_id: string;
|
||||
transcript_path: string | null;
|
||||
cwd: string;
|
||||
hook_event_name: "UserPromptSubmit";
|
||||
model: string;
|
||||
permission_mode: string;
|
||||
prompt: string;
|
||||
};
|
||||
|
||||
export type CodexPostToolUseInput = {
|
||||
session_id: string;
|
||||
turn_id: string;
|
||||
transcript_path: string | null;
|
||||
cwd: string;
|
||||
hook_event_name: "PostToolUse";
|
||||
model: string;
|
||||
permission_mode: string;
|
||||
tool_name: string;
|
||||
tool_input: unknown;
|
||||
tool_response: unknown;
|
||||
tool_use_id: string;
|
||||
};
|
||||
|
||||
export type CodexPostCompactInput = {
|
||||
session_id: string;
|
||||
turn_id: string;
|
||||
transcript_path: string | null;
|
||||
cwd: string;
|
||||
hook_event_name: "PostCompact";
|
||||
model: string;
|
||||
trigger: "manual" | "auto";
|
||||
};
|
||||
|
||||
export interface CodexRulesHookOptions {
|
||||
env?: NodeJS.ProcessEnv;
|
||||
pluginDataRoot?: string;
|
||||
}
|
||||
|
||||
export async function runSessionStartHook(
|
||||
input: CodexSessionStartInput,
|
||||
options: CodexRulesHookOptions = {},
|
||||
): Promise<string> {
|
||||
const cachePath = sessionCachePath(input.session_id, options.pluginDataRoot);
|
||||
if (input.source === "clear") {
|
||||
clearSessionState(cachePath);
|
||||
} else if (input.source !== "resume" && !hasPostCompactPending(cachePath)) {
|
||||
clearSessionState(cachePath);
|
||||
}
|
||||
const postCompactPending = input.source !== "clear" && isPostCompactPending(cachePath, "static");
|
||||
const transcriptPath = input.source === "clear" ? null : input.transcript_path;
|
||||
return runStaticInjection(
|
||||
input.cwd,
|
||||
transcriptPath,
|
||||
"SessionStart",
|
||||
cachePath,
|
||||
options,
|
||||
postCompactPending ? "static" : undefined,
|
||||
{ latestCompactedReplacementOnly: postCompactPending },
|
||||
);
|
||||
}
|
||||
|
||||
export async function runPostCompactHook(
|
||||
input: CodexPostCompactInput,
|
||||
options: CodexRulesHookOptions = {},
|
||||
): Promise<string> {
|
||||
markSessionCompacted(sessionCachePath(input.session_id, options.pluginDataRoot));
|
||||
return "";
|
||||
}
|
||||
|
||||
export async function runUserPromptSubmitHook(
|
||||
input: CodexUserPromptSubmitInput,
|
||||
options: CodexRulesHookOptions = {},
|
||||
): Promise<string> {
|
||||
const cachePath = sessionCachePath(input.session_id, options.pluginDataRoot);
|
||||
const postCompactPending = isPostCompactPending(cachePath, "static");
|
||||
return runStaticInjection(
|
||||
input.cwd,
|
||||
input.transcript_path,
|
||||
"UserPromptSubmit",
|
||||
cachePath,
|
||||
options,
|
||||
postCompactPending ? "static" : undefined,
|
||||
{ latestCompactedReplacementOnly: postCompactPending },
|
||||
);
|
||||
}
|
||||
|
||||
export async function runPostToolUseHook(
|
||||
input: CodexPostToolUseInput,
|
||||
options: CodexRulesHookOptions = {},
|
||||
): Promise<string> {
|
||||
const debugTimer = createHookDebugTimer("PostToolUse");
|
||||
const config = configFromEnvironment(options.env);
|
||||
debugTimer.lap("config", { disabled: config.disabled, mode: config.mode });
|
||||
if (config.disabled || config.mode === "off" || config.mode === "static") {
|
||||
debugTimer.done({ outputBytes: 0, reason: "disabled" });
|
||||
return "";
|
||||
}
|
||||
|
||||
const targetPaths = extractCodexToolPaths(input, input.cwd);
|
||||
debugTimer.lap("extract", {
|
||||
targets: targetPaths.length,
|
||||
uniqueTargets: uniqueStrings(targetPaths).length,
|
||||
tool: input.tool_name,
|
||||
});
|
||||
const firstTargetPath = targetPaths[0];
|
||||
if (firstTargetPath === undefined) {
|
||||
debugTimer.done({ outputBytes: 0, reason: "no-target" });
|
||||
return "";
|
||||
}
|
||||
|
||||
const cachePath = sessionCachePath(input.session_id, options.pluginDataRoot);
|
||||
const postCompactPending = isPostCompactPending(cachePath, "dynamic");
|
||||
const engine = createRulesEngine(options, postCompactPending ? withPostCompactBudget(config) : config);
|
||||
hydrateEngineState(engine, cachePath);
|
||||
debugTimer.lap("hydrate", {
|
||||
dynamicDedupScopes: engine.state.dynamicDedup.size,
|
||||
dynamicTargetFingerprints: engine.state.dynamicTargetFingerprints.size,
|
||||
staticDedup: engine.state.staticDedup.size,
|
||||
});
|
||||
const dynamicTargetFingerprints = fingerprintDynamicTargets(input.cwd, targetPaths, config);
|
||||
debugTimer.lap("fingerprint", { fingerprints: dynamicTargetFingerprints.length });
|
||||
const pendingTargetFingerprints = dynamicTargetFingerprints.filter(
|
||||
(target) => engine.state.dynamicTargetFingerprints.get(target.cacheKey) !== target.fingerprint,
|
||||
);
|
||||
debugTimer.lap("pending", { pending: pendingTargetFingerprints.length });
|
||||
if (pendingTargetFingerprints.length === 0) {
|
||||
persistEngineState(engine, cachePath, postCompactPending ? "dynamic" : undefined);
|
||||
debugTimer.lap("persist", { reason: "no-pending" });
|
||||
debugTimer.done({ outputBytes: 0, reason: "no-pending" });
|
||||
return "";
|
||||
}
|
||||
|
||||
const loaded = engine.loadDynamicRules(
|
||||
input.cwd,
|
||||
pendingTargetFingerprints.map((target) => target.targetPath),
|
||||
);
|
||||
debugTimer.lap("load", { diagnostics: loaded.diagnostics.length, loadedRules: loaded.rules.length });
|
||||
const rules = filterRulesAlreadyInTranscript(
|
||||
loaded.rules.filter((rule) => !engine.isStaticInjected(rule) && !engine.isDynamicInjected(rule)),
|
||||
input.transcript_path,
|
||||
(rule) => {
|
||||
engine.markDynamicInjected(rule);
|
||||
},
|
||||
{ latestCompactedReplacementOnly: postCompactPending },
|
||||
);
|
||||
debugTimer.lap("filter", { rules: rules.length });
|
||||
for (const target of pendingTargetFingerprints) {
|
||||
engine.state.dynamicTargetFingerprints.set(target.cacheKey, target.fingerprint);
|
||||
}
|
||||
if (rules.length === 0) {
|
||||
persistEngineState(engine, cachePath, postCompactPending ? "dynamic" : undefined);
|
||||
debugTimer.lap("persist", { reason: "no-rules" });
|
||||
debugTimer.done({ outputBytes: 0, reason: "no-rules" });
|
||||
return "";
|
||||
}
|
||||
|
||||
const firstPendingTargetPath = pendingTargetFingerprints[0]?.targetPath ?? firstTargetPath;
|
||||
const block = engine.formatDynamic(rules, displayPath(input.cwd, firstPendingTargetPath));
|
||||
debugTimer.lap("format", { blockChars: block.length, rules: rules.length });
|
||||
for (const rule of rules) {
|
||||
engine.markDynamicInjected(rule);
|
||||
}
|
||||
persistEngineState(engine, cachePath, postCompactPending ? "dynamic" : undefined);
|
||||
debugTimer.lap("persist", { reason: "emit" });
|
||||
const output = formatAdditionalContextOutput("PostToolUse", block);
|
||||
debugTimer.done({ outputBytes: Buffer.byteLength(output), reason: "emit" });
|
||||
return output;
|
||||
}
|
||||
|
||||
function runStaticInjection(
|
||||
cwd: string,
|
||||
transcriptPath: string | null,
|
||||
eventName: "SessionStart" | "UserPromptSubmit",
|
||||
cachePath: string,
|
||||
options: CodexRulesHookOptions,
|
||||
completedPostCompactChannel?: "static",
|
||||
transcriptSearchOptions: TranscriptSearchOptions = {},
|
||||
): string {
|
||||
const config = configFromEnvironment(options.env);
|
||||
if (config.disabled || config.mode === "off" || config.mode === "dynamic") {
|
||||
return "";
|
||||
}
|
||||
|
||||
const effectiveConfig = completedPostCompactChannel === undefined ? config : withPostCompactBudget(config);
|
||||
const engine = createRulesEngine(options, effectiveConfig);
|
||||
hydrateEngineState(engine, cachePath);
|
||||
engine.state.cwd = cwd;
|
||||
|
||||
const loaded = engine.loadStaticRules(cwd);
|
||||
const rules = filterRulesAlreadyInTranscript(
|
||||
loaded.rules.filter((rule) => !engine.isStaticInjected(rule)),
|
||||
transcriptPath,
|
||||
(rule) => {
|
||||
engine.markStaticInjected(rule);
|
||||
},
|
||||
transcriptSearchOptions,
|
||||
);
|
||||
if (rules.length === 0) {
|
||||
persistEngineState(engine, cachePath, completedPostCompactChannel);
|
||||
return "";
|
||||
}
|
||||
|
||||
const block = engine.formatStatic(rules);
|
||||
for (const rule of rules) {
|
||||
engine.markStaticInjected(rule);
|
||||
}
|
||||
persistEngineState(engine, cachePath, completedPostCompactChannel);
|
||||
return formatAdditionalContextOutput(eventName, block);
|
||||
}
|
||||
Reference in New Issue
Block a user