diff --git a/.claude/skills/hyper-api-release-verify-upcoming-packages/SKILL.md b/.claude/skills/hyper-api-release-verify-upcoming-packages/SKILL.md
new file mode 100644
index 0000000..347f138
--- /dev/null
+++ b/.claude/skills/hyper-api-release-verify-upcoming-packages/SKILL.md
@@ -0,0 +1,56 @@
+---
+name: hyper-api-release-verify-upcoming-packages
+description: Verify that all Tableau Hyper DB release packages on the upcoming releases page are downloadable and contain valid zip archives with the correct version number in their filenames. Use when verifying Hyper API release packages before or after publishing.
+---
+
+# Verify Hyper Releases
+
+Verifies that every package advertised on the Tableau Hyper DB upcoming releases page is downloadable and contains a valid zip archive whose filename carries the advertised version number.
+
+The deterministic workflow (fetch, parse, download, integrity-check, report) lives in the bundled `verify_release.py`. This SKILL.md is a thin wrapper that tells you when and how to invoke it.
+
+## When to Use
+
+- User asks to verify, check, or validate an upcoming Hyper API / Hyper DB release.
+- User mentions the releases page at `tableau.github.io/hyper-db/upcoming/docs/releases`.
+- User wants to confirm the published `.zip` / `.whl` artifacts are reachable and not corrupt, before or after publishing.
+
+## How to Run
+
+The script path below is relative to this SKILL.md's directory.
+
+```bash
+python3 verify_release.py [--version X.Y.ZZZZZ] [--keep]
+```
+
+### Flags
+
+- `--version X.Y.ZZZZZ` — Expected version (e.g. `0.0.25080`). The script asserts the releases page advertises exactly this version and exits 2 on mismatch before downloading anything. Use when the user names a specific release to verify.
+- `--keep` — Keep the temp download directory even when all checks pass. Use when the user wants to inspect or reuse the downloaded artifacts afterward.
+
+The script prints progress to stdout and a markdown summary table at the end. Relay that table and the `OVERALL: PASS` / `OVERALL: FAIL` verdict back to the user verbatim. On failure, include the preserved temp directory path so the user can inspect.
+
+## What the Script Does
+
+1. Fetches the releases page and extracts the advertised version and every `.zip` / `.whl` download URL. With `--version`, asserts the page matches before going further.
+2. Creates a temp directory for downloads.
+3. For each package: checks the version string appears in the filename, downloads via Python's `urllib` (120s socket timeout, follows redirects), and verifies zip/whl archive integrity using `zipfile.testzip()`.
+4. Deletes the temp directory on full success, unless `--keep` is set; keeps it and prints its path on any failure.
+5. Prints a markdown summary table plus `OVERALL: PASS` or `OVERALL: FAIL`.
+
+## Exit Codes
+
+| Code | Meaning |
+| :---: | --- |
+| 0 | All packages passed every check |
+| 1 | One or more checks failed (temp dir preserved for inspection) |
+| 2 | Setup error: page unreachable, version not found, no download URLs, or `--version` mismatch |
+
+## Expected Package Count
+
+At time of writing, the page advertises 12 packages (4 platforms x 3 language bindings: Python wheel, C++ zip, Java zip). This count is advisory and **will change** as platforms or bindings are added, removed, or renamed. The script prints a warning but continues if the count differs — treat a mismatch as worth mentioning, not as a hard failure, and consider updating this section if the new count is the new steady state.
+
+## Requirements
+
+- Python 3.10+ (standard library only — `urllib`, `zipfile`, `argparse`, `shutil`)
+- Outbound network access to `tableau.github.io` and `downloads.tableau.com`
diff --git a/.claude/skills/hyper-api-release-verify-upcoming-packages/verify_release.py b/.claude/skills/hyper-api-release-verify-upcoming-packages/verify_release.py
new file mode 100755
index 0000000..ec61362
--- /dev/null
+++ b/.claude/skills/hyper-api-release-verify-upcoming-packages/verify_release.py
@@ -0,0 +1,222 @@
+#!/usr/bin/env python3
+"""Verify Tableau Hyper DB release packages.
+
+Fetches the upcoming releases page, extracts the current version and all
+`.zip`/`.whl` download URLs, then for each package:
+
+ * verifies the version string appears in the filename,
+ * downloads it via urllib (follows redirects, 120s timeout),
+ * checks zip archive integrity (whl files are zip archives internally).
+
+Uses only the Python standard library — no external tools required.
+
+Prints a markdown summary table and exits:
+
+ 0 — all checks passed (temp dir deleted unless --keep)
+ 1 — one or more checks failed (temp dir preserved, path printed)
+ 2 — setup error (page unreachable, no version, or version mismatch)
+"""
+
+from __future__ import annotations
+
+import argparse
+import re
+import shutil
+import sys
+import tempfile
+import urllib.request
+import zipfile
+from pathlib import Path
+
+RELEASES_URL = "https://tableau.github.io/hyper-db/upcoming/docs/releases"
+EXPECTED_LINK_COUNT = 12 # 4 platforms x 3 language bindings; advisory only
+FETCH_TIMEOUT_SECONDS = 30 # small HTML page, kept separate from package downloads
+DOWNLOAD_TIMEOUT_SECONDS = 120
+
+
+def fetch_releases_page() -> str:
+ req = urllib.request.Request(
+ RELEASES_URL, headers={"User-Agent": "verify-upcoming-hyper-release/1.0"}
+ )
+ with urllib.request.urlopen(req, timeout=FETCH_TIMEOUT_SECONDS) as resp:
+ return resp.read().decode("utf-8")
+
+
+def extract_version(html: str) -> str | None:
+ # The page renders "latest available version is v0.0.NNNNN",
+ # so the v and the digits are split by an HTML comment in the raw source.
+ # Strip HTML comments before matching.
+ stripped = re.sub(r"", "", html, flags=re.DOTALL)
+ m = re.search(
+ r"latest available version is[^<]*v(0\.0\.\d+)",
+ stripped,
+ flags=re.IGNORECASE,
+ )
+ if m:
+ return m.group(1)
+ # Fallback: any v0.0.NNNNN after comment stripping.
+ m = re.search(r"v(0\.0\.\d+)", stripped)
+ return m.group(1) if m else None
+
+
+def extract_download_urls(html: str) -> list[str]:
+ pattern = r'https?://[^\s"\'<>]*downloads\.tableau\.com/[^\s"\'<>]+?\.(?:zip|whl)'
+ seen: set[str] = set()
+ urls: list[str] = []
+ for url in re.findall(pattern, html):
+ if url not in seen:
+ seen.add(url)
+ urls.append(url)
+ return urls
+
+
+def download_file(url: str, dest: Path) -> tuple[bool, str]:
+ # urllib follows redirects by default; timeout covers the full transfer
+ # only in the sense that the socket read will error if it stalls.
+ req = urllib.request.Request(
+ url, headers={"User-Agent": "verify-upcoming-hyper-release/1.0"}
+ )
+ try:
+ with urllib.request.urlopen(req, timeout=DOWNLOAD_TIMEOUT_SECONDS) as resp:
+ with dest.open("wb") as f:
+ shutil.copyfileobj(resp, f)
+ return True, ""
+ except Exception as e:
+ # Best-effort: remove partial file so the zip check doesn't see a stub
+ dest.unlink(missing_ok=True)
+ return False, str(e)
+
+
+def check_zip(path: Path) -> tuple[bool, str | None]:
+ try:
+ with zipfile.ZipFile(path) as zf:
+ bad = zf.testzip()
+ if bad is None:
+ return True, None
+ return False, f"bad file in archive: {bad}"
+ except Exception as e:
+ return False, str(e)
+
+
+def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
+ parser = argparse.ArgumentParser(
+ description="Verify Tableau Hyper DB release packages are downloadable and valid."
+ )
+ parser.add_argument(
+ "--version",
+ metavar="X.Y.ZZZZZ",
+ help=(
+ "Expected version string (e.g. 0.0.25080). If set, the script asserts "
+ "the releases page advertises this version and fails fast otherwise."
+ ),
+ )
+ parser.add_argument(
+ "--keep",
+ action="store_true",
+ help="Keep the temp download directory even when all checks pass.",
+ )
+ return parser.parse_args(argv)
+
+
+def main(argv: list[str] | None = None) -> int:
+ args = parse_args(argv)
+
+ # Step 1: fetch and parse
+ print(f"Fetching {RELEASES_URL} ...")
+ try:
+ html = fetch_releases_page()
+ except Exception as e:
+ print(f"ERROR: failed to fetch releases page: {e}", file=sys.stderr)
+ return 2
+
+ version = extract_version(html)
+ if not version:
+ print("ERROR: could not identify version number on releases page", file=sys.stderr)
+ return 2
+
+ if args.version and args.version != version:
+ print(
+ f"ERROR: expected version {args.version} but releases page advertises {version}",
+ file=sys.stderr,
+ )
+ return 2
+
+ urls = extract_download_urls(html)
+ if not urls:
+ print("ERROR: no download URLs found on releases page", file=sys.stderr)
+ return 2
+
+ print(f"Version: {version}")
+ print(f"Found {len(urls)} download links")
+ if len(urls) != EXPECTED_LINK_COUNT:
+ print(
+ f"WARNING: expected {EXPECTED_LINK_COUNT} links "
+ f"(4 platforms x 3 bindings), got {len(urls)}"
+ )
+
+ # Step 2: create temp dir
+ tmpdir = Path(tempfile.mkdtemp(prefix="verify-hyper-"))
+ print(f"Temp dir: {tmpdir}")
+
+ # Steps 3-5: check each package
+ results: list[dict[str, object]] = []
+ for i, url in enumerate(urls, 1):
+ filename = url.rsplit("/", 1)[-1]
+ row: dict[str, object] = {"num": i, "filename": filename}
+
+ row["version_match"] = "PASS" if version in filename else "FAIL"
+
+ dest = tmpdir / filename
+ print(f"[{i}/{len(urls)}] Downloading {filename} ...")
+ ok, err = download_file(url, dest)
+ row["download"] = "PASS" if ok else "FAIL"
+ if not ok:
+ if err:
+ print(f" download failed: {err}")
+ row["zip_valid"] = "SKIP"
+ else:
+ ok, zip_err = check_zip(dest)
+ row["zip_valid"] = "PASS" if ok else "FAIL"
+ if not ok:
+ print(f" zip check failed: {zip_err}")
+
+ results.append(row)
+
+ # Step 6: cleanup
+ all_pass = all(
+ r["version_match"] == "PASS"
+ and r["download"] == "PASS"
+ and r["zip_valid"] == "PASS"
+ for r in results
+ )
+ kept = not all_pass or args.keep
+ if not kept:
+ shutil.rmtree(tmpdir, ignore_errors=True)
+
+ # Step 7: summary
+ print()
+ print("## Verification Results")
+ print()
+ print(f"**Version:** {version}")
+ print(f"**Packages found:** {len(urls)}")
+ print()
+ print("| # | Package | Version Match | Download | Zip Valid |")
+ print("|---|---------|:---:|:---:|:---:|")
+ for r in results:
+ print(
+ f"| {r['num']} | {r['filename']} | "
+ f"{r['version_match']} | {r['download']} | {r['zip_valid']} |"
+ )
+ print()
+ if all_pass:
+ print("**OVERALL: PASS**")
+ if kept:
+ print(f"Downloads preserved in: {tmpdir}")
+ return 0
+ print("**OVERALL: FAIL**")
+ print(f"Failed files preserved in: {tmpdir}")
+ return 1
+
+
+if __name__ == "__main__":
+ sys.exit(main(sys.argv[1:]))
diff --git a/.claude/skills/update-hyperd-release/SKILL.md b/.claude/skills/update-hyperd-release/SKILL.md
new file mode 100644
index 0000000..9b25f6b
--- /dev/null
+++ b/.claude/skills/update-hyperd-release/SKILL.md
@@ -0,0 +1,148 @@
+---
+name: update-hyperd-release
+description: Use when bumping the pinned hyperd release for hyperdb-bootstrap — finding the latest Tableau Hyper API version, updating hyperd-version.toml (version + build_id + 4 sha256s), verifying the pin, running the full test suite, A/B benchmarking against the previous pin, logging the result per release, and opening the PR.
+---
+
+# Update the pinned `hyperd` release
+
+Bumps the `hyperd` binary that `hyperdb-bootstrap` downloads, then proves the new
+engine is correct and measures how it moved performance. Codifies the procedure in
+[AGENTS.md](../../../AGENTS.md) ("Bootstrapping `hyperd`") plus the operational
+gotchas learned in practice.
+
+## Key facts (don't relearn these the hard way)
+
+- **The pin lives in [`hyperdb-bootstrap/hyperd-version.toml`](../../../hyperdb-bootstrap/hyperd-version.toml)** — `version`, `build_id`, and four per-platform sha256s. That's the whole source of truth; contributors without an override get exactly this release.
+- **We download the Java bundle, NOT the C++ one.** The C++ `macos-arm64` zip ships an **x86_64** `hyperd` (upstream packaging defect) that only runs under Rosetta on Apple Silicon. The Java `macos-arm64` bundle carries a **native arm64** `hyperd`. Same URL template (only the `java`/`cxx` token differs), same internal layout (`lib/hyper/hyperd`). **Verify this invariant every bump** (step 5) — if a future Java bundle regresses to x86_64, the whole reason for using it is gone.
+- **URL template:** `https://downloads.tableau.com/tssoftware/tableauhyperapi-java--release-main...zip` — platforms: `macos-arm64`, `macos-x86_64`, `linux-x86_64`, `windows-x86_64`.
+- **Crate version is workspace-driven + release-please.** `hyperdb-bootstrap` uses `version.workspace = true`; do **not** hand-edit a crate version. The conventional-commit type drives the release — use `fix(bootstrap): ...` for a routine bump (patch release).
+- **Never invent `hyperd` flags** (AGENTS.md reminder #9) and **never report tests/benches green without real output** (#10). Tests start a real `hyperd` subprocess; a misconfigured server hangs rather than erroring.
+
+## Procedure
+
+Track these as todos. Each step gates the next.
+
+### 1. Create a branch
+
+```bash
+git checkout -b chore/bump-hyperd- # e.g. chore/bump-hyperd-0.0.26225
+```
+
+### 2. Find the latest version + build id
+
+```bash
+curl -sL "https://tableau.github.io/hyper-db/docs/releases" | rg -o "0\.0\.[0-9]+" | head -1
+curl -sL "https://tableau.github.io/hyper-db/docs/releases" | \
+ rg -o "tableauhyperapi-java-[a-z0-9_-]+-release-main\.\.r[a-z0-9]+\.zip" | sort -u
+```
+
+Confirm all four platform zips are listed for that version and share one build id.
+(The [`hyper-api-release-verify-upcoming-packages`](../hyper-api-release-verify-upcoming-packages/SKILL.md)
+skill — bundled `verify_release.py` — validates the whole page's downloadability and
+zip integrity for a given `--version`.)
+
+### 3. Compute the four sha256s
+
+Download each Java zip and hash it. The values go verbatim into the toml.
+
+```bash
+V=; B=; cd "$(mktemp -d)"
+for p in macos-arm64 macos-x86_64 linux-x86_64 windows-x86_64; do
+ curl -sL --fail -o "$p.zip" \
+ "https://downloads.tableau.com/tssoftware/tableauhyperapi-java-$p-release-main.$V.$B.zip" &
+done; wait
+for p in macos-arm64 macos-x86_64 linux-x86_64 windows-x86_64; do
+ printf '%-16s ' "$p"; shasum -a 256 "$p.zip" | awk '{print $1}'
+done
+```
+
+### 4. Edit `hyperd-version.toml`
+
+Update `version`, `build_id`, and all four `[sha256]` entries. Record the **old**
+version/build_id first — you need it for the A/B benchmark (step 7).
+
+### 5. Verify the pin + the arm64 invariant
+
+```bash
+make verify-hyperd-pin # all four platforms → HTTP 200 at the new pin
+make download-hyperd # re-verifies the macos-arm64 sha256 on download
+.hyperd/current/hyperd --version # should report main..
+file .hyperd/current/hyperd # MUST say "Mach-O 64-bit executable arm64" on Apple Silicon
+```
+
+If `file` reports `x86_64`, **stop** — the Java bundle no longer carries a native
+arm64 binary and the bundle choice needs re-evaluation.
+
+### 6. Run the full test suite against the NEW engine
+
+Point `HYPERD_PATH` at the freshly downloaded binary — do **not** rely on the
+workstation default (`~/dev/bin/hyperd`), which may be an old or unversioned build.
+
+```bash
+export HYPERD_PATH="$PWD/.hyperd/current/hyperd"
+cargo test --workspace 2>&1 | rg "test result:" | \
+ awk '{p+=$4; f+=$6} END {print "TOTAL passed="p" failed="f}'
+```
+
+Require `failed=0`. Then the pre-commit gate: `cargo fmt --all -- --check` and
+`cargo clippy --workspace --all-targets --all-features -- -D warnings` (CI's exact
+clippy command).
+
+### 7. A/B benchmark vs the previous pin
+
+The canonical harness is the **unified suite**
+([`hyperdb-api/benches/benchmark_suite.rs`](../../../hyperdb-api/benches/benchmark_suite.rs)).
+Download the **old** pin into a separate dir, then run the same suite on both.
+See [docs/BENCHMARK_GUIDE.md](../../../docs/BENCHMARK_GUIDE.md) for the harness details.
+
+```bash
+# Old engine into a scratch dir (sha256 skipped — that's fine for a throwaway baseline)
+cargo run --release -p hyperdb-bootstrap --bin hyperdb-bootstrap -- \
+ download --version --build-id --dest .hyperd-old
+
+cargo build -q -p hyperdb-api --release --example benchmark_suite
+BIN=target/release/examples/benchmark_suite; ROWS=100000000 # 100M for signal over noise
+
+# 3 runs each so you can take medians, not single noisy samples.
+for i in 1 2 3; do HYPERD_PATH="$PWD/.hyperd-old/current/hyperd" "$BIN" $ROWS 4 2>&1 | rg "· " | rg "sync|async"; done
+for i in 1 2 3; do HYPERD_PATH="$PWD/.hyperd/current/hyperd" "$BIN" $ROWS 4 2>&1 | rg "· " | rg "sync|async"; done
+
+rm -rf .hyperd-old # clean up the scratch baseline (also add to .gitignore if you keep it)
+```
+
+**Benchmark caveats — do not skip:**
+- **Use medians of ≥3 runs at 100M rows.** Single sub-second 10M-row runs have huge run-to-run variance; a "regression" at that size is usually noise (proven on the 0.0.26225 bump — a −20% insert delta at 10M vanished to −5–7% at 100M).
+- **Distrust `× 4` / parallel numbers on a laptop.** They throttle thermally — throughput declines monotonically across sequential runs because the machine is hotter for the second engine. Report single-connection deltas as the reliable signal; withhold multi-connection deltas unless run on a cooled/pinned host.
+- Report throughput as **M rows/s**, not wall time.
+
+### 8. Log the release in the benchmark tracker
+
+Append a row per engine to
+[`docs/hyperd-release-benchmarks.md`](../../../docs/hyperd-release-benchmarks.md)
+(median single-connection numbers + the machine + the caveat). This builds the
+per-release history the BENCHMARK_GUIDE's by-platform tables don't capture.
+
+### 9. Changelog
+
+Add a `### Changed` bullet under `## [Unreleased]` in
+[`hyperdb-bootstrap/CHANGELOG.md`](../../../hyperdb-bootstrap/CHANGELOG.md): the new
+version/build, "verified native arm64", and the headline performance A/B (with the
+thermal caveat on multi-connection numbers).
+
+### 10. Commit + PR
+
+- Commit with `git add ` (never `-A`), type `fix(bootstrap): bump pinned hyperd to ()`.
+- **gh account:** the EMU account (`ssteiner_sfemu`) is Unauthorized on upstream. `gh auth switch --hostname github.com --user StefanSteiner`, then target upstream (it has the CI runners): `gh pr create --repo tableau/hyper-api-rust --base main --head StefanSteiner:`.
+- Put the verification checklist + performance table in the PR body.
+
+## Verification checklist (what "done" means)
+
+- [ ] `make verify-hyperd-pin` → all four platforms HTTP 200
+- [ ] `.hyperd/current/hyperd --version` reports the new version/build
+- [ ] `file` confirms macos-arm64 binary is native arm64
+- [ ] `cargo test --workspace` → `failed=0` against the new engine
+- [ ] `cargo fmt --check` + CI-exact `cargo clippy` clean
+- [ ] A/B benchmark done (medians of ≥3 runs @ 100M rows); scratch `.hyperd-old` removed
+- [ ] Row appended to `docs/hyperd-release-benchmarks.md`
+- [ ] CHANGELOG `[Unreleased]` bullet added
+- [ ] PR opened against `tableau/hyper-api-rust` from `StefanSteiner:`
diff --git a/.claude/workflows/plan-to-release.js b/.claude/workflows/plan-to-release.js
new file mode 100644
index 0000000..78f9fb2
--- /dev/null
+++ b/.claude/workflows/plan-to-release.js
@@ -0,0 +1,604 @@
+/**
+ * plan-to-release — a reusable Harness driver that takes a *vetted* implementation
+ * plan and drives it from adversarial review through release-ready, the manual
+ * release, and a post-publish npm smoke test.
+ *
+ * This is the offline, operator-gated, role-separated ("Harness") workflow:
+ * the main thread is the command center, every fan-out is an explicit phase,
+ * and read-only validators gate the merge (doer != validator != merger).
+ *
+ * Invoke by name, e.g.:
+ * Workflow({ name: 'plan-to-release', args: {
+ * planPath: 'docs/superpowers/plans/2026-07-11-kv-mcp-llm-ergonomics.md',
+ * specPath: 'docs/superpowers/specs/2026-07-10-kv-mcp-llm-ergonomics-design.md',
+ * issue: 192,
+ * branch: 'feat/kv-mcp-llm-ergonomics-192',
+ * mode: 'full', // vet | execute | sweep | release | smoke | full
+ * releaseTag: 'v0.7.0', // required for mode release/smoke
+ * releaseStage: 'pr', // for mode release: 'pr' | 'publish'
+ * npmPackage: 'hyperdb-mcp'
+ * }})
+ *
+ * MODES
+ * - vet : adversarial plan review (2 reviewer lenses) + independent verify.
+ * Returns { blocking, findings }. If blocking, DO NOT execute — revise the plan.
+ * - execute : parse plan -> sequential iterations. Each: engineer implements,
+ * real build/test/clippy/fmt gate (captured output), adversarial review, commit.
+ * - sweep : full E2E verification of the integrated branch + final adversarial
+ * sweep (both reviewer lenses) -> confidence verdict.
+ * - release : mechanical release automation with a 2-try check valve.
+ * releaseStage 'pr' -> push fork branch, open/refresh upstream PR, wait CI green.
+ * releaseStage 'publish' -> merge release-please PR, HAND-CREATE the vX.Y.Z tag+release
+ * (repo sets skip-github-release: true), trigger both publish
+ * workflows, wait them green. Irreversible steps are reported,
+ * not silently forced.
+ * - smoke : force-pull the freshly published npm package and exercise the new KV features
+ * through a CLEAN-ROOM `npx @ --ephemeral-only --no-daemon` server,
+ * gated on the server's self-reported version. The session's connected MCP may be
+ * config-pinned to an OLDER version, so it is NOT trusted as the source of truth.
+ * - full : vet -> (gate) -> execute -> (gate) -> sweep. STOPS at release-ready.
+ * release + smoke are invoked separately (human-gated + post-publish timing).
+ */
+
+export const meta = {
+ name: 'plan-to-release',
+ description: 'Drive a vetted implementation plan through adversarial review, execution, E2E sweep, release, and npm smoke test',
+ whenToUse: 'When a vetted plan file exists and you want an operator-gated, role-separated agent team to drive it to a release-ready branch (and optionally through the manual release + npm smoke test).',
+ phases: [
+ { title: 'Vet', detail: 'Two reviewer lenses adversarially review the plan; each finding is independently verified' },
+ { title: 'Execute', detail: 'Parse plan; per iteration: engineer implements, build/test/clippy/fmt gate, adversarial review, commit' },
+ { title: 'Sweep', detail: 'Full E2E verification + final adversarial sweep across both reviewer lenses' },
+ { title: 'Release', detail: 'Push fork branch, open upstream PR, wait CI green; publish stage tags + triggers publish workflows (2-try check valve)' },
+ { title: 'Smoke', detail: 'Clean-room npx spawn of the published package (--ephemeral-only --no-daemon), version-gated, exercising new KV features — session MCP not trusted' },
+ ],
+}
+
+// ---------------------------------------------------------------------------
+// Config (from args, with defaults tuned to this repo/workstation)
+// ---------------------------------------------------------------------------
+const REPO = (args && args.repo) || '/Users/ssteiner/dev/hyper-api-rust'
+const planPath = args && args.planPath
+const specPath = (args && args.specPath) || null
+const issue = (args && args.issue) || null
+const branch = (args && args.branch) || null
+const mode = (args && args.mode) || 'full'
+const releaseTag = (args && args.releaseTag) || null
+const releaseStage = (args && args.releaseStage) || 'pr'
+const npmPackage = (args && args.npmPackage) || 'hyperdb-mcp'
+const upstream = (args && args.upstream) || 'tableau/hyper-api-rust'
+const forkOwner = (args && args.forkOwner) || 'StefanSteiner'
+const hyperdPath = (args && args.hyperdPath) || '~/dev/bin/hyperd'
+// Max iterations dispatched from a parsed plan (backstop; logged if exceeded).
+const MAX_ITERATIONS = (args && args.maxIterations) || 24
+
+// Shared context every agent needs (they do NOT see this conversation).
+const REPO_CTX = `You are working in the Rust workspace at ${REPO} (a fork of ${upstream}).
+Non-negotiable project rules (from AGENTS.md):
+- Tests start a real hyperd subprocess: ALWAYS export HYPERD_PATH=${hyperdPath} for cargo test/run.
+- NEVER invent hyperd flags or engine parameters. Start servers only via HyperProcess::new() in tests or the Makefile targets.
+- NEVER report a build/test as passing without seeing REAL captured output and a 0 exit code. If a command emits nothing for ~30s, treat it as HANGING/FAILED and say so.
+- Match CI exactly for the lint gate: \`cargo clippy --workspace --all-targets --all-features -- -D warnings\` and \`cargo fmt --all --check\`, on the repo's pinned stable toolchain.
+- BAN narrowing integer \`as\` casts (e.g. i64 as usize, usize as i64, i128 as i64). Use TryFrom; the codebase treats narrowing casts as latent data-corruption. Flag/convert any you touch.
+- Propagate errors with \`?\`; never panic in library code.
+- Keep sync and async twins in lockstep (KvStore <-> AsyncKvStore).
+- Conventional Commits; commit with explicit \`git add \`, never \`git add -A\`.
+- release-please owns version numbers + the root CHANGELOG.md (x-release-please markers, extra-files). Do NOT hand-edit Cargo.toml versions, the workspace version, or root CHANGELOG.md. Per-crate CHANGELOG.md \`## [Unreleased]\` bullets ARE hand-maintained (AGENTS.md rule 8).
+- GitHub: the active github.com account must be \`${forkOwner}\` (the EMU account is Unauthorized on upstream). PRs target upstream (\`${upstream}\`, which has CI runners) with \`--head ${forkOwner}:\`.`
+
+// Harness execution constraints for any agent that polls the network or waits on
+// CI. Learned the hard way on the v0.7.0 run: a single blocking \`gh ... --watch\`
+// (or a \`sleep 100 && gh api ...\`) exceeds the ~2-minute Bash ceiling and is
+// KILLED (Exit 143), which reads as a spurious hang/failure — not a real result.
+const POLL_CTX = `
+Harness polling constraints (do NOT ignore — a killed command is a false failure, not a result):
+- The Bash tool has a hard ~2-minute wall-clock ceiling. A single foreground command that blocks longer is KILLED with Exit 143.
+- Do NOT wait on CI with one blocking \`gh run watch --exit-status\` or \`gh pr checks --watch\` — a real CI run outlasts the ceiling and the watch gets killed mid-wait.
+- Instead POLL: loop over short, non-blocking status queries (\`gh run list\`, \`gh pr checks \`, \`gh api .../check-runs\`) with a bounded sleep BETWEEN them. Keep each sleep <= 90s when the polled command is itself a network round-trip (the sleep + the API call together must stay under the ceiling); <= 100s is fine only when the polled command is a fast local check.
+- Treat "no output for ~30s from a command you expected to print" as HANGING/FAILED and report it, per AGENTS.md rule 10.`
+
+// ---------------------------------------------------------------------------
+// Schemas
+// ---------------------------------------------------------------------------
+const FINDINGS_SCHEMA = {
+ type: 'object',
+ properties: {
+ findings: {
+ type: 'array',
+ items: {
+ type: 'object',
+ properties: {
+ id: { type: 'string' },
+ severity: { type: 'string', enum: ['critical', 'major', 'minor', 'nit'] },
+ category: { type: 'string' },
+ claim: { type: 'string', description: 'the exact plan/spec statement or omission at issue' },
+ problem: { type: 'string', description: 'why it is wrong/risky, with source evidence (file:line) actually checked' },
+ fix: { type: 'string', description: 'concrete suggested change' },
+ },
+ required: ['id', 'severity', 'category', 'claim', 'problem', 'fix'],
+ },
+ },
+ },
+ required: ['findings'],
+}
+
+const VERDICT_SCHEMA = {
+ type: 'object',
+ properties: {
+ verdict: { type: 'string', enum: ['CONFIRMED', 'REJECTED', 'PARTIAL'] },
+ reasoning: { type: 'string' },
+ corrected_fix: { type: 'string' },
+ },
+ required: ['verdict', 'reasoning'],
+}
+
+const PLAN_PARSE_SCHEMA = {
+ type: 'object',
+ properties: {
+ iterations: {
+ type: 'array',
+ items: {
+ type: 'object',
+ properties: {
+ n: { type: 'integer' },
+ title: { type: 'string' },
+ files: { type: 'array', items: { type: 'string' } },
+ acceptance: { type: 'array', items: { type: 'string' }, description: 'concrete acceptance criteria the reviewer can check' },
+ commit_type: { type: 'string', description: 'conventional-commit type, e.g. feat, fix, feat!, docs, test' },
+ commit_message: { type: 'string' },
+ },
+ required: ['n', 'title', 'files', 'acceptance', 'commit_message'],
+ },
+ },
+ notes: { type: 'string' },
+ },
+ required: ['iterations'],
+}
+
+const ITERATION_RESULT_SCHEMA = {
+ type: 'object',
+ properties: {
+ iteration: { type: 'integer' },
+ implemented: { type: 'boolean' },
+ gate_passed: { type: 'boolean', description: 'true ONLY if clippy + fmt + relevant tests all passed with captured 0-exit output' },
+ gate_output_summary: { type: 'string', description: 'real command output excerpts proving pass/fail' },
+ files_changed: { type: 'array', items: { type: 'string' } },
+ committed: { type: 'boolean' },
+ commit_sha: { type: 'string' },
+ blockers: { type: 'array', items: { type: 'string' } },
+ notes: { type: 'string' },
+ },
+ required: ['iteration', 'implemented', 'gate_passed', 'gate_output_summary', 'committed'],
+}
+
+const SWEEP_SCHEMA = {
+ type: 'object',
+ properties: {
+ e2e_passed: { type: 'boolean' },
+ commands_run: { type: 'array', items: { type: 'string' } },
+ confidence: { type: 'string', enum: ['high', 'medium', 'low'] },
+ blocking_issues: { type: 'array', items: { type: 'string' } },
+ summary: { type: 'string' },
+ },
+ required: ['e2e_passed', 'confidence', 'summary'],
+}
+
+const RELEASE_SCHEMA = {
+ type: 'object',
+ properties: {
+ stage: { type: 'string' },
+ success: { type: 'boolean' },
+ attempts: { type: 'integer' },
+ pr_url: { type: 'string' },
+ ci_status: { type: 'string', description: 'success | failure | pending | unknown' },
+ actions_taken: { type: 'array', items: { type: 'string' } },
+ manual_commands: { type: 'array', items: { type: 'string' }, description: 'exact commands the human/main-thread must run for irreversible or blocked steps' },
+ notes: { type: 'string' },
+ },
+ required: ['stage', 'success', 'attempts', 'notes'],
+}
+
+const SMOKE_SCHEMA = {
+ type: 'object',
+ properties: {
+ resolved_version: { type: 'string', description: 'the version npm actually served / the fresh binary reported' },
+ expected_version: { type: 'string' },
+ version_match: { type: 'boolean' },
+ tests: {
+ type: 'array',
+ items: {
+ type: 'object',
+ properties: {
+ name: { type: 'string' },
+ passed: { type: 'boolean' },
+ detail: { type: 'string' },
+ },
+ required: ['name', 'passed', 'detail'],
+ },
+ },
+ all_passed: { type: 'boolean' },
+ summary: { type: 'string' },
+ },
+ required: ['resolved_version', 'tests', 'all_passed', 'summary'],
+}
+
+// ---------------------------------------------------------------------------
+// Helpers
+// ---------------------------------------------------------------------------
+
+// Adversarially review a target with two reviewer lenses, then independently
+// verify every finding; keep only CONFIRMED/PARTIAL. Used for both plan-vet and
+// the final sweep. `phaseName` groups agents in the progress display.
+async function reviewAndVerify(targetDescription, lensBriefs, phaseName) {
+ const reviews = await parallel(
+ lensBriefs.map(lens => () =>
+ agent(lens.prompt, { label: `review:${lens.key}`, phase: phaseName, schema: FINDINGS_SCHEMA, agentType: lens.agentType })
+ )
+ )
+ const raw = []
+ reviews.filter(Boolean).forEach((r, i) => {
+ const key = lensBriefs[i].key
+ ;((r && r.findings) || []).forEach(f => raw.push({ lens: key, finding: f }))
+ })
+ if (!raw.length) return { findings: [], blocking: false }
+
+ const verified = await parallel(
+ raw.map(item => () =>
+ agent(
+ `You are an independent skeptic verifying ONE review finding. Do NOT trust it — check it yourself against the real files in ${REPO}.
+
+TARGET: ${targetDescription}
+
+FINDING (lens: ${item.lens}):
+- id: ${item.finding.id}
+- severity claimed: ${item.finding.severity}
+- category: ${item.finding.category}
+- claim: ${item.finding.claim}
+- problem: ${item.finding.problem}
+- proposed fix: ${item.finding.fix}
+
+Open the cited files/lines. Decide: CONFIRMED (real defect, fix is right), PARTIAL (real issue but proposed fix is wrong/incomplete — give corrected_fix), or REJECTED (not a real problem / misreads source). Default to REJECTED if you cannot substantiate it against actual evidence.`,
+ { label: `verify:${item.lens}:${item.finding.id}`, phase: phaseName, schema: VERDICT_SCHEMA }
+ ).then(v => ({ ...item, verdict: v }))
+ )
+ )
+
+ const survivors = verified
+ .filter(Boolean)
+ .filter(r => r.verdict && (r.verdict.verdict === 'CONFIRMED' || r.verdict.verdict === 'PARTIAL'))
+ .map(r => ({
+ lens: r.lens,
+ id: r.finding.id,
+ severity: r.finding.severity,
+ category: r.finding.category,
+ claim: r.finding.claim,
+ problem: r.finding.problem,
+ fix: r.verdict.corrected_fix || r.finding.fix,
+ verdict: r.verdict.verdict,
+ verify_reasoning: r.verdict.reasoning,
+ }))
+
+ const blocking = survivors.some(s => s.severity === 'critical' || s.severity === 'major')
+ return { findings: survivors, blocking }
+}
+
+// ---------------------------------------------------------------------------
+// Phase: VET — adversarial plan review, hard gate before execution
+// ---------------------------------------------------------------------------
+async function vet() {
+ phase('Vet')
+ if (!planPath) return { blocking: true, findings: [], error: 'no planPath provided to vet' }
+ const target = `The implementation plan at ${planPath}${specPath ? ` (design spec: ${specPath})` : ''}${issue ? `, for issue #${issue}` : ''}.`
+ const lenses = [
+ {
+ key: 'mechanics',
+ agentType: 'feature-dev:code-reviewer',
+ prompt: `${REPO_CTX}
+
+Adversarially review the implementation plan at ${planPath}${specPath ? ` against its design spec ${specPath}` : ''}. Find every LINE-LEVEL and MECHANICAL flaw before any code is written:
+- Ambiguous or wrong file:line targets, off-by-N line ranges, stale baselines.
+- Steps that contradict the current source (open the real files the plan cites and verify each claim).
+- Missing edits that a later step depends on; iterations that won't compile in isolation.
+- Verification commands that are wrong for this repo (e.g. forgetting HYPERD_PATH, wrong clippy scope, invented hyperd flags).
+- Narrowing \`as\` casts introduced by the plan.
+- Any place the plan hand-edits release-please-owned files (Cargo.toml versions, root CHANGELOG.md).
+Report only real defects via the schema. Cite the file:line you checked.`,
+ },
+ {
+ key: 'architecture',
+ agentType: 'code-review',
+ prompt: `${REPO_CTX}
+
+Adversarially review the implementation plan at ${planPath}${specPath ? ` against its design spec ${specPath}` : ''}. Find ARCHITECTURAL and CROSS-FILE concerns a line-level reviewer would miss:
+- Premise errors (the plan misidentifies what a symbol/function actually does).
+- Missed downstream impact: does the plan account for every in-repo caller of changed APIs (search hyperdb-api, hyperdb-mcp, hyperdb-api-node, examples, tests, sea-query-hyperdb, salesforce)?
+- Breaking-change correctness: is the version/release story right for release-please, and does the Node binding (hyperdb-api-node) ripple?
+- Security implications (e.g. arbitrary file read via value_path), over-engineering, scope creep beyond the issue.
+- Cross-file consistency: response-shape changes vs existing tests; sync/async twin drift.
+Report only real defects via the schema. Cite evidence you actually checked.`,
+ },
+ ]
+ const result = await reviewAndVerify(target, lenses, 'Vet')
+ log(`Vet: ${result.findings.length} verified findings; blocking=${result.blocking}`)
+ return result
+}
+
+// ---------------------------------------------------------------------------
+// Phase: EXECUTE — parse plan, then drive iterations sequentially
+// ---------------------------------------------------------------------------
+async function execute() {
+ phase('Execute')
+ if (!planPath) return { gate_failed: true, error: 'no planPath provided to execute', iterations: [] }
+
+ // Preflight: confirm we are on a feature branch, not main. (Bail loudly if main.)
+ const preflight = await agent(
+ `${REPO_CTX}
+
+Run these read-only checks in ${REPO} and report findings as plain text:
+1. \`git -C ${REPO} rev-parse --abbrev-ref HEAD\` — the current branch.
+2. \`git -C ${REPO} status --porcelain\` — is the tree clean?
+${branch ? `The expected working branch is \`${branch}\`. If HEAD is not on it, and the branch exists, note that; if HEAD is on \`main\`, that is a BLOCKER.` : ''}
+Report: current branch, whether it is safe to commit here (must NOT be main), and any uncommitted changes.`,
+ { label: 'execute:preflight', phase: 'Execute', agentType: 'general-purpose' }
+ )
+ log(`Execute preflight: ${String(preflight).slice(0, 300)}`)
+
+ // Parse the plan into concrete iterations.
+ const parsed = await agent(
+ `${REPO_CTX}
+
+Read the implementation plan at ${planPath}. Extract its ordered iterations into structured form. For each iteration capture: n (order), title, the files it touches, its concrete acceptance criteria (what a reviewer must be able to verify), the conventional-commit type (feat/fix/feat!/docs/test/...), and a one-line commit message. Preserve the plan's ordering exactly — later iterations may depend on earlier ones. Do not invent iterations that aren't in the plan.`,
+ { label: 'execute:parse', phase: 'Execute', schema: PLAN_PARSE_SCHEMA, agentType: 'general-purpose' }
+ )
+
+ let iterations = (parsed && parsed.iterations) || []
+ if (!iterations.length) return { gate_failed: true, error: 'plan parsed to zero iterations', iterations: [] }
+ if (iterations.length > MAX_ITERATIONS) {
+ log(`WARNING: plan has ${iterations.length} iterations; capping dispatch at ${MAX_ITERATIONS}. Remaining will NOT be executed this run.`)
+ iterations = iterations.slice(0, MAX_ITERATIONS)
+ }
+ log(`Execute: ${iterations.length} iterations parsed`)
+
+ // Sequential loop — dependent iterations sharing files must not run in parallel.
+ const results = []
+ for (const it of iterations) {
+ const impl = await agent(
+ `${REPO_CTX}
+
+You are the ENGINEER (doer). Execute EXACTLY iteration ${it.n} ("${it.title}") from the plan at ${planPath} — read the plan for the full step detail; do not run other iterations. Touch only these files (plus their tests): ${JSON.stringify(it.files)}.
+
+After editing, run the build/test gate and CAPTURE REAL OUTPUT:
+1. \`cd ${REPO} && cargo fmt --all\` then \`cargo fmt --all --check\`
+2. \`cd ${REPO} && cargo clippy --workspace --all-targets --all-features -- -D warnings\`
+3. The relevant tests with the real engine, e.g. \`HYPERD_PATH=${hyperdPath} cargo test -p \` (and the async twin). If a command hangs (no output ~30s), treat it as FAILED.
+Set gate_passed=true ONLY if fmt, clippy, and the relevant tests all pass with 0 exit and captured output.
+
+If (and only if) the gate passes, commit with an explicit \`git add \` (never -A) and this message: "${it.commit_message}". Report the commit sha. If the gate fails, do NOT commit; report the failure output in gate_output_summary and list blockers.
+
+Acceptance criteria for this iteration: ${JSON.stringify(it.acceptance)}.`,
+ { label: `iter${it.n}:engineer`, phase: 'Execute', schema: ITERATION_RESULT_SCHEMA, agentType: 'general-purpose' }
+ )
+
+ // If the engineer couldn't get a green gate, stop — later iterations depend on this.
+ if (!impl || !impl.gate_passed) {
+ results.push(impl || { iteration: it.n, implemented: false, gate_passed: false, gate_output_summary: 'engineer agent returned null', committed: false })
+ log(`Execute STOPPED at iteration ${it.n}: gate not green.`)
+ return { gate_failed: true, stopped_iteration: it.n, iterations: results }
+ }
+
+ // Adversarial per-iteration review (fast lens only — speed matters in the loop).
+ const review = await agent(
+ `${REPO_CTX}
+
+You are the REVIEWER (validator) for iteration ${it.n} ("${it.title}"). You did NOT write this code. Review the most recent commit's diff in ${REPO} (\`git show HEAD\` / \`git diff HEAD~1 HEAD\`) against these acceptance criteria: ${JSON.stringify(it.acceptance)}.
+Check for: correctness bugs, narrowing \`as\` casts, sync/async twin drift, missing error propagation, response-shape changes that break existing tests, and any deviation from the plan. Report only real defects via the schema; empty findings means the iteration passed review.`,
+ { label: `iter${it.n}:review`, phase: 'Execute', schema: FINDINGS_SCHEMA, agentType: 'feature-dev:code-reviewer' }
+ )
+ const reviewFindings = (review && review.findings) || []
+ const blocking = reviewFindings.filter(f => f.severity === 'critical' || f.severity === 'major')
+
+ // If the reviewer flagged blocking issues, have the engineer fix them in a follow-up before advancing.
+ if (blocking.length) {
+ log(`Iteration ${it.n}: reviewer flagged ${blocking.length} blocking issue(s); dispatching a fix pass.`)
+ const fix = await agent(
+ `${REPO_CTX}
+
+You are the ENGINEER. A reviewer flagged blocking issues on your iteration ${it.n} commit. Fix ONLY these, re-run the full gate (fmt/clippy/relevant tests, captured output), and amend or add a commit. Blocking issues:
+${JSON.stringify(blocking, null, 2)}
+Report the same iteration-result shape; gate_passed=true only with real green output.`,
+ { label: `iter${it.n}:fix`, phase: 'Execute', schema: ITERATION_RESULT_SCHEMA, agentType: 'general-purpose' }
+ )
+ results.push({ ...impl, review_findings: reviewFindings, fix })
+ if (!fix || !fix.gate_passed) {
+ log(`Execute STOPPED at iteration ${it.n}: fix pass did not go green.`)
+ return { gate_failed: true, stopped_iteration: it.n, iterations: results }
+ }
+ } else {
+ results.push({ ...impl, review_findings: reviewFindings })
+ }
+ log(`Iteration ${it.n} complete: committed ${impl.commit_sha || '(see notes)'}, ${reviewFindings.length} review notes.`)
+ }
+
+ return { gate_failed: false, iterations: results }
+}
+
+// ---------------------------------------------------------------------------
+// Phase: SWEEP — full E2E verification + final adversarial sweep
+// ---------------------------------------------------------------------------
+async function sweep() {
+ phase('Sweep')
+
+ // One heavy agent runs the integrated E2E gate (sequential; captures output).
+ const e2e = await agent(
+ `${REPO_CTX}
+
+You are running the FULL end-to-end verification of the integrated branch in ${REPO}. Run and CAPTURE REAL OUTPUT (0 exit required for each), reporting exactly what passed/failed:
+1. \`cd ${REPO} && cargo fmt --all --check\`
+2. \`cd ${REPO} && cargo clippy --workspace --all-targets --all-features -- -D warnings\`
+3. \`cd ${REPO} && HYPERD_PATH=${hyperdPath} cargo test --workspace\` (real hyperd; if it hangs with no output ~30s, treat as FAILED).
+4. If examples are relevant, \`cd ${REPO} && make examples\` or run the touched examples.
+Set e2e_passed=true ONLY if fmt+clippy+workspace tests all pass with captured output. Give confidence (high/medium/low) and list any blocking issues. commands_run should list what you actually ran.`,
+ { label: 'sweep:e2e', phase: 'Sweep', schema: SWEEP_SCHEMA, agentType: 'general-purpose' }
+ )
+
+ // Final adversarial sweep — BOTH reviewer lenses on the integrated diff vs main.
+ const target = `The integrated feature branch${branch ? ` \`${branch}\`` : ''} in ${REPO}, diffed against \`main\` (\`git diff main...HEAD\`)${issue ? `, implementing issue #${issue}` : ''}.`
+ const lenses = [
+ {
+ key: 'final-mechanics',
+ agentType: 'feature-dev:code-reviewer',
+ prompt: `${REPO_CTX}
+
+Final adversarial sweep. Review the WHOLE integrated diff \`git diff main...HEAD\` in ${REPO}. Find line-level defects that only appear on the integrated whole: narrowing casts, sync/async drift, response-shape/test mismatches, missing per-crate CHANGELOG \`## [Unreleased]\` bullets, incorrect commit types for release-please. Report real defects via the schema.`,
+ },
+ {
+ key: 'final-architecture',
+ agentType: 'code-review',
+ prompt: `${REPO_CTX}
+
+Final adversarial sweep (deep lens). Review the WHOLE integrated diff \`git diff main...HEAD\` in ${REPO}. Find cross-file inconsistencies, missed callers of changed APIs, security regressions, over-engineering, docs/README that still promise old behavior, and any breaking-change/versioning story that will confuse release-please or downstream. Report real defects via the schema.`,
+ },
+ ]
+ const reviewResult = await reviewAndVerify(target, lenses, 'Sweep')
+
+ const releaseReady = !!(e2e && e2e.e2e_passed) && !reviewResult.blocking
+ log(`Sweep: e2e_passed=${e2e && e2e.e2e_passed}, blocking review findings=${reviewResult.blocking}, release_ready=${releaseReady}`)
+ return { e2e, review: reviewResult, release_ready: releaseReady }
+}
+
+// ---------------------------------------------------------------------------
+// Phase: RELEASE — mechanical automation with a 2-try check valve
+// ---------------------------------------------------------------------------
+async function release() {
+ phase('Release')
+
+ if (releaseStage === 'pr') {
+ const r = await agent(
+ `${REPO_CTX}
+${POLL_CTX}
+
+You are the PUBLISHER, releaseStage=PR. Do the mechanical, reversible release-prep steps for the feature branch${branch ? ` \`${branch}\`` : ''} in ${REPO}. Each network op gets AT MOST TWO attempts — if it fails twice, STOP that step, record it, and put the exact manual command in manual_commands. Never force irreversible actions.
+
+Steps:
+1. Ensure the active github.com account is \`${forkOwner}\`: \`gh auth switch --hostname github.com --user ${forkOwner}\` (verify with \`gh auth status\`).
+2. Push the branch to the fork: \`git -C ${REPO} push -u origin ${branch || 'HEAD'}\` (2-try).
+3. Open or update a PR to upstream: \`gh pr create -R ${upstream} --head ${forkOwner}:${branch || ''} --fill\` (if it already exists, fetch it with \`gh pr view -R ${upstream} --head ${forkOwner}:${branch}\`). Capture pr_url.
+4. Wait for CI to FINISH by POLLING (per the Harness polling constraints above — do NOT use a single blocking \`--watch\`): loop \`gh pr checks -R ${upstream}\` with a <=90s sleep between polls until every check concludes. Report ci_status = success/failure/pending. Do NOT merge — merging to main is a human decision that triggers release-please.
+Report actions_taken, pr_url, ci_status, attempts, and any manual_commands needed. success=true only if the branch is pushed, the PR is open, and CI concluded green.`,
+ { label: 'release:pr', phase: 'Release', schema: RELEASE_SCHEMA, agentType: 'general-purpose' }
+ )
+ return { stage: 'pr', ...(r || { success: false, attempts: 0, notes: 'release:pr agent returned null' }) }
+ }
+
+ if (releaseStage === 'publish') {
+ if (!releaseTag) return { stage: 'publish', success: false, attempts: 0, notes: 'releaseTag (e.g. v0.7.0) is required for the publish stage' }
+ const r = await agent(
+ `${REPO_CTX}
+${POLL_CTX}
+
+You are the PUBLISHER, releaseStage=PUBLISH, targeting tag ${releaseTag}. This repo uses release-please with \`skip-github-release: true\`, so the GitHub Release + git tag are created BY HAND after the release PR merges. Each network op gets AT MOST TWO attempts; if it fails twice, STOP, record it, and emit the exact manual command in manual_commands. These steps are IRREVERSIBLE once publishes fire — be conservative and verify preconditions before each.
+
+Preconditions to VERIFY first (report and STOP if unmet):
+- The feature PR for issue #${issue || '(n/a)'} is merged into \`${upstream}\` main.
+- release-please has opened a \`chore(main): release ${releaseTag.replace(/^v/, '')}\` PR (\`gh pr list -R ${upstream} --search "release-please"\`).
+
+Steps (each 2-try):
+1. Confirm/merge the release-please PR (only if it is the correct version ${releaseTag}). Report its number and merge result.
+2. After it merges, verify the manifest on the merge commit shows the new version:
+ \`gh api repos/${upstream}/contents/.release-please-manifest.json?ref= --jq '.content' | base64 -d\`.
+3. HAND-CREATE the release (repo sets skip-github-release): create the tag on the merge commit and the GitHub Release:
+ \`gh release create ${releaseTag} -R ${upstream} --target --title ${releaseTag} --notes ""\` (this also creates the tag).
+4. Trigger the two publish workflows (2-try each):
+ \`gh workflow run release.yml -R ${upstream} -f tag=${releaseTag}\`
+ \`gh workflow run npm-build-publish.yml -R ${upstream} -f tag=${releaseTag}\`
+5. WAIT for both runs to FINISH successfully by POLLING (per the Harness polling constraints above — NOT one blocking \`gh run watch\`, which a real publish run outlasts and gets killed): loop \`gh run list -R ${upstream} --workflow= --limit 1\` (or \`gh run view --json status,conclusion\`) with a <=90s sleep between polls until both conclude. Report ci_status.
+6. Promote the release PR label so future release-please runs don't abort:
+ \`gh pr edit -R ${upstream} --remove-label "autorelease: pending" --add-label "autorelease: tagged"\`.
+Report actions_taken, attempts, ci_status, and manual_commands for anything you could not complete in two tries. success=true only if the tag+release exist and both publish workflows concluded green.`,
+ { label: 'release:publish', phase: 'Release', schema: RELEASE_SCHEMA, agentType: 'general-purpose' }
+ )
+ return { stage: 'publish', ...(r || { success: false, attempts: 0, notes: 'release:publish agent returned null' }) }
+ }
+
+ return { stage: releaseStage, success: false, attempts: 0, notes: `unknown releaseStage "${releaseStage}" (expected 'pr' or 'publish')` }
+}
+
+// ---------------------------------------------------------------------------
+// Phase: SMOKE — post-publish npm smoke test via the MCP
+// ---------------------------------------------------------------------------
+async function smoke() {
+ phase('Smoke')
+ const expected = releaseTag ? releaseTag.replace(/^v/, '') : '(latest)'
+ const r = await agent(
+ `${REPO_CTX}
+
+You are running a POST-RELEASE npm SMOKE TEST of the published \`${npmPackage}\` package (expected version ${expected}). The goal: prove the freshly published package pulls from npm and that the NEW KV features work end-to-end through the MCP.
+
+CRITICAL — verify against the REAL published artifact, NOT this session's connected MCP. The connected \`mcp__hyperdb-npm__*\` server was spawned at session start and may be pinned to an OLDER version in \`~/.claude.json\` (an explicit \`${npmPackage}@X.Y.Z\`, not \`@latest\`); a live session cannot hot-swap its own MCP process. If you smoke-test through those connected tools you may exercise the OLD binary and get a FALSE GREEN — exactly the failure AGENTS.md rule 10 exists to prevent. So the clean-room \`npx\` spawn below is the PRIMARY verification path, and every assertion is gated on the server's SELF-REPORTED version.
+
+1. Confirm npm serves the new version: \`npm view ${npmPackage} version\` and \`npm view ${npmPackage} dist-tags\`. Record resolved_version. If it is not ${expected}, the publish hasn't propagated — report version_match=false and stop early with that finding (do NOT fail the other tests spuriously).
+2. Force a FRESH pull (bypass any npx cache) and prove the binary runs:
+ \`npx -y ${npmPackage}@${expected} --help\` — capture output. Confirm the flags used below (\`--ephemeral-only\`, \`--no-daemon\`) actually appear in --help before relying on them (AGENTS.md rule 9 — never invent flags).
+3. Drive a FRESHLY-SPAWNED clean-room server over stdio — do NOT use the session's connected \`mcp__hyperdb-npm__*\` tools as the source of truth:
+ \`npx -y ${npmPackage}@${expected} --ephemeral-only --no-daemon\`
+ These two flags avoid two real collisions with the session's live MCP, observed on the v0.7.0 run:
+ - \`--ephemeral-only\`: the shared persistent \`workspace.hyper\` is held open by the session's server; opening it from a second process throws SQLSTATE 55006 ("database file is locked by another process"). The KV smoke checks only need the ephemeral DB, so skip persistent entirely.
+ - \`--no-daemon\`: a newer client performs a daemon version-takeover on the shared port 7485 (a shipped feature) — killing the session's daemon. \`--no-daemon\` spawns a private hyperd and leaves the session's daemon alone.
+ Do the MCP JSON-RPC handshake (initialize -> notifications/initialized -> tools/call). GATE FIRST: call \`status\` and assert its reported version starts with "${expected}". If it does not, STOP — you are not testing the new artifact; report version_match=false. Only if the version matches, run these checks and record each as a test:
+ - kv_set returns a \`created\` field (true on first write, false on overwrite of the same key) and \`value_bytes\`.
+ - kv_set with overwrite:false on an existing key returns \`{stored:false, existed:true}\` / does not clobber.
+ - kv_set with value_path reads a temp file's contents (create one with a known string first).
+ - kv_size returns a \`bytes\` field alongside the key count.
+ - kv_set_many writes multiple entries atomically and reports created/overwritten + total_bytes.
+ - kv_list with values:true returns entries with values (not just keys).
+ - get_readme / the KV schema resource documents the value::json JSON-query pattern and the ::numeric scale-0 gotcha.
+Record resolved_version = the server's SELF-REPORTED version (not just what npm claims), all_passed, and a concise summary. Use ONLY documented tool parameters; do not invent flags.`,
+ { label: 'smoke:npm-mcp', phase: 'Smoke', schema: SMOKE_SCHEMA, agentType: 'general-purpose' }
+ )
+ const out = r || { resolved_version: 'unknown', tests: [], all_passed: false, summary: 'smoke agent returned null' }
+ out.expected_version = expected
+ out.version_match = out.resolved_version === expected
+ log(`Smoke: resolved ${out.resolved_version} (expected ${expected}), all_passed=${out.all_passed}`)
+ return out
+}
+
+// ---------------------------------------------------------------------------
+// Dispatch
+// ---------------------------------------------------------------------------
+let result
+if (mode === 'vet') {
+ result = await vet()
+} else if (mode === 'execute') {
+ result = { execute: await execute() }
+} else if (mode === 'sweep') {
+ result = await sweep()
+} else if (mode === 'release') {
+ result = await release()
+} else if (mode === 'smoke') {
+ result = await smoke()
+} else {
+ // full: vet -> (gate) -> execute -> (gate) -> sweep. Stop at release-ready.
+ const v = await vet()
+ if (v.blocking) {
+ log('FULL stopped at VET: blocking findings — revise the plan on the main thread before executing.')
+ result = { stopped_at: 'vet', vet: v, next: 'revise plan, re-run mode=full' }
+ } else {
+ const e = await execute()
+ if (e.gate_failed) {
+ log('FULL stopped at EXECUTE: a build/test gate did not go green.')
+ result = { stopped_at: 'execute', vet: v, execute: e, next: 'inspect the failing iteration on the main thread' }
+ } else {
+ const s = await sweep()
+ result = {
+ stopped_at: s.release_ready ? 'release-ready' : 'sweep',
+ vet: v,
+ execute: e,
+ sweep: s,
+ next: s.release_ready
+ ? `Release-ready. Run mode=release releaseStage=pr, merge upstream, then mode=release releaseStage=publish releaseTag=${releaseTag || 'vX.Y.Z'}, then mode=smoke.`
+ : 'Sweep found blocking issues or E2E failed — fix on the main thread and re-run mode=sweep.',
+ }
+ }
+ }
+}
+
+return result
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 8b700dc..626c873 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -150,6 +150,26 @@ jobs:
if: steps.hyperd-cache.outputs.cache-hit != 'true'
run: cargo run --release -p hyperdb-bootstrap --bin hyperdb-bootstrap -- download
+ - name: Start macOS hang watchdog
+ # DIAGNOSTIC (temporary): hyperd 0.0.26225 wedges after its
+ # callback connection succeeds on the macOS-14 runner only, so
+ # the test binary stalls until the 45-min job cap (see PR #219).
+ # This backgrounds a watchdog that, once a hyperd has been alive
+ # past a threshold, `sample`s its native C++ stack + the stalled
+ # test process, copies hyperd's JSON log, uploads them as an
+ # artifact, and kills the wedged processes so the job ends in
+ # minutes. Remove once the hang is root-caused and fixed.
+ if: runner.os == 'macOS'
+ shell: bash
+ env:
+ # Pass the workspace path via env (not inline in run:) so the
+ # expansion stays out of the shell command line.
+ DIAG_DIR: ${{ github.workspace }}/macos-hang-diagnostics
+ run: |
+ nohup bash scripts/macos-hang-watchdog.sh "$DIAG_DIR" \
+ > macos-hang-diagnostics-nohup.log 2>&1 &
+ echo "watchdog started (pid $!)"
+
- name: Workspace tests
shell: bash
env:
@@ -165,6 +185,20 @@ jobs:
# its own workflow when wired up. hyperdb-bootstrap has its own
# coverage (next step) and doesn't need hyperd running.
+ - name: Upload macOS hang diagnostics
+ # Runs even when the test step failed/was cancelled — that's the
+ # whole point: the watchdog fires precisely when tests hang.
+ # Paired with the diagnostic watchdog above; remove together.
+ if: always() && runner.os == 'macOS'
+ uses: actions/upload-artifact@v7
+ with:
+ name: macos-hang-diagnostics
+ path: |
+ macos-hang-diagnostics/
+ macos-hang-diagnostics-nohup.log
+ if-no-files-found: ignore
+ retention-days: 14
+
- name: hyperdb-bootstrap tests
run: cargo test -p hyperdb-bootstrap
diff --git a/AGENTS.md b/AGENTS.md
index ed80a8d..2a72027 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -4,7 +4,7 @@ This file provides guidance to AI coding assistants working with code in this re
**Subdirectory guidance:** The [`hyperdb-api-node/`](hyperdb-api-node/AGENTS.md) directory has its own `AGENTS.md` covering the Node.js/TypeScript bindings, napi-rs build system, and JS-specific patterns.
-**Bootstrapping `hyperd`:** Contributors obtain the `hyperd` executable by running `make download-hyperd` (or `.\build.ps1 download-hyperd`). The implementation lives in the [`hyperdb-bootstrap`](hyperdb-bootstrap/) crate; the pinned release is baked into [`hyperdb-bootstrap/hyperd-version.toml`](hyperdb-bootstrap/hyperd-version.toml). Bumping `hyperd` = edit that file (version + build_id + per-platform sha256s), bump the crate version, publish.
+**Bootstrapping `hyperd`:** Contributors obtain the `hyperd` executable by running `make download-hyperd` (or `.\build.ps1 download-hyperd`). The implementation lives in the [`hyperdb-bootstrap`](hyperdb-bootstrap/) crate; the pinned release is baked into [`hyperdb-bootstrap/hyperd-version.toml`](hyperdb-bootstrap/hyperd-version.toml). Bumping `hyperd` = edit that file (version + build_id + per-platform sha256s), then let the `fix(bootstrap):` commit drive the version via release-please (the crate uses `version.workspace = true` — don't hand-edit a crate version). The full repeatable procedure — verify the pin, run the suite, A/B benchmark against the previous pin, and log the result — is captured in the [`update-hyperd-release`](.claude/skills/update-hyperd-release/SKILL.md) skill; per-release performance history is tracked in [`docs/hyperd-release-benchmarks.md`](docs/hyperd-release-benchmarks.md).
## Project Overview
diff --git a/docs/BENCHMARK_GUIDE.md b/docs/BENCHMARK_GUIDE.md
index ab6da5f..b7a10a5 100644
--- a/docs/BENCHMARK_GUIDE.md
+++ b/docs/BENCHMARK_GUIDE.md
@@ -448,6 +448,10 @@ out differently — worth measuring.
- [DEVELOPMENT.md](../DEVELOPMENT.md) — workspace architecture, build
instructions, and pointers to crate-level dev guides.
+- [hyperd-release-benchmarks.md](hyperd-release-benchmarks.md) — per-release
+ performance history of the pinned `hyperd` engine. Where this guide files
+ results *by platform*, that file tracks them *by release* so an engine bump's
+ effect is visible over time. Populated by the `update-hyperd-release` skill.
## Reproducibility notes
diff --git a/docs/hyperd-release-benchmarks.md b/docs/hyperd-release-benchmarks.md
new file mode 100644
index 0000000..c08d502
--- /dev/null
+++ b/docs/hyperd-release-benchmarks.md
@@ -0,0 +1,43 @@
+# hyperd release benchmark tracker
+
+Per-release performance history for the pinned `hyperd` engine. Every time the pin
+in [`hyperdb-bootstrap/hyperd-version.toml`](../hyperdb-bootstrap/hyperd-version.toml)
+is bumped, add a row here from an A/B run of the unified suite
+([`hyperdb-api/benches/benchmark_suite.rs`](../hyperdb-api/benches/benchmark_suite.rs))
+against the previous pin. The procedure lives in the
+[`update-hyperd-release`](../.claude/skills/update-hyperd-release/SKILL.md) skill.
+
+This complements [BENCHMARK_GUIDE.md](BENCHMARK_GUIDE.md), which files results
+**by platform**; this file tracks results **by release** so a regression or win
+introduced by an engine bump is visible over time.
+
+## Methodology
+
+- **Harness:** `benchmark_suite`, TCP transport, 4 workers.
+- **Numbers below are median of ≥3 runs at 100M rows** (10M-row runs are too short
+ to distinguish signal from variance).
+- **Only single-connection deltas are reported as reliable.** Multi-connection
+ (`× 4`) workloads throttle thermally on laptops — throughput declines across
+ sequential runs — so they are excluded from the headline deltas unless the run
+ was on a cooled/pinned host.
+- Throughput in **M rows/s**. "Δ vs prev" compares to the release in the row above.
+
+## Insert (single-connection, M rows/s)
+
+| Release | Build | Date | Machine | Inserter (sync) | ChunkSender (sync) | AsyncArrowInserter | Δ vs prev | Notes |
+|---|---|---|---|---:|---:|---:|---|---|
+| 0.0.25080 | r2bfd835b | (baseline) | M-series (thermal, laptop) | 26.87 | 26.10 | 30.01 | — | Prior pin; measured as A/B baseline during the 0.0.26225 bump. |
+| 0.0.26225 | rbf04a855 | 2026-08-07 | M-series (thermal, laptop) | 24.94 | 24.67 | 29.95 | sync insert −5–7%; async ~flat | See PR #219. |
+
+## Query (single-connection, M rows/s)
+
+| Release | Build | Date | Machine | full_scan (sync) | full_scan (async) | filtered (sync) | filtered (async) | Δ vs prev | Notes |
+|---|---|---|---|---:|---:|---:|---:|---|---|
+| 0.0.25080 | r2bfd835b | (baseline) | M-series (thermal, laptop) | 18.79 | 18.73 | 33.23 | 27.05 | — | Prior pin. |
+| 0.0.26225 | rbf04a855 | 2026-08-07 | M-series (thermal, laptop) | 31.23 | 25.10 | 32.89 | 27.18 | **full_scan +66% sync / +34% async**; filtered ~flat | Large win on the dominant query path. All 1485 workspace tests pass; identical query results. |
+
+## How to add a release
+
+Follow the [`update-hyperd-release`](../.claude/skills/update-hyperd-release/SKILL.md)
+skill (step 8). In short: run the A/B, then append one insert row and one query row
+with median single-connection numbers, the machine, and any caveat worth recording.
diff --git a/hyperdb-bootstrap/CHANGELOG.md b/hyperdb-bootstrap/CHANGELOG.md
index 42f2b79..5b49e75 100644
--- a/hyperdb-bootstrap/CHANGELOG.md
+++ b/hyperdb-bootstrap/CHANGELOG.md
@@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
## [Unreleased]
+### Changed
+
+- **Bump the pinned `hyperd` release to `0.0.26225` (build `rbf04a855`).**
+ Updates the version, build id, and all four per-platform sha256s in
+ `hyperd-version.toml`. The `macos-arm64` Java bundle's `hyperd` was
+ verified to be a native arm64 Mach-O binary, preserving the reason this
+ crate downloads from the Java bundle rather than the C++ one.
+
+ **Performance.** A/B benchmark of the unified suite
+ (`hyperdb-api/benches/benchmark_suite.rs`, 100M rows, median of 3 runs,
+ Apple Silicon) against the previous pin `0.0.25080`: single-connection
+ full-scan query throughput improves substantially — **sync +66%**
+ (18.8 → 31.2 M rows/s) and **async +34%** (18.7 → 25.1 M rows/s) —
+ while single-threaded insert throughput dips **~5–7%**
+ (Inserter 26.9 → 24.9 M rows/s). Multi-connection (`× 4`) workloads
+ were dominated by thermal throttling on the test machine and are not
+ reported as a reliable delta. All 1485 workspace tests pass against the
+ new engine with identical query results.
+
### Fixed
- **Download `hyperd` from the Java API bundle instead of the C++ bundle.**
diff --git a/hyperdb-bootstrap/hyperd-version.toml b/hyperdb-bootstrap/hyperd-version.toml
index 5fa731f..c1c72f9 100644
--- a/hyperdb-bootstrap/hyperd-version.toml
+++ b/hyperdb-bootstrap/hyperd-version.toml
@@ -8,14 +8,14 @@
#
# Bump these values (and sha256s) when upgrading. Contributors without
# an override get this exact release, so reproducibility depends on it.
-version = "0.0.25080"
-build_id = "r2bfd835b"
+version = "0.0.26225"
+build_id = "rbf04a855"
# sha256 of each platform's .zip. Omit a platform to skip verification
# for that platform (not recommended). Compute with:
# shasum -a 256 tableauhyperapi-java--release-main...zip
[sha256]
-"macos-arm64" = "2b0fa3fefcf4eba60f052e1cb51abfc32d8c84354274513763760f9549b45991"
-"macos-x86_64" = "2fb7e58a449f5902e603f46ee7a73c70c85a2ef01e0c6435a26f893c2b9ee1f0"
-"linux-x86_64" = "3d3fd2104f55f7fad832470592394dc78f350a03d52e89d36c5288b202dd0bc0"
-"windows-x86_64" = "9dc4851d416e0e6e00f0367ee6b45fcd676e7ba3a110d4644e3bec871b9aa1de"
+"macos-arm64" = "6d7afccdf013feaae8ce3d9d75a85f1537dcc90579cfa50e059560a8f769fc18"
+"macos-x86_64" = "44fcb84cf325f4c7069ec0424ccd78bd4e4563118f3a09c7dbc1cf95864ccff5"
+"linux-x86_64" = "09ebe670548efb1e0abc1713c58f1503cb1cc1ce42a7bf9999ffc10851678376"
+"windows-x86_64" = "73daa8100cf5fc478afc69f42146d62f184007b39bc730b92475a8ae6afdb2c2"
diff --git a/scripts/macos-hang-watchdog.sh b/scripts/macos-hang-watchdog.sh
new file mode 100755
index 0000000..464f6a6
--- /dev/null
+++ b/scripts/macos-hang-watchdog.sh
@@ -0,0 +1,154 @@
+#!/usr/bin/env bash
+# Copyright (c) 2026, Salesforce, Inc. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0 OR MIT
+#
+# CI-only diagnostic watchdog for the macOS-14 test hang.
+#
+# Background: hyperd 0.0.26225 (rbf04a855) wedges *after* its callback
+# connection succeeds, on the macOS-14 (Sonoma, 3-core) CI runner only.
+# The client blocks in the libpq startup handshake read (which has no
+# timeout), so the `arrow_inserter_tests` binary stalls until the 45-min
+# job cap. Linux, Windows, and local macOS 26 all pass. See PR #219.
+#
+# This script does NOT fix the hang — it captures the evidence needed to
+# root-cause it. It polls for a long-lived hyperd (and its stalled test
+# harness), and once one has been alive past THRESHOLD_SECS it:
+# 1. `sample`s the hyperd native stack (symbolized C++ frames) — this
+# names the exact wedged engine function.
+# 2. `sample`s the stalled test process too.
+# 3. Snapshots `ps` and copies any hyperd JSON log it can find.
+# 4. Writes everything under $OUT_DIR for artifact upload, then kills
+# the wedged processes so the job ends in minutes instead of 45.
+#
+# It is invoked in the background by the macOS test step. On every other
+# platform the workflow never runs it. If no hang occurs, it detects the
+# `cargo test` parent exiting and quits cleanly, capturing nothing.
+#
+# Deliberately uses only base-system tools (`sample`, `ps`, `pgrep`,
+# `pkill`) — all present on the GitHub macos-14 image.
+
+set -uo pipefail
+
+OUT_DIR="${1:-macos-hang-diagnostics}"
+# A hyperd that has been alive longer than this while tests are running
+# is considered wedged. The unit-test binary finishes in <1s; a healthy
+# hyperd-backed test spawns and exits its engine in a few seconds. 150s
+# is comfortably past any legitimate cold-start on a 3-core runner yet
+# far below the 45-min job cap, so we capture and bail early.
+THRESHOLD_SECS="${WATCHDOG_THRESHOLD_SECS:-150}"
+# Overall ceiling: stop watching after this long even if nothing wedged,
+# so the backgrounded script can never outlive a healthy job.
+MAX_WATCH_SECS="${WATCHDOG_MAX_WATCH_SECS:-1800}"
+POLL_SECS=10
+
+mkdir -p "$OUT_DIR"
+LOG="$OUT_DIR/watchdog.log"
+
+log() { echo "[watchdog $(date -u +%H:%M:%S)] $*" | tee -a "$LOG"; }
+
+# Convert ps `etime` ([[DD-]HH:]MM:SS) to whole seconds.
+etime_to_secs() {
+ local e="$1" days=0 hms
+ if [[ "$e" == *-* ]]; then days="${e%%-*}"; hms="${e#*-}"; else hms="$e"; fi
+ local IFS=: parts secs=0 p
+ read -ra parts <<< "$hms"
+ for p in "${parts[@]}"; do secs=$(( secs * 60 + 10#$p )); done
+ echo $(( secs + 10#$days * 86400 ))
+}
+
+# Print "pid etime_secs" for every running hyperd process.
+hyperd_procs() {
+ # -o with trailing '=' suppresses headers. comm gives the basename so
+ # we match the engine regardless of its absolute install path.
+ ps -Ao pid=,etime=,comm= 2>/dev/null | while read -r pid etime comm; do
+ case "$comm" in
+ *hyperd|*hyperd.exe) echo "$pid $(etime_to_secs "$etime")" ;;
+ esac
+ done
+}
+
+capture() {
+ local reason="$1"
+ log "CAPTURING diagnostics ($reason) -> $OUT_DIR"
+
+ ps -Ao pid=,ppid=,etime=,rss=,command= > "$OUT_DIR/ps-snapshot.txt" 2>&1 || true
+
+ # Sample every live hyperd (there are typically 3 in a wedged run).
+ local pid
+ for pid in $(hyperd_procs | awk '{print $1}'); do
+ log "sampling hyperd pid $pid"
+ sample "$pid" 2 -file "$OUT_DIR/hyperd-$pid.sample.txt" >/dev/null 2>&1 \
+ && log " -> hyperd-$pid.sample.txt" \
+ || log " -> sample FAILED for pid $pid"
+ done
+
+ # Sample the stalled test harness(es) too — its Rust-side stack shows
+ # exactly where the client blocks (startup()/read_message()).
+ for pid in $(pgrep -f 'target/debug/deps' 2>/dev/null); do
+ log "sampling test proc pid $pid"
+ sample "$pid" 2 -file "$OUT_DIR/testproc-$pid.sample.txt" >/dev/null 2>&1 || true
+ done
+
+ # hyperd JSON logs: the test harness points --log-dir at test_results/,
+ # but copy from a few likely locations to be safe.
+ local d
+ for d in test_results "$HOME/.hyperdb/logs" .; do
+ if [[ -d "$d" ]]; then
+ find "$d" -maxdepth 2 -name 'hyperd*.log' -type f 2>/dev/null | while read -r f; do
+ cp "$f" "$OUT_DIR/$(echo "$f" | tr '/' '_')" 2>/dev/null \
+ && log "copied log $f" || true
+ done
+ fi
+ done
+
+ log "killing wedged processes so the job can end"
+ # Kill the cargo driver first so it can't spawn the next (also-hanging)
+ # test target, then the stalled test binaries and hyperd itself. The
+ # watchdog's own command line ("bash .../macos-hang-watchdog.sh") does
+ # not match any of these patterns, so it never signals itself.
+ pkill -9 -f 'cargo test' 2>/dev/null || true
+ pkill -9 -f 'target/debug/deps' 2>/dev/null || true
+ pkill -9 -x hyperd 2>/dev/null || true
+ log "capture complete"
+}
+
+log "started (threshold=${THRESHOLD_SECS}s, max_watch=${MAX_WATCH_SECS}s, poll=${POLL_SECS}s)"
+
+# Returns 0 while a cargo-test driver or test binary is running.
+test_running() {
+ pgrep -f 'cargo test' >/dev/null 2>&1 || pgrep -f 'target/debug/deps' >/dev/null 2>&1
+}
+
+# This watchdog is backgrounded in a step that finishes before the
+# "Workspace tests" step launches `cargo test`, so at first poll there
+# may be no test process yet. Only treat "no test process" as a healthy
+# finish AFTER we've actually observed one — otherwise the startup race
+# makes the watchdog exit immediately and capture nothing.
+seen_test=0
+elapsed=0
+while (( elapsed < MAX_WATCH_SECS )); do
+ if test_running; then
+ seen_test=1
+ elif (( seen_test == 1 )); then
+ log "no test process remains — suite finished healthily, exiting"
+ exit 0
+ fi
+
+ # Any hyperd alive past the threshold => wedged. Capture and bail.
+ oldest=0
+ while read -r pid secs; do
+ [[ -n "${secs:-}" ]] && (( secs > oldest )) && oldest="$secs"
+ done < <(hyperd_procs)
+
+ if (( oldest >= THRESHOLD_SECS )); then
+ log "hyperd alive ${oldest}s (>= ${THRESHOLD_SECS}s) — treating as wedged"
+ capture "hyperd exceeded ${THRESHOLD_SECS}s"
+ exit 0
+ fi
+
+ sleep "$POLL_SECS"
+ elapsed=$(( elapsed + POLL_SECS ))
+done
+
+log "max watch window reached without detecting a wedge — exiting"
+exit 0