1---2name: ase-code-dissect3description: Dissect the current Git change set, treated as an epic, domain-wise and logically into cohesive parts and materialize each part in its own dedicated Git WorkTree. Use when the user calls to "dissect", "split", "break up", or "decompose" a large change set into atomic, separately committable parts.4---5
6@${CLAUDE_SKILL_DIR}/../../meta/ase-control.md
7@${CLAUDE_SKILL_DIR}/../../meta/ase-skill.md
8@${CLAUDE_SKILL_DIR}/../../meta/ase-getopt.md
9
10<purpose name="ase-code-dissect">
11Dissect a Change Set
12</purpose>
13
14<expand name="getopt"
15 arg1="ase-code-dissect"
16 arg2="--max-parts|-m=8 --staged|-s --dry|-d --force|-f">
17 $ARGUMENTS
18</expand>
19
20<objective>
21*Dissect* the current Git change set, treated as an *epic*, domain-wise
22and logically into *cohesive parts*, and materialize every part in its
23own dedicated *Git WorkTree*, so each part can be reviewed and
24committed *atomically* and *independently*.
25</objective>
26
27@${CLAUDE_SKILL_DIR}/../../meta/ase-common-dissect.md
28
29Procedure
30---------
31
32<flow>
33
341. <step id="STEP 1: Determine Change Set and Hint">
35
36 1. Determine the *dissection hint*: set
37 <dissect-hint><getopt-arguments/></dissect-hint>, with any
38 leading and trailing whitespace stripped. Additionally, inherit
39 the always existing <ase-project-id/> from the current context,
40 as it names the worktrees and branches of all derived parts.
41
42 <if condition="<dissect-hint/> is not empty">
43 Only output the following <template/>:
44
45 <template>
46 ⧉ **ASE**: ✪ skill: **ase-code-dissect**, ⇌ hint: **<dissect-hint/>**
47 </template>
48 </if>
49 <else>
50 No dissection hint was given, so the parts are derived from the
51 change set alone. Do not output anything.
52 </else>
53
54 2. Determine the *repository root* by running the corresponding
55 command (taken exactly as given) and capturing its output into
56 <repo-root/>:
57
58 `git rev-parse --show-toplevel`
59
60 3. Determine the *diff details* and the *diff statistics* by
61 running the corresponding commands (taken exactly as given) and
62 capturing their full outputs into <diff/> and <stat/>:
63
64 <if condition="<getopt-option-staged/> is equal `true`">
65 `git diff --cached HEAD`
66
67 `git diff --cached --numstat HEAD`
68 </if>
69 <else>
70 `git diff`
71
72 `git diff --numstat`
73 </else>
74
75 4. <if condition="<getopt-option-staged/> is not equal `true`">
76 Additionally, *fold in the untracked files* -- they are part of
77 the working copy change set, but carry no diff of their own.
78 Determine them *read-only* by running the corresponding command
79 (taken exactly as given):
80
81 `git -C "<repo-root/>" ls-files --others --exclude-standard`
82
83 *Skip* every listed entry below the `.ase/` directory -- it
84 carries *ASE*'s own state and the worktrees created by this very
85 skill, and hence is never part of the user's change set.
86
87 Then, for *every* remaining listed file, capture its creation
88 diff by running the corresponding command (taken exactly as
89 given) and *append* its output to <diff/>:
90
91 `git -C "<repo-root/>" diff --no-index --binary /dev/null "<file/>"`
92
93 This command intentionally exits with a non-zero status,
94 because the two compared paths differ; treat this exit status
95 as *success*, not as an error. Judge the *outcome* by the
96 *output* instead: a run which emits *no* diff on standard output
97 but an `error:` or `fatal:` message (e.g. the entry is a *nested*
98 Git repository, which `git ls-files` reports as a directory) is a
99 *real* failure -- append nothing for that entry and only output
100 the following <template/>, then continue with the next file:
101
102 <template>
103 ⧉ **ASE**: ✪ skill: **ase-code-dissect**, ⊘ untracked: `<file/>`, ▶ status: **not foldable into the change set**
104 </template>
105 </if>
106 <else>
107 Untracked files are *not* folded in under `--staged`/`-s`,
108 because they are by definition *not* part of the Git index. Do
109 not output anything.
110 </else>
111
112 5. <if condition="<diff/> is empty">
113 Only output the following <template/> and then *STOP* immediately:
114
115 <template>
116 ⧉ **ASE**: ✪ skill: **ase-code-dissect**, ▶ status: **no changes to dissect**
117 </template>
118 </if>
119
120 </step>
121
1222. <step id="STEP 2: Dissect Change Set">
123
124 1. *Derive the parts* of the epic:
125
126 <expand
127 name="dissect-derive"
128 arg1="ase-code-dissect"
129 arg2="<dissect-hint/>"
130 arg3="<ase-project-id/>"
131 >
132 the individual hunks of the captured <diff/>, weighted by the
133 line counts of <stat/> and -- for the folded-in untracked files,
134 which carry no <stat/> entry -- by their own diff line counts
135 </expand>
136
137 Additionally, try to keep *all* hunks of *one* file in the
138 *same* part, and split a file's hunks across parts *only* when
139 they are genuinely unrelated -- this keeps the per-part patches
140 applicable.
141
142 A *single* hunk is *atomic* here, so rule 3's splitting
143 permission does *not* apply to it: you *MUST NOT* break a hunk
144 into sub-hunks, because this would require re-computing its `@@`
145 header and hence destroy the byte-exactness the per-part
146 <patch/> depends on.
147
148 2. *Report the parts*:
149
150 <expand name="dissect-report" arg1="<ase-project-id/>"></expand>
151
152 </step>
153
1543. <step id="STEP 3: Detect Target Collisions"
155 condition="<getopt-option-dry/> is not equal `true`">
156
157 1. Determine the *existing worktrees* and *existing branches* by
158 running the corresponding commands (taken exactly as given) and
159 capturing their outputs:
160
161 `git worktree list --porcelain`
162
163 `git branch --list`
164
165 2. Set <collisions/> to all <part-id/> of <parts/> for which either
166 a worktree directory `<repo-root/>/.ase/worktree/<part-id/>` or
167 a branch `<part-id/>` already exists.
168
169 3. <if condition="<collisions/> is not empty AND <getopt-option-force/> is not equal `true`">
170 Only output the following <template/> -- with <collisions/>
171 rendered as a comma-separated list of code spans -- and then
172 immediately *STOP* processing the entire current skill, leaving
173 *all* existing worktrees and branches untouched:
174
175 <template>
176 ⧉ **ASE**: ✪ skill: **ase-code-dissect**, ⊘ collisions: <collisions/>
177 ⧉ **ASE**: ✪ skill: **ase-code-dissect**, ✪ dissection: **<n/>** parts, ▶ status: **targets exist**
178 </template>
179
180 Directly *after* this <template/>, and *before* stopping, give
181 the corrective hint by expanding the following (which, depending
182 on the configured <ase-guidance-level/>, may expand into nothing
183 and hence emit no output at all):
184
185 <ase-tpl-hint level="minimal">
186 Re-run `/ase-code-dissect --force` to remove and re-create the colliding worktrees and branches.
187 </ase-tpl-hint>
188 </if>
189
190 4. <if condition="<collisions/> is not empty AND <getopt-option-force/> is equal `true`">
191 *Remove* every colliding target by running the corresponding
192 commands (taken exactly as given) per colliding <part-id/>, and
193 silently ignore the failure of an individual command when the
194 corresponding worktree or branch does not exist:
195
196 `git worktree remove --force "<repo-root/>/.ase/worktree/<part-id/>"`
197
198 `git worktree prune`
199
200 `git branch -D <part-id/>`
201 </if>
202
203 </step>
204
2054. <step id="STEP 4: Materialize WorkTrees"
206 condition="<getopt-option-dry/> is not equal `true`">
207
208 You *MUST* *NEVER* mutate the *original* working copy in this step:
209 do *not* modify, stage, stash, revert, or commit anything outside of
210 the freshly created worktrees.
211
212 1. Set <tmp-dir/> to the value of the `TMPDIR` environment variable
213 if it is known in the current session context, and to `/tmp`
214 otherwise. Do not output anything.
215
216 2. For *every* part in <parts/>, in their derived order:
217
218 1. Assemble <patch/> from the *verbatim* subset of <diff/>
219 assigned to this part: keep the complete `diff --git` file
220 headers and the complete `@@` hunk headers of the assigned
221 hunks, and change *nothing* inside them.
222
223 The captured <diff/> is *byte-exact* input and <patch/> stays
224 *byte-exact*, too, because `git apply` rejects even the
225 smallest deviation. You *MUST* therefore *NEVER* re-wrap a
226 line, re-indent a line, normalize or strip trailing
227 whitespace, drop the leading context/`+`/`-` marker column,
228 re-encode a character, or omit the trailing newline -- and
229 you *MUST* *NEVER* re-render any part of the diff as
230 Markdown, so no bullet marker and no inline code span is ever
231 introduced into <patch/>.
232
233 2. Use the `Write` tool to write <patch/> to the patch file
234 `<tmp-dir/>/ase-dissect-<part-id/>.patch`.
235
236 3. Create the worktree by running the corresponding command
237 (taken exactly as given), which creates the directory
238 *and* -- named after its last path component -- the branch
239 <part-id/> from `HEAD`. The `.ase` directory is usually
240 git-ignored, so the worktree itself never shows up as a
241 change:
242
243 `git worktree add "<repo-root/>/.ase/worktree/<part-id/>"`
244
245 <if condition="this command fails">
246 Only output the following <template/>, then *continue* with
247 the *next* part -- a single failing part *never* aborts the
248 remaining ones:
249
250 <template>
251 ⧉ **ASE**: ✪ skill: **ase-code-dissect**, ◉ part: **<part-id/>**, ▶ status: **worktree failed to create**
252 </template>
253 </if>
254
255 4. Apply the patch *inside* the freshly created worktree by
256 running the corresponding command (taken exactly as given):
257
258 `git -C "<repo-root/>/.ase/worktree/<part-id/>" apply --whitespace=nowarn "<tmp-dir/>/ase-dissect-<part-id/>.patch"`
259
260 <if condition="this command fails">
261 Only output the following <template/>, then *continue* with
262 the *next* part -- a single failing part *never* aborts the
263 remaining ones:
264
265 <template>
266 ⧉ **ASE**: ✪ skill: **ase-code-dissect**, ◉ part: **<part-id/>**, ▶ status: **patch failed to apply**
267 </template>
268 </if>
269
270 5. <if condition="a `CHANGELOG.md` file exists in the created worktree">
271 Add *one* new entry to the *first* (most recent) section of
272 that `CHANGELOG.md` *inside the worktree*, summarizing
273 *this part's* change set only, and strictly aligned with the
274 established style and conventions of the project (usually
275 `- <change-type/> [<artifact-kind/>]: <summary/>`).
276
277 The *existing* `CHANGELOG.md` is *changed*, never replaced:
278 you *MUST* use the `Read` tool to read it and the `Edit` tool
279 to insert the single new entry *in place*, and you *MUST*
280 *NEVER* use the `Write` tool on it, as this would drop the
281 entire remaining change history.
282
283 <if condition="the patch of this part already added an entry to that `CHANGELOG.md`">
284 The part's own change set already carries its `CHANGELOG.md`
285 entry, so you *MUST* *NOT* add a second one. Keep the entry
286 which came with the patch and, if it mentions changes which
287 landed in *other* parts, reduce it to *this* part's change
288 set only.
289 </if>
290 </if>
291 <else>
292 The project keeps no `CHANGELOG.md`, so nothing is added and
293 no `CHANGELOG.md` is created. Do not output anything.
294 </else>
295
296 6. Leave the worktree *uncommitted*: do *not* run `git add` and
297 do *not* run `git commit`, so the user keeps full control
298 over the final commit of every part.
299
300 7. Only output the following <template/>:
301
302 <template>
303 ⧉ **ASE**: ✪ skill: **ase-code-dissect**, ◉ part: **<part-id/>**, ▶ status: **worktree created**
304 </template>
305
306 3. *Clean up* the temporary patch files by running the
307 corresponding command (taken exactly as given) once per
308 <part-id/> of the *successfully* materialized <parts/>, and
309 silently ignore the failure of an individual command when the
310 corresponding patch file does not exist:
311
312 `rm -f "<tmp-dir/>/ase-dissect-<part-id/>.patch"`
313
314 The patch file of a *successful* part is a pure *intermediate*:
315 it was already consumed by `git apply` and its content is fully
316 preserved in the worktree, so it is removed. The patch file of a
317 part whose worktree or patch *failed* is *kept* instead, because
318 it is that part's only materialization and would otherwise be
319 lost. Do not output anything.
320
321 </step>
322
3235. <step id="STEP 5: Report Result">
324
325 1. <if condition="<getopt-option-dry/> is equal `true`">
326 Only output the following <template/>:
327
328 <template>
329 ⧉ **ASE**: ✪ skill: **ase-code-dissect**, ✪ dissection: **<n/>** parts, ▶ status: **dry-run -- no worktrees created**
330 </template>
331 </if>
332 <else>
333 Only output the following <template/>:
334
335 <template>
336 ⧉ **ASE**: ✪ skill: **ase-code-dissect**, ✪ dissection: **<n/>** parts, ▶ status: **change set dissected**
337 </template>
338 </else>
339
340 2. Finally, give the closing hints by expanding the following
341 (which, depending on the configured <ase-guidance-level/>, may
342 each expand into nothing and hence emit no output at all):
343
344 <if condition="<getopt-option-dry/> is not equal `true`">
345 <ase-tpl-hint level="minimal">
346 The parts are uncommitted in `.ase/worktree/<id>` -- review and commit each of them separately, then remove them via `git worktree remove`.
347 </ase-tpl-hint>
348 </if>
349 <else>
350 <ase-tpl-hint level="minimal">
351 Re-run `/ase-code-dissect` without `--dry` to actually create the reported worktrees, optionally with a `<dissect-hint>` argument if the reported split is not the intended one.
352 </ase-tpl-hint>
353 </else>
354
355 <ase-tpl-hint level="normal">
356 Use `/ase-meta-diff` and `/ase-meta-review` inside a part's worktree to summarize and review it before committing.
357 </ase-tpl-hint>
358
359 <ase-tpl-hint level="verbose">
360 Use `/ase-code-dissect --staged` to dissect the staged changes only, `--max-parts <count>` to bound the number of parts, `--force` to re-create already existing worktrees, and a `<dissect-hint>` argument to steer the split.
361 </ase-tpl-hint>
362
363 </step>
364
365</flow>