@erclx/aitk 0.92.0 → 0.94.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-docs/SKILL.md +1 -1
- package/claude/skills/claude-orchestrate/references/orchestrator-handoff.md +7 -6
- package/claude/skills/claude-ux-audit/REQUIREMENT.md +1 -0
- package/claude/skills/claude-ux-audit/SKILL.md +2 -1
- package/claude/skills/claude-ux-measure/REQUIREMENT.md +45 -0
- package/claude/skills/claude-ux-measure/SKILL.md +110 -0
- package/claude/skills/setup-gov/REQUIREMENT.md +3 -0
- package/claude/skills/setup-gov/SKILL.md +5 -0
- package/claude/skills/setup-indexes/REQUIREMENT.md +3 -0
- package/claude/skills/setup-indexes/SKILL.md +1 -0
- package/claude/skills/setup-init/REQUIREMENT.md +8 -0
- package/claude/skills/setup-init/SKILL.md +16 -4
- package/claude/skills/toolkit-operator/REQUIREMENT.md +1 -0
- package/claude/skills/toolkit-operator/SKILL.md +5 -2
- package/docs/agents/commands.md +16 -15
- package/docs/agents/index.md +1 -1
- package/docs/agents/skills-audit.md +34 -1
- package/docs/ai-workflow.md +1 -0
- package/docs/target-projects.md +5 -1
- package/docs/visual-design-workflow.md +1 -0
- package/package.json +1 -1
- package/src/claude/skills-drift.ts +156 -0
- package/src/commands/claude.ts +106 -4
|
@@ -217,7 +217,7 @@ Write the retarget as a markdown link, `Plan: [feature-<slug>](../plans-archive/
|
|
|
217
217
|
|
|
218
218
|
If `.claude/review/review-<slug>.md` exists, delete it. `claude-review` writes with this convention. Do not sweep any other `review-*.md` file.
|
|
219
219
|
|
|
220
|
-
Do not sweep `ui-checklist-*.md` (pending human verification) or `ux-
|
|
220
|
+
Do not sweep `ui-checklist-*.md` (pending human verification), `ux-audit-*.md`, or `ux-measure-*.md` (standalone deliverables).
|
|
221
221
|
|
|
222
222
|
Output one line per file swept:
|
|
223
223
|
|
|
@@ -6,9 +6,10 @@ description: Memory capture at the close of a session, what to write to .claude/
|
|
|
6
6
|
Capture what the session learned, then write the pre-compact handoff as orchestrator. Do both before a compaction, because a compaction keeps conclusions and drops the reasoning that produced them, and no other file in the repository carries that reasoning.
|
|
7
7
|
|
|
8
8
|
1. Invoke `aitk:claude-memory-capture` and tell it this session does not commit. Capture reads the session and this handoff summarizes it, so capturing first lets the handoff cite what was written instead of restating the same lesson in prose.
|
|
9
|
-
2.
|
|
10
|
-
3.
|
|
11
|
-
4.
|
|
9
|
+
2. Run `aitk claude skills drift <commit this session started from>` and re-read any body it names before writing anything below. Nothing on the machine records that commit, so recover it from how long the session has been running with `git log -1 --format=%H --before='<duration> ago'`, rounding the duration up rather than down. A ref older than the oldest load over-reports and confirming a name costs one read of the body, so the generous end is the safe one and a guess at the exact commit is not worth making. A skill body enters a session once and re-invoking the skill replays the held copy rather than the file, so the drift is worst at exactly this moment and a name here is a body this session has been following out of date. Record what it named under `## Standing cautions`. The verb answers where the working directory carries `claude/skills/` with history behind it, which is the toolkit repository itself, and refuses by naming the absent tree anywhere else. A project consuming the plugin from a marketplace cache is that second case, so read the refusal as the boundary rather than as a fault.
|
|
10
|
+
3. Resolve the main worktree root with `git worktree list --porcelain | grep -m 1 '^worktree ' | cut -d' ' -f2-`, falling back to `pwd`. Write `.claude/tasks/session.md` under it.
|
|
11
|
+
4. Write only what a compaction destroys and no other file already carries. The board holds the ordering and what each task waits on, a task file holds its own findings, and a groundwork folder holds its track.
|
|
12
|
+
5. Use this shape, resolving `${CLAUDE_SKILL_DIR}/references/orchestrator-resume.md` and `${CLAUDE_SKILL_DIR}/references/orchestrator-poll.md` to absolute paths as you write it and pasting each in place of `<RESUME_RUNBOOK>` and `<POLL_RUNBOOK>`:
|
|
12
13
|
|
|
13
14
|
```markdown
|
|
14
15
|
---
|
|
@@ -41,10 +42,10 @@ Resume by loading the orchestrator skill and asking it to resume after a compact
|
|
|
41
42
|
That resume reads the board and stops. It restarts nothing, so the review poll is a second thing owed here, and <POLL_RUNBOOK> holds the prompt and the condition. Do not reach for `session-resume`, which reads tracked work and knows nothing about this board or the workers on it.
|
|
42
43
|
```
|
|
43
44
|
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
6. Cite a commit, a task, or a file and line for every claim, so the next session can tell a read from a recall.
|
|
46
|
+
7. Overwrite the previous handoff rather than appending to it. A stale entry read as current is worse than no handoff.
|
|
46
47
|
|
|
47
|
-
The substitution belongs in step
|
|
48
|
+
The substitution belongs in step 5 because step 7 ends the write. A reader who treats the list as finished there ships the literal placeholders, and the variable expands while this runbook renders rather than in the turn that reads the handoff back, so a path left unresolved reaches a session holding no skill as a string matching nothing. `orchestrator-poll.md` resolves its script at the same point and for the same reason.
|
|
48
49
|
|
|
49
50
|
Add a section only for content that fits none of the four and would otherwise be lost. Do not restate the board, and do not summarize the work that shipped, because git already carries it. The closing block is the one exception, and the paragraph below states why.
|
|
50
51
|
|
|
@@ -37,3 +37,4 @@ The pass slides into fixing what it finds, and then the audit and the change lan
|
|
|
37
37
|
- Feature planning, which `claude-feature` owns
|
|
38
38
|
- Verifying one specific change, which `claude-ui-test` owns
|
|
39
39
|
- Defining the intent it audits against, which `claude-design-extract` and the wireframes own
|
|
40
|
+
- Measuring what a running interface costs to paint, block, or shift, which `claude-ux-measure` owns. Contrast stays here rather than going with it, being computable from two color values this skill already reads off the token table.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: claude-ux-audit
|
|
3
|
-
description: Audits the current UI for incomplete, inconsistent, or confusing patterns. Reads DESIGN.md and .claude/wireframes/ for intent, scans UI files, and outputs observations grouped by surface. Use when asked "audit the UX", "audit the UI", "UX audit", or "find UI roughness". Do NOT use for new feature planning or code changes
|
|
3
|
+
description: Audits the current UI for incomplete, inconsistent, or confusing patterns. Reads DESIGN.md and .claude/wireframes/ for intent, scans UI files, and outputs observations grouped by surface. Use when asked "audit the UX", "audit the UI", "UX audit", or "find UI roughness". Do NOT use for new feature planning or code changes, and do NOT use to measure what a running interface costs to paint, which is `claude-ux-measure`.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Claude UX audit
|
|
@@ -8,6 +8,7 @@ description: Audits the current UI for incomplete, inconsistent, or confusing pa
|
|
|
8
8
|
## Guards
|
|
9
9
|
|
|
10
10
|
- If no UI files exist in the project (no JSX, TSX, Vue, Svelte, or HTML under `src/`), stop: `❌ No UI surfaces found to audit.`
|
|
11
|
+
- If the request asks what the interface costs to paint, block, or shift at runtime, run nothing and name `claude-ux-measure`. This skill reads source and reaches no number a browser produces. Contrast is the exception and stays here, since it is computable from the two color values already in the token table.
|
|
11
12
|
|
|
12
13
|
## Step 1: read context
|
|
13
14
|
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: claude-ux-measure
|
|
3
|
+
description: Why a rendering cost question is answered with a number against a published threshold, and why the runner is detected rather than prescribed
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Claude UX measure requirement
|
|
7
|
+
|
|
8
|
+
## Gap
|
|
9
|
+
|
|
10
|
+
Without this skill, a session asked what a page costs answers from source. It reads component code, names a suspicious loop, and reports a judgment, so nobody learns the number and the next session repeats the guess. Nothing in the corpus starts a browser, so the question has no surface at all and lands on whichever skill matched the word "UI".
|
|
11
|
+
|
|
12
|
+
A session that does start one picks a runner on the spot. The reading then comes from a browser nobody chose, cannot be compared against the next run, and disappears with the chat. A number reported with no boundary beside it is the same dead end as the judgment it replaced, because a reader holding `1.9s` and no threshold concludes nothing from it.
|
|
13
|
+
|
|
14
|
+
## Must
|
|
15
|
+
|
|
16
|
+
- Detect the harness the project already carries before naming any runner
|
|
17
|
+
- Report a number per metric beside the threshold it is measured against and the published source of that threshold
|
|
18
|
+
- Cover paint, processor, and layout cost, and name what the run left unmeasured
|
|
19
|
+
- Report what the measurement needs and stop when no harness is detected, since that is a project without a runner rather than a failure here
|
|
20
|
+
- Take several readings and report the median, since one load carries startup noise wider than the gap between two thresholds
|
|
21
|
+
- Write the reading to the branch-derived path at the main worktree root, overwriting
|
|
22
|
+
- Reach the URL before measuring against it, starting the project's own command only when nothing already answers there
|
|
23
|
+
- Leave the interface stopped and the probe deleted
|
|
24
|
+
|
|
25
|
+
## Must not
|
|
26
|
+
|
|
27
|
+
- Install a runner, add a dependency, or write a config to make detection succeed
|
|
28
|
+
- Invent a threshold, or move a published one to fit a reading
|
|
29
|
+
- Report an observation about the source in place of a measurement
|
|
30
|
+
- Suggest a fix for what it measured, which is a change with its own review
|
|
31
|
+
- Measure anything past the three metrics, since every adjacent ask doubles the run
|
|
32
|
+
|
|
33
|
+
## Guards
|
|
34
|
+
|
|
35
|
+
- No command that serves an interface and no URL from the user: stop, since there is nothing to reach. Test for a servable interface rather than for source under a named folder, which refuses a project laying its tree out differently while its server sits ready
|
|
36
|
+
- No harness detected: name the runners this can drive and stop
|
|
37
|
+
- No URL derivable from the project's own commands: ask rather than guessing a port
|
|
38
|
+
|
|
39
|
+
## Out of scope
|
|
40
|
+
|
|
41
|
+
- Judging the interface against stated intent, which `claude-ux-audit` owns
|
|
42
|
+
- Contrast, which is computable from two color values in the token table `claude-ux-audit` already reads. A contrast failure from a color computed at runtime stays invisible to that reader, and the cost is accepted rather than overlooked.
|
|
43
|
+
- Network waterfall, bundle size, and accessibility, which no version of this measures
|
|
44
|
+
- Writing behavioral tests against the interface, which `claude-ui-test` owns
|
|
45
|
+
- Fixing what the reading found
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: claude-ux-measure
|
|
3
|
+
description: Measures paint, processor, and layout cost against a running interface and reports numbers against published thresholds. Detects the project's existing browser harness rather than requiring one. Use when asked "how fast is this page", "measure the UI", "what does this cost to render", "check Core Web Vitals", or "profile the interface". Do NOT use to judge UI quality by reading source, which is `claude-ux-audit`.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Claude UX measure
|
|
7
|
+
|
|
8
|
+
Report numbers. A sentence about what the source looks like is what this exists to replace, so every finding is a reading beside the threshold it is measured against.
|
|
9
|
+
|
|
10
|
+
## Guards
|
|
11
|
+
|
|
12
|
+
- If the project names no command that serves an interface and the user names no URL, stop: `❌ Nothing to serve. Name a running URL or a command that starts one.` Test for a servable interface rather than for source under a particular folder, since this measures what a browser receives and never reads the tree that produced it.
|
|
13
|
+
- If the request is about intent, consistency, missing states, or contrast, run nothing and name `claude-ux-audit`. That skill reads source and this one runs the interface.
|
|
14
|
+
|
|
15
|
+
## Step 1: reach the interface
|
|
16
|
+
|
|
17
|
+
Read these in parallel from the project root, skipping any that do not exist:
|
|
18
|
+
|
|
19
|
+
- `package.json`: the `scripts` block naming a dev, preview, or start command
|
|
20
|
+
- `.claude/context/development/`: the documented run commands and the port each serves
|
|
21
|
+
- `CLAUDE.md`: project type and conventions
|
|
22
|
+
|
|
23
|
+
Prefer a production-shaped build (`preview`, `start`) over the dev server. A dev server ships unminified modules and reports a cost no user pays.
|
|
24
|
+
|
|
25
|
+
Ask for the URL when nothing names one. Do not guess a port.
|
|
26
|
+
|
|
27
|
+
Then reach it. Request the URL first and measure whatever already answers, since a server the user started is the one they mean. Build and start the chosen command only when nothing answers, wait for it to accept a request before going on, and stop it once Step 4 has the readings. A run that measures before the server is listening reports a connection failure as a cost.
|
|
28
|
+
|
|
29
|
+
## Step 2: detect the harness
|
|
30
|
+
|
|
31
|
+
Test each in order and take the first that resolves. Name the one found before running it.
|
|
32
|
+
|
|
33
|
+
1. A connected browser MCP server exposing navigation and a performance trace
|
|
34
|
+
2. `@playwright/test` or `playwright` in `package.json`, or a `playwright.config.*` at the project root
|
|
35
|
+
3. `lighthouse` in `package.json`, or a script whose command names it
|
|
36
|
+
|
|
37
|
+
Never install a runner, add a dependency, or write a config to make one of these resolve. The project chooses its harness and this skill reads that choice.
|
|
38
|
+
|
|
39
|
+
When none resolves, report what the measurement needs and stop:
|
|
40
|
+
|
|
41
|
+
```plaintext
|
|
42
|
+
❌ No browser harness detected. This measurement drives one of:
|
|
43
|
+
- a browser MCP server with a performance trace
|
|
44
|
+
- Playwright (@playwright/test)
|
|
45
|
+
- Lighthouse (lighthouse)
|
|
46
|
+
Install one, or name a running URL and which runner to drive it with.
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Step 3: measure
|
|
50
|
+
|
|
51
|
+
Take three readings against the same URL on a cold profile and report the median. One load carries startup noise wider than the gap between two thresholds, so a single number cannot be placed against them.
|
|
52
|
+
|
|
53
|
+
Measure these three and nothing else:
|
|
54
|
+
|
|
55
|
+
- **Paint**: Largest Contentful Paint, the moment the largest element in the viewport finishes rendering
|
|
56
|
+
- **Processor**: Total Blocking Time, main-thread time past 50ms per long task between first paint and interactive
|
|
57
|
+
- **Layout**: Cumulative Layout Shift, the summed score of unexpected shifts over the page lifetime
|
|
58
|
+
|
|
59
|
+
Through Playwright or an MCP server, read them from a `PerformanceObserver` registered before navigation against the `largest-contentful-paint`, `longtask`, and `layout-shift` entry types. Lighthouse reports all three under its JSON audits, so parse rather than re-derive them.
|
|
60
|
+
|
|
61
|
+
Write any probe the harness needs into the project's own test folder, run it, then delete it. Leave no file behind.
|
|
62
|
+
|
|
63
|
+
### Thresholds
|
|
64
|
+
|
|
65
|
+
| Metric | Good | Needs work | Poor |
|
|
66
|
+
| ------ | --------- | ---------- | --------- |
|
|
67
|
+
| LCP | `≤ 2.5s` | `≤ 4.0s` | `> 4.0s` |
|
|
68
|
+
| TBT | `≤ 200ms` | `≤ 600ms` | `> 600ms` |
|
|
69
|
+
| CLS | `≤ 0.1` | `≤ 0.25` | `> 0.25` |
|
|
70
|
+
|
|
71
|
+
These are Google's published Core Web Vitals boundaries, with the Lighthouse lab boundary for Total Blocking Time. Cite that source in the report. Never move a number to fit a reading, and never invent a fourth metric's boundary.
|
|
72
|
+
|
|
73
|
+
## Step 4: report and persist
|
|
74
|
+
|
|
75
|
+
### Report format
|
|
76
|
+
|
|
77
|
+
```markdown
|
|
78
|
+
3 metrics measured against <url> over 3 runs. Harness: <name>.
|
|
79
|
+
|
|
80
|
+
| Metric | Median | Threshold | Verdict |
|
|
81
|
+
| ------ | ------ | --------- | ---------------------------- |
|
|
82
|
+
| LCP | <n> | `≤ 2.5s` | <good \| needs work \| poor> |
|
|
83
|
+
| TBT | <n> | `≤ 200ms` | <verdict> |
|
|
84
|
+
| CLS | <n> | `≤ 0.1` | <verdict> |
|
|
85
|
+
|
|
86
|
+
Thresholds: Google Core Web Vitals, Lighthouse lab boundary for TBT.
|
|
87
|
+
|
|
88
|
+
Not measured: network waterfall, bundle size, accessibility, contrast.
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Report the reading and stop there. A remedy for a poor verdict is a change with its own review, so name no fix and edit no source.
|
|
92
|
+
|
|
93
|
+
### Persist
|
|
94
|
+
|
|
95
|
+
Derive `<slug>` per `.claude/standards/slug.md`, or `${CLAUDE_SKILL_DIR}/../../standards/slug.md` when the project does not have it. Fall back to `latest` on an empty result.
|
|
96
|
+
|
|
97
|
+
Write the full reading directly to `.claude/review/ux-measure-<slug>.md` at the main worktree root, not the current worktree. See Worktrees in `CLAUDE.md`. Create the directory if it does not exist. Always overwrite.
|
|
98
|
+
|
|
99
|
+
From a linked worktree the file-editing tools refuse that path, so the reading goes out through `Bash`. Send the `mkdir -p` and the heredoc as two plain commands rather than joining them with `&&`, which is refused as compound.
|
|
100
|
+
|
|
101
|
+
The `.claude/review/` directory is gitignored. Do not stage or commit the file.
|
|
102
|
+
|
|
103
|
+
### Chat output
|
|
104
|
+
|
|
105
|
+
Output the summary line and the file path. Do not repeat the table in chat.
|
|
106
|
+
|
|
107
|
+
```plaintext
|
|
108
|
+
3 metrics measured against <url>. <n> past threshold.
|
|
109
|
+
📝 Wrote .claude/review/ux-measure-<slug>.md
|
|
110
|
+
```
|
|
@@ -11,12 +11,15 @@ Without this skill, a session picks a stack name from memory and installs rules
|
|
|
11
11
|
|
|
12
12
|
The two costly failures are quieter. An unmatched technology gets the nearest rule instead of a stop, so the project carries governance authored for something adjacent and nothing marks it as a guess. A rule written directly into the target lands where no sync will ever reach it, so it drifts from the toolkit copy that owns it.
|
|
13
13
|
|
|
14
|
+
The last failure sits at a boundary rather than inside a run. `setup-init` turns away a project whose language it carries no stack for and sends it here, and a body stating only what this skill does not cover leaves that route running one way, so a person arriving on it reads a skill that never says it is the destination.
|
|
15
|
+
|
|
14
16
|
## Must
|
|
15
17
|
|
|
16
18
|
- Read the catalog at run time and resolve both the stack and every extra against it
|
|
17
19
|
- Name the evidence file behind each detected technology in the preview, so a wrong match is visible before the install rather than after
|
|
18
20
|
- Dedupe extras against the rules the picked stack already resolves
|
|
19
21
|
- Run the install non-interactively, since the CLI picker blocks where no terminal is attached
|
|
22
|
+
- State the inbound route from `setup-init`, so a project sent here for the language-neutral rule layer can tell it landed at the destination the chain named
|
|
20
23
|
|
|
21
24
|
## Must not
|
|
22
25
|
|
|
@@ -7,6 +7,11 @@ description: Detects a project's stack from its files and installs matching tool
|
|
|
7
7
|
|
|
8
8
|
Automates `aitk gov install` by inferring the stack and extras from the current project, then shelling out to the CLI with the resolved arguments.
|
|
9
9
|
|
|
10
|
+
## Scope
|
|
11
|
+
|
|
12
|
+
- Governance rules and nothing else. `setup-init` is the one-shot chain that installs governance beside tooling, standards, snippets, and the seeds.
|
|
13
|
+
- This is where `setup-init` sends a project whose language the toolkit carries no stack for. Rules install without the `base` development dependencies the tooling layer would drop on it. Resolve and stop exactly as below, since a language with no stack is the unmatched case `## Gap handling` already owns and arriving by that route changes none of it.
|
|
14
|
+
|
|
10
15
|
## Read the catalog
|
|
11
16
|
|
|
12
17
|
Run this first to load the current stacks and rules. Never hardcode names. The catalog is the source of truth.
|
|
@@ -11,6 +11,8 @@ Without this skill, a session bootstrapping the index system writes `index.md` i
|
|
|
11
11
|
|
|
12
12
|
The scan is where the other failures start. A walk that does not prune reaches `node_modules`, gitignored output, and the snippets folder, which is invoked by reference rather than browsed and needs neither an index nor per-file frontmatter. A folder that already carries an `index.md` gets a second one written over the first.
|
|
13
13
|
|
|
14
|
+
One failure predates the scan. `setup-init` declines an install that wants the Claude layer without the tooling chain and names this skill as where the index system gets bootstrapped, and a body that never states the inbound route leaves the person arriving on it unable to tell whether they landed at the destination or somewhere adjacent to it.
|
|
15
|
+
|
|
14
16
|
Drafted frontmatter is a proposal, and a session that writes it before the user sees it turns a review into a cleanup. The convention block has the mirror failure. A session that paraphrases it into `CLAUDE.md` produces a copy that reads correctly and no longer matches its source, so the two drift with nothing reporting it. And a project with no `CLAUDE.md` gets one scaffolded to hold the block, which installs a file the project declined.
|
|
15
17
|
|
|
16
18
|
## Must
|
|
@@ -21,6 +23,7 @@ Drafted frontmatter is a proposal, and a session that writes it before the user
|
|
|
21
23
|
- Validate with a dry run and stop on the first reported error, before writing for real
|
|
22
24
|
- Paste the convention block verbatim from its single source
|
|
23
25
|
- Emit the closeout exactly once, whatever the seed step concluded
|
|
26
|
+
- State the inbound route from `setup-init`, so an install wanting the Claude layer without the tooling chain can tell it landed at the destination the chain named
|
|
24
27
|
|
|
25
28
|
## Must not
|
|
26
29
|
|
|
@@ -13,6 +13,7 @@ Read `.claude/context/indexes.md` from the toolkit if context on the system is n
|
|
|
13
13
|
|
|
14
14
|
- Bootstrap un-indexed folders only. Skip folders that already contain an `index.md`.
|
|
15
15
|
- All-or-nothing per chosen folder. Every `*.md` sibling in a chosen folder gets `title` and `description` injected, or none does. Partial migration creates folders that hard-error on regen.
|
|
16
|
+
- This is where `setup-init` sends an install that wants the Claude layer without the tooling chain, once `aitk claude init` has seeded the docs. The work is the same either way, since the scan below prunes `.claude` and reads the project's own documentation folders whichever route reached it.
|
|
16
17
|
|
|
17
18
|
## Scan
|
|
18
19
|
|
|
@@ -11,12 +11,18 @@ Without this skill, a session installs a stack whose name it recalled rather tha
|
|
|
11
11
|
|
|
12
12
|
Two failures are the expensive ones because both return success. A monorepo run drops husky into every subtree, git honors one `core.hooksPath`, and the extra hook directories go quiet with nothing reporting it. A caller reads the chain as onboarding complete when index bootstrap and plugin provisioning never ran, since no step states whether they belong to the chain.
|
|
13
13
|
|
|
14
|
+
Two more start at the resolve step. A project whose language the toolkit carries no stack for resolves to `base`, and the preview reports the resolved name without marking it as a fallback, so `base` development dependencies, scripts, and hooks land on a project that will not use them and nobody had the moment to decline.
|
|
15
|
+
|
|
16
|
+
The other is the refusal that ends rather than routes. An existing project, an install wanting the Claude layer without the tooling chain, and a language with no stack are the three states the chain does not serve, and each is declined by a declaration naming no destination, so the person who arrived with one reads a stop and goes no further.
|
|
17
|
+
|
|
14
18
|
## Must
|
|
15
19
|
|
|
16
20
|
- Resolve every domain argument against a catalog read at run time, so a stack, rule, snippet, or tooling name the catalog does not carry cannot reach the command line
|
|
17
21
|
- Preview the resolved chain before the first command runs, naming the evidence file behind each detected technology
|
|
18
22
|
- Pass `--skip base` on every subtree past the first in a monorepo, so the shared hook layer lands once
|
|
19
23
|
- State which onboarding steps the chain does not run, so a caller knows what is left rather than inferring completeness from a clean report
|
|
24
|
+
- Mark a stack reached by fallback as a fallback in the preview, and name what `base` carries, so the write can be declined at the one point it is still declinable
|
|
25
|
+
- Name a destination for every state the chain does not serve, in this body rather than only in a reference doc the person would have to already know to open, and say for each whether the chain stops or runs on a default
|
|
20
26
|
|
|
21
27
|
## Must not
|
|
22
28
|
|
|
@@ -35,3 +41,5 @@ Two failures are the expensive ones because both return success. A monorepo run
|
|
|
35
41
|
- Provisioning Claude Code plugins: `setup-plugins`. Those install once per machine rather than into a project, so no project-scoped chain can carry them.
|
|
36
42
|
- Running the verification scripts, which `setup-verify` owns and this chain invokes as its last step
|
|
37
43
|
- Re-installing into a project that already has the files, which the per-domain `aitk <domain> install` and `aitk sync` commands own
|
|
44
|
+
- Deciding which of those per-domain commands an existing project needs: `toolkit-operator`. That skill reads the target before naming one, which this chain never does, so it is the destination rather than a guess made here.
|
|
45
|
+
- Seeding the `.claude/` folder without the tooling chain, which `aitk claude init` does on its own and `setup-indexes` finishes
|
|
@@ -12,6 +12,16 @@ Orchestrates the onboarding chain. Detects project type, resolves per-domain arg
|
|
|
12
12
|
- This skill and `aitk init` run once on a fresh scaffold, never on an existing project. They do not guard against clobbering existing configs. When tempted to add guards, mode switches, or an existing-project branch, stop. Extend the per-domain `aitk <domain> install` or `aitk sync` paths instead.
|
|
13
13
|
- The chain does not bootstrap the `index.md` system and does not provision Claude Code plugins. `setup-indexes` owns the first. `setup-plugins` owns the second, which installs once per machine rather than into a project, so no project-scoped chain can carry it. Name both in the report so a clean result does not read as onboarding complete.
|
|
14
14
|
|
|
15
|
+
## Declined states
|
|
16
|
+
|
|
17
|
+
Three states reach this skill that the chain does not serve. Name the destination for each, so the refusal routes rather than ends. The first two stop the chain outright and the third runs it on a default the person may not want.
|
|
18
|
+
|
|
19
|
+
- **An existing project.** Stop and hand off to `toolkit-operator`. It reads what the target already carries before it names a command, which this chain never does, so any per-domain install picked here is a guess against configs nobody read. The Scope bullet above names the same commands as the authoring alternative, and this is the destination a person takes.
|
|
20
|
+
- **An install wanting the `.claude/` folder alone.** Stop. Run `aitk claude init` for the seed docs, then invoke `setup-indexes` to bootstrap the `index.md` system over the project's own documentation folders. Neither needs the tooling sync this chain runs.
|
|
21
|
+
- **A language the toolkit carries no stack for.** The chain still runs, on `base`, with the fallback marked in the preview. A project that wants none of what `base` carries declines there and takes `setup-gov` for the governance layer, which is language-neutral. Say so at the preview rather than resolving it here, since the fallback is a working default and only the person can say whether it fits.
|
|
22
|
+
|
|
23
|
+
Do not add a stack, a mode switch, or an existing-project branch to satisfy one of these. Each destination already exists and routing to it costs a line.
|
|
24
|
+
|
|
15
25
|
## Read catalogs
|
|
16
26
|
|
|
17
27
|
Run in parallel. Never hardcode stack, rule, snippet, or standards names.
|
|
@@ -36,8 +46,8 @@ Read these from the project root in parallel, skipping any that do not exist:
|
|
|
36
46
|
|
|
37
47
|
## Resolve arguments
|
|
38
48
|
|
|
39
|
-
- **Stack:** pick the closest governance stack by matching detected runtime or framework against stack names in the catalog. If nothing matches, fall back to `base
|
|
40
|
-
- **Tooling stack:** pick the closest tooling stack from `aitk tooling list --json` (e.g. `vite-react`, `astro`). Distinct from the governance stack. Fall back to `base` if no framework match.
|
|
49
|
+
- **Stack:** pick the closest governance stack by matching detected runtime or framework against stack names in the catalog. If nothing matches, fall back to `base` and carry the fallback into the preview.
|
|
50
|
+
- **Tooling stack:** pick the closest tooling stack from `aitk tooling list --json` (e.g. `vite-react`, `astro`). Distinct from the governance stack. Fall back to `base` if no framework match, and carry that fallback into the preview too.
|
|
41
51
|
- **Extras:** identify technologies not already covered by the picked stack. For each, find a rule whose `description` or `paths` points at that technology and pass it via `--add`. Do not add a rule the stack already pulls in.
|
|
42
52
|
- **Snippets:** default to `all`. Narrow only if the user asked for a specific category.
|
|
43
53
|
- **Skip (`--skip`):** `standards` and `wiki` are core and install by default. Add `--skip standards` or `--skip wiki` only when the user explicitly wants them left out.
|
|
@@ -56,14 +66,16 @@ Rules, snippets, and stacks are authored in the toolkit repo, never in the targe
|
|
|
56
66
|
Before executing, output:
|
|
57
67
|
|
|
58
68
|
- **Detected:** each technology with its evidence file
|
|
59
|
-
- **Stack:** picked governance stack + resolved rule count
|
|
60
|
-
- **Tooling stack:** picked tooling stack
|
|
69
|
+
- **Stack:** picked governance stack + resolved rule count. Mark it `fallback` when no detected runtime or framework matched a catalog name.
|
|
70
|
+
- **Tooling stack:** picked tooling stack. Mark it `fallback` on the same test, and name what `base` lands: configs, seeds, and gitignore entries in every case, plus the JavaScript development dependencies, scripts, and hook activation wherever a `package.json` exists to carry them. A project outside that ecosystem runs none of the second group and keeps the first.
|
|
61
71
|
- **Extras:** each `--add` rule with a one-line reason
|
|
62
72
|
- **Snippets:** resolved category
|
|
63
73
|
- **Skip:** any `--skip` entries with reason
|
|
64
74
|
- **Target:** resolved target path
|
|
65
75
|
- **Commands:** the full chain that will run
|
|
66
76
|
|
|
77
|
+
A resolved name and a fallback read alike once written, so mark the fallback here rather than in the report. The preview is the last point before the first write, and the report runs after the files have landed.
|
|
78
|
+
|
|
67
79
|
## Execute
|
|
68
80
|
|
|
69
81
|
Run the chain in order, starting immediately after the preview. Each step's permission dialog is the confirmation gate. Do not pause for additional confirmation. Run from the target project's current directory.
|
|
@@ -31,6 +31,7 @@ The last failure is a section no route reaches. `## Route` maps an intent or a d
|
|
|
31
31
|
- Route a measurement intent to the audit offers from the route table itself, rather than leaving the section reachable only by reading past the table
|
|
32
32
|
- Rank a lifecycle row against the audit offers where the route table itself states it, so a session acting on the lifecycle row reads the ranking without opening the section being ranked
|
|
33
33
|
- Offer every audit whose surface the target carries, and offer none whose surface it lacks
|
|
34
|
+
- Hand off an audit that is a skill rather than a command, since the execute contract governs a CLI run and says nothing about a skill's own guards
|
|
34
35
|
- Re-check the target after any operation that wrote, and report what those writes changed against the state read before acting
|
|
35
36
|
|
|
36
37
|
## Must not
|
|
@@ -69,7 +69,7 @@ On a measured report, take `<stack>` from the first name in `tooling.chain`, whi
|
|
|
69
69
|
|
|
70
70
|
### Audits
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
Five audits measure a surface without changing it. Offer the ones whose surface the target carries, list them together, and let the user pick. Run none of them unasked, and never treat a finding as a reason to abandon the operation the user asked for, since each reports judgments beside facts.
|
|
73
73
|
|
|
74
74
|
A lifecycle row and these offers fire together on a project carrying a context folder and no installed domain, which is the ranking the preamble states. Scaffold work on a target with nothing installed is real work, so an audit offered instead of it answers a question nobody asked, while an audit dropped in favor of it loses the one the user did.
|
|
75
75
|
|
|
@@ -77,10 +77,13 @@ A lifecycle row and these offers fire together on a project carrying a context f
|
|
|
77
77
|
- A record folder present under `.claude/`, one of `plans`, `groundwork`, `intake`, or `memory`: offer `aitk records validate <kind>` for each one found
|
|
78
78
|
- Markdown that git lists: offer `aitk markdown audit`
|
|
79
79
|
- TypeScript or shell source present: offer `aitk comments scan`
|
|
80
|
+
- A `package.json` script that serves an interface, one of `dev`, `preview`, `serve`, or `start`: offer `claude-ux-measure`
|
|
80
81
|
|
|
81
82
|
An audit offered against a surface the target lacks reports an empty run as a finding, which is the same defect as never offering it at all. Check the surface before naming the command.
|
|
82
83
|
|
|
83
|
-
The markdown row is the one every target satisfies, since a project with no markdown is not one this reaches. Its condition is stated so the
|
|
84
|
+
The markdown row is the one every target satisfies, since a project with no markdown is not one this reaches. Its condition is stated so the five rows read alike, and the row needs no gate beyond it.
|
|
85
|
+
|
|
86
|
+
The last row is a skill handoff rather than a command, so `## Execute` does not govern it. Hand it off and stop, the way a lifecycle row hands off. Its condition reads a serving command rather than a source folder, since that skill measures what a browser receives and never opens the tree behind it. It carries a second condition this skill cannot read, which is a browser harness the project already installed, and the skill itself reports what it needs when it finds none. Testing for that here would put the detection in two places and let this one answer stale.
|
|
84
87
|
|
|
85
88
|
## Execute
|
|
86
89
|
|
package/docs/agents/commands.md
CHANGED
|
@@ -40,27 +40,28 @@ Full help: `aitk <command> --help`. Behavior notes for the install and sync verb
|
|
|
40
40
|
| `aitk context audit` | Report required sections, length, cited paths, reference form, catalog tables, provenance, superseded-decision narration, and index drift |
|
|
41
41
|
| `aitk markdown audit` | Fail any markdown path on a banned character, word, or spelling, and report the structural checkpoints |
|
|
42
42
|
| `aitk claude skills audit` | Report both skill corpora against the mechanical rules in `standards/skill.md` |
|
|
43
|
+
| `aitk claude skills drift` | Name the shipped skill bodies rewritten between a given ref and `HEAD` (`--json`) |
|
|
43
44
|
| `aitk capture [source]` | Render HTML capture sources to PNG, toolkit-only and absent from an installed package |
|
|
44
45
|
|
|
45
46
|
## Domain commands
|
|
46
47
|
|
|
47
48
|
Each domain exposes a consistent shape where applicable: `list`, `install`, `sync`, `create`.
|
|
48
49
|
|
|
49
|
-
| Domain | Subcommands
|
|
50
|
-
| ----------- |
|
|
51
|
-
| `tooling` | `list`, `sync`, `ref`, `create`, `verify`, `inject`, `prune-gitignore`
|
|
52
|
-
| `snippets` | `list`, `install`, `sync`, `create`
|
|
53
|
-
| `standards` | `list`, `install`, `sync`, `<name>`
|
|
54
|
-
| `gov` | `list`, `install`, `sync`, `build`
|
|
55
|
-
| `claude` | `init`, `sync`, `seeds list`, `skills list`, `skills audit`, `setup [dest]` |
|
|
56
|
-
| `wiki` | `init`
|
|
57
|
-
| `design` | `render`
|
|
58
|
-
| `slides` | `render`, `list`
|
|
59
|
-
| `tasks` | `archive`, `validate`
|
|
60
|
-
| `intake` | `list`, `answer`
|
|
61
|
-
| `comments` | `scan`
|
|
62
|
-
| `context` | `audit`
|
|
63
|
-
| `markdown` | `audit`
|
|
50
|
+
| Domain | Subcommands |
|
|
51
|
+
| ----------- | ------------------------------------------------------------------------------------------- |
|
|
52
|
+
| `tooling` | `list`, `sync`, `ref`, `create`, `verify`, `inject`, `prune-gitignore` |
|
|
53
|
+
| `snippets` | `list`, `install`, `sync`, `create` |
|
|
54
|
+
| `standards` | `list`, `install`, `sync`, `<name>` |
|
|
55
|
+
| `gov` | `list`, `install`, `sync`, `build` |
|
|
56
|
+
| `claude` | `init`, `sync`, `seeds list`, `skills list`, `skills audit`, `skills drift`, `setup [dest]` |
|
|
57
|
+
| `wiki` | `init` |
|
|
58
|
+
| `design` | `render` |
|
|
59
|
+
| `slides` | `render`, `list` |
|
|
60
|
+
| `tasks` | `archive`, `validate` |
|
|
61
|
+
| `intake` | `list`, `answer` |
|
|
62
|
+
| `comments` | `scan` |
|
|
63
|
+
| `context` | `audit` |
|
|
64
|
+
| `markdown` | `audit` |
|
|
64
65
|
|
|
65
66
|
Common patterns:
|
|
66
67
|
|
package/docs/agents/index.md
CHANGED
|
@@ -24,5 +24,5 @@ CLI catalog and invocation rules for agents, split by command domain. Start with
|
|
|
24
24
|
- [Sandbox](sandbox.md): Scenario routing, the expectation scoring surface, and the coverage census over scenarios and skills
|
|
25
25
|
- [Scripting](scripting.md): The runtime catalogs that replace hardcoded names, what each carries, and a headless invocation per domain
|
|
26
26
|
- [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
|
|
27
|
-
- [Skill audit](skills-audit.md): Measuring both skill corpora against standards/skill.md, the checks it reads,
|
|
27
|
+
- [Skill audit](skills-audit.md): Measuring both skill corpora against standards/skill.md, the checks it reads, the requirement gate that is the only failing one, and the drift verb that names bodies rewritten since a ref
|
|
28
28
|
- [Tasks](tasks.md): Selecting a shipped task by stem or pull request, recording a number and closing an outcome, the refusal reasons, the board checks validate runs, and why the board root defaults to the main worktree
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Skill audit
|
|
3
|
-
description: Measuring both skill corpora against standards/skill.md, the checks it reads,
|
|
3
|
+
description: Measuring both skill corpora against standards/skill.md, the checks it reads, the requirement gate that is the only failing one, and the drift verb that names bodies rewritten since a ref
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Skill audit
|
|
@@ -53,3 +53,36 @@ Exit codes are `0` for a clean run, `1` for a refusal, and `2` for a skill folde
|
|
|
53
53
|
`--requirements-only` is the half wired into `bun run check`. Presence of a required file is a fact with no false positives, and the rule had nothing reading it, so a skill shipped without the sibling passed every stage while the standard required it.
|
|
54
54
|
|
|
55
55
|
The check is preventive rather than diagnostic. Every mechanical rule passed across both corpora the day it shipped, so what it buys is the regression it stops rather than a backlog it surfaces.
|
|
56
|
+
|
|
57
|
+
## Drift since a ref
|
|
58
|
+
|
|
59
|
+
`aitk claude skills drift <ref>` names the shipped skill bodies rewritten between that commit and the current `HEAD`.
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
aitk claude skills drift HEAD~20
|
|
63
|
+
aitk claude skills drift 02d7b265 --json
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
| Option | Behavior |
|
|
67
|
+
| -------- | ---------------------------------------------------------- |
|
|
68
|
+
| `--json` | Add a machine-readable record on stdout, keeping the frame |
|
|
69
|
+
|
|
70
|
+
A skill body enters a session once and stays. Re-invoking the skill replays the copy the session already holds rather than re-reading the file, so a session that outlives a merge touching a body it loaded keeps applying what it loaded and nothing compares the two. A compaction carries the held copy forward with the summary it writes, which makes the exposure the age of the oldest load rather than the age of the session.
|
|
71
|
+
|
|
72
|
+
The ref is required and carries no default. `HEAD` is the only value the command could supply for itself, and it answers every run with nothing moved, which is the silence this verb exists to break. A session passes the commit it started from.
|
|
73
|
+
|
|
74
|
+
Nothing on the machine records that commit, so a session recovers it from its own elapsed run time with `git log -1 --format=%H --before='<duration> ago'` and rounds the duration up. Over-reporting costs one read of a body while under-reporting costs the answer, so the generous end of the estimate is the correct one and no session needs to pin the exact commit.
|
|
75
|
+
|
|
76
|
+
### What drift reads
|
|
77
|
+
|
|
78
|
+
Bodies alone, one `SKILL.md` per folder, across `claude/skills/` and not the internal tree. A reference or a requirement beside the body is read by whoever opens it and holds no copy that outlives the read, and the internal corpus never loads into a target session at all.
|
|
79
|
+
|
|
80
|
+
The report names the newest commit that rewrote each body rather than the first, so a body rewritten twice in the range points at the version on disk now.
|
|
81
|
+
|
|
82
|
+
### What drift cannot answer
|
|
83
|
+
|
|
84
|
+
The report says a file moved, not that a session holds a stale copy. A session cannot read its own loaded body as bytes, so the comparison runs against history and a ref older than the oldest load over-reports. That is the safe direction, since confirming a name costs one read of the body while the failure being answered is silence. Every run states the bound, including the run that names nothing.
|
|
85
|
+
|
|
86
|
+
Two cases fall outside the range. An uncommitted edit in the working tree is not history yet, so a body changed and left unstaged reports as unmoved. A target project loads the plugin from a marketplace cache with no repository behind it, where the verb refuses and names the absent history rather than reporting a clean tree.
|
|
87
|
+
|
|
88
|
+
Exit codes are `0` when history was read, whether or not a body moved, and `1` when the question could not be answered. An absent shipped corpus, an absent repository, and a ref matching no commit each refuse with their own reason on stderr. A command exiting zero on a question it never answered is the failure `.claude/ARCHITECTURE.md` already records against two skills, which is why no path here reports an empty result in place of a refusal.
|
package/docs/ai-workflow.md
CHANGED
|
@@ -181,6 +181,7 @@ Before the first feature session on a UI-heavy project, pick a design tier. The
|
|
|
181
181
|
| `aitk:claude-docs` | When decisions diverged from plan, update `.claude/` docs |
|
|
182
182
|
| `aitk:claude-ui-test` | After UI changes, generate and run e2e tests + visual checklist |
|
|
183
183
|
| `aitk:claude-ux-audit` | Audit existing UI surfaces for missing states, edge cases, inconsistencies |
|
|
184
|
+
| `aitk:claude-ux-measure` | Measure paint, processor, and layout cost against a running interface, on the harness the project already carries |
|
|
184
185
|
| `aitk:claude-autoship` | After plan approval, chain implement → verify → review → draft PR |
|
|
185
186
|
| `aitk:systematic-debugging` | When a test fails or bug surfaces, enforce root-cause investigation before fixes |
|
|
186
187
|
| `aitk:git-ship` | Post-feature: sync docs, commit, rename branch, open PR |
|
package/docs/target-projects.md
CHANGED
|
@@ -49,6 +49,8 @@ The chain is:
|
|
|
49
49
|
- The agent follows the reference to generate eslint, vitest, playwright configs and the stack's setup script, and extends `.claude/context/ci.md` and `.claude/context/development.md` per the reference's extend sections <!-- audit-ignore-citations -->
|
|
50
50
|
- `setup-verify` runs the installed `package.json` scripts (lint, typecheck, check, test, build) and reports pass or fail
|
|
51
51
|
|
|
52
|
+
The chain serves a fresh scaffold and names a destination for the three states it does not. An existing project goes to `aitk:toolkit-operator`, which reads what the project already carries before naming a per-domain command. An install wanting the Claude layer without the tooling chain runs `aitk claude init` for the seed docs and then `aitk:setup-indexes` for the index system. A language the toolkit ships no stack for is the one of the three the chain still runs for, on `base`, with the fallback marked in the preview so it can be declined there.
|
|
53
|
+
|
|
52
54
|
Keep the `## Scripts` table in `.claude/context/development.md` current as scripts are added. Base tooling seeds that entry with the commands it installs, and each stack reference extends the table. `project-commands` reads it to start the app or run a check on request, so a command missing from the table cannot be run that way. A project whose entry outgrew one file and split into `.claude/context/development/` keeps the table in `overview.md`, which is where the skill looks next. <!-- audit-ignore-citations -->
|
|
53
55
|
|
|
54
56
|
### From scaffold to first feature
|
|
@@ -76,6 +78,8 @@ Escalate only for real web apps. The `setup-init` skill reads `package.json` and
|
|
|
76
78
|
|
|
77
79
|
Markdown-heavy projects, CLI tools, docs sites, research notebooks, and scripting repos stay on `base`. Escalation is a ceiling move, not a default.
|
|
78
80
|
|
|
81
|
+
A project the toolkit ships no stack for lands on `base` the same way, and the skill marks that resolution as a fallback in its preview rather than reporting it as a match. Configs, seeds, and gitignore entries land either way, and the JavaScript development dependencies, scripts, and hook activation land only where a `package.json` exists to carry them. A project outside that ecosystem declines at the preview and takes `aitk:setup-gov` for the governance layer alone, which is language-neutral.
|
|
82
|
+
|
|
79
83
|
Run `aitk tooling list --json` and `aitk gov list --json` to see the current catalogs. Never hardcode stack names.
|
|
80
84
|
|
|
81
85
|
### Core domains and skips
|
|
@@ -174,7 +178,7 @@ cd <your-project>
|
|
|
174
178
|
claude
|
|
175
179
|
```
|
|
176
180
|
|
|
177
|
-
In the session, invoke `aitk:setup-init`. The skill detects no framework, resolves tooling to `base`, governance to `base`, snippets to `all`, and auto-enables `standards` if `docs/` exists.
|
|
181
|
+
In the session, invoke `aitk:setup-init`. The skill detects no framework, resolves tooling to `base`, governance to `base`, snippets to `all`, and auto-enables `standards` if `docs/` exists. The preview marks both stacks as fallbacks, since neither came from a match, then the chain runs `aitk init`.
|
|
178
182
|
|
|
179
183
|
Ongoing: run `aitk sync --check .` to see what has drifted, then invoke `aitk:claude-seed-sync` for seed drift or `aitk sync .` for a catch-all refresh.
|
|
180
184
|
|
|
@@ -34,6 +34,7 @@ The `aitk:claude-design-extract` skill drafts the file, sourcing tokens from a p
|
|
|
34
34
|
- `aitk:claude-design-extract` to draft `.claude/DESIGN.md`, from existing project signals or from requirements alone on day one
|
|
35
35
|
- `aitk:claude-ui-test` for e2e test generation after UI changes
|
|
36
36
|
- `aitk:claude-ux-audit` for UX gap detection on existing surfaces
|
|
37
|
+
- `aitk:claude-ux-measure` for what a running surface costs to paint, read against published thresholds
|
|
37
38
|
- Anthropic's `frontend-design` plugin optional for light visual steering
|
|
38
39
|
|
|
39
40
|
### When to pick
|
package/package.json
CHANGED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { execaSync } from 'execa'
|
|
4
|
+
import { gitEnv } from '@/git-env'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The shipped corpus alone. A session loads a body from the plugin root, which
|
|
8
|
+
* resolves to `claude/skills/`, so the internal tree carries no held copy that
|
|
9
|
+
* could go stale in one.
|
|
10
|
+
*
|
|
11
|
+
* Held with posix separators because it is passed to git as a pathspec. Git
|
|
12
|
+
* emits and accepts posix separators on every platform, so a `join()` form would
|
|
13
|
+
* reach a Windows checkout as `claude\skills`, match no path, and return an
|
|
14
|
+
* empty log that reads as a tree where nothing moved.
|
|
15
|
+
*/
|
|
16
|
+
const SHIPPED_SKILLS = 'claude/skills'
|
|
17
|
+
|
|
18
|
+
/** A `%H` line, which is 40 hex characters under sha1 and 64 under sha256. */
|
|
19
|
+
const COMMIT = /^[0-9a-f]{40,64}$/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A skill body, which is the only file in the folder a session loads. A
|
|
23
|
+
* reference or a requirement beside it is read by whoever opens it, so it holds
|
|
24
|
+
* no copy that outlives the read.
|
|
25
|
+
*/
|
|
26
|
+
const BODY = /^claude\/skills\/([^/]+)\/SKILL\.md$/
|
|
27
|
+
|
|
28
|
+
export interface MovedBody {
|
|
29
|
+
readonly name: string
|
|
30
|
+
/** The newest commit that rewrote this body, not the first. */
|
|
31
|
+
readonly commit: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type DriftReport =
|
|
35
|
+
| {
|
|
36
|
+
readonly kind: 'measured'
|
|
37
|
+
readonly base: string
|
|
38
|
+
readonly head: string
|
|
39
|
+
readonly moved: readonly MovedBody[]
|
|
40
|
+
}
|
|
41
|
+
| { readonly kind: 'unreadable'; readonly reason: string }
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Reads `--name-only` log output into one entry per skill whose body moved.
|
|
45
|
+
*
|
|
46
|
+
* Reverse-chronological input means the first commit naming a body is the
|
|
47
|
+
* newest that rewrote it, so an existing name is never overwritten. A body
|
|
48
|
+
* rewritten twice therefore reports the recent commit, which is the one a
|
|
49
|
+
* session can read to see what it is missing.
|
|
50
|
+
*/
|
|
51
|
+
export function parseMovedBodies(output: string): MovedBody[] {
|
|
52
|
+
const moved = new Map<string, string>()
|
|
53
|
+
let commit = ''
|
|
54
|
+
|
|
55
|
+
for (const line of output.split('\n')) {
|
|
56
|
+
const trimmed = line.trim()
|
|
57
|
+
if (trimmed === '') continue
|
|
58
|
+
|
|
59
|
+
if (COMMIT.test(trimmed)) {
|
|
60
|
+
commit = trimmed
|
|
61
|
+
continue
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const match = BODY.exec(trimmed)
|
|
65
|
+
if (match === null || commit === '') continue
|
|
66
|
+
if (!moved.has(match[1])) moved.set(match[1], commit)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return [...moved]
|
|
70
|
+
.map(([name, sha]) => ({ name, commit: sha }))
|
|
71
|
+
.sort((left, right) => left.name.localeCompare(right.name))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Which shipped skill bodies were rewritten between `ref` and the current
|
|
76
|
+
* `HEAD` of the tree at `root`.
|
|
77
|
+
*
|
|
78
|
+
* This measures a file moving rather than a held copy differing. A session
|
|
79
|
+
* cannot read its own loaded body as bytes, so the comparison runs against
|
|
80
|
+
* history and a session passing a ref older than its oldest load over-reports.
|
|
81
|
+
* That is the safe direction, since confirming a name costs one read of the
|
|
82
|
+
* body while the failure this answers is silence.
|
|
83
|
+
*
|
|
84
|
+
* Every path that cannot answer returns `unreadable` with its own reason. A
|
|
85
|
+
* target project loads the plugin from a marketplace cache with no history
|
|
86
|
+
* behind it, where reporting an empty result would read as a clean tree.
|
|
87
|
+
*/
|
|
88
|
+
export function readDrift(root: string, ref: string): DriftReport {
|
|
89
|
+
if (!existsSync(join(root, SHIPPED_SKILLS))) {
|
|
90
|
+
return {
|
|
91
|
+
kind: 'unreadable',
|
|
92
|
+
reason: `No shipped skill corpus under ${root}. Looked for ${SHIPPED_SKILLS}.`,
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const head = revParse(root, 'HEAD')
|
|
97
|
+
if (head === undefined) {
|
|
98
|
+
return {
|
|
99
|
+
kind: 'unreadable',
|
|
100
|
+
reason: `No git history under ${root}. A plugin loaded from a marketplace cache carries none, so a ref cannot be resolved there.`,
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const base = revParse(root, ref)
|
|
105
|
+
if (base === undefined) {
|
|
106
|
+
return {
|
|
107
|
+
kind: 'unreadable',
|
|
108
|
+
reason: `Ref ${ref} resolves to no commit in ${root}. Pass a commit this tree carries.`,
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const result = execaSync(
|
|
113
|
+
'git',
|
|
114
|
+
[
|
|
115
|
+
'-C',
|
|
116
|
+
root,
|
|
117
|
+
'log',
|
|
118
|
+
'--name-only',
|
|
119
|
+
'--no-renames',
|
|
120
|
+
'--format=%H',
|
|
121
|
+
`${base}..${head}`,
|
|
122
|
+
'--',
|
|
123
|
+
SHIPPED_SKILLS,
|
|
124
|
+
],
|
|
125
|
+
{ reject: false, env: gitEnv(), extendEnv: false },
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
if (result.exitCode !== 0) {
|
|
129
|
+
return {
|
|
130
|
+
kind: 'unreadable',
|
|
131
|
+
reason: `Reading history between ${ref} and HEAD failed: ${result.stderr || result.stdout}`,
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
kind: 'measured',
|
|
137
|
+
base,
|
|
138
|
+
head,
|
|
139
|
+
moved: parseMovedBodies(result.stdout),
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* The commit a ref names, or `undefined` when the tree carries no repository or
|
|
145
|
+
* no such ref. `^{commit}` is what turns a tag or a tree into the commit behind
|
|
146
|
+
* it, so a caller never compares a ref against an object of another type.
|
|
147
|
+
*/
|
|
148
|
+
function revParse(root: string, ref: string): string | undefined {
|
|
149
|
+
const result = execaSync(
|
|
150
|
+
'git',
|
|
151
|
+
['-C', root, 'rev-parse', '--verify', '--quiet', `${ref}^{commit}`],
|
|
152
|
+
{ reject: false, env: gitEnv(), extendEnv: false },
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
return result.exitCode === 0 ? result.stdout.trim() : undefined
|
|
156
|
+
}
|
package/src/commands/claude.ts
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
type SkillFinding,
|
|
22
22
|
type SkillsAudit,
|
|
23
23
|
} from '@/claude/skills-audit'
|
|
24
|
+
import { type DriftReport, readDrift } from '@/claude/skills-drift'
|
|
24
25
|
import { listSkills } from '@/claude/skills-list'
|
|
25
26
|
import {
|
|
26
27
|
planSettings,
|
|
@@ -67,6 +68,10 @@ interface SkillsAuditOptions {
|
|
|
67
68
|
readonly requirementsOnly?: boolean
|
|
68
69
|
}
|
|
69
70
|
|
|
71
|
+
interface SkillsDriftOptions {
|
|
72
|
+
readonly json?: boolean
|
|
73
|
+
}
|
|
74
|
+
|
|
70
75
|
const SEEDED_FILES: readonly string[] = [
|
|
71
76
|
'ARCHITECTURE.md',
|
|
72
77
|
'REQUIREMENTS.md',
|
|
@@ -157,15 +162,15 @@ export function register(program: Command): void {
|
|
|
157
162
|
|
|
158
163
|
const skills = claude
|
|
159
164
|
.command('skills')
|
|
160
|
-
.description('Plugin skill catalog (list, audit)')
|
|
161
|
-
.argument('[subcommand]', "One of 'list' or '
|
|
165
|
+
.description('Plugin skill catalog (list, audit, drift)')
|
|
166
|
+
.argument('[subcommand]', "One of 'list', 'audit', or 'drift'")
|
|
162
167
|
.helpOption('-h, --help', 'Show this help message')
|
|
163
168
|
.action((subcommand: string | undefined) => {
|
|
164
169
|
intro('aitk claude')
|
|
165
170
|
logError(
|
|
166
171
|
subcommand === undefined
|
|
167
|
-
? "Missing subcommand. Use 'list' or '
|
|
168
|
-
: `Unknown subcommand: ${subcommand}. Use 'list' or '
|
|
172
|
+
? "Missing subcommand. Use 'list', 'audit', or 'drift'."
|
|
173
|
+
: `Unknown subcommand: ${subcommand}. Use 'list', 'audit', or 'drift'.`,
|
|
169
174
|
)
|
|
170
175
|
outro()
|
|
171
176
|
process.exitCode = 1
|
|
@@ -225,6 +230,34 @@ export function register(program: Command): void {
|
|
|
225
230
|
.action(async (path: string | undefined, opts: SkillsAuditOptions) => {
|
|
226
231
|
process.exitCode = await runSkillsAudit(path, opts)
|
|
227
232
|
})
|
|
233
|
+
|
|
234
|
+
skills
|
|
235
|
+
.command('drift')
|
|
236
|
+
.description('Name the shipped skill bodies rewritten since a given ref')
|
|
237
|
+
.argument('<ref>', 'The commit a session started from')
|
|
238
|
+
.helpOption('-h, --help', 'Show this help message')
|
|
239
|
+
.option('--json', 'Add a machine-readable record on stdout')
|
|
240
|
+
.addHelpText(
|
|
241
|
+
'after',
|
|
242
|
+
[
|
|
243
|
+
'',
|
|
244
|
+
'Exit codes:',
|
|
245
|
+
' 0 history was read, whether or not a body moved',
|
|
246
|
+
' 1 the question could not be answered, with the reason on stderr',
|
|
247
|
+
'',
|
|
248
|
+
'A moved body means the file changed, not that a session holds a stale',
|
|
249
|
+
'copy. Passing a ref older than the oldest load over-reports, which is',
|
|
250
|
+
'the safe direction. Confirm a name by reading the body.',
|
|
251
|
+
'',
|
|
252
|
+
'Examples:',
|
|
253
|
+
' aitk claude skills drift HEAD~20',
|
|
254
|
+
' aitk claude skills drift 02d7b265 --json',
|
|
255
|
+
'',
|
|
256
|
+
].join('\n'),
|
|
257
|
+
)
|
|
258
|
+
.action((ref: string, opts: SkillsDriftOptions) => {
|
|
259
|
+
process.exitCode = runSkillsDrift(ref, opts)
|
|
260
|
+
})
|
|
228
261
|
}
|
|
229
262
|
|
|
230
263
|
function succeed(message: string): number {
|
|
@@ -467,6 +500,75 @@ function runSkillsList(opts: SkillsListOptions): number {
|
|
|
467
500
|
return 0
|
|
468
501
|
}
|
|
469
502
|
|
|
503
|
+
/**
|
|
504
|
+
* Measures the cwd for the same reason the audit does, and takes the ref as a
|
|
505
|
+
* required argument with no default. `HEAD` would be the only defensible one and
|
|
506
|
+
* it answers every run with nothing moved, which is the silence this reports
|
|
507
|
+
* against.
|
|
508
|
+
*/
|
|
509
|
+
function runSkillsDrift(ref: string, opts: SkillsDriftOptions): number {
|
|
510
|
+
const root = process.cwd()
|
|
511
|
+
const report = readDrift(root, ref)
|
|
512
|
+
|
|
513
|
+
if (report.kind === 'measured') {
|
|
514
|
+
intro('aitk claude skills drift')
|
|
515
|
+
reportDrift(report, ref)
|
|
516
|
+
outro()
|
|
517
|
+
} else {
|
|
518
|
+
frameError(report.reason)
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
if (opts.json) {
|
|
522
|
+
process.stdout.write(
|
|
523
|
+
`${JSON.stringify(
|
|
524
|
+
report.kind === 'measured'
|
|
525
|
+
? {
|
|
526
|
+
root,
|
|
527
|
+
ref,
|
|
528
|
+
base: report.base,
|
|
529
|
+
head: report.head,
|
|
530
|
+
moved: report.moved,
|
|
531
|
+
}
|
|
532
|
+
: { root, ref, unreadable: report.reason },
|
|
533
|
+
)}\n`,
|
|
534
|
+
)
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
return report.kind === 'measured' ? 0 : 1
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* States the bound on every run, including the run that names nothing. A report
|
|
542
|
+
* listing only what moved reads as a verdict on what a session holds, and the
|
|
543
|
+
* command has no access to that.
|
|
544
|
+
*/
|
|
545
|
+
function reportDrift(
|
|
546
|
+
report: Extract<DriftReport, { kind: 'measured' }>,
|
|
547
|
+
ref: string,
|
|
548
|
+
): void {
|
|
549
|
+
logStep('Range')
|
|
550
|
+
logInfo(`${ref} to HEAD, resolved as ${report.base}..${report.head}.`)
|
|
551
|
+
logInfo(
|
|
552
|
+
'A body here changed on disk. Whether a session still holds the old one is what reading it settles.',
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
logStep('Moved bodies')
|
|
556
|
+
if (report.moved.length === 0) {
|
|
557
|
+
logInfo('No shipped body changed in this range.')
|
|
558
|
+
return
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const count = report.moved.length
|
|
562
|
+
logWarn(
|
|
563
|
+
`${count} skill ${count === 1 ? 'body' : 'bodies'} rewritten since ${ref}`,
|
|
564
|
+
)
|
|
565
|
+
pipeOutput(
|
|
566
|
+
report.moved
|
|
567
|
+
.map((moved) => `${moved.name} ${moved.commit.slice(0, 8)}`)
|
|
568
|
+
.join('\n'),
|
|
569
|
+
)
|
|
570
|
+
}
|
|
571
|
+
|
|
470
572
|
/**
|
|
471
573
|
* Measures the tree at the cwd rather than the toolkit root the catalog reads,
|
|
472
574
|
* so a linked worktree audits its own branch instead of reporting on `main`. A
|