Skip to content
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ concurrency:
cancel-in-progress: true

env:
GO_VERSION: "1.26.5"
GO_VERSION: "1.26.6"
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"

jobs:
Expand Down
38 changes: 38 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,44 @@ Format based on [Keep a Changelog](https://keepachangelog.com/). This project ad
- asciinema export — `trace session export --format asciinema` renders a transcript as a playable v2 cast.

### Changed
- **README truth fixes** — Quick Start now describes both composition
surfaces accurately (library embedded as `hawk trace ...`, plus the
buildable-but-unreleased `cmd/trace` standalone entrypoint), workflow
examples note the `hawk trace` prefix, the Configuration section documents
the legacy `.entire/` read compat, and the Development section points at
`docs/architecture.md` instead of the GitNexus-boilerplate CLAUDE.md.
- **ogen generator pin aligned with go.mod** — the `//go:generate` directive
in `internal/coreapi/gen.go` now pins ogen@v1.23.0 (go.mod requires
v1.23.0; the directive previously pinned v1.20.3). Regenerating the
oas_*.go files is still pending (large diff, tracked separately).
- **Settings moved to `.trace/` with a read-compat shim** — the canonical
settings files are now `.trace/settings.json` and
`.trace/settings.local.json` (matching the README), replacing the
pre-rebrand `.entire/` locations. Reads fall back to the legacy `.entire/`
files when the canonical ones are absent (including setup detection via
`IsSetUp`/`IsSetUpAny` and the raw read-modify-write API), and the first
save copies the legacy content to the canonical location — a lazy,
non-destructive migration that never deletes or rewrites the legacy files.
Session data keeps using `.entire/metadata` on the checkpoint branch, and
both directories are excluded from checkpoints and cleaned up by
`uninstall --force`.
- **Hook binary resolution survives the rebrand and hawk embedding** — the
default git-hook prefix and the production wrapper probes no longer hardcode
the literal `entire`. They resolve through the new `agent.HookCommandPrefix`
in priority order: the basename of the running executable when it is a known
name (`trace`, `entire`, or `hawk`, which embeds this CLI as `hawk trace`),
then `trace` when reachable on PATH, then legacy `entire`, with `entire` as
the final fallback so already-installed setups keep working. Under hawk
distributions (no `entire` on PATH) wrappers now probe and exec `hawk trace`
instead of silently exiting and disabling capture. Wrapper recognition
(uninstall/upgrade) accepts every binary-name form, including wrappers
installed by older builds.
- **Root command rebranded to `trace`** — `cli.NewRootCmd()` now reports
`Use: "trace"` (was `entire`), `Short` matches the README tagline ("Git-native
session capture for AI coding agents"), the getting-started help no longer
points at the defunct `docs.entire.io` docs, the version banner reads
`Trace CLI`, hidden-alias deprecation hints name `trace …` commands, and the
agent-help drill-down pointer derives from the live root command name.
- **Version re-baselined to `0.1.0`** in
`cli/versioninfo/versioninfo.go`. Aligns trace with the rest
of the hawk-eco ecosystem (`hawk`, `tok`, `eyrie`, `yaad`, `sight`,
Expand Down
18 changes: 13 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,11 @@ individual strategy, checkpoint, agent, storage, or UI packages. Changes to

## Quick Start

Trace is a **library**, not a standalone binary. Its full command tree is built by
`cli.NewRootCmd()` and surfaced inside the **Hawk** CLI as `hawk trace ...` (Hawk is in
development — no public install yet). There is no separate `trace` binary to install.
Trace is a **library first**: its full command tree is built by `cli.NewRootCmd()`
and surfaced inside the **Hawk** CLI as `hawk trace ...` (Hawk is in development —
no public install yet). A minimal standalone entrypoint also exists at
`cmd/trace/main.go`; it is not built or released by the Makefile, but contributors
can produce a local `trace` binary with `go build ./cmd/trace`.

**Contributors — build/test the library from source:**

Expand Down Expand Up @@ -118,6 +120,9 @@ Your Branch trace/checkpoints/v1

## Typical Workflow

The examples below use the standalone `trace` binary; under Hawk the same
commands run as `hawk trace ...`.

### 1. Enable

```bash
Expand Down Expand Up @@ -205,7 +210,10 @@ Run `trace <command> --help` for detailed usage.

## Configuration

Trace stores config in `.trace/` at the repo root.
Trace stores config in `.trace/` at the repo root. Repositories set up before
the rebrand keep working: a legacy `.entire/settings.json` (or
`.entire/settings.local.json`) is still read when the `.trace/` file is absent,
and its content is copied to the new location on the first save.

### Project settings (`.trace/settings.json`)

Expand Down Expand Up @@ -315,7 +323,7 @@ mise run test:ci
mise run fmt && mise run lint
```

See [CLAUDE.md](CLAUDE.md) for architecture details.
See [docs/architecture.md](docs/architecture.md) for architecture details.

---

Expand Down
154 changes: 154 additions & 0 deletions cli/agent/binary.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package agent

import (
"os"
"os/exec"
"path/filepath"
"strings"
)

// Binary names for hook-command generation and recognition.
//
// These live in the shared agent package (not package cli) because both the
// strategy layer (git hooks) and every agent integration (wrapper probes)
// need them, while package cli imports both of those and cannot be imported
// by them without a cycle.
const (
// BinaryName is the canonical name of this CLI's binary. It is the name
// hooks assume when nothing more specific can be resolved, and the name
// the root cobra command reports (see cli.NewRootCmd).
BinaryName = "trace"

// LegacyBinaryName is the pre-rebrand binary name. Hook commands and
// config files installed by older builds still reference it, so it is
// kept as a recognition and fallback name.
LegacyBinaryName = "entire"

// EmbeddedHostPrefix is the invocation prefix used when this CLI runs
// inside the hawk binary, which embeds the root command as its `trace`
// subcommand (hawk's cmd/trace.go: rootCmd.AddCommand(tracecli.NewRootCmd())).
EmbeddedHostPrefix = "hawk trace"
)

// hookBinaryExecutable and hookBinaryLookPath are indirected so tests can
// simulate the running binary and the searchable PATH. Override them via
// SetHookBinaryResolutionForTesting.
var (
hookBinaryExecutable = os.Executable
hookBinaryLookPath = exec.LookPath
)

// SetHookBinaryResolutionForTesting overrides how HookCommandPrefix observes
// the running executable and the PATH, and returns a restore function.
// Test-only; non-parallel tests only (parallel tests read these vars).
func SetHookBinaryResolutionForTesting(exe func() (string, error), lookPath func(string) (string, error)) func() {
oldExe, oldLookPath := hookBinaryExecutable, hookBinaryLookPath
hookBinaryExecutable = exe
hookBinaryLookPath = lookPath
return func() {
hookBinaryExecutable = oldExe
hookBinaryLookPath = oldLookPath
}
}

// PinLegacyHookBinaryForTesting pins hook-binary resolution to the legacy
// name so tests asserting the historical wrapper strings stay deterministic
// regardless of what trace/entire binaries happen to be on the host PATH
// (e.g. macOS ships an unrelated /usr/bin/trace). Test-only; non-parallel
// tests only.
func PinLegacyHookBinaryForTesting() func() {
notFound := func(string) (string, error) {
return "", &os.PathError{Op: "exec.LookPath", Path: "x", Err: os.ErrNotExist}
}
return SetHookBinaryResolutionForTesting(func() (string, error) {
return "/usr/local/bin/entire", nil
}, notFound)
}

// HookCommandPrefix returns the command prefix installed hooks should use to
// invoke this CLI ("trace", "entire", or "hawk trace"), resolved in priority
// order:
//
// 1. the basename of the running executable, when it is a known name
// (trace, entire, or the hawk host that embeds this CLI);
// 2. BinaryName ("trace"), when a binary of that name is reachable on PATH;
// 3. LegacyBinaryName ("entire"), when only the legacy name is on PATH;
// 4. LegacyBinaryName — the final fallback, matching the historical baked
// default so resolution is never worse than older builds' behavior.
//
// Callers that offer an explicit override (the local-dev launcher or
// --absolute-git-hook-path) apply it before consulting this function.
//
// Like UseWindowsProductionHooks, this is deliberately not memoized: it runs
// once per hook install, and re-probing lets a host that gains or loses a
// binary migrate its hooks on the next install.
func HookCommandPrefix() string {
if exe, err := hookBinaryExecutable(); err == nil {
if prefix, ok := knownExecutableHookPrefix(filepath.Base(exe)); ok {
return prefix
}
}
if _, err := hookBinaryLookPath(BinaryName); err == nil {
return BinaryName
}
if _, err := hookBinaryLookPath(LegacyBinaryName); err == nil {
return LegacyBinaryName
}
return LegacyBinaryName
}

// knownExecutableHookPrefix maps a known executable basename to the hook
// invocation prefix for it. Windows basenames keep their ".exe" suffix.
func knownExecutableHookPrefix(base string) (string, bool) {
base = strings.TrimSuffix(base, ".exe")
switch base {
case BinaryName:
return BinaryName, true
case LegacyBinaryName:
return LegacyBinaryName, true
case "hawk":
return EmbeddedHostPrefix, true
}
return "", false
}

// hookBinaryProbeNames lists every binary name that generated wrappers may
// probe for. Recognition uses the full set so hooks installed under any name
// (including by older builds) stay recognizable for removal and upgrade.
func hookBinaryProbeNames() []string {
return []string{BinaryName, LegacyBinaryName, "hawk"}
}

// managedBinaryCommandPrefixes are command prefixes for every binary-name
// form hooks may be invoked through, independent of caller-supplied prefix
// lists. They keep uninstall/upgrade flows recognizing hooks after the rebrand
// renamed the binary (trace) and hawk embedding introduced the "hawk trace"
// form, without requiring every agent package to extend its own prefix list.
func managedBinaryCommandPrefixes() []string {
return []string{
BinaryName + " ",
LegacyBinaryName + " ",
EmbeddedHostPrefix + " ",
}
}

// productionHookWrapperTarget resolves the binary a production wrapper should
// probe and exec. The wrapped command's leading legacy binary token (agents
// still build commands as "entire hooks <agent> <verb>") is rewritten to the
// resolved invocation prefix; commands that do not start with the legacy
// binary name (local-dev launchers, absolute paths) pass through unchanged.
func productionHookWrapperTarget(command string) (probe, adaptedCommand string) {
prefix := HookCommandPrefix()
adapted := command
if prefix != LegacyBinaryName && strings.HasPrefix(command, LegacyBinaryName+" ") {
adapted = prefix + command[len(LegacyBinaryName):]
}
return hookCommandProbeName(prefix), adapted
}

// hookCommandProbeName returns the single PATH entry name a wrapper probes
// for the given invocation prefix ("hawk trace" probes "hawk").
func hookCommandProbeName(prefix string) string {
name, _, _ := strings.Cut(prefix, " ")
return name
}
Loading
Loading