@erclx/aitk 0.10.0 → 0.11.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/docs/agents.md +10 -0
- package/package.json +1 -1
- package/scripts/core/regen-claude-copies.sh +10 -1
- package/scripts/core/verify.sh +1 -1
- package/src/commands/gov.ts +40 -0
- package/src/gov/consumed.ts +129 -0
package/docs/agents.md
CHANGED
|
@@ -136,6 +136,16 @@ headlessly, so a call that names its stack or category is unchanged.
|
|
|
136
136
|
`aitk snippets install`. Both resolve the target before anything else, so a path
|
|
137
137
|
that does not exist fails rather than being scaffolded.
|
|
138
138
|
|
|
139
|
+
`aitk gov regen` is the one governance verb that runs against the toolkit root,
|
|
140
|
+
because the `.claude/rules/` it writes there is produced output rather than an
|
|
141
|
+
operator's working copy. It reads the stack recorded in `internal/governance.toml`,
|
|
142
|
+
installs it alongside anything under `internal/rules/`, and clears the
|
|
143
|
+
destination first so a rule the record stopped naming disappears. It takes
|
|
144
|
+
`--root <path>` and defaults to the toolkit root, prints nothing on success, and
|
|
145
|
+
reports the reason on stderr with exit 1 when the record names a stack or rule
|
|
146
|
+
that does not resolve. `scripts/core/regen-claude-copies.sh` calls it, and the
|
|
147
|
+
Consumed copies stage of `bun run check` asserts the result is committed.
|
|
148
|
+
|
|
139
149
|
`aitk sync` runs every installed domain sync, then offers to commit the result
|
|
140
150
|
and open a pull request. Under `AITK_NON_INTERACTIVE=1` it applies the domain
|
|
141
151
|
syncs and then refuses the git workflow, reporting the branch and commit it
|
package/package.json
CHANGED
|
@@ -23,4 +23,13 @@ mirror_dir "$PROJECT_ROOT/snippets" "$PROJECT_ROOT/.claude/snippets" -name "*.md
|
|
|
23
23
|
|
|
24
24
|
# `internal/` is the surface the plugin does not symlink. Mirrored on its own so
|
|
25
25
|
# toolkit sessions read it at a `.claude/` path like every other consumed copy.
|
|
26
|
-
|
|
26
|
+
# `internal/rules/` is excluded because it lands in `.claude/rules/` below, and
|
|
27
|
+
# mirroring it here too would publish each rule at a second inert path. The
|
|
28
|
+
# exclusion is anchored to that one folder, since an unanchored `*/rules/*` would
|
|
29
|
+
# also drop a later `internal/standards/rules/` and report nothing for it.
|
|
30
|
+
mirror_dir "$PROJECT_ROOT/internal" "$PROJECT_ROOT/.claude/internal" -name "*.md" -not -path "$PROJECT_ROOT/internal/rules/*"
|
|
31
|
+
|
|
32
|
+
# `.claude/rules/` is a subset rather than a mirror, so it resolves through the
|
|
33
|
+
# stack machinery instead of a fourth `mirror_dir` call. The record naming the
|
|
34
|
+
# subset is `internal/governance.toml`.
|
|
35
|
+
bun "$PROJECT_ROOT/src/cli.ts" gov regen --root "$PROJECT_ROOT"
|
package/scripts/core/verify.sh
CHANGED
|
@@ -132,7 +132,7 @@ main() {
|
|
|
132
132
|
|
|
133
133
|
log_step "Consumed copies"
|
|
134
134
|
run_check "bash $PROJECT_ROOT/scripts/core/regen-claude-copies.sh" "Consumed-copy regen failed"
|
|
135
|
-
assert_no_drift ".claude/standards .claude/snippets .claude/internal" "Consumed copies drifted. Run bun run check and commit .claude/standards, .claude/snippets, and .claude/
|
|
135
|
+
assert_no_drift ".claude/standards .claude/snippets .claude/internal .claude/rules" "Consumed copies drifted. Run bun run check and commit .claude/standards, .claude/snippets, .claude/internal, and .claude/rules."
|
|
136
136
|
log_info "Consumed copies clean"
|
|
137
137
|
|
|
138
138
|
log_step "Skill references"
|
package/src/commands/gov.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type { Command } from 'commander'
|
|
|
5
5
|
import { registerPassThroughVerbs } from '@/commands/pass-through'
|
|
6
6
|
import { PROJECT_ROOT } from '@/exec'
|
|
7
7
|
import { createGovAdapter } from '@/gov/adapter'
|
|
8
|
+
import { regenConsumedRules } from '@/gov/consumed'
|
|
8
9
|
import { hasStandards, installRules, lookupRules } from '@/gov/install'
|
|
9
10
|
import { buildRulesPayload, listRuleFiles } from '@/gov/payload'
|
|
10
11
|
import {
|
|
@@ -39,6 +40,10 @@ interface InstallOptions {
|
|
|
39
40
|
readonly add?: string
|
|
40
41
|
}
|
|
41
42
|
|
|
43
|
+
interface RegenOptions {
|
|
44
|
+
readonly root?: string
|
|
45
|
+
}
|
|
46
|
+
|
|
42
47
|
export function register(program: Command): void {
|
|
43
48
|
const gov = program
|
|
44
49
|
.command('gov')
|
|
@@ -95,9 +100,44 @@ export function register(program: Command): void {
|
|
|
95
100
|
process.exitCode = await runBuild(target)
|
|
96
101
|
})
|
|
97
102
|
|
|
103
|
+
gov
|
|
104
|
+
.command('regen')
|
|
105
|
+
.description("Rebuild a repository's own .claude/rules/ from its record")
|
|
106
|
+
.helpOption('-h, --help', 'Show this help message')
|
|
107
|
+
.option('--root <path>', 'Repository root to regenerate', PROJECT_ROOT)
|
|
108
|
+
.addHelpText(
|
|
109
|
+
'after',
|
|
110
|
+
[
|
|
111
|
+
'',
|
|
112
|
+
'Reads internal/governance.toml and installs the stack it names, plus',
|
|
113
|
+
'any rules under internal/rules/. Unlike install and sync, this runs',
|
|
114
|
+
'against the toolkit root, whose .claude/rules/ is produced output.',
|
|
115
|
+
'',
|
|
116
|
+
].join('\n'),
|
|
117
|
+
)
|
|
118
|
+
.action(async (opts: RegenOptions) => {
|
|
119
|
+
process.exitCode = await runRegen(opts)
|
|
120
|
+
})
|
|
121
|
+
|
|
98
122
|
registerPassThroughVerbs(gov, 'gov', PASS_THROUGH_VERBS)
|
|
99
123
|
}
|
|
100
124
|
|
|
125
|
+
/**
|
|
126
|
+
* Silent on success so the consumed-copy stage that calls it stays as quiet as
|
|
127
|
+
* the three `mirror_dir` lines it sits beside. The installed set is readable on
|
|
128
|
+
* disk, so printing it would only add noise to every `bun run check`.
|
|
129
|
+
*/
|
|
130
|
+
async function runRegen(opts: RegenOptions): Promise<number> {
|
|
131
|
+
const result = await regenConsumedRules(resolve(opts.root ?? PROJECT_ROOT))
|
|
132
|
+
|
|
133
|
+
if (!result.ok) {
|
|
134
|
+
process.stderr.write(`Consumed-rules regen failed: ${result.reason}\n`)
|
|
135
|
+
return 1
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return 0
|
|
139
|
+
}
|
|
140
|
+
|
|
101
141
|
/**
|
|
102
142
|
* Renders the target-relative path the prompt quotes, keeping the argument the
|
|
103
143
|
* caller typed rather than the absolute path it resolves to.
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
2
|
+
import { rm } from 'node:fs/promises'
|
|
3
|
+
import { basename, join } from 'node:path'
|
|
4
|
+
import {
|
|
5
|
+
installRules,
|
|
6
|
+
installedRulesDir,
|
|
7
|
+
lookupRules,
|
|
8
|
+
type RuleSource,
|
|
9
|
+
ruleSubdir,
|
|
10
|
+
} from '@/gov/install'
|
|
11
|
+
import { mergeExtraRules, resolveRules } from '@/gov/stacks'
|
|
12
|
+
|
|
13
|
+
export const RECORD_REL = join('internal', 'governance.toml')
|
|
14
|
+
|
|
15
|
+
const INTERNAL_RULES_REL = join('internal', 'rules')
|
|
16
|
+
|
|
17
|
+
/** The stack a repository installs into its own `.claude/rules/`. */
|
|
18
|
+
export interface ConsumedRecord {
|
|
19
|
+
readonly stack: string
|
|
20
|
+
readonly add: readonly string[]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type ConsumedResult =
|
|
24
|
+
| { readonly ok: true; readonly installed: readonly string[] }
|
|
25
|
+
| { readonly ok: false; readonly reason: string }
|
|
26
|
+
|
|
27
|
+
export function consumedRecordPath(root: string): string {
|
|
28
|
+
return join(root, RECORD_REL)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function internalRulesDir(root: string): string {
|
|
32
|
+
return join(root, INTERNAL_RULES_REL)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Reads the record naming the consumed stack. A file with no `stack` reads the
|
|
37
|
+
* same as no file at all, since neither tells the producer what to install and
|
|
38
|
+
* the caller's message covers both.
|
|
39
|
+
*/
|
|
40
|
+
export function readConsumedRecord(root: string): ConsumedRecord | undefined {
|
|
41
|
+
const path = consumedRecordPath(root)
|
|
42
|
+
if (!existsSync(path)) return undefined
|
|
43
|
+
|
|
44
|
+
const parsed = Bun.TOML.parse(readFileSync(path, 'utf8')) as Record<
|
|
45
|
+
string,
|
|
46
|
+
unknown
|
|
47
|
+
>
|
|
48
|
+
const stack = typeof parsed.stack === 'string' ? parsed.stack : ''
|
|
49
|
+
if (stack === '') return undefined
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
stack,
|
|
53
|
+
add: Array.isArray(parsed.add)
|
|
54
|
+
? parsed.add.filter(
|
|
55
|
+
(rule): rule is string => typeof rule === 'string' && rule !== '',
|
|
56
|
+
)
|
|
57
|
+
: [],
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Lists rules authored under `internal/rules/`. These govern toolkit authoring
|
|
63
|
+
* against paths only this repository has, so they install into the consumed
|
|
64
|
+
* copy without ever entering `governance/rules/`, which ships to targets.
|
|
65
|
+
*/
|
|
66
|
+
export function listInternalRules(root: string): RuleSource[] {
|
|
67
|
+
const dir = internalRulesDir(root)
|
|
68
|
+
if (!existsSync(dir)) return []
|
|
69
|
+
|
|
70
|
+
return [...new Bun.Glob('**/*.md').scanSync({ cwd: dir, onlyFiles: true })]
|
|
71
|
+
.sort()
|
|
72
|
+
.map((rel) => {
|
|
73
|
+
const src = join(dir, rel)
|
|
74
|
+
return { rule: basename(rel, '.md'), src, subdir: ruleSubdir(src, dir) }
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Rebuilds a repository's own `.claude/rules/` from its record. Unlike
|
|
80
|
+
* `gov install` and `gov sync`, this runs against the toolkit root on purpose:
|
|
81
|
+
* those two refuse it because a target's rules are the operator's to edit,
|
|
82
|
+
* while this destination is produced output that happens to live beside its
|
|
83
|
+
* source.
|
|
84
|
+
*/
|
|
85
|
+
export async function regenConsumedRules(
|
|
86
|
+
root: string,
|
|
87
|
+
): Promise<ConsumedResult> {
|
|
88
|
+
const record = readConsumedRecord(root)
|
|
89
|
+
if (record === undefined) {
|
|
90
|
+
return { ok: false, reason: `No stack recorded at ${RECORD_REL}` }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const resolution = resolveRules(root, record.stack)
|
|
94
|
+
if (!resolution.ok) {
|
|
95
|
+
return { ok: false, reason: `Stack not found: ${resolution.missingStack}` }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const { found, missing } = lookupRules(
|
|
99
|
+
root,
|
|
100
|
+
mergeExtraRules(resolution.rules, record.add.join(',')),
|
|
101
|
+
)
|
|
102
|
+
if (missing.length > 0) {
|
|
103
|
+
return { ok: false, reason: `No source for: ${missing.join(', ')}` }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const internal = listInternalRules(root)
|
|
107
|
+
const stackRules = new Set(found.map((entry) => entry.rule))
|
|
108
|
+
const shadowed = internal
|
|
109
|
+
.filter((entry) => stackRules.has(entry.rule))
|
|
110
|
+
.map((entry) => entry.rule)
|
|
111
|
+
if (shadowed.length > 0) {
|
|
112
|
+
return {
|
|
113
|
+
ok: false,
|
|
114
|
+
reason: `Internal rules shadow stack rules: ${shadowed.join(', ')}`,
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Clearing first is what makes a rule the record stopped naming disappear.
|
|
119
|
+
// Copying over the destination would leave it behind as an unsourced file,
|
|
120
|
+
// which is the state this producer exists to end.
|
|
121
|
+
await rm(installedRulesDir(root), { recursive: true, force: true })
|
|
122
|
+
|
|
123
|
+
const installed = [
|
|
124
|
+
...(await installRules(found, root)),
|
|
125
|
+
...(await installRules(internal, root)),
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
return { ok: true, installed: installed.sort() }
|
|
129
|
+
}
|