@erclx/aitk 0.43.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.
Files changed (35) hide show
  1. package/claude/.claude-plugin/plugin.json +1 -1
  2. package/claude/skills/claude-autoship/SKILL.md +15 -10
  3. package/claude/skills/claude-docs/SKILL.md +16 -2
  4. package/claude/skills/claude-memory-capture/REQUIREMENT.md +14 -4
  5. package/claude/skills/claude-memory-capture/SKILL.md +45 -16
  6. package/claude/skills/claude-memory-review/REQUIREMENT.md +8 -2
  7. package/claude/skills/claude-memory-review/SKILL.md +41 -25
  8. package/claude/skills/claude-orchestrate/SKILL.md +5 -0
  9. package/claude/skills/git-ship/SKILL.md +14 -11
  10. package/claude/skills/session-resume/SKILL.md +2 -2
  11. package/docs/agents/commands.md +36 -34
  12. package/docs/agents/index.md +2 -1
  13. package/docs/agents/indexes.md +3 -1
  14. package/docs/agents/scripting.md +4 -3
  15. package/docs/agents/skills-audit.md +55 -0
  16. package/docs/agents/tasks.md +33 -1
  17. package/docs/ai-workflow.md +5 -2
  18. package/package.json +1 -1
  19. package/scripts/core/verify.sh +8 -0
  20. package/snippets/claude/orchestrator-sweep.md +2 -0
  21. package/src/claude/seeds.ts +1 -0
  22. package/src/claude/skills-audit.ts +215 -0
  23. package/src/claude/skills-list.ts +3 -3
  24. package/src/commands/claude.ts +283 -5
  25. package/src/commands/context.ts +1 -4
  26. package/src/commands/tasks.ts +113 -0
  27. package/src/tasks/archive.ts +5 -3
  28. package/src/tasks/validate.ts +401 -0
  29. package/src/ui.ts +5 -0
  30. package/standards/tasks.md +5 -0
  31. package/tooling/claude/reference.md +11 -1
  32. package/tooling/claude/seeds/.claude/hooks/memory-index.sh +60 -0
  33. package/tooling/claude/seeds/.claude/memory/index.md +8 -0
  34. package/tooling/claude/seeds/.claude/settings.json +4 -0
  35. package/tooling/claude/seeds/CLAUDE.md +3 -0
@@ -0,0 +1,401 @@
1
+ import { existsSync } from 'node:fs'
2
+ import { readdir, readFile } from 'node:fs/promises'
3
+ import { join, resolve } from 'node:path'
4
+ import { RESERVED_STEMS, tasksDir } from '@/tasks/archive'
5
+
6
+ const ORDERING_FILE = 'priority.md'
7
+
8
+ /**
9
+ * The readiness headings `.claude/standards/tasks.md` fixes. The names are the
10
+ * contract rather than a suggestion, so a board grouping under names of its own
11
+ * reads as carrying no group at all and the run refuses instead of reporting a
12
+ * clean board it never parsed.
13
+ */
14
+ export const BOARD_GROUPS = ['Run now', 'Up next', 'Needs a plan'] as const
15
+
16
+ export type BoardGroup = (typeof BOARD_GROUPS)[number]
17
+
18
+ export const VALIDATE_REFUSALS = [
19
+ 'no-board',
20
+ 'no-ordering',
21
+ 'no-groups',
22
+ ] as const
23
+
24
+ export type ValidateRefusal = (typeof VALIDATE_REFUSALS)[number]
25
+
26
+ export const FINDING_KINDS = [
27
+ 'plan-unstated',
28
+ 'plan-unresolved',
29
+ 'task-unresolved',
30
+ 'row-missing',
31
+ 'row-duplicated',
32
+ 'touches-unstated',
33
+ 'touches-collided',
34
+ ] as const
35
+
36
+ export type FindingKind = (typeof FINDING_KINDS)[number]
37
+
38
+ export interface Finding {
39
+ readonly kind: FindingKind
40
+ readonly group: BoardGroup | undefined
41
+ readonly subject: string
42
+ readonly message: string
43
+ }
44
+
45
+ export interface BoardRow {
46
+ readonly group: BoardGroup
47
+ readonly label: string
48
+ readonly stem: string | undefined
49
+ readonly plan: string | undefined
50
+ /** Absent when the group fixes no `Touches` column, empty when it read none. */
51
+ readonly touches: readonly string[] | undefined
52
+ }
53
+
54
+ export interface ValidateReport {
55
+ readonly ok: true
56
+ readonly rows: number
57
+ readonly tasks: number
58
+ readonly findings: readonly Finding[]
59
+ }
60
+
61
+ export interface ValidateRefused {
62
+ readonly ok: false
63
+ readonly reason: ValidateRefusal
64
+ readonly message: string
65
+ }
66
+
67
+ export type ValidateOutcome = ValidateReport | ValidateRefused
68
+
69
+ export function orderingPath(root: string): string {
70
+ return join(tasksDir(root), ORDERING_FILE)
71
+ }
72
+
73
+ /**
74
+ * Pulls the target out of a markdown link, which is how both the `Task` and the
75
+ * `Plan` column spell their pointer. A cell carrying prose instead of a link
76
+ * yields nothing, and that absence is the finding rather than a parse failure.
77
+ */
78
+ function linkTarget(cell: string): string | undefined {
79
+ const match = /\[[^\]]*\]\(([^)]+)\)/.exec(cell)
80
+ return match ? match[1].trim() : undefined
81
+ }
82
+
83
+ function linkText(cell: string): string {
84
+ const match = /\[([^\]]*)\]\([^)]+\)/.exec(cell)
85
+ return (match ? match[1] : cell).trim()
86
+ }
87
+
88
+ function stemOf(target: string): string | undefined {
89
+ const name = target.split('/').pop()
90
+ if (!name || !name.endsWith('.md')) return undefined
91
+ return name.slice(0, -'.md'.length)
92
+ }
93
+
94
+ /**
95
+ * Reads the file set out of a `Touches` cell written as prose. Paths are the
96
+ * backticked spans, which is the only marker the column carries, and a span
97
+ * naming a skill or a command rather than a file is dropped by the same test.
98
+ *
99
+ * An extension opens with a letter, which is what separates `commands.md` from
100
+ * a task version like `v40.2`. Reading the version as a path would collide two
101
+ * rows that merely cite the same task.
102
+ */
103
+ export function readPaths(cell: string): string[] {
104
+ const spans = cell.match(/`[^`]+`/g) ?? []
105
+ const paths = spans
106
+ .map((span) => span.slice(1, -1).trim())
107
+ .filter(
108
+ (span) => span.includes('/') || /\.[A-Za-z][A-Za-z0-9]*$/.test(span),
109
+ )
110
+ .map((span) => span.replace(/^\.\//, '').replace(/\/+$/, ''))
111
+
112
+ return [...new Set(paths)]
113
+ }
114
+
115
+ /**
116
+ * Two paths touch the same work when they are equal or one contains the other.
117
+ * A row naming a directory and a row naming a file inside it collide, which a
118
+ * set intersection on the written strings alone would miss.
119
+ */
120
+ function sharesPath(left: string, right: string): boolean {
121
+ return (
122
+ left === right ||
123
+ left.startsWith(`${right}/`) ||
124
+ right.startsWith(`${left}/`)
125
+ )
126
+ }
127
+
128
+ function splitCells(line: string): string[] {
129
+ return line
130
+ .trim()
131
+ .replace(/^\|/, '')
132
+ .replace(/\|$/, '')
133
+ .split('|')
134
+ .map((cell) => cell.trim())
135
+ }
136
+
137
+ function isSeparator(cells: readonly string[]): boolean {
138
+ return cells.every((cell) => /^:?-{3,}:?$/.test(cell))
139
+ }
140
+
141
+ function columnIndex(header: readonly string[], name: string): number {
142
+ return header.findIndex((cell) => cell.toLowerCase() === name)
143
+ }
144
+
145
+ function isGroup(heading: string): heading is BoardGroup {
146
+ return (BOARD_GROUPS as readonly string[]).includes(heading)
147
+ }
148
+
149
+ /**
150
+ * Parses the ordering file into rows keyed by readiness group. Columns are read
151
+ * from each table's own header rather than by position, so a group whose shape
152
+ * differs from this repository's is reported for what it lacks instead of
153
+ * having its second cell read as something it never was.
154
+ */
155
+ export function readBoard(text: string): {
156
+ readonly rows: readonly BoardRow[]
157
+ readonly groups: readonly BoardGroup[]
158
+ } {
159
+ const rows: BoardRow[] = []
160
+ const groups: BoardGroup[] = []
161
+
162
+ let group: BoardGroup | undefined
163
+ let header: string[] | undefined
164
+
165
+ for (const line of text.split('\n')) {
166
+ const heading = /^##\s+(.+?)\s*$/.exec(line)
167
+ if (heading) {
168
+ const title = heading[1]
169
+ group = isGroup(title) ? title : undefined
170
+ header = undefined
171
+ if (group && !groups.includes(group)) groups.push(group)
172
+ continue
173
+ }
174
+
175
+ if (!group || !line.trimStart().startsWith('|')) continue
176
+
177
+ const cells = splitCells(line)
178
+ if (isSeparator(cells)) continue
179
+
180
+ if (!header) {
181
+ header = cells
182
+ continue
183
+ }
184
+
185
+ const taskAt = columnIndex(header, 'task')
186
+ const planAt = columnIndex(header, 'plan')
187
+ const touchesAt = columnIndex(header, 'touches')
188
+
189
+ const task = taskAt >= 0 ? (cells[taskAt] ?? '') : ''
190
+ const target = linkTarget(task)
191
+ const plan = planAt >= 0 ? linkTarget(cells[planAt] ?? '') : undefined
192
+
193
+ rows.push({
194
+ group,
195
+ label: linkText(task) || task,
196
+ stem: target ? stemOf(target) : undefined,
197
+ plan,
198
+ touches: touchesAt >= 0 ? readPaths(cells[touchesAt] ?? '') : undefined,
199
+ })
200
+ }
201
+
202
+ return { rows, groups }
203
+ }
204
+
205
+ async function listTaskStems(dir: string): Promise<string[]> {
206
+ const entries = await readdir(dir)
207
+ const reserved: readonly string[] = RESERVED_STEMS
208
+
209
+ return entries
210
+ .filter((entry) => entry.endsWith('.md'))
211
+ .map((entry) => entry.slice(0, -'.md'.length))
212
+ .filter((stem) => !reserved.includes(stem))
213
+ .sort()
214
+ }
215
+
216
+ /**
217
+ * Resolves a pointer against the board and against the project root both, the
218
+ * way `claude-docs` reads the same line. A fragment is dropped first, since an
219
+ * anchor is part of the link and never part of the path.
220
+ */
221
+ function resolves(target: string, dir: string, root: string): boolean {
222
+ const path = target.split('#')[0]
223
+ if (!path) return false
224
+
225
+ return existsSync(resolve(dir, path)) || existsSync(resolve(root, path))
226
+ }
227
+
228
+ function checkMapping(
229
+ rows: readonly BoardRow[],
230
+ stems: readonly string[],
231
+ dir: string,
232
+ ): Finding[] {
233
+ const findings: Finding[] = []
234
+ const seen = new Map<string, number>()
235
+
236
+ for (const row of rows) {
237
+ if (!row.stem) {
238
+ findings.push({
239
+ kind: 'task-unresolved',
240
+ group: row.group,
241
+ subject: row.label,
242
+ message: 'names no task file, so the row points at nothing.',
243
+ })
244
+ continue
245
+ }
246
+
247
+ seen.set(row.stem, (seen.get(row.stem) ?? 0) + 1)
248
+
249
+ if (!existsSync(join(dir, `${row.stem}.md`))) {
250
+ findings.push({
251
+ kind: 'task-unresolved',
252
+ group: row.group,
253
+ subject: row.stem,
254
+ message: 'has a row and no task file.',
255
+ })
256
+ }
257
+ }
258
+
259
+ for (const [stem, count] of seen) {
260
+ if (count > 1) {
261
+ findings.push({
262
+ kind: 'row-duplicated',
263
+ group: undefined,
264
+ subject: stem,
265
+ message: `carries ${count} rows. A task belongs to exactly one group.`,
266
+ })
267
+ }
268
+ }
269
+
270
+ for (const stem of stems) {
271
+ if (!seen.has(stem)) {
272
+ findings.push({
273
+ kind: 'row-missing',
274
+ group: undefined,
275
+ subject: stem,
276
+ message: 'is a task file with no row on the board.',
277
+ })
278
+ }
279
+ }
280
+
281
+ return findings
282
+ }
283
+
284
+ function checkPlans(
285
+ rows: readonly BoardRow[],
286
+ dir: string,
287
+ root: string,
288
+ ): Finding[] {
289
+ const findings: Finding[] = []
290
+
291
+ for (const row of rows) {
292
+ if (row.group !== 'Run now') continue
293
+
294
+ if (!row.plan) {
295
+ findings.push({
296
+ kind: 'plan-unstated',
297
+ group: row.group,
298
+ subject: row.stem ?? row.label,
299
+ message:
300
+ 'states no plan pointer, so the readiness claim cannot be checked.',
301
+ })
302
+ continue
303
+ }
304
+
305
+ if (!resolves(row.plan, dir, root)) {
306
+ findings.push({
307
+ kind: 'plan-unresolved',
308
+ group: row.group,
309
+ subject: row.stem ?? row.label,
310
+ message: `points at ${row.plan}, which resolves to no file.`,
311
+ })
312
+ }
313
+ }
314
+
315
+ return findings
316
+ }
317
+
318
+ /**
319
+ * The half of the `## Run now` test a person cannot check by eye. Two rows a
320
+ * worker may be handed at once must touch disjoint files, and the `Touches`
321
+ * column is the only place either set is written down.
322
+ */
323
+ function checkCollisions(rows: readonly BoardRow[]): Finding[] {
324
+ const findings: Finding[] = []
325
+ const ready = rows.filter((row) => row.group === 'Run now')
326
+
327
+ for (const row of ready) {
328
+ // An absent column and an unreadable one both leave the row untested by
329
+ // the loop below, so reporting only the second would pass a board whose
330
+ // `## Run now` table declares no file set at all.
331
+ if (!row.touches || row.touches.length === 0) {
332
+ findings.push({
333
+ kind: 'touches-unstated',
334
+ group: row.group,
335
+ subject: row.stem ?? row.label,
336
+ message: 'names no file, so its collisions cannot be read.',
337
+ })
338
+ }
339
+ }
340
+
341
+ for (let i = 0; i < ready.length; i += 1) {
342
+ for (let j = i + 1; j < ready.length; j += 1) {
343
+ const left = ready[i]
344
+ const right = ready[j]
345
+ const shared = (left.touches ?? []).filter((path) =>
346
+ (right.touches ?? []).some((other) => sharesPath(path, other)),
347
+ )
348
+
349
+ if (shared.length === 0) continue
350
+
351
+ findings.push({
352
+ kind: 'touches-collided',
353
+ group: 'Run now',
354
+ subject: `${left.stem ?? left.label} and ${right.stem ?? right.label}`,
355
+ message: `both touch ${shared.join(', ')}.`,
356
+ })
357
+ }
358
+ }
359
+
360
+ return findings
361
+ }
362
+
363
+ function refuse(reason: ValidateRefusal, message: string): ValidateRefused {
364
+ return { ok: false, reason, message }
365
+ }
366
+
367
+ /**
368
+ * Reports what every board row claims against what the tree holds. It writes
369
+ * nothing: a row is a session's claim about readiness, and a validator that
370
+ * repaired one would be asserting the claim it exists to test.
371
+ */
372
+ export async function validateBoard(root: string): Promise<ValidateOutcome> {
373
+ const dir = tasksDir(root)
374
+ if (!existsSync(dir)) {
375
+ return refuse('no-board', `No task board at ${dir}.`)
376
+ }
377
+
378
+ const ordering = orderingPath(root)
379
+ if (!existsSync(ordering)) {
380
+ return refuse('no-ordering', `No ordering file at ${ordering}.`)
381
+ }
382
+
383
+ const { rows, groups } = readBoard(await readFile(ordering, 'utf8'))
384
+
385
+ if (groups.length === 0) {
386
+ return refuse(
387
+ 'no-groups',
388
+ `No readiness group in ${ORDERING_FILE}. Expected one of: ${BOARD_GROUPS.join(', ')}.`,
389
+ )
390
+ }
391
+
392
+ const stems = await listTaskStems(dir)
393
+
394
+ const findings = [
395
+ ...checkMapping(rows, stems, dir),
396
+ ...checkPlans(rows, dir, root),
397
+ ...checkCollisions(rows),
398
+ ]
399
+
400
+ return { ok: true, rows: rows.length, tasks: stems.length, findings }
401
+ }
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`,
@@ -26,12 +26,15 @@ Does not govern:
26
26
  .claude/tasks/
27
27
  ├── index.md ← generated, never hand-edited
28
28
  ├── priority.md ← hand-maintained execution order
29
+ ├── session.md ← optional, what a compaction is about to destroy
29
30
  ├── v09.0-sync-paths.md
30
31
  └── v13.0-toolkit-drift.md
31
32
  ```
32
33
 
33
34
  One file per task is what keeps the board safe under parallel sessions. Two sessions working different tasks never write the same file, which matters because a gitignored board has no history to recover a clobbered write from.
34
35
 
36
+ Three siblings sit in the folder without being tasks, and each earns its place by being governed somewhere. `index.md` and `priority.md` are governed here. `session.md` is the pre-compaction handoff, written by `orchestrator-handoff` and read by `orchestrator-resume`, and it is optional: a project running no orchestrator carries no such file. Anything reading the folder as a task list skips all three, so a name outside the set is a task whatever it holds.
37
+
35
38
  `index.md` is generated from sibling frontmatter. The folder is gitignored, so the whole-repo index walk skips it and a hook passing the changed path regenerates it instead. Never hand-edit it.
36
39
 
37
40
  The `claude-tasks` skill creates and archives task files. `claude-docs` marks outcomes `[x]` in an existing file and sweeps the plans those tasks cite. Neither does the other's job.
@@ -50,6 +53,8 @@ Readiness is three groups under fixed headings, `## Run now`, `## Up next`, and
50
53
 
51
54
  Each group fixes its own columns, which follow from the test above it rather than from preference. Neither half of the `## Run now` test is checkable without the file set and the plan sitting beside the task. The blocker column under `## Up next` names whether a collision or a dependency holds the row. `## Needs a plan` states no file set at all, because a task with no plan has no bounded one to state. A group with no rows keeps its heading and its header row.
52
55
 
56
+ `aitk tasks validate` reads those columns and reports where a row's claim and the tree disagree: a plan pointer resolving to no file, a row and a task file that do not map one to one, a task in two groups, and two `## Run now` rows touching a path in common. Run it when the readiness claim is made rather than on a schedule, since the board is gitignored per-machine scratch and no shared moment exists to hang it on. It reports and never writes, so a session fixes the row it names.
57
+
53
58
  ```markdown
54
59
  ---
55
60
  title: Priority
@@ -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 memory files, gitignored
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
@@ -0,0 +1,8 @@
1
+ ---
2
+ title: Memory
3
+ subtitle: Session facts with no owning surface, grouped by kind. Feedback is the working set.
4
+ ---
5
+
6
+ # Memory
7
+
8
+ Session facts with no owning surface, grouped by kind. Feedback is the working set.
@@ -31,6 +31,10 @@
31
31
  {
32
32
  "type": "command",
33
33
  "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/tasks-index.sh"
34
+ },
35
+ {
36
+ "type": "command",
37
+ "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/memory-index.sh"
34
38
  }
35
39
  ]
36
40
  }
@@ -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