@iceinvein/agent-skills 0.14.0 → 0.15.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 +59 -9
- package/package.json +1 -1
- package/skills/index.json +2 -2
- package/skills/sluice/SKILL.md +14 -5
- package/skills/sluice/references/deep-channel.md +51 -8
- package/skills/sluice/references/finish.md +30 -0
- package/skills/sluice/references/meter.md +2 -1
- package/skills/sluice/references/status.md +65 -12
- package/skills/sluice/scripts/session-start.sh +62 -0
- package/skills/sluice/scripts/status.sh +113 -18
- package/skills/sluice/skill.json +4 -3
package/dist/cli/index.js
CHANGED
|
@@ -296,6 +296,9 @@ function validateManifest(data) {
|
|
|
296
296
|
if (a.claudeHookDirective !== undefined && typeof a.claudeHookDirective !== "string") {
|
|
297
297
|
return { ok: false, error: "'activation.claudeHookDirective' must be a string" };
|
|
298
298
|
}
|
|
299
|
+
if (a.claudeHookScript !== undefined && typeof a.claudeHookScript !== "string") {
|
|
300
|
+
return { ok: false, error: "'activation.claudeHookScript' must be a string" };
|
|
301
|
+
}
|
|
299
302
|
}
|
|
300
303
|
return { ok: true, manifest: d };
|
|
301
304
|
}
|
|
@@ -425,12 +428,17 @@ function runScript(scriptPath, cwd) {
|
|
|
425
428
|
});
|
|
426
429
|
return { ok: result.exitCode === 0, code: result.exitCode ?? 1 };
|
|
427
430
|
}
|
|
428
|
-
function
|
|
431
|
+
function shq(s) {
|
|
432
|
+
return "'" + s.replace(/'/g, "'\\''") + "'";
|
|
433
|
+
}
|
|
434
|
+
function matchesSkillDirective(hook, skillName, directive) {
|
|
429
435
|
if (hook.skill !== undefined)
|
|
430
436
|
return hook.skill === skillName;
|
|
437
|
+
if (directive !== undefined && hook.command.includes(`echo ${shq(directive)}`))
|
|
438
|
+
return true;
|
|
431
439
|
return hook.command.includes(`Activate ${skillName} skill`);
|
|
432
440
|
}
|
|
433
|
-
async function wireSessionStartHook(settingsPath, skillName, directive) {
|
|
441
|
+
async function wireSessionStartHook(settingsPath, skillName, directive, scriptPath) {
|
|
434
442
|
let settings = {};
|
|
435
443
|
if (existsSync2(settingsPath)) {
|
|
436
444
|
settings = await Bun.file(settingsPath).json();
|
|
@@ -439,12 +447,50 @@ async function wireSessionStartHook(settingsPath, skillName, directive) {
|
|
|
439
447
|
settings.hooks = {};
|
|
440
448
|
if (!settings.hooks.SessionStart)
|
|
441
449
|
settings.hooks.SessionStart = [];
|
|
442
|
-
const command = `echo
|
|
450
|
+
const command = scriptPath ? `echo ${shq(directive)}; if [ -f ${shq(scriptPath)} ]; then bash ${shq(scriptPath)}; fi` : `echo ${shq(directive)}`;
|
|
451
|
+
const legacy = `echo ${shq(directive)}`;
|
|
452
|
+
let adopted = false;
|
|
453
|
+
let custom = false;
|
|
454
|
+
let changed = false;
|
|
443
455
|
for (const group of settings.hooks.SessionStart) {
|
|
456
|
+
const kept = [];
|
|
444
457
|
for (const hook of group.hooks ?? []) {
|
|
445
|
-
if (matchesSkillDirective(hook, skillName))
|
|
446
|
-
|
|
458
|
+
if (!matchesSkillDirective(hook, skillName, directive)) {
|
|
459
|
+
kept.push(hook);
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
const plain = hook.skill !== undefined || hook.command === legacy;
|
|
463
|
+
if (!plain) {
|
|
464
|
+
custom = true;
|
|
465
|
+
kept.push(hook);
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
if (adopted) {
|
|
469
|
+
changed = true;
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
adopted = true;
|
|
473
|
+
if (hook.command !== command || hook.skill !== skillName) {
|
|
474
|
+
hook.command = command;
|
|
475
|
+
hook.skill = skillName;
|
|
476
|
+
changed = true;
|
|
477
|
+
}
|
|
478
|
+
kept.push(hook);
|
|
479
|
+
}
|
|
480
|
+
if (kept.length !== (group.hooks ?? []).length)
|
|
481
|
+
group.hooks = kept;
|
|
482
|
+
}
|
|
483
|
+
settings.hooks.SessionStart = settings.hooks.SessionStart.filter((group) => !group.hooks || group.hooks.length > 0);
|
|
484
|
+
if (adopted || custom) {
|
|
485
|
+
if (custom && !adopted && scriptPath) {
|
|
486
|
+
console.warn(`${skillName}: a hand-edited SessionStart hook already carries its directive, so it was left as is and ${scriptPath} was not wired; add it to that entry yourself if you want it.`);
|
|
447
487
|
}
|
|
488
|
+
if (changed) {
|
|
489
|
+
mkdirSync(dirname(settingsPath), { recursive: true });
|
|
490
|
+
await Bun.write(settingsPath, JSON.stringify(settings, null, 2) + `
|
|
491
|
+
`);
|
|
492
|
+
}
|
|
493
|
+
return;
|
|
448
494
|
}
|
|
449
495
|
settings.hooks.SessionStart.push({
|
|
450
496
|
hooks: [{ type: "command", command, skill: skillName }]
|
|
@@ -453,15 +499,17 @@ async function wireSessionStartHook(settingsPath, skillName, directive) {
|
|
|
453
499
|
await Bun.write(settingsPath, JSON.stringify(settings, null, 2) + `
|
|
454
500
|
`);
|
|
455
501
|
}
|
|
456
|
-
async function unwireSessionStartHook(settingsPath, skillName) {
|
|
502
|
+
async function unwireSessionStartHook(settingsPath, skillName, directive) {
|
|
457
503
|
if (!existsSync2(settingsPath))
|
|
458
504
|
return;
|
|
459
505
|
const settings = await Bun.file(settingsPath).json();
|
|
460
506
|
const sessionStart = settings.hooks?.SessionStart;
|
|
461
507
|
if (!sessionStart)
|
|
462
508
|
return;
|
|
509
|
+
const legacy = directive === undefined ? undefined : `echo ${shq(directive)}`;
|
|
510
|
+
const ours = (h) => matchesSkillDirective(h, skillName, directive) && (h.skill !== undefined || legacy === undefined || h.command === legacy);
|
|
463
511
|
const filteredGroups = sessionStart.map((group) => ({
|
|
464
|
-
hooks: (group.hooks ?? []).filter((h) => !
|
|
512
|
+
hooks: (group.hooks ?? []).filter((h) => !ours(h))
|
|
465
513
|
})).filter((group) => group.hooks.length > 0);
|
|
466
514
|
if (filteredGroups.length === 0) {
|
|
467
515
|
delete settings.hooks.SessionStart;
|
|
@@ -529,7 +577,9 @@ var claudeAdapter = {
|
|
|
529
577
|
}
|
|
530
578
|
if (activation === "global" && manifest.activation?.claudeHookDirective) {
|
|
531
579
|
const settingsPath = join2(cwd, ".claude/settings.json");
|
|
532
|
-
|
|
580
|
+
const script = manifest.activation.claudeHookScript;
|
|
581
|
+
const scriptPath = script && config.bundleRoot ? join2(cwd, config.bundleRoot, script) : undefined;
|
|
582
|
+
await wireSessionStartHook(settingsPath, manifest.name, manifest.activation.claudeHookDirective, scriptPath);
|
|
533
583
|
if (!installed.includes(".claude/settings.json")) {
|
|
534
584
|
installed.push(".claude/settings.json");
|
|
535
585
|
}
|
|
@@ -594,7 +644,7 @@ var claudeAdapter = {
|
|
|
594
644
|
}
|
|
595
645
|
if (manifest.activation?.claudeHookDirective) {
|
|
596
646
|
const settingsPath = join2(cwd, ".claude/settings.json");
|
|
597
|
-
await unwireSessionStartHook(settingsPath, manifest.name);
|
|
647
|
+
await unwireSessionStartHook(settingsPath, manifest.name, manifest.activation.claudeHookDirective);
|
|
598
648
|
}
|
|
599
649
|
}
|
|
600
650
|
};
|
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. 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
|
|
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, one status command and a SessionStart hook can answer where the run is (the hook prints a live run at every session start, compaction included), and closes each run with a ledger read out of the session transcript: elapsed, tools, tokens, and what each dispatched agent cost where the transcript recorded it. Claude Code only; stands down where the superpowers pipeline governs the repo.",
|
|
285
285
|
"type": "prompt",
|
|
286
|
-
"version": "0.
|
|
286
|
+
"version": "0.16.0"
|
|
287
287
|
},
|
|
288
288
|
{
|
|
289
289
|
"name": "temporal-coupling-detector",
|
package/skills/sluice/SKILL.md
CHANGED
|
@@ -30,8 +30,11 @@ back to you and the run never left your hands.
|
|
|
30
30
|
|
|
31
31
|
Name the channel and the signal that actually routed you there. The strings above
|
|
32
32
|
are examples, not fixed copy, and a channel with a two-part signal should say
|
|
33
|
-
which part applied.
|
|
34
|
-
|
|
33
|
+
which part applied. One thing is fixed: the words `<channel> channel` open the
|
|
34
|
+
message or follow a label such as `Sluice:`, because the meter finds the run by
|
|
35
|
+
them and an announcement worded otherwise reports as `not announced`. `bypass`
|
|
36
|
+
says nothing at all, because a question that gets announced stops being a
|
|
37
|
+
question.
|
|
35
38
|
|
|
36
39
|
**`root-cause`, `finish`, `meter` and `show-or-say` are not channel-assigned.**
|
|
37
40
|
The code misbehaving triggers the first: a bug report, a red test, behaviour you
|
|
@@ -157,10 +160,16 @@ discharge pre-flight, not the approval: one reply arrives for several
|
|
|
157
160
|
obligations, so a "yes" with no rows behind it signed off the plan and nothing
|
|
158
161
|
else.
|
|
159
162
|
|
|
160
|
-
|
|
163
|
+
Four from that file that catch people out: concurrent implementers need a
|
|
161
164
|
worktree each and the flip runs alone, review is tiered rather than automatic,
|
|
162
|
-
|
|
163
|
-
something, not quietly ship without one.
|
|
165
|
+
a `deep` run that cannot dispatch has to replace the review tier with
|
|
166
|
+
something, not quietly ship without one, and the run ends: `status.sh final`
|
|
167
|
+
when the plan's own review clears, `status.sh close` once the work is no longer
|
|
168
|
+
yours to act on, after a local merge, when the branch is left as it stands, or
|
|
169
|
+
when an open PR lands, not when it opens. A run left open reads as live to the
|
|
170
|
+
statusline and blocks the next one. The SessionStart hook prints a live run at every session start,
|
|
171
|
+
compaction and resume included, so a run you did not start is one you were
|
|
172
|
+
shown, not one you have to remember.
|
|
164
173
|
|
|
165
174
|
## Conflicts
|
|
166
175
|
|
|
@@ -253,10 +253,18 @@ declared schedule is wrong the moment one task lands late or comes back with a
|
|
|
253
253
|
blocking finding. A derived one just recomputes, which is the whole reason `ready`
|
|
254
254
|
reads the run state rather than the plan: it sees what has actually landed.
|
|
255
255
|
|
|
256
|
-
- One row per task in `run.json`,
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
256
|
+
- One row per task in `run.json`, and you are the one who flips it; an
|
|
257
|
+
implementer reports, it does not write the run state. `active` at dispatch,
|
|
258
|
+
which records the base as the HEAD of the tree the command is pointed at
|
|
259
|
+
unless you pass `--base`. `review` when the task's commit is in and its
|
|
260
|
+
reviewer has gone out: the paths stay held, because a finding may send the
|
|
261
|
+
implementer back into them. `done --commit <sha> --reviewed` when the review
|
|
262
|
+
clears, or `done --commit <sha>` alone for a tier 0 task, which was owed a
|
|
263
|
+
stat read and no dispatch, and for a task whose dispatch pre-flight declined;
|
|
264
|
+
the debt count counts the second kind and not the first. That state outlives
|
|
265
|
+
compaction; your memory doesn't.
|
|
266
|
+
- Each task goes to a fresh agent carrying the brief below and nothing this
|
|
267
|
+
session accumulated. What you hold is yours to hold, not theirs.
|
|
260
268
|
- **Label the dispatch `T<n>: <task name>`.** The harness lists running agents
|
|
261
269
|
under whatever label the dispatch gave them, so labelled by task that list
|
|
262
270
|
reads as the plan and labelled anything else it reads as a row of anonymous
|
|
@@ -307,6 +315,31 @@ commit only when asked is about that outward-facing act. The plan's sign-off
|
|
|
307
315
|
is the asking. Nothing a task commits reaches anywhere your partner has not
|
|
308
316
|
already agreed to, so the instruction is satisfied rather than excepted.
|
|
309
317
|
|
|
318
|
+
## The dispatch brief
|
|
319
|
+
|
|
320
|
+
An implementer sees its task and nothing else, so anything it has to obey that
|
|
321
|
+
is not in the task text has to be in the brief. The rules it would otherwise
|
|
322
|
+
never meet are the ones this skill spends the most words on, and an agent that
|
|
323
|
+
never loaded the skill follows none of them by default. The brief carries, in
|
|
324
|
+
this order:
|
|
325
|
+
|
|
326
|
+
- **Ground Rules**, verbatim from the plan. They bind every task and are
|
|
327
|
+
repeated in none, so the brief is where they reach the implementer.
|
|
328
|
+
- **The task**, whole: heading, Contract, Touches, Flips, steps.
|
|
329
|
+
- **The implementer contract**, five lines that are the same for every task:
|
|
330
|
+
the test comes first and is watched failing before the code
|
|
331
|
+
(`references/test-first.md`, one paragraph of it, not a pointer the agent
|
|
332
|
+
cannot follow); nothing outside `Touches` is edited; the commit stages only
|
|
333
|
+
the paths in `Touches`, never `git add -A`; the reply reports the commit SHA
|
|
334
|
+
and names any behaviour left untested and why; and `.sluice/` is not written,
|
|
335
|
+
the run state being yours to flip.
|
|
336
|
+
- **The label**, `T<n>: <task name>`, on the dispatch itself.
|
|
337
|
+
|
|
338
|
+
Leave out how you got here. The design, the record, the other tasks and this
|
|
339
|
+
session's reasoning are what dispatch exists to keep out of the implementer's
|
|
340
|
+
context, and a brief that carries them has spent the fresh context it was
|
|
341
|
+
buying.
|
|
342
|
+
|
|
310
343
|
## When dispatch is unavailable
|
|
311
344
|
|
|
312
345
|
Separate two cases first. A session where dispatch is off unless your partner
|
|
@@ -409,10 +442,12 @@ a reviewer writes nothing, so it collides with nothing. The final review is
|
|
|
409
442
|
the only one that waits, because it is the only one that needs everything to
|
|
410
443
|
have landed.
|
|
411
444
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
445
|
+
The base is recorded when the task goes `active`: the flip takes the HEAD of
|
|
446
|
+
the tree it is pointed at, `--dir` if you passed one and the tree you issued it
|
|
447
|
+
from otherwise, so point it at the tree the implementer is cut from, or pass
|
|
448
|
+
`--base <sha>` where that differs. Recovering it afterwards is
|
|
449
|
+
archaeology, and the answer you will guess at is `HEAD~1`, which
|
|
450
|
+
`references/review.md` already names as the standing mistake.
|
|
416
451
|
|
|
417
452
|
**A finding surviving two rounds may be a defect in the criterion, not the
|
|
418
453
|
work.** Before a third round, ask whether any output could satisfy it. A
|
|
@@ -452,6 +487,14 @@ absorb into the next task's brief.
|
|
|
452
487
|
|
|
453
488
|
## The final review
|
|
454
489
|
|
|
490
|
+
It runs once every task is `done` and every per-task review is back, and it
|
|
491
|
+
comes before `references/finish.md` starts: the suite run there is the last
|
|
492
|
+
check before the three options, and a review that lands after it would be
|
|
493
|
+
reviewing a tree the suite never saw. Mark it with `status.sh final` when it
|
|
494
|
+
clears, so `show` and `close` report it done rather than pending; the per-task
|
|
495
|
+
marks cannot stand in for it, because they count dispatches the tier table owed
|
|
496
|
+
and this one is owed by the plan as a whole.
|
|
497
|
+
|
|
455
498
|
It covers cross-task integration and everything the record accumulated, not
|
|
456
499
|
lines a per-task review already cleared. It is a dispatch, on this session's
|
|
457
500
|
model and never a downshifted one, and it gets the whole-plan diff and the
|
|
@@ -5,6 +5,10 @@ in every channel. A commit made in passing during `fast` channel work is
|
|
|
5
5
|
not one of those: the branch as a whole has to be about to leave your
|
|
6
6
|
hands.
|
|
7
7
|
|
|
8
|
+
In `deep`, the final review comes before this file starts, and its verdict is
|
|
9
|
+
in hand or it is not: `references/deep-channel.md` owns it. Nothing here
|
|
10
|
+
dispatches a review; this is where what the reviews found gets said.
|
|
11
|
+
|
|
8
12
|
Start here: run every test the project has, not a sample of them. A red
|
|
9
13
|
result stops the process; there is no menu after a failure. A pass from
|
|
10
14
|
earlier in the session doesn't count: the tree has changed since, and
|
|
@@ -30,6 +34,32 @@ After a local merge, run the whole suite again over the merged tree before
|
|
|
30
34
|
deleting anything. A failure there stops the cleanup; you haven't pushed
|
|
31
35
|
anything yet, so you can still walk it back.
|
|
32
36
|
|
|
37
|
+
## The handback message
|
|
38
|
+
|
|
39
|
+
The parts are owned by three references and read as one message, so this is
|
|
40
|
+
the one list of them. In order:
|
|
41
|
+
|
|
42
|
+
1. The suite's result, as it printed, with the command that produced it.
|
|
43
|
+
2. The ledger, `scripts/run-stats.sh --tests "<that result>"`, pasted
|
|
44
|
+
unedited. `references/meter.md`
|
|
45
|
+
3. In `deep`, `status.sh show`, pasted, so "four of nine, task five blocked,
|
|
46
|
+
two unreviewed, final review pending" is on the page rather than in your
|
|
47
|
+
account of it. `references/status.md`
|
|
48
|
+
4. One clause on review: dispatched and clear, dispatched with findings still
|
|
49
|
+
open, or not dispatched and why. In `deep` that clause covers the per-task
|
|
50
|
+
tiers and the final review separately, because the debt count carries one
|
|
51
|
+
and not the other.
|
|
52
|
+
5. The three options, and nothing after them.
|
|
53
|
+
|
|
54
|
+
A `deep` run closes when the work stops being yours to act on: after a local
|
|
55
|
+
merge and its re-run of the suite, or when your partner leaves the branch where
|
|
56
|
+
it stands. While a PR is open it stays live, because the review comments come
|
|
57
|
+
back as work on those same tasks and the rows are where that work is tracked;
|
|
58
|
+
it closes when the PR lands. `status.sh close` archives the state and prints
|
|
59
|
+
one line saying what it archived, and that line goes in your reply. A run left
|
|
60
|
+
open blocks the next `init` and renders a finished plan in the statusline as a
|
|
61
|
+
live one, which is how a run comes to sit at 0/9 for three weeks.
|
|
62
|
+
|
|
33
63
|
While a PR is open, the workspace survives: it is where the review
|
|
34
64
|
comments get answered, and tearing it down means rebuilding it the moment
|
|
35
65
|
the first one arrives. Discarding needs an explicit, confirmed ask from
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# Meter the run
|
|
2
2
|
|
|
3
3
|
Run `scripts/run-stats.sh` as part of the message that hands the work back,
|
|
4
|
-
and paste its output into that message unedited
|
|
4
|
+
and paste its output into that message unedited; `references/finish.md` says
|
|
5
|
+
where in that message it sits:
|
|
5
6
|
|
|
6
7
|
```
|
|
7
8
|
bash <skill-dir>/scripts/run-stats.sh --tests "<what the suite actually printed>"
|
|
@@ -20,6 +20,7 @@ bash <skill-dir>/scripts/status.sh preflight --review "tier 3 only" --model "6 o
|
|
|
20
20
|
--workspace "one worktree per implementer"
|
|
21
21
|
bash <skill-dir>/scripts/status.sh show
|
|
22
22
|
bash <skill-dir>/scripts/status.sh ready
|
|
23
|
+
bash <skill-dir>/scripts/status.sh final
|
|
23
24
|
bash <skill-dir>/scripts/status.sh line --full
|
|
24
25
|
bash <skill-dir>/scripts/status.sh close
|
|
25
26
|
```
|
|
@@ -30,7 +31,29 @@ run as the tree it was cut from. Statuses
|
|
|
30
31
|
are `todo`, `active`, `review`, `done` and `blocked`. A new id needs `--name`;
|
|
31
32
|
after that every call is a bare flip, so keeping it current costs one command
|
|
32
33
|
per transition rather than a paragraph. `close` archives the run under
|
|
33
|
-
`.sluice/archive
|
|
34
|
+
`.sluice/archive/`, prints one line saying what it archived, progress, review
|
|
35
|
+
debt and whether the final review landed, and frees the tree for the next one.
|
|
36
|
+
|
|
37
|
+
The controller writes every row. An implementer reports its SHA in its reply
|
|
38
|
+
and touches nothing under `.sluice/`; the brief in `references/deep-channel.md`
|
|
39
|
+
says so to it. `active` is the dispatch, `review` is the commit in and a
|
|
40
|
+
reviewer out, with the task's paths still held because a finding may send the
|
|
41
|
+
implementer back into them, and `done` is the end: with `--reviewed` when a
|
|
42
|
+
review cleared it, without when the tier owed one and pre-flight declined it,
|
|
43
|
+
which is the row the debt count counts.
|
|
44
|
+
|
|
45
|
+
A task going `active` with no `--base` takes the HEAD of the tree the command
|
|
46
|
+
is pointed at, `--dir` if given and the current tree otherwise, once; a base
|
|
47
|
+
already on the row is kept. Pass `--base` when the implementer's tree is
|
|
48
|
+
neither.
|
|
49
|
+
|
|
50
|
+
Every write stamps `updated`. Past a day since the last one, `show` and the
|
|
51
|
+
statusline both say how long the run has sat idle, because a finished plan
|
|
52
|
+
whose run was never closed looks exactly like a live one otherwise, and it
|
|
53
|
+
blocks the next `init`.
|
|
54
|
+
|
|
55
|
+
`final` records that the plan's final review cleared. `show` reports it
|
|
56
|
+
pending until then, and `close` says which it was.
|
|
34
57
|
|
|
35
58
|
A command that cannot finish leaves the state exactly as it found it, so a
|
|
36
59
|
failed `task` never costs you the rows already in the file. Two argument rules
|
|
@@ -54,17 +77,23 @@ the plan seeded would be absent from every implementer, `init` there would
|
|
|
54
77
|
start a second run nothing else reads, and the worktree would take that state
|
|
55
78
|
with it when it went.
|
|
56
79
|
|
|
57
|
-
So
|
|
58
|
-
|
|
59
|
-
window, a flip
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
80
|
+
So a tree with no run of its own anchors on the main worktree of its set, and
|
|
81
|
+
the set's run covers it: the statusline renders the controller's run in every
|
|
82
|
+
implementer's window, and a flip issued from any of those trees lands where
|
|
83
|
+
every other one is watching. A tree's own run comes first, though. `init`
|
|
84
|
+
always lands in the tree it is given, and every other command reads that
|
|
85
|
+
tree's state when it has one, so two sessions working independently in two
|
|
86
|
+
worktrees of one repo each keep their own run and neither is shown the
|
|
87
|
+
other's. Anchored unconditionally, as this once was, the first `init` in the
|
|
88
|
+
set took over every other session's statusline and refused every other `init`.
|
|
89
|
+
A submodule anchors on its own checkout, not the superproject's, and a
|
|
90
|
+
directory that is no git work tree keeps its run exactly where it sits.
|
|
63
91
|
|
|
64
92
|
One file for several writers is one file to contend on, so `init`, `task`,
|
|
65
|
-
`preflight` and `close` take a lock first:
|
|
66
|
-
|
|
67
|
-
before the earlier one landed, dropping that row without
|
|
93
|
+
`preflight`, `final` and `close` take a lock first: two flips issued at the
|
|
94
|
+
same moment from different trees would otherwise have the later write built on
|
|
95
|
+
a snapshot taken before the earlier one landed, dropping that row without
|
|
96
|
+
saying so. The lock
|
|
68
97
|
carries its holder's pid, so a killed run is broken through rather than waited
|
|
69
98
|
out. Reads take nothing, state being installed through a rename, which is what
|
|
70
99
|
keeps `line` cheap enough to render on.
|
|
@@ -108,8 +137,9 @@ have gone that way. Those are different claims.
|
|
|
108
137
|
## Reading it back
|
|
109
138
|
|
|
110
139
|
`show` prints the whole run: channel, topic, how many tasks are done, the plan
|
|
111
|
-
and record paths,
|
|
112
|
-
|
|
140
|
+
and record paths, how long it has sat idle once that passes a day, the review
|
|
141
|
+
debt, the final review, the pre-flight answers, and a row per task with its
|
|
142
|
+
base, commit, tier and model. Run it after compaction instead of reconstructing the
|
|
113
143
|
run from what you remember, and run it in the message that hands the work back,
|
|
114
144
|
where "four of nine, task five blocked" is a fact your partner can act on.
|
|
115
145
|
|
|
@@ -184,6 +214,26 @@ establishes is what the tasks after it are checked against.
|
|
|
184
214
|
Derive the wave here rather than writing wave numbers into the plan. A declared
|
|
185
215
|
schedule is wrong the moment one task lands late; this recomputes.
|
|
186
216
|
|
|
217
|
+
What a wave of several means is read off the pre-flight workspace answer: an
|
|
218
|
+
answer containing "per implementer", "per concurrent implementer", "each
|
|
219
|
+
implementer" or "worktree each" prints "a worktree each", any other recorded
|
|
220
|
+
answer prints "serial, one at a time in the shared tree", and no answer prints
|
|
221
|
+
neither. It is a match on words, not a reading of the sentence, so record the
|
|
222
|
+
answer in one of those phrases when worktrees were bought and in none of them
|
|
223
|
+
when they were not.
|
|
224
|
+
|
|
225
|
+
## On session start
|
|
226
|
+
|
|
227
|
+
`scripts/session-start.sh` is the SessionStart hook a global install wires,
|
|
228
|
+
after the routing directive. On startup, resume, clear and compact it runs
|
|
229
|
+
`show` against the tree the session opened in, a subdirectory or a linked
|
|
230
|
+
worktree included since `show` anchors on the main worktree, and prints the
|
|
231
|
+
run when there is one, with one sentence more: after a compaction, resume or
|
|
232
|
+
clear, that the record is to be read before the next dispatch; on a fresh
|
|
233
|
+
start, that a run not being continued was left open and wants `close`. A tree
|
|
234
|
+
with no run prints nothing. The reading-back rule above still stands; this is
|
|
235
|
+
the harness doing it at the one moment memory has just been cut.
|
|
236
|
+
|
|
187
237
|
## Statusline
|
|
188
238
|
|
|
189
239
|
This is the part that makes a run visible without anyone asking. Give it rows of
|
|
@@ -240,6 +290,9 @@ because the mapping from state to colour belongs next to the state. A caller tha
|
|
|
240
290
|
coloured the line itself would have to re-derive each cell's meaning from its
|
|
241
291
|
glyph, which is the same fact stored twice.
|
|
242
292
|
|
|
293
|
+
Past a day since the last write the first row gains `· idle 2d1h`, so a run
|
|
294
|
+
nobody closed reads as one.
|
|
295
|
+
|
|
243
296
|
A run that is only visible to the session running it is a run your partner
|
|
244
297
|
cannot redirect. That is the same argument the channel announcement makes, and
|
|
245
298
|
the statusline is where it holds for the hour after the announcement scrolled
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# SessionStart hook: show a live sluice run to the session that just opened.
|
|
3
|
+
#
|
|
4
|
+
# A deep run outlives the context window that started it. The prose asks the
|
|
5
|
+
# model to run `status.sh show` after a compaction, and that is the one rule with
|
|
6
|
+
# nothing but memory behind it, in the one moment memory has just been cut. So
|
|
7
|
+
# the harness does the asking: on startup, resume, clear and compact this prints
|
|
8
|
+
# the run, and on a tree with no run it prints nothing.
|
|
9
|
+
#
|
|
10
|
+
# Reads the harness's session JSON on stdin for `cwd` and `source`, falling back
|
|
11
|
+
# to $PWD when there is none. Always exits 0: a hook that fails is noise in a
|
|
12
|
+
# session that has not started yet.
|
|
13
|
+
|
|
14
|
+
set -uo pipefail
|
|
15
|
+
|
|
16
|
+
here="$(cd "$(dirname "$0")" && pwd)"
|
|
17
|
+
STATUS="$here/status.sh"
|
|
18
|
+
|
|
19
|
+
# Bounded, and skipped on a terminal: the harness closes stdin after the JSON,
|
|
20
|
+
# but a hook that inherits an open, silent stdin would otherwise never return,
|
|
21
|
+
# and a hook that never returns holds the session start with it. Read a byte at
|
|
22
|
+
# a time: on a timeout bash 3.2, which is what macOS ships, discards whatever
|
|
23
|
+
# the interrupted read had gathered where bash 4 keeps it, and a byte read
|
|
24
|
+
# whole before the timeout is kept by both, trailing newline or not.
|
|
25
|
+
input=""
|
|
26
|
+
if [ ! -t 0 ]; then
|
|
27
|
+
while IFS= read -r -n 1 -t 2 c; do
|
|
28
|
+
if [ -z "$c" ]; then input="$input
|
|
29
|
+
"; else input="$input$c"; fi
|
|
30
|
+
done
|
|
31
|
+
fi
|
|
32
|
+
cwd="$PWD"
|
|
33
|
+
source=""
|
|
34
|
+
if [ -n "$input" ] && command -v jq >/dev/null 2>&1; then
|
|
35
|
+
got="$(printf '%s' "$input" | jq -r '.cwd // empty' 2>/dev/null || true)"
|
|
36
|
+
[ -n "$got" ] && cwd="$got"
|
|
37
|
+
source="$(printf '%s' "$input" | jq -r '.source // empty' 2>/dev/null || true)"
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
# No gate of its own beyond the directory existing: status.sh anchors on the
|
|
41
|
+
# main worktree of whatever tree it is given, which is what lets a session
|
|
42
|
+
# opened in a subdirectory or a linked worktree find the run, and on a tree
|
|
43
|
+
# with no run it exits 2 in silence. Once per session start, that is cheap.
|
|
44
|
+
[ -d "$cwd" ] || exit 0
|
|
45
|
+
[ -f "$STATUS" ] || exit 0
|
|
46
|
+
|
|
47
|
+
shown="$(bash "$STATUS" show --dir "$cwd" 2>/dev/null)" || exit 0
|
|
48
|
+
[ -n "$shown" ] || exit 0
|
|
49
|
+
|
|
50
|
+
echo "A sluice run is live in this tree. Its state, from .sluice/run.json:"
|
|
51
|
+
echo
|
|
52
|
+
echo "$shown"
|
|
53
|
+
echo
|
|
54
|
+
case "$source" in
|
|
55
|
+
compact | resume | clear)
|
|
56
|
+
echo "This session's context was summarised, resumed or cleared. Read the run record before the next dispatch and trust the two files over what you remember."
|
|
57
|
+
;;
|
|
58
|
+
*)
|
|
59
|
+
echo "If this run is not the work you were asked to continue, it was left open: close it with status.sh close before starting another."
|
|
60
|
+
;;
|
|
61
|
+
esac
|
|
62
|
+
exit 0
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
# status.sh preflight [--review <t>] [--model <t>] [--workspace <t>]
|
|
16
16
|
# status.sh show [--json]
|
|
17
17
|
# status.sh ready
|
|
18
|
+
# status.sh final
|
|
18
19
|
# status.sh line [--full]
|
|
19
20
|
# status.sh close
|
|
20
21
|
#
|
|
@@ -92,18 +93,29 @@ if [ -z "$SUB" ]; then
|
|
|
92
93
|
exit 4
|
|
93
94
|
fi
|
|
94
95
|
|
|
95
|
-
# A
|
|
96
|
-
#
|
|
97
|
-
#
|
|
98
|
-
#
|
|
99
|
-
#
|
|
100
|
-
#
|
|
96
|
+
# A tree's own run comes first, and only a tree with none reads the set's. Two
|
|
97
|
+
# layouts share this script and pull opposite ways. A deep run plans in the main
|
|
98
|
+
# tree and cuts implementer worktrees after the plan: the run directory ignores
|
|
99
|
+
# itself, so `git worktree add` never carries it across, and read from the tree
|
|
100
|
+
# it was called in the run would be absent from every implementer. Those trees
|
|
101
|
+
# hold no run of their own, so they anchor on the main worktree, the one path
|
|
102
|
+
# the set agrees on. Independent sessions, one per worktree, each start a run
|
|
103
|
+
# where they sit: anchored unconditionally, the first `init` in the set took
|
|
104
|
+
# over every other session's statusline and refused every other `init`. So
|
|
105
|
+
# `init` always lands in the tree it was given, and every other command reads
|
|
106
|
+
# the tree's own state when it has one.
|
|
101
107
|
#
|
|
102
108
|
# `git worktree list` names the main worktree first. A submodule names its own
|
|
103
109
|
# checkout there rather than the superproject's, which is what keeps a
|
|
104
110
|
# submodule's run beside its own working tree, and a directory that is no git
|
|
105
111
|
# work tree at all is left exactly as it was given.
|
|
106
|
-
|
|
112
|
+
#
|
|
113
|
+
# The tree the command was issued from, kept apart from the anchored one: a
|
|
114
|
+
# base defaulted at dispatch is the HEAD of the tree the implementer is about to
|
|
115
|
+
# be cut from, which in a worktree set is not always the main worktree's.
|
|
116
|
+
ORIG_DIR="$DIR"
|
|
117
|
+
if [ "$SUB" != "init" ] && [ ! -f "$DIR/.sluice/run.json" ] \
|
|
118
|
+
&& [ "$(git -C "$DIR" rev-parse --is-inside-work-tree 2>/dev/null)" = "true" ]; then
|
|
107
119
|
MAIN_TREE="$(git -C "$DIR" worktree list --porcelain 2>/dev/null | sed -n '1s/^worktree //p')"
|
|
108
120
|
if [ -n "${MAIN_TREE:-}" ] && [ -d "$MAIN_TREE" ]; then
|
|
109
121
|
DIR="$MAIN_TREE"
|
|
@@ -200,11 +212,23 @@ if [ "$SUB" = "line" ]; then
|
|
|
200
212
|
else "◷ \($m / 60 | floor)h\($m % 60)m"
|
|
201
213
|
end
|
|
202
214
|
end) as $clock
|
|
215
|
+
# Idle is the time since the last write, and it only shows once it passes
|
|
216
|
+
# a day: a run in progress is written every few minutes, so a day of
|
|
217
|
+
# silence is a run that was forgotten or finished without being closed. A
|
|
218
|
+
# run written before `updated` existed falls back to `started`.
|
|
219
|
+
| ((.updated // .started // "" | try fromdateiso8601 catch 0) as $u
|
|
220
|
+
| if $u == 0 then ""
|
|
221
|
+
else (($now - $u) / 3600 | floor) as $h
|
|
222
|
+
| if $h < 24 then ""
|
|
223
|
+
else "idle \($h / 24 | floor)d\($h % 24)h"
|
|
224
|
+
end
|
|
225
|
+
end) as $idle
|
|
203
226
|
| ( paint("1;96"; "⧗") + " "
|
|
204
227
|
+ ([ paint("1;96"; (.channel // "?")),
|
|
205
228
|
paint("2"; (.topic // ""))
|
|
206
229
|
] | join_parts)
|
|
207
230
|
+ (if $clock == "" then "" else " " + paint("2"; $clock) end)
|
|
231
|
+
+ (if $idle == "" then "" else " " + paint("2"; "·") + " " + paint("33"; $idle) end)
|
|
208
232
|
),
|
|
209
233
|
# The flip is drawn as a rule before its task: everything left of it is
|
|
210
234
|
# inert and safe to leave landed, everything right of it is not. That is
|
|
@@ -260,15 +284,21 @@ mk_dir() { # <directory to create under .sluice>
|
|
|
260
284
|
# stdin, so a jq that died upstream of this feeds it nothing, and installing
|
|
261
285
|
# nothing atomically is still a wipe of the one file in the run that outlives
|
|
262
286
|
# compaction. A command that cannot finish leaves the state as it found it.
|
|
287
|
+
#
|
|
288
|
+
# Every write stamps `updated`, which is what lets a reader tell a run that is
|
|
289
|
+
# moving from one that was left behind: `started` only says how old it is.
|
|
263
290
|
write_state() {
|
|
264
|
-
local tmp="$STATE.tmp.$$"
|
|
291
|
+
local tmp="$STATE.tmp.$$" stamped="$STATE.stamped.$$"
|
|
265
292
|
cat >"$tmp"
|
|
266
|
-
|
|
267
|
-
|
|
293
|
+
# One jq does both the check and the stamp: it fails on malformed input and
|
|
294
|
+
# writes nothing on empty input, and either leaves the candidate unfit.
|
|
295
|
+
if ! jq --arg now "$(date -u +%Y-%m-%dT%H:%M:%SZ)" 'if type != "object" then error("state is not an object") else .updated = $now end' "$tmp" >"$stamped" 2>/dev/null || [ ! -s "$stamped" ]; then
|
|
296
|
+
rm -f "$tmp" "$stamped"
|
|
268
297
|
err "refusing to write $STATE: the update produced no valid state, so the existing state is unchanged"
|
|
269
298
|
exit 1
|
|
270
299
|
fi
|
|
271
|
-
|
|
300
|
+
rm -f "$tmp"
|
|
301
|
+
mv "$stamped" "$STATE" || { rm -f "$stamped"; err "could not replace $STATE"; exit 1; }
|
|
272
302
|
}
|
|
273
303
|
|
|
274
304
|
# One state file now serves a whole worktree set, so two implementers can flip
|
|
@@ -410,6 +440,20 @@ case "$SUB" in
|
|
|
410
440
|
exit 4
|
|
411
441
|
fi
|
|
412
442
|
|
|
443
|
+
# The base is cheap to know at dispatch and archaeology afterwards, and
|
|
444
|
+
# the guess it gets recovered as is HEAD~1. So a task going active with
|
|
445
|
+
# no base takes the HEAD of the tree the command was issued from, once:
|
|
446
|
+
# a base already on the row was a decision and a second flip keeps it.
|
|
447
|
+
if [ "$STATUS" = "active" ] && [ -z "$BASE" ]; then
|
|
448
|
+
has_base="$(jq --argjson id "$ID" '[.tasks[]? | select(.id == $id and .base != null)] | length' "$STATE" 2>/dev/null)"
|
|
449
|
+
case "$has_base" in
|
|
450
|
+
'' | *[!0-9]*) err "could not read the task list from $STATE"; exit 6 ;;
|
|
451
|
+
esac
|
|
452
|
+
if [ "$has_base" = "0" ]; then
|
|
453
|
+
BASE="$(git -C "$ORIG_DIR" rev-parse --short HEAD 2>/dev/null || true)"
|
|
454
|
+
fi
|
|
455
|
+
fi
|
|
456
|
+
|
|
413
457
|
patch="$(jq -n \
|
|
414
458
|
--arg name "$NAME" --arg status "$STATUS" --arg base "$BASE" \
|
|
415
459
|
--arg commit "$COMMIT" --arg tier "$TIER" --arg model "$MODEL" \
|
|
@@ -494,7 +538,7 @@ case "$SUB" in
|
|
|
494
538
|
# Header and rows are laid out from the same widths, so the two cannot
|
|
495
539
|
# drift apart, and an over-long value is clipped with a marker rather
|
|
496
540
|
# than silently reading as the whole value.
|
|
497
|
-
jq -r '
|
|
541
|
+
jq -r --argjson now "$(date -u +%s)" '
|
|
498
542
|
def dash: if . == null or . == "" then "-" else . end;
|
|
499
543
|
def cell($w): tostring
|
|
500
544
|
| if length > $w then .[0:$w - 1] + "…"
|
|
@@ -505,11 +549,24 @@ case "$SUB" in
|
|
|
505
549
|
$c[6]] | join(" "));
|
|
506
550
|
([.tasks[]? | select(.status == "done")] | length) as $done
|
|
507
551
|
| ["sluice \(.channel) · \(.topic) · \($done)/\(.tasks | length) done"]
|
|
508
|
-
+ ["plan
|
|
509
|
-
+ ["record
|
|
552
|
+
+ ["plan \(.plan | dash)"]
|
|
553
|
+
+ ["record \(.record | dash)"]
|
|
554
|
+
# Past a day since the last write the run is idle, and that is said
|
|
555
|
+
# here because a stale run blocks the next init and nothing else
|
|
556
|
+
# would name it.
|
|
557
|
+
# A run written before `updated` existed falls back to `started`,
|
|
558
|
+
# which is the case the line was added for.
|
|
559
|
+
+ ((.updated // .started // "" | try fromdateiso8601 catch 0) as $u
|
|
560
|
+
| if $u == 0 then []
|
|
561
|
+
else (($now - $u) / 3600 | floor) as $h
|
|
562
|
+
| if $h < 24 then []
|
|
563
|
+
else ["idle \($h / 24 | floor)d\($h % 24)h since the last write"]
|
|
564
|
+
end
|
|
565
|
+
end)
|
|
510
566
|
+ (([.tasks[]? | select(.status == "done" and (.tier // 0) >= 1 and (.reviewed // false) == false)] | length) as $debt
|
|
511
|
-
| if $debt == 0 then [] else ["unreviewed
|
|
512
|
-
+ ["
|
|
567
|
+
| if $debt == 0 then [] else ["unreviewed \($debt) done, owed a review the tier table promised"] end)
|
|
568
|
+
+ ["final review " + (if .final_review then "done" else "pending" end)]
|
|
569
|
+
+ ["pre-flight " + (
|
|
513
570
|
if (.preflight // {} | length) == 0 then "not recorded"
|
|
514
571
|
else [(.preflight | to_entries[] | "\(.key)=\(.value)")] | join("; ")
|
|
515
572
|
end)]
|
|
@@ -558,9 +615,18 @@ case "$SUB" in
|
|
|
558
615
|
"no contract graph in the run state.",
|
|
559
616
|
"re-run `plan.sh import <plan>` to record Needs, Offers and Touches."
|
|
560
617
|
else
|
|
618
|
+
# What a wave of several means depends on what pre-flight bought:
|
|
619
|
+
# worktrees per implementer run it at once, anything else runs it
|
|
620
|
+
# one at a time, and an answer never recorded says neither.
|
|
561
621
|
(
|
|
562
622
|
"\($ready | length) ready now"
|
|
563
|
-
+ (if ($ready | length) > 1 then
|
|
623
|
+
+ (if ($ready | length) > 1 then
|
|
624
|
+
(.preflight.workspace // "") as $ws
|
|
625
|
+
| if $ws == "" then ""
|
|
626
|
+
elif ($ws | test("per (concurrent )?implementer|each implementer|worktree each"; "i")) then " · a worktree each"
|
|
627
|
+
else " · serial, one at a time in the shared tree"
|
|
628
|
+
end
|
|
629
|
+
else "" end)
|
|
564
630
|
),
|
|
565
631
|
($ready[] | " T\(.id) \(.name // "" | pad(38))\((.touches // []) | join(", "))"),
|
|
566
632
|
# A shared path is what rules two ready tasks out of the same wave,
|
|
@@ -586,11 +652,36 @@ case "$SUB" in
|
|
|
586
652
|
' "$STATE"
|
|
587
653
|
;;
|
|
588
654
|
|
|
655
|
+
final)
|
|
656
|
+
[ $# -eq 0 ] || { err "final takes no arguments"; exit 4; }
|
|
657
|
+
require_run
|
|
658
|
+
take_lock
|
|
659
|
+
require_readable
|
|
660
|
+
|
|
661
|
+
# The per-task marks count dispatches the tier table owed. The final
|
|
662
|
+
# review is owed by the plan as a whole, so it is a fact about the run
|
|
663
|
+
# rather than a row, and `show` reports it pending until this lands.
|
|
664
|
+
jq --arg now "$(date -u +%Y-%m-%dT%H:%M:%SZ)" '.final_review = $now' "$STATE" | write_state
|
|
665
|
+
;;
|
|
666
|
+
|
|
589
667
|
close)
|
|
590
668
|
[ $# -eq 0 ] || { err "close takes no arguments"; exit 4; }
|
|
591
669
|
require_run
|
|
592
670
|
take_lock
|
|
593
671
|
|
|
672
|
+
# Said once, as the run leaves: a close that archives six unreviewed
|
|
673
|
+
# tasks and no final review in silence lets those facts leave with it.
|
|
674
|
+
# Best effort on state nothing else will parse, which is why it is not
|
|
675
|
+
# allowed to stop the archive.
|
|
676
|
+
summary="$(jq -r '
|
|
677
|
+
([.tasks[]? | select(.status == "done")] | length) as $done
|
|
678
|
+
| ([.tasks[]? | select(.status == "done" and (.tier // 0) >= 1 and (.reviewed // false) == false)] | length) as $debt
|
|
679
|
+
| [ "closed \(.topic // "run"): \($done)/\(.tasks | length) done",
|
|
680
|
+
(if $debt > 0 then "\($debt) unreviewed" else empty end),
|
|
681
|
+
"final review \(if .final_review then "done" else "pending" end)"
|
|
682
|
+
] | join(" · ")
|
|
683
|
+
' "$STATE" 2>/dev/null || true)"
|
|
684
|
+
|
|
594
685
|
# Deliberately not `require_readable`. The parse error every other
|
|
595
686
|
# subcommand raises names close as the way out, so close is the one
|
|
596
687
|
# command that has to accept state nothing else will touch: it moves the
|
|
@@ -612,7 +703,11 @@ case "$SUB" in
|
|
|
612
703
|
dest="$ARCHIVE/$stamp-$slug-$n.json"
|
|
613
704
|
n=$((n + 1))
|
|
614
705
|
done
|
|
615
|
-
mv "$STATE" "$dest"
|
|
706
|
+
mv "$STATE" "$dest" || { err "could not archive $STATE"; exit 1; }
|
|
707
|
+
# The summary needs parseable state and close is the one command that
|
|
708
|
+
# does not, so an unreadable run still gets a line naming where it went.
|
|
709
|
+
[ -n "$summary" ] || summary="closed $(basename "$dest"): state was unreadable, no summary"
|
|
710
|
+
echo "$summary"
|
|
616
711
|
;;
|
|
617
712
|
|
|
618
713
|
*)
|
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. 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
|
|
3
|
+
"version": "0.16.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, one status command and a SessionStart hook can answer where the run is (the hook prints a live run at every session start, compaction included), and closes each run with a ledger read out of the session transcript: elapsed, tools, tokens, and what each dispatched agent cost where the transcript recorded it. Claude Code only; stands down where the superpowers pipeline governs the repo.",
|
|
5
5
|
"author": "iceinvein",
|
|
6
6
|
"type": "prompt",
|
|
7
7
|
"tools": [
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"global"
|
|
29
29
|
],
|
|
30
30
|
"default": "global",
|
|
31
|
-
"claudeHookDirective": "Before acting on a request that changes code, pick a sluice channel and state which one."
|
|
31
|
+
"claudeHookDirective": "Before acting on a request that changes code, pick a sluice channel and state which one.",
|
|
32
|
+
"claudeHookScript": "scripts/session-start.sh"
|
|
32
33
|
}
|
|
33
34
|
}
|