@erclx/aitk 3.0.0 → 3.1.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-design-extract/SKILL.md +1 -1
- package/claude/skills/claude-intake-answer/REQUIREMENT.md +1 -1
- package/claude/skills/decision-escalate/REQUIREMENT.md +2 -2
- package/claude/skills/setup-indexes/SKILL.md +5 -3
- package/claude/skills/youtube-transcripts/SKILL.md +1 -1
- package/docs/agents/audits.md +2 -2
- package/docs/agents/commands.md +20 -19
- package/docs/agents/index.md +1 -0
- package/docs/agents/skills-reach.md +57 -0
- package/package.json +1 -1
- package/src/audits/catalog.ts +36 -0
- package/src/claude/skills-reach.ts +193 -0
- package/src/commands/claude.ts +123 -4
|
@@ -35,7 +35,7 @@ Read these on both paths, skipping any that do not exist:
|
|
|
35
35
|
- `${CLAUDE_SKILL_DIR}/../../standards/markdown.md`: word, punctuation, and formatting constraints
|
|
36
36
|
- The `write-human` skill: tone and sentence construction constraints
|
|
37
37
|
|
|
38
|
-
On the source path, also read the UI surfaces matched in Step 1 plus `docs/agents/output-shape.md` and `docs/index.md` for output shape or framing rules already documented.
|
|
38
|
+
On the source path, also read the UI surfaces matched in Step 1 plus any `docs/agents/output-shape.md` and `docs/index.md` the project itself carries, for output shape or framing rules already documented. Those two are the toolkit's own spelling, so a project keeping its framing rules elsewhere is read there instead.
|
|
39
39
|
|
|
40
40
|
On the greenfield path, also read `.claude/ARCHITECTURE.md` for platform, tech stack, and surface type. Do not scan `src/`, stylesheets, or UI modules. Step 1 already established they hold nothing.
|
|
41
41
|
|
|
@@ -43,6 +43,6 @@ The batching fails a fifth way. A folder holding thirty unread items put as thir
|
|
|
43
43
|
|
|
44
44
|
- Filing a dump and writing the items, which is `claude-intake` and owns every other write into the folder
|
|
45
45
|
- Promoting an answered item onto the board, which is `claude-tasks` and runs after the answers land
|
|
46
|
-
- The item format, the answer contract, and retrieval, which `standards/intake.md` owns and this skill cites
|
|
46
|
+
- The item format, the answer contract, and retrieval, which the toolkit's `standards/intake.md` owns and this skill cites
|
|
47
47
|
- The comparable answer slots in groundwork and feature plans, which carry their own contracts and are a separate measurement
|
|
48
48
|
- Deciding when to fire. The skill is user-invoked through `disable-model-invocation`, so answering is the operator's call rather than a description match.
|
|
@@ -15,7 +15,7 @@ A body written around a named tool fails a third way. A session on a surface car
|
|
|
15
15
|
|
|
16
16
|
The opposite failure is the one that ships silently. A session that takes a preference decision itself produces work the operator did not ask for and never learns a choice was made, because nothing in the output says a fork was passed.
|
|
17
17
|
|
|
18
|
-
`CLAUDE.md` states both branches of the rule, sending an ordinary judgment call to a pick with the tradeoff in one sentence and a preference-deciding call to the operator. The first branch has `snippets/decision-help.md` behind it and the second had no surface at all.
|
|
18
|
+
`CLAUDE.md` states both branches of the rule, sending an ordinary judgment call to a pick with the tradeoff in one sentence and a preference-deciding call to the operator. The first branch has the toolkit's `snippets/decision-help.md` behind it, which installs into a project as `.claude/snippets/decision-help.md`, and the second had no surface at all.
|
|
19
19
|
|
|
20
20
|
## Must
|
|
21
21
|
|
|
@@ -40,6 +40,6 @@ The opposite failure is the one that ships silently. A session that takes a pref
|
|
|
40
40
|
## Out of scope
|
|
41
41
|
|
|
42
42
|
- Making the ordinary judgment call, which is a pick plus a one-sentence tradeoff and needs no surface
|
|
43
|
-
- The chat-side pick with no repository behind it, which `snippets/decision-help.md` covers and reaches a different reader
|
|
43
|
+
- The chat-side pick with no repository behind it, which the toolkit's `snippets/decision-help.md` covers and reaches a different reader
|
|
44
44
|
- Writing the decision into a plan, task, or architecture record, which each owning standard governs and this skill only routes to
|
|
45
45
|
- Deciding when to fire. The skill is user-invoked through `disable-model-invocation`, so escalating is the operator's call rather than a description match.
|
|
@@ -111,7 +111,7 @@ grep -l '^## Indexes' CLAUDE.md 2>/dev/null
|
|
|
111
111
|
|
|
112
112
|
If the section is present, skip the seed silently.
|
|
113
113
|
|
|
114
|
-
If `CLAUDE.md` exists but has no `## Indexes` section, offer to install the canonical convention block. The text below is the source of truth and is mirrored in `tooling/claude/seeds/CLAUDE.md`. Paste it verbatim. Do not rewrite, paraphrase, condense, or add punctuation.
|
|
114
|
+
If `CLAUDE.md` exists but has no `## Indexes` section, offer to install the canonical convention block. The text below is the source of truth and is mirrored in the toolkit's `tooling/claude/seeds/CLAUDE.md`. Paste it verbatim. Do not rewrite, paraphrase, condense, or add punctuation.
|
|
115
115
|
|
|
116
116
|
```markdown
|
|
117
117
|
- When a folder has an `index.md`, check it before reading individual files in that folder.
|
|
@@ -147,5 +147,7 @@ Replace bracketed values with the values from this run. Drop the "Folders bootst
|
|
|
147
147
|
|
|
148
148
|
## Reference
|
|
149
149
|
|
|
150
|
-
|
|
151
|
-
|
|
150
|
+
Both pages sit in the toolkit and install nowhere, so a target reads them there rather than in its own tree.
|
|
151
|
+
|
|
152
|
+
- The toolkit's `.claude/context/indexes.md`: system rationale, frontmatter contract, when to adopt
|
|
153
|
+
- The toolkit's `docs/agents/indexes.md`: `aitk indexes regen` flags, exit codes, JSON shape
|
|
@@ -5,7 +5,7 @@ description: Fetches a YouTube video transcript with metadata frontmatter into t
|
|
|
5
5
|
|
|
6
6
|
# YouTube transcripts
|
|
7
7
|
|
|
8
|
-
Turn a pasted YouTube URL into a markdown file with YAML frontmatter and a cleaned prose body. The `aitk transcripts` command owns the fetch, VTT cleanup, and frontmatter. Do not reimplement that logic.
|
|
8
|
+
Turn a pasted YouTube URL into a markdown file with YAML frontmatter and a cleaned prose body. The `aitk transcripts` command owns the fetch, VTT cleanup, and frontmatter. Do not reimplement that logic. The output format and field list live in the toolkit's `.claude/context/transcripts.md`, which a target does not receive.
|
|
9
9
|
|
|
10
10
|
## Guards
|
|
11
11
|
|
package/docs/agents/audits.md
CHANGED
|
@@ -22,11 +22,11 @@ aitk audits list --json
|
|
|
22
22
|
|
|
23
23
|
## What it runs
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
Fifteen verbs, listed by `aitk audits list`. Each runs once in its fullest form, and the aggregate reads that verb's own record rather than imposing a shared envelope on it. Every one of those records already has consumers naming its keys, so a common shape would be a breaking change bought for tidiness.
|
|
26
26
|
|
|
27
27
|
The verbs walk separate trees and share no state, so they run together. Measured on the authoring machine at twelve verbs, a run finished in 0.8 seconds of wall clock against 4.4 seconds of processor, which is under every other stage in `bun run check`. `aitk deps audit` is the one that changes that reading, since it reaches a network rather than a tree and its latency is the index's rather than this machine's.
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
Thirteen of the fifteen read a tree on this disk. The two added by `state-scoped-risk.md` read committed state rather than an arriving change, which is the gap every review surface here leaves by construction.
|
|
30
30
|
|
|
31
31
|
Each is invoked as the CLI the caller is running rather than as a global `aitk`. A globally installed binary resolves to the main checkout no matter which worktree is executing, so the aggregate would measure a tree the branch never touched and report a pass over it.
|
|
32
32
|
|
package/docs/agents/commands.md
CHANGED
|
@@ -46,6 +46,7 @@ Full help: `aitk <command> --help`. Behavior notes for the install and sync verb
|
|
|
46
46
|
| `aitk markdown audit` | Fail any markdown path on a banned character, word, or spelling, and report the structural checkpoints |
|
|
47
47
|
| `aitk claude skills audit` | Report both skill corpora against the mechanical rules in `standards/skill.md` |
|
|
48
48
|
| `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`) |
|
|
49
|
+
| `aitk claude skills reach` | Report the shipped bodies citing a toolkit path no target project receives, exiting 2 on an unqualified one |
|
|
49
50
|
| `aitk gov test-order` | Report where an implementation reached history ahead of the test covering it (`--json`) |
|
|
50
51
|
| `aitk secrets scan` | Report credential-shaped values in the tree the package ships, keyed on issued values rather than on words (`--json`) |
|
|
51
52
|
| `aitk deps audit` | Report published advisories against the resolved dependency set, refusing rather than reporting clean when the index is unreachable (`--json`) |
|
|
@@ -58,25 +59,25 @@ Full help: `aitk <command> --help`. Behavior notes for the install and sync verb
|
|
|
58
59
|
|
|
59
60
|
Each domain exposes a consistent shape where applicable: `list`, `install`, `sync`, `create`.
|
|
60
61
|
|
|
61
|
-
| Domain | Subcommands
|
|
62
|
-
| ----------- |
|
|
63
|
-
| `tooling` | `list`, `sync`, `ref`, `create`, `verify`, `inject`, `prune-gitignore`
|
|
64
|
-
| `snippets` | `list`, `install`, `sync`, `create`
|
|
65
|
-
| `standards` | `list`, `<name>`
|
|
66
|
-
| `gov` | `list`, `install`, `sync`, `build`, `regen`, `test-order`
|
|
67
|
-
| `claude` | `init`, `sync`, `seeds list`, `skills list`, `skills audit`, `skills drift`, `setup [dest]` |
|
|
68
|
-
| `wiki` | `init`
|
|
69
|
-
| `design` | `render`
|
|
70
|
-
| `slides` | `render`, `list`
|
|
71
|
-
| `tasks` | `archive`, `validate`
|
|
72
|
-
| `intake` | `list`, `answer`
|
|
73
|
-
| `teach` | `list`, `open`, `resource`, `glossary`
|
|
74
|
-
| `comments` | `scan`
|
|
75
|
-
| `context` | `audit`
|
|
76
|
-
| `markdown` | `audit`
|
|
77
|
-
| `secrets` | `scan`
|
|
78
|
-
| `deps` | `audit`
|
|
79
|
-
| `audits` | `run`, `list`
|
|
62
|
+
| Domain | Subcommands |
|
|
63
|
+
| ----------- | ----------------------------------------------------------------------------------------------------------- |
|
|
64
|
+
| `tooling` | `list`, `sync`, `ref`, `create`, `verify`, `inject`, `prune-gitignore` |
|
|
65
|
+
| `snippets` | `list`, `install`, `sync`, `create` |
|
|
66
|
+
| `standards` | `list`, `<name>` |
|
|
67
|
+
| `gov` | `list`, `install`, `sync`, `build`, `regen`, `test-order` |
|
|
68
|
+
| `claude` | `init`, `sync`, `seeds list`, `skills list`, `skills audit`, `skills drift`, `skills reach`, `setup [dest]` |
|
|
69
|
+
| `wiki` | `init` |
|
|
70
|
+
| `design` | `render` |
|
|
71
|
+
| `slides` | `render`, `list` |
|
|
72
|
+
| `tasks` | `archive`, `validate` |
|
|
73
|
+
| `intake` | `list`, `answer` |
|
|
74
|
+
| `teach` | `list`, `open`, `resource`, `glossary` |
|
|
75
|
+
| `comments` | `scan` |
|
|
76
|
+
| `context` | `audit` |
|
|
77
|
+
| `markdown` | `audit` |
|
|
78
|
+
| `secrets` | `scan` |
|
|
79
|
+
| `deps` | `audit` |
|
|
80
|
+
| `audits` | `run`, `list` |
|
|
80
81
|
|
|
81
82
|
Common patterns:
|
|
82
83
|
|
package/docs/agents/index.md
CHANGED
|
@@ -26,6 +26,7 @@ CLI catalog and invocation rules for agents, split by command domain. Start with
|
|
|
26
26
|
- [Scripting](scripting.md): The runtime catalogs that replace hardcoded names, what each carries, and a headless invocation per domain
|
|
27
27
|
- [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
|
|
28
28
|
- [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
|
|
29
|
+
- [Citation reach](skills-reach.md): Reporting the shipped skill bodies that cite a path no target project receives, 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
|
|
29
30
|
- [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
|
|
30
31
|
- [Tasks](tasks.md): Selecting a shipped task by stem or pull request, recording a number and closing an outcome, the refusal reasons, the board and backlog checks validate runs, and why the board root defaults to the main worktree
|
|
31
32
|
- [Teach](teach.md): Listing learning workspaces and the ordinal a new one takes, opening one with its required files, recording sources and glossary terms, resolving what the next lesson needs before it is written, the refusal reasons, and why every write here runs through a verb
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Citation reach
|
|
3
|
+
description: Reporting the shipped skill bodies that cite a path no target project receives, 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
|
+
---
|
|
5
|
+
|
|
6
|
+
# Citation reach
|
|
7
|
+
|
|
8
|
+
`aitk claude skills reach [path]` reports every shipped skill body citing a path that exists in the toolkit and reaches no target project. It reads and reports. Repairing what it finds is separate work.
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
aitk claude skills reach
|
|
12
|
+
aitk claude skills reach --json
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
| Option | Behavior |
|
|
16
|
+
| -------- | ---------------------------------------------------------- |
|
|
17
|
+
| `--json` | Add a machine-readable record on stdout, keeping the frame |
|
|
18
|
+
|
|
19
|
+
## The defect it reads for
|
|
20
|
+
|
|
21
|
+
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.
|
|
22
|
+
|
|
23
|
+
## What counts as a citation
|
|
24
|
+
|
|
25
|
+
A backticked token carrying a separator and an extension, which is how every body spells a path it means a reader to open. Three forms are skipped by construction.
|
|
26
|
+
|
|
27
|
+
- A placeholder such as `.claude/context/<domain>.md`, which names a shape rather than a file
|
|
28
|
+
- A path resolved through `${CLAUDE_SKILL_DIR}`, which is self-contained wherever the plugin loads
|
|
29
|
+
- A sibling named relatively, such as `references/labels.md`, which matches no authoring root and travels with the body
|
|
30
|
+
|
|
31
|
+
The same sibling named from the repository root as `claude/skills/<name>/references/<file>.md` is reported rather than skipped. The plugin loads from a cache rather than from the project tree, so that spelling resolves for nobody and the report is correct.
|
|
32
|
+
|
|
33
|
+
A path the toolkit does not hold is dropped rather than reported. The measure asks whether a claim true here is false in a target, and a path true in neither is a different defect that `aitk context audit` already reports against its own corpus.
|
|
34
|
+
|
|
35
|
+
## The ownership key
|
|
36
|
+
|
|
37
|
+
A cited path counts when it sits under an authoring root no install channel delivers. Standards install nowhere and are read through the plugin corpus, snippets land under `.claude/snippets/`, governance rules under `.claude/rules/`, and the rest is this repository's own source, catalogs, and contract pages.
|
|
38
|
+
|
|
39
|
+
`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
|
+
|
|
41
|
+
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
|
+
|
|
43
|
+
## The qualifier
|
|
44
|
+
|
|
45
|
+
A correct citation and a defective one are the same string, and the sentence around it is the difference. A citation counts as decided when its line names the toolkit as the owner, matching the bodies that already spell it that way. The repair for a finding is to say whose copy the path is, never to delete the citation, since the paths name real documents a reader wants.
|
|
46
|
+
|
|
47
|
+
```markdown
|
|
48
|
+
Read `.claude/context/indexes.md` from the toolkit if context on the system is needed.
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
A line mentioning the toolkit for an unrelated reason exempts a citation on it. That is the accepted cost of a word over a notation every future body would have to learn.
|
|
52
|
+
|
|
53
|
+
## Exit codes
|
|
54
|
+
|
|
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 no `claude/skills/`, which ships no plugin body to measure, and it reports the reason rather than a clean count over nothing.
|
|
56
|
+
|
|
57
|
+
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/package.json
CHANGED
package/src/audits/catalog.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ReachRefusal } from '@/claude/skills-reach'
|
|
1
2
|
import type { AuditRefusal } from '@/deps/audit'
|
|
2
3
|
import type { ValidateRefusal as RecordRefusal } from '@/records/validate'
|
|
3
4
|
import type { ScanRefusal } from '@/secrets/scan'
|
|
@@ -286,6 +287,21 @@ function skillCounts(record: unknown): Record<string, number> | undefined {
|
|
|
286
287
|
)
|
|
287
288
|
}
|
|
288
289
|
|
|
290
|
+
/**
|
|
291
|
+
* Reads the unqualified citations alone, leaving the qualified ones out.
|
|
292
|
+
*
|
|
293
|
+
* A qualified citation is a repair that already landed, so folding the two
|
|
294
|
+
* together would report a corpus getting worse every time one is fixed. The
|
|
295
|
+
* key is still read rather than assumed present, since a record carrying
|
|
296
|
+
* neither array is a shape that moved rather than a catalog with nothing in it.
|
|
297
|
+
*/
|
|
298
|
+
function reachCounts(record: unknown): Record<string, number> | undefined {
|
|
299
|
+
const root = asObject(record)
|
|
300
|
+
if (root === undefined || !Array.isArray(root.qualified)) return undefined
|
|
301
|
+
|
|
302
|
+
return allOf({ unqualifiedCitations: lengthOf(root.unqualified) })
|
|
303
|
+
}
|
|
304
|
+
|
|
289
305
|
function boardCounts(record: unknown): Record<string, number> | undefined {
|
|
290
306
|
const root = asObject(record)
|
|
291
307
|
if (root === undefined) return undefined
|
|
@@ -403,6 +419,26 @@ export const AUDITS: readonly AuditSpec[] = [
|
|
|
403
419
|
corpus: 'tracked',
|
|
404
420
|
counts: skillCounts,
|
|
405
421
|
},
|
|
422
|
+
{
|
|
423
|
+
id: 'skills-reach',
|
|
424
|
+
label: 'Shipped citation reach',
|
|
425
|
+
argv: ['claude', 'skills', 'reach', '--json'],
|
|
426
|
+
// Reports rather than gates, on the split this file already draws. A body
|
|
427
|
+
// naming a toolkit path is sometimes correct, since the instruction may be
|
|
428
|
+
// meant for a session in this repository, so the verdict is a judgment and
|
|
429
|
+
// a push failing on one teaches a contributor to route around the stage.
|
|
430
|
+
gatingExits: [],
|
|
431
|
+
corpus: 'tracked',
|
|
432
|
+
// The one reason this verb refuses for, and it is an absence rather than a
|
|
433
|
+
// break. A tracked corpus normally allows nothing, since a tree that ships
|
|
434
|
+
// to targets and cannot be found is a broken checkout, and this is the
|
|
435
|
+
// second exception on the same test the secret scan takes: no target holds
|
|
436
|
+
// `claude/skills/`, so without the allowance every project installing this
|
|
437
|
+
// CLI reports the verb unmeasured on every run and never changes, which is
|
|
438
|
+
// the permanent signal the per-machine allowance exists against.
|
|
439
|
+
absentReasons: ['no-skills'] satisfies ReachRefusal[],
|
|
440
|
+
counts: reachCounts,
|
|
441
|
+
},
|
|
406
442
|
{
|
|
407
443
|
id: 'tasks',
|
|
408
444
|
label: 'Task board',
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
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')
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The authoring roots this repository owns and no install channel delivers.
|
|
13
|
+
*
|
|
14
|
+
* Every entry is a folder a target never holds under that spelling. Standards
|
|
15
|
+
* install nowhere and are reached through the plugin corpus, rules install
|
|
16
|
+
* under `.claude/rules/`, snippets under `.claude/snippets/`, and the rest are
|
|
17
|
+
* this repository's own source, docs, and catalogs.
|
|
18
|
+
*
|
|
19
|
+
* `src/`, `scripts/`, and bare `docs/` are deliberately absent. A body naming
|
|
20
|
+
* one of those is describing the reader's own tree, so listing them would
|
|
21
|
+
* report a correct citation on every run and bury the defect this measures.
|
|
22
|
+
* `docs/agents/` is the exception, being the CLI contract pages that exist
|
|
23
|
+
* here alone.
|
|
24
|
+
*/
|
|
25
|
+
const AUTHORING_ROOTS = [
|
|
26
|
+
'.claude/context/',
|
|
27
|
+
'claude/',
|
|
28
|
+
'docs/agents/',
|
|
29
|
+
'governance/',
|
|
30
|
+
'internal/',
|
|
31
|
+
'snippets/',
|
|
32
|
+
'standards/',
|
|
33
|
+
'tooling/',
|
|
34
|
+
'wiki/',
|
|
35
|
+
] as const
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* What marks a citation as deliberately naming this repository's own copy.
|
|
39
|
+
*
|
|
40
|
+
* The word rather than a notation, matching the three bodies that already
|
|
41
|
+
* spell it and the repair the plan settled on. A parser-visible syntax was the
|
|
42
|
+
* alternative and it invents a spelling for a handful of lines while leaving
|
|
43
|
+
* the shipped precedent unreadable.
|
|
44
|
+
*/
|
|
45
|
+
const QUALIFIER = /toolkit/i
|
|
46
|
+
|
|
47
|
+
/** A backticked token, which is how every body spells a path it cites. */
|
|
48
|
+
const TOKEN = /`([^`\s]+)`/g
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A path a reader could open, which is the only kind worth measuring.
|
|
52
|
+
*
|
|
53
|
+
* Requires an extension and a separator, and admits no `<`, `$`, or `*`. A
|
|
54
|
+
* body writes `.claude/context/<domain>.md` to name a shape rather than a
|
|
55
|
+
* file, and `${CLAUDE_SKILL_DIR}/../../standards/markdown.md` to resolve
|
|
56
|
+
* against the plugin root, which is self-contained by construction.
|
|
57
|
+
*/
|
|
58
|
+
const CONCRETE = /^[.A-Za-z0-9_][A-Za-z0-9._/-]*\.[a-z]{1,4}$/
|
|
59
|
+
|
|
60
|
+
export interface Citation {
|
|
61
|
+
readonly file: string
|
|
62
|
+
/** One-based, matching the `file:line` form a reader clicks. */
|
|
63
|
+
readonly line: number
|
|
64
|
+
readonly path: string
|
|
65
|
+
readonly qualified: boolean
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Why a scan produced no corpus, which is never the same as a clean one. */
|
|
69
|
+
export type ReachRefusal = 'no-skills'
|
|
70
|
+
|
|
71
|
+
export type ReachReport =
|
|
72
|
+
| {
|
|
73
|
+
readonly kind: 'measured'
|
|
74
|
+
/** Files opened, so a report can state what the verdict covers. */
|
|
75
|
+
readonly bodies: number
|
|
76
|
+
readonly qualified: readonly Citation[]
|
|
77
|
+
readonly unqualified: readonly Citation[]
|
|
78
|
+
}
|
|
79
|
+
| { readonly kind: 'refused'; readonly reason: ReachRefusal }
|
|
80
|
+
|
|
81
|
+
export function isQualified(line: string): boolean {
|
|
82
|
+
return QUALIFIER.test(line)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Every path a seed lands on in a target, spelled the way a body would cite it.
|
|
87
|
+
*
|
|
88
|
+
* Read off the seed tree rather than listed, so a seed added to any stack
|
|
89
|
+
* clears its own citations without this module being edited. Dotfiles are in
|
|
90
|
+
* scope because the whole seeded context corpus sits under `.claude/`.
|
|
91
|
+
*/
|
|
92
|
+
export function readReceivedPaths(root: string): Set<string> {
|
|
93
|
+
const toolingRoot = join(root, 'tooling')
|
|
94
|
+
if (!existsSync(toolingRoot)) return new Set()
|
|
95
|
+
|
|
96
|
+
const received = new Set<string>()
|
|
97
|
+
for (const path of new Bun.Glob('*/seeds/**/*').scanSync({
|
|
98
|
+
cwd: toolingRoot,
|
|
99
|
+
onlyFiles: true,
|
|
100
|
+
dot: true,
|
|
101
|
+
})) {
|
|
102
|
+
const posix = path.replaceAll('\\', '/')
|
|
103
|
+
received.add(posix.replace(/^[^/]+\/seeds\//, ''))
|
|
104
|
+
}
|
|
105
|
+
return received
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Whether a cited path is this repository's own rather than the reader's.
|
|
110
|
+
*
|
|
111
|
+
* A seeded path is disowned twice over: under its own name, and under the
|
|
112
|
+
* folder spelling it takes once a target splits the entry. A domain that
|
|
113
|
+
* outgrows one file becomes `<domain>/`, which is still the entry the seed
|
|
114
|
+
* delivered, so reporting the split form would fail a target for growing.
|
|
115
|
+
*/
|
|
116
|
+
export function isToolkitOwned(path: string, received: Set<string>): boolean {
|
|
117
|
+
if (received.has(path)) return false
|
|
118
|
+
|
|
119
|
+
for (const seeded of received) {
|
|
120
|
+
const stem = seeded.replace(/\.md$/, '')
|
|
121
|
+
if (stem !== seeded && path.startsWith(`${stem}/`)) return false
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return AUTHORING_ROOTS.some((prefix) => path.startsWith(prefix))
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Every toolkit-owned path one shipped file cites, with the line's verdict.
|
|
129
|
+
*
|
|
130
|
+
* Existence is not checked here. A body may name a path this repository once
|
|
131
|
+
* held, and separating the shape test from the disk read is what lets the
|
|
132
|
+
* shape be tested without a tree on disk.
|
|
133
|
+
*/
|
|
134
|
+
export function citationsIn(
|
|
135
|
+
file: string,
|
|
136
|
+
text: string,
|
|
137
|
+
received: Set<string>,
|
|
138
|
+
): Citation[] {
|
|
139
|
+
const citations: Citation[] = []
|
|
140
|
+
|
|
141
|
+
for (const [index, line] of text.split('\n').entries()) {
|
|
142
|
+
const qualified = isQualified(line)
|
|
143
|
+
|
|
144
|
+
for (const match of line.matchAll(TOKEN)) {
|
|
145
|
+
const path = match[1]
|
|
146
|
+
if (!CONCRETE.test(path) || !path.includes('/')) continue
|
|
147
|
+
if (!isToolkitOwned(path, received)) continue
|
|
148
|
+
|
|
149
|
+
citations.push({ file, line: index + 1, path, qualified })
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return citations
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Reads every shipped body for a path its reader cannot open.
|
|
158
|
+
*
|
|
159
|
+
* A citation of a path this repository does not hold is dropped rather than
|
|
160
|
+
* reported. The measure asks whether a claim true here is false in a target,
|
|
161
|
+
* and a path true in neither is a different defect that `aitk context audit`
|
|
162
|
+
* already reports against its own corpus.
|
|
163
|
+
*/
|
|
164
|
+
export function scanReach(root: string): ReachReport {
|
|
165
|
+
const skillsRoot = join(root, SHIPPED_SKILLS)
|
|
166
|
+
if (!existsSync(skillsRoot)) return { kind: 'refused', reason: 'no-skills' }
|
|
167
|
+
|
|
168
|
+
const received = readReceivedPaths(root)
|
|
169
|
+
const files = [
|
|
170
|
+
...new Bun.Glob('**/*.md').scanSync({ cwd: skillsRoot, onlyFiles: true }),
|
|
171
|
+
].sort()
|
|
172
|
+
|
|
173
|
+
const qualified: Citation[] = []
|
|
174
|
+
const unqualified: Citation[] = []
|
|
175
|
+
|
|
176
|
+
for (const file of files) {
|
|
177
|
+
const posix = file.replaceAll('\\', '/')
|
|
178
|
+
const text = readFileSync(join(skillsRoot, file), 'utf8')
|
|
179
|
+
|
|
180
|
+
for (const citation of citationsIn(
|
|
181
|
+
`${SHIPPED_SKILLS.replaceAll('\\', '/')}/${posix}`,
|
|
182
|
+
text,
|
|
183
|
+
received,
|
|
184
|
+
)) {
|
|
185
|
+
if (!existsSync(join(root, citation.path))) continue
|
|
186
|
+
|
|
187
|
+
if (citation.qualified) qualified.push(citation)
|
|
188
|
+
else unqualified.push(citation)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return { kind: 'measured', bodies: files.length, qualified, unqualified }
|
|
193
|
+
}
|
package/src/commands/claude.ts
CHANGED
|
@@ -23,6 +23,11 @@ import {
|
|
|
23
23
|
} from '@/claude/skills-audit'
|
|
24
24
|
import { type DriftReport, readDrift } from '@/claude/skills-drift'
|
|
25
25
|
import { listSkills } from '@/claude/skills-list'
|
|
26
|
+
import {
|
|
27
|
+
type ReachRefusal,
|
|
28
|
+
type ReachReport,
|
|
29
|
+
scanReach,
|
|
30
|
+
} from '@/claude/skills-reach'
|
|
26
31
|
import {
|
|
27
32
|
planSettings,
|
|
28
33
|
readSettings,
|
|
@@ -70,6 +75,10 @@ interface SkillsDriftOptions {
|
|
|
70
75
|
readonly json?: boolean
|
|
71
76
|
}
|
|
72
77
|
|
|
78
|
+
interface SkillsReachOptions {
|
|
79
|
+
readonly json?: boolean
|
|
80
|
+
}
|
|
81
|
+
|
|
73
82
|
const SEEDED_FILES: readonly string[] = [
|
|
74
83
|
'ARCHITECTURE.md',
|
|
75
84
|
'REQUIREMENTS.md',
|
|
@@ -160,15 +169,15 @@ export function register(program: Command): void {
|
|
|
160
169
|
|
|
161
170
|
const skills = claude
|
|
162
171
|
.command('skills')
|
|
163
|
-
.description('Plugin skill catalog (list, audit, drift)')
|
|
164
|
-
.argument('[subcommand]', "One of 'list', 'audit', or '
|
|
172
|
+
.description('Plugin skill catalog (list, audit, drift, reach)')
|
|
173
|
+
.argument('[subcommand]', "One of 'list', 'audit', 'drift', or 'reach'")
|
|
165
174
|
.helpOption('-h, --help', 'Show this help message')
|
|
166
175
|
.action((subcommand: string | undefined) => {
|
|
167
176
|
intro('aitk claude')
|
|
168
177
|
logError(
|
|
169
178
|
subcommand === undefined
|
|
170
|
-
? "Missing subcommand. Use 'list', 'audit', or '
|
|
171
|
-
: `Unknown subcommand: ${subcommand}. Use 'list', 'audit', or '
|
|
179
|
+
? "Missing subcommand. Use 'list', 'audit', 'drift', or 'reach'."
|
|
180
|
+
: `Unknown subcommand: ${subcommand}. Use 'list', 'audit', 'drift', or 'reach'.`,
|
|
172
181
|
)
|
|
173
182
|
outro()
|
|
174
183
|
process.exitCode = 1
|
|
@@ -260,6 +269,42 @@ export function register(program: Command): void {
|
|
|
260
269
|
.action(async (ref: string, opts: SkillsDriftOptions) => {
|
|
261
270
|
process.exitCode = await runSkillsDrift(ref, opts)
|
|
262
271
|
})
|
|
272
|
+
|
|
273
|
+
skills
|
|
274
|
+
.command('reach')
|
|
275
|
+
.description('Report shipped bodies citing a path no target receives')
|
|
276
|
+
.argument('[path]', 'Repository root, defaulting to the current directory')
|
|
277
|
+
.helpOption('-h, --help', 'Show this help message')
|
|
278
|
+
.option('--json', 'Add a machine-readable record on stdout')
|
|
279
|
+
.addHelpText(
|
|
280
|
+
'after',
|
|
281
|
+
[
|
|
282
|
+
'',
|
|
283
|
+
'Scope:',
|
|
284
|
+
' Every markdown file under claude/skills/, which is the tree that',
|
|
285
|
+
' installs into a target. A cited path counts when it sits under an',
|
|
286
|
+
' authoring root no install channel delivers and this repository',
|
|
287
|
+
' holds it. A path under src/, scripts/, or bare docs/ names the',
|
|
288
|
+
" reader's own tree and is not measured.",
|
|
289
|
+
'',
|
|
290
|
+
'Exit codes:',
|
|
291
|
+
' 0 every citation names the toolkit as the owner',
|
|
292
|
+
' 1 refused, with the reason on stderr',
|
|
293
|
+
' 2 at least one citation is unqualified',
|
|
294
|
+
'',
|
|
295
|
+
'Reports rather than gates. A toolkit-scoped instruction is sometimes',
|
|
296
|
+
'meant for a session in this repository, so the verdict is a reading',
|
|
297
|
+
'and the repair is to name the owner in the sentence.',
|
|
298
|
+
'',
|
|
299
|
+
'Examples:',
|
|
300
|
+
' aitk claude skills reach',
|
|
301
|
+
' aitk claude skills reach --json',
|
|
302
|
+
'',
|
|
303
|
+
].join('\n'),
|
|
304
|
+
)
|
|
305
|
+
.action((path: string | undefined, opts: SkillsReachOptions) => {
|
|
306
|
+
process.exitCode = runSkillsReach(path, opts)
|
|
307
|
+
})
|
|
263
308
|
}
|
|
264
309
|
|
|
265
310
|
function succeed(message: string): number {
|
|
@@ -567,6 +612,80 @@ function reportSkew(skew: SkewReport): void {
|
|
|
567
612
|
else logInfo(describeSkew(skew))
|
|
568
613
|
}
|
|
569
614
|
|
|
615
|
+
/** What a reader does about the one way the corpus fails to build. */
|
|
616
|
+
const REACH_REFUSALS: Record<ReachRefusal, string> = {
|
|
617
|
+
'no-skills':
|
|
618
|
+
'No claude/skills/ here, so this tree ships no plugin body to measure.',
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Measures the cwd rather than the toolkit root, matching the audit and drift
|
|
623
|
+
* verbs, so a linked worktree reads its own branch instead of `main`.
|
|
624
|
+
*/
|
|
625
|
+
function runSkillsReach(
|
|
626
|
+
path: string | undefined,
|
|
627
|
+
opts: SkillsReachOptions,
|
|
628
|
+
): number {
|
|
629
|
+
const root = resolve(path ?? process.cwd())
|
|
630
|
+
const report = scanReach(root)
|
|
631
|
+
|
|
632
|
+
if (report.kind === 'refused') {
|
|
633
|
+
frameError(REACH_REFUSALS[report.reason])
|
|
634
|
+
if (opts.json) {
|
|
635
|
+
process.stdout.write(
|
|
636
|
+
`${JSON.stringify({
|
|
637
|
+
root,
|
|
638
|
+
reason: report.reason,
|
|
639
|
+
message: REACH_REFUSALS[report.reason],
|
|
640
|
+
})}\n`,
|
|
641
|
+
)
|
|
642
|
+
}
|
|
643
|
+
return 1
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
intro('aitk claude skills reach')
|
|
647
|
+
reportReach(report)
|
|
648
|
+
outro()
|
|
649
|
+
|
|
650
|
+
if (opts.json) {
|
|
651
|
+
process.stdout.write(
|
|
652
|
+
`${JSON.stringify({
|
|
653
|
+
root,
|
|
654
|
+
bodies: report.bodies,
|
|
655
|
+
qualified: report.qualified,
|
|
656
|
+
unqualified: report.unqualified,
|
|
657
|
+
})}\n`,
|
|
658
|
+
)
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
return report.unqualified.length === 0 ? 0 : 2
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* States the corpus on every run, including the clean one. A count of what
|
|
666
|
+
* failed reads as a verdict on the catalog unless the run also says how many
|
|
667
|
+
* bodies it opened and how many citations it already accepted.
|
|
668
|
+
*/
|
|
669
|
+
function reportReach(report: Extract<ReachReport, { kind: 'measured' }>): void {
|
|
670
|
+
logStep('Corpus')
|
|
671
|
+
logInfo(
|
|
672
|
+
`${plural(report.bodies, 'shipped file')} read, ${plural(report.qualified.length, 'citation')} already naming the toolkit as owner`,
|
|
673
|
+
)
|
|
674
|
+
|
|
675
|
+
logStep('Unqualified citations')
|
|
676
|
+
if (report.unqualified.length === 0) {
|
|
677
|
+
logInfo('Every toolkit-owned path a shipped body cites names its owner.')
|
|
678
|
+
return
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
logWarn(plural(report.unqualified.length, 'citation'))
|
|
682
|
+
pipeOutput(
|
|
683
|
+
report.unqualified
|
|
684
|
+
.map((citation) => `${citation.file}:${citation.line} ${citation.path}`)
|
|
685
|
+
.join('\n'),
|
|
686
|
+
)
|
|
687
|
+
}
|
|
688
|
+
|
|
570
689
|
/**
|
|
571
690
|
* States the bound on every run, including the run that names nothing. A report
|
|
572
691
|
* listing only what moved reads as a verdict on what a session holds, and the
|