stamp: 1.91.245.0 (#3460 slot 5, merge main d62eb914)

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 12:16:28 +00:00
commit a01bb21430
133 files changed
+5672 -994

No files matched your search

+4 -1
View File
@@ -111,7 +111,10 @@ determined leaker (a CHANGELOG line that does would fail a hostile screenshotter
- **Engine + taxonomy:** `lib/redact-patterns.ts` (the single source of truth —
3 tiers; HIGH = genuinely-secret credentials that block, MEDIUM = PII/legal/
internal + high-FP credential shapes that confirm via AskUserQuestion, LOW =
FYI) and `lib/redact-engine.ts` (pure `scan()` + `applyRedactions()`).
FYI) and `lib/redact-engine.ts` (`scan()` + `applyRedactions()`; no I/O,
and `scan()`'s one ambient input is the credential-named variables of
`process.env`, read by value for the `env.value` HIGH finding unless
`ScanOptions.processEnv` names another environment).
Calibration matters: a gate that cries wolf gets ignored, so context-variable
shapes (Stripe `pk_live_`, Google `AIza`, JWT, env `*_KEY=`) sit at MEDIUM.
- **CLI:** `bin/gstack-redact` (exit 0 clean / 2 MEDIUM / 3 HIGH; `--json`,
+34 -5
View File
@@ -269,8 +269,8 @@ through the `bun` shim beside it.
- `GSTACK_BUN_SHIM=0` at install time writes no shim.
- Removing it: delete `~/.local/bin/bun` and `~/.local/bin/bunx`. gstack
replaces only a missing file or a file carrying the marker; a foreign file
or a dangling link there is left untouched with a note, and the
`GSTACK_LAUNCH` prefix stays the way to reach the certified Bun.
or a dangling link there is left untouched with a note, and sourcing the
launch file (`. <GSTACK_LAUNCH_FILE>`) stays the way to reach the certified Bun.
- The doctor's `runtime` row notes the shim; the resolver never treats a
shim-marked file as a Bun candidate.
@@ -345,8 +345,37 @@ Every successful run, `--check` included, prints before the trailer:
```
GSTACK_LAUNCH: PATH=<dir>:$PATH GSTACK_SESSION_KIND=unattended GSTACK_STATE_ROOT=<path> GSTACK_EPHEMERAL=1 GSTACK_CAPY=1 GSTACK_INSTALL_VERDICT=<file>
GSTACK_LAUNCH_PATH: <dir>
GSTACK_LAUNCH_FILE: <verdict file without .json>.env
```
`GSTACK_LAUNCH_FILE` names the same assignments as a sourceable script (mode
0600, beside the verdict under the state root; the PATH prepend is idempotent).
The file is part of the ok verdict: a passing run that cannot write it exits 1
with `gstack: fail launch-file — could not write the launch file <path>; fix:
make <dir> writable, or set GSTACK_STATE_ROOT to a writable directory
(LAUNCH_FILE_UNWRITABLE)`, and a failing run prints no `GSTACK_LAUNCH_FILE` line
and removes the file an earlier passing run left beside the verdict it rewrites
(a profile check that keeps a fuller ok verdict leaves that verdict's file).
`. <that file>` at the start of a shell applies them without ever printing them,
which is the only way to hand them to a child agent: pass the path, never the
values. None of the assignments is a secret, but during GSTA-96's unattended
/ship a docs-audit child re-typed them as `export $(...)`; the substitution came
back empty, the bare `export` printed the whole environment, and three API key
values landed in its tool output. Never `export $(...)`, `env`, `printenv`, a
bare `set` or a bare `export` in a tool fence (`test/launch-env-echo-ratchet.test.ts`
keeps them out of every skill fence and shell helper), and the redaction
engine now refuses the VALUE of any `*_KEY`, `*_TOKEN`, `*_SECRET`,
`*_PASSWORD`, `*_PASSWD`, `*_PASS`, `*_PAT`, `*_APIKEY` or `*_PWD` variable
in the writer's environment (any case; names carrying a `PUBLIC` word are
framework-public and exempt, gstack's own `GSTACK_BRAIN_LOCK_TOKEN` is not a
secret) (`env.value`, HIGH), so a leaked value cannot reach a PR body, receipt,
report or Drive file. There is no path exemption: a path-valued `SSL_KEY` that
must appear in a text goes through `bin/gstack-redact --allowlist` or the
pre-push hook's committed `.gstack-redact-allowlist`; the other writers
(`gstack-post`, receipts, reports) take no allowlist, so for one of those
commands set the variable empty for that command alone
(`SSL_KEY= gstack-post …`): an empty value is below the length floor.
`PATH=<dir>:$PATH` and `GSTACK_LAUNCH_PATH` appear only when the certified Bun,
or the project's resolved Node, is not first on PATH (older Capy images ship
`/usr/local/bin/bun` 1.3.14; 57 gstack commands start with `#!/usr/bin/env bun`).
@@ -358,8 +387,8 @@ directory holding only `bun`, `bunx` and, when needed, `node`, `npm`, `npx` and
never puts that directory's other tools (the image's node) in front of PATH.
The pregate's lanes launch through the same directory. A child shell's `export` cannot
configure the parent's later commands, so the parent supplies these
assignments to every gstack command it launches (prefix the command, or
export them once in the shell that runs it). A fresh shell without them keeps
assignments to every gstack command it launches by sourcing the launch file
(`. <GSTACK_LAUNCH_FILE>`) at the start of the shell that runs it. A fresh shell without them keeps
reporting the Bun it would really run: `FAIL runtime — bun 1.3.14 at
/usr/local/bin/bun ...; verified bun 1.4.2 found at ~/.bun/bin (not on PATH);
fix: export PATH="$HOME/.bun/bin:$PATH" or prefix commands with the
@@ -434,6 +463,6 @@ Wrapper 2 only adds optional exports (the trailer's `latest:` note and the
| Exit | Meaning |
|-----:|---------|
| 0 | `gstack: ok` — every required component passed |
| 1 | `gstack: fail <ids>` — a required component failed; each row has a fix |
| 1 | `gstack: fail <ids>` — a required component failed; each row has a fix. Also `gstack: fail launch-file …` when a passing run could not write its launch file ([LAUNCH_FILE_UNWRITABLE](troubleshooting.md#capy-install-launch-file-unwritable)) |
| 2 | usage error |
| 3 | preflight stopped before `./setup` (`INSTALL_PREFLIGHT_FAILED`, `RUNTIME_PIN_CONFLICT`); nothing was installed |
+24 -8
View File
@@ -178,8 +178,11 @@ draft are cut: the artifact path is tool-owned, a child's self-reported
path list is both a pathspec-injection surface and a self-report, and the
only history need is calibration, which `--commit <sha>` covers by scanning
one commit's first-parent delta without the working tree.) Positional
base defaults the way `gstack-diff-scope` does (`origin/HEAD` → `main` →
`master`); an already-qualified ref is used as given. stdout: one
base defaults the way `gstack-diff-scope` does (the remote's `HEAD` target,
origin first, then the other remotes → `main` → `master`); an
already-qualified ref is used as given, and a branch name with no local
branch resolves through its remote-tracking twin (`origin/main` for `main`:
the merge-base is the same commit), noted once on stderr. stdout: one
`GATE_SUMMARY: listed=N read=N tagged={RH-1:2,RH-15:1} unmatched=N
coverage=<patterns> languages_unlisted=<…> candidate=<sha+wtree>` line, then
the read-level listing (bounded; the full listing goes to `--out`).
@@ -207,9 +210,10 @@ hunks and 200 KB; `GATE_SUMMARY` reports `eligible`, `inspected`,
like UNAVAILABLE, and never as `none detected`.
**Exit contract.** 0 with or without hunks; 2 only when the base is
unresolvable (`GATE_ERROR=no_base ref=<ref>` with the concrete recovery:
`git fetch <remote> <branch>` from the detected remote, or "deepen the
shallow history") or the tool itself failed. Unmatched paths are reported
unresolvable locally and as `<remote>/<ref>` (`GATE_ERROR=no_base ref=<ref>`
with the concrete recovery: `git fetch <remote> +<branch>:refs/remotes/<remote>/<branch>`
into the remote-tracking ref, `--unshallow` on a shallow clone, or a plain
fetch/deepen for a sha or relative ref) or the tool itself failed. Unmatched paths are reported
in `GATE_SUMMARY: unmatched=N` and listed, never an exit 2. The template
block that calls it branches on the exit explicitly, never
`2>/dev/null || true`:
@@ -350,9 +354,21 @@ a test count natively.
saved log is capped at 2 MB and would truncate the summary) and parses
it into `tests_ran` for the supported runners only: bun (reusing
`shard-engine`'s classifier; executed = pass + fail, so an all-skip run is
ZERO-RUN), jest/vitest, pytest, rspec, and Go's zero-only case (every
package `[no test files]`); the last summary per runner wins. Everything
else is `unknown`. `check` reports ZERO-RUN as STALE-equivalent and
ZERO-RUN), jest/vitest, pytest, rspec, Go's zero-only case (every
package `[no test files]`), gstack's shard runner (the sum of its per-shard
`[test:free] PASS — N tests` epilogues, or a files-from run's
`FILES-FROM RESULT: … tests=N`), and a `gstack-pregate --remote` lane (its
`PREGATE_REMOTE: tree=… tests=N failed=M` row or the obligation's
`cleared by remote-gate tree=… tests=N` receipt line); when the shard runner
spoke its verdict stands for the whole tail (the tail is stdout then stderr,
so position cannot order bun's stderr counts against the runner's stdout
epilogues), and any summary when it did not speak follows the
last-summary-wins rule. Everything else is `unknown`. The shard runner and the
remote gate print only bun's terminal totals, which count skipped tests, so
for those two `tests_ran` is ran-with-skips rather than executed: the shard
runner refuses a shard whose bun summary counts fewer files than planned, but
a shard whose files all loaded and executed nothing still passes with that
total. `check` reports ZERO-RUN as STALE-equivalent and
unknown as non-blocking. The TRANSPARENCY INVARIANT holds: `run`'s exit is
always the child's; detection lives in the receipt and in `check`.
- After Step 7's test-generation child returns and after Step 9.4's parent
+30
View File
@@ -194,6 +194,36 @@ Two stages with separate clocks, both printed:
issue templates, CODEOWNERS and dependabot config). Stylesheets (they import
each other), any other dotfile (`.rspec`, `.husky/` hooks, `.env*`, version
pins), CI workflows and actions, and `.gstack/` still need a lane.
A release manifest the branch only re-stamped is waived as
version-stamp-only (`version-stamp-only (no lane needed): …`): the file's
diff against the merge base changes nothing but its version values. Three
file kinds qualify, each by its own exact rule (lib/pregate/stamp-only.ts).
A JSON file (`*.json`) whose two sides parse to objects of the same shape
(the same own keys in the same order in every object, the same length in
every array, the same type at every leaf), whose only differing leaf is the
document's top-level `version` with a bare version on both sides (`1.2.3`,
`v1.2.3.4`, `1.2.3-rc.1`; never a range), and whose text is line-for-line
identical once that line has its old quoted version replaced by the new
one. A plist whose differing lines are all `<string>` versions under
`<key>CFBundleShortVersionString</key>` or `<key>CFBundleVersion</key>`. A
`VERSION` or `VERSION.txt` file (any case) holding one bare version, with at
most a trailing LF, on both sides. Texts are compared byte for byte outside
the stamped value, so a line-ending change or a BOM is content. Nothing
nested in JSON qualifies, so a dependency pin or range, a lockfile entry, a
catalog or a package named `version` is never a stamp; nor is a changed
description, a new, removed or reordered key, a floor such as
`minHostVersion`, `schemaVersion` or `MinimumOSVersion`, a reflow, a change
the parser would erase (a big integer, a duplicate key), a file that is not
valid UTF-8 or is over 1 MiB, a mode change or a symlink on either side, an
added, deleted or renamed manifest, or any other file kind: each keeps its
`PREGATE_NO_LANE`. The rule runs only after the import graph, the
declarations, the regen registry and both empty lanes found nothing, so a
manifest a test reads still selects that test, and never under a test,
fixture, golden, snapshot, `.gstack/` or CI-workflow tree, whose JSON a test
may compare against by a path the graph cannot see.
`--explain` prints the verdict per file (`version-stamp-only against
<merge-base> (version 1.0.0 → 1.0.1)` or `not version-stamp-only
(<reason>)`).
In a Django project (a manage.py or code importing django, plus Python
code, not a string or comment, that assigns `INSTALLED_APPS` or
`ROOT_URLCONF`, or a config file names `DJANGO_SETTINGS_MODULE`), the files
+58
View File
@@ -0,0 +1,58 @@
# Proactive skill suggestions
gstack notices what stage you are in (a new idea, a diff to review, "does this
work?", time to land it) and can invoke the matching skill without you typing
its slash command. The `proactive` config key says how far that goes.
| Value | What auto-invokes | Who it is for |
|-------|-------------------|---------------|
| `core` (default) | `/office-hours`, `/review`, `/qa` and `/ship` only. Every other skill waits for you to type it, unless a skill you ran hands off to it (`/office-hours` offering the next review). | Most people, and anyone running gstack beside another skill pack: the four core skills are the ones users praise, and the others no longer compete with another pack's niche skills. |
| `all` | Any skill whose purpose matches your request. | You use gstack on its own and want the whole sprint suggested. |
| `off` | Nothing. gstack never suggests or auto-invokes a skill, not even by asking "want me to run /X?". | You type every command yourself. |
`true` and `false` still work as the older spellings of `all` and `off`, so a
config written before the `core` default keeps its behavior; `gstack-skill-start`
reports them to the skill as `all` and `off`. A hand edit may be quoted, upper-case
or commented (`proactive: "OFF" # mine`), and the YAML yes/no family (`yes`, `on`,
`1`; `no`, `none`, `0`, `disabled`, `never`) reads as `all` or `off`, in
`gstack-config set` as well, so a typed opt-out never fails open.
```bash
gstack-config get proactive # core, all, off (or a legacy true/false)
gstack-config set proactive all # suggest every matching skill
gstack-config set proactive off # suggest nothing
gstack-config set proactive core # back to the default
```
A skill you name is always invoked, whatever the setting, and so is a skill that
a running skill invokes itself (`/ship` dispatching `/document-release`). The
setting governs unasked suggestions, and an in-skill *offer* to run another skill
is one: under `off`, `/office-hours` recommends the next review in one line
instead of offering to launch it, and the one-time first-run
tips stay quiet. The change takes effect on the next skill run; no `./setup` is
needed, because every skill reads `PROACTIVE` from `gstack-skill-start` at the
start of each run and the router (`/gstack`) applies it to its routing rules. A
value gstack does not know (a hand-edited `proactive: maybe`) reads as `core`.
Two surfaces follow the setting only in part. The first is the `## Skill routing`
section gstack offers to append to your project's CLAUDE.md. Under `off` the offer is
not made, because those rules are the auto-invocation you turned off (a section
already in CLAUDE.md stays; delete it yourself). Under `core` the offered
section still lists every routing rule and tells the host to invoke any
matching skill. If you accepted it before the `core` default, trim that section
to the four core rules (or delete it and let gstack offer it again); making the
offered section follow `core` is tracked in #3446.
The second is the full skill descriptions, which a `--catalog-mode=full` build
and the non-Claude hosts keep. Non-core skills still say "Proactively suggest
when ..." there, and that text does not change with the setting, so a host
that picks skills from those descriptions can still reach them under `core`.
The default Claude build uses one-line descriptions without that sentence.
#3446 tracks that too.
gstack asks once, after the telemetry question, which of the three you want;
if you stored `off` before that, it takes the answer as given and does not ask.
"Say stop suggesting" in a session runs `gstack-config set proactive off` for you.
If collisions with other skill packs keep coming up under `core`, the fallback
Garry approved on 2026-10-25 is `off` by default.
+4
View File
@@ -1236,6 +1236,10 @@ Outside-review routing follows the harness, independently of the configured mode
`/claude` was renamed to `/claude-code` without an alias. Run `./setup --host <name>` to migrate managed installations, including installations sharing that checkout. Setup retains a working old installation when replacement generation or installation fails.
## Proactive skill suggestions
gstack can invoke a skill for you when your request matches one. By default (`proactive: core`) only `/office-hours`, `/review`, `/qa` and `/ship` auto-invoke; `all` frees every skill and `off` suggests nothing. See [docs/proactive.md](proactive.md).
## Safety & Guardrails
Four skills that add safety rails to any Claude Code session. They work via Claude Code's PreToolUse hooks — transparent, session-scoped, no configuration required.
+7 -3
View File
@@ -166,14 +166,18 @@ option and never writes there.
A child shell's `export` cannot configure the parent's later commands
(`lib/state-root.ts` reads each process's own environment), so the installer
ends with one machine-readable line the parent supplies to every gstack
command it launches:
command it launches, and the same assignments as a sourceable file:
```
GSTACK_LAUNCH: GSTACK_SESSION_KIND=unattended GSTACK_STATE_ROOT=<path> GSTACK_EPHEMERAL=1
GSTACK_LAUNCH_FILE: <verdict file without .json>.env
```
Prefix each command with those assignments (or export them once in the shell
that runs them). See [docs/capy-install.md](capy-install.md).
Apply them with `. <that file>` at the start of the shell that runs each
command; never re-type them as `export $(...)`, and never run `env`, `printenv`,
a bare `set` or a bare `export` in a tool fence (a bare `export` prints every
variable, credentials included). See
[docs/capy-install.md](capy-install.md#the-launch-line).
## Uninstall
+48 -2
View File
@@ -1470,6 +1470,20 @@ or nvm), then re-run `bin/gstack-capy-install`. A checksum mismatch means the
upstream installer changed: update `GSTACK_BUN_INSTALL_SHA` only after
reviewing the new script.
<a id="capy-install-launch-file-unwritable"></a>
### `gstack: fail launch-file — could not write the launch file <path>; fix: ... (LAUNCH_FILE_UNWRITABLE)`
**Meaning.** The doctor passed, but `bin/gstack-capy-install` could not write
the launch file (`GSTACK_LAUNCH_FILE`, the sourceable mode-0600 copy of the
`GSTACK_LAUNCH` assignments beside the install verdict). The file is part of the
ok verdict: without it a caller would be left with only the line and the
re-typing path that leaked an environment in GSTA-96, so the run fails instead.
The directory under the state root is unwritable, or a temp file could not be
moved into place.
**Fix.** Make the verdict directory writable, or set `GSTACK_STATE_ROOT` to a
writable directory, then re-run `bin/gstack-capy-install`.
<a id="upgrade-refused"></a>
### `gstack-upgrade: <cause>; nothing was changed; fix: <command> (UPGRADE_REFUSED)`
@@ -2244,7 +2258,11 @@ entry that no longer applies; rotate a credential that is real.
<a id="redact-version-as-ip"></a>
### `pii.ip_public` MEDIUM on a four-part version number
**Meaning.** The line does not declare the value as a version.
**Meaning.** The line does not declare the value as a version. When the
text goes through `gstack-post` from inside the repository, four-part versions
equal to the root `VERSION` or to a `## [x.y.z.w]` / `## vx.y.z.w` CHANGELOG.md heading are
exempt wherever they appear, so there the finding means the quad is not a
release of this repository (or the post ran outside the checkout).
**Fix.** Write it as `version: 1.2.3.4`, `"version": "..."` or `v1.2.3.4`.
MEDIUM findings do not block pushes.
@@ -2620,8 +2638,22 @@ read the receipt again.
**Meaning.** Two sections share one version heading, usually a stamp that
re-headed `[Unreleased]` beside an entry that already carried the version.
In `gstack-restamp` only duplicates the PR introduces count: the headings are
compared against the base branch's CHANGELOG as read at `origin/<base>` and at
the commit the branch's own lines are counted from (its fork point, or the
predecessor's head in a gate-ahead stamp); the larger count per heading wins,
so a version the base already heads twice (released history the PR inherited
and must keep) passes as long as the PR adds no further copy of it. When a
reading is unavailable (the PR adds the CHANGELOG itself, or the file moved),
the stage detail and the refusal name it: with no reading left every duplicate
counts, otherwise inherited headings are counted at the readable commit only. Two paths keep the strict rule: the section being re-headed itself
(`[Unreleased]`, or the branch's earlier stamp) may exist only once, and
`gstack-version-bump write --changelog` (the claim-at-open bump) compares the
file with itself.
**Fix.** Merge them into one entry under the new version.
**Fix.** Merge the duplicate sections the PR introduced into one entry under
the new version. A duplicate that is on the base branch itself is not this
PR's to fix in a restamp; leave it and the stamp proceeds.
<a id="changelog-top-mismatch"></a>
### `CHANGELOG_TOP_MISMATCH`
@@ -2999,6 +3031,20 @@ test. An unmapped source or code file still fails. A file under a test
directory is not this empty lane. Images, fonts and media, and
allowlisted tool config no test names, are the assets-and-tool-config empty lane,
not this error.
A release manifest the branch only re-stamped is not this error either: when a
touched `*.json`, `*.plist`, `VERSION` or `VERSION.txt` file differs from the
merge base in nothing but its version values, `lanes` waives it as
version-stamp-only and prints `version-stamp-only (no lane needed): <files>`.
The exact per-kind rule (the document's top-level `version` in JSON of the
same shape and text, the two bundle version keys in a plist, one bare version
in a VERSION file) is in [docs/pregate.md](pregate.md#gstack-pregate). A
restamp PR on a base whose policy declares no lane for its manifests (gbrain's
`release:restamp` and its plugin manifests) therefore passes without a
policy change. A manifest whose description, keys, key order, nested
versions, line endings or any non-version byte changed, one under a test,
fixture, golden, `.gstack/` or workflow tree, or one added, deleted, renamed
or re-moded, is still this error; `--explain` prints `not version-stamp-only (<reason>)`
for it.
**Fix.** Add a test that names the file, or declare its lane (an explicit `[]`
for files with no behaviour) under `dependencies` in `.gstack/pregate.json` (or the user-owned file the message names).