@iceinvein/agent-skills 0.2.0 → 0.3.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/README.md CHANGED
@@ -103,7 +103,7 @@ Skills that shape how work gets done rather than analyzing code.
103
103
 
104
104
  | Skill | What it does |
105
105
  |-------|--------------|
106
- | **sluice** | 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. Six rules live as one-liners in the router; the full treatment sits in references read only on friction. Claude Code only. Conflicts with the superpowers plugin, which requires its own fixed pipeline up front for anything that adds to or changes what the software does, not just code edits; disable superpowers before installing. |
106
+ | **sluice** | 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. Seven rules live as one-liners in the router; the full treatment sits in references read only on friction. Each run closes with a ledger read out of the session transcript: elapsed, tools, tokens, and what every dispatched agent cost. Claude Code only. Conflicts with the superpowers plugin, which requires its own fixed pipeline up front for anything that adds to or changes what the software does, not just code edits; disable superpowers before installing. |
107
107
 
108
108
  ### Orchestration
109
109
 
package/dist/cli/index.js CHANGED
@@ -402,7 +402,7 @@ function resolveBundlePaths(entries, bundle) {
402
402
  }
403
403
 
404
404
  // src/cli/adapters/claude.ts
405
- import { chmodSync, existsSync as existsSync2, mkdirSync, rmSync, statSync, unlinkSync } from "node:fs";
405
+ import { chmodSync, existsSync as existsSync2, mkdirSync, rmSync, rmdirSync, statSync, unlinkSync } from "node:fs";
406
406
  import { dirname, join as join2 } from "node:path";
407
407
  function shouldBeExecutable(relPath, content) {
408
408
  if (relPath.endsWith(".sh"))
@@ -569,7 +569,7 @@ var claudeAdapter = {
569
569
  const stopAt = config.bundleRoot ? join2(cwd, config.bundleRoot, "..") : cwd;
570
570
  while (dir !== stopAt && dir !== "/" && dir.startsWith(cwd)) {
571
571
  try {
572
- rmSync(dir, { recursive: false });
572
+ rmdirSync(dir);
573
573
  } catch {
574
574
  break;
575
575
  }
@@ -598,7 +598,7 @@ var claudeAdapter = {
598
598
  };
599
599
 
600
600
  // src/cli/adapters/cursor.ts
601
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, unlinkSync as unlinkSync2, rmSync as rmSync2 } from "node:fs";
601
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, unlinkSync as unlinkSync2, rmdirSync as rmdirSync2 } from "node:fs";
602
602
  import { dirname as dirname2, join as join3 } from "node:path";
603
603
  var cursorAdapter = {
604
604
  name: "cursor",
@@ -651,7 +651,7 @@ var cursorAdapter = {
651
651
  if (existsSync3(fullPath)) {
652
652
  unlinkSync2(fullPath);
653
653
  try {
654
- rmSync2(dirname2(fullPath), { recursive: false });
654
+ rmdirSync2(dirname2(fullPath));
655
655
  } catch {}
656
656
  }
657
657
  }
@@ -728,7 +728,7 @@ var codexAdapter = {
728
728
  };
729
729
 
730
730
  // src/cli/adapters/gemini.ts
731
- import { existsSync as existsSync5, mkdirSync as mkdirSync3, unlinkSync as unlinkSync3, rmSync as rmSync3 } from "node:fs";
731
+ import { existsSync as existsSync5, mkdirSync as mkdirSync3, unlinkSync as unlinkSync3, rmdirSync as rmdirSync3 } from "node:fs";
732
732
  import { dirname as dirname3, join as join5 } from "node:path";
733
733
  var geminiAdapter = {
734
734
  name: "gemini",
@@ -781,7 +781,7 @@ var geminiAdapter = {
781
781
  if (existsSync5(fullPath)) {
782
782
  unlinkSync3(fullPath);
783
783
  try {
784
- rmSync3(dirname3(fullPath), { recursive: false });
784
+ rmdirSync3(dirname3(fullPath));
785
785
  } catch {}
786
786
  }
787
787
  }
@@ -868,7 +868,7 @@ async function installSkill(cwd, manifest, files, targetTools, activation) {
868
868
  }
869
869
 
870
870
  // src/cli/commands/remove.ts
871
- import { existsSync as existsSync6, statSync as statSync2, unlinkSync as unlinkSync4, rmSync as rmSync4 } from "node:fs";
871
+ import { existsSync as existsSync6, statSync as statSync2, unlinkSync as unlinkSync4, rmSync as rmSync2 } from "node:fs";
872
872
  import { basename, join as join7 } from "node:path";
873
873
 
874
874
  // src/cli/github.ts
@@ -992,7 +992,7 @@ function isSharedConfigFile(relPath) {
992
992
  function removeFileOrDir(fullPath) {
993
993
  const stat = statSync2(fullPath);
994
994
  if (stat.isDirectory()) {
995
- rmSync4(fullPath, { recursive: true, force: true });
995
+ rmSync2(fullPath, { recursive: true, force: true });
996
996
  } else {
997
997
  unlinkSync4(fullPath);
998
998
  }
@@ -1015,6 +1015,8 @@ async function removeSkill(cwd, skillName) {
1015
1015
  return true;
1016
1016
  if (config.supporting && Object.values(config.supporting).includes(f))
1017
1017
  return true;
1018
+ if (config.bundleRoot && f.startsWith(`${config.bundleRoot}/`) && !isSharedConfigFile(f))
1019
+ return true;
1018
1020
  if (config.mcpServers && (f.includes(".claude/settings") || f.includes(".cursor/mcp") || f.includes(".gemini/settings")))
1019
1021
  return true;
1020
1022
  return false;
@@ -1064,7 +1066,7 @@ async function infoSkill(skillName) {
1064
1066
  }
1065
1067
 
1066
1068
  // src/cli/commands/remove.ts
1067
- import { existsSync as existsSync7, statSync as statSync3, unlinkSync as unlinkSync5, rmSync as rmSync5 } from "node:fs";
1069
+ import { existsSync as existsSync7, statSync as statSync3, unlinkSync as unlinkSync5, rmSync as rmSync3 } from "node:fs";
1068
1070
  import { basename as basename2, join as join8 } from "node:path";
1069
1071
  var SHARED_CONFIG_FILES2 = new Set([
1070
1072
  ".claude/settings.json",
@@ -1083,7 +1085,7 @@ function isSharedConfigFile2(relPath) {
1083
1085
  function removeFileOrDir2(fullPath) {
1084
1086
  const stat = statSync3(fullPath);
1085
1087
  if (stat.isDirectory()) {
1086
- rmSync5(fullPath, { recursive: true, force: true });
1088
+ rmSync3(fullPath, { recursive: true, force: true });
1087
1089
  } else {
1088
1090
  unlinkSync5(fullPath);
1089
1091
  }
@@ -1106,6 +1108,8 @@ async function removeSkill2(cwd, skillName) {
1106
1108
  return true;
1107
1109
  if (config.supporting && Object.values(config.supporting).includes(f))
1108
1110
  return true;
1111
+ if (config.bundleRoot && f.startsWith(`${config.bundleRoot}/`) && !isSharedConfigFile2(f))
1112
+ return true;
1109
1113
  if (config.mcpServers && (f.includes(".claude/settings") || f.includes(".cursor/mcp") || f.includes(".gemini/settings")))
1110
1114
  return true;
1111
1115
  return false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iceinvein/agent-skills",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Install agent skills into AI coding tools",
5
5
  "author": "iceinvein",
6
6
  "license": "MIT",
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 six rules as one-liners in the router and the full treatment in references read only on friction. Claude Code only; conflicts with the superpowers plugin.",
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 every dispatched agent cost. Claude Code only; conflicts with the superpowers plugin.",
285
285
  "type": "prompt",
286
- "version": "1.0.0"
286
+ "version": "0.1.1"
287
287
  },
288
288
  {
289
289
  "name": "temporal-coupling-detector",
@@ -21,17 +21,20 @@ announcement is how your partner redirects you without being asked.
21
21
  | `deep` | Several subsystems, or a plan was asked for | + written design and plan | "Deep channel, several subsystems. Design before code." |
22
22
 
23
23
  `bypass`, `fast`, and `main` proceed without stopping for approval; only
24
- `deep` stops, for design sign-off before code.
24
+ `deep` stops, for design sign-off before code, and again for the plan when
25
+ agents rather than you will carry it out.
25
26
 
26
27
  Name the channel and the signal that actually routed you there. The strings above
27
28
  are examples, not fixed copy, and a channel with a two-part signal should say
28
29
  which part applied. `bypass` says nothing at all, because a question that gets
29
30
  announced stops being a question.
30
31
 
31
- **`root-cause` and `finish` are not channel-assigned.** The code misbehaving
32
- triggers the first: a bug report, a red test, behaviour you cannot account
33
- for. An integration event, merging, pushing, or opening a PR, triggers the
34
- second. Both fire in every channel, `bypass` included.
32
+ **`root-cause`, `finish` and `meter` are not channel-assigned.** The code
33
+ misbehaving triggers the first: a bug report, a red test, behaviour you cannot
34
+ account for. An integration event, merging, pushing, or opening a PR, triggers
35
+ the second. Handing the work back triggers the third, whether or not it ever
36
+ reaches an integration event. The first two fire in every channel, `bypass`
37
+ included; `meter` cannot, because `bypass` announces nothing to measure from.
35
38
 
36
39
  ## The rules
37
40
 
@@ -56,6 +59,10 @@ yourself wanting to skip the rule, or arguing that this one is the exception.
56
59
  yours. `references/review.md`
57
60
  - **Finish** deliberately. Green suite first, then let your partner pick
58
61
  merge, PR, or leave it. Never pick for them. `references/finish.md`
62
+ - **Meter the run** as you hand it back. `scripts/run-stats.sh` reads the
63
+ ledger out of the session transcript: elapsed, tools, tokens, and what each
64
+ dispatched agent cost. A summary you assemble from memory is the one that
65
+ flatters you. `references/meter.md`
59
66
 
60
67
  ## Changing channel
61
68
 
@@ -70,9 +77,15 @@ Design to `docs/specs/YYYY-MM-DD-<topic>.md`, plan to
70
77
  `docs/plans/YYYY-MM-DD-<topic>.md`, unless the repo has a convention or
71
78
  your partner states a preference. Get the design signed off before code.
72
79
 
80
+ The plan needs sign-off as well whenever agents will execute it, because each
81
+ one sees only its own task and so nobody ever reads the plan whole. Executing
82
+ it yourself makes it a worklist instead: write it and carry on.
83
+
73
84
  Then read `references/deep-channel.md` for the plan format, the dispatch rules,
74
- and when a task actually needs a reviewer. Two that catch people out: never run
75
- two implementers at once, and review is tiered, not automatic.
85
+ and when a task actually needs a reviewer. Three that catch people out: never
86
+ run two implementers at once, review is tiered rather than automatic, and a
87
+ `deep` run that cannot dispatch has to replace the review tier with something,
88
+ not quietly ship without one.
76
89
 
77
90
  ## Conflicts
78
91
 
@@ -50,6 +50,26 @@
50
50
  - Match model to task: cheap for mechanical work, stronger for judgment
51
51
  and final review.
52
52
 
53
+ ## When dispatch is unavailable
54
+
55
+ A session may forbid subagents outright. `deep` then loses most of the
56
+ machinery this file describes, and the place to say so is the routing
57
+ announcement, where your partner can still act on it, not the summary at the
58
+ end where it reads as an excuse.
59
+
60
+ Three things change. The plan becomes your own worklist and needs no sign-off,
61
+ since you are the only one who will ever read it. Task isolation is gone, so
62
+ `TaskCreate` now carries all of the state that outlives compaction and matters
63
+ more, not less. And fresh context is unavailable, which was the entire thing
64
+ review was buying.
65
+
66
+ One thing does not change: the work still owes a review. Reading your own diff
67
+ is not one, and the table below still names which tasks needed the stronger
68
+ tier. List those tasks when you hand back, as review outstanding rather than
69
+ review passed. A `deep` run that ships with nobody having read it has become a
70
+ `fast` run with a design document attached, and your partner is entitled to
71
+ know that before they decide anything.
72
+
53
73
  ## Review policy
54
74
 
55
75
  A review costs about what the implementation cost, so reviewing every task
@@ -14,8 +14,10 @@ Confirm the base branch instead of assuming it; untangling a wrong merge
14
14
  costs far more than asking would have.
15
15
 
16
16
  With the suite green and the base confirmed, put exactly three options to
17
- your partner: merge it locally, push it and open a PR, or leave the branch
18
- as it stands. Those three exhaust what you may offer. Your partner may still
17
+ your partner, with the run ledger above them so the choice is made against
18
+ what the work actually cost rather than against your account of it;
19
+ `references/meter.md` owns that. The three: merge it locally, push it and
20
+ open a PR, or leave the branch as it stands. Those three exhaust what you may offer. Your partner may still
19
21
  ask for something off the list, most notably scrapping the branch, but the
20
22
  request has to originate with them and be unmistakable. Then stop. Which of
21
23
  the three it is belongs to them, however obvious the choice looks from where
@@ -0,0 +1,38 @@
1
+ # Meter the run
2
+
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:
5
+
6
+ ```
7
+ bash <skill-dir>/scripts/run-stats.sh --tests "<what the suite actually printed>"
8
+ ```
9
+
10
+ It reads the session transcript, so it needs no state of its own and cannot
11
+ drift from what happened. `--tests` is the one field it cannot know: only you
12
+ watched the suite run. Fill it from the output you read in this session, and
13
+ leave it off entirely if no suite ran, which prints "not reported" rather
14
+ than a number nobody checked. `--base <ref>` overrides the diff base when the
15
+ merge-base with `main` or `master` is not where this branch actually began.
16
+
17
+ The run starts at your channel announcement, not at the session, so three
18
+ unrelated tasks in one session report as three runs. A previous ledger closes
19
+ the run before it. `bypass` announces nothing, so there is nothing to meter
20
+ and the script exits quietly.
21
+
22
+ What the ledger is for: the cost of a channel is otherwise invisible, and a
23
+ channel nobody can price is a channel nobody can choose between. Six agents
24
+ and four hours of agent wall-clock on a two-file change is the sort of thing
25
+ that only becomes obvious once it is written down next to the diff. Expect
26
+ some runs to make the case for a shallower channel next time. That is the
27
+ ledger working, not the ledger complaining.
28
+
29
+ Read it before you paste it. A row showing an agent that errored, or an agent
30
+ whose token count dwarfs every other row, is a finding about the run and
31
+ belongs in your prose, not left for your partner to spot in a table.
32
+
33
+ Exit 2 means there was no run to report. That is a fact about the work, not a
34
+ failure: do not synthesise a ledger to fill the gap.
35
+
36
+ The friction line: "I know roughly what this cost." Roughly is the problem.
37
+ Every number here is already on disk, and the remembered version is reliably
38
+ the flattering one.
@@ -0,0 +1,236 @@
1
+ #!/usr/bin/env bash
2
+ # Print the run ledger: what this piece of work actually cost.
3
+ #
4
+ # The run starts at the channel announcement, not at the session, so a session
5
+ # holding three unrelated tasks reports each one separately. A previous ledger
6
+ # ends the run before it. `bypass` announces nothing and so prints nothing.
7
+ #
8
+ # Usage: run-stats.sh [--tests "<what the suite reported>"] [--base <git-ref>]
9
+ # [--transcript <path>]
10
+ #
11
+ # Exit 0 printed a ledger, 1 could not read a transcript, 2 no run to report.
12
+
13
+ set -uo pipefail
14
+
15
+ TESTS=""
16
+ BASE=""
17
+ TRANSCRIPT=""
18
+
19
+ while [ $# -gt 0 ]; do
20
+ case "$1" in
21
+ --tests) TESTS="${2-}"; shift 2 ;;
22
+ --base) BASE="${2-}"; shift 2 ;;
23
+ --transcript) TRANSCRIPT="${2-}"; shift 2 ;;
24
+ -h|--help) sed -n '2,11p' "$0" | cut -c3-; exit 0 ;;
25
+ *) echo "run-stats: unknown argument '$1'" >&2; exit 1 ;;
26
+ esac
27
+ done
28
+
29
+ if ! command -v jq >/dev/null 2>&1; then
30
+ echo "run-stats: jq is required and was not found on PATH" >&2
31
+ exit 1
32
+ fi
33
+
34
+ if [ -z "$TRANSCRIPT" ]; then
35
+ sid="${CLAUDE_CODE_SESSION_ID:-}"
36
+ if [ -z "$sid" ]; then
37
+ echo "run-stats: no --transcript given and CLAUDE_CODE_SESSION_ID is unset" >&2
38
+ exit 1
39
+ fi
40
+ # The session id is unique across projects, so glob rather than guess the slug.
41
+ TRANSCRIPT="$(ls "$HOME"/.claude/projects/*/"$sid".jsonl 2>/dev/null | head -1)"
42
+ fi
43
+
44
+ if [ -z "$TRANSCRIPT" ] || [ ! -f "$TRANSCRIPT" ]; then
45
+ echo "run-stats: transcript not found (looked for '${TRANSCRIPT:-<unresolved>}')" >&2
46
+ exit 1
47
+ fi
48
+
49
+ SUMMARY="$(jq -s '
50
+ # ---- what counts as a real turn ----------------------------------------
51
+ # Transcript stamps carry milliseconds, which fromdateiso8601 will not take.
52
+ def ts: sub("\\.[0-9]+Z$"; "Z") | fromdateiso8601;
53
+ def is_meta: (.isMeta == true) or (.isSidechain == true);
54
+ def texts: [ .message.content[]? | select(.type == "text") | .text ] | join("\n");
55
+ # The channel is stated in one line before proceeding, so the announcement
56
+ # opens the message. Anchoring there is what separates it from prose that
57
+ # merely discusses a channel, which a session about sluice is full of.
58
+ def marker: "^[*_#>[:space:]]*(?<c>fast|main|deep)[[:space:]]+channel";
59
+ def announces: (.type == "assistant") and (is_meta | not)
60
+ and (texts | test(marker; "i"));
61
+ def chan: texts | capture(marker; "i") | .c | ascii_downcase;
62
+ def is_stats_call: (.type == "assistant") and ([
63
+ .message.content[]? | select(.type == "tool_use" and .name == "Bash")
64
+ | select((.input.command // "") | test("run-stats\\.sh"))
65
+ ] | length > 0);
66
+ # Waiting is any turn the partner had to take: a prompt, or an answer to a
67
+ # question you put to them. Leaving the latter out understates the wait on
68
+ # exactly the runs that stopped to ask, which are the ones worth pricing.
69
+ def is_prompt: (.type == "user") and (is_meta | not)
70
+ and (((.message.content | type) == "string"
71
+ or ((.message.content | type) == "array"
72
+ and ([ .message.content[]? | select(.type == "tool_result") ] | length == 0)))
73
+ or ((.toolUseResult | type) == "object" and (.toolUseResult | has("answers"))));
74
+
75
+ . as $all
76
+ | [ range(0; $all | length) ] as $ix
77
+ | [ $ix[] | select($all[.] | announces) ] as $ann
78
+ | if ($ann | length) == 0 then { empty: true } else
79
+
80
+ # A previous ledger closes the run before it. The call running right now has
81
+ # no announcement after it, so it is skipped rather than closing this run.
82
+ ([ $ix[] | select($all[.] | is_stats_call) ]
83
+ | map(select(. < ($ann | max)))
84
+ | if length == 0 then -1 else max end) as $boundary
85
+ | [ $ann[] | select(. > $boundary) ] as $ann
86
+ | ($ann | min) as $from
87
+ | [ $all[$from:][] | select(.timestamp) ] as $run
88
+
89
+ # ---- channel, and the trail if it escalated ----------------------------
90
+ | ([ $ann[] | $all[.] | chan ]
91
+ | reduce .[] as $c ([]; if (. | last) == $c then . else . + [$c] end)) as $trail
92
+
93
+ # ---- clocks ------------------------------------------------------------
94
+ | ($run | map(.timestamp) | min) as $start
95
+ | ($run | map(.timestamp) | max) as $end
96
+ | (($end | ts) - ($start | ts)) as $elapsed
97
+ | ([ range(1; $run | length)
98
+ | select($run[.] | is_prompt)
99
+ | ($run[.].timestamp | ts) - ($run[. - 1].timestamp | ts) ]
100
+ | add // 0) as $waiting
101
+
102
+ # ---- tools and tokens, main loop only ----------------------------------
103
+ | [ $run[] | select((.type == "assistant") and (is_meta | not))
104
+ | .message.content[]? | select(.type == "tool_use") | .name ] as $tools
105
+ | ([ $run[] | select((.type == "assistant") and (is_meta | not))
106
+ | .message.usage.output_tokens // 0 ] | add // 0) as $out_tok
107
+ | ([ $run[] | select((.type == "assistant") and (is_meta | not))
108
+ | .message.usage.cache_read_input_tokens // 0 ] | add // 0) as $cache_tok
109
+
110
+ # ---- dispatched agents -------------------------------------------------
111
+ | ([ $run[] | select(.type == "assistant") | .message.content[]?
112
+ | select(.type == "tool_use" and (.name == "Agent" or .name == "Task"))
113
+ | { key: .id, value: (.input.description // .input.subagent_type // "agent") } ]
114
+ | from_entries) as $labels
115
+ # toolUseResult is whatever the tool returned: object, array or string.
116
+ | [ $run[] | select((.toolUseResult | type) == "object") | select(.toolUseResult.agentId)
117
+ | { label: ($labels[[ .message.content[]? | select(.type == "tool_result") | .tool_use_id ][0]] // "agent"),
118
+ model: (.toolUseResult.resolvedModel // "?"),
119
+ status: (.toolUseResult.status // "?"),
120
+ tokens: (.toolUseResult.totalTokens // 0),
121
+ ms: (.toolUseResult.totalDurationMs // 0),
122
+ tools: (.toolUseResult.totalToolUseCount // 0) } ] as $agents
123
+
124
+ | { empty: false,
125
+ trail: ($trail | join(" → ")),
126
+ elapsed: $elapsed, waiting: $waiting,
127
+ tool_total: ($tools | length),
128
+ tool_top: ([ $tools | group_by(.)[] | { n: length, name: (.[0] | ascii_downcase | split("__") | last) } ]
129
+ | sort_by(-.n) | .[0:3]),
130
+ out_tok: $out_tok, cache_tok: $cache_tok,
131
+ agents: $agents,
132
+ agent_tokens: ([ $agents[].tokens ] | add // 0),
133
+ agent_ms: ([ $agents[].ms ] | add // 0) }
134
+ end
135
+ ' "$TRANSCRIPT" 2>/dev/null)"
136
+
137
+ if [ -z "$SUMMARY" ]; then
138
+ echo "run-stats: could not parse '$TRANSCRIPT'" >&2
139
+ exit 1
140
+ fi
141
+
142
+ if [ "$(jq -r '.empty' <<<"$SUMMARY")" = "true" ]; then
143
+ exit 2
144
+ fi
145
+
146
+ # ---- formatting -------------------------------------------------------------
147
+
148
+ dur() { # seconds -> 1h05m | 3m11s | 42s
149
+ local s=${1%.*} h m
150
+ h=$((s / 3600)); m=$(((s % 3600) / 60)); s=$((s % 60))
151
+ if [ "$h" -gt 0 ]; then printf '%dh%02dm' "$h" "$m"
152
+ elif [ "$m" -gt 0 ]; then printf '%dm%ds' "$m" "$s"
153
+ else printf '%ds' "$s"; fi
154
+ }
155
+
156
+ tok() { # tokens -> 1.2M | 313k | 51.0k | 350
157
+ local n=$1
158
+ if [ "$n" -ge 1000000 ]; then awk -v n="$n" 'BEGIN{printf "%.1fM", n/1000000}'
159
+ elif [ "$n" -ge 100000 ]; then awk -v n="$n" 'BEGIN{printf "%dk", n/1000}'
160
+ elif [ "$n" -ge 1000 ]; then awk -v n="$n" 'BEGIN{printf "%.1fk", n/1000}'
161
+ else printf '%d' "$n"; fi
162
+ }
163
+
164
+ g() { jq -r "$1" <<<"$SUMMARY"; }
165
+
166
+ printf '─── run %s\n' "$(printf '─%.0s' $(seq 1 36))"
167
+ printf 'channel %s\n' "$(g '.trail')"
168
+
169
+ elapsed=$(g '.elapsed'); waiting=$(g '.waiting')
170
+ if [ "${waiting%.*}" -gt 0 ]; then
171
+ printf 'elapsed %s · %s of that waiting on you\n' "$(dur "$elapsed")" "$(dur "$waiting")"
172
+ else
173
+ printf 'elapsed %s\n' "$(dur "$elapsed")"
174
+ fi
175
+
176
+ top="$(g '[.tool_top[] | "\(.n) \(.name)"] | join(" · ")')"
177
+ if [ -n "$top" ]; then
178
+ printf 'tools %s · %s\n' "$(g '.tool_total')" "$top"
179
+ else
180
+ printf 'tools %s\n' "$(g '.tool_total')"
181
+ fi
182
+
183
+ printf 'tokens %s out · %s cache read\n' "$(tok "$(g '.out_tok')")" "$(tok "$(g '.cache_tok')")"
184
+ printf 'tests %s\n' "${TESTS:-not reported}"
185
+
186
+ # The diff is the branch as a whole against where it left the base, working
187
+ # tree included, because uncommitted work is still work this run produced.
188
+ if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
189
+ if [ -z "$BASE" ]; then
190
+ for cand in main master; do
191
+ if git rev-parse --verify --quiet "$cand" >/dev/null 2>&1; then
192
+ BASE="$(git merge-base HEAD "$cand" 2>/dev/null)" && break
193
+ fi
194
+ done
195
+ [ -z "$BASE" ] && BASE="HEAD"
196
+ fi
197
+ stat="$(git diff --shortstat "$BASE" 2>/dev/null | sed 's/^ *//')"
198
+ files="$(git diff --name-only "$BASE" 2>/dev/null | wc -l | tr -d ' ')"
199
+ # awk, not sed: BSD sed has no \+ and silently matches nothing.
200
+ ins="$(awk '{for(i=1;i<=NF;i++) if($i ~ /^insertion/) print $(i-1)}' <<<"$stat")"
201
+ del="$(awk '{for(i=1;i<=NF;i++) if($i ~ /^deletion/) print $(i-1)}' <<<"$stat")"
202
+ ins=${ins:-0}; del=${del:-0}
203
+
204
+ # A file the run created is not in the diff until it is staged, and staging
205
+ # it here would be a side effect on someone else's index. Count it instead.
206
+ while IFS= read -r f; do
207
+ [ -n "$f" ] || continue
208
+ files=$((files + 1))
209
+ ins=$((ins + $(wc -l <"$f" 2>/dev/null || echo 0)))
210
+ done < <(git ls-files --others --exclude-standard 2>/dev/null)
211
+
212
+ [ "$files" -gt 0 ] && printf 'diff +%s −%s across %s files\n' "$ins" "$del" "$files"
213
+ fi
214
+
215
+ count=$(g '.agents | length')
216
+ if [ "$count" -eq 0 ]; then
217
+ printf 'agents none dispatched\n'
218
+ else
219
+ printf 'agents %s dispatched · %s tok · %s wall\n' \
220
+ "$count" "$(tok "$(g '.agent_tokens')")" "$(dur "$(( $(g '.agent_ms') / 1000 ))")"
221
+
222
+ # Up to a dozen rows read as the narrative of the plan. Past that the order
223
+ # stops helping, so show what the run actually spent on and say what is cut.
224
+ if [ "$count" -le 12 ]; then rows='.agents[]'; else rows='(.agents | sort_by(-.tokens) | .[0:10][])'; fi
225
+ while IFS=$'\t' read -r label model status tokens ms tools; do
226
+ model="${model#claude-}"; model="$(sed 's/-[0-9]\{8\}$//' <<<"$model")"
227
+ [ "$status" = "completed" ] && status="" || status=" ($status)"
228
+ printf ' %-22.22s %-10s %6s %7s %s tools%s\n' \
229
+ "$label" "$model" "$(tok "$tokens")" "$(dur "$((ms / 1000))")" "$tools" "$status"
230
+ done < <(g "$rows | [.label, .model, .status, .tokens, .ms, .tools] | @tsv")
231
+
232
+ if [ "$count" -gt 12 ]; then
233
+ printf ' +%s more · %s tok (dearest 10 shown)\n' \
234
+ "$((count - 10))" "$(tok "$(g '(.agents | sort_by(-.tokens) | .[10:] | map(.tokens) | add) // 0')")"
235
+ fi
236
+ fi
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sluice",
3
- "version": "1.0.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 six rules as one-liners in the router and the full treatment in references read only on friction. Claude Code only; conflicts with the superpowers plugin.",
3
+ "version": "0.1.1",
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 every dispatched agent cost. Claude Code only; conflicts with the superpowers plugin.",
5
5
  "author": "iceinvein",
6
6
  "type": "prompt",
7
7
  "tools": [
@@ -12,7 +12,8 @@
12
12
  },
13
13
  "bundle": {
14
14
  "include": [
15
- "references"
15
+ "references",
16
+ "scripts"
16
17
  ]
17
18
  },
18
19
  "install": {