mirror of
https://github.com/obra/superpowers.git
synced 2026-08-08 07:08:46 +08:00
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.
This commit is contained in:
committed by
Jesse Vincent
parent
3e1ecde38f
commit
707b155a38
304
docs/superpowers/plans/2026-08-06-hermes-version-bump-wiring.md
Normal file
304
docs/superpowers/plans/2026-08-06-hermes-version-bump-wiring.md
Normal file
@@ -0,0 +1,304 @@
|
|||||||
|
# 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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git fetch origin dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: command exits 0 and refreshes `origin/dev`.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Rebase the task branch**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/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`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "path": ".hermes-plugin/plugin.yaml", "field": "version" },
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 6: Run the initial test to verify GREEN**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/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`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/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`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
preflight_manifests
|
||||||
|
|
||||||
|
echo "Bumping all declared files to $new_version..."
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 10: Run focused verification**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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.
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
**Date:** 2026-08-05
|
**Date:** 2026-08-05
|
||||||
**Revised:** 2026-08-06
|
**Revised:** 2026-08-06
|
||||||
**Status:** Draft for Drew review
|
**Status:** Approved
|
||||||
|
|
||||||
## Goal
|
## Goal
|
||||||
|
|
||||||
@@ -21,11 +21,11 @@ to process YAML without implementing a YAML parser in Bash.
|
|||||||
are out of scope.
|
are out of scope.
|
||||||
- Route `--check`, `--audit`, and version updates through the same small
|
- Route `--check`, `--audit`, and version updates through the same small
|
||||||
read/write dispatcher.
|
read/write dispatcher.
|
||||||
- Before any non-help command, run one read-only preflight that validates the
|
- Before a version bump writes any manifest, run one read-only preflight that
|
||||||
required tools and configured extensions, then reads every present declared
|
validates the required tools and reads every present declared manifest
|
||||||
manifest. This prevents a deterministic YAML failure from occurring after
|
through the dispatcher. This prevents a deterministic YAML failure from
|
||||||
earlier JSON files have already been updated. Missing-file behavior remains
|
occurring after earlier JSON files have already been updated. Missing-file
|
||||||
unchanged, and `--help` still works without `jq` or `yq`.
|
behavior remains unchanged, and `--help` still works without `jq` or `yq`.
|
||||||
|
|
||||||
The preflight is the only reliability addition. It does not make the script
|
The preflight is the only reliability addition. It does not make the script
|
||||||
transactional or redesign its existing audit and error-status behavior.
|
transactional or redesign its existing audit and error-status behavior.
|
||||||
@@ -35,12 +35,15 @@ transactional or redesign its existing audit and error-status behavior.
|
|||||||
Three focused behavioral tests run the real script against an isolated
|
Three focused behavioral tests run the real script against an isolated
|
||||||
temporary fixture and prove:
|
temporary fixture and prove:
|
||||||
|
|
||||||
- aligned JSON and YAML pass `--check`, and a bump updates both formats;
|
- aligned JSON and YAML pass `--check` and `--audit`, and a bump updates both
|
||||||
- a preflight failure leaves every manifest unchanged; and
|
formats;
|
||||||
|
- an actual bump with JSON declared first and a later YAML manifest whose
|
||||||
|
top-level `version` is not a string exits nonzero and leaves every manifest
|
||||||
|
byte-for-byte unchanged; and
|
||||||
- the real `.version-bump.json` registers the Hermes manifest.
|
- the real `.version-bump.json` registers the Hermes manifest.
|
||||||
|
|
||||||
Verification also runs the existing Hermes tests, shell lint, and
|
Verification also runs shell lint and `scripts/bump-version.sh --check` against
|
||||||
`scripts/bump-version.sh --check` against the repository.
|
the repository.
|
||||||
|
|
||||||
## Non-Goals
|
## Non-Goals
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user