go-cli-default-path-fallback
Root cause (GAP-008): the list-known-friends subcommand hardcoded a deployment-specific default, /opt/hermes-demo/helix/known-friends.json. Every host that is not that one deployment reported FILE_NOT_FOUND, and the error gave no hint about how to point at the right file. Two secondary defects: an empty registry file was fed to json.Unmarshal and surfaced as unexpected end of JSON input (a parse error) instead of "no agents", and the missing-file error gave users no actionable hint.
The fix, in three parts (full working code in ~/gap008/):
1. os.Stat-based default resolution — prod path if present, else ~/.helix/<file>:
var ProdDefault = "/opt/hermes-demo/helix/known-friends.json" // var, so tests can probe a fake deployment
func ResolveDefault(explicit string) string {
if explicit != "" { // --registry wins; no stat probing needed
return explicit
}
if _, err := os.Stat(ProdDefault); err == nil { // host IS the prod deployment
return ProdDefault
}
home, err := os.UserHomeDir()
if err != nil || home == "" {
return "known-friends.json" // last resort, still better than a foreign path
}
return filepath.Join(home, ".helix", "known-friends.json") // every other host
}
2. Empty registry file ⇒ NO_AGENTS, exit 0 (not a parse error):
reg := &Registry{}
if len(bytes.TrimSpace(data)) == 0 { // zero bytes OR whitespace-only
// Valid empty registry: "no entries", exit 0. Never feed this to
// json.Unmarshal, which would error "unexpected end of JSON input".
return reg, nil
}
if err := json.Unmarshal(data, reg); err != nil {
return nil, fmt.Errorf("parse %s: %w", path, err) // real corruption still errors
}
3. FILE_NOT_FOUND error with a pass --flag or set ENV hint:
var ErrFileNotFound = errors.New("FILE_NOT_FOUND")
func Load(path string) (*Registry, error) {
data, err := os.ReadFile(path)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil, fmt.Errorf("%w: %s (pass --registry or set HELIX_REGISTRY)",
ErrFileNotFound, path)
}
return nil, fmt.Errorf("read %s: %w", path, err)
}
// ...empty check, json.Unmarshal as above
}
Exit codes: 0 OK/NO_AGENTS, 1 FILE_NOT_FOUND, 2 parse error. Callers use errors.Is(err, ErrFileNotFound) to route to the right code, so the hint survives wrapping.
Verified with a real Go module (`go1.26.0`), `go vet` clean, **9/9 unit tests passing**, plus a live binary run: ``` $ go test ./... -v --- PASS: TestResolveDefault_ProdPathPresent (prod path exists → prod path chosen) --- PASS: TestResolveDefault_FallsBackToUserLocal (no prod path → ~/.helix/..., never /opt/hermes-demo) --- PASS: TestResolveDefault_ExplicitPathWins (--registry beats default probing) --- PASS: TestLoad_EmptyFileIsNoAgents (0-byte file → NO_AGENTS, exit 0) --- PASS: TestLoad_WhitespaceOnlyFileIsNoAgents (\n\n \n → NO_AGENTS, exit 0) --- PASS: TestLoad_FileNotFoundHasHint (FILE_NOT_FOUND + --registry + HELIX_REGISTRY hint) --- PASS: TestLoad_MalformedNonEmptyIsParseError (garbage still parse error, not FILE_NOT_FOUND) --- PASS: TestLoad_ValidEntries (happy path prints entries, exit 0) --- PASS: TestMainEndToEnd (full subcommand flow) ok gap008/internal/registry 0.003s ``` End-to-end binary output (real exit codes): | Scenario | Output | Exit | |---|---|---| | Missing file | `FILE_NOT_FOUND: /tmp/definitely-missing.json (pass --registry or set HELIX_REGISTRY)` | 1 | | Empty file | `NO_AGENTS` | 0 | | Whitespace-only file | `NO_AGENTS` | 0 | | Valid entries | `alice 0x1` / `bob 0x2` | 0 | | No prod path, default run | `FILE_NOT_FOUND: /tmp/fakehome/.helix/known-friends.json (pass ...)` | 1 | | Malformed non-empty | `parse /tmp/bad.json: invalid character 'n' ...` | 2 | Edge cases covered: empty vs. whitespace-only files (both NO_AGENTS), genuine JSON corruption still rejected, explicit `--registry` overriding defaults, prod host behavior preserved, and `HOME` unset/cwd fallback. The test suite itself caught two real bugs during development (prod path not overridable in tests; whitespace-only files misparsed) — both fixed.
{"model": "deepseek-v4-flash", "problem_class": "go-cli-default-path-fallback", "result": "passed", "tests": 9}Root cause (GAP-008): the list-known-friends subcommand hardcoded a deployment-specific default, /opt/hermes-demo/helix/known-friends.json. Every host that is not that one deployment reported FILE_NOT_FOUND, and the error gave no hint about how to point at the right file. Two secondary defects: an empty registry file was fed to json.Unmarshal and surfaced as unexpected end of JSON input (a parse error) instead of "no agents", and the missing-file error gave users no actionable hint.
The fix, in three parts (full working code in ~/gap008/):
1. os.Stat-based default resolution — prod path if present, else ~/.helix/<file>:
var ProdDefault = "/opt/hermes-demo/helix/known-friends.json" // var, so tests can probe a fake deployment
func ResolveDefault(explicit string) string {
if explicit != "" { // --registry wins; no stat probing needed
return explicit
}
if _, err := os.Stat(ProdDefault); err == nil { // host IS the prod deployment
return ProdDefault
}
home, err := os.UserHomeDir()
if err != nil || home == "" {
return "known-friends.json" // last resort, still better than a foreign path
}
return filepath.Join(home, ".helix", "known-friends.json") // every other host
}
2. Empty registry file ⇒ NO_AGENTS, exit 0 (not a parse error):
reg := &Registry{}
if len(bytes.TrimSpace(data)) == 0 { // zero bytes OR whitespace-only
// Valid empty registry: "no entries", exit 0. Never feed this to
// json.Unmarshal, which would error "unexpected end of JSON input".
return reg, nil
}
if err := json.Unmarshal(data, reg); err != nil {
return nil, fmt.Errorf("parse %s: %w", path, err) // real corruption still errors
}
3. FILE_NOT_FOUND error with a pass --flag or set ENV hint:
var ErrFileNotFound = errors.New("FILE_NOT_FOUND")
func Load(path string) (*Registry, error) {
data, err := os.ReadFile(path)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil, fmt.Errorf("%w: %s (pass --registry or set HELIX_REGISTRY)",
ErrFileNotFound, path)
}
return nil, fmt.Errorf("read %s: %w", path, err)
}
// ...empty check, json.Unmarshal as above
}
Exit codes: 0 OK/NO_AGENTS, 1 FILE_NOT_FOUND, 2 parse error. Callers use errors.Is(err, ErrFileNotFound) to route to the right code, so the hint survives wrapping.
Verified with a real Go module (`go1.26.0`), `go vet` clean, **9/9 unit tests passing**, plus a live binary run: ``` $ go test ./... -v --- PASS: TestResolveDefault_ProdPathPresent (prod path exists → prod path chosen) --- PASS: TestResolveDefault_FallsBackToUserLocal (no prod path → ~/.helix/..., never /opt/hermes-demo) --- PASS: TestResolveDefault_ExplicitPathWins (--registry beats default probing) --- PASS: TestLoad_EmptyFileIsNoAgents (0-byte file → NO_AGENTS, exit 0) --- PASS: TestLoad_WhitespaceOnlyFileIsNoAgents (\n\n \n → NO_AGENTS, exit 0) --- PASS: TestLoad_FileNotFoundHasHint (FILE_NOT_FOUND + --registry + HELIX_REGISTRY hint) --- PASS: TestLoad_MalformedNonEmptyIsParseError (garbage still parse error, not FILE_NOT_FOUND) --- PASS: TestLoad_ValidEntries (happy path prints entries, exit 0) --- PASS: TestMainEndToEnd (full subcommand flow) ok gap008/internal/registry 0.003s ``` End-to-end binary output (real exit codes): | Scenario | Output | Exit | |---|---|---| | Missing file | `FILE_NOT_FOUND: /tmp/definitely-missing.json (pass --registry or set HELIX_REGISTRY)` | 1 | | Empty file | `NO_AGENTS` | 0 | | Whitespace-only file | `NO_AGENTS` | 0 | | Valid entries | `alice 0x1` / `bob 0x2` | 0 | | No prod path, default run | `FILE_NOT_FOUND: /tmp/fakehome/.helix/known-friends.json (pass ...)` | 1 | | Malformed non-empty | `parse /tmp/bad.json: invalid character 'n' ...` | 2 | Edge cases covered: empty vs. whitespace-only files (both NO_AGENTS), genuine JSON corruption still rejected, explicit `--registry` overriding defaults, prod host behavior preserved, and `HOME` unset/cwd fallback. The test suite itself caught two real bugs during development (prod path not overridable in tests; whitespace-only files misparsed) — both fixed.
{"model": "deepseek-v4-flash", "problem_class": "go-cli-default-path-fallback", "result": "passed", "tests": 9}