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
123 changes: 55 additions & 68 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,64 +4,79 @@

# go-code

`go-code` is a local-first coding agent for working inside real repositories. It gives you an installable terminal command, a TUI for interactive development, a streamed HTTP runtime, provider and model routing, workspace-aware tool calls, and enough visibility to understand what the agent is doing while it works.
`go-code` is a local-first coding agent written in Go. One installed command gives you the same agent three ways: an interactive TUI for daily development, a single-shot CLI for scripting, and a streamed HTTP API for building on top of — all running in the repository where you launched it, against the model provider you choose.

The project is implemented in Go. The public command is `go-code`; the internal module name is still `go-agent-harness` while the product surface settles.
There is no hosted service or separate control plane. The TUI and CLI are thin clients over one public HTTP/SSE API: `go-code` auto-starts the local `harnessd` daemon when needed, streams every model turn, tool call, and approval as events, and records runs so you can inspect, continue, replay, and search them later.

## What You Get
## Use the TUI

- `go-code`: launch the TUI from any repository.
- `go-code "prompt"`: run one coding prompt and stream the result.
- `go-code --server`: start the local `harnessd` daemon and leave it running.
- A streamed event API for runs, tool calls, model usage, approvals, subagents, conversations, and replay.
- Provider catalog support for OpenAI, Anthropic, Google, DeepSeek, Z.ai, OpenRouter-style routes, and local catalog pricing.
- Workspace-aware execution so the agent works in the project directory where you launched it.

## Install

Install the latest `main` build with Homebrew:
Install the latest `main` build with Homebrew and launch it from any repository:

```bash
brew install --HEAD dennisonbertram/go-code/go-code
```

Then run it from any repository:

```bash
export OPENAI_API_KEY="..."
go-code
```

Source install is still available if you do not use Homebrew:
The TUI runs the agent in the project directory where you launched it, streaming assistant output and tool activity as it works. A daemon is started automatically if one is not already running; a pre-existing server is left alone.

If you do not use Homebrew, install from source:

```bash
git clone https://github.com/dennisonbertram/go-code.git
cd go-code
./scripts/install.sh --add-to-path
```

Open a new shell, or add the printed PATH line for your current shell.
Then open a new shell, or add the printed PATH line to your current one.

## Script it from the shell

Common modes:
For one-shot prompts and run management, the same command works headlessly:

```bash
go-code # interactive TUI in the current project
go-code "summarize this repo" # single-shot prompt
go-code --server # persistent local daemon
go-code runs # list known runs
go-code "summarize this repo" # single-shot prompt, streamed to stdout
go-code runs # list known runs
go-code show <run-id> # inspect one run
go-code continue <run-id> "..." # continue and stream a completed run
go-code replay <run-id> # replay a recorded run when rollout capture is enabled
go-code search <query> # search run metadata
go-code improve --dry-run # plan the self-improvement test loop
```

Completed runs also keep a searchable workflow recap when persistence is enabled:
goal, changed files, tests run, failure cause, fix pattern, useful commands, and
a continuation prompt. `go-code search <query>` matches those recap fields.
When persistence is enabled, completed runs keep a searchable workflow recap — goal, changed files, tests run, failure cause, fix pattern, useful commands, and a continuation prompt. `go-code search <query>` matches those recap fields.

## API Keys
## Build on the API

Set the provider keys you plan to use before starting a run:
Everything the TUI does goes through the daemon's streamed run API, which you can use directly:

```bash
go-code --server # start harnessd in the background and print its URL
```

The most commonly used endpoints:

```text
GET /healthz
GET /v1/models
GET /v1/providers
POST /v1/runs
GET /v1/runs/{id}/events
POST /v1/runs/{id}/continue
POST /v1/runs/{id}/steer
POST /v1/runs/{id}/compact
POST /v1/runs/{id}/cancel
GET /v1/conversations/
GET /v1/skills
GET /v1/subagents
POST /v1/subagents
```

Run requests support prompt, model, provider, workspace, sandbox, approval, tool, profile, reasoning, and budget fields. Canonical event names live in `internal/harness/events.go`.

## Pick your providers

Set keys for the providers you plan to use — only those:

```bash
export OPENAI_API_KEY="..."
Expand All @@ -71,9 +86,11 @@ export DEEPSEEK_API_KEY="..."
export ZAI_API_KEY="..."
```

You only need keys for the providers you use. You can also configure provider keys through the TUI and server APIs as the harness evolves.
The provider catalog covers OpenAI, Anthropic, Google, DeepSeek, Z.ai, and OpenRouter-style routes, with local catalog pricing for cost tracking. Keys can also be configured through the TUI and server APIs.

## Develop this repository

## Running From Source
The public command is `go-code`; the internal Go module is still named `go-agent-harness` while the product surface settles.

For development or debugging, run the server and CLI directly:

Expand All @@ -89,7 +106,9 @@ tmux new-session -d -s go-code-server 'cd /path/to/go-code && go run ./cmd/harne
tmux attach-session -t go-code-server
```

## Repository Map
### Layout

The core of the system:

- `cmd/harnesscli`: command-line client and terminal UI.
- `cmd/harnessd`: local HTTP daemon and runtime bootstrap.
Expand All @@ -98,43 +117,11 @@ tmux attach-session -t go-code-server
- `internal/provider`: provider clients, model catalogs, pricing, and routing.
- `internal/workspace`: local, container, VM, and worktree workspace implementations.
- `catalog/`: model and pricing catalogs used at runtime.
- `prompts/`: bundled prompt assets installed with `go-code`.
- `apps/`: experimental apps that integrate with the harness.
- `benchmarks/`: Terminal Bench and overnight benchmark harnesses.
- `harness_agent/`: Python adapter used by benchmark runners.
- `skills/`: bundled skill fixtures and validation coverage.
- `demo/`: small static demos and smoke-test pages.
- `build/`: container/build packaging assets.
- `testdata/`: shared test fixtures.
- `playground/`: separate-module experiments and training exercises.
- `docs/`: runbooks, design notes, logs, Pages source, and project context.
- `scripts/`: install, development, Symphony, and regression helpers.

The repo root is kept for product entrypoints and project metadata. Scratch snippets and exercises should live under `playground/` or a dedicated test fixture.

## HTTP Surface

The server exposes a streamed coding-agent API. The most commonly used endpoints are:

```text
GET /healthz
GET /v1/models
GET /v1/providers
POST /v1/runs
GET /v1/runs/{id}/events
POST /v1/runs/{id}/continue
POST /v1/runs/{id}/steer
POST /v1/runs/{id}/compact
POST /v1/runs/{id}/cancel
GET /v1/conversations/
GET /v1/skills
GET /v1/subagents
POST /v1/subagents
```

Run requests support prompt, model, provider, workspace, sandbox, approval, tool, profile, reasoning, and budget fields. Canonical event names live in `internal/harness/events.go`.
Supporting directories: `prompts/` (bundled prompt assets), `apps/` (experimental integrations), `benchmarks/` and `harness_agent/` (Terminal Bench harnesses; Python), `skills/` (bundled skill fixtures), `demo/` (static demos), `build/` (packaging assets), `testdata/` (shared fixtures), `playground/` (separate-module experiments), and `scripts/` (install, development, Symphony, and regression helpers). The repo root is kept for product entrypoints and project metadata; scratch snippets belong under `playground/` or a dedicated test fixture.

## Testing
### Testing

Focused checks for the install and TUI path:

Expand All @@ -151,7 +138,7 @@ GOCACHE=/tmp/go-build ./scripts/test-regression.sh

Follow `docs/runbooks/testing.md` for strict TDD expectations, behavior tests, regression tests, and merge gates.

## Documentation
### Documentation

- Public page source: `docs/site/`
- Distribution runbook: `docs/runbooks/distribution.md`
Expand Down
72 changes: 72 additions & 0 deletions docs/design/nanocodex-comparison-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Cleaner Boundaries and Tool Ergonomics (nanocodex comparison)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Add the new design doc to the index

This commit adds a new file under docs/design, but docs/design/INDEX.md still has no entry for nanocodex-comparison-plan.md. AGENTS.md directs agents to maintain folder indexes via docs/runbooks/documentation-maintenance.md, which requires updating the local INDEX.md immediately after adding a file, so the documented navigation future agents rely on is now stale; please add a concise index entry for this plan.

Useful? React with 👍 / 👎.


Status: Workstream A landed; B and C planned.

A code comparison against [nanocodex](https://github.com/gakonst/nanocodex) — a
deliberately small, library-first Rust agents SDK — surfaced three improvements
worth adopting here, without giving up what makes go-code different (a local
product with a service API, multi-provider routing, approvals, persistence).

## Workstream A — Typed tool registration (landed)

Nanocodex's `#[tool]` macro derives the JSON schema from a typed function
signature. Our Go equivalent is `tools.NewTyped` / `tools.MustTyped`
(`internal/harness/tools/typed.go`): define a tool as a typed function over an
args struct and the Parameters schema is derived by reflection from the
struct's fields and tags (`json` name + omitempty/pointer optionality, `desc`,
`min`/`max`, `enum`). The schema and the decode target can no longer drift
apart, and per-tool boilerplate (unmarshal, error wrapping, result
marshalling) disappears.

Pilot migrations: `glob` and `git_status`. Schema parity with the previous
hand-written literals is locked in by `typed_parity_test.go` (canonical-JSON
comparison), alongside behavior tests. Remaining hand-written tools migrate
opportunistically — `NewTyped` wraps the existing `Tool`/`Handler` types, so
both styles coexist and the registry, `ParallelSafe`/`Mutating` flags, policy
wrapping, and approval machinery are untouched.

## Workstream B — Split `internal/harness` (planned)

`internal/harness` is a single ~100k-line package with 124 root files; its
types file imports forensics, observational memory, store, and working memory
directly. Nanocodex's five-crate layout (dependency-light core / service /
tools / mcp / facade) is the model. Plan:

1. **Inventory PR (no code moves):** script the intra-package dependency graph
and record the target package map here. Candidate clusters from the file
layout: `event` (events.go), `permission` (permission_rules, plan_mode),
`convstore` (conversation_store*, sqlite), `broker` (approval / ask-user /
checkpoint brokers), `registry` (registry.go + typed registration), with
the runner loop remaining in `harness`.
2. **Extract leaf-first, one cluster per PR, using type aliases** (`type X =
newpkg.X` left in `package harness`) so the ~30 importing packages are
untouched in the move PR. Each PR is small, CI-green, merged same day —
never a long-lived split branch (`main` squash-merges concurrently).
3. **Importer-update PRs** then retire the aliases cluster by cluster.
4. **Guardrail:** a lint or test asserting the extracted core types package
imports nothing heavier than stdlib, so the boundary stays true.

Verification per PR: `go build ./internal/... ./cmd/...`, the key-free smoke
(`go test ./internal/server/... -run TestRunSmoke`), and
`scripts/test-regression.sh` before merge.

## Workstream C — Public Go client (planned, after B's events extraction)

An embeddable story shaped like nanocodex's three-line hello world, but honest
to our architecture: a typed client over the existing HTTP/SSE API rather than
an in-process runner.

1. Graduate the extracted event/run-request wire types to a public package
(e.g. `pkg/gocode/wire`) that `internal/server` itself imports — a single
source of truth, no drift, no parity tests to maintain. This is why C waits
for B's events extraction.
2. Build `pkg/gocode`: a `Client` with `StartRun`, `Continue`, `Cancel`, and
an `Events(ctx, runID)` SSE iterator returning typed events. Marked
v0/experimental.
3. Integration-test against the fake provider (`HARNESS_PROVIDER=fake`).
4. Add a short Go example to README's "Build on the API" section.

## Sequencing

A ships first (this PR). B1 (inventory) and B2 (events extraction) next; C
starts once B2 lands; remaining B extractions continue in parallel with C.
15 changes: 15 additions & 0 deletions docs/logs/long-term-thinking-log.md
Original file line number Diff line number Diff line change
Expand Up @@ -1270,3 +1270,18 @@ Decision rule: when uncertain, default to `command intent` and `user intent` bel
- Open questions:
- None for this tier.
- Next verification step: review the three-commit diff on `fix-harness-tier2-security`, then promote through the repo's normal verify-and-merge flow.

## 2026-07-19 (Typed tool registration — Workstream A of the nanocodex-comparison plan)

- Command intent: Land the first workstream of `docs/design/nanocodex-comparison-plan.md` on `claude/reader-cleanup-value-prop-spooyb`: a typed tool-registration helper (`tools.NewTyped`/`MustTyped`) that derives the Parameters JSON schema from a Go args struct, plus pilot migrations of `glob` and `git_status`, with exact schema parity to the previous hand-written literals.
- User intent: Close the DX gap with nanocodex's `#[tool]` macro — a tool's schema and its decode target should be one artifact that cannot drift — while leaving the registry, `ParallelSafe`/`Mutating` flags, policy wrapping, and approval machinery untouched.
- Success definition:
- `internal/harness/tools/typed.go` derives schemas by reflection (json-tag names, omitempty/pointer optionality, `desc`/`min`/`max`/`enum` tags, nested structs/slices/maps), decodes args (empty/null tolerated as zero value), and marshals results (string/RawMessage passthrough); proven by 8 `TestNewTyped*` tests including construction-error paths.
- Pilot migrations preserve wire-visible behavior: `typed_parity_test.go` locks the generated schemas against the pre-migration literals via canonical-JSON comparison, plus metadata (Action/ParallelSafe/Tags) and behavior tests (glob match/limit/error paths; git_status porcelain default via `*bool` since the old handler defaulted true only when the key was absent).
- Verification green: `go build ./internal/... ./cmd/...`, `go vet ./internal/harness/tools/`, full `./internal/harness/tools/...` suites, `./internal/server/... -run TestRunSmoke`, and harness `Registry|Catalog` tests.
- Non-goals: migrating the remaining ~50 hand-written tools (opportunistic, both styles coexist); Workstreams B (harness split) and C (public Go client) — planned in the design doc, not started.
- Guardrails/constraints:
- One deliberate error-message change: glob with empty raw args now fails "pattern is required" instead of a JSON parse error (the wrapper tolerates absent args); noted in tests.
- Known pre-existing failures, unrelated (verified via `git stash` on the unmodified tree): `benchmarks/terminal_bench/reference_solutions` packages don't build (no `main`), and `TestEnsureWorkspaceRootUsable/non-writable_root` fails when running as uid 0 (root writes to non-writable dirs).
- Open questions: whether `min`/`max`/`enum` tag coverage suffices for the wider migration or a `schema:"..."` escape hatch is needed — decide when migrating a tool that needs more.
- Next verification step: review the PR diff, then B1 (dependency-graph inventory) per the design doc's sequencing.
44 changes: 17 additions & 27 deletions internal/harness/tools/git_status.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package tools

import (
"context"
"encoding/json"
"fmt"
"path/filepath"
"strings"
Expand All @@ -11,54 +10,45 @@ import (
"go-agent-harness/internal/harness/tools/descriptions"
)

type gitStatusArgs struct {
// Porcelain is a pointer so an absent key keeps the porcelain default
// while an explicit false switches to human-readable output.
Porcelain *bool `json:"porcelain,omitempty"`
}

func gitStatusTool(workspaceRoot string) Tool {
def := Definition{
return MustTyped(TypedSpec{
Name: "git_status",
Description: descriptions.Load("git_status"),
Action: ActionRead,
ParallelSafe: true,
Tags: []string{"git", "status", "repository", "staged", "modified"},
Parameters: map[string]any{
"type": "object",
"properties": map[string]any{
"porcelain": map[string]any{"type": "boolean"},
},
},
}

handler := func(ctx context.Context, raw json.RawMessage) (string, error) {
args := struct {
Porcelain bool `json:"porcelain"`
}{Porcelain: true}
if len(raw) > 0 {
if err := json.Unmarshal(raw, &args); err != nil {
return "", fmt.Errorf("parse git_status args: %w", err)
}
}, func(ctx context.Context, args gitStatusArgs) (any, error) {
porcelain := true
if args.Porcelain != nil {
porcelain = *args.Porcelain
}

absRoot, err := filepath.Abs(workspaceRoot)
if err != nil {
return "", fmt.Errorf("resolve workspace root: %w", err)
return nil, fmt.Errorf("resolve workspace root: %w", err)
}

cmdArgs := []string{"-C", absRoot, "status"}
if args.Porcelain {
if porcelain {
cmdArgs = append(cmdArgs, "--porcelain=v1")
}
output, exitCode, timedOut, err := runCommand(ctx, 30*time.Second, "git", cmdArgs...)
if err != nil {
return "", fmt.Errorf("git status failed: %w", err)
return nil, fmt.Errorf("git status failed: %w", err)
}

trimmed := strings.TrimSpace(output)
result := map[string]any{
return map[string]any{
"clean": trimmed == "",
"output": trimmed,
"exit_code": exitCode,
"timed_out": timedOut,
}
return MarshalToolResult(result)
}

return Tool{Definition: def, Handler: handler}
}, nil
})
}
Loading
Loading