@iceinvein/agent-skills 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iceinvein/agent-skills",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "Install agent skills into AI coding tools",
5
5
  "author": "iceinvein",
6
6
  "license": "MIT",
package/skills/index.json CHANGED
@@ -221,7 +221,7 @@
221
221
  "name": "magpie",
222
222
  "description": "Interactive PR review pipeline. Runs five parallel specialist subagents (security, bugs, performance, code-smells, architecture), dedupes findings, applies a critic rubric, peer-reviews via codex exec (falling back to a Claude second opinion when codex is unavailable), and serves an interactive HTML report for selecting findings to post via gh. Bundles a Bun CLI installed onto PATH via the skill's postinstall step. Use when the user asks to review a GitHub pull request. Splits oversized diffs into budgeted shards and rebuilds the diff from the local clone when gh pr diff refuses it.",
223
223
  "type": "prompt",
224
- "version": "0.10.0"
224
+ "version": "0.10.1"
225
225
  },
226
226
  {
227
227
  "name": "migrate",
@@ -281,9 +281,9 @@
281
281
  },
282
282
  {
283
283
  "name": "sluice",
284
- "description": "Routes work by change shape into four channels (bypass, fast, main, deep) and applies only the rules each channel needs, so a one-line fix does not pay the cost of a multi-subsystem build. Carries seven rules as one-liners in the router and the full treatment in references read only on friction, and closes each run with a ledger read out of the session transcript: elapsed, tools, tokens, and what every dispatched agent cost. 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 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.3.0"
286
+ "version": "0.5.0"
287
287
  },
288
288
  {
289
289
  "name": "temporal-coupling-detector",
@@ -105,7 +105,11 @@ test('killServer escalates to SIGKILL when SIGTERM is ignored', async () => {
105
105
  expect(result.outcome).toBe('sigkill')
106
106
  expect(result.pid).toBe(child.pid)
107
107
  await child.exited
108
- })
108
+ // Everything asserted here is bounded: killServer polls to a 200ms deadline
109
+ // and then signals. The clock this test can actually run out of is subprocess
110
+ // startup, seven git spawns in beforeEach plus a cold `node`, which on a
111
+ // shared runner has overrun the 5s default and failed a release.
112
+ }, 30_000)
109
113
 
110
114
  test('runCleanup logs kill outcome to log.jsonl', async () => {
111
115
  await writeFile(join(runDir, 'log.jsonl'), '')
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "magpie",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
4
4
  "description": "Interactive PR review pipeline. Runs five parallel specialist subagents (security, bugs, performance, code-smells, architecture), dedupes findings, applies a critic rubric, peer-reviews via codex exec (falling back to a Claude second opinion when codex is unavailable), and serves an interactive HTML report for selecting findings to post via gh. Bundles a Bun CLI installed onto PATH via the skill's postinstall step. Use when the user asks to review a GitHub pull request. Splits oversized diffs into budgeted shards and rebuilds the diff from the local clone when gh pr diff refuses it.",
5
5
  "author": "iceinvein",
6
6
  "type": "prompt",
@@ -33,15 +33,18 @@ are examples, not fixed copy, and a channel with a two-part signal should say
33
33
  which part applied. `bypass` says nothing at all, because a question that gets
34
34
  announced stops being a question.
35
35
 
36
- **`root-cause`, `finish` and `meter` are not channel-assigned.** The code
37
- misbehaving triggers the first: a bug report, a red test, behaviour you cannot
38
- account for. An integration event, merging, pushing, or opening a PR, triggers
39
- the second. Handing the work back triggers the third, whether or not it ever
40
- reaches an integration event. The first two fire in every channel, `bypass`
41
- included; `meter` cannot, because `bypass` announces nothing to measure from.
42
- A question that turns on how something looks is triggered the same way and in
43
- any channel: `references/show-or-say.md` decides whether to show it or say it,
44
- and `bypass` is where it comes up most.
36
+ **`root-cause`, `finish`, `meter` and `show-or-say` are not channel-assigned.**
37
+ The code misbehaving triggers the first: a bug report, a red test, behaviour you
38
+ cannot account for. An integration event, merging, pushing, or opening a PR,
39
+ triggers the second. Handing the work back triggers the third, whether or not it
40
+ ever reaches an integration event. A question that turns on how something looks
41
+ rather than on what it means triggers the fourth: say it where a sentence would
42
+ settle it, show it where the candidates have to sit side by side for an eye to
43
+ compare them. `references/show-or-say.md`
44
+
45
+ The first, second and fourth fire in every channel, `bypass` included, and
46
+ `bypass` is where the fourth comes up most. `meter` does not, because `bypass`
47
+ announces nothing to measure from.
45
48
 
46
49
  ## The rules
47
50
 
@@ -63,8 +66,10 @@ yourself wanting to skip the rule, or arguing that this one is the exception.
63
66
  something anyone else can check. `references/verify.md`
64
67
  - **Review** before merge. Dispatch a reviewer with fresh context and put the
65
68
  diff on disk for it to read, so those bytes fill their context instead of
66
- yours. Cannot dispatch one? Say so in the announcement and again at the
67
- merge, and nowhere in between. `references/review.md`
69
+ yours. A session that dispatches only when asked has not refused you one, so
70
+ ask rather than announce that you cannot. Genuinely without one? Say so in
71
+ the announcement and again at the merge, and nowhere in between.
72
+ `references/review.md`
68
73
  - **Finish** deliberately. Green suite first, then let your partner pick
69
74
  merge, PR, or leave it. Never pick for them. `references/finish.md`
70
75
  - **Meter the run** as you hand it back. `scripts/run-stats.sh` reads the
@@ -85,32 +90,32 @@ Design to `docs/specs/YYYY-MM-DD-<topic>.md`, plan to
85
90
  `docs/plans/YYYY-MM-DD-<topic>.md`, unless the repo has a convention or
86
91
  your partner states a preference. Get the design signed off before code.
87
92
 
88
- The plan gets a stop of its own, whoever executes it. Dispatched agents each
89
- see only their own task, so nobody reads it whole; executing it yourself is
90
- the weaker case rather than the exempt one, because then nobody reads it but
91
- its author.
93
+ The second stop is the plan's, whoever executes it. Dispatched agents each see
94
+ only their own task, so nobody reads it whole; executing it yourself is the
95
+ weaker case rather than the exempt one, because then nobody reads it but its
96
+ author.
92
97
 
93
98
  Order the plan so the tasks that change nothing come first, and mark the one
94
99
  task that turns the new behaviour on. Then a late re-baseline, re-blessed
95
100
  snapshots, regenerated fixtures, attributes to that one point instead of to
96
101
  the branch at large.
97
102
 
98
- Pre-flight rides in the plan's stop: which flagged tasks get a reviewer, and
103
+ Pre-flight rides in that same stop: which flagged tasks get a reviewer, and
99
104
  whether the work runs in a worktree. Ask them as choices with the counts in
100
- them, never as a paragraph, then end the turn on the answers rather than
101
- opening Task 1 with them. A session that dispatches only when asked has not
102
- ruled dispatch out, it has made this question the place to ask. Genuine
103
- unavailability is the tool not being there at all, which is a different case
104
- and a different paragraph. Review that turns out to be missing is only
105
- actionable while the plan can still change.
105
+ them, never as a paragraph, then end the turn. Task 1 opens on their next
106
+ instruction, and an answer that already carries one is that instruction. A
107
+ session that dispatches only when asked has not ruled dispatch out, it has made
108
+ this question the place to ask; genuine unavailability is the tool not being
109
+ there at all, which `references/deep-channel.md` handles separately. Review that
110
+ turns out to be missing is only actionable while the plan can still change.
106
111
 
107
112
  Read the plan as a graph rather than a list. `Needs` and `Offers` are
108
113
  dependency edges and `Touches` says what cannot overlap, so which tasks may
109
114
  run at once is derivable rather than guessed. Fan out wherever that graph
110
115
  allows; serial is the fallback for where it doesn't, not the default.
111
116
 
112
- Then read `references/deep-channel.md` for the plan format, that stop,
113
- the dispatch rules, and when a task actually needs a reviewer. Three that
117
+ Then read `references/deep-channel.md` for the plan format, that stop, the run
118
+ record, the dispatch rules, and when a task actually needs a reviewer. Three that
114
119
  catch people out: concurrent implementers need a worktree each and the flip
115
120
  runs alone, review is tiered rather than automatic, and a `deep` run that
116
121
  cannot dispatch has to replace the review tier with something, not quietly
@@ -58,15 +58,38 @@ the diff, and they will derive it wrong.
58
58
  Two tasks carrying `Flips` means the plan holds two branches' worth of work.
59
59
  Split it. A plan with none is not a `deep` plan: nothing in it does anything.
60
60
 
61
+ ## The run record
62
+
63
+ A `deep` run outlives its own context, so what it learns has to sit on disk
64
+ rather than in the session. Open one file for the run before pre-flight and
65
+ write it as you go. It holds what a stranger resuming tomorrow would need and
66
+ you would otherwise be recalling: the base each task was dispatched from, each
67
+ task with its status and its commits, the review decisions pre-flight settled
68
+ and the reason each one was settled that way, and any finding belonging to a
69
+ task other than the one that surfaced it.
70
+
71
+ Where it goes follows the repo if the repo has a convention, and
72
+ `docs/plans/YYYY-MM-DD-<topic>-record.md` if it does not. It belongs to you
73
+ rather than to any task, so it lives in the tree you are working from, no
74
+ task's `Touches` names it, and you commit it yourself alongside the plan.
75
+ Assembling it at handback defeats it: a record written from memory is memory,
76
+ which is the one thing the file exists to replace.
77
+
78
+ A file only outlives compaction if you go back to it. Read it before the next
79
+ dispatch whenever this session has been summarised, and treat what it says over
80
+ what you remember, including where the two agree. Each task closes by writing
81
+ its commit into the record, which means asking the implementer to report the
82
+ SHA it committed and putting that in the row rather than deriving it later.
83
+
61
84
  ## Pre-flight
62
85
 
63
- Design signed off, plan written, nothing built yet. Before Task 1, hand back
64
- once and settle two things with your partner. Ask them as questions with
65
- options, not as a paragraph they have to reply to in prose: what you are after
66
- is a decision, and a wall of considerations asks them to extract the decision
67
- from it first.
86
+ Design signed off, plan written, nothing built yet. Before Task 1, stop once
87
+ and settle two things with your partner. Ask them as questions with options,
88
+ not as a paragraph they have to reply to in prose: what you are after is a
89
+ decision, and a wall of considerations asks them to extract the decision from
90
+ it first.
68
91
 
69
- This handback is the plan's sign-off as well, so it ends your turn, and a
92
+ This stop is the plan's sign-off as well, so it ends your turn, and a
70
93
  question tool does not end it for you. That tool returns an answer without
71
94
  returning control: two options came back, the plan itself did not, and your
72
95
  partner reads the summary of it in the same message as Task 1's first edit,
@@ -99,8 +122,8 @@ repo, a lint target that resolves through a symlink, a build that needs its own
99
122
  install, belongs in the option text where it can affect the answer, not in a
100
123
  footnote after they have chosen.
101
124
 
102
- If one of the two has only one live answer, say which and ask the other. A
103
- checkpoint down to a single question is still a checkpoint.
125
+ If one of the two has only one live answer, say which and ask the other. A stop
126
+ down to a single question is still a stop.
104
127
 
105
128
  A session that forbids subagents does not skip this; it changes what the
106
129
  review options are. Skipping it is how "review outstanding" first appears in
@@ -121,8 +144,8 @@ Derive the sets at dispatch rather than writing wave numbers into the plan. A
121
144
  declared schedule is wrong the moment one task lands late or comes back with a
122
145
  blocking finding. A derived one just recomputes.
123
146
 
124
- - One `TaskCreate` per task, marked in progress then complete. That state
125
- outlives compaction; your memory doesn't.
147
+ - One line per task in the run record, marked in progress then complete as it
148
+ moves. That state outlives compaction; your memory doesn't.
126
149
  - Each task goes to a fresh agent with that task's text and nothing else.
127
150
  What this session accumulated is yours to hold, not theirs.
128
151
  - **Fan out wherever the graph allows.** Work that does not write is always
@@ -169,10 +192,11 @@ say so is the routing announcement, where your partner can still act on it,
169
192
  not the summary at the end where it reads as an excuse.
170
193
 
171
194
  Three things change. The plan stops being a brief for strangers and becomes
172
- your own worklist, which makes its handback the only outside read it will ever
173
- get rather than a formality on the way to dispatch. Task isolation is gone, so
174
- `TaskCreate` now carries all of the state that outlives compaction and matters
175
- more, not less. And fresh context is unavailable, which was the entire thing
195
+ your own worklist, so its stop is no longer buying alignment with the agents
196
+ who will carry it out, only your partner's read of work you will do yourself.
197
+ Task isolation is gone, so
198
+ the run record now carries all of the state that outlives compaction and
199
+ matters more, not less. And fresh context is unavailable, which was the entire thing
176
200
  review was buying.
177
201
 
178
202
  One thing does not change: the work still owes a review. Reading your own diff
@@ -225,8 +249,8 @@ a reviewer writes nothing, so it collides with nothing. The final review is
225
249
  the only one that waits, because it is the only one that needs everything to
226
250
  have landed.
227
251
 
228
- Record the base in the task's `TaskCreate` entry when you dispatch, before the
229
- agent's first commit lands. Recovering it afterwards is archaeology, and the
252
+ Record the base against that task in the run record when you dispatch, before
253
+ the agent's first commit lands. Recovering it afterwards is archaeology, and the
230
254
  answer you will guess at is `HEAD~1`, which `references/review.md` already
231
255
  names as the standing mistake.
232
256
 
@@ -260,8 +284,8 @@ Contracts mean tasks are built blind, so a reviewer reading Task 6 will
260
284
  sometimes be right about Task 2. That finding fits neither destination: Task
261
285
  6's agent cannot act on it, and it does not quietly become yours to fix.
262
286
 
263
- Record it on the earlier task's `TaskCreate` entry, and send it to that task's
264
- agent as well if that agent is still live. If it invalidates an `Offers` that
287
+ Record it against the earlier task in the run record, and send it to that
288
+ task's agent as well if that agent is still live. If it invalidates an `Offers` that
265
289
  later tasks have already built against, it has stopped being a finding and
266
290
  become a plan change, which is your partner's call rather than something to
267
291
  absorb into the next task's brief.
@@ -14,10 +14,13 @@ leave it off entirely if no suite ran, which prints "not reported" rather
14
14
  than a number nobody checked. `--base <ref>` overrides the diff base when the
15
15
  merge-base with `main` or `master` is not where this branch actually began.
16
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.
17
+ The run starts at your channel announcement, or at the point this skill was
18
+ invoked by name when that came first, not at the session. Three unrelated tasks
19
+ in one session therefore report as three runs, and a previous ledger closes the
20
+ run before it. `bypass` announces nothing, so there is nothing there to meter,
21
+ and the rule does not fire; if you run the script anyway in a session that
22
+ loaded the skill, it will happily print a ledger headed `not announced`, which
23
+ is a measurement of nothing you were asked to account for.
21
24
 
22
25
  What the ledger is for: the cost of a channel is otherwise invisible, and a
23
26
  channel nobody can price is a channel nobody can choose between. Six agents
@@ -31,26 +34,40 @@ summed, over the span those agents actually occupied. 1.0× means every agent
31
34
  had the clock to itself. That is the right number for a plan whose graph was a
32
35
  chain and a finding for one whose graph was not, so read it against the plan
33
36
  rather than on its own. Nine agents at 1.0× on a plan with four independent
34
- tasks in it is a run that took four times longer than it needed to.
37
+ tasks in it is a run that took four times longer than it needed to. Both
38
+ figures cover only the agents the harness timed, so a run whose agents were all
39
+ dispatched into the background shows neither.
35
40
 
36
- An agent dispatched into the background is handed back before it runs, so the
37
- result the session sees carries no cost at all. The ledger reads that agent's
38
- own transcript instead, which is why a plan run entirely on dispatched agents
39
- still prices. Agent rows count output tokens, the same unit the run reports for
40
- itself, so they can be read against each other and against the run.
41
+ Agents are priced from two different sources, and the line keeps them apart
42
+ rather than adding them. Where the harness priced an agent itself, that is the
43
+ figure: it is the accounting the session was billed by and it covers input as
44
+ well as output. An agent dispatched into the background is handed back before
45
+ it runs, so the harness never priced it at all, and the only record left is the
46
+ agent's own transcript. Those rows are marked `~`, count output tokens only,
47
+ and carry no wall-clock, because a log holds no duration: its span runs from
48
+ first message to last and swallows the idle whenever an agent was resumed.
49
+ Adding a `~` figure to a harness figure would produce a number that means
50
+ nothing, which is why you will see two totals on one line and never one.
41
51
 
42
- Where that log has been cleaned up the row reads "cost not reported", the
43
- totals leave it out, and the agent line carries the count. That is a gap in
44
- what could be measured rather than a cheap run, and it needs a sentence from
45
- you: a smaller total must not be allowed to stand for the whole.
52
+ An agent nothing priced reads "cost not reported" and the totals leave it out,
53
+ with the count on the agent line. That is a gap in what could be measured
54
+ rather than a cheap run, and it needs a sentence from you: a smaller total must
55
+ not be allowed to stand for the whole.
46
56
 
47
57
  Read it before you paste it. A row showing an agent that errored, or an agent
48
58
  whose token count dwarfs every other row, is a finding about the run and
49
- belongs in your prose, not left for your partner to spot in a table.
59
+ belongs in your prose, not left for your partner to spot in a table. A row
60
+ whose figure is missing or marked is a finding about the ledger, and saying
61
+ which of the two you are looking at is the whole job here.
50
62
 
51
63
  Exit 2 means neither an announcement nor an invocation of this skill was found,
52
64
  so there was no run to report. That is a fact about the work, not a failure: do
53
- not synthesise a ledger to fill the gap.
65
+ not synthesise a ledger to fill the gap. It can also mean your announcement was
66
+ worded in a shape the script does not recognise, so check that it is not simply
67
+ missing you before you report the run as nothing. Any other non-zero exit is a
68
+ broken tool, not a fact about the work: it prints its reason on stderr, and the
69
+ answer is to say the ledger could not be produced rather than to describe the
70
+ run from memory.
54
71
 
55
72
  The friction line: "I know roughly what this cost." Roughly is the problem.
56
73
  Every number here is already on disk, and the remembered version is reliably
@@ -21,9 +21,12 @@ wrong still gets raised, then argued.
21
21
 
22
22
  Every finding has one of two destinations. Either it goes back to the agent
23
23
  that wrote the code and the work stays where it is until the fix lands, or it
24
- goes onto the task's record, which is what the final review reads. Reviewers
25
- grade findings Critical, Important or Minor, and the grade picks between the
26
- two: only Minor may wait for the record, and anything above it blocks.
24
+ is written down against the task and waits. In `deep` that is the run record
25
+ `references/deep-channel.md` sets up, and it is what the final review reads. In
26
+ `main` there is no plan and no record, so a finding that waits waits in the
27
+ message you hand back, named there rather than carried silently to the merge.
28
+ Reviewers grade findings Critical, Important or Minor, and the grade picks
29
+ between the two: only Minor may wait, and anything above it blocks.
27
30
 
28
31
  The grade is the sort, and it has to be load-bearing. The alternative is you
29
32
  reading every finding to decide where it goes, which is an adjudication pass
@@ -8,7 +8,9 @@
8
8
  # Usage: run-stats.sh [--tests "<what the suite reported>"] [--base <git-ref>]
9
9
  # [--transcript <path>]
10
10
  #
11
- # Exit 0 printed a ledger, 1 could not read a transcript, 2 no run to report.
11
+ # Exit 0 printed a ledger, 2 no run to report. Any other non-zero is the tool
12
+ # failing rather than a fact about the run: no jq, no readable transcript, an
13
+ # unknown argument, an unresolvable session id. Each prints its reason first.
12
14
 
13
15
  set -uo pipefail
14
16
 
@@ -46,32 +48,43 @@ if [ -z "$TRANSCRIPT" ] || [ ! -f "$TRANSCRIPT" ]; then
46
48
  exit 1
47
49
  fi
48
50
 
49
- # An agent handed back before it ran leaves no cost on its tool result, but it
51
+ # An agent the harness never priced leaves no cost on its tool result, but it
50
52
  # keeps a transcript of its own beside the session. That file is the only place
51
- # a backgrounded agent's cost is ever written down, so read it rather than
52
- # reporting the work as free.
53
+ # such an agent is ever written down, so read it rather than reporting the work
54
+ # as free. Only ids this transcript actually mentions are read: a long-lived
55
+ # session accumulates far more logs than any one run dispatched.
56
+ #
57
+ # No duration is taken from a log. Its span is first timestamp to last, which
58
+ # covers the idle between rounds whenever the agent was resumed, and that is
59
+ # not a measure of work. Tokens are summed per message rather than per line,
60
+ # because one message is written as several lines carrying the same cumulative
61
+ # usage, and summing the lines counts the message more than once.
53
62
  SUBS="$(dirname "$TRANSCRIPT")/$(basename "$TRANSCRIPT" .jsonl)/subagents"
54
63
  COSTS='{}'
55
64
  if [ -d "$SUBS" ]; then
56
65
  COSTS="$(
57
- for f in "$SUBS"/agent-*.jsonl; do
58
- [ -e "$f" ] || continue
59
- id="$(basename "$f" .jsonl)"; id="${id#agent-}"
66
+ jq -r 'select((.toolUseResult | type) == "object")
67
+ | .toolUseResult.agentId // empty' "$TRANSCRIPT" 2>/dev/null | sort -u |
68
+ while IFS= read -r id; do
69
+ [ -n "$id" ] || continue
70
+ f="$SUBS/agent-$id.jsonl"
71
+ [ -f "$f" ] || continue
60
72
  jq -s --arg id "$id" '
61
- def ts: sub("\\.[0-9]+Z$"; "Z") | fromdateiso8601;
62
- [ .[] | select(.timestamp) ] as $t
63
- | { ($id): {
64
- tokens: ([ .[] | .message.usage.output_tokens // 0 ] | add // 0),
73
+ [ .[] | select(.message.usage) ] as $u
74
+ | if ($u | length) == 0 then {} else
75
+ { ($id): {
76
+ tokens: ([ $u[] | { k: (.message.id // "?"),
77
+ v: (.message.usage.output_tokens // 0) } ]
78
+ | group_by(.k) | map(map(.v) | max) | add // 0),
65
79
  tools: ([ .[] | .message.content[]? | select(.type == "tool_use") ] | length),
66
- model: ([ .[] | .message.model // empty ] | last // "?"),
67
- starts: (if ($t | length) > 0 then ($t[0].timestamp | ts) else 0 end),
68
- ends: (if ($t | length) > 0 then ($t[-1].timestamp | ts) else 0 end) } }' "$f"
69
- done | jq -s 'add // {}'
80
+ model: ([ .[] | .message.model // empty ] | last // "?") } }
81
+ end' "$f" 2>/dev/null
82
+ done | jq -s 'add // {}' 2>/dev/null
70
83
  )"
71
84
  fi
72
85
  [ -n "$COSTS" ] || COSTS='{}'
73
86
 
74
- SUMMARY="$(jq -s --argjson costs "$COSTS" '
87
+ SUMMARY="$(jq -s --argjson costs "$COSTS" --arg sid "$(basename "$TRANSCRIPT" .jsonl)" '
75
88
  # ---- what counts as a real turn ----------------------------------------
76
89
  # Transcript stamps carry milliseconds, which fromdateiso8601 will not take.
77
90
  def ts: sub("\\.[0-9]+Z$"; "Z") | fromdateiso8601;
@@ -82,22 +95,30 @@ SUMMARY="$(jq -s --argjson costs "$COSTS" '
82
95
  # merely discusses a channel, which a session about sluice is full of.
83
96
  def marker: "^[*_#>[:space:]]*(?<c>fast|main|deep)[[:space:]]+channel";
84
97
  # Real announcements carry a lead-in the anchor above misses: "Sluice: **deep
85
- # channel**", "Tier 2 (new contract surface) = **deep channel**". Emphasis is
86
- # what still separates one from prose naming a channel, and holding it to the
87
- # opening line keeps a session reviewing sluice from starting a run per quote.
88
- def emph: "\\*\\*[[:space:]]*(?<c>fast|main|deep)[[:space:]]+channel";
89
- def head_line: texts | split("\n") | (.[0] // "");
98
+ # channel**", "Tier 2 (new contract surface) = **deep channel**". What makes
99
+ # those announcements and not prose is that a label introduces them, so the
100
+ # channel has to follow a colon or an equals directly. Emphasis alone will not
101
+ # do: a closing ** reads the same as an opening one, which let "not a **big**
102
+ # deep channel job" announce. The lead-in cannot cross a sentence or a line,
103
+ # which is what keeps a session reviewing sluice from starting a run per quote.
104
+ def lead: "^[^.!?\n]{0,100}[:=][[:space:]]*[*_]*(?<c>fast|main|deep)[[:space:]]+channel";
90
105
  def announces: (.type == "assistant") and (is_meta | not)
91
- and ((texts | test(marker; "i")) or (head_line | test(emph; "i")));
106
+ and ((texts | test(marker; "i")) or (texts | test(lead; "i")));
92
107
  def chan: (if (texts | test(marker; "i")) then (texts | capture(marker; "i"))
93
- else (head_line | capture(emph; "i")) end) | .c | ascii_downcase;
108
+ else (texts | capture(lead; "i")) end) | .c | ascii_downcase;
94
109
  def invokes_sluice: (.type == "assistant") and (is_meta | not) and ([
95
110
  .message.content[]? | select(.type == "tool_use" and .name == "Skill")
96
111
  | select((.input.skill // "") == "sluice")
97
112
  ] | length > 0);
113
+ # A call carrying --transcript is reading another run rather than closing this
114
+ # one, unless the transcript it names is this session, which is a documented
115
+ # and legitimate way to meter yourself. Without the exception, a session
116
+ # working on the ledger clips its own run at the last session it tested.
98
117
  def is_stats_call: (.type == "assistant") and ([
99
118
  .message.content[]? | select(.type == "tool_use" and .name == "Bash")
100
- | select((.input.command // "") | test("run-stats\\.sh"))
119
+ | (.input.command // "") as $cmd
120
+ | select($cmd | test("run-stats\\.sh"))
121
+ | select(($cmd | test("--transcript") | not) or ($cmd | test($sid)))
101
122
  ] | length > 0);
102
123
  # Waiting is any turn the partner had to take: a prompt, or an answer to a
103
124
  # question you put to them. Leaving the latter out understates the wait on
@@ -112,9 +133,11 @@ SUMMARY="$(jq -s --argjson costs "$COSTS" '
112
133
  | [ range(0; $all | length) ] as $ix
113
134
  | [ $ix[] | select($all[.] | announces) ] as $said
114
135
  # A run whose announcement never matched is still a run when the skill was
115
- # invoked by name. Anchoring there beats reporting the work as nothing.
136
+ # invoked by name. Both kinds of anchor go in together rather than the
137
+ # invocations being a fallback for the whole file: a session where run 1
138
+ # announced and run 2 did not still has to report run 2 on its own.
116
139
  | [ $ix[] | select($all[.] | invokes_sluice) ] as $called
117
- | (if ($said | length) > 0 then $said else $called end) as $ann
140
+ | (($said + $called) | sort) as $ann
118
141
  | if ($ann | length) == 0 then { empty: true } else
119
142
 
120
143
  # A previous ledger closes the run before it. The call running right now has
@@ -142,10 +165,18 @@ SUMMARY="$(jq -s --argjson costs "$COSTS" '
142
165
  # ---- tools and tokens, main loop only ----------------------------------
143
166
  | [ $run[] | select((.type == "assistant") and (is_meta | not))
144
167
  | .message.content[]? | select(.type == "tool_use") | .name ] as $tools
168
+ # One message is written as several lines, each repeating the same cumulative
169
+ # usage, so these are summed per message rather than per line. Summed per line
170
+ # the figure for this run roughly doubles, and it is the one most likely to
171
+ # be read.
145
172
  | ([ $run[] | select((.type == "assistant") and (is_meta | not))
146
- | .message.usage.output_tokens // 0 ] | add // 0) as $out_tok
147
- | ([ $run[] | select((.type == "assistant") and (is_meta | not))
148
- | .message.usage.cache_read_input_tokens // 0 ] | add // 0) as $cache_tok
173
+ | select(.message.usage)
174
+ | { k: (.message.id // .uuid // "?"),
175
+ o: (.message.usage.output_tokens // 0),
176
+ c: (.message.usage.cache_read_input_tokens // 0) } ]
177
+ | group_by(.k)) as $usage
178
+ | ([ $usage[] | map(.o) | max ] | add // 0) as $out_tok
179
+ | ([ $usage[] | map(.c) | max ] | add // 0) as $cache_tok
149
180
 
150
181
  # ---- dispatched agents -------------------------------------------------
151
182
  | ([ $run[] | select(.type == "assistant") | .message.content[]?
@@ -155,37 +186,44 @@ SUMMARY="$(jq -s --argjson costs "$COSTS" '
155
186
  # toolUseResult is whatever the tool returned: object, array or string.
156
187
  | [ $run[] | select((.toolUseResult | type) == "object") | select(.toolUseResult.agentId)
157
188
  | . as $e
158
- # Inline cost when the agent ran to completion here, its own transcript
159
- # when it was handed back before it ran, and neither only when the file
160
- # has been cleaned up. Priced at zero it would understate the run and drag
161
- # the concurrency factor to a number the ledger has no meaning for.
189
+ # Where the harness priced the agent, that is the number: it is the
190
+ # accounting the session itself was billed by, and it covers input as well
191
+ # as output. The log is the fallback for agents the harness never priced,
192
+ # and it holds output tokens only, so the two are different units and the
193
+ # totals below never add one to the other.
162
194
  | ($costs[$e.toolUseResult.agentId] // null) as $c
163
- # The log wins where there is one. The two sources count different things,
164
- # the inline total being the harness accounting and the log being output
165
- # tokens, and rows drawn from different bases cannot be read against each
166
- # other. Output tokens is also the unit the run reports for itself.
167
- | (if $c == null then $e.toolUseResult.totalDurationMs
168
- else (($c.ends - $c.starts) * 1000) end) as $ms
169
- | ($c.tokens // $e.toolUseResult.totalTokens) as $tokens
170
- | { label: ($labels[[ $e.message.content[]? | select(.type == "tool_result") | .tool_use_id ][0]]
171
- // $e.toolUseResult.commandName // "agent"),
172
- model: ($c.model // $e.toolUseResult.resolvedModel // "?"),
195
+ | (if ($e.toolUseResult.totalTokens != null)
196
+ or ($e.toolUseResult.totalDurationMs != null) then "inline"
197
+ elif $c != null then "log"
198
+ else "none" end) as $src
199
+ # Indexing with null throws, and a result without a tool_result block is
200
+ # not worth failing the whole ledger over.
201
+ | (([ $e.message.content[]? | select(.type == "tool_result") | .tool_use_id ][0]) // "") as $tid
202
+ | { label: ($labels[$tid] // $e.toolUseResult.commandName // "agent"),
203
+ src: $src,
173
204
  status: ($e.toolUseResult.status // "?"),
174
- measured: (($ms != null) or ($tokens != null)),
175
- tokens: ($tokens // 0),
176
- ms: ($ms // 0),
205
+ # Model, tool count and cost all come from whichever source priced the
206
+ # agent, so a row never mixes one source with another.
207
+ model: (if $src == "log" then $c.model
208
+ else $e.toolUseResult.resolvedModel end // "?"),
209
+ tokens: (if $src == "log" then $c.tokens
210
+ else $e.toolUseResult.totalTokens end // 0),
211
+ tools: (if $src == "log" then $c.tools
212
+ else $e.toolUseResult.totalToolUseCount end // 0),
213
+ ms: ($e.toolUseResult.totalDurationMs // 0),
177
214
  # The session transcript timestamps when an agent returned, not when
178
- # it began, so that start is back-derived from its own duration. An
179
- # agent log of its own carries both ends directly.
180
- ends: (if $c != null then $c.ends else ($e.timestamp | ts) end),
181
- starts: (if $c != null then $c.starts
182
- else (($e.timestamp | ts) - (($ms // 0) / 1000)) end),
183
- tools: ($c.tools // $e.toolUseResult.totalToolUseCount // 0) } ] as $agents
215
+ # it began, so the start is back-derived from its own duration.
216
+ ends: ($e.timestamp | ts),
217
+ starts: (($e.timestamp | ts)
218
+ - (($e.toolUseResult.totalDurationMs // 0) / 1000)) } ] as $agents
184
219
 
185
- | [ $agents[] | select(.measured) ] as $priced
220
+ | [ $agents[] | select(.src == "inline") ] as $priced
221
+ | [ $agents[] | select(.src == "log") ] as $logged
222
+ | [ $agents[] | select(.src == "none") ] as $unpriced
186
223
 
187
224
  # Union of the agent intervals: sum the merged runs rather than the raw ones,
188
- # so overlapping agents are counted once against the clock they shared.
225
+ # so overlapping agents are counted once against the clock they shared. Only
226
+ # agents the harness timed are in here; a log carries no duration to add.
189
227
  | ([ $priced[] | { s: .starts, e: .ends } ] | sort_by(.s)
190
228
  | reduce .[] as $i ([];
191
229
  if (length == 0) or (.[-1].e < $i.s)
@@ -201,8 +239,11 @@ SUMMARY="$(jq -s --argjson costs "$COSTS" '
201
239
  | sort_by(-.n) | .[0:3]),
202
240
  out_tok: $out_tok, cache_tok: $cache_tok,
203
241
  agents: $agents,
204
- unmeasured: (($agents | length) - ($priced | length)),
242
+ priced_count: ($priced | length),
243
+ logged_count: ($logged | length),
244
+ unpriced_count: ($unpriced | length),
205
245
  agent_tokens: ([ $priced[].tokens ] | add // 0),
246
+ logged_tokens: ([ $logged[].tokens ] | add // 0),
206
247
  agent_ms: ([ $priced[].ms ] | add // 0),
207
248
  concurrency: (if $agent_span > 0
208
249
  then (([ $priced[].ms ] | add // 0) / 1000) / $agent_span
@@ -289,41 +330,60 @@ if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
289
330
  fi
290
331
 
291
332
  count=$(g '.agents | length')
292
- unmeasured=$(g '.unmeasured')
333
+ priced=$(g '.priced_count')
334
+ logged=$(g '.logged_count')
335
+ unpriced=$(g '.unpriced_count')
293
336
  if [ "$count" -eq 0 ]; then
294
337
  printf 'agents none dispatched\n'
295
338
  else
296
- if [ "$count" -eq "$unmeasured" ]; then
297
- # Every agent was handed back before it ran, so there is no cost to report
298
- # and none to invent. Saying so beats a row of zeroes that reads as a total.
299
- printf 'agents %s dispatched · cost not reported\n' "$count"
300
- else
339
+ printf 'agents %s dispatched' "$count"
340
+ if [ "$priced" -gt 0 ]; then
301
341
  # 1.0× means every agent had the clock to itself. On a plan whose graph had
302
342
  # independent tasks in it, that number is the finding.
303
- printf 'agents %s dispatched · %s tok · %s wall · %s× concurrent' \
304
- "$count" "$(tok "$(g '.agent_tokens')")" "$(dur "$(( $(g '.agent_ms') / 1000 ))")" \
343
+ printf ' · %s tok · %s wall · %s× concurrent' \
344
+ "$(tok "$(g '.agent_tokens')")" "$(dur "$(( $(g '.agent_ms') / 1000 ))")" \
305
345
  "$(printf '%.1f' "$(g '.concurrency')")"
306
- [ "$unmeasured" -gt 0 ] && printf ' · %s unmeasured' "$unmeasured"
307
- printf '\n'
308
346
  fi
347
+ if [ "$logged" -gt 1 ]; then
348
+ printf ' · %s out from %s logs' "$(tok "$(g '.logged_tokens')")" "$logged"
349
+ elif [ "$logged" -eq 1 ]; then
350
+ printf ' · %s out from 1 log' "$(tok "$(g '.logged_tokens')")"
351
+ fi
352
+ if [ "$priced" -eq 0 ] && [ "$logged" -eq 0 ]; then
353
+ # Nothing priced any of them, so there is no cost to report and none to
354
+ # invent. Saying so beats a row of zeroes that reads as a total, and beats
355
+ # a count of unpriced agents when the count is all of them.
356
+ printf ' · cost not reported'
357
+ elif [ "$unpriced" -gt 0 ]; then
358
+ printf ' · %s unpriced' "$unpriced"
359
+ fi
360
+ printf '\n'
309
361
 
310
362
  # Up to a dozen rows read as the narrative of the plan. Past that the order
311
363
  # stops helping, so show what the run actually spent on and say what is cut.
312
364
  if [ "$count" -le 12 ]; then rows='.agents[]'; else rows='(.agents | sort_by(-.tokens) | .[0:10][])'; fi
313
- while IFS=$'\t' read -r label model status tokens ms tools measured; do
365
+ while IFS=$'\t' read -r label model status tokens ms tools src; do
314
366
  model="${model#claude-}"; model="$(sed 's/-[0-9]\{8\}$//' <<<"$model")"
315
367
  [ "$status" = "completed" ] && status="" || status=" ($status)"
316
- if [ "$measured" = "true" ]; then
317
- printf ' %-22.22s %-10s %6s %7s %s tools%s\n' \
318
- "$label" "$model" "$(tok "$tokens")" "$(dur "$((ms / 1000))")" "$tools" "$status"
319
- else
320
- printf ' %-22.22s %-10s %6s %7s %s%s\n' \
321
- "$label" "$model" "-" "-" "cost not reported" "$status"
322
- fi
323
- done < <(g "$rows | [.label, .model, .status, .tokens, .ms, .tools, .measured] | @tsv")
368
+ case "$src" in
369
+ inline) printf ' %-22.22s %-10s %6s %7s %s tools%s\n' \
370
+ "$label" "$model" "$(tok "$tokens")" "$(dur "$((ms / 1000))")" "$tools" "$status" ;;
371
+ # A tilde marks the other unit, and the dash is the duration a log cannot
372
+ # give. Both are explained by the footnote below.
373
+ log) printf ' %-22.22s %-10s %6s %7s %s tools%s\n' \
374
+ "$label" "$model" "~$(tok "$tokens")" "-" "$tools" "$status" ;;
375
+ *) printf ' %-22.22s %-10s %6s %7s %s%s\n' \
376
+ "$label" "$model" "-" "-" "cost not reported" "$status" ;;
377
+ esac
378
+ done < <(g "$rows | [.label, .model, .status, .tokens, .ms, .tools, .src] | @tsv")
379
+
380
+ [ "$logged" -gt 0 ] && printf ' ~ output tokens from the agent log; the harness never priced these\n'
324
381
 
325
382
  if [ "$count" -gt 12 ]; then
326
- printf ' +%s more · %s tok (dearest 10 shown)\n' \
327
- "$((count - 10))" "$(tok "$(g '(.agents | sort_by(-.tokens) | .[10:] | map(.tokens) | add) // 0')")"
383
+ cut_priced="$(g '(.agents | sort_by(-.tokens) | .[10:] | map(select(.src == "inline") | .tokens) | add) // 0')"
384
+ cut_unpriced="$(g '(.agents | sort_by(-.tokens) | .[10:] | map(select(.src != "inline")) | length)')"
385
+ printf ' +%s more · %s tok (dearest 10 shown)' "$((count - 10))" "$(tok "$cut_priced")"
386
+ [ "$cut_unpriced" -gt 0 ] && printf ', %s of them unpriced' "$cut_unpriced"
387
+ printf '\n'
328
388
  fi
329
389
  fi
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sluice",
3
- "version": "0.3.0",
4
- "description": "Routes work by change shape into four channels (bypass, fast, main, deep) and applies only the rules each channel needs, so a one-line fix does not pay the cost of a multi-subsystem build. Carries seven rules as one-liners in the router and the full treatment in references read only on friction, and closes each run with a ledger read out of the session transcript: elapsed, tools, tokens, and what every dispatched agent cost. Claude Code only; conflicts with the superpowers plugin.",
3
+ "version": "0.5.0",
4
+ "description": "Routes work by change shape into four channels (bypass, fast, main, deep) and applies only the rules each channel needs, so a one-line fix does not pay the cost of a multi-subsystem build. Carries seven rules as one-liners in the router and the full treatment in references read only on friction, and closes each run with a ledger read out of the session transcript: elapsed, tools, tokens, and what each dispatched agent cost where the transcript recorded it. Claude Code only; stands down where the superpowers pipeline governs the repo.",
5
5
  "author": "iceinvein",
6
6
  "type": "prompt",
7
7
  "tools": [