@monte3l/groundwork 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -0
- package/bin/m3l-groundwork.mjs +10 -0
- package/dist/assets.d.ts +20 -0
- package/dist/assets.js +79 -0
- package/dist/caps.d.ts +25 -0
- package/dist/caps.js +69 -0
- package/dist/conflicts.d.ts +12 -0
- package/dist/conflicts.js +77 -0
- package/dist/emit.d.ts +7 -0
- package/dist/emit.js +42 -0
- package/dist/git.d.ts +3 -0
- package/dist/git.js +9 -0
- package/dist/harness/conformance.d.ts +20 -0
- package/dist/harness/conformance.js +18 -0
- package/dist/harness/frontmatter.d.ts +38 -0
- package/dist/harness/frontmatter.js +204 -0
- package/dist/harness/grade.d.ts +4 -0
- package/dist/harness/grade.js +105 -0
- package/dist/harness/rules.d.ts +55 -0
- package/dist/harness/rules.js +580 -0
- package/dist/harness/types.d.ts +32 -0
- package/dist/harness/types.js +9 -0
- package/dist/inventory.d.ts +63 -0
- package/dist/inventory.js +66 -0
- package/dist/jsonc.d.ts +14 -0
- package/dist/jsonc.js +83 -0
- package/dist/main.d.ts +24 -0
- package/dist/main.js +297 -0
- package/dist/merge-json.d.ts +74 -0
- package/dist/merge-json.js +135 -0
- package/dist/mode.d.ts +19 -0
- package/dist/mode.js +53 -0
- package/dist/packs.d.ts +61 -0
- package/dist/packs.js +186 -0
- package/dist/plugin.d.ts +23 -0
- package/dist/plugin.js +79 -0
- package/dist/report.d.ts +4 -0
- package/dist/report.js +323 -0
- package/dist/survey/fs-walk.d.ts +14 -0
- package/dist/survey/fs-walk.js +60 -0
- package/dist/survey/survey-docs.d.ts +4 -0
- package/dist/survey/survey-docs.js +69 -0
- package/dist/survey/survey-harness.d.ts +4 -0
- package/dist/survey/survey-harness.js +121 -0
- package/dist/survey/survey-shape.d.ts +4 -0
- package/dist/survey/survey-shape.js +182 -0
- package/dist/survey/survey-toolchain.d.ts +4 -0
- package/dist/survey/survey-toolchain.js +217 -0
- package/dist/survey/survey.d.ts +5 -0
- package/dist/survey/survey.js +21 -0
- package/dist/survey/types.d.ts +117 -0
- package/dist/survey/types.js +8 -0
- package/dist/tokens.d.ts +13 -0
- package/dist/tokens.js +13 -0
- package/dist/toolchain/conformance.d.ts +20 -0
- package/dist/toolchain/conformance.js +30 -0
- package/dist/toolchain/grade.d.ts +4 -0
- package/dist/toolchain/grade.js +244 -0
- package/dist/toolchain/rules.d.ts +118 -0
- package/dist/toolchain/rules.js +706 -0
- package/dist/toolchain/tsconfig-chain.d.ts +36 -0
- package/dist/toolchain/tsconfig-chain.js +116 -0
- package/dist/toolchain/types.d.ts +27 -0
- package/dist/toolchain/types.js +9 -0
- package/package.json +59 -0
- package/plugin/skills/customize/SKILL.md +305 -0
- package/plugin/src/domain-map.ts +134 -0
- package/plugin/src/index.ts +4 -0
- package/plugin/src/kind-facet-map.ts +174 -0
- package/plugin/src/pack-map.ts +65 -0
- package/templates/core/.claude/agents/Explore.md +43 -0
- package/templates/core/.claude/agents/code-implementer.md +258 -0
- package/templates/core/.claude/agents/code-reviewer.md +163 -0
- package/templates/core/.claude/agents/silent-failure-hunter.md +191 -0
- package/templates/core/.claude/agents/test-author.md +211 -0
- package/templates/core/.claude/hooks/guard-branch-isolation.mjs +123 -0
- package/templates/core/.claude/hooks/guard-double-background.mjs +113 -0
- package/templates/core/.claude/hooks/guard-git-push-signed.mjs +90 -0
- package/templates/core/.claude/hooks/guard-hub-src-writes.mjs +88 -0
- package/templates/core/.claude/hooks/guard-js-extension.mjs +66 -0
- package/templates/core/.claude/hooks/guard-no-commonjs.mjs +105 -0
- package/templates/core/.claude/hooks/guard-protected-paths.mjs +45 -0
- package/templates/core/.claude/hooks/guard-secret-writes.mjs +183 -0
- package/templates/core/.claude/hooks/inject-decision-gate.mjs +119 -0
- package/templates/core/.claude/hooks/post-edit-verify.mjs +150 -0
- package/templates/core/.claude/rules/agent-dispatch.md +121 -0
- package/templates/core/.claude/rules/refactoring.md +52 -0
- package/templates/core/.claude/rules/src.md +114 -0
- package/templates/core/.claude/rules/tests.md +129 -0
- package/templates/core/.claude/settings.json +111 -0
- package/templates/core/.claude/skills/creating-prs/SKILL.md +132 -0
- package/templates/core/.claude/skills/finishing-work/SKILL.md +117 -0
- package/templates/core/.claude/skills/harness-guidance/SKILL.md +140 -0
- package/templates/core/.claude/skills/harness-guidance/references/official-sources.md +58 -0
- package/templates/core/.claude/skills/starting-work/SKILL.md +94 -0
- package/templates/core/.claude/skills/triaging-ci/SKILL.md +111 -0
- package/templates/core/.claude/skills/typescript-guidance/SKILL.md +143 -0
- package/templates/core/.claude/skills/typescript-guidance/references/typescript-sources.md +102 -0
- package/templates/core/.claude/skills/writing-commits/SKILL.md +248 -0
- package/templates/core/.github/workflows/ci.yml +123 -0
- package/templates/core/.github/workflows/dependency-review.yml +26 -0
- package/templates/core/.github/workflows/security-audit.yml +54 -0
- package/templates/core/.node-version +1 -0
- package/templates/core/.prettierignore +5 -0
- package/templates/core/.prettierrc.json +4 -0
- package/templates/core/CLAUDE.md +127 -0
- package/templates/core/README.md +24 -0
- package/templates/core/_gitignore +19 -0
- package/templates/core/_npmrc +1 -0
- package/templates/core/bin/check-exports.mjs +92 -0
- package/templates/core/bin/check-harness.mjs +27 -0
- package/templates/core/bin/check-node-version.mjs +51 -0
- package/templates/core/bin/check-toolchain.mjs +20 -0
- package/templates/core/bin/lib/agent-roster.mjs +8 -0
- package/templates/core/bin/lib/frontmatter.mjs +210 -0
- package/templates/core/bin/lib/harness-rules.mjs +916 -0
- package/templates/core/bin/lib/protected-paths.mjs +23 -0
- package/templates/core/bin/lib/report.mjs +56 -0
- package/templates/core/bin/lib/signed-range.mjs +178 -0
- package/templates/core/bin/lib/toolchain-rules.mjs +1264 -0
- package/templates/core/bin/lib/verify-steps.mjs +131 -0
- package/templates/core/bin/lib/verify-steps.packs.json +1 -0
- package/templates/core/bin/lint-commit.mjs +50 -0
- package/templates/core/bin/strip-claude-trailers.mjs +25 -0
- package/templates/core/bin/verify.mjs +64 -0
- package/templates/core/commitlint.config.js +11 -0
- package/templates/core/docs/research/harness-refresh.md +27 -0
- package/templates/core/docs/research/typescript-refresh.md +32 -0
- package/templates/core/eslint.config.js +105 -0
- package/templates/core/knip.json +6 -0
- package/templates/core/lefthook.yml +39 -0
- package/templates/core/package.json +58 -0
- package/templates/core/pnpm-workspace.yaml +13 -0
- package/templates/core/src/index.ts +12 -0
- package/templates/core/tests/index.test.ts +8 -0
- package/templates/core/tsconfig.base.json +36 -0
- package/templates/core/tsconfig.build.json +10 -0
- package/templates/core/tsconfig.json +11 -0
- package/templates/core/vitest.config.ts +32 -0
- package/templates/packs/README.md +81 -0
- package/templates/packs/harness-extras/files/.claude/agents/type-design-analyzer.md +188 -0
- package/templates/packs/harness-extras/files/.claude/hooks/guard-readonly-bash.mjs +324 -0
- package/templates/packs/harness-extras/files/.claude/hooks/reinject-compact-handoff.mjs +197 -0
- package/templates/packs/harness-extras/files/.claude/hooks/write-compact-handoff.mjs +180 -0
- package/templates/packs/harness-extras/files/bin/check-file-budget.mjs +407 -0
- package/templates/packs/harness-extras/files/bin/file-budget-baseline.json +1 -0
- package/templates/packs/harness-extras/pack.json +65 -0
- package/templates/packs/statusline/files/.claude/hooks/statusline-layout.mjs +365 -0
- package/templates/packs/statusline/files/.claude/hooks/statusline.mjs +996 -0
- package/templates/packs/statusline/files/.claude/hooks/subagent-statusline.mjs +203 -0
- package/templates/packs/statusline/pack.json +31 -0
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The harness rule set: a declarative list of checks, each a pure function
|
|
3
|
+
* over a `HarnessSnapshot` (everything the grader read from disk, already in
|
|
4
|
+
* memory). Rules never touch the filesystem, so they are testable without
|
|
5
|
+
* temp directories and cannot drift on I/O behaviour.
|
|
6
|
+
*
|
|
7
|
+
* Structural rules encode wiring integrity. Rubric rules encode Anthropic's
|
|
8
|
+
* published guidance (skill body at most 500 lines, a scoped tool list, a
|
|
9
|
+
* description substantial enough to trigger on) and only ever warn.
|
|
10
|
+
*
|
|
11
|
+
* `templates/core/bin/lib/harness-rules.mjs` is the emitted ESM twin of this
|
|
12
|
+
* file; `tests/harness-parity.test.ts` runs both over the real baseline and
|
|
13
|
+
* asserts identical findings, so the two cannot silently diverge.
|
|
14
|
+
*/
|
|
15
|
+
import { isRecord } from "../merge-json.js";
|
|
16
|
+
import { fieldList, fieldText, parseFrontmatter } from "./frontmatter.js";
|
|
17
|
+
/**
|
|
18
|
+
* Model ids and aliases considered current. Bump alongside the
|
|
19
|
+
* `harness-guidance` refresh sweep; `templates/core/bin/lib/harness-rules.mjs`
|
|
20
|
+
* carries the same list and the parity test keeps the two equal.
|
|
21
|
+
*/
|
|
22
|
+
export const CURRENT_MODELS = [
|
|
23
|
+
"inherit",
|
|
24
|
+
"opus",
|
|
25
|
+
"sonnet",
|
|
26
|
+
"haiku",
|
|
27
|
+
"fable",
|
|
28
|
+
"claude-opus-5",
|
|
29
|
+
"claude-sonnet-5",
|
|
30
|
+
"claude-fable-5-1",
|
|
31
|
+
"claude-haiku-4-5",
|
|
32
|
+
"claude-haiku-4-5-20251001",
|
|
33
|
+
];
|
|
34
|
+
const SKILL_BODY_LINE_LIMIT = 500;
|
|
35
|
+
const DESCRIPTION_MIN = 40;
|
|
36
|
+
const DESCRIPTION_MAX = 1024;
|
|
37
|
+
const BARE_ENTRY_POINT = /process\.argv\[1\]\s*===\s*fileURLToPath\(import\.meta\.url\)/;
|
|
38
|
+
const HOOK_PATH = /\.claude\/hooks\/([A-Za-z0-9_.-]+)/g;
|
|
39
|
+
const CLAUDE_PATH = /\.claude\/[A-Za-z0-9_.*/-]+/g;
|
|
40
|
+
const REFERENCE_PATH = /\breferences\/[A-Za-z0-9_./-]+\.md/g;
|
|
41
|
+
function hookRegistrations(settings) {
|
|
42
|
+
if (!isRecord(settings) || !isRecord(settings["hooks"]))
|
|
43
|
+
return [];
|
|
44
|
+
const registrations = [];
|
|
45
|
+
for (const [event, entries] of Object.entries(settings["hooks"])) {
|
|
46
|
+
if (!Array.isArray(entries))
|
|
47
|
+
continue;
|
|
48
|
+
for (const entry of entries) {
|
|
49
|
+
if (!isRecord(entry) || !Array.isArray(entry["hooks"]))
|
|
50
|
+
continue;
|
|
51
|
+
for (const hook of entry["hooks"]) {
|
|
52
|
+
if (!isRecord(hook) || typeof hook["command"] !== "string")
|
|
53
|
+
continue;
|
|
54
|
+
registrations.push({
|
|
55
|
+
event,
|
|
56
|
+
command: hook["command"],
|
|
57
|
+
hasTimeout: typeof hook["timeout"] === "number",
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return registrations;
|
|
63
|
+
}
|
|
64
|
+
function allRegistrations(snapshot) {
|
|
65
|
+
return [
|
|
66
|
+
...hookRegistrations(snapshot.settings.parsed),
|
|
67
|
+
...hookRegistrations(snapshot.settingsLocal),
|
|
68
|
+
];
|
|
69
|
+
}
|
|
70
|
+
/** Every string anywhere inside a parsed JSON value. */
|
|
71
|
+
function collectStrings(value) {
|
|
72
|
+
if (typeof value === "string")
|
|
73
|
+
return [value];
|
|
74
|
+
if (Array.isArray(value)) {
|
|
75
|
+
return value.flatMap(collectStrings);
|
|
76
|
+
}
|
|
77
|
+
if (isRecord(value))
|
|
78
|
+
return Object.values(value).flatMap(collectStrings);
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Hook files named by any command in settings -- `hooks` registrations and
|
|
83
|
+
* also top-level command keys such as `statusLine` -- named directly, since
|
|
84
|
+
* a wiring reference is a wiring reference wherever it sits.
|
|
85
|
+
*/
|
|
86
|
+
function registeredHookFiles(snapshot) {
|
|
87
|
+
const names = new Set();
|
|
88
|
+
for (const text of [
|
|
89
|
+
...collectStrings(snapshot.settings.parsed),
|
|
90
|
+
...collectStrings(snapshot.settingsLocal),
|
|
91
|
+
]) {
|
|
92
|
+
for (const match of text.matchAll(HOOK_PATH)) {
|
|
93
|
+
if (match[1] !== undefined)
|
|
94
|
+
names.add(match[1]);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return names;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* `registeredHookFiles` plus every hook file a reachable hook's source
|
|
101
|
+
* mentions by name -- a helper module imported by a registered script is
|
|
102
|
+
* wired in even though no settings command names it. Followed to a
|
|
103
|
+
* fixpoint, so a helper's own helpers count too.
|
|
104
|
+
*/
|
|
105
|
+
function reachableHookFiles(snapshot) {
|
|
106
|
+
const reachable = registeredHookFiles(snapshot);
|
|
107
|
+
let grew = true;
|
|
108
|
+
while (grew) {
|
|
109
|
+
grew = false;
|
|
110
|
+
for (const name of snapshot.hooks.keys()) {
|
|
111
|
+
if (reachable.has(name))
|
|
112
|
+
continue;
|
|
113
|
+
const mentioned = [...reachable].some((other) => snapshot.hooks.get(other)?.includes(name));
|
|
114
|
+
if (mentioned) {
|
|
115
|
+
reachable.add(name);
|
|
116
|
+
grew = true;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return reachable;
|
|
121
|
+
}
|
|
122
|
+
/** Translates a Claude Code `paths` glob (`**`, `*`, `?`, `{a,b}`) into a RegExp. */
|
|
123
|
+
function globToRegExp(glob) {
|
|
124
|
+
const translate = (source) => {
|
|
125
|
+
let out = "";
|
|
126
|
+
for (let i = 0; i < source.length; i++) {
|
|
127
|
+
const ch = source[i] ?? "";
|
|
128
|
+
if (ch === "*") {
|
|
129
|
+
if (source[i + 1] === "*") {
|
|
130
|
+
i++;
|
|
131
|
+
if (source[i + 1] === "/") {
|
|
132
|
+
i++;
|
|
133
|
+
out += "(?:.*/)?";
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
out += ".*";
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
out += "[^/]*";
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
else if (ch === "?") {
|
|
144
|
+
out += "[^/]";
|
|
145
|
+
}
|
|
146
|
+
else if (ch === "{" && source.indexOf("}", i) !== -1) {
|
|
147
|
+
const close = source.indexOf("}", i);
|
|
148
|
+
out += `(?:${source
|
|
149
|
+
.slice(i + 1, close)
|
|
150
|
+
.split(",")
|
|
151
|
+
.map(translate)
|
|
152
|
+
.join("|")})`;
|
|
153
|
+
i = close;
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
out += ch.replace(/[.+^$()|[\]\\{}]/g, "\\$&");
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return out;
|
|
160
|
+
};
|
|
161
|
+
return new RegExp(`^${translate(glob)}$`);
|
|
162
|
+
}
|
|
163
|
+
function bodyLineCount(body) {
|
|
164
|
+
return body.replace(/\n+$/, "").split("\n").length;
|
|
165
|
+
}
|
|
166
|
+
const settingsParses = {
|
|
167
|
+
id: "settings-parses",
|
|
168
|
+
level: "structural",
|
|
169
|
+
category: "settings",
|
|
170
|
+
check: (s) => ({
|
|
171
|
+
checked: s.settings.present ? 1 : 0,
|
|
172
|
+
failures: s.settings.error === undefined
|
|
173
|
+
? []
|
|
174
|
+
: [
|
|
175
|
+
{
|
|
176
|
+
subject: ".claude/settings.json",
|
|
177
|
+
message: `does not parse: ${s.settings.error}`,
|
|
178
|
+
},
|
|
179
|
+
],
|
|
180
|
+
}),
|
|
181
|
+
};
|
|
182
|
+
const hookDangling = {
|
|
183
|
+
id: "hook-dangling",
|
|
184
|
+
level: "structural",
|
|
185
|
+
category: "hooks",
|
|
186
|
+
check: (s) => {
|
|
187
|
+
if (s.settings.error !== undefined)
|
|
188
|
+
return { checked: 0, failures: [] };
|
|
189
|
+
const referenced = registeredHookFiles(s);
|
|
190
|
+
return {
|
|
191
|
+
checked: referenced.size,
|
|
192
|
+
failures: [...referenced]
|
|
193
|
+
.filter((name) => !s.hooks.has(name))
|
|
194
|
+
.map((name) => ({
|
|
195
|
+
subject: `.claude/hooks/${name}`,
|
|
196
|
+
message: "is named by a hook registration but does not exist on disk",
|
|
197
|
+
})),
|
|
198
|
+
};
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
const hookOrphan = {
|
|
202
|
+
id: "hook-orphan",
|
|
203
|
+
level: "structural",
|
|
204
|
+
category: "hooks",
|
|
205
|
+
check: (s) => {
|
|
206
|
+
if (s.settings.error !== undefined)
|
|
207
|
+
return { checked: 0, failures: [] };
|
|
208
|
+
const referenced = reachableHookFiles(s);
|
|
209
|
+
const hookFiles = [...s.hooks.keys()].filter((name) => name.endsWith(".mjs") || name.endsWith(".js"));
|
|
210
|
+
return {
|
|
211
|
+
checked: hookFiles.length,
|
|
212
|
+
failures: hookFiles
|
|
213
|
+
.filter((name) => !referenced.has(name))
|
|
214
|
+
.map((name) => ({
|
|
215
|
+
subject: `.claude/hooks/${name}`,
|
|
216
|
+
message: "exists but nothing runs it -- no settings.json command names it and no registered hook imports it",
|
|
217
|
+
})),
|
|
218
|
+
};
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
const hookEntrypoint = {
|
|
222
|
+
id: "hook-entrypoint",
|
|
223
|
+
level: "structural",
|
|
224
|
+
category: "hooks",
|
|
225
|
+
check: (s) => {
|
|
226
|
+
const sources = [...s.hooks].filter(([, source]) => source.includes("process.argv[1]"));
|
|
227
|
+
return {
|
|
228
|
+
checked: sources.length,
|
|
229
|
+
failures: sources
|
|
230
|
+
.filter(([, source]) => BARE_ENTRY_POINT.test(source) ||
|
|
231
|
+
!source.includes("realpathSync(process.argv[1])"))
|
|
232
|
+
.map(([name]) => ({
|
|
233
|
+
subject: `.claude/hooks/${name}`,
|
|
234
|
+
message: "compares process.argv[1] to import.meta.url without realpathSync -- false under any symlinked path, so the hook fails open",
|
|
235
|
+
})),
|
|
236
|
+
};
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
const skillShape = {
|
|
240
|
+
id: "skill-shape",
|
|
241
|
+
level: "structural",
|
|
242
|
+
category: "skills",
|
|
243
|
+
check: (s) => {
|
|
244
|
+
const failures = [];
|
|
245
|
+
for (const skill of s.skills) {
|
|
246
|
+
const subject = `.claude/skills/${skill.name}`;
|
|
247
|
+
if (skill.skillMd === undefined) {
|
|
248
|
+
failures.push({ subject, message: "has no SKILL.md" });
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
const parsed = parseFrontmatter(skill.skillMd);
|
|
252
|
+
if (!parsed.ok) {
|
|
253
|
+
failures.push({ subject, message: `SKILL.md: ${parsed.error}` });
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
for (const problem of parsed.problems) {
|
|
257
|
+
failures.push({ subject, message: `SKILL.md: ${problem}` });
|
|
258
|
+
}
|
|
259
|
+
// `name` is optional for a skill (it defaults to the directory), so
|
|
260
|
+
// only a name that is present and different is a wiring defect.
|
|
261
|
+
const name = fieldText(parsed.fields, "name");
|
|
262
|
+
if (name !== undefined && name !== "" && name !== skill.name) {
|
|
263
|
+
failures.push({
|
|
264
|
+
subject,
|
|
265
|
+
message: `SKILL.md \`name\` is "${name}" but the directory is "${skill.name}"`,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return { checked: s.skills.length, failures };
|
|
270
|
+
},
|
|
271
|
+
};
|
|
272
|
+
const agentShape = {
|
|
273
|
+
id: "agent-shape",
|
|
274
|
+
level: "structural",
|
|
275
|
+
category: "agents",
|
|
276
|
+
check: (s) => {
|
|
277
|
+
const failures = [];
|
|
278
|
+
for (const [file, text] of s.agents) {
|
|
279
|
+
const subject = `.claude/agents/${file}`;
|
|
280
|
+
const parsed = parseFrontmatter(text);
|
|
281
|
+
if (!parsed.ok) {
|
|
282
|
+
failures.push({ subject, message: parsed.error });
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
for (const problem of parsed.problems) {
|
|
286
|
+
failures.push({ subject, message: problem });
|
|
287
|
+
}
|
|
288
|
+
const expected = file.replace(/\.md$/, "");
|
|
289
|
+
const name = fieldText(parsed.fields, "name");
|
|
290
|
+
if (name === undefined || name === "") {
|
|
291
|
+
failures.push({ subject, message: "has no `name`" });
|
|
292
|
+
}
|
|
293
|
+
else if (name !== expected) {
|
|
294
|
+
failures.push({
|
|
295
|
+
subject,
|
|
296
|
+
message: `\`name\` is "${name}" but the file is "${file}"`,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
const description = fieldText(parsed.fields, "description");
|
|
300
|
+
if (description === undefined || description === "") {
|
|
301
|
+
failures.push({ subject, message: "has no `description`" });
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return { checked: s.agents.size, failures };
|
|
305
|
+
},
|
|
306
|
+
};
|
|
307
|
+
const ruleShape = {
|
|
308
|
+
id: "rule-shape",
|
|
309
|
+
level: "structural",
|
|
310
|
+
category: "rules",
|
|
311
|
+
check: (s) => {
|
|
312
|
+
const failures = [];
|
|
313
|
+
for (const [file, text] of s.rules) {
|
|
314
|
+
const subject = `.claude/rules/${file}`;
|
|
315
|
+
// A rule with no frontmatter at all is a legitimate unconditional rule.
|
|
316
|
+
if (!text.startsWith("---"))
|
|
317
|
+
continue;
|
|
318
|
+
const parsed = parseFrontmatter(text);
|
|
319
|
+
if (!parsed.ok) {
|
|
320
|
+
failures.push({ subject, message: parsed.error });
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
for (const problem of parsed.problems) {
|
|
324
|
+
failures.push({ subject, message: problem });
|
|
325
|
+
}
|
|
326
|
+
const paths = fieldList(parsed.fields, "paths");
|
|
327
|
+
if (paths !== undefined && paths.length === 0) {
|
|
328
|
+
failures.push({ subject, message: "declares `paths` but it is empty" });
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return { checked: s.rules.size, failures };
|
|
332
|
+
},
|
|
333
|
+
};
|
|
334
|
+
const claudeMdRefs = {
|
|
335
|
+
id: "claudemd-refs",
|
|
336
|
+
level: "structural",
|
|
337
|
+
category: "claude-md",
|
|
338
|
+
check: (s) => {
|
|
339
|
+
if (s.claudeMd === undefined)
|
|
340
|
+
return { checked: 0, failures: [] };
|
|
341
|
+
const failures = [];
|
|
342
|
+
const refs = new Set();
|
|
343
|
+
for (const match of s.claudeMd.matchAll(CLAUDE_PATH)) {
|
|
344
|
+
const path = match[0].replace(/[.,;:]+$/, "").replace(/\/+$/, "");
|
|
345
|
+
if (path.includes("*") || path === ".claude/settings.local.json")
|
|
346
|
+
continue;
|
|
347
|
+
if (path === ".claude")
|
|
348
|
+
continue;
|
|
349
|
+
refs.add(path);
|
|
350
|
+
}
|
|
351
|
+
for (const path of refs) {
|
|
352
|
+
if (!s.claudeTree.has(path)) {
|
|
353
|
+
failures.push({
|
|
354
|
+
subject: "CLAUDE.md",
|
|
355
|
+
message: `names ${path}, which does not exist`,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
for (const file of s.rules.keys()) {
|
|
360
|
+
if (!s.claudeMd.includes(`.claude/rules/${file}`)) {
|
|
361
|
+
failures.push({
|
|
362
|
+
subject: `.claude/rules/${file}`,
|
|
363
|
+
message: "exists but CLAUDE.md never names it",
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return { checked: refs.size + s.rules.size, failures };
|
|
368
|
+
},
|
|
369
|
+
};
|
|
370
|
+
const skillBodySize = {
|
|
371
|
+
id: "skill-body-size",
|
|
372
|
+
level: "rubric",
|
|
373
|
+
category: "skills",
|
|
374
|
+
check: (s) => {
|
|
375
|
+
const failures = [];
|
|
376
|
+
let checked = 0;
|
|
377
|
+
for (const skill of s.skills) {
|
|
378
|
+
if (skill.skillMd === undefined)
|
|
379
|
+
continue;
|
|
380
|
+
const parsed = parseFrontmatter(skill.skillMd);
|
|
381
|
+
if (!parsed.ok)
|
|
382
|
+
continue;
|
|
383
|
+
checked++;
|
|
384
|
+
const lines = bodyLineCount(parsed.body);
|
|
385
|
+
if (lines > SKILL_BODY_LINE_LIMIT) {
|
|
386
|
+
failures.push({
|
|
387
|
+
subject: `.claude/skills/${skill.name}`,
|
|
388
|
+
message: `SKILL.md body is ${lines} lines; Anthropic's guidance is <= ${SKILL_BODY_LINE_LIMIT} -- move detail into references/`,
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return { checked, failures };
|
|
393
|
+
},
|
|
394
|
+
};
|
|
395
|
+
const descriptionSubstance = {
|
|
396
|
+
id: "description-substance",
|
|
397
|
+
level: "rubric",
|
|
398
|
+
category: "skills",
|
|
399
|
+
check: (s) => {
|
|
400
|
+
const failures = [];
|
|
401
|
+
let checked = 0;
|
|
402
|
+
const examine = (subject, text, required) => {
|
|
403
|
+
const parsed = parseFrontmatter(text);
|
|
404
|
+
if (!parsed.ok)
|
|
405
|
+
return;
|
|
406
|
+
const description = fieldText(parsed.fields, "description");
|
|
407
|
+
if (description === undefined || description === "") {
|
|
408
|
+
// Agents require one and shape-check it; a skill may omit it (Claude
|
|
409
|
+
// falls back to the first paragraph), which is a quality gap.
|
|
410
|
+
if (required)
|
|
411
|
+
return;
|
|
412
|
+
checked++;
|
|
413
|
+
failures.push({
|
|
414
|
+
subject,
|
|
415
|
+
message: "has no `description`, so Claude has nothing to match the skill against but its first paragraph",
|
|
416
|
+
});
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
checked++;
|
|
420
|
+
if (description.length < DESCRIPTION_MIN) {
|
|
421
|
+
failures.push({
|
|
422
|
+
subject,
|
|
423
|
+
message: `description is ${description.length} chars -- too thin to trigger on reliably (>= ${DESCRIPTION_MIN})`,
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
else if (description.length > DESCRIPTION_MAX) {
|
|
427
|
+
failures.push({
|
|
428
|
+
subject,
|
|
429
|
+
message: `description is ${description.length} chars; over ${DESCRIPTION_MAX} it is truncated in the skill listing`,
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
for (const skill of s.skills) {
|
|
434
|
+
if (skill.skillMd !== undefined) {
|
|
435
|
+
examine(`.claude/skills/${skill.name}`, skill.skillMd, false);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
for (const [file, text] of s.agents) {
|
|
439
|
+
examine(`.claude/agents/${file}`, text, true);
|
|
440
|
+
}
|
|
441
|
+
return { checked, failures };
|
|
442
|
+
},
|
|
443
|
+
};
|
|
444
|
+
const modelPinCurrency = {
|
|
445
|
+
id: "model-pin-currency",
|
|
446
|
+
level: "rubric",
|
|
447
|
+
category: "agents",
|
|
448
|
+
check: (s) => {
|
|
449
|
+
const failures = [];
|
|
450
|
+
let checked = 0;
|
|
451
|
+
for (const [file, text] of s.agents) {
|
|
452
|
+
const parsed = parseFrontmatter(text);
|
|
453
|
+
if (!parsed.ok)
|
|
454
|
+
continue;
|
|
455
|
+
checked++;
|
|
456
|
+
const model = fieldText(parsed.fields, "model");
|
|
457
|
+
const subject = `.claude/agents/${file}`;
|
|
458
|
+
if (model === undefined || model === "") {
|
|
459
|
+
failures.push({
|
|
460
|
+
subject,
|
|
461
|
+
message: "pins no `model`, so it inherits whatever the session runs",
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
else if (!CURRENT_MODELS.includes(model)) {
|
|
465
|
+
failures.push({
|
|
466
|
+
subject,
|
|
467
|
+
message: `\`model: ${model}\` is not a known-current model id or alias`,
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
return { checked, failures };
|
|
472
|
+
},
|
|
473
|
+
};
|
|
474
|
+
const agentToolScope = {
|
|
475
|
+
id: "agent-tool-scope",
|
|
476
|
+
level: "rubric",
|
|
477
|
+
category: "agents",
|
|
478
|
+
check: (s) => {
|
|
479
|
+
const failures = [];
|
|
480
|
+
let checked = 0;
|
|
481
|
+
for (const [file, text] of s.agents) {
|
|
482
|
+
const parsed = parseFrontmatter(text);
|
|
483
|
+
if (!parsed.ok)
|
|
484
|
+
continue;
|
|
485
|
+
checked++;
|
|
486
|
+
if (fieldText(parsed.fields, "tools") === undefined) {
|
|
487
|
+
failures.push({
|
|
488
|
+
subject: `.claude/agents/${file}`,
|
|
489
|
+
message: "declares no `tools`, so it inherits every tool",
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
return { checked, failures };
|
|
494
|
+
},
|
|
495
|
+
};
|
|
496
|
+
const hookTimeout = {
|
|
497
|
+
id: "hook-timeout",
|
|
498
|
+
level: "rubric",
|
|
499
|
+
category: "hooks",
|
|
500
|
+
check: (s) => {
|
|
501
|
+
const registrations = allRegistrations(s);
|
|
502
|
+
return {
|
|
503
|
+
checked: registrations.length,
|
|
504
|
+
failures: registrations
|
|
505
|
+
.filter((registration) => !registration.hasTimeout)
|
|
506
|
+
.map((registration) => ({
|
|
507
|
+
subject: `${registration.event} hook`,
|
|
508
|
+
message: `\`${registration.command}\` has no \`timeout\``,
|
|
509
|
+
})),
|
|
510
|
+
};
|
|
511
|
+
},
|
|
512
|
+
};
|
|
513
|
+
const ruleGlobsLive = {
|
|
514
|
+
id: "rule-globs-live",
|
|
515
|
+
level: "rubric",
|
|
516
|
+
category: "rules",
|
|
517
|
+
check: (s) => {
|
|
518
|
+
const failures = [];
|
|
519
|
+
let checked = 0;
|
|
520
|
+
for (const [file, text] of s.rules) {
|
|
521
|
+
const parsed = parseFrontmatter(text);
|
|
522
|
+
if (!parsed.ok)
|
|
523
|
+
continue;
|
|
524
|
+
for (const glob of fieldList(parsed.fields, "paths") ?? []) {
|
|
525
|
+
checked++;
|
|
526
|
+
const pattern = globToRegExp(glob);
|
|
527
|
+
if (!s.projectFiles.some((path) => pattern.test(path))) {
|
|
528
|
+
failures.push({
|
|
529
|
+
subject: `.claude/rules/${file}`,
|
|
530
|
+
message: `\`paths\` glob "${glob}" matches no file in the project, so the rule never loads`,
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
return { checked, failures };
|
|
536
|
+
},
|
|
537
|
+
};
|
|
538
|
+
const skillReferencesResolve = {
|
|
539
|
+
id: "skill-references-resolve",
|
|
540
|
+
level: "rubric",
|
|
541
|
+
category: "skills",
|
|
542
|
+
check: (s) => {
|
|
543
|
+
const failures = [];
|
|
544
|
+
let checked = 0;
|
|
545
|
+
for (const skill of s.skills) {
|
|
546
|
+
if (skill.skillMd === undefined)
|
|
547
|
+
continue;
|
|
548
|
+
const referenced = new Set([...skill.skillMd.matchAll(REFERENCE_PATH)].map((match) => match[0]));
|
|
549
|
+
for (const path of referenced) {
|
|
550
|
+
checked++;
|
|
551
|
+
if (!skill.files.includes(path)) {
|
|
552
|
+
failures.push({
|
|
553
|
+
subject: `.claude/skills/${skill.name}`,
|
|
554
|
+
message: `SKILL.md points at ${path}, which does not exist`,
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
return { checked, failures };
|
|
560
|
+
},
|
|
561
|
+
};
|
|
562
|
+
/** Every rule, structural first. Order is the order findings are reported in. */
|
|
563
|
+
export const RULES = [
|
|
564
|
+
settingsParses,
|
|
565
|
+
hookDangling,
|
|
566
|
+
hookOrphan,
|
|
567
|
+
hookEntrypoint,
|
|
568
|
+
skillShape,
|
|
569
|
+
agentShape,
|
|
570
|
+
ruleShape,
|
|
571
|
+
claudeMdRefs,
|
|
572
|
+
skillBodySize,
|
|
573
|
+
descriptionSubstance,
|
|
574
|
+
modelPinCurrency,
|
|
575
|
+
agentToolScope,
|
|
576
|
+
hookTimeout,
|
|
577
|
+
ruleGlobsLive,
|
|
578
|
+
skillReferencesResolve,
|
|
579
|
+
];
|
|
580
|
+
//# sourceMappingURL=rules.js.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shapes shared by the harness grader. `structural` findings are defects in
|
|
3
|
+
* the harness's own wiring (a hook that points at nothing, a skill with no
|
|
4
|
+
* frontmatter) and fail a gate; `rubric` findings are quality judgements
|
|
5
|
+
* distilled from Anthropic's published guidance and only ever warn -- a
|
|
6
|
+
* subjective rule must never block a push.
|
|
7
|
+
*/
|
|
8
|
+
export type RuleLevel = "structural" | "rubric";
|
|
9
|
+
export type HarnessCategory = "settings" | "hooks" | "skills" | "agents" | "rules" | "claude-md";
|
|
10
|
+
export declare const HARNESS_CATEGORIES: readonly HarnessCategory[];
|
|
11
|
+
export interface HarnessFinding {
|
|
12
|
+
ruleId: string;
|
|
13
|
+
level: RuleLevel;
|
|
14
|
+
category: HarnessCategory;
|
|
15
|
+
/** The file, skill, agent, or registration the finding is about. */
|
|
16
|
+
subject: string;
|
|
17
|
+
message: string;
|
|
18
|
+
}
|
|
19
|
+
/** How many subjects a set of rules examined, and how many of those failed. */
|
|
20
|
+
export interface CheckTally {
|
|
21
|
+
checked: number;
|
|
22
|
+
failed: number;
|
|
23
|
+
}
|
|
24
|
+
export interface HarnessGrade {
|
|
25
|
+
findings: HarnessFinding[];
|
|
26
|
+
structural: CheckTally;
|
|
27
|
+
/** Rubric tallies per category -- the absolute-quality measurement. */
|
|
28
|
+
rubric: Record<HarnessCategory, CheckTally>;
|
|
29
|
+
/** `1 - failed/checked` over every rubric check; `1` when nothing was checked. */
|
|
30
|
+
rubricScore: number;
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { CapCounts } from "./caps.js";
|
|
2
|
+
import type { FileConflict } from "./conflicts.js";
|
|
3
|
+
import type { HarnessConformance } from "./harness/conformance.js";
|
|
4
|
+
import type { HarnessGrade } from "./harness/types.js";
|
|
5
|
+
import type { ModeDetection } from "./mode.js";
|
|
6
|
+
import type { PackWiring } from "./packs.js";
|
|
7
|
+
import type { ProjectSurvey } from "./survey/survey.js";
|
|
8
|
+
import type { ToolchainConformance } from "./toolchain/conformance.js";
|
|
9
|
+
import type { ToolchainGrade } from "./toolchain/types.js";
|
|
10
|
+
export declare const INVENTORY_SCHEMA_VERSION = 4;
|
|
11
|
+
export interface PackSurvey {
|
|
12
|
+
name: string;
|
|
13
|
+
modes: string[];
|
|
14
|
+
budget: CapCounts;
|
|
15
|
+
/** File collisions a pack's `files/` tree would have against the target -- the same shape `conflicts` uses for `templates/core`. */
|
|
16
|
+
fileConflicts: FileConflict[];
|
|
17
|
+
/** The pack's declared wiring, verbatim and unapplied -- adopt mode never applies it. */
|
|
18
|
+
wiring: PackWiring;
|
|
19
|
+
/** Index-level facts about how the wiring would land, never a verdict. */
|
|
20
|
+
wiringObservations: string[];
|
|
21
|
+
adoptNotes: string | undefined;
|
|
22
|
+
}
|
|
23
|
+
export interface Inventory {
|
|
24
|
+
schemaVersion: number;
|
|
25
|
+
cliVersion: string;
|
|
26
|
+
generatedAt: string;
|
|
27
|
+
modeSignal: string;
|
|
28
|
+
templateRoot: string;
|
|
29
|
+
targetDir: string;
|
|
30
|
+
survey: ProjectSurvey;
|
|
31
|
+
conflicts: FileConflict[];
|
|
32
|
+
packs: PackSurvey[];
|
|
33
|
+
/** Wiring integrity and rubric quality of the project's existing harness. Absent when schemaVersion is below 3. */
|
|
34
|
+
harnessGrade: HarnessGrade;
|
|
35
|
+
/** How far the existing harness has drifted from the baseline's -- information, never a defect. Absent when schemaVersion is below 3. */
|
|
36
|
+
harnessConformance: HarnessConformance;
|
|
37
|
+
/** Wiring integrity and rubric quality of the project's TypeScript toolchain. Absent when schemaVersion is below 4. */
|
|
38
|
+
toolchainGrade: ToolchainGrade;
|
|
39
|
+
/** How far the project's toolchain files have drifted from the baseline's -- information, never a defect. Absent when schemaVersion is below 4. */
|
|
40
|
+
toolchainConformance: ToolchainConformance;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Resolves this CLI package's own declared version. `packageJsonPath`
|
|
44
|
+
* defaults to this module's own `package.json` -- overridable so callers
|
|
45
|
+
* (and tests) can exercise the missing-file/unparseable/non-string-version
|
|
46
|
+
* fallbacks without needing a broken real package.json on disk.
|
|
47
|
+
*/
|
|
48
|
+
export declare function resolveCliVersion(packageJsonPath?: string): string;
|
|
49
|
+
export interface BuildInventoryParams {
|
|
50
|
+
detection: ModeDetection;
|
|
51
|
+
templateRoot: string;
|
|
52
|
+
targetDir: string;
|
|
53
|
+
survey: ProjectSurvey;
|
|
54
|
+
conflicts: FileConflict[];
|
|
55
|
+
packs: PackSurvey[];
|
|
56
|
+
harnessGrade: HarnessGrade;
|
|
57
|
+
toolchainGrade: ToolchainGrade;
|
|
58
|
+
}
|
|
59
|
+
/** Builds the inventory object. Does not write anything -- see `writeInventory`. */
|
|
60
|
+
export declare function buildInventory(params: BuildInventoryParams): Inventory;
|
|
61
|
+
/** Writes `inventory.json` into `groundworkDir`, creating it if needed. */
|
|
62
|
+
export declare function writeInventory(inventory: Inventory, groundworkDir: string): string;
|
|
63
|
+
//# sourceMappingURL=inventory.d.ts.map
|