@erclx/aitk 1.7.1 → 2.1.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 (49) hide show
  1. package/README.md +2 -2
  2. package/claude/.claude-plugin/plugin.json +1 -1
  3. package/claude/skills/claude-autoship/SKILL.md +2 -2
  4. package/claude/skills/claude-docs/SKILL.md +11 -0
  5. package/claude/skills/claude-groundwork/SKILL.md +1 -1
  6. package/claude/skills/claude-seed-sync/REQUIREMENT.md +2 -2
  7. package/claude/skills/claude-seed-sync/SKILL.md +15 -18
  8. package/claude/skills/create-standard/REQUIREMENT.md +7 -10
  9. package/claude/skills/create-standard/SKILL.md +10 -11
  10. package/claude/skills/git-pr/references/labels.md +7 -1
  11. package/claude/skills/migration-standards/REQUIREMENT.md +10 -7
  12. package/claude/skills/migration-standards/SKILL.md +24 -22
  13. package/claude/skills/migration-superseded/REQUIREMENT.md +2 -2
  14. package/claude/skills/migration-superseded/SKILL.md +7 -9
  15. package/claude/skills/setup-gov/REQUIREMENT.md +2 -2
  16. package/claude/skills/toolkit-cli/SKILL.md +13 -14
  17. package/docs/agents/commands.md +1 -1
  18. package/docs/agents/install-and-sync.md +27 -41
  19. package/docs/agents/scripting.md +2 -9
  20. package/docs/target-projects.md +14 -15
  21. package/package.json +1 -1
  22. package/scripts/core/install-check.sh +5 -1
  23. package/scripts/lib/sandbox-git.sh +56 -4
  24. package/scripts/manage-sandbox.sh +4 -7
  25. package/scripts/standards/list.sh +6 -4
  26. package/src/cli.ts +1 -1
  27. package/src/commands/gov.ts +1 -7
  28. package/src/commands/init.ts +1 -4
  29. package/src/commands/standards.ts +9 -141
  30. package/src/commands/sync.ts +1 -2
  31. package/src/gov/install.ts +0 -9
  32. package/src/init/flags.ts +3 -10
  33. package/src/init/plan.ts +8 -26
  34. package/src/init/steps.ts +0 -24
  35. package/src/standards/read.ts +16 -14
  36. package/src/sync/check.ts +4 -7
  37. package/src/sync/layout.ts +8 -10
  38. package/src/sync/stamp.ts +18 -15
  39. package/src/sync/target.ts +1 -8
  40. package/standards/skill.md +6 -6
  41. package/standards/standard.md +1 -1
  42. package/tooling/claude/seeds/.claude/ARCHITECTURE.md +1 -1
  43. package/tooling/claude/seeds/.claude/DESIGN.md +1 -1
  44. package/tooling/claude/seeds/.claude/REQUIREMENTS.md +1 -1
  45. package/tooling/claude/seeds/CLAUDE.md +2 -2
  46. package/src/standards/adapter.ts +0 -51
  47. package/src/standards/closure.ts +0 -200
  48. package/src/standards/index-refresh.ts +0 -44
  49. package/src/standards/install.ts +0 -52
@@ -34,7 +34,6 @@ import {
34
34
  import { describeSkew } from '@/version/skew'
35
35
 
36
36
  const SYNC_ARGS: Record<SyncDomain, readonly string[]> = {
37
- standards: ['standards', 'sync'],
38
37
  snippets: ['snippets', 'sync'],
39
38
  governance: ['gov', 'sync'],
40
39
  claude: ['claude', 'sync'],
@@ -379,7 +378,7 @@ async function runSync(target: string): Promise<number> {
379
378
 
380
379
  if (existsSync(join(resolved, '.claude'))) {
381
380
  process.stderr.write(
382
- `${GREY}Tip: run \`/claude-seed-sync\` to audit seed and standards drift per section, preserving local customizations.${NC}\n`,
381
+ `${GREY}Tip: run \`/claude-seed-sync\` to audit seed drift per section, preserving local customizations.${NC}\n`,
383
382
  )
384
383
  }
385
384
 
@@ -109,12 +109,3 @@ export async function installRules(
109
109
 
110
110
  return installed
111
111
  }
112
-
113
- /**
114
- * The bash checked `<target>/standards`, but `aitk standards install` writes
115
- * `.claude/standards/`, which is also the path the rules reference and the
116
- * path the warning names. The guard therefore fired on every modern install.
117
- */
118
- export function hasStandards(target: string): boolean {
119
- return existsSync(join(target, '.claude', 'standards'))
120
- }
package/src/init/flags.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import type { Command } from 'commander'
2
2
  import { DEFAULT_STACK, SKIPPABLE_DOMAINS } from '@/init/plan'
3
- import { ALL_SELECTION } from '@/standards/closure'
4
3
 
5
4
  interface InitOptionSpec {
6
5
  /** The option value key commander stores the parsed value under. */
@@ -33,12 +32,6 @@ export const INIT_OPTIONS: readonly InitOptionSpec[] = [
33
32
  description: "Snippets preset, category, or 'all'",
34
33
  defaultValue: 'essentials',
35
34
  },
36
- {
37
- key: 'standards',
38
- flags: '--standards <selection>',
39
- description: "Comma-separated standard names, or 'all'",
40
- defaultValue: ALL_SELECTION,
41
- },
42
35
  {
43
36
  key: 'skip',
44
37
  flags: '--skip <list>',
@@ -62,9 +55,9 @@ export function applyInitOptions(command: Command): Command {
62
55
 
63
56
  /**
64
57
  * Whether the operator passed any flag, which is what makes the command
65
- * scriptable by suppressing the confirmation prompt. `--stack`, `--snippets`,
66
- * and `--standards` all carry defaults, so presence has to be read from where
67
- * the value came from rather than from the value itself.
58
+ * scriptable by suppressing the confirmation prompt. `--stack` and `--snippets`
59
+ * both carry defaults, so presence has to be read from where the value came
60
+ * from rather than from the value itself.
68
61
  */
69
62
  export function flagsProvided(cmd: Command): boolean {
70
63
  return INIT_OPTIONS.some(
package/src/init/plan.ts CHANGED
@@ -1,13 +1,11 @@
1
- import { ALL_SELECTION } from '@/standards/closure'
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
- * the standards that install alongside it arrive with the rules that route to
10
- * them, and `--skip governance` is the one spelling for declining.
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
- * Governance is the one skip that costs another domain something, so its
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 notes: string[] = []
95
- if (flags.add !== undefined && flags.add !== '')
96
- notes.push(`--add ${flags.add} not installed`)
97
- if (!flags.skip.skipped.has('standards'))
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
@@ -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
- import { standardsSourceDir } from '@/standards/adapter'
6
- import { INDEX_FILE } from '@/standards/index-refresh'
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 three roots a standard resolves against, in precedence order.
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
- * A project copy wins wherever one exists. An installed standard is a seed a
33
- * project edits, so a package copy overriding it would discard that edit with
34
- * nothing said, and the fallback answers absence rather than staleness. The
35
- * package root is what lets a project that installed no standards still be
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
- * standards measures from, silently dropping a standards change out of the read.
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,
@@ -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 `standards/` of project docs and never have installed the
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 standards install flat while
111
- * snippets nest by category, and the question here is only whether any file is
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 three attribute file by file through
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 stamp whose
227
- * domain value is the wrong shape would otherwise reach `attribute`, where a
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).every(
234
- ([domain, record]) =>
235
- (STAMP_DOMAINS as readonly string[]).includes(domain) &&
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
  /**
@@ -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'],
@@ -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
- A standard reaches a skill by two routes, and a body that names only the first breaks in a project that installed the plugin without running `aitk standards install`.
234
+ No standard installs into a project, so a body cites one place rather than choosing between two.
235
235
 
236
- - Cite `.claude/standards/X.md` first, then name `${CLAUDE_SKILL_DIR}/../../standards/X.md` as the fallback. The plugin ships the whole standards folder beside `skills/`, so the second path resolves in every install.
237
- - The project copy wins when it exists, which keeps a target's local edits authoritative. The fallback only covers the case where the project lacks that file.
238
- - Condition the fallback on the standard, never on the `.claude/standards/` directory. `aitk standards sync` updates only filenames it already finds and never adds one, so a project that installed before a standard existed keeps the directory and never receives that file. A directory test passes there, no fallback engages, and the standard reads as absent.
239
- - State the fallback 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 fallback at every mention is noise rather than instruction.
240
- - A guard on a standard's presence names the file and tests both paths before it stops. A guard that tests only `.claude/standards/` refuses to run in a plugin-only project that has the file, and a guard that tests the directory passes in the partial-install case it exists to catch.
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.
@@ -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 standards install as siblings, and name the surface instead where the owner is one, such as a coding rule, a project policy, or the code.
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.
@@ -1,6 +1,6 @@
1
1
  # Architecture
2
2
 
3
- Authoring guidance: `.claude/standards/architecture.md`.
3
+ Authoring guidance: the architecture standard.
4
4
 
5
5
  ## Overview
6
6
 
@@ -1,6 +1,6 @@
1
1
  # Design
2
2
 
3
- Authoring guidance: `.claude/standards/design.md`.
3
+ Authoring guidance: the design standard.
4
4
 
5
5
  ## Personality
6
6
 
@@ -1,6 +1,6 @@
1
1
  # Requirements
2
2
 
3
- Authoring guidance: `.claude/standards/requirements.md`.
3
+ Authoring guidance: the requirements standard.
4
4
 
5
5
  ## Problem
6
6
 
@@ -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 `.claude/standards/markdown.md` instead, which backticks a file reference and never repeats it as a link label.
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 `.claude/standards/memory.md` 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.
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
 
@@ -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
- }