Skip to content
Merged
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
13 changes: 13 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Normalize line endings. Go tooling (gofmt, go vet) treats CRLF as
# unformatted, so everything textual is stored and checked out as LF
# regardless of the contributor's platform.
* text=auto eol=lf

# Binary assets — never touch line endings.
*.png binary
*.gif binary
*.jpg binary
*.jpeg binary
*.webm binary
*.mp4 binary
*.ico binary
43 changes: 43 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
build-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-go@v5
with:
# Match the toolchain pinned in go.work.
go-version: "1.26.x"
cache-dependency-path: |
**/go.sum

- name: gofmt
run: |
unformatted="$(gofmt -l $(git ls-files '*.go'))"
if [ -n "$unformatted" ]; then
echo "gofmt needs to run on:" >&2
echo "$unformatted" >&2
exit 1
fi

# The repo root is a go.work workspace, not a module, so
# `go <cmd> ./...` from the root doesn't span the members.
# Run vet / build / test inside each workspace module instead;
# the module list is read from go.work so it stays in sync as
# modules are added or removed.
- name: Build, vet, and test each workspace module
run: |
set -e
for m in $(grep -oE '\./[^ ]+' go.work); do
echo "::group::$m"
( cd "$m" && go vet ./... && go build ./... && go test ./... -count=1 )
echo "::endgroup::"
done
14 changes: 7 additions & 7 deletions adapters/claudecode/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,8 @@ type sessionState struct {
// to send back in the control_response and a chan to wake up the
// goroutine that's blocking on the user's decision.
type pendingPerm struct {
id string // GACT permission id (perm_xxx)
requestID string // claude's control_request request_id
id string // GACT permission id (perm_xxx)
requestID string // claude's control_request request_id
sessionID string
record map[string]any // PermissionRequest dict for GET /v1/permissions
resp chan permResp
Expand Down Expand Up @@ -666,11 +666,11 @@ func (s *Server) captureCatalogs(initEv map[string]any) {
gactStatus = "disconnected"
}
s.mcpServers = append(s.mcpServers, map[string]any{
"id": slugify(name),
"name": name,
"transport": "stdio",
"status": gactStatus,
"x_claudecode_raw_status": rawStatus,
"id": slugify(name),
"name": name,
"transport": "stdio",
"status": gactStatus,
"x_claudecode_raw_status": rawStatus,
})
}
}
Expand Down
1 change: 0 additions & 1 deletion adapters/claudecode/subprocess.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,4 +193,3 @@ func (cp *claudeProcess) close() {
_ = cp.stdin.Close()
_ = cp.cmd.Wait()
}

6 changes: 3 additions & 3 deletions adapters/crush/messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,9 +175,9 @@ func translatePart(w crushPartWrapper, index int) (gact.Part, string, error) {
}
return gact.Part{
ID: id, Type: gact.PartTypeToolResult,
CallID: d.ToolCallID,
IsError: d.IsError,
Content: []gact.Part{{Type: gact.PartTypeText, Text: d.Content}},
CallID: d.ToolCallID,
IsError: d.IsError,
Content: []gact.Part{{Type: gact.PartTypeText, Text: d.Content}},
Metadata: meta,
}, "", nil

Expand Down
22 changes: 11 additions & 11 deletions adapters/crush/translate.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,17 @@ type CrushWorkspace struct {

// CrushSession mirrors crush proto.Session.
type CrushSession struct {
ID string `json:"id"`
WorkspaceID string `json:"workspace_id,omitempty"`
ParentSessionID string `json:"parent_session_id,omitempty"`
Title string `json:"title,omitempty"`
MessageCount int `json:"message_count,omitempty"`
PromptTokens int `json:"prompt_tokens,omitempty"`
CompletionTokens int `json:"completion_tokens,omitempty"`
Cost float64 `json:"cost,omitempty"`
SummaryMessageID string `json:"summary_message_id,omitempty"`
CreatedAt int64 `json:"created_at,omitempty"`
UpdatedAt int64 `json:"updated_at,omitempty"`
ID string `json:"id"`
WorkspaceID string `json:"workspace_id,omitempty"`
ParentSessionID string `json:"parent_session_id,omitempty"`
Title string `json:"title,omitempty"`
MessageCount int `json:"message_count,omitempty"`
PromptTokens int `json:"prompt_tokens,omitempty"`
CompletionTokens int `json:"completion_tokens,omitempty"`
Cost float64 `json:"cost,omitempty"`
SummaryMessageID string `json:"summary_message_id,omitempty"`
CreatedAt int64 `json:"created_at,omitempty"`
UpdatedAt int64 `json:"updated_at,omitempty"`
}

// WorkspaceToGact maps Crush workspace → GACT workspace. Crush carries
Expand Down
2 changes: 1 addition & 1 deletion adapters/crush/translate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ func TestSessionToGact(t *testing.T) {
in := CrushSession{
ID: "ses_1", Title: "fix it", PromptTokens: 1500, CompletionTokens: 600, Cost: 0.0135,
SummaryMessageID: "msg_summary",
CreatedAt: 1700000000,
CreatedAt: 1700000000,
}
g := SessionToGact(in, "ws_default")
if g.WorkspaceID != "ws_default" {
Expand Down
6 changes: 3 additions & 3 deletions adapters/goose/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -537,9 +537,9 @@ func (s *Server) runUpstreamReply(sid, text string) {

// Mark session running.
s.broadcast(sid, eventEnvelope("session.status_changed", map[string]any{
"session_id": sid,
"status": gact.StatusRunning,
"prev_status": gact.StatusIdle,
"session_id": sid,
"status": gact.StatusRunning,
"prev_status": gact.StatusIdle,
}))

resp, err := s.client.Post(s.upstream+"/reply",
Expand Down
25 changes: 13 additions & 12 deletions adapters/goose/translate.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,11 +170,11 @@ func languageFor(path string) string {
// counts, recipe, extension data) are left out so JSON decode is
// tolerant to additions on the upstream side.
type gooseSession struct {
ID string `json:"id"`
Name string `json:"name"`
WorkingDir string `json:"working_dir"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
ID string `json:"id"`
Name string `json:"name"`
WorkingDir string `json:"working_dir"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
// MMMMMMM1: conversation is included on per-id session reads (it's
// Option<Conversation> upstream, where Conversation is a newtype
// over Vec<Message>). When absent we just expose an empty
Expand All @@ -186,10 +186,10 @@ type gooseSession struct {
// the fields the bridge needs are decoded; metadata + everything else
// flows through opaquely.
type gooseMessage struct {
ID *string `json:"id,omitempty"`
Role string `json:"role"`
Created int64 `json:"created"`
Content []map[string]any `json:"content"`
ID *string `json:"id,omitempty"`
Role string `json:"role"`
Created int64 `json:"created"`
Content []map[string]any `json:"content"`
}

// gooseSessionList is the shape returned by Goose's GET /sessions.
Expand Down Expand Up @@ -247,6 +247,7 @@ func roleToGact(r string) string {
// handles both shapes:
// - {"text": {"text": "..."}} (untagged)
// - {"type": "text", "text": "..."} (internally tagged)
//
// Unknown variants serialise as a text placeholder per the SPEC §5.4
// forward-compat rule.
func contentToGactPart(raw map[string]any) gact.Part {
Expand Down Expand Up @@ -352,9 +353,9 @@ func toolRespToGact(raw map[string]any) gact.Part {
contentParts = append(contentParts, gact.NewTextPart("[empty tool response]"))
}
return gact.Part{
Type: gact.PartTypeToolResult,
CallID: id,
Content: contentParts,
Type: gact.PartTypeToolResult,
CallID: id,
Content: contentParts,
}
}

Expand Down
20 changes: 10 additions & 10 deletions adapters/opencode/translate.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,17 +130,17 @@ func WorkspaceFromProject(p OcProjectInfo) gact.Workspace {
// packages/opencode/src/session/message-v2.ts. The TS shape is
// discriminated by `role`. We carry only the fields we translate.
type OcMessage struct {
ID string `json:"id"`
SessionID string `json:"sessionID"`
Role string `json:"role"` // "user" | "assistant"
Time OcTimes `json:"time"`
ParentID string `json:"parentID,omitempty"`
ProviderID string `json:"providerID,omitempty"`
ModelID string `json:"modelID,omitempty"`
Agent string `json:"agent,omitempty"`
Cost float64 `json:"cost,omitempty"`
ID string `json:"id"`
SessionID string `json:"sessionID"`
Role string `json:"role"` // "user" | "assistant"
Time OcTimes `json:"time"`
ParentID string `json:"parentID,omitempty"`
ProviderID string `json:"providerID,omitempty"`
ModelID string `json:"modelID,omitempty"`
Agent string `json:"agent,omitempty"`
Cost float64 `json:"cost,omitempty"`
Tokens OcTokens `json:"tokens,omitempty"`
Finish string `json:"finish,omitempty"`
Finish string `json:"finish,omitempty"`
}

// OcTokens mirrors OpenCode's tokens sub-object on assistant messages.
Expand Down
6 changes: 3 additions & 3 deletions adapters/opencode/translate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,10 +114,10 @@ func TestWorkspaceFromProject_DefaultsName(t *testing.T) {

func TestSanitizeID(t *testing.T) {
cases := map[string]string{
"abc123": "abc123",
"a_b-c": "a_b-c",
"abc123": "abc123",
"a_b-c": "a_b-c",
"with/slash": "withslash",
"emoji🎉": "emoji",
"emoji🎉": "emoji",
}
for in, want := range cases {
if got := sanitizeID(in); got != want {
Expand Down
8 changes: 4 additions & 4 deletions contract/conformance/reporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,10 @@ func FromTest(t *testing.T) Reporter {

type testTReporter struct{ t *testing.T }

func (r *testTReporter) Helper() { r.t.Helper() }
func (r *testTReporter) Errorf(format string, args ...any) { r.t.Errorf(format, args...) }
func (r *testTReporter) Fatal(args ...any) { r.t.Fatal(args...) }
func (r *testTReporter) Fatalf(format string, args ...any) { r.t.Fatalf(format, args...) }
func (r *testTReporter) Helper() { r.t.Helper() }
func (r *testTReporter) Errorf(format string, args ...any) { r.t.Errorf(format, args...) }
func (r *testTReporter) Fatal(args ...any) { r.t.Fatal(args...) }
func (r *testTReporter) Fatalf(format string, args ...any) { r.t.Fatalf(format, args...) }
func (r *testTReporter) Run(name string, fn func(Reporter)) {
r.t.Run(name, func(child *testing.T) {
fn(&testTReporter{t: child})
Expand Down
14 changes: 7 additions & 7 deletions emulator/cmd/emulator-server/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,13 +139,13 @@ func TestE2E_FullScenarioFlow(t *testing.T) {

// Read events until message.completed (then the wrap-up status_changed).
wantSeen := map[string]bool{
"message.created": false,
"message.part.added": false,
"message.part.delta": false,
"message.part.completed": false,
"tool.call.started": false,
"tool.call.completed": false,
"message.completed": false,
"message.created": false,
"message.part.added": false,
"message.part.delta": false,
"message.part.completed": false,
"tool.call.started": false,
"tool.call.completed": false,
"message.completed": false,
}
deadline := time.After(8 * time.Second)
doneEvents := 0
Expand Down
25 changes: 13 additions & 12 deletions emulator/internal/scenario/diff_script.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ import (
)

// runDiffScript demonstrates the file_diff part flow:
// parent assistant turn proposes a code change as a file_diff part with
// before/after content. The TUI renders the diff inline and lets the
// user apply or reject via /v1/sessions/{id}/diffs/{apply,reject}.
//
// parent assistant turn proposes a code change as a file_diff part with
// before/after content. The TUI renders the diff inline and lets the
// user apply or reject via /v1/sessions/{id}/diffs/{apply,reject}.
//
// Triggered by "diff" / "edit" / "patch" in the user's message.
//
Expand Down Expand Up @@ -60,23 +61,23 @@ var diffVariants = []struct {
after string
}{
{
intro: "Here's the change. Press **a** to apply or **r** to reject from the conversation pane.",
path: "main.go",
lang: "go",
intro: "Here's the change. Press **a** to apply or **r** to reject from the conversation pane.",
path: "main.go",
lang: "go",
before: "package main\n\nfunc main() {\n\tprintln(\"hello\")\n}\n",
after: "package main\n\nimport \"log\"\n\nfunc main() {\n\tlog.Println(\"hello, world\")\n}\n",
},
{
intro: "Wrap the network call in a try/except so a transient DNS blip doesn't kill the worker. **a**=apply, **r**=reject.",
path: "worker/fetch.py",
lang: "python",
intro: "Wrap the network call in a try/except so a transient DNS blip doesn't kill the worker. **a**=apply, **r**=reject.",
path: "worker/fetch.py",
lang: "python",
before: "import requests\n\n\ndef fetch_user(uid: str) -> dict:\n r = requests.get(f\"https://api.example.com/users/{uid}\", timeout=5)\n return r.json()\n",
after: "import logging\nimport requests\n\nlog = logging.getLogger(__name__)\n\n\ndef fetch_user(uid: str) -> dict:\n try:\n r = requests.get(f\"https://api.example.com/users/{uid}\", timeout=5)\n r.raise_for_status()\n return r.json()\n except requests.RequestException as exc:\n log.warning(\"fetch_user(%s) failed: %s\", uid, exc)\n return {}\n",
},
{
intro: "Swap the callback chain for async/await — same semantics, an order of magnitude less indentation. **a**=apply, **r**=reject.",
path: "src/loader.js",
lang: "javascript",
intro: "Swap the callback chain for async/await — same semantics, an order of magnitude less indentation. **a**=apply, **r**=reject.",
path: "src/loader.js",
lang: "javascript",
before: "function loadUser(id, cb) {\n db.get('users', id, function (err, row) {\n if (err) return cb(err);\n cache.set(id, row, function (err2) {\n if (err2) return cb(err2);\n cb(null, row);\n });\n });\n}\n",
after: "async function loadUser(id) {\n const row = await db.get('users', id);\n await cache.set(id, row);\n return row;\n}\n",
},
Expand Down
10 changes: 5 additions & 5 deletions emulator/internal/scenario/rich_scripts.go
Original file line number Diff line number Diff line change
Expand Up @@ -573,11 +573,11 @@ type multiToolStep struct {
// empty the script skips emitting it — variants 1 + 2 that don't
// centre on an edit keep their old shape.
var multiToolVariants = []struct {
intro string
tools []multiToolStep
followup string
diffPath string
diffLang string
intro string
tools []multiToolStep
followup string
diffPath string
diffLang string
diffBefore string
diffAfter string
}{
Expand Down
16 changes: 8 additions & 8 deletions emulator/internal/scenario/routing_script.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ import (
//
// What it emits (in order):
//
// 1. session.status_changed → running
// 2. routing_decision part as the FIRST part of the assistant
// message — selected_agent matched by keyword against the
// catalog's tier-2 entries (code_expert / research_expert /
// data_expert). heuristic = true, confidence = 0.85.
// 3. session.agent_routed event carrying selected_agent + rationale.
// 4. A text part answering with the picked agent's voice.
// 5. message.completed.
// 1. session.status_changed → running
// 2. routing_decision part as the FIRST part of the assistant
// message — selected_agent matched by keyword against the
// catalog's tier-2 entries (code_expert / research_expert /
// data_expert). heuristic = true, confidence = 0.85.
// 3. session.agent_routed event carrying selected_agent + rationale.
// 4. A text part answering with the picked agent's voice.
// 5. message.completed.
//
// This is the end-to-end path a TUI consumer exercises for badge
// rendering + routing rationale display. Pairs with
Expand Down
4 changes: 2 additions & 2 deletions emulator/internal/scenario/routing_script_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,8 @@ func TestRoutingScript_EmitsRoutingDecisionPart(t *testing.T) {
// distinct keyword hints.
func TestRoutingScript_PicksByKeyword(t *testing.T) {
cases := []struct {
prompt string
wantAgent string
prompt string
wantAgent string
}{
{"please refactor this function", "code_expert"},
{"can you search the web for pandas", "research_expert"},
Expand Down
4 changes: 2 additions & 2 deletions emulator/internal/scenario/scenario.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,8 +188,8 @@ func (e *Engine) publishStatus(sessionID, status string) {
WorkspaceID: updated.WorkspaceID,
SessionID: sessionID,
Payload: map[string]any{
"session_id": sessionID,
"status": status,
"session_id": sessionID,
"status": status,
"prev_status": prev,
},
})
Expand Down
Loading
Loading