Install portable agent-neutral Hop skill and documentation

Hop-State: A_06FN69X2VW0BAXG4A4DT6V0
Hop-Proposal: R_06FN69VET1STZNKPWK913K8
Hop-Task: T_06FN637799RW5Q7WH9H6PJR
Hop-Attempt: AT_06FN63779BJRBK1VFQ94GRR
This commit is contained in:
Hop
2026-07-11 14:48:54 -07:00
parent 2f00b2dd3f
commit 68d8bf2f7d
20 changed files with 649 additions and 124 deletions
+17 -8
View File
@@ -30,9 +30,11 @@ and safe multi-agent integration.
prompt → durable intent → isolated agent work → validate + merge → accepted code prompt → durable intent → isolated agent work → validate + merge → accepted code
``` ```
You keep using Codex Desktop normally. The installed Hop skill handles this Hop ships an open [Agent Skills](https://agentskills.io/) bundle and a controller
lifecycle for the agent—there is no prompt wrapper, special app, or manual protocol. The skill makes prompt capture the agent's first repository action; a
branch workflow. controller can capture before model delivery. Both use the same state,
workspace, validation, reconciliation, and landing protocol. Codex Desktop is
one bundled integration, not a boundary of the product.
## Install ## Install
@@ -50,21 +52,28 @@ curl -fsSL https://githop.xyz/GnosysLabs/Hop/raw/branch/main/scripts/install.sh
irm https://githop.xyz/GnosysLabs/Hop/raw/branch/main/scripts/install.ps1 | iex irm https://githop.xyz/GnosysLabs/Hop/raw/branch/main/scripts/install.ps1 | iex
``` ```
The installer adds both the Hop CLI and its Codex skill. For source builds, The installer adds the Hop CLI and writes the same embedded skill version to
version pinning, custom locations, and verification, see the `~/.agents/skills/hop` and `${CODEX_HOME:-~/.codex}/skills/hop`. For CLI-only
installation, source builds, version pinning, custom locations, and
verification, see the
[installation guide](https://githop.xyz/GnosysLabs/Hop/wiki/Installation). [installation guide](https://githop.xyz/GnosysLabs/Hop/wiki/Installation).
## Get started ## Get started
1. Install Hop. 1. Install Hop.
2. Restart Codex Desktop if it was already open. 2. Open a Git project in an Agent Skills-compatible client, or make it the
3. Open a Git project in Codex Desktop. controller's working directory.
4. Ask Codex to make a change as you normally would. 3. Ask the agent to make a change as you normally would.
That is the full user workflow. You do not run `hop init`, route prompts through That is the full user workflow. You do not run `hop init`, route prompts through
a terminal, or work inside `.hop` yourself. After a task, `hop status` shows the a terminal, or work inside `.hop` yourself. After a task, `hop status` shows the
accepted state and whether the visible project folder is synchronized. accepted state and whether the visible project folder is synchronized.
For example, Codex Desktop users restart Codex after installation, select a Git
project, and prompt normally. Other compatible runtimes can read the shared
skill bundle or receive a single-target installation with the explicit
`hop skill install --path /path/to/agent/skills --force` form.
Hop is currently an early alpha. Expect its state model and CLI to evolve before Hop is currently an early alpha. Expect its state model and CLI to evolve before
1.0. 1.0.
+10 -2
View File
@@ -528,14 +528,22 @@ func runSkillCLI(args []string, jsonOutput bool, stdout, stderr io.Writer) int {
if err := fs.Parse(args[1:]); err != nil || len(fs.Args()) != 0 { if err := fs.Parse(args[1:]); err != nil || len(fs.Args()) != 0 {
return 2 return 2
} }
result, err := InstallSkill(*path, *force) var result SkillInstallResult
var err error
if strings.TrimSpace(*path) == "" {
result, err = InstallDefaultSkills(*force)
} else {
result, err = InstallSkill(*path, *force)
}
if err != nil { if err != nil {
return printCLIError(err, jsonOutput, stdout, stderr) return printCLIError(err, jsonOutput, stdout, stderr)
} }
if jsonOutput { if jsonOutput {
writeJSON(stdout, map[string]any{"ok": true, "data": result}) writeJSON(stdout, map[string]any{"ok": true, "data": result})
} else { } else {
fmt.Fprintf(stdout, "Installed Hop skill at %s\n", result.Path) for _, installedPath := range result.Paths {
fmt.Fprintf(stdout, "Installed Hop skill at %s\n", installedPath)
}
} }
return 0 return 0
case "print": case "print":
+184 -10
View File
@@ -12,6 +12,7 @@ import (
type SkillInstallResult struct { type SkillInstallResult struct {
Path string `json:"path"` Path string `json:"path"`
Paths []string `json:"paths,omitempty"`
Files []string `json:"files"` Files []string `json:"files"`
} }
@@ -27,6 +28,26 @@ func DefaultSkillBase() (string, error) {
return filepath.Join(home, "skills"), nil return filepath.Join(home, "skills"), nil
} }
func DefaultSharedSkillBase() (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("find home directory: %w", err)
}
return filepath.Join(home, ".agents", "skills"), nil
}
func DefaultSkillBases() ([]string, error) {
codex, err := DefaultSkillBase()
if err != nil {
return nil, err
}
shared, err := DefaultSharedSkillBase()
if err != nil {
return nil, err
}
return []string{codex, shared}, nil
}
func EmbeddedSkillText() (string, error) { func EmbeddedSkillText() (string, error) {
contents, err := hopskill.Files.ReadFile("SKILL.md") contents, err := hopskill.Files.ReadFile("SKILL.md")
if err != nil { if err != nil {
@@ -35,9 +56,10 @@ func EmbeddedSkillText() (string, error) {
return string(contents), nil return string(contents), nil
} }
// InstallSkill writes the embedded skill below a skills directory. Existing // InstallSkill writes the embedded skill below one skills directory. Existing
// bundles require force; known files are overwritten without deleting unknown // bundles require force; known files are overwritten without deleting unknown
// user files from the destination. // user files from the destination. An empty base retains the legacy Codex
// destination; the CLI uses InstallDefaultSkills for its no-path behavior.
func InstallSkill(base string, force bool) (SkillInstallResult, error) { func InstallSkill(base string, force bool) (SkillInstallResult, error) {
if strings.TrimSpace(base) == "" { if strings.TrimSpace(base) == "" {
var err error var err error
@@ -46,30 +68,182 @@ func InstallSkill(base string, force bool) (SkillInstallResult, error) {
return SkillInstallResult{}, err return SkillInstallResult{}, err
} }
} }
target, err := resolveSkillTarget(base)
if err != nil {
return SkillInstallResult{}, err
}
if err := preflightSkillTarget(target, force); err != nil {
return SkillInstallResult{}, err
}
return writeSkillTarget(target)
}
// InstallDefaultSkills installs the same embedded files in the client-specific
// Codex directory and the cross-client .agents directory. Destinations are
// canonicalized and preflighted before any write to avoid partial upgrades when
// one existing bundle requires --force.
func InstallDefaultSkills(force bool) (SkillInstallResult, error) {
bases, err := DefaultSkillBases()
if err != nil {
return SkillInstallResult{}, err
}
var targets []string
seen := make(map[string]struct{}, len(bases))
for _, base := range bases {
target, err := resolveSkillTarget(base)
if err != nil {
return SkillInstallResult{}, err
}
key, err := canonicalPathKey(target)
if err != nil {
return SkillInstallResult{}, err
}
if _, exists := seen[key]; exists {
continue
}
seen[key] = struct{}{}
targets = append(targets, target)
}
for _, target := range targets {
if err := preflightSkillTarget(target, force); err != nil {
return SkillInstallResult{}, err
}
}
var combined SkillInstallResult
for _, target := range targets {
installed, err := writeSkillTarget(target)
if err != nil {
return SkillInstallResult{}, err
}
if combined.Path == "" {
combined.Path = installed.Path
combined.Files = installed.Files
}
combined.Paths = append(combined.Paths, installed.Path)
}
return combined, nil
}
func resolveSkillTarget(base string) (string, error) {
absBase, err := filepath.Abs(base) absBase, err := filepath.Abs(base)
if err != nil { if err != nil {
return SkillInstallResult{}, fmt.Errorf("resolve skills directory: %w", err) return "", fmt.Errorf("resolve skills directory: %w", err)
}
return filepath.Join(absBase, "hop"), nil
}
func canonicalPathKey(path string) (string, error) {
if err := preflightSkillAncestors(path); err != nil {
return "", err
}
current := filepath.Clean(path)
var suffix []string
for {
resolved, err := filepath.EvalSymlinks(current)
if err == nil {
for index := len(suffix) - 1; index >= 0; index-- {
resolved = filepath.Join(resolved, suffix[index])
}
return filepath.Clean(resolved), nil
}
if !os.IsNotExist(err) {
return "", fmt.Errorf("canonicalize skill destination: %w", err)
}
parent := filepath.Dir(current)
if parent == current {
return filepath.Clean(path), nil
}
suffix = append(suffix, filepath.Base(current))
current = parent
}
}
func preflightSkillTarget(target string, force bool) error {
if err := preflightSkillAncestors(target); err != nil {
return err
} }
target := filepath.Join(absBase, "hop")
if info, err := os.Lstat(target); err == nil { if info, err := os.Lstat(target); err == nil {
if info.Mode()&os.ModeSymlink != 0 { if info.Mode()&os.ModeSymlink != 0 {
return SkillInstallResult{}, fmt.Errorf("refusing to install through symlink %s", target) return fmt.Errorf("refusing to install through symlink %s", target)
} }
if !info.IsDir() { if !info.IsDir() {
return SkillInstallResult{}, fmt.Errorf("skill target exists and is not a directory: %s", target) return fmt.Errorf("skill target exists and is not a directory: %s", target)
} }
if !force { if !force {
return SkillInstallResult{}, fmt.Errorf("Hop skill already exists at %s; pass --force to update it", target) return fmt.Errorf("Hop skill already exists at %s; pass --force to update it", target)
} }
} else if !os.IsNotExist(err) { } else if !os.IsNotExist(err) {
return SkillInstallResult{}, fmt.Errorf("inspect skill target: %w", err) return fmt.Errorf("inspect skill target: %w", err)
} }
return fs.WalkDir(hopskill.Files, ".", func(path string, entry fs.DirEntry, walkErr error) error {
if walkErr != nil || path == "." {
return walkErr
}
destination := filepath.Join(target, filepath.FromSlash(path))
info, err := os.Lstat(destination)
if os.IsNotExist(err) {
return nil
}
if err != nil {
return fmt.Errorf("inspect skill destination %s: %w", destination, err)
}
if info.Mode()&os.ModeSymlink != 0 {
return fmt.Errorf("refusing to overwrite symlink %s", destination)
}
if entry.IsDir() && !info.IsDir() {
return fmt.Errorf("skill directory destination is not a directory: %s", destination)
}
if !entry.IsDir() && info.IsDir() {
return fmt.Errorf("skill file destination is a directory: %s", destination)
}
return nil
})
}
// preflightSkillAncestors finds the nearest existing path component before any
// writes. This distinguishes an ordinary missing destination from a dangling
// parent symlink, which filepath.EvalSymlinks otherwise reports as os.ErrNotExist.
func preflightSkillAncestors(path string) error {
target := filepath.Clean(path)
current := target
for {
info, err := os.Lstat(current)
if err == nil {
if info.Mode()&os.ModeSymlink != 0 {
resolved, resolveErr := filepath.EvalSymlinks(current)
if resolveErr != nil {
return fmt.Errorf("resolve skill destination ancestor %s: %w", current, resolveErr)
}
resolvedInfo, statErr := os.Stat(resolved)
if statErr != nil {
return fmt.Errorf("inspect resolved skill destination ancestor %s: %w", current, statErr)
}
if current != target && !resolvedInfo.IsDir() {
return fmt.Errorf("skill destination ancestor is not a directory: %s", current)
}
} else if current != target && !info.IsDir() {
return fmt.Errorf("skill destination ancestor is not a directory: %s", current)
}
return nil
}
if !os.IsNotExist(err) {
return fmt.Errorf("inspect skill destination ancestor %s: %w", current, err)
}
parent := filepath.Dir(current)
if parent == current {
return nil
}
current = parent
}
}
func writeSkillTarget(target string) (SkillInstallResult, error) {
if err := os.MkdirAll(target, 0o755); err != nil { if err := os.MkdirAll(target, 0o755); err != nil {
return SkillInstallResult{}, fmt.Errorf("create skill target: %w", err) return SkillInstallResult{}, fmt.Errorf("create skill target: %w", err)
} }
result := SkillInstallResult{Path: target} result := SkillInstallResult{Path: target, Paths: []string{target}}
err = fs.WalkDir(hopskill.Files, ".", func(path string, entry fs.DirEntry, walkErr error) error { err := fs.WalkDir(hopskill.Files, ".", func(path string, entry fs.DirEntry, walkErr error) error {
if walkErr != nil { if walkErr != nil {
return walkErr return walkErr
} }
+155 -2
View File
@@ -1,8 +1,10 @@
package hop package hop
import ( import (
"bytes"
"os" "os"
"path/filepath" "path/filepath"
"runtime"
"strings" "strings"
"testing" "testing"
) )
@@ -16,6 +18,9 @@ func TestInstallSkillBundle(t *testing.T) {
if result.Path != filepath.Join(base, "hop") { if result.Path != filepath.Join(base, "hop") {
t.Fatalf("skill path = %s", result.Path) t.Fatalf("skill path = %s", result.Path)
} }
if len(result.Paths) != 1 || result.Paths[0] != result.Path {
t.Fatalf("skill paths = %#v, want only %s", result.Paths, result.Path)
}
wantFiles := []string{"SKILL.md", "agents/openai.yaml", "references/protocol.md"} wantFiles := []string{"SKILL.md", "agents/openai.yaml", "references/protocol.md"}
for _, relative := range wantFiles { for _, relative := range wantFiles {
contents, err := os.ReadFile(filepath.Join(result.Path, relative)) contents, err := os.ReadFile(filepath.Join(result.Path, relative))
@@ -31,7 +36,7 @@ func TestInstallSkillBundle(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
if !strings.Contains(string(metadata), "allow_implicit_invocation: true") { if !strings.Contains(string(metadata), "allow_implicit_invocation: true") {
t.Fatal("installed skill does not permit Desktop implicit invocation") t.Fatal("installed OpenAI metadata does not permit implicit invocation")
} }
skill, err := os.ReadFile(filepath.Join(result.Path, "SKILL.md")) skill, err := os.ReadFile(filepath.Join(result.Path, "SKILL.md"))
if err != nil { if err != nil {
@@ -58,9 +63,149 @@ func TestInstallSkillBundle(t *testing.T) {
} }
} }
func TestInstallDefaultSkillsWritesSharedAndCodexBundles(t *testing.T) {
home := t.TempDir()
codexHome := filepath.Join(home, "codex-home")
t.Setenv("HOME", home)
t.Setenv("USERPROFILE", home)
t.Setenv("CODEX_HOME", codexHome)
result, err := InstallDefaultSkills(false)
if err != nil {
t.Fatal(err)
}
codexTarget := filepath.Join(codexHome, "skills", "hop")
sharedTarget := filepath.Join(home, ".agents", "skills", "hop")
if result.Path != codexTarget {
t.Fatalf("legacy primary path = %s, want %s", result.Path, codexTarget)
}
wantPaths := []string{codexTarget, sharedTarget}
if len(result.Paths) != len(wantPaths) {
t.Fatalf("default paths = %#v, want %#v", result.Paths, wantPaths)
}
for index, want := range wantPaths {
if result.Paths[index] != want {
t.Fatalf("default path %d = %s, want %s", index, result.Paths[index], want)
}
}
codexSkill, err := os.ReadFile(filepath.Join(codexTarget, "SKILL.md"))
if err != nil {
t.Fatal(err)
}
sharedSkill, err := os.ReadFile(filepath.Join(sharedTarget, "SKILL.md"))
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(codexSkill, sharedSkill) {
t.Fatal("default skill bundles differ")
}
}
func TestInstallDefaultSkillsPreflightsAndDeduplicates(t *testing.T) {
t.Run("preflight", func(t *testing.T) {
home := t.TempDir()
codexHome := filepath.Join(home, "codex-home")
t.Setenv("HOME", home)
t.Setenv("USERPROFILE", home)
t.Setenv("CODEX_HOME", codexHome)
codexTarget := filepath.Join(codexHome, "skills", "hop")
if err := os.MkdirAll(codexTarget, 0o755); err != nil {
t.Fatal(err)
}
unknown := filepath.Join(codexTarget, "user-note.txt")
if err := os.WriteFile(unknown, []byte("keep me"), 0o644); err != nil {
t.Fatal(err)
}
if _, err := InstallDefaultSkills(false); err == nil || !strings.Contains(err.Error(), "--force") {
t.Fatalf("default preflight error = %v", err)
}
sharedTarget := filepath.Join(home, ".agents", "skills", "hop")
if _, err := os.Stat(sharedTarget); !os.IsNotExist(err) {
t.Fatalf("partial shared install exists after preflight failure: %v", err)
}
if _, err := InstallDefaultSkills(true); err != nil {
t.Fatal(err)
}
if contents, err := os.ReadFile(unknown); err != nil || string(contents) != "keep me" {
t.Fatalf("force install removed unknown file: %q, %v", string(contents), err)
}
})
t.Run("nested symlink", func(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("symlink creation requires additional privileges on Windows")
}
home := t.TempDir()
codexHome := filepath.Join(home, "codex-home")
t.Setenv("HOME", home)
t.Setenv("USERPROFILE", home)
t.Setenv("CODEX_HOME", codexHome)
sharedTarget := filepath.Join(home, ".agents", "skills", "hop")
if err := os.MkdirAll(sharedTarget, 0o755); err != nil {
t.Fatal(err)
}
outside := filepath.Join(home, "outside")
if err := os.WriteFile(outside, []byte("do not overwrite"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.Symlink(outside, filepath.Join(sharedTarget, "SKILL.md")); err != nil {
t.Fatal(err)
}
if _, err := InstallDefaultSkills(true); err == nil || !strings.Contains(err.Error(), "symlink") {
t.Fatalf("nested symlink preflight error = %v", err)
}
codexTarget := filepath.Join(codexHome, "skills", "hop")
if _, err := os.Stat(codexTarget); !os.IsNotExist(err) {
t.Fatalf("partial Codex install exists after nested preflight failure: %v", err)
}
if contents, err := os.ReadFile(outside); err != nil || string(contents) != "do not overwrite" {
t.Fatalf("nested symlink target changed: %q, %v", contents, err)
}
})
t.Run("dangling parent symlink", func(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("symlink creation requires additional privileges on Windows")
}
home := t.TempDir()
codexHome := filepath.Join(home, "codex-home")
t.Setenv("HOME", home)
t.Setenv("USERPROFILE", home)
t.Setenv("CODEX_HOME", codexHome)
if err := os.Symlink(filepath.Join(home, "missing-agents-home"), filepath.Join(home, ".agents")); err != nil {
t.Fatal(err)
}
if _, err := InstallDefaultSkills(true); err == nil || !strings.Contains(err.Error(), "ancestor") {
t.Fatalf("dangling parent preflight error = %v", err)
}
codexTarget := filepath.Join(codexHome, "skills", "hop")
if _, err := os.Stat(codexTarget); !os.IsNotExist(err) {
t.Fatalf("partial Codex install exists after dangling-parent failure: %v", err)
}
})
t.Run("deduplicate", func(t *testing.T) {
home := t.TempDir()
t.Setenv("HOME", home)
t.Setenv("USERPROFILE", home)
t.Setenv("CODEX_HOME", filepath.Join(home, ".agents"))
result, err := InstallDefaultSkills(false)
if err != nil {
t.Fatal(err)
}
if len(result.Paths) != 1 {
t.Fatalf("aliased default paths were not deduplicated: %#v", result.Paths)
}
})
}
func TestSkillCLIWorksOutsideHopProject(t *testing.T) { func TestSkillCLIWorksOutsideHopProject(t *testing.T) {
var stdout, stderr strings.Builder var stdout, stderr strings.Builder
base := t.TempDir() home := t.TempDir()
t.Setenv("HOME", home)
t.Setenv("USERPROFILE", home)
t.Setenv("CODEX_HOME", filepath.Join(home, "codex-home"))
base := filepath.Join(home, "custom-skills")
code := RunCLI([]string{"skill", "install", "--path", base, "--json"}, &stdout, &stderr) code := RunCLI([]string{"skill", "install", "--path", base, "--json"}, &stdout, &stderr)
if code != 0 { if code != 0 {
t.Fatalf("skill install exited %d: %s", code, stderr.String()) t.Fatalf("skill install exited %d: %s", code, stderr.String())
@@ -68,6 +213,14 @@ func TestSkillCLIWorksOutsideHopProject(t *testing.T) {
if !strings.Contains(stdout.String(), filepath.Join(base, "hop")) { if !strings.Contains(stdout.String(), filepath.Join(base, "hop")) {
t.Fatalf("skill install JSON omitted target: %s", stdout.String()) t.Fatalf("skill install JSON omitted target: %s", stdout.String())
} }
for _, unexpected := range []string{
filepath.Join(home, ".agents", "skills", "hop"),
filepath.Join(home, "codex-home", "skills", "hop"),
} {
if _, err := os.Stat(unexpected); !os.IsNotExist(err) {
t.Fatalf("explicit --path also installed default target %s: %v", unexpected, err)
}
}
stdout.Reset() stdout.Reset()
stderr.Reset() stderr.Reset()
code = RunCLI([]string{"skill", "print"}, &stdout, &stderr) code = RunCLI([]string{"skill", "print"}, &stdout, &stderr)
+4 -1
View File
@@ -73,11 +73,14 @@ try {
if (-not $SkipSkill) { if (-not $SkipSkill) {
& $installedBinary skill install --force & $installedBinary skill install --force
if ($LASTEXITCODE -ne 0) {
throw "Hop skill installation failed with exit code $LASTEXITCODE"
}
} }
Write-Host "Installed $(& $installedBinary version)" Write-Host "Installed $(& $installedBinary version)"
Write-Host "Binary: $installedBinary" Write-Host "Binary: $installedBinary"
if (-not $SkipSkill) { if (-not $SkipSkill) {
Write-Host "Restart Codex if it is open, then use it normally in any Git repository." Write-Host "Restart any open agent application, then use it normally in any Git repository."
} }
} finally { } finally {
Remove-Item -Recurse -Force -ErrorAction SilentlyContinue $tempDir Remove-Item -Recurse -Force -ErrorAction SilentlyContinue $tempDir
+1 -1
View File
@@ -103,5 +103,5 @@ fi
printf 'Installed %s\n' "$("$install_dir/hop" version)" printf 'Installed %s\n' "$("$install_dir/hop" version)"
printf 'Binary: %s\n' "$install_dir/hop" printf 'Binary: %s\n' "$install_dir/hop"
if [ "$install_skill" = 1 ]; then if [ "$install_skill" = 1 ]; then
printf 'Restart Codex if it is open, then use it normally in any Git repository.\n' printf 'Restart any open agent application, then use it normally in any Git repository.\n'
fi fi
+70 -2
View File
@@ -7,11 +7,16 @@ $tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ("hop-installer-test-" +
$fixtures = Join-Path $tempDir "fixtures" $fixtures = Join-Path $tempDir "fixtures"
$payload = Join-Path $tempDir "payload" $payload = Join-Path $tempDir "payload"
$installDir = Join-Path $tempDir "install" $installDir = Join-Path $tempDir "install"
$testHome = Join-Path $tempDir "home"
$testCodexHome = Join-Path $testHome ".codex"
$testArch = if ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString() -eq "Arm64") { "arm64" } else { "amd64" } $testArch = if ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString() -eq "Arm64") { "arm64" } else { "amd64" }
$asset = "hop_windows_${testArch}.zip" $asset = "hop_windows_${testArch}.zip"
$originalHome = $env:HOME
$originalUserProfile = $env:USERPROFILE
$originalCodexHome = $env:CODEX_HOME
New-Item -ItemType Directory -Path $tempDir | Out-Null New-Item -ItemType Directory -Path $tempDir | Out-Null
New-Item -ItemType Directory -Path $fixtures, $payload | Out-Null New-Item -ItemType Directory -Path $fixtures, $payload, $testHome | Out-Null
try { try {
$binary = Join-Path $payload "hop.exe" $binary = Join-Path $payload "hop.exe"
& go build -trimpath ` & go build -trimpath `
@@ -53,11 +58,14 @@ try {
} }
} }
$env:HOME = $testHome
$env:USERPROFILE = $testHome
$env:CODEX_HOME = $testCodexHome
& (Join-Path $root "scripts/install.ps1") ` & (Join-Path $root "scripts/install.ps1") `
-GiteaUrl "https://gitea.test" ` -GiteaUrl "https://gitea.test" `
-Repository "GnosysLabs/Hop" ` -Repository "GnosysLabs/Hop" `
-InstallDir $installDir ` -InstallDir $installDir `
-SkipSkill `
-SkipPath -SkipPath
$installed = Join-Path $installDir "hop.exe" $installed = Join-Path $installDir "hop.exe"
@@ -66,7 +74,67 @@ try {
if ($LASTEXITCODE -ne 0 -or $version -ne "hop 9.9.9-installer-test") { if ($LASTEXITCODE -ne 0 -or $version -ne "hop 9.9.9-installer-test") {
throw "Unexpected installed version: $version" throw "Unexpected installed version: $version"
} }
$sharedBundle = Join-Path $testHome ".agents\skills\hop"
$codexBundle = Join-Path $testCodexHome "skills\hop"
$sharedSkill = Join-Path $sharedBundle "SKILL.md"
$codexSkill = Join-Path $codexBundle "SKILL.md"
if (-not (Test-Path -LiteralPath $sharedSkill -PathType Leaf) -or (Get-Item -LiteralPath $sharedSkill).Length -eq 0) {
throw "Installer did not install the shared Hop skill"
}
if (-not (Test-Path -LiteralPath $codexSkill -PathType Leaf) -or (Get-Item -LiteralPath $codexSkill).Length -eq 0) {
throw "Installer did not install the Codex Hop skill"
}
function Get-BundleHashes {
param([Parameter(Mandatory)][string]$BundlePath)
$hashes = @{}
Get-ChildItem -LiteralPath $BundlePath -File -Recurse | ForEach-Object {
$relative = $_.FullName.Substring($BundlePath.Length).TrimStart(
[System.IO.Path]::DirectorySeparatorChar,
[System.IO.Path]::AltDirectorySeparatorChar
)
$hashes[$relative] = (Get-FileHash -Algorithm SHA256 -LiteralPath $_.FullName).Hash
}
return $hashes
}
$sharedHashes = Get-BundleHashes $sharedBundle
$codexHashes = Get-BundleHashes $codexBundle
$sharedFiles = @($sharedHashes.Keys | Sort-Object)
$codexFiles = @($codexHashes.Keys | Sort-Object)
if ($sharedFiles.Count -eq 0 -or (Compare-Object -ReferenceObject $sharedFiles -DifferenceObject $codexFiles)) {
throw "Shared and Codex Hop skill bundles contain different files"
}
foreach ($relative in $sharedFiles) {
if ($sharedHashes[$relative] -ne $codexHashes[$relative]) {
throw "Shared and Codex Hop skill bundles differ at $relative"
}
}
$blockedCodexHome = Join-Path $testHome "blocked-codex"
$blockedSkills = Join-Path $blockedCodexHome "skills"
New-Item -ItemType Directory -Path $blockedSkills | Out-Null
"blocked" | Set-Content -Encoding ascii (Join-Path $blockedSkills "hop")
$env:CODEX_HOME = $blockedCodexHome
$installFailed = $false
try {
& (Join-Path $root "scripts/install.ps1") `
-GiteaUrl "https://gitea.test" `
-Repository "GnosysLabs/Hop" `
-InstallDir $installDir `
-SkipPath
} catch {
$installFailed = $true
}
if (-not $installFailed) {
throw "Installer did not fail when skill installation failed"
}
$env:CODEX_HOME = $testCodexHome
Write-Host "PowerShell installer smoke test passed." Write-Host "PowerShell installer smoke test passed."
} finally { } finally {
$env:HOME = $originalHome
$env:USERPROFILE = $originalUserProfile
$env:CODEX_HOME = $originalCodexHome
Remove-Item -Recurse -Force -ErrorAction SilentlyContinue $tempDir Remove-Item -Recurse -Force -ErrorAction SilentlyContinue $tempDir
} }
+13 -2
View File
@@ -64,6 +64,7 @@ MOCK_CURL
chmod 0755 "$tmp_dir/mock-bin/curl" chmod 0755 "$tmp_dir/mock-bin/curl"
HOME="$tmp_dir/home" \ HOME="$tmp_dir/home" \
CODEX_HOME="$tmp_dir/home/.codex" \
PATH="$tmp_dir/mock-bin:$PATH" \ PATH="$tmp_dir/mock-bin:$PATH" \
HOP_TEST_FIXTURES="$tmp_dir/fixtures" \ HOP_TEST_FIXTURES="$tmp_dir/fixtures" \
HOP_GITEA_URL="https://gitea.test" \ HOP_GITEA_URL="https://gitea.test" \
@@ -77,7 +78,17 @@ version=$($tmp_dir/home/bin/hop version)
printf 'unexpected installed version: %s\n' "$version" >&2 printf 'unexpected installed version: %s\n' "$version" >&2
exit 1 exit 1
} }
[ -s "$tmp_dir/home/.codex/skills/hop/SKILL.md" ] || { shared_bundle="$tmp_dir/home/.agents/skills/hop"
printf 'installer did not install the embedded Hop skill\n' >&2 codex_bundle="$tmp_dir/home/.codex/skills/hop"
[ -s "$shared_bundle/SKILL.md" ] || {
printf 'installer did not install the shared Hop skill\n' >&2
exit 1 exit 1
} }
[ -s "$codex_bundle/SKILL.md" ] || {
printf 'installer did not install the Codex Hop skill\n' >&2
exit 1
}
if ! diff -r "$shared_bundle" "$codex_bundle" >/dev/null; then
printf 'shared and Codex Hop skill bundles differ\n' >&2
exit 1
fi
+35 -14
View File
@@ -1,6 +1,6 @@
--- ---
name: hop name: hop
description: Capture local repository prompts as Hop states and perform agent work in isolated Hop workspaces. Use at the start of every Codex Desktop or CLI repository turn and follow-up, before inspecting files, running project commands, editing, reviewing, delegating, landing, or undoing—even when the user does not mention Hop. Also use whenever HOP_STATE_ID, HOP_TASK_ID, HOP_ATTEMPT_ID, CODEX_THREAD_ID, or .hop/hop.db is present. description: Capture local repository prompts as Hop states and perform agent work in isolated Hop workspaces. Use at the start of every interactive coding-agent repository turn and follow-up, before inspecting files, running project commands, editing, reviewing, delegating, landing, or undoing—even when the user does not mention Hop. Also use whenever HOP_STATE_ID, HOP_TASK_ID, HOP_ATTEMPT_ID, HOP_AGENT, CODEX_THREAD_ID, or .hop/hop.db is present.
--- ---
# Hop # Hop
@@ -11,25 +11,45 @@ returned Hop workspace.
## Capture the current prompt first ## Capture the current prompt first
Do not inspect repository files, plan from repository contents, run project Do not inspect repository files, plan from repository contents, run project
commands, edit, or delegate before capture. Run this from the selected project commands, edit, or delegate before capture. Run the form for the current shell
directory: from the selected project directory.
POSIX shell:
```bash ```bash
hop begin --agent codex --heredoc <<'HOP_PROMPT_EOF' hop begin --heredoc <<'HOP_PROMPT_EOF'
<copy the current user message verbatim> <copy the current user message verbatim>
HOP_PROMPT_EOF HOP_PROMPT_EOF
``` ```
Choose a different quoted delimiter if that exact delimiter appears in the PowerShell:
message. Include visible attachment paths and references. Do not paraphrase,
pre-redact, or omit a suspected credential in this one capture stream; Hop must
see it to replace it deterministically before persistence. `--heredoc` removes
only the shell-added final newline. Never copy the credential anywhere else.
`hop begin` performs the Desktop bootstrap: ```powershell
$hopPrompt = @'
<copy the current user message verbatim>
'@
$hopPrompt | hop begin --heredoc
```
Choose a different non-interpolating stdin construction if the applicable
terminator appears in the message. Include visible attachment paths and
references. Do not paraphrase, pre-redact, or omit a suspected credential in
this one capture stream; Hop must see it to replace it deterministically before
persistence. `--heredoc` removes only the shell-added final newline. Never copy
the credential anywhere else.
An integration may identify its runtime through `HOP_AGENT` or `--agent`, and
should pass a stable `--session` value when it has one. A stable session lets
Hop connect unfinished follow-ups without making the user carry state IDs.
Codex is one adapter example: when `CODEX_THREAD_ID` is present, `hop begin`
uses it as the default session and identifies the runtime as `codex` unless
`HOP_AGENT` or `--agent` overrides that name.
`hop begin` performs the interactive-agent bootstrap:
- Initialize Hop automatically when the project has not used it before. - Initialize Hop automatically when the project has not used it before.
- Use `CODEX_THREAD_ID` to bind this Codex task to its unfinished Hop work. - Use the integration's stable session identity to bind later messages to
unfinished Hop work. Without one, each invocation begins independent work.
- Create a prompt state and isolated workspace on the first turn. - Create a prompt state and isolated workspace on the first turn.
- Checkpoint prior workspace effects and append follow-ups until that work lands. - Checkpoint prior workspace effects and append follow-ups until that work lands.
- Follow a reconciliation into its fresh attempt, then start the first prompt - Follow a reconciliation into its fresh attempt, then start the first prompt
@@ -149,10 +169,11 @@ safe continuation needs new user authority.
If visible-root synchronization is blocked, do not bypass it with `hop accept`, If visible-root synchronization is blocked, do not bypass it with `hop accept`,
force checkout, reset, or file copying. Preserve the proposal and identify the force checkout, reset, or file copying. Preserve the proposal and identify the
user-owned paths that must be resolved. `hop accept` is reserved for an user-owned paths that must be resolved. `hop accept` is reserved for an
explicitly controller-only workflow; Desktop work always uses `hop land`. explicitly controller-only workflow; interactive agent work uses `hop land`.
Use `hop undo` only after a separately captured, explicit user request. Use `hop undo` only after a separately captured, explicit user request.
Read [references/protocol.md](references/protocol.md) for state semantics, exit Read [references/protocol.md](references/protocol.md) for state semantics, exit
codes, recovery, and controller-grade pre-delivery capture. Skill-driven codes, recovery, and controller-grade pre-delivery capture. Skill-driven
Desktop capture is a pre-project-effect boundary; it does not claim the prompt interactive capture is a pre-project-effect boundary; it does not claim the
was stored before Codex received it. prompt was stored before the runtime received it. On Codex Desktop, for
example, Codex has already received the prompt before this skill can run.
+55 -26
View File
@@ -34,12 +34,16 @@ Prompt, checkpoint, and proposal states may reference identical Git trees while
| `HOP_TASK_ID` | Logical task grouping related prompts and attempts | | `HOP_TASK_ID` | Logical task grouping related prompts and attempts |
| `HOP_ATTEMPT_ID` | Current agent approach/run | | `HOP_ATTEMPT_ID` | Current agent approach/run |
| `HOP_WORKSPACE` | Only directory the agent may modify | | `HOP_WORKSPACE` | Only directory the agent may modify |
| `HOP_AGENT` | Optional runtime name used by `hop begin` when `--agent` is omitted |
Interactive agents may begin without these variables. `hop begin` returns the Interactive agents may begin without these variables. `hop begin` returns the
equivalent IDs and workspace, while `CODEX_THREAD_ID` binds later messages in equivalent IDs and workspace. Integrations should identify themselves with
the same Codex task to unfinished work. Follow-ups before acceptance continue `HOP_AGENT` or `--agent` and pass a stable `--session` value when available.
the attempt; the first prompt after acceptance starts a fresh task and attempt That session binds later messages to unfinished work; without it, each
at the latest accepted state. invocation begins independent work. The Codex adapter uses `CODEX_THREAD_ID` as
the default session and `codex` as the default runtime name. Follow-ups before
acceptance continue the attempt; the first prompt after acceptance starts a
fresh task and attempt at the latest accepted state.
## Command contract ## Command contract
@@ -61,10 +65,26 @@ hop undo
### Agent ### Agent
POSIX shell:
```bash ```bash
hop begin --agent codex --heredoc <<'HOP_PROMPT_EOF' hop begin --heredoc <<'HOP_PROMPT_EOF'
<exact current user message> <exact current user message>
HOP_PROMPT_EOF HOP_PROMPT_EOF
```
PowerShell:
```powershell
$hopPrompt = @'
<exact current user message>
'@
$hopPrompt | hop begin --heredoc
```
Then continue in the returned workspace:
```bash
hop state "$HOP_STATE_ID" --json hop state "$HOP_STATE_ID" --json
hop status --json hop status --json
hop check "$HOP_STATE_ID" -- <command> hop check "$HOP_STATE_ID" -- <command>
@@ -73,6 +93,11 @@ hop land <proposal-state> -- <final validation command>
hop refresh <proposal-state> hop refresh <proposal-state>
``` ```
An adapter may set `HOP_AGENT=<runtime>` or pass `--agent <runtime>`. If it has
a stable conversation/run identifier, it should also pass `--session <id>` on
every `hop begin`. Codex normally needs neither explicit flag because
`CODEX_THREAD_ID` supplies its session adapter automatically.
`hop check` snapshots the attempt and runs the command in a detached worktree materialized from that exact checkpoint. Edits made concurrently in the live workspace do not change the tested tree. `hop check` snapshots the attempt and runs the command in a detached worktree materialized from that exact checkpoint. Edits made concurrently in the live workspace do not change the tested tree.
`hop propose` freezes the current nonignored workspace tree. Later workspace edits cannot change the proposal. `hop propose` freezes the current nonignored workspace tree. Later workspace edits cannot change the proposal.
@@ -85,13 +110,13 @@ unresolved product ambiguity, or newly required destructive/external scope
stops automatic acceptance. Path overlap and a stale accepted head do not: stops automatic acceptance. Path overlap and a stale accepted head do not:
Hop merges, retries, or prepares agent reconciliation. Hop merges, retries, or prepares agent reconciliation.
`hop land` is the Desktop operation. It performs a real Git three-way content `hop land` is the interactive working-root operation. It performs a real Git
merge, so compatible edits in the same file and identical changes compose three-way content merge, so compatible edits in the same file and identical
automatically. It validates and advances accepted state, then safely changes compose automatically. It validates and advances accepted state, then
materializes that tree into the selected visible project root. The root must safely materializes that tree into the selected visible project root. The root
still match an accepted Hop ancestor, and ignored or untracked destination must still match an accepted Hop ancestor, and ignored or untracked destination
collisions block before acceptance. Materialization uses a disposable index and collisions block before acceptance. Materialization uses a disposable index
never moves HEAD, the active branch, or the user's real index. and never moves HEAD, the active branch, or the user's real index.
When the three-way merge has genuine unresolved conflicts, `hop land` returns When the three-way merge has genuine unresolved conflicts, `hop land` returns
exit `20` and automatically prepares a reconciliation prompt in the original exit `20` and automatically prepares a reconciliation prompt in the original
@@ -110,13 +135,16 @@ explicit form of the same preparation step.
`hop sync` safely catches a stale accepted-ancestor root up to the current `hop sync` safely catches a stale accepted-ancestor root up to the current
accepted state, including projects created with older Hop builds. accepted state, including projects created with older Hop builds.
`hop begin` is the Codex Desktop entry point. It initializes Hop when necessary, `hop begin` is the interactive-agent entry point. It initializes Hop when
captures the current message before the agent performs project work, and uses necessary and captures the current message before the agent performs project
`CODEX_THREAD_ID` as the default session key. A later `hop begin` in the same work. Runtime adapters identify themselves through `HOP_AGENT` or `--agent`
Codex task checkpoints the prior workspace before appending a follow-up while and use `--session` to supply a stable conversation/run key. A later
that work remains unfinished. Reconciliation transfers the session to its fresh `hop begin` with the same session checkpoints the prior workspace before
attempt. After a proposal is accepted, the next `hop begin` starts from the appending a follow-up while that work remains unfinished. Reconciliation
latest accepted state and never reopens the completed workspace. transfers the session to its fresh attempt. After a proposal is accepted, the
next `hop begin` starts from the latest accepted state and never reopens the
completed workspace. Codex Desktop supplies `CODEX_THREAD_ID` as its default
session key, so its adapter does not need to add `--session` explicitly.
Pass the original message to `hop begin` without model-side redaction. Hop's Pass the original message to `hop begin` without model-side redaction. Hop's
sanitizer replaces detected credential values before any durable write and sanitizer replaces detected credential values before any durable write and
@@ -139,19 +167,20 @@ A failed `hop check` or final landing check persists its evidence. A blocked or
## Capture modes ## Capture modes
### Codex Desktop skill ### Interactive agent skill
The user types normally in Codex Desktop. The Hop skill makes `hop begin` its The user types normally in their agent interface. The Hop skill makes
first project action and then directs every operation into the returned `hop begin` its first project action and then directs every operation into the
workspace. This is a pre-project-effect boundary: Codex has already received returned workspace. This is a pre-project-effect boundary: the runtime has
the prompt, but no repository inspection, command, or modification may precede already received the prompt, but no repository inspection, command, or
the durable prompt state. modification may precede the durable prompt state. Codex Desktop is one such
adapter; it provides session continuity through `CODEX_THREAD_ID`.
### Controller-grade pre-delivery capture ### Controller-grade pre-delivery capture
```bash ```bash
hop init hop init
hop start --agent codex "Add password reset emails" hop start --agent <runtime> "Add password reset emails"
``` ```
Use the returned workspace and environment to launch the agent. For example, conceptually: Use the returned workspace and environment to launch the agent. For example, conceptually:
+27 -10
View File
@@ -1,16 +1,26 @@
# Codex Desktop and agent workflow # Agent integrations and workflow
## Codex Desktop ## Skill-based integration
Users type into Codex normally. The installed skill makes prompt capture the A compatible agent skill makes prompt capture the agent's first repository
agent's first repository action: action. The integration supplies a stable agent and session identity. In a
POSIX shell:
```bash ```bash
hop begin --agent codex --heredoc <<'HOP_PROMPT_EOF' hop begin --agent my-agent --session stable-session-id --heredoc <<'HOP_PROMPT_EOF'
<exact visible user message> <exact visible user message>
HOP_PROMPT_EOF HOP_PROMPT_EOF
``` ```
In PowerShell:
```powershell
$hopPrompt = @'
<exact visible user message>
'@
$hopPrompt | hop begin --agent my-agent --session stable-session-id --heredoc
```
The agent adopts the returned `HOP_STATE_ID`, `HOP_TASK_ID`, The agent adopts the returned `HOP_STATE_ID`, `HOP_TASK_ID`,
`HOP_ATTEMPT_ID`, and `HOP_WORKSPACE`, then confines reads, commands, edits, and `HOP_ATTEMPT_ID`, and `HOP_WORKSPACE`, then confines reads, commands, edits, and
tests to that workspace. tests to that workspace.
@@ -26,15 +36,22 @@ hop land R_... -- go test ./...
No second landing authorization is requested unless the user explicitly asks No second landing authorization is requested unless the user explicitly asks
for review-first behavior. for review-first behavior.
Desktop capture stores the agent's verbatim transcription of the visible Skill-based capture stores the agent's verbatim transcription of the visible
message and its attachment references. Because the skill runs after Codex message and its attachment references. Because the skill runs after the client
receives the message, it cannot prove byte-for-byte fidelity with the raw receives the message, it cannot prove byte-for-byte fidelity with the raw
submission. A trusted prompt-submission hook or controller is the deterministic submission. A trusted prompt-submission hook or controller is the deterministic
capture boundary. capture boundary.
### Codex Desktop example
The bundled Codex integration uses `CODEX_THREAD_ID` as its stable session key,
defaults the agent name to `codex`, and lets the user type normally. Its bundle
is installed at `${CODEX_HOME:-~/.codex}/skills/hop`; the same files are also
installed at `~/.agents/skills/hop` for compatible clients.
## Follow-up messages ## Follow-up messages
A later `hop begin` with the same Codex task session checkpoints existing A later `hop begin` with the same integration session checkpoints existing
workspace effects, appends a new prompt state, and continues the same attempt workspace effects, appends a new prompt state, and continues the same attempt
while that work remains unfinished. If Hop prepares reconciliation, the session while that work remains unfinished. If Hop prepares reconciliation, the session
follows its fresh workspace. After the result lands, the next prompt starts a follows its fresh workspace. After the result lands, the next prompt starts a
@@ -60,8 +77,8 @@ follow-ups use:
hop prompt --from P_... --heredoc hop prompt --from P_... --heredoc
``` ```
This provides a stronger pre-delivery boundary than a Desktop skill, which can This provides a stronger pre-delivery boundary than an agent-side skill, which
only guarantee capture before project effects. can only guarantee capture before project effects.
## Agent rules ## Agent rules
+4 -4
View File
@@ -25,7 +25,7 @@ mirrored at `refs/hop/accepted`.
- prompt/task/attempt identity; - prompt/task/attempt identity;
- typed state edges and accepted lineage; - typed state edges and accepted lineage;
- evidence tied to exact source trees; - evidence tied to exact source trees;
- session heads for Desktop follow-ups; - session heads for interactive-agent follow-ups;
- materialized-root state; and - materialized-root state; and
- immutable audit events. - immutable audit events.
@@ -46,9 +46,9 @@ initialization when `.hop` is already tracked as user-owned project content.
## Acceptance consistency ## Acceptance consistency
Acceptance is serialized and compare-and-swapped. SQLite is authoritative; Acceptance is serialized and compare-and-swapped. SQLite is authoritative;
derived Git refs can be repaired by `hop doctor --repair`. Desktop landing also derived Git refs can be repaired by `hop doctor --repair`. Visible-root landing
tracks which accepted state is physically visible, allowing safe catch-up with also tracks which accepted state is physically visible, allowing safe catch-up
`hop sync` without treating a divergent folder as disposable. with `hop sync` without treating a divergent folder as disposable.
For the full product direction, read the For the full product direction, read the
[product blueprint](https://githop.xyz/GnosysLabs/Hop/src/branch/main/docs/product-blueprint.md). [product blueprint](https://githop.xyz/GnosysLabs/Hop/src/branch/main/docs/product-blueprint.md).
+5 -4
View File
@@ -18,7 +18,7 @@ release.
| Command | Purpose | | Command | Purpose |
|---|---| |---|---|
| `hop init [path]` | Initialize Hop without moving the Git branch or index | | `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 begin ...` | Interactive-agent entry point: initialize if needed, capture prompt, continue session |
| `hop prompt ...` | Controller-managed prompt or follow-up capture | | `hop prompt ...` | Controller-managed prompt or follow-up capture |
| `hop checkpoint STATE` | Freeze current attempt progress | | `hop checkpoint STATE` | Freeze current attempt progress |
| `hop check STATE -- COMMAND...` | Validate an immutable checkpoint | | `hop check STATE -- COMMAND...` | Validate an immutable checkpoint |
@@ -49,15 +49,16 @@ release.
| `hop history` | Accepted lineage | | `hop history` | Accepted lineage |
| `hop version` | Installed version | | `hop version` | Installed version |
## Skill distribution ## Agent integration bundle
```bash ```bash
hop skill install [--path SKILLS_DIR] [--force] hop skill install [--path SKILLS_DIR] [--force]
hop skill print hop skill print
``` ```
Without `--path`, the skill installs under Without `--path`, Hop installs the same Hop-managed skill files at
`${CODEX_HOME:-~/.codex}/skills/hop`. `~/.agents/skills/hop` and `${CODEX_HOME:-~/.codex}/skills/hop`. With `--path`,
it installs only to `SKILLS_DIR/hop`.
## Exit codes ## Exit codes
+8 -7
View File
@@ -22,9 +22,10 @@ different moments and causal roles.
A task groups the prompts and attempts pursuing one user outcome. Follow-up A task groups the prompts and attempts pursuing one user outcome. Follow-up
messages pursuing unfinished work stay connected automatically through messages pursuing unfinished work stay connected automatically through
`CODEX_THREAD_ID`. Once that outcome is accepted, the next message starts a new a stable session ID supplied by the integration. Codex Desktop supplies
Hop task at the latest accepted state even when the Codex conversation stays `CODEX_THREAD_ID` automatically. Once that outcome is accepted, the next message
open. starts a new Hop task at the latest accepted state even when the client
conversation stays open.
## Attempt and workspace ## Attempt and workspace
@@ -54,7 +55,7 @@ state but intentionally leaves the visible folder unchanged.
## Visible root ## Visible root
The visible root is the project directory selected in Codex. Hop only The visible root is the project directory selected in an agent client or passed
materializes into it when it still matches an accepted Hop ancestor. Untracked, as the controller's working directory. Hop only materializes into it when it
ignored, staged, or ordinary file divergence that could be overwritten causes a still matches an accepted Hop ancestor. Untracked, ignored, staged, or ordinary
fail-closed error. file divergence that could be overwritten causes a fail-closed error.
+21 -11
View File
@@ -1,18 +1,27 @@
# Getting started # Getting started
## Use Hop from Codex Desktop ## Use Hop with an agent integration
1. [Install Hop](Installation). 1. [Install Hop](Installation).
2. Restart Codex Desktop if it was already open. 2. Select an existing Git project in a compatible agent client, or make it the
3. Select an existing Git project as the Codex working directory. controller's working directory.
4. Ask Codex to make a normal change. 3. Ask the agent to make a normal change.
That is the full user workflow. Do not manually create `.hop`, route the prompt 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 through a terminal, or tell the agent to work inside `.hop/workspaces`. A Hop
does that coordination for the agent. integration does that coordination for the agent.
The skill is eligible for implicit activation on every repository task. Mention Without `--path`, `hop skill install` writes the same Hop-managed skill files to
`$hop` in the task if you want deterministic explicit activation. `~/.agents/skills/hop` and `${CODEX_HOME:-~/.codex}/skills/hop`. Compatible
runtimes can use the shared bundle. An explicit `--path` installs only to the
requested skills directory.
### Codex Desktop example
Restart Codex Desktop after installing or upgrading the skill, select a Git
project, and ask Codex to work normally. The skill is eligible for implicit
activation on every repository task; mention `$hop` for deterministic explicit
activation.
## What happens on the first task ## What happens on the first task
@@ -37,7 +46,7 @@ hop history
hop doctor hop doctor
``` ```
A normal Desktop result reports `Root: synchronized`. A normal interactive result reports `Root: synchronized`.
## Ask for review before landing ## Ask for review before landing
@@ -48,9 +57,10 @@ local code change. To stop at a proposal, say one of the following in the task:
- `proposal only` - `proposal only`
- `do not land` - `do not land`
## Use another agent runtime ## Connect another agent runtime
Install the embedded skill into that runtime's skills directory: If a compatible runtime does not read `~/.agents/skills`, install the embedded
bundle into that runtime's skills directory:
```bash ```bash
hop skill install --path /path/to/agent/skills --force hop skill install --path /path/to/agent/skills --force
+5 -4
View File
@@ -10,7 +10,7 @@ visible project folder.
- [Installation](Installation) - [Installation](Installation)
- [Getting started](Getting-Started) - [Getting started](Getting-Started)
- [Core concepts](Core-Concepts) - [Core concepts](Core-Concepts)
- [Codex Desktop and agent workflow](Agent-Workflow) - [Agent integrations and workflow](Agent-Workflow)
- [Parallel agents and conflict resolution](Parallel-Agents-and-Conflicts) - [Parallel agents and conflict resolution](Parallel-Agents-and-Conflicts)
## Reference and operations ## Reference and operations
@@ -36,9 +36,10 @@ Windows PowerShell:
irm https://githop.xyz/GnosysLabs/Hop/raw/branch/main/scripts/install.ps1 | iex irm https://githop.xyz/GnosysLabs/Hop/raw/branch/main/scripts/install.ps1 | iex
``` ```
Restart Codex Desktop, open a Git project, and ask Codex to work normally. The Open a Git project in a compatible agent client and work normally. The installed
installed skill activates Hop before the agent inspects or changes the project; skill bundle activates Hop before the agent inspects or changes the project;
there is no separate prompt router and no required manual `hop init` step. controllers can invoke the same protocol directly. Codex Desktop is a bundled
integration, and there is no required manual `hop init` step.
Hop is currently an alpha. Keep Git history and normal backups, read release Hop is currently an alpha. Keep Git history and normal backups, read release
notes before upgrading, and report unexpected behavior with `hop doctor` output notes before upgrading, and report unexpected behavior with `hop doctor` output
+19 -8
View File
@@ -16,7 +16,11 @@ The installer:
3. downloads `checksums.txt` and verifies SHA-256 before extraction; 3. downloads `checksums.txt` and verifies SHA-256 before extraction;
4. installs the CLI to `~/.local/bin/hop`; 4. installs the CLI to `~/.local/bin/hop`;
5. adds `~/.local/bin` to `.zprofile` or `.profile` when necessary; and 5. adds `~/.local/bin` to `.zprofile` or `.profile` when necessary; and
6. runs `hop skill install --force` for Codex. 6. runs `hop skill install --force` to install the bundled agent integration.
The no-path skill command writes the same Hop-managed skill files to
`~/.agents/skills/hop` and `${CODEX_HOME:-~/.codex}/skills/hop`. The second path
supports Codex Desktop; compatible clients can use the shared `.agents` path.
Review before execution: Review before execution:
@@ -32,7 +36,7 @@ Installer options are environment variables:
|---|---|---| |---|---|---|
| `HOP_VERSION` | `latest` | Release tag such as `v0.1.0` | | `HOP_VERSION` | `latest` | Release tag such as `v0.1.0` |
| `HOP_INSTALL_DIR` | `~/.local/bin` | Binary destination | | `HOP_INSTALL_DIR` | `~/.local/bin` | Binary destination |
| `HOP_INSTALL_SKILL` | `1` | Set to `0` to skip Codex skill installation | | `HOP_INSTALL_SKILL` | `1` | Set to `0` for a CLI-only or custom integration |
| `HOP_MODIFY_PATH` | `1` | Set to `0` to leave shell profiles unchanged | | `HOP_MODIFY_PATH` | `1` | Set to `0` to leave shell profiles unchanged |
| `HOP_GITEA_URL` | `https://githop.xyz` | Gitea instance base URL | | `HOP_GITEA_URL` | `https://githop.xyz` | Gitea instance base URL |
| `HOP_REPOSITORY` | `GnosysLabs/Hop` | Alternate Gitea owner/repository | | `HOP_REPOSITORY` | `GnosysLabs/Hop` | Alternate Gitea owner/repository |
@@ -52,15 +56,15 @@ irm https://githop.xyz/GnosysLabs/Hop/raw/branch/main/scripts/install.ps1 | iex
``` ```
The script verifies the Windows archive, installs to The script verifies the Windows archive, installs to
`%LOCALAPPDATA%\Programs\Hop`, updates the user PATH, and installs the Codex `%LOCALAPPDATA%\Programs\Hop`, updates the user PATH, and installs the shared and
skill. To pin a version after downloading the script: Codex-compatible skill bundles. To pin a version after downloading the script:
```powershell ```powershell
.\install.ps1 -Version v0.1.0 .\install.ps1 -Version v0.1.0
``` ```
Use `-SkipSkill` only when another agent runtime will receive the skill. Use Use `-SkipSkill` for a CLI-only or separately managed integration. Use `-SkipPath`
`-SkipPath` when another tool manages your PATH. when another tool manages your PATH.
## Go install ## Go install
@@ -71,7 +75,8 @@ go install githop.xyz/GnosysLabs/Hop/cmd/hop@latest
hop skill install --force hop skill install --force
``` ```
Put `$(go env GOPATH)/bin` on PATH if `hop` is not found. Put `$(go env GOPATH)/bin` on PATH if `hop` is not found. The second command
installs both default skill bundles; omit it for a CLI-only installation.
## Build from source ## Build from source
@@ -87,6 +92,12 @@ install -m 755 hop "$HOME/.local/bin/hop"
Source builds are intended for contributors and as a pre-release fallback. Source builds are intended for contributors and as a pre-release fallback.
To install only one compatible runtime target, pass its parent skills directory:
```bash
hop skill install --path /path/to/agent/skills --force
```
## Verify ## Verify
```bash ```bash
@@ -95,5 +106,5 @@ hop help
git --version git --version
``` ```
If Codex Desktop was open during installation, restart it. See Restart any agent client that was open while its skill bundle changed. See
[Getting started](Getting-Started) next. [Getting started](Getting-Started) next.
+2 -1
View File
@@ -40,7 +40,8 @@ scripts/release-local.sh --snapshot
Inspect `dist/` and test at least one archive on each operating system family. Inspect `dist/` and test at least one archive on each operating system family.
Confirm `hop version` reports the snapshot/tag-injected version and Confirm `hop version` reports the snapshot/tag-injected version and
`hop skill install --force` installs usable skill files. `hop skill install --force` installs identical Hop-managed files at both
default skill destinations while preserving unrelated user files.
## Create a release ## Create a release
+6 -4
View File
@@ -13,14 +13,16 @@ printf '%s\n' "$PATH"
The default Unix location is `~/.local/bin`. On Windows it is The default Unix location is `~/.local/bin`. On Windows it is
`%LOCALAPPDATA%\Programs\Hop`. `%LOCALAPPDATA%\Programs\Hop`.
## Codex does not activate Hop ## An agent integration does not activate Hop
```bash ```bash
hop skill install --force hop skill install --force
``` ```
Restart Codex Desktop. Mention `$hop` in a task as a deterministic activation The command refreshes both default skill bundles. Restart the agent client. For
fallback. Codex Desktop, mention `$hop` in a task as a deterministic activation fallback.
For another compatible runtime, confirm that it reads `~/.agents/skills` or run
`hop skill install --path /path/to/agent/skills --force`.
## The installer cannot find a release ## The installer cannot find a release
@@ -55,7 +57,7 @@ workspace, and rerun the check.
Hop found visible files or index state it will not overwrite. Preserve those 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 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. retry `hop land`. Do not bypass this with `hop accept` in interactive workflows.
## Internal ref or object warning ## Internal ref or object warning
+8 -3
View File
@@ -2,7 +2,8 @@
## Upgrade packaged installations ## Upgrade packaged installations
Rerun the installer. It replaces the binary and refreshes the embedded skill: Rerun the installer. It replaces the binary and refreshes both default skill
bundles:
```bash ```bash
curl -fsSL https://githop.xyz/GnosysLabs/Hop/raw/branch/main/scripts/install.sh | sh curl -fsSL https://githop.xyz/GnosysLabs/Hop/raw/branch/main/scripts/install.sh | sh
@@ -31,7 +32,7 @@ hop skill install --force
hop doctor hop doctor
``` ```
Restart Codex Desktop when the installed skill changed. Restart any agent client whose installed skill changed.
## Upgrade Go installations ## Upgrade Go installations
@@ -46,12 +47,13 @@ Hop opens and migrates older supported SQLite schemas automatically. Back up
important repositories before alpha upgrades and read the release notes for any important repositories before alpha upgrades and read the release notes for any
one-way schema change. one-way schema change.
## Uninstall the CLI and skill ## Uninstall the CLI and skill bundles
Unix default: Unix default:
```bash ```bash
rm -f "$HOME/.local/bin/hop" rm -f "$HOME/.local/bin/hop"
rm -rf "$HOME/.agents/skills/hop"
rm -rf "${CODEX_HOME:-$HOME/.codex}/skills/hop" rm -rf "${CODEX_HOME:-$HOME/.codex}/skills/hop"
``` ```
@@ -59,9 +61,12 @@ Windows PowerShell:
```powershell ```powershell
Remove-Item -Force "$env:LOCALAPPDATA\Programs\Hop\hop.exe" Remove-Item -Force "$env:LOCALAPPDATA\Programs\Hop\hop.exe"
Remove-Item -Recurse -Force "$HOME\.agents\skills\hop"
Remove-Item -Recurse -Force "$HOME\.codex\skills\hop" Remove-Item -Recurse -Force "$HOME\.codex\skills\hop"
``` ```
If `CODEX_HOME` points somewhere else, remove its `skills\hop` directory instead
of `$HOME\.codex\skills\hop`. Remove any explicit `--path` target manually.
Remove the Hop install directory from PATH if it is no longer used. Remove the Hop install directory from PATH if it is no longer used.
Uninstalling the program does not delete project-local `.hop/` histories. That Uninstalling the program does not delete project-local `.hop/` histories. That