wavework 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/LICENSE +21 -0
  3. package/README.md +144 -2
  4. package/bin/wavework.mjs +7 -0
  5. package/examples/shipir.wavework.json +54 -0
  6. package/package.json +35 -4
  7. package/src/cli/args.mjs +28 -0
  8. package/src/cli/dashboard.mjs +41 -0
  9. package/src/cli/diff-stat.mjs +41 -0
  10. package/src/cli/hooks.mjs +109 -0
  11. package/src/cli/init.mjs +104 -0
  12. package/src/cli/safe-path.mjs +63 -0
  13. package/src/cli/sync.mjs +302 -0
  14. package/src/cli/update.mjs +24 -0
  15. package/src/cli.mjs +78 -0
  16. package/src/config.mjs +313 -0
  17. package/src/context.mjs +133 -0
  18. package/src/render.mjs +148 -0
  19. package/src/targets.mjs +100 -0
  20. package/templates/agents/implementer.md +16 -0
  21. package/templates/dashboard/README.md +96 -0
  22. package/templates/dashboard/hooks/session-start.mjs +305 -0
  23. package/templates/dashboard/hooks/subagent-stop.mjs +129 -0
  24. package/templates/dashboard/lib/ci.mjs +52 -0
  25. package/templates/dashboard/lib/derive.mjs +105 -0
  26. package/templates/dashboard/lib/settings.mjs +34 -0
  27. package/templates/dashboard/lib/tokens.mjs +21 -0
  28. package/templates/dashboard/pricing.json +11 -0
  29. package/templates/dashboard/public/app.css +134 -0
  30. package/templates/dashboard/public/app.js +260 -0
  31. package/templates/dashboard/public/default-theme.css +86 -0
  32. package/templates/dashboard/public/format.mjs +138 -0
  33. package/templates/dashboard/public/index.html +20 -0
  34. package/templates/dashboard/record.mjs +144 -0
  35. package/templates/dashboard/server.mjs +254 -0
  36. package/templates/dashboard-config.json +1 -0
  37. package/templates/scripts/red-green-check.sh +287 -0
  38. package/templates/scripts/red-green-check.test.sh +151 -0
  39. package/templates/settings/hooks.json +31 -0
  40. package/templates/skills/next-task/SKILL.md +77 -0
  41. package/templates/skills/next-task-batch/SKILL.md +27 -0
  42. package/templates/skills/next-task-beads/SKILL.md +29 -0
  43. package/templates/skills/next-task-branch/SKILL.md +19 -0
  44. package/templates/skills/next-task-code/SKILL.md +22 -0
  45. package/templates/skills/next-task-deps/SKILL.md +31 -0
  46. package/templates/skills/next-task-e2e/SKILL.md +35 -0
  47. package/templates/skills/next-task-findings/SKILL.md +65 -0
  48. package/templates/skills/next-task-finish/SKILL.md +43 -0
  49. package/templates/skills/next-task-implement/SKILL.md +34 -0
  50. package/templates/skills/next-task-plan/SKILL.md +52 -0
  51. package/templates/skills/next-task-review/SKILL.md +61 -0
  52. package/templates/skills/next-task-test/SKILL.md +30 -0
  53. package/templates/skills/next-task-write-tests/SKILL.md +34 -0
  54. package/templates/skills/text-to-task/SKILL.md +78 -0
  55. package/templates/skills/watch-ci/SKILL.md +42 -0
@@ -0,0 +1,287 @@
1
+ #!/usr/bin/env bash
2
+ # Runs a child's new/changed tests against the pre-change code and classifies every test:
3
+ # RED failed on old code (good: the test proves something)
4
+ # RED-COMPILE old code does not compile / import against the test (accepted)
5
+ # GREEN-ON-OLD passed on old code (suspicious: the test may prove nothing)
6
+ # UNCHANGED passed on old code, but the child did not touch this test (ignore)
7
+ # SKIP <area>: unsupported the area's test layout is not understood (see below); judge its tests from the diff
8
+ # Usage: red-green-check.sh <worktree> [<base-ref>] (base-ref defaults to HEAD)
9
+ # Exit: 0 no GREEN-ON-OLD (or no changed tests), 1 any GREEN-ON-OLD, 2 tool/setup error.
10
+ # Writes only into a throwaway detached worktree in a temp dir (removed on exit). Tests in a changed file that
11
+ # were not themselves changed are classified too: judge those by the diff.
12
+ #
13
+ # Areas come from the checks of .claude/wavework.json (generated by `wavework init`/`update`, do not edit):
14
+ # one line 'name|kind|dir'. Understood kinds:
15
+ # maven *Test.java / *Tests.java / *IT.java under <dir>/src/test, run with ./mvnw (or mvn) test -Dtest=...
16
+ # vitest *.test.ts / *.test.tsx / *.test.js / *.test.jsx under <dir>, run with pnpm (pnpm-lock.yaml) or npm (package-lock.json)
17
+ # Any other area is `unsupported`: its tests are not run and the script prints SKIP.
18
+ # Needs bash, git and node. Testing seam: RED_GREEN_AREAS (same line format, one per line) replaces the table.
19
+ set -o pipefail
20
+
21
+ AREAS=(
22
+ {{redGreenAreas}}
23
+ )
24
+ if [[ -n "${RED_GREEN_AREAS:-}" ]]; then
25
+ AREAS=()
26
+ while IFS= read -r line; do [[ -n "$line" ]] && AREAS+=("$line"); done <<< "$RED_GREEN_AREAS"
27
+ fi
28
+
29
+ die() { echo "red-green-check: $*" >&2; exit 2; }
30
+
31
+ [[ $# -ge 1 && $# -le 2 ]] || die "usage: $0 <worktree> [<base-ref>]"
32
+ WT="$(cd "$1" 2>/dev/null && pwd)" || die "worktree not found: $1"
33
+ BASE="${2:-HEAD}"
34
+ git -C "$WT" rev-parse --git-dir >/dev/null 2>&1 || die "not a git worktree: $WT"
35
+ git -C "$WT" rev-parse --verify --quiet "$BASE^{commit}" >/dev/null || die "unknown base ref: $BASE"
36
+ command -v node >/dev/null || die "node is required"
37
+
38
+ TMP="$(mktemp -d)" || die "cannot create temp dir"
39
+ OLD="$TMP/old"
40
+ cleanup() {
41
+ git -C "$WT" worktree remove --force "$OLD" >/dev/null 2>&1 || true
42
+ git -C "$WT" worktree prune >/dev/null 2>&1 || true
43
+ rm -rf "$TMP"
44
+ }
45
+ trap cleanup EXIT
46
+
47
+ # Per area i: $TMP/test.i (changed test files), $TMP/support.i (changed helpers), $TMP/seen.i (any changed file).
48
+ AREA_NAME=() AREA_KIND=() AREA_DIR=()
49
+ for i in "${!AREAS[@]}"; do
50
+ IFS="|" read -r "AREA_NAME[$i]" "AREA_KIND[$i]" "AREA_DIR[$i]" <<< "${AREAS[$i]}"
51
+ : > "$TMP/test.$i"; : > "$TMP/support.$i"; : > "$TMP/seen.$i"
52
+ done
53
+
54
+ # Area of a path: the one with the longest matching dir ("." matches everything). Prints the index or nothing.
55
+ area_of() {
56
+ local f="$1" best="" bestlen=-1 i d
57
+ for i in "${!AREA_DIR[@]}"; do
58
+ d="${AREA_DIR[$i]}"
59
+ if [[ "$d" == "." ]]; then
60
+ [[ 0 -gt $bestlen ]] && { best=$i; bestlen=0; }
61
+ elif [[ "$f" == "$d"/* && ${#d} -gt $bestlen ]]; then
62
+ best=$i; bestlen=${#d}
63
+ fi
64
+ done
65
+ echo "$best"
66
+ }
67
+
68
+ # Changed files (added/copied/modified/renamed, tracked + untracked), NUL separated so names with spaces work.
69
+ while IFS= read -r -d '' f; do
70
+ i="$(area_of "$f")"
71
+ [[ -n "$i" ]] || continue
72
+ rel="$f"; [[ "${AREA_DIR[$i]}" == "." ]] || rel="${f#"${AREA_DIR[$i]}"/}"
73
+ echo "$f" >> "$TMP/seen.$i"
74
+ case "${AREA_KIND[$i]}" in
75
+ maven)
76
+ case "$rel" in
77
+ src/test/*.java) case "$rel" in *Test.java|*Tests.java|*IT.java) echo "$f" >> "$TMP/test.$i" ;; *) echo "$f" >> "$TMP/support.$i" ;; esac ;;
78
+ src/test/*) echo "$f" >> "$TMP/support.$i" ;;
79
+ esac ;;
80
+ vitest)
81
+ case "$rel" in
82
+ *.test.ts|*.test.tsx|*.test.js|*.test.jsx) echo "$f" >> "$TMP/test.$i" ;;
83
+ src/test/*) echo "$f" >> "$TMP/support.$i" ;;
84
+ esac ;;
85
+ esac
86
+ done < <(
87
+ { git -C "$WT" diff --name-only -z --diff-filter=ACMR "$BASE" --; git -C "$WT" ls-files -z --others --exclude-standard; } | sort -zu
88
+ )
89
+
90
+ ANY=0
91
+ for i in "${!AREA_NAME[@]}"; do
92
+ if [[ "${AREA_KIND[$i]}" != maven && "${AREA_KIND[$i]}" != vitest ]]; then
93
+ [[ -s "$TMP/seen.$i" ]] && echo "SKIP ${AREA_NAME[$i]}: unsupported (no test discovery for this area; judge its tests from the diff)"
94
+ elif [[ -s "$TMP/test.$i" ]]; then
95
+ ANY=1
96
+ fi
97
+ done
98
+ if [[ $ANY -eq 0 ]]; then
99
+ echo "no tests changed"
100
+ exit 0
101
+ fi
102
+
103
+ git -C "$WT" worktree add --detach "$OLD" "$BASE" >/dev/null 2>&1 || die "cannot create worktree at $BASE"
104
+
105
+ for i in "${!AREA_NAME[@]}"; do
106
+ while IFS= read -r f; do
107
+ mkdir -p "$OLD/$(dirname "$f")" && cp -p "$WT/$f" "$OLD/$f" || die "cannot copy $f"
108
+ done < <(cat "$TMP/test.$i" "$TMP/support.$i")
109
+ done
110
+
111
+ RESULTS="$TMP/results.tsv" # status<TAB>path<TAB>title<TAB>display
112
+ : > "$RESULTS"
113
+
114
+ # Results parsers (node, no dependencies). Argument: results file, area name, then the inputs.
115
+ parse_surefire() {
116
+ node - "$@" <<'JS'
117
+ const fs = require('fs');
118
+ const [results, area, ...files] = process.argv.slice(2);
119
+ const decode = (v) => v
120
+ .replace(/&#x([0-9a-fA-F]+);/g, (m, h) => String.fromCodePoint(parseInt(h, 16)))
121
+ .replace(/&#(\d+);/g, (m, d) => String.fromCodePoint(Number(d)))
122
+ .replace(/&quot;/g, '"').replace(/&apos;/g, "'").replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&amp;/g, '&');
123
+ const attr = (head, k) => {
124
+ const m = head.match(new RegExp('\\s' + k + '="([^"]*)"'));
125
+ return m ? decode(m[1]) : '';
126
+ };
127
+ let out = '';
128
+ for (const file of files) {
129
+ const xml = fs.readFileSync(file, 'utf8');
130
+ for (let part of xml.split(/<testcase\b/).slice(1)) {
131
+ part = part.split('</testsuite>')[0];
132
+ const head = part.slice(0, part.indexOf('>'));
133
+ // match result elements only outside CDATA and system-out/system-err (a test may print "<failure>")
134
+ const body = part.replace(/<!\[CDATA\[[\s\S]*?\]\]>/g, '').replace(/<system-(out|err)\b[\s\S]*?<\/system-\1>/g, '');
135
+ if (/<skipped\b/.test(body)) continue;
136
+ const bad = /<(failure|error)\b/.test(body);
137
+ const classname = attr(head, 'classname');
138
+ const name = attr(head, 'name');
139
+ const cls = classname.split('$')[0].split('.').pop();
140
+ out += [bad ? 'RED' : 'GREEN-ON-OLD', cls + '.java', name.split('(')[0], area + ' ' + classname + '.' + name].join('\t') + '\n';
141
+ }
142
+ }
143
+ fs.appendFileSync(results, out);
144
+ JS
145
+ }
146
+
147
+ parse_vitest() {
148
+ node - "$@" <<'JS'
149
+ const fs = require('fs');
150
+ const [results, area, dir, json, ...rels] = process.argv.slice(2);
151
+ const prefix = dir === '.' ? '' : dir + '/';
152
+ let out = '';
153
+ for (const suite of JSON.parse(fs.readFileSync(json, 'utf8')).testResults || []) {
154
+ const name = suite.name || '?';
155
+ const rel = rels.find((r) => name === r || name.endsWith('/' + r)) || name;
156
+ const tests = suite.assertionResults || [];
157
+ if (tests.length === 0) {
158
+ if (suite.status === 'failed') out += ['RED-COMPILE', prefix + rel, '', area + ' ' + rel + ' (suite failed to load on old code)'].join('\t') + '\n';
159
+ continue;
160
+ }
161
+ for (const t of tests) {
162
+ if (['pending', 'skipped', 'todo'].includes(t.status)) continue;
163
+ out += [t.status === 'failed' ? 'RED' : 'GREEN-ON-OLD', prefix + rel, t.title || '', area + ' ' + rel + ' > ' + (t.fullName || '?')].join('\t') + '\n';
164
+ }
165
+ }
166
+ fs.appendFileSync(results, out);
167
+ JS
168
+ }
169
+
170
+ run_maven() { # index
171
+ local i="$1" dir="${AREA_DIR[$1]}" classes=() f
172
+ while IFS= read -r f; do f="${f##*/}"; classes+=("${f%.java}"); done < "$TMP/test.$i"
173
+ local list; list="$(IFS=,; echo "${classes[*]}")"
174
+ local log="$TMP/maven.$i.log" mvn_cmd=(mvn)
175
+ [[ -x "$OLD/$dir/mvnw" ]] && mvn_cmd=(./mvnw)
176
+ ( cd "$OLD/$dir" && "${mvn_cmd[@]}" test "-Dtest=$list" -Dsurefire.failIfNoSpecifiedTests=false ) >"$log" 2>&1
177
+ local rc=$?
178
+ local reports=( "$OLD/$dir"/target/surefire-reports/TEST-*.xml )
179
+ if [[ -e "${reports[0]}" ]]; then
180
+ parse_surefire "$RESULTS" "${AREA_NAME[$i]}" "${reports[@]}" || return 2
181
+ return 0
182
+ fi
183
+ if grep -q "COMPILATION ERROR" "$log"; then
184
+ local c
185
+ for c in "${classes[@]}"; do printf 'RED-COMPILE\t%s.java\t\t%s %s (old code does not compile)\n' "$c" "${AREA_NAME[$i]}" "$c" >> "$RESULTS"; done
186
+ return 0
187
+ fi
188
+ echo "${AREA_NAME[$i]}: run produced no reports (exit $rc); tail of output:" >&2
189
+ tail -n 30 "$log" >&2
190
+ return 2
191
+ }
192
+
193
+ run_vitest() { # index
194
+ local i="$1" dir="${AREA_DIR[$1]}" rel=() f prefix=""
195
+ [[ "$dir" == "." ]] || prefix="$dir/"
196
+ while IFS= read -r f; do rel+=("${f#"$prefix"}"); done < "$TMP/test.$i"
197
+ local log="$TMP/vitest.$i.log" json="$TMP/vitest.$i.json" install runner
198
+ if [[ -f "$OLD/$dir/pnpm-lock.yaml" ]]; then
199
+ install=(pnpm install --frozen-lockfile --prefer-offline); runner=(pnpm vitest)
200
+ elif [[ -f "$OLD/$dir/package-lock.json" ]]; then
201
+ install=(npm ci); runner=(npx vitest)
202
+ else
203
+ echo "SKIP ${AREA_NAME[$i]}: unsupported (no pnpm-lock.yaml or package-lock.json in $dir; judge its tests from the diff)"
204
+ return 0
205
+ fi
206
+ ( cd "$OLD/$dir" && "${install[@]}" ) >"$log" 2>&1 || { echo "${AREA_NAME[$i]}: install failed; tail:" >&2; tail -n 30 "$log" >&2; return 2; }
207
+ ( cd "$OLD/$dir" && "${runner[@]}" run "${rel[@]}" --reporter=json "--outputFile=$json" ) >>"$log" 2>&1
208
+ if [[ ! -s "$json" ]]; then
209
+ echo "${AREA_NAME[$i]}: vitest produced no JSON report; tail:" >&2; tail -n 30 "$log" >&2; return 2
210
+ fi
211
+ parse_vitest "$RESULTS" "${AREA_NAME[$i]}" "$dir" "$json" "${rel[@]}" || return 2
212
+ return 0
213
+ }
214
+
215
+ STATUS=0
216
+ for i in "${!AREA_NAME[@]}"; do
217
+ [[ -s "$TMP/test.$i" ]] || continue
218
+ case "${AREA_KIND[$i]}" in
219
+ maven) run_maven "$i" || STATUS=2 ;;
220
+ vitest) run_vitest "$i" || STATUS=2 ;;
221
+ esac
222
+ done
223
+ [[ $STATUS -eq 2 ]] && die "could not run the tests on the old code (not a pass)"
224
+
225
+ cat "$TMP"/test.* > "$TMP/testfiles"
226
+ echo "red-green-check: base=$BASE"
227
+ # A GREEN test only counts as GREEN-ON-OLD when the child touched it (changed lines per file from git; every
228
+ # line of an untracked or new file); untouched tests in a changed file show as UNCHANGED.
229
+ node - "$WT" "$BASE" "$RESULTS" "$TMP/testfiles" <<'JS'
230
+ const fs = require('fs');
231
+ const path = require('path');
232
+ const { spawnSync } = require('child_process');
233
+ const [wt, base, results, testfiles] = process.argv.slice(2);
234
+ const git = (...args) => spawnSync('git', ['-C', wt, ...args], { encoding: 'utf8' });
235
+ const touched = new Map();
236
+ for (const f of fs.readFileSync(testfiles, 'utf8').split('\n').filter(Boolean)) {
237
+ const lines = new Set();
238
+ if (git('ls-files', '--error-unmatch', '--', f).status === 0 && git('cat-file', '-e', base + ':' + f).status === 0) {
239
+ for (const l of git('diff', '-U0', base, '--', f).stdout.split('\n')) {
240
+ const m = l.match(/^@@ -\S+ \+(\d+)(?:,(\d+))? @@/);
241
+ if (!m) continue;
242
+ const a = Number(m[1]);
243
+ const n = m[2] === undefined ? 1 : Number(m[2]);
244
+ for (let i = a; i < a + n; i++) lines.add(i);
245
+ }
246
+ } else {
247
+ const count = fs.readFileSync(path.join(wt, f), 'utf8').split('\n').length;
248
+ for (let i = 1; i <= count; i++) lines.add(i);
249
+ }
250
+ touched.set(f, lines);
251
+ }
252
+ const starts = /^\s*(@Test|@ParameterizedTest|@RepeatedTest|it\(|it\.each|test\(|it\.skip|test\.each)/;
253
+ function segmentTouched(file, title) {
254
+ const key = [...touched.keys()].find((k) => k === file || k.endsWith('/' + file));
255
+ if (!title || key === undefined) return true;
256
+ let lines;
257
+ try { lines = fs.readFileSync(path.join(wt, key), 'utf8').split('\n'); } catch (e) { return true; }
258
+ const idx = [];
259
+ lines.forEach((l, i) => { if (starts.test(l)) idx.push(i + 1); });
260
+ idx.push(lines.length + 1);
261
+ for (let k = 0; k + 1 < idx.length; k++) {
262
+ const a = idx[k];
263
+ const b = idx[k + 1];
264
+ if (lines.slice(a - 1, b - 1).some((l) => l.includes(title))) {
265
+ return [...touched.get(key)].some((n) => n >= a && n < b);
266
+ }
267
+ }
268
+ return true; // title not found: stay conservative
269
+ }
270
+ const rows = [];
271
+ let green = 0;
272
+ for (const l of fs.readFileSync(results, 'utf8').split('\n').filter(Boolean)) {
273
+ const [status, file, title, disp] = l.split('\t');
274
+ let st = status;
275
+ if (st === 'GREEN-ON-OLD') {
276
+ if (!segmentTouched(file, title)) st = 'UNCHANGED';
277
+ else green++;
278
+ }
279
+ rows.push([st, disp]);
280
+ }
281
+ const order = { 'GREEN-ON-OLD': 0, 'RED-COMPILE': 1, 'RED': 2, 'UNCHANGED': 3 };
282
+ rows.sort((x, y) => order[x[0]] - order[y[0]] || (x[1] < y[1] ? -1 : x[1] > y[1] ? 1 : 0));
283
+ for (const [st, disp] of rows) console.log(st.padEnd(13) + ' ' + disp);
284
+ console.log('RESULT: ' + (green ? 'GREEN-ON-OLD tests found (suspicious)' : 'all changed tests are red on old code'));
285
+ process.exit(green ? 1 : 0);
286
+ JS
287
+ exit $?
@@ -0,0 +1,151 @@
1
+ #!/usr/bin/env bash
2
+ # Shell test for red-green-check.sh with a throwaway git repo and stubbed mvnw/pnpm.
3
+ # Needs only bash, git and node: no Maven, pnpm, Docker, database or network. It sets its own area table
4
+ # (RED_GREEN_AREAS), so it tests the generic parts: argument parsing, test discovery, RED / GREEN-ON-OLD /
5
+ # RED-COMPILE classification and the SKIP of unsupported areas. Usage: .claude/scripts/red-green-check.test.sh
6
+ set -euo pipefail
7
+
8
+ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
9
+ SCRIPT="$HERE/red-green-check.sh"
10
+ TMP="$(mktemp -d)"
11
+ export RED_GREEN_AREAS="backend|maven|backend
12
+ frontend|vitest|frontend
13
+ svc|unsupported|svc"
14
+ trap 'rm -rf "$TMP"' EXIT
15
+ FAILS=0
16
+ check() { # description, condition result (0 = ok)
17
+ if [[ "$2" -eq 0 ]]; then echo "ok - $1"; else echo "FAIL - $1"; FAILS=$((FAILS + 1)); fi
18
+ }
19
+
20
+ # Stub mvnw: a test class "*RedTest" fails unless backend/src/main/feature.txt exists, "*GreenTest" always passes.
21
+ # BREAK file -> compilation error; FAIL_TOOL file -> exit 1 with no report.
22
+ make_repo() {
23
+ local r="$1"
24
+ mkdir -p "$r/backend/src/main" "$r/backend/src/test/java/x" "$r/frontend/src" "$r/svc" "$r/bin"
25
+ echo lock > "$r/frontend/pnpm-lock.yaml"
26
+ cat > "$r/backend/mvnw" <<'STUB'
27
+ #!/usr/bin/env bash
28
+ root="$(cd "$(dirname "$0")/.." && pwd)"
29
+ [[ -e "$root/backend/src/main/FAIL_TOOL" ]] && { echo "Docker not running"; exit 1; }
30
+ [[ -e "$root/backend/src/main/BREAK" ]] && { echo "[ERROR] COMPILATION ERROR :"; exit 1; }
31
+ for a in "$@"; do [[ "$a" == -Dtest=* ]] && list="${a#-Dtest=}"; done
32
+ mkdir -p "$root/backend/target/surefire-reports"
33
+ echo '<testsuite>' > "$root/backend/target/surefire-reports/TEST-all.xml"
34
+ IFS=, read -ra cls <<< "$list"
35
+ for c in "${cls[@]}"; do
36
+ if [[ "$c" == *NoiseTest ]]; then
37
+ # realistic surefire output: properties block, passing test with entities in its name, noise in system-out/err
38
+ echo "<properties><property name=\"x\" value=\"<error>\"/></properties>"
39
+ echo "<testcase classname=\"x.$c\" name=\"it&apos;s &#233; &#x41;\" time=\"0.1\"><system-out><![CDATA[<failure> noise <error>]]></system-out><system-err>&lt;skipped&gt; <failure></system-err></testcase>"
40
+ elif [[ "$c" == *RedTest && ! -e "$root/backend/src/main/feature.txt" ]]; then
41
+ echo "<testcase classname=\"x.$c\" name=\"works\"><failure message=\"boom\"/></testcase>"
42
+ else
43
+ echo "<testcase classname=\"x.$c\" name=\"works\"/>"
44
+ fi
45
+ done >> "$root/backend/target/surefire-reports/TEST-all.xml"
46
+ echo '</testsuite>' >> "$root/backend/target/surefire-reports/TEST-all.xml"
47
+ STUB
48
+ chmod +x "$r/backend/mvnw"
49
+ # pnpm stub (put on PATH): install is a no-op; vitest writes JSON, failing "red space.test.ts" when feature.txt is absent.
50
+ cat > "$r/bin/pnpm" <<'STUB'
51
+ #!/usr/bin/env bash
52
+ [[ "$1" == install ]] && exit 0
53
+ out=""; files=()
54
+ for a in "$@"; do
55
+ case "$a" in --outputFile=*) out="${a#--outputFile=}" ;; vitest|run|--reporter=json) ;; *) files+=("$a") ;; esac
56
+ done
57
+ root="$(cd ../ && pwd)"
58
+ printf '{"testResults":[' > "$out"; sep=""
59
+ for f in "${files[@]}"; do
60
+ st=passed
61
+ [[ "$f" == *red* && ! -e "$root/backend/src/main/feature.txt" ]] && st=failed
62
+ printf '%s{"name":"%s/frontend/%s","assertionResults":[{"fullName":"does it","status":"%s"}]}' "$sep" "$root" "$f" "$st" >> "$out"; sep=","
63
+ done
64
+ printf ']}' >> "$out"
65
+ STUB
66
+ chmod +x "$r/bin/pnpm"
67
+ ( cd "$r" && git init -q && git config user.email t@t && git config user.name t \
68
+ && echo base > README && git add -A -f && git commit -qm base )
69
+ }
70
+
71
+ run() { ( cd "$1" && PATH="$1/bin:$PATH" "$SCRIPT" "$1" "${@:2}" ) ; }
72
+
73
+ # 1. failing on old code -> RED, exit 0 (backend + frontend, file name with spaces)
74
+ R1="$TMP/r1"; make_repo "$R1"
75
+ echo 'class T {}' > "$R1/backend/src/test/java/x/FooRedTest.java"
76
+ echo 'x' > "$R1/frontend/src/red space.test.ts"
77
+ touch "$R1/backend/src/main/feature.txt" # the "implementation", present only in the working tree
78
+ set +e; OUT="$(run "$R1" 2>&1)"; RC=$?; set -e
79
+ check "red tests exit 0" $([[ $RC -eq 0 ]]; echo $?)
80
+ check "backend RED reported" $([[ "$OUT" == *"RED "*"backend x.FooRedTest.works"* ]]; echo $?)
81
+ check "frontend RED with spaces in name" $([[ "$OUT" == *"RED "*"red space.test.ts"* ]]; echo $?)
82
+
83
+ # 2. passing on old code -> GREEN-ON-OLD, exit 1
84
+ R2="$TMP/r2"; make_repo "$R2"
85
+ echo 'class T {}' > "$R2/backend/src/test/java/x/BarGreenTest.java"
86
+ set +e; OUT="$(run "$R2" 2>&1)"; RC=$?; set -e
87
+ check "green-on-old exits 1" $([[ $RC -eq 1 ]]; echo $?)
88
+ check "GREEN-ON-OLD reported" $([[ "$OUT" == *"GREEN-ON-OLD"*"BarGreenTest"* ]]; echo $?)
89
+
90
+ # 3. old code does not compile -> RED-COMPILE, exit 0 (BREAK is committed in base)
91
+ R3="$TMP/r3"; make_repo "$R3"
92
+ touch "$R3/backend/src/main/BREAK"; ( cd "$R3" && git add -f -A && git commit -qm brk )
93
+ echo 'class T {}' > "$R3/backend/src/test/java/x/NewGreenTest.java"
94
+ set +e; OUT="$(run "$R3" 2>&1)"; RC=$?; set -e
95
+ check "red-compile exits 0" $([[ $RC -eq 0 ]]; echo $?)
96
+ check "RED-COMPILE reported" $([[ "$OUT" == *"RED-COMPILE"* ]]; echo $?)
97
+
98
+ # 4. no changed tests -> exit 0
99
+ R4="$TMP/r4"; make_repo "$R4"; echo change >> "$R4/README"
100
+ set +e; OUT="$(run "$R4" 2>&1)"; RC=$?; set -e
101
+ check "no tests changed exits 0" $([[ $RC -eq 0 && "$OUT" == *"no tests changed"* ]]; echo $?)
102
+
103
+ # 5. tool failure -> exit 2
104
+ R5="$TMP/r5"; make_repo "$R5"
105
+ touch "$R5/backend/src/main/FAIL_TOOL"; ( cd "$R5" && git add -f -A && git commit -qm tool )
106
+ echo 'class T {}' > "$R5/backend/src/test/java/x/AnyGreenTest.java"
107
+ set +e; OUT="$(run "$R5" 2>&1)"; RC=$?; set -e
108
+ check "tool failure exits 2" $([[ $RC -eq 2 ]]; echo $?)
109
+
110
+ # 6. worktree untouched and temp worktree removed
111
+ BEFORE="$(git -C "$R2" status --porcelain)"
112
+ set +e; run "$R2" >/dev/null 2>&1; set -e
113
+ check "git status unchanged" $([[ "$BEFORE" == "$(git -C "$R2" status --porcelain)" ]]; echo $?)
114
+ check "no leftover worktrees" $([[ "$(git -C "$R2" worktree list | wc -l | tr -d ' ')" == 1 ]]; echo $?)
115
+
116
+ # 7. frontend test that passes on old code -> GREEN-ON-OLD, exit 1 (stub passes anything not named *red*)
117
+ R7="$TMP/r7"; make_repo "$R7"
118
+ echo 'x' > "$R7/frontend/src/ok.test.ts"
119
+ set +e; OUT="$(run "$R7" 2>&1)"; RC=$?; set -e
120
+ check "frontend green-on-old exits 1" $([[ $RC -eq 1 && "$OUT" == *"GREEN-ON-OLD"*"frontend src/ok.test.ts"* ]]; echo $?)
121
+
122
+ # 8. changed files of an unsupported area are skipped with a clear message, not run and not a failure
123
+ R8="$TMP/r8"; make_repo "$R8"
124
+ echo 'def test_x(): pass' > "$R8/svc/test_x.py"
125
+ set +e; OUT="$(run "$R8" 2>&1)"; RC=$?; set -e
126
+ check "unsupported area: SKIP, exit 0" $([[ $RC -eq 0 && "$OUT" == *"SKIP svc: unsupported"* && "$OUT" == *"no tests changed"* ]]; echo $?)
127
+
128
+ # 9. a supported area alongside an unsupported one: tests run, SKIP is still reported
129
+ R9="$TMP/r9"; make_repo "$R9"
130
+ echo 'def test_x(): pass' > "$R9/svc/test_x.py"
131
+ echo 'class T {}' > "$R9/backend/src/test/java/x/BazGreenTest.java"
132
+ set +e; OUT="$(run "$R9" 2>&1)"; RC=$?; set -e
133
+ check "mixed areas: SKIP plus GREEN-ON-OLD" $([[ $RC -eq 1 && "$OUT" == *"SKIP svc"* && "$OUT" == *"GREEN-ON-OLD"*"BazGreenTest"* ]]; echo $?)
134
+
135
+ # 10. helper files (not tests) alone do not count as changed tests
136
+ R10="$TMP/r10"; make_repo "$R10"
137
+ echo 'class H {}' > "$R10/backend/src/test/java/x/Helper.java"
138
+ set +e; OUT="$(run "$R10" 2>&1)"; RC=$?; set -e
139
+ check "support file only: no tests changed" $([[ $RC -eq 0 && "$OUT" == *"no tests changed"* ]]; echo $?)
140
+
141
+ # 10b. noise in <system-out>/CDATA and entities in names: passing test stays GREEN-ON-OLD, name is decoded
142
+ R10B="$TMP/r10b"; make_repo "$R10B"
143
+ echo 'class T {}' > "$R10B/backend/src/test/java/x/QuietNoiseTest.java"
144
+ set +e; OUT="$(run "$R10B" 2>&1)"; RC=$?; set -e
145
+ check "system-out noise is not a failure" $([[ $RC -eq 1 && "$OUT" == *"GREEN-ON-OLD"*"it's é A"* && "$OUT" != *"RED "* ]]; echo $?)
146
+
147
+ # 11. argument errors exit 2
148
+ set +e; "$SCRIPT" >/dev/null 2>&1; RC1=$?; "$SCRIPT" "$R4" no-such-ref >/dev/null 2>&1; RC2=$?; "$SCRIPT" "$TMP/missing" >/dev/null 2>&1; RC3=$?; set -e
149
+ check "usage, unknown ref and missing worktree exit 2" $([[ $RC1 -eq 2 && $RC2 -eq 2 && $RC3 -eq 2 ]]; echo $?)
150
+
151
+ [[ $FAILS -eq 0 ]] && echo "all passed" || { echo "$FAILS failed"; exit 1; }
@@ -0,0 +1,31 @@
1
+ {
2
+ "hooks": {},
3
+ "features": {
4
+ "dashboard": {
5
+ "hooks": {
6
+ "SessionStart": [
7
+ {
8
+ "hooks": [
9
+ {
10
+ "type": "command",
11
+ "command": "node \"$CLAUDE_PROJECT_DIR/.claude/wavework/dashboard/hooks/session-start.mjs\"",
12
+ "timeout": 5
13
+ }
14
+ ]
15
+ }
16
+ ],
17
+ "SubagentStop": [
18
+ {
19
+ "hooks": [
20
+ {
21
+ "type": "command",
22
+ "command": "node \"$CLAUDE_PROJECT_DIR/.claude/wavework/dashboard/hooks/subagent-stop.mjs\"",
23
+ "timeout": 10
24
+ }
25
+ ]
26
+ }
27
+ ]
28
+ }
29
+ }
30
+ }
31
+ }
@@ -0,0 +1,77 @@
1
+ ---
2
+ name: next-task
3
+ description: Implement a whole beads epic end to end. Takes an epic ID, creates a branch, prepares dependencies, plans the work, works in its own git worktree, hands each child task to an implementer sub-agent (tests first, implementation, e2e, focused test runs), records out-of-scope findings as beads, then closes the beads, updates docs, pushes to {{baseBranch}} and removes the worktree and branch. Use when the user says "next-task <epic-id>" or asks to implement an epic.
4
+ ---
5
+
6
+ # next-task
7
+
8
+ Orchestrates one epic from branch to push. You coordinate; the implementer sub-agents write the code. **Model check first:** before any other step or tool call, check the session model. If it isn't {{orchestrationModelName}} (Model Policy in CLAUDE.md), stop, say so and suggest `/model {{models.orchestration}}`. Don't create branches or claim beads first.
9
+
10
+ **Input:** an epic ID, optionally with the flag `--watch-ci` (`$ARGUMENTS`). Remove the flag, then take the rest as the epic ID. If the ID is missing, or `bd show <id>` says it isn't an epic, stop and ask for one, whatever flags were passed. A whole-branch review always runs before the push (step 7). Never pick an epic yourself.
11
+
12
+ **By invoking this skill the user authorizes** for this epic: creating and deleting its branch and worktree, committing, pushing the branch to `{{baseBranch}}` (fast-forward only) and filing `finding` beads (under the standing findings epics, see `next-task-findings`). Nothing else: never force-push, never rewrite pushed history{{#if noAttribution}}, never add attribution lines (see CLAUDE.md){{/if}}.
13
+
14
+ ## Steps
15
+
16
+ Each step lives in its own skill under `.claude/skills/`. Read that skill's `SKILL.md` and follow it. Stop and report to the user whenever a step says to stop. Don't skip ahead.
17
+
18
+ {{#if dashboard}}
19
+ **Recording:** `Record:` lines call `node .claude/wavework/dashboard/record.mjs <event> --run <RUN> ...` from `<WT>` (record from `<WT>` while it exists; record the end of the run before the worktree is removed). `<RUN>` = the branch name with `/` replaced by `.` (e.g. `epic.shipr-1hxo-pipeline-dashboard`). Fire-and-forget: it always exits 0, never wait on or react to it. Fields: `.claude/wavework/dashboard/README.md`.
20
+
21
+ Record: every stop in steps 1–7 (failed bead check or deps, failed plan, failed verification after the retry, review round 3 still failing, blocked child): `run-end --status blocked`.
22
+
23
+ {{/if}}
24
+ 1. **Branch:** `next-task-branch` with prefix `epic`. Creates `{{epicBranch}}` from `origin/{{baseBranch}}` in its own worktree `<WT>`. Every later step, including yours, runs only in `<WT>` (`cd <WT> && ...`, absolute paths under it); never switch branches in the main checkout.
25
+ {{#if dashboard}}
26
+ Record: `run-start --kind next-task --units <epic-id> --branch <branch> --worktree <WT>` (under `next-task-batch` add `--parent <BATCH-RUN>`).
27
+ {{/if}}
28
+ 2. **Check beads:** `next-task-plan`, part 1. Validates the epic and its children.
29
+ {{#if dashboard}}
30
+ Record: `step-start --step plan`.
31
+ {{/if}}
32
+ 3. **Dependencies:** `next-task-deps`. Installs packages and applies safe updates as their own commit.
33
+ {{#if dashboard}}
34
+ Record: `step-start --step deps` before, `step-end --step deps` after.
35
+ {{/if}}
36
+ 4. **Plan implementation:** `next-task-plan`, part 2. Orders the children into waves, with the model and file set for each.
37
+ {{#if dashboard}}
38
+ Record: `step-end --step plan`.
39
+ {{/if}}
40
+ 5. **Hand off to sub-agents:** for each wave, start one implementer per child. Use {{implementerAgentsOr}} according to the child's `model:` label. Children in the same wave run in parallel; waves run one after another. Give each agent the prompt template from `next-task-plan`, with the worktree path.{{#if dashboard}} Record: `step-start --step wave --wave <n>` per wave and `step-end --step wave --wave <n>` when it is done; `child-start --bead <child> --model <{{implementerModelPipe}}> --wave <n>` when starting a child.{{/if}} Each sub-agent then follows `next-task-implement`:
41
+ 1. write tests (`next-task-write-tests`)
42
+ 2. implement (`next-task-code`)
43
+ 3. e2e tests (`next-task-e2e`)
44
+ 4. run the focused checks of its area (`next-task-test`)
45
+ 5. report back as finished, with out-of-scope findings
46
+ 6. **After each sub-agent reports:** verify the result yourself before accepting it:
47
+ - `cd <WT> && git branch --show-current` still shows the epic branch, and the changed files are in `<WT>`, not in the main checkout
48
+ - re-run `next-task-test` for its area
49
+ - the report has a **Red before green** section (failing test name + assertion line, or an explicit allowed exception); if missing, treat it as a review must-fix and send it back to the implementer
50
+ - **Red-green check:** after re-running `next-task-test` and before the review, run `.claude/scripts/red-green-check.sh <WT>` (it runs the child's new/changed tests against the pre-change code in a throwaway worktree; tests that need external services need them running). Exit 0 = ok, 1 = some test is `GREEN-ON-OLD`, 2 = tool/setup error (fix the cause and rerun; never treat as a pass). Pass its output to the reviewer. Skip only for a stated exception (pure refactor/docs/config), where it prints "no tests changed" anyway
51
+ - check the diff against the acceptance criteria and the CLAUDE.md rules (architecture, security{{#if uiSkill}}, and `{{uiSkill.name}}` for UI under {{uiPaths}}{{/if}})
52
+ - **Review:** run `next-task-review` (fresh read-only reviewer sub-agent). Must-fix findings go back to the same implementer (SendMessage), then re-verify and review again; at most 3 review rounds per child. Out-of-scope findings go to `next-task-findings`. If round 3 still has must-fix findings, stop that child uncommitted and hand off to the user with the open findings
53
+ - after a review PASS, commit only that child's files: `git commit -m "<child title> (<child-id>)"`
54
+ - **Findings:** run `next-task-findings` for the agent's reported findings and whatever your own review of the diff turned up outside the child's scope
55
+ {{#if dashboard}}
56
+ - Record: `child-end --bead <child> --result <committed|blocked> --redBeforeGreen <yes|exception>` after the commit, the BLOCKED handling or a review round 3 that still fails; `finding --bead <finding-id>` per filed finding
57
+ {{/if}}
58
+
59
+ If **verification** fails or is incomplete (not review findings, which follow the review loop above), send the agent the concrete findings once (SendMessage). If it still isn't right, stop and report.
60
+
61
+ **If the agent reports BLOCKED** (retry cap in `next-task-implement`): `bd update <child> --status blocked --append-notes "<reason + failing output tail>"`. Leave its changes uncommitted, keep worktree and branch, and don't start any child that depends on it, directly or transitively. Continue the independent children. Don't start the next wave until every child in the current wave is committed or blocked.
62
+ 7. **When all sub-agents are finished:** if any child is blocked, don't run `next-task-finish`: stop and report the blocked children, skipped dependents and the worktree path. Rerunning `/next-task <epic-id>` resumes in that worktree (`next-task-branch`), picks up blocked and open children, and finishes then.{{#if dashboard}} Record the stop with `run-end --status blocked`.{{/if}} Otherwise `next-task-finish` (it includes the branch review). Rebase onto `origin/{{baseBranch}}`, final full test run, remaining findings, close the beads, write the documentation, push to `{{baseBranch}}`, then remove the worktree and branch.
63
+ 8. **Watch CI (optional):** only after step 7 has pushed `{{baseBranch}}` successfully.
64
+ - With `--watch-ci`: run the `watch-ci` skill for `{{baseBranch}}` (branch argument `{{baseBranch}}`) right away.
65
+ - Without it: ask once with AskUserQuestion, "Skip" as the first and default option, and "Watch CI" second. On "Skip", don't watch.
66
+ - A CI failure never reopens beads, reverts or re-merges anything. Report it; `watch-ci` then offers its bug bead. Don't fix or re-run anything here.
67
+
68
+ ## Report
69
+
70
+ At the end, keep it short:
71
+ - the epic and each child with its commit hash
72
+ - the test results
73
+ - anything deferred, and the `finding` beads filed (ID, priority, title)
74
+ - the push result
75
+ - the CI result from `watch-ci` (run conclusions and URLs), or "CI not watched"
76
+
77
+ If the run stopped early, say which step stopped it, why, and what state the branch and the worktree (path) are in.
@@ -0,0 +1,27 @@
1
+ ---
2
+ name: next-task-batch
3
+ description: Implement several beads epics in one run. Orders them by their dependencies, then runs the full /next-task pipeline for each epic one after another (each in its own worktree, each pushed to {{baseBranch}} before the next starts), and watches CI once at the end. Use when the user says "next-task-batch <epic-id> <epic-id> ..." or asks to implement multiple epics.
4
+ ---
5
+
6
+ # next-task-batch
7
+
8
+ A loop over `next-task`. **Model check first:** before any other step or tool call, check the session model. If it isn't {{orchestrationModelName}} (Model Policy in CLAUDE.md), stop, say so and suggest `/model {{models.orchestration}}`. Don't create branches or claim beads first.
9
+
10
+ **Input:** two or more epic IDs, optionally with the flag `--watch-ci` (`$ARGUMENTS`). Every epic's `next-task` run includes the whole-branch review before its push; an epic whose review leaves must-fix findings stops as in step 4. If any ID is missing or not an epic (`bd show`), stop and ask before starting anything; point to `/next-task-beads` for non-epic beads. Never add epics yourself.
11
+
12
+ **By invoking this skill the user authorizes** for each listed epic what `next-task` authorizes for one. Nothing more.
13
+
14
+ ## Steps
15
+
16
+ 1. **Check all epics up front:** run `next-task-plan` part 1 for every epic before starting the first one, so a broken bead late in the batch is found now. If any fails, stop and report all failures together.
17
+ 2. **Order:** epics run **sequentially**, one finished and pushed before the next starts. That way each epic branches from a `{{baseBranch}}` that already contains the earlier ones, and conflicts can't pile up. Keep the user's order, except move an epic after any other listed epic it depends on (`bd dep list`, including dependencies of its children on the other epic's children). A dependency on an open epic that is not in the batch: that epic is skipped (see 4). Show the final order before starting.
18
+ 3. **Run each epic:** follow `next-task` steps 1–7 for it (own worktree via `next-task-branch`, the **Red before green** check and the `next-task-review` loop after each child with its 3-round cap, findings, `next-task-finish`). Don't watch CI per epic.
19
+ {{#if dashboard}}
20
+ Record: `<BATCH-RUN>` = `batch.<first epic id in the final order>`. Right after `next-task-branch` creates the first epic's worktree, from that `<WT>`: `node .claude/wavework/dashboard/record.mjs run-start --run <BATCH-RUN> --kind batch --units <epic ids>` (no `--worktree`, so a cwd lookup never matches the batch run). Each epic's `run-start` adds `--parent <BATCH-RUN>`; its `run-end` for the batch is in `next-task-finish`.
21
+ {{/if}}
22
+ 4. **When an epic stops** (failed gate, blocked child, review still failing after 3 rounds, open must-fix findings of the branch review, conflict, unclear bead, rejected push): leave its branch and worktree as they are, mark it stopped, and also skip every later epic that depends on it. Continue with the independent ones. Never retry or resolve a stopped epic yourself.
23
+ 5. **Watch CI once** after the last epic, only if at least one was pushed: with `--watch-ci` run `watch-ci` for `{{baseBranch}}`; without it ask once with AskUserQuestion ("Skip" first and default, "Watch CI" second).
24
+
25
+ ## Report
26
+
27
+ A table: epic, status (pushed / stopped / skipped), children with commit hashes, findings filed. Then the test results per epic, for stopped ones the step, reason and worktree path, and the CI result or "CI not watched".
@@ -0,0 +1,29 @@
1
+ ---
2
+ name: next-task-beads
3
+ description: Implement one or more individual beads (tasks, bugs, chores, findings — not epics) end to end, like /next-task but without an epic. Creates a worktree branch, plans waves, hands each bead to an implementer sub-agent, records findings, closes the beads, pushes to {{baseBranch}}. Use when the user says "next-task-beads <id> [<id> ...]" or asks to implement specific beads.
4
+ ---
5
+
6
+ # next-task-beads
7
+
8
+ Same pipeline as `next-task`, for a hand-picked set of beads instead of an epic's children. **Model check first:** before any other step or tool call, check the session model. If it isn't {{orchestrationModelName}} (Model Policy in CLAUDE.md), stop, say so and suggest `/model {{models.orchestration}}`. Don't create branches or claim beads first.
9
+
10
+ **Input:** one or more bead IDs, optionally with the flag `--watch-ci` (`$ARGUMENTS`; the whole-branch review before the push always runs, against the beads' descriptions and criteria). If none is given, stop and ask; never pick beads yourself. If an ID is an epic, stop and point to `/next-task` (or `/next-task-batch` for several epics).
11
+
12
+ **By invoking this skill the user authorizes** for these beads: creating and deleting one branch and worktree, committing, pushing it to `{{baseBranch}}` (fast-forward only) and filing `finding` beads. Nothing else: never force-push, never rewrite pushed history{{#if noAttribution}}, never add attribution lines{{/if}}.
13
+
14
+ ## Steps
15
+
16
+ Follow `next-task` steps 1–8, with these differences:
17
+
18
+ {{#if dashboard}}
19
+ Record: the `run-start` uses `--kind beads --units <bead ids>`.
20
+ {{/if}}
21
+ 1. **Branch:** `next-task-branch` with prefix `task`, unit ID = the first bead ID, title = that bead's title (one bead) or a short summary of the set. E.g. `{{taskBranchExample}}`.
22
+ 2. **Check beads:** `next-task-plan` part 1 with the given beads as the work set. Beads without a `model:` label fail the check: stop and suggest `/text-to-task` or adding the label. (Findings filed by `next-task-findings` already carry one. To run a whole group of them, use `/next-task <findings-epic>`.) Dependencies between the given beads order the waves; an open blocker outside the set stops the run.
23
+ 3. **Dependencies, plan, hand-off, verify, findings:** exactly as in `next-task` steps 3–6, including the **Red before green** check (a report without it is a must-fix) and the **Red-green check** script run (`.claude/scripts/red-green-check.sh <WT>` after re-running `next-task-test`, before the review; its output goes to the reviewer) and the `next-task-review` loop after each bead (max 3 rounds, then stop that bead and hand off) and the BLOCKED handling (blocked bead: status `blocked`, notes, worktree kept, dependents skipped, no finish; rerunning resumes in the worktree). Commit each bead as `<bead title> (<bead-id>)`.
24
+ 4. **Finish:** `next-task-finish` as a bead run (no epic to close; docs commit `Document <short summary> (<ids>)`).
25
+ 5. **Watch CI:** as `next-task` step 8.
26
+
27
+ ## Report
28
+
29
+ As in `next-task`: each bead with its commit hash, test results, findings filed, push result, CI result. If stopped early: which step, why, branch and worktree state.
@@ -0,0 +1,19 @@
1
+ ---
2
+ name: next-task-branch
3
+ description: Step 1 of /next-task, /next-task-beads and /next-task-batch. Creates the working branch in its own git worktree from the latest origin/{{baseBranch}}, so parallel sessions and sub-agents never share a checkout. Normally invoked by those skills, not directly.
4
+ ---
5
+
6
+ # next-task-branch
7
+
8
+ **Input:** a unit ID (the epic ID, or for `/next-task-beads` the first bead ID), a branch prefix (`epic` or `task`) and a title for the slug.
9
+
10
+ A branch belongs to a checkout, not to an agent. Every run therefore gets its **own worktree**; the main checkout is never switched, so other sessions working there are not disturbed and nothing lands on the wrong branch.
11
+
12
+ 1. **Find the main repo:** `MAIN=$(dirname "$(git rev-parse --path-format=absolute --git-common-dir)")`. The worktree path is `WT={{worktreeExpr}}`, e.g. `{{worktreeExample}}`.
13
+ 2. **Fetch {{baseBranch}}:** `git -C "$MAIN" fetch origin {{baseBranch}}`. Never base the branch on the current branch, `HEAD` or the local `{{baseBranch}}`.
14
+ 3. **Create branch and worktree in one step:** `git -C "$MAIN" worktree add -b {{branchExpr}} "$WT" origin/{{baseBranch}}`. The slug is 2–5 lowercase words from the title joined by hyphens, e.g. `epic/shipr-yy7-project-skeleton`.
15
+ - **Resume:** if `WT` already exists (`git worktree list`) and has a `{{branchGlob}}` branch checked out, reuse it instead of failing: skip steps 2–3, don't touch its commits or uncommitted changes, and report "resumed" with `git status --short` and `git log --oneline origin/{{baseBranch}}..HEAD`. A blocked bead's work is still there.
16
+ - If the path or branch exists in any other way (other branch in the path, branch without worktree), ask the user whether to resume or start fresh. Starting fresh deletes the old worktree and branch, so confirm first, and never remove a worktree with uncommitted changes.
17
+ - Uncommitted changes in the main checkout are not your concern: they stay there and are not carried over.
18
+ 4. **From now on, work only in `$WT`.** Start every Bash command with `cd <WT> &&` and use absolute paths under `<WT>` for Read/Edit/Write. Never run `git switch`/`checkout` anywhere.
19
+ 5. Report the branch name, the worktree path and the `origin/{{baseBranch}}` commit it starts from (`git -C "$MAIN" rev-parse --short origin/{{baseBranch}}`).