@iceinvein/agent-skills 0.23.5 → 0.23.7

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iceinvein/agent-skills",
3
- "version": "0.23.5",
3
+ "version": "0.23.7",
4
4
  "description": "Install agent skills into AI coding tools",
5
5
  "author": "iceinvein",
6
6
  "license": "MIT",
package/skills/index.json CHANGED
@@ -293,9 +293,9 @@
293
293
  },
294
294
  {
295
295
  "name": "sluice",
296
- "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. Two Stop hooks hold a handback: one refuses a tree that moved since its last recorded check, the other refuses a changed existing test not proven to fail on the session's starting code (or recorded with a reason). Claude Code only; stands down where the superpowers pipeline governs the repo.",
296
+ "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), lists every live deep run and routed fast or main session on the machine in one board.sh view (drawn once, watched, or as JSON), 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. Two Stop hooks hold a handback: one refuses a tree that moved since its last recorded check, the other refuses a changed existing test not proven to fail on the session's starting code (or recorded with a reason). Claude Code only; stands down where the superpowers pipeline governs the repo.",
297
297
  "type": "prompt",
298
- "version": "0.25.5"
298
+ "version": "0.26.1"
299
299
  },
300
300
  {
301
301
  "name": "temporal-coupling-detector",
@@ -29,14 +29,21 @@ bash <skill-dir>/scripts/status.sh close
29
29
  bash <skill-dir>/scripts/status.sh route deep
30
30
  bash <skill-dir>/scripts/status.sh verify -- bun test
31
31
  bash <skill-dir>/scripts/status.sh verify --none "docs only, nothing executes them"
32
+ bash <skill-dir>/scripts/board.sh
33
+ bash <skill-dir>/scripts/board.sh --watch 5
34
+ bash <skill-dir>/scripts/board.sh --json
32
35
  ```
33
36
 
34
37
  `route` and `verify` are the two exceptions: neither needs a run.
35
38
 
36
- `route` reads and writes no state, and prints `sluice: <channel> channel` so
37
- the channel lands in the transcript as a tool input, which is kept as written
38
- where the announcement's prose may not be. `references/meter.md` says why that
39
- matters.
39
+ `route` prints `sluice: <channel> channel` so the channel lands in the
40
+ transcript as a tool input, which is kept as written where the announcement's
41
+ prose may not be. `references/meter.md` says why that matters. It opens no run,
42
+ but with a session id (`--session`, defaulting to `$CLAUDE_CODE_SESSION_ID`) it
43
+ writes that session's entry on the board, because a route is the only sign a
44
+ `fast` or `main` session leaves. With no id it writes nothing: there is nobody
45
+ to name, and none is made up. Either way it exits 0 once the channel is
46
+ printed, since the announcement is the part that matters.
40
47
 
41
48
  `verify -- <cmd>` runs the check from the tree's top level, passes a failing
42
49
  check's exit code through, and on a pass writes the tree's content digest to
@@ -483,6 +490,90 @@ cannot redirect. That is the same argument the channel announcement makes, and
483
490
  the statusline is where it holds for the hour after the announcement scrolled
484
491
  away.
485
492
 
493
+ ## Board
494
+
495
+ Everything above answers from inside one worktree set. `scripts/board.sh`
496
+ answers across the machine: every live `deep` run and every routed `fast` or
497
+ `main` session, drawn once, or redrawn in place every two seconds (or every
498
+ `<seconds>` given) with `--watch` until interrupted. `<seconds>` is a positive
499
+ whole number read in base ten, leading zeros allowed (`05` is five, `08`
500
+ eight); `0`, `00` or anything else exits 4. `--json` prints the combined state
501
+ for another reader instead, with the counts the header shows, and does not
502
+ combine with `--watch`.
503
+
504
+ It reads an index of pointers, never copies, at `$SLUICE_BOARD_DIR`, else
505
+ `$CLAUDE_CONFIG_DIR/sluice/board`, else `$HOME/.claude/sluice/board`; with none
506
+ of the three set nothing is written and the board is empty. `runs/` holds one
507
+ file per tree with a live run, naming the tree, and `sessions/<id>.json` one
508
+ per routed session. `run.json` stays the single authority on a run, so the
509
+ board can never show a stale copy of one. `SLUICE_BOARD_DIR` exists so the
510
+ tests can point every script at a temporary directory and never touch the real
511
+ board: the suite's preload sets it to a fresh one and makes every child a test
512
+ spawns inherit it, since Bun hands a child spawned with no `env` the
513
+ environment bun started with rather than the one the preload changed.
514
+
515
+ `scripts/board-index.sh` is the only writer. `init` adds the run's pointer,
516
+ `move` re-points it and `close` drops it. A task flip writes nothing to the
517
+ board, because a plan's flips are many and each write is a process; the cost
518
+ is that a run opened before the board existed stays off it until it is
519
+ closed and a new one opened. `route` writes the session's
520
+ entry as `working`, a later route in the same session overwriting it, so an
521
+ escalation from `fast` to `main` shows as `main`. The Stop hook marks a `fast`
522
+ or `main` entry `handed back` when it lets a stop through, and only then: a
523
+ refused stop is the session still working. It marks a `deep` entry the same
524
+ way when no run is live (the design stop before any run exists, the stop
525
+ after `close`, a session that routed deep and never opened a run), and leaves
526
+ it as it is while one is, since the board draws that run's bar for it. The
527
+ run is found the way the hook's run guard finds one, taking the first in this
528
+ order: a run in the stop's own tree, then the run found through the session's
529
+ pointer at the main tree, then the tree's `run.at` forward, never the main
530
+ tree's run read from a runless worktree. That first run counts as live only
531
+ when it is the session's own: its `run.json` names that session. A run that
532
+ names no session is no session's own, matching the board, which hides a
533
+ session row only behind a run naming it. When it names none or another session,
534
+ whether sitting in the stop's own tree or reached through the forward, or its
535
+ `run.json` cannot be read, the entry is marked `handed back`, and no later run
536
+ in the order is looked at. A failed
537
+ board write is one
538
+ `status.sh: note: board not updated: ...` line on stderr and never changes the
539
+ command's result, because the board is a view across trees and not part of
540
+ any run.
541
+
542
+ The header counts each channel's working and handed-back work. A drawn run
543
+ counts as working in its `run.json` channel: a `deep` run toward `N deep`
544
+ alongside the working `deep` sessions, a `main` run toward `N main working`, a
545
+ `fast` run toward `N fast working`. A run naming any other channel, or none,
546
+ counts toward `deep`, since it still draws as a run's bar. `--json` `counts`
547
+ holds the same numbers: `counts.working.<channel>` is that channel's working
548
+ sessions plus its drawn runs (so `working.deep` includes the `deep` runs, where
549
+ before it held the working `deep` sessions only and the header added the runs
550
+ itself), and `counts.runs` is every run drawn, whatever its channel.
551
+
552
+ A run block is `status.sh line --full` for its tree, unchanged, so it reads
553
+ exactly as the statusline does. Blocked runs sort first, then those with a task
554
+ active or in review, then the rest. A session row shows its channel, the tree's
555
+ name and its state: `working` with the time since it was routed, `handed back`
556
+ with the time since. The channel, name and state columns are padded to the
557
+ widest value among that frame's session rows, two spaces apart, so the ages
558
+ line up. Width is counted in characters of the drawn text (jq's count, so
559
+ the same under a C locale), never colour codes. A wide glyph such as `日` that
560
+ takes two terminal cells counts as one, so a row holding one draws a cell
561
+ short; a name stored decomposed (a letter plus a combining accent, as some
562
+ filesystems write paths) counts one column per code point, so it can draw a
563
+ cell short too. A `deep` session row reads `handed back` once a turn ends with
564
+ no run live for it, and otherwise stays `working` while its run's bar is
565
+ drawn. A session of any channel whose id is a drawn run's `session` is not
566
+ listed or counted again, so one piece of work is one row and one count. Session
567
+ entries past a day since their last update are hidden, the same one-day line
568
+ `show` uses for an idle run, and the next `init`, or `route` with a session
569
+ id, removes them along with pointers to trees that no longer hold a run. An
570
+ entry that cannot be read is skipped on its own, so one bad file costs one row
571
+ rather than the board.
572
+
573
+ The board writes nothing, so it takes no lock. It needs `jq` and exits 5
574
+ without it, and 6 when the board cannot be read at all, because unlike the
575
+ statusline it has somewhere to put an error.
576
+
486
577
  ## Friction
487
578
 
488
579
  "I know which task I'm on." You do, until this session is summarised. The file
@@ -0,0 +1,160 @@
1
+ #!/usr/bin/env bash
2
+ # sluice board index: which trees hold a live run and which sessions are
3
+ # routed where, kept in one place so a board can list them without walking
4
+ # every worktree on the machine.
5
+ #
6
+ # board-index.sh run-add <tree>
7
+ # board-index.sh run-drop <tree>
8
+ # board-index.sh session <id> <fast|main|deep> <tree>
9
+ # board-index.sh handback <id> [--keep-deep]
10
+ # board-index.sh prune
11
+ #
12
+ # The root is $SLUICE_BOARD_DIR, else $CLAUDE_CONFIG_DIR/sluice/board, else
13
+ # $HOME/.claude/sluice/board. With none of the three set there is nowhere safe
14
+ # to write, so every verb exits 0 having written nothing. Runs live at
15
+ # <root>/runs/<key>, one line naming the tree, where <key> is the tree with %
16
+ # encoded as %25 and / as %2F. Sessions live at <root>/sessions/<id>.json.
17
+ #
18
+ # `--keep-deep` is for a stop while the session's run is live: the board draws
19
+ # that run's bar for a deep session, so its entry stays as it is.
20
+ #
21
+ # Exit: 0 ok, 1 could not write, 4 bad arguments, 5 jq missing.
22
+
23
+ set -u
24
+
25
+ err() { echo "board-index.sh: $*" >&2; }
26
+
27
+ usage() {
28
+ echo "usage: board-index.sh run-add <tree> | run-drop <tree> | session <id> <fast|main|deep> <tree> | handback <id> [--keep-deep] | prune" >&2
29
+ exit 4
30
+ }
31
+
32
+ now() { date -u +%Y-%m-%dT%H:%M:%SZ; }
33
+
34
+ key() {
35
+ local k="${1//%/%25}"
36
+ echo "${k//\//%2F}"
37
+ }
38
+
39
+ # Writes $2 to $1 through a temp file so a reader never sees half an entry. The
40
+ # temp is dotted so the runs/* and sessions/*.json globs never match it. Never
41
+ # call this on the right of a pipe: its exit would end only the subshell.
42
+ put() {
43
+ local tmp
44
+ tmp="$(dirname "$1")/.$(basename "$1").tmp.$$"
45
+ if ! { printf '%s\n' "$2" >"$tmp"; } 2>/dev/null || ! mv "$tmp" "$1" 2>/dev/null; then
46
+ rm -f "$tmp"
47
+ err "could not write $1"
48
+ exit 1
49
+ fi
50
+ }
51
+
52
+ need_jq() {
53
+ command -v jq >/dev/null 2>&1 || { err "jq is required"; exit 5; }
54
+ }
55
+
56
+ verb="${1:-}"
57
+ [ $# -gt 0 ] && shift
58
+ case "$verb" in
59
+ run-add|run-drop) [ $# -eq 1 ] || usage ;;
60
+ handback)
61
+ [ $# -eq 1 ] || { [ $# -eq 2 ] && [ "$2" = --keep-deep ]; } || usage
62
+ ;;
63
+ session) [ $# -eq 3 ] || usage ;;
64
+ prune) [ $# -eq 0 ] || usage ;;
65
+ *) usage ;;
66
+ esac
67
+
68
+ if [ -n "${SLUICE_BOARD_DIR:-}" ]; then
69
+ ROOT="$SLUICE_BOARD_DIR"
70
+ elif [ -n "${CLAUDE_CONFIG_DIR:-}" ]; then
71
+ ROOT="$CLAUDE_CONFIG_DIR/sluice/board"
72
+ elif [ -n "${HOME:-}" ]; then
73
+ ROOT="$HOME/.claude/sluice/board"
74
+ else
75
+ exit 0
76
+ fi
77
+
78
+ case "$verb" in
79
+ run-add)
80
+ [ -n "$1" ] || exit 0
81
+ path="$ROOT/runs/$(key "$1")"
82
+ mkdir -p "$ROOT/runs" 2>/dev/null || { err "could not write $path"; exit 1; }
83
+ put "$path" "$1"
84
+ ;;
85
+ run-drop)
86
+ [ -n "$1" ] || exit 0
87
+ path="$ROOT/runs/$(key "$1")"
88
+ # Checked rather than read off rm's status: GNU rm -f takes "not a
89
+ # directory" for "nothing there" and exits 0 where BSD rm fails.
90
+ for dir in "$ROOT" "$ROOT/runs"; do
91
+ [ ! -e "$dir" ] || [ -d "$dir" ] || { err "could not remove $path"; exit 1; }
92
+ done
93
+ rm -f "$path" 2>/dev/null && [ ! -e "$path" ] || { err "could not remove $path"; exit 1; }
94
+ ;;
95
+ session)
96
+ id="$1" channel="$2" tree="$3"
97
+ case "$id" in
98
+ */*|.*) err "bad session id: $id"; exit 4 ;;
99
+ esac
100
+ case "$channel" in
101
+ fast|main|deep) ;;
102
+ *) err "unknown channel: $channel"; exit 4 ;;
103
+ esac
104
+ [ -n "$id" ] && [ -n "$tree" ] || exit 0
105
+ need_jq
106
+ path="$ROOT/sessions/$id.json"
107
+ mkdir -p "$ROOT/sessions" 2>/dev/null || { err "could not write $path"; exit 1; }
108
+ t="$(now)"
109
+ entry="$(jq -n --arg s "$id" --arg c "$channel" --arg tr "$tree" --arg t "$t" \
110
+ '{session: $s, channel: $c, tree: $tr, routed: $t, state: "working", updated: $t}')"
111
+ put "$path" "$entry"
112
+ ;;
113
+ handback)
114
+ id="$1" keep_deep="${2:-}"
115
+ case "$id" in
116
+ ''|*/*|.*) exit 0 ;;
117
+ esac
118
+ path="$ROOT/sessions/$id.json"
119
+ [ -f "$path" ] || exit 0
120
+ need_jq
121
+ channel="$(jq -r '.channel' "$path" 2>/dev/null)" || exit 0
122
+ [ -n "$keep_deep" ] && [ "$channel" = deep ] && exit 0
123
+ updated="$(jq --arg t "$(now)" '.state = "handed back" | .updated = $t' "$path")" || exit 0
124
+ put "$path" "$updated"
125
+ ;;
126
+ prune)
127
+ need_jq
128
+ # The builtin read rather than a process per pointer, because prune runs
129
+ # on every init and route and pointers pile up; read whole, so a tree
130
+ # path holding a newline still names its tree. read -d '' stops at EOF
131
+ # with a non-zero status, so a failed open is told from a short read by
132
+ # whether the file is readable; a pointer that cannot be read is kept.
133
+ for f in "$ROOT"/runs/*; do
134
+ [ -f "$f" ] || continue
135
+ tree=""
136
+ { IFS= read -r -d '' tree; } 2>/dev/null <"$f" || [ -n "$tree" ] || [ -r "$f" ] || continue
137
+ tree="${tree%$'\n'}"
138
+ [ -f "$tree/.sluice/run.json" ] || rm -f "$f"
139
+ done
140
+ # One jq for every session rather than one each, because prune runs on
141
+ # every route and a jq per file made each route slower than the last day
142
+ # of sessions. Contents go on stdin as NUL-separated name and contents
143
+ # records, as board.sh feeds them, so no count of files meets the argv
144
+ # limit. Anything unreadable or unparseable comes back to be removed.
145
+ for f in "$ROOT"/sessions/*.json; do
146
+ [ -f "$f" ] || continue
147
+ entry=""
148
+ { IFS= read -r -d '' entry; } 2>/dev/null <"$f"
149
+ printf '%s\0%s\0' "$f" "$entry"
150
+ done | jq -R -s -j '
151
+ (split("\u0000") | .[:-1]) as $p
152
+ | range(0; $p | length; 2)
153
+ | select(($p[. + 1] | try (fromjson | (now - (.updated | fromdateiso8601)) <= 86400) catch false) != true)
154
+ | $p[.] + "\u0000"
155
+ ' 2>/dev/null | while IFS= read -r -d '' f; do
156
+ rm -f "$f"
157
+ done
158
+ ;;
159
+ esac
160
+ exit 0
@@ -0,0 +1,304 @@
1
+ #!/usr/bin/env bash
2
+ # sluice board: every live run and every routed session on the machine, drawn
3
+ # from the board index so nobody has to walk the worktrees to find them.
4
+ #
5
+ # board.sh [--json] [--watch [<seconds>]]
6
+ #
7
+ # --json prints the combined state instead of drawing it. --watch redraws every
8
+ # <seconds> (default 2) until interrupted. The index is read, never written.
9
+ # The root is $SLUICE_BOARD_DIR, else $CLAUDE_CONFIG_DIR/sluice/board, else
10
+ # $HOME/.claude/sluice/board; with none of the three set the index is empty.
11
+ #
12
+ # Exit: 0 ok, 4 bad arguments, 5 jq missing, 6 the board could not be read.
13
+
14
+ set -u
15
+
16
+ err() { echo "board.sh: $*" >&2; }
17
+
18
+ usage() {
19
+ echo "usage: board.sh [--json] [--watch [<seconds>]]" >&2
20
+ exit 4
21
+ }
22
+
23
+ json=0 watch=0 interval=2
24
+ while [ $# -gt 0 ]; do
25
+ case "$1" in
26
+ --json) json=1; shift ;;
27
+ --watch)
28
+ watch=1
29
+ shift
30
+ case "${1:-}" in
31
+ ''|--*) ;;
32
+ *) interval="$1"; shift ;;
33
+ esac
34
+ ;;
35
+ *) usage ;;
36
+ esac
37
+ done
38
+ # Leading zeros are stripped as text rather than through $((10#...)), which
39
+ # would wrap a long run of digits into some other number.
40
+ seconds="$interval"
41
+ while case "$seconds" in 0?*) true ;; *) false ;; esac; do
42
+ seconds="${seconds#0}"
43
+ done
44
+ case "$seconds" in
45
+ ''|0|*[!0-9]*) err "--watch needs a positive whole number of seconds, not $interval"; exit 4 ;;
46
+ esac
47
+ interval="$seconds"
48
+ if [ "$watch" = 1 ] && [ "$json" = 1 ]; then
49
+ err "--watch and --json do not combine"
50
+ exit 4
51
+ fi
52
+
53
+ command -v jq >/dev/null 2>&1 || { err "jq is required"; exit 5; }
54
+
55
+ if [ -n "${SLUICE_BOARD_DIR:-}" ]; then
56
+ ROOT="$SLUICE_BOARD_DIR"
57
+ elif [ -n "${CLAUDE_CONFIG_DIR:-}" ]; then
58
+ ROOT="$CLAUDE_CONFIG_DIR/sluice/board"
59
+ elif [ -n "${HOME:-}" ]; then
60
+ ROOT="$HOME/.claude/sluice/board"
61
+ else
62
+ ROOT=""
63
+ fi
64
+
65
+ ESC="$(printf '\033')"
66
+ if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then
67
+ colour=1
68
+ else
69
+ colour=0
70
+ fi
71
+
72
+ paint() { # <sgr> <text>
73
+ if [ "$colour" = 1 ]; then
74
+ printf '%s[%sm%s%s[0m' "$ESC" "$1" "$2" "$ESC"
75
+ else
76
+ printf '%s' "$2"
77
+ fi
78
+ }
79
+
80
+ # The board polls for hours while runs close and sessions are pruned under it,
81
+ # so each file is read into a variable first and skipped when that read fails,
82
+ # and jq gets contents rather than paths that may be gone by the time it opens
83
+ # them. The read is the builtin, so a frame spawns no process per file. TREES
84
+ # keeps each run's path by index, so a path holding a newline or a tab reaches
85
+ # status.sh whole rather than through a line-split read. KEYS and VALUES are
86
+ # the records emit hands jq. Returns 1 when the root, runs/ or sessions/
87
+ # exists but cannot be listed: one not yet created is empty, since nothing has
88
+ # been written there, but one that cannot be read would draw as nothing running
89
+ # when it may hold every run on the machine.
90
+ gather() {
91
+ local n=0 f tree run entry d
92
+ TREES=()
93
+ KEYS=()
94
+ VALUES=()
95
+ [ -n "$ROOT" ] || return 0
96
+ for d in "$ROOT" "$ROOT/runs" "$ROOT/sessions"; do
97
+ if [ -e "$d" ] || [ -L "$d" ]; then
98
+ [ -d "$d" ] && [ -r "$d" ] && [ -x "$d" ] || return 1
99
+ fi
100
+ done
101
+ for f in "$ROOT"/runs/*; do
102
+ [ -f "$f" ] || continue
103
+ # read -d '' stops at EOF with a non-zero status, so a short read is told
104
+ # from a failed open by whether anything arrived.
105
+ tree=""
106
+ { IFS= read -r -d '' tree || [ -n "$tree" ]; } 2>/dev/null <"$f" || continue
107
+ tree="${tree%$'\n'}"
108
+ [ -n "$tree" ] || continue
109
+ run=""
110
+ { IFS= read -r -d '' run || [ -n "$run" ]; } 2>/dev/null <"$tree/.sluice/run.json" || continue
111
+ TREES[$n]="$tree"
112
+ KEYS+=("t$n" "r$n")
113
+ VALUES+=("$tree" "$run")
114
+ n=$((n + 1))
115
+ done
116
+ n=0
117
+ for f in "$ROOT"/sessions/*.json; do
118
+ [ -f "$f" ] || continue
119
+ entry=""
120
+ { IFS= read -r -d '' entry || [ -n "$entry" ]; } 2>/dev/null <"$f" || continue
121
+ KEYS+=("s$n")
122
+ VALUES+=("$entry")
123
+ n=$((n + 1))
124
+ done
125
+ }
126
+
127
+ # Contents go to jq on stdin as NUL-separated key and value records rather than
128
+ # as arguments, so no file's size runs into the argv limit. JSON text never
129
+ # holds a NUL, and a tree path cannot.
130
+ emit() {
131
+ local i=0
132
+ while [ "$i" -lt "${#KEYS[@]}" ]; do
133
+ printf '%s\0%s\0' "${KEYS[$i]}" "${VALUES[$i]}"
134
+ i=$((i + 1))
135
+ done
136
+ }
137
+
138
+ combine() { # [jq filter appended to COMBINE]
139
+ emit | jq -R -s --arg home "${HOME:-}" "$COMBINE${1:-}" 2>/dev/null
140
+ }
141
+
142
+ # Builds {board, order, lines}: board is what --json prints, order the runs to
143
+ # draw as "<index into TREES>\t<name>\t<path as shown>", lines the session rows.
144
+ # Those fields are joined rather than put through @tsv, which escapes a
145
+ # backslash that read -r then keeps, so a name would draw doubled; clean has
146
+ # already dropped every tab and newline a field could hold. Session rows are
147
+ # padded here because jq counts a string in characters whatever the locale,
148
+ # where bash under C counts bytes and would push a row holding café out of line.
149
+ # A wide CJK glyph still counts as one, so a row holding one draws a cell short.
150
+ # Each entry is evaluated on its own inside try, so one with an odd shape costs
151
+ # only itself, and the counts are taken from the same filtered list the rows
152
+ # are drawn from.
153
+ COMBINE='
154
+ def clean: if type == "string" then gsub("[\u0000-\u001f\u007f]"; "") else . end;
155
+ def base: split("/") | map(select(. != "")) | last // "/";
156
+ def age($t): ((now - ($t | fromdateiso8601)) / 60 | floor | if . < 0 then 0 else . end)
157
+ | if . < 60 then "\(.)m" else "\(. / 60 | floor)h\(. % 60)m" end;
158
+ def need(cond; why): if cond then . else error(why) end;
159
+ (split("\u0000") | .[:-1]) as $parts
160
+ | ([range(0; $parts | length; 2) | {key: $parts[.], value: $parts[. + 1]}] | from_entries) as $n
161
+ | [$n | keys[] | select(test("^t[0-9]+$")) | ltrimstr("t") | tonumber
162
+ | . as $i
163
+ | try (
164
+ $n["r\($i)"] | fromjson
165
+ | need(type == "object"; "not a run")
166
+ | need((.tasks | type) == "array"; "no tasks")
167
+ | need(all(.tasks[]; type == "object" and (.status | type) == "string"); "odd tasks")
168
+ | {i: $i, tree: $n["t\($i)"], run: .,
169
+ rank: (if any(.tasks[]; .status == "blocked") then 0
170
+ elif any(.tasks[]; .status == "active" or .status == "review") then 1
171
+ else 2 end),
172
+ t: (.updated // .started | fromdateiso8601)}
173
+ ) catch empty]
174
+ | reduce .[] as $r ([]; if any(.[]; .tree == $r.tree) then . else . + [$r] end)
175
+ | sort_by([.rank, -.t])
176
+ | . as $runs
177
+ | [$runs[].run.session | strings] as $taken
178
+ | [$n | keys[] | select(test("^s[0-9]+$")) | $n[.]
179
+ | try (
180
+ fromjson
181
+ | need(type == "object"; "not an entry")
182
+ | with_entries(.value |= clean)
183
+ | need((.tree | type) == "string" and .tree != ""; "no tree")
184
+ | need(.channel == "fast" or .channel == "main" or .channel == "deep"; "bad channel")
185
+ | need(.state == "working" or .state == "handed back"; "bad state")
186
+ | (.updated | fromdateiso8601) as $t
187
+ | select(now - $t <= 86400)
188
+ | select(.session as $s | $taken | index([$s]) | not)
189
+ | {entry: ., t: $t, cells: [.channel, (.tree | base), .state,
190
+ (if .state == "working" then "◷ \(age(.routed))" else "\(age(.updated)) ago" end)]}
191
+ ) catch empty]
192
+ | sort_by(-.t)
193
+ | . as $sessions
194
+ | [range(0; 3) as $k | [$sessions[].cells[$k] | length] | max // 0] as $widths
195
+ | def pad($w): . + (" " * ($w - length));
196
+ # A run of any other channel still draws as a run, the bar deep runs draw, so
197
+ # it is counted with them rather than vanishing from the header.
198
+ def channel: if . == "fast" or . == "main" then . else "deep" end;
199
+ def tally($state): reduce ("fast", "main", "deep") as $c ({}; .[$c] =
200
+ ([$sessions[] | select(.entry.channel == $c and .entry.state == $state)] | length)
201
+ + (if $state == "working" then [$runs[] | select(.run.channel | channel == $c)] | length else 0 end));
202
+ {board: {runs: [$runs[] | {tree, run}], sessions: [$sessions[].entry],
203
+ counts: {runs: ($runs | length), working: tally("working"), handed_back: tally("handed back")}},
204
+ order: [$runs[] | (.tree | clean) as $c | [.i, ($c | base),
205
+ (if $home != "" and $c == $home then "~"
206
+ elif $home != "" and ($c | startswith($home + "/")) then "~" + ($c | ltrimstr($home))
207
+ else $c end)] | map(tostring) | join("\t")],
208
+ lines: [$sessions[].cells | [range(0; 3) as $k | .[$k] | pad($widths[$k])] + [.[3]] | join("\t")]}
209
+ '
210
+
211
+ # Prints the frame, or returns 1 having printed nothing when the state could
212
+ # not be built at all. Colour is painted here and stripped by the caller, once
213
+ # per frame, when it is off.
214
+ render() {
215
+ local s tag a b c d e f head first=1
216
+ gather || return 1
217
+ s="$(combine)" && [ -n "$s" ] || return 1
218
+ while IFS="$TAB" read -r tag a b c d e f; do
219
+ case "$tag" in
220
+ counts)
221
+ head=""
222
+ [ "$a" -gt 0 ] && head="$head · $a deep"
223
+ [ "$b" -gt 0 ] && head="$head · $b deep handed back"
224
+ [ "$c" -gt 0 ] && head="$head · $c fast working"
225
+ [ "$d" -gt 0 ] && head="$head · $d fast handed back"
226
+ [ "$e" -gt 0 ] && head="$head · $e main working"
227
+ [ "$f" -gt 0 ] && head="$head · $f main handed back"
228
+ [ -n "$head" ] || head=" · nothing running"
229
+ paint 1 sluice
230
+ printf '%s ' "$head"
231
+ paint 2 "$CLOCK"
232
+ echo
233
+ ;;
234
+ run)
235
+ echo
236
+ paint 1 "$b"
237
+ printf ' '
238
+ paint 2 "$c"
239
+ echo
240
+ bash "$SCRIPTS/status.sh" line --full --dir "${TREES[$a]}" 2>/dev/null
241
+ ;;
242
+ session)
243
+ if [ "$first" = 1 ]; then
244
+ echo
245
+ first=0
246
+ fi
247
+ paint 1 "$a"
248
+ printf ' %s %s ' "$b" "$c"
249
+ paint 2 "$d"
250
+ echo
251
+ ;;
252
+ esac
253
+ done <<EOF
254
+ $(jq -r '
255
+ (.board.counts | ["counts", .working.deep, .handed_back.deep, .working.fast, .handed_back.fast, .working.main, .handed_back.main] | @tsv),
256
+ ("run\t" + .order[]),
257
+ ("session\t" + .lines[])' <<<"$s")
258
+ EOF
259
+ return 0
260
+ }
261
+
262
+ UNREADABLE="could not read the board"
263
+ TAB="$(printf '\t')"
264
+ SCRIPTS="$(dirname "$0")"
265
+ STRIP="s/$ESC\[[0-9;]*m//g"
266
+ [ "$colour" = 1 ] && STRIP=""
267
+
268
+ if [ "$json" = 1 ]; then
269
+ gather || { err "$UNREADABLE"; exit 6; }
270
+ combine ' | .board' || { err "$UNREADABLE"; exit 6; }
271
+ exit 0
272
+ fi
273
+
274
+ if [ "$watch" = 0 ]; then
275
+ CLOCK="$(date +%H:%M:%S)"
276
+ frame="$(render)" || { err "$UNREADABLE"; exit 6; }
277
+ printf '%s\n' "$frame" | sed "$STRIP"
278
+ exit 0
279
+ fi
280
+
281
+ sleeper=""
282
+ restore() {
283
+ [ -n "$sleeper" ] && kill "$sleeper" 2>/dev/null
284
+ printf '%s[?25h' "$ESC"
285
+ }
286
+ trap restore EXIT
287
+ trap 'exit 130' INT
288
+ trap 'exit 143' TERM
289
+
290
+ printf '%s[?25l%s[2J' "$ESC" "$ESC"
291
+ while :; do
292
+ CLOCK="$(date +%H:%M:%S)"
293
+ frame="$(render)" || frame="board.sh: $UNREADABLE"
294
+ # Each line clears its own tail, so a row shorter than the one it overwrites
295
+ # leaves nothing of the old one behind. No newline after the last row: one
296
+ # there scrolls a frame as tall as the terminal on every redraw.
297
+ frame="$(printf '%s\n' "$frame" | sed -e "$STRIP" -e "s/\$/$ESC[K/")"
298
+ printf '%s[H%s%s[J' "$ESC" "$frame" "$ESC"
299
+ # Backgrounded so a signal ends the wait at once rather than after the sleep.
300
+ sleep "$interval" &
301
+ sleeper=$!
302
+ wait "$sleeper"
303
+ sleeper=""
304
+ done
@@ -48,6 +48,15 @@
48
48
  # naming the tree the run is in, so that session finds its run from the main
49
49
  # tree however many other runs the set holds.
50
50
  #
51
+ # `init` adds the run's tree to the machine's board index through
52
+ # board-index.sh, `move` re-points it, and `close` drops it; a task flip
53
+ # leaves the index alone. `route`
54
+ # with a session id writes that session's entry (no id, nothing written).
55
+ # `init`, and `route` with a session id, sweep the index of dead entries. The
56
+ # sweep is best effort and silent, so only a run-add, run-drop or session
57
+ # write that fails costs a note: one `status.sh: note: board not updated: ...`
58
+ # line on stderr, never the command's result. scripts/board.sh draws the index.
59
+ #
51
60
  # A flag value may not begin with `--`: unchecked, an omitted value silently
52
61
  # becomes the next flag and the field holds a flag name.
53
62
 
@@ -386,10 +395,27 @@ if [ "$SUB" = "line" ]; then
386
395
  exit 0
387
396
  fi
388
397
 
398
+ # The board is a view across every tree, not part of any run, so a board that
399
+ # cannot be written costs a line on stderr and never the command's own result.
400
+ # board-index.sh's own complaint is folded into the note, so a failure reads as
401
+ # one line rather than two that look like separate problems.
402
+ board_index() { # <verb> [args...]
403
+ local msg
404
+ msg="$(bash "$(dirname "$0")/board-index.sh" "$@" 2>&1 >/dev/null)" && return 0
405
+ msg="${msg//board-index.sh: /}"
406
+ err "note: board not updated: ${msg//$'\n'/; }"
407
+ }
408
+
409
+ # Pruning only tidies what the board reads, which skips dead entries anyway.
410
+ board_prune() {
411
+ bash "$(dirname "$0")/board-index.sh" prune >/dev/null 2>&1 || true
412
+ }
413
+
389
414
  # Exists to put the channel into the transcript verbatim, as a command that
390
- # ran, which run-stats and the stop guard read back. So it touches no state:
391
- # a route is called before any run is opened, and fast opens none at all. For
392
- # the same reason it runs ahead of the jq check below, which only state needs.
415
+ # ran, which run-stats and the stop guard read back. It opens no run, since a
416
+ # route is called before any run is opened and fast opens none at all, but with
417
+ # a session id known it writes that session's entry on the board. It still runs
418
+ # ahead of the jq check below, because the announcement must work without jq.
393
419
  # Bypass is refused because it is no code change, and a marker for it would
394
420
  # read back as a routed change that never happened.
395
421
  if [ "$SUB" = "route" ]; then
@@ -400,6 +426,14 @@ if [ "$SUB" = "route" ]; then
400
426
  [ "$ROUTE" != "bypass" ] || { err "bypass is no code change, so there is nothing to route; just answer"; exit 4; }
401
427
  in_set "$ROUTE" "$ROUTE_CHANNELS" || { err "unknown channel: $ROUTE (one of: $ROUTE_CHANNELS)"; exit 4; }
402
428
  echo "sluice: $ROUTE channel"
429
+ # A route is the only sign a fast or main session leaves, so the board learns
430
+ # of it here. With no session id there is nobody to name, and none is made up.
431
+ if [ -n "$SESSION" ]; then
432
+ ROUTE_TREE="$(git -C "$ORIG_DIR" rev-parse --show-toplevel 2>/dev/null)"
433
+ [ -n "$ROUTE_TREE" ] || ROUTE_TREE="$(cd "$ORIG_DIR" 2>/dev/null && pwd -P)"
434
+ board_prune
435
+ board_index session "$SESSION" "$ROUTE" "$ROUTE_TREE"
436
+ fi
403
437
  exit 0
404
438
  fi
405
439
 
@@ -904,6 +938,10 @@ case "$SUB" in
904
938
  done
905
939
  fi
906
940
 
941
+ # Opening a run is a rare enough moment to sweep the board of runs and
942
+ # sessions that ended without saying so.
943
+ board_prune
944
+
907
945
  jq -n \
908
946
  --arg topic "$TOPIC" \
909
947
  --arg channel "$CHANNEL" \
@@ -917,6 +955,9 @@ case "$SUB" in
917
955
  + (if $session == "" then {} else {session: $session} end)
918
956
  + {tasks: []}
919
957
  ' | write_state || exit $?
958
+ # Here and in move only, not on every write: a flip spawning a board
959
+ # write was what a plan of sixty tasks paid sixty times over.
960
+ board_index run-add "$(cd "$DIR" && pwd -P)"
920
961
 
921
962
  POINTER_TREE="$(main_tree "$DIR")"
922
963
  if [ -n "$SESSION" ] && [ -n "$POINTER_TREE" ] && [ -d "$POINTER_TREE" ] \
@@ -1326,6 +1367,10 @@ case "$SUB" in
1326
1367
  exit 3
1327
1368
  fi
1328
1369
  mv "$STATE" "$DEST_STATE" || { err "could not move $STATE to $DEST_STATE"; exit 1; }
1370
+ # Only once the state has arrived, so the board never loses a run that
1371
+ # failed to move.
1372
+ board_index run-drop "$(cd "$DIR" && pwd -P)"
1373
+ board_index run-add "$TO"
1329
1374
 
1330
1375
  # After the state has arrived, never before: a note pointing at a run
1331
1376
  # that never got there is worse than no note, being indistinguishable
@@ -1407,6 +1452,8 @@ case "$SUB" in
1407
1452
  n=$((n + 1))
1408
1453
  done
1409
1454
  mv "$STATE" "$dest" || { err "could not archive $STATE"; exit 1; }
1455
+ # After the archive, so a close that failed leaves the run on the board.
1456
+ board_index run-drop "$(cd "$DIR" && pwd -P)"
1410
1457
 
1411
1458
  # A note naming this tree has outlived the run it forwarded to. Left
1412
1459
  # behind, it does not go quiet: the next run opened in this tree inherits
@@ -62,6 +62,106 @@ if [ -n "$input" ]; then
62
62
  sid="$(printf '%s' "$input" | jq -r '.session_id // empty' 2>/dev/null || true)"
63
63
  transcript="$(printf '%s' "$input" | jq -r '.transcript_path // empty' 2>/dev/null || true)"
64
64
  fi
65
+
66
+ # The run the session's own tree answers for, and only that: the state beside
67
+ # it, or the state it forwarded into a worktree when `move` sent the run on
68
+ # without the session. status.sh also lets a tree with no run of its own read
69
+ # the main worktree's, and that one is not taken here: a Stop in such a tree may
70
+ # be an unrelated session, and a remedy printed to it would reach into somebody
71
+ # else's run.
72
+ #
73
+ # The forward is read here rather than left to status.sh's resolution because
74
+ # the two questions differ. Resolution answers "which run can this tree read",
75
+ # which is the right question for a render and the wrong one for a refusal. The
76
+ # tree named in the note is then passed as `--dir`, so what follows asks about
77
+ # one named tree and carries no layout knowledge of its own.
78
+ #
79
+ # The run this session opened in a worktree it never entered is its own too,
80
+ # found through the pointer `init` left at the main tree, and it comes ahead of
81
+ # the forward: the forward names whichever run was moved last, which with
82
+ # several controllers in the main tree may be another session's.
83
+ #
84
+ # Sets top to the stop's tree and run_tree to the tree holding that run, or
85
+ # leaves run_tree empty when there is none. One function for the run guard and
86
+ # the board, so the run that keeps a deep session off the board is the run the
87
+ # guard would refuse over.
88
+ top=""
89
+ run_tree=""
90
+ resolved=""
91
+ resolve_run_tree() {
92
+ local main session_tree="" forward=""
93
+ resolved=1
94
+ run_tree=""
95
+ top="$(git -C "$cwd" rev-parse --show-toplevel 2>/dev/null)"
96
+ [ -n "$top" ] || top="$cwd"
97
+ case "$sid" in
98
+ "" | */* | .*) ;;
99
+ *)
100
+ main="$(git -C "$cwd" worktree list --porcelain 2>/dev/null | sed -n '1s/^worktree //p')"
101
+ [ -n "$main" ] && [ -f "$main/.sluice/sessions/$sid" ] \
102
+ && IFS= read -r session_tree <"$main/.sluice/sessions/$sid" 2>/dev/null
103
+ if [ -z "$session_tree" ] || [ ! -f "$session_tree/.sluice/run.json" ] \
104
+ || [ "$(jq -r '.session // empty' "$session_tree/.sluice/run.json" 2>/dev/null)" != "$sid" ]; then
105
+ session_tree=""
106
+ fi
107
+ ;;
108
+ esac
109
+ if [ -f "$top/.sluice/run.json" ]; then
110
+ run_tree="$top"
111
+ elif [ -n "$session_tree" ]; then
112
+ run_tree="$session_tree"
113
+ elif [ -f "$top/.sluice/run.at" ]; then
114
+ IFS= read -r forward <"$top/.sluice/run.at" 2>/dev/null || return 0
115
+ # A note outliving the run it named is stale, not a run to refuse a stop over.
116
+ [ -n "$forward" ] && [ -f "$forward/.sluice/run.json" ] || return 0
117
+ run_tree="$forward"
118
+ fi
119
+ }
120
+
121
+ # A stop let through is the session handing back, so the board marks it. A
122
+ # trap rather than a call before each exit: the hook lets a stop through from
123
+ # a dozen places, and a new one added later is covered without anyone having
124
+ # to remember the board. Every block decision sets refused first, since a
125
+ # refused stop is the session still working. The board's output and failure
126
+ # are dropped: stdout carries the decision, and the hook always exits 0.
127
+ #
128
+ # A deep session whose run is live keeps its entry, because the board draws the
129
+ # run's bar for it. Whether the run is live is asked here rather than taken
130
+ # from the run guard, since most exits come before the guard runs, and a deep
131
+ # session mid-run must not read as handed back on any of them. Resolving costs
132
+ # git calls on every stop, so it is skipped for a session with no entry, and
133
+ # not repeated when the run guard already did it.
134
+ #
135
+ # The run resolved is only the one this tree can reach, which through a forward
136
+ # or a shared tree may be another session's; that run's bar is not this
137
+ # session's work, so the entry is kept only for a run that names this session.
138
+ # A run naming no session is no session's own, as the board draws it, and a
139
+ # state file that cannot be read names nobody it can be kept for.
140
+ refused=""
141
+ board_handback() {
142
+ local keep="" root owner
143
+ [ -z "$refused" ] && [ -n "$sid" ] || return 0
144
+ case "$sid" in */* | .*) return 0 ;; esac
145
+ # Must match the root board-index.sh and board.sh resolve.
146
+ if [ -n "${SLUICE_BOARD_DIR:-}" ]; then
147
+ root="$SLUICE_BOARD_DIR"
148
+ elif [ -n "${CLAUDE_CONFIG_DIR:-}" ]; then
149
+ root="$CLAUDE_CONFIG_DIR/sluice/board"
150
+ elif [ -n "${HOME:-}" ]; then
151
+ root="$HOME/.claude/sluice/board"
152
+ else
153
+ return 0
154
+ fi
155
+ [ -f "$root/sessions/$sid.json" ] || return 0
156
+ [ -n "$resolved" ] || resolve_run_tree
157
+ if [ -n "$run_tree" ] && owner="$(jq -r '.session // empty' "$run_tree/.sluice/run.json" 2>/dev/null)" \
158
+ && [ "$owner" = "$sid" ]; then
159
+ keep="--keep-deep"
160
+ fi
161
+ bash "$here/board-index.sh" handback "$sid" $keep >/dev/null 2>&1 || true
162
+ }
163
+ trap board_handback EXIT
164
+
65
165
  [ "$active" = "true" ] && exit 0
66
166
  [ -d "$cwd" ] || exit 0
67
167
 
@@ -175,6 +275,7 @@ entry_nudge() {
175
275
 
176
276
  mkdir -p "$root/nudged" 2>/dev/null || true
177
277
  : >"$root/nudged/$sid" 2>/dev/null || true
278
+ refused=1
178
279
  jq -n '{
179
280
  decision: "block",
180
281
  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."
@@ -225,6 +326,7 @@ verify_gate() {
225
326
  [ -f "$moved_tree/.sluice/verified" ] && recorded="$(sed -n '1p' "$moved_tree/.sluice/verified" 2>/dev/null)"
226
327
  [ "$recorded" = "$now" ] && return 0
227
328
 
329
+ refused=1
228
330
  jq -n --arg channel "$channel" '{
229
331
  decision: "block",
230
332
  reason: ("sluice: this \($channel) session has changed the tree and no passing check is recorded against it as it stands now, so the handback would carry a change nothing has proved. Run the check that proves it through the recorder: status.sh verify -- <cmd> (the tests, the build, whatever exercises this change). If no check can cover it (docs, config), record why: status.sh verify --none \"<reason>\". Then hand back.")
@@ -305,6 +407,7 @@ test_gate() {
305
407
 
306
408
  # Paths land in a reason the harness prints, so control bytes are stripped
307
409
  # as the run guard strips them from the topic and tree.
410
+ refused=1
308
411
  jq -n --arg paths "$missing" --arg base "$base" '{
309
412
  decision: "block",
310
413
  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: "
@@ -320,50 +423,9 @@ fi
320
423
 
321
424
  [ -f "$STATUS" ] || exit 0
322
425
 
323
- # The run the session's own tree answers for, and only that: the state beside
324
- # it, or the state it forwarded into a worktree when `move` sent the run on
325
- # without the session. status.sh also lets a tree with no run of its own read
326
- # the main worktree's, and that one is not taken here: a Stop in such a tree may
327
- # be an unrelated session, and a remedy printed to it would reach into somebody
328
- # else's run.
329
- #
330
- # The forward is read here rather than left to status.sh's resolution because
331
- # the two questions differ. Resolution answers "which run can this tree read",
332
- # which is the right question for a render and the wrong one for a refusal. The
333
- # tree named in the note is then passed as `--dir`, so what follows asks about
334
- # one named tree and carries no layout knowledge of its own.
335
- #
336
- # The run this session opened in a worktree it never entered is its own too,
337
- # found through the pointer `init` left at the main tree, and it comes ahead of
338
- # the forward: the forward names whichever run was moved last, which with
339
- # several controllers in the main tree may be another session's.
340
- top="$(git -C "$cwd" rev-parse --show-toplevel 2>/dev/null)"
341
- [ -n "$top" ] || top="$cwd"
342
- session_tree=""
343
- case "$sid" in
344
- "" | */* | .*) ;;
345
- *)
346
- main="$(git -C "$cwd" worktree list --porcelain 2>/dev/null | sed -n '1s/^worktree //p')"
347
- [ -n "$main" ] && [ -f "$main/.sluice/sessions/$sid" ] \
348
- && IFS= read -r session_tree <"$main/.sluice/sessions/$sid" 2>/dev/null
349
- if [ -z "$session_tree" ] || [ ! -f "$session_tree/.sluice/run.json" ] \
350
- || [ "$(jq -r '.session // empty' "$session_tree/.sluice/run.json" 2>/dev/null)" != "$sid" ]; then
351
- session_tree=""
352
- fi
353
- ;;
354
- esac
355
- if [ -f "$top/.sluice/run.json" ]; then
356
- tree="$top"
357
- elif [ -n "$session_tree" ]; then
358
- tree="$session_tree"
359
- elif [ -f "$top/.sluice/run.at" ]; then
360
- tree=""
361
- IFS= read -r tree <"$top/.sluice/run.at" 2>/dev/null || exit 0
362
- # A note outliving the run it named is stale, not a run to refuse a stop over.
363
- [ -n "$tree" ] && [ -f "$tree/.sluice/run.json" ] || exit 0
364
- else
365
- exit 0
366
- fi
426
+ resolve_run_tree
427
+ [ -n "$run_tree" ] || exit 0
428
+ tree="$run_tree"
367
429
 
368
430
  run="$(bash "$STATUS" show --json --dir "$tree" 2>/dev/null)" || exit 0
369
431
  [ -n "$run" ] || exit 0
@@ -413,6 +475,7 @@ verdict="$(printf '%s' "$run" | jq -c --argjson now "$(date -u +%s)" --arg tree
413
475
  # anyone into. What replaces it is the step `move` could not take -- the session
414
476
  # following the run -- because a controller guarded here is a controller sitting
415
477
  # in the tree its own run left.
478
+ refused=1
416
479
  printf '%s' "$verdict" | jq 2>/dev/null '{
417
480
  decision: "block",
418
481
  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. Still open: \(.open)."
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sluice",
3
- "version": "0.25.5",
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. Two Stop hooks hold a handback: one refuses a tree that moved since its last recorded check, the other refuses a changed existing test not proven to fail on the session's starting code (or recorded with a reason). Claude Code only; stands down where the superpowers pipeline governs the repo.",
3
+ "version": "0.26.1",
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), lists every live deep run and routed fast or main session on the machine in one board.sh view (drawn once, watched, or as JSON), 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. Two Stop hooks hold a handback: one refuses a tree that moved since its last recorded check, the other refuses a changed existing test not proven to fail on the session's starting code (or recorded with a reason). Claude Code only; stands down where the superpowers pipeline governs the repo.",
5
5
  "author": "iceinvein",
6
6
  "type": "prompt",
7
7
  "tools": [