@kleber.mottajr/juninho 1.2.0 → 2.0.0

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 (39) hide show
  1. package/README.md +14 -15
  2. package/dist/config.d.ts +31 -1
  3. package/dist/config.d.ts.map +1 -1
  4. package/dist/config.js +57 -3
  5. package/dist/config.js.map +1 -1
  6. package/dist/installer.d.ts.map +1 -1
  7. package/dist/installer.js +59 -45
  8. package/dist/installer.js.map +1 -1
  9. package/dist/lint-detection.d.ts +2 -2
  10. package/dist/lint-detection.d.ts.map +1 -1
  11. package/dist/lint-detection.js +33 -7
  12. package/dist/lint-detection.js.map +1 -1
  13. package/dist/project-types.d.ts +7 -2
  14. package/dist/project-types.d.ts.map +1 -1
  15. package/dist/project-types.js +36 -3
  16. package/dist/project-types.js.map +1 -1
  17. package/dist/templates/agents.d.ts +2 -2
  18. package/dist/templates/agents.d.ts.map +1 -1
  19. package/dist/templates/agents.js +551 -100
  20. package/dist/templates/agents.js.map +1 -1
  21. package/dist/templates/commands.d.ts.map +1 -1
  22. package/dist/templates/commands.js +330 -285
  23. package/dist/templates/commands.js.map +1 -1
  24. package/dist/templates/docs.js +36 -24
  25. package/dist/templates/docs.js.map +1 -1
  26. package/dist/templates/plugins.d.ts.map +1 -1
  27. package/dist/templates/plugins.js +699 -99
  28. package/dist/templates/plugins.js.map +1 -1
  29. package/dist/templates/state.d.ts.map +1 -1
  30. package/dist/templates/state.js +138 -186
  31. package/dist/templates/state.js.map +1 -1
  32. package/dist/templates/support-scripts.d.ts.map +1 -1
  33. package/dist/templates/support-scripts.js +927 -247
  34. package/dist/templates/support-scripts.js.map +1 -1
  35. package/dist/templates/tools.d.ts +2 -2
  36. package/dist/templates/tools.d.ts.map +1 -1
  37. package/dist/templates/tools.js +2 -2
  38. package/dist/templates/tools.js.map +1 -1
  39. package/package.json +5 -2
@@ -11,7 +11,10 @@ function writeSupportScripts(projectDir, projectType = "node-nextjs", isKotlin =
11
11
  writeExecutable(path_1.default.join(scriptsDir, "pre-commit.sh"), PRE_COMMIT);
12
12
  writeExecutable(path_1.default.join(scriptsDir, "lint-structure.sh"), lintStructure(projectType, isKotlin, lintTool));
13
13
  writeExecutable(path_1.default.join(scriptsDir, "test-related.sh"), testRelated(projectType, isKotlin));
14
+ writeExecutable(path_1.default.join(scriptsDir, "run-test-scope.sh"), runTestScope(projectType, isKotlin));
14
15
  writeExecutable(path_1.default.join(scriptsDir, "check-all.sh"), checkAll(projectType, isKotlin, lintTool));
16
+ writeExecutable(path_1.default.join(scriptsDir, "scaffold-spec-state.sh"), SCAFFOLD_SPEC_STATE);
17
+ writeExecutable(path_1.default.join(scriptsDir, "harness-feature-integration.sh"), HARNESS_FEATURE_INTEGRATION);
15
18
  }
16
19
  function writeExecutable(filePath, content) {
17
20
  (0, fs_1.writeFileSync)(filePath, content);
@@ -44,7 +47,6 @@ echo "[juninho:pre-commit] Running related tests..."
44
47
 
45
48
  echo "[juninho:pre-commit] Local checks passed"
46
49
  `;
47
- /* ─── Lint Structure ─── */
48
50
  function lintStructure(projectType, isKotlin, lintTool) {
49
51
  const header = `#!/bin/sh
50
52
  set -e
@@ -72,16 +74,14 @@ fi
72
74
  case "go":
73
75
  return header + lintGoBody(lintTool);
74
76
  case "java":
75
- return isKotlin
76
- ? header + lintKotlinBody(lintTool)
77
- : header + lintJavaBody(lintTool);
77
+ return isKotlin ? header + lintKotlinBody(lintTool) : header + lintJavaBody(lintTool);
78
78
  case "generic":
79
79
  return header + lintGenericBody();
80
80
  }
81
81
  }
82
82
  function lintNodeBody(lintTool) {
83
83
  const priority = lintTool
84
- ? `# Priority linter: ${lintTool}\nif command -v npx >/dev/null 2>&1; then\n npx ${lintTool} $FILES\n exit 0\nfi\n\n`
84
+ ? `if command -v npx >/dev/null 2>&1; then\n npx ${lintTool} $FILES\n exit 0\nfi\n\n`
85
85
  : "";
86
86
  return `${priority}has_package_script() {
87
87
  [ -f package.json ] || return 1
@@ -104,15 +104,14 @@ if command -v npx >/dev/null 2>&1 && npx --yes eslint --version >/dev/null 2>&1;
104
104
  fi
105
105
 
106
106
  echo "[juninho:lint-structure] No structure lint configured."
107
- echo "[juninho:lint-structure] Customize .opencode/scripts/lint-structure.sh or run /j.init-deep."
107
+ echo "[juninho:lint-structure] Customize .opencode/scripts/lint-structure.sh or run /j.finish-setup."
108
108
  `;
109
109
  }
110
110
  function lintPythonBody(lintTool) {
111
111
  const priority = lintTool
112
- ? `# Priority linter: ${lintTool}\nif command -v ${lintTool} >/dev/null 2>&1; then\n ${lintTool} check $FILES\n exit 0\nfi\n\n`
112
+ ? `if command -v ${lintTool} >/dev/null 2>&1; then\n ${lintTool} check $FILES\n exit 0\nfi\n\n`
113
113
  : "";
114
- return `${priority}# Python lint chain: ruff flake8 → pylint
115
- if command -v ruff >/dev/null 2>&1; then
114
+ return `${priority}if command -v ruff >/dev/null 2>&1; then
116
115
  ruff check $FILES
117
116
  exit 0
118
117
  fi
@@ -132,10 +131,9 @@ echo "[juninho:lint-structure] No Python linter found. Install ruff, flake8, or
132
131
  }
133
132
  function lintGoBody(lintTool) {
134
133
  const priority = lintTool
135
- ? `# Priority linter: ${lintTool}\nif command -v ${lintTool} >/dev/null 2>&1; then\n ${lintTool} run\n exit 0\nfi\n\n`
134
+ ? `if command -v ${lintTool} >/dev/null 2>&1; then\n ${lintTool} run\n exit 0\nfi\n\n`
136
135
  : "";
137
- return `${priority}# Go lint chain: golangci-lint go vet
138
- if command -v golangci-lint >/dev/null 2>&1; then
136
+ return `${priority}if command -v golangci-lint >/dev/null 2>&1; then
139
137
  golangci-lint run
140
138
  exit 0
141
139
  fi
@@ -143,12 +141,8 @@ fi
143
141
  go vet ./...
144
142
  `;
145
143
  }
146
- function lintJavaBody(lintTool) {
147
- const priority = lintTool
148
- ? `# Priority linter: ${lintTool}\n`
149
- : "";
150
- return `${priority}# Java lint chain: gradle checkstyle → maven checkstyle
151
- if [ -x "./gradlew" ]; then
144
+ function lintJavaBody(_lintTool) {
145
+ return `if [ -x "./gradlew" ]; then
152
146
  ./gradlew checkstyleMain 2>/dev/null && exit 0
153
147
  echo "[juninho:lint-structure] Gradle checkstyle not configured. Add checkstyle plugin to build.gradle."
154
148
  exit 0
@@ -163,33 +157,24 @@ fi
163
157
  echo "[juninho:lint-structure] No Java build tool found."
164
158
  `;
165
159
  }
166
- function lintKotlinBody(lintTool) {
167
- const priority = lintTool
168
- ? `# Priority linter: ${lintTool}\n`
169
- : "";
170
- return `${priority}# Kotlin lint chain: ktlint → detekt → compileKotlin warnings
171
- if [ -x "./gradlew" ]; then
172
- # Try ktlint first (most common for Kotlin formatting/style)
160
+ function lintKotlinBody(_lintTool) {
161
+ return `if [ -x "./gradlew" ]; then
173
162
  if ./gradlew tasks --all 2>/dev/null | grep -q "ktlintCheck"; then
174
163
  ./gradlew ktlintCheck
175
164
  exit 0
176
165
  fi
177
166
 
178
- # Try detekt (static analysis)
179
167
  if ./gradlew tasks --all 2>/dev/null | grep -q "detekt"; then
180
168
  ./gradlew detekt
181
169
  exit 0
182
170
  fi
183
171
 
184
- # Fallback: compile with warnings treated as errors
185
172
  ./gradlew compileKotlin 2>&1
186
173
  exit 0
187
174
  fi
188
175
 
189
176
  if [ -x "./mvnw" ]; then
190
- # Maven ktlint plugin
191
177
  ./mvnw antrun:run@ktlint-check 2>/dev/null && exit 0
192
- # Fallback to compile
193
178
  ./mvnw compile 2>&1
194
179
  exit 0
195
180
  fi
@@ -199,8 +184,7 @@ echo "[juninho:lint-structure] Add ktlint or detekt Gradle plugin for structural
199
184
  `;
200
185
  }
201
186
  function lintGenericBody() {
202
- return `# Generic: try common linters across stacks
203
- if [ -f package.json ]; then
187
+ return `if [ -f package.json ]; then
204
188
  if command -v npx >/dev/null 2>&1; then
205
189
  npx eslint --max-warnings=0 $FILES 2>/dev/null && exit 0
206
190
  fi
@@ -221,43 +205,36 @@ fi
221
205
  echo "[juninho:lint-structure] No linter detected. Customize .opencode/scripts/lint-structure.sh."
222
206
  `;
223
207
  }
224
- /* ─── Test Related ─── */
225
208
  function testRelated(projectType, isKotlin) {
226
- const header = `#!/bin/sh
209
+ switch (projectType) {
210
+ case "node-nextjs":
211
+ case "node-generic":
212
+ return nodeTestRelated();
213
+ case "python":
214
+ return pythonTestRelated();
215
+ case "go":
216
+ return goTestRelated();
217
+ case "java":
218
+ return isKotlin ? kotlinTestRelated() : javaTestRelated();
219
+ case "generic":
220
+ return genericTestRelated();
221
+ }
222
+ }
223
+ function nodeTestRelated() {
224
+ return `#!/bin/sh
227
225
  set -e
228
226
 
229
227
  ROOT_DIR="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
230
228
  cd "$ROOT_DIR"
231
229
 
232
- staged_files_as_args() {
233
- printf '%s\\n' "$JUNINHO_STAGED_FILES" | sed '/^$/d' | tr '\\n' ' '
234
- }
235
-
236
- FILES="$(staged_files_as_args)"
230
+ FILES="\${JUNINHO_STAGED_FILES:-}"
237
231
 
238
232
  if [ -z "$FILES" ]; then
239
233
  echo "[juninho:test-related] No staged files. Skipping."
240
234
  exit 0
241
235
  fi
242
- `;
243
- switch (projectType) {
244
- case "node-nextjs":
245
- case "node-generic":
246
- return header + testNodeBody();
247
- case "python":
248
- return header + testPythonBody();
249
- case "go":
250
- return header + testGoBody();
251
- case "java":
252
- return isKotlin
253
- ? header + testKotlinBody()
254
- : header + testJavaBody();
255
- case "generic":
256
- return header + testGenericBody();
257
- }
258
- }
259
- function testNodeBody() {
260
- return `has_package_script() {
236
+
237
+ has_package_script() {
261
238
  [ -f package.json ] || return 1
262
239
  node -e "const fs=require('fs'); const pkg=JSON.parse(fs.readFileSync('package.json','utf8')); process.exit(pkg.scripts && pkg.scripts[process.argv[1]] ? 0 : 1)" "$1" >/dev/null 2>&1
263
240
  }
@@ -278,46 +255,66 @@ if command -v npx >/dev/null 2>&1 && npx --yes vitest --version >/dev/null 2>&1;
278
255
  fi
279
256
 
280
257
  echo "[juninho:test-related] No related-test command configured."
281
- echo "[juninho:test-related] Customize .opencode/scripts/test-related.sh or run /j.init-deep."
258
+ echo "[juninho:test-related] Customize .opencode/scripts/test-related.sh or run /j.finish-setup."
259
+ `;
260
+ }
261
+ function pythonTestRelated() {
262
+ return `#!/bin/sh
263
+ set -e
264
+
265
+ ROOT_DIR="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
266
+ cd "$ROOT_DIR"
267
+
268
+ FILES="\${JUNINHO_STAGED_FILES:-}"
269
+
270
+ if [ -z "$FILES" ]; then
271
+ echo "[juninho:test-related] No staged files. Skipping."
272
+ exit 0
273
+ fi
274
+
275
+ PY_FILES=""
276
+ for f in $FILES; do
277
+ case "$f" in *.py) PY_FILES="$PY_FILES $f" ;; esac
278
+ done
279
+
280
+ if [ -z "$PY_FILES" ]; then
281
+ echo "[juninho:test-related] No Python files staged. Skipping tests."
282
+ exit 0
283
+ fi
284
+
285
+ TEST_TARGETS=""
286
+ for f in $PY_FILES; do
287
+ dir=$(dirname "$f")
288
+ base=$(basename "$f" .py)
289
+ for candidate in "\${dir}/test_\${base}.py" "\${dir}/\${base}_test.py" "tests/test_\${base}.py" "tests/\${dir}/test_\${base}.py"; do
290
+ if [ -f "$candidate" ]; then
291
+ TEST_TARGETS="$TEST_TARGETS $candidate"
292
+ fi
293
+ done
294
+ done
295
+
296
+ if [ -n "$TEST_TARGETS" ]; then
297
+ pytest $TEST_TARGETS --no-header -q 2>/dev/null && exit 0
298
+ python -m pytest $TEST_TARGETS --no-header -q 2>/dev/null && exit 0
299
+ fi
300
+
301
+ echo "[juninho:test-related] No related tests found for staged Python files."
282
302
  `;
283
303
  }
284
- function testPythonBody() {
285
- // Use string concatenation to avoid template literal escaping issues with shell variables
286
- const lines = [
287
- '# Python: run pytest scoped to staged files',
288
- 'PY_FILES=""',
289
- 'for f in $FILES; do',
290
- ' case "$f" in *.py) PY_FILES="$PY_FILES $f" ;; esac',
291
- 'done',
292
- '',
293
- 'if [ -z "$PY_FILES" ]; then',
294
- ' echo "[juninho:test-related] No Python files staged. Skipping tests."',
295
- ' exit 0',
296
- 'fi',
297
- '',
298
- '# Derive test file paths from source files',
299
- 'TEST_TARGETS=""',
300
- 'for f in $PY_FILES; do',
301
- ' dir=$(dirname "$f")',
302
- ' base=$(basename "$f" .py)',
303
- ' for candidate in "${dir}/test_${base}.py" "${dir}/${base}_test.py" "tests/test_${base}.py" "tests/${dir}/test_${base}.py"; do',
304
- ' if [ -f "$candidate" ]; then',
305
- ' TEST_TARGETS="$TEST_TARGETS $candidate"',
306
- ' fi',
307
- ' done',
308
- 'done',
309
- '',
310
- 'if [ -n "$TEST_TARGETS" ]; then',
311
- ' pytest $TEST_TARGETS --no-header -q 2>/dev/null && exit 0',
312
- ' python -m pytest $TEST_TARGETS --no-header -q 2>/dev/null && exit 0',
313
- 'fi',
314
- '',
315
- 'echo "[juninho:test-related] No related tests found for staged Python files."',
316
- ];
317
- return lines.join('\n') + '\n';
318
- }
319
- function testGoBody() {
320
- return `# Go: run tests for packages containing staged files
304
+ function goTestRelated() {
305
+ return `#!/bin/sh
306
+ set -e
307
+
308
+ ROOT_DIR="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
309
+ cd "$ROOT_DIR"
310
+
311
+ FILES="\${JUNINHO_STAGED_FILES:-}"
312
+
313
+ if [ -z "$FILES" ]; then
314
+ echo "[juninho:test-related] No staged files. Skipping."
315
+ exit 0
316
+ fi
317
+
321
318
  GO_FILES=""
322
319
  for f in $FILES; do
323
320
  case "$f" in *.go) GO_FILES="$GO_FILES $f" ;; esac
@@ -328,7 +325,6 @@ if [ -z "$GO_FILES" ]; then
328
325
  exit 0
329
326
  fi
330
327
 
331
- # Extract unique package directories
332
328
  PACKAGES=""
333
329
  for f in $GO_FILES; do
334
330
  pkg="./$(dirname "$f")"
@@ -341,102 +337,132 @@ done
341
337
  go test -count=1 $PACKAGES
342
338
  `;
343
339
  }
344
- function testJavaBody() {
345
- const lines = [
346
- '# Java: run tests scoped to staged files',
347
- 'JAVA_FILES=""',
348
- 'for f in $FILES; do',
349
- ' case "$f" in *.java) JAVA_FILES="$JAVA_FILES $f" ;; esac',
350
- 'done',
351
- '',
352
- 'if [ -z "$JAVA_FILES" ]; then',
353
- ' echo "[juninho:test-related] No Java files staged. Skipping tests."',
354
- ' exit 0',
355
- 'fi',
356
- '',
357
- '# Extract test class names from staged source files',
358
- 'TEST_FILTER=""',
359
- 'for f in $JAVA_FILES; do',
360
- ' base=$(basename "$f" .java)',
361
- ' case "$base" in *Test|*Tests|*IT)',
362
- ' TEST_FILTER="$TEST_FILTER --tests *${base}"',
363
- ' continue',
364
- ' ;;',
365
- ' esac',
366
- ' TEST_FILTER="$TEST_FILTER --tests *${base}Test"',
367
- 'done',
368
- '',
369
- 'if [ -x "./gradlew" ]; then',
370
- ' ./gradlew test $TEST_FILTER 2>/dev/null || ./gradlew test',
371
- ' exit 0',
372
- 'fi',
373
- '',
374
- 'if [ -x "./mvnw" ]; then',
375
- ' MAVEN_FILTER=""',
376
- ' for f in $JAVA_FILES; do',
377
- ' base=$(basename "$f" .java)',
378
- ' MAVEN_FILTER="${MAVEN_FILTER},${base}Test"',
379
- ' done',
380
- ' MAVEN_FILTER=$(echo "$MAVEN_FILTER" | sed \'s/^,//\')',
381
- ' ./mvnw test -Dtest="$MAVEN_FILTER" 2>/dev/null || ./mvnw test',
382
- ' exit 0',
383
- 'fi',
384
- '',
385
- 'echo "[juninho:test-related] No Java build tool found."',
386
- ];
387
- return lines.join('\n') + '\n';
388
- }
389
- function testKotlinBody() {
390
- const lines = [
391
- '# Kotlin: run tests scoped to staged files',
392
- 'KT_FILES=""',
393
- 'JAVA_FILES=""',
394
- 'for f in $FILES; do',
395
- ' case "$f" in',
396
- ' *.kt|*.kts) KT_FILES="$KT_FILES $f" ;;',
397
- ' *.java) JAVA_FILES="$JAVA_FILES $f" ;;',
398
- ' esac',
399
- 'done',
400
- '',
401
- 'ALL_FILES="$KT_FILES $JAVA_FILES"',
402
- 'if [ -z "$(echo "$ALL_FILES" | tr -d \' \')" ]; then',
403
- ' echo "[juninho:test-related] No Kotlin/Java files staged. Skipping tests."',
404
- ' exit 0',
405
- 'fi',
406
- '',
407
- '# Extract test class names from staged source files',
408
- 'TEST_FILTER=""',
409
- 'for f in $KT_FILES $JAVA_FILES; do',
410
- ' ext="${f##*.}"',
411
- ' base=$(basename "$f" ".$ext")',
412
- ' case "$base" in *Test|*Tests|*IT|*Spec)',
413
- ' TEST_FILTER="$TEST_FILTER --tests *${base}"',
414
- ' continue',
415
- ' ;;',
416
- ' esac',
417
- ' TEST_FILTER="$TEST_FILTER --tests *${base}Test"',
418
- 'done',
419
- '',
420
- 'if [ -x "./gradlew" ]; then',
421
- ' if [ -n "$TEST_FILTER" ]; then',
422
- ' ./gradlew test $TEST_FILTER 2>/dev/null || ./gradlew test',
423
- ' else',
424
- ' ./gradlew test',
425
- ' fi',
426
- ' exit 0',
427
- 'fi',
428
- '',
429
- 'if [ -x "./mvnw" ]; then',
430
- ' ./mvnw test',
431
- ' exit 0',
432
- 'fi',
433
- '',
434
- 'echo "[juninho:test-related] No Kotlin build tool found."',
435
- ];
436
- return lines.join('\n') + '\n';
437
- }
438
- function testGenericBody() {
439
- return `# Generic: try common test runners
340
+ function javaTestRelated() {
341
+ return `#!/bin/sh
342
+ set -e
343
+
344
+ ROOT_DIR="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
345
+ cd "$ROOT_DIR"
346
+
347
+ FILES="\${JUNINHO_STAGED_FILES:-}"
348
+
349
+ if [ -z "$FILES" ]; then
350
+ echo "[juninho:test-related] No staged files. Skipping."
351
+ exit 0
352
+ fi
353
+
354
+ JAVA_FILES=""
355
+ for f in $FILES; do
356
+ case "$f" in *.java) JAVA_FILES="$JAVA_FILES $f" ;; esac
357
+ done
358
+
359
+ if [ -z "$JAVA_FILES" ]; then
360
+ echo "[juninho:test-related] No Java files staged. Skipping tests."
361
+ exit 0
362
+ fi
363
+
364
+ TEST_FILTER=""
365
+ for f in $JAVA_FILES; do
366
+ base=$(basename "$f" .java)
367
+ case "$base" in *Test|*Tests|*IT)
368
+ TEST_FILTER="$TEST_FILTER --tests *\${base}"
369
+ continue
370
+ ;;
371
+ esac
372
+ TEST_FILTER="$TEST_FILTER --tests *\${base}Test"
373
+ done
374
+
375
+ if [ -x "./gradlew" ]; then
376
+ ./gradlew test $TEST_FILTER 2>/dev/null || ./gradlew test
377
+ exit 0
378
+ fi
379
+
380
+ if [ -x "./mvnw" ]; then
381
+ MAVEN_FILTER=""
382
+ for f in $JAVA_FILES; do
383
+ base=$(basename "$f" .java)
384
+ MAVEN_FILTER="\${MAVEN_FILTER},\${base}Test"
385
+ done
386
+ MAVEN_FILTER=$(echo "$MAVEN_FILTER" | sed 's/^,//')
387
+ ./mvnw test -Dtest="$MAVEN_FILTER" 2>/dev/null || ./mvnw test
388
+ exit 0
389
+ fi
390
+
391
+ echo "[juninho:test-related] No Java build tool found."
392
+ `;
393
+ }
394
+ function kotlinTestRelated() {
395
+ return `#!/bin/sh
396
+ set -e
397
+
398
+ ROOT_DIR="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
399
+ cd "$ROOT_DIR"
400
+
401
+ FILES="\${JUNINHO_STAGED_FILES:-}"
402
+
403
+ if [ -z "$FILES" ]; then
404
+ echo "[juninho:test-related] No staged files. Skipping."
405
+ exit 0
406
+ fi
407
+
408
+ KT_FILES=""
409
+ JAVA_FILES=""
410
+ for f in $FILES; do
411
+ case "$f" in
412
+ *.kt|*.kts) KT_FILES="$KT_FILES $f" ;;
413
+ *.java) JAVA_FILES="$JAVA_FILES $f" ;;
414
+ esac
415
+ done
416
+
417
+ ALL_FILES="$KT_FILES $JAVA_FILES"
418
+ if [ -z "$(echo "$ALL_FILES" | tr -d ' ')" ]; then
419
+ echo "[juninho:test-related] No Kotlin/Java files staged. Skipping tests."
420
+ exit 0
421
+ fi
422
+
423
+ TEST_FILTER=""
424
+ for f in $KT_FILES $JAVA_FILES; do
425
+ ext="\${f##*.}"
426
+ base=$(basename "$f" ".$ext")
427
+ case "$base" in *Test|*Tests|*IT|*Spec)
428
+ TEST_FILTER="$TEST_FILTER --tests *\${base}"
429
+ continue
430
+ ;;
431
+ esac
432
+ TEST_FILTER="$TEST_FILTER --tests *\${base}Test"
433
+ done
434
+
435
+ if [ -x "./gradlew" ]; then
436
+ if [ -n "$TEST_FILTER" ]; then
437
+ ./gradlew test $TEST_FILTER 2>/dev/null || ./gradlew test
438
+ else
439
+ ./gradlew test
440
+ fi
441
+ exit 0
442
+ fi
443
+
444
+ if [ -x "./mvnw" ]; then
445
+ ./mvnw test
446
+ exit 0
447
+ fi
448
+
449
+ echo "[juninho:test-related] No Kotlin build tool found."
450
+ `;
451
+ }
452
+ function genericTestRelated() {
453
+ return `#!/bin/sh
454
+ set -e
455
+
456
+ ROOT_DIR="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
457
+ cd "$ROOT_DIR"
458
+
459
+ FILES="\${JUNINHO_STAGED_FILES:-}"
460
+
461
+ if [ -z "$FILES" ]; then
462
+ echo "[juninho:test-related] No staged files. Skipping."
463
+ exit 0
464
+ fi
465
+
440
466
  if [ -f package.json ]; then
441
467
  if command -v npx >/dev/null 2>&1; then
442
468
  npx jest --findRelatedTests --passWithNoTests $FILES 2>/dev/null && exit 0
@@ -459,13 +485,34 @@ fi
459
485
  echo "[juninho:test-related] No test runner detected. Customize .opencode/scripts/test-related.sh."
460
486
  `;
461
487
  }
462
- /* ─── Check All ─── */
488
+ function runTestScope(projectType, isKotlin) {
489
+ switch (projectType) {
490
+ case "node-nextjs":
491
+ case "node-generic":
492
+ return RUN_TEST_SCOPE_GENERIC_NODE;
493
+ case "python":
494
+ return RUN_TEST_SCOPE_PYTHON;
495
+ case "go":
496
+ return RUN_TEST_SCOPE_GO;
497
+ case "java":
498
+ return isKotlin ? RUN_TEST_SCOPE_KOTLIN : RUN_TEST_SCOPE_JAVA;
499
+ case "generic":
500
+ return RUN_TEST_SCOPE_GENERIC;
501
+ }
502
+ }
463
503
  function checkAll(projectType, isKotlin, lintTool) {
464
504
  const header = `#!/bin/sh
465
505
  set -e
466
506
 
467
507
  ROOT_DIR="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
468
508
  cd "$ROOT_DIR"
509
+
510
+ sh "$ROOT_DIR/.opencode/scripts/harness-feature-integration.sh" switch-active >/dev/null 2>&1 || true
511
+
512
+ CURRENT_BRANCH="$(git symbolic-ref --quiet --short HEAD 2>/dev/null || true)"
513
+ if [ -n "$CURRENT_BRANCH" ]; then
514
+ echo "[juninho:check-all] Running on branch: $CURRENT_BRANCH"
515
+ fi
469
516
  `;
470
517
  switch (projectType) {
471
518
  case "node-nextjs":
@@ -476,9 +523,7 @@ cd "$ROOT_DIR"
476
523
  case "go":
477
524
  return header + checkAllGoBody(lintTool);
478
525
  case "java":
479
- return isKotlin
480
- ? header + checkAllKotlinBody(lintTool)
481
- : header + checkAllJavaBody(lintTool);
526
+ return isKotlin ? header + checkAllKotlinBody(lintTool) : header + checkAllJavaBody(lintTool);
482
527
  case "generic":
483
528
  return header + checkAllGenericBody();
484
529
  }
@@ -489,35 +534,35 @@ function checkAllNodeBody() {
489
534
  node -e "const fs=require('fs'); const pkg=JSON.parse(fs.readFileSync('package.json','utf8')); process.exit(pkg.scripts && pkg.scripts[process.argv[1]] ? 0 : 1)" "$1" >/dev/null 2>&1
490
535
  }
491
536
 
537
+ echo "[juninho:check-all] Running formatting checks..."
538
+ if has_package_script "lint"; then
539
+ npm run lint
540
+ elif has_package_script "check:all"; then
541
+ npm run check:all
542
+ fi
543
+
544
+ echo "[juninho:check-all] Running repo-wide tests..."
492
545
  if has_package_script "check:all"; then
493
546
  npm run check:all
494
547
  exit 0
495
548
  fi
496
549
 
497
- if [ -f package.json ]; then
498
- if has_package_script "typecheck"; then
499
- npm run typecheck
500
- fi
501
-
502
- if has_package_script "lint"; then
503
- npm run lint
504
- fi
505
-
506
- if has_package_script "test"; then
507
- npm test -- --runInBand
508
- fi
550
+ if has_package_script "typecheck"; then
551
+ npm run typecheck
552
+ fi
509
553
 
554
+ if has_package_script "test"; then
555
+ npm test
510
556
  exit 0
511
557
  fi
512
558
 
513
559
  echo "[juninho:check-all] No full verification command configured."
514
- echo "[juninho:check-all] Customize .opencode/scripts/check-all.sh or run /j.init-deep."
560
+ echo "[juninho:check-all] Customize .opencode/scripts/check-all.sh or run /j.finish-setup."
515
561
  `;
516
562
  }
517
563
  function checkAllPythonBody(lintTool) {
518
564
  const lint = lintTool ?? "ruff";
519
- return `# Python: lint + test
520
- echo "[juninho:check-all] Running lint..."
565
+ return `echo "[juninho:check-all] Running formatting checks..."
521
566
  if command -v ${lint} >/dev/null 2>&1; then
522
567
  ${lint} check .
523
568
  elif command -v ruff >/dev/null 2>&1; then
@@ -526,82 +571,215 @@ elif command -v flake8 >/dev/null 2>&1; then
526
571
  flake8 .
527
572
  fi
528
573
 
529
- echo "[juninho:check-all] Running tests..."
530
- if command -v pytest >/dev/null 2>&1; then
531
- pytest
532
- elif command -v python >/dev/null 2>&1; then
533
- python -m pytest
534
- fi
574
+ echo "[juninho:check-all] Running repo-wide tests..."
575
+ sh "$ROOT_DIR/.opencode/scripts/run-test-scope.sh" full
535
576
  `;
536
577
  }
537
578
  function checkAllGoBody(lintTool) {
538
579
  const lint = lintTool ?? "golangci-lint";
539
- return `# Go: vet + lint + test
540
- echo "[juninho:check-all] Running go vet..."
580
+ return `echo "[juninho:check-all] Running formatting checks..."
541
581
  go vet ./...
542
-
543
- echo "[juninho:check-all] Running lint..."
544
582
  if command -v ${lint} >/dev/null 2>&1; then
545
583
  ${lint} run
546
584
  fi
547
585
 
548
- echo "[juninho:check-all] Running tests..."
549
- go test ./...
586
+ echo "[juninho:check-all] Running repo-wide tests..."
587
+ sh "$ROOT_DIR/.opencode/scripts/run-test-scope.sh" full
550
588
  `;
551
589
  }
552
590
  function checkAllJavaBody(lintTool) {
553
- return `# Java: full build with tests
591
+ return `echo "[juninho:check-all] Running formatting checks..."
554
592
  if [ -x "./gradlew" ]; then
555
- ${lintTool ? `echo "[juninho:check-all] Running lint..."\n ./gradlew checkstyleMain 2>/dev/null || true\n ` : ""}echo "[juninho:check-all] Running tests..."
556
- ./gradlew test
593
+ ${lintTool ? "./gradlew checkstyleMain 2>/dev/null || true\n" : ""}echo "[juninho:check-all] Running repo-wide tests..."
594
+ sh "$ROOT_DIR/.opencode/scripts/run-test-scope.sh" full
557
595
  exit 0
558
596
  fi
559
597
 
560
598
  if [ -x "./mvnw" ]; then
561
- ${lintTool ? `echo "[juninho:check-all] Running lint..."\n ./mvnw checkstyle:check 2>/dev/null || true\n ` : ""}echo "[juninho:check-all] Running tests..."
562
- ./mvnw test
599
+ ${lintTool ? "./mvnw checkstyle:check 2>/dev/null || true\n" : ""}echo "[juninho:check-all] Running repo-wide tests..."
600
+ sh "$ROOT_DIR/.opencode/scripts/run-test-scope.sh" full
563
601
  exit 0
564
602
  fi
565
603
 
566
604
  echo "[juninho:check-all] No Java build tool found."
567
605
  `;
568
606
  }
569
- function checkAllKotlinBody(lintTool) {
570
- return `# Kotlin: lint + compile + test
607
+ function checkAllKotlinBody(_lintTool) {
608
+ return `echo "[juninho:check-all] Running formatting checks..."
571
609
  if [ -x "./gradlew" ]; then
572
- echo "[juninho:check-all] Running Kotlin lint..."
573
- # Try ktlint first, then detekt
574
610
  ./gradlew ktlintCheck 2>/dev/null || ./gradlew detekt 2>/dev/null || true
611
+ fi
612
+
613
+ echo "[juninho:check-all] Running repo-wide tests..."
614
+ sh "$ROOT_DIR/.opencode/scripts/run-test-scope.sh" full
615
+ `;
616
+ }
617
+ function checkAllGenericBody() {
618
+ return `echo "[juninho:check-all] Running repo-wide tests..."
619
+ sh "$ROOT_DIR/.opencode/scripts/run-test-scope.sh" full
620
+ `;
621
+ }
622
+ const RUN_TEST_SCOPE_GENERIC_NODE = `#!/bin/sh
623
+ set -e
624
+
625
+ ROOT_DIR="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
626
+ cd "$ROOT_DIR"
627
+
628
+ TEST_SCOPE="\${1:-}"
629
+
630
+ if [ -z "$TEST_SCOPE" ]; then
631
+ echo "[juninho:run-test-scope] Missing test scope. Pass related files or 'full'."
632
+ exit 1
633
+ fi
634
+
635
+ has_package_script() {
636
+ [ -f package.json ] || return 1
637
+ node -e "const fs=require('fs'); const pkg=JSON.parse(fs.readFileSync('package.json','utf8')); process.exit(pkg.scripts && pkg.scripts[process.argv[1]] ? 0 : 1)" "$1" >/dev/null 2>&1
638
+ }
639
+
640
+ if [ "$TEST_SCOPE" = "full" ]; then
641
+ if has_package_script "check:all"; then
642
+ npm run check:all
643
+ exit 0
644
+ fi
645
+ if has_package_script "test"; then
646
+ npm test -- --runInBand
647
+ exit 0
648
+ fi
649
+ fi
650
+
651
+ if has_package_script "test:related"; then
652
+ npm run test:related -- $TEST_SCOPE
653
+ exit 0
654
+ fi
655
+
656
+ echo "[juninho:run-test-scope] No test scope runner configured."
657
+ `;
658
+ const RUN_TEST_SCOPE_PYTHON = `#!/bin/sh
659
+ set -e
660
+
661
+ ROOT_DIR="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
662
+ cd "$ROOT_DIR"
663
+
664
+ TEST_SCOPE="\${1:-}"
665
+
666
+ if [ -z "$TEST_SCOPE" ]; then
667
+ echo "[juninho:run-test-scope] Missing test scope. Pass a file path, expression, or 'full'."
668
+ exit 1
669
+ fi
670
+
671
+ if [ "$TEST_SCOPE" = "full" ]; then
672
+ pytest 2>/dev/null || python -m pytest
673
+ exit 0
674
+ fi
575
675
 
576
- echo "[juninho:check-all] Compiling..."
577
- ./gradlew compileKotlin
676
+ pytest $TEST_SCOPE 2>/dev/null || python -m pytest $TEST_SCOPE
677
+ `;
678
+ const RUN_TEST_SCOPE_GO = `#!/bin/sh
679
+ set -e
680
+
681
+ ROOT_DIR="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
682
+ cd "$ROOT_DIR"
683
+
684
+ TEST_SCOPE="\${1:-}"
685
+
686
+ if [ -z "$TEST_SCOPE" ] || [ "$TEST_SCOPE" = "full" ]; then
687
+ go test ./...
688
+ exit 0
689
+ fi
690
+
691
+ go test -count=1 $TEST_SCOPE
692
+ `;
693
+ const RUN_TEST_SCOPE_JAVA = `#!/bin/sh
694
+ set -e
695
+
696
+ ROOT_DIR="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
697
+ cd "$ROOT_DIR"
698
+
699
+ TEST_SCOPE="\${1:-}"
578
700
 
579
- echo "[juninho:check-all] Running tests..."
580
- ./gradlew test
701
+ if [ -z "$TEST_SCOPE" ]; then
702
+ echo "[juninho:run-test-scope] Missing test scope. Pass a Maven/Gradle test selector or 'full'."
703
+ exit 1
704
+ fi
705
+
706
+ if [ -x "./gradlew" ]; then
707
+ if [ "$TEST_SCOPE" = "full" ]; then
708
+ ./gradlew test
709
+ else
710
+ ./gradlew test --tests "$TEST_SCOPE" 2>/dev/null || ./gradlew test
711
+ fi
581
712
  exit 0
582
713
  fi
583
714
 
584
715
  if [ -x "./mvnw" ]; then
585
- echo "[juninho:check-all] Compiling and testing..."
586
- ./mvnw test
716
+ if [ "$TEST_SCOPE" = "full" ]; then
717
+ ./mvnw test
718
+ else
719
+ ./mvnw test -Dtest="$TEST_SCOPE" 2>/dev/null || ./mvnw test
720
+ fi
587
721
  exit 0
588
722
  fi
589
723
 
590
- echo "[juninho:check-all] No Kotlin build tool found."
724
+ echo "[juninho:run-test-scope] No Java build tool found."
591
725
  `;
592
- }
593
- function checkAllGenericBody() {
594
- return `# Generic: detect and run what's available
595
- if [ -f package.json ]; then
726
+ const RUN_TEST_SCOPE_KOTLIN = `#!/bin/sh
727
+ set -e
728
+
729
+ ROOT_DIR="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
730
+ cd "$ROOT_DIR"
731
+
732
+ TEST_SCOPE="\${1:-}"
733
+
734
+ if [ -z "$TEST_SCOPE" ]; then
735
+ echo "[juninho:run-test-scope] Missing test scope. Pass a Maven/Gradle test selector or 'full'."
736
+ exit 1
737
+ fi
738
+
739
+ if [ -x "./gradlew" ]; then
740
+ if [ "$TEST_SCOPE" = "full" ]; then
741
+ ./gradlew test
742
+ else
743
+ ./gradlew test --tests "$TEST_SCOPE" 2>/dev/null || ./gradlew test
744
+ fi
745
+ exit 0
746
+ fi
747
+
748
+ if [ -x "./mvnw" ]; then
749
+ if [ "$TEST_SCOPE" = "full" ]; then
750
+ ./mvnw test
751
+ else
752
+ ./mvnw test -Dtest="$TEST_SCOPE" 2>/dev/null || ./mvnw test
753
+ fi
754
+ exit 0
755
+ fi
756
+
757
+ echo "[juninho:run-test-scope] No Kotlin build tool found."
758
+ `;
759
+ const RUN_TEST_SCOPE_GENERIC = `#!/bin/sh
760
+ set -e
761
+
762
+ ROOT_DIR="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
763
+ cd "$ROOT_DIR"
764
+
765
+ TEST_SCOPE="\${1:-}"
766
+
767
+ if [ -f package.json ] && [ "$TEST_SCOPE" = "full" ]; then
596
768
  npm test 2>/dev/null && exit 0
597
769
  fi
598
770
 
599
771
  if command -v pytest >/dev/null 2>&1; then
600
- pytest 2>/dev/null && exit 0
772
+ if [ "$TEST_SCOPE" = "full" ] || [ -z "$TEST_SCOPE" ]; then
773
+ pytest && exit 0
774
+ fi
775
+ pytest $TEST_SCOPE && exit 0
601
776
  fi
602
777
 
603
778
  if [ -f go.mod ]; then
604
- go test ./... 2>/dev/null && exit 0
779
+ if [ "$TEST_SCOPE" = "full" ] || [ -z "$TEST_SCOPE" ]; then
780
+ go test ./... && exit 0
781
+ fi
782
+ go test -count=1 $TEST_SCOPE && exit 0
605
783
  fi
606
784
 
607
785
  if [ -x "./gradlew" ]; then
@@ -612,8 +790,510 @@ if [ -x "./mvnw" ]; then
612
790
  ./mvnw test 2>/dev/null && exit 0
613
791
  fi
614
792
 
615
- echo "[juninho:check-all] No full verification command configured."
616
- echo "[juninho:check-all] Customize .opencode/scripts/check-all.sh or run /j.init-deep."
793
+ echo "[juninho:run-test-scope] No test scope runner detected."
617
794
  `;
795
+ const SCAFFOLD_SPEC_STATE = `#!/bin/sh
796
+ set -e
797
+
798
+ ROOT_DIR="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
799
+ FEATURE_SLUG="\${1:-}"
800
+
801
+ [ -n "$FEATURE_SLUG" ] || {
802
+ echo "Usage: $0 <feature-slug>" >&2
803
+ exit 1
804
+ }
805
+
806
+ STATE_DIR="$ROOT_DIR/docs/specs/$FEATURE_SLUG/state"
807
+ TEMPLATE_PATH="$ROOT_DIR/.opencode/templates/spec-state-readme.md"
808
+
809
+ mkdir -p "$STATE_DIR/tasks" "$STATE_DIR/sessions"
810
+
811
+ if [ -f "$TEMPLATE_PATH" ] && [ ! -f "$STATE_DIR/README.md" ]; then
812
+ sed "s/{feature-slug}/$FEATURE_SLUG/g" "$TEMPLATE_PATH" > "$STATE_DIR/README.md"
813
+ fi
814
+ `;
815
+ const HARNESS_FEATURE_INTEGRATION = `#!/bin/sh
816
+ set -e
817
+
818
+ ROOT_DIR="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
819
+ cd "$ROOT_DIR"
820
+ TAB="$(printf '\t')"
821
+
822
+ if command -v node >/dev/null 2>&1; then
823
+ JS_RUNTIME="node"
824
+ elif command -v bun >/dev/null 2>&1; then
825
+ JS_RUNTIME="bun"
826
+ else
827
+ echo "[juninho:feature-integration] Missing JavaScript runtime (node or bun)" >&2
828
+ exit 1
829
+ fi
830
+
831
+ fail() {
832
+ echo "[juninho:feature-integration] $*" >&2
833
+ exit 1
834
+ }
835
+
836
+ current_branch() {
837
+ git symbolic-ref --quiet --short HEAD 2>/dev/null || true
838
+ }
839
+
840
+ state_file_path() {
841
+ local_name="$1"
842
+ printf '%s/.opencode/state/%s\n' "$ROOT_DIR" "$local_name"
843
+ }
844
+
845
+ default_base_ref() {
846
+ if git show-ref --verify --quiet "refs/remotes/origin/main"; then
847
+ printf '%s\n' "refs/remotes/origin/main"
848
+ return
849
+ fi
850
+ if git show-ref --verify --quiet "refs/remotes/origin/master"; then
851
+ printf '%s\n' "refs/remotes/origin/master"
852
+ return
853
+ fi
854
+
855
+ branch="$(current_branch)"
856
+ [ -n "$branch" ] || fail "Detached HEAD. Provide an explicit base branch."
857
+ printf '%s\n' "$branch"
858
+ }
859
+
860
+ normalize_base_branch() {
861
+ input="$1"
862
+ case "$input" in
863
+ refs/remotes/origin/*)
864
+ printf '%s\n' "\${input#refs/remotes/origin/}"
865
+ ;;
866
+ origin/*)
867
+ printf '%s\n' "\${input#origin/}"
868
+ ;;
869
+ refs/heads/*)
870
+ printf '%s\n' "\${input#refs/heads/}"
871
+ ;;
872
+ *)
873
+ printf '%s\n' "$input"
874
+ ;;
875
+ esac
876
+ }
877
+
878
+ resolve_base_ref() {
879
+ input="$1"
880
+ if [ -z "$input" ]; then
881
+ default_base_ref
882
+ return
883
+ fi
884
+
885
+ case "$input" in
886
+ refs/remotes/*|refs/heads/*)
887
+ printf '%s\n' "$input"
888
+ ;;
889
+ origin/*)
890
+ printf '%s\n' "refs/remotes/$input"
891
+ ;;
892
+ *)
893
+ if git show-ref --verify --quiet "refs/remotes/origin/$input"; then
894
+ printf '%s\n' "refs/remotes/origin/$input"
895
+ else
896
+ printf '%s\n' "$input"
897
+ fi
898
+ ;;
899
+ esac
900
+ }
901
+
902
+ task_branch_name() {
903
+ printf 'feature/%s-task-%s' "$1" "$2"
904
+ }
905
+
906
+ find_existing_feature_commit() {
907
+ feature_branch="$1"
908
+ validated_commit="$2"
909
+ git log "$feature_branch" --format='%H' --grep="cherry picked from commit $validated_commit" -n 1 2>/dev/null || true
910
+ }
911
+
912
+ feature_branch_name() {
913
+ printf 'feature/%s' "$1"
914
+ }
915
+
916
+ manifest_path() {
917
+ printf '%s/docs/specs/%s/state/integration-state.json' "$ROOT_DIR" "$1"
918
+ }
919
+
920
+ ensure_manifest_dir() {
921
+ sh "$ROOT_DIR/.opencode/scripts/scaffold-spec-state.sh" "$1"
922
+ }
923
+
924
+ json_read_field() {
925
+ MANIFEST_PATH="$1" FIELD_PATH="$2" "$JS_RUNTIME" - <<'NODE'
926
+ const fs = require("fs")
927
+
928
+ const manifestPath = process.env.MANIFEST_PATH
929
+ const fieldPath = process.env.FIELD_PATH || ""
930
+
931
+ if (!manifestPath || !fs.existsSync(manifestPath)) process.exit(1)
932
+
933
+ const data = JSON.parse(fs.readFileSync(manifestPath, "utf8"))
934
+ let value = data
935
+ for (const key of fieldPath.split(".").filter(Boolean)) {
936
+ if (value == null || !(key in value)) process.exit(1)
937
+ value = value[key]
938
+ }
939
+
940
+ if (value == null) process.exit(1)
941
+ if (typeof value === "string") {
942
+ process.stdout.write(value)
943
+ process.exit(0)
944
+ }
945
+
946
+ process.stdout.write(JSON.stringify(value))
947
+ NODE
948
+ }
949
+
950
+ parse_active_feature_slug() {
951
+ execution_state="$(state_file_path execution-state.md)"
952
+ [ -f "$execution_state" ] || return 0
953
+ grep "Feature slug" "$execution_state" 2>/dev/null | head -n 1 | cut -d':' -f2 | tr -d ' '
618
954
  }
955
+
956
+ cmd="\${1:-}"
957
+
958
+ case "$cmd" in
959
+ ensure)
960
+ feature_slug="\${2:-}"
961
+ [ -n "$feature_slug" ] || fail "Usage: ensure <feature-slug> [base-branch]"
962
+
963
+ base_ref="$(resolve_base_ref "\${3:-}")"
964
+ base_branch="$(normalize_base_branch "$base_ref")"
965
+
966
+ ensure_manifest_dir "$feature_slug"
967
+ feature_branch="$(feature_branch_name "$feature_slug")"
968
+ base_sha="$(git rev-parse "$base_ref" 2>/dev/null)" || fail "Unknown base branch/ref: $base_ref"
969
+
970
+ if ! git show-ref --verify --quiet "refs/heads/$feature_branch"; then
971
+ git branch "$feature_branch" "$base_sha" >/dev/null
972
+ fi
973
+
974
+ manifest="$(manifest_path "$feature_slug")"
975
+ FEATURE_SLUG="$feature_slug" FEATURE_BRANCH="$feature_branch" BASE_BRANCH="$base_branch" BASE_REF="$base_ref" BASE_SHA="$base_sha" MANIFEST_PATH="$manifest" "$JS_RUNTIME" - <<'NODE'
976
+ const fs = require("fs")
977
+ const path = require("path")
978
+
979
+ const manifestPath = process.env.MANIFEST_PATH
980
+ const featureSlug = process.env.FEATURE_SLUG
981
+ const featureBranch = process.env.FEATURE_BRANCH
982
+ const baseBranch = process.env.BASE_BRANCH
983
+ const baseRef = process.env.BASE_REF
984
+ const baseSha = process.env.BASE_SHA
985
+
986
+ const now = new Date().toISOString()
987
+ const next = fs.existsSync(manifestPath)
988
+ ? JSON.parse(fs.readFileSync(manifestPath, "utf8"))
989
+ : {}
990
+
991
+ const manifest = {
992
+ featureSlug,
993
+ featureBranch,
994
+ baseBranch,
995
+ baseRef,
996
+ baseStartPoint: next.baseStartPoint || baseSha,
997
+ createdAt: next.createdAt || now,
998
+ lastUpdatedAt: now,
999
+ tasks: next.tasks || {},
1000
+ }
1001
+
1002
+ fs.mkdirSync(path.dirname(manifestPath), { recursive: true })
1003
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n", "utf8")
1004
+ NODE
1005
+
1006
+ printf '%s\n' "$feature_branch"
1007
+ ;;
1008
+
1009
+ print-task-base)
1010
+ feature_slug="\${2:-}"
1011
+ depends_csv="\${3:-}"
1012
+ [ -n "$feature_slug" ] || fail "Usage: print-task-base <feature-slug> [depends-csv]"
1013
+
1014
+ manifest="$(manifest_path "$feature_slug")"
1015
+ [ -f "$manifest" ] || fail "Missing integration manifest: $manifest"
1016
+
1017
+ feature_branch="$(json_read_field "$manifest" "featureBranch")" || fail "Unable to read feature branch"
1018
+ base_start_point="$(json_read_field "$manifest" "baseStartPoint")" || fail "Unable to read base start point"
1019
+
1020
+ if [ -n "$depends_csv" ]; then
1021
+ DEPENDS_CSV="$depends_csv" MANIFEST_PATH="$manifest" "$JS_RUNTIME" - <<'NODE'
1022
+ const fs = require("fs")
1023
+
1024
+ const manifest = JSON.parse(fs.readFileSync(process.env.MANIFEST_PATH, "utf8"))
1025
+ for (const dep of (process.env.DEPENDS_CSV || "").split(",").map((value) => value.trim()).filter(Boolean)) {
1026
+ const entry = manifest.tasks?.[dep]
1027
+ const status = entry?.integration?.status
1028
+ if (entry && (!status || status === "pending")) {
1029
+ throw new Error("Dependency " + dep + " is not integrated yet")
1030
+ }
1031
+ }
1032
+ NODE
1033
+ printf '%s\n' "$feature_branch"
1034
+ exit 0
1035
+ fi
1036
+
1037
+ printf '%s\n' "$base_start_point"
1038
+ ;;
1039
+
1040
+ prepare-task-branch)
1041
+ feature_slug="\${2:-}"
1042
+ task_id="\${3:-}"
1043
+ depends_csv="\${4:-}"
1044
+ worktree_directory="\${5:-}"
1045
+
1046
+ [ -n "$feature_slug" ] || fail "Usage: prepare-task-branch <feature-slug> <task-id> [depends-csv] [worktree-directory]"
1047
+ [ -n "$task_id" ] || fail "Missing task id"
1048
+
1049
+ manifest="$(manifest_path "$feature_slug")"
1050
+ [ -f "$manifest" ] || fail "Missing integration manifest: $manifest"
1051
+
1052
+ task_branch="$(task_branch_name "$feature_slug" "$task_id")"
1053
+ task_base="$(sh "$0" print-task-base "$feature_slug" "$depends_csv")"
1054
+
1055
+ if [ -n "$worktree_directory" ]; then
1056
+ if [ -d "$worktree_directory" ]; then
1057
+ printf '%s\n' "$task_branch"
1058
+ exit 0
1059
+ fi
1060
+
1061
+ parent_dir=$(dirname "$worktree_directory")
1062
+ [ -d "$parent_dir" ] || fail "Missing worktree parent directory: $parent_dir"
1063
+
1064
+ if git show-ref --verify --quiet "refs/heads/$task_branch"; then
1065
+ git worktree add "$worktree_directory" "$task_branch" >/dev/null
1066
+ else
1067
+ git worktree add -b "$task_branch" "$worktree_directory" "$task_base" >/dev/null
1068
+ fi
1069
+ printf '%s\n' "$task_branch"
1070
+ exit 0
1071
+ fi
1072
+
1073
+ if git show-ref --verify --quiet "refs/heads/$task_branch"; then
1074
+ git switch "$task_branch" >/dev/null
1075
+ else
1076
+ git switch -c "$task_branch" "$task_base" >/dev/null
1077
+ fi
1078
+ printf '%s\n' "$task_branch"
1079
+ ;;
1080
+
1081
+ switch)
1082
+ feature_slug="\${2:-}"
1083
+ [ -n "$feature_slug" ] || fail "Usage: switch <feature-slug>"
1084
+
1085
+ manifest="$(manifest_path "$feature_slug")"
1086
+ if [ -f "$manifest" ]; then
1087
+ feature_branch="$(json_read_field "$manifest" "featureBranch")" || fail "Unable to read feature branch from $manifest"
1088
+ else
1089
+ feature_branch="$(feature_branch_name "$feature_slug")"
1090
+ fi
1091
+
1092
+ git switch "$feature_branch" >/dev/null
1093
+ printf '%s\n' "$feature_branch"
1094
+ ;;
1095
+
1096
+ switch-active)
1097
+ feature_slug="$(parse_active_feature_slug)"
1098
+ [ -n "$feature_slug" ] || exit 0
1099
+ sh "$0" switch "$feature_slug"
1100
+ ;;
1101
+
1102
+ record-task)
1103
+ feature_slug="\${2:-}"
1104
+ task_id="\${3:-}"
1105
+ task_branch="\${4:-}"
1106
+ validated_commit="\${5:-}"
1107
+ attempt="\${6:-}"
1108
+ worktree_directory="\${7:-}"
1109
+ task_label="\${8:-}"
1110
+
1111
+ [ -n "$feature_slug" ] || fail "Usage: record-task <feature-slug> <task-id> <task-branch> <validated-commit> <attempt> [worktree] [label]"
1112
+ [ -n "$task_id" ] || fail "Missing task id"
1113
+ [ -n "$task_branch" ] || fail "Missing task branch"
1114
+ [ -n "$validated_commit" ] || fail "Missing validated commit"
1115
+ [ -n "$attempt" ] || fail "Missing attempt"
1116
+
1117
+ manifest="$(manifest_path "$feature_slug")"
1118
+ [ -f "$manifest" ] || fail "Missing integration manifest: $manifest"
1119
+
1120
+ task_tip="$(git rev-parse "refs/heads/$task_branch" 2>/dev/null || printf '%s' "$validated_commit")"
1121
+
1122
+ FEATURE_SLUG="$feature_slug" TASK_ID="$task_id" TASK_BRANCH="$task_branch" VALIDATED_COMMIT="$validated_commit" TASK_TIP="$task_tip" TASK_ATTEMPT="$attempt" WORKTREE_DIRECTORY="$worktree_directory" TASK_LABEL="$task_label" MANIFEST_PATH="$manifest" "$JS_RUNTIME" - <<'NODE'
1123
+ const fs = require("fs")
1124
+
1125
+ const manifest = JSON.parse(fs.readFileSync(process.env.MANIFEST_PATH, "utf8"))
1126
+ const existing = manifest.tasks?.[process.env.TASK_ID]
1127
+
1128
+ manifest.tasks = manifest.tasks || {}
1129
+ manifest.tasks[process.env.TASK_ID] = {
1130
+ ...(existing || {}),
1131
+ taskID: process.env.TASK_ID,
1132
+ taskBranch: process.env.TASK_BRANCH,
1133
+ validatedCommit: process.env.VALIDATED_COMMIT,
1134
+ taskTip: process.env.TASK_TIP,
1135
+ attempt: Number(process.env.TASK_ATTEMPT),
1136
+ worktreeDirectory: process.env.WORKTREE_DIRECTORY || "",
1137
+ taskLabel: process.env.TASK_LABEL || "",
1138
+ recordedAt: new Date().toISOString(),
1139
+ integration: existing?.integration || { status: "pending" },
1140
+ }
1141
+ manifest.lastUpdatedAt = new Date().toISOString()
1142
+ fs.writeFileSync(process.env.MANIFEST_PATH, JSON.stringify(manifest, null, 2) + "\n", "utf8")
1143
+ NODE
1144
+
1145
+ printf '%s\n' "$validated_commit"
1146
+ ;;
1147
+
1148
+ integrate-task)
1149
+ feature_slug="\${2:-}"
1150
+ task_id="\${3:-}"
1151
+
1152
+ [ -n "$feature_slug" ] || fail "Usage: integrate-task <feature-slug> <task-id>"
1153
+ [ -n "$task_id" ] || fail "Missing task id"
1154
+
1155
+ manifest="$(manifest_path "$feature_slug")"
1156
+ [ -f "$manifest" ] || fail "Missing integration manifest: $manifest"
1157
+
1158
+ feature_branch="$(json_read_field "$manifest" "featureBranch")" || fail "Unable to read feature branch"
1159
+ validated_commit="$(json_read_field "$manifest" "tasks.$task_id.validatedCommit")" || fail "Task $task_id has no validated commit"
1160
+ task_branch="$(json_read_field "$manifest" "tasks.$task_id.taskBranch")" || fail "Task $task_id has no task branch"
1161
+
1162
+ git switch "$feature_branch" >/dev/null
1163
+
1164
+ integration_status="already-contained"
1165
+ integration_method="ancestor"
1166
+ if git merge-base --is-ancestor "$validated_commit" HEAD; then
1167
+ integrated_commit="$validated_commit"
1168
+ elif git cherry "$feature_branch" "$validated_commit" 2>/dev/null | grep -q "^- $validated_commit$"; then
1169
+ integration_method="patch-equivalent"
1170
+ integrated_commit="$(find_existing_feature_commit "$feature_branch" "$validated_commit")"
1171
+ if [ -z "$integrated_commit" ]; then
1172
+ integrated_commit="$(git rev-parse HEAD)"
1173
+ fi
1174
+ elif git merge-base --is-ancestor HEAD "$validated_commit"; then
1175
+ git merge --ff-only "$validated_commit" >/dev/null
1176
+ integration_status="ff-only"
1177
+ integration_method="ff-only"
1178
+ integrated_commit="$(git rev-parse HEAD)"
1179
+ else
1180
+ git cherry-pick -x "$validated_commit" >/dev/null
1181
+ integration_status="cherry-picked"
1182
+ integration_method="cherry-pick"
1183
+ integrated_commit="$(git rev-parse HEAD)"
1184
+ fi
1185
+
1186
+ TASK_ID="$task_id" INTEGRATED_STATUS="$integration_status" INTEGRATION_METHOD="$integration_method" INTEGRATED_COMMIT="$integrated_commit" FEATURE_BRANCH="$feature_branch" TASK_BRANCH="$task_branch" MANIFEST_PATH="$manifest" "$JS_RUNTIME" - <<'NODE'
1187
+ const fs = require("fs")
1188
+
1189
+ const manifest = JSON.parse(fs.readFileSync(process.env.MANIFEST_PATH, "utf8"))
1190
+ const task = manifest.tasks?.[process.env.TASK_ID]
1191
+
1192
+ if (!task) throw new Error("Task " + process.env.TASK_ID + " is missing from manifest")
1193
+
1194
+ task.integration = {
1195
+ status: process.env.INTEGRATED_STATUS,
1196
+ method: process.env.INTEGRATION_METHOD,
1197
+ featureBranch: process.env.FEATURE_BRANCH,
1198
+ taskBranch: process.env.TASK_BRANCH,
1199
+ integratedAt: new Date().toISOString(),
1200
+ integratedCommit: process.env.INTEGRATED_COMMIT,
1201
+ }
1202
+ manifest.lastUpdatedAt = new Date().toISOString()
1203
+ fs.writeFileSync(process.env.MANIFEST_PATH, JSON.stringify(manifest, null, 2) + "\n", "utf8")
1204
+ NODE
1205
+
1206
+ printf '%s\n' "$integrated_commit"
1207
+ ;;
1208
+
1209
+ cleanup)
1210
+ feature_slug="\${2:-}"
1211
+ [ -n "$feature_slug" ] || fail "Usage: cleanup <feature-slug>"
1212
+
1213
+ manifest="$(manifest_path "$feature_slug")"
1214
+ [ -f "$manifest" ] || fail "Missing integration manifest: $manifest"
1215
+
1216
+ feature_branch="$(json_read_field "$manifest" "featureBranch")" || fail "Unable to read feature branch"
1217
+ git switch "$feature_branch" >/dev/null
1218
+
1219
+ cleanup_rows="$(MANIFEST_PATH="$manifest" "$JS_RUNTIME" - <<'NODE'
1220
+ const fs = require("fs")
1221
+
1222
+ const manifest = JSON.parse(fs.readFileSync(process.env.MANIFEST_PATH, "utf8"))
1223
+ for (const [taskId, task] of Object.entries(manifest.tasks || {})) {
1224
+ if (!task?.integration?.status || task.integration.status === "pending") continue
1225
+ process.stdout.write(taskId + "\t" + (task.taskBranch || "") + "\t" + (task.worktreeDirectory || "") + "\n")
1226
+ }
1227
+ NODE
1228
+ )"
1229
+
1230
+ if [ -n "$cleanup_rows" ]; then
1231
+ printf '%s\n' "$cleanup_rows" | while IFS="$TAB" read -r task_id task_branch worktree_directory; do
1232
+ if [ -n "$worktree_directory" ] && [ -d "$worktree_directory" ]; then
1233
+ git worktree remove "$worktree_directory" >/dev/null
1234
+ fi
1235
+
1236
+ if [ -n "$task_branch" ] && [ "$task_branch" != "$feature_branch" ] && git show-ref --verify --quiet "refs/heads/$task_branch"; then
1237
+ git branch -d "$task_branch" >/dev/null
1238
+ fi
1239
+ done
1240
+ fi
1241
+
1242
+ MANIFEST_PATH="$manifest" "$JS_RUNTIME" - <<'NODE'
1243
+ const fs = require("fs")
1244
+
1245
+ const manifest = JSON.parse(fs.readFileSync(process.env.MANIFEST_PATH, "utf8"))
1246
+ for (const task of Object.values(manifest.tasks || {})) {
1247
+ if (!task.integration?.status || task.integration.status === "pending") continue
1248
+ task.cleanup = {
1249
+ status: "done",
1250
+ cleanedAt: new Date().toISOString(),
1251
+ }
1252
+ }
1253
+ manifest.lastUpdatedAt = new Date().toISOString()
1254
+ fs.writeFileSync(process.env.MANIFEST_PATH, JSON.stringify(manifest, null, 2) + "\n", "utf8")
1255
+ NODE
1256
+
1257
+ printf '%s\n' "$feature_branch"
1258
+ ;;
1259
+
1260
+ print-feature-branch)
1261
+ feature_slug="\${2:-}"
1262
+ [ -n "$feature_slug" ] || fail "Usage: print-feature-branch <feature-slug>"
1263
+ manifest="$(manifest_path "$feature_slug")"
1264
+ if [ -f "$manifest" ]; then
1265
+ json_read_field "$manifest" "featureBranch"
1266
+ else
1267
+ feature_branch_name "$feature_slug"
1268
+ fi
1269
+ printf '\n'
1270
+ ;;
1271
+
1272
+ print-base-branch)
1273
+ feature_slug="\${2:-}"
1274
+ [ -n "$feature_slug" ] || fail "Usage: print-base-branch <feature-slug>"
1275
+ manifest="$(manifest_path "$feature_slug")"
1276
+ [ -f "$manifest" ] || fail "Missing integration manifest: $manifest"
1277
+ json_read_field "$manifest" "baseBranch"
1278
+ printf '\n'
1279
+ ;;
1280
+
1281
+ print-base-ref)
1282
+ feature_slug="\${2:-}"
1283
+ [ -n "$feature_slug" ] || fail "Usage: print-base-ref <feature-slug>"
1284
+ manifest="$(manifest_path "$feature_slug")"
1285
+ [ -f "$manifest" ] || fail "Missing integration manifest: $manifest"
1286
+ if json_read_field "$manifest" "baseRef" >/dev/null 2>&1; then
1287
+ json_read_field "$manifest" "baseRef"
1288
+ else
1289
+ json_read_field "$manifest" "baseBranch"
1290
+ fi
1291
+ printf '\n'
1292
+ ;;
1293
+
1294
+ *)
1295
+ fail "Unknown command: \${cmd:-<empty>}"
1296
+ ;;
1297
+ esac
1298
+ `;
619
1299
  //# sourceMappingURL=support-scripts.js.map