@@ -23,37 +23,145 @@ There are 63 `main()` methods in the codebase split into three categories:
2323 ` BatchReader ` , ` GetDiffOutput ` , ` GitAmend ` , ` GitMergeLinear ` , ` GitRebase ` , ` GitSquash ` ,
2424 ` RecordLearning ` , ` WorkPrepare ` , ` WriteAndCommit `
2525
26- ** Needs work — CLI tools with ` run() ` but no ` *MainTest.java ` ** (6 classes):
27- ` GetFile ` , ` GetSkill ` , ` IssueLock ` , ` MergeAndCleanup ` , ` VerifyDeferPlanGeneration ` ,
28- ` WriteSessionMarker `
26+ ** Needs work — CLI tools with ` run() ` but no ` *MainTest.java ` ** (5 classes):
27+ ` AotTraining ` , ` HookRegistrar ` , ` IssueLock ` , ` VerifyDeferPlanGeneration ` , ` WriteSessionMarker `
2928
30- ** Needs work — CLI tools with logic in ` main() ` and no ` run() ` ** (~ 34 classes):
31- ` AotTraining ` , ` EmpiricalTestRunner ` , ` EnforceStatusOutput ` , ` ExistingWorkChecker ` ,
32- ` Feedback ` , ` GetAddOutput ` , ` GetCheckpointOutput ` , ` GetCleanupOutput ` , ` GetConfigOutput ` ,
29+ ** Needs work — CLI tools with logic in ` main() ` and no ` run() ` ** (34 classes):
30+
31+ * skills/ package (19 classes):*
32+ ` EmpiricalTestRunner ` , ` GetAddOutput ` , ` GetCheckpointOutput ` , ` GetCleanupOutput ` , ` GetConfigOutput ` ,
3333` GetIssueCompleteOutput ` , ` GetNextIssueOutput ` , ` GetOutput ` , ` GetRetrospectiveOutput ` ,
34- ` GetSkill ` , ` GetStakeholderConcernBox ` , ` GetStakeholderReviewBox ` ,
35- ` GetStakeholderSelectionBox ` , ` GetStatusOutput ` , ` GetStatuslineOutput ` ,
36- ` GetSubagentStatusOutput ` , ` GetTokenReportOutput ` , ` InvestigationContextExtractor ` ,
37- ` IssueCreator ` , ` MarkdownWrapper ` , ` ProgressBanner ` , ` RetrospectiveMigrator ` ,
38- ` RootCauseAnalyzer ` , ` SessionAnalyzer ` , ` StatusAlignmentValidator ` , ` StatuslineCommand ` ,
39- ` StatuslineInstall ` , ` TokenCounter ` , ` VerifyAudit `
34+ ` GetStakeholderConcernBox ` , ` GetStakeholderReviewBox ` , ` GetStakeholderSelectionBox ` ,
35+ ` GetStatusOutput ` , ` GetStatuslineOutput ` , ` GetSubagentStatusOutput ` , ` GetTokenReportOutput ` ,
36+ ` ProgressBanner ` , ` SkillTestRunner ` , ` VerifyAudit `
37+
38+ * util/ package (13 classes):*
39+ ` ExistingWorkChecker ` , ` Feedback ` , ` GetFile ` , ` InvestigationContextExtractor ` , ` IssueCreator ` ,
40+ ` MarkdownWrapper ` , ` MergeAndCleanup ` , ` RetrospectiveMigrator ` , ` RootCauseAnalyzer ` ,
41+ ` SessionAnalyzer ` , ` StatusAlignmentValidator ` , ` StatuslineCommand ` , ` StatuslineInstall `
42+
43+ * hooks/ package (2 classes):*
44+ ` EnforceStatusOutput ` , ` TokenCounter `
45+
46+ ** Excluded from this issue:**
47+ - ` GetSkill ` — covered by ` 2.1-fix-get-skill-uses-main-claude-hook `
48+ - ` HookRunner ` — infrastructure class that defines ` execute() ` , not a CLI tool
49+
50+ ## Research Findings
51+
52+ ### Scope Type Mapping
53+
54+ Each class's ` run() ` method must accept the correct scope type based on what ` main() ` currently creates:
55+
56+ | Scope in main() | run() accepts | Test scope |
57+ | ------------------| ---------------| ------------|
58+ | ` new MainClaudeTool() ` → ` ClaudeTool ` | ` JvmScope ` (wider interface, testable) | ` TestClaudeTool(tempDir, tempDir) ` |
59+ | ` new MainClaudeHook() ` → ` ClaudeHook ` | ` JvmScope ` (wider interface, testable) | ` TestClaudeTool(tempDir, tempDir) ` |
60+ | No scope (pure utility) | ` JvmScope ` | ` TestClaudeTool(tempDir, tempDir) ` |
61+
62+ All 34 classes needing run() extraction use ` MainClaudeTool ` except:
63+ - ` EnforceStatusOutput ` uses ` MainClaudeHook `
64+ - ` MarkdownWrapper ` and ` StatusAlignmentValidator ` use no scope (pure utilities)
65+
66+ ### Correct Patterns (from existing examples)
67+
68+ ** main() pattern (BatchReader.java lines 159-165):**
69+ ``` java
70+ public static void main(String [] args)
71+ {
72+ try (ClaudeTool scope = new MainClaudeTool ())
73+ {
74+ run(scope, args, System . out);
75+ }
76+ }
77+ ```
78+
79+ ** run() pattern (BatchReader.java line 178):**
80+ ``` java
81+ public static void run(JvmScope scope, String [] args, PrintStream out)
82+ {
83+ requireThat(args, " args" ). isNotNull();
84+ requireThat(out, " out" ). isNotNull();
85+ // All business logic here
86+ }
87+ ```
88+
89+ ** MainTest pattern (BatchReaderMainTest.java):**
90+ ``` java
91+ public class BatchReaderMainTest
92+ {
93+ @Test
94+ public void noArgsProducesBlockResponseWithUsage () throws IOException
95+ {
96+ Path tempDir = Files . createTempDirectory(" batch-reader-main-test-" );
97+ try (JvmScope scope = new TestClaudeTool (tempDir, tempDir))
98+ {
99+ ByteArrayOutputStream buffer = new ByteArrayOutputStream ();
100+ PrintStream out = new PrintStream (buffer, true , StandardCharsets . UTF_8 );
101+ BatchReader . run(scope, new String []{}, out);
102+ String output = buffer. toString(StandardCharsets . UTF_8 ). strip();
103+ // Verify output content
104+ }
105+ finally
106+ {
107+ TestUtils . deleteDirectoryRecursively(tempDir);
108+ }
109+ }
110+ }
111+ ```
112+
113+ ### Classes That Read from stdin
114+
115+ Some classes read from ` System.in ` in their ` main() ` . For these, ` run() ` must accept an ` InputStream ` parameter
116+ in addition to the standard ` (JvmScope, String[], PrintStream) ` :
117+ - ` EnforceStatusOutput ` — reads hook JSON from stdin
118+ - ` StatusAlignmentValidator ` — reads validation input from stdin
119+ - ` TokenCounter ` — reads file paths from stdin
120+
121+ For these, the signature is: ` public static void run(JvmScope scope, String[] args, InputStream in, PrintStream out) `
122+
123+ ### main() Error Handling Pattern
124+
125+ ` main() ` must follow the single-scope error handling pattern from ` .claude/rules/java.md ` :
126+ ``` java
127+ public static void main(String [] args)
128+ {
129+ try (ClaudeTool scope = new MainClaudeTool ())
130+ {
131+ try
132+ {
133+ run(scope, args, System . out);
134+ }
135+ catch (RuntimeException | AssertionError e)
136+ {
137+ Logger log = LoggerFactory . getLogger(ClassName . class);
138+ log. error(" Unexpected error" , e);
139+ System . out. println(new ClaudeHook (scope). block(
140+ Objects . toString(e. getMessage(), e. getClass(). getSimpleName())));
141+ }
142+ }
143+ }
144+ ```
145+
146+ For classes that currently catch ` IOException ` or ` IllegalArgumentException ` in ` main() ` , move those catches
147+ into ` run() ` (they become part of the business logic). ` main() ` only catches ` RuntimeException | AssertionError ` .
40148
41149## Pre-conditions
42150
43151- All 63 ` main() ` classes exist in ` client/src/main/java/ `
44152
45153## Post-conditions
46154
47- - [ ] Every non-hook CLI class has a ` public static void run(scope, String[] args, PrintStream out) `
155+ - [x ] Every non-hook CLI class has a ` public static void run(scope, String[] args, PrintStream out) `
48156 method (or equivalent signature matching the class's scope type)
49- - [ ] Every ` main() ` in a non-hook CLI class contains exactly: scope instantiation +
157+ - [x ] Every ` main() ` in a non-hook CLI class contains exactly: scope instantiation +
50158 ` run(scope, args, System.out) ` + unexpected-error catch block — no argument parsing,
51159 no business logic, no conditional branches
52- - [ ] Every non-hook CLI class has a ` *MainTest.java ` (or equivalent tests in its existing
160+ - [x ] Every non-hook CLI class has a ` *MainTest.java ` (or equivalent tests in its existing
53161 test file) that calls ` run() ` directly with:
54162 - Missing/invalid arguments → verifies error output and/or exception
55163 - Representative valid arguments → verifies output contains expected content
56- - [ ] ` mvn -f client/pom.xml verify -e ` passes
164+ - [x ] ` mvn -f client/pom.xml verify -e ` passes
57165
58166## TDD Approach
59167
@@ -72,3 +180,97 @@ For each class in the "needs work" categories:
72180 ` IllegalArgumentException ` ) belong in ` run() ` , not ` main() `
73181- ` main() ` catches only ` RuntimeException | AssertionError ` for unexpected errors
74182- ` GetSkill ` is covered by ` 2.1-fix-get-skill-uses-main-claude-hook ` — skip it here
183+
184+ ## Sub-Agent Waves
185+
186+ ### Wave 1
187+
188+ Extract ` run() ` and add ` *MainTest.java ` for all ** skills/ package** classes (19 classes) plus the
189+ 5 classes that already have ` run() ` but need ` *MainTest.java ` :
190+
191+ ** Need run() extraction + MainTest (skills/ package — all use ` MainClaudeTool ` , so run() accepts ` JvmScope ` ):**
192+
193+ For each of these 19 classes in ` client/src/main/java/io/github/cowwoc/cat/hooks/skills/ ` :
194+ 1 . ` EmpiricalTestRunner `
195+ 2 . ` GetAddOutput `
196+ 3 . ` GetCheckpointOutput `
197+ 4 . ` GetCleanupOutput `
198+ 5 . ` GetConfigOutput `
199+ 6 . ` GetIssueCompleteOutput `
200+ 7 . ` GetNextIssueOutput `
201+ 8 . ` GetOutput `
202+ 9 . ` GetRetrospectiveOutput `
203+ 10 . ` GetStakeholderConcernBox `
204+ 11 . ` GetStakeholderReviewBox `
205+ 12 . ` GetStakeholderSelectionBox `
206+ 13 . ` GetStatusOutput `
207+ 14 . ` GetStatuslineOutput `
208+ 15 . ` GetSubagentStatusOutput `
209+ 16 . ` GetTokenReportOutput `
210+ 17 . ` ProgressBanner `
211+ 18 . ` SkillTestRunner `
212+ 19 . ` VerifyAudit `
213+
214+ For each class above:
215+ 1 . Read the current ` main() ` method to understand its structure
216+ 2 . Create ` public static void run(JvmScope scope, String[] args, PrintStream out) ` containing all logic from ` main() `
217+ 3 . Add ` requireThat(args, "args").isNotNull(); requireThat(out, "out").isNotNull(); ` at the start of ` run() `
218+ 4 . Replace all ` System.out.println(...) ` / ` System.out.print(...) ` calls with ` out.println(...) ` / ` out.print(...) `
219+ 5 . Move any ` IOException ` /` IllegalArgumentException ` catch blocks from ` main() ` into ` run() `
220+ 6 . Thin ` main() ` to: ` try (ClaudeTool scope = new MainClaudeTool()) { try { run(scope, args, System.out); } catch (RuntimeException | AssertionError e) { ... } } `
221+ 7 . Create ` *MainTest.java ` in ` client/src/test/java/io/github/cowwoc/cat/hooks/test/ ` with at minimum:
222+ - ` noArgsProducesErrorOutput() ` — calls ` run(scope, new String[]{}, out) ` and verifies error/block output
223+ - Follow the ` BatchReaderMainTest ` pattern: ` TestClaudeTool ` , ` ByteArrayOutputStream ` , ` PrintStream ` , try-finally cleanup
224+
225+ ** Need MainTest only (already have run()):**
226+
227+ For each of these 5 classes, create ` *MainTest.java ` following the same pattern:
228+ 20 . ` AotTraining ` — in ` client/src/main/java/io/github/cowwoc/cat/hooks/ ` (uses ` MainClaudeHook ` , run() returns int)
229+ 21 . ` HookRegistrar ` — in ` client/src/main/java/io/github/cowwoc/cat/hooks/util/ `
230+ 22 . ` IssueLock ` — in ` client/src/main/java/io/github/cowwoc/cat/hooks/util/ `
231+ 23 . ` VerifyDeferPlanGeneration ` — in ` client/src/main/java/io/github/cowwoc/cat/hooks/util/ `
232+ 24 . ` WriteSessionMarker ` — in ` client/src/main/java/io/github/cowwoc/cat/hooks/util/ `
233+
234+ For each class above:
235+ 1 . Read the existing ` run() ` method signature
236+ 2 . Create ` *MainTest.java ` that calls ` run() ` with missing/invalid args and verifies error output
237+
238+ Commit: ` refactor: extract run() and add MainTest for skills/ package CLI classes `
239+
240+ ### Wave 2
241+
242+ Extract ` run() ` and add ` *MainTest.java ` for all ** util/ package** and ** hooks/ package** classes (15 classes):
243+
244+ ** Need run() extraction + MainTest (util/ package — all use ` MainClaudeTool ` except where noted):**
245+
246+ For each of these 13 classes in ` client/src/main/java/io/github/cowwoc/cat/hooks/util/ ` :
247+ 1 . ` ExistingWorkChecker `
248+ 2 . ` Feedback `
249+ 3 . ` GetFile `
250+ 4 . ` InvestigationContextExtractor `
251+ 5 . ` IssueCreator `
252+ 6 . ` MarkdownWrapper ` — ** no scope** (pure utility); add ` JvmScope ` parameter for consistency; ` run() ` also needs ` InputStream in ` since it reads from stdin
253+ 7 . ` MergeAndCleanup `
254+ 8 . ` RetrospectiveMigrator `
255+ 9 . ` RootCauseAnalyzer `
256+ 10 . ` SessionAnalyzer `
257+ 11 . ` StatusAlignmentValidator ` — ** no scope** (pure utility); add ` JvmScope ` parameter; ` run() ` needs ` InputStream in `
258+ 12 . ` StatuslineCommand `
259+ 13 . ` StatuslineInstall `
260+
261+ ** Need run() extraction + MainTest (hooks/ package):**
262+
263+ 14 . ` EnforceStatusOutput ` — uses ` MainClaudeHook ` ; ` run() ` needs ` InputStream in ` since it reads hook JSON from stdin
264+ 15 . ` TokenCounter ` — uses ` MainClaudeTool ` ; ` run() ` needs ` InputStream in ` since it reads file paths from stdin
265+
266+ Apply the same transformation pattern as Wave 1. For classes that read from ` System.in ` , the signature is:
267+ ` public static void run(JvmScope scope, String[] args, InputStream in, PrintStream out) `
268+
269+ After all classes are updated, run the full test suite:
270+ ``` bash
271+ mvn -f client/pom.xml verify -e
272+ ```
273+
274+ Update ` index.json ` : set status to ` closed ` , progress to 100%.
275+
276+ Commit: ` refactor: extract run() and add MainTest for util/ and hooks/ package CLI classes `
0 commit comments