@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.
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/claude-address-review/SKILL.md +5 -6
- package/claude/skills/claude-autoship/SKILL.md +2 -2
- package/claude/skills/claude-design-extract/SKILL.md +3 -5
- package/claude/skills/claude-diagram/SKILL.md +2 -2
- package/claude/skills/claude-docs/SKILL.md +11 -11
- package/claude/skills/claude-feature/SKILL.md +3 -3
- package/claude/skills/claude-groundwork/SKILL.md +2 -2
- package/claude/skills/claude-intake/SKILL.md +1 -1
- package/claude/skills/claude-intake-answer/SKILL.md +1 -1
- package/claude/skills/claude-memory-capture/SKILL.md +7 -9
- package/claude/skills/claude-memory-review/SKILL.md +6 -8
- package/claude/skills/claude-memory-review/references/receipt-format.md +1 -1
- package/claude/skills/claude-orchestrate/SKILL.md +3 -3
- package/claude/skills/claude-orchestrate/references/orchestrator-handoff.md +1 -1
- package/claude/skills/claude-orchestrate/references/orchestrator-resume.md +1 -1
- package/claude/skills/claude-pr-review/SKILL.md +2 -2
- package/claude/skills/claude-review/SKILL.md +1 -1
- package/claude/skills/claude-seed-sync/SKILL.md +1 -1
- package/claude/skills/claude-standards-audit/SKILL.md +4 -4
- package/claude/skills/claude-tasks/SKILL.md +34 -9
- package/claude/skills/claude-teach/SKILL.md +2 -2
- package/claude/skills/claude-ui-test/SKILL.md +1 -1
- package/claude/skills/claude-ux-audit/SKILL.md +2 -4
- package/claude/skills/claude-ux-measure/SKILL.md +1 -1
- package/claude/skills/claude-worktree/SKILL.md +3 -3
- package/claude/skills/create-rule/SKILL.md +2 -2
- package/claude/skills/create-skill/SKILL.md +4 -6
- package/claude/skills/create-snippet/SKILL.md +1 -3
- package/claude/skills/create-standard/SKILL.md +2 -4
- package/claude/skills/decision-escalate/SKILL.md +1 -1
- package/claude/skills/docs-sync/SKILL.md +3 -5
- package/claude/skills/git-commit/SKILL.md +1 -3
- package/claude/skills/git-followup/SKILL.md +1 -1
- package/claude/skills/git-issue/SKILL.md +2 -4
- package/claude/skills/git-pr/SKILL.md +4 -6
- package/claude/skills/git-split/SKILL.md +1 -1
- package/claude/skills/git-stage/SKILL.md +1 -3
- package/claude/skills/migration-claude-md/SKILL.md +2 -2
- package/claude/skills/session-resume/SKILL.md +1 -1
- package/claude/skills/setup-gov/SKILL.md +1 -1
- package/claude/skills/setup-indexes/SKILL.md +1 -1
- package/claude/skills/write-human/SKILL.md +2 -2
- package/docs/agents/commands.md +2 -1
- package/docs/agents/index.md +1 -1
- package/docs/agents/markdown-audit.md +3 -1
- package/docs/agents/records.md +32 -4
- package/docs/ai-workflow.md +3 -1
- package/docs/target-projects.md +3 -3
- package/governance/rules/claude/501-markdown.md +1 -1
- package/governance/rules/claude/510-context.md +1 -1
- package/governance/rules/claude/520-wireframes.md +1 -1
- package/governance/rules/claude/530-requirements.md +1 -1
- package/governance/rules/claude/540-architecture.md +1 -1
- package/governance/rules/claude/550-design.md +1 -1
- package/governance/rules/claude/555-tasks.md +1 -1
- package/governance/rules/claude/556-groundwork.md +1 -1
- package/governance/rules/claude/557-intake.md +1 -1
- package/governance/rules/claude/558-plan.md +1 -1
- package/governance/rules/claude/559-memory.md +1 -1
- package/governance/rules/claude/560-diagrams.md +1 -1
- package/governance/rules/claude/561-teach.md +1 -1
- package/governance/rules/claude/562-session.md +1 -1
- package/governance/rules/claude/570-skill.md +1 -1
- package/governance/rules/claude/580-readme.md +1 -1
- package/governance/rules/claude/590-rule-authoring.md +1 -1
- package/governance/rules/claude/591-standard-authoring.md +1 -1
- package/package.json +1 -1
- package/src/cli.ts +2 -1
- package/src/commands/records.ts +145 -7
- package/src/markdown/structure.ts +39 -4
- package/src/records/size.ts +260 -0
- 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
|
+
}
|
package/standards/tasks.md
CHANGED
|
@@ -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.
|
|
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
|
|