@iceinvein/agent-skills 0.8.4 → 0.9.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/dist/cli/index.js +10 -8
- package/package.json +1 -1
- package/skills/index.json +2 -2
- package/skills/sluice/SKILL.md +20 -7
- package/skills/sluice/references/deep-channel.md +122 -29
- package/skills/sluice/references/status.md +110 -0
- package/skills/sluice/scripts/plan.sh +399 -0
- package/skills/sluice/scripts/status.sh +365 -0
- package/skills/sluice/skill.json +2 -2
package/dist/cli/index.js
CHANGED
|
@@ -21,12 +21,14 @@ var __toESM = (mod, isNodeMode, target) => {
|
|
|
21
21
|
}
|
|
22
22
|
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
23
23
|
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
24
|
+
if (mod && typeof mod === "object" || typeof mod === "function") {
|
|
25
|
+
for (let key of __getOwnPropNames(mod))
|
|
26
|
+
if (!__hasOwnProp.call(to, key))
|
|
27
|
+
__defProp(to, key, {
|
|
28
|
+
get: __accessProp.bind(mod, key),
|
|
29
|
+
enumerable: true
|
|
30
|
+
});
|
|
31
|
+
}
|
|
30
32
|
if (canCache)
|
|
31
33
|
cache.set(mod, to);
|
|
32
34
|
return to;
|
|
@@ -35,7 +37,7 @@ var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports,
|
|
|
35
37
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
36
38
|
|
|
37
39
|
// node_modules/cli-width/index.js
|
|
38
|
-
var require_cli_width = __commonJS((exports, module)
|
|
40
|
+
var require_cli_width = __commonJS(function(exports, module) {
|
|
39
41
|
module.exports = cliWidth;
|
|
40
42
|
function normalizeOpts(options) {
|
|
41
43
|
const defaultOpts = {
|
|
@@ -75,7 +77,7 @@ var require_cli_width = __commonJS((exports, module) => {
|
|
|
75
77
|
});
|
|
76
78
|
|
|
77
79
|
// node_modules/mute-stream/lib/index.js
|
|
78
|
-
var require_lib = __commonJS((exports, module)
|
|
80
|
+
var require_lib = __commonJS(function(exports, module) {
|
|
79
81
|
var Stream = __require("stream");
|
|
80
82
|
|
|
81
83
|
class MuteStream extends Stream {
|
package/package.json
CHANGED
package/skills/index.json
CHANGED
|
@@ -281,9 +281,9 @@
|
|
|
281
281
|
},
|
|
282
282
|
{
|
|
283
283
|
"name": "sluice",
|
|
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, 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.",
|
|
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.10.0"
|
|
287
287
|
},
|
|
288
288
|
{
|
|
289
289
|
"name": "temporal-coupling-detector",
|
package/skills/sluice/SKILL.md
CHANGED
|
@@ -90,6 +90,16 @@ Design to `docs/specs/YYYY-MM-DD-<topic>.md`, plan to
|
|
|
90
90
|
`docs/plans/YYYY-MM-DD-<topic>.md`, unless the repo has a convention or
|
|
91
91
|
your partner states a preference. Get the design signed off before code.
|
|
92
92
|
|
|
93
|
+
Take the design stop through the harness's plan mode where there is one. Its
|
|
94
|
+
gate is enforced rather than requested and it holds edits shut while it is open,
|
|
95
|
+
so nothing gets built against a design nobody signed. It carries the first stop
|
|
96
|
+
only; pre-flight still wants answers, and an approval is not one.
|
|
97
|
+
|
|
98
|
+
The run's state goes in `.sluice/run.json`, written a command at a time by
|
|
99
|
+
`scripts/status.sh`. That is what makes the breakdown readable from outside the
|
|
100
|
+
session running it: a statusline segment while it runs, `status.sh show` after a
|
|
101
|
+
compaction, and neither costing your partner a turn to ask. `references/status.md`
|
|
102
|
+
|
|
93
103
|
**Read `references/deep-channel.md` before you write the plan.** It carries the
|
|
94
104
|
plan format, the pre-flight questions, the run record, the dispatch rules and
|
|
95
105
|
the review tiers, and none of those are derivable from what is on this page.
|
|
@@ -112,6 +122,7 @@ plan:
|
|
|
112
122
|
**Touches:** <path> (new) | <path> (edit) | <path> (test)
|
|
113
123
|
**Flips:** <what changes, from what, or omit>
|
|
114
124
|
**Review:** <reason, or omit>
|
|
125
|
+
**Model:** <cheaper model, and why the work is mechanical, or omit>
|
|
115
126
|
- [ ] <action> -> <proof>
|
|
116
127
|
```
|
|
117
128
|
|
|
@@ -130,19 +141,21 @@ task that turns the new behaviour on with `Flips`. Then a late re-baseline,
|
|
|
130
141
|
re-blessed snapshots, regenerated fixtures, attributes to that one point
|
|
131
142
|
instead of to the branch at large.
|
|
132
143
|
|
|
133
|
-
Pre-flight rides in that same stop: which flagged tasks get a reviewer,
|
|
134
|
-
|
|
135
|
-
|
|
144
|
+
Pre-flight rides in that same stop: which flagged tasks get a reviewer, which
|
|
145
|
+
mechanical ones the plan marked for a cheaper model, and whether the work runs
|
|
146
|
+
in a worktree. Ask them as choices with the counts in them, never as a
|
|
147
|
+
paragraph, then end the turn. Task 1 opens on their next
|
|
136
148
|
instruction, and an answer that already carries one is that instruction. A
|
|
137
149
|
session that dispatches only when asked has not ruled dispatch out, it has made
|
|
138
150
|
this question the place to ask; genuine unavailability is the tool not being
|
|
139
151
|
there at all, which `references/deep-channel.md` handles separately. Review that
|
|
140
152
|
turns out to be missing is only actionable while the plan can still change.
|
|
141
153
|
|
|
142
|
-
|
|
143
|
-
reason
|
|
144
|
-
approval: one reply arrives for
|
|
145
|
-
it signed off the plan and nothing
|
|
154
|
+
All three answers go into `run.json` and the run record before Task 1's first
|
|
155
|
+
edit, the answer in the first and the reason in the second. Those rows are what
|
|
156
|
+
discharge pre-flight, not the approval: one reply arrives for several
|
|
157
|
+
obligations, so a "yes" with no rows behind it signed off the plan and nothing
|
|
158
|
+
else.
|
|
146
159
|
|
|
147
160
|
Three from that file that catch people out: concurrent implementers need a
|
|
148
161
|
worktree each and the flip runs alone, review is tiered rather than automatic,
|
|
@@ -20,6 +20,16 @@
|
|
|
20
20
|
corrupted concurrent run.
|
|
21
21
|
- A **Review** field may name why the task needs the stronger tier (auth,
|
|
22
22
|
data, money, concurrency); the table below decides otherwise.
|
|
23
|
+
- A **Model** field may put the task on a cheaper model, and says why the work
|
|
24
|
+
is mechanical: the Contract is exact, the tests it has to satisfy already
|
|
25
|
+
exist, and nothing in it turns on judgment. Omit it and the task runs on this
|
|
26
|
+
session's model, so the marked tasks are the exceptions rather than the rule.
|
|
27
|
+
Anything the table below sends to tier 3 is disqualified, `Flips` with it:
|
|
28
|
+
those are the tasks where being wrong is expensive, and the saving is not
|
|
29
|
+
worth pricing against that. Mark it here rather than deciding at dispatch,
|
|
30
|
+
because whether a task is mechanical is fixed the moment its Contract and
|
|
31
|
+
Touches are written, and pre-flight is the last point your partner can price
|
|
32
|
+
it.
|
|
23
33
|
- **Order the plan so the inert tasks come first.** A task is inert when it
|
|
24
34
|
adds capability, config, a schema or a code path that nothing reads yet:
|
|
25
35
|
landing it changes no observable behaviour, so it is safe to land alone and
|
|
@@ -36,6 +46,16 @@
|
|
|
36
46
|
the instruction out again in full. "TBD" is another. So is naming a type
|
|
37
47
|
or a function that no task in the plan ever creates.
|
|
38
48
|
|
|
49
|
+
**Run `scripts/plan.sh validate <plan>` on the finished plan, before pre-flight.**
|
|
50
|
+
Most of what this section asks for needs no judgement to check: a `Needs` no
|
|
51
|
+
task `Offers`, a "TBD", a step deferring to a neighbour, a missing `Contract`,
|
|
52
|
+
a plan with no flip or two of them, a `Model` mark on a task the tier table
|
|
53
|
+
sends to tier 3. Those come back as errors with the task number on them. The
|
|
54
|
+
warnings are the judgement calls left to you: a step with no proof, a `Needs`
|
|
55
|
+
satisfied only by a later task, two tasks whose `Touches` overlap. Reading the
|
|
56
|
+
plan yourself catches these on a good day, and the point of a check is the
|
|
57
|
+
other kind of day.
|
|
58
|
+
|
|
39
59
|
```
|
|
40
60
|
# Plan: <topic>
|
|
41
61
|
## Ground Rules
|
|
@@ -45,6 +65,7 @@
|
|
|
45
65
|
**Touches:** <path> (new) | <path> (edit) | <path> (test)
|
|
46
66
|
**Flips:** <what changes, from what, or omit>
|
|
47
67
|
**Review:** <reason, or omit>
|
|
68
|
+
**Model:** <cheaper model, and why the work is mechanical, or omit>
|
|
48
69
|
- [ ] <action> -> <proof>
|
|
49
70
|
```
|
|
50
71
|
|
|
@@ -61,14 +82,31 @@ Split it. A plan with none is not a `deep` plan: nothing in it does anything.
|
|
|
61
82
|
## The run record
|
|
62
83
|
|
|
63
84
|
A `deep` run outlives its own context, so what it learns has to sit on disk
|
|
64
|
-
rather than in the session.
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
and
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
85
|
+
rather than in the session. That lands in two files, and which one a thing goes
|
|
86
|
+
in follows from who has to read it.
|
|
87
|
+
|
|
88
|
+
`.sluice/run.json` holds the state that moves: each task with its status, base
|
|
89
|
+
and commit, its tier, its `Model` mark and its `Flips` line, and the answers
|
|
90
|
+
pre-flight settled. `scripts/status.sh` writes and reads it, and
|
|
91
|
+
`references/status.md` carries the commands and the statusline segment that
|
|
92
|
+
makes a run visible without anyone asking. Open it with `init` when you open the
|
|
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. Import is safe to re-run: it
|
|
95
|
+
sets names, model marks and the flip, and leaves a status already recorded
|
|
96
|
+
alone, so resuming after a compaction cannot rewind the run.
|
|
97
|
+
|
|
98
|
+
The record is the other file, and it holds what a status cannot: the reason
|
|
99
|
+
review went the way it did, the reason a task was downshifted, the reason the
|
|
100
|
+
workspace answer went that way, any finding belonging to a task other than the
|
|
101
|
+
one that surfaced it, and whatever else a stranger resuming tomorrow would need
|
|
102
|
+
and could not derive. **It no longer carries task rows with statuses in them.**
|
|
103
|
+
Status written in both places drifts, and once it has there are two answers and
|
|
104
|
+
nothing to say which is stale.
|
|
105
|
+
|
|
106
|
+
Pre-flight lands in both, which is the one deliberate overlap: `run.json` holds
|
|
107
|
+
the answer, so the file can say whether the stop happened at all, and the record
|
|
108
|
+
holds the reason, so a reader can tell whether it should have gone that way.
|
|
109
|
+
Those rows come first and open the record for the same reason they always did.
|
|
72
110
|
|
|
73
111
|
Where it goes follows the repo if the repo has a convention, and
|
|
74
112
|
`docs/plans/YYYY-MM-DD-<topic>-record.md` if it does not. It belongs to you
|
|
@@ -77,16 +115,47 @@ task's `Touches` names it, and you commit it yourself alongside the plan.
|
|
|
77
115
|
Assembling it at handback defeats it: a record written from memory is memory,
|
|
78
116
|
which is the one thing the file exists to replace.
|
|
79
117
|
|
|
80
|
-
A file only outlives compaction if you go back to it.
|
|
81
|
-
dispatch whenever this session has been
|
|
82
|
-
what you remember, including where the
|
|
83
|
-
|
|
84
|
-
|
|
118
|
+
A file only outlives compaction if you go back to it. Run `status.sh show` and
|
|
119
|
+
read the record before the next dispatch whenever this session has been
|
|
120
|
+
summarised, and treat what they say over what you remember, including where the
|
|
121
|
+
two agree. Each task closes by writing its commit into `run.json`, which means
|
|
122
|
+
asking the implementer to report the SHA it committed and passing it to
|
|
123
|
+
`status.sh task <id> --status done --commit <sha>` rather than deriving it later.
|
|
124
|
+
|
|
125
|
+
## The design stop and plan mode
|
|
126
|
+
|
|
127
|
+
The design stop is a stop because a plan written against the wrong design wastes
|
|
128
|
+
a plan's worth of work. Sluice enforced it with a sentence, "a stop ends your
|
|
129
|
+
turn", which is the weakest gate available in a harness that has a real one.
|
|
130
|
+
|
|
131
|
+
Claude Code's plan mode is the real one. `EnterPlanMode` needs your partner's
|
|
132
|
+
consent to enter, `ExitPlanMode` will not proceed without their approval, and
|
|
133
|
+
edits are held shut in between, so the design cannot be quietly built against
|
|
134
|
+
while it is still a draft. Take the design stop through it.
|
|
135
|
+
|
|
136
|
+
What that changes: the design gets drafted in the plan file the harness names,
|
|
137
|
+
and `ExitPlanMode` is the sign-off rather than a paragraph asking for one.
|
|
138
|
+
|
|
139
|
+
What it does not replace is pre-flight. That stop wants three answers, and an
|
|
140
|
+
approval is not an answer to any of them, so it stays where it is, after plan
|
|
141
|
+
mode has exited and the plan is written. One enforced gate does not collapse two
|
|
142
|
+
stops into one; it only makes the first of them hold.
|
|
143
|
+
|
|
144
|
+
**The harness's plan file is not the artifact.** It belongs to the mode and not
|
|
145
|
+
to the run. On approval, write the design to `docs/specs/YYYY-MM-DD-<topic>.md`,
|
|
146
|
+
the plan to `docs/plans/YYYY-MM-DD-<topic>.md`, and open the run record and
|
|
147
|
+
`run.json`. Those are the durable files, the ones a session resuming next week
|
|
148
|
+
reads, and none of them is the one you drafted in.
|
|
149
|
+
|
|
150
|
+
Where plan mode is unavailable, the prose stop is what you have and it is the
|
|
151
|
+
same stop: end the turn on the design and let the next instruction start the
|
|
152
|
+
plan. Nothing else about this section changes, because the obligation was never
|
|
153
|
+
the mode's, only the enforcement was.
|
|
85
154
|
|
|
86
155
|
## Pre-flight
|
|
87
156
|
|
|
88
157
|
Design signed off, plan written, nothing built yet. Before Task 1, stop once
|
|
89
|
-
and settle
|
|
158
|
+
and settle three things with your partner. Ask them as questions with options,
|
|
90
159
|
not as a paragraph they have to reply to in prose: what you are after is a
|
|
91
160
|
decision, and a wall of considerations asks them to extract the decision from
|
|
92
161
|
it first.
|
|
@@ -103,11 +172,18 @@ your sign-off and Task 1 begins.
|
|
|
103
172
|
**Review.** Name the tasks the table below sends to a reviewer, each with the
|
|
104
173
|
trigger that qualified it, and say how many of the rest skip with a ledger
|
|
105
174
|
line. Then offer the choice: dispatch a reviewer at each of them, dispatch
|
|
106
|
-
only at
|
|
175
|
+
only at tier 3, or hand back with those tasks listed as
|
|
107
176
|
review outstanding. The options are what makes the cost legible.
|
|
108
177
|
"Four of nine need a reviewer" is a decision your partner can price; "I will
|
|
109
178
|
review where appropriate" is not.
|
|
110
179
|
|
|
180
|
+
**Model.** The plan already marked which tasks are mechanical, so this is a
|
|
181
|
+
ratification rather than a fresh judgment, and it carries the count: six of
|
|
182
|
+
nine on the cheaper model and three on this session's, take it, put everything
|
|
183
|
+
on the session model, or name the exceptions. The count is what your partner
|
|
184
|
+
prices. "I will use the cheaper model where it fits" prices nothing, and it
|
|
185
|
+
also arrives after the tokens are spent.
|
|
186
|
+
|
|
111
187
|
**Workspace, commits and concurrency.** One worktree for the plan, one per
|
|
112
188
|
concurrent implementer, or straight onto the current branch; and with it who
|
|
113
189
|
commits, each agent committing its own task as it finishes or agents leaving
|
|
@@ -137,9 +213,10 @@ is the declared schedule the dispatch rules reject.
|
|
|
137
213
|
If one of the two has only one live answer, say which and ask the other. A stop
|
|
138
214
|
down to a single question is still a stop.
|
|
139
215
|
|
|
140
|
-
**Write the answers
|
|
141
|
-
|
|
142
|
-
|
|
216
|
+
**Write the answers down before Task 1's first edit.** Both files:
|
|
217
|
+
`status.sh preflight` for the answers, the run record for the reason each one
|
|
218
|
+
went that way. That pair is what discharges pre-flight, rather than the
|
|
219
|
+
approval you got, and the distinction is the whole point: a stop
|
|
143
220
|
that carries the plan and pre-flight together has one reply for two obligations,
|
|
144
221
|
so a bare "yes" satisfies the plan and leaves no trace either way of the
|
|
145
222
|
questions. Rows in a file leave that trace. If Task 1 is about to open and the
|
|
@@ -166,10 +243,15 @@ Derive the sets at dispatch rather than writing wave numbers into the plan. A
|
|
|
166
243
|
declared schedule is wrong the moment one task lands late or comes back with a
|
|
167
244
|
blocking finding. A derived one just recomputes.
|
|
168
245
|
|
|
169
|
-
- One
|
|
246
|
+
- One row per task in `run.json`, flipped to `active` and then `done` as it
|
|
170
247
|
moves. That state outlives compaction; your memory doesn't.
|
|
171
248
|
- Each task goes to a fresh agent with that task's text and nothing else.
|
|
172
249
|
What this session accumulated is yours to hold, not theirs.
|
|
250
|
+
- **Label the dispatch `T<n>: <task name>`.** The harness lists running agents
|
|
251
|
+
under whatever label the dispatch gave them, so labelled by task that list
|
|
252
|
+
reads as the plan and labelled anything else it reads as a row of anonymous
|
|
253
|
+
agents. It costs nothing and it is the only place a partner can see which
|
|
254
|
+
task is in flight without asking.
|
|
173
255
|
- **Fan out wherever the graph allows.** Work that does not write is always
|
|
174
256
|
safe and always parallel: investigations, searches and reviewers, one agent
|
|
175
257
|
per question, all in one message so they run at once.
|
|
@@ -201,8 +283,11 @@ blocking finding. A derived one just recomputes.
|
|
|
201
283
|
authoring a message for a diff you did not write means reading that diff,
|
|
202
284
|
which is the context dispatch exists to keep out of this session. Pre-flight
|
|
203
285
|
can overturn it for a given run.
|
|
204
|
-
-
|
|
205
|
-
and
|
|
286
|
+
- **The plan and pre-flight decide the model, not the moment of dispatch.** A
|
|
287
|
+
task with no `Model` line goes out on this session's, and a downshift that
|
|
288
|
+
was neither marked nor ratified is a saving your partner never agreed to.
|
|
289
|
+
Reviewers are the other half of that rule: a review never runs below the
|
|
290
|
+
model that built the task, so a downshifted task's reviewer comes back up.
|
|
206
291
|
|
|
207
292
|
A per-task commit is not an integration event. `references/finish.md` owns
|
|
208
293
|
push, PR and merge, none of which happen here, and a standing instruction to
|
|
@@ -225,8 +310,8 @@ Three things change. The plan stops being a brief for strangers and becomes
|
|
|
225
310
|
your own worklist, so its stop is no longer buying alignment with the agents
|
|
226
311
|
who will carry it out, only your partner's read of work you will do yourself.
|
|
227
312
|
Task isolation is gone, so
|
|
228
|
-
the
|
|
229
|
-
|
|
313
|
+
`run.json` and the record now carry all of the state that outlives compaction
|
|
314
|
+
and matter more, not less. And fresh context is unavailable, which was the entire thing
|
|
230
315
|
review was buying.
|
|
231
316
|
|
|
232
317
|
One thing does not change: the work still owes a review. Reading your own diff
|
|
@@ -259,8 +344,15 @@ and it fails by printing success rather than by erroring.
|
|
|
259
344
|
| 0 | Created files only, executable tests exist and pass, Contract matches | No dispatch. Read the commit stat yourself. |
|
|
260
345
|
| 1 | Modified existing code, or later tasks build on it | One reviewer dispatch |
|
|
261
346
|
| 2 | No executable test covers it: prose, config, docs | One reviewer dispatch; a stat cannot confirm the words are right |
|
|
262
|
-
| 3 | Auth, data, money, concurrency, or the plan flags it | One reviewer dispatch
|
|
263
|
-
| 3 | Carries the `Flips` line | One reviewer dispatch
|
|
347
|
+
| 3 | Auth, data, money, concurrency, or the plan flags it | One reviewer dispatch; the task may not be downshifted |
|
|
348
|
+
| 3 | Carries the `Flips` line | One reviewer dispatch; the task may not be downshifted |
|
|
349
|
+
|
|
350
|
+
No tier buys a bigger model, because a plan that downshifted nothing has every
|
|
351
|
+
agent on the same one, and a tier promising something stronger would be
|
|
352
|
+
promising what is already in use. What a tier buys is a dispatch. The one model
|
|
353
|
+
rule that holds across all of them is that a review never runs below the model
|
|
354
|
+
that built the task, so tiers 1 and 2 over a downshifted task come back up to
|
|
355
|
+
this session's.
|
|
264
356
|
|
|
265
357
|
A task matching more than one row takes the highest tier of them. Tier is the
|
|
266
358
|
number, not the row order and not which shape sounds more serious. A task that
|
|
@@ -279,8 +371,8 @@ a reviewer writes nothing, so it collides with nothing. The final review is
|
|
|
279
371
|
the only one that waits, because it is the only one that needs everything to
|
|
280
372
|
have landed.
|
|
281
373
|
|
|
282
|
-
Record the base against that task
|
|
283
|
-
the agent's first commit lands. Recovering it afterwards is archaeology, and the
|
|
374
|
+
Record the base against that task with `status.sh task <id> --base <sha>` when
|
|
375
|
+
you dispatch, before the agent's first commit lands. Recovering it afterwards is archaeology, and the
|
|
284
376
|
answer you will guess at is `HEAD~1`, which `references/review.md` already
|
|
285
377
|
names as the standing mistake.
|
|
286
378
|
|
|
@@ -323,8 +415,9 @@ absorb into the next task's brief.
|
|
|
323
415
|
## The final review
|
|
324
416
|
|
|
325
417
|
It covers cross-task integration and everything the record accumulated, not
|
|
326
|
-
lines a per-task review already cleared. It is a dispatch, on
|
|
327
|
-
model, and it gets the whole-plan diff and the
|
|
418
|
+
lines a per-task review already cleared. It is a dispatch, on this session's
|
|
419
|
+
model and never a downshifted one, and it gets the whole-plan diff and the
|
|
420
|
+
deferred findings as a list.
|
|
328
421
|
|
|
329
422
|
Size the brief to what it is actually carrying, and say which of two things it
|
|
330
423
|
is. After nine per-task reviews cleared, it is an integration check. When
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Run state
|
|
2
|
+
|
|
3
|
+
The plan says what the tasks are. The run record says why each decision went
|
|
4
|
+
the way it did. Neither answers "where is this right now" to anything but a
|
|
5
|
+
model reading prose, which is why a `deep` run is invisible from outside the
|
|
6
|
+
session that is running it: your partner has to ask, and asking costs a turn
|
|
7
|
+
and gets an answer from memory.
|
|
8
|
+
|
|
9
|
+
`.sluice/run.json` is that answer in a form something else can read. One file
|
|
10
|
+
per tree, holding only what changes as the run moves.
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
bash <skill-dir>/scripts/status.sh init --topic <t> --channel deep \
|
|
14
|
+
--plan docs/plans/<date>-<topic>.md --record docs/plans/<date>-<topic>-record.md
|
|
15
|
+
bash <skill-dir>/scripts/status.sh task 3 --name "adapter seam" --tier 1 --model cheap
|
|
16
|
+
bash <skill-dir>/scripts/status.sh task 3 --status active --base 75014c9
|
|
17
|
+
bash <skill-dir>/scripts/status.sh task 3 --status done --commit 2c7f261
|
|
18
|
+
bash <skill-dir>/scripts/status.sh preflight --review "tier 3 only" --model "6 of 9 cheap" \
|
|
19
|
+
--workspace "one worktree per implementer"
|
|
20
|
+
bash <skill-dir>/scripts/status.sh show
|
|
21
|
+
bash <skill-dir>/scripts/status.sh close
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`--dir <path>` reads another tree, which is what the statusline uses. Statuses
|
|
25
|
+
are `todo`, `active`, `review`, `done` and `blocked`. A new id needs `--name`;
|
|
26
|
+
after that every call is a bare flip, so keeping it current costs one command
|
|
27
|
+
per transition rather than a paragraph. `close` archives the run under
|
|
28
|
+
`.sluice/archive/` and frees the tree for the next one.
|
|
29
|
+
|
|
30
|
+
A command that cannot finish leaves the state exactly as it found it, so a
|
|
31
|
+
failed `task` never costs you the rows already in the file. Two argument rules
|
|
32
|
+
follow from that being worth guaranteeing: a value beginning with `--` is
|
|
33
|
+
rejected rather than accepted as a value, and a flag with no value at all is
|
|
34
|
+
rejected rather than taking the next flag as one.
|
|
35
|
+
|
|
36
|
+
Gitignore `.sluice/`. It is working state, and everything durable in it lands
|
|
37
|
+
somewhere else anyway: the commits are in git and the reasons are in the record,
|
|
38
|
+
which is the file that does get committed.
|
|
39
|
+
|
|
40
|
+
Open it with `init` when you open the run record, at the same point and for the
|
|
41
|
+
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 one tier
|
|
43
|
+
the plan settles are all fixed the moment the plan is written and are already in
|
|
44
|
+
the file, so typing them again is transcription with a chance of error in it.
|
|
45
|
+
|
|
46
|
+
Re-importing is safe and is the right move after the plan changes. It refreshes
|
|
47
|
+
names, the flip and the tier, and it leaves a status or a ratified model already
|
|
48
|
+
recorded alone, so resuming after a compaction cannot rewind the run.
|
|
49
|
+
|
|
50
|
+
## What goes where
|
|
51
|
+
|
|
52
|
+
**`run.json` owns status. The record owns why.** A status written into both
|
|
53
|
+
drifts, and the moment it does there are two answers and no way to tell which
|
|
54
|
+
is stale. So the record stops carrying task rows with statuses in them and
|
|
55
|
+
carries what a status cannot hold: the reason review went the way it did, the
|
|
56
|
+
reason a task was downshifted, a finding that belongs to a task other than the
|
|
57
|
+
one that surfaced it, and what a stranger resuming tomorrow would need and
|
|
58
|
+
could not derive.
|
|
59
|
+
|
|
60
|
+
Pre-flight answers land in both, and that is deliberate rather than an
|
|
61
|
+
exception: `run.json` holds the answer so the file can say whether the stop
|
|
62
|
+
happened, the record holds the reason so a reader can tell whether it should
|
|
63
|
+
have gone that way. Those are different claims.
|
|
64
|
+
|
|
65
|
+
## Reading it back
|
|
66
|
+
|
|
67
|
+
`show` prints the whole run: channel, topic, how many tasks are done, the plan
|
|
68
|
+
and record paths, the pre-flight answers, and a row per task with its base,
|
|
69
|
+
commit, tier and model. Run it after compaction instead of reconstructing the
|
|
70
|
+
run from what you remember, and run it in the message that hands the work back,
|
|
71
|
+
where "four of nine, task five blocked" is a fact your partner can act on.
|
|
72
|
+
|
|
73
|
+
`show --json` is the same state for another reader. `line` is the one-line form,
|
|
74
|
+
and it exits 0 in silence on a missing run, unreadable state or a missing jq,
|
|
75
|
+
because its caller is a status bar with nowhere to put an error.
|
|
76
|
+
|
|
77
|
+
## Statusline
|
|
78
|
+
|
|
79
|
+
This is the part that makes a run visible without anyone asking. The segment
|
|
80
|
+
goes in the Claude Code statusline command, keyed off the state file existing so
|
|
81
|
+
a session with no run in flight spawns nothing:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
sluice_line=""
|
|
85
|
+
if [ -n "$cwd" ] && [ -f "$cwd/.sluice/run.json" ]; then
|
|
86
|
+
for sluice_sh in "$cwd/.claude/skills/sluice/scripts/status.sh" \
|
|
87
|
+
"$HOME/.claude/skills/sluice/scripts/status.sh"; do
|
|
88
|
+
[ -f "$sluice_sh" ] || continue
|
|
89
|
+
sluice_line=$(bash "$sluice_sh" line --dir "$cwd" 2>/dev/null)
|
|
90
|
+
break
|
|
91
|
+
done
|
|
92
|
+
fi
|
|
93
|
+
[ -n "$sluice_line" ] && printf ' %s' "$sluice_line"
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`$cwd` is `workspace.current_dir` from the JSON the harness sends on stdin. It
|
|
97
|
+
renders as `sluice deep 4/9 ▸T5`, or `!T5` where a task is blocked, which is
|
|
98
|
+
the one state worth colouring as a warning rather than as progress.
|
|
99
|
+
|
|
100
|
+
A run that is only visible to the session running it is a run your partner
|
|
101
|
+
cannot redirect. That is the same argument the channel announcement makes, and
|
|
102
|
+
the statusline is where it holds for the hour after the announcement scrolled
|
|
103
|
+
away.
|
|
104
|
+
|
|
105
|
+
## Friction
|
|
106
|
+
|
|
107
|
+
"I know which task I'm on." You do, until this session is summarised. The file
|
|
108
|
+
costs one command per transition and is the only thing in the run that survives
|
|
109
|
+
that, and a plan whose progress lives in one context window is a plan nobody
|
|
110
|
+
outside that window can read.
|
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# sluice plan tooling: the parts of the plan format that a reader can check
|
|
3
|
+
# without judgement.
|
|
4
|
+
#
|
|
5
|
+
# `references/deep-channel.md` is 21KB read once, at the point the plan is
|
|
6
|
+
# written, and relied on from memory for the rest of the run. Most of what it
|
|
7
|
+
# asks for is not a matter of taste: a Needs no task Offers, a "TBD", a step
|
|
8
|
+
# deferring to another task, a plan with no flip or two. Those are checkable, so
|
|
9
|
+
# they should be checked rather than remembered.
|
|
10
|
+
#
|
|
11
|
+
# plan.sh validate <plan.md>
|
|
12
|
+
# plan.sh import <plan.md> [--dir <path>] [--force]
|
|
13
|
+
#
|
|
14
|
+
# validate prints one line per finding. An error means the plan cannot be
|
|
15
|
+
# dispatched as written; a warning is a judgement call left to its author.
|
|
16
|
+
# import seeds the run state's task rows from the plan, so the ids, names, the
|
|
17
|
+
# flip, the model marks and the one tier the plan settles come from the file
|
|
18
|
+
# rather than from a dozen hand-typed commands. It is safe to re-run: a status or
|
|
19
|
+
# a ratified model already recorded is left alone.
|
|
20
|
+
#
|
|
21
|
+
# Exit: 0 no errors, 2 errors found, 4 bad arguments, 5 jq missing (import only).
|
|
22
|
+
|
|
23
|
+
set -uo pipefail
|
|
24
|
+
|
|
25
|
+
err() { echo "plan.sh: $*" >&2; }
|
|
26
|
+
|
|
27
|
+
usage() {
|
|
28
|
+
echo "usage:" >&2
|
|
29
|
+
sed -n '/^# plan.sh validate/,/^# Exit:/p' "$0" | sed 's/^# \{0,2\}//' >&2
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
# The parser. Emits tab-separated rows on stdout:
|
|
33
|
+
# summary <task count> <flip task or 0>
|
|
34
|
+
# error|warn <message>
|
|
35
|
+
# task <id> <name> <tier 3 or -> <model 1|0> <flips 1|0>
|
|
36
|
+
#
|
|
37
|
+
# Only tier 3 is derivable from the plan: Flips and a Review flag are the two
|
|
38
|
+
# triggers the plan itself settles. The rest turn on task shape and stay for the
|
|
39
|
+
# tier table to decide.
|
|
40
|
+
#
|
|
41
|
+
# One pass, line-oriented, because the plan format is a strict skeleton rather
|
|
42
|
+
# than free markdown. Fenced blocks are skipped: a plan carries signatures and
|
|
43
|
+
# commands in them, and a `- [ ]` inside a fence is an example, not a step.
|
|
44
|
+
PARSER='
|
|
45
|
+
function trim(s) { gsub(/^[ \t]+|[ \t]+$/, "", s); return s }
|
|
46
|
+
|
|
47
|
+
# Symbols on one side of a Contract line. The two sides are read differently
|
|
48
|
+
# because a mistake costs differently on each.
|
|
49
|
+
#
|
|
50
|
+
# Supply (Offers) is generous: every identifier in the half goes on offer, so a
|
|
51
|
+
# return shape spelled `parsePlan(path) -> `PlanDoc`` puts PlanDoc on offer as
|
|
52
|
+
# well as parsePlan, which is what the format asks for when it says both sides
|
|
53
|
+
# are spelled out whole. An extra symbol on offer costs nothing.
|
|
54
|
+
#
|
|
55
|
+
# Demand (Needs) is strict, because an extra symbol here is a false error:
|
|
56
|
+
# backticked names and signatures only, falling back to the leading identifier
|
|
57
|
+
# of each comma-separated part when the half carries neither.
|
|
58
|
+
function symbols(half, out, generous, count, rest, tok, lower, n, parts, i, chunk) {
|
|
59
|
+
lower = half
|
|
60
|
+
gsub(/`/, "", lower)
|
|
61
|
+
lower = tolower(trim(lower))
|
|
62
|
+
if (lower == "" || lower == "none" || lower == "-" || lower == "n/a") return 0
|
|
63
|
+
count = 0
|
|
64
|
+
|
|
65
|
+
if (generous) {
|
|
66
|
+
rest = half
|
|
67
|
+
gsub(/`/, " ", rest)
|
|
68
|
+
while (match(rest, /[A-Za-z_][A-Za-z0-9_]*/)) {
|
|
69
|
+
out[++count] = substr(rest, RSTART, RLENGTH)
|
|
70
|
+
rest = substr(rest, RSTART + RLENGTH)
|
|
71
|
+
}
|
|
72
|
+
return count
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
rest = half
|
|
76
|
+
while (match(rest, /`[^`]*`/)) {
|
|
77
|
+
chunk = substr(rest, RSTART + 1, RLENGTH - 2)
|
|
78
|
+
rest = substr(rest, RSTART + RLENGTH)
|
|
79
|
+
if (match(chunk, /[A-Za-z_][A-Za-z0-9_]*/))
|
|
80
|
+
out[++count] = substr(chunk, RSTART, RLENGTH)
|
|
81
|
+
}
|
|
82
|
+
rest = half
|
|
83
|
+
gsub(/`/, "", rest)
|
|
84
|
+
while (match(rest, /[A-Za-z_][A-Za-z0-9_]*\(/)) {
|
|
85
|
+
tok = substr(rest, RSTART, RLENGTH - 1)
|
|
86
|
+
out[++count] = tok
|
|
87
|
+
rest = substr(rest, RSTART + RLENGTH)
|
|
88
|
+
}
|
|
89
|
+
if (count == 0) {
|
|
90
|
+
rest = half
|
|
91
|
+
gsub(/`/, "", rest)
|
|
92
|
+
n = split(rest, parts, /[,;]/)
|
|
93
|
+
for (i = 1; i <= n; i++)
|
|
94
|
+
if (match(parts[i], /[A-Za-z_][A-Za-z0-9_]*/))
|
|
95
|
+
out[++count] = substr(parts[i], RSTART, RLENGTH)
|
|
96
|
+
}
|
|
97
|
+
return count
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function finding(sev, msg) { out[++nout] = sev "\t" msg }
|
|
101
|
+
|
|
102
|
+
/^[ \t]*```/ { fenced = 1 - fenced; next }
|
|
103
|
+
fenced { next }
|
|
104
|
+
|
|
105
|
+
/^# Plan:/ { has_title = 1 }
|
|
106
|
+
|
|
107
|
+
/^## / {
|
|
108
|
+
in_rules = (tolower($0) ~ /^## ground rules/) ? 1 : 0
|
|
109
|
+
if (in_rules) has_rules_heading = 1
|
|
110
|
+
# A section after the last task belongs to the plan, not to that task. Left
|
|
111
|
+
# set, cur attributes its prose and any checkbox in it to a task that ended
|
|
112
|
+
# several headings ago.
|
|
113
|
+
cur = 0
|
|
114
|
+
next
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/^### Task / {
|
|
118
|
+
in_rules = 0
|
|
119
|
+
s = substr($0, 10)
|
|
120
|
+
colon = index(s, ":")
|
|
121
|
+
if (colon == 0) { finding("error", "a task heading has no name: " $0); next }
|
|
122
|
+
id = trim(substr(s, 1, colon - 1))
|
|
123
|
+
if (id !~ /^[0-9]+$/) { finding("error", "a task heading is not numbered: " $0); next }
|
|
124
|
+
id = id + 0
|
|
125
|
+
if (id in seen) {
|
|
126
|
+
finding("error", "task " id " appears twice; a repeated number makes the dependency order unreadable")
|
|
127
|
+
}
|
|
128
|
+
seen[id] = 1
|
|
129
|
+
order[++ntasks] = id
|
|
130
|
+
name[id] = trim(substr(s, colon + 1))
|
|
131
|
+
cur = id
|
|
132
|
+
next
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
in_rules && /^- / { nrules++ }
|
|
136
|
+
|
|
137
|
+
# Everything below belongs to a task, so a field outside one is a field nobody
|
|
138
|
+
# reads.
|
|
139
|
+
!cur { next }
|
|
140
|
+
|
|
141
|
+
# Scanned over every line of a task body, and deliberately ahead of the field
|
|
142
|
+
# and step rules: those end in `next`, so a check placed after them never sees a
|
|
143
|
+
# step, which is exactly where a "TBD" turns up.
|
|
144
|
+
{
|
|
145
|
+
if ($0 ~ /TBD/)
|
|
146
|
+
finding("error", "task " cur ": \"TBD\" is not an instruction")
|
|
147
|
+
if (tolower($0) ~ /same as (task|step)/)
|
|
148
|
+
finding("error", "task " cur ": \"same as ...\" defers to a task its implementer never sees")
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/^\*\*Contract:\*\*/ {
|
|
152
|
+
has_contract[cur] = 1
|
|
153
|
+
body = substr($0, length("**Contract:**") + 1)
|
|
154
|
+
oi = index(body, "Offers:")
|
|
155
|
+
if (oi > 0) { nh = substr(body, 1, oi - 1); oh = substr(body, oi + 7) }
|
|
156
|
+
else { nh = body; oh = "" }
|
|
157
|
+
ni = index(nh, "Needs:")
|
|
158
|
+
if (ni > 0) nh = substr(nh, ni + 6)
|
|
159
|
+
sub(/\|[ \t]*$/, "", nh)
|
|
160
|
+
|
|
161
|
+
delete syms
|
|
162
|
+
n = symbols(nh, syms, 0)
|
|
163
|
+
for (i = 1; i <= n; i++) needs[cur] = needs[cur] " " syms[i]
|
|
164
|
+
|
|
165
|
+
delete syms
|
|
166
|
+
n = symbols(oh, syms, 1)
|
|
167
|
+
for (i = 1; i <= n; i++) {
|
|
168
|
+
if (!(syms[i] in offered_by) || cur < offered_by[syms[i]]) offered_by[syms[i]] = cur
|
|
169
|
+
}
|
|
170
|
+
next
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/^\*\*Touches:\*\*/ {
|
|
174
|
+
has_touches[cur] = 1
|
|
175
|
+
body = substr($0, length("**Touches:**") + 1)
|
|
176
|
+
n = split(body, parts, /\|/)
|
|
177
|
+
for (i = 1; i <= n; i++) {
|
|
178
|
+
p = parts[i]
|
|
179
|
+
sub(/\([^)]*\)[ \t]*$/, "", p)
|
|
180
|
+
p = trim(p)
|
|
181
|
+
if (p == "") continue
|
|
182
|
+
# Accumulated rather than assigned: with three tasks on one path,
|
|
183
|
+
# reporting a single pair leaves the reader serialising two of them and
|
|
184
|
+
# still running the third alongside.
|
|
185
|
+
if (index(" " owners[p] " ", " " cur " ") == 0)
|
|
186
|
+
owners[p] = owners[p] (owners[p] == "" ? "" : " ") cur
|
|
187
|
+
}
|
|
188
|
+
next
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/^\*\*Flips:\*\*/ { has_flips[cur] = 1; nflips++; flips_list = flips_list (flips_list == "" ? "" : ", ") cur; next }
|
|
192
|
+
/^\*\*Review:\*\*/ { has_review[cur] = 1; next }
|
|
193
|
+
/^\*\*Model:\*\*/ { has_model[cur] = 1; next }
|
|
194
|
+
|
|
195
|
+
/^- \[[ xX]\]/ {
|
|
196
|
+
nsteps[cur]++
|
|
197
|
+
# Counted rather than reported here. One warning per step turns a plan
|
|
198
|
+
# written before the proof convention into forty identical lines, and the
|
|
199
|
+
# errors underneath them stop being read.
|
|
200
|
+
if (index($0, "->") == 0) noproof[cur]++
|
|
201
|
+
next
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
END {
|
|
205
|
+
if (!has_title) finding("warn", "no \"# Plan: <topic>\" heading, so the file does not say what it plans")
|
|
206
|
+
|
|
207
|
+
if (!has_rules_heading || nrules == 0)
|
|
208
|
+
finding("error", "no Ground Rules with entries; an implementer arrives with its own defaults and uses them on anything left unsaid")
|
|
209
|
+
|
|
210
|
+
if (ntasks == 0) {
|
|
211
|
+
finding("error", "no tasks, so there is nothing to dispatch")
|
|
212
|
+
} else {
|
|
213
|
+
for (i = 1; i <= ntasks; i++) {
|
|
214
|
+
id = order[i]
|
|
215
|
+
if (id != i)
|
|
216
|
+
finding("error", "tasks are not numbered 1.." ntasks " in sequence: task " id " sits where " i " should")
|
|
217
|
+
}
|
|
218
|
+
for (i = 1; i <= ntasks; i++) {
|
|
219
|
+
id = order[i]
|
|
220
|
+
if (!(id in has_contract))
|
|
221
|
+
finding("error", "task " id " has no Contract, so a symbol absent from it does not exist for whoever builds it")
|
|
222
|
+
if (!(id in has_touches))
|
|
223
|
+
finding("error", "task " id " has no Touches, and concurrency is derived from it")
|
|
224
|
+
if (!(id in nsteps))
|
|
225
|
+
finding("error", "task " id " has no steps")
|
|
226
|
+
else if (id in noproof)
|
|
227
|
+
finding("warn", "task " id ": " noproof[id] " of " nsteps[id] " steps have no proof after ->, so nothing says they worked")
|
|
228
|
+
if ((id in has_model) && (id in has_flips))
|
|
229
|
+
finding("error", "task " id " carries Flips and a Model mark; the flip is tier 3 and may not be downshifted")
|
|
230
|
+
if ((id in has_model) && (id in has_review))
|
|
231
|
+
finding("error", "task " id " is flagged for Review and carries a Model mark; a tier 3 task may not be downshifted")
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (nflips == 0)
|
|
235
|
+
finding("error", "no task carries Flips, so nothing in the plan turns anything on")
|
|
236
|
+
else if (nflips > 1)
|
|
237
|
+
finding("error", "two or more tasks carry Flips (" flips_list "); a plan with more than one holds that many branches of work")
|
|
238
|
+
|
|
239
|
+
for (i = 1; i <= ntasks; i++) {
|
|
240
|
+
id = order[i]
|
|
241
|
+
n = split(needs[id], want, " ")
|
|
242
|
+
for (j = 1; j <= n; j++) {
|
|
243
|
+
sym = want[j]
|
|
244
|
+
if (sym == "" || ((id "\t" sym) in reported)) continue
|
|
245
|
+
reported[id "\t" sym] = 1
|
|
246
|
+
if (!(sym in offered_by))
|
|
247
|
+
finding("error", "task " id " Needs " sym ", which no task Offers")
|
|
248
|
+
else if (offered_by[sym] > id)
|
|
249
|
+
finding("warn", "task " id " Needs " sym ", offered only by the later task " offered_by[sym])
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
for (p in owners) {
|
|
255
|
+
n = split(owners[p], who, " ")
|
|
256
|
+
if (n < 2) continue
|
|
257
|
+
list = who[1]
|
|
258
|
+
for (i = 2; i <= n; i++) list = list (i == n ? " and " : ", ") who[i]
|
|
259
|
+
finding("warn", "tasks " list " " (n == 2 ? "both" : "all") " touch " p ", so they cannot run at the same time")
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
flip = 0
|
|
263
|
+
for (i = 1; i <= ntasks; i++) if (order[i] in has_flips) { flip = order[i]; break }
|
|
264
|
+
print "summary\t" ntasks "\t" flip
|
|
265
|
+
for (i = 1; i <= nout; i++) print out[i]
|
|
266
|
+
for (i = 1; i <= ntasks; i++) {
|
|
267
|
+
id = order[i]
|
|
268
|
+
tier = ((id in has_flips) || (id in has_review)) ? "3" : "-"
|
|
269
|
+
print "task\t" id "\t" name[id] "\t" tier "\t" ((id in has_model) ? 1 : 0) "\t" ((id in has_flips) ? 1 : 0)
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
'
|
|
273
|
+
|
|
274
|
+
# Runs the parser and splits its output into the shell. Sets: NTASKS, FLIP,
|
|
275
|
+
# FINDINGS (newline-separated sev\tmsg), TASKROWS (newline-separated), NERR,
|
|
276
|
+
# NWARN.
|
|
277
|
+
parse_plan() {
|
|
278
|
+
local plan="$1" raw
|
|
279
|
+
raw="$(awk "$PARSER" "$plan")" || { err "could not read $plan"; exit 4; }
|
|
280
|
+
|
|
281
|
+
local summary
|
|
282
|
+
summary="$(printf '%s\n' "$raw" | grep '^summary ' | head -1)"
|
|
283
|
+
NTASKS="$(printf '%s' "$summary" | cut -f2)"
|
|
284
|
+
FLIP="$(printf '%s' "$summary" | cut -f3)"
|
|
285
|
+
FINDINGS="$(printf '%s\n' "$raw" | grep -E '^(error|warn) ' || true)"
|
|
286
|
+
TASKROWS="$(printf '%s\n' "$raw" | grep '^task ' || true)"
|
|
287
|
+
NERR="$(printf '%s\n' "$FINDINGS" | grep -c '^error ' || true)"
|
|
288
|
+
NWARN="$(printf '%s\n' "$FINDINGS" | grep -c '^warn ' || true)"
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
# "1 error" / "2 errors", so the header does not read as a template.
|
|
292
|
+
plural() { [ "$1" = "1" ] && echo "$1 $2" || echo "$1 $2s"; }
|
|
293
|
+
|
|
294
|
+
SUB="${1-}"
|
|
295
|
+
[ $# -gt 0 ] && shift
|
|
296
|
+
if [ -z "$SUB" ]; then
|
|
297
|
+
usage
|
|
298
|
+
exit 4
|
|
299
|
+
fi
|
|
300
|
+
|
|
301
|
+
case "$SUB" in
|
|
302
|
+
validate)
|
|
303
|
+
PLAN="${1-}"
|
|
304
|
+
[ -n "$PLAN" ] || { err "validate needs a plan path"; exit 4; }
|
|
305
|
+
[ -f "$PLAN" ] || { err "no such plan: $PLAN"; exit 4; }
|
|
306
|
+
shift
|
|
307
|
+
[ $# -eq 0 ] || { err "unknown argument: $1"; exit 4; }
|
|
308
|
+
|
|
309
|
+
parse_plan "$PLAN"
|
|
310
|
+
|
|
311
|
+
head="$(basename "$PLAN"): $(plural "$NTASKS" task)"
|
|
312
|
+
[ "$FLIP" != "0" ] && head="$head, flip at task $FLIP"
|
|
313
|
+
if [ "$NERR" = "0" ] && [ "$NWARN" = "0" ]; then
|
|
314
|
+
echo "$head, no errors"
|
|
315
|
+
exit 0
|
|
316
|
+
fi
|
|
317
|
+
[ "$NERR" != "0" ] && head="$head, $(plural "$NERR" error)"
|
|
318
|
+
[ "$NWARN" != "0" ] && head="$head, $(plural "$NWARN" warning)"
|
|
319
|
+
[ "$NERR" = "0" ] && head="$head, no errors"
|
|
320
|
+
echo "$head"
|
|
321
|
+
echo
|
|
322
|
+
# Errors before warnings. Within each, the parser's own order, which
|
|
323
|
+
# follows the file.
|
|
324
|
+
{ printf '%s\n' "$FINDINGS" | grep '^error ' || true
|
|
325
|
+
printf '%s\n' "$FINDINGS" | grep '^warn ' || true
|
|
326
|
+
} | while IFS="$(printf '\t')" read -r sev msg; do
|
|
327
|
+
[ -n "$sev" ] || continue
|
|
328
|
+
printf ' %-5s %s\n' "$sev" "$msg"
|
|
329
|
+
done
|
|
330
|
+
|
|
331
|
+
[ "$NERR" = "0" ] || exit 2
|
|
332
|
+
;;
|
|
333
|
+
|
|
334
|
+
import)
|
|
335
|
+
PLAN="${1-}"
|
|
336
|
+
[ -n "$PLAN" ] || { err "import needs a plan path"; exit 4; }
|
|
337
|
+
[ -f "$PLAN" ] || { err "no such plan: $PLAN"; exit 4; }
|
|
338
|
+
shift
|
|
339
|
+
|
|
340
|
+
DIR="$PWD" FORCE=""
|
|
341
|
+
while [ $# -gt 0 ]; do
|
|
342
|
+
case "$1" in
|
|
343
|
+
--dir)
|
|
344
|
+
if [ $# -lt 2 ] || [ -z "${2-}" ]; then err "--dir needs a path"; exit 4; fi
|
|
345
|
+
case "$2" in --*) err "--dir needs a path, but the next argument is the flag $2"; exit 4 ;; esac
|
|
346
|
+
DIR="$2"; shift 2 ;;
|
|
347
|
+
--force) FORCE=1; shift ;;
|
|
348
|
+
*) err "unknown flag: $1"; exit 4 ;;
|
|
349
|
+
esac
|
|
350
|
+
done
|
|
351
|
+
|
|
352
|
+
command -v jq >/dev/null 2>&1 || { err "jq is required"; exit 5; }
|
|
353
|
+
STATUS="$(dirname "$0")/status.sh"
|
|
354
|
+
[ -f "$STATUS" ] || { err "status.sh not found beside this script"; exit 4; }
|
|
355
|
+
|
|
356
|
+
parse_plan "$PLAN"
|
|
357
|
+
|
|
358
|
+
# A plan with errors in it is a plan nobody should be running yet, so
|
|
359
|
+
# importing one would seed a run against work that has to change first.
|
|
360
|
+
if [ "$NERR" != "0" ]; then
|
|
361
|
+
if [ -n "$FORCE" ]; then
|
|
362
|
+
err "$PLAN has $(plural "$NERR" error); importing anyway because --force was passed"
|
|
363
|
+
else
|
|
364
|
+
err "$PLAN has $(plural "$NERR" error); fix them or re-run with --force (plan.sh validate lists them)"
|
|
365
|
+
exit 2
|
|
366
|
+
fi
|
|
367
|
+
fi
|
|
368
|
+
|
|
369
|
+
[ "$NTASKS" != "0" ] || { err "$PLAN has no tasks to import"; exit 2; }
|
|
370
|
+
|
|
371
|
+
# The plan marks that a task is mechanical; pre-flight ratifies which model
|
|
372
|
+
# it actually runs on, and that answer lives in run.json. Re-import must
|
|
373
|
+
# not replace it with the placeholder, so the ids already carrying a model
|
|
374
|
+
# are read first and skipped.
|
|
375
|
+
HAS_MODEL=" $(bash "$STATUS" show --json --dir "$DIR" 2>/dev/null |
|
|
376
|
+
jq -r '[.tasks[]? | select(.model != null) | .id] | join(" ")' 2>/dev/null) "
|
|
377
|
+
|
|
378
|
+
printf '%s\n' "$TASKROWS" | while IFS="$(printf '\t')" read -r _ id name tier model flips; do
|
|
379
|
+
[ -n "${id:-}" ] || continue
|
|
380
|
+
set -- task "$id" --name "$name" --dir "$DIR"
|
|
381
|
+
[ "$flips" = "1" ] && set -- "$@" --flips
|
|
382
|
+
[ "$tier" != "-" ] && [ -n "$tier" ] && set -- "$@" --tier "$tier"
|
|
383
|
+
case "$HAS_MODEL" in
|
|
384
|
+
*" $id "*) ;;
|
|
385
|
+
*) [ "$model" = "1" ] && set -- "$@" --model cheap ;;
|
|
386
|
+
esac
|
|
387
|
+
bash "$STATUS" "$@" || exit 1
|
|
388
|
+
done || exit 1
|
|
389
|
+
imported="$NTASKS"
|
|
390
|
+
|
|
391
|
+
echo "imported $(plural "$imported" task) from $(basename "$PLAN")"
|
|
392
|
+
;;
|
|
393
|
+
|
|
394
|
+
*)
|
|
395
|
+
err "unknown subcommand: $SUB"
|
|
396
|
+
usage
|
|
397
|
+
exit 4
|
|
398
|
+
;;
|
|
399
|
+
esac
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# sluice run state: the task breakdown, in a form something other than a
|
|
3
|
+
# language model can read.
|
|
4
|
+
#
|
|
5
|
+
# The prose run record still holds the reasons and the cross-task findings.
|
|
6
|
+
# This file holds only what changes as the run moves, so a statusline, a
|
|
7
|
+
# resumed session or a reader who never saw the plan can answer "where is
|
|
8
|
+
# this" without parsing markdown.
|
|
9
|
+
#
|
|
10
|
+
# status.sh init --topic <t> --channel <c> [--plan <p>] [--record <p>] [--force]
|
|
11
|
+
# status.sh task <id> [--name <n>] [--status <s>] [--base <sha>]
|
|
12
|
+
# [--commit <sha>] [--tier 0-3] [--model <m>] [--flips]
|
|
13
|
+
# status.sh preflight [--review <t>] [--model <t>] [--workspace <t>]
|
|
14
|
+
# status.sh show [--json]
|
|
15
|
+
# status.sh line
|
|
16
|
+
# status.sh close
|
|
17
|
+
#
|
|
18
|
+
# --dir <path> selects the tree to read (default: $PWD). State lives at
|
|
19
|
+
# <dir>/.sluice/run.json and closed runs at <dir>/.sluice/archive/.
|
|
20
|
+
#
|
|
21
|
+
# Exit: 0 ok, 1 the state could not be written, 2 no live run, 3 a run is
|
|
22
|
+
# already live, 4 bad arguments, 5 jq missing, 6 the state file is unreadable.
|
|
23
|
+
# `line` is exempt and always exits 0 in silence, because a statusline renders
|
|
24
|
+
# on every keystroke and has nowhere to put an error.
|
|
25
|
+
#
|
|
26
|
+
# A flag value may not begin with `--`: unchecked, an omitted value silently
|
|
27
|
+
# becomes the next flag and the field holds a flag name.
|
|
28
|
+
|
|
29
|
+
set -uo pipefail
|
|
30
|
+
|
|
31
|
+
STATUSES="todo active review done blocked"
|
|
32
|
+
CHANNELS="bypass fast main deep"
|
|
33
|
+
|
|
34
|
+
err() { echo "status.sh: $*" >&2; }
|
|
35
|
+
|
|
36
|
+
usage() {
|
|
37
|
+
echo "usage:" >&2
|
|
38
|
+
sed -n '/^# status.sh init/,/^# 5 jq missing/p' "$0" | sed 's/^# \{0,2\}//' >&2
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# A flag's value has to be checked before `shift 2`, not after. Bash refuses to
|
|
42
|
+
# shift when fewer than two arguments remain and returns non-zero instead, and
|
|
43
|
+
# with no `set -e` the flag loop then spins forever: $# stops decreasing and the
|
|
44
|
+
# loop condition stays true. Runs in the current shell rather than a subshell so
|
|
45
|
+
# its exit is the script's.
|
|
46
|
+
need_value() { # <flag> <remaining $#> <candidate>
|
|
47
|
+
if [ "$2" -lt 2 ]; then
|
|
48
|
+
err "$1 needs a value"
|
|
49
|
+
exit 4
|
|
50
|
+
fi
|
|
51
|
+
case "$3" in
|
|
52
|
+
--*) err "$1 needs a value, but the next argument is the flag $3"; exit 4 ;;
|
|
53
|
+
esac
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
# A word from a space-separated set. Keeps validation in one place so every
|
|
57
|
+
# rejection reads the same and names the offending value.
|
|
58
|
+
in_set() {
|
|
59
|
+
local needle="$1" hay="$2" w
|
|
60
|
+
for w in $hay; do [ "$w" = "$needle" ] && return 0; done
|
|
61
|
+
return 1
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
# --dir may arrive anywhere in the line, so it is stripped before the
|
|
65
|
+
# subcommand's own flag loop ever sees it.
|
|
66
|
+
DIR="$PWD"
|
|
67
|
+
ARGS=()
|
|
68
|
+
while [ $# -gt 0 ]; do
|
|
69
|
+
case "$1" in
|
|
70
|
+
--dir)
|
|
71
|
+
need_value --dir $# "${2-}"
|
|
72
|
+
[ -n "$2" ] || { err "--dir needs a path"; exit 4; }
|
|
73
|
+
DIR="$2"
|
|
74
|
+
shift 2
|
|
75
|
+
;;
|
|
76
|
+
*)
|
|
77
|
+
ARGS+=("$1")
|
|
78
|
+
shift
|
|
79
|
+
;;
|
|
80
|
+
esac
|
|
81
|
+
done
|
|
82
|
+
set -- "${ARGS[@]+"${ARGS[@]}"}"
|
|
83
|
+
|
|
84
|
+
SUB="${1-}"
|
|
85
|
+
[ $# -gt 0 ] && shift
|
|
86
|
+
if [ -z "$SUB" ]; then
|
|
87
|
+
usage
|
|
88
|
+
exit 4
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
STATE="$DIR/.sluice/run.json"
|
|
92
|
+
ARCHIVE="$DIR/.sluice/archive"
|
|
93
|
+
|
|
94
|
+
# `line` swallows everything: a missing jq, unreadable state, no run at all.
|
|
95
|
+
# Any of those printing would put permanent clutter in the status bar.
|
|
96
|
+
if [ "$SUB" = "line" ]; then
|
|
97
|
+
[ $# -eq 0 ] || { err "line takes no arguments"; exit 4; }
|
|
98
|
+
command -v jq >/dev/null 2>&1 || exit 0
|
|
99
|
+
[ -f "$STATE" ] || exit 0
|
|
100
|
+
jq -r '
|
|
101
|
+
([.tasks[]? | select(.status == "done")] | length) as $done
|
|
102
|
+
| [ "sluice",
|
|
103
|
+
(.channel // "?"),
|
|
104
|
+
"\($done)/\(.tasks | length)",
|
|
105
|
+
([.tasks[]? | select(.status == "active") | "▸T\(.id)"] | first // empty),
|
|
106
|
+
([.tasks[]? | select(.status == "blocked") | "!T\(.id)"] | first // empty)
|
|
107
|
+
] | join(" ")
|
|
108
|
+
' "$STATE" 2>/dev/null || exit 0
|
|
109
|
+
exit 0
|
|
110
|
+
fi
|
|
111
|
+
|
|
112
|
+
command -v jq >/dev/null 2>&1 || { err "jq is required"; exit 5; }
|
|
113
|
+
|
|
114
|
+
require_run() {
|
|
115
|
+
[ -f "$STATE" ] || { err "no run is live in $DIR (start one with: status.sh init)"; exit 2; }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
# Distinct from a missing jq, which is exit 5: reported as that, a corrupt state
|
|
119
|
+
# file sends the reader off to install a tool they already have.
|
|
120
|
+
require_readable() {
|
|
121
|
+
jq -e . "$STATE" >/dev/null 2>&1 || {
|
|
122
|
+
err "cannot parse $STATE (repair it, or archive it with: status.sh close)"
|
|
123
|
+
exit 6
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
# Written through a temporary file so an interrupted write cannot leave the
|
|
128
|
+
# run state half-serialised, which would read as a corrupted run rather than
|
|
129
|
+
# as a failed command.
|
|
130
|
+
#
|
|
131
|
+
# The candidate is checked before it is installed, because the failure to guard
|
|
132
|
+
# against is an empty one rather than a malformed one: `cat` succeeds on empty
|
|
133
|
+
# stdin, so a jq that died upstream of this feeds it nothing, and installing
|
|
134
|
+
# nothing atomically is still a wipe of the one file in the run that outlives
|
|
135
|
+
# compaction. A command that cannot finish leaves the state as it found it.
|
|
136
|
+
write_state() {
|
|
137
|
+
local tmp="$STATE.tmp.$$"
|
|
138
|
+
cat >"$tmp"
|
|
139
|
+
if [ ! -s "$tmp" ] || ! jq -e . "$tmp" >/dev/null 2>&1; then
|
|
140
|
+
rm -f "$tmp"
|
|
141
|
+
err "refusing to write $STATE: the update produced no valid state, so the existing state is unchanged"
|
|
142
|
+
exit 1
|
|
143
|
+
fi
|
|
144
|
+
mv "$tmp" "$STATE" || { rm -f "$tmp"; err "could not replace $STATE"; exit 1; }
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
case "$SUB" in
|
|
148
|
+
init)
|
|
149
|
+
TOPIC="" CHANNEL="" PLAN="" RECORD="" FORCE=0
|
|
150
|
+
while [ $# -gt 0 ]; do
|
|
151
|
+
case "$1" in
|
|
152
|
+
--topic) need_value --topic $# "${2-}"; TOPIC="$2"; shift 2 ;;
|
|
153
|
+
--channel) need_value --channel $# "${2-}"; CHANNEL="$2"; shift 2 ;;
|
|
154
|
+
--plan) need_value --plan $# "${2-}"; PLAN="$2"; shift 2 ;;
|
|
155
|
+
--record) need_value --record $# "${2-}"; RECORD="$2"; shift 2 ;;
|
|
156
|
+
--force) FORCE=1; shift ;;
|
|
157
|
+
*) err "unknown flag: $1"; exit 4 ;;
|
|
158
|
+
esac
|
|
159
|
+
done
|
|
160
|
+
[ -n "$TOPIC" ] || { err "init needs --topic"; exit 4; }
|
|
161
|
+
[ -n "$CHANNEL" ] || { err "init needs --channel"; exit 4; }
|
|
162
|
+
in_set "$CHANNEL" "$CHANNELS" || { err "unknown channel: $CHANNEL (one of: $CHANNELS)"; exit 4; }
|
|
163
|
+
|
|
164
|
+
if [ -f "$STATE" ] && [ "$FORCE" -eq 0 ]; then
|
|
165
|
+
live="$(jq -r '.topic // "?"' "$STATE" 2>/dev/null || echo "?")"
|
|
166
|
+
err "a run is already live (topic: $live); pass --force to replace it"
|
|
167
|
+
exit 3
|
|
168
|
+
fi
|
|
169
|
+
|
|
170
|
+
mkdir -p "$DIR/.sluice"
|
|
171
|
+
jq -n \
|
|
172
|
+
--arg topic "$TOPIC" \
|
|
173
|
+
--arg channel "$CHANNEL" \
|
|
174
|
+
--arg started "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
|
175
|
+
--arg plan "$PLAN" \
|
|
176
|
+
--arg record "$RECORD" '
|
|
177
|
+
{schema: 1, topic: $topic, channel: $channel, started: $started}
|
|
178
|
+
+ (if $plan == "" then {} else {plan: $plan} end)
|
|
179
|
+
+ (if $record == "" then {} else {record: $record} end)
|
|
180
|
+
+ {tasks: []}
|
|
181
|
+
' | write_state
|
|
182
|
+
;;
|
|
183
|
+
|
|
184
|
+
task)
|
|
185
|
+
ID="${1-}"
|
|
186
|
+
[ $# -gt 0 ] && shift
|
|
187
|
+
case "$ID" in
|
|
188
|
+
"" ) err "task needs an id"; exit 4 ;;
|
|
189
|
+
*[!0-9]* | 0 ) err "task id must be a positive integer, got: $ID"; exit 4 ;;
|
|
190
|
+
esac
|
|
191
|
+
|
|
192
|
+
NAME="" STATUS="" BASE="" COMMIT="" TIER="" MODEL="" FLIPS=false
|
|
193
|
+
while [ $# -gt 0 ]; do
|
|
194
|
+
case "$1" in
|
|
195
|
+
--name) need_value --name $# "${2-}"; NAME="$2"; shift 2 ;;
|
|
196
|
+
--status) need_value --status $# "${2-}"; STATUS="$2"; shift 2 ;;
|
|
197
|
+
--base) need_value --base $# "${2-}"; BASE="$2"; shift 2 ;;
|
|
198
|
+
--commit) need_value --commit $# "${2-}"; COMMIT="$2"; shift 2 ;;
|
|
199
|
+
--tier) need_value --tier $# "${2-}"; TIER="$2"; shift 2 ;;
|
|
200
|
+
--model) need_value --model $# "${2-}"; MODEL="$2"; shift 2 ;;
|
|
201
|
+
--flips) FLIPS=true; shift ;;
|
|
202
|
+
*) err "unknown flag: $1"; exit 4 ;;
|
|
203
|
+
esac
|
|
204
|
+
done
|
|
205
|
+
|
|
206
|
+
if [ -n "$STATUS" ]; then
|
|
207
|
+
in_set "$STATUS" "$STATUSES" || { err "unknown status: $STATUS (one of: $STATUSES)"; exit 4; }
|
|
208
|
+
fi
|
|
209
|
+
if [ -n "$TIER" ]; then
|
|
210
|
+
case "$TIER" in
|
|
211
|
+
0|1|2|3) ;;
|
|
212
|
+
*) err "tier must be 0, 1, 2 or 3, got: $TIER"; exit 4 ;;
|
|
213
|
+
esac
|
|
214
|
+
fi
|
|
215
|
+
|
|
216
|
+
require_run
|
|
217
|
+
require_readable
|
|
218
|
+
|
|
219
|
+
# A row with no name is a number nobody can act on, so a new id has to
|
|
220
|
+
# bring one. An existing id does not, which is what makes every later
|
|
221
|
+
# call a bare status flip.
|
|
222
|
+
#
|
|
223
|
+
# A read that failed is not the same answer as "the id is known", so the
|
|
224
|
+
# count is checked rather than compared: unchecked, an empty result skips
|
|
225
|
+
# the guard the way a hit would.
|
|
226
|
+
known="$(jq --argjson id "$ID" '[.tasks[]? | select(.id == $id)] | length' "$STATE" 2>/dev/null)"
|
|
227
|
+
case "$known" in
|
|
228
|
+
'' | *[!0-9]*) err "could not read the task list from $STATE"; exit 6 ;;
|
|
229
|
+
esac
|
|
230
|
+
if [ "$known" = "0" ] && [ -z "$NAME" ]; then
|
|
231
|
+
err "task $ID is new here, so it needs --name"
|
|
232
|
+
exit 4
|
|
233
|
+
fi
|
|
234
|
+
|
|
235
|
+
patch="$(jq -n \
|
|
236
|
+
--arg name "$NAME" --arg status "$STATUS" --arg base "$BASE" \
|
|
237
|
+
--arg commit "$COMMIT" --arg tier "$TIER" --arg model "$MODEL" \
|
|
238
|
+
--argjson flips "$FLIPS" '
|
|
239
|
+
{}
|
|
240
|
+
+ (if $name == "" then {} else {name: $name} end)
|
|
241
|
+
+ (if $status == "" then {} else {status: $status} end)
|
|
242
|
+
+ (if $base == "" then {} else {base: $base} end)
|
|
243
|
+
+ (if $commit == "" then {} else {commit: $commit} end)
|
|
244
|
+
+ (if $tier == "" then {} else {tier: ($tier | tonumber)} end)
|
|
245
|
+
+ (if $model == "" then {} else {model: $model} end)
|
|
246
|
+
+ (if $flips then {flips: true} else {} end)
|
|
247
|
+
')"
|
|
248
|
+
|
|
249
|
+
jq --argjson id "$ID" --argjson patch "$patch" '
|
|
250
|
+
.tasks = (
|
|
251
|
+
if any(.tasks[]?; .id == $id)
|
|
252
|
+
then [.tasks[] | if .id == $id then . + $patch else . end]
|
|
253
|
+
else .tasks + [{id: $id, status: "todo"} + $patch]
|
|
254
|
+
end
|
|
255
|
+
)
|
|
256
|
+
| .tasks |= sort_by(.id)
|
|
257
|
+
' "$STATE" | write_state
|
|
258
|
+
;;
|
|
259
|
+
|
|
260
|
+
preflight)
|
|
261
|
+
REVIEW="" MODEL="" WORKSPACE=""
|
|
262
|
+
while [ $# -gt 0 ]; do
|
|
263
|
+
case "$1" in
|
|
264
|
+
--review) need_value --review $# "${2-}"; REVIEW="$2"; shift 2 ;;
|
|
265
|
+
--model) need_value --model $# "${2-}"; MODEL="$2"; shift 2 ;;
|
|
266
|
+
--workspace) need_value --workspace $# "${2-}"; WORKSPACE="$2"; shift 2 ;;
|
|
267
|
+
*) err "unknown flag: $1"; exit 4 ;;
|
|
268
|
+
esac
|
|
269
|
+
done
|
|
270
|
+
if [ -z "$REVIEW$MODEL$WORKSPACE" ]; then
|
|
271
|
+
err "preflight needs at least one of --review, --model, --workspace"
|
|
272
|
+
exit 4
|
|
273
|
+
fi
|
|
274
|
+
require_run
|
|
275
|
+
require_readable
|
|
276
|
+
|
|
277
|
+
jq --arg review "$REVIEW" --arg model "$MODEL" --arg workspace "$WORKSPACE" '
|
|
278
|
+
.preflight = ((.preflight // {})
|
|
279
|
+
+ (if $review == "" then {} else {review: $review} end)
|
|
280
|
+
+ (if $model == "" then {} else {model: $model} end)
|
|
281
|
+
+ (if $workspace == "" then {} else {workspace: $workspace} end))
|
|
282
|
+
' "$STATE" | write_state
|
|
283
|
+
;;
|
|
284
|
+
|
|
285
|
+
show)
|
|
286
|
+
JSON=0
|
|
287
|
+
while [ $# -gt 0 ]; do
|
|
288
|
+
case "$1" in
|
|
289
|
+
--json) JSON=1; shift ;;
|
|
290
|
+
*) err "unknown flag: $1"; exit 4 ;;
|
|
291
|
+
esac
|
|
292
|
+
done
|
|
293
|
+
require_run
|
|
294
|
+
require_readable
|
|
295
|
+
|
|
296
|
+
if [ "$JSON" -eq 1 ]; then
|
|
297
|
+
jq . "$STATE"
|
|
298
|
+
exit 0
|
|
299
|
+
fi
|
|
300
|
+
|
|
301
|
+
# Header and rows are laid out from the same widths, so the two cannot
|
|
302
|
+
# drift apart, and an over-long value is clipped with a marker rather
|
|
303
|
+
# than silently reading as the whole value.
|
|
304
|
+
jq -r '
|
|
305
|
+
def dash: if . == null or . == "" then "-" else . end;
|
|
306
|
+
def cell($w): tostring
|
|
307
|
+
| if length > $w then .[0:$w - 1] + "…"
|
|
308
|
+
else . + (" " * ($w - length))
|
|
309
|
+
end;
|
|
310
|
+
def row($c): " " + ([($c[0] | cell(3)), ($c[1] | cell(8)), ($c[2] | cell(29)),
|
|
311
|
+
($c[3] | cell(9)), ($c[4] | cell(9)), ($c[5] | cell(4)),
|
|
312
|
+
$c[6]] | join(" "));
|
|
313
|
+
([.tasks[]? | select(.status == "done")] | length) as $done
|
|
314
|
+
| ["sluice \(.channel) · \(.topic) · \($done)/\(.tasks | length) done"]
|
|
315
|
+
+ ["plan \(.plan | dash)"]
|
|
316
|
+
+ ["record \(.record | dash)"]
|
|
317
|
+
+ ["pre-flight " + (
|
|
318
|
+
if (.preflight // {} | length) == 0 then "not recorded"
|
|
319
|
+
else [(.preflight | to_entries[] | "\(.key)=\(.value)")] | join("; ")
|
|
320
|
+
end)]
|
|
321
|
+
+ [""]
|
|
322
|
+
+ [row(["id", "status", "task", "base", "commit", "tier", "model"])]
|
|
323
|
+
+ [ .tasks[]?
|
|
324
|
+
| (if .flips then " FLIPS" else "" end) as $flips
|
|
325
|
+
| row([.id, .status, (.name | dash), (.base | dash),
|
|
326
|
+
(.commit | dash), (.tier | dash), (.model | dash)]) + $flips
|
|
327
|
+
]
|
|
328
|
+
| .[]
|
|
329
|
+
' "$STATE"
|
|
330
|
+
;;
|
|
331
|
+
|
|
332
|
+
close)
|
|
333
|
+
[ $# -eq 0 ] || { err "close takes no arguments"; exit 4; }
|
|
334
|
+
require_run
|
|
335
|
+
|
|
336
|
+
# Deliberately not `require_readable`. The parse error every other
|
|
337
|
+
# subcommand raises names close as the way out, so close is the one
|
|
338
|
+
# command that has to accept state nothing else will touch: it moves the
|
|
339
|
+
# file aside intact rather than leaving it wedged in the tree.
|
|
340
|
+
started="$(jq -r '.started // empty' "$STATE" 2>/dev/null)"
|
|
341
|
+
topic="$(jq -r '.topic // empty' "$STATE" 2>/dev/null)"
|
|
342
|
+
stamp="$(printf '%s' "$started" | tr -cd '0-9TZ')"
|
|
343
|
+
slug="$(printf '%s' "${topic:-run}" | tr -cs 'A-Za-z0-9._-' '-')"
|
|
344
|
+
[ -n "$stamp" ] || stamp="unknown"
|
|
345
|
+
mkdir -p "$ARCHIVE"
|
|
346
|
+
|
|
347
|
+
# The archive holds the only copy of a closed run, and two runs sharing a
|
|
348
|
+
# start second and a topic name the same file. mv would leave one of them,
|
|
349
|
+
# so the name gets a suffix rather than the earlier run being silently
|
|
350
|
+
# dropped.
|
|
351
|
+
dest="$ARCHIVE/$stamp-$slug.json"
|
|
352
|
+
n=2
|
|
353
|
+
while [ -e "$dest" ]; do
|
|
354
|
+
dest="$ARCHIVE/$stamp-$slug-$n.json"
|
|
355
|
+
n=$((n + 1))
|
|
356
|
+
done
|
|
357
|
+
mv "$STATE" "$dest"
|
|
358
|
+
;;
|
|
359
|
+
|
|
360
|
+
*)
|
|
361
|
+
err "unknown subcommand: $SUB"
|
|
362
|
+
usage
|
|
363
|
+
exit 4
|
|
364
|
+
;;
|
|
365
|
+
esac
|
package/skills/sluice/skill.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sluice",
|
|
3
|
-
"version": "0.
|
|
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, 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.",
|
|
3
|
+
"version": "0.10.0",
|
|
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",
|
|
7
7
|
"tools": [
|