diff --git a/docs/pregate.md b/docs/pregate.md
index 58314a53d..859ba1407 100644
--- a/docs/pregate.md
+++ b/docs/pregate.md
@@ -19,7 +19,8 @@ Three bins, all over `lib/`:
A pin file names commands. Its presence never authorizes execution: both tools
read the committed copy on `origin/` (a PR cannot smuggle a command into
-its own gate) and run commands only with `--allow-repo-commands`, which `/ship`
+its own gate; `gstack-pregate run` alone also reads a user-owned file outside
+the repository when the base has none, see [pregate v1](#pregate-v1)) and run commands only with `--allow-repo-commands`, which `/ship`
passes. Standalone, `gstack-regen check` lists the commands and exits 3
(`REPO_COMMANDS_NOT_ALLOWED`); `gstack-pregate` marks `regen` and `lanes`
`incomplete`, never `pass`. `--policy-from head` (a PR that introduces or
@@ -89,7 +90,10 @@ so, or says `(absent)` when no file is there.
It covers `gstack-pregate run` only: `sealed` and `price_file` are read from the
base branch's copy, so the user file refuses them (`PREGATE_CONFIG_INVALID`)
rather than holding a policy nothing reads. A user file that exists but cannot
-be read refuses the same way instead of falling back to defaults.
+be read, or a symlink on its path that points nowhere, refuses the same way
+instead of falling back to defaults. A base copy git cannot read (and
+`git ls-tree` cannot confirm is absent) refuses with `POLICY_SOURCE_UNAVAILABLE`
+instead of handing the run to the user file.
`gstack-pregate validate` / `init` cover the working-tree file.
## `gstack-pregate`
@@ -123,13 +127,15 @@ Two stages with separate clocks, both printed:
`TIMING_ASSERTIONS: found= escaped= files=` summary. It warns
(`PREGATE_TIMING_ASSERTION`); it does not block. See [timing](#timing).
-- `guards`: each guard in `.gstack/pregate.json` whose `when` globs match a
+- `guards`: each guard in the active policy (`.gstack/pregate.json`, or the
+ user-owned file) whose `when` globs match a
touched file (renames included) runs its `command` in its own process, under
the verified launch Bun, with its own `timeout_s` (default `--timeout`). A
non-zero exit fails pregate naming the guard and its `fix`
(`PREGATE_GUARD_FAILED`); a timeout is `incomplete`; without
`--allow-repo-commands` the selected guards are listed and the check is
- `incomplete`. Guards are read from the base branch's copy and cannot be
+ `incomplete`. Guards are read from the base branch's copy (or the
+ user-owned file when the base carries none) and cannot be
downgraded to warn. See [guards](#guards).
**Stage 2, test execution:**
@@ -150,7 +156,7 @@ Two stages with separate clocks, both printed:
`set -o pipefail`, judged on the runner's summary line (zero tests ran is a
failure, a `| tail`-hidden exit is still an exit); a platform lane this
machine cannot run becomes a `requires-remote` obligation naming
- `workflow.yml/job`. `--explain` prints each test's provenance and every
+ `workflow.yml/job`, unless `local` names that lane. `--explain` prints each test's provenance and every
uncertain mapping (a planner-computed matrix without a `selection` manifest
selects conservatively). Test files matching `serial_patterns` (default
`**/*.serial.test.*`) run one process per file after the unit files. The
@@ -163,7 +169,7 @@ Two stages with separate clocks, both printed:
runner process (the unit-file group, each serial file, each guard without
its own `timeout_s`), not their total; the stall timeout catches a hung or
silent one sooner.
-- Under 8 cores, a base-branch `remote_gate` or a remote `test_backend`
+- Under 8 cores, a `remote_gate` in the active policy or a remote `test_backend`
(`gstack-config set test_backend ubicloud`) records the local test tier as a
`requires-remote` obligation instead of running it: see [remote gate](#remote-gate).
@@ -229,8 +235,8 @@ misses a descendant that was reparented or started detached.
## Small machines and the remote gate
-Under 8 cores (`os.availableParallelism()`), when the base branch's
-`.gstack/pregate.json` declares `remote_gate` or `gstack-config get
+Under 8 cores (`os.availableParallelism()`), when the active policy (the
+base branch's `.gstack/pregate.json`, or the user-owned file) declares `remote_gate` or `gstack-config get
test_backend` is not `local`, the lanes check runs nothing locally: it records
the selected local files as a `requires-remote` obligation with the command to
run, `pregate.json/remote_gate` (the declared command) or
diff --git a/docs/state-root.md b/docs/state-root.md
index 5cd662ee5..45c246ec6 100644
--- a/docs/state-root.md
+++ b/docs/state-root.md
@@ -104,6 +104,9 @@ lives in `projects//`. `bin/gstack-slug` picks the slug from the git
and the ssh, https and ssh-with-port spellings of one repository share one.
- Without a remote, the project root's directory name is used.
+A bucket can also hold a user-owned `pregate.json` policy, for a repo that keeps
+`.gstack/` out of git (see [pregate v1](pregate.md#pregate-v1)).
+
The first slug gstack resolves in a directory sticks (it is cached under
`slug-cache/`), so a project keeps its bucket when it adds a remote later.
diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md
index b47ead752..aa1652f35 100644
--- a/docs/troubleshooting.md
+++ b/docs/troubleshooting.md
@@ -2354,7 +2354,10 @@ file is described in [docs/ship-policy.md](ship-policy.md); the queue flow in
**Meaning.** The policy is read from the committed copy on `origin/`,
and that ref does not resolve here (a shallow or detached gate checkout, or a
fetch that never happened). The tool exits 3 and never falls back to the
-working-tree file silently.
+working-tree file silently. For `gstack-pregate` the same refusal covers a base
+`.gstack/pregate.json` git cannot read (a timeout, an oversized blob, a missing
+or corrupt object) that `git ls-tree` cannot confirm is absent; it never falls
+through to the user-owned file.
**Fix.** `git fetch origin --depth=1`, then rerun. To run against the
branch's own file deliberately, pass `--policy-from head --allow-repo-commands`;
@@ -2812,7 +2815,7 @@ repository's lockfile), then rerun `gstack-regen check` or the pre-gate.
that escapes the repository, a runner without `{files}`, or `secrets` in `warn`.
The user-owned `/projects//pregate.json` also refuses when it
sets `sealed` or `price_file` (both are read only from the base branch's copy)
-or exists but cannot be read.
+or exists but cannot be read (a dangling symlink on its path counts).
**Fix.** `gstack-pregate validate`. `secrets` and required-lane obligations
cannot be downgraded; every other check id in `warn` is honoured only from the
@@ -2859,7 +2862,7 @@ test. An unmapped source or code file still fails. A file under a test
directory is not this empty lane.
**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`.
+for files with no behaviour) under `dependencies` in `.gstack/pregate.json` (or the user-owned file the message names).
Other non-Markdown documentation goes in `empty_lanes` on the base branch.
`--explain` prints every mapping and why. For a file with no lane it also prints the import
specifiers in test files that resolved to no repo file (for example a
@@ -2892,7 +2895,7 @@ keep the tree still, then rerun.
**Meaning.** A test command the lanes check (or a guard, or the `--remote`
gate) launched wrote nothing to its log for the stall timeout (default 300 s,
-`stall_timeout_s` in the base branch's `.gstack/pregate.json`, or
+`stall_timeout_s` in the base branch's `.gstack/pregate.json` or the user-owned file, or
`--stall-timeout`). pregate killed its process group and names the last test
file started. The check is incomplete, never pass.
@@ -3149,8 +3152,9 @@ next `./setup` prints no CRLF warning.
### `PREGATE_GUARD_FAILED`
-**Meaning.** A guard from the base branch's `.gstack/pregate.json` (a repo
-command such as a serial-test manifest check or a CLI golden test) matched a
+**Meaning.** A guard (a repo command such as a serial-test manifest check or a
+CLI golden test) from the base branch's `.gstack/pregate.json`, or from the
+user-owned file when the base carries none, matched a
file the branch touched and exited non-zero. Guards cover what the import graph
cannot see: manifests, subprocess goldens, registries. They cannot be
downgraded to warn.