@spardutti/claude-skills 2.60.4 → 2.61.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 +4 -2
- package/hooks/registry-lib.mjs +1 -1
- package/hooks/registry.mjs +18 -1
- package/hooks/ship-log.mjs +13 -4
- package/hooks/ship-prep.sh +2 -5
- package/package.json +1 -1
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.
|
|
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 |
|
|
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 |
|
|
@@ -177,7 +177,9 @@ never: sort or filter enemies inside a skill
|
|
|
177
177
|
|
|
178
178
|
`/plan-feature` reads every entry and must mark each one `reuse` or `skip`. A plan
|
|
179
179
|
fails if it says `reuse` while its stubs never use the names in `extend_by`, and an
|
|
180
|
-
entry whose names left the code is flagged as stale.
|
|
180
|
+
entry whose names left the code is flagged as stale. An entry can also list the calls
|
|
181
|
+
that do its job, `does: [insert(events)]`, and `/registry eval` names every file outside
|
|
182
|
+
its folder that makes one — the second way to save an event that nobody planned.
|
|
181
183
|
`/registry aim` drafts an entry for your yes. Replayed on a real game feature, the
|
|
182
184
|
targeting rule went inside the skill without an entry, and into `Aim` with one, 5 runs of 5.
|
|
183
185
|
|
package/hooks/registry-lib.mjs
CHANGED
|
@@ -40,7 +40,7 @@ export function parseEntry(text) {
|
|
|
40
40
|
const m = line.match(/^([a-z_]+):\s*(.*?)\s*(?:\s#.*)?$/);
|
|
41
41
|
if (m) entry[m[1]] = value(m[2]);
|
|
42
42
|
}
|
|
43
|
-
|
|
43
|
+
for (const key of ["triggers", "does", "allowed"]) entry[key] = [].concat(entry[key] ?? []).filter(Boolean);
|
|
44
44
|
return entry;
|
|
45
45
|
}
|
|
46
46
|
|
package/hooks/registry.mjs
CHANGED
|
@@ -16,6 +16,8 @@ const usesIn = (text, name) => {
|
|
|
16
16
|
return uses(text, parts[0]) && new RegExp(`\\.${escape(parts.at(-1))}\\b`).test(text);
|
|
17
17
|
};
|
|
18
18
|
const fromCwd = (f) => relative(root, resolve(f));
|
|
19
|
+
// `insert(events)` is code, not a word: it may follow a dot, never more letters.
|
|
20
|
+
const calls = (text, code) => new RegExp(`${/^\w/.test(code) ? "\\b" : ""}${escape(code)}`).test(text);
|
|
19
21
|
|
|
20
22
|
function list() {
|
|
21
23
|
if (!entries.length) return console.log(`registry none — no *${REGISTRY_SUFFIX} in this project`);
|
|
@@ -56,6 +58,17 @@ function checkPlan(planPath, stubs) {
|
|
|
56
58
|
if (!asks.length) console.log("registry ok");
|
|
57
59
|
}
|
|
58
60
|
|
|
61
|
+
function evaluate() {
|
|
62
|
+
if (!entries.length) return console.log(`registry none — no *${REGISTRY_SUFFIX} in this project`);
|
|
63
|
+
for (const e of entries) {
|
|
64
|
+
if (!e.does.length) { console.log(`registry eval ${e.name}: skipped, no does`); continue; }
|
|
65
|
+
const outside = files.filter((f) => !insideLocation(e, f) && !e.allowed.includes(f) && !f.endsWith(REGISTRY_SUFFIX));
|
|
66
|
+
const hits = outside.flatMap((f) => readFileSync(join(root, f), "utf8").split("\n").flatMap((line, i) =>
|
|
67
|
+
e.does.filter((code) => calls(line, code)).map((code) => `registry SUSPECT ${e.name} ${f}:${i + 1} ${code}`)));
|
|
68
|
+
console.log(hits.length ? hits.join("\n") : `registry eval ${e.name}: nothing outside ${e.location} does its job`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
59
72
|
function stale() {
|
|
60
73
|
const problems = [];
|
|
61
74
|
const seen = new Set();
|
|
@@ -65,6 +78,8 @@ function stale() {
|
|
|
65
78
|
if (!e[key]) say(`no ${key}`);
|
|
66
79
|
else if (/^[|>]/.test(e[key])) say(`${key} must sit on one line`);
|
|
67
80
|
}
|
|
81
|
+
if (!e.triggers.length) say("no triggers");
|
|
82
|
+
for (const f of e.allowed) if (!existsSync(join(root, f))) say(`allowed ${f} not found`);
|
|
68
83
|
if (e.name && !/^[\w.-]+$/.test(e.name)) say(`name ${e.name} may hold only letters, digits, dashes and dots`);
|
|
69
84
|
if (/^\s+-\s/m.test(e.text)) say("write lists on one line, like triggers: [aim, target]");
|
|
70
85
|
if (seen.has(e.name)) say(`name ${e.name} is used twice`);
|
|
@@ -82,6 +97,7 @@ function stale() {
|
|
|
82
97
|
if (/^[a-z][a-z0-9]*$/.test(s)) say(`\`${s}\` is too common a word to prove use — name its owner, like \`Owner.${s}\``);
|
|
83
98
|
else if (!s.split(".").every((part) => uses(code, part))) say(`${s} from extend_by is not found in ${e.location}`);
|
|
84
99
|
}
|
|
100
|
+
for (const c of e.does) if (!calls(code, c)) say(`does ${c} is not found in ${e.location}`);
|
|
85
101
|
}
|
|
86
102
|
if (problems.length) fail(problems);
|
|
87
103
|
}
|
|
@@ -95,7 +111,8 @@ const [mode, ...rest] = process.argv.slice(2);
|
|
|
95
111
|
if (mode === undefined) list();
|
|
96
112
|
else if (mode === "--plan" && rest[0]) checkPlan(resolve(rest[0]), rest.slice(1));
|
|
97
113
|
else if (mode === "--stale") stale();
|
|
114
|
+
else if (mode === "--eval") evaluate();
|
|
98
115
|
else {
|
|
99
|
-
console.error("usage: registry.mjs [--plan PLAN.md <stub files…> | --stale]");
|
|
116
|
+
console.error("usage: registry.mjs [--plan PLAN.md <stub files…> | --stale | --eval]");
|
|
100
117
|
process.exit(2);
|
|
101
118
|
}
|
package/hooks/ship-log.mjs
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawnSync } from "node:child_process";
|
|
3
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { basename, dirname, join, resolve } from "node:path";
|
|
5
5
|
import { git, parseReport, prSize } from "./ship-log-parse.mjs";
|
|
6
6
|
import { printReport, tally } from "./ship-log-report.mjs";
|
|
7
7
|
|
|
8
8
|
const HERE = dirname(resolve(process.argv[1]));
|
|
9
9
|
const VERDICTS = ["confirmed", "refuted", "unconfirmed", "skipped"];
|
|
10
|
+
// A release ships only work its feature PRs already logged.
|
|
11
|
+
const RELEASE = /^(main|master|dev|develop|development|chore\/release-.*)$/;
|
|
10
12
|
const now = () => new Date().toISOString().replace(/\.\d+Z$/, "Z");
|
|
11
13
|
|
|
12
14
|
// gate-config.sh owns the config files and their order, so the gate and this agree on "on".
|
|
@@ -69,7 +71,7 @@ function finalize(dir, path, record, tag, status, reason, at) {
|
|
|
69
71
|
}
|
|
70
72
|
|
|
71
73
|
function prOf(branch) {
|
|
72
|
-
const r = spawnSync("gh", ["pr", "list", "--head", branch, "--state", "all", "--json", "number,createdAt,state", "--limit", "1"], { encoding: "utf8" });
|
|
74
|
+
const r = spawnSync("gh", ["pr", "list", "--head", branch, "--state", "all", "--json", "number,createdAt,closedAt,state", "--limit", "1"], { encoding: "utf8" });
|
|
73
75
|
try { return r.status === 0 ? JSON.parse(r.stdout)[0] : undefined; } catch { return undefined; }
|
|
74
76
|
}
|
|
75
77
|
|
|
@@ -80,8 +82,10 @@ function closeOpened(dir, app) {
|
|
|
80
82
|
const open = join(dir, "open");
|
|
81
83
|
for (const f of existsSync(open) ? readdirSync(open).filter((n) => n.startsWith(`${app}__`)) : []) {
|
|
82
84
|
const path = join(open, f), record = JSON.parse(readFileSync(path, "utf8"));
|
|
83
|
-
|
|
84
|
-
|
|
85
|
+
const pr = prOf(record.branch), empty = !record.gate.length && !record.findings.length;
|
|
86
|
+
// A /ship that only merged fills nothing, so once its PR closes there is nothing to keep.
|
|
87
|
+
if (empty && pr?.closedAt >= record.started) { rmSync(path); continue; }
|
|
88
|
+
if (empty) continue;
|
|
85
89
|
const opened = pr && pr.createdAt >= record.started;
|
|
86
90
|
if (!pr || (!opened && pr.state !== "OPEN")) continue;
|
|
87
91
|
finalize(dir, path, Object.assign(record, { pr: pr.number }), `pr${pr.number}`,
|
|
@@ -156,6 +160,11 @@ function run(mode, args) {
|
|
|
156
160
|
if (mode === "on") return c.on ? 0 : 1;
|
|
157
161
|
if (!c.on) return 0;
|
|
158
162
|
const id = identity(c.root), path = openPath(c.dir, id), record = load(path, id);
|
|
163
|
+
if (mode === "start" && RELEASE.test(id.branch)) {
|
|
164
|
+
closeOpened(c.dir, id.app);
|
|
165
|
+
if (existsSync(path)) rmSync(path);
|
|
166
|
+
return 0;
|
|
167
|
+
}
|
|
159
168
|
if (mode === "start") return restart(c.dir, path, record, id), 0;
|
|
160
169
|
if (mode === "ready") {
|
|
161
170
|
const todo = existsSync(path) ? missing(record, c) : [];
|
package/hooks/ship-prep.sh
CHANGED
|
@@ -69,11 +69,8 @@ scope() {
|
|
|
69
69
|
fi
|
|
70
70
|
echo "diff $diff"
|
|
71
71
|
echo "review /code-review medium origin/$(base_branch)...$branch"
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
node "$HERE/ship-log.mjs" step audit start >/dev/null 2>&1
|
|
75
|
-
node "$HERE/ship-log.mjs" step review start >/dev/null 2>&1 ;;
|
|
76
|
-
esac
|
|
72
|
+
node "$HERE/ship-log.mjs" step audit start >/dev/null 2>&1
|
|
73
|
+
node "$HERE/ship-log.mjs" step review start >/dev/null 2>&1
|
|
77
74
|
return 0
|
|
78
75
|
}
|
|
79
76
|
|