docs(quick-261008-k7s): Bootstrap script PATH hint per login shell

This commit is contained in:
Jakub Zych
2026-10-08 15:36:01 +02:00
parent ddf1178c51
commit a6e2c399a5
3 changed files with 280 additions and 3 deletions

View File

@@ -5,10 +5,10 @@ current_phase: "14.3"
current_phase_name: "One-command app scaffolding: summer new and summercms.io/new bootstrap script"
status: verifying
stopped_at: Completed 14.3-04-PLAN.md
last_updated: "2026-10-08T10:32:08.732Z"
last_updated: "2026-10-08T13:36:01.429Z"
last_activity: 2026-10-08
last_activity_desc: Phase 14.3 execution started
state_head: 9dac7b8f67e4345c93e778b17c50ab6fd399d343
state_head: ddf1178c513c290221ca417f487e3da29d2190a1
progress:
total_phases: 31
completed_phases: 13
@@ -31,7 +31,7 @@ See: .planning/PROJECT.md (updated 2026-09-16)
Phase: 14.3 (One-command app scaffolding: summer new and summercms.io/new bootstrap script) — EXECUTING
Plan: 4 of 4
Status: Phase complete — ready for verification
Last activity: 2026-10-08 — Phase 14.3 execution started
Last activity: 2026-10-08 - Completed quick task 261008-k7s: Bootstrap script PATH hint per login shell
Progress: [██████████] 100%
@@ -669,6 +669,7 @@ Recent decisions affecting current work:
| 261007-p4y | Switch toggle pixel sizing; settings entries linking to admin controllers; translate Locales moved to Settings | 2026-10-07 | 0b09acd | [261007-p4y-toggle-proportions-and-settings-controll](./quick/261007-p4y-toggle-proportions-and-settings-controll/) |
| 18 | cabana list: join to-many relation column values with ", " | 2026-10-07 | fa25b4e | — |
| 261007-qgx | Admin record heading, crumb and title from the first text or mltext field (default locale) | 2026-10-07 | a714bb4 | [261007-qgx-admin-record-heading-uses-translatable-m](./quick/261007-qgx-admin-record-heading-uses-translatable-m/) |
| 261008-k7s | Bootstrap script PATH hint per login shell (fish/zsh/bash) | 2026-10-08 | ddf1178 | [261008-k7s-bootstrap-script-path-hint-per-shell](./quick/261008-k7s-bootstrap-script-path-hint-per-shell/) |
## Deferred Items

View File

@@ -0,0 +1,200 @@
---
phase: quick-261008-k7s
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- modules/sunrise/new.sh
- modules/sunrise/script_behavior_test.go
- modules/sunrise/sunrise_test.go
- modules/sunrise/README.md
- docs/setup/installation.md
autonomous: true
requirements: [QUICK-261008-k7s]
estimate:
tokens: 45000
raw_tokens: 45000
tasks: 2
confidence: low
must_haves:
truths:
- "With SHELL naming fish, the bootstrap script prints ` fish_add_path \"<gobin>\"` after a header saying fish_add_path also keeps it for new fish sessions, and prints no export line and no shell-profile advice."
- "With SHELL naming zsh it prints ` export PATH=\"<gobin>:$PATH\"` and tells the user to add it to ~/.zshrc; with SHELL naming bash, the same line and ~/.bashrc."
- "With SHELL empty, truly unset (main runs under set -u) or naming any other shell, the hint output is byte-identical to today's: the export line plus 'and add it to your shell profile'."
- "$PATH stays literal in the printed export line; an install directory containing backslash, double quote, dollar, backtick or ! is escaped so the printed line, pasted into the named shell, adds exactly that directory."
- "When the install directory is on PATH no hint is printed, whatever SHELL says."
- "TestScriptBehavior is deterministic on any developer machine: the harness sets SHELL itself, because bash fills an unset SHELL from the passwd entry (on this host /usr/bin/fish)."
- "The sunrise README and docs/setup/installation.md describe the per-shell hint, the manual PATH section shows `fish_add_path (go env GOPATH)/bin` for fish, and TestDocsTree plus `summer docs:build --check` pass."
artifacts:
- path: modules/sunrise/new.sh
provides: "login-shell-aware PATH hint and a double-quote escaping helper"
contains: "fish_add_path"
- path: modules/sunrise/script_behavior_test.go
provides: "per-shell hint cases (fish, zsh, bash, empty, unset, unknown) and metacharacter escaping cases"
contains: "SHELL="
- path: modules/sunrise/sunrise_test.go
provides: "TestBootstrapEndToEnd pinned to SHELL=/bin/bash"
contains: "SHELL=/bin/bash"
- path: modules/sunrise/README.md
provides: "per-shell hint description"
contains: "fish_add_path"
- path: docs/setup/installation.md
provides: "per-shell hint in step 3 and fish variant in the manual PATH section"
contains: "fish_add_path (go env GOPATH)/bin"
key_links:
- from: modules/sunrise/new.sh
to: modules/sunrise/script_behavior_test.go
via: "exact header notes and command lines asserted per SHELL value"
pattern: "fish_add_path also keeps it for new fish sessions"
- from: modules/sunrise/script_behavior_test.go
to: "harness run() environment"
via: "default SHELL= entry placed before h.env so per-case SHELL values win (exec.Cmd keeps the last duplicate)"
pattern: "\"SHELL=\""
- from: modules/sunrise/sunrise_test.go
to: "scripts/check-phase14.3.sh acceptance grep"
via: "both keep the export line shape for bash: export PATH=\"<dir>:$PATH\""
pattern: "export PATH="
---
<objective>
Make the bootstrap script's closing PATH hint match the user's login shell. Under `curl | bash` the script always runs in bash, but `SHELL` still names the login shell, so `new.sh` reads the basename of `SHELL` and prints `fish_add_path "<gobin>"` for fish (which persists the entry itself), `export PATH="<gobin>:$PATH"` plus `~/.zshrc` for zsh or `~/.bashrc` for bash, and today's generic export line plus "your shell profile" for anything else, empty or unset. The install directory is escaped for the target shell's double quotes, so the printed line is safe to paste.
Purpose: a fish user who pastes today's `export PATH=...` line gets a session-only change and is told to edit a profile that fish does not read. The hint should be one correct line per shell.
Output: updated `modules/sunrise/new.sh`, its behaviour tests, the gated end-to-end test, the sunrise README and the installation docs page, all in one commit (project rule: code and its README/docs change together). No tag, push or deploy.
</objective>
<execution_context>
@~/.claude/gsd-core/workflows/execute-plan.md
@~/.claude/gsd-core/templates/summary.md
</execution_context>
<context>
@.planning/STATE.md
@CLAUDE.md
@modules/sunrise/new.sh
@modules/sunrise/script_behavior_test.go
@modules/sunrise/README.md
@docs/setup/installation.md
Facts established while planning (do not re-derive):
- Bash fills an unset `SHELL` from the passwd entry: `env -u SHELL bash -c 'declare -p SHELL'` prints `declare -- SHELL="/usr/bin/fish"` on this host. The test harness builds the environment from scratch without `SHELL`, so without a harness default every case would see the developer's login shell and the existing `export PATH` assertions would fail on a fish machine. `SHELL=` (set, empty) survives: bash keeps an existing variable.
- Go's `exec.Cmd` keeps the last value when `Env` holds a key twice, so a default appended before `h.env` is overridden by a per-case entry in `h.env`.
- `scripts/check-phase14.3.sh` (acceptance and production stages, near line 306) greps `export PATH="/root/go/bin:$PATH"` from a node:24-bookworm container. There `SHELL` is not in the environment, so bash fills it from root's passwd entry (`/bin/bash`), and the bash branch prints exactly that export line. The gate keeps working unchanged; do not edit it. The site repo's TestTerminalCommands puts GOBIN first on PATH, so it never sees a hint.
- `.planning/phases/14.3-.../14.3-SECURITY-REVIEW.md` says new.sh is shellcheck clean "except one deliberate SC2016 for the literal `$PATH` in the hint". Keep that true: no new shellcheck disable directives.
- The `--forbidden` gate stage fails if new.sh contains the framework configuration prefix (the upper-case word SUMMER followed by an underscore). The script's own `SUMMERCMS_` names do not match it. Do not add new variable names with that prefix.
- The docs checker only checks `pkg.Ident` code spans and `summer` / `./bin/<app>` commands in shell fences, so `fish_add_path`, `~/.zshrc` and a fish line inside an `sh` fence are safe. `sh` is the only shell fence language the docs use; do not add a `fish` fence.
- macOS still ships bash 3.2 for `curl | bash`, and bash 5.2 turned on `patsub_replacement`, which changes how `&` and backslashes behave in the replacement part of the substitution expansion. A per-character loop behaves the same on both.
</context>
<tasks>
<task type="tracer" tdd="true">
<name>Task 1: Per-shell PATH hint in new.sh, proven end to end by the behaviour harness</name>
<files>modules/sunrise/new.sh, modules/sunrise/script_behavior_test.go, modules/sunrise/sunrise_test.go</files>
<behavior>
- Harness default SHELL is empty: the hint is `\n==> summer is installed in <gobin>, which is not on your PATH. Run this (and add it to your shell profile):\n export PATH="<gobin>:$PATH"\n`, byte-identical to today's.
- SHELL truly unset (harness option that unsets it inside bash before sourcing the script): same generic output, exit 0, no "unbound variable" error.
- SHELL=/bin/tcsh (unknown): generic output.
- SHELL=/usr/bin/fish: header note `fish_add_path also keeps it for new fish sessions`, command line ` fish_add_path "<gobin>"`; stdout lacks `export PATH` and `shell profile`.
- SHELL=/bin/zsh: note `and add it to ~/.zshrc`, export line; lacks `fish_add_path`.
- SHELL=/usr/local/bin/bash: note `and add it to ~/.bashrc`, export line; lacks `fish_add_path`.
- Install dir containing a space, backslash, double quote, dollar, backtick and `!`, with SHELL=/bin/zsh: export line holds the POSIX-escaped dir; with SHELL=/usr/bin/fish: fish_add_path line holds the fish-escaped dir (backtick and `!` unescaped).
- GOBIN on PATH with SHELL=/usr/bin/fish: no `export PATH`, no `fish_add_path`, no `not on your PATH`.
</behavior>
<action>
In `modules/sunrise/new.sh`:
1. Add a function, defined above `main` like every other helper so the truncation guarantee holds (name it `dq_escape` or similar, with a one-line comment in the file's style). It prints its first argument escaped for use inside double quotes of the shell named by its second argument. For every shell: a backslash before each backslash, double quote and dollar sign. For any shell other than fish, also a backslash before each backtick, and each `!` replaced by close-quote, single-quoted `!`, reopen-quote (the three characters `"'!'"` around nothing else), so interactive bash and zsh do not history-expand a pasted line. Fish double quotes treat backtick and `!` literally, and a backslash before them would stay in the path, so the fish form leaves them alone. Build the result with a per-character loop over `${#s}` / `${s:i:1}` and a `case` on the character, not the substitution expansion (see the bash 3.2 / 5.2 fact in context). Only builtins, all locals declared `local`.
2. Replace the hint block at the end of `main` (the `if [[ :${PATH:-}: != *":$gobin:"* ]]` block; the PATH test itself stays as it is). Inside it: take the login shell name as the basename of `${SHELL:-}` by parameter expansion (`##*/`), always via `${SHELL:-}` because `main` runs under `set -u`. Choose a note and a form by a `case` on the name: `fish` gives the note `fish_add_path also keeps it for new fish sessions`; `zsh` gives `and add it to ~/.zshrc`; `bash` gives `and add it to ~/.bashrc`; anything else, empty included, gives `and add it to your shell profile`. Print the header with the existing format, `\n==> summer is installed in %s, which is not on your PATH. Run this (%s):\n`, fed the raw gobin and the note. Then for fish print ` fish_add_path "%s"\n` with the fish-escaped dir; for every other shell keep the single existing ` export PATH="%s:$PATH"\n` printf, now fed the POSIX-escaped dir, with its existing SC2016 disable comment and the "$PATH stays literal" comment. That stays the only disable directive in the file. Store notes in variables or format strings, never as a bare quoted word that starts with a tilde, because shellcheck SC2088 flags those.
3. In the header comment, after the environment overrides block, add one line saying the login shell named by SHELL only picks the form of the PATH hint printed at the end (fish, zsh, bash, or a generic export line). Leave the "Only bash builtins and the git, go and docker commands are used" sentence true.
In `modules/sunrise/script_behavior_test.go`:
4. In `run()`, add `"SHELL="` to the environment before `h.env` is appended, with a comment saying bash fills an unset SHELL from the passwd entry, so the default keeps cases independent of the developer's login shell and a case sets its own SHELL through `h.env`.
5. Add a harness field `unsetShell bool`. When it is true, `run()` starts bash with `-c` and a command string that unsets SHELL and then sources the script given as `$0` with the remaining arguments (`h.script` as `$0`, the case args after it), so the script runs with SHELL truly unset under `set -u`. All other `run()` behaviour (dir, env, stdin probe, result) stays the same.
6. Add two Go helpers: `expectLacks` (the mirror of `expectContains`) and a function that returns the expected escaped dir for the POSIX or the fish form, built with `strings.NewReplacer` using exactly the mapping from step 1. Use it for every expected hint line so the expectations do not depend on characters in the temp root.
7. Replace the "GOBIN off PATH prints the hint" case and add cases so the table covers each bullet in `<behavior>`: empty SHELL (harness default), unset (`h.unsetShell = true`), unknown (`/bin/tcsh`), fish, zsh, bash (`/usr/local/bin/bash`, a path that differs from the common default), and the two metacharacter cases. For the metacharacter cases, set `h.gobin` to a new dir under `h.root` whose name holds a space, backslash, double quote, dollar, backtick and `!`, create it with `os.MkdirAll` before `h.mustRun("demo")`, and assert the exact full command line, for example the export line with the escaped dir followed by `:$PATH"`. Extend "GOBIN on PATH prints no hint": set SHELL=/usr/bin/fish and also reject `fish_add_path`. "empty GOBIN installs into GOPATH/bin" keeps its export assertion (it runs with the empty default). Raise the `len(cases) < 18` floor to the new total.
In `modules/sunrise/sunrise_test.go` `TestBootstrapEndToEnd`:
8. Drop SHELL from the inherited environment (add `key == "SHELL"` to the skip list) and add `"SHELL=/bin/bash"` to the initial env slice, with a short comment that the hint form follows SHELL. Keep the existing `export PATH="`+gobin+`:$PATH"` assertion and also require `(and add it to ~/.bashrc):`.
Do not commit in this task. Task 2 commits code and docs together.
</action>
<verify>
<automated>cd /media/nvme/dev/golem15/summercms.io/summercms/summercms.go && set -o pipefail && shellcheck -s bash modules/sunrise/new.sh && test "$(grep -c 'shellcheck disable' modules/sunrise/new.sh)" -eq 1 && bash -n modules/sunrise/new.sh && go vet ./modules/sunrise/ && go test ./modules/sunrise/ -short -count=1 -v -run '^(TestScriptEmbedded|TestScriptShape|TestScriptTruncationRunsNothing|TestScriptBehavior|TestScriptShellcheck)$' 2>&1 | grep -E '^(--- |ok|FAIL|panic)'</automated>
</verify>
<done>shellcheck prints nothing, the file has exactly one disable directive, and go vet passes. The go test output shows `--- PASS:` for TestScriptEmbedded, TestScriptShape, TestScriptTruncationRunsNothing, TestScriptBehavior and TestScriptShellcheck (not SKIP), with no `--- FAIL`. Every new TestScriptBehavior subtest passes, and the generic-hint output is byte-identical to the pre-change text.</done>
</task>
<task type="auto">
<name>Task 2: README and installation docs, full gates, single commit</name>
<files>modules/sunrise/README.md, docs/setup/installation.md</files>
<action>
1. `modules/sunrise/README.md`:
- Overview, the sentence ending "an `export PATH=...` hint": say that when the Go install directory is not on `PATH` the script ends with a hint for the login shell named by `SHELL`: `fish_add_path <dir>` for fish (which keeps it for new sessions), an `export PATH=...` line with `~/.zshrc` or `~/.bashrc` named for zsh and bash, and the generic `export PATH=...` line with "your shell profile" for any other shell.
- Features: add one bullet for the login-shell-aware PATH hint, noting that the directory is escaped for the target shell's double quotes and `$PATH` is printed literally.
- Configuration: the intro says the script reads three environment variables. Add that it also reads `SHELL`, only to pick the PATH hint form. Keep the `SUMMERCMS_` table as it is. Follow the README rules in CLAUDE.md: no consuming-application names, and every identifier named must exist.
2. `docs/setup/installation.md`:
- Step 3 of "What the script does" (around line 33): replace "the script ends with the `export PATH=...` line to run and to add to your shell profile" with the per-shell description: the script reads your login shell from `SHELL` and ends with `fish_add_path <dir>` in fish, which keeps the directory for new sessions, or with `export PATH="<dir>:$PATH"`, to add to `~/.zshrc` in zsh, `~/.bashrc` in bash or your shell profile in any other shell.
- "Install the summer CLI" (around lines 102-106): keep the `export PATH="$(go env GOPATH)/bin:$PATH"` fence for bash and zsh, saying to add it to `~/.bashrc` or `~/.zshrc` to keep it. Then add a fish sentence and a second `sh` fence containing `fish_add_path (go env GOPATH)/bin`, noting that fish keeps it for new sessions without a profile edit. Use `sh` as the fence language (see context).
- Leave the frontmatter `description` unchanged.
3. Run the full gates (the verify command below). `go test ./... -short` can take several minutes, so run it with a generous timeout. Then confirm the gate's pinned hint still holds without editing the gate: `grep -nF 'export PATH="/root/go/bin:$PATH"' scripts/check-phase14.3.sh` still matches, and the bash branch of new.sh prints that shape (Task 1 covers this). Do not edit `scripts/check-phase14.3.sh` or `14.3-SECURITY-REVIEW.md`.
4. Optional, when network and time allow: `SUMMERCMS_BOOTSTRAP_CHECK=1 go test ./modules/sunrise/ -run TestBootstrapEndToEnd -count=1 -timeout 20m`. It is not part of the acceptance.
5. Make one commit holding exactly the five files in `files_modified`, for example `feat(sunrise): print the PATH hint for the user's login shell`, with a body naming the fish, zsh, bash and generic forms and the escaping. Add no co-author or attribution trailer (user and project rule). Do not tag, push or deploy.
</action>
<verify>
<automated>cd /media/nvme/dev/golem15/summercms.io/summercms/summercms.go && grep -qF 'fish_add_path (go env GOPATH)/bin' docs/setup/installation.md && grep -qF 'fish_add_path' modules/sunrise/README.md && grep -qF '~/.zshrc' docs/setup/installation.md && grep -qF 'export PATH="/root/go/bin:$PATH"' scripts/check-phase14.3.sh && go vet ./... && go test ./cmd/summer -run '^TestDocsTree$' -count=1 && go run ./cmd/summer docs:build --check && bash scripts/check-phase14.3.sh --forbidden && go test ./... -short -count=1</automated>
</verify>
<done>go vet ./... and go test ./... -short are green. TestDocsTree passes, `summer docs:build --check` reports no problems, and the `--forbidden` stage prints "Phase 14.3 forbidden-surface scan passed". The README and installation page describe the fish, zsh, bash and generic forms, and the manual section shows `fish_add_path (go env GOPATH)/bin`. scripts/check-phase14.3.sh is unchanged. One commit holds the five files, with no co-author trailer. Nothing is tagged, pushed or deployed.</done>
</task>
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| script output → user's interactive shell | The user pastes the printed hint line into fish, zsh or bash, so whatever the line contains runs there. |
| environment → script | `SHELL`, `GOBIN` and `GOPATH` come from the user's own environment and Go config. |
## STRIDE Threat Register
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|-----------|----------|-----------|----------|-------------|-----------------|
| T-k7s-01 | Elevation of Privilege | new.sh hint line (install dir interpolated into a pasted command) | medium | mitigate | The escaping helper backslash-escapes backslash, double quote and dollar for every shell, plus backtick for POSIX shells, and isolates `!` in single quotes for bash and zsh, so command substitution or history expansion in the dir cannot run when pasted. Pinned by the two metacharacter TestScriptBehavior cases. |
| T-k7s-02 | Tampering | `SHELL` value | low | accept | Used only as a `case` pattern subject after basename stripping. It is never printed, executed or used as a path, and an unknown value falls back to the generic hint. |
| T-k7s-03 | Denial of Service | `set -u` with SHELL unset | low | mitigate | Read via `${SHELL:-}`. Pinned by the unset-SHELL harness case. |
| T-k7s-04 | Tampering | newline inside GOBIN/GOPATH | low | accept | Pathological Go configuration that the user controls. The printed line would wrap but stays quoted, and nothing is executed by the script itself. |
| T-k7s-SC | Tampering | npm/pip/cargo installs | low | accept | This plan installs no packages and adds no Go module requirement. The `--forbidden` stage confirms go.mod gained no require. |
</threat_model>
<verification>
- `shellcheck -s bash modules/sunrise/new.sh` is clean, with exactly one disable directive (the literal `$PATH` SC2016).
- `go vet ./...` and `go test ./... -short` are green. TestScriptBehavior covers the fish, zsh, bash, empty, unset and unknown shells, both metacharacter cases and the on-PATH no-hint case.
- `go test ./cmd/summer -run TestDocsTree` and `go run ./cmd/summer docs:build --check` pass.
- `bash scripts/check-phase14.3.sh --forbidden` passes, and the gate script is unchanged.
</verification>
<success_criteria>
A fish user running the one-liner with GOBIN off PATH sees `fish_add_path "<dir>"` and is told fish keeps it. zsh and bash users see the export line with `~/.zshrc` or `~/.bashrc`. Everyone else sees today's exact text. The tests are deterministic regardless of the developer's login shell, the docs match, and the change lands as one commit with no tag, push or deploy.
</success_criteria>
<output>
Create `.planning/quick/261008-k7s-bootstrap-script-path-hint-per-shell/261008-k7s-SUMMARY.md` when done
</output>

View File

@@ -0,0 +1,76 @@
---
phase: quick-261008-k7s
plan: 01
subsystem: sunrise (bootstrap script)
tags: [bootstrap, shell, path-hint, fish, docs]
status: complete
requires: []
provides:
- login-shell-aware PATH hint in modules/sunrise/new.sh
- dq_escape double-quote escaping helper (POSIX and fish forms)
affects:
- scripts/check-phase14.3.sh acceptance grep (unchanged, still matches the bash branch)
tech-stack:
added: []
patterns:
- "Test harness pins SHELL= by default because bash fills an unset SHELL from passwd"
key-files:
created: []
modified:
- modules/sunrise/new.sh
- modules/sunrise/script_behavior_test.go
- modules/sunrise/sunrise_test.go
- modules/sunrise/README.md
- docs/setup/installation.md
decisions:
- "Hint form is picked by the basename of ${SHELL:-}: fish -> fish_add_path, zsh/bash -> export line naming ~/.zshrc / ~/.bashrc, anything else (empty, unset, unknown) -> byte-identical generic hint"
- "Install dir is escaped per character (no ${//} substitution) so bash 3.2 and 5.2 patsub_replacement behave the same; fish leaves backtick and ! literal"
metrics:
duration: ~20m
completed: 2026-10-08
actuals:
tokens: 9000
tasks: 2
commits: 1
plan_head_before: 8973da5cc1408ac0c0fdf1f1692309d189e1c67e
plan_head_after: ddf1178c513c290221ca417f487e3da29d2190a1
---
# Quick 261008-k7s Plan 01: Bootstrap script PATH hint per login shell Summary
The bootstrap script now reads the login shell from `SHELL` and ends with `fish_add_path "<dir>"` for fish, an `export PATH="<dir>:$PATH"` line naming `~/.zshrc` or `~/.bashrc` for zsh and bash, or today's exact generic hint for anything else. The directory is escaped for the target shell's double quotes.
## What was done
**Task 1 (tracer, TDD): per-shell hint in new.sh**
- RED: added the per-shell and metacharacter cases to TestScriptBehavior. The fish, zsh, bash and both metacharacter cases failed against the old script. The generic, unset and on-PATH cases passed, so the generic text is byte-identical.
- GREEN: added `dq_escape` above `main`. It is a per-character loop over `${s:i:1}` with a `case`. It always escapes `\`, `"` and `$`. For shells other than fish it also escapes the backtick and turns `!` into `"'!'"`. The hint block now does a `case` on `${SHELL:-}` stripped by `##*/`. The single SC2016 disable directive remains the only one in the file. The header comment notes that SHELL only picks the hint form.
- Harness: `SHELL=` is placed before `h.env` so a per-case value wins. The new `unsetShell` mode runs `bash -c 'unset SHELL; . "$0" "$@"'`. New helpers: `expectLacks`, `hintDir` (a `strings.NewReplacer` with the same mapping), `hint`/`exportHint`/`fishHint`, `expectHintSuffix` and `metaGobin`. The case floor went from 18 to 32.
- TestBootstrapEndToEnd drops the inherited SHELL, pins `SHELL=/bin/bash` and also requires `(and add it to ~/.bashrc):`.
- Tracer gate: the verify command re-ran green. A manual paste test confirmed that interactive bash, with history expansion on, and fish both recover the exact directory ``/tmp/go bin\x"y$HOME`id`!z`` from the printed lines. zsh is not installed on this host. Its double-quote rules match bash for these characters, and the harness covers its output.
**Task 2: docs and gates**
- sunrise README: the Overview sentence describes the per-shell hint. A new Features bullet covers the escaping and the literal `$PATH`. The Configuration intro notes that `SHELL` only picks the hint form.
- docs/setup/installation.md: step 3 describes the per-shell hint. The manual PATH section names `~/.bashrc`/`~/.zshrc` and adds a fish `sh` fence with `fish_add_path (go env GOPATH)/bin`.
- Gates: `go vet ./...`, TestDocsTree, `summer docs:build --check` ("no problems found"), `check-phase14.3.sh --forbidden` ("forbidden-surface scan passed") and `go test ./... -short -count=1` all passed. `scripts/check-phase14.3.sh` is unchanged and still contains `export PATH="/root/go/bin:$PATH"`, which matches the bash branch.
## Commits
- ddf1178: feat(sunrise): print the PATH hint for the user's login shell (all five files; no co-author trailer)
## Deviations from Plan
None. The plan was executed as written. The optional gated `SUMMERCMS_BOOTSTRAP_CHECK=1` end-to-end run (step 4 of Task 2, not part of acceptance) was not run.
## Known Stubs
None.
## Self-Check: PASSED
- FOUND: modules/sunrise/new.sh (contains fish_add_path, dq_escape; one shellcheck disable)
- FOUND: modules/sunrise/script_behavior_test.go (contains "SHELL=")
- FOUND: modules/sunrise/sunrise_test.go (contains SHELL=/bin/bash)
- FOUND: modules/sunrise/README.md (contains fish_add_path)
- FOUND: docs/setup/installation.md (contains fish_add_path (go env GOPATH)/bin)
- FOUND: commit ddf1178