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