@erclx/aitk 0.44.0 → 0.46.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 (38) 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/claude-seed-sync/REQUIREMENT.md +3 -1
  10. package/claude/skills/claude-seed-sync/SKILL.md +16 -4
  11. package/claude/skills/git-ship/SKILL.md +14 -11
  12. package/claude/skills/session-resume/SKILL.md +2 -2
  13. package/claude/skills/toolkit-operator/SKILL.md +18 -1
  14. package/docs/agents/commands.md +36 -35
  15. package/docs/agents/index.md +1 -0
  16. package/docs/agents/indexes.md +3 -1
  17. package/docs/agents/install-and-sync.md +46 -0
  18. package/docs/agents/scripting.md +4 -3
  19. package/docs/agents/skills-audit.md +55 -0
  20. package/docs/ai-workflow.md +3 -2
  21. package/docs/target-projects.md +5 -1
  22. package/package.json +1 -1
  23. package/scripts/core/verify.sh +8 -0
  24. package/src/claude/seeds.ts +7 -1
  25. package/src/claude/skills-audit.ts +215 -0
  26. package/src/claude/skills-list.ts +3 -3
  27. package/src/commands/claude.ts +283 -5
  28. package/src/commands/context.ts +1 -4
  29. package/src/commands/sync.ts +54 -1
  30. package/src/sync/check.ts +71 -0
  31. package/src/sync/layout.ts +139 -0
  32. package/src/sync/seeds-report.ts +111 -0
  33. package/src/ui.ts +5 -0
  34. package/tooling/claude/reference.md +11 -1
  35. package/tooling/claude/seeds/.claude/hooks/memory-index.sh +60 -0
  36. package/tooling/claude/seeds/.claude/memory/index.md +8 -0
  37. package/tooling/claude/seeds/.claude/settings.json +4 -0
  38. package/tooling/claude/seeds/CLAUDE.md +3 -0
@@ -0,0 +1,139 @@
1
+ import { existsSync, statSync } from 'node:fs'
2
+ import { basename, join } from 'node:path'
3
+ import { SUBDIRS } from '@/claude/seeds'
4
+ import { snippetsSourceDir } from '@/snippets/categories'
5
+ import { standardsSourceDir } from '@/standards/adapter'
6
+ import type { StampDomain } from '@/sync/stamp'
7
+
8
+ const CLAUDE_DIR = '.claude'
9
+
10
+ /**
11
+ * Domains an older toolkit installed at the project root, each with the source
12
+ * folder naming what it owns. Governance is absent because its rules have always
13
+ * landed under `.claude/rules/`, so there is no earlier location to be stranded
14
+ * at.
15
+ *
16
+ * A tuple array rather than a partial record, so the domain key stays typed
17
+ * without asserting an `Object.entries` result back into the union.
18
+ */
19
+ const ROOT_LAYOUTS: readonly (readonly [
20
+ StampDomain,
21
+ string,
22
+ (root: string) => string,
23
+ ])[] = [
24
+ ['standards', 'standards', standardsSourceDir],
25
+ ['snippets', 'snippets', snippetsSourceDir],
26
+ ]
27
+
28
+ /**
29
+ * A target file that a shipped seed folder replaced. Carries no source and
30
+ * queues no change, because the file holds content the project wrote and only
31
+ * the user can decide where it moves.
32
+ */
33
+ export interface SupersededEntry {
34
+ readonly rel: string
35
+ readonly replacedBy: string
36
+ }
37
+
38
+ /**
39
+ * A domain whose files sit at the root layout an older toolkit installed to,
40
+ * with nothing at the path the current one reads. Distinct from a domain that
41
+ * was never installed, which has neither.
42
+ */
43
+ export interface UnmigratedDomain {
44
+ readonly domain: StampDomain
45
+ readonly rootPath: string
46
+ readonly installPath: string
47
+ /** Root files the toolkit ships under this domain, not every file present. */
48
+ readonly files: number
49
+ }
50
+
51
+ /**
52
+ * Pairs each seed subdirectory against an uppercase-stem sibling in the target,
53
+ * so a project still holding `.claude/TASKS.md` is reported against the
54
+ * `.claude/tasks/` folder that replaced it.
55
+ *
56
+ * Deriving from the seed tree rather than from a fixed list means a folder
57
+ * added later is covered without editing this file. The cost is that only an
58
+ * exact stem matches, so a suffixed variant such as `TASKS-ARCHIVE.md` is not
59
+ * reported.
60
+ */
61
+ export function collectSuperseded(target: string): SupersededEntry[] {
62
+ const entries: SupersededEntry[] = []
63
+
64
+ for (const subdir of SUBDIRS) {
65
+ const rel = join(CLAUDE_DIR, `${subdir.toUpperCase()}.md`)
66
+ if (!isFile(join(target, rel))) continue
67
+
68
+ entries.push({ rel, replacedBy: join(CLAUDE_DIR, subdir) })
69
+ }
70
+
71
+ return entries
72
+ }
73
+
74
+ /**
75
+ * Domains the target holds at the root rather than under `.claude/`. Reported
76
+ * separately from the per-domain scan because that scan lists only domains it
77
+ * finds installed, so an unmigrated project would otherwise read as one that
78
+ * never installed the domain at all.
79
+ *
80
+ * A root folder is claimed only when it holds a file the toolkit ships under
81
+ * that domain. Presence of the folder alone is not evidence: a project can
82
+ * carry its own `standards/` of project docs and never have installed the
83
+ * domain, and calling that unmigrated would fail `--exit-code` with no action
84
+ * that clears it.
85
+ */
86
+ export function detectUnmigrated(
87
+ toolkitRoot: string,
88
+ target: string,
89
+ ): UnmigratedDomain[] {
90
+ const found: UnmigratedDomain[] = []
91
+
92
+ for (const [domain, rootPath, sourceDir] of ROOT_LAYOUTS) {
93
+ const installPath = join(CLAUDE_DIR, rootPath)
94
+ if (isDirectoryWithFiles(join(target, installPath))) continue
95
+
96
+ const files = countToolkitOwned(
97
+ join(target, rootPath),
98
+ sourceDir(toolkitRoot),
99
+ )
100
+ if (files === 0) continue
101
+
102
+ found.push({ domain, rootPath, installPath, files })
103
+ }
104
+
105
+ return found
106
+ }
107
+
108
+ /**
109
+ * Root files whose basename matches something the toolkit ships for this domain.
110
+ * Basenames rather than relative paths, because standards install flat while
111
+ * snippets nest by category, and the question here is only whether any file is
112
+ * toolkit-owned rather than which source each one came from.
113
+ */
114
+ function countToolkitOwned(dir: string, sourceDir: string): number {
115
+ const owned = new Set(listMarkdown(sourceDir).map((rel) => basename(rel)))
116
+ if (owned.size === 0) return 0
117
+
118
+ return listMarkdown(dir).filter((rel) => owned.has(basename(rel))).length
119
+ }
120
+
121
+ function isFile(path: string): boolean {
122
+ return existsSync(path) && statSync(path).isFile()
123
+ }
124
+
125
+ function isDirectoryWithFiles(path: string): boolean {
126
+ return listMarkdown(path).length > 0
127
+ }
128
+
129
+ function listMarkdown(dir: string): string[] {
130
+ if (!existsSync(dir) || !statSync(dir).isDirectory()) return []
131
+
132
+ return [
133
+ ...new Bun.Glob('**/*.md').scanSync({
134
+ cwd: dir,
135
+ onlyFiles: true,
136
+ dot: true,
137
+ }),
138
+ ]
139
+ }
@@ -0,0 +1,111 @@
1
+ import { readFileSync } from 'node:fs'
2
+ import { relative } from 'node:path'
3
+ import { planSeeds, type Seed } from '@/claude/seeds'
4
+ import { findInstalledOrigin, readHistoryIndex } from '@/sync/history'
5
+
6
+ /**
7
+ * How an installed seed compares to the seed the toolkit currently ships.
8
+ *
9
+ * `missing` has no counterpart in the domain scan, which walks what a target
10
+ * installed and so cannot see a file that never arrived. Seeds are enumerated
11
+ * from the source instead, which is what makes the absence legible.
12
+ *
13
+ * There is no `customized` here. That verdict needs a stamp, and seeds carry
14
+ * none, so a file history cannot attribute stays `drifted` and a consumer reads
15
+ * it as the local edit it almost always is.
16
+ */
17
+ export type SeedState = 'matching' | 'stale' | 'drifted' | 'missing'
18
+
19
+ export interface SeedReportEntry {
20
+ readonly state: SeedState
21
+ readonly rel: string
22
+ /** Toolkit revision this file's content came from, when history proved it. */
23
+ readonly since?: string
24
+ }
25
+
26
+ export interface SeedsReport {
27
+ readonly entries: readonly SeedReportEntry[]
28
+ /** Set when a file needed history to attribute it and this toolkit has none. */
29
+ readonly historyUnavailable: boolean
30
+ }
31
+
32
+ /**
33
+ * Classifies every seed the toolkit ships against the target's copy, and never
34
+ * returns a change. Seeds are copy-once files a project is expected to edit, so
35
+ * the engine's copy path would overwrite `CLAUDE.md` wholesale. Reporting alone
36
+ * is what lets `claude-seed-sync` merge one section at a time instead.
37
+ *
38
+ * Attribution reuses the history reader rather than the engine's own recovery
39
+ * pass, which is private and takes a `SyncAdapter` seeds have no way to supply.
40
+ */
41
+ export function buildSeedsReport(
42
+ toolkitRoot: string,
43
+ target: string,
44
+ ): SeedsReport {
45
+ const differing: DifferingSeed[] = []
46
+ const entries: SeedReportEntry[] = []
47
+
48
+ for (const { seed, present } of planSeeds(toolkitRoot, target)) {
49
+ const rel = relative(target, seed.dest)
50
+
51
+ if (!present) {
52
+ entries.push({ state: 'missing', rel })
53
+ continue
54
+ }
55
+
56
+ if (sameContent(seed.src, seed.dest)) {
57
+ entries.push({ state: 'matching', rel })
58
+ continue
59
+ }
60
+
61
+ differing.push({ index: entries.length, seed })
62
+ entries.push({ state: 'drifted', rel })
63
+ }
64
+
65
+ const historyUnavailable = attribute(toolkitRoot, entries, differing)
66
+
67
+ return { entries, historyUnavailable }
68
+ }
69
+
70
+ interface DifferingSeed {
71
+ readonly index: number
72
+ readonly seed: Seed
73
+ }
74
+
75
+ /**
76
+ * Second pass over the seeds that differ, matching installed content against
77
+ * every version the toolkit ever published. A match proves the file is
78
+ * untouched since it landed, so the toolkit is what moved and the entry becomes
79
+ * `stale`. Runs as one git call for the whole set rather than one per file.
80
+ */
81
+ function attribute(
82
+ toolkitRoot: string,
83
+ entries: SeedReportEntry[],
84
+ differing: readonly DifferingSeed[],
85
+ ): boolean {
86
+ if (differing.length === 0) return false
87
+
88
+ const index = readHistoryIndex(
89
+ toolkitRoot,
90
+ differing.map((file) => relative(toolkitRoot, file.seed.src)),
91
+ )
92
+
93
+ if (index === undefined) return true
94
+
95
+ for (const file of differing) {
96
+ const since = findInstalledOrigin(
97
+ index,
98
+ relative(toolkitRoot, file.seed.src),
99
+ file.seed.dest,
100
+ )
101
+
102
+ if (since === undefined) continue
103
+ entries[file.index] = { ...entries[file.index], state: 'stale', since }
104
+ }
105
+
106
+ return false
107
+ }
108
+
109
+ function sameContent(source: string, dest: string): boolean {
110
+ return readFileSync(source).equals(readFileSync(dest))
111
+ }
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 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