@erclx/aitk 0.29.0 → 0.30.1

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "aitk",
3
3
  "description": "Automated governance, versioning, and discovery tools for Claude Code.",
4
- "version": "0.29.0",
4
+ "version": "0.30.1",
5
5
  "author": {
6
6
  "name": "Eric Le",
7
7
  "url": "https://github.com/erclx"
@@ -16,6 +16,7 @@ Without this skill, the memory folder grows and never drains. Entries pile up re
16
16
  - Rewrite a rule into the destination's voice instead of moving it unchanged
17
17
  - Write the proposal to a receipt on disk and take no action until the user decides per item
18
18
  - Keep a promotion on its own commit, since a change to how the agent operates should not ride inside a feature a reviewer is vetting for something else
19
+ - Confine the pass that runs after application to the one receipt it tested, so tidying up removes a file whose decisions are known to be resolved and leaves the pen and every untested receipt alone
19
20
 
20
21
  ## Must not
21
22
 
@@ -23,12 +24,14 @@ Without this skill, the memory folder grows and never drains. Entries pile up re
23
24
  - Author a governance rule inline. Coding-standards rules have an owner and a scaffolding path, and a rule written here bypasses both.
24
25
  - Mutate tracked files from the main worktree
25
26
  - Answer a question raised in a decision slot while applying. Discussion and application are separate passes so an approval is never inferred from a reply.
27
+ - Delete a memory entry outside the approved-per-item pass. The folder is gitignored with no history behind it, so a removal any other phase makes has no undo and no record of what it took.
26
28
 
27
29
  ## Guards
28
30
 
29
31
  - No memory folder at the main root: stop
30
32
  - The folder holds only its index: stop with a pass, not an error
31
33
  - Apply invoked from the main worktree: stop and name the worktree command
34
+ - Cleanup invoked with no receipt on disk: stop with a pass, not an error
32
35
 
33
36
  ## Out of scope
34
37
 
@@ -7,21 +7,22 @@ description: Reviews `.claude/memory/` and proposes per-entry actions (promote t
7
7
 
8
8
  This skill drives the full memory review lifecycle in five phases. Pick the phase from what the user said and whether a review receipt already exists at `<main-root>/.claude/review/memory-review-*.md`.
9
9
 
10
- | User intent | Phase | Mutates |
11
- | ---------------------------------------------- | --------- | ---------------------------- |
12
- | "review memory", "promote memory" (no receipt) | Propose | review file only |
13
- | "challenge the promotes" (receipt exists) | Challenge | review file only |
14
- | "discuss", "respond to questions" | Discuss | review file only |
15
- | "apply decisions", "commit", "ship the review" | Apply | tracked files + memory files |
16
- | "cleanup", "sweep stale", "delete the receipt" | Cleanup | memory files + review file |
10
+ | User intent | Phase | Mutates |
11
+ | ---------------------------------------------------------------------- | --------- | ---------------------------- |
12
+ | "review memory", "promote memory", "sweep stale memories" (no receipt) | Propose | review file only |
13
+ | "challenge the promotes" (receipt exists) | Challenge | review file only |
14
+ | "discuss", "respond to questions" | Discuss | review file only |
15
+ | "apply decisions", "commit", "ship the review" | Apply | tracked files + memory files |
16
+ | "cleanup", "delete the receipt" | Cleanup | one review file |
17
17
 
18
- If the user just re-pings the skill with no new phrase and a receipt exists, default to Discuss when any `Decision:` contains `?`, otherwise Apply.
18
+ If the user re-pings the skill with no new phrase and a receipt exists, default to Discuss when any `Decision:` contains `?`, otherwise Apply.
19
19
 
20
20
  ## Guards
21
21
 
22
22
  - All `.claude/memory/` reads, edits, and deletes resolve at the main worktree root, not the current worktree. See Worktrees in `CLAUDE.md`.
23
23
  - If no `.claude/memory/` directory exists at the main worktree root, stop: `❌ No .claude/memory/ directory found.`
24
24
  - If `.claude/memory/` contains no `*.md` entries other than `MEMORY.md`, stop: `✅ No memory entries to review.`
25
+ - Cleanup is exempt from the two stops above. It works on receipts in `.claude/review/`, and a drained pen is the normal state once Apply has run, so a pen-shaped stop would strand the receipt it exists to delete.
25
26
  - Resolve the main root via `git worktree list --porcelain | grep -m 1 '^worktree ' | cut -d' ' -f2-`, falling back to `pwd`. All review and memory reads anchor here.
26
27
 
27
28
  ## Propose phase
@@ -186,14 +187,17 @@ End with: `✅ Applied: <nums> | ⏭ Skipped: <nums> | 📝 Pending: <nums>`. Om
186
187
 
187
188
  ## Cleanup phase
188
189
 
189
- Trigger: user says "cleanup", "sweep stale memories", or "delete the receipt" after Apply has run.
190
+ Trigger: user says "cleanup" or "delete the receipt" after Apply has run.
190
191
 
191
- 1. Read the latest `.claude/review/memory-review-*.md` to see which entries were skipped.
192
- 2. Treat a `Skip` decision as terminal. Do not delete a memory just because it would Skip again next review. Target only entries whose decision value is exhausted: already-applied promotions and stale receipts.
193
- 3. Delete the review file itself.
194
- 4. Leave applied promotions, governance handoffs, and user-type memories alone.
192
+ Cleanup removes one receipt and nothing else. Apply is the only phase that deletes a memory entry, and it does so per approved item against a folder that is gitignored with no history behind it, so a deletion made anywhere else has no undo and leaves no record of what it took. A user asking to sweep stale memories wants Propose, which classifies entries and writes a decision slot per entry.
195
193
 
196
- Do not promote or rewrite. Cleanup only.
194
+ If no `.claude/review/memory-review-*.md` exists at the main root, stop: `✅ No review receipt to clean up.` Every other refusal in this skill carries a message, and the phase reads a receipt before it does anything else.
195
+
196
+ 1. Read the latest `.claude/review/memory-review-*.md` at the main root and confirm Apply has run against it. If any item is still 📝 pending, stop and name the pending numbers.
197
+ 2. Delete that one file. Leave every other receipt beside it in place, because the pending test above covers the file it read and nothing has tested the rest.
198
+ 3. Leave every memory entry in place. A `Skip` decision is terminal, and applied promotions, governance handoffs, and user-type memories each stay as the review left them.
199
+
200
+ Do not promote, rewrite, or delete a memory entry. Receipts only.
197
201
 
198
202
  ## After completion
199
203
 
@@ -202,5 +206,6 @@ Output one line per action taken in the most recent phase:
202
206
  - `✅ Promoted: .claude/memory/<memory-file> → <target>`
203
207
  - `✅ Handed off: .claude/memory/<memory-file> → governance`
204
208
  - `🗑 Deleted: .claude/memory/<memory-file>`
209
+ - `🗑 Swept: .claude/review/<review-file>`
205
210
 
206
211
  If the user accepted nothing, output: `✅ No changes applied.`
package/docs/agents.md CHANGED
@@ -80,7 +80,7 @@ Full help: `aitk <command> --help`.
80
80
  | `aitk transcripts <url>` | Fetch a YouTube transcript with metadata frontmatter (needs `yt-dlp`) |
81
81
  | `aitk tasks archive` | Move a shipped task off the board, clear its ordering row, and regenerate the index |
82
82
  | `aitk comments scan` | Measure comment density by language and comment kind, with a trend recomputed from git |
83
- | `aitk context audit` | Report entry length, depth, cited-path resolution, and index drift in the context folders |
83
+ | `aitk context audit` | Report entry length, depth, cited-path resolution, provenance, and index drift |
84
84
  | `aitk capture [source]` | Render HTML capture sources to PNG, toolkit-only and absent from an installed package |
85
85
 
86
86
  ### Domain commands
@@ -387,7 +387,7 @@ aitk context audit --folder context,diagrams
387
387
 
388
388
  Scope defaults to `context`, `diagrams`, and `wireframes`, and a folder the project does not carry is skipped rather than reported. A domain that outgrew one file and split into `<domain>/` is audited as its own folder, so a split entry measures at the same grain as a flat one.
389
389
 
390
- Exit codes are `0` for a clean run, `1` for a refusal, and `2` for an unresolved citation. Only the citation check sets a failing code. Length, depth, table, and index findings print and return `0`, because each is a judgment threshold and failing a push on one would make the check something to route around.
390
+ Exit codes are `0` for a clean run, `1` for a refusal, and `2` for an unresolved citation. Only the citation check sets a failing code. Length, depth, table, provenance, and index findings print and return `0`, because each is a judgment and failing a push on one would make the check something to route around.
391
391
 
392
392
  ### What each check reports
393
393
 
@@ -395,6 +395,8 @@ Length and depth quote their checkpoints from `.claude/standards/context.md`: ro
395
395
 
396
396
  The table check reports a catalog that grows a row per shipped thing, not a table count. A fixed comparison table never reflows, so its size costs nothing. A table qualifies at six or more body rows whose first column mostly carries a path, command, or link, which is what separates a catalog from a comparison without reading the prose.
397
397
 
398
+ The provenance check reports the markers narrating how a domain reached its shape rather than describing what it is: a date, a change number, or a release label. The standard admits a rejected alternative and the reasoning that killed it while refusing the provenance attached to it, so a marker names a line to read rather than a line to delete. Findings group by entry and sort left to right within a line, since what a reader acts on is which file to open. Fenced blocks are excluded, which keeps a pinned version in an install command from reading as a claim the entry makes. Frontmatter is excluded with them, since every check reads the body alone, and that is what keeps a diagram entry's dated `verified` stamp a record of its last check rather than a marker to settle.
399
+
398
400
  Index drift compares an index against its siblings in both directions. An entry the index does not link is invisible to a session choosing what to open, and a linked name resolving to nothing sends one to a path that opens nothing.
399
401
 
400
402
  ### The citation gate
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@erclx/aitk",
3
3
  "type": "module",
4
- "version": "0.29.0",
4
+ "version": "0.30.1",
5
5
  "description": "Infrastructure and quality tooling for developer workflows",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -45,7 +45,9 @@ export function register(program: Command): void {
45
45
 
46
46
  context
47
47
  .command('audit')
48
- .description('Report entry length, depth, citations, and index drift')
48
+ .description(
49
+ 'Report entry length, depth, citations, provenance, and index drift',
50
+ )
49
51
  .argument('[path]', 'Project root, defaulting to the current directory')
50
52
  .helpOption('-h, --help', 'Show this help message')
51
53
  .option('--json', 'Add a machine-readable record on stdout')
@@ -61,7 +63,7 @@ export function register(program: Command): void {
61
63
  ' 2 a cited path did not resolve',
62
64
  '',
63
65
  'Only unresolved citations set a failing exit code. Length, depth,',
64
- 'table, and index findings are advisory.',
66
+ 'table, provenance, and index findings are advisory.',
65
67
  '',
66
68
  'Examples:',
67
69
  ' aitk context audit',
@@ -134,6 +136,7 @@ async function runAudit(
134
136
  reportLength(entries)
135
137
  reportDepth(entries)
136
138
  reportTables(entries)
139
+ reportProvenance(entries)
137
140
  reportDrift(drift)
138
141
  outro()
139
142
  }
@@ -303,6 +306,46 @@ function reportTables(entries: readonly EntryReport[]): void {
303
306
  pipeOutput(candidates.join('\n'))
304
307
  }
305
308
 
309
+ /**
310
+ * Groups by entry rather than listing every marker.
311
+ *
312
+ * The two entries carrying most of a corpus's markers carry them a dozen at a
313
+ * time, and a flat list of those buries the entries holding one. What a reader
314
+ * acts on is which file to open, so the count sits beside the name and the
315
+ * lines follow it.
316
+ */
317
+ function reportProvenance(entries: readonly EntryReport[]): void {
318
+ logStep('Provenance')
319
+ logInfo('Fenced blocks are excluded. A marker is a judgment, never a defect.')
320
+
321
+ const carrying = entries
322
+ .filter((entry) => entry.provenance.length > 0)
323
+ .sort((a, b) => b.provenance.length - a.provenance.length)
324
+
325
+ if (carrying.length === 0) {
326
+ logInfo('No entry narrates a change.')
327
+ return
328
+ }
329
+
330
+ const total = carrying.reduce(
331
+ (sum, entry) => sum + entry.provenance.length,
332
+ 0,
333
+ )
334
+ logWarn(
335
+ `${plural(total, 'marker')} across ${carrying.length} ${carrying.length === 1 ? 'entry' : 'entries'}`,
336
+ )
337
+ pipeOutput(
338
+ carrying
339
+ .map(
340
+ (entry) =>
341
+ `${entry.rel} ${plural(entry.provenance.length, 'marker')}\n${entry.provenance
342
+ .map((found) => ` :${found.line} ${found.kind} ${found.text}`)
343
+ .join('\n')}`,
344
+ )
345
+ .join('\n'),
346
+ )
347
+ }
348
+
306
349
  function reportDrift(drift: readonly FolderDrift[]): void {
307
350
  logStep('Index drift')
308
351
 
@@ -25,11 +25,35 @@ const TABLE_ROW = /^\s*\|/
25
25
  const TABLE_SEPARATOR = /^\s*\|[\s:|-]+\|\s*$/
26
26
  const NAMED_CELL = /`[^`]+`|\[[^\]]+\]\([^)]+\)/
27
27
 
28
+ /**
29
+ * Spellings of how the domain reached its shape rather than what it is now.
30
+ *
31
+ * The standard admits a rejected alternative and its reasoning while refusing
32
+ * the provenance attached to it, and these three are what a session reaches for
33
+ * when it records the second: when a change landed, which change carried it,
34
+ * and which release labelled it. A marker is a judgment rather than a defect,
35
+ * so this is measured and reported and never gates.
36
+ */
37
+ const PROVENANCE: readonly { kind: ProvenanceKind; pattern: RegExp }[] = [
38
+ { kind: 'date', pattern: /\b\d{4}-\d{2}-\d{2}\b/g },
39
+ { kind: 'change', pattern: /#\d{3,}\b/g },
40
+ { kind: 'release', pattern: /\bv\d+\.\d+(?:\.\d+)?\b/g },
41
+ ]
42
+
43
+ export type ProvenanceKind = 'date' | 'change' | 'release'
44
+
28
45
  export interface TableFinding {
29
46
  readonly line: number
30
47
  readonly rows: number
31
48
  }
32
49
 
50
+ export interface ProvenanceFinding {
51
+ readonly line: number
52
+ readonly kind: ProvenanceKind
53
+ /** The marker as written, so a report names what to go and look at. */
54
+ readonly text: string
55
+ }
56
+
33
57
  export interface EntryReport {
34
58
  readonly rel: string
35
59
  readonly lines: number
@@ -37,6 +61,7 @@ export interface EntryReport {
37
61
  /** First line of the longest run, or 0 when the entry has no run at all. */
38
62
  readonly longestRunLine: number
39
63
  readonly catalogTables: readonly TableFinding[]
64
+ readonly provenance: readonly ProvenanceFinding[]
40
65
  }
41
66
 
42
67
  interface BodyLine {
@@ -191,6 +216,44 @@ function catalogTables(lines: readonly BodyLine[]): TableFinding[] {
191
216
  return findings
192
217
  }
193
218
 
219
+ /**
220
+ * Finds the markers narrating a change rather than describing the domain.
221
+ *
222
+ * Fenced blocks are skipped for the same reason the table scan skips them: a
223
+ * sample command or a fixture inside an example is content the entry displays
224
+ * rather than a claim it makes, and a version pinned in an install line is the
225
+ * ordinary shape of one.
226
+ */
227
+ function provenance(lines: readonly BodyLine[]): ProvenanceFinding[] {
228
+ // Scanning one pattern at a time emits a line's markers grouped by kind, so
229
+ // the column is carried out of the match and sorted on. Without it a line
230
+ // holding a date and two change numbers reports them in an order the reader
231
+ // cannot find by scanning left to right.
232
+ const found: { finding: ProvenanceFinding; column: number }[] = []
233
+ let fenced = false
234
+
235
+ for (const line of lines) {
236
+ if (FENCE.test(line.text)) {
237
+ fenced = !fenced
238
+ continue
239
+ }
240
+ if (fenced) continue
241
+
242
+ for (const { kind, pattern } of PROVENANCE) {
243
+ for (const match of line.text.matchAll(pattern)) {
244
+ found.push({
245
+ finding: { line: line.number, kind, text: match[0] },
246
+ column: match.index,
247
+ })
248
+ }
249
+ }
250
+ }
251
+
252
+ return found
253
+ .sort((a, b) => a.finding.line - b.finding.line || a.column - b.column)
254
+ .map((each) => each.finding)
255
+ }
256
+
194
257
  export function measureEntry(rel: string, source: string): EntryReport {
195
258
  const lines = bodyLines(source)
196
259
  const run = longestRun(lines)
@@ -201,6 +264,7 @@ export function measureEntry(rel: string, source: string): EntryReport {
201
264
  longestRun: run.length,
202
265
  longestRunLine: run.line,
203
266
  catalogTables: catalogTables(lines),
267
+ provenance: provenance(lines),
204
268
  }
205
269
  }
206
270
 
@@ -94,11 +94,13 @@ Only the `development` entry carries this section. It is not a general-purpose h
94
94
  - Tutorials or human onboarding. Those go in `docs/` if a public audience exists.
95
95
  - Generated content (API references). Generate, do not write by hand.
96
96
  - Anything already in `.claude/REQUIREMENTS.md` or `.claude/ARCHITECTURE.md`.
97
+ - The history of how the domain reached its current shape. An entry describes the repository as it stands, so a change number, release label, or date attached to a change goes wherever the project tracks work.
98
+ - A rejected alternative's provenance, which is the same rule at the one place the section above admits history. Keep what was tried and why it lost. Cut who tried it and when.
97
99
 
98
100
  ## Length
99
101
 
100
102
  - Aim for one entry per domain. There is no hard cap. Length is a symptom, not the defect.
101
- - Past roughly 150 lines, check two things before adding more: whether the entry still covers a single domain, and whether it has filled with content `ls` or `--help` reproduces. Fix whichever is true rather than trimming to hit a number.
103
+ - Past roughly 150 lines, check three things before adding more: whether the entry still covers a single domain, whether it has filled with content `ls` or `--help` reproduces, and whether it has accumulated the history of its own changes. Fix whichever is true rather than trimming to hit a number.
102
104
  - Past roughly 40 lines with no heading of any level breaking them, add a subheading at the seam. Measure the longest such run rather than everything under one `##`, and exclude fenced code blocks. The number is a checkpoint like the 150 above, not a cap.
103
105
  - Exempt a block whose lines are all list items at one level. A long list of peers is already navigable, and a subheading dropped into it splits a set that belongs together. Mixing prose with the list, or nesting levels inside it, ends the exemption.
104
106
  - Never cut a `## Decisions` or `## Gotchas` entry to shorten a file. Cut a `## Layout` or `## CLI` section instead.