@spardutti/claude-skills 2.64.0 → 2.66.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -223,6 +223,7 @@ scripts/ validate-skills.mjs — checks skill length caps and reference in
223
223
  version-check.sh — SessionStart nudge when a newer catalog is published
224
224
  skill-flags-record.sh — logs each /ship skills audit and /code-review; skill-report.mjs tallies them
225
225
  edit-check.sh — PostToolUse check on each edited file: line-limit.sh, lint-ratchet.mjs, .edit-rules.json
226
+ bash-check.sh — PostToolUse check on files a shell command changed: runs edit-check.sh on each
226
227
  diary-check.mjs — run at the end of /ship; says when the diaries hold enough for /add-rule
227
228
  registry.mjs — the *.registry.yaml checks for /plan-feature and /registry
228
229
  line-limit.sh — the 200-line ratchet, shared by the edit check, the ship gate and preflight
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env bash
2
+
3
+ INPUT=$(cat)
4
+ HERE="$(cd "$(dirname "$0")" && pwd)"
5
+ PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$PWD}"
6
+ TOP=$(git -C "$PROJECT_DIR" rev-parse --show-toplevel 2>/dev/null) || exit 0
7
+ PREFIX=$(git -C "$PROJECT_DIR" rev-parse --show-prefix)
8
+ export GIT_OPTIONAL_LOCKS=0
9
+
10
+ changed_files() {
11
+ { git -c core.quotepath=off diff --name-only --relative HEAD 2>/dev/null
12
+ git -c core.quotepath=off ls-files --others --exclude-standard
13
+ } | while IFS= read -r f; do [ -f "$f" ] && printf '%s\n' "$f"; done | sort -u
14
+ }
15
+
16
+ # The seen list judges each file version once, so a known failure does not block every later command.
17
+ check_dir() (
18
+ cd "$1" || exit 0
19
+ local seen paths now fresh f hashes
20
+ seen=$(git rev-parse --git-path claude-bash-check-seen)
21
+ paths=$(changed_files)
22
+ [ -n "$paths" ] && hashes=$(printf '%s\n' "$paths" | git hash-object --stdin-paths)
23
+ now=$([ -n "$paths" ] && paste -d' ' <(printf '%s\n' "$hashes") <(printf '%s\n' "$paths"))
24
+ fresh=$(printf '%s\n' "$now" | grep -Fxvf "$([ -f "$seen" ] && echo "$seen" || echo /dev/null)")
25
+ printf '%s\n' "$now" > "$seen"
26
+ printf '%s\n' "$fresh" | while read -r _ f; do
27
+ [ -n "$f" ] || continue
28
+ printf '{"tool_input":{"file_path":"%s/%s"}}' "$PWD" "$f" \
29
+ | CLAUDE_PROJECT_DIR="$PWD" bash "$HERE/edit-check.sh" 2>&1 >/dev/null
30
+ done
31
+ )
32
+
33
+ # A worktree counts only when this command names it, so another session's files never block this one.
34
+ FAILS=""
35
+ while IFS= read -r wt; do
36
+ case "$INPUT $TOP" in *"$wt"*) ;; *) continue ;; esac
37
+ dir="$wt/$PREFIX"
38
+ [ -d "$dir" ] && FAILS="$FAILS$(check_dir "$dir")"
39
+ done < <(git -C "$TOP" worktree list --porcelain | sed -n 's/^worktree //p')
40
+
41
+ [ -z "$FAILS" ] && exit 0
42
+ printf 'A shell command changed files that break the edit rules:\n%s\n' "$FAILS" >&2
43
+ exit 2
@@ -77,10 +77,11 @@ export function triggerHits(entry, text) {
77
77
  return entry.triggers.filter((t) => new RegExp(`\\b${escape(t)}\\b`, "i").test(text));
78
78
  }
79
79
 
80
- // A location is a folder or one file, like sim/aim.gd beside unrelated systems in sim/.
80
+ export const locationsOf = (entry) => [].concat(entry.location ?? []).map((l) => l.replace(/^\.\//, "")).filter(Boolean);
81
+
82
+ // A location is a folder or files, like sim/aim.gd beside unrelated systems in sim/.
81
83
  export function insideLocation(entry, file) {
82
- const loc = (entry.location ?? "").replace(/^\.\//, "");
83
- return loc !== "" && (file === loc || file.startsWith(loc.replace(/\/?$/, "/")));
84
+ return locationsOf(entry).some((loc) => file === loc || file.startsWith(loc.replace(/\/?$/, "/")));
84
85
  }
85
86
 
86
87
  export const isAllowed = (entry, file) => entry.allowed.some((a) => (a.endsWith("/") ? file.startsWith(a) : file === a));
@@ -2,7 +2,7 @@
2
2
  import { existsSync, readFileSync, statSync } from "node:fs";
3
3
  import { dirname, join, relative, resolve } from "node:path";
4
4
  import {
5
- REGISTRY_SUFFIX, addedText, escape, insideLocation, isAllowed, loadRegistry, planMarks, projectFiles, projectRoot, symbolsOf, triggerHits,
5
+ REGISTRY_SUFFIX, addedText, escape, insideLocation, isAllowed, loadRegistry, locationsOf, planMarks, projectFiles, projectRoot, symbolsOf, triggerHits,
6
6
  } from "./registry-lib.mjs";
7
7
  import { loadEditRules } from "./text-rules.mjs";
8
8
 
@@ -61,7 +61,7 @@ function evaluate() {
61
61
  const outside = files.filter((f) => !insideLocation(e, f) && !isAllowed(e, f) && !f.endsWith(REGISTRY_SUFFIX));
62
62
  const hits = outside.flatMap((f) => readFileSync(join(root, f), "utf8").split("\n").flatMap((line, i) =>
63
63
  e.does.filter((code) => calls(line, code)).map((code) => `registry SUSPECT ${e.name} ${f}:${i + 1} ${code}`)));
64
- console.log(hits.length ? hits.join("\n") : `registry eval ${e.name}: nothing outside ${e.location} does its job`);
64
+ console.log(hits.length ? hits.join("\n") : `registry eval ${e.name}: nothing outside ${locationsOf(e).join(", ")} does its job`);
65
65
  }
66
66
  }
67
67
 
@@ -82,16 +82,30 @@ function stale() {
82
82
  if (/^\s+-\s/m.test(e.text)) say("write lists on one line, like triggers: [aim, target]");
83
83
  if (seen.has(e.name)) say(`name ${e.name} is used twice`);
84
84
  seen.add(e.name);
85
+ staleLocation(e, say);
85
86
  staleCode(e, say);
86
87
  }
87
88
  if (problems.length) fail(problems);
88
89
  }
89
90
 
91
+ // A folder wide enough to hold another system hides that folder from eval.
92
+ function staleLocation(e, say) {
93
+ const bare = (l) => l.replace(/\/$/, "");
94
+ const own = locationsOf(e).map(bare);
95
+ for (const other of entries) {
96
+ if (other === e) continue;
97
+ const held = locationsOf(other).map(bare).filter((l) => !own.includes(l) && insideLocation(e, l));
98
+ if (held.length) say(`location holds ${other.name}'s ${held.join(", ")} — list this system's own files`);
99
+ }
100
+ }
101
+
90
102
  function staleCode(e, say) {
91
- const loc = join(root, e.location ?? "");
92
- if (!e.location || !existsSync(loc)) return say(`location ${e.location} not found`);
93
- const near = statSync(loc).isDirectory() ? loc : dirname(loc);
94
- if (e.example && !existsSync(join(near, e.example)) && !existsSync(join(root, e.example))) say(`example ${e.example} not found`);
103
+ const locs = locationsOf(e);
104
+ const where = locs.join(", ");
105
+ const gone = locs.filter((l) => !existsSync(join(root, l)));
106
+ if (!locs.length || gone.length) return say(`location ${gone.join(", ") || e.location} not found`);
107
+ const near = locs.map((l) => (statSync(join(root, l)).isDirectory() ? join(root, l) : dirname(join(root, l))));
108
+ if (e.example && !near.some((n) => existsSync(join(n, e.example))) && !existsSync(join(root, e.example))) say(`example ${e.example} not found`);
95
109
  const syms = symbolsOf(e.extend_by);
96
110
  if (!syms.length) say("extend_by names no symbol — write names like `AimStrategy`");
97
111
  const code = files.filter((f) => insideLocation(e, f) && !f.endsWith(REGISTRY_SUFFIX))
@@ -99,9 +113,9 @@ function staleCode(e, say) {
99
113
  for (const s of syms) {
100
114
  // A plain word like `first` matches `var first = enemies[0]`; `Aim.first` only matches real use.
101
115
  if (/^[a-z][a-z0-9]*$/.test(s)) say(`\`${s}\` is too common a word to prove use — name its owner, like \`Owner.${s}\``);
102
- else if (!s.split(".").every((part) => uses(code, part))) say(`${s} from extend_by is not found in ${e.location}`);
116
+ else if (!s.split(".").every((part) => uses(code, part))) say(`${s} from extend_by is not found in ${where}`);
103
117
  }
104
- for (const c of e.does) if (!calls(code, c)) say(`does ${c} is not found in ${e.location}`);
118
+ for (const c of e.does) if (!calls(code, c)) say(`does ${c} is not found in ${where}`);
105
119
  }
106
120
 
107
121
  function fail(lines) {
@@ -13,6 +13,7 @@ const SHIP_GATE_HOOK_FILENAME = "ship-gate-hook.sh";
13
13
  const VERSION_CHECK_FILENAME = "version-check.sh";
14
14
  const SKILL_FLAGS_FILENAME = "skill-flags-record.sh";
15
15
  const EDIT_CHECK_FILENAME = "edit-check.sh";
16
+ const BASH_CHECK_FILENAME = "bash-check.sh";
16
17
  const LEGACY_EVAL_FILENAME = "skill-forced-eval-hook.sh";
17
18
  const LEGACY_AUDIT_RUNNER_FILENAME = "skill-audit-runner.sh";
18
19
 
@@ -103,6 +104,12 @@ export async function registerHooks(settingsPath) {
103
104
  };
104
105
  upsert(settings.hooks.PostToolUse, EDIT_CHECK_FILENAME, editCheckEntry);
105
106
 
107
+ const bashCheckEntry = {
108
+ matcher: "Bash",
109
+ hooks: [{ type: "command", command: `$CLAUDE_PROJECT_DIR/.claude/hooks/${BASH_CHECK_FILENAME}` }],
110
+ };
111
+ upsert(settings.hooks.PostToolUse, BASH_CHECK_FILENAME, bashCheckEntry);
112
+
106
113
  // Register SessionStart version check. No matcher: startup and resume both
107
114
  // want it, and the script's own daily stamp keeps a burst of resumes quiet.
108
115
  const versionCheckCommand = `$CLAUDE_PROJECT_DIR/.claude/hooks/${VERSION_CHECK_FILENAME}`;
@@ -31,6 +31,7 @@ import { homedir } from "node:os";
31
31
  // and finding, one file per ship, in one folder
32
32
  // edit-check.sh PostToolUse — line-limit.sh, lint-ratchet.mjs and
33
33
  // text-rules.mjs on each edited file
34
+ // bash-check.sh PostToolUse on Bash — edit-check.sh on files a command changed
34
35
  // registry.mjs run by /plan-feature and /registry — the *.registry.yaml checks
35
36
 
36
37
  const GATE_FILENAME = "skill-gate.sh";
@@ -47,6 +48,7 @@ const VERSION_CHECK_FILENAME = "version-check.sh";
47
48
  const SKILL_FLAGS_FILENAME = "skill-flags-record.sh";
48
49
  const SKILL_REPORT_FILENAME = "skill-report.mjs";
49
50
  const EDIT_CHECK_FILENAME = "edit-check.sh";
51
+ const BASH_CHECK_FILENAME = "bash-check.sh";
50
52
  const LINE_LIMIT_FILENAME = "line-limit.sh";
51
53
  const LINT_RATCHET_FILENAME = "lint-ratchet.mjs";
52
54
  const TEXT_RULES_FILENAME = "text-rules.mjs";
@@ -157,6 +159,7 @@ export async function setupHook(targetDir = process.cwd()) {
157
159
  SKILL_FLAGS_FILENAME,
158
160
  SKILL_REPORT_FILENAME,
159
161
  EDIT_CHECK_FILENAME,
162
+ BASH_CHECK_FILENAME,
160
163
  LINE_LIMIT_FILENAME,
161
164
  LINT_RATCHET_FILENAME,
162
165
  TEXT_RULES_FILENAME,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spardutti/claude-skills",
3
- "version": "2.64.0",
3
+ "version": "2.66.0",
4
4
  "description": "Guardrails for Claude Code — best-practice skills, planning commands, and delivery gates.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -11,7 +11,7 @@
11
11
  "claude-skills": "bin/cli.mjs"
12
12
  },
13
13
  "scripts": {
14
- "prepack": "cp ../README.md README.md && mkdir -p hooks && cp ../scripts/skill-gate.sh ../scripts/skill-gate-automark.sh ../scripts/skill-application-gate.sh ../scripts/gauntlet.sh ../scripts/ship-gate.sh ../scripts/gate-config.sh ../scripts/ship-gate-projects.sh ../scripts/ship-gate-structure.sh ../scripts/ship-gate-files.sh ../scripts/ship-gate-hook.sh ../scripts/version-check.sh ../scripts/skill-flags-record.sh ../scripts/skill-report.mjs ../scripts/edit-check.sh ../scripts/line-limit.sh ../scripts/lint-ratchet.mjs ../scripts/text-rules.mjs ../scripts/ratchet-baseline.mjs ../scripts/diary-check.mjs ../scripts/diary-lib.mjs ../scripts/ship-log.mjs ../scripts/ship-log-parse.mjs ../scripts/ship-log-report.mjs ../scripts/ship-mark.sh ../scripts/ship-prep.sh ../scripts/registry.mjs ../scripts/registry-lib.mjs hooks/",
14
+ "prepack": "cp ../README.md README.md && mkdir -p hooks && cp ../scripts/skill-gate.sh ../scripts/skill-gate-automark.sh ../scripts/skill-application-gate.sh ../scripts/gauntlet.sh ../scripts/ship-gate.sh ../scripts/gate-config.sh ../scripts/ship-gate-projects.sh ../scripts/ship-gate-structure.sh ../scripts/ship-gate-files.sh ../scripts/ship-gate-hook.sh ../scripts/version-check.sh ../scripts/skill-flags-record.sh ../scripts/skill-report.mjs ../scripts/edit-check.sh ../scripts/bash-check.sh ../scripts/line-limit.sh ../scripts/lint-ratchet.mjs ../scripts/text-rules.mjs ../scripts/ratchet-baseline.mjs ../scripts/diary-check.mjs ../scripts/diary-lib.mjs ../scripts/ship-log.mjs ../scripts/ship-log-parse.mjs ../scripts/ship-log-report.mjs ../scripts/ship-mark.sh ../scripts/ship-prep.sh ../scripts/registry.mjs ../scripts/registry-lib.mjs hooks/",
15
15
  "postpack": "git checkout -- README.md && rm -rf hooks"
16
16
  },
17
17
  "engines": {