Skip to content

Commit ee0a0b7

Browse files
authored
Feat/cursor runtime parity (#3522)
* feat(cursor): verify CLI preset, agent process disambiguation, beads handoff - Cursor preset: cursor-agent + agent ProcessNames with tmux session env gate - tmux: processNamesForSession drops ambiguous agent unless GT_AGENT=cursor - config: CLI contract test, GT_AGENT_STUB_BIN_DIR, compact_report test fix - docs/scripts: cursor-runtime beads task seed + handoff doc Closes gastown-at8.1 (verify cursor-agent CLI). * config: set Cursor ReadyDelayMs to 5000 for nudge poller Cursor has HasTurnBoundaryDrain false; readiness uses delay + background nudge poller. Align default delay with Copilot until a stable prompt prefix exists. Closes gastown-at8.2 Made-with: Cursor * runtime: extend orphan/doctor/down filters for Cursor and Copilot - TTY orphan/zombie detection: track cursor-agent, agent, copilot comm names alongside existing agents (internal/util/orphan.go). - gt down town-root scan: allow the same comm names. - doctor orphan-process check: match per-agent YOLO argv (claude/codex --dangerously-skip-permissions, cursor-agent -f, agent -f with Cursor tokens, copilot --yolo) instead of Claude-only flag. - Add unit tests for gasTownRuntimeYOLO and argvHasFlag. Closes gastown-at8.3 Made-with: Cursor * web: detect cursor-agent and copilot in crew pane command Extend pane #{pane_current_command} matching for dashboard crew state: cursor-agent, copilot symlink/binary name, and bare "agent". Closes gastown-at8.4 Made-with: Cursor * cursor: add project skill for Gas Town + cursor-agent Add .cursor/skills/gas-town-cursor with preset vs binary, hooks path, and pointers to docs and AGENTS.md. Closes gastown-at8.7 Made-with: Cursor * cursor: add .cursor/README onboarding and beads doc links Prerequisites, preset vs cursor-agent, hooks, test gate script reference, manual smoke checklist, and link to docs/cursor-runtime-beads-tasks.md. Closes gastown-at8.8 Closes gastown-at8.9 Made-with: Cursor * config: dynamic built-in preset list in gt config help + otel GT_AGENT row - Add config.BuiltInAgentPresetSummary() for sorted preset names. - Populate gt config agent list/remove/default-agent long help and --provider flag text from the registry (includes cursor, copilot, opencode, …). - Document GT_AGENT as preset names in otel architecture table. Closes gastown-at8.5 Made-with: Cursor * test: Cursor EnsureSettingsForRole, preset summary, and root-safe chmod tests - Assert Cursor hooks land in workDir; TestBuiltInAgentPresetSummary for gt config help - Skip chmod read-only installer/atomic tests when euid is root - Tweak .cursor README and cursor-runtime-bd-tasks seed wording Made-with: Cursor
1 parent e06530e commit ee0a0b7

23 files changed

Lines changed: 608 additions & 91 deletions

.cursor/README.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Cursor / `cursor-agent` in this repo
2+
3+
This directory holds **Cursor-specific** onboarding. For general Gas Town agent instructions, see [`../AGENTS.md`](../AGENTS.md) and [`../CLAUDE.md`](../CLAUDE.md).
4+
5+
## Prerequisites
6+
7+
1. **Build `gt`** from the repo root (`make build` or `go install ./cmd/gt`). Gas Town expects a working `gt` on your `PATH` for hooks and crew workflows.
8+
2. **`bd` (beads)** — issue DB under `.beads/`; see [`../CONTRIBUTING.md`](../CONTRIBUTING.md) for workflow.
9+
3. **Cursor Agent CLI** — install the `cursor-agent` binary per Cursor’s documentation. The Gas Town preset name is **`cursor`**; the process is typically **`cursor-agent`** (an **`agent`** symlink may exist).
10+
11+
## Preset vs binary
12+
13+
- **Preset:** `cursor` (`GT_AGENT=cursor`) — defined in `internal/config/agents.go`.
14+
- **CLI:** `cursor-agent` (args include **`-f`** for auto-approve in autonomous flows).
15+
16+
## Hooks
17+
18+
Hooks are installed under **`.cursor/hooks.json`** when roles are provisioned (`EnsureSettingsForRole`). After template or hook changes, restart agents (e.g. **`gt up --restore`**) so sessions pick up new files.
19+
20+
## Skills
21+
22+
See [`.cursor/skills/gas-town-cursor/SKILL.md`](skills/gas-town-cursor/SKILL.md) for agent-facing workflow (gt, resume, pointers to code).
23+
24+
## Beads / plan tracking
25+
26+
Epic tasks for Cursor runtime parity are tracked in beads; coordination notes and script:
27+
28+
- [`docs/cursor-runtime-beads-tasks.md`](../docs/cursor-runtime-beads-tasks.md)
29+
- [`scripts/cursor-runtime-bd-tasks.sh`](../scripts/cursor-runtime-bd-tasks.sh)
30+
31+
## Automated regression (local)
32+
33+
CI already runs **`go test ./...`** (same coverage as a “gate” over these packages). For a **faster loop** while touching Cursor-related code, narrow packages:
34+
35+
```bash
36+
go test ./internal/config/... ./internal/hooks/... ./internal/crew/... ./internal/tmux/... ./internal/runtime/... -count=1 -short
37+
```
38+
39+
Run as a **non-root** user if you want chmod/read-only failure tests in `hooks` and `util` (root skips those cases by design).
40+
41+
## Manual smoke (short)
42+
43+
Run these only when changing behavior that tests do not cover end-to-end:
44+
45+
1. `make build``gt` binary builds.
46+
2. `gt config agent list` — output includes the **`cursor`** preset.
47+
3. Start or attach to a dev session with **`GT_AGENT=cursor`** (or `--agent cursor`) and confirm the pane command shows **`cursor-agent`** or **`agent`** and the session receives hooks/nudges as expected.
48+
49+
For full §9-style checklists, see the Cursor parity plan document in your planning folder if present; prefer adding **automated** tests in-repo when possible.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
---
2+
name: gas-town-cursor
3+
description: >
4+
Develop and operate Gas Town with the Cursor agent preset (cursor-agent CLI):
5+
gt flags, hooks at .cursor/hooks.json, session resume, and how this repo differs from README marketing copy.
6+
---
7+
8+
# Gas Town + Cursor Agent CLI
9+
10+
Use this skill when working **in this repository** with the **`cursor`** agent preset (CLI binary **`cursor-agent`**, sometimes symlinked as **`agent`**).
11+
12+
## Concepts
13+
14+
| Name | Meaning |
15+
|------|---------|
16+
| **Preset `cursor`** | Gas Town agent id (`GT_AGENT=cursor`). Config lives in `internal/config/agents.go` (`AgentCursor`). |
17+
| **Binary `cursor-agent`** | The Cursor Agent CLI process name for pane/detection; install docs may also symlink `agent` → same binary. |
18+
| **Hooks** | Cursor lifecycle hooks are configured at **`.cursor/hooks.json`** (see preset `HooksSettingsFile`). |
19+
20+
## Essential commands
21+
22+
- Build / run `gt` from repo root: `make build` or `go run ./cmd/gt …`.
23+
- Point a session at the Cursor preset: spawn or config so the runtime uses **`--agent cursor`** (or set **`GT_AGENT=cursor`** where applicable).
24+
- After changing hooks or settings: **`gt up --restore`** (or role-specific restart) so agents reload config.
25+
26+
## Resume semantics
27+
28+
The Cursor preset uses **`--resume <chatId>`** style resume (`ResumeStyle: flag`). Session identity is not carried in a dedicated env var in the same way as Claude’s `CLAUDE_SESSION_ID`; follow the preset fields in `internal/config/agents.go` (`ResumeFlag`, `ResumeStyle`).
29+
30+
## Read more
31+
32+
- Beads / plan handoff: [`docs/cursor-runtime-beads-tasks.md`](../../../docs/cursor-runtime-beads-tasks.md)
33+
- Agent instructions for automation: [`AGENTS.md`](../../../AGENTS.md) and [`CLAUDE.md`](../../../CLAUDE.md) (project-wide, not Cursor-only)
34+
35+
## Boundary
36+
37+
Project **README** is user-facing product overview; **`.cursor/README.md`** is Cursor-specific onboarding for this repo. Prefer linking to those files instead of duplicating long install steps inside skills.

docs/agent-provider-integration.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -529,7 +529,7 @@ Current agent capabilities at a glance:
529529
| Claude | Yes (settings.json) | `--resume` (flag) | Native | Yes | arg | node, claude |
530530
| Gemini | Yes | `--resume` (flag) | `-p` | No | arg | gemini |
531531
| Codex | No | `resume` (subcmd) | `exec` subcmd | No | none | codex |
532-
| Cursor | No | `--resume` (flag) | `-p` | No | arg | cursor-agent |
532+
| Cursor | Yes (`.cursor/hooks.json`) | `--resume` (flag) | `-p` / `--print` + `--output-format` | No | arg | cursor-agent, agent |
533533
| Auggie | No | `--resume` (flag) | No | No | arg | auggie |
534534
| AMP | No | `threads continue` (subcmd) | No | No | arg | amp |
535535
| OpenCode | Yes (plugin JS) | No | `run` subcmd | No | none | opencode, node, bun |

docs/cursor-runtime-beads-tasks.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Cursor runtime plan — Beads tasks (handoff)
2+
3+
These issues track **Cursor runtime parity**, **user-facing documentation clarity** (preset `cursor` vs CLI `cursor-agent` / `agent`), and **`.cursor/` onboarding**. Issue IDs vary by database.
4+
5+
**Create issues (idempotent — skips if open `cursor-runtime`+`plan` issues exist):**
6+
7+
```bash
8+
./scripts/cursor-runtime-bd-tasks.sh
9+
```
10+
11+
**Full task scope:** see **§10a** and **§4b** in the Cursor parity plan (`cursor_runtime_parity_df5a36d7.plan.md` in your editor plans folder).
12+
13+
**T5 (docs + CLI)** explicitly covers:
14+
15+
- `gt config` / `internal/cmd/config.go` help — list **all** built-in presets, not only claude/gemini/codex.
16+
- **README** prerequisites — optional **Cursor Agent CLI** install; clarify **preset `cursor`** vs binaries.
17+
- **docs/INSTALLING.md**, **docs/reference.md** — same built-in lists as README; short note on **`cursor`**`cursor-agent`.
18+
19+
**Contributing:** [`CONTRIBUTING.md`](../CONTRIBUTING.md). Do not add `.beads/issues.jsonl` at repo root (CI). `bd vc commit` when persisting beads DB changes.
20+
21+
**Migration:** If you seeded tasks with an older script, **retitle T5** in `bd` to match the table in the plan §10a, or close duplicates.

docs/design/otel/otel-architecture.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -410,7 +410,7 @@ run.id:uuid-1234
410410
| `GT_POLECAT` | `Toast`, `Shadow`, `Furiosa` | Polecat name (rig-specific) |
411411
| `GT_CREW` | `max`, `jane` | Crew member name |
412412
| `GT_SESSION` | `gt-gastown-Toast`, `hq-mayor` | Tmux session name |
413-
| `GT_AGENT` | `claudecode`, `codex`, `copilot` | Agent override (if specified) |
413+
| `GT_AGENT` | Preset names: `claude`, `gemini`, `codex`, `cursor`, `copilot`, `opencode`, … | Agent override (if specified) |
414414
| `GT_RUN` | UUID v4 | **PR #2199** — Run identifier, primary waterfall correlation key |
415415
| `GT_ROOT` | `/Users/pa/gt` | Town root path |
416416
| `CLAUDE_CONFIG_DIR` | `~/gt/.claude` | Runtime config directory (for agent overrides) |

internal/cmd/compact_report_test.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,20 @@ func TestWispTypeToCategory(t *testing.T) {
2828
t.Run(tc.wispType, func(t *testing.T) {
2929
got := wispTypeToCategory(tc.wispType, "")
3030
if got != tc.want {
31-
t.Errorf("wispTypeToCategory(%q) = %q, want %q", tc.wispType, got, tc.want)
31+
t.Errorf("wispTypeToCategory(%q, \"\") = %q, want %q", tc.wispType, got, tc.want)
3232
}
3333
})
3434
}
3535
}
3636

37+
func TestWispTypeToCategory_TitlePatrolFallback(t *testing.T) {
38+
t.Parallel()
39+
got := wispTypeToCategory("", "nightly patrol sweep")
40+
if got != "Patrols" {
41+
t.Errorf("empty wisp_type + patrol in title = %q, want Patrols", got)
42+
}
43+
}
44+
3745
func TestBuildReport(t *testing.T) {
3846
result := &compactResult{
3947
Deleted: []compactAction{

internal/cmd/config.go

Lines changed: 45 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,8 @@ Commands:
4242
var configAgentListCmd = &cobra.Command{
4343
Use: "list",
4444
Short: "List all agents",
45-
Long: `List all available agents (built-in and custom).
46-
47-
Shows all built-in agent presets (claude, gemini, codex) and any
48-
custom agents defined in your town settings.
49-
50-
Examples:
51-
gt config agent list # Text output
52-
gt config agent list --json # JSON output`,
53-
RunE: runConfigAgentList,
45+
Long: "", // Set in init() — includes full built-in preset list from config.BuiltInAgentPresetSummary()
46+
RunE: runConfigAgentList,
5447
}
5548

5649
var configAgentGetCmd = &cobra.Command{
@@ -97,15 +90,9 @@ Examples:
9790
var configAgentRemoveCmd = &cobra.Command{
9891
Use: "remove <name>",
9992
Short: "Remove custom agent",
100-
Long: `Remove a custom agent definition from town settings.
101-
102-
This removes a custom agent from your town settings. Built-in agents
103-
(claude, gemini, codex) cannot be removed.
104-
105-
Examples:
106-
gt config agent remove claude-glm`,
107-
Args: cobra.ExactArgs(1),
108-
RunE: runConfigAgentRemove,
93+
Long: "", // Set in init() — includes full built-in preset list
94+
Args: cobra.ExactArgs(1),
95+
RunE: runConfigAgentRemove,
10996
}
11097

11198
// Cost-tier subcommand
@@ -192,24 +179,8 @@ func runConfigCostTier(cmd *cobra.Command, args []string) error {
192179
var configDefaultAgentCmd = &cobra.Command{
193180
Use: "default-agent [name]",
194181
Short: "Get or set default agent",
195-
Long: `Get or set the default agent for the town.
196-
197-
With no arguments, shows the current default agent.
198-
With an argument, sets the default agent to the specified name.
199-
200-
The default agent is used when a rig doesn't specify its own agent
201-
setting. Can be a built-in preset (claude, gemini, codex) or a
202-
custom agent name.
203-
204-
Use 'gt config default-agent list' to see all available agents.
205-
206-
Examples:
207-
gt config default-agent # Show current default
208-
gt config default-agent list # List available agents
209-
gt config default-agent claude # Set to claude
210-
gt config default-agent gemini # Set to gemini
211-
gt config default-agent my-custom # Set to custom agent`,
212-
RunE: runConfigDefaultAgent,
182+
Long: "", // Set in init() — includes full built-in preset list
183+
RunE: runConfigDefaultAgent,
213184
}
214185

215186
var configDefaultAgentListCmd = &cobra.Command{
@@ -1255,10 +1226,47 @@ func parseBool(s string) (bool, error) {
12551226
}
12561227

12571228
func init() {
1229+
presets := config.BuiltInAgentPresetSummary()
1230+
1231+
configAgentListCmd.Long = fmt.Sprintf(`List all available agents (built-in and custom).
1232+
1233+
Shows all built-in agent presets (%s) and any
1234+
custom agents defined in your town settings.
1235+
1236+
Examples:
1237+
gt config agent list # Text output
1238+
gt config agent list --json # JSON output`, presets)
1239+
1240+
configAgentRemoveCmd.Long = fmt.Sprintf(`Remove a custom agent definition from town settings.
1241+
1242+
This removes a custom agent from your town settings. Built-in agents
1243+
(%s) cannot be removed.
1244+
1245+
Examples:
1246+
gt config agent remove claude-glm`, presets)
1247+
1248+
configDefaultAgentCmd.Long = fmt.Sprintf(`Get or set the default agent for the town.
1249+
1250+
With no arguments, shows the current default agent.
1251+
With an argument, sets the default agent to the specified name.
1252+
1253+
The default agent is used when a rig doesn't specify its own agent
1254+
setting. Can be a built-in preset (%s) or a
1255+
custom agent name.
1256+
1257+
Use 'gt config default-agent list' to see all available agents.
1258+
1259+
Examples:
1260+
gt config default-agent # Show current default
1261+
gt config default-agent list # List available agents
1262+
gt config default-agent claude # Set to claude
1263+
gt config default-agent gemini # Set to gemini
1264+
gt config default-agent my-custom # Set to custom agent`, presets)
1265+
12581266
// Add flags
12591267
configAgentListCmd.Flags().BoolVar(&configAgentListJSON, "json", false, "Output as JSON")
12601268
configDefaultAgentListCmd.Flags().BoolVar(&configDefaultAgentListJSON, "json", false, "Output as JSON")
1261-
configAgentSetCmd.Flags().StringVar(&configAgentSetProvider, "provider", "", "Agent provider preset (e.g. claude, gemini, codex); inferred from command name if not set")
1269+
configAgentSetCmd.Flags().StringVar(&configAgentSetProvider, "provider", "", fmt.Sprintf("Agent provider preset (e.g. %s); inferred from command name if not set", presets))
12621270

12631271
// Add agent subcommands
12641272
configAgentCmd := &cobra.Command{

internal/cmd/down.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -742,8 +742,8 @@ func verifyShutdown(t *tmux.Tmux, townRoot string) []string {
742742
return respawned
743743
}
744744

745-
// findOrphanedClaudeProcesses finds Claude/node processes that are running in the
746-
// town directory but aren't associated with any active tmux session.
745+
// findOrphanedClaudeProcesses finds Gas Town agent processes (claude/codex/opencode/cursor-agent/copilot/node)
746+
// that are running in the town directory but aren't associated with any active tmux session.
747747
// This can happen when tmux sessions are killed but child processes don't terminate.
748748
//
749749
// Only matches processes whose full command line references the town root path,
@@ -778,7 +778,7 @@ func findOrphanedClaudeProcesses(townRoot string) []int {
778778
// Only consider known Gas Town process names
779779
comm := strings.ToLower(fields[1])
780780
switch comm {
781-
case "claude", "claude-code", "codex", "node":
781+
case "claude", "claude-code", "codex", "opencode", "cursor-agent", "agent", "copilot", "node":
782782
// Potential Gas Town process
783783
default:
784784
continue
@@ -1205,4 +1205,3 @@ func containsPathBoundary(line, path string) bool {
12051205
}
12061206
return false
12071207
}
1208-

internal/config/agents.go

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"encoding/json"
66
"os"
77
"path/filepath"
8+
"sort"
89
"strings"
910
"sync"
1011
)
@@ -61,7 +62,7 @@ type AgentPresetInfo struct {
6162

6263
// ProcessNames are the process names to look for when detecting if the agent is running.
6364
// Used by tmux.IsAgentRunning to check pane_current_command.
64-
// E.g., ["node"] for Claude, ["cursor-agent"] for Cursor.
65+
// E.g., ["node"] for Claude, ["cursor-agent", "agent"] for Cursor (install script symlinks both names).
6566
ProcessNames []string `json:"process_names,omitempty"`
6667

6768
// SessionIDEnv is the environment variable for session ID.
@@ -285,15 +286,18 @@ var builtinPresets = map[AgentPreset]*AgentPresetInfo{
285286
InstructionsFile: "AGENTS.md",
286287
},
287288
AgentCursor: {
288-
Name: AgentCursor,
289-
Command: "cursor-agent",
290-
Args: []string{"-f"}, // Force mode (YOLO equivalent), -p requires prompt
291-
ProcessNames: []string{"cursor-agent"},
289+
Name: AgentCursor,
290+
Command: "cursor-agent",
291+
// -f/--force: auto-approve tool use (see cursor-agent --help). Install script also symlinks "agent" -> same binary.
292+
Args: []string{"-f"},
293+
// cursor-agent + agent (install symlinks). Pane matching for "agent" requires session env (GT_AGENT=cursor or GT_PROCESS_NAMES includes cursor-agent); see internal/tmux processNamesForSession.
294+
ProcessNames: []string{"cursor-agent", "agent"},
292295
SessionIDEnv: "", // Uses --resume with chatId directly
293296
ResumeFlag: "--resume",
294297
ResumeStyle: "flag",
295298
SupportsHooks: true,
296299
SupportsForkSession: false,
300+
// Non-interactive/headless: -p/--print + --output-format json (matches cursor-agent --help).
297301
NonInteractive: &NonInteractiveConfig{
298302
PromptFlag: "-p",
299303
OutputFlag: "--output-format json",
@@ -303,8 +307,10 @@ var builtinPresets = map[AgentPreset]*AgentPresetInfo{
303307
ConfigDir: ".cursor",
304308
HooksProvider: "cursor",
305309
HooksDir: ".cursor",
306-
HooksSettingsFile: "hooks.json",
310+
HooksSettingsFile: "hooks.json", // installed path: .cursor/hooks.json
307311
InstructionsFile: "AGENTS.md",
312+
// No stable ReadyPromptPrefix yet; delay before nudge poller / early input (HasTurnBoundaryDrain is false — see Copilot).
313+
ReadyDelayMs: 5000,
308314
},
309315
AgentAuggie: {
310316
Name: AgentAuggie,
@@ -582,6 +588,14 @@ func ListAgentPresets() []string {
582588
return names
583589
}
584590

591+
// BuiltInAgentPresetSummary returns a sorted, comma-separated list of built-in preset names
592+
// for CLI help text (gt config agent list, default-agent, --provider, etc.).
593+
func BuiltInAgentPresetSummary() string {
594+
names := ListAgentPresets()
595+
sort.Strings(names)
596+
return strings.Join(names, ", ")
597+
}
598+
585599
// DefaultAgentPreset returns the default agent preset (Claude).
586600
func DefaultAgentPreset() AgentPreset {
587601
return AgentClaude

0 commit comments

Comments
 (0)