From a4c1fa1e73a98790ff7b68ab1cb5d25c32296076 Mon Sep 17 00:00:00 2001 From: Patrick Roza Date: Mon, 3 Aug 2026 20:18:52 +0200 Subject: [PATCH 1/2] =?UTF-8?q?feat(agent):=20ship=20gate=20=E2=80=94=20ch?= =?UTF-8?q?ecks=20run=20at=20the=20push,=20not=20on=20request?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ports the agent ship gate from macs-scanner, minus everything that does not apply to a library monorepo. `.githooks/pre-push` is a no-op for humans and for draft / no-PR pushes, and runs the full gate on ready PRs. Drafts stay free so sharing early costs nothing. Publishing is gated rather than forbidden: `pnpm pr:ready` is the explicit path and plain `gh pr ready` reaches the same place, because `.tools/bin/gh` runs the gate first and passes the command through when it is green. Nothing to remember, and no way around it. The gate is `pnpm check` -> `pnpm lint` -> `pnpm test`: exactly what ci.yml runs, deliberately not a superset. There is no application to stand up and no browser suite here, so the unit run is the whole gate — `pnpm -r --no-bail test:run` is the closest analogue of the E2E stage the product repos gate on. The validated HEAD SHA is cached under `.run/`, so a commit is validated once however often it is pushed or published, and AGENTS.md now says plainly not to hand-run these checks as routine verification. Repeatedly re-proving one commit is the cost the gate exists to remove, not to add. Two details carried over from what the other ports taught: - The gate supplies its own PATH (node_modules/.bin). pnpm and the hook both provide it, so the dependency stays invisible until something runs the gate from a bare process — which the gh shim does — and it dies with "command not found". - The hook scrubs git's local env vars before running, so nested git commands discover their own repository instead of inheriting the outer push's GIT_DIR. Verified: the shim intercepts `gh pr ready`, runs the gate, and refuses on a dirty worktree. Co-Authored-By: Claude Opus 5 (1M context) --- .envrc | 6 +- .githooks/pre-push | 12 ++ .gitignore | 4 + AGENTS.md | 48 ++++++-- package.json | 3 +- scripts/agent-gh.mjs | 60 ++++++++++ scripts/agent-pr-ready.mjs | 65 +++++++++++ scripts/agent-pre-push.mjs | 161 ++++++++++++++++++++++++++ scripts/install-git-hooks.mjs | 62 ++++++++++ scripts/lib/agent-env.mjs | 26 +++++ scripts/lib/agent-gh-policy.mjs | 155 +++++++++++++++++++++++++ scripts/lib/agent-pr-state.mjs | 123 ++++++++++++++++++++ scripts/lib/agent-ship-gate-cache.mjs | 121 +++++++++++++++++++ 13 files changed, 837 insertions(+), 9 deletions(-) create mode 100755 .githooks/pre-push create mode 100644 scripts/agent-gh.mjs create mode 100644 scripts/agent-pr-ready.mjs create mode 100644 scripts/agent-pre-push.mjs create mode 100644 scripts/install-git-hooks.mjs create mode 100644 scripts/lib/agent-env.mjs create mode 100644 scripts/lib/agent-gh-policy.mjs create mode 100644 scripts/lib/agent-pr-state.mjs create mode 100644 scripts/lib/agent-ship-gate-cache.mjs diff --git a/.envrc b/.envrc index 8392d159f2..03113d3dba 100644 --- a/.envrc +++ b/.envrc @@ -1 +1,5 @@ -use flake \ No newline at end of file +use flake + +# Repo `gh` policy shim: publishing runs the ship gate first (see AGENTS.md). +# Installed by scripts/install-git-hooks.mjs; run `direnv allow` once after cloning. +PATH_add .tools/bin diff --git a/.githooks/pre-push b/.githooks/pre-push new file mode 100755 index 0000000000..d2386cb765 --- /dev/null +++ b/.githooks/pre-push @@ -0,0 +1,12 @@ +# Agent-only ship gate (humans: no-op). See scripts/agent-pre-push.mjs. +# Draft / no PR: free push. Ready PR: check + lint + test. +# Publish: pnpm pr:ready, or raw `gh pr ready` — the shim runs the same gate first. +# +# Humans: SKIP_AGENT_PREPUSH=1 git push +# Agents: never SKIP_AGENT_PREPUSH / never --no-verify +# Nested Git commands must discover their own repositories, not inherit this +# hook's GIT_DIR/GIT_WORK_TREE from the outer push. +for name in $(git rev-parse --local-env-vars); do + unset "$name" +done +pnpm exec node scripts/agent-pre-push.mjs diff --git a/.gitignore b/.gitignore index 99d20f97aa..e98229014a 100644 --- a/.gitignore +++ b/.gitignore @@ -58,3 +58,7 @@ run.sh # Generated publish staging directories packages/*/.publish/ + +# agent gh shim (generated) + local ship-gate state +.tools/ +.run/ diff --git a/AGENTS.md b/AGENTS.md index a152daf982..61bcf99c55 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,11 +11,40 @@ This is the Effect App library repository, focusing on functional programming pa Always open a PR for agent work that changes the repo — do not leave finished work only on a local branch. -- **Draft early**: open a draft PR as soon as there is a meaningful commit (or when starting multi-step work that will land), so review/CI can track progress while local validation is still in flight. +- **Draft early**: open a draft PR as soon as there is a meaningful commit (or when starting multi-step work that will land), so review/CI can track progress while local validation is still in flight. Pushes to a draft are free — the gate does not run on them. - **Keep the PR current**: push commits as you go; update the PR body if scope shifts. -- **Ready when done**: after mandatory validation (`pnpm lint-fix`, `pnpm check`, and relevant tests) passes, mark the PR ready for review (undraft / publish). Do not leave a finished, validated change as draft. +- **Ready when done**: publish with `pnpm pr:ready`, or with plain `gh pr ready` — both run the ship gate first and undraft only if it passes. Do not hand-run the checks beforehand and do not leave a finished, validated change as draft. - **Base**: target `main` unless the work is explicitly stacked on another branch. +### The gate runs the checks — you do not + +`.githooks/pre-push` is the agent ship gate. It is a **no-op for humans** and +fires only for coding agents (`GROK_AGENT` / `T3_AGENT` / `AI_AGENT` / Claude / +Cursor / Codex env markers). + +| Branch PR state | Agent pre-push | +| ----------------------- | --------------------------------- | +| No open PR | **skip** — free push | +| **Draft** | **skip** — free push, share early | +| **Ready** for review | full ship gate | +| `gh` / PR lookup failed | full ship gate (**fail closed**) | + +The gate is `pnpm check` → `pnpm lint` → `pnpm test`, which is what CI runs and +nothing more. This is a library monorepo — no application to stand up, no browser +suite — so the unit run *is* the gate. It caches the validated HEAD SHA in +`.run/agent-ship-gate.json`, so **one commit is validated once** however many +times you push or publish it. Force a re-run with `AGENT_SHIP_GATE_FORCE=1`. + +**Do not hand-run `pnpm check` / `lint` / `test` as routine verification.** +Validating the same commit repeatedly costs the same each time and proves nothing +the first run did not. While iterating, narrow proof is the right tool — the one +test you are fixing, or a typecheck of the package you touched. Whole-gate runs +belong to the push. + +`.envrc` puts the `gh` shim on `PATH`; run **`direnv allow`** once after cloning +(`command -v gh` should print `.tools/bin/gh`). Never `--no-verify`, and never set +`SKIP_AGENT_PREPUSH` — that is the human escape hatch. + ### Core Principles - **Zero Tolerance for Errors**: All automated checks must pass @@ -48,13 +77,18 @@ Anti-patterns that mean you skipped the checklist: - Adding a sleep / retry to "give it time to work" instead of finding the missing wake signal. - Disabling a hook (`--no-verify`) or a check to make the diff land. -### Mandatory Validation Steps +### Validation + +The ship gate owns validation — see *The gate runs the checks — you do not*. It +runs `pnpm check` → `pnpm lint` → `pnpm test` on publish and on pushes to a ready +PR, once per commit. -After **all** changes are made, run these from the **repo root**: +`pnpm lint-fix` is the one thing worth running by hand, because it *writes*: it +formats and auto-fixes across packages, and the gate only reports what it would +have fixed. Run it when you are done editing, stage what it changes, then push. -1. `pnpm lint-fix` — auto-formats and fixes lint issues across all packages; apply all resulting changes -2. `pnpm check` — type-checks all packages (dependency changes in one package can break others); fix all reported errors - - If type checking continues to fail, run `pnpm clean` to clear caches, then re-run `pnpm check` +If type checking fails in a way that makes no sense against the diff, `pnpm clean` +clears the caches — stale incremental state produces phantom errors.