Keeps a cpm-next skill's frontmatter model and effort for every turn of its run, including your replies to its questions, and runs `do all` at high effort…

A Claude Code plugin marketplace whose main offering is cpm-next: a planning and building method for Claude Code, plus the mods that keep its model, effort and progress in view. It also holds smaller development tools.
cpm-next turns an idea into working, tested code through a short chain of artefacts kept in your repository's docs/ folder: a discussion record, a product brief, a specification, and epics made of stories with acceptance criteria. Six skills cover the whole path:
| Skill | What it does | Finished when |
|---|---|---|
/cpm-next:party | Discussion with named specialist personas (PM, Architect, Developer, UX, QA and others) | A discussion record is saved and the next step is named |
/cpm-next:plan | Writes whatever is missing of brief, spec and epics, from whatever already exists | The artefacts down to the epics exist and trace upward |
/cpm-next:review | Independent challenge of the epics before they are built | Critical and Warning findings are fixed or waiting on you |
/cpm-next:do | Builds stories, verifies each acceptance criterion with evidence, and has each story audited | Every story in scope is Complete and the tests pass |
/cpm-next:library | Curates reference documents in docs/library/ that planning and building read | Documents carry complete front-matter |
/cpm-next:status | Read-only report of where things stand and the next command to run | Nothing is written |
Each skill declares its model and effort: Opus plans and reviews, Sonnet builds, an Opus auditor agent checks every story before it closes, a Haiku scout agent answers lookups, and status runs on Haiku. The artefact formats are defined in cpm-next/shared/artifacts.md; the plugin's own log of changes is in cpm-next/README.md.
Training material (open in a browser):
cpm-next-onboarding.html — install it and run your first spec in an afternooncpm-next-presentation.html — a slide deck introducing the methodcpm-next-training-guide.html — the full guide: every skill, the artefacts, the mods and day-to-day practiceCPM and DPM, the earlier planning plugins, were withdrawn on 2026-10-08. Their source is in this repository's git history.
Inside Claude Code:
/plugin marketplace add ninthspace/claude-code-marketplace
The suffix is the marketplace's name, not the repository's. marketplace.json declares ninthspace-marketplace, and every installed plugin is keyed by it, so cpm-next@claude-code-marketplace resolves to nothing.
Install these at user scope (the default), so one install serves every repository:
/plugin install cpm-next@ninthspace-marketplace # the six skills, the auditor and scout agents
/plugin install cpm-next-models@ninthspace-marketplace # mod: holds each skill's model and effort, per-story effort
/plugin install cpm-next-progress@ninthspace-marketplace # mod: /progress-tracker, a claude.ai tracker for a spec's epics
/plugin install whats-next@ninthspace-marketplace # mod: /next, a live pane of the work left in the repository
/plugin install plugin-sync@ninthspace-marketplace # mod: reloads open sessions when a plugin here is updated
/reload-plugins
Only cpm-next is required. The four mods are optional, and each adds one thing:
| Plugin | Kind | Without it |
|---|---|---|
cpm-next | Skills and agents | — |
cpm-next-models | Mod (function hooks) | A skill's model and effort last only for the turn that invoked it; later turns of a do run fall back to the session model, and stories are not given their own effort |
cpm-next-progress | Mod, adds /progress-tracker | No shareable progress page; read the epic docs or run /cpm-next:status |
whats-next | Mod, adds /next | No live pane; run /cpm-next:status |
plugin-sync | Mod | After an update, run /reload-plugins in each open session |
Mods are plugins of function hooks that run inside Claude Code. They load like any plugin, run no model calls of their own unless stated, and are switched off and on with the plugin. A session that was open before you installed one needs /reload-plugins to load it.
Per-project setup for /progress-tracker. In a repository whose spec has epics in docs/epics/, run /progress-tracker 01 (the spec's number), then ask Claude to publish the page it writes. The mod saves the tracker's link and keeps the table in step with the epic docs from then on. Details under CPM Next Progress.
Keeping up to date. The marketplace auto-updates when Claude Code starts. plugin-sync then reloads sessions that were already open. To update straight away, from any directory:
claude plugin marketplace update ninthspace-marketplace
claude plugin update cpm-next@ninthspace-marketplace # and each other plugin that changed
/plugin install noteplan@ninthspace-marketplace
/plugin install php-lsp@ninthspace-marketplace
/plugin install js-simplifier@ninthspace-marketplace
/plugin install filament-mockup@ninthspace-marketplace
/plugin install generated-files@ninthspace-marketplace
/plugin install weather@ninthspace-marketplace
Plan and build with six skills: party, plan, review, do, library, status
The skills read and write Markdown artefacts under docs/: discussions/, briefs/, architecture/, specifications/, epics/, reviews/, retros/, quick/ and library/. The artefacts are the only state; there are no progress files to clean up. A typical path:
/cpm-next:party to talk an idea through (optional)./cpm-next:plan to write the brief, spec and epics. It reads what exists, asks its questions in one batch, and fills only the gaps. In a brownfield project it grounds every requirement in the current code./cpm-next:review before a large or risky epic (optional)./cpm-next:do to build: one story (/cpm-next:do 3), one epic, or all, which runs unattended and marks a blocked story instead of stopping. Each acceptance criterion gets an Evidence line, and each story is audited before it closes./cpm-next:status whenever you return to the project.Story effort. plan gives a story Effort: high where a mistake is costly or hard to see, and Effort: low for mechanical edits; with cpm-next-models installed, do builds each story at that effort and raises it for a fix after a failed audit. A low story marked Model: haiku is built by a Haiku subagent.
Agents: auditor (Opus, read-only) checks a finished story's evidence, diff, scope and tests; scout (Haiku, read-only) answers lookups such as where something is used or what a read-only query returns.
Quick Start:
/plugin install cpm-next@ninthspace-marketplace
/reload-plugins
/cpm-next:plan a CSV export for the bookings report
Holds each cpm-next skill's model and effort for its whole run
A Claude Code mod. A skill's model and effort frontmatter normally lasts only for the turn that invoked it. This mod keeps them for every later turn of the run, including your replies to the skill's questions, until another skill runs, you switch model or effort yourself, or /clear. It runs do all at high effort, and gives /cpm-next:do a set_story_effort tool so each story is built at its own Effort, never below high in a do all run. The footer shows what is held, for example cpm-next:do · story 3 · sonnet · low.
Commands: /cpm-models shows what is held; /cpm-models off releases it.
Quick Start:
/plugin install cpm-next-models@ninthspace-marketplace
/reload-plugins
Develop: claude plugin validate cpm-next-models and claude plugin test cpm-next-models.
A claude.ai progress tracker for a cpm-next spec's epics, with a check that it matches the epic docs
A Claude Code mod (a plugin of function hooks). Name a spec and the mod writes a build order for its stories to a small JSON file, which you can edit; Claude then publishes a private claude.ai artifact showing that order as a table, grouped by phase. The mod compares the table with the epic docs in docs/epics/ whenever they change, and tells Claude which rows to update. Status, titles and outstanding blockers come from the epic docs; the order, phase labels, notes and open decisions come from the build-order file.
The mod only reads. It lists the artifact's rows through the ArtifactData tool, which auto mode does not ask about, and never writes to it. Claude's own ArtifactData calls do the writing, so no permission rule is needed. An earlier version wrote to the artifact itself, and auto mode refused those calls intermittently with "The server-side auto mode classifier gave no verdict for ArtifactData".
Build-order file: docs/specifications/NN-build-order.json:
{
"title": "01-Series Build Order",
"spec": "docs/specifications/01-spec-requirements-matrix-todos.md",
"decisions": ["Questions still open, shown in a box above the table"],
"phases": [
{ "label": "No visible change", "items": [
{ "epic": "01-05", "story": 1, "note": "Default value keeps today's text" },
{ "epic": "01-05", "task": "3.1" }
] },
{ "label": "Waiting on input", "note": "Shown beside the phase label", "items": [
{ "epic": "01-05", "story": 2, "waitingOn": "new name from the client" }
] }
]
}
An item is a story (story) or one task (task) of an epic, named by the epic's number prefix. waitingOn shows the item as Waiting on input until it is removed from the file or the item starts. artifact holds the tracker's link; the mod adds it when Claude publishes the page.
The first build order is written from the spec's epics (those whose number starts with the spec's): one phase per epic, epics ordered by their Blocked by epics, each epic's stories ordered by their Blocked by stories and then by number. Superseded and withdrawn epics and stories are left out. Edit the file afterwards to reorder, regroup, split a story into tasks or add waitingOn.
Statuses: Complete, In progress (the story says so, or one of its tasks has started), Waiting on input, Pending. A row's date moves only when its status changes. Blockers shown are the story's and epic's Blocked by items not yet Complete, earlier unfinished tasks of the same story, and any waitingOn.
How it works:
.claude/cpm-next-progress/NN-build-order.pending.json, shows you a one-line notice, and gives Claude a note with the artifact link and the writes. Claude applies them with one ArtifactData batch. More than eight writes stay in the file, and Claude hands them to a Haiku subagent, which reads the file and applies each batch, so they never enter the main conversation.The folder .claude/cpm-next-progress/ holds generated files (the page and the pending writes); /progress-tracker 01 adds it to the project's .gitignore.
Commands:
/progress-tracker — checks every tracker in the repository now and prints what differs, or "up to date"./progress-tracker 01 (or a spec file, or a build-order file) — writes docs/specifications/01-build-order.json from the spec's epics if it does not exist yet, writes the page to .claude/cpm-next-progress/, and adds that folder to .gitignore. Then ask Claude to publish the page. When it does, the mod saves the link to the build-order file and gives Claude the rows to write. init before the name still works.Data layout (in the artifact's database, readable by anyone it is shared with, writable by editors): collection items, one document per row (01-05-s1, 01-05-t3.1); document meta/tracker with the title, spec, last update and decisions.
Quick Start:
/plugin install cpm-next-progress@ninthspace-marketplace
/reload-plugins
/progress-tracker 01
Then ask Claude to publish the page. The spec needs its epics in docs/epics/ first.
Reads: the cpm-next epic format (cpm-next/shared/artifacts.md), as What's Next does.
Develop: claude plugin validate cpm-next-progress and claude plugin test cpm-next-progress. To run the working tree, start Claude Code with --plugin-dir cpm-next-progress.
A live pane and band showing the cpm-next work left in the current repository
A Claude Code mod (a plugin of function hooks). It reads the docs/epics/ and docs/specifications/ folders of the repository the session runs in — or the nearest folder above it that has either — and shows every story not yet Complete, in the order to build them, and every spec no epic has been planned from yet. It reads the files directly, with no model calls, so it stays current as /cpm-next:do or you edit the epics.
What it shows:
doing, ready, or after Story 1 / after Epic …), and each open epic's story count. Opens by itself in a repository with work left when the terminal is at least 144 columns wide; /next opens it at any width.docs/specifications/ that no epic was planned from, in number order. A spec counts as planned when an epic is numbered after it (03-spec-… → 03-01-epic-…) or an epic names its file in Source spec; a spec whose own Status is Complete, Superseded or Withdrawn is left out, as is a withdrawal notice (a Withdrawn or Superseded by field, or WITHDRAWN / SUPERSEDED in its title).Ask Claude button (hotkey a) that asks Sonnet for a short note on what to do next, from the ordered list and the first two stories in full. The note is kept per repository across sessions and dimmed once the epics change after it was written.Order of execution: stories already In Progress first; then the other stories of epics under way (the epic's own Status is In Progress, or one of its stories is); then everything else. Within each group, repeatedly, the ready story with the lowest epic number and story number, treating each as done before choosing the next. So working on a higher-numbered epic out of order moves it to the top once its Status says In Progress. A story is ready when everything its own Blocked by and its epic's Blocked by name is Complete; epics in docs/archive/epics/ count when resolving those dependencies. Stories whose dependencies can never be met (an unknown epic, a cycle) are listed last.
Quick Start:
/plugin install whats-next@ninthspace-marketplace
/reload-plugins
# Open the pane and print the ordered list into the conversation
/next
Reads: the cpm-next epic format (cpm-next/shared/artifacts.md) — Status, Blocked by, Story and Task fields, read case-insensitively, with Done read as Complete. Superseded and Withdrawn epics are skipped.
Develop: claude plugin validate whats-next and claude plugin test whats-next. To run the working tree instead of the installed release, start Claude Code with --plugin-dir whats-next (and uninstall the release, or both draw).
Open sessions pick up plugin updates without a manual /reload-plugins
A Claude Code mod (a plugin of function hooks). Every five minutes, and after each answer, it compares ~/.claude/plugins/installed_plugins.json with the versions of this marketplace's plugins that the session loaded. When one differs, it shows a one-line notice naming the plugins and versions, and runs /reload-plugins, which waits until the session is idle. A project-scope install is compared for sessions in that project, and the user-scope install for all others.
It does not install updates. The marketplace's auto-update does that when any new session starts, or run it yourself once, from any directory:
claude plugin marketplace update ninthspace-marketplace
claude plugin update cpm-next@ninthspace-marketplace # each plugin that changed
Quick Start:
/plugin install plugin-sync@ninthspace-marketplace
/reload-plugins
Install it at user scope so every session loads it. A session already open needs one manual /reload-plugins to load the mod; after that it reloads itself.
Develop: claude plugin validate plugin-sync and claude plugin test plugin-sync.
Search and query NotePlan notes from Claude Code
A skill for searching NotePlan content across:
Results are sorted by most recently modified first.
Quick Start:
# Search for a term
/noteplan coffee
# List all Spaces notes
/noteplan --list --spaces
# Fetch full note by ID
/noteplan --get UUID
# Search with date filters
/noteplan meeting --after 2025-01-01
# Natural language queries
/noteplan find me everything about project planning
Key Features:
Requirements:
PHP semantic code intelligence for Claude Code
Adds 24 LSP tools to Claude Code for PHP files via intelephense and the lsp-mcp-server bridge.
Capabilities:
Quick Start:
# One-time setup (installs intelephense + lsp-mcp-server, configures project)
/php-lsp:setup
# Restart Claude Code — LSP auto-starts on first use
# Check everything is working
/php-lsp:status
Requirements:
Simplify and improve JavaScript and TypeScript code across an entire codebase
A skill that scans all JS/TS files (or a configurable subset) and applies clarity, consistency, and maintainability improvements while preserving exact functionality. Unlike targeted simplification of recently changed files, this skill works across the whole codebase.
Three parallel analysis agents:
Quick Start:
# Simplify all JS/TS files in the project
/js-simplify
# Narrow to a specific directory
/js-simplify src/
# Only git-modified files
/js-simplify only changed
# Focus on a specific pattern
/js-simplify focus on async patterns
Key Features:
Supported File Types:
.js, .mjs, .cjs, .jsx, .ts, .tsxBuild high-fidelity Filament v5 admin mockups for stakeholder sign-off
A skill that turns a product brief or spec into a single self-contained HTML file that looks pixel-accurate to a real Filament v5 admin panel — clickable enough to walk a stakeholder through every screen and flow, and throwaway by design (the real Filament build regenerates all of it natively). Mockups use the real captured Filament theme CSS and Filament's exact fi-* markup, so what stakeholders sign off on is what gets built. Not for production Filament code or customer-facing/front-end mockups.
Workflow:
fi-* grammar; mark genuinely custom components with the mk- namespacedocs/mockups/surface-routing.md) so the downstream builder mockup-to-filament knows which surfaces it owns (works stand-alone — no brief-to-mockups prerequisite)Quick Start:
# Turn a brief/spec into clickable admin screens
create a Filament mockup from docs/specifications/05-spec-admin-panel.md
# Or describe it directly
mock up the admin panel for this PRD
Key Features:
file://, zero environment to stand upmk- vs fi-* boundary distinguishes mockup scaffolding from real Filamentfi-* grammar cheat-sheetmockup-to-filament for Filament, mockup-to-blade for bespoke); emits a routing handoff naming the lane per surfaceRequires: Node + Playwright for the capture/verify scripts (npm i -D playwright && npx playwright install chromium).
A pane listing the files Claude generated this session, each with an Open button
A Claude Code mod (a plugin of function hooks). Skills such as code-to-uml, filament-mockup and the md2docx wrapper write HTML, Office and image files, often into the session scratchpad; this pane collects them so they can be opened without finding the path.
What it records: files with the extensions .html .htm .svg .png .jpg .jpeg .gif .webp .docx .xlsx .pptx .pdf that are
cd <dir> && sets the folder relative paths resolve against). For md2docx and pandoc runs, the .docx beside each .md named is checked too, since md2docx writes there by default.What it shows: a "Files" pane, newest first, up to 30 files: each file's name and fo
hooks/register.ts 164 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { ModelPin, PendingSkill } from '../types'
5import {
6 effortFor, effortStep, floorFor, isHeldSkill, isPersonsSwitch, isStoryEffort, overrideFor, parseFrontmatter,
7 pickInstall, resolveModel, skillName, slashSkill, statusText, storyStep,
8} from './pin'
9
10const STORY_TOOL = 'set_story_effort'
11
12const pin = atom({ plugin: 'cpm-next-models', key: 'pin' } as const, null)
13const pending = atom({ plugin: 'cpm-next-models', key: 'pending' } as const, null)
14
15async function setPin($: EngineInterface, value: ModelPin) {
16 await update($, pin, () => value)
17}
18
19async function release($: EngineInterface) {
20 await update($, pending, () => null)
21 await setPin($, null)
22}
23
24// A skill is seen by up to three events (the typed prompt, the expansion, the Skill tool); the first one
25// to name it sets the pending skill and the rest leave it be.
26async function capture($: EngineInterface, name: string, args: string) {
27 const skill = skillName(name)
28 if (!isHeldSkill(skill)) return
29 const waiting: PendingSkill = await read($, pending)
30 if (waiting?.skill === skill) return
31 await update($, pending, () => ({ skill, args }))
32}
33
34// The engine doesn't pass a skill's frontmatter effort on to the request, and passes its model only in the
35// Skill tool's result, so both are read from the installed SKILL.md.
36async function frontmatterOf($: EngineInterface, skill: string) {
37 try {
38 const home = await $.env.get('HOME')
39 const [plugin, name] = skill.split(':')
40 const installed = JSON.parse(await $.fs.read(`${home}/.claude/plugins/installed_plugins.json`)) as {
41 plugins?: Record<string, { scope?: string; projectPath?: string; installPath: string }[]>
42 }
43 const key = Object.keys(installed.plugins ?? {}).find(k => k.startsWith(`${plugin}@`))
44 const path = pickInstall(installed.plugins?.[key ?? ''] ?? [], await $.session.root())
45
46 return path === undefined ? {} : parseFrontmatter(await $.fs.read(`${path}/skills/${name}/SKILL.md`))
47 } catch {
48 return {}
49 }
50}
51
52export const register: Register = on => {
53 on('prompt.submit', async ($, e, next) => {
54 const typed = slashSkill(e.text)
55 if (typed !== null) await capture($, typed.skill, typed.args)
56
57 return next(e)
58 })
59
60 on('classic.UserPromptExpansion', async ($, e, next) => {
61 await capture($, e.command_name, e.command_args)
62
63 return next(e)
64 })
65
66 on('skill.prompt', async ($, e, next) => {
67 if (isHeldSkill(skillName(e.skill))) await capture($, e.skill, '')
68 else await release($)
69
70 return next(e)
71 })
72
73 on('tool.call', { tool: 'Skill' }, async ($, e, next) => {
74 await capture($, e.skill, e.args ?? '')
75
76 return next(e)
77 })
78
79 // Main loop only: subagents keep the model and effort their spawn chose.
80 on('turn.step', async function* ($, e, next) {
81 if (e.agentId !== undefined) return yield* next(e)
82
83 const waiting = await read($, pending)
84 if (waiting !== null) {
85 await update($, pending, () => null)
86 const found = await frontmatterOf($, waiting.skill)
87 const model = resolveModel(found.model, e.model)
88 const effort = effortFor(waiting.skill, waiting.args, found.effort ?? e.effort)
89 const floor = floorFor(waiting.skill, waiting.args)
90 await setPin($, { skill: waiting.skill, model, effort, turnId: e.turnId, ...(floor === undefined ? {} : { floor }) })
91 return yield* next({ ...e, model, effort })
92 }
93
94 const held = await read($, pin)
95 const stepped = effortStep(held, e.turnId, e.effort)
96 if (stepped.pin !== held) await setPin($, stepped.pin)
97 const model = overrideFor(held, e.model) ?? e.model
98
99 return yield* next({ ...e, model, effort: stepped.effort })
100 })
101
102 on('classic.PostModelSwitch', async ($, e, next) => {
103 if (isPersonsSwitch(e.source)) await release($)
104
105 return next(e)
106 })
107
108 on('session.end', async ($, e, next) => {
109 if (e.reason === 'clear') await release($)
110
111 return next(e)
112 })
113
114 on('session.start', async ($, e, next) => {
115 await $.command.register({ name: 'cpm-models', description: 'Show which cpm-next skill model and effort are held, or "off" to release them' })
116 await $.tool.register({
117 name: STORY_TOOL,
118 description: 'Sets the effort the current /cpm-next:do run builds at, for the story it is starting or the fix it is making. Changes nothing outside a do run, never goes below the run\'s floor, and leaves an effort the person set with /effort alone.',
119 inputSchema: {
120 type: 'object',
121 properties: {
122 effort: { type: 'string', enum: ['low', 'medium', 'high', 'xhigh'] },
123 story: { type: 'string', description: 'The story number, e.g. "3"' },
124 },
125 required: ['effort'],
126 },
127 })
128 // A status entry pinned by an earlier load outlives a reload; this mod draws in the footer instead.
129 $.ui.status(undefined)
130
131 return next(e)
132 })
133
134 // Drawn as one of the footer's mode labels, beside the session model that the status line keeps showing.
135 on('ui.render', { component: 'SessionMode' }, async ($, e, next) => {
136 const label = statusText(await read($, pin))
137 if (label === undefined) return next(e)
138
139 return next({ ...e, props: { ...e.props, modes: [...e.props.modes, label] } })
140 })
141
142 // Unfiltered: a registered tool's name is not in the typed tool list until the mod has reloaded once.
143 on('tool.call', async ($, e, next) => {
144 if (String(e.tool) !== `mcp__cpm-next-models__${STORY_TOOL}`) return next(e)
145 const { effort, story } = e as unknown as { effort?: unknown; story?: unknown }
146 if (!isStoryEffort(effort)) return { deny: 'effort must be one of low, medium, high, xhigh.' }
147 const held = await read($, pin)
148 const step = storyStep(held, effort, typeof story === 'string' && story !== '' ? story : undefined)
149 if (step.pin !== held) await setPin($, step.pin)
150
151 return { result: step.text }
152 }).catch(($, e, next) => (next.called ? next(e) : { deny: `${STORY_TOOL} failed; build at the current effort.` }))
153
154 on('command.run', { command: 'cpm-models' }, async ($, e) => {
155 if (e.args.trim() === 'off') {
156 await release($)
157 return { text: 'Released: turns now run on the session model.' }
158 }
159 const held = await read($, pin)
160
161 return { text: held === null ? 'Nothing held: turns run on the session model.' : `Holding ${held.model}${held.effort === undefined ? '' : ` at ${held.effort} effort`} for ${held.skill}. "/cpm-models off" releases it.` }
162 })
163}
164hooks/pin.ts 121 lines1import type { Effort, ModelPin } from '../types'
2
3const PREFIX = 'cpm-next:'
4
5/** `/cpm-next:do` → `cpm-next:do`: slash expansions and the Skill tool spell the name differently. */
6export function skillName(name: string): string {
7 return name.replace(/^\//, '')
8}
9
10/** Whether a skill's model is held across turns: every cpm-next skill, so one without a model releases the last one's. */
11export function isHeldSkill(skill: string): boolean {
12 return skillName(skill).startsWith(PREFIX)
13}
14
15/**
16 * The effort to hold for a skill: `high` for an unattended `do all` run, where Sonnet tends to stop and
17 * check in at `medium`; otherwise the frontmatter's, as the engine resolved it for the skill's first request.
18 */
19export function effortFor(skill: string, args: string, resolved: Effort | undefined): Effort | undefined {
20 return floorFor(skill, args) ?? resolved
21}
22
23/** The lowest effort a story may set in this run: `high` for `do all`, so a `low` story can't bring back the check-ins. */
24export function floorFor(skill: string, args: string): Effort | undefined {
25 return skillName(skill) === 'cpm-next:do' && /^\s*all\b/i.test(args) ? 'high' : undefined
26}
27
28const STORY_LEVELS = ['low', 'medium', 'high', 'xhigh'] as const
29
30/** Whether a value is an effort a story may set: one of the four build levels. */
31export function isStoryEffort(value: unknown): value is (typeof STORY_LEVELS)[number] {
32 return typeof value === 'string' && (STORY_LEVELS as readonly string[]).includes(value)
33}
34
35/**
36 * The pin after `do` sets a story's effort, and what the tool tells the model. Only a `do` run's held effort
37 * changes, never below its floor; a person's `/effort` (which clears the held effort) is left to stand.
38 */
39export function storyStep(pin: ModelPin, effort: Effort, story: string | undefined): { pin: ModelPin; text: string } {
40 if (pin === null || pin.skill !== 'cpm-next:do') return { pin, text: 'No /cpm-next:do run is held; the session effort applies.' }
41 if (pin.effort === undefined) return { pin, text: 'The person set /effort during this run; their effort applies.' }
42 const floor = pin.floor
43 const level = floor !== undefined && rank(effort) < rank(floor) ? floor : effort
44 const next = { ...pin, effort: level, ...(story === undefined ? {} : { story }) }
45 const raised = level === effort ? '' : ` (raised from ${effort}: this run's floor is ${floor})`
46
47 return { pin: next, text: `Effort ${level}${raised}${story === undefined ? '' : ` for story ${story}`}.` }
48}
49
50const rank = (effort: Effort) => typeof effort === 'number' ? effort : LEVELS.indexOf(effort)
51
52/** The model a main-loop request should be sent on instead of `stepModel`, or undefined to leave it. */
53export function overrideFor(pin: ModelPin, stepModel: string): string | undefined {
54 return pin !== null && pin.model !== stepModel ? pin.model : undefined
55}
56
57/**
58 * The effort a main-loop request should carry, and the pin as it stands afterwards. Inside the skill's own
59 * turn the held effort applies. On the first request of a later turn the session's effort is recorded as the
60 * baseline; a later request whose effort differs from it means the person ran `/effort`, which wins.
61 */
62export function effortStep(pin: ModelPin, turnId: string, stepEffort: Effort | undefined): { effort: Effort | undefined; pin: ModelPin } {
63 if (pin === null || pin.effort === undefined) return { effort: stepEffort, pin }
64 if (turnId === pin.turnId) return { effort: pin.effort, pin }
65 if (pin.baseline === undefined) return { effort: pin.effort, pin: stepEffort === undefined ? pin : { ...pin, baseline: stepEffort } }
66 if (stepEffort !== pin.baseline) return { effort: stepEffort, pin: { ...pin, effort: undefined } }
67
68 return { effort: pin.effort, pin }
69}
70
71/** Whether a model switch is the person's own choice, which releases the hold; a fallback or resume does not. */
72export function isPersonsSwitch(source: string): boolean {
73 return source === 'command' || source === 'picker' || source === 'sdk'
74}
75
76/** `claude-sonnet-5-5` → `sonnet`; an id with no family name is shown whole. */
77export function familyOf(model: string): string {
78 return /opus|sonnet|haiku|fable/.exec(model)?.[0] ?? model
79}
80
81/** The footer label, e.g. "cpm-next:do · story 3 · sonnet · high"; undefined when nothing is held. */
82export function statusText(pin: ModelPin): string | undefined {
83 if (pin === null) return undefined
84 const story = pin.story === undefined ? '' : ` · story ${pin.story}`
85 const effort = pin.effort === undefined ? '' : ` · ${pin.effort}`
86
87 return `${pin.skill}${story} · ${familyOf(pin.model)}${effort}`
88}
89
90const LEVELS = ['low', 'medium', 'high', 'xhigh', 'max']
91
92/** The `model` and `effort` lines of a SKILL.md's frontmatter; absent keys, or an unknown effort, are left out. */
93export function parseFrontmatter(text: string): { model?: string; effort?: Effort } {
94 const block = /^---\r?\n([\s\S]*?)\r?\n---/.exec(text)?.[1]
95 if (block === undefined) return {}
96 const get = (key: string) => new RegExp(`^${key}:\\s*(\\S+)\\s*$`, 'm').exec(block)?.[1]
97 const effort = get('effort')
98
99 return { model: get('model'), effort: LEVELS.includes(effort ?? '') ? (effort as Effort) : undefined }
100}
101
102/** `/cpm-next:do all` → `{ skill: 'cpm-next:do', args: 'all' }`; null for anything else a person types. */
103export function slashSkill(text: string): { skill: string; args: string } | null {
104 const found = /^\/(cpm-next:[\w-]+)(?:\s+([\s\S]*))?$/.exec(text.trim())
105
106 return found === null ? null : { skill: found[1] ?? '', args: found[2] ?? '' }
107}
108
109/** A frontmatter model as a request can name it: a bare family (`sonnet`) takes the session model's version. */
110export function resolveModel(requested: string | undefined, sessionModel: string): string {
111 if (requested === undefined || requested === 'inherit') return sessionModel
112 if (!/^(opus|sonnet|haiku|fable)$/.test(requested)) return requested
113
114 return sessionModel.replace(/opus|sonnet|haiku|fable/, requested)
115}
116
117/** Which install of a plugin the session runs: the project's own, else the user-scope one, else the first. */
118export function pickInstall(installs: { scope?: string; projectPath?: string; installPath: string }[], root: string): string | undefined {
119 return (installs.find(i => i.projectPath === root) ?? installs.find(i => i.scope === 'user') ?? installs[0])?.installPath
120}
121types/index.d.ts 28 lines1/** An effort level as a model request carries it. */
2export type Effort = 'low' | 'medium' | 'high' | 'xhigh' | 'max' | number
3
4/**
5 * The cpm-next skill whose model and effort are held; null when nothing is held.
6 * `turnId` is the turn the skill started in. `baseline` is the session's effort as first seen after that
7 * turn, so a later `/effort` shows up as a difference from it and releases the held effort.
8 * `floor` is the lowest effort a story may set (`high` in a `do all` run); `story` is the story `do` is building.
9 */
10export type ModelPin = {
11 skill: string
12 model: string
13 effort?: Effort
14 turnId: string
15 baseline?: Effort
16 floor?: Effort
17 story?: string
18} | null
19
20/** A cpm-next skill just invoked, with its arguments and, from the Skill tool, its frontmatter model. */
21export type PendingSkill = { skill: string; args: string; model?: string } | null
22
23declare module 'claude-code' {
24 interface PluginState {
25 'cpm-next-models': { pin: ModelPin; pending: PendingSkill }
26 }
27}
28