diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..e5d22db --- /dev/null +++ b/.dockerignore @@ -0,0 +1,13 @@ +.git +.github +.gitignore +*.md +.env +.env.* +Dockerfile +.dockerignore +coverage.out +docs/ +deploy/ +api/ +e2e/ diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..e5c8eb8 --- /dev/null +++ b/.env.example @@ -0,0 +1,8 @@ +# trace environment variables — copy to .env and fill in +# trace is a CLI tool with no persistent network service. + +# PostHog analytics (optional — leave empty to disable telemetry) +POSTHOG_API_KEY= +POSTHOG_ENDPOINT=https://app.posthog.com +# Set to "true" to disable all telemetry +TRACE_NO_TELEMETRY=false diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..8631f57 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,63 @@ +name: Docker + +on: + push: + branches: [main] + tags: ["v*"] + pull_request: + branches: [main] + paths: + - "Dockerfile" + - "**.go" + - "go.mod" + - "go.sum" + +permissions: + contents: read + packages: write + +env: + REGISTRY: ghcr.io + IMAGE_NAME: graycodeai/trace + +jobs: + build-and-push: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GHCR + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=sha,prefix=sha- + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + build-args: | + VERSION=${{ github.ref_name }} + COMMIT=${{ github.sha }} + BUILD_DATE=${{ github.event.head_commit.timestamp }} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..746fec0 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,27 @@ +FROM golang:1.26.3-alpine AS builder + +RUN apk add --no-cache git ca-certificates tzdata + +WORKDIR /build +COPY . . +RUN go mod download && go mod verify +ARG VERSION=dev +ARG COMMIT=none +ARG BUILD_DATE=unknown +RUN CGO_ENABLED=0 GOOS=linux go build -trimpath \ + -ldflags="-s -w \ + -X main.Version=${VERSION} \ + -X main.Commit=${COMMIT} \ + -X main.BuildDate=${BUILD_DATE}" \ + -o trace ./cmd/trace + +FROM alpine:3.21 +RUN apk add --no-cache ca-certificates tini git && \ + adduser -D -u 1000 trace + +COPY --from=builder /build/trace /usr/local/bin/trace +COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo + +USER trace +ENTRYPOINT ["tini", "--", "trace"] +CMD ["--help"] diff --git a/api/openapi.yaml b/api/openapi.yaml new file mode 100644 index 0000000..732b00f --- /dev/null +++ b/api/openapi.yaml @@ -0,0 +1,72 @@ +openapi: "3.1.0" +info: + title: trace — Session Capture Tool Reference + description: | + trace is a git-native session capture CLI for AI coding agents. + It hooks into the git workflow to capture agent sessions alongside commits, + creating a searchable record of how code was written. + + trace is a CLI tool — no HTTP server is exposed. + This document describes the CLI command surface as a machine-readable reference. + version: "0.1.0" + license: + name: MIT + url: https://github.com/GrayCodeAI/trace/blob/main/LICENSE + contact: + url: https://github.com/GrayCodeAI/trace + +# No servers — trace is a CLI tool, not a network service. + +x-cli-commands: + trace_enable: + description: Install git hooks to capture sessions in this repository + usage: trace enable + flags: [] + + trace_disable: + description: Remove git hooks and stop capturing sessions + usage: trace disable + flags: [] + + trace_status: + description: Show capture status and current session info + usage: trace status + flags: [] + + trace_checkpoint: + description: Create a checkpoint in the current session + usage: trace checkpoint [message] + subcommands: + rewind: + description: Rewind to a previous checkpoint + usage: trace checkpoint rewind + + trace_session: + description: Manage sessions + subcommands: + resume: + description: Resume a previous session + usage: trace session resume + list: + description: List all sessions + usage: trace session list + + trace_investigate: + description: Investigate a session or commit for AI agent activity + usage: trace investigate + flags: + - name: format + short: f + type: string + enum: [text, json, markdown] + default: text + + trace_doctor: + description: Run diagnostics on trace installation + usage: trace doctor + flags: [] + + trace_agent: + description: Configure agent-specific integrations + usage: trace agent + flags: [] diff --git a/cmd/trace/cli/explain_test.go b/cmd/trace/cli/explain_test.go index e1c5cc7..21e1fe8 100644 --- a/cmd/trace/cli/explain_test.go +++ b/cmd/trace/cli/explain_test.go @@ -1911,6 +1911,13 @@ func TestRunExplainCheckpoint_V2CheckpointRemoteFallbackResolvesRawTranscript(t checkpointRepo, err := git.PlainOpen(checkpointDir) require.NoError(t, err) + t.Cleanup(func() { + // Close the underlying storage to release file descriptors before + // t.TempDir() attempts to remove the directory. + if storer, ok := checkpointRepo.Storer.(interface{ Close() error }); ok { + _ = storer.Close() + } + }) cpID := id.MustCheckpointID("121212121212") rawTranscript := []byte(`{"type":"user","message":{"content":[{"type":"text","text":"raw from checkpoint_remote"}]}}` + "\n") diff --git a/cmd/trace/cli/testutil/testutil.go b/cmd/trace/cli/testutil/testutil.go index 64fde7d..1da0f5b 100644 --- a/cmd/trace/cli/testutil/testutil.go +++ b/cmd/trace/cli/testutil/testutil.go @@ -52,6 +52,8 @@ func InitRepo(t *testing.T, repoDir string) { cfg.Raw = config.New() } cfg.Raw.Section("commit").SetOption("gpgsign", "false") + cfg.Raw.Section("gc").SetOption("auto", "0") + cfg.Raw.Section("gc").SetOption("autoDetach", "false") cfg.Core.AutoCRLF = "true" if err := repo.SetConfig(cfg); err != nil { diff --git a/deploy/docker/docker-compose.yml b/deploy/docker/docker-compose.yml new file mode 100644 index 0000000..d21a6b5 --- /dev/null +++ b/deploy/docker/docker-compose.yml @@ -0,0 +1,15 @@ +name: trace + +services: + trace: + build: + context: ../../ + dockerfile: Dockerfile + image: ghcr.io/graycodeai/trace:dev + entrypoint: ["trace"] + command: ["--help"] + volumes: + - type: bind + source: ${PWD} + target: /workspace + working_dir: /workspace diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..2e200bc --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,112 @@ +
+ +# 📸 trace Architecture + +**Git-Native Session Capture for AI Coding Agents** + +[![Go](https://img.shields.io/badge/Go-1.26+-00ADD8?logo=go)](https://go.dev/) +[![Type](https://img.shields.io/badge/Type-CLI-green)]() + +
+ +--- + +## 🎯 Overview + +trace hooks into your git workflow to capture AI agent sessions as you work. Sessions are indexed alongside commits, creating a **searchable record of how code was written**. + +> 💡 Works with Claude Code, Codex, Gemini CLI, Cursor, Copilot CLI, and hawk. + +--- + +## 🧱 Components + +``` +trace/ +├── api/openapi.yaml 📜 CLI command surface reference +├── cmd/trace/ 🖥️ CLI entry point +│ ├── main.go ⚡ Root cobra command +│ └── cli/ 📂 Subcommand implementations +│ ├── trace_cmd.go 🔧 Core commands (enable, disable, status) +│ ├── hooks.go 🪝 Git hook management +│ ├── checkpoint/ 💾 Checkpoint storage & retrieval +│ ├── recap/ 📝 Session recap rendering +│ ├── settings/ ⚙️ Configuration management +│ └── agent/ 🤖 Agent-specific integrations +├── codegraph_snapshot.go 📊 CodeGraphSnapshot, GraphStats, GraphDelta +├── strategy/ 📋 Checkpoint strategies +├── session/ 📂 Session state management +├── redact/ +│ ├── redact.go 🔒 Secret redaction (entropy + pattern + keyword) +│ ├── packs.go 📦 Vendor-specific pattern packs +│ ├── pii.go 🔐 PII detection +│ └── custom.go ⚙️ Custom redaction rules +├── internal/ +│ └── agentlaunch/ 🚀 Agent launch detection +├── e2e/ 🧪 End-to-end test suite +├── perf/ 📈 Performance benchmarks +└── docs/ 📖 Architecture and usage docs +``` + +--- + +## 📂 Session Model + +A **session** is a unit of work containing **checkpoints**: + +| Type | Storage | Content | +|------|---------|---------| +| 💾 **Temporary** | Shadow branch (`.git/trace-sessions/`) | Full working tree state | +| 📋 **Committed** | `trace/checkpoints/v1` orphan branch | Metadata only | + +Each checkpoint has a stable **12-hex-char ID** linking user commits to metadata. + +--- + +## 🖥️ CLI Commands + +| Command | Description | +|---------|-------------| +| `trace enable` | 🪝 Install git hooks | +| `trace disable` | 🧹 Remove hooks | +| `trace status` | 📊 Show capture status | +| `trace checkpoint "msg"` | 💾 Create checkpoint | +| `trace checkpoint rewind ` | ⏪ Rewind to checkpoint | +| `trace session resume ` | 🔄 Resume a session | +| `trace investigate ` | 🔍 Investigate AI activity on a commit | +| `trace doctor` | 🩺 Run diagnostics | +| `trace agent ` | 🤖 Configure agent integration | + +--- + +## 🌳 Git-Native Storage + +Sessions are stored as **git objects** on the `trace/checkpoints/v1` orphan branch — never on the working branch. + +> 💡 Standard git tools can inspect the data. No external database required. + +--- + +## 🔒 Redaction + +Multi-layer secret redaction before storing any session data: + +| Layer | Strategy | Example | +|-------|----------|---------| +| 📊 **Entropy** | Shannon score > 4.5 | `AKIA...` (high-entropy strings) | +| 🔑 **Pattern** | Regex matching | JWTs, base64, DB URIs, API keys | +| 📝 **Keyword** | Key name detection | `password=`, `secret=` | +| 🔐 **PII** | Personal data | Emails, phone numbers, SSNs | + +--- + +## 📊 Code Graph Snapshots + +`CodeGraphSnapshot` captures project structure at checkpoint time: + +| Type | Contains | +|------|----------| +| `SymbolInfo` | Functions, types, variables | +| `ModuleInfo` | Package structure, imports | +| `ComplexityMetrics` | Cyclomatic complexity, nesting | +| `GraphDelta` | Diff between two snapshots (`CompareSnapshots()`) |