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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## 0.5.1 — from-plan pass-through for external spec formats

### Fixed

- **`/cursor:from-plan` no longer discards plans it cannot parse** (#16). A plan/spec whose headings don't match the Claude plan-mode shape (e.g. files produced by other spec/plan tooling) was reduced to a four-placeholder skeleton — the entire plan body was silently dropped and Cursor received an empty task. Such documents are now embedded **verbatim** in the task file (with the guardrail block still appended), matching the pass-through behaviour the module docs always promised. `SECTION_HINTS` additionally learned common spec-driven headings (`overview`, `problem statement`, `requirements`, `design`, `spec`, `tasks`, `steps`, `testing`, `validation`, `success criteria`), so those map onto the proper task sections instead of falling back. README now documents that `from-plan` accepts any path, not just `~/.claude/plans/`.

## 0.5.0 — session lifecycle hooks, stop review gate, process-group cancel

Second port wave from upstream [`openai/codex-plugin-cc`](https://github.com/openai/codex-plugin-cc) (session tracking, the stop-time review gate, prompt templates, structured review output), adapted to the Cursor CLI and the zero-deps runtime — plus a real cancellation bug found while comparing the two codebases. (#17, #18, #20, #21)
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,15 @@ Examples:
/cursor:from-plan --list # show the 15 most recent plans
```

**Not limited to Claude's plan mode.** The `[plan-name]` argument also accepts an absolute or repo-relative **path to any Markdown plan/spec file** — one written by another spec/plan plugin (Superpowers, GSD, …) or by hand:

```
/cursor:from-plan ./specs/checkout-flow/plan.md
/cursor:from-plan --delegate docs/rfc-042.md
```

Sections named like `Overview`, `Requirements`, `Design`, `Tasks`, or `Testing` are mapped onto the task shape; a document whose structure isn't recognised at all is embedded **verbatim** (with the guardrail block appended) instead of being reduced to placeholders. And if you don't need the task-file conversion, `/cursor:delegate "Implement @specs/checkout-flow/plan.md"` sends the file to Cursor directly.

This is the closest thing to "plan in Claude, execute in Cursor" in one session: Claude does the thinking, Cursor does the typing, and the task file is a durable contract between the two.

### `/cursor:review [flags] [focus...]`
Expand Down
4 changes: 2 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.

2 changes: 1 addition & 1 deletion plugins/cursor/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "cursor-plugin-cc",
"version": "0.5.0",
"version": "0.5.1",
"description": "Use Cursor CLI from Claude Code to delegate coding tasks to Composer and other Cursor models.",
"type": "module",
"license": "MIT",
Expand Down
2 changes: 1 addition & 1 deletion plugins/cursor/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "cursor",
"version": "0.5.0",
"version": "0.5.1",
"description": "Hand off tasks from Claude Code to cursor-agent. Composer-optimised.",
"author": {
"name": "Tomas Grasl",
Expand Down
77 changes: 73 additions & 4 deletions plugins/cursor/scripts/lib/plan.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -176,9 +176,32 @@ export function slugify(s) {
return cleaned.slice(0, 50) || 'plan';
}

// Hints are ordered most- to least-specific per intent. The tail of each list
// covers common spec-driven formats from other plan/spec tooling (Superpowers,
// GSD, hand-written specs), not just Claude Code's plan-mode shape. (#16)
const SECTION_HINTS = {
context: ['context', 'background', 'why', 'motivation'],
approach: ['approach', 'plan', 'implementation', 'solution', 'design'],
context: [
'context',
'background',
'why',
'motivation',
'problem statement',
'problem',
'overview',
'summary',
],
approach: [
'approach',
'plan',
'implementation',
'solution',
'design',
'requirements',
'specification',
'spec',
'tasks',
'steps',
],
files: [
'file-by-file change list',
'files to touch',
Expand All @@ -188,7 +211,16 @@ const SECTION_HINTS = {
'critical files to modify',
'files',
],
verification: ['verification', 'how to verify', 'test plan', 'tests', 'acceptance criteria'],
verification: [

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] The new verification hint 'testing' matches via key.startsWith('testing '), so a heading like ## Testing infrastructure notes (developer commentary about test setup, not verification steps for the task) would be pulled into the '## How to verify' section. This is a broad generic hint added alongside more specific ones, and could occasionally pull in dev-notes rather than actual acceptance/verification steps.

Suggestion: Consider it acceptable as a heuristic, but if false positives show up in practice, narrow the hint to exact 'testing' (drop the key.startsWith(hint + ' ') prefix match for this specific generic term) so it only fires on a bare ## Testing heading.

'verification',
'how to verify',
'test plan',
'tests',
'acceptance criteria',
'testing',
'validation',
'success criteria',
],
};

/**
Expand Down Expand Up @@ -231,6 +263,36 @@ export function buildTaskContent(plan) {
lines.push('');
lines.push(`> Generated from Claude Code plan: \`${plan.path}\``);
lines.push('');

// Pass-through fallback: a plan/spec in a shape we don't recognise (another
// spec plugin's format, a hand-written doc) must never be reduced to four
// placeholder sections — that silently discards the entire plan body.
// Embed the document verbatim instead and let Cursor follow it as written.
if (!context && !approach && !files && !verification) {
const rawBody = String(plan.raw ?? '')
.replace(/^\s*#\s[^\n]*\n/, '') // drop the leading H1 — already emitted above

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.

[MINOR] In the new pass-through fallback, the regex ^\s*#\s[^\n]*\n used to strip the leading H1 from plan.raw only matches when the H1 title line is literally the first thing in the file (after only whitespace). If the source document has any preamble before the H1 — most commonly a YAML frontmatter block (---\ntitle: ...\n---\n# Title), which is a very plausible shape for 'a hand-written doc' or another spec tool's output that this feature is explicitly designed to support — the regex fails to match, the H1 line is not stripped from rawBody, and the title ends up rendered twice in the generated task file (once from lines.push(\# ${plan.title...}`)and once inside the embedded verbatim body). Verified by reproduction: a raw doc with a---/frontmatter block before # Checkout flowplus an unrecognised section produces# Checkout flow` twice in the output.

Suggestion: Strip the title line by locating and removing the first line that actually matched as title during splitSections (e.g., track its line index, or replace the first occurrence of ^#\s+<escaped title>\s*$ scanned line-by-line) rather than anchoring the regex to the start of the whole string. Alternatively, reconstruct rawBody by re-joining raw.split('\n') after removing the specific line index recorded when the title was parsed.

.trim();
lines.push('## Task specification');
lines.push('');
lines.push(
'_The source plan uses its own structure (not the Claude plan-mode shape); it is included verbatim below — follow it as written._',
);
lines.push('');
lines.push(rawBody || '(the source plan file is empty)');
lines.push('');
lines.push('## How to verify');
lines.push('');
lines.push(
'Follow any verification steps in the specification above; otherwise:\n\n' +
"- Run the project's test suite (`npm test`, `pnpm test`, `task test`, etc.).\n" +
'- Run the type-check / lint if the project has one.\n' +
'- Manual spot-check of the changed behaviour.',
);
lines.push('');
pushConstraints(lines);
return lines.join('\n');
}

lines.push('## Goal');
lines.push('');
lines.push(plan.title ? plan.title : '(see Context below)');
Expand Down Expand Up @@ -258,6 +320,14 @@ export function buildTaskContent(plan) {
'- Manual spot-check of the changed behaviour.',
);
lines.push('');
pushConstraints(lines);
return lines.join('\n');
}

/**
* @param {string[]} lines
*/
function pushConstraints(lines) {
lines.push('## Constraints');
lines.push('');
lines.push(
Expand All @@ -269,5 +339,4 @@ export function buildTaskContent(plan) {
'- Do not modify lockfiles (`package-lock.json`, `pnpm-lock.yaml`, `yarn.lock`) unless dependencies are part of the task.',
);
lines.push('');
return lines.join('\n');
}
66 changes: 66 additions & 0 deletions plugins/cursor/tests/plan.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,72 @@ describe('buildTaskContent', () => {
expect(out).toContain('just this.');
expect(out).toContain('(no Approach');
});

it('passes an unrecognised plan shape through verbatim instead of placeholders', () => {
const raw = [
'# Externí spec',
'',
'## Etapa 1',
'',
'Přidat endpoint /health.',
'',
'## Poznámky',
'',
'Držet se stylu okolního kódu.',
'',
].join('\n');
const plan = {
path: '/tmp/external-spec.md',
title: 'Externí spec',
slug: 'externi-spec',
sections: splitSections(raw).sections,
raw,
};
const out = buildTaskContent(plan);
// The whole body survives…
expect(out).toContain('Přidat endpoint /health.');
expect(out).toContain('Držet se stylu okolního kódu.');
expect(out).toContain('follow it as written');
// …and no placeholder skeleton is emitted.
expect(out).not.toContain('(no Context section');
expect(out).not.toContain('(no Approach');
// Guardrails still apply.
expect(out).toContain('## Constraints');
// The leading H1 is not duplicated.
expect(out.match(/# Externí spec/g)).toHaveLength(1);
});

it('maps common spec-driven headings onto the task sections', () => {
const raw = [
'# Spec-shaped plan',
'',
'## Overview',
'',
'Why we do this.',
'',
'## Requirements',
'',
'- must do X',
'',
'## Testing',
'',
'- run `npm test`',
'',
].join('\n');
const plan = {
path: '/tmp/spec.md',
title: 'Spec-shaped plan',
slug: 'spec-shaped-plan',
sections: splitSections(raw).sections,
raw,
};
const out = buildTaskContent(plan);
expect(out).toContain('Why we do this.');
expect(out).toContain('- must do X');
expect(out).toContain('- run `npm test`');
expect(out).not.toContain('(no Context section');
expect(out).not.toContain('(no Approach');
});
});

describe('resolvePlanPath + parsePlanFile against a temp plans dir', () => {
Expand Down
Loading