vouchington-tooling 0.1.9 → 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.
Files changed (45) hide show
  1. package/README.md +13 -5
  2. package/dist/cli/commands/link-skill.d.mts +5 -0
  3. package/dist/cli/commands/link-skill.mjs +6 -0
  4. package/dist/cli/index.mjs +3 -0
  5. package/dist/cli/parse.d.mts +5 -0
  6. package/dist/cli/parse.mjs +25 -0
  7. package/dist/cli/usage.d.mts +1 -1
  8. package/dist/cli/usage.mjs +2 -0
  9. package/dist/index.d.mts +2 -0
  10. package/dist/index.mjs +1 -0
  11. package/dist/skill-discovery/index.d.mts +12 -0
  12. package/dist/skill-discovery/index.mjs +64 -0
  13. package/dist/skill-discovery/manifest.d.mts +15 -0
  14. package/dist/skill-discovery/manifest.mjs +85 -0
  15. package/dist/skill-discovery/target-directory.d.mts +10 -0
  16. package/dist/skill-discovery/target-directory.mjs +126 -0
  17. package/package.json +6 -1
  18. package/scripts/build.mjs +16 -4
  19. package/skills/agent-workflow/SKILL.md +5 -0
  20. package/skills/agent-workflow/references/evidence-sweep.md +8 -0
  21. package/skills/agent-workflow/references/implementation-and-review.md +9 -0
  22. package/skills/agent-workflow/references/implementation.md +8 -0
  23. package/skills/agent-workflow/references/review.md +7 -0
  24. package/skills/backend-vitest-test-authoring/SKILL.md +15 -0
  25. package/skills/backend-vitest-test-authoring/references/integration-boundaries.md +10 -0
  26. package/skills/dotnet-test-authoring/SKILL.md +16 -0
  27. package/skills/manifest.json +158 -0
  28. package/skills/nextjs-vitest-test-authoring/SKILL.md +15 -0
  29. package/skills/nextjs-vitest-test-authoring/references/framework-boundaries.md +9 -0
  30. package/skills/planning/SKILL.md +3 -0
  31. package/skills/planning/references/impact-discovery.md +9 -0
  32. package/skills/playwright-authoring/SKILL.md +15 -0
  33. package/skills/playwright-authoring/references/browser-reliability.md +9 -0
  34. package/skills/postgres-node-performance-tuning/SKILL.md +15 -0
  35. package/skills/postgres-node-performance-tuning/references/performance-patterns.md +14 -0
  36. package/skills/postgres-partitioning-uuid-v7/SKILL.md +16 -0
  37. package/skills/postgres-partitioning-uuid-v7/references/partition-lifecycle.md +14 -0
  38. package/skills/storybook-authoring/SKILL.md +15 -0
  39. package/skills/storybook-authoring/references/component-coverage.md +9 -0
  40. package/skills/swift-test-authoring/SKILL.md +15 -0
  41. package/skills/swift-test-authoring/references/network-test-doubles.md +9 -0
  42. package/skills/test-authoring/SKILL.md +17 -0
  43. package/skills/test-authoring/references/core-practice.md +10 -0
  44. package/skills/vitest-test-authoring/SKILL.md +15 -0
  45. package/skills/vitest-test-authoring/references/mock-boundaries.md +9 -0
package/README.md CHANGED
@@ -199,8 +199,16 @@ process-group probe and bounded-drain semantics outside a browser session.
199
199
 
200
200
  ## Workflow skills outside plugins
201
201
 
202
- The package ships the canonical Vouchington workflow skill tree at
203
- `skills/<skill>/SKILL.md`. This stable installed path supports agents that do not
204
- load Claude or Codex plugins. The Claude and Codex plugin manifests continue to reference the same
205
- canonical source tree under `plugins/vouchington-workflow/skills`; package build materializes that tree
206
- without hand-copying skill content.
202
+ The package ships a flat union of canonical workflow, testing, and database skills at
203
+ `skills/<skill>/SKILL.md`. This stable installed path supports agents that do not load Claude or
204
+ Codex plugins. The package build materializes plugin source trees without hand-copying skill content
205
+ and writes sorted schema-v1 provenance to `skills/manifest.json`.
206
+ Each manifest entry may declare its ordered `prerequisites`; ordinary Markdown links remain
207
+ cross-references and never cause additional skills to be linked.
208
+
209
+ Use `readSkillManifest(skillsRoot)` to discover installed skills or
210
+ `linkSkill({ name, sourceRoot, targetRoot })` to link one into an explicit consumer directory. The
211
+ CLI equivalent is `vouchington link-skill <name> --source-root <skills-dir> --target-root <dir>`.
212
+ It rejects unknown names, paths outside either root, and existing non-matching destinations.
213
+ `targetRoot` must already exist as a physical directory path: symlinked target roots or ancestors are
214
+ rejected.
@@ -0,0 +1,5 @@
1
+ export declare function runLinkSkill(options: {
2
+ name: string;
3
+ sourceRoot: string;
4
+ targetRoot: string;
5
+ }): Promise<number>;
@@ -0,0 +1,6 @@
1
+ import { linkSkill } from '../../skill-discovery/index.mjs';
2
+ export async function runLinkSkill(options) {
3
+ const result = await linkSkill(options);
4
+ process.stdout.write(`${result.created ? 'linked' : 'already-linked'} ${result.path}\n`);
5
+ return 0;
6
+ }
@@ -15,6 +15,7 @@ import { runStageReviewPayloadCommand } from './commands/stage-review-payload.mj
15
15
  import { runSwiftSemanticEqualCommand } from './commands/swift-semantic-equal.mjs';
16
16
  import { runVitestBlobManifestCommand } from './commands/vitest-blob-manifest.mjs';
17
17
  import { runRetrospectiveTranscriptCommand } from './commands/retrospective-transcript.mjs';
18
+ import { runLinkSkill } from './commands/link-skill.mjs';
18
19
  import { runWithHostLock } from './commands/with-host-lock.mjs';
19
20
  import { parseCli } from './parse.mjs';
20
21
  import { packageScriptPath } from './script-path.mjs';
@@ -99,6 +100,8 @@ export function runCli(argv = process.argv) {
99
100
  return runGhaArtifactsCleanup(parsed);
100
101
  case 'retrospective-transcript':
101
102
  return runRetrospectiveTranscriptCommand(parsed.args);
103
+ case 'link-skill':
104
+ return runLinkSkill(parsed);
102
105
  }
103
106
  }
104
107
  function readInstalledVersion() {
@@ -43,6 +43,11 @@ export type ParsedCli = {
43
43
  } | {
44
44
  kind: 'retrospective-transcript';
45
45
  args: string[];
46
+ } | {
47
+ kind: 'link-skill';
48
+ name: string;
49
+ sourceRoot: string;
50
+ targetRoot: string;
46
51
  } | ParsedGhaRuntimeAudit | ParsedGhaArtifactsCleanup;
47
52
  export type ScriptCommand = 'gha-output' | 'gha-needs-results' | 'download-with-diagnostics' | 'download-optional-run-artifacts' | 'host-pressure-diagnostics' | 'allocate-browser-safe-ports' | 'diagnose-port-collision' | 'prepare-trivy-db' | 'check-cache-size' | 'make-shard-matrix' | 'load-runner-env' | 'clean-workspace' | 'install-github-release' | 'run-with-timeout' | 'lint-links' | 'materialize-pr-context' | 'wait-for-apt-locks' | 'install-playwright-chromium-arm64' | 'ghcr-package-retention';
48
53
  export declare function parseCli(argv: readonly string[]): ParsedCli;
@@ -50,6 +50,8 @@ export function parseCli(argv) {
50
50
  return parseHttpOrigin(rest);
51
51
  if (command === 'retrospective-transcript')
52
52
  return { kind: 'retrospective-transcript', args: rest };
53
+ if (command === 'link-skill')
54
+ return parseLinkSkill(rest);
53
55
  if (command === 'gha-artifacts-cleanup')
54
56
  return parseGhaArtifactsCleanup(rest);
55
57
  if (command !== undefined && SCRIPT_COMMANDS.has(command)) {
@@ -57,6 +59,29 @@ export function parseCli(argv) {
57
59
  }
58
60
  return { kind: 'error', message: `unknown command: ${command}` };
59
61
  }
62
+ function parseLinkSkill(args) {
63
+ const [name, ...flags] = args;
64
+ if (name === undefined || name.startsWith('-'))
65
+ return { kind: 'error', message: 'link-skill requires a skill name' };
66
+ let sourceRoot;
67
+ let targetRoot;
68
+ for (let index = 0; index < flags.length; index += 1) {
69
+ const flag = flags[index];
70
+ const value = flags[index + 1];
71
+ if (flag !== '--source-root' && flag !== '--target-root')
72
+ return { kind: 'error', message: `unknown link-skill option: ${flag}` };
73
+ if (value === undefined)
74
+ return { kind: 'error', message: `${flag} requires a path` };
75
+ if (flag === '--source-root')
76
+ sourceRoot = value;
77
+ else
78
+ targetRoot = value;
79
+ index += 1;
80
+ }
81
+ if (sourceRoot === undefined || targetRoot === undefined)
82
+ return { kind: 'error', message: 'link-skill requires --source-root and --target-root' };
83
+ return { kind: 'link-skill', name, sourceRoot, targetRoot };
84
+ }
60
85
  function parseRunnerPortPolicy(args) {
61
86
  let file;
62
87
  let reserved;
@@ -1,2 +1,2 @@
1
- export declare const USAGE = "Usage: vouchington <command> [options]\n\nCommands:\n runner-port-policy Print or validate a runner port policy\n with-host-lock Run a command under a host-wide lock\n gha-runtime-audit Audit successful GitHub Actions job runtimes\n gha-output Write a collision-safe multiline GITHUB_OUTPUT record\n gha-needs-results Fail if required GitHub Actions job results failed\n download-with-diagnostics Download a URL and report HTTP status on failure\n download-optional-run-artifacts Download optional artifacts from the current run\n host-pressure-diagnostics Print a bounded host memory/OOM/PSI snapshot\n allocate-browser-safe-ports Allocate Fetch-safe localhost ports\n diagnose-port-collision Capture bounded localhost port diagnostics\n prepare-trivy-db Download the Trivy vulnerability database\n gha-artifacts-cleanup Delete classified GitHub Actions artifacts\n http-origin Validate an optional HTTP(S) origin\n vitest-blob-manifest Stamp a vitest-blob-manifest:v1 identity file\n pnpm-install Install a pnpm workspace with retry and release-age fail-fast\n check-cache-size Measure a path and decide whether to save a GHA cache\n make-shard-matrix Emit a [1..N] GitHub Actions shard matrix\n load-runner-env Overlay a runner env file onto GITHUB_ENV with injection guards\n clean-workspace Reset a persistent-runner workspace with a fork-PR trust gate\n install-github-release Download a checksum-verified GitHub Release binary\n run-with-timeout Run a command with GNU timeout or a Perl fallback\n lint-links Two-pass lychee: internal links fail, external warn\n materialize-pr-context Dump PR title/body/files/diff/comments and #N crawl\n wait-for-apt-locks Wait until apt/dpkg lock files are free\n install-playwright-chromium-arm64 Install Playwright Chromium from browsers.json\n ghcr-package-retention Delete old GHCR package versions past KEEP_MIN\n nuget-central-version Validate a Directory.Packages.props PackageVersion delta\n swift-semantic-equal Compare Swift sources ignoring comments and whitespace\n post-review Post one COMMENT review from a staged payload file\n stage-review-payload Validate a review payload file into a staging directory\n retrospective-transcript Format facts from Claude-compatible, Codex, or Grok transcripts\n\nOptions:\n -h, --help Show this help\n -v, --version Print the package version\n\nrunner-port-policy\n (no args) Print the shipped policy as JSON\n --file <path> Validate and print a policy file\n --reserved <port> Print true if the port is reserved\n\nwith-host-lock\n --name <family>\n [--slots <n>]\n --timeout-seconds <n>\n [--command-timeout-seconds <n>]\n [--failure-diagnostics <absolute-script>]\n [--on-acquire-timeout fail|run-unlocked]\n -- <command> [args...]\n\ngha-runtime-audit\n [--repository owner/name] Default GITHUB_REPOSITORY\n [--branch main]\n --pr-workflow <name|/regex/> Repeatable\n --push-workflow <name|/regex/> Repeatable\n\ngha-output <name>\ngha-needs-results [label]\ndownload-with-diagnostics <url> <destination> [-- curl-args...]\ndownload-optional-run-artifacts (--name <name> | --pattern <pattern>) --dir <directory>\nhost-pressure-diagnostics\nallocate-browser-safe-ports [count] [--policy path] [--forbidden-ports path]\ndiagnose-port-collision [--ports \"2200 2216\"] [--output-dir PATH]\nprepare-trivy-db\ngha-artifacts-cleanup run --run-id <id> [--keep-pattern glob] [--delete-pattern glob] [--patterns-file json]\ngha-artifacts-cleanup sweep --older-than-hours <n> [--keep-pattern glob] [--delete-pattern glob] [--patterns-file json]\nhttp-origin [--field NAME] [value]\nvitest-blob-manifest <suite> [reports-directory]\npnpm-install --runner-lifecycle persistent|ephemeral|ephemeral-full --install-scripts true|false\ncheck-cache-size <path> <max-bytes> <label>\nmake-shard-matrix <total>\nload-runner-env\nclean-workspace\ninstall-github-release --repo owner/name --version X --asset 'name-{platform}.tar.gz' --bin name [--tag-prefix PREFIX] [--expected-sha256 SHA256] [--no-checksum] [--checksums-asset NAME] [--version-flag FLAG] [--bin-dir DIR]\nrun-with-timeout <timeout-seconds> <kill-after-seconds> <command...>\nlint-links [--offline] [--config PATH] [--glob PATTERN] [files...]\nmaterialize-pr-context\nwait-for-apt-locks\ninstall-playwright-chromium-arm64 [name:archive...]\nghcr-package-retention <url-encoded-package>...\nnuget-central-version <trusted-props> <candidate-props> <metadata-json> <output-props>\nswift-semantic-equal <base> <head> <file.swift>\npost-review\nstage-review-payload optional|required <source> <destination>\nretrospective-transcript [--session-id ID] [--jsonl PATH] [--projects-dir PATH] [--codex-sessions-dir PATH] [--grok-sessions-dir PATH]\n";
1
+ export declare const USAGE = "Usage: vouchington <command> [options]\n\nCommands:\n runner-port-policy Print or validate a runner port policy\n with-host-lock Run a command under a host-wide lock\n gha-runtime-audit Audit successful GitHub Actions job runtimes\n gha-output Write a collision-safe multiline GITHUB_OUTPUT record\n gha-needs-results Fail if required GitHub Actions job results failed\n download-with-diagnostics Download a URL and report HTTP status on failure\n download-optional-run-artifacts Download optional artifacts from the current run\n host-pressure-diagnostics Print a bounded host memory/OOM/PSI snapshot\n allocate-browser-safe-ports Allocate Fetch-safe localhost ports\n diagnose-port-collision Capture bounded localhost port diagnostics\n prepare-trivy-db Download the Trivy vulnerability database\n gha-artifacts-cleanup Delete classified GitHub Actions artifacts\n http-origin Validate an optional HTTP(S) origin\n vitest-blob-manifest Stamp a vitest-blob-manifest:v1 identity file\n pnpm-install Install a pnpm workspace with retry and release-age fail-fast\n check-cache-size Measure a path and decide whether to save a GHA cache\n make-shard-matrix Emit a [1..N] GitHub Actions shard matrix\n load-runner-env Overlay a runner env file onto GITHUB_ENV with injection guards\n clean-workspace Reset a persistent-runner workspace with a fork-PR trust gate\n install-github-release Download a checksum-verified GitHub Release binary\n run-with-timeout Run a command with GNU timeout or a Perl fallback\n lint-links Two-pass lychee: internal links fail, external warn\n materialize-pr-context Dump PR title/body/files/diff/comments and #N crawl\n wait-for-apt-locks Wait until apt/dpkg lock files are free\n install-playwright-chromium-arm64 Install Playwright Chromium from browsers.json\n ghcr-package-retention Delete old GHCR package versions past KEEP_MIN\n nuget-central-version Validate a Directory.Packages.props PackageVersion delta\n swift-semantic-equal Compare Swift sources ignoring comments and whitespace\n post-review Post one COMMENT review from a staged payload file\n stage-review-payload Validate a review payload file into a staging directory\n retrospective-transcript Format facts from Claude-compatible, Codex, or Grok transcripts\n link-skill Link one packaged skill into an explicit consumer directory\n\nOptions:\n -h, --help Show this help\n -v, --version Print the package version\n\nrunner-port-policy\n (no args) Print the shipped policy as JSON\n --file <path> Validate and print a policy file\n --reserved <port> Print true if the port is reserved\n\nwith-host-lock\n --name <family>\n [--slots <n>]\n --timeout-seconds <n>\n [--command-timeout-seconds <n>]\n [--failure-diagnostics <absolute-script>]\n [--on-acquire-timeout fail|run-unlocked]\n -- <command> [args...]\n\ngha-runtime-audit\n [--repository owner/name] Default GITHUB_REPOSITORY\n [--branch main]\n --pr-workflow <name|/regex/> Repeatable\n --push-workflow <name|/regex/> Repeatable\n\ngha-output <name>\ngha-needs-results [label]\ndownload-with-diagnostics <url> <destination> [-- curl-args...]\ndownload-optional-run-artifacts (--name <name> | --pattern <pattern>) --dir <directory>\nhost-pressure-diagnostics\nallocate-browser-safe-ports [count] [--policy path] [--forbidden-ports path]\ndiagnose-port-collision [--ports \"2200 2216\"] [--output-dir PATH]\nprepare-trivy-db\ngha-artifacts-cleanup run --run-id <id> [--keep-pattern glob] [--delete-pattern glob] [--patterns-file json]\ngha-artifacts-cleanup sweep --older-than-hours <n> [--keep-pattern glob] [--delete-pattern glob] [--patterns-file json]\nhttp-origin [--field NAME] [value]\nvitest-blob-manifest <suite> [reports-directory]\npnpm-install --runner-lifecycle persistent|ephemeral|ephemeral-full --install-scripts true|false\ncheck-cache-size <path> <max-bytes> <label>\nmake-shard-matrix <total>\nload-runner-env\nclean-workspace\ninstall-github-release --repo owner/name --version X --asset 'name-{platform}.tar.gz' --bin name [--tag-prefix PREFIX] [--expected-sha256 SHA256] [--no-checksum] [--checksums-asset NAME] [--version-flag FLAG] [--bin-dir DIR]\nrun-with-timeout <timeout-seconds> <kill-after-seconds> <command...>\nlint-links [--offline] [--config PATH] [--glob PATTERN] [files...]\nmaterialize-pr-context\nwait-for-apt-locks\ninstall-playwright-chromium-arm64 [name:archive...]\nghcr-package-retention <url-encoded-package>...\nnuget-central-version <trusted-props> <candidate-props> <metadata-json> <output-props>\nswift-semantic-equal <base> <head> <file.swift>\npost-review\nstage-review-payload optional|required <source> <destination>\nretrospective-transcript [--session-id ID] [--jsonl PATH] [--projects-dir PATH] [--codex-sessions-dir PATH] [--grok-sessions-dir PATH]\nlink-skill <name> --source-root <skills-dir> --target-root <consumer-skills-dir>\n";
2
2
  export declare function printUsage(stream?: NodeJS.WritableStream): void;
@@ -32,6 +32,7 @@ Commands:
32
32
  post-review Post one COMMENT review from a staged payload file
33
33
  stage-review-payload Validate a review payload file into a staging directory
34
34
  retrospective-transcript Format facts from Claude-compatible, Codex, or Grok transcripts
35
+ link-skill Link one packaged skill into an explicit consumer directory
35
36
 
36
37
  Options:
37
38
  -h, --help Show this help
@@ -86,6 +87,7 @@ swift-semantic-equal <base> <head> <file.swift>
86
87
  post-review
87
88
  stage-review-payload optional|required <source> <destination>
88
89
  retrospective-transcript [--session-id ID] [--jsonl PATH] [--projects-dir PATH] [--codex-sessions-dir PATH] [--grok-sessions-dir PATH]
90
+ link-skill <name> --source-root <skills-dir> --target-root <consumer-skills-dir>
89
91
  `;
90
92
  export function printUsage(stream = process.stdout) {
91
93
  stream.write(USAGE);
package/dist/index.d.mts CHANGED
@@ -1,3 +1,5 @@
1
+ export { linkSkill, readSkillManifest } from './skill-discovery/index.mts';
2
+ export type { LinkSkillOptions, LinkSkillResult, SkillManifest, SkillManifestEntry, } from './skill-discovery/index.mts';
1
3
  export { codexChildren, codexIdentity, computeTranscriptFacts, formatTranscriptFacts, formatUnavailable, resolveTranscriptFile, runRetrospectiveTranscript, } from './retrospective-transcript/index.mts';
2
4
  export type { ResolveOptions, TokenTotals, TranscriptFacts, } from './retrospective-transcript/index.mts';
3
5
  export { buildSessionFrictionReport, classifyFrictionObservation, FRICTION_LOG_MAX_EVENTS, isConformingCiFailureBlock, normalizeCommandPrefix, readFrictionLog, recordFriction, } from './session-friction/index.mts';
package/dist/index.mjs CHANGED
@@ -1,4 +1,5 @@
1
1
  /* eslint-disable max-lines -- package entry point enumerates the supported public API. */
2
+ export { linkSkill, readSkillManifest } from './skill-discovery/index.mjs';
2
3
  export { codexChildren, codexIdentity, computeTranscriptFacts, formatTranscriptFacts, formatUnavailable, resolveTranscriptFile, runRetrospectiveTranscript, } from './retrospective-transcript/index.mjs';
3
4
  export { buildSessionFrictionReport, classifyFrictionObservation, FRICTION_LOG_MAX_EVENTS, isConformingCiFailureBlock, normalizeCommandPrefix, readFrictionLog, recordFriction, } from './session-friction/index.mjs';
4
5
  export { EphemeralListenerAttemptsExhaustedError, isRunnerReservedPort, listenOnRunnerUnreservedEphemeralPort, loadRunnerPortPolicy, runnerPortPolicy, validateRunnerPortPolicy, } from './runner-port-policy/index.mjs';
@@ -0,0 +1,12 @@
1
+ export { readSkillManifest, type SkillManifest, type SkillManifestEntry } from './manifest.mts';
2
+ export type LinkSkillOptions = {
3
+ name: string;
4
+ sourceRoot: string;
5
+ targetRoot: string;
6
+ };
7
+ export type LinkSkillResult = {
8
+ created: boolean;
9
+ path: string;
10
+ source: string;
11
+ };
12
+ export declare function linkSkill(options: LinkSkillOptions): Promise<LinkSkillResult>;
@@ -0,0 +1,64 @@
1
+ import { lstat, realpath } from 'node:fs/promises';
2
+ import { dirname, resolve } from 'node:path';
3
+ import { linkDirectoryEntry, resolveTargetDirectory, } from './target-directory.mjs';
4
+ import { assertContained, isContained, isSafeSkillName, readSkillManifest, } from './manifest.mjs';
5
+ export { readSkillManifest } from './manifest.mjs';
6
+ export async function linkSkill(options) {
7
+ if (!isSafeSkillName(options.name))
8
+ throw new Error(`Invalid skill name: ${options.name}`);
9
+ const sourceRoot = resolve(options.sourceRoot);
10
+ const canonicalSourceRoot = await realpath(sourceRoot);
11
+ const manifest = await readSkillManifest(sourceRoot);
12
+ const entry = manifest.skills.find((candidate) => candidate.name === options.name);
13
+ if (entry === undefined)
14
+ throw new Error(`Unknown skill: ${options.name}`);
15
+ const targetRoot = await resolveTargetDirectory(options.targetRoot);
16
+ const linked = new Set();
17
+ const linking = new Set();
18
+ return linkManifestSkill(sourceRoot, canonicalSourceRoot, targetRoot, manifest, entry, linked, linking);
19
+ }
20
+ async function linkManifestSkill(sourceRoot, canonicalSourceRoot, targetRoot, manifest, entry, linked, linking) {
21
+ if (linked.has(entry.name))
22
+ return linkResult(targetRoot, entry.name, sourceRoot, canonicalSourceRoot, entry.path);
23
+ if (linking.has(entry.name))
24
+ throw new Error(`Circular skill prerequisite: ${entry.name}`);
25
+ linking.add(entry.name);
26
+ try {
27
+ for (const prerequisite of prerequisitesFor(manifest, entry)) {
28
+ await linkManifestSkill(sourceRoot, canonicalSourceRoot, targetRoot, manifest, prerequisite, linked, linking);
29
+ }
30
+ const result = await linkResult(targetRoot, entry.name, sourceRoot, canonicalSourceRoot, entry.path);
31
+ linked.add(entry.name);
32
+ return result;
33
+ }
34
+ finally {
35
+ linking.delete(entry.name);
36
+ }
37
+ }
38
+ async function linkResult(targetRoot, name, sourceRoot, canonicalSourceRoot, skillPath) {
39
+ const source = await resolveSkillSource(sourceRoot, canonicalSourceRoot, skillPath);
40
+ const path = assertContained(targetRoot.path, name);
41
+ return { created: await linkDirectoryEntry(source, targetRoot, name), path, source };
42
+ }
43
+ function prerequisitesFor(manifest, entry) {
44
+ const entriesByName = new Map(manifest.skills.map((candidate) => [candidate.name, candidate]));
45
+ return (entry.prerequisites ?? []).map((name) => entriesByName.get(name));
46
+ }
47
+ async function resolveSkillSource(sourceRoot, canonicalSourceRoot, skillPath) {
48
+ const candidate = assertContained(sourceRoot, skillPath);
49
+ let skill;
50
+ try {
51
+ skill = await realpath(candidate);
52
+ }
53
+ catch {
54
+ /* v8 ignore next 2 -- only an attacker replacing a validated source can reach this. */
55
+ throw new Error(`Invalid skill source: ${skillPath}`);
56
+ }
57
+ /* v8 ignore next 2 -- only an attacker replacing a validated source can reach this. */
58
+ if (!isContained(canonicalSourceRoot, skill) || !(await lstat(skill)).isFile())
59
+ throw new Error(`Skill source escapes root: ${skillPath}`);
60
+ // Validate through the canonical path, but retain the caller's logical root in
61
+ // the link target. Package managers replace their physical store paths during
62
+ // upgrades while the logical installed path remains stable.
63
+ return dirname(candidate);
64
+ }
@@ -0,0 +1,15 @@
1
+ export type SkillManifestEntry = {
2
+ name: string;
3
+ plugin: string;
4
+ pluginVersion: string;
5
+ path: string;
6
+ prerequisites?: string[];
7
+ };
8
+ export type SkillManifest = {
9
+ version: 1;
10
+ skills: SkillManifestEntry[];
11
+ };
12
+ export declare function readSkillManifest(sourceRoot: string): Promise<SkillManifest>;
13
+ export declare function assertContained(root: string, child: string): string;
14
+ export declare function isSafeSkillName(name: string): boolean;
15
+ export declare function isContained(root: string, path: string): boolean;
@@ -0,0 +1,85 @@
1
+ import { lstat, readFile, realpath } from 'node:fs/promises';
2
+ import { basename, isAbsolute, relative, resolve, join } from 'node:path';
3
+ export async function readSkillManifest(sourceRoot) {
4
+ const root = await realpath(resolve(sourceRoot));
5
+ const parsed = JSON.parse(await readFile(join(root, 'manifest.json'), 'utf8'));
6
+ if (!isManifest(parsed))
7
+ throw new Error('Invalid skills manifest');
8
+ await validateManifestEntries(root, parsed.skills);
9
+ return parsed;
10
+ }
11
+ export function assertContained(root, child) {
12
+ if (isAbsolute(child))
13
+ throw new Error(`Skill path escapes root: ${child}`);
14
+ const path = resolve(root, child);
15
+ if (!isContained(root, path))
16
+ throw new Error(`Skill path escapes root: ${child}`);
17
+ return path;
18
+ }
19
+ export function isSafeSkillName(name) {
20
+ return /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name);
21
+ }
22
+ export function isContained(root, path) {
23
+ const pathRelative = relative(root, path);
24
+ return !pathRelative.startsWith('..') && !isAbsolute(pathRelative);
25
+ }
26
+ async function validateManifestEntries(root, entries) {
27
+ const names = new Set();
28
+ const lexicalPaths = new Set();
29
+ const canonicalPaths = new Set();
30
+ for (const entry of entries) {
31
+ if (!isSafeSkillName(entry.name))
32
+ throw new Error(`Invalid skill name: ${entry.name}`);
33
+ if (names.has(entry.name))
34
+ throw new Error(`Duplicate skill name: ${entry.name}`);
35
+ names.add(entry.name);
36
+ const prerequisites = entry.prerequisites ?? [];
37
+ if (new Set(prerequisites).size !== prerequisites.length)
38
+ throw new Error(`Duplicate skill prerequisite: ${entry.name}`);
39
+ for (const prerequisite of prerequisites) {
40
+ if (!isSafeSkillName(prerequisite))
41
+ throw new Error(`Invalid skill prerequisite: ${prerequisite}`);
42
+ }
43
+ if (basename(entry.path) !== 'SKILL.md')
44
+ throw new Error(`Invalid skill source: ${entry.path}`);
45
+ const path = assertContained(root, entry.path);
46
+ if (lexicalPaths.has(path))
47
+ throw new Error(`Duplicate skill path: ${entry.path}`);
48
+ lexicalPaths.add(path);
49
+ let canonicalPath;
50
+ try {
51
+ canonicalPath = await realpath(path);
52
+ }
53
+ catch {
54
+ throw new Error(`Invalid skill source: ${entry.path}`);
55
+ }
56
+ if (!isContained(root, canonicalPath))
57
+ throw new Error(`Skill source escapes root: ${entry.path}`);
58
+ if (!(await lstat(canonicalPath)).isFile())
59
+ throw new Error(`Invalid skill source: ${entry.path}`);
60
+ if (canonicalPaths.has(canonicalPath))
61
+ throw new Error(`Duplicate skill path: ${entry.path}`);
62
+ canonicalPaths.add(canonicalPath);
63
+ }
64
+ for (const entry of entries) {
65
+ for (const prerequisite of entry.prerequisites ?? []) {
66
+ if (!names.has(prerequisite))
67
+ throw new Error(`Missing prerequisite skill: ${prerequisite}`);
68
+ }
69
+ }
70
+ }
71
+ function isManifest(value) {
72
+ if (value === null || typeof value !== 'object')
73
+ return false;
74
+ const manifest = value;
75
+ return manifest.version === 1 && Array.isArray(manifest.skills) && manifest.skills.every(isEntry);
76
+ }
77
+ function isEntry(value) {
78
+ if (value === null || typeof value !== 'object')
79
+ return false;
80
+ const entry = value;
81
+ return ([entry.name, entry.plugin, entry.pluginVersion, entry.path].every((field) => typeof field === 'string' && field.length > 0) &&
82
+ (entry.prerequisites === undefined ||
83
+ (Array.isArray(entry.prerequisites) &&
84
+ entry.prerequisites.every((name) => typeof name === 'string'))));
85
+ }
@@ -0,0 +1,10 @@
1
+ export type TargetDirectory = {
2
+ path: string;
3
+ dev: bigint;
4
+ ino: bigint;
5
+ };
6
+ export declare function resolveTargetDirectory(targetRoot: string, beforeRevalidate?: () => Promise<void>): Promise<TargetDirectory>;
7
+ export declare function linkDirectoryEntry(source: string, target: TargetDirectory, name: string, beforeWorker?: () => Promise<void>, worker?: DirectoryLinkWorker, afterWorker?: () => Promise<void>): Promise<boolean>;
8
+ type DirectoryLinkWorker = (source: string, target: TargetDirectory, name: string) => Promise<string>;
9
+ export declare function snapshotTargetDirectory(path: string): Promise<TargetDirectory>;
10
+ export {};
@@ -0,0 +1,126 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { lstat } from 'node:fs/promises';
3
+ import { parse, relative, resolve, sep, join } from 'node:path';
4
+ import { promisify } from 'node:util';
5
+ const execFileAsync = promisify(execFile);
6
+ export async function resolveTargetDirectory(targetRoot, beforeRevalidate) {
7
+ const path = resolve(targetRoot);
8
+ const ancestors = await snapshotTargetAncestors(path);
9
+ await beforeRevalidate?.();
10
+ await assertTargetAncestorsUnchanged(ancestors);
11
+ return ancestors.at(-1);
12
+ }
13
+ export async function linkDirectoryEntry(source, target, name, beforeWorker, worker = runDirectoryLinkWorker, afterWorker) {
14
+ await beforeWorker?.();
15
+ const stdout = await worker(source, target, name);
16
+ await afterWorker?.();
17
+ await assertTargetUnchanged(target);
18
+ if (stdout === 'created')
19
+ return true;
20
+ if (stdout === 'existing')
21
+ return false;
22
+ throw new Error('Skill link worker returned an invalid result');
23
+ }
24
+ async function runDirectoryLinkWorker(source, target, name) {
25
+ try {
26
+ const { stdout } = await execFileAsync(process.execPath, [
27
+ '--input-type=module',
28
+ '--eval',
29
+ LINK_WORKER,
30
+ source,
31
+ name,
32
+ String(target.dev),
33
+ String(target.ino),
34
+ ], { cwd: target.path, windowsHide: true });
35
+ return stdout;
36
+ }
37
+ catch (error) {
38
+ if (error.stderr?.includes('Target root changed during skill linking'))
39
+ throw new Error('Target root changed during skill linking');
40
+ throw error;
41
+ }
42
+ }
43
+ export async function snapshotTargetDirectory(path) {
44
+ const stat = await lstat(path, { bigint: true });
45
+ if (stat.isSymbolicLink())
46
+ throw new Error(`Target root contains symlink: ${path}`);
47
+ if (!stat.isDirectory())
48
+ throw new Error(`Invalid target root: ${path}`);
49
+ return { path, dev: stat.dev, ino: stat.ino };
50
+ }
51
+ async function assertTargetUnchanged(target) {
52
+ try {
53
+ const current = await snapshotTargetDirectory(target.path);
54
+ if (current.dev === target.dev && current.ino === target.ino)
55
+ return;
56
+ }
57
+ catch { }
58
+ throw new Error('Target root changed during skill linking');
59
+ }
60
+ async function snapshotTargetAncestors(path) {
61
+ const parsed = parse(path);
62
+ let ancestor = parsed.root;
63
+ const ancestors = [];
64
+ for (const component of relative(parsed.root, path).split(sep)) {
65
+ ancestor = join(ancestor, component);
66
+ try {
67
+ ancestors.push(await snapshotTargetDirectory(ancestor));
68
+ }
69
+ catch (error) {
70
+ if (error.code === 'ENOENT')
71
+ throw new Error(`Target root must exist: ${path}`);
72
+ throw error;
73
+ }
74
+ }
75
+ return ancestors;
76
+ }
77
+ async function assertTargetAncestorsUnchanged(ancestors) {
78
+ try {
79
+ for (const target of ancestors) {
80
+ const current = await snapshotTargetDirectory(target.path);
81
+ if (current.dev !== target.dev || current.ino !== target.ino)
82
+ throw new Error('Target root changed while resolving');
83
+ }
84
+ }
85
+ catch {
86
+ throw new Error('Target root changed while resolving');
87
+ }
88
+ }
89
+ const LINK_WORKER = String.raw `
90
+ import { lstat, readlink, symlink } from 'node:fs/promises'
91
+
92
+ const [source, name, dev, ino] = process.argv.slice(1)
93
+ const directory = await lstat('.', { bigint: true })
94
+ if (!directory.isDirectory() || directory.isSymbolicLink() || directory.dev !== BigInt(dev) || directory.ino !== BigInt(ino))
95
+ throw new Error('Target root changed during skill linking')
96
+ async function assertExistingMatchesSource() {
97
+ const destination = await lstat(name)
98
+ if (!destination.isSymbolicLink() || (await readlink(name)) !== source)
99
+ throw new Error('Destination already exists: ' + name)
100
+ }
101
+
102
+ let created = false
103
+ try {
104
+ await assertExistingMatchesSource()
105
+ } catch (error) {
106
+ if (error?.code !== 'ENOENT') throw error
107
+ try {
108
+ await symlink(source, name, 'dir')
109
+ created = true
110
+ } catch (error) {
111
+ if (error?.code === 'EEXIST') {
112
+ await assertExistingMatchesSource()
113
+ } else {
114
+ if (process.platform !== 'win32' || !['EACCES', 'EPERM'].includes(error?.code)) throw error
115
+ try {
116
+ await symlink(source, name, 'junction')
117
+ created = true
118
+ } catch (error) {
119
+ if (error?.code !== 'EEXIST') throw error
120
+ await assertExistingMatchesSource()
121
+ }
122
+ }
123
+ }
124
+ }
125
+ process.stdout.write(created ? 'created' : 'existing')
126
+ `;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vouchington-tooling",
3
- "version": "0.1.9",
3
+ "version": "0.2.0",
4
4
  "description": "Vouchington CLI and extractable tooling libraries.",
5
5
  "homepage": "https://github.com/vouchington/vouchington-tooling/tree/main/packages/vouchington-tooling#readme",
6
6
  "bugs": {
@@ -31,6 +31,11 @@
31
31
  "import": "./dist/index.mjs",
32
32
  "default": "./dist/index.mjs"
33
33
  },
34
+ "./skill-discovery": {
35
+ "types": "./dist/skill-discovery/index.d.mts",
36
+ "import": "./dist/skill-discovery/index.mjs",
37
+ "default": "./dist/skill-discovery/index.mjs"
38
+ },
34
39
  "./runner-port-policy": {
35
40
  "types": "./dist/runner-port-policy/index.d.mts",
36
41
  "import": "./dist/runner-port-policy/index.mjs",
package/scripts/build.mjs CHANGED
@@ -1,7 +1,10 @@
1
1
  import { execFileSync } from 'node:child_process'
2
2
  import { chmod, copyFile, cp, mkdir, rm } from 'node:fs/promises'
3
+ import { join } from 'node:path'
3
4
  import { fileURLToPath } from 'node:url'
4
5
 
6
+ import skillManifest from '../skill-manifest.json' with { type: 'json' }
7
+
5
8
  const packageRoot = new URL('..', import.meta.url)
6
9
  const dist = new URL('../dist', import.meta.url)
7
10
 
@@ -16,9 +19,18 @@ await copyFile(
16
19
  new URL('../src/runner-port-policy/runner-port-policy.json', import.meta.url),
17
20
  new URL('../dist/runner-port-policy/runner-port-policy.json', import.meta.url),
18
21
  )
19
- await cp(
20
- new URL('../../../plugins/vouchington-workflow/skills', import.meta.url),
21
- new URL('../skills', import.meta.url),
22
- { recursive: true },
22
+ const pluginRoot = fileURLToPath(new URL('../../../plugins', import.meta.url))
23
+ const skillsRoot = fileURLToPath(new URL('../skills/', import.meta.url))
24
+ const seenSkills = new Set()
25
+ for (const skill of skillManifest.skills) {
26
+ if (seenSkills.has(skill.name)) throw new Error(`Duplicate skill: ${skill.name}`)
27
+ seenSkills.add(skill.name)
28
+ await cp(join(pluginRoot, skill.plugin, 'skills', skill.name), join(skillsRoot, skill.name), {
29
+ recursive: true,
30
+ })
31
+ }
32
+ await copyFile(
33
+ new URL('../skill-manifest.json', import.meta.url),
34
+ new URL('../skills/manifest.json', import.meta.url),
23
35
  )
24
36
  await chmod(new URL('../dist/cli/index.mjs', import.meta.url), 0o755)
@@ -23,5 +23,10 @@ branching, review, and release policy.
23
23
  6. Review the diff for accidental files, secrets, generated output, broken documentation links,
24
24
  and assumptions that belong in local instructions instead.
25
25
 
26
+ For portable implementation and review checks, read [implementation](references/implementation.md),
27
+ [review](references/review.md), and [evidence sweep](references/evidence-sweep.md). The older
28
+ [implementation and review](references/implementation-and-review.md) remains a compact overview.
29
+ Local instructions remain authoritative for commands, commits, review systems, and release policy.
30
+
26
31
  Do not invent a default branch, runner class, documentation root, review system, merge policy, or
27
32
  command catalog. A consumer wrapper or local instruction file owns those choices.
@@ -0,0 +1,8 @@
1
+ # Evidence sweep
2
+
3
+ Map every acceptance criterion to a focused test, inspection, or manual verification with a concrete
4
+ result. Run the required broader checks after focused validation. Treat skipped validation as a
5
+ finding: state the blocker, affected surface, and remaining risk.
6
+
7
+ Before handoff, compare the final diff to the accepted decision ledger and verify documentation,
8
+ generated output, and public contracts agree with the implementation.
@@ -0,0 +1,9 @@
1
+ # Implementation and review
2
+
3
+ Before editing, map callers, tests, configuration, and documentation that own the behavior. Keep the
4
+ change within the accepted boundary; record a new dependency or product decision before widening it.
5
+
6
+ After focused validation, inspect the complete diff for accidental generated files, secret exposure,
7
+ dead paths, and documentation drift. Validate the public interface and failure paths, not only the
8
+ happy path. Before a reviewable commit, run the repository-required checks and state any skipped
9
+ checks with their concrete blocker.
@@ -0,0 +1,8 @@
1
+ # Implementation
2
+
3
+ Keep an accepted-decision ledger for interfaces, data boundaries, validation, and ownership choices
4
+ that could otherwise be lost during coding. Start from a failing behavioral test when practical and
5
+ complete the real path rather than adding placeholders or compatibility shims without a requirement.
6
+
7
+ When behavior changes, update the public contract, documentation, fixtures, and generated artifacts
8
+ that describe it. Keep local commands, branch policy, and release mechanics in the consumer wrapper.
@@ -0,0 +1,7 @@
1
+ # Review
2
+
3
+ Review the complete diff, not only changed implementation lines. Check premise and scope, public
4
+ contract compatibility, failure paths, authorization, untrusted input, secrets, and security
5
+ boundaries. Remove dead paths and stale documentation rather than labeling them as legacy.
6
+
7
+ Record unresolved risk as an explicit accepted decision or follow-up, never as an unnoticed gap.
@@ -0,0 +1,15 @@
1
+ ---
2
+ name: backend-vitest-test-authoring
3
+ description: Apply backend Vitest patterns for service, provider, persistence, and queue boundaries.
4
+ ---
5
+
6
+ # Backend Vitest test authoring
7
+
8
+ Apply [vitest-test-authoring](../vitest-test-authoring/SKILL.md) first. Exercise real integration
9
+ boundaries only when their lifecycle is controlled by the test environment; otherwise mock the
10
+ network or provider edge. Randomize fixture identities where shared stores can collide, assert
11
+ authorization and retry behavior at boundaries, and clean up resources deterministically.
12
+
13
+ Consumer wrappers own database setup, queue providers, test projects, and rate-limit policy.
14
+
15
+ Read [integration boundaries](references/integration-boundaries.md) for fixture and collision safety.
@@ -0,0 +1,10 @@
1
+ # Integration boundaries
2
+
3
+ Use real persistence, queue, and internal-service boundaries when the test environment controls
4
+ their lifecycle. Mock only external provider edges. Create collision-safe fixture identities and
5
+ register event listeners before the mutation that produces them. Cleanup must be ownership-scoped;
6
+ never broadly reset shared state used by concurrent tests.
7
+
8
+ Assert persisted state, emitted work, or externally visible results at the boundary. A mock-only
9
+ test is insufficient when import wiring, serialization, transaction behavior, or retries are part
10
+ of the contract.
@@ -0,0 +1,16 @@
1
+ ---
2
+ name: dotnet-test-authoring
3
+ description: Add maintainable .NET tests with appropriate unit, app, and integration boundaries.
4
+ ---
5
+
6
+ # .NET test authoring
7
+
8
+ Place tests at the narrowest layer that proves the behavior. Batch native or external selection
9
+ where the project requires it, isolate process and filesystem state, and assert serialized contracts
10
+ instead of private implementation details. Keep test helpers typed and reusable only when they remove
11
+ repeated setup without hiding important expectations.
12
+
13
+ Consumer wrappers own solution layout, test frameworks, native dependencies, and coverage policy.
14
+
15
+ Keep portable library tests separate from rendered application tests; batch compatible native targets
16
+ in one selection pass so a shared build validates the same source set.
@@ -0,0 +1,158 @@
1
+ {
2
+ "version": 1,
3
+ "skills": [
4
+ {
5
+ "name": "agent-workflow",
6
+ "plugin": "vouchington-workflow",
7
+ "pluginVersion": "0.3.0",
8
+ "path": "agent-workflow/SKILL.md"
9
+ },
10
+ {
11
+ "name": "backend-vitest-test-authoring",
12
+ "plugin": "vouchington-testing",
13
+ "pluginVersion": "0.1.0",
14
+ "path": "backend-vitest-test-authoring/SKILL.md",
15
+ "prerequisites": ["vitest-test-authoring"]
16
+ },
17
+ {
18
+ "name": "blackboard",
19
+ "plugin": "vouchington-workflow",
20
+ "pluginVersion": "0.3.0",
21
+ "path": "blackboard/SKILL.md"
22
+ },
23
+ {
24
+ "name": "dotnet-test-authoring",
25
+ "plugin": "vouchington-testing",
26
+ "pluginVersion": "0.1.0",
27
+ "path": "dotnet-test-authoring/SKILL.md"
28
+ },
29
+ {
30
+ "name": "git-commit-checklist",
31
+ "plugin": "vouchington-workflow",
32
+ "pluginVersion": "0.3.0",
33
+ "path": "git-commit-checklist/SKILL.md"
34
+ },
35
+ {
36
+ "name": "github-actions-checklist",
37
+ "plugin": "vouchington-workflow",
38
+ "pluginVersion": "0.3.0",
39
+ "path": "github-actions-checklist/SKILL.md"
40
+ },
41
+ {
42
+ "name": "github-issue",
43
+ "plugin": "vouchington-workflow",
44
+ "pluginVersion": "0.3.0",
45
+ "path": "github-issue/SKILL.md"
46
+ },
47
+ {
48
+ "name": "nextjs-vitest-test-authoring",
49
+ "plugin": "vouchington-testing",
50
+ "pluginVersion": "0.1.0",
51
+ "path": "nextjs-vitest-test-authoring/SKILL.md",
52
+ "prerequisites": ["vitest-test-authoring"]
53
+ },
54
+ {
55
+ "name": "organize-github-issues",
56
+ "plugin": "vouchington-workflow",
57
+ "pluginVersion": "0.3.0",
58
+ "path": "organize-github-issues/SKILL.md"
59
+ },
60
+ {
61
+ "name": "package-json-checklist",
62
+ "plugin": "vouchington-workflow",
63
+ "pluginVersion": "0.3.0",
64
+ "path": "package-json-checklist/SKILL.md"
65
+ },
66
+ {
67
+ "name": "planning",
68
+ "plugin": "vouchington-workflow",
69
+ "pluginVersion": "0.3.0",
70
+ "path": "planning/SKILL.md"
71
+ },
72
+ {
73
+ "name": "playwright-authoring",
74
+ "plugin": "vouchington-testing",
75
+ "pluginVersion": "0.1.0",
76
+ "path": "playwright-authoring/SKILL.md"
77
+ },
78
+ {
79
+ "name": "postgres-node-performance-tuning",
80
+ "plugin": "vouchington-database",
81
+ "pluginVersion": "0.1.0",
82
+ "path": "postgres-node-performance-tuning/SKILL.md"
83
+ },
84
+ {
85
+ "name": "postgres-partitioning-uuid-v7",
86
+ "plugin": "vouchington-database",
87
+ "pluginVersion": "0.1.0",
88
+ "path": "postgres-partitioning-uuid-v7/SKILL.md"
89
+ },
90
+ {
91
+ "name": "pr-description",
92
+ "plugin": "vouchington-workflow",
93
+ "pluginVersion": "0.3.0",
94
+ "path": "pr-description/SKILL.md"
95
+ },
96
+ {
97
+ "name": "retrospective",
98
+ "plugin": "vouchington-workflow",
99
+ "pluginVersion": "0.3.0",
100
+ "path": "retrospective/SKILL.md"
101
+ },
102
+ {
103
+ "name": "retrospective-distill",
104
+ "plugin": "vouchington-workflow",
105
+ "pluginVersion": "0.3.0",
106
+ "path": "retrospective-distill/SKILL.md"
107
+ },
108
+ {
109
+ "name": "review-ci-logs",
110
+ "plugin": "vouchington-workflow",
111
+ "pluginVersion": "0.3.0",
112
+ "path": "review-ci-logs/SKILL.md"
113
+ },
114
+ {
115
+ "name": "review-github-issue-taxonomy",
116
+ "plugin": "vouchington-workflow",
117
+ "pluginVersion": "0.3.0",
118
+ "path": "review-github-issue-taxonomy/SKILL.md"
119
+ },
120
+ {
121
+ "name": "revisit-followups",
122
+ "plugin": "vouchington-workflow",
123
+ "pluginVersion": "0.3.0",
124
+ "path": "revisit-followups/SKILL.md"
125
+ },
126
+ {
127
+ "name": "static-analysis-checklist",
128
+ "plugin": "vouchington-workflow",
129
+ "pluginVersion": "0.3.0",
130
+ "path": "static-analysis-checklist/SKILL.md"
131
+ },
132
+ {
133
+ "name": "storybook-authoring",
134
+ "plugin": "vouchington-testing",
135
+ "pluginVersion": "0.1.0",
136
+ "path": "storybook-authoring/SKILL.md"
137
+ },
138
+ {
139
+ "name": "swift-test-authoring",
140
+ "plugin": "vouchington-testing",
141
+ "pluginVersion": "0.1.0",
142
+ "path": "swift-test-authoring/SKILL.md"
143
+ },
144
+ {
145
+ "name": "test-authoring",
146
+ "plugin": "vouchington-testing",
147
+ "pluginVersion": "0.1.0",
148
+ "path": "test-authoring/SKILL.md"
149
+ },
150
+ {
151
+ "name": "vitest-test-authoring",
152
+ "plugin": "vouchington-testing",
153
+ "pluginVersion": "0.1.0",
154
+ "path": "vitest-test-authoring/SKILL.md",
155
+ "prerequisites": ["test-authoring"]
156
+ }
157
+ ]
158
+ }
@@ -0,0 +1,15 @@
1
+ ---
2
+ name: nextjs-vitest-test-authoring
3
+ description: Apply Vitest patterns for Next.js components, routes, and server-side modules.
4
+ ---
5
+
6
+ # Next.js Vitest test authoring
7
+
8
+ Apply [vitest-test-authoring](../vitest-test-authoring/SKILL.md) first. Mock framework navigation,
9
+ headers, and server-only boundaries at the framework edge, then test component behavior through
10
+ rendered output and user-visible state. Keep API-response fixtures representative and avoid testing
11
+ framework internals. Use browser tests for behavior that cannot be represented in the test runtime.
12
+
13
+ Consumer wrappers own framework mock helpers, render libraries, and route fixture conventions.
14
+
15
+ Read [framework boundaries](references/framework-boundaries.md) for module shape and server-only cases.
@@ -0,0 +1,9 @@
1
+ # Framework boundaries
2
+
3
+ Mock framework navigation, headers, cookies, and server-only APIs at the framework edge, not inside
4
+ the component or route under test. Preserve typed module shape and use the consumer's shared mocks
5
+ when they exist. Build API responses with typed factories rather than inline partial objects.
6
+
7
+ Use direct rendering for component behavior. If request scope, hydration, browser APIs, or server
8
+ module resolution cannot be represented faithfully, move the assertion to an integration or browser
9
+ test instead of widening a unit-test mock.
@@ -19,5 +19,8 @@ Use before implementation work that needs a durable plan. Repository-local `AGEN
19
19
  5. Validate and save the plan using the repository's required issue or document workflow before
20
20
  implementation when local policy requires one.
21
21
 
22
+ For cross-cutting changes, read [impact discovery](references/impact-discovery.md) before selecting
23
+ tests or concluding that a surface has no dependents.
24
+
22
25
  Do not invent a plan template, default repository, issue taxonomy, dependency graph tool, or
23
26
  approval workflow. A consumer wrapper owns those choices.
@@ -0,0 +1,9 @@
1
+ # Impact discovery
2
+
3
+ Start from the changed surface and trace imports, callers, configuration, generated artifacts, and
4
+ tests. Use the repository's graph or search tools where available; otherwise record the evidence
5
+ used and the uncertainty that remains. Select focused tests from real dependents, then include
6
+ broader validation when a shared contract, package boundary, or generated artifact changes.
7
+
8
+ Do not treat a tool's incomplete graph as proof that no dependent exists. Escalate uncertain
9
+ high-risk boundaries for independent review or a broader test selection.
@@ -0,0 +1,15 @@
1
+ ---
2
+ name: playwright-authoring
3
+ description: Author reliable Playwright browser tests, fixtures, selectors, and user flows.
4
+ ---
5
+
6
+ # Playwright authoring
7
+
8
+ Use stable, accessible locators and assert user-observable outcomes. Establish data, authentication,
9
+ and server state through supported fixtures or APIs; do not rely on test ordering or arbitrary waits.
10
+ Keep each scenario independently repeatable, use auto-waiting assertions, and capture diagnostics on
11
+ failure. Prefer browser coverage for real browser interactions rather than duplicating unit tests.
12
+
13
+ Consumer wrappers own environments, credentials, personas, fixtures, and suite commands.
14
+
15
+ Read [browser reliability](references/browser-reliability.md) for locator, waiting, state, and network rules.
@@ -0,0 +1,9 @@
1
+ # Browser reliability
2
+
3
+ Use the browser only for browser-owned behavior. Prefer stable accessibility or test-id locators,
4
+ scope repeated content, and assert visible outcomes. Never use arbitrary sleeps; wait for a precise
5
+ URL, request, response, DOM state, or user-visible completion signal.
6
+
7
+ Seed deterministic data and establish authentication through supported fixtures. Mock network only
8
+ for third-party behavior, fault injection, streaming, or conditions impossible to seed. Register
9
+ response waits before triggering mutations, and make each scenario independent of prior state.
@@ -0,0 +1,15 @@
1
+ ---
2
+ name: postgres-node-performance-tuning
3
+ description: Diagnose and improve PostgreSQL performance in Node.js applications with large data volumes.
4
+ ---
5
+
6
+ # PostgreSQL and Node.js performance
7
+
8
+ Measure query plans and workload shape before changing code. Select only required columns, bound
9
+ result sets, paginate or stream large reads, batch writes within explicit transaction limits, and
10
+ keep connection-pool usage bounded. Validate query changes with representative cardinality and
11
+ watch latency, memory, lock time, and connection pressure together.
12
+
13
+ Consumer wrappers own schema ownership, operational thresholds, pooling configuration, and rollout.
14
+
15
+ Read [performance patterns](references/performance-patterns.md) before changing high-volume paths.
@@ -0,0 +1,14 @@
1
+ # Performance patterns
2
+
3
+ Use pools and release checked-out clients in `finally`, especially for cursors, streams, and COPY.
4
+ Route ordinary reads to a replica when its lag is acceptable; route read-after-write, locking, and
5
+ transaction-consistent reads to the writer. Do not hold a client across unrelated application work.
6
+
7
+ For large reads, use keyset pagination, cursors, or streams with cancellation and bounded batches.
8
+ For writes, prefer set-based batches such as UNNEST or COPY when they preserve validation and error
9
+ handling. Measure query plans with representative cardinality before changing an index or query.
10
+
11
+ Check query predicates, joins, ordering, and selected columns against index shape. Use EXPLAIN
12
+ evidence to confirm planner behavior. Add extended statistics only when observed estimates show a
13
+ correlation problem; verify the statistics are collected and used. Partitioning can reduce scanned
14
+ data, but it does not replace suitable local indexes or predicates that permit pruning.
@@ -0,0 +1,16 @@
1
+ ---
2
+ name: postgres-partitioning-uuid-v7
3
+ description: Design PostgreSQL partitions and indexes that use time-ordered UUIDv7 identifiers efficiently.
4
+ ---
5
+
6
+ # PostgreSQL partitioning with UUIDv7
7
+
8
+ Partition only after confirming lifecycle, retention, and query predicates benefit from it. Treat a
9
+ UUIDv7 as time ordered but not as a replacement for explicit business timestamps where semantics
10
+ matter. Align partition keys, primary keys, indexes, constraints, and query predicates so partition
11
+ pruning is observable. Plan creation, retention, migration, and verification as one deployable
12
+ lifecycle, including rollback and independent-reader compatibility.
13
+
14
+ Consumer wrappers own partition intervals, migration tooling, retention policy, and deploy sequencing.
15
+
16
+ Read [partition lifecycle](references/partition-lifecycle.md) before a schema or retention migration.
@@ -0,0 +1,14 @@
1
+ # Partition lifecycle
2
+
3
+ UUIDv7 ordering supports range bounds and index-friendly time windows. Generate bounds from time
4
+ instead of extracting timestamps in predicates; use a generated timestamp only when application
5
+ semantics require one. Partition on the range key that queries and retention actually constrain.
6
+
7
+ Create future ranges before writes need them and maintain a default partition only with an explicit
8
+ attachment plan. Before attaching a populated range, prove the default partition excludes that range
9
+ or move conflicting rows; otherwise attachment can scan and lock the default partition.
10
+
11
+ Every primary or unique constraint on a partitioned table must include its partition key. Verify
12
+ pruning with predicates on that key, including joins whose other side needs an equivalent range
13
+ condition. UUIDv7 values are time ordered, not a promise that independently generated identifiers
14
+ have a strict ordering relationship.
@@ -0,0 +1,15 @@
1
+ ---
2
+ name: storybook-authoring
3
+ description: Add maintainable Storybook stories and browser-mode component coverage.
4
+ ---
5
+
6
+ # Storybook authoring
7
+
8
+ Create stories that show meaningful supported states with realistic args and fixtures. Keep story
9
+ data local and deterministic, expose important visual or interaction variants, and add browser-mode
10
+ coverage where it catches behavior unavailable to unit tests. Do not use stories as a substitute for
11
+ end-to-end setup or production data handling.
12
+
13
+ Consumer wrappers own Storybook configuration, exclusions, visual baselines, and commands.
14
+
15
+ Read [component coverage](references/component-coverage.md) for direct stories and browser isolation.
@@ -0,0 +1,9 @@
1
+ # Component coverage
2
+
3
+ Keep a direct story for each supported reusable component state. When a component needs server-only
4
+ modules, request context, or browser-hostile imports, isolate the presentational surface or alias the
5
+ boundary to a deterministic fixture. Test interactive story behavior in browser mode when it owns
6
+ the component contract.
7
+
8
+ Allow story discovery to follow the consumer's module graph and glob configuration. Do not hand
9
+ maintain duplicate registration lists or make module-top-level browser assumptions.
@@ -0,0 +1,15 @@
1
+ ---
2
+ name: swift-test-authoring
3
+ description: Add Swift and SwiftUI tests with deterministic state, networking, and view inspection.
4
+ ---
5
+
6
+ # Swift test authoring
7
+
8
+ Test public behavior with deterministic inputs and injected dependencies. For SwiftUI, inspect or
9
+ interact through the project's supported test approach; for networking, use a protocol-level test
10
+ double and keep request/response synchronization explicit. Avoid sleeps and global state, and keep
11
+ fixtures small enough to make failures readable.
12
+
13
+ Consumer wrappers own test targets, coverage thresholds, view-inspection libraries, and fixture APIs.
14
+
15
+ Read [network test doubles](references/network-test-doubles.md) for cancellation and shared-state safety.
@@ -0,0 +1,9 @@
1
+ # Network test doubles
2
+
3
+ Delayed URLProtocol callbacks must honor `stopLoading()`: guard delivery with synchronized stopped
4
+ state so cancellation cannot call a released client. Keep shared response and captured-request state
5
+ private behind one synchronization boundary, reset related fields atomically, and expose coherent
6
+ snapshots for assertions.
7
+
8
+ Avoid sleeping to coordinate tests. Inject scheduling or await explicit completion so networking,
9
+ view state, and cancellation remain deterministic under parallel execution.
@@ -0,0 +1,17 @@
1
+ ---
2
+ name: test-authoring
3
+ description: Write focused, maintainable tests and test fixtures for application or library behavior.
4
+ ---
5
+
6
+ # Test authoring
7
+
8
+ Use the repository's test conventions and runner skill. Test observable behavior, boundary failures,
9
+ and regressions rather than implementation details. Build fixtures through public constructors or
10
+ documented helpers; keep data minimal, explicit, and representative. Add a focused failing test
11
+ before a behavior change when practical, then run the narrowest relevant test and required checks.
12
+
13
+ Do not invent project test commands, coverage targets, mock libraries, or integration environments.
14
+ A consumer wrapper owns those choices.
15
+
16
+ Read [core practice](references/core-practice.md) for the shared boundary, completion, and evidence
17
+ rules before choosing a runner-specific approach.
@@ -0,0 +1,10 @@
1
+ # Core test practice
2
+
3
+ Choose the lowest realistic boundary that can observe the contract. Mock external systems and
4
+ uncontrolled infrastructure; exercise internal module composition where practical. Test behavior,
5
+ failure paths, authorization, and security-relevant validation rather than private calls.
6
+
7
+ Start with a failing test when the behavior is testable. Finish only when the production path, its
8
+ public contract, documentation, and generated artifacts move together. Do not leave placeholders or
9
+ test-only production branches. For every acceptance criterion, retain evidence from a focused test,
10
+ review, or explicitly justified manual check.
@@ -0,0 +1,15 @@
1
+ ---
2
+ name: vitest-test-authoring
3
+ description: Apply Vitest-specific patterns when adding or changing Vitest tests, mocks, or fixtures.
4
+ ---
5
+
6
+ # Vitest test authoring
7
+
8
+ Apply [test-authoring](../test-authoring/SKILL.md) first. Keep tests isolated with Vitest lifecycle
9
+ hooks, restore spies and globals after each test, and prefer deterministic async assertions over
10
+ timing waits. Mock external boundaries rather than modules under test, and use typed factories when
11
+ the project supplies them. Run the selected file or project before broader validation.
12
+
13
+ Consumer wrappers own project selection, mock boundaries, fixture names, and coverage policy.
14
+
15
+ Read [mock boundaries](references/mock-boundaries.md) when adding module mocks or changing exports.
@@ -0,0 +1,9 @@
1
+ # Mock boundaries
2
+
3
+ Mock providers, transports, clocks, and environment boundaries rather than application modules.
4
+ When a module mock is necessary, preserve its runtime export shape, type the factory against the
5
+ real module, and spread actual exports unless omission is intentional. Update static factories when
6
+ the mocked module gains an export.
7
+
8
+ Prefer spies or dependency injection for a narrow seam. Restore mocks, environment, and globals
9
+ after each test so a test cannot alter another test's module graph or process state.