diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cc85699..3eca1ce 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,3 +36,6 @@ jobs: - name: Lint run: npm run lint + + - name: Typecheck + run: npm run typecheck diff --git a/AGENTS.md b/AGENTS.md index e87d7cc..e60af02 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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/.mjs`.** Their wrappers at `plugins/cursor/commands/.md` must use `node "${CLAUDE_PLUGIN_ROOT}/scripts/.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//`.** Never break that layout; users point scripts at those files when reporting bugs. @@ -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. @@ -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. diff --git a/CHANGELOG.md b/CHANGELOG.md index 6688eb4..da08eaf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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`. diff --git a/plugins/cursor/package-lock.json b/plugins/cursor/package-lock.json index 3f9d472..0db9578 100644 --- a/plugins/cursor/package-lock.json +++ b/plugins/cursor/package-lock.json @@ -1,18 +1,20 @@ { "name": "cursor-plugin-cc", - "version": "0.2.2", + "version": "0.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cursor-plugin-cc", - "version": "0.2.2", + "version": "0.4.0", "license": "MIT", "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" }, "engines": { @@ -1133,6 +1135,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/node": { + "version": "22.20.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", + "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, "node_modules/@vitest/coverage-v8": { "version": "2.1.9", "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.1.9.tgz", @@ -2874,6 +2886,27 @@ "node": ">= 0.8.0" } }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", diff --git a/plugins/cursor/package.json b/plugins/cursor/package.json index 49b4d2a..700fa3d 100644 --- a/plugins/cursor/package.json +++ b/plugins/cursor/package.json @@ -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", @@ -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" } } diff --git a/plugins/cursor/scripts/lib/git.mjs b/plugins/cursor/scripts/lib/git.mjs index bb262b5..61cec92 100644 --- a/plugins/cursor/scripts/lib/git.mjs +++ b/plugins/cursor/scripts/lib/git.mjs @@ -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` diff --git a/plugins/cursor/scripts/lib/parse.mjs b/plugins/cursor/scripts/lib/parse.mjs index b76e4fa..6035415 100644 --- a/plugins/cursor/scripts/lib/parse.mjs +++ b/plugins/cursor/scripts/lib/parse.mjs @@ -126,15 +126,16 @@ export function* walkToolUses(node) { for (const item of node) yield* walkToolUses(item); return; } - const type = node.type; - const name = typeof node.name === 'string' ? node.name : undefined; + const obj = /** @type {Record} */ (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); } /** diff --git a/plugins/cursor/skills/composer-prompting/SKILL.md b/plugins/cursor/skills/composer-prompting/SKILL.md index 48b347f..33e575a 100644 --- a/plugins/cursor/skills/composer-prompting/SKILL.md +++ b/plugins/cursor/skills/composer-prompting/SKILL.md @@ -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 ` 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=`**: 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. diff --git a/plugins/cursor/skills/composer-prompting/references/composer-antipatterns.md b/plugins/cursor/skills/composer-prompting/references/composer-antipatterns.md new file mode 100644 index 0000000..ba1ab01 --- /dev/null +++ b/plugins/cursor/skills/composer-prompting/references/composer-antipatterns.md @@ -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. diff --git a/plugins/cursor/skills/composer-prompting/references/model-selection.md b/plugins/cursor/skills/composer-prompting/references/model-selection.md new file mode 100644 index 0000000..5161985 --- /dev/null +++ b/plugins/cursor/skills/composer-prompting/references/model-selection.md @@ -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 ` 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=`**: 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. diff --git a/plugins/cursor/skills/composer-prompting/references/prompt-anatomy.md b/plugins/cursor/skills/composer-prompting/references/prompt-anatomy.md new file mode 100644 index 0000000..5ea35bd --- /dev/null +++ b/plugins/cursor/skills/composer-prompting/references/prompt-anatomy.md @@ -0,0 +1,55 @@ +# Prompt Anatomy — the five sections + +Every prompt sent to Composer **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. + +## Worked example + +```markdown +# Goal + +Add a `GET /health` endpoint returning `{ status, version, uptime }`. Step 1 of the monitoring epic; keep it self-contained. + +# Repo context + +Express 5 + TypeScript. Follow conventions in `AGENTS.md` (English identifiers, Czech user-facing strings). + +# Acceptance criteria + +- `GET /health` responds 200 with JSON `{ status: "ok", version, uptime }`. +- `version` is read from `package.json`, not hard-coded. +- `uptime` is `process.uptime()` in whole seconds. +- A test covers the happy path and asserts all three fields. + +# Files to touch + +- `src/routes/health.ts` (new) +- `src/app.ts` (mount the route) +- `tests/health.test.ts` (new) + +# How to verify + +- `npm test` +- `npm run typecheck` + +# Guardrails + +- Do not touch files outside the list above. +- Do not rename public APIs. +- Do not modify lockfiles. +- If a pre-existing test already fails, report it — do not fix it as a side task. +``` + +Why this shape works: every section is either an instruction Composer can follow mechanically (files, commands) or a check it can self-verify against (criteria). There is nothing to interpret — which is exactly what a no-conversation-context executor needs. diff --git a/plugins/cursor/tsconfig.check.json b/plugins/cursor/tsconfig.check.json new file mode 100644 index 0000000..9408249 --- /dev/null +++ b/plugins/cursor/tsconfig.check.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "allowJs": true, + "checkJs": true, + "noEmit": true, + "strict": false, + "noImplicitAny": false, + "useUnknownInCatchVariables": false, + "skipLibCheck": true, + "types": ["node"] + }, + "include": ["scripts/lib/*.mjs"] +}