@erclx/aitk 1.2.0 → 1.4.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/docs/agents/commands.md +2 -1
- package/docs/agents/index.md +1 -1
- package/docs/agents/output-shape.md +13 -0
- package/docs/agents/records.md +32 -4
- package/docs/ai-workflow.md +3 -1
- package/docs/visual-design-workflow.md +2 -0
- package/package.json +1 -1
- package/src/cli.ts +6 -5
- package/src/commands/claude.ts +4 -4
- package/src/commands/design.ts +2 -6
- package/src/commands/feedback.ts +2 -4
- package/src/commands/gov.ts +3 -3
- package/src/commands/init.ts +3 -5
- package/src/commands/records.ts +145 -7
- package/src/commands/slides.ts +4 -6
- package/src/commands/snippets.ts +2 -3
- package/src/commands/standards.ts +2 -4
- package/src/commands/sync.ts +3 -4
- package/src/commands/tooling.ts +2 -3
- package/src/commands/transcripts.ts +2 -6
- package/src/commands/wiki.ts +2 -3
- package/src/design/parse.ts +42 -5
- package/src/design/render.ts +106 -27
- package/src/records/size.ts +260 -0
- package/src/sync/engine.ts +3 -4
- package/src/sync/workflow.ts +2 -3
- package/src/ui.ts +66 -6
- package/standards/design.md +10 -0
package/docs/agents/commands.md
CHANGED
|
@@ -37,7 +37,8 @@ Full help: `aitk <command> --help`. Behavior notes for the install and sync verb
|
|
|
37
37
|
| `aitk teach resource` | Record sources and leads in a workspace, repeating `--read` or `--lead` as `<title>=<url>` (`--json`) |
|
|
38
38
|
| `aitk teach glossary` | Add terms to a workspace glossary alphabetically, repeating `--term <term>=<definition>` (`--json`) |
|
|
39
39
|
| `aitk records validate` | Report a session record or a standard against the standard governing it, per kind (`--json`) |
|
|
40
|
-
| `aitk records
|
|
40
|
+
| `aitk records size` | Report what each record folder holds and how much of it is recent, heaviest first (`--json`) |
|
|
41
|
+
| `aitk records push` | Commit the nine backed record folders and push them to a private records remote (`--json`) |
|
|
41
42
|
| `aitk records pull` | Fetch the records remote and write it back, refusing rather than discarding unpushed records (`--json`) |
|
|
42
43
|
| `aitk sessions list` | Resolve live sessions to the worktree and branch each holds, filtered by `--branch` (`--json`) |
|
|
43
44
|
| `aitk comments scan` | Measure comment density by language and comment kind, with a trend recomputed from git |
|
package/docs/agents/index.md
CHANGED
|
@@ -21,7 +21,7 @@ CLI catalog and invocation rules for agents, split by command domain. Start with
|
|
|
21
21
|
- [Markdown audit](markdown-audit.md): Running the audit over any markdown path, where its bans and checkpoints are read from, what each check reports, and why the ban half gates while the structural half reports
|
|
22
22
|
- [Output shape](output-shape.md): Two framed shapes every command renders into, how JSON and --names modes keep stdout clean, and the exit discipline that lets piped output drain
|
|
23
23
|
- [Overview](overview.md): What this folder covers, the invocation rules every command inherits, and where domain behavior is documented instead
|
|
24
|
-
- [Records](records.md): Validating the session records under .claude/ and the standards corpus, the per-kind checks, the refusal reasons, backing the folders to a private remote, and which root each kind defaults to
|
|
24
|
+
- [Records](records.md): Validating the session records under .claude/ and the standards corpus, the per-kind checks, the refusal reasons, reading each folder's size and growth, backing the folders to a private remote, and which root each kind defaults to
|
|
25
25
|
- [Sandbox](sandbox.md): Scenario routing, the expectation scoring surface, and the coverage census over scenarios and skills
|
|
26
26
|
- [Scripting](scripting.md): The runtime catalogs that replace hardcoded names, what each carries, and a headless invocation per domain
|
|
27
27
|
- [Sessions](sessions.md): Resolving live peer sessions to the worktree and branch each holds, the liveness confidence field, the unresolved reasons, and what the read depends on
|
|
@@ -46,6 +46,19 @@ Help skips the banner. The `Usage:` line sits directly on `├`. Help writes to
|
|
|
46
46
|
|
|
47
47
|
`--json` and `--names` keep stdout clean and machine-readable. The frame still renders on stderr (open, banner, close) so the stream discipline is consistent across modes. Consumers that only read stdout see pure data.
|
|
48
48
|
|
|
49
|
+
## Color
|
|
50
|
+
|
|
51
|
+
Escape sequences reach a destination that renders them and nowhere else. The question is asked per stream rather than once for the process, so a run piping its data while keeping a terminal on stderr still gets color on the frame.
|
|
52
|
+
|
|
53
|
+
Either condition alone turns color off:
|
|
54
|
+
|
|
55
|
+
- `NO_COLOR` is set to any non-empty value, whatever that value says
|
|
56
|
+
- The destination is not a terminal, which covers a pipe, a file, and a captured session
|
|
57
|
+
|
|
58
|
+
The frame survives both. `┌`, `│`, `├`, `└`, and the `✓ ! + - ✗` marks are structure rather than color, and they are what lets a captured run still read as one block. A caller wanting neither the frame nor the color reads `--json` instead.
|
|
59
|
+
|
|
60
|
+
Terminal control is a separate question this section does not cover. The cursor and key sequences an interactive prompt writes run only where a terminal already exists.
|
|
61
|
+
|
|
49
62
|
## Process exit
|
|
50
63
|
|
|
51
64
|
A command action sets `process.exitCode` and returns. Calling `process.exit()` there ends the process before a stdout write drains, which truncates piped output at the 64K pipe buffer while still reporting the right exit code. Redirecting to a file hides the truncation, so it surfaces only through a pipe, which is what a check has to use to catch it.
|
package/docs/agents/records.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Records
|
|
3
|
-
description: Validating the session records under .claude/ and the standards corpus, the per-kind checks, the refusal reasons, backing the folders to a private remote, and which root each kind defaults to
|
|
3
|
+
description: Validating the session records under .claude/ and the standards corpus, the per-kind checks, the refusal reasons, reading each folder's size and growth, backing the folders to a private remote, and which root each kind defaults to
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Records
|
|
@@ -72,6 +72,34 @@ aitk records validate plans --json | jq -r '.findings[] | "\(.kind): \(.subject)
|
|
|
72
72
|
|
|
73
73
|
For the shapes each check enforces, see `.claude/standards/plan.md`, `.claude/standards/groundwork.md`, `.claude/standards/intake.md`, `.claude/standards/memory.md`, and `.claude/standards/standard.md`.
|
|
74
74
|
|
|
75
|
+
## Size
|
|
76
|
+
|
|
77
|
+
`aitk records size` reports what each record folder holds and how much of it is recent. It reads the nine backed folders named under Push and pull, plus `.claude/.tmp`, and it gates nothing.
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
aitk records size
|
|
81
|
+
aitk records size --json
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
| Option | Behavior |
|
|
85
|
+
| --------------- | --------------------------------------------- |
|
|
86
|
+
| `--json` | Add a machine-readable record on stdout |
|
|
87
|
+
| `--root <path>` | Project root, defaulting to the main worktree |
|
|
88
|
+
|
|
89
|
+
The table carries one row per folder that exists, heaviest first, with the file count, the bytes, a count for each growth window, and the dates of the least and most recently written file. Those dates render in the machine's local time, which is the calendar day whoever wrote the file was living in, and the reading is per-machine already. Folders that do not exist are named on one line below it rather than printed as rows of zeros. The record a `--json` call emits carries every folder either way, each with a `present` flag, so a caller reading the record gets a stable set of keys and can tell an absent folder from one the reading skipped.
|
|
90
|
+
|
|
91
|
+
Ordering by weight is what makes the reading worth taking. A folder listed alphabetically hides behind its neighbors, and the row a reader came for is the one that grew.
|
|
92
|
+
|
|
93
|
+
The reading carries two windows rather than one, at 7 and 30 days. A single window cannot separate a folder growing steadily from one that took a single batch: a folder whose 7-day count is most of its 30-day count moved in one pass, and one where the two are proportional is growing at a rate.
|
|
94
|
+
|
|
95
|
+
Nothing fails on a number here. A record folder has no correct size, so the reading is a number to notice rather than a threshold to gate, and the point of the verb is that the next reading is taken by a command instead of by someone remembering to count the folder. The memory pen went from 44 entries to 236 between two readings taken by hand two weeks apart, which is the measurement this replaces.
|
|
96
|
+
|
|
97
|
+
`.claude/.tmp` is read here and skipped by a backup, because deletable without loss is not the same as empty. The routing handoffs and the memory archive both sit there and both accumulate. `.claude/.records.git` stays out because it is the backup history rather than a record, and `.claude/worktrees/` stays out because each entry is a checkout of the project with its own removal verb, and one of them outweighs every record folder combined.
|
|
98
|
+
|
|
99
|
+
The window counts read `mtime`, so what they report is a file written inside the window rather than one created there. An entry edited long after it landed reads as recent, which overstates growth and never understates it, and these folders are append-mostly so the two readings agree on nearly every file. The one reading that is wrong rather than early is a machine restored by `aitk records pull`, which resets the work tree hard and re-dates every file it writes, so a window taken there counts the restore. Nothing on the filesystem separates the two, since a restored file is new by every stamp it carries.
|
|
100
|
+
|
|
101
|
+
Exit codes: `0` the reading completed, `1` refused. The one refusal is `no-folder`, raised when the root holds no `.claude` directory at all.
|
|
102
|
+
|
|
75
103
|
## Push and pull
|
|
76
104
|
|
|
77
105
|
`aitk records push` commits the backed record folders to a private remote and pushes them. `aitk records pull` fetches the other direction and writes them back. Both take `--json` and `--root` the way `validate` does, and both exit `0` on agreement and `1` on a refusal.
|
|
@@ -82,9 +110,9 @@ aitk records push --json
|
|
|
82
110
|
aitk records pull
|
|
83
111
|
```
|
|
84
112
|
|
|
85
|
-
The backed folders are `groundwork`, `intake`, `memory`, `plans`, `plans-archive`, `review`, `task-archive`, and `
|
|
113
|
+
The backed folders are `groundwork`, `intake`, `memory`, `plans`, `plans-archive`, `review`, `task-archive`, `tasks`, and `teach`, all under `.claude/`. They are the gitignored Claude group minus `.claude/.tmp`, which is deletable without loss, and `.claude/worktrees/`, whose contents belong to the project repository already. The list is a constant rather than configuration, matching the four folder names `validate` hardcodes.
|
|
86
114
|
|
|
87
|
-
Records are gitignored by design, so the history lives in a second git directory at `.claude/.records.git` with `.claude/` as its work tree. Every path stays where it is, which is what a separate checkout could not do. The verbs stage the
|
|
115
|
+
Records are gitignored by design, so the history lives in a second git directory at `.claude/.records.git` with `.claude/` as its work tree. Every path stays where it is, which is what a separate checkout could not do. The verbs stage the nine folders by explicit pathspec with `--force`, so nothing outside them can enter the index however the ignore rules read, and the project working tree and its index are never touched.
|
|
88
116
|
|
|
89
117
|
### Setup
|
|
90
118
|
|
|
@@ -113,7 +141,7 @@ Point it at a private repository, and at one that is not a remote of the project
|
|
|
113
141
|
| `local-ahead` | `pull` found local commits that never reached the origin |
|
|
114
142
|
| `git-failed` | A git call failed, with its stderr in the message |
|
|
115
143
|
|
|
116
|
-
The two `pull` refusals exist because the directions are not symmetric. A push only adds, while a pull onto a machine holding work that never left it would discard that work. Resolve either by running `push` first, or by moving the local folders aside. A machine holding none of the
|
|
144
|
+
The two `pull` refusals exist because the directions are not symmetric. A push only adds, while a pull onto a machine holding work that never left it would discard that work. Resolve either by running `push` first, or by moving the local folders aside. A machine holding none of the nine has nothing to lose, so a restore onto a fresh checkout runs straight through.
|
|
117
145
|
|
|
118
146
|
### When it runs
|
|
119
147
|
|
package/docs/ai-workflow.md
CHANGED
|
@@ -93,10 +93,12 @@ An execution that picks other than the suggestion rewrites the `- Suggested:` li
|
|
|
93
93
|
|
|
94
94
|
`aitk records validate plans` reports where a plan and that standard disagree: a filename that is not `feature-<slug>.md`, a missing required section, a files-to-touch entry naming no file, and a question carrying a suggestion with no answer slot. The same verb takes `groundwork`, `intake`, `memory`, and `teach`, which are governed the same way and were unreachable for the same reason. Nothing fires it automatically, because all five folders are gitignored and every check the repository runs reads changed files from git. It reports and never writes, since the folders are per-machine scratch with no history to recover a wrong repair from.
|
|
95
95
|
|
|
96
|
-
`aitk records push` carries these folders off the disk they live on, and `aitk records pull` brings them back.
|
|
96
|
+
`aitk records push` carries these folders off the disk they live on, and `aitk records pull` brings them back. Nine of them are backed: `groundwork`, `intake`, `memory`, `plans`, `plans-archive`, `review`, `task-archive`, `tasks`, and `teach`. The history lives in a second git directory at `.claude/.records.git` with `.claude/` as its work tree, so every path a task file cites stays where it is.
|
|
97
97
|
|
|
98
98
|
A person points it at a private repository once and both verbs refuse until they have, and `push` refuses when that origin is also a remote of the project, since the payload is the memory pen and the groundwork trails. `.husky/post-merge` runs the push after its archive loop, on every merge rather than only on one that closed a task. See [records](agents/records.md) for the refusal table.
|
|
99
99
|
|
|
100
|
+
`aitk records size` reports what each of these folders holds, heaviest first, along with `.claude/.tmp`. Each row carries the file count, the bytes, how many files were written in the last 7 and 30 days, and the dates of the least and most recently written one. Nothing fails on a number, because a record folder has no correct size. What the verb replaces is a reading somebody had to remember to take: the memory pen went from 44 entries to 236 between two counts made by hand a fortnight apart, and nothing reported the rate in between.
|
|
101
|
+
|
|
100
102
|
A plan that ships is archived, never deleted. `aitk:claude-docs` moves it to `.claude/plans-archive/` and retargets the task file's `Plan:` line at the new location, so a completed task still leads to the reasoning behind it. Both folders are gitignored, which is why a deleted plan had no recovery path. A plan cited by more than one task stays put until the last of them closes, since moving it early would strand every other pointer.
|
|
101
103
|
|
|
102
104
|
The sweep reads the whole board rather than the tasks the session touched. It is the one place the skill reaches past its own rule against editing a task file the session did not change, because a task that closed while an earlier run missed its archive is exactly what the sweep exists to clear. Reaching it is safe: the archive moves the plan and retargets the pointer in the same pass, so an untouched task ends up with a working link rather than a broken one.
|
|
@@ -24,6 +24,8 @@ The toolkit seed in `tooling/claude/seeds/.claude/DESIGN.md` ships a token-table
|
|
|
24
24
|
|
|
25
25
|
The `aitk:claude-design-extract` skill drafts the file, sourcing tokens from a project's existing prose and CLI UI surfaces, or proposing them from `.claude/REQUIREMENTS.md` and a `## Personality` paragraph when no UI code exists yet. `aitk design render` writes an HTML plus CSS preview to `.claude/review/design/` for eyeballing the current system without leaving Claude Code. See `.claude/context/design.md`.
|
|
26
26
|
|
|
27
|
+
A cell no source anchors ends in `? verify`, and the preview shows that marker beside the value rather than folding it in, so a swatch and a font sample stay built from the value alone. A confidence line above the sections names how many cells are anchored against how many are tagged, which is what tells a reader whether they are looking at a record of the code or a proposal about it. It reads the columns a source could anchor and leaves out the row names, so the ratio is not diluted by cells no tag could ever reach. The proposal path tags nearly all of them, so that count reads low on day one by design.
|
|
28
|
+
|
|
27
29
|
### Tools
|
|
28
30
|
|
|
29
31
|
- None beyond Claude Code itself
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -28,12 +28,12 @@ import { register as sessions } from '@/commands/sessions'
|
|
|
28
28
|
import { register as audits } from '@/commands/audits'
|
|
29
29
|
import { register as upgrade } from '@/commands/upgrade'
|
|
30
30
|
import { readInstalled, UNKNOWN_LABEL } from '@/version/installed'
|
|
31
|
-
|
|
32
|
-
const GREY = '\x1b[0;90m'
|
|
33
|
-
const WHITE = '\x1b[1;37m'
|
|
34
|
-
const NC = '\x1b[0m'
|
|
31
|
+
import { palette } from '@/ui'
|
|
35
32
|
|
|
36
33
|
function showHelp(): void {
|
|
34
|
+
// The help text is the one framed surface written to stdout, so it asks
|
|
35
|
+
// about that stream rather than the stderr every other writer here uses.
|
|
36
|
+
const { GREY, NC, WHITE } = palette(process.stdout)
|
|
37
37
|
const lines = [
|
|
38
38
|
`${GREY}┌${NC}`,
|
|
39
39
|
`${GREY}├${NC} ${WHITE}Usage:${NC} aitk [command]`,
|
|
@@ -61,7 +61,7 @@ function showHelp(): void {
|
|
|
61
61
|
`${GREY}│${NC} comments [cmd] ${GREY}# Measure comment density and trend (scan)${NC}`,
|
|
62
62
|
`${GREY}│${NC} context [cmd] ${GREY}# Report context folder health (audit)${NC}`,
|
|
63
63
|
`${GREY}│${NC} markdown [cmd] ${GREY}# Report markdown against the attribute standards (audit)${NC}`,
|
|
64
|
-
`${GREY}│${NC} records [cmd] ${GREY}# Session records under .claude/ (validate, push, pull)${NC}`,
|
|
64
|
+
`${GREY}│${NC} records [cmd] ${GREY}# Session records under .claude/ (validate, size, push, pull)${NC}`,
|
|
65
65
|
`${GREY}│${NC} sessions [cmd] ${GREY}# Resolve live sessions to worktree and branch (list)${NC}`,
|
|
66
66
|
`${GREY}│${NC} audits [cmd] ${GREY}# Run every health check as one set (run, list)${NC}`,
|
|
67
67
|
`${GREY}│${NC} upgrade ${GREY}# Reinstall the CLI globally with the manager that installed it${NC}`,
|
|
@@ -101,6 +101,7 @@ function showHelp(): void {
|
|
|
101
101
|
`${GREY}│${NC} aitk context audit --json`,
|
|
102
102
|
`${GREY}│${NC} aitk markdown audit .claude/rules --json`,
|
|
103
103
|
`${GREY}│${NC} aitk records validate plans`,
|
|
104
|
+
`${GREY}│${NC} aitk records size --json`,
|
|
104
105
|
`${GREY}│${NC} aitk records push --json`,
|
|
105
106
|
`${GREY}│${NC} aitk sessions list --json`,
|
|
106
107
|
`${GREY}│${NC} aitk audits run --json`,
|
package/src/commands/claude.ts
CHANGED
|
@@ -44,16 +44,13 @@ import {
|
|
|
44
44
|
logStep,
|
|
45
45
|
logWarn,
|
|
46
46
|
outro,
|
|
47
|
+
palette,
|
|
47
48
|
pipeOutput,
|
|
48
49
|
plural,
|
|
49
50
|
select,
|
|
50
51
|
} from '@/ui'
|
|
51
52
|
import { describeSkew, readSkew, type SkewReport } from '@/version/skew'
|
|
52
53
|
|
|
53
|
-
const GREEN = '\x1b[0;32m'
|
|
54
|
-
const GREY = '\x1b[0;90m'
|
|
55
|
-
const NC = '\x1b[0m'
|
|
56
|
-
|
|
57
54
|
interface SeedsListOptions {
|
|
58
55
|
readonly json?: boolean
|
|
59
56
|
readonly names?: boolean
|
|
@@ -266,6 +263,7 @@ export function register(program: Command): void {
|
|
|
266
263
|
}
|
|
267
264
|
|
|
268
265
|
function succeed(message: string): number {
|
|
266
|
+
const { GREEN, NC } = palette(process.stderr)
|
|
269
267
|
outro()
|
|
270
268
|
process.stderr.write(`${GREEN}✓ ${message}${NC}\n`)
|
|
271
269
|
return 0
|
|
@@ -472,6 +470,7 @@ async function runSeedsList(opts: SeedsListOptions): Promise<number> {
|
|
|
472
470
|
return 0
|
|
473
471
|
}
|
|
474
472
|
|
|
473
|
+
const { GREY, NC } = palette(process.stderr)
|
|
475
474
|
intro('aitk claude')
|
|
476
475
|
logStep('Seed docs')
|
|
477
476
|
for (const listing of listings) {
|
|
@@ -531,6 +530,7 @@ async function runSkillsDrift(
|
|
|
531
530
|
// is the moment a skew warning is worth most, since an old binary is one
|
|
532
531
|
// reason the cache and the CLI disagree in the first place.
|
|
533
532
|
if (skew.state === 'behind') {
|
|
533
|
+
const { GREY, NC } = palette(process.stderr)
|
|
534
534
|
process.stderr.write(`${GREY}${describeSkew(skew)}${NC}\n`)
|
|
535
535
|
}
|
|
536
536
|
}
|
package/src/commands/design.ts
CHANGED
|
@@ -2,12 +2,7 @@ import { existsSync } from 'node:fs'
|
|
|
2
2
|
import { resolve } from 'node:path'
|
|
3
3
|
import type { Command } from 'commander'
|
|
4
4
|
import { renderDesignDoc } from '@/design/render'
|
|
5
|
-
|
|
6
|
-
const GREY = '\x1b[0;90m'
|
|
7
|
-
const WHITE = '\x1b[1;37m'
|
|
8
|
-
const RED = '\x1b[0;31m'
|
|
9
|
-
const GREEN = '\x1b[0;32m'
|
|
10
|
-
const NC = '\x1b[0m'
|
|
5
|
+
import { palette } from '@/ui'
|
|
11
6
|
|
|
12
7
|
export function register(program: Command): void {
|
|
13
8
|
const design = program
|
|
@@ -22,6 +17,7 @@ export function register(program: Command): void {
|
|
|
22
17
|
.action((opts: { source: string; out: string }) => {
|
|
23
18
|
const sourcePath = resolve(process.cwd(), opts.source)
|
|
24
19
|
const outDir = resolve(process.cwd(), opts.out)
|
|
20
|
+
const { GREEN, GREY, NC, RED, WHITE } = palette(process.stderr)
|
|
25
21
|
if (!existsSync(sourcePath)) {
|
|
26
22
|
process.stderr.write(
|
|
27
23
|
`${GREY}┌${NC}\n${GREY}│${NC} ${RED}✗${NC} ${opts.source} not found\n${GREY}└${NC}\n`,
|
package/src/commands/feedback.ts
CHANGED
|
@@ -4,10 +4,7 @@ import type { Command } from 'commander'
|
|
|
4
4
|
import { deriveSlug, deriveTitle } from '@/commands/feedback-format'
|
|
5
5
|
import { PROJECT_ROOT } from '@/project-root'
|
|
6
6
|
import { createGithubIssue } from '@/github'
|
|
7
|
-
import { frameError, frameSuccess } from '@/ui'
|
|
8
|
-
|
|
9
|
-
const YELLOW = '\x1b[0;33m'
|
|
10
|
-
const NC = '\x1b[0m'
|
|
7
|
+
import { frameError, frameSuccess, palette } from '@/ui'
|
|
11
8
|
|
|
12
9
|
function readStdin(): Promise<string> {
|
|
13
10
|
return new Promise((resolveStream, rejectStream) => {
|
|
@@ -88,6 +85,7 @@ export function register(program: Command): void {
|
|
|
88
85
|
process.exitCode = 1
|
|
89
86
|
return
|
|
90
87
|
}
|
|
88
|
+
const { NC, YELLOW } = palette(process.stderr)
|
|
91
89
|
process.stderr.write(
|
|
92
90
|
`${YELLOW}! gh unavailable, wrote local scratch instead${NC}\n`,
|
|
93
91
|
)
|
package/src/commands/gov.ts
CHANGED
|
@@ -30,12 +30,10 @@ import {
|
|
|
30
30
|
logStep,
|
|
31
31
|
logWarn,
|
|
32
32
|
outro,
|
|
33
|
+
palette,
|
|
33
34
|
select,
|
|
34
35
|
} from '@/ui'
|
|
35
36
|
|
|
36
|
-
const GREEN = '\x1b[0;32m'
|
|
37
|
-
const NC = '\x1b[0m'
|
|
38
|
-
|
|
39
37
|
const PAYLOAD_REL = join('.claude', '.tmp', 'gov', 'rules.md')
|
|
40
38
|
const RULES_REL = join('.claude', 'rules')
|
|
41
39
|
|
|
@@ -461,6 +459,7 @@ async function runInstall(
|
|
|
461
459
|
)
|
|
462
460
|
}
|
|
463
461
|
|
|
462
|
+
const { GREEN, NC } = palette(process.stderr)
|
|
464
463
|
outro()
|
|
465
464
|
process.stderr.write(`${GREEN}✓ Rules installed${NC}\n`)
|
|
466
465
|
return 0
|
|
@@ -510,6 +509,7 @@ async function runBuild(target: string): Promise<number> {
|
|
|
510
509
|
await writeFile(output, buildRulesPayload(files))
|
|
511
510
|
logAdd(PAYLOAD_REL)
|
|
512
511
|
|
|
512
|
+
const { GREEN, NC } = palette(process.stderr)
|
|
513
513
|
outro()
|
|
514
514
|
process.stderr.write(
|
|
515
515
|
`${GREEN}✓ Rules built (${files.length} rules → ${PAYLOAD_REL})${NC}\n`,
|
package/src/commands/init.ts
CHANGED
|
@@ -6,11 +6,7 @@ import { type InitFlags, parseSkip, planInit } from '@/init/plan'
|
|
|
6
6
|
import { runDomains } from '@/init/run'
|
|
7
7
|
import { buildSteps } from '@/init/steps'
|
|
8
8
|
import { resolveTarget } from '@/target'
|
|
9
|
-
import { intro, logInfo, logStep, logWarn, outro, select } from '@/ui'
|
|
10
|
-
|
|
11
|
-
const GREEN = '\x1b[0;32m'
|
|
12
|
-
const YELLOW = '\x1b[0;33m'
|
|
13
|
-
const NC = '\x1b[0m'
|
|
9
|
+
import { intro, logInfo, logStep, logWarn, outro, palette, select } from '@/ui'
|
|
14
10
|
|
|
15
11
|
interface InitOptions {
|
|
16
12
|
/** Always present: the option falls back to `DEFAULT_STACK`. */
|
|
@@ -107,6 +103,8 @@ async function runInit(
|
|
|
107
103
|
outro()
|
|
108
104
|
process.stderr.write('\n')
|
|
109
105
|
|
|
106
|
+
const { GREEN, NC, YELLOW } = palette(process.stderr)
|
|
107
|
+
|
|
110
108
|
if (failed.length === 0) {
|
|
111
109
|
process.stderr.write(
|
|
112
110
|
`${GREEN}✓ Project initialized (${plan.total} domains)${NC}\n`,
|
package/src/commands/records.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import type { Command } from 'commander'
|
|
2
|
+
import { BACKED_FOLDERS, pullRecords, pushRecords } from '@/records/backup'
|
|
2
3
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
type FolderSize,
|
|
5
|
+
formatBytes,
|
|
6
|
+
GROWTH_WINDOWS,
|
|
7
|
+
SIZED_FOLDERS,
|
|
8
|
+
type SizeOutcome,
|
|
9
|
+
sizeRecords,
|
|
10
|
+
} from '@/records/size'
|
|
9
11
|
import {
|
|
10
12
|
type Finding,
|
|
11
13
|
isRecordKind,
|
|
@@ -23,6 +25,7 @@ import {
|
|
|
23
25
|
logWarn,
|
|
24
26
|
outro,
|
|
25
27
|
pipeOutput,
|
|
28
|
+
plural,
|
|
26
29
|
} from '@/ui'
|
|
27
30
|
import { currentWorktreeRoot, mainWorktreeRoot } from '@/worktree'
|
|
28
31
|
|
|
@@ -87,6 +90,41 @@ export function register(program: Command): void {
|
|
|
87
90
|
process.exitCode = await runValidate(kind, opts)
|
|
88
91
|
})
|
|
89
92
|
|
|
93
|
+
records
|
|
94
|
+
.command('size')
|
|
95
|
+
.description('Report what each record folder holds and how much is recent')
|
|
96
|
+
.helpOption('-h, --help', 'Show this help message')
|
|
97
|
+
.option('--json', 'Add a machine-readable record on stdout')
|
|
98
|
+
.option('--root <path>', 'Project root, defaulting to the main worktree')
|
|
99
|
+
.addHelpText(
|
|
100
|
+
'after',
|
|
101
|
+
[
|
|
102
|
+
'',
|
|
103
|
+
'Folders read under .claude/:',
|
|
104
|
+
` ${SIZED_FOLDERS.join(', ')}`,
|
|
105
|
+
'',
|
|
106
|
+
'Exit codes:',
|
|
107
|
+
' 0 the reading completed',
|
|
108
|
+
' 1 refused, with the reason on stderr or in the JSON record',
|
|
109
|
+
'',
|
|
110
|
+
'It gates nothing. A record folder has no correct size, so the reading is',
|
|
111
|
+
'a number to notice rather than a threshold to fail, and a session takes it',
|
|
112
|
+
`by running this rather than by counting the folder. The ${GROWTH_WINDOWS.join(
|
|
113
|
+
' and ',
|
|
114
|
+
)} day`,
|
|
115
|
+
'counts read mtime, so a file rewritten long after it landed reads as recent,',
|
|
116
|
+
'and a machine restored by records pull reads its whole tree as one week old.',
|
|
117
|
+
'',
|
|
118
|
+
'Examples:',
|
|
119
|
+
' aitk records size',
|
|
120
|
+
' aitk records size --json',
|
|
121
|
+
'',
|
|
122
|
+
].join('\n'),
|
|
123
|
+
)
|
|
124
|
+
.action(async (opts: BackupCommandOptions) => {
|
|
125
|
+
process.exitCode = await runSize(opts)
|
|
126
|
+
})
|
|
127
|
+
|
|
90
128
|
records
|
|
91
129
|
.command('push')
|
|
92
130
|
.description(
|
|
@@ -149,6 +187,99 @@ function backupHelp(verb: 'push' | 'pull'): string {
|
|
|
149
187
|
].join('\n')
|
|
150
188
|
}
|
|
151
189
|
|
|
190
|
+
async function runSize(opts: BackupCommandOptions): Promise<number> {
|
|
191
|
+
const root = opts.root ?? (await mainWorktreeRoot())
|
|
192
|
+
const outcome = await sizeRecords(root)
|
|
193
|
+
|
|
194
|
+
if (!outcome.ok)
|
|
195
|
+
return reportRefusal('aitk records size', outcome, opts.json ?? false)
|
|
196
|
+
|
|
197
|
+
if (opts.json ?? false) {
|
|
198
|
+
process.stdout.write(`${JSON.stringify(outcome)}\n`)
|
|
199
|
+
return 0
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
reportSize(outcome)
|
|
203
|
+
return 0
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Widest cell in the column, so a row lines up against the header as well. */
|
|
207
|
+
function columnWidth(header: string, cells: readonly string[]): number {
|
|
208
|
+
return Math.max(header.length, ...cells.map((cell) => cell.length))
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function sizeRow(entry: FolderSize): string[] {
|
|
212
|
+
return [
|
|
213
|
+
entry.folder,
|
|
214
|
+
String(entry.files),
|
|
215
|
+
formatBytes(entry.bytes),
|
|
216
|
+
...entry.touched.map((window) => String(window.files)),
|
|
217
|
+
entry.oldest ?? '',
|
|
218
|
+
entry.newest ?? '',
|
|
219
|
+
]
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Renders the present folders as a table, heaviest first.
|
|
224
|
+
*
|
|
225
|
+
* The order is what makes the reading worth taking. A folder listed
|
|
226
|
+
* alphabetically hides behind its neighbors, and the one that grew is the row
|
|
227
|
+
* a reader came for, so it leads.
|
|
228
|
+
*/
|
|
229
|
+
function reportSize(outcome: Extract<SizeOutcome, { ok: true }>): void {
|
|
230
|
+
const present = outcome.folders
|
|
231
|
+
.filter((entry) => entry.present)
|
|
232
|
+
.toSorted((left, right) => right.files - left.files)
|
|
233
|
+
const absent = outcome.folders
|
|
234
|
+
.filter((entry) => !entry.present)
|
|
235
|
+
.map((entry) => entry.folder)
|
|
236
|
+
|
|
237
|
+
intro('aitk records size')
|
|
238
|
+
logStep('Folders')
|
|
239
|
+
|
|
240
|
+
if (present.length === 0) {
|
|
241
|
+
logInfo('none of the record folders exist yet')
|
|
242
|
+
} else {
|
|
243
|
+
const headers = [
|
|
244
|
+
'folder',
|
|
245
|
+
'files',
|
|
246
|
+
'size',
|
|
247
|
+
...GROWTH_WINDOWS.map((days) => `${days}d`),
|
|
248
|
+
'oldest',
|
|
249
|
+
'newest',
|
|
250
|
+
]
|
|
251
|
+
const rows = present.map(sizeRow)
|
|
252
|
+
const widths = headers.map((header, column) =>
|
|
253
|
+
columnWidth(
|
|
254
|
+
header,
|
|
255
|
+
rows.map((row) => row[column]),
|
|
256
|
+
),
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
// The name column reads as a list and the rest as numbers, so one is
|
|
260
|
+
// left-aligned and the others are not.
|
|
261
|
+
const render = (cells: readonly string[]): string =>
|
|
262
|
+
cells
|
|
263
|
+
.map((cell, column) =>
|
|
264
|
+
column === 0
|
|
265
|
+
? cell.padEnd(widths[column])
|
|
266
|
+
: cell.padStart(widths[column]),
|
|
267
|
+
)
|
|
268
|
+
.join(' ')
|
|
269
|
+
.trimEnd()
|
|
270
|
+
|
|
271
|
+
pipeOutput([render(headers), ...rows.map(render)].join('\n'))
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (absent.length > 0) logInfo(`absent: ${absent.join(', ')}`)
|
|
275
|
+
|
|
276
|
+
logStep('Total')
|
|
277
|
+
logInfo(
|
|
278
|
+
`${plural(outcome.files, 'file')}, ${formatBytes(outcome.bytes)} across ${plural(present.length, 'folder')}`,
|
|
279
|
+
)
|
|
280
|
+
outro()
|
|
281
|
+
}
|
|
282
|
+
|
|
152
283
|
async function runPush(opts: BackupCommandOptions): Promise<number> {
|
|
153
284
|
const root = opts.root ?? (await mainWorktreeRoot())
|
|
154
285
|
const outcome = await pushRecords(root)
|
|
@@ -197,9 +328,16 @@ async function runPull(opts: BackupCommandOptions): Promise<number> {
|
|
|
197
328
|
return 0
|
|
198
329
|
}
|
|
199
330
|
|
|
331
|
+
/**
|
|
332
|
+
* Reports a refusal from any of the three verbs that carry one.
|
|
333
|
+
*
|
|
334
|
+
* The parameter is structural rather than the union of their outcome types,
|
|
335
|
+
* because the three refusal vocabularies are separate lists and naming them all
|
|
336
|
+
* here would grow with every verb added.
|
|
337
|
+
*/
|
|
200
338
|
function reportRefusal(
|
|
201
339
|
banner: string,
|
|
202
|
-
outcome:
|
|
340
|
+
outcome: { readonly reason: string; readonly message: string },
|
|
203
341
|
emitJson: boolean,
|
|
204
342
|
): number {
|
|
205
343
|
if (emitJson) {
|
package/src/commands/slides.ts
CHANGED
|
@@ -5,12 +5,7 @@ import { LAYOUTS } from '@/slides/layouts'
|
|
|
5
5
|
import { openDeck } from '@/slides/open'
|
|
6
6
|
import { renderSlidesDoc } from '@/slides/render'
|
|
7
7
|
import type { Variant } from '@/slides/styles'
|
|
8
|
-
import { intro, outro } from '@/ui'
|
|
9
|
-
|
|
10
|
-
const GREY = '\x1b[0;90m'
|
|
11
|
-
const GREEN = '\x1b[0;32m'
|
|
12
|
-
const RED = '\x1b[0;31m'
|
|
13
|
-
const NC = '\x1b[0m'
|
|
8
|
+
import { intro, outro, palette } from '@/ui'
|
|
14
9
|
|
|
15
10
|
export function register(program: Command): void {
|
|
16
11
|
const slides = program
|
|
@@ -44,6 +39,7 @@ export function register(program: Command): void {
|
|
|
44
39
|
}
|
|
45
40
|
const variant = parseVariant(opts.variant)
|
|
46
41
|
const mirror = resolveMirror(opts.mirror)
|
|
42
|
+
const { GREEN, GREY, NC, RED } = palette(process.stderr)
|
|
47
43
|
intro('Render slides')
|
|
48
44
|
const result = await renderSlidesDoc(sourcePath, outDir, {
|
|
49
45
|
variant,
|
|
@@ -81,6 +77,7 @@ export function register(program: Command): void {
|
|
|
81
77
|
process.stdout.write(`${JSON.stringify(LAYOUTS)}\n`)
|
|
82
78
|
return
|
|
83
79
|
}
|
|
80
|
+
const { GREEN, GREY, NC } = palette(process.stderr)
|
|
84
81
|
intro('Slide layouts')
|
|
85
82
|
for (const layout of LAYOUTS) {
|
|
86
83
|
process.stderr.write(
|
|
@@ -121,6 +118,7 @@ function fail(message: string): never {
|
|
|
121
118
|
*/
|
|
122
119
|
function reportFailure(error: unknown): void {
|
|
123
120
|
if (!(error instanceof SlidesError)) throw error
|
|
121
|
+
const { GREY, NC, RED } = palette(process.stderr)
|
|
124
122
|
process.stderr.write(
|
|
125
123
|
`${GREY}┌${NC}\n${GREY}│${NC} ${RED}✗${NC} ${error.message}\n${GREY}└${NC}\n`,
|
|
126
124
|
)
|
package/src/commands/snippets.ts
CHANGED
|
@@ -21,12 +21,10 @@ import {
|
|
|
21
21
|
logStep,
|
|
22
22
|
logWarn,
|
|
23
23
|
outro,
|
|
24
|
+
palette,
|
|
24
25
|
select,
|
|
25
26
|
} from '@/ui'
|
|
26
27
|
|
|
27
|
-
const GREEN = '\x1b[0;32m'
|
|
28
|
-
const NC = '\x1b[0m'
|
|
29
|
-
|
|
30
28
|
const SNIPPETS_REL = '.claude/snippets'
|
|
31
29
|
|
|
32
30
|
const PASS_THROUGH_VERBS = ['create'] as const
|
|
@@ -180,6 +178,7 @@ async function runInstall(
|
|
|
180
178
|
}
|
|
181
179
|
await recordStamp(createSnippetsAdapter(PROJECT_ROOT), resolved, new Date())
|
|
182
180
|
|
|
181
|
+
const { GREEN, NC } = palette(process.stderr)
|
|
183
182
|
outro()
|
|
184
183
|
process.stderr.write(`${GREEN}✓ Snippets installed${NC}\n`)
|
|
185
184
|
return 0
|
|
@@ -21,13 +21,10 @@ import {
|
|
|
21
21
|
logStep,
|
|
22
22
|
logWarn,
|
|
23
23
|
outro,
|
|
24
|
+
palette,
|
|
24
25
|
select,
|
|
25
26
|
} from '@/ui'
|
|
26
27
|
|
|
27
|
-
const GREEN = '\x1b[0;32m'
|
|
28
|
-
const GREY = '\x1b[0;90m'
|
|
29
|
-
const NC = '\x1b[0m'
|
|
30
|
-
|
|
31
28
|
interface InstallOptions {
|
|
32
29
|
/** Always present: the option falls back to `ALL_SELECTION`. */
|
|
33
30
|
readonly only: string
|
|
@@ -196,6 +193,7 @@ async function runInstall(target: string, selection: string): Promise<number> {
|
|
|
196
193
|
await recordStamp(createStandardsAdapter(PROJECT_ROOT), resolved, new Date())
|
|
197
194
|
|
|
198
195
|
outro()
|
|
196
|
+
const { GREEN, GREY, NC } = palette(process.stderr)
|
|
199
197
|
process.stderr.write(
|
|
200
198
|
`\n${GREEN}✓ Standards installed${NC} ${GREY}(${files.length} files)${NC}\n`,
|
|
201
199
|
)
|
package/src/commands/sync.ts
CHANGED
|
@@ -29,13 +29,10 @@ import {
|
|
|
29
29
|
logStep,
|
|
30
30
|
logWarn,
|
|
31
31
|
outro,
|
|
32
|
+
palette,
|
|
32
33
|
} from '@/ui'
|
|
33
34
|
import { describeSkew } from '@/version/skew'
|
|
34
35
|
|
|
35
|
-
const GREY = '\x1b[0;90m'
|
|
36
|
-
const YELLOW = '\x1b[0;33m'
|
|
37
|
-
const NC = '\x1b[0m'
|
|
38
|
-
|
|
39
36
|
const SYNC_ARGS: Record<SyncDomain, readonly string[]> = {
|
|
40
37
|
standards: ['standards', 'sync'],
|
|
41
38
|
snippets: ['snippets', 'sync'],
|
|
@@ -180,6 +177,7 @@ function renderCheck(report: CheckReport): void {
|
|
|
180
177
|
)
|
|
181
178
|
if (uncovered.length === 0) return
|
|
182
179
|
|
|
180
|
+
const { GREY, NC } = palette(process.stderr)
|
|
183
181
|
process.stderr.write(
|
|
184
182
|
`${GREY}Unstamped: ${uncovered.join(', ')}. Run the matching sync to record one.${NC}\n`,
|
|
185
183
|
)
|
|
@@ -338,6 +336,7 @@ async function runSync(target: string): Promise<number> {
|
|
|
338
336
|
if (typeof resolved === 'number') return resolved
|
|
339
337
|
|
|
340
338
|
const git = createGitRunner(resolved)
|
|
339
|
+
const { GREY, NC, YELLOW } = palette(process.stderr)
|
|
341
340
|
|
|
342
341
|
logStep('Checking working tree')
|
|
343
342
|
if (!isTreeClean(await git.status([]))) {
|
package/src/commands/tooling.ts
CHANGED
|
@@ -28,12 +28,10 @@ import {
|
|
|
28
28
|
logStep,
|
|
29
29
|
logWarn,
|
|
30
30
|
outro,
|
|
31
|
+
palette,
|
|
31
32
|
select,
|
|
32
33
|
} from '@/ui'
|
|
33
34
|
|
|
34
|
-
const GREEN = '\x1b[0;32m'
|
|
35
|
-
const NC = '\x1b[0m'
|
|
36
|
-
|
|
37
35
|
const PASS_THROUGH_VERBS = ['ref', 'create', 'verify'] as const
|
|
38
36
|
|
|
39
37
|
interface SyncOptions {
|
|
@@ -256,6 +254,7 @@ async function runSync(
|
|
|
256
254
|
report(result, includeReferences)
|
|
257
255
|
|
|
258
256
|
const mode = resolveWriteMode(opts)
|
|
257
|
+
const { GREEN, NC } = palette(process.stderr)
|
|
259
258
|
|
|
260
259
|
if (result.totalChanges === 0) {
|
|
261
260
|
// The stamp is a write like any other, so a run with no authority to write
|