gsd-unsupervised
v1.1.0
Published
Autonomous orchestrator for Cursor agent + GSD framework
Downloads
400
Maintainers
Readme
gsd-unsupervised
Autonomous orchestrator that drives Cursor's headless agent through the full GSD (Get Shit Done) lifecycle. It reads goals from a queue, invokes cursor-agent with GSD commands, monitors progress via .planning/STATE.md, and advances phases automatically. Built for reliable, hands-off goal-to-completion automation on a single machine.
Features
- Goal queue — Define work in
goals.md; the daemon processes pending goals sequentially or in parallel. - GSD lifecycle — Runs
/gsd/new-project→/gsd/create-roadmap→/gsd/plan-phase→/gsd/execute-planin the correct order. - Cursor agent integration — Spawns
cursor-agentheadlessly, streams commands, and handles process lifecycle (timeouts, tree-kill on shutdown). - State monitoring — Watches
.planning/STATE.mdfor phase/plan progress and emits events (phase_advanced, plan_advanced, phase_completed, goal_completed). - Crash detection & recovery — Session log at project root, resume from exact phase/plan on next run, heartbeat for liveness.
- Resource governor — CPU + memory headroom checks before each agent call so the daemon backs off instead of thrashing your box.
- Local status dashboard — Optional HTTP server (
--status-server <port>) serving an HTML dashboard and/api/statusJSON. Use--ngrokto have the daemon runngrok http <port>so the dashboard is reachable via a public URL while the process runs. - Optional SMS (Twilio) — Notifications for goal complete, goal failed, and daemon paused; requires
TWILIO_ACCOUNT_SID,TWILIO_AUTH_TOKEN,TWILIO_FROM,TWILIO_TO. If unset, the daemon runs without SMS.
Requirements
Node ≥ 18, tmux, and one of: Cursor, Claude Code, Continue (cn), Gemini CLI, or Codex.
Prerequisites
- Node.js ≥ 18
- Cursor with GSD rules installed (e.g. in
.cursor/rules/) - cursor-agent CLI (path configurable; default
agent) - CURSOR_API_KEY — Required for live runs. Get from Cursor Dashboard → Cloud Agents → User API Keys. Not required for
--dry-run.
Using cn (Continue CLI)
You can use Continue's headless CLI (cn) instead of Cursor as the agent:
- Install cn:
npm install -g @continuedev/cliorcurl -fsSL https://raw.githubusercontent.com/continuedev/continue/main/extensions/cli/scripts/install.sh | bash. - Config: Fill in the
modelssection in.continue/config.yaml(at project root). The file references GSD rules from.cursor/rules/; add your model (e.g. Anthropic, OpenAI) per Continue config reference. - Set agent:
--agent cnor"agent": "cn"in.planning/config.json. - CONTINUE_API_KEY: Required for CI/headless use. Get from continue.dev/settings/api-keys.
- Binary path: Use
GSD_CN_BINenv orcontinueCliPathin config ifcnis not on PATH.
cn outputs plain text (not NDJSON). GSD rules load from .continue/config.yaml, which references .cursor/rules/.
WSL Support & Paths
This project is WSL-aware and includes helpers and a diagnostics script for path resolution when running under WSL2:
- WSL detection lives in
src/config/wsl.ts, which can answer whether the current process is running under WSL and convert/mnt/<drive>/...paths to Windows-styleX:\...paths. - Centralized path resolution is provided by
src/config/paths.ts:getCursorBinaryPathchooses the effective Cursor agent binary path, preferring theGSD_CURSOR_BINenvironment variable, thencursorAgentPathfrom config, and finally falling back tocursor-agent. On WSL it can map/mnt/*paths to Windows-style paths when needed.getClipExePathresolves a Windowsclip.exelocation when running under WSL (defaulting toC:\Windows\System32\clip.exewhen present), or returnsnullwhen clipboard integration is unavailable.getWorkspaceDisplayPathexposes both the WSL path and, when possible, a corresponding Windows path for the workspace root.
- WSL bootstrap in
src/bootstrap/wsl-bootstrap.tswires these helpers together and is invoked from the CLI startup so the daemon has a single place to understand the current environment. - Diagnostics script at
scripts/bootstrap-wsl.shruns a focused WSL environment check from your shell and prints suggested values forGSD_CURSOR_BINandGSD_CLIP_EXE.
When clip.exe cannot be resolved (for example, on non-WSL Linux), clipboard integration should be treated as optional by higher-level tooling: consumers should check for null and simply skip clipboard-related features instead of failing daemon startup. The diagnostics script helps you see exactly what the project can and cannot infer about your environment.
Quick Start
# 1. Install
npm install -g gsd-unsupervised
# or clone and use locally:
git clone https://github.com/0jrm/gsd-unsupervised && cd gsd-unsupervised && npm install
# 2. Initialize in your project
cd your-project
./setup.sh
# or non-interactively:
npx gsd-unsupervised init --agent cursor --goals ./goals.md
# 3. Add a goal
echo "- [ ] Add dark mode to the dashboard" >> goals.md
# 4. Start the daemon
./run
# Attach to watch: tmux attach -t gsd-selfThat's it. The daemon will read goals.md, invoke your agent, and SMS you when done (if Twilio is configured).
Install
From npm (recommended):
npm install -g gsd-unsupervisedFrom source:
git clone https://github.com/0jrm/gsd-unsupervised
cd gsd-unsupervised
npm install
npm run buildWSL Bootstrap & Diagnostics
On WSL2, from the project root you can run a quick diagnostics pass:
bash scripts/bootstrap-wsl.shThis script:
- Detects WSL.
- Shows your workspace path in both WSL and Windows form when possible.
- Reports current and suggested values for
GSD_CURSOR_BINandGSD_CLIP_EXE. - Exits non-zero when it detects WSL but cannot infer a reliable Windows mapping for the workspace, so you can catch misconfigurations early.
For a deeper explanation of how WSL detection and path resolution work (and more examples of environment variable configuration), see docs/wsl-bootstrap.md.
Usage
Two modes
- SELF — Daemon improves this repo (
gsd-unsupervised). Workspace and goals live here; state in.gsd/state.json. - PROJECT — Daemon works on another repo. You run
npx gsd-unsupervised initin that repo; state and goals live under that repo’s.gsd/.
First-time setup (any repo)
./setup.sh
# or non-interactively:
npx gsd-unsupervised init --agent cursor --goals ./goals.mdsetup.sh asks: agent type, goals path, status port, optional Twilio. Writes .gsd/state.json and goals.md. Then start with ./run.
Recommended (dashboard + public URL)
From the project root you can use the run script (reads .gsd/state.json, loads .env, starts daemon + optional ngrok + tmux):
./runIf not yet initialized, run ./setup.sh or npx gsd-unsupervised init first.
Or run the daemon explicitly with the status server and ngrok:
export CURSOR_API_KEY=your_key_here
./bin/gsd-unsupervised --goals goals.md --status-server 4173 --ngrok --verboseExtra args are passed through (e.g. ./run --parallel).
- Status server on port
4173: openhttp://localhost:4173for the HTML dashboard. - ngrok runs
ngrok http 4173for the same process; the public URL appears in the terminal. When the daemon exits, ngrok is stopped.
Requires ngrok on your PATH and an ngrok authtoken (e.g. ngrok config add-authtoken <token>). Set CURSOR_API_KEY in your environment or in a .env file.
Other ways to run
# Preview the goal queue (no API key needed)
./bin/gsd-unsupervised --dry-run --goals goals.md
# Run without dashboard
./bin/gsd-unsupervised --goals goals.md --verbose
# Dashboard only (no ngrok, localhost only)
./bin/gsd-unsupervised --goals goals.md --status-server 4173 --verboseCLI options
| Option | Default | Description |
|--------|---------|-------------|
| --goals <path> | ./goals.md | Path to the goals queue file |
| --config <path> | ./.autopilot/config.json | Config file (optional) |
| --parallel | false | Enable parallel project execution |
| --max-concurrent <n> | 3 | Max concurrent goals when --parallel |
| --verbose | false | Debug logging and pretty output |
| --dry-run | false | Parse goals and show plan only; no agent calls |
| --agent <name> | cursor | Agent type: cursor, cn, claude-code, gemini-cli, codex. Invalid names fail fast. |
| --agent-path <path> | agent | Path to cursor-agent binary |
| --agent-timeout <ms> | 600000 | Agent invocation timeout (ms) |
| --status-server <port> | — | Enable local HTTP status server: GET / = dashboard HTML, GET /status or /api/status = JSON |
| --ngrok | false | Start ngrok http <port> when status server is enabled; tunnel and process share the same lifecycle |
Agent selection (--agent)
| Agent | Status | Notes |
|-------|--------|-------|
| cursor | Supported | Default. CURSOR_API_KEY required. |
| cn | Supported | Continue CLI. npm install -g @continuedev/cli, set CONTINUE_API_KEY. |
| claude-code | Stub | Coming soon. |
| gemini-cli | Stub | Coming soon. |
| codex | Stub | Coming soon. |
Invalid names fail fast at startup.
Goals file (goals.md)
Use sections Pending, In Progress, and Done. List goals as markdown checkboxes under the right section. The orchestrator processes items in Pending and moves them to In Progress / Done as it runs.
Example:
## Pending
- [ ] Your next goal
## In Progress
<!-- moved here while running -->
## Done
<!-- completed goals -->All roadmap phases (1–7) are implemented: Foundation, Lifecycle, Agent Integration, State Monitoring, Crash Detection & Recovery, Status Server, WSL Bootstrap. Use goals.md for new work items.
Configuration
Config can come from a JSON file (--config) and is overridden by CLI options. All fields are optional.
| Field | Default | Description |
|-------|---------|-------------|
| goalsPath | "./goals.md" | Goals file path |
| parallel | false | Parallel mode |
| maxConcurrent | 3 | Max concurrent goals (1–10) |
| verbose | false | Verbose logging |
| logLevel | "info" | debug | info | warn | error |
| workspaceRoot | process.cwd() | Project root (for .planning/, etc.) |
| agent | "cursor" | Agent type: cursor, cn, claude-code, gemini-cli, codex |
| cursorAgentPath | "cursor-agent" | cursor-agent binary path |
| continueCliPath | "cn" | cn (Continue CLI) binary path; used when agent is cn |
| agentTimeoutMs | 600000 | Agent timeout (≥ 10000) |
| sessionLogPath | "./session-log.jsonl" | Session log file |
| stateWatchDebounceMs | 500 | STATE.md watcher debounce (≥ 100) |
| requireCleanGitBeforePlan | true | Refuse execute-plan when git working tree is dirty |
| autoCheckpoint | false | When true and tree dirty, create a checkpoint commit before plan |
| statusServerPort | — | When set, start local HTTP status server on this port (dashboard + /api/status) |
| ngrok | false | When true and status server is enabled, run ngrok http <port> for the process lifetime |
Example .autopilot/config.json:
{
"goalsPath": "./goals.md",
"verbose": true,
"stateWatchDebounceMs": 500
}Project structure
├── bin/gsd-unsupervised # CLI entry (Node)
├── src/
│ ├── cli.ts # Commander setup, dry-run, daemon entry
│ ├── config.ts # Zod config schema and loader
│ ├── daemon.ts # Goal loop, StateWatcher per goal
│ ├── orchestrator.ts # GSD state machine, agent invoker, reportProgress
│ ├── lifecycle.ts # Goal phases and command sequence
│ ├── goals.ts # goals.md parser
│ ├── roadmap-parser.ts # ROADMAP.md / phase / plan discovery
│ ├── state-parser.ts # STATE.md "Current Position" parser
│ ├── state-watcher.ts # Chokidar watcher, progress events
│ ├── cursor-agent.ts # cursor-agent invoker, API key validation
│ ├── logger.ts # Pino logger init
│ └── ...
├── .planning/ # GSD project state (STATE.md, ROADMAP.md, phases/)
├── goals.md # Goal queue
└── package.jsonSee docs/ARCHITECTURE.md for module roles and data flow.
Crash detection and recovery
The daemon appends one JSON line per agent run to session-log.jsonl at the project root (config sessionLogPath, default ./session-log.jsonl). Each entry includes goalTitle, phaseNumber, planNumber, and status (running | done | crashed | timeout). On startup, if the last entry is running or crashed and the first pending goal matches, the daemon computes a resume point from STATE.md (or the log) and passes it to the orchestrator, which re-runs only that plan then continues.
Example session-log.jsonl (2 lines):
{"timestamp":"2026-03-17T12:00:00.000Z","goalTitle":"Complete Phase 5","phase":"/gsd/execute-plan","phaseNumber":2,"planNumber":1,"sessionId":null,"command":"/gsd/execute-plan .planning/phases/02-x/02-01-PLAN.md","status":"running"}
{"timestamp":"2026-03-17T12:05:00.000Z","goalTitle":"Complete Phase 5","phase":"/gsd/execute-plan","phaseNumber":2,"planNumber":1,"sessionId":"abc","command":"/gsd/execute-plan .planning/phases/02-x/02-01-PLAN.md","status":"crashed","durationMs":300000,"error":"Agent exited with code 1"}Corresponding STATE.md Current Position (when crash occurred):
## Current Position
Phase: 2 of 7 (Core Orchestration Loop)
Plan: 1 of 3 in current phase
Status: Executing plan
Last activity: 2026-03-17 — Running 02-01-PLAN.md
Progress: ██░░░░░░░░ 14%Resume uses this to re-run execute-plan for phase 2 plan 1 only, then continue.
- requireCleanGitBeforePlan (default
true): the orchestrator refuses to runexecute-planwhen the git working tree has uncommitted changes, unless autoCheckpoint istrue, in which case it creates a checkpoint commit first. - How to recover manually: (1) Inspect
session-log.jsonl(last line = last run;statuscrashedorrunning). (2) Read.planning/STATE.mdfor "Current Position" (phase/plan). (3) Either run the daemon again with the same goal so it resumes automatically, or run/gsd/execute-plan .planning/phases/<phase-dir>/<phase>-<plan>-PLAN.mdfor the failed plan.
Status server and dashboard: Use --status-server <port> to enable the local HTTP status server (e.g. ./bin/gsd-unsupervised --goals goals.md --status-server 4173). Add --ngrok to have the daemon run ngrok http <port> for the same lifecycle: the public URL appears in ngrok’s output and the tunnel is closed when the daemon exits. GET / serves the HTML dashboard; GET /status returns legacy JSON; GET /api/status returns rich JSON including stateSnapshot, session log window, git feed, and systemLoad. GET /api/config and POST /api/config expose and update .planning/config.json (used for the sequential/parallel toggle).
Hot-reload and webhook: The daemon watches goals.md and merges new pending goals into the queue when the file changes. With the status server running: POST /api/goals (JSON { "title": "...", "priority": 1 }) appends to goals and enqueues; POST /api/todos (JSON { "title": "...", "area": "api" }) creates .planning/todos/pending/; POST /webhook/twilio accepts inbound SMS (e.g. add <goal> or todo <task>) and replies with TwiML. Point your Twilio number webhook at <ngrok-url>/webhook/twilio.
Parallel goal pool: With --parallel, a worker pool of size --max-concurrent is used; a per-workspace mutex keeps one goal running at a time for a single workspace (phase-level parallel inside execute-phase still applies).
SMS (Twilio): Optional. Three message types: goal started [gsd] Started: …, goal complete, goal crashed [gsd] Crashed: …. Set TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_FROM, and TWILIO_TO in .env. setup.sh prompts for Twilio credentials when you answer y to SMS notifications. To verify delivery, run npx gsd-unsupervised test-sms.
State and heartbeat: When started via ./run or gsd-unsupervised run --state .gsd/state.json, the daemon writes to .gsd/state.json (PID, current goal, progress, lastHeartbeat). You can use lastHeartbeat in an external cron or script to send a periodic "alive" SMS (e.g. every 30 min) or alert if the heartbeat is stale (e.g. >10 min).
Development
npm run build # Compile TypeScript
npm test # Run tests (Vitest)
npm run dev # Watch buildTests include state parser, stream events, lifecycle, session-log, roadmap-parser, status-server, and resume integration. Run with npm test or npm test -- state-parser. Integration tests (crash/resume): npm run test:integration.
License
MIT
