@iceinvein/agent-skills 0.18.2 → 0.19.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 (71) hide show
  1. package/dist/cli/index.js +14 -6
  2. package/package.json +1 -1
  3. package/skills/index.json +1 -1
  4. package/skills/sluice/SKILL.md +6 -0
  5. package/skills/sluice/evals/README.md +79 -0
  6. package/skills/sluice/evals/bypass-question-stays-silent/graders/answers-the-question.md +9 -0
  7. package/skills/sluice/evals/bypass-question-stays-silent/graders/no-channel-announcement.md +7 -0
  8. package/skills/sluice/evals/bypass-question-stays-silent/graders/writes-nothing.md +5 -0
  9. package/skills/sluice/evals/bypass-question-stays-silent/prompt.md +10 -0
  10. package/skills/sluice/evals/deep-plan-across-subsystems/graders/announces-deep-channel.md +7 -0
  11. package/skills/sluice/evals/deep-plan-across-subsystems/graders/design-written-to-docs.md +5 -0
  12. package/skills/sluice/evals/deep-plan-across-subsystems/graders/no-implementation-yet.md +6 -0
  13. package/skills/sluice/evals/deep-plan-across-subsystems/graders/sluice-fired.md +5 -0
  14. package/skills/sluice/evals/deep-plan-across-subsystems/graders/stopped-for-signoff.md +8 -0
  15. package/skills/sluice/evals/deep-plan-across-subsystems/prompt.md +11 -0
  16. package/skills/sluice/evals/deep-run-blocks-on-a-real-decision/case.yaml +4 -0
  17. package/skills/sluice/evals/deep-run-blocks-on-a-real-decision/fixture.sh +332 -0
  18. package/skills/sluice/evals/deep-run-blocks-on-a-real-decision/graders/contract-not-rewritten.md +6 -0
  19. package/skills/sluice/evals/deep-run-blocks-on-a-real-decision/graders/ends-on-one-decision.md +15 -0
  20. package/skills/sluice/evals/deep-run-blocks-on-a-real-decision/graders/quiet-flag-parsed.md +5 -0
  21. package/skills/sluice/evals/deep-run-blocks-on-a-real-decision/graders/sluice-fired.md +5 -0
  22. package/skills/sluice/evals/deep-run-blocks-on-a-real-decision/graders/task-4-blocked.md +7 -0
  23. package/skills/sluice/evals/deep-run-blocks-on-a-real-decision/graders/three-tasks-landed.md +7 -0
  24. package/skills/sluice/evals/deep-run-blocks-on-a-real-decision/prompt.md +11 -0
  25. package/skills/sluice/evals/deep-run-finishes-every-task/case.yaml +4 -0
  26. package/skills/sluice/evals/deep-run-finishes-every-task/fixture.sh +304 -0
  27. package/skills/sluice/evals/deep-run-finishes-every-task/graders/did-not-check-in-between-tasks.md +13 -0
  28. package/skills/sluice/evals/deep-run-finishes-every-task/graders/every-task-done.md +7 -0
  29. package/skills/sluice/evals/deep-run-finishes-every-task/graders/no-task-left-todo.md +7 -0
  30. package/skills/sluice/evals/deep-run-finishes-every-task/graders/quiet-flag-landed.md +5 -0
  31. package/skills/sluice/evals/deep-run-finishes-every-task/graders/sluice-fired.md +5 -0
  32. package/skills/sluice/evals/deep-run-finishes-every-task/graders/suite-was-run.md +6 -0
  33. package/skills/sluice/evals/deep-run-finishes-every-task/prompt.md +11 -0
  34. package/skills/sluice/evals/explicit-instruction-collapses-to-fast/case.yaml +4 -0
  35. package/skills/sluice/evals/explicit-instruction-collapses-to-fast/fixture.sh +73 -0
  36. package/skills/sluice/evals/explicit-instruction-collapses-to-fast/graders/announces-fast-channel.md +7 -0
  37. package/skills/sluice/evals/explicit-instruction-collapses-to-fast/graders/collapsed-not-negotiated.md +10 -0
  38. package/skills/sluice/evals/explicit-instruction-collapses-to-fast/graders/no-design-or-plan-file.md +6 -0
  39. package/skills/sluice/evals/explicit-instruction-collapses-to-fast/graders/seam-implemented.md +9 -0
  40. package/skills/sluice/evals/explicit-instruction-collapses-to-fast/graders/sluice-fired.md +5 -0
  41. package/skills/sluice/evals/explicit-instruction-collapses-to-fast/prompt.md +11 -0
  42. package/skills/sluice/evals/fast-flag-on-existing-command/case.yaml +4 -0
  43. package/skills/sluice/evals/fast-flag-on-existing-command/fixture.sh +73 -0
  44. package/skills/sluice/evals/fast-flag-on-existing-command/graders/announces-fast-channel.md +7 -0
  45. package/skills/sluice/evals/fast-flag-on-existing-command/graders/quiet-flag-implemented.md +5 -0
  46. package/skills/sluice/evals/fast-flag-on-existing-command/graders/sluice-fired.md +5 -0
  47. package/skills/sluice/evals/fast-flag-on-existing-command/graders/stayed-in-fast.md +9 -0
  48. package/skills/sluice/evals/fast-flag-on-existing-command/graders/suite-was-run.md +6 -0
  49. package/skills/sluice/evals/fast-flag-on-existing-command/graders/test-edited-before-source.md +6 -0
  50. package/skills/sluice/evals/fast-flag-on-existing-command/prompt.md +11 -0
  51. package/skills/sluice/evals/main-new-interface/case.yaml +4 -0
  52. package/skills/sluice/evals/main-new-interface/fixture.sh +73 -0
  53. package/skills/sluice/evals/main-new-interface/graders/announces-main-channel.md +7 -0
  54. package/skills/sluice/evals/main-new-interface/graders/behaviour-preserved.md +9 -0
  55. package/skills/sluice/evals/main-new-interface/graders/shape-agreed-before-building.md +10 -0
  56. package/skills/sluice/evals/main-new-interface/graders/sluice-fired.md +5 -0
  57. package/skills/sluice/evals/main-new-interface/graders/suite-was-run.md +6 -0
  58. package/skills/sluice/evals/main-new-interface/prompt.md +11 -0
  59. package/skills/sluice/evals/results/2026-09-20T01-51-28-540Z/aggregate-result.json +105 -0
  60. package/skills/sluice/evals/results/2026-09-20T01-51-28-540Z/report.html +300 -0
  61. package/skills/sluice/evals/results/2026-09-20T01-52-06-287Z/aggregate-result.json +122 -0
  62. package/skills/sluice/evals/results/2026-09-20T01-52-06-287Z/report.html +324 -0
  63. package/skills/sluice/evals/superpowers-conflict-stands-down/case.yaml +4 -0
  64. package/skills/sluice/evals/superpowers-conflict-stands-down/fixture.sh +39 -0
  65. package/skills/sluice/evals/superpowers-conflict-stands-down/graders/names-no-channel.md +8 -0
  66. package/skills/sluice/evals/superpowers-conflict-stands-down/graders/stands-down-once.md +9 -0
  67. package/skills/sluice/evals/superpowers-conflict-stands-down/prompt.md +10 -0
  68. package/skills/sluice/scripts/session-start.sh +51 -0
  69. package/skills/sluice/scripts/stop-guard.sh +101 -0
  70. package/skills/sluice/scripts/tree-snapshot.sh +75 -0
  71. package/skills/sluice/skill.json +1 -1
@@ -45,13 +45,114 @@ fi
45
45
 
46
46
  cwd="$PWD"
47
47
  active="false"
48
+ sid=""
49
+ transcript=""
48
50
  if [ -n "$input" ]; then
49
51
  got="$(printf '%s' "$input" | jq -r '.cwd // empty' 2>/dev/null || true)"
50
52
  [ -n "$got" ] && cwd="$got"
51
53
  active="$(printf '%s' "$input" | jq -r '.stop_hook_active // false' 2>/dev/null || echo false)"
54
+ sid="$(printf '%s' "$input" | jq -r '.session_id // empty' 2>/dev/null || true)"
55
+ transcript="$(printf '%s' "$input" | jq -r '.transcript_path // empty' 2>/dev/null || true)"
52
56
  fi
53
57
  [ "$active" = "true" ] && exit 0
54
58
  [ -d "$cwd" ] || exit 0
59
+
60
+ # ---- entry check --------------------------------------------------------
61
+ # The run guard below only arms once .sluice/run.json exists, and that file
62
+ # only exists once the skill has been invoked. So the session that never
63
+ # routed at all, the one this whole guard is for, walks straight past it in
64
+ # silence. This catches that session from the other end: the tree moved while
65
+ # it held it, and no channel was ever announced.
66
+ #
67
+ # The comparison is against where the tree stood when the session opened, not
68
+ # against whether it is dirty now. A session that opens on someone's
69
+ # work-in-progress and only answers questions changed nothing, and nudging it
70
+ # would teach its partner to ignore the nudge.
71
+ #
72
+ # Refused once and then never again for this session: the check cannot tell
73
+ # a partner who routed afterwards from one who read the nudge and chose to
74
+ # carry on, and only the first of those is worth a second refusal.
75
+ #
76
+ # The stamp is per session and the tree it watches is not, so anything else
77
+ # writing to the tree reads as this session's work: a second session, the
78
+ # partner's own editor, an install touching a lockfile, a build emitting
79
+ # something the tree does not ignore. There is nothing in a git tree that
80
+ # attributes a change to who made it, so this is not fixable here, only
81
+ # bounded: one refusal per session, and a reason that offers "not mine" as an
82
+ # answer and takes it.
83
+ entry_nudge() {
84
+ local stamps tree line_tree line_digest now
85
+ [ -n "$sid" ] || return 0
86
+ case "$sid" in */* | .*) return 0 ;; esac
87
+
88
+ # Two directories rather than one name and one suffixed name: sharing a
89
+ # namespace means a session id ending in `.nudged` silently disarms the
90
+ # session whose id is its prefix.
91
+ local root="${CLAUDE_CONFIG_DIR:-${HOME:-}/.claude}/sluice"
92
+ stamps="$root/stamps"
93
+ [ -f "$stamps/$sid" ] || return 0
94
+ [ -f "$root/nudged/$sid" ] && return 0
95
+
96
+ tree="$(git -C "$cwd" rev-parse --show-toplevel 2>/dev/null)" || return 0
97
+ [ -n "$tree" ] || return 0
98
+
99
+ line_tree="$(sed -n '1p' "$stamps/$sid" 2>/dev/null)"
100
+ line_digest="$(sed -n '2p' "$stamps/$sid" 2>/dev/null)"
101
+ [ -n "$line_tree" ] && [ -n "$line_digest" ] || return 0
102
+ # A session that moved trees carries a baseline for the one it left, and
103
+ # reading this tree's changes against it would be reading someone else's.
104
+ [ "$line_tree" = "$tree" ] || return 0
105
+
106
+ # A tree that cannot be read is unknown, not moved. Losing git's exit
107
+ # status here would turn a held lock or a rebase in flight into a refused
108
+ # turn over a tree nobody touched.
109
+ now="$(bash "$here/tree-snapshot.sh" "$tree" 2>/dev/null)" || return 0
110
+ [ -n "$now" ] || return 0
111
+ [ "$now" = "$line_digest" ] && return 0
112
+
113
+ # Whether the session routed is read exactly the way run-stats.sh reads it,
114
+ # from a copy of its `marker` and `lead` patterns and its `invokes_sluice`:
115
+ # a gate that disagreed with the meter would refuse turns the ledger
116
+ # reports as a run. The copy is kept in step with that file by hand.
117
+ #
118
+ # Lines are parsed one at a time and unparseable ones dropped, because the
119
+ # harness is still appending to this file while the hook reads it and the
120
+ # last line is regularly half written. Slurping would fail on that whole
121
+ # file and read a routed session as an unrouted one.
122
+ [ -f "$transcript" ] || return 0
123
+ jq -e -n -R '
124
+ def marker: "^[*_#>[:space:]]*(fast|main|deep)[[:space:]]+channel";
125
+ def lead: "^[^.!?\n]{0,100}[:=][[:space:]]*[*_]*(fast|main|deep)[[:space:]]+channel";
126
+ def texts: [ .message.content[]? | select(.type == "text") | .text ] | join("\n");
127
+ def invokes_sluice: [ .message.content[]?
128
+ | select(.type == "tool_use" and .name == "Skill")
129
+ | .input.skill? // empty ] | any(. == "sluice");
130
+ [ inputs | fromjson? // empty ]
131
+ | any(.[];
132
+ (.type == "assistant") and (((.isMeta == true) or (.isSidechain == true)) | not)
133
+ and ((texts | test(marker; "i") or test(lead; "i")) or invokes_sluice))
134
+ ' "$transcript" >/dev/null 2>&1
135
+ case "$?" in
136
+ # 0 routed, 1 not. Anything else is jq failing rather than an answer
137
+ # about this session, and an unanswered question is not a refusal.
138
+ 0) return 0 ;;
139
+ 1) ;;
140
+ *) return 0 ;;
141
+ esac
142
+
143
+ mkdir -p "$root/nudged" 2>/dev/null || true
144
+ : >"$root/nudged/$sid" 2>/dev/null || true
145
+ jq -n '{
146
+ decision: "block",
147
+ reason: "sluice: this session has changed the tree since it opened and no channel was ever announced, so the work is running with none of the rules that its shape calls for and nothing to meter it from. Invoke the sluice skill now, route what you have been doing, and say which channel it is. If it genuinely changed nothing you own (a scratch file, or an edit that was not yours), say so and stop; this will not ask twice."
148
+ }'
149
+ return 1
150
+ }
151
+
152
+ if ! entry_nudge; then
153
+ exit 0
154
+ fi
155
+
55
156
  [ -f "$STATUS" ] || exit 0
56
157
 
57
158
  # The session's own tree, and only that. status.sh lets a tree with no run of
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env bash
2
+ # Print one digest of where a git tree stands, for stop-guard.sh's entry check.
3
+ #
4
+ # Usage: tree-snapshot.sh <tree>
5
+ # Exit 0 printed a digest, non-zero the tree could not be read. Prints nothing
6
+ # on failure, and a caller that cannot read the tree has no comparison to make
7
+ # rather than a change to report: an interrupted index, a lock held by another
8
+ # git, or a rebase in flight are all "unknown", never "moved".
9
+ #
10
+ # It lives in its own file because two hooks compute it at two different moments
11
+ # and a difference between their two copies would not read as a bug, it would
12
+ # read as the tree having moved. That is the whole measurement.
13
+ #
14
+ # Three signals, because each covers a way of finishing work that the others
15
+ # call standing still:
16
+ #
17
+ # HEAD a session that committed leaves a clean tree
18
+ # diff HEAD a session that rewrote a file already modified
19
+ # leaves the porcelain's shape untouched
20
+ # status --porcelain -uall a session that filled an untracked directory
21
+ # leaves one collapsed `?? build/` either way
22
+ #
23
+ # What it still misses, and why each is left:
24
+ #
25
+ # - the content of a file that was already untracked when the session opened.
26
+ # Listing untracked paths is cheap and hashing their contents is not, and
27
+ # this runs at the end of every turn.
28
+ # - anything inside a nested repository. `status` does not descend into a
29
+ # directory carrying its own .git, so a session spent editing a scratch
30
+ # clone or a vendored checkout reads as a tree nobody touched.
31
+ # - who made the change. Nothing in a git tree records that, so a branch
32
+ # switch, a pull, or another session writing to the same tree all read as
33
+ # this session's work. stop-guard.sh bounds that rather than fixing it.
34
+
35
+ set -uo pipefail
36
+
37
+ tree="${1-}"
38
+ [ -n "$tree" ] || exit 1
39
+ command -v cksum >/dev/null 2>&1 || exit 1
40
+
41
+ # `git diff HEAD` has no meaning before the first commit, and a repo without one
42
+ # is a repo whose every file is untracked, which the porcelain covers. So an
43
+ # unresolvable HEAD is carried as "no-head" rather than refused.
44
+ #
45
+ # It would be better to refuse when HEAD is unresolvable because the ref store
46
+ # cannot be read, as opposed to because no commit exists yet, since the first is
47
+ # a tree this script cannot describe. git gives no way to tell them apart:
48
+ # `rev-parse --verify --quiet` exits 1 for both, and `for-each-ref` returns
49
+ # empty for both, hiding an unreadable ref exactly as it reports an absent one.
50
+ # Refusing on both would disarm the check permanently in any repo before its
51
+ # first commit, which is an ordinary place to be writing code. Carrying both
52
+ # costs a single nudge, and only in a tree whose refs broke partway through the
53
+ # session it had already been read in.
54
+ head="$(git -C "$tree" rev-parse --verify --quiet HEAD 2>/dev/null)" || head=""
55
+
56
+ # Streamed into cksum rather than gathered into variables first. A diff is
57
+ # unbounded, and this runs at the end of every turn until the session is either
58
+ # routed or nudged: an 80MB file rewritten peaks at 334MB resident streamed,
59
+ # against 622MB for a 61MB one held in a shell variable and then piped. What is
60
+ # left is git loading the blobs, which is the price of reading content at all
61
+ # and is the same for --numstat. Streaming also keeps NUL bytes intact, which a
62
+ # command substitution silently drops, and dropping them would hide a change
63
+ # confined to them.
64
+ #
65
+ # `exit 1` inside the group reaches the caller through pipefail, and the digest
66
+ # cksum prints from a truncated read is discarded with it: a caller that cannot
67
+ # read the tree needs no answer, and a short answer is a wrong one.
68
+ digest="$( {
69
+ printf '%s\n' "${head:-no-head}"
70
+ git -C "$tree" status --porcelain -uall 2>/dev/null || exit 1
71
+ [ -n "$head" ] && { git -C "$tree" diff HEAD 2>/dev/null || exit 1; }
72
+ true
73
+ } | cksum )" || exit 1
74
+
75
+ printf '%s\n' "$digest"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sluice",
3
- "version": "0.19.2",
3
+ "version": "0.20.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 seven 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",