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.
- package/.claude-plugin/plugin.json +18 -0
- package/.plugin/pins.json +1 -0
- package/.plugin/plugin.json +18 -0
- package/agents/aced-case-judge.md +165 -0
- package/agents/aced-impl-judge.md +55 -0
- package/agents/aced-scenario-writer.md +76 -0
- package/agents/aced-spec-validator.md +66 -0
- package/governances/README.md +52 -0
- package/governances/agent-tool-output.md +86 -0
- package/governances/cli-resolution.md +102 -0
- package/governances/skill-design.md +258 -0
- package/governances/skill-repo-structure.md +173 -0
- package/package.json +44 -0
- package/readme.md +124 -0
- package/skills/aced-architect-impl/README.md +9 -0
- package/skills/aced-architect-impl/SKILL.md +73 -0
- package/skills/aced-builder-impl/README.md +14 -0
- package/skills/aced-builder-impl/SKILL.md +46 -0
- package/skills/aced-builder-spec/README.md +15 -0
- package/skills/aced-builder-spec/SKILL.md +130 -0
- package/skills/aced-fit/README.md +21 -0
- package/skills/aced-fit/SKILL.md +105 -0
- package/skills/aced-impl-producer/SKILL.md +75 -0
- package/skills/add-scenario/README.md +25 -0
- package/skills/add-scenario/SKILL.md +75 -0
- package/skills/bench/README.md +64 -0
- package/skills/bench/SKILL.md +199 -0
- package/skills/bench/scripts/bench-claude-code.mts +187 -0
- package/skills/bench/scripts/bench-compare.mts +369 -0
- package/skills/bench/scripts/bench-record.mts +133 -0
- package/skills/bench/scripts/bench.mts +930 -0
- package/skills/check-freshness/README.md +57 -0
- package/skills/check-freshness/SKILL.md +76 -0
- package/skills/check-freshness/scripts/check-freshness.mts +399 -0
- package/skills/compare/README.md +25 -0
- package/skills/compare/SKILL.md +87 -0
- package/skills/contribute-skill/README.md +25 -0
- package/skills/contribute-skill/SKILL.md +241 -0
- package/skills/contribute-skill/scripts/source.mts +188 -0
- package/skills/define-agent/README.md +40 -0
- package/skills/define-agent/SKILL.md +279 -0
- package/skills/define-command/README.md +36 -0
- package/skills/define-command/SKILL.md +174 -0
- package/skills/define-governance/SKILL.md +151 -0
- package/skills/define-skill/README.md +37 -0
- package/skills/define-skill/SKILL.md +200 -0
- package/skills/extract-situation/README.md +32 -0
- package/skills/extract-situation/SKILL.md +54 -0
- package/skills/extract-situation/scripts/extract-situation.mts +287 -0
- package/skills/improve/README.md +25 -0
- package/skills/improve/SKILL.md +100 -0
- package/skills/improve-skill/README.md +38 -0
- package/skills/improve-skill/SKILL.md +169 -0
- package/skills/improve-skill/references/check-definitions.md +215 -0
- package/skills/improve-skill/scripts/validate.mts +1033 -0
- package/skills/init-aced/README.md +5 -0
- package/skills/init-aced/SKILL.md +76 -0
- package/skills/init-aced/scripts/ensure-results-ignored.mts +117 -0
- package/skills/list-skills/README.md +19 -0
- package/skills/list-skills/SKILL.md +55 -0
- package/skills/list-skills/scripts/list-skills.mts +210 -0
- package/skills/manage/README.md +35 -0
- package/skills/manage/SKILL.md +65 -0
- package/skills/manage-model-runners/README.md +25 -0
- package/skills/manage-model-runners/SKILL.md +113 -0
- package/skills/manage-skill-dirs/README.md +17 -0
- package/skills/manage-skill-dirs/SKILL.md +54 -0
- package/skills/manage-skill-dirs/scripts/manage-skill-dirs.mts +328 -0
- package/skills/repair-private-skills/README.md +17 -0
- package/skills/repair-private-skills/SKILL.md +59 -0
- package/skills/repair-private-skills/scripts/repair-private-skills.mts +279 -0
- package/skills/report/README.md +25 -0
- package/skills/report/SKILL.md +74 -0
- package/skills/run/README.md +25 -0
- package/skills/run/SKILL.md +220 -0
- package/skills/skillify/README.md +25 -0
- 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.
|