@erclx/aitk 0.28.0 → 0.30.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 +5 -1
- package/claude/.claude-plugin/plugin.json +1 -1
- package/docs/agents.md +24 -11
- package/package.json +1 -1
- package/scripts/core/regen-hero.sh +128 -0
- package/scripts/core/verify.sh +26 -0
- package/src/claude/skills-list.ts +53 -0
- package/src/commands/claude.ts +66 -0
- package/src/commands/context.ts +45 -2
- package/src/context/audit.ts +64 -0
- package/standards/context.md +3 -1
- package/standards/index.md +1 -1
- package/standards/tasks.md +16 -4
package/README.md
CHANGED
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
|
|
7
7
|
One source for your repos' AI conventions. Install once, sync everywhere.
|
|
8
8
|
|
|
9
|
-

|
|
10
|
+
|
|
11
|
+
If you work across more than one repository and your AI setup has started to drift between them, this is for you. The counts above are read from the catalogs when the image is built, so they're what the repo actually ships today.
|
|
10
12
|
|
|
11
13
|
## Install
|
|
12
14
|
|
|
@@ -17,6 +19,8 @@ claude plugin marketplace add https://github.com/erclx/aitk
|
|
|
17
19
|
claude plugin install aitk@aitk
|
|
18
20
|
```
|
|
19
21
|
|
|
22
|
+

|
|
23
|
+
|
|
20
24
|
The skills land as `/aitk:<name>`. If your session was already open, run `/reload-plugins` to pick them up. Updates arrive on release, so a push to this repo won't reach your installed copy.
|
|
21
25
|
|
|
22
26
|
Several skills call the `aitk` CLI to read catalogs and run installs, and the plugin doesn't put it on your path. Install it from the registry.
|
package/docs/agents.md
CHANGED
|
@@ -80,7 +80,7 @@ Full help: `aitk <command> --help`.
|
|
|
80
80
|
| `aitk transcripts <url>` | Fetch a YouTube transcript with metadata frontmatter (needs `yt-dlp`) |
|
|
81
81
|
| `aitk tasks archive` | Move a shipped task off the board, clear its ordering row, and regenerate the index |
|
|
82
82
|
| `aitk comments scan` | Measure comment density by language and comment kind, with a trend recomputed from git |
|
|
83
|
-
| `aitk context audit` | Report entry length, depth, cited-path resolution, and index drift
|
|
83
|
+
| `aitk context audit` | Report entry length, depth, cited-path resolution, provenance, and index drift |
|
|
84
84
|
| `aitk capture [source]` | Render HTML capture sources to PNG, toolkit-only and absent from an installed package |
|
|
85
85
|
|
|
86
86
|
### Domain commands
|
|
@@ -93,7 +93,7 @@ Each domain exposes a consistent shape where applicable: `list`, `install`, `syn
|
|
|
93
93
|
| `snippets` | `list`, `install`, `sync`, `create` |
|
|
94
94
|
| `standards` | `list`, `install`, `sync` |
|
|
95
95
|
| `gov` | `list`, `install`, `sync`, `build` |
|
|
96
|
-
| `claude` | `init`, `sync`, `seeds list`, `setup [dest]`
|
|
96
|
+
| `claude` | `init`, `sync`, `seeds list`, `skills list`, `setup [dest]` |
|
|
97
97
|
| `wiki` | `init` |
|
|
98
98
|
| `design` | `render` |
|
|
99
99
|
| `slides` | `render`, `list` |
|
|
@@ -327,6 +327,8 @@ aitk capture assets/install.html
|
|
|
327
327
|
aitk capture assets --out .claude/review/captures
|
|
328
328
|
```
|
|
329
329
|
|
|
330
|
+
`assets/` now holds two sources, so a bare `aitk capture` rebuilds both. `install.html` is hand-authored and its text came from a real run. `hero.html` is generated by `scripts/core/regen-hero.sh` from the CLI catalogs and must not be edited by hand, since `bun run check` regenerates it and fails on the difference. Only the HTML is asserted for drift. The PNG is a chromium render whose bytes move with the browser version, so rebuild it with `aitk capture assets/hero.html` when the check reports the HTML changed.
|
|
331
|
+
|
|
330
332
|
| Option | Behavior |
|
|
331
333
|
| ------------------ | ------------------------------------------------- |
|
|
332
334
|
| `--out <dir>` | Write every PNG here instead of beside its source |
|
|
@@ -385,7 +387,7 @@ aitk context audit --folder context,diagrams
|
|
|
385
387
|
|
|
386
388
|
Scope defaults to `context`, `diagrams`, and `wireframes`, and a folder the project does not carry is skipped rather than reported. A domain that outgrew one file and split into `<domain>/` is audited as its own folder, so a split entry measures at the same grain as a flat one.
|
|
387
389
|
|
|
388
|
-
Exit codes are `0` for a clean run, `1` for a refusal, and `2` for an unresolved citation. Only the citation check sets a failing code. Length, depth, table, and index findings print and return `0`, because each is a judgment
|
|
390
|
+
Exit codes are `0` for a clean run, `1` for a refusal, and `2` for an unresolved citation. Only the citation check sets a failing code. Length, depth, table, provenance, and index findings print and return `0`, because each is a judgment and failing a push on one would make the check something to route around.
|
|
389
391
|
|
|
390
392
|
### What each check reports
|
|
391
393
|
|
|
@@ -393,6 +395,8 @@ Length and depth quote their checkpoints from `.claude/standards/context.md`: ro
|
|
|
393
395
|
|
|
394
396
|
The table check reports a catalog that grows a row per shipped thing, not a table count. A fixed comparison table never reflows, so its size costs nothing. A table qualifies at six or more body rows whose first column mostly carries a path, command, or link, which is what separates a catalog from a comparison without reading the prose.
|
|
395
397
|
|
|
398
|
+
The provenance check reports the markers narrating how a domain reached its shape rather than describing what it is: a date, a change number, or a release label. The standard admits a rejected alternative and the reasoning that killed it while refusing the provenance attached to it, so a marker names a line to read rather than a line to delete. Findings group by entry and sort left to right within a line, since what a reader acts on is which file to open. Fenced blocks are excluded, which keeps a pinned version in an install command from reading as a claim the entry makes. Frontmatter is excluded with them, since every check reads the body alone, and that is what keeps a diagram entry's dated `verified` stamp a record of its last check rather than a marker to settle.
|
|
399
|
+
|
|
396
400
|
Index drift compares an index against its siblings in both directions. An entry the index does not link is invisible to a session choosing what to open, and a linked name resolving to nothing sends one to a path that opens nothing.
|
|
397
401
|
|
|
398
402
|
### The citation gate
|
|
@@ -407,14 +411,15 @@ What remains is a sentence naming a hypothetical entry to show the shape of a na
|
|
|
407
411
|
|
|
408
412
|
Use these to discover what's available instead of hardcoding names.
|
|
409
413
|
|
|
410
|
-
| Command
|
|
411
|
-
|
|
|
412
|
-
| `aitk tooling list --json`
|
|
413
|
-
| `aitk snippets list --json`
|
|
414
|
-
| `aitk standards list --json`
|
|
415
|
-
| `aitk gov list --json`
|
|
416
|
-
| `aitk claude seeds list --json`
|
|
417
|
-
| `aitk
|
|
414
|
+
| Command | Returns |
|
|
415
|
+
| -------------------------------- | -------------------------------------------- |
|
|
416
|
+
| `aitk tooling list --json` | Stacks, extends chain, dep and script counts |
|
|
417
|
+
| `aitk snippets list --json` | Presets and categories with their slugs |
|
|
418
|
+
| `aitk standards list --json` | Standards docs and the paths each governs |
|
|
419
|
+
| `aitk gov list --json` | Governance stacks and rule sets |
|
|
420
|
+
| `aitk claude seeds list --json` | Seed doc sources with content |
|
|
421
|
+
| `aitk claude skills list --json` | Plugin skills with their descriptions |
|
|
422
|
+
| `aitk docs list --json` | Consumer docs plus per-domain context |
|
|
418
423
|
|
|
419
424
|
Every catalog serializes through `JSON.stringify`, so a name carrying a quote
|
|
420
425
|
emits valid JSON. `aitk tooling list` and `aitk snippets list` previously built
|
|
@@ -433,6 +438,14 @@ listing and the install cannot disagree. It now reports
|
|
|
433
438
|
`.claude/context/index.md`, which `init` has always installed and the listing
|
|
434
439
|
never named, and it emits the project-level `CLAUDE.md` last rather than first.
|
|
435
440
|
|
|
441
|
+
`aitk claude skills list` reads `claude/skills/*/SKILL.md` and reports the folder
|
|
442
|
+
name with the frontmatter description, sorted by name. Internal skills under
|
|
443
|
+
`.claude/skills/` are excluded, since they never install into a target and a
|
|
444
|
+
count spanning both overstates what ships. A skill whose frontmatter is missing
|
|
445
|
+
or unparseable returns an empty description rather than failing the listing, so
|
|
446
|
+
one malformed file cannot hide the rest of the catalog. `--names` emits skill
|
|
447
|
+
names one per line.
|
|
448
|
+
|
|
436
449
|
## Non-interactive examples
|
|
437
450
|
|
|
438
451
|
```bash
|
package/package.json
CHANGED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Fills assets/hero.html.tmpl from the CLI catalogs and writes assets/hero.html.
|
|
3
|
+
#
|
|
4
|
+
# Only the HTML regenerates here. The PNG beside it is a chromium render whose
|
|
5
|
+
# bytes move with the browser version, so asserting it in verify.sh would fail
|
|
6
|
+
# on a machine whose chromium differs rather than on a stale count. Rebuild the
|
|
7
|
+
# image with `aitk capture assets/hero.html` after this script reports a change.
|
|
8
|
+
# The frame carries no version. `package.json` is bumped on main by the release
|
|
9
|
+
# tooling, so embedding it drifts every open branch on the next release and the
|
|
10
|
+
# stage then fails for work that touched nothing.
|
|
11
|
+
#
|
|
12
|
+
# Clone-only. `src/capture` is excluded from the published tarball, and this
|
|
13
|
+
# script reads the repository's own catalogs, so a registry install has neither.
|
|
14
|
+
set -e
|
|
15
|
+
set -o pipefail
|
|
16
|
+
|
|
17
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
18
|
+
PROJECT_ROOT="${PROJECT_ROOT:-$(cd "$SCRIPT_DIR/../.." && pwd)}"
|
|
19
|
+
|
|
20
|
+
TEMPLATE="$PROJECT_ROOT/assets/hero.html.tmpl"
|
|
21
|
+
OUTPUT="$PROJECT_ROOT/assets/hero.html"
|
|
22
|
+
LISTED=10
|
|
23
|
+
|
|
24
|
+
# `bun src/cli.ts` rather than `aitk`, since a globally linked binary resolves to
|
|
25
|
+
# the main checkout no matter which worktree is running.
|
|
26
|
+
catalog() {
|
|
27
|
+
(cd "$PROJECT_ROOT" && AITK_NON_INTERACTIVE=1 bun src/cli.ts "$@" --json 2>/dev/null)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if [ ! -f "$TEMPLATE" ]; then
|
|
31
|
+
echo "regen-hero: missing template at $TEMPLATE" >&2
|
|
32
|
+
exit 1
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
SKILLS_JSON="$(catalog claude skills list)"
|
|
36
|
+
GOV_JSON="$(catalog gov list)"
|
|
37
|
+
STANDARDS_JSON="$(catalog standards list)"
|
|
38
|
+
SNIPPETS_JSON="$(catalog snippets list)"
|
|
39
|
+
TOOLING_JSON="$(catalog tooling list)"
|
|
40
|
+
|
|
41
|
+
for payload in "$SKILLS_JSON" "$GOV_JSON" "$STANDARDS_JSON" "$SNIPPETS_JSON" "$TOOLING_JSON"; do
|
|
42
|
+
if [ -z "$payload" ]; then
|
|
43
|
+
echo "regen-hero: a catalog returned nothing, refusing to write a zeroed hero" >&2
|
|
44
|
+
exit 1
|
|
45
|
+
fi
|
|
46
|
+
done
|
|
47
|
+
|
|
48
|
+
export SKILLS_JSON GOV_JSON STANDARDS_JSON SNIPPETS_JSON TOOLING_JSON
|
|
49
|
+
export TEMPLATE OUTPUT LISTED PROJECT_ROOT
|
|
50
|
+
|
|
51
|
+
bun --eval '
|
|
52
|
+
const {
|
|
53
|
+
SKILLS_JSON, GOV_JSON, STANDARDS_JSON, SNIPPETS_JSON, TOOLING_JSON,
|
|
54
|
+
TEMPLATE, OUTPUT, LISTED, PROJECT_ROOT,
|
|
55
|
+
} = process.env
|
|
56
|
+
|
|
57
|
+
const listed = Number(LISTED)
|
|
58
|
+
const skills = JSON.parse(SKILLS_JSON).skills.map((entry) => entry.name)
|
|
59
|
+
const gov = JSON.parse(GOV_JSON)
|
|
60
|
+
// Rule names carry a numeric prefix that orders the load, not the identity a
|
|
61
|
+
// reader knows them by, so the frame shows the slug alone.
|
|
62
|
+
const rules = gov.rules.map((entry) => entry.name.replace(/^\d+-/, ""))
|
|
63
|
+
const standards = JSON.parse(STANDARDS_JSON).standards.map((entry) => entry.name)
|
|
64
|
+
const snippets = new Set(
|
|
65
|
+
JSON.parse(SNIPPETS_JSON).categories.flatMap((category) => category.entries),
|
|
66
|
+
)
|
|
67
|
+
const toolingStacks = JSON.parse(TOOLING_JSON).stacks
|
|
68
|
+
|
|
69
|
+
const escape = (value) =>
|
|
70
|
+
value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")
|
|
71
|
+
|
|
72
|
+
// Even spacing across the sorted catalog rather than its first N. The skill
|
|
73
|
+
// names are prefixed by domain, so an alphabetical head returns eight
|
|
74
|
+
// `claude-*` entries and reads as a narrower catalog than the one that ships.
|
|
75
|
+
const sample = (names) => {
|
|
76
|
+
if (names.length <= listed) return names
|
|
77
|
+
const step = names.length / listed
|
|
78
|
+
return Array.from({ length: listed }, (_, i) => names[Math.floor(i * step)])
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const entries = (names) =>
|
|
82
|
+
sample(names)
|
|
83
|
+
.map((name) => ` <div class="entry">${escape(name)}</div>`)
|
|
84
|
+
.join("\n")
|
|
85
|
+
|
|
86
|
+
const remaining = (names) => String(Math.max(0, names.length - listed))
|
|
87
|
+
|
|
88
|
+
// An empty array is well-formed JSON, so the shell guard on an empty payload
|
|
89
|
+
// does not catch a catalog whose tree went missing. A zeroed count would ship a
|
|
90
|
+
// frame claiming the domain has nothing in it.
|
|
91
|
+
for (const [label, list] of [
|
|
92
|
+
["skills", skills], ["rules", rules], ["standards", standards],
|
|
93
|
+
["snippets", [...snippets]], ["tooling stacks", toolingStacks], ["gov stacks", gov.stacks],
|
|
94
|
+
]) {
|
|
95
|
+
if (list.length === 0) {
|
|
96
|
+
console.error(`regen-hero: the ${label} catalog is empty, refusing to write a zeroed hero`)
|
|
97
|
+
process.exit(1)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const values = {
|
|
102
|
+
SKILL_COUNT: String(skills.length),
|
|
103
|
+
RULE_COUNT: String(rules.length),
|
|
104
|
+
STANDARD_COUNT: String(standards.length),
|
|
105
|
+
SNIPPET_COUNT: String(snippets.size),
|
|
106
|
+
GOV_STACK_COUNT: String(gov.stacks.length),
|
|
107
|
+
TOOLING_STACK_COUNT: String(toolingStacks.length),
|
|
108
|
+
SKILL_ENTRIES: entries(skills),
|
|
109
|
+
RULE_ENTRIES: entries(rules),
|
|
110
|
+
STANDARD_ENTRIES: entries(standards),
|
|
111
|
+
SKILL_MORE: remaining(skills),
|
|
112
|
+
RULE_MORE: remaining(rules),
|
|
113
|
+
STANDARD_MORE: remaining(standards),
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
let html = await Bun.file(TEMPLATE).text()
|
|
117
|
+
for (const [key, value] of Object.entries(values)) {
|
|
118
|
+
html = html.replaceAll(`{{${key}}}`, value)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const unresolved = html.match(/{{[A-Z_]+}}/g)
|
|
122
|
+
if (unresolved) {
|
|
123
|
+
console.error(`regen-hero: unresolved placeholders ${[...new Set(unresolved)].join(", ")}`)
|
|
124
|
+
process.exit(1)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
await Bun.write(OUTPUT, html)
|
|
128
|
+
'
|
package/scripts/core/verify.sh
CHANGED
|
@@ -99,6 +99,23 @@ collect_plugin_manifests() {
|
|
|
99
99
|
} | sort -u
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
# The drift assert covers the HTML because the PNG is a chromium render whose
|
|
103
|
+
# bytes move with the browser. That leaves the artifact a visitor actually sees
|
|
104
|
+
# asserted nowhere, so a branch that regenerates the HTML and never runs the
|
|
105
|
+
# capture passes every stage while shipping an image with the old counts.
|
|
106
|
+
#
|
|
107
|
+
# The two files move together or the image is stale, so their last-touching
|
|
108
|
+
# commit is the same commit. Comparing the branch's file list instead would pass
|
|
109
|
+
# any branch that touched both anywhere, including one that regenerated the HTML
|
|
110
|
+
# alone in a later commit. Both absent resolves to two empty strings and passes,
|
|
111
|
+
# which is correct for a tree that carries neither.
|
|
112
|
+
assert_hero_pair() {
|
|
113
|
+
local html_commit png_commit
|
|
114
|
+
html_commit=$(git -C "$PROJECT_ROOT" log -1 --format=%H -- assets/hero.html)
|
|
115
|
+
png_commit=$(git -C "$PROJECT_ROOT" log -1 --format=%H -- assets/hero.png)
|
|
116
|
+
[ "$html_commit" = "$png_commit" ]
|
|
117
|
+
}
|
|
118
|
+
|
|
102
119
|
assert_no_drift() {
|
|
103
120
|
local paths=$1
|
|
104
121
|
local err_msg=$2
|
|
@@ -135,6 +152,15 @@ main() {
|
|
|
135
152
|
assert_no_drift ".claude/standards .claude/snippets .claude/internal .claude/rules" "Consumed copies drifted. Run bun run check and commit .claude/standards, .claude/snippets, .claude/internal, and .claude/rules."
|
|
136
153
|
log_info "Consumed copies clean"
|
|
137
154
|
|
|
155
|
+
# Only the HTML is asserted. The PNG beside it is a chromium render whose bytes
|
|
156
|
+
# move with the browser version, so a drift check over it would fail on a
|
|
157
|
+
# machine whose chromium differs rather than on a stale count.
|
|
158
|
+
log_step "Hero"
|
|
159
|
+
run_check "bash $PROJECT_ROOT/scripts/core/regen-hero.sh" "Hero regen failed"
|
|
160
|
+
assert_no_drift "assets/hero.html" "Hero counts drifted. Run bun run check, then aitk capture assets/hero.html, and commit assets/hero.html with assets/hero.png."
|
|
161
|
+
run_check "assert_hero_pair" "Hero HTML and image last moved in different commits, so the image may be stale. Run aitk capture assets/hero.html and commit both files together."
|
|
162
|
+
log_info "Hero clean"
|
|
163
|
+
|
|
138
164
|
log_step "Skill references"
|
|
139
165
|
run_check "bash $PROJECT_ROOT/scripts/core/regen-skill-references.sh" "Skill-reference regen failed"
|
|
140
166
|
assert_no_drift "claude/skills/*/references" "Skill references drifted. Run bun run check and commit the updated reference files."
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
2
|
+
import { dirname, join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
const FRONTMATTER = /^---\n([\s\S]*?)\n---/
|
|
5
|
+
|
|
6
|
+
export interface SkillListing {
|
|
7
|
+
readonly name: string
|
|
8
|
+
readonly description: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Enumerates the plugin skill catalog, which is the corpus under `claude/`
|
|
13
|
+
* rather than the internal skills under `.claude/`. Only the former installs
|
|
14
|
+
* into a target, so a count taken across both overstates what ships.
|
|
15
|
+
*
|
|
16
|
+
* The folder name wins over the frontmatter `name` when they disagree, because
|
|
17
|
+
* Claude Code invokes a skill by its directory.
|
|
18
|
+
*/
|
|
19
|
+
export function listSkills(root: string): SkillListing[] {
|
|
20
|
+
const skillsRoot = join(root, 'claude', 'skills')
|
|
21
|
+
if (!existsSync(skillsRoot)) return []
|
|
22
|
+
|
|
23
|
+
const paths = [
|
|
24
|
+
...new Bun.Glob('*/SKILL.md').scanSync({
|
|
25
|
+
cwd: skillsRoot,
|
|
26
|
+
onlyFiles: true,
|
|
27
|
+
}),
|
|
28
|
+
].sort()
|
|
29
|
+
|
|
30
|
+
return paths.map((path) => ({
|
|
31
|
+
name: dirname(path),
|
|
32
|
+
description: readDescription(join(skillsRoot, path)),
|
|
33
|
+
}))
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Returns an empty description rather than throwing on a skill whose
|
|
38
|
+
* frontmatter is missing or unparseable, so one malformed file does not hide
|
|
39
|
+
* the rest of the catalog from a caller counting it.
|
|
40
|
+
*/
|
|
41
|
+
function readDescription(path: string): string {
|
|
42
|
+
const match = FRONTMATTER.exec(readFileSync(path, 'utf8'))
|
|
43
|
+
if (!match) return ''
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const parsed = Bun.YAML.parse(match[1])
|
|
47
|
+
if (typeof parsed !== 'object' || parsed === null) return ''
|
|
48
|
+
const { description } = parsed as Record<string, unknown>
|
|
49
|
+
return typeof description === 'string' ? description : ''
|
|
50
|
+
} catch {
|
|
51
|
+
return ''
|
|
52
|
+
}
|
|
53
|
+
}
|
package/src/commands/claude.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
type Seed,
|
|
13
13
|
} from '@/claude/seeds'
|
|
14
14
|
import { listSeeds, readSeedContents } from '@/claude/seeds-list'
|
|
15
|
+
import { listSkills } from '@/claude/skills-list'
|
|
15
16
|
import {
|
|
16
17
|
planSettings,
|
|
17
18
|
readSettings,
|
|
@@ -43,6 +44,11 @@ interface SeedsListOptions {
|
|
|
43
44
|
readonly names?: boolean
|
|
44
45
|
}
|
|
45
46
|
|
|
47
|
+
interface SkillsListOptions {
|
|
48
|
+
readonly json?: boolean
|
|
49
|
+
readonly names?: boolean
|
|
50
|
+
}
|
|
51
|
+
|
|
46
52
|
const SEEDED_FILES: readonly string[] = [
|
|
47
53
|
'ARCHITECTURE.md',
|
|
48
54
|
'REQUIREMENTS.md',
|
|
@@ -130,6 +136,42 @@ export function register(program: Command): void {
|
|
|
130
136
|
.action(async (opts: SeedsListOptions) => {
|
|
131
137
|
process.exitCode = await runSeedsList(opts)
|
|
132
138
|
})
|
|
139
|
+
|
|
140
|
+
const skills = claude
|
|
141
|
+
.command('skills')
|
|
142
|
+
.description('Plugin skill catalog (list)')
|
|
143
|
+
.argument('[subcommand]', "Only 'list' is supported")
|
|
144
|
+
.helpOption('-h, --help', 'Show this help message')
|
|
145
|
+
.action((subcommand: string | undefined) => {
|
|
146
|
+
intro('aitk claude')
|
|
147
|
+
logError(
|
|
148
|
+
subcommand === undefined
|
|
149
|
+
? "Missing subcommand. Use 'list'."
|
|
150
|
+
: `Unknown subcommand: ${subcommand}. Use 'list'.`,
|
|
151
|
+
)
|
|
152
|
+
outro()
|
|
153
|
+
process.exitCode = 1
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
skills
|
|
157
|
+
.command('list')
|
|
158
|
+
.description('List the plugin skills shipped under claude/skills/')
|
|
159
|
+
.helpOption('-h, --help', 'Show this help message')
|
|
160
|
+
.option('--json', 'Emit JSON with name and description')
|
|
161
|
+
.option('--names', 'Only list skill names, one per line')
|
|
162
|
+
.addHelpText(
|
|
163
|
+
'after',
|
|
164
|
+
[
|
|
165
|
+
'',
|
|
166
|
+
'Notes:',
|
|
167
|
+
' Internal skills under .claude/skills/ are excluded, since they',
|
|
168
|
+
' never install into a target project.',
|
|
169
|
+
'',
|
|
170
|
+
].join('\n'),
|
|
171
|
+
)
|
|
172
|
+
.action((opts: SkillsListOptions) => {
|
|
173
|
+
process.exitCode = runSkillsList(opts)
|
|
174
|
+
})
|
|
133
175
|
}
|
|
134
176
|
|
|
135
177
|
function succeed(message: string): number {
|
|
@@ -347,3 +389,27 @@ async function runSeedsList(opts: SeedsListOptions): Promise<number> {
|
|
|
347
389
|
outro()
|
|
348
390
|
return 0
|
|
349
391
|
}
|
|
392
|
+
|
|
393
|
+
function runSkillsList(opts: SkillsListOptions): number {
|
|
394
|
+
const listings = listSkills(PROJECT_ROOT)
|
|
395
|
+
|
|
396
|
+
if (opts.json) {
|
|
397
|
+
process.stdout.write(`${JSON.stringify({ skills: listings })}\n`)
|
|
398
|
+
return 0
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (opts.names) {
|
|
402
|
+
process.stdout.write(
|
|
403
|
+
listings.map((listing) => listing.name).join('\n') + '\n',
|
|
404
|
+
)
|
|
405
|
+
return 0
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
intro('aitk claude')
|
|
409
|
+
logStep('Plugin skills')
|
|
410
|
+
for (const listing of listings) {
|
|
411
|
+
logInfo(listing.name)
|
|
412
|
+
}
|
|
413
|
+
outro()
|
|
414
|
+
return 0
|
|
415
|
+
}
|
package/src/commands/context.ts
CHANGED
|
@@ -45,7 +45,9 @@ export function register(program: Command): void {
|
|
|
45
45
|
|
|
46
46
|
context
|
|
47
47
|
.command('audit')
|
|
48
|
-
.description(
|
|
48
|
+
.description(
|
|
49
|
+
'Report entry length, depth, citations, provenance, and index drift',
|
|
50
|
+
)
|
|
49
51
|
.argument('[path]', 'Project root, defaulting to the current directory')
|
|
50
52
|
.helpOption('-h, --help', 'Show this help message')
|
|
51
53
|
.option('--json', 'Add a machine-readable record on stdout')
|
|
@@ -61,7 +63,7 @@ export function register(program: Command): void {
|
|
|
61
63
|
' 2 a cited path did not resolve',
|
|
62
64
|
'',
|
|
63
65
|
'Only unresolved citations set a failing exit code. Length, depth,',
|
|
64
|
-
'table, and index findings are advisory.',
|
|
66
|
+
'table, provenance, and index findings are advisory.',
|
|
65
67
|
'',
|
|
66
68
|
'Examples:',
|
|
67
69
|
' aitk context audit',
|
|
@@ -134,6 +136,7 @@ async function runAudit(
|
|
|
134
136
|
reportLength(entries)
|
|
135
137
|
reportDepth(entries)
|
|
136
138
|
reportTables(entries)
|
|
139
|
+
reportProvenance(entries)
|
|
137
140
|
reportDrift(drift)
|
|
138
141
|
outro()
|
|
139
142
|
}
|
|
@@ -303,6 +306,46 @@ function reportTables(entries: readonly EntryReport[]): void {
|
|
|
303
306
|
pipeOutput(candidates.join('\n'))
|
|
304
307
|
}
|
|
305
308
|
|
|
309
|
+
/**
|
|
310
|
+
* Groups by entry rather than listing every marker.
|
|
311
|
+
*
|
|
312
|
+
* The two entries carrying most of a corpus's markers carry them a dozen at a
|
|
313
|
+
* time, and a flat list of those buries the entries holding one. What a reader
|
|
314
|
+
* acts on is which file to open, so the count sits beside the name and the
|
|
315
|
+
* lines follow it.
|
|
316
|
+
*/
|
|
317
|
+
function reportProvenance(entries: readonly EntryReport[]): void {
|
|
318
|
+
logStep('Provenance')
|
|
319
|
+
logInfo('Fenced blocks are excluded. A marker is a judgment, never a defect.')
|
|
320
|
+
|
|
321
|
+
const carrying = entries
|
|
322
|
+
.filter((entry) => entry.provenance.length > 0)
|
|
323
|
+
.sort((a, b) => b.provenance.length - a.provenance.length)
|
|
324
|
+
|
|
325
|
+
if (carrying.length === 0) {
|
|
326
|
+
logInfo('No entry narrates a change.')
|
|
327
|
+
return
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const total = carrying.reduce(
|
|
331
|
+
(sum, entry) => sum + entry.provenance.length,
|
|
332
|
+
0,
|
|
333
|
+
)
|
|
334
|
+
logWarn(
|
|
335
|
+
`${plural(total, 'marker')} across ${carrying.length} ${carrying.length === 1 ? 'entry' : 'entries'}`,
|
|
336
|
+
)
|
|
337
|
+
pipeOutput(
|
|
338
|
+
carrying
|
|
339
|
+
.map(
|
|
340
|
+
(entry) =>
|
|
341
|
+
`${entry.rel} ${plural(entry.provenance.length, 'marker')}\n${entry.provenance
|
|
342
|
+
.map((found) => ` :${found.line} ${found.kind} ${found.text}`)
|
|
343
|
+
.join('\n')}`,
|
|
344
|
+
)
|
|
345
|
+
.join('\n'),
|
|
346
|
+
)
|
|
347
|
+
}
|
|
348
|
+
|
|
306
349
|
function reportDrift(drift: readonly FolderDrift[]): void {
|
|
307
350
|
logStep('Index drift')
|
|
308
351
|
|
package/src/context/audit.ts
CHANGED
|
@@ -25,11 +25,35 @@ const TABLE_ROW = /^\s*\|/
|
|
|
25
25
|
const TABLE_SEPARATOR = /^\s*\|[\s:|-]+\|\s*$/
|
|
26
26
|
const NAMED_CELL = /`[^`]+`|\[[^\]]+\]\([^)]+\)/
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Spellings of how the domain reached its shape rather than what it is now.
|
|
30
|
+
*
|
|
31
|
+
* The standard admits a rejected alternative and its reasoning while refusing
|
|
32
|
+
* the provenance attached to it, and these three are what a session reaches for
|
|
33
|
+
* when it records the second: when a change landed, which change carried it,
|
|
34
|
+
* and which release labelled it. A marker is a judgment rather than a defect,
|
|
35
|
+
* so this is measured and reported and never gates.
|
|
36
|
+
*/
|
|
37
|
+
const PROVENANCE: readonly { kind: ProvenanceKind; pattern: RegExp }[] = [
|
|
38
|
+
{ kind: 'date', pattern: /\b\d{4}-\d{2}-\d{2}\b/g },
|
|
39
|
+
{ kind: 'change', pattern: /#\d{3,}\b/g },
|
|
40
|
+
{ kind: 'release', pattern: /\bv\d+\.\d+(?:\.\d+)?\b/g },
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
export type ProvenanceKind = 'date' | 'change' | 'release'
|
|
44
|
+
|
|
28
45
|
export interface TableFinding {
|
|
29
46
|
readonly line: number
|
|
30
47
|
readonly rows: number
|
|
31
48
|
}
|
|
32
49
|
|
|
50
|
+
export interface ProvenanceFinding {
|
|
51
|
+
readonly line: number
|
|
52
|
+
readonly kind: ProvenanceKind
|
|
53
|
+
/** The marker as written, so a report names what to go and look at. */
|
|
54
|
+
readonly text: string
|
|
55
|
+
}
|
|
56
|
+
|
|
33
57
|
export interface EntryReport {
|
|
34
58
|
readonly rel: string
|
|
35
59
|
readonly lines: number
|
|
@@ -37,6 +61,7 @@ export interface EntryReport {
|
|
|
37
61
|
/** First line of the longest run, or 0 when the entry has no run at all. */
|
|
38
62
|
readonly longestRunLine: number
|
|
39
63
|
readonly catalogTables: readonly TableFinding[]
|
|
64
|
+
readonly provenance: readonly ProvenanceFinding[]
|
|
40
65
|
}
|
|
41
66
|
|
|
42
67
|
interface BodyLine {
|
|
@@ -191,6 +216,44 @@ function catalogTables(lines: readonly BodyLine[]): TableFinding[] {
|
|
|
191
216
|
return findings
|
|
192
217
|
}
|
|
193
218
|
|
|
219
|
+
/**
|
|
220
|
+
* Finds the markers narrating a change rather than describing the domain.
|
|
221
|
+
*
|
|
222
|
+
* Fenced blocks are skipped for the same reason the table scan skips them: a
|
|
223
|
+
* sample command or a fixture inside an example is content the entry displays
|
|
224
|
+
* rather than a claim it makes, and a version pinned in an install line is the
|
|
225
|
+
* ordinary shape of one.
|
|
226
|
+
*/
|
|
227
|
+
function provenance(lines: readonly BodyLine[]): ProvenanceFinding[] {
|
|
228
|
+
// Scanning one pattern at a time emits a line's markers grouped by kind, so
|
|
229
|
+
// the column is carried out of the match and sorted on. Without it a line
|
|
230
|
+
// holding a date and two change numbers reports them in an order the reader
|
|
231
|
+
// cannot find by scanning left to right.
|
|
232
|
+
const found: { finding: ProvenanceFinding; column: number }[] = []
|
|
233
|
+
let fenced = false
|
|
234
|
+
|
|
235
|
+
for (const line of lines) {
|
|
236
|
+
if (FENCE.test(line.text)) {
|
|
237
|
+
fenced = !fenced
|
|
238
|
+
continue
|
|
239
|
+
}
|
|
240
|
+
if (fenced) continue
|
|
241
|
+
|
|
242
|
+
for (const { kind, pattern } of PROVENANCE) {
|
|
243
|
+
for (const match of line.text.matchAll(pattern)) {
|
|
244
|
+
found.push({
|
|
245
|
+
finding: { line: line.number, kind, text: match[0] },
|
|
246
|
+
column: match.index,
|
|
247
|
+
})
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return found
|
|
253
|
+
.sort((a, b) => a.finding.line - b.finding.line || a.column - b.column)
|
|
254
|
+
.map((each) => each.finding)
|
|
255
|
+
}
|
|
256
|
+
|
|
194
257
|
export function measureEntry(rel: string, source: string): EntryReport {
|
|
195
258
|
const lines = bodyLines(source)
|
|
196
259
|
const run = longestRun(lines)
|
|
@@ -201,6 +264,7 @@ export function measureEntry(rel: string, source: string): EntryReport {
|
|
|
201
264
|
longestRun: run.length,
|
|
202
265
|
longestRunLine: run.line,
|
|
203
266
|
catalogTables: catalogTables(lines),
|
|
267
|
+
provenance: provenance(lines),
|
|
204
268
|
}
|
|
205
269
|
}
|
|
206
270
|
|
package/standards/context.md
CHANGED
|
@@ -94,11 +94,13 @@ Only the `development` entry carries this section. It is not a general-purpose h
|
|
|
94
94
|
- Tutorials or human onboarding. Those go in `docs/` if a public audience exists.
|
|
95
95
|
- Generated content (API references). Generate, do not write by hand.
|
|
96
96
|
- Anything already in `.claude/REQUIREMENTS.md` or `.claude/ARCHITECTURE.md`.
|
|
97
|
+
- The history of how the domain reached its current shape. An entry describes the repository as it stands, so a change number, release label, or date attached to a change goes wherever the project tracks work.
|
|
98
|
+
- A rejected alternative's provenance, which is the same rule at the one place the section above admits history. Keep what was tried and why it lost. Cut who tried it and when.
|
|
97
99
|
|
|
98
100
|
## Length
|
|
99
101
|
|
|
100
102
|
- Aim for one entry per domain. There is no hard cap. Length is a symptom, not the defect.
|
|
101
|
-
- Past roughly 150 lines, check
|
|
103
|
+
- Past roughly 150 lines, check three things before adding more: whether the entry still covers a single domain, whether it has filled with content `ls` or `--help` reproduces, and whether it has accumulated the history of its own changes. Fix whichever is true rather than trimming to hit a number.
|
|
102
104
|
- Past roughly 40 lines with no heading of any level breaking them, add a subheading at the seam. Measure the longest such run rather than everything under one `##`, and exclude fenced code blocks. The number is a checkpoint like the 150 above, not a cap.
|
|
103
105
|
- Exempt a block whose lines are all list items at one level. A long list of peers is already navigable, and a subheading dropped into it splits a set that belongs together. Mixing prose with the list, or nesting levels inside it, ends the exemption.
|
|
104
106
|
- Never cut a `## Decisions` or `## Gotchas` entry to shorten a file. Cut a `## Layout` or `## CLI` section instead.
|
package/standards/index.md
CHANGED
|
@@ -19,6 +19,6 @@ Reference docs for consistent authoring across the toolkit and target projects.
|
|
|
19
19
|
- [Claude skill reference](skill.md): Claude skill structure and authoring rules
|
|
20
20
|
- [Slug reference](slug.md): Transform from a git branch name to a slug, and the three responses to an empty result
|
|
21
21
|
- [Standard reference](standard.md): Shape and content rules for authoring a standard
|
|
22
|
-
- [Tasks reference](tasks.md): Folder layout, filename convention, and content rules for .claude/tasks/
|
|
22
|
+
- [Tasks reference](tasks.md): Folder layout, filename convention, readiness groups, and content rules for .claude/tasks/
|
|
23
23
|
- [Versioning reference](versioning.md): Phase label vs semver discipline across tasks, PRs, reviews, issues, commits, and tags
|
|
24
24
|
- [Wireframe reference](wireframes.md): Shape and content rules for .claude/wireframes/<surface>.md files
|
package/standards/tasks.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Tasks reference
|
|
3
|
-
description: Folder layout, filename convention, and content rules for .claude/tasks/
|
|
3
|
+
description: Folder layout, filename convention, readiness groups, and content rules for .claude/tasks/
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Tasks reference
|
|
@@ -11,7 +11,7 @@ The folder is gitignored. Board state changes when work ships rather than when a
|
|
|
11
11
|
|
|
12
12
|
## Scope
|
|
13
13
|
|
|
14
|
-
Governs the task board under `.claude/tasks/`: folder layout, filenames, frontmatter, file format, origin lines, and archiving.
|
|
14
|
+
Governs the task board under `.claude/tasks/`: folder layout, filenames, frontmatter, file format, origin lines, execution ordering, and archiving.
|
|
15
15
|
|
|
16
16
|
Does not govern:
|
|
17
17
|
|
|
@@ -34,11 +34,23 @@ One file per task is what keeps the board safe under parallel sessions. Two sess
|
|
|
34
34
|
|
|
35
35
|
`index.md` is generated from sibling frontmatter. The folder is gitignored, so the whole-repo index walk skips it and a hook passing the changed path regenerates it instead. Never hand-edit it.
|
|
36
36
|
|
|
37
|
+
The `claude-tasks` skill creates and archives task files. `claude-docs` marks outcomes `[x]` in an existing file and sweeps the plans those tasks cite. Neither does the other's job.
|
|
38
|
+
|
|
39
|
+
## Ordering
|
|
40
|
+
|
|
37
41
|
`priority.md` carries execution order and what each task is waiting on. The generated index sorts by filename and says nothing about order, so without this file board state gets reconstructed by hand every session. Why the order is what it is belongs in `.claude/ROADMAP.md`, which is committed because that rationale has no substitute record.
|
|
38
42
|
|
|
39
|
-
Group tasks by readiness rather than by status, one row per task,
|
|
43
|
+
Group tasks by readiness rather than by status, one row per task, under the columns each group fixes below. Keep it to links and blockers: tables, plus at most one sentence per section. A paragraph in `priority.md` is a defect whatever it says. Stating the shape this way is what lets a single diff fail, since a size cap only trips after the fact and every addition looks defensible on its own.
|
|
40
44
|
|
|
41
|
-
|
|
45
|
+
Readiness is three groups under fixed headings, `## Run now`, `## Up next`, and `## Needs a plan`, in that order. The names are the contract rather than a suggestion, because a board grouped by readiness under names of its own satisfies every other rule here and still reads as empty to anything counting rows under a heading. Add no fourth group. A task belongs to exactly one, and the tests are read in order.
|
|
46
|
+
|
|
47
|
+
- `## Run now`: a written plan covers every open outcome, and the files the task touches collide with nothing already running. A worker is handed a task from this group alone.
|
|
48
|
+
- `## Up next`: a written plan exists, and the task either collides with something running or waits on another task to land. The blocker column names which.
|
|
49
|
+
- `## Needs a plan`: everything else. The task has no plan, or the plan it carries no longer describes the work.
|
|
50
|
+
|
|
51
|
+
Each group fixes its own columns, which follow from the test above it rather than from preference. `## Run now` carries `Task`, `Touches`, and `Plan`, since neither half of its test is checkable without the last two. `## Up next` carries `Task`, `Touches`, and `Waiting on`, the last naming whether a collision or a dependency holds it. `## Needs a plan` carries `Task` and `Waiting on` alone, because a task with no plan has no bounded file set to state.
|
|
52
|
+
|
|
53
|
+
The tests live here so the board does not carry them. Writing them as a sentence under each heading produces the paragraph the rule above deletes, and a criterion with no home gets restated from memory every time the board is touched.
|
|
42
54
|
|
|
43
55
|
## Filenames
|
|
44
56
|
|