@erclx/aitk 3.19.0 → 3.21.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/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/create-rule/REQUIREMENT.md +10 -8
- package/claude/skills/create-rule/SKILL.md +14 -14
- package/claude/skills/setup-init/SKILL.md +2 -2
- package/claude/skills/toolkit-cli/SKILL.md +11 -11
- package/docs/agents/install-and-sync.md +15 -0
- package/docs/target-projects.md +5 -5
- package/governance/rules/claude/590-rule-authoring.md +4 -2
- package/package.json +1 -1
- package/src/commands/tooling.ts +56 -32
- package/src/sync/check.ts +1 -5
- package/src/sync/engine.ts +47 -5
- package/src/tooling/inject.ts +1 -38
- package/src/tooling/manifest.ts +0 -2
- package/src/tooling/read.ts +65 -0
- package/src/tooling/scan.ts +4 -43
- package/standards/rule.md +15 -5
- package/scripts/tooling/ref.sh +0 -147
|
@@ -1,39 +1,41 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: create-rule
|
|
3
|
-
description: Why a project-local rule needs a
|
|
3
|
+
description: Why a project-local rule needs a subdir, a number from the band reserved for one, and a scope key that decides when it fires
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Create rule requirement
|
|
7
7
|
|
|
8
8
|
## Gap
|
|
9
9
|
|
|
10
|
-
Without this skill, a project rule is written straight into the rules folder on a number already taken, so two rules collide and one loses. The subtler collision was with the toolkit itself: a number free in the target today could be the number a shipped rule landed on tomorrow, and the next install double-booked it, so a rule the project wrote got overwritten by one it never chose. Writing under `.claude/rules/project/` closes that half regardless of the number, since the sync engine orphans the file by location before it ever compares names
|
|
10
|
+
Without this skill, a project rule is written straight into the rules folder on a number already taken, so two rules collide and one loses. The subtler collision was with the toolkit itself: a number free in the target today could be the number a shipped rule landed on tomorrow, and the next install double-booked it, so a rule the project wrote got overwritten by one it never chose. Writing under `.claude/rules/project/` closes that half regardless of the number, since the sync engine orphans the file by location before it ever compares names.
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
Numbering in `900-999` closes the other half. A number scanned against today's toolkit catalog goes stale on the next release, and a reserved band does not.
|
|
13
|
+
|
|
14
|
+
The subdir gets picked by feel, so a UI copy rule lands in the always-on folder and loads on every session for the rest of the project's life. The scope key fails in both directions. An always-on rule carrying a path scope fires only on files it was never about, and a path-scoped rule missing it loads constantly. Written from memory rather than from the rule standard, the body comes out in a shape the rest of the catalog does not share.
|
|
13
15
|
|
|
14
16
|
## Must
|
|
15
17
|
|
|
16
18
|
- Resolve what the rule enforces and where it applies, asking only for what the request leaves missing
|
|
17
|
-
- Pick the
|
|
19
|
+
- Pick the subdir from the topic and take the folder from that choice
|
|
18
20
|
- Write under `.claude/rules/project/`, the subfolder the sync engine reads as project-authored by location
|
|
19
|
-
-
|
|
21
|
+
- Take the lowest free number at or above `900`, scanning every project subdir the target holds rather than one
|
|
20
22
|
- Read the rule standard before writing the body
|
|
21
23
|
- Emit the path scope for a path-scoped rule and omit the key entirely for an always-on one
|
|
22
|
-
- Preview the resolved path,
|
|
24
|
+
- Preview the resolved path, subdir, number, and frontmatter, then write without pausing
|
|
23
25
|
- Say when the rule loads, since path-scoped and always-on rules behave differently
|
|
24
26
|
|
|
25
27
|
## Must not
|
|
26
28
|
|
|
27
29
|
- Edit a toolkit source rule, which is authored in the toolkit and would be overwritten here
|
|
28
30
|
- Work the body shape or the frontmatter from memory
|
|
29
|
-
- Take a number
|
|
31
|
+
- Take a number below `900`, which a later toolkit release can ship into
|
|
30
32
|
- Write more than one topic into a single rule
|
|
31
33
|
|
|
32
34
|
## Guards
|
|
33
35
|
|
|
34
36
|
- No project Claude directory: stop, since there is nowhere for the rule to live
|
|
35
37
|
- The request names no behavior to enforce: stop rather than inventing one
|
|
36
|
-
- The
|
|
38
|
+
- The reserved band holds no free number: stop rather than reaching below `900`
|
|
37
39
|
|
|
38
40
|
## Out of scope
|
|
39
41
|
|
|
@@ -5,7 +5,7 @@ description: Scaffolds a project-specific governance rule into `.claude/rules/pr
|
|
|
5
5
|
|
|
6
6
|
# Create rule
|
|
7
7
|
|
|
8
|
-
Author a project-local governance rule. The rule lives in the target project, not the toolkit, so `aitk gov sync` never overwrites it: the sync engine orphans anything under `.claude/rules/project/` by location, before it ever checks the rule's name against the toolkit catalog.
|
|
8
|
+
Author a project-local governance rule. The rule lives in the target project, not the toolkit, so `aitk gov sync` never overwrites it: the sync engine orphans anything under `.claude/rules/project/` by location, before it ever checks the rule's name against the toolkit catalog. A rule written anywhere else under `.claude/rules/` survives the sync too, and the report offers it the path under the project subfolder it would take.
|
|
9
9
|
|
|
10
10
|
## Guards
|
|
11
11
|
|
|
@@ -19,30 +19,30 @@ Resolve both from the request, and ask only for what is missing. Attach a propos
|
|
|
19
19
|
- What the rule enforces: one topic, phrased as a standard (`<topic> conventions`).
|
|
20
20
|
- Scope: a path glob relative to the project root (`<dir>/**/*.<ext>`) for a path-scoped rule, or always-on when the rule states a global principle with no file scope.
|
|
21
21
|
|
|
22
|
-
## Step 2: resolve
|
|
22
|
+
## Step 2: resolve the subdir
|
|
23
23
|
|
|
24
|
-
Pick the
|
|
24
|
+
Pick the subdir from the topic. It sits under `.claude/rules/project/` and names the rule's domain:
|
|
25
25
|
|
|
26
|
-
- `core
|
|
27
|
-
- `lang
|
|
28
|
-
- `framework
|
|
29
|
-
- `lib
|
|
30
|
-
- `ui
|
|
31
|
-
- `claude
|
|
26
|
+
- `core/`: global persona, testing, error handling, planning. Always-on, no `paths:`.
|
|
27
|
+
- `lang/`: one programming language.
|
|
28
|
+
- `framework/`: one framework.
|
|
29
|
+
- `lib/`: one library or tool.
|
|
30
|
+
- `ui/`: UI copy, accessibility, forms.
|
|
31
|
+
- `claude/`: `.claude/` authoring surfaces.
|
|
32
32
|
|
|
33
33
|
## Step 3: pick a free number
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
A project-authored rule numbers in `900-999`, the band reserved for one. `000-899` belongs to the toolkit, so a number free in the target today is one a later release can ship into.
|
|
36
36
|
|
|
37
|
-
- Scan
|
|
38
|
-
-
|
|
39
|
-
-
|
|
37
|
+
- Scan every `.claude/rules/project/<subdir>/` in the target for used prefixes, not only the subdir this rule lands in. The band runs as one sequence across all of them.
|
|
38
|
+
- Take the lowest unused number at or above `900`.
|
|
39
|
+
- Stop and say so if the band is full, rather than reaching below `900`.
|
|
40
40
|
|
|
41
41
|
## Step 4: write the rule
|
|
42
42
|
|
|
43
43
|
Read `${CLAUDE_SKILL_DIR}/../../standards/rule.md` for frontmatter, body shape, and voice before writing the body. Do not work the shape from memory.
|
|
44
44
|
|
|
45
|
-
Write `.claude/rules/project/<subdir>/<n>-<slug>.md` where `<slug>` is a 1-to-3-word kebab topic. Preview the resolved path,
|
|
45
|
+
Write `.claude/rules/project/<subdir>/<n>-<slug>.md` where `<slug>` is a 1-to-3-word kebab topic. Preview the resolved path, subdir, number, and frontmatter, then write immediately. The tool permission dialog is the confirmation gate.
|
|
46
46
|
|
|
47
47
|
Frontmatter carries the Claude shape. Path-scoped rules emit one `paths:` entry per glob. Always-on rules omit `paths:` entirely.
|
|
48
48
|
|
|
@@ -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
|
|
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.**
|
|
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
|
-
|
|
|
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
|
|
|
@@ -16,6 +16,21 @@ is left alone regardless of its name, since that subfolder is project-authored
|
|
|
16
16
|
by location. It also removes a stale `.claude/GOV.md`
|
|
17
17
|
from the retired build. Use `aitk gov install` to add rules.
|
|
18
18
|
|
|
19
|
+
A rule the toolkit finds no source for, sitting in a shared subdirectory rather
|
|
20
|
+
than under `.claude/rules/project/`, is reported with the path under that
|
|
21
|
+
subfolder it would take. The offer is conditional on the project having written
|
|
22
|
+
the rule, since a rule the toolkit shipped and later renamed reaches the same
|
|
23
|
+
line, and moving one there would mark it the project's for good. Nothing is
|
|
24
|
+
moved either way, because a rule's installed path is one the project's own
|
|
25
|
+
rules, skills, and docs may cite. `aitk standards rule` carries the reserved
|
|
26
|
+
number bands behind that placement, where `900-999` is the range a
|
|
27
|
+
project-authored rule takes and everything below it belongs to the toolkit.
|
|
28
|
+
|
|
29
|
+
`aitk snippets sync` reports the same way for a snippet outside
|
|
30
|
+
`.claude/snippets/project/`, since both adapters declare the subfolder.
|
|
31
|
+
`aitk sync --check` does not. It skips every orphaned entry, so the destination
|
|
32
|
+
reaches the two domain sync verbs alone.
|
|
33
|
+
|
|
19
34
|
When the target's install recorded a stack, `aitk gov sync` also reports a
|
|
20
35
|
rule that stack lists and `.claude/rules/` does not hold, as a `missing` entry
|
|
21
36
|
carrying no change. This is what makes a target whose recorded sync point
|
package/docs/target-projects.md
CHANGED
|
@@ -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
|
|
49
|
-
- The agent
|
|
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
|
|
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
|
|
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
|
|
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
|
|
|
@@ -8,8 +8,10 @@ paths:
|
|
|
8
8
|
|
|
9
9
|
## Numbering
|
|
10
10
|
|
|
11
|
-
- Name a rule `.claude/rules/<subdirectory>/<n>-<slug>.md
|
|
12
|
-
-
|
|
11
|
+
- Name a rule `.claude/rules/<subdirectory>/<n>-<slug>.md`. Give every rule a numeric prefix and keep the slug to one to three kebab words.
|
|
12
|
+
- Treat `000-899` as reserved for a rule set that ships to targets, `000-599` across the shared subdirectories and `600-899` held for ones it has not added.
|
|
13
|
+
- Number a rule the project authored itself in `900-999` and write it at `.claude/rules/project/<subdirectory>/<n>-<slug>.md`.
|
|
14
|
+
- Do not take a reserved number for a project-authored rule because nothing installed holds it today. A later release can ship into it.
|
|
13
15
|
|
|
14
16
|
## Body
|
|
15
17
|
|
package/package.json
CHANGED
package/src/commands/tooling.ts
CHANGED
|
@@ -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 = ['
|
|
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,
|
|
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
|
|
252
|
-
const result = scan(prepared.chain, prepared.target, { includeReferences })
|
|
299
|
+
const result = scan(prepared.chain, prepared.target)
|
|
253
300
|
|
|
254
|
-
report(result
|
|
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
|
|
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
|
|
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
|
}
|
package/src/sync/engine.ts
CHANGED
|
@@ -81,9 +81,10 @@ export interface ScanEntry {
|
|
|
81
81
|
/** Toolkit revision this file's content came from, when history proved it. */
|
|
82
82
|
readonly since?: string
|
|
83
83
|
/**
|
|
84
|
-
* Overrides `report`'s generic text for this entry's state.
|
|
85
|
-
*
|
|
86
|
-
* resolved it, and
|
|
84
|
+
* Overrides `report`'s generic text for this entry's state. Two producers
|
|
85
|
+
* write one: `collectMissing`, since a stack name is only known to the
|
|
86
|
+
* adapter that resolved it, and an orphan sitting outside the declared
|
|
87
|
+
* project subfolder, since the generic line names no destination.
|
|
87
88
|
*/
|
|
88
89
|
readonly notice?: string
|
|
89
90
|
}
|
|
@@ -143,6 +144,9 @@ export interface SyncAdapter {
|
|
|
143
144
|
* location rather than by the name inference `locateSource` runs.
|
|
144
145
|
* Checked before `locateSource`, so a file here is orphaned even when its
|
|
145
146
|
* name also matches a toolkit source, and never enters the stamp.
|
|
147
|
+
*
|
|
148
|
+
* Declaring it also gives the report a destination to name when the name
|
|
149
|
+
* inference orphans a file sitting anywhere else.
|
|
146
150
|
*/
|
|
147
151
|
readonly projectSubdir?: string
|
|
148
152
|
/** Defaults to applying. */
|
|
@@ -200,7 +204,7 @@ export function planSync(adapter: SyncAdapter, target: string): SyncPlan {
|
|
|
200
204
|
const source = adapter.locateSource(file)
|
|
201
205
|
|
|
202
206
|
if (source === undefined || !existsSync(source)) {
|
|
203
|
-
entries.push(
|
|
207
|
+
entries.push(misplacedOrphan(adapter, target, file))
|
|
204
208
|
continue
|
|
205
209
|
}
|
|
206
210
|
|
|
@@ -419,7 +423,8 @@ function report(adapter: SyncAdapter, plan: SyncPlan): void {
|
|
|
419
423
|
logWarn(
|
|
420
424
|
entry.notice ?? `${entry.rel} (listed by the stack, not installed)`,
|
|
421
425
|
)
|
|
422
|
-
else
|
|
426
|
+
else
|
|
427
|
+
logWarn(entry.notice ?? `${entry.rel} (not in toolkit source, skipping)`)
|
|
423
428
|
}
|
|
424
429
|
|
|
425
430
|
for (const surface of plan.retired) {
|
|
@@ -486,6 +491,43 @@ function isProjectAuthored(adapter: SyncAdapter, file: InstalledFile): boolean {
|
|
|
486
491
|
return file.relToRoot.split('/')[0] === adapter.projectSubdir
|
|
487
492
|
}
|
|
488
493
|
|
|
494
|
+
/**
|
|
495
|
+
* An orphan the name inference caught rather than the location test. No source
|
|
496
|
+
* name matched, which is what a project-authored file looks like and also what
|
|
497
|
+
* a file the toolkit shipped and later renamed looks like, so the line offers
|
|
498
|
+
* the destination on a condition rather than asserting who wrote the file.
|
|
499
|
+
* Moving a toolkit leftover into the project subfolder would mark it the
|
|
500
|
+
* project's permanently, and only the operator can tell the two apart.
|
|
501
|
+
*
|
|
502
|
+
* The stamp cannot tell them apart either. `recordStamp` skips a file whose
|
|
503
|
+
* source is gone and `writeStamp` replaces the domain's whole `files` map, so
|
|
504
|
+
* a renamed rule's entry survives exactly one sync past the rename, and a
|
|
505
|
+
* target installed before stamping shipped has no entry to read at all.
|
|
506
|
+
*
|
|
507
|
+
* Naming the destination is all this does. Moving the file rewrites a path the
|
|
508
|
+
* project's own rules, skills, and docs may cite, so the sync leaves it where
|
|
509
|
+
* it is.
|
|
510
|
+
*/
|
|
511
|
+
function misplacedOrphan(
|
|
512
|
+
adapter: SyncAdapter,
|
|
513
|
+
target: string,
|
|
514
|
+
file: InstalledFile,
|
|
515
|
+
): ScanEntry {
|
|
516
|
+
const subdir = adapter.projectSubdir
|
|
517
|
+
if (subdir === undefined) return { state: 'orphaned', rel: file.rel }
|
|
518
|
+
|
|
519
|
+
const belongs = relative(
|
|
520
|
+
target,
|
|
521
|
+
resolve(adapter.installedRoot(target), subdir, file.relToRoot),
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
return {
|
|
525
|
+
state: 'orphaned',
|
|
526
|
+
rel: file.rel,
|
|
527
|
+
notice: `${file.rel} (not in toolkit source, skipping. Move it to ${belongs} if the project authored it.)`,
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
489
531
|
function isInside(target: string, path: string): boolean {
|
|
490
532
|
const rel = relative(target, path)
|
|
491
533
|
return rel !== '' && !rel.startsWith('..') && !isAbsolute(rel)
|
package/src/tooling/inject.ts
CHANGED
|
@@ -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
|
-
}
|
package/src/tooling/manifest.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/tooling/scan.ts
CHANGED
|
@@ -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
|
|
59
|
-
*
|
|
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/rule.md
CHANGED
|
@@ -33,17 +33,27 @@ Write both when both apply. A rule stating the directive and a skill stating how
|
|
|
33
33
|
|
|
34
34
|
- Rules live at `.claude/rules/<subdirectory>/<n>-<slug>.md`
|
|
35
35
|
- Subdirectories group by domain: `core/`, `lang/`, `framework/`, `lib/`, `ui/`, `claude/`
|
|
36
|
-
- `<n>` is a number
|
|
37
|
-
-
|
|
36
|
+
- `<n>` is a number from the band reserved for the rule's source and `<slug>` is a one-to-three-word kebab topic
|
|
37
|
+
- A rule the project authored itself lives at `.claude/rules/project/<subdirectory>/<n>-<slug>.md`, keeping the same subdirectory names
|
|
38
38
|
- Give every rule a numeric prefix. A bare-word filename reads as a folder name where a stack names its rules, so a rule without one is unreachable from a stack entry.
|
|
39
39
|
|
|
40
|
+
## Reserved numbers
|
|
41
|
+
|
|
42
|
+
`000-899` is reserved for a rule set that ships to targets. `000-599` is what the six subdirectories above already divide, at 100 per subdirectory, and `600-899` is held for subdirectories a shipped set has not added yet.
|
|
43
|
+
|
|
44
|
+
A project-authored rule takes `900-999`, one sequence across every subdirectory under `.claude/rules/project/`. Scanning for a free number instead is what fails, because a shipped set fills its own band release by release, so what reads as free today is what a later release lands on. One target authored `claude/561-self-check.md` on a day nothing shipped at 561, met `claude/561-teach.md` on its next install, and now reads two numbers differently from everywhere else.
|
|
45
|
+
|
|
46
|
+
The cost is that a project-authored rule's leading digit stops naming its domain. Its subdirectory names it instead, and install preserves that either way. What a shared band costs is worse: a session that loads two rules reading as one number, with nothing in the folder to say which is which.
|
|
47
|
+
|
|
48
|
+
`aitk gov sync` reports a rule it finds no toolkit source for, outside `.claude/rules/project/`, and offers the path under it. The offer is conditional on the project having authored the rule, because a rule the toolkit shipped and later renamed looks identical from the outside and moving one there would mark it the project's for good. Nothing is moved either way, since a rule's installed path is one the project's own rules, skills, and docs may cite.
|
|
49
|
+
|
|
40
50
|
## Two sources numbering into one folder
|
|
41
51
|
|
|
42
|
-
|
|
52
|
+
The reservation above divides one pair, being a shipped set against the rules a project wrote for itself. A third source numbering into the same folder needs its own division, which is the case for a rule set held back from targets and installed only where it was authored. Divide by source rather than by topic: one source takes the top of each subdirectory's band and the other takes the gaps between the tens.
|
|
43
53
|
|
|
44
|
-
The collision this prevents is silent. Two rules that resolve to the same `<n>-<slug>` path leave one file in the installed folder, and neither the install nor the session that reads it reports which source lost. Nothing checks the
|
|
54
|
+
The collision this prevents is silent. Two rules that resolve to the same `<n>-<slug>` path leave one file in the installed folder, and neither the install nor the session that reads it reports which source lost. Nothing checks a division outside the reserved bands, so it holds only while both sources follow it.
|
|
45
55
|
|
|
46
|
-
State
|
|
56
|
+
State that division where the rule sources are described, not inside the rules it divides. A rule states its own topic, and a numbering convention spanning two sources belongs to whatever documents the pair.
|
|
47
57
|
|
|
48
58
|
## Frontmatter
|
|
49
59
|
|
package/scripts/tooling/ref.sh
DELETED
|
@@ -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 "$@"
|