@erclx/aitk 1.7.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/claude-autoship/SKILL.md +2 -2
- package/claude/skills/claude-docs/SKILL.md +11 -0
- package/claude/skills/claude-groundwork/SKILL.md +1 -1
- package/claude/skills/claude-seed-sync/REQUIREMENT.md +2 -2
- package/claude/skills/claude-seed-sync/SKILL.md +15 -18
- package/claude/skills/create-standard/REQUIREMENT.md +7 -10
- package/claude/skills/create-standard/SKILL.md +10 -11
- package/claude/skills/git-pr/references/labels.md +7 -1
- package/claude/skills/migration-standards/REQUIREMENT.md +10 -7
- package/claude/skills/migration-standards/SKILL.md +24 -22
- package/claude/skills/migration-superseded/REQUIREMENT.md +2 -2
- package/claude/skills/migration-superseded/SKILL.md +7 -9
- package/claude/skills/setup-gov/REQUIREMENT.md +2 -2
- package/claude/skills/toolkit-cli/SKILL.md +13 -14
- package/docs/agents/commands.md +1 -1
- package/docs/agents/install-and-sync.md +27 -41
- package/docs/agents/records.md +1 -1
- package/docs/agents/scripting.md +2 -9
- package/docs/target-projects.md +14 -15
- package/package.json +1 -1
- package/scripts/core/install-check.sh +5 -1
- package/scripts/manage-sandbox.sh +4 -7
- package/scripts/standards/list.sh +6 -4
- package/src/cli.ts +1 -1
- package/src/commands/gov.ts +1 -7
- package/src/commands/init.ts +1 -4
- package/src/commands/standards.ts +9 -141
- package/src/commands/sync.ts +1 -2
- package/src/gov/install.ts +0 -9
- package/src/init/flags.ts +3 -10
- package/src/init/plan.ts +8 -26
- package/src/init/steps.ts +0 -24
- package/src/records/backup.ts +11 -4
- package/src/standards/read.ts +16 -14
- package/src/sync/check.ts +4 -7
- package/src/sync/layout.ts +8 -10
- package/src/sync/stamp.ts +18 -15
- package/src/sync/target.ts +1 -8
- package/standards/skill.md +6 -6
- package/standards/standard.md +1 -1
- package/tooling/claude/manifest.toml +1 -1
- package/tooling/claude/reference.md +1 -1
- package/tooling/claude/seeds/.claude/ARCHITECTURE.md +1 -1
- package/tooling/claude/seeds/.claude/DESIGN.md +1 -1
- package/tooling/claude/seeds/.claude/REQUIREMENTS.md +1 -1
- package/tooling/claude/seeds/CLAUDE.md +2 -2
- package/src/standards/adapter.ts +0 -51
- package/src/standards/closure.ts +0 -200
- package/src/standards/index-refresh.ts +0 -44
- package/src/standards/install.ts +0 -52
package/src/init/plan.ts
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
export const SKIPPABLE_DOMAINS = ['wiki', 'standards', 'governance'] as const
|
|
1
|
+
export const SKIPPABLE_DOMAINS = ['wiki', 'governance'] as const
|
|
4
2
|
|
|
5
3
|
export type SkippableDomain = (typeof SKIPPABLE_DOMAINS)[number]
|
|
6
4
|
|
|
7
5
|
/**
|
|
8
6
|
* The stack a caller gets without asking. Governance installs on every init so
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* a scaffolded project arrives with the rules that route it, and
|
|
8
|
+
* `--skip governance` is the one spelling for declining.
|
|
11
9
|
*/
|
|
12
10
|
export const DEFAULT_STACK = 'base'
|
|
13
11
|
|
|
@@ -33,7 +31,6 @@ export interface InitFlags {
|
|
|
33
31
|
readonly stack?: string
|
|
34
32
|
readonly add?: string
|
|
35
33
|
readonly snippets: string
|
|
36
|
-
readonly standards: string
|
|
37
34
|
readonly skip: SkipPlan
|
|
38
35
|
}
|
|
39
36
|
|
|
@@ -73,11 +70,7 @@ export function parseSkip(csv: string | undefined): SkipPlan {
|
|
|
73
70
|
* prints nothing at all and declined governance prints a warning, so neither
|
|
74
71
|
* reaches the total.
|
|
75
72
|
*
|
|
76
|
-
*
|
|
77
|
-
* warning names the consequence rather than only the action. Declining
|
|
78
|
-
* standards too removes that consequence, and the warning drops it.
|
|
79
|
-
*
|
|
80
|
-
* The skip also drops `--add`, which names rules the caller asked for. Input
|
|
73
|
+
* The skip drops `--add`, which names rules the caller asked for. Input
|
|
81
74
|
* that goes nowhere is reported for the same reason `parseSkip` reports an
|
|
82
75
|
* unrecognized value, so the warning names the flag rather than dropping it
|
|
83
76
|
* without a word.
|
|
@@ -91,13 +84,10 @@ export function planInit(flags: InitFlags): InitPlan {
|
|
|
91
84
|
const stack = resolveStack(flags.stack)
|
|
92
85
|
|
|
93
86
|
if (flags.skip.skipped.has('governance')) {
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
notes.push('standards land without the rules that route to them')
|
|
99
|
-
|
|
100
|
-
const detail = notes.length === 0 ? '' : `, ${notes.join(', ')}`
|
|
87
|
+
const detail =
|
|
88
|
+
flags.add === undefined || flags.add === ''
|
|
89
|
+
? ''
|
|
90
|
+
: `, --add ${flags.add} not installed`
|
|
101
91
|
preview.push({ level: 'warn', text: `governance (skipped${detail})` })
|
|
102
92
|
} else if (flags.add === undefined || flags.add === '') {
|
|
103
93
|
preview.push({ level: 'info', text: `governance (stack: ${stack})` })
|
|
@@ -108,14 +98,6 @@ export function planInit(flags: InitFlags): InitPlan {
|
|
|
108
98
|
})
|
|
109
99
|
}
|
|
110
100
|
|
|
111
|
-
if (!flags.skip.skipped.has('standards')) {
|
|
112
|
-
const detail =
|
|
113
|
-
flags.standards === ALL_SELECTION
|
|
114
|
-
? 'authoring conventions'
|
|
115
|
-
: `${flags.standards}, plus what they cite`
|
|
116
|
-
preview.push({ level: 'info', text: `standards (${detail})` })
|
|
117
|
-
}
|
|
118
|
-
|
|
119
101
|
preview.push({ level: 'info', text: `snippets (${flags.snippets})` })
|
|
120
102
|
|
|
121
103
|
if (!flags.skip.skipped.has('wiki')) {
|
package/src/init/steps.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { type InitFlags, resolveStack } from '@/init/plan'
|
|
2
2
|
import type { DomainStep } from '@/init/run'
|
|
3
|
-
import { ALL_SELECTION } from '@/standards/closure'
|
|
4
3
|
|
|
5
4
|
/** Builds the child-process invocation for one domain. */
|
|
6
5
|
export type RunFactory = (args: readonly string[]) => () => Promise<boolean>
|
|
@@ -49,14 +48,6 @@ export function buildSteps(
|
|
|
49
48
|
})
|
|
50
49
|
}
|
|
51
50
|
|
|
52
|
-
if (!flags.skip.skipped.has('standards')) {
|
|
53
|
-
steps.push({
|
|
54
|
-
kind: 'run',
|
|
55
|
-
label: 'Standards',
|
|
56
|
-
run: child(standardsArgs(flags.standards, resolved)),
|
|
57
|
-
})
|
|
58
|
-
}
|
|
59
|
-
|
|
60
51
|
steps.push({
|
|
61
52
|
kind: 'run',
|
|
62
53
|
label: 'Snippets',
|
|
@@ -74,21 +65,6 @@ export function buildSteps(
|
|
|
74
65
|
return steps
|
|
75
66
|
}
|
|
76
67
|
|
|
77
|
-
/**
|
|
78
|
-
* Builds the `standards install` argv. `all` is left off rather than spelled
|
|
79
|
-
* out, so the default init runs the same command it ran before the flag
|
|
80
|
-
* existed.
|
|
81
|
-
*/
|
|
82
|
-
function standardsArgs(selection: string, path: string): string[] {
|
|
83
|
-
const args = ['standards', 'install']
|
|
84
|
-
if (selection !== '' && selection !== ALL_SELECTION) {
|
|
85
|
-
args.push('--only', selection)
|
|
86
|
-
}
|
|
87
|
-
args.push(path)
|
|
88
|
-
|
|
89
|
-
return args
|
|
90
|
-
}
|
|
91
|
-
|
|
92
68
|
/**
|
|
93
69
|
* Builds the `gov install` argv. The run and the recovery command a skip prints
|
|
94
70
|
* come from here both, so the command a caller is told to paste installs what
|
package/src/records/backup.ts
CHANGED
|
@@ -5,10 +5,17 @@ import { gitEnv } from '@/git-env'
|
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* The folders a backup carries, relative to `.claude/`. They are the `# Claude`
|
|
8
|
-
* group
|
|
9
|
-
* without loss,
|
|
10
|
-
* enclosing repository already
|
|
11
|
-
*
|
|
8
|
+
* group the claude manifest ships, minus three: `.claude/.tmp`, which is
|
|
9
|
+
* defined as deletable without loss, `.claude/worktrees/`, whose contents
|
|
10
|
+
* belong to the enclosing repository already, and `.claude/.records.git/`,
|
|
11
|
+
* which is the history the other ten are pushed into. The list is spelled out
|
|
12
|
+
* rather than read off that group so adding an ignore entry cannot silently
|
|
13
|
+
* enlarge the payload.
|
|
14
|
+
*
|
|
15
|
+
* The manifest group is the one this reads rather than the enclosing
|
|
16
|
+
* repository's own `.gitignore`, which spreads the same entries across two
|
|
17
|
+
* headers and carries `.claude/README.md` that no target receives. Subtracting
|
|
18
|
+
* three from that file instead yields eleven names against this list of ten.
|
|
12
19
|
*
|
|
13
20
|
* `RECORD_KINDS` in `validate.ts` overlaps this on five names and carries one
|
|
14
21
|
* more that no backup reaches. The two lists differ on purpose: one is what a
|
package/src/standards/read.ts
CHANGED
|
@@ -2,8 +2,13 @@ import { existsSync, readdirSync, readFileSync } from 'node:fs'
|
|
|
2
2
|
import { basename, join } from 'node:path'
|
|
3
3
|
import { stripFrontmatter } from '@/frontmatter'
|
|
4
4
|
import { PROJECT_ROOT } from '@/project-root'
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
|
|
6
|
+
export const INDEX_FILE = 'index.md'
|
|
7
|
+
|
|
8
|
+
/** The folder a repository authors standards in, toolkit or project alike. */
|
|
9
|
+
export function standardsSourceDir(root: string): string {
|
|
10
|
+
return join(root, 'standards')
|
|
11
|
+
}
|
|
7
12
|
|
|
8
13
|
/**
|
|
9
14
|
* Spells the package root in a report where every other root spells a
|
|
@@ -27,22 +32,19 @@ export interface ResolvedStandard {
|
|
|
27
32
|
}
|
|
28
33
|
|
|
29
34
|
/**
|
|
30
|
-
* The
|
|
35
|
+
* The roots a standard resolves against. No toolkit standard installs into a
|
|
36
|
+
* project any more, so `.claude/standards/` is gone from the list and a target
|
|
37
|
+
* carries neither entry below: the package corpus is the only root that
|
|
38
|
+
* answers there, which is what leaves no precedence to reason about.
|
|
31
39
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* measured against one, since `package.json` ships the corpus beside the code
|
|
37
|
-
* doing the reading.
|
|
40
|
+
* The authoring root stays ahead of the package corpus for the repository that
|
|
41
|
+
* writes standards, where an edit in progress has to win over the copy
|
|
42
|
+
* `package.json` shipped. A project that authors standards of its own is the
|
|
43
|
+
* same case under the same folder.
|
|
38
44
|
*/
|
|
39
45
|
export function standardRoots(root: string): StandardRoot[] {
|
|
40
46
|
return [
|
|
41
|
-
{
|
|
42
|
-
dir: join(root, '.claude', 'standards'),
|
|
43
|
-
label: join('.claude', 'standards'),
|
|
44
|
-
},
|
|
45
|
-
{ dir: join(root, 'standards'), label: 'standards' },
|
|
47
|
+
{ dir: standardsSourceDir(root), label: 'standards' },
|
|
46
48
|
{
|
|
47
49
|
dir: standardsSourceDir(PROJECT_ROOT),
|
|
48
50
|
label: join(PACKAGE_LABEL, 'standards'),
|
package/src/sync/check.ts
CHANGED
|
@@ -23,7 +23,6 @@ import {
|
|
|
23
23
|
stampedCommit,
|
|
24
24
|
type StampDomain,
|
|
25
25
|
} from '@/sync/stamp'
|
|
26
|
-
import { createStandardsAdapter } from '@/standards/adapter'
|
|
27
26
|
import { isDirectory } from '@/target'
|
|
28
27
|
import { loadManifest } from '@/tooling/manifest'
|
|
29
28
|
import { scan } from '@/tooling/scan'
|
|
@@ -32,10 +31,11 @@ import { readSkew, type SkewReport } from '@/version/skew'
|
|
|
32
31
|
/**
|
|
33
32
|
* Domains the sync engine walks file by file. Tooling is a stamp domain without
|
|
34
33
|
* being one of these, because `src/tooling/` never calls `planSync`, so the
|
|
35
|
-
* three lookups below have no entry to offer it.
|
|
34
|
+
* three lookups below have no entry to offer it. Standards left the list with
|
|
35
|
+
* the install channel: nothing writes the corpus into a target, so there is no
|
|
36
|
+
* installed copy to attribute.
|
|
36
37
|
*/
|
|
37
38
|
export const SCANNED_DOMAINS = [
|
|
38
|
-
'standards',
|
|
39
39
|
'snippets',
|
|
40
40
|
'governance',
|
|
41
41
|
] as const satisfies readonly StampDomain[]
|
|
@@ -48,19 +48,16 @@ export type ScannedDomain = (typeof SCANNED_DOMAINS)[number]
|
|
|
48
48
|
* never go stale and belong in the read-only section instead.
|
|
49
49
|
*/
|
|
50
50
|
const SYNCED_SOURCES: Record<ScannedDomain, string> = {
|
|
51
|
-
standards: 'standards/',
|
|
52
51
|
snippets: 'snippets/',
|
|
53
52
|
governance: 'governance/rules/',
|
|
54
53
|
}
|
|
55
54
|
|
|
56
55
|
const ADAPTERS: Record<ScannedDomain, (root: string) => SyncAdapter> = {
|
|
57
|
-
standards: createStandardsAdapter,
|
|
58
56
|
snippets: createSnippetsAdapter,
|
|
59
57
|
governance: createGovAdapter,
|
|
60
58
|
}
|
|
61
59
|
|
|
62
60
|
const INSTALL_MARKERS: Record<ScannedDomain, readonly string[]> = {
|
|
63
|
-
standards: ['.claude', 'standards'],
|
|
64
61
|
snippets: ['.claude', 'snippets'],
|
|
65
62
|
governance: ['.claude', 'rules'],
|
|
66
63
|
}
|
|
@@ -304,7 +301,7 @@ export function hasDrift(report: CheckReport): boolean {
|
|
|
304
301
|
/**
|
|
305
302
|
* Bounds each domain's upstream read by that domain's own anchor and its own
|
|
306
303
|
* source path. A shared anchor would let a gov sync advance the revision
|
|
307
|
-
*
|
|
304
|
+
* snippets measures from, silently dropping a snippets change out of the read.
|
|
308
305
|
*/
|
|
309
306
|
export async function buildCheckReport(
|
|
310
307
|
toolkitRoot: string,
|
package/src/sync/layout.ts
CHANGED
|
@@ -2,7 +2,6 @@ import { existsSync, statSync } from 'node:fs'
|
|
|
2
2
|
import { basename, join } from 'node:path'
|
|
3
3
|
import { SUBDIRS } from '@/claude/seeds'
|
|
4
4
|
import { snippetsSourceDir } from '@/snippets/categories'
|
|
5
|
-
import { standardsSourceDir } from '@/standards/adapter'
|
|
6
5
|
import type { StampDomain } from '@/sync/stamp'
|
|
7
6
|
|
|
8
7
|
const CLAUDE_DIR = '.claude'
|
|
@@ -11,7 +10,9 @@ const CLAUDE_DIR = '.claude'
|
|
|
11
10
|
* Domains an older toolkit installed at the project root, each with the source
|
|
12
11
|
* folder naming what it owns. Governance is absent because its rules have always
|
|
13
12
|
* landed under `.claude/rules/`, so there is no earlier location to be stranded
|
|
14
|
-
* at.
|
|
13
|
+
* at. Standards is absent because no copy installs into a target at all now, so
|
|
14
|
+
* a root `standards/` folder there is the project's own authoring surface and
|
|
15
|
+
* reporting it as unmigrated would propose moving files nothing installed.
|
|
15
16
|
*
|
|
16
17
|
* A tuple array rather than a partial record, so the domain key stays typed
|
|
17
18
|
* without asserting an `Object.entries` result back into the union.
|
|
@@ -20,10 +21,7 @@ const ROOT_LAYOUTS: readonly (readonly [
|
|
|
20
21
|
StampDomain,
|
|
21
22
|
string,
|
|
22
23
|
(root: string) => string,
|
|
23
|
-
])[] = [
|
|
24
|
-
['standards', 'standards', standardsSourceDir],
|
|
25
|
-
['snippets', 'snippets', snippetsSourceDir],
|
|
26
|
-
]
|
|
24
|
+
])[] = [['snippets', 'snippets', snippetsSourceDir]]
|
|
27
25
|
|
|
28
26
|
/**
|
|
29
27
|
* A target file that a shipped seed folder replaced. Carries no source and
|
|
@@ -79,7 +77,7 @@ export function collectSuperseded(target: string): SupersededEntry[] {
|
|
|
79
77
|
*
|
|
80
78
|
* A root folder is claimed only when it holds a file the toolkit ships under
|
|
81
79
|
* that domain. Presence of the folder alone is not evidence: a project can
|
|
82
|
-
* carry its own `
|
|
80
|
+
* carry its own `snippets/` of prompts it wrote and never have installed the
|
|
83
81
|
* domain, and calling that unmigrated would fail `--exit-code` with no action
|
|
84
82
|
* that clears it.
|
|
85
83
|
*/
|
|
@@ -107,9 +105,9 @@ export function detectUnmigrated(
|
|
|
107
105
|
|
|
108
106
|
/**
|
|
109
107
|
* Root files whose basename matches something the toolkit ships for this domain.
|
|
110
|
-
* Basenames rather than relative paths, because
|
|
111
|
-
*
|
|
112
|
-
* toolkit-owned rather than which source each one came from.
|
|
108
|
+
* Basenames rather than relative paths, because the root layout an older toolkit
|
|
109
|
+
* wrote is flat while the source nests by category, and the question here is only
|
|
110
|
+
* whether any file is toolkit-owned rather than which source each one came from.
|
|
113
111
|
*/
|
|
114
112
|
function countToolkitOwned(dir: string, sourceDir: string): number {
|
|
115
113
|
const owned = new Set(listMarkdown(sourceDir).map((rel) => basename(rel)))
|
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. The first
|
|
8
|
+
* Domains the stamp can record. The first two attribute file by file through
|
|
9
9
|
* the 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
|
+
*
|
|
12
|
+
* A stamp written before the standards install channel closed still carries a
|
|
13
|
+
* `standards` record. `isStamp` ignores the key and `sortDomains` drops it on
|
|
14
|
+
* the next write, so the target loses a domain nothing can refresh rather than
|
|
15
|
+
* losing the whole file.
|
|
11
16
|
*/
|
|
12
|
-
export const STAMP_DOMAINS = [
|
|
13
|
-
'standards',
|
|
14
|
-
'snippets',
|
|
15
|
-
'governance',
|
|
16
|
-
'tooling',
|
|
17
|
-
] as const
|
|
17
|
+
export const STAMP_DOMAINS = ['snippets', 'governance', 'tooling'] as const
|
|
18
18
|
|
|
19
19
|
export type StampDomain = (typeof STAMP_DOMAINS)[number]
|
|
20
20
|
|
|
@@ -223,18 +223,21 @@ function sortDomains(
|
|
|
223
223
|
}
|
|
224
224
|
|
|
225
225
|
/**
|
|
226
|
-
* Validates every domain record rather than only the container. A
|
|
227
|
-
* domain value is the wrong shape would otherwise reach
|
|
228
|
-
* bad hash lookup silently reads as a customization.
|
|
226
|
+
* Validates every recognized domain record rather than only the container. A
|
|
227
|
+
* stamp whose domain value is the wrong shape would otherwise reach
|
|
228
|
+
* `attribute`, where a bad hash lookup silently reads as a customization.
|
|
229
|
+
*
|
|
230
|
+
* A key naming no current domain is ignored rather than failing the parse. A
|
|
231
|
+
* target stamped before the standards install channel closed carries one, and
|
|
232
|
+
* rejecting the file would read as unstamped and then drop the three records it
|
|
233
|
+
* does carry on the next write. `sortDomains` retires the key at that write.
|
|
229
234
|
*/
|
|
230
235
|
function isStamp(value: unknown): value is Stamp {
|
|
231
236
|
if (!isRecord(value) || !isRecord(value.domains)) return false
|
|
232
237
|
|
|
233
|
-
return Object.entries(value.domains)
|
|
234
|
-
([domain
|
|
235
|
-
|
|
236
|
-
isDomainStamp(record),
|
|
237
|
-
)
|
|
238
|
+
return Object.entries(value.domains)
|
|
239
|
+
.filter(([domain]) => (STAMP_DOMAINS as readonly string[]).includes(domain))
|
|
240
|
+
.every(([, record]) => isDomainStamp(record))
|
|
238
241
|
}
|
|
239
242
|
|
|
240
243
|
/**
|
package/src/sync/target.ts
CHANGED
|
@@ -2,12 +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 = [
|
|
6
|
-
'standards',
|
|
7
|
-
'snippets',
|
|
8
|
-
'governance',
|
|
9
|
-
'claude',
|
|
10
|
-
] as const
|
|
5
|
+
export const SYNC_DOMAINS = ['snippets', 'governance', 'claude'] as const
|
|
11
6
|
|
|
12
7
|
export type SyncDomain = (typeof SYNC_DOMAINS)[number]
|
|
13
8
|
|
|
@@ -17,7 +12,6 @@ export interface DomainState {
|
|
|
17
12
|
}
|
|
18
13
|
|
|
19
14
|
const DOMAIN_MARKERS: Record<SyncDomain, string> = {
|
|
20
|
-
standards: join('.claude', 'standards'),
|
|
21
15
|
snippets: join('.claude', 'snippets'),
|
|
22
16
|
governance: join('.claude', 'rules'),
|
|
23
17
|
claude: '.claude',
|
|
@@ -30,7 +24,6 @@ const DOMAIN_MARKERS: Record<SyncDomain, string> = {
|
|
|
30
24
|
* because that is the only file `aitk claude sync` writes.
|
|
31
25
|
*/
|
|
32
26
|
const DOMAIN_PATHS: Record<SyncDomain, readonly string[]> = {
|
|
33
|
-
standards: ['.claude/standards/'],
|
|
34
27
|
snippets: ['.claude/snippets/'],
|
|
35
28
|
governance: ['.claude/rules/', '.claude/GOV.md'],
|
|
36
29
|
claude: ['.gitignore'],
|
package/standards/skill.md
CHANGED
|
@@ -231,13 +231,13 @@ A skill reads from two roots. Know which one a file lives under before referenci
|
|
|
231
231
|
|
|
232
232
|
### Citing a standard
|
|
233
233
|
|
|
234
|
-
|
|
234
|
+
No standard installs into a project, so a body cites one place rather than choosing between two.
|
|
235
235
|
|
|
236
|
-
- Cite
|
|
237
|
-
-
|
|
238
|
-
-
|
|
239
|
-
- State the
|
|
240
|
-
- A guard on a standard's presence names the file
|
|
236
|
+
- 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.
|
|
237
|
+
- 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
|
+
- 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.
|
|
239
|
+
- 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.
|
|
240
|
+
- 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.
|
|
241
241
|
- Use `${CLAUDE_SKILL_DIR}`, never a bare `../../` and never `${CLAUDE_PLUGIN_ROOT}`. Only `${CLAUDE_SKILL_DIR}` is expanded before the body reaches the model. The other two leave the model to infer a base path, which it may resolve against the session cwd instead.
|
|
242
242
|
- Cite a shared procedure, never restate it. A procedure two or more skills execute gets one definition in a standard and a citation in each body. Nothing catches a restatement that drifts, because the drift assertion covers generated copies and a hand-written one is not generated, so the guarantee is only that a single definition exists to correct.
|
|
243
243
|
- Keep the trigger in the body and the procedure in the standard. The citing skill states when the procedure runs and what it runs against, since that varies per skill and the standard cannot know it.
|
package/standards/standard.md
CHANGED
|
@@ -38,7 +38,7 @@ A standard failing these questions is non-conforming even when it satisfies ever
|
|
|
38
38
|
|
|
39
39
|
- Govern one document type per standard, or one attribute across every document. Split unrelated conventions into separate files.
|
|
40
40
|
- Open with a `## Scope` section stating what the standard governs and what it does not, placed above the shape rules. A standard that specifies shape exhaustively and jurisdiction nowhere cannot refuse a rule, so the rule with no obvious owner lands in whichever standard sits nearest.
|
|
41
|
-
- Write it as one line naming the artifact or attribute and where it applies, then a `Does not govern:` list. Give each entry the excluded concern and the owner it goes to. Name a sibling standard by bare filename, since
|
|
41
|
+
- Write it as one line naming the artifact or attribute and where it applies, then a `Does not govern:` list. Give each entry the excluded concern and the owner it goes to. Name a sibling standard by bare filename, since the corpus is one flat folder, and name the surface instead where the owner is one, such as a coding rule, a project policy, or the code.
|
|
42
42
|
- Put the governed path in backticks inside the first sentence of that line, anchored deep enough to resolve from a project root. A bare folder name matches a same-named folder elsewhere in the tree, and a path held back until a later sentence sits beside every other path the statement mentions, including the ones it excludes.
|
|
43
43
|
- Say in that same sentence when the standard governs an attribute rather than a document type. An attribute is written across every document and has no path to name, so the silence reads as an omission unless the sentence claims it.
|
|
44
44
|
- Cut an entry that names no owner at all. It is either excluding something nothing was going to claim, or it is a content exclusion, which the rule below sends to the shape rules instead.
|
|
@@ -8,4 +8,4 @@ runtime = ""
|
|
|
8
8
|
scaffold = ""
|
|
9
9
|
|
|
10
10
|
[gitignore]
|
|
11
|
-
"# Claude" = [".claude/.tmp/", ".claude/groundwork/", ".claude/intake/", ".claude/memory/", ".claude/plans/", ".claude/plans-archive/", ".claude/review/", ".claude/review-archive/", ".claude/task-archive/", ".claude/worktrees/", ".claude/tasks/"]
|
|
11
|
+
"# Claude" = [".claude/.records.git/", ".claude/.tmp/", ".claude/groundwork/", ".claude/intake/", ".claude/memory/", ".claude/plans/", ".claude/plans-archive/", ".claude/review/", ".claude/review-archive/", ".claude/task-archive/", ".claude/worktrees/", ".claude/tasks/", ".claude/teach/"]
|
|
@@ -50,7 +50,7 @@ A project installed before the diagram surface became a folder still holds `.cla
|
|
|
50
50
|
|
|
51
51
|
## Gitignore
|
|
52
52
|
|
|
53
|
-
- `# Claude`: `.claude/.tmp/`, `.claude/groundwork/`, `.claude/intake/`, `.claude/memory/`, `.claude/plans/`, `.claude/plans-archive/`, `.claude/review/`, `.claude/review-archive/`, `.claude/task-archive/`, `.claude/worktrees/`, `.claude/tasks/`
|
|
53
|
+
- `# Claude`: `.claude/.records.git/`, `.claude/.tmp/`, `.claude/groundwork/`, `.claude/intake/`, `.claude/memory/`, `.claude/plans/`, `.claude/plans-archive/`, `.claude/review/`, `.claude/review-archive/`, `.claude/task-archive/`, `.claude/worktrees/`, `.claude/tasks/`, `.claude/teach/`
|
|
54
54
|
|
|
55
55
|
## CLI
|
|
56
56
|
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
- Read `CLAUDE_CODE_ENTRYPOINT` once, at the first response that emits a path, and reuse it for the rest of the session. The surface cannot change mid-session, so a second read only confirms the first.
|
|
41
41
|
- When it reads `claude-desktop`, emit each path as a markdown link carrying the path as its text and an absolute `file://` URI as its target, resolving a relative path against the main project root to build that target. The desktop file tree hides dotted folders, so a bare path into one names a file the reader cannot reach.
|
|
42
42
|
- On every other value, including unset, emit the path bare. A terminal emulator makes it clickable through its own path detection, and link markup defeats that.
|
|
43
|
-
- Both forms govern a path emitted in a response. A path written into a markdown file follows
|
|
43
|
+
- Both forms govern a path emitted in a response. A path written into a markdown file follows the markdown standard instead, which your toolkit resolves by name, and which backticks a file reference and never repeats it as a link label.
|
|
44
44
|
- Use the path the user's editor can resolve. The editor is rooted at the main project root.
|
|
45
45
|
- In the main worktree: relative from `pwd` works because `pwd` equals the editor root.
|
|
46
46
|
- In a linked worktree (under `.claude/worktrees/<name>/`): use absolute paths. Relative paths from worktree `pwd` would not resolve against the editor's project root.
|
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
- Write all memory files to `.claude/memory/`, not `~/.claude/projects/`
|
|
79
79
|
- A fact about a domain goes to that domain's `.claude/context/` entry, not to memory. `claude-memory-capture` routes it there and `claude-docs` folds it in. Memory keeps only what no context entry owns.
|
|
80
80
|
- Never delete a memory entry. Retire one by moving it to `.claude/.tmp/memory-archive/`. A bulk retire runs through the shell, where no file edit fires a path-scoped rule, and the folder is gitignored with nothing to recover from.
|
|
81
|
-
- Follow
|
|
81
|
+
- Follow the memory standard, which your toolkit resolves by name, for the filename and type prefix, the frontmatter, the body shape each type carries, and the lifecycle. Check every entry in the pen against that standard and fix what breaks it, since nothing keeps the folder conforming on its own.
|
|
82
82
|
|
|
83
83
|
## Scratch
|
|
84
84
|
|
package/src/standards/adapter.ts
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs'
|
|
2
|
-
import { basename, join } from 'node:path'
|
|
3
|
-
import {
|
|
4
|
-
INDEX_FILE,
|
|
5
|
-
refreshIndex,
|
|
6
|
-
standardsInstallDir,
|
|
7
|
-
} from '@/standards/index-refresh'
|
|
8
|
-
import type { InstalledFile, SyncAdapter } from '@/sync/engine'
|
|
9
|
-
|
|
10
|
-
export function standardsSourceDir(root: string): string {
|
|
11
|
-
return join(root, 'standards')
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Matches installed standards to sources by filename against the flat
|
|
16
|
-
* `standards/` root. A standard that lives in a source subfolder, such as
|
|
17
|
-
* `bundled/`, has no flat sibling and so reads as project-authored, which is
|
|
18
|
-
* what keeps `install` and `sync` agreeing on the same set.
|
|
19
|
-
*
|
|
20
|
-
* Standards are seeds a project is expected to edit, unlike gov rules and
|
|
21
|
-
* snippets, so a headless run refuses to overwrite drift rather than applying
|
|
22
|
-
* it. That refusal is the reason the engine grew a per-adapter policy.
|
|
23
|
-
*/
|
|
24
|
-
export function createStandardsAdapter(root: string): SyncAdapter {
|
|
25
|
-
const sourceDir = standardsSourceDir(root)
|
|
26
|
-
|
|
27
|
-
return {
|
|
28
|
-
banner: 'aitk standards sync',
|
|
29
|
-
label: 'standards',
|
|
30
|
-
missingMessage:
|
|
31
|
-
"No .claude/standards/ found in target. Run 'aitk standards install' first.",
|
|
32
|
-
unit: 'standards',
|
|
33
|
-
installedRoot: standardsInstallDir,
|
|
34
|
-
isExcluded: (file: InstalledFile) => basename(file.path) === INDEX_FILE,
|
|
35
|
-
locateSource: (file: InstalledFile) =>
|
|
36
|
-
locateSource(sourceDir, basename(file.path)),
|
|
37
|
-
nonInteractive: {
|
|
38
|
-
kind: 'refuse',
|
|
39
|
-
message:
|
|
40
|
-
'Drifts detected. Refusing to auto-apply in non-interactive mode.',
|
|
41
|
-
hint: 'Run interactively, or use /claude-seed-sync for per-section audit that preserves customizations.',
|
|
42
|
-
},
|
|
43
|
-
onComplete: (target: string) => refreshIndex(sourceDir, target),
|
|
44
|
-
stamp: { domain: 'standards', toolkitRoot: root },
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function locateSource(sourceDir: string, name: string): string | undefined {
|
|
49
|
-
const source = join(sourceDir, name)
|
|
50
|
-
return existsSync(source) ? source : undefined
|
|
51
|
-
}
|