Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 9 additions & 8 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ corepack enable
pnpm install
```

Pushgate uses pnpm for its Node config parser dependencies and scripts. The
hook, installer, and templates remain shell and YAML.
Pushgate uses pnpm for its Node config parser, runner tests, and scripts. The
installed command is a small Node entrypoint, the hook and installer are shell,
and templates remain YAML.

---

Expand Down Expand Up @@ -71,19 +72,19 @@ commit as-is and customise from there.

### Fixing the hook script

`hook/pre-push` has been hardened over many iterations. Before making changes:
`hook/pre-push` is the thin delegator between Git and the managed Pushgate
runner. Before making changes:

- Run `bash -n hook/pre-push` to validate syntax before committing
- Avoid `eval`, heredoc variable expansion, and unquoted variable interpolation
- File lists must always be passed as arrays, never as interpolated strings
- Test on both macOS (BSD tools) and Linux (GNU tools) if possible — `sed`,
`grep`, and `printf` behave differently between them
- Keep hook arguments, stdin, and exit codes intact across the runner boundary
- Keep missing-runner and incompatible-protocol diagnostics actionable
- Avoid adding policy execution back into the installed hook

### Fixing the installer

`install.sh` follows the same shell safety rules as the hook. Additionally:
- It must work when piped through `bash` (`curl ... | bash`)
- It must not assume any tools beyond `bash`, `curl`, and `git` are available
- It must not assume any tools beyond `bash`, `curl`, `git`, and `node` are available

---

Expand Down
24 changes: 15 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# ai-pushgate

A language-agnostic push gate for regular git push workflows. An installed pre-push hook runs local checks and AI review before the push proceeds, helping clean up obvious issues early and prevent sensitive or unwanted changes from reaching the next layer of review.
A language-agnostic push gate for regular git push workflows. An installed pre-push hook delegates into a managed Pushgate runner so local checks and AI review can fit the normal `git push` flow before changes reach the next layer of review.

## How it works
## Target workflow

```
git push
Expand Down Expand Up @@ -35,6 +35,11 @@ Local deterministic checks can block a push. Local AI supports `blocking`, `advi

`.pushgate.yml` is the primary project config. `.push-review.yml` belongs to migration compatibility rather than the public config contract.

The current M1 runner boundary is intentionally thin: the installer wires the
hook to the managed `pushgate` command, the command accepts Git pre-push
context, and policy execution lands in the changed-file, deterministic-check,
and AI runner work that follows.

## Install

```bash
Expand Down Expand Up @@ -63,20 +68,22 @@ The installer:
2. Downloads and validates `hook/pre-push` → `.git/hooks/pre-push`
3. Backs up any existing `pre-push` hook before overwriting
4. Downloads the template config → `.pushgate.yml` (only on first install — never overwrites)
5. Checks configured runtimes and AI dependencies
5. Checks the Node.js runtime required by the managed command

## Requirements

**Git** is required. Pushgate plugs into its `pre-push` hook path.

**Node.js** is required by the installer-managed `pushgate` command.

**AI providers** depend on the configured mode. For example, Claude feedback requires Claude Code CLI:

```bash
npm install -g @anthropic-ai/claude-code
claude /login
```

**Runtime dependencies** depend on the tools you configure:
**Configured tool runtimes** depend on the tools you configure:

| Runtime | Required by |
|---------|-------------|
Expand All @@ -85,8 +92,6 @@ claude /login
| Python | Python tools (manual config) |
| Go | Go tools (manual config) |

The installer checks which runtimes your config requires and warns about any that are missing.

## Configuration

After install, edit `.pushgate.yml` in your project root:
Expand Down Expand Up @@ -148,14 +153,15 @@ To bypass the hook for a single push:
git push --no-verify
```

To keep deterministic checks but skip AI for one push, use Git's temporary config channel:
Scoped one-push skip controls are the v2 contract for the runner work that
follows. They use Git's temporary config channel:

```bash
git -c pushgate.skip-ai-check=true push
git -c pushgate.skip-all-checks=true push
```

The optional wrapper maps friendly flags to the same one-push config:
The planned optional wrapper maps friendly flags to the same one-push config:

```bash
pushgate push --skip-ai-check
Expand All @@ -164,7 +170,7 @@ pushgate push --skip-all-checks

## Updating

Re-run the installer to update the hook script. Your `.pushgate.yml` is **never overwritten** — it stays exactly as you've configured it.
Re-run the installer to update the managed command and hook script. Your `.pushgate.yml` is **never overwritten** — it stays exactly as you've configured it.

```bash
curl -fsSL https://raw.githubusercontent.com/rootstrap/ai-pushgate/main/install.sh | bash
Expand Down
42 changes: 42 additions & 0 deletions bin/pushgate.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#!/usr/bin/env node

const HOOK_PROTOCOL = "1";
const USAGE = `Usage:
pushgate hook-protocol
pushgate pre-push [git-hook-args...]`;

const [command, ...args] = process.argv.slice(2);

switch (command) {
case "hook-protocol":
if (args.length > 0) {
fail(`hook-protocol does not accept arguments: ${args.join(" ")}`);
break;
}

process.stdout.write(`${HOOK_PROTOCOL}\n`);
break;
case "pre-push":
await drainStdin();
break;
default:
fail(command ? `Unsupported Pushgate command: ${command}` : "Missing Pushgate command.");
}

function fail(message) {
process.stderr.write(`${message}\n\n${USAGE}\n`);
process.exitCode = 64;
}

async function drainStdin() {
try {
for await (const _chunk of process.stdin) {
// Drain Git hook ref updates. Later runner layers will parse this stream.
}
} catch (error) {
const detail = error instanceof Error ? error.message : String(error);

process.stderr.write(`Failed to read pre-push input: ${detail}\n`);
process.exitCode = 1;
}
}
Loading
Loading