From 3d7cd59d24a306345d9343787032e471cb51d129 Mon Sep 17 00:00:00 2001 From: Paolo Dettori Date: Thu, 3 Sep 2026 16:27:31 -0400 Subject: [PATCH 1/2] docs(demos): offer sandbox placement for /promote, now that --exclude-prompt exists Completes the follow-up #218 promised and #217 could not take. #217 had to install /promote into real user scope, and justified it with a constraint that no longer holds: that a command living in the project it promotes would ship itself into every bundle. --exclude-prompt removes that, so the placement that actually buys local/remote parity becomes available. The runbook now offers both, and says what each costs: - **Option A, real user scope.** Simpler. The authoring session loads your whole ~/.claude, so the local agent has every skill you own while the promoted run gets the sandbox's one -- which makes "it behaved the same locally" weaker evidence than it looks. - **Option B, in the sandbox**, with Claude Code launched as `HOME=$SANDBOX claude`, so the local agent sees exactly what the promoted run will. Costs a re-auth and your own skills for the duration. That is the standard dev/prod-parity trade, and parity is this demo's whole claim. /promote self-excludes rather than making the reader remember: a Context probe reports whether `.claude/commands/promote.md` exists in the project, and the body adds `--exclude-prompt promote` only in that case. The condition matters as much as the flag -- passing it unconditionally would trip the flag's own typo guard (`prompt_exclude_unmatched`), which exists precisely because an unmatched exclusion ships the prompt you meant to omit. Measured, and recorded in the runbook because it is the reassuring part: **both options produce the same bundle.** Option B without the exclusion is 19456 bytes -- the command itself travelling -- and with it 12288 bytes at sha256:43b8c4c0..., byte-identical to Option A. So every digest quoted in the walkthrough holds for either placement. Also reconciles what the rebase exposed: README carried #217's claim that a project-local /promote "would ship itself into every bundle" two paragraphs above #218 documenting the flag that prevents it, plus a doubled lead-in from the two edits landing separately. The "Notes and limits" fidelity entry is rewritten rather than deleted: Option A's gap is real, it is now a choice rather than a constraint, and a performer should say which option they ran if asked whether local matched remote. Verification: `make lint` (9 hooks), `make test-deploy` (129 checks), the demo at 14 passed / 0 failed against kind, and Option B exercised end to end from a scratch sandbox -- with and without the self-exclusion -- to produce the two byte counts above. Assisted-By: Claude (Anthropic AI) Signed-off-by: Paolo Dettori --- README.md | 16 ++++---- deploy/claude/commands/promote.md | 10 ++++- deploy/claude/tests/promote-command.test.sh | 11 +++++ docs/demos/promoted-workflow-demo.md | 45 +++++++++++++++++---- 4 files changed, 67 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 2f3954e..cacb373 100644 --- a/README.md +++ b/README.md @@ -183,10 +183,8 @@ troubleshooting — is in **[`deploy/knative/README-ocp.md`](deploy/knative/READ ### Promoting a local Claude Code workflow -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: +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 @@ -198,9 +196,13 @@ export SH_HARNESS_DIR=/path/to/serverless-harness # once, per shell `/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. +afterwards. + +Installed in your **real** `~/.claude/commands/` as above, it is visible in every project and never +travels. You can instead keep it **in the project**, which lets you run Claude Code with +`HOME=` so the local agent sees exactly what the promoted run will; `/promote` then detects +that it lives there and self-excludes with `--exclude-prompt promote` (see below), because otherwise +it would ship itself into every bundle. Or drive the CLI directly: diff --git a/deploy/claude/commands/promote.md b/deploy/claude/commands/promote.md index b76b064..598cec9 100644 --- a/deploy/claude/commands/promote.md +++ b/deploy/claude/commands/promote.md @@ -12,7 +12,8 @@ allowed-tools notes, so a later edit does not widen this back: * pnpm stays broad on purpose. The documented invocation begins with env assignments (`HOME="$PWD" REDIS_URL=... pnpm --dir ...`), so a narrower `Bash(pnpm --dir:*)` prefix is not reliably matched, and the env has to be inline because exports do not survive between tool calls. - * echo is granted because five of the six Context probes end in `&& echo`/`|| echo`. + * echo is granted because the Context probes end in `&& echo`/`|| echo`. + * the self-exclusion probe needs nothing new: `test -f` and `echo` are already granted. * jq and `git rev-parse` were granted but never invoked, and are gone. `git init` stays: guard 1 offers it. --> @@ -25,6 +26,7 @@ allowed-tools notes, so a later edit does not widen this back: - 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"` +- This command lives in the project: !`test -f .claude/commands/promote.md && echo "yes — must self-exclude" || echo "no"` ## Your task @@ -62,6 +64,12 @@ HOME="$PWD" REDIS_URL=redis://localhost:16379 \ pnpm --dir "$SH_HARNESS_DIR/harness" promote --entry --project "$PWD" ``` +**If the Context above says this command lives in the project, add +`--exclude-prompt promote`.** Otherwise it ships itself into the bundle as a prompt template, since +`HOME="$PWD"` makes this project's `.claude/commands/` the prompts directory. Add it **only** in that +case: passing it when there is no local `promote.md` produces a `prompt_exclude_unmatched` warning, +which is the flag's own typo guard firing on a false alarm. + 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 diff --git a/deploy/claude/tests/promote-command.test.sh b/deploy/claude/tests/promote-command.test.sh index fc70a80..dc74427 100644 --- a/deploy/claude/tests/promote-command.test.sh +++ b/deploy/claude/tests/promote-command.test.sh @@ -103,6 +103,17 @@ check "runs the CLI via --dir on the harness checkout" \ check "explains why --project is not redundant" \ "$(grep -ciE 'do not "simplify"|not "simplify" them' "$CMD")" "1" +echo "== it self-excludes when it lives in the project it promotes" +# Without this the command ships itself as a prompt template whenever it is installed in the project +# (which is the placement that buys local/remote parity). The conditional matters as much as the +# flag: passing --exclude-prompt unconditionally would trip the flag's own typo guard. +check "probes whether the project carries this command" \ + "$(grep -c 'test -f .claude/commands/promote.md' "$CMD")" "1" +check "tells the model to pass --exclude-prompt promote" \ + "$(grep -c -- '--exclude-prompt promote' "$CMD")" "1" +check "says to add it ONLY when the command is local" \ + "$(grep -ciE 'only.*in that case|prompt_exclude_unmatched' "$CMD" | awk '$1>0{print 1; exit} {print 0}')" "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" diff --git a/docs/demos/promoted-workflow-demo.md b/docs/demos/promoted-workflow-demo.md index 32d9ce7..07dec5a 100644 --- a/docs/demos/promoted-workflow-demo.md +++ b/docs/demos/promoted-workflow-demo.md @@ -84,19 +84,45 @@ export SH_HARNESS_DIR=$(pwd) ### Install the `/promote` command -Act 2 runs promotion from **inside** Claude Code. Install the command once: +Act 2 runs promotion from **inside** Claude Code, and there are two places to install it. They differ +in one thing that matters for this demo: whether the session you author in resembles the one that +runs remotely. + +**Option A — real user scope. Simpler; use this if you are just following along.** ```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. +Claude Code sees `/promote` in every project, and `promote` — reading the sandbox as user scope — +never sees it, so it cannot travel. The cost: your authoring session also loads your real +`~/.claude`, so locally the agent has every skill you own while the promoted run gets the sandbox's +one. + +**Option B — in the sandbox, for actual parity.** Put the command in the sandbox and launch Claude +Code with `HOME` pointed there, so the local agent sees _exactly_ what the promoted run will: + +```bash +mkdir -p $SH_DEMO_SANDBOX/.claude/commands +cp deploy/claude/commands/promote.md $SH_DEMO_SANDBOX/.claude/commands/promote.md +cd $SH_DEMO_SANDBOX && HOME=$SH_DEMO_SANDBOX claude # instead of Act 1b's plain `claude` +``` + +> **This is what `--exclude-prompt` is for.** With `HOME` pointed at the sandbox, that +> `.claude/commands/` _is_ promote's prompts directory, and every markdown file in it travels — so +> without the flag the command would ship itself into every bundle as a prompt template. `/promote` +> detects that it lives in the project and adds `--exclude-prompt promote` itself; you do not pass +> it by hand. It adds the flag **only** in that case, because an exclusion matching nothing warns — +> the flag's own typo guard. +> +> The trade: a fresh `HOME` means Claude Code re-authenticates, and you lose your own skills for the +> duration. That is the standard dev/prod-parity cost, and parity is the whole claim this demo makes, +> so Option B is the more honest way to perform it. +> +> **Either option produces the same bundle**, so every digest quoted below holds for both. Measured: +> Option B without the exclusion is 19456 bytes (the command itself travelling); with it, 12288 bytes +> and `sha256:43b8c4c0…` — byte-identical to Option A. ### Open the two tunnels @@ -456,6 +482,11 @@ kubectl exec -n $NS deploy/redis -- redis-cli DEL "config:bundle:$DIGEST" ## Notes and limits +- **Option A's authoring session is not what runs remotely.** With your real `HOME`, Claude Code + loads your whole `~/.claude` while the promoted run gets the sandbox's one skill — so "it behaved + the same locally" is weaker evidence than it looks. Option B closes that with + `HOME=$SH_DEMO_SANDBOX` plus `--exclude-prompt`, at the cost of a re-auth. Say which one you ran + if someone asks whether local matched remote. - **The `TOKEN:` line is the one model-dependent claim.** The format and the token both require the model to choose to `read` the skill body. `CLAUDE.md` says it MUST, and the deterministic channel carries that instruction, so it is reliable in practice — but it is not a mechanical guarantee the From 2a18d637bce678b3da082a50fd6992d05d78ee51 Mon Sep 17 00:00:00 2001 From: Paolo Dettori Date: Fri, 4 Sep 2026 09:08:06 -0400 Subject: [PATCH 2/2] docs(demos): make Option B's narration and install match what it actually runs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three review findings on #219. Two were seams in Option B rather than the flag logic, and both would have misled a performer reading aloud. **The narrated invocation did not match the digest above it.** Act 2a quotes the command as "the command it runs", directly beneath the `12288 bytes` / `sha256:43b8c4c0…` output — but under Option B what runs carries `--exclude-prompt promote`, and the quoted form produces 19456 bytes. That is the exact discrepancy this PR measured, reproduced in its own narration. The block now names the flag and says what omitting it costs, so "either option produces the same bundle" is literally true of the command on screen and not just of the digests. **The Option B install block folded a later step into an earlier one.** `HOME=$SH_DEMO_SANDBOX claude` sat inside the Act 0 install fence with only a trailing comment to say it was for later. Run where it sat it would start Claude Code on an empty sandbox with no tunnels open, and leave the shell in `$SH_DEMO_SANDBOX`, where Act 1a's repo-relative `cp` paths cannot resolve. Install is now install only, and the launch moved to Act 1b as an explicit Option B variant -- correct in time, and next to the plain launch it replaces. **A dead regex left the conditionality unpinned.** `grep -ciE 'only.*in that case|…'` never matched its first alternative, because grep is line-based and that phrase wraps in `promote.md` -- measured 0 for the phrase and 1 for the warning name, so the check passed on the alternative alone. A reflow that dropped the "only" while keeping `prompt_exclude_unmatched` would have stayed green, in a check named for the conditionality. Now folds newlines first, and negative-tested: removing "only" from the command fails the check. Verification: `make lint` (9 hooks), `make test-deploy` (129 checks), demo 14 passed / 0 failed against kind. Also re-checked the ordering the review flagged as safe -- installing promote.md first and then running Act 1a's `cp -R` leaves both promote.md and ship-note.md in place, since the fixture commands dir holds only ship-note.md. Assisted-By: Claude (Anthropic AI) Signed-off-by: Paolo Dettori --- deploy/claude/tests/promote-command.test.sh | 5 ++++- docs/demos/promoted-workflow-demo.md | 21 ++++++++++++++++++--- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/deploy/claude/tests/promote-command.test.sh b/deploy/claude/tests/promote-command.test.sh index dc74427..0445e31 100644 --- a/deploy/claude/tests/promote-command.test.sh +++ b/deploy/claude/tests/promote-command.test.sh @@ -111,8 +111,11 @@ check "probes whether the project carries this command" \ "$(grep -c 'test -f .claude/commands/promote.md' "$CMD")" "1" check "tells the model to pass --exclude-prompt promote" \ "$(grep -c -- '--exclude-prompt promote' "$CMD")" "1" +# Folded to one line before matching: `grep` is line-based and this phrase wraps in promote.md, so +# an `only.*in that case` pattern never matched and the check passed on the warning name alone -- +# leaving the conditionality it is named for unpinned against a reflow that dropped the "only". check "says to add it ONLY when the command is local" \ - "$(grep -ciE 'only.*in that case|prompt_exclude_unmatched' "$CMD" | awk '$1>0{print 1; exit} {print 0}')" "1" + "$(tr '\n' ' ' < "$CMD" | grep -ciE 'add it [^.]*only[^.]*in that case' | awk '$1>0{print 1; exit} {print 0}')" "1" echo "== the redis tunnel is the private port, not 6379" check "port-forwards 16379:6379" "$(grep -c '16379:6379' "$CMD")" "1" diff --git a/docs/demos/promoted-workflow-demo.md b/docs/demos/promoted-workflow-demo.md index 07dec5a..6d934e3 100644 --- a/docs/demos/promoted-workflow-demo.md +++ b/docs/demos/promoted-workflow-demo.md @@ -100,15 +100,19 @@ never sees it, so it cannot travel. The cost: your authoring session also loads `~/.claude`, so locally the agent has every skill you own while the promoted run gets the sandbox's one. -**Option B — in the sandbox, for actual parity.** Put the command in the sandbox and launch Claude -Code with `HOME` pointed there, so the local agent sees _exactly_ what the promoted run will: +**Option B — in the sandbox, for actual parity.** Put the command in the sandbox, so the local agent +can be made to see _exactly_ what the promoted run will: ```bash mkdir -p $SH_DEMO_SANDBOX/.claude/commands cp deploy/claude/commands/promote.md $SH_DEMO_SANDBOX/.claude/commands/promote.md -cd $SH_DEMO_SANDBOX && HOME=$SH_DEMO_SANDBOX claude # instead of Act 1b's plain `claude` ``` +Installing is all that happens here. The matching launch (`HOME=$SH_DEMO_SANDBOX claude`) belongs in +**Act 1b**, and only after Act 1a has populated the sandbox: run it now and Claude Code would start +on an empty sandbox with no tunnels open, and your shell would be left in `$SH_DEMO_SANDBOX`, where +Act 1a's repo-relative `cp` paths cannot resolve. + > **This is what `--exclude-prompt` is for.** With `HOME` pointed at the sandbox, that > `.claude/commands/` _is_ promote's prompts directory, and every markdown file in it travels — so > without the flag the command would ship itself into every bundle as a prompt template. `/promote` @@ -210,6 +214,13 @@ It records that the regression is tracked as **KAG-4471** and ships behind cd $SH_DEMO_SANDBOX && claude ``` +Under **Option B**, launch it this way instead — a sandbox `HOME` is what makes the local agent's +configuration identical to the promoted run's: + +```bash +cd $SH_DEMO_SANDBOX && HOME=$SH_DEMO_SANDBOX claude +``` + Ask it: `Write the ship note for the auth timeout fix.` You get the house `SHIP NOTE` block with the ticket and the risk line. This is the "works on my laptop" baseline — the thing that normally does not survive the trip. @@ -258,8 +269,12 @@ dispatch with: {"sessionId":"/","kind":"prompt","prompt":"…","conf > ```bash > HOME="$PWD" REDIS_URL=redis://localhost:16379 \ > pnpm --dir "$SH_HARNESS_DIR/harness" promote --entry ship-note --project "$PWD" +> # ...plus --exclude-prompt promote under Option B, which /promote adds for you > ``` > +> That trailing flag is not cosmetic: under Option B the command is in the sandbox, so without it the +> bundle is 19456 bytes rather than the 12288 printed above. +> > `--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`.