@softspark/ai-toolkit 4.18.0 → 4.19.1

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/CHANGELOG.md CHANGED
@@ -7,6 +7,60 @@ Versioning follows [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ---
9
9
 
10
+ ## v4.19.1 — re-release from corrected history (2026-07-27)
11
+
12
+ No functional change. The package contents are identical to v4.19.0.
13
+
14
+ v4.19.0 was committed with two commit messages attached to the wrong changes:
15
+ the commit titled `chore: release v4.19.0` held only the post-release testing
16
+ SOP, and the release itself shipped under a recycled message about a CodeQL
17
+ action bump. `main` has been rewritten so the history states what each commit
18
+ actually did, and this version is published from that corrected history.
19
+
20
+ The `v4.19.0` tag is deliberately left pointing at the pre-rewrite commit
21
+ `8c90024`. That commit is what the published SLSA attestation for v4.19.0 pins,
22
+ so the tag keeps it fetchable even though it is no longer an ancestor of `main`.
23
+ Do not delete that tag.
24
+
25
+ ---
26
+
27
+ ## v4.19.0 — rtk-pack retired (2026-07-27)
28
+
29
+ ### Removed
30
+
31
+ - **`rtk-pack`**, one day after it shipped. The first real install proved every
32
+ rewritten command failed with exit 127: rtk names itself bare in the rewrite
33
+ and the pack keeps its binary off `PATH` on purpose, so the shell could not
34
+ find it. `git`, `ls`, `cat`, `find`, `grep`, `diff` and the rest all died
35
+ before running. A second defect fetched the Intel build onto Apple Silicon,
36
+ because `platform.machine()` reports `x86_64` from a Rosetta-translated
37
+ Python. The pack's own `plugin status` reported both as green.
38
+
39
+ All three were fixed and tested before the decision. The measured saving was
40
+ 0.0615% of input tokens against a kill number of 0.05%, a margin the
41
+ integration plan itself called "a pass, not a vindication", which does not
42
+ justify a supply-chain surface, a five-target cross-build workflow, an
43
+ upstream-sync SOP, and a hook that rewrites every command before it runs.
44
+
45
+ Full postmortem: `kb/history/completed/rtk-pack-retirement-20260727.md`.
46
+ - `.github/workflows/rtk-build.yml`, `scripts/verify_rtk_binary.py`,
47
+ `tests/test_rtk_pack.bats`, `tests/test_verify_rtk_binary.bats`,
48
+ `kb/procedures/rtk-upstream-sync-sop.md`, and the GitHub Release
49
+ `softspark-rtk-v0.44.0-1` holding the five cross-built binaries.
50
+
51
+ If you installed the pack under v4.18.0, run
52
+ `ai-toolkit plugin remove rtk-pack`.
53
+
54
+ ### Kept
55
+
56
+ Everything the pack work built underneath it, none of which is rtk-specific:
57
+ pack hook wiring for Cursor and Gemini, `supported_editors` in the manifest,
58
+ generic `plugin status` dispatch to a pack's own `scripts/status.py`,
59
+ version-aware `plugin update`, and `audit_skills.py --ci` plus the ShellCheck
60
+ gate covering `app/plugins/`.
61
+
62
+ ---
63
+
10
64
  ## v4.18.0 — rtk-pack, an opt-in command rewriter (2026-07-26)
11
65
 
12
66
  ### Added
package/README.md CHANGED
@@ -6,18 +6,22 @@
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
7
7
  [![Skills](https://img.shields.io/badge/skills-108-brightgreen)](app/skills/)
8
8
  [![Agents](https://img.shields.io/badge/agents-44-blue)](app/agents/)
9
- [![Tests](https://img.shields.io/badge/tests-1484%20passing-success)](tests/)
9
+ [![Tests](https://img.shields.io/badge/tests-1465%20passing-success)](tests/)
10
10
 
11
- ## What's New in v4.18.0
11
+ ## What's New in v4.19.1
12
12
 
13
- v4.18.0 adds `rtk-pack`, an opt-in command rewriter, and makes `ai-toolkit update` maintain installed packs.
13
+ v4.19.1 is v4.19.0 re-published from a corrected commit history — same package contents, no functional change. What that release does:
14
14
 
15
- - **`rtk-pack`, opt-in only**: rewrites Bash commands at `PreToolUse` via [rtk](https://github.com/rtk-ai/rtk) (Apache-2.0). Nothing installs it for you. `plugin install` fetches a platform binary from an ai-toolkit Release and verifies its SHA-256 before installing anything. Measured saving on the reference workload is 0.0615% of input tokens; the pack README states the trust boundary and the numbers up front.
16
- - **Binaries built from source, telemetry compiled out**: a manual-dispatch workflow cross-builds five targets with the compile-time telemetry endpoint undefined, proves each artifact starts, writes no telemetry state, and on Linux behaves identically with no network route.
17
- - **`ai-toolkit update` maintains installed packs**: it now runs the equivalent of `plugin update --all`. Updates are version-aware, so a pack whose manifest has not moved is a silent no-op instead of a remove-and-reinstall.
15
+ Removes `rtk-pack`. The pack that v4.18.0 shipped is gone; everything the work built underneath it stays.
16
+
17
+ - **`rtk-pack` retired**: it broke every command it rewrote. rtk names itself bare in the rewrite, the pack kept its binary off `PATH` on purpose, and the result was exit 127 on `git`, `ls`, `cat`, `find`, `grep` and the rest. Measured value before the defect was 0.0615% of input tokens against a 0.05% kill number, so the fix was not worth the surface. See [the retirement note](kb/history/completed/rtk-pack-retirement-20260727.md).
18
+ - **Pack hook wiring for Cursor and Gemini stays**: packs write user-scope entries to `~/.cursor/hooks.json` and `~/.gemini/settings.json`, tagged per pack, and `plugin remove` takes only its own back out.
19
+ - **`ai-toolkit update` maintains installed packs**: it runs the equivalent of `plugin update --all`. Updates are version-aware, so a pack whose manifest has not moved is a silent no-op instead of a remove-and-reinstall.
18
20
  - **Packs can report their own health**: `plugin status` picks up a pack's `scripts/status.py`, replacing a hardcoded per-pack branch.
19
21
  - **Security gates cover packs**: `audit_skills.py --ci` and the ShellCheck gate now scan `app/plugins/`, which they previously skipped.
20
22
 
23
+ If you installed the pack under v4.18.0, run `ai-toolkit plugin remove rtk-pack`.
24
+
21
25
  See [CHANGELOG.md](CHANGELOG.md) for full history.
22
26
 
23
27
  ---
@@ -143,7 +147,7 @@ See [CLI Reference](kb/reference/cli-reference.md) for all commands and options.
143
147
  | `skills/` (knowledge) | 46 | Domain knowledge auto-loaded by agents (includes 13 `<lang>-rules` skills) |
144
148
  | `agents/` | 44 | Specialized agents across 10 categories |
145
149
  | `hooks/` | 28 entries / 14 events + statusLine | Quality gates, path safety, prompt governance, loop guard, session lifecycle |
146
- | `plugins/` | 12 packs | Opt-in domain bundles (security, research, frontend, enterprise, memory, rtk, 6 language packs) |
150
+ | `plugins/` | 11 packs | Opt-in domain bundles (security, research, frontend, enterprise, memory, 6 language packs) |
147
151
  | `constitution.md` | 7 articles | Machine-enforced safety rules |
148
152
  | `rules/` | auto-synced | Global/project rule files for Claude and other editors |
149
153
  | `kb/` | reference docs | Architecture, procedures, and best practices |
@@ -166,7 +170,7 @@ ai-toolkit/
166
170
  │ └── ARCHITECTURE.md # Full system design
167
171
  ├── kb/ # Reference docs, procedures, plans
168
172
  ├── scripts/ # Validation, install, evaluation scripts
169
- ├── tests/ # Bats and Python test suite (1484 tests)
173
+ ├── tests/ # Bats and Python test suite (1494 tests)
170
174
  └── CHANGELOG.md
171
175
  ```
172
176
 
@@ -3,7 +3,7 @@
3
3
  "name": "ai-toolkit",
4
4
  "displayName": "AI Toolkit",
5
5
  "description": "Professional-grade engineering skills, agents, rules, and lifecycle guardrails for Claude Code, Claude Chat, and Cowork.",
6
- "version": "4.18.0",
6
+ "version": "4.19.1",
7
7
  "author": {
8
8
  "name": "SoftSpark",
9
9
  "url": "https://github.com/softspark"
@@ -17,7 +17,7 @@ Plugin packs can target:
17
17
  These packs are not directly uploadable Claude app plugins. For Claude Chat,
18
18
  Desktop, and Cowork use `ai-toolkit claude-app export`.
19
19
 
20
- Current packs (12):
20
+ Current packs (11):
21
21
  - `csharp-pack`
22
22
  - `enterprise-pack`
23
23
  - `frontend-pack`
@@ -25,14 +25,14 @@ Current packs (12):
25
25
  - `kotlin-pack`
26
26
  - `memory-pack`
27
27
  - `research-pack`
28
- - `rtk-pack`
29
28
  - `ruby-pack`
30
29
  - `rust-pack`
31
30
  - `security-pack`
32
31
  - `swift-pack`
33
32
 
34
- `rtk-pack` is the only one that fetches a native binary at install time and the
35
- only one that rewrites commands before they run. Read its README before
36
- installing it.
33
+ Every pack here is content: agents, skills, rules and hook scripts that ship in
34
+ this repository. None fetches a binary at install time. `rtk-pack` did, and was
35
+ retired in v4.19.0; see `kb/history/completed/rtk-pack-retirement-20260727.md`
36
+ before proposing another pack of that shape.
37
37
 
38
38
  See `kb/reference/plugin-pack-conventions.md` for pack rules, validation, and adoption guidance.
@@ -1,5 +1,5 @@
1
1
  {
2
- "last_run": "2026-07-26T11:36:57Z",
2
+ "last_run": "2026-07-27T07:12:31Z",
3
3
  "schema_version": 1,
4
4
  "tools": {
5
5
  "aider": {
@@ -24,7 +24,7 @@
24
24
  }
25
25
  },
26
26
  "augment": {
27
- "docs_hash": "1dd9e9f490e7103a",
27
+ "docs_hash": "b8b7ddbeaccd618d",
28
28
  "headings": [
29
29
  "Admin",
30
30
  "Auggie CLI",
@@ -66,7 +66,7 @@
66
66
  }
67
67
  },
68
68
  "claude-app": {
69
- "docs_hash": "9a5cb995316c7c71",
69
+ "docs_hash": "1925050f50fb6fc3",
70
70
  "headings": [
71
71
  "Add global and folder instructions",
72
72
  "Availability",
@@ -107,7 +107,7 @@
107
107
  }
108
108
  },
109
109
  "claude-code": {
110
- "docs_hash": "d373a3e4db1f4c79",
110
+ "docs_hash": "e85f8751f08026f0",
111
111
  "headings": [
112
112
  "Core concepts",
113
113
  "Documentation Index",
@@ -166,7 +166,7 @@
166
166
  "version": "2.1.220 (Claude Code)"
167
167
  },
168
168
  "cline": {
169
- "docs_hash": "d76db24ff58cb7ea",
169
+ "docs_hash": "3bbabe935a9e6ebb",
170
170
  "headings": [
171
171
  "API Reference",
172
172
  "Best Practices",
@@ -364,7 +364,7 @@
364
364
  }
365
365
  },
366
366
  "gemini-cli": {
367
- "docs_hash": "c6d93617f9359329",
367
+ "docs_hash": "1149b358f401b6b3",
368
368
  "headings": [
369
369
  "Breadcrumbs",
370
370
  "Directory actions",
@@ -444,7 +444,7 @@
444
444
  }
445
445
  },
446
446
  "google-antigravity": {
447
- "docs_hash": "03145f425dd475d1",
447
+ "docs_hash": "1e0c0d249231d2a9",
448
448
  "headings": [],
449
449
  "markers": {
450
450
  "AGENTS.md": false,
@@ -520,7 +520,7 @@
520
520
  }
521
521
  },
522
522
  "windsurf": {
523
- "docs_hash": "d57d379406804a4b",
523
+ "docs_hash": "56080b3b62a1de8e",
524
524
  "headings": [
525
525
  "Accounts",
526
526
  "Advanced",
package/bin/ai-toolkit.js CHANGED
@@ -281,7 +281,7 @@ function showHelp() {
281
281
  console.log(' remove --all Remove all installed plugins');
282
282
  console.log(' list Show available plugin packs with install status');
283
283
  console.log(' status Show currently installed plugins with data stats');
284
- console.log(' --editor <list> Runtime target: claude, codex, or all (default: claude)');
284
+ console.log(' --editor <list> Runtime target: claude, codex, cursor, gemini, or all (default: claude)');
285
285
  console.log('\nOptions for claude-app:');
286
286
  console.log(' export [--output FILE] [--no-custom-rules] [--verify]');
287
287
  console.log(' Build a ZIP for Customize > Plugins and Cowork instructions');
@@ -0,0 +1,168 @@
1
+ ---
2
+ title: "Retirement: rtk-pack — Broken On Install, Removed One Day After Shipping"
3
+ category: planning
4
+ service: ai-toolkit
5
+ tags:
6
+ - rtk
7
+ - plugin-pack
8
+ - token-reduction
9
+ - postmortem
10
+ - measurement
11
+ - release-process
12
+ doc_type: postmortem
13
+ status: completed
14
+ created: "2026-07-27"
15
+ last_updated: "2026-07-27"
16
+ shipped_in: "v4.19.0 (removal)"
17
+ description: "Why rtk-pack, shipped in v4.18.0, was removed in v4.19.0: the first real install proved every rewritten command failed with exit 127, the wrong architecture was fetched on Apple Silicon, and the pack's own status check reported both as green. Measured value was 0.0615% of input tokens, so neither defect was worth fixing."
18
+ ---
19
+
20
+ # Retirement: rtk-pack
21
+
22
+ **Shipped:** v4.18.0 (2026-07-26). **Removed:** v4.19.0 (2026-07-27).
23
+
24
+ The pack was installed on a maintainer's machine for the first time one day
25
+ after release. It did not work, in the strongest sense available: it broke the
26
+ shell.
27
+
28
+ ## Defect 1: every rewritten command failed with exit 127
29
+
30
+ rtk emits its rewrite as a bare `rtk git status`. The pack installs its binary
31
+ at `~/.softspark/ai-toolkit/plugin-scripts/rtk-pack/bin/rtk` and never puts that
32
+ directory on `PATH` — deliberately, so a checksum-pinned binary cannot shadow
33
+ anything system-wide. The two decisions are individually defensible and jointly
34
+ fatal: the shell could not find `rtk`, so every command the hook touched died
35
+ before running.
36
+
37
+ Observed on the first three commands issued after install:
38
+
39
+ | Command | Result |
40
+ |---|---|
41
+ | `cat ~/.softspark/ai-toolkit/plugins.json` | `command not found: rtk` |
42
+ | `git --no-pager diff` | `command not found: rtk` |
43
+ | `find ~/.softspark -path '*rtk*'` | `command not found: rtk` |
44
+
45
+ The blast radius is every family in `rtk --help`: `git`, `ls`, `read`, `find`,
46
+ `grep`, `rg`, `diff`, `docker`, `kubectl`, `npm`, `jest`, `tsc`. On the reference
47
+ workload that is 35% of Bash bytes, which is the same 35% the pack was built to
48
+ save. The mechanism that produced the benefit produced the outage.
49
+
50
+ ## Defect 2: the Intel build on an Apple Silicon host
51
+
52
+ `detect_platform()` trusted `platform.machine()`. The maintainer's `python3` is
53
+ an Intel Homebrew build at `/usr/local/opt/python@3.14`, so it runs under
54
+ Rosetta 2, where every architecture API inside the process reports `x86_64` —
55
+ `platform.machine()`, `os.uname()` and `uname -m` alike. The pack fetched
56
+ `rtk-x86_64-apple-darwin.tar.gz` onto an `arm64` machine and ran it emulated.
57
+
58
+ `sysctl.proc_translated` answers the question that distinguishes the two cases
59
+ and was not consulted. Note that CI had already met Rosetta on this project:
60
+ commit `30614ca`, *"build x86_64-darwin on arm64 and verify it under Rosetta"*.
61
+ The build pipeline knew. The install path did not.
62
+
63
+ ## Defect 3: the pack's own health check called both of them green
64
+
65
+ `plugin status` reported the binary present, the digest recorded, `runs: rtk
66
+ 0.44.0`, the hook script present and the hook registered. All true, all useless.
67
+ The pack's `status.py` was written specifically to *"distinguish installed from
68
+ working"*, and it checked only the installed half.
69
+
70
+ `kb/procedures/post-release-testing-sop.md`, written the day before the release,
71
+ requires exactly the missing step:
72
+
73
+ > Presence is not function. Drive the hook directly.
74
+
75
+ The SOP was written and not run. Every defect above would have surfaced in its
76
+ first five minutes.
77
+
78
+ ## The number that made fixing it not worth it
79
+
80
+ All three defects were fixed and tested before the removal decision: a `PATH`
81
+ prefix on the emitted command, `sysctl.proc_translated` in the detector, and a
82
+ status check that executes what the hook emits and looks for 127. Six tests,
83
+ all passing, all failing against the previous code. The work was not hard.
84
+
85
+ It was measured against this, from `rtk-pack-integration-20260726.md` §10.1:
86
+
87
+ | | |
88
+ |---|---:|
89
+ | Measured saving | 1.44 MB = 360,529 tokens |
90
+ | As a share of input tokens | **0.0615%** |
91
+ | Kill number, published before the measurement | 0.05% |
92
+ | Margin | ×1.23 |
93
+
94
+ The integration plan's own verdict on that margin was *"a pass, not a
95
+ vindication"*. A pack that survives its kill number by 23%, carries a
96
+ supply-chain surface, an upstream-sync SOP, a cross-build workflow for five
97
+ targets, and a hook that rewrites every command before it runs, is not worth
98
+ three defect classes discovered on first contact. The cost side moved; the
99
+ benefit side never did.
100
+
101
+ ## Why the ceiling was always low, independent of any defect
102
+
103
+ `Read` results are 62.8% of tool-result bytes on this traffic, and `rtk read`
104
+ measures 0.0% — at its default `--level none` it returns files verbatim. Tool
105
+ results are 4.54% of input token volume, so that is the arithmetic ceiling for
106
+ any tool-output mechanism here, and rtk addresses 8.8% of it.
107
+
108
+ Replayed against upstream's own claims, the families that can be measured
109
+ deliver 25.5% in aggregate against a claimed 60–90%:
110
+
111
+ | Family | Measured | Claimed |
112
+ |---|---:|---:|
113
+ | `rtk find` | 35.6% | 70% |
114
+ | `rtk git` | 33.1% | 70% |
115
+ | `rtk grep` | 22.3% | 75% |
116
+ | `rtk rg` | 7.0% | 75% |
117
+ | `rtk read` | 0.0% | 60% |
118
+
119
+ Better engineering does not move any of this. The lever is in the wrong place.
120
+
121
+ ## The process lesson
122
+
123
+ The previous retirement in this series
124
+ ([output-filter-retirement-20260726.md](output-filter-retirement-20260726.md))
125
+ concluded that premise validation must come first, with a kill number published
126
+ before the measurement. rtk-pack did that, and did it well: Phase 0 ran on 1224
127
+ transcripts before any build work, the kill number was published in advance, and
128
+ Phase 3 was cut on a measured 0.008%.
129
+
130
+ It then shipped without anyone installing it.
131
+
132
+ Measurement discipline and release discipline are different disciplines, and
133
+ this project now has one clean failure of each. The first shipped a feature that
134
+ worked and saved nothing. The second shipped a feature that would have saved
135
+ something and did not work. The next plan of this shape needs both gates, and
136
+ the second one is the cheap one: install the artifact, run the thing, look at
137
+ what happens.
138
+
139
+ ## What survives
140
+
141
+ - **Multi-runtime pack hook wiring.** Packs write user-scope entries to
142
+ `~/.cursor/hooks.json` and `~/.gemini/settings.json`, tagged per pack, with
143
+ `plugin remove` taking only its own back out. Generic; no rtk in it.
144
+ - **`supported_editors` in the manifest.** A pack declares the runtimes it works
145
+ on instead of installing everywhere and silently doing nothing.
146
+ - **Generic `plugin status` dispatch.** Any pack can ship `scripts/status.py`.
147
+ The lesson attached: a status check must prove the working half by exercising
148
+ it.
149
+ - **Version-aware `plugin update`.** A pack whose manifest has not moved is a
150
+ silent no-op.
151
+ - **`audit_skills.py --ci` and the ShellCheck gate now cover `app/plugins/`.**
152
+ - **[Post-Release Testing SOP](../../procedures/post-release-testing-sop.md).**
153
+ Kept, and now carries the note that the one time it existed and was skipped,
154
+ this happened.
155
+
156
+ ## What was removed
157
+
158
+ `app/plugins/rtk-pack/`, `.github/workflows/rtk-build.yml`,
159
+ `scripts/verify_rtk_binary.py`, `tests/test_rtk_pack.bats`,
160
+ `tests/test_verify_rtk_binary.bats`, `kb/procedures/rtk-upstream-sync-sop.md`,
161
+ and the GitHub Release `softspark-rtk-v0.44.0-1` holding the five cross-built
162
+ binaries.
163
+
164
+ Anyone who installed the pack under v4.18.0 should run
165
+ `ai-toolkit plugin remove rtk-pack`. With the release deleted, a fresh
166
+ `plugin install rtk-pack` on v4.18.0 fails at the fetch and leaves the pack
167
+ inert rather than half-installed, which is the degraded path the pack was
168
+ designed for.
@@ -0,0 +1,217 @@
1
+ ---
2
+ title: "SOP: Post-Release Testing"
3
+ category: procedures
4
+ service: ai-toolkit
5
+ tags: [sop, post-release, smoke-test, npm, sandbox, plugin-pack, provenance, isolation]
6
+ version: "1.0.0"
7
+ created: "2026-07-26"
8
+ last_updated: "2026-07-26"
9
+ description: "Smoke-test a published @softspark/ai-toolkit release from npm in an isolated HOME and npm prefix, without touching the maintainer's real install. Covers provenance, CLI, doctor, and the full plugin-pack lifecycle including the degraded-install path. Written for v4.18.0 and not run; v4.18.0 shipped a pack that broke every command it touched, and every step here would have caught it."
10
+ ---
11
+
12
+ # SOP: Post-Release Testing
13
+
14
+ Runs **after** `publish.yml` succeeds on a tag. Verifies the artifact users will
15
+ actually install, from npm, rather than the working tree.
16
+
17
+ Sibling procedures exist for `jira-mcp` and `legal-pl-pack`; this is the
18
+ ai-toolkit equivalent. It complements
19
+ [Release Verification](release-verification-sop.md), which checks the toolkit
20
+ from the maintainer's own installed copy. The difference that matters: this one
21
+ never writes to the maintainer's `~/.claude` or `~/.softspark`.
22
+
23
+ **Time:** 10 minutes.
24
+
25
+ ## Why isolation is the first step, not a detail
26
+
27
+ The toolkit installs into `$HOME`. Testing a release against your own HOME
28
+ means the test either pollutes your working setup or, worse, passes because of
29
+ state your setup already had. Both make the result meaningless.
30
+
31
+ Every command below runs against a throwaway HOME and a throwaway npm prefix.
32
+ Nothing is global.
33
+
34
+ ## Phase 1: Sandbox
35
+
36
+ ```bash
37
+ SB=$(mktemp -d)
38
+ mkdir -p "$SB/home" "$SB/npm"
39
+ export HOME="$SB/home"
40
+ AT="$SB/npm/bin/ai-toolkit"
41
+ echo "sandbox: $SB"
42
+ ```
43
+
44
+ Record the real state now, so Phase 7 can prove it is unchanged:
45
+
46
+ ```bash
47
+ python3 -c "
48
+ import json, pathlib
49
+ p = pathlib.Path('$SB/../real-before.json')
50
+ import os
51
+ home = pathlib.Path(os.path.expanduser('~'))
52
+ " 2>/dev/null
53
+ # Simpler: note what exists today.
54
+ cat ~/.softspark/ai-toolkit/plugins.json 2>/dev/null
55
+ ```
56
+
57
+ ## Phase 2: Provenance
58
+
59
+ Do this before installing anything: an unsigned publish is a release-blocking
60
+ regression, and there is no point smoke-testing a build you would have to redo.
61
+
62
+ ```bash
63
+ VERSION="X.Y.Z"
64
+ npm view "@softspark/ai-toolkit@${VERSION}" --json \
65
+ | python3 -c "
66
+ import json, sys
67
+ d = json.load(sys.stdin); att = d['dist'].get('attestations', {})
68
+ pt = att.get('provenance', {}).get('predicateType')
69
+ assert pt == 'https://slsa.dev/provenance/v1', f'NO PROVENANCE: {pt}'
70
+ print('PROVENANCE OK:', att['url'])
71
+ "
72
+ ```
73
+
74
+ ## Phase 3: Install from npm
75
+
76
+ ```bash
77
+ npm install -g --prefix "$SB/npm" "@softspark/ai-toolkit@${VERSION}"
78
+ "$AT" --version # must equal VERSION
79
+ "$AT" --help >/dev/null && echo "help OK"
80
+ ```
81
+
82
+ ## Phase 4: Core surfaces
83
+
84
+ ```bash
85
+ "$AT" install # full global install into the sandbox HOME
86
+ "$AT" doctor # must end: Errors: 0 | Warnings: 0
87
+ "$AT" status
88
+ "$AT" plugin list # pack count must match app/plugins/
89
+ ```
90
+
91
+ **A doctor run before `install` reports `agents directory missing` and
92
+ `skills directory missing`.** That is the sandbox being empty, not a defect.
93
+ Install first, then judge doctor.
94
+
95
+ ## Phase 5: Plugin-pack lifecycle
96
+
97
+ Run this for any pack the release touched. For a pack that downloads a binary,
98
+ every step below has caught a real defect at least once.
99
+
100
+ ```bash
101
+ "$AT" plugin install <pack>
102
+ "$AT" plugin status # does it report itself working?
103
+ find "$HOME/.softspark/ai-toolkit" -path '*<pack>*' # what actually landed
104
+ ```
105
+
106
+ **Verify:**
107
+ - [ ] `plugin status` distinguishes *installed* from *working*, not just present
108
+ - [ ] For a binary pack: the binary runs and reports the pinned upstream version
109
+ - [ ] The hook is registered in `~/.claude/settings.json` with the pack's `_source`
110
+
111
+ **Does it do its job?** Presence is not function. Drive the hook directly:
112
+
113
+ ```bash
114
+ printf '%s' '{"tool_name":"Bash","tool_input":{"command":"git status"}}' \
115
+ | bash "$HOME/.softspark/ai-toolkit/hooks/plugin-<pack>-<hook>.sh"
116
+ ```
117
+
118
+ **Update path:**
119
+
120
+ ```bash
121
+ "$AT" plugin update --editor claude --all # current version: silent
122
+ "$AT" plugin update --editor claude --all --dry-run # says "up to date"
123
+ # force a stale marker, then confirm it updates and re-records:
124
+ python3 -c "
125
+ import json, pathlib, os
126
+ p = pathlib.Path(os.environ['HOME'] + '/.softspark/ai-toolkit/plugins.json')
127
+ d = json.loads(p.read_text()); d['targets']['claude']['versions']['<pack>'] = '0.0.9'
128
+ p.write_text(json.dumps(d, indent=2))
129
+ "
130
+ "$AT" plugin update --editor claude --all # reports 0.0.9 -> <version>
131
+ "$AT" update # core update leaves a current pack alone
132
+ ```
133
+
134
+ **Removal must be complete:**
135
+
136
+ ```bash
137
+ "$AT" plugin remove <pack>
138
+ find "$HOME/.softspark" -path '*<pack>*' | wc -l # must be 0
139
+ python3 -c "
140
+ import json, pathlib, os
141
+ d = json.loads(pathlib.Path(os.environ['HOME'] + '/.claude/settings.json').read_text())
142
+ print('hook still present:', '<pack>' in json.dumps(d.get('hooks', {})))
143
+ "
144
+ "$AT" plugin install <pack> # re-install must work
145
+ ```
146
+
147
+ ## Phase 6: The degraded path
148
+
149
+ **This is the step most worth keeping.** A pack that fetches anything can fail
150
+ to fetch, and the failure mode must be inert rather than broken or silent.
151
+
152
+ No pack in the toolkit fetches anything today. Run this phase if one ever does
153
+ again, pointing its source-override variable at a dead URL:
154
+
155
+ ```bash
156
+ "$AT" plugin remove <pack>
157
+ <PACK>_RELEASE_BASE_URL="file:///nonexistent" "$AT" plugin install <pack>
158
+ ```
159
+
160
+ **Verify:**
161
+ - [ ] Install reports the failure in words a user can act on, and does not claim success
162
+ - [ ] No partial artifact is left behind
163
+ - [ ] The hook is still wired, and passes commands through untouched
164
+ - [ ] `plugin status` says the pack is inert and names the fix
165
+ - [ ] Re-installing without the broken source recovers
166
+
167
+ ## Phase 7: Prove the real environment is untouched
168
+
169
+ ```bash
170
+ python3 -c "
171
+ import json, pathlib
172
+ d = json.loads(pathlib.Path.home().joinpath('.softspark/ai-toolkit/plugins.json').read_text())
173
+ print('plugins.json:', d['targets']['claude'])
174
+ p = pathlib.Path.home() / '.claude/settings.json'
175
+ print('pack hook leaked into real settings:', '<pack>' in json.dumps(json.loads(p.read_text()).get('hooks', {})) if p.exists() else False)
176
+ print('pack paths in real ~/.softspark:', len(list(pathlib.Path.home().joinpath('.softspark').rglob('*<pack>*'))))
177
+ "
178
+ ```
179
+
180
+ All three must show the pre-test state.
181
+
182
+ ## Phase 8: Clean up
183
+
184
+ `guard-destructive.sh` blocks `rm -rf` on a `PreToolUse` hook, so removal goes
185
+ through an enumerated delete that reports what it removed:
186
+
187
+ ```bash
188
+ python3 -c "
189
+ import pathlib, shutil
190
+ sb = pathlib.Path('$SB')
191
+ assert sb.is_dir() and str(sb).startswith(('/tmp', '/var/folders')), sb
192
+ n = sum(1 for _ in sb.rglob('*') if _.is_file())
193
+ shutil.rmtree(sb)
194
+ print(f'removed {sb} ({n} files)')
195
+ "
196
+ ```
197
+
198
+ ## Success criteria
199
+
200
+ | Area | Criterion |
201
+ |---|---|
202
+ | Supply chain | `predicateType == https://slsa.dev/provenance/v1` |
203
+ | CLI | `--version` equals the tag, `--help` renders |
204
+ | Health | `doctor` after `install`: 0 errors, 0 warnings |
205
+ | Catalog | `plugin list` count matches `app/plugins/` |
206
+ | Pack install | Binary present, runs, digest verified, hook registered |
207
+ | Pack function | Driving the hook produces the expected effect, not just exit 0 |
208
+ | Pack update | Current version silent; stale version updates and re-records |
209
+ | Pack removal | Zero residue in `~/.softspark` and `settings.json`; re-install works |
210
+ | Degraded path | Fetch failure is inert, loud in status, and recoverable |
211
+ | Isolation | Real `~/.claude` and `~/.softspark` byte-identical to pre-test |
212
+
213
+ ## Related
214
+
215
+ - [Release Preparation](release-preparation-sop.md) — run before tagging
216
+ - [Release Verification](release-verification-sop.md) — the maintainer-install checks
217
+ - [rtk-pack Retirement](../history/completed/rtk-pack-retirement-20260727.md) — what happened the one time this SOP was written and not run
@@ -3,9 +3,9 @@ title: "SOP: Release Preparation"
3
3
  category: procedures
4
4
  service: ai-toolkit
5
5
  tags: [sop, release, version, publish, changelog, semver, provenance, sarif, ecosystem, shellcheck]
6
- version: "1.11.1"
6
+ version: "1.11.2"
7
7
  created: "2026-04-10"
8
- last_updated: "2026-07-24"
8
+ last_updated: "2026-07-27"
9
9
  description: "Step-by-step checklist for preparing a new ai-toolkit release — ecosystem-sync drift check, version sync, changelog, artifact regeneration, validation, and tagging. Run BEFORE every git tag. Includes mandatory Provenance, SARIF, and checksum-pin checks added in v2.8.0, the single-run npm test discipline added in v1.8.0, the ecosystem-sync gate added in v1.9.0, the registry-vs-generators drift gate added in v1.10.0, and the mandatory pre-tag ShellCheck gate added in v1.11.0 (publish.yml does not run ShellCheck, so a hook lint failure can publish while reddening main CI — see the v4.5.1 postmortem in Phase 5)."
10
10
  ---
11
11
 
@@ -295,7 +295,15 @@ diff \
295
295
  && echo "OK: registry matches filesystem" \
296
296
  || { echo "DRIFT: update supported-tools-registry.md before tagging"; exit 1; }
297
297
 
298
- # Run npm test ONCE, cache output, parse from file. The suite is 900+ bats
298
+ # Stage first IF this release adds or deletes a kb/ file. The test
299
+ # "npm package KB files match the tracked release set" compares `git ls-files
300
+ # kb` (the index) against what `npm pack` sees (the working tree), so an
301
+ # unstaged addition reads as "extra" and an unstaged deletion as "missing".
302
+ # Phase 6 stages, and it runs after this one, so the ordering fails the test
303
+ # for any release that touches the KB. Staging early costs nothing.
304
+ git status --porcelain kb/ | grep -qE '^(\?\?| D|\?M)' && git add -A kb/
305
+
306
+ # Run npm test ONCE, cache output, parse from file. The suite is 1400+ bats
299
307
  # cases — rerunning it per check wastes minutes. Do not pipe npm test into
300
308
  # tail/grep multiple times in the same session.
301
309
  npm test > /tmp/npm-test.log 2>&1
@@ -452,7 +460,7 @@ git push origin --delete vX.Y.Z
452
460
  | 13 | ShellCheck hooks | `shellcheck --severity=warning app/hooks/*.sh` | Exit 0, no output (mirrors ci.yml; publish.yml does NOT run it) |
453
461
  | 14 | Provenance flag check | `grep -- '--provenance' .github/workflows/publish.yml` | Present |
454
462
  | 15 | Checksum-pin backfill | `sources.json` entries all have `sha256` | No unpinned URL sources |
455
- | 16 | Tests | `npm test` | All pass |
463
+ | 16 | Tests | `git add -A kb/` if the KB changed, then `npm test` | All pass |
456
464
  | 17 | Commit | `git commit` | Clean working tree |
457
465
  | 18 | Tag | `git tag vX.Y.Z` | Tag exists |
458
466
  | 19 | Push | `git push origin main --tags` | CI triggered with `id-token: write` |