@spardutti/claude-skills 2.58.0 → 2.59.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 +3 -1
- package/hooks/registry-lib.mjs +82 -0
- package/hooks/registry.mjs +91 -0
- package/lib/setup-hook.mjs +3 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -134,7 +134,8 @@ Portable slash commands installed to `.claude/commands/`. Some orchestrate paral
|
|
|
134
134
|
|---------|--------------|
|
|
135
135
|
| `/ship` | Unified delivery pipeline — commit → gate → PR → merge → release. The gate is the one enforcement moment: it checks the diff's file lengths and that new files sit in their kind folder, audits it against the skills installed in the project, mutation-tests the changed lines to prove the tests would catch a break, and runs one `/code-review` round for logic bugs — **fixing what it finds** rather than handing you a list. It runs before the **PR**, not before every commit: its scope is the whole branch, so gating each commit re-mutated every file the branch had ever touched. `--force` skips it. No argument steps through interactively; `/ship pr` runs through PR creation; `/ship release` runs the full pipeline |
|
|
136
136
|
| `/discover` | Find the right problem before deciding what to build — diverges first: generates competing framings through blind subagents under forced constraints, stress-tests the winner with a blind critic, and refuses to converge until every open question is answered or deferred, then drafts scope, non-goals, edge cases and success criteria for you to correct. Run before `/plan-feature` |
|
|
137
|
-
| `/plan-feature` | Integration-first feature planning, then building — one agent scans the repo for reusable code, patterns and touch points, grounded clarifying questions follow, the plan is written as type-checked stubs at the real paths, and on your go it fills them in |
|
|
137
|
+
| `/plan-feature` | Integration-first feature planning, then building — one agent scans the repo for reusable code, patterns and touch points, grounded clarifying questions follow, the plan is written as type-checked stubs at the real paths, and on your go it fills them in. It reads every `*.registry.yaml` first and must mark each one reuse or skip |
|
|
138
|
+
| `/registry` | Record the project's reusable systems — aiming methods, a state machine, an effect pipeline — as a small `<name>.registry.yaml` next to their code, so `/plan-feature` extends them instead of rebuilding them. You name the system, it drafts the entry for your yes. Nothing reads the registry outside planning |
|
|
138
139
|
| `/optimize` | Make one measured job faster without changing what it proves — fixes a benchmark and a result that must not move, sends 4 read-only subagents to find suspects, then changes one thing per round with a prediction written first, keeping only what beats the noise. Profiles after every wrong prediction, reruns before judging a result that flipped, and ends by writing the cause as a skill rule for you |
|
|
139
140
|
| `/handoff` | Write a handoff doc so a fresh session picks the work up — captures what auto-compaction loses: decisions made and rejected, dead ends already ruled out, why the tree is dirty, and the one next step |
|
|
140
141
|
| `/lockdown` | Per-repo supply-chain hardening — detects the package managers, Dockerfiles, and CI in use, then guides you through and applies install-time, deploy, and pipeline hardening for npm/pnpm, pip/uv, Docker, and GitHub Actions |
|
|
@@ -199,6 +200,7 @@ scripts/ validate-skills.mjs — checks skill length caps and reference in
|
|
|
199
200
|
skill-flags-record.sh — logs each /ship skills audit and /code-review; skill-report.mjs tallies them
|
|
200
201
|
edit-check.sh — PostToolUse check on each edited file: line-limit.sh, lint-ratchet.mjs, .edit-rules.json
|
|
201
202
|
diary-check.mjs — run at the end of /ship; says when the diaries hold enough for /add-rule
|
|
203
|
+
registry.mjs — the *.registry.yaml checks for /plan-feature and /registry
|
|
202
204
|
line-limit.sh — the 200-line ratchet, shared by the edit check, the ship gate and preflight
|
|
203
205
|
gauntlet-selftest.sh — behavioural tests for the hooks (runs in preflight)
|
|
204
206
|
gauntlet-survey.sh — what the hook would do in every repo under a directory
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join, relative } from "node:path";
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
|
|
5
|
+
export const REGISTRY_SUFFIX = ".registry.yaml";
|
|
6
|
+
const SKIP_DIRS = new Set(["node_modules", ".git", "dist", "build", ".venv", "venv", "__pycache__"]);
|
|
7
|
+
|
|
8
|
+
export function projectRoot() {
|
|
9
|
+
const dir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
10
|
+
const top = spawnSync("git", ["-C", dir, "rev-parse", "--show-toplevel"], { encoding: "utf8" });
|
|
11
|
+
return top.status === 0 ? top.stdout.trim() : dir;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function walk(dir, root, out) {
|
|
15
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
16
|
+
if (e.isDirectory() && !SKIP_DIRS.has(e.name)) walk(join(dir, e.name), root, out);
|
|
17
|
+
else if (e.isFile()) out.push(relative(root, join(dir, e.name)));
|
|
18
|
+
}
|
|
19
|
+
return out;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function projectFiles(root) {
|
|
23
|
+
const git = spawnSync("git", ["-C", root, "ls-files", "--cached", "--others", "--exclude-standard"],
|
|
24
|
+
{ encoding: "utf8", maxBuffer: 64 << 20 });
|
|
25
|
+
if (git.status !== 0) return walk(root, root, []);
|
|
26
|
+
return git.stdout.split("\n").filter((f) => f && existsSync(join(root, f)));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function value(raw) {
|
|
30
|
+
const v = raw.trim();
|
|
31
|
+
if (v.startsWith("[") && v.endsWith("]")) {
|
|
32
|
+
return v.slice(1, -1).split(",").map((s) => s.trim().replace(/^["']|["']$/g, "")).filter(Boolean);
|
|
33
|
+
}
|
|
34
|
+
return v.replace(/^["']|["']$/g, "");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function parseEntry(text) {
|
|
38
|
+
const entry = {};
|
|
39
|
+
for (const line of text.split("\n")) {
|
|
40
|
+
const m = line.match(/^([a-z_]+):\s*(.*?)\s*(?:\s#.*)?$/);
|
|
41
|
+
if (m) entry[m[1]] = value(m[2]);
|
|
42
|
+
}
|
|
43
|
+
entry.triggers = [].concat(entry.triggers ?? []).filter(Boolean);
|
|
44
|
+
return entry;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function loadRegistry(root, files) {
|
|
48
|
+
return files
|
|
49
|
+
.filter((f) => f.endsWith(REGISTRY_SUFFIX))
|
|
50
|
+
.sort()
|
|
51
|
+
.map((file) => {
|
|
52
|
+
const text = readFileSync(join(root, file), "utf8");
|
|
53
|
+
return { file, text, ...parseEntry(text) };
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Code-shaped words (AimStrategy, pick_target, aimRegistry) or `backticked` — never "implement".
|
|
58
|
+
export function symbolsOf(extendBy = "") {
|
|
59
|
+
const ticked = [...extendBy.matchAll(/`([\w.]+)`/g)].map((m) => m[1]);
|
|
60
|
+
const shaped = extendBy.match(/\b(?:[A-Z][a-z0-9]+[A-Z]\w*|[a-z]\w*[A-Z]\w*|\w+_\w+)\b/g) ?? [];
|
|
61
|
+
return [...new Set([...ticked, ...shaped])];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
65
|
+
|
|
66
|
+
export function triggerHits(entry, text) {
|
|
67
|
+
return entry.triggers.filter((t) => new RegExp(`\\b${escape(t)}\\b`, "i").test(text));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// A location is a folder or one file, like sim/aim.gd beside unrelated systems in sim/.
|
|
71
|
+
export function insideLocation(entry, file) {
|
|
72
|
+
const loc = (entry.location ?? "").replace(/^\.\//, "");
|
|
73
|
+
return loc !== "" && (file === loc || file.startsWith(loc.replace(/\/?$/, "/")));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Only what the stub added: an extended file's old code would trip triggers it never wrote.
|
|
77
|
+
export function addedText(root, file) {
|
|
78
|
+
const tracked = spawnSync("git", ["-C", root, "ls-files", "--error-unmatch", file], { encoding: "utf8" });
|
|
79
|
+
if (tracked.status !== 0) return existsSync(join(root, file)) ? readFileSync(join(root, file), "utf8") : "";
|
|
80
|
+
const diff = spawnSync("git", ["-C", root, "diff", "HEAD", "--", file], { encoding: "utf8" }).stdout ?? "";
|
|
81
|
+
return diff.split("\n").filter((l) => l.startsWith("+") && !l.startsWith("+++")).map((l) => l.slice(1)).join("\n");
|
|
82
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
3
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
REGISTRY_SUFFIX, addedText, escape, insideLocation, loadRegistry, projectFiles, projectRoot, symbolsOf, triggerHits,
|
|
6
|
+
} from "./registry-lib.mjs";
|
|
7
|
+
|
|
8
|
+
const root = projectRoot();
|
|
9
|
+
const files = projectFiles(root);
|
|
10
|
+
const entries = loadRegistry(root, files);
|
|
11
|
+
const uses = (text, word) => new RegExp(`\\b${escape(word)}\\b`).test(text);
|
|
12
|
+
const fromCwd = (f) => relative(root, resolve(f));
|
|
13
|
+
|
|
14
|
+
function list() {
|
|
15
|
+
if (!entries.length) return console.log(`registry none — no *${REGISTRY_SUFFIX} in this project`);
|
|
16
|
+
for (const e of entries) console.log(`== ${e.file} ==\n${e.text.trim()}\n`);
|
|
17
|
+
stale();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function planMarks(planPath) {
|
|
21
|
+
const text = existsSync(planPath) ? readFileSync(planPath, "utf8") : null;
|
|
22
|
+
if (text === null) return null;
|
|
23
|
+
const section = text.split(/^## /m).find((s) => /^Registry\s*$/m.test(s.split("\n")[0])) ?? "";
|
|
24
|
+
const marks = new Map();
|
|
25
|
+
for (const m of section.matchAll(/^[-*]\s*`?([\w.-]+)`?\s*:\s*(reuse|skip)\b/gim)) marks.set(m[1], m[2].toLowerCase());
|
|
26
|
+
return marks;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function checkPlan(planPath, stubs) {
|
|
30
|
+
if (!entries.length) return console.log("registry ok — no entries");
|
|
31
|
+
const marks = planMarks(planPath);
|
|
32
|
+
if (!marks) return fail([`registry FAIL ${planPath} not found — save the plan notes there first`]);
|
|
33
|
+
const added = stubs.map((f) => addedText(root, fromCwd(f))).join("\n");
|
|
34
|
+
const fails = [], asks = [];
|
|
35
|
+
for (const name of marks.keys()) {
|
|
36
|
+
if (!entries.some((e) => e.name === name)) fails.push(`registry FAIL ${name}: no such entry`);
|
|
37
|
+
}
|
|
38
|
+
for (const e of entries) {
|
|
39
|
+
const mark = marks.get(e.name);
|
|
40
|
+
if (!mark) fails.push(`registry FAIL ${e.name}: missing from ## Registry — mark it reuse or skip`);
|
|
41
|
+
else if (mark === "reuse" && !symbolsOf(e.extend_by).some((s) => uses(added, s))) {
|
|
42
|
+
fails.push(`registry FAIL ${e.name}: marked reuse, but no stub uses ${symbolsOf(e.extend_by).join(", ") || "a name from extend_by"}`);
|
|
43
|
+
} else if (mark === "skip") {
|
|
44
|
+
const hits = triggerHits(e, added);
|
|
45
|
+
if (hits.length) asks.push(`registry ASK ${e.name}: skipped, but the stubs mention ${hits.join(", ")} — ask the user before stopping`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
for (const line of asks) console.log(line);
|
|
49
|
+
if (fails.length) return fail(fails);
|
|
50
|
+
if (!asks.length) console.log("registry ok");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function stale() {
|
|
54
|
+
const problems = [];
|
|
55
|
+
const seen = new Set();
|
|
56
|
+
for (const e of entries) {
|
|
57
|
+
const say = (why) => problems.push(`registry stale ${e.file}: ${why}`);
|
|
58
|
+
for (const key of ["name", "summary", "location", "extend_by"]) {
|
|
59
|
+
if (!e[key]) say(`no ${key}`);
|
|
60
|
+
else if (/^[|>]/.test(e[key])) say(`${key} must sit on one line`);
|
|
61
|
+
}
|
|
62
|
+
if (e.name && !/^[\w.-]+$/.test(e.name)) say(`name ${e.name} may hold only letters, digits, dashes and dots`);
|
|
63
|
+
if (/^\s+-\s/m.test(e.text)) say("write lists on one line, like triggers: [aim, target]");
|
|
64
|
+
if (seen.has(e.name)) say(`name ${e.name} is used twice`);
|
|
65
|
+
seen.add(e.name);
|
|
66
|
+
const loc = join(root, e.location ?? "");
|
|
67
|
+
if (!e.location || !existsSync(loc)) { say(`location ${e.location} not found`); continue; }
|
|
68
|
+
const near = statSync(loc).isDirectory() ? loc : dirname(loc);
|
|
69
|
+
if (e.example && !existsSync(join(near, e.example)) && !existsSync(join(root, e.example))) say(`example ${e.example} not found`);
|
|
70
|
+
const syms = symbolsOf(e.extend_by);
|
|
71
|
+
if (!syms.length) say("extend_by names no symbol — write names like `AimStrategy`");
|
|
72
|
+
const code = files.filter((f) => insideLocation(e, f) && !f.endsWith(REGISTRY_SUFFIX))
|
|
73
|
+
.map((f) => readFileSync(join(root, f), "utf8")).join("\n");
|
|
74
|
+
for (const s of syms) if (!uses(code, s)) say(`${s} from extend_by is not found in ${e.location}`);
|
|
75
|
+
}
|
|
76
|
+
if (problems.length) fail(problems);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function fail(lines) {
|
|
80
|
+
for (const line of lines) console.log(line);
|
|
81
|
+
process.exitCode = 1;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const [mode, ...rest] = process.argv.slice(2);
|
|
85
|
+
if (mode === undefined) list();
|
|
86
|
+
else if (mode === "--plan" && rest[0]) checkPlan(resolve(rest[0]), rest.slice(1));
|
|
87
|
+
else if (mode === "--stale") stale();
|
|
88
|
+
else {
|
|
89
|
+
console.error("usage: registry.mjs [--plan PLAN.md <stub files…> | --stale]");
|
|
90
|
+
process.exit(2);
|
|
91
|
+
}
|
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
|
+
// registry.mjs run by /plan-feature and /registry — the *.registry.yaml checks
|
|
34
35
|
|
|
35
36
|
const GATE_FILENAME = "skill-gate.sh";
|
|
36
37
|
const AUTO_MARK_FILENAME = "skill-gate-automark.sh";
|
|
@@ -57,6 +58,7 @@ const SHIP_LOG_PARSE_FILENAME = "ship-log-parse.mjs";
|
|
|
57
58
|
const SHIP_LOG_REPORT_FILENAME = "ship-log-report.mjs";
|
|
58
59
|
const SHIP_MARK_FILENAME = "ship-mark.sh";
|
|
59
60
|
const SHIP_PREP_FILENAME = "ship-prep.sh";
|
|
61
|
+
const REGISTRY_FILENAMES = ["registry.mjs", "registry-lib.mjs"];
|
|
60
62
|
const LEGACY_EVAL_FILENAME = "skill-forced-eval-hook.sh";
|
|
61
63
|
const LEGACY_AUDIT_RUNNER_FILENAME = "skill-audit-runner.sh";
|
|
62
64
|
|
|
@@ -166,6 +168,7 @@ export async function setupHook(targetDir = process.cwd()) {
|
|
|
166
168
|
SHIP_LOG_REPORT_FILENAME,
|
|
167
169
|
SHIP_MARK_FILENAME,
|
|
168
170
|
SHIP_PREP_FILENAME,
|
|
171
|
+
...REGISTRY_FILENAMES,
|
|
169
172
|
]) {
|
|
170
173
|
const dest = join(hooksDir, name);
|
|
171
174
|
await writeFile(dest, await readHookSource(name), { mode: 0o755 });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spardutti/claude-skills",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.59.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 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/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": {
|