@spardutti/claude-skills 2.63.0 → 2.64.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
@@ -135,7 +135,7 @@ Portable slash commands installed to `.claude/commands/`. Some orchestrate paral
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
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. `/registry eval` lists existing code that does an entry's job outside its folder |
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. `/registry eval` lists existing code that does an entry's job outside its folder, and `/registry update` redrafts every entry against today's code |
139
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 |
140
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 |
141
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 |
@@ -83,6 +83,8 @@ export function insideLocation(entry, file) {
83
83
  return loc !== "" && (file === loc || file.startsWith(loc.replace(/\/?$/, "/")));
84
84
  }
85
85
 
86
+ export const isAllowed = (entry, file) => entry.allowed.some((a) => (a.endsWith("/") ? file.startsWith(a) : file === a));
87
+
86
88
  // Only what the stub added: an extended file's old code would trip triggers it never wrote.
87
89
  export function addedText(root, file) {
88
90
  const tracked = spawnSync("git", ["-C", root, "ls-files", "--error-unmatch", file], { encoding: "utf8" });
@@ -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, loadRegistry, planMarks, projectFiles, projectRoot, symbolsOf, triggerHits,
5
+ REGISTRY_SUFFIX, addedText, escape, insideLocation, isAllowed, loadRegistry, planMarks, projectFiles, projectRoot, symbolsOf, triggerHits,
6
6
  } from "./registry-lib.mjs";
7
7
  import { loadEditRules } from "./text-rules.mjs";
8
8
 
@@ -50,11 +50,15 @@ function checkPlan(planPath, stubs) {
50
50
  if (!asks.length) console.log("registry ok");
51
51
  }
52
52
 
53
+ function planMissing() {
54
+ if (entries.length && !planMarks(join(root, "PLAN.md"))?.size) console.log(`registry unplanned — ${entries.length} entries, no marks in PLAN.md`);
55
+ }
56
+
53
57
  function evaluate() {
54
58
  if (!entries.length) return console.log(`registry none — no *${REGISTRY_SUFFIX} in this project`);
55
59
  for (const e of entries) {
56
60
  if (!e.does.length) { console.log(`registry eval ${e.name}: skipped, no does`); continue; }
57
- const outside = files.filter((f) => !insideLocation(e, f) && !e.allowed.includes(f) && !f.endsWith(REGISTRY_SUFFIX));
61
+ const outside = files.filter((f) => !insideLocation(e, f) && !isAllowed(e, f) && !f.endsWith(REGISTRY_SUFFIX));
58
62
  const hits = outside.flatMap((f) => readFileSync(join(root, f), "utf8").split("\n").flatMap((line, i) =>
59
63
  e.does.filter((code) => calls(line, code)).map((code) => `registry SUSPECT ${e.name} ${f}:${i + 1} ${code}`)));
60
64
  console.log(hits.length ? hits.join("\n") : `registry eval ${e.name}: nothing outside ${e.location} does its job`);
@@ -78,24 +82,28 @@ function stale() {
78
82
  if (/^\s+-\s/m.test(e.text)) say("write lists on one line, like triggers: [aim, target]");
79
83
  if (seen.has(e.name)) say(`name ${e.name} is used twice`);
80
84
  seen.add(e.name);
81
- const loc = join(root, e.location ?? "");
82
- if (!e.location || !existsSync(loc)) { say(`location ${e.location} not found`); continue; }
83
- const near = statSync(loc).isDirectory() ? loc : dirname(loc);
84
- if (e.example && !existsSync(join(near, e.example)) && !existsSync(join(root, e.example))) say(`example ${e.example} not found`);
85
- const syms = symbolsOf(e.extend_by);
86
- if (!syms.length) say("extend_by names no symbol — write names like `AimStrategy`");
87
- const code = files.filter((f) => insideLocation(e, f) && !f.endsWith(REGISTRY_SUFFIX))
88
- .map((f) => readFileSync(join(root, f), "utf8")).join("\n");
89
- for (const s of syms) {
90
- // A plain word like `first` matches `var first = enemies[0]`; `Aim.first` only matches real use.
91
- if (/^[a-z][a-z0-9]*$/.test(s)) say(`\`${s}\` is too common a word to prove use — name its owner, like \`Owner.${s}\``);
92
- else if (!s.split(".").every((part) => uses(code, part))) say(`${s} from extend_by is not found in ${e.location}`);
93
- }
94
- for (const c of e.does) if (!calls(code, c)) say(`does ${c} is not found in ${e.location}`);
85
+ staleCode(e, say);
95
86
  }
96
87
  if (problems.length) fail(problems);
97
88
  }
98
89
 
90
+ 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`);
95
+ const syms = symbolsOf(e.extend_by);
96
+ if (!syms.length) say("extend_by names no symbol — write names like `AimStrategy`");
97
+ const code = files.filter((f) => insideLocation(e, f) && !f.endsWith(REGISTRY_SUFFIX))
98
+ .map((f) => readFileSync(join(root, f), "utf8")).join("\n");
99
+ for (const s of syms) {
100
+ // A plain word like `first` matches `var first = enemies[0]`; `Aim.first` only matches real use.
101
+ 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}`);
103
+ }
104
+ for (const c of e.does) if (!calls(code, c)) say(`does ${c} is not found in ${e.location}`);
105
+ }
106
+
99
107
  function fail(lines) {
100
108
  for (const line of lines) console.log(line);
101
109
  process.exitCode = 1;
@@ -104,9 +112,10 @@ function fail(lines) {
104
112
  const [mode, ...rest] = process.argv.slice(2);
105
113
  if (mode === undefined) list();
106
114
  else if (mode === "--plan" && rest[0]) checkPlan(resolve(rest[0]), rest.slice(1));
115
+ else if (mode === "--plan-missing") planMissing();
107
116
  else if (mode === "--stale") stale();
108
117
  else if (mode === "--eval") evaluate();
109
118
  else {
110
- console.error("usage: registry.mjs [--plan PLAN.md <stub files…> | --stale | --eval]");
119
+ console.error("usage: registry.mjs [--plan PLAN.md <stub files…> | --plan-missing | --stale | --eval]");
111
120
  process.exit(2);
112
121
  }
@@ -59,6 +59,9 @@ scope() {
59
59
  base=$(printf '%s\n' "$scope" | sed -n '1s/^base //p')
60
60
  while IFS=$'\t' read -r f _; do [ -n "$f" ] && files+=("$f"); done < <(printf '%s\n' "$scope" | sed '1d')
61
61
  skills_report "${files[@]}"
62
+ if printf '%s\n' "$scope" | grep -q '(new)$' && [ -n "$(node "$HERE/registry.mjs" --plan-missing 2>/dev/null)" ]; then
63
+ echo "plan none: this branch adds files and no /plan-feature marked the registry"
64
+ fi
62
65
  diff="/tmp/claude-ship-$(printf '%s %s' "$PWD" "$branch" | cksum | cut -d' ' -f1).diff"
63
66
  : > "$diff"
64
67
  if [ ${#files[@]} -gt 0 ]; then
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spardutti/claude-skills",
3
- "version": "2.63.0",
3
+ "version": "2.64.0",
4
4
  "description": "Guardrails for Claude Code — best-practice skills, planning commands, and delivery gates.",
5
5
  "type": "module",
6
6
  "repository": {