From de99aad4d4fc0fc6d5ae9b97465a81ca1803fdf7 Mon Sep 17 00:00:00 2001 From: Hop Date: Sat, 11 Jul 2026 12:10:59 -0700 Subject: [PATCH] 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 --- .gitea/workflows/ci.yml | 67 +++++++++++++++ .gitea/workflows/release.yml | 59 +++++++++++++ .goreleaser.yaml | 71 ++++++++++++++++ README.md | 116 ++++++++++++++++++++++---- cmd/hop/main.go | 2 +- go.mod | 2 +- go.sum | 26 ++++++ internal/hop/cli.go | 20 ++++- internal/hop/docs_test.go | 67 +++++++++++++++ internal/hop/service_smoke_test.go | 18 ++++ internal/hop/skill.go | 2 +- scripts/install.ps1 | 84 +++++++++++++++++++ scripts/install.sh | 107 ++++++++++++++++++++++++ scripts/test-install.ps1 | 72 ++++++++++++++++ scripts/test-install.sh | 83 ++++++++++++++++++ wiki/Agent-Workflow.md | 65 +++++++++++++++ wiki/Architecture.md | 54 ++++++++++++ wiki/CLI-Reference.md | 64 ++++++++++++++ wiki/Core-Concepts.md | 58 +++++++++++++ wiki/Getting-Started.md | 60 +++++++++++++ wiki/Home.md | 45 ++++++++++ wiki/Installation.md | 99 ++++++++++++++++++++++ wiki/Parallel-Agents-and-Conflicts.md | 49 +++++++++++ wiki/Release-Checklist.md | 83 ++++++++++++++++++ wiki/Security-and-Privacy.md | 52 ++++++++++++ wiki/Troubleshooting.md | 77 +++++++++++++++++ wiki/Upgrading-and-Uninstalling.md | 70 ++++++++++++++++ wiki/_Sidebar.md | 14 ++++ 28 files changed, 1565 insertions(+), 21 deletions(-) create mode 100644 .gitea/workflows/ci.yml create mode 100644 .gitea/workflows/release.yml create mode 100644 .goreleaser.yaml create mode 100644 internal/hop/docs_test.go create mode 100644 scripts/install.ps1 create mode 100755 scripts/install.sh create mode 100644 scripts/test-install.ps1 create mode 100755 scripts/test-install.sh create mode 100644 wiki/Agent-Workflow.md create mode 100644 wiki/Architecture.md create mode 100644 wiki/CLI-Reference.md create mode 100644 wiki/Core-Concepts.md create mode 100644 wiki/Getting-Started.md create mode 100644 wiki/Home.md create mode 100644 wiki/Installation.md create mode 100644 wiki/Parallel-Agents-and-Conflicts.md create mode 100644 wiki/Release-Checklist.md create mode 100644 wiki/Security-and-Privacy.md create mode 100644 wiki/Troubleshooting.md create mode 100644 wiki/Upgrading-and-Uninstalling.md create mode 100644 wiki/_Sidebar.md diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..0f0ca43 --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -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 diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml new file mode 100644 index 0000000..83a0f99 --- /dev/null +++ b/.gitea/workflows/release.yml @@ -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 diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 0000000..00b6dea --- /dev/null +++ b/.goreleaser.yaml @@ -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 diff --git a/README.md b/README.md index bc5d433..39ab24c 100644 --- a/README.md +++ b/README.md @@ -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 user’s 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 agent’s 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: diff --git a/cmd/hop/main.go b/cmd/hop/main.go index c4fe403..8094290 100644 --- a/cmd/hop/main.go +++ b/cmd/hop/main.go @@ -3,7 +3,7 @@ package main import ( "os" - "github.com/hop-vcs/hop/internal/hop" + "githop.xyz/hop/hop/internal/hop" ) func main() { diff --git a/go.mod b/go.mod index 8464255..ace2063 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/hop-vcs/hop +module githop.xyz/hop/hop go 1.26 diff --git a/go.sum b/go.sum index 7aee46f..4ffbac8 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/internal/hop/cli.go b/internal/hop/cli.go index 784cdba..687b0e3 100644 --- a/internal/hop/cli.go +++ b/internal/hop/cli.go @@ -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 } diff --git a/internal/hop/docs_test.go b/internal/hop/docs_test.go new file mode 100644 index 0000000..4ef3fba --- /dev/null +++ b/internal/hop/docs_test.go @@ -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) + } + } +} diff --git a/internal/hop/service_smoke_test.go b/internal/hop/service_smoke_test.go index cbd0ad2..ff8d93a 100644 --- a/internal/hop/service_smoke_test.go +++ b/internal/hop/service_smoke_test.go @@ -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"}) diff --git a/internal/hop/skill.go b/internal/hop/skill.go index cce0489..1069b06 100644 --- a/internal/hop/skill.go +++ b/internal/hop/skill.go @@ -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 { diff --git a/scripts/install.ps1 b/scripts/install.ps1 new file mode 100644 index 0000000..ffc40fc --- /dev/null +++ b/scripts/install.ps1 @@ -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 +} diff --git a/scripts/install.sh b/scripts/install.sh new file mode 100755 index 0000000..5156e33 --- /dev/null +++ b/scripts/install.sh @@ -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 diff --git a/scripts/test-install.ps1 b/scripts/test-install.ps1 new file mode 100644 index 0000000..3de413f --- /dev/null +++ b/scripts/test-install.ps1 @@ -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 +} diff --git a/scripts/test-install.sh b/scripts/test-install.sh new file mode 100755 index 0000000..c41b18b --- /dev/null +++ b/scripts/test-install.sh @@ -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 +} diff --git a/wiki/Agent-Workflow.md b/wiki/Agent-Workflow.md new file mode 100644 index 0000000..7fee7a9 --- /dev/null +++ b/wiki/Agent-Workflow.md @@ -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' + +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. diff --git a/wiki/Architecture.md b/wiki/Architecture.md new file mode 100644 index 0000000..a427568 --- /dev/null +++ b/wiki/Architecture.md @@ -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). diff --git a/wiki/CLI-Reference.md b/wiki/CLI-Reference.md new file mode 100644 index 0000000..bc803fa --- /dev/null +++ b/wiki/CLI-Reference.md @@ -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. diff --git a/wiki/Core-Concepts.md b/wiki/Core-Concepts.md new file mode 100644 index 0000000..756e3bb --- /dev/null +++ b/wiki/Core-Concepts.md @@ -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. diff --git a/wiki/Getting-Started.md b/wiki/Getting-Started.md new file mode 100644 index 0000000..9839a47 --- /dev/null +++ b/wiki/Getting-Started.md @@ -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). diff --git a/wiki/Home.md b/wiki/Home.md new file mode 100644 index 0000000..7240d1f --- /dev/null +++ b/wiki/Home.md @@ -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. diff --git a/wiki/Installation.md b/wiki/Installation.md new file mode 100644 index 0000000..5b36109 --- /dev/null +++ b/wiki/Installation.md @@ -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. diff --git a/wiki/Parallel-Agents-and-Conflicts.md b/wiki/Parallel-Agents-and-Conflicts.md new file mode 100644 index 0000000..a51b263 --- /dev/null +++ b/wiki/Parallel-Agents-and-Conflicts.md @@ -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. diff --git a/wiki/Release-Checklist.md b/wiki/Release-Checklist.md new file mode 100644 index 0000000..81af57e --- /dev/null +++ b/wiki/Release-Checklist.md @@ -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. diff --git a/wiki/Security-and-Privacy.md b/wiki/Security-and-Privacy.md new file mode 100644 index 0000000..da7c77c --- /dev/null +++ b/wiki/Security-and-Privacy.md @@ -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. diff --git a/wiki/Troubleshooting.md b/wiki/Troubleshooting.md new file mode 100644 index 0000000..cd4588e --- /dev/null +++ b/wiki/Troubleshooting.md @@ -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. diff --git a/wiki/Upgrading-and-Uninstalling.md b/wiki/Upgrading-and-Uninstalling.md new file mode 100644 index 0000000..188e56d --- /dev/null +++ b/wiki/Upgrading-and-Uninstalling.md @@ -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. diff --git a/wiki/_Sidebar.md b/wiki/_Sidebar.md new file mode 100644 index 0000000..d877ea9 --- /dev/null +++ b/wiki/_Sidebar.md @@ -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)