@iceinvein/agent-skills 0.9.0 → 0.10.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/references/deep-channel.md +24 -6
- package/skills/sluice/references/status.md +119 -16
- package/skills/sluice/scripts/plan.sh +158 -33
- package/skills/sluice/scripts/status.sh +198 -14
- package/skills/sluice/skill.json +1 -1
package/package.json
CHANGED
package/skills/index.json
CHANGED
|
@@ -283,7 +283,7 @@
|
|
|
283
283
|
"name": "sluice",
|
|
284
284
|
"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 and one status command can answer where the run is, 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.",
|
|
285
285
|
"type": "prompt",
|
|
286
|
-
"version": "0.
|
|
286
|
+
"version": "0.12.0"
|
|
287
287
|
},
|
|
288
288
|
{
|
|
289
289
|
"name": "temporal-coupling-detector",
|
|
@@ -91,9 +91,11 @@ pre-flight settled. `scripts/status.sh` writes and reads it, and
|
|
|
91
91
|
`references/status.md` carries the commands and the statusline segment that
|
|
92
92
|
makes a run visible without anyone asking. Open it with `init` when you open the
|
|
93
93
|
record, seed the rows with `scripts/plan.sh import <plan>` rather than typing a
|
|
94
|
-
command per task, then flip each task as it moves.
|
|
95
|
-
|
|
96
|
-
|
|
94
|
+
command per task, then flip each task as it moves. It carries the ids, the names,
|
|
95
|
+
the flip, the model marks and the tiers, the last of these floored off `Touches`
|
|
96
|
+
and the contract graph rather than guessed. Import is safe to re-run: a status, a
|
|
97
|
+
review mark or a ratified model already recorded is left alone and a tier is only
|
|
98
|
+
ever raised, so resuming after a compaction cannot rewind the run.
|
|
97
99
|
|
|
98
100
|
The record is the other file, and it holds what a status cannot: the reason
|
|
99
101
|
review went the way it did, the reason a task was downshifted, the reason the
|
|
@@ -234,14 +236,22 @@ about it.
|
|
|
234
236
|
Read the plan as a graph before you read it as a list. `Needs` and `Offers`
|
|
235
237
|
are dependency edges, not only blindness insurance: a task is ready when every
|
|
236
238
|
`Needs` it names is offered by a task already done, and any two ready tasks
|
|
237
|
-
with disjoint `Touches` can go at the same time.
|
|
238
|
-
|
|
239
|
+
with disjoint `Touches` can go at the same time.
|
|
240
|
+
|
|
241
|
+
**`scripts/status.sh ready` does that read.** It prints the ready set, names which
|
|
242
|
+
of them share a path and so cannot go together, says which tasks are still waiting
|
|
243
|
+
on a contract and what for, and holds the flip out of every wave. Run it before
|
|
244
|
+
each wave rather than deriving it again by hand, and note that it also checks the
|
|
245
|
+
candidates against whatever is already `active` or in `review`: those hold their
|
|
246
|
+
paths too, and a wave checked only against itself reads as safe while colliding
|
|
247
|
+
with work in flight. A plan run in the order it happened to be written is a plan whose
|
|
239
248
|
graph nobody looked at, and inert-first ordering tends to put the independent
|
|
240
249
|
tasks at the front, so the opportunity is usually real.
|
|
241
250
|
|
|
242
251
|
Derive the sets at dispatch rather than writing wave numbers into the plan. A
|
|
243
252
|
declared schedule is wrong the moment one task lands late or comes back with a
|
|
244
|
-
blocking finding. A derived one just recomputes
|
|
253
|
+
blocking finding. A derived one just recomputes, which is the whole reason `ready`
|
|
254
|
+
reads the run state rather than the plan: it sees what has actually landed.
|
|
245
255
|
|
|
246
256
|
- One row per task in `run.json`, flipped to `active` and then `done` as it
|
|
247
257
|
moves. That state outlives compaction; your memory doesn't.
|
|
@@ -365,6 +375,14 @@ or four dispatches rather than nine. If most of your plan qualifies for a
|
|
|
365
375
|
dispatch, the tasks are interleaved rather than ordered, and reordering them
|
|
366
376
|
is cheaper than reviewing them.
|
|
367
377
|
|
|
378
|
+
**Mark each review with `status.sh task <id> --reviewed` when it comes back.**
|
|
379
|
+
What that buys is a count of what this table promised and nobody delivered: tasks
|
|
380
|
+
that are done, that qualified for a dispatch, and that carry no mark. `show` and
|
|
381
|
+
the statusline both carry it from the moment it exists, which is the whole point.
|
|
382
|
+
Unmarked, the count sits permanently non-zero and stops being a signal, and
|
|
383
|
+
"review outstanding" goes back to first appearing in the closing summary, at the
|
|
384
|
+
one moment your partner can no longer do anything about it.
|
|
385
|
+
|
|
368
386
|
Reviews are reads, so they are always parallel. Every review a wave earned
|
|
369
387
|
goes out in one message, and they run while the next wave's implementers work:
|
|
370
388
|
a reviewer writes nothing, so it collides with nothing. The final review is
|
|
@@ -15,9 +15,12 @@ bash <skill-dir>/scripts/status.sh init --topic <t> --channel deep \
|
|
|
15
15
|
bash <skill-dir>/scripts/status.sh task 3 --name "adapter seam" --tier 1 --model cheap
|
|
16
16
|
bash <skill-dir>/scripts/status.sh task 3 --status active --base 75014c9
|
|
17
17
|
bash <skill-dir>/scripts/status.sh task 3 --status done --commit 2c7f261
|
|
18
|
+
bash <skill-dir>/scripts/status.sh task 3 --reviewed
|
|
18
19
|
bash <skill-dir>/scripts/status.sh preflight --review "tier 3 only" --model "6 of 9 cheap" \
|
|
19
20
|
--workspace "one worktree per implementer"
|
|
20
21
|
bash <skill-dir>/scripts/status.sh show
|
|
22
|
+
bash <skill-dir>/scripts/status.sh ready
|
|
23
|
+
bash <skill-dir>/scripts/status.sh line --full
|
|
21
24
|
bash <skill-dir>/scripts/status.sh close
|
|
22
25
|
```
|
|
23
26
|
|
|
@@ -39,13 +42,24 @@ which is the file that does get committed.
|
|
|
39
42
|
|
|
40
43
|
Open it with `init` when you open the run record, at the same point and for the
|
|
41
44
|
same reason, then seed the rows with `plan.sh import <plan>` rather than a
|
|
42
|
-
command per task. The ids, names, the flip, the `Model` marks and the
|
|
43
|
-
|
|
44
|
-
|
|
45
|
+
command per task. The ids, names, the flip, the `Model` marks and the tiers are
|
|
46
|
+
all fixed the moment the plan is written and are already in the file, so typing
|
|
47
|
+
them again is transcription with a chance of error in it.
|
|
48
|
+
|
|
49
|
+
The tier import writes is a floor read off `Touches`: an `(edit)` means existing
|
|
50
|
+
code changed, no `(test)` means nothing executable covers the task, and `Flips`
|
|
51
|
+
or a `Review` flag is tier 3 outright. Raise one by hand with `--tier` where the
|
|
52
|
+
work is more delicate than its paths suggest; nothing lowers it for you, because
|
|
53
|
+
the tier table takes the highest row a task matches.
|
|
45
54
|
|
|
46
55
|
Re-importing is safe and is the right move after the plan changes. It refreshes
|
|
47
|
-
names, the
|
|
48
|
-
|
|
56
|
+
names, the contract graph and the flip, moving the flip when the plan moved it,
|
|
57
|
+
and it raises a tier without ever lowering one. A status, a review mark or a model
|
|
58
|
+
ratified at pre-flight is left alone, so resuming after a compaction cannot rewind
|
|
59
|
+
the run. The one consequence worth knowing: adding a missing `(test)` to a plan
|
|
60
|
+
will not drop a task from tier 2 back to tier 1, because the tier table takes the
|
|
61
|
+
highest row a task matches and nothing here can tell a correction from a
|
|
62
|
+
regression. Lower it by hand with `--tier` if that is what you mean.
|
|
49
63
|
|
|
50
64
|
## What goes where
|
|
51
65
|
|
|
@@ -70,15 +84,86 @@ commit, tier and model. Run it after compaction instead of reconstructing the
|
|
|
70
84
|
run from what you remember, and run it in the message that hands the work back,
|
|
71
85
|
where "four of nine, task five blocked" is a fact your partner can act on.
|
|
72
86
|
|
|
73
|
-
`show --json` is the same state for another reader. `line` is the
|
|
74
|
-
and
|
|
75
|
-
because
|
|
87
|
+
`show --json` is the same state for another reader. `line` is the compact
|
|
88
|
+
one-line form and `line --full` the wide one; both exit 0 in silence on a missing
|
|
89
|
+
run, unreadable state or a missing jq, because their caller is a status bar with
|
|
90
|
+
nowhere to put an error.
|
|
91
|
+
|
|
92
|
+
`line --full` renders three rows: the run and its clock, the bar alone, then the
|
|
93
|
+
detail. The bar gets a row to itself so it never competes with text for width,
|
|
94
|
+
which is what lets a cell be wide enough to read as a block rather than a tick.
|
|
95
|
+
|
|
96
|
+
A group of cells per task, one repeated glyph each: `▰` done, `◈` active, `▨` in
|
|
97
|
+
review, `▮` blocked, `▱` still to do. The glyphs are distinct before they are
|
|
98
|
+
coloured, so the rows survive having their colour stripped. The width is chosen
|
|
99
|
+
from what the whole bar would occupy, gaps included, rather than from the task
|
|
100
|
+
count: keyed off the count alone the schedule was not monotonic, and thirty tasks
|
|
101
|
+
at two cells each ran wider than twelve at three.
|
|
102
|
+
|
|
103
|
+
**A done task still owed a review trails the review glyph**, `▰▰▨` against
|
|
104
|
+
`▰▰▰`. Debt then reads in position rather than only as a count at the end of the
|
|
105
|
+
row, which is the difference between knowing how much there is and knowing where.
|
|
106
|
+
Tier 0 was never owed a dispatch, so it reads as plainly done. On a plan long
|
|
107
|
+
enough to narrow cells to one, there is no trailing cell to give up and the
|
|
108
|
+
positional reading stops: the count in the third row is then the only carrier,
|
|
109
|
+
which is why it is printed whether or not the bar could show the same thing.
|
|
110
|
+
|
|
111
|
+
**The flip draws as a rule, `┃`, before its task.** Everything left of it is inert
|
|
112
|
+
and safe to leave landed; everything right of it is not. That is what the flip
|
|
113
|
+
means, and it is a boundary between tasks rather than a property of one, so a
|
|
114
|
+
name in the header could not say it. `plan.sh validate` rejects a plan with two
|
|
115
|
+
flips and `import` clears a stale one, so the bar is only ever asked to draw the
|
|
116
|
+
single legal case.
|
|
117
|
+
|
|
118
|
+
The third row carries the progress count, whichever task wants attention, and the
|
|
119
|
+
review debt. A blocked task displaces the active one there, being the one of the
|
|
120
|
+
two worth interrupting for, and a `+n` follows when more than one task shares that
|
|
121
|
+
state, since a plan running four wide has four actives by design.
|
|
122
|
+
|
|
123
|
+
Mark a review with `task <id> --reviewed` when a reviewer comes back. What that
|
|
124
|
+
buys is the debt count: a task that is done, that the tier table owed a dispatch,
|
|
125
|
+
and that nobody marked. Tier 0 is excluded, having only ever been owed a stat
|
|
126
|
+
read. Without it "review outstanding" first appears in the closing summary, at
|
|
127
|
+
the one moment your partner can no longer do anything about it, and `show` and
|
|
128
|
+
the statusline both carry it from the moment it exists.
|
|
129
|
+
|
|
130
|
+
## The next wave
|
|
131
|
+
|
|
132
|
+
`ready` answers the one question the other commands do not: not what the state is,
|
|
133
|
+
but what may go now. A task is ready when every symbol it `Needs` is offered by a
|
|
134
|
+
task already done, and two ready tasks are safe together when their `Touches` are
|
|
135
|
+
disjoint.
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
6 ready now · a worktree each
|
|
139
|
+
T1 extract the bundle writer src/cli/adapters/bundle.ts, …
|
|
140
|
+
T4 record what the harness offers skills/sluice/scripts/status.sh, …
|
|
141
|
+
T4 and T5 share skills/sluice/scripts/status.sh, so not together
|
|
142
|
+
|
|
143
|
+
2 waiting on a contract
|
|
144
|
+
T2 cursor and gemini write bundles needs writeBundle
|
|
145
|
+
|
|
146
|
+
the flip runs alone
|
|
147
|
+
T8 sluice installs on four harnesses
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
It reads the graph `plan.sh import` recorded, so a run seeded before that existed
|
|
151
|
+
says so and tells you to re-import rather than reporting everything ready. The
|
|
152
|
+
flip is held out of every wave whatever the graph says, because the invariant it
|
|
153
|
+
establishes is what the tasks after it are checked against.
|
|
154
|
+
|
|
155
|
+
Derive the wave here rather than writing wave numbers into the plan. A declared
|
|
156
|
+
schedule is wrong the moment one task lands late; this recomputes.
|
|
76
157
|
|
|
77
158
|
## Statusline
|
|
78
159
|
|
|
79
|
-
This is the part that makes a run visible without anyone asking.
|
|
80
|
-
|
|
81
|
-
|
|
160
|
+
This is the part that makes a run visible without anyone asking. Give it rows of
|
|
161
|
+
its own rather than a segment among the badges: it then costs nothing when no run
|
|
162
|
+
is live and contends with nothing for width when one is, which is what lets the
|
|
163
|
+
bar be wide and the task carry its name rather than only its number.
|
|
164
|
+
|
|
165
|
+
Capture it wherever the statusline command builds its other lines, keyed off the
|
|
166
|
+
state file existing so a session with no run spawns no process at all:
|
|
82
167
|
|
|
83
168
|
```bash
|
|
84
169
|
sluice_line=""
|
|
@@ -86,16 +171,34 @@ if [ -n "$cwd" ] && [ -f "$cwd/.sluice/run.json" ]; then
|
|
|
86
171
|
for sluice_sh in "$cwd/.claude/skills/sluice/scripts/status.sh" \
|
|
87
172
|
"$HOME/.claude/skills/sluice/scripts/status.sh"; do
|
|
88
173
|
[ -f "$sluice_sh" ] || continue
|
|
89
|
-
sluice_line=$(bash "$sluice_sh" line --dir "$cwd" 2>/dev/null)
|
|
174
|
+
sluice_line=$(bash "$sluice_sh" line --full --dir "$cwd" 2>/dev/null)
|
|
90
175
|
break
|
|
91
176
|
done
|
|
92
177
|
fi
|
|
93
|
-
[ -n "$sluice_line" ] && printf ' %s' "$sluice_line"
|
|
94
178
|
```
|
|
95
179
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
180
|
+
then print it last, after whatever else the command emits:
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
if [ -n "$sluice_line" ]; then printf '%s\n' "$sluice_line"; fi
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
`if` rather than `[ ... ] &&`: as the last command of a statusline script the
|
|
187
|
+
short form makes it exit 1 on every render with no run live, which is the common
|
|
188
|
+
case. `%s` rather than `%b`: the render already carries real escape bytes, and
|
|
189
|
+
`%b` would reinterpret a backslash inside a task name. `$cwd` is
|
|
190
|
+
`workspace.current_dir` from the JSON the harness sends on stdin. It renders as:
|
|
191
|
+
|
|
192
|
+
```
|
|
193
|
+
⧗ deep · sluice-cross-harness ◷ 38m
|
|
194
|
+
▰▰▰ ▰▰▨ ▨▨▨ ◈◈◈ ▱▱▱ ▮▮▮ ▱▱▱ ┃ ▱▱▱ ▱▱▱
|
|
195
|
+
2/9 done · !T6 model tiers rather than model names +1 · ⟲1 unreviewed
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
The colour comes out of the script rather than being applied by the caller,
|
|
199
|
+
because the mapping from state to colour belongs next to the state. A caller that
|
|
200
|
+
coloured the line itself would have to re-derive each cell's meaning from its
|
|
201
|
+
glyph, which is the same fact stored twice.
|
|
99
202
|
|
|
100
203
|
A run that is only visible to the session running it is a run your partner
|
|
101
204
|
cannot redirect. That is the same argument the channel announcement makes, and
|
|
@@ -14,9 +14,9 @@
|
|
|
14
14
|
# validate prints one line per finding. An error means the plan cannot be
|
|
15
15
|
# dispatched as written; a warning is a judgement call left to its author.
|
|
16
16
|
# import seeds the run state's task rows from the plan, so the ids, names, the
|
|
17
|
-
# flip, the model marks and the
|
|
18
|
-
#
|
|
19
|
-
#
|
|
17
|
+
# flip, the model marks and the tiers come from the file rather than from a dozen
|
|
18
|
+
# hand-typed commands. It is safe to re-run: a status, a review mark or a
|
|
19
|
+
# ratified model already recorded is left alone, and a tier is only ever raised.
|
|
20
20
|
#
|
|
21
21
|
# Exit: 0 no errors, 2 errors found, 4 bad arguments, 5 jq missing (import only).
|
|
22
22
|
|
|
@@ -24,24 +24,40 @@ set -uo pipefail
|
|
|
24
24
|
|
|
25
25
|
err() { echo "plan.sh: $*" >&2; }
|
|
26
26
|
|
|
27
|
+
# ASCII unit separator. Tab would be merged by `read`; see the parser comment.
|
|
28
|
+
SEP="$(printf '\037')"
|
|
29
|
+
|
|
27
30
|
usage() {
|
|
28
31
|
echo "usage:" >&2
|
|
29
32
|
sed -n '/^# plan.sh validate/,/^# Exit:/p' "$0" | sed 's/^# \{0,2\}//' >&2
|
|
30
33
|
}
|
|
31
34
|
|
|
32
|
-
# The parser. Emits
|
|
35
|
+
# The parser. Emits rows on stdout separated by 0x1f, the ASCII unit separator,
|
|
36
|
+
# NOT by a tab. Tab is an IFS *whitespace* character, so `read` merges runs of it
|
|
37
|
+
# and drops empty fields: one task with an empty Needs then shifts every later
|
|
38
|
+
# field left, which is how the graph columns silently swapped. 0x1f is
|
|
39
|
+
# non-whitespace, so empty columns survive.
|
|
40
|
+
#
|
|
41
|
+
# Rows:
|
|
33
42
|
# summary <task count> <flip task or 0>
|
|
34
43
|
# error|warn <message>
|
|
35
|
-
# task <id> <name> <tier 3
|
|
44
|
+
# task <id> <name> <tier 0-3> <model 1|0> <flips 1|0> <needs> <offers> <touches>
|
|
45
|
+
#
|
|
46
|
+
# The last three are space-separated and are what answers "which tasks may go
|
|
47
|
+
# now": a task is ready when every Needs it names is offered by something already
|
|
48
|
+
# done, and two ready tasks are safe together when their Touches are disjoint.
|
|
36
49
|
#
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
# tier
|
|
50
|
+
# The tier is a floor derived from what the plan actually settles: an (edit) in
|
|
51
|
+
# Touches means existing code changed, no (test) means nothing executable covers
|
|
52
|
+
# the task, and Flips or a Review flag is tier 3 outright. A task matching more
|
|
53
|
+
# than one row takes the highest, per the tier table.
|
|
40
54
|
#
|
|
41
55
|
# One pass, line-oriented, because the plan format is a strict skeleton rather
|
|
42
56
|
# than free markdown. Fenced blocks are skipped: a plan carries signatures and
|
|
43
57
|
# commands in them, and a `- [ ]` inside a fence is an example, not a step.
|
|
44
58
|
PARSER='
|
|
59
|
+
BEGIN { SEP = sprintf("%c", 31) }
|
|
60
|
+
|
|
45
61
|
function trim(s) { gsub(/^[ \t]+|[ \t]+$/, "", s); return s }
|
|
46
62
|
|
|
47
63
|
# Symbols on one side of a Contract line. The two sides are read differently
|
|
@@ -54,7 +70,16 @@ function trim(s) { gsub(/^[ \t]+|[ \t]+$/, "", s); return s }
|
|
|
54
70
|
#
|
|
55
71
|
# Demand (Needs) is strict, because an extra symbol here is a false error:
|
|
56
72
|
# backticked names and signatures only, falling back to the leading identifier
|
|
57
|
-
# of each comma-separated part when the half carries neither.
|
|
73
|
+
# of each comma-separated part when the half carries neither. Primitive type
|
|
74
|
+
# names are dropped from it: a return shape drags in whatever it is spelled with,
|
|
75
|
+
# and no task in any plan creates `string`.
|
|
76
|
+
function is_primitive(w) {
|
|
77
|
+
return (w == "string" || w == "number" || w == "boolean" || w == "bool" ||
|
|
78
|
+
w == "void" || w == "any" || w == "unknown" || w == "never" ||
|
|
79
|
+
w == "null" || w == "undefined" || w == "int" || w == "float" ||
|
|
80
|
+
w == "object" || w == "Promise" || w == "Array")
|
|
81
|
+
}
|
|
82
|
+
|
|
58
83
|
function symbols(half, out, generous, count, rest, tok, lower, n, parts, i, chunk) {
|
|
59
84
|
lower = half
|
|
60
85
|
gsub(/`/, "", lower)
|
|
@@ -76,28 +101,43 @@ function symbols(half, out, generous, count, rest, tok, lower, n, parts, i, ch
|
|
|
76
101
|
while (match(rest, /`[^`]*`/)) {
|
|
77
102
|
chunk = substr(rest, RSTART + 1, RLENGTH - 2)
|
|
78
103
|
rest = substr(rest, RSTART + RLENGTH)
|
|
79
|
-
|
|
80
|
-
|
|
104
|
+
# The leading identifier of a backticked chunk, unless that is a wrapper
|
|
105
|
+
# like Promise<Config> or Array<TaskRow>: then the dependency is what it
|
|
106
|
+
# wraps, and taking the leading one both misses the real symbol and
|
|
107
|
+
# invents a false one.
|
|
108
|
+
while (match(chunk, /[A-Za-z_][A-Za-z0-9_]*/)) {
|
|
109
|
+
tok = substr(chunk, RSTART, RLENGTH)
|
|
110
|
+
chunk = substr(chunk, RSTART + RLENGTH)
|
|
111
|
+
if (!is_primitive(tok)) { out[++count] = tok; break }
|
|
112
|
+
}
|
|
81
113
|
}
|
|
82
114
|
rest = half
|
|
83
115
|
gsub(/`/, "", rest)
|
|
84
116
|
while (match(rest, /[A-Za-z_][A-Za-z0-9_]*\(/)) {
|
|
85
117
|
tok = substr(rest, RSTART, RLENGTH - 1)
|
|
86
|
-
out[++count] = tok
|
|
118
|
+
if (!is_primitive(tok)) out[++count] = tok
|
|
87
119
|
rest = substr(rest, RSTART + RLENGTH)
|
|
88
120
|
}
|
|
89
121
|
if (count == 0) {
|
|
90
122
|
rest = half
|
|
91
123
|
gsub(/`/, "", rest)
|
|
92
124
|
n = split(rest, parts, /[,;]/)
|
|
93
|
-
for (i = 1; i <= n; i++)
|
|
94
|
-
|
|
95
|
-
|
|
125
|
+
for (i = 1; i <= n; i++) {
|
|
126
|
+
chunk = parts[i]
|
|
127
|
+
# Guarded like the strict rules above. Unguarded, a half whose every
|
|
128
|
+
# candidate was a primitive fell through to here and the filter was
|
|
129
|
+
# undone by the code meant to back it up.
|
|
130
|
+
while (match(chunk, /[A-Za-z_][A-Za-z0-9_]*/)) {
|
|
131
|
+
tok = substr(chunk, RSTART, RLENGTH)
|
|
132
|
+
chunk = substr(chunk, RSTART + RLENGTH)
|
|
133
|
+
if (!is_primitive(tok)) { out[++count] = tok; break }
|
|
134
|
+
}
|
|
135
|
+
}
|
|
96
136
|
}
|
|
97
137
|
return count
|
|
98
138
|
}
|
|
99
139
|
|
|
100
|
-
function finding(sev, msg) { out[++nout] = sev
|
|
140
|
+
function finding(sev, msg) { out[++nout] = sev SEP msg }
|
|
101
141
|
|
|
102
142
|
/^[ \t]*```/ { fenced = 1 - fenced; next }
|
|
103
143
|
fenced { next }
|
|
@@ -166,6 +206,7 @@ in_rules && /^- / { nrules++ }
|
|
|
166
206
|
n = symbols(oh, syms, 1)
|
|
167
207
|
for (i = 1; i <= n; i++) {
|
|
168
208
|
if (!(syms[i] in offered_by) || cur < offered_by[syms[i]]) offered_by[syms[i]] = cur
|
|
209
|
+
offers[cur] = offers[cur] " " syms[i]
|
|
169
210
|
}
|
|
170
211
|
next
|
|
171
212
|
}
|
|
@@ -175,15 +216,34 @@ in_rules && /^- / { nrules++ }
|
|
|
175
216
|
body = substr($0, length("**Touches:**") + 1)
|
|
176
217
|
n = split(body, parts, /\|/)
|
|
177
218
|
for (i = 1; i <= n; i++) {
|
|
219
|
+
# The path is everything before the first parenthesis. Stripping only a
|
|
220
|
+
# trailing annotation left the rest of a mid-field one in the path, and
|
|
221
|
+
# those fragments then read as real paths: two tasks with disjoint files
|
|
222
|
+
# came out sharing "(edit)" and were serialised for nothing.
|
|
178
223
|
p = parts[i]
|
|
179
|
-
|
|
224
|
+
if (index(p, "(") > 0) p = substr(p, 1, index(p, "(") - 1)
|
|
180
225
|
p = trim(p)
|
|
181
226
|
if (p == "") continue
|
|
227
|
+
# The annotation is what the tier table turns on, so it is kept rather
|
|
228
|
+
# than stripped and forgotten: an (edit) means existing code changed, and
|
|
229
|
+
# the absence of any (test) means nothing executable covers the task.
|
|
230
|
+
if (parts[i] ~ /\(edit\)/) has_edit[cur] = 1
|
|
231
|
+
if (parts[i] ~ /\(test\)/) has_test[cur] = 1
|
|
232
|
+
if (parts[i] ~ /\(new\)/) has_new[cur] = 1
|
|
233
|
+
# Under-tiering is the unsafe direction: an unrecognised annotation, or a
|
|
234
|
+
# missing one, reads as "nothing was edited here" and the task then owes
|
|
235
|
+
# no review. Say so rather than deriving from a spelling nobody checked.
|
|
236
|
+
if (parts[i] !~ /\((new|edit|test)\)/)
|
|
237
|
+
finding("warn", "task " cur " Touches " p " with no (new), (edit) or (test) annotation, so its tier is derived as if nothing was edited")
|
|
238
|
+
else if (parts[i] !~ /\((new|edit|test)\)[ \t]*$/)
|
|
239
|
+
finding("warn", "task " cur " Touches " p " with text after its annotation; the tier reads correctly but the line is not the format")
|
|
182
240
|
# Accumulated rather than assigned: with three tasks on one path,
|
|
183
241
|
# reporting a single pair leaves the reader serialising two of them and
|
|
184
242
|
# still running the third alongside.
|
|
185
243
|
if (index(" " owners[p] " ", " " cur " ") == 0)
|
|
186
244
|
owners[p] = owners[p] (owners[p] == "" ? "" : " ") cur
|
|
245
|
+
if (index(" " paths[cur] " ", " " p " ") == 0)
|
|
246
|
+
paths[cur] = paths[cur] (paths[cur] == "" ? "" : " ") p
|
|
187
247
|
}
|
|
188
248
|
next
|
|
189
249
|
}
|
|
@@ -259,14 +319,55 @@ END {
|
|
|
259
319
|
finding("warn", "tasks " list " " (n == 2 ? "both" : "all") " touch " p ", so they cannot run at the same time")
|
|
260
320
|
}
|
|
261
321
|
|
|
322
|
+
# A task matching more than one row takes the highest of them, which is what
|
|
323
|
+
# the tier table says and the reason this is a max rather than a chain of
|
|
324
|
+
# elses. Computed once, ahead of both the histogram and the task rows.
|
|
325
|
+
# The LAST task demanding each symbol, so "later tasks build on it" answers
|
|
326
|
+
# for any consumer rather than only the earliest. Kept as a minimum, one early
|
|
327
|
+
# consumer hid every later one and the producer three tasks were built blind
|
|
328
|
+
# against came out owing no review at all.
|
|
329
|
+
for (i = 1; i <= ntasks; i++) {
|
|
330
|
+
n = split(needs[order[i]], want, " ")
|
|
331
|
+
for (j = 1; j <= n; j++)
|
|
332
|
+
if (want[j] != "" && (!(want[j] in last_demand) || order[i] > last_demand[want[j]]))
|
|
333
|
+
last_demand[want[j]] = order[i]
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
for (i = 1; i <= ntasks; i++) {
|
|
337
|
+
id = order[i]
|
|
338
|
+
t = 0
|
|
339
|
+
if (id in has_edit) t = 1
|
|
340
|
+
# A task whose only path is a test is changing a suite that already
|
|
341
|
+
# exists: the format offers no "(test) (edit)" spelling, so the annotation
|
|
342
|
+
# cannot say so, and reading it as a creation leaves the task owing
|
|
343
|
+
# nothing. Under-tiering is the unsafe direction.
|
|
344
|
+
if ((id in has_test) && !(id in has_new) && !(id in has_edit)) t = 1
|
|
345
|
+
n = split(offers[id], mine, " ")
|
|
346
|
+
for (j = 1; j <= n; j++)
|
|
347
|
+
if (mine[j] != "" && (mine[j] in last_demand) && last_demand[mine[j]] > id) t = (t > 1 ? t : 1)
|
|
348
|
+
if (!(id in has_test)) t = (t > 2 ? t : 2)
|
|
349
|
+
if ((id in has_flips) || (id in has_review)) t = 3
|
|
350
|
+
tier_of[id] = t
|
|
351
|
+
}
|
|
352
|
+
|
|
262
353
|
flip = 0
|
|
263
354
|
for (i = 1; i <= ntasks; i++) if (order[i] in has_flips) { flip = order[i]; break }
|
|
264
|
-
|
|
355
|
+
# The histogram is what prices the review question at pre-flight: "four of
|
|
356
|
+
# nine need a reviewer" is a decision a partner can weigh, and counting it
|
|
357
|
+
# by hand off a nine-task plan is how the count comes out wrong.
|
|
358
|
+
hist = ""
|
|
359
|
+
for (t = 0; t <= 3; t++) {
|
|
360
|
+
c = 0
|
|
361
|
+
for (i = 1; i <= ntasks; i++) if (tier_of[order[i]] == t) c++
|
|
362
|
+
if (c > 0) hist = hist (hist == "" ? "" : " ") t ":" c
|
|
363
|
+
}
|
|
364
|
+
print "summary" SEP ntasks SEP flip SEP hist
|
|
265
365
|
for (i = 1; i <= nout; i++) print out[i]
|
|
266
366
|
for (i = 1; i <= ntasks; i++) {
|
|
267
367
|
id = order[i]
|
|
268
|
-
tier =
|
|
269
|
-
print "task
|
|
368
|
+
tier = tier_of[id]
|
|
369
|
+
print "task" SEP id SEP name[id] SEP tier SEP ((id in has_model) ? 1 : 0) SEP ((id in has_flips) ? 1 : 0) \
|
|
370
|
+
SEP trim(needs[id]) SEP trim(offers[id]) SEP trim(paths[id])
|
|
270
371
|
}
|
|
271
372
|
}
|
|
272
373
|
'
|
|
@@ -279,13 +380,14 @@ parse_plan() {
|
|
|
279
380
|
raw="$(awk "$PARSER" "$plan")" || { err "could not read $plan"; exit 4; }
|
|
280
381
|
|
|
281
382
|
local summary
|
|
282
|
-
summary="$(printf '%s\n' "$raw" | grep
|
|
283
|
-
NTASKS="$(printf '%s' "$summary" | cut -f2)"
|
|
284
|
-
FLIP="$(printf '%s' "$summary" | cut -f3)"
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
383
|
+
summary="$(printf '%s\n' "$raw" | grep "^summary$SEP" | head -1)"
|
|
384
|
+
NTASKS="$(printf '%s' "$summary" | cut -d"$SEP" -f2)"
|
|
385
|
+
FLIP="$(printf '%s' "$summary" | cut -d"$SEP" -f3)"
|
|
386
|
+
TIERS="$(printf '%s' "$summary" | cut -d"$SEP" -f4)"
|
|
387
|
+
FINDINGS="$(printf '%s\n' "$raw" | grep -E "^(error|warn)$SEP" || true)"
|
|
388
|
+
TASKROWS="$(printf '%s\n' "$raw" | grep "^task$SEP" || true)"
|
|
389
|
+
NERR="$(printf '%s\n' "$FINDINGS" | grep -c "^error$SEP" || true)"
|
|
390
|
+
NWARN="$(printf '%s\n' "$FINDINGS" | grep -c "^warn$SEP" || true)"
|
|
289
391
|
}
|
|
290
392
|
|
|
291
393
|
# "1 error" / "2 errors", so the header does not read as a template.
|
|
@@ -310,6 +412,7 @@ case "$SUB" in
|
|
|
310
412
|
|
|
311
413
|
head="$(basename "$PLAN"): $(plural "$NTASKS" task)"
|
|
312
414
|
[ "$FLIP" != "0" ] && head="$head, flip at task $FLIP"
|
|
415
|
+
[ -n "$TIERS" ] && head="$head, tiers $TIERS"
|
|
313
416
|
if [ "$NERR" = "0" ] && [ "$NWARN" = "0" ]; then
|
|
314
417
|
echo "$head, no errors"
|
|
315
418
|
exit 0
|
|
@@ -321,9 +424,9 @@ case "$SUB" in
|
|
|
321
424
|
echo
|
|
322
425
|
# Errors before warnings. Within each, the parser's own order, which
|
|
323
426
|
# follows the file.
|
|
324
|
-
{ printf '%s\n' "$FINDINGS" | grep
|
|
325
|
-
printf '%s\n' "$FINDINGS" | grep
|
|
326
|
-
} | while IFS="$
|
|
427
|
+
{ printf '%s\n' "$FINDINGS" | grep "^error$SEP" || true
|
|
428
|
+
printf '%s\n' "$FINDINGS" | grep "^warn$SEP" || true
|
|
429
|
+
} | while IFS="$SEP" read -r sev msg; do
|
|
327
430
|
[ -n "$sev" ] || continue
|
|
328
431
|
printf ' %-5s %s\n' "$sev" "$msg"
|
|
329
432
|
done
|
|
@@ -375,15 +478,37 @@ case "$SUB" in
|
|
|
375
478
|
HAS_MODEL=" $(bash "$STATUS" show --json --dir "$DIR" 2>/dev/null |
|
|
376
479
|
jq -r '[.tasks[]? | select(.model != null) | .id] | join(" ")' 2>/dev/null) "
|
|
377
480
|
|
|
378
|
-
|
|
481
|
+
# The tier table takes the highest row a task matches, so a tier raised by
|
|
482
|
+
# hand is a decision and re-import may only ever raise. Lowering it back
|
|
483
|
+
# silently un-decides it and drops the task out of the review debt it was
|
|
484
|
+
# owed, which is the opposite of what re-import is for.
|
|
485
|
+
RECORDED_TIERS="$(bash "$STATUS" show --json --dir "$DIR" 2>/dev/null |
|
|
486
|
+
jq -r '[.tasks[]? | select(.tier != null) | "\(.id):\(.tier)"] | join(" ")' 2>/dev/null)"
|
|
487
|
+
|
|
488
|
+
printf '%s\n' "$TASKROWS" | while IFS="$SEP" read -r _ id name tier model flips needs offers touches; do
|
|
379
489
|
[ -n "${id:-}" ] || continue
|
|
380
490
|
set -- task "$id" --name "$name" --dir "$DIR"
|
|
381
|
-
|
|
382
|
-
|
|
491
|
+
# The flip is a plan fact rather than a run decision, so import is
|
|
492
|
+
# authoritative on it both ways. Add-only left a moved flip set on two
|
|
493
|
+
# tasks, and the render then named the wrong milestone.
|
|
494
|
+
if [ "$flips" = "1" ]; then set -- "$@" --flips; else set -- "$@" --no-flips; fi
|
|
495
|
+
if [ -n "$tier" ] && [ "$tier" != "-" ]; then
|
|
496
|
+
recorded=""
|
|
497
|
+
for pair in $RECORDED_TIERS; do
|
|
498
|
+
case "$pair" in "$id:"*) recorded="${pair#*:}" ;; esac
|
|
499
|
+
done
|
|
500
|
+
if [ -z "$recorded" ] || [ "$tier" -gt "$recorded" ]; then
|
|
501
|
+
set -- "$@" --tier "$tier"
|
|
502
|
+
fi
|
|
503
|
+
fi
|
|
383
504
|
case "$HAS_MODEL" in
|
|
384
505
|
*" $id "*) ;;
|
|
385
506
|
*) [ "$model" = "1" ] && set -- "$@" --model cheap ;;
|
|
386
507
|
esac
|
|
508
|
+
# Passed unconditionally, empty included: the graph is a plan fact like
|
|
509
|
+
# the flip, so an edge the plan dropped has to be cleared rather than
|
|
510
|
+
# left behind reporting a contract that no longer exists.
|
|
511
|
+
set -- "$@" --needs "${needs:-}" --offers "${offers:-}" --touches "${touches:-}"
|
|
387
512
|
bash "$STATUS" "$@" || exit 1
|
|
388
513
|
done || exit 1
|
|
389
514
|
imported="$NTASKS"
|
|
@@ -9,10 +9,13 @@
|
|
|
9
9
|
#
|
|
10
10
|
# status.sh init --topic <t> --channel <c> [--plan <p>] [--record <p>] [--force]
|
|
11
11
|
# status.sh task <id> [--name <n>] [--status <s>] [--base <sha>]
|
|
12
|
-
# [--commit <sha>] [--tier 0-3] [--model <m>]
|
|
12
|
+
# [--commit <sha>] [--tier 0-3] [--model <m>]
|
|
13
|
+
# [--flips | --no-flips] [--reviewed]
|
|
14
|
+
# [--needs <syms>] [--offers <syms>] [--touches <paths>]
|
|
13
15
|
# status.sh preflight [--review <t>] [--model <t>] [--workspace <t>]
|
|
14
16
|
# status.sh show [--json]
|
|
15
|
-
# status.sh
|
|
17
|
+
# status.sh ready
|
|
18
|
+
# status.sh line [--full]
|
|
16
19
|
# status.sh close
|
|
17
20
|
#
|
|
18
21
|
# --dir <path> selects the tree to read (default: $PWD). State lives at
|
|
@@ -94,17 +97,110 @@ ARCHIVE="$DIR/.sluice/archive"
|
|
|
94
97
|
# `line` swallows everything: a missing jq, unreadable state, no run at all.
|
|
95
98
|
# Any of those printing would put permanent clutter in the status bar.
|
|
96
99
|
if [ "$SUB" = "line" ]; then
|
|
97
|
-
|
|
100
|
+
FULL=0
|
|
101
|
+
while [ $# -gt 0 ]; do
|
|
102
|
+
case "$1" in
|
|
103
|
+
--full) FULL=1; shift ;;
|
|
104
|
+
*) err "unknown flag: $1"; exit 4 ;;
|
|
105
|
+
esac
|
|
106
|
+
done
|
|
98
107
|
command -v jq >/dev/null 2>&1 || exit 0
|
|
99
108
|
[ -f "$STATE" ] || exit 0
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
109
|
+
|
|
110
|
+
if [ "$FULL" -eq 0 ]; then
|
|
111
|
+
jq -r '
|
|
112
|
+
([.tasks[]? | select(.status == "done")] | length) as $done
|
|
113
|
+
| [ "sluice",
|
|
114
|
+
(.channel // "?"),
|
|
115
|
+
"\($done)/\(.tasks | length)",
|
|
116
|
+
([.tasks[]? | select(.status == "active") | "▸T\(.id)"] | first // empty),
|
|
117
|
+
([.tasks[]? | select(.status == "blocked") | "!T\(.id)"] | first // empty)
|
|
118
|
+
] | join(" ")
|
|
119
|
+
' "$STATE" 2>/dev/null || exit 0
|
|
120
|
+
exit 0
|
|
121
|
+
fi
|
|
122
|
+
|
|
123
|
+
# The wide render, three rows: the run and its clock, the bar alone, then the
|
|
124
|
+
# detail. The bar gets a row to itself so it never competes with text for
|
|
125
|
+
# width, which is what lets a cell be wide enough to read as a block.
|
|
126
|
+
#
|
|
127
|
+
# The flip draws as a rule in the bar rather than a name in the first row: it
|
|
128
|
+
# is a boundary between tasks, not a property of one, and the first row cannot
|
|
129
|
+
# say how much of the plan is still reversible.
|
|
130
|
+
#
|
|
131
|
+
# Elapsed is passed in rather than computed here, jq having no clock, and
|
|
132
|
+
# fromdateiso8601 raises rather than returning null, so a bad start time must
|
|
133
|
+
# cost the clock cell and not the render.
|
|
134
|
+
jq -r \
|
|
135
|
+
--argjson now "$(date -u +%s)" \
|
|
136
|
+
--arg esc "$(printf '\033')" '
|
|
137
|
+
def paint($c; $t): "\($esc)[\($c)m\($t)\($esc)[0m";
|
|
138
|
+
def join_parts: map(select(. != null and . != "")) | join(" \($esc)[2m·\($esc)[0m ");
|
|
139
|
+
# Done splits in two. A task that is done and was owed a review nobody has
|
|
140
|
+
# marked keeps the done shape but trails the review glyph, so the debt reads
|
|
141
|
+
# in position rather than only as a count. Tier 0 was never owed a dispatch,
|
|
142
|
+
# so it is plainly done.
|
|
143
|
+
def cellgroup($w):
|
|
144
|
+
(.status == "done"
|
|
145
|
+
and (.tier // 0) >= 1
|
|
146
|
+
and (.reviewed // false) == false) as $owed
|
|
147
|
+
| (if .status == "done" then ["32", "▰"]
|
|
148
|
+
elif .status == "active" then ["96", "◈"]
|
|
149
|
+
elif .status == "review" then ["33", "▨"]
|
|
150
|
+
elif .status == "blocked" then ["91", "▮"]
|
|
151
|
+
else ["2", "▱"]
|
|
152
|
+
end) as $s
|
|
153
|
+
| if $owed and $w > 1
|
|
154
|
+
then paint($s[0]; ($s[1] * ($w - 1))) + paint("33"; "▨")
|
|
155
|
+
else paint($s[0]; ($s[1] * $w))
|
|
156
|
+
end;
|
|
157
|
+
|
|
158
|
+
# Three cells read as a block, one reads as a tick. The width is chosen from
|
|
159
|
+
# what the whole bar would occupy, gaps and the flip boundary included, so
|
|
160
|
+
# the schedule is monotonic in the task count. Keyed off the count alone it
|
|
161
|
+
# was not: thirty tasks at two wide ran wider than twelve at three.
|
|
162
|
+
(.tasks | length) as $n
|
|
163
|
+
| (if $n == 0 then 3
|
|
164
|
+
elif ($n * 4 + 2) <= 74 then 3
|
|
165
|
+
elif ($n * 3 + 2) <= 74 then 2
|
|
166
|
+
else 1 end) as $w
|
|
167
|
+
| (if $w > 1 then " " else "" end) as $gap
|
|
168
|
+
| ([.tasks[]? | select(.status == "done")] | length) as $done
|
|
169
|
+
| ([.tasks[]? | select(.status == "done" and (.tier // 0) >= 1 and (.reviewed // false) == false)] | length) as $debt
|
|
170
|
+
| [.tasks[]? | select(.status == "blocked")] as $blockedAll
|
|
171
|
+
| [.tasks[]? | select(.status == "active")] as $activeAll
|
|
172
|
+
| ($blockedAll | first) as $blocked
|
|
173
|
+
| ($activeAll | first) as $active
|
|
174
|
+
| (if ($blockedAll | length) > 0 then ($blockedAll | length) else ($activeAll | length) end) as $attn
|
|
175
|
+
| ((.started // "" | try fromdateiso8601 catch 0) as $t
|
|
176
|
+
| if $t == 0 then ""
|
|
177
|
+
else (($now - $t) / 60 | floor) as $m
|
|
178
|
+
| if $m < 1 then "◷ <1m"
|
|
179
|
+
elif $m < 60 then "◷ \($m)m"
|
|
180
|
+
else "◷ \($m / 60 | floor)h\($m % 60)m"
|
|
181
|
+
end
|
|
182
|
+
end) as $clock
|
|
183
|
+
| ( paint("1;96"; "⧗") + " "
|
|
184
|
+
+ ([ paint("1;96"; (.channel // "?")),
|
|
185
|
+
paint("2"; (.topic // ""))
|
|
186
|
+
] | join_parts)
|
|
187
|
+
+ (if $clock == "" then "" else " " + paint("2"; $clock) end)
|
|
188
|
+
),
|
|
189
|
+
# The flip is drawn as a rule before its task: everything left of it is
|
|
190
|
+
# inert and safe to leave landed, everything right of it is not. That is
|
|
191
|
+
# what the flip means, and a name in the header could not say it.
|
|
192
|
+
( " " + ([ .tasks[]?
|
|
193
|
+
| (if .flips then paint("95"; "┃") + $gap else "" end)
|
|
194
|
+
+ cellgroup($w)
|
|
195
|
+
] | join($gap)) ),
|
|
196
|
+
( " " + ([ paint("1"; "\($done)/\(.tasks | length)") + " done",
|
|
197
|
+
(if $blocked then paint("1;91"; "!T\($blocked.id) \($blocked.name // "")")
|
|
198
|
+
elif $active then paint("96"; "▸T\($active.id)") + " " + ($active.name // "")
|
|
199
|
+
else "" end)
|
|
200
|
+
+ (if $attn > 1 then paint("2"; " +\($attn - 1)") else "" end),
|
|
201
|
+
(if $debt > 0 then paint("33"; "⟲\($debt) unreviewed") else "" end)
|
|
202
|
+
] | join_parts)
|
|
203
|
+
)
|
|
108
204
|
' "$STATE" 2>/dev/null || exit 0
|
|
109
205
|
exit 0
|
|
110
206
|
fi
|
|
@@ -189,7 +285,8 @@ case "$SUB" in
|
|
|
189
285
|
*[!0-9]* | 0 ) err "task id must be a positive integer, got: $ID"; exit 4 ;;
|
|
190
286
|
esac
|
|
191
287
|
|
|
192
|
-
NAME="" STATUS="" BASE="" COMMIT="" TIER="" MODEL="" FLIPS=false
|
|
288
|
+
NAME="" STATUS="" BASE="" COMMIT="" TIER="" MODEL="" FLIPS=false UNFLIP=false REVIEWED=false
|
|
289
|
+
NEEDS="" OFFERS="" TOUCHES="" GRAPH=0
|
|
193
290
|
while [ $# -gt 0 ]; do
|
|
194
291
|
case "$1" in
|
|
195
292
|
--name) need_value --name $# "${2-}"; NAME="$2"; shift 2 ;;
|
|
@@ -199,10 +296,19 @@ case "$SUB" in
|
|
|
199
296
|
--tier) need_value --tier $# "${2-}"; TIER="$2"; shift 2 ;;
|
|
200
297
|
--model) need_value --model $# "${2-}"; MODEL="$2"; shift 2 ;;
|
|
201
298
|
--flips) FLIPS=true; shift ;;
|
|
299
|
+
--no-flips) UNFLIP=true; shift ;;
|
|
300
|
+
--reviewed) REVIEWED=true; shift ;;
|
|
301
|
+
--needs) need_value --needs $# "${2-}"; NEEDS="$2"; GRAPH=1; shift 2 ;;
|
|
302
|
+
--offers) need_value --offers $# "${2-}"; OFFERS="$2"; GRAPH=1; shift 2 ;;
|
|
303
|
+
--touches) need_value --touches $# "${2-}"; TOUCHES="$2"; GRAPH=1; shift 2 ;;
|
|
202
304
|
*) err "unknown flag: $1"; exit 4 ;;
|
|
203
305
|
esac
|
|
204
306
|
done
|
|
205
307
|
|
|
308
|
+
if [ "$FLIPS" = true ] && [ "$UNFLIP" = true ]; then
|
|
309
|
+
err "--flips and --no-flips were both given; they contradict"
|
|
310
|
+
exit 4
|
|
311
|
+
fi
|
|
206
312
|
if [ -n "$STATUS" ]; then
|
|
207
313
|
in_set "$STATUS" "$STATUSES" || { err "unknown status: $STATUS (one of: $STATUSES)"; exit 4; }
|
|
208
314
|
fi
|
|
@@ -235,7 +341,13 @@ case "$SUB" in
|
|
|
235
341
|
patch="$(jq -n \
|
|
236
342
|
--arg name "$NAME" --arg status "$STATUS" --arg base "$BASE" \
|
|
237
343
|
--arg commit "$COMMIT" --arg tier "$TIER" --arg model "$MODEL" \
|
|
238
|
-
--argjson flips "$FLIPS"
|
|
344
|
+
--argjson flips "$FLIPS" --argjson unflip "$UNFLIP" --argjson reviewed "$REVIEWED" \
|
|
345
|
+
--arg needs "$NEEDS" --arg offers "$OFFERS" --arg touches "$TOUCHES" \
|
|
346
|
+
--argjson graph "$GRAPH" '
|
|
347
|
+
def words: split(" ") | map(select(. != "")) | unique;
|
|
348
|
+
# A given-but-empty column clears the key: the caller passing the graph
|
|
349
|
+
# is authoritative on it, so a dropped edge does not survive.
|
|
350
|
+
def edge($v): if $v == "" then null else ($v | words) end;
|
|
239
351
|
{}
|
|
240
352
|
+ (if $name == "" then {} else {name: $name} end)
|
|
241
353
|
+ (if $status == "" then {} else {status: $status} end)
|
|
@@ -244,15 +356,23 @@ case "$SUB" in
|
|
|
244
356
|
+ (if $tier == "" then {} else {tier: ($tier | tonumber)} end)
|
|
245
357
|
+ (if $model == "" then {} else {model: $model} end)
|
|
246
358
|
+ (if $flips then {flips: true} else {} end)
|
|
359
|
+
+ (if $unflip then {flips: null} else {} end)
|
|
360
|
+
+ (if $reviewed then {reviewed: true} else {} end)
|
|
361
|
+
+ (if $graph == 0 then {}
|
|
362
|
+
else {needs: edge($needs), offers: edge($offers), touches: edge($touches)}
|
|
363
|
+
end)
|
|
247
364
|
')"
|
|
248
365
|
|
|
249
366
|
jq --argjson id "$ID" --argjson patch "$patch" '
|
|
250
367
|
.tasks = (
|
|
251
368
|
if any(.tasks[]?; .id == $id)
|
|
252
|
-
then [.tasks[] | if .id == $id then . + $patch else . end]
|
|
369
|
+
then [.tasks[] | if .id == $id then (. + $patch) else . end]
|
|
253
370
|
else .tasks + [{id: $id, status: "todo"} + $patch]
|
|
254
371
|
end
|
|
255
372
|
)
|
|
373
|
+
# A null in the patch means clear, not store: leaving it would make
|
|
374
|
+
# every reader test for absent and for null.
|
|
375
|
+
| .tasks |= map(with_entries(select(.value != null)))
|
|
256
376
|
| .tasks |= sort_by(.id)
|
|
257
377
|
' "$STATE" | write_state
|
|
258
378
|
;;
|
|
@@ -314,6 +434,8 @@ case "$SUB" in
|
|
|
314
434
|
| ["sluice \(.channel) · \(.topic) · \($done)/\(.tasks | length) done"]
|
|
315
435
|
+ ["plan \(.plan | dash)"]
|
|
316
436
|
+ ["record \(.record | dash)"]
|
|
437
|
+
+ (([.tasks[]? | select(.status == "done" and (.tier // 0) >= 1 and (.reviewed // false) == false)] | length) as $debt
|
|
438
|
+
| if $debt == 0 then [] else ["unreviewed \($debt) done, owed a review the tier table promised"] end)
|
|
317
439
|
+ ["pre-flight " + (
|
|
318
440
|
if (.preflight // {} | length) == 0 then "not recorded"
|
|
319
441
|
else [(.preflight | to_entries[] | "\(.key)=\(.value)")] | join("; ")
|
|
@@ -329,6 +451,68 @@ case "$SUB" in
|
|
|
329
451
|
' "$STATE"
|
|
330
452
|
;;
|
|
331
453
|
|
|
454
|
+
ready)
|
|
455
|
+
[ $# -eq 0 ] || { err "ready takes no arguments"; exit 4; }
|
|
456
|
+
require_run
|
|
457
|
+
require_readable
|
|
458
|
+
|
|
459
|
+
# The wave question, which is a graph query rather than a status display:
|
|
460
|
+
# a task is ready when every symbol it Needs is offered by something
|
|
461
|
+
# already done, and two ready tasks are safe together when their Touches
|
|
462
|
+
# are disjoint. Nothing goes concurrent with the flip whatever the graph
|
|
463
|
+
# says, because the invariant it establishes is what later tasks are
|
|
464
|
+
# checked against.
|
|
465
|
+
jq -r '
|
|
466
|
+
# Clipped with a marker, so a cut name does not read as the whole name,
|
|
467
|
+
# and always followed by a gap so it cannot run into the next column.
|
|
468
|
+
def pad($n):
|
|
469
|
+
if length > $n then .[0:$n - 1] + "… "
|
|
470
|
+
else . + (" " * ($n - length + 1))
|
|
471
|
+
end;
|
|
472
|
+
[.tasks[]? | select(.status == "done") | (.offers // [])[]] as $supplied
|
|
473
|
+
| [.tasks[]? | select(((.needs // []) | length) > 0 or ((.touches // []) | length) > 0)] as $withgraph
|
|
474
|
+
| [.tasks[]? | select(.status == "todo" and (.flips // false) == false)] as $pending
|
|
475
|
+
# Active and in-review tasks still hold their paths. Checked only
|
|
476
|
+
# against each other, a wave reads as safe while colliding with work
|
|
477
|
+
# already running, which is worse than not checking at all: the output
|
|
478
|
+
# says "a worktree each".
|
|
479
|
+
| [.tasks[]? | select(.status == "active" or .status == "review")] as $inflight
|
|
480
|
+
| [$pending[] | select([(.needs // [])[] | select(. as $s | $supplied | index($s) == null)] | length == 0)] as $ready
|
|
481
|
+
| [$pending[] | select([(.needs // [])[] | select(. as $s | $supplied | index($s) == null)] | length > 0)] as $waiting
|
|
482
|
+
| ([.tasks[]? | select(.flips)] | first) as $flip
|
|
483
|
+
|
|
484
|
+
| if ($withgraph | length) == 0 then
|
|
485
|
+
"no contract graph in the run state.",
|
|
486
|
+
"re-run `plan.sh import <plan>` to record Needs, Offers and Touches."
|
|
487
|
+
else
|
|
488
|
+
(
|
|
489
|
+
"\($ready | length) ready now"
|
|
490
|
+
+ (if ($ready | length) > 1 then " · a worktree each" else "" end)
|
|
491
|
+
),
|
|
492
|
+
($ready[] | " T\(.id) \(.name // "" | pad(38))\((.touches // []) | join(", "))"),
|
|
493
|
+
# A shared path is what rules two ready tasks out of the same wave,
|
|
494
|
+
# so it is named rather than left to be noticed.
|
|
495
|
+
( [ $ready[] as $a | ($ready + $inflight)[] as $b
|
|
496
|
+
| select($a.id != $b.id)
|
|
497
|
+
| select(($b.status != "todo") or ($a.id < $b.id))
|
|
498
|
+
| [(($a.touches // [])[] | select(. as $p | ($b.touches // []) | index($p)))] as $clash
|
|
499
|
+
| select(($clash | length) > 0)
|
|
500
|
+
| if $b.status == "todo"
|
|
501
|
+
then " T\($a.id) and T\($b.id) share \($clash | join(", ")), so not together"
|
|
502
|
+
else " T\($a.id) shares \($clash | join(", ")) with T\($b.id), already \($b.status)"
|
|
503
|
+
end
|
|
504
|
+
] | unique | .[] ),
|
|
505
|
+
(if ($waiting | length) > 0 then
|
|
506
|
+
"", "\($waiting | length) waiting on a contract",
|
|
507
|
+
($waiting[] | " T\(.id) \(.name // "" | pad(38))needs \([(.needs // [])[] | select(. as $s | $supplied | index($s) == null)] | join(", "))")
|
|
508
|
+
else empty end),
|
|
509
|
+
(if $flip != null and $flip.status != "done" then
|
|
510
|
+
"", "the flip runs alone", " T\($flip.id) \($flip.name // "")"
|
|
511
|
+
else empty end)
|
|
512
|
+
end
|
|
513
|
+
' "$STATE"
|
|
514
|
+
;;
|
|
515
|
+
|
|
332
516
|
close)
|
|
333
517
|
[ $# -eq 0 ] || { err "close takes no arguments"; exit 4; }
|
|
334
518
|
require_run
|
package/skills/sluice/skill.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sluice",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.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 and one status command can answer where the run is, 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",
|