@iceinvein/agent-skills 0.23.5 → 0.23.8
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 +2 -2
- package/skills/sluice/SKILL.md +8 -0
- package/skills/sluice/evals/README.md +3 -2
- package/skills/sluice/evals/deep-plan-traces-the-example/case.yaml +4 -0
- package/skills/sluice/evals/deep-plan-traces-the-example/fixture.sh +151 -0
- package/skills/sluice/evals/deep-plan-traces-the-example/graders/announces-deep-channel.md +12 -0
- package/skills/sluice/evals/deep-plan-traces-the-example/graders/design-written-to-docs.md +5 -0
- package/skills/sluice/evals/deep-plan-traces-the-example/graders/no-source-edit.md +11 -0
- package/skills/sluice/evals/deep-plan-traces-the-example/graders/no-source-write.md +11 -0
- package/skills/sluice/evals/deep-plan-traces-the-example/graders/sluice-fired.md +5 -0
- package/skills/sluice/evals/deep-plan-traces-the-example/graders/spec-has-a-trace.md +7 -0
- package/skills/sluice/evals/deep-plan-traces-the-example/graders/stopped-for-signoff.md +8 -0
- package/skills/sluice/evals/deep-plan-traces-the-example/graders/trace-finds-no-accepted-slide.md +27 -0
- package/skills/sluice/evals/deep-plan-traces-the-example/prompt.md +17 -0
- package/skills/sluice/evals/rules.json +12 -2
- package/skills/sluice/references/deep-channel.md +38 -0
- package/skills/sluice/references/status.md +95 -4
- package/skills/sluice/scripts/board-index.sh +160 -0
- package/skills/sluice/scripts/board.sh +304 -0
- package/skills/sluice/scripts/status.sh +50 -3
- package/skills/sluice/scripts/stop-guard.sh +107 -44
- package/skills/sluice/skill.json +2 -2
|
@@ -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.
|
|
391
|
-
#
|
|
392
|
-
#
|
|
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
|