@erclx/aitk 0.37.0 → 0.39.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/REQUIREMENT.md +42 -0
- package/claude/skills/claude-diagram/REQUIREMENT.md +45 -0
- package/claude/skills/claude-groundwork/REQUIREMENT.md +44 -0
- package/claude/skills/claude-memory-capture/REQUIREMENT.md +40 -0
- package/claude/skills/claude-orchestrate/REQUIREMENT.md +44 -0
- package/claude/skills/claude-roadmap/REQUIREMENT.md +41 -0
- package/claude/skills/claude-screencast/REQUIREMENT.md +39 -0
- package/claude/skills/claude-slides-draft/REQUIREMENT.md +39 -0
- package/claude/skills/claude-tasks/REQUIREMENT.md +43 -0
- package/claude/skills/claude-ui-test/REQUIREMENT.md +40 -0
- package/claude/skills/claude-ux-audit/REQUIREMENT.md +39 -0
- package/claude/skills/claude-worktree/REQUIREMENT.md +41 -0
- package/claude/skills/cli-script/REQUIREMENT.md +40 -0
- package/claude/skills/create-rule/REQUIREMENT.md +42 -0
- package/claude/skills/create-skill/REQUIREMENT.md +6 -2
- package/claude/skills/create-skill/SKILL.md +5 -2
- package/claude/skills/create-snippet/REQUIREMENT.md +36 -0
- package/claude/skills/create-snippet/SKILL.md +23 -5
- package/claude/skills/{create-standard → create-snippet}/references/snippets.md +1 -1
- package/claude/skills/create-standard/REQUIREMENT.md +38 -0
- package/claude/skills/create-standard/SKILL.md +8 -12
- package/claude/skills/migration-claude-md/REQUIREMENT.md +41 -0
- package/claude/skills/migration-standards/REQUIREMENT.md +38 -0
- package/claude/skills/project-commands/REQUIREMENT.md +42 -0
- package/claude/skills/session-resume/REQUIREMENT.md +38 -0
- package/claude/skills/setup-indexes/REQUIREMENT.md +42 -0
- package/claude/skills/setup-plugins/REQUIREMENT.md +42 -0
- package/claude/skills/setup-verify/REQUIREMENT.md +39 -0
- package/claude/skills/systematic-debugging/REQUIREMENT.md +41 -0
- package/claude/skills/toolkit-cli/REQUIREMENT.md +37 -0
- package/claude/skills/toolkit-feedback/REQUIREMENT.md +40 -0
- package/claude/skills/toolkit-operator/REQUIREMENT.md +39 -0
- package/claude/skills/toolkit-triage/REQUIREMENT.md +40 -0
- package/claude/skills/youtube-transcripts/REQUIREMENT.md +38 -0
- package/docs/agents.md +17 -9
- package/docs/operating-model.md +1 -1
- package/docs/visual-design-workflow.md +1 -1
- package/package.json +1 -1
- package/scripts/manage-sandbox.sh +4 -0
- package/src/commands/context.ts +76 -11
- package/src/context/folders.ts +85 -13
- package/src/sandbox/census.ts +1 -1
- package/standards/bundled/snippets.md +1 -1
- package/standards/skill.md +4 -4
- package/claude/skills/claude-design-propose/SKILL.md +0 -15
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: toolkit-triage
|
|
3
|
+
description: Why the feedback queue is read from GitHub rather than local scratch, and why triage routes instead of implementing
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Toolkit triage requirement
|
|
7
|
+
|
|
8
|
+
## Gap
|
|
9
|
+
|
|
10
|
+
Without this skill, the feedback queue fills and nothing drains it. Reports arrive from projects the toolkit never sees, and a queue nobody reads on a schedule is the same as no queue. The local review folder looks like the right place to read, and it is the wrong one, because that content is per-machine session scratch that any cleanup removes.
|
|
11
|
+
|
|
12
|
+
Triage fails three ways once it starts. Unrelated issues get batched into one branch, so review has to judge several unconnected changes at once and can approve none of them cleanly. An issue needing a plan gets implemented inline from a paragraph of description, which skips the step where scope is argued. And an issue whose report contradicts itself gets a guess rather than a question, so the fix addresses a defect nobody confirmed.
|
|
13
|
+
|
|
14
|
+
The queue also fails to drain even when the work ships. A fix merged with no link back leaves its issue open, and the next triage re-reads work already done.
|
|
15
|
+
|
|
16
|
+
## Must
|
|
17
|
+
|
|
18
|
+
- Read the durable queue rather than local session scratch
|
|
19
|
+
- Classify each issue in a fixed order and stop at the first match, stating the class and its one-line reason before routing
|
|
20
|
+
- Route to the skills that own planning, branch naming, and the pull request body rather than restating them
|
|
21
|
+
- Keep one issue to one branch and one pull request
|
|
22
|
+
- Link a fix to its issue so merge closes it, and preserve that link when the body is regenerated
|
|
23
|
+
- Ask on the issue when the report is unreadable, then move on rather than blocking the batch
|
|
24
|
+
|
|
25
|
+
## Must not
|
|
26
|
+
|
|
27
|
+
- Batch unrelated fixes into one branch
|
|
28
|
+
- Implement a plan-worthy issue, which stops at the plan handed back to the user
|
|
29
|
+
- Close an issue whose pull request has yet to merge
|
|
30
|
+
|
|
31
|
+
## Guards
|
|
32
|
+
|
|
33
|
+
- The `gh` CLI absent or unauthenticated stops, since the queue is unreachable
|
|
34
|
+
- An empty queue reports nothing open rather than widening the label to find work
|
|
35
|
+
|
|
36
|
+
## Out of scope
|
|
37
|
+
|
|
38
|
+
- Filing new feedback: `toolkit-feedback`
|
|
39
|
+
- Writing the plan a plan-worthy issue needs: `claude-feature`
|
|
40
|
+
- Triage of issues carrying any other label, which surface here by design only under the feedback label
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: youtube-transcripts
|
|
3
|
+
description: Why the transcript fetch stays in the CLI, and why fetching stops short of reading
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Youtube transcripts requirement
|
|
7
|
+
|
|
8
|
+
## Gap
|
|
9
|
+
|
|
10
|
+
Without this skill, a pasted video link turns into captions printed in chat, which persist nowhere and cannot be cited by a later session. The file is the point. A transcript saved with its metadata is context the repository holds, and a transcript in a message is context that ends with the turn.
|
|
11
|
+
|
|
12
|
+
A session that fetches on its own gets the shape wrong. Raw captions carry timing cues and duplicated lines, and a file written without frontmatter has no title, no source URL, and no way to tell which video it came from. Reimplementing the cleanup also duplicates logic the CLI already owns, so the two drift and the drift shows up as inconsistent files rather than as an error.
|
|
13
|
+
|
|
14
|
+
Two smaller failures follow the fetch. A video with no captions produces a file that looks empty rather than one reported as having none, so the user reads it as a failed fetch. And a session that summarizes what it fetched answers a question nobody asked, spending context on a reading the user may not want yet.
|
|
15
|
+
|
|
16
|
+
## Must
|
|
17
|
+
|
|
18
|
+
- Run the CLI, which owns the fetch, the caption cleanup, and the frontmatter
|
|
19
|
+
- Surface the written path as a full relative path so the terminal makes it clickable
|
|
20
|
+
- Report a video with no captions as a fact about the video, since the file still exists and carries its metadata
|
|
21
|
+
- Name the missing external binary and stop, leaving the install to the user
|
|
22
|
+
|
|
23
|
+
## Must not
|
|
24
|
+
|
|
25
|
+
- Reimplement the fetch or the cleanup
|
|
26
|
+
- Summarize or annotate the transcript unasked, which is a separate request against the file
|
|
27
|
+
- Install the external binary the command shells out to
|
|
28
|
+
|
|
29
|
+
## Guards
|
|
30
|
+
|
|
31
|
+
- No URL stops, since there is nothing to fetch
|
|
32
|
+
- A link that is not a YouTube URL stops rather than attempting a generic fetch the command cannot do
|
|
33
|
+
|
|
34
|
+
## Out of scope
|
|
35
|
+
|
|
36
|
+
- Downloading the video or its audio
|
|
37
|
+
- Summarizing a transcript already on disk, which is a read of that file
|
|
38
|
+
- Choosing where transcripts live, which the command defaults and a flag overrides
|
package/docs/agents.md
CHANGED
|
@@ -377,16 +377,21 @@ aitk context audit
|
|
|
377
377
|
aitk context audit --json
|
|
378
378
|
aitk context audit --citations-only
|
|
379
379
|
aitk context audit --folder context,diagrams
|
|
380
|
+
aitk context audit --folder docs
|
|
380
381
|
```
|
|
381
382
|
|
|
382
|
-
| Option | Behavior
|
|
383
|
-
| ------------------ |
|
|
384
|
-
| `--json` | Add a machine-readable record on stdout, keeping the frame
|
|
385
|
-
| `--folder <list>` | Comma-separated folder names
|
|
386
|
-
| `--citations-only` | Run the gating citation check alone, printing nothing when it passes
|
|
383
|
+
| Option | Behavior |
|
|
384
|
+
| ------------------ | -------------------------------------------------------------------- |
|
|
385
|
+
| `--json` | Add a machine-readable record on stdout, keeping the frame |
|
|
386
|
+
| `--folder <list>` | Comma-separated folder names (default: the three below) |
|
|
387
|
+
| `--citations-only` | Run the gating citation check alone, printing nothing when it passes |
|
|
387
388
|
|
|
388
389
|
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.
|
|
389
390
|
|
|
391
|
+
A name passed to `--folder` resolves under `.claude/` first and at the project root second, which is what puts `docs/` and any later corpus in reach of the same engine. The root base is reached only by a name the caller passes, so the default list still resolves under `.claude/` alone and a project holding a root `wireframes/` is not audited against a standard it never adopted. The scope line prints the resolved path, so a project carrying both spellings reads which one was taken. The JSON record carries the base per folder as `folders[].base`.
|
|
392
|
+
|
|
393
|
+
A run where no requested name resolves refuses, whichever list it read. Naming the absent ones narrows to `--folder`, since a project carrying one of the three default folders is the ordinary case and a name it never asked for is not a typo. The JSON record carries those names as `unresolvedFolders`.
|
|
394
|
+
|
|
390
395
|
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. Required-section, length, depth, bullet weight, 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.
|
|
391
396
|
|
|
392
397
|
### What each check reports
|
|
@@ -419,6 +424,8 @@ Three exclusions keep it from firing on prose about paths. Fenced blocks are ski
|
|
|
419
424
|
|
|
420
425
|
What remains is a sentence naming a hypothetical entry to show the shape of a name, which no syntax separates from a real reference. Append `<!-- audit-ignore-citations -->` to that source line. The marker suppresses citation checking for its own line only.
|
|
421
426
|
|
|
427
|
+
The pattern spells the `.claude/` prefix, so a folder resolved at the project root is measured by every other check and contributes nothing here. Widening it to a bare `docs/x.md` would match prose that references nothing, which is a separate decision from where entries come from. A run whose folders all resolved at the root says the check is out of scope rather than reporting that zero paths resolved, and the same run under `--citations-only` refuses, because a gate exiting clean on a scope it could not build is the failure the gate exists to catch.
|
|
428
|
+
|
|
422
429
|
## Runtime catalogs
|
|
423
430
|
|
|
424
431
|
Use these to discover what's available instead of hardcoding names.
|
|
@@ -459,10 +466,11 @@ one malformed file cannot hide the rest of the catalog. `--names` emits skill
|
|
|
459
466
|
names one per line.
|
|
460
467
|
|
|
461
468
|
Each entry also carries `requirement`, whether the folder holds a sibling
|
|
462
|
-
`REQUIREMENT.md`.
|
|
463
|
-
|
|
464
|
-
listing the directory itself.
|
|
465
|
-
|
|
469
|
+
`REQUIREMENT.md`. Every skill is meant to carry one, so a `false` is a gap to
|
|
470
|
+
close rather than a recorded exemption, and the flag answers which skills are
|
|
471
|
+
missing theirs without a caller listing the directory itself. Nothing gates the
|
|
472
|
+
rule yet, which is why the flag is worth reading against the shipped corpus after
|
|
473
|
+
a merge.
|
|
466
474
|
|
|
467
475
|
## Non-interactive examples
|
|
468
476
|
|
package/docs/operating-model.md
CHANGED
|
@@ -104,4 +104,4 @@ port-collision detail.
|
|
|
104
104
|
|
|
105
105
|
- [Claude Code and git worktrees](../wiki/claude-worktrees.md) for the isolation and fan-out mechanics
|
|
106
106
|
- [Claude Code subagents](../wiki/claude-subagents.md) for in-session parallelism without worktrees
|
|
107
|
-
- `.claude/context/claude-plugin/
|
|
107
|
+
- `.claude/context/claude-plugin/skill-strategy.md` for how the skills in the loop are categorized
|
|
@@ -130,7 +130,7 @@ Zero or one yes: tier 0. Two or three: tier 1. Four: tier 2. Resist over-tiering
|
|
|
130
130
|
|
|
131
131
|
- [Stitch](../wiki/stitch.md): Gemini-powered design product with the MCP server that anchors tier 1
|
|
132
132
|
- [Claude Design](../wiki/claude-design.md): first-party hosted design product and handoff bundle
|
|
133
|
-
- `.claude/context/claude-plugin/
|
|
133
|
+
- `.claude/context/claude-plugin/skill-strategy.md`: how to decide between workflow and domain-knowledge skills
|
|
134
134
|
- [Visual wireframes](../wiki/visual-wireframes.md): Excalidraw research and setup for the tier 1 wireframe companion
|
|
135
135
|
- [Community skills and plugins](../wiki/community-skills.md): catalog of frontend design skills and integrations
|
|
136
136
|
- [Community MCP servers](../wiki/community-mcp-servers.md): catalog of MCPs referenced across all tiers
|
package/package.json
CHANGED
|
@@ -296,6 +296,10 @@ inject_changed_skills() {
|
|
|
296
296
|
[ -z "$combined" ] && return
|
|
297
297
|
|
|
298
298
|
while IFS= read -r skill_path; do
|
|
299
|
+
# The diff against main lists a deleted skill alongside a changed one, and
|
|
300
|
+
# there is nothing left to inject for a name this branch removed.
|
|
301
|
+
[ -f "$PROJECT_ROOT/$skill_path" ] || continue
|
|
302
|
+
|
|
299
303
|
local skill_name
|
|
300
304
|
skill_name=$(basename "$(dirname "$skill_path")")
|
|
301
305
|
local target_dir="$SANDBOX/.claude/skills/$skill_name"
|
package/src/commands/context.ts
CHANGED
|
@@ -59,7 +59,10 @@ export function register(program: Command): void {
|
|
|
59
59
|
.argument('[path]', 'Project root, defaulting to the current directory')
|
|
60
60
|
.helpOption('-h, --help', 'Show this help message')
|
|
61
61
|
.option('--json', 'Add a machine-readable record on stdout')
|
|
62
|
-
.option(
|
|
62
|
+
.option(
|
|
63
|
+
'--folder <list>',
|
|
64
|
+
'Comma-separated folder names, resolved under .claude/ then the project root',
|
|
65
|
+
)
|
|
63
66
|
.option('--citations-only', 'Run the gating citation check alone')
|
|
64
67
|
.addHelpText(
|
|
65
68
|
'after',
|
|
@@ -78,6 +81,7 @@ export function register(program: Command): void {
|
|
|
78
81
|
' aitk context audit --json',
|
|
79
82
|
' aitk context audit --citations-only',
|
|
80
83
|
' aitk context audit --folder context,diagrams',
|
|
84
|
+
' aitk context audit --folder docs',
|
|
81
85
|
'',
|
|
82
86
|
].join('\n'),
|
|
83
87
|
)
|
|
@@ -96,11 +100,11 @@ function parseFolders(list: string | undefined): string[] | string {
|
|
|
96
100
|
|
|
97
101
|
if (names.length === 0) return 'Empty --folder list. Pass at least one name.'
|
|
98
102
|
|
|
99
|
-
// `..` would resolve the audited folder above
|
|
100
|
-
// and the citation pattern outside the tree the audit describes.
|
|
103
|
+
// `..` would resolve the audited folder above the project root, taking the
|
|
104
|
+
// scan and the citation pattern outside the tree the audit describes.
|
|
101
105
|
const invalid = names.filter((name) => !FOLDER_NAME.test(name))
|
|
102
106
|
if (invalid.length > 0) {
|
|
103
|
-
return `--folder takes folder names
|
|
107
|
+
return `--folder takes folder names, not paths: ${invalid.join(', ')}`
|
|
104
108
|
}
|
|
105
109
|
|
|
106
110
|
return names
|
|
@@ -116,15 +120,38 @@ async function runAudit(
|
|
|
116
120
|
|
|
117
121
|
if (typeof names === 'string') return refuse(names, gateOnly)
|
|
118
122
|
|
|
119
|
-
|
|
123
|
+
// The root base is opt-in. A target carrying a root `wireframes/` would
|
|
124
|
+
// otherwise be audited against a standard it never adopted, on a bare run
|
|
125
|
+
// that named nothing.
|
|
126
|
+
const named = opts.folder !== undefined
|
|
127
|
+
const { folders, missing } = await resolveFolders(root, names, {
|
|
128
|
+
canResolveAtRoot: named,
|
|
129
|
+
})
|
|
120
130
|
if (folders.length === 0) {
|
|
121
131
|
return refuse(
|
|
122
|
-
`No audited folder found under .claude
|
|
132
|
+
`No audited folder found ${named ? 'under .claude/ or the project root' : 'under .claude/'}. Looked for: ${names.join(', ')}.`,
|
|
123
133
|
gateOnly,
|
|
124
134
|
)
|
|
125
135
|
}
|
|
126
136
|
|
|
127
|
-
|
|
137
|
+
// A default folder a project does not carry is the ordinary case and stays
|
|
138
|
+
// silent. A name passed by hand that resolves nowhere is a typo, and the run
|
|
139
|
+
// measuring the names that did resolve reads as a pass against a folder it
|
|
140
|
+
// never opened.
|
|
141
|
+
const unresolved = named ? missing : []
|
|
142
|
+
|
|
143
|
+
// The gate runs one check. Letting it exit 0 against a scope it could not
|
|
144
|
+
// build reports a pass on nothing measured, which is the outcome a gate is
|
|
145
|
+
// there to prevent.
|
|
146
|
+
const cited = presentNames(folders)
|
|
147
|
+
if (gateOnly && cited.length === 0) {
|
|
148
|
+
return refuse(
|
|
149
|
+
`The citation check spells the .claude/ prefix and no audited folder resolved there. Looked for: ${names.join(', ')}.`,
|
|
150
|
+
gateOnly,
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const citations = await auditCitations(root, cited)
|
|
128
155
|
if (citations.kind === 'unavailable') {
|
|
129
156
|
return refuse(
|
|
130
157
|
'git could not list the tree, so no citation was checked. Run inside a git repository.',
|
|
@@ -140,8 +167,8 @@ async function runAudit(
|
|
|
140
167
|
reportGate(citations)
|
|
141
168
|
} else {
|
|
142
169
|
intro('aitk context audit')
|
|
143
|
-
reportScope(folders)
|
|
144
|
-
reportCitations(citations)
|
|
170
|
+
reportScope(folders, unresolved)
|
|
171
|
+
reportCitations(citations, cited)
|
|
145
172
|
reportSections(sections, folders)
|
|
146
173
|
reportLength(entries)
|
|
147
174
|
reportDepth(entries)
|
|
@@ -158,9 +185,11 @@ async function runAudit(
|
|
|
158
185
|
root,
|
|
159
186
|
folders: folders.map((folder) => ({
|
|
160
187
|
path: folder.rel,
|
|
188
|
+
base: folder.base,
|
|
161
189
|
entries: folder.entries.length,
|
|
162
190
|
governsContent: governsContent(folder),
|
|
163
191
|
})),
|
|
192
|
+
unresolvedFolders: unresolved,
|
|
164
193
|
citations: {
|
|
165
194
|
scanned: citations.scanned,
|
|
166
195
|
total: citations.total,
|
|
@@ -228,18 +257,54 @@ function plural(count: number, noun: string): string {
|
|
|
228
257
|
return `${count} ${noun}${count === 1 ? '' : 's'}`
|
|
229
258
|
}
|
|
230
259
|
|
|
231
|
-
|
|
260
|
+
/**
|
|
261
|
+
* Names the resolved path of every audited folder, plus the requested names
|
|
262
|
+
* that resolved nowhere.
|
|
263
|
+
*
|
|
264
|
+
* The path is what says which base a name was taken from, which matters once a
|
|
265
|
+
* name can resolve under `.claude/` or at the project root and a project may
|
|
266
|
+
* carry both.
|
|
267
|
+
*/
|
|
268
|
+
function reportScope(
|
|
269
|
+
folders: readonly AuditedFolder[],
|
|
270
|
+
unresolved: readonly string[],
|
|
271
|
+
): void {
|
|
232
272
|
logStep('Scope')
|
|
233
273
|
|
|
234
274
|
for (const folder of folders) {
|
|
235
275
|
logInfo(`${folder.rel}: ${folder.entries.length} entries`)
|
|
236
276
|
}
|
|
277
|
+
|
|
278
|
+
if (unresolved.length === 0) return
|
|
279
|
+
|
|
280
|
+
logWarn(
|
|
281
|
+
`Under neither .claude/ nor the project root: ${unresolved.join(', ')}`,
|
|
282
|
+
)
|
|
237
283
|
}
|
|
238
284
|
|
|
239
285
|
type ScannedCitations = Extract<CitationReport, { kind: 'scanned' }>
|
|
240
286
|
|
|
241
|
-
|
|
287
|
+
/**
|
|
288
|
+
* States the reach before the count, for the reason the provenance report
|
|
289
|
+
* states its own.
|
|
290
|
+
*
|
|
291
|
+
* A run auditing a folder at the project root builds no pattern, and a count of
|
|
292
|
+
* zero followed by a line saying every path resolves is indistinguishable from
|
|
293
|
+
* a corpus that cites nothing.
|
|
294
|
+
*/
|
|
295
|
+
function reportCitations(
|
|
296
|
+
report: ScannedCitations,
|
|
297
|
+
cited: readonly string[],
|
|
298
|
+
): void {
|
|
242
299
|
logStep('Citations')
|
|
300
|
+
|
|
301
|
+
if (cited.length === 0) {
|
|
302
|
+
logInfo(
|
|
303
|
+
'Out of scope. The pattern spells the .claude/ prefix, and no audited folder resolved there.',
|
|
304
|
+
)
|
|
305
|
+
return
|
|
306
|
+
}
|
|
307
|
+
|
|
243
308
|
logInfo(
|
|
244
309
|
`${plural(report.total, 'cited path')} across ${plural(report.scanned, 'file')}, fixtures and fenced examples excluded`,
|
|
245
310
|
)
|
package/src/context/folders.ts
CHANGED
|
@@ -17,6 +17,12 @@ export const DEFAULT_FOLDERS: readonly string[] = [
|
|
|
17
17
|
'wireframes',
|
|
18
18
|
]
|
|
19
19
|
|
|
20
|
+
/** The base every folder in the default list sits under. */
|
|
21
|
+
const CLAUDE_BASE = '.claude'
|
|
22
|
+
|
|
23
|
+
/** The project root, reached only by a name the caller asked for. */
|
|
24
|
+
const ROOT_BASE = '.'
|
|
25
|
+
|
|
20
26
|
export interface AuditedFolder {
|
|
21
27
|
/**
|
|
22
28
|
* The requested folder name this was resolved under, which is what says
|
|
@@ -25,6 +31,11 @@ export interface AuditedFolder {
|
|
|
25
31
|
* `.claude/context/claude-plugin` is governed as `context`.
|
|
26
32
|
*/
|
|
27
33
|
readonly name: string
|
|
34
|
+
/**
|
|
35
|
+
* The base the name resolved under, which is what says whether the folder is
|
|
36
|
+
* in the citation check's scope.
|
|
37
|
+
*/
|
|
38
|
+
readonly base: string
|
|
28
39
|
/** Repo-relative folder path, used verbatim in every report line. */
|
|
29
40
|
readonly rel: string
|
|
30
41
|
readonly indexPath: string
|
|
@@ -43,16 +54,28 @@ export interface AuditedFolder {
|
|
|
43
54
|
}
|
|
44
55
|
|
|
45
56
|
/**
|
|
46
|
-
* Names the requested folders that actually exist, which is the
|
|
47
|
-
* check's scope.
|
|
57
|
+
* Names the requested `.claude/` folders that actually exist, which is the
|
|
58
|
+
* citation check's scope.
|
|
48
59
|
*
|
|
49
60
|
* A skill or seed pointing into `.claude/wireframes/` is a live instruction for
|
|
50
61
|
* a project that carries the folder and says nothing about one that does not.
|
|
51
62
|
* Checking a path into an absent folder would fail eight shipped references
|
|
52
63
|
* here for the sole reason that this repository has no wireframes.
|
|
64
|
+
*
|
|
65
|
+
* A folder resolved at the project root is measured and stays out of this. The
|
|
66
|
+
* pattern the citation check builds spells the `.claude/` prefix, so admitting
|
|
67
|
+
* a root name there would check `.claude/<name>/` paths the audit never read.
|
|
68
|
+
* Widening the pattern to the root spelling is a separate change, since a bare
|
|
69
|
+
* `docs/x.md` appears in prose that references nothing.
|
|
53
70
|
*/
|
|
54
71
|
export function presentNames(folders: readonly AuditedFolder[]): string[] {
|
|
55
|
-
return [
|
|
72
|
+
return [
|
|
73
|
+
...new Set(
|
|
74
|
+
folders
|
|
75
|
+
.filter((folder) => folder.base === CLAUDE_BASE)
|
|
76
|
+
.map((folder) => folder.name),
|
|
77
|
+
),
|
|
78
|
+
]
|
|
56
79
|
}
|
|
57
80
|
|
|
58
81
|
async function readEntries(dir: string): Promise<string[]> {
|
|
@@ -70,6 +93,40 @@ async function readEntries(dir: string): Promise<string[]> {
|
|
|
70
93
|
return paths.sort()
|
|
71
94
|
}
|
|
72
95
|
|
|
96
|
+
export interface FolderResolution {
|
|
97
|
+
/** Every folder that resolved, with the nested splits beneath each. */
|
|
98
|
+
readonly folders: readonly AuditedFolder[]
|
|
99
|
+
/**
|
|
100
|
+
* Requested names that resolved under no base, reported rather than dropped.
|
|
101
|
+
* Which absences are worth saying out loud is the caller's judgment: a
|
|
102
|
+
* default folder a project does not carry is ordinary, and a name passed by
|
|
103
|
+
* hand that resolves nowhere is a typo that would otherwise read as a pass.
|
|
104
|
+
*/
|
|
105
|
+
readonly missing: readonly string[]
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface ResolveOptions {
|
|
109
|
+
/**
|
|
110
|
+
* Whether a name may resolve at the project root when `.claude/` does not
|
|
111
|
+
* carry it. False for the default list, which names three folders a project
|
|
112
|
+
* is expected to hold under `.claude/` and nowhere else.
|
|
113
|
+
*/
|
|
114
|
+
readonly canResolveAtRoot?: boolean
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function locate(
|
|
118
|
+
root: string,
|
|
119
|
+
name: string,
|
|
120
|
+
bases: readonly string[],
|
|
121
|
+
): { readonly dir: string; readonly base: string } | undefined {
|
|
122
|
+
for (const base of bases) {
|
|
123
|
+
const dir = resolve(root, base, name)
|
|
124
|
+
if (existsSync(`${dir}/${INDEX_FILE}`)) return { dir, base }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return undefined
|
|
128
|
+
}
|
|
129
|
+
|
|
73
130
|
/**
|
|
74
131
|
* Resolves the folders to audit under `root`.
|
|
75
132
|
*
|
|
@@ -77,34 +134,49 @@ async function readEntries(dir: string): Promise<string[]> {
|
|
|
77
134
|
* folder beneath it, so a domain that outgrew one file and split is audited at
|
|
78
135
|
* the same grain as one that did not. Discovery of the nested folders runs
|
|
79
136
|
* through the shared walker, which is what keeps `.gitignore` and the vendored
|
|
80
|
-
* prune governing this scan as well as index regeneration.
|
|
137
|
+
* prune governing this scan as well as index regeneration. That prune is what
|
|
138
|
+
* lets a root folder be walked at all, since a name at the project root sits
|
|
139
|
+
* beside `node_modules` and a build output.
|
|
81
140
|
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
141
|
+
* The project root is reached only when the caller opts in, so a target holding
|
|
142
|
+
* a root `wireframes/` is not audited against a standard it never adopted by
|
|
143
|
+
* the mere act of running the command. `.claude/` still wins a name carried by
|
|
144
|
+
* both, and the scope line prints the resolved path so a caller reads which
|
|
145
|
+
* base was taken rather than inferring it.
|
|
146
|
+
*
|
|
147
|
+
* Nothing above this asks where a folder came from. A name that resolves at the
|
|
148
|
+
* root is measured by every rule that generalizes and gated out of the rules a
|
|
149
|
+
* single standard carries, which `governsContent` decides from the name.
|
|
85
150
|
*/
|
|
86
151
|
export async function resolveFolders(
|
|
87
152
|
root: string,
|
|
88
153
|
names: readonly string[] = DEFAULT_FOLDERS,
|
|
89
|
-
|
|
154
|
+
{ canResolveAtRoot = false }: ResolveOptions = {},
|
|
155
|
+
): Promise<FolderResolution> {
|
|
156
|
+
const bases = canResolveAtRoot ? [CLAUDE_BASE, ROOT_BASE] : [CLAUDE_BASE]
|
|
90
157
|
const folders: AuditedFolder[] = []
|
|
158
|
+
const missing: string[] = []
|
|
91
159
|
|
|
92
160
|
for (const name of names) {
|
|
93
|
-
const
|
|
94
|
-
if (!
|
|
161
|
+
const found = locate(root, name, bases)
|
|
162
|
+
if (!found) {
|
|
163
|
+
missing.push(name)
|
|
164
|
+
continue
|
|
165
|
+
}
|
|
95
166
|
|
|
96
|
-
const dirs = [dir, ...(await listIndexes(dir)).map(dirname)]
|
|
167
|
+
const dirs = [found.dir, ...(await listIndexes(found.dir)).map(dirname)]
|
|
97
168
|
|
|
98
169
|
for (const each of [...new Set(dirs)].sort()) {
|
|
99
170
|
folders.push({
|
|
100
171
|
name,
|
|
172
|
+
base: found.base,
|
|
101
173
|
rel: relative(root, each),
|
|
102
174
|
indexPath: `${each}/${INDEX_FILE}`,
|
|
103
175
|
entries: await readEntries(each),
|
|
104
|
-
nested: each !== dir,
|
|
176
|
+
nested: each !== found.dir,
|
|
105
177
|
})
|
|
106
178
|
}
|
|
107
179
|
}
|
|
108
180
|
|
|
109
|
-
return folders
|
|
181
|
+
return { folders, missing }
|
|
110
182
|
}
|
package/src/sandbox/census.ts
CHANGED
|
@@ -70,7 +70,7 @@ export function listSkills(root: string): string[] {
|
|
|
70
70
|
* Maps a scenario to the skill it drives, trying two spellings in order.
|
|
71
71
|
*
|
|
72
72
|
* `<category>-<command>` is the rule `.claude/context/sandbox/coverage.md` states, and it
|
|
73
|
-
* alone pairs 29 of
|
|
73
|
+
* alone pairs 29 of 54 skills. The bare `<command>` fallback is what reaches the
|
|
74
74
|
* rest: `claude/setup-init.sh` drives the `setup-init` skill, not a
|
|
75
75
|
* `claude-setup-init` that does not exist. Stating one spelling and shipping two
|
|
76
76
|
* is what let the audit report a paired skill as unpaired.
|
package/standards/skill.md
CHANGED
|
@@ -11,7 +11,7 @@ Skills give Claude Code domain-specific constraints and rules inline, so it can
|
|
|
11
11
|
|
|
12
12
|
## Scope
|
|
13
13
|
|
|
14
|
-
Governs a skill folder under `skills/` as one artifact: `SKILL.md`, its
|
|
14
|
+
Governs a skill folder under `skills/` as one artifact: `SKILL.md`, its sibling `REQUIREMENT.md`, and the bundled reference, script, and asset folders beside them.
|
|
15
15
|
|
|
16
16
|
Does not govern:
|
|
17
17
|
|
|
@@ -68,14 +68,14 @@ allowed-tools: <tools required>
|
|
|
68
68
|
|
|
69
69
|
## Structure
|
|
70
70
|
|
|
71
|
-
- Skill is a folder named in kebab-case containing `SKILL.md` (required), `REQUIREMENT.md` (
|
|
71
|
+
- Skill is a folder named in kebab-case containing `SKILL.md` (required), `REQUIREMENT.md` (required), `scripts/` (optional), `references/` (optional), `assets/` (optional)
|
|
72
72
|
- `SKILL.md` must start with YAML frontmatter between `---` delimiters
|
|
73
73
|
- No `README.md` inside the skill folder
|
|
74
74
|
- No spaces, capitals, or underscores in folder or skill name
|
|
75
75
|
|
|
76
76
|
## Requirement
|
|
77
77
|
|
|
78
|
-
`REQUIREMENT.md` states what a skill is for, so a proposed change can be argued against something. This standard governs shape across the corpus and the requirement governs scope for one skill. Claude Code loads `SKILL.md` as the entry and ignores the sibling, so the file costs a target session nothing.
|
|
78
|
+
`REQUIREMENT.md` states what a skill is for, so a proposed change can be argued against something and the corpus can be read to decide whether a skill should exist at all. This standard governs shape across the corpus and the requirement governs scope and behavior for one skill. Claude Code loads `SKILL.md` as the entry and ignores the sibling, so the file costs a target session nothing.
|
|
79
79
|
|
|
80
80
|
The file serves a second purpose beside the argument. A skill body is procedural by design, so what the skill is for sits spread across its steps rather than stated in one place. The requirement is the compressed statement a reader gets before opening the body, and that purpose holds whether or not the skill's scope is contested.
|
|
81
81
|
|
|
@@ -83,7 +83,7 @@ Read it before editing the skill. When a change closes no gap the requirement st
|
|
|
83
83
|
|
|
84
84
|
Write the gaps from what the skill is for, then compare the body against them. Deriving the requirement from the body is circular, because a requirement reverse-engineered from an overfitted skill records the overfitting as the requirement. That failure costs more under the orientation purpose than under the argument one, since a file summarizing the body misinforms whoever reads it in place of the body rather than merely failing to gate a change.
|
|
85
85
|
|
|
86
|
-
|
|
86
|
+
Every skill carries one. Coverage was selective while the file existed only to disambiguate a contested boundary, and a file present for some skills and absent for others cannot be scanned, because an absence reads as a gap rather than as a verdict. A skill whose scope nobody contests still owes the statement, since the reader deciding whether a skill should exist reads the corpus rather than one entry. A new skill is created with the sibling beside it rather than acquiring one in a later sweep.
|
|
87
87
|
|
|
88
88
|
### What a working requirement looks like
|
|
89
89
|
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: claude-design-propose
|
|
3
|
-
description: Retired. `claude-design-extract` absorbed this skill and now takes the greenfield path itself. Invoke `aitk:claude-design-extract` instead.
|
|
4
|
-
disable-model-invocation: true
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
# Design propose
|
|
8
|
-
|
|
9
|
-
Retired. `claude-design-extract` absorbed the greenfield path, picks it from what the project has, and its description carries every trigger this skill used to route on.
|
|
10
|
-
|
|
11
|
-
Invoke `aitk:claude-design-extract` instead. It reads `.claude/REQUIREMENTS.md` for the `## Personality` paragraph and `.claude/ARCHITECTURE.md` for platform signals when the project has no UI code, which is what this skill did.
|
|
12
|
-
|
|
13
|
-
This body exists so a project that installed the plugin before the merge keeps resolving the old name for one sync cycle. It ships in `0.18.0` and is removed in `0.19.0`.
|
|
14
|
-
|
|
15
|
-
`scripts/sandbox/claude/design-propose.sh` goes at the same time, but its fixture does not. That scenario is the only greenfield coverage the survivor has, so `0.19.0` folds it into `scripts/sandbox/claude/design-extract.sh` as a second arm rather than deleting it. Removing the file outright drops the greenfield path from the catalog.
|