@erclx/aitk 3.20.0 → 3.22.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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "aitk",
3
3
  "description": "Automated governance, versioning, and discovery tools for Claude Code.",
4
- "version": "3.20.0",
4
+ "version": "3.22.0",
5
5
  "author": {
6
6
  "name": "Eric Le",
7
7
  "url": "https://github.com/erclx"
@@ -107,13 +107,13 @@ AITK_NON_INTERACTIVE=1 aitk tooling sync vite-react ./frontend --skip base --wri
107
107
  AITK_NON_INTERACTIVE=1 aitk tooling sync python ./backend --skip base --write
108
108
  ```
109
109
 
110
- Without `--skip base`, each subtree re-drops husky, and git honors only one `core.hooksPath`, so the extra hook dirs silently break. Each subtree keeps its own framework configs and its own `.claude/tooling/<stack>.md` audit docs.
110
+ Without `--skip base`, each subtree re-drops husky, and git honors only one `core.hooksPath`, so the extra hook dirs silently break. Each subtree keeps its own framework configs, and its own stack reference reads through `aitk tooling reference <stack>`.
111
111
 
112
112
  Step 3: post-sync fixups. Golden configs arrive from sync, so no config generation is required. But a few items may need a one-time touch:
113
113
 
114
114
  - **ESLint version pin.** If `bun create vite` installed `eslint@^10` and the manifest pins `eslint@^9`, sync does not override a present dep. Run `bun add -d eslint@^9` if `bun run lint:fix` fails with `Class extends value undefined`.
115
115
  - **File naming.** `bun create vite`'s `App.tsx` violates the `KEBAB_CASE` rule. Rename to `app.tsx` and update the import in `main.tsx`.
116
- - **Docs.** Open `<target>/.claude/tooling/<tooling-stack>.md` and `<target>/.claude/tooling/web.md` for any stack-specific follow-ups (Chrome extension overrides, setup script details).
116
+ - **Docs.** Run `aitk tooling reference <tooling-stack>` and `aitk tooling reference web` for any stack-specific follow-ups (Chrome extension overrides, setup script details).
117
117
 
118
118
  Do not generate ESLint, Vitest, or Playwright configs. They ship as golden files. Generating from prose duplicates what sync already installed.
119
119
 
@@ -9,17 +9,17 @@ What each `aitk` sync or install command does to existing files in a target proj
9
9
 
10
10
  ## Overwrite contract
11
11
 
12
- | Surface | Command | Effect on existing files |
13
- | -------------------------------------------------------- | ------------------- | ----------------------------------------------------------------- |
14
- | Golden configs, listed in full below | `aitk tooling sync` | Overwritten once `--write` is passed. Local edits are lost. |
15
- | Dictionary seeds (`.cspell/*.txt`) | `aitk tooling sync` | Merged and sorted. Existing terms preserved. |
16
- | Other seeds (`cspell.json`, `.lintstagedrc`, state docs) | `aitk tooling sync` | Copy-once. Dropped on first install, untouched after. |
17
- | Standards | none | Nothing installs. `aitk standards <name>` reads and never writes. |
18
- | Seed docs and `CLAUDE.md` | `aitk claude init` | Skipped when present. Never overwritten. |
19
- | Seed docs | `aitk claude sync` | Never touched. Only `.gitignore` is written. |
20
- | References (`.claude/tooling/<stack>.md`) | `aitk tooling ref` | Overwritten. |
21
- | `.gitignore`, deps, scripts | any sync | Additive. Existing entries preserved. Deps re-pin on major skew. |
22
- | Generated `index.md` | any sync or regen | Rewritten from target state. Hand edits are lost. |
12
+ | Surface | Command | Effect on existing files |
13
+ | -------------------------------------------------------- | ------------------- | -------------------------------------------------------------------------- |
14
+ | Golden configs, listed in full below | `aitk tooling sync` | Overwritten once `--write` is passed. Local edits are lost. |
15
+ | Dictionary seeds (`.cspell/*.txt`) | `aitk tooling sync` | Merged and sorted. Existing terms preserved. |
16
+ | Other seeds (`cspell.json`, `.lintstagedrc`, state docs) | `aitk tooling sync` | Copy-once. Dropped on first install, untouched after. |
17
+ | Standards | none | Nothing installs. `aitk standards <name>` reads and never writes. |
18
+ | Seed docs and `CLAUDE.md` | `aitk claude init` | Skipped when present. Never overwritten. |
19
+ | Seed docs | `aitk claude sync` | Never touched. Only `.gitignore` is written. |
20
+ | Stack references | none | Nothing installs. `aitk tooling reference <stack>` reads and never writes. |
21
+ | `.gitignore`, deps, scripts | any sync | Additive. Existing entries preserved. Deps re-pin on major skew. |
22
+ | Generated `index.md` | any sync or regen | Rewritten from target state. Hand edits are lost. |
23
23
 
24
24
  ## What a tooling sync can overwrite
25
25
 
@@ -45,8 +45,8 @@ Two steps, in order:
45
45
  The chain is:
46
46
 
47
47
  - `aitk init` installs base tooling, Claude seeds, and governance rules into `.claude/rules/` in the same pass
48
- - `aitk tooling sync <stack> --write` adds stack-specific deps, scripts, gitignore entries, and drops `.claude/tooling/<stack>.md` (plus parents) as the agent's audit context
49
- - The agent follows the reference to generate eslint, vitest, playwright configs and the stack's setup script, and extends `.claude/context/ci.md` and `.claude/context/development.md` per the reference's extend sections <!-- audit-ignore-citations -->
48
+ - `aitk tooling sync <stack> --write` adds stack-specific deps, scripts, and gitignore entries
49
+ - The agent reads `aitk tooling reference <stack>` (plus parents) as its audit context, follows it to generate eslint, vitest, playwright configs and the stack's setup script, and extends `.claude/context/ci.md` and `.claude/context/development.md` per the reference's extend sections <!-- audit-ignore-citations -->
50
50
  - `setup-verify` runs the installed `package.json` scripts (lint, typecheck, check, test, build) and reports pass or fail
51
51
 
52
52
  The chain serves a fresh scaffold and names a destination for the three states it does not. An existing project goes to `aitk:toolkit-operator`, which reads what the project already carries before naming a per-domain command. An install wanting the Claude layer without the tooling chain runs `aitk claude init` for the seed docs and then `aitk:setup-indexes` for the index system. A language the toolkit ships no stack for is the one of the three the chain still runs for, on `base`, with the fallback marked in the preview so it can be declined there.
@@ -175,7 +175,7 @@ Reconcile the configs with `aitk tooling sync <stack> <path> --check` to read wh
175
175
 
176
176
  `aitk sync <path>` runs every installed domain's sync in sequence. Safe to run on a cadence.
177
177
 
178
- It never touches user-owned seed files. Governance rules in `.claude/rules/`, tooling configs, and reference docs refresh in place. Stale `.claude/GOV.md` from earlier installs is removed.
178
+ It never touches user-owned seed files. Governance rules in `.claude/rules/` and tooling configs refresh in place. Stale `.claude/GOV.md` from earlier installs is removed.
179
179
 
180
180
  Standards take no part in that run. Nothing installed them, so there is no copy to reconcile and no `aitk standards sync` to reach for.
181
181
 
@@ -184,7 +184,7 @@ Standards take no part in that run. Nothing installed them, so there is no copy
184
184
  - Claude seed docs such as `CLAUDE.md` and `.claude/REQUIREMENTS.md`: invoke `aitk:claude-seed-sync`. The skill splits each file into a preamble (between the H1 and the first H2) plus one part per `##` section, then diffs part by part and proposes per-part edits. User customizations are preserved.
185
185
  - Governance rules already installed: `aitk gov sync <path>` diffs and applies, and never adds new rules. A rule your recorded stack lists reports as `missing` instead.
186
186
  - Tooling configs and seeds: `aitk tooling <stack> <path>` overwrites golden configs and merges seeds
187
- - Reference docs for a stack: `aitk tooling ref <stack> <path>`
187
+ - Reference docs for a stack: `aitk tooling reference <stack>` reads and never writes, so there is nothing to sync
188
188
  - Index regeneration after markdown edits: `aitk indexes regen`
189
189
 
190
190
  Use a targeted entry point when only one surface moved upstream. Use the catch-all when the toolkit lands a bundled release.
@@ -233,7 +233,7 @@ aitk tooling sync vite-react ./frontend --skip base --write
233
233
  aitk tooling sync python ./backend --skip base --write
234
234
  ```
235
235
 
236
- `--skip base` drops the `base` layer from each subtree sync, so husky, prettier, cspell, commitlint, and CI stay single at the repo root. Without it, every subtree re-drops husky, and since git honors only one `core.hooksPath` the extra hook dirs silently break. Each subtree still gets its own framework configs (eslint, vitest, tsconfig, vite) and its own `.claude/tooling/<stack>.md` audit docs.
236
+ `--skip base` drops the `base` layer from each subtree sync, so husky, prettier, cspell, commitlint, and CI stay single at the repo root. Without it, every subtree re-drops husky, and since git honors only one `core.hooksPath` the extra hook dirs silently break. Each subtree still gets its own framework configs (eslint, vitest, tsconfig, vite), and its own stack reference reads through `aitk tooling reference <stack>`.
237
237
 
238
238
  ## Running sync from an agent session
239
239
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@erclx/aitk",
3
3
  "type": "module",
4
- "version": "3.20.0",
4
+ "version": "3.22.0",
5
5
  "description": "Infrastructure and quality tooling for developer workflows",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -3,7 +3,6 @@ import type { Command } from 'commander'
3
3
  import { execScript } from '@/exec'
4
4
  import { PROJECT_ROOT } from '@/project-root'
5
5
  import {
6
- applyReferences,
7
6
  injectConfigs,
8
7
  injectGitignore,
9
8
  injectManifest,
@@ -18,12 +17,14 @@ import {
18
17
  resolveChain,
19
18
  stackExists,
20
19
  } from '@/tooling/manifest'
20
+ import { readReference, resolveReference } from '@/tooling/read'
21
21
  import { scan, type ScanResult } from '@/tooling/scan'
22
22
  import { recordToolingChain } from '@/tooling/stamp'
23
23
  import {
24
24
  intro,
25
25
  isNonInteractive,
26
26
  logAdd,
27
+ logError,
27
28
  logInfo,
28
29
  logStep,
29
30
  logWarn,
@@ -32,10 +33,9 @@ import {
32
33
  select,
33
34
  } from '@/ui'
34
35
 
35
- const PASS_THROUGH_VERBS = ['ref', 'create', 'verify'] as const
36
+ const PASS_THROUGH_VERBS = ['create', 'verify'] as const
36
37
 
37
38
  interface SyncOptions {
38
- readonly ref?: boolean
39
39
  readonly skip?: string
40
40
  readonly check?: boolean
41
41
  readonly write?: boolean
@@ -64,7 +64,7 @@ type Prepared =
64
64
  export function register(program: Command): void {
65
65
  const tooling = program
66
66
  .command('tooling')
67
- .description('Manage tooling stacks (sync, ref, create)')
67
+ .description('Manage tooling stacks (sync, reference, create)')
68
68
  .helpOption('-h, --help', 'Show this help message')
69
69
 
70
70
  tooling
@@ -73,7 +73,6 @@ export function register(program: Command): void {
73
73
  .argument('[stack]', 'Tooling stack name (e.g. base, vite-react)')
74
74
  .argument('[target]', 'Target directory', '.')
75
75
  .helpOption('-h, --help', 'Show this help message')
76
- .option('--no-ref', 'Skip dropping reference docs')
77
76
  .option('--skip <stack>', 'Drop a layer from the extends chain')
78
77
  .option('--check', 'Report what would change and write nothing')
79
78
  .option('--write', 'Apply every change without prompting')
@@ -134,6 +133,29 @@ export function register(program: Command): void {
134
133
  process.exitCode = runList(opts)
135
134
  })
136
135
 
136
+ tooling
137
+ .command('reference')
138
+ .description("Print a stack's reference doc")
139
+ .argument('<stack>', 'Tooling stack name (e.g. base, vite-react)')
140
+ .helpOption('-h, --help', 'Show this help message')
141
+ .addHelpText(
142
+ 'after',
143
+ [
144
+ '',
145
+ 'A stack resolves under tooling/ at the working root, then the corpus',
146
+ 'inside the aitk package. No reference installs into a project, so the',
147
+ 'package corpus is what answers there. The frame names the copy it read.',
148
+ '',
149
+ 'Examples:',
150
+ ' aitk tooling reference base',
151
+ ' aitk tooling reference vite-react',
152
+ '',
153
+ ].join('\n'),
154
+ )
155
+ .action((stack: string) => {
156
+ process.exitCode = printReference(stack)
157
+ })
158
+
137
159
  for (const verb of PASS_THROUGH_VERBS) {
138
160
  tooling
139
161
  .command(verb)
@@ -172,6 +194,32 @@ function runList(opts: ListOptions): number {
172
194
  return 0
173
195
  }
174
196
 
197
+ /**
198
+ * Writes the reference to stdout and every frame line to stderr, so a caller
199
+ * capturing the output with `$(...)` receives the document alone. Mirrors
200
+ * `print` in `src/commands/standards.ts`.
201
+ */
202
+ function printReference(stack: string): number {
203
+ intro('aitk tooling reference')
204
+
205
+ const root = process.cwd()
206
+ const resolved = resolveReference(root, stack)
207
+
208
+ if (!resolved) {
209
+ logWarn(`Unknown stack: ${stack}`)
210
+ logStep('Available stacks')
211
+ for (const each of listStacks(PROJECT_ROOT)) logInfo(each)
212
+ logError("Run 'aitk tooling list' for descriptions.")
213
+ outro()
214
+ return 1
215
+ }
216
+
217
+ logStep(resolved.source)
218
+ process.stdout.write(readReference(resolved))
219
+ outro()
220
+ return 0
221
+ }
222
+
175
223
  /**
176
224
  * Resolves the chain and rejects the same inputs the bash rejected, so a bad
177
225
  * stack name fails before anything touches the target.
@@ -248,10 +296,9 @@ async function runSync(
248
296
  return 1
249
297
  }
250
298
 
251
- const includeReferences = opts.ref !== false
252
- const result = scan(prepared.chain, prepared.target, { includeReferences })
299
+ const result = scan(prepared.chain, prepared.target)
253
300
 
254
- report(result, includeReferences)
301
+ report(result)
255
302
 
256
303
  const mode = resolveWriteMode(opts)
257
304
  const { GREEN, NC } = palette(process.stderr)
@@ -290,15 +337,6 @@ async function runSync(
290
337
  await injectSeeds(prepared.chain, prepared.target)
291
338
  await injectManifest(prepared.chain, prepared.target)
292
339
 
293
- const pending = result.references
294
- .filter((entry) => entry.state === 'pending')
295
- .map((entry) => entry.stack)
296
-
297
- if (pending.length > 0) {
298
- logStep('Applying references')
299
- await applyReferences(prepared.chain, prepared.target, pending)
300
- }
301
-
302
340
  await stampChain(prepared.chain, prepared.target)
303
341
 
304
342
  outro()
@@ -448,7 +486,7 @@ async function promptForStack(): Promise<string | undefined> {
448
486
  })
449
487
  }
450
488
 
451
- function report(result: ScanResult, includeReferences: boolean): void {
489
+ function report(result: ScanResult): void {
452
490
  logStep('Scanning configs')
453
491
  for (const entry of result.configs) {
454
492
  if (entry.state === 'matching') logInfo(entry.rel)
@@ -483,16 +521,6 @@ function report(result: ScanResult, includeReferences: boolean): void {
483
521
  for (const entry of result.gitignore) {
484
522
  if (entry.state === 'missing') logAdd(entry.entry)
485
523
  }
486
-
487
- if (!includeReferences) return
488
-
489
- logStep('Scanning references')
490
- for (const entry of result.references) {
491
- if (entry.state === 'matching') logInfo(`.claude/tooling/${entry.stack}.md`)
492
- }
493
- for (const entry of result.references) {
494
- if (entry.state === 'pending') logAdd(`.claude/tooling/${entry.stack}.md`)
495
- }
496
524
  }
497
525
 
498
526
  function reportPackage(result: ScanResult): void {
@@ -540,10 +568,6 @@ function summarize(result: ScanResult): string {
540
568
  result.gitignore.filter((entry) => entry.state === 'missing').length,
541
569
  'gitignore',
542
570
  )
543
- add(
544
- result.references.filter((entry) => entry.state === 'pending').length,
545
- 'refs',
546
- )
547
571
 
548
572
  return parts.join(', ')
549
573
  }
package/src/sync/check.ts CHANGED
@@ -103,7 +103,6 @@ export interface ToolingCounts {
103
103
  readonly scripts: number
104
104
  readonly deps: number
105
105
  readonly gitignore: number
106
- readonly references: number
107
106
  }
108
107
 
109
108
  /**
@@ -134,7 +133,6 @@ const UNMEASURED_TOOLING: ToolingReport = {
134
133
  scripts: 0,
135
134
  deps: 0,
136
135
  gitignore: 0,
137
- references: 0,
138
136
  },
139
137
  changes: 0,
140
138
  }
@@ -219,7 +217,7 @@ export function buildToolingReport(
219
217
 
220
218
  if (manifests.length === 0) return { ...UNMEASURED_TOOLING, chain }
221
219
 
222
- const result = scan(manifests, target, { includeReferences: true })
220
+ const result = scan(manifests, target)
223
221
  const record = stamp?.domains.tooling
224
222
 
225
223
  return {
@@ -236,8 +234,6 @@ export function buildToolingReport(
236
234
  deps: result.deps.filter((entry) => entry.state === 'missing').length,
237
235
  gitignore: result.gitignore.filter((entry) => entry.state === 'missing')
238
236
  .length,
239
- references: result.references.filter((entry) => entry.state === 'pending')
240
- .length,
241
237
  },
242
238
  changes: result.totalChanges,
243
239
  }
@@ -1,12 +1,5 @@
1
1
  import { existsSync } from 'node:fs'
2
- import {
3
- copyFile,
4
- mkdir,
5
- readFile,
6
- rm,
7
- rmdir,
8
- writeFile,
9
- } from 'node:fs/promises'
2
+ import { copyFile, mkdir, readFile, writeFile } from 'node:fs/promises'
10
3
  import { dirname, join } from 'node:path'
11
4
  import { $ } from 'bun'
12
5
  import { copyPreservingMode } from '@/copy'
@@ -198,33 +191,3 @@ export async function injectManifest(
198
191
 
199
192
  await injectGitignore(chain, target)
200
193
  }
201
-
202
- /**
203
- * Drops reference docs into `.claude/tooling/`, clearing the legacy
204
- * `tooling/` location the earlier layout used.
205
- */
206
- export async function applyReferences(
207
- chain: readonly Manifest[],
208
- target: string,
209
- stacks: readonly string[],
210
- ): Promise<string[]> {
211
- const destDir = join(target, '.claude', 'tooling')
212
- await mkdir(destDir, { recursive: true })
213
-
214
- const applied: string[] = []
215
- const byName = new Map(chain.map((manifest) => [manifest.name, manifest]))
216
-
217
- for (const stack of stacks) {
218
- const manifest = byName.get(stack)
219
- if (!manifest) continue
220
-
221
- await copyFile(manifest.referenceFile, join(destDir, `${stack}.md`))
222
- logAdd(`.claude/tooling/${stack}.md`)
223
- applied.push(stack)
224
- await rm(join(target, 'tooling', `${stack}.md`), { force: true })
225
- }
226
-
227
- await rmdir(join(target, 'tooling')).catch(() => {})
228
-
229
- return applied
230
- }
@@ -14,7 +14,6 @@ export interface Manifest {
14
14
  readonly dir: string
15
15
  readonly configsDir: string
16
16
  readonly seedsDir: string
17
- readonly referenceFile: string
18
17
  readonly scripts: Readonly<Record<string, string>>
19
18
  readonly scriptOverrides: Readonly<Record<string, string>>
20
19
  readonly gitignore: readonly GitignoreSection[]
@@ -100,7 +99,6 @@ export function loadManifest(
100
99
  dir,
101
100
  configsDir: join(dir, 'configs'),
102
101
  seedsDir: join(dir, 'seeds'),
103
- referenceFile: join(dir, 'reference.md'),
104
102
  scripts: pickStrings(scriptsTable),
105
103
  scriptOverrides: pickStrings(asTable(scriptsTable.override)),
106
104
  gitignore: readGitignoreSections(asTable(parsed.gitignore)),
@@ -0,0 +1,65 @@
1
+ import { existsSync, readFileSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+ import { PROJECT_ROOT } from '@/project-root'
4
+
5
+ /**
6
+ * Spells the package root in a report where every other root spells a
7
+ * project-relative path. The `source` field promises a path inside the project
8
+ * and a package copy is the one source that promise cannot cover, so the
9
+ * spelling has to be one nothing will join to a project root.
10
+ */
11
+ const PACKAGE_LABEL = '<aitk>'
12
+
13
+ export interface ReferenceRoot {
14
+ /** Absolute `tooling/` directory to search. */
15
+ readonly dir: string
16
+ /** How a report spells a copy found under this root. */
17
+ readonly label: string
18
+ }
19
+
20
+ export interface ResolvedReference {
21
+ readonly path: string
22
+ /** The label of the root that won, joined to the stack and filename. */
23
+ readonly source: string
24
+ }
25
+
26
+ function toolingSourceDir(root: string): string {
27
+ return join(root, 'tooling')
28
+ }
29
+
30
+ /**
31
+ * The roots a stack reference resolves against, mirroring `standardRoots` in
32
+ * `src/standards/read.ts`. No stack reference installs into a project any
33
+ * more, so the package corpus is the only root that answers there. The
34
+ * authoring root stays ahead of it for the toolkit's own repository, which
35
+ * authors the corpus this reads, and for a project authoring stacks of its
36
+ * own under the same layout.
37
+ */
38
+ export function referenceRoots(root: string): ReferenceRoot[] {
39
+ return [
40
+ { dir: toolingSourceDir(root), label: 'tooling' },
41
+ {
42
+ dir: toolingSourceDir(PROJECT_ROOT),
43
+ label: join(PACKAGE_LABEL, 'tooling'),
44
+ },
45
+ ]
46
+ }
47
+
48
+ /** Resolves a stack's reference doc, working root first. */
49
+ export function resolveReference(
50
+ root: string,
51
+ stack: string,
52
+ ): ResolvedReference | undefined {
53
+ for (const { dir, label } of referenceRoots(root)) {
54
+ const path = join(dir, stack, 'reference.md')
55
+ if (existsSync(path)) {
56
+ return { path, source: join(label, stack, 'reference.md') }
57
+ }
58
+ }
59
+
60
+ return undefined
61
+ }
62
+
63
+ export function readReference(reference: ResolvedReference): string {
64
+ return readFileSync(reference.path, 'utf8')
65
+ }
@@ -26,26 +26,16 @@ export interface EntryState {
26
26
  readonly state: 'missing' | 'present'
27
27
  }
28
28
 
29
- export interface ReferenceState {
30
- readonly stack: string
31
- readonly state: 'pending' | 'matching'
32
- }
33
-
34
29
  export interface ScanResult {
35
30
  readonly configs: readonly ConfigState[]
36
31
  readonly seeds: readonly SeedState[]
37
32
  readonly scripts: readonly ScriptState[]
38
33
  readonly deps: readonly DepState[]
39
34
  readonly gitignore: readonly EntryState[]
40
- readonly references: readonly ReferenceState[]
41
35
  readonly hasPackageJson: boolean
42
36
  readonly totalChanges: number
43
37
  }
44
38
 
45
- export interface ScanOptions {
46
- readonly includeReferences: boolean
47
- }
48
-
49
39
  function isIdentical(a: string, b: string): boolean {
50
40
  if (!existsSync(b)) return false
51
41
  return readFileSync(a).equals(readFileSync(b))
@@ -55,14 +45,10 @@ function isIdentical(a: string, b: string): boolean {
55
45
  * Compares every stack in the chain against the target and reports what would
56
46
  * change. Nothing is written. Which stack wins a duplicate differs per
57
47
  * category and mirrors the bash: configs, seeds, and scripts resolve nearest
58
- * stack first, while dependencies, gitignore entries, and references resolve
59
- * from the furthest ancestor inward.
48
+ * stack first, while dependencies and gitignore entries resolve from the
49
+ * furthest ancestor inward.
60
50
  */
61
- export function scan(
62
- chain: readonly Manifest[],
63
- target: string,
64
- options: ScanOptions,
65
- ): ScanResult {
51
+ export function scan(chain: readonly Manifest[], target: string): ScanResult {
66
52
  const configs: ConfigState[] = []
67
53
  const seenConfigs = new Set<string>()
68
54
 
@@ -109,17 +95,12 @@ export function scan(
109
95
  : ''
110
96
  const gitignore = scanGitignore(chain, gitignoreContent)
111
97
 
112
- const references = options.includeReferences
113
- ? scanReferences(chain, target)
114
- : []
115
-
116
98
  const totalChanges =
117
99
  configs.filter((entry) => entry.state !== 'matching').length +
118
100
  seeds.filter((entry) => entry.state === 'missing').length +
119
101
  scripts.filter((entry) => entry.state !== 'matching').length +
120
102
  deps.filter((entry) => entry.state === 'missing').length +
121
- gitignore.filter((entry) => entry.state === 'missing').length +
122
- references.filter((entry) => entry.state === 'pending').length
103
+ gitignore.filter((entry) => entry.state === 'missing').length
123
104
 
124
105
  return {
125
106
  configs,
@@ -127,7 +108,6 @@ export function scan(
127
108
  scripts,
128
109
  deps,
129
110
  gitignore,
130
- references,
131
111
  hasPackageJson: pkg !== undefined,
132
112
  totalChanges,
133
113
  }
@@ -165,22 +145,3 @@ function scanGitignore(
165
145
 
166
146
  return states
167
147
  }
168
-
169
- function scanReferences(
170
- chain: readonly Manifest[],
171
- target: string,
172
- ): ReferenceState[] {
173
- const states: ReferenceState[] = []
174
-
175
- for (const manifest of ancestorsFirst(chain)) {
176
- if (!existsSync(manifest.referenceFile)) continue
177
-
178
- const dest = join(target, '.claude', 'tooling', `${manifest.name}.md`)
179
- states.push({
180
- stack: manifest.name,
181
- state: isIdentical(manifest.referenceFile, dest) ? 'matching' : 'pending',
182
- })
183
- }
184
-
185
- return states
186
- }
package/standards/plan.md CHANGED
@@ -123,7 +123,7 @@ This contract inverts the one an intake folder keeps, where an empty slot means
123
123
  - Write the plan before implementation starts, and treat it as the scope of the run that executes it.
124
124
  - Keep every plan at one root. A plan copied into each parallel working tree forks, and the copies answer the same question differently.
125
125
  - Amend the plan in place when a decision changes mid-flight. Do not append a second passage narrating the change, which leaves a reader to work out which of two answers is current. An execution-time deviation from a suggestion is one such amendment, and the contract above fixes which line takes it.
126
- - Move the plan to `.claude/plans/archive/` when the work it describes ships. Never delete it, because the plan is where the rejected alternative is written down and nothing else records it.
126
+ - Move the plan to `.claude/plans/archive/` when the work it describes ships. Never delete it, because the plan is where the rejected alternative is written down and nothing else records it. The archive nests inside `.claude/plans/` rather than sitting beside it as `.claude/plans-archive/`, so an older layout is a flat sibling by that name and a current one is not.
127
127
  - Write the plan in the same session that opens the task it serves. The session executing it later inherits reasoning it would otherwise re-derive.
128
128
 
129
129
  ## Anti-patterns
@@ -217,7 +217,7 @@ Phase-label format and where labels may appear are governed by `standards/versio
217
217
 
218
218
  `Plan:` points at `../plans/feature-<slug>.md` while the task is open. Once the task ships and the plan is archived, it points at `../plans/archive/feature-<slug>.md`, and at `../../plans/archive/feature-<slug>.md` once the task itself is archived a folder deeper. Retarget both halves of the link rather than dropping it, so a completed task still leads to the reasoning behind it.
219
219
 
220
- A project that archived plans before the folder nested under `.claude/plans/` holds closed tasks pointing at `../plans-archive/`, or at `../.tmp/plans-archive/` from before the durable records left the scratch tree. Each form resolves against the files it names, so leave those pointers where they are. Nothing migrates them, and a task retargeted without its plan moving leads nowhere.
220
+ A project that archived plans before the folder nested under `.claude/plans/` holds closed tasks pointing at `../plans-archive/`, or at `../.tmp/plans-archive/` from before the durable records left the scratch tree. Each form resolves against the files it names, so leave those pointers where they are. A named route now moves the folder and retargets its pointers together, but no automation runs it, so an unmigrated project keeps holding the old spelling until someone does, and a task retargeted without its plan moving leads nowhere.
221
221
 
222
222
  One plan per task. A plan cited by two tasks is a misfile rather than a shape to design for, which is why the sweep counts citations before archiving: the count is a guard against the misfile stranding a pointer, not support for the shape.
223
223
 
@@ -249,6 +249,8 @@ The line is what lets a merge close its own task. Every merge on `main` is a squ
249
249
 
250
250
  Never delete a task file. A shipped task moves to `.claude/tasks/archive/` under its own name, and the live index regenerates without it. `aitk tasks archive` owns the move, the ordering-row removal, and the index regen as one unit.
251
251
 
252
+ The archive nests inside `.claude/tasks/` rather than sitting beside it as a flat `.claude/task-archive/`. Nesting is what lets a reader tell the two shapes apart on sight: the flat sibling is what a binary predating this convention still writes, so meeting one names an older checkout rather than a second archive to reconcile against this one.
253
+
252
254
  Two callers reach that command. The `claude-tasks` skill runs it inside a session, and the `post-merge` hook runs it unattended after a pull that merged the work. Both go through the command rather than moving the file themselves, so the two paths cannot drift into archiving differently. Every gate the command applies refuses with a non-zero exit rather than reporting, because a caller with nobody watching cannot act on a warning.
253
255
 
254
256
  One destination rather than a per-project choice is what lets the move happen without asking. It mirrors the plans archive at `.claude/plans/archive/`, sitting inside the folder it archives the same way, and it inherits the board's own ignore entry rather than needing one of its own. The cost is that an archived task does not appear in diffs, which is the cost the live board already carries.
@@ -1,147 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -e
3
- set -o pipefail
4
-
5
- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6
- PROJECT_ROOT="${PROJECT_ROOT:-$(dirname "$(dirname "$SCRIPT_DIR")")}"
7
-
8
- source "$PROJECT_ROOT/scripts/lib/ui.sh"
9
- source "$PROJECT_ROOT/scripts/lib/tooling.sh"
10
- trap close_timeline EXIT
11
-
12
- show_help() {
13
- echo -e "${GREY}┌${NC}"
14
- log_step "Tooling ref usage"
15
- echo -e "${GREY}│${NC} ${WHITE}Usage:${NC} aitk tooling ref [stack] [target-path]"
16
- echo -e "${GREY}│${NC}"
17
- echo -e "${GREY}│${NC} Drops reference docs only. No config or dependency changes."
18
- echo -e "${GREY}│${NC}"
19
- echo -e "${GREY}│${NC} ${WHITE}Arguments:${NC}"
20
- echo -e "${GREY}│${NC} stack Name of the tooling stack (e.g., base, vite-react)"
21
- echo -e "${GREY}│${NC} target-path Target directory (default: current directory)"
22
- echo -e "${GREY}│${NC}"
23
- echo -e "${GREY}│${NC} ${WHITE}Options:${NC}"
24
- echo -e "${GREY}│${NC} -h, --help ${GREY}# Show this help message${NC}"
25
- echo -e "${GREY}└${NC}"
26
- exit 0
27
- }
28
-
29
- select_stack() {
30
- local stacks=()
31
- mapfile -t stacks < <(list_tooling_stacks)
32
-
33
- if [ ${#stacks[@]} -eq 0 ]; then
34
- log_error "No tooling stacks found in $PROJECT_ROOT/tooling"
35
- fi
36
-
37
- select_option "Select tooling stack:" "${stacks[@]}"
38
- echo "$SELECTED_OPTION"
39
- }
40
-
41
- collect_references() {
42
- local stack="$1"
43
- local target="$2"
44
- local -n _pending=$3
45
-
46
- local manifest="$PROJECT_ROOT/tooling/$stack/manifest.toml"
47
- [ ! -f "$manifest" ] && return
48
-
49
- local extends
50
- extends=$(grep '^extends' "$manifest" 2>/dev/null | cut -d'"' -f2)
51
-
52
- if [ -n "$extends" ]; then
53
- collect_references "$extends" "$target" "$3"
54
- fi
55
-
56
- local reference_file="$PROJECT_ROOT/tooling/$stack/reference.md"
57
- [ ! -f "$reference_file" ] && return
58
-
59
- local dest="$target/.claude/tooling/$stack.md"
60
-
61
- if [ -f "$dest" ] && diff -q "$reference_file" "$dest" >/dev/null 2>&1; then
62
- log_info ".claude/tooling/$stack.md"
63
- return
64
- fi
65
-
66
- log_add ".claude/tooling/$stack.md"
67
- _pending+=("$stack")
68
- }
69
-
70
- apply_references() {
71
- local target="$1"
72
- shift
73
- local stacks=("$@")
74
-
75
- mkdir -p "$target/.claude/tooling"
76
-
77
- for stack in "${stacks[@]}"; do
78
- local src="$PROJECT_ROOT/tooling/$stack/reference.md"
79
- cp "$src" "$target/.claude/tooling/$stack.md"
80
- log_add ".claude/tooling/$stack.md"
81
- rm -f "$target/tooling/$stack.md"
82
- done
83
- rmdir "$target/tooling" 2>/dev/null || true
84
- }
85
-
86
- main() {
87
- if [[ "$1" == "-h" || "$1" == "--help" ]]; then
88
- show_help
89
- fi
90
-
91
- local stack="$1"
92
- local target="${2:-.}"
93
-
94
- # Every frame below goes to stderr, so it asks about that stream rather than
95
- # reading the source-time answer, which is for stdout and is what `show_help`
96
- # above wants. Declared after the help branch so that call keeps it.
97
- local GREEN RED YELLOW WHITE GREY NC
98
- set_palette 2
99
-
100
- echo -e "${GREY}┌${NC}" >&2
101
- echo -e "${GREY}│${NC} ${WHITE}aitk tooling ref${NC}" >&2
102
-
103
- if [ -z "$stack" ]; then
104
- stack=$(select_stack)
105
- fi
106
-
107
- if is_tooling_stack_excluded "$stack"; then
108
- log_error "Claude is managed by \`aitk claude\`, not \`aitk tooling\`."
109
- fi
110
-
111
- if [ ! -d "$PROJECT_ROOT/tooling/$stack" ]; then
112
- log_error "Stack not found: $stack"
113
- fi
114
-
115
- guard_root "$target"
116
-
117
- local pending=()
118
-
119
- echo -e "${GREY}├${NC} ${WHITE}Scanning references: $stack${NC}" >&2
120
- collect_references "$stack" "$target" pending
121
-
122
- if [ "${#pending[@]}" -eq 0 ]; then
123
- trap - EXIT
124
- echo -e "${GREY}└${NC}\n" >&2
125
- echo -e "${GREEN}✓ References up to date${NC}" >&2
126
- exit 0
127
- fi
128
-
129
- local dest_display="$target/.claude/tooling/"
130
- dest_display="${dest_display#./}"
131
-
132
- select_option "Sync ${#pending[@]} reference(s) to $dest_display?" "Yes" "No"
133
-
134
- if [ "$SELECTED_OPTION" = "No" ]; then
135
- log_warn "Cancelled"
136
- exit 0
137
- fi
138
-
139
- log_step "Applying changes"
140
- apply_references "$target" "${pending[@]}"
141
-
142
- trap - EXIT
143
- echo -e "${GREY}└${NC}\n" >&2
144
- echo -e "${GREEN}✓ References synced${NC}" >&2
145
- }
146
-
147
- main "$@"