@erclx/aitk 3.25.0 → 3.25.2

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 (44) hide show
  1. package/README.md +1 -1
  2. package/claude/.claude-plugin/plugin.json +1 -1
  3. package/claude/skills/create-snippet/REQUIREMENT.md +3 -3
  4. package/claude/skills/create-snippet/SKILL.md +3 -3
  5. package/claude/skills/migration-claude-md/REQUIREMENT.md +0 -1
  6. package/claude/skills/migration-context/REQUIREMENT.md +0 -1
  7. package/claude/skills/migration-superseded/REQUIREMENT.md +0 -1
  8. package/claude/skills/setup-init/REQUIREMENT.md +1 -1
  9. package/claude/skills/setup-init/SKILL.md +4 -9
  10. package/claude/skills/toolkit-cli/REQUIREMENT.md +1 -2
  11. package/claude/skills/toolkit-operator/REQUIREMENT.md +1 -1
  12. package/claude/skills/toolkit-operator/SKILL.md +1 -1
  13. package/docs/agents/commands.md +1 -1
  14. package/docs/agents/context-audit.md +1 -1
  15. package/docs/agents/install-and-sync.md +30 -45
  16. package/docs/agents/scripting.md +0 -6
  17. package/docs/agents/skills-reach.md +1 -1
  18. package/docs/ai-workflow.md +0 -1
  19. package/docs/target-projects.md +12 -14
  20. package/governance/stacks/base.toml +5 -3
  21. package/package.json +1 -1
  22. package/scripts/core/install-check.sh +10 -12
  23. package/scripts/core/verify.sh +9 -7
  24. package/src/claude/cases/setup.ts +8 -5
  25. package/src/claude/skills-reach.ts +3 -3
  26. package/src/cli.ts +2 -3
  27. package/src/commands/context.ts +2 -2
  28. package/src/commands/init.ts +0 -3
  29. package/src/commands/snippets.ts +2 -157
  30. package/src/commands/sync.ts +2 -5
  31. package/src/context/citations.ts +41 -1
  32. package/src/init/flags.ts +3 -8
  33. package/src/init/plan.ts +1 -21
  34. package/src/init/steps.ts +1 -21
  35. package/src/sync/check.ts +6 -11
  36. package/src/sync/layout.ts +9 -6
  37. package/src/sync/reverse.ts +3 -5
  38. package/src/sync/stamp.ts +7 -7
  39. package/src/sync/target.ts +1 -3
  40. package/standards/skill.md +1 -1
  41. package/claude/skills/migration-standards/REQUIREMENT.md +0 -50
  42. package/claude/skills/migration-standards/SKILL.md +0 -99
  43. package/src/snippets/adapter.ts +0 -35
  44. package/src/snippets/install.ts +0 -172
@@ -1,50 +0,0 @@
1
- ---
2
- name: migration-standards
3
- description: Why the root-to-.claude snippets relocation ships as git mv commands the user runs, which inbound references are worth fixing, and why standards no longer travel with it
4
- ---
5
-
6
- # Migration standards requirement
7
-
8
- ## Gap
9
-
10
- Without this skill, a project whose rules cite `.claude/snippets/` while the files sit at the root gets fixed by hand, and the hand fix loses what version control was holding. A plain `mv` breaks the rename chain, so every relocated snippet reads as a delete beside an unrelated add and its history stops at the move.
11
-
12
- A fourth failure comes before those three, from deciding what is unmigrated by listing the folder. A root `snippets/` can hold the project's own prompts and nothing the toolkit ever installed, and a listing cannot tell the two apart, so the skill proposes moving project files under `.claude/` where a sync walks them. The drift report already answers this, counting only files whose basename the toolkit ships, and it is the same report `toolkit-operator` reads to route here.
13
-
14
- Standards left this skill when the corpus stopped installing into a project. The report no longer names the domain, so a body still describing the move would propose relocating a folder the project authored, which is the exact failure the paragraph above records in the other direction.
15
-
16
- Taking detection from a command opens a failure the listing never had. A CLI predating the field exits zero with a well-formed report that never mentions it, so a skill treating an absent key as an empty answer tells a project whose every domain sits at the root that it has nothing to relocate. That is the population this skill exists for, and a silent false negative there costs more than the unfiltered count reading the folder would have produced.
17
-
18
- The re-sync the skill names has the same gap in the other direction. The relocation is what makes those commands land on a path that held nothing before, so the run hands over a command whose overwrite behavior it never states, and the reference holding that answer is named in the boundary below where no session reads it. Claude Code loads the skill entry alone.
19
-
20
- Three failures follow from acting without looking first. A move onto an existing `.claude/snippets/` copy overwrites the files already installed there, which is the case the relocation was supposed to be unnecessary for. A `git mv` on a dirty tree lands the relocation in the same commit as unrelated work, so neither can be reviewed or reverted alone. And a session that rewrites every inbound reference spends its effort on toolkit-owned rules and skills, which the next sync overwrites, while the author-owned lines that actually break go unmentioned.
21
-
22
- ## Must
23
-
24
- - Take what to relocate from `aitk sync --check --json`, and report its filtered count rather than a folder listing
25
- - Say in one line that a root `standards/` folder is left alone, since a user who came here expecting both domains reads silence as an oversight
26
- - Keep the listing as the fallback for a target where `aitk` is absent, the command fails, or the report carries no `unmigrated` key, and say that its counts are unfiltered
27
- - Detect an existing copy under `.claude/` and skip the move rather than merging into it
28
- - Read the working tree state and require it clean before the moves, since the relocation has to be revertible on its own
29
- - Propose `git mv` so history follows each file
30
- - Report author-owned inbound references as TODO lines the user fixes, and leave the reference itself untouched
31
- - Name the re-sync commands that reinstall toolkit-owned content at the new path, since the move alone leaves the install stale
32
- - Route to the overwrite contract from the body where those commands are printed, and carry its destructive rows into the reminder
33
- - Report the already-relocated case as a pass
34
-
35
- ## Must not
36
-
37
- - Run the moves, or edit `CLAUDE.md`, a rule file, or a doc
38
- - Surface references inside toolkit-owned rules and skills, which the next sync rewrites anyway
39
-
40
- ## Guards
41
-
42
- - No root `snippets/` present stops, since there is nothing to relocate
43
- - A directory that is not a git work tree stops, since `git mv` needs version control and a plain move is the failure the skill exists to prevent
44
-
45
- ## Out of scope
46
-
47
- - Classifying `CLAUDE.md` sections into the three-tier model: `migration-claude-md`
48
- - Relocating `docs/` files by audience: `migration-context`
49
- - Running the re-sync, which the user does after applying the moves
50
- - What each sync command overwrites once it runs: `toolkit-cli`
@@ -1,99 +0,0 @@
1
- ---
2
- name: migration-standards
3
- description: Proposes `git mv` commands to relocate a target project's root `snippets/` folder into `.claude/snippets/`, the install layout newer toolkit versions expect. Use when asked to "relocate snippets", "move snippets to .claude", "migrate snippets to .claude", or after a toolkit upgrade leaves a rule pointing at `.claude/snippets/` while the files sit at the root. Do NOT auto-execute moves or edit rule files. Proposal only.
4
- ---
5
-
6
- # Claude standards relocate
7
-
8
- Snippets is the one domain this skill relocates. Standards used to travel beside it and the toolkit corpus now installs into no project at all, so a root `standards/` folder in a target holds work the project authored and nothing proposes moving it. `aitk standards <name>` reads a toolkit standard wherever a session needs one.
9
-
10
- ## Guards
11
-
12
- - If `snippets/` does not exist at `pwd`, stop: `❌ No root snippets/ to relocate.`
13
- - If `pwd` is not a git work tree, stop: `❌ Not a git repository. git mv needs version control.`
14
-
15
- ## Step 1: read the report
16
-
17
- Run `aitk sync --check . --json` from the project root. Its `unmigrated` array is the detection. Each entry carries `domain`, `rootPath`, `installPath`, and `files`, and a domain appears only when the root folder holds a file the toolkit ships and nothing sits at the install path. Report `files` as the count, since it excludes files the project wrote into the same folder.
18
-
19
- Run these beside it, in parallel. The report answers whether to move and neither of these is derivable from it:
20
-
21
- - `ls .claude/snippets/ 2>/dev/null`: separate a domain already relocated from one never installed, which the report cannot, since both are absent from `unmigrated`
22
- - `git status --short 2>/dev/null`: confirm a clean tree before proposing moves
23
-
24
- ### When the report is unavailable
25
-
26
- Fall back to `ls snippets/` on any of three conditions. Say in the output that the count is unfiltered, because a root folder can hold project-authored files the report would have excluded and the fallback counts every one of them.
27
-
28
- - `aitk` is not on `PATH`
29
- - The command exits non-zero
30
- - The report parses and carries no `unmigrated` key at all
31
-
32
- The third is the one that decides whether this skill works for the projects it exists for. `unmigrated` reached a release in `0.46.0`, and a CLI older than that exits zero with a well-formed report that never mentions the field. Reading an absent key as an empty list sends the run down the nothing-to-relocate branch and reports a clean layout to a project whose every domain sits at the root, which is a silent false negative where the unfiltered count is a visible imprecision.
33
-
34
- An absent key and an empty array are different states, so test for the key rather than for emptiness. A current CLI reporting `"unmigrated": []` has looked and found nothing, and falling back there would trade a correct answer for a listing that proposes moving whatever the folder happens to hold.
35
-
36
- Do not fall back on `historyUnavailable`. That field reports failed attribution on a domain or on `seeds`, and `unmigrated` is a filesystem read carrying no attribution of its own, so a report that cannot date a file still detects the layout correctly.
37
-
38
- ## Step 2: check conflicts
39
-
40
- - If `.claude/snippets/` already holds `.md` files, mark snippets as "already relocated" and skip the move.
41
- - If `git status` is not clean, add a TODO line telling the user to commit or stash first. `git mv` on a dirty tree mixes the move with unrelated changes.
42
- - If the root folder holds files and no domain names it in a report that carried the key, propose nothing. The content is the project's own, and moving it under `.claude/` puts project files where a sync walks. This reads a present key alone. A report with no `unmigrated` key never reaches here, since Step 1 sends it to the fallback.
43
- - A root `standards/` folder is never a conflict and never a move. Say so in one line when one is present, since a user who came here expecting both domains reads silence as an oversight.
44
-
45
- ## Step 3: find author-owned inbound references
46
-
47
- Toolkit-owned rules and skills are re-synced, so do not rewrite them. Surface only references the user authored:
48
-
49
- ```bash
50
- git grep -n "snippets/" -- 'CLAUDE.md' '.claude/rules/**' 'docs/**' 2>/dev/null
51
- ```
52
-
53
- Report each hit as a TODO line. Exclude paths already under `.claude/snippets/`.
54
-
55
- ## Step 4: output
56
-
57
- Print one grouped proposal block. Omit empty groups.
58
-
59
- ```markdown
60
- ## Relocate
61
-
62
- - snippets/ → .claude/snippets/ (<count> files)
63
-
64
- ## Already relocated
65
-
66
- - .claude/snippets/ exists. Skipping.
67
-
68
- ## Left alone
69
-
70
- - standards/ is the project's own. No toolkit standard installs into a project.
71
-
72
- ## Suggested git mv commands
73
-
74
- mkdir -p .claude
75
- git mv snippets .claude/snippets
76
-
77
- ## Re-sync after moving
78
-
79
- aitk snippets sync .
80
- aitk gov sync .
81
-
82
- ## Inbound references to verify
83
-
84
- - CLAUDE.md:42 references snippets/align.md
85
- - docs/contributing.md:8 references snippets/claude/feature
86
-
87
- ## Reminder
88
-
89
- Re-syncing reinstalls toolkit-owned rules and snippets at the new path. Author-owned references above need a manual one-line fix.
90
- ```
91
-
92
- Two states produce no proposal at all, and each gets its own line rather than an empty block:
93
-
94
- - The folder already lives under `.claude/`: `✅ snippets/ already lives under .claude/. Nothing to relocate.`
95
- - The root folder exists and no domain names it in `unmigrated`: `✅ snippets/ holds no file the toolkit installed. Nothing to relocate.` The guard passed because the folder is there, so a session that printed nothing would leave the user reading silence as a failed run rather than as the answer.
96
-
97
- Read the `toolkit-cli` skill before printing the re-sync block, and name in the reminder any surface it lists as overwritten. The user runs those commands against a tree the moves above relocated, and that relocation is what puts an overwrite onto files never installed at the new path.
98
-
99
- Do not run the `git mv` commands. Do not edit `CLAUDE.md`, rule files, or docs. The user runs the commands and fixes author-owned references after reviewing.
@@ -1,35 +0,0 @@
1
- import { existsSync } from 'node:fs'
2
- import { join } from 'node:path'
3
- import { snippetsSourceDir } from '@/snippets/categories'
4
- import type { InstalledFile, SyncAdapter } from '@/sync/engine'
5
-
6
- /**
7
- * Matches installed snippets to sources by relative path, where gov matches by
8
- * rule name. `derive_dest_rel_path` in `scripts/snippets/install.sh` keeps the
9
- * immediate parent for a nested snippet and flattens a root-level one to its
10
- * filename, which is the shape a source-relative path already has, so
11
- * destination and source rel paths agree for everything the toolkit ships.
12
- */
13
- export function createSnippetsAdapter(root: string): SyncAdapter {
14
- const sourceDir = snippetsSourceDir(root)
15
-
16
- return {
17
- banner: 'aitk snippets sync',
18
- label: 'snippets',
19
- missingMessage:
20
- "No .claude/snippets/ found in target. Run 'aitk snippets install' first.",
21
- unit: 'snippets',
22
- installedRoot: (target: string) => join(target, '.claude', 'snippets'),
23
- locateSource: (file: InstalledFile) => locateSource(sourceDir, file),
24
- projectSubdir: 'project',
25
- stamp: { domain: 'snippets', toolkitRoot: root },
26
- }
27
- }
28
-
29
- function locateSource(
30
- sourceDir: string,
31
- file: InstalledFile,
32
- ): string | undefined {
33
- const source = join(sourceDir, file.relToRoot)
34
- return existsSync(source) ? source : undefined
35
- }
@@ -1,172 +0,0 @@
1
- import { existsSync } from 'node:fs'
2
- import { basename, dirname, join } from 'node:path'
3
- import { copyPreservingMode } from '@/copy'
4
- import {
5
- BASE_CATEGORY,
6
- categoryDir,
7
- categoryExists,
8
- listCategories,
9
- listEntries,
10
- NONE_CATEGORY,
11
- snippetsSourceDir,
12
- } from '@/snippets/categories'
13
- import { findPreset, loadPresets } from '@/snippets/presets'
14
- import { isDirectory } from '@/target'
15
-
16
- export const ALL_CATEGORY = 'all'
17
-
18
- export interface SnippetFile {
19
- readonly src: string
20
- readonly relPath: string
21
- }
22
-
23
- export type SnippetResolution =
24
- | {
25
- readonly ok: true
26
- readonly step: string
27
- readonly files: readonly SnippetFile[]
28
- readonly missing: readonly string[]
29
- }
30
- | { readonly ok: false; readonly unknownCategory: string }
31
-
32
- export function installedSnippetsDir(target: string): string {
33
- return join(target, '.claude', 'snippets')
34
- }
35
-
36
- /**
37
- * Derives the destination path. A snippet directly under `snippets/` installs
38
- * flat and a nested one keeps its immediate parent, so the installed tree
39
- * mirrors the source one level deep.
40
- */
41
- export function deriveDestRelPath(root: string, src: string): string {
42
- const parent = dirname(src)
43
- const filename = basename(src)
44
-
45
- return parent === snippetsSourceDir(root)
46
- ? filename
47
- : `${basename(parent)}/${filename}`
48
- }
49
-
50
- function filesInCategory(root: string, category: string): SnippetFile[] {
51
- return listEntries(root, category).map((slug) => {
52
- const src = join(categoryDir(root, category), `${slug}.md`)
53
- return { src, relPath: deriveDestRelPath(root, src) }
54
- })
55
- }
56
-
57
- /**
58
- * Resolves the one argument three ways. `all` wins, then a preset name, then a
59
- * folder, which is the bash precedence and matters because a folder sharing a
60
- * preset name resolves to the preset.
61
- *
62
- * Preset slugs pass the internal-category filter too. A slug is a path relative
63
- * to `snippets/`, so a preset naming `aitk/<slug>` would otherwise reach an
64
- * internal snippet through the one install path the filter did not cover.
65
- */
66
- export function resolveSnippets(
67
- root: string,
68
- category: string,
69
- ): SnippetResolution {
70
- if (category === ALL_CATEGORY) {
71
- const files = listCategories(root).flatMap((name) =>
72
- filesInCategory(root, name),
73
- )
74
- return { ok: true, step: 'Resolving all categories', files, missing: [] }
75
- }
76
-
77
- if (category === NONE_CATEGORY) {
78
- return {
79
- ok: true,
80
- step: 'Resolving category: none',
81
- files: [],
82
- missing: [],
83
- }
84
- }
85
-
86
- const preset = findPreset(root, category)
87
- if (preset) {
88
- const files: SnippetFile[] = []
89
- const missing: string[] = []
90
-
91
- for (const slug of preset.slugs) {
92
- const src = join(snippetsSourceDir(root), `${slug}.md`)
93
- if (existsSync(src)) files.push({ src, relPath: `${slug}.md` })
94
- else missing.push(slug)
95
- }
96
-
97
- return { ok: true, step: `Resolving preset: ${category}`, files, missing }
98
- }
99
-
100
- if (category !== BASE_CATEGORY && !categoryExists(root, category)) {
101
- return { ok: false, unknownCategory: category }
102
- }
103
-
104
- return {
105
- ok: true,
106
- step: `Resolving category: ${category}`,
107
- files: filesInCategory(root, category),
108
- missing: [],
109
- }
110
- }
111
-
112
- /**
113
- * Lists everything the picker offers: presets first, then `base`, then the
114
- * folder categories, matching the order the bash built the menu in.
115
- */
116
- export function installableCategories(root: string): string[] {
117
- return [
118
- ...loadPresets(root).map((preset) => preset.name),
119
- ...listCategories(root),
120
- ]
121
- }
122
-
123
- export async function installSnippets(
124
- files: readonly SnippetFile[],
125
- target: string,
126
- ): Promise<string[]> {
127
- const destDir = installedSnippetsDir(target)
128
- const installed: string[] = []
129
-
130
- for (const file of files) {
131
- await copyPreservingMode(file.src, join(destDir, file.relPath))
132
- installed.push(join('.claude', 'snippets', file.relPath))
133
- }
134
-
135
- return installed
136
- }
137
-
138
- function snippetsRuleSourceDir(root: string): string {
139
- return join(root, 'governance', 'rules', 'snippets')
140
- }
141
-
142
- function installedSnippetsRuleDir(target: string): string {
143
- return join(target, '.claude', 'rules', 'snippets')
144
- }
145
-
146
- /**
147
- * Installs the `@`-reference convention rule alongside the snippets a caller
148
- * took. No stack names this folder, so a project that declined snippets never
149
- * receives a rule describing a behavior it holds no snippet to exercise.
150
- */
151
- export async function installSnippetsRule(
152
- root: string,
153
- target: string,
154
- ): Promise<string[]> {
155
- const dir = snippetsRuleSourceDir(root)
156
- if (!isDirectory(dir)) return []
157
-
158
- const installed: string[] = []
159
-
160
- for (const rel of new Bun.Glob('*.md').scanSync({
161
- cwd: dir,
162
- onlyFiles: true,
163
- })) {
164
- await copyPreservingMode(
165
- join(dir, rel),
166
- join(installedSnippetsRuleDir(target), rel),
167
- )
168
- installed.push(join('.claude', 'rules', 'snippets', rel))
169
- }
170
-
171
- return installed
172
- }