cyber-aced 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.
Files changed (77) hide show
  1. package/.claude-plugin/plugin.json +18 -0
  2. package/.plugin/pins.json +1 -0
  3. package/.plugin/plugin.json +18 -0
  4. package/agents/aced-case-judge.md +165 -0
  5. package/agents/aced-impl-judge.md +55 -0
  6. package/agents/aced-scenario-writer.md +76 -0
  7. package/agents/aced-spec-validator.md +66 -0
  8. package/governances/README.md +52 -0
  9. package/governances/agent-tool-output.md +86 -0
  10. package/governances/cli-resolution.md +102 -0
  11. package/governances/skill-design.md +258 -0
  12. package/governances/skill-repo-structure.md +173 -0
  13. package/package.json +44 -0
  14. package/readme.md +124 -0
  15. package/skills/aced-architect-impl/README.md +9 -0
  16. package/skills/aced-architect-impl/SKILL.md +73 -0
  17. package/skills/aced-builder-impl/README.md +14 -0
  18. package/skills/aced-builder-impl/SKILL.md +46 -0
  19. package/skills/aced-builder-spec/README.md +15 -0
  20. package/skills/aced-builder-spec/SKILL.md +130 -0
  21. package/skills/aced-fit/README.md +21 -0
  22. package/skills/aced-fit/SKILL.md +105 -0
  23. package/skills/aced-impl-producer/SKILL.md +75 -0
  24. package/skills/add-scenario/README.md +25 -0
  25. package/skills/add-scenario/SKILL.md +75 -0
  26. package/skills/bench/README.md +64 -0
  27. package/skills/bench/SKILL.md +199 -0
  28. package/skills/bench/scripts/bench-claude-code.mts +187 -0
  29. package/skills/bench/scripts/bench-compare.mts +369 -0
  30. package/skills/bench/scripts/bench-record.mts +133 -0
  31. package/skills/bench/scripts/bench.mts +930 -0
  32. package/skills/check-freshness/README.md +57 -0
  33. package/skills/check-freshness/SKILL.md +76 -0
  34. package/skills/check-freshness/scripts/check-freshness.mts +399 -0
  35. package/skills/compare/README.md +25 -0
  36. package/skills/compare/SKILL.md +87 -0
  37. package/skills/contribute-skill/README.md +25 -0
  38. package/skills/contribute-skill/SKILL.md +241 -0
  39. package/skills/contribute-skill/scripts/source.mts +188 -0
  40. package/skills/define-agent/README.md +40 -0
  41. package/skills/define-agent/SKILL.md +279 -0
  42. package/skills/define-command/README.md +36 -0
  43. package/skills/define-command/SKILL.md +174 -0
  44. package/skills/define-governance/SKILL.md +151 -0
  45. package/skills/define-skill/README.md +37 -0
  46. package/skills/define-skill/SKILL.md +200 -0
  47. package/skills/extract-situation/README.md +32 -0
  48. package/skills/extract-situation/SKILL.md +54 -0
  49. package/skills/extract-situation/scripts/extract-situation.mts +287 -0
  50. package/skills/improve/README.md +25 -0
  51. package/skills/improve/SKILL.md +100 -0
  52. package/skills/improve-skill/README.md +38 -0
  53. package/skills/improve-skill/SKILL.md +169 -0
  54. package/skills/improve-skill/references/check-definitions.md +215 -0
  55. package/skills/improve-skill/scripts/validate.mts +1033 -0
  56. package/skills/init-aced/README.md +5 -0
  57. package/skills/init-aced/SKILL.md +76 -0
  58. package/skills/init-aced/scripts/ensure-results-ignored.mts +117 -0
  59. package/skills/list-skills/README.md +19 -0
  60. package/skills/list-skills/SKILL.md +55 -0
  61. package/skills/list-skills/scripts/list-skills.mts +210 -0
  62. package/skills/manage/README.md +35 -0
  63. package/skills/manage/SKILL.md +65 -0
  64. package/skills/manage-model-runners/README.md +25 -0
  65. package/skills/manage-model-runners/SKILL.md +113 -0
  66. package/skills/manage-skill-dirs/README.md +17 -0
  67. package/skills/manage-skill-dirs/SKILL.md +54 -0
  68. package/skills/manage-skill-dirs/scripts/manage-skill-dirs.mts +328 -0
  69. package/skills/repair-private-skills/README.md +17 -0
  70. package/skills/repair-private-skills/SKILL.md +59 -0
  71. package/skills/repair-private-skills/scripts/repair-private-skills.mts +279 -0
  72. package/skills/report/README.md +25 -0
  73. package/skills/report/SKILL.md +74 -0
  74. package/skills/run/README.md +25 -0
  75. package/skills/run/SKILL.md +220 -0
  76. package/skills/skillify/README.md +25 -0
  77. package/skills/skillify/SKILL.md +141 -0
@@ -0,0 +1,113 @@
1
+ ---
2
+ name: manage-model-runners
3
+ description: "Partial Skill: invoke by name only — the ACED manage-model-runners engine — loaded in-session by the ACED manage gateway, not user-triggered."
4
+ user-invocable: false
5
+ metadata:
6
+ internal: true
7
+ ---
8
+
9
+ # manage-model-runners
10
+
11
+ Maintain a family of **runner agent definitions — one per model**. Each runner is a **neutral
12
+ executor** pinned to a single model so a skill-under-test can be run as a real subagent under that
13
+ model, giving ACED a real (not judge-simulated) signal for effectiveness and token/cost. Loaded
14
+ in-session by the `manage` gateway; **not user-invocable**.
15
+
16
+ Three operations, all **additive**: **add** the missing runners, **list** the family, **remove** only
17
+ the runners the user explicitly names. This engine **never auto-removes** — a model missing from a
18
+ target list is left alone (the user may run multiple harnesses, so a runner this engine did not just
19
+ create must not be culled).
20
+
21
+ Canonical files live at user-global `~/.agents/agents/model-runner-<model>.md`, with runtime symlinks
22
+ — the exact placement + `ln -sf` procedure `define-agent` uses.
23
+
24
+ ## Determine the operation
25
+
26
+ Read the request for the operation. If it is unclear, ask (add / list / remove).
27
+
28
+ ## Resolve the target model list (add)
29
+
30
+ Before writing anything, resolve which models to cover, in order:
31
+
32
+ 1. **Explicit** — models named in the request/args.
33
+ 2. **Curated config** — `.agents/aced/models.toml`, a `models = ["opus", "sonnet", ...]` list the
34
+ user maintains (nothing in the environment enumerates available models, so this is the source of
35
+ truth when present).
36
+ 3. **Proposed default** — when neither is given, propose the known model aliases the agent-definition
37
+ `model:` field accepts (`opus`, `sonnet`, `haiku`) and any newer aliases the user names.
38
+
39
+ **Propose and confirm — never guess.** Show the resolved list and confirm it before writing. A model
40
+ value is whatever the runtime's agent-definition `model:` field accepts.
41
+
42
+ ## add — create the missing runners
43
+
44
+ For each target model:
45
+
46
+ - If `~/.agents/agents/model-runner-<model>.md` **already exists**, leave it untouched (add is
47
+ **idempotent** — no duplicate, no overwrite).
48
+ - Otherwise write a **new runner def** (below) at the canonical path and create one runtime symlink
49
+ per selected runtime.
50
+
51
+ Exactly **one def per model** — model is the only axis. Do not fan out over effort (see the effort
52
+ note below).
53
+
54
+ ### The runner def (neutral executor)
55
+
56
+ Write the canonical file. The body is **identical across the family** — only `name` and `model`
57
+ differ:
58
+
59
+ ```markdown
60
+ ---
61
+ name: model-runner-<model>
62
+ description: >
63
+ Use this agent to run an ACED skill-under-test as a real subagent under the <model> model, so its
64
+ effectiveness and token/cost can be measured. Delegated runner — it executes the given task exactly
65
+ and returns the result.
66
+ tools: *
67
+ model: <model>
68
+ ---
69
+
70
+ # model-runner-<model>
71
+
72
+ You are a neutral executor pinned to the **<model>** model. Run the skill or task you are given
73
+ **exactly as specified** — invoke the named skill, follow its steps, and return its result.
74
+
75
+ ## Responsibilities
76
+
77
+ - Execute the given skill/task faithfully, adding no scope of your own.
78
+ - Return exactly what the task asks for (a result, an artifact path, or a confirmation).
79
+
80
+ ## Out of scope
81
+
82
+ - Do not reinterpret, expand, or optimize the task beyond what it states.
83
+ - Do not choose a different model — you exist to exercise **<model>** specifically.
84
+ ```
85
+
86
+ Then create the runtime symlinks (Claude Code `~/.claude/agents/<name>.md`, and Cursor / Codex if the
87
+ user selected them) with relative `ln -sf`, and verify each resolves — as in `define-agent`.
88
+
89
+ ## list — report the family
90
+
91
+ Scan `~/.agents/agents/` for `model-runner-*.md` and report each: the model, the canonical path, and
92
+ any `effort:` stamp. Show nothing else.
93
+
94
+ ## remove — only what the user names
95
+
96
+ Remove **only** the runner def(s) the user **explicitly names** (by model or path). For each:
97
+
98
+ - Show the file to be deleted and **confirm before deleting** the user-global file and its symlinks.
99
+ - **Never** remove a runner just because it is absent from a target list — this engine does no
100
+ reconcile-delete. If the user asks to "clean up", list the family and let them name which to remove.
101
+
102
+ ## Effort stamp (optional)
103
+
104
+ A plain `Agent`/`Task` spawn can override a subject's `model` but **not** its `effort` (effort lives
105
+ in the def frontmatter); a Workflow `agent()` call can pass `effort` per call. So one def per model
106
+ serves a Workflow-based runner directly. When a **plain-spawn** runner needs a fixed effort, stamp it
107
+ on request: add/update writes an `effort: <low|medium|high|xhigh|max>` field on the named model's def,
108
+ still keeping exactly one def per model. Do not create model×effort defs.
109
+
110
+ ## Report
111
+
112
+ Summarize what changed: models added (with canonical paths + symlinks), the current family (list), or
113
+ the runners removed. Point the user at the future ACED eval-run sweep that will use these runners.
@@ -0,0 +1,17 @@
1
+ # manage-skill-dirs
2
+
3
+ The concrete engine for the ACED **skill-dirs** config — the opt-in registry of extra skill-scan
4
+ locations `improve-skill`'s validate engine scans on top of its two built-in default roots. A
5
+ non-user-invocable skill, loaded in-session by the `manage` gateway, carrying a self-contained `.mts`
6
+ script that lists / CRUDs / induces / previews the patterns so users never hand-edit the config file.
7
+
8
+ - **Skill contract:** [`SKILL.md`](./SKILL.md)
9
+ - **Script:** [`scripts/manage-skill-dirs.mts`](./scripts/manage-skill-dirs.mts)
10
+ - **Tests:** [`scripts/manage-skill-dirs.test.mts`](./scripts/manage-skill-dirs.test.mts) (`node:test`)
11
+
12
+ ```bash
13
+ node scripts/manage-skill-dirs.mts --list
14
+ node scripts/manage-skill-dirs.mts --induce plugins/aced/skills
15
+ node scripts/manage-skill-dirs.mts --preview 'plugins/*/skills'
16
+ node scripts/manage-skill-dirs.mts --add 'plugins/*/skills'
17
+ ```
@@ -0,0 +1,54 @@
1
+ ---
2
+ name: manage-skill-dirs
3
+ description: "Partial Skill: invoke by name only — config-authoring/skill-dirs' curation engine for the extra skill-scan locations improve-skill's validate engine uses — loaded by the manage gateway, not triggered by users directly."
4
+ user-invocable: false
5
+ metadata:
6
+ internal: true
7
+ ---
8
+
9
+ # Manage Skill Dirs
10
+
11
+ The concrete engine for the ACED **skill-dirs** config — the opt-in registry of **extra** skill-scan
12
+ locations the validate engine scans on top of its two built-in default roots. It exists so a user
13
+ curates those locations through a clean interface instead of hand-editing the config directly.
14
+ Loaded **in-session** by the `manage` gateway; it carries a self-contained `.mts` script (the repo's
15
+ node-≥23.6 / no-deps convention).
16
+
17
+ ## The config it curates
18
+
19
+ A single key — `anchors = [ … ]` — a list of repo-relative directory patterns. `*` globs one
20
+ segment; `**` globs **zero or more** segments (any depth, including zero). There is **no capture
21
+ token**: a skill's name comes from its own directory basename, not from the pattern, so a pattern
22
+ containing `<` or `>` is malformed. Each pattern names a directory whose **immediate children** are
23
+ scanned for `SKILL.md`. The two default roots (the repo-root public and private agentskills-standard
24
+ skill directories) are **implicit and always scanned** — never listed here, and never curatable.
25
+
26
+ ## Run an operation
27
+
28
+ ```bash
29
+ node "<skill>/scripts/manage-skill-dirs.mts" [--root .] <operation>
30
+ ```
31
+
32
+ | Operation | Effect |
33
+ |---|---|
34
+ | `--list` | list the two fixed default roots (each explained) + every custom pattern, flagged fixed/custom |
35
+ | `--add <pattern>` | validate + append a custom pattern (creates the config if absent); refuses a fixed default root or a malformed pattern; adding an already-present pattern is a no-op |
36
+ | `--remove <pattern>` | drop a custom pattern; a no-op (config unchanged) when the pattern is absent |
37
+ | `--edit <old> <new>` | replace one custom pattern; refuses a malformed `<new>`; a no-op when `<old>` is absent |
38
+ | `--induce <path>` | from a sample directory that contains skill subdirectories, offer a literal-dir candidate and a `*`-generalization of a variable segment (e.g. a plugin name); refuses a path that is not a directory under the repo |
39
+ | `--preview <pattern>` | list the skills a candidate pattern would discover, **without** persisting it; a malformed pattern is refused, a zero-match pattern reports none |
40
+
41
+ **Curate with preview.** The intended flow: take the user's sample path → `--induce` it → `--preview`
42
+ each candidate to show the matched skills → confirm with the user → `--add` the chosen pattern. Never
43
+ write a pattern the user has not seen the effect of.
44
+
45
+ ## Boundaries
46
+
47
+ Writes **only** the skill-dirs config — never a skill, `spec.md`, `status`, `approval`, or a freeze;
48
+ it is operational config, not skill content (so `manage`'s write-ownership guard holds). It does
49
+ **not** scan or validate skills — that is `improve-skill`'s validate engine, which *reads* this
50
+ config. This engine validates on the **write** side so a bad pattern is never persisted.
51
+
52
+ When `node` is absent, an agent performs the same edits by hand: read the `anchors` array, apply the
53
+ CRUD, validate a pattern is repo-relative with only literal / `*` / `**` segments (never `<` or `>`),
54
+ and never write a fixed default root.
@@ -0,0 +1,328 @@
1
+ #!/usr/bin/env node
2
+ // manage-skill-dirs — config-authoring/skill-dirs' concrete curation engine. Declares and curates the
3
+ // OPT-IN extra skill-dir patterns the improve-skill validate engine scans on top of its two built-in
4
+ // default roots (skills/, .agents/skills/). It reads/writes ONLY `.agents/aced/skill-dirs.toml` —
5
+ // never a SKILL.md, spec.md, status, approval, or freeze.
6
+ //
7
+ // Operations:
8
+ // --list list the two fixed default roots (each explained) + every custom pattern
9
+ // --add <pattern> validate + append a custom pattern (creates the config if absent)
10
+ // --remove <pattern> drop a custom pattern (no-op + unchanged when absent)
11
+ // --edit <old> <new> replace one custom pattern (rejects a malformed <new>)
12
+ // --induce <path> candidate patterns for a sample dir containing skill subdirectories
13
+ // (literal + a * generalization of a variable segment such as a plugin name)
14
+ // --preview <pattern> the SKILL.md file(s) a candidate pattern would discover, without persisting
15
+ //
16
+ // A pattern is a repo-relative directory pattern; `*` globs one segment, `**` globs zero or more
17
+ // segments (any depth). There is NO capture token — a skill's name comes from its own directory
18
+ // basename, not from the pattern, so a pattern containing `<` or `>` is malformed. Fixed default
19
+ // roots are implicit and cannot be curated here.
20
+ // Pure functions are exported for node:test; running the file directly drives the CLI. No deps.
21
+
22
+ import { existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, statSync, writeFileSync } from 'node:fs'
23
+ import { dirname, join } from 'node:path'
24
+ import { pathToFileURL } from 'node:url'
25
+
26
+ const SKILL_DIRS_CONFIG = '.agents/aced/skill-dirs.toml'
27
+ const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', '.turbo', '.next', 'coverage'])
28
+
29
+ export interface SkillDirEntry {
30
+ pattern: string
31
+ kind: 'fixed' | 'custom'
32
+ explanation: string
33
+ }
34
+
35
+ // The two fixed default roots (agentskills-standard), always scanned by validate and never curated here.
36
+ export const FIXED_SKILL_DIRS: SkillDirEntry[] = [
37
+ { pattern: 'skills/', kind: 'fixed', explanation: 'the repo-root public skill directory (agentskills-standard)' },
38
+ {
39
+ pattern: '.agents/skills/',
40
+ kind: 'fixed',
41
+ explanation: 'the repo-root private skill directory (agentskills-standard)',
42
+ },
43
+ ]
44
+
45
+ // ── Config read/write (a minimal TOML: the `anchors = [ … ]` string array) ──
46
+
47
+ export function parseSkillDirsToml(text: string): string[] {
48
+ const m = /(^|\n)\s*anchors\s*=\s*\[([\s\S]*?)\]/.exec(text)
49
+ if (!m) return []
50
+ const out: string[] = []
51
+ for (const q of m[2].matchAll(/"([^"]*)"|'([^']*)'/g)) out.push((q[1] ?? q[2]).trim())
52
+ return out.filter((s) => s !== '')
53
+ }
54
+
55
+ export function readCustomSkillDirs(root: string): string[] {
56
+ const file = join(root, SKILL_DIRS_CONFIG)
57
+ if (!existsSync(file)) return []
58
+ try {
59
+ return parseSkillDirsToml(readFileSync(file, 'utf8'))
60
+ } catch {
61
+ return []
62
+ }
63
+ }
64
+
65
+ export function serializeSkillDirs(patterns: string[]): string {
66
+ if (patterns.length === 0) return 'anchors = []\n'
67
+ return `anchors = [\n${patterns.map((p) => ` "${p}",`).join('\n')}\n]\n`
68
+ }
69
+
70
+ function writeCustomSkillDirs(root: string, patterns: string[]): void {
71
+ const file = join(root, SKILL_DIRS_CONFIG)
72
+ mkdirSync(dirname(file), { recursive: true })
73
+ writeFileSync(file, serializeSkillDirs(patterns))
74
+ }
75
+
76
+ // ── Validation ──
77
+
78
+ // A pattern is well-formed iff it is repo-relative (no leading slash, no `..`) and every segment is a
79
+ // literal, `*`, or `**` (no capture token — `<` / `>` anywhere is malformed, no empty segment).
80
+ export function isValidPattern(pattern: string): boolean {
81
+ const p = pattern.trim().replace(/\\/g, '/')
82
+ if (p === '' || p.startsWith('/') || /(^|\/)\.\.(\/|$)/.test(p)) return false
83
+ const segs = p.replace(/^\/+|\/+$/g, '').split('/')
84
+ if (segs.some((s) => s === '')) return false
85
+ return segs.every((s) => s === '*' || s === '**' || !/[<>]/.test(s))
86
+ }
87
+
88
+ // True when a pattern names one of the two fixed default roots (implicit, not curated).
89
+ export function isFixedConvention(pattern: string): boolean {
90
+ const p = pattern
91
+ .trim()
92
+ .replace(/\\/g, '/')
93
+ .replace(/^\/+|\/+$/g, '')
94
+ return p === 'skills' || p === '.agents/skills'
95
+ }
96
+
97
+ // ── list ──
98
+
99
+ export function listSkillDirs(root: string): SkillDirEntry[] {
100
+ const custom = readCustomSkillDirs(root).map(
101
+ (pattern): SkillDirEntry => ({ pattern, kind: 'custom', explanation: 'a declared extra skill-dir pattern' }),
102
+ )
103
+ return [...FIXED_SKILL_DIRS, ...custom]
104
+ }
105
+
106
+ // ── add / remove / edit ──
107
+
108
+ export type CrudResult =
109
+ | { ok: true; changed: boolean; patterns: string[]; message: string }
110
+ | { ok: false; reason: string }
111
+
112
+ export function addSkillDir(root: string, pattern: string): CrudResult {
113
+ const p = pattern.trim()
114
+ if (isFixedConvention(p)) return { ok: false, reason: 'that is a fixed default root — always scanned, never curated' }
115
+ if (!isValidPattern(p)) return { ok: false, reason: 'malformed skill-dir pattern' }
116
+ const current = readCustomSkillDirs(root)
117
+ if (current.includes(p)) return { ok: true, changed: false, patterns: current, message: 'already present' }
118
+ const next = [...current, p]
119
+ writeCustomSkillDirs(root, next)
120
+ return { ok: true, changed: true, patterns: next, message: `added ${p}` }
121
+ }
122
+
123
+ export function removeSkillDir(root: string, pattern: string): CrudResult {
124
+ const p = pattern.trim()
125
+ const current = readCustomSkillDirs(root)
126
+ if (!current.includes(p)) return { ok: true, changed: false, patterns: current, message: 'nothing was removed' }
127
+ const next = current.filter((a) => a !== p)
128
+ writeCustomSkillDirs(root, next)
129
+ return { ok: true, changed: true, patterns: next, message: `removed ${p}` }
130
+ }
131
+
132
+ export function editSkillDir(root: string, oldPattern: string, newPattern: string): CrudResult {
133
+ const oldP = oldPattern.trim()
134
+ const newP = newPattern.trim()
135
+ if (isFixedConvention(oldP)) return { ok: false, reason: 'cannot edit a fixed default root' }
136
+ if (!isValidPattern(newP)) return { ok: false, reason: 'malformed replacement pattern' }
137
+ if (isFixedConvention(newP)) return { ok: false, reason: 'replacement is a fixed default root' }
138
+ const current = readCustomSkillDirs(root)
139
+ if (!current.includes(oldP)) return { ok: false, reason: `no such custom pattern: ${oldP}` }
140
+ const next = current.map((a) => (a === oldP ? newP : a))
141
+ writeCustomSkillDirs(root, next)
142
+ return { ok: true, changed: true, patterns: next, message: `edited ${oldP} -> ${newP}` }
143
+ }
144
+
145
+ // ── induce ──
146
+
147
+ export type InduceResult = { ok: true; candidates: string[] } | { ok: false; reason: string }
148
+
149
+ // From a sample path to a directory that CONTAINS skill subdirectories, offer a literal-directory
150
+ // candidate and a *-generalized candidate (the variable segment right before the sample dir's own
151
+ // name — e.g. a plugin name — replaced with a *). Rejects a path that is not a directory under the
152
+ // repo root.
153
+ export function inducePatterns(root: string, samplePath: string): InduceResult {
154
+ const rel = samplePath
155
+ .trim()
156
+ .replace(/\\/g, '/')
157
+ .replace(/^\/+|\/+$/g, '')
158
+ if (rel === '' || /(^|\/)\.\.(\/|$)/.test(rel)) return { ok: false, reason: 'path is not repo-relative' }
159
+ let ok = false
160
+ try {
161
+ ok = statSync(join(root, rel)).isDirectory()
162
+ } catch {
163
+ ok = false
164
+ }
165
+ if (!ok) return { ok: false, reason: `sample path does not resolve to a directory under the repo: ${rel}` }
166
+ const segs = rel.split('/')
167
+ let generalized = rel
168
+ if (segs.length >= 2) {
169
+ const g = [...segs]
170
+ g[segs.length - 2] = '*'
171
+ generalized = g.join('/')
172
+ }
173
+ const candidates = generalized === rel ? [rel] : [rel, generalized]
174
+ return { ok: true, candidates }
175
+ }
176
+
177
+ // ── preview ──
178
+
179
+ interface PreviewMatch {
180
+ path: string
181
+ name: string
182
+ }
183
+
184
+ export type PreviewResult = { ok: true; matches: PreviewMatch[] } | { ok: false; reason: string }
185
+
186
+ // Every dir reachable from `startDir` by descending zero or more levels (startDir itself included),
187
+ // skipping SKIP_DIRS. Backs the `**` segment (any-depth glob) below.
188
+ function collectDescendants(root: string, startDir: string): string[] {
189
+ const out = [startDir]
190
+ let entries: import('node:fs').Dirent[]
191
+ try {
192
+ entries = readdirSync(join(root, startDir), { withFileTypes: true })
193
+ } catch {
194
+ return out
195
+ }
196
+ for (const e of entries) {
197
+ if (!e.isDirectory() || SKIP_DIRS.has(e.name)) continue
198
+ out.push(...collectDescendants(root, startDir ? `${startDir}/${e.name}` : e.name))
199
+ }
200
+ return out
201
+ }
202
+
203
+ // Expand a pattern against the filesystem into the set of directories it matches (mirrors the
204
+ // improve-skill validate engine's own expansion). Does NOT read or persist anything to the config.
205
+ function expandPattern(root: string, pattern: string): string[] {
206
+ const segs = pattern
207
+ .replace(/\\/g, '/')
208
+ .replace(/^\/+|\/+$/g, '')
209
+ .split('/')
210
+ .filter(Boolean)
211
+ let frontier: string[] = ['']
212
+ for (const seg of segs) {
213
+ const next: string[] = []
214
+ if (seg === '**') {
215
+ for (const dir of frontier) next.push(...collectDescendants(root, dir))
216
+ frontier = next
217
+ continue
218
+ }
219
+ if (seg === '*') {
220
+ for (const dir of frontier) {
221
+ let entries: import('node:fs').Dirent[]
222
+ try {
223
+ entries = readdirSync(join(root, dir), { withFileTypes: true })
224
+ } catch {
225
+ continue
226
+ }
227
+ for (const e of entries) {
228
+ if (!e.isDirectory() || SKIP_DIRS.has(e.name)) continue
229
+ next.push(dir ? `${dir}/${e.name}` : e.name)
230
+ }
231
+ }
232
+ frontier = next
233
+ continue
234
+ }
235
+ frontier = frontier.map((dir) => (dir ? `${dir}/${seg}` : seg))
236
+ }
237
+ return frontier
238
+ }
239
+
240
+ // A skill-dir pattern names a directory whose IMMEDIATE children are skill directories: preview
241
+ // probes `<matched-dir>/*/SKILL.md`, listing the SKILL.md file(s) that pattern would add to the scan
242
+ // — mirroring what the built-in defaults (skills/, .agents/skills/) already do.
243
+ export function previewPattern(root: string, pattern: string): PreviewResult {
244
+ const p = pattern.trim()
245
+ if (!isValidPattern(p)) return { ok: false, reason: 'malformed candidate pattern' }
246
+ const matches: PreviewMatch[] = []
247
+ for (const matchedDir of expandPattern(root, p)) {
248
+ let entries: import('node:fs').Dirent[]
249
+ try {
250
+ entries = readdirSync(join(root, matchedDir), { withFileTypes: true })
251
+ } catch {
252
+ continue
253
+ }
254
+ for (const e of entries) {
255
+ if (!e.isDirectory() || SKIP_DIRS.has(e.name)) continue
256
+ const skillFile = matchedDir ? `${matchedDir}/${e.name}/SKILL.md` : `${e.name}/SKILL.md`
257
+ if (!existsSync(join(root, skillFile))) continue
258
+ matches.push({ path: skillFile, name: e.name })
259
+ }
260
+ }
261
+ matches.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0))
262
+ return { ok: true, matches }
263
+ }
264
+
265
+ // ── CLI ──
266
+
267
+ function flag(argv: string[], name: string): string | undefined {
268
+ const i = argv.indexOf(name)
269
+ return i === -1 ? undefined : argv[i + 1]
270
+ }
271
+
272
+ export function main(argv: string[]): number {
273
+ const root = flag(argv, '--root') ?? '.'
274
+ const w = (s: string) => process.stdout.write(`${s}\n`)
275
+
276
+ if (argv.includes('--list')) {
277
+ w('Skill-dir patterns the validate engine scans (fixed default roots are always scanned):')
278
+ for (const a of listSkillDirs(root)) w(` [${a.kind}] ${a.pattern} — ${a.explanation}`)
279
+ return 0
280
+ }
281
+ if (argv.includes('--add')) {
282
+ const r = addSkillDir(root, flag(argv, '--add') ?? '')
283
+ w(r.ok ? r.message : `refused: ${r.reason}`)
284
+ return r.ok ? 0 : 1
285
+ }
286
+ if (argv.includes('--remove')) {
287
+ const r = removeSkillDir(root, flag(argv, '--remove') ?? '')
288
+ w(r.ok ? r.message : `refused: ${r.reason}`)
289
+ return r.ok ? 0 : 1
290
+ }
291
+ if (argv.includes('--edit')) {
292
+ const i = argv.indexOf('--edit')
293
+ const r = editSkillDir(root, argv[i + 1] ?? '', argv[i + 2] ?? '')
294
+ w(r.ok ? r.message : `refused: ${r.reason}`)
295
+ return r.ok ? 0 : 1
296
+ }
297
+ if (argv.includes('--induce')) {
298
+ const r = inducePatterns(root, flag(argv, '--induce') ?? '')
299
+ if (!r.ok) {
300
+ w(`unusable: ${r.reason}`)
301
+ return 1
302
+ }
303
+ w('Candidate skill-dir patterns:')
304
+ for (const c of r.candidates) w(` ${c}`)
305
+ return 0
306
+ }
307
+ if (argv.includes('--preview')) {
308
+ const r = previewPattern(root, flag(argv, '--preview') ?? '')
309
+ if (!r.ok) {
310
+ w(`invalid: ${r.reason}`)
311
+ return 1
312
+ }
313
+ if (r.matches.length === 0) {
314
+ w('This pattern matches no skill.')
315
+ return 0
316
+ }
317
+ w('This pattern would discover:')
318
+ for (const m of r.matches) w(` ${m.path} (${m.name})`)
319
+ return 0
320
+ }
321
+
322
+ w('usage: manage-skill-dirs --list | --add <p> | --remove <p> | --edit <old> <new> | --induce <path> | --preview <p>')
323
+ return 0
324
+ }
325
+
326
+ if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
327
+ process.exit(main(process.argv.slice(2)))
328
+ }
@@ -0,0 +1,17 @@
1
+ # repair-private-skills
2
+
3
+ The concrete validate/repair engine for repo-private skill hygiene under `.agents/skills`. A
4
+ non-user-invocable skill, loaded in-session by the ACED `manage` gateway, carrying a self-contained
5
+ `.mts` script that flags (`validate`, read-only) or fixes (`repair`, writes) a stray symlink into the
6
+ public `skills/` tree and a `SKILL.md` missing `metadata.internal: true`. `repair` writes/deletes only
7
+ under `.agents/skills` — it never touches the public `skills/` tree.
8
+
9
+ - **Skill contract:** [`SKILL.md`](./SKILL.md)
10
+ - **Script:** [`scripts/repair-private-skills.mts`](./scripts/repair-private-skills.mts)
11
+ - **Tests:** [`scripts/repair-private-skills.test.mts`](./scripts/repair-private-skills.test.mts) (`node:test`)
12
+
13
+ ```bash
14
+ node scripts/repair-private-skills.mts --root . validate
15
+ node scripts/repair-private-skills.mts --root . validate --format json
16
+ node scripts/repair-private-skills.mts --root . repair
17
+ ```
@@ -0,0 +1,59 @@
1
+ ---
2
+ name: repair-private-skills
3
+ description: "Partial Skill: invoke by name only — the ACED manage-gateway engine that validates and repairs repo-private skill hygiene — loaded in-session by the ACED manage gateway, not user-triggered."
4
+ user-invocable: false
5
+ metadata:
6
+ internal: true
7
+ ---
8
+
9
+ # repair-private-skills
10
+
11
+ The concrete engine for repo-private skill hygiene under `.agents/skills`. It catches two problems
12
+ a private skill can drift into:
13
+
14
+ - a **stray symlink** whose target resolves into the public `skills/` tree (should be a real
15
+ repo-private directory, not a link into the public one)
16
+ - a **SKILL.md missing `metadata.internal: true`** in its YAML frontmatter (the marker that keeps a
17
+ repo-private skill from leaking as a public one)
18
+
19
+ A directory holding only `SKILL.local.md` / `SKILL.project.md` (no `SKILL.md`) is an
20
+ **augmentation-only** entry and is never flagged. Loaded **in-session** by the `manage` gateway
21
+ (Config runners / hygiene group); it carries a self-contained `.mts` script (the repo's
22
+ node-≥23.6 / no-deps convention).
23
+
24
+ ## Run an operation
25
+
26
+ ```bash
27
+ node "<skill>/scripts/repair-private-skills.mts" [--root .] <validate|repair> [--format json]
28
+ ```
29
+
30
+ | Operation | Effect |
31
+ |---|---|
32
+ | `validate` | read-only; reports every issue found; **makes no filesystem changes**; exits nonzero when issues exist, 0 when clean |
33
+ | `repair` | writes fixes — deletes a stray public-tree symlink, inserts the missing `metadata.internal: true`; exits 0 |
34
+ | `--format json` | (either operation) emit the result as a single JSON object instead of one line per entry |
35
+
36
+ Issue kinds `validate` reports: `public_skill_symlink`, `missing_skill_file`, `missing_frontmatter`,
37
+ `missing_metadata_internal`. Action kinds `repair` records: `removed_public_symlink`,
38
+ `updated_metadata`, `already_internal`, `local_augmentation_only`, `skipped_missing_skill`,
39
+ `skipped_no_frontmatter`.
40
+
41
+ **Curate with validate first.** The intended flow: run `validate` to see what is wrong, then run
42
+ `repair` to fix it. `repair` is idempotent — a clean tree makes no writes.
43
+
44
+ ## Boundaries
45
+
46
+ `validate` performs **zero filesystem writes** — it only reads and reports. `repair` writes/deletes
47
+ **only under `.agents/skills`** — it never creates, modifies, or deletes anything under the public
48
+ `skills/` tree, even though it reads that tree (via a resolved realpath comparison) to detect a stray
49
+ symlink pointing into it.
50
+
51
+ `repair-private-skills` does not discover skills across all sources — that is `list-skills`. It does
52
+ not maintain the per-model runner-def family — that is `manage-model-runners`.
53
+
54
+ When `node` is absent, an agent performs the same checks by hand: for each entry under
55
+ `.agents/skills`, `lstat` it — if it is a symlink whose realpath resolves inside the public `skills/`
56
+ tree, delete it; otherwise read `SKILL.md` (skip if absent and a `SKILL.local.md` /
57
+ `SKILL.project.md` exists instead; flag if neither exists), skip if its first line is not `---`
58
+ (malformed frontmatter is never auto-fixed), and insert `metadata.internal: true` under an existing
59
+ `metadata:` block (or append a new one) when it is missing.