@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 +1 -0
- package/hooks/bash-check.sh +43 -0
- package/hooks/registry-lib.mjs +4 -3
- package/hooks/registry.mjs +22 -8
- package/lib/hook-settings.mjs +7 -0
- package/lib/setup-hook.mjs +3 -0
- package/package.json +2 -2
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
|
package/hooks/registry-lib.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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));
|
package/hooks/registry.mjs
CHANGED
|
@@ -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.
|
|
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
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
if (
|
|
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 ${
|
|
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 ${
|
|
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) {
|
package/lib/hook-settings.mjs
CHANGED
|
@@ -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}`;
|
package/lib/setup-hook.mjs
CHANGED
|
@@ -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.
|
|
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": {
|