Record Drew's approved reduced design after the second staff review. Limit preflight to the mutating bump path, cover audit's independent read path, and require byte-for-byte proof that deterministic YAML failures cannot partially update earlier JSON manifests. Provide one TDD implementation task for the Hermes registry entry, jq/yq dispatch, focused preflight, and three behavioral checks. Explicitly defer rollback, audit-status changes, nested YAML, runtime changes, and broader release-tool refactoring.
9.3 KiB
Hermes Version-Bump Wiring Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Keep the Hermes YAML manifest version synchronized with every other declared release manifest.
Spec: docs/superpowers/specs/2026-08-05-hermes-version-bump-wiring-design.md
Architecture: Extend the existing release script with a small extension-based dispatcher: JSON continues through jq, while .yaml uses Mike Farah yq v4. Before the mutating bump loop, read every present manifest through that dispatcher so deterministic format or field failures occur before the first write.
Tech Stack: Bash 3.2-compatible shell, jq, Mike Farah yq v4, existing shell-lint tooling.
Global Constraints
- Support only
.jsonand.yaml;.ymland other extensions remain unsupported. - YAML fields are present top-level strings; nested YAML fields are out of scope.
- Pass the YAML field and new value through environment data, never interpolate either into a
yqexpression. - Keep
yqconfined to maintainer release tooling; do not add a plugin runtime dependency. - Preserve the existing missing-file behavior:
--checkreports missing files and a bump skips them. - Preflight only the mutating bump path; do not add rollback or transactional writes.
- Do not change audit status behavior, version validation, or the existing JSON field-expression implementation.
File Map
- Create:
tests/version-bump/test-bump-version.sh- Exercise the real script in temporary JSON/YAML fixtures and check the real registry.
- Modify:
scripts/bump-version.sh- Add YAML read/write helpers, format dispatch, and bump-only read preflight.
- Modify:
.version-bump.json- Register
.hermes-plugin/plugin.yamlat top-level fieldversion.
- Register
Task 1: Wire Hermes Into The Existing Version-Bump Script
Files:
- Create:
tests/version-bump/test-bump-version.sh - Modify:
scripts/bump-version.sh - Modify:
.version-bump.json
Interfaces:
-
Consumes:
.version-bump.jsonrecords shaped as{ "path": string, "field": string }. -
Produces:
read_manifest_field FILE FIELD,write_manifest_field FILE FIELD VALUE, andpreflight_manifestsBash helpers. -
Step 1: Fetch the current development base
Run:
git fetch origin dev
Expected: command exits 0 and refreshes origin/dev.
- Step 2: Rebase the task branch
Run:
git rebase origin/dev
Expected: command exits 0, and git status --short --branch no longer reports the branch behind origin/dev.
- Step 3: Add the initial failing behavioral test
Create tests/version-bump/test-bump-version.sh with the happy-path fixture and real registry assertion:
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
SCRIPT_SOURCE="$REPO_ROOT/scripts/bump-version.sh"
TEST_ROOT="$(mktemp -d)"
cleanup() {
rm -rf "$TEST_ROOT"
}
trap cleanup EXIT
fail() {
echo "FAIL: $*" >&2
exit 1
}
make_fixture() {
local repo="$1"
local yaml_body="$2"
mkdir -p "$repo/scripts" "$repo/.hermes-plugin"
cp "$SCRIPT_SOURCE" "$repo/scripts/bump-version.sh"
cat >"$repo/.version-bump.json" <<'JSON'
{
"files": [
{ "path": "package.json", "field": "version" },
{ "path": ".hermes-plugin/plugin.yaml", "field": "version" }
],
"audit": { "exclude": [] }
}
JSON
cat >"$repo/package.json" <<'JSON'
{
"name": "fixture",
"version": "1.2.3"
}
JSON
printf '%s\n' "$yaml_body" >"$repo/.hermes-plugin/plugin.yaml"
}
happy_repo="$TEST_ROOT/happy"
make_fixture "$happy_repo" $'name: superpowers\nversion: 1.2.3'
/bin/bash "$happy_repo/scripts/bump-version.sh" --check >"$TEST_ROOT/check.out"
/bin/bash "$happy_repo/scripts/bump-version.sh" --audit >"$TEST_ROOT/audit.out"
/bin/bash "$happy_repo/scripts/bump-version.sh" 2.3.4 >"$TEST_ROOT/bump.out"
[[ "$(jq -r '.version' "$happy_repo/package.json")" == "2.3.4" ]] \
|| fail "JSON manifest was not bumped"
[[ "$(yq -r '.version' "$happy_repo/.hermes-plugin/plugin.yaml")" == "2.3.4" ]] \
|| fail "YAML manifest was not bumped"
jq -e '
any(.files[];
.path == ".hermes-plugin/plugin.yaml" and .field == "version")
' "$REPO_ROOT/.version-bump.json" >/dev/null \
|| fail "Hermes manifest is not registered"
echo "Version-bump tests passed"
- Step 4: Run the test to verify RED
Run:
/bin/bash tests/version-bump/test-bump-version.sh
Expected: FAIL before Version-bump tests passed; the current JSON-only reader cannot process the YAML fixture.
- Step 5: Add minimal YAML dispatch and register Hermes
In scripts/bump-version.sh, add these helpers after write_json_field:
require_tool() {
command -v "$1" >/dev/null 2>&1 || {
echo "error: required tool '$1' is not on PATH" >&2
return 1
}
}
read_yaml_field() {
local file="$1" field="$2"
require_tool yq || return 1
FIELD="$field" yq -er '.[strenv(FIELD)] | select(tag == "!!str")' "$file"
}
write_yaml_field() {
local file="$1" field="$2" value="$3"
FIELD="$field" VALUE="$value" \
yq -i '.[strenv(FIELD)] = strenv(VALUE)' "$file"
}
read_manifest_field() {
local file="$1"
case "$file" in
*.json) read_json_field "$@" ;;
*.yaml) read_yaml_field "$@" ;;
*)
echo "error: unsupported manifest format: $file" >&2
return 1
;;
esac
}
write_manifest_field() {
local file="$1"
case "$file" in
*.json) write_json_field "$@" ;;
*.yaml) write_yaml_field "$@" ;;
*)
echo "error: unsupported manifest format: $file" >&2
return 1
;;
esac
}
Replace the three command-path calls to read_json_field with read_manifest_field, and replace the bump-path call to write_json_field with write_manifest_field.
Add this exact entry to .version-bump.json immediately after package.json:
{ "path": ".hermes-plugin/plugin.yaml", "field": "version" },
- Step 6: Run the initial test to verify GREEN
Run:
/bin/bash tests/version-bump/test-bump-version.sh
Expected: PASS with Version-bump tests passed.
- Step 7: Add the failing no-partial-write regression
Insert this block before the final success message in tests/version-bump/test-bump-version.sh:
invalid_repo="$TEST_ROOT/invalid"
make_fixture "$invalid_repo" $'name: superpowers\nversion: 123'
cp "$invalid_repo/package.json" "$TEST_ROOT/package.before"
cp "$invalid_repo/.hermes-plugin/plugin.yaml" "$TEST_ROOT/plugin.before"
if /bin/bash "$invalid_repo/scripts/bump-version.sh" 2.3.4 \
>"$TEST_ROOT/invalid.out" 2>&1; then
fail "bump accepted a non-string YAML version"
fi
cmp -s "$TEST_ROOT/package.before" "$invalid_repo/package.json" \
|| fail "JSON manifest changed before YAML validation failed"
cmp -s "$TEST_ROOT/plugin.before" "$invalid_repo/.hermes-plugin/plugin.yaml" \
|| fail "invalid YAML manifest changed"
- Step 8: Run the regression to verify RED
Run:
/bin/bash tests/version-bump/test-bump-version.sh
Expected: FAIL with JSON manifest changed before YAML validation failed; without preflight, the JSON manifest is written before the later YAML reader rejects its non-string version.
- Step 9: Add the bump-only preflight
Add this helper after declared_files in scripts/bump-version.sh:
preflight_manifests() {
local path field fullpath
require_tool jq || return 1
while IFS=$'\t' read -r path field; do
fullpath="$REPO_ROOT/$path"
[[ -f "$fullpath" ]] || continue
if ! read_manifest_field "$fullpath" "$field" >/dev/null; then
echo "error: cannot read declared manifest: $path ($field)" >&2
return 1
fi
done < <(declared_files)
}
Call it in cmd_bump after version-format validation and before the first bump output or write:
preflight_manifests
echo "Bumping all declared files to $new_version..."
- Step 10: Run focused verification
Run:
/bin/bash tests/version-bump/test-bump-version.sh
scripts/lint-shell.sh scripts/bump-version.sh tests/version-bump/test-bump-version.sh
scripts/bump-version.sh --check
git diff --check
Expected:
-
The behavioral test prints
Version-bump tests passed. -
Shell lint reports both scripts with no errors.
-
--checklists eight declared manifests, including.hermes-plugin/plugin.yaml, all at6.2.0. -
git diff --checkprints nothing. -
Step 11: Review and commit the implementation
Run:
git status --short
git diff -- .version-bump.json scripts/bump-version.sh tests/version-bump/test-bump-version.sh
git add .version-bump.json scripts/bump-version.sh tests/version-bump/test-bump-version.sh
git commit \
-m "fix(release): wire Hermes into version bumps" \
-m "Register the Hermes YAML manifest alongside the existing JSON manifests. Route manifest reads and writes by extension through jq or Mike Farah yq v4, with field names and values passed as data." \
-m "Preflight every present manifest before the mutating bump loop so a deterministic YAML read failure cannot leave earlier JSON manifests partially updated. Cover check, audit, bump, registry wiring, and byte-for-byte no-partial-write behavior with one focused fixture test."
Expected: the commit succeeds with only the three implementation paths staged.