Reconcile Gitea distribution with current alpha behavior and release tooling

Hop-State: A_06FN55REVAR7VQG4BWJBCD8
Hop-Proposal: R_06FN55QTEWKH16NSR59YJYG
Hop-Task: T_06FN3MBF98GWD4NA5PA1RWG
Hop-Attempt: AT_06FN55BQ80H2S8WHNDD24G0
This commit is contained in:
Hop
2026-07-11 12:10:59 -07:00
parent 23a4891099
commit de99aad4d4
28 changed files with 1565 additions and 21 deletions
+67
View File
@@ -0,0 +1,67 @@
name: CI
on:
push:
branches:
- main
pull_request:
permissions:
code: read
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: https://gitea.com/actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Set up Go
uses: https://gitea.com/actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
with:
go-version-file: go.mod
cache: true
- name: Test with race detection
run: go test -race ./...
- name: Vet
run: go vet ./...
- name: Cross-compile supported targets
shell: bash
run: |
set -euo pipefail
for target in darwin/amd64 darwin/arm64 linux/amd64 linux/arm64 windows/amd64 windows/arm64; do
os=${target%/*}
arch=${target#*/}
output="dist-smoke/hop_${os}_${arch}"
if [[ "$os" == windows ]]; then output="${output}.exe"; fi
GOOS="$os" GOARCH="$arch" CGO_ENABLED=0 go build -trimpath -o "$output" ./cmd/hop
done
- name: Validate POSIX installer syntax
run: sh -n scripts/install.sh
- name: Smoke-test POSIX installer
run: sh scripts/test-install.sh
powershell-installer:
runs-on: windows-latest
steps:
- name: Check out repository
uses: https://gitea.com/actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Set up Go
uses: https://gitea.com/actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
with:
go-version-file: go.mod
cache: true
- name: Validate PowerShell installer syntax
shell: pwsh
run: |
$tokens = $null
$errors = $null
[void][System.Management.Automation.Language.Parser]::ParseFile(
(Resolve-Path scripts/install.ps1), [ref]$tokens, [ref]$errors
)
if ($errors.Count -gt 0) {
$errors | Format-List | Out-String | Write-Error
exit 1
}
- name: Smoke-test PowerShell installer
shell: pwsh
run: ./scripts/test-install.ps1
+59
View File
@@ -0,0 +1,59 @@
name: Release
on:
push:
tags:
- "v*"
permissions:
code: read
releases: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Check out full history
uses: https://gitea.com/actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0
- name: Set up Go
uses: https://gitea.com/actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
with:
go-version-file: go.mod
cache: true
- name: Require a public-release license
run: test -f LICENSE
- name: Test with race detection
run: go test -race ./...
- name: Vet
run: go vet ./...
- name: Build and upload a draft Gitea Release
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
run: |
set -euo pipefail
version=2.17.0
case "$(uname -m)" in
x86_64|amd64)
arch=x86_64
expected=dde10e2d5a13cef969c0eec00c74f359c0ac306d702b1bd291ad9337b4e54c1d
;;
aarch64|arm64)
arch=arm64
expected=75f93fc0e25d10d8535ffd0e4abcf39d6784a2467ba453d479ae513729a9ebbf
;;
*)
echo "Unsupported release runner architecture: $(uname -m)" >&2
exit 1
;;
esac
archive="goreleaser_Linux_${arch}.tar.gz"
tmp_dir=$(mktemp -d)
trap 'rm -rf "$tmp_dir"' EXIT
curl -fsSL --proto '=https' --tlsv1.2 \
-o "$tmp_dir/$archive" \
"https://github.com/goreleaser/goreleaser/releases/download/v${version}/${archive}"
printf '%s %s\n' "$expected" "$tmp_dir/$archive" | sha256sum -c -
tar -xzf "$tmp_dir/$archive" -C "$tmp_dir" goreleaser
"$tmp_dir/goreleaser" release --clean
+71
View File
@@ -0,0 +1,71 @@
version: 2
project_name: hop
builds:
- id: hop
main: ./cmd/hop
binary: hop
env:
- CGO_ENABLED=0
goos:
- darwin
- linux
- windows
goarch:
- amd64
- arm64
goamd64:
- v1
flags:
- -trimpath
ldflags:
- >-
-s -w
-X githop.xyz/hop/hop/internal/hop.Version={{ .Version }}
archives:
- id: release
ids:
- hop
formats:
- tar.gz
format_overrides:
- goos: windows
formats:
- zip
# Stable names keep installer asset lookup deterministic across releases.
name_template: "hop_{{ .Os }}_{{ .Arch }}"
files:
- README.md
- LICENSE*
checksum:
name_template: checksums.txt
algorithm: sha256
gitea_urls:
api: https://githop.xyz/api/v1
download: https://githop.xyz
skip_tls_verify: false
snapshot:
version_template: "{{ incpatch .Version }}-next"
changelog:
sort: asc
filters:
exclude:
- "^docs?:"
- "^test:"
- "^chore:"
- "^ci:"
- "Merge pull request"
- "Merge branch"
release:
gitea:
owner: hop
name: hop
draft: true
prerelease: auto
+101 -15
View File
@@ -1,6 +1,6 @@
# Hop
Hop is an experimental prompt-native version-control kernel for coding agents.
Hop is prompt-native version control for coding agents.
Every instruction becomes an immutable state before project effects. Agent work produces checkpoint and proposal states beneath it. Landing a proposal creates a new accepted state and safely materializes it in the visible project folder without moving the users Git branch or real index. Controller integrations may capture the stronger pre-delivery boundary as well.
@@ -41,7 +41,9 @@ This repository contains the first local alpha kernel. It supports:
It does not yet include a trusted raw-prompt hook, project knowledge, claims, remote synchronization, a GUI, or semantic merging.
See [the product blueprint](docs/product-blueprint.md) for the complete model,
Start with [Installation](#installation) and [Getting started](#getting-started),
or browse the [documentation wiki](wiki/Home.md). See
[the product blueprint](docs/product-blueprint.md) for the complete model,
design principles, and phased roadmap.
## How the pieces fit
@@ -62,30 +64,106 @@ Agent validate → auto-land → accepted state
The user continues typing into Codex Desktop normally. The skill invokes `hop begin` before inspecting or changing the project. `hop begin` initializes Hop when needed, uses `CODEX_THREAD_ID` to recognize follow-ups, checkpoints prior effects, and returns the isolated workspace. The skill confines work to that workspace, validates it, and automatically lands a successful proposal. `hop land` advances accepted state and then projects that exact tree into the selected folder. It proceeds only when the visible source still matches an accepted Hop state, so user edits are never overwritten. The original task prompt is the authorization; no extra landing prompt is required. Say `review first` or `proposal only` to opt into manual approval. This is a pre-project-effect boundary: the prompt is stored after Codex receives it but before the agent performs project work. A controller or future trusted prompt hook can provide strict pre-delivery capture.
## Build
## Installation
Requires Go 1.26+ and Git 2.40+.
Packaged releases require Git 2.40 or newer. Go is not required unless you use
the developer installation path.
### macOS and Linux — recommended
The installer detects your operating system and CPU, downloads the matching
release archive, verifies its SHA-256 checksum, installs `hop` to
`~/.local/bin`, adds that directory to your shell PATH when necessary, and
installs the Codex skill:
```bash
go build -o hop ./cmd/hop
./hop help
curl -fsSL https://githop.xyz/hop/hop/raw/branch/main/scripts/install.sh | sh
```
## Quick start
Install the embedded skill for Codex. By default this writes to `${CODEX_HOME:-~/.codex}/skills/hop`:
To inspect the installer before running it:
```bash
hop skill install
curl -fsSLO https://githop.xyz/hop/hop/raw/branch/main/scripts/install.sh
less install.sh
sh install.sh
```
Export it to another agents skills directory with:
Pin a version or choose another destination with environment variables:
```bash
hop skill install --path /path/to/agent/skills
HOP_VERSION=v0.1.0 HOP_INSTALL_DIR="$HOME/bin" sh install.sh
```
Restart Codex after installation if the skill is not yet visible. Then use Codex Desktop normally. The skill is configured for implicit invocation on every local repository turn. Its first project action is equivalent to:
### Windows PowerShell
Run PowerShell as your normal user. The installer verifies the release,
installs `hop.exe` under `%LOCALAPPDATA%\Programs\Hop`, adds it to your user
PATH, and installs the Codex skill:
```powershell
irm https://githop.xyz/hop/hop/raw/branch/main/scripts/install.ps1 | iex
```
### Go install — developer path
If Go 1.26 or newer is already installed:
```bash
go install githop.xyz/hop/hop/cmd/hop@latest
hop skill install --force
```
Ensure `$(go env GOPATH)/bin` is on PATH. Tagged module builds report the tag
through `hop version`.
### Build from source
Cloning is supported, but it is the contributor/fallback path rather than the
normal product installation:
```bash
git clone https://githop.xyz/hop/hop.git
cd hop
go test ./...
go build -trimpath -o hop ./cmd/hop
mkdir -p "$HOME/.local/bin"
install -m 755 hop "$HOME/.local/bin/hop"
"$HOME/.local/bin/hop" skill install --force
```
### Verify the installation
```bash
hop version
hop help
```
The packaged installers install two things:
- the `hop` CLI; and
- the embedded Hop agent skill at `${CODEX_HOME:-~/.codex}/skills/hop`.
No project is modified during installation. A project receives its local
`.hop/` state only when Hop first runs inside that project.
Release installer URLs become available after the corresponding Gitea Release
is published. Until the first public release exists, use the source build.
## Getting started
1. Install Hop using one of the methods above.
2. Restart Codex Desktop if it was open during installation.
3. Open any existing Git project in Codex Desktop.
4. Ask Codex to change the project normally. You do **not** need to run
`hop init`, create a branch, or route prompts through another application.
5. After the first task, run `hop status` in the project to inspect its accepted
state and confirm the visible root is synchronized.
The skill is configured for implicit use on every local repository turn. If an
agent ever fails to activate it automatically, mention `$hop` in that task as a
deterministic fallback.
The agent's first project action is equivalent to:
```bash
hop begin --agent codex --heredoc <<'HOP_PROMPT_EOF'
@@ -93,9 +171,17 @@ hop begin --agent codex --heredoc <<'HOP_PROMPT_EOF'
HOP_PROMPT_EOF
```
The agent—not the user—runs this command. It initializes Hop without changing the current Git branch, working tree, or index, stores the prompt, and returns the state IDs and isolated workspace. Follow-up messages in the same Codex task automatically continue the same Hop attempt.
The agent—not the user—runs this command. It initializes Hop without changing
the current Git branch, working tree, or index, stores the prompt, and returns
the state IDs and isolated workspace. Follow-up messages in the same Codex task
automatically continue the same Hop attempt.
Codex chooses implicitly invoked skills from their descriptions. Explicitly mentioning `$hop` remains the deterministic fallback if a task does not activate it automatically.
For another agent runtime, export the embedded skill to that runtime's skills
directory:
```bash
hop skill install --path /path/to/agent/skills --force
```
After editing the printed workspace, the agent runs this lifecycle automatically:
+1 -1
View File
@@ -3,7 +3,7 @@ package main
import (
"os"
"github.com/hop-vcs/hop/internal/hop"
"githop.xyz/hop/hop/internal/hop"
)
func main() {
+1 -1
View File
@@ -1,4 +1,4 @@
module github.com/hop-vcs/hop
module githop.xyz/hop/hop
go 1.26
+26
View File
@@ -1,5 +1,7 @@
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
@@ -10,14 +12,38 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
modernc.org/cc/v4 v4.26.5 h1:xM3bX7Mve6G8K8b+T11ReenJOT+BmVqQj0FY5T4+5Y4=
modernc.org/cc/v4 v4.26.5/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A=
modernc.org/ccgo/v4 v4.28.1/go.mod h1:uD+4RnfrVgE6ec9NGguUNdhqzNIeeomeXf6CL0GTE5Q=
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.66.10 h1:yZkb3YeLx4oynyR+iUsXsybsX4Ubx7MQlSYEw4yj59A=
modernc.org/libc v1.66.10/go.mod h1:8vGSEwvoUoltr4dlywvHqjtAqHBaw0j1jI7iFBTAr2I=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.39.1 h1:H+/wGFzuSCIEVCvXYVHX5RQglwhMOvtHSv+VtidL2r4=
modernc.org/sqlite v1.39.1/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
+17 -3
View File
@@ -8,6 +8,7 @@ import (
"fmt"
"io"
"os"
"runtime/debug"
"strings"
)
@@ -39,7 +40,19 @@ Usage:
Add --json anywhere for machine-readable output.
`
const Version = "0.1.0-alpha.4"
// Version is replaced by GoReleaser through -ldflags. Source installs made by
// `go install module@version` fall back to the module version in Go build info.
var Version = "dev"
func effectiveVersion() string {
if Version != "" && Version != "dev" {
return strings.TrimPrefix(Version, "v")
}
if info, ok := debug.ReadBuildInfo(); ok && info.Main.Version != "" && info.Main.Version != "(devel)" {
return strings.TrimPrefix(info.Main.Version, "v")
}
return "dev"
}
func RunCLI(args []string, stdout, stderr io.Writer) int {
return RunCLIWithInput(args, os.Stdin, stdout, stderr)
@@ -56,10 +69,11 @@ func RunCLIWithInput(args []string, stdin io.Reader, stdout, stderr io.Writer) i
command := args[0]
commandArgs := args[1:]
if command == "version" || command == "--version" {
version := effectiveVersion()
if jsonOutput {
writeJSON(stdout, map[string]any{"ok": true, "version": Version})
writeJSON(stdout, map[string]any{"ok": true, "version": version})
} else {
fmt.Fprintf(stdout, "hop %s\n", Version)
fmt.Fprintf(stdout, "hop %s\n", version)
}
return 0
}
+67
View File
@@ -0,0 +1,67 @@
package hop
import (
"os"
"path/filepath"
"regexp"
"strings"
"testing"
)
var markdownLink = regexp.MustCompile(`\[[^]]+\]\(([^)]+)\)`)
func TestDocumentationLinksResolve(t *testing.T) {
root := filepath.Clean(filepath.Join("..", ".."))
files := []string{filepath.Join(root, "README.md")}
wiki, err := filepath.Glob(filepath.Join(root, "wiki", "*.md"))
if err != nil {
t.Fatal(err)
}
files = append(files, wiki...)
if len(wiki) < 10 {
t.Fatalf("wiki contains %d pages, want at least 10", len(wiki))
}
for _, file := range files {
contents, err := os.ReadFile(file)
if err != nil {
t.Fatal(err)
}
for _, match := range markdownLink.FindAllStringSubmatch(string(contents), -1) {
target := match[1]
if strings.HasPrefix(target, "http://") || strings.HasPrefix(target, "https://") ||
strings.HasPrefix(target, "#") || strings.HasPrefix(target, "mailto:") {
continue
}
if anchor := strings.IndexByte(target, '#'); anchor >= 0 {
target = target[:anchor]
}
if target == "" {
continue
}
resolved := filepath.Clean(filepath.Join(filepath.Dir(file), filepath.FromSlash(target)))
if filepath.Dir(file) == filepath.Join(root, "wiki") && filepath.Ext(resolved) == "" {
resolved += ".md"
}
if _, err := os.Stat(resolved); err != nil {
t.Errorf("%s links to missing %s: %v", file, target, err)
}
}
}
}
func TestProductDocumentationUsesCanonicalGiteaHost(t *testing.T) {
for _, relative := range []string{"README.md", "wiki/Home.md", "wiki/Installation.md"} {
contents, err := os.ReadFile(filepath.Join("..", "..", filepath.FromSlash(relative)))
if err != nil {
t.Fatal(err)
}
text := string(contents)
if !strings.Contains(text, "githop.xyz") {
t.Errorf("%s does not name the canonical distribution host", relative)
}
if strings.Contains(text, "raw.githubusercontent.com/hop-vcs/hop") ||
strings.Contains(text, "github.com/hop-vcs/hop") {
t.Errorf("%s still points installation at GitHub", relative)
}
}
}
+18
View File
@@ -458,6 +458,24 @@ func TestCLILandConflictReturnsAutomaticReconciliation(t *testing.T) {
}
}
func TestCLIVersionUsesReleaseLinkerValue(t *testing.T) {
previous := Version
Version = "v1.2.3"
t.Cleanup(func() { Version = previous })
var stdout, stderr bytes.Buffer
if code := RunCLI([]string{"version", "--json"}, &stdout, &stderr); code != 0 {
t.Fatalf("version exited %d: %s", code, stderr.String())
}
var response map[string]any
if err := json.Unmarshal(stdout.Bytes(), &response); err != nil {
t.Fatal(err)
}
if got, _ := response["version"].(string); got != "1.2.3" {
t.Fatalf("version = %q, want 1.2.3", got)
}
}
func TestOpenProjectInsideFinalValidationDoesNotReacquireAcceptanceLock(t *testing.T) {
ctx := context.Background()
service, _ := newTestProject(t, map[string]string{"base.txt": "base\n"})
+1 -1
View File
@@ -7,7 +7,7 @@ import (
"path/filepath"
"strings"
hopskill "github.com/hop-vcs/hop/skills/hop"
hopskill "githop.xyz/hop/hop/skills/hop"
)
type SkillInstallResult struct {
+84
View File
@@ -0,0 +1,84 @@
[CmdletBinding()]
param(
[string]$GiteaUrl = $(if ($env:HOP_GITEA_URL) { $env:HOP_GITEA_URL } else { "https://githop.xyz" }),
[string]$Repository = $(if ($env:HOP_REPOSITORY) { $env:HOP_REPOSITORY } else { "hop/hop" }),
[string]$Version = $(if ($env:HOP_VERSION) { $env:HOP_VERSION } else { "latest" }),
[string]$InstallDir = $(if ($env:HOP_INSTALL_DIR) { $env:HOP_INSTALL_DIR } else { Join-Path $env:LOCALAPPDATA "Programs\Hop" }),
[switch]$SkipSkill,
[switch]$SkipPath
)
$ErrorActionPreference = "Stop"
$GiteaUrl = $GiteaUrl.TrimEnd("/")
switch ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString()) {
"X64" { $arch = "amd64" }
"Arm64" { $arch = "arm64" }
default { throw "Unsupported Windows architecture: $($_)" }
}
$asset = "hop_windows_${arch}.zip"
if ($Version -eq "latest") {
# Gitea's /releases/latest endpoint omits prereleases. Select the newest
# published release so the normal installer also works during Hop's alpha.
$releases = @(Invoke-RestMethod -Uri "$GiteaUrl/api/v1/repos/$Repository/releases?draft=false&page=1&limit=1")
$latestRelease = $releases | Select-Object -First 1
$tag = $latestRelease.tag_name
if (-not $tag) { throw "Could not determine the latest published release" }
} else {
$tag = if ($Version.StartsWith("v")) { $Version } else { "v$Version" }
}
if ($tag -notmatch '^[A-Za-z0-9._-]+$') {
throw "Release API returned an unsafe tag: $tag"
}
$releaseUrl = "$GiteaUrl/$Repository/releases/download/$tag"
$tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ("hop-install-" + [guid]::NewGuid())
New-Item -ItemType Directory -Path $tempDir | Out-Null
try {
$archivePath = Join-Path $tempDir $asset
$checksumsPath = Join-Path $tempDir "checksums.txt"
Write-Host "Downloading $asset..."
Invoke-WebRequest -UseBasicParsing -Uri "$releaseUrl/$asset" -OutFile $archivePath
Invoke-WebRequest -UseBasicParsing -Uri "$releaseUrl/checksums.txt" -OutFile $checksumsPath
$escapedAsset = [regex]::Escape($asset)
$checksumLine = Get-Content $checksumsPath | Where-Object { $_ -match "^([a-fA-F0-9]{64})\s+\*?$escapedAsset$" } | Select-Object -First 1
if (-not $checksumLine) { throw "checksums.txt does not contain $asset" }
$expected = ([regex]::Match($checksumLine, "^[a-fA-F0-9]{64}")).Value.ToLowerInvariant()
$actual = (Get-FileHash -Algorithm SHA256 $archivePath).Hash.ToLowerInvariant()
if ($actual -ne $expected) { throw "Checksum verification failed for $asset" }
$extractPath = Join-Path $tempDir "archive"
Expand-Archive -Path $archivePath -DestinationPath $extractPath
$binary = Join-Path $extractPath "hop.exe"
if (-not (Test-Path $binary)) { throw "Release archive does not contain hop.exe" }
New-Item -ItemType Directory -Force -Path $InstallDir | Out-Null
$installedBinary = Join-Path $InstallDir "hop.exe"
Copy-Item -Force $binary $installedBinary
if (-not $SkipPath) {
$userPath = [Environment]::GetEnvironmentVariable("Path", "User")
$pathEntries = @($userPath -split ";" | Where-Object { $_ })
if ($pathEntries -notcontains $InstallDir) {
$newUserPath = (($pathEntries + $InstallDir) -join ";")
[Environment]::SetEnvironmentVariable("Path", $newUserPath, "User")
Write-Host "Added $InstallDir to your user PATH."
}
if (($env:Path -split ";") -notcontains $InstallDir) {
$env:Path = "$InstallDir;$env:Path"
}
}
if (-not $SkipSkill) {
& $installedBinary skill install --force
}
Write-Host "Installed $(& $installedBinary version)"
Write-Host "Binary: $installedBinary"
if (-not $SkipSkill) {
Write-Host "Restart Codex if it is open, then use it normally in any Git repository."
}
} finally {
Remove-Item -Recurse -Force -ErrorAction SilentlyContinue $tempDir
}
+107
View File
@@ -0,0 +1,107 @@
#!/bin/sh
set -eu
gitea_url=${HOP_GITEA_URL:-https://githop.xyz}
gitea_url=${gitea_url%/}
repository=${HOP_REPOSITORY:-hop/hop}
requested_version=${HOP_VERSION:-latest}
install_dir=${HOP_INSTALL_DIR:-"$HOME/.local/bin"}
install_skill=${HOP_INSTALL_SKILL:-1}
modify_path=${HOP_MODIFY_PATH:-1}
fail() {
printf 'hop installer: %s\n' "$*" >&2
exit 1
}
command -v curl >/dev/null 2>&1 || fail "curl is required"
command -v tar >/dev/null 2>&1 || fail "tar is required"
case $(uname -s) in
Darwin) os=darwin ;;
Linux) os=linux ;;
*) fail "unsupported operating system: $(uname -s)" ;;
esac
case $(uname -m) in
x86_64 | amd64) arch=amd64 ;;
arm64 | aarch64) arch=arm64 ;;
*) fail "unsupported architecture: $(uname -m)" ;;
esac
asset="hop_${os}_${arch}.tar.gz"
if [ "$requested_version" = latest ]; then
# Gitea's /releases/latest endpoint omits prereleases. Hop is distributed as
# an alpha today, so select the newest published release from the list API.
latest_json=$(curl -fL --retry 3 --proto '=https' --tlsv1.2 \
"$gitea_url/api/v1/repos/$repository/releases?draft=false&page=1&limit=1")
tag=$(printf '%s' "$latest_json" | sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')
[ -n "$tag" ] || fail "could not determine the latest published release"
else
case "$requested_version" in
v*) tag=$requested_version ;;
*) tag="v${requested_version}" ;;
esac
fi
case "$tag" in
*[!A-Za-z0-9._-]*) fail "release API returned an unsafe tag: $tag" ;;
esac
release_url="$gitea_url/$repository/releases/download/${tag}"
tmp_dir=$(mktemp -d 2>/dev/null || mktemp -d -t hop-install)
cleanup() {
rm -rf "$tmp_dir"
}
trap cleanup EXIT HUP INT TERM
printf 'Downloading %s...\n' "$asset"
curl -fL --retry 3 --proto '=https' --tlsv1.2 \
-o "$tmp_dir/$asset" "$release_url/$asset"
curl -fL --retry 3 --proto '=https' --tlsv1.2 \
-o "$tmp_dir/checksums.txt" "$release_url/checksums.txt"
expected=$(awk -v name="$asset" '$2 == name || $2 == "*" name { print $1; exit }' "$tmp_dir/checksums.txt")
[ -n "$expected" ] || fail "checksums.txt does not contain $asset"
if command -v sha256sum >/dev/null 2>&1; then
actual=$(sha256sum "$tmp_dir/$asset" | awk '{print $1}')
elif command -v shasum >/dev/null 2>&1; then
actual=$(shasum -a 256 "$tmp_dir/$asset" | awk '{print $1}')
else
fail "sha256sum or shasum is required to verify the download"
fi
[ "$actual" = "$expected" ] || fail "checksum verification failed for $asset"
tar -xzf "$tmp_dir/$asset" -C "$tmp_dir"
[ -f "$tmp_dir/hop" ] || fail "release archive does not contain hop"
mkdir -p "$install_dir"
cp "$tmp_dir/hop" "$install_dir/hop"
chmod 0755 "$install_dir/hop"
case ":${PATH:-}:" in
*":$install_dir:"*) ;;
*)
if [ "$modify_path" = 1 ] && [ "$install_dir" = "$HOME/.local/bin" ]; then
case ${SHELL:-} in
*/zsh) profile="$HOME/.zprofile" ;;
*) profile="$HOME/.profile" ;;
esac
path_line='export PATH="$HOME/.local/bin:$PATH"'
if ! grep -F "$path_line" "$profile" >/dev/null 2>&1; then
printf '\n%s\n' "$path_line" >>"$profile"
printf 'Added ~/.local/bin to PATH in %s.\n' "$profile"
fi
else
printf 'Add %s to PATH before opening a new terminal.\n' "$install_dir" >&2
fi
;;
esac
if [ "$install_skill" = 1 ]; then
"$install_dir/hop" skill install --force
fi
printf 'Installed %s\n' "$("$install_dir/hop" version)"
printf 'Binary: %s\n' "$install_dir/hop"
if [ "$install_skill" = 1 ]; then
printf 'Restart Codex if it is open, then use it normally in any Git repository.\n'
fi
+72
View File
@@ -0,0 +1,72 @@
[CmdletBinding()]
param()
$ErrorActionPreference = "Stop"
$root = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path)
$tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ("hop-installer-test-" + [guid]::NewGuid())
$fixtures = Join-Path $tempDir "fixtures"
$payload = Join-Path $tempDir "payload"
$installDir = Join-Path $tempDir "install"
$testArch = if ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString() -eq "Arm64") { "arm64" } else { "amd64" }
$asset = "hop_windows_${testArch}.zip"
New-Item -ItemType Directory -Path $tempDir | Out-Null
New-Item -ItemType Directory -Path $fixtures, $payload | Out-Null
try {
$binary = Join-Path $payload "hop.exe"
& go build -trimpath `
-ldflags "-X githop.xyz/hop/hop/internal/hop.Version=9.9.9-installer-test" `
-o $binary (Join-Path $root "cmd/hop")
if ($LASTEXITCODE -ne 0) { throw "Could not build installer test binary" }
$archive = Join-Path $fixtures $asset
Compress-Archive -Path $binary -DestinationPath $archive
$hash = (Get-FileHash -Algorithm SHA256 $archive).Hash.ToLowerInvariant()
"$hash $asset" | Set-Content -Encoding ascii (Join-Path $fixtures "checksums.txt")
function Invoke-RestMethod {
[CmdletBinding()]
param([Parameter(Mandatory)][string]$Uri)
if (-not $Uri.EndsWith("/releases?draft=false&page=1&limit=1")) {
throw "Unexpected installer API URL: $Uri"
}
return ,([pscustomobject]@{
tag_name = "v9.9.9-installer-test"
prerelease = $true
draft = $false
})
}
function Invoke-WebRequest {
[CmdletBinding()]
param(
[switch]$UseBasicParsing,
[Parameter(Mandatory)][string]$Uri,
[Parameter(Mandatory)][string]$OutFile
)
if ($Uri.EndsWith("/checksums.txt")) {
Copy-Item (Join-Path $fixtures "checksums.txt") $OutFile
} elseif ($Uri.EndsWith("/$asset")) {
Copy-Item (Join-Path $fixtures $asset) $OutFile
} else {
throw "Unexpected installer asset URL: $Uri"
}
}
& (Join-Path $root "scripts/install.ps1") `
-GiteaUrl "https://gitea.test" `
-Repository "hop/hop" `
-InstallDir $installDir `
-SkipSkill `
-SkipPath
$installed = Join-Path $installDir "hop.exe"
if (-not (Test-Path $installed)) { throw "Installer did not install hop.exe" }
$version = & $installed version
if ($LASTEXITCODE -ne 0 -or $version -ne "hop 9.9.9-installer-test") {
throw "Unexpected installed version: $version"
}
Write-Host "PowerShell installer smoke test passed."
} finally {
Remove-Item -Recurse -Force -ErrorAction SilentlyContinue $tempDir
}
+83
View File
@@ -0,0 +1,83 @@
#!/bin/sh
set -eu
root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
cd "$root"
tmp_dir=$(mktemp -d 2>/dev/null || mktemp -d -t hop-installer-test)
cleanup() {
rm -rf "$tmp_dir"
}
trap cleanup EXIT HUP INT TERM
case $(uname -s) in
Darwin) os=darwin ;;
Linux) os=linux ;;
*) printf 'installer smoke test does not support %s\n' "$(uname -s)" >&2; exit 1 ;;
esac
case $(uname -m) in
x86_64 | amd64) arch=amd64 ;;
arm64 | aarch64) arch=arm64 ;;
*) printf 'installer smoke test does not support %s\n' "$(uname -m)" >&2; exit 1 ;;
esac
asset="hop_${os}_${arch}.tar.gz"
mkdir -p "$tmp_dir/payload" "$tmp_dir/fixtures" "$tmp_dir/mock-bin" "$tmp_dir/home"
go build -trimpath \
-ldflags '-X githop.xyz/hop/hop/internal/hop.Version=9.9.9-installer-test' \
-o "$tmp_dir/payload/hop" "$root/cmd/hop"
tar -czf "$tmp_dir/fixtures/$asset" -C "$tmp_dir/payload" hop
if command -v sha256sum >/dev/null 2>&1; then
hash=$(sha256sum "$tmp_dir/fixtures/$asset" | awk '{print $1}')
else
hash=$(shasum -a 256 "$tmp_dir/fixtures/$asset" | awk '{print $1}')
fi
printf '%s %s\n' "$hash" "$asset" >"$tmp_dir/fixtures/checksums.txt"
cat >"$tmp_dir/mock-bin/curl" <<'MOCK_CURL'
#!/bin/sh
set -eu
output=
url=
while [ "$#" -gt 0 ]; do
case "$1" in
-o) shift; output=$1 ;;
http://* | https://*) url=$1 ;;
esac
shift
done
case "$url" in
*/api/v1/repos/*/releases\?draft=false\&page=1\&limit=1)
printf '[{"tag_name":"v9.9.9-installer-test","prerelease":true,"draft":false}]'
;;
*/checksums.txt)
cp "$HOP_TEST_FIXTURES/checksums.txt" "$output"
;;
*/hop_*.tar.gz)
cp "$HOP_TEST_FIXTURES/${url##*/}" "$output"
;;
*)
printf 'unexpected installer URL: %s\n' "$url" >&2
exit 1
;;
esac
MOCK_CURL
chmod 0755 "$tmp_dir/mock-bin/curl"
HOME="$tmp_dir/home" \
PATH="$tmp_dir/mock-bin:$PATH" \
HOP_TEST_FIXTURES="$tmp_dir/fixtures" \
HOP_GITEA_URL="https://gitea.test" \
HOP_REPOSITORY="hop/hop" \
HOP_INSTALL_DIR="$tmp_dir/home/bin" \
HOP_MODIFY_PATH=0 \
sh "$root/scripts/install.sh"
version=$($tmp_dir/home/bin/hop version)
[ "$version" = "hop 9.9.9-installer-test" ] || {
printf 'unexpected installed version: %s\n' "$version" >&2
exit 1
}
[ -s "$tmp_dir/home/.codex/skills/hop/SKILL.md" ] || {
printf 'installer did not install the embedded Hop skill\n' >&2
exit 1
}
+65
View File
@@ -0,0 +1,65 @@
# Codex Desktop and agent workflow
## Codex Desktop
Users type into Codex normally. The installed skill makes prompt capture the
agent's first repository action:
```bash
hop begin --agent codex --heredoc <<'HOP_PROMPT_EOF'
<exact visible user message>
HOP_PROMPT_EOF
```
The agent adopts the returned `HOP_STATE_ID`, `HOP_TASK_ID`,
`HOP_ATTEMPT_ID`, and `HOP_WORKSPACE`, then confines reads, commands, edits, and
tests to that workspace.
The normal lifecycle is:
```bash
hop check P_... -- go test ./...
hop propose --summary "Implemented the requested behavior" P_...
hop land R_... -- go test ./...
```
No second landing authorization is requested unless the user explicitly asks
for review-first behavior.
## Follow-up messages
A later `hop begin` with the same Codex task session checkpoints existing
workspace effects, appends a new prompt state, and continues the same attempt.
The user does not carry state IDs between messages.
## Controller-grade capture
A harness that can persist before delivering a prompt to the model can use:
```bash
hop init
hop start --agent my-agent --heredoc <<'HOP_PROMPT_EOF'
Add password reset emails
HOP_PROMPT_EOF
eval "$(hop env P_...)"
```
Only deliver the prompt after `hop start` exits successfully. Controller-managed
follow-ups use:
```bash
hop prompt --from P_... --heredoc
```
This provides a stronger pre-delivery boundary than a Desktop skill, which can
only guarantee capture before project effects.
## Agent rules
- Never edit the canonical project root directly.
- Never mutate a frozen proposal.
- Do not bypass `hop land` with Git reset, checkout, worktree, or manual copying.
- Run validation against immutable checkpoints and the final integrated tree.
- Let Hop merge compatible concurrent work.
- Resolve genuine reconciliation workspaces without asking the user to perform
source-control mechanics, unless the underlying product intents are ambiguous.
+54
View File
@@ -0,0 +1,54 @@
# Architecture
Hop uses Git under the hood, but it is not merely a directory of metadata files
inside `.git`.
## Git responsibilities
Git provides:
- content-addressed blobs and trees;
- synthetic commits for immutable snapshots;
- detached worktrees for attempt isolation;
- three-way merge behavior;
- diffs and path inspection; and
- interoperability with existing repositories.
Hop never needs to move the user's active branch or rewrite the real index.
Private objects are pinned beneath `refs/hop/states/*`; accepted history is
mirrored at `refs/hop/accepted`.
## Hop responsibilities
`.hop/hop.db` is a SQLite WAL database containing:
- prompt/task/attempt identity;
- typed state edges and accepted lineage;
- evidence tied to exact source trees;
- session heads for Desktop follow-ups;
- materialized-root state; and
- immutable audit events.
## Project layout
```text
.hop/
├── hop.db
├── workspaces/
├── checks/
├── integration/
└── *.lock
```
`.hop/` is added to `.git/info/exclude`, not the public `.gitignore`. Hop refuses
initialization when `.hop` is already tracked as user-owned project content.
## Acceptance consistency
Acceptance is serialized and compare-and-swapped. SQLite is authoritative;
derived Git refs can be repaired by `hop doctor --repair`. Desktop landing also
tracks which accepted state is physically visible, allowing safe catch-up with
`hop sync` without treating a divergent folder as disposable.
For the full product direction, read the
[product blueprint](https://githop.xyz/hop/hop/src/branch/main/docs/product-blueprint.md).
+64
View File
@@ -0,0 +1,64 @@
# CLI reference
Add `--json` anywhere in a command for machine-readable output.
## Project and prompt lifecycle
| Command | Purpose |
|---|---|
| `hop init [path]` | Initialize Hop without moving the Git branch or index |
| `hop begin ...` | Desktop entry point: initialize if needed, capture prompt, continue session |
| `hop prompt ...` | Controller-managed prompt or follow-up capture |
| `hop checkpoint STATE` | Freeze current attempt progress |
| `hop check STATE -- COMMAND...` | Validate an immutable checkpoint |
| `hop propose [--summary TEXT] STATE` | Freeze a candidate proposal |
| `hop land PROPOSAL [-- COMMAND...]` | Accept and synchronize the visible root |
| `hop refresh PROPOSAL` | Explicitly prepare/reuse conflict reconciliation |
`hop start` aliases `hop prompt`; `hop reconcile` aliases `hop refresh`.
## Controller and synchronization commands
| Command | Purpose |
|---|---|
| `hop accept PROPOSAL [-- COMMAND...]` | Accept internally without changing visible files |
| `hop sync` | Materialize the current accepted tree from a safe accepted ancestor |
| `hop undo` | Create a forward-only acceptance that restores the previous accepted tree |
| `hop doctor [--repair]` | Validate database/object/ref consistency |
## Inspection
| Command | Purpose |
|---|---|
| `hop status` | Accepted head, attempts, and visible-root status |
| `hop graph` | State graph |
| `hop state STATE` | One state and its provenance |
| `hop env STATE` | Shell exports for an attempt |
| `hop diff STATE` | Diff represented by a state |
| `hop history` | Accepted lineage |
| `hop version` | Installed version |
## Skill distribution
```bash
hop skill install [--path SKILLS_DIR] [--force]
hop skill print
```
Without `--path`, the skill installs under
`${CODEX_HOME:-~/.codex}/skills/hop`.
## Exit codes
| Code | Meaning |
|---:|---|
| `0` | Success |
| `1` | Git, SQLite, filesystem, or internal failure |
| `2` | Invalid usage |
| `20` | Merge conflict; reconciliation workspace was prepared |
| `21` | Attempt or accepted head changed during compare-and-swap |
| `22` | Validation command failed |
| `23` | Visible-root divergence or overwrite collision |
Exit `20` is a continuation signal for an agent, not a request for the user to
manually merge files.
+58
View File
@@ -0,0 +1,58 @@
# Core concepts
Hop versions intent and source together. Git remains the content store; Hop adds
the causal state graph that explains which instruction produced which result.
## States
| Prefix | State | Meaning |
|---|---|---|
| `A_` | Accepted | Canonical Hop project revision |
| `P_` | Prompt | Durable instruction and pre-effect context |
| `C_` | Checkpoint | Immutable snapshot of attempt progress |
| `R_` | Proposal | Frozen candidate result |
| `F_` | Failed | Durable failed execution or validation result |
| `X_` | Cancelled | Terminal cancelled result |
Two states can reference the same Git tree and still be distinct occurrences.
For example, a prompt and checkpoint may contain identical files but represent
different moments and causal roles.
## Task
A task groups the prompts and attempts pursuing one user outcome. Follow-up
messages in the same Codex task stay connected automatically through
`CODEX_THREAD_ID`.
## Attempt and workspace
An attempt is one agent approach. Each attempt has a detached Git worktree under
`.hop/workspaces/`. Agents edit there instead of racing in the visible project
root.
## Evidence
`hop check` snapshots the workspace and runs validation against that immutable
tree. Evidence stores the command, redacted output, exit code, and exact tree
hash.
## Proposal
`hop propose` freezes a candidate tree. Later workspace edits cannot mutate the
proposal.
## Landing
`hop land` composes the proposal onto the current accepted state, runs optional
final-tree validation, advances accepted history with compare-and-swap, and
safely materializes the result into the visible project directory.
`hop accept` is lower-level controller behavior: it advances internal accepted
state but intentionally leaves the visible folder unchanged.
## Visible root
The visible root is the project directory selected in Codex. Hop only
materializes into it when it still matches an accepted Hop ancestor. Untracked,
ignored, staged, or ordinary file divergence that could be overwritten causes a
fail-closed error.
+60
View File
@@ -0,0 +1,60 @@
# Getting started
## Use Hop from Codex Desktop
1. [Install Hop](Installation).
2. Restart Codex Desktop if it was already open.
3. Select an existing Git project as the Codex working directory.
4. Ask Codex to make a normal change.
That is the full user workflow. Do not manually create `.hop`, route the prompt
through a terminal, or tell Codex to work inside `.hop/workspaces`. The Hop skill
does that coordination for the agent.
The skill is eligible for implicit activation on every repository task. Mention
`$hop` in the task if you want deterministic explicit activation.
## What happens on the first task
Before reading or changing project files, the agent runs `hop begin`. Hop then:
- initializes local state without moving the Git branch or index;
- stores the prompt after redacting detected credentials;
- creates an isolated attempt workspace;
- returns the state and workspace to the agent; and
- keeps all project-changing work inside that workspace.
The agent validates, proposes, and lands the result. A successful `hop land`
updates the visible project folder to the accepted tree.
## Confirm the result
From the selected project directory:
```bash
hop status
hop history
hop doctor
```
A normal Desktop result reports `Root: synchronized`.
## Ask for review before landing
Automatic landing is the default because the original task authorizes the
local code change. To stop at a proposal, say one of the following in the task:
- `review first`
- `proposal only`
- `do not land`
## Use another agent runtime
Install the embedded skill into that runtime's skills directory:
```bash
hop skill install --path /path/to/agent/skills --force
```
Controllers that can persist a prompt before model delivery should use
`hop start`, `hop env`, and `hop prompt`; see [Agent workflow](Agent-Workflow).
+45
View File
@@ -0,0 +1,45 @@
# Hop documentation
Hop is prompt-native version control for coding agents. It stores each prompt
as an immutable project state, gives agent work an isolated workspace, validates
the exact tree being accepted, and safely materializes accepted results into the
visible project folder.
## Start here
- [Installation](Installation)
- [Getting started](Getting-Started)
- [Core concepts](Core-Concepts)
- [Codex Desktop and agent workflow](Agent-Workflow)
- [Parallel agents and conflict resolution](Parallel-Agents-and-Conflicts)
## Reference and operations
- [CLI reference](CLI-Reference)
- [Architecture](Architecture)
- [Security and privacy](Security-and-Privacy)
- [Troubleshooting](Troubleshooting)
- [Upgrading and uninstalling](Upgrading-and-Uninstalling)
- [Release checklist](Release-Checklist)
## Thirty-second install
macOS or Linux:
```bash
curl -fsSL https://githop.xyz/hop/hop/raw/branch/main/scripts/install.sh | sh
```
Windows PowerShell:
```powershell
irm https://githop.xyz/hop/hop/raw/branch/main/scripts/install.ps1 | iex
```
Restart Codex Desktop, open a Git project, and ask Codex to work normally. The
installed skill activates Hop before the agent inspects or changes the project;
there is no separate prompt router and no required manual `hop init` step.
Hop is currently an alpha. Keep Git history and normal backups, read release
notes before upgrading, and report unexpected behavior with `hop doctor` output
after removing private paths or data.
+99
View File
@@ -0,0 +1,99 @@
# Installation
Packaged binaries are the recommended installation. They need Git 2.40 or
newer; they do not need a local Go toolchain.
## macOS and Linux
```bash
curl -fsSL https://githop.xyz/hop/hop/raw/branch/main/scripts/install.sh | sh
```
The installer:
1. detects macOS/Linux and `amd64`/`arm64`;
2. downloads the matching archive from the latest Gitea Release;
3. downloads `checksums.txt` and verifies SHA-256 before extraction;
4. installs the CLI to `~/.local/bin/hop`;
5. adds `~/.local/bin` to `.zprofile` or `.profile` when necessary; and
6. runs `hop skill install --force` for Codex.
Review before execution:
```bash
curl -fsSLO https://githop.xyz/hop/hop/raw/branch/main/scripts/install.sh
less install.sh
sh install.sh
```
Installer options are environment variables:
| Variable | Default | Purpose |
|---|---|---|
| `HOP_VERSION` | `latest` | Release tag such as `v0.1.0` |
| `HOP_INSTALL_DIR` | `~/.local/bin` | Binary destination |
| `HOP_INSTALL_SKILL` | `1` | Set to `0` to skip Codex skill installation |
| `HOP_MODIFY_PATH` | `1` | Set to `0` to leave shell profiles unchanged |
| `HOP_GITEA_URL` | `https://githop.xyz` | Gitea instance base URL |
| `HOP_REPOSITORY` | `hop/hop` | Alternate Gitea owner/repository |
Example:
```bash
HOP_VERSION=v0.1.0 HOP_INSTALL_DIR="$HOME/bin" sh install.sh
```
## Windows
In PowerShell as your normal user:
```powershell
irm https://githop.xyz/hop/hop/raw/branch/main/scripts/install.ps1 | iex
```
The script verifies the Windows archive, installs to
`%LOCALAPPDATA%\Programs\Hop`, updates the user PATH, and installs the Codex
skill. To pin a version after downloading the script:
```powershell
.\install.ps1 -Version v0.1.0
```
Use `-SkipSkill` only when another agent runtime will receive the skill. Use
`-SkipPath` when another tool manages your PATH.
## Go install
With Go 1.26 or newer:
```bash
go install githop.xyz/hop/hop/cmd/hop@latest
hop skill install --force
```
Put `$(go env GOPATH)/bin` on PATH if `hop` is not found.
## Build from source
```bash
git clone https://githop.xyz/hop/hop.git
cd hop
go test ./...
go build -trimpath -o hop ./cmd/hop
mkdir -p "$HOME/.local/bin"
install -m 755 hop "$HOME/.local/bin/hop"
"$HOME/.local/bin/hop" skill install --force
```
Source builds are intended for contributors and as a pre-release fallback.
## Verify
```bash
hop version
hop help
git --version
```
If Codex Desktop was open during installation, restart it. See
[Getting started](Getting-Started) next.
+49
View File
@@ -0,0 +1,49 @@
# Parallel agents and conflict resolution
Each independent prompt starts from an accepted state and receives its own
worktree. Multiple agents can therefore read, edit, and test the same codebase
without sharing a mutable working directory.
## Compatible work
Hop uses Git's real three-way content merge:
- disjoint files compose automatically;
- independent hunks in the same file compose automatically;
- identical same-file changes coalesce; and
- compatible rename/content and mode/content changes can compose.
Before acceptance, Hop validates the exact integrated tree rather than trusting
tests run only against an agent's stale starting point.
## Genuine conflicts
When Git cannot compose both intents, `hop land` exits with code `20` and
creates a fresh reconciliation attempt. The response includes:
- a reconciliation prompt state;
- a new isolated workspace;
- current accepted and proposed inputs; and
- conflict candidate paths.
The agent switches to that workspace, preserves both compatible intents,
resolves the conflict, runs `hop check`, creates a new proposal, and lands again.
Hop requires successful checked evidence before a reconciliation proposal can
be frozen.
Text conflicts usually contain diff3 markers. Delete/rename, binary, symlink,
mode, and directory conflicts may not, so the agent must inspect both input
states rather than assuming the provisional tree is resolved.
## What still needs a person
The agent should stop only when the source conflict exposes a real product
decision—for example, two prompts require mutually exclusive API behavior and
neither recorded intent determines the right result. Ordinary code overlap is
not user work.
## Visible-root safety
The selected project directory remains at the last accepted state while
reconciliation is underway. If that folder has user-owned divergence, Hop does
not overwrite it; `hop land` exits with code `23` and reports the paths.
+83
View File
@@ -0,0 +1,83 @@
# Release checklist
Hop releases are built on `githop.xyz` by Gitea Actions and uploaded by
GoReleaser as draft Gitea Releases.
The examples assume the canonical repository is `githop.xyz/hop/hop`.
## One-time Gitea setup
- Create the `hop/hop` repository and set its default branch to `main`.
- Configure `origin` as `https://githop.xyz/hop/hop.git`.
- Run a currently patched Gitea 1.26.x or newer, enable Actions, and enable
Actions for the repository. Hop's least-privilege workflow uses the 1.26 job
token permission model.
- Register trusted, isolated `ubuntu-latest` and `windows-latest` act runners.
- Allow the repository job token `code: read` and `releases: write`.
- Ensure `${{ secrets.GITEA_TOKEN }}` can create releases in this repository.
- Keep third-party Actions pinned to reviewed commit SHAs. When upgrading
GoReleaser, update its version and both hard-coded Linux archive checksums in
`.gitea/workflows/release.yml` from the official release checksum file.
- Permit release attachment MIME types for `.tar.gz`, `.zip`, and `.txt` in
Gitea's `[attachment] ALLOWED_TYPES` configuration.
- Enable the repository wiki, then push the files in `wiki/` to its wiki Git
repository.
## Public-launch gates
- Choose and add a `LICENSE`. The release workflow intentionally fails without
one; this is a product/legal decision, not a build default.
- Add `SECURITY.md` with a monitored private disclosure address.
- Create an offline-controlled release-signing key, publish its public key, and
add detached signing for `checksums.txt` before general availability.
- Confirm the `githop.xyz/hop/hop` Go import path serves valid `go-import`
metadata.
- Back up the Gitea database, repositories, release attachments, and Actions
secrets.
## Validate before tagging
```bash
go test -race ./...
go vet ./...
sh -n scripts/install.sh
goreleaser check
goreleaser release --snapshot --clean
```
Inspect `dist/` and test at least one archive on each operating system family.
Confirm `hop version` reports the snapshot/tag-injected version and
`hop skill install --force` installs usable skill files.
## Create a release
1. Update release notes and the expected version.
2. Create a signed semantic-version tag such as `v0.1.0-alpha.1`.
3. Push the tag to `githop.xyz`.
4. The `.gitea/workflows/release.yml` workflow runs race tests and vet, builds
six platform archives, generates `checksums.txt`, and uploads a draft.
5. Download the draft assets and independently verify checksums, version output,
skill installation, and a disposable Hop project.
6. Publish the Gitea draft only after those checks pass.
7. Test both one-command installers against the now-published release.
## Expected assets
```text
hop_darwin_amd64.tar.gz
hop_darwin_arm64.tar.gz
hop_linux_amd64.tar.gz
hop_linux_arm64.tar.gz
hop_windows_amd64.zip
hop_windows_arm64.zip
checksums.txt
```
## After the first release
- Create a Gitea-hosted Homebrew tap/cask fed by immutable release URLs and
checksums; do not publish placeholder hashes.
- Add Windows package-manager metadata only after the Windows artifact has been
tested on a real signed build.
- Establish release retention, package cleanup, rollback, and incident-response
procedures for the custom Gitea instance.
+52
View File
@@ -0,0 +1,52 @@
# Security and privacy
## Local data
Hop stores state in the project under `.hop/`. Prompt text, source trees,
commands, and check output are local unless the project or its filesystem is
copied elsewhere. SQLite data is not encrypted at rest.
`.hop/` is excluded through `.git/info/exclude`, so ordinary Git operations do
not publish it. Initialization refuses to hide a `.hop` directory that the
project already tracks.
## Credential redaction
Before persistence, Hop redacts high-confidence provider keys, contextual
tokens/passwords, private keys, authorization headers, and credential-bearing
URLs. The same sanitizer is applied to proposal summaries and recorded check
commands/output.
Detection is defense in depth, not a guarantee. Use environment variables or a
secret manager. Rotate any real credential pasted into any agent prompt even
when Hop reports a redaction.
## Installer and release integrity
Packaged installers download `checksums.txt` from the same published Gitea
Release and verify the selected archive before extraction. Gitea Releases are
created as drafts, after race tests, vetting, and cross-platform builds, then
must be reviewed before publication.
For stronger provenance before general availability, the release owner should
sign `checksums.txt` with an offline-controlled release key and publish the
public key independently. Checksum signing is listed as a launch gate in the
[release checklist](Release-Checklist).
## Runner trust
Release jobs execute on Gitea act runners. Only trusted, isolated runners should
receive release tags or release tokens. Do not run secret-bearing release jobs
from unreviewed fork pull requests.
## Filesystem safety
Hop does not use `reset --hard`, move the active branch, or write the user's
real Git index. Visible-root synchronization fails closed when files, ignored
destinations, or staged state could be overwritten.
## Reporting a vulnerability
Before the public security contact is configured, disclose vulnerabilities
privately to the repository owner rather than opening a public issue. Add a
`SECURITY.md` with the final contact before the first public release.
+77
View File
@@ -0,0 +1,77 @@
# Troubleshooting
## `hop: command not found`
Open a new terminal after installation. Confirm the install directory is on
PATH:
```bash
command -v hop
printf '%s\n' "$PATH"
```
The default Unix location is `~/.local/bin`. On Windows it is
`%LOCALAPPDATA%\Programs\Hop`.
## Codex does not activate Hop
```bash
hop skill install --force
```
Restart Codex Desktop. Mention `$hop` in a task as a deterministic activation
fallback.
## The installer cannot find a release
Only published Gitea Releases appear through the public releases API. Drafts
and an instance that is not live will return an error; published prereleases
are supported. Pin an existing tag with `HOP_VERSION`, or use the source build
until the first release is published.
## Git is too old
Hop requires Git 2.40 or newer for structured, explicit-base `merge-tree`
behavior:
```bash
git --version
```
Upgrade Git through the operating system package manager before retrying.
## Exit code 20: merge conflict
The agent should adopt the returned reconciliation prompt and workspace,
resolve both intents, run `hop check`, propose, and land again. Users should not
need to perform ordinary source merges.
## Exit code 22: validation failed
The accepted head did not advance. Inspect the recorded output, fix the attempt
workspace, and rerun the check.
## Exit code 23 or `Root: diverged`
Hop found visible files or index state it will not overwrite. Preserve those
changes. Capture them as a new Hop task or resolve them intentionally, then
retry `hop land`. Do not bypass this with `hop accept` in Desktop workflows.
## Internal ref or object warning
```bash
hop doctor
```
If SQLite is healthy and the report specifically identifies derived refs:
```bash
hop doctor --repair
```
Do not repair while a final validation command is running.
## A secret was pasted
Rotate it. Hop redaction reduces durable exposure but cannot prove that every
credential format, attachment, agent log, or external system omitted the value.
+70
View File
@@ -0,0 +1,70 @@
# Upgrading and uninstalling
## Upgrade packaged installations
Rerun the installer. It replaces the binary and refreshes the embedded skill:
```bash
curl -fsSL https://githop.xyz/hop/hop/raw/branch/main/scripts/install.sh | sh
```
Pin a release when required:
```bash
curl -fsSL https://githop.xyz/hop/hop/raw/branch/main/scripts/install.sh | \
HOP_VERSION=v0.1.0 sh
```
Windows:
```powershell
irm https://githop.xyz/hop/hop/raw/branch/main/scripts/install.ps1 -OutFile install.ps1
.\install.ps1 -Version v0.1.0
Remove-Item install.ps1
```
After upgrading:
```bash
hop version
hop skill install --force
hop doctor
```
Restart Codex Desktop when the installed skill changed.
## Upgrade Go installations
```bash
go install githop.xyz/hop/hop/cmd/hop@latest
hop skill install --force
```
## Project migrations
Hop opens and migrates older supported SQLite schemas automatically. Back up
important repositories before alpha upgrades and read the release notes for any
one-way schema change.
## Uninstall the CLI and skill
Unix default:
```bash
rm -f "$HOME/.local/bin/hop"
rm -rf "${CODEX_HOME:-$HOME/.codex}/skills/hop"
```
Windows PowerShell:
```powershell
Remove-Item -Force "$env:LOCALAPPDATA\Programs\Hop\hop.exe"
Remove-Item -Recurse -Force "$HOME\.codex\skills\hop"
```
Remove the Hop install directory from PATH if it is no longer used.
Uninstalling the program does not delete project-local `.hop/` histories. That
is intentional, so reinstalling restores access. Deleting a project's `.hop/`
directory permanently removes its prompt graph, evidence, workspaces, and
accepted Hop history; make a backup and treat that as destructive data removal.
+14
View File
@@ -0,0 +1,14 @@
## Hop
- [Home](Home)
- [Installation](Installation)
- [Getting started](Getting-Started)
- [Core concepts](Core-Concepts)
- [Agent workflow](Agent-Workflow)
- [Parallel agents and conflicts](Parallel-Agents-and-Conflicts)
- [CLI reference](CLI-Reference)
- [Architecture](Architecture)
- [Security and privacy](Security-and-Privacy)
- [Troubleshooting](Troubleshooting)
- [Upgrading and uninstalling](Upgrading-and-Uninstalling)
- [Release checklist](Release-Checklist)