@erclx/aitk 0.54.0 โ 0.56.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/ci-workflow/REQUIREMENT.md +3 -0
- package/claude/skills/ci-workflow/SKILL.md +13 -6
- package/claude/skills/ci-workflow/references/workflows.md +47 -23
- package/claude/skills/claude-autoship/REQUIREMENT.md +4 -2
- package/claude/skills/claude-autoship/SKILL.md +9 -5
- package/claude/skills/migration-standards/REQUIREMENT.md +6 -0
- package/claude/skills/migration-standards/SKILL.md +27 -7
- package/docs/agents/install-and-sync.md +7 -0
- package/docs/agents/output-shape.md +4 -2
- package/docs/ai-workflow.md +2 -1
- package/package.json +1 -1
- package/scripts/core/verify.sh +51 -0
- package/scripts/lib/sandbox-fixtures.sh +21 -0
- package/src/commands/design.ts +2 -1
- package/src/commands/feedback.ts +8 -4
- package/src/commands/slides.ts +50 -28
- package/src/commands/transcripts.ts +1 -1
- package/standards/skill.md +11 -0
- package/standards/standard.md +11 -0
- package/tooling/base/reference.md +82 -100
- package/tooling/claude/seeds/.claude/hooks/index-reminder.sh +8 -1
- package/tooling/claude/seeds/.claude/hooks/memory-index.sh +11 -3
- package/tooling/claude/seeds/.claude/hooks/scratch-guard.sh +8 -1
- package/tooling/claude/seeds/.claude/hooks/standards-audit.sh +9 -1
- package/tooling/claude/seeds/.claude/hooks/tasks-index.sh +11 -3
|
@@ -9,11 +9,14 @@ description: Scope boundary for pipeline structure against job contents, and the
|
|
|
9
9
|
|
|
10
10
|
Without this skill, a session writes a pipeline whose jobs run in sequence because `needs` was used to express the order a person reads them in rather than a data dependency. The pipeline then costs the sum of its jobs where it could have cost the longest one, and the waste compounds on every push.
|
|
11
11
|
|
|
12
|
+
The same session splits a gate that finishes in a minute into three parallel jobs, which fails in the other direction. Each job repays checkout, dependency install, and toolchain setup before it reaches a stage, so the gate gets slower while reporting no earlier, and the roster reads as a design decision rather than as a cost nobody measured.
|
|
13
|
+
|
|
12
14
|
The rest are reproducibility failures that surface as flakes. An action pinned to a moving ref changes under the project, so a run that passed yesterday fails today with no commit behind it and the diff explains nothing. A cache keyed on a static string serves a stale browser or toolchain after a version bump, and the failure reads as a broken test rather than a stale cache. Artifacts upload on every run and never expire, so storage grows with the commit count while the ones worth reading are the failures. And a workflow with no manual trigger can only be reproduced by pushing a commit, which is the wrong instrument for a run that failed for an environmental reason.
|
|
13
15
|
|
|
14
16
|
## Must
|
|
15
17
|
|
|
16
18
|
- Gate a job only on a data dependency or on a cost that justifies the wait, and leave every other job parallel
|
|
19
|
+
- Start the checks that share setup in one job and split them only once a run log puts the gate past roughly two minutes
|
|
17
20
|
- Pin every action to a tag that cannot cross a major version
|
|
18
21
|
- Key a cache on the version string of the thing it caches
|
|
19
22
|
- Give every workflow a manual trigger beside its primary one
|
|
@@ -20,14 +20,21 @@ Generate GitHub Actions workflow files for CI pipelines. Enforce parallel job ex
|
|
|
20
20
|
|
|
21
21
|
## Job naming
|
|
22
22
|
|
|
23
|
-
- Name jobs with emoji + title: `๐ก๏ธ Static Checks`, `๐งช Unit Tests`, `๐ฆ Build Check`, `๐ญ E2E Tests`, `๐ Deploy`, `๐ Code Quality`, `๐ท๏ธ Release`, `๐ Security`.
|
|
23
|
+
- Name jobs with emoji + title: `๐ก๏ธ Checks`, `๐ก๏ธ Static Checks`, `๐งช Unit Tests`, `๐ฆ Build Check`, `๐ญ E2E Tests`, `๐ Deploy`, `๐ Code Quality`, `๐ท๏ธ Release`, `๐ Security`.
|
|
24
|
+
|
|
25
|
+
## Job granularity
|
|
26
|
+
|
|
27
|
+
- Start a new pipeline with the checks that share setup in one job: static analysis, unit tests, and build. A project with no run history has nothing to measure, and one job is the shape that costs least to reverse.
|
|
28
|
+
- Keep them in one job while the gate runs under roughly two minutes end to end. Each extra job repays checkout, dependency install, and toolchain setup before it reaches a stage, so a split at that duration costs more than the parallelism returns.
|
|
29
|
+
- Split once a run log puts the gate past that, then apply the dependency rules below. The run log is the revision trigger rather than the entry condition, since repository size and stage count answer nothing.
|
|
30
|
+
- Give E2E, release, and deploy their own jobs from the start. Each carries a data dependency or a gate, so this rule never folds them into the check job.
|
|
24
31
|
|
|
25
32
|
## Job dependencies
|
|
26
33
|
|
|
27
34
|
- Run independent jobs in parallel.
|
|
28
35
|
- Use `needs` only when there is a data dependency (a job requires an artifact) or the job is prohibitively expensive relative to its gate.
|
|
29
|
-
- Run static, unit, and build jobs in parallel.
|
|
30
|
-
- Gate E2E on build, since it
|
|
36
|
+
- Run static, unit, and build jobs in parallel once the gate is split.
|
|
37
|
+
- Gate E2E on whichever job uploads the build artifact, since it consumes that artifact. That job is the folded check job in a new pipeline and the separate build job once the gate is split.
|
|
31
38
|
- Gate release and deploy on E2E.
|
|
32
39
|
|
|
33
40
|
## Artifacts
|
|
@@ -42,7 +49,7 @@ Generate GitHub Actions workflow files for CI pipelines. Enforce parallel job ex
|
|
|
42
49
|
|
|
43
50
|
## Template
|
|
44
51
|
|
|
45
|
-
Load `${CLAUDE_SKILL_DIR}/references/workflows.md` for the base workflow template. Adapt it to the project's stack, test commands, and build output. Add or remove jobs as needed
|
|
52
|
+
Load `${CLAUDE_SKILL_DIR}/references/workflows.md` for the base workflow template. Adapt it to the project's stack, test commands, and build output. Add or remove jobs as needed. The template already carries the folded shape, and the split beside it applies once the granularity rule above calls for it.
|
|
46
53
|
|
|
47
54
|
## Validation
|
|
48
55
|
|
|
@@ -50,8 +57,8 @@ Before responding, verify:
|
|
|
50
57
|
|
|
51
58
|
- `workflow_dispatch` is present alongside the primary trigger.
|
|
52
59
|
- All actions pinned to major version tags, no `@latest` or `@main`.
|
|
53
|
-
-
|
|
54
|
-
-
|
|
60
|
+
- A new pipeline folds static, unit, and build into one job, and a gate a run log put past two minutes gives them no `needs` so they run in parallel.
|
|
61
|
+
- Every name in a `needs` matches a job declared in the same file. E2E names the artifact producer, release and deploy name E2E.
|
|
55
62
|
- Artifacts upload on `if: failure()` only with `retention-days: 7`.
|
|
56
63
|
- Job names use emoji + title format.
|
|
57
64
|
- Deploy, publish, and release jobs carry a placeholder step and a named handoff, never a guessed deploy command.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# CI workflow template
|
|
2
2
|
|
|
3
|
-
Copy this base template and adapt it to the project's stack, test commands, and build output.
|
|
3
|
+
Copy this base template and adapt it to the project's stack, test commands, and build output. It carries the shape a new pipeline starts in, with the checks that share setup in one job, per the granularity rule in the skill body. `e2e` and anything gated on it stay separate jobs at either size.
|
|
4
4
|
|
|
5
5
|
```yaml
|
|
6
6
|
name: CI
|
|
@@ -13,8 +13,8 @@ on:
|
|
|
13
13
|
workflow_dispatch:
|
|
14
14
|
|
|
15
15
|
jobs:
|
|
16
|
-
|
|
17
|
-
name: '๐ก๏ธ
|
|
16
|
+
checks:
|
|
17
|
+
name: '๐ก๏ธ Checks'
|
|
18
18
|
runs-on: ubuntu-latest
|
|
19
19
|
steps:
|
|
20
20
|
- uses: actions/checkout@v4
|
|
@@ -23,9 +23,16 @@ jobs:
|
|
|
23
23
|
bun-version: latest
|
|
24
24
|
- run: bun install --frozen-lockfile
|
|
25
25
|
- run: bun run check
|
|
26
|
+
- run: bun run test
|
|
27
|
+
- run: bun run build
|
|
28
|
+
- uses: actions/upload-artifact@v4
|
|
29
|
+
with:
|
|
30
|
+
name: build-output
|
|
31
|
+
path: dist/
|
|
26
32
|
|
|
27
|
-
|
|
28
|
-
name: '
|
|
33
|
+
e2e:
|
|
34
|
+
name: '๐ญ E2E Tests'
|
|
35
|
+
needs: checks
|
|
29
36
|
runs-on: ubuntu-latest
|
|
30
37
|
steps:
|
|
31
38
|
- uses: actions/checkout@v4
|
|
@@ -33,10 +40,27 @@ jobs:
|
|
|
33
40
|
with:
|
|
34
41
|
bun-version: latest
|
|
35
42
|
- run: bun install --frozen-lockfile
|
|
36
|
-
-
|
|
43
|
+
- uses: actions/download-artifact@v4
|
|
44
|
+
with:
|
|
45
|
+
name: build-output
|
|
46
|
+
path: dist/
|
|
47
|
+
- run: bun run test:e2e
|
|
48
|
+
- uses: actions/upload-artifact@v4
|
|
49
|
+
if: failure()
|
|
50
|
+
with:
|
|
51
|
+
name: e2e-results
|
|
52
|
+
path: test-results/
|
|
53
|
+
retention-days: 7
|
|
54
|
+
```
|
|
37
55
|
|
|
38
|
-
|
|
39
|
-
|
|
56
|
+
## Splitting the check job
|
|
57
|
+
|
|
58
|
+
Apply this once a run log puts the gate past roughly two minutes, never before. Each job below repays checkout, dependency install, and toolchain setup before it reaches a stage, which is the cost the duration has to cover. Replace the `checks` job above with these three and point `e2e` at `needs: build`.
|
|
59
|
+
|
|
60
|
+
```yaml
|
|
61
|
+
jobs:
|
|
62
|
+
static:
|
|
63
|
+
name: '๐ก๏ธ Static Checks'
|
|
40
64
|
runs-on: ubuntu-latest
|
|
41
65
|
steps:
|
|
42
66
|
- uses: actions/checkout@v4
|
|
@@ -44,15 +68,21 @@ jobs:
|
|
|
44
68
|
with:
|
|
45
69
|
bun-version: latest
|
|
46
70
|
- run: bun install --frozen-lockfile
|
|
47
|
-
- run: bun run
|
|
48
|
-
|
|
71
|
+
- run: bun run check
|
|
72
|
+
|
|
73
|
+
unit:
|
|
74
|
+
name: '๐งช Unit Tests'
|
|
75
|
+
runs-on: ubuntu-latest
|
|
76
|
+
steps:
|
|
77
|
+
- uses: actions/checkout@v4
|
|
78
|
+
- uses: oven-sh/setup-bun@v2
|
|
49
79
|
with:
|
|
50
|
-
|
|
51
|
-
|
|
80
|
+
bun-version: latest
|
|
81
|
+
- run: bun install --frozen-lockfile
|
|
82
|
+
- run: bun run test
|
|
52
83
|
|
|
53
|
-
|
|
54
|
-
name: '
|
|
55
|
-
needs: build
|
|
84
|
+
build:
|
|
85
|
+
name: '๐ฆ Build Check'
|
|
56
86
|
runs-on: ubuntu-latest
|
|
57
87
|
steps:
|
|
58
88
|
- uses: actions/checkout@v4
|
|
@@ -60,15 +90,9 @@ jobs:
|
|
|
60
90
|
with:
|
|
61
91
|
bun-version: latest
|
|
62
92
|
- run: bun install --frozen-lockfile
|
|
63
|
-
-
|
|
93
|
+
- run: bun run build
|
|
94
|
+
- uses: actions/upload-artifact@v4
|
|
64
95
|
with:
|
|
65
96
|
name: build-output
|
|
66
97
|
path: dist/
|
|
67
|
-
- run: bun run test:e2e
|
|
68
|
-
- uses: actions/upload-artifact@v4
|
|
69
|
-
if: failure()
|
|
70
|
-
with:
|
|
71
|
-
name: e2e-results
|
|
72
|
-
path: test-results/
|
|
73
|
-
retention-days: 7
|
|
74
98
|
```
|
|
@@ -16,7 +16,7 @@ Review is the step that varies most. It gets skipped on a diff that needed one,
|
|
|
16
16
|
- Take the approved plan for the branch as the scope, and implement only what it describes
|
|
17
17
|
- Give every step a stop condition, and leave the code on the branch and the receipts on disk at each one
|
|
18
18
|
- Classify the changed-file list by path as well as by extension, so informational prose skips a code review with no signal on it and executable prose still reaches one
|
|
19
|
-
-
|
|
19
|
+
- Split findings by origin, stopping on a critical or should-fix one the branch inherited and repairing one this run caused
|
|
20
20
|
- Open the pull request as a draft, then watch continuous integration to a terminal state
|
|
21
21
|
- Name the recovery for the stop it took, since the value of stopping is that the user knows where to resume
|
|
22
22
|
|
|
@@ -24,7 +24,9 @@ Review is the step that varies most. It gets skipped on a diff that needed one,
|
|
|
24
24
|
|
|
25
25
|
- Expand past the plan, refactor a neighbor, or touch a file outside it without reason
|
|
26
26
|
- Loop on a failed verify. One fix attempt against the reported errors, then stop.
|
|
27
|
-
- Fix
|
|
27
|
+
- Fix an inherited review finding or a failing check. Both stops are deliberate, since a green pull request reached by auto-fix hides what broke.
|
|
28
|
+
- Loop on a self-introduced finding. One repair pass, then stop, which is the bound a failed verify already carries.
|
|
29
|
+
- Read the plan's file list as the boundary on a repair. It scopes what the run builds, and a finding this run caused is in reach wherever it landed.
|
|
28
30
|
- Run the memory Apply phase. Promoting an entry changes how the agent operates and ships as its own change.
|
|
29
31
|
- Read an empty changed-file list as prose-only. It satisfies that test vacuously and would route the branch past review instead of through it.
|
|
30
32
|
- Read a markdown extension as evidence the change only informs. A skill body, a governance rule, and a standard are behavior written in prose.
|
|
@@ -90,12 +90,15 @@ The list covers this toolkit's authoring layout and the layout it installs, whic
|
|
|
90
90
|
|
|
91
91
|
## Step 6: evaluate findings
|
|
92
92
|
|
|
93
|
-
Skip this step when Step 5 skipped review. Otherwise read `.claude/review/review-<slug>.md` at the main worktree root.
|
|
93
|
+
Skip this step when Step 5 skipped review. Otherwise read `.claude/review/review-<slug>.md` at the main worktree root. Split every finding by origin before parsing the summary line (`X critical, Y should-fix, Z minor`), since the stop exists for a defect the branch inherited rather than for one this run introduced.
|
|
94
94
|
|
|
95
|
-
-
|
|
96
|
-
-
|
|
95
|
+
- **This run caused it, at any severity.** Fix it, re-run the Step 3 verify commands, re-read the fixed file against what the finding claimed, and continue. Do not report it as a stop and do not offer the fix as a choice, which is the same stop wearing a proposal.
|
|
96
|
+
- **It predates this run, critical or should-fix.** Stop: `โ Review found non-minor issues that predate this run. See .claude/review/review-<slug>.md. Fix and run /git-ship.`
|
|
97
|
+
- **It predates this run, minor only.** Continue. The minor findings stay in the on-disk review receipt. Fold any a reviewer needs into the PR's `## Technical Context`. Do not add a separate review-notes section to the PR body.
|
|
97
98
|
|
|
98
|
-
|
|
99
|
+
Read origin as causation rather than authorship. Staleness this run induced in a file it never opened is a finding it caused, and the plan's "Files to touch" list scopes what the run builds rather than what it may repair.
|
|
100
|
+
|
|
101
|
+
Bound the repair at one pass, the way Step 3 bounds verify. When that re-read shows the finding still standing, stop: `โ A self-introduced finding survived one fix pass. See .claude/review/review-<slug>.md. Fix and run /git-ship.`
|
|
99
102
|
|
|
100
103
|
## Step 7: ship
|
|
101
104
|
|
|
@@ -151,5 +154,6 @@ Every stop point leaves recoverable state. The user resumes manually from the ap
|
|
|
151
154
|
| Branch collision on worktree entry | `claude-worktree` Step 5 found `<slug>` already as a local branch. Resolve manually (rename or delete the stale branch), then re-run autoship. |
|
|
152
155
|
| Verify fails | Read logs, fix manually, run `/git-ship` |
|
|
153
156
|
| UI checklist | Verify visually, run `/git-ship` |
|
|
154
|
-
|
|
|
157
|
+
| Inherited review findings | Fix findings, run `/git-ship` |
|
|
158
|
+
| Self-introduced finding survived | Read the receipt for what the one repair pass left open, fix it, run `/git-ship` |
|
|
155
159
|
| git-ship fails | Inspect hook or remote error, run again |
|
|
@@ -9,10 +9,16 @@ description: Why the root-to-.claude relocation ships as git mv commands the use
|
|
|
9
9
|
|
|
10
10
|
Without this skill, a project whose rules cite `.claude/standards/` while the files sit at the root gets fixed by hand, and the hand fix loses what version control was holding. A plain `mv` breaks the rename chain, so every relocated standard reads as a delete beside an unrelated add and its history stops at the move.
|
|
11
11
|
|
|
12
|
+
A fourth failure comes before those three, from deciding what is unmigrated by listing the folder. A root `standards/` can hold the project's own docs and nothing the toolkit ever installed, and a listing cannot tell the two apart, so the skill proposes moving project files under `.claude/` where a sync walks them. The drift report already answers this, counting only files whose basename the toolkit ships, and it is the same report `toolkit-operator` reads to route here.
|
|
13
|
+
|
|
14
|
+
Taking detection from a command opens a failure the listing never had. A CLI predating the field exits zero with a well-formed report that never mentions it, so a skill treating an absent key as an empty answer tells a project whose every domain sits at the root that it has nothing to relocate. That is the population this skill exists for, and a silent false negative there costs more than the unfiltered count reading the folder would have produced.
|
|
15
|
+
|
|
12
16
|
Three failures follow from acting without looking first. A move onto an existing `.claude/standards/` copy overwrites the files already installed there, which is the case the relocation was supposed to be unnecessary for. A `git mv` on a dirty tree lands the relocation in the same commit as unrelated work, so neither can be reviewed or reverted alone. And a session that rewrites every inbound reference spends its effort on toolkit-owned rules and skills, which the next sync overwrites, while the author-owned lines that actually break go unmentioned.
|
|
13
17
|
|
|
14
18
|
## Must
|
|
15
19
|
|
|
20
|
+
- Take the set of domains to relocate from `aitk sync --check --json`, and report its filtered count rather than a folder listing
|
|
21
|
+
- Keep the listing as the fallback for a target where `aitk` is absent, the command fails, or the report carries no `unmigrated` key, and say that its counts are unfiltered
|
|
16
22
|
- Detect an existing copy under `.claude/` and skip that folder's move rather than merging into it
|
|
17
23
|
- Read the working tree state and require it clean before the moves, since the relocation has to be revertible on its own
|
|
18
24
|
- Propose `git mv` so history follows each file
|
|
@@ -10,21 +10,38 @@ description: Proposes `git mv` commands to relocate a target project's root `sta
|
|
|
10
10
|
- If neither `standards/` nor `snippets/` exists at `pwd`, stop: `โ No root standards/ or snippets/ to relocate.`
|
|
11
11
|
- If `pwd` is not a git work tree, stop: `โ Not a git repository. git mv needs version control.`
|
|
12
12
|
|
|
13
|
-
## Step 1:
|
|
13
|
+
## Step 1: read the report
|
|
14
14
|
|
|
15
|
-
Run
|
|
15
|
+
Run `aitk sync --check . --json` from the project root. Its `unmigrated` array is the detection. Each entry carries `domain`, `rootPath`, `installPath`, and `files`, and a domain appears only when the root folder holds a file the toolkit ships and nothing sits at the install path. Report `files` as the count, since it excludes files the project wrote into the same folder.
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
- `ls .claude/standards/ 2>/dev/null`:
|
|
20
|
-
- `ls .claude/snippets/ 2>/dev/null`:
|
|
17
|
+
Run these beside it, in parallel. The report answers which domains to move and neither of these is derivable from it:
|
|
18
|
+
|
|
19
|
+
- `ls .claude/standards/ 2>/dev/null`: separate a domain already relocated from one never installed, which the report cannot, since both are absent from `unmigrated`
|
|
20
|
+
- `ls .claude/snippets/ 2>/dev/null`: the same, for the other domain
|
|
21
21
|
- `git status --short 2>/dev/null`: confirm a clean tree before proposing moves
|
|
22
22
|
|
|
23
|
+
Keep the two listings as separate commands. `ls` labels its output with a `dir:` header only when more than one operand succeeds, so a combined call with one directory present prints that directory's filenames bare while the redirect swallows the other's failure, and the result reads as whichever directory was expected.
|
|
24
|
+
|
|
25
|
+
### When the report is unavailable
|
|
26
|
+
|
|
27
|
+
Fall back to `ls standards/*.md` and `ls snippets/` on any of three conditions. Say in the output that the counts are unfiltered, because a root folder can hold project-authored files the report would have excluded and the fallback counts every one of them.
|
|
28
|
+
|
|
29
|
+
- `aitk` is not on `PATH`
|
|
30
|
+
- The command exits non-zero
|
|
31
|
+
- The report parses and carries no `unmigrated` key at all
|
|
32
|
+
|
|
33
|
+
The third is the one that decides whether this skill works for the projects it exists for. `unmigrated` reached a release in `0.46.0`, and a CLI older than that exits zero with a well-formed report that never mentions the field. Reading an absent key as an empty list sends the run down the nothing-to-relocate branch and reports a clean layout to a project whose every domain sits at the root, which is a silent false negative where the unfiltered count is a visible imprecision.
|
|
34
|
+
|
|
35
|
+
An absent key and an empty array are different states, so test for the key rather than for emptiness. A current CLI reporting `"unmigrated": []` has looked and found nothing, and falling back there would trade a correct answer for a listing that proposes moving whatever the folder happens to hold.
|
|
36
|
+
|
|
37
|
+
Do not fall back on `historyUnavailable`. That field reports failed attribution on a domain or on `seeds`, and `unmigrated` is a filesystem read carrying no attribution of its own, so a report that cannot date a file still detects the layout correctly.
|
|
38
|
+
|
|
23
39
|
## Step 2: check conflicts
|
|
24
40
|
|
|
25
41
|
- If `.claude/standards/` already holds `.md` files, mark standards as "already relocated" and skip its move.
|
|
26
42
|
- If `.claude/snippets/` already holds `.md` files, mark snippets as "already relocated" and skip its move.
|
|
27
43
|
- If `git status` is not clean, add a TODO line telling the user to commit or stash first. `git mv` on a dirty tree mixes the move with unrelated changes.
|
|
44
|
+
- If a root folder holds files and no domain names it in a report that carried the key, propose nothing for it. The content is the project's own, and moving it under `.claude/` puts project files where a sync walks. This reads a present key alone. A report with no `unmigrated` key never reaches here, since Step 1 sends it to the fallback.
|
|
28
45
|
|
|
29
46
|
## Step 3: find author-owned inbound references
|
|
30
47
|
|
|
@@ -70,6 +87,9 @@ aitk gov sync .
|
|
|
70
87
|
Re-syncing reinstalls toolkit-owned rules and standards at the new path. Author-owned references above need a manual one-line fix.
|
|
71
88
|
```
|
|
72
89
|
|
|
73
|
-
|
|
90
|
+
Two states produce no proposal at all, and each gets its own line rather than an empty block:
|
|
91
|
+
|
|
92
|
+
- Both folders already live under `.claude/`: `โ
standards/ and snippets/ already live under .claude/. Nothing to relocate.`
|
|
93
|
+
- A root folder exists and no domain names it in `unmigrated`: `โ
<folder>/ holds no file the toolkit installed. Nothing to relocate.` Name every such folder. The guard passed because the folder is there, so a session that printed nothing would leave the user reading silence as a failed run rather than as the answer.
|
|
74
94
|
|
|
75
95
|
Do not run the `git mv` commands. Do not edit `CLAUDE.md`, rule files, or docs. The user runs the commands and fixes author-owned references after reviewing.
|
|
@@ -164,6 +164,13 @@ to, with nothing at the path the current one reads. It carries `rootPath`,
|
|
|
164
164
|
root reports zero entries for that domain and reads as clean, which is the most
|
|
165
165
|
misleading state the report can produce. Route it to `migration-standards`.
|
|
166
166
|
|
|
167
|
+
That skill reads this field rather than listing the folder itself, so the entry
|
|
168
|
+
is the detection on both sides of the handoff and the two cannot disagree. The
|
|
169
|
+
count is what makes the difference visible: it counts only files whose basename
|
|
170
|
+
the toolkit ships, so a root folder holding the project's own documents beside
|
|
171
|
+
the installed ones reports the installed subset, where a listing reports every
|
|
172
|
+
file and proposes relocating the lot.
|
|
173
|
+
|
|
167
174
|
`unmigrated` counts toward `--exit-code`, since running the relocation closes it.
|
|
168
175
|
`superseded` and every seed state are excluded, for the reason `orphaned` already
|
|
169
176
|
is: only the user can move content they wrote, so failing a job on it leaves the
|
|
@@ -46,8 +46,10 @@ Help skips the banner. The `Usage:` line sits directly on `โ`. Help writes to
|
|
|
46
46
|
|
|
47
47
|
## Process exit
|
|
48
48
|
|
|
49
|
-
A command action
|
|
49
|
+
A command action sets `process.exitCode` and returns. Calling `process.exit()` there ends the process before a stdout write drains, which truncates piped output at the 64K pipe buffer while still reporting the right exit code. Redirecting to a file hides the truncation, so it surfaces only through a pipe, which is what a check has to use to catch it.
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
The rule reaches an error path that writes to stderr alone, where the truncation has nothing to cut. Scoping it to the stdout writers was the alternative and it makes the floor depend on a detail that moves, since an action grows a stdout write long after its error branches are written. An action always has a return path, so the requirement costs it a line.
|
|
52
|
+
|
|
53
|
+
The exit belongs to a helper that has no caller to unwind through. A prompt inside a promise executor and a wrapper propagating a child process status both qualify, and neither can hand a value back to a caller expecting one. A validation helper called from an action does not: it throws from a `never` return, which keeps its caller exhaustive to the compiler while the action catches and owns the code.
|
|
52
54
|
|
|
53
55
|
Diagnostics reach stderr in every mode, including `--json`. Name the file and the field that failed, because a JSON record carries an action and a reason and an operator reading stderr alone sees neither.
|
package/docs/ai-workflow.md
CHANGED
|
@@ -102,7 +102,8 @@ It announces and moves nothing, so `aitk:claude-tasks` stays the only writer. A
|
|
|
102
102
|
For features on a mature stack, chain the post-plan pipeline in one session. Approve the plan, invoke `aitk:claude-autoship`, and the skill runs implement โ verify โ review โ ship sequentially.
|
|
103
103
|
|
|
104
104
|
- Use when the plan is tight and the stack has real verify commands and test coverage
|
|
105
|
-
- Autoship stops on: verify failure after one fix attempt, UI manual checklist non-empty,
|
|
105
|
+
- Autoship stops on: verify failure after one fix attempt, UI manual checklist non-empty, an inherited review finding above minor, no diff baseline resolving against `main`, an empty changed-file list, or hook failure
|
|
106
|
+
- Review findings split by origin before severity is read. One the branch inherited stops the chain, and one the run itself caused is repaired in place at any severity, bounded at a single pass. Origin is causation rather than authorship, so staleness the run induced in a file it never opened counts as its own and the plan's file list bounds what it builds rather than what it may repair.
|
|
106
107
|
- Review is skipped when the diff is prose that only informs: every changed file matches `*.md` or `*.txt`, and none sits under a behavior path. Behavior paths cover skills, rules, standards, snippets, and `tooling/` in both the authoring and the installed spelling, plus root `CLAUDE.md`, so the list matches whether a repository authors those surfaces or consumed them from the toolkit. Markdown under one states what an agent does, so a branch touching it reaches review while `docs/` and `wiki/` still skip and stay gated by `docs-sync`, `claude-standards-audit`, and pre-push hooks.
|
|
107
108
|
- An empty changed-file list stops the chain rather than counting as prose-only. The filename test passes vacuously on an empty set, which routed a branch past review instead of through it.
|
|
108
109
|
- Every stop leaves recoverable state. Fix and resume with `/git-ship`
|
package/package.json
CHANGED
package/scripts/core/verify.sh
CHANGED
|
@@ -13,6 +13,11 @@ WRITE="${VERIFY_WRITE:-true}"
|
|
|
13
13
|
SCOPED=true
|
|
14
14
|
CHANGED_FILES=""
|
|
15
15
|
|
|
16
|
+
# Scenarios declaring no expectation, taken from `aitk sandbox coverage` against a
|
|
17
|
+
# clean tree. Raising it is a deliberate edit that says which scenario shipped
|
|
18
|
+
# unarmed and why.
|
|
19
|
+
SANDBOX_UNDECLARED_CEILING=47
|
|
20
|
+
|
|
16
21
|
check_dependencies() {
|
|
17
22
|
command -v bun >/dev/null 2>&1 || log_error "bun is not installed"
|
|
18
23
|
}
|
|
@@ -138,6 +143,13 @@ seed_entry_count() {
|
|
|
138
143
|
awk '{ total += $1 } END { print total + 0 }'
|
|
139
144
|
}
|
|
140
145
|
|
|
146
|
+
# One numeric summary key out of a coverage report. The per-scenario objects carry
|
|
147
|
+
# neither key this is called with, so the match reaches the top level alone and
|
|
148
|
+
# the caller does not depend on the order the keys are emitted in.
|
|
149
|
+
sandbox_summary_field() {
|
|
150
|
+
printf '%s' "$2" | grep -o "\"$1\":[0-9]\+" | grep -o '[0-9]\+'
|
|
151
|
+
}
|
|
152
|
+
|
|
141
153
|
assert_no_drift() {
|
|
142
154
|
local paths=$1
|
|
143
155
|
local err_msg=$2
|
|
@@ -274,8 +286,47 @@ main() {
|
|
|
274
286
|
run_check "cd $PROJECT_ROOT && bun src/cli.ts claude skills audit --requirements-only" "A skill folder carries no REQUIREMENT.md. Run bun src/cli.ts claude skills audit."
|
|
275
287
|
log_info "Skill requirements present"
|
|
276
288
|
|
|
289
|
+
# `aitk sandbox coverage` moves only when a person runs it, so a scenario added
|
|
290
|
+
# with no expectation ships unnoticed. The gate is an absolute count of
|
|
291
|
+
# undeclared scenarios rather than a ratio or a floor under the declared count.
|
|
292
|
+
# A floor under the declared count passes the case this exists to catch, since
|
|
293
|
+
# adding an unarmed scenario leaves that number where it was. A ratio moves
|
|
294
|
+
# when a scenario is legitimately deleted, and this ceiling does not: deleting
|
|
295
|
+
# an unarmed scenario lowers it and deleting an armed one leaves it alone.
|
|
296
|
+
log_step "Sandbox coverage"
|
|
297
|
+
local coverage_output coverage_status=0 total armed undeclared
|
|
298
|
+
coverage_output=$(cd "$PROJECT_ROOT" && bun src/cli.ts sandbox coverage --json 2>/dev/null) || coverage_status=$?
|
|
299
|
+
if [ "$coverage_status" -ne 0 ]; then
|
|
300
|
+
if [ "${CI:-false}" = true ]; then
|
|
301
|
+
log_error "bun src/cli.ts sandbox coverage --json exited $coverage_status. The scenario tree ships in the checkout, so a run that does not report is a broken command rather than an absent tree, and skipping would report the pass this stage exists to withhold."
|
|
302
|
+
fi
|
|
303
|
+
log_warn "Skipped, the scenario tree did not report"
|
|
304
|
+
else
|
|
305
|
+
# `|| x=""` on both, because a grep that matches nothing exits non-zero and
|
|
306
|
+
# errexit would take the script down at the assignment, before the guard
|
|
307
|
+
# below could name what went missing.
|
|
308
|
+
total=$(sandbox_summary_field totalScenarios "$coverage_output") || total=""
|
|
309
|
+
armed=$(sandbox_summary_field armedScenarios "$coverage_output") || armed=""
|
|
310
|
+
if [ -z "$total" ] || [ -z "$armed" ]; then
|
|
311
|
+
log_error "The coverage report carried no scenario totals, so the stage measured nothing. Run bun src/cli.ts sandbox coverage --json."
|
|
312
|
+
fi
|
|
313
|
+
undeclared=$((total - armed))
|
|
314
|
+
if [ "$undeclared" -gt "$SANDBOX_UNDECLARED_CEILING" ]; then
|
|
315
|
+
log_error "$undeclared of $total scenarios declare no expectation, over the ceiling of $SANDBOX_UNDECLARED_CEILING. Declare expectations on the new scenario, or raise SANDBOX_UNDECLARED_CEILING in this script and say which scenario shipped unarmed."
|
|
316
|
+
fi
|
|
317
|
+
log_info "$armed of $total scenarios declare expectations, $undeclared undeclared against a ceiling of $SANDBOX_UNDECLARED_CEILING"
|
|
318
|
+
fi
|
|
319
|
+
|
|
320
|
+
# The plugin is the second delivery path and this is the only stage gating it,
|
|
321
|
+
# so the skip below is for a contributor's machine rather than for the merge
|
|
322
|
+
# gate. A runner installs the CLI as a workflow step, which makes an absent
|
|
323
|
+
# binary there a broken workflow, and skipping would report a pass for every
|
|
324
|
+
# manifest on the way to a marketplace install.
|
|
277
325
|
log_step "Plugin manifests"
|
|
278
326
|
if ! command -v claude >/dev/null 2>&1; then
|
|
327
|
+
if [ "${CI:-false}" = true ]; then
|
|
328
|
+
log_error "claude is not installed. CI installs it before this stage, so read the Install Plugin CLI step in .github/workflows/verify.yml."
|
|
329
|
+
fi
|
|
279
330
|
log_info "Skipped, claude is not installed"
|
|
280
331
|
else
|
|
281
332
|
local manifests manifest
|
|
@@ -78,6 +78,27 @@ append_from_fixtures() {
|
|
|
78
78
|
done < <(list_fixture_files "$source_dir")
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
# Stages the first `count` markdown files the toolkit ships for a domain into
|
|
82
|
+
# `dest`, flattening any source subfolder. Source rather than fixture tree,
|
|
83
|
+
# because an arm modelling a real install wants the files a target actually
|
|
84
|
+
# received and a copy under `fixtures/` would drift from them silently.
|
|
85
|
+
#
|
|
86
|
+
# The flattening is load-bearing rather than incidental. Both `detectUnmigrated`
|
|
87
|
+
# and the sync engine match a target file to its source by basename against the
|
|
88
|
+
# flat domain root, so a file lifted out of a source subfolder such as `bundled/`
|
|
89
|
+
# has no flat sibling and reads as project-authored. An arm staging one claims a
|
|
90
|
+
# drift or an unmigrated domain it did not stage.
|
|
91
|
+
stage_toolkit_markdown() {
|
|
92
|
+
local src="$1"
|
|
93
|
+
local dest="$2"
|
|
94
|
+
local count="$3"
|
|
95
|
+
|
|
96
|
+
mkdir -p "$dest"
|
|
97
|
+
while IFS= read -r file; do
|
|
98
|
+
cp "$file" "$dest/$(basename "$file")"
|
|
99
|
+
done < <(find "$src" -maxdepth 1 -type f -name "*.md" ! -name "index.md" | sort | head -n "$count")
|
|
100
|
+
}
|
|
101
|
+
|
|
81
102
|
# Stages one step of a scenario arm into the sandbox working directory.
|
|
82
103
|
# Scenarios call this once per step so their own git operations stay between
|
|
83
104
|
# the steps, where they are visible.
|
package/src/commands/design.ts
CHANGED
|
@@ -26,7 +26,8 @@ export function register(program: Command): void {
|
|
|
26
26
|
process.stderr.write(
|
|
27
27
|
`${GREY}โ${NC}\n${GREY}โ${NC} ${RED}โ${NC} ${opts.source} not found\n${GREY}โ${NC}\n`,
|
|
28
28
|
)
|
|
29
|
-
process.
|
|
29
|
+
process.exitCode = 1
|
|
30
|
+
return
|
|
30
31
|
}
|
|
31
32
|
process.stderr.write(
|
|
32
33
|
`${GREY}โ${NC}\n${GREY}โ${NC} ${WHITE}Render design tokens${NC}\n`,
|
package/src/commands/feedback.ts
CHANGED
|
@@ -60,12 +60,14 @@ export function register(program: Command): void {
|
|
|
60
60
|
frameError(
|
|
61
61
|
'No feedback on stdin. Pipe a markdown block: pbpaste | aitk feedback',
|
|
62
62
|
)
|
|
63
|
-
process.
|
|
63
|
+
process.exitCode = 1
|
|
64
|
+
return
|
|
64
65
|
}
|
|
65
66
|
const body = (await readStdin()).trim()
|
|
66
67
|
if (!body) {
|
|
67
68
|
frameError('Empty feedback body. Provide a markdown block on stdin.')
|
|
68
|
-
process.
|
|
69
|
+
process.exitCode = 1
|
|
70
|
+
return
|
|
69
71
|
}
|
|
70
72
|
|
|
71
73
|
if (opts.github) {
|
|
@@ -83,7 +85,8 @@ export function register(program: Command): void {
|
|
|
83
85
|
frameError(
|
|
84
86
|
'gh unavailable and no toolkit source to fall back to. Install gh, or file it at https://github.com/erclx/aitk/issues/new',
|
|
85
87
|
)
|
|
86
|
-
process.
|
|
88
|
+
process.exitCode = 1
|
|
89
|
+
return
|
|
87
90
|
}
|
|
88
91
|
process.stderr.write(
|
|
89
92
|
`${YELLOW}! gh unavailable, wrote local scratch instead${NC}\n`,
|
|
@@ -94,7 +97,8 @@ export function register(program: Command): void {
|
|
|
94
97
|
frameError(
|
|
95
98
|
'Local scratch needs the toolkit source. Re-run with --github to open an issue instead.',
|
|
96
99
|
)
|
|
97
|
-
process.
|
|
100
|
+
process.exitCode = 1
|
|
101
|
+
return
|
|
98
102
|
}
|
|
99
103
|
|
|
100
104
|
const filePath = writeLocal(body)
|
package/src/commands/slides.ts
CHANGED
|
@@ -36,35 +36,39 @@ export function register(program: Command): void {
|
|
|
36
36
|
mirror?: string
|
|
37
37
|
open?: boolean
|
|
38
38
|
}) => {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
`${GREY}โ${NC} ${GREEN}โ${NC} ${result.slideCount} slides\n${GREY}โ${NC} ${GREEN}โ${NC} ${result.pptxPath}\n`,
|
|
53
|
-
)
|
|
54
|
-
if (result.mirrorPath) {
|
|
39
|
+
try {
|
|
40
|
+
const sourcePath = resolve(process.cwd(), opts.source)
|
|
41
|
+
const outDir = resolve(process.cwd(), opts.out)
|
|
42
|
+
if (!existsSync(sourcePath)) {
|
|
43
|
+
fail(`${opts.source} not found`)
|
|
44
|
+
}
|
|
45
|
+
const variant = parseVariant(opts.variant)
|
|
46
|
+
const mirror = resolveMirror(opts.mirror)
|
|
47
|
+
intro('Render slides')
|
|
48
|
+
const result = await renderSlidesDoc(sourcePath, outDir, {
|
|
49
|
+
variant,
|
|
50
|
+
mirror,
|
|
51
|
+
})
|
|
55
52
|
process.stderr.write(
|
|
56
|
-
`${GREY}โ${NC} ${GREEN}โ${NC}
|
|
53
|
+
`${GREY}โ${NC} ${GREEN}โ${NC} ${result.slideCount} slides\n${GREY}โ${NC} ${GREEN}โ${NC} ${result.pptxPath}\n`,
|
|
57
54
|
)
|
|
55
|
+
if (result.mirrorPath) {
|
|
56
|
+
process.stderr.write(
|
|
57
|
+
`${GREY}โ${NC} ${GREEN}โ${NC} mirrored to ${result.mirrorPath}\n`,
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
if (opts.open) {
|
|
61
|
+
const target = result.mirrorPath ?? result.pptxPath
|
|
62
|
+
const opened = await openDeck(target)
|
|
63
|
+
const mark = opened ? `${GREEN}โ${NC}` : `${RED}โ${NC}`
|
|
64
|
+
process.stderr.write(
|
|
65
|
+
`${GREY}โ${NC} ${mark} ${opened ? 'opened' : 'could not open'} ${target}\n`,
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
outro()
|
|
69
|
+
} catch (error) {
|
|
70
|
+
reportFailure(error)
|
|
58
71
|
}
|
|
59
|
-
if (opts.open) {
|
|
60
|
-
const target = result.mirrorPath ?? result.pptxPath
|
|
61
|
-
const opened = await openDeck(target)
|
|
62
|
-
const mark = opened ? `${GREEN}โ${NC}` : `${RED}โ${NC}`
|
|
63
|
-
process.stderr.write(
|
|
64
|
-
`${GREY}โ${NC} ${mark} ${opened ? 'opened' : 'could not open'} ${target}\n`,
|
|
65
|
-
)
|
|
66
|
-
}
|
|
67
|
-
outro()
|
|
68
72
|
},
|
|
69
73
|
)
|
|
70
74
|
|
|
@@ -98,9 +102,27 @@ function resolveMirror(value: string | undefined): string | undefined {
|
|
|
98
102
|
return mirror ? resolve(process.cwd(), mirror) : undefined
|
|
99
103
|
}
|
|
100
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Carries a fail-fast message from a validation helper to the action that
|
|
107
|
+
* called it. `fail` has to keep its `never` return, since that is what makes
|
|
108
|
+
* `parseVariant` exhaustive to the compiler, and a helper deep in the call
|
|
109
|
+
* stack cannot set `process.exitCode` and unwind on its own.
|
|
110
|
+
*/
|
|
111
|
+
class SlidesError extends Error {}
|
|
112
|
+
|
|
101
113
|
function fail(message: string): never {
|
|
114
|
+
throw new SlidesError(message)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* `src/cli.ts` calls `program.parse()` without awaiting it, so a rejected
|
|
119
|
+
* action promise reaches no handler and Bun prints a stack trace. Every action
|
|
120
|
+
* that calls `fail` catches at its own boundary.
|
|
121
|
+
*/
|
|
122
|
+
function reportFailure(error: unknown): void {
|
|
123
|
+
if (!(error instanceof SlidesError)) throw error
|
|
102
124
|
process.stderr.write(
|
|
103
|
-
`${GREY}โ${NC}\n${GREY}โ${NC} ${RED}โ${NC} ${message}\n${GREY}โ${NC}\n`,
|
|
125
|
+
`${GREY}โ${NC}\n${GREY}โ${NC} ${RED}โ${NC} ${error.message}\n${GREY}โ${NC}\n`,
|
|
104
126
|
)
|
|
105
|
-
process.
|
|
127
|
+
process.exitCode = 1
|
|
106
128
|
}
|
package/standards/skill.md
CHANGED
|
@@ -22,6 +22,17 @@ Does not govern:
|
|
|
22
22
|
- The transform from a branch name to a slug a skill carries in a filename: `slug.md`
|
|
23
23
|
- The domain conventions a skill cites, each of which belongs to the standard that owns it
|
|
24
24
|
|
|
25
|
+
## Changing a skill
|
|
26
|
+
|
|
27
|
+
Answer all four before editing, and carry the answers into wherever the change is proposed.
|
|
28
|
+
|
|
29
|
+
- What problem does this solve? Name the run that went wrong, rather than the improvement the change makes.
|
|
30
|
+
- Which surface owns the rule today? A rule already stated in a standard, a governance rule, or a sibling skill is cited or moved, never restated in the body.
|
|
31
|
+
- What deterministic check catches a regression? Name it, or say none exists and the step holds on a session reading it.
|
|
32
|
+
- What does this collide with? Name the sibling skill, rule, or requirement it contradicts, or state that nothing does.
|
|
33
|
+
|
|
34
|
+
The second question is the one that decides between a body and a rule, which the next section tests in two parts.
|
|
35
|
+
|
|
25
36
|
## Whether a rule belongs beside the skill
|
|
26
37
|
|
|
27
38
|
A skill fires when a session invokes it or its description matches the request. A path-scoped rule fires when a session reads a file matching its glob, with no decision from the session at all. The two are layers rather than alternatives, so the rule is the floor and the skill is the depth.
|
package/standards/standard.md
CHANGED
|
@@ -82,6 +82,17 @@ A standard failing these questions is non-conforming even when it satisfies ever
|
|
|
82
82
|
|
|
83
83
|
## Changing a standard
|
|
84
84
|
|
|
85
|
+
### The checkpoint
|
|
86
|
+
|
|
87
|
+
Answer all four before editing, and carry the answers into wherever the change is proposed. This section governs itself, so the next edit to this file answers them too.
|
|
88
|
+
|
|
89
|
+
- What problem does this solve? Name the artifact that went wrong, rather than the improvement the change makes.
|
|
90
|
+
- Which surface owns the rule today? A rule already stated somewhere moves or is cited, never restated in a second place.
|
|
91
|
+
- What deterministic check catches a regression? Name it, or say none exists and the rule holds on reading alone.
|
|
92
|
+
- What does this collide with? Name the sibling standard, rule, or template it contradicts, or state that nothing does.
|
|
93
|
+
|
|
94
|
+
### What justifies a change
|
|
95
|
+
|
|
85
96
|
- Change a standard on a failure, not on a finding. A finding is that the docs say X or a paper suggests Y. A failure is a conforming artifact that satisfied every shape rule and still missed the success criterion.
|
|
86
97
|
- Park findings wherever the project tracks pending work, or in the standard's own backlog section when it tracks none. They are hypotheses to test, not instructions to apply.
|
|
87
98
|
- Cite the failing artifact in the change that fixes it, so the next reader can tell which rules were paid for by evidence.
|
|
@@ -1,114 +1,96 @@
|
|
|
1
1
|
# Tooling base reference
|
|
2
2
|
|
|
3
|
-
##
|
|
4
|
-
|
|
5
|
-
- Use `bun` as package manager and script runner.
|
|
6
|
-
- Use `bunx` instead of `npx` for one-off executables.
|
|
7
|
-
|
|
8
|
-
## Prettier
|
|
9
|
-
|
|
10
|
-
- Config: `.prettierrc` (JSON) at root.
|
|
11
|
-
- Rules: `semi: false`, `singleQuote: true`.
|
|
12
|
-
- Add parser overrides for non-standard extensions (e.g., `.mdx` โ `markdown`).
|
|
13
|
-
- Ignore paths via `.gitignore` and `.prettierignore`. Pass both as `--ignore-path` on all prettier invocations.
|
|
14
|
-
- `.prettierignore` is a user-owned seed. It is created empty on install. Projects add their own entries.
|
|
15
|
-
- Use `--log-level warn` on all prettier invocations to suppress per-file `(unchanged)` output.
|
|
16
|
-
|
|
17
|
-
## Dev Dependencies
|
|
18
|
-
|
|
19
|
-
- `prettier`, `cspell`, `husky`, `@commitlint/cli`, `@commitlint/config-conventional`.
|
|
20
|
-
- Install via `bun add -D`.
|
|
21
|
-
- Ensure `.gitignore` contains `node_modules/`.
|
|
22
|
-
|
|
23
|
-
## CSpell
|
|
24
|
-
|
|
25
|
-
- `cspell.json` is a user-owned seed at root. Sync drops it once on first install and never overwrites it. Projects extend it with extra `import` and `dictionaryDefinitions` entries.
|
|
26
|
-
- The seeded baseline includes `version: "0.2"`, `language: "en"`, `useGitignore: true`, `gitignoreRoot: ["."]`, dictionary definitions for `project-terms` and `tech-stack`, and `ignorePaths: [".cspell/**", ".git/**"]` to skip dictionary self-checks and git object files. Both dictionary definitions set `addWords: true`.
|
|
27
|
-
- `gitignoreRoot: ["."]` pins the `.gitignore` search to the project root. Without it, cspell run from inside a linked worktree under `.claude/worktrees/` walks up into the parent repo's `.gitignore`, resolves every worktree file as living under the ignored worktree path, and checks zero files. New words then pass locally and fail in CI. `gitignoreRoot` stops the walk at the root while the worktree's own `.gitignore` still excludes `node_modules` and build output.
|
|
28
|
-
- Dictionary files in `.cspell/`: `project-terms.txt`, `tech-stack.txt`.
|
|
29
|
-
- Include dotfolders in the spell glob: `cspell '**' '.*/**' '.*' ...`. The default `**` skips dot-prefixed folders, so `.claude/`, `.github/`, and `.husky/` go unchecked without explicit globs.
|
|
30
|
-
- Keep dictionary entries sorted alphabetically, one word per line.
|
|
31
|
-
|
|
32
|
-
## Shell Tooling
|
|
33
|
-
|
|
34
|
-
- Format: `shfmt --indent 2 scripts/`. shfmt supports directory args natively, no `find` needed.
|
|
35
|
-
- Lint: `find scripts -name '*.sh' -exec shellcheck --severity=warning {} +`. shellcheck has no directory mode, `find` is required.
|
|
36
|
-
- Config: `.shellcheckrc` with `external-sources=true`. Required for shellcheck to follow `source` directives. Keep even with EditorConfig present.
|
|
37
|
-
- All shell scripts live in `scripts/`. Do not place `.sh` files outside `scripts/`.
|
|
38
|
-
- EditorConfig: `.editorconfig` at root with `[*.sh]` block enforcing `indent_style = space`, `indent_size = 2`. Prevents editor/shfmt conflicts that produce spurious git diffs.
|
|
39
|
-
|
|
40
|
-
## Commit Lint
|
|
41
|
-
|
|
42
|
-
- Config: `commitlint.config.js` (ESM default export).
|
|
43
|
-
- Extends: `@commitlint/config-conventional`.
|
|
44
|
-
- Rules: `header-max-length: 72`, `scope-case: lower-case`, `subject-full-stop: never`, `subject-case: disabled`.
|
|
45
|
-
- Format: `<type>(<scope>): <subject>` (imperative mood, no trailing period).
|
|
46
|
-
|
|
47
|
-
## Husky + Lint-Staged
|
|
48
|
-
|
|
49
|
-
- `.lintstagedrc` is a user-owned seed at root. Sync drops it once on first install and never overwrites it. Projects extend it with extra glob โ command entries (e.g. `aitk indexes regen`).
|
|
50
|
-
- Seeded baseline globs:
|
|
51
|
-
- `**/*.{json,md,mdc}` โ `["prettier --write --ignore-path .gitignore --ignore-path .prettierignore", "cspell --no-must-find-files"]`
|
|
52
|
-
- `**/*.md` โ `["aitk indexes regen"]`
|
|
53
|
-
- `**/*.sh` โ `["shfmt --write --indent 2", "shellcheck --severity=warning"]`
|
|
54
|
-
- Hooks in `.husky/`:
|
|
55
|
-
- `pre-commit` โ `bunx lint-staged`
|
|
56
|
-
- `commit-msg` โ `bunx commitlint --edit "$1"`
|
|
57
|
-
- `pre-push` โ `bun run check`
|
|
58
|
-
- `post-merge` โ names `.claude/tasks/` archive candidates, silent otherwise and when the board is absent
|
|
59
|
-
- `post-rewrite` โ delegates to `post-merge` on `rebase`, so a `pull.rebase=true` machine still gets the check
|
|
60
|
-
- Husky runs hooks as `sh -e`, so a hook carrying logic is POSIX sh under errexit no matter what its shebang says.
|
|
61
|
-
- Note: lint-staged handles its own glob expansion and passes matched files as arguments. `**/*.sh` is safe here, unlike in package.json scripts.
|
|
62
|
-
|
|
63
|
-
## GitHub
|
|
64
|
-
|
|
65
|
-
- PR template: `.github/pull_request_template.md`.
|
|
66
|
-
- Sections: `## Summary`, `## Key Changes`, `## Technical Context`, `## Testing`.
|
|
67
|
-
- Visuals: HTML comment only, never a visible section header.
|
|
68
|
-
- Imperative mood, no "This PR" opener, no buzzwords, name specific files and functions.
|
|
69
|
-
- CI workflow: `.github/workflows/verify.yml`. Runs on pull requests targeting `main` and on `workflow_dispatch`.
|
|
70
|
-
- Workflow steps: checkout, setup Bun (latest), `bun install --frozen-lockfile`, install `shfmt` and `shellcheck` via apt, then `check:format`, `check:spell`, `check:shell`.
|
|
71
|
-
- Does not run `format` before asserting. CI asserts only. Format must be clean before push.
|
|
3
|
+
## Overview
|
|
72
4
|
|
|
73
|
-
|
|
5
|
+
The base layer covers every project the toolkit scaffolds, whatever language sits on top. It ships formatting, spelling, shell linting, conventional commits, git hooks, CI, and three maintenance scripts. Every other stack extends it, so a decision made here is one every stack inherits.
|
|
74
6
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
7
|
+
## What ships as golden configs
|
|
8
|
+
|
|
9
|
+
Golden config files live in `tooling/base/configs/` and are copied into the target on `aitk tooling sync base .`. They are the source of truth. The reference covers rationale and tradeoffs. Configs show the concrete setup.
|
|
10
|
+
|
|
11
|
+
- `.prettierrc`: `semi: false`, `singleQuote: true`, plus a parser override per non-standard extension (`.mdx` to `markdown`).
|
|
12
|
+
- `.shellcheckrc`: `external-sources=true`. Required for shellcheck to follow `source` directives.
|
|
13
|
+
- `.editorconfig`: `root = true`, with an `[*.sh]` block setting `indent_style = space` and `indent_size = 2`.
|
|
14
|
+
- `commitlint.config.js`: ESM default export extending `@commitlint/config-conventional`. Rules are `header-max-length: 72`, `scope-case: lower-case`, `subject-full-stop: never`, and `subject-case` disabled.
|
|
15
|
+
- `.husky/`: `pre-commit`, `commit-msg`, `pre-push`, `post-merge`, `post-rewrite`.
|
|
16
|
+
- `.github/workflows/verify.yml`: runs on pull requests targeting `main` and on `workflow_dispatch`.
|
|
17
|
+
- `.github/pull_request_template.md`: `## Summary`, `## Key Changes`, `## Technical Context`, `## Testing`.
|
|
18
|
+
- `.vscode/extensions.json` and `.vscode/settings.json`: editor wiring for Prettier, cspell, shfmt, and shellcheck.
|
|
19
|
+
- `scripts/verify.sh`, `scripts/clean.sh`, `scripts/update.sh`: the maintenance entry points behind `check`, `clean`, and `update`.
|
|
20
|
+
|
|
21
|
+
## What ships as user-owned seeds
|
|
22
|
+
|
|
23
|
+
Seeds live in `tooling/base/seeds/`. Sync drops each once on first install and never overwrites it, so a project extends them freely.
|
|
24
|
+
|
|
25
|
+
- `cspell.json`: `version: "0.2"`, `language: "en"`, `useGitignore: true`, `gitignoreRoot: ["."]`, dictionary definitions for `project-terms` and `tech-stack` with `addWords: true` on both, and `ignorePaths: [".cspell/**", ".git/**"]` to skip dictionary self-checks and git object files.
|
|
26
|
+
- `.cspell/project-terms.txt` and `.cspell/tech-stack.txt`: one word per line, sorted alphabetically.
|
|
27
|
+
- `.lintstagedrc`: the glob map below.
|
|
28
|
+
- `.prettierignore`: created empty. Projects add their own entries.
|
|
29
|
+
- `.claude/context/ci.md` and `.claude/context/development.md`: extend with project-specific commands, workflows, or deploy steps. Canonical rationale stays in this reference.
|
|
78
30
|
|
|
79
|
-
##
|
|
31
|
+
## Tool pairing
|
|
80
32
|
|
|
81
|
-
-
|
|
82
|
-
-
|
|
83
|
-
-
|
|
84
|
-
-
|
|
85
|
-
-
|
|
33
|
+
- Runtime: `bun` as package manager and script runner, `bunx` over `npx` for a one-off executable.
|
|
34
|
+
- Formatting: Prettier for what it parses, shfmt for shell. Two formatters because Prettier has no shell parser.
|
|
35
|
+
- Spelling: cspell over the whole tree, with project vocabulary split into a project-terms dictionary and a tech-stack one.
|
|
36
|
+
- Shell: shfmt formats and shellcheck lints at warning severity. shfmt takes a directory argument, shellcheck has no directory mode and needs `find`.
|
|
37
|
+
- Commits: commitlint against conventional commits, wired through the husky `commit-msg` hook. Format is `<type>(<scope>): <subject>` in imperative mood with no trailing period.
|
|
38
|
+
- Dev dependencies: `prettier`, `cspell`, `husky`, `@commitlint/cli`, `@commitlint/config-conventional`. Install via `bun add -D`.
|
|
86
39
|
|
|
87
|
-
##
|
|
40
|
+
## File layout
|
|
88
41
|
|
|
89
|
-
-
|
|
90
|
-
-
|
|
91
|
-
-
|
|
42
|
+
- All shell scripts live in `scripts/`. Do not place a `.sh` file outside it.
|
|
43
|
+
- Dictionaries live in `.cspell/`, hooks in `.husky/`, seeded context docs in `.claude/context/`.
|
|
44
|
+
- The `.claude/context/` location matches the three-tier context model: project-wide invariants in `CLAUDE.md`, `.claude/REQUIREMENTS.md`, and `.claude/ARCHITECTURE.md`, path-scoped rules in `.claude/rules/`, and on-demand domain narrative in `.claude/context/`. Indexes stay opt-in.
|
|
92
45
|
|
|
93
|
-
##
|
|
46
|
+
## Hooks
|
|
94
47
|
|
|
95
|
-
-
|
|
96
|
-
-
|
|
48
|
+
- `pre-commit` runs `bunx lint-staged`.
|
|
49
|
+
- `commit-msg` runs `bunx commitlint --edit "$1"`.
|
|
50
|
+
- `pre-push` runs `bun run check`.
|
|
51
|
+
- `post-merge` names `.claude/tasks/` archive candidates, staying silent otherwise and when the board is absent.
|
|
52
|
+
- `post-rewrite` delegates to `post-merge` on `rebase`, so a `pull.rebase=true` machine still gets the check.
|
|
97
53
|
|
|
98
|
-
##
|
|
54
|
+
## lint-staged
|
|
99
55
|
|
|
100
|
-
|
|
101
|
-
- Each carries `title` and `description` frontmatter so the files slot into `.claude/context/index.md` if the project adopts the `indexes` system. Indexes stay opt-in.
|
|
102
|
-
- Extend freely with project-specific commands, workflows, or deploy steps. Canonical rationale stays in this reference.
|
|
103
|
-
- The `.claude/context/` location matches the three-tier context model: project-wide invariants in `CLAUDE.md` and `.claude/REQUIREMENTS.md`/`.claude/ARCHITECTURE.md`, path-scoped rules in `.claude/rules/`, and on-demand domain narrative in `.claude/context/`.
|
|
56
|
+
Seeded baseline globs:
|
|
104
57
|
|
|
105
|
-
|
|
58
|
+
- `**/*.{json,md,mdc}` runs `prettier --write --ignore-path .gitignore --ignore-path .prettierignore` then `cspell --no-must-find-files`
|
|
59
|
+
- `**/*.md` runs `aitk indexes regen`
|
|
60
|
+
- `**/*.sh` runs `shfmt --write --indent 2` then `shellcheck --severity=warning`
|
|
61
|
+
|
|
62
|
+
## CI
|
|
63
|
+
|
|
64
|
+
- Steps: checkout, setup Bun at latest, `bun install --frozen-lockfile`, install `shfmt` and `shellcheck` via apt, then `check:format`, `check:spell`, and `check:shell`.
|
|
65
|
+
- CI asserts and never writes. Format must be clean before push.
|
|
66
|
+
|
|
67
|
+
## Gitignore
|
|
68
|
+
|
|
69
|
+
- `# System`: `.DS_Store`
|
|
70
|
+
- `# Dependencies`: `node_modules/`
|
|
71
|
+
- `# Secrets`: `.env`, `.env.*`, `*.local`, `!.env.example`
|
|
106
72
|
|
|
107
|
-
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
- `
|
|
112
|
-
-
|
|
113
|
-
-
|
|
114
|
-
- `
|
|
73
|
+
## Anti-patterns
|
|
74
|
+
|
|
75
|
+
Sticky negative knowledge. Do not relearn.
|
|
76
|
+
|
|
77
|
+
- Do NOT drop `gitignoreRoot: ["."]` from `cspell.json`. Without it, cspell run from inside a linked worktree under `.claude/worktrees/` walks up into the parent repo's `.gitignore`, resolves every worktree file as living under the ignored worktree path, and checks zero files. New words then pass locally and fail in CI.
|
|
78
|
+
- Do NOT rely on the default `**` glob for spelling. It skips dot-prefixed folders, leaving `.claude/`, `.github/`, and `.husky/` unchecked. Pass `'**' '.*/**' '.*'` explicitly.
|
|
79
|
+
- Do NOT invoke prettier without both `--ignore-path .gitignore --ignore-path .prettierignore`. Passing one drops the other, since the flag replaces the default rather than adding to it.
|
|
80
|
+
- Do NOT omit `--log-level warn` from a prettier invocation. The default prints a line per unchanged file.
|
|
81
|
+
- Do NOT put logic in a husky hook and expect its shebang to hold. Husky runs hooks as `sh -e`, so a hook carrying logic is POSIX sh under errexit whatever the first line says.
|
|
82
|
+
- Do NOT drop the `[*.sh]` block from `.editorconfig` because shfmt already sets indentation. The editor and shfmt then disagree and produce spurious git diffs.
|
|
83
|
+
- Do NOT copy lint-staged's `**/*.sh` glob into a package.json script. lint-staged expands its own globs and passes matched files as arguments, which a bare shell script does not.
|
|
84
|
+
|
|
85
|
+
## CLI
|
|
86
|
+
|
|
87
|
+
| Script | What it does |
|
|
88
|
+
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
89
|
+
| `bun run check` | Full verification suite via `scripts/verify.sh`. Runs `format` first to auto-fix drifted code, then asserts. Honors `VERIFY_NESTED=true` to suppress timeline boundaries when another script calls it. |
|
|
90
|
+
| `bun run check:format` | Asserts prettier and shfmt formatting without writing |
|
|
91
|
+
| `bun run check:spell` | Runs cspell across every file, with context on failures |
|
|
92
|
+
| `bun run check:shell` | Runs shellcheck at warning severity |
|
|
93
|
+
| `bun run format` | Writes prettier and shfmt formatting in place |
|
|
94
|
+
| `bun run prepare` | Initializes husky hooks, run automatically on `bun install` |
|
|
95
|
+
| `bun run clean` | Removes `node_modules/`, clears the bun cache, reinstalls fresh |
|
|
96
|
+
| `bun run update` | Runs `bun update --interactive`, then `verify.sh` with `VERIFY_NESTED=true` |
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
# Claude Code sends a payload and closes stdin. A bare read with nothing feeding
|
|
4
|
+
# it blocks forever and holds the session open, so the read is bounded. `read`
|
|
5
|
+
# rather than `timeout cat`, which macOS does not ship.
|
|
6
|
+
IFS= read -r -d '' -t 2 input
|
|
7
|
+
[ -n "$input" ] || {
|
|
8
|
+
printf '%s reads a Claude Code hook payload on stdin and cannot be run by hand.\n' "${0##*/}" >&2
|
|
9
|
+
exit 1
|
|
10
|
+
}
|
|
4
11
|
|
|
5
12
|
tool=$(printf '%s' "$input" | jq -r '.tool_name // empty')
|
|
6
13
|
case "$tool" in
|
|
@@ -3,10 +3,18 @@
|
|
|
3
3
|
# Regenerates .claude/memory/index.md after a memory file changes.
|
|
4
4
|
#
|
|
5
5
|
# The memory folder is gitignored, so the whole-repo walk in `bun run check`
|
|
6
|
-
# drops it and never regenerates this index.
|
|
7
|
-
#
|
|
6
|
+
# drops it and never regenerates this index. Naming the file as a positional
|
|
7
|
+
# argument to `aitk indexes regen` below bypasses that filter, which makes this
|
|
8
|
+
# hook the only trigger that reaches the folder.
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
# Claude Code sends a payload and closes stdin. A bare read with nothing feeding
|
|
11
|
+
# it blocks forever and holds the session open, so the read is bounded. `read`
|
|
12
|
+
# rather than `timeout cat`, which macOS does not ship.
|
|
13
|
+
IFS= read -r -d '' -t 2 input
|
|
14
|
+
[ -n "$input" ] || {
|
|
15
|
+
printf '%s reads a Claude Code hook payload on stdin and cannot be run by hand.\n' "${0##*/}" >&2
|
|
16
|
+
exit 1
|
|
17
|
+
}
|
|
10
18
|
|
|
11
19
|
tool=$(printf '%s' "$input" | jq -r '.tool_name // empty')
|
|
12
20
|
case "$tool" in
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
# Claude Code sends a payload and closes stdin. A bare read with nothing feeding
|
|
4
|
+
# it blocks forever and holds the session open, so the read is bounded. `read`
|
|
5
|
+
# rather than `timeout cat`, which macOS does not ship.
|
|
6
|
+
IFS= read -r -d '' -t 2 input
|
|
7
|
+
[ -n "$input" ] || {
|
|
8
|
+
printf '%s reads a Claude Code hook payload on stdin and cannot be run by hand.\n' "${0##*/}" >&2
|
|
9
|
+
exit 1
|
|
10
|
+
}
|
|
4
11
|
|
|
5
12
|
tool=$(printf '%s' "$input" | jq -r '.tool_name // empty')
|
|
6
13
|
case "$tool" in
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
# Claude Code sends a payload and closes stdin. A bare read with nothing feeding
|
|
4
|
+
# it blocks forever and holds the session open, so the read is bounded. `read`
|
|
5
|
+
# rather than `timeout cat`, which macOS does not ship.
|
|
6
|
+
IFS= read -r -d '' -t 2 input
|
|
7
|
+
[ -n "$input" ] || {
|
|
8
|
+
printf '%s reads a Claude Code hook payload on stdin and cannot be run by hand.\n' "${0##*/}" >&2
|
|
9
|
+
exit 1
|
|
10
|
+
}
|
|
11
|
+
|
|
4
12
|
file=$(printf '%s' "$input" | jq -r '.tool_input.file_path // .tool_response.filePath // empty')
|
|
5
13
|
|
|
6
14
|
file="${file//\\//}"
|
|
@@ -3,10 +3,18 @@
|
|
|
3
3
|
# Regenerates .claude/tasks/index.md after a task file changes.
|
|
4
4
|
#
|
|
5
5
|
# The task folder is gitignored, so the whole-repo walk in `bun run check`
|
|
6
|
-
# drops it and never regenerates this index.
|
|
7
|
-
#
|
|
6
|
+
# drops it and never regenerates this index. Naming the file as a positional
|
|
7
|
+
# argument to `aitk indexes regen` below bypasses that filter, which makes this
|
|
8
|
+
# hook the only trigger that reaches the folder.
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
# Claude Code sends a payload and closes stdin. A bare read with nothing feeding
|
|
11
|
+
# it blocks forever and holds the session open, so the read is bounded. `read`
|
|
12
|
+
# rather than `timeout cat`, which macOS does not ship.
|
|
13
|
+
IFS= read -r -d '' -t 2 input
|
|
14
|
+
[ -n "$input" ] || {
|
|
15
|
+
printf '%s reads a Claude Code hook payload on stdin and cannot be run by hand.\n' "${0##*/}" >&2
|
|
16
|
+
exit 1
|
|
17
|
+
}
|
|
10
18
|
|
|
11
19
|
tool=$(printf '%s' "$input" | jq -r '.tool_name // empty')
|
|
12
20
|
case "$tool" in
|