diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 7e1a5b8..1fa93ca 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -10,7 +10,7 @@ "plugins": [ { "name": "vstack", - "version": "1.67.0", + "version": "1.68.0", "source": "./claude", "description": "28 skills that fire without a slash command, 14 agents, 15 commands, and the session hook that routes situations to skills. Most skills are ported from pstack and Superpowers \u2014 see claude/skills/ATTRIBUTION.md for per-skill source and license.", "category": "workflow" diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f91bfa..b7cbe3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,16 @@ Versions follow [semver](https://semver.org). The version lives in two manifests `.claude-plugin/marketplace.json` and `claude/.claude-plugin/plugin.json`, and check 13 of `.claude/verify.sh` fails when they disagree. +## 1.68.0 — 2026-09-03 + +- **`overlay.sh --check` reports drift without writing.** Every file the write path would + touch is compared against the payload and reported as `stale`, `absent`, `legacy` or + `differs (repo-owned)`; exit 1 on anything the next `overlay` run would change. Overlaid repos + can now learn that the template moved. (#10) +- **`tests/inventory-contract.sh --write` re-points the inventory.** Replaces the hand-run jq + after every payload commit; refuses on a dirty tree, re-validates, prints the commit line, and + is a no-op when the contract already holds. (#9) + ## 1.67.0 — 2026-09-02 - **A security toolchain ships with the overlay.** `claude/security-scan.sh` runs gitleaks, diff --git a/README.md b/README.md index d279ce9..0d6d202 100644 --- a/README.md +++ b/README.md @@ -201,6 +201,14 @@ only lane a cloud sandbox without your home directory can reach): | `claude/security.yml.tmpl` | `.github/workflows/security.yml` | seeded if absent | | `claude/dependabot.yml.tmpl` | `.github/dependabot.yml` | seeded if absent | +A repo overlaid at commit X keeps that commit's hooks, agents, commands, skills and scanner +until someone re-runs overlay — nothing else reports the staleness. `vstack overlay --check +` (or `./overlay.sh --check `) diffs the destination against the "always +overwritten" file list above plus the two seeded templates and the `.conductor/settings.toml` +pin, writes nothing, and prints one line per drifted file followed by `overlay --check: N +stale, M repo-owned diffs, K missing`. Exit 0 only when N and K are both 0; repo-owned template +edits (M) never fail the check. + ## Day to day | Command | What it does | diff --git a/bin/vstack b/bin/vstack index 10d2750..869ef26 100755 --- a/bin/vstack +++ b/bin/vstack @@ -14,6 +14,10 @@ commands: install [args...] run the repo's install.sh (args passed through, e.g. --dry-run) doctor [args...] run ~/.config/agents/bin/doctor (args passed through, e.g. --drift) overlay run the repo's overlay.sh against + overlay --check + read-only: report drift against a repo already overlaid (stale + copies, repo-owned template edits, missing files, conductor pin + behind), write nothing, exit 1 if any file is stale or missing verify run the repo's .claude/verify.sh test run tests/auto-trigger.sh (live skill-firing regression; needs claude auth, ~15 min) trust [repo-dir] [--yes] diff --git a/claude/.claude-plugin/plugin.json b/claude/.claude-plugin/plugin.json index c395f17..6b695d3 100644 --- a/claude/.claude-plugin/plugin.json +++ b/claude/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "vstack", - "version": "1.67.0", + "version": "1.68.0", "description": "Skills that fire on the situation instead of a slash command, plus the subagents, commands, and session hook that make them fire. Verification gates, parallel fan-out, code review, and writing discipline.", "author": { "name": "Vedant Kumar" diff --git a/claude/inventory.json b/claude/inventory.json index f5aa7c5..41ab25e 100644 --- a/claude/inventory.json +++ b/claude/inventory.json @@ -3,7 +3,7 @@ "contract_version": 1, "product": { "name": "vstack", - "version": "1.67.0", + "version": "1.68.0", "version_source": [ "claude/.claude-plugin/plugin.json", ".claude-plugin/marketplace.json" @@ -11,8 +11,8 @@ "repository": "https://github.com/itsvedantkumar/vstack" }, "derived_at": { - "head": "4d0166f635f1e8647363b1eee769191512b9f547", - "payload_digest": "61967c2a5347ad45701ae581b99db1e624832a5144d7af58d2ec4756e3150ac8", + "head": "96b94950accb42a31c97c9ff17e4c325f5dfe77a", + "payload_digest": "f615cff0d365b6e010c19b4b24de68424c72f4f6705646b866b10593f603e306", "digest_recipe_source": "tests/inventory-contract.sh:payload_digest_compute()", "digest_recipe_note": "payload_digest hashes the working-tree BYTES of every payload file -- tracked and untracked-but-not-ignored -- together with its executable bit and path, then hashes that listing. Recompute it with `tests/inventory-contract.sh --print-digest` and nothing else: typing a recipe by hand into a shell is a second implementation. This field used to hold the recipe as an executable string that the validator eval'd, on the reasoning that the two could then never disagree. That made the artifact its own oracle -- editing the recipe and the digest together passed while measuring nothing -- and it also hashed index blob ids plus `git status --porcelain`, which names files and status letters but never their contents, so two different unstaged edits to the same path produced the same digest. Both were confirmed by running them.", "staleness_is_the_signal": "This file is a snapshot. When the payload digest no longer matches, the validator regenerates every derived field and diffs. A mismatch is the intended failure, not a maintenance chore: it means a component moved and at least one consumer has not been told.", diff --git a/overlay.sh b/overlay.sh index 8df3095..0ceefc5 100755 --- a/overlay.sh +++ b/overlay.sh @@ -6,11 +6,26 @@ # `.claude/` overlay is the ONLY config lane that reaches it. Run this in every repo you # dispatch work to from your phone. # -# Usage: ./overlay.sh [target-repo-dir] (default: $PWD) +# Usage: ./overlay.sh [--check] [target-repo-dir] (default: $PWD) +# --check: report drift against a repo already overlaid, write nothing, exit 1 if stale set -euo pipefail SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -DEST="${1:-$PWD}" + +# --check may come before or after the dest arg (`overlay.sh --check repo` and the vstack +# dispatcher's `overlay --check` both need to work), so it is pulled out of the +# positional list rather than assumed to be $1. Every other positional arg — today there is +# only ever one — passes through untouched, which is what keeps a plain `./overlay.sh ` +# byte-identical to before this flag existed. +CHECK=0 +ARGS=() +for a in "$@"; do + case "$a" in + --check) CHECK=1 ;; + *) ARGS+=("$a") ;; + esac +done +DEST="${ARGS[0]:-$PWD}" # -e, not -d: inside a git worktree .git is a file pointing at the real git dir. The -d test # rejected every Conductor workspace, which is precisely where this needs to run — Conductor @@ -18,6 +33,128 @@ DEST="${1:-$PWD}" [ -e "$DEST/.git" ] || { echo "error: $DEST is not a git repo or worktree" >&2; exit 1; } [ -f "$SRC/claude/settings.json" ] || { echo "error: run from the vstack repo" >&2; exit 1; } +# --check stops here, before anything below that writes. It walks the exact same file lists +# overlay would copy — unconditional copies, seed_tmpl targets, the conductor pin — but only +# ever reads, so a repo can be checked in CI or a cron without risking the write path neither +# of those callers reviewed. +if [ "$CHECK" -eq 1 ]; then + STALE=0 + OWNED=0 + MISSING=0 + + #