@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.
- package/README.md +1 -1
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/create-snippet/REQUIREMENT.md +3 -3
- package/claude/skills/create-snippet/SKILL.md +3 -3
- package/claude/skills/migration-claude-md/REQUIREMENT.md +0 -1
- package/claude/skills/migration-context/REQUIREMENT.md +0 -1
- package/claude/skills/migration-superseded/REQUIREMENT.md +0 -1
- package/claude/skills/setup-init/REQUIREMENT.md +1 -1
- package/claude/skills/setup-init/SKILL.md +4 -9
- package/claude/skills/toolkit-cli/REQUIREMENT.md +1 -2
- package/claude/skills/toolkit-operator/REQUIREMENT.md +1 -1
- package/claude/skills/toolkit-operator/SKILL.md +1 -1
- package/docs/agents/commands.md +1 -1
- package/docs/agents/context-audit.md +1 -1
- package/docs/agents/install-and-sync.md +30 -45
- package/docs/agents/scripting.md +0 -6
- package/docs/agents/skills-reach.md +1 -1
- package/docs/ai-workflow.md +0 -1
- package/docs/target-projects.md +12 -14
- package/governance/stacks/base.toml +5 -3
- package/package.json +1 -1
- package/scripts/core/install-check.sh +10 -12
- package/scripts/core/verify.sh +9 -7
- package/src/claude/cases/setup.ts +8 -5
- package/src/claude/skills-reach.ts +3 -3
- package/src/cli.ts +2 -3
- package/src/commands/context.ts +2 -2
- package/src/commands/init.ts +0 -3
- package/src/commands/snippets.ts +2 -157
- package/src/commands/sync.ts +2 -5
- package/src/context/citations.ts +41 -1
- package/src/init/flags.ts +3 -8
- package/src/init/plan.ts +1 -21
- package/src/init/steps.ts +1 -21
- package/src/sync/check.ts +6 -11
- package/src/sync/layout.ts +9 -6
- package/src/sync/reverse.ts +3 -5
- package/src/sync/stamp.ts +7 -7
- package/src/sync/target.ts +1 -3
- package/standards/skill.md +1 -1
- package/claude/skills/migration-standards/REQUIREMENT.md +0 -50
- package/claude/skills/migration-standards/SKILL.md +0 -99
- package/src/snippets/adapter.ts +0 -35
- package/src/snippets/install.ts +0 -172
|
@@ -40,11 +40,6 @@ export const SETUP_CASES: readonly SkillCase[] = [
|
|
|
40
40
|
'Move the agent-flavored docs out of the docs folder and into context.',
|
|
41
41
|
expect: 'migration-context',
|
|
42
42
|
},
|
|
43
|
-
{
|
|
44
|
-
prompt:
|
|
45
|
-
'The snippets folder needs to move under .claude to match the current layout.',
|
|
46
|
-
expect: 'migration-standards',
|
|
47
|
-
},
|
|
48
43
|
{
|
|
49
44
|
prompt:
|
|
50
45
|
'This file was replaced by a folder, help me split its content into it.',
|
|
@@ -65,6 +60,14 @@ export const SETUP_CASES: readonly SkillCase[] = [
|
|
|
65
60
|
"I don't know which specific toolkit skill I need, just handle it for me.",
|
|
66
61
|
expect: 'toolkit-operator',
|
|
67
62
|
},
|
|
63
|
+
// The performing skill wins a phrase naming its operation over the front
|
|
64
|
+
// door's own routing framing, even though toolkit-operator's description
|
|
65
|
+
// quotes a phrase near this one. See .claude/context/cli/audits.md.
|
|
66
|
+
{
|
|
67
|
+
prompt:
|
|
68
|
+
"Whatever the right toolkit command is, get this project's rules installed.",
|
|
69
|
+
expect: 'setup-gov',
|
|
70
|
+
},
|
|
68
71
|
{
|
|
69
72
|
prompt:
|
|
70
73
|
'Work through the open feedback issues on the toolkit repo one by one.',
|
|
@@ -12,9 +12,9 @@ const SHIPPED_SKILLS = join('claude', 'skills')
|
|
|
12
12
|
* The authoring roots this repository owns and no install channel delivers.
|
|
13
13
|
*
|
|
14
14
|
* Every entry is a folder a target never holds under that spelling. Standards
|
|
15
|
-
* install nowhere and are reached through the plugin corpus,
|
|
16
|
-
*
|
|
17
|
-
*
|
|
15
|
+
* and snippets install nowhere and are reached through the plugin corpus,
|
|
16
|
+
* rules install under `.claude/rules/`, and the rest are this repository's
|
|
17
|
+
* own source, docs, and catalogs.
|
|
18
18
|
*
|
|
19
19
|
* `src/`, `scripts/`, and bare `docs/` are deliberately absent. A body naming
|
|
20
20
|
* one of those is describing the reader's own tree, so listing them would
|
package/src/cli.ts
CHANGED
|
@@ -49,7 +49,7 @@ function showHelp(): void {
|
|
|
49
49
|
`${GREY}│${NC} sandbox [cat:cmd] ${GREY}# Provision and run sandbox scenarios${NC}`,
|
|
50
50
|
`${GREY}│${NC} gov [command] ${GREY}# Governance commands (install, sync)${NC}`,
|
|
51
51
|
`${GREY}│${NC} standards [cmd] ${GREY}# Standards commands (install, sync, list, <name>)${NC}`,
|
|
52
|
-
`${GREY}│${NC} snippets [cmd] ${GREY}# Snippets commands (
|
|
52
|
+
`${GREY}│${NC} snippets [cmd] ${GREY}# Snippets commands (create, list)${NC}`,
|
|
53
53
|
`${GREY}│${NC} tooling [cmd] ${GREY}# Manage tooling stacks (sync, ref, create)${NC}`,
|
|
54
54
|
`${GREY}│${NC} claude [cmd] ${GREY}# Claude workflow (init, sync, setup)${NC}`,
|
|
55
55
|
`${GREY}│${NC} wiki [cmd] ${GREY}# Wiki commands (init)${NC}`,
|
|
@@ -89,8 +89,7 @@ function showHelp(): void {
|
|
|
89
89
|
`${GREY}│${NC} aitk gov sync ../my-app`,
|
|
90
90
|
`${GREY}│${NC} aitk gov restated --json`,
|
|
91
91
|
`${GREY}│${NC} aitk standards markdown`,
|
|
92
|
-
`${GREY}│${NC} aitk snippets
|
|
93
|
-
`${GREY}│${NC} aitk snippets sync ../my-app`,
|
|
92
|
+
`${GREY}│${NC} aitk snippets list`,
|
|
94
93
|
`${GREY}│${NC} aitk init ../my-app`,
|
|
95
94
|
`${GREY}│${NC} aitk tooling sync base`,
|
|
96
95
|
`${GREY}│${NC} aitk tooling create`,
|
package/src/commands/context.ts
CHANGED
|
@@ -180,7 +180,7 @@ async function runAudit(
|
|
|
180
180
|
if (folders.length === 0) {
|
|
181
181
|
return refuse(
|
|
182
182
|
'no-folders',
|
|
183
|
-
`No audited folder found ${named ? 'under .claude/ or the project root' : 'under .claude/'}. Looked for: ${names.join(', ')}.`,
|
|
183
|
+
`No audited folder found ${named ? 'under .claude/ or the project root' : 'under .claude/'}, since resolving one needs its own index.md file. Looked for: ${names.join(', ')}.`,
|
|
184
184
|
gateOnly,
|
|
185
185
|
root,
|
|
186
186
|
opts.json ?? false,
|
|
@@ -392,7 +392,7 @@ function reportScope(
|
|
|
392
392
|
if (unresolved.length === 0) return
|
|
393
393
|
|
|
394
394
|
logWarn(
|
|
395
|
-
`
|
|
395
|
+
`Not audited, since resolving needs its own index.md: ${unresolved.join(', ')}`,
|
|
396
396
|
)
|
|
397
397
|
}
|
|
398
398
|
|
package/src/commands/init.ts
CHANGED
|
@@ -12,7 +12,6 @@ interface InitOptions {
|
|
|
12
12
|
/** Always present: the option falls back to `DEFAULT_STACK`. */
|
|
13
13
|
readonly stack: string
|
|
14
14
|
readonly add?: string
|
|
15
|
-
readonly snippets?: string
|
|
16
15
|
readonly skip?: string
|
|
17
16
|
}
|
|
18
17
|
|
|
@@ -35,7 +34,6 @@ export function register(program: Command): void {
|
|
|
35
34
|
' aitk init ../my-app',
|
|
36
35
|
' aitk init --stack astro --add 260-shadcn ../my-app',
|
|
37
36
|
' aitk init --skip governance ../my-app',
|
|
38
|
-
' aitk init --snippets all ../my-app',
|
|
39
37
|
'',
|
|
40
38
|
].join('\n'),
|
|
41
39
|
)
|
|
@@ -62,7 +60,6 @@ async function runInit(
|
|
|
62
60
|
const flags: InitFlags = {
|
|
63
61
|
stack: options.stack,
|
|
64
62
|
add: options.add,
|
|
65
|
-
snippets: options.snippets,
|
|
66
63
|
skip,
|
|
67
64
|
}
|
|
68
65
|
|
package/src/commands/snippets.ts
CHANGED
|
@@ -1,32 +1,9 @@
|
|
|
1
1
|
import type { Command } from 'commander'
|
|
2
2
|
import { registerPassThroughVerbs } from '@/commands/pass-through'
|
|
3
3
|
import { PROJECT_ROOT } from '@/project-root'
|
|
4
|
-
import { createSnippetsAdapter } from '@/snippets/adapter'
|
|
5
4
|
import { BASE_CATEGORY } from '@/snippets/categories'
|
|
6
|
-
import {
|
|
7
|
-
ALL_CATEGORY,
|
|
8
|
-
installSnippets,
|
|
9
|
-
installSnippetsRule,
|
|
10
|
-
installableCategories,
|
|
11
|
-
resolveSnippets,
|
|
12
|
-
} from '@/snippets/install'
|
|
13
5
|
import { buildSnippetsCatalog } from '@/snippets/list'
|
|
14
|
-
import {
|
|
15
|
-
import { resolveTarget } from '@/target'
|
|
16
|
-
import {
|
|
17
|
-
intro,
|
|
18
|
-
isNonInteractive,
|
|
19
|
-
logAdd,
|
|
20
|
-
logError,
|
|
21
|
-
logInfo,
|
|
22
|
-
logStep,
|
|
23
|
-
logWarn,
|
|
24
|
-
outro,
|
|
25
|
-
palette,
|
|
26
|
-
select,
|
|
27
|
-
} from '@/ui'
|
|
28
|
-
|
|
29
|
-
const SNIPPETS_REL = '.claude/snippets'
|
|
6
|
+
import { intro, logInfo, logStep, outro } from '@/ui'
|
|
30
7
|
|
|
31
8
|
const PASS_THROUGH_VERBS = ['create'] as const
|
|
32
9
|
|
|
@@ -39,42 +16,8 @@ interface ListOptions {
|
|
|
39
16
|
export function register(program: Command): void {
|
|
40
17
|
const snippets = program
|
|
41
18
|
.command('snippets')
|
|
42
|
-
.description('Snippets commands (
|
|
43
|
-
.helpOption('-h, --help', 'Show this help message')
|
|
44
|
-
|
|
45
|
-
snippets
|
|
46
|
-
.command('install')
|
|
47
|
-
.description('Install snippets into .claude/snippets/')
|
|
48
|
-
.argument('[category]', "Preset, folder, or 'all'")
|
|
49
|
-
.argument('[target]', 'Target directory', '.')
|
|
19
|
+
.description('Snippets commands (create, list)')
|
|
50
20
|
.helpOption('-h, --help', 'Show this help message')
|
|
51
|
-
.addHelpText(
|
|
52
|
-
'after',
|
|
53
|
-
[
|
|
54
|
-
'',
|
|
55
|
-
'Examples:',
|
|
56
|
-
' aitk snippets install essentials',
|
|
57
|
-
' aitk snippets install all',
|
|
58
|
-
' aitk snippets install claude ../my-app',
|
|
59
|
-
'',
|
|
60
|
-
].join('\n'),
|
|
61
|
-
)
|
|
62
|
-
.action(async (category: string | undefined, target: string) => {
|
|
63
|
-
process.exitCode = await runInstall(category, target)
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
snippets
|
|
67
|
-
.command('sync')
|
|
68
|
-
.description('Update snippets already installed under .claude/snippets/')
|
|
69
|
-
.argument('[target]', 'Target directory', '.')
|
|
70
|
-
.helpOption('-h, --help', 'Show this help message')
|
|
71
|
-
.action(async (target: string) => {
|
|
72
|
-
process.exitCode = await runDomainSync(
|
|
73
|
-
createSnippetsAdapter(PROJECT_ROOT),
|
|
74
|
-
target,
|
|
75
|
-
{ protectedRoot: PROJECT_ROOT },
|
|
76
|
-
)
|
|
77
|
-
})
|
|
78
21
|
|
|
79
22
|
snippets
|
|
80
23
|
.command('list')
|
|
@@ -90,104 +33,6 @@ export function register(program: Command): void {
|
|
|
90
33
|
registerPassThroughVerbs(snippets, 'snippets', PASS_THROUGH_VERBS)
|
|
91
34
|
}
|
|
92
35
|
|
|
93
|
-
/**
|
|
94
|
-
* Refuses rather than picking, the same judgment `aitk gov install` applies to
|
|
95
|
-
* its stack picker. Headless, `select_option` returned the first option, which
|
|
96
|
-
* here was `all`, so an agent omitting the argument installed every category.
|
|
97
|
-
*/
|
|
98
|
-
async function chooseCategory(root: string): Promise<string | number> {
|
|
99
|
-
const categories = installableCategories(root)
|
|
100
|
-
|
|
101
|
-
if (categories.length === 0) {
|
|
102
|
-
logError('No categories found in snippets source.')
|
|
103
|
-
outro()
|
|
104
|
-
return 1
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
if (isNonInteractive()) {
|
|
108
|
-
logError(
|
|
109
|
-
`Category argument is required in non-interactive mode. One of: ${[ALL_CATEGORY, ...categories].join(', ')}.`,
|
|
110
|
-
)
|
|
111
|
-
outro()
|
|
112
|
-
return 1
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
return select({
|
|
116
|
-
message: 'Select category to install:',
|
|
117
|
-
options: [ALL_CATEGORY, ...categories].map((name) => ({
|
|
118
|
-
value: name,
|
|
119
|
-
label: name,
|
|
120
|
-
})),
|
|
121
|
-
})
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
async function runInstall(
|
|
125
|
-
category: string | undefined,
|
|
126
|
-
target: string,
|
|
127
|
-
): Promise<number> {
|
|
128
|
-
intro('aitk snippets install')
|
|
129
|
-
|
|
130
|
-
const resolved = resolveTarget(target, PROJECT_ROOT)
|
|
131
|
-
if (typeof resolved === 'number') return resolved
|
|
132
|
-
|
|
133
|
-
let selected = category
|
|
134
|
-
if (selected === undefined) {
|
|
135
|
-
const choice = await chooseCategory(PROJECT_ROOT)
|
|
136
|
-
if (typeof choice === 'number') return choice
|
|
137
|
-
selected = choice
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const resolution = resolveSnippets(PROJECT_ROOT, selected)
|
|
141
|
-
if (!resolution.ok) {
|
|
142
|
-
logError(`Category not found: ${resolution.unknownCategory}`)
|
|
143
|
-
outro()
|
|
144
|
-
return 1
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
logStep(resolution.step)
|
|
148
|
-
for (const slug of resolution.missing) {
|
|
149
|
-
logWarn(`${slug} (source not found, skipping)`)
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
if (resolution.files.length === 0) {
|
|
153
|
-
logWarn(`No snippets found for category: ${selected}`)
|
|
154
|
-
outro()
|
|
155
|
-
return 0
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
for (const file of resolution.files) logInfo(file.relPath)
|
|
159
|
-
|
|
160
|
-
const display = target.replace(/\/$/, '')
|
|
161
|
-
const shouldInstall = await select({
|
|
162
|
-
message: `Install ${resolution.files.length} snippets to ${display}/${SNIPPETS_REL}?`,
|
|
163
|
-
options: [
|
|
164
|
-
{ value: true, label: 'Yes' },
|
|
165
|
-
{ value: false, label: 'No' },
|
|
166
|
-
],
|
|
167
|
-
nonInteractiveDefault: true,
|
|
168
|
-
})
|
|
169
|
-
|
|
170
|
-
if (!shouldInstall) {
|
|
171
|
-
logWarn('Cancelled')
|
|
172
|
-
outro()
|
|
173
|
-
return 0
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
logStep('Installing snippets')
|
|
177
|
-
for (const rel of await installSnippets(resolution.files, resolved)) {
|
|
178
|
-
logAdd(rel)
|
|
179
|
-
}
|
|
180
|
-
for (const rel of await installSnippetsRule(PROJECT_ROOT, resolved)) {
|
|
181
|
-
logAdd(rel)
|
|
182
|
-
}
|
|
183
|
-
await recordStamp(createSnippetsAdapter(PROJECT_ROOT), resolved, new Date())
|
|
184
|
-
|
|
185
|
-
const { GREEN, NC } = palette(process.stderr)
|
|
186
|
-
outro()
|
|
187
|
-
process.stderr.write(`${GREEN}✓ Snippets installed${NC}\n`)
|
|
188
|
-
return 0
|
|
189
|
-
}
|
|
190
|
-
|
|
191
36
|
/**
|
|
192
37
|
* `JSON.stringify` replaces a `printf` template that interpolated names into a
|
|
193
38
|
* JSON string literal unescaped, so a slug or category carrying a quote emitted
|
package/src/commands/sync.ts
CHANGED
|
@@ -34,7 +34,6 @@ import {
|
|
|
34
34
|
import { describeSkew } from '@/version/skew'
|
|
35
35
|
|
|
36
36
|
const SYNC_ARGS: Record<SyncDomain, readonly string[]> = {
|
|
37
|
-
snippets: ['snippets', 'sync'],
|
|
38
37
|
governance: ['gov', 'sync'],
|
|
39
38
|
claude: ['claude', 'sync'],
|
|
40
39
|
}
|
|
@@ -157,7 +156,7 @@ function renderCheck(report: CheckReport): void {
|
|
|
157
156
|
logWarn(
|
|
158
157
|
`${entry.files} files at ${entry.rootPath}/, nothing at ${entry.installPath}/`,
|
|
159
158
|
)
|
|
160
|
-
logInfo('
|
|
159
|
+
logInfo('Move the content yourself. No sync command touches these.')
|
|
161
160
|
}
|
|
162
161
|
|
|
163
162
|
renderSeeds(report)
|
|
@@ -338,9 +337,7 @@ function describeUnclaimed(entry: UnclaimedFolder): string {
|
|
|
338
337
|
}
|
|
339
338
|
|
|
340
339
|
/**
|
|
341
|
-
* The
|
|
342
|
-
* gets the same treatment from the unmigrated section above, which is the
|
|
343
|
-
* precedent this follows rather than a second spelling of it.
|
|
340
|
+
* The proposal-only skills no other field reaches.
|
|
344
341
|
*/
|
|
345
342
|
function renderMigrations(report: CheckReport): void {
|
|
346
343
|
const { migrations } = report.reverse
|
package/src/context/citations.ts
CHANGED
|
@@ -14,6 +14,43 @@ import { listRepositoryFiles } from '@/git-files'
|
|
|
14
14
|
*/
|
|
15
15
|
export const IGNORE_MARKER = 'audit-ignore-citations'
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Matches the marker plus an optional comma-separated path list.
|
|
19
|
+
*
|
|
20
|
+
* `<!-- audit-ignore-citations: .claude/standards/X.md -->` narrows the skip
|
|
21
|
+
* to the paths named. The bare form with no colon skips every citation the
|
|
22
|
+
* line carries, which is the only shape a line with nothing else worth
|
|
23
|
+
* checking needs and the shape every marker predating this pattern still
|
|
24
|
+
* carries.
|
|
25
|
+
*/
|
|
26
|
+
const IGNORE_MARKER_LINE =
|
|
27
|
+
/<!--\s*audit-ignore-citations(?::\s*([^>]+?))?\s*-->/
|
|
28
|
+
|
|
29
|
+
interface IgnoredCitations {
|
|
30
|
+
readonly all: boolean
|
|
31
|
+
readonly paths: ReadonlySet<string>
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Reads which citations on a line the marker excuses.
|
|
36
|
+
*
|
|
37
|
+
* A bare marker excuses every citation the line carries, matching the
|
|
38
|
+
* pre-named-path behavior. A named marker excuses only the paths listed,
|
|
39
|
+
* so a line carrying both a placeholder and a real reference keeps the real
|
|
40
|
+
* one checked rather than losing it to the placeholder beside it.
|
|
41
|
+
*/
|
|
42
|
+
function ignoredCitations(line: string): IgnoredCitations | undefined {
|
|
43
|
+
const match = line.match(IGNORE_MARKER_LINE)
|
|
44
|
+
if (!match) return undefined
|
|
45
|
+
if (match[1] === undefined) return { all: true, paths: new Set() }
|
|
46
|
+
|
|
47
|
+
const paths = match[1]
|
|
48
|
+
.split(',')
|
|
49
|
+
.map((path) => path.trim())
|
|
50
|
+
.filter(Boolean)
|
|
51
|
+
return { all: false, paths: new Set(paths) }
|
|
52
|
+
}
|
|
53
|
+
|
|
17
54
|
/**
|
|
18
55
|
* Trees holding content authored to be parsed rather than followed.
|
|
19
56
|
*
|
|
@@ -87,9 +124,12 @@ export function collectCitations(
|
|
|
87
124
|
fenced = !fenced
|
|
88
125
|
continue
|
|
89
126
|
}
|
|
90
|
-
if (fenced
|
|
127
|
+
if (fenced) continue
|
|
128
|
+
|
|
129
|
+
const ignored = ignoredCitations(line)
|
|
91
130
|
|
|
92
131
|
for (const match of line.matchAll(pattern)) {
|
|
132
|
+
if (ignored && (ignored.all || ignored.paths.has(match[0]))) continue
|
|
93
133
|
found.push({ file: rel, line: index + 1, path: match[0] })
|
|
94
134
|
}
|
|
95
135
|
}
|
package/src/init/flags.ts
CHANGED
|
@@ -26,11 +26,6 @@ export const INIT_OPTIONS: readonly InitOptionSpec[] = [
|
|
|
26
26
|
flags: '--add <rules>',
|
|
27
27
|
description: 'Comma-separated governance rules to layer on',
|
|
28
28
|
},
|
|
29
|
-
{
|
|
30
|
-
key: 'snippets',
|
|
31
|
-
flags: '--snippets <category>',
|
|
32
|
-
description: "Snippets preset, category, 'none', or 'all'. Omit to skip",
|
|
33
|
-
},
|
|
34
29
|
{
|
|
35
30
|
key: 'skip',
|
|
36
31
|
flags: '--skip <list>',
|
|
@@ -54,9 +49,9 @@ export function applyInitOptions(command: Command): Command {
|
|
|
54
49
|
|
|
55
50
|
/**
|
|
56
51
|
* Whether the operator passed any flag, which is what makes the command
|
|
57
|
-
* scriptable by suppressing the confirmation prompt. `--stack`
|
|
58
|
-
*
|
|
59
|
-
*
|
|
52
|
+
* scriptable by suppressing the confirmation prompt. `--stack` carries a
|
|
53
|
+
* default, so presence has to be read from where the value came from rather
|
|
54
|
+
* than from the value itself.
|
|
60
55
|
*/
|
|
61
56
|
export function flagsProvided(cmd: Command): boolean {
|
|
62
57
|
return INIT_OPTIONS.some(
|
package/src/init/plan.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const SKIPPABLE_DOMAINS = ['wiki', 'governance'
|
|
1
|
+
export const SKIPPABLE_DOMAINS = ['wiki', 'governance'] as const
|
|
2
2
|
|
|
3
3
|
export type SkippableDomain = (typeof SKIPPABLE_DOMAINS)[number]
|
|
4
4
|
|
|
@@ -30,23 +30,9 @@ export interface InitPlan {
|
|
|
30
30
|
export interface InitFlags {
|
|
31
31
|
readonly stack?: string
|
|
32
32
|
readonly add?: string
|
|
33
|
-
/** Absent when `--snippets` was never passed, which installs none. */
|
|
34
|
-
readonly snippets?: string
|
|
35
33
|
readonly skip: SkipPlan
|
|
36
34
|
}
|
|
37
35
|
|
|
38
|
-
/**
|
|
39
|
-
* Reads why the snippets step will not run, or `undefined` when it will.
|
|
40
|
-
* Omitting `--snippets` reaches the same skip as `--skip snippets` explicitly,
|
|
41
|
-
* so a plain `aitk init` installs none and both routes share one recovery
|
|
42
|
-
* line rather than two behaviors with one spelling each.
|
|
43
|
-
*/
|
|
44
|
-
export function snippetsSkipReason(flags: InitFlags): string | undefined {
|
|
45
|
-
if (flags.skip.skipped.has('snippets')) return '--skip snippets'
|
|
46
|
-
if (flags.snippets === undefined) return 'no --snippets given'
|
|
47
|
-
return undefined
|
|
48
|
-
}
|
|
49
|
-
|
|
50
36
|
/**
|
|
51
37
|
* Resolves the stack the run will install. An empty `--stack` reads as absent
|
|
52
38
|
* rather than as a way to decline, so the flag carries a real name or nothing
|
|
@@ -111,12 +97,6 @@ export function planInit(flags: InitFlags): InitPlan {
|
|
|
111
97
|
})
|
|
112
98
|
}
|
|
113
99
|
|
|
114
|
-
if (snippetsSkipReason(flags) !== undefined) {
|
|
115
|
-
preview.push({ level: 'warn', text: 'snippets (skipped)' })
|
|
116
|
-
} else {
|
|
117
|
-
preview.push({ level: 'info', text: `snippets (${flags.snippets})` })
|
|
118
|
-
}
|
|
119
|
-
|
|
120
100
|
if (!flags.skip.skipped.has('wiki')) {
|
|
121
101
|
preview.push({
|
|
122
102
|
level: 'info',
|
package/src/init/steps.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type InitFlags, resolveStack
|
|
1
|
+
import { type InitFlags, resolveStack } from '@/init/plan'
|
|
2
2
|
import type { DomainStep } from '@/init/run'
|
|
3
3
|
|
|
4
4
|
/** Builds the child-process invocation for one domain. */
|
|
@@ -48,26 +48,6 @@ export function buildSteps(
|
|
|
48
48
|
})
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
// `snippets === undefined` here can never fire on its own: `snippetsSkipReason`
|
|
52
|
-
// already returns a reason whenever `flags.snippets` is undefined. It is what
|
|
53
|
-
// narrows `snippets` to `string` for the child() call below, since TypeScript
|
|
54
|
-
// cannot narrow a property through a function call.
|
|
55
|
-
const snippets = flags.snippets
|
|
56
|
-
const skipReason = snippetsSkipReason(flags)
|
|
57
|
-
if (skipReason !== undefined || snippets === undefined) {
|
|
58
|
-
steps.push({
|
|
59
|
-
kind: 'skip',
|
|
60
|
-
label: 'Snippets',
|
|
61
|
-
notice: `Skipped: ${skipReason ?? 'no --snippets given'}. Run 'aitk snippets install essentials ${target}' to install snippets.`,
|
|
62
|
-
})
|
|
63
|
-
} else {
|
|
64
|
-
steps.push({
|
|
65
|
-
kind: 'run',
|
|
66
|
-
label: 'Snippets',
|
|
67
|
-
run: child(['snippets', 'install', snippets, resolved]),
|
|
68
|
-
})
|
|
69
|
-
}
|
|
70
|
-
|
|
71
51
|
if (!flags.skip.skipped.has('wiki')) {
|
|
72
52
|
steps.push({
|
|
73
53
|
kind: 'run',
|
package/src/sync/check.ts
CHANGED
|
@@ -4,7 +4,6 @@ import { execa } from 'execa'
|
|
|
4
4
|
import { gitEnv } from '@/git-env'
|
|
5
5
|
import { createGovAdapter, rulesSourceDir } from '@/gov/adapter'
|
|
6
6
|
import { loadGovStack, resolveMissingRules, resolveRules } from '@/gov/stacks'
|
|
7
|
-
import { createSnippetsAdapter } from '@/snippets/adapter'
|
|
8
7
|
import { planSync, type ScanEntry, type SyncAdapter } from '@/sync/engine'
|
|
9
8
|
import {
|
|
10
9
|
collectSuperseded,
|
|
@@ -34,12 +33,11 @@ import { readSkew, type SkewReport } from '@/version/skew'
|
|
|
34
33
|
/**
|
|
35
34
|
* Domains the sync engine walks file by file. Tooling is a stamp domain without
|
|
36
35
|
* being one of these, because `src/tooling/` never calls `planSync`, so the
|
|
37
|
-
* three lookups below have no entry to offer it. Standards
|
|
38
|
-
* the install
|
|
39
|
-
* installed copy to attribute.
|
|
36
|
+
* three lookups below have no entry to offer it. Standards and snippets left
|
|
37
|
+
* the list with their install channels: nothing writes either corpus into a
|
|
38
|
+
* target, so there is no installed copy to attribute.
|
|
40
39
|
*/
|
|
41
40
|
export const SCANNED_DOMAINS = [
|
|
42
|
-
'snippets',
|
|
43
41
|
'governance',
|
|
44
42
|
] as const satisfies readonly StampDomain[]
|
|
45
43
|
|
|
@@ -51,17 +49,14 @@ export type ScannedDomain = (typeof SCANNED_DOMAINS)[number]
|
|
|
51
49
|
* never go stale and belong in the read-only section instead.
|
|
52
50
|
*/
|
|
53
51
|
const SYNCED_SOURCES: Record<ScannedDomain, string> = {
|
|
54
|
-
snippets: 'snippets/',
|
|
55
52
|
governance: 'governance/rules/',
|
|
56
53
|
}
|
|
57
54
|
|
|
58
55
|
const ADAPTERS: Record<ScannedDomain, (root: string) => SyncAdapter> = {
|
|
59
|
-
snippets: createSnippetsAdapter,
|
|
60
56
|
governance: createGovAdapter,
|
|
61
57
|
}
|
|
62
58
|
|
|
63
59
|
const INSTALL_MARKERS: Record<ScannedDomain, readonly string[]> = {
|
|
64
|
-
snippets: ['.claude', 'snippets'],
|
|
65
60
|
governance: ['.claude', 'rules'],
|
|
66
61
|
}
|
|
67
62
|
|
|
@@ -547,9 +542,9 @@ export function baseBands(root: string): Set<string> {
|
|
|
547
542
|
* The diff-and-bands path stays as the fallback for a target stamped before
|
|
548
543
|
* governance recorded a chain. Rules are domain-scoped there too, so it
|
|
549
544
|
* measures from governance's own anchor rather than from the oldest anchor
|
|
550
|
-
* across domains the way `readNewSkills` does. A shared anchor would let
|
|
551
|
-
*
|
|
552
|
-
* of the read.
|
|
545
|
+
* across domains the way `readNewSkills` does. A shared anchor would let
|
|
546
|
+
* another domain's sync move the revision rules are measured from and drop a
|
|
547
|
+
* rule out of the read.
|
|
553
548
|
*
|
|
554
549
|
* A target carrying no chain and no governance anchor reports nothing. It has
|
|
555
550
|
* no date to measure against, and diffing from the beginning of history would
|
package/src/sync/layout.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { existsSync, statSync } from 'node:fs'
|
|
2
2
|
import { basename, join } from 'node:path'
|
|
3
3
|
import { SUBDIRS } from '@/claude/seeds'
|
|
4
|
-
import { snippetsSourceDir } from '@/snippets/categories'
|
|
5
4
|
import type { StampDomain } from '@/sync/stamp'
|
|
6
5
|
|
|
7
6
|
const CLAUDE_DIR = '.claude'
|
|
@@ -10,18 +9,22 @@ const CLAUDE_DIR = '.claude'
|
|
|
10
9
|
* Domains an older toolkit installed at the project root, each with the source
|
|
11
10
|
* folder naming what it owns. Governance is absent because its rules have always
|
|
12
11
|
* landed under `.claude/rules/`, so there is no earlier location to be stranded
|
|
13
|
-
* at. Standards
|
|
14
|
-
* a root `standards/` folder there is the
|
|
15
|
-
* reporting it as unmigrated would propose
|
|
12
|
+
* at. Standards and snippets are absent because no copy installs into a target
|
|
13
|
+
* at all now, so a root `standards/` or `snippets/` folder there is the
|
|
14
|
+
* project's own authoring surface and reporting it as unmigrated would propose
|
|
15
|
+
* moving files nothing installed.
|
|
16
16
|
*
|
|
17
17
|
* A tuple array rather than a partial record, so the domain key stays typed
|
|
18
|
-
* without asserting an `Object.entries` result back into the union.
|
|
18
|
+
* without asserting an `Object.entries` result back into the union. Empty
|
|
19
|
+
* now that both former entries retired their install channels. A future
|
|
20
|
+
* domain installing at a target's root, the way an older toolkit installed
|
|
21
|
+
* standards and snippets there, would be the next entry to add.
|
|
19
22
|
*/
|
|
20
23
|
const ROOT_LAYOUTS: readonly (readonly [
|
|
21
24
|
StampDomain,
|
|
22
25
|
string,
|
|
23
26
|
(root: string) => string,
|
|
24
|
-
])[] = [
|
|
27
|
+
])[] = []
|
|
25
28
|
|
|
26
29
|
/**
|
|
27
30
|
* A target file that a shipped seed folder replaced. Carries no source and
|
package/src/sync/reverse.ts
CHANGED
|
@@ -47,8 +47,7 @@ export interface UnclaimedFolder {
|
|
|
47
47
|
|
|
48
48
|
/**
|
|
49
49
|
* A proposal-only skill with a live case in this target. The skills propose and
|
|
50
|
-
* never execute, so the report names one rather than queueing a change
|
|
51
|
-
* is the treatment `unmigrated` already gives `migration-standards`.
|
|
50
|
+
* never execute, so the report names one rather than queueing a change.
|
|
52
51
|
*/
|
|
53
52
|
export interface MigrationCandidate {
|
|
54
53
|
readonly skill: string
|
|
@@ -208,9 +207,8 @@ function covers(index: HistoryIndex, sourceRel: string): boolean {
|
|
|
208
207
|
}
|
|
209
208
|
|
|
210
209
|
/**
|
|
211
|
-
* Live cases for the two proposal-only skills no report field
|
|
212
|
-
*
|
|
213
|
-
* this its two siblings are documented and unreachable.
|
|
210
|
+
* Live cases for the two proposal-only skills no other report field names.
|
|
211
|
+
* Without this, both are documented and unreachable.
|
|
214
212
|
*
|
|
215
213
|
* Both tests read what the skill itself acts on, so a proposal the report makes
|
|
216
214
|
* is one the skill has work to do for. A context tier that already holds files
|
package/src/sync/stamp.ts
CHANGED
|
@@ -5,16 +5,16 @@ import { dirname, join, sep } from 'node:path'
|
|
|
5
5
|
import { execa } from 'execa'
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
* Domains the stamp can record.
|
|
9
|
-
*
|
|
8
|
+
* Domains the stamp can record. Governance attributes file by file through the
|
|
9
|
+
* sync engine. Tooling runs its own inject and manifest machinery, so it
|
|
10
10
|
* records the stack chain it resolved instead and carries no file hashes.
|
|
11
11
|
*
|
|
12
|
-
* A stamp written before the standards install channel closed
|
|
13
|
-
* `standards` record. `isStamp` ignores the key
|
|
14
|
-
* the next write, so the target loses a domain
|
|
15
|
-
* losing the whole file.
|
|
12
|
+
* A stamp written before the standards or snippets install channel closed
|
|
13
|
+
* still carries a `standards` or `snippets` record. `isStamp` ignores the key
|
|
14
|
+
* and `sortDomains` drops it on the next write, so the target loses a domain
|
|
15
|
+
* nothing can refresh rather than losing the whole file.
|
|
16
16
|
*/
|
|
17
|
-
export const STAMP_DOMAINS = ['
|
|
17
|
+
export const STAMP_DOMAINS = ['governance', 'tooling'] as const
|
|
18
18
|
|
|
19
19
|
export type StampDomain = (typeof STAMP_DOMAINS)[number]
|
|
20
20
|
|
package/src/sync/target.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { existsSync } from 'node:fs'
|
|
|
2
2
|
import { join } from 'node:path'
|
|
3
3
|
import { isDirectory } from '@/target'
|
|
4
4
|
|
|
5
|
-
export const SYNC_DOMAINS = ['
|
|
5
|
+
export const SYNC_DOMAINS = ['governance', 'claude'] as const
|
|
6
6
|
|
|
7
7
|
export type SyncDomain = (typeof SYNC_DOMAINS)[number]
|
|
8
8
|
|
|
@@ -12,7 +12,6 @@ export interface DomainState {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
const DOMAIN_MARKERS: Record<SyncDomain, string> = {
|
|
15
|
-
snippets: join('.claude', 'snippets'),
|
|
16
15
|
governance: join('.claude', 'rules'),
|
|
17
16
|
claude: '.claude',
|
|
18
17
|
}
|
|
@@ -24,7 +23,6 @@ const DOMAIN_MARKERS: Record<SyncDomain, string> = {
|
|
|
24
23
|
* because that is the only file `aitk claude sync` writes.
|
|
25
24
|
*/
|
|
26
25
|
const DOMAIN_PATHS: Record<SyncDomain, readonly string[]> = {
|
|
27
|
-
snippets: ['.claude/snippets/'],
|
|
28
26
|
governance: ['.claude/rules/', '.claude/GOV.md'],
|
|
29
27
|
claude: ['.gitignore'],
|
|
30
28
|
}
|
package/standards/skill.md
CHANGED
|
@@ -235,7 +235,7 @@ A skill reads from two roots. Know which one a file lives under before referenci
|
|
|
235
235
|
No standard installs into a project, so a body cites one place rather than choosing between two.
|
|
236
236
|
|
|
237
237
|
- Cite `${CLAUDE_SKILL_DIR}/../../standards/X.md`. The plugin ships the whole standards folder beside `skills/`, so the path resolves in every install and needs no fallback behind it.
|
|
238
|
-
- Never cite `.claude/standards/X.md` from a shipped body. A target holds no such folder, and one left behind by an older toolkit is a stale copy no resolver reads.
|
|
238
|
+
- Never cite `.claude/standards/X.md` from a shipped body. A target holds no such folder, and one left behind by an older toolkit is a stale copy no resolver reads. <!-- audit-ignore-citations: .claude/standards/X.md -->
|
|
239
239
|
- Name `aitk standards X` instead where the body wants the document rather than a path to open, such as a value it captures or reports. That verb resolves `standards/` at the project root and then the corpus inside the package.
|
|
240
240
|
- State the path once per body, at the site that reads the standard. A later mention of a standard the body already read stays bare, since repeating the path at every mention is noise rather than instruction.
|
|
241
241
|
- A guard on a standard's presence names the file rather than the folder holding it, since a folder test answers for a sibling that happens to be there.
|