@erclx/aitk 3.19.0 → 3.20.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/docs/agents/install-and-sync.md +15 -0
- package/governance/rules/claude/590-rule-authoring.md +4 -2
- package/package.json +1 -1
- package/src/sync/engine.ts +47 -5
- package/standards/rule.md +15 -5
|
@@ -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
|
|
|
@@ -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
|
|
@@ -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/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/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
|
|