diff --git a/bin/gstack-capy-install b/bin/gstack-capy-install index 43710f0c3..528c8aeee 100755 --- a/bin/gstack-capy-install +++ b/bin/gstack-capy-install @@ -35,9 +35,9 @@ # installs it on first use); --with-browser installs it now. The last lines # are the GSTACK_LAUNCH line the parent supplies to every gstack command it # launches, GSTACK_LAUNCH_PATH when the certified Bun is not first on PATH, -# GSTACK_LAUNCH_FILE (the same assignments as a sourceable mode-0600 file -# beside the verdict; callers and their children `. ` it, never re-type -# it) and the doctor's trailer (`gstack: ok ...` or `gstack: fail ...`; +# GSTACK_LAUNCH_FILE on a passing run (the same assignments as a sourceable +# mode-0600 file beside the verdict; callers and their children `. ` it, +# never re-type it) and the doctor's trailer (`gstack: ok ...` or `gstack: fail ...`; # grep that, never pipe through tail). # Exit: 0 ok, 1 a component failed, 2 usage, 3 preflight stopped before ./setup. set -u @@ -605,7 +605,7 @@ write_launch_file() { echo "export GSTACK_INSTALL_VERDICT=$(shq "$VERDICT")" } > "$tmp" && chmod 0600 "$tmp" && mv -f "$tmp" "$LAUNCH_FILE" || { rm -f "$tmp"; return 1; } } -write_launch_file || { say "could not write the launch file $LAUNCH_FILE"; LAUNCH_FILE=""; } +if [ "$RC" -eq 0 ]; then write_launch_file || { say "could not write the launch file $LAUNCH_FILE"; LAUNCH_FILE=""; }; else LAUNCH_FILE=""; fi echo "GSTACK_LAUNCH: ${LAUNCH_DIR:+PATH=$LAUNCH_DIR:\$PATH }GSTACK_SESSION_KIND=unattended GSTACK_STATE_ROOT=$GSTACK_STATE_ROOT GSTACK_EPHEMERAL=${GSTACK_EPHEMERAL:-1} GSTACK_CAPY=1 GSTACK_INSTALL_VERDICT=$VERDICT" [ -n "$LAUNCH_DIR" ] && echo "GSTACK_LAUNCH_PATH: $LAUNCH_DIR" [ -n "$LAUNCH_FILE" ] && echo "GSTACK_LAUNCH_FILE: $LAUNCH_FILE" diff --git a/capy/skill/gstack/SKILL.md b/capy/skill/gstack/SKILL.md index 02e7312d5..b67c678e5 100644 --- a/capy/skill/gstack/SKILL.md +++ b/capy/skill/gstack/SKILL.md @@ -22,7 +22,7 @@ Resolve `scripts/install.sh` relative to this SKILL.md, not the working reposito ## Launch every gstack command with the GSTACK_LAUNCH line -Every successful run, `--check` included, prints `GSTACK_LAUNCH: PATH=:$PATH GSTACK_SESSION_KIND=unattended GSTACK_STATE_ROOT= GSTACK_EPHEMERAL=1 GSTACK_CAPY=1 GSTACK_INSTALL_VERDICT=` (the `PATH=` part only when the verified Bun is not first on PATH), then `GSTACK_LAUNCH_PATH: ` for callers that do not evaluate shell syntax, then `GSTACK_LAUNCH_FILE: `: the same assignments as a sourceable script, mode 0600, under the state root beside the install verdict. Apply them with `. ` at the start of every shell that runs a gstack command (each Capy bash call is a fresh shell, and a child shell's export cannot configure the parent's later commands), or prefix the command with the assignments from the line. Never re-type them as `export $(...)`, and never run `env`, `printenv`, a bare `set` or a bare `export` in a tool fence: when the substitution comes back empty, `export $(...)` is a bare `export`, which prints every exported variable, API keys included, into the tool output (that is how GSTA-96's docs-audit child leaked three keys). Hand a child agent the file path, never the values. The PATH part is what makes gstack's `#!/usr/bin/env bun` commands run the Bun the trailer certified; the Capy image's `/usr/local/bin/bun` is 1.3.14, below gstack's minimum. +Every successful run, `--check` included, prints `GSTACK_LAUNCH: PATH=:$PATH GSTACK_SESSION_KIND=unattended GSTACK_STATE_ROOT= GSTACK_EPHEMERAL=1 GSTACK_CAPY=1 GSTACK_INSTALL_VERDICT=` (the `PATH=` part only when the verified Bun is not first on PATH), then `GSTACK_LAUNCH_PATH: ` for callers that do not evaluate shell syntax, then `GSTACK_LAUNCH_FILE: `: the same assignments as a sourceable script, mode 0600, under the state root beside the install verdict. Apply them with `. ` at the start of every shell that runs a gstack command (each Capy bash call is a fresh shell, and a child shell's export cannot configure the parent's later commands); the plain `GSTACK_LAUNCH:` line is for callers that do not evaluate shell syntax, never for re-typing. The file is written only on a passing run. Never re-type them as `export $(...)`, and never run `env`, `printenv`, a bare `set` or a bare `export` in a tool fence: when the substitution comes back empty, `export $(...)` is a bare `export`, which prints every exported variable, API keys included, into the tool output (that is how GSTA-96's docs-audit child leaked three keys). Hand a child agent the file path, never the values. The PATH part is what makes gstack's `#!/usr/bin/env bun` commands run the Bun the trailer certified; the Capy image's `/usr/local/bin/bun` is 1.3.14, below gstack's minimum. With `GSTACK_CAPY=1`, a workflow started in an unattended or spawned session checks the install verdict the last run wrote for this checkout, HEAD and workflow, and the Bun on PATH. When either fails it prints an `INSTALL: fail …` row with its fix and `GSTACK_RESULT: skill= status=refused run=-` and exits 3: stop, run the fix, start again. An ok whose pins row was `SKIP pins (planning-only; …)` does not admit a workflow that runs the repo's tests (`review`, `ship`, `qa`), and a verdict that did not check a component the workflow needs does not admit it either; run `--for ` first. diff --git a/test/launch-env-echo-ratchet.test.ts b/test/launch-env-echo-ratchet.test.ts index c74f6b159..6d189d447 100644 --- a/test/launch-env-echo-ratchet.test.ts +++ b/test/launch-env-echo-ratchet.test.ts @@ -182,6 +182,10 @@ describe('launch-env echo ratchet', () => { expect(installer).toContain('LAUNCH_FILE="${VERDICT%.json}.env"'); expect(installer).toContain('chmod 0600 "$tmp"'); for (const name of CAPY_LAUNCH_MARKERS) expect(installer, name).toMatch(new RegExp(`^\\s*echo "export ${name}=`, 'm')); + // Both directions: every variable the launch file exports is either a marker the shard sandbox + // and the test preload strip, or one of the two they keep on purpose (GSTA-99). + const exported = [...installer.matchAll(/^\s*echo "export ([A-Z_]+)=/gm)].map(m => m[1]).sort(); + expect(exported).toEqual([...CAPY_LAUNCH_MARKERS, 'GSTACK_STATE_ROOT'].sort()); expect(installer).toContain('echo "GSTACK_LAUNCH_FILE: $LAUNCH_FILE"'); for (const file of ['ship/sections/documentation.md', 'document-release/SKILL.md', 'autoplan/DRIVER.md', 'capy/skill/gstack/SKILL.md']) { const text = fs.readFileSync(path.join(ROOT, file), 'utf8');