diff --git a/Makefile b/Makefile index b76823d..9af53d1 100644 --- a/Makefile +++ b/Makefile @@ -15,8 +15,9 @@ test: # Cluster-free unit tests for the deploy/ shell scripts: kubectl, kind and docker are # mocked on PATH and only the call log is asserted. Run in CI by the `deploy-scripts` job. # `set -e` so one failing test file fails the target instead of being scrolled past. +# deploy/claude/tests covers the /promote slash-command asset, which nothing else type-checks. test-deploy: - @set -e; for t in deploy/knative/tests/*.test.sh; do echo "== $$t"; bash "$$t"; done + @set -e; for t in deploy/knative/tests/*.test.sh deploy/claude/tests/*.test.sh; do echo "== $$t"; bash "$$t"; done typecheck: cd harness && pnpm exec tsc --noEmit diff --git a/README.md b/README.md index 9d4a70f..dd3b584 100644 --- a/README.md +++ b/README.md @@ -186,6 +186,24 @@ troubleshooting — is in **[`deploy/knative/README-ocp.md`](deploy/knative/READ Iterate on a workflow locally in Claude Code — skills, `CLAUDE.md`, memory, a slash command — then promote it: +From inside Claude Code, in the project you want to promote: + +```bash +mkdir -p ~/.claude/commands # once +cp deploy/claude/commands/promote.md ~/.claude/commands/ # once +export SH_HARNESS_DIR=/path/to/serverless-harness # once, per shell + +/promote my-workflow # in any project, from Claude Code +``` + +`/promote` sets the `HOME` override and `--project` for you, checks that the repo boundary and the +Redis tunnel are right before uploading, and reads the digest back through the cluster's own client +afterwards. Install it into your **real** `~/.claude/commands/`: with `HOME` pointed at the project, +`promote` bundles every prompt in that project's `.claude/commands/`, so a `/promote` living there +would ship itself into every bundle. + +Or drive the CLI directly: + ```bash cd harness && pnpm promote --entry my-workflow --project /path/to/your/project ``` diff --git a/deploy/claude/commands/promote.md b/deploy/claude/commands/promote.md new file mode 100644 index 0000000..b76b064 --- /dev/null +++ b/deploy/claude/commands/promote.md @@ -0,0 +1,98 @@ +--- +description: Promote this project's Claude Code workflow into the serverless harness +argument-hint: [--dry-run] [--deny ] [--sandbox-image ] +allowed-tools: Bash(pnpm:*), Bash(kubectl port-forward:*), Bash(kubectl exec:*), Bash(test:*), Bash(ls:*), Bash(lsof:*), Bash(pwd), Bash(echo:*), Bash(git init:*) +--- + + + +## Context + +- Project being promoted (this is what travels): !`pwd` +- Harness checkout (`$SH_HARNESS_DIR`): !`echo "${SH_HARNESS_DIR:-UNSET}"` +- Harness CLI present: !`test -f "${SH_HARNESS_DIR:-/nonexistent}/harness/package.json" && echo yes || echo NO` +- Workflow config here: !`ls -d .claude/skills .claude/commands 2>/dev/null || echo "no .claude/skills or .claude/commands"` +- Repo boundary for the context walk: !`test -e .git && echo ".git present" || echo "NO .git — see guard 1"` +- Cluster Redis tunnel on 16379: !`lsof -nP -iTCP:16379 -sTCP:LISTEN >/dev/null 2>&1 && echo "listening" || echo "absent"` + +## Your task + +Promote the workflow in the current directory into the harness, then explain the result. + +`$ARGUMENTS` — the **first** word is the entry prompt name (a file in `.claude/commands/.md`). +Pass any remaining words through to the CLI verbatim. With no arguments, list the available entry +names from `.claude/commands/` and stop; do not guess one. + +### Check the three guards first + +These are not hypothetical — each one produced a confident, wrong result during development. + +1. **No `.git` in this directory.** `promote` bounds its `CLAUDE.md` chain walk at a `.git` entry. + Without one it climbs into ancestor directories and sweeps their context files — including a + personal `~/CLAUDE.md` — into a bundle bound for a shared store. If the Context above says + `NO .git`, say so and offer `git init` before promoting. Do not promote past this silently. + +2. **`$SH_HARNESS_DIR` unset or wrong.** The CLI must run from the harness checkout: there is no + installed binary, and the sandbox inventory is resolved relative to the harness _module_, so + running it from here would silently degrade the binary check to `inventory_unavailable`. If it is + unset or the CLI is missing, stop and tell the user to + `export SH_HARNESS_DIR=/path/to/serverless-harness`. + +3. **Redis.** Upload must go to the **cluster's** Redis. If the tunnel is `absent`, start it in the + background: `kubectl port-forward -n default svc/redis 16379:6379`. Use **16379**, never 6379 — + this repo's own test container publishes `0.0.0.0:6379`, and promoting into it reports a + successful upload while the harness then fails with `config bundle not found` for that exact + digest. If 16379 is in use by something that is not this tunnel, say so rather than assuming. + +### Promote + +```bash +HOME="$PWD" REDIS_URL=redis://localhost:16379 \ + pnpm --dir "$SH_HARNESS_DIR/harness" promote --entry --project "$PWD" +``` + +Two parts of that are load-bearing, so do not "simplify" them: + +- **`HOME="$PWD"`** makes this directory its own user scope, so only this workflow travels. Without + it, promote reads your real `~/.claude` — measured at 56 travelling skills and ~8.6 MB versus one + skill and ~12 KB — and it is also what makes this project's `.claude/commands/` entry prompt + visible at all, since promote reads prompts from user scope only. +- **`--project "$PWD"`** is passed explicitly even though it looks redundant: run without it the CLI + promotes whatever directory the process started in, which through `pnpm --dir` is the harness + checkout, not this project. + +### Then verify and explain + +- If the upload succeeded, confirm the bundle reached the **cluster's** store by reading it back + through the cluster's own client, not through the tunnel — this is the only check that + distinguishes the two Redises: + `kubectl exec -n default deploy/redis -- redis-cli EXISTS "config:bundle:"` → expect `1`. +- Report: how many skills travelled, how many dropped and **why** for each, whether preflight was + clean, the digest, the bundle size, whether the upload was skipped as unchanged, and the lockfile + path. +- Show the dispatch envelope the CLI prints, with the real digest filled in. + +Interpret the exit code rather than echoing the raw error: + +- **exit 2 — preflight errors.** Name what blocked. `unknown_entry` means the entry prompt is not in + the bundle; list what is. +- **exit 3 — a structural credential match.** Give the `path:line` and the rule that matched. Say + plainly that this one blocks because a credential reaching a shared store cannot be un-shared by + re-promoting, and that the fix is removing it or adding the file to a deny-list — not a retry. +- **Warnings are not errors.** A clean promotion can still carry warnings (a dropped skill, a + binary the sandbox may not have). Summarise them; do not present them as failure. + +Finally, note what promotion does **not** carry: MCP servers and subagents are out of scope, and +promoted memory travels read-only, so a remote run consumes what it was taught locally and returns +discoveries in the leaf result instead of writing back. diff --git a/deploy/claude/tests/promote-command.test.sh b/deploy/claude/tests/promote-command.test.sh new file mode 100644 index 0000000..fc70a80 --- /dev/null +++ b/deploy/claude/tests/promote-command.test.sh @@ -0,0 +1,134 @@ +#!/usr/bin/env bash +# deploy/claude/tests/promote-command.test.sh +# +# Cluster-free tests for the /promote slash command asset. The command is a markdown prompt, so +# nothing type-checks it and nothing runs it in CI -- exactly the conditions under which +# promote-live-smoke.test.ts sat broken for a whole PR. These checks pin the parts that make it +# correct rather than merely present: +# +# - `allowed-tools` must cover every command the body actually invokes -- including after `&&` and +# `||`, which is where `echo` hid while an earlier version of this check claimed to cover +# "every command" and did not -- or the user approves a permission prompt on every run and the +# "one command" claim is false. Grants are matched whole, so tightening `kubectl:*` to per-verb +# grants strengthens the check instead of bypassing it. +# - the two flags whose absence silently promotes the WRONG THING (`--project`, `HOME=`) must be +# in the documented invocation. Both were verified by measurement: without --project the CLI +# promotes the harness checkout it was launched from, and without HOME it resolves 56 +# travelling skills and cannot find the entry prompt at all. +# - the redis tunnel must be 16379. On 6379 it reaches this repo's own test container, which +# reports a successful upload and then a `config bundle not found` from the harness. +# +# No cluster required. Run: bash deploy/claude/tests/promote-command.test.sh +set -uo pipefail + +CMD="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/commands/promote.md" +fails=0 +check() { if [ "$2" = "$3" ]; then echo " ok: $1"; else + echo " FAIL: $1 (want '$3', got '$2')" + fails=$((fails + 1)) +fi; } + +echo "== the asset exists and has frontmatter" +check "promote.md exists" "$([ -f "$CMD" ] && echo yes || echo no)" "yes" +[ -f "$CMD" ] || { + echo "FAILED: no command file" + exit 1 +} +check "opens with a frontmatter fence" "$(head -1 "$CMD")" "---" +FM="$(awk '/^---$/{n++; next} n==1{print} n>1{exit}' "$CMD")" +for key in description argument-hint allowed-tools; do + check "frontmatter has $key" "$(grep -cE "^$key:" <<< "$FM")" "1" +done + +echo "== allowed-tools covers every command the body invokes" +# Grants, kept WHOLE: "Bash(kubectl port-forward:*)" -> "kubectl port-forward". Collapsing these to +# a first word would accept any `kubectl` subcommand against a deliberately per-verb grant, so the +# check would silently weaken exactly when the grant is tightened. +PERMITTED="$(grep -oE 'Bash\([^):]+' <<< "$FM" | sed 's/^Bash(//' | sort -u)" +check "permits pnpm" "$(grep -cE '^pnpm' <<< "$PERMITTED")" "1" +check "grants kubectl per verb, not kubectl:*" \ + "$(grep -cxF 'kubectl' <<< "$PERMITTED")" "0" + +# Commands the body actually runs. Split on && || ; | and newlines FIRST: keeping only the first +# word of each probe hid every `&& echo`/`|| echo`, which is how `echo` stayed ungranted while a +# test claiming to cover "every command" passed. Env assignments are stripped so +# `HOME=... pnpm ...` is seen as pnpm. +USED="$( { + grep -oE '!`[^`]+`' "$CMD" | sed 's/^!`//; s/`$//' + awk '/^```bash$/{f=1; next} /^```$/{f=0} f' "$CMD" +} | sed -E 's/[[:space:]]*(&&|\|\||;|\|)[[:space:]]*/\n/g' | + sed -E 's/^[[:space:]]+//; s/^[0-9]*>[^[:space:]]*[[:space:]]*//' | + sed -E 's/^([A-Za-z_][A-Za-z0-9_]*=("[^"]*"|[^[:space:]]*)[[:space:]]+)+//' | + grep -oE '^[a-z][a-z0-9_-]*([[:space:]]+[a-z][a-z0-9_-]*)?' | sort -u)" + +# Each invocation must be covered by SOME grant, matched as a prefix so a two-word grant +# ("kubectl port-forward") covers a longer invocation and a one-word grant ("ls") still covers "ls -d". +for u in $(printf '%s\n' "$USED" | tr ' ' '@'); do + inv="$(printf '%s' "$u" | tr '@' ' ')" + covered=no + while read -r g; do + [ -z "$g" ] && continue + case "$inv " in "$g "*) covered=yes; break ;; esac + case "$inv" in "$g") covered=yes; break ;; esac + done <<< "$PERMITTED" + # A bare first word of a two-word grant is not itself an invocation (e.g. "kubectl" alone never + # runs); skip it rather than demanding a grant that would have to be widened. + if [ "$covered" = no ] && grep -qE "^$inv " <<< "$PERMITTED"; then covered=skip; fi + case "$covered" in + yes) echo " ok: allowed-tools covers '$inv'" ;; + skip) echo " ok: '$inv' is a grant prefix, not an invocation" ;; + *) + echo " FAIL: '$inv' is invoked but not granted" + fails=$((fails + 1)) + ;; + esac +done + +# kubectl is instructed in prose (guard 3's port-forward, the redis-cli EXISTS verify) rather than +# inside a ! probe or a bash fence, so the extraction above cannot see it. This floor exists to +# cover that blind spot -- it is not redundant with the loop. +for needed in "kubectl port-forward" "kubectl exec"; do + check "grants '$needed', which appears only in prose" \ + "$(grep -cxF "$needed" <<< "$PERMITTED")" "1" +done + +echo "== the invocation cannot silently promote the wrong thing" +# Counted as present-or-absent: both legitimately appear twice, once in the invocation and once +# in the prose explaining why they are not redundant. +present() { [ "$1" -ge 1 ] && echo 1 || echo 0; } +check "passes --project explicitly" "$(present "$(grep -c -- '--project "\$PWD"' "$CMD")")" "1" +check "sets HOME to the project" "$(present "$(grep -c 'HOME="\$PWD"' "$CMD")")" "1" +check "runs the CLI via --dir on the harness checkout" \ + "$(grep -c -- '--dir "\$SH_HARNESS_DIR/harness"' "$CMD")" "1" +check "explains why --project is not redundant" \ + "$(grep -ciE 'do not "simplify"|not "simplify" them' "$CMD")" "1" + +echo "== the redis tunnel is the private port, not 6379" +check "port-forwards 16379:6379" "$(grep -c '16379:6379' "$CMD")" "1" +check "REDIS_URL uses 16379" "$(grep -c 'redis://localhost:16379' "$CMD")" "1" +check "never points REDIS_URL at 6379" "$(grep -c 'redis://localhost:6379' "$CMD")" "0" +check "says why 6379 is wrong" "$(grep -ciE '0\.0\.0\.0:6379|test container publishes' "$CMD")" "1" + +echo "== the three measured guards are all present" +check "guard: .git bounds the context walk" "$(grep -ciE '\.git' "$CMD" | awk '$1>0{print 1; exit} {print 0}')" "1" +check "guard: SH_HARNESS_DIR / inventory resolution" \ + "$(grep -ciE 'inventory_unavailable' "$CMD" | awk '$1>0{print 1; exit} {print 0}')" "1" +check "guard: cluster-side verification of the upload" \ + "$(grep -c 'redis-cli EXISTS' "$CMD")" "1" + +echo "== exit codes are interpreted, not echoed" +check "explains exit 2 (preflight)" "$(grep -c 'exit 2' "$CMD")" "1" +check "explains exit 3 (structural secret)" "$(grep -c 'exit 3' "$CMD")" "1" +check "distinguishes warnings from errors" \ + "$(grep -ciE 'Warnings are not errors' "$CMD")" "1" + +echo "== it does not overpromise what promotion carries" +check "names MCP and subagents as out of scope" \ + "$(grep -ciE 'MCP servers and subagents' "$CMD")" "1" +check "says memory travels read-only" "$(grep -ciE 'read-only' "$CMD" | awk '$1>0{print 1; exit} {print 0}')" "1" + +if [ "$fails" -ne 0 ]; then + echo "FAILED: $fails check(s)" + exit 1 +fi +echo "PASS" diff --git a/deploy/knative/demo-promoted-workflow.sh b/deploy/knative/demo-promoted-workflow.sh index 3fd6816..5dd751d 100644 --- a/deploy/knative/demo-promoted-workflow.sh +++ b/deploy/knative/demo-promoted-workflow.sh @@ -282,16 +282,33 @@ mapfile -t POOL_PODS < <(kubectl get pods -n "$NS" -l "$POOL_SEL" -o name 2>/dev exit 1 } CACHE_DIR="/workspace/.sh-config/sha256-${DIGEST#sha256:}" +# Purge EVERY cached bundle, not just this run's digest. Purging one digest is not enough: any other +# bundle in the cache that happens to carry the same memory fact answers the bare arm just as well. +# Measured -- a bundle built from this same fixture before Prettier normalised one emphasis marker +# in the memory file has a different content digest, sat alongside, and made the control fail with +# the per-digest purge passing. for p in "${POOL_PODS[@]}"; do - kubectl exec -n "$NS" "$p" -- rm -rf "$CACHE_DIR" 2>/dev/null || true + kubectl exec -n "$NS" "$p" -- sh -c 'rm -rf /workspace/.sh-config/sha256-*' 2>/dev/null || true done still=0 +unreadable=0 for p in "${POOL_PODS[@]}"; do - kubectl exec -n "$NS" "$p" -- test -e "$CACHE_DIR" 2>/dev/null && still=$((still + 1)) + # Distinguish "zero bundles" from "could not look". An empty $n means the exec failed -- pod not + # ready, evicted, transient API error -- and `${n:-0}` would silently score that as a clean pod, + # letting the run claim the cache is empty having verified nothing. That is the same shape of + # false green this whole purge exists to prevent, so it must not be counted as evidence. + n=$(kubectl exec -n "$NS" "$p" -- sh -c 'ls -1d /workspace/.sh-config/sha256-* 2>/dev/null | wc -l' 2>/dev/null | tr -d ' \r') + if [ -z "$n" ]; then + unreadable=$((unreadable + 1)) + continue + fi + still=$((still + n)) done +[ "$unreadable" -eq 0 ] || + ko "could not inspect the cache on $unreadable pod(s); the control is unverified" [ "$still" -eq 0 ] && - ok "shared config cache purged from ${#POOL_PODS[@]} pool sandbox(es), so the control is honest" || - ko "the digest is still cached in $still sandbox(es); the bare run could read it" + ok "config cache emptied on ${#POOL_PODS[@]} pool sandbox(es), so the control is honest" || + ko "$still cached bundle(s) remain; the bare run could read one of them" SID_A="demo-bare-$$" SID_B="demo-promoted-$$" diff --git a/docs/demos/promoted-workflow-demo.md b/docs/demos/promoted-workflow-demo.md index 421f28d..32d9ce7 100644 --- a/docs/demos/promoted-workflow-demo.md +++ b/docs/demos/promoted-workflow-demo.md @@ -21,15 +21,17 @@ laptop cluster One digest names both halves. The dispatch carries the digest and nothing else about the workflow. -| Act | What "move my workflow to the server" normally needs | What promotion needs | -| ---------------------- | ------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | -| **1 — Author small** | A hand-written manifest listing what to ship, kept in sync by hand | **One env var.** `HOME=$SH_DEMO_SANDBOX` makes the sandbox its own user scope | -| **2 — Promote** | An image rebuild, a redeploy, a registry push | `pnpm promote` — a 12 KB tar, content-addressed; re-promotion uploads **nothing** | -| **3 — Run it** | A bespoke endpoint that knows about your skills | **One field.** `"configRef": "sha256:…"` on the existing prompt envelope | -| **4 — Know it landed** | Read the pod logs and hope | The run cites a fact only your memory holds, and a token only the sandbox can read | - -Prefer it non-interactive? `make demo-promoted-workflow` does all of this and asserts every claim. -This document is the version you drive by hand so you can explain each move. +| Act | What "move my workflow to the server" normally needs | What promotion needs | +| ---------------------- | ------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | +| **1 — Author small** | A hand-written manifest listing what to ship, kept in sync by hand | **One env var.** `HOME=$SH_DEMO_SANDBOX` makes the sandbox its own user scope | +| **2 — Promote** | An image rebuild, a redeploy, a registry push | **`/promote`, inside Claude Code** — a 12 KB tar, content-addressed; re-promotion uploads **nothing** | +| **3 — Run it** | A bespoke endpoint that knows about your skills | **One field.** `"configRef": "sha256:…"` on the existing prompt envelope | +| **4 — Know it landed** | Read the pod logs and hope | The run cites a fact only your memory holds, and a token only the sandbox can read | + +Promotion runs from **inside Claude Code** as `/promote`, in the same session you authored the +workflow in — Act 0 installs it. Prefer it non-interactive? `make demo-promoted-workflow` does all of +this and asserts every claim, driving the CLI directly since it has no Claude session to run in. This +document is the version you drive by hand so you can explain each move. --- @@ -75,11 +77,27 @@ export SH_DEMO_SANDBOX=/tmp/sh-demo export SH_DEMO_REDIS_PORT=16379 export SH_MODEL=claude-haiku-4-5 -# Where this repo is checked out. Every `pnpm --dir` below uses it, because from Act 1b onward -# your shell's cwd is the sandbox, not the checkout. -export HARNESS=$(pwd)/harness +# Where this repo is checked out. `/promote` reads this, because from Act 1b onward your shell's +# cwd is the sandbox, not the checkout -- and the CLI has no installed binary. +export SH_HARNESS_DIR=$(pwd) ``` +### Install the `/promote` command + +Act 2 runs promotion from **inside** Claude Code. Install the command once: + +```bash +mkdir -p ~/.claude/commands +cp deploy/claude/commands/promote.md ~/.claude/commands/promote.md +``` + +> **It belongs in your real `~/.claude/commands/`, not in the sandbox.** With `HOME` pointed at the +> sandbox, `promote` reads the sandbox's `.claude/commands/` as its prompts directory and bundles +> every markdown file there unconditionally — there is no exclusion flag, since the deny-list only +> covers skills. A `/promote` living in the sandbox would therefore ship itself into every bundle as +> a prompt template. In your real user scope, Claude Code sees it in every project and `promote` +> never does. + ### Open the two tunnels ```bash @@ -177,12 +195,15 @@ does not survive the trip. ## Act 2: Promote -### 2a. One command, and one env var that is the whole idea +### 2a. One slash command, from the session you authored in + +In the Claude Code session from Act 1b — still in the sandbox — run: -```bash -HOME=$SH_DEMO_SANDBOX pnpm --dir $HARNESS promote \ - --entry ship-note --project $SH_DEMO_SANDBOX ``` +/promote ship-note +``` + +Claude checks the three guards, starts the Redis tunnel if it is not up, and runs the CLI for you: ``` project: /tmp/sh-demo @@ -196,17 +217,26 @@ inventory: …/sandbox-inventory/ghcr.io_rossoctl_serverless-harness-sandbox_la preflight: no findings - bundle sha256:46ee1106… (12288 bytes, uploaded) + bundle sha256:43b8c4c0… (12288 bytes, uploaded) lockfile .claude/promoted.lock.json -dispatch with: {"sessionId":"/","kind":"prompt","prompt":"…","configRef":"sha256:46ee1106…"} +dispatch with: {"sessionId":"/","kind":"prompt","prompt":"…","configRef":"sha256:43b8c4c0…"} ``` -> **`HOME=$SH_DEMO_SANDBOX` is the demo, not a shortcut.** `promote` reads _user_ scope from -> `$HOME/.claude`. Pointing `HOME` at the sandbox makes the sandbox its own user scope, so the -> bundle holds this workflow and nothing else. It is also what makes the **slash command** travel: -> `promote` reads prompts from user scope only, so a project-scope `.claude/commands/` file is -> invisible to it without this. +> **The `HOME` override is the demo, not a shortcut** — `/promote` sets it for you, but say what it +> does. `promote` reads _user_ scope from `$HOME/.claude`; pointing `HOME` at the sandbox makes the +> sandbox its own user scope, so the bundle holds this workflow and nothing else. It is also what +> makes this project's **entry prompt** travel at all, since `promote` reads prompts from user scope +> only. The command it runs is: +> +> ```bash +> HOME="$PWD" REDIS_URL=redis://localhost:16379 \ +> pnpm --dir "$SH_HARNESS_DIR/harness" promote --entry ship-note --project "$PWD" +> ``` +> +> `--project "$PWD"` looks redundant and is not: without it the CLI promotes the directory the +> process started in, which through `pnpm --dir` is the harness checkout. Measured — it reports +> `project: …/serverless-harness/harness`. > > Note `travels 1`, `preflight: no findings`, and `12288 bytes`. Those three numbers are the > argument for sandbox-first authoring, and the last line hands you the exact dispatch envelope. @@ -216,7 +246,7 @@ The lockfile is committable, and it is also the most convenient place to read th ```bash export DIGEST=$(jq -r .digest $SH_DEMO_SANDBOX/.claude/promoted.lock.json) echo $DIGEST -# => sha256:46ee11062906cf70d6770a1a9c58b01429836ba4b16861b65110673904603bb4 +# => sha256:43b8c4c04fc084ed0df724572082ab35b381cd803d86675b8e52a2efb5e7cee6 ``` ### 2b. Verify it landed in the cluster's Redis, not somewhere else @@ -231,12 +261,14 @@ kubectl exec -n $NS deploy/redis -- redis-cli EXISTS "config:bundle:$DIGEST" ### 2c. Re-promotion is free -```bash -HOME=$SH_DEMO_SANDBOX pnpm --dir $HARNESS promote --entry ship-note --project $SH_DEMO_SANDBOX | tail -3 +Run it again, unchanged: + +``` +/promote ship-note ``` ``` - bundle sha256:46ee1106… (12288 bytes, unchanged — upload skipped) + bundle sha256:43b8c4c0… (12288 bytes, unchanged — upload skipped) ``` > Same digest, no upload. The bundle is content-addressed over a **canonical** tar — sorted paths, @@ -275,10 +307,16 @@ for p in $POOL; do kubectl exec -n $NS "$p" -- find /workspace/.sh-config -type ```bash for p in $POOL; do - kubectl exec -n $NS "$p" -- rm -rf "/workspace/.sh-config/sha256-${DIGEST#sha256:}" + kubectl exec -n $NS "$p" -- sh -c 'rm -rf /workspace/.sh-config/sha256-*' done ``` +> **Empty the whole cache, not just this digest.** Purging only the digest you are about to dispatch +> is not enough: any _other_ cached bundle carrying the same memory fact answers the bare arm just as +> well. That is not hypothetical — a bundle built from this same fixture before Prettier normalised +> one emphasis marker in the memory file has a different content digest, sits alongside, and made +> this control fail while a per-digest purge reported success. + ### 3b. Run A — bare ```bash @@ -377,8 +415,9 @@ You moved a workflow off your laptop without writing a manifest: 1. **Authored small** — one skill, a `CLAUDE.md`, one memory file, one slash command, in a throwaway sandbox. `HOME=$SH_DEMO_SANDBOX` made it its own user scope: **1 skill, 12 KB, zero preflight findings**, against 56 skills and ~8.6 MB for a real `~/.claude` (Act 1, 2a). -2. **Promoted with one command** — content-addressed over a canonical tar, so the second promotion - uploaded nothing and the digest was identical (Act 2a, 2c). +2. **Promoted with one slash command, without leaving Claude Code** — `/promote ship-note`, in the + same session you authored the workflow in. Content-addressed over a canonical tar, so the second + promotion uploaded nothing and the digest was identical (Act 2a, 2c). 3. **Dispatched with one field** — `configRef` on the existing prompt envelope. No new endpoint, no redeploy, no image rebuild (Act 3c). 4. **Proved all three channels arrived** — the skill's format, the `CLAUDE.md` rule, and an