diff --git a/CHANGELOG.md b/CHANGELOG.md index 652c7d273..ec6fb1db2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## [Unreleased] + +**`gstack-pregate sealed` no longer prints a line that reads like a push confirmation.** On gstack-evals#29 the lane read `pushed aeb2413` in the `SEALED:` line as the push landing, while the remote stayed at 9036ab1 (#3565). `sealed` never pushes: it checks the commits a push would publish. `pushed ` was its label for the commit whose `sealed` policy it read beside the base branch's. + +### Fixed +- **`lib/sealed-guard.ts`**: that label now reads `commit ` (`policy=origin/main + commit aeb2413f0ca1`), in the `SEALED:` line, in the managed pre-push hook's messages and in the unreadable-policy error. +- **`gstack-pregate --help`** and **docs/pregate.md#sealed** say the command never pushes and that `git ls-remote ` confirms a push. + +### Tests +- `test/sealed-guard.test.ts`: `gstack-pregate sealed` names the checked commit, prints no `pushed`, and leaves the remote's refs exactly as they were. It failed before the fix on `policy=origin/main + pushed `. + ## [1.91.299.0] - 2026-10-28 **The Captain brief now says what `rec` does when a gate carries earlier decisions.** At a gate after the owner already answered some items, such as a round-cap gate, `gstack-owner-brief render --captain` listed each item's recommendation ("keep the plan as written", `rec resolves to d2a`) while `gstack-gate render` and `gstack-autoplan answer --reply rec` keep the carried answer (`d2b`). A Captain relaying the brief told the owner the opposite of what their one-word reply would record (#3546). The brief now takes its choices from the same resolver `gstack-gate render` uses. diff --git a/bin/gstack-pregate b/bin/gstack-pregate index 8613150ed..0a6700a7f 100755 --- a/bin/gstack-pregate +++ b/bin/gstack-pregate @@ -57,7 +57,8 @@ const HELP = `gstack-pregate — cheap checks before anything paid or remote (do validate | init the working-tree ${PREGATE_CONFIG_PATH} sealed [--range ] [--remote ] [--base ] the sealed-output guard (docs/pregate.md#sealed) over the commits lacks, or the range: - run it before any push or PR creation in a repo with a \`sealed\` config; exit 1 refuses (SEALED_OUTPUT_REFUSED) + run it before any push or PR creation in a repo with a \`sealed\` config; exit 1 refuses (SEALED_OUTPUT_REFUSED); + it never pushes: policy= + commit names where the policy was read regate --against --bundle [--require ] [--pr ] [--predecessor-pr | --predecessor-ref ] a fresh tree receipt, each lane's reuse verdict, migration collisions (base branch and open PRs), then REGATE: needed|not-needed (docs/pregate.md#regate); \`gstack-pregate --against \` is the same verb; exit 1 = needed diff --git a/docs/pregate.md b/docs/pregate.md index a42b37831..735a73c81 100644 --- a/docs/pregate.md +++ b/docs/pregate.md @@ -788,7 +788,10 @@ gstack-pregate sealed [--range ] [--remote origin] [--base ] [--js (`/home//`, `/Users//`, `C:\Users\\`) check each commit's added lines. Findings print rule, `file[:line]` and commit, never the text. - **Policy.** The union of the destination base branch's `sealed` and the - pushed commit's: a push can tighten it, never relax it. A malformed or + pushed commit's: a push can tighten it, never relax it. The `SEALED:` line + names both as `policy=/ + commit `. `gstack-pregate sealed` + only checks and never pushes, so confirm a push with + `git ls-remote `. A malformed or unreadable policy refuses. Waivers come only from the base branch's `.gstack/sealed-allow.tsv` (`rulepathshareason`) and are listed on every push. diff --git a/lib/sealed-guard.ts b/lib/sealed-guard.ts index e10c08a6a..8b72a3d7b 100644 --- a/lib/sealed-guard.ts +++ b/lib/sealed-guard.ts @@ -233,7 +233,7 @@ export function evaluateSealed(o: EvaluateOptions): SealedReport { let policy: SealedPolicy | null = base.policy; const labels = base.policy ? [base.label] : []; for (const t of o.targets) { - const pushed = sealedPolicyAt(o.repoRoot, t.pushedSha, `pushed ${t.pushedSha.slice(0, 12)}`); + const pushed = sealedPolicyAt(o.repoRoot, t.pushedSha, `commit ${t.pushedSha.slice(0, 12)}`); if ('error' in pushed) return { ...blank, status: 'error', error: `the sealed policy in ${pushed.label} is unreadable: ${pushed.error}` }; if (pushed.policy) labels.push(pushed.label); policy = unionPolicies(policy, pushed.policy); diff --git a/test/sealed-guard.test.ts b/test/sealed-guard.test.ts index 1fff7c24f..6a0c249bb 100644 --- a/test/sealed-guard.test.ts +++ b/test/sealed-guard.test.ts @@ -221,3 +221,17 @@ describe('visibility (cache under the state root; unknown counts as public)', () } finally { fs.rmSync(state, { recursive: true, force: true }); } }); }); + +describe('gstack-pregate sealed is a check, not a push (#3565)', () => { + test('the SEALED line names the checked commit, never says pushed, and the remote ref does not move', () => { + const s = scratch({ '.gstack/pregate.json': policy(SEALED) }); + const head = commit(s, { 'notes.md': 'ok\n' }); + const before = sh(s.work, s.env, ['ls-remote', 'origin']); + const r = spawnSync('bun', [PREGATE, 'sealed'], { cwd: s.work, encoding: 'utf8', timeout: 60_000, env: { ...process.env, ...s.env } }); + expect(r.status).toBe(0); + expect(r.stdout).toContain(`policy=origin/main + commit ${head.slice(0, 12)}`); + expect(`${r.stdout}${r.stderr}`).not.toMatch(/\bpushed\b/); + expect(sh(s.work, s.env, ['ls-remote', 'origin'])).toBe(before); + expect(before).not.toContain('refs/heads/results'); + }); +});