@erclx/aitk 3.39.0 → 3.40.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/.claude-plugin/plugin.json +1 -1
- package/claude/skills/claude-autoship/SKILL.md +1 -1
- package/docs/agents/commands.md +2 -2
- package/docs/agents/index.md +1 -1
- package/docs/agents/skills-reach.md +10 -3
- package/docs/ai-workflow.md +1 -1
- package/package.json +1 -1
- package/scripts/core/regen-claude-copies.sh +1 -25
- package/scripts/core/verify.sh +1 -1
- package/src/audits/catalog.ts +10 -7
- package/src/claude/skills-audit.ts +6 -0
- package/src/claude/skills-list.ts +41 -1
- package/src/claude/skills-rank.ts +93 -13
- package/src/claude/skills-reach.ts +51 -17
- package/src/commands/claude.ts +86 -34
- package/src/commands/gov.ts +3 -3
- package/standards/skill.md +1 -1
- package/standards/snippets.md +1 -2
|
@@ -89,7 +89,7 @@ Behavior paths carry two spellings, the one a surface authors at and the one it
|
|
|
89
89
|
- `claude/skills/` and `.claude/skills/`
|
|
90
90
|
- `governance/rules/` and `.claude/rules/`
|
|
91
91
|
- `standards/`, which is the authoring root and reaches a reader by resolution rather than by an install, so it carries no `.claude/` spelling
|
|
92
|
-
- `snippets
|
|
92
|
+
- `snippets/`, which reaches a session through the `claude/snippets` symlink rather than an install, so it carries no `.claude/` spelling either
|
|
93
93
|
- `internal/` and `tooling/`, which hold the stack references and the seed documents a target is handed
|
|
94
94
|
- `CLAUDE.md` at the repository root, named as a file because a path prefix reaches nothing that sits in no folder
|
|
95
95
|
|
package/docs/agents/commands.md
CHANGED
|
@@ -49,8 +49,8 @@ Full help: `aitk <command> --help`. Behavior notes for the install and sync verb
|
|
|
49
49
|
| `aitk claude skills audit` | Report both skill corpora against the mechanical rules in `standards/skill.md` |
|
|
50
50
|
| `aitk standards audit` | Report the corpus against the `## Success criterion` gate, failing only on a standard new to the branch (`--json`, `--arrivals-only`) |
|
|
51
51
|
| `aitk claude skills drift` | Name the shipped skill bodies rewritten between a given ref and `HEAD`, and the installed version against the newest published (`--json`) |
|
|
52
|
-
| `aitk claude skills reach` | Report the
|
|
53
|
-
| `aitk claude skills rank` | Score
|
|
52
|
+
| `aitk claude skills reach` | Report the bodies in either skill corpus citing a toolkit path no target project receives, exiting 2 on an unqualified one |
|
|
53
|
+
| `aitk claude skills rank` | Score either skill corpus's descriptions against a case corpus by TF-IDF cosine similarity, reporting rank-one and top-three (`--cases <path>`) |
|
|
54
54
|
| `aitk claude routing` | Report per `CLAUDE.md` section how many bullets name a path and how many of those a path-scoped rule already covers (`--json`) |
|
|
55
55
|
| `aitk gov test-order` | Report where an implementation reached history ahead of the test covering it (`--json`) |
|
|
56
56
|
| `aitk gov superseded` | Report where the tree still asserts a value a changed convention no longer produces, keyed on the value (`--json`) |
|
package/docs/agents/index.md
CHANGED
|
@@ -32,7 +32,7 @@ CLI catalog and invocation rules for agents, split by command domain. Start with
|
|
|
32
32
|
- [Scripting](scripting.md): The runtime catalogs that replace hardcoded names, what each carries, and a headless invocation per domain
|
|
33
33
|
- [Sessions](sessions.md): Resolving live peer sessions to the worktree and branch each holds, the liveness confidence field, the unresolved reasons, and what the read depends on
|
|
34
34
|
- [Skill audit](skills-audit.md): Measuring both skill corpora against standards/skill.md, the checks it reads, the requirement gate that is the only failing one, and the drift verb that names bodies rewritten since a ref
|
|
35
|
-
- [Citation reach](skills-reach.md): Reporting the
|
|
35
|
+
- [Citation reach](skills-reach.md): Reporting the skill bodies that cite a path no target project receives, which corpus the verb reads, the ownership key that decides what counts, the one-word qualifier that marks a citation as decided, and why the verb reports instead of gating
|
|
36
36
|
- [Standard success criteria](standards-audit.md): Reading the corpus against the Success criterion gate, why the check scopes to arrival rather than the whole corpus, and the exit codes it sets
|
|
37
37
|
- [State-scoped risk](state-scoped-risk.md): Reading committed state rather than an arriving change, the shipped-tree corpus the secret scan reads, what it keys on and how a deliberate value is exempted, the advisory check and its network failure mode, and why one gates while the other reports
|
|
38
38
|
- [Superseded values](superseded.md): Reading where the tree still asserts a value a changed convention no longer produces, why the sweep keys on the value rather than the file, the exemption marker, the blind spot it cannot reach, and why it reports rather than gates
|
|
@@ -1,21 +1,26 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Citation reach
|
|
3
|
-
description: Reporting the
|
|
3
|
+
description: Reporting the skill bodies that cite a path no target project receives, which corpus the verb reads, the ownership key that decides what counts, the one-word qualifier that marks a citation as decided, and why the verb reports instead of gating
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Citation reach
|
|
7
7
|
|
|
8
|
-
`aitk claude skills reach [path]` reports every
|
|
8
|
+
`aitk claude skills reach [path]` reports every skill body citing a path that exists in the project and reaches no reader elsewhere. It reads and reports. Repairing what it finds is separate work.
|
|
9
9
|
|
|
10
10
|
```bash
|
|
11
11
|
aitk claude skills reach
|
|
12
12
|
aitk claude skills reach --json
|
|
13
|
+
aitk claude skills reach ~/repos/my-project
|
|
13
14
|
```
|
|
14
15
|
|
|
15
16
|
| Option | Behavior |
|
|
16
17
|
| -------- | ---------------------------------------------------------- |
|
|
17
18
|
| `--json` | Add a machine-readable record on stdout, keeping the frame |
|
|
18
19
|
|
|
20
|
+
## Which corpus it reads
|
|
21
|
+
|
|
22
|
+
`claude/skills/` when the project holds it, and `.claude/skills/` otherwise. The shipped tree wins where both are present, so a toolkit reads what it ships and a project holding only its own skills is measured rather than refused. Every report names the corpus it read on its `Corpus` line and carries it as `corpus` in the `--json` record.
|
|
23
|
+
|
|
19
24
|
## The defect it reads for
|
|
20
25
|
|
|
21
26
|
A plugin skill installs into a project and the toolkit's own tree is not there. A body naming `.claude/context/transcripts.md` resolves for a session running in this repository and sends every other reader to nothing, and no stage asked the question until this one. The shape is wider than one folder: a seed naming a standard with no route and a machine-readable field naming a toolkit-only path are the same claim, true here and false in a target.
|
|
@@ -38,6 +43,8 @@ A cited path counts when it sits under an authoring root no install channel deli
|
|
|
38
43
|
|
|
39
44
|
`src/`, `scripts/`, and bare `docs/` are deliberately outside the list. A body naming one of those is describing the reader's own tree, so listing them reports a correct citation on every run and buries the finding under the pass.
|
|
40
45
|
|
|
46
|
+
`.claude/context/` joins them when the corpus read is a project's own. A seed put those entries there and the project owns them afterward, so a body under `.claude/skills/` naming one points at a file its reader holds. The seed disowning below cannot answer that in a project, since it reads a `tooling/` tree only the toolkit carries.
|
|
47
|
+
|
|
41
48
|
A path a seed installs is disowned twice, under its own name and under the folder spelling it takes once a project splits the entry. A domain that outgrows one file becomes `<domain>/`, which is still the entry the seed delivered, so reporting the split form would fail a project for growing.
|
|
42
49
|
|
|
43
50
|
## The qualifier
|
|
@@ -52,6 +59,6 @@ A line mentioning the toolkit for an unrelated reason exempts a citation on it.
|
|
|
52
59
|
|
|
53
60
|
## Exit codes
|
|
54
61
|
|
|
55
|
-
Exit codes are `0` when every citation names its owner, `1` for a refusal, and `2` when at least one is unqualified. The refusal is a tree carrying
|
|
62
|
+
Exit codes are `0` when every citation names its owner, `1` for a refusal, and `2` when at least one is unqualified. The refusal is a tree carrying neither `claude/skills/` nor `.claude/skills/`, which holds no skill body to measure, and it reports the reason rather than a clean count over nothing.
|
|
56
63
|
|
|
57
64
|
The verb reports rather than gates. A toolkit-scoped instruction is sometimes meant for a session in this repository, so failing a push on one would make the check something to route around. `aitk audits run` registers it with no gating exit for the same reason, and carries `unqualifiedCitations` as its retained count.
|
package/docs/ai-workflow.md
CHANGED
|
@@ -138,7 +138,7 @@ For features on a mature stack, chain the post-plan pipeline in one session. App
|
|
|
138
138
|
|
|
139
139
|
Review findings split by origin before severity is read. One the branch inherited stops the chain, and one the run itself caused is repaired in place at any severity, bounded at a single pass. Origin is causation rather than authorship, so staleness the run induced in a file it never opened counts as its own and the plan's file list bounds what it builds rather than what it may repair.
|
|
140
140
|
|
|
141
|
-
Review is skipped when the diff is prose that only informs: every changed file matches `*.md` or `*.txt`, and none sits under a behavior path. Behavior paths cover skills
|
|
141
|
+
Review is skipped when the diff is prose that only informs: every changed file matches `*.md` or `*.txt`, and none sits under a behavior path. Behavior paths cover skills and rules in both the authoring and the installed spelling, so the list matches whether a repository authors those surfaces or consumed them from the toolkit. Standards, snippets, `internal/`, and `tooling/` carry the authoring spelling alone, since none of the four reaches a session through a `.claude/` copy, and root `CLAUDE.md` is named as a file because a path prefix reaches nothing sitting in no folder.
|
|
142
142
|
|
|
143
143
|
Markdown under one states what an agent does, so a branch touching it reaches review while `docs/` and `wiki/` still skip and stay gated by `docs-sync`, `claude-standards-audit`, and pre-push hooks.
|
|
144
144
|
|
package/package.json
CHANGED
|
@@ -5,30 +5,6 @@ set -o pipefail
|
|
|
5
5
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
6
6
|
PROJECT_ROOT="${PROJECT_ROOT:-$(cd "$SCRIPT_DIR/../.." && pwd)}"
|
|
7
7
|
|
|
8
|
-
mirror_dir() {
|
|
9
|
-
local src="$1"
|
|
10
|
-
local dest="$2"
|
|
11
|
-
local find_args=("${@:3}")
|
|
12
|
-
|
|
13
|
-
rm -rf "$dest"
|
|
14
|
-
while IFS= read -r file; do
|
|
15
|
-
local rel="${file#"$src"/}"
|
|
16
|
-
mkdir -p "$dest/$(dirname "$rel")"
|
|
17
|
-
cp "$file" "$dest/$rel"
|
|
18
|
-
done < <(find "$src" -type f "${find_args[@]}" | sort)
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
mirror_dir "$PROJECT_ROOT/snippets" "$PROJECT_ROOT/.claude/snippets" -name "*.md"
|
|
22
|
-
|
|
23
|
-
# `internal/` is the surface the plugin does not symlink. Mirrored on its own so
|
|
24
|
-
# toolkit sessions read it at a `.claude/` path like every other consumed copy.
|
|
25
|
-
# `internal/rules/` is excluded because it lands in `.claude/rules/` below, and
|
|
26
|
-
# mirroring it here too would publish each rule at a second inert path. The
|
|
27
|
-
# exclusion is anchored to that one folder, since an unanchored `*/rules/*` would
|
|
28
|
-
# also drop a later `internal/standards/rules/` and report nothing for it.
|
|
29
|
-
mirror_dir "$PROJECT_ROOT/internal" "$PROJECT_ROOT/.claude/internal" -name "*.md" -not -path "$PROJECT_ROOT/internal/rules/*"
|
|
30
|
-
|
|
31
8
|
# `.claude/rules/` is a subset rather than a mirror, so it resolves through the
|
|
32
|
-
# stack machinery
|
|
33
|
-
# subset is `internal/governance.toml`.
|
|
9
|
+
# stack machinery. The record naming the subset is `internal/governance.toml`.
|
|
34
10
|
bun "$PROJECT_ROOT/src/cli.ts" gov regen --root "$PROJECT_ROOT"
|
package/scripts/core/verify.sh
CHANGED
|
@@ -261,7 +261,7 @@ main() {
|
|
|
261
261
|
|
|
262
262
|
log_step "Consumed copies"
|
|
263
263
|
run_check "bash $PROJECT_ROOT/scripts/core/regen-claude-copies.sh" "Consumed-copy regen failed"
|
|
264
|
-
assert_no_drift ".claude/
|
|
264
|
+
assert_no_drift ".claude/rules" "Consumed copies drifted. Run bun run check and commit .claude/rules."
|
|
265
265
|
log_info "Consumed copies clean"
|
|
266
266
|
|
|
267
267
|
# Only the HTML is asserted. The PNG beside it is a chromium render whose bytes
|
package/src/audits/catalog.ts
CHANGED
|
@@ -530,10 +530,11 @@ export const AUDITS: readonly AuditSpec[] = [
|
|
|
530
530
|
// The one reason this verb refuses for, and it is an absence rather than a
|
|
531
531
|
// break. A tracked corpus normally allows nothing, since a tree that ships
|
|
532
532
|
// to targets and cannot be found is a broken checkout, and this is the
|
|
533
|
-
// second exception on the same test the secret scan takes:
|
|
534
|
-
// `claude/skills
|
|
535
|
-
//
|
|
536
|
-
//
|
|
533
|
+
// second exception on the same test the secret scan takes: a project
|
|
534
|
+
// carrying neither `claude/skills/` nor `.claude/skills/` has adopted no
|
|
535
|
+
// skill convention, so without the allowance it reports the verb
|
|
536
|
+
// unmeasured on every run and never changes, which is the permanent signal
|
|
537
|
+
// the per-machine allowance exists against.
|
|
537
538
|
absentReasons: ['no-skills'] satisfies ReachRefusal[],
|
|
538
539
|
counts: reachCounts,
|
|
539
540
|
},
|
|
@@ -547,9 +548,11 @@ export const AUDITS: readonly AuditSpec[] = [
|
|
|
547
548
|
// moved rank would teach a contributor to route around the stage.
|
|
548
549
|
gatingExits: [],
|
|
549
550
|
corpus: 'tracked',
|
|
550
|
-
// The one reason this
|
|
551
|
-
// reason the reach check's is: a project carrying
|
|
552
|
-
// adopted no skill convention this audit reads.
|
|
551
|
+
// The one reason this run refuses for, and it is an absence for the same
|
|
552
|
+
// reason the reach check's is: a project carrying neither `claude/skills/`
|
|
553
|
+
// nor `.claude/skills/` has adopted no skill convention this audit reads.
|
|
554
|
+
// The two case-corpus refusals never reach here, since they need `--cases`
|
|
555
|
+
// and this argv passes none.
|
|
553
556
|
absentReasons: ['no-skills'] satisfies RankRefusal[],
|
|
554
557
|
counts: rankCounts,
|
|
555
558
|
},
|
|
@@ -43,6 +43,12 @@ export interface SkillFinding {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
export interface CorpusReport {
|
|
46
|
+
/**
|
|
47
|
+
* Left as `join` produced it, where `SkillsCorpus.rel` in `skills-list.ts`
|
|
48
|
+
* normalizes the same spelling to POSIX. This one is an existing JSON field
|
|
49
|
+
* a caller already reads, so the split holds until a branch reading this verb
|
|
50
|
+
* is the one to close it.
|
|
51
|
+
*/
|
|
46
52
|
readonly rel: string
|
|
47
53
|
readonly skills: number
|
|
48
54
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs'
|
|
2
2
|
import { dirname, join } from 'node:path'
|
|
3
|
+
import { CORPORA } from '@/claude/skills-audit'
|
|
3
4
|
|
|
4
5
|
const FRONTMATTER = /^---\n([\s\S]*?)\n---/
|
|
5
6
|
|
|
@@ -9,6 +10,38 @@ export interface SkillListing {
|
|
|
9
10
|
readonly requirement: boolean
|
|
10
11
|
}
|
|
11
12
|
|
|
13
|
+
export interface SkillsCorpus {
|
|
14
|
+
/**
|
|
15
|
+
* The corpus spelling in POSIX form, so a report reads the same on Windows.
|
|
16
|
+
* `CorpusReport.rel` in `skills-audit.ts` is the same spelling left as `join`
|
|
17
|
+
* produced it, so the two verbs disagree there. Normalizing it is a contract
|
|
18
|
+
* change on an existing JSON field and belongs to a branch reading the audit.
|
|
19
|
+
*/
|
|
20
|
+
readonly rel: string
|
|
21
|
+
/** The folder a listing reads, absolute against the root it was resolved at. */
|
|
22
|
+
readonly dir: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The skill corpus a measure reads at a given root: the shipped tree in this
|
|
27
|
+
* repository and a target's own `.claude/skills/` in a project that consumes
|
|
28
|
+
* it. `CORPORA` order settles a tree carrying both, so every reading taken
|
|
29
|
+
* here still comes from `claude/skills/`.
|
|
30
|
+
*
|
|
31
|
+
* Kept apart from `listSkills` deliberately. `src/counts/catalogs.ts` counts
|
|
32
|
+
* the shipped catalog through that function, so teaching it to read both
|
|
33
|
+
* corpora would move the reported total off the tree that installs and
|
|
34
|
+
* falsify every sentence in the corpus stating it.
|
|
35
|
+
*/
|
|
36
|
+
export function resolveSkillsCorpus(root: string): SkillsCorpus | undefined {
|
|
37
|
+
for (const rel of CORPORA) {
|
|
38
|
+
const dir = join(root, rel)
|
|
39
|
+
if (existsSync(dir)) return { rel: rel.replaceAll('\\', '/'), dir }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return undefined
|
|
43
|
+
}
|
|
44
|
+
|
|
12
45
|
/**
|
|
13
46
|
* Enumerates the plugin skill catalog, which is the corpus under `claude/`
|
|
14
47
|
* rather than the internal skills under `.claude/`. Only the former installs
|
|
@@ -22,7 +55,14 @@ export interface SkillListing {
|
|
|
22
55
|
* `aitk claude skills audit` is what fails on it, across both corpora.
|
|
23
56
|
*/
|
|
24
57
|
export function listSkills(root: string): SkillListing[] {
|
|
25
|
-
|
|
58
|
+
return listSkillsAt(join(root, 'claude', 'skills'))
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The same enumeration against a corpus folder the caller already resolved,
|
|
63
|
+
* which is what `resolveSkillsCorpus` hands a measure that reaches a target.
|
|
64
|
+
*/
|
|
65
|
+
export function listSkillsAt(skillsRoot: string): SkillListing[] {
|
|
26
66
|
if (!existsSync(skillsRoot)) return []
|
|
27
67
|
|
|
28
68
|
const paths = [
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs'
|
|
2
|
-
import {
|
|
3
|
-
import { listSkills } from '@/claude/skills-list'
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
2
|
+
import { listSkillsAt, resolveSkillsCorpus } from '@/claude/skills-list'
|
|
4
3
|
|
|
5
4
|
/**
|
|
6
5
|
* Whether a prompt reaches the right skill, measured by TF-IDF cosine
|
|
@@ -14,8 +13,6 @@ import { listSkills } from '@/claude/skills-list'
|
|
|
14
13
|
* tracks on a cadence.
|
|
15
14
|
*/
|
|
16
15
|
|
|
17
|
-
const SKILLS_DIR = join('claude', 'skills')
|
|
18
|
-
|
|
19
16
|
const STOP = new Set(
|
|
20
17
|
'a about after again all also and any are as at be before but by can do does for from has have help how in into is it its just make not of on or our so that the then there these this to use used uses using want was what when where which who why with you your run'.split(
|
|
21
18
|
' ',
|
|
@@ -44,11 +41,25 @@ export interface Miss {
|
|
|
44
41
|
}
|
|
45
42
|
|
|
46
43
|
/** Why a measure produced no reading, which is never the same as a clean one. */
|
|
47
|
-
export type RankRefusal = 'no-skills'
|
|
44
|
+
export type RankRefusal = 'no-skills' | 'no-cases' | 'bad-cases'
|
|
45
|
+
|
|
46
|
+
/** The refusals a case corpus read produces, which the scan itself cannot raise. */
|
|
47
|
+
export type CaseCorpusRefusal = Extract<RankRefusal, 'no-cases' | 'bad-cases'>
|
|
48
|
+
|
|
49
|
+
export type CaseCorpusReport =
|
|
50
|
+
| { readonly kind: 'cases'; readonly cases: readonly SkillCase[] }
|
|
51
|
+
| {
|
|
52
|
+
readonly kind: 'refused'
|
|
53
|
+
readonly reason: CaseCorpusRefusal
|
|
54
|
+
/** What the caller has to change, which the reason alone never says. */
|
|
55
|
+
readonly detail: string
|
|
56
|
+
}
|
|
48
57
|
|
|
49
58
|
export type RankReport =
|
|
50
59
|
| {
|
|
51
60
|
readonly kind: 'measured'
|
|
61
|
+
/** The corpus spelling measured, since a root can carry either one. */
|
|
62
|
+
readonly corpus: string
|
|
52
63
|
readonly skills: number
|
|
53
64
|
readonly cases: number
|
|
54
65
|
readonly rank1: number
|
|
@@ -59,6 +70,67 @@ export type RankReport =
|
|
|
59
70
|
}
|
|
60
71
|
| { readonly kind: 'refused'; readonly reason: RankRefusal }
|
|
61
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Reads a project's own case corpus, which is JSON in the shape `SKILL_CASES`
|
|
75
|
+
* already holds. A target authors its own skills and its own vocabulary, so
|
|
76
|
+
* the toolkit corpus answers a question no other project asked.
|
|
77
|
+
*
|
|
78
|
+
* No standard stands behind the shape until a third project needs one, so
|
|
79
|
+
* every way the file fails is reported with what to change rather than
|
|
80
|
+
* measured against a spec. An empty array refuses for the reason a missing
|
|
81
|
+
* file does: a corpus of nothing scores 0 of 0 and reads as a clean pass.
|
|
82
|
+
*/
|
|
83
|
+
export function loadCaseCorpus(path: string): CaseCorpusReport {
|
|
84
|
+
if (!existsSync(path)) {
|
|
85
|
+
return { kind: 'refused', reason: 'no-cases', detail: path }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
let parsed: unknown
|
|
89
|
+
try {
|
|
90
|
+
parsed = JSON.parse(readFileSync(path, 'utf8'))
|
|
91
|
+
} catch (error) {
|
|
92
|
+
const detail = error instanceof Error ? error.message : String(error)
|
|
93
|
+
return { kind: 'refused', reason: 'bad-cases', detail }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!Array.isArray(parsed)) {
|
|
97
|
+
return {
|
|
98
|
+
kind: 'refused',
|
|
99
|
+
reason: 'bad-cases',
|
|
100
|
+
detail: 'the file holds something other than an array of cases',
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const cases: SkillCase[] = []
|
|
105
|
+
for (const [index, entry] of parsed.entries()) {
|
|
106
|
+
const record = entry as Record<string, unknown> | null
|
|
107
|
+
if (
|
|
108
|
+
typeof record !== 'object' ||
|
|
109
|
+
record === null ||
|
|
110
|
+
typeof record.prompt !== 'string' ||
|
|
111
|
+
typeof record.expect !== 'string'
|
|
112
|
+
) {
|
|
113
|
+
return {
|
|
114
|
+
kind: 'refused',
|
|
115
|
+
reason: 'bad-cases',
|
|
116
|
+
detail: `case ${index} carries no string prompt or expect`,
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
cases.push({ prompt: record.prompt, expect: record.expect })
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (cases.length === 0) {
|
|
124
|
+
return {
|
|
125
|
+
kind: 'refused',
|
|
126
|
+
reason: 'bad-cases',
|
|
127
|
+
detail: 'the file holds no cases at all',
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return { kind: 'cases', cases }
|
|
132
|
+
}
|
|
133
|
+
|
|
62
134
|
/**
|
|
63
135
|
* Every shipped skill's frontmatter description, read the way a prompt is
|
|
64
136
|
* matched against it: whole, including the quoted trigger phrases it states.
|
|
@@ -66,7 +138,13 @@ export type RankReport =
|
|
|
66
138
|
* and never wins a rank, so it is dropped rather than scored on nothing.
|
|
67
139
|
*/
|
|
68
140
|
export function loadCatalog(root: string): RankedSkill[] {
|
|
69
|
-
|
|
141
|
+
const corpus = resolveSkillsCorpus(root)
|
|
142
|
+
return corpus === undefined ? [] : loadCatalogAt(corpus.dir)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** The same read against a corpus folder the caller already resolved. */
|
|
146
|
+
export function loadCatalogAt(skillsRoot: string): RankedSkill[] {
|
|
147
|
+
return listSkillsAt(skillsRoot)
|
|
70
148
|
.filter((skill) => skill.description !== '')
|
|
71
149
|
.map((skill) => ({ name: skill.name, description: skill.description }))
|
|
72
150
|
}
|
|
@@ -214,22 +292,24 @@ export function measureCases(
|
|
|
214
292
|
}
|
|
215
293
|
|
|
216
294
|
/**
|
|
217
|
-
* Reads
|
|
218
|
-
*
|
|
219
|
-
* the reach and audit verbs, so a linked worktree reads its own
|
|
295
|
+
* Reads whichever skill corpus the root carries and scores it against the
|
|
296
|
+
* given cases. Measures the cwd's catalog rather than the toolkit root,
|
|
297
|
+
* matching the reach and audit verbs, so a linked worktree reads its own
|
|
298
|
+
* branch and a target reads the skills it wrote itself.
|
|
220
299
|
*/
|
|
221
300
|
export function scanRank(
|
|
222
301
|
root: string,
|
|
223
302
|
cases: readonly SkillCase[],
|
|
224
303
|
): RankReport {
|
|
225
|
-
const
|
|
226
|
-
if (
|
|
304
|
+
const corpus = resolveSkillsCorpus(root)
|
|
305
|
+
if (corpus === undefined) return { kind: 'refused', reason: 'no-skills' }
|
|
227
306
|
|
|
228
|
-
const catalog =
|
|
307
|
+
const catalog = loadCatalogAt(corpus.dir)
|
|
229
308
|
const { rank1, top3, misses, unmeasurable } = measureCases(catalog, cases)
|
|
230
309
|
|
|
231
310
|
return {
|
|
232
311
|
kind: 'measured',
|
|
312
|
+
corpus: corpus.rel,
|
|
233
313
|
skills: catalog.length,
|
|
234
314
|
cases: cases.length,
|
|
235
315
|
rank1,
|
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs'
|
|
2
2
|
import { join } from 'node:path'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* The tree that installs into a target. The internal skills under `.claude/`
|
|
6
|
-
* never leave this repository, so a citation there is read by a session that
|
|
7
|
-
* already has the file and cannot be a reach defect.
|
|
8
|
-
*/
|
|
9
|
-
const SHIPPED_SKILLS = join('claude', 'skills')
|
|
3
|
+
import { resolveSkillsCorpus } from '@/claude/skills-list'
|
|
10
4
|
|
|
11
5
|
/**
|
|
12
6
|
* The authoring roots this repository owns and no install channel delivers.
|
|
@@ -71,6 +65,8 @@ export type ReachRefusal = 'no-skills'
|
|
|
71
65
|
export type ReachReport =
|
|
72
66
|
| {
|
|
73
67
|
readonly kind: 'measured'
|
|
68
|
+
/** The corpus spelling read, since a root can carry either one. */
|
|
69
|
+
readonly corpus: string
|
|
74
70
|
/** Files opened, so a report can state what the verdict covers. */
|
|
75
71
|
readonly bodies: number
|
|
76
72
|
readonly qualified: readonly Citation[]
|
|
@@ -82,6 +78,25 @@ export function isQualified(line: string): boolean {
|
|
|
82
78
|
return QUALIFIER.test(line)
|
|
83
79
|
}
|
|
84
80
|
|
|
81
|
+
/**
|
|
82
|
+
* The roots that belong to the toolkit rather than to the reader, read
|
|
83
|
+
* against the corpus being measured.
|
|
84
|
+
*
|
|
85
|
+
* A target's `.claude/context/` is the reader's own tree. A seed put the
|
|
86
|
+
* entries there and the project owns them afterward, so a body under
|
|
87
|
+
* `.claude/skills/` citing one names a file its reader holds. Measuring it
|
|
88
|
+
* would report a correct citation on every run, which is exactly why `src/`
|
|
89
|
+
* and `scripts/` are absent from the list above. In this repository the seed
|
|
90
|
+
* tree settles the same question through `readReceivedPaths`, which a target
|
|
91
|
+
* carrying no `tooling/` folder cannot answer at all, so the root comes out
|
|
92
|
+
* by corpus instead.
|
|
93
|
+
*/
|
|
94
|
+
export function authoringRootsFor(corpus: string): readonly string[] {
|
|
95
|
+
if (!corpus.startsWith('.claude/')) return AUTHORING_ROOTS
|
|
96
|
+
|
|
97
|
+
return AUTHORING_ROOTS.filter((root) => !root.startsWith('.claude/'))
|
|
98
|
+
}
|
|
99
|
+
|
|
85
100
|
/**
|
|
86
101
|
* Every path a seed lands on in a target, spelled the way a body would cite it.
|
|
87
102
|
*
|
|
@@ -113,7 +128,11 @@ export function readReceivedPaths(root: string): Set<string> {
|
|
|
113
128
|
* outgrows one file becomes `<domain>/`, which is still the entry the seed
|
|
114
129
|
* delivered, so reporting the split form would fail a target for growing.
|
|
115
130
|
*/
|
|
116
|
-
export function isToolkitOwned(
|
|
131
|
+
export function isToolkitOwned(
|
|
132
|
+
path: string,
|
|
133
|
+
received: Set<string>,
|
|
134
|
+
roots: readonly string[] = AUTHORING_ROOTS,
|
|
135
|
+
): boolean {
|
|
117
136
|
if (received.has(path)) return false
|
|
118
137
|
|
|
119
138
|
for (const seeded of received) {
|
|
@@ -121,7 +140,7 @@ export function isToolkitOwned(path: string, received: Set<string>): boolean {
|
|
|
121
140
|
if (stem !== seeded && path.startsWith(`${stem}/`)) return false
|
|
122
141
|
}
|
|
123
142
|
|
|
124
|
-
return
|
|
143
|
+
return roots.some((prefix) => path.startsWith(prefix))
|
|
125
144
|
}
|
|
126
145
|
|
|
127
146
|
/**
|
|
@@ -135,6 +154,7 @@ export function citationsIn(
|
|
|
135
154
|
file: string,
|
|
136
155
|
text: string,
|
|
137
156
|
received: Set<string>,
|
|
157
|
+
roots: readonly string[] = AUTHORING_ROOTS,
|
|
138
158
|
): Citation[] {
|
|
139
159
|
const citations: Citation[] = []
|
|
140
160
|
|
|
@@ -144,7 +164,7 @@ export function citationsIn(
|
|
|
144
164
|
for (const match of line.matchAll(TOKEN)) {
|
|
145
165
|
const path = match[1]
|
|
146
166
|
if (!CONCRETE.test(path) || !path.includes('/')) continue
|
|
147
|
-
if (!isToolkitOwned(path, received)) continue
|
|
167
|
+
if (!isToolkitOwned(path, received, roots)) continue
|
|
148
168
|
|
|
149
169
|
citations.push({ file, line: index + 1, path, qualified })
|
|
150
170
|
}
|
|
@@ -154,20 +174,27 @@ export function citationsIn(
|
|
|
154
174
|
}
|
|
155
175
|
|
|
156
176
|
/**
|
|
157
|
-
* Reads every
|
|
177
|
+
* Reads every body in the root's skill corpus for a path its reader cannot
|
|
178
|
+
* open.
|
|
158
179
|
*
|
|
159
180
|
* A citation of a path this repository does not hold is dropped rather than
|
|
160
181
|
* reported. The measure asks whether a claim true here is false in a target,
|
|
161
182
|
* and a path true in neither is a different defect that `aitk context audit`
|
|
162
183
|
* already reports against its own corpus.
|
|
184
|
+
*
|
|
185
|
+
* `resolveSkillsCorpus` prefers `claude/skills/`, which is the tree that
|
|
186
|
+
* installs into a target, so this repository's own reading is the one it
|
|
187
|
+
* always was. A project carrying `.claude/skills/` alone has no shipped tree
|
|
188
|
+
* and its own skills are the whole corpus a reader there opens.
|
|
163
189
|
*/
|
|
164
190
|
export function scanReach(root: string): ReachReport {
|
|
165
|
-
const
|
|
166
|
-
if (
|
|
191
|
+
const corpus = resolveSkillsCorpus(root)
|
|
192
|
+
if (corpus === undefined) return { kind: 'refused', reason: 'no-skills' }
|
|
167
193
|
|
|
168
194
|
const received = readReceivedPaths(root)
|
|
195
|
+
const roots = authoringRootsFor(corpus.rel)
|
|
169
196
|
const files = [
|
|
170
|
-
...new Bun.Glob('**/*.md').scanSync({ cwd:
|
|
197
|
+
...new Bun.Glob('**/*.md').scanSync({ cwd: corpus.dir, onlyFiles: true }),
|
|
171
198
|
].sort()
|
|
172
199
|
|
|
173
200
|
const qualified: Citation[] = []
|
|
@@ -175,12 +202,13 @@ export function scanReach(root: string): ReachReport {
|
|
|
175
202
|
|
|
176
203
|
for (const file of files) {
|
|
177
204
|
const posix = file.replaceAll('\\', '/')
|
|
178
|
-
const text = readFileSync(join(
|
|
205
|
+
const text = readFileSync(join(corpus.dir, file), 'utf8')
|
|
179
206
|
|
|
180
207
|
for (const citation of citationsIn(
|
|
181
|
-
`${
|
|
208
|
+
`${corpus.rel}/${posix}`,
|
|
182
209
|
text,
|
|
183
210
|
received,
|
|
211
|
+
roots,
|
|
184
212
|
)) {
|
|
185
213
|
if (!existsSync(join(root, citation.path))) continue
|
|
186
214
|
|
|
@@ -189,5 +217,11 @@ export function scanReach(root: string): ReachReport {
|
|
|
189
217
|
}
|
|
190
218
|
}
|
|
191
219
|
|
|
192
|
-
return {
|
|
220
|
+
return {
|
|
221
|
+
kind: 'measured',
|
|
222
|
+
corpus: corpus.rel,
|
|
223
|
+
bodies: files.length,
|
|
224
|
+
qualified,
|
|
225
|
+
unqualified,
|
|
226
|
+
}
|
|
193
227
|
}
|
package/src/commands/claude.ts
CHANGED
|
@@ -36,9 +36,11 @@ import {
|
|
|
36
36
|
} from '@/claude/skills-reach'
|
|
37
37
|
import { SKILL_CASES } from '@/claude/cases/all'
|
|
38
38
|
import {
|
|
39
|
+
loadCaseCorpus,
|
|
39
40
|
type RankRefusal,
|
|
40
41
|
type RankReport,
|
|
41
42
|
scanRank,
|
|
43
|
+
type SkillCase,
|
|
42
44
|
} from '@/claude/skills-rank'
|
|
43
45
|
import {
|
|
44
46
|
planSettings,
|
|
@@ -93,6 +95,7 @@ interface SkillsReachOptions {
|
|
|
93
95
|
|
|
94
96
|
interface SkillsRankOptions {
|
|
95
97
|
readonly json?: boolean
|
|
98
|
+
readonly cases?: string
|
|
96
99
|
}
|
|
97
100
|
|
|
98
101
|
interface RoutingOptions {
|
|
@@ -331,8 +334,8 @@ export function register(program: Command): void {
|
|
|
331
334
|
|
|
332
335
|
skills
|
|
333
336
|
.command('reach')
|
|
334
|
-
.description('Report
|
|
335
|
-
.argument('[path]', '
|
|
337
|
+
.description('Report skill bodies citing a path no target receives')
|
|
338
|
+
.argument('[path]', 'Project root, defaulting to the current directory')
|
|
336
339
|
.helpOption('-h, --help', 'Show this help message')
|
|
337
340
|
.option('--json', 'Add a machine-readable record on stdout')
|
|
338
341
|
.addHelpText(
|
|
@@ -341,10 +344,12 @@ export function register(program: Command): void {
|
|
|
341
344
|
'',
|
|
342
345
|
'Scope:',
|
|
343
346
|
' Every markdown file under claude/skills/, which is the tree that',
|
|
344
|
-
' installs into a target
|
|
345
|
-
'
|
|
347
|
+
' installs into a target, or under .claude/skills/ in a project',
|
|
348
|
+
' carrying that corpus alone. A cited path counts when it sits under',
|
|
349
|
+
' an authoring root no install channel delivers and the project',
|
|
346
350
|
' holds it. A path under src/, scripts/, or bare docs/ names the',
|
|
347
|
-
" reader's own tree and is not measured.",
|
|
351
|
+
" reader's own tree and is not measured, and .claude/context/ joins",
|
|
352
|
+
" them when the corpus read is a project's own.",
|
|
348
353
|
'',
|
|
349
354
|
'Exit codes:',
|
|
350
355
|
' 0 every citation names the toolkit as the owner',
|
|
@@ -358,6 +363,7 @@ export function register(program: Command): void {
|
|
|
358
363
|
'Examples:',
|
|
359
364
|
' aitk claude skills reach',
|
|
360
365
|
' aitk claude skills reach --json',
|
|
366
|
+
' aitk claude skills reach ~/repos/my-project',
|
|
361
367
|
'',
|
|
362
368
|
].join('\n'),
|
|
363
369
|
)
|
|
@@ -367,21 +373,33 @@ export function register(program: Command): void {
|
|
|
367
373
|
|
|
368
374
|
skills
|
|
369
375
|
.command('rank')
|
|
370
|
-
.description('Score
|
|
371
|
-
.argument('[path]', '
|
|
376
|
+
.description('Score a skill catalog against a routing case corpus')
|
|
377
|
+
.argument('[path]', 'Project root, defaulting to the current directory')
|
|
372
378
|
.helpOption('-h, --help', 'Show this help message')
|
|
373
379
|
.option('--json', 'Add a machine-readable record on stdout')
|
|
380
|
+
.option(
|
|
381
|
+
'--cases <path>',
|
|
382
|
+
"A project's own case corpus as JSON, replacing the toolkit's",
|
|
383
|
+
)
|
|
374
384
|
.addHelpText(
|
|
375
385
|
'after',
|
|
376
386
|
[
|
|
377
387
|
'',
|
|
378
388
|
'Scope:',
|
|
379
|
-
' TF-IDF cosine similarity over every
|
|
380
|
-
'
|
|
381
|
-
'
|
|
382
|
-
'
|
|
383
|
-
'
|
|
384
|
-
'
|
|
389
|
+
' TF-IDF cosine similarity over every SKILL.md frontmatter',
|
|
390
|
+
' description under claude/skills/, or under .claude/skills/ in a',
|
|
391
|
+
' project carrying that corpus alone, scored against the',
|
|
392
|
+
' hand-authored corpus at src/claude/cases/. A necessary condition',
|
|
393
|
+
' rather than a report of real routing behavior: it asks whether the',
|
|
394
|
+
' descriptions are separable by the words they use, and Claude Code',
|
|
395
|
+
' does not route this way.',
|
|
396
|
+
'',
|
|
397
|
+
'The case corpus:',
|
|
398
|
+
' --cases takes a JSON array of { "prompt", "expect" } objects, the',
|
|
399
|
+
' shape src/claude/cases/ already holds, where expect is a skill',
|
|
400
|
+
" folder name. A project's own skills need its own prompts, so the",
|
|
401
|
+
' toolkit corpus is not a default anything else can measure against.',
|
|
402
|
+
' No standard stands behind the file until a third project needs one.',
|
|
385
403
|
'',
|
|
386
404
|
'Exit codes:',
|
|
387
405
|
' 0 the catalog was read, whether or not a case missed rank one',
|
|
@@ -394,6 +412,7 @@ export function register(program: Command): void {
|
|
|
394
412
|
'Examples:',
|
|
395
413
|
' aitk claude skills rank',
|
|
396
414
|
' aitk claude skills rank --json',
|
|
415
|
+
' aitk claude skills rank ~/repos/my-project --cases cases.json',
|
|
397
416
|
'',
|
|
398
417
|
].join('\n'),
|
|
399
418
|
)
|
|
@@ -794,12 +813,14 @@ function reportRouting(
|
|
|
794
813
|
/** What a reader does about the one way the corpus fails to build. */
|
|
795
814
|
const REACH_REFUSALS: Record<ReachRefusal, string> = {
|
|
796
815
|
'no-skills':
|
|
797
|
-
'
|
|
816
|
+
'Neither claude/skills/ nor .claude/skills/ here, so this project carries no skill body to measure.',
|
|
798
817
|
}
|
|
799
818
|
|
|
800
819
|
/**
|
|
801
820
|
* Measures the cwd rather than the toolkit root, matching the audit and drift
|
|
802
|
-
* verbs, so a linked worktree reads its own branch instead of `main
|
|
821
|
+
* verbs, so a linked worktree reads its own branch instead of `main`, and a
|
|
822
|
+
* target carrying `.claude/skills/` alone is in scope the way the audit
|
|
823
|
+
* already has it.
|
|
803
824
|
*/
|
|
804
825
|
function runSkillsReach(
|
|
805
826
|
path: string | undefined,
|
|
@@ -830,6 +851,7 @@ function runSkillsReach(
|
|
|
830
851
|
process.stdout.write(
|
|
831
852
|
`${JSON.stringify({
|
|
832
853
|
root,
|
|
854
|
+
corpus: report.corpus,
|
|
833
855
|
bodies: report.bodies,
|
|
834
856
|
qualified: report.qualified,
|
|
835
857
|
unqualified: report.unqualified,
|
|
@@ -848,7 +870,7 @@ function runSkillsReach(
|
|
|
848
870
|
function reportReach(report: Extract<ReachReport, { kind: 'measured' }>): void {
|
|
849
871
|
logStep('Corpus')
|
|
850
872
|
logInfo(
|
|
851
|
-
`${plural(report.bodies, '
|
|
873
|
+
`${report.corpus}: ${plural(report.bodies, 'file')} read, ${plural(report.qualified.length, 'citation')} already naming the toolkit as owner`,
|
|
852
874
|
)
|
|
853
875
|
|
|
854
876
|
logStep('Unqualified citations')
|
|
@@ -865,37 +887,44 @@ function reportReach(report: Extract<ReachReport, { kind: 'measured' }>): void {
|
|
|
865
887
|
)
|
|
866
888
|
}
|
|
867
889
|
|
|
868
|
-
/** What a reader does about
|
|
890
|
+
/** What a reader does about each way the measure fails to build. */
|
|
869
891
|
const RANK_REFUSALS: Record<RankRefusal, string> = {
|
|
870
892
|
'no-skills':
|
|
871
|
-
'
|
|
893
|
+
'Neither claude/skills/ nor .claude/skills/ here, so this project carries no skill body to measure.',
|
|
894
|
+
'no-cases': 'No case corpus at the path given to --cases.',
|
|
895
|
+
'bad-cases':
|
|
896
|
+
'The case corpus is not a JSON array of { "prompt", "expect" } objects.',
|
|
872
897
|
}
|
|
873
898
|
|
|
874
899
|
/**
|
|
875
900
|
* Measures the cwd rather than the toolkit root, matching the reach and audit
|
|
876
|
-
* verbs, so a linked worktree reads its own branch instead of `main
|
|
877
|
-
*
|
|
878
|
-
*
|
|
901
|
+
* verbs, so a linked worktree reads its own branch instead of `main`, and a
|
|
902
|
+
* target carrying `.claude/skills/` alone is in scope.
|
|
903
|
+
*
|
|
904
|
+
* The toolkit's own cases are the default and answer for this catalog alone.
|
|
905
|
+
* A project measuring its own skills supplies its own prompts through
|
|
906
|
+
* `--cases`, since a corpus written against skills it did not author scores
|
|
907
|
+
* vocabulary it never uses.
|
|
879
908
|
*/
|
|
880
909
|
function runSkillsRank(
|
|
881
910
|
path: string | undefined,
|
|
882
911
|
opts: SkillsRankOptions,
|
|
883
912
|
): number {
|
|
884
913
|
const root = resolve(path ?? process.cwd())
|
|
885
|
-
const report = scanRank(root, SKILL_CASES)
|
|
886
914
|
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
root,
|
|
893
|
-
reason: report.reason,
|
|
894
|
-
message: RANK_REFUSALS[report.reason],
|
|
895
|
-
})}\n`,
|
|
896
|
-
)
|
|
915
|
+
let cases: readonly SkillCase[] = SKILL_CASES
|
|
916
|
+
if (opts.cases !== undefined) {
|
|
917
|
+
const corpus = loadCaseCorpus(resolve(opts.cases))
|
|
918
|
+
if (corpus.kind === 'refused') {
|
|
919
|
+
return refuseRank(root, corpus.reason, corpus.detail, opts)
|
|
897
920
|
}
|
|
898
|
-
|
|
921
|
+
cases = corpus.cases
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
const report = scanRank(root, cases)
|
|
925
|
+
|
|
926
|
+
if (report.kind === 'refused') {
|
|
927
|
+
return refuseRank(root, report.reason, '', opts)
|
|
899
928
|
}
|
|
900
929
|
|
|
901
930
|
intro('aitk claude skills rank')
|
|
@@ -906,6 +935,7 @@ function runSkillsRank(
|
|
|
906
935
|
process.stdout.write(
|
|
907
936
|
`${JSON.stringify({
|
|
908
937
|
root,
|
|
938
|
+
corpus: report.corpus,
|
|
909
939
|
skills: report.skills,
|
|
910
940
|
cases: report.cases,
|
|
911
941
|
rank1: report.rank1,
|
|
@@ -919,6 +949,28 @@ function runSkillsRank(
|
|
|
919
949
|
return 0
|
|
920
950
|
}
|
|
921
951
|
|
|
952
|
+
/**
|
|
953
|
+
* Carries the detail beside the reason, since three refusals share one verb
|
|
954
|
+
* and only one of them names a path the caller can correct without it.
|
|
955
|
+
*/
|
|
956
|
+
function refuseRank(
|
|
957
|
+
root: string,
|
|
958
|
+
reason: RankRefusal,
|
|
959
|
+
detail: string,
|
|
960
|
+
opts: SkillsRankOptions,
|
|
961
|
+
): number {
|
|
962
|
+
const message = RANK_REFUSALS[reason]
|
|
963
|
+
frameError(detail === '' ? message : `${message} ${detail}`)
|
|
964
|
+
|
|
965
|
+
if (opts.json) {
|
|
966
|
+
process.stdout.write(
|
|
967
|
+
`${JSON.stringify({ root, reason, message, detail })}\n`,
|
|
968
|
+
)
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
return 1
|
|
972
|
+
}
|
|
973
|
+
|
|
922
974
|
/**
|
|
923
975
|
* States the corpus and both counts on every run, including a clean one. A
|
|
924
976
|
* miss list alone reads as a verdict on the catalog unless the run also says
|
|
@@ -927,7 +979,7 @@ function runSkillsRank(
|
|
|
927
979
|
function reportRank(report: Extract<RankReport, { kind: 'measured' }>): void {
|
|
928
980
|
logStep('Corpus')
|
|
929
981
|
logInfo(
|
|
930
|
-
`${plural(report.skills, 'skill')} scored against ${plural(report.cases, 'case')}`,
|
|
982
|
+
`${report.corpus}: ${plural(report.skills, 'skill')} scored against ${plural(report.cases, 'case')}`,
|
|
931
983
|
)
|
|
932
984
|
|
|
933
985
|
logStep('Score')
|
package/src/commands/gov.ts
CHANGED
|
@@ -774,9 +774,9 @@ function runList(opts: ListOptions): number {
|
|
|
774
774
|
}
|
|
775
775
|
|
|
776
776
|
/**
|
|
777
|
-
* Silent on success so the consumed-copy stage that calls it stays
|
|
778
|
-
* the
|
|
779
|
-
*
|
|
777
|
+
* Silent on success so the consumed-copy stage that calls it stays quiet. It is
|
|
778
|
+
* the only work that stage does now. The installed set is readable on disk, so
|
|
779
|
+
* printing it would only add noise to every `bun run check`.
|
|
780
780
|
*/
|
|
781
781
|
async function runRegen(opts: RegenOptions): Promise<number> {
|
|
782
782
|
const result = await regenConsumedRules(resolve(opts.root ?? PROJECT_ROOT))
|
package/standards/skill.md
CHANGED
|
@@ -91,7 +91,7 @@ allowed-tools: <tools required>
|
|
|
91
91
|
|
|
92
92
|
- Skill is a folder named in kebab-case containing `SKILL.md` (required), `REQUIREMENT.md` (required), `EVAL.md` (optional), `scripts/` (optional), `references/` (optional), `assets/` (optional)
|
|
93
93
|
- Name a file sitting directly in the skill folder in capitals and a bundled folder in lowercase, so the parts a reader opens are distinct from the ones a skill loads
|
|
94
|
-
- `EVAL.md` holds prompts and a judging rubric a person runs by hand,
|
|
94
|
+
- `EVAL.md` holds prompts and a judging rubric a person runs by hand, which is the path open to everyone. `claude plugin eval` is the automated one and it is gated in early access, refusing before it parses a target, so write the hand-run rubric and know that the runner exists rather than that it does not
|
|
95
95
|
- `SKILL.md` must start with YAML frontmatter between `---` delimiters
|
|
96
96
|
- No `README.md` inside the skill folder
|
|
97
97
|
- No spaces, capitals, or underscores in folder or skill name
|
package/standards/snippets.md
CHANGED
|
@@ -33,8 +33,7 @@ Overlapping a skill that does the same job is not disqualifying on its own. A sn
|
|
|
33
33
|
## Invocation channels
|
|
34
34
|
|
|
35
35
|
- Chrome extension: type `>slug` in a supported chat UI (claude.ai, gemini.google.com) to insert the snippet text inline
|
|
36
|
-
- Claude Code terminal: prefix the
|
|
37
|
-
- Snippets install preserving the source folder structure. A snippet at `claude/edit.md` installs as `.claude/snippets/claude/edit.md` and is invoked as `@.claude/snippets/claude/edit`
|
|
36
|
+
- Claude Code terminal: prefix the plugin path with `@` (e.g. `@claude/snippets/claude/feature-recap`). `claude/snippets` is a symlink to the authoring root, so the folder a snippet is written in is the folder it is invoked from
|
|
38
37
|
|
|
39
38
|
## Use patterns
|
|
40
39
|
|