Skip to content

Commit 6d89acb

Browse files
committed
refactor: replace InjectSessionInstructions with plugin-bundled rule files
1 parent a2f5735 commit 6d89acb

27 files changed

Lines changed: 851 additions & 428 deletions

.claude/cat/issues/v2/v2.1/refactor-plugin-bundled-rules/PLAN.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,9 @@ None — architectural refactor
8282
- Extend `RulesDiscovery.getCatRulesForAudience()` to accept a list of source directories instead of one:
8383
- Files: `RulesDiscovery.java`
8484
- New signature: `getCatRulesForAudience(List<Path> rulesDirs, ...)` or overload that merges results
85-
- Discover rules from all directories, merge, then filter by audience
86-
- Deduplicate if same filename appears in both sources (project-local overrides plugin-bundled)
85+
- Discover rules from all directories, concatenate (plugin rules first, project-local second), then
86+
filter by audience
87+
- No filename-based deduplication — if the same filename exists in both sources, both are included
8788

8889
- Update `InjectRulesToMainAgent` to read from both sources:
8990
- Files: `InjectRulesToMainAgent.java`
@@ -100,7 +101,7 @@ None — architectural refactor
100101
- Add/update tests:
101102
- Files: test classes for `InjectRulesToMainAgent`, `InjectRulesToSubAgent`, `RulesDiscovery`
102103
- Test: plugin rules directory is read and content injected
103-
- Test: project rules and plugin rules are merged correctly
104+
- Test: plugin rules and project rules are concatenated in order (plugin first, project second)
104105
- Test: audience filtering works across both sources
105106
- Test: missing plugin rules directory is handled gracefully
106107
- Test: session ID is still injected (by existing EchoSessionId, not by plugin rules)
Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# State
22

3-
- **Status:** open
4-
- **Progress:** 0%
3+
- **Status:** closed
4+
- **Resolution:** implemented
5+
- **Progress:** 100%
56
- **Dependencies:** [refactor-rules-injection-classes]
67
- **Blocks:** []
8+
- **Target Branch:** v2.1

.claude/rules/license-header.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ The following files do not require license headers:
9494
- `*.xml` files (configuration files, no semantic code)
9595
- All `SKILL.md` files in plugin skills (`first-use.md` companions are NOT exempt and require headers)
9696
- All `*.md` files in `plugin/agents/` (injected into subagent context as prompts; same rationale as SKILL.md)
97+
- All `*.md` files in `plugin/rules/` (injected verbatim into agent context on every session; license headers waste context tokens)
9798
- Files in `.claude/cat/` (planning artifacts, config, runtime data)
9899
- `LICENSE.md` itself
99100
- Build artifacts (`target/`, `node_modules/`, etc.)

client/src/main/java/io/github/cowwoc/cat/hooks/SessionStartHook.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
import io.github.cowwoc.cat.hooks.session.InjectMainAgentRules;
1818
import io.github.cowwoc.cat.hooks.session.InjectCriticalThinking;
1919
import io.github.cowwoc.cat.hooks.session.InjectEnv;
20-
import io.github.cowwoc.cat.hooks.session.InjectSessionInstructions;
2120
import io.github.cowwoc.cat.hooks.session.InjectSkillListing;
2221
import io.github.cowwoc.cat.hooks.session.RestoreCwdAfterCompaction;
2322
import io.github.cowwoc.cat.hooks.session.RestoreWorktreeOnResume;
@@ -53,7 +52,6 @@ public SessionStartHook(JvmScope scope)
5352
new WarnUnknownTerminal(scope),
5453
new EchoSessionId(),
5554
new CheckRetrospectiveDue(scope),
56-
new InjectSessionInstructions(),
5755
new InjectMainAgentRules(scope),
5856
new InjectSkillListing(scope),
5957
new InjectCriticalThinking(),

client/src/main/java/io/github/cowwoc/cat/hooks/session/InjectMainAgentRules.java

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,13 @@
1616
import static io.github.cowwoc.requirements13.java.DefaultJavaValidators.requireThat;
1717

1818
/**
19-
* Injects audience-filtered rules from {@code .claude/cat/rules/} into main agent context.
19+
* Injects audience-filtered rules from plugin-bundled and project-local rule directories into main
20+
* agent context.
2021
* <p>
21-
* Discovers all rule files, filters to those with {@code mainAgent: true}, applies any {@code paths}
22-
* restrictions, and injects matching content as additional context.
22+
* Discovers all rule files from both {@code ${CLAUDE_PLUGIN_ROOT}/rules/} (plugin-bundled) and
23+
* {@code ${projectDir}/.claude/cat/rules/} (project-local), concatenates them (plugin-bundled
24+
* first, project-local second), filters to those with {@code mainAgent: true}, applies any
25+
* {@code paths} restrictions, and injects matching content as additional context.
2326
*/
2427
public final class InjectMainAgentRules implements SessionStartHandler
2528
{
@@ -39,6 +42,13 @@ public InjectMainAgentRules(JvmScope scope)
3942

4043
/**
4144
* Discovers and injects CAT rules applicable to the main agent.
45+
* <p>
46+
* Reads from two sources in order (plugin-bundled first, project-local second):
47+
* <ol>
48+
* <li>{@code ${CLAUDE_PLUGIN_ROOT}/rules/} — plugin-bundled rules</li>
49+
* <li>{@code ${projectDir}/.claude/cat/rules/} — project-local rules</li>
50+
* </ol>
51+
* Both sources are concatenated; no filename-based deduplication is performed.
4252
*
4353
* @param input the hook input
4454
* @return a result with the injected rules content, or empty if no rules apply
@@ -49,11 +59,12 @@ public Result handle(HookInput input)
4959
{
5060
requireThat(input, "input").isNotNull();
5161

52-
Path rulesDir = scope.getClaudeProjectDir().resolve(".claude/cat/rules");
62+
Path pluginRulesDir = scope.getClaudePluginRoot().resolve("rules");
63+
Path projectRulesDir = scope.getClaudeProjectDir().resolve(".claude/cat/rules");
5364
// Rules with paths: restrictions are injected dynamically by InjectPathRules (PreToolUse hook)
5465
// when matching files are accessed. Only non-paths rules are injected here at session start.
55-
String content = RulesDiscovery.getCatRulesForAudience(rulesDir, scope.getYamlMapper(),
56-
RulesDiscovery::filterForMainAgent, List.of());
66+
String content = RulesDiscovery.getCatRulesForAudience(List.of(pluginRulesDir, projectRulesDir),
67+
scope.getYamlMapper(), RulesDiscovery::filterForMainAgent, List.of());
5768
if (content.isBlank())
5869
return Result.empty();
5970

0 commit comments

Comments
 (0)