@erclx/aitk 0.109.0 → 0.111.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/README.md +5 -1
- package/claude/.claude-plugin/plugin.json +1 -1
- package/docs/agents/audits.md +88 -0
- package/docs/agents/commands.md +20 -2
- package/docs/agents/index.md +1 -0
- package/docs/agents/scripting.md +13 -9
- package/docs/target-projects.md +6 -0
- package/package.json +1 -1
- package/scripts/core/verify.sh +74 -6
- package/src/audits/baseline.ts +194 -0
- package/src/audits/catalog.ts +464 -0
- package/src/audits/run.ts +202 -0
- package/src/cli.ts +10 -20
- package/src/commands/audits.ts +342 -0
- package/src/commands/claude.ts +34 -4
- package/src/commands/sync.ts +21 -0
- package/src/commands/upgrade.ts +231 -0
- package/src/sync/check.ts +18 -0
- package/src/version/compare.ts +53 -0
- package/src/version/installed.ts +40 -0
- package/src/version/manager.ts +67 -0
- package/src/version/skew.ts +192 -0
package/src/cli.ts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
|
-
import { readFileSync } from 'node:fs'
|
|
4
|
-
import { join } from 'node:path'
|
|
5
3
|
import { Command } from 'commander'
|
|
6
4
|
import { register as init } from '@/commands/init'
|
|
7
5
|
import { register as sandbox } from '@/commands/sandbox'
|
|
@@ -27,7 +25,9 @@ import { register as context } from '@/commands/context'
|
|
|
27
25
|
import { register as markdown } from '@/commands/markdown'
|
|
28
26
|
import { register as records } from '@/commands/records'
|
|
29
27
|
import { register as sessions } from '@/commands/sessions'
|
|
30
|
-
import {
|
|
28
|
+
import { register as audits } from '@/commands/audits'
|
|
29
|
+
import { register as upgrade } from '@/commands/upgrade'
|
|
30
|
+
import { readInstalled, UNKNOWN_LABEL } from '@/version/installed'
|
|
31
31
|
|
|
32
32
|
const GREY = '\x1b[0;90m'
|
|
33
33
|
const WHITE = '\x1b[1;37m'
|
|
@@ -63,6 +63,8 @@ function showHelp(): void {
|
|
|
63
63
|
`${GREY}│${NC} markdown [cmd] ${GREY}# Report markdown against the attribute standards (audit)${NC}`,
|
|
64
64
|
`${GREY}│${NC} records [cmd] ${GREY}# Session records under .claude/ (validate, push, pull)${NC}`,
|
|
65
65
|
`${GREY}│${NC} sessions [cmd] ${GREY}# Resolve live sessions to worktree and branch (list)${NC}`,
|
|
66
|
+
`${GREY}│${NC} audits [cmd] ${GREY}# Run every health check as one set (run, list)${NC}`,
|
|
67
|
+
`${GREY}│${NC} upgrade ${GREY}# Reinstall the CLI globally with the manager that installed it${NC}`,
|
|
66
68
|
`${GREY}│${NC}`,
|
|
67
69
|
`${GREY}│${NC} ${WHITE}Sandbox:${NC}`,
|
|
68
70
|
`${GREY}│${NC} aitk sandbox ${GREY}# Interactive scenario picker${NC}`,
|
|
@@ -101,31 +103,17 @@ function showHelp(): void {
|
|
|
101
103
|
`${GREY}│${NC} aitk records validate plans`,
|
|
102
104
|
`${GREY}│${NC} aitk records push --json`,
|
|
103
105
|
`${GREY}│${NC} aitk sessions list --json`,
|
|
106
|
+
`${GREY}│${NC} aitk audits run --json`,
|
|
107
|
+
`${GREY}│${NC} aitk upgrade --json`,
|
|
104
108
|
`${GREY}└${NC}`,
|
|
105
109
|
]
|
|
106
110
|
console.log(lines.join('\n'))
|
|
107
111
|
}
|
|
108
112
|
|
|
109
|
-
/**
|
|
110
|
-
* Read at runtime rather than inlined, because a literal here is a second place
|
|
111
|
-
* the version lives and it stopped tracking `package.json` at `0.1.0`. The
|
|
112
|
-
* release tool writes one file and this follows it. `package.json` ships in
|
|
113
|
-
* every npm tarball regardless of the `files` list, so the read resolves from a
|
|
114
|
-
* registry install as well as from a clone.
|
|
115
|
-
*/
|
|
116
|
-
function readVersion(): string {
|
|
117
|
-
try {
|
|
118
|
-
const raw = readFileSync(join(PROJECT_ROOT, 'package.json'), 'utf8')
|
|
119
|
-
return (JSON.parse(raw) as { version?: string }).version ?? 'unknown'
|
|
120
|
-
} catch {
|
|
121
|
-
return 'unknown'
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
113
|
const program = new Command()
|
|
126
114
|
program
|
|
127
115
|
.name('aitk')
|
|
128
|
-
.version(
|
|
116
|
+
.version(readInstalled().version ?? UNKNOWN_LABEL)
|
|
129
117
|
.enablePositionalOptions()
|
|
130
118
|
.helpOption(false)
|
|
131
119
|
program.action(() => showHelp())
|
|
@@ -159,5 +147,7 @@ context(program)
|
|
|
159
147
|
markdown(program)
|
|
160
148
|
records(program)
|
|
161
149
|
sessions(program)
|
|
150
|
+
audits(program)
|
|
151
|
+
upgrade(program)
|
|
162
152
|
|
|
163
153
|
program.parse()
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import { execa } from 'execa'
|
|
2
|
+
import type { Command } from 'commander'
|
|
3
|
+
import {
|
|
4
|
+
BASELINE_REL,
|
|
5
|
+
type Baseline,
|
|
6
|
+
baselineFrom,
|
|
7
|
+
compareBaseline,
|
|
8
|
+
type Delta,
|
|
9
|
+
readBaseline,
|
|
10
|
+
writeBaseline,
|
|
11
|
+
} from '@/audits/baseline'
|
|
12
|
+
import { AUDITS, type AuditResult } from '@/audits/catalog'
|
|
13
|
+
import {
|
|
14
|
+
exitCodeFor,
|
|
15
|
+
runAudits,
|
|
16
|
+
spawnAudit,
|
|
17
|
+
type Summary,
|
|
18
|
+
summarize,
|
|
19
|
+
} from '@/audits/run'
|
|
20
|
+
import { gitEnv } from '@/git-env'
|
|
21
|
+
import { intro, logError, logInfo, logStep, logWarn, outro, plural } from '@/ui'
|
|
22
|
+
import { currentWorktreeRoot } from '@/worktree'
|
|
23
|
+
|
|
24
|
+
interface RunCommandOptions {
|
|
25
|
+
readonly json?: boolean
|
|
26
|
+
readonly record?: boolean
|
|
27
|
+
readonly root?: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface ListCommandOptions {
|
|
31
|
+
readonly json?: boolean
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function register(program: Command): void {
|
|
35
|
+
const audits = program
|
|
36
|
+
.command('audits')
|
|
37
|
+
.description('Run every health check this repository owns as one set')
|
|
38
|
+
.helpOption('-h, --help', 'Show this help message')
|
|
39
|
+
|
|
40
|
+
audits
|
|
41
|
+
.command('run')
|
|
42
|
+
.description(
|
|
43
|
+
'Run every audit, report per check under one verdict, and compare each count to the recorded baseline',
|
|
44
|
+
)
|
|
45
|
+
.helpOption('-h, --help', 'Show this help message')
|
|
46
|
+
.option('--json', 'Add a machine-readable record on stdout')
|
|
47
|
+
.option(
|
|
48
|
+
'--root <path>',
|
|
49
|
+
'Tree to measure, defaulting to the current worktree',
|
|
50
|
+
)
|
|
51
|
+
.option(
|
|
52
|
+
'--record',
|
|
53
|
+
`Write this run's tracked counts to ${BASELINE_REL} as the new baseline`,
|
|
54
|
+
)
|
|
55
|
+
.addHelpText(
|
|
56
|
+
'after',
|
|
57
|
+
[
|
|
58
|
+
'',
|
|
59
|
+
'Exit codes:',
|
|
60
|
+
' 0 every audit reported and none carried a finding that is a fact',
|
|
61
|
+
' 1 refused, with the reason on stderr',
|
|
62
|
+
' 2 an audit carries a finding that is a fact',
|
|
63
|
+
' 3 an audit did not report, so the run measured less than the set',
|
|
64
|
+
'',
|
|
65
|
+
'It gates on exactly what already gates a push and on nothing new:',
|
|
66
|
+
'an unresolved context citation, a banned character, word, or spelling,',
|
|
67
|
+
'and a skill folder carrying no REQUIREMENT.md. Every other measure is a',
|
|
68
|
+
'judgment a reader settles, and failing a push on one teaches',
|
|
69
|
+
'contributors to route around the stage.',
|
|
70
|
+
'',
|
|
71
|
+
'Exit 3 is a defect in the run rather than in the tree. An aggregate that',
|
|
72
|
+
'reports a pass over a set it did not finish measuring is the failure this',
|
|
73
|
+
'command exists against, so a verb that did not report takes its own code.',
|
|
74
|
+
'',
|
|
75
|
+
`The baseline at ${BASELINE_REL} holds the counts from the last recorded`,
|
|
76
|
+
'run, so a measure that reports rather than gates still costs something',
|
|
77
|
+
'when it grows. Only a tracked corpus is retained: a gitignored record',
|
|
78
|
+
"folder holds one machine's scratch, and its counts answer nobody else.",
|
|
79
|
+
'',
|
|
80
|
+
'Examples:',
|
|
81
|
+
' aitk audits run',
|
|
82
|
+
' aitk audits run --json',
|
|
83
|
+
' aitk audits run --record',
|
|
84
|
+
'',
|
|
85
|
+
].join('\n'),
|
|
86
|
+
)
|
|
87
|
+
.action(async (opts: RunCommandOptions) => {
|
|
88
|
+
process.exitCode = await runAll(opts)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
audits
|
|
92
|
+
.command('list')
|
|
93
|
+
.description(
|
|
94
|
+
'List every audit this command runs and what each one gates on',
|
|
95
|
+
)
|
|
96
|
+
.helpOption('-h, --help', 'Show this help message')
|
|
97
|
+
.option('--json', 'Emit JSON with the id, invocation, corpus, and gate')
|
|
98
|
+
.addHelpText(
|
|
99
|
+
'after',
|
|
100
|
+
[
|
|
101
|
+
'',
|
|
102
|
+
'Exit codes:',
|
|
103
|
+
' 0 the catalog was listed',
|
|
104
|
+
'',
|
|
105
|
+
'Examples:',
|
|
106
|
+
' aitk audits list',
|
|
107
|
+
' aitk audits list --json',
|
|
108
|
+
'',
|
|
109
|
+
].join('\n'),
|
|
110
|
+
)
|
|
111
|
+
.action((opts: ListCommandOptions) => {
|
|
112
|
+
process.exitCode = runList(opts)
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function runList(opts: ListCommandOptions): number {
|
|
117
|
+
if (opts.json) {
|
|
118
|
+
process.stdout.write(
|
|
119
|
+
`${JSON.stringify({
|
|
120
|
+
audits: AUDITS.map((audit) => ({
|
|
121
|
+
id: audit.id,
|
|
122
|
+
label: audit.label,
|
|
123
|
+
command: `aitk ${audit.argv.join(' ')}`,
|
|
124
|
+
corpus: audit.corpus,
|
|
125
|
+
gates: audit.gatingExits.length > 0,
|
|
126
|
+
})),
|
|
127
|
+
})}\n`,
|
|
128
|
+
)
|
|
129
|
+
return 0
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
intro('aitk audits list')
|
|
133
|
+
for (const audit of AUDITS) {
|
|
134
|
+
logStep(audit.label)
|
|
135
|
+
logInfo(`aitk ${audit.argv.join(' ')}`)
|
|
136
|
+
logInfo(
|
|
137
|
+
`${audit.corpus} corpus, ${audit.gatingExits.length > 0 ? 'gates on a fact' : 'reports only'}`,
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
outro()
|
|
141
|
+
return 0
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* The day a record is stamped with, as `YYYY-MM-DD`.
|
|
146
|
+
*
|
|
147
|
+
* Local rather than UTC, because the stamp is read beside a commit date in a
|
|
148
|
+
* context entry and a run taken in the evening should not record tomorrow.
|
|
149
|
+
*/
|
|
150
|
+
function today(): string {
|
|
151
|
+
const now = new Date()
|
|
152
|
+
return [
|
|
153
|
+
now.getFullYear(),
|
|
154
|
+
String(now.getMonth() + 1).padStart(2, '0'),
|
|
155
|
+
String(now.getDate()).padStart(2, '0'),
|
|
156
|
+
].join('-')
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* The commit the counts were read at, or `unknown` outside a repository.
|
|
161
|
+
*
|
|
162
|
+
* A target project can install this CLI without a git history behind it, and
|
|
163
|
+
* refusing there would withhold the whole report over a field that only makes
|
|
164
|
+
* the record reproducible.
|
|
165
|
+
*/
|
|
166
|
+
async function headCommit(root: string): Promise<string> {
|
|
167
|
+
const result = await execa('git', ['-C', root, 'rev-parse', 'HEAD'], {
|
|
168
|
+
reject: false,
|
|
169
|
+
env: gitEnv(),
|
|
170
|
+
extendEnv: false,
|
|
171
|
+
})
|
|
172
|
+
return result.exitCode === 0 ? result.stdout.trim() : 'unknown'
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function runAll(opts: RunCommandOptions): Promise<number> {
|
|
176
|
+
const emitJson = opts.json ?? false
|
|
177
|
+
const root = opts.root ?? (await currentWorktreeRoot())
|
|
178
|
+
|
|
179
|
+
let baseline: Baseline | undefined
|
|
180
|
+
try {
|
|
181
|
+
baseline = await readBaseline(root)
|
|
182
|
+
} catch (error) {
|
|
183
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
184
|
+
if (emitJson) {
|
|
185
|
+
process.stderr.write(`${message}\n`)
|
|
186
|
+
process.stdout.write(
|
|
187
|
+
`${JSON.stringify({ ok: false, reason: 'bad-baseline', message })}\n`,
|
|
188
|
+
)
|
|
189
|
+
return 1
|
|
190
|
+
}
|
|
191
|
+
intro('aitk audits run')
|
|
192
|
+
logStep('Refused')
|
|
193
|
+
logError(message)
|
|
194
|
+
outro()
|
|
195
|
+
return 1
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const results = await runAudits(AUDITS, spawnAudit(root))
|
|
199
|
+
const deltas = compareBaseline(baseline, results)
|
|
200
|
+
const summary = summarize(results, deltas)
|
|
201
|
+
|
|
202
|
+
let recorded: string | undefined
|
|
203
|
+
if (opts.record) {
|
|
204
|
+
const next = baselineFrom(results, {
|
|
205
|
+
recordedAt: today(),
|
|
206
|
+
commit: await headCommit(root),
|
|
207
|
+
})
|
|
208
|
+
recorded = await writeBaseline(root, next)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (emitJson) {
|
|
212
|
+
process.stdout.write(
|
|
213
|
+
`${JSON.stringify({
|
|
214
|
+
ok: true,
|
|
215
|
+
root,
|
|
216
|
+
// Flat scalars, so a shell stage greps one out without a JSON parser.
|
|
217
|
+
// The nested arrays below carry the detail behind each number.
|
|
218
|
+
summary,
|
|
219
|
+
baseline: baseline
|
|
220
|
+
? { recordedAt: baseline.recordedAt, commit: baseline.commit }
|
|
221
|
+
: undefined,
|
|
222
|
+
recorded,
|
|
223
|
+
audits: results,
|
|
224
|
+
deltas,
|
|
225
|
+
})}\n`,
|
|
226
|
+
)
|
|
227
|
+
} else {
|
|
228
|
+
report(results, deltas, baseline, recorded, summary)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return exitCodeFor(results)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** The counts of one result, rendered as `key n` pairs a reader can scan. */
|
|
235
|
+
function countLine(counts: Record<string, number>): string {
|
|
236
|
+
return Object.entries(counts)
|
|
237
|
+
.map(([key, value]) => `${key} ${value}`)
|
|
238
|
+
.join(', ')
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* The delta line for one audit, or nothing when there is no comparison to
|
|
243
|
+
* report. A first run says so rather than showing a delta of zero, since zero
|
|
244
|
+
* against an absent baseline says the same as a corpus that did not move.
|
|
245
|
+
*/
|
|
246
|
+
function deltaLine(delta: Delta): string | undefined {
|
|
247
|
+
if (delta.kind === 'per-machine' || delta.kind === 'unmeasured') {
|
|
248
|
+
return undefined
|
|
249
|
+
}
|
|
250
|
+
if (delta.kind === 'unrecorded') return 'No recorded baseline to compare'
|
|
251
|
+
|
|
252
|
+
const parts = [
|
|
253
|
+
...delta.moved.map(
|
|
254
|
+
({ key, from, to, delta: moved }) =>
|
|
255
|
+
`${key} ${from} to ${to} (${moved > 0 ? '+' : ''}${moved})`,
|
|
256
|
+
),
|
|
257
|
+
...delta.added.map(({ key, to }) => `${key} ${to}, newly measured`),
|
|
258
|
+
...delta.dropped.map(({ key, from }) => `${key} was ${from}, not measured`),
|
|
259
|
+
]
|
|
260
|
+
|
|
261
|
+
return parts.length === 0 ? undefined : parts.join(', ')
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function report(
|
|
265
|
+
results: readonly AuditResult[],
|
|
266
|
+
deltas: readonly Delta[],
|
|
267
|
+
baseline: Baseline | undefined,
|
|
268
|
+
recorded: string | undefined,
|
|
269
|
+
summary: Summary,
|
|
270
|
+
): void {
|
|
271
|
+
const byId = new Map(deltas.map((delta) => [delta.id, delta]))
|
|
272
|
+
|
|
273
|
+
intro('aitk audits run')
|
|
274
|
+
|
|
275
|
+
logStep('Baseline')
|
|
276
|
+
if (baseline === undefined) {
|
|
277
|
+
logWarn(`None recorded. Take one with aitk audits run --record.`)
|
|
278
|
+
} else {
|
|
279
|
+
logInfo(`${baseline.recordedAt} at ${baseline.commit.slice(0, 8)}`)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
for (const result of results) {
|
|
283
|
+
logStep(result.label)
|
|
284
|
+
|
|
285
|
+
if (result.status === 'absent') {
|
|
286
|
+
logInfo(`No corpus on this machine: ${result.reason ?? 'not found'}`)
|
|
287
|
+
continue
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (result.status === 'unmeasured') {
|
|
291
|
+
logWarn(`Did not report: ${result.reason ?? 'no reason given'}`)
|
|
292
|
+
continue
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const counts = result.counts ?? {}
|
|
296
|
+
const line = countLine(counts)
|
|
297
|
+
if (result.status === 'finding') {
|
|
298
|
+
logError(line === '' ? 'A finding that is a fact' : line)
|
|
299
|
+
} else {
|
|
300
|
+
logInfo(line === '' ? 'Reported' : line)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (!result.tracked) {
|
|
304
|
+
logInfo('Per-machine corpus, so no baseline is kept')
|
|
305
|
+
continue
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const moved = deltaLine(
|
|
309
|
+
byId.get(result.id) ?? { id: result.id, kind: 'unrecorded' },
|
|
310
|
+
)
|
|
311
|
+
if (moved !== undefined) logInfo(moved)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (recorded !== undefined) {
|
|
315
|
+
logStep('Recorded')
|
|
316
|
+
logInfo(recorded)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
logStep('Verdict')
|
|
320
|
+
|
|
321
|
+
if (summary.verdict === 'findings') {
|
|
322
|
+
logError(
|
|
323
|
+
`${plural(summary.facts, 'audit')} carrying a finding that is a fact. Fix what each names.`,
|
|
324
|
+
)
|
|
325
|
+
} else if (summary.verdict === 'incomplete') {
|
|
326
|
+
logError(
|
|
327
|
+
`${plural(summary.unmeasured, 'audit')} did not report, so this run measured less than the set.`,
|
|
328
|
+
)
|
|
329
|
+
} else if (summary.verdict === 'reported') {
|
|
330
|
+
logInfo('No fact. Every other finding is a judgment a reader settles.')
|
|
331
|
+
} else {
|
|
332
|
+
logInfo('Every audit reported and every count is zero.')
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Stated on every run, including a clean one. A count of what passed reads as
|
|
336
|
+
// a verdict on the whole set unless the run also says what it never reached.
|
|
337
|
+
logInfo(
|
|
338
|
+
`${summary.audited} of ${results.length} corpora measured, ${summary.absent} absent on this machine`,
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
outro()
|
|
342
|
+
}
|
package/src/commands/claude.ts
CHANGED
|
@@ -48,6 +48,7 @@ import {
|
|
|
48
48
|
plural,
|
|
49
49
|
select,
|
|
50
50
|
} from '@/ui'
|
|
51
|
+
import { describeSkew, readSkew, type SkewReport } from '@/version/skew'
|
|
51
52
|
|
|
52
53
|
const GREEN = '\x1b[0;32m'
|
|
53
54
|
const GREY = '\x1b[0;90m'
|
|
@@ -249,14 +250,18 @@ export function register(program: Command): void {
|
|
|
249
250
|
'copy. Passing a ref older than the oldest load over-reports, which is',
|
|
250
251
|
'the safe direction. Confirm a name by reading the body.',
|
|
251
252
|
'',
|
|
253
|
+
'Every run also reports the installed version against the newest',
|
|
254
|
+
'published one. That report never changes the exit code, so an offline',
|
|
255
|
+
'machine reads it as unknown rather than as a failure.',
|
|
256
|
+
'',
|
|
252
257
|
'Examples:',
|
|
253
258
|
' aitk claude skills drift HEAD~20',
|
|
254
259
|
' aitk claude skills drift 02d7b265 --json',
|
|
255
260
|
'',
|
|
256
261
|
].join('\n'),
|
|
257
262
|
)
|
|
258
|
-
.action((ref: string, opts: SkillsDriftOptions) => {
|
|
259
|
-
process.exitCode = runSkillsDrift(ref, opts)
|
|
263
|
+
.action(async (ref: string, opts: SkillsDriftOptions) => {
|
|
264
|
+
process.exitCode = await runSkillsDrift(ref, opts)
|
|
260
265
|
})
|
|
261
266
|
}
|
|
262
267
|
|
|
@@ -506,16 +511,28 @@ function runSkillsList(opts: SkillsListOptions): number {
|
|
|
506
511
|
* it answers every run with nothing moved, which is the silence this reports
|
|
507
512
|
* against.
|
|
508
513
|
*/
|
|
509
|
-
function runSkillsDrift(
|
|
514
|
+
async function runSkillsDrift(
|
|
515
|
+
ref: string,
|
|
516
|
+
opts: SkillsDriftOptions,
|
|
517
|
+
): Promise<number> {
|
|
510
518
|
const root = process.cwd()
|
|
511
519
|
const report = readDrift(root, ref)
|
|
520
|
+
const skew = await readSkew()
|
|
512
521
|
|
|
513
522
|
if (report.kind === 'measured') {
|
|
514
523
|
intro('aitk claude skills drift')
|
|
524
|
+
reportSkew(skew)
|
|
515
525
|
reportDrift(report, ref)
|
|
516
526
|
outro()
|
|
517
527
|
} else {
|
|
518
528
|
frameError(report.reason)
|
|
529
|
+
// The refusal path names the binary too. A project consuming the plugin
|
|
530
|
+
// from a marketplace cache is refused here for having no history, and that
|
|
531
|
+
// is the moment a skew warning is worth most, since an old binary is one
|
|
532
|
+
// reason the cache and the CLI disagree in the first place.
|
|
533
|
+
if (skew.state === 'behind') {
|
|
534
|
+
process.stderr.write(`${GREY}${describeSkew(skew)}${NC}\n`)
|
|
535
|
+
}
|
|
519
536
|
}
|
|
520
537
|
|
|
521
538
|
if (opts.json) {
|
|
@@ -528,8 +545,9 @@ function runSkillsDrift(ref: string, opts: SkillsDriftOptions): number {
|
|
|
528
545
|
base: report.base,
|
|
529
546
|
head: report.head,
|
|
530
547
|
moved: report.moved,
|
|
548
|
+
skew,
|
|
531
549
|
}
|
|
532
|
-
: { root, ref, unreadable: report.reason },
|
|
550
|
+
: { root, ref, unreadable: report.reason, skew },
|
|
533
551
|
)}\n`,
|
|
534
552
|
)
|
|
535
553
|
}
|
|
@@ -537,6 +555,18 @@ function runSkillsDrift(ref: string, opts: SkillsDriftOptions): number {
|
|
|
537
555
|
return report.kind === 'measured' ? 0 : 1
|
|
538
556
|
}
|
|
539
557
|
|
|
558
|
+
/**
|
|
559
|
+
* The binary reports before the range does, for the reason the range section
|
|
560
|
+
* states about itself: the command answers what changed on disk, and a binary
|
|
561
|
+
* behind the published one is a second way the tree a session reads differs
|
|
562
|
+
* from the tree it holds.
|
|
563
|
+
*/
|
|
564
|
+
function reportSkew(skew: SkewReport): void {
|
|
565
|
+
logStep('Toolkit version')
|
|
566
|
+
if (skew.state === 'behind') logWarn(describeSkew(skew))
|
|
567
|
+
else logInfo(describeSkew(skew))
|
|
568
|
+
}
|
|
569
|
+
|
|
540
570
|
/**
|
|
541
571
|
* States the bound on every run, including the run that names nothing. A report
|
|
542
572
|
* listing only what moved reads as a verdict on what a session holds, and the
|
package/src/commands/sync.ts
CHANGED
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
logWarn,
|
|
31
31
|
outro,
|
|
32
32
|
} from '@/ui'
|
|
33
|
+
import { describeSkew } from '@/version/skew'
|
|
33
34
|
|
|
34
35
|
const GREY = '\x1b[0;90m'
|
|
35
36
|
const YELLOW = '\x1b[0;33m'
|
|
@@ -100,6 +101,8 @@ async function runCheck(target: string, options: SyncOptions): Promise<number> {
|
|
|
100
101
|
function renderCheck(report: CheckReport): void {
|
|
101
102
|
intro('aitk sync --check')
|
|
102
103
|
|
|
104
|
+
renderSkew(report)
|
|
105
|
+
|
|
103
106
|
if (!report.managed) {
|
|
104
107
|
logStep('Not a toolkit project')
|
|
105
108
|
logWarn('No .claude/ directory and no CLAUDE.md at the target.')
|
|
@@ -182,6 +185,24 @@ function renderCheck(report: CheckReport): void {
|
|
|
182
185
|
)
|
|
183
186
|
}
|
|
184
187
|
|
|
188
|
+
/**
|
|
189
|
+
* The binary reports before any domain does, since a stale binary is what makes
|
|
190
|
+
* every section below it a reading from the wrong toolkit. It prints on all
|
|
191
|
+
* three states rather than only when behind: a check that goes quiet when the
|
|
192
|
+
* registry is unreachable is indistinguishable from one that found nothing.
|
|
193
|
+
*/
|
|
194
|
+
function renderSkew(report: CheckReport): void {
|
|
195
|
+
const { skew } = report
|
|
196
|
+
logStep('toolkit version')
|
|
197
|
+
|
|
198
|
+
if (skew.state === 'behind') {
|
|
199
|
+
logWarn(describeSkew(skew))
|
|
200
|
+
return
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
logInfo(describeSkew(skew))
|
|
204
|
+
}
|
|
205
|
+
|
|
185
206
|
/**
|
|
186
207
|
* Tooling prints whether it was measured before it prints any count, because a
|
|
187
208
|
* target with no chain recorded produces the same zero a current target does.
|