@csark0812/skeleton 1.5.2 → 1.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -36
- package/dist/cli.js +389 -137
- package/dist/hooks/customize-on-skill-read.js +11 -14
- package/package.json +1 -1
- package/schemas/config.schema.json +22 -0
- package/templates/skeleton-init/config.yaml +6 -0
package/README.md
CHANGED
|
@@ -2,17 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
**Source of truth for** Package overview.
|
|
4
4
|
|
|
5
|
-
<!-- doc-meta: owner=eng | last-reviewed=2026-07-
|
|
5
|
+
<!-- doc-meta: owner=eng | last-reviewed=2026-07-15 -->
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Agent repos get messy fast. Skills get copied around, docs disagree, links go stale, and nobody remembers which file is actually canonical.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Skeleton is an SSOT linter for that layer. You define the contract once; Skeleton checks it locally and in CI.
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
If a canonical doc disappears, a registry drifts, a skill index stops matching disk, or a generated reference gets edited by hand, the audit fails before merge.
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
Think ESLint, but for the docs and skills your agents rely on.
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
Skeleton is **not** a runtime agent harness. It doesn't execute tools, enforce permissions, or manage memory. It checks whether the repo around those systems still holds together.
|
|
16
|
+
|
|
17
|
+
## Why this matters
|
|
18
|
+
|
|
19
|
+
Agents can read the repo. They can't reliably infer which of three conflicting docs wins, whether a synced skill should be edited here, or which validation command actually proves a change.
|
|
20
|
+
|
|
21
|
+
That stuff needs to be explicit. More importantly, it needs to stay true after the next 50 PRs.
|
|
22
|
+
|
|
23
|
+
Skeleton turns those repo conventions into checks:
|
|
16
24
|
|
|
17
25
|
| Code repos | Agent repos |
|
|
18
26
|
| ------------------------------------------------------- | -------------------------------------------------------------------------------------- |
|
|
@@ -20,7 +28,20 @@ Code has linters. Agent repos need the same thing for docs and SSOT.
|
|
|
20
28
|
| `eslint --fix` on changed files | `skeleton validate changed` on changed docs and skills |
|
|
21
29
|
| Pre-commit + CI gate | `--staged` pre-commit + `--base` CI gate |
|
|
22
30
|
|
|
23
|
-
Skill linters
|
|
31
|
+
Skill linters answer: _"Is this SKILL.md well-formed?"_
|
|
32
|
+
|
|
33
|
+
Skeleton answers the repo-level question: _"Does this whole thing still agree with itself?"_
|
|
34
|
+
|
|
35
|
+
## What the research says
|
|
36
|
+
|
|
37
|
+
Research here is still early, but the direction is useful: more context isn't automatically better.
|
|
38
|
+
|
|
39
|
+
- A [2026 study of repo-level context files](https://doi.org/10.48550/arxiv.2602.11988) tested 438 coding tasks. Human-written files improved resolution by 4% on average; generated files reduced it by 3%. Both increased inference cost by more than 20%. The recommendation was pretty direct: keep instructions minimal and include what the agent can't infer.
|
|
40
|
+
- A [2025 METR randomized trial](https://metr.org/blog/2025-07-10-early-2025-ai-experienced-os-dev-study/) found experienced open-source developers took 19% longer with early-2025 AI tools while believing they were faster.
|
|
41
|
+
|
|
42
|
+
Neither study proves Skeleton improves task success. That needs a Skeleton-specific benchmark.
|
|
43
|
+
|
|
44
|
+
What they do support is the constraint Skeleton is built around: keep the useful context small, make ownership clear, and verify the rest instead of trusting that it stayed correct.
|
|
24
45
|
|
|
25
46
|
## Quick start
|
|
26
47
|
|
|
@@ -29,7 +50,7 @@ npm install -D @csark0812/skeleton
|
|
|
29
50
|
npx skeleton init --skills
|
|
30
51
|
```
|
|
31
52
|
|
|
32
|
-
|
|
53
|
+
That writes `.skeleton/`, adds the validation scripts, and wires customize hooks for Cursor, Claude Code, and Codex.
|
|
33
54
|
|
|
34
55
|
Edit `.skeleton/config.yaml` for your repo layout, then verify:
|
|
35
56
|
|
|
@@ -37,7 +58,7 @@ Edit `.skeleton/config.yaml` for your repo layout, then verify:
|
|
|
37
58
|
npx skeleton audit self
|
|
38
59
|
```
|
|
39
60
|
|
|
40
|
-
|
|
61
|
+
Flag details: [install](docs/developer/install.md).
|
|
41
62
|
|
|
42
63
|
## What it checks
|
|
43
64
|
|
|
@@ -50,7 +71,7 @@ See [install](docs/developer/install.md) for flags and options.
|
|
|
50
71
|
- **Prose policy** (optional plugins) — YAML pattern rules; idle with no plugins
|
|
51
72
|
- **Shell / JSON syntax** — lightweight checks on changed `.sh` and `.json` files
|
|
52
73
|
|
|
53
|
-
|
|
74
|
+
Skeleton doesn't replace your code gates. Keep TypeScript, Python, Nx, pytest, etc. in the repo that owns them.
|
|
54
75
|
|
|
55
76
|
## The `.skeleton/` contract
|
|
56
77
|
|
|
@@ -75,7 +96,7 @@ Register it:
|
|
|
75
96
|
skeleton register docs/developer/api.md
|
|
76
97
|
```
|
|
77
98
|
|
|
78
|
-
Synced
|
|
99
|
+
Synced skills stay pristine. Project overrides live in `.skeleton/customize/<slug>.md` and inject when the skill is read. No editing copied `SKILL.md` files.
|
|
79
100
|
|
|
80
101
|
## Commands
|
|
81
102
|
|
|
@@ -91,41 +112,33 @@ skeleton customize resolve <slug>
|
|
|
91
112
|
|
|
92
113
|
**Validate changed** routes git diffs to the right audit:
|
|
93
114
|
|
|
94
|
-
| Path | Action
|
|
95
|
-
| ----------------------------------------------------------------------------------- |
|
|
96
|
-
| Docs in scan perimeter | path-scoped audit
|
|
97
|
-
|
|
|
98
|
-
|
|
|
99
|
-
|
|
|
100
|
-
|
|
|
115
|
+
| Path | Action |
|
|
116
|
+
| ----------------------------------------------------------------------------------- | ------------------------------------------------------------- |
|
|
117
|
+
| Docs in scan perimeter | path-scoped audit |
|
|
118
|
+
| Owned skill bodies (`SKILL.md` trees) | exit 1 → run `audit skills` |
|
|
119
|
+
| Foreign / lockfile-synced skill bodies | skip → lint in the owning skills/toolbox repo |
|
|
120
|
+
| `.sh`, `.bash`, `.zsh` | shellcheck or `bash -n` |
|
|
121
|
+
| Other `.json` | JSONC-tolerant syntax check |
|
|
122
|
+
| `.ts`, `.tsx`, `.js`, `.jsx`, `.mjs`, `.cjs`, `.py`, `package.json`, `project.json` | skip (exits 1 if all skip) |
|
|
101
123
|
|
|
102
124
|
Pre-commit: `skeleton validate changed --staged` (path-scoped, fast).
|
|
125
|
+
|
|
103
126
|
CI: `skeleton validate changed --base origin/main` (global rules first, then changed files).
|
|
104
127
|
|
|
105
128
|
## Ecosystem
|
|
106
129
|
|
|
107
|
-
|
|
130
|
+
The model is simple:
|
|
108
131
|
|
|
109
|
-
|
|
|
110
|
-
|
|
|
111
|
-
| **
|
|
112
|
-
| **
|
|
113
|
-
| **
|
|
114
|
-
| **Consumer apps** | Call skeleton for SSOT; keep code validation local |
|
|
132
|
+
| Layer | Role |
|
|
133
|
+
| ----------------- | ---------------------------------------------------------------------------------------------- |
|
|
134
|
+
| **Skeleton** | Defines and checks the SSOT contract |
|
|
135
|
+
| **Shared skills** | Reusable team or public skills. [toolbox](https://github.com/csark0812/toolbox) is one example |
|
|
136
|
+
| **Consumer apps** | Pull in the skills, run Skeleton on SSOT paths, and keep their own code gates |
|
|
115
137
|
|
|
116
|
-
Skeleton never calls Nx or
|
|
138
|
+
Skeleton never calls Nx or another app task runner. Consumer repos keep ownership of test, typecheck, and build.
|
|
117
139
|
|
|
118
140
|
See [tiers](docs/tiers.md).
|
|
119
141
|
|
|
120
|
-
## Distribution
|
|
121
|
-
|
|
122
|
-
| Channel | Installs |
|
|
123
|
-
| ---------------------------------------------------- | ------------------------------------------------------- |
|
|
124
|
-
| `npm install -D @csark0812/skeleton` | CLI, schemas, audit engine, hook script |
|
|
125
|
-
| `npx skills add csark0812/skeleton --skill skeleton` | `/skeleton` agent skill (ops manual, not the installer) |
|
|
126
|
-
|
|
127
|
-
One command for humans: `npx skeleton init --skills`.
|
|
128
|
-
|
|
129
142
|
## Docs
|
|
130
143
|
|
|
131
144
|
- [Install](docs/developer/install.md)
|
|
@@ -145,6 +158,8 @@ bun install
|
|
|
145
158
|
bun run check
|
|
146
159
|
```
|
|
147
160
|
|
|
148
|
-
`bun run check`
|
|
161
|
+
`bun run check` = lint + test + typecheck + build + `audit:self`.
|
|
162
|
+
|
|
163
|
+
`validate:changed` is docs/config only for path-scoped work — it skips code/config extensions (see table above). Owned skill-body edits need `audit skills`. All-skip, owned skill paths (alone or mixed with docs), and missing paths exit non-zero. For code: `bun test`, `bun run typecheck`, `bun run build`.
|
|
149
164
|
|
|
150
165
|
Optional: `brew install pre-commit` (or `pipx install pre-commit`), then `pre-commit install` to wire `.pre-commit-config.yaml`.
|
package/dist/cli.js
CHANGED
|
@@ -15651,6 +15651,9 @@ var require_extend = __commonJS((exports, module) => {
|
|
|
15651
15651
|
};
|
|
15652
15652
|
});
|
|
15653
15653
|
|
|
15654
|
+
// src/cli.ts
|
|
15655
|
+
import { readFileSync as readFileSync22 } from "node:fs";
|
|
15656
|
+
|
|
15654
15657
|
// src/audit/config/load.ts
|
|
15655
15658
|
var import_ajv = __toESM(require_ajv(), 1);
|
|
15656
15659
|
import { existsSync, readFileSync } from "node:fs";
|
|
@@ -16984,12 +16987,99 @@ async function loadPlugins(root, config) {
|
|
|
16984
16987
|
}
|
|
16985
16988
|
|
|
16986
16989
|
// src/audit/core/collect.ts
|
|
16987
|
-
import { existsSync as
|
|
16988
|
-
import { join as
|
|
16990
|
+
import { existsSync as existsSync7, readFileSync as readFileSync5, realpathSync as realpathSync4, statSync as statSync2 } from "node:fs";
|
|
16991
|
+
import { join as join6, relative as relative4 } from "node:path";
|
|
16992
|
+
|
|
16993
|
+
// src/audit/core/skill-roots.ts
|
|
16994
|
+
import { existsSync as existsSync6, readdirSync as readdirSync2, readlinkSync, realpathSync as realpathSync3 } from "node:fs";
|
|
16995
|
+
import { join as join5, relative as relative3 } from "node:path";
|
|
16996
|
+
|
|
16997
|
+
// src/audit/core/skill-provenance.ts
|
|
16998
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4 } from "node:fs";
|
|
16999
|
+
import { join as join4 } from "node:path";
|
|
17000
|
+
var DEFAULT_SKILLS_LOCKFILE = "skills-lock.json";
|
|
17001
|
+
function isForeignLockSourceType(sourceType) {
|
|
17002
|
+
return sourceType !== "local";
|
|
17003
|
+
}
|
|
17004
|
+
function loadSkillsLock(root, lockfileRel = DEFAULT_SKILLS_LOCKFILE) {
|
|
17005
|
+
const warnings = [];
|
|
17006
|
+
const abs = join4(root, lockfileRel);
|
|
17007
|
+
if (!existsSync5(abs)) {
|
|
17008
|
+
return { lockfile: null, entries: {}, warnings };
|
|
17009
|
+
}
|
|
17010
|
+
let raw;
|
|
17011
|
+
try {
|
|
17012
|
+
raw = JSON.parse(readFileSync4(abs, "utf8"));
|
|
17013
|
+
} catch (error) {
|
|
17014
|
+
warnings.push(`malformed ${lockfileRel}: ${error instanceof Error ? error.message : String(error)}`);
|
|
17015
|
+
return { lockfile: lockfileRel, entries: {}, warnings };
|
|
17016
|
+
}
|
|
17017
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
17018
|
+
warnings.push(`${lockfileRel}: expected a JSON object`);
|
|
17019
|
+
return { lockfile: lockfileRel, entries: {}, warnings };
|
|
17020
|
+
}
|
|
17021
|
+
const obj = raw;
|
|
17022
|
+
if (obj.version !== 1) {
|
|
17023
|
+
warnings.push(`${lockfileRel}: unsupported version ${String(obj.version)} (expected 1); ignoring entries`);
|
|
17024
|
+
return { lockfile: lockfileRel, entries: {}, warnings };
|
|
17025
|
+
}
|
|
17026
|
+
const skillsRaw = obj.skills;
|
|
17027
|
+
if (!skillsRaw || typeof skillsRaw !== "object" || Array.isArray(skillsRaw)) {
|
|
17028
|
+
warnings.push(`${lockfileRel}: missing or invalid "skills" object`);
|
|
17029
|
+
return { lockfile: lockfileRel, entries: {}, warnings };
|
|
17030
|
+
}
|
|
17031
|
+
const entries = {};
|
|
17032
|
+
for (const [slug, value] of Object.entries(skillsRaw)) {
|
|
17033
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
17034
|
+
warnings.push(`${lockfileRel}: skill "${slug}" has invalid entry`);
|
|
17035
|
+
continue;
|
|
17036
|
+
}
|
|
17037
|
+
const entry = value;
|
|
17038
|
+
const source = entry.source;
|
|
17039
|
+
const sourceType = entry.sourceType;
|
|
17040
|
+
if (typeof source !== "string" || source.length === 0) {
|
|
17041
|
+
warnings.push(`${lockfileRel}: skill "${slug}" missing source`);
|
|
17042
|
+
continue;
|
|
17043
|
+
}
|
|
17044
|
+
if (typeof sourceType !== "string" || sourceType.length === 0) {
|
|
17045
|
+
warnings.push(`${lockfileRel}: skill "${slug}" missing sourceType`);
|
|
17046
|
+
continue;
|
|
17047
|
+
}
|
|
17048
|
+
const parsed = { source, sourceType };
|
|
17049
|
+
if (typeof entry.skillPath === "string")
|
|
17050
|
+
parsed.skillPath = entry.skillPath;
|
|
17051
|
+
if (typeof entry.computedHash === "string")
|
|
17052
|
+
parsed.computedHash = entry.computedHash;
|
|
17053
|
+
entries[slug] = parsed;
|
|
17054
|
+
}
|
|
17055
|
+
return { lockfile: lockfileRel, entries, warnings };
|
|
17056
|
+
}
|
|
17057
|
+
function classifySkillOwnership(slug, provenance, ownership) {
|
|
17058
|
+
const ownedOverrides = new Set(ownership?.ownedSlugs ?? []);
|
|
17059
|
+
const foreignOverrides = new Set(ownership?.foreignSlugs ?? []);
|
|
17060
|
+
if (ownedOverrides.has(slug))
|
|
17061
|
+
return "owned";
|
|
17062
|
+
if (foreignOverrides.has(slug))
|
|
17063
|
+
return "foreign";
|
|
17064
|
+
const entry = provenance.entries[slug];
|
|
17065
|
+
if (entry && isForeignLockSourceType(entry.sourceType))
|
|
17066
|
+
return "foreign";
|
|
17067
|
+
return "owned";
|
|
17068
|
+
}
|
|
17069
|
+
function resolveOwnershipForSlugs(slugs, provenance, ownership) {
|
|
17070
|
+
const ownedSlugs = [];
|
|
17071
|
+
const foreignSlugs = [];
|
|
17072
|
+
for (const slug of slugs) {
|
|
17073
|
+
if (classifySkillOwnership(slug, provenance, ownership) === "foreign") {
|
|
17074
|
+
foreignSlugs.push(slug);
|
|
17075
|
+
} else {
|
|
17076
|
+
ownedSlugs.push(slug);
|
|
17077
|
+
}
|
|
17078
|
+
}
|
|
17079
|
+
return { ownedSlugs, foreignSlugs };
|
|
17080
|
+
}
|
|
16989
17081
|
|
|
16990
17082
|
// src/audit/core/skill-roots.ts
|
|
16991
|
-
import { existsSync as existsSync5, readdirSync as readdirSync2, readlinkSync, realpathSync as realpathSync3 } from "node:fs";
|
|
16992
|
-
import { join as join4, relative as relative3 } from "node:path";
|
|
16993
17083
|
var NESTED_SKILL_ROOTS = [".claude/skills", ".agents/skills"];
|
|
16994
17084
|
var FLAT_SKILL_DENYLIST = new Set([
|
|
16995
17085
|
".git",
|
|
@@ -17018,10 +17108,10 @@ function safeRealpath(path) {
|
|
|
17018
17108
|
}
|
|
17019
17109
|
}
|
|
17020
17110
|
function listNestedSlugs(root, relRoot) {
|
|
17021
|
-
const absRoot =
|
|
17022
|
-
if (!
|
|
17111
|
+
const absRoot = join5(root, relRoot);
|
|
17112
|
+
if (!existsSync6(absRoot))
|
|
17023
17113
|
return [];
|
|
17024
|
-
return readdirSync2(absRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".") && !NESTED_EXCLUDED_DIRS.has(entry.name)).filter((entry) =>
|
|
17114
|
+
return readdirSync2(absRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".") && !NESTED_EXCLUDED_DIRS.has(entry.name)).filter((entry) => existsSync6(join5(absRoot, entry.name, "SKILL.md"))).map((entry) => entry.name).sort();
|
|
17025
17115
|
}
|
|
17026
17116
|
function listFlatSlugs(root) {
|
|
17027
17117
|
const slugs = [];
|
|
@@ -17030,7 +17120,7 @@ function listFlatSlugs(root) {
|
|
|
17030
17120
|
continue;
|
|
17031
17121
|
if (FLAT_SKILL_DENYLIST.has(entry.name))
|
|
17032
17122
|
continue;
|
|
17033
|
-
if (
|
|
17123
|
+
if (existsSync6(join5(root, entry.name, "SKILL.md"))) {
|
|
17034
17124
|
slugs.push(entry.name);
|
|
17035
17125
|
}
|
|
17036
17126
|
}
|
|
@@ -17040,8 +17130,8 @@ function detectSkillRoots(root) {
|
|
|
17040
17130
|
const roots = [];
|
|
17041
17131
|
let claudeReal = null;
|
|
17042
17132
|
for (const relRoot of NESTED_SKILL_ROOTS) {
|
|
17043
|
-
const abs =
|
|
17044
|
-
if (!
|
|
17133
|
+
const abs = join5(root, relRoot);
|
|
17134
|
+
if (!existsSync6(abs))
|
|
17045
17135
|
continue;
|
|
17046
17136
|
if (relRoot === ".claude/skills") {
|
|
17047
17137
|
claudeReal = safeRealpath(abs);
|
|
@@ -17055,11 +17145,11 @@ function detectSkillRoots(root) {
|
|
|
17055
17145
|
continue;
|
|
17056
17146
|
try {
|
|
17057
17147
|
const link = readlinkSync(abs);
|
|
17058
|
-
if (link && claudeReal && safeRealpath(
|
|
17148
|
+
if (link && claudeReal && safeRealpath(join5(root, link)) === claudeReal)
|
|
17059
17149
|
continue;
|
|
17060
17150
|
} catch {}
|
|
17061
17151
|
}
|
|
17062
|
-
if (listNestedSlugs(root, relRoot).length > 0 ||
|
|
17152
|
+
if (listNestedSlugs(root, relRoot).length > 0 || existsSync6(abs)) {
|
|
17063
17153
|
roots.push({ kind: "nested", relPath: relRoot });
|
|
17064
17154
|
}
|
|
17065
17155
|
}
|
|
@@ -17069,12 +17159,13 @@ function detectSkillRoots(root) {
|
|
|
17069
17159
|
}
|
|
17070
17160
|
return roots;
|
|
17071
17161
|
}
|
|
17072
|
-
function buildSkillIndex(root) {
|
|
17162
|
+
function buildSkillIndex(root, ownership) {
|
|
17073
17163
|
const roots = detectSkillRoots(root);
|
|
17074
17164
|
const slugSet = new Set;
|
|
17075
17165
|
const slugs = [];
|
|
17166
|
+
const flatSlugs = listFlatSlugs(root);
|
|
17076
17167
|
for (const skillRoot of roots) {
|
|
17077
|
-
const rootSlugs = skillRoot.kind === "nested" ? listNestedSlugs(root, skillRoot.relPath) :
|
|
17168
|
+
const rootSlugs = skillRoot.kind === "nested" ? listNestedSlugs(root, skillRoot.relPath) : flatSlugs;
|
|
17078
17169
|
for (const slug of rootSlugs) {
|
|
17079
17170
|
if (!slugSet.has(slug)) {
|
|
17080
17171
|
slugSet.add(slug);
|
|
@@ -17082,32 +17173,50 @@ function buildSkillIndex(root) {
|
|
|
17082
17173
|
}
|
|
17083
17174
|
}
|
|
17084
17175
|
}
|
|
17085
|
-
|
|
17176
|
+
const lockfileRel = ownership?.lockfile ?? DEFAULT_SKILLS_LOCKFILE;
|
|
17177
|
+
const provenance = loadSkillsLock(root, lockfileRel);
|
|
17178
|
+
const { ownedSlugs, foreignSlugs } = resolveOwnershipForSlugs(slugs, provenance, ownership);
|
|
17179
|
+
return { roots, slugs, flatSlugs, ownedSlugs, foreignSlugs, provenance };
|
|
17180
|
+
}
|
|
17181
|
+
function isForeignSkillSlug(index, slug) {
|
|
17182
|
+
return index.foreignSlugs.includes(slug);
|
|
17086
17183
|
}
|
|
17087
17184
|
function resolveSkillPath(index, root, slug) {
|
|
17088
17185
|
for (const skillRoot of index.roots) {
|
|
17089
|
-
const candidate = skillRoot.kind === "nested" ?
|
|
17090
|
-
if (
|
|
17186
|
+
const candidate = skillRoot.kind === "nested" ? join5(root, skillRoot.relPath, slug, "SKILL.md") : join5(root, slug, "SKILL.md");
|
|
17187
|
+
if (existsSync6(candidate)) {
|
|
17091
17188
|
return normalizeRelPath(relative3(root, candidate));
|
|
17092
17189
|
}
|
|
17093
17190
|
}
|
|
17094
17191
|
return null;
|
|
17095
17192
|
}
|
|
17096
17193
|
function isSkillPath(relPath, index) {
|
|
17194
|
+
return skillSlugForPath(relPath, index) !== null;
|
|
17195
|
+
}
|
|
17196
|
+
function skillSlugForPath(relPath, index) {
|
|
17097
17197
|
const normalized = normalizeRelPath(relPath);
|
|
17098
|
-
|
|
17099
|
-
return true;
|
|
17198
|
+
const flat = new Set(index.flatSlugs);
|
|
17100
17199
|
for (const skillRoot of index.roots) {
|
|
17101
|
-
|
|
17102
|
-
|
|
17103
|
-
|
|
17104
|
-
|
|
17105
|
-
const
|
|
17106
|
-
if (
|
|
17107
|
-
return
|
|
17200
|
+
if (skillRoot.kind === "nested") {
|
|
17201
|
+
const prefix = `${skillRoot.relPath}/`;
|
|
17202
|
+
if (!normalized.startsWith(prefix))
|
|
17203
|
+
continue;
|
|
17204
|
+
const slug = normalized.slice(prefix.length).split("/")[0];
|
|
17205
|
+
if (slug && index.slugs.includes(slug))
|
|
17206
|
+
return slug;
|
|
17207
|
+
continue;
|
|
17108
17208
|
}
|
|
17209
|
+
const first = normalized.split("/")[0];
|
|
17210
|
+
if (first && flat.has(first))
|
|
17211
|
+
return first;
|
|
17109
17212
|
}
|
|
17110
|
-
return
|
|
17213
|
+
return null;
|
|
17214
|
+
}
|
|
17215
|
+
function isForeignSkillPath(relPath, index) {
|
|
17216
|
+
const slug = skillSlugForPath(relPath, index);
|
|
17217
|
+
if (!slug)
|
|
17218
|
+
return false;
|
|
17219
|
+
return isForeignSkillSlug(index, slug);
|
|
17111
17220
|
}
|
|
17112
17221
|
var NESTED_SKILL_SLUG_RE = /(?:^|\/)\.(?:claude|agents)\/skills\/([a-z0-9-]+)\//;
|
|
17113
17222
|
var FLAT_SKILL_REFERENCES_RE = /(?:^|\/)([a-z0-9-]+)\/references(?:\/|$)/;
|
|
@@ -17135,18 +17244,23 @@ function slugFromPath(filePath, workspaceRoot) {
|
|
|
17135
17244
|
if (!flatSlug || FLAT_SKILL_DENYLIST.has(flatSlug))
|
|
17136
17245
|
return null;
|
|
17137
17246
|
if (workspaceRoot) {
|
|
17138
|
-
if (!
|
|
17247
|
+
if (!existsSync6(join5(workspaceRoot, flatSlug, "SKILL.md")))
|
|
17139
17248
|
return null;
|
|
17140
17249
|
}
|
|
17141
17250
|
return flatSlug;
|
|
17142
17251
|
}
|
|
17143
17252
|
function skillCollectAugments(index) {
|
|
17253
|
+
const owned = new Set(index.ownedSlugs);
|
|
17144
17254
|
const patterns = [];
|
|
17145
17255
|
for (const skillRoot of index.roots) {
|
|
17146
17256
|
if (skillRoot.kind === "nested") {
|
|
17147
|
-
|
|
17257
|
+
for (const slug of index.ownedSlugs) {
|
|
17258
|
+
patterns.push(`${skillRoot.relPath}/${slug}/**`);
|
|
17259
|
+
}
|
|
17148
17260
|
} else {
|
|
17149
|
-
for (const slug of index.
|
|
17261
|
+
for (const slug of index.flatSlugs) {
|
|
17262
|
+
if (!owned.has(slug))
|
|
17263
|
+
continue;
|
|
17150
17264
|
patterns.push(`${slug}/**`);
|
|
17151
17265
|
}
|
|
17152
17266
|
}
|
|
@@ -17154,12 +17268,15 @@ function skillCollectAugments(index) {
|
|
|
17154
17268
|
return patterns;
|
|
17155
17269
|
}
|
|
17156
17270
|
function listSkillMarkdownPaths(root, index) {
|
|
17271
|
+
const owned = new Set(index.ownedSlugs);
|
|
17157
17272
|
const paths = new Set;
|
|
17158
17273
|
for (const skillRoot of index.roots) {
|
|
17159
17274
|
const slugs = skillRoot.kind === "nested" ? listNestedSlugs(root, skillRoot.relPath) : listFlatSlugs(root);
|
|
17160
17275
|
for (const slug of slugs) {
|
|
17161
|
-
|
|
17162
|
-
|
|
17276
|
+
if (!owned.has(slug))
|
|
17277
|
+
continue;
|
|
17278
|
+
const absDir = skillRoot.kind === "nested" ? join5(root, skillRoot.relPath, slug) : join5(root, slug);
|
|
17279
|
+
if (!existsSync6(absDir))
|
|
17163
17280
|
continue;
|
|
17164
17281
|
for (const abs of globSync("**/*.{md,mdc}", {
|
|
17165
17282
|
cwd: absDir,
|
|
@@ -17176,6 +17293,9 @@ function listSkillMarkdownPaths(root, index) {
|
|
|
17176
17293
|
function listSkillSlugs(index) {
|
|
17177
17294
|
return index.slugs;
|
|
17178
17295
|
}
|
|
17296
|
+
function listOwnedSkillSlugs(index) {
|
|
17297
|
+
return index.ownedSlugs;
|
|
17298
|
+
}
|
|
17179
17299
|
|
|
17180
17300
|
// src/audit/core/collect.ts
|
|
17181
17301
|
var MARKDOWN_GLOBS = ["**/*.md", "**/*.mdc"];
|
|
@@ -17220,7 +17340,13 @@ function collectScanFiles(config, root, skillIndex) {
|
|
|
17220
17340
|
if (skillIndex) {
|
|
17221
17341
|
includePatterns.push(...skillCollectAugments(skillIndex));
|
|
17222
17342
|
}
|
|
17223
|
-
|
|
17343
|
+
const files = expandPatterns(root, includePatterns, exclude);
|
|
17344
|
+
if (!skillIndex)
|
|
17345
|
+
return files;
|
|
17346
|
+
return files.filter((abs) => {
|
|
17347
|
+
const rel = normalizeRelPath(relative4(root, abs));
|
|
17348
|
+
return !isForeignSkillPath(rel, skillIndex);
|
|
17349
|
+
});
|
|
17224
17350
|
}
|
|
17225
17351
|
function collectBannedFiles(config, root) {
|
|
17226
17352
|
if (config.scan.banned.length === 0)
|
|
@@ -17266,19 +17392,19 @@ function collectDocMetaPaths(config, root, registryPaths, skillIndex) {
|
|
|
17266
17392
|
}
|
|
17267
17393
|
const extras = ["docs/README.md", ".skeleton/registry.md"];
|
|
17268
17394
|
for (const file of extras) {
|
|
17269
|
-
const abs =
|
|
17270
|
-
if (
|
|
17395
|
+
const abs = join6(root, file);
|
|
17396
|
+
if (existsSync7(abs))
|
|
17271
17397
|
paths.push(normalizeRelPath(file));
|
|
17272
17398
|
}
|
|
17273
17399
|
for (const rel of registryPaths) {
|
|
17274
17400
|
if (!rel.endsWith(".md") && !rel.endsWith(".mdc"))
|
|
17275
17401
|
continue;
|
|
17276
|
-
const abs =
|
|
17277
|
-
if (
|
|
17402
|
+
const abs = join6(root, rel);
|
|
17403
|
+
if (existsSync7(abs))
|
|
17278
17404
|
paths.push(normalizeRelPath(rel));
|
|
17279
17405
|
}
|
|
17280
17406
|
for (const abs of collectScanFiles(config, root, skillIndex)) {
|
|
17281
|
-
const content =
|
|
17407
|
+
const content = readFileSync5(abs, "utf8");
|
|
17282
17408
|
if (/<!--\s*doc-meta:/.test(content)) {
|
|
17283
17409
|
paths.push(normalizeRelPath(relative4(root, abs)));
|
|
17284
17410
|
}
|
|
@@ -17288,7 +17414,7 @@ function collectDocMetaPaths(config, root, registryPaths, skillIndex) {
|
|
|
17288
17414
|
function validateScanRoots(config, root) {
|
|
17289
17415
|
const missing = [];
|
|
17290
17416
|
for (const tree of extractScanRootsFromInclude(config.scan.include)) {
|
|
17291
|
-
if (!
|
|
17417
|
+
if (!existsSync7(join6(root, tree)))
|
|
17292
17418
|
missing.push(tree);
|
|
17293
17419
|
}
|
|
17294
17420
|
return missing;
|
|
@@ -17310,8 +17436,8 @@ function includeExplicitMarkdownPaths(files, paths, root) {
|
|
|
17310
17436
|
const out = new Set(files);
|
|
17311
17437
|
for (const raw of paths) {
|
|
17312
17438
|
const rel = normalizeRelPath(raw);
|
|
17313
|
-
const abs =
|
|
17314
|
-
if (!
|
|
17439
|
+
const abs = join6(root, rel);
|
|
17440
|
+
if (!existsSync7(abs))
|
|
17315
17441
|
continue;
|
|
17316
17442
|
if (isMarkdownFile(rel)) {
|
|
17317
17443
|
out.add(abs);
|
|
@@ -17335,23 +17461,23 @@ function includeExplicitMarkdownPaths(files, paths, root) {
|
|
|
17335
17461
|
return [...out];
|
|
17336
17462
|
}
|
|
17337
17463
|
function readFileContent(absPath) {
|
|
17338
|
-
return
|
|
17464
|
+
return readFileSync5(absPath, "utf8");
|
|
17339
17465
|
}
|
|
17340
17466
|
function relPath(absPath, root) {
|
|
17341
17467
|
return normalizeRelPath(relative4(root, absPath));
|
|
17342
17468
|
}
|
|
17343
17469
|
|
|
17344
17470
|
// src/audit/core/registry.ts
|
|
17345
|
-
import { existsSync as
|
|
17346
|
-
import { join as
|
|
17471
|
+
import { existsSync as existsSync8, readFileSync as readFileSync6 } from "node:fs";
|
|
17472
|
+
import { join as join7, relative as relative5, resolve as resolve4 } from "node:path";
|
|
17347
17473
|
var REGISTRY_TABLE_ROW_RE = /^\|\s*[^|]+\|\s*\[[^\]]*\]\(([^)]+)\)\s*\|/;
|
|
17348
17474
|
var REGISTRY_TABLE_HEADER_RE = /\|\s*Topic\s*\|\s*Canonical file\s*\|/i;
|
|
17349
17475
|
function parseRegistry(root) {
|
|
17350
|
-
const abs =
|
|
17351
|
-
if (!
|
|
17476
|
+
const abs = join7(root, REGISTRY_REL_PATH);
|
|
17477
|
+
if (!existsSync8(abs)) {
|
|
17352
17478
|
return { paths: [], hasTableHeader: false };
|
|
17353
17479
|
}
|
|
17354
|
-
const content =
|
|
17480
|
+
const content = readFileSync6(abs, "utf8");
|
|
17355
17481
|
const hasTableHeader = REGISTRY_TABLE_HEADER_RE.test(content);
|
|
17356
17482
|
const paths = [];
|
|
17357
17483
|
for (const line of content.split(`
|
|
@@ -17362,7 +17488,7 @@ function parseRegistry(root) {
|
|
|
17362
17488
|
const linkTarget = match[1].trim();
|
|
17363
17489
|
if (linkTarget.startsWith("#"))
|
|
17364
17490
|
continue;
|
|
17365
|
-
const resolved = resolve4(
|
|
17491
|
+
const resolved = resolve4(join7(root, REGISTRY_DIR_REL), linkTarget);
|
|
17366
17492
|
paths.push(normalizeRelPath(relative5(root, resolved)));
|
|
17367
17493
|
}
|
|
17368
17494
|
return { paths: [...new Set(paths)], hasTableHeader };
|
|
@@ -17371,27 +17497,11 @@ function parseRegistryPaths(root) {
|
|
|
17371
17497
|
return parseRegistry(root).paths;
|
|
17372
17498
|
}
|
|
17373
17499
|
|
|
17374
|
-
// src/audit/core/skills-lock.ts
|
|
17375
|
-
import { existsSync as existsSync8, readFileSync as readFileSync6 } from "node:fs";
|
|
17376
|
-
import { join as join7 } from "node:path";
|
|
17377
|
-
var SKILLS_LOCK_REL = "skills-lock.json";
|
|
17378
|
-
function lockedSkillSlugs(root) {
|
|
17379
|
-
const abs = join7(root, SKILLS_LOCK_REL);
|
|
17380
|
-
if (!existsSync8(abs))
|
|
17381
|
-
return new Set;
|
|
17382
|
-
try {
|
|
17383
|
-
const parsed = JSON.parse(readFileSync6(abs, "utf8"));
|
|
17384
|
-
return new Set(Object.keys(parsed.skills ?? {}));
|
|
17385
|
-
} catch {
|
|
17386
|
-
return new Set;
|
|
17387
|
-
}
|
|
17388
|
-
}
|
|
17389
|
-
|
|
17390
17500
|
// src/audit/core/context.ts
|
|
17391
17501
|
function createContext(options = {}) {
|
|
17392
17502
|
const root = options.root ?? findRepoRoot();
|
|
17393
17503
|
const config = loadConfig(root);
|
|
17394
|
-
const skillIndex = buildSkillIndex(root);
|
|
17504
|
+
const skillIndex = buildSkillIndex(root, config.skillOwnership);
|
|
17395
17505
|
let files = collectScanFiles(config, root, skillIndex);
|
|
17396
17506
|
if (options.includeExcludedSkillTrees) {
|
|
17397
17507
|
files = includeExplicitMarkdownPaths(files, listSkillMarkdownPaths(root, skillIndex), root);
|
|
@@ -17411,7 +17521,7 @@ function createContext(options = {}) {
|
|
|
17411
17521
|
registryHasTableHeader: registry.hasTableHeader,
|
|
17412
17522
|
retiredSkills: new Set(retiredSkills(config)),
|
|
17413
17523
|
skillIndex,
|
|
17414
|
-
lockedSkillSlugs:
|
|
17524
|
+
lockedSkillSlugs: new Set(skillIndex.foreignSlugs),
|
|
17415
17525
|
policies: options.policies ?? []
|
|
17416
17526
|
};
|
|
17417
17527
|
}
|
|
@@ -29325,10 +29435,10 @@ function findLocalCanonicalLinks(root2, content3, sourceFile) {
|
|
|
29325
29435
|
}
|
|
29326
29436
|
return links;
|
|
29327
29437
|
}
|
|
29328
|
-
function discoverSkillReferencePlans(root2) {
|
|
29329
|
-
const index2 = buildSkillIndex(root2);
|
|
29438
|
+
function discoverSkillReferencePlans(root2, ownership) {
|
|
29439
|
+
const index2 = buildSkillIndex(root2, ownership);
|
|
29330
29440
|
const plans = [];
|
|
29331
|
-
for (const slug2 of index2.
|
|
29441
|
+
for (const slug2 of index2.ownedSlugs) {
|
|
29332
29442
|
const skillDir = join10(root2, slug2);
|
|
29333
29443
|
if (!existsSync12(join10(skillDir, "SKILL.md")))
|
|
29334
29444
|
continue;
|
|
@@ -29419,12 +29529,13 @@ function listAllGeneratedFiles(root2) {
|
|
|
29419
29529
|
walk(root2);
|
|
29420
29530
|
return files;
|
|
29421
29531
|
}
|
|
29422
|
-
function runGeneratedReferencesCheck(root2) {
|
|
29532
|
+
function runGeneratedReferencesCheck(root2, ownership) {
|
|
29423
29533
|
const issues = [];
|
|
29424
29534
|
const canonicalDir = join11(root2, CANONICAL_REFS_DIR);
|
|
29425
29535
|
if (!existsSync13(canonicalDir))
|
|
29426
29536
|
return issues;
|
|
29427
|
-
const
|
|
29537
|
+
const skillIndex = buildSkillIndex(root2, ownership);
|
|
29538
|
+
const plans = discoverSkillReferencePlans(root2, ownership);
|
|
29428
29539
|
const needed = new Set;
|
|
29429
29540
|
for (const plan of plans) {
|
|
29430
29541
|
for (const refPath of plan.refPaths) {
|
|
@@ -29455,6 +29566,8 @@ function runGeneratedReferencesCheck(root2) {
|
|
|
29455
29566
|
}
|
|
29456
29567
|
}
|
|
29457
29568
|
for (const generatedRel of listAllGeneratedFiles(root2)) {
|
|
29569
|
+
if (isForeignSkillPath(generatedRel, skillIndex))
|
|
29570
|
+
continue;
|
|
29458
29571
|
if (!needed.has(generatedRel)) {
|
|
29459
29572
|
issues.push(issue("generated-references", generatedRel, "orphaned generated copy — run skeleton references sync"));
|
|
29460
29573
|
}
|
|
@@ -29486,7 +29599,7 @@ function runGeneratedReferencesCheck(root2) {
|
|
|
29486
29599
|
return issues;
|
|
29487
29600
|
}
|
|
29488
29601
|
function runGeneratedReferencesRule(ctx) {
|
|
29489
|
-
return runGeneratedReferencesCheck(ctx.root);
|
|
29602
|
+
return runGeneratedReferencesCheck(ctx.root, ctx.config.skillOwnership);
|
|
29490
29603
|
}
|
|
29491
29604
|
var generatedReferencesRule = {
|
|
29492
29605
|
id: "generated-references",
|
|
@@ -29801,7 +29914,8 @@ function validateReadmeTaxonomy(ctx, index2, diskSlugs) {
|
|
|
29801
29914
|
continue;
|
|
29802
29915
|
const taxonomySlugs = parseReadmeTaxonomySlugs(readme);
|
|
29803
29916
|
const nestedSlugs = diskSlugs.filter((slug2) => existsSync17(join14(ctx.root, skillRoot.relPath, slug2, "SKILL.md")));
|
|
29804
|
-
const
|
|
29917
|
+
const foreign = new Set(index2.foreignSlugs);
|
|
29918
|
+
const publicSlugs = nestedSlugs.filter((slug2) => !nonPublic.has(slug2) && !foreign.has(slug2));
|
|
29805
29919
|
const relReadme = `${skillRoot.relPath}/README.md`;
|
|
29806
29920
|
for (const slug2 of publicSlugs) {
|
|
29807
29921
|
if (!taxonomySlugs.includes(slug2)) {
|
|
@@ -29820,30 +29934,45 @@ function runSkillIndexRule(ctx) {
|
|
|
29820
29934
|
const issues = [];
|
|
29821
29935
|
const index2 = ctx.skillIndex;
|
|
29822
29936
|
const diskSlugs = listSkillSlugs(index2);
|
|
29937
|
+
for (const warning of index2.provenance.warnings) {
|
|
29938
|
+
issues.push(issue("skill-index", index2.provenance.lockfile ?? "skills-lock.json", `skill provenance: ${warning}`, {
|
|
29939
|
+
severity: "warning"
|
|
29940
|
+
}));
|
|
29941
|
+
}
|
|
29823
29942
|
issues.push(...validateReadmeTaxonomy(ctx, index2, diskSlugs));
|
|
29943
|
+
const owned = new Set(index2.ownedSlugs);
|
|
29824
29944
|
for (const skillRoot of index2.roots) {
|
|
29825
|
-
|
|
29826
|
-
|
|
29827
|
-
|
|
29828
|
-
|
|
29945
|
+
if (skillRoot.kind === "nested") {
|
|
29946
|
+
for (const slug2 of index2.ownedSlugs) {
|
|
29947
|
+
const skillDir = join14(ctx.root, skillRoot.relPath, slug2);
|
|
29948
|
+
if (!existsSync17(skillDir))
|
|
29949
|
+
continue;
|
|
29950
|
+
for (const skillMd of walkSkillMarkdown(skillDir)) {
|
|
29951
|
+
issues.push(...scanFileForSkillLinks(ctx, skillMd, index2));
|
|
29952
|
+
}
|
|
29829
29953
|
}
|
|
29954
|
+
continue;
|
|
29830
29955
|
}
|
|
29831
|
-
|
|
29832
|
-
|
|
29833
|
-
|
|
29834
|
-
|
|
29835
|
-
|
|
29836
|
-
|
|
29837
|
-
|
|
29838
|
-
|
|
29956
|
+
for (const slug2 of index2.flatSlugs) {
|
|
29957
|
+
if (!owned.has(slug2))
|
|
29958
|
+
continue;
|
|
29959
|
+
const skillDir = join14(ctx.root, slug2);
|
|
29960
|
+
if (!existsSync17(skillDir))
|
|
29961
|
+
continue;
|
|
29962
|
+
for (const skillMd of walkSkillMarkdown(skillDir)) {
|
|
29963
|
+
issues.push(...scanFileForSkillLinks(ctx, skillMd, index2));
|
|
29839
29964
|
}
|
|
29840
29965
|
}
|
|
29841
29966
|
}
|
|
29842
29967
|
return issues;
|
|
29843
29968
|
}
|
|
29844
29969
|
var skillIndexRule = { id: "skill-index", run: runSkillIndexRule };
|
|
29845
|
-
function
|
|
29846
|
-
|
|
29970
|
+
function skillAuditSuffix(ctx) {
|
|
29971
|
+
const owned = listOwnedSkillSlugs(ctx.skillIndex).length;
|
|
29972
|
+
const foreign = ctx.skillIndex.foreignSlugs.length;
|
|
29973
|
+
if (foreign === 0)
|
|
29974
|
+
return ` (${owned} skills on disk)`;
|
|
29975
|
+
return ` (${owned} owned skills audited, ${foreign} foreign ignored)`;
|
|
29847
29976
|
}
|
|
29848
29977
|
|
|
29849
29978
|
// src/audit/rules/index.ts
|
|
@@ -30012,7 +30141,7 @@ async function runAudit(options) {
|
|
|
30012
30141
|
json: options.json,
|
|
30013
30142
|
label,
|
|
30014
30143
|
fileCount: options.suite === "docs" || options.suite === "self" ? ctx.files.length : undefined,
|
|
30015
|
-
successSuffix: options.suite === "skills" ?
|
|
30144
|
+
successSuffix: options.suite === "skills" ? skillAuditSuffix(ctx) : undefined
|
|
30016
30145
|
});
|
|
30017
30146
|
}
|
|
30018
30147
|
|
|
@@ -30111,6 +30240,88 @@ function resolveCustomizeFromRoot(slug2, startDir) {
|
|
|
30111
30240
|
return resolveCustomize(root2, slug2);
|
|
30112
30241
|
}
|
|
30113
30242
|
|
|
30243
|
+
// src/hooks/run.ts
|
|
30244
|
+
function parsePayload(raw) {
|
|
30245
|
+
if (!raw.trim())
|
|
30246
|
+
return {};
|
|
30247
|
+
return JSON.parse(raw);
|
|
30248
|
+
}
|
|
30249
|
+
function extractPath(payload) {
|
|
30250
|
+
const input = payload.tool_input ?? payload.toolInput ?? {};
|
|
30251
|
+
const candidates = [
|
|
30252
|
+
input.path,
|
|
30253
|
+
input.file_path,
|
|
30254
|
+
input.filePath,
|
|
30255
|
+
input.target_file,
|
|
30256
|
+
input.targetFile
|
|
30257
|
+
];
|
|
30258
|
+
for (const value of candidates) {
|
|
30259
|
+
if (typeof value === "string" && value.trim())
|
|
30260
|
+
return normalizeRelPath(value.trim());
|
|
30261
|
+
}
|
|
30262
|
+
return null;
|
|
30263
|
+
}
|
|
30264
|
+
function extractSkillSlug(payload) {
|
|
30265
|
+
const tool = payload.tool_name ?? payload.toolName ?? "";
|
|
30266
|
+
if (tool === "Skill" || tool === "skill") {
|
|
30267
|
+
const input = payload.tool_input ?? payload.toolInput ?? {};
|
|
30268
|
+
const slug2 = input.skill ?? input.slug ?? input.name;
|
|
30269
|
+
if (typeof slug2 === "string" && slug2.trim())
|
|
30270
|
+
return slug2.trim();
|
|
30271
|
+
}
|
|
30272
|
+
const path2 = extractPath(payload);
|
|
30273
|
+
if (path2)
|
|
30274
|
+
return slugFromPath(path2, process.cwd());
|
|
30275
|
+
return null;
|
|
30276
|
+
}
|
|
30277
|
+
function cursorResponse(content3) {
|
|
30278
|
+
return JSON.stringify({ additional_context: content3 });
|
|
30279
|
+
}
|
|
30280
|
+
function claudeResponse(content3) {
|
|
30281
|
+
return JSON.stringify({
|
|
30282
|
+
hookSpecificOutput: {
|
|
30283
|
+
additionalContext: content3
|
|
30284
|
+
}
|
|
30285
|
+
});
|
|
30286
|
+
}
|
|
30287
|
+
function codexResponse(content3) {
|
|
30288
|
+
return JSON.stringify({ additionalContext: content3 });
|
|
30289
|
+
}
|
|
30290
|
+
function formatResponse(payload, content3) {
|
|
30291
|
+
const tool = payload.tool_name ?? payload.toolName ?? "";
|
|
30292
|
+
if (tool === "read_file")
|
|
30293
|
+
return codexResponse(content3);
|
|
30294
|
+
if (payload.hook_event_name?.toLowerCase().includes("claude"))
|
|
30295
|
+
return claudeResponse(content3);
|
|
30296
|
+
if (tool === "Read" && payload.hook_event_name === "postToolUse")
|
|
30297
|
+
return cursorResponse(content3);
|
|
30298
|
+
if (tool === "Read" || tool === "Skill")
|
|
30299
|
+
return claudeResponse(content3);
|
|
30300
|
+
return cursorResponse(content3);
|
|
30301
|
+
}
|
|
30302
|
+
function runCustomizeHook(raw) {
|
|
30303
|
+
try {
|
|
30304
|
+
const payload = parsePayload(raw);
|
|
30305
|
+
const slug2 = extractSkillSlug(payload);
|
|
30306
|
+
if (!slug2)
|
|
30307
|
+
return "{}";
|
|
30308
|
+
const resolved = resolveCustomizeFromRoot(slug2);
|
|
30309
|
+
if (!resolved.content)
|
|
30310
|
+
return "{}";
|
|
30311
|
+
const from = resolved.included.length > 0 ? resolved.included.join(", ") : resolved.path ?? ".skeleton/customize";
|
|
30312
|
+
const prefix = `
|
|
30313
|
+
|
|
30314
|
+
---
|
|
30315
|
+
Customize override for /${slug2} (from ${from}):
|
|
30316
|
+
|
|
30317
|
+
`;
|
|
30318
|
+
return formatResponse(payload, prefix + resolved.content);
|
|
30319
|
+
} catch (error) {
|
|
30320
|
+
console.error(`customize hook error: ${error}`);
|
|
30321
|
+
return "{}";
|
|
30322
|
+
}
|
|
30323
|
+
}
|
|
30324
|
+
|
|
30114
30325
|
// src/init/init.ts
|
|
30115
30326
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
30116
30327
|
import { copyFileSync, existsSync as existsSync22, mkdirSync as mkdirSync2, readFileSync as readFileSync17 } from "node:fs";
|
|
@@ -30142,29 +30353,37 @@ function resolveTemplatesDir() {
|
|
|
30142
30353
|
}
|
|
30143
30354
|
|
|
30144
30355
|
// src/init/resolve-hook-command.ts
|
|
30145
|
-
import { existsSync as existsSync20 } from "node:fs";
|
|
30356
|
+
import { existsSync as existsSync20, realpathSync as realpathSync6 } from "node:fs";
|
|
30146
30357
|
import { createRequire as createRequire3 } from "node:module";
|
|
30147
30358
|
import { dirname as dirname9, join as join17, relative as relative10, resolve as resolve8 } from "node:path";
|
|
30148
30359
|
var PACKAGE_NAME = "@csark0812/skeleton";
|
|
30149
|
-
var
|
|
30150
|
-
var HOOK_SRC = "src/hooks/customize-on-skill-read.ts";
|
|
30360
|
+
var CLI_DIST = "dist/cli.js";
|
|
30151
30361
|
var PACKAGE_ROOT = resolvePackageRoot();
|
|
30362
|
+
var DEV_HOOK_COMMAND = "bun src/cli.ts hook customize";
|
|
30363
|
+
var FALLBACK_HOOK_COMMAND = `node node_modules/${PACKAGE_NAME}/${CLI_DIST} hook customize`;
|
|
30364
|
+
function safeRealpath2(path2) {
|
|
30365
|
+
try {
|
|
30366
|
+
return realpathSync6(path2);
|
|
30367
|
+
} catch {
|
|
30368
|
+
return path2;
|
|
30369
|
+
}
|
|
30370
|
+
}
|
|
30152
30371
|
function toRepoRelative(cwd, absPath) {
|
|
30153
|
-
const rel = relative10(cwd, absPath).replace(/\\/g, "/");
|
|
30372
|
+
const rel = relative10(safeRealpath2(cwd), safeRealpath2(absPath)).replace(/\\/g, "/");
|
|
30154
30373
|
return rel.startsWith("..") ? absPath.replace(/\\/g, "/") : rel;
|
|
30155
30374
|
}
|
|
30156
|
-
function
|
|
30375
|
+
function tryResolvePublishedCli(cwd) {
|
|
30157
30376
|
try {
|
|
30158
30377
|
const req = createRequire3(join17(cwd, "package.json"));
|
|
30159
|
-
return req.resolve(`${PACKAGE_NAME}/${
|
|
30378
|
+
return req.resolve(`${PACKAGE_NAME}/${CLI_DIST}`);
|
|
30160
30379
|
} catch {
|
|
30161
30380
|
return null;
|
|
30162
30381
|
}
|
|
30163
30382
|
}
|
|
30164
|
-
function
|
|
30383
|
+
function walkNodeModulesCli(cwd) {
|
|
30165
30384
|
let dir = cwd;
|
|
30166
30385
|
while (true) {
|
|
30167
|
-
const candidate = join17(dir, "node_modules", PACKAGE_NAME,
|
|
30386
|
+
const candidate = join17(dir, "node_modules", PACKAGE_NAME, CLI_DIST);
|
|
30168
30387
|
if (existsSync20(candidate))
|
|
30169
30388
|
return candidate;
|
|
30170
30389
|
const parent = dirname9(dir);
|
|
@@ -30178,28 +30397,25 @@ function isInsidePackageRoot(cwd) {
|
|
|
30178
30397
|
const rel = relative10(PACKAGE_ROOT, resolve8(cwd)).replace(/\\/g, "/");
|
|
30179
30398
|
return rel === "" || !rel.startsWith("..") && !rel.startsWith("/");
|
|
30180
30399
|
}
|
|
30400
|
+
function nodeCliHookCommand(cliPath) {
|
|
30401
|
+
return `node ${cliPath} hook customize`;
|
|
30402
|
+
}
|
|
30181
30403
|
function resolveHookCommand(cwd) {
|
|
30182
|
-
|
|
30404
|
+
if (isInsidePackageRoot(cwd))
|
|
30405
|
+
return DEV_HOOK_COMMAND;
|
|
30406
|
+
const published = tryResolvePublishedCli(cwd);
|
|
30183
30407
|
if (published)
|
|
30184
|
-
return toRepoRelative(cwd, published);
|
|
30185
|
-
const hoisted =
|
|
30408
|
+
return nodeCliHookCommand(toRepoRelative(cwd, published));
|
|
30409
|
+
const hoisted = walkNodeModulesCli(cwd);
|
|
30186
30410
|
if (hoisted)
|
|
30187
|
-
return toRepoRelative(cwd, hoisted);
|
|
30188
|
-
|
|
30189
|
-
const distHook = join17(PACKAGE_ROOT, HOOK_DIST);
|
|
30190
|
-
if (existsSync20(distHook))
|
|
30191
|
-
return toRepoRelative(cwd, distHook);
|
|
30192
|
-
const srcHook = join17(PACKAGE_ROOT, HOOK_SRC);
|
|
30193
|
-
if (existsSync20(srcHook)) {
|
|
30194
|
-
const rel = toRepoRelative(cwd, srcHook);
|
|
30195
|
-
return rel.includes("/") ? `bun ${rel}` : `bun ./${rel}`;
|
|
30196
|
-
}
|
|
30197
|
-
}
|
|
30198
|
-
return `node node_modules/${PACKAGE_NAME}/${HOOK_DIST}`;
|
|
30411
|
+
return nodeCliHookCommand(toRepoRelative(cwd, hoisted));
|
|
30412
|
+
return FALLBACK_HOOK_COMMAND;
|
|
30199
30413
|
}
|
|
30200
30414
|
function isSkeletonHookCommand(command) {
|
|
30201
30415
|
if (!command)
|
|
30202
30416
|
return false;
|
|
30417
|
+
if (/\bhook\s+customize\b/.test(command))
|
|
30418
|
+
return true;
|
|
30203
30419
|
return /customize-on-skill-read\.(js|ts)\b/.test(command);
|
|
30204
30420
|
}
|
|
30205
30421
|
|
|
@@ -30646,6 +30862,15 @@ import {
|
|
|
30646
30862
|
writeFileSync as writeFileSync4
|
|
30647
30863
|
} from "node:fs";
|
|
30648
30864
|
import { dirname as dirname12, join as join20, relative as relative11 } from "node:path";
|
|
30865
|
+
function resolveOwnership(root2, override) {
|
|
30866
|
+
if (override !== undefined)
|
|
30867
|
+
return override;
|
|
30868
|
+
try {
|
|
30869
|
+
return loadConfig(root2).skillOwnership;
|
|
30870
|
+
} catch {
|
|
30871
|
+
return;
|
|
30872
|
+
}
|
|
30873
|
+
}
|
|
30649
30874
|
function walkMarkdownFiles2(dir, root2) {
|
|
30650
30875
|
const files = [];
|
|
30651
30876
|
if (!existsSync24(dir))
|
|
@@ -30700,7 +30925,7 @@ function syncReferences(options = {}) {
|
|
|
30700
30925
|
removed: [],
|
|
30701
30926
|
skipped: []
|
|
30702
30927
|
};
|
|
30703
|
-
const plans = discoverSkillReferencePlans(root2);
|
|
30928
|
+
const plans = discoverSkillReferencePlans(root2, resolveOwnership(root2, options.ownership));
|
|
30704
30929
|
for (const plan of plans) {
|
|
30705
30930
|
const skillDir = join20(root2, plan.skill);
|
|
30706
30931
|
for (const refPath of plan.refPaths) {
|
|
@@ -30756,7 +30981,13 @@ function runReferencesSync(options = {}) {
|
|
|
30756
30981
|
}
|
|
30757
30982
|
function runReferencesCheck(options = {}) {
|
|
30758
30983
|
const root2 = options.root ?? process.cwd();
|
|
30759
|
-
|
|
30984
|
+
let ownership;
|
|
30985
|
+
try {
|
|
30986
|
+
ownership = loadConfig(root2).skillOwnership;
|
|
30987
|
+
} catch {
|
|
30988
|
+
ownership = undefined;
|
|
30989
|
+
}
|
|
30990
|
+
const issues = runGeneratedReferencesCheck(root2, ownership);
|
|
30760
30991
|
return printReport(issues, {
|
|
30761
30992
|
strict: options.strict,
|
|
30762
30993
|
json: options.json,
|
|
@@ -30823,7 +31054,7 @@ function pathFromRegistryLink(root2, link2) {
|
|
|
30823
31054
|
}
|
|
30824
31055
|
function isOutsideScan(root2, relPath2) {
|
|
30825
31056
|
const config = loadConfig(root2);
|
|
30826
|
-
const skillIndex = buildSkillIndex(root2);
|
|
31057
|
+
const skillIndex = buildSkillIndex(root2, config.skillOwnership);
|
|
30827
31058
|
const scanned = collectScanFiles(config, root2, skillIndex).map((abs) => normalizeRelPath(relative12(root2, abs)));
|
|
30828
31059
|
if (scanned.includes(relPath2))
|
|
30829
31060
|
return false;
|
|
@@ -30976,7 +31207,7 @@ function isSkeletonYamlCandidate(normalized, ext) {
|
|
|
30976
31207
|
return false;
|
|
30977
31208
|
return true;
|
|
30978
31209
|
}
|
|
30979
|
-
function bucketFor(relPath2, root2, wiredPolicies) {
|
|
31210
|
+
function bucketFor(relPath2, root2, wiredPolicies, skillIndex) {
|
|
30980
31211
|
const normalized = normalizeRelPath(relPath2);
|
|
30981
31212
|
const ext = extname2(normalized).toLowerCase();
|
|
30982
31213
|
const name = basename5(normalized);
|
|
@@ -30989,9 +31220,11 @@ function bucketFor(relPath2, root2, wiredPolicies) {
|
|
|
30989
31220
|
return "policy";
|
|
30990
31221
|
return "skip";
|
|
30991
31222
|
}
|
|
30992
|
-
|
|
30993
|
-
|
|
31223
|
+
if (isSkillPath(normalized, skillIndex)) {
|
|
31224
|
+
if (isForeignSkillPath(normalized, skillIndex))
|
|
31225
|
+
return "foreign-skill";
|
|
30994
31226
|
return "skills";
|
|
31227
|
+
}
|
|
30995
31228
|
if (DOC_EXTENSIONS.has(ext)) {
|
|
30996
31229
|
const config = loadConfig(root2);
|
|
30997
31230
|
if (isInScanPerimeter(normalized, config, root2, skillIndex))
|
|
@@ -31102,6 +31335,7 @@ async function runValidateChanged(options = {}) {
|
|
|
31102
31335
|
return 0;
|
|
31103
31336
|
}
|
|
31104
31337
|
const config = loadConfig(root2);
|
|
31338
|
+
const skillIndex = buildSkillIndex(root2, config.skillOwnership);
|
|
31105
31339
|
let wiredPolicies;
|
|
31106
31340
|
try {
|
|
31107
31341
|
wiredPolicies = await collectWiredPolicyRelPaths(root2, config);
|
|
@@ -31118,6 +31352,7 @@ async function runValidateChanged(options = {}) {
|
|
|
31118
31352
|
};
|
|
31119
31353
|
let missing = 0;
|
|
31120
31354
|
let skipped = 0;
|
|
31355
|
+
let foreignSkipped = 0;
|
|
31121
31356
|
const orphans = [];
|
|
31122
31357
|
for (const relPath2 of relPaths) {
|
|
31123
31358
|
const normalized = normalizeRelPath(relPath2);
|
|
@@ -31132,11 +31367,16 @@ async function runValidateChanged(options = {}) {
|
|
|
31132
31367
|
orphans.push(normalized);
|
|
31133
31368
|
continue;
|
|
31134
31369
|
}
|
|
31135
|
-
const bucket = bucketFor(normalized, root2, wiredPolicies);
|
|
31370
|
+
const bucket = bucketFor(normalized, root2, wiredPolicies, skillIndex);
|
|
31136
31371
|
if (bucket === "skip") {
|
|
31137
31372
|
skipped++;
|
|
31138
31373
|
continue;
|
|
31139
31374
|
}
|
|
31375
|
+
if (bucket === "foreign-skill") {
|
|
31376
|
+
foreignSkipped++;
|
|
31377
|
+
console.log(`validate changed: skipping foreign skill ${normalized} (owned upstream; see skills-lock.json / skillOwnership)`);
|
|
31378
|
+
continue;
|
|
31379
|
+
}
|
|
31140
31380
|
buckets[bucket].push(normalized);
|
|
31141
31381
|
}
|
|
31142
31382
|
if (orphans.length > 0) {
|
|
@@ -31184,24 +31424,24 @@ async function runValidateChanged(options = {}) {
|
|
|
31184
31424
|
exitCode = 1;
|
|
31185
31425
|
}
|
|
31186
31426
|
if (buckets.skills.length > 0) {
|
|
31187
|
-
|
|
31188
|
-
if (skillsOnly && !options.base) {
|
|
31427
|
+
if (!options.base) {
|
|
31189
31428
|
console.error(`validate changed: skill paths need the full skills suite (path-scoped skill rules are empty).
|
|
31190
31429
|
` + ` Run: skeleton audit skills
|
|
31191
31430
|
` + " (audit self covers docs + .skeleton; excluded skill trees still need audit skills)");
|
|
31192
|
-
return 1;
|
|
31193
|
-
}
|
|
31194
|
-
const skillExit = await runAudit({
|
|
31195
|
-
suite: "skills",
|
|
31196
|
-
strict: false,
|
|
31197
|
-
json: false,
|
|
31198
|
-
paths: buckets.skills,
|
|
31199
|
-
only: null,
|
|
31200
|
-
root: root2,
|
|
31201
|
-
pathScopedOnly: true
|
|
31202
|
-
});
|
|
31203
|
-
if (skillExit !== 0)
|
|
31204
31431
|
exitCode = 1;
|
|
31432
|
+
} else {
|
|
31433
|
+
const skillExit = await runAudit({
|
|
31434
|
+
suite: "skills",
|
|
31435
|
+
strict: false,
|
|
31436
|
+
json: false,
|
|
31437
|
+
paths: buckets.skills,
|
|
31438
|
+
only: null,
|
|
31439
|
+
root: root2,
|
|
31440
|
+
pathScopedOnly: true
|
|
31441
|
+
});
|
|
31442
|
+
if (skillExit !== 0)
|
|
31443
|
+
exitCode = 1;
|
|
31444
|
+
}
|
|
31205
31445
|
}
|
|
31206
31446
|
for (const relPath2 of buckets.shell) {
|
|
31207
31447
|
if (validateShell(relPath2, root2) !== 0)
|
|
@@ -31227,7 +31467,7 @@ async function runValidateChanged(options = {}) {
|
|
|
31227
31467
|
});
|
|
31228
31468
|
if (proseExit !== 0)
|
|
31229
31469
|
exitCode = 1;
|
|
31230
|
-
const skillPaths = listSkillMarkdownPaths(root2,
|
|
31470
|
+
const skillPaths = listSkillMarkdownPaths(root2, skillIndex);
|
|
31231
31471
|
if (skillPaths.length > 0) {
|
|
31232
31472
|
const skillProseExit = await runAudit({
|
|
31233
31473
|
suite: "skills",
|
|
@@ -31242,17 +31482,20 @@ async function runValidateChanged(options = {}) {
|
|
|
31242
31482
|
exitCode = 1;
|
|
31243
31483
|
}
|
|
31244
31484
|
} else {
|
|
31245
|
-
|
|
31246
|
-
console.error(`validate changed: policy YAML changes need a full prose-policy pass (path-scoped docs are not enough).
|
|
31485
|
+
console.error(`validate changed: policy YAML changes need a full prose-policy pass (path-scoped docs are not enough).
|
|
31247
31486
|
` + ` Run: skeleton audit docs
|
|
31248
31487
|
` + ` And: skeleton audit skills
|
|
31249
31488
|
` + " (audit self covers docs + .skeleton; excluded skill trees still need audit skills)");
|
|
31250
|
-
}
|
|
31251
31489
|
return 1;
|
|
31252
31490
|
}
|
|
31253
31491
|
}
|
|
31254
31492
|
if (exitCode === 0) {
|
|
31255
|
-
const
|
|
31493
|
+
const parts = [];
|
|
31494
|
+
if (skipped > 0)
|
|
31495
|
+
parts.push(`${skipped} path(s) skipped`);
|
|
31496
|
+
if (foreignSkipped > 0)
|
|
31497
|
+
parts.push(`${foreignSkipped} foreign skill(s) ignored`);
|
|
31498
|
+
const note = parts.length > 0 ? ` (${parts.join(", ")})` : "";
|
|
31256
31499
|
console.log(`validate changed passed${note}.`);
|
|
31257
31500
|
}
|
|
31258
31501
|
return exitCode;
|
|
@@ -31270,6 +31513,7 @@ Commands:
|
|
|
31270
31513
|
validate changed [paths…] [--staged] [--base <ref>]
|
|
31271
31514
|
register <path> [--topic=…] [--dry-run] [--json]
|
|
31272
31515
|
customize resolve <slug> [--json]
|
|
31516
|
+
hook customize (reads a host hook payload on stdin)
|
|
31273
31517
|
references sync [--dry-run] [--no-rewrite-links]
|
|
31274
31518
|
references check [--json] [--strict]`);
|
|
31275
31519
|
}
|
|
@@ -31366,6 +31610,14 @@ async function main() {
|
|
|
31366
31610
|
}
|
|
31367
31611
|
process.exit(0);
|
|
31368
31612
|
}
|
|
31613
|
+
if (command === "hook") {
|
|
31614
|
+
if (argv[1] !== "customize") {
|
|
31615
|
+
usage();
|
|
31616
|
+
process.exit(1);
|
|
31617
|
+
}
|
|
31618
|
+
process.stdout.write(runCustomizeHook(readFileSync22(0, "utf8")));
|
|
31619
|
+
process.exit(0);
|
|
31620
|
+
}
|
|
31369
31621
|
if (command === "init") {
|
|
31370
31622
|
const parsed = parseInitArgs(argv.slice(1));
|
|
31371
31623
|
runInit(parsed);
|
|
@@ -13552,7 +13552,7 @@ function resolveCustomizeFromRoot(slug, startDir) {
|
|
|
13552
13552
|
return resolveCustomize(root, slug);
|
|
13553
13553
|
}
|
|
13554
13554
|
|
|
13555
|
-
// src/hooks/
|
|
13555
|
+
// src/hooks/run.ts
|
|
13556
13556
|
function parsePayload(raw) {
|
|
13557
13557
|
if (!raw.trim())
|
|
13558
13558
|
return {};
|
|
@@ -13611,20 +13611,15 @@ function formatResponse(payload, content) {
|
|
|
13611
13611
|
return claudeResponse(content);
|
|
13612
13612
|
return cursorResponse(content);
|
|
13613
13613
|
}
|
|
13614
|
-
function
|
|
13614
|
+
function runCustomizeHook(raw) {
|
|
13615
13615
|
try {
|
|
13616
|
-
const raw = readFileSync4(0, "utf8");
|
|
13617
13616
|
const payload = parsePayload(raw);
|
|
13618
13617
|
const slug = extractSkillSlug(payload);
|
|
13619
|
-
if (!slug)
|
|
13620
|
-
|
|
13621
|
-
return;
|
|
13622
|
-
}
|
|
13618
|
+
if (!slug)
|
|
13619
|
+
return "{}";
|
|
13623
13620
|
const resolved = resolveCustomizeFromRoot(slug);
|
|
13624
|
-
if (!resolved.content)
|
|
13625
|
-
|
|
13626
|
-
return;
|
|
13627
|
-
}
|
|
13621
|
+
if (!resolved.content)
|
|
13622
|
+
return "{}";
|
|
13628
13623
|
const from = resolved.included.length > 0 ? resolved.included.join(", ") : resolved.path ?? ".skeleton/customize";
|
|
13629
13624
|
const prefix = `
|
|
13630
13625
|
|
|
@@ -13632,10 +13627,12 @@ function main() {
|
|
|
13632
13627
|
Customize override for /${slug} (from ${from}):
|
|
13633
13628
|
|
|
13634
13629
|
`;
|
|
13635
|
-
|
|
13630
|
+
return formatResponse(payload, prefix + resolved.content);
|
|
13636
13631
|
} catch (error) {
|
|
13637
13632
|
console.error(`customize hook error: ${error}`);
|
|
13638
|
-
|
|
13633
|
+
return "{}";
|
|
13639
13634
|
}
|
|
13640
13635
|
}
|
|
13641
|
-
|
|
13636
|
+
|
|
13637
|
+
// src/hooks/customize-on-skill-read.ts
|
|
13638
|
+
process.stdout.write(runCustomizeHook(readFileSync4(0, "utf8")));
|
package/package.json
CHANGED
|
@@ -55,6 +55,28 @@
|
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
},
|
|
58
|
+
"skillOwnership": {
|
|
59
|
+
"type": "object",
|
|
60
|
+
"additionalProperties": false,
|
|
61
|
+
"description": "Provenance-aware skill body linting: foreign (synced) skills are skipped; owned local skills and .skeleton/customize stay audited",
|
|
62
|
+
"properties": {
|
|
63
|
+
"lockfile": {
|
|
64
|
+
"type": "string",
|
|
65
|
+
"minLength": 1,
|
|
66
|
+
"description": "Repo-relative skills lock path (default: skills-lock.json)"
|
|
67
|
+
},
|
|
68
|
+
"ownedSlugs": {
|
|
69
|
+
"type": "array",
|
|
70
|
+
"items": { "type": "string", "pattern": "^[a-z0-9-]+$" },
|
|
71
|
+
"description": "Force these skill slugs to be treated as owned (lint here) even when the lockfile marks them foreign"
|
|
72
|
+
},
|
|
73
|
+
"foreignSlugs": {
|
|
74
|
+
"type": "array",
|
|
75
|
+
"items": { "type": "string", "pattern": "^[a-z0-9-]+$" },
|
|
76
|
+
"description": "Force these skill slugs to be treated as foreign (skip body lint) even when absent from the lockfile"
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
},
|
|
58
80
|
"plugins": {
|
|
59
81
|
"type": "array",
|
|
60
82
|
"items": { "type": "string", "minLength": 1 },
|
|
@@ -18,6 +18,12 @@ daysUntilStale: 180
|
|
|
18
18
|
# alwaysInclude:
|
|
19
19
|
# - shared-agent-references.md
|
|
20
20
|
|
|
21
|
+
# Optional: skill body ownership (skills-lock.json github entries are foreign by default)
|
|
22
|
+
# skillOwnership:
|
|
23
|
+
# lockfile: skills-lock.json
|
|
24
|
+
# ownedSlugs: []
|
|
25
|
+
# foreignSlugs: []
|
|
26
|
+
|
|
21
27
|
# Optional: plugin `.ts` entries relative to .skeleton/ (built sibling `.mjs` required)
|
|
22
28
|
# plugins: []
|
|
23
29
|
# Optional: draft-marker allow-list prefixes (plus `_draft-*.md` filenames)
|