Files
superpowers/docs/superpowers/plans/2026-08-06-hermes-version-bump-wiring.md
Drew Ritter 707b155a38 docs: plan Hermes version-bump wiring
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.
2026-08-06 16:21:12 -07:00

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 .json and .yaml; .yml and 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 yq expression.
  • Keep yq confined to maintainer release tooling; do not add a plugin runtime dependency.
  • Preserve the existing missing-file behavior: --check reports 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.yaml at top-level field version.

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.json records shaped as { "path": string, "field": string }.

  • Produces: read_manifest_field FILE FIELD, write_manifest_field FILE FIELD VALUE, and preflight_manifests Bash 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.

  • --check lists eight declared manifests, including .hermes-plugin/plugin.yaml, all at 6.2.0.

  • git diff --check prints 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.