@linchpinagency/skills 0.1.10 → 0.1.11
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 +70 -7
- package/bin/install.mjs +374 -4
- package/package.json +1 -1
- package/skills/agent-capabilities/SKILL.md +139 -0
- package/skills/agent-capabilities/references/mcp-scoping.md +96 -0
- package/skills/agent-capabilities/references/skills-audit.md +116 -0
- package/skills/commit-and-release/SKILL.md +58 -4
- package/skills/project-context/SKILL.md +16 -1
- package/skills/quality-gates/SKILL.md +54 -1
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ GitHub Copilot, and other compatible coding agents.
|
|
|
12
12
|

|
|
13
13
|
|
|
14
14
|
<!-- x-release-please-start-version -->
|
|
15
|
-
### Latest release: 0.1.
|
|
15
|
+
### Latest release: 0.1.11
|
|
16
16
|
<!-- x-release-please-end -->
|
|
17
17
|
|
|
18
18
|
| Release | Skill standard | Install |
|
|
@@ -66,9 +66,10 @@ Then start a new session in your project and ask for something real — "what ki
|
|
|
66
66
|
is this?" should pull in `project-context` and get you a summary of the repo shape, local
|
|
67
67
|
environment, and host.
|
|
68
68
|
|
|
69
|
-
**Re-run the same command to update.** There's no upgrade command
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
**Re-run the same command to update.** There's no upgrade command — the installer diffs each
|
|
70
|
+
skill's version against what you have, shows what would change, and asks before applying it.
|
|
71
|
+
A run with nothing to change exits immediately, so re-running costs nothing. Do it every few
|
|
72
|
+
weeks, or when someone announces a new skill.
|
|
72
73
|
|
|
73
74
|
Full flag reference: [Install options](#install-options).
|
|
74
75
|
|
|
@@ -87,7 +88,7 @@ The fastest way to understand the library is to run one loop end to end:
|
|
|
87
88
|
| Handle a client support ticket | "the client says their contact form isn't sending" | `support-triage` |
|
|
88
89
|
| Add guardrails before touching prod | "careful mode — I'm on production" | `safety-hooks` |
|
|
89
90
|
|
|
90
|
-
The full list is in [Available skills](#available-skills) —
|
|
91
|
+
The full list is in [Available skills](#available-skills) — 23 of them, each with a
|
|
91
92
|
`When to use` section that says exactly when it applies and which skill to use instead.
|
|
92
93
|
|
|
93
94
|
**When you want to be explicit**, name the skill: *"use the wp-audit skill on the homepage."*
|
|
@@ -177,6 +178,9 @@ npx @linchpinagency/skills --global
|
|
|
177
178
|
|
|
178
179
|
# Install the Linchpin skills only, without the upstream base layer
|
|
179
180
|
npx @linchpinagency/skills --skip-upstream
|
|
181
|
+
|
|
182
|
+
# Audit every scope for duplicate installs; install nothing
|
|
183
|
+
npx @linchpinagency/skills --check
|
|
180
184
|
```
|
|
181
185
|
|
|
182
186
|
> Pin a version when you need reproducibility — `npx @linchpinagency/skills@0.1.1` — or omit
|
|
@@ -188,8 +192,38 @@ npx @linchpinagency/skills --skip-upstream
|
|
|
188
192
|
> network access and a system `tar`; if either is missing it warns and still installs the
|
|
189
193
|
> Linchpin skills. Pass `--skip-upstream` to install the Linchpin skills alone.
|
|
190
194
|
|
|
191
|
-
**Updating:** re-run the same command
|
|
192
|
-
|
|
195
|
+
**Updating:** re-run the same command — that *is* the update path. Rather than overwriting
|
|
196
|
+
silently, the installer compares each skill's own `version` against what is installed,
|
|
197
|
+
prints the diff, and asks before touching anything:
|
|
198
|
+
|
|
199
|
+
```
|
|
200
|
+
@linchpinagency/skills v0.2.0 — Claude Code
|
|
201
|
+
|
|
202
|
+
task-tracking v1.3.0 -> v1.4.0 update
|
|
203
|
+
agent-capabilities -> v1.0.0 new
|
|
204
|
+
quality-gates v1.0.0 local edits will be lost
|
|
205
|
+
(20 unchanged)
|
|
206
|
+
|
|
207
|
+
3 change(s): 1 update, 1 new, 1 modified
|
|
208
|
+
|
|
209
|
+
Apply? [y/N]
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
A run with nothing to change says so and exits without prompting, so re-running is cheap
|
|
213
|
+
and safe. `--dry-run` shows the diff and writes nothing; `--yes` skips the prompt;
|
|
214
|
+
`--force` reinstalls everything regardless.
|
|
215
|
+
|
|
216
|
+
> A **non-interactive** run — piped stdin, CI, a script — proceeds without prompting, so
|
|
217
|
+
> existing automation keeps working. Use `--dry-run` when you want a preview rather than an
|
|
218
|
+
> install.
|
|
219
|
+
|
|
220
|
+
Three statuses are worth knowing:
|
|
221
|
+
|
|
222
|
+
- **local edits will be lost** — the installed copy was hand-edited. Skills are owned by
|
|
223
|
+
this package; change them here and re-install rather than editing an install in place.
|
|
224
|
+
- **DOWNGRADE** — the package you invoked is *older* than what is installed. Usually a
|
|
225
|
+
pinned `npx @linchpinagency/skills@0.1.1` you meant to drop.
|
|
226
|
+
- **new** — the skill did not exist in your installed version.
|
|
193
227
|
|
|
194
228
|
### Keeping skills current
|
|
195
229
|
|
|
@@ -231,6 +265,34 @@ skips itself whenever `CI` is set.
|
|
|
231
265
|
A project that wants skills in more than one agent's directory should run
|
|
232
266
|
`--agent all` rather than copying directories around by hand.
|
|
233
267
|
|
|
268
|
+
### One scope per skill
|
|
269
|
+
|
|
270
|
+
Agents load **every** skills directory they find and **do not dedupe by name**. A skill
|
|
271
|
+
installed both globally and in a project is listed twice, and its `description` is loaded
|
|
272
|
+
twice in every session before any work starts.
|
|
273
|
+
|
|
274
|
+
So the installer refuses to create the second copy:
|
|
275
|
+
|
|
276
|
+
```
|
|
277
|
+
Refusing to install: 22 of these skills are already installed at another scope.
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
It reports which directory, what would be duplicated, and the command to remove just the
|
|
281
|
+
overlapping skills — never the whole directory, which usually holds skills from other
|
|
282
|
+
sources too. `--force` overrides it for the rare case where you want both.
|
|
283
|
+
|
|
284
|
+
```bash
|
|
285
|
+
npx @linchpinagency/skills --check # audit; exits 1 if duplicates exist
|
|
286
|
+
npx @linchpinagency/skills --check --agent codex # a different agent's directories
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
`--check` also catches the accident that is easiest to miss: an install in a *parent* of the
|
|
290
|
+
repo (running the installer from `~/GitHub` rather than inside a checkout), which shadows
|
|
291
|
+
nothing and duplicates everything below it.
|
|
292
|
+
|
|
293
|
+
Choosing a scope, the MCP-server equivalent of the same problem, and how to record the
|
|
294
|
+
decision are covered by [`agent-capabilities`](skills/agent-capabilities/SKILL.md).
|
|
295
|
+
|
|
234
296
|
> Skills are loaded by the **agent/harness**, not the model — so "Copilot running Claude"
|
|
235
297
|
> still needs the skill installed in Copilot's own directory. The installer handles that.
|
|
236
298
|
|
|
@@ -251,6 +313,7 @@ A project that wants skills in more than one agent's directory should run
|
|
|
251
313
|
| `wp-implementation-choice` | WordPress | Decide what a request should become — theme work, content, a custom block, a functionality plugin, or an existing plugin — before any code is written. |
|
|
252
314
|
| `design-previews` | Design | Generate three genuinely different visual directions as self-contained HTML previews, screenshot them at desktop and mobile via the Chrome DevTools MCP (or Playwright), and get a pick before theme or block work starts. |
|
|
253
315
|
| `project-context` | Workflow | Orient before acting — repo shape, local environment, host, ClickUp space, and release model, read from the project's own config rather than assumed. Referenced by other skills' Preflight. |
|
|
316
|
+
| `agent-capabilities` | Workflow | Right-size what a project loads — audit skill installs for cross-scope duplicates (`--check`), decide which MCP servers the repo actually needs, and scope them so every session stops paying for all of them. |
|
|
254
317
|
| `quality-gates` | Workflow | Run a project's own lint, PHPCS, PHPStan, and test gates before committing — detected from `composer.json`, `package.json`, `phpcs.xml.dist`, and `lint-staged`, never assumed. |
|
|
255
318
|
| `web-qa` | Workflow | QA like a real user and fix what you find — front end, wp-admin, and block editor, with severity, evidence, one atomic commit per fix, and a report-only mode. |
|
|
256
319
|
| `investigate` | Workflow | Root-cause a bug before changing anything — reproduce, read the real error, isolate the layer, explain the mechanism, with WordPress first checks. |
|
package/bin/install.mjs
CHANGED
|
@@ -9,6 +9,8 @@ import fs from 'node:fs';
|
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import os from 'node:os';
|
|
11
11
|
import { execFileSync } from 'node:child_process';
|
|
12
|
+
import crypto from 'node:crypto';
|
|
13
|
+
import readline from 'node:readline';
|
|
12
14
|
import { fileURLToPath } from 'node:url';
|
|
13
15
|
|
|
14
16
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -40,13 +42,28 @@ function resolveAgents(id) {
|
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
function parseArgs(argv) {
|
|
43
|
-
const opts = {
|
|
45
|
+
const opts = {
|
|
46
|
+
agent: 'claude-code',
|
|
47
|
+
global: false,
|
|
48
|
+
list: false,
|
|
49
|
+
help: false,
|
|
50
|
+
skipUpstream: false,
|
|
51
|
+
force: false,
|
|
52
|
+
check: false,
|
|
53
|
+
yes: false,
|
|
54
|
+
dryRun: false,
|
|
55
|
+
skills: [],
|
|
56
|
+
};
|
|
44
57
|
for (let i = 0; i < argv.length; i++) {
|
|
45
58
|
const a = argv[i];
|
|
46
59
|
if (a === '--global' || a === '-g') opts.global = true;
|
|
47
60
|
else if (a === '--list' || a === '-l') opts.list = true;
|
|
48
61
|
else if (a === '--help' || a === '-h') opts.help = true;
|
|
49
62
|
else if (a === '--skip-upstream') opts.skipUpstream = true;
|
|
63
|
+
else if (a === '--force' || a === '-f') opts.force = true;
|
|
64
|
+
else if (a === '--check') opts.check = true;
|
|
65
|
+
else if (a === '--yes' || a === '-y') opts.yes = true;
|
|
66
|
+
else if (a === '--dry-run' || a === '-n') opts.dryRun = true;
|
|
50
67
|
else if (a === '--agent') opts.agent = argv[++i];
|
|
51
68
|
else if (a.startsWith('--agent=')) opts.agent = a.slice('--agent='.length);
|
|
52
69
|
else if (a.startsWith('-')) {
|
|
@@ -78,6 +95,206 @@ function availableSkills() {
|
|
|
78
95
|
.sort();
|
|
79
96
|
}
|
|
80
97
|
|
|
98
|
+
// --- Scope collisions ----------------------------------------------------------------
|
|
99
|
+
// Agents load every skills directory they can see and do NOT dedupe by name. The same
|
|
100
|
+
// skill installed at two scopes is therefore listed twice and its `description` is paid
|
|
101
|
+
// for twice in the context window, every session, before any work starts. Installing on
|
|
102
|
+
// top of an install at another scope is always waste, never a merge — so we stop.
|
|
103
|
+
|
|
104
|
+
function installedSkillsIn(dir) {
|
|
105
|
+
try {
|
|
106
|
+
return fs
|
|
107
|
+
.readdirSync(dir, { withFileTypes: true })
|
|
108
|
+
.filter((e) => e.isDirectory() && fs.existsSync(path.join(dir, e.name, 'SKILL.md')))
|
|
109
|
+
.map((e) => e.name)
|
|
110
|
+
.sort();
|
|
111
|
+
} catch {
|
|
112
|
+
return [];
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function readStamp(dir) {
|
|
117
|
+
try {
|
|
118
|
+
return JSON.parse(fs.readFileSync(path.join(dir, STAMP_DIR, STAMP_FILE), 'utf8'));
|
|
119
|
+
} catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Every directory the selected agents also read, other than the ones we're about to write:
|
|
125
|
+
// the opposite scope, plus any project-scope dir in a parent directory. That last case is
|
|
126
|
+
// the one people hit by accident — running the installer from a checkouts folder like
|
|
127
|
+
// ~/GitHub instead of inside a repo seeds a directory that shadows nothing and duplicates
|
|
128
|
+
// everything.
|
|
129
|
+
function rivalDirs(agentIds, opts) {
|
|
130
|
+
const home = os.homedir();
|
|
131
|
+
const seen = new Set();
|
|
132
|
+
const out = [];
|
|
133
|
+
const add = (dir, scope) => {
|
|
134
|
+
const resolved = path.resolve(dir);
|
|
135
|
+
if (seen.has(resolved)) return;
|
|
136
|
+
seen.add(resolved);
|
|
137
|
+
out.push({ dir: resolved, scope });
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
for (const id of agentIds) {
|
|
141
|
+
const opposite = opts.global ? AGENTS[id].project : AGENTS[id].global;
|
|
142
|
+
const oppositeRoot = opts.global ? process.cwd() : home;
|
|
143
|
+
for (const rel of opposite) add(path.join(oppositeRoot, rel), opts.global ? 'project' : 'global');
|
|
144
|
+
|
|
145
|
+
// Walk up to, but not into, home — home is the global scope, already covered above.
|
|
146
|
+
let cur = path.dirname(process.cwd());
|
|
147
|
+
while (cur.startsWith(home + path.sep)) {
|
|
148
|
+
for (const rel of AGENTS[id].project) add(path.join(cur, rel), 'ancestor');
|
|
149
|
+
const next = path.dirname(cur);
|
|
150
|
+
if (next === cur) break;
|
|
151
|
+
cur = next;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return out;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function findCollisions(agentIds, opts, wanted, targetDirs) {
|
|
158
|
+
const mine = new Set(targetDirs.map((d) => path.resolve(d)));
|
|
159
|
+
const want = new Set(wanted);
|
|
160
|
+
return rivalDirs(agentIds, opts)
|
|
161
|
+
.filter((r) => !mine.has(r.dir))
|
|
162
|
+
.map((r) => {
|
|
163
|
+
const present = installedSkillsIn(r.dir);
|
|
164
|
+
return { ...r, present, overlap: present.filter((n) => want.has(n)), stamp: readStamp(r.dir) };
|
|
165
|
+
})
|
|
166
|
+
.filter((r) => r.present.length);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function stampLine(stamp) {
|
|
170
|
+
if (!stamp) return 'no install stamp — copied by hand, or by a pre-0.2 installer';
|
|
171
|
+
return `v${stamp.version}, ${stamp.scope} scope, installed ${String(stamp.installedAt).slice(0, 10)}`;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const SCOPE_HINT = {
|
|
175
|
+
global: 'the user-global directory — loaded in every project',
|
|
176
|
+
project: 'a project directory — loaded when working in that repo',
|
|
177
|
+
ancestor: 'a parent of the current directory — almost certainly an installer run from the wrong folder',
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// --- Update planning -----------------------------------------------------------------
|
|
181
|
+
// Re-running the installer is the update path, so a re-run should say what it is about to
|
|
182
|
+
// change before it changes it. Skills carry their own `version` in frontmatter, so the diff
|
|
183
|
+
// is per skill rather than per package — a release usually touches two or three of them.
|
|
184
|
+
|
|
185
|
+
function readSkillVersion(skillDir) {
|
|
186
|
+
try {
|
|
187
|
+
const md = fs.readFileSync(path.join(skillDir, 'SKILL.md'), 'utf8');
|
|
188
|
+
const fm = md.match(/^---\n([\s\S]*?)\n---/);
|
|
189
|
+
if (!fm) return null;
|
|
190
|
+
const v = fm[1].match(/^version:\s*(.*)$/m);
|
|
191
|
+
return v ? v[1].replace(/^["']|["']$/g, '').trim() : null;
|
|
192
|
+
} catch {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Content fingerprint over every file in the skill, so we can tell "same version, edited
|
|
198
|
+
// in place" from "same version, untouched". Hand-edits in a consuming project get silently
|
|
199
|
+
// overwritten by an install; the least we can do is name them first.
|
|
200
|
+
function hashSkillDir(dir) {
|
|
201
|
+
const h = crypto.createHash('sha1');
|
|
202
|
+
const walk = (cur, rel) => {
|
|
203
|
+
let entries;
|
|
204
|
+
try {
|
|
205
|
+
entries = fs.readdirSync(cur, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
206
|
+
} catch {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
for (const e of entries) {
|
|
210
|
+
const next = path.join(cur, e.name);
|
|
211
|
+
const nextRel = rel ? `${rel}/${e.name}` : e.name;
|
|
212
|
+
if (e.isDirectory()) walk(next, nextRel);
|
|
213
|
+
else {
|
|
214
|
+
h.update(nextRel);
|
|
215
|
+
h.update('\0');
|
|
216
|
+
try {
|
|
217
|
+
h.update(fs.readFileSync(next));
|
|
218
|
+
} catch {
|
|
219
|
+
/* unreadable file — its absence from the hash is itself a difference */
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
walk(dir, '');
|
|
225
|
+
return h.digest('hex');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function compareSemver(a, b) {
|
|
229
|
+
if (!a || !b) return null;
|
|
230
|
+
const pa = String(a).split('.').map((n) => parseInt(n, 10));
|
|
231
|
+
const pb = String(b).split('.').map((n) => parseInt(n, 10));
|
|
232
|
+
for (let i = 0; i < 3; i++) {
|
|
233
|
+
const x = pa[i] || 0;
|
|
234
|
+
const y = pb[i] || 0;
|
|
235
|
+
if (x !== y) return x < y ? -1 : 1;
|
|
236
|
+
}
|
|
237
|
+
return 0;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// What installing `wanted` into `base` would actually do, per skill.
|
|
241
|
+
function planFor(base, wanted) {
|
|
242
|
+
return wanted.map((name) => {
|
|
243
|
+
const src = path.join(SKILLS_ROOT, name);
|
|
244
|
+
const dest = path.join(base, name);
|
|
245
|
+
const to = readSkillVersion(src);
|
|
246
|
+
if (!fs.existsSync(path.join(dest, 'SKILL.md'))) return { name, from: null, to, status: 'new' };
|
|
247
|
+
|
|
248
|
+
const from = readSkillVersion(dest);
|
|
249
|
+
const cmp = compareSemver(from, to);
|
|
250
|
+
if (cmp === -1) return { name, from, to, status: 'update' };
|
|
251
|
+
if (cmp === 1) return { name, from, to, status: 'downgrade' };
|
|
252
|
+
if (hashSkillDir(src) !== hashSkillDir(dest)) return { name, from, to, status: 'modified' };
|
|
253
|
+
return { name, from, to, status: 'unchanged' };
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const PLAN_LABEL = {
|
|
258
|
+
new: 'new',
|
|
259
|
+
update: 'update',
|
|
260
|
+
downgrade: 'DOWNGRADE',
|
|
261
|
+
modified: 'local edits will be lost',
|
|
262
|
+
unchanged: 'unchanged',
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
function renderPlan(plan) {
|
|
266
|
+
const shown = plan.filter((p) => p.status !== 'unchanged');
|
|
267
|
+
const width = Math.max(0, ...shown.map((p) => p.name.length));
|
|
268
|
+
for (const p of shown) {
|
|
269
|
+
const ver =
|
|
270
|
+
p.status === 'new'
|
|
271
|
+
? ` -> v${p.to}`
|
|
272
|
+
: p.from === p.to
|
|
273
|
+
? ` v${p.from}`
|
|
274
|
+
: ` v${p.from} -> v${p.to}`;
|
|
275
|
+
console.log(` ${p.name.padEnd(width)}${ver} ${PLAN_LABEL[p.status]}`);
|
|
276
|
+
}
|
|
277
|
+
const same = plan.length - shown.length;
|
|
278
|
+
if (same) console.log(` (${same} unchanged)`);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function confirm(question) {
|
|
282
|
+
return new Promise((resolve) => {
|
|
283
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
284
|
+
let done = false;
|
|
285
|
+
const finish = (value) => {
|
|
286
|
+
if (done) return;
|
|
287
|
+
done = true;
|
|
288
|
+
rl.close();
|
|
289
|
+
resolve(value);
|
|
290
|
+
};
|
|
291
|
+
// Ctrl-D or a closed stdin resolves as "no" — without this the promise never settles
|
|
292
|
+
// and the installer exits silently having done nothing.
|
|
293
|
+
rl.on('close', () => finish(false));
|
|
294
|
+
rl.question(question, (answer) => finish(/^y(es)?$/i.test(answer.trim())));
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
81
298
|
function readUpstreamManifest() {
|
|
82
299
|
try {
|
|
83
300
|
const m = JSON.parse(fs.readFileSync(UPSTREAM_MANIFEST, 'utf8'));
|
|
@@ -199,15 +416,35 @@ Arguments:
|
|
|
199
416
|
|
|
200
417
|
Options:
|
|
201
418
|
-l, --list List available skills (Linchpin + pinned base layer) and exit
|
|
419
|
+
--check Audit every scope this agent reads for duplicate skills; install nothing
|
|
202
420
|
-g, --global Install to the user-global skills dir instead of the project
|
|
203
421
|
--agent <id> Target agent: claude-code (default) | github-copilot | codex | cursor
|
|
204
422
|
| all (installs into every agent's directory)
|
|
205
423
|
--skip-upstream Install only Linchpin skills; don't vendor the upstream base layer
|
|
424
|
+
-f, --force Reinstall everything, and install even if these skills already exist
|
|
425
|
+
at another scope
|
|
426
|
+
-y, --yes Skip the confirmation prompt (implied when not a TTY)
|
|
427
|
+
-n, --dry-run Show what would change and exit without writing anything
|
|
206
428
|
-h, --help Show this help
|
|
207
429
|
|
|
430
|
+
Updating:
|
|
431
|
+
Re-running is the update path. The installer compares each skill's version against what
|
|
432
|
+
is installed, prints what would change, and asks before touching anything. A run with
|
|
433
|
+
nothing to change exits early. --dry-run shows the diff and writes nothing; note that a
|
|
434
|
+
non-interactive run (piped stdin, CI) proceeds without prompting.
|
|
435
|
+
|
|
436
|
+
Scopes:
|
|
437
|
+
Agents load every skills directory they find and do not dedupe by name, so a skill
|
|
438
|
+
installed both globally and in a project is listed twice and costs its description
|
|
439
|
+
twice in every session. Installing over another scope aborts unless you pass --force.
|
|
440
|
+
Use --check to see what is already installed where.
|
|
441
|
+
|
|
208
442
|
Examples:
|
|
209
443
|
npx @linchpinagency/skills # Linchpin skills + base layer -> ./.claude/skills
|
|
210
444
|
npx @linchpinagency/skills wp-studio-cli # one Linchpin skill (+ base layer)
|
|
445
|
+
npx @linchpinagency/skills --check # audit scopes, install nothing
|
|
446
|
+
npx @linchpinagency/skills --dry-run # preview an update, write nothing
|
|
447
|
+
npx @linchpinagency/skills --yes # update without the confirmation prompt
|
|
211
448
|
npx @linchpinagency/skills --skip-upstream # Linchpin skills only
|
|
212
449
|
npx @linchpinagency/skills --agent github-copilot
|
|
213
450
|
npx @linchpinagency/skills --agent all # every agent dir in this project
|
|
@@ -263,16 +500,150 @@ async function main() {
|
|
|
263
500
|
}
|
|
264
501
|
if (!wanted.length && !sources.length) return console.log('No skills to install.');
|
|
265
502
|
|
|
503
|
+
const labels = agentIds.map((id) => AGENTS[id].label).join(', ');
|
|
504
|
+
|
|
505
|
+
if (opts.check) {
|
|
506
|
+
// An audit reports what is actually installed, not just what this package ships —
|
|
507
|
+
// duplicates seeded by an older version or another library still cost context.
|
|
508
|
+
const installedHere = new Set(targets.flatMap((t) => installedSkillsIn(t.dir)));
|
|
509
|
+
const audit = findCollisions(agentIds, opts, [...installedHere], bases);
|
|
510
|
+
console.log(`Skill directories ${labels} reads, for this project:\n`);
|
|
511
|
+
for (const t of targets) {
|
|
512
|
+
const have = installedSkillsIn(t.dir);
|
|
513
|
+
console.log(` [target] ${t.dir}`);
|
|
514
|
+
console.log(` ${have.length} skill(s) — ${stampLine(readStamp(t.dir))}\n`);
|
|
515
|
+
}
|
|
516
|
+
for (const c of audit) {
|
|
517
|
+
console.log(` [${c.scope}] ${c.dir}`);
|
|
518
|
+
console.log(` ${c.present.length} skill(s) — ${stampLine(c.stamp)}`);
|
|
519
|
+
console.log(` ${SCOPE_HINT[c.scope]}`);
|
|
520
|
+
if (c.overlap.length) console.log(` ${c.overlap.length} duplicate(s): ${c.overlap.join(', ')}`);
|
|
521
|
+
console.log();
|
|
522
|
+
}
|
|
523
|
+
const dupes = audit.reduce((n, c) => n + c.overlap.length, 0);
|
|
524
|
+
if (!dupes) {
|
|
525
|
+
console.log('No duplicate skills across scopes.');
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
console.log(`${dupes} duplicate skill copies across scopes. Each one's description is loaded`);
|
|
529
|
+
console.log('once per copy, every session. Remove whichever copy you do not want.');
|
|
530
|
+
process.exitCode = 1;
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Plan first: whether a collision is a problem depends on whether this run would be
|
|
535
|
+
// *creating* the second copy or merely maintaining one that already exists.
|
|
536
|
+
const version = packageVersion();
|
|
537
|
+
const plans = targets.map((t) => ({ target: t, plan: planFor(t.dir, wanted) }));
|
|
538
|
+
const changed = plans.flatMap((p) => p.plan).filter((p) => p.status !== 'unchanged');
|
|
539
|
+
|
|
540
|
+
// A skill that is 'new' in every target but already present at another scope is a
|
|
541
|
+
// duplicate about to be born — that we refuse. A skill already installed here is an
|
|
542
|
+
// update; refusing it would only strand someone on a stale copy without removing the
|
|
543
|
+
// duplication, so it warns instead.
|
|
544
|
+
const arriving = new Set(
|
|
545
|
+
plans.flatMap(({ plan }) => plan.filter((p) => p.status === 'new').map((p) => p.name))
|
|
546
|
+
);
|
|
547
|
+
const existing = new Set(
|
|
548
|
+
plans.flatMap(({ plan }) => plan.filter((p) => p.status !== 'new').map((p) => p.name))
|
|
549
|
+
);
|
|
550
|
+
const collisions = findCollisions(agentIds, opts, wanted, bases);
|
|
551
|
+
const wouldCreate = collisions
|
|
552
|
+
.map((c) => ({ ...c, overlap: c.overlap.filter((n) => arriving.has(n) && !existing.has(n)) }))
|
|
553
|
+
.filter((c) => c.overlap.length);
|
|
554
|
+
const preExisting = collisions
|
|
555
|
+
.map((c) => ({ ...c, overlap: c.overlap.filter((n) => existing.has(n)) }))
|
|
556
|
+
.filter((c) => c.overlap.length);
|
|
557
|
+
|
|
558
|
+
if (wouldCreate.length && !opts.force) {
|
|
559
|
+
const dupes = wouldCreate.reduce((n, c) => n + c.overlap.length, 0);
|
|
560
|
+
console.error(`Refusing to install: ${dupes} of these skills are already installed at another scope.\n`);
|
|
561
|
+
for (const c of wouldCreate) {
|
|
562
|
+
console.error(` ${c.dir}`);
|
|
563
|
+
console.error(` ${SCOPE_HINT[c.scope]}`);
|
|
564
|
+
console.error(` ${stampLine(c.stamp)}`);
|
|
565
|
+
console.error(` would duplicate: ${c.overlap.join(', ')}\n`);
|
|
566
|
+
}
|
|
567
|
+
console.error(`${labels} loads every directory it finds and does not dedupe by name, so each`);
|
|
568
|
+
console.error('duplicate is listed twice and costs its description twice in every session.\n');
|
|
569
|
+
console.error('Pick one scope:');
|
|
570
|
+
console.error(' - keep the existing copy — nothing to do here; this install would be redundant');
|
|
571
|
+
console.error(' - move them here — drop just the duplicates at the other scope, then re-run:');
|
|
572
|
+
for (const c of wouldCreate) {
|
|
573
|
+
console.error(` (cd ${c.dir} && rm -rf ${c.overlap.join(' ')})`);
|
|
574
|
+
}
|
|
575
|
+
console.error(' - keep both anyway — re-run with --force');
|
|
576
|
+
console.error('\nRun with --check to audit every scope without installing.');
|
|
577
|
+
process.exit(1);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
if (preExisting.length) {
|
|
581
|
+
const dupes = preExisting.reduce((n, c) => n + c.overlap.length, 0);
|
|
582
|
+
console.log(`! ${dupes} of these skills are also installed at another scope, and were before this run:`);
|
|
583
|
+
for (const c of preExisting) console.log(` ${c.dir} (${c.scope})`);
|
|
584
|
+
console.log(' Updating here does not fix that. Run --check for the duplicates and how to drop them.\n');
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// A re-run is the update path, so most runs land here with a handful of skills to
|
|
588
|
+
// update and the rest already current.
|
|
589
|
+
if (!changed.length && !opts.force) {
|
|
590
|
+
console.log(`Already up to date — ${wanted.length} skill(s) for ${labels}, nothing to change.`);
|
|
591
|
+
if (!opts.dryRun) console.log('Re-run with --force to reinstall anyway.');
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
if (changed.length) {
|
|
596
|
+
console.log(`@linchpinagency/skills v${version} — ${labels}\n`);
|
|
597
|
+
for (const { target, plan } of plans) {
|
|
598
|
+
if (plans.length > 1) console.log(`${target.dir}`);
|
|
599
|
+
renderPlan(plan);
|
|
600
|
+
if (plans.length > 1) console.log();
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const edited = changed.filter((p) => p.status === 'modified');
|
|
604
|
+
if (edited.length) {
|
|
605
|
+
console.log(`\n! ${edited.length} skill(s) were edited in place and will be overwritten.`);
|
|
606
|
+
console.log(' Skills are owned by this package — change them in the library, not the install.');
|
|
607
|
+
}
|
|
608
|
+
const down = changed.filter((p) => p.status === 'downgrade');
|
|
609
|
+
if (down.length) {
|
|
610
|
+
console.log(`\n! ${down.length} skill(s) would go BACKWARDS — this package is older than what is installed.`);
|
|
611
|
+
console.log(' Check the version you invoked before continuing.');
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const counts = ['new', 'update', 'downgrade', 'modified']
|
|
615
|
+
.map((k) => [k, changed.filter((p) => p.status === k).length])
|
|
616
|
+
.filter(([, n]) => n)
|
|
617
|
+
.map(([k, n]) => `${n} ${k}`)
|
|
618
|
+
.join(', ');
|
|
619
|
+
console.log(`\n${changed.length} change(s): ${counts}`);
|
|
620
|
+
|
|
621
|
+
if (opts.dryRun) {
|
|
622
|
+
console.log('\nDry run — nothing was written.');
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
if (!opts.yes && !opts.force) {
|
|
627
|
+
if (!process.stdin.isTTY) {
|
|
628
|
+
console.log('Non-interactive — proceeding. Pass --yes to silence this notice.');
|
|
629
|
+
} else if (!(await confirm('\nApply? [y/N] '))) {
|
|
630
|
+
console.log('Nothing changed.');
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
console.log();
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
const applied = new Set(changed.map((p) => p.name));
|
|
266
638
|
for (const base of bases) {
|
|
267
639
|
fs.mkdirSync(base, { recursive: true });
|
|
268
640
|
for (const name of wanted) {
|
|
269
641
|
const dest = path.join(base, name);
|
|
270
642
|
fs.rmSync(dest, { recursive: true, force: true });
|
|
271
643
|
fs.cpSync(path.join(SKILLS_ROOT, name), dest, { recursive: true });
|
|
272
|
-
console.log(`✓ ${name} -> ${dest}`);
|
|
644
|
+
if (applied.has(name) || opts.force) console.log(`✓ ${name} -> ${dest}`);
|
|
273
645
|
}
|
|
274
646
|
}
|
|
275
|
-
const labels = agentIds.map((id) => AGENTS[id].label).join(', ');
|
|
276
647
|
console.log(`\nInstalled ${wanted.length} Linchpin skill(s) for ${labels}.`);
|
|
277
648
|
|
|
278
649
|
const upstream = [];
|
|
@@ -286,7 +657,6 @@ async function main() {
|
|
|
286
657
|
}
|
|
287
658
|
|
|
288
659
|
// Stamp last, so `upstream` reflects what actually landed rather than what was intended.
|
|
289
|
-
const version = packageVersion();
|
|
290
660
|
const stamped = targets.filter((t) => writeStamp(t, { version, opts, skills: wanted, upstream }));
|
|
291
661
|
if (stamped.length && agentIds.includes('claude-code')) {
|
|
292
662
|
const rel = path.join(STAMP_DIR, CHECKER);
|
package/package.json
CHANGED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: agent-capabilities
|
|
3
|
+
description: Right-size what an agent loads on a project — audit skill installs for cross-scope duplicates, work out which MCP servers this repo actually needs, and scope them so every session stops paying for all of them. Use when context feels full before any work starts, when the same skill appears twice in the skills list, when deciding whether an MCP server belongs globally or in one repo, when onboarding a repo, or when someone asks which skills or servers a project should have. Not for writing a skill — use `write-a-linchpin-skill`.
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Agent capabilities
|
|
8
|
+
|
|
9
|
+
Skills and MCP servers are loaded by the **harness, not the model**, and they are loaded
|
|
10
|
+
*before* the first message. Every installed skill spends its `description` on every session;
|
|
11
|
+
every connected MCP server spends its full tool schemas. Neither is free, and neither asks
|
|
12
|
+
permission.
|
|
13
|
+
|
|
14
|
+
That cost is invisible until you look for it, which is why it drifts. The failure mode is
|
|
15
|
+
not one bad skill — it is thirty fine skills installed at two scopes and nine MCP servers
|
|
16
|
+
where a repo needed two.
|
|
17
|
+
|
|
18
|
+
**The rule this skill enforces:** a project loads what it uses, once.
|
|
19
|
+
|
|
20
|
+
## When to use
|
|
21
|
+
|
|
22
|
+
- Context feels consumed before you have typed anything.
|
|
23
|
+
- A skill appears **twice** in the skills list — a real symptom with a real cause.
|
|
24
|
+
- Deciding where a new MCP server belongs: this repo, or every repo.
|
|
25
|
+
- Onboarding a repo, or handing one to someone else.
|
|
26
|
+
- Someone asks "which skills/servers should this project have?"
|
|
27
|
+
|
|
28
|
+
**Not this skill:** authoring or reviewing a skill's content —
|
|
29
|
+
[`write-a-linchpin-skill`](../write-a-linchpin-skill/SKILL.md). Identifying the project's
|
|
30
|
+
shape — [`project-context`](../project-context/SKILL.md), whose orientation pass this
|
|
31
|
+
extends.
|
|
32
|
+
|
|
33
|
+
## Owns
|
|
34
|
+
|
|
35
|
+
Canonical for: **which** capabilities a project should load and **at what scope**, and the
|
|
36
|
+
MCP-server-per-project decision.
|
|
37
|
+
|
|
38
|
+
Deliberately **not** owned here:
|
|
39
|
+
|
|
40
|
+
- *Detecting* duplicate skill installs — the installer owns that (`--check`, below). This
|
|
41
|
+
skill decides what to do about the answer.
|
|
42
|
+
- What any individual skill or server *does* — that's its own skill.
|
|
43
|
+
|
|
44
|
+
## Preflight
|
|
45
|
+
|
|
46
|
+
Orient first ([`project-context`](../project-context/SKILL.md)) — the right capability set
|
|
47
|
+
follows from the project's shape, not its name. Then read what is actually loaded:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npx @linchpinagency/skills --check # every skills dir this agent reads, and any duplicates
|
|
51
|
+
claude mcp list # configured servers and their scope
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`--check` installs nothing and exits non-zero when duplicates exist, so it also works as a
|
|
55
|
+
CI or hook check.
|
|
56
|
+
|
|
57
|
+
## Procedure
|
|
58
|
+
|
|
59
|
+
### 1. Audit the skill installs
|
|
60
|
+
|
|
61
|
+
Agents load **every** skills directory they find and **do not dedupe by name**. The same
|
|
62
|
+
skill at two scopes is listed twice and billed twice, every session.
|
|
63
|
+
|
|
64
|
+
`--check` reports each directory, its scope, and the overlap. Three scopes collide in
|
|
65
|
+
practice — user-global, project, and a stray install in a *parent* directory (running the
|
|
66
|
+
installer from a checkouts folder instead of inside a repo). Details, per-agent directory
|
|
67
|
+
map, and the remediation commands: [`references/skills-audit.md`](references/skills-audit.md).
|
|
68
|
+
|
|
69
|
+
Resolve to **one scope per skill**. The installer now refuses a colliding install rather
|
|
70
|
+
than silently creating the second copy.
|
|
71
|
+
|
|
72
|
+
### 2. Decide the skill set for this project
|
|
73
|
+
|
|
74
|
+
Global is the wrong default for anything stack-specific. A WordPress skill installed
|
|
75
|
+
globally is loaded on every Workers repo you open, forever.
|
|
76
|
+
|
|
77
|
+
| Install at | What belongs there |
|
|
78
|
+
| --- | --- |
|
|
79
|
+
| **User-global** | Only what is true on *every* repo you touch — `task-tracking`, `commit-and-release`, `project-context` |
|
|
80
|
+
| **Project** | Everything stack-specific — the `wp-*` set on WordPress repos, and nowhere else |
|
|
81
|
+
|
|
82
|
+
If a skill would never fire in this repo, it should not be installed in this repo.
|
|
83
|
+
|
|
84
|
+
### 3. Decide the MCP servers for this project
|
|
85
|
+
|
|
86
|
+
Server schemas cost substantially more than skill descriptions — this is usually the larger
|
|
87
|
+
half of the problem. Derive the set from the work, not from what is already configured.
|
|
88
|
+
|
|
89
|
+
The skill→server dependency map, the scope table, and the per-project-shape recipes live in
|
|
90
|
+
[`references/mcp-scoping.md`](references/mcp-scoping.md). The short version:
|
|
91
|
+
|
|
92
|
+
| Tier | Servers |
|
|
93
|
+
| --- | --- |
|
|
94
|
+
| Baseline, user scope | `clickup` — every unit of work routes through it |
|
|
95
|
+
| Per project | The one or two the repo's actual work needs |
|
|
96
|
+
| Never global | Anything stack-specific (`wordpress-studio`, `pressable`, `shopify-dev-mcp`, `shadcn`) |
|
|
97
|
+
|
|
98
|
+
**Prefer project scope (`.mcp.json`, committed).** It travels with the repo, so the next
|
|
99
|
+
person gets the right set without being told, and it disappears when they leave the repo.
|
|
100
|
+
|
|
101
|
+
### 4. Make the decision stick
|
|
102
|
+
|
|
103
|
+
A decision nobody re-reads decays. Record it in the project, three complementary ways:
|
|
104
|
+
|
|
105
|
+
1. **`.mcp.json`, committed** — the servers this repo needs, as config rather than prose.
|
|
106
|
+
2. **A line in the project's `CLAUDE.md`/`AGENTS.md`** naming the intended capability set,
|
|
107
|
+
so a human or agent adding a server knows there was an intent to violate.
|
|
108
|
+
3. **A `SessionStart` hook** running `npx @linchpinagency/skills --check` when you want
|
|
109
|
+
duplicates to surface on their own rather than when someone notices.
|
|
110
|
+
|
|
111
|
+
The hook snippet and the honest tradeoff (it costs a subprocess on every session to catch a
|
|
112
|
+
rare, sticky misconfiguration) are in
|
|
113
|
+
[`references/skills-audit.md`](references/skills-audit.md).
|
|
114
|
+
|
|
115
|
+
## Guardrails
|
|
116
|
+
|
|
117
|
+
- **Never `rm -rf` a whole skills directory to fix an overlap.** Those directories hold
|
|
118
|
+
skills from several sources — remove the specific duplicated skill directories. `--check`
|
|
119
|
+
prints the exact list.
|
|
120
|
+
- **Never hand-edit skills inside a consuming project's `.claude/skills/`** — the installer
|
|
121
|
+
overwrites them. Change them in this library and re-install.
|
|
122
|
+
- **Never add an MCP server at user scope to solve a one-repo problem.** That is how nine
|
|
123
|
+
servers happen. Use `-s project` or `-s local`.
|
|
124
|
+
- **Never remove a server or skill someone else's workflow depends on without saying so** —
|
|
125
|
+
scope is a shared decision on a shared repo.
|
|
126
|
+
- Removing a server can silently disable a skill that needs it (`task-tracking` without
|
|
127
|
+
`clickup`). Check the dependency map before pruning.
|
|
128
|
+
- If you cannot tell whether a capability is used, leave it and say so. Under-loading is a
|
|
129
|
+
cheaper mistake than a broken workflow, but a *silent* prune is worse than either.
|
|
130
|
+
|
|
131
|
+
## Done
|
|
132
|
+
|
|
133
|
+
- [ ] `--check` reports no cross-scope duplicates, or the remaining ones are deliberate.
|
|
134
|
+
- [ ] Every installed skill could plausibly fire in this repo.
|
|
135
|
+
- [ ] Stack-specific skills are at project scope, not user scope.
|
|
136
|
+
- [ ] The MCP server set was derived from the repo's work, not inherited.
|
|
137
|
+
- [ ] Project-scoped servers are in a committed `.mcp.json`.
|
|
138
|
+
- [ ] The intended set is recorded where the next person will see it.
|
|
139
|
+
- [ ] Nothing was pruned that a skill still depends on.
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# MCP scoping — which servers a project needs, and where to put them
|
|
2
|
+
|
|
3
|
+
An MCP server's cost is its **tool schemas**, loaded before the first message. A handful of
|
|
4
|
+
servers outweighs the entire skills library. Servers are also the easier mistake to make:
|
|
5
|
+
adding one is a single command, and the default scope makes it apply beyond the repo you
|
|
6
|
+
were in.
|
|
7
|
+
|
|
8
|
+
## Scopes
|
|
9
|
+
|
|
10
|
+
`claude mcp add` defaults to **`local`** — private to you, tied to the current project.
|
|
11
|
+
That default is fine for experiments and wrong for anything a teammate also needs.
|
|
12
|
+
|
|
13
|
+
| Scope | Stored in | Applies to | Shared? | Use for |
|
|
14
|
+
| --- | --- | --- | --- | --- |
|
|
15
|
+
| `local` (default) | `~/.claude.json`, under this project | This repo, you only | No | Trying a server out; personal credentials |
|
|
16
|
+
| `project` | `.mcp.json` in the repo root | This repo, everyone | **Yes, via git** | The servers the repo's work actually needs |
|
|
17
|
+
| `user` | `~/.claude.json`, top level | **Every** repo you open | No | Only genuinely universal servers |
|
|
18
|
+
|
|
19
|
+
Precedence when a name appears more than once: `local` > `project` > `user`.
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
claude mcp list # what is configured, and where
|
|
23
|
+
claude mcp get <name> # one server's details
|
|
24
|
+
claude mcp add <name> -s project -- <command> # committed, shared with the repo
|
|
25
|
+
claude mcp remove <name> -s user # prune from the global scope
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`.mcp.json` servers are approval-gated on first encounter — a repo cannot silently run a
|
|
29
|
+
server on someone's machine. Approvals are recorded per project in `~/.claude.json`
|
|
30
|
+
(`enabledMcpjsonServers` / `disabledMcpjsonServers`).
|
|
31
|
+
|
|
32
|
+
**Prefer `project`.** It travels with the repo, documents itself, and disappears when you
|
|
33
|
+
leave the directory. `user` scope is where sprawl accumulates, because nothing about
|
|
34
|
+
working in an unrelated repo ever reminds you it is there.
|
|
35
|
+
|
|
36
|
+
## Which skills need which servers
|
|
37
|
+
|
|
38
|
+
Removing a server can silently disable a skill. Check here before pruning.
|
|
39
|
+
|
|
40
|
+
| Skill | Needs | Degrades to |
|
|
41
|
+
| --- | --- | --- |
|
|
42
|
+
| [`task-tracking`](../../task-tracking/SKILL.md) | `clickup` | Manual task lookup; `NO-TASK` scope |
|
|
43
|
+
| [`wp-studio-cli`](../../wp-studio-cli/SKILL.md) | `wordpress-studio` | The `studio` CLI |
|
|
44
|
+
| [`browser-automation`](../../browser-automation/SKILL.md) | `chrome-devtools` | Playwright headless |
|
|
45
|
+
| [`wp-pressable`](../../wp-pressable/SKILL.md) | `pressable`, `1password` | SSH + WP-CLI, manual credentials |
|
|
46
|
+
| [`design-previews`](../../design-previews/SKILL.md) | `chrome-devtools` | Playwright headless |
|
|
47
|
+
| [`wp-audit`](../../wp-audit/SKILL.md) | `chrome-devtools` | Lighthouse CLI |
|
|
48
|
+
| [`web-qa`](../../web-qa/SKILL.md) | via `browser-automation` | as above |
|
|
49
|
+
|
|
50
|
+
Every one of these has a fallback, which is the point: a missing server is a slower path,
|
|
51
|
+
not a broken one. That makes pruning safer than it feels — but say what you pruned.
|
|
52
|
+
|
|
53
|
+
## Deciding the set
|
|
54
|
+
|
|
55
|
+
Derive from the work, not from what is already configured. Ask what this repo's tasks
|
|
56
|
+
actually touch.
|
|
57
|
+
|
|
58
|
+
| Server | Include when |
|
|
59
|
+
| --- | --- |
|
|
60
|
+
| `clickup` | Always — every unit of work routes through it (house rule) |
|
|
61
|
+
| `chrome-devtools` | The repo has a UI someone looks at |
|
|
62
|
+
| `wordpress-studio` | WordPress site repo with a registered Studio site |
|
|
63
|
+
| `pressable` + `1password` | Pressable-hosted, and you operate the server |
|
|
64
|
+
| `figma` | There is a Figma file in play for this project |
|
|
65
|
+
| `playwright` | Scripted or CI browser runs — not needed alongside `chrome-devtools` for ad-hoc QA |
|
|
66
|
+
| `shadcn` | React project using shadcn/ui — never on a WordPress repo |
|
|
67
|
+
| `shopify-dev-mcp` | Shopify project |
|
|
68
|
+
|
|
69
|
+
Then read it back against the repo's shape ([`project-context`](../../project-context/SKILL.md)):
|
|
70
|
+
|
|
71
|
+
- **WordPress site repo, Pressable-hosted** — `clickup` (user) + `wordpress-studio`,
|
|
72
|
+
`pressable`, `chrome-devtools` (project). `1password` local, since credentials are personal.
|
|
73
|
+
- **WordPress plugin/product repo** — `clickup` + `wordpress-studio`. No `pressable`; the
|
|
74
|
+
repo does not own a server.
|
|
75
|
+
- **Cloudflare Workers service** — `clickup`, and `chrome-devtools` only if it serves a UI.
|
|
76
|
+
No WordPress servers at all.
|
|
77
|
+
- **Design-phase project** — `clickup`, `figma`, `chrome-devtools`. Add the WordPress set
|
|
78
|
+
when build starts, not before.
|
|
79
|
+
|
|
80
|
+
A server that is not on the list for this repo's shape is one you are paying for in every
|
|
81
|
+
session and using in none.
|
|
82
|
+
|
|
83
|
+
## Migrating a sprawling global set
|
|
84
|
+
|
|
85
|
+
Servers accumulate at `user` scope because that is where they got added first. Moving them
|
|
86
|
+
is mechanical:
|
|
87
|
+
|
|
88
|
+
1. `claude mcp list` — write down what is at `user` scope.
|
|
89
|
+
2. For each, name the repos that actually use it. If it is more than "all of them", it does
|
|
90
|
+
not belong at `user`.
|
|
91
|
+
3. `claude mcp remove <name> -s user`, then `claude mcp add <name> -s project -- …` in each
|
|
92
|
+
repo that needs it.
|
|
93
|
+
4. Commit each repo's `.mcp.json` so the next person inherits the decision.
|
|
94
|
+
|
|
95
|
+
Do this per server, verifying as you go — not as one sweep. A server you remove and forget
|
|
96
|
+
to re-add somewhere shows up as a skill mysteriously taking the slow path weeks later.
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# Skills audit — scopes, duplicates, and the session hook
|
|
2
|
+
|
|
3
|
+
Detection is owned by the installer (`npx @linchpinagency/skills --check`). This file
|
|
4
|
+
explains what it reports and what to do about it.
|
|
5
|
+
|
|
6
|
+
## Why duplicates happen
|
|
7
|
+
|
|
8
|
+
Agents load **every** skills directory they can see and **do not dedupe by name**. Two
|
|
9
|
+
copies of `task-tracking` means two entries in the skills list and two copies of its
|
|
10
|
+
description in the context window, every session, forever.
|
|
11
|
+
|
|
12
|
+
Nothing warns you. The installer is happy to write into any directory you point it at, and
|
|
13
|
+
until v0.2 neither install knew the other existed.
|
|
14
|
+
|
|
15
|
+
## The three colliding scopes
|
|
16
|
+
|
|
17
|
+
| Scope | Path | How it happens |
|
|
18
|
+
| --- | --- | --- |
|
|
19
|
+
| **User-global** | `~/.claude/skills` | `--global` — loaded in *every* project |
|
|
20
|
+
| **Project** | `<repo>/.claude/skills` | The default — loaded in that repo |
|
|
21
|
+
| **Ancestor** | `<parent-of-repo>/.claude/skills` | Running the installer from a checkouts folder (`~/GitHub`) instead of inside a repo |
|
|
22
|
+
|
|
23
|
+
The ancestor case is the sneaky one: it looks like a project install, sits above every
|
|
24
|
+
sibling checkout, and is easy to create by pressing enter in the wrong terminal.
|
|
25
|
+
|
|
26
|
+
## Per-agent directories
|
|
27
|
+
|
|
28
|
+
Each agent reads its own directories, which is why the installer fans out. Only the
|
|
29
|
+
directories for the agent you are *running* cost that agent context — a `.codex/skills` copy
|
|
30
|
+
is invisible to Claude Code, and vice versa.
|
|
31
|
+
|
|
32
|
+
| Agent | Project | Global |
|
|
33
|
+
| --- | --- | --- |
|
|
34
|
+
| Claude Code | `.claude/skills` | `~/.claude/skills` |
|
|
35
|
+
| GitHub Copilot | `.agents/skills`, `.github/skills` | `~/.copilot/skills` |
|
|
36
|
+
| Codex | `.codex/skills` | `~/.codex/skills` |
|
|
37
|
+
| Cursor | `.cursor/skills` | `~/.cursor/skills` |
|
|
38
|
+
|
|
39
|
+
So a repo carrying all four agents' copies is not wasting Claude's context — it is just
|
|
40
|
+
using disk. Don't "clean up" another agent's directory to save context that was never spent.
|
|
41
|
+
|
|
42
|
+
## Auditing
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npx @linchpinagency/skills --check # Claude Code, this project
|
|
46
|
+
npx @linchpinagency/skills --check --agent codex # a different agent
|
|
47
|
+
npx @linchpinagency/skills --check --global # from the global scope's point of view
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Reports every directory the agent reads, its skill count, its install stamp, and the exact
|
|
51
|
+
overlapping skill names. Exits `1` when duplicates exist, so it composes into CI or a hook.
|
|
52
|
+
|
|
53
|
+
## Fixing an overlap
|
|
54
|
+
|
|
55
|
+
Decide which scope should own each duplicated skill, then remove the *other copy only*:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
cd ~/.claude/skills && rm -rf task-tracking wp-studio-cli # …the names --check printed
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`--check` prints this command with the names filled in. **Never remove the whole
|
|
62
|
+
directory** — it holds skills from several sources (upstream WordPress, Cloudflare, Figma),
|
|
63
|
+
and the overlap is usually a subset.
|
|
64
|
+
|
|
65
|
+
Then re-install into the scope you chose. The installer refuses a colliding install:
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
Refusing to install: 22 of these skills are already installed at another scope.
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`--force` overrides it, for the rare case where you genuinely want both.
|
|
72
|
+
|
|
73
|
+
## Choosing the scope
|
|
74
|
+
|
|
75
|
+
| Install at | What belongs there |
|
|
76
|
+
| --- | --- |
|
|
77
|
+
| User-global | Only skills true on *every* repo — `task-tracking`, `commit-and-release`, `project-context` |
|
|
78
|
+
| Project | Everything stack-specific — the `wp-*` set on WordPress repos, and nowhere else |
|
|
79
|
+
|
|
80
|
+
Global feels convenient and is how the sprawl starts: a WordPress skill installed globally
|
|
81
|
+
is loaded on every Workers repo you open. Project scope also means the skill set travels
|
|
82
|
+
with the repo, so a teammate cloning it gets the right tools without being told.
|
|
83
|
+
|
|
84
|
+
## SessionStart hook
|
|
85
|
+
|
|
86
|
+
Surfaces duplicates on their own rather than when someone happens to notice. In
|
|
87
|
+
`.claude/settings.json` (project) or `~/.claude/settings.json` (all projects):
|
|
88
|
+
|
|
89
|
+
```json
|
|
90
|
+
{
|
|
91
|
+
"hooks": {
|
|
92
|
+
"SessionStart": [
|
|
93
|
+
{
|
|
94
|
+
"hooks": [
|
|
95
|
+
{
|
|
96
|
+
"type": "command",
|
|
97
|
+
"command": "npx -y @linchpinagency/skills --check | tail -3"
|
|
98
|
+
}
|
|
99
|
+
]
|
|
100
|
+
}
|
|
101
|
+
]
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
**The honest tradeoff:** this spends a subprocess on every session to catch a rare,
|
|
107
|
+
one-time misconfiguration. It earns its place on a machine where installs drift — several
|
|
108
|
+
repos, several agents, more than one person running the installer. On a stable single-repo
|
|
109
|
+
setup, run `--check` by hand when onboarding and skip the hook.
|
|
110
|
+
|
|
111
|
+
`npx` resolves from cache after the first run. If session startup latency matters more than
|
|
112
|
+
freshness, install once (`npm i -g @linchpinagency/skills`) and call `skills --check`.
|
|
113
|
+
|
|
114
|
+
Hook mechanics and the destructive-command guard are owned by
|
|
115
|
+
[`safety-hooks`](../../safety-hooks/SKILL.md) — this is one more `SessionStart` entry
|
|
116
|
+
alongside it, not a competing configuration.
|
|
@@ -39,6 +39,8 @@ write and what release-please generates. Defers task resolution **and branch nam
|
|
|
39
39
|
| --- | --- |
|
|
40
40
|
| `commitlint.config.js` → `type-enum` | The types **this** repo accepts — they are not the same everywhere |
|
|
41
41
|
| `commitlint.config.js` → `parserOpts.headerPattern` | The exact header regex, including which scopes count (`NO-TASK`, sometimes `NO-JIRA`, `#123`) |
|
|
42
|
+
| `commitlint.config.js` → `extends` | What is inherited but never written down locally. `@commitlint/config-conventional` is where `header-max-length` comes from — see below |
|
|
43
|
+
| `.github/workflows/` → the commit-message job | Whether CI **also** lints the PR title, which the husky hook never sees |
|
|
42
44
|
| `release-please-config.json` → `changelog-sections` | Which types appear in the changelog, and which are hidden |
|
|
43
45
|
| `release-please-config.json` → `extra-files` | **Every file whose version string is machine-owned** — never hand-edit these |
|
|
44
46
|
| `.release-please-manifest.json` | The current version (also machine-owned) |
|
|
@@ -67,10 +69,27 @@ fix(LINCHPIN-4980): Correct masthead gutter on columns children
|
|
|
67
69
|
improve(NO-TASK): Tidy editorconfig and ignore rules
|
|
68
70
|
```
|
|
69
71
|
|
|
72
|
+
**Length: 100 characters, and it is an error.** `header-max-length` is not in Linchpin's
|
|
73
|
+
shared config — it arrives through `extends: ['@commitlint/config-conventional']`, at
|
|
74
|
+
severity `error`, counting the whole header including `type(SCOPE): `. A task key plus a
|
|
75
|
+
colon and a space is ~22 of the budget, so the subject has about 78. Count it before you
|
|
76
|
+
push; a title that reads well in a PR form is easily 110.
|
|
77
|
+
|
|
70
78
|
**Punctuation gotcha:** the header pattern accepts only letters, digits, spaces, commas and
|
|
71
|
-
hyphens in the subject
|
|
72
|
-
|
|
73
|
-
|
|
79
|
+
hyphens in the subject — `[\w\d\s,\-]`. Everything else cuts the parsed subject short at
|
|
80
|
+
the first offender, silently, because what is left still matches. Periods, colons,
|
|
81
|
+
parentheses and slashes are the obvious ones: `Update wp-scripts to v27.1` parses as
|
|
82
|
+
`Update wp-scripts to v27`.
|
|
83
|
+
|
|
84
|
+
The two that catch people writing a prose-y PR title are less obvious, because both are
|
|
85
|
+
what a careful writer reaches for:
|
|
86
|
+
|
|
87
|
+
- **The apostrophe.** `\w` is `[A-Za-z0-9_]`, so `WooCommerce's account menu` parses as
|
|
88
|
+
`WooCommerce`. Write `the WooCommerce account menu`.
|
|
89
|
+
- **The em dash.** Not in the set either, so a title with a `—` clause keeps only the half
|
|
90
|
+
before it. Use a comma, or split the thought into the body.
|
|
91
|
+
|
|
92
|
+
Keep version numbers, possessives, dashes and punctuation out of subjects entirely.
|
|
74
93
|
|
|
75
94
|
**Breaking changes:** `feat(KEY)!: …` or a `BREAKING CHANGE: …` footer. This drives a major
|
|
76
95
|
version bump, so use it deliberately.
|
|
@@ -90,6 +109,38 @@ use `main` as a scope for normal work.
|
|
|
90
109
|
5. **Push the branch and open the PR.** Title follows the same convention (squash merges use
|
|
91
110
|
the PR title as the commit message, so a malformed title breaks the changelog); body
|
|
92
111
|
links the ClickUp task. → PR open against the base branch, never a push to `main`.
|
|
112
|
+
|
|
113
|
+
**The PR title is linted separately, and the husky hook never saw it.** The shared
|
|
114
|
+
workflow pipes `gh pr view --json title -q .title` through commitlint as its own step, so
|
|
115
|
+
every commit can pass locally and the PR still go red — the usual cause is the 100-char
|
|
116
|
+
limit, since a PR title is written in a web form with no counter. Check it before you
|
|
117
|
+
wait on CI:
|
|
118
|
+
|
|
119
|
+
Run these from inside the repo whose rules you are checking — commitlint resolves the
|
|
120
|
+
config from the working directory, and from anywhere else it reports
|
|
121
|
+
"Please add rules to your `commitlint.config.js`" rather than an answer about your title:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
# Before opening: is the title you are about to use legal?
|
|
125
|
+
title="feat(PROJ-123): Subject in sentence case"
|
|
126
|
+
printf '%s' "$title" | npx commitlint --verbose
|
|
127
|
+
|
|
128
|
+
# Already open: lint the live title, and see its length.
|
|
129
|
+
gh pr view <number> --json title -q .title | tee >(awk '{print length" chars"}' >&2) \
|
|
130
|
+
| npx commitlint --verbose
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Fixing it takes two steps, because the shared workflow runs on
|
|
134
|
+
`pull_request: types: [opened, synchronize, reopened]` — `edited` is not in that list, so
|
|
135
|
+
correcting the title does **not** re-run anything and the PR sits on a stale red check:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
gh pr edit <number> --title 'feat(PROJ-123): A legal subject'
|
|
139
|
+
gh run rerun <run-id> --job <job-id> # both are in `gh pr checks <number>`
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
`gh run rerun` is refused while the run is still in progress, so wait for it to finish
|
|
143
|
+
before retrying.
|
|
93
144
|
6. **Let release-please do the release.** Merging to `main` opens or updates a release PR
|
|
94
145
|
that bumps versions and writes `CHANGELOG.md`; merging *that* tags the release, which is
|
|
95
146
|
what deploy workflows trigger from. → Confirm the release PR reflects your change under
|
|
@@ -131,6 +182,9 @@ release note — pick the type that reflects what actually changed.
|
|
|
131
182
|
- [ ] Type is in **this** repo's `type-enum`; subject is sentence case with no trailing
|
|
132
183
|
period or mid-subject punctuation.
|
|
133
184
|
- [ ] Commit passed the husky/commitlint hook without `--no-verify`.
|
|
134
|
-
- [ ]
|
|
185
|
+
- [ ] Header is under 100 characters, and carries no apostrophe, em dash or other
|
|
186
|
+
punctuation outside `[\w\d\s,\-]`.
|
|
187
|
+
- [ ] PR title follows the same convention, was linted in its own right (not just the
|
|
188
|
+
commits), and the body links the ClickUp task.
|
|
135
189
|
- [ ] No version string, `CHANGELOG.md`, manifest, or tag was written by hand.
|
|
136
190
|
- [ ] The resulting release PR shows the change under the expected section.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: project-context
|
|
3
3
|
description: Orient before acting on a Linchpin project — identify the repo and branch, the local environment (Studio, wp-env, LocalWP), the host (Pressable or Cloudflare), the ClickUp space, and the release model, from .linchpin.json, composer.json, package.json, and the git remote. Use when starting work on an unfamiliar repo, before running commands that assume an environment, when a skill's Preflight needs the project's shape, or when something behaves differently than expected. Not for running the checks themselves.
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Project context
|
|
@@ -55,6 +55,21 @@ Then read what exists:
|
|
|
55
55
|
| `commitlint.config.js` | This repo's allowed commit types — they differ between repos |
|
|
56
56
|
| Deploy workflows referencing Pressable | Hosted on Pressable ([`wp-pressable`](../wp-pressable/SKILL.md)) |
|
|
57
57
|
| Git remote name | Infers the ClickUp space ([`task-tracking`](../task-tracking/SKILL.md)) |
|
|
58
|
+
| `.mcp.json` | The MCP servers this repo declares it needs ([`agent-capabilities`](../agent-capabilities/SKILL.md)) |
|
|
59
|
+
|
|
60
|
+
## Capability surface
|
|
61
|
+
|
|
62
|
+
Part of "what am I working in?" is what the agent itself loaded. If the project's shape and
|
|
63
|
+
its capabilities disagree — WordPress skills on a Workers repo, no `.mcp.json` on a repo
|
|
64
|
+
whose work needs one, the same skill listed twice — say so once and move on;
|
|
65
|
+
[`agent-capabilities`](../agent-capabilities/SKILL.md) owns the fix.
|
|
66
|
+
|
|
67
|
+
Worth a look on an unfamiliar repo, not every session:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
npx @linchpinagency/skills --check # duplicate skill installs across scopes
|
|
71
|
+
claude mcp list # configured servers and their scope
|
|
72
|
+
```
|
|
58
73
|
|
|
59
74
|
## What to report
|
|
60
75
|
|
|
@@ -11,7 +11,10 @@ scripts, `package.json` scripts, `phpcs.xml.dist`, `lint-staged.config.js`. Your
|
|
|
11
11
|
**find those declarations and run them**, not to invent commands. Getting this right is
|
|
12
12
|
what makes the difference between a clean PR and a red CI run.
|
|
13
13
|
|
|
14
|
-
The gates are the same ones CI runs, so passing here means passing there.
|
|
14
|
+
The gates are the same ones CI runs, so passing here almost always means passing there.
|
|
15
|
+
Almost: see [Green locally, red in CI](#green-locally-red-in-ci) for the one way the
|
|
16
|
+
shared lint workflow disagrees with a clean local run, which is not a difference in what
|
|
17
|
+
is checked but in what counts as a failure.
|
|
15
18
|
|
|
16
19
|
## When to use
|
|
17
20
|
|
|
@@ -69,6 +72,55 @@ Full command matrix: [`references/toolchain.md`](references/toolchain.md).
|
|
|
69
72
|
repo lacks a house script, propose it (see `references/toolchain.md`) and add it **only
|
|
70
73
|
with approval**. → Then go to [`commit-and-release`](../commit-and-release/SKILL.md).
|
|
71
74
|
|
|
75
|
+
## Green locally, red in CI
|
|
76
|
+
|
|
77
|
+
The shared lint workflow (`linchpin/actions`) runs the same phpcs you do, and then does
|
|
78
|
+
two things to the result that your terminal does not.
|
|
79
|
+
|
|
80
|
+
**It sniffs only the files the PR changed.** `git diff --diff-filter=ACMRT <base>...HEAD`,
|
|
81
|
+
which means **you inherit the debt of every file you touch**. A file carrying violations
|
|
82
|
+
nobody has cleaned up becomes your problem the moment you edit one line of it.
|
|
83
|
+
|
|
84
|
+
**It pipes the report through `cs2pr`, and `cs2pr` fails on warnings.** This is the part
|
|
85
|
+
that surprises people:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
phpcs -q --runtime-set ignore_warnings_on_exit 1 --report=checkstyle "${files[@]}" | cs2pr
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`ignore_warnings_on_exit` does exactly what it says — **phpcs itself exits 0** on a
|
|
92
|
+
warnings-only run. But the checkstyle report still lists every warning as
|
|
93
|
+
`<error severity="warning">`, `cs2pr` turns each into an annotation and exits non-zero
|
|
94
|
+
when there is one, and the step runs under `set -e` with the pipeline's exit code being
|
|
95
|
+
the last command's. So `composer lint` is green, `phpcs` on its own is green, and the job
|
|
96
|
+
is red.
|
|
97
|
+
|
|
98
|
+
Put together: **one unfixed warning in a file makes every future PR that touches it red on
|
|
99
|
+
arrival.** Seen twice on linchpin.com — `PSR1.Files.SideEffects` on the standard
|
|
100
|
+
`defined( 'ABSPATH' ) || exit;` guard (which cost a PR merged red), and
|
|
101
|
+
`WordPress.WP.Capabilities.Unknown` on two custom capabilities.
|
|
102
|
+
|
|
103
|
+
Check the same thing CI checks — the count of annotations, warnings included, over the
|
|
104
|
+
files the PR changed:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
git diff --name-only --diff-filter=ACMRT "$(git merge-base HEAD origin/main)"...HEAD -- '*.php' \
|
|
108
|
+
| tr '\n' '\0' | xargs -0 vendor/bin/phpcs -q --report=checkstyle \
|
|
109
|
+
| grep -c 'severity='
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Zero means the job will pass. Any other number is what CI will annotate, whether phpcs
|
|
113
|
+
called them errors or not. (Piped through `xargs -0` rather than an unquoted `$files`
|
|
114
|
+
because zsh does not word-split, so the obvious version passes phpcs one long filename
|
|
115
|
+
and reports a file that does not exist.)
|
|
116
|
+
|
|
117
|
+
Fixing it is the same rule as everywhere else in this skill — **fix the warning, do not
|
|
118
|
+
silence the sniff.** Most are legitimately configuration rather than code: a custom
|
|
119
|
+
capability belongs in `custom_capabilities` in `phpcs.xml.dist`, a text domain in
|
|
120
|
+
`text_domain`. Register the real value and say in a comment where it comes from; a typo
|
|
121
|
+
registered there hides exactly the bug the sniff exists to catch. Setting the sniff to
|
|
122
|
+
`<severity>0</severity>` is the last resort, not the first.
|
|
123
|
+
|
|
72
124
|
## Guardrails
|
|
73
125
|
|
|
74
126
|
- **Never** commit with `--no-verify`. The hook is the gate; if it blocks you, fix the code.
|
|
@@ -90,5 +142,6 @@ Full command matrix: [`references/toolchain.md`](references/toolchain.md).
|
|
|
90
142
|
- [ ] PHP gate passed (or is correctly not applicable — no `phpcs.xml.dist`, no PHP changed).
|
|
91
143
|
- [ ] JS/CSS gate passed in the owning workspace (or correctly not applicable).
|
|
92
144
|
- [ ] Tests run for touched, covered code.
|
|
145
|
+
- [ ] The changed-file annotation count is zero — warnings included, not just errors.
|
|
93
146
|
- [ ] No suppressions, config widenings, or `--no-verify` were used to get green.
|
|
94
147
|
- [ ] Skipped gates and missing house scripts are named in the report.
|