Skip to content

Commit 1b9106f

Browse files
committed
refactor: thin main() methods, add run() delegates, and MainTest for all non-hook CLI classes
1 parent 0fe4588 commit 1b9106f

97 files changed

Lines changed: 6309 additions & 951 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"status": "open"}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Plan
2+
3+
## Goal
4+
5+
Run instruction-builder on tee-piped-output skill and add enforcement tests to ensure piped bash commands use tee.
6+
7+
## Pre-conditions
8+
9+
(none)
10+
11+
## Post-conditions
12+
13+
- [ ] Instruction-builder has been run on the tee-piped-output skill, producing valid optimized instructions
14+
- [ ] Enforcement tests exist that verify piped bash commands without tee are detected as non-compliant
15+
- [ ] Enforcement tests verify that compliant piped commands (using tee) pass validation
16+
- [ ] All existing tests continue to pass (no regressions)
17+
- [ ] E2E verification: Run instruction-builder on tee-piped-output and confirm output is valid; run tests confirming non-compliant piped commands are flagged
Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,7 @@
1-
{"status": "open", "dependencies": [], "blocks": []}
1+
{
2+
"status" : "closed",
3+
"resolution" : "implemented",
4+
"dependencies" : [ ],
5+
"blocks" : [ ],
6+
"target_branch" : "v2.1"
7+
}

.cat/issues/v2/v2.1/thin-main-methods-and-add-run-tests/plan.md

Lines changed: 218 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -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`

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

Lines changed: 50 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,15 @@
66
*/
77
package io.github.cowwoc.cat.hooks;
88

9+
import static io.github.cowwoc.cat.hooks.Strings.block;
10+
import static io.github.cowwoc.requirements13.java.DefaultJavaValidators.requireThat;
11+
912
import tools.jackson.databind.JsonNode;
1013
import tools.jackson.databind.json.JsonMapper;
1114

1215
import java.io.IOException;
16+
import java.io.InputStream;
17+
import java.io.PrintStream;
1318
import java.nio.file.Files;
1419
import java.nio.file.NoSuchFileException;
1520
import java.nio.file.Path;
@@ -21,8 +26,6 @@
2126
import org.slf4j.Logger;
2227
import org.slf4j.LoggerFactory;
2328

24-
import static io.github.cowwoc.cat.hooks.Strings.block;
25-
2629
/**
2730
* enforce-status-output - Stop hook to enforce verbatim status box output.
2831
* <p>
@@ -62,27 +65,14 @@ public static void main(String[] args)
6265
{
6366
try (ClaudeHook scope = new MainClaudeHook())
6467
{
65-
JsonMapper mapper = scope.getJsonMapper();
6668
try
6769
{
68-
String output;
69-
try
70-
{
71-
String transcriptPath = scope.getString("transcript_path");
72-
boolean stopHookActive = scope.getBoolean("stop_hook_active", false);
73-
String sessionId = scope.getSessionId();
74-
Path sessionBasePath = scope.getClaudeSessionsPath();
75-
output = check(mapper, transcriptPath, stopHookActive, scope, sessionId, sessionBasePath);
76-
}
77-
catch (Exception e)
78-
{
79-
String errorMessage =
80-
"❌ Hook error: " + e.getMessage() + "\n" +
81-
"\n" +
82-
"Blocking as fail-safe. Please verify your working environment.";
83-
output = block(scope, errorMessage);
84-
}
85-
System.out.println(output);
70+
run(scope, args, System.in, System.out);
71+
}
72+
catch (IllegalArgumentException e)
73+
{
74+
System.out.println(block(scope,
75+
Objects.toString(e.getMessage(), e.getClass().getSimpleName())));
8676
}
8777
catch (RuntimeException | AssertionError e)
8878
{
@@ -94,6 +84,45 @@ public static void main(String[] args)
9484
}
9585
}
9686

87+
/**
88+
* Executes the status output enforcement check.
89+
*
90+
* @param scope the JVM scope (must be a {@link ClaudeHook} to access hook input fields)
91+
* @param args command line arguments (unused)
92+
* @param in the input stream (unused in current implementation)
93+
* @param out the output stream to write the hook decision to
94+
* @throws NullPointerException if any of {@code scope}, {@code args}, {@code in}, or {@code out} are null
95+
*/
96+
public static void run(JvmScope scope, String[] args, InputStream in, PrintStream out)
97+
{
98+
requireThat(scope, "scope").isNotNull();
99+
requireThat(args, "args").isNotNull();
100+
requireThat(in, "in").isNotNull();
101+
requireThat(out, "out").isNotNull();
102+
if (args.length > 0)
103+
throw new IllegalArgumentException("Unexpected arguments: " + String.join(" ", args));
104+
ClaudeHook hookScope = (ClaudeHook) scope;
105+
JsonMapper mapper = scope.getJsonMapper();
106+
String output;
107+
try
108+
{
109+
String transcriptPath = hookScope.getString("transcript_path");
110+
boolean stopHookActive = hookScope.getBoolean("stop_hook_active", false);
111+
String sessionId = hookScope.getSessionId();
112+
Path sessionBasePath = hookScope.getClaudeSessionsPath();
113+
output = check(mapper, transcriptPath, stopHookActive, hookScope, sessionId, sessionBasePath);
114+
}
115+
catch (Exception e)
116+
{
117+
String errorMessage =
118+
"❌ Hook error: " + e.getMessage() + "\n" +
119+
"\n" +
120+
"Blocking as fail-safe. Please verify your working environment.";
121+
output = block(scope, errorMessage);
122+
}
123+
out.println(output);
124+
}
125+
97126
/**
98127
* Checks the transcript and returns the hook decision, with pending-agent-result enforcement.
99128
* <p>

0 commit comments

Comments
 (0)