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
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,6 @@ jobs:

- name: Lint
run: npm run lint

- name: Typecheck
run: npm run typecheck
8 changes: 4 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ A Claude Code plugin that delegates coding tasks from Claude to the Cursor CLI (
## Hard rules

1. **Zero runtime dependencies.** The plugin ships as plain ESM `.mjs` and must execute directly after `/plugin install` with zero `npm install` in the user's plugin cache. If you are about to add `execa`, `zod`, `nanoid`, a HTTP client, a command parser, or any other third-party runtime package — stop. Write a small inline helper instead. See `plugins/cursor/scripts/lib/run.mjs` as the reference pattern (it replaced `execa` in ~80 lines).
2. **No build step.** No TypeScript, no bundler, no `dist/`. `scripts/*.mjs` IS the ship artefact. If you find yourself wanting one, something has gone wrong with the approach.
2. **No build step.** No TypeScript source, no bundler, no `dist/`. `scripts/*.mjs` IS the ship artefact. If you find yourself wanting one, something has gone wrong with the approach. (`npm run typecheck` runs `tsc --checkJs --noEmit` over the JSDoc annotations in `scripts/lib/` — that is a dev-time check like eslint, not a build step; `typescript` stays a devDependency.)
3. **Slash command scripts live under `plugins/cursor/scripts/<cmd>.mjs`.** Their wrappers at `plugins/cursor/commands/<cmd>.md` must use `node "${CLAUDE_PLUGIN_ROOT}/scripts/<cmd>.mjs" -- "$ARGUMENTS"` with quoted `$ARGUMENTS` — unquoted breaks under zsh on any prompt containing `?`, `*`, or `@`. Exception: `review.md` and `adversarial-review.md` are model-orchestrated (they estimate the diff and ask wait-vs-background before running), so they give Claude the `node …` command in a fenced block rather than an auto-executing `!` line, and `adversarial-review` reuses `review.mjs --adversarial` instead of shipping its own script.
4. **`Bash(node:*)` is the only permission pattern used in `allowed-tools`.** Do not invent path-based patterns — Claude Code does not expand `${CLAUDE_PLUGIN_ROOT}` inside `allowed-tools`. Exception: the two estimate-first review commands additionally list `Bash(git:*)`, `AskUserQuestion`, and `Read, Glob, Grep` for the size-estimate/ask step — those are tool-name patterns, not path-based ones, so they are fine.
5. **Jobs are persisted under `~/.cursor-plugin-cc/jobs/<repo-hash>/`.** Never break that layout; users point scripts at those files when reporting bugs.
Expand All @@ -31,8 +31,8 @@ Plus a **Constraints** block that forbids: touching files outside the list, rena
## How to make a change

1. Branch named `feat/…`, `fix/…`, `refactor/…`, or `docs/…`.
2. Work inside `plugins/cursor/`. `cd plugins/cursor && npm install` installs dev deps (vitest, eslint, prettier — the only ones).
3. Run tests: `npm test`. Run lint: `npm run lint`. Both must be green before committing.
2. Work inside `plugins/cursor/`. `cd plugins/cursor && npm install` installs dev deps (vitest, eslint, prettier, typescript — dev-time only).
3. Run tests: `npm test`. Run lint: `npm run lint`. Run types: `npm run typecheck`. All three must be green before committing.
4. Commit messages in conventional style (`type(scope): subject`).
5. Open a PR against `main` with a summary + test plan. CI must pass across Node 18.18 / 20 / 22 × Ubuntu / macOS.
6. Squash-merge only.
Expand All @@ -50,7 +50,7 @@ Plus a **Constraints** block that forbids: touching files outside the list, rena
- `plugins/cursor/scripts/lib/*.mjs` — shared helpers (run, id, args, paths, jobs, kill, parse, cursor, git, invoked, plan, hints, md).
- `plugins/cursor/commands/*.md` — slash command wrappers.
- `plugins/cursor/agents/cursor-runner.md` — the handoff subagent prompt.
- `plugins/cursor/skills/composer-prompting/SKILL.md` — Cursor prompt-shaping guidance the `cursor-runner` subagent references via its `skills:` frontmatter.
- `plugins/cursor/skills/composer-prompting/` — Cursor prompt-shaping guidance the `cursor-runner` subagent references via its `skills:` frontmatter. `SKILL.md` is the always-loaded spine; the detailed material lives in `references/*.md` (prompt anatomy, model selection, anti-patterns), loaded on demand.
- `plugins/cursor/tests/*.test.mjs` — vitest specs + fixtures.
- `.claude-plugin/marketplace.json` — what Claude Code's `/plugin install` reads.

Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

### Added

- **`npm run typecheck`** — `tsc --checkJs --noEmit` over the JSDoc annotations in `scripts/lib/` (`tsconfig.check.json`), wired into CI. Dev-time only: `typescript` is a devDependency, nothing is compiled, `.mjs` stays the ship artefact. The first run surfaced (and this change fixes) two real annotation gaps: `collectReviewContext`'s `mode` was inferred as plain `string` against the declared `'working-tree'|'branch'` union, and `walkToolUses` accessed properties on a value narrowed only to `object`.

### Changed

- **`composer-prompting` skill split into SKILL.md + `references/`** (progressive disclosure, mirroring codex's `gpt-5-4-prompting` layout). `SKILL.md` keeps the always-relevant spine (when to use, repo grounding, assembly checklist); the detail moved to `references/prompt-anatomy.md` (five sections + guardrails, now with a full worked example), `references/model-selection.md` (escalation ladder, chunking, resume-vs-fresh), and the new `references/composer-antipatterns.md` (six prompt shapes that reliably produce bad Composer runs, adapted from codex's anti-patterns).

### Fixed

- **`/cursor:cancel` no longer orphans the running `cursor-agent`.** Background jobs run as a detached worker (its own process group) that spawns `cursor-agent` as a child. Cancelling signalled only the worker pid, so the worker died but `cursor-agent` kept running — still editing files and consuming Cursor credits — while `/cursor:status` reported the job as `cancelled`. `cancelJob` now signals the worker's whole process group (SIGTERM, then SIGKILL after the grace period) via the new `lib/kill.mjs#killTree`, falling back to a single-pid kill for foreground jobs whose recorded pid is not a group leader. On Windows the tree is terminated with `taskkill /T /F`.
Expand Down
37 changes: 35 additions & 2 deletions plugins/cursor/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions plugins/cursor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
"lint": "prettier --check . && eslint .",
"typecheck": "tsc -p tsconfig.check.json",
"format": "prettier --write .",
"delegate": "node scripts/delegate.mjs",
"status": "node scripts/status.mjs",
Expand All @@ -52,9 +53,11 @@
},
"devDependencies": {
"@eslint/js": "^9.16.0",
"@types/node": "^22.20.1",
"@vitest/coverage-v8": "^2.1.8",
"eslint": "^9.16.0",
"prettier": "^3.4.2",
"typescript": "^5.9.3",
"vitest": "^2.1.8"
}
}
1 change: 1 addition & 0 deletions plugins/cursor/scripts/lib/git.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ export async function collectReviewContext(cwd, opts = {}) {
const maxDiffBytes = opts.maxDiffBytes ?? MAX_DIFF_BYTES;
const branch = await currentBranch(cwd);

/** @type {'working-tree'|'branch'} */
let mode;
let baseRef = base;
// Reused below for working-tree mode so we don't run `git diff`/`ls-files`
Expand Down
9 changes: 5 additions & 4 deletions plugins/cursor/scripts/lib/parse.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -126,15 +126,16 @@ export function* walkToolUses(node) {
for (const item of node) yield* walkToolUses(item);

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[SUGGESTION] Introducing the obj alias only inside walkToolUses (while pickText above still accesses item.type/item.text on values narrowed similarly) is a slightly inconsistent style — future readers may wonder why one function needs the cast and the other doesn't.

Suggestion: Optional: add a short comment noting that the cast is required here because checkJs won't let TS index a bare object-narrowed value, whereas pickText's item is already inferred as any/unknown-safe from its call context.

return;
}
const type = node.type;
const name = typeof node.name === 'string' ? node.name : undefined;
const obj = /** @type {Record<string, unknown>} */ (node);
const type = obj.type;
const name = typeof obj.name === 'string' ? obj.name : undefined;
if ((type === 'tool_use' || type === 'tool_call') && name) {
yield {
name,
input: node.input ?? node.arguments ?? node.params ?? node.tool_input,
input: obj.input ?? obj.arguments ?? obj.params ?? obj.tool_input,
};
}
for (const v of Object.values(node)) yield* walkToolUses(v);
for (const v of Object.values(obj)) yield* walkToolUses(v);
}

/**
Expand Down
52 changes: 6 additions & 46 deletions plugins/cursor/skills/composer-prompting/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,57 +20,17 @@ Before writing the prompt, use `Read` (only) to check the target repo for:

**Language and style follow the target repo, not this plugin.** If the repo's commits, comments, or UI strings are in Czech / German / any other language, Composer must match — do not force English. If the repo is mixed (code in English, user copy in Czech), say so explicitly. When in doubt, tell Cursor: "match the existing style of surrounding files."

## Prompt anatomy — the five sections
## References — read the one you need

Every prompt you send **must** have these sections, in this order:

1. **Goal** — one or two sentences. What is the outcome? What is this a step of, if anything?
2. **Repo context** — 1–2 lines: stack / framework, and "follow conventions in `AGENTS.md` / `.cursor/rules` / whichever you actually found."
3. **Acceptance criteria** — 1–5 bullet points, concrete and verifiable.
4. **Files to touch** — an explicit list. Unless the task inherently cannot predict this, Composer must not wander outside it.
5. **How to verify** — the exact commands that prove the task is done (e.g. `npm test`, `task typecheck && task test`, `pnpm lint`). Not optional — without it Composer will declare "done" on unverified work.

Then a **Guardrails** block, short and blunt:

- Do not delete files outside the list.
- Do not rename public APIs unless asked.
- Do not touch lockfiles (`package-lock.json`, `pnpm-lock.yaml`, `yarn.lock`) unless the task is explicitly about dependencies.
- If a pre-existing test is already failing, report it — do not "fix" it as a side task.

## Chunk oversized plans before delegating

`cursor-agent --force` will YOLO through anything you hand it. That is the point — and also the risk. **Refuse to delegate a single monolithic blob of work.** Heuristics:

- More than **~5 discrete steps** → split into one `/cursor:delegate` call per step (or per coherent slice).
- More than **~10 files** or crossing **more than 2 architectural layers** → ask the main Claude to narrow the slice first.
- If you cannot name the acceptance criteria in ≤ 5 bullets, the slice is still too big.

Small slices give Composer a tight scope, make the diff reviewable, and make failures cheap to retry.

## Pick a model

Default is `composer-2.5-fast` — Cursor's own current default and the fastest Composer variant. Escalate only when the task warrants it:

- `composer-2.5` (non-fast) — quality matters slightly more than latency, but the task is still well-scoped.
- `sonnet` (`claude-4.6-sonnet-medium`) — more than ~5 files touched, or moderate architecture changes.
- `opus` (`claude-opus-4-7-high`) — cross-cutting refactor, subtle correctness, or a prior `composer` run failed.
- `gpt` / `codex` (`gpt-5.3-codex`) — only when the user explicitly asks for it.

Unknown aliases are forwarded as-is, so `--model <whatever>` always works. Do not escalate without a reason — `composer-2.5-fast` is the default for speed and cost.

## Resume or fresh

- **`--resume`** (default when not specified): continue the latest Cursor chat for this repo. Use it when **iterating on the same task** — "also cover the 429 path", "rename the helper you just added". Cheap, preserves Composer's mental model.
- **`--resume=<chat-id>`**: same, but target a specific prior chat — when `/cursor:status` or the user pointed you at one explicitly.
- **`--fresh`**: start a brand-new Cursor session. Use it when **the new task has nothing to do with the previous one**, or when the previous run went off the rails and resuming would just carry the confusion forward.

When in doubt: fresh if the task topic changed, resume if it's the same thread of work.
- **[references/prompt-anatomy.md](references/prompt-anatomy.md)** — the five mandatory prompt sections + guardrails block, with a full worked example. Read before composing any delegate prompt.
- **[references/model-selection.md](references/model-selection.md)** — the model escalation ladder, chunking heuristics for oversized plans, and resume-vs-fresh routing.
- **[references/composer-antipatterns.md](references/composer-antipatterns.md)** — prompt shapes that reliably produce bad Composer runs, each with the fix. Skim when a previous run went sideways.

## Assembly checklist

1. Ground the prompt in the target repo's conventions and verify commands.
2. Write the five sections plus the guardrails block, in order.
3. Chunk anything bigger than one reviewable slice.
2. Write the five sections plus the guardrails block, in order (see prompt-anatomy).
3. Chunk anything bigger than one reviewable slice (see model-selection).
4. Pick the smallest model that fits; default to `composer-2.5-fast`.
5. Decide resume vs fresh.
6. Remove redundant instructions before sending.
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Composer Prompt Anti-Patterns

Prompt shapes that reliably produce bad Composer runs, each with the fix. Adapted for Composer from `openai/codex-plugin-cc`'s prompt anti-patterns; the failure modes are the same, the fixes use the five-section anatomy.

## Vague goal

Bad:

```text
Improve the error handling in the API layer.
```

"Improve" has no end state — Composer will pick one for you, everywhere it can reach. Better: name the concrete outcome and the boundary.

```text
# Goal
Wrap the three fetch calls in src/api/client.ts in try/catch and surface failures as ApiError with the upstream status code. Nothing outside client.ts.
```

## Missing verify commands

Bad: a prompt that ends at acceptance criteria. Composer declares "done" on unverified work — the run reports success and the tests were never executed. Always end with **How to verify** listing exact commands (`npm test`, `task typecheck`). If you don't know the commands, that's a grounding failure — go read `package.json` first.

## Kitchen-sink run

Bad:

```text
Fix the failing login test, and while you're there update the README and clean up the unused imports.
```

Three unrelated jobs → one muddy diff you can't review or revert independently. One `/cursor:delegate` per coherent slice; queue the rest.

## Prose instead of a file list

Bad:

```text
You'll probably need to touch the router and maybe the middleware.
```

"Probably/maybe" reads as permission to wander. Either name the files (**Files to touch** section) or state explicitly that discovery is part of the task and bound it: "locate the middleware that sets the session cookie; modify only that file."

## Re-explaining instead of re-scoping

When a run goes wrong, the instinct is to resume with a longer explanation of what you meant. If the prompt was structurally bad (vague goal, no verify, no file list), a longer version of it is still bad. Rewrite the prompt with the five sections and start `--fresh` — carrying a confused session forward compounds the confusion.

## Asking Composer to make design decisions

Bad:

```text
Add caching to the product endpoint — whatever approach you think is best.
```

Design decisions belong to the Claude thread (or the user), not the executor. Decide the approach first ("in-memory LRU, max 500 entries, 60 s TTL"), then delegate the implementation. If you can't specify the approach, the task isn't ready to delegate.
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Model Selection, Chunking, and Resume Routing

## Chunk oversized plans before delegating

`cursor-agent --force` will YOLO through anything you hand it. That is the point — and also the risk. **Refuse to delegate a single monolithic blob of work.** Heuristics:

- More than **~5 discrete steps** → split into one `/cursor:delegate` call per step (or per coherent slice).
- More than **~10 files** or crossing **more than 2 architectural layers** → ask the main Claude to narrow the slice first.
- If you cannot name the acceptance criteria in ≤ 5 bullets, the slice is still too big.

Small slices give Composer a tight scope, make the diff reviewable, and make failures cheap to retry.

## Pick a model

Default is `composer-2.5-fast` — Cursor's own current default and the fastest Composer variant. Escalate only when the task warrants it:

- `composer-2.5` (non-fast) — quality matters slightly more than latency, but the task is still well-scoped.
- `sonnet` (`claude-4.6-sonnet-medium`) — more than ~5 files touched, or moderate architecture changes.
- `opus` (`claude-opus-4-7-high`) — cross-cutting refactor, subtle correctness, or a prior `composer` run failed.
- `gpt` / `codex` (`gpt-5.3-codex`) — only when the user explicitly asks for it.

Unknown aliases are forwarded as-is, so `--model <whatever>` always works. Do not escalate without a reason — `composer-2.5-fast` is the default for speed and cost.

## Resume or fresh

- **`--resume`** (default when not specified): continue the latest Cursor chat for this repo. Use it when **iterating on the same task** — "also cover the 429 path", "rename the helper you just added". Cheap, preserves Composer's mental model.
- **`--resume=<chat-id>`**: same, but target a specific prior chat — when `/cursor:status` or the user pointed you at one explicitly.
- **`--fresh`**: start a brand-new Cursor session. Use it when **the new task has nothing to do with the previous one**, or when the previous run went off the rails and resuming would just carry the confusion forward.

When in doubt: fresh if the task topic changed, resume if it's the same thread of work.
Loading
Loading