@@ -16,3 +16,202 @@ Clarify test_set_sha as SHA-256 of skill file content and require SPRT re-run on
1616- [ ] ` instruction-builder-agent/first-use.md ` updated: ` INSTRUCTION_DRAFT_SHA ` computed as ` sha256(skill_file) ` not a git commit SHA; compression/rewrite passes explicitly require full SPRT re-run
1717- [ ] ` batch-write-agent/test-results.json ` ` test_set_sha ` updated to actual SHA-256 of ` first-use.md `
1818- [ ] Tests passing, no regressions
19+
20+ ## Research Findings
21+
22+ ### Current Implementation
23+
24+ ** ` InstructionTestRunner.detectChanges() ` (lines 309–468)**
25+ - First argument ` old_skill_sha ` validated as ` [0-9a-f]{7,40} ` (git commit SHA format)
26+ - Retrieves old skill content via ` git rev-parse --show-toplevel ` + ` git show <sha>:<relpath> `
27+ - Writes old content to temp file, parses both old and new skill files
28+ - Compares frontmatter SHA-256 and body diff to produce ` frontmatter_changed ` , ` body_changed ` , ` changed_ranges `
29+ - Calls ` hasTransitiveDependencyChanged() ` which runs ` git diff --name-only <sha> -- <skillDir> `
30+ to detect changes to sibling ` .md ` files (companion files like ` first-use.md ` )
31+ - Returns JSON with fine-grained fields: ` skill_changed ` , ` frontmatter_changed ` , ` body_changed ` ,
32+ ` changed_ranges ` , ` all_test_case_ids ` , ` rerun_test_case_ids ` , ` carryforward_test_case_ids ` ,
33+ and ` requires_unit_mapping ` / ` semantic_units_path_hint `
34+
35+ ** ` sha256File() ` already exists** in ` InstructionTestRunner ` (line 1354): computes SHA-256 hex digest of a file.
36+
37+ ** ` instruction-builder-agent/first-use.md ` **
38+ - Line ~ 303: "Store the SHA as ` INSTRUCTION_DRAFT_SHA ` " (refers to git commit SHA from subagent result)
39+ - Lines ~ 576-580: Context derivation code sets ` INSTRUCTION_DRAFT_SHA="${DRAFT_COMMIT}" `
40+ - Line ~ 562-563: Invokes ` detect-changes <INSTRUCTION_DRAFT_SHA> <INSTRUCTION_TEXT_PATH> "${TEST_DIR}" `
41+ - Line ~ 629: Second ` detect-changes ` invocation with same arg pattern
42+ - Line ~ 1646: "Store the returned commit SHA as ` INSTRUCTION_DRAFT_SHA ` " (after improvement commit)
43+ - Line ~ 1928: Compression commits instruction file with ` refactor: accept final compression ... `
44+ but does NOT explicitly say to recompute ` INSTRUCTION_DRAFT_SHA `
45+
46+ ** ` batch-write-agent/test-results.json ` **
47+ - ` "test_set_sha": "b40012f59" ` — 9-char partial git commit SHA
48+ - SHA-256 of ` plugin/skills/batch-write-agent/first-use.md ` : ` 0babde5b6695c94703ce79903a36b3e32858ca30550ac025d3db88d7a0dc8fbd `
49+
50+ ### New Design
51+
52+ The new ` detectChanges() ` compares SHA-256 of the current skill file against the provided SHA-256 hash:
53+ - If hashes match → ` skill_changed=false ` , all test cases carry forward
54+ - If hashes differ → ` skill_changed=true ` , all test cases rerun
55+ - No git operations needed; ` hasTransitiveDependencyChanged() ` removed
56+ - Fine-grained fields (` frontmatter_changed ` , ` body_changed ` , ` changed_ranges ` , ` requires_unit_mapping ` ) removed
57+ - Simplified output: ` skill_changed ` , ` all_test_case_ids ` , ` rerun_test_case_ids ` , ` carryforward_test_case_ids `
58+
59+ ** Trade-off** : Companion file changes (e.g., ` compression-protocol.md ` ) are no longer automatically detected,
60+ since only the tracked file's SHA-256 is compared. This is acceptable per the issue scope.
61+
62+ ## Jobs
63+
64+ ### Job 1
65+
66+ TDD-first implementation in Java and plugin skill updates (all independent files).
67+
68+ - ** Write failing tests** for new ` detectChanges() ` behavior BEFORE modifying production code:
69+ In ` client/src/test/java/io/github/cowwoc/cat/hooks/test/InstructionTestRunnerTest.java ` :
70+ - Remove all 5 existing ` detectChanges* ` tests that use git repos and git commit SHAs:
71+ ` detectChangesNoChanges ` , ` detectChangesFrontmatterChanged ` , ` detectChangesBodyOnlyChanged ` ,
72+ ` detectChangesTransitiveDependencyChanged ` , ` detectChangesNoTransitiveDependencyChange `
73+ - Add new tests (write failing first, then they pass after production code change):
74+ - ` detectChanges_sha256Match_allCarriedForward() ` : Create temp skill file with content,
75+ compute its SHA-256, call ` detectChanges(sha256, path, testDir) ` → ` skill_changed=false ` ,
76+ all test case IDs in ` carryforward_test_case_ids ` , ` rerun_test_case_ids ` empty
77+ - ` detectChanges_sha256Mismatch_allRerun() ` : Create temp skill file, use a SHA-256 of
78+ different content (e.g., SHA-256 of empty string = ` e3b0c44... ` 64-char hex), call
79+ ` detectChanges(wrongSha256, path, testDir) ` → ` skill_changed=true ` ,
80+ all test case IDs in ` rerun_test_case_ids ` , ` carryforward_test_case_ids ` empty
81+ - ` detectChanges_invalidSha_shortString_throwsIllegalArgument() ` : Call with 9-char hex string
82+ (old git SHA format) → throws ` IllegalArgumentException ` with message containing "64"
83+ - ` detectChanges_invalidSha_notHex_throwsIllegalArgument() ` : Call with 64-char string
84+ containing non-hex chars → throws ` IllegalArgumentException `
85+ - Run build: ` mvn -f client/pom.xml verify -e ` — expect FAILURES (new tests fail, old tests deleted)
86+
87+ - ** Update ` InstructionTestRunner.detectChanges() ` method** in
88+ ` client/src/main/java/io/github/cowwoc/cat/hooks/skills/InstructionTestRunner.java ` :
89+ - Update Javadoc (lines 309-323): Change description from git-SHA-based comparison to
90+ SHA-256 content hash comparison. Remove mention of transitive dependencies, ` git show ` ,
91+ and "changed line ranges". New description:
92+ ```
93+ Compares the SHA-256 content hash of the current skill file against the provided hash,
94+ and partitions test cases into rerun vs carry-forward.
95+ @param args [old_skill_sha256, new_skill_path, test_dir_path]
96+ @throws IOException if files cannot be read
97+ ```
98+ - Change SHA validation (line 336) from `[0-9a-f]{7,40}` to `[0-9a-f]{64}` (exactly 64 chars).
99+ Update error message: `"invalid SHA-256 content hash format: '" + oldSha + "'. Expected 64 lowercase hex characters."`
100+ - Remove all git operations:
101+ - Lines 347-366: Remove `ProcessRunner.run(..., "git", "rev-parse", "--show-toplevel")` block
102+ - Lines 356-366: Remove relPath derivation
103+ - Lines 360-366: Remove `ProcessRunner.run(..., "git", "show", ...)` block
104+ - Replace old-content temp file logic (lines 368-468) with:
105+ ```java
106+ String currentSha = sha256File(newSkillPath);
107+ boolean skillChanged = !currentSha.equals(oldSha);
108+ List<String> allTestCaseIds = readAllTestCaseIds(testDirPath);
109+ JsonMapper mapper = scope.getJsonMapper();
110+ ObjectNode result = mapper.createObjectNode();
111+ result.put("skill_changed", skillChanged);
112+ ArrayNode allIdsArray = mapper.createArrayNode();
113+ for (String id : allTestCaseIds)
114+ allIdsArray.add(id);
115+ result.set("all_test_case_ids", allIdsArray);
116+ if (skillChanged)
117+ {
118+ result.set("rerun_test_case_ids", allIdsArray.deepCopy());
119+ result.set("carryforward_test_case_ids", mapper.createArrayNode());
120+ }
121+ else
122+ {
123+ result.set("rerun_test_case_ids", mapper.createArrayNode());
124+ result.set("carryforward_test_case_ids", allIdsArray.deepCopy());
125+ result.put("semantic_units_path_hint",
126+ "Run: skill-test-runner extract-units " + args[1]);
127+ }
128+ return compactJson(result);
129+ ```
130+ - Remove `repoRoot`, `relPath` variables and their usages
131+ - Remove `oldTempFile`, `newTempFile`, `oldBodyFile`, `newBodyFile` and their try-finally blocks
132+ - Remove `frontmatterChanged`, `bodyChanged`, `transitiveDependencyChanged`, `changedRanges` variables
133+ - Remove `hasTransitiveDependencyChanged()` call
134+
135+ - **Remove `hasTransitiveDependencyChanged()` method** (lines 485-508) from `InstructionTestRunner.java`
136+
137+ - **Update `plugin/skills/instruction-builder-agent/first-use.md`**:
138+ 1. Lines ~303-308: Change the paragraph that starts "The subagent returns `{"status": "success",
139+ "commit_sha": "<SHA>"}`. Store the SHA as `INSTRUCTION_DRAFT_SHA`" to:
140+ "The subagent returns `{"status": "success", "commit_sha": "<SHA>"}`. Compute `INSTRUCTION_DRAFT_SHA`
141+ as the SHA-256 content hash of `INSTRUCTION_TEXT_PATH`:
142+ ```bash
143+ INSTRUCTION_DRAFT_SHA=$(sha256sum "${INSTRUCTION_TEXT_PATH}" | awk '{print $1}')
144+ ```
145+ The instruction text is now on disk and committed, so subagents can read it via `cat <INSTRUCTION_TEXT_PATH>`."
146+ (Remove the `git show <SHA>:<INSTRUCTION_TEXT_PATH>` reference since git SHA is no longer tracked.)
147+ 2. Lines ~576-580: Retain the `DRAFT_COMMIT` and `INSTRUCTION_TEXT_PATH` derivation lines (still
148+ needed to locate the instruction file). Only change the last line:
149+ - Keep: `DRAFT_COMMIT=$(git log --oneline --all | grep "write instruction draft" | head -1 | awk '{print $1}')`
150+ - Keep: `INSTRUCTION_TEXT_PATH=$(git show "${DRAFT_COMMIT}" --name-only --format='' | grep -v '^$' | head -1)`
151+ - Change: `INSTRUCTION_DRAFT_SHA="${DRAFT_COMMIT}"` →
152+ `INSTRUCTION_DRAFT_SHA=$(sha256sum "${INSTRUCTION_TEXT_PATH}" | awk '{print $1}')`
153+ Update the comment on the last line from `# Compute INSTRUCTION_DRAFT_SHA` to
154+ `# Compute INSTRUCTION_DRAFT_SHA as SHA-256 of the current instruction file content`.
155+ 3. Line ~1646: Change "Store the returned commit SHA as `INSTRUCTION_DRAFT_SHA` before returning
156+ to Step 6." to "Compute `INSTRUCTION_DRAFT_SHA` as SHA-256 of the updated
157+ `INSTRUCTION_TEXT_PATH` and store it before returning to Step 6:
158+ ```bash
159+ INSTRUCTION_DRAFT_SHA=$(sha256sum "${INSTRUCTION_TEXT_PATH}" | awk '{print $1}')
160+ ```"
161+ Also update the continuation shortcut paragraph at lines ~1648-1653 that says
162+ "committed with new INSTRUCTION_DRAFT_SHA abc123" — change the example value
163+ `abc123` to a description of the new format, e.g.:
164+ "committed with new INSTRUCTION_DRAFT_SHA <64-char-sha256>"
165+ to clarify that `INSTRUCTION_DRAFT_SHA` is now a 64-character SHA-256 hex string, not a
166+ short git commit SHA.
167+ 4. Lines ~631-648 (the output fields list and test-case-selection table):
168+ - Update the `skill_changed` field description: change from "whether the skill or any of its
169+ transitive dependencies changed since the last SPRT run. Transitive dependencies are all
170+ `.md` files co-located with the skill file..." to "whether the skill file content changed —
171+ `true` when the SHA-256 of the current file differs from the provided hash."
172+ - Remove `prior_test_case_ids` from the output fields list (this field no longer exists).
173+ - Add `rerun_test_case_ids` and `carryforward_test_case_ids` to the output fields list:
174+ - `rerun_test_case_ids`: test case IDs that must re-run (all IDs when `skill_changed=true`,
175+ empty when `skill_changed=false`)
176+ - `carryforward_test_case_ids`: test case IDs that carry forward from prior results (all IDs
177+ when `skill_changed=false`, empty when `skill_changed=true`)
178+ - Update the three-row test-case-selection table (currently references `prior_test_case_ids`).
179+ Replace it with a two-row table:
180+ | Condition | Which test cases to run |
181+ |-----------|------------------------|
182+ | `skill_changed: true` | **All** test cases (full SPRT re-run). |
183+ | `skill_changed: false` | Carry all results forward. Skip re-test entirely. |
184+ (The "new test cases exist" row is removed because this detection required `prior_test_case_ids`
185+ which is no longer output by `detect-changes`.)
186+ 5. After compression commit (around line 1928, the `refactor: accept final compression` commit):
187+ Add a sentence: "Recompute `INSTRUCTION_DRAFT_SHA` as SHA-256 of the compressed file —
188+ any content change requires a fresh SHA:
189+ ```bash
190+ INSTRUCTION_DRAFT_SHA=$(sha256sum "${INSTRUCTION_TEXT_PATH}" | awk '{print $1}')
191+ ```
192+ This ensures the next `detect-changes` call correctly detects that the skill changed
193+ (full SPRT re-run required)."
194+
195+ - **Update `plugin/tests/skills/batch-write-agent/first-use/test-results.json`**:
196+ Change `"test_set_sha": "b40012f59"` to
197+ `"test_set_sha": "0babde5b6695c94703ce79903a36b3e32858ca30550ac025d3db88d7a0dc8fbd"`
198+ (SHA-256 of `plugin/skills/batch-write-agent/first-use.md`)
199+
200+ - **Run the full build** after all changes:
201+ ```bash
202+ mvn -f client/pom.xml verify -e
203+ ```
204+ All tests must pass. Fix any linter errors (Checkstyle, PMD) before committing.
205+
206+ - ** Commit** all changes (client Java + plugin skill + test data) in a single commit:
207+ ```
208+ bugfix: fix detect-changes to use SHA-256 content hash instead of git commit SHA
209+ ```
210+ (Commit type ` bugfix: ` because it fixes a bug where git SHA was used as a proxy for file
211+ identity, which could miss changes and used an incorrect format for test_set_sha.)
212+
213+ - ** Update index.json** : status=closed, progress=100%
214+
215+ ## Commit Type
216+
217+ ` bugfix: ` (Java client + plugin skill files + test data all relate to the same fix)
0 commit comments