@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.
Files changed (46) hide show
  1. package/claude/.claude-plugin/plugin.json +1 -1
  2. package/claude/skills/claude-design-extract/REQUIREMENT.md +42 -0
  3. package/claude/skills/claude-diagram/REQUIREMENT.md +45 -0
  4. package/claude/skills/claude-groundwork/REQUIREMENT.md +44 -0
  5. package/claude/skills/claude-memory-capture/REQUIREMENT.md +40 -0
  6. package/claude/skills/claude-orchestrate/REQUIREMENT.md +44 -0
  7. package/claude/skills/claude-roadmap/REQUIREMENT.md +41 -0
  8. package/claude/skills/claude-screencast/REQUIREMENT.md +39 -0
  9. package/claude/skills/claude-slides-draft/REQUIREMENT.md +39 -0
  10. package/claude/skills/claude-tasks/REQUIREMENT.md +43 -0
  11. package/claude/skills/claude-ui-test/REQUIREMENT.md +40 -0
  12. package/claude/skills/claude-ux-audit/REQUIREMENT.md +39 -0
  13. package/claude/skills/claude-worktree/REQUIREMENT.md +41 -0
  14. package/claude/skills/cli-script/REQUIREMENT.md +40 -0
  15. package/claude/skills/create-rule/REQUIREMENT.md +42 -0
  16. package/claude/skills/create-skill/REQUIREMENT.md +6 -2
  17. package/claude/skills/create-skill/SKILL.md +5 -2
  18. package/claude/skills/create-snippet/REQUIREMENT.md +36 -0
  19. package/claude/skills/create-snippet/SKILL.md +23 -5
  20. package/claude/skills/{create-standard → create-snippet}/references/snippets.md +1 -1
  21. package/claude/skills/create-standard/REQUIREMENT.md +38 -0
  22. package/claude/skills/create-standard/SKILL.md +8 -12
  23. package/claude/skills/migration-claude-md/REQUIREMENT.md +41 -0
  24. package/claude/skills/migration-standards/REQUIREMENT.md +38 -0
  25. package/claude/skills/project-commands/REQUIREMENT.md +42 -0
  26. package/claude/skills/session-resume/REQUIREMENT.md +38 -0
  27. package/claude/skills/setup-indexes/REQUIREMENT.md +42 -0
  28. package/claude/skills/setup-plugins/REQUIREMENT.md +42 -0
  29. package/claude/skills/setup-verify/REQUIREMENT.md +39 -0
  30. package/claude/skills/systematic-debugging/REQUIREMENT.md +41 -0
  31. package/claude/skills/toolkit-cli/REQUIREMENT.md +37 -0
  32. package/claude/skills/toolkit-feedback/REQUIREMENT.md +40 -0
  33. package/claude/skills/toolkit-operator/REQUIREMENT.md +39 -0
  34. package/claude/skills/toolkit-triage/REQUIREMENT.md +40 -0
  35. package/claude/skills/youtube-transcripts/REQUIREMENT.md +38 -0
  36. package/docs/agents.md +17 -9
  37. package/docs/operating-model.md +1 -1
  38. package/docs/visual-design-workflow.md +1 -1
  39. package/package.json +1 -1
  40. package/scripts/manage-sandbox.sh +4 -0
  41. package/src/commands/context.ts +76 -11
  42. package/src/context/folders.ts +85 -13
  43. package/src/sandbox/census.ts +1 -1
  44. package/standards/bundled/snippets.md +1 -1
  45. package/standards/skill.md +4 -4
  46. 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 under `.claude/` (default: the three below) |
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`. Coverage of that file is selective by design, so a `false` is
463
- not a gap to close and the flag answers which skills carry one without a caller
464
- listing the directory itself. It says nothing about why, which is a judgment the
465
- toolkit records against its own corpus rather than in the catalog.
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
 
@@ -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/skills.md` for how the skills in the loop are categorized
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/skills.md`: how to decide between workflow and domain-knowledge skills
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@erclx/aitk",
3
3
  "type": "module",
4
- "version": "0.37.0",
4
+ "version": "0.39.0",
5
5
  "description": "Infrastructure and quality tooling for developer workflows",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -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"
@@ -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('--folder <list>', `Comma-separated folder names under .claude/`)
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 `.claude/`, taking the scan
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 under .claude/, not paths: ${invalid.join(', ')}`
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
- const folders = await resolveFolders(root, names)
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/. Looked for: ${names.join(', ')}.`,
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
- const citations = await auditCitations(root, presentNames(folders))
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
- function reportScope(folders: readonly AuditedFolder[]): void {
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
- function reportCitations(report: ScannedCitations): void {
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
  )
@@ -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 citation
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 [...new Set(folders.map((folder) => folder.name))]
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
- * A requested folder that does not exist is dropped rather than reported. The
83
- * default list names three folders and a project carrying one of them is the
84
- * ordinary case.
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
- ): Promise<AuditedFolder[]> {
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 dir = resolve(root, '.claude', name)
94
- if (!existsSync(`${dir}/${INDEX_FILE}`)) continue
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
  }
@@ -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 55 skills. The bare `<command>` fallback is what reaches the
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.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  title: Snippet reference
3
3
  description: Snippet reference and authoring conventions
4
- consumers: create-standard
4
+ consumers: create-snippet
5
5
  ---
6
6
 
7
7
  # Snippet reference
@@ -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 optional sibling `REQUIREMENT.md`, and the bundled reference, script, and asset folders beside them.
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` (optional), `scripts/` (optional), `references/` (optional), `assets/` (optional)
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
- A skill earns one when a reader cannot recover what the skill is for from the body alone. Apply the test to a skill nobody in the session has read before: read the body once, then state both the failures the skill exists to prevent and the nearest thing it deliberately does not do. A body answering both is its own specification and needs no sibling, and a body leaving either open earns one. Length is a weak proxy for that, since a short skill with a contested boundary earns one and a long skill doing one plain thing may not.
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.