Skip to content

Change Ledger

Structured record of toolkit changes — the "why" behind the "what". Read by /vt-c-doc-sync to generate rich What's New content.


2026-08-02

SPEC-163 (W5–W8): the split lands — core-standardsvt-base — v5.0.0

Scope shipped: the destructive half of the plugin split. 132 skills, 36 commands and 62 agents partitioned across eight plugins, each with its own prefix (vt-c- base, vt-d- product-dev, vt-o- office, vt-t- toolkit, vt-p- pm-intake, vt-b- brand, vt-v- vms, vt-f- finance); the base plugin renamed; no aliases and no deprecation window, per Amendment A1 — Claude Code keys skill identity on the SKILL.md name: field, so a second symlink cannot alias a skill, and the redirect-directory fallback was rejected for inflating the exact surface being shrunk. Migration guide: docs/guides/migration-5.0.md. Why the rename had to be one commit (AD-3): resolve_plugins() hard-fails on a dependency directory that does not exist, so a plugin still naming core-standards after the rename aborts the whole install, not just its own. plugins/core-standardsplugins/vt-base, every depends_on, the registry key, the plugin's own name, and ~200 live files moved together. The blanket rename rewrote three things it should not have, all one lesson — a token sweep cannot distinguish the thing being renamed from a string that merely contains its name. The spec directory 163-core-standards-plugin-split was rewritten in 10 files, so the assignment TSVs resolved nowhere and four suites failed at their first line; split_plugins() held core-standards vt-base deliberately (to work either side of the rename) and collapsed to vt-base vt-base, double-counting the base's 15 skills; and test_base_rename.bats used the literal core-standards as the value that must NOT appear, so the sweep rewrote the forbidden value into the expected one and inverted three assertions. The forbidden literal is now assembled at runtime ("core-" "standards") so no future sweep can reach it.

SPEC-163 review pass — four defect classes the suite could not see

Verdict was FAIL. The skill and command halves verified exactly — 132 and 36, zero lost, duplicated or colliding, every name: matching its manifest LHS, every plugin.json reconciling. Everything below is the agent layer and the tests. (1) Agent identity is a FIFTH reference class. The spec's "four classes of reference" taxonomy — deployed name, hardcoded directory path, self-relative path, composed path — is entirely about skill references. W6 Task 25 rewrote 13 agents' name: frontmatter to match their prefixed manifest LHS while all 43 base agents and the 6 vms agents kept bare names. Agents key on name: exactly as skills do, so persona-select.sh — which emits document-quality-reviewer — dispatched to a subagent_type that no longer resolved for every document / presentation / research / project-plan review. rewrite-skill-refs.sh does not touch agent names, prune_orphaned_links() cannot reach a nested agent path, and test-persona-selection.sh pins the OLD names, so the suite stayed green across the break. Task 25 never asked for the rename; Task 26 asked for it explicitly for commands, and commands are 36/36 correct. Reverted to bare, plus finance/budget-analyst (name: Budget Analyst — spaces and capitals, deployed for the first time by Task 28) and vms/ims-writer, which the review missed. Guard: name: == bare basename, derived from every manifest with a ≥55 non-vacuity floor. (2) The pruner could not reach agents at all. prune_orphaned_links() globbed "$dir"/* and skipped anything not a symlink — but every agent link lives in agents/<category>/, so the glob yielded real directories and the agents branch never inspected a single link. Independently, is_toolkit_owned() matched ~/.claude/agents/vt-?-*, which a nested path cannot match, so the toolkit's own agent links classified as USER-owned and were unprunable on two counts. A 4.x → 5.0 upgrade left 12 dead agent links pointing into the deleted core-standards — measured, and live on the maintainer's machine. The guard test probed with -maxdepth 1, the exact depth at which zero agent links exist, and seeded only a skill as its stale fixture, so its "no dangling symlinks remain" assertion was vacuous for agents. Fixed with find -type l + a $dir-relative name (the manifest LHS is subdir-qualified) + a nested is_toolkit_owned arm; -maxdepth 1 dropped; a must-prune and a must-NOT-prune fixture now sit at the same depth, because widening what may be deleted must never start deleting the user's own agents. (3) Four tests were structurally inert — they could not fail for any input. test_product_scope_paths looped over vt-base/skills/*/SKILL.md where zero of 15 carry scope: product: (W5 moved all four scoped skills out), so every iteration hit continue; the W8 sweep had rewritten the directory in the glob without noticing the skills had left it. test_deprecated_retirement greped vt-base/skills/kw-prd/SKILL.md, which does not exist — grep exits 2, the if reads that as "no marker found", so all three A2-held skills could be marked deprecated silently. test_base_rename used cmd && { echo; false; } || true, where || true converts the failure case to a pass and it was the body's last command. test_hook_deploy greped secret-scanner.sh, which occurs six times in pre-commit — five in comments and error strings — so deleting the one line that is the fallback still passed. All four re-anchored and mutation-verified: each fix was proven to fail against a seeded defect, and each mutation was proven not to be a no-op first (the BUG-023/024 trap). (4) Two installer defects. do_backup() still recreated $CONFIGS/skill-symlinks.manifest — the file Task 34 deleted — from whatever was live in ~/.claude, making a machine-derived second source of truth for deployed names and the one path that could still materialise an alias under A1. Its guard test searched for the literal user-global/skill-symlinks, which that line does not spell, so it certified the file had no writer while the writer sat 40 lines away in the same script. And scripts/install.sh was an orphaned second installer: it pointed ~/.claude/plugins/company-claude-toolkit at plugins/vt-base while setup.sh points it at TOOLKIT_ROOT, and the deployed SessionStart hook resolves through that link into configs/user-global/hooks/ — a directory that does not exist under vt-base. It also created zero ~/.claude symlinks and overwrote the user's CLAUDE.md (the SPEC-124 clobber). Retired to a redirect; the three docs referencing it updated, including the WSL copy-mode row, which was already non-functional. Undeclared cross-plugin dependencies (H3), corrected downward. The review reported "36 edges"; that was a count of references. Derived per source→target pair, the real undeclared set is two: vt-office → vt-brand and vt-product-dev → vt-brand, across four skills that read ~/.claude/skills/vt-b-… CSS tokens, logos and validate_document.py at run time. Both now declare it. The review also flagged the required base reaching into optional plugins (implementation-orchestrator → persona-select, check_registration.shaggregate.sh); those resolve against $TOOLKIT_ROOT/plugins/…, the repo tree, where every plugin directory exists regardless of what is installed — not broken, and deliberately not flagged by the new guard, which tests only the deployed-path form that can actually be missing. Counts stopped being literals. scripts/docs-gen/macros.py had been narrowed to vt-base's own 15 / 43 / 7 while every consumer renders them as toolkit-wide, so by-role.md opened with "The toolkit ships 15 skills" against a real 132. A literal cannot distinguish "the base plugin's count" from "the toolkit's count". The macros now derive from the per-plugin manifests at build time (132 / 62 / 36 / 8 / 8 plugins), which makes the next split a no-op here. A false claim in the W8 commit message, corrected. It reported Suite: PASS=92 FAIL=3 — all pre-existing and boasted "the suite caught every one". plugins/vt-base/tests/test-review-gates.sh was failing 37 of 69 checks: W8's rename repointed its PLUGIN_ROOT at vt-base while W5 had already moved workflow-4-review, workflow-5-finalize, promote and six others to vt-product-dev/vt-pm-intake. Every sibling test file was correctly repointed; this one was missed. It is not green on main either — it fails the same 3 assertions there, so it was already one of the nine pre-existing reds, and the branch had taken it from 3 to 37. Restored to the main baseline of 3. Guards added: agent name: == bare basename (all plugins, ≥55 floor) · nested-agent prune + user-owned nested link preserved · is_toolkit_owned nested fixtures both ways · deployed-path cross-plugin reference implies declared depends_on (≥10-edge floor) · install.sh creates nothing and exits non-zero · $CONFIGS/*-symlinks.manifest has no non-comment writer · product-scope corpus ≥4 · retirement checked across all plugins and all prefixes.


2026-07-29

SPEC-163 Amendment A3: product scoping matched nothing — v4.0.1

Discovery: A3 was drafted at W3 as "drop W4, FR-8 is already satisfied". Checked against disk, two of its three premises were false. scope: product: restricts Claude's auto-matching to matching repo paths (docs/concepts/skill-scoping.md); it does not gate deployment — setup.sh reads it only for a paths: drift check — so all three product-scope skills were deployed to ~/.claude and FR-8's "not global deploy" was never true. And intake/projects.yaml does register VisiMatch (C001, C035) and VisiFair repos, so relocation targets existed. Only the third premise held, weakly: visitrans_cd is identically scoped and stays in the manifest, but it spans four products including the VisiTrans umbrella, where these three are single-product. The defect neither the plan nor A3 saw: every glob in projects.yaml's products: block encoded the repository parent-directory numbering from before the renumber — VisiTrans **/03-VisiTrans/** (real 01-VisiTrans/), VisiMatch **/04-VisiMatch/** (real 02-VisiMatch/), VisiFair **/04-VisiFair-*/** (real 03-VisiFair/), VisiArea **/04-VisiArea-*/** (real 04-VisiArea/). 04- is VisiArea's prefix, so the VisiMatch glob pointed at a different product. Net effect: the four product-scoped skills deployed everywhere and auto-matched nowhere — the opposite of FR-8 in both halves, silently, for months. The projects: paths were corrected at renumber time (93293d43); these were missed because nothing tested them, and a stale glob is indistinguishable from a correct one by inspection. Fix: globs corrected, every generated paths: block regenerated (container-logistics-ux-expert, messegelaende-cleanup, skill-venue-hall-research, visitrans_cd). W4 is still dropped, but on inverted reasoning: its remaining action — deleting the three manifest lines — would stop them deploying at all, and no mechanism carries them to the product repos in exchange. The three are assigned to vt-product-dev (62 → 65) and keep scope: product:; ownership and scoping are orthogonal — the plugin decides where a skill lives, scope: decides where Claude offers it, and conflating them is what produced the original A3. FR-8 amended from "product-scope … not global deploy" to "product-scope and deploy through the owning plugin": the original wording described a capability SPEC-123 does not provide. Not done: relocating into the product repos — cross-repo work across three targets, excluded by the standing rule that a V025 spec whose deliverable lives elsewhere is flagged, not activated. Also fixed: the generated skill-{orchestration-map,audit-inventory}.{md,json} shipped inside 4.0.0 still stamped v3.54.0 — regenerated during merge-conflict resolution before the version was settled, and never re-run. Surfaced only because the A3 SKILL.md changes made the pre-commit hook flag them. Guard: test_product_scope_paths.bats checks the globs against the registry's own registered project paths — no filesystem dependency, so it works on any machine — and asserts no generated block still carries a pre-renumber prefix. Red against the stale globs, green after. test_assignment_reconciliation.bats pins all three to vt-product-dev by name and separately asserts they keep scope: product:, so the two mechanisms cannot be collapsed again. 12/12 + 3/3. PATCH, not MINOR: no skill added, removed or renamed; count stays 119. Every row of skill-assignment.tsv now names a real plugin — no marker targets remain, which is the precondition W5 needed. Files: intake/projects.yaml, plugins/core-standards/skills/{container-logistics-ux-expert,messegelaende-cleanup,skill-venue-hall-research,visitrans_cd}/SKILL.md, specs/163-core-standards-plugin-split/{spec.md,skill-assignment.tsv}, tests/spec-163/{test_product_scope_paths.bats,test_assignment_reconciliation.bats}, docs/evaluations/dev-flow-plugin-skill-map.md, docs/skill-{orchestration-map,audit-inventory}.{md,json}, plugins/{registry.json,core-standards/.claude-plugin/plugin.json,core-standards/CHANGELOG.md}, docs/plugins/core-standards.md, scripts/docs-gen/macros.py.

SPEC-163 (W0–W3): core-standards split — foundation — v4.0.0

Scope shipped: the non-destructive foundation of the plugin split — deployment enablement, five registered-but-empty plugin scaffolds, the retirement of nine deprecated skills, and the mechanical reference-rewrite tool. No skill has been renamed or moved; every surviving command works exactly as before. W5–W8 (the ~119 renames and core-standardsvt-base) are NOT in this release. Why 4.0.0 and not 3.54.0: W3 deletes nine skills that deployed as working commands. The constitution classes a skill removal as a breaking change to skill contracts and requires deprecation to precede removal by ≥1 MINOR. All nine carried a [DEPRECATED]/[SUPERSEDED] marker naming their replacement through several MINOR releases, which makes the removal permissible — it does not make it minor. The single precedent for removing a skill inside a MINOR (SPEC-158, v3.51.0) rested on an explicit never-functional-exception waiver: the skill and command deleted there were never invocable, so no live contract existed. That reasoning does not transfer — /vt-c-kw-0-start and its eight siblings resolved and ran. The prior checkpoint's "W0–W3 is shippable as a MINOR" was therefore wrong, and the spec's plan to spend 4.0.0 at W8 is superseded: W5–W8 will need 5.0.0. Migration guide: docs/guides/migration-4.0.md (the constitution requires one for a MAJOR). Three amendments, all from premises that failed on contact with disk. A1 — aliases withdrawn: the W0 runtime spike (a hard plan gate) returned FAIL — Claude Code keys skill identity on the SKILL.md name: field, not the deployed directory name, so a second symlink does not alias a skill. The fallback (one real redirect directory per alias) was priced at 113→133 visible skills and rejected, since it inflates exactly the surface being shrunk and voids the premise that aliases stay invisible to generators and count tests. AD-6's mechanical rewrite alone now satisfies FR-11, which is why R-10 was raised to Critical: a missed reference is a dead command, not a stale mention. A2 — FR-9 retires nine, not twelve: kw-prd (337 lines), kw-prototype (607) and kw-user-test (695) carry no deprecation marker and are not stubs; their pd-* counterparts are the same job plus design-workflow wiring, not supersets in substance. Deleting them would have removed live capability with no warning window, violating the one-MINOR rule this release otherwise honours. This invalidated the plan-gate's constitution check, which had asserted all twelve were stubs. A3 (recommended, unwritten at release) — drop W4: believed at the time to be because FR-8 was already satisfied via SPEC-123. [Updated 2026-07-29: A3 has since been written and two of its three premises were false — scope: product: restricts auto-matching, not deployment, so FR-8's "not global deploy" was never satisfied; and relocation targets do exist in intake/projects.yaml. W4 is still dropped, on different grounds. See spec Amendment A3.] Two further premise failures, this time in the handoff note itself (A4): the checkpoint listed three count discrepancies as W5 blockers; two did not hold. vt-base 17 vs FR-1's 15 was never a conflict — 17 = 15 + the two brand skills, and FR-4 had already resolved that judgment call by giving them their own vt-brand plugin. gc was never unassigned — it sits in the map's vt-toolkit bucket, whose twelve listed names are exactly twelve. Verified mechanically that the union of every map bucket is exactly the pre-W3 128-entry manifest. Only "the total is 119" held. Running tally: six premises checked, three false, and the false ones were as likely to sit in a handoff note as in the spec — a handoff note is not evidence. Reference-rewrite tool (AD-6), the load-bearing piece: with aliases withdrawn this is the only thing keeping references alive across renames. The first implementation was O(files × names × 3 processes) — per file per name it spawned shasum twice and perl once, plus a grep -Iq . per file merely to build the candidate list — measuring 8m47s for seven names, extrapolating to hours per W5 row. Rewritten so one generated perl program carries every substitution and runs once per file (batched by xargs) behind a single batched grep -l -I -F -f pre-filter that also absorbs the binary check: 6.7s for a full 119-name rewrite (866 files), 6.0s for --check. Three earlier attempts had been reverted, both failure modes silent (exit 0, "0 file(s) changed", nothing rewritten): an END block overriding the exit status, and a payload placed inside printf's format string emitting a literal \n. This implementation is immune by construction (no END block — the pre-filter is grep; every generated line passes its payload as an argument to %s), and belt-and-braces the program is perl -c checked before use with any non-zero perl exit fatal. It also fixed a latent cascade bug: sequential per-name passes rewrote already-rewritten text, so a map with alpha→beta and beta→gamma carried a live alpha through to gamma. Validated against the fixture suite and at real scale: the 7-name --check produces an identical unique hit set to the old tool (1361 = 1361, zero under/over-reach), and a full 119-name rewrite over a repo copy leaves --check clean, all eight history files byte-identical, and all nine real longer tokens sharing a mapped prefix untouched. Rehearse on a copy — .claude-checkpoint.md is gitignored and git checkout would not restore it. Assignment made executable (A4/A5): W5's correctness claim is a set claim — after the last row the union of the per-plugin manifests must equal the pre-split manifest exactly. Prose in an evaluation appendix had already drifted twice, so the assignment now lives in specs/163-*/skill-assignment.tsv, pinned by test_assignment_reconciliation.bats (union == manifest derived at runtime, per-plugin counts == their FR numbers, no duplicates, no ghosts; every absence assertion mutation-checked). A5 closed A2's open question: the three held skills go to vt-product-dev (59 → 62). New evidence A2 lacked — A2 measured body sections, but the frontmatter descriptions are byte-identical for kw-prd/pd-2-prd and kw-prototype/pd-3-prototype, so selection between them is arbitrary. That is a live defect independent of this split; keeping each pair in one plugin makes it one owner's problem. Reconciled: 15 + 2 + 62 + 13 + 1 + 12 + 11 + 3 = 119. Three W3 regressions found by the first full suite run since W2, and fixed. The retirement sweep had mechanically replaced retired names inside historical and frozen records — the exact failure mode AD-6's history-file exclusion exists to prevent, applied to file classes never added to the exclusion list. (a) Two frozen legacy specs/*/plan.md were semantically falsified: each held a redirect table whose left column named the skill being retired, and the sweep rewrote that column too, so rows came to describe the replacement redirecting to itself while their file-path column still pointed at the retired skill's directory — reddening SPEC-143's T4 bit-identity gate. Restored, and specs/*/plan.md added to is_excluded(); this matters most for W5, which runs the same sweep ~119 names per row and would falsify every spec plan at ~17× the scale while --check reported clean. (b) test_calibration.bats D8's deprecated-stub fixture was hardcoded, then substituted for a live skill when the original was retired, so the test went red while looking maintained; now derived from the screen's own output with a loud empty-corpus guard (exactly one skill still qualifies). (c) test_plugin_scaffolds.bats passes 11/11 but takes 7m21s against the runner's 300s default, putting it in the state the repo's testing rules forbid — passing alone, failing in suite. Fixed with a per-suite budget table rather than a global raise, because three suites hang outright under BUG-006 and every added second is spent three times per run waiting on a known hang. Merge reconciliation: main had moved 27 commits to 3.53.0 (SPEC-165). History is criss-cross — two merge bases — so a single-base diff overstated the overlap at 19 files; the real 3-way conflict set was 6, as git merge-tree predicted. The four generated skill-{orchestration-map,audit-inventory}.{json,md} were resolved by regeneration, not hand-merging, under UTF-8 (never LC_ALL=C, which byte-truncates multi-byte descriptions and fabricates a diff). That merge also exposed a real gap: registry.json auto-merged to 3.53.0 while plugin.json moved on, and the anti-drift test whose entire subject is "the surfaces cannot drift apart" passed — it looped only over the five new scaffolds and omitted core-standards, the highest-coupling entry. The N-copy/(N−1)-test gap, found the hard way; now closed and mutation-checked. Bugs: 13 filed across the spec, 4 fixed here — BUG-013 (critical, --plugins dead on bash 3.2 via local -A, which blocked SC-1/SC-2, the spec's headline claim), BUG-009 (high, setup.sh baking worktree paths into ~/.claude; three occurrences in four days, 248 dead links), BUG-011 (vms half — /vt-v-draft now deploys), BUG-002. Still open and NOT claimed fixed: BUG-012 — main labels it "fix owned by SPEC-163", but --backup still writes live state into the repo and W0–W3 did not touch it; BUG-006 (setup.sh blocks on non-TTY stdin — do not paper over with `