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
30 changes: 30 additions & 0 deletions orchestrate/.cursor-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "orchestrate",
"displayName": "Orchestrate",
"version": "1.0.0",
"description": "Fan a large task out across parallel Cursor cloud agents via the Cursor SDK: planners publish tasks, workers hand off back up, and a script reconciles the tree from disk and git.",
"author": {
"name": "Cursor",
"email": "plugins@cursor.com"
},
"homepage": "https://github.com/cursor/plugins/tree/main/orchestrate",
"repository": "https://github.com/cursor/plugins",
"license": "MIT",
"keywords": [
"orchestrate",
"cursor",
"cursor-sdk",
"agents",
"cloud-agents",
"parallel",
"automation"
],
"category": "developer-tools",
"tags": [
"agents",
"automation",
"developer-tools",
"sdk"
],
"skills": "./skills/"
}
10 changes: 10 additions & 0 deletions orchestrate/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
node_modules/
.DS_Store

# Runtime artifacts written by the orchestrate CLI in the user's workspace
# and by `bun test` when fixtures don't override cwd.
bc-*/
state.json
plan.json
handoffs/
comment-retry-queue.json
21 changes: 21 additions & 0 deletions orchestrate/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 Cursor

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
70 changes: 70 additions & 0 deletions orchestrate/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Orchestrate

Fan a large task out across parallel Cursor cloud agents via the Cursor SDK. Planners publish tasks, workers hand off back up, and a script reconciles the tree from disk and git, so the spawn / wait / handoff loop keeps converging without long-running agent state.

The skill itself lives in [`skills/orchestrate/SKILL.md`](./skills/orchestrate/SKILL.md). Read that for the full operating manual; this README only covers what to set up before you invoke it.

## Prerequisites

- `bun` on PATH.
- A Cursor API key in `CURSOR_API_KEY`.
- Optional Slack app and bot token if you want a Slack thread mirroring the run.

## Cursor API key

1. Open [https://cursor.com/dashboard/integrations](https://cursor.com/dashboard/integrations).
2. Create a personal user API key. The value starts with `cursor_`.
3. Export it: `export CURSOR_API_KEY="cursor_..."`.

Team service-account keys (Team Settings → Service accounts) also work for both local and cloud runs. See the [`cursor-sdk` plugin](https://github.com/cursor/plugins/tree/main/cursor-sdk) for the full auth model.

## Slack app (optional)

Slack visibility is opt-in. When the token is unset, the script logs once and runs without Slack; correctness does not change. To enable it:

1. Create a Slack app at [https://api.slack.com/apps](https://api.slack.com/apps) → **From scratch**. Pick a name and a workspace.
2. Under **OAuth & Permissions** → **Bot Token Scopes**, add:

| Scope | Why |
| --- | --- |
| `chat:write` | Post and edit messages. |
| `chat:write.customize` | Set the bot username and icon on each post. |
| `chat:write.public` | Post in public channels without inviting the bot first. |
| `files:write` | Upload handoff artifacts to the run thread. |
| `files:read` | Paired with `files:write` for the upload v2 flow. |
| `reactions:read` | Watch the Andon `:rotating_light:` reaction on the kickoff message. |
| `channels:history` | Read thread replies. Use `groups:history` instead if your run channel is private. |

Optional but recommended:

| Scope | Why |
| --- | --- |
| `users:read.email` | Resolve the dispatcher's first name from `git config user.email`. Without it, pass `--dispatcher-name` explicitly. |

3. **Install to Workspace** and copy the **Bot User OAuth Token** (`xoxb-...`).
4. Export it: `export SLACK_BOT_TOKEN="xoxb-..."`.
5. Invite the bot to the channel where you want runs to thread (`/invite @your-bot`). Public channels with `chat:write.public` skip this; private channels require the invite.
6. Grab the channel ID. In Slack: right-click the channel → **View channel details** → bottom of the dialog. Pass it via `--slack-channel <id>` on `kickoff` (or set `SLACK_CHANNEL_ID`). The first kickoff persists the id on the plan; subplanners and later `run` invocations inherit it.

## Install

```bash
cd skills/orchestrate/scripts
bun install
```

The scripts live outside the host repo's package manager workspace on purpose.

## Invoke

```bash
bun skills/orchestrate/scripts/cli.ts kickoff "<goal>" \
[--repo <url>] [--ref main] [--model claude-opus-4-7] \
[--slack-channel <id>] [--dispatcher-name "<first name>"]
```

The CLI prints `{ agentId, runId, status, url }`; from there the cloud root planner self-drives. See the skill for `run`, `spawn`, `respawn`, `kill`, `tail`, `comment`, and `andon` subcommands.

## License

MIT. See [`LICENSE`](./LICENSE).
47 changes: 47 additions & 0 deletions orchestrate/skills/orchestrate/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
---
name: orchestrate
description: Use only when the user explicitly types `/orchestrate <goal>` to decompose a large task, spawn a tree of parallel cloud-agent workers/subplanners/verifiers via the Cursor SDK, and collect structured handoffs; do not invoke autonomously.
disable-model-invocation: true
---

# Orchestrate

An explicit `/orchestrate <goal>` fans out a large task across parallel Cursor cloud agents. Workers don't talk to each other; they talk up through structured handoffs. The spawn, wait, and handoff loop lives in `scripts/cli.ts`. The planner writes `plan.json`, the script executes it, and the planner reads handoffs to decide what comes next. Long-running agent loops drift; a script with a JSON state file keeps its footing.

**Required reading: the `cursor-sdk` skill ([cursor/plugins/cursor-sdk](https://github.com/cursor/plugins/tree/main/cursor-sdk)).** Spawning, auth, and the error taxonomy live there. Don't reimplement what that skill already documents.

## Setup

- `CURSOR_API_KEY` must be a personal/user key. Create it from [Cursor Dashboard > Integrations](https://cursor.com/dashboard/integrations), then read `cursor-sdk` Auth before using it.
- `SLACK_BOT_TOKEN` is optional. When set, pass `--slack-channel <id>` to `kickoff` or the first `run --root`, or set `SLACK_CHANNEL_ID`. The script stores the channel in `plan.slackChannel`, posts the kickoff thread there, mirrors task status, and reads Andon reactions. When the token is unset, the script logs once and runs without Slack visibility; correctness does not change.

## Core principles

These rules make the tree self-converging without global coordination.

1. **Planners own scopes and publish tasks. They do no coding.** Writing `plan.json`, reading handoffs, and deciding what's next are planner work. Editing files, running `git merge`, and fixing conflicts inline are not. If a planner feels the urge to code, it publishes a task for a worker instead.
2. **Planners don't know who picks up their tasks.** The script routes each task to a cloud agent. The planner's mental model stays at the task level.
3. **Workers are isolated.** One task, one clone of the repo, no channel to any other agent. One handoff when done.
4. **Subplanners are recursive planners.** A planner publishes a "subplan this slice" task; the subplanner fully owns that slice and hands back an aggregated handoff.
5. **Continuous motion via handoffs.** A planner that thought it was done can receive a late handoff and replan. No "finished" state until the planner decides to stop publishing.
6. **Propagation, not synchronization.** No cross-talk between siblings. No shared state between levels. Each level sees only its children's handoffs.

## Node types

| Node | Runs the loop? | Scope | Output |
| -------------- | -------------- | -------------------------------- | --------------------------------------- |
| Planner | yes | Entire user goal | User-facing message + optional PR |
| Subplanner (↻) | yes | One slice of parent's scope | Handoff to parent |
| Worker | no | One concrete task | Handoff to spawning planner |
| Verifier | no | One target's acceptance criteria | Verdict handoff to spawning planner |
| Git | n/a | Shared medium | Branches (code) + handoffs/ (meaning) |

## Role

Two roles, one skill. Read your role's reference file and skip the other.

**Dispatcher.** You're in a local IDE session and the user typed `/orchestrate <goal>`. Your job is to kick off a cloud root planner and return its URL. See `references/dispatcher.md`. One-shot; you are not the planner.

**Planner (root or sub).** You were spawned with a structured prompt that opens with "You are the root planner for:" or "You are a subplanner for:". Or the user chose to run the planning loop locally. You own a scope, publish tasks, read handoffs, decide what's next. See `references/planner.md`.

`disable-model-invocation: true` means this skill loads only on explicit invocation.
7 changes: 7 additions & 0 deletions orchestrate/skills/orchestrate/prompts/andon-block.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@

### Andon
Halts new spawns across the whole tree. Raise only with concrete evidence that continued spawning produces garbage: upstream output is wrong and downstream tasks will fail against it, verifier cascade shows acceptance was wrong, auth or infra is unrecoverable. A task hitting its own snag is a `Status: blocked` handoff, not Andon.

bun <path-to-orchestrate>/scripts/cli.ts andon raise --reason "<why>"{{agentIdFlag}} --workspace <workspace>

`--reason` is required and posts to the run thread so the tree can see why orchestration paused. The reaction is the cheap gate children poll. `--agent-id` adds a footer link back to the agent that raised it.
10 changes: 10 additions & 0 deletions orchestrate/skills/orchestrate/prompts/empty-error-handoff.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
Cloud run errored before producing a final message.

agent: https://cursor.com/agents/{{agentId}}
agentId: {{agentId}}
runId: {{runId}}
resultStatus: {{resultStatus}}
result:
```json
{{resultDataJson}}
```
24 changes: 24 additions & 0 deletions orchestrate/skills/orchestrate/prompts/failure-handoff.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<!-- orchestrate failure handoff
task: {{taskName}}
branch: {{branch}}
agentId: {{agentId}}
runId: {{runId}}
failureMode: {{failureMode}}
terminatedAt: {{terminatedAt}}
-->

# {{taskName}} failure handoff

Status: error (cloud agent terminated without writing a handoff)
Failure mode: {{failureMode}}
Cloud agent: {{agentId}}
Started: {{startedAt}}
Terminated: {{terminatedAt}}
Duration: {{duration}}
Last activity: {{lastActivityLine}}
Last tool call: {{lastToolCall}}
Branch: {{branch}}
SDK error: {{sdkError}}

## Suggested next steps
{{suggestions}}
21 changes: 21 additions & 0 deletions orchestrate/skills/orchestrate/prompts/finished-no-handoff.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<!-- orchestrate finished-no-handoff
task: {{taskName}}
branch: {{branch}}
agentId: {{agentId}}
runId: {{runId}}
resultStatus: {{resultStatus}}
terminatedAt: {{terminatedAt}}
-->

# {{taskName}} finished without handoff

Status: {{resultStatus}} (cloud agent ended cleanly but never wrote a `## Status` handoff)
Cloud agent: {{agentId}}
Run: {{runId}}
Branch: {{branch}}
Terminated: {{terminatedAt}}

## Suggested next steps
- Inspect the raw handoff at `handoffs/{{taskName}}.md` to see what the worker actually emitted.
- Retry as-is if this looks like a prompt-misfire (worker produced prose but not the structured template).
- Abandon: skip task, replan around it if the goal genuinely has no acceptable output.{{rawSnippetBlock}}
15 changes: 15 additions & 0 deletions orchestrate/skills/orchestrate/prompts/loop-hygiene.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
Loop hygiene:
- Run `bun cli.ts run{{rootFlag}} <workspace>` in the foreground. The Shell default backgrounds the loop and breaks the heartbeat when your turn ends.
- Exit code 100 is a planned checkpoint restart, not an error. Rerun the same command immediately; it resumes from committed `state.json`.
- Exit code 1 on a non-empty error set is your turn. The loop exited because a task crashed; the script already wrote a synthetic `handoffs/<task>-failure.md` for each dead worker and any `handoffs/<task>-finished-no-handoff.md` for workers that ended without a structured handoff. In-flight workers keep running; the next `run` reattaches via `recoverRunning`.
- After `run` returns, call `tree`. If any task is still `pending` or `running`, loop again.
- Don't end your turn while this workspace has non-terminal tasks.

Reacting to failure handoffs:
For each task with `status: "error"` and a matching `handoffs/<task>-failure.md`, read the `Failure mode` line and decide:
- `cap-hit` or `oom`: retry with smaller scope (split into narrower tasks, tighter `pathsAllowed`, leaner `scopedGoal`).
- `network-drop`: retry as-is; treat as transient.
- `tool-error`: retry with a different `model`.
- `unknown`: read the `Last activity` and `SDK error` lines; if no signal, treat as transient and retry as-is; abandon if it fails again.
For `<task>-finished-no-handoff.md`, read the raw snippet at `handoffs/<task>.md` and decide whether the worker's intent was recoverable; retry or abandon.
Each retry costs another cloud-agent run; budget your decisions. After 2 retries on the same task, prefer abandon (drop the task from `plan.json`, replan around it) over a 3rd attempt unless you have specific evidence the next retry will succeed. Update `plan.json`, then re-run `bun cli.ts run{{rootFlag}} <workspace>` to continue.
11 changes: 11 additions & 0 deletions orchestrate/skills/orchestrate/prompts/root.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
You are the root planner for: {{goal}}

Read this skill's `SKILL.md` and follow it.

Your cloud agent id is `{{agentId}}`. Set `plan.selfAgentId` in plan.json to that string so spawns record `parentAgentId` and `kill-tree --agent-id` can target this planner.

Write `plan.summary` as a one-line orientation for the human in the Slack thread (e.g. `"smoke test of the new orchestrate substrate"`). Kickoff posts the summary; without it, kickoff falls back to a truncated `goal`.{{dispatcherInstruction}}{{slackChannelInstruction}}

Discover here before you publish tasks. Bootstrap workers hold reference material for descendants, not one-off discovery.

{{loopHygiene}}
11 changes: 11 additions & 0 deletions orchestrate/skills/orchestrate/prompts/slack-block.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@

Slack visibility:
- Write like a human typing in Slack. Short, terse, intent-first. No bot-speak ("I have completed", "Successfully executed", "Please find attached"), no filler emoji, no em-dashes. Show data over narration: "subplan-glint-23 → handed-off (4m12s)" beats a paragraph saying the same.
- The script mirrors task status in `{{channel}}` / `{{threadTs}}`. Don't edit those messages.
- Don't post to the channel root or open another kickoff. Stay in the run thread.
- Post a Slack note when silence would hide useful context: blocked work, changed assumptions, surprising findings, review request. Otherwise stay quiet.
- Default to autonomous. Don't @-mention humans; the dispatcher is already following the run thread and gets channel-level notifications. Posting in-thread is enough.
- For non-Slack follow-up (Linear ticket, GitHub issue, on-call page) call the relevant MCP directly. Orchestrate's structured plumbing is Slack-only; runtime MCPs are not.
- `bun <path-to-orchestrate>/scripts/cli.ts comment "<note>" --thread-ts {{threadTs}} --sender {{taskName}}{{agentIdFlag}} --workspace <workspace>`. `--agent-id` adds a footer link back to your cursor.com page.
- File attachments (repro/fix videos): `bun cli.ts comment --thread-ts {{threadTs}} --file <path> --comment "<initial>" --sender {{taskName}}{{agentIdFlag}} --workspace <workspace>`. Lands in the run thread alongside the status mirror.
- Add `--criticality required` for messages that must land. Default is best-effort.
59 changes: 59 additions & 0 deletions orchestrate/skills/orchestrate/prompts/subplanner.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
You are a subplanner for: {{scopedGoal}}

Read this skill's `SKILL.md` and follow it.

**You fully own this slice.** Your parent gave you a goal, path boundaries, and acceptance, not a sub-plan. Decide your own decomposition. If `scopedGoal` below includes hints about how to split the work, treat them as weak hints at most; you are authoritative on your subtree's structure.

**Recursion.** You are a planner: use workers for leaf-sized slices; add `subplanner` tasks when the slice still needs internal structure or merge/verify passes. No depth limit. Use judgment on count: one worker can carry a multi-file, multi-step slice. Default to fewer, broader workers; see `references/planner.md` "Planning rules" for the spawn-scope tradeoff.

**Child tasks.** Write child tasks as `plan.tasks[]` entries in your own plan.json. For code-editing children, propagate per the code discipline: no narrative comments. Comment only non-obvious *why*.

**Workspace convention.** Put your orchestrate workspace at `.orchestrate/{{name}}/` and set `plan.rootSlug = "{{name}}"`. The parent records your actual cloud-agent branch after handoff; use the branch already checked out and don't create or rename one to match a planned name. Set `plan.repoUrl` to `{{repoUrl}}` so descendants stay in the parent's repo. When `{{andonStateRef}}` and `{{andonStatePath}}` are non-empty, copy them so Andon state stays shared. Omit any field whose placeholder renders empty.

**Lineage.** If `{{selfAgentId}}` is set, put `plan.selfAgentId = "{{selfAgentId}}"` in plan.json (SDK does not surface it elsewhere). Details: `references/spawning.md`.

{{loopHygiene}}

Overall goal (parent's framing, context only):

{{goal}}

Your scoped sub-goal:

{{scopedGoal}}

Paths you may MODIFY (read any file in the repo):
{{allow}}

Paths you must NOT modify (owned by siblings):
{{forbid}}

Acceptance criteria for your subtree:
{{accept}}{{verifyPlan}}{{upstream}}
Model selection: pick `tasks[].model` per task by capability. Available models:

{{modelCatalog}}

Your **final message** is your handoff to your parent. Use exactly this structure:

## Status
success | partial | blocked

## Branch
`<actual branch name>`

## What my subtree did
- <aggregated summary of your children's work>

## Verification
<one of: live-ui-verified | unit-test-verified | type-check-only | verifier-blocked | verifier-failed | not-verified>

Aggregate the strongest claim your subtree's evidence actually supports for the deliverable on `## Branch`. Definitions live in `prompts/verifier.md`. Pass `verifier-blocked` through unchanged rather than rounding up to a thinner verified value. Use `not-verified` only when no verifier ran and your workers didn't self-report a stronger claim.

## Notes, concerns, deviations, findings, thoughts, feedback
- <anything bubbled up from your children that the parent should know, plus your own thoughts about how this sub-goal was scoped>

## Suggested follow-ups
- <tasks the parent should consider publishing>

Do not open a PR. Your parent decides what to do with your branch.
Loading