fix(capy-install): the launch file is written only on a passing run; the ratchet checks the marker list both ways

A gstack: fail run no longer writes or announces a sourceable file whose PATH
prepend points at an unverified Bun. The ratchet also asserts every variable
the launch file exports is a CAPY_LAUNCH_MARKERS entry or GSTACK_STATE_ROOT,
so a sixth export cannot reach fixture children unstripped. The Capy skill's
prose no longer offers re-typing the assignments as an alternative to sourcing.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
garrytanandcapy-ai[bot] committed 2026-10-26 04:56:16 +00:00
1 parent 7765db84dc
commit 6aab86b2f7
3 files changed
+9 -5

No files matched your search

+4 -4
View File
@@ -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 `. <file>` 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 `. <file>` 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"
+1 -1
View File
@@ -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=<dir>:$PATH GSTACK_SESSION_KIND=unattended GSTACK_STATE_ROOT=<path> GSTACK_EPHEMERAL=1 GSTACK_CAPY=1 GSTACK_INSTALL_VERDICT=<file>` (the `PATH=` part only when the verified Bun is not first on PATH), then `GSTACK_LAUNCH_PATH: <dir>` for callers that do not evaluate shell syntax, then `GSTACK_LAUNCH_FILE: <file>`: the same assignments as a sourceable script, mode 0600, under the state root beside the install verdict. Apply them with `. <that file>` 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=<dir>:$PATH GSTACK_SESSION_KIND=unattended GSTACK_STATE_ROOT=<path> GSTACK_EPHEMERAL=1 GSTACK_CAPY=1 GSTACK_INSTALL_VERDICT=<file>` (the `PATH=` part only when the verified Bun is not first on PATH), then `GSTACK_LAUNCH_PATH: <dir>` for callers that do not evaluate shell syntax, then `GSTACK_LAUNCH_FILE: <file>`: the same assignments as a sourceable script, mode 0600, under the state root beside the install verdict. Apply them with `. <that file>` 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=<s> 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 <that skill>` first.
+4
View File
@@ -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');