fix(omo-codex): bypass per-rule truncation for plugin-bundled rules
The 12000-char per-rule cap exists to guard against absurd user-authored AGENTS.md files. For PLUGIN_BUNDLED rules the plugin author already controls the body size, and silent mid-section truncation breaks the contract that bundled discipline lands in full. The current bundled Hephaestus baseline is 16k chars: everything from `# AGENTS.md` onward (Output, Success Criteria, Stop Rules, Task Tracking) was being dropped from every Codex session without warning. Skip truncateRule when the source is plugin-bundled; the maxResultChars budget (40000 default) via truncateBudget still caps total injection. Locks the behavior with two regression tests in bundled-rules.test.ts: bundled body > maxRuleChars lands intact, project body > maxRuleChars still truncates with the standard notice.
This commit is contained in:
@@ -20,7 +20,14 @@ function truncateRules(rules: ReadonlyArray<LoadedRule>, options: FormatOptions)
|
||||
const perRuleTruncated = rules.map((rule) => ({
|
||||
path: rule.path,
|
||||
relativePath: rule.relativePath,
|
||||
body: truncateRule(rule.body, { maxChars: options.maxRuleChars, relativePath: rule.relativePath }).body,
|
||||
// Plugin-bundled rules ship as-is. The per-rule cap exists to guard against absurd
|
||||
// user-authored AGENTS.md files; bundled rules are author-controlled and silent
|
||||
// mid-section truncation would break the contract that the rule landed in full.
|
||||
// The overall maxResultChars budget still applies via truncateBudget below.
|
||||
body:
|
||||
rule.source === "plugin-bundled"
|
||||
? rule.body
|
||||
: truncateRule(rule.body, { maxChars: options.maxRuleChars, relativePath: rule.relativePath }).body,
|
||||
}));
|
||||
const budgetedRules = truncateBudget({
|
||||
rules: perRuleTruncated.map((rule) => ({ body: rule.body, relativePath: rule.relativePath })),
|
||||
|
||||
@@ -212,4 +212,57 @@ describe("plugin bundled rules", () => {
|
||||
expect(compactOutput).toBe("");
|
||||
expect(output).toContain(BUNDLED_BODY);
|
||||
});
|
||||
|
||||
it("#given bundled rule body exceeds per-rule cap #when SessionStart runs #then bundled body lands in full without truncation", async () => {
|
||||
// given
|
||||
const root = mkdtempSync(join(tmpdir(), "codex-rules-bundled-large-project-"));
|
||||
const pluginRoot = mkdtempSync(join(tmpdir(), "codex-rules-bundled-large-plugin-"));
|
||||
const pluginData = mkdtempSync(join(tmpdir(), "codex-rules-bundled-large-data-"));
|
||||
tempDirectories.push(root, pluginRoot, pluginData);
|
||||
writeFileSync(join(root, "package.json"), JSON.stringify({ name: "fixture" }));
|
||||
mkdirSync(join(pluginRoot, "bundled-rules"), { recursive: true });
|
||||
const oversizedBody = "The bundled craftsman discipline is non-negotiable. ".repeat(400);
|
||||
expect(oversizedBody.length).toBeGreaterThan(12000);
|
||||
const tailMarker = "BUNDLED_TAIL_SENTINEL_LANDS_IN_FULL";
|
||||
const bundledBody = `${oversizedBody}\n\n${tailMarker}\n`;
|
||||
writeFileSync(join(pluginRoot, "bundled-rules", "hephaestus.md"), ruleMarkdown(bundledBody));
|
||||
process.env["PLUGIN_ROOT"] = pluginRoot;
|
||||
|
||||
// when
|
||||
const output = await runSessionStartHook(sessionStartInput(root), {
|
||||
pluginDataRoot: pluginData,
|
||||
env: BUNDLED_ONLY_ENV,
|
||||
});
|
||||
|
||||
// then
|
||||
expect(output).toContain(tailMarker);
|
||||
expect(output).not.toContain("[Rule truncated. Read full rule:");
|
||||
});
|
||||
|
||||
it("#given project rule body exceeds per-rule cap #when SessionStart runs #then project body is truncated", async () => {
|
||||
// given
|
||||
const root = mkdtempSync(join(tmpdir(), "codex-rules-project-large-project-"));
|
||||
const pluginRoot = mkdtempSync(join(tmpdir(), "codex-rules-project-large-plugin-"));
|
||||
const pluginData = mkdtempSync(join(tmpdir(), "codex-rules-project-large-data-"));
|
||||
tempDirectories.push(root, pluginRoot, pluginData);
|
||||
writeFileSync(join(root, "package.json"), JSON.stringify({ name: "fixture" }));
|
||||
mkdirSync(join(root, ".omo", "rules"), { recursive: true });
|
||||
mkdirSync(join(pluginRoot, "bundled-rules"), { recursive: true });
|
||||
const oversizedBody = "The project rule body is intentionally oversized for the cap test. ".repeat(300);
|
||||
expect(oversizedBody.length).toBeGreaterThan(12000);
|
||||
const tailMarker = "PROJECT_TAIL_SENTINEL_SHOULD_NOT_LAND";
|
||||
const projectBody = `${oversizedBody}\n\n${tailMarker}\n`;
|
||||
writeFileSync(join(root, ".omo", "rules", "oversized.md"), ruleMarkdown(projectBody));
|
||||
process.env["PLUGIN_ROOT"] = pluginRoot;
|
||||
|
||||
// when
|
||||
const output = await runSessionStartHook(sessionStartInput(root), {
|
||||
pluginDataRoot: pluginData,
|
||||
env: { CODEX_RULES_ENABLED_SOURCES: ".omo/rules" },
|
||||
});
|
||||
|
||||
// then
|
||||
expect(output).toContain("[Rule truncated. Read full rule: .omo/rules/oversized.md]");
|
||||
expect(output).not.toContain(tailMarker);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user