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:
YeonGyu-Kim
2026-05-27 15:49:32 +09:00
parent 94b94f26d6
commit 171a2535d7
2 changed files with 61 additions and 1 deletions
@@ -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);
});
});