@iceinvein/agent-skills 0.18.3 → 0.20.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/package.json +1 -1
- package/skills/index.json +1 -1
- package/skills/sluice/SKILL.md +10 -1
- package/skills/sluice/evals/README.md +100 -0
- package/skills/sluice/evals/bypass-question-stays-silent/graders/answers-the-question.md +9 -0
- package/skills/sluice/evals/bypass-question-stays-silent/graders/no-channel-announcement.md +7 -0
- package/skills/sluice/evals/bypass-question-stays-silent/graders/writes-nothing.md +5 -0
- package/skills/sluice/evals/bypass-question-stays-silent/prompt.md +10 -0
- package/skills/sluice/evals/deep-plan-across-subsystems/case.yaml +4 -0
- package/skills/sluice/evals/deep-plan-across-subsystems/fixture.sh +105 -0
- package/skills/sluice/evals/deep-plan-across-subsystems/graders/announces-deep-channel.md +11 -0
- package/skills/sluice/evals/deep-plan-across-subsystems/graders/design-written-to-docs.md +5 -0
- package/skills/sluice/evals/deep-plan-across-subsystems/graders/no-implementation-yet.md +10 -0
- package/skills/sluice/evals/deep-plan-across-subsystems/graders/sluice-fired.md +5 -0
- package/skills/sluice/evals/deep-plan-across-subsystems/graders/stopped-for-signoff.md +8 -0
- package/skills/sluice/evals/deep-plan-across-subsystems/prompt.md +11 -0
- package/skills/sluice/evals/deep-run-blocks-on-a-real-decision/case.yaml +4 -0
- package/skills/sluice/evals/deep-run-blocks-on-a-real-decision/fixture.sh +332 -0
- package/skills/sluice/evals/deep-run-blocks-on-a-real-decision/graders/contract-not-rewritten.md +6 -0
- package/skills/sluice/evals/deep-run-blocks-on-a-real-decision/graders/ends-on-one-decision.md +15 -0
- package/skills/sluice/evals/deep-run-blocks-on-a-real-decision/graders/quiet-flag-parsed.md +5 -0
- package/skills/sluice/evals/deep-run-blocks-on-a-real-decision/graders/sluice-fired.md +5 -0
- package/skills/sluice/evals/deep-run-blocks-on-a-real-decision/graders/task-4-blocked.md +7 -0
- package/skills/sluice/evals/deep-run-blocks-on-a-real-decision/graders/three-tasks-landed.md +7 -0
- package/skills/sluice/evals/deep-run-blocks-on-a-real-decision/prompt.md +11 -0
- package/skills/sluice/evals/deep-run-finishes-every-task/case.yaml +4 -0
- package/skills/sluice/evals/deep-run-finishes-every-task/fixture.sh +304 -0
- package/skills/sluice/evals/deep-run-finishes-every-task/graders/did-not-check-in-between-tasks.md +13 -0
- package/skills/sluice/evals/deep-run-finishes-every-task/graders/every-task-done.md +7 -0
- package/skills/sluice/evals/deep-run-finishes-every-task/graders/no-task-left-todo.md +7 -0
- package/skills/sluice/evals/deep-run-finishes-every-task/graders/quiet-flag-landed.md +5 -0
- package/skills/sluice/evals/deep-run-finishes-every-task/graders/sluice-fired.md +5 -0
- package/skills/sluice/evals/deep-run-finishes-every-task/graders/suite-was-run.md +6 -0
- package/skills/sluice/evals/deep-run-finishes-every-task/prompt.md +11 -0
- package/skills/sluice/evals/explicit-instruction-collapses-to-fast/case.yaml +4 -0
- package/skills/sluice/evals/explicit-instruction-collapses-to-fast/fixture.sh +73 -0
- package/skills/sluice/evals/explicit-instruction-collapses-to-fast/graders/announces-fast-channel.md +7 -0
- package/skills/sluice/evals/explicit-instruction-collapses-to-fast/graders/collapsed-not-negotiated.md +10 -0
- package/skills/sluice/evals/explicit-instruction-collapses-to-fast/graders/no-design-or-plan-file.md +6 -0
- package/skills/sluice/evals/explicit-instruction-collapses-to-fast/graders/seam-implemented.md +9 -0
- package/skills/sluice/evals/explicit-instruction-collapses-to-fast/graders/sluice-fired.md +5 -0
- package/skills/sluice/evals/explicit-instruction-collapses-to-fast/prompt.md +11 -0
- package/skills/sluice/evals/fast-flag-on-existing-command/case.yaml +4 -0
- package/skills/sluice/evals/fast-flag-on-existing-command/fixture.sh +73 -0
- package/skills/sluice/evals/fast-flag-on-existing-command/graders/announces-fast-channel.md +7 -0
- package/skills/sluice/evals/fast-flag-on-existing-command/graders/quiet-flag-implemented.md +5 -0
- package/skills/sluice/evals/fast-flag-on-existing-command/graders/sluice-fired.md +5 -0
- package/skills/sluice/evals/fast-flag-on-existing-command/graders/stayed-in-fast.md +9 -0
- package/skills/sluice/evals/fast-flag-on-existing-command/graders/suite-was-run.md +6 -0
- package/skills/sluice/evals/fast-flag-on-existing-command/graders/test-edited-before-source.md +6 -0
- package/skills/sluice/evals/fast-flag-on-existing-command/prompt.md +11 -0
- package/skills/sluice/evals/main-new-interface/case.yaml +4 -0
- package/skills/sluice/evals/main-new-interface/fixture.sh +73 -0
- package/skills/sluice/evals/main-new-interface/graders/announces-main-channel.md +11 -0
- package/skills/sluice/evals/main-new-interface/graders/behaviour-preserved.md +9 -0
- package/skills/sluice/evals/main-new-interface/graders/shape-agreed-before-building.md +13 -0
- package/skills/sluice/evals/main-new-interface/graders/sluice-fired.md +5 -0
- package/skills/sluice/evals/main-new-interface/graders/suite-was-run.md +6 -0
- package/skills/sluice/evals/main-new-interface/prompt.md +11 -0
- package/skills/sluice/evals/results/2026-09-20T01-51-28-540Z/aggregate-result.json +105 -0
- package/skills/sluice/evals/results/2026-09-20T01-51-28-540Z/report.html +300 -0
- package/skills/sluice/evals/results/2026-09-20T01-52-06-287Z/aggregate-result.json +122 -0
- package/skills/sluice/evals/results/2026-09-20T01-52-06-287Z/report.html +324 -0
- package/skills/sluice/evals/superpowers-conflict-stands-down/case.yaml +4 -0
- package/skills/sluice/evals/superpowers-conflict-stands-down/fixture.sh +39 -0
- package/skills/sluice/evals/superpowers-conflict-stands-down/graders/names-no-channel.md +8 -0
- package/skills/sluice/evals/superpowers-conflict-stands-down/graders/stands-down-once.md +9 -0
- package/skills/sluice/evals/superpowers-conflict-stands-down/prompt.md +10 -0
- package/skills/sluice/references/deep-channel.md +17 -6
- package/skills/sluice/references/status.md +45 -11
- package/skills/sluice/scripts/session-start.sh +52 -1
- package/skills/sluice/scripts/status.sh +96 -4
- package/skills/sluice/scripts/statusline.sh +6 -1
- package/skills/sluice/scripts/stop-guard.sh +148 -16
- package/skills/sluice/scripts/tree-snapshot.sh +75 -0
- package/skills/sluice/skill.json +1 -1
|
@@ -30,9 +30,13 @@ bash <skill-dir>/scripts/status.sh close
|
|
|
30
30
|
|
|
31
31
|
`--dir <path>` reads another tree, which is what the statusline uses. A tree
|
|
32
32
|
with a run of its own is read as itself; one with none resolves to the main
|
|
33
|
-
worktree of its set,
|
|
34
|
-
|
|
35
|
-
worktree>` finds
|
|
33
|
+
worktree of its set, and from there to whatever tree the run has since moved
|
|
34
|
+
into. So any tree in the set reads the one run, wherever in the set it lives,
|
|
35
|
+
and `--dir <implementer worktree>` finds it too. `show` prints a `tree` row
|
|
36
|
+
naming where the run is whenever that is not the tree it was pointed at, since
|
|
37
|
+
a run read from a tree it does not live in otherwise answers "where is this"
|
|
38
|
+
with the tree the reader is already in. `show --json` stays the state file
|
|
39
|
+
verbatim, and the row is not in it. Statuses
|
|
36
40
|
are `todo`, `active`, `review`, `done` and `blocked`. A new id needs `--name`;
|
|
37
41
|
after that every call is a bare flip, so keeping it current costs one command
|
|
38
42
|
per transition rather than a paragraph. `close` archives the run under
|
|
@@ -56,9 +60,10 @@ is pointed at, `--dir` if given and the current tree otherwise, once; a base
|
|
|
56
60
|
already on the row is kept. Issued from the controller's tree that is the
|
|
57
61
|
controller's HEAD, which is what an implementer worktree cut from that branch
|
|
58
62
|
starts at, so the default is right at dispatch. Where the implementer's tree
|
|
59
|
-
has moved on,
|
|
60
|
-
|
|
61
|
-
pointed at the
|
|
63
|
+
has moved on, point the command at that tree: `--dir <implementer worktree>`
|
|
64
|
+
resolves the run through the set and takes the base from the tree it was
|
|
65
|
+
pointed at, which is the one about to be built in. `--base $(git -C
|
|
66
|
+
<implementer worktree> rev-parse --short HEAD)` says the same thing outright.
|
|
62
67
|
|
|
63
68
|
`init` reports any other run live in a tree of the same set, without refusing:
|
|
64
69
|
two sessions in two worktrees is legal, and a run stranded in the main tree
|
|
@@ -103,6 +108,24 @@ that already holds a run or that is not a work tree of the same repository. A
|
|
|
103
108
|
submodule anchors on its own checkout, not the superproject's, and a directory
|
|
104
109
|
that is no git work tree keeps its run exactly where it sits.
|
|
105
110
|
|
|
111
|
+
`move` is half a step, and the half it cannot take is the session. The harness
|
|
112
|
+
holds one working directory and no command here reaches it, so the controller
|
|
113
|
+
goes on asking about the tree it is still sitting in: its statusline draws for
|
|
114
|
+
that tree, so does the SessionStart hook, and so does every bare `git`, build
|
|
115
|
+
and test command it runs. **Move the session into the tree the run went to**,
|
|
116
|
+
with the harness's worktree tool where there is one, entering the worktree by
|
|
117
|
+
path when it was cut by hand. `move` says so on the way out, because that is
|
|
118
|
+
the moment it is still cheap.
|
|
119
|
+
|
|
120
|
+
Until the session moves, the tree it came from reads the run rather than
|
|
121
|
+
denying it: `move` leaves `.sluice/run.at` at the main worktree, one line
|
|
122
|
+
naming the tree the run is in, and a tree with no run of its own follows it.
|
|
123
|
+
The note lives at the main worktree and nowhere else, so a run moved twice
|
|
124
|
+
forwards once rather than down a chain of trees, and it is followed only while
|
|
125
|
+
the run it names is really there. `close` removes a note naming its own tree,
|
|
126
|
+
because a note that outlived its run would hand the next run opened in that
|
|
127
|
+
tree to whoever reads the main tree.
|
|
128
|
+
|
|
106
129
|
One file for several writers is one file to contend on, so `init`, `task`,
|
|
107
130
|
`preflight`, `final`, `pause`, `resume`, `close` and `move` take a lock first, `move` taking the
|
|
108
131
|
destination tree's as well as its own: two flips issued at the same moment
|
|
@@ -290,11 +313,22 @@ every task done, a run idle for a day, and a turn where the harness says a
|
|
|
290
313
|
stop hook already fired, which is what keeps it from looping. That last rule
|
|
291
314
|
means it refuses once per turn and lets the next attempt through: a nudge, not
|
|
292
315
|
a wall. The gate keys on the git top level of the session's working
|
|
293
|
-
directory
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
316
|
+
directory: the run this tree answers for is the state beside it, or the state
|
|
317
|
+
it forwarded into a worktree when `move` sent the run on without the session.
|
|
318
|
+
Both are this tree's, and a controller that moved its run out and stayed put is
|
|
319
|
+
guarded for the rest of the run rather than quietly let go at the moment it
|
|
320
|
+
moved. The main-worktree fallback stays untaken, so a session in a worktree
|
|
321
|
+
that merely reads the set's run is let stop as before.
|
|
322
|
+
|
|
323
|
+
Once the run is in another tree the remedy changes with it. The refusal names
|
|
324
|
+
that tree and says to move the session into it, and it stops offering `close`:
|
|
325
|
+
from here that would archive a run live somewhere else, which may be another
|
|
326
|
+
session's, and talking anyone into that is the one thing this hook must not do.
|
|
327
|
+
The offer comes back when the run is in the tree the stop came from. None of
|
|
328
|
+
this makes staying put correct: open the run after the worktree is cut, or
|
|
329
|
+
`move` it and enter that worktree, since `move` relocates the run and not the
|
|
330
|
+
session, and every bare `git`, build and test command meanwhile still lands in
|
|
331
|
+
the tree the work left.
|
|
298
332
|
|
|
299
333
|
`pause --reason <text>` records why a run is standing still; `show` and the
|
|
300
334
|
statusline carry it, and `resume` clears it. A pause with no reason is refused,
|
|
@@ -31,12 +31,63 @@ if [ ! -t 0 ]; then
|
|
|
31
31
|
fi
|
|
32
32
|
cwd="$PWD"
|
|
33
33
|
source=""
|
|
34
|
+
sid=""
|
|
34
35
|
if [ -n "$input" ] && command -v jq >/dev/null 2>&1; then
|
|
35
36
|
got="$(printf '%s' "$input" | jq -r '.cwd // empty' 2>/dev/null || true)"
|
|
36
37
|
[ -n "$got" ] && cwd="$got"
|
|
37
38
|
source="$(printf '%s' "$input" | jq -r '.source // empty' 2>/dev/null || true)"
|
|
39
|
+
sid="$(printf '%s' "$input" | jq -r '.session_id // empty' 2>/dev/null || true)"
|
|
38
40
|
fi
|
|
39
41
|
|
|
42
|
+
# The baseline for stop-guard.sh's entry check. That guard asks whether the tree
|
|
43
|
+
# moved while this session held it, and this hook runs at the only moment the
|
|
44
|
+
# answer is still "not yet". Taken for every session, run or no run: a session
|
|
45
|
+
# that routes nothing is exactly the one the guard is there to catch.
|
|
46
|
+
#
|
|
47
|
+
# It lives beside the transcripts rather than in the tree, because a session
|
|
48
|
+
# that opens in someone's repo should not leave a directory behind in it, and
|
|
49
|
+
# because a baseline is spent the moment its session ends. A stamp that is
|
|
50
|
+
# missing, unreadable or stale costs the guard its check and nothing else, so
|
|
51
|
+
# every failure here is silent.
|
|
52
|
+
stamp_baseline() {
|
|
53
|
+
local sid="$1" src="$2" tree digest
|
|
54
|
+
[ -n "$sid" ] || return 0
|
|
55
|
+
case "$sid" in */* | .*) return 0 ;; esac
|
|
56
|
+
tree="$(git -C "$cwd" rev-parse --show-toplevel 2>/dev/null)" || return 0
|
|
57
|
+
[ -n "$tree" ] || return 0
|
|
58
|
+
|
|
59
|
+
local dir="${CLAUDE_CONFIG_DIR:-${HOME:-}/.claude}/sluice/stamps"
|
|
60
|
+
mkdir -p "$dir" 2>/dev/null || return 0
|
|
61
|
+
|
|
62
|
+
# This hook fires again on compact, resume and clear, all carrying the
|
|
63
|
+
# session id the first one carried, and they are not the same event.
|
|
64
|
+
#
|
|
65
|
+
# A compact happens inside a session that never let go of the tree, so its
|
|
66
|
+
# baseline still stands; re-stamping there would move it onto the work
|
|
67
|
+
# already done and read a half-finished session as an untouched tree, and a
|
|
68
|
+
# session long enough to compact is the one this exists for.
|
|
69
|
+
#
|
|
70
|
+
# A resume reopens a session that had stopped, and a clear throws away what
|
|
71
|
+
# it was doing. Both leave the old baseline describing a tree from some
|
|
72
|
+
# unbounded time ago, and everything that happened to it since, by a
|
|
73
|
+
# colleague or an editor or another session, would be charged to whoever
|
|
74
|
+
# reopens it. Those two start again, and give back the nudge the previous
|
|
75
|
+
# sitting may have spent.
|
|
76
|
+
case "$src" in
|
|
77
|
+
resume | clear) rm -f "$dir/$sid" "${dir%/stamps}/nudged/$sid" 2>/dev/null || true ;;
|
|
78
|
+
*) [ -e "$dir/$sid" ] && return 0 ;;
|
|
79
|
+
esac
|
|
80
|
+
|
|
81
|
+
# A stamp outlives nothing but its session, and the harness never deletes
|
|
82
|
+
# one. Without this the directory grows for the life of the machine.
|
|
83
|
+
find "$dir" "${dir%/stamps}/nudged" -type f -mtime +7 -delete 2>/dev/null || true
|
|
84
|
+
|
|
85
|
+
digest="$(bash "$here/tree-snapshot.sh" "$tree" 2>/dev/null)" || return 0
|
|
86
|
+
[ -n "$digest" ] || return 0
|
|
87
|
+
printf '%s\n%s\n' "$tree" "$digest" >"$dir/$sid" 2>/dev/null || true
|
|
88
|
+
}
|
|
89
|
+
stamp_baseline "$sid" "$source"
|
|
90
|
+
|
|
40
91
|
# No gate of its own beyond the directory existing: status.sh anchors on the
|
|
41
92
|
# main worktree of whatever tree it is given, which is what lets a session
|
|
42
93
|
# opened in a subdirectory or a linked worktree find the run, and on a tree
|
|
@@ -47,7 +98,7 @@ fi
|
|
|
47
98
|
shown="$(bash "$STATUS" show --dir "$cwd" 2>/dev/null)" || exit 0
|
|
48
99
|
[ -n "$shown" ] || exit 0
|
|
49
100
|
|
|
50
|
-
echo "A sluice run is live
|
|
101
|
+
echo "A sluice run is live. Its state, from .sluice/run.json:"
|
|
51
102
|
echo
|
|
52
103
|
echo "$shown"
|
|
53
104
|
echo
|
|
@@ -23,8 +23,11 @@
|
|
|
23
23
|
# status.sh close
|
|
24
24
|
#
|
|
25
25
|
# --dir <path> selects the tree to read (default: $PWD). State lives at
|
|
26
|
-
# <dir>/.sluice/run.json and closed runs at <dir>/.sluice/archive/.
|
|
27
|
-
#
|
|
26
|
+
# <dir>/.sluice/run.json and closed runs at <dir>/.sluice/archive/. A tree the
|
|
27
|
+
# run has moved out of keeps <dir>/.sluice/run.at, one line naming the tree it
|
|
28
|
+
# went to, so a session still sitting there resolves the run rather than reading
|
|
29
|
+
# it as gone. The directory ignores itself, so no project needs a .gitignore
|
|
30
|
+
# line for it.
|
|
28
31
|
#
|
|
29
32
|
# Exit: 0 ok, 1 the state could not be written, 2 no live run, 3 a run is
|
|
30
33
|
# already live (here, or at move's destination), 4 bad arguments, 5 jq missing,
|
|
@@ -107,6 +110,15 @@ if [ -z "$SUB" ]; then
|
|
|
107
110
|
exit 4
|
|
108
111
|
fi
|
|
109
112
|
|
|
113
|
+
# One function rather than the same pipeline at three call sites, because they
|
|
114
|
+
# have to agree: the tree resolution anchors on and the tree `move` and `close`
|
|
115
|
+
# leave their forwarding note in are the same tree by definition, and a note
|
|
116
|
+
# left anywhere else is a note nothing reads. Empty for a directory that is no
|
|
117
|
+
# git work tree.
|
|
118
|
+
main_tree() { # <dir>
|
|
119
|
+
git -C "$1" worktree list --porcelain 2>/dev/null | sed -n '1s/^worktree //p'
|
|
120
|
+
}
|
|
121
|
+
|
|
110
122
|
# A tree's own run comes first, and only a tree with none reads the set's. Two
|
|
111
123
|
# layouts share this script and pull opposite ways. A deep run plans in the main
|
|
112
124
|
# tree and cuts implementer worktrees after the plan: the run directory ignores
|
|
@@ -130,9 +142,29 @@ fi
|
|
|
130
142
|
ORIG_DIR="$DIR"
|
|
131
143
|
if [ "$SUB" != "init" ] && [ ! -f "$DIR/.sluice/run.json" ] \
|
|
132
144
|
&& [ "$(git -C "$DIR" rev-parse --is-inside-work-tree 2>/dev/null)" = "true" ]; then
|
|
133
|
-
MAIN_TREE="$(
|
|
145
|
+
MAIN_TREE="$(main_tree "$DIR")"
|
|
134
146
|
if [ -n "${MAIN_TREE:-}" ] && [ -d "$MAIN_TREE" ]; then
|
|
135
147
|
DIR="$MAIN_TREE"
|
|
148
|
+
|
|
149
|
+
# Then forward, where the main tree's run has moved on into a worktree.
|
|
150
|
+
# A `move` is half a step: it relocates the state and cannot relocate the
|
|
151
|
+
# session, the harness holding one working directory that no command here
|
|
152
|
+
# reaches. So the controller's session goes on asking about the tree it
|
|
153
|
+
# still sits in, and per-tree resolution answers that the run is gone --
|
|
154
|
+
# to its statusline, to the SessionStart hook and to a bare `show`, all
|
|
155
|
+
# on a run that is live two directories away.
|
|
156
|
+
#
|
|
157
|
+
# The note lives at the main tree and nowhere else, so a run moved twice
|
|
158
|
+
# forwards once rather than down a chain, and so the tree every other
|
|
159
|
+
# tree in the set already resolves to is the tree that knows. It is
|
|
160
|
+
# followed only while the run it names is really there: a note outliving
|
|
161
|
+
# its run is stale, not a second answer.
|
|
162
|
+
if [ ! -f "$DIR/.sluice/run.json" ] && [ -f "$DIR/.sluice/run.at" ]; then
|
|
163
|
+
# `read`, not `cat`: a builtin, and the first line is the whole note.
|
|
164
|
+
AT=""
|
|
165
|
+
IFS= read -r AT <"$DIR/.sluice/run.at" 2>/dev/null || true
|
|
166
|
+
[ -n "$AT" ] && [ -f "$AT/.sluice/run.json" ] && DIR="$AT"
|
|
167
|
+
fi
|
|
136
168
|
fi
|
|
137
169
|
fi
|
|
138
170
|
|
|
@@ -306,6 +338,16 @@ mk_dir() { # <directory to create under .sluice> [<tree whose .sluice it is, def
|
|
|
306
338
|
[ -e "$ignore" ] || printf '*\n' >"$ignore" 2>/dev/null || true
|
|
307
339
|
}
|
|
308
340
|
|
|
341
|
+
# Where the set's run went, written at the main tree for resolution to follow.
|
|
342
|
+
# Not `mk_dir`, which exits: this runs after the state has already arrived in
|
|
343
|
+
# the destination, and a note that could not be written is a blank statusline
|
|
344
|
+
# in one tree rather than a move that failed. The caller reports it instead.
|
|
345
|
+
leave_note() { # <main tree> <tree the run is now in>
|
|
346
|
+
mkdir -p "$1/.sluice" 2>/dev/null || return 1
|
|
347
|
+
[ -e "$1/.sluice/.gitignore" ] || printf '*\n' >"$1/.sluice/.gitignore" 2>/dev/null || true
|
|
348
|
+
printf '%s\n' "$2" >"$1/.sluice/run.at" 2>/dev/null
|
|
349
|
+
}
|
|
350
|
+
|
|
309
351
|
# Written through a temporary file so an interrupted write cannot leave the
|
|
310
352
|
# run state half-serialised, which would read as a corrupted run rather than
|
|
311
353
|
# as a failed command.
|
|
@@ -582,10 +624,28 @@ case "$SUB" in
|
|
|
582
624
|
exit 0
|
|
583
625
|
fi
|
|
584
626
|
|
|
627
|
+
# Where the run is, said only when that is not the tree the command was
|
|
628
|
+
# pointed at. A run read from a tree it does not live in answers "where
|
|
629
|
+
# is this" silently wrong otherwise: the reader takes the tree they are
|
|
630
|
+
# in, which after a `move` is the one tree the run is not in.
|
|
631
|
+
#
|
|
632
|
+
# A directory inside the tree holding the run is that tree, one level
|
|
633
|
+
# down, not somewhere else -- without that, the row would fire on every
|
|
634
|
+
# session that works from a subdirectory.
|
|
635
|
+
ELSEWHERE=""
|
|
636
|
+
run_tree="$(cd "$DIR" 2>/dev/null && pwd -P)"
|
|
637
|
+
asked="$(cd "$ORIG_DIR" 2>/dev/null && pwd -P)"
|
|
638
|
+
if [ -n "$run_tree" ] && [ -n "$asked" ]; then
|
|
639
|
+
case "$asked" in
|
|
640
|
+
"$run_tree" | "$run_tree"/*) ;;
|
|
641
|
+
*) ELSEWHERE="$run_tree" ;;
|
|
642
|
+
esac
|
|
643
|
+
fi
|
|
644
|
+
|
|
585
645
|
# Header and rows are laid out from the same widths, so the two cannot
|
|
586
646
|
# drift apart, and an over-long value is clipped with a marker rather
|
|
587
647
|
# than silently reading as the whole value.
|
|
588
|
-
jq -r --argjson now "$(date -u +%s)" '
|
|
648
|
+
jq -r --argjson now "$(date -u +%s)" --arg elsewhere "$ELSEWHERE" '
|
|
589
649
|
def dash: if . == null or . == "" then "-" else . end;
|
|
590
650
|
# Same reason as the statusline render: state written before the check
|
|
591
651
|
# on the way in, or edited by hand, holds bytes a terminal would act on
|
|
@@ -601,6 +661,7 @@ case "$SUB" in
|
|
|
601
661
|
$c[6]] | join(" "));
|
|
602
662
|
([.tasks[]? | select(.status == "done")] | length) as $done
|
|
603
663
|
| ["sluice \(.channel | clean) · \(.topic | clean) · \($done)/\(.tasks | length) done"]
|
|
664
|
+
+ (if $elsewhere == "" then [] else ["tree \($elsewhere | clean)"] end)
|
|
604
665
|
+ ["plan \(.plan | dash | clean)"]
|
|
605
666
|
+ ["record \(.record | dash | clean)"]
|
|
606
667
|
# Past a day since the last write the run is idle, and that is said
|
|
@@ -797,7 +858,26 @@ case "$SUB" in
|
|
|
797
858
|
exit 3
|
|
798
859
|
fi
|
|
799
860
|
mv "$STATE" "$DEST_STATE" || { err "could not move $STATE to $DEST_STATE"; exit 1; }
|
|
861
|
+
|
|
862
|
+
# After the state has arrived, never before: a note pointing at a run
|
|
863
|
+
# that never got there is worse than no note, being indistinguishable
|
|
864
|
+
# from one pointing at a run that did.
|
|
865
|
+
NOTE_TREE="$(main_tree "$TO")"
|
|
866
|
+
if [ -n "$NOTE_TREE" ] && [ -d "$NOTE_TREE" ]; then
|
|
867
|
+
if [ "$NOTE_TREE" = "$TO" ]; then
|
|
868
|
+
# The run is back where resolution already looks, so a note would
|
|
869
|
+
# only point the main tree at itself.
|
|
870
|
+
rm -f "$NOTE_TREE/.sluice/run.at"
|
|
871
|
+
elif ! leave_note "$NOTE_TREE" "$TO"; then
|
|
872
|
+
err "note: the run moved, but $NOTE_TREE/.sluice/run.at could not be written, so a session in $NOTE_TREE will read no run until it moves to $TO"
|
|
873
|
+
fi
|
|
874
|
+
fi
|
|
875
|
+
|
|
800
876
|
echo "moved $(jq -r '.topic // "run"' "$DEST_STATE" 2>/dev/null || echo run) to $TO"
|
|
877
|
+
# The session is the half of the move no command can make. Left where it
|
|
878
|
+
# was, every bare git, build and test command it runs still lands in the
|
|
879
|
+
# tree the run just left.
|
|
880
|
+
echo "move this session there too, with the harness's worktree tool where it has one; every status.sh call from elsewhere needs --dir $TO"
|
|
801
881
|
;;
|
|
802
882
|
|
|
803
883
|
close)
|
|
@@ -844,6 +924,18 @@ case "$SUB" in
|
|
|
844
924
|
n=$((n + 1))
|
|
845
925
|
done
|
|
846
926
|
mv "$STATE" "$dest" || { err "could not archive $STATE"; exit 1; }
|
|
927
|
+
|
|
928
|
+
# A note naming this tree has outlived the run it forwarded to. Left
|
|
929
|
+
# behind, it does not go quiet: the next run opened in this tree inherits
|
|
930
|
+
# the forward and reads as the main tree's, though nobody there opened
|
|
931
|
+
# it. Only a note naming this tree is ours to remove -- one naming
|
|
932
|
+
# another tree belongs to a run this close knows nothing about.
|
|
933
|
+
NOTE_TREE="$(main_tree "$DIR")"
|
|
934
|
+
if [ -n "$NOTE_TREE" ] && [ -f "$NOTE_TREE/.sluice/run.at" ]; then
|
|
935
|
+
AT=""
|
|
936
|
+
IFS= read -r AT <"$NOTE_TREE/.sluice/run.at" 2>/dev/null || true
|
|
937
|
+
[ "$AT" = "$(cd "$DIR" && pwd -P)" ] && rm -f "$NOTE_TREE/.sluice/run.at"
|
|
938
|
+
fi
|
|
847
939
|
# The summary needs parseable state and close is the one command that
|
|
848
940
|
# does not, so an unreadable run still gets a line naming where it went.
|
|
849
941
|
[ -n "$summary" ] || summary="closed $(basename "$dest"): state was unreadable, no summary"
|
|
@@ -65,7 +65,11 @@ done
|
|
|
65
65
|
d="$(cd "$DIR" 2>/dev/null && pwd -P)" || exit 0
|
|
66
66
|
[ -n "$d" ] || exit 0
|
|
67
67
|
|
|
68
|
-
#
|
|
68
|
+
# Five answers end the walk. A state file is a run, wherever it was found. So is
|
|
69
|
+
# a forwarding note beside where one used to be: the run moved out into a
|
|
70
|
+
# worktree, this tree is where the session that moved it is still sitting, and
|
|
71
|
+
# testing only the state file blanked its bar on a live run -- ahead of the
|
|
72
|
+
# `.git` directory below, which would otherwise answer for this tree first. A
|
|
69
73
|
# `.git` that is a regular file is a linked worktree or a submodule, which may
|
|
70
74
|
# hold no state of its own and still belong to a set that does, so it is a maybe
|
|
71
75
|
# and status.sh resolves it. A `.git` that is a directory is the top of an
|
|
@@ -78,6 +82,7 @@ d="$(cd "$DIR" 2>/dev/null && pwd -P)" || exit 0
|
|
|
78
82
|
# would put the gate's cost in the same range as the render it is avoiding.
|
|
79
83
|
while :; do
|
|
80
84
|
[ -f "$d/.sluice/run.json" ] && break
|
|
85
|
+
[ -f "$d/.sluice/run.at" ] && break
|
|
81
86
|
[ -f "$d/.git" ] && break
|
|
82
87
|
[ -d "$d/.git" ] && exit 0
|
|
83
88
|
[ "$d" = "${HOME:-}" ] && exit 0
|
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
# Every stop that is a real stop is let through: no run, a channel other than
|
|
11
11
|
# deep, pre-flight not yet answered (that stop is owed), a blocked task, a run
|
|
12
12
|
# paused on purpose with `status.sh pause --reason`, every task done (the
|
|
13
|
-
# handback), a run idle for a day, a run
|
|
14
|
-
#
|
|
15
|
-
# this turn, which is what keeps this from looping. One refusal per turn, then:
|
|
13
|
+
# handback), a run idle for a day, a run another tree owns rather than one this
|
|
14
|
+
# tree moved out, and any attempt where the harness says a stop hook already
|
|
15
|
+
# fired this turn, which is what keeps this from looping. One refusal per turn, then:
|
|
16
16
|
# a nudge with the state in it rather than a wall.
|
|
17
17
|
#
|
|
18
18
|
# Reads the harness's stop JSON on stdin for `cwd` and `stop_hook_active`. To
|
|
@@ -45,30 +45,147 @@ 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
|
-
# The session's own tree, and only that
|
|
58
|
-
#
|
|
59
|
-
#
|
|
60
|
-
#
|
|
61
|
-
#
|
|
158
|
+
# The run the session's own tree answers for, and only that: the state beside
|
|
159
|
+
# it, or the state it forwarded into a worktree when `move` sent the run on
|
|
160
|
+
# without the session. status.sh also lets a tree with no run of its own read
|
|
161
|
+
# the main worktree's, and that one is not taken here: a Stop in such a tree may
|
|
162
|
+
# be an unrelated session, and a remedy printed to it would reach into somebody
|
|
163
|
+
# else's run.
|
|
164
|
+
#
|
|
165
|
+
# The forward is read here rather than left to status.sh's resolution because
|
|
166
|
+
# the two questions differ. Resolution answers "which run can this tree read",
|
|
167
|
+
# which is the right question for a render and the wrong one for a refusal. The
|
|
168
|
+
# tree named in the note is then passed as `--dir`, so what follows asks about
|
|
169
|
+
# one named tree and carries no layout knowledge of its own.
|
|
62
170
|
top="$(git -C "$cwd" rev-parse --show-toplevel 2>/dev/null)"
|
|
63
171
|
[ -n "$top" ] || top="$cwd"
|
|
64
|
-
[ -f "$top/.sluice/run.json" ]
|
|
172
|
+
if [ -f "$top/.sluice/run.json" ]; then
|
|
173
|
+
tree="$top"
|
|
174
|
+
elif [ -f "$top/.sluice/run.at" ]; then
|
|
175
|
+
tree=""
|
|
176
|
+
IFS= read -r tree <"$top/.sluice/run.at" 2>/dev/null || exit 0
|
|
177
|
+
# A note outliving the run it named is stale, not a run to refuse a stop over.
|
|
178
|
+
[ -n "$tree" ] && [ -f "$tree/.sluice/run.json" ] || exit 0
|
|
179
|
+
else
|
|
180
|
+
exit 0
|
|
181
|
+
fi
|
|
65
182
|
|
|
66
|
-
run="$(bash "$STATUS" show --json --dir "$
|
|
183
|
+
run="$(bash "$STATUS" show --json --dir "$tree" 2>/dev/null)" || exit 0
|
|
67
184
|
[ -n "$run" ] || exit 0
|
|
68
185
|
|
|
69
186
|
# One JSON object out, read back with jq: the topic is user text, and word
|
|
70
187
|
# splitting it would truncate at the first space and glob on the rest.
|
|
71
|
-
verdict="$(printf '%s' "$run" | jq -c --argjson now "$(date -u +%s)" '
|
|
188
|
+
verdict="$(printf '%s' "$run" | jq -c --argjson now "$(date -u +%s)" --arg tree "$tree" --arg here "$top" '
|
|
72
189
|
(.tasks // []) as $t
|
|
73
190
|
| ([$t[] | select(.status == "done")] | length) as $done
|
|
74
191
|
| ([$t[] | select(.status == "blocked")] | length) as $blocked
|
|
@@ -84,18 +201,33 @@ verdict="$(printf '%s' "$run" | jq -c --argjson now "$(date -u +%s)" '
|
|
|
84
201
|
# A run nobody has written to for a day is a stale run, not a live one;
|
|
85
202
|
# refusing its stop would press an abandoned plan on whoever opened here.
|
|
86
203
|
elif $idle_h >= 24 then {block: false}
|
|
87
|
-
# The topic
|
|
88
|
-
# would be acted on by the terminal rather than read. State
|
|
89
|
-
# status.sh refused those, or edited by hand, can still hold
|
|
204
|
+
# The topic and the tree land in a reason the harness prints, so a control
|
|
205
|
+
# byte in either would be acted on by the terminal rather than read. State
|
|
206
|
+
# written before status.sh refused those, or edited by hand, can still hold
|
|
207
|
+
# one, and so can a path.
|
|
90
208
|
else {block: true, progress: "\($done)/\($t | length)",
|
|
91
|
-
topic: (.topic // "run" | gsub("[\u0000-\u001f\u007f]"; ""))
|
|
209
|
+
topic: (.topic // "run" | gsub("[\u0000-\u001f\u007f]"; "")),
|
|
210
|
+
tree: ($tree | gsub("[\u0000-\u001f\u007f]"; "")),
|
|
211
|
+
elsewhere: ($tree != $here)}
|
|
92
212
|
end
|
|
93
213
|
' 2>/dev/null)" || exit 0
|
|
94
214
|
|
|
95
215
|
[ "$(printf '%s' "$verdict" | jq -r '.block' 2>/dev/null)" = "true" ] || exit 0
|
|
96
216
|
|
|
217
|
+
# Two remedies, because the last line of the local one is wrong once the run
|
|
218
|
+
# has moved: `close` from here would archive a run that is live in another tree
|
|
219
|
+
# and may be another session's, which is the one thing this hook must never talk
|
|
220
|
+
# anyone into. What replaces it is the step `move` could not take -- the session
|
|
221
|
+
# following the run -- because a controller guarded here is a controller sitting
|
|
222
|
+
# in the tree its own run left.
|
|
97
223
|
printf '%s' "$verdict" | jq 2>/dev/null '{
|
|
98
224
|
decision: "block",
|
|
99
|
-
reason: ("sluice: the deep run \(.topic) is \(.progress) done with tasks still to go and nothing marked blocked or paused, so ending the turn here hands a live run back with nothing for your partner to decide.
|
|
225
|
+
reason: ("sluice: the deep run \(.topic) is \(.progress) done with tasks still to go and nothing marked blocked or paused, so ending the turn here hands a live run back with nothing for your partner to decide."
|
|
226
|
+
+ (if .elsewhere then " The run lives in \(.tree), not in this tree: `move` relocated the run and not this session. If it is yours, move this session into that tree -- the worktree tool in your harness enters one that already exists -- and go on from there." else "" end)
|
|
227
|
+
+ " Continue: run status.sh ready and dispatch the next wave in this same message. If a task genuinely needs them, mark it: status.sh task <id> --status blocked. If the run has to stand still for a reason, record it: status.sh pause --reason \"<why>\", then say so and stop."
|
|
228
|
+
+ (if .elsewhere
|
|
229
|
+
then " If the run is not yours, it belongs to the session working in \(.tree): say so and stop, rather than closing it from here."
|
|
230
|
+
else " If this run is not the work you were asked to do, it was left open: status.sh close."
|
|
231
|
+
end))
|
|
100
232
|
}'
|
|
101
233
|
exit 0
|
|
@@ -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"
|
package/skills/sluice/skill.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sluice",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.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",
|