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-13

BUG-031: the deliverable-type vocabulary is now closed — vt-product-dev 1.2.1, vt-base 6.2.2

What was broken. persona-select.sh matched deliverable types by exact string identity, so a spec declaring docs — or doc, documents, documentation, or any mixed case — did not match the single spelling document and silently lost document-quality-reviewer. Worse than a missing reviewer: an unrecognized token produced no warning and exit 0, so the review ran at the constitutional floor and reported success. A spec author had no way to tell "reviewed thoroughly" from "reviewed by nobody".

Why it stayed invisible. The vocabulary was open — anything unrecognized was simply ignored. Two shipped specs proved it: SPEC-141 declared [skill, script, reference] and SPEC-140 declared [script, skill]; not one of those five tokens was ever legal, so both specs were reviewed at the floor for months without a single diagnostic. The fix closes the vocabulary (12 legal types), canonicalizes the aliases, and makes an illegal token exit non-zero naming the offender.

Two findings reproduced BUG-031 inside its own fix, which is the part worth remembering: --types "," returned output byte-identical to the unfixed selector because the whitespace guard ran before tr turned commas into whitespace; and for t in $norm_types did pathname expansion, so --types '*' selected reviewers based on what files happened to sit in the working directory. A normalizer that runs in the wrong order relative to its matcher is the same defect class the bug describes.

Dogfooding note. document-quality-reviewer was selected for this change only because of the fix under review — on the pre-fix selector this half-prose change would have had no document reviewer at all, and that reviewer found the stale-section defect in the write-up.

Guarded, not just fixed. New scripts/tests/setup/58-deliverable-types-are-declared.sh checks every declaration in the registry and in specs/*/{spec,plan,tasks}.md against the vocabulary (174 keys, 241 tokens at merge — the denominators track the corpus and are printed every run, so they move as specs land). test-persona-selection.sh gains section T12 plus a MIN_ASSERTIONS floor, because deleting a test previously left the suite green at a lower count.

Key files: plugins/vt-product-dev/skills/workflow-4-review/scripts/persona-select.sh, scripts/tests/setup/58-deliverable-types-are-declared.sh, plugins/vt-base/tests/test-persona-selection.sh, .design-state.yaml, specs/139-ims-workflow-gates/spec.md.

BUG-067: the count-drift detection signal is derived per plugin, and now executed — docs + scripts (no version bump)

critical-patterns.md P-003 published grep -c "^vt-c-" skill-symlinks.manifest as the detection signal for components.* drift. It returns 0 for seven of eight plugins, and 0 reads exactly like "no drift" — the same laundering shape as the registry gap fixed at f91807f9. The filed premise was wrong and was corrected rather than built on. Both the report and the investigating orchestrator blamed SPEC-163's plugin split. Measured at the authoring commit (948bf7d3, 2026-05-27), the manifests already read vt-f-report and vt-v-write: the signal was wrong for finance (0 vs 1) and vms (0 vs 9) on day one. SPEC-163 widened it from 2-of-4 to 7-of-8, it did not cause it. That distinction decided the fix — a doc that rotted argues for better rename sweeps, whereas one born plugin-blind argues for deriving the prefix and for making something execute the published command. Why SPEC-163 walked past it twice. cb96b776 rewrote this file's /vt-c-4-review and b6c5c395 rewrote P-002's path; both sweeps matched full artifact names, and "^vt-c-" is a bare prefix inside a fenced shell command. The file emerged from the migration looking migrated — a sixth reference class, after agent-identity-is-a-fifth-reference-class. Three things shipped, not one. The signal itself now derives per plugin and strips the header comment first (every manifest header contains a literal ->). The SPEC-167 guardrail in v025-multi-worktree-workflow.md, which warned the signal was stale and quoted the literal being removed, is amended in the same commit — left alone it would have become a stale warning about staleness one link away. And scripts/tests/setup/57-plugin-component-counts-agree.sh now executes the derivation the doc publishes, with a second assertion that fails if P-003 ever republishes a prefix literal. Fixing prose without pinning it only resets the same clock. The guard lands green, so it is mutation-proven five ways, each confirmed red: a count bump names the plugin; a removed components.skills key fails rather than skips; dropping the comment strip goes red on all eight at exactly +1 each; republishing the prefix trips the recurrence check; and renaming the anchor heading trips the vacuity check instead of passing silently. Setup suite measured PASS=54 FAIL=0. The agents checklist line was deliberately not touched — agent names were never prefixed, it is correct on all five plugins that have a manifest, and on the three without it fails loudly instead of returning a plausible 0. Filed, not folded in: BUG-070 — P-002's TOOLKIT_ROOT snippet in this same file has one .. too many (wrong since 2026-03-05), resolving to the repo's parent and silently no-opping the registration block it feeds. Same silent-no-op class, older, distinct defect.

BUG-041: partition-guard misread every manifest — vt-base 6.2.2

Six months silently broken, and the bug report was half wrong about why. partition-guard.sh read .repo-manifest.yaml through an awk helper carrying two defects, byte-identical since 04bc61f3 (2026-02-13) — latent, never a regression. The key was passed as an unanchored regex, so it matched the paths: line under every partition; and the reset rule /^[[:space:]]*[^-]/ matched the list-item lines themselves, because [[:space:]]* can match zero characters and [^-] was therefore tested against the leading space. With no next on the print rule, every list truncated to its first item. The two defects partially CANCEL, which is why the filed report was wrong. It claimed the context branch was structurally unreachable; measurement showed that branch firing correctly for four of six context roots. The report's own repro used a fixture with one path per partition — the single shape in which "first of each" and "all of each" are indistinguishable. The report was amended (original preserved verbatim) before any code was written. Real damage: 6 of 12 declared roots misclassified, 3 of 4 deployable roots falling through to unknown. Three parsers, one contract — resolved by test, not by a shared file. repo-audit-ci.sh already had a correct reader; commit-size-monitor.sh ignores the manifest entirely (filed as BUG-071). Sharing one helper would have made partition-guard.sh the first script in plugins/vt-base/scripts/ to source a sibling (0 of 26 do), risking the silent-no-op that hit cleanup_override.sh. So the contract lives in scripts/tests/setup/56-manifest-readers-agree.sh, which extracts and runs each reader from its own file — an earlier revision hand-copied the audit's awk into the test, which meant breaking the real audit left the test green. Four review rounds, and every blocker they found was in the fix, not the bug. Round 1: the agreement check was vacuous. Round 2: the version bump moved 2 of 5 mirrors and turned guard 22 red (green on main), and the trailing-comment tolerance was applied to the partition key but not to the paths: key one line below — silencing a single partition while its siblings kept working. Also fixed, both pre-existing: ${TARGET_FILE#"$PROJECT_ROOT"/} was unquoted, so a checkout path containing [ never stripped and the guard went totally dead; and 06 BUGchen/ could never match because the old reader ran tr -d ' '. repo-audit-ci.sh was not "already correct" either. The first ledger draft said it was. It carried defect 2 unfixed — the same [^-] reset class — plus a hardcoded 6-space list indent, so a 4-space manifest or a single whitespace-only line reduced it to zero paths while partition-guard.sh read on. Its Check 1 is a CI gate that exits 1, so an empty list makes it print PASS (0 violations) while checking nothing. Found in round 4; the agreement test now sweeps four manifest shapes.

Sandbox classification finally has a consumer. check_partition could always return sandbox; nothing read it, so the context branch's own advice ("place in 04-prototyp/ (sandbox)") pointed at a partition the guard could say nothing about.


2026-08-12

SPEC-167: parallel-work guardrails + a spec/phase-aware statusline — vt-product-dev (unreleased)

Announce this, or it did not ship. Two of the three deliverables are documentation, and docs/solutions/patterns/distribution-as-completion-precondition.md is explicit that a discoverability feature is done when humans are told, not when the file lands. So: (1) the multi-worktree pattern doc now carries a Parallel-work hazards and guardrails section — read it before running several worktrees, because all four hazards in it are ones this repo has already paid for; (2) there is an opt-in statusline showing your active spec and workflow phase, and the toolkit deliberately does not install it — see vt-d-statusline-spec-phase for the one block to paste into your own ~/.claude/settings.json. What was actually missing: V025 had every piece for parallel work (vt-c-git-worktree, activate waves, context: fork review/finalize, domain-dashboard) but no honest account of what running several worktrees costs, and no spec/phase progress surface at all. Two spec premises were false and were corrected rather than built around. (a) The spec cited a shipped statusLine key holding npx -y ccstatusline@latest; SEC-4 had deleted that key the day before shaping (3abc23da) on the grounds that a statusline is personal preference, not a toolkit standard. FR-3 therefore ships the SCRIPT ONLY — reinforced by a second, independent reason: merge_settings.py is user-wins, so a shipped key would silently never reach anyone who already had their own. (b) FR-4 (a multi-task view) went through a hard feasibility gate and was dropped: Claude Code exposes no cross-session registry, so it would have required a bespoke state file plus SessionStart/SessionEnd hooks with orphan cleanup — disproportionate for a P2. Findings recorded in research.md so it can be revived with the cost known. The load-bearing design decision was falsified on disk before it shipped. Phase was originally to be derived from gate-FILE presence. But .review-gate.md/.test-gate.md are gitignored (.gitignore:44-47) and zero exist under specs/ — that helper would have reported "not built" for every spec ever built, in every fresh worktree. Rewritten (AD-2) onto the committed state.yaml, where status: is present in 172/172 spec files. The falsification case is now a test: fixtures contain no gate files at all, and build must still be derived. Running it over real data found two defects the fixtures had missed. SPEC-121 rendered finalize — it is rejected and was never planned, but carries four PENDING gate placeholders, so gate-first precedence displayed a rejected spec as nearly shipped. And a PENDING gate was counted as a gate reached; three real specs carry them. Both fixed, both pinned by regression tests copied from what was on disk. Hand-written fixtures would not have caught either. The guardrails are scoped against what the target doc already said, not against the spec's assumptions. Hazard (c) is a 9-line pointer because BUG-009 is already fixed and its report holds the forensics; hazard (d) writes only the content-conflict half, because the merge mechanics already occupy that doc's ### Merge into main section. Hazard (d) also had to drop the historical --theirs-on-gate-files rule: untracked files cannot conflict, so publishing it as live guidance would have sent readers hunting an impossible conflict. No throughput claim is made anywhere, and that is enforced. The premise that parallel work is faster is an unvalidated hypothesis — the source labels it verbatim "The hypothesis (to validate, not assert)" — and the trial that would have tested it was cut as an organizational experiment, not a toolkit concern (the SPEC-121 fusion). A test forbids throughput language and permits the word "faster" only where the claim is being denied. Every suite was mutation-tested, not merely observed green. Eight mutations across the two new suites, each confirming the intended assertion goes red; one of them exposed that a heading rename would silently break dev-start's cross-file link with nothing catching it, which is now AC-13. Hot path measured: ~87 ms per render against Claude Code's 300 ms debounce, pinned by a test asserting exactly one python3 spawn and zero git shell-outs. AD-9 — the new skill is excluded from SPEC-163's historical 119-skill cohort via absorbed_skills(), and no FR-pinned literal was edited: the cohort records what the split moved, and a skill created afterwards never moved. test_assignment_reconciliation 16/16 green. Known, left as found (not regressions): test_manifest_integrity (h) fails with "only 7 /vt- references found — assertion is vacuous" — confirmed pre-existing by reverting plugin.json to HEAD. And docs/solutions/patterns/critical-patterns.md:100-130 publishes a stale detection signal, grep -c "^vt-c-", which returns 0 for every plugin that is not vt-base since the split; hazard (b) flags it at the link and supplies a working derivation instead. Version: no bump yet — deferred to /vt-d-5-finalize, recorded as advisory A2 in .plan-gate.md so it is a decision rather than an omission. Files: docs/solutions/patterns/v025-multi-worktree-workflow.md, plugins/vt-product-dev/skills/statusline-spec-phase/{SKILL.md,scripts/statusline.sh,scripts/spec-phase.sh}, plugins/vt-product-dev/skills/dev-start/SKILL.md, plugins/vt-base/tests/test-dev-start.sh, tests/spec-167/{test_guardrails.bats,test_statusline.bats}, tests/fixtures/spec-167/**, tests/spec-163/test_assignment_reconciliation.bats and tests/spec-163/test_deprecated_retirement.bats (both AD-9 cohort exclusions — the second was the regression review caught), scripts/run-tests.sh (stale suite-count comment), .design-state.yaml, plugins/vt-product-dev/.claude-plugin/{plugin.json,skill-symlinks.manifest}, docs/plugins/vt-product-dev.md, configs/user-global/CLAUDE.defaults.md, docs/skill-{orchestration-map,audit-inventory}.{md,json}, specs/167-*.


2026-08-09

SPEC-168 Wave 1: PHP stops being reviewed as TypeScript — vt-base v6.1.0

What was broken: persona-select.sh knew angular.json, nest-cli.json and the Python markers, and nothing else. A PHP repo therefore fell through to kieran-typescript-reviewer — and a pure-.php diff was worse than that: want_lang was false, so it selected no language reviewer at all and PHP was reviewed by nobody. Four repos were affected (02-VisiMatch/backend, 02-VisiMatch/VisiPlan, 02-VisiMatch/e2e-specs, VisiFair). The defect that mattered was found by execution, not by reading. The plan's own first-draft idiom, [ -f "$PROJECT_ROOT"/*/composer.json ], was run by the iteration-2 plan validator against 02-VisiMatch/, which carries four depth-1 composer.json files. The glob expands to four operands, [ exits 2, and the branch is skipped silently — the BUG-004/BUG-021 silent-drop class, reproduced before a line of implementation existed. Replaced with a find probe, -maxdepth 2, excluding vendor/ and node_modules/, with "$PROJECT_ROOT" as a mandatory operand (a bare find -maxdepth 2 walks the CWD and would have passed any test whose fixture root happened to equal it). Depth 1 is the whole point, not a generalisation. VisiFair's real composer.json is at visifair-files/composer.json. A root-only marker check — the obvious implementation, and the one every sibling detector uses — would have shipped green while missing the single repo the reviewer was written to unblock. A repo-scoped fact must never drive a diff-scoped decision. The first implementation collapsed PHP's two signals into one boolean and reused BUG-004's HP-1 guard verbatim, so a composer.json marker — "this project is PHP" — was treated as evidence for "this diff is not TypeScript" and suppressed the default TS reviewer. Every .md, .sql or .yaml-only change in a repo carrying a manifest silently lost its TypeScript reviewer, and depth-1 detection made that fire across all four PHP repos rather than none. This was R5 in the spec's own risk register, and the test written for it, T8(f), asserted the defect as intended behaviour rather than catching it — a fixture can encode a bug as a requirement, and then no amount of green tells you anything. Now has_php_diff and has_php_repo stay separate: selection ORs them, suppression reads only the diff. Same category error D7 names for descriptors. A composer.json/composer.lock diff also became a trigger — a dependency bump is precisely the Composer-hygiene surface the reviewer covers, and matching \.php$ alone made it invisible. Additive, not a replacement. php-reviewer started from BUG-004's HP-1 guard. A composer.json marker does not imply the diff is non-TypeScript (unlike angular.json/nest-cli.json, which are TypeScript), so a mixed monorepo keeps both reviewers, and the reviewer also joins the unknown-diff conservative branch — without that it would be structurally excluded from the non-regression roster forever, which is the same silent drop in a different costume. The reconciliation assertion caught a bug in itself. Six companion surfaces document what the selector emits, and D4 required a mechanical check rather than a checklist. The natural implementation, mirror_text "$i" "$root" | grep -qF "$r", is wrong under set -o pipefail: grep -q exits on the first match, the producer takes SIGPIPE, and the pipeline reports failure — so a reviewer that is present reads as drift, and only in the larger files where the match lands early. It false-flagged kieran-typescript-reviewer in two of six surfaces before being replaced with a pure-bash substring match. A test that reports drift is not thereby evidence of drift. Scope narrowed at the gate, on a measurement. SPEC-168 originally carried a declarative stack: descriptor, a generic tool auto-detector and a bootstrap/pd-3 dedupe. Of 73 repos under ~/01-repositories, 8 carry a .repo-manifest.yaml and 0 of the 4 PHP repos do — so the descriptor's primary surface is absent in exactly the projects the PHP reviewer exists to unblock, and it could not have served this spec's own motivating case. Deferred to a successor spec with plans, decisions and risks preserved verbatim; the deferred requirements are struck through in spec.md rather than deleted, so the successor inherits an intact record. Also fixed: docs/workflows/development/4-review.md had never listed kieran-python-reviewer, stale since BUG-021 shipped it. Found by reconciling the same table. MINOR, not PATCH: a new agent is a new capability. Bumped across all nine version-mirror files (ten occurrences) — derived by grepping for the outgoing version, never from a remembered count, since SPEC-169's bump missed docs/plugins/vt-base.md by trusting one. Files: plugins/vt-base/agents/review/php-reviewer.md (new), plugins/vt-product-dev/skills/workflow-4-review/scripts/persona-select.sh, plugins/vt-base/tests/test-persona-selection.sh (T8/T9/T10), plugins/vt-base/.claude-plugin/agent-symlinks.manifest, the six companion surfaces (workflow-4-review/references/persona-catalog.md, workflow-4-review/SKILL.md ×2, plugins/vt-base/agents/orchestrators/implementation-orchestrator.md, docs/concepts/agent-architecture-patterns.md, docs/workflows/development/4-review.md), specs/168-*, version mirrors across plugins/{registry.json,vt-base}, .claude-plugin/marketplace.json, configs/user-global/installed_plugins.json, docs/plugins/vt-base.md and the generated skill docs.


2026-08-07

SPEC-169 Wave 1: the installer stops destroying user config — vt-base v6.0.0 (BREAKING), vt-toolkit v1.0.1

What was broken: three defects that destroyed user data at exit 0, with no warning and nothing counted. (1) The directory ownership hash read only regular files and threw away filenames, so a user directory holding only symlinks produced the SHA-1 of empty input — indistinguishable from an empty directory. It was adopted as toolkit-owned and rm -rf'd. (2) A symlink at a shipped path was written through: the record functions dereferenced, storing the link target's hash under the link's path, so cp followed it and replaced a file outside ~/.claude entirely. (3) Every write discarded its own failurecp … && record … then an unconditional return 0, so a failed copy reported nothing while the summary claimed success; the directory path removed the user's copy before the replacement existed, leaving them with neither under a mode that takes no backup. Why MAJOR: FR-8 deletes --overwrite and its --full alias, the SPEC-124 regression's delivery mechanism. README.md told every user to run --full until this release; it now exits 1. Migration is one word (--full--safe), but a documented CLI flag disappearing is breaking, and versioning it as anything less would have misrepresented it. The durable lesson — a green suite is not evidence. The setup suite was 23/23 while two of those three defects were live, and 29/29 while the third was. It missed them for a reason worth stating: the acceptance fixture seeds a regular SKILL.md into its decoy skill directory, and a regular file is exactly the input shape for which the broken hash happened to work. A fixture that seeds the passing case proves the guarantee only where it already holds. Every fixture added since is mutation-verified — the defect reverted into setup.sh and the fixture watched to go red — because that is the only step that distinguishes a test from a decoration. Two assertions I wrote were themselves vacuous, and only mutation testing caught it. One grepped for [ERR] anywhere and passed before the fix, matching three unrelated MCP rows; the real failure was never named by the installer at all. Another block wrote an "update landed" marker into a tracked repo file and never read it back — asserting preservation twice and the update half zero times, while committing a test artifact that shipped into every user's ~/.claude/agents/. Writing a vacuous assertion while fixing a defect found by a vacuous assertion is the recurring shape here. Review structure mattered more than review effort. Five consecutive passes wrote their gate at zero reviewer returns, and in every one the late arrivals carried the most severe finding — including, in pass 2, the CRITICAL that wrote outside ~/.claude. A pass with no returns is INCOMPLETE, never a result. Two of pass 2's three blockers were defects in pass 1b's own fixes. One reported blocker was refuted: "fixture 28 reverts setup.sh" turned out to be a second subagent running git checkout -- in the same worktree during the first one's mutation test — concurrent agents sharing a worktree corrupt each other's measurements, and one silently turned a real red into a green. Decision 8, and its amendment. Bootstrap adoption is now scoped to files and never claims directories: replacing an adopted directory is unbounded loss, the trigger is noise rather than intent (measured on a live ~/.claude, a shipped skill directory differed by exactly two .DS_Store files), and adoption buys nothing for a directory that matches, since a matching tree already self-records. Then F1 falsified the decision's own premise — through a symlink, overwriting an adopted file is unbounded too, and escapes the directory the toolkit governs. Amended rather than silently patched, because the transferable lesson is the error: the premise reasoned about what the toolkit ships when the question that mattered was what sits at the destination, and only the second is under the user's control. The FR-7 gate was rewritten after being measured, not read. It caught exactly one shape — a .md/.sh file, outside a substring allowlist, with setup.sh and the flag adjacent on one line. Probing found eight shapes it missed, including docs/debugs/ exempted because bugs/ is a substring of it, and scripts/docs-gen/gen_plugin_catalog.py, which writes the published documentation site's install page at build time — so no scan of tracked .md files could ever have reached it. Now: path-component allowlist, files selected by being text rather than by extension, two-stage detection, exemption anchored to the flag's own clause with past tense as the tell for incident narrative. 932 → 1495 files scanned, and it immediately found five live instructions. What this does NOT fix. A fresh install still writes the maintainer's absolute paths and marketplace list into a colleague's registries (BUG-035). That is SPEC-171's FR-6, split out because it depends on the Claude CLI — an ownership correction applied to bugs/{034,035,036} in this branch, which had named SPEC-169. The review gate reads PASS for the Wave 1 scope only, a distinction recorded in the gate itself: a green Wave 1 is not working coexistence, and SPEC-171 must follow. Also: filed BUG-039 — test_manifest_integrity assertion (h) is a vacuity floor that has been firing unread on main ("only 7 /vt- references found", against a floor of 20). The floor is working; the finding is the drop, and the test cannot tell legitimate removal from accidental. Lowering it to 7 without investigating would convert a working alarm into permanent silence. Files: scripts/setup.sh, scripts/tests/setup/{24,25,26,27,28,29}-*.sh (three new, three rewritten), scripts/docs-gen/gen_plugin_catalog.py, docs/concepts/skill-scoping.md, docs/solutions/patterns/{defensive-toolkit-install,external-cli-pinned-and-gated}.md, plugins/vt-base/.claude-plugin/skill-symlinks.manifest, tests/spec-130/*profile*.bats, specs/169-*/{decisions,review-triage,state}.yaml|md, bugs/{034,035,036}-*, intake/pending/bugs/BUG-039.md, version mirrors across plugins/{registry.json,vt-base,vt-toolkit} and .claude-plugin/marketplace.json.


2026-08-05

BUG-037: the finalize version gate had been dead since 5.0.0 — vt-product-dev v1.0.1

What was broken: workflow-5-finalize/SKILL.md told the agent to run $CLAUDE_PLUGIN_ROOT/scripts/check-finalize-versions.sh, with a documented fallback of ../../scripts/…. The skill lives in vt-product-dev, so both resolved to plugins/vt-product-dev/scripts/ — a directory that does not exist. The script is in vt-base. SPEC-163 W5 (cb96b776) moved the skill out of the base plugin and left the script behind; both paths had been correct before the split. Every release from 5.0.0 onward ran with a version gate that could not find itself. Why it survived so long, and the durable lesson. The skill documented a graceful-degradation branch: "If the script fails to run (command not found, Python3 missing, etc.) … display warning but proceed." That message cannot distinguish "your machine is missing a dependency" from "this skill points at a path that has not existed for months", so a toolkit defect rendered as an environment hiccup the operator was explicitly told to ignore. A graceful-degradation branch must never be able to absorb a toolkit defect silently — it converts a loud, fixable breakage into a permanent quiet one. Step 5 is now split: a missing file is a TOOLKIT DEFECT reported as FAIL naming the path that was tried and recorded as a failed check; a script that exists but errors still skips. A version gate that cannot find itself has not passed. Confirmed by dogfooding, twice. The /vt-d-5-finalize run on the BUG-030 branch reported "Version gate: SKIPPED — script execution refused" and attributed it to permissions; the version bump was then done by hand across twelve mirrors and change-ledger.md was found missing its DoD-3 entry — the two omissions the gate exists to catch. A later independent finalize session reproduced the path failure live without knowing a fix existed. And once repaired, the first thing the gate did was fail its own branch: vt-product-dev: Version bump required, changed files: 2. Bumped 1.0.0 → 1.0.1 across all four mirrors together. The guard, and what it immediately disproved. Nothing in the suite executed a skill's instructions, so a path written in prose had no coverage at all — scripts/tests/setup/23-skill-script-paths-resolve.sh now resolves every script path a SKILL.md tells the agent to run. Its first draft checked two reference forms while its header claimed to cover every path; measuring its own blind spots found ~60 references in uncovered forms. The fix had committed the same over-claiming defect it was written to fix. Widened to six forms (33 → 67 references), with three subtleties a naive widening would have turned into ~40 false positives: a bare scripts/X usually means the skill's own scripts/ dir rather than the repo root (both interpretations tried, either passing); prose mentions like - **Example**: scripts/rotate_pdf.py are not invocations (bare forms now require an interpreter prefix); and https://…py matched the naive pattern. Vacuity floor raised 5 → 40 to match. Mutation-verified 6/6 — and mutation #3 passed vacuously on the first attempt because the sed removed 1 of 2 occurrences, redone with the occurrence count asserted to zero before the result was trusted (the BUG-023/024 trap). A second instance found by the guard, not by reading. activate/SKILL.md invoked ${CLAUDE_PLUGIN_ROOT}/skills/git-worktree/scripts/worktree-manager.sh, but git-worktree is a vt-base skill — same defect, same split, fixed with the same sibling hop. The manual sweep had missed it because it only matched paths with /scripts/ directly after the root; the far more common $CLAUDE_PLUGIN_ROOT/skills/<skill>/scripts/<file> shape was invisible to it. A manual sweep encodes an assumption about the shape of the thing it is looking for. BUG-019 re-scoped by measurement. Its title names two skills; the guard measured 21 non-resolving invocations across six vt-office skills (docs-pipeline-orchestrator, markdown-diagram-processor, mermaid-to-images, mermaid-diagrams-branded, meeting-minutes, c4-diagram) — the whole document pipeline invokes converter scripts that were never written. All six are excepted in the guard by bug id, and the guard asserts bugs/019-* exists, so the exception cannot decay into a silent suppression; each entry must be deleted as its skill is fixed. Full inventory in bugs/019-*. PATCH, not MINOR: no skill added, removed or renamed. Review was deliberately skipped for this branch (recorded in .review-gate.md as SKIPPED_BY_DECISION, not as a pass) — reviewer subagents cannot execute here, since e0cc65cf is not on main, so a review would have returned static-reading leads requiring re-verification by hand. Files: plugins/vt-product-dev/skills/workflow-5-finalize/SKILL.md, plugins/vt-product-dev/skills/activate/SKILL.md, scripts/tests/setup/23-skill-script-paths-resolve.sh (new), plugins/vt-product-dev/{CHANGELOG.md,.claude-plugin/plugin.json}, plugins/registry.json, .claude-plugin/marketplace.json, docs/plugins/vt-product-dev.md, bugs/{019,037}-*.


2026-08-04

BUG-030 / BUG-032: the marketplace half of the plugin registration never existed — v5.0.1

What shipped: .claude-plugin/marketplace.json declared the marketplace as company-saas-toolkit — an unreplaced scaffold name — while settings.json shipped the enable flag vt-base@visitrans. For roughly six months claude plugin list reported enabled: true for an id that claude plugin details answered with "Marketplace visitrans not found". The plugin never loaded through the plugin system at all. The marketplace is now visitrans, versioned with the toolkit, and lists all 8 registry plugins rather than 1. Separately, a workflows key in vt-base/plugin.json is not in the vendor schema and was a hard error, not a warning, so claude plugin validate failed and the plugin was uninstallable. Why nothing caught it: plugin identity is mirrored across five surfaces — marketplace name, plugin.json name+version, installed_plugins.json key, settings.json enabledPlugins, and the SPEC-130 fixture — with no test pinning any of them together. The count-reconciliation-single-source corollary again: the omitted mirror was the highest-coupling one, the marketplace declaration that makes the @ segment resolvable at all. The fix for BUG-032 was itself a silent regression, and this is the durable lesson. Deleting the schema-invalid workflows key satisfied the validator. But plugins/vt-base/bin/generate-skill-map.sh was its only reader, so the next regeneration would have rendered four documented pipelines — the /vt-d-0-start → … → /vt-d-6-operate development chain, the pd- design chain, the iteration loop and knowledge-work — as four empty headings. The guard was assert_file_contains "$OUT_JSON" '"workflows"', a substring check that passes against "workflows": {}. The generator carries a comment block apologising for this exact failure from a previous incident, which is what makes it worth writing down rather than just fixing: deleting data to satisfy a schema is a move, not a delete — find the readers first, and a substring assertion over generated output cannot distinguish "present and populated" from "present and empty", so it certifies the very failure it appears to guard. The pipelines now live at the top level of plugins/registry.json (toolkit-owned, outside the vendor schema — the disposition BUG-032 already gives manifests); the generator hard-fails on an empty pipeline instead of emitting a blank section; and the assertion is structural. Stripping the key now fails 37 of 39 skill-map assertions. The review had to be run twice, and the second run inverted the first. Six reviewer subagents were dispatched and none returned before the gate was written, so pass 1 was published from direct file verification alone — including a line recording "no plugin.json retains a workflows key" under verified-good. When the reviewers did return, that fact was true but its significance was inverted: it was the branch's only Critical. Two of their findings were then rejected on evidence — an errexit claim disproved by executing [ … ] && continue under set -e in both bare and in-loop form (the fail line is reached), and a python3-in-a-loop performance concern measured at 6 total spawns against minutes-long suites. A third, whether the tagskeywords swap is valid at marketplace-entry level, was settled by running the validator: keywords produces no diagnostic, exit 0. Records that were asserting things that were not true. bugs/030-*/state.yaml declared spawned_followups: [BUG-034, BUG-035, BUG-036] and the review repeated it as "already filed"; none of the three existed, so four findings recorded as "deferred to tracked work" were tracked nowhere. All three are now real, each verified on disk before filing — which is how BUG-035 turned out to be wider than reported (5 hardcoded /Users/rolf paths across two shipped registries, not 1 file). spec.md FR-6 and decisions.md Decision 3 both instructed a future planner to stop shipping the registries, a premise BUG-030 disproved — the isLocal entry is load-bearing and claude plugin install freezes a cache copy — so Decision 3 now carries the SUPERSEDED block Decision 1 already had. fix_scope listed a SPEC-130 fixture the branch never touched, contradicting the correction 30 lines below it. BUG-032's record read triaged while a commit named after it had shipped two of its three defects. Test hardening, all mutation-verified. Six defects were seeded and each observed to fail: marketplace-name reversion, installed_plugins.json version drift, metadata.version drift, a registry key rename, emptied config files, and deletion of the workflows key. Three of those previously passed green — assertion 1 silently reduced to zero iterations whenever the registry gate swallowed every id (and BUG-030 was caused by exactly such a rename sweep), so it now floors the number of ids actually checked. A dead known_marketplaces.json fallback was removed: its only reachable future case was one of our own plugins naming a foreign marketplace — the BUG-030 shape it would have turned green. The claude plugin validate call now runs under an isolated $HOME with a 60s timeout, and exit 124 reports a hang rather than misdiagnosing it as BUG-032. The test also carries its own negative self-test, so it detects its own inertness on every run. PATCH, not MINOR: no skill added, removed or renamed; count stays 132. All twelve 5.0.0 mirrors moved together, four of them generated — bumping only the test-pinned subset would have manufactured the exact version-drift class this release fixes. Known limitation — BUG-030 is NOT closed by this release. setup.sh performs no marketplace registration and the shipped known_marketplaces.json still has no visitrans entry, so vt-base@visitrans does not resolve after a clean install. The behavioural evidence recorded during the build holds only on a machine where the marketplace was added by hand — which BUG-034 shows setup.sh --update reverts. The installer half is SPEC-169 FR-2a + FR-6, which also closes BUG-034, BUG-036 and the residual test-coverage gap: they are one defect seen from four angles. Files:* .claude-plugin/marketplace.json, plugins/vt-base/.claude-plugin/plugin.json, plugins/registry.json, plugins/vt-base/bin/generate-skill-map.sh, plugins/vt-base/tests/test-spec-125-skill-map.sh, scripts/tests/setup/22-plugin-registration-resolves.sh, configs/user-global/installed_plugins.json, docs/concepts/plugin-architecture.md, plugins/vt-base/CHANGELOG.md, docs/plugins/vt-base.md, docs/skill-{orchestration-map,audit-inventory}.{md,json}, bugs/{030,032,034,035,036}-*/, specs/169-*/{spec.md,decisions.md}.

SPEC-170: the output-clarity standard — answer-first + a mandatory "Next actions" block

Scope shipped: one documented standard (CLAUDE.defaults ## Before Every Response), one mechanical gate (plugins/vt-base/bin/check-output-contract.sh + plugins/vt-base/lib/output-contract-baseline.txt), a 162-assertion suite, and conformance for the 9 core phase skills. A skill's output class is declared by output_contract:multi-step (the default when absent) requires a delimited ## Next actions / ## Next steps block with real content; single-fact opts out. The baseline is a shrink-only ratchet: the legacy skills grandfathered at seed time WARN (125 at seed, 124 now — every shrink is banked into BASELINE_MAX in the same commit), anything off-baseline hard-FAILs, and coverage climbs as skills conform — toward a floor of 5, not 0: five baseline entries name skills in the unregistered plugins/support and plugins/ufi directories, which never deploy and which nobody will conform (LATE-M1). Deleting those lines would redden the gate rather than clean it up.

Why a ratchet and not a flag day. The strict rule measured 12 of 137 skills already conforming. Hard-failing the other 125 is the "mass false-fails → gate gets disabled" outcome the whole mechanism exists to avoid, so enforcement is opt-in-by-conformance rather than opt-out-by-suppression. BASELINE_MAX is an exact-equality assertion, not <=: it makes the suite go red on progress (conform a skill, edit the constant), and that tax is the feature — <= would silently hand back the regrowth budget the membership check cannot police.

The review found six High defects, four of them in the gate's own correctness. Every one was reproduced before being fixed, and three were only visible because seven independent reviewers read the same diff: - The anchor matched inside fenced code blocks. activate/SKILL.md was certified as conforming by a quoted example at line 604 — sample output of a mid-workflow substep — and its entire contribution to the spec was one frontmatter line. Worse, the suite's "all 9 core phase skills PASS" assertion was green because of that false positive. The detector also took grep | head -1, so only the first anchor in a file was ever evaluated; git-worktree and using-git-worktrees passed on real headings purely because theirs happened to sort first. Both closed by a single fence-aware awk pass that evaluates every anchor. The inverse case is now pinned too: a fence suppresses anchor matching, but a genuine block whose body is a command fence still counts. - A case desync reopened the placeholder hole. Detection used grep -i; the label-stripper was case-sensitive. So ## NEXT ACTIONS matched, failed to strip, kept its own heading text as "content", and a [TODO]-only body reported PASS — the exact defeat that the placeholder rule had closed one iteration earlier for the lowercase form. Both regex forms now derive from a single ANCHOR_BASE; neither is case-folded. - The standard documented a label the gate rejects. CLAUDE.defaults blessed "Required actions"; the anchor accepts only "Next actions"/"Next steps". An author following the single-source statement of the standard verbatim got a hard FAIL telling them to add the block they had just added. Four reviewers found this independently — and no assertion saw it, because the mirror greped the one label all three surfaces happened to share. This is the failure the spec exists to prevent, occurring inside the spec's own deliverable. The mirror now pins the accepted variant and asserts the rejected synonym is only ever named as a rejection. - The nine-core-skill assertion evaluated nothing on any path containing a space. The paths were joined into a string and expanded unquoted, so word-splitting applied; the check died with ERROR: not a file and exit 2, 2>&1 folded that into the captured output, the FAIL/WARN grep found no match, and the test printed ✓. The adjacent "files exist" guard did not catch it — it tested each path quoted, so it passed while the real assertion ran on fragments. Now an array, and graded on the exit code rather than on absent output. - The authoring rule loaded for 15 of the 137 skills it governs (paths: ["plugins/vt-base/**"]), while the check enforces across all eight plugins. Eight of the nine skills this spec conformed, and the scaffold it changed, sat outside that scope. Widened to plugins/**, with the genuinely vt-base-only rules now saying so inline. - init_skill.py interpolated an unvalidated sys.argv[1] into both the YAML frontmatter and the output directory path, so a name containing a newline injected frontmatter keys and one containing ../ escaped the tree — where mkdir(parents=True) would create it and drop a 0755 script. Pre-existing, but the scaffold's input is natural language, which makes prompt injection the realistic vector. Now regex-validated and confinement-asserted.

Two ratchet-honesty fixes. The shrink-only comparison ran against HEAD, so it caught only uncommitted growth — commit the growth and it passed forever after. It now compares against git merge-base main HEAD, making it branch-scoped so it holds up to the merge that is the actual review point. And a skipped comparison used to increment PASS; skips now report NOT VERIFIED and exit non-zero, because run-tests.sh grades by exit code and a banner alone let a single-branch clone report green on an unvalidated ratchet. Opt out deliberately with OUTPUT_CONTRACT_ALLOW_SKIP=1.

A suppressor removed rather than re-commented. generate-skill-audit.sh carried --exclude='output-contract-baseline.txt', justified as "shipped inside the skill-audit skill". After the port that premise is false — the baseline lives in plugins/vt-base/lib/, which the marker scan never reaches. Measured both ways: classification is byte-identical. It is deleted rather than kept-with-a-corrected-comment because a bare-basename --exclude is a far looser suppressor than the detection it cancels — it would have silently dropped any file of that name, anywhere under any scanned skill directory, from every private-data scan. (suppression-mirrors-detection.md.)

Known gap, deliberately deferred: the 36 command files (plugins/*/commands/**.md) are user-facing and unaudited. Tracked in intake/pending/spec-170-followup-command-output-contract.md, not left in spec prose. Note for whoever picks it up: a plugins/*/commands/*.md glob mirroring the skills one scans 35 of 36 — vt-base/commands/workflows/compound.md sits a level deeper, the same nested-path blindness as SPEC-163's agent pruner.


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 `