diff --git a/CHANGELOG.md b/CHANGELOG.md index f08e5adb4..f6e40735e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## [1.91.319.0] - 2026-10-29 + +**`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.318.0] - 2026-10-29 **/office-hours now has eval tasks for the shared skill eval, and /write-skill's diet locks its tasks before a paid run.** gstack-evals' Lane A will re-check the office-hours diet from #3525. To do that it needs office-hours' tasks in gstack's contract format, a sidecar for what Lane A adds, and a diet step that commits the lock a paid run requires. #3525 never committed the task set GSTA-134 ran, so these tasks rebuild it from the pilot's written summary. diff --git a/VERSION b/VERSION index 5cbe83882..c6d249a3f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.91.318.0 +1.91.319.0 diff --git a/agents-digest/gstack-AGENTS.md b/agents-digest/gstack-AGENTS.md index 28a0e1743..3a2793d5c 100644 --- a/agents-digest/gstack-AGENTS.md +++ b/agents-digest/gstack-AGENTS.md @@ -1,4 +1,4 @@ -# gstack digest v1.91.318.0 — regenerate/re-copy after upgrading gstack +# gstack digest v1.91.319.0 — regenerate/re-copy after upgrading gstack Behavioral rules from gstack (https://github.com/garrytan/gstack), compressed for agent hosts without a full skill install. The full skills add workflows, diff --git a/bin/gstack-pregate b/bin/gstack-pregate index 446e4be7a..a7995aa41 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 1e8698852..9e8aaefce 100644 --- a/docs/pregate.md +++ b/docs/pregate.md @@ -825,7 +825,10 @@ gstack-pregate sealed [--range ] [--remote origin] [--base ] [--js `.gstack/ship-policy.json` to that release before landing either key. - **Policy.** The union of the destination base branch's `sealed` and the pushed commit's: a push can tighten it, never relax it (`allow_paths` come - from the base branch alone, and a pushed deny glob cannot override one). A malformed or + from the base branch alone, and a pushed deny glob cannot override one). 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 6dd1572fa..cda351a97 100644 --- a/lib/sealed-guard.ts +++ b/lib/sealed-guard.ts @@ -251,7 +251,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/package.json b/package.json index a8f23e8f0..bb309fc9b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gstack", - "version": "1.91.318", + "version": "1.91.319", "description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.", "license": "MIT", "type": "module", diff --git a/test/sealed-guard.test.ts b/test/sealed-guard.test.ts index a9f044f6d..00874c0a9 100644 --- a/test/sealed-guard.test.ts +++ b/test/sealed-guard.test.ts @@ -358,3 +358,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'); + }); +});