dotnet-production-agent-skills 0.1.0 → 0.2.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 +24 -2
- package/copilot/production-instructions.md +8 -0
- package/dist/src/cli.js +68 -34
- package/dist/src/copilot/hooks.js +1 -0
- package/dist/src/copilot/instructions.js +74 -0
- package/dist/src/skills/validation.js +65 -0
- package/package.json +7 -3
- package/skills/api-contract-safety/SKILL.md +1 -1
- package/skills/api-endpoint-deprecation/SKILL.md +1 -1
- package/skills/aspnet-core-api-development/SKILL.md +1 -1
- package/skills/code-quality/SKILL.md +1 -1
- package/skills/distributed-workflows/SKILL.md +1 -1
- package/skills/exception-handling/SKILL.md +1 -1
- package/skills/google-bigquery-dotnet/SKILL.md +1 -1
- package/skills/google-cloud-pubsub-dotnet/SKILL.md +1 -1
- package/skills/google-firestore-dotnet/SKILL.md +1 -1
- package/skills/production-logging/SKILL.md +1 -1
- package/skills/relational-database-dotnet/SKILL.md +1 -1
- package/skills/retry-resilience/SKILL.md +14 -0
- package/skills/task-router/SKILL.md +1 -1
- package/skills/unit-testing/SKILL.md +1 -1
package/README.md
CHANGED
|
@@ -277,10 +277,32 @@ External consumers are never assumed absent just because no internal repository
|
|
|
277
277
|
|
|
278
278
|
## TypeScript CLI Reference
|
|
279
279
|
|
|
280
|
-
|
|
280
|
+
### How guidance is applied
|
|
281
|
+
|
|
282
|
+
| Concern | Mechanism | When it applies |
|
|
283
|
+
| :--- | :--- | :--- |
|
|
284
|
+
| Universal engineering rules | Managed section in project `.github/copilot-instructions.md` | Every Copilot coding task in that project |
|
|
285
|
+
| Specialized .NET knowledge | Individual `SKILL.md` files | The coding agent selects relevant skills from their descriptions and the request |
|
|
286
|
+
| Deterministic command enforcement | Optional host hooks/guardrails | Only when a supported host integration and policy are explicitly enabled |
|
|
287
|
+
|
|
288
|
+
The CLI installs and manages guidance; it does not route prompts or load every skill for every task. Clear skill descriptions help the coding agent select relevant knowledge. Universal rules belong in repository instructions. Command blocking belongs in host-supported hooks. Hook installation is currently an extension point in `src/copilot/hooks.ts`; no command hook is installed because a portable supported Copilot hook configuration has not been established.
|
|
289
|
+
|
|
290
|
+
Copilot project installs manage only the text between `<!-- dotnet-production-agent-skills:start -->` and `<!-- dotnet-production-agent-skills:end -->`. Existing team instructions outside those markers remain intact. Uninstall removes only the managed section. User-scope Copilot installs install skills into `~/.copilot/skills`; repository instructions apply only to project scope.
|
|
291
|
+
|
|
292
|
+
```sh
|
|
293
|
+
dotnet-production-agent-skills install --target copilot --scope project
|
|
294
|
+
dotnet-production-agent-skills install --target copilot --scope user
|
|
295
|
+
dotnet-production-agent-skills update --target copilot --scope project
|
|
296
|
+
dotnet-production-agent-skills doctor --target copilot --scope project
|
|
297
|
+
dotnet-production-agent-skills list --target copilot --scope project
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
`--mode copy` installs portable snapshots, while `--mode link` creates links to the installed npm package and depends on that package remaining at the same location. Use `--dry-run` to preview changes without writing. Repeated installs skip unchanged snapshots; updates replace snapshots only when the packaged skill changes. Forced replacement or removal backs up locally modified skills.
|
|
301
|
+
|
|
302
|
+
The installer CLI is written in TypeScript and compiles to Node.js ESM (requiring Node.js >= 18). The runtime uses the small `yaml` dependency to validate skill metadata.
|
|
281
303
|
|
|
282
304
|
```text
|
|
283
|
-
dotnet-agent-skills <command> [options]
|
|
305
|
+
dotnet-production-agent-skills <command> [options]
|
|
284
306
|
|
|
285
307
|
Commands:
|
|
286
308
|
install Install canonical skills into agent discovery paths
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
Understand the existing implementation before changing code. Follow its architecture and conventions.
|
|
2
|
+
Reuse existing helpers and services; avoid duplicated logic and keep new code maintainable under repository analyzers and SonarCloud.
|
|
3
|
+
Preserve compatibility and public API contracts unless a change is explicitly requested. Avoid unrelated refactors.
|
|
4
|
+
Never expose stack traces, credentials, tokens, secrets, connection strings, internal URLs, or database details through customer-facing APIs.
|
|
5
|
+
Never log secrets or sensitive payloads. Add operationally useful logs, and limit noise in high-traffic APIs.
|
|
6
|
+
Do not introduce retries automatically. Add them only when requested or required by existing architecture, and only for safe transient operations.
|
|
7
|
+
When behavior changes, check whether existing tests need updating. Avoid redundant tests that slow CI.
|
|
8
|
+
Run the smallest appropriate validation, build, or test command after changing code.
|
package/dist/src/cli.js
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createHash, randomUUID } from "node:crypto";
|
|
3
|
-
import { existsSync } from "node:fs";
|
|
4
3
|
import { cp, lstat, mkdir, readFile, readdir, readlink, realpath, rename, rm, symlink, writeFile, } from "node:fs/promises";
|
|
5
4
|
import { homedir } from "node:os";
|
|
6
5
|
import path from "node:path";
|
|
7
6
|
import process from "node:process";
|
|
8
7
|
import { fileURLToPath } from "node:url";
|
|
9
8
|
import { createInterface } from "node:readline/promises";
|
|
9
|
+
import { changeInstructions, inspectInstructions } from "./copilot/instructions.js";
|
|
10
|
+
import { validateSkills } from "./skills/validation.js";
|
|
10
11
|
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
11
12
|
const SKILLS_ROOT = path.join(PACKAGE_ROOT, "skills");
|
|
12
13
|
const MANIFEST_NAME = ".dotnet-agent-skills.json";
|
|
13
14
|
const SUPPORTED_TARGETS = new Set(["all", "shared", "codex", "copilot", "claude", "cursor"]);
|
|
14
15
|
function usage() {
|
|
15
|
-
return `Usage: dotnet-agent-skills <command> [options]
|
|
16
|
+
return `Usage: dotnet-production-agent-skills <command> [options]
|
|
16
17
|
|
|
17
18
|
Commands:
|
|
18
19
|
install Install canonical skills
|
|
@@ -226,16 +227,28 @@ async function moveToBackup(destination, name, target) {
|
|
|
226
227
|
}
|
|
227
228
|
async function installOrUpdate(options, command) {
|
|
228
229
|
const skills = await packagedSkills();
|
|
230
|
+
const validationErrors = await validateCanonical();
|
|
231
|
+
if (validationErrors.length)
|
|
232
|
+
throw new Error(`invalid packaged skills: ${validationErrors.join("; ")}`);
|
|
233
|
+
if ((options.target === "copilot" || options.target === "all") && options.scope === "project") {
|
|
234
|
+
const instructions = await inspectInstructions(options.project);
|
|
235
|
+
if (instructions.state === "malformed")
|
|
236
|
+
throw new Error(`malformed managed Copilot instructions: ${instructions.path}`);
|
|
237
|
+
}
|
|
229
238
|
const packaged = new Set(skills);
|
|
230
239
|
const packageJson = JSON.parse(await readFile(path.join(PACKAGE_ROOT, "package.json"), "utf8"));
|
|
231
240
|
const destinationPlans = [];
|
|
241
|
+
const digests = new Map(await Promise.all(skills.map(async (name) => [name, await digestDirectory(path.join(SKILLS_ROOT, name))])));
|
|
232
242
|
for (const destination of destinations(options)) {
|
|
233
243
|
const manifest = await readManifest(destination.directory);
|
|
234
244
|
const plans = [];
|
|
235
245
|
for (const name of skills) {
|
|
236
246
|
const state = await currentState(destination.directory, name, manifest.skills[name]);
|
|
237
|
-
const
|
|
238
|
-
|
|
247
|
+
const digest = digests.get(name);
|
|
248
|
+
const action = state.state === "missing" ? "install" : state.state === "unchanged"
|
|
249
|
+
? manifest.skills[name]?.digest === digest && manifest.skills[name]?.mode === options.mode ? "skip" : "replace"
|
|
250
|
+
: "conflict";
|
|
251
|
+
plans.push({ name, state, action, digest });
|
|
239
252
|
}
|
|
240
253
|
const retirements = [];
|
|
241
254
|
if (command === "update") {
|
|
@@ -259,6 +272,10 @@ async function installOrUpdate(options, command) {
|
|
|
259
272
|
if (!options.dryRun)
|
|
260
273
|
await mkdir(destination.directory, { recursive: true });
|
|
261
274
|
for (const plan of plans) {
|
|
275
|
+
if (plan.action === "skip") {
|
|
276
|
+
console.log(`skip ${plan.state.installedPath}`);
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
262
279
|
const source = path.join(SKILLS_ROOT, plan.name);
|
|
263
280
|
const target = plan.state.installedPath;
|
|
264
281
|
if (plan.action !== "install") {
|
|
@@ -281,7 +298,7 @@ async function installOrUpdate(options, command) {
|
|
|
281
298
|
await cp(source, target, { recursive: true, errorOnExist: true });
|
|
282
299
|
}
|
|
283
300
|
manifest.skills[plan.name] = {
|
|
284
|
-
digest:
|
|
301
|
+
digest: plan.digest,
|
|
285
302
|
mode: options.mode,
|
|
286
303
|
source: options.mode === "link" ? await realpath(source) : null,
|
|
287
304
|
};
|
|
@@ -305,8 +322,19 @@ async function installOrUpdate(options, command) {
|
|
|
305
322
|
manifest.target = destination.target;
|
|
306
323
|
await writeManifest(destination.directory, manifest, options.dryRun);
|
|
307
324
|
}
|
|
325
|
+
if ((options.target === "copilot" || options.target === "all") && options.scope === "project") {
|
|
326
|
+
const action = await changeInstructions(options.project, "install", options.dryRun);
|
|
327
|
+
console.log(`${action === "skip" ? "skip" : options.dryRun ? `would ${action}` : action} Copilot production instructions`);
|
|
328
|
+
if (!options.dryRun)
|
|
329
|
+
console.log("Copilot CLI: /skills list | /skills reload | /skills info <skill-name>");
|
|
330
|
+
}
|
|
308
331
|
}
|
|
309
332
|
async function uninstall(options) {
|
|
333
|
+
if ((options.target === "copilot" || options.target === "all") && options.scope === "project") {
|
|
334
|
+
const instructions = await inspectInstructions(options.project);
|
|
335
|
+
if (instructions.state === "malformed")
|
|
336
|
+
throw new Error(`malformed managed Copilot instructions: ${instructions.path}`);
|
|
337
|
+
}
|
|
310
338
|
const destinationPlans = [];
|
|
311
339
|
for (const destination of destinations(options)) {
|
|
312
340
|
const manifest = await readManifest(destination.directory);
|
|
@@ -329,8 +357,14 @@ async function uninstall(options) {
|
|
|
329
357
|
for (const { destination, manifest, skills: plans } of destinationPlans) {
|
|
330
358
|
for (const plan of plans) {
|
|
331
359
|
console.log(`${options.dryRun ? "would remove" : "remove"} ${plan.state.installedPath}`);
|
|
332
|
-
if (!options.dryRun)
|
|
333
|
-
|
|
360
|
+
if (!options.dryRun) {
|
|
361
|
+
if (plan.state.state === "modified") {
|
|
362
|
+
const backup = await moveToBackup(destination.directory, plan.name, plan.state.installedPath);
|
|
363
|
+
console.log(`backup ${backup}`);
|
|
364
|
+
}
|
|
365
|
+
else
|
|
366
|
+
await rm(plan.state.installedPath, { recursive: true, force: true });
|
|
367
|
+
}
|
|
334
368
|
delete manifest.skills[plan.name];
|
|
335
369
|
}
|
|
336
370
|
if (!options.dryRun) {
|
|
@@ -341,48 +375,40 @@ async function uninstall(options) {
|
|
|
341
375
|
await writeManifest(destination.directory, manifest, false);
|
|
342
376
|
}
|
|
343
377
|
}
|
|
378
|
+
if ((options.target === "copilot" || options.target === "all") && options.scope === "project") {
|
|
379
|
+
const action = await changeInstructions(options.project, "uninstall", options.dryRun);
|
|
380
|
+
if (action !== "skip")
|
|
381
|
+
console.log(`${options.dryRun ? "would remove" : "remove"} Copilot production instructions`);
|
|
382
|
+
}
|
|
344
383
|
}
|
|
345
384
|
async function inspect(options) {
|
|
346
385
|
const skills = await packagedSkills();
|
|
347
386
|
const packaged = new Set(skills);
|
|
387
|
+
const packageJson = JSON.parse(await readFile(path.join(PACKAGE_ROOT, "package.json"), "utf8"));
|
|
348
388
|
const installations = [];
|
|
389
|
+
const digests = new Map(await Promise.all(skills.map(async (name) => [name, await digestDirectory(path.join(SKILLS_ROOT, name))])));
|
|
349
390
|
for (const destination of destinations(options)) {
|
|
350
391
|
const manifest = await readManifest(destination.directory);
|
|
351
392
|
const states = [];
|
|
352
393
|
for (const name of skills) {
|
|
353
394
|
const state = await currentState(destination.directory, name, manifest.skills[name]);
|
|
354
|
-
|
|
395
|
+
const record = manifest.skills[name];
|
|
396
|
+
states.push({ name, state: state.state, mode: record?.mode ?? null, packaged: record ? record.digest === digests.get(name) ? "current" : "outdated" : null });
|
|
355
397
|
}
|
|
356
398
|
for (const [name, record] of Object.entries(manifest.skills)) {
|
|
357
399
|
if (!packaged.has(name))
|
|
358
|
-
states.push({ name, state: "stale", mode: record.mode });
|
|
400
|
+
states.push({ name, state: "stale", mode: record.mode, packaged: null });
|
|
359
401
|
}
|
|
360
|
-
installations.push({ ...destination, packageVersion: manifest.packageVersion,
|
|
402
|
+
installations.push({ ...destination, packageVersion: manifest.packageVersion,
|
|
403
|
+
packageVersionMismatch: manifest.packageVersion !== null && manifest.packageVersion !== packageJson.version,
|
|
404
|
+
skills: states });
|
|
361
405
|
}
|
|
362
|
-
|
|
406
|
+
const copilotInstructions = (options.target === "copilot" || options.target === "all") && options.scope === "project"
|
|
407
|
+
? await inspectInstructions(options.project) : undefined;
|
|
408
|
+
return { packagedSkills: skills, currentPackageVersion: packageJson.version, installations, ...(copilotInstructions ? { copilotInstructions } : {}) };
|
|
363
409
|
}
|
|
364
410
|
async function validateCanonical() {
|
|
365
|
-
|
|
366
|
-
for (const name of await packagedSkills()) {
|
|
367
|
-
const skillFile = path.join(SKILLS_ROOT, name, "SKILL.md");
|
|
368
|
-
if (!existsSync(skillFile)) {
|
|
369
|
-
errors.push(`${name}: missing SKILL.md`);
|
|
370
|
-
continue;
|
|
371
|
-
}
|
|
372
|
-
const contents = await readFile(skillFile, "utf8");
|
|
373
|
-
const match = contents.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/);
|
|
374
|
-
if (!match?.[1])
|
|
375
|
-
errors.push(`${name}: invalid frontmatter delimiters`);
|
|
376
|
-
else {
|
|
377
|
-
const declaredName = match[1].match(/^name:\s*(.+)$/m)?.[1]?.trim();
|
|
378
|
-
const description = match[1].match(/^description:\s*(.+)$/m)?.[1]?.trim();
|
|
379
|
-
if (declaredName !== name)
|
|
380
|
-
errors.push(`${name}: frontmatter name does not match directory`);
|
|
381
|
-
if (!description)
|
|
382
|
-
errors.push(`${name}: missing description`);
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
return errors;
|
|
411
|
+
return validateSkills(SKILLS_ROOT, await packagedSkills());
|
|
386
412
|
}
|
|
387
413
|
function countStates(skills) {
|
|
388
414
|
return skills.reduce((counts, entry) => {
|
|
@@ -414,8 +440,9 @@ async function main() {
|
|
|
414
440
|
const report = await inspect(parsed.options);
|
|
415
441
|
const validationErrors = command === "doctor" ? await validateCanonical() : [];
|
|
416
442
|
const unhealthy = report.installations.flatMap((entry) => entry.skills)
|
|
417
|
-
.filter((entry) => entry.state !== "unchanged");
|
|
418
|
-
const
|
|
443
|
+
.filter((entry) => entry.state !== "unchanged" || entry.packaged === "outdated");
|
|
444
|
+
const instructionsUnhealthy = report.copilotInstructions && !["unchanged", "unmanaged"].includes(report.copilotInstructions.state);
|
|
445
|
+
const output = { ...report, validationErrors, healthy: validationErrors.length === 0 && unhealthy.length === 0 && !instructionsUnhealthy };
|
|
419
446
|
if (parsed.options.json)
|
|
420
447
|
console.log(JSON.stringify(output, null, 2));
|
|
421
448
|
else {
|
|
@@ -424,7 +451,14 @@ async function main() {
|
|
|
424
451
|
const summary = Object.entries(countStates(installation.skills))
|
|
425
452
|
.map(([key, value]) => `${key}=${value}`).join(" ");
|
|
426
453
|
console.log(`${installation.target.padEnd(7)} ${installation.directory} ${summary}`);
|
|
454
|
+
if (installation.packageVersionMismatch)
|
|
455
|
+
console.log(` installed package version: ${installation.packageVersion}; current: ${report.currentPackageVersion}`);
|
|
456
|
+
const outdated = installation.skills.filter((skill) => skill.packaged === "outdated");
|
|
457
|
+
if (outdated.length)
|
|
458
|
+
console.log(` packaged updates: ${outdated.map((skill) => skill.name).join(", ")}`);
|
|
427
459
|
}
|
|
460
|
+
if (report.copilotInstructions)
|
|
461
|
+
console.log(`Copilot instructions: ${report.copilotInstructions.state} ${report.copilotInstructions.path}`);
|
|
428
462
|
validationErrors.forEach((error) => console.error(`error: ${error}`));
|
|
429
463
|
}
|
|
430
464
|
if (command === "doctor" && !output.healthy)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { readFile, mkdir, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
const start = "<!-- dotnet-production-agent-skills:start -->";
|
|
5
|
+
const end = "<!-- dotnet-production-agent-skills:end -->";
|
|
6
|
+
const source = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../../copilot/production-instructions.md");
|
|
7
|
+
export async function instructionsPath(base) {
|
|
8
|
+
return path.join(base, ".github", "copilot-instructions.md");
|
|
9
|
+
}
|
|
10
|
+
async function optionalRead(file) {
|
|
11
|
+
try {
|
|
12
|
+
return await readFile(file, "utf8");
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
16
|
+
return null;
|
|
17
|
+
throw error;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function section(text) {
|
|
21
|
+
const from = text.indexOf(start);
|
|
22
|
+
const closing = text.indexOf(end);
|
|
23
|
+
if ((from < 0) !== (closing < 0) || (from >= 0 && (closing < from || text.indexOf(start, from + start.length) >= 0 || text.indexOf(end, closing + end.length) >= 0))) {
|
|
24
|
+
throw new Error("malformed managed Copilot instructions section");
|
|
25
|
+
}
|
|
26
|
+
return from < 0 ? null : { from, to: closing + end.length };
|
|
27
|
+
}
|
|
28
|
+
export async function inspectInstructions(base) {
|
|
29
|
+
const file = await instructionsPath(base);
|
|
30
|
+
const existing = await optionalRead(file);
|
|
31
|
+
if (existing === null)
|
|
32
|
+
return { path: file, state: "missing" };
|
|
33
|
+
let bounds;
|
|
34
|
+
try {
|
|
35
|
+
bounds = section(existing);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return { path: file, state: "malformed" };
|
|
39
|
+
}
|
|
40
|
+
if (!bounds)
|
|
41
|
+
return { path: file, state: "unmanaged" };
|
|
42
|
+
const canonical = (await readFile(source, "utf8")).trim();
|
|
43
|
+
const managed = existing.slice(bounds.from + start.length, bounds.to - end.length).trim();
|
|
44
|
+
return { path: file, state: managed === canonical ? "unchanged" : "outdated" };
|
|
45
|
+
}
|
|
46
|
+
export async function changeInstructions(base, operation, dryRun) {
|
|
47
|
+
const file = await instructionsPath(base);
|
|
48
|
+
const existing = await optionalRead(file);
|
|
49
|
+
const bounds = section(existing ?? "");
|
|
50
|
+
if (operation === "uninstall") {
|
|
51
|
+
if (!bounds || existing === null)
|
|
52
|
+
return "skip";
|
|
53
|
+
const remaining = `${existing.slice(0, bounds.from).trimEnd()}${existing.slice(bounds.to).trimStart()}`.trim();
|
|
54
|
+
if (!dryRun) {
|
|
55
|
+
if (remaining)
|
|
56
|
+
await writeFile(file, `${remaining}\n`, "utf8");
|
|
57
|
+
else
|
|
58
|
+
await rm(file);
|
|
59
|
+
}
|
|
60
|
+
return "remove";
|
|
61
|
+
}
|
|
62
|
+
const canonical = (await readFile(source, "utf8")).trim();
|
|
63
|
+
const managed = `${start}\n${canonical}\n${end}`;
|
|
64
|
+
const next = bounds && existing !== null
|
|
65
|
+
? `${existing.slice(0, bounds.from)}${managed}${existing.slice(bounds.to)}`
|
|
66
|
+
: `${existing?.trimEnd() ? `${existing.trimEnd()}\n\n` : ""}${managed}\n`;
|
|
67
|
+
if (next === existing)
|
|
68
|
+
return "skip";
|
|
69
|
+
if (!dryRun) {
|
|
70
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
71
|
+
await writeFile(file, next, "utf8");
|
|
72
|
+
}
|
|
73
|
+
return bounds ? "update" : "install";
|
|
74
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { readFile, stat } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import YAML from "yaml";
|
|
4
|
+
const safeName = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
5
|
+
export async function validateSkills(root, names) {
|
|
6
|
+
const errors = [];
|
|
7
|
+
const declared = new Set();
|
|
8
|
+
for (const name of names) {
|
|
9
|
+
if (!safeName.test(name) || name.length > 64) {
|
|
10
|
+
errors.push(`${name}: unsafe skill directory name`);
|
|
11
|
+
continue;
|
|
12
|
+
}
|
|
13
|
+
const directory = path.join(root, name);
|
|
14
|
+
let contents;
|
|
15
|
+
try {
|
|
16
|
+
contents = await readFile(path.join(directory, "SKILL.md"), "utf8");
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
errors.push(`${name}: missing or unreadable SKILL.md`);
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
const match = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(contents);
|
|
23
|
+
if (!match) {
|
|
24
|
+
errors.push(`${name}: invalid frontmatter delimiters`);
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
const document = YAML.parseDocument(match[1], { uniqueKeys: true });
|
|
28
|
+
if (document.errors.length) {
|
|
29
|
+
errors.push(`${name}: invalid YAML: ${document.errors[0].message}`);
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
const metadata = document.toJS();
|
|
33
|
+
if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
|
|
34
|
+
errors.push(`${name}: frontmatter must be a mapping`);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const fields = metadata;
|
|
38
|
+
if (typeof fields.name !== "string" || fields.name !== name)
|
|
39
|
+
errors.push(`${name}: frontmatter name does not match directory`);
|
|
40
|
+
if (typeof fields.description !== "string" || !fields.description.trim())
|
|
41
|
+
errors.push(`${name}: missing description`);
|
|
42
|
+
if (typeof fields.name === "string") {
|
|
43
|
+
if (declared.has(fields.name))
|
|
44
|
+
errors.push(`${name}: duplicate skill name ${fields.name}`);
|
|
45
|
+
declared.add(fields.name);
|
|
46
|
+
}
|
|
47
|
+
for (const link of contents.matchAll(/\]\(([^)#]+)(?:#[^)]*)?\)/g)) {
|
|
48
|
+
const target = link[1];
|
|
49
|
+
if (/^[a-z]+:/i.test(target) || target.startsWith("/"))
|
|
50
|
+
continue;
|
|
51
|
+
const resolved = path.resolve(directory, target);
|
|
52
|
+
if (!resolved.startsWith(`${directory}${path.sep}`)) {
|
|
53
|
+
errors.push(`${name}: linked file escapes skill directory: ${target}`);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
await stat(resolved);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
errors.push(`${name}: missing linked file: ${target}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return errors;
|
|
65
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dotnet-production-agent-skills",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Portable production .NET engineering skills for coding agents",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -17,10 +17,11 @@
|
|
|
17
17
|
},
|
|
18
18
|
"type": "module",
|
|
19
19
|
"bin": {
|
|
20
|
-
"dotnet-agent-skills": "dist/src/cli.js"
|
|
20
|
+
"dotnet-production-agent-skills": "dist/src/cli.js"
|
|
21
21
|
},
|
|
22
22
|
"files": [
|
|
23
23
|
"dist/src",
|
|
24
|
+
"copilot",
|
|
24
25
|
"skills",
|
|
25
26
|
"README.md",
|
|
26
27
|
"LICENSE"
|
|
@@ -49,5 +50,8 @@
|
|
|
49
50
|
"copilot",
|
|
50
51
|
"claude-code",
|
|
51
52
|
"cursor"
|
|
52
|
-
]
|
|
53
|
+
],
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"yaml": "^2.9.1"
|
|
56
|
+
}
|
|
53
57
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: api-contract-safety
|
|
3
|
-
description:
|
|
3
|
+
description: Use when changing or reviewing existing API routes, versions, verbs, headers, request/response DTOs, serialization, status codes, error shapes, authorization, or public interfaces.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# API contract safety
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: api-endpoint-deprecation
|
|
3
|
-
description:
|
|
3
|
+
description: Use when planning, implementing, or reviewing API deprecation, replacement versions, sunset headers, migration windows, consumer verification, or endpoint removal.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Safe API endpoint deprecation
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: aspnet-core-api-development
|
|
3
|
-
description:
|
|
3
|
+
description: Use when building a new ASP.NET Core customer-facing API endpoint or version, including routing, header and request validation, service boundaries, authorization, error responses, and OpenAPI. Use api-contract-safety when modifying an existing contract.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# New customer-facing ASP.NET Core APIs
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: code-quality
|
|
3
|
-
description:
|
|
3
|
+
description: Use when improving or reviewing modified .NET code for SonarCloud/Roslyn findings, duplication, complexity, maintainability, or existing helper reuse. Avoid unrelated legacy cleanup.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# .NET code quality
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: distributed-workflows
|
|
3
|
-
description:
|
|
3
|
+
description: Use when designing or changing .NET message handlers, background workers, idempotency, duplicate delivery, partial failures, or existing retry behavior. Introduce new retries only when requested or required by existing architecture.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Distributed workflows
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: exception-handling
|
|
3
|
-
description:
|
|
3
|
+
description: Use when implementing, modifying, or reviewing .NET exception handling, customer-facing API error translation, recovery boundaries, cancellation, and safe logging. Prevent internal details and secrets from reaching callers.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# .NET exception handling
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: google-bigquery-dotnet
|
|
3
|
-
description:
|
|
3
|
+
description: Use when implementing, modifying, reviewing, troubleshooting, or optimizing Google BigQuery integrations in .NET, including BigQueryClient, authentication, parameterized queries, DATE/DATETIME/TIMESTAMP types, query costs, serialization, and result mapping.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Google BigQuery for .NET
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: google-cloud-pubsub-dotnet
|
|
3
|
-
description:
|
|
3
|
+
description: Use when implementing, modifying, reviewing, or troubleshooting Google Cloud Pub/Sub publishers, subscribers, pull workers, acknowledgements, concurrency, flow control, dead-letter handling, ordering, or BackgroundService integration in .NET.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Google Cloud Pub/Sub for .NET
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: google-firestore-dotnet
|
|
3
|
-
description:
|
|
3
|
+
description: Use when implementing, reviewing, or troubleshooting .NET Firestore document models, queries, transactions, batched writes, indexes, optimistic concurrency, or read/write costs.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Google Cloud Firestore for .NET
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: production-logging
|
|
3
|
-
description:
|
|
3
|
+
description: Use when adding, reviewing, or reducing .NET structured logs, exception telemetry, correlation IDs, sensitive-data exposure, or high-traffic API logging costs.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Production logging for .NET
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: relational-database-dotnet
|
|
3
|
-
description:
|
|
3
|
+
description: Use when implementing, reviewing, or troubleshooting .NET PostgreSQL or Oracle SQL, ADO.NET providers, parameterized queries, connections, transactions, mapping, or database performance. Compose with ef-core-safety for EF Core.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Relational database access for .NET
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: retry-resilience
|
|
3
|
+
description: >
|
|
4
|
+
Use when the user explicitly requests retry behavior or when reviewing existing
|
|
5
|
+
retry and resilience logic for transient HTTP, database, Pub/Sub, or cloud API
|
|
6
|
+
dependencies. Do not introduce new retries unless requested or required by
|
|
7
|
+
existing architecture.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Retry and resilience in .NET
|
|
11
|
+
|
|
12
|
+
Inspect existing policies, timeouts, cancellation, and failure semantics first. Retry only transient failures on operations safe to repeat, or operations with a reliable idempotency key. Bound attempts, total time, and concurrency; use backoff with jitter where appropriate. Honor server retry guidance when supported.
|
|
13
|
+
|
|
14
|
+
Never retry validation failures or permanent authorization errors. Avoid layered retries that multiply calls and costs. Account for duplicate messages and ambiguous outcomes. Test the retry boundary with representative transient and permanent failures, and report the operational tradeoff.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: task-router
|
|
3
|
-
description:
|
|
3
|
+
description: Use only when the user explicitly requests help selecting a workflow or skill for a complex software task; choose relevant skills from their descriptions without implementing a custom skill router.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Task router
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: unit-testing
|
|
3
|
-
description:
|
|
3
|
+
description: Use when adding or improving focused .NET unit tests for changed business behavior or regression fixes. Prefer parameterized cases and avoid trivial redundant tests that increase CI time.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# .NET unit testing
|