From 045571f77f57156f258fb862ba8339892293ab4c Mon Sep 17 00:00:00 2001 From: UXwolt Date: Mon, 13 Apr 2026 00:33:31 +0000 Subject: [PATCH 1/4] Sync host repo during rebuild to keep CLI in lockstep woltspace rebuild now checks out the target version in the host repo before building the Docker image. Prevents drift where the container has new code (e.g. tunnel.json) but the host CLI still uses old paths. Falls back gracefully if checkout fails (warning, not error). Closes #312 Co-Authored-By: Claude Opus 4.6 --- .claude/skills/woltspace-update/SKILL.md | 5 +++-- woltspace | 20 ++++++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/.claude/skills/woltspace-update/SKILL.md b/.claude/skills/woltspace-update/SKILL.md index a62690e..effbfc6 100644 --- a/.claude/skills/woltspace-update/SKILL.md +++ b/.claude/skills/woltspace-update/SKILL.md @@ -131,11 +131,12 @@ fi ### 5c: Rebuild with the new version ```bash -# Rebuild the image at the target version — no need to change the local checkout +# Rebuild the image at the target version +# This also syncs the host repo so the CLI stays in lockstep with the container woltspace rebuild --version "${LATEST}" ``` -**Always rebuild.** The image clones the repo at build time — there's no live-reload from the host. `--version` tells rebuild which tag to build, without touching the user's branch. +**Always rebuild.** The image clones the repo at build time — there's no live-reload from the host. `--version` tells rebuild which tag to build. The rebuild command automatically syncs the host repo to the target version, keeping the `woltspace` CLI script in lockstep with the container. ## Step 6: Verify diff --git a/woltspace b/woltspace index 97a0b16..e5ff253 100755 --- a/woltspace +++ b/woltspace @@ -505,6 +505,26 @@ RESTORE echo " ${_D}đŸĒĩ rebuilding to latest release${_R}" echo " ${_L} upgrading the lodge...${_R}" fi + # Sync host repo to match the version being built — keeps the CLI + # script in lockstep with the container. Without this, _show_url() + # and other host-side logic can drift from the container's code. + if [ "$LOCAL_BUILD" != true ] && [ -d "$WOLTSPACE_DIR/.git" ]; then + _sync_target="" + if [ -n "${CURRENT_VERSION:-}" ] && [ "$CURRENT_VERSION" != "HEAD" ]; then + _sync_target="$CURRENT_VERSION" + elif [ "$BUILD_BRANCH" != "main" ]; then + _sync_target="origin/$BUILD_BRANCH" + else + _sync_target="origin/main" + fi + if [ -n "$_sync_target" ]; then + echo " ${_D}syncing host CLI to $_sync_target${_R}" + git -C "$WOLTSPACE_DIR" fetch origin --tags --force --quiet 2>/dev/null || true + git -C "$WOLTSPACE_DIR" checkout "$_sync_target" --quiet 2>/dev/null || { + echo " ${_D}⚠ couldn't sync host repo — CLI may be stale${_R}" + } + fi + fi docker rm -f "$CONTAINER_NAME" 2>/dev/null || true rm -f "$WOLTS_DIR/.space/platform/tunnel.json" _build_image From ec5cfe4d3b65ee0f37c6259117fe11bf31b58530 Mon Sep 17 00:00:00 2001 From: UXwolt Date: Mon, 13 Apr 2026 04:14:47 +0000 Subject: [PATCH 2/4] Tighten host sync: only on --version, abort on dirty tree MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous version synced the host repo on every rebuild and silently warned-and-proceeded if the checkout failed — which reintroduces the exact drift bug we're trying to fix (image moves, host stays put). New contract: - Sync only when --version is explicitly passed. Plain 'rebuild' and branch builds leave the host repo alone; the host is the source of truth in those cases. - Skip when already at the target ref (noop, no fetch). - Abort with a clear message if the working tree is dirty. Silent drift is worse than a loud stop — user commits/stashes and retries. - Abort on checkout failure instead of warning. Same reasoning. This keeps the CLI ↔ container lockstep invariant without yanking devs or tagged users off their intended version. Co-Authored-By: Claude Opus 4.6 --- .claude/skills/woltspace-update/SKILL.md | 4 ++- woltspace | 37 +++++++++++++++--------- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/.claude/skills/woltspace-update/SKILL.md b/.claude/skills/woltspace-update/SKILL.md index effbfc6..57dfe92 100644 --- a/.claude/skills/woltspace-update/SKILL.md +++ b/.claude/skills/woltspace-update/SKILL.md @@ -136,7 +136,9 @@ fi woltspace rebuild --version "${LATEST}" ``` -**Always rebuild.** The image clones the repo at build time — there's no live-reload from the host. `--version` tells rebuild which tag to build. The rebuild command automatically syncs the host repo to the target version, keeping the `woltspace` CLI script in lockstep with the container. +**Always rebuild.** The image clones the repo at build time — there's no live-reload from the host. `--version` tells rebuild which tag to build. When `--version` is passed, rebuild also syncs the host repo to that tag so the `woltspace` CLI script stays in lockstep with the container. + +**If the host repo is dirty**, rebuild will abort before touching anything — check Step 1's dirty-tree rule and resolve (commit or stash) before retrying. ## Step 6: Verify diff --git a/woltspace b/woltspace index e5ff253..9d9cf7c 100755 --- a/woltspace +++ b/woltspace @@ -508,21 +508,30 @@ RESTORE # Sync host repo to match the version being built — keeps the CLI # script in lockstep with the container. Without this, _show_url() # and other host-side logic can drift from the container's code. - if [ "$LOCAL_BUILD" != true ] && [ -d "$WOLTSPACE_DIR/.git" ]; then - _sync_target="" - if [ -n "${CURRENT_VERSION:-}" ] && [ "$CURRENT_VERSION" != "HEAD" ]; then - _sync_target="$CURRENT_VERSION" - elif [ "$BUILD_BRANCH" != "main" ]; then - _sync_target="origin/$BUILD_BRANCH" - else - _sync_target="origin/main" - fi - if [ -n "$_sync_target" ]; then - echo " ${_D}syncing host CLI to $_sync_target${_R}" + # + # Only runs when --version is explicit: that's the one case where the + # user is deliberately moving to a specific version. Plain 'rebuild' + # or branch builds leave the host repo alone — it's the source of truth. + if [ -n "$BUILD_VERSION" ] && [ "$LOCAL_BUILD" != true ] && [ -d "$WOLTSPACE_DIR/.git" ]; then + _current_ref=$(git -C "$WOLTSPACE_DIR" describe --tags --exact-match 2>/dev/null \ + || git -C "$WOLTSPACE_DIR" rev-parse --abbrev-ref HEAD 2>/dev/null \ + || echo "") + if [ "$_current_ref" != "$BUILD_VERSION" ]; then + # Refuse to move a dirty working tree — silent drift is worse than a clear abort. + if ! git -C "$WOLTSPACE_DIR" diff --quiet HEAD 2>/dev/null \ + || [ -n "$(git -C "$WOLTSPACE_DIR" status --porcelain 2>/dev/null)" ]; then + echo " ${_D}✗ host repo has uncommitted changes at $WOLTSPACE_DIR${_R}" + echo " ${_L} commit or stash them, then retry.${_R}" + echo " ${_L} (the CLI must move with the image — aborting before drift.)${_R}" + exit 1 + fi + echo " ${_D}syncing host CLI to $BUILD_VERSION${_R}" git -C "$WOLTSPACE_DIR" fetch origin --tags --force --quiet 2>/dev/null || true - git -C "$WOLTSPACE_DIR" checkout "$_sync_target" --quiet 2>/dev/null || { - echo " ${_D}⚠ couldn't sync host repo — CLI may be stale${_R}" - } + if ! git -C "$WOLTSPACE_DIR" checkout "$BUILD_VERSION" --quiet 2>/dev/null; then + echo " ${_D}✗ couldn't check out $BUILD_VERSION in $WOLTSPACE_DIR${_R}" + echo " ${_L} resolve manually, then retry.${_R}" + exit 1 + fi fi fi docker rm -f "$CONTAINER_NAME" 2>/dev/null || true From 8d3e62954d13b7c2a19073babfd309907b479a13 Mon Sep 17 00:00:00 2001 From: jerpint Date: Mon, 13 Apr 2026 12:08:41 -0400 Subject: [PATCH 3/4] update skill --- .claude/skills/woltspace-update/SKILL.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/.claude/skills/woltspace-update/SKILL.md b/.claude/skills/woltspace-update/SKILL.md index 57dfe92..949c171 100644 --- a/.claude/skills/woltspace-update/SKILL.md +++ b/.claude/skills/woltspace-update/SKILL.md @@ -6,10 +6,19 @@ user_invocable: true # Update Woltspace -You are **updateWolt** đŸĻĢ â€” a beaver, the colony's builder. Introduce yourself as updateWolt when you first respond. You've been called to check on the lodge and bring in new timber if there's any. You're practical, clear, and careful. You don't pull without asking. +You are **updateWolt** đŸĻĢ â€” a wolt of the woltspace colony, the builder. You've been called to check on the lodge and bring in new timber if there's any. You're practical, clear, and careful. You don't pull without asking. **Your job:** check if a newer tagged version of woltspace exists, show the human what's changed, and update if they say yes. +## Step 0: Greet first, then work + +**Before running any commands, send a short greeting.** Introduce yourself as updateWolt and say what you're about to do — one or two sentences. Example: + +> đŸĻĢ updateWolt here — going to check the lodge for new timber. One sec. + +Then, in right away, start running the checks in Steps 1–2 and deliver the report in Step 3. +Don't front-load the work before the intro; the human wants to know who's on the job before the commands start flying. + ## Context You're running on the HOST machine, not inside the Docker container. You have access to: @@ -19,6 +28,8 @@ You're running on the HOST machine, not inside the Docker container. You have ac The wolts directory is at `$WOLTS_DIR` (default: `~/.woltspace/wolts`). Read it from the environment or fall back to the default. +**Host repo vs. container version.** Two things carry a version: the container (built from a tagged clone of the repo) and the host's `woltspace` CLI (a bash script in this repo). They can drift. `woltspace rebuild --version ` syncs both — rebuilding the image AND checking out the tag on the host so the CLI matches. Always trust the container's `.version` as the source of truth for what's deployed — with the caveat that for non-tag builds (`--local`, `--branch`) it reports the nearest ancestor tag, not the exact code. + **Updates are tag-based.** We only update to tagged releases (e.g. `v0.3.2` → `v0.4.0`). Unreleased commits on main are not offered as updates. ## Step 1: Check current version From 049bc11a7b10cbc5c6c252a6e25438282d357d27 Mon Sep 17 00:00:00 2001 From: jerpint Date: Mon, 13 Apr 2026 14:36:29 -0400 Subject: [PATCH 4/4] update skill --- .claude/skills/woltspace-update/SKILL.md | 40 ++++++++++-------------- 1 file changed, 17 insertions(+), 23 deletions(-) diff --git a/.claude/skills/woltspace-update/SKILL.md b/.claude/skills/woltspace-update/SKILL.md index 949c171..01ff6f8 100644 --- a/.claude/skills/woltspace-update/SKILL.md +++ b/.claude/skills/woltspace-update/SKILL.md @@ -32,37 +32,31 @@ The wolts directory is at `$WOLTS_DIR` (default: `~/.woltspace/wolts`). Read it **Updates are tag-based.** We only update to tagged releases (e.g. `v0.3.2` → `v0.4.0`). Unreleased commits on main are not offered as updates. -## Step 1: Check current version +## Steps 1–2: Preflight (keep it small) -```bash -# Current version from the running container -CURRENT=$(docker exec woltspace cat /workspace/woltspace/.version 2>/dev/null || echo "unknown") +**Guiding principle:** start with the smallest, least scary set of commands that tells you if an update is needed. Escalate only if the first pass is inconclusive. The human is approving each shell call — four one-liners read as obviously safe; one fat script does not. -# Or from git if container isn't running -if [ "$CURRENT" = "unknown" ]; then - CURRENT=$(git describe --tags --exact-match 2>/dev/null || git describe --tags --abbrev=0 2>/dev/null || git rev-parse --short HEAD) -fi +Run these as **separate Bash calls in parallel**, each with a short human description so each approval prompt is a tiny, focused thing: -# Fetch tags from remote (--force handles retagged releases) -git fetch origin --tags --force --quiet +1. **Deployed version** — `docker exec woltspace sh -c 'cat /workspace/woltspace/.version 2>/dev/null || (cd /workspace/woltspace && git describe --tags --abbrev=0 2>/dev/null) || echo unknown'` +2. **Latest release tag** — `git fetch origin --tags --quiet && git tag --sort=-v:refname | head -1` +3. **Container state** — `docker ps --filter name=woltspace --format '{{.Names}} {{.Status}}'` +4. **Local changes** — `git status --short` -# Latest release tag -LATEST=$(git tag --sort=-v:refname | head -1) -``` +That's the whole preflight. If `CURRENT == LATEST`, you're done — jump to Step 3's "up to date" path, no more calls. -## Step 2: Check container state +**Why the version call has fallbacks:** `.version` is stamped at image build time, but on older images or `--local` dev builds the file may be missing. The inline fallback (try `.version`, else `git describe` inside the container, else `"unknown"`) keeps the preflight silent and robust — no follow-up calls needed in the common cases. -```bash -# Is the container running? -CONTAINER_RUNNING=$(docker ps --filter name=woltspace --format '{{.Names}}' 2>/dev/null) +**Handling `unknown` or weird values:** +- `unknown` → container has no `.version` AND no git metadata. Report that you couldn't determine the deployed version and ask the user if they want to rebuild anyway (treat the latest tag as a safe upgrade target). +- Non-tag value (e.g. a SHA or `"local"`) → dev build or off-tag image. Don't compare with `==` to a tag — tell the user what you see and let them decide if an update makes sense. -# If running, check for active sessions -if [ -n "$CONTAINER_RUNNING" ]; then - ACTIVE_SESSIONS=$(docker exec woltspace session-reg list 2>/dev/null || true) -fi -``` +**Escalate only when needed:** +- Container isn't running → skip the version call entirely; treat host as source of truth via `git describe --tags --abbrev=0` +- An update is available AND the container is running → then list active sessions with `docker exec woltspace session-reg list` (no point before — if there's no update, session count doesn't matter) +- Retagged release suspected (rare) → re-run fetch with `--force` -Include this in your report: +Include in your report: - **Container running** with active sessions → warn: "You have active sessions. Updating will interrupt them." - **Container running** with no sessions → note: "Container is running, no active sessions." - **Container not running** → note: "Container isn't running — safe to update."