@erclx/aitk 1.1.0 → 1.3.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 (73) hide show
  1. package/claude/.claude-plugin/plugin.json +1 -1
  2. package/claude/skills/claude-address-review/SKILL.md +5 -6
  3. package/claude/skills/claude-autoship/SKILL.md +2 -2
  4. package/claude/skills/claude-design-extract/SKILL.md +3 -5
  5. package/claude/skills/claude-diagram/SKILL.md +2 -2
  6. package/claude/skills/claude-docs/SKILL.md +11 -11
  7. package/claude/skills/claude-feature/SKILL.md +3 -3
  8. package/claude/skills/claude-groundwork/SKILL.md +2 -2
  9. package/claude/skills/claude-intake/SKILL.md +1 -1
  10. package/claude/skills/claude-intake-answer/SKILL.md +1 -1
  11. package/claude/skills/claude-memory-capture/SKILL.md +7 -9
  12. package/claude/skills/claude-memory-review/SKILL.md +6 -8
  13. package/claude/skills/claude-memory-review/references/receipt-format.md +1 -1
  14. package/claude/skills/claude-orchestrate/SKILL.md +3 -3
  15. package/claude/skills/claude-orchestrate/references/orchestrator-handoff.md +1 -1
  16. package/claude/skills/claude-orchestrate/references/orchestrator-resume.md +1 -1
  17. package/claude/skills/claude-pr-review/SKILL.md +2 -2
  18. package/claude/skills/claude-review/SKILL.md +1 -1
  19. package/claude/skills/claude-seed-sync/SKILL.md +1 -1
  20. package/claude/skills/claude-standards-audit/SKILL.md +4 -4
  21. package/claude/skills/claude-tasks/SKILL.md +34 -9
  22. package/claude/skills/claude-teach/SKILL.md +2 -2
  23. package/claude/skills/claude-ui-test/SKILL.md +1 -1
  24. package/claude/skills/claude-ux-audit/SKILL.md +2 -4
  25. package/claude/skills/claude-ux-measure/SKILL.md +1 -1
  26. package/claude/skills/claude-worktree/SKILL.md +3 -3
  27. package/claude/skills/create-rule/SKILL.md +2 -2
  28. package/claude/skills/create-skill/SKILL.md +4 -6
  29. package/claude/skills/create-snippet/SKILL.md +1 -3
  30. package/claude/skills/create-standard/SKILL.md +2 -4
  31. package/claude/skills/decision-escalate/SKILL.md +1 -1
  32. package/claude/skills/docs-sync/SKILL.md +3 -5
  33. package/claude/skills/git-commit/SKILL.md +1 -3
  34. package/claude/skills/git-followup/SKILL.md +1 -1
  35. package/claude/skills/git-issue/SKILL.md +2 -4
  36. package/claude/skills/git-pr/SKILL.md +4 -6
  37. package/claude/skills/git-split/SKILL.md +1 -1
  38. package/claude/skills/git-stage/SKILL.md +1 -3
  39. package/claude/skills/migration-claude-md/SKILL.md +2 -2
  40. package/claude/skills/session-resume/SKILL.md +1 -1
  41. package/claude/skills/setup-gov/SKILL.md +1 -1
  42. package/claude/skills/setup-indexes/SKILL.md +1 -1
  43. package/claude/skills/write-human/SKILL.md +2 -2
  44. package/docs/agents/commands.md +2 -1
  45. package/docs/agents/index.md +1 -1
  46. package/docs/agents/markdown-audit.md +3 -1
  47. package/docs/agents/records.md +32 -4
  48. package/docs/ai-workflow.md +3 -1
  49. package/docs/target-projects.md +3 -3
  50. package/governance/rules/claude/501-markdown.md +1 -1
  51. package/governance/rules/claude/510-context.md +1 -1
  52. package/governance/rules/claude/520-wireframes.md +1 -1
  53. package/governance/rules/claude/530-requirements.md +1 -1
  54. package/governance/rules/claude/540-architecture.md +1 -1
  55. package/governance/rules/claude/550-design.md +1 -1
  56. package/governance/rules/claude/555-tasks.md +1 -1
  57. package/governance/rules/claude/556-groundwork.md +1 -1
  58. package/governance/rules/claude/557-intake.md +1 -1
  59. package/governance/rules/claude/558-plan.md +1 -1
  60. package/governance/rules/claude/559-memory.md +1 -1
  61. package/governance/rules/claude/560-diagrams.md +1 -1
  62. package/governance/rules/claude/561-teach.md +1 -1
  63. package/governance/rules/claude/562-session.md +1 -1
  64. package/governance/rules/claude/570-skill.md +1 -1
  65. package/governance/rules/claude/580-readme.md +1 -1
  66. package/governance/rules/claude/590-rule-authoring.md +1 -1
  67. package/governance/rules/claude/591-standard-authoring.md +1 -1
  68. package/package.json +1 -1
  69. package/src/cli.ts +2 -1
  70. package/src/commands/records.ts +145 -7
  71. package/src/markdown/structure.ts +39 -4
  72. package/src/records/size.ts +260 -0
  73. package/standards/tasks.md +3 -1
@@ -0,0 +1,260 @@
1
+ import { existsSync, type Stats } from 'node:fs'
2
+ import { readdir, stat } from 'node:fs/promises'
3
+ import { join } from 'node:path'
4
+ import { BACKED_FOLDERS } from '@/records/backup'
5
+
6
+ /**
7
+ * The folders a size reading covers, relative to `.claude/`.
8
+ *
9
+ * It is the backed set plus `.tmp`, which a backup skips because it is
10
+ * deletable without loss and a reading covers because deletable is not the same
11
+ * as empty: the routing handoffs and the memory archive both sit there and both
12
+ * accumulate. `.records.git` stays out because it is the backup history rather
13
+ * than a record, and `worktrees/` stays out because each entry there is a
14
+ * checkout of the enclosing repository with its own removal verb, and one of
15
+ * them outweighs every record folder combined.
16
+ */
17
+ export const SIZED_FOLDERS = [...BACKED_FOLDERS, '.tmp'] as const
18
+
19
+ /**
20
+ * The windows a reading reports, in days.
21
+ *
22
+ * There are two rather than one, because a single window cannot separate a
23
+ * folder that grows steadily from one that took a single batch. A folder whose
24
+ * 7-day count is most of its 30-day count moved in one pass, and one where the
25
+ * two are proportional is growing at a rate.
26
+ */
27
+ export const GROWTH_WINDOWS = [7, 30] as const
28
+
29
+ const DAY_MS = 24 * 60 * 60 * 1000
30
+
31
+ export interface WindowCount {
32
+ readonly days: number
33
+ readonly files: number
34
+ }
35
+
36
+ export interface FolderSize {
37
+ /** Relative to `.claude/`, which is the name a reader opens. */
38
+ readonly folder: string
39
+ readonly present: boolean
40
+ readonly files: number
41
+ readonly bytes: number
42
+ /** `YYYY-MM-DD` of the least and most recently modified file, absent when the folder holds none. */
43
+ readonly oldest?: string
44
+ readonly newest?: string
45
+ readonly touched: readonly WindowCount[]
46
+ }
47
+
48
+ export interface SizeReport {
49
+ readonly ok: true
50
+ readonly root: string
51
+ readonly folders: readonly FolderSize[]
52
+ readonly files: number
53
+ readonly bytes: number
54
+ }
55
+
56
+ export const SIZE_REFUSALS = ['no-folder'] as const
57
+
58
+ export type SizeRefusal = (typeof SIZE_REFUSALS)[number]
59
+
60
+ export interface SizeRefused {
61
+ readonly ok: false
62
+ readonly reason: SizeRefusal
63
+ readonly message: string
64
+ }
65
+
66
+ export type SizeOutcome = SizeReport | SizeRefused
67
+
68
+ interface Walked {
69
+ files: number
70
+ bytes: number
71
+ oldest?: number
72
+ newest?: number
73
+ touched: number[]
74
+ }
75
+
76
+ /**
77
+ * Renders the calendar date the writer saw, which is the local one.
78
+ *
79
+ * `toISOString` renders in UTC, so a file written after 17:00 at `-0700` dates
80
+ * to the following day and a reader comparing the column against their own
81
+ * memory of writing it finds the two disagree. The reading is per-machine
82
+ * already, since these folders are gitignored and hold whatever that disk holds,
83
+ * so a local date is the answer consistent with the rest of the report.
84
+ */
85
+ function day(ms: number): string {
86
+ const at = new Date(ms)
87
+ const month = String(at.getMonth() + 1).padStart(2, '0')
88
+ const date = String(at.getDate()).padStart(2, '0')
89
+ return `${at.getFullYear()}-${month}-${date}`
90
+ }
91
+
92
+ /**
93
+ * Adds one file to the running totals.
94
+ *
95
+ * The window counts read `mtime`, so what they report is a file written inside
96
+ * the window rather than one created there. An entry edited long after it
97
+ * landed counts as recent, which overstates growth and never understates it.
98
+ * That is the safe direction for a number whose whole job is to be noticed, and
99
+ * these folders are append-mostly, so the two readings agree on nearly every
100
+ * file.
101
+ *
102
+ * The one reading that is wrong rather than early is a machine restored by
103
+ * `aitk records pull`, which resets the work tree hard and re-dates every file
104
+ * it writes. A window taken there counts the restore. Nothing separates the two
105
+ * from the filesystem, since a restored file is new by every stamp it carries,
106
+ * so the caveat is published rather than corrected.
107
+ */
108
+ function absorb(into: Walked, bytes: number, mtime: number, now: number): void {
109
+ into.files += 1
110
+ into.bytes += bytes
111
+ into.oldest = into.oldest === undefined ? mtime : Math.min(into.oldest, mtime)
112
+ into.newest = into.newest === undefined ? mtime : Math.max(into.newest, mtime)
113
+
114
+ GROWTH_WINDOWS.forEach((days, index) => {
115
+ if (now - mtime <= days * DAY_MS) into.touched[index] += 1
116
+ })
117
+ }
118
+
119
+ /**
120
+ * Reads one entry, or undefined when it left between the listing and the read.
121
+ *
122
+ * These folders are written by whatever sessions are running, so a path listed
123
+ * a moment ago can be gone by the time it is read. A vanished file is a file
124
+ * the folder no longer holds, which is the answer the count wants, and letting
125
+ * `ENOENT` out would fail the whole reading over one deleted scratch file.
126
+ * Every other error propagates, since a permission or IO failure would
127
+ * undercount with nothing said.
128
+ */
129
+ async function readSize(path: string): Promise<Stats | undefined> {
130
+ try {
131
+ return await stat(path)
132
+ } catch (error) {
133
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return undefined
134
+ throw error
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Walks one folder, counting files and never following a symlink.
140
+ *
141
+ * `isFile()` answers false for a link, so a folder holding one reports it
142
+ * nowhere rather than counting whatever sits behind it. The corpus symlinks
143
+ * point out of `.claude/` and into the authoring roots, where a second reading
144
+ * of the same bytes would be the wrong answer twice over.
145
+ */
146
+ async function walk(dir: string, into: Walked, now: number): Promise<void> {
147
+ const entries = await readdir(dir, { withFileTypes: true }).catch(
148
+ (error: NodeJS.ErrnoException) => {
149
+ if (error.code === 'ENOENT') return []
150
+ throw error
151
+ },
152
+ )
153
+
154
+ for (const entry of entries) {
155
+ const path = join(dir, entry.name)
156
+
157
+ if (entry.isDirectory()) {
158
+ await walk(path, into, now)
159
+ continue
160
+ }
161
+ if (!entry.isFile()) continue
162
+
163
+ const info = await readSize(path)
164
+ if (info) absorb(into, info.size, info.mtimeMs, now)
165
+ }
166
+ }
167
+
168
+ async function measure(
169
+ root: string,
170
+ folder: string,
171
+ now: number,
172
+ ): Promise<FolderSize> {
173
+ const path = join(root, '.claude', folder)
174
+ const empty = GROWTH_WINDOWS.map((days) => ({ days, files: 0 }))
175
+
176
+ if (!existsSync(path)) {
177
+ return { folder, present: false, files: 0, bytes: 0, touched: empty }
178
+ }
179
+
180
+ const walked: Walked = {
181
+ files: 0,
182
+ bytes: 0,
183
+ touched: GROWTH_WINDOWS.map(() => 0),
184
+ }
185
+ await walk(path, walked, now)
186
+
187
+ return {
188
+ folder,
189
+ present: true,
190
+ files: walked.files,
191
+ bytes: walked.bytes,
192
+ oldest: walked.oldest === undefined ? undefined : day(walked.oldest),
193
+ newest: walked.newest === undefined ? undefined : day(walked.newest),
194
+ touched: GROWTH_WINDOWS.map((days, index) => ({
195
+ days,
196
+ files: walked.touched[index],
197
+ })),
198
+ }
199
+ }
200
+
201
+ /**
202
+ * Reports what each record folder holds and how much of it is recent.
203
+ *
204
+ * Every folder is reported whether or not it exists, since a caller reading the
205
+ * record wants a stable set of keys, and a folder absent from the output is
206
+ * indistinguishable from one the reading skipped.
207
+ *
208
+ * `now` is a parameter rather than a call inside the walk so a test can pin the
209
+ * windows against fixture timestamps.
210
+ */
211
+ export async function sizeRecords(
212
+ root: string,
213
+ now: number = Date.now(),
214
+ ): Promise<SizeOutcome> {
215
+ if (!existsSync(join(root, '.claude'))) {
216
+ return {
217
+ ok: false,
218
+ reason: 'no-folder',
219
+ message: `No .claude directory at ${root}, so there are no record folders to read.`,
220
+ }
221
+ }
222
+
223
+ // Each folder is walked independently, and the report is ordered by the
224
+ // caller rather than by arrival, so `Promise.all` keeps the input order while
225
+ // the ten walks overlap.
226
+ const folders = await Promise.all(
227
+ SIZED_FOLDERS.map((folder) => measure(root, folder, now)),
228
+ )
229
+
230
+ return {
231
+ ok: true,
232
+ root,
233
+ folders,
234
+ files: folders.reduce((total, entry) => total + entry.files, 0),
235
+ bytes: folders.reduce((total, entry) => total + entry.bytes, 0),
236
+ }
237
+ }
238
+
239
+ const UNITS = ['B', 'K', 'M', 'G'] as const
240
+
241
+ /**
242
+ * Renders a byte count at three significant figures or fewer.
243
+ *
244
+ * The reading is a prompt to go and look rather than an accounting figure, so
245
+ * an exact byte count buys nothing and costs a reader the comparison between
246
+ * two rows.
247
+ */
248
+ export function formatBytes(bytes: number): string {
249
+ let value = bytes
250
+ let unit = 0
251
+
252
+ while (value >= 1024 && unit < UNITS.length - 1) {
253
+ value /= 1024
254
+ unit += 1
255
+ }
256
+
257
+ const rendered =
258
+ unit === 0 || value >= 10 ? Math.round(value) : value.toFixed(1)
259
+ return `${rendered}${UNITS[unit]}`
260
+ }
@@ -207,7 +207,9 @@ Why this task exists and what it depends on.
207
207
 
208
208
  Every task names where it came from, through a `Plan:`, `Groundwork:`, `Intake:`, or `Issue:` line under the title. Include each only when the file, folder, or issue it names exists.
209
209
 
210
- A task with no origin is either lost context or work nobody decided to do. The invariant runs both ways, and the second direction is the one that bites: a groundwork track or an open issue that no task points at is work already decided and on its way to being forgotten. An intake folder is exempt from that direction, since it dispositions many items at once and most of them close without ever becoming a task.
210
+ A task with no origin is either lost context or work nobody decided to do. The invariant runs both ways, and the second direction is the one that bites: a groundwork track, an intake folder, or an open issue that no task points at is work already decided and on its way to being forgotten.
211
+
212
+ An intake folder answers that direction at folder scope rather than item scope, since one dump dispositions many items and most close without ever becoming a task. What names a folder is every item answered and no task citing it, on the board or in the archive. That is a dump nobody acted on. Counting the archive beside the board is what separates it from one already promoted and shipped, and a check reading the board alone calls every finished folder abandoned.
211
213
 
212
214
  `Plan:`, `Groundwork:`, and `Intake:` name their target as a markdown link whose text is the file or folder stem, so the line resolves on a ctrl-click the way `priority.md` rows already do. Write the path relative to `.claude/tasks/`, which makes it `../plans/`, `../groundwork/`, and `../intake/`. A path written from the project root renders as a link and resolves to nothing in an editor rooted at the project. `Issue:` stays a bare `#NNN`, since an issue number is not a path and a full URL would write the remote into a gitignored file.
213
215