dotnet-production-agent-skills 0.2.0 → 0.3.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 +18 -6
- package/RELEASE_NOTES.md +59 -0
- package/dist/src/cli.js +39 -6
- package/dist/src/hooks.js +138 -0
- package/hooks/guard.mjs +43 -0
- package/package.json +6 -3
- package/skills/redis-dotnet/SKILL.md +16 -0
- package/dist/src/copilot/hooks.js +0 -1
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
A focused collection of `SKILL.md` guidance built to reduce repeated prompting, unnecessary token use, and avoidable rework when developing production .NET software with AI coding tools.
|
|
12
12
|
|
|
13
|
-
[Browse the skills catalog](https://dprakash2101.github.io/dotnet-production-agent-skills/#skills) · [Build an install command](https://dprakash2101.github.io/dotnet-production-agent-skills/#install) · [Report an issue](https://github.com/dprakash2101/dotnet-production-agent-skills/issues)
|
|
13
|
+
[Browse the skills catalog](https://dprakash2101.github.io/dotnet-production-agent-skills/#skills) · [Build an install command](https://dprakash2101.github.io/dotnet-production-agent-skills/#install) · [Release notes](RELEASE_NOTES.md) · [Report an issue](https://github.com/dprakash2101/dotnet-production-agent-skills/issues)
|
|
14
14
|
|
|
15
15
|
</div>
|
|
16
16
|
|
|
@@ -36,6 +36,7 @@ A focused collection of `SKILL.md` guidance built to reduce repeated prompting,
|
|
|
36
36
|
- [Safety & Conflict Handling](#safety--conflict-handling)
|
|
37
37
|
- [Host Compatibility](#host-compatibility)
|
|
38
38
|
- [Validation & Development](#validation--development)
|
|
39
|
+
- [Release Notes](#release-notes)
|
|
39
40
|
- [Why I built this](#why-i-built-this)
|
|
40
41
|
- [License](#license)
|
|
41
42
|
|
|
@@ -58,7 +59,8 @@ skills/ (canonical portable Agent Skills)
|
|
|
58
59
|
├── Codex → .agents/skills
|
|
59
60
|
├── GitHub Copilot → .github/skills | .copilot/skills
|
|
60
61
|
├── Claude Code → .claude/skills
|
|
61
|
-
|
|
62
|
+
├── Cursor → .cursor/skills | .agents/skills
|
|
63
|
+
└── Antigravity → .agents/skills + .agents/hooks.json
|
|
62
64
|
```
|
|
63
65
|
|
|
64
66
|
### Core Engineering Invariant
|
|
@@ -201,12 +203,14 @@ The catalog is grouped by task so agents can load only the guidance relevant to
|
|
|
201
203
|
| [`google-cloud-pubsub-dotnet`](skills/google-cloud-pubsub-dotnet/SKILL.md) | Implement robust Google Cloud Pub/Sub publishers and pull subscribers with proper ack/nack semantics. | [client-patterns](skills/google-cloud-pubsub-dotnet/references/client-patterns.md) |
|
|
202
204
|
| [`google-bigquery-dotnet`](skills/google-bigquery-dotnet/SKILL.md) | Query BigQuery with parameterization, pagination, streaming buffers, and strict query-cost awareness. | [query-safety](skills/google-bigquery-dotnet/references/query-safety.md) |
|
|
203
205
|
| [`google-firestore-dotnet`](skills/google-firestore-dotnet/SKILL.md) | Model Firestore documents, transactions, and batched writes with concurrency and read-cost awareness. | — |
|
|
206
|
+
| [`redis-dotnet`](skills/redis-dotnet/SKILL.md) | Design secure .NET Redis caches, expiration, invalidation, and token protection. | — |
|
|
204
207
|
|
|
205
208
|
### 6. ⚡ Distributed Resilience & Security
|
|
206
209
|
|
|
207
210
|
| Skill | Focus & Trigger | Progressive References |
|
|
208
211
|
| :--- | :--- | :--- |
|
|
209
212
|
| [`distributed-workflows`](skills/distributed-workflows/SKILL.md) | Build idempotent consumers, handle partial failures, manage distributed retries, and design background workers. | — |
|
|
213
|
+
| [`retry-resilience`](skills/retry-resilience/SKILL.md) | Add explicitly requested retries or review existing transient failure policies; avoid automatic retries. | — |
|
|
210
214
|
| [`dotnet-security`](skills/dotnet-security/SKILL.md) | Threat-model .NET boundaries, protect against SSRF/XSS/SQLi, enforce authorization, and handle secrets safely. | — |
|
|
211
215
|
| [`production-logging`](skills/production-logging/SKILL.md) | Produce structured, high-signal, sensitive-data-masked, and cost-efficient Serilog/ILogger production logs. | — |
|
|
212
216
|
|
|
@@ -283,9 +287,11 @@ External consumers are never assumed absent just because no internal repository
|
|
|
283
287
|
| :--- | :--- | :--- |
|
|
284
288
|
| Universal engineering rules | Managed section in project `.github/copilot-instructions.md` | Every Copilot coding task in that project |
|
|
285
289
|
| Specialized .NET knowledge | Individual `SKILL.md` files | The coding agent selects relevant skills from their descriptions and the request |
|
|
286
|
-
| Deterministic command enforcement |
|
|
290
|
+
| Deterministic command enforcement | Project hooks/guardrails | Installed for Codex, Copilot, Claude Code, Cursor, and Antigravity in project scope |
|
|
287
291
|
|
|
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.
|
|
292
|
+
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. Project installs now configure native hooks for every supported coding agent using one shared, dependency-free guard script. Existing unrelated hook entries are preserved.
|
|
293
|
+
|
|
294
|
+
The initial policy blocks force-push, destructive Git reset/clean/branch deletion, broad recursive deletion, and direct access to common credential-bearing files. Safe commands continue normally. Host configuration is written to `.codex/hooks.json`, `.github/hooks/dotnet-production-agent-skills.json`, `.claude/settings.json`, `.cursor/hooks.json`, or `.agents/hooks.json` for Antigravity.
|
|
289
295
|
|
|
290
296
|
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
297
|
|
|
@@ -316,7 +322,7 @@ Commands:
|
|
|
316
322
|
|
|
317
323
|
| Flag | Description | Default |
|
|
318
324
|
| :--- | :--- | :--- |
|
|
319
|
-
| `--target <host>` | Target agent: `all`, `shared`, `codex`, `copilot`, `claude`, `cursor` | `all` |
|
|
325
|
+
| `--target <host>` | Target agent: `all`, `shared`, `codex`, `copilot`, `claude`, `cursor`, `antigravity` | `all` |
|
|
320
326
|
| `--scope <scope>` | Installation scope: `user` (home dir) or `project` (repository dir) | `user` |
|
|
321
327
|
| `--project <path>`| Explicit path to repository root (forces `--scope project`) | Current dir |
|
|
322
328
|
| `--mode <mode>` | Installation mode: `copy` (portable) or `link` (symlinks) | `copy` |
|
|
@@ -333,6 +339,7 @@ Commands:
|
|
|
333
339
|
| `copilot` | `~/.copilot/skills` | `<project>/.github/skills` |
|
|
334
340
|
| `claude` | `~/.claude/skills` | `<project>/.claude/skills` |
|
|
335
341
|
| `cursor` | `~/.cursor/skills` | `<project>/.cursor/skills` |
|
|
342
|
+
| `antigravity` | `~/.agents/skills` | `<project>/.agents/skills` |
|
|
336
343
|
| `all` | `~/.agents/skills` + `~/.claude/skills` | `<project>/.agents/skills` + `<project>/.claude/skills` |
|
|
337
344
|
|
|
338
345
|
### Safety & Conflict Handling
|
|
@@ -352,6 +359,7 @@ Commands:
|
|
|
352
359
|
| **GitHub Copilot** | `.github/skills`, `.agents/skills` | Installs to `.github/skills` (project) or `.copilot/skills` (user) | Surface capabilities vary across VS Code, Visual Studio, and JetBrains. |
|
|
353
360
|
| **Claude Code** | Portable `SKILL.md` in `.claude/skills` | Direct copy or symlink into `.claude/skills` | Supports project & user scopes. Committed project skills recommended for team consistency. |
|
|
354
361
|
| **Cursor** | `.cursor/skills` and `.agents/skills` | Installs to `.cursor/skills` or shared `.agents/skills` | Auto-detects compatible `.agents/skills` layouts. |
|
|
362
|
+
| **Google Antigravity** | `.agents/skills` and `.agents/hooks.json` | Uses shared skills plus a native project hook adapter | Works across Antigravity 2.0, CLI, and IDE workspace customization. |
|
|
355
363
|
|
|
356
364
|
---
|
|
357
365
|
|
|
@@ -368,7 +376,7 @@ GitHub Actions publishes the package from [`.github/workflows/publish-npm.yml`](
|
|
|
368
376
|
- **Workflow dispatch**: choose `keep` to publish the current `package.json` version as-is, or `patch` / `minor` / `major` / `prerelease` to bump first. Optionally override the npm dist-tag, and optionally commit/tag changes back to the branch before publish.
|
|
369
377
|
3. Dist-tags default from the version (`latest` for stable, `beta` / `next` / `alpha` / `rc` for matching prereleases) unless you override them on dispatch.
|
|
370
378
|
|
|
371
|
-
`package.json`
|
|
379
|
+
For GitHub release events, the semver release tag is authoritative and both package files are aligned to it before the npm availability check. For manual workflow runs, `package.json` remains the version source of truth. If `package-lock.json` drifts, the workflow aligns it without inventing another semver. `keep` does not bump; it only publishes the current version after lock synchronization. Republishing the resolved version is still rejected.
|
|
372
380
|
|
|
373
381
|
Successful publishes are recorded under the repository **npm** environment (Deployments on the GitHub repo page) and link to the published package version on npmjs.com.
|
|
374
382
|
|
|
@@ -396,6 +404,10 @@ npm pack --dry-run
|
|
|
396
404
|
|
|
397
405
|
---
|
|
398
406
|
|
|
407
|
+
## Release Notes
|
|
408
|
+
|
|
409
|
+
See [release notes](RELEASE_NOTES.md) or the [documentation page](https://dprakash2101.github.io/dotnet-production-agent-skills/release-notes.html) for changes, compatibility notes, and publication status.
|
|
410
|
+
|
|
399
411
|
## Why I built this
|
|
400
412
|
|
|
401
413
|
I created **dotnet-production-agent-skills** after repeatedly explaining the same production expectations to GitHub Copilot—then spending more tokens correcting and reworking the result. This project captures those lessons as focused, reusable guidance so coding agents can start with better context and produce safer .NET code the first time.
|
package/RELEASE_NOTES.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Release notes
|
|
2
|
+
|
|
3
|
+
## v0.3.0 — Multi-agent hooks and Antigravity
|
|
4
|
+
|
|
5
|
+
### New
|
|
6
|
+
|
|
7
|
+
- Added `antigravity` as a first-class CLI target using `.agents/skills` and native project hooks in `.agents/hooks.json`.
|
|
8
|
+
- Project installs can now configure command guardrails for Codex, GitHub Copilot, Claude Code, Cursor, and Google Antigravity. `--target all --scope project` installs all five adapters.
|
|
9
|
+
- Added a shared, dependency-free guard script that blocks force-push, destructive Git operations, broad recursive deletion, and direct access to common credential-bearing files.
|
|
10
|
+
- `list` and `doctor` report each project hook installation. Updates are idempotent, unrelated existing hook entries are preserved, and uninstall removes only package-managed entries.
|
|
11
|
+
|
|
12
|
+
### Compatibility
|
|
13
|
+
|
|
14
|
+
- Existing targets, scopes, copy/link modes, dry runs, conflict handling, and managed Copilot instructions remain supported.
|
|
15
|
+
- Hooks are installed only for project scope. User-scope behavior is unchanged.
|
|
16
|
+
|
|
17
|
+
### Release pipeline
|
|
18
|
+
|
|
19
|
+
- GitHub release tags are authoritative for release-triggered npm publishes. A `v0.3.0` release aligns both package files to `0.3.0` before checking npm availability or publishing, even when the tagged commit still contains an already-published version.
|
|
20
|
+
- The workflow now fails early if the resolved tag version and `package.json` ever disagree, preventing an old package version from being checked or published accidentally.
|
|
21
|
+
|
|
22
|
+
## Unreleased — Redis caching skill
|
|
23
|
+
|
|
24
|
+
Proposed in [PR #6](https://github.com/dprakash2101/dotnet-production-agent-skills/pull/6); this change is not yet published to npm.
|
|
25
|
+
|
|
26
|
+
### New
|
|
27
|
+
|
|
28
|
+
- Added `redis-dotnet` guidance for .NET and ASP.NET Core Redis caching: key design, expiration, invalidation, concurrency, failure handling, and targeted validation.
|
|
29
|
+
- The skill prohibits plaintext credentials in Redis. Credential ciphertext must be bound to the cache key, trusted tenant/account, and credential purpose using authenticated associated data or equivalent purpose isolation, with encryption keys outside Redis. Tests must reject ciphertext copied to another key, tenant/account, or purpose.
|
|
30
|
+
- Added the skill to the README and website catalog.
|
|
31
|
+
|
|
32
|
+
## v0.2.0 — Copilot instructions and safer skill management
|
|
33
|
+
|
|
34
|
+
The implementation was merged in [PR #4](https://github.com/dprakash2101/dotnet-production-agent-skills/pull/4) and published as [`dotnet-production-agent-skills@0.2.0`](https://www.npmjs.com/package/dotnet-production-agent-skills/v/0.2.0). These notes document that release; the release notes page is a follow-up documentation change.
|
|
35
|
+
|
|
36
|
+
### New
|
|
37
|
+
|
|
38
|
+
- Copilot project installs manage an always-on engineering section in `.github/copilot-instructions.md`. Existing team instructions outside the managed markers remain intact; uninstall removes only the managed section.
|
|
39
|
+
- A `retry-resilience` skill covers explicitly requested retries and reviews of existing retry policies. Its guidance avoids introducing retries automatically.
|
|
40
|
+
- The CLI has a separate Copilot hooks interface for future command guardrails. No hooks are installed by this change.
|
|
41
|
+
|
|
42
|
+
### Improved
|
|
43
|
+
|
|
44
|
+
- Repeated installs skip unchanged managed skills. An updated packaged skill replaces an unchanged installed copy.
|
|
45
|
+
- Forced uninstall backs up locally edited managed skills and reports the backup path.
|
|
46
|
+
- `doctor` parses YAML frontmatter, accepts multiline descriptions, checks skill metadata and local references, reports packaged updates and Copilot instruction state, and exits nonzero for installation or validation errors.
|
|
47
|
+
- Skill descriptions now give agents clearer cues for selecting task-specific guidance. Copilot installation output includes skill discovery commands.
|
|
48
|
+
- CLI help displays the published executable name, `dotnet-production-agent-skills`.
|
|
49
|
+
|
|
50
|
+
### Compatibility and upgrade
|
|
51
|
+
|
|
52
|
+
- Targets (`all`, `shared`, `codex`, `copilot`, `claude`, `cursor`), user and project scopes, copy and link modes, and existing CLI flags remain supported.
|
|
53
|
+
- Copilot instructions are added only for project-scope Copilot installs (including `--target all --scope project`). User-scope Copilot installs continue to install skills in `~/.copilot/skills`.
|
|
54
|
+
- Review any local skill edits before updating. The CLI still requires `--force --yes` for noninteractive replacement of conflicts, with a backup of the replaced content.
|
|
55
|
+
- The CLI now depends on the `yaml` npm package. Install the package normally so npm resolves its runtime dependency.
|
|
56
|
+
|
|
57
|
+
### Validation
|
|
58
|
+
|
|
59
|
+
`npm run validate` passed: 30 packaged skills validated and 14 CLI tests passed. The CLI help, Copilot install dry run, doctor output, package dry run, and `git diff --check` were also exercised.
|
package/dist/src/cli.js
CHANGED
|
@@ -7,11 +7,12 @@ import process from "node:process";
|
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import { createInterface } from "node:readline/promises";
|
|
9
9
|
import { changeInstructions, inspectInstructions } from "./copilot/instructions.js";
|
|
10
|
+
import { changeHooks, hookTargets, inspectHooks, removeSharedHookScript } from "./hooks.js";
|
|
10
11
|
import { validateSkills } from "./skills/validation.js";
|
|
11
12
|
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
12
13
|
const SKILLS_ROOT = path.join(PACKAGE_ROOT, "skills");
|
|
13
14
|
const MANIFEST_NAME = ".dotnet-agent-skills.json";
|
|
14
|
-
const SUPPORTED_TARGETS = new Set(["all", "shared", "codex", "copilot", "claude", "cursor"]);
|
|
15
|
+
const SUPPORTED_TARGETS = new Set(["all", "shared", "codex", "copilot", "claude", "cursor", "antigravity"]);
|
|
15
16
|
function usage() {
|
|
16
17
|
return `Usage: dotnet-production-agent-skills <command> [options]
|
|
17
18
|
|
|
@@ -23,7 +24,8 @@ Commands:
|
|
|
23
24
|
uninstall Remove only skills tracked by this package
|
|
24
25
|
|
|
25
26
|
Options:
|
|
26
|
-
--target <all|shared|codex|copilot|claude|cursor>
|
|
27
|
+
--target <all|shared|codex|copilot|claude|cursor|antigravity>
|
|
28
|
+
Default: all
|
|
27
29
|
--scope <user|project> Default: user
|
|
28
30
|
--project <path> Project root; implies project scope
|
|
29
31
|
--mode <copy|link> Default: copy
|
|
@@ -33,8 +35,9 @@ Options:
|
|
|
33
35
|
--json JSON output for list/doctor
|
|
34
36
|
-h, --help Show help
|
|
35
37
|
|
|
36
|
-
For --target all, one shared .agents/skills installation serves
|
|
37
|
-
|
|
38
|
+
For --target all, one shared .agents/skills installation serves compatible agents,
|
|
39
|
+
a second .claude/skills installation serves Claude Code, and project scope installs
|
|
40
|
+
native command-guardrail hooks for Codex, Copilot, Claude, Cursor, and Antigravity.`;
|
|
38
41
|
}
|
|
39
42
|
function requireValue(argv, index, option) {
|
|
40
43
|
const value = argv[index + 1];
|
|
@@ -103,6 +106,7 @@ function destinations(options) {
|
|
|
103
106
|
copilot: ".copilot/skills",
|
|
104
107
|
claude: ".claude/skills",
|
|
105
108
|
cursor: ".cursor/skills",
|
|
109
|
+
antigravity: ".agents/skills",
|
|
106
110
|
}
|
|
107
111
|
: {
|
|
108
112
|
shared: ".agents/skills",
|
|
@@ -110,6 +114,7 @@ function destinations(options) {
|
|
|
110
114
|
copilot: ".github/skills",
|
|
111
115
|
claude: ".claude/skills",
|
|
112
116
|
cursor: ".cursor/skills",
|
|
117
|
+
antigravity: ".agents/skills",
|
|
113
118
|
};
|
|
114
119
|
const targets = options.target === "all" ? ["shared", "claude"] : [options.target];
|
|
115
120
|
const unique = new Map();
|
|
@@ -235,6 +240,9 @@ async function installOrUpdate(options, command) {
|
|
|
235
240
|
if (instructions.state === "malformed")
|
|
236
241
|
throw new Error(`malformed managed Copilot instructions: ${instructions.path}`);
|
|
237
242
|
}
|
|
243
|
+
if (options.scope === "project") {
|
|
244
|
+
await Promise.all(hookTargets(options.target).map((target) => inspectHooks(options.project, PACKAGE_ROOT, target)));
|
|
245
|
+
}
|
|
238
246
|
const packaged = new Set(skills);
|
|
239
247
|
const packageJson = JSON.parse(await readFile(path.join(PACKAGE_ROOT, "package.json"), "utf8"));
|
|
240
248
|
const destinationPlans = [];
|
|
@@ -328,6 +336,12 @@ async function installOrUpdate(options, command) {
|
|
|
328
336
|
if (!options.dryRun)
|
|
329
337
|
console.log("Copilot CLI: /skills list | /skills reload | /skills info <skill-name>");
|
|
330
338
|
}
|
|
339
|
+
if (options.scope === "project") {
|
|
340
|
+
for (const target of hookTargets(options.target)) {
|
|
341
|
+
const action = await changeHooks(options.project, PACKAGE_ROOT, target, "install", options.dryRun);
|
|
342
|
+
console.log(`${action === "skip" ? "skip" : options.dryRun ? `would ${action}` : action} ${target} project hooks`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
331
345
|
}
|
|
332
346
|
async function uninstall(options) {
|
|
333
347
|
if ((options.target === "copilot" || options.target === "all") && options.scope === "project") {
|
|
@@ -335,6 +349,9 @@ async function uninstall(options) {
|
|
|
335
349
|
if (instructions.state === "malformed")
|
|
336
350
|
throw new Error(`malformed managed Copilot instructions: ${instructions.path}`);
|
|
337
351
|
}
|
|
352
|
+
if (options.scope === "project") {
|
|
353
|
+
await Promise.all(hookTargets(options.target).map((target) => inspectHooks(options.project, PACKAGE_ROOT, target)));
|
|
354
|
+
}
|
|
338
355
|
const destinationPlans = [];
|
|
339
356
|
for (const destination of destinations(options)) {
|
|
340
357
|
const manifest = await readManifest(destination.directory);
|
|
@@ -380,6 +397,16 @@ async function uninstall(options) {
|
|
|
380
397
|
if (action !== "skip")
|
|
381
398
|
console.log(`${options.dryRun ? "would remove" : "remove"} Copilot production instructions`);
|
|
382
399
|
}
|
|
400
|
+
if (options.scope === "project") {
|
|
401
|
+
const targets = hookTargets(options.target);
|
|
402
|
+
for (const target of targets) {
|
|
403
|
+
const action = await changeHooks(options.project, PACKAGE_ROOT, target, "uninstall", options.dryRun);
|
|
404
|
+
if (action !== "skip")
|
|
405
|
+
console.log(`${options.dryRun ? "would remove" : "remove"} ${target} project hooks`);
|
|
406
|
+
}
|
|
407
|
+
if (options.target === "all" && targets.length > 0)
|
|
408
|
+
await removeSharedHookScript(options.project, options.dryRun);
|
|
409
|
+
}
|
|
383
410
|
}
|
|
384
411
|
async function inspect(options) {
|
|
385
412
|
const skills = await packagedSkills();
|
|
@@ -405,7 +432,11 @@ async function inspect(options) {
|
|
|
405
432
|
}
|
|
406
433
|
const copilotInstructions = (options.target === "copilot" || options.target === "all") && options.scope === "project"
|
|
407
434
|
? await inspectInstructions(options.project) : undefined;
|
|
408
|
-
|
|
435
|
+
const hooks = options.scope === "project"
|
|
436
|
+
? await Promise.all(hookTargets(options.target).map((target) => inspectHooks(options.project, PACKAGE_ROOT, target)))
|
|
437
|
+
: undefined;
|
|
438
|
+
return { packagedSkills: skills, currentPackageVersion: packageJson.version, installations,
|
|
439
|
+
...(copilotInstructions ? { copilotInstructions } : {}), ...(hooks ? { hooks } : {}) };
|
|
409
440
|
}
|
|
410
441
|
async function validateCanonical() {
|
|
411
442
|
return validateSkills(SKILLS_ROOT, await packagedSkills());
|
|
@@ -442,7 +473,8 @@ async function main() {
|
|
|
442
473
|
const unhealthy = report.installations.flatMap((entry) => entry.skills)
|
|
443
474
|
.filter((entry) => entry.state !== "unchanged" || entry.packaged === "outdated");
|
|
444
475
|
const instructionsUnhealthy = report.copilotInstructions && !["unchanged", "unmanaged"].includes(report.copilotInstructions.state);
|
|
445
|
-
const
|
|
476
|
+
const hooksUnhealthy = report.hooks?.some((hook) => hook.state !== "unchanged") ?? false;
|
|
477
|
+
const output = { ...report, validationErrors, healthy: validationErrors.length === 0 && unhealthy.length === 0 && !instructionsUnhealthy && !hooksUnhealthy };
|
|
446
478
|
if (parsed.options.json)
|
|
447
479
|
console.log(JSON.stringify(output, null, 2));
|
|
448
480
|
else {
|
|
@@ -459,6 +491,7 @@ async function main() {
|
|
|
459
491
|
}
|
|
460
492
|
if (report.copilotInstructions)
|
|
461
493
|
console.log(`Copilot instructions: ${report.copilotInstructions.state} ${report.copilotInstructions.path}`);
|
|
494
|
+
report.hooks?.forEach((hook) => console.log(`${hook.target} hooks: ${hook.state} ${hook.config}`));
|
|
462
495
|
validationErrors.forEach((error) => console.error(`error: ${error}`));
|
|
463
496
|
}
|
|
464
497
|
if (command === "doctor" && !output.healthy)
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { cp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
const OWNER = "dotnet-production-agent-skills";
|
|
4
|
+
const COMMAND = "node .agent-guardrails/guard.mjs";
|
|
5
|
+
const SCRIPT_DIRECTORY = ".agent-guardrails";
|
|
6
|
+
const SCRIPT_NAME = "guard.mjs";
|
|
7
|
+
function isObject(value) {
|
|
8
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9
|
+
}
|
|
10
|
+
async function readJson(file) {
|
|
11
|
+
try {
|
|
12
|
+
const parsed = JSON.parse(await readFile(file, "utf8"));
|
|
13
|
+
if (!isObject(parsed))
|
|
14
|
+
throw new Error("root must be a JSON object");
|
|
15
|
+
return parsed;
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
19
|
+
return {};
|
|
20
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
21
|
+
throw new Error(`invalid hook configuration ${file}: ${detail}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function locations(projectRoot, target) {
|
|
25
|
+
const configs = {
|
|
26
|
+
codex: ".codex/hooks.json",
|
|
27
|
+
copilot: ".github/hooks/dotnet-production-agent-skills.json",
|
|
28
|
+
claude: ".claude/settings.json",
|
|
29
|
+
cursor: ".cursor/hooks.json",
|
|
30
|
+
antigravity: ".agents/hooks.json",
|
|
31
|
+
};
|
|
32
|
+
return { config: path.join(projectRoot, configs[target]), script: path.join(projectRoot, SCRIPT_DIRECTORY, SCRIPT_NAME) };
|
|
33
|
+
}
|
|
34
|
+
function handler(target) {
|
|
35
|
+
return { type: "command", command: `${COMMAND} ${target}`, timeout: 10 };
|
|
36
|
+
}
|
|
37
|
+
function managedGroup(target) {
|
|
38
|
+
return { matcher: target === "antigravity" ? "run_command|view_file|write_to_file|replace_file_content|multi_replace_file_content" : "Bash|PowerShell|Read|Write|Edit", hooks: [handler(target)] };
|
|
39
|
+
}
|
|
40
|
+
function containsManaged(value) {
|
|
41
|
+
return JSON.stringify(value).includes(COMMAND);
|
|
42
|
+
}
|
|
43
|
+
function withoutManaged(value) {
|
|
44
|
+
return Array.isArray(value) ? value.filter((entry) => !containsManaged(entry)) : [];
|
|
45
|
+
}
|
|
46
|
+
function mergeConfig(current, target, install) {
|
|
47
|
+
if (target === "copilot") {
|
|
48
|
+
return install ? {
|
|
49
|
+
version: 1,
|
|
50
|
+
description: `${OWNER} project command guardrails`,
|
|
51
|
+
hooks: { preToolUse: [{ type: "command", command: `${COMMAND} copilot`, timeoutSec: 10 }] },
|
|
52
|
+
} : {};
|
|
53
|
+
}
|
|
54
|
+
if (target === "antigravity") {
|
|
55
|
+
const next = { ...current };
|
|
56
|
+
if (install)
|
|
57
|
+
next[OWNER] = { enabled: true, PreToolUse: [managedGroup(target)] };
|
|
58
|
+
else
|
|
59
|
+
delete next[OWNER];
|
|
60
|
+
return next;
|
|
61
|
+
}
|
|
62
|
+
const hooks = isObject(current.hooks) ? { ...current.hooks } : {};
|
|
63
|
+
const event = target === "cursor" ? "preToolUse" : "PreToolUse";
|
|
64
|
+
const entries = withoutManaged(hooks[event]);
|
|
65
|
+
if (install)
|
|
66
|
+
entries.push(target === "cursor"
|
|
67
|
+
? { command: `${COMMAND} cursor`, timeout: 10, matcher: "Shell|Read|Write", failClosed: true }
|
|
68
|
+
: managedGroup(target));
|
|
69
|
+
if (entries.length)
|
|
70
|
+
hooks[event] = entries;
|
|
71
|
+
else
|
|
72
|
+
delete hooks[event];
|
|
73
|
+
const next = { ...current };
|
|
74
|
+
if (target === "cursor")
|
|
75
|
+
next.version = typeof current.version === "number" ? current.version : 1;
|
|
76
|
+
if (Object.keys(hooks).length)
|
|
77
|
+
next.hooks = hooks;
|
|
78
|
+
else
|
|
79
|
+
delete next.hooks;
|
|
80
|
+
return next;
|
|
81
|
+
}
|
|
82
|
+
async function expectedScript(packageRoot) {
|
|
83
|
+
return readFile(path.join(packageRoot, "hooks", SCRIPT_NAME), "utf8");
|
|
84
|
+
}
|
|
85
|
+
export async function inspectHooks(projectRoot, packageRoot, target) {
|
|
86
|
+
const files = locations(projectRoot, target);
|
|
87
|
+
try {
|
|
88
|
+
const [current, script] = await Promise.all([readJson(files.config), readFile(files.script, "utf8")]);
|
|
89
|
+
const expected = mergeConfig(current, target, true);
|
|
90
|
+
const configMatches = JSON.stringify(current) === JSON.stringify(expected);
|
|
91
|
+
return { target, state: configMatches && script === await expectedScript(packageRoot) ? "unchanged" : "modified", ...files };
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
95
|
+
return { target, state: "missing", ...files };
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
export async function changeHooks(projectRoot, packageRoot, target, action, dryRun) {
|
|
100
|
+
const files = locations(projectRoot, target);
|
|
101
|
+
const current = await readJson(files.config);
|
|
102
|
+
const next = mergeConfig(current, target, action === "install");
|
|
103
|
+
const hasConfigChange = JSON.stringify(current) !== JSON.stringify(next);
|
|
104
|
+
let scriptMatches = false;
|
|
105
|
+
try {
|
|
106
|
+
scriptMatches = await readFile(files.script, "utf8") === await expectedScript(packageRoot);
|
|
107
|
+
}
|
|
108
|
+
catch { /* missing */ }
|
|
109
|
+
if (action === "install") {
|
|
110
|
+
if (!hasConfigChange && scriptMatches)
|
|
111
|
+
return "skip";
|
|
112
|
+
if (!dryRun) {
|
|
113
|
+
await mkdir(path.dirname(files.config), { recursive: true });
|
|
114
|
+
await mkdir(path.dirname(files.script), { recursive: true });
|
|
115
|
+
await writeFile(files.config, `${JSON.stringify(next, null, 2)}\n`, "utf8");
|
|
116
|
+
await cp(path.join(packageRoot, "hooks", SCRIPT_NAME), files.script);
|
|
117
|
+
}
|
|
118
|
+
return Object.keys(current).length ? "update" : "install";
|
|
119
|
+
}
|
|
120
|
+
if (!hasConfigChange)
|
|
121
|
+
return "skip";
|
|
122
|
+
if (!dryRun) {
|
|
123
|
+
if (Object.keys(next).length)
|
|
124
|
+
await writeFile(files.config, `${JSON.stringify(next, null, 2)}\n`, "utf8");
|
|
125
|
+
else
|
|
126
|
+
await rm(files.config, { force: true });
|
|
127
|
+
}
|
|
128
|
+
return "remove";
|
|
129
|
+
}
|
|
130
|
+
export function hookTargets(target) {
|
|
131
|
+
if (target === "all")
|
|
132
|
+
return ["codex", "copilot", "claude", "cursor", "antigravity"];
|
|
133
|
+
return target === "shared" ? [] : [target];
|
|
134
|
+
}
|
|
135
|
+
export async function removeSharedHookScript(projectRoot, dryRun) {
|
|
136
|
+
if (!dryRun)
|
|
137
|
+
await rm(path.join(projectRoot, SCRIPT_DIRECTORY), { recursive: true, force: true });
|
|
138
|
+
}
|
package/hooks/guard.mjs
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const host = process.argv[2] ?? "unknown";
|
|
4
|
+
let input = {};
|
|
5
|
+
try {
|
|
6
|
+
const chunks = [];
|
|
7
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
8
|
+
input = JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}");
|
|
9
|
+
} catch {
|
|
10
|
+
process.stderr.write("Guardrail input was not valid JSON.\n");
|
|
11
|
+
process.exit(2);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const tool = input.tool_name ?? input.toolName ?? input.toolCall?.name ?? "";
|
|
15
|
+
const args = input.tool_input ?? input.toolArgs ?? input.toolCall?.args ?? {};
|
|
16
|
+
const command = String(args.command ?? args.CommandLine ?? args.script ?? "").trim();
|
|
17
|
+
const file = String(args.file_path ?? args.path ?? args.TargetFile ?? args.AbsolutePath ?? "").replaceAll("\\", "/");
|
|
18
|
+
|
|
19
|
+
const checks = [
|
|
20
|
+
[/\bgit\s+push\b[^\n;&|]*(?:--force(?:-with-lease)?|-f)\b/i, "Force-push is blocked. Use a normal push or request explicit human approval."],
|
|
21
|
+
[/\bgit\s+(?:reset\s+--hard|clean\s+-[^\s]*f[^\s]*|branch\s+-D)(?:\s|$)/i, "Destructive Git operations are blocked. Preserve work and request explicit human approval."],
|
|
22
|
+
[/(?:^|[;&|]\s*)(?:rm\s+-[^\s]*r[^\s]*f|Remove-Item\b[^\n;&|]*-Recurse[^\n;&|]*-Force)\s+(?:\/|~|\$HOME|\$env:USERPROFILE)(?:\s|$)/i, "Recursive deletion of a broad system or home path is blocked."],
|
|
23
|
+
[/(?:cat|type|Get-Content|more|less)\s+[^\n;&|]*(?:\.env(?:\.|\s|$)|id_(?:rsa|ed25519)|\.pem\b|\.pfx\b|secrets\.json\b)/i, "Reading credential-bearing files through the shell is blocked."],
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
const sensitivePath = /(?:^|\/)(?:\.env(?:\.[^/]*)?|id_(?:rsa|ed25519)|[^/]*\.(?:pem|pfx)|secrets\.json)$/i;
|
|
27
|
+
let reason = sensitivePath.test(file) ? "Direct access to a credential-bearing file is blocked." : "";
|
|
28
|
+
if (!reason && command) reason = checks.find(([pattern]) => pattern.test(command))?.[1] ?? "";
|
|
29
|
+
|
|
30
|
+
function response(decision, message = "") {
|
|
31
|
+
if (host === "cursor") return decision === "deny"
|
|
32
|
+
? { permission: "deny", user_message: message, agent_message: message }
|
|
33
|
+
: { permission: "allow" };
|
|
34
|
+
if (host === "antigravity") return { decision, ...(message ? { reason: message } : {}) };
|
|
35
|
+
if (host === "copilot") return decision === "deny"
|
|
36
|
+
? { permissionDecision: "deny", permissionDecisionReason: message }
|
|
37
|
+
: { permissionDecision: "allow" };
|
|
38
|
+
return decision === "deny" ? {
|
|
39
|
+
hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: message },
|
|
40
|
+
} : {};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
process.stdout.write(`${JSON.stringify(response(reason ? "deny" : "allow", reason))}\n`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dotnet-production-agent-skills",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Portable production .NET engineering skills for coding agents",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -21,8 +21,10 @@
|
|
|
21
21
|
},
|
|
22
22
|
"files": [
|
|
23
23
|
"dist/src",
|
|
24
|
+
"hooks",
|
|
24
25
|
"copilot",
|
|
25
26
|
"skills",
|
|
27
|
+
"RELEASE_NOTES.md",
|
|
26
28
|
"README.md",
|
|
27
29
|
"LICENSE"
|
|
28
30
|
],
|
|
@@ -35,7 +37,7 @@
|
|
|
35
37
|
},
|
|
36
38
|
"scripts": {
|
|
37
39
|
"build": "tsc -p tsconfig.json",
|
|
38
|
-
"test": "npm run build && node --test dist/test/cli.test.js",
|
|
40
|
+
"test": "npm run build && node --test dist/test/cli.test.js scripts/*.test.mjs",
|
|
39
41
|
"validate": "python3 scripts/validate_skills.py && npm test",
|
|
40
42
|
"prepack": "npm run build"
|
|
41
43
|
},
|
|
@@ -49,7 +51,8 @@
|
|
|
49
51
|
"codex",
|
|
50
52
|
"copilot",
|
|
51
53
|
"claude-code",
|
|
52
|
-
"cursor"
|
|
54
|
+
"cursor",
|
|
55
|
+
"antigravity"
|
|
53
56
|
],
|
|
54
57
|
"dependencies": {
|
|
55
58
|
"yaml": "^2.9.1"
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: redis-dotnet
|
|
3
|
+
description: Implement, review, or troubleshoot Redis caching in .NET and ASP.NET Core, including StackExchange.Redis, IDistributedCache, key design, expiration, cache invalidation, performance, and secure handling of cached tokens or secrets.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Redis caching for .NET
|
|
7
|
+
|
|
8
|
+
Inspect the application's existing cache abstraction, Redis client registration, serializers, and failure policy before changing code. Reuse established helpers and avoid adding a second connection or serialization scheme without a reason.
|
|
9
|
+
|
|
10
|
+
- Choose a stable namespaced key with environment and tenant/customer isolation where needed. Do not place access tokens, secret keys, passwords, or sensitive personal data in key names; keys appear in diagnostics and operational tools.
|
|
11
|
+
- Set an explicit TTL from the data's real validity. For access tokens, expire the cache entry before the token's expiry with a small safety margin, and handle refresh races across instances. Define invalidation when source data changes. Avoid unbounded or permanently cached entries by default.
|
|
12
|
+
- Keep values small and version serialized payloads when schemas can change. Do not cache null/error responses without a deliberate short-lived policy. Avoid large scans, blocking commands, and per-request connection creation; bound timeouts and concurrency.
|
|
13
|
+
- **Never store access tokens, refresh tokens, API keys, secret keys, passwords, or other credentials in Redis as plaintext values.** Prefer caching a non-sensitive reference when possible. If a credential must be cached, encrypt before writing and decrypt only after reading. Require the established, vetted helper to authenticate the intended cache key, trusted tenant/account identity, and credential purpose as associated data (AAD), or provide equivalent purpose isolation for that exact context. Derive the same unambiguous, versioned context independently from the authorized request and computed key on both paths; never accept tenant or purpose solely from Redis or the ciphertext. Reject a mismatch without returning or using the credential. If the helper cannot bind this context, extend or wrap it safely before caching credentials. Keep encryption keys outside Redis in the existing secret-management system and support key rotation; do not invent cryptography.
|
|
14
|
+
- Protect Redis transport with TLS and restrict access using the deployment's network and least-privilege authentication controls. Encryption in transit or at rest does not replace application-level protection of sensitive cached values. Do not log cache values, credentials, connection strings, or decrypted payloads.
|
|
15
|
+
- Decide whether a Redis miss or outage should fall back to the source, fail closed, or surface an error based on the flow. Avoid a thundering herd on misses and keep any lock or refresh mechanism bounded. Add retries only when requested or required by existing architecture.
|
|
16
|
+
- Test expiration, invalidation, tenant isolation, concurrent cache misses, serialization changes, and Redis failure behavior where relevant. For credential caching, verify plaintext secrecy, decrypt failures, and key rotation. Copy a valid ciphertext to another cache key, tenant/account, or credential purpose and verify decryption or use fails closed in each case.
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|