Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ Trace hooks into your Git workflow to capture AI agent sessions as you work. Ses

Trace is a Hawk support engine. Keep the dependency edge one-way:

- trace uses local-only types (trace/redaction event types are trace-scoped, not shared contracts)
- trace keeps redaction and storage types local; portable graph exports use
`hawk-core-contracts/graph`
- do not import `hawk/internal/*`
- do not import removed legacy path `hawk/shared/types`
- do not import other engines (`eyrie`, `yaad`, `tok`, `sight`, `inspect`) — engines are peers, not dependencies
Expand Down Expand Up @@ -164,6 +165,8 @@ trace disable # Removes hooks, code untouched
| `trace checkpoint` | List, explain, rewind, search checkpoints |
| `trace checkpoint rewind` | Rewind to a previous checkpoint |
| `trace checkpoint explain` | Explain a session or checkpoint |
| `trace graph export` | Export sessions and checkpoints as portable graph nodes, edges, and events |
| `trace graph correlation --hawk-session <id>` | Resolve exact Trace session/checkpoint IDs captured for a Hawk persisted session |
| `trace fork` | Clone a checkpoint into a new independent session for A/B testing |
| `trace annotate` | Attach a comment to a session or checkpoint |
| `trace ci-init` | Configure Trace to auto-capture sessions in CI |
Expand All @@ -176,6 +179,26 @@ trace disable # Removes hooks, code untouched
| `trace login` | Authenticate with Trace |
| `trace version` | Show CLI version |

### Hawk correlation

Set `TRACE_TAG_HAWK_SESSION_ID` to the Hawk persisted-session ID before Trace's
session-start hook runs. Trace stores it through the existing session-tag
mechanism:

```bash
TRACE_TAG_HAWK_SESSION_ID=hawk-session-123 <start-agent-command>
trace graph correlation --hawk-session hawk-session-123
```

The correlation command is read-only. It returns `trace.correlation/v1` JSON
with every exact Trace session match and its committed checkpoint IDs.
`checkpoint_lookup_complete` states whether checkpoint enumeration succeeded;
session identity remains usable when it is false, but consumers must ignore
the checkpoint list. A missing mapping produces `"matches": []`; Trace never
guesses from a branch, commit, timestamp, prompt, or similar-looking session
ID. The response exposes only correlation IDs and lifecycle fields, not
arbitrary session metadata.

Run `trace <command> --help` for detailed usage.

---
Expand Down
25 changes: 22 additions & 3 deletions cli/agent_group.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cli

import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
Expand Down Expand Up @@ -56,16 +57,19 @@ func runAgentMenu(ctx context.Context, w io.Writer) error {
}

func newAgentListCmd() *cobra.Command {
return &cobra.Command{
var jsonOut bool
cmd := &cobra.Command{
Use: "list",
Short: "List installed and available agents",
RunE: func(cmd *cobra.Command, _ []string) error {
return runAgentList(cmd.Context(), cmd.OutOrStdout())
return runAgentList(cmd.Context(), cmd.OutOrStdout(), jsonOut)
},
}
cmd.Flags().BoolVar(&jsonOut, "json", false, "output agent list as JSON")
return cmd
}

func runAgentList(ctx context.Context, w io.Writer) error {
func runAgentList(ctx context.Context, w io.Writer, jsonOut bool) error {
installed := GetAgentsWithHooksInstalled(ctx)
installedSet := make(map[types.AgentName]struct{}, len(installed))
for _, name := range installed {
Expand All @@ -74,6 +78,21 @@ func runAgentList(ctx context.Context, w io.Writer) error {

all := agent.StringList()

if jsonOut {
type agentEntry struct {
Name string `json:"name"`
Installed bool `json:"installed"`
}
entries := make([]agentEntry, 0, len(all))
for _, name := range all {
_, ok := installedSet[types.AgentName(name)]
entries = append(entries, agentEntry{Name: name, Installed: ok})
}
enc := json.NewEncoder(w)
enc.SetIndent("", " ")
return enc.Encode(entries)
}

fmt.Fprintln(w, "Agents:")
for _, name := range all {
marker := " "
Expand Down
41 changes: 39 additions & 2 deletions cli/agent_group_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package cli
import (
"bytes"
"context"
"encoding/json"
"strings"
"testing"

Expand All @@ -14,7 +15,7 @@ func TestRunAgentList_ListsAvailableAgents(t *testing.T) {
t.Parallel()

var buf bytes.Buffer
if err := runAgentList(context.Background(), &buf); err != nil {
if err := runAgentList(context.Background(), &buf, false); err != nil {
t.Fatalf("runAgentList: %v", err)
}
out := buf.String()
Expand Down Expand Up @@ -44,7 +45,7 @@ func TestRunAgentList_MarksInstalledWithCheck(t *testing.T) {
t.Parallel()

var buf bytes.Buffer
if err := runAgentList(context.Background(), &buf); err != nil {
if err := runAgentList(context.Background(), &buf, false); err != nil {
t.Fatalf("runAgentList: %v", err)
}
out := buf.String()
Expand All @@ -57,6 +58,42 @@ func TestRunAgentList_MarksInstalledWithCheck(t *testing.T) {
}
}

func TestRunAgentList_JSONOutput(t *testing.T) {
t.Parallel()

var buf bytes.Buffer
if err := runAgentList(context.Background(), &buf, true); err != nil {
t.Fatalf("runAgentList --json: %v", err)
}

var entries []struct {
Name string `json:"name"`
Installed bool `json:"installed"`
}
if err := json.Unmarshal(buf.Bytes(), &entries); err != nil {
t.Fatalf("invalid JSON output: %v\n%s", err, buf.String())
}
if len(entries) == 0 {
t.Fatalf("expected at least one agent entry, got none")
}
registered := agent.StringList()
found := false
for _, name := range registered {
for _, e := range entries {
if e.Name == name {
found = true
break
}
}
if found {
break
}
}
if !found {
t.Errorf("none of registered agents %v appeared in JSON output", registered)
}
}

func TestAgentGroupBareCommandRunsAgentMenu(t *testing.T) {
// t.Chdir cannot coexist with t.Parallel; this test mutates process CWD.
dir := t.TempDir()
Expand Down
Loading
Loading