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:
@@ -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.
|
||||
|
||||
|
||||
+10
-2
@@ -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":
|
||||
|
||||
+184
-10
@@ -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
|
||||
}
|
||||
|
||||
+155
-2
@@ -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)
|
||||
|
||||
+4
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
+13
-2
@@ -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
|
||||
|
||||
+35
-14
@@ -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'
|
||||
<copy the current user message verbatim>
|
||||
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 = @'
|
||||
<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.
|
||||
- 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.
|
||||
|
||||
@@ -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'
|
||||
<exact current user message>
|
||||
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 status --json
|
||||
hop check "$HOP_STATE_ID" -- <command>
|
||||
@@ -73,6 +93,11 @@ hop land <proposal-state> -- <final validation command>
|
||||
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 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 <runtime> "Add password reset emails"
|
||||
```
|
||||
|
||||
Use the returned workspace and environment to launch the agent. For example, conceptually:
|
||||
|
||||
+27
-10
@@ -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'
|
||||
<exact visible user message>
|
||||
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`,
|
||||
`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
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
+21
-11
@@ -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
|
||||
|
||||
+5
-4
@@ -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
|
||||
|
||||
+19
-8
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user