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..0445e31 100644 --- a/deploy/claude/tests/promote-command.test.sh +++ b/deploy/claude/tests/promote-command.test.sh @@ -103,6 +103,20 @@ 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" +# 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" \ + "$(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" 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..6d934e3 100644 --- a/docs/demos/promoted-workflow-demo.md +++ b/docs/demos/promoted-workflow-demo.md @@ -84,19 +84,49 @@ 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, 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 +``` + +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` +> 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 @@ -184,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. @@ -232,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`. @@ -456,6 +497,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