@iceinvein/agent-skills 0.22.0 → 0.23.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.
- package/README.md +1 -1
- package/package.json +1 -1
- package/skills/bug-hunter/SKILL.md +5 -0
- package/skills/bug-hunter/evals/README.md +6 -1
- package/skills/bug-hunter/evals/callers-tree-unchanged/graders/output-ignores-itself.md +11 -0
- package/skills/bug-hunter/evals/rules.json +8 -0
- package/skills/bug-hunter/skill.json +1 -1
- package/skills/index.json +4 -4
- package/skills/magpie/README.md +5 -3
- package/skills/magpie/SKILL.md +40 -15
- package/skills/magpie/bin/magpie.ts +83 -7
- package/skills/magpie/evals/README.md +7 -4
- package/skills/magpie/evals/local-mode-hands-back/case.yaml +4 -0
- package/skills/magpie/evals/local-mode-hands-back/fixture.sh +300 -0
- package/skills/magpie/evals/local-mode-hands-back/graders/branch-was-reachable.md +5 -0
- package/skills/magpie/evals/local-mode-hands-back/graders/cleanup-after-handoff.md +6 -0
- package/skills/magpie/evals/local-mode-hands-back/graders/handoff-was-run.md +6 -0
- package/skills/magpie/evals/local-mode-hands-back/graders/nothing-served-or-posted.md +7 -0
- package/skills/magpie/evals/local-mode-hands-back/graders/reply-lists-both-findings.md +10 -0
- package/skills/magpie/evals/local-mode-hands-back/graders/skill-fired.md +5 -0
- package/skills/magpie/evals/local-mode-hands-back/prompt.md +11 -0
- package/skills/magpie/evals/quality/README.md +73 -2
- package/skills/magpie/evals/quality/__tests__/recall.test.ts +234 -0
- package/skills/magpie/evals/quality/__tests__/replay-full.test.ts +164 -0
- package/skills/magpie/evals/quality/corpus.ts +4 -1
- package/skills/magpie/evals/quality/recall.ts +249 -0
- package/skills/magpie/evals/quality/replay-critic.ts +3 -0
- package/skills/magpie/evals/quality/replay-full.ts +168 -6
- package/skills/magpie/evals/rules.json +9 -1
- package/skills/magpie/references/checks.md +78 -0
- package/skills/magpie/references/peer-review.md +7 -0
- package/skills/magpie/references/scout.md +12 -5
- package/skills/magpie/references/specialists.md +10 -8
- package/skills/magpie/scripts/__tests__/cleanup-cmd.test.ts +94 -0
- package/skills/magpie/scripts/__tests__/critic-cmd.test.ts +18 -0
- package/skills/magpie/scripts/__tests__/dedupe-cmd.test.ts +80 -1
- package/skills/magpie/scripts/__tests__/handoff-cmd.test.ts +319 -0
- package/skills/magpie/scripts/__tests__/housekeeping-cmd.test.ts +14 -0
- package/skills/magpie/scripts/__tests__/local-diff.test.ts +275 -0
- package/skills/magpie/scripts/__tests__/pipeline-e2e.test.ts +1 -0
- package/skills/magpie/scripts/__tests__/resolve-run.test.ts +28 -0
- package/skills/magpie/scripts/__tests__/server.test.ts +30 -1
- package/skills/magpie/scripts/__tests__/setup-cmd.test.ts +324 -1
- package/skills/magpie/scripts/__tests__/skill-lint.test.ts +8 -5
- package/skills/magpie/scripts/__tests__/status-cmd.test.ts +44 -0
- package/skills/magpie/scripts/__tests__/target.test.ts +84 -0
- package/skills/magpie/scripts/__tests__/types.test.ts +2 -1
- package/skills/magpie/scripts/cleanup-cmd.ts +5 -1
- package/skills/magpie/scripts/critic-cmd.ts +2 -1
- package/skills/magpie/scripts/dedupe-cmd.ts +15 -5
- package/skills/magpie/scripts/handoff-cmd.ts +132 -0
- package/skills/magpie/scripts/housekeeping-cmd.ts +7 -5
- package/skills/magpie/scripts/local-diff.ts +228 -0
- package/skills/magpie/scripts/post-cmd.ts +1 -0
- package/skills/magpie/scripts/render-annotation.ts +1 -0
- package/skills/magpie/scripts/resolve-run.ts +17 -3
- package/skills/magpie/scripts/server.ts +9 -1
- package/skills/magpie/scripts/setup-cmd.ts +241 -49
- package/skills/magpie/scripts/status-cmd.ts +22 -6
- package/skills/magpie/scripts/target.ts +56 -0
- package/skills/magpie/scripts/types.ts +1 -0
- package/skills/magpie/skill.json +2 -2
- package/skills/sluice/SKILL.md +3 -1
- package/skills/sluice/evals/README.md +10 -2
- package/skills/sluice/evals/fast-leaves-an-unrelated-red-test-alone/case.yaml +4 -0
- package/skills/sluice/evals/fast-leaves-an-unrelated-red-test-alone/fixture.sh +108 -0
- package/skills/sluice/evals/fast-leaves-an-unrelated-red-test-alone/graders/deploy-names-its-steps.md +9 -0
- package/skills/sluice/evals/fast-leaves-an-unrelated-red-test-alone/graders/kept-status-by-changing-code.md +12 -0
- package/skills/sluice/evals/fast-leaves-an-unrelated-red-test-alone/graders/sluice-fired.md +5 -0
- package/skills/sluice/evals/fast-leaves-an-unrelated-red-test-alone/graders/status-test-unchanged.md +6 -0
- package/skills/sluice/evals/fast-leaves-an-unrelated-red-test-alone/prompt.md +11 -0
- package/skills/sluice/evals/fast-updates-the-test-the-change-replaces/case.yaml +4 -0
- package/skills/sluice/evals/fast-updates-the-test-the-change-replaces/fixture.sh +73 -0
- package/skills/sluice/evals/fast-updates-the-test-the-change-replaces/graders/proved-after-the-last-edit.md +18 -0
- package/skills/sluice/evals/fast-updates-the-test-the-change-replaces/graders/sluice-fired.md +5 -0
- package/skills/sluice/evals/fast-updates-the-test-the-change-replaces/graders/test-expects-the-new-line.md +5 -0
- package/skills/sluice/evals/fast-updates-the-test-the-change-replaces/graders/updated-without-asking.md +11 -0
- package/skills/sluice/evals/fast-updates-the-test-the-change-replaces/prompt.md +11 -0
- package/skills/sluice/evals/rules.json +11 -0
- package/skills/sluice/references/review.md +6 -4
- package/skills/sluice/references/test-first.md +24 -4
- package/skills/sluice/scripts/content-digest.sh +1 -1
- package/skills/sluice/scripts/run-stats.sh +14 -0
- package/skills/sluice/scripts/session-start.sh +44 -2
- package/skills/sluice/scripts/status.sh +293 -0
- package/skills/sluice/scripts/stop-guard.sh +80 -6
- package/skills/sluice/scripts/test-changes.sh +135 -0
- package/skills/sluice/skill.json +1 -1
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
# status.sh close
|
|
24
24
|
# status.sh route <fast|main|deep>
|
|
25
25
|
# status.sh verify -- <cmd> [args...] | verify --none <reason>
|
|
26
|
+
# status.sh tests-changed --because <t> [--base <ref>] [--only <path>]... {[--with <path>]... -- <cmd> | --none <reason>}
|
|
26
27
|
#
|
|
27
28
|
# --dir <path> selects the tree to read (default: $PWD). State lives at
|
|
28
29
|
# <dir>/.sluice/run.json and closed runs at <dir>/.sluice/archive/. A tree the
|
|
@@ -37,6 +38,10 @@
|
|
|
37
38
|
# `line` is exempt and always exits 0 in silence, because a statusline renders
|
|
38
39
|
# on every keystroke and has nowhere to put an error. `verify -- <cmd>` exits
|
|
39
40
|
# with the command's own code, so a failing check reads as that failure.
|
|
41
|
+
# `tests-changed` adds 7, a flagged test passes on the old code, and 8, the
|
|
42
|
+
# command failed on the old code with the flagged tests at their old version;
|
|
43
|
+
# neither records anything. Its exit 1 also covers a tree whose changed tests
|
|
44
|
+
# cannot be read.
|
|
40
45
|
#
|
|
41
46
|
# A flag value may not begin with `--`: unchecked, an omitted value silently
|
|
42
47
|
# becomes the next flag and the field holds a flag name.
|
|
@@ -406,6 +411,294 @@ if [ "$SUB" = "verify" ]; then
|
|
|
406
411
|
exit 0
|
|
407
412
|
fi
|
|
408
413
|
|
|
414
|
+
# Records why the session's changed tests are allowed to ask what they now ask,
|
|
415
|
+
# for the test-change gate. A changed test is proved when it fails on the code
|
|
416
|
+
# it was changed against: one that still passes there does not pin the change.
|
|
417
|
+
# The runs happen in a scratch worktree at the base, so the partner's tree is
|
|
418
|
+
# never touched. Like verify it needs no run and reads ORIG_DIR.
|
|
419
|
+
if [ "$SUB" = "tests-changed" ]; then
|
|
420
|
+
TREE="$(git -C "$ORIG_DIR" rev-parse --show-toplevel 2>/dev/null)"
|
|
421
|
+
[ -n "$TREE" ] || { err "tests-changed needs a git work tree, and $ORIG_DIR is not in one"; exit 4; }
|
|
422
|
+
BECAUSE=""
|
|
423
|
+
BASE=""
|
|
424
|
+
REASON=""
|
|
425
|
+
NONE=0
|
|
426
|
+
RUN=0
|
|
427
|
+
WITH=()
|
|
428
|
+
ONLY=""
|
|
429
|
+
while [ $# -gt 0 ]; do
|
|
430
|
+
case "$1" in
|
|
431
|
+
--only) need_value --only $# "${2-}"; ONLY="$ONLY$2"$'\n'; shift 2 ;;
|
|
432
|
+
--because) need_value --because $# "${2-}"; BECAUSE="$2"; shift 2 ;;
|
|
433
|
+
--base) need_value --base $# "${2-}"; BASE="$2"; shift 2 ;;
|
|
434
|
+
--with) need_value --with $# "${2-}"; WITH+=("$2"); shift 2 ;;
|
|
435
|
+
--none) need_value --none $# "${2-}"; REASON="$2"; NONE=1; shift 2 ;;
|
|
436
|
+
--) shift; RUN=1; break ;;
|
|
437
|
+
*) err "tests-changed: unknown argument $1"; exit 4 ;;
|
|
438
|
+
esac
|
|
439
|
+
done
|
|
440
|
+
[ -n "$BECAUSE" ] || { err "tests-changed needs --because <text>"; exit 4; }
|
|
441
|
+
if [ "$RUN" -eq "$NONE" ]; then
|
|
442
|
+
err "tests-changed needs one of -- <cmd> [args...] or --none <reason>"
|
|
443
|
+
exit 4
|
|
444
|
+
fi
|
|
445
|
+
[ "$RUN" -eq 0 ] || [ $# -gt 0 ] || { err "tests-changed -- needs a command to run"; exit 4; }
|
|
446
|
+
[ "$NONE" -eq 0 ] || [ -n "$REASON" ] || { err "tests-changed --none needs a reason"; exit 4; }
|
|
447
|
+
[ "$NONE" -eq 0 ] || [ "${#WITH[@]}" -eq 0 ] || { err "--with only applies to a run, not to --none"; exit 4; }
|
|
448
|
+
for p in "${WITH[@]+"${WITH[@]}"}"; do
|
|
449
|
+
# The link has to land inside the scratch worktree and point inside the tree.
|
|
450
|
+
case "/$p/" in
|
|
451
|
+
//* | */../*) err "--with takes a path inside the tree, and $p is not one"; exit 4 ;;
|
|
452
|
+
esac
|
|
453
|
+
[ -e "$TREE/$p" ] || { err "--with $p: no such path in $TREE"; exit 4; }
|
|
454
|
+
done
|
|
455
|
+
# A command that names the tree or the main checkout by absolute path runs
|
|
456
|
+
# their files, not the scratch worktree's, so both runs would run the new
|
|
457
|
+
# code. Compared after resolving symlinks, since git reports physical paths
|
|
458
|
+
# (/private/var on macOS) and a caller usually passes logical ones.
|
|
459
|
+
if [ "$RUN" -eq 1 ]; then
|
|
460
|
+
ROOTS="$(cd "$TREE" && pwd -P)"
|
|
461
|
+
# The first entry of the list is the main checkout, unless the repo is
|
|
462
|
+
# bare: then it is the git dir, and its parent holds no checkout at all.
|
|
463
|
+
FIRST="$(git -C "$TREE" worktree list --porcelain 2>/dev/null | sed '/^$/q')"
|
|
464
|
+
[ -n "$FIRST" ] || { err "could not list the worktrees of $TREE"; exit 1; }
|
|
465
|
+
case $'\n'"$FIRST"$'\n' in
|
|
466
|
+
*$'\n'bare$'\n'*) ;;
|
|
467
|
+
*)
|
|
468
|
+
MAIN_ROOT="$(printf '%s\n' "$FIRST" | sed -n '1s/^worktree //p')"
|
|
469
|
+
[ -d "$MAIN_ROOT" ] && ROOTS="$ROOTS
|
|
470
|
+
$(cd "$MAIN_ROOT" && pwd -P)"
|
|
471
|
+
;;
|
|
472
|
+
esac
|
|
473
|
+
for a in "$@"; do
|
|
474
|
+
case "$a" in
|
|
475
|
+
/*) ;;
|
|
476
|
+
*) continue ;;
|
|
477
|
+
esac
|
|
478
|
+
d="$a"
|
|
479
|
+
rest=""
|
|
480
|
+
while [ ! -d "$d" ]; do
|
|
481
|
+
rest="/$(basename "$d")$rest"
|
|
482
|
+
d="$(dirname "$d")"
|
|
483
|
+
done
|
|
484
|
+
resolved="$(cd "$d" && pwd -P)$rest"
|
|
485
|
+
while IFS= read -r root; do
|
|
486
|
+
case "$resolved/" in
|
|
487
|
+
"$root"/*) err "the command names $a, inside $root, so both runs would run the new code; name it relative to the top of the tree"; exit 4 ;;
|
|
488
|
+
esac
|
|
489
|
+
done <<<"$ROOTS"
|
|
490
|
+
done
|
|
491
|
+
fi
|
|
492
|
+
|
|
493
|
+
# An explicit base wins; otherwise the session's own, from line 3 of its
|
|
494
|
+
# stamp. A missing line or one gc has pruned is unknown, and guessing a base
|
|
495
|
+
# would prove the tests against code they were never changed from.
|
|
496
|
+
if [ -z "$BASE" ]; then
|
|
497
|
+
SID="${CLAUDE_CODE_SESSION_ID-}"
|
|
498
|
+
[ -n "$SID" ] || { err "tests-changed needs --base <ref> when CLAUDE_CODE_SESSION_ID is not set"; exit 4; }
|
|
499
|
+
case "$SID" in
|
|
500
|
+
*/* | .*) err "session id $SID is not a stamp name; pass --base <ref>"; exit 4 ;;
|
|
501
|
+
esac
|
|
502
|
+
BASE="$(sed -n 3p "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/sluice/stamps/$SID" 2>/dev/null)"
|
|
503
|
+
[ -n "$BASE" ] || { err "the session stamp holds no base commit, so the base is unknown; pass --base <ref>"; exit 4; }
|
|
504
|
+
fi
|
|
505
|
+
BASE_SHA="$(git -C "$TREE" rev-parse --verify --quiet "$BASE^{commit}" 2>/dev/null)"
|
|
506
|
+
[ -n "$BASE_SHA" ] || { err "base $BASE no longer resolves to a commit here, so the base is unknown; pass --base <ref>"; exit 4; }
|
|
507
|
+
|
|
508
|
+
TEST_CHANGES="$(dirname "$0")/test-changes.sh"
|
|
509
|
+
FLAGGED="$(bash "$TEST_CHANGES" "$TREE" "$BASE_SHA")" || {
|
|
510
|
+
err "could not read the changed tests in $TREE against $BASE_SHA, so nothing was recorded"
|
|
511
|
+
exit 1
|
|
512
|
+
}
|
|
513
|
+
# --only narrows what this call proves or records to the named paths. A
|
|
514
|
+
# flagged path left out is then just another changed file: present at its
|
|
515
|
+
# current version in every run, so a test reading an edited fixture sees it,
|
|
516
|
+
# and a fixture that passes on the base cannot sink the test beside it.
|
|
517
|
+
if [ -n "$ONLY" ]; then
|
|
518
|
+
while IFS= read -r p; do
|
|
519
|
+
[ -n "$p" ] || continue
|
|
520
|
+
case $'\n'"$FLAGGED"$'\n' in
|
|
521
|
+
*$'\n'"$p"$'\n'*) ;;
|
|
522
|
+
*) err "--only $p: not a flagged test path, so there is nothing to record for it"; exit 4 ;;
|
|
523
|
+
esac
|
|
524
|
+
done <<<"$ONLY"
|
|
525
|
+
NAMED=""
|
|
526
|
+
while IFS= read -r p; do
|
|
527
|
+
case $'\n'"$ONLY" in
|
|
528
|
+
*$'\n'"$p"$'\n'*) NAMED="$NAMED$p"$'\n' ;;
|
|
529
|
+
esac
|
|
530
|
+
done <<<"$FLAGGED"
|
|
531
|
+
FLAGGED="${NAMED%$'\n'}"
|
|
532
|
+
fi
|
|
533
|
+
if [ -z "$FLAGGED" ]; then
|
|
534
|
+
echo "sluice: no changed test needs a record"
|
|
535
|
+
exit 0
|
|
536
|
+
fi
|
|
537
|
+
|
|
538
|
+
mk_dir "$TREE/.sluice" "$TREE"
|
|
539
|
+
# As in verify: a record git can see moves the tree it records.
|
|
540
|
+
git -C "$TREE" check-ignore -q .sluice/tests-changed || {
|
|
541
|
+
err "$TREE/.sluice/.gitignore does not ignore tests-changed, so the record would move the tree it records; add * to it"
|
|
542
|
+
exit 1
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
# Hashed path-aware, so filters apply, the way the stop gate hashes it. A
|
|
546
|
+
# directory where the file was (a fixture split into many) is that file's
|
|
547
|
+
# deletion, here, in proof mode and in the gate alike: it cannot be hashed,
|
|
548
|
+
# and read as unreadable it could never be recorded.
|
|
549
|
+
current_blob() { # <path>: its blob, or - when it is gone
|
|
550
|
+
local sha
|
|
551
|
+
if [ -f "$TREE/$1" ]; then
|
|
552
|
+
sha="$(git -C "$TREE" hash-object -- "$1" 2>/dev/null)"
|
|
553
|
+
[ -n "$sha" ] || { err "could not read $TREE/$1, so nothing was recorded"; exit 1; }
|
|
554
|
+
printf '%s\n' "$sha"
|
|
555
|
+
else
|
|
556
|
+
printf '%s\n' "-"
|
|
557
|
+
fi
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
STATUS=proved
|
|
561
|
+
WHY="$BECAUSE"
|
|
562
|
+
SKIPPED=""
|
|
563
|
+
if [ "$NONE" -eq 1 ]; then
|
|
564
|
+
STATUS=unproven
|
|
565
|
+
WHY="$BECAUSE (unproven: $REASON)"
|
|
566
|
+
# A path already recorded at its current content needs nothing more, so
|
|
567
|
+
# --none after a proof adds only the deletions, and a repeat adds nothing.
|
|
568
|
+
UNRECORDED=""
|
|
569
|
+
while IFS= read -r p; do
|
|
570
|
+
sha="$(current_blob "$p")" || exit 1
|
|
571
|
+
found=""
|
|
572
|
+
if [ -f "$TREE/.sluice/tests-changed" ]; then
|
|
573
|
+
while IFS=$'\t' read -r rec_sha rec_path rest; do
|
|
574
|
+
if [ "$rec_sha" = "$sha" ] && [ "$rec_path" = "$p" ]; then
|
|
575
|
+
found=1
|
|
576
|
+
break
|
|
577
|
+
fi
|
|
578
|
+
done <"$TREE/.sluice/tests-changed"
|
|
579
|
+
fi
|
|
580
|
+
[ -n "$found" ] || UNRECORDED="$UNRECORDED$p"$'\n'
|
|
581
|
+
done <<<"$FLAGGED"
|
|
582
|
+
FLAGGED="${UNRECORDED%$'\n'}"
|
|
583
|
+
if [ -z "$FLAGGED" ]; then
|
|
584
|
+
echo "sluice: every flagged test already has a record at its current content"
|
|
585
|
+
exit 0
|
|
586
|
+
fi
|
|
587
|
+
else
|
|
588
|
+
# Removing a test can only make a suite pass, and a command naming the
|
|
589
|
+
# missing file fails on "not found", so no run can prove a deletion. The
|
|
590
|
+
# deletions are left for --none and the edits beside them proved as usual.
|
|
591
|
+
REMAINING=""
|
|
592
|
+
while IFS= read -r p; do
|
|
593
|
+
if [ -f "$TREE/$p" ]; then
|
|
594
|
+
REMAINING="$REMAINING$p"$'\n'
|
|
595
|
+
else
|
|
596
|
+
SKIPPED="$SKIPPED${SKIPPED:+, }$p"
|
|
597
|
+
fi
|
|
598
|
+
done <<<"$FLAGGED"
|
|
599
|
+
FLAGGED="${REMAINING%$'\n'}"
|
|
600
|
+
if [ -z "$FLAGGED" ]; then
|
|
601
|
+
err "every flagged test was deleted ($SKIPPED), and no run can prove a deletion; record them with --none <reason>. Nothing was recorded"
|
|
602
|
+
exit 4
|
|
603
|
+
fi
|
|
604
|
+
LISTED="$(bash "$TEST_CHANGES" --list "$TREE" "$BASE_SHA")" || {
|
|
605
|
+
err "could not list the changed tests in $TREE against $BASE_SHA, so nothing was recorded"
|
|
606
|
+
exit 1
|
|
607
|
+
}
|
|
608
|
+
# Outside the repo, so nothing the command resolves by walking up from the
|
|
609
|
+
# worktree (node_modules, a config file) can come from the tree or the
|
|
610
|
+
# main checkout instead of from the base.
|
|
611
|
+
WT="$(mktemp -d "${TMPDIR:-/tmp}/sluice-tc.XXXXXX" 2>/dev/null)" || { err "could not make a scratch directory in ${TMPDIR:-/tmp}"; exit 1; }
|
|
612
|
+
trap 'git -C "$TREE" worktree remove --force "$WT" >/dev/null 2>&1; rm -rf "$WT"; git -C "$TREE" worktree prune >/dev/null 2>&1' EXIT
|
|
613
|
+
# Bash skips the EXIT trap on a signal it has no trap for.
|
|
614
|
+
trap 'exit 130' INT
|
|
615
|
+
trap 'exit 143' TERM
|
|
616
|
+
ADD_ERR="$(git -C "$TREE" worktree add --detach "$WT" "$BASE_SHA" 2>&1 >/dev/null)" || {
|
|
617
|
+
err "could not check out $BASE_SHA to run on: $ADD_ERR"
|
|
618
|
+
exit 1
|
|
619
|
+
}
|
|
620
|
+
# Ignored dependencies never reach a fresh checkout.
|
|
621
|
+
for p in "${WITH[@]+"${WITH[@]}"}"; do
|
|
622
|
+
[ ! -e "$WT/$p" ] && [ ! -L "$WT/$p" ] || { err "--with $p is tracked at the base, so there is nothing to link"; exit 4; }
|
|
623
|
+
mkdir -p "$(dirname "$WT/$p")" && ln -s "$TREE/$p" "$WT/$p" || { err "could not link $p into the scratch worktree"; exit 1; }
|
|
624
|
+
done
|
|
625
|
+
|
|
626
|
+
# A directory that replaced a file only removes the file here: its
|
|
627
|
+
# contents are listed paths of their own, sorted after it.
|
|
628
|
+
overlay() { # <path>: puts the tree's current version of it over the base
|
|
629
|
+
if [ -f "$TREE/$1" ] || [ -L "$TREE/$1" ]; then
|
|
630
|
+
mkdir -p "$(dirname "$WT/$1")" && cp -P "$TREE/$1" "$WT/$1" || { err "could not copy $1 onto the base"; exit 1; }
|
|
631
|
+
else
|
|
632
|
+
rm -f "$WT/$1" || { err "could not delete $1 from the base"; exit 1; }
|
|
633
|
+
fi
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
# The control run is the base with every changed test but the flagged ones
|
|
637
|
+
# brought forward, and each proof run differs from it by one flagged path,
|
|
638
|
+
# so a proof run's failure is that path's. Bringing them all forward at
|
|
639
|
+
# once let any new test that fails on old code prove every flagged path.
|
|
640
|
+
# The unit is the file: one file that loosens an assertion and adds a new
|
|
641
|
+
# failing test is still proved, and so is a flagged test that newly imports
|
|
642
|
+
# source the base lacks, since it fails on the base for that alone.
|
|
643
|
+
#
|
|
644
|
+
# Rebuilt from the base before every run, because a run writes into the
|
|
645
|
+
# worktree (fixtures, databases, caches) and what one leaves would decide
|
|
646
|
+
# the next. -x takes ignored residue too; the --with links are kept.
|
|
647
|
+
control_tree() {
|
|
648
|
+
local excludes=() w listed
|
|
649
|
+
for w in "${WITH[@]+"${WITH[@]}"}"; do
|
|
650
|
+
excludes+=(-e "/$w")
|
|
651
|
+
done
|
|
652
|
+
git -C "$WT" checkout -q HEAD -- . && git -C "$WT" clean -fdxq "${excludes[@]+"${excludes[@]}"}" || {
|
|
653
|
+
err "could not reset the scratch worktree to $BASE_SHA"
|
|
654
|
+
exit 1
|
|
655
|
+
}
|
|
656
|
+
while IFS= read -r listed; do
|
|
657
|
+
[ -n "$listed" ] || continue
|
|
658
|
+
case $'\n'"$FLAGGED"$'\n' in
|
|
659
|
+
*$'\n'"$listed"$'\n'*) continue ;;
|
|
660
|
+
esac
|
|
661
|
+
overlay "$listed"
|
|
662
|
+
done <<<"$LISTED"
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
control_tree
|
|
666
|
+
|
|
667
|
+
# Without this a suite that cannot run on the base at all (a missing
|
|
668
|
+
# dependency, a new test for code the base lacks, a test already red there)
|
|
669
|
+
# would fail every proof run and read as proved. Every run's stdin is
|
|
670
|
+
# /dev/null: the proof loop's list arrives on stdin, so a command that
|
|
671
|
+
# reads it would swallow the paths still to be proved, and a runner
|
|
672
|
+
# waiting on input would hang the call.
|
|
673
|
+
if ! (cd "$WT" && "$@") </dev/null; then
|
|
674
|
+
err "the command failed on $BASE_SHA with the flagged tests left as they were, so it cannot prove anything here; link an ignored dependency with --with <path>, narrow the command to the flagged files, or record with --none <reason>. Nothing was recorded"
|
|
675
|
+
exit 8
|
|
676
|
+
fi
|
|
677
|
+
|
|
678
|
+
while IFS= read -r p; do
|
|
679
|
+
control_tree
|
|
680
|
+
overlay "$p"
|
|
681
|
+
if (cd "$WT" && "$@") </dev/null; then
|
|
682
|
+
err "$p passes on $BASE_SHA, the code it was changed against, so it does not pin the change. Nothing was recorded"
|
|
683
|
+
exit 7
|
|
684
|
+
fi
|
|
685
|
+
done <<<"$FLAGGED"
|
|
686
|
+
fi
|
|
687
|
+
|
|
688
|
+
LINES=""
|
|
689
|
+
COUNT=0
|
|
690
|
+
while IFS= read -r p; do
|
|
691
|
+
SHA="$(current_blob "$p")" || exit 1
|
|
692
|
+
LINES="$LINES$SHA $p $STATUS $WHY
|
|
693
|
+
"
|
|
694
|
+
COUNT=$((COUNT + 1))
|
|
695
|
+
done <<<"$FLAGGED"
|
|
696
|
+
printf '%s' "$LINES" >>"$TREE/.sluice/tests-changed" || { err "could not write $TREE/.sluice/tests-changed"; exit 1; }
|
|
697
|
+
echo "sluice: recorded $COUNT changed test(s) as $STATUS"
|
|
698
|
+
[ -z "$SKIPPED" ] || echo "sluice: not recorded, since no run can prove a deletion: $SKIPPED; record it with --none <reason>"
|
|
699
|
+
exit 0
|
|
700
|
+
fi
|
|
701
|
+
|
|
409
702
|
command -v jq >/dev/null 2>&1 || { err "jq is required"; exit 5; }
|
|
410
703
|
|
|
411
704
|
require_run() {
|
|
@@ -16,10 +16,12 @@
|
|
|
16
16
|
# fired this turn, which is what keeps this from looping. One refusal per turn, then:
|
|
17
17
|
# a nudge with the state in it rather than a wall.
|
|
18
18
|
#
|
|
19
|
-
#
|
|
20
|
-
# since the session opened: an entry check for a session that never routed,
|
|
21
|
-
#
|
|
22
|
-
#
|
|
19
|
+
# Three checks run ahead of the run guard, all keyed on the tree having moved
|
|
20
|
+
# since the session opened: an entry check for a session that never routed, a
|
|
21
|
+
# verification gate refusing a fast or main handback with no passing check
|
|
22
|
+
# recorded against the tree as it stands, and a test gate refusing a handback
|
|
23
|
+
# that weakened an existing test with no record of why (see their own
|
|
24
|
+
# sections).
|
|
23
25
|
#
|
|
24
26
|
# Reads the harness's stop JSON on stdin for `cwd` and `stop_hook_active`. To
|
|
25
27
|
# refuse, prints {"decision":"block","reason":...} on stdout. Always exits 0.
|
|
@@ -70,6 +72,7 @@ fi
|
|
|
70
72
|
# moved_tree only when it moved; it stays empty when it did not or when that
|
|
71
73
|
# cannot be told.
|
|
72
74
|
moved_tree=""
|
|
75
|
+
moved_stamp=""
|
|
73
76
|
read_moved() {
|
|
74
77
|
local stamp tree line_tree line_digest now
|
|
75
78
|
[ -n "$sid" ] || return 0
|
|
@@ -89,12 +92,14 @@ read_moved() {
|
|
|
89
92
|
|
|
90
93
|
# A tree that cannot be read is unknown, not moved. Losing git's exit
|
|
91
94
|
# status here would turn a held lock or a rebase in flight into a refused
|
|
92
|
-
# turn over a tree nobody touched.
|
|
93
|
-
|
|
95
|
+
# turn over a tree nobody touched. Without optional locks for the reason
|
|
96
|
+
# session-start.sh gives: a hook must leave the index as it found it.
|
|
97
|
+
now="$(GIT_OPTIONAL_LOCKS=0 bash "$here/tree-snapshot.sh" "$tree" 2>/dev/null)" || return 0
|
|
94
98
|
[ -n "$now" ] || return 0
|
|
95
99
|
[ "$now" = "$line_digest" ] && return 0
|
|
96
100
|
|
|
97
101
|
moved_tree="$tree"
|
|
102
|
+
moved_stamp="$stamp"
|
|
98
103
|
}
|
|
99
104
|
read_moved
|
|
100
105
|
|
|
@@ -231,6 +236,75 @@ if ! verify_gate; then
|
|
|
231
236
|
exit 0
|
|
232
237
|
fi
|
|
233
238
|
|
|
239
|
+
# ---- test gate -------------------------------------------------------------
|
|
240
|
+
# A test edited until it agrees with the new code proves nothing about that
|
|
241
|
+
# code, and a passing check cannot tell that edit from an honest one. So a
|
|
242
|
+
# changed existing test (a removed line, a deletion, a skip or focus marker,
|
|
243
|
+
# as test-changes.sh decides) needs a line in .sluice/tests-changed naming its
|
|
244
|
+
# current content: either proved red on the session's base by
|
|
245
|
+
# `status.sh tests-changed -- <cmd>`, or recorded unproven with a reason. An
|
|
246
|
+
# edit made after its record names other content and is refused again. A new
|
|
247
|
+
# test only asks more of the code, so it needs nothing.
|
|
248
|
+
#
|
|
249
|
+
# The base is line 3 of the stamp read_moved found. A stamp without one, or
|
|
250
|
+
# one gc has pruned, is unknown, and test-changes.sh fails on a base it cannot
|
|
251
|
+
# read, so either way this stays silent. It applies in every channel, after
|
|
252
|
+
# the verify gate so one attempt carries one refusal, and once per turn comes
|
|
253
|
+
# from the `stop_hook_active` exit at the top.
|
|
254
|
+
test_gate() {
|
|
255
|
+
local base flagged missing p sha rec_sha rec_path rest found
|
|
256
|
+
[ -n "$moved_tree" ] || return 0
|
|
257
|
+
base="$(sed -n '3p' "$moved_stamp" 2>/dev/null)"
|
|
258
|
+
[ -n "$base" ] || return 0
|
|
259
|
+
base="$(git -C "$moved_tree" rev-parse --verify --quiet "$base^{commit}" 2>/dev/null)" || return 0
|
|
260
|
+
[ -n "$base" ] || return 0
|
|
261
|
+
flagged="$(bash "$here/test-changes.sh" "$moved_tree" "$base" 2>/dev/null)" || return 0
|
|
262
|
+
[ -n "$flagged" ] || return 0
|
|
263
|
+
|
|
264
|
+
missing=""
|
|
265
|
+
while IFS= read -r p; do
|
|
266
|
+
if [ -f "$moved_tree/$p" ]; then
|
|
267
|
+
# Hashed the way status.sh records it, path-aware so filters apply.
|
|
268
|
+
# A file that cannot be hashed has no record status.sh could have
|
|
269
|
+
# written either, so it is named as unreadable rather than letting
|
|
270
|
+
# one path silence the gate for every other. A directory where the
|
|
271
|
+
# file was is its deletion, as status.sh reads it.
|
|
272
|
+
sha="$(git -C "$moved_tree" hash-object -- "$p" 2>/dev/null)"
|
|
273
|
+
if [ -z "$sha" ]; then
|
|
274
|
+
missing="$missing$p (cannot be read)"$'\n'
|
|
275
|
+
continue
|
|
276
|
+
fi
|
|
277
|
+
else
|
|
278
|
+
sha="-"
|
|
279
|
+
fi
|
|
280
|
+
found=""
|
|
281
|
+
if [ -f "$moved_tree/.sluice/tests-changed" ]; then
|
|
282
|
+
while IFS=$'\t' read -r rec_sha rec_path rest; do
|
|
283
|
+
if [ "$rec_sha" = "$sha" ] && [ "$rec_path" = "$p" ]; then
|
|
284
|
+
found=1
|
|
285
|
+
break
|
|
286
|
+
fi
|
|
287
|
+
done <"$moved_tree/.sluice/tests-changed"
|
|
288
|
+
fi
|
|
289
|
+
[ -n "$found" ] || missing="$missing$p"$'\n'
|
|
290
|
+
done <<<"$flagged"
|
|
291
|
+
[ -n "$missing" ] || return 0
|
|
292
|
+
|
|
293
|
+
# Paths land in a reason the harness prints, so control bytes are stripped
|
|
294
|
+
# as the run guard strips them from the topic and tree.
|
|
295
|
+
jq -n --arg paths "$missing" --arg base "$base" '{
|
|
296
|
+
decision: "block",
|
|
297
|
+
reason: ("sluice: this session changed existing tests in a way that asks less of the code (an edited or removed assertion, a deleted test, a skip or focus marker), and nothing records why: "
|
|
298
|
+
+ ($paths | split("\n") | map(select(. != "") | gsub("[\u0000-\u001f\u007f]"; "")) | join(", "))
|
|
299
|
+
+ ". A test bent to fit new code proves nothing about it, so each one needs a record against the session base \($base). Prove it fails on the old code: status.sh tests-changed --because \"<the behaviour the change replaces>\" -- <the suite, scoped to these files> (the run is in a scratch worktree of the base, so a node suite usually needs --with node_modules). If it cannot run on the old code, record why: status.sh tests-changed --because \"<the behaviour the change replaces>\" --none \"<why it cannot run on the old code>\". No run can prove a deleted test, so prove the edited ones first, then record each deletion with --none. --only <path> proves or records just those paths, so a fixture that cannot be proved can go to --none --only <fixture> on its own. Then name each changed test, and why it changed, in your reply.")
|
|
300
|
+
}'
|
|
301
|
+
return 1
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if ! test_gate; then
|
|
305
|
+
exit 0
|
|
306
|
+
fi
|
|
307
|
+
|
|
234
308
|
[ -f "$STATUS" ] || exit 0
|
|
235
309
|
|
|
236
310
|
# The run the session's own tree answers for, and only that: the state beside
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Print the test paths a session changed in a way that needs a record, for the
|
|
3
|
+
# test-change gate.
|
|
4
|
+
#
|
|
5
|
+
# Usage: test-changes.sh [--list] <tree> <base>
|
|
6
|
+
# Without --list, prints each test path needing a record, one per line, sorted;
|
|
7
|
+
# empty output means none does. With --list, prints every test path that
|
|
8
|
+
# differs from base (added, modified, deleted, and both ends of a rename), for
|
|
9
|
+
# the proof run to copy or, where the path is gone now, delete. Exit 0 printed
|
|
10
|
+
# the answer, exit 1 the tree or base could not be read, and then nothing is
|
|
11
|
+
# printed: an unreadable tree is "unknown", never "changed", and the caller
|
|
12
|
+
# stays silent.
|
|
13
|
+
#
|
|
14
|
+
# A test that only grows is a test that only asks more of the code, so it
|
|
15
|
+
# passes. What needs a record is a test that asks less, or asks nothing:
|
|
16
|
+
#
|
|
17
|
+
# - a modified file with any removed line, since an edited assertion is a
|
|
18
|
+
# removed line plus an added one;
|
|
19
|
+
# - a deleted file;
|
|
20
|
+
# - an added line carrying a skip or focus marker, in a new file or an old
|
|
21
|
+
# one, since `it.skip(` switches a test off without removing a line, and
|
|
22
|
+
# `.only(` switches every other test off.
|
|
23
|
+
#
|
|
24
|
+
# A 100% rename moves a test without changing what it asks, so it passes.
|
|
25
|
+
#
|
|
26
|
+
# The comparison is base against the working tree as `git add -A` would stage
|
|
27
|
+
# it, so untracked test files count. It is staged into a copy of the index, as
|
|
28
|
+
# content-digest.sh does it: staging the partner's work in the real index to
|
|
29
|
+
# read it would be a change of its own.
|
|
30
|
+
|
|
31
|
+
set -uo pipefail
|
|
32
|
+
|
|
33
|
+
mode=records
|
|
34
|
+
if [ "${1-}" = "--list" ]; then
|
|
35
|
+
mode=list
|
|
36
|
+
shift
|
|
37
|
+
fi
|
|
38
|
+
tree="${1-}"
|
|
39
|
+
base="${2-}"
|
|
40
|
+
[ -n "$tree" ] && [ -n "$base" ] || exit 1
|
|
41
|
+
cd "$tree" 2>/dev/null || exit 1
|
|
42
|
+
git rev-parse --show-toplevel >/dev/null 2>&1 || exit 1
|
|
43
|
+
cd "$(git rev-parse --show-toplevel)" 2>/dev/null || exit 1
|
|
44
|
+
git rev-parse --verify --quiet "$base^{commit}" >/dev/null 2>&1 || exit 1
|
|
45
|
+
index="$(git rev-parse --git-path index 2>/dev/null)" || exit 1
|
|
46
|
+
[ -n "$index" ] || exit 1
|
|
47
|
+
|
|
48
|
+
markers='\.(skip|only)\(|\bx(it|describe|test)\(|@pytest\.mark\.(skip|xfail)|pytest\.skip\(|\bt\.Skip|@Disabled|@Ignore|#\[ignore\]'
|
|
49
|
+
|
|
50
|
+
# The list comes from the base rather than HEAD or the working tree: a session
|
|
51
|
+
# commits, so HEAD is its to move, and it should not be able to move its own
|
|
52
|
+
# tests out of view by editing .sluice-test-paths.
|
|
53
|
+
pathspecs=()
|
|
54
|
+
if git cat-file -e "$base:.sluice-test-paths" 2>/dev/null; then
|
|
55
|
+
listed="$(git cat-file blob "$base:.sluice-test-paths" 2>/dev/null)" || exit 1
|
|
56
|
+
while IFS= read -r line; do
|
|
57
|
+
line="${line%$'\r'}"
|
|
58
|
+
case "$line" in
|
|
59
|
+
'' | '#'*) continue ;;
|
|
60
|
+
esac
|
|
61
|
+
pathspecs+=(":(glob)$line")
|
|
62
|
+
done <<<"$listed"
|
|
63
|
+
# A list of nothing but comments would otherwise diff every path.
|
|
64
|
+
[ "${#pathspecs[@]}" -gt 0 ] || exit 0
|
|
65
|
+
else
|
|
66
|
+
for glob in '**/test/**' '**/tests/**' '**/__tests__/**' '**/spec/**' \
|
|
67
|
+
'**/*.test.*' '**/*.spec.*' '**/*_test.*' '**/test_*.*' \
|
|
68
|
+
'**/__snapshots__/**' '**/*.snap' '**/testdata/**' '**/golden/**'; do
|
|
69
|
+
pathspecs+=(":(glob)$glob")
|
|
70
|
+
done
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
# The scratch index lives in the git dir for content-digest.sh's reason: a
|
|
74
|
+
# sandbox can refuse the temp dir, and git add has to write the git dir anyway.
|
|
75
|
+
gitdir="$(git rev-parse --absolute-git-dir 2>/dev/null)" || exit 1
|
|
76
|
+
scratch="$(mktemp -d "$gitdir/sluice-test-changes.XXXXXX" 2>/dev/null)" || exit 1
|
|
77
|
+
trap 'rm -rf "$scratch"' EXIT
|
|
78
|
+
# -p keeps the index's own mtime. A fresh one would make git trust the stat
|
|
79
|
+
# data of a file edited in the same second it was staged, and miss a same-size
|
|
80
|
+
# edit to it.
|
|
81
|
+
if [ -f "$index" ]; then
|
|
82
|
+
cp -p "$index" "$scratch/index" 2>/dev/null || exit 1
|
|
83
|
+
fi
|
|
84
|
+
export GIT_INDEX_FILE="$scratch/index"
|
|
85
|
+
git add -A >/dev/null 2>&1 || exit 1
|
|
86
|
+
|
|
87
|
+
# The repo's colour, external diff and textconv settings would all change the
|
|
88
|
+
# lines the marker grep reads, so every diff turns them off.
|
|
89
|
+
plain=(--no-color --no-ext-diff --no-textconv)
|
|
90
|
+
|
|
91
|
+
# NUL-separated, because git C-quotes a name holding a quote, backslash, tab or
|
|
92
|
+
# newline in any other form, and a variable cannot hold a NUL.
|
|
93
|
+
git diff --cached "${plain[@]}" -M100% --name-status -z "$base" -- "${pathspecs[@]}" >"$scratch/statuses" || exit 1
|
|
94
|
+
|
|
95
|
+
flagged=""
|
|
96
|
+
while IFS= read -r -d '' status; do
|
|
97
|
+
IFS= read -r -d '' path || exit 1
|
|
98
|
+
source_path=""
|
|
99
|
+
case "$status" in
|
|
100
|
+
R*)
|
|
101
|
+
source_path="$path"
|
|
102
|
+
IFS= read -r -d '' path || exit 1
|
|
103
|
+
;;
|
|
104
|
+
esac
|
|
105
|
+
if [ "$mode" = list ]; then
|
|
106
|
+
# The source too: it is gone now, and the proof's overlay has to remove it
|
|
107
|
+
# from the base, or a file moved into a directory of its own name blocks
|
|
108
|
+
# that directory.
|
|
109
|
+
flagged="$flagged$source_path${source_path:+$'\n'}$path"$'\n'
|
|
110
|
+
continue
|
|
111
|
+
fi
|
|
112
|
+
case "$status" in
|
|
113
|
+
D | T) flagged="$flagged$path"$'\n' ;;
|
|
114
|
+
A | M)
|
|
115
|
+
# A binary file reports "-" for its line counts, and a changed binary
|
|
116
|
+
# snapshot is as much an edit as a changed text one. A new file has
|
|
117
|
+
# nothing to remove, binary or not.
|
|
118
|
+
if [ "$status" = M ]; then
|
|
119
|
+
numstat="$(git diff --cached "${plain[@]}" --numstat "$base" -- ":(literal)$path")" || exit 1
|
|
120
|
+
removed="$(printf '%s\n' "$numstat" | cut -f2)"
|
|
121
|
+
if [ "$removed" != 0 ]; then
|
|
122
|
+
flagged="$flagged$path"$'\n'
|
|
123
|
+
continue
|
|
124
|
+
fi
|
|
125
|
+
fi
|
|
126
|
+
diff="$(git diff --cached "${plain[@]}" -U0 "$base" -- ":(literal)$path")" || exit 1
|
|
127
|
+
if printf '%s\n' "$diff" | sed -n '/^@@/,$p' | grep '^+' | grep -Eq "$markers"; then
|
|
128
|
+
flagged="$flagged$path"$'\n'
|
|
129
|
+
fi
|
|
130
|
+
;;
|
|
131
|
+
esac
|
|
132
|
+
done <"$scratch/statuses"
|
|
133
|
+
|
|
134
|
+
printf '%s' "$flagged" | grep -v '^$' | LC_ALL=C sort -u
|
|
135
|
+
exit 0
|
package/skills/sluice/skill.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sluice",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.0",
|
|
4
4
|
"description": "Routes work by change shape into four channels (bypass, fast, main, deep) and applies only the rules each channel needs, so a one-line fix does not pay the cost of a multi-subsystem build. Carries nine rules as one-liners in the router and the full treatment in references read only on friction. Checks the finished plan with plan.sh validate rather than trusting it to memory, seeds the run state from it, keeps a deep run's task breakdown in .sluice/run.json so a statusline segment, one status command and a SessionStart hook can answer where the run is (the hook prints a live run at every session start, compaction included), and closes each run with a ledger read out of the session transcript: elapsed, tools, tokens, and what each dispatched agent cost where the transcript recorded it. Claude Code only; stands down where the superpowers pipeline governs the repo.",
|
|
5
5
|
"author": "iceinvein",
|
|
6
6
|
"type": "prompt",
|