@erclx/aitk 0.18.0 → 0.20.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/README.md +1 -1
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/claude-design-extract/SKILL.md +49 -15
- package/claude/skills/claude-design-propose/SKILL.md +6 -66
- package/claude/skills/create-snippet/SKILL.md +5 -20
- package/claude/skills/create-standard/SKILL.md +11 -7
- package/claude/skills/{create-snippet → create-standard}/references/snippets.md +1 -1
- package/docs/ai-workflow.md +18 -18
- package/docs/target-projects.md +1 -1
- package/docs/visual-design-workflow.md +2 -2
- package/package.json +1 -1
- package/scripts/lib/sandbox-path.sh +108 -0
- package/scripts/lib/ui.sh +9 -1
- package/scripts/manage-sandbox.sh +10 -4
- package/src/commands/sandbox.ts +25 -1
- package/src/sandbox/expect.ts +38 -0
- package/standards/bundled/snippets.md +1 -1
package/README.md
CHANGED
|
@@ -48,7 +48,7 @@ Each domain has a canonical source in this repo and a thin install or sync CLI o
|
|
|
48
48
|
- [Standards](.claude/context/standards.md): shared authoring conventions, synced to projects
|
|
49
49
|
- [Snippets](.claude/context/snippets.md): reusable prompts for Claude and Gemini chat
|
|
50
50
|
- [Tooling stacks](.claude/context/tooling.md): golden configs, seeds, and references per framework
|
|
51
|
-
- [Design system](.claude/context/design.md): `DESIGN.md` token shape, extract skill, render command
|
|
51
|
+
- [Design system](.claude/context/design.md): `DESIGN.md` token shape, extract skill and its two paths, render command
|
|
52
52
|
- [Slides](.claude/context/slides.md): `SLIDES.md` source, layout catalog, render command, draft skill
|
|
53
53
|
- [Transcripts](.claude/context/transcripts.md): fetch a YouTube transcript with metadata frontmatter via `aitk transcripts`
|
|
54
54
|
- [Sandbox](.claude/context/sandbox.md): scenario-based scaffolds for verifying each domain flow
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: claude-design-extract
|
|
3
|
-
description: Drafts `.claude/DESIGN.md` from a project's existing prose and shell UI surfaces
|
|
3
|
+
description: Drafts `.claude/DESIGN.md` from a project's existing prose and shell UI surfaces, or proposes token values from `REQUIREMENTS.md` and a `## Personality` section when no UI code exists yet. Use when asked to "extract the design system", "draft DESIGN.md", "bootstrap design tokens", "capture the visual system", "propose a design system", "bootstrap DESIGN.md from scratch", "draft tokens for a greenfield project", or "replace Claude Design onboarding". Do NOT use to mutate an existing `.claude/DESIGN.md`.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Design extract
|
|
@@ -10,23 +10,39 @@ description: Drafts `.claude/DESIGN.md` from a project's existing prose and shel
|
|
|
10
10
|
- If `.claude/DESIGN.md` already exists and has content beyond the seed template, stop: `❌ .claude/DESIGN.md already populated. Edit directly or archive the existing file first.`
|
|
11
11
|
- If `aitk` is not on PATH, stop: `❌ aitk CLI not found.`
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
Step 1 carries two more stops that apply to one path only. Do not evaluate them before the path is picked.
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
## Step 1: pick the path from what the project already has
|
|
16
|
+
|
|
17
|
+
Check the project root for UI surfaces: `src/ui.ts`, `src/ui.tsx`, `src/components/**`, `scripts/lib/ui.sh`, and any `*.css`, `tailwind.config.*`, or `theme.*`.
|
|
18
|
+
|
|
19
|
+
- Any match takes the source path. Existing code defines the system, so every filled cell traces to a value already in the tree.
|
|
20
|
+
- No match takes the greenfield path. Nothing anchors a value, so every filled cell is a proposal and carries the tag that says so.
|
|
21
|
+
|
|
22
|
+
The project decides this, never a user flag or an argument. Announce which path ran in one line before Step 2, since the two produce different-looking output from the same skill.
|
|
23
|
+
|
|
24
|
+
The greenfield path needs a personality paragraph to propose against, and stops without one. The source path needs neither file and skips both stops.
|
|
25
|
+
|
|
26
|
+
- If `.claude/REQUIREMENTS.md` is missing, stop: `❌ .claude/REQUIREMENTS.md not found. Write requirements before proposing a design system.`
|
|
27
|
+
- If `.claude/REQUIREMENTS.md` has no `## Personality` section, stop: `❌ .claude/REQUIREMENTS.md missing ## Personality section. Add a paragraph describing voice and tone before running this skill.`
|
|
28
|
+
|
|
29
|
+
## Step 2: read source signals in parallel
|
|
30
|
+
|
|
31
|
+
Read these from the project root on both paths, skipping any that do not exist:
|
|
16
32
|
|
|
17
33
|
- `CLAUDE.md`: voice, personality, spelling rules
|
|
18
|
-
- `.claude/REQUIREMENTS.md`: worldview
|
|
34
|
+
- `.claude/REQUIREMENTS.md`: the `## Personality` paragraph, worldview, non-goals
|
|
19
35
|
- `.claude/standards/prose.md`: tone constraints
|
|
20
|
-
- `src/ui.ts`, `src/ui.tsx`, `src/components/**`: color codes, typography, spacing constants
|
|
21
|
-
- `scripts/lib/ui.sh`, `scripts/lib/*.sh`: ANSI color codes, frame glyphs, spacing conventions
|
|
22
|
-
- `docs/agents.md`, `docs/index.md`: any output shape or framing rules already documented
|
|
23
|
-
- Any `*.css`, `tailwind.config.*`, or `theme.*` file at the project root
|
|
24
36
|
|
|
25
37
|
Read a standard from `${CLAUDE_SKILL_DIR}/../../standards/` instead when the project does not have it.
|
|
26
38
|
|
|
39
|
+
On the source path, also read the UI surfaces matched in Step 1 plus `docs/agents.md` and `docs/index.md` for output shape or framing rules already documented.
|
|
40
|
+
|
|
41
|
+
On the greenfield path, also read `.claude/ARCHITECTURE.md` for platform, tech stack, and surface type. Do not scan `src/`, stylesheets, or UI modules. Step 1 already established they hold nothing.
|
|
42
|
+
|
|
27
43
|
Run these reads in parallel. Do not speculatively recurse into every directory.
|
|
28
44
|
|
|
29
|
-
## Step
|
|
45
|
+
## Step 3: fetch the seed template
|
|
30
46
|
|
|
31
47
|
Run this from the project root:
|
|
32
48
|
|
|
@@ -36,20 +52,38 @@ aitk claude seeds list --json | jq -r '.[] | select(.path == ".claude/DESIGN.md"
|
|
|
36
52
|
|
|
37
53
|
Use the returned content as the target shape. Keep every section heading and every table header intact. The `aitk design render` parser depends on them.
|
|
38
54
|
|
|
39
|
-
## Step
|
|
55
|
+
## Step 4: fill the template
|
|
56
|
+
|
|
57
|
+
Walk each section once. Follow `.claude/standards/prose.md` throughout: no em dashes, no semicolons, no marketing buzzwords. Use commas or separate sentences instead.
|
|
58
|
+
|
|
59
|
+
Mark any cell not traced to a source value by appending ` ? verify` inside the cell value, never as a trailing column. The cell stays inside the table shape: `| #ffffff ? verify |`. A trailing `| ? verify` after the row breaks the parser. A prose section takes its uncertainty inline instead, for example `Proposed 150ms ease-out, not yet confirmed.`, because a trailing tag on a sentence renders raw in the preview.
|
|
60
|
+
|
|
61
|
+
On the source path, the tag marks the exception. On the greenfield path it marks nearly every cell, since the values are speculative until code or a designer anchors them.
|
|
40
62
|
|
|
41
|
-
|
|
63
|
+
### Source path
|
|
42
64
|
|
|
43
|
-
- **Personality**: one paragraph. Transcribe what `CLAUDE.md` and `.claude/REQUIREMENTS.md` say about voice, tone, and visual feeling. Do not invent rules the source does not state.
|
|
65
|
+
- **Personality**: one paragraph. Transcribe what `CLAUDE.md` and `.claude/REQUIREMENTS.md` say about voice, tone, and visual feeling. Do not invent rules the source does not state. If nothing matches, write a one-sentence placeholder ending in `? verify`.
|
|
44
66
|
- **Color**: one row per role. Source hex values from the CLI UI files or stylesheets. If a role has no source signal, leave `Value` blank rather than guessing.
|
|
45
67
|
- **Typography**: one row per role. Source families and sizes from stylesheet or theme config. Leave cells blank when no signal exists.
|
|
46
68
|
- **Spacing**: fill the base unit and multipliers from stylesheet tokens or obvious repeated values in the UI code.
|
|
47
69
|
- **Borders**: one row per role. Source from stylesheet or CSS variables.
|
|
48
70
|
- **Motion** and **Iconography**: one line each. Default to `No animation.` and `No custom icons.` when no evidence exists.
|
|
49
71
|
|
|
50
|
-
|
|
72
|
+
### Greenfield path
|
|
73
|
+
|
|
74
|
+
Anchor every proposal to a signal, never to a default. "Calm and dense" pins muted grays and tight spacing. A requirements non-goal of "no motion" makes Motion read `No animation.` with no tag. A CLI-only surface leans Typography monospaced and keeps Borders minimal.
|
|
75
|
+
|
|
76
|
+
- **Personality**: transcribe the `## Personality` paragraph from `.claude/REQUIREMENTS.md` verbatim. This is the one section that is not a proposal. No tag.
|
|
77
|
+
- **Color**: one row per role. Rewrite the Intent cell in personality language, for example `warm off-white page canvas` instead of the seed default `page canvas`. Propose hex values matching the personality. Dense and calm gives low saturation and high text contrast. Playful gives saturated accents. Every Intent and Value cell gets `? verify`.
|
|
78
|
+
- **Typography**: one row per role. Propose families fitting the platform, system UI for web, monospaced for CLI tools, serif for editorial, and a harmonious scale. Every cell gets `? verify`.
|
|
79
|
+
- **Spacing**: propose a base unit matching density intent. Dense gives a 4px base, roomy gives 8px. Keep the Multiplier column as the seed ships it, no tag. Only the Value column gets `? verify`.
|
|
80
|
+
- **Borders**: propose radius and width per role. Sharp and technical gives a small radius, soft gives a larger one. Every Radius and Width cell gets `? verify`.
|
|
81
|
+
- **Motion**: one line. Write `No animation.` when the requirements forbid motion. Otherwise phrase the uncertainty inline.
|
|
82
|
+
- **Iconography**: one line. Propose style and source library matching personality, phrasing the uncertainty inline.
|
|
83
|
+
|
|
84
|
+
Do not invent non-goals. A proposed motion line is fine when neither the personality paragraph nor the requirements rule motion out.
|
|
51
85
|
|
|
52
|
-
## Step
|
|
86
|
+
## Step 5: write and render
|
|
53
87
|
|
|
54
88
|
Write the filled template to `.claude/DESIGN.md` from the project root. Then run:
|
|
55
89
|
|
|
@@ -65,5 +99,5 @@ The command writes an HTML plus CSS preview to `.claude/review/design/`. Output
|
|
|
65
99
|
📝 Wrote .claude/DESIGN.md
|
|
66
100
|
📝 Wrote .claude/review/design/index.html
|
|
67
101
|
|
|
68
|
-
N cells marked `? verify`. Open the preview and confirm before committing.
|
|
102
|
+
Ran the <source|greenfield> path. N cells marked `? verify`. Open the preview and confirm before committing.
|
|
69
103
|
```
|
|
@@ -1,75 +1,15 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: claude-design-propose
|
|
3
|
-
description:
|
|
3
|
+
description: Retired. `claude-design-extract` absorbed this skill and now takes the greenfield path itself. Invoke `aitk:claude-design-extract` instead.
|
|
4
|
+
disable-model-invocation: true
|
|
4
5
|
---
|
|
5
6
|
|
|
6
7
|
# Design propose
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
Retired. `claude-design-extract` absorbed the greenfield path, picks it from what the project has, and its description carries every trigger this skill used to route on.
|
|
9
10
|
|
|
10
|
-
-
|
|
11
|
-
- If `.claude/REQUIREMENTS.md` is missing, stop: `❌ .claude/REQUIREMENTS.md not found. Write requirements before proposing a design system.`
|
|
12
|
-
- If `.claude/REQUIREMENTS.md` has no `## Personality` section, stop: `❌ .claude/REQUIREMENTS.md missing ## Personality section. Add a paragraph describing voice and tone before running this skill.`
|
|
13
|
-
- If `aitk` is not on PATH, stop: `❌ aitk CLI not found.`
|
|
11
|
+
Invoke `aitk:claude-design-extract` instead. It reads `.claude/REQUIREMENTS.md` for the `## Personality` paragraph and `.claude/ARCHITECTURE.md` for platform signals when the project has no UI code, which is what this skill did.
|
|
14
12
|
|
|
15
|
-
|
|
13
|
+
This body exists so a project that installed the plugin before the merge keeps resolving the old name for one sync cycle. It ships in `0.18.0` and is removed in `0.19.0`.
|
|
16
14
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
- `.claude/REQUIREMENTS.md`: the `## Personality` paragraph, worldview, non-goals
|
|
20
|
-
- `.claude/ARCHITECTURE.md`: platform, tech stack, surface type (CLI, web, desktop)
|
|
21
|
-
- `CLAUDE.md`: voice rules, spelling, conventions
|
|
22
|
-
- `.claude/standards/prose.md`: tone constraints if present
|
|
23
|
-
|
|
24
|
-
Read a standard from `${CLAUDE_SKILL_DIR}/../../standards/` instead when the project does not have it.
|
|
25
|
-
|
|
26
|
-
Do not scan `src/`, stylesheets, or UI modules. This skill runs before code exists. If those files are present, the scenario is wrong and `claude-design-extract` is the correct tool.
|
|
27
|
-
|
|
28
|
-
## Step 2: fetch the seed template
|
|
29
|
-
|
|
30
|
-
Run this from the project root:
|
|
31
|
-
|
|
32
|
-
```bash
|
|
33
|
-
aitk claude seeds list --json | jq -r '.[] | select(.path == ".claude/DESIGN.md") | .content'
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
Use the returned content as the target shape. Keep every section heading and every table header intact. The `aitk design render` parser depends on them.
|
|
37
|
-
|
|
38
|
-
## Step 3: propose tokens from personality and requirements
|
|
39
|
-
|
|
40
|
-
Walk each section once. Follow `.claude/standards/prose.md` throughout: no em dashes, no semicolons, no marketing buzzwords. Use commas or separate sentences instead.
|
|
41
|
-
|
|
42
|
-
Every proposed table cell is speculative, so every filled table cell gets a trailing `? verify` tag inside the cell value, never as a trailing column.
|
|
43
|
-
|
|
44
|
-
Anchor proposals to signals, not defaults. If the personality says "calm and dense", that pins muted grays and tight spacing. If the requirements say "no motion", Motion writes `No animation.` with no tag. If the architecture names a CLI-only surface, Typography leans monospaced and Borders stays minimal.
|
|
45
|
-
|
|
46
|
-
- **Personality**: transcribe the `## Personality` paragraph from `.claude/REQUIREMENTS.md` verbatim. This is the one section that is not a proposal. No tag.
|
|
47
|
-
- **Color**: one row per role. Rewrite the Intent cell in personality language, for example `warm off-white page canvas` instead of the seed default `page canvas`. Propose hex values that match the personality. Dense and calm → low saturation, high text contrast. Playful → saturated accents. Every Intent and Value cell gets `? verify`.
|
|
48
|
-
- **Typography**: one row per role. Propose families that fit the platform (system UI for web, monospaced for CLI tools, serif for editorial) and a harmonious scale. Every cell gets `? verify`.
|
|
49
|
-
- **Spacing**: propose a base unit that matches density intent. Dense → 4px base. Roomy → 8px base. Keep the Multiplier column as the seed ships it, no tag. Only the Value column gets `? verify`.
|
|
50
|
-
- **Borders**: propose radius and width per role. Sharp and technical → small radius. Soft → larger radius. Every Radius and Width cell gets `? verify`.
|
|
51
|
-
- **Motion**: one line. If the requirements forbid motion, write `No animation.` If motion is allowed, phrase uncertainty inline, for example `Proposed 150ms ease-out, not yet confirmed.` Do not append a trailing `? verify` tag to a prose sentence. It renders raw in the preview.
|
|
52
|
-
- **Iconography**: one line. Propose style and source library matching personality. Phrase uncertainty inline, for example `Proposed outline style, source library not yet chosen.` Do not append a trailing `? verify` tag.
|
|
53
|
-
|
|
54
|
-
Do not invent non-goals. If the personality paragraph does not mention motion and the requirements do not forbid it, a proposed motion line is acceptable.
|
|
55
|
-
|
|
56
|
-
A cell marked `? verify` must stay inside the table shape: `| #ffffff ? verify |`. A trailing `| ? verify` column breaks the parser.
|
|
57
|
-
|
|
58
|
-
## Step 4: write and render
|
|
59
|
-
|
|
60
|
-
Write the filled template to `.claude/DESIGN.md` from the project root. Then run:
|
|
61
|
-
|
|
62
|
-
```bash
|
|
63
|
-
aitk design render
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
The command writes an HTML plus CSS preview to `.claude/review/design/`. Output the path in chat so the user can open it.
|
|
67
|
-
|
|
68
|
-
## Response format
|
|
69
|
-
|
|
70
|
-
```plaintext
|
|
71
|
-
📝 Wrote .claude/DESIGN.md
|
|
72
|
-
📝 Wrote .claude/review/design/index.html
|
|
73
|
-
|
|
74
|
-
N cells marked `? verify`. Open the preview, confirm the personality read as intended, then edit DESIGN.md directly to lock values.
|
|
75
|
-
```
|
|
15
|
+
`scripts/sandbox/claude/design-propose.sh` goes at the same time, but its fixture does not. That scenario is the only greenfield coverage the survivor has, so `0.19.0` folds it into `scripts/sandbox/claude/design-extract.sh` as a second arm rather than deleting it. Removing the file outright drops the greenfield path from the catalog.
|
|
@@ -1,28 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: create-snippet
|
|
3
|
-
description:
|
|
3
|
+
description: Retired. `create-standard` absorbed this skill and writes snippets as well as standards. Invoke `aitk:create-standard` instead.
|
|
4
|
+
disable-model-invocation: true
|
|
4
5
|
---
|
|
5
6
|
|
|
6
7
|
# Create snippet
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
Retired. `create-standard` absorbed the snippet surface, resolves `snippets/` or `.claude/snippets/` the same way it resolves the standards folders, and its description carries every trigger this skill used to route on.
|
|
9
10
|
|
|
10
|
-
-
|
|
11
|
-
- `.claude/standards/prose.md` from the project root: prose conventions for all generated text
|
|
11
|
+
Invoke `aitk:create-standard` instead and ask for a snippet. The name understates what the skill covers, which is why its description names both surfaces.
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
## Guards
|
|
16
|
-
|
|
17
|
-
- If neither `snippets/` nor `.claude/snippets/` exists, stop: `❌ No snippets/ or .claude/snippets/ directory found.`
|
|
18
|
-
|
|
19
|
-
## Steps
|
|
20
|
-
|
|
21
|
-
1. Resolve the write surface: `snippets/` at the project root if present, the toolkit's own authoring source. Otherwise `.claude/snippets/`, a target project's installed copy.
|
|
22
|
-
2. Confirm the slug and full content with the user before writing
|
|
23
|
-
3. Write the file to `<surface>/<slug>.md`
|
|
24
|
-
|
|
25
|
-
## After writing
|
|
26
|
-
|
|
27
|
-
- Root surface (`snippets/`): this is the toolkit's authoring source. Remind the user `bun run check` regenerates the consumed copy under `.claude/snippets/`.
|
|
28
|
-
- `.claude/snippets/` surface: the file is project-local. `aitk snippets sync` leaves it alone, since sync only updates filenames it recognizes from the toolkit. Remind the user to copy it to the toolkit repo, under `snippets/<category>/<name>.md`, if it should ship to every project.
|
|
13
|
+
This body exists so a project that installed the plugin before the merge keeps resolving the old name for one sync cycle. It ships in `0.18.0` and is removed in `0.19.0`.
|
|
@@ -1,25 +1,29 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: create-standard
|
|
3
|
-
description: Creates a new standard file in `standards/`
|
|
3
|
+
description: Creates a new standard file in `standards/` or `.claude/standards/`, or a new snippet file in `snippets/` or `.claude/snippets/`. Use when asked to create a standard, add a standard, write a new authoring convention, create a snippet, add a snippet, or make a new snippet. Do NOT use to edit an existing standard or snippet.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Create standard
|
|
7
7
|
|
|
8
|
+
Creates one authoring file on either surface. Read the user's request for which artifact they asked for, standard or snippet, and carry that choice through every step below. Ask only when the request names neither.
|
|
9
|
+
|
|
8
10
|
Read these files in parallel:
|
|
9
11
|
|
|
10
|
-
- `.claude/standards/standard.md` from the project root: the meta-standard for a standard's shape, frontmatter, and structure
|
|
11
12
|
- `.claude/standards/prose.md` from the project root: prose conventions for all generated text
|
|
13
|
+
- `.claude/standards/standard.md` from the project root, for a standard: the meta-standard for shape, frontmatter, and structure
|
|
14
|
+
- `${CLAUDE_SKILL_DIR}/references/snippets.md`, for a snippet: authoring conventions, invocation channels, use patterns
|
|
12
15
|
|
|
13
16
|
Read a standard from `${CLAUDE_SKILL_DIR}/../../standards/` instead when the project does not have it.
|
|
14
17
|
|
|
15
18
|
## Guards
|
|
16
19
|
|
|
17
|
-
-
|
|
20
|
+
- For a standard, if neither `standards/` nor `.claude/standards/` exists, stop: `❌ No standards/ or .claude/standards/ directory found.`
|
|
21
|
+
- For a snippet, if neither `snippets/` nor `.claude/snippets/` exists, stop: `❌ No snippets/ or .claude/snippets/ directory found.`
|
|
18
22
|
|
|
19
23
|
## Steps
|
|
20
24
|
|
|
21
|
-
1. Resolve the write surface: `standards/`
|
|
22
|
-
2. Draft the
|
|
25
|
+
1. Resolve the write surface for the chosen artifact: the root folder (`standards/` or `snippets/`) if present, the toolkit's own authoring source. Otherwise the `.claude/` folder, a target project's installed copy.
|
|
26
|
+
2. Draft the content from the user's description. A standard follows `standard.md` for frontmatter, headings, and structure.
|
|
23
27
|
3. Confirm the slug and full content with the user before writing
|
|
24
28
|
4. Write the file to `<surface>/<slug>.md`
|
|
25
29
|
|
|
@@ -27,5 +31,5 @@ Read a standard from `${CLAUDE_SKILL_DIR}/../../standards/` instead when the pro
|
|
|
27
31
|
|
|
28
32
|
Emit the full path on its own line.
|
|
29
33
|
|
|
30
|
-
- Root surface
|
|
31
|
-
- `.claude
|
|
34
|
+
- Root surface: this is the toolkit's authoring source. Remind the user to run `bun run check` to regenerate the consumed copy under `.claude/`. For a standard, that pass also regenerates the `standards/index.md` entry, and the user adds a row to the standards table in `.claude/context/standards.md`.
|
|
35
|
+
- `.claude/` surface: the file is project-local. `aitk standards sync` and `aitk snippets sync` leave it alone, since sync only updates filenames it recognizes from the toolkit. Remind the user to copy it to the toolkit repo, under `standards/<slug>.md` or `snippets/<category>/<name>.md`, if it should ship to every project.
|
package/docs/ai-workflow.md
CHANGED
|
@@ -126,24 +126,24 @@ Before the first feature session on a UI-heavy project, pick a design tier. The
|
|
|
126
126
|
|
|
127
127
|
## Skills
|
|
128
128
|
|
|
129
|
-
| Skill | When to use
|
|
130
|
-
| ---------------------------- |
|
|
131
|
-
| `aitk:claude-groundwork` | Before a plan is warranted, measure an unknown in a scratch folder under `.claude/.tmp/groundwork/`
|
|
132
|
-
| `aitk:claude-feature` | Before implementation, scan for conflicts and ambiguities
|
|
133
|
-
| `aitk:claude-roadmap` | Sequence MVP scope into ordered versions in `.claude/ROADMAP.md`
|
|
134
|
-
| `aitk:claude-orchestrate` | Assert the orchestrator role, refill the ready queue, and dispatch the roadmap, feature, and review skills
|
|
135
|
-
| `aitk:claude-diagram` | Draft per-kind mermaid entries under `.claude/diagrams/` from architecture and code, then verify each rendered image
|
|
136
|
-
| `aitk:claude-design-
|
|
137
|
-
| `aitk:claude-review` | In a fresh session, review all changes since main
|
|
138
|
-
| `aitk:claude-pr-review` | Review an open PR from an independent session, then close the review out against the commits added since
|
|
139
|
-
| `aitk:claude-address-review` | Address PR findings and CI status, refresh stale docs, then push a follow-up
|
|
140
|
-
| `aitk:claude-tasks` | Add a task to `.claude/tasks/` or archive a shipped one out of the folder
|
|
141
|
-
| `aitk:claude-docs` | When decisions diverged from plan, update `.claude/` docs
|
|
142
|
-
| `aitk:claude-ui-test` | After UI changes, generate and run e2e tests + visual checklist
|
|
143
|
-
| `aitk:claude-ux-audit` | Audit existing UI surfaces for missing states, edge cases, inconsistencies
|
|
144
|
-
| `aitk:claude-autoship` | After plan approval, chain implement → verify → review → draft PR
|
|
145
|
-
| `aitk:systematic-debugging` | When a test fails or bug surfaces, enforce root-cause investigation before fixes
|
|
146
|
-
| `aitk:git-ship` | Post-feature: sync docs, commit, rename branch, open PR
|
|
129
|
+
| Skill | When to use |
|
|
130
|
+
| ---------------------------- | -------------------------------------------------------------------------------------------------------------------- |
|
|
131
|
+
| `aitk:claude-groundwork` | Before a plan is warranted, measure an unknown in a scratch folder under `.claude/.tmp/groundwork/` |
|
|
132
|
+
| `aitk:claude-feature` | Before implementation, scan for conflicts and ambiguities |
|
|
133
|
+
| `aitk:claude-roadmap` | Sequence MVP scope into ordered versions in `.claude/ROADMAP.md` |
|
|
134
|
+
| `aitk:claude-orchestrate` | Assert the orchestrator role, refill the ready queue, and dispatch the roadmap, feature, and review skills |
|
|
135
|
+
| `aitk:claude-diagram` | Draft per-kind mermaid entries under `.claude/diagrams/` from architecture and code, then verify each rendered image |
|
|
136
|
+
| `aitk:claude-design-extract` | Draft `.claude/DESIGN.md`, sourcing tokens from existing UI code or proposing them from requirements on day one |
|
|
137
|
+
| `aitk:claude-review` | In a fresh session, review all changes since main |
|
|
138
|
+
| `aitk:claude-pr-review` | Review an open PR from an independent session, then close the review out against the commits added since |
|
|
139
|
+
| `aitk:claude-address-review` | Address PR findings and CI status, refresh stale docs, then push a follow-up |
|
|
140
|
+
| `aitk:claude-tasks` | Add a task to `.claude/tasks/` or archive a shipped one out of the folder |
|
|
141
|
+
| `aitk:claude-docs` | When decisions diverged from plan, update `.claude/` docs |
|
|
142
|
+
| `aitk:claude-ui-test` | After UI changes, generate and run e2e tests + visual checklist |
|
|
143
|
+
| `aitk:claude-ux-audit` | Audit existing UI surfaces for missing states, edge cases, inconsistencies |
|
|
144
|
+
| `aitk:claude-autoship` | After plan approval, chain implement → verify → review → draft PR |
|
|
145
|
+
| `aitk:systematic-debugging` | When a test fails or bug surfaces, enforce root-cause investigation before fixes |
|
|
146
|
+
| `aitk:git-ship` | Post-feature: sync docs, commit, rename branch, open PR |
|
|
147
147
|
|
|
148
148
|
## Feedback routing
|
|
149
149
|
|
package/docs/target-projects.md
CHANGED
|
@@ -56,7 +56,7 @@ Keep the `## Scripts` table in `.claude/context/development.md` current as scrip
|
|
|
56
56
|
Scaffold installs tooling and seeds. It does not fill the planning docs or the design system. Complete those before the first feature session:
|
|
57
57
|
|
|
58
58
|
1. Fill `.claude/REQUIREMENTS.md` and `.claude/ARCHITECTURE.md`. The seed provides the files, the scope and decisions are yours to write.
|
|
59
|
-
2. For a UI project, invoke `aitk:claude-design-
|
|
59
|
+
2. For a UI project, invoke `aitk:claude-design-extract` to draft `.claude/DESIGN.md`. With no UI code yet it takes the greenfield path and proposes tokens from the requirements and a `## Personality` section. Skip for non-UI projects.
|
|
60
60
|
3. Optionally invoke `aitk:claude-diagram` to draft entries under `.claude/diagrams/` from the architecture and the requirements. One file per diagram kind, so a later refresh of one kind leaves the others untouched. It renders each diagram it writes to verify the layout, which downloads the Mermaid CLI on first use and takes about 15 seconds. A machine without a renderer still gets the diagrams and is told which check was skipped. Each entry records the commit and date it was last verified against, and `aitk:claude-docs` maintains that record on every ship: it annotates an entry whose cited code path left the tree and stubs a kind whose source signal arrived uncovered. The sweep writes frontmatter only, so a diagram's picture and prose change when you redraw them and at no other time.
|
|
61
61
|
4. Start the feature loop. See [AI workflow](ai-workflow.md) for the per-feature sequence.
|
|
62
62
|
|
|
@@ -18,7 +18,7 @@ The default. `.claude/DESIGN.md` holds visual intent as prose plus token tables
|
|
|
18
18
|
|
|
19
19
|
### Seed shape
|
|
20
20
|
|
|
21
|
-
The toolkit seed in `tooling/claude/seeds/.claude/DESIGN.md` ships a token-table template. The `aitk:claude-design-extract` skill drafts the file from a project's existing prose and CLI UI surfaces. `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`.
|
|
21
|
+
The toolkit seed in `tooling/claude/seeds/.claude/DESIGN.md` ships a token-table template. 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`.
|
|
22
22
|
|
|
23
23
|
### Tools
|
|
24
24
|
|
|
@@ -27,7 +27,7 @@ The toolkit seed in `tooling/claude/seeds/.claude/DESIGN.md` ships a token-table
|
|
|
27
27
|
|
|
28
28
|
### Skills
|
|
29
29
|
|
|
30
|
-
- `aitk:claude-design-extract` to draft `.claude/DESIGN.md
|
|
30
|
+
- `aitk:claude-design-extract` to draft `.claude/DESIGN.md`, from existing project signals or from requirements alone on day one
|
|
31
31
|
- `aitk:claude-ui-test` for e2e test generation after UI changes
|
|
32
32
|
- `aitk:claude-ux-audit` for UX gap detection on existing surfaces
|
|
33
33
|
- Anthropic's `frontend-design` plugin optional for light visual steering
|
package/package.json
CHANGED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
# The sandbox tree lives outside the toolkit worktree. `scripts/sandbox/run.sh`
|
|
4
|
+
# sets cwd to it for `claude -p`, and every `CLAUDE.md` between that cwd and the
|
|
5
|
+
# filesystem root loads into the session. Under the repository the toolkit's own
|
|
6
|
+
# instructions join that chain beside the seeded copy the scenario installed,
|
|
7
|
+
# both carry the rule sending shared session scratch to the main worktree root,
|
|
8
|
+
# and nothing decides which root wins. A session picking the toolkit writes its
|
|
9
|
+
# output where no manifest reads it, so the run reports success while the verdict
|
|
10
|
+
# reports no writes at all. `scripts/eval/run.sh` keeps its fixture outside the
|
|
11
|
+
# repository for the same reason.
|
|
12
|
+
#
|
|
13
|
+
# Twin of `SANDBOX_DIR` in `src/commands/sandbox.ts`. The exec boundary rules out
|
|
14
|
+
# a shared constant, so a change to the default lands on both sides.
|
|
15
|
+
resolve_sandbox_dir() {
|
|
16
|
+
if [ -n "${AITK_SANDBOX_DIR:-}" ]; then
|
|
17
|
+
printf '%s\n' "$AITK_SANDBOX_DIR"
|
|
18
|
+
return 0
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
printf '%s/aitk/sandbox\n' "${XDG_STATE_HOME:-$HOME/.local/state}"
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
# Collapses repeated separators and strips every trailing one, leaving a bare
|
|
25
|
+
# root as `/`. Every comparison below is a string test, so `//` and `$HOME//`
|
|
26
|
+
# would otherwise read as paths no rule names.
|
|
27
|
+
normalize_sandbox_path() {
|
|
28
|
+
local path="$1"
|
|
29
|
+
|
|
30
|
+
while [ "$path" != "${path//\/\//\/}" ]; do
|
|
31
|
+
path="${path//\/\//\/}"
|
|
32
|
+
done
|
|
33
|
+
|
|
34
|
+
while [ "${#path}" -gt 1 ] && [ "${path%/}" != "$path" ]; do
|
|
35
|
+
path="${path%/}"
|
|
36
|
+
done
|
|
37
|
+
|
|
38
|
+
printf '%s' "$path"
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# Whether `candidate` is `target` or a directory containing it. Removing the
|
|
42
|
+
# former removes the latter, which is what makes an ancestor as dangerous as an
|
|
43
|
+
# exact match.
|
|
44
|
+
is_at_or_above() {
|
|
45
|
+
local candidate="$1"
|
|
46
|
+
local target="$2"
|
|
47
|
+
|
|
48
|
+
[ "$candidate" = "$target" ] || [ "${target#"$candidate"/}" != "$target" ]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
# Prints the reason a location is unusable and returns non-zero, so the caller
|
|
52
|
+
# reports one message naming the path rather than a bare refusal.
|
|
53
|
+
#
|
|
54
|
+
# Provisioning runs `rm -rf` on this path at three sites before staging, so the
|
|
55
|
+
# test is an allowlist rather than a list of paths to refuse. A blocklist has to
|
|
56
|
+
# name every system directory to be right once and stays wrong as soon as one is
|
|
57
|
+
# missed, while requiring a strict descendant of the home directory or of the
|
|
58
|
+
# temp root admits the default and every reasonable override and refuses `/`,
|
|
59
|
+
# `/usr`, `/etc`, and `$HOME` itself without naming any of them.
|
|
60
|
+
#
|
|
61
|
+
# The repository test is separate and runs both ways. A path under the worktree
|
|
62
|
+
# restores the ancestor chain the relocation removed, and a path above it is one
|
|
63
|
+
# `rm -rf` away from deleting the repository. It resolves the main worktree root
|
|
64
|
+
# rather than trusting `$PROJECT_ROOT`, since a linked worktree is itself inside
|
|
65
|
+
# that root.
|
|
66
|
+
assert_sandbox_dir_safe() {
|
|
67
|
+
local raw="$1"
|
|
68
|
+
local root="${2:-${PROJECT_ROOT:-$PWD}}"
|
|
69
|
+
|
|
70
|
+
if [ -z "$raw" ] || [ "${raw#/}" = "$raw" ]; then
|
|
71
|
+
printf 'AITK_SANDBOX_DIR must be an absolute path, got: %s\n' "${raw:-<empty>}"
|
|
72
|
+
return 1
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
local dir home temp
|
|
76
|
+
dir="$(normalize_sandbox_path "$raw")"
|
|
77
|
+
home="$(normalize_sandbox_path "${HOME:-/root}")"
|
|
78
|
+
temp="$(normalize_sandbox_path "${TMPDIR:-/tmp}")"
|
|
79
|
+
|
|
80
|
+
if ! is_at_or_above "$home" "$dir" && ! is_at_or_above "$temp" "$dir"; then
|
|
81
|
+
printf 'Refusing %s as the sandbox. Provisioning removes the tree first, so the path has to sit under %s or %s.\n' "$raw" "$home" "$temp"
|
|
82
|
+
return 1
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
if [ "$dir" = "$home" ] || [ "$dir" = "$temp" ]; then
|
|
86
|
+
printf 'Refusing %s as the sandbox. Provisioning removes the tree first, so the path has to sit under %s rather than be it.\n' "$raw" "$dir"
|
|
87
|
+
return 1
|
|
88
|
+
fi
|
|
89
|
+
|
|
90
|
+
local main_root
|
|
91
|
+
main_root="$(git -C "$root" worktree list --porcelain 2>/dev/null |
|
|
92
|
+
grep -m 1 '^worktree ' | cut -d' ' -f2-)"
|
|
93
|
+
main_root="$(normalize_sandbox_path "${main_root:-$root}")"
|
|
94
|
+
|
|
95
|
+
if is_at_or_above "$dir" "$main_root"; then
|
|
96
|
+
printf 'Refusing %s as the sandbox. Provisioning removes the tree first, and that path contains %s.\n' "$raw" "$main_root"
|
|
97
|
+
return 1
|
|
98
|
+
fi
|
|
99
|
+
|
|
100
|
+
case "$dir" in
|
|
101
|
+
"$main_root"/*)
|
|
102
|
+
printf 'Sandbox at %s sits inside %s, which puts the toolkit CLAUDE.md back on the session ancestor chain. Point AITK_SANDBOX_DIR outside the repository.\n' "$raw" "$main_root"
|
|
103
|
+
return 1
|
|
104
|
+
;;
|
|
105
|
+
esac
|
|
106
|
+
|
|
107
|
+
return 0
|
|
108
|
+
}
|
package/scripts/lib/ui.sh
CHANGED
|
@@ -6,6 +6,12 @@ if [ "${BASH_VERSINFO[0]:-0}" -lt 4 ]; then
|
|
|
6
6
|
exit 1
|
|
7
7
|
fi
|
|
8
8
|
|
|
9
|
+
# Resolved from this file's own location rather than `$PROJECT_ROOT`, because
|
|
10
|
+
# `require_project_root` is what several scripts call before anything has
|
|
11
|
+
# established a root.
|
|
12
|
+
# shellcheck source=/dev/null
|
|
13
|
+
source "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/sandbox-path.sh"
|
|
14
|
+
|
|
9
15
|
GREEN='\033[0;32m'
|
|
10
16
|
RED='\033[0;31m'
|
|
11
17
|
YELLOW='\033[0;33m'
|
|
@@ -46,7 +52,9 @@ guard_root() {
|
|
|
46
52
|
}
|
|
47
53
|
|
|
48
54
|
require_project_root() {
|
|
49
|
-
|
|
55
|
+
local sandbox
|
|
56
|
+
sandbox="$(resolve_sandbox_dir)"
|
|
57
|
+
if [[ "$PWD" == "$sandbox" || "$PWD" == "$sandbox"/* ]]; then
|
|
50
58
|
echo -e "${GREY}┌${NC}" >&2
|
|
51
59
|
log_error "Execution restricted: Command cannot be run from inside the sandbox environment."
|
|
52
60
|
fi
|
|
@@ -8,6 +8,7 @@ export PROJECT_ROOT
|
|
|
8
8
|
|
|
9
9
|
source "$PROJECT_ROOT/scripts/config.sh"
|
|
10
10
|
source "$PROJECT_ROOT/scripts/lib/ui.sh"
|
|
11
|
+
source "$PROJECT_ROOT/scripts/lib/sandbox-path.sh"
|
|
11
12
|
source "$PROJECT_ROOT/scripts/lib/sandbox-git.sh"
|
|
12
13
|
source "$PROJECT_ROOT/scripts/lib/sandbox-fixtures.sh"
|
|
13
14
|
|
|
@@ -135,8 +136,8 @@ validate_environment() {
|
|
|
135
136
|
log_error "Sandbox directory not found at: $SANDBOX_DIR"
|
|
136
137
|
fi
|
|
137
138
|
|
|
138
|
-
if [[ "$PWD" ==
|
|
139
|
-
log_warn "Detected execution inside
|
|
139
|
+
if [[ "$PWD" == "$SANDBOX" || "$PWD" == "$SANDBOX"/* ]]; then
|
|
140
|
+
log_warn "Detected execution inside the sandbox. Switching to project root..."
|
|
140
141
|
cd "$PROJECT_ROOT" || log_error "Failed to switch to project root."
|
|
141
142
|
fi
|
|
142
143
|
}
|
|
@@ -324,7 +325,7 @@ finalize_sandbox_run() {
|
|
|
324
325
|
cmd_clean() {
|
|
325
326
|
log_step "Removing sandbox"
|
|
326
327
|
rm -rf "$SANDBOX"
|
|
327
|
-
log_rem "
|
|
328
|
+
log_rem "$SANDBOX"
|
|
328
329
|
trap - EXIT
|
|
329
330
|
close_timeline
|
|
330
331
|
echo "" >&2
|
|
@@ -422,9 +423,14 @@ main() {
|
|
|
422
423
|
log_error "Context error: you must run this command from inside the toolkit repository."
|
|
423
424
|
fi
|
|
424
425
|
|
|
425
|
-
SANDBOX="$
|
|
426
|
+
SANDBOX="$(resolve_sandbox_dir)"
|
|
426
427
|
SANDBOX_DIR="$PROJECT_ROOT/scripts/sandbox"
|
|
427
428
|
|
|
429
|
+
local unsafe
|
|
430
|
+
if ! unsafe="$(assert_sandbox_dir_safe "$SANDBOX" "$PROJECT_ROOT")"; then
|
|
431
|
+
log_error "$unsafe"
|
|
432
|
+
fi
|
|
433
|
+
|
|
428
434
|
if [[ "$1" == "reset" ]]; then
|
|
429
435
|
reset_sandbox
|
|
430
436
|
exit 0
|
package/src/commands/sandbox.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { existsSync, readFileSync, readdirSync } from 'node:fs'
|
|
2
|
+
import { homedir } from 'node:os'
|
|
2
3
|
import { join } from 'node:path'
|
|
3
4
|
import type { Command } from 'commander'
|
|
4
5
|
import { PROJECT_ROOT, execScript } from '@/exec'
|
|
@@ -28,6 +29,29 @@ import {
|
|
|
28
29
|
|
|
29
30
|
const SANDBOX_DIR = join(PROJECT_ROOT, 'scripts', 'sandbox')
|
|
30
31
|
|
|
32
|
+
/**
|
|
33
|
+
* The provisioned tree, as opposed to `SANDBOX_DIR` above, which holds the
|
|
34
|
+
* scenario scripts. It sits outside the toolkit worktree so the toolkit's own
|
|
35
|
+
* `CLAUDE.md` stays off the ancestor chain of the session `run.sh` spawns with
|
|
36
|
+
* cwd here.
|
|
37
|
+
*
|
|
38
|
+
* Twin of `resolve_sandbox_dir` in `scripts/lib/sandbox-path.sh`. The exec
|
|
39
|
+
* boundary rules out a shared constant, so a change to the default lands on both
|
|
40
|
+
* sides.
|
|
41
|
+
*/
|
|
42
|
+
function sandboxTree(): string {
|
|
43
|
+
const override = process.env.AITK_SANDBOX_DIR
|
|
44
|
+
if (override !== undefined && override !== '') return override
|
|
45
|
+
|
|
46
|
+
const state = process.env.XDG_STATE_HOME
|
|
47
|
+
const base =
|
|
48
|
+
state !== undefined && state !== ''
|
|
49
|
+
? state
|
|
50
|
+
: join(homedir(), '.local', 'state')
|
|
51
|
+
|
|
52
|
+
return join(base, 'aitk', 'sandbox')
|
|
53
|
+
}
|
|
54
|
+
|
|
31
55
|
/**
|
|
32
56
|
* Holds fixture content for scenarios rather than scenarios of its own.
|
|
33
57
|
* Twin of the `-not -name fixtures` filter in `scripts/manage-sandbox.sh`.
|
|
@@ -224,7 +248,7 @@ function runCheck(
|
|
|
224
248
|
// A sandbox that was never provisioned fails every path assertion, reading as a
|
|
225
249
|
// skill that did nothing rather than a caller that ran check too early. The
|
|
226
250
|
// whole point of a verdict is that it means what it says.
|
|
227
|
-
const sandboxDir =
|
|
251
|
+
const sandboxDir = sandboxTree()
|
|
228
252
|
if (!existsSync(sandboxDir)) {
|
|
229
253
|
logError(`No sandbox at ${sandboxDir}. Provision one with aitk sandbox.`)
|
|
230
254
|
outro()
|
package/src/sandbox/expect.ts
CHANGED
|
@@ -141,9 +141,33 @@ function contentArray(value: unknown): ContentAssertion[] {
|
|
|
141
141
|
if (typeof entry !== 'object' || entry === null) continue
|
|
142
142
|
|
|
143
143
|
const record = entry as Record<string, unknown>
|
|
144
|
+
|
|
145
|
+
// Dropping a half-written entry rather than throwing is deliberate, and it
|
|
146
|
+
// reads as the opposite of the stray-key check below. An entry missing its
|
|
147
|
+
// path or pattern declares no assertion to lose, and `checkExpectation`
|
|
148
|
+
// fails an arm whose surviving declaration asserts nothing, so the vacuous
|
|
149
|
+
// pass is already closed one level up. A stray key is the reverse: the entry
|
|
150
|
+
// is well-formed and the declaration around it silently lost a key it
|
|
151
|
+
// appears to carry, which nothing downstream can see.
|
|
144
152
|
if (typeof record.path !== 'string' || record.path === '') continue
|
|
145
153
|
if (typeof record.pattern !== 'string' || record.pattern === '') continue
|
|
146
154
|
|
|
155
|
+
// A bare key written below a `[[content]]` header belongs to that table in
|
|
156
|
+
// TOML, not to the document, so a declaration listing `manual` or
|
|
157
|
+
// `max_turns` after its content blocks parses clean and silently asserts
|
|
158
|
+
// neither. The `claude/ui-test` arm shipped that way: a turn ceiling that
|
|
159
|
+
// never ran and five manual entries that never reached the unchecked count,
|
|
160
|
+
// while `aitk sandbox coverage` counted the arm as armed. Nothing at the
|
|
161
|
+
// top level can see the difference, so the check belongs here.
|
|
162
|
+
const stray = Object.keys(record).filter(
|
|
163
|
+
(key) => key !== 'path' && key !== 'pattern',
|
|
164
|
+
)
|
|
165
|
+
if (stray.length > 0) {
|
|
166
|
+
throw new Error(
|
|
167
|
+
`content entry for ${record.path} carries ${stray.join(', ')}. Move top-level keys above the first [[content]] block.`,
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
|
|
147
171
|
assertions.push({ path: record.path, pattern: record.pattern })
|
|
148
172
|
}
|
|
149
173
|
|
|
@@ -257,6 +281,20 @@ function checkWriteScope(
|
|
|
257
281
|
}
|
|
258
282
|
}
|
|
259
283
|
|
|
284
|
+
// A scope produces one result per write, so a run that wrote nothing produces
|
|
285
|
+
// none, and without this the declaration vanishes from the verdict entirely:
|
|
286
|
+
// no result, no skipped entry, and no contribution to the unchecked count that
|
|
287
|
+
// exists to surface exactly this. The `undefined` branch above cannot stand in,
|
|
288
|
+
// since `run.sh` always passes `--writes` and `readWrites` returns `[]` for an
|
|
289
|
+
// empty file. An arm whose output escaped the snapshot reads as a clean run,
|
|
290
|
+
// which is the vacuous pass the harness exists to remove.
|
|
291
|
+
if (writes.length === 0) {
|
|
292
|
+
return {
|
|
293
|
+
results: [],
|
|
294
|
+
skipped: ['write scope: the run wrote nothing, so no path was checked'],
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
260
298
|
const globs = expectation.writeScope.map((glob) => new Bun.Glob(glob))
|
|
261
299
|
|
|
262
300
|
return {
|