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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@

# Built binaries
/agents/entire-agent-kiro/entire-agent-kiro
/agents/entire-agent-windsurf/entire-agent-windsurf
bin/
104 changes: 104 additions & 0 deletions agents/entire-agent-windsurf/AGENT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# Windsurf - External Agent Research

## Verdict: COMPATIBLE

Windsurf Cascade exposes three lifecycle hooks that map cleanly onto the Entire turn lifecycle. Because Windsurf is an IDE (not a headless CLI), session management is fully driven by hook payloads rather than a native CLI or database.

## Static Checks

| Check | Result | Notes |
|-------|--------|-------|
| Binary present | IDE-only | Windsurf is a desktop IDE; no headless CLI equivalent exists |
| Hook keywords | PASS | `pre_user_prompt`, `post_write_code`, `post_cascade_response` |
| Session keywords | PASS | `trajectory_id` (stable per-conversation ID in all hook payloads) |
| Config directory | PASS | `.windsurf/hooks.json` (workspace-level hook config) |
| Documentation | PASS | https://docs.windsurf.com/windsurf/cascade/cascade-hooks |

## Hook Mechanism

- Config file: `.windsurf/hooks.json`
- Config format: top-level `"hooks"` object with per-hook arrays of entries
- Hook command field: `"command"` on Unix/macOS, `"powershell"` on Windows
- Hook payload: JSON on stdin (or empty for hooks that fire without payload)

| Native Hook Name | When It Fires | Protocol Event Type |
|-----------------|---------------|---------------------|
| `pre_user_prompt` | Before Cascade processes a user message | `TurnStart` (type 2) |
| `post_write_code` | After Cascade writes a file | *(no lifecycle event — file path recorded to transcript)* |
| `post_cascade_response` | After Cascade produces a response | `TurnEnd` (type 3) |

Hook payload fields:
- `trajectory_id` — stable UUID for the current Cascade conversation (used as session ID)
- `timestamp` — ISO 8601 timestamp
- `tool_info.user_prompt` — user prompt text (in `pre_user_prompt`)
- `tool_info.file_path` — file written (in `post_write_code`)
- `tool_info.response` — assistant response text (in `post_cascade_response`)

## Session Management

- Session ID source: `trajectory_id` from the hook payload; falls back to cached session ID file, then generates a UUID
- Session ID cache: `.entire/tmp/windsurf-active-session` — written on `pre_user_prompt`, read when subsequent hooks arrive without `trajectory_id`
- Session directory: `.entire/tmp/` under the repo root
- Session file path: `.entire/tmp/<trajectory_id>.json`
- Session file format: JSONL — one record per line (`{"v":1,"type":"prompt"|"file"|"response",...}`)

## Transcript

- Format: JSONL appended incrementally across hook invocations
- Record types:
- `{"v":1,"type":"prompt","content":"<user prompt>","ts":"<ISO8601>"}` — written on `pre_user_prompt`
- `{"v":1,"type":"file","path":"<file path>"}` — written on `post_write_code`
- `{"v":1,"type":"response","content":"<assistant response>","ts":"<ISO8601>"}` — written on `post_cascade_response`
- Transcript position: line count (used as offset for incremental extraction)
- No external binary or database required — transcript is built entirely from hook events

## Protocol Mapping

| Subcommand | Native Concept | Implementation Notes |
|-----------|---------------|---------------------|
| `info` | adapter metadata | declares `hooks`, `transcript_analyzer`, `compact_transcript` |
| `detect` | `.windsurf` directory | returns present=true when `.windsurf/` exists in repo root |
| `get-session-id` | cached stable session ID | reads `.entire/tmp/windsurf-active-session` |
| `get-session-dir` | Entire cache directory | returns `<repo>/.entire/tmp` |
| `resolve-session-file` | normalized cache path | returns `<dir>/<id>.json` |
| `read-session` | JSONL transcript file | reads `.entire/tmp/<id>.json` |
| `write-session` | JSONL transcript file | writes bytes back to `.entire/tmp/<id>.json` |
| `read-transcript` | raw JSONL bytes | return raw bytes from session file |
| `chunk-transcript` | raw bytes | split into base64 chunks for transport |
| `reassemble-transcript` | chunk reassembly | reverse `chunk-transcript` |
| `format-resume-command` | Windsurf resume | returns `"windsurf"` (user opens the IDE; no CLI resume path) |
| `parse-hook pre_user_prompt` | turn start | emits `EventJSON{Type:2}` with session ID and prompt |
| `parse-hook post_write_code` | file write | returns nil (records file path to transcript only) |
| `parse-hook post_cascade_response` | turn end | emits `EventJSON{Type:3}` with session ref |
| `install-hooks` | `.windsurf/hooks.json` | writes all three lifecycle hooks; idempotent |
| `uninstall-hooks` | reverse install | removes `.windsurf/hooks.json` |
| `are-hooks-installed` | config presence check | true when `.windsurf/hooks.json` exists with Entire entries |
| `get-transcript-position` | JSONL line count | returns number of lines in session file |
| `extract-modified-files` | file records in JSONL | deduplicates `"file"` records from offset |
| `extract-prompts` | prompt records in JSONL | returns `"prompt"` record content from offset |
| `extract-summary` | last response | returns last `"response"` record content |
| `compact-transcript` | JSONL → base64 JSONL | emits `user`/`assistant` lines; skips `file` records |

## Selected Capabilities

| Capability | Declared | Justification |
|-----------|----------|---------------|
| `hooks` | true | Windsurf exposes three workspace-level Cascade lifecycle hooks |
| `transcript_analyzer` | true | JSONL transcript is parsed for prompts, files, and summaries |
| `compact_transcript` | true | JSONL is compacted to base64-encoded user/assistant pairs |
| `transcript_preparer` | false | transcript is built incrementally from hooks, not pre-processed |
| `token_calculator` | false | no token-counting path needed |
| `text_generator` | false | not used |
| `hook_response_writer` | false | not used |
| `subagent_aware_extractor` | false | Windsurf Cascade does not expose a subagent model |

## Lifecycle Tests

Windsurf is an IDE agent with no automatable CLI. Lifecycle E2E tests cannot run in CI without a live Windsurf session. The agent is excluded from the default E2E suite and is opt-in via `E2E_AGENT=windsurf`. Protocol compliance is verified through `external-agents-tests verify ./entire-agent-windsurf`.

## Gaps & Limitations

- `format-resume-command` returns `"windsurf"` — there is no CLI resume path; the user must open the IDE
- `post_write_code` does not emit a lifecycle event (only records the file path to the transcript)
- Hook payload shape may change as Windsurf's hooks API evolves (noted in README with preview warning)
- Truly concurrent Cascade conversations in the same project would share the session ID cache; this is best-effort and matches the constraint that Windsurf's `trajectory_id` is always present in the payload when available
168 changes: 168 additions & 0 deletions agents/entire-agent-windsurf/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
# Windsurf External Agent for Entire CLI

> **Preview** — this integration is functional but not yet stable. Hook names and payload shapes may change as Windsurf's hooks API evolves.

Enables Entire CLI checkpoints, rewind, and transcript capture for [Windsurf](https://windsurf.com) Cascade coding sessions. Once installed, Entire automatically tracks your Windsurf sessions — creating checkpoints on commits and capturing transcripts for review.

## Prerequisites

- **Entire CLI** installed and on `PATH`
- **Windsurf** installed
- **Go 1.26+** (to build from source)

## Quick Start

### 1. Build the binary

```bash
cd agents/entire-agent-windsurf
go build -o entire-agent-windsurf ./cmd/entire-agent-windsurf
```

Or install directly:

```bash
go install ./cmd/entire-agent-windsurf
```

### 2. Verify the agent is discoverable

```bash
entire-agent-windsurf info
```

Should print JSON describing the agent's capabilities.

### 3. Enable the agent in your project

```bash
cd /path/to/your/project
entire enable
```

Entire discovers `entire-agent-windsurf` on your `PATH` and installs the hooks automatically.

### 4. Verify hooks are installed

```bash
entire-agent-windsurf are-hooks-installed
```

Should return `{"installed": true}`.

### 5. Start using Windsurf

Entire will now automatically capture checkpoints and transcripts during your Windsurf Cascade sessions.

## What Gets Installed

When you run `entire enable --agent windsurf`, the agent installs hooks in:

| Location | Purpose |
|----------|---------|
| `.windsurf/hooks.json` | Workspace-level Cascade hook configuration |

The config uses Windsurf's native top-level `"hooks"` format with three lifecycle hooks:

```json
{
"hooks": {
"pre_user_prompt": [{ "command": "entire hooks windsurf pre_user_prompt" }],
"post_write_code": [{ "command": "entire hooks windsurf post_write_code" }],
"post_cascade_response": [{ "command": "entire hooks windsurf post_cascade_response" }]
}
}
```

On Windows, `"powershell"` is used instead of `"command"`.

## Hook Lifecycle

| Windsurf hook | Entire event | What it records |
|---|---|---|
| `pre_user_prompt` | TurnStart (type 2) | Session ID from `trajectory_id`, user prompt text |
| `post_write_code` | *(no event)* | File path written to turn transcript |
| `post_cascade_response` | TurnEnd (type 3) | Response text + `session_ref` path |

Each Windsurf conversation (`trajectory_id`) maps to one Entire session. Transcripts are stored as JSONL at `.entire/tmp/<trajectory_id>.json`.

## Capabilities

| Capability | Supported | Description |
|------------|-----------|-------------|
| `hooks` | Yes | Installs and manages Windsurf Cascade lifecycle hooks |
| `transcript_analyzer` | Yes | Extracts modified files, prompts, and summaries from transcripts |
| `compact_transcript` | Yes | Produces compact JSONL for checkpoint storage |
| `transcript_preparer` | No | — |
| `token_calculator` | No | — |
| `text_generator` | No | — |
| `hook_response_writer` | No | — |
| `subagent_aware_extractor` | No | — |

## Supported Subcommands

All subcommands required by the [external agent protocol](https://github.com/entireio/cli/blob/main/docs/architecture/external-agent-protocol.md):

**Core:** `info`, `detect`, `get-session-id`, `get-session-dir`, `resolve-session-file`, `read-session`, `write-session`, `format-resume-command`

**Hooks:** `parse-hook`, `install-hooks`, `are-hooks-installed`, `uninstall-hooks`

**Transcript:** `read-transcript`, `chunk-transcript`, `reassemble-transcript`, `compact-transcript`, `get-transcript-position`, `extract-modified-files`, `extract-prompts`, `extract-summary`

## Development

```bash
# From agents/entire-agent-windsurf:
go build ./... # Build
go test ./... # Run unit tests
go build -o entire-agent-windsurf ./cmd/entire-agent-windsurf # Produce binary

# Run directly without installing:
go run ./cmd/entire-agent-windsurf info
```

## Testing

Windsurf is validated in two places:

- **Unit tests** live in this module (`internal/windsurf/*_test.go`) and cover hook parsing, hook install/uninstall, transcript extraction, and compact output.
- **Protocol compliance** runs in GitHub Actions through [`entireio/external-agents-tests`](https://github.com/entireio/external-agents-tests) against the built `entire-agent-windsurf` binary.

Windsurf is an IDE agent with no automatable CLI, so the repo-root `e2e/` lifecycle harness does not run it by default. It is registered only when `E2E_AGENT=windsurf` is set explicitly.

### Running unit tests

```bash
# From this module:
go test ./...

# With verbose output:
go test -v ./...
```

## Troubleshooting

**Agent not discovered by Entire**
- Verify the binary is on your `PATH`: `which entire-agent-windsurf`
- Confirm external-plugin discovery is enabled: `external_agents: true` must be set in your repo's `.entire/settings.json`
- Check detection: `ENTIRE_REPO_ROOT=$PWD entire-agent-windsurf detect`

**`entire enable` doesn't install hooks**

Install hooks directly:

```bash
cd /path/to/your/project
ENTIRE_REPO_ROOT=$PWD entire-agent-windsurf install-hooks --force
```

Add `--local-dev` to use the local-dev hook command form (references `${WINDSURF_PROJECT_DIR}` instead of `entire`).

**Hooks not firing**
- Verify `.windsurf/hooks.json` exists in your project
- Confirm Windsurf has workspace hooks enabled (they are on by default for workspace-level config)
- Check that `entire` is on your `PATH` when Windsurf spawns the hook shell

## Protocol

This agent implements the [Entire external agent protocol](https://github.com/entireio/cli/blob/main/docs/architecture/external-agent-protocol.md).
75 changes: 75 additions & 0 deletions agents/entire-agent-windsurf/cmd/entire-agent-windsurf/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package main

import (
"fmt"
"os"

"github.com/entireio/external-agents/agents/entire-agent-windsurf/internal/protocol"
"github.com/entireio/external-agents/agents/entire-agent-windsurf/internal/windsurf"
)

func main() {
agent := windsurf.New()

if len(os.Args) < 2 {
fatalf("usage: entire-agent-windsurf <subcommand> [args]")
}

var err error

switch os.Args[1] {
case "info":
err = protocol.WriteJSON(os.Stdout, agent.Info())
case "detect":
err = protocol.WriteJSON(os.Stdout, agent.Detect())
case "get-session-id":
err = protocol.HandleGetSessionID(os.Stdin, os.Stdout, agent)
case "get-session-dir":
err = protocol.HandleGetSessionDir(os.Args[2:], os.Stdout, agent)
case "resolve-session-file":
err = protocol.HandleResolveSessionFile(os.Args[2:], os.Stdout, agent)
case "read-session":
err = protocol.HandleReadSession(os.Stdin, os.Stdout, agent)
case "write-session":
err = protocol.HandleWriteSession(os.Stdin, agent)
case "read-transcript":
err = protocol.HandleReadTranscript(os.Args[2:], os.Stdout, agent)
case "chunk-transcript":
err = protocol.HandleChunkTranscript(os.Args[2:], os.Stdin, os.Stdout, agent)
case "reassemble-transcript":
err = protocol.HandleReassembleTranscript(os.Stdin, os.Stdout, agent)
case "compact-transcript":
err = protocol.HandleCompactTranscript(os.Args[2:], os.Stdout, agent)
case "format-resume-command":
err = protocol.HandleFormatResumeCommand(os.Args[2:], os.Stdout, agent)
case "parse-hook":
err = protocol.HandleParseHook(os.Args[2:], os.Stdin, os.Stdout, agent)
case "install-hooks":
err = protocol.HandleInstallHooks(os.Args[2:], os.Stdout, agent)
case "uninstall-hooks":
err = agent.UninstallHooks()
case "are-hooks-installed":
err = protocol.WriteJSON(os.Stdout, protocol.AreHooksInstalledResponse{
Installed: agent.AreHooksInstalled(),
})
case "get-transcript-position":
err = protocol.HandleGetTranscriptPosition(os.Args[2:], os.Stdout, agent)
case "extract-modified-files":
err = protocol.HandleExtractModifiedFiles(os.Args[2:], os.Stdout, agent)
case "extract-prompts":
err = protocol.HandleExtractPrompts(os.Args[2:], os.Stdout, agent)
case "extract-summary":
err = protocol.HandleExtractSummary(os.Args[2:], os.Stdout, agent)
default:
fatalf("unknown subcommand: %s", os.Args[1])
}

if err != nil {
fatalf("%v", err)
}
}

func fatalf(format string, args ...any) {
_, _ = fmt.Fprintf(os.Stderr, format+"\n", args...)
os.Exit(1)
}
3 changes: 3 additions & 0 deletions agents/entire-agent-windsurf/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module github.com/entireio/external-agents/agents/entire-agent-windsurf

go 1.26.0
Loading
Loading