This document follows one push through installation, hook delegation, deterministic checks, local AI review, and local failure semantics.
sequenceDiagram
participant User
participant Installer as install.sh
participant Git as target repo .git/hooks
participant Home as ~/.pushgate/bin
User->>Installer: curl | bash [--template name]
Installer->>Home: download bin/pushgate.mjs as pushgate
Installer->>Installer: node --check runner
Installer->>Git: backup existing pre-push hook when present
Installer->>Git: install hook/pre-push
Installer->>Git: write .pushgate.yml when absent
The installer places the runner in ~/.pushgate/bin/pushgate and installs the
thin hook into the target repository. Project repositories do not need local
Node dependencies at push time.
hook/pre-push owns a narrow interface:
- Resolve the repository root for diagnostics.
- Resolve the runner path in this order: local
git config pushgate.runner,PUSHGATE_RUNNER, then~/.pushgate/bin/pushgate. - Ensure the runner exists and is executable.
- Run
pushgate hook-protocol. - Require protocol
1. exec pushgate pre-push "$@".
The hook does not parse config, inspect changed files, run tools, or invoke AI providers.
| Command | Purpose |
|---|---|
hook-protocol |
Compatibility handshake for the shell hook. |
pre-push |
Internal hook entry point that runs the Pushgate workflow. |
Unsupported command shapes return usage output with exit code 64. Runtime
workflow failures are rendered through writePushgateError and return 1.
flowchart TD
Start["runPrePushWorkflow"] --> Drain["drain Git hook stdin"]
Drain --> Repo["resolveGitRepositoryRoot"]
Repo --> Skip["resolveSkipControlState"]
Skip --> ConfigDecision{"skip all?"}
ConfigDecision -->|yes| Done0["exit 0"]
ConfigDecision -->|no| Config["loadConfig"]
Config --> Changed{"changed files required?"}
Changed -->|yes| Path["resolveChangedFiles"]
Changed -->|no| NoPath["changedFileResolution = null"]
Path --> Det["runDeterministicChecks"]
NoPath --> Det
Det -->|blocked| Done1["return deterministic exit code"]
Det -->|warnings| Confirm1["confirm warnings"]
Confirm1 -->|declined| Done1
Confirm1 -->|confirmed| AIGate{"AI skipped or off?"}
Det -->|passed| AIGate
AIGate -->|yes| Done0
AIGate -->|no| AI["runLocalAiReview"]
AI --> Confirm2["confirm AI warnings"]
Confirm2 --> DoneAI["return final exit code"]
Changed files are resolved once and shared between deterministic checks and local AI. Deleted files remain in the normalized changed-file result for diff and AI context, but configured tools receive only live current paths.
The deterministic phase consumes normalized config and normalized changed-file resolution.
- Count enabled built-in policies, plugin checks, and configured tools.
- If none are configured, print a visible no-checks message and pass.
- Run built-in policies first.
- Run plugin checks such as Gitleaks.
- Run configured tools in order.
- Expand
{changed_files}into argv entries without shell interpolation. - Apply timeout, output-tail, mode, and fail-fast behavior.
- Return a deterministic check summary while streaming Transcript output.
flowchart TD
AI["runLocalAiReview"] --> Provider["resolveLocalAiProviderRuntime"]
Provider --> Guard1["evaluateChangedFileGuardrails"]
Guard1 -->|no files| Skip["render skip transcript, exit 0"]
Guard1 -->|too many changed lines| Block["render block transcript, exit 1"]
Guard1 --> Payload["buildLocalAiReviewPayload"]
Payload --> Prompt["renderLocalAiPrompt"]
Prompt --> Guard2["evaluatePromptGuardrail"]
Guard2 -->|too many prompt tokens| Skip
Guard2 --> Adapter["provider.runReview"]
Adapter --> Contract["validate AI review contract"]
Contract --> Verdict["buildLocalAiVerdict"]
Verdict --> Transcript["transcript.localAi.writeEvents"]
Provider adapters currently exist for Claude and Copilot. Both receive the same rendered payload and return a provider-neutral review result or provider failure.
| Failure | Blocking mode | Advisory mode | Off mode |
|---|---|---|---|
| Deterministic blocking check | Blocks | Blocks | Blocks |
| Deterministic warning check | Requires confirmation | Requires confirmation | Requires confirmation |
| Provider missing, unauthenticated, failed, timed out, empty, invalid | Blocks | Warns, then requires confirmation | Not run |
| AI blocking findings | Blocks | Warns, then requires confirmation | Not run |
| AI warning findings only | Requires confirmation | Requires confirmation | Not run |
| Changed-line guardrail exceeded | Blocks before provider invocation | Blocks before provider invocation | Not run |
| Prompt-token guardrail exceeded | Skips local AI only | Skips local AI only | Not run |
Skip controls are intentionally visible:
git push --no-verifybypasses the Git hook entirely.git -c pushgate.skip-all-checks=true pushbypasses all local Pushgate work.git -c pushgate.skip-ai-check=true pushkeeps deterministic checks and skips only local AI.