@erclx/aitk 0.44.0 → 0.45.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/claude-autoship/SKILL.md +15 -10
- package/claude/skills/claude-docs/SKILL.md +16 -2
- package/claude/skills/claude-memory-capture/REQUIREMENT.md +14 -4
- package/claude/skills/claude-memory-capture/SKILL.md +45 -16
- package/claude/skills/claude-memory-review/REQUIREMENT.md +8 -2
- package/claude/skills/claude-memory-review/SKILL.md +41 -25
- package/claude/skills/claude-orchestrate/SKILL.md +5 -0
- package/claude/skills/git-ship/SKILL.md +14 -11
- package/claude/skills/session-resume/SKILL.md +2 -2
- package/docs/agents/commands.md +36 -35
- package/docs/agents/index.md +1 -0
- package/docs/agents/indexes.md +3 -1
- package/docs/agents/scripting.md +4 -3
- package/docs/agents/skills-audit.md +55 -0
- package/docs/ai-workflow.md +3 -2
- package/package.json +1 -1
- package/scripts/core/verify.sh +8 -0
- package/src/claude/seeds.ts +1 -0
- package/src/claude/skills-audit.ts +215 -0
- package/src/claude/skills-list.ts +3 -3
- package/src/commands/claude.ts +283 -5
- package/src/commands/context.ts +1 -4
- package/src/ui.ts +5 -0
- package/tooling/claude/reference.md +11 -1
- package/tooling/claude/seeds/.claude/hooks/memory-index.sh +60 -0
- package/tooling/claude/seeds/.claude/memory/index.md +8 -0
- package/tooling/claude/seeds/.claude/settings.json +4 -0
- package/tooling/claude/seeds/CLAUDE.md +3 -0
package/src/commands/claude.ts
CHANGED
|
@@ -12,6 +12,15 @@ import {
|
|
|
12
12
|
type Seed,
|
|
13
13
|
} from '@/claude/seeds'
|
|
14
14
|
import { listSeeds, readSeedContents } from '@/claude/seeds-list'
|
|
15
|
+
import {
|
|
16
|
+
auditExitCode,
|
|
17
|
+
auditSkills,
|
|
18
|
+
CORPORA,
|
|
19
|
+
DESCRIPTION_LIMIT,
|
|
20
|
+
REQUIREMENT_SECTIONS,
|
|
21
|
+
type SkillFinding,
|
|
22
|
+
type SkillsAudit,
|
|
23
|
+
} from '@/claude/skills-audit'
|
|
15
24
|
import { listSkills } from '@/claude/skills-list'
|
|
16
25
|
import {
|
|
17
26
|
planSettings,
|
|
@@ -24,6 +33,7 @@ import { execScript, PROJECT_ROOT } from '@/exec'
|
|
|
24
33
|
import { isDirectory, resolveTarget } from '@/target'
|
|
25
34
|
import { injectGitignore, pruneGitignore } from '@/tooling/inject'
|
|
26
35
|
import {
|
|
36
|
+
frameError,
|
|
27
37
|
intro,
|
|
28
38
|
isNonInteractive,
|
|
29
39
|
logAdd,
|
|
@@ -32,6 +42,8 @@ import {
|
|
|
32
42
|
logStep,
|
|
33
43
|
logWarn,
|
|
34
44
|
outro,
|
|
45
|
+
pipeOutput,
|
|
46
|
+
plural,
|
|
35
47
|
select,
|
|
36
48
|
} from '@/ui'
|
|
37
49
|
|
|
@@ -49,12 +61,17 @@ interface SkillsListOptions {
|
|
|
49
61
|
readonly names?: boolean
|
|
50
62
|
}
|
|
51
63
|
|
|
64
|
+
interface SkillsAuditOptions {
|
|
65
|
+
readonly json?: boolean
|
|
66
|
+
readonly requirementsOnly?: boolean
|
|
67
|
+
}
|
|
68
|
+
|
|
52
69
|
const SEEDED_FILES: readonly string[] = [
|
|
53
70
|
'ARCHITECTURE.md',
|
|
54
71
|
'REQUIREMENTS.md',
|
|
55
72
|
'DESIGN.md',
|
|
56
73
|
]
|
|
57
|
-
const SEEDED_DIRS: readonly string[] = ['tasks', 'wireframes']
|
|
74
|
+
const SEEDED_DIRS: readonly string[] = ['memory', 'tasks', 'wireframes']
|
|
58
75
|
const USER_DIR = join('tooling', 'claude', 'user')
|
|
59
76
|
const STATUSLINE = 'statusline-command.sh'
|
|
60
77
|
|
|
@@ -139,15 +156,15 @@ export function register(program: Command): void {
|
|
|
139
156
|
|
|
140
157
|
const skills = claude
|
|
141
158
|
.command('skills')
|
|
142
|
-
.description('Plugin skill catalog (list)')
|
|
143
|
-
.argument('[subcommand]', "
|
|
159
|
+
.description('Plugin skill catalog (list, audit)')
|
|
160
|
+
.argument('[subcommand]', "One of 'list' or 'audit'")
|
|
144
161
|
.helpOption('-h, --help', 'Show this help message')
|
|
145
162
|
.action((subcommand: string | undefined) => {
|
|
146
163
|
intro('aitk claude')
|
|
147
164
|
logError(
|
|
148
165
|
subcommand === undefined
|
|
149
|
-
? "Missing subcommand. Use 'list'."
|
|
150
|
-
: `Unknown subcommand: ${subcommand}. Use 'list'.`,
|
|
166
|
+
? "Missing subcommand. Use 'list' or 'audit'."
|
|
167
|
+
: `Unknown subcommand: ${subcommand}. Use 'list' or 'audit'.`,
|
|
151
168
|
)
|
|
152
169
|
outro()
|
|
153
170
|
process.exitCode = 1
|
|
@@ -172,6 +189,41 @@ export function register(program: Command): void {
|
|
|
172
189
|
.action((opts: SkillsListOptions) => {
|
|
173
190
|
process.exitCode = runSkillsList(opts)
|
|
174
191
|
})
|
|
192
|
+
|
|
193
|
+
skills
|
|
194
|
+
.command('audit')
|
|
195
|
+
.description(
|
|
196
|
+
'Report both skill corpora against the mechanical rules in standards/skill.md',
|
|
197
|
+
)
|
|
198
|
+
.argument('[path]', 'Project root, defaulting to the current directory')
|
|
199
|
+
.helpOption('-h, --help', 'Show this help message')
|
|
200
|
+
.option('--json', 'Add a machine-readable record on stdout')
|
|
201
|
+
.option(
|
|
202
|
+
'--requirements-only',
|
|
203
|
+
'Run the gating requirement-presence check alone',
|
|
204
|
+
)
|
|
205
|
+
.addHelpText(
|
|
206
|
+
'after',
|
|
207
|
+
[
|
|
208
|
+
'',
|
|
209
|
+
'Exit codes:',
|
|
210
|
+
' 0 the audit completed with every skill carrying a requirement',
|
|
211
|
+
' 1 refused, with the reason on stderr',
|
|
212
|
+
' 2 a skill folder carries no REQUIREMENT.md',
|
|
213
|
+
'',
|
|
214
|
+
'Only a missing REQUIREMENT.md sets a failing exit code. Name, description,',
|
|
215
|
+
'folder, and requirement-section findings are advisory.',
|
|
216
|
+
'',
|
|
217
|
+
'Examples:',
|
|
218
|
+
' aitk claude skills audit',
|
|
219
|
+
' aitk claude skills audit --json',
|
|
220
|
+
' aitk claude skills audit --requirements-only',
|
|
221
|
+
'',
|
|
222
|
+
].join('\n'),
|
|
223
|
+
)
|
|
224
|
+
.action(async (path: string | undefined, opts: SkillsAuditOptions) => {
|
|
225
|
+
process.exitCode = await runSkillsAudit(path, opts)
|
|
226
|
+
})
|
|
175
227
|
}
|
|
176
228
|
|
|
177
229
|
function succeed(message: string): number {
|
|
@@ -413,3 +465,229 @@ function runSkillsList(opts: SkillsListOptions): number {
|
|
|
413
465
|
outro()
|
|
414
466
|
return 0
|
|
415
467
|
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Measures the tree at the cwd rather than the toolkit root the catalog reads,
|
|
471
|
+
* so a linked worktree audits its own branch instead of reporting on `main`. A
|
|
472
|
+
* target carrying `.claude/skills/` alone is in scope for the same reason.
|
|
473
|
+
*/
|
|
474
|
+
async function runSkillsAudit(
|
|
475
|
+
path: string | undefined,
|
|
476
|
+
opts: SkillsAuditOptions,
|
|
477
|
+
): Promise<number> {
|
|
478
|
+
const root = resolve(path ?? process.cwd())
|
|
479
|
+
const gateOnly = opts.requirementsOnly ?? false
|
|
480
|
+
const report = await auditSkills(root)
|
|
481
|
+
|
|
482
|
+
if (report.corpora.length === 0) {
|
|
483
|
+
return refuseAudit(
|
|
484
|
+
`No skill corpus under ${root}. Looked for ${CORPORA.join(' and ')}.`,
|
|
485
|
+
gateOnly,
|
|
486
|
+
)
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
if (gateOnly) {
|
|
490
|
+
reportRequirementGate(report)
|
|
491
|
+
} else {
|
|
492
|
+
intro('aitk claude skills audit')
|
|
493
|
+
reportScope(report)
|
|
494
|
+
reportRequirements(report)
|
|
495
|
+
reportFrontmatter(report)
|
|
496
|
+
reportFolder(report)
|
|
497
|
+
reportRequirementShape(report)
|
|
498
|
+
reportUnmeasured()
|
|
499
|
+
outro()
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (opts.json) {
|
|
503
|
+
process.stdout.write(
|
|
504
|
+
`${JSON.stringify({
|
|
505
|
+
root,
|
|
506
|
+
corpora: report.corpora.map((corpus) => ({
|
|
507
|
+
path: corpus.rel,
|
|
508
|
+
skills: corpus.skills,
|
|
509
|
+
})),
|
|
510
|
+
skills: report.skills,
|
|
511
|
+
findings: {
|
|
512
|
+
missingRequirement: report.missingRequirement,
|
|
513
|
+
nameMismatch: report.nameMismatch,
|
|
514
|
+
missingDescription: report.missingDescription,
|
|
515
|
+
longDescription: report.longDescription,
|
|
516
|
+
readme: report.readme,
|
|
517
|
+
folderName: report.folderName,
|
|
518
|
+
requirementSections: report.requirementSections,
|
|
519
|
+
},
|
|
520
|
+
checkpoints: {
|
|
521
|
+
descriptionLimit: DESCRIPTION_LIMIT,
|
|
522
|
+
requirementSections: REQUIREMENT_SECTIONS,
|
|
523
|
+
corpora: CORPORA,
|
|
524
|
+
},
|
|
525
|
+
})}\n`,
|
|
526
|
+
)
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
return auditExitCode(report)
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function refuseAudit(message: string, gateOnly: boolean): number {
|
|
533
|
+
if (gateOnly) {
|
|
534
|
+
frameError(message)
|
|
535
|
+
return 1
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
intro('aitk claude skills audit')
|
|
539
|
+
logStep('Refused')
|
|
540
|
+
logWarn(message)
|
|
541
|
+
outro()
|
|
542
|
+
return 1
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Prints nothing when every skill carries a requirement.
|
|
547
|
+
*
|
|
548
|
+
* `--requirements-only` is what `verify.sh` runs on every push, and that script
|
|
549
|
+
* pipes a stage's whole output into its own frame. A passing gate that printed
|
|
550
|
+
* its frame would nest one inside the other on every contributor's push.
|
|
551
|
+
*/
|
|
552
|
+
function reportRequirementGate(report: SkillsAudit): void {
|
|
553
|
+
const missing = report.missingRequirement
|
|
554
|
+
if (missing.length === 0) return
|
|
555
|
+
|
|
556
|
+
intro('aitk claude skills audit')
|
|
557
|
+
logError(
|
|
558
|
+
missing.length === 1
|
|
559
|
+
? '1 skill folder carries no REQUIREMENT.md'
|
|
560
|
+
: `${missing.length} skill folders carry no REQUIREMENT.md`,
|
|
561
|
+
)
|
|
562
|
+
pipeOutput(missing.join('\n'))
|
|
563
|
+
outro()
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function reportFindings(findings: readonly SkillFinding[]): void {
|
|
567
|
+
pipeOutput(
|
|
568
|
+
findings.map((found) => `${found.rel} ${found.detail}`).join('\n'),
|
|
569
|
+
)
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Names each corpus that resolved, since a corpus the tree does not carry is
|
|
574
|
+
* skipped silently and a count taken over one of the two reads as the whole.
|
|
575
|
+
*/
|
|
576
|
+
function reportScope(report: SkillsAudit): void {
|
|
577
|
+
logStep('Scope')
|
|
578
|
+
for (const corpus of report.corpora) {
|
|
579
|
+
logInfo(`${corpus.rel}: ${plural(corpus.skills, 'skill')}`)
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function reportRequirements(report: SkillsAudit): void {
|
|
584
|
+
logStep('Requirements')
|
|
585
|
+
logInfo('Every skill folder carries REQUIREMENT.md beside SKILL.md.')
|
|
586
|
+
logInfo('This is the only measure here that fails a run.')
|
|
587
|
+
|
|
588
|
+
if (report.missingRequirement.length === 0) {
|
|
589
|
+
logInfo('Every skill carries one.')
|
|
590
|
+
return
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
logWarn(`${plural(report.missingRequirement.length, 'skill')} without one`)
|
|
594
|
+
pipeOutput(report.missingRequirement.join('\n'))
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function reportFrontmatter(report: SkillsAudit): void {
|
|
598
|
+
logStep('Frontmatter')
|
|
599
|
+
logInfo(
|
|
600
|
+
`name matches the folder, and description is present and under ${DESCRIPTION_LIMIT} characters.`,
|
|
601
|
+
)
|
|
602
|
+
logInfo('A body whose frontmatter does not parse reads as declaring neither.')
|
|
603
|
+
|
|
604
|
+
const findings =
|
|
605
|
+
report.nameMismatch.length +
|
|
606
|
+
report.missingDescription.length +
|
|
607
|
+
report.longDescription.length
|
|
608
|
+
if (findings === 0) {
|
|
609
|
+
logInfo('Every body declares both fields.')
|
|
610
|
+
return
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
if (report.nameMismatch.length > 0) {
|
|
614
|
+
logWarn(
|
|
615
|
+
`${plural(report.nameMismatch.length, 'body')} whose name does not match its folder`,
|
|
616
|
+
)
|
|
617
|
+
reportFindings(report.nameMismatch)
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
if (report.missingDescription.length > 0) {
|
|
621
|
+
logWarn(
|
|
622
|
+
`${plural(report.missingDescription.length, 'body')} without a description`,
|
|
623
|
+
)
|
|
624
|
+
pipeOutput(report.missingDescription.join('\n'))
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
if (report.longDescription.length > 0) {
|
|
628
|
+
logWarn(
|
|
629
|
+
`${plural(report.longDescription.length, 'description')} past the ${DESCRIPTION_LIMIT}-character ceiling`,
|
|
630
|
+
)
|
|
631
|
+
reportFindings(report.longDescription)
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function reportFolder(report: SkillsAudit): void {
|
|
636
|
+
logStep('Folder')
|
|
637
|
+
logInfo(
|
|
638
|
+
'No README.md inside a skill folder, and a folder name in kebab-case carrying no capital or underscore.',
|
|
639
|
+
)
|
|
640
|
+
|
|
641
|
+
if (report.readme.length === 0 && report.folderName.length === 0) {
|
|
642
|
+
logInfo('Every folder conforms.')
|
|
643
|
+
return
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
if (report.readme.length > 0) {
|
|
647
|
+
logWarn(`${plural(report.readme.length, 'folder')} carrying a README.md`)
|
|
648
|
+
pipeOutput(report.readme.join('\n'))
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
if (report.folderName.length > 0) {
|
|
652
|
+
logWarn(
|
|
653
|
+
`${plural(report.folderName.length, 'folder name')} outside kebab-case`,
|
|
654
|
+
)
|
|
655
|
+
pipeOutput(report.folderName.join('\n'))
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function reportRequirementShape(report: SkillsAudit): void {
|
|
660
|
+
logStep('Requirement shape')
|
|
661
|
+
logInfo(
|
|
662
|
+
`Each REQUIREMENT.md declares ${REQUIREMENT_SECTIONS.join(' and ')}, matched at any heading level.`,
|
|
663
|
+
)
|
|
664
|
+
logInfo(
|
|
665
|
+
'A folder carrying no requirement is reported above rather than counted twice here.',
|
|
666
|
+
)
|
|
667
|
+
|
|
668
|
+
if (report.requirementSections.length === 0) {
|
|
669
|
+
logInfo('Every requirement declares both.')
|
|
670
|
+
return
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
logWarn(
|
|
674
|
+
`${plural(report.requirementSections.length, 'requirement')} short a declared section`,
|
|
675
|
+
)
|
|
676
|
+
reportFindings(report.requirementSections)
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* Stated on every run, including the run where everything above passed. A
|
|
681
|
+
* report that lists only what it measured reads as a verdict on the standard
|
|
682
|
+
* rather than on the half of it a parser can reach.
|
|
683
|
+
*/
|
|
684
|
+
function reportUnmeasured(): void {
|
|
685
|
+
logStep('Unmeasured')
|
|
686
|
+
logInfo(
|
|
687
|
+
'The standard states rules no parser reads, and a pass above says nothing about them.',
|
|
688
|
+
)
|
|
689
|
+
logInfo(
|
|
690
|
+
'Whether each Must traces to a stated gap, whether a gap reads as an observed failure rather than an intent, and whether a description routes.',
|
|
691
|
+
)
|
|
692
|
+
logInfo('The 5,000-word body ceiling is mechanical and still absent here.')
|
|
693
|
+
}
|
package/src/commands/context.ts
CHANGED
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
logWarn,
|
|
32
32
|
outro,
|
|
33
33
|
pipeOutput,
|
|
34
|
+
plural,
|
|
34
35
|
} from '@/ui'
|
|
35
36
|
|
|
36
37
|
/** Returned when an unresolved citation is found, which is the gating check. */
|
|
@@ -253,10 +254,6 @@ function reportGate(report: ScannedCitations): void {
|
|
|
253
254
|
outro()
|
|
254
255
|
}
|
|
255
256
|
|
|
256
|
-
function plural(count: number, noun: string): string {
|
|
257
|
-
return `${count} ${noun}${count === 1 ? '' : 's'}`
|
|
258
|
-
}
|
|
259
|
-
|
|
260
257
|
/**
|
|
261
258
|
* Names the resolved path of every audited folder, plus the requested names
|
|
262
259
|
* that resolved nowhere.
|
package/src/ui.ts
CHANGED
|
@@ -59,6 +59,11 @@ export function pipeOutput(text: string): void {
|
|
|
59
59
|
)
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
/** Counts a noun for a report line, where the plural is the bare `s` form. */
|
|
63
|
+
export function plural(count: number, noun: string): string {
|
|
64
|
+
return `${count} ${noun}${count === 1 ? '' : 's'}`
|
|
65
|
+
}
|
|
66
|
+
|
|
62
67
|
export function frameError(message: string): void {
|
|
63
68
|
process.stderr.write(
|
|
64
69
|
`${GREY}┌${NC}\n${GREY}│${NC} ${RED}✗${NC} ${message}\n${GREY}└${NC}\n`,
|
|
@@ -20,7 +20,7 @@ The claude stack installs the `.claude/` workflow directory into a project. Stat
|
|
|
20
20
|
├── plans/ ← execution detail for multi-step tasks, gitignored. `feature-*.md` entries swept by claude-docs.
|
|
21
21
|
├── review/ ← scratch for claude-review and claude-ui-test output, gitignored
|
|
22
22
|
├── .tmp/ ← ephemeral scratch space, gitignored
|
|
23
|
-
└── memory/ ← session
|
|
23
|
+
└── memory/ ← session facts no context entry owns, gitignored. `index.md` regenerated by a hook.
|
|
24
24
|
```
|
|
25
25
|
|
|
26
26
|
## Upgrading from a single-file board
|
|
@@ -33,6 +33,16 @@ Convert by hand, once per project:
|
|
|
33
33
|
2. Run `aitk indexes regen --no-stage --root . .claude/tasks/<any-task>.md` to build the catalog.
|
|
34
34
|
3. Delete `.claude/TASKS.md`, and swap its `.gitignore` entry for `.claude/tasks/`.
|
|
35
35
|
|
|
36
|
+
## Upgrading a hand-appended memory index
|
|
37
|
+
|
|
38
|
+
A project installed before the memory folder gained a generated index still holds `.claude/memory/MEMORY.md`, and its entries still carry `name` and `type` frontmatter. Nothing migrates it. `claude-memory-capture` stops appending rows once the new seed lands, so the old file freezes at whatever it held while the folder keeps growing past it.
|
|
39
|
+
|
|
40
|
+
Convert by hand, once per project:
|
|
41
|
+
|
|
42
|
+
1. Rewrite each entry's `name` key to `title` and its `type` key to a sentence-case `category`, quoting any `description` that opens with a backtick or a colon so the frontmatter parses.
|
|
43
|
+
2. Replace `MEMORY.md` with an `index.md` carrying `title` and `subtitle` frontmatter and nothing else.
|
|
44
|
+
3. Run `aitk indexes regen --no-stage --root . .claude/memory/index.md` to build the catalog, and compare its entry count against the file count before deleting anything.
|
|
45
|
+
|
|
36
46
|
## Upgrading from a single-file diagram set
|
|
37
47
|
|
|
38
48
|
A project installed before the diagram surface became a folder still holds `.claude/DIAGRAMS.md`. Unlike the board, this one migrates itself. The `claude-diagram` skill reads the flat file when `.claude/diagrams/` holds no entries, splits it by kind into the folder, and reports what it wrote. The old file stays on disk so the split can be compared against its source, and deleting it is a manual step once that check passes.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
# Regenerates .claude/memory/index.md after a memory file changes.
|
|
4
|
+
#
|
|
5
|
+
# The memory folder is gitignored, so the whole-repo walk in `bun run check`
|
|
6
|
+
# drops it and never regenerates this index. A positional path bypasses that
|
|
7
|
+
# filter, which makes this hook the only trigger that reaches the folder.
|
|
8
|
+
|
|
9
|
+
input=$(cat)
|
|
10
|
+
|
|
11
|
+
tool=$(printf '%s' "$input" | jq -r '.tool_name // empty')
|
|
12
|
+
case "$tool" in
|
|
13
|
+
Write | Edit | MultiEdit) ;;
|
|
14
|
+
*) exit 0 ;;
|
|
15
|
+
esac
|
|
16
|
+
|
|
17
|
+
file_path=$(printf '%s' "$input" | jq -r '.tool_input.file_path // empty')
|
|
18
|
+
[ -n "$file_path" ] || exit 0
|
|
19
|
+
|
|
20
|
+
case "$file_path" in
|
|
21
|
+
*/.claude/memory/*.md) ;;
|
|
22
|
+
*) exit 0 ;;
|
|
23
|
+
esac
|
|
24
|
+
|
|
25
|
+
case "$file_path" in
|
|
26
|
+
*/.claude/memory/index.md) exit 0 ;;
|
|
27
|
+
esac
|
|
28
|
+
|
|
29
|
+
# Report a missing CLI rather than exiting quietly. The path guard above already
|
|
30
|
+
# scopes this to a memory-file edit, so the message only fires where the stale
|
|
31
|
+
# index it warns about is the actual outcome.
|
|
32
|
+
if ! command -v aitk >/dev/null 2>&1; then
|
|
33
|
+
jq -nc --arg msg 'aitk is not on PATH, so .claude/memory/index.md was not regenerated and is now stale. Install the toolkit CLI or run aitk indexes regen by hand.' \
|
|
34
|
+
'{hookSpecificOutput:{hookEventName:"PostToolUse",additionalContext:$msg}}'
|
|
35
|
+
exit 0
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
# The walk-up boundary has to come from the path, not from the session. Shared
|
|
39
|
+
# scratch resolves at the main worktree root, so a session inside a linked
|
|
40
|
+
# worktree passes a path that sits outside its own project directory and the
|
|
41
|
+
# default boundary would reject it.
|
|
42
|
+
root="${file_path%/.claude/memory/*}"
|
|
43
|
+
[ -n "$root" ] || exit 0
|
|
44
|
+
|
|
45
|
+
# `--no-stage` because a hook has no business touching the index. On a project
|
|
46
|
+
# whose memory folder is not gitignored, the default auto-stage would silently
|
|
47
|
+
# add memory files to whatever commit is being assembled.
|
|
48
|
+
output=$(aitk indexes regen --no-stage --root "$root" "$file_path" 2>&1) && exit 0
|
|
49
|
+
|
|
50
|
+
# Regen failed, which on this folder means a memory file is missing `title` or
|
|
51
|
+
# `description`. Report it. Nothing else can: the folder is gitignored, so the
|
|
52
|
+
# whole-repo walk never reaches it and no gate stage will ever fail on a stale
|
|
53
|
+
# index. Staying quiet here is what makes the drift permanent.
|
|
54
|
+
errors=$(printf '%s\n' "$output" | grep '^ERROR: ' | head -5)
|
|
55
|
+
[ -n "$errors" ] || errors="$output"
|
|
56
|
+
|
|
57
|
+
msg="Memory index regen failed, so .claude/memory/index.md is now stale. Fix the frontmatter and save again. $errors"
|
|
58
|
+
jq -nc --arg msg "$msg" \
|
|
59
|
+
'{hookSpecificOutput:{hookEventName:"PostToolUse",additionalContext:$msg}}'
|
|
60
|
+
exit 0
|
|
@@ -72,9 +72,12 @@
|
|
|
72
72
|
## Memory
|
|
73
73
|
|
|
74
74
|
- Write all memory files to `.claude/memory/`, not `~/.claude/projects/`
|
|
75
|
+
- A fact about a domain goes to that domain's `.claude/context/` entry, not to memory. `claude-memory-capture` routes it there and `claude-docs` folds it in. Memory keeps only what no context entry owns.
|
|
75
76
|
- Save a feedback memory only when the same mistake happens twice in the session, or when the user explicitly corrects you. First-occurrence slips are noise.
|
|
76
77
|
- Keep feedback memories to 3 lines: the rule, a one-line Why, and a one-line How to apply. Capture the pattern, not the recovery narrative.
|
|
77
78
|
- Before creating a new memory file, check for an existing one on the same topic. Update rather than duplicate.
|
|
79
|
+
- Give every entry `title`, `description`, and a sentence-case `category`. Never hand-edit `.claude/memory/index.md`. A hook regenerates it from sibling frontmatter.
|
|
80
|
+
- Never delete a memory entry. `claude-memory-review` moves a retired one to `.claude/.tmp/memory-archive/`.
|
|
78
81
|
|
|
79
82
|
## Scratch
|
|
80
83
|
|