@iceinvein/agent-skills 0.23.2 → 0.23.4

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
@@ -44,8 +44,8 @@ Each skill is also a plugin in the `iceinvein-skills` marketplace:
44
44
 
45
45
  | Skill | What it does |
46
46
  |-------|--------------|
47
- | **migrate** | Walks a legacy codebase through probe, enumerate, seam, extract, parity, queue, adjudicate and handoff: measured surface coverage, an empirically derived capability seam, cited requirements, a parity plan, a batch decision queue, and work items emitted to markdown, GitHub or a flow target with coverage and forecast read back. Bundles a Bun CLI that enforces the coverage arithmetic and phase ordering instead of trusting it. |
48
- | **scope-estimate** | The lighter path for a proposal: read-only discovery, a capability register with status and disposition, sized functional requirements, contracts-first work packages, and a build schedule across engineers x worktrees at fast, planning and floor rates. Bundles a Bun scheduler that refuses a plan missing or double-counting a requirement, so every figure comes from one reproducible run. |
47
+ | **migrate** | Walks a legacy codebase through probe, enumerate, seam, extract, parity, queue, adjudicate and handoff: measured surface coverage, an empirically derived capability seam, cited requirements, a parity plan, a batch decision queue, and work items emitted to markdown, GitHub or a flow target with coverage and forecast read back. Bundles a Bun CLI that enforces the coverage arithmetic and phase ordering instead of trusting it. Claude Code only. |
48
+ | **scope-estimate** | The lighter path for a proposal: read-only discovery, a capability register with status and disposition, sized functional requirements, contracts-first work packages, and a build schedule across engineers x worktrees at fast, planning and floor rates. Bundles a Bun scheduler that refuses a plan missing or double-counting a requirement, so every figure comes from one reproducible run. Claude Code only. |
49
49
 
50
50
  ### Code Architecture
51
51
 
@@ -100,7 +100,7 @@ Skills that check code does what it was meant to do, with evidence rather than o
100
100
 
101
101
  | Skill | Based on | What it does |
102
102
  |-------|----------|--------------|
103
- | **bug-hunter** | QA scenario testing; intent-first reproduction | Proves logic bugs with failing repros checked against stated intent. Writes scenarios before reading the code, and reports bugs, ambiguities, and covered scenarios. |
103
+ | **bug-hunter** | QA scenario testing; intent-first reproduction | Proves logic bugs with failing repros checked against stated intent. Writes scenarios before reading the code, and reports bugs, ambiguities, and covered scenarios. Claude Code only. |
104
104
 
105
105
  ### Communication
106
106
 
@@ -128,7 +128,7 @@ Skills that shape how work gets done rather than analyzing code.
128
128
 
129
129
  | Skill | What it does |
130
130
  |-------|--------------|
131
- | **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. |
131
+ | **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. Eight 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. |
132
132
 
133
133
  ### Orchestration
134
134
 
@@ -137,7 +137,7 @@ Skills that compose the other audit skills into higher-level workflows.
137
137
  | Skill | What it does |
138
138
  |-------|--------------|
139
139
  | **improve-my-codebase** | Runs every applicable audit skill in parallel against your codebase, ranks findings by convergence (multiple audits agreeing) and per-file rollup, and writes a prioritized improvement report. Default sweeps everything; positional args switch modes (`quick`, `diff`, `interactive`) or narrow scope (`focus <area>`, `module <path>`). |
140
- | **magpie** | 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`, and serves an interactive HTML report for selecting findings to post via `gh`. A local mode reviews the current branch diff before a PR exists and hands the findings back in the terminal. |
140
+ | **magpie** | 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`, and serves an interactive HTML report for selecting findings to post via `gh`. A local mode reviews the current branch diff before a PR exists and hands the findings back in the terminal. Claude Code only. |
141
141
 
142
142
  ## Commands
143
143
 
@@ -174,7 +174,7 @@ bunx @iceinvein/agent-skills list # show installed versions
174
174
  After any command other than `update`, the CLI runs a background freshness check (at most once every 24 hours) and prints a one-line notice if anything is out of date:
175
175
 
176
176
  ```
177
- some-skill (v0.2.0 → v0.3.0)
177
+ ⚠ Updates available: some-skill (v0.2.0 → v0.3.0)
178
178
  Run: agent-skills update --all
179
179
  ```
180
180
 
@@ -195,12 +195,7 @@ If a skill declares activation support and you're installing for Claude Code int
195
195
 
196
196
  ## Supported Tools
197
197
 
198
- | Tool | Prompt Skills | MCP Skills |
199
- |------|:---:|:---:|
200
- | Claude Code | Yes | Yes |
201
- | Cursor | Yes | Yes |
202
- | Codex | Yes | – |
203
- | Gemini CLI | Yes | Yes |
198
+ Claude Code, Cursor, Codex and Gemini CLI. Most skills install into all four; bug-hunter, magpie, migrate, scope-estimate and sluice are Claude Code only, and the CLI skips the other tools for them.
204
199
 
205
200
  ## How It Works
206
201
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iceinvein/agent-skills",
3
- "version": "0.23.2",
3
+ "version": "0.23.4",
4
4
  "description": "Install agent skills into AI coding tools",
5
5
  "author": "iceinvein",
6
6
  "license": "MIT",
package/skills/index.json CHANGED
@@ -295,7 +295,7 @@
295
295
  "name": "sluice",
296
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.",
297
297
  "type": "prompt",
298
- "version": "0.25.2"
298
+ "version": "0.25.4"
299
299
  },
300
300
  {
301
301
  "name": "temporal-coupling-detector",
@@ -145,6 +145,16 @@ the run it names is really there. `close` removes a note naming its own tree,
145
145
  because a note that outlived its run would hand the next run opened in that
146
146
  tree to whoever reads the main tree.
147
147
 
148
+ One note cannot serve a repo whose own rules cut worktrees by hand, outside the
149
+ repo, where no worktree tool will enter them: every controller sits in the main
150
+ tree at once, each with its own run elsewhere, and none of them ran `move`. So
151
+ `init` also records the session that opened the run (`--session`, defaulting to
152
+ `$CLAUDE_CODE_SESSION_ID`) and leaves `.sluice/sessions/<id>` at the main
153
+ worktree naming the run's tree. A tree with no run of its own follows that
154
+ pointer before the note, only while the run there is still that session's.
155
+ `move` points it at the new tree and `close` removes it, so the statusline, the
156
+ SessionStart hook and the Stop hook each find the run of the session asking.
157
+
148
158
  One file for several writers is one file to contend on, so `init`, `task`,
149
159
  `preflight`, `final`, `pause`, `resume`, `close` and `move` take a lock first, `move` taking the
150
160
  destination tree's as well as its own: two flips issued at the same moment
@@ -380,10 +390,10 @@ is live and contends with nothing for width when one is, which is what lets the
380
390
  bar be wide and the task carry its name rather than only its number.
381
391
 
382
392
  Capture it wherever the statusline command builds its other lines, passing the
383
- directory the session is in and nothing else:
393
+ directory the session is in and the session's id, and nothing else:
384
394
 
385
395
  ```bash
386
- sluice_line=$(bash "$HOME/.claude/skills/sluice/scripts/statusline.sh" --dir "$cwd" 2>/dev/null)
396
+ sluice_line=$(bash "$HOME/.claude/skills/sluice/scripts/statusline.sh" --dir "$cwd" --session "$session_id" 2>/dev/null)
387
397
  ```
388
398
 
389
399
  then print it last, after whatever else the command emits:
@@ -392,10 +402,12 @@ then print it last, after whatever else the command emits:
392
402
  if [ -n "$sluice_line" ]; then printf '%s\n' "$sluice_line"; fi
393
403
  ```
394
404
 
395
- `$cwd` is `workspace.current_dir` from the JSON the harness sends on stdin.
396
- Substitute the install path: `${CLAUDE_CONFIG_DIR:-$HOME/.claude}` where a
397
- session may set one, or `$cwd/.claude/skills/sluice/scripts/statusline.sh` for a
398
- project-local install.
405
+ `$cwd` is `workspace.current_dir` and `$session_id` is `session_id`, both from
406
+ the JSON the harness sends on stdin. The id is what finds a run this session
407
+ opened in a worktree it never entered, which is every run in a repo that cuts
408
+ its worktrees by hand. Substitute the install path:
409
+ `${CLAUDE_CONFIG_DIR:-$HOME/.claude}` where a session may set one, or
410
+ `$cwd/.claude/skills/sluice/scripts/statusline.sh` for a project-local install.
399
411
 
400
412
  Those two lines are the whole contract, and they are deliberately empty of
401
413
  judgement. Whether a run is visible from a given directory is a question about
@@ -96,11 +96,12 @@ sluice: to show a live run on your status line, add these to your statusline
96
96
  command. The first goes wherever it builds its other lines, the second last of
97
97
  all, after everything else it prints:
98
98
 
99
- sluice_line=\$(bash "$RENDER" --dir "\$cwd" 2>/dev/null)
99
+ sluice_line=\$(bash "$RENDER" --dir "\$cwd" --session "\$session_id" 2>/dev/null)
100
100
 
101
101
  if [ -n "\$sluice_line" ]; then printf '%s\\n' "\$sluice_line"; fi
102
102
 
103
- \$cwd is workspace.current_dir from the JSON the harness sends on stdin. Those
104
- two lines carry no knowledge of where a run lives, so they stay correct as this
105
- skill changes; every install updates the script behind them.
103
+ \$cwd is workspace.current_dir and \$session_id is session_id, both from the JSON
104
+ the harness sends on stdin. Those two lines carry no knowledge of where a run
105
+ lives, so they stay correct as this skill changes; every install updates the
106
+ script behind them.
106
107
  EOF
@@ -137,7 +137,7 @@ stamp_baseline "$sid" "$source"
137
137
  [ -d "$cwd" ] || exit 0
138
138
  [ -f "$STATUS" ] || exit 0
139
139
 
140
- shown="$(bash "$STATUS" show --dir "$cwd" 2>/dev/null)" || exit 0
140
+ shown="$(bash "$STATUS" show --dir "$cwd" ${sid:+--session "$sid"} 2>/dev/null)" || exit 0
141
141
  [ -n "$shown" ] || exit 0
142
142
 
143
143
  echo "A sluice run is live. Its state, from .sluice/run.json:"
@@ -43,6 +43,11 @@
43
43
  # neither records anything. Its exit 1 also covers a tree whose changed tests
44
44
  # cannot be read.
45
45
  #
46
+ # --session <id> names the session asking (default: $CLAUDE_CODE_SESSION_ID).
47
+ # `init` records it in the run and leaves <main tree>/.sluice/sessions/<id>
48
+ # naming the tree the run is in, so that session finds its run from the main
49
+ # tree however many other runs the set holds.
50
+ #
46
51
  # A flag value may not begin with `--`: unchecked, an omitted value silently
47
52
  # becomes the next flag and the field holds a flag name.
48
53
 
@@ -95,6 +100,7 @@ in_set() {
95
100
  # subcommand's own flag loop ever sees it. Everything after `--` is verify's
96
101
  # command, kept verbatim: a `--dir` in it belongs to that command, not to us.
97
102
  DIR="$PWD"
103
+ SESSION="${CLAUDE_CODE_SESSION_ID-}"
98
104
  ARGS=()
99
105
  while [ $# -gt 0 ]; do
100
106
  case "$1" in
@@ -108,6 +114,11 @@ while [ $# -gt 0 ]; do
108
114
  DIR="$2"
109
115
  shift 2
110
116
  ;;
117
+ --session)
118
+ need_value --session $# "${2-}"
119
+ SESSION="$2"
120
+ shift 2
121
+ ;;
111
122
  *)
112
123
  ARGS+=("$1")
113
124
  shift
@@ -116,6 +127,12 @@ while [ $# -gt 0 ]; do
116
127
  done
117
128
  set -- "${ARGS[@]+"${ARGS[@]}"}"
118
129
 
130
+ # The id becomes a file name under .sluice/sessions, so one that could climb out
131
+ # of that directory is refused rather than followed.
132
+ case "$SESSION" in
133
+ */* | .*) err "session id $SESSION is not a file name"; exit 4 ;;
134
+ esac
135
+
119
136
  SUB="${1-}"
120
137
  [ $# -gt 0 ] && shift
121
138
  if [ -z "$SUB" ]; then
@@ -132,6 +149,40 @@ main_tree() { # <dir>
132
149
  git -C "$1" worktree list --porcelain 2>/dev/null | sed -n '1s/^worktree //p'
133
150
  }
134
151
 
152
+ # The tree a session's run is in, from the pointer `init` left at the main tree.
153
+ # `run.at` cannot answer this: it is one note for the whole set, and a repo that
154
+ # cuts worktrees by hand has every controller sitting in the main tree at once,
155
+ # each with its own run elsewhere. Followed only while the run there is still
156
+ # that session's, so a pointer that outlived its run never hands over the next
157
+ # one opened in that tree. Empty when there is no such run.
158
+ session_run() { # <main tree>
159
+ [ -n "$SESSION" ] && [ -n "$1" ] && [ -f "$1/.sluice/sessions/$SESSION" ] || return 0
160
+ local at=""
161
+ IFS= read -r at <"$1/.sluice/sessions/$SESSION" 2>/dev/null || true
162
+ [ -n "$at" ] && [ -f "$at/.sluice/run.json" ] || return 0
163
+ [ "$(jq -r '.session // empty' "$at/.sluice/run.json" 2>/dev/null)" = "$SESSION" ] || return 0
164
+ printf '%s\n' "$at"
165
+ }
166
+
167
+ # Like `leave_note`, a pointer that could not be written is a blank statusline
168
+ # for one session rather than a failed command, so the caller reports it.
169
+ point_session() { # <main tree> <session id> <tree the run is in>
170
+ mkdir -p "$1/.sluice/sessions" 2>/dev/null || return 1
171
+ [ -e "$1/.sluice/.gitignore" ] || printf '*\n' >"$1/.sluice/.gitignore" 2>/dev/null || true
172
+ printf '%s\n' "$3" >"$1/.sluice/sessions/$2" 2>/dev/null
173
+ }
174
+
175
+ # The session a run was opened by, read back from the run itself rather than
176
+ # from whoever is asking: `move` and `close` may come from another session.
177
+ run_owner() { # <state file>
178
+ local owner
179
+ owner="$(jq -r '.session // empty' "$1" 2>/dev/null)"
180
+ case "$owner" in
181
+ */* | .*) return 0 ;;
182
+ esac
183
+ printf '%s' "$owner"
184
+ }
185
+
135
186
  # A tree's own run comes first, and only a tree with none reads the set's. Two
136
187
  # layouts share this script and pull opposite ways. A deep run plans in the main
137
188
  # tree and cuts implementer worktrees after the plan: the run directory ignores
@@ -160,8 +211,12 @@ if [ "$SUB" != "init" ] && [ ! -f "$DIR/.sluice/run.json" ] \
160
211
  # package straight past that worktree's run to the main tree's.
161
212
  OWN_TREE="$(git -C "$DIR" rev-parse --show-toplevel 2>/dev/null)"
162
213
  MAIN_TREE="$(main_tree "$DIR")"
214
+ # The session's own run outranks the set's, which may be anybody's.
215
+ SESSION_TREE="$(session_run "${MAIN_TREE:-}")"
163
216
  if [ -n "${OWN_TREE:-}" ] && [ -f "$OWN_TREE/.sluice/run.json" ]; then
164
217
  DIR="$OWN_TREE"
218
+ elif [ -n "$SESSION_TREE" ]; then
219
+ DIR="$SESSION_TREE"
165
220
  elif [ -n "${MAIN_TREE:-}" ] && [ -d "$MAIN_TREE" ]; then
166
221
  DIR="$MAIN_TREE"
167
222
 
@@ -541,12 +596,17 @@ $(cd "$MAIN_ROOT" && pwd -P)"
541
596
  exit 0
542
597
  fi
543
598
 
544
- mk_dir "$TREE/.sluice" "$TREE"
545
- # As in verify: a record git can see moves the tree it records.
546
- git -C "$TREE" check-ignore -q .sluice/tests-changed || {
547
- err "$TREE/.sluice/.gitignore does not ignore tests-changed, so the record would move the tree it records; add * to it"
548
- exit 1
549
- }
599
+ # In the repo's common git dir rather than the tree, so every worktree reads
600
+ # the same records: a test recorded where it was edited still counts in the
601
+ # tree it is merged into. Inside the git dir it is never content of the tree,
602
+ # so it cannot move the tree it records. One file per call, because sessions
603
+ # in several worktrees record at once and bash splits a long append into
604
+ # several writes that interleave. The tree's own .sluice/tests-changed is
605
+ # where records were written before, and is still read.
606
+ COMMON="$(git -C "$TREE" rev-parse --path-format=absolute --git-common-dir 2>/dev/null)"
607
+ [ -n "$COMMON" ] || { err "could not find the git dir of $TREE, so nothing was recorded"; exit 1; }
608
+ RECORDS="$COMMON/sluice/tests-changed"
609
+ mkdir -p "$RECORDS" || { err "could not make $RECORDS"; exit 1; }
550
610
 
551
611
  # Hashed path-aware, so filters apply, the way the stop gate hashes it. A
552
612
  # directory where the file was (a fixture split into many) is that file's
@@ -575,14 +635,16 @@ $(cd "$MAIN_ROOT" && pwd -P)"
575
635
  while IFS= read -r p; do
576
636
  sha="$(current_blob "$p")" || exit 1
577
637
  found=""
578
- if [ -f "$TREE/.sluice/tests-changed" ]; then
638
+ for rec in "$RECORDS"/* "$TREE/.sluice/tests-changed"; do
639
+ [ -f "$rec" ] || continue
579
640
  while IFS=$'\t' read -r rec_sha rec_path rest; do
580
641
  if [ "$rec_sha" = "$sha" ] && [ "$rec_path" = "$p" ]; then
581
642
  found=1
582
643
  break
583
644
  fi
584
- done <"$TREE/.sluice/tests-changed"
585
- fi
645
+ done <"$rec"
646
+ [ -n "$found" ] && break
647
+ done
586
648
  [ -n "$found" ] || UNRECORDED="$UNRECORDED$p"$'\n'
587
649
  done <<<"$FLAGGED"
588
650
  FLAGGED="${UNRECORDED%$'\n'}"
@@ -699,7 +761,13 @@ $(cd "$MAIN_ROOT" && pwd -P)"
699
761
  "
700
762
  COUNT=$((COUNT + 1))
701
763
  done <<<"$FLAGGED"
702
- printf '%s' "$LINES" >>"$TREE/.sluice/tests-changed" || { err "could not write $TREE/.sluice/tests-changed"; exit 1; }
764
+ # Written under a dot name and renamed, so a reader never sees half of it.
765
+ NEW="$(mktemp "$RECORDS/.XXXXXX")" || { err "could not make a record file in $RECORDS"; exit 1; }
766
+ printf '%s' "$LINES" >"$NEW" && mv "$NEW" "$RECORDS/${NEW##*/.}" || {
767
+ rm -f "$NEW"
768
+ err "could not write a record to $RECORDS"
769
+ exit 1
770
+ }
703
771
  echo "sluice: recorded $COUNT changed test(s) as $STATUS"
704
772
  [ -z "$SKIPPED" ] || echo "sluice: not recorded, since no run can prove a deletion: $SKIPPED; record it with --none <reason>"
705
773
  exit 0
@@ -841,12 +909,20 @@ case "$SUB" in
841
909
  --arg channel "$CHANNEL" \
842
910
  --arg started "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
843
911
  --arg plan "$PLAN" \
844
- --arg record "$RECORD" '
912
+ --arg record "$RECORD" \
913
+ --arg session "$SESSION" '
845
914
  {schema: 1, topic: $topic, channel: $channel, started: $started}
846
915
  + (if $plan == "" then {} else {plan: $plan} end)
847
916
  + (if $record == "" then {} else {record: $record} end)
917
+ + (if $session == "" then {} else {session: $session} end)
848
918
  + {tasks: []}
849
- ' | write_state
919
+ ' | write_state || exit $?
920
+
921
+ POINTER_TREE="$(main_tree "$DIR")"
922
+ if [ -n "$SESSION" ] && [ -n "$POINTER_TREE" ] && [ -d "$POINTER_TREE" ] \
923
+ && ! point_session "$POINTER_TREE" "$SESSION" "$(cd "$DIR" && pwd -P)"; then
924
+ err "note: the run is open, but $POINTER_TREE/.sluice/sessions/$SESSION could not be written, so this session reads no run from $POINTER_TREE"
925
+ fi
850
926
  ;;
851
927
 
852
928
  task)
@@ -1263,6 +1339,20 @@ case "$SUB" in
1263
1339
  elif ! leave_note "$NOTE_TREE" "$TO"; then
1264
1340
  err "note: the run moved, but $NOTE_TREE/.sluice/run.at could not be written, so a session in $NOTE_TREE will read no run until it moves to $TO"
1265
1341
  fi
1342
+ # The note above names only the last run moved; the owner's pointer
1343
+ # keeps finding this one after another run is moved past it. Only a
1344
+ # pointer still naming the tree left behind follows the run: one
1345
+ # naming elsewhere is on a later run the same session opened.
1346
+ OWNER="$(run_owner "$DEST_STATE")"
1347
+ if [ -n "$OWNER" ]; then
1348
+ AT=""
1349
+ [ -f "$NOTE_TREE/.sluice/sessions/$OWNER" ] \
1350
+ && IFS= read -r AT <"$NOTE_TREE/.sluice/sessions/$OWNER" 2>/dev/null
1351
+ if { [ -z "$AT" ] || [ "$AT" = "$(cd "$DIR" && pwd -P)" ]; } \
1352
+ && ! point_session "$NOTE_TREE" "$OWNER" "$TO"; then
1353
+ err "note: the run moved, but $NOTE_TREE/.sluice/sessions/$OWNER could not be written"
1354
+ fi
1355
+ fi
1266
1356
  fi
1267
1357
 
1268
1358
  echo "moved $(jq -r '.topic // "run"' "$DEST_STATE" 2>/dev/null || echo run) to $TO"
@@ -1300,6 +1390,7 @@ case "$SUB" in
1300
1390
  # file aside intact rather than leaving it wedged in the tree.
1301
1391
  started="$(jq -r '.started // empty' "$STATE" 2>/dev/null)"
1302
1392
  topic="$(jq -r '.topic // empty' "$STATE" 2>/dev/null)"
1393
+ owner="$(run_owner "$STATE")"
1303
1394
  stamp="$(printf '%s' "$started" | tr -cd '0-9TZ')"
1304
1395
  slug="$(printf '%s' "${topic:-run}" | tr -cs 'A-Za-z0-9._-' '-')"
1305
1396
  [ -n "$stamp" ] || stamp="unknown"
@@ -1328,6 +1419,13 @@ case "$SUB" in
1328
1419
  IFS= read -r AT <"$NOTE_TREE/.sluice/run.at" 2>/dev/null || true
1329
1420
  [ "$AT" = "$(cd "$DIR" && pwd -P)" ] && rm -f "$NOTE_TREE/.sluice/run.at"
1330
1421
  fi
1422
+ # The same holds for the owner's pointer, which may by now name a later
1423
+ # run the same session opened elsewhere.
1424
+ if [ -n "$NOTE_TREE" ] && [ -n "$owner" ] && [ -f "$NOTE_TREE/.sluice/sessions/$owner" ]; then
1425
+ AT=""
1426
+ IFS= read -r AT <"$NOTE_TREE/.sluice/sessions/$owner" 2>/dev/null || true
1427
+ [ "$AT" = "$(cd "$DIR" && pwd -P)" ] && rm -f "$NOTE_TREE/.sluice/sessions/$owner"
1428
+ fi
1331
1429
  # The summary needs parseable state and close is the one command that
1332
1430
  # does not, so an unreadable run still gets a line naming where it went.
1333
1431
  [ -n "$summary" ] || summary="closed $(basename "$dest"): state was unreadable, no summary"
@@ -36,7 +36,13 @@ IFS= read -r -d "" -t 2 input || true
36
36
  # `cwd` is the older spelling of the same field and both are still sent, so it
37
37
  # is a fallback rather than a bug. Neither present is not an error worth a row:
38
38
  # it means this is not the JSON we were written against.
39
- dir="$(printf '%s' "$input" | jq -r '.workspace.current_dir // .cwd // empty' 2>/dev/null || true)"
40
- [ -n "$dir" ] || exit 0
39
+ #
40
+ # One jq for both fields, a line each: the render budget has no room for a
41
+ # second fork, and a newline cannot appear in either value.
42
+ {
43
+ IFS= read -r dir
44
+ IFS= read -r session
45
+ } < <(printf '%s' "$input" | jq -r '(.workspace.current_dir // .cwd // ""), (.session_id // "")' 2>/dev/null || true)
46
+ [ -n "${dir:-}" ] || exit 0
41
47
 
42
- bash "$RENDER" --dir "$dir" 2>/dev/null || exit 0
48
+ bash "$RENDER" --dir "$dir" ${session:+--session "$session"} 2>/dev/null || exit 0
@@ -3,12 +3,13 @@
3
3
  # call. The caller passes the directory the session is in and prints whatever
4
4
  # comes back.
5
5
  #
6
- # statusline.sh [--dir <path>]
6
+ # statusline.sh [--dir <path>] [--session <id>]
7
7
  #
8
- # --dir defaults to $PWD. Always exits 0 in silence on anything it cannot
9
- # render: a status bar draws on every keystroke and has nowhere to put an error,
10
- # so a message here would be permanent clutter rather than a report anyone acts
11
- # on.
8
+ # --dir defaults to $PWD. --session is the harness's session_id, which finds the
9
+ # run that session opened in another worktree while it sits in the main tree.
10
+ # Always exits 0 in silence on anything it cannot render: a status bar draws on
11
+ # every keystroke and has nowhere to put an error, so a message here would be
12
+ # permanent clutter rather than a report anyone acts on.
12
13
  #
13
14
  # Why this exists as its own script rather than as a snippet in the caller.
14
15
  # Whether a run is visible from a given directory is a question about sluice's
@@ -36,6 +37,7 @@ here="$(cd "$(dirname "$0")" && pwd)"
36
37
  STATUS="$here/status.sh"
37
38
 
38
39
  DIR="$PWD"
40
+ SESSION=""
39
41
  while [ $# -gt 0 ]; do
40
42
  case "$1" in
41
43
  --dir)
@@ -44,6 +46,11 @@ while [ $# -gt 0 ]; do
44
46
  DIR="$2"
45
47
  shift 2
46
48
  ;;
49
+ --session)
50
+ [ $# -ge 2 ] && [ "${2#--}" = "$2" ] || exit 0
51
+ SESSION="$2"
52
+ shift 2
53
+ ;;
47
54
  *) exit 0 ;;
48
55
  esac
49
56
  done
@@ -65,11 +72,13 @@ done
65
72
  d="$(cd "$DIR" 2>/dev/null && pwd -P)" || exit 0
66
73
  [ -n "$d" ] || exit 0
67
74
 
68
- # Five answers end the walk. A state file is a run, wherever it was found. So is
75
+ # Six answers end the walk. A state file is a run, wherever it was found. So is
69
76
  # a forwarding note beside where one used to be: the run moved out into a
70
77
  # worktree, this tree is where the session that moved it is still sitting, and
71
78
  # testing only the state file blanked its bar on a live run -- ahead of the
72
- # `.git` directory below, which would otherwise answer for this tree first. A
79
+ # `.git` directory below, which would otherwise answer for this tree first. So
80
+ # is this session's pointer, for a run it opened in a worktree it never
81
+ # entered, which is the same blank bar reached without a `move`. A
73
82
  # `.git` that is a regular file is a linked worktree or a submodule, which may
74
83
  # hold no state of its own and still belong to a set that does, so it is a maybe
75
84
  # and status.sh resolves it. A `.git` that is a directory is the top of an
@@ -83,6 +92,7 @@ d="$(cd "$DIR" 2>/dev/null && pwd -P)" || exit 0
83
92
  while :; do
84
93
  [ -f "$d/.sluice/run.json" ] && break
85
94
  [ -f "$d/.sluice/run.at" ] && break
95
+ [ -n "$SESSION" ] && [ -f "$d/.sluice/sessions/$SESSION" ] && break
86
96
  [ -f "$d/.git" ] && break
87
97
  [ -d "$d/.git" ] && exit 0
88
98
  [ "$d" = "${HOME:-}" ] && exit 0
@@ -100,6 +110,8 @@ done
100
110
  # already coloured: the mapping from state to colour belongs next to the state,
101
111
  # and a caller that applied it would have to re-derive each cell's meaning from
102
112
  # its glyph.
103
- rendered="$(bash "$STATUS" line --full --dir "$DIR" 2>/dev/null)" || exit 0
113
+ # --session only when given, so a caller passing none leaves status.sh its own
114
+ # default rather than an empty override.
115
+ rendered="$(bash "$STATUS" line --full --dir "$DIR" ${SESSION:+--session "$SESSION"} 2>/dev/null)" || exit 0
104
116
  [ -n "$rendered" ] || exit 0
105
117
  printf '%s\n' "$rendered"
@@ -240,7 +240,7 @@ fi
240
240
  # A test edited until it agrees with the new code proves nothing about that
241
241
  # code, and a passing check cannot tell that edit from an honest one. So a
242
242
  # changed existing test (a removed line, a deletion, a skip or focus marker,
243
- # as test-changes.sh decides) needs a line in .sluice/tests-changed naming its
243
+ # as test-changes.sh decides) needs a line in the repo's tests-changed record naming its
244
244
  # current content: either proved red on the session's base by
245
245
  # `status.sh tests-changed -- <cmd>`, or recorded unproven with a reason. An
246
246
  # edit made after its record names other content and is refused again. A new
@@ -261,6 +261,17 @@ test_gate() {
261
261
  flagged="$(bash "$here/test-changes.sh" "$moved_tree" "$base" 2>/dev/null)" || return 0
262
262
  [ -n "$flagged" ] || return 0
263
263
 
264
+ # Records live in the repo's common git dir, one file per status.sh call,
265
+ # shared by every worktree, so a test recorded in the worktree it was edited
266
+ # in still counts once merged here. A record names content, not a tree, so it
267
+ # holds wherever that content is. The tree's own .sluice/tests-changed is
268
+ # where records were written before, and those still stand.
269
+ local common records rec
270
+ common="$(git -C "$moved_tree" rev-parse --path-format=absolute --git-common-dir 2>/dev/null)"
271
+ records=()
272
+ [ -n "$common" ] && records+=("$common/sluice/tests-changed"/*)
273
+ records+=("$moved_tree/.sluice/tests-changed")
274
+
264
275
  missing=""
265
276
  while IFS= read -r p; do
266
277
  if [ -f "$moved_tree/$p" ]; then
@@ -278,14 +289,16 @@ test_gate() {
278
289
  sha="-"
279
290
  fi
280
291
  found=""
281
- if [ -f "$moved_tree/.sluice/tests-changed" ]; then
292
+ for rec in "${records[@]}"; do
293
+ [ -f "$rec" ] || continue
282
294
  while IFS=$'\t' read -r rec_sha rec_path rest; do
283
295
  if [ "$rec_sha" = "$sha" ] && [ "$rec_path" = "$p" ]; then
284
296
  found=1
285
297
  break
286
298
  fi
287
- done <"$moved_tree/.sluice/tests-changed"
288
- fi
299
+ done <"$rec"
300
+ [ -n "$found" ] && break
301
+ done
289
302
  [ -n "$found" ] || missing="$missing$p"$'\n'
290
303
  done <<<"$flagged"
291
304
  [ -n "$missing" ] || return 0
@@ -296,7 +309,7 @@ test_gate() {
296
309
  decision: "block",
297
310
  reason: ("sluice: this session changed existing tests in a way that asks less of the code (an edited or removed assertion, a deleted test, a skip or focus marker), and nothing records why: "
298
311
  + ($paths | split("\n") | map(select(. != "") | gsub("[\u0000-\u001f\u007f]"; "")) | join(", "))
299
- + ". A test bent to fit new code proves nothing about it, so each one needs a record against the session base \($base). Prove it fails on the old code: status.sh tests-changed --because \"<the behaviour the change replaces>\" -- <the suite, scoped to these files> (the run is in a scratch worktree of the base, so a node suite usually needs --with node_modules). If it cannot run on the old code, record why: status.sh tests-changed --because \"<the behaviour the change replaces>\" --none \"<why it cannot run on the old code>\". No run can prove a deleted test, so prove the edited ones first, then record each deletion with --none. --only <path> proves or records just those paths, so a fixture that cannot be proved can go to --none --only <fixture> on its own. Then name each changed test, and why it changed, in your reply.")
312
+ + ". A test bent to fit new code proves nothing about it, so each one needs a record against the session base \($base). Prove it fails on the old code: status.sh tests-changed --because \"<the behaviour the change replaces>\" -- <the suite, scoped to these files> (the run is in a scratch worktree of the base, so a node suite usually needs --with node_modules). If it cannot run on the old code, record why: status.sh tests-changed --because \"<the behaviour the change replaces>\" --none \"<why it cannot run on the old code>\". No run can prove a deleted test, so prove the edited ones first, then record each deletion with --none. --only <path> proves or records just those paths, so a fixture that cannot be proved can go to --none --only <fixture> on its own. Then name each changed test, and why it changed, in your reply. The base is where this tree stood when the session opened, so a test that arrived through a merge of another session into this tree since then is listed too. Do not give a reason for a change that is not yours: record it as what it is, status.sh tests-changed --because \"arrived through the merge of <branch>, not from this session\" --none \"not this session to prove\" --only <path>, and name it in your reply as not from this session.")
300
313
  }'
301
314
  return 1
302
315
  }
@@ -319,10 +332,30 @@ fi
319
332
  # which is the right question for a render and the wrong one for a refusal. The
320
333
  # tree named in the note is then passed as `--dir`, so what follows asks about
321
334
  # one named tree and carries no layout knowledge of its own.
335
+ #
336
+ # The run this session opened in a worktree it never entered is its own too,
337
+ # found through the pointer `init` left at the main tree, and it comes ahead of
338
+ # the forward: the forward names whichever run was moved last, which with
339
+ # several controllers in the main tree may be another session's.
322
340
  top="$(git -C "$cwd" rev-parse --show-toplevel 2>/dev/null)"
323
341
  [ -n "$top" ] || top="$cwd"
342
+ session_tree=""
343
+ case "$sid" in
344
+ "" | */* | .*) ;;
345
+ *)
346
+ main="$(git -C "$cwd" worktree list --porcelain 2>/dev/null | sed -n '1s/^worktree //p')"
347
+ [ -n "$main" ] && [ -f "$main/.sluice/sessions/$sid" ] \
348
+ && IFS= read -r session_tree <"$main/.sluice/sessions/$sid" 2>/dev/null
349
+ if [ -z "$session_tree" ] || [ ! -f "$session_tree/.sluice/run.json" ] \
350
+ || [ "$(jq -r '.session // empty' "$session_tree/.sluice/run.json" 2>/dev/null)" != "$sid" ]; then
351
+ session_tree=""
352
+ fi
353
+ ;;
354
+ esac
324
355
  if [ -f "$top/.sluice/run.json" ]; then
325
356
  tree="$top"
357
+ elif [ -n "$session_tree" ]; then
358
+ tree="$session_tree"
326
359
  elif [ -f "$top/.sluice/run.at" ]; then
327
360
  tree=""
328
361
  IFS= read -r tree <"$top/.sluice/run.at" 2>/dev/null || exit 0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sluice",
3
- "version": "0.25.2",
3
+ "version": "0.25.4",
4
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 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.",
5
5
  "author": "iceinvein",
6
6
  "type": "prompt",