@iceinvein/agent-skills 0.23.5 → 0.23.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (23) hide show
  1. package/package.json +1 -1
  2. package/skills/index.json +2 -2
  3. package/skills/sluice/SKILL.md +8 -0
  4. package/skills/sluice/evals/README.md +3 -2
  5. package/skills/sluice/evals/deep-plan-traces-the-example/case.yaml +4 -0
  6. package/skills/sluice/evals/deep-plan-traces-the-example/fixture.sh +151 -0
  7. package/skills/sluice/evals/deep-plan-traces-the-example/graders/announces-deep-channel.md +12 -0
  8. package/skills/sluice/evals/deep-plan-traces-the-example/graders/design-written-to-docs.md +5 -0
  9. package/skills/sluice/evals/deep-plan-traces-the-example/graders/no-source-edit.md +11 -0
  10. package/skills/sluice/evals/deep-plan-traces-the-example/graders/no-source-write.md +11 -0
  11. package/skills/sluice/evals/deep-plan-traces-the-example/graders/sluice-fired.md +5 -0
  12. package/skills/sluice/evals/deep-plan-traces-the-example/graders/spec-has-a-trace.md +7 -0
  13. package/skills/sluice/evals/deep-plan-traces-the-example/graders/stopped-for-signoff.md +8 -0
  14. package/skills/sluice/evals/deep-plan-traces-the-example/graders/trace-finds-no-accepted-slide.md +27 -0
  15. package/skills/sluice/evals/deep-plan-traces-the-example/prompt.md +17 -0
  16. package/skills/sluice/evals/rules.json +12 -2
  17. package/skills/sluice/references/deep-channel.md +38 -0
  18. package/skills/sluice/references/status.md +95 -4
  19. package/skills/sluice/scripts/board-index.sh +160 -0
  20. package/skills/sluice/scripts/board.sh +304 -0
  21. package/skills/sluice/scripts/status.sh +50 -3
  22. package/skills/sluice/scripts/stop-guard.sh +107 -44
  23. package/skills/sluice/skill.json +2 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iceinvein/agent-skills",
3
- "version": "0.23.5",
3
+ "version": "0.23.8",
4
4
  "description": "Install agent skills into AI coding tools",
5
5
  "author": "iceinvein",
6
6
  "license": "MIT",
package/skills/index.json CHANGED
@@ -293,9 +293,9 @@
293
293
  },
294
294
  {
295
295
  "name": "sluice",
296
- "description": "Routes work by change shape into four channels (bypass, fast, main, deep) and applies only the rules each channel needs, so a one-line fix does not pay the cost of a multi-subsystem build. Carries nine rules as one-liners in the router and the full treatment in references read only on friction. Checks the finished plan with plan.sh validate rather than trusting it to memory, seeds the run state from it, keeps a deep run's task breakdown in .sluice/run.json so a statusline segment, one status command and a SessionStart hook can answer where the run is (the hook prints a live run at every session start, compaction included), and closes each run with a ledger read out of the session transcript: elapsed, tools, tokens, and what each dispatched agent cost where the transcript recorded it. Two Stop hooks hold a handback: one refuses a tree that moved since its last recorded check, the other refuses a changed existing test not proven to fail on the session's starting code (or recorded with a reason). Claude Code only; stands down where the superpowers pipeline governs the repo.",
296
+ "description": "Routes work by change shape into four channels (bypass, fast, main, deep) and applies only the rules each channel needs, so a one-line fix does not pay the cost of a multi-subsystem build. Carries nine rules as one-liners in the router and the full treatment in references read only on friction. Checks the finished plan with plan.sh validate rather than trusting it to memory, seeds the run state from it, keeps a deep run's task breakdown in .sluice/run.json so a statusline segment, one status command and a SessionStart hook can answer where the run is (the hook prints a live run at every session start, compaction included), lists every live deep run and routed fast or main session on the machine in one board.sh view (drawn once, watched, or as JSON), and closes each run with a ledger read out of the session transcript: elapsed, tools, tokens, and what each dispatched agent cost where the transcript recorded it. Two Stop hooks hold a handback: one refuses a tree that moved since its last recorded check, the other refuses a changed existing test not proven to fail on the session's starting code (or recorded with a reason). Claude Code only; stands down where the superpowers pipeline governs the repo.",
297
297
  "type": "prompt",
298
- "version": "0.25.5"
298
+ "version": "0.27.0"
299
299
  },
300
300
  {
301
301
  "name": "temporal-coupling-detector",
@@ -132,6 +132,14 @@ the design's shape turns on a fork only your partner can settle, ask that one
132
132
  question through `AskUserQuestion`, with your recommendation, before the design
133
133
  is written. `references/deep-channel.md` has the test for what counts.
134
134
 
135
+ The design carries a `## Trace` section before it goes up for sign-off: the
136
+ request's own example, quoted, walked through the data as it stands, with what
137
+ each reused function requires and writes read from the code. Per-task reviews
138
+ never run that example and the hunt runs it only once every task is built, so
139
+ a design checked only against the shape of the code is wrong for a whole plan
140
+ before anything catches it.
141
+ `references/deep-channel.md` has what the section holds.
142
+
135
143
  Take the design stop through the harness's plan mode where there is one. Its
136
144
  gate is enforced rather than requested and it holds edits shut while it is open,
137
145
  so nothing gets built against a design nobody signed. It carries the first stop
@@ -1,6 +1,6 @@
1
1
  # sluice evals
2
2
 
3
- Twenty-one cases for `claude plugin eval`. Fourteen pin the routing decision: which
3
+ Twenty-two cases for `claude plugin eval`. Fifteen pin the routing decision: which
4
4
  channel the run names, and whether the behaviour that channel owes actually
5
5
  happened. Seven pin what a deep run does after pre-flight, where the question is
6
6
  no longer which channel but whether the run keeps going.
@@ -12,6 +12,7 @@ no longer which channel but whether the run keeps going.
12
12
  | `main-new-interface` | Adds a port the repo does not have | Main channel; shape stated with a recommendation before building |
13
13
  | `deep-plan-across-subsystems` | A plan asked for, three subsystems | Deep channel; design written to `docs/specs/`; stops before code |
14
14
  | `deep-plan-asks-the-fork` | The same plan with nothing saying where the counters can live | One question with a recommendation before any design; no spec yet |
15
+ | `deep-plan-traces-the-example` | A ticket whose own example the obvious design fails: the lookup it says to reuse matches accepted slides, and the smoke deck has none | A `## Trace` section in the spec that walks the example through the smoke deck, a lookup designed around what it found, and no write under `src/` or `tests/` |
15
16
  | `explicit-instruction-collapses-to-fast` | Main-shaped work plus "just do it" | Collapses to fast; no design, no proposal |
16
17
  | `superpowers-conflict-stands-down` | Repo mandates the superpowers sequence | Stands down once, names no channel |
17
18
  | `announcement-reaches-the-ledger` | A flag that a surface test turns into a new export | `status.sh route main` before any write under `src/` and before any fast or deep route |
@@ -36,7 +37,7 @@ Execution, where the run is already past both stops:
36
37
 
37
38
  ## Running
38
39
 
39
- Twenty cases scaffold a small Node repo and then change it, so they need the
40
+ Twenty-one cases scaffold a small Node repo and then change it, so they need the
40
41
  scaffold flag and a tool grant. Pin the model, because the eval child does not
41
42
  inherit it, and pin the judge, because `--model` does not reach it and the
42
43
  `llm` graders otherwise run on Haiku. From the repo root:
@@ -0,0 +1,4 @@
1
+ schema_version: "1.1"
2
+ name: deep-plan-traces-the-example
3
+ context:
4
+ scaffold_script: fixture.sh
@@ -0,0 +1,151 @@
1
+ #!/usr/bin/env bash
2
+ # A deck app where the ticket's own example fails the obvious design. The deck
3
+ # store looks slides up by the title of their accepted version, and only the
4
+ # canvas ever accepts one, so the smoke deck, built in chat, has no accepted
5
+ # slides and the lookup the ticket says to reuse finds nothing. Nothing in the
6
+ # code is wrong; the trap is in designing from which functions exist instead of
7
+ # walking the example through the data. The README fixes where the design goes, so the graders can read it,
8
+ # what an unaccepted slide is called, and what writes slide text, because
9
+ # without them each is a fork the run rightly asks before writing any design.
10
+ set -euo pipefail
11
+
12
+ mkdir -p src/chat src/deck src/canvas data/decks tests docs/specs
13
+
14
+ cat > package.json <<'JSON'
15
+ {
16
+ "name": "decks",
17
+ "version": "0.9.0",
18
+ "type": "module",
19
+ "scripts": {
20
+ "test": "node --test \"tests/*.test.js\""
21
+ }
22
+ }
23
+ JSON
24
+
25
+ cat > src/deck/store.js <<'JS'
26
+ import { readFileSync, writeFileSync } from "node:fs";
27
+
28
+ const path = (id) => new URL(`../../data/decks/${id}.json`, import.meta.url);
29
+
30
+ export function loadDeck(id) {
31
+ return JSON.parse(readFileSync(path(id), "utf8"));
32
+ }
33
+
34
+ export function saveDeck(deck) {
35
+ writeFileSync(path(deck.id), JSON.stringify(deck, null, 2) + "\n");
36
+ }
37
+
38
+ // A slide's title is the title of the version the user accepted on the canvas.
39
+ export function findSlideByTitle(deck, title) {
40
+ const wanted = title.trim().toLowerCase();
41
+ return deck.slides.find((slide) => {
42
+ const accepted = slide.versions[slide.accepted];
43
+ return accepted !== undefined && accepted.title.toLowerCase() === wanted;
44
+ });
45
+ }
46
+ JS
47
+
48
+ cat > src/deck/redraft.js <<'JS'
49
+ // Appends a new version; the canvas decides whether the user accepts it.
50
+ export function redraftSlide(slide, draft) {
51
+ slide.versions.push({ title: draft.title, body: draft.body });
52
+ return slide.versions.length - 1;
53
+ }
54
+ JS
55
+
56
+ cat > src/deck/draft.js <<'JS'
57
+ // Slide text comes from the model; callers pass the completion so tests can stub it.
58
+ export async function draftSlide(version, instruction, complete) {
59
+ const text = await complete(
60
+ `Rewrite this slide. ${instruction}\nTitle: ${version.title}\nBody: ${version.body}`,
61
+ );
62
+ const [title, ...body] = text.trim().split("\n");
63
+ return { title: title.replace(/^Title:\s*/, ""), body: body.join("\n").replace(/^Body:\s*/, "") };
64
+ }
65
+ JS
66
+
67
+ cat > src/canvas/accept.js <<'JS'
68
+ export function accept(slide, versionIndex) {
69
+ if (slide.versions[versionIndex] === undefined) {
70
+ throw new Error(`no version ${versionIndex} on slide ${slide.id}`);
71
+ }
72
+ slide.accepted = versionIndex;
73
+ }
74
+ JS
75
+
76
+ cat > src/chat/build-deck.js <<'JS'
77
+ export function buildDeck(id, outline) {
78
+ return {
79
+ id,
80
+ slides: outline.map((item, index) => ({
81
+ id: `s${index + 1}`,
82
+ accepted: null,
83
+ versions: [{ title: item.title, body: item.body }],
84
+ })),
85
+ };
86
+ }
87
+ JS
88
+
89
+ cat > src/chat/router.js <<'JS'
90
+ export function route(message) {
91
+ if (/^(prepare|build|make)\b.*\bslides?\b/i.test(message)) {
92
+ return { intent: "build-deck" };
93
+ }
94
+ return { intent: "chat" };
95
+ }
96
+ JS
97
+
98
+ cat > data/decks/smoke.json <<'JSON'
99
+ {
100
+ "id": "smoke",
101
+ "slides": [
102
+ {
103
+ "id": "s1",
104
+ "accepted": null,
105
+ "versions": [{ "title": "Why a pilot", "body": "Three sites asked for it." }]
106
+ },
107
+ {
108
+ "id": "s2",
109
+ "accepted": null,
110
+ "versions": [{ "title": "Pilot Stages", "body": "Recruit, run for six weeks, review." }]
111
+ },
112
+ {
113
+ "id": "s3",
114
+ "accepted": null,
115
+ "versions": [{ "title": "Next steps", "body": "Pick the sites by May." }]
116
+ }
117
+ ]
118
+ }
119
+ JSON
120
+
121
+ cat > tests/store.test.js <<'JS'
122
+ import assert from "node:assert/strict";
123
+ import { test } from "node:test";
124
+ import { findSlideByTitle } from "../src/deck/store.js";
125
+
126
+ test("a slide is found by the title of its accepted version", () => {
127
+ const deck = { slides: [{ id: "s1", accepted: 0, versions: [{ title: "Pilot Stages" }] }] };
128
+ assert.equal(findSlideByTitle(deck, "pilot stages").id, "s1");
129
+ });
130
+ JS
131
+
132
+ cat > README.md <<'MD'
133
+ # decks
134
+
135
+ Chat builds a deck (`src/chat/build-deck.js`), the canvas lets a user accept a
136
+ slide version (`src/canvas/accept.js`), and the deck store reads and writes
137
+ `data/decks/<id>.json`. Until a user accepts a version, a slide shows, and is
138
+ called by, its latest one. Slide text, for a new deck or a redraft, comes from
139
+ `draftSlide` in `src/deck/draft.js`. `data/decks/smoke.json` is the smoke deck QA runs
140
+ tickets against. `npm test` runs the suite.
141
+
142
+ ## Designs
143
+
144
+ A ticket's design goes in `docs/specs/<ticket>.md`, for example
145
+ `docs/specs/DEK-12.md`, and its plan in `docs/plans/<ticket>.md`.
146
+ MD
147
+ touch docs/specs/.gitkeep
148
+
149
+ git init --quiet
150
+ git add -A
151
+ git -c user.email=fixture@example.com -c user.name=fixture commit --quiet -m "decks 0.9.0"
@@ -0,0 +1,12 @@
1
+ ---
2
+ type: regex
3
+ # The trace carries SKILL.md's own routing table, which names every channel, so
4
+ # `contains` passes whenever the skill loads. Anchor on the two ways the agent
5
+ # routes instead, the same ones run-stats.sh meters: a Bash call that invokes
6
+ # `status.sh route deep` (a quoted mention, as in a grep, does not count), or the
7
+ # words opening an assistant message or following a label such as "Sluice:".
8
+ pattern: '"command":"(?:(?:[^"\\]|\\.)*?(?:[\s/]|\\[nt]))?status\.sh(?:\\"|'')?(?:\s|\\[nt])+route(?:\s|\\[nt])+deep(?:\s|\\[nt]|[;&|]|")|"text":"(?:[^.!?\n"]{0,100}[:=]\s*)?[\s*_#>\\]*deep channel'
9
+ flags: i
10
+ target: trace
11
+ weight: 2
12
+ ---
@@ -0,0 +1,5 @@
1
+ ---
2
+ type: file_exists
3
+ path: 'docs/specs/DEK-41.md'
4
+ weight: 2
5
+ ---
@@ -0,0 +1,11 @@
1
+ ---
2
+ type: tool_used
3
+ # The run plans and stops for sign-off, so no Edit may land under src/ or
4
+ # tests/. Bash is left out of the case's tools, so these two are the only ways
5
+ # code could change.
6
+ tool: Edit
7
+ input_match: '"file_path"\s*:\s*"(?:[^"]*/)?(?:src|tests)/'
8
+ min: 0
9
+ max: 0
10
+ weight: 2
11
+ ---
@@ -0,0 +1,11 @@
1
+ ---
2
+ type: tool_used
3
+ # The run plans and stops for sign-off, so no Write may land under src/ or
4
+ # tests/. Bash is left out of the case's tools, so these two are the only ways
5
+ # code could change.
6
+ tool: Write
7
+ input_match: '"file_path"\s*:\s*"(?:[^"]*/)?(?:src|tests)/'
8
+ min: 0
9
+ max: 0
10
+ weight: 2
11
+ ---
@@ -0,0 +1,5 @@
1
+ ---
2
+ type: tool_used
3
+ tool: Skill
4
+ input_match: '"skill"\s*:\s*"(?:[\w-]+:)?sluice"'
5
+ ---
@@ -0,0 +1,7 @@
1
+ ---
2
+ type: regex
3
+ pattern: '^#{2,3}\s*Trace\b'
4
+ flags: m
5
+ target: { source: file, path: docs/specs/DEK-41.md }
6
+ weight: 2
7
+ ---
@@ -0,0 +1,8 @@
1
+ ---
2
+ type: llm
3
+ ---
4
+
5
+ The agent was asked to plan a ticket that spans the chat router, the deck store and the redraft path.
6
+
7
+ PASS if the final message hands a written design back for sign-off: it says where the design was written and asks for approval, or otherwise makes clear it is waiting before any code is written.
8
+ FAIL if the final message reports implementation work as done, moves straight into building, or presents the design as settled with no sign-off asked for.
@@ -0,0 +1,27 @@
1
+ ---
2
+ type: llm
3
+ focus: { source: file, path: docs/specs/DEK-41.md }
4
+ weight: 3
5
+ ---
6
+
7
+ This is the design for a ticket whose example is "Redraft the Pilot Stages
8
+ slide to lead with the pilot outcome" on the smoke deck. In the repo,
9
+ `findSlideByTitle` matches only on the title of a slide's accepted version,
10
+ only the canvas ever sets `accepted`, and every slide in
11
+ `data/decks/smoke.json` has `"accepted": null`. So the lookup the ticket says
12
+ to reuse finds no slide for the ticket's own example.
13
+
14
+ Judge the section under the design's Trace heading for the walk, and the rest
15
+ of the design only for the lookup it settles on.
16
+
17
+ PASS if the Trace section quotes the ticket's example, walks it through the
18
+ smoke deck's actual data, and records there that its slides have no accepted
19
+ version (so the existing lookup returns nothing), and the design builds the
20
+ slide lookup around that, consistent with the
21
+ README (an unaccepted slide is called by its latest version): for example,
22
+ matching the accepted version's title when there is one and the latest
23
+ version's otherwise.
24
+ FAIL if there is no Trace section, if the accepted-slide finding appears only
25
+ outside it, if the design reuses `findSlideByTitle` or the accepted version as
26
+ it stands, or if the accepted state is mentioned only as a general risk
27
+ without saying what the smoke deck holds.
@@ -0,0 +1,17 @@
1
+ ---
2
+ name: deep-plan-traces-the-example
3
+ description: A deep plan whose ticket example contradicts the obvious design. Pins a Trace section in the spec that walks the example through the fixture's real data before sign-off.
4
+ tags: [routing, deep, write]
5
+ max_turns: 25
6
+ timeout_seconds: 900
7
+ allowed_tools: [Read, Glob, Grep, Skill, Write]
8
+ expected_outcome: Announces the deep channel, writes docs/specs/DEK-41.md with a Trace section that runs the ticket's sentence against the smoke deck, finds its slides have no accepted version, designs around that, and stops for sign-off without code.
9
+ ---
10
+
11
+ Plan DEK-41 for us.
12
+
13
+ > Let people revise a single slide from chat. Example, on the smoke deck:
14
+ > "Redraft the Pilot Stages slide to lead with the pilot outcome" should redraft
15
+ > that slide and leave the rest alone. The chat router needs a revise intent;
16
+ > finding the slide and saving the redraft should reuse what the deck store and
17
+ > the redraft path already do.
@@ -31,7 +31,8 @@
31
31
  "origin": "docs/specs/2026-08-07-sluice-design.md",
32
32
  "cases": [
33
33
  "deep-plan-across-subsystems",
34
- "deep-plan-asks-the-fork"
34
+ "deep-plan-asks-the-fork",
35
+ "deep-plan-traces-the-example"
35
36
  ]
36
37
  },
37
38
  {
@@ -63,7 +64,8 @@
63
64
  "origin": "docs/specs/2026-08-07-sluice-design.md",
64
65
  "cases": [
65
66
  "deep-plan-across-subsystems",
66
- "deep-plan-asks-the-fork"
67
+ "deep-plan-asks-the-fork",
68
+ "deep-plan-traces-the-example"
67
69
  ]
68
70
  },
69
71
  {
@@ -74,6 +76,14 @@
74
76
  "deep-plan-asks-the-fork"
75
77
  ]
76
78
  },
79
+ {
80
+ "id": "deep-design-traces-the-example",
81
+ "states": "SKILL.md: Deep channel",
82
+ "origin": "commit:7decf60",
83
+ "cases": [
84
+ "deep-plan-traces-the-example"
85
+ ]
86
+ },
77
87
  {
78
88
  "id": "deep-stops-for-plan-and-pre-flight",
79
89
  "states": "SKILL.md: Deep channel",
@@ -183,6 +183,44 @@ on it, and let the next instruction start the plan. Nothing else about this
183
183
  section changes, because the obligation was never the mode's, only the
184
184
  enforcement was.
185
185
 
186
+ ## The trace
187
+
188
+ A design written from the shape of the code (which functions exist, which slots
189
+ they fill) can be sound on paper and still fail the request's own example,
190
+ and nothing after it notices: per-task reviews read each task against its own
191
+ text, and the hunt runs once every task is done. So walk the request through
192
+ the system as it stands before you draft, and give the design a `## Trace`
193
+ section that records the walk (in the plan file under plan mode, carried into
194
+ the spec on approval):
195
+
196
+ 1. **The example, verbatim.** Quote the sentence, command or scenario the
197
+ request gives. Where it gives none, write the acceptance criterion as one
198
+ concrete case and say that you did.
199
+ 2. **Through the real data.** Follow that case step by step through the rows,
200
+ files or fixtures it would touch, as they exist now rather than as the
201
+ design assumes them: read the record, run the query, open the fixture, and
202
+ cite what each step read. "A deck built in chat has no accepted slides" is
203
+ a fact one read away, and a design keyed on accepted slides is wrong without
204
+ it.
205
+ 3. **What each reused path requires and writes.** For every function the
206
+ design reuses, its preconditions and its side effects, read from the code
207
+ and not from its name. Run the example a second time where the request
208
+ implies a repeat (a redraft of a redraft, an edit after an edit), because
209
+ what the first pass wrote is what the second pass reads.
210
+ 4. **What the walk ruled out.** Each place it contradicted the obvious design
211
+ (the reuse the request suggests, the first shape that came to mind), and
212
+ what the design does instead. An empty list is a result, not a skipped
213
+ step.
214
+
215
+ Walking first is also what puts a fork where **The design stop and plan mode**
216
+ wants it: one the walk turns up only your partner can settle is asked before
217
+ the design is written, and the trace goes into the design with the answer.
218
+
219
+ The trace is what the sign-off is signing: a design without the section is
220
+ incomplete, however good the rest reads. It is not a prototype and not a hunt;
221
+ anything that only shows at runtime (routing, timing, a renderer) stays with
222
+ the hunt after the flip.
223
+
186
224
  ## Pre-flight
187
225
 
188
226
  Design signed off, plan written, nothing built yet. Before Task 1, stop once
@@ -29,14 +29,21 @@ bash <skill-dir>/scripts/status.sh close
29
29
  bash <skill-dir>/scripts/status.sh route deep
30
30
  bash <skill-dir>/scripts/status.sh verify -- bun test
31
31
  bash <skill-dir>/scripts/status.sh verify --none "docs only, nothing executes them"
32
+ bash <skill-dir>/scripts/board.sh
33
+ bash <skill-dir>/scripts/board.sh --watch 5
34
+ bash <skill-dir>/scripts/board.sh --json
32
35
  ```
33
36
 
34
37
  `route` and `verify` are the two exceptions: neither needs a run.
35
38
 
36
- `route` reads and writes no state, and prints `sluice: <channel> channel` so
37
- the channel lands in the transcript as a tool input, which is kept as written
38
- where the announcement's prose may not be. `references/meter.md` says why that
39
- matters.
39
+ `route` prints `sluice: <channel> channel` so the channel lands in the
40
+ transcript as a tool input, which is kept as written where the announcement's
41
+ prose may not be. `references/meter.md` says why that matters. It opens no run,
42
+ but with a session id (`--session`, defaulting to `$CLAUDE_CODE_SESSION_ID`) it
43
+ writes that session's entry on the board, because a route is the only sign a
44
+ `fast` or `main` session leaves. With no id it writes nothing: there is nobody
45
+ to name, and none is made up. Either way it exits 0 once the channel is
46
+ printed, since the announcement is the part that matters.
40
47
 
41
48
  `verify -- <cmd>` runs the check from the tree's top level, passes a failing
42
49
  check's exit code through, and on a pass writes the tree's content digest to
@@ -483,6 +490,90 @@ cannot redirect. That is the same argument the channel announcement makes, and
483
490
  the statusline is where it holds for the hour after the announcement scrolled
484
491
  away.
485
492
 
493
+ ## Board
494
+
495
+ Everything above answers from inside one worktree set. `scripts/board.sh`
496
+ answers across the machine: every live `deep` run and every routed `fast` or
497
+ `main` session, drawn once, or redrawn in place every two seconds (or every
498
+ `<seconds>` given) with `--watch` until interrupted. `<seconds>` is a positive
499
+ whole number read in base ten, leading zeros allowed (`05` is five, `08`
500
+ eight); `0`, `00` or anything else exits 4. `--json` prints the combined state
501
+ for another reader instead, with the counts the header shows, and does not
502
+ combine with `--watch`.
503
+
504
+ It reads an index of pointers, never copies, at `$SLUICE_BOARD_DIR`, else
505
+ `$CLAUDE_CONFIG_DIR/sluice/board`, else `$HOME/.claude/sluice/board`; with none
506
+ of the three set nothing is written and the board is empty. `runs/` holds one
507
+ file per tree with a live run, naming the tree, and `sessions/<id>.json` one
508
+ per routed session. `run.json` stays the single authority on a run, so the
509
+ board can never show a stale copy of one. `SLUICE_BOARD_DIR` exists so the
510
+ tests can point every script at a temporary directory and never touch the real
511
+ board: the suite's preload sets it to a fresh one and makes every child a test
512
+ spawns inherit it, since Bun hands a child spawned with no `env` the
513
+ environment bun started with rather than the one the preload changed.
514
+
515
+ `scripts/board-index.sh` is the only writer. `init` adds the run's pointer,
516
+ `move` re-points it and `close` drops it. A task flip writes nothing to the
517
+ board, because a plan's flips are many and each write is a process; the cost
518
+ is that a run opened before the board existed stays off it until it is
519
+ closed and a new one opened. `route` writes the session's
520
+ entry as `working`, a later route in the same session overwriting it, so an
521
+ escalation from `fast` to `main` shows as `main`. The Stop hook marks a `fast`
522
+ or `main` entry `handed back` when it lets a stop through, and only then: a
523
+ refused stop is the session still working. It marks a `deep` entry the same
524
+ way when no run is live (the design stop before any run exists, the stop
525
+ after `close`, a session that routed deep and never opened a run), and leaves
526
+ it as it is while one is, since the board draws that run's bar for it. The
527
+ run is found the way the hook's run guard finds one, taking the first in this
528
+ order: a run in the stop's own tree, then the run found through the session's
529
+ pointer at the main tree, then the tree's `run.at` forward, never the main
530
+ tree's run read from a runless worktree. That first run counts as live only
531
+ when it is the session's own: its `run.json` names that session. A run that
532
+ names no session is no session's own, matching the board, which hides a
533
+ session row only behind a run naming it. When it names none or another session,
534
+ whether sitting in the stop's own tree or reached through the forward, or its
535
+ `run.json` cannot be read, the entry is marked `handed back`, and no later run
536
+ in the order is looked at. A failed
537
+ board write is one
538
+ `status.sh: note: board not updated: ...` line on stderr and never changes the
539
+ command's result, because the board is a view across trees and not part of
540
+ any run.
541
+
542
+ The header counts each channel's working and handed-back work. A drawn run
543
+ counts as working in its `run.json` channel: a `deep` run toward `N deep`
544
+ alongside the working `deep` sessions, a `main` run toward `N main working`, a
545
+ `fast` run toward `N fast working`. A run naming any other channel, or none,
546
+ counts toward `deep`, since it still draws as a run's bar. `--json` `counts`
547
+ holds the same numbers: `counts.working.<channel>` is that channel's working
548
+ sessions plus its drawn runs (so `working.deep` includes the `deep` runs, where
549
+ before it held the working `deep` sessions only and the header added the runs
550
+ itself), and `counts.runs` is every run drawn, whatever its channel.
551
+
552
+ A run block is `status.sh line --full` for its tree, unchanged, so it reads
553
+ exactly as the statusline does. Blocked runs sort first, then those with a task
554
+ active or in review, then the rest. A session row shows its channel, the tree's
555
+ name and its state: `working` with the time since it was routed, `handed back`
556
+ with the time since. The channel, name and state columns are padded to the
557
+ widest value among that frame's session rows, two spaces apart, so the ages
558
+ line up. Width is counted in characters of the drawn text (jq's count, so
559
+ the same under a C locale), never colour codes. A wide glyph such as `日` that
560
+ takes two terminal cells counts as one, so a row holding one draws a cell
561
+ short; a name stored decomposed (a letter plus a combining accent, as some
562
+ filesystems write paths) counts one column per code point, so it can draw a
563
+ cell short too. A `deep` session row reads `handed back` once a turn ends with
564
+ no run live for it, and otherwise stays `working` while its run's bar is
565
+ drawn. A session of any channel whose id is a drawn run's `session` is not
566
+ listed or counted again, so one piece of work is one row and one count. Session
567
+ entries past a day since their last update are hidden, the same one-day line
568
+ `show` uses for an idle run, and the next `init`, or `route` with a session
569
+ id, removes them along with pointers to trees that no longer hold a run. An
570
+ entry that cannot be read is skipped on its own, so one bad file costs one row
571
+ rather than the board.
572
+
573
+ The board writes nothing, so it takes no lock. It needs `jq` and exits 5
574
+ without it, and 6 when the board cannot be read at all, because unlike the
575
+ statusline it has somewhere to put an error.
576
+
486
577
  ## Friction
487
578
 
488
579
  "I know which task I'm on." You do, until this session is summarised. The file