tasks-toolkit 0.1.3__py3-none-any.whl

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 (65) hide show
  1. scripts/__init__.py +0 -0
  2. scripts/agents_sync.py +61 -0
  3. scripts/apply-plan.sh +51 -0
  4. scripts/build.sh +7 -0
  5. scripts/codex-task.sh +198 -0
  6. scripts/install-hooks.sh +9 -0
  7. scripts/junie-status.sh +52 -0
  8. scripts/junie-task.sh +183 -0
  9. scripts/kilo-task.sh +226 -0
  10. scripts/needs-dotnet-checks.sh +24 -0
  11. scripts/plan-sprint.sh +92 -0
  12. scripts/run-task.sh +79 -0
  13. scripts/run.sh +7 -0
  14. scripts/run_stats.py +141 -0
  15. scripts/run_summary.py +94 -0
  16. scripts/spec-lint.sh +139 -0
  17. scripts/sprint-auto.sh +79 -0
  18. scripts/sprint_auto_execute.py +160 -0
  19. scripts/sprint_auto_preflight.py +309 -0
  20. scripts/sprint_auto_prepare.py +271 -0
  21. scripts/sprint_auto_resume.py +153 -0
  22. scripts/sprint_auto_state.py +191 -0
  23. scripts/sprint_plan.py +238 -0
  24. scripts/task-report.sh +45 -0
  25. scripts/tasks.py +1813 -0
  26. scripts/tasks_cli.py +14 -0
  27. scripts/tasks_commands.py +11 -0
  28. scripts/tasks_init.py +140 -0
  29. scripts/tasks_mcp.py +309 -0
  30. scripts/tasks_model.py +173 -0
  31. scripts/tasks_plan_import.py +269 -0
  32. scripts/tasks_render.py +148 -0
  33. scripts/tasks_storage.py +373 -0
  34. scripts/tasks_watch.py +159 -0
  35. scripts/test.sh +43 -0
  36. scripts/test_agent_config.py +97 -0
  37. scripts/test_plan_sprint.sh +172 -0
  38. scripts/test_python_fast.sh +26 -0
  39. scripts/test_python_lint.sh +22 -0
  40. scripts/test_python_slow.sh +14 -0
  41. scripts/test_run_stats.py +64 -0
  42. scripts/test_runners.sh +588 -0
  43. scripts/test_spec_lint.sh +438 -0
  44. scripts/test_sprint_auto.py +50 -0
  45. scripts/test_sprint_auto_execute.py +112 -0
  46. scripts/test_sprint_auto_preflight.py +143 -0
  47. scripts/test_sprint_auto_prepare.py +150 -0
  48. scripts/test_sprint_auto_resume.py +104 -0
  49. scripts/test_sprint_auto_state.py +157 -0
  50. scripts/test_tasks.py +1678 -0
  51. scripts/test_tasks_mcp.py +233 -0
  52. scripts/test_tasks_plan_import.py +129 -0
  53. scripts/test_tasks_storage.py +67 -0
  54. scripts/test_tasks_watch.py +239 -0
  55. scripts/test_worktree.sh +2112 -0
  56. scripts/worktree.sh +992 -0
  57. scripts/worktree_mr.py +133 -0
  58. scripts/worktree_path.py +70 -0
  59. scripts/worktree_sprint.py +70 -0
  60. tasks_toolkit-0.1.3.dist-info/METADATA +121 -0
  61. tasks_toolkit-0.1.3.dist-info/RECORD +65 -0
  62. tasks_toolkit-0.1.3.dist-info/WHEEL +5 -0
  63. tasks_toolkit-0.1.3.dist-info/entry_points.txt +4 -0
  64. tasks_toolkit-0.1.3.dist-info/licenses/LICENSE +21 -0
  65. tasks_toolkit-0.1.3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,588 @@
1
+ #!/usr/bin/env bash
2
+ # Tests for the delegation runners' model recording (junie-task.sh / kilo-task.sh):
3
+ # every status line carries model=/runner=, the final stdout report has runner: and
4
+ # model: header lines, tmp/runs/<name>.summary.json is written with the right verdict,
5
+ # and junie-status.sh reports runner/model from the summary (or the last status line).
6
+ # Plain bash, no external dependencies beyond git, python3 and grep. Each test runs in
7
+ # its own temporary git repo with a fake `junie`/`kilo` executable first in PATH that
8
+ # edits a file and exits 0 or non-zero (see make_repo). Prints PASS/FAIL per case.
9
+ # Exit code: 0 if all pass, 1 if any fail.
10
+ set -uo pipefail
11
+
12
+ passed=0
13
+ failed=0
14
+ total=0
15
+
16
+ pass() { passed=$((passed + 1)); total=$((total + 1)); echo " PASS: $1"; }
17
+ fail() { failed=$((failed + 1)); total=$((total + 1)); echo " FAIL: $1 — $2"; }
18
+
19
+ PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
20
+
21
+ # ── helpers ──────────────────────────────────────────────────────────────
22
+
23
+ # make_repo <name>: build a throw-away git repo with the runner scripts installed,
24
+ # a fake junie/kilo in bin/ (first in PATH), a fake task-report.sh that logs its
25
+ # arguments (so tracker notes are assertable) and a ready "worktree" for <name>.
26
+ make_repo() {
27
+ local name="$1"
28
+ TEST_REPO="$(mktemp -d)"
29
+ cd "$TEST_REPO"
30
+ git init -q
31
+ git config user.name "test"
32
+ git config user.email "test@localhost"
33
+ mkdir -p scripts tmp/worktrees/"$name" tmp/meta tmp/runs tmp/patches tmp/xdg-empty bin
34
+ for f in worktree.sh junie-task.sh kilo-task.sh codex-task.sh run-task.sh junie-status.sh run_summary.py spec-lint.sh; do
35
+ cp "$PROJECT_ROOT/scripts/$f" "scripts/$f"
36
+ done
37
+ mkdir -p agents/junie agents/kilo scripts/coder-git-hooks
38
+ cp "$PROJECT_ROOT/agents/junie/RUN.md" agents/junie/RUN.md
39
+ cp "$PROJECT_ROOT/agents/kilo/RUN.md" agents/kilo/RUN.md
40
+ cp "$PROJECT_ROOT/scripts/coder-git-hooks/prepare-commit-msg" scripts/coder-git-hooks/prepare-commit-msg
41
+ # fake task-report.sh: record every call so the model= in tracker notes is assertable
42
+ cat > scripts/task-report.sh <<'FAKE'
43
+ #!/usr/bin/env bash
44
+ printf '%s\n' "$*" >> "${TASK_REPORT_LOG:?}"
45
+ exit 0
46
+ FAKE
47
+ chmod +x scripts/worktree.sh scripts/junie-task.sh scripts/kilo-task.sh scripts/run-task.sh scripts/junie-status.sh scripts/task-report.sh scripts/coder-git-hooks/prepare-commit-msg
48
+ # Runner artifacts live under tmp/ and must not leak into the agent patch;
49
+ # tmp/worktrees/ stays visible so the fake agent's edits are stageable.
50
+ cat > .gitignore <<'EOF'
51
+ tmp/runs/
52
+ tmp/meta/
53
+ tmp/patches/
54
+ tmp/xdg-empty/
55
+ EOF
56
+ cat > bin/junie <<'FAKE'
57
+ #!/usr/bin/env bash
58
+ # fake agent for tests: writes solution.txt into the agent worktree (given via
59
+ # --project=<dir> or --dir <dir>), then exits 3/5 if the prompt carries EXIT_3/EXIT_5.
60
+ set -u
61
+ wt=""
62
+ shift_wt=0
63
+ for a in "$@"; do
64
+ case "$a" in
65
+ --project=*) wt="${a#--project=}" ;;
66
+ --dir=*) wt="${a#--dir=}" ;;
67
+ --dir) shift_wt=1 ;;
68
+ *)
69
+ if [ "$shift_wt" = "1" ]; then
70
+ wt="$a"
71
+ shift_wt=0
72
+ fi
73
+ ;;
74
+ esac
75
+ done
76
+ [ -n "$wt" ] || exit 9
77
+ if [ -n "${FAKE_KILO_ARGS_LOG:-}" ]; then
78
+ printf '%s\n' "$*" > "$FAKE_KILO_ARGS_LOG"
79
+ fi
80
+ case "$*" in *STALL*) sleep 60; exit 0 ;; esac
81
+ if [ "${FAKE_JUNIE_LINES:-0}" -gt 0 ]; then
82
+ : > "$wt/oversized.txt"
83
+ for ((line = 0; line < FAKE_JUNIE_LINES; line++)); do
84
+ printf 'line %s\n' "$line" >> "$wt/oversized.txt"
85
+ done
86
+ else
87
+ printf 'solution %s\n' "$*" > "$wt/solution.txt"
88
+ fi
89
+ if [ "${FAKE_JUNIE_TRY_COMMIT:-0}" = "1" ]; then
90
+ git -C "$wt" add solution.txt
91
+ commit_rc=0
92
+ git -C "$wt" commit --no-verify -m "agent-owned-commit" \
93
+ > "$wt/../../runs/junie-commit-attempt.log" 2>&1 || commit_rc=$?
94
+ printf '%s\n' "$commit_rc" > "$wt/../../runs/junie-commit-attempt.rc"
95
+ fi
96
+ sleep 2
97
+ case "$*" in
98
+ *EXIT_3*) exit 3 ;;
99
+ *EXIT_5*) exit 5 ;;
100
+ esac
101
+ exit 0
102
+ FAKE
103
+ cp bin/junie bin/kilo
104
+ chmod +x bin/junie bin/kilo
105
+ # fake codex: takes the worktree from `-C <dir>`, writes solution.txt, exits 0/3/5.
106
+ cat > bin/codex <<'FAKE'
107
+ #!/usr/bin/env bash
108
+ set -u
109
+ wt=""
110
+ while [ $# -gt 0 ]; do
111
+ case "$1" in
112
+ -C) wt="$2"; shift 2 ;;
113
+ *) shift ;;
114
+ esac
115
+ done
116
+ [ -n "$wt" ] || exit 9
117
+ case "$*" in *STALL*) sleep 60; exit 0 ;; esac
118
+ printf 'solution %s\n' "$*" > "$wt/solution.txt"
119
+ sleep 1
120
+ case "$*" in
121
+ *EXIT_3*) exit 3 ;;
122
+ *EXIT_5*) exit 5 ;;
123
+ esac
124
+ exit 0
125
+ FAKE
126
+ chmod +x bin/codex
127
+ echo "initial" > solution.txt
128
+ git add -A
129
+ git commit -q -m "initial"
130
+ git rev-parse HEAD > "tmp/meta/$name.base"
131
+ export PATH="$TEST_REPO/bin:$PATH"
132
+ export TASK_REPORT_LOG="$TEST_REPO/tmp/runs/task-report.log"
133
+ }
134
+
135
+ cleanup_repo() {
136
+ rm -rf "$TEST_REPO"
137
+ }
138
+
139
+ # run_runner <name> <junie-task.sh|kilo-task.sh> <model> <verify-cmd> <task-text>
140
+ # Runs the runner; leaves its exit code in $RUNNER_RC.
141
+ run_runner() {
142
+ local name="$1" script="$2" model="$3" verify="$4" text="$5"
143
+ local rc=0
144
+ if [ "$script" = "junie-task.sh" ]; then
145
+ JUNIE_MODEL="$model" JUNIE_VERIFY_CMD="$verify" JUNIE_PING=1 JUNIE_TIMEOUT=120 \
146
+ bash scripts/junie-task.sh "$name" "$text" >"tmp/runs/$name.runner.out" 2>&1 || rc=$?
147
+ else
148
+ KILO_MODEL="$model" KILO_VERIFY_CMD="$verify" KILO_PING=1 KILO_TIMEOUT=120 \
149
+ bash scripts/kilo-task.sh "$name" "$text" >"tmp/runs/$name.runner.out" 2>&1 || rc=$?
150
+ fi
151
+ RUNNER_RC=$rc
152
+ }
153
+
154
+ # json_field <file> <key>: print one field of a JSON summary ("" when absent).
155
+ json_field() {
156
+ python3 -c 'import json,sys; print(json.load(open(sys.argv[1])).get(sys.argv[2], ""))' "$1" "$2"
157
+ }
158
+
159
+ # status_lines_ok <name> <runner>: lines of tmp/runs/<name>.status that do NOT end
160
+ # with the model=/runner= tail (empty when every line carries the tail).
161
+ status_lines_ok() {
162
+ grep -v "model=[^ ]* runner=$2\$" "tmp/runs/$1.status" 2>/dev/null || true
163
+ }
164
+
165
+ # ── test cases ───────────────────────────────────────────────────────────
166
+
167
+ test_success_junie() {
168
+ make_repo ok
169
+ trap cleanup_repo RETURN
170
+
171
+ run_runner ok junie-task.sh junie-model true "write the solution EXIT_0"
172
+
173
+ if [ "$RUNNER_RC" -eq 0 ]; then
174
+ pass "success (junie): runner exit 0"
175
+ else
176
+ fail "success (junie): runner exit" "runner exit $RUNNER_RC, want 0"
177
+ return
178
+ fi
179
+
180
+ local bad
181
+ bad="$(status_lines_ok ok junie)"
182
+ if [ -z "$bad" ]; then
183
+ pass "success (junie): all status lines carry model=/runner=junie"
184
+ else
185
+ fail "success (junie): status lines" "lines without the model/runner tail: $bad"
186
+ return
187
+ fi
188
+
189
+ if python3 -c 'import json,sys; json.load(open(sys.argv[1]))' "tmp/runs/ok.summary.json" 2>/dev/null; then
190
+ pass "success (junie): summary.json is valid JSON"
191
+ else
192
+ fail "success (junie): summary.json" "not valid JSON"
193
+ return
194
+ fi
195
+
196
+ local runner model verdict agent_exit verify_exit files_changed agent
197
+ runner="$(json_field tmp/runs/ok.summary.json runner)"
198
+ model="$(json_field tmp/runs/ok.summary.json model)"
199
+ verdict="$(json_field tmp/runs/ok.summary.json verdict)"
200
+ agent_exit="$(json_field tmp/runs/ok.summary.json agent_exit)"
201
+ verify_exit="$(json_field tmp/runs/ok.summary.json verify_exit)"
202
+ files_changed="$(json_field tmp/runs/ok.summary.json files_changed)"
203
+ agent="$(json_field tmp/runs/ok.summary.json agent)"
204
+ if [ "$runner" = "junie" ] && [ "$model" = "junie-model" ] && [ "$verdict" = "verified" ] \
205
+ && [ "$agent_exit" = "0" ] && [ "$verify_exit" = "0" ] && [ "$files_changed" = "1" ] \
206
+ && [ -z "$agent" ]; then
207
+ pass "success (junie): summary fields"
208
+ else
209
+ fail "success (junie): summary fields" "runner=$runner model=$model verdict=$verdict agent_exit=$agent_exit verify_exit=$verify_exit files_changed=$files_changed agent='$agent'"
210
+ fi
211
+
212
+ local out
213
+ out="$(bash scripts/junie-status.sh ok)"
214
+ if printf '%s\n' "$out" | grep -q '^runner: junie$' && printf '%s\n' "$out" | grep -q '^model: junie-model$'; then
215
+ pass "success (junie): junie-status.sh prints runner/model"
216
+ else
217
+ fail "success (junie): junie-status.sh" "output missing runner/model: $out"
218
+ fi
219
+
220
+ if grep -q 'model=junie-model' "tmp/runs/task-report.log" 2>/dev/null; then
221
+ pass "success (junie): tracker note includes model="
222
+ else
223
+ fail "success (junie): tracker note" "note log lacks model=: $(cat tmp/runs/task-report.log 2>/dev/null || true)"
224
+ fi
225
+ }
226
+
227
+ test_junie_commit_is_blocked_but_parent_runner_commits() {
228
+ make_repo guarded
229
+ trap cleanup_repo RETURN
230
+
231
+ local rc=0 attempt_rc attempt_log subject body commit_count
232
+ FAKE_JUNIE_TRY_COMMIT=1 JUNIE_MODEL=guard-model JUNIE_VERIFY_CMD=true JUNIE_PING=1 JUNIE_TIMEOUT=120 \
233
+ bash scripts/junie-task.sh guarded "write the solution" >"tmp/runs/guarded.runner.out" 2>&1 || rc=$?
234
+ attempt_rc="$(cat tmp/runs/junie-commit-attempt.rc 2>/dev/null || true)"
235
+ attempt_log="$(cat tmp/runs/junie-commit-attempt.log 2>/dev/null || true)"
236
+ subject="$(git -C tmp/worktrees/guarded log -1 --format=%s 2>/dev/null || true)"
237
+ body="$(git -C tmp/worktrees/guarded log -1 --format=%B 2>/dev/null || true)"
238
+ commit_count="$(git rev-list --count HEAD)"
239
+ if [ "$rc" -eq 0 ] && [ "$attempt_rc" -ne 0 ] \
240
+ && printf '%s\n' "$attempt_log" | grep -q 'Task coder agents may not commit' \
241
+ && [ "$subject" != "agent-owned-commit" ] && [ "$commit_count" -eq 2 ] \
242
+ && printf '%s\n' "$body" | grep -q '^runner: junie$' \
243
+ && printf '%s\n' "$body" | grep -q '^model: guard-model$'; then
244
+ pass "Junie --no-verify commit is blocked; parent runner commits with trailers"
245
+ else
246
+ fail "Junie commit guard" "rc=$rc attempt=$attempt_rc subject='$subject' count=$commit_count"
247
+ fi
248
+ }
249
+
250
+ test_kilo_commit_is_blocked_but_parent_runner_commits() {
251
+ make_repo guarded-kilo
252
+ trap cleanup_repo RETURN
253
+
254
+ local rc=0 attempt_rc attempt_log subject body commit_count
255
+ FAKE_JUNIE_TRY_COMMIT=1 KILO_MODEL=guard-model KILO_VERIFY_CMD=true KILO_PING=1 KILO_TIMEOUT=120 \
256
+ bash scripts/kilo-task.sh guarded-kilo "write the solution" >"tmp/runs/guarded-kilo.runner.out" 2>&1 || rc=$?
257
+ attempt_rc="$(cat tmp/runs/junie-commit-attempt.rc 2>/dev/null || true)"
258
+ attempt_log="$(cat tmp/runs/junie-commit-attempt.log 2>/dev/null || true)"
259
+ subject="$(git -C tmp/worktrees/guarded-kilo log -1 --format=%s 2>/dev/null || true)"
260
+ body="$(git -C tmp/worktrees/guarded-kilo log -1 --format=%B 2>/dev/null || true)"
261
+ commit_count="$(git rev-list --count HEAD)"
262
+ if [ "$rc" -eq 0 ] && [ "$attempt_rc" -ne 0 ] \
263
+ && printf '%s\n' "$attempt_log" | grep -q 'Task coder agents may not commit' \
264
+ && [ "$subject" != "agent-owned-commit" ] && [ "$commit_count" -eq 2 ] \
265
+ && printf '%s\n' "$body" | grep -q '^runner: kilo$' \
266
+ && printf '%s\n' "$body" | grep -q '^model: guard-model$'; then
267
+ pass "Kilo --no-verify commit is blocked; parent runner commits with trailers"
268
+ else
269
+ fail "Kilo commit guard" "rc=$rc attempt=$attempt_rc subject='$subject' count=$commit_count"
270
+ fi
271
+ }
272
+
273
+ test_kilo_uses_configured_model_when_unset() {
274
+ make_repo configured-model
275
+ trap cleanup_repo RETURN
276
+
277
+ local rc=0 reported_model
278
+ FAKE_KILO_ARGS_LOG="$TEST_REPO/tmp/runs/kilo.args" KILO_VERIFY_CMD=true KILO_PING=1 KILO_TIMEOUT=120 \
279
+ bash scripts/kilo-task.sh configured-model "write the solution" >"tmp/runs/configured-model.runner.out" 2>&1 || rc=$?
280
+ reported_model="$(json_field tmp/runs/configured-model.summary.json model)"
281
+ if [ "$rc" -eq 0 ] && [ "$reported_model" = "configured-default" ] \
282
+ && ! grep -Eq '(^| )-m( |$)|--model' tmp/runs/kilo.args; then
283
+ pass "Kilo leaves model selection to its configuration when KILO_MODEL is unset"
284
+ else
285
+ fail "Kilo configured model" "rc=$rc model=$reported_model args=$(cat tmp/runs/kilo.args 2>/dev/null)"
286
+ fi
287
+ }
288
+
289
+ test_kilo_oversized_diff_is_preserved_without_commit() {
290
+ make_repo oversized-kilo
291
+ trap cleanup_repo RETURN
292
+
293
+ local rc=0 base head commit_log patch_lines
294
+ FAKE_JUNIE_LINES=301 KILO_MODEL=large-model KILO_VERIFY_CMD=true KILO_PING=1 KILO_TIMEOUT=120 \
295
+ bash scripts/kilo-task.sh oversized-kilo "write an oversized solution" >"tmp/runs/oversized-kilo.runner.out" 2>&1 || rc=$?
296
+ base="$(cat tmp/meta/oversized-kilo.base)"
297
+ head="$(git rev-parse HEAD)"
298
+ commit_log="$(cat tmp/runs/oversized-kilo.commit.log 2>/dev/null || true)"
299
+ patch_lines="$(wc -l < tmp/patches/oversized-kilo.patch 2>/dev/null || printf '0')"
300
+ if [ "$rc" -eq 1 ] && [ "$head" = "$base" ] \
301
+ && printf '%s\n' "$commit_log" | grep -q '301 added/deleted lines (limit 300)' \
302
+ && [ -f tmp/worktrees/oversized-kilo/oversized.txt ] \
303
+ && [ "$(wc -l < tmp/worktrees/oversized-kilo/oversized.txt)" -eq 301 ] \
304
+ && [ "$patch_lines" -gt 0 ]; then
305
+ pass "Kilo diff over 300 lines is not committed and worktree/patch are preserved"
306
+ else
307
+ fail "Kilo 300-line guard" "rc=$rc head=$head base=$base log='$commit_log' patch_lines=$patch_lines"
308
+ fi
309
+ }
310
+
311
+ test_junie_run_policy_is_explicit() {
312
+ if grep -qi 'Never perform Git writes' "$PROJECT_ROOT/agents/junie/RUN.md" \
313
+ && grep -qi '300 added plus deleted' "$PROJECT_ROOT/agents/junie/RUN.md" \
314
+ && grep -qi 'never run Git write commands' "$PROJECT_ROOT/agents/junie/INSTRUCTIONS.md" \
315
+ && grep -qi 'Never perform Git writes' "$PROJECT_ROOT/agents/kilo/RUN.md" \
316
+ && grep -qi '300 added plus deleted' "$PROJECT_ROOT/agents/kilo/RUN.md" \
317
+ && grep -qi 'never run Git write commands' "$PROJECT_ROOT/agents/kilo/INSTRUCTIONS.md"; then
318
+ pass "Junie and Kilo instructions forbid Git writes and require pre-edit scope limits"
319
+ else
320
+ fail "Junie run policy" "missing no-write or 300-line instruction"
321
+ fi
322
+ }
323
+
324
+ test_success_codex() {
325
+ make_repo "cx"
326
+ trap cleanup_repo RETURN
327
+ printf 'write the solution EXIT_0\n' > spec.md
328
+
329
+ local rc=0
330
+ CODEX_VERIFY_CMD=true CODEX_PING=1 CODEX_TIMEOUT=120 \
331
+ bash scripts/codex-task.sh cx --file spec.md >/dev/null 2>&1 || rc=$?
332
+ local verdict runner model
333
+ verdict="$(json_field tmp/runs/cx.summary.json verdict)"
334
+ runner="$(json_field tmp/runs/cx.summary.json runner)"
335
+ model="$(json_field tmp/runs/cx.summary.json model)"
336
+ if [ "$rc" -eq 0 ] && [ "$verdict" = "verified" ] && [ "$runner" = "codex" ] && [ "$model" = "codex" ]; then
337
+ pass "codex runner: exit 0, verified, runner/model recorded"
338
+ else
339
+ fail "codex runner" "rc=$rc verdict=$verdict runner=$runner model=$model"
340
+ fi
341
+ }
342
+
343
+ test_agent_failure_kilo() {
344
+ make_repo agent-fail
345
+ trap cleanup_repo RETURN
346
+
347
+ run_runner agent-fail kilo-task.sh kilo-model true "make it fail EXIT_3"
348
+
349
+ if [ "$RUNNER_RC" -eq 3 ]; then
350
+ pass "agent failure (kilo): runner exit 3"
351
+ else
352
+ fail "agent failure (kilo): runner exit" "runner exit $RUNNER_RC, want 3"
353
+ return
354
+ fi
355
+
356
+ local bad
357
+ bad="$(status_lines_ok agent-fail kilo)"
358
+ if [ -z "$bad" ]; then
359
+ pass "agent failure (kilo): all status lines carry model=/runner=kilo"
360
+ else
361
+ fail "agent failure (kilo): status lines" "lines without the model/runner tail: $bad"
362
+ fi
363
+
364
+ local runner model verdict agent_exit verify_exit agent
365
+ runner="$(json_field tmp/runs/agent-fail.summary.json runner)"
366
+ model="$(json_field tmp/runs/agent-fail.summary.json model)"
367
+ verdict="$(json_field tmp/runs/agent-fail.summary.json verdict)"
368
+ agent_exit="$(json_field tmp/runs/agent-fail.summary.json agent_exit)"
369
+ verify_exit="$(json_field tmp/runs/agent-fail.summary.json verify_exit)"
370
+ agent="$(json_field tmp/runs/agent-fail.summary.json agent)"
371
+ if [ "$runner" = "kilo" ] && [ "$model" = "kilo-model" ] && [ "$verdict" = "agent_failed" ] \
372
+ && [ "$agent_exit" = "3" ] && [ "$verify_exit" = "0" ] && [ "$agent" = "code" ]; then
373
+ pass "agent failure (kilo): summary fields (incl. agent)"
374
+ else
375
+ fail "agent failure (kilo): summary fields" "runner=$runner model=$model verdict=$verdict agent_exit=$agent_exit verify_exit=$verify_exit agent='$agent'"
376
+ fi
377
+
378
+ if grep -q 'model=kilo-model' "tmp/runs/task-report.log" 2>/dev/null; then
379
+ pass "agent failure (kilo): tracker note includes model="
380
+ else
381
+ fail "agent failure (kilo): tracker note" "note log lacks model=: $(cat tmp/runs/task-report.log 2>/dev/null || true)"
382
+ fi
383
+ }
384
+
385
+ test_verify_failure_junie() {
386
+ make_repo verify-fail
387
+ trap cleanup_repo RETURN
388
+
389
+ run_runner verify-fail junie-task.sh junie-model false "write the solution EXIT_0"
390
+
391
+ if [ "$RUNNER_RC" -eq 1 ]; then
392
+ pass "verification failure (junie): runner exit 1"
393
+ else
394
+ fail "verification failure (junie): runner exit" "runner exit $RUNNER_RC, want 1"
395
+ return
396
+ fi
397
+
398
+ local verdict agent_exit verify_exit
399
+ verdict="$(json_field tmp/runs/verify-fail.summary.json verdict)"
400
+ agent_exit="$(json_field tmp/runs/verify-fail.summary.json agent_exit)"
401
+ verify_exit="$(json_field tmp/runs/verify-fail.summary.json verify_exit)"
402
+ if [ "$verdict" = "verify_failed" ] && [ "$agent_exit" = "0" ] && [ "$verify_exit" = "1" ]; then
403
+ pass "verification failure (junie): summary fields"
404
+ else
405
+ fail "verification failure (junie): summary fields" "verdict=$verdict agent_exit=$agent_exit verify_exit=$verify_exit"
406
+ fi
407
+ }
408
+
409
+ test_status_fallback() {
410
+ # No summary yet: junie-status.sh takes runner/model from the last status line.
411
+ make_repo fb
412
+ trap cleanup_repo RETURN
413
+
414
+ echo "[00:00] junie running - 1 file(s) changed; last: working model=junie-lite runner=junie" > "tmp/runs/fb.status"
415
+ touch "tmp/runs/fb.stdout.log"
416
+ local out
417
+ out="$(bash scripts/junie-status.sh fb)"
418
+ if printf '%s\n' "$out" | grep -q '^runner: junie$' && printf '%s\n' "$out" | grep -q '^model: junie-lite$'; then
419
+ pass "junie-status.sh: fallback to last status line"
420
+ else
421
+ fail "junie-status.sh: fallback" "output: $out"
422
+ fi
423
+ }
424
+
425
+ test_prechange_run() {
426
+ # Runs that predate model recording have no summary and no model= in the status
427
+ # file; junie-status.sh must still work and print no runner/model lines.
428
+ make_repo pre
429
+ trap cleanup_repo RETURN
430
+
431
+ echo "[00:00] starting - worktree=/x" > "tmp/runs/pre.status"
432
+ touch "tmp/runs/pre.stdout.log"
433
+ local rc=0 out
434
+ out="$(bash scripts/junie-status.sh pre)" || rc=$?
435
+ if [ "$rc" -eq 0 ] && ! printf '%s\n' "$out" | grep -q '^runner:'; then
436
+ pass "junie-status.sh: pre-change run still works"
437
+ else
438
+ fail "junie-status.sh: pre-change run" "rc=$rc output: $out"
439
+ fi
440
+ }
441
+
442
+ # The runner closes the tracker task itself after the commit and verification:
443
+ # green -> done, red -> blocked (with the failing check as reason).
444
+ test_close_status_after_commit() {
445
+ make_repo "close"
446
+ trap cleanup_repo RETURN
447
+ printf 'write the solution EXIT_0\n' > spec.md
448
+
449
+ JUNIE_MODEL=jm JUNIE_VERIFY_CMD=true JUNIE_PING=1 JUNIE_TIMEOUT=120 \
450
+ bash scripts/junie-task.sh close --file spec.md --task-id 7.9 >/dev/null 2>&1 || true
451
+ if grep -q '^close 7.9 green' tmp/runs/task-report.log 2>/dev/null; then
452
+ pass "runner closes the task green after a verified commit"
453
+ else
454
+ fail "runner close green" "log: $(cat tmp/runs/task-report.log 2>/dev/null)"
455
+ fi
456
+
457
+ : > tmp/runs/task-report.log
458
+ JUNIE_MODEL=jm JUNIE_VERIFY_CMD=false JUNIE_PING=1 JUNIE_TIMEOUT=120 \
459
+ bash scripts/junie-task.sh close --file spec.md --task-id 7.9 >/dev/null 2>&1 || true
460
+ if grep -q '^close 7.9 red ' tmp/runs/task-report.log 2>/dev/null; then
461
+ pass "runner closes the task red (blocked) after a failed verification"
462
+ else
463
+ fail "runner close red" "log: $(cat tmp/runs/task-report.log 2>/dev/null)"
464
+ fi
465
+ }
466
+
467
+ # Real task-report.sh: `close green` maps to done, `close red <reason>` to blocked with the reason.
468
+ test_task_report_close() {
469
+ local root out; root="$(mktemp -d)"
470
+ (
471
+ export VIBE_TASKS_ROOT="$root"
472
+ python3 "$PROJECT_ROOT/scripts/tasks_init.py" --path "$root/tasks/tasks.db" --project T >/dev/null 2>&1
473
+ python3 "$PROJECT_ROOT/scripts/tasks.py" add-sprint S >/dev/null 2>&1
474
+ python3 "$PROJECT_ROOT/scripts/tasks.py" add 1 T1 >/dev/null 2>&1
475
+ bash "$PROJECT_ROOT/scripts/task-report.sh" close 1.1 green >/dev/null 2>&1
476
+ python3 "$PROJECT_ROOT/scripts/tasks.py" show 1.1 > "$root/green.json" 2>/dev/null
477
+ python3 "$PROJECT_ROOT/scripts/tasks.py" reset 1.1 >/dev/null 2>&1
478
+ bash "$PROJECT_ROOT/scripts/task-report.sh" close 1.1 red "bad check" >/dev/null 2>&1
479
+ python3 "$PROJECT_ROOT/scripts/tasks.py" show 1.1 > "$root/red.json" 2>/dev/null
480
+ )
481
+ out="$(python3 -c 'import json,sys; d=sys.argv[1]; g=json.load(open(d+"/green.json")); r=json.load(open(d+"/red.json")); print(g["effective_status"], r["effective_status"], r["reason"])' "$root")"
482
+ if [ "$out" = "done blocked bad check" ]; then
483
+ pass "task-report close: green -> done, red -> blocked with reason"
484
+ else
485
+ fail "task-report close mapping" "got: '$out'"
486
+ fi
487
+ rm -rf "$root"
488
+ }
489
+
490
+ # ── main ─────────────────────────────────────────────────────────────────
491
+
492
+ echo "=== runner model recording tests ==="
493
+
494
+ # These tests use minimal spec files that do not follow the spec template;
495
+ # disable spec-lint so the existing assertions remain valid.
496
+ export SPEC_LINT=off
497
+
498
+ # A stuck agent (no file changed after RUNNER_STALL seconds) is killed and reported.
499
+ test_stall_detector() {
500
+ local script
501
+ for script in junie-task.sh kilo-task.sh; do
502
+ make_repo "stall"
503
+ local start=$SECONDS rc=0
504
+ if [ "$script" = "junie-task.sh" ]; then
505
+ RUNNER_STALL=2 JUNIE_PING=1 JUNIE_TIMEOUT=120 JUNIE_VERIFY_CMD=true \
506
+ bash scripts/junie-task.sh stall "do nothing STALL" >tmp/runs/stall.runner.out 2>&1 || rc=$?
507
+ else
508
+ RUNNER_STALL=2 KILO_PING=1 KILO_TIMEOUT=120 KILO_VERIFY_CMD=true \
509
+ bash scripts/kilo-task.sh stall "do nothing STALL" >tmp/runs/stall.runner.out 2>&1 || rc=$?
510
+ fi
511
+ local took=$((SECONDS - start))
512
+ if [ "$rc" -eq 3 ] && grep -q "stalled" tmp/runs/stall.status tmp/runs/stall.runner.out && [ "$took" -lt 40 ]; then
513
+ pass "stall detector ($script): killed, exit 3, 'stalled' reported in ${took}s"
514
+ else
515
+ fail "stall detector ($script)" "rc=$rc took=${took}s status=$(tail -1 tmp/runs/stall.status 2>/dev/null)"
516
+ fi
517
+ cleanup_repo
518
+ done
519
+ }
520
+
521
+ # run-task.sh: junie fails (exit 3) -> exactly one retry with kilo, which succeeds; a
522
+ # verification failure is NOT retried; fallback=none keeps exit 3.
523
+ test_run_task_fallback() {
524
+ make_repo "fb"
525
+ sed '/EXIT_3/d;/EXIT_5/d' bin/junie > bin/kilo; chmod +x bin/kilo
526
+ printf 'make it fail EXIT_3\n' > spec.md
527
+ local rc=0
528
+ JUNIE_MODEL=jm KILO_MODEL=km JUNIE_VERIFY_CMD=true KILO_VERIFY_CMD=true JUNIE_PING=1 KILO_PING=1 \
529
+ bash scripts/run-task.sh fb --file spec.md --runner junie --fallback kilo >tmp/runs/fb.rt.out 2>&1 || rc=$?
530
+ local attempts; attempts="$(cat tmp/runs/fb.attempts 2>/dev/null)"
531
+ if [ "$rc" -eq 0 ] && [ "$(printf '%s\n' "$attempts" | wc -l)" -eq 2 ] \
532
+ && printf '%s' "$attempts" | grep -q "runner=junie exit=3" && printf '%s' "$attempts" | grep -q "runner=kilo exit=0"; then
533
+ pass "run-task: junie failure falls back to kilo once, overall exit 0, both attempts recorded"
534
+ else
535
+ fail "run-task fallback" "rc=$rc attempts=$attempts"
536
+ fi
537
+ cleanup_repo
538
+
539
+ make_repo "default-codex"
540
+ printf 'write a successful solution\n' > spec.md; rc=0
541
+ CODEX_VERIFY_CMD=true CODEX_PING=1 \
542
+ bash scripts/run-task.sh default-codex --file spec.md >tmp/runs/default-codex.rt.out 2>&1 || rc=$?
543
+ local default_attempts default_runner
544
+ default_attempts="$(cat tmp/runs/default-codex.attempts 2>/dev/null)"
545
+ default_runner="$(json_field tmp/runs/default-codex.summary.json runner)"
546
+ if [ "$rc" -eq 0 ] && [ "$(printf '%s\n' "$default_attempts" | wc -l)" -eq 1 ] \
547
+ && printf '%s' "$default_attempts" | grep -q 'runner=codex exit=0' \
548
+ && [ "$default_runner" = "codex" ]; then
549
+ pass "run-task: default coder is Codex, single attempt, no fallback"
550
+ else
551
+ fail "run-task default Codex" "rc=$rc runner=$default_runner attempts=$default_attempts"
552
+ fi
553
+ cleanup_repo
554
+
555
+ make_repo "vf"
556
+ printf 'write it EXIT_0\n' > spec.md; rc=0
557
+ JUNIE_MODEL=jm JUNIE_VERIFY_CMD=false JUNIE_PING=1 bash scripts/run-task.sh vf --file spec.md >/dev/null 2>&1 || rc=$?
558
+ if [ "$rc" -eq 1 ] && [ "$(wc -l < tmp/runs/vf.attempts)" -eq 1 ]; then
559
+ pass "run-task: verification failure (exit 1) is not retried on another runner"
560
+ else
561
+ fail "run-task no retry on verify failure" "rc=$rc"
562
+ fi
563
+ cleanup_repo
564
+ }
565
+
566
+ test_success_junie
567
+ test_junie_commit_is_blocked_but_parent_runner_commits
568
+ test_kilo_commit_is_blocked_but_parent_runner_commits
569
+ test_kilo_uses_configured_model_when_unset
570
+ test_kilo_oversized_diff_is_preserved_without_commit
571
+ test_junie_run_policy_is_explicit
572
+ test_success_codex
573
+ test_agent_failure_kilo
574
+ test_verify_failure_junie
575
+ test_status_fallback
576
+ test_prechange_run
577
+ test_stall_detector
578
+ test_run_task_fallback
579
+ test_close_status_after_commit
580
+ test_task_report_close
581
+
582
+ echo ""
583
+ echo "=== Results: $passed/$total passed, $failed failed ==="
584
+
585
+ if [ "$failed" -gt 0 ]; then
586
+ exit 1
587
+ fi
588
+ exit 0