diff --git a/README.md b/README.md index 836b428..5d7c118 100644 --- a/README.md +++ b/README.md @@ -30,9 +30,11 @@ and safe multi-agent integration. prompt → durable intent → isolated agent work → validate + merge → accepted code ``` -You keep using Codex Desktop normally. The installed Hop skill handles this -lifecycle for the agent—there is no prompt wrapper, special app, or manual -branch workflow. +Hop ships an open [Agent Skills](https://agentskills.io/) bundle and a controller +protocol. The skill makes prompt capture the agent's first repository action; a +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 @@ -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 ``` -The installer adds both the Hop CLI and its Codex skill. For source builds, -version pinning, custom locations, and verification, see the +The installer adds the Hop CLI and writes the same embedded skill version to +`~/.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). ## Get started 1. Install Hop. -2. Restart Codex Desktop if it was already open. -3. Open a Git project in Codex Desktop. -4. Ask Codex to make a change as you normally would. +2. Open a Git project in an Agent Skills-compatible client, or make it the + controller's working directory. +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 a terminal, or work inside `.hop` yourself. After a task, `hop status` shows the 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 1.0. diff --git a/internal/hop/cli.go b/internal/hop/cli.go index 687b0e3..51ba97c 100644 --- a/internal/hop/cli.go +++ b/internal/hop/cli.go @@ -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 { 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 { return printCLIError(err, jsonOutput, stdout, stderr) } if jsonOutput { writeJSON(stdout, map[string]any{"ok": true, "data": result}) } 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 case "print": diff --git a/internal/hop/skill.go b/internal/hop/skill.go index a08d9ca..edd94b5 100644 --- a/internal/hop/skill.go +++ b/internal/hop/skill.go @@ -12,6 +12,7 @@ import ( type SkillInstallResult struct { Path string `json:"path"` + Paths []string `json:"paths,omitempty"` Files []string `json:"files"` } @@ -27,6 +28,26 @@ func DefaultSkillBase() (string, error) { 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) { contents, err := hopskill.Files.ReadFile("SKILL.md") if err != nil { @@ -35,9 +56,10 @@ func EmbeddedSkillText() (string, error) { 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 -// 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) { if strings.TrimSpace(base) == "" { var err error @@ -46,30 +68,182 @@ func InstallSkill(base string, force bool) (SkillInstallResult, error) { 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) 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.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() { - 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 { - 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) { - 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 { return SkillInstallResult{}, fmt.Errorf("create skill target: %w", err) } - result := SkillInstallResult{Path: target} - err = fs.WalkDir(hopskill.Files, ".", func(path string, entry fs.DirEntry, walkErr error) error { + result := SkillInstallResult{Path: target, Paths: []string{target}} + err := fs.WalkDir(hopskill.Files, ".", func(path string, entry fs.DirEntry, walkErr error) error { if walkErr != nil { return walkErr } diff --git a/internal/hop/skill_test.go b/internal/hop/skill_test.go index ac8a1bc..3c5348e 100644 --- a/internal/hop/skill_test.go +++ b/internal/hop/skill_test.go @@ -1,8 +1,10 @@ package hop import ( + "bytes" "os" "path/filepath" + "runtime" "strings" "testing" ) @@ -16,6 +18,9 @@ func TestInstallSkillBundle(t *testing.T) { if result.Path != filepath.Join(base, "hop") { 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"} for _, relative := range wantFiles { contents, err := os.ReadFile(filepath.Join(result.Path, relative)) @@ -31,7 +36,7 @@ func TestInstallSkillBundle(t *testing.T) { t.Fatal(err) } 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")) 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) { 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) if code != 0 { 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")) { 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() stderr.Reset() code = RunCLI([]string{"skill", "print"}, &stdout, &stderr) diff --git a/scripts/install.ps1 b/scripts/install.ps1 index 02f96a0..fc25767 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -73,11 +73,14 @@ try { if (-not $SkipSkill) { & $installedBinary skill install --force + if ($LASTEXITCODE -ne 0) { + throw "Hop skill installation failed with exit code $LASTEXITCODE" + } } 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." + Write-Host "Restart any open agent application, 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 index 334dd0c..a12d94b 100644 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -103,5 +103,5 @@ 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' + printf 'Restart any open agent application, then use it normally in any Git repository.\n' fi diff --git a/scripts/test-install.ps1 b/scripts/test-install.ps1 index a80598d..c20125e 100644 --- a/scripts/test-install.ps1 +++ b/scripts/test-install.ps1 @@ -7,11 +7,16 @@ $tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ("hop-installer-test-" + $fixtures = Join-Path $tempDir "fixtures" $payload = Join-Path $tempDir "payload" $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" } $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 $fixtures, $payload | Out-Null +New-Item -ItemType Directory -Path $fixtures, $payload, $testHome | Out-Null try { $binary = Join-Path $payload "hop.exe" & go build -trimpath ` @@ -53,11 +58,14 @@ try { } } + $env:HOME = $testHome + $env:USERPROFILE = $testHome + $env:CODEX_HOME = $testCodexHome + & (Join-Path $root "scripts/install.ps1") ` -GiteaUrl "https://gitea.test" ` -Repository "GnosysLabs/Hop" ` -InstallDir $installDir ` - -SkipSkill ` -SkipPath $installed = Join-Path $installDir "hop.exe" @@ -66,7 +74,67 @@ try { if ($LASTEXITCODE -ne 0 -or $version -ne "hop 9.9.9-installer-test") { 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." } finally { + $env:HOME = $originalHome + $env:USERPROFILE = $originalUserProfile + $env:CODEX_HOME = $originalCodexHome Remove-Item -Recurse -Force -ErrorAction SilentlyContinue $tempDir } diff --git a/scripts/test-install.sh b/scripts/test-install.sh index 68c626f..6d654eb 100644 --- a/scripts/test-install.sh +++ b/scripts/test-install.sh @@ -64,6 +64,7 @@ MOCK_CURL chmod 0755 "$tmp_dir/mock-bin/curl" HOME="$tmp_dir/home" \ +CODEX_HOME="$tmp_dir/home/.codex" \ PATH="$tmp_dir/mock-bin:$PATH" \ HOP_TEST_FIXTURES="$tmp_dir/fixtures" \ 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 exit 1 } -[ -s "$tmp_dir/home/.codex/skills/hop/SKILL.md" ] || { - printf 'installer did not install the embedded Hop skill\n' >&2 +shared_bundle="$tmp_dir/home/.agents/skills/hop" +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 } +[ -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 diff --git a/skills/hop/SKILL.md b/skills/hop/SKILL.md index 590b2d5..5c0d06f 100644 --- a/skills/hop/SKILL.md +++ b/skills/hop/SKILL.md @@ -1,6 +1,6 @@ --- 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 @@ -11,25 +11,45 @@ returned Hop workspace. ## Capture the current prompt first Do not inspect repository files, plan from repository contents, run project -commands, edit, or delegate before capture. Run this from the selected project -directory: +commands, edit, or delegate before capture. Run the form for the current shell +from the selected project directory. + +POSIX shell: ```bash -hop begin --agent codex --heredoc <<'HOP_PROMPT_EOF' +hop begin --heredoc <<'HOP_PROMPT_EOF' HOP_PROMPT_EOF ``` -Choose a different quoted delimiter if that exact delimiter 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. +PowerShell: -`hop begin` performs the Desktop bootstrap: +```powershell +$hopPrompt = @' + +'@ +$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. -- 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. - Checkpoint prior workspace effects and append follow-ups until that work lands. - 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`, 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 -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. Read [references/protocol.md](references/protocol.md) for state semantics, exit codes, recovery, and controller-grade pre-delivery capture. Skill-driven -Desktop capture is a pre-project-effect boundary; it does not claim the prompt -was stored before Codex received it. +interactive capture is a pre-project-effect boundary; it does not claim the +prompt was stored before the runtime received it. On Codex Desktop, for +example, Codex has already received the prompt before this skill can run. diff --git a/skills/hop/references/protocol.md b/skills/hop/references/protocol.md index 34d2481..c06b8ae 100644 --- a/skills/hop/references/protocol.md +++ b/skills/hop/references/protocol.md @@ -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_ATTEMPT_ID` | Current agent approach/run | | `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 -equivalent IDs and workspace, while `CODEX_THREAD_ID` binds later messages in -the same Codex task to unfinished work. Follow-ups before acceptance continue -the attempt; the first prompt after acceptance starts a fresh task and attempt -at the latest accepted state. +equivalent IDs and workspace. Integrations should identify themselves with +`HOP_AGENT` or `--agent` and pass a stable `--session` value when available. +That session binds later messages to unfinished work; without it, each +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 @@ -61,10 +65,26 @@ hop undo ### Agent +POSIX shell: + ```bash -hop begin --agent codex --heredoc <<'HOP_PROMPT_EOF' +hop begin --heredoc <<'HOP_PROMPT_EOF' HOP_PROMPT_EOF +``` + +PowerShell: + +```powershell +$hopPrompt = @' + +'@ +$hopPrompt | hop begin --heredoc +``` + +Then continue in the returned workspace: + +```bash hop state "$HOP_STATE_ID" --json hop status --json hop check "$HOP_STATE_ID" -- @@ -73,6 +93,11 @@ hop land -- hop refresh ``` +An adapter may set `HOP_AGENT=` or pass `--agent `. If it has +a stable conversation/run identifier, it should also pass `--session ` 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 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: Hop merges, retries, or prepares agent reconciliation. -`hop land` is the Desktop operation. It performs a real Git three-way content -merge, so compatible edits in the same file and identical changes compose -automatically. It validates and advances accepted state, then safely -materializes that tree into the selected visible project root. The root must -still match an accepted Hop ancestor, and ignored or untracked destination -collisions block before acceptance. Materialization uses a disposable index and -never moves HEAD, the active branch, or the user's real index. +`hop land` is the interactive working-root operation. It performs a real Git +three-way content merge, so compatible edits in the same file and identical +changes compose automatically. It validates and advances accepted state, then +safely materializes that tree into the selected visible project root. The root +must still match an accepted Hop ancestor, and ignored or untracked destination +collisions block before acceptance. Materialization uses a disposable 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 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 accepted state, including projects created with older Hop builds. -`hop begin` is the Codex Desktop entry point. It initializes Hop when necessary, -captures the current message before the agent performs project work, and uses -`CODEX_THREAD_ID` as the default session key. A later `hop begin` in the same -Codex task checkpoints the prior workspace before appending a follow-up while -that work remains unfinished. Reconciliation 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. +`hop begin` is the interactive-agent entry point. It initializes Hop when +necessary and captures the current message before the agent performs project +work. Runtime adapters identify themselves through `HOP_AGENT` or `--agent` +and use `--session` to supply a stable conversation/run key. A later +`hop begin` with the same session checkpoints the prior workspace before +appending a follow-up while that work remains unfinished. Reconciliation +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 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 -### Codex Desktop skill +### Interactive agent skill -The user types normally in Codex Desktop. The Hop skill makes `hop begin` its -first project action and then directs every operation into the returned -workspace. This is a pre-project-effect boundary: Codex has already received -the prompt, but no repository inspection, command, or modification may precede -the durable prompt state. +The user types normally in their agent interface. The Hop skill makes +`hop begin` its first project action and then directs every operation into the +returned workspace. This is a pre-project-effect boundary: the runtime has +already received the prompt, but no repository inspection, command, or +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 ```bash hop init -hop start --agent codex "Add password reset emails" +hop start --agent "Add password reset emails" ``` Use the returned workspace and environment to launch the agent. For example, conceptually: diff --git a/wiki/Agent-Workflow.md b/wiki/Agent-Workflow.md index ac81245..048e8f1 100644 --- a/wiki/Agent-Workflow.md +++ b/wiki/Agent-Workflow.md @@ -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 -agent's first repository action: +A compatible agent skill makes prompt capture the agent's first repository +action. The integration supplies a stable agent and session identity. In a +POSIX shell: ```bash -hop begin --agent codex --heredoc <<'HOP_PROMPT_EOF' +hop begin --agent my-agent --session stable-session-id --heredoc <<'HOP_PROMPT_EOF' HOP_PROMPT_EOF ``` +In PowerShell: + +```powershell +$hopPrompt = @' + +'@ +$hopPrompt | hop begin --agent my-agent --session stable-session-id --heredoc +``` + 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. @@ -26,15 +36,22 @@ hop land R_... -- go test ./... No second landing authorization is requested unless the user explicitly asks for review-first behavior. -Desktop capture stores the agent's verbatim transcription of the visible -message and its attachment references. Because the skill runs after Codex +Skill-based capture stores the agent's verbatim transcription of the visible +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 submission. A trusted prompt-submission hook or controller is the deterministic 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 -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 while that work remains unfinished. If Hop prepares reconciliation, the session 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 ``` -This provides a stronger pre-delivery boundary than a Desktop skill, which can -only guarantee capture before project effects. +This provides a stronger pre-delivery boundary than an agent-side skill, which +can only guarantee capture before project effects. ## Agent rules diff --git a/wiki/Architecture.md b/wiki/Architecture.md index c871df0..c80e00b 100644 --- a/wiki/Architecture.md +++ b/wiki/Architecture.md @@ -25,7 +25,7 @@ mirrored at `refs/hop/accepted`. - prompt/task/attempt identity; - typed state edges and accepted lineage; - evidence tied to exact source trees; -- session heads for Desktop follow-ups; +- session heads for interactive-agent follow-ups; - materialized-root state; and - immutable audit events. @@ -46,9 +46,9 @@ 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. +derived Git refs can be repaired by `hop doctor --repair`. Visible-root 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/GnosysLabs/Hop/src/branch/main/docs/product-blueprint.md). diff --git a/wiki/CLI-Reference.md b/wiki/CLI-Reference.md index 962edd6..21267c6 100644 --- a/wiki/CLI-Reference.md +++ b/wiki/CLI-Reference.md @@ -18,7 +18,7 @@ release. | 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 begin ...` | Interactive-agent 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 | @@ -49,15 +49,16 @@ release. | `hop history` | Accepted lineage | | `hop version` | Installed version | -## Skill distribution +## Agent integration bundle ```bash hop skill install [--path SKILLS_DIR] [--force] hop skill print ``` -Without `--path`, the skill installs under -`${CODEX_HOME:-~/.codex}/skills/hop`. +Without `--path`, Hop installs the same Hop-managed skill files at +`~/.agents/skills/hop` and `${CODEX_HOME:-~/.codex}/skills/hop`. With `--path`, +it installs only to `SKILLS_DIR/hop`. ## Exit codes diff --git a/wiki/Core-Concepts.md b/wiki/Core-Concepts.md index 77fe933..4e53f31 100644 --- a/wiki/Core-Concepts.md +++ b/wiki/Core-Concepts.md @@ -22,9 +22,10 @@ different moments and causal roles. A task groups the prompts and attempts pursuing one user outcome. Follow-up messages pursuing unfinished work stay connected automatically through -`CODEX_THREAD_ID`. Once that outcome is accepted, the next message starts a new -Hop task at the latest accepted state even when the Codex conversation stays -open. +a stable session ID supplied by the integration. Codex Desktop supplies +`CODEX_THREAD_ID` automatically. Once that outcome is accepted, the next message +starts a new Hop task at the latest accepted state even when the client +conversation stays open. ## Attempt and workspace @@ -54,7 +55,7 @@ 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. +The visible root is the project directory selected in an agent client or passed +as the controller's working directory. 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 index 9839a47..62cd559 100644 --- a/wiki/Getting-Started.md +++ b/wiki/Getting-Started.md @@ -1,18 +1,27 @@ # Getting started -## Use Hop from Codex Desktop +## Use Hop with an agent integration 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. +2. Select an existing Git project in a compatible agent client, or make it the + controller's working directory. +3. Ask the agent 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. +through a terminal, or tell the agent to work inside `.hop/workspaces`. A Hop +integration 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. +Without `--path`, `hop skill install` writes the same Hop-managed skill files to +`~/.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 @@ -37,7 +46,7 @@ hop history hop doctor ``` -A normal Desktop result reports `Root: synchronized`. +A normal interactive result reports `Root: synchronized`. ## 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` - `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 hop skill install --path /path/to/agent/skills --force diff --git a/wiki/Home.md b/wiki/Home.md index 17a24af..3d8b554 100644 --- a/wiki/Home.md +++ b/wiki/Home.md @@ -10,7 +10,7 @@ visible project folder. - [Installation](Installation) - [Getting started](Getting-Started) - [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) ## Reference and operations @@ -36,9 +36,10 @@ Windows PowerShell: 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 -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. +Open a Git project in a compatible agent client and work normally. The installed +skill bundle activates Hop before the agent inspects or changes the project; +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 notes before upgrading, and report unexpected behavior with `hop doctor` output diff --git a/wiki/Installation.md b/wiki/Installation.md index 6b77af9..dc592f1 100644 --- a/wiki/Installation.md +++ b/wiki/Installation.md @@ -16,7 +16,11 @@ The installer: 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. +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: @@ -32,7 +36,7 @@ Installer options are environment variables: |---|---|---| | `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_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_GITEA_URL` | `https://githop.xyz` | Gitea instance base URL | | `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 -`%LOCALAPPDATA%\Programs\Hop`, updates the user PATH, and installs the Codex -skill. To pin a version after downloading the script: +`%LOCALAPPDATA%\Programs\Hop`, updates the user PATH, and installs the shared and +Codex-compatible skill bundles. 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. +Use `-SkipSkill` for a CLI-only or separately managed integration. Use `-SkipPath` +when another tool manages your PATH. ## Go install @@ -71,7 +75,8 @@ go install githop.xyz/GnosysLabs/Hop/cmd/hop@latest 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 @@ -87,6 +92,12 @@ install -m 755 hop "$HOME/.local/bin/hop" 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 ```bash @@ -95,5 +106,5 @@ hop help 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. diff --git a/wiki/Release-Checklist.md b/wiki/Release-Checklist.md index cda5c9d..78126d7 100644 --- a/wiki/Release-Checklist.md +++ b/wiki/Release-Checklist.md @@ -40,7 +40,8 @@ scripts/release-local.sh --snapshot 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. +`hop skill install --force` installs identical Hop-managed files at both +default skill destinations while preserving unrelated user files. ## Create a release diff --git a/wiki/Troubleshooting.md b/wiki/Troubleshooting.md index cd4588e..72f16dd 100644 --- a/wiki/Troubleshooting.md +++ b/wiki/Troubleshooting.md @@ -13,14 +13,16 @@ printf '%s\n' "$PATH" The default Unix location is `~/.local/bin`. On Windows it is `%LOCALAPPDATA%\Programs\Hop`. -## Codex does not activate Hop +## An agent integration does not activate Hop ```bash hop skill install --force ``` -Restart Codex Desktop. Mention `$hop` in a task as a deterministic activation -fallback. +The command refreshes both default skill bundles. Restart the agent client. For +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 @@ -55,7 +57,7 @@ workspace, and rerun the check. 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. +retry `hop land`. Do not bypass this with `hop accept` in interactive workflows. ## Internal ref or object warning diff --git a/wiki/Upgrading-and-Uninstalling.md b/wiki/Upgrading-and-Uninstalling.md index a931fe6..83feeb7 100644 --- a/wiki/Upgrading-and-Uninstalling.md +++ b/wiki/Upgrading-and-Uninstalling.md @@ -2,7 +2,8 @@ ## 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 curl -fsSL https://githop.xyz/GnosysLabs/Hop/raw/branch/main/scripts/install.sh | sh @@ -31,7 +32,7 @@ hop skill install --force hop doctor ``` -Restart Codex Desktop when the installed skill changed. +Restart any agent client whose installed skill changed. ## 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 one-way schema change. -## Uninstall the CLI and skill +## Uninstall the CLI and skill bundles Unix default: ```bash rm -f "$HOME/.local/bin/hop" +rm -rf "$HOME/.agents/skills/hop" rm -rf "${CODEX_HOME:-$HOME/.codex}/skills/hop" ``` @@ -59,9 +61,12 @@ Windows PowerShell: ```powershell 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" ``` +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. Uninstalling the program does not delete project-local `.hop/` histories. That