@iceinvein/agent-skills 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -20,7 +20,7 @@ var __toESM = (mod, isNodeMode, target) => {
20
20
  return cached;
21
21
  }
22
22
  target = mod != null ? __create(__getProtoOf(mod)) : {};
23
- const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
23
+ const to = isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
24
24
  if (mod && typeof mod === "object" || typeof mod === "function") {
25
25
  for (let key of __getOwnPropNames(mod))
26
26
  if (!__hasOwnProp.call(to, key))
@@ -1032,9 +1032,9 @@ async function removeSkill(cwd, skillName) {
1032
1032
  const warnings = [];
1033
1033
  for (const file of entry.files) {
1034
1034
  if (isSharedConfigFile(file)) {
1035
- const fullPath2 = join7(cwd, file);
1036
- if (existsSync6(fullPath2)) {
1037
- await unwireSessionStartHook(fullPath2, skillName);
1035
+ const fullPath = join7(cwd, file);
1036
+ if (existsSync6(fullPath)) {
1037
+ await unwireSessionStartHook(fullPath, skillName);
1038
1038
  }
1039
1039
  warnings.push(`Manifest unavailable (${manifestResult.error}). Left '${file}' in place, only removing '${skillName}'s SessionStart hook from it; any MCP server entries it owns were not touched.`);
1040
1040
  continue;
@@ -1125,9 +1125,9 @@ async function removeSkill2(cwd, skillName) {
1125
1125
  const warnings = [];
1126
1126
  for (const file of entry.files) {
1127
1127
  if (isSharedConfigFile2(file)) {
1128
- const fullPath2 = join8(cwd, file);
1129
- if (existsSync7(fullPath2)) {
1130
- await unwireSessionStartHook(fullPath2, skillName);
1128
+ const fullPath = join8(cwd, file);
1129
+ if (existsSync7(fullPath)) {
1130
+ await unwireSessionStartHook(fullPath, skillName);
1131
1131
  }
1132
1132
  warnings.push(`Manifest unavailable (${manifestResult.error}). Left '${file}' in place, only removing '${skillName}'s SessionStart hook from it; any MCP server entries it owns were not touched.`);
1133
1133
  continue;
@@ -1518,7 +1518,7 @@ function isFactory(value) {
1518
1518
  }
1519
1519
  function useState(defaultValue) {
1520
1520
  return withPointer((pointer) => {
1521
- const setState = AsyncResource2.bind(function setState2(newValue) {
1521
+ const setState = AsyncResource2.bind(function setState(newValue) {
1522
1522
  if (pointer.get() !== newValue) {
1523
1523
  pointer.set(newValue);
1524
1524
  handleChange();
@@ -2387,7 +2387,7 @@ if (process.platform === "linux") {
2387
2387
  }
2388
2388
 
2389
2389
  // node_modules/signal-exit/dist/mjs/index.js
2390
- var processOk = (process3) => !!process3 && typeof process3 === "object" && typeof process3.removeListener === "function" && typeof process3.emit === "function" && typeof process3.reallyExit === "function" && typeof process3.listeners === "function" && typeof process3.kill === "function" && typeof process3.pid === "number" && typeof process3.on === "function";
2390
+ var processOk = (process2) => !!process2 && typeof process2 === "object" && typeof process2.removeListener === "function" && typeof process2.emit === "function" && typeof process2.reallyExit === "function" && typeof process2.listeners === "function" && typeof process2.kill === "function" && typeof process2.pid === "number" && typeof process2.on === "function";
2391
2391
  var kExitEmitter = Symbol.for("signal-exit emitter");
2392
2392
  var global = globalThis;
2393
2393
  var ObjectDefineProperty = Object.defineProperty.bind(Object);
@@ -2477,15 +2477,15 @@ class SignalExit extends SignalExitBase {
2477
2477
  #originalProcessReallyExit;
2478
2478
  #sigListeners = {};
2479
2479
  #loaded = false;
2480
- constructor(process3) {
2480
+ constructor(process2) {
2481
2481
  super();
2482
- this.#process = process3;
2482
+ this.#process = process2;
2483
2483
  this.#sigListeners = {};
2484
2484
  for (const sig of signals) {
2485
2485
  this.#sigListeners[sig] = () => {
2486
2486
  const listeners = this.#process.listeners(sig);
2487
2487
  let { count } = this.#emitter;
2488
- const p = process3;
2488
+ const p = process2;
2489
2489
  if (typeof p.__signal_exit_emitter__ === "object" && typeof p.__signal_exit_emitter__.count === "number") {
2490
2490
  count += p.__signal_exit_emitter__.count;
2491
2491
  }
@@ -2494,12 +2494,12 @@ class SignalExit extends SignalExitBase {
2494
2494
  const ret = this.#emitter.emit("exit", null, sig);
2495
2495
  const s = sig === "SIGHUP" ? this.#hupSig : sig;
2496
2496
  if (!ret)
2497
- process3.kill(process3.pid, s);
2497
+ process2.kill(process2.pid, s);
2498
2498
  }
2499
2499
  };
2500
2500
  }
2501
- this.#originalProcessReallyExit = process3.reallyExit;
2502
- this.#originalProcessEmit = process3.emit;
2501
+ this.#originalProcessReallyExit = process2.reallyExit;
2502
+ this.#originalProcessEmit = process2.emit;
2503
2503
  }
2504
2504
  onExit(cb, opts) {
2505
2505
  if (!processOk(this.#process)) {
@@ -2725,8 +2725,8 @@ function createPrompt(view) {
2725
2725
  signal.addEventListener("abort", abort);
2726
2726
  cleanups.add(() => signal.removeEventListener("abort", abort));
2727
2727
  }
2728
- cleanups.add(onExit((code, signal2) => {
2729
- reject(new ExitPromptError(`User force closed the prompt with ${code} ${signal2}`));
2728
+ cleanups.add(onExit((code, signal) => {
2729
+ reject(new ExitPromptError(`User force closed the prompt with ${code} ${signal}`));
2730
2730
  }));
2731
2731
  const sigint = () => reject(new ExitPromptError(`User force closed the prompt with SIGINT`));
2732
2732
  rl.on("SIGINT", sigint);
@@ -2847,12 +2847,12 @@ function normalizeChoices(choices) {
2847
2847
  if (Separator.isSeparator(choice))
2848
2848
  return choice;
2849
2849
  if (typeof choice !== "object" || choice === null || !("value" in choice)) {
2850
- const name2 = String(choice);
2850
+ const name = String(choice);
2851
2851
  return {
2852
2852
  value: choice,
2853
- name: name2,
2854
- short: name2,
2855
- checkedName: name2,
2853
+ name,
2854
+ short: name,
2855
+ checkedName: name,
2856
2856
  disabled: false,
2857
2857
  checked: false
2858
2858
  };
@@ -2957,8 +2957,8 @@ var dist_default4 = createPrompt((config, done) => {
2957
2957
  const cursor = isActive ? theme.icon.cursor : " ";
2958
2958
  if (item.disabled) {
2959
2959
  const disabledLabel = typeof item.disabled === "string" ? item.disabled : "(disabled)";
2960
- const checkbox2 = item.checked ? theme.icon.disabledChecked : theme.icon.disabledUnchecked;
2961
- return theme.style.disabled(`${cursor}${checkbox2} ${item.name} ${disabledLabel}`);
2960
+ const checkbox = item.checked ? theme.icon.disabledChecked : theme.icon.disabledUnchecked;
2961
+ return theme.style.disabled(`${cursor}${checkbox} ${item.name} ${disabledLabel}`);
2962
2962
  }
2963
2963
  if (isActive) {
2964
2964
  description = item.description;
@@ -3021,11 +3021,11 @@ function normalizeChoices2(choices) {
3021
3021
  if (Separator.isSeparator(choice))
3022
3022
  return choice;
3023
3023
  if (typeof choice !== "object" || choice === null || !("value" in choice)) {
3024
- const name2 = String(choice);
3024
+ const name = String(choice);
3025
3025
  return {
3026
3026
  value: choice,
3027
- name: name2,
3028
- short: name2,
3027
+ name,
3028
+ short: name,
3029
3029
  disabled: false
3030
3030
  };
3031
3031
  }
@@ -3095,8 +3095,8 @@ var dist_default5 = createPrompt((config, done) => {
3095
3095
  } else if (isNumberKey(key) && !Number.isNaN(Number(rl.line))) {
3096
3096
  const selectedIndex = Number(rl.line) - 1;
3097
3097
  let selectableIndex = -1;
3098
- const position = items.findIndex((item2) => {
3099
- if (Separator.isSeparator(item2))
3098
+ const position = items.findIndex((item) => {
3099
+ if (Separator.isSeparator(item))
3100
3100
  return false;
3101
3101
  selectableIndex++;
3102
3102
  return selectableIndex === selectedIndex;
@@ -3477,10 +3477,10 @@ Installing ${names.length} skills...
3477
3477
  }
3478
3478
  const other = await otherScopeSkillCount(updateDir);
3479
3479
  if (other.count > 0) {
3480
- const command2 = other.scope === "global" ? "agent-skills update --all -g" : "agent-skills update --all";
3480
+ const command = other.scope === "global" ? "agent-skills update --all -g" : "agent-skills update --all";
3481
3481
  const noun = other.count === 1 ? "skill" : "skills";
3482
3482
  console.log(`
3483
- Note: ${other.count} ${noun} installed in the ${other.scope} scope. Run \`${command2}\` to update those too.`);
3483
+ Note: ${other.count} ${noun} installed in the ${other.scope} scope. Run \`${command}\` to update those too.`);
3484
3484
  }
3485
3485
  break;
3486
3486
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iceinvein/agent-skills",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "Install agent skills into AI coding tools",
5
5
  "author": "iceinvein",
6
6
  "license": "MIT",
package/skills/index.json CHANGED
@@ -283,7 +283,7 @@
283
283
  "name": "sluice",
284
284
  "description": "Routes work by change shape into four channels (bypass, fast, main, deep) and applies only the rules each channel needs, so a one-line fix does not pay the cost of a multi-subsystem build. Carries seven rules as one-liners in the router and the full treatment in references read only on friction. Checks the finished plan with plan.sh validate rather than trusting it to memory, seeds the run state from it, keeps a deep run's task breakdown in .sluice/run.json so a statusline segment and one status command can answer where the run is, 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.13.0"
286
+ "version": "0.14.0"
287
287
  },
288
288
  {
289
289
  "name": "temporal-coupling-detector",
@@ -285,7 +285,9 @@ reads the run state rather than the plan: it sees what has actually landed.
285
285
  - Isolate the workspace before a multi-task plan: the harness's worktree
286
286
  tool, not `git worktree` yourself. Implementing straight onto main or
287
287
  master needs your partner's say-so, which pre-flight is where you got, and
288
- it forecloses concurrent implementers for the whole run.
288
+ it forecloses concurrent implementers for the whole run. The run state
289
+ follows the set rather than the tree, so a worktree cut after the plan still
290
+ reads the rows the plan seeded and flips them where you are watching.
289
291
  - **The agent that built the task commits it**, once its own tests pass, and
290
292
  only the paths in its `Touches`. Never `git add -A`: the tree is shared, and
291
293
  on a branch you did not isolate it holds work that is not this task's. The
@@ -7,7 +7,7 @@ session that is running it: your partner has to ask, and asking costs a turn
7
7
  and gets an answer from memory.
8
8
 
9
9
  `.sluice/run.json` is that answer in a form something else can read. One file
10
- per tree, holding only what changes as the run moves.
10
+ per worktree set, holding only what changes as the run moves.
11
11
 
12
12
  ```
13
13
  bash <skill-dir>/scripts/status.sh init --topic <t> --channel deep \
@@ -24,7 +24,9 @@ bash <skill-dir>/scripts/status.sh line --full
24
24
  bash <skill-dir>/scripts/status.sh close
25
25
  ```
26
26
 
27
- `--dir <path>` reads another tree, which is what the statusline uses. Statuses
27
+ `--dir <path>` reads another tree, which is what the statusline uses; it names
28
+ a tree in the set rather than a state file, so a worktree resolves to the same
29
+ run as the tree it was cut from. Statuses
28
30
  are `todo`, `active`, `review`, `done` and `blocked`. A new id needs `--name`;
29
31
  after that every call is a bare flip, so keeping it current costs one command
30
32
  per transition rather than a paragraph. `close` archives the run under
@@ -42,6 +44,31 @@ everything durable in it lands somewhere else anyway: the commits are in git and
42
44
  the reasons are in the record, which is the file that does get committed. Delete
43
45
  that `.gitignore` if you want a run tracked; it is only written when absent.
44
46
 
47
+ ## Worktrees
48
+
49
+ Ignoring itself is what makes the run invisible to a worktree unless something
50
+ is done about it, and a `deep` run makes worktrees after the plan is written:
51
+ `git worktree add` gives the implementer a clean checkout, and an ignored
52
+ directory is not in a checkout. Read from the tree it was called in, the run
53
+ the plan seeded would be absent from every implementer, `init` there would
54
+ start a second run nothing else reads, and the worktree would take that state
55
+ with it when it went.
56
+
57
+ So every command anchors on the main worktree of whatever tree it is pointed
58
+ at, and one run covers the set. The statusline renders the same run in every
59
+ window, a flip made by an implementer lands where the controller is watching,
60
+ and `init` from a worktree reports the run that is already live rather than
61
+ replacing it. A submodule anchors on its own checkout, not the superproject's,
62
+ and a directory that is no git work tree keeps its run exactly where it sits.
63
+
64
+ One file for several writers is one file to contend on, so `init`, `task`,
65
+ `preflight` and `close` take a lock first: four implementers each flipping
66
+ their own row would otherwise have the later write built on a snapshot taken
67
+ before the earlier one landed, dropping that row without saying so. The lock
68
+ carries its holder's pid, so a killed run is broken through rather than waited
69
+ out. Reads take nothing, state being installed through a rename, which is what
70
+ keeps `line` cheap enough to render on.
71
+
45
72
  Open it with `init` when you open the run record, at the same point and for the
46
73
  same reason, then seed the rows with `plan.sh import <plan>` rather than a
47
74
  command per task. The ids, names, the flip, the `Model` marks and the tiers are
@@ -169,7 +196,7 @@ state file existing so a session with no run spawns no process at all:
169
196
 
170
197
  ```bash
171
198
  sluice_line=""
172
- if [ -n "$cwd" ] && [ -f "$cwd/.sluice/run.json" ]; then
199
+ if [ -n "$cwd" ] && { [ -f "$cwd/.sluice/run.json" ] || [ -f "$cwd/.git" ]; }; then
173
200
  for sluice_sh in "$cwd/.claude/skills/sluice/scripts/status.sh" \
174
201
  "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/skills/sluice/scripts/status.sh" \
175
202
  "$HOME/.claude/skills/sluice/scripts/status.sh"; do
@@ -186,6 +213,12 @@ then print it last, after whatever else the command emits:
186
213
  if [ -n "$sluice_line" ]; then printf '%s\n' "$sluice_line"; fi
187
214
  ```
188
215
 
216
+ The gate is two tests because a linked worktree holds no state file of its own:
217
+ there `.git` is a regular file naming the tree it was cut from, and the script
218
+ resolves the run from it. In a tree with no run and no worktree behind it `.git`
219
+ is a directory, so both tests fail and no process is spawned, which is the
220
+ property the gate is for.
221
+
189
222
  `if` rather than `[ ... ] &&`: as the last command of a statusline script the
190
223
  short form makes it exit 1 on every render with no run live, which is the common
191
224
  case. `%s` rather than `%b`: the render already carries real escape bytes, and
@@ -92,8 +92,27 @@ if [ -z "$SUB" ]; then
92
92
  exit 4
93
93
  fi
94
94
 
95
+ # A linked worktree is another view of the same run, not a new one. The run
96
+ # directory ignores itself, so `git worktree add` never carries it across: read
97
+ # from the tree it was called in, the run a plan seeded is absent from every
98
+ # implementer created after that plan, and an `init` there lands a rival state
99
+ # file that dies with the worktree. Every tree in a set anchors on the main
100
+ # worktree instead, which is the one path all of them agree on.
101
+ #
102
+ # `git worktree list` names the main worktree first. A submodule names its own
103
+ # checkout there rather than the superproject's, which is what keeps a
104
+ # submodule's run beside its own working tree, and a directory that is no git
105
+ # work tree at all is left exactly as it was given.
106
+ if [ "$(git -C "$DIR" rev-parse --is-inside-work-tree 2>/dev/null)" = "true" ]; then
107
+ MAIN_TREE="$(git -C "$DIR" worktree list --porcelain 2>/dev/null | sed -n '1s/^worktree //p')"
108
+ if [ -n "${MAIN_TREE:-}" ] && [ -d "$MAIN_TREE" ]; then
109
+ DIR="$MAIN_TREE"
110
+ fi
111
+ fi
112
+
95
113
  STATE="$DIR/.sluice/run.json"
96
114
  ARCHIVE="$DIR/.sluice/archive"
115
+ LOCK="$DIR/.sluice/run.lock"
97
116
 
98
117
  # `line` swallows everything: a missing jq, unreadable state, no run at all.
99
118
  # Any of those printing would put permanent clutter in the status bar.
@@ -252,6 +271,45 @@ write_state() {
252
271
  mv "$tmp" "$STATE" || { rm -f "$tmp"; err "could not replace $STATE"; exit 1; }
253
272
  }
254
273
 
274
+ # One state file now serves a whole worktree set, so two implementers can flip
275
+ # their own task at the same moment. A flip reads the whole file, edits it with
276
+ # jq and writes it back, so unserialised the later write is built on a snapshot
277
+ # taken before the earlier one landed and drops that row without saying so.
278
+ # mkdir is the atomic primitive every platform this runs on has; flock is Linux
279
+ # only. Reads do not take it: write_state installs through a rename, so a reader
280
+ # sees either the whole old file or the whole new one.
281
+ #
282
+ # The holder's pid goes inside the directory so a killed run cannot wedge every
283
+ # later one. A lock whose holder is gone is broken rather than waited out, and
284
+ # one whose holder is alive is waited on for a bounded time and then reported,
285
+ # because a command that hangs in a status bar is worse than one that fails.
286
+ lock_taken=0
287
+ release_lock() {
288
+ [ "$lock_taken" -eq 1 ] || return 0
289
+ rm -rf "$LOCK"
290
+ lock_taken=0
291
+ }
292
+
293
+ take_lock() {
294
+ local waited=0 holder
295
+ while ! mkdir "$LOCK" 2>/dev/null; do
296
+ holder="$(cat "$LOCK/pid" 2>/dev/null)"
297
+ if [ -n "$holder" ] && ! kill -0 "$holder" 2>/dev/null; then
298
+ rm -rf "$LOCK"
299
+ continue
300
+ fi
301
+ if [ "$waited" -ge 100 ]; then
302
+ err "another sluice command has held $LOCK for 10s; remove it if nothing is running"
303
+ exit 1
304
+ fi
305
+ sleep 0.1
306
+ waited=$((waited + 1))
307
+ done
308
+ printf '%s\n' "$$" >"$LOCK/pid" 2>/dev/null || true
309
+ lock_taken=1
310
+ trap release_lock EXIT INT TERM
311
+ }
312
+
255
313
  case "$SUB" in
256
314
  init)
257
315
  TOPIC="" CHANNEL="" PLAN="" RECORD="" FORCE=0
@@ -269,13 +327,14 @@ case "$SUB" in
269
327
  [ -n "$CHANNEL" ] || { err "init needs --channel"; exit 4; }
270
328
  in_set "$CHANNEL" "$CHANNELS" || { err "unknown channel: $CHANNEL (one of: $CHANNELS)"; exit 4; }
271
329
 
330
+ mk_dir "$DIR/.sluice"
331
+ take_lock
272
332
  if [ -f "$STATE" ] && [ "$FORCE" -eq 0 ]; then
273
333
  live="$(jq -r '.topic // "?"' "$STATE" 2>/dev/null || echo "?")"
274
334
  err "a run is already live (topic: $live); pass --force to replace it"
275
335
  exit 3
276
336
  fi
277
337
 
278
- mk_dir "$DIR/.sluice"
279
338
  jq -n \
280
339
  --arg topic "$TOPIC" \
281
340
  --arg channel "$CHANNEL" \
@@ -332,6 +391,7 @@ case "$SUB" in
332
391
  fi
333
392
 
334
393
  require_run
394
+ take_lock
335
395
  require_readable
336
396
 
337
397
  # A row with no name is a number nobody can act on, so a new id has to
@@ -404,6 +464,7 @@ case "$SUB" in
404
464
  exit 4
405
465
  fi
406
466
  require_run
467
+ take_lock
407
468
  require_readable
408
469
 
409
470
  jq --arg review "$REVIEW" --arg model "$MODEL" --arg workspace "$WORKSPACE" '
@@ -528,6 +589,7 @@ case "$SUB" in
528
589
  close)
529
590
  [ $# -eq 0 ] || { err "close takes no arguments"; exit 4; }
530
591
  require_run
592
+ take_lock
531
593
 
532
594
  # Deliberately not `require_readable`. The parse error every other
533
595
  # subcommand raises names close as the way out, so close is the one
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sluice",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
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 seven rules as one-liners in the router and the full treatment in references read only on friction. Checks the finished plan with plan.sh validate rather than trusting it to memory, seeds the run state from it, keeps a deep run's task breakdown in .sluice/run.json so a statusline segment and one status command can answer where the run is, 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",