@bridge_gpt/mcp-server 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -193,7 +193,7 @@ These features are useful for most tickets.
193
193
  **4. Deep Research**
194
194
  - **What it does:** Runs multi-source, fact-checked web research on a technical topic and returns a cited report.
195
195
  - **When it's useful:** (Architecture | Refinement) When a decision hinges on outside knowledge (libraries, best practices, standards) you don't already have.
196
- - **How to use it:** `/deep-research <question>`
196
+ - **How to use it:** `/bridge-research <question>`
197
197
 
198
198
  **5. Jira Ticket Writer**
199
199
  - **What it does:** An agent that drafts a well-structured Jira ticket from a plain description, applying your project's standards.
@@ -211,57 +211,57 @@ These features are useful for most tickets.
211
211
 
212
212
  These features are good to know, but you probably won't use them every day.
213
213
 
214
- **1. Second Opinion**
215
- - **What it does:** Gets an immediate critique of any text from a different model family — no artifact saved, just the reply.
216
- - **When it's useful:** (Architecture | Refinement | Implementation) Any time you want a quick sanity check on a plan, draft, or decision from a fresh perspective.
217
- - **How to use it:** ask your agent — *"Get a second opinion from Gemini on whether the BAPI-123 plan's migration step is safe to run against prod."*
218
- - **Options:** pick the provider (anthropic / openai / gemini) and the tier (cheap / basic / premium).
219
-
220
- **2. Idea to Ticket**
221
- - **What it does:** Turns a one-line idea into a Jira Task/Spike (or an Epic plus child tickets), with research, duplicate detection, and a critique pass built in.
222
- - **When it's useful:** (Refinement | Automation) When you have a rough idea and want a fully-formed, uploaded ticket without the manual draft-and-refine loop.
223
- - **How to use it:** `/idea-to-ticket <idea>`
224
-
225
- **3. Explore Ticket**
226
- - **What it does:** Explores the codebase for a task and recommends implementation options or surfaces clarifying questions, with optional research.
227
- - **When it's useful:** (Architecture | Refinement) Before writing a ticket or plan, when you're unsure how a change would fit the existing code.
228
- - **How to use it:** `/explore-ticket <task>` — *"Explore the codebase for how we'd add a Mistral LLM provider and recommend 2–3 implementation options."*
229
-
230
- **4. Plan Ticket**
214
+ **1. Plan Ticket**
231
215
  - **What it does:** Generates a step-by-step implementation plan for a ticket, with references to real code files, and saves it locally.
232
216
  - **When it's useful:** (Refinement | Implementation) Once a ticket is solid and you want a concrete build plan before (or instead of) auto-implementing.
233
217
  - **How to use it:** `/plan-ticket BAPI-123`
234
218
  - **Flags:** `--provider <name>` choose the model provider · `--second-opinion <provider>` cross-check the plan with a second provider.
235
219
 
236
- **5. Clarify Ticket**
220
+ **2. Clarify Ticket**
237
221
  - **What it does:** Generates clarifying questions for a ticket (or debugging guidance for bugs) and saves them locally.
238
222
  - **When it's useful:** (Refinement) When a ticket feels under-specified and you want the open questions made explicit.
239
223
  - **How to use it:** `/clarify-ticket BAPI-123` — *"Generate clarifying questions for BAPI-123"*
240
224
  - **Flags:** `--provider <name>` choose the model provider · `--second-opinion <provider>` cross-check with a second provider.
241
225
 
242
- **6. Critique Ticket**
226
+ **3. Critique Ticket**
243
227
  - **What it does:** Critiques a ticket's quality against your project standards and lists deviations + improvements.
244
228
  - **When it's useful:** (Refinement) When you want a quality gate on a ticket before it's worked.
245
229
  - **How to use it:** `/critique-ticket BAPI-123` — *"Critique BAPI-123 against our project standards and list what's missing or deviating."*
246
230
  - **Flags:** `--provider <name>` choose the model provider · `--second-opinion <provider>` cross-check with a second provider.
247
231
 
248
- **7. Full Automation**
249
- - **What it does:** Drives the whole chain end-to-end: idea ticket(s) review each spawn worktrees to implement.
250
- - **When it's useful:** (Automation) When you want to go from a raw idea to in-progress implementation with minimal hands-on steps.
251
- - **How to use it:** `/full-automation <idea>` (command only creates tickets, spawns worktrees, and carries scheduling/`--max-children` flags that free text can't).
252
- - **Flags:** `--require-approval` toggle the approval gates, full automation runs end to end by default.
232
+ **4. Explore Ticket**
233
+ - **What it does:** Explores the codebase for a task and recommends implementation options or surfaces clarifying questions, with optional research.
234
+ - **When it's useful:** (Architecture | Refinement) Before writing a ticket or plan, when you're unsure how a change would fit the existing code.
235
+ - **How to use it:** `/explore-ticket <task>` *"Explore the codebase for how we'd add a Mistral LLM provider and recommend 2–3 implementation options."*
236
+
237
+ **5. Second Opinion**
238
+ - **What it does:** Gets an immediate critique of any text from a different model family — no artifact saved, just the reply.
239
+ - **When it's useful:** (Architecture | Refinement | Implementation) Any time you want a quick sanity check on a plan, draft, or decision from a fresh perspective.
240
+ - **How to use it:** ask your agent — *"Get a second opinion from Gemini on whether the BAPI-123 plan's migration step is safe to run against prod."*
241
+ - **Options:** pick the provider (anthropic / openai / gemini) and the tier (cheap / basic / premium).
242
+
243
+ **6. Generate Image**
244
+ - **What it does:** Generates an image from a text prompt using a provider image model (OpenAI `gpt-image-2` by default, or Google Imagen) and returns the image directly. Spends provider credits on every call.
245
+ - **When it's useful:** (Architecture | Refinement) When you want a quick visual — a UI mockup, diagram, or illustration — to anchor a design discussion or attach to a ticket.
246
+ - **How to use it:** ask your agent — *"Generate an image of a dashboard showing SOC2 evidence freshness as a traffic-light grid."*
247
+ - **Options:** `provider` openai (`gpt-image-2`) / gemini (Imagen — adds an invisible SynthID watermark) · `quality` low (default, cheapest) / medium / high · `size` 1024x1024 / 1024x1536 / 1536x1024. The image is always saved to `BAPI_DOCS_DIR/images/` and also returned inline.
253
248
 
254
- **8. Implement Ticket**
249
+ **7. Implement Ticket**
255
250
  - **What it does:** Full build for one ticket: generate a plan, write the code, commit, open a PR, and monitor CI.
256
251
  - **When it's useful:** (Implementation) When a ticket is ready and you want it taken from plan to open PR in one go.
257
252
  - **How to use it:** `/implement-ticket BAPI-123` (command only — "implement X" as free text almost always triggers a freehand build instead of the Bridge plan→code→PR→CI pipeline).
258
253
  - **Flags:** `--auto` skip the approval gates (e.g. auto-commit/push).
259
254
 
260
- **9. Generate Image**
261
- - **What it does:** Generates an image from a text prompt using a provider image model (OpenAI `gpt-image-2` by default, or Google Imagen) and returns the image directly. Spends provider credits on every call.
262
- - **When it's useful:** (Architecture | Refinement) When you want a quick visual a UI mockup, diagram, or illustration — to anchor a design discussion or attach to a ticket.
263
- - **How to use it:** ask your agent*"Generate an image of a dashboard showing SOC2 evidence freshness as a traffic-light grid."*
264
- - **Options:** `provider` openai (`gpt-image-2`) / gemini (Imagen adds an invisible SynthID watermark) · `quality` low (default, cheapest) / medium / high · `size` 1024x1024 / 1024x1536 / 1536x1024 · `save_locally` write the image to `docs/images`.
255
+ **8. Full Automation**
256
+ - **What it does:** Drives the whole chain end-to-end: idea ticket(s) review each spawn worktrees to implement.
257
+ - **When it's useful:** (Automation) When you want to go from a raw idea to in-progress implementation with minimal hands-on steps.
258
+ - **How to use it:** `/full-automation <idea>` (command only creates tickets, spawns worktrees, and carries scheduling/`--max-children` flags that free text can't).
259
+ - **Flags:** `--require-approval` toggle the approval gates, full automation runs end to end by default.
260
+
261
+ **9. Idea to Ticket**
262
+ - **What it does:** Turns a one-line idea into a Jira Task/Spike (or an Epic plus child tickets), with research, duplicate detection, and a critique pass built in.
263
+ - **When it's useful:** (Refinement | Automation) When you have a rough idea and want a fully-formed, uploaded ticket without the manual draft-and-refine loop.
264
+ - **How to use it:** `/idea-to-ticket <idea>`
265
265
 
266
266
  ### Tier 3 — Now and then
267
267
 
@@ -277,40 +277,40 @@ These features are useful once in a while, but you probably won't need them ever
277
277
  - **When it's useful:** (Implementation) After making changes, to confirm everything passes and auto-fix straightforward breakages.
278
278
  - **How to use it:** `/run-tests` (`--unit-only`, `--skip-e2e`)
279
279
 
280
- **3. Learn Repository**
281
- - **What it does:** Researches and documents the repo's architecture, testing, review, and correctness standards, then saves them to Bridge for future agents.
282
- - **When it's useful:** (Setup/Learning) When onboarding a new repo, or after big changes, so Bridge's agents follow your conventions.
283
- - **How to use it:** `/learn-repository`
284
-
285
- **4. Teach Bridge**
286
- - **What it does:** Takes a plain-English instruction, figures out which standards field it belongs to, and merges it in (admin only).
287
- - **When it's useful:** (Setup/Learning) When you notice the agents missing a convention and want to correct it in one sentence.
288
- - **How to use it:** `/teach-bridge <teaching>` — *"Teach Bridge: always use data-testid selectors in E2E tests."*
280
+ **3. Plan Epic**
281
+ - **What it does:** Decomposes a large epic into sub-tasks with a structured exploration doc for each.
282
+ - **When it's useful:** (Architecture | Refinement) When a feature is too big for one ticket and you need it broken down and scoped.
283
+ - **How to use it:** `/plan-epic <epic>` — *"Decompose the epic 'migrate PayPal token storage off Custom Objects' into sub-tasks with an exploration doc for each."*
289
284
 
290
- **5. Update Ticket**
285
+ **4. Update Ticket**
291
286
  - **What it does:** Synthesizes a ticket's clarifying answers and critique into a rewritten description and pushes it to Jira.
292
287
  - **When it's useful:** (Refinement) After review, to fold the resolved questions and fixes back into the ticket itself.
293
288
  - **How to use it:** `/update-ticket BAPI-123` (command only — does a full overwrite of the live Jira description; "update" as free text is both vague and hard to reverse).
294
289
 
295
- **6. Plan Epic**
296
- - **What it does:** Decomposes a large epic into sub-tasks with a structured exploration doc for each.
297
- - **When it's useful:** (Architecture | Refinement) When a feature is too big for one ticket and you need it broken down and scoped.
298
- - **How to use it:** `/plan-epic <epic>` — *"Decompose the epic 'migrate PayPal token storage off Custom Objects' into sub-tasks with an exploration doc for each."*
290
+ **5. Get Ticket**
291
+ - **What it does:** Retrieves the full details of a Jira ticket (summary, status, description, etc.).
292
+ - **When it's useful:** (Refinement | Implementation) Any time you want the agent to read a ticket before acting on it.
293
+ - **How to use it:** ask your agent — *"Pull up BAPI-123 and show me its description, status, and acceptance criteria."*
294
+
295
+ **6. Write Comment**
296
+ - **What it does:** Posts a comment on a Jira ticket (markdown; long ones can attach as a file).
297
+ - **When it's useful:** (Refinement | Implementation) To leave context, status, or a decision trail on the ticket.
298
+ - **How to use it:** ask your agent — *"Post a comment on BAPI-123: blocked on the expired Atlassian token — will retry after it's rotated."*
299
299
 
300
300
  **7. Download / Upload Attachment**
301
301
  - **What it does:** Pulls files off a Jira ticket to disk, or attaches a local file to a ticket.
302
302
  - **When it's useful:** (Refinement | Implementation) When a ticket has design files/logs you need locally, or you want to attach output back to it.
303
303
  - **How to use it:** ask your agent — *"Download the design mockups attached to BAPI-123 into my docs folder."* / *"Attach build-log.txt to BAPI-123."*
304
304
 
305
- **8. Write Comment**
306
- - **What it does:** Posts a comment on a Jira ticket (markdown; long ones can attach as a file).
307
- - **When it's useful:** (Refinement | Implementation) To leave context, status, or a decision trail on the ticket.
308
- - **How to use it:** ask your agent — *"Post a comment on BAPI-123: blocked on the expired Atlassian token — will retry after it's rotated."*
305
+ **8. Learn Repository**
306
+ - **What it does:** Researches and documents the repo's architecture, testing, review, and correctness standards, then saves them to Bridge for future agents.
307
+ - **When it's useful:** (Setup/Learning) When onboarding a new repo, or after big changes, so Bridge's agents follow your conventions.
308
+ - **How to use it:** `/learn-repository`
309
309
 
310
- **9. Get Ticket**
311
- - **What it does:** Retrieves the full details of a Jira ticket (summary, status, description, etc.).
312
- - **When it's useful:** (Refinement | Implementation) Any time you want the agent to read a ticket before acting on it.
313
- - **How to use it:** ask your agent — *"Pull up BAPI-123 and show me its description, status, and acceptance criteria."*
310
+ **9. Teach Bridge**
311
+ - **What it does:** Takes a plain-English instruction, figures out which standards field it belongs to, and merges it in (admin only).
312
+ - **When it's useful:** (Setup/Learning) When you notice the agents missing a convention and want to correct it in one sentence.
313
+ - **How to use it:** `/teach-bridge <teaching>` — *"Teach Bridge: always use data-testid selectors in E2E tests."*
314
314
 
315
315
  ### Operational commands
316
316
 
@@ -372,6 +372,8 @@ Each `KEY` must match `[A-Z]+-[0-9]+` (e.g., `BAPI-248`). The CLI creates/switch
372
372
  npx -y @bridge_gpt/mcp-server start-tickets --agent cursor-agent BAPI-248
373
373
  ```
374
374
 
375
+ **Difficulty-based model routing.** Before launching each agent, the CLI selects an implementation **model tier** from the ticket's `difficulty` (1-2 → cheap, 3-5 → basic, 6+ → premium) and injects it as a `--model` flag at the spawn boundary. The Python backend returns only the coarse tier (`GET /jira/tickets/{KEY}/model-tier`, computing + caching difficulty on demand); this CLI alone maps a tier to the agent-specific alias (`claude`: `haiku`/`sonnet`/`opus`; `cursor-agent`: version-suffixed strings validated against `cursor-agent --list-models`). It is gated per repo by `difficulty_model_routing_enabled` (default **ON**) with an optional `difficulty_model_tier_overrides` JSON map (tier → alias). Routing is **fail-open**: missing credentials, an evaluation failure/timeout, a backend `fallback`, an invalid/unavailable alias, an unadvertised Cursor model, or an agent without `--model` support all omit `--model` (the agent uses its default) and surface a per-ticket warning rather than failing the spawn. `--dry-run` does **not** fetch tiers or inject `--model`.
376
+
375
377
  **Cross-platform spawning.** The CLI routes spawning per platform; `--dry-run` previews the platform-correct command form on any OS. An unsupported `process.platform` (not `darwin`/`win32`/`linux`) fails fast with a clear "unsupported platform" message.
376
378
 
377
379
  - **macOS** — opens a Terminal.app or iTerm tab via `osascript`.
@@ -540,7 +542,7 @@ The server registers **55 tools**. Async AI tools follow a request/get pattern:
540
542
  - **Jira status** — `get_jira_transitions`, `update_jira_status`, `resolve_target_status`
541
543
  - **Repository & CI** — `parse_repository`, `get_parse_status`, `regenerate_directory_map`, `create_pull_request`, `resolve_ci_checks`, `poll_ci_checks`
542
544
  - **Pipelines & automation** — `list_pipelines`, `get_pipeline_recipe`, `run_pipeline`, `resume_pipeline`, `list_pipeline_runs`, `delete_pipeline_run`, `run_full_automation`, `resume_full_automation`
543
- - **Config & telemetry** — `get_project_standards`, `list_config_fields`, `get_config_field`, `update_config_field`, `record_tiered_section_metric`
545
+ - **Config** — `get_project_standards`, `list_config_fields`, `get_config_field`, `update_config_field`
544
546
 
545
547
  ### Bundled pipelines
546
548
 
@@ -1,15 +1,17 @@
1
1
  /**
2
- * The v1 `claude` agent launcher (BAPI-327).
2
+ * The `claude` agent launcher (BAPI-327, generalized in BAPI-351).
3
3
  *
4
4
  * Resolves the `claude` binary against the *baked schedule-time PATH* (not the
5
- * ambient process default), builds the locked
6
- * `/full-automation --scheduled-at <T> --idea-file <abs> [--auto]`
7
- * prompt, and emits `{ exe, args: ["-p", prompt] }`. Claude Code has no
5
+ * ambient process default) and builds the scheduled-run invocation by delegating
6
+ * to the shared `renderScheduledPrompt` renderer (no hard-coded `/full-automation`
7
+ * prompt). It emits `{ exe, args: ["-p", prompt] }`. Claude Code has no
8
8
  * working-directory flag, so the cwd is always set by the scheduler unit — this
9
- * adapter must never add a cwd argument to the invocation.
9
+ * adapter must never add a working-directory argument to the invocation.
10
10
  */
11
11
  import { pathApiForPlatform } from "../scheduler-backends/types.js";
12
12
  import { posixShellQuote, windowsCmdQuote } from "../scheduler-backends/escaping.js";
13
+ import { renderScheduledPrompt } from "../scheduled-prompt.js";
14
+ import { quotePromptToken } from "../scheduled-prompt.js";
13
15
  /**
14
16
  * Resolve a bare command to an absolute path using the platform PATH-probe,
15
17
  * forcing the baked PATH so the schedule resolves the same binary the user had
@@ -40,24 +42,30 @@ export async function resolveCommandOnPath(command, envPath, deps) {
40
42
  return pathApi.normalize(candidate);
41
43
  }
42
44
  /**
43
- * Quote the idea-file path so it survives as a single `/full-automation`
44
- * argument even when it contains spaces or markup-significant characters
45
- * (`&`, `<`, `>`, …) that would otherwise be split or mangled when the slash
46
- * command is parsed. Only an embedded double quote needs escaping; the same
47
- * absolute path is additionally baked into `BRIDGE_GPT_IDEA_FILE` by every
48
- * backend as a robust environment fallback.
45
+ * Backward-compatibility shim for callers that quoted an idea-file path for a
46
+ * `/full-automation` prompt. Generalized prompt-token quoting now lives in
47
+ * `quotePromptToken`; this preserves the original quote semantics (always wrap in
48
+ * double quotes, escape embedded quotes) for any remaining path callers.
49
49
  */
50
50
  export function quoteIdeaFileForPrompt(ideaFile) {
51
51
  return `"${ideaFile.replace(/"/g, '\\"')}"`;
52
52
  }
53
+ /** Re-export the generalized token quoter so launcher consumers have one import. */
54
+ export { quotePromptToken };
53
55
  /**
54
- * Build the exact full-automation prompt. `--auto` is appended by
55
- * default; it is omitted only when the caller selected `--no-auto`.
56
+ * Build the scheduled-run prompt for the Claude launcher by delegating to the
57
+ * shared renderer. Exposed for focused unit testing of the launcher wiring.
56
58
  */
57
59
  export function buildClaudePrompt(input) {
58
- const base = `/full-automation --scheduled-at ${input.runAtIso} ` +
59
- `--idea-file ${quoteIdeaFileForPrompt(input.ideaFile)}`;
60
- return input.autoApprove ? `${base} --auto` : base;
60
+ return renderScheduledPrompt({
61
+ scheduleId: input.scheduleId,
62
+ scheduledAt: input.runAtIso,
63
+ autoApprove: input.autoApprove,
64
+ commandName: input.commandName,
65
+ args: input.args,
66
+ commandBody: input.commandBody,
67
+ schema: input.schema,
68
+ });
61
69
  }
62
70
  const CLAUDE_CAPABILITY = {
63
71
  name: "claude",
@@ -65,7 +73,7 @@ const CLAUDE_CAPABILITY = {
65
73
  supportsCwdFlag: false,
66
74
  promptFlag: "-p",
67
75
  };
68
- /** Create the v1 Claude agent launcher. */
76
+ /** Create the Claude agent launcher. */
69
77
  export function createClaudeLauncher() {
70
78
  return {
71
79
  capability: CLAUDE_CAPABILITY,
@@ -0,0 +1,65 @@
1
+ /**
2
+ * The `cursor-agent` agent launcher (BAPI-351).
3
+ *
4
+ * A near-mirror of the Claude launcher: it resolves the `cursor-agent` binary
5
+ * against the baked schedule-time PATH and builds the scheduled-run prompt via
6
+ * the shared `renderScheduledPrompt` renderer (so the drift gate and command body
7
+ * are identical across agents). A local spike confirmed cursor-agent resolves the
8
+ * same `.claude/commands` catalog and exits cleanly in headless mode.
9
+ *
10
+ * Cursor differences vs. Claude:
11
+ * - headless invocation needs `--output-format text`, `--trust`, and a
12
+ * `--workspace <repoPath>` flag (Cursor's working-directory flag), with the
13
+ * prompt as the final positional token;
14
+ * - headless auth is via the `CURSOR_API_KEY` environment variable. This adapter
15
+ * never reads or prints that value — auth readiness is a prerequisite check.
16
+ */
17
+ import { posixShellQuote, windowsCmdQuote } from "../scheduler-backends/escaping.js";
18
+ import { renderScheduledPrompt } from "../scheduled-prompt.js";
19
+ import { resolveCommandOnPath } from "./claude.js";
20
+ /** Build the scheduled-run prompt for the Cursor launcher via the shared renderer. */
21
+ export function buildCursorPrompt(input) {
22
+ return renderScheduledPrompt({
23
+ scheduleId: input.scheduleId,
24
+ scheduledAt: input.runAtIso,
25
+ autoApprove: input.autoApprove,
26
+ commandName: input.commandName,
27
+ args: input.args,
28
+ commandBody: input.commandBody,
29
+ schema: input.schema,
30
+ });
31
+ }
32
+ const CURSOR_CAPABILITY = {
33
+ name: "cursor-agent",
34
+ command: "cursor-agent",
35
+ supportsCwdFlag: true,
36
+ promptFlag: "-p",
37
+ };
38
+ /** Create the cursor-agent launcher. */
39
+ export function createCursorLauncher() {
40
+ return {
41
+ capability: CURSOR_CAPABILITY,
42
+ resolveBinary(envPath, deps) {
43
+ return resolveCommandOnPath("cursor-agent", envPath, deps);
44
+ },
45
+ buildInvocation(exe, input) {
46
+ const prompt = buildCursorPrompt(input);
47
+ // Cursor takes the working directory via --workspace and the prompt as the
48
+ // final positional token; CURSOR_API_KEY (auth) is never placed in argv.
49
+ const args = [
50
+ CURSOR_CAPABILITY.promptFlag,
51
+ "--output-format",
52
+ "text",
53
+ "--trust",
54
+ "--workspace",
55
+ input.repoPath,
56
+ prompt,
57
+ ];
58
+ return { exe, args, prompt };
59
+ },
60
+ formatInvocationLine(invocation, platform) {
61
+ const quote = platform === "win32" ? windowsCmdQuote : posixShellQuote;
62
+ return [invocation.exe, ...invocation.args].map((part) => quote(part)).join(" ");
63
+ },
64
+ };
65
+ }
@@ -1,17 +1,32 @@
1
1
  /**
2
- * Agent-launcher registry (BAPI-327). v1 ships only `claude`; the lookup returns
3
- * `null` for every other name so the CLI can reject unsupported agents with a
4
- * clear message rather than silently falling back.
2
+ * Agent-launcher registry (BAPI-327, extended in BAPI-351).
3
+ *
4
+ * Maps a launcher name to its adapter. BAPI-351 added `cursor-agent` beside the
5
+ * default `claude`; the lookup returns `null` for every other name so the CLI can
6
+ * reject unsupported agents with a clear message rather than silently falling
7
+ * back. The supported names are kept aligned with `AgentName` from
8
+ * `agent-registry.ts` to avoid registry drift.
5
9
  */
6
10
  import { createClaudeLauncher } from "./claude.js";
11
+ import { createCursorLauncher } from "./cursor.js";
7
12
  export { createClaudeLauncher } from "./claude.js";
13
+ export { createCursorLauncher } from "./cursor.js";
8
14
  const CLAUDE_LAUNCHER = createClaudeLauncher();
9
- /** Return the launcher for a name, or `null` when unsupported in v1. */
15
+ const CURSOR_LAUNCHER = createCursorLauncher();
16
+ /** All supported launcher names, in deterministic order (claude first/default). */
17
+ const LAUNCHER_NAMES = ["claude", "cursor-agent"];
18
+ /** Return the launcher for a name, or `null` when unsupported. */
10
19
  export function getAgentLauncher(name) {
11
- return name === "claude" ? CLAUDE_LAUNCHER : null;
20
+ switch (name) {
21
+ case "claude":
22
+ return CLAUDE_LAUNCHER;
23
+ case "cursor-agent":
24
+ return CURSOR_LAUNCHER;
25
+ default:
26
+ return null;
27
+ }
12
28
  }
13
- /** Comma-separated list of valid agent-launcher names (just `claude` in v1). */
29
+ /** Comma-separated list of valid agent-launcher names (`claude, cursor-agent`). */
14
30
  export function formatValidAgentLauncherNames() {
15
- const names = ["claude"];
16
- return names.join(", ");
31
+ return LAUNCHER_NAMES.join(", ");
17
32
  }
@@ -11,6 +11,19 @@
11
11
  * `start-tickets.ts` / `start-tickets-prereqs.ts` / `doctor.ts`) so every other
12
12
  * module can import it without risking a circular dependency.
13
13
  */
14
+ /**
15
+ * Conservative allowlist pattern for any model alias before it can reach shell
16
+ * construction. Mirrors the backend `_MODEL_ALIAS_PATTERN` in jira_api.py.
17
+ */
18
+ export const MODEL_ALIAS_PATTERN = /^[A-Za-z0-9._:-]+$/;
19
+ /** True only when `value` is a non-empty string matching the alias pattern. */
20
+ export function isValidModelAlias(value) {
21
+ return typeof value === "string" && value.length > 0 && MODEL_ALIAS_PATTERN.test(value);
22
+ }
23
+ /** Type guard: true only for the three known tier names. */
24
+ export function isModelTier(value) {
25
+ return value === "cheap" || value === "basic" || value === "premium";
26
+ }
14
27
  /**
15
28
  * The registry: the ONLY place mapping an agent name to its command/spec. Seeded
16
29
  * with exactly `claude` (default) and `cursor-agent`. `as const satisfies` keeps
@@ -28,6 +41,12 @@ export const AGENT_REGISTRY = {
28
41
  win32: "npm install -g @anthropic-ai/claude-code",
29
42
  },
30
43
  authNote: "Claude Code authenticates interactively on first run — follow its login/auth prompt if asked.",
44
+ supportsModelOverride: true,
45
+ modelFlag: "--model",
46
+ // Claude family aliases float to the latest release and never drift, so they
47
+ // can be validated against a static allowlist.
48
+ tierModels: { cheap: "haiku", basic: "sonnet", premium: "opus" },
49
+ staticModelAliasAllowlist: ["haiku", "sonnet", "opus"],
31
50
  },
32
51
  "cursor-agent": {
33
52
  name: "cursor-agent",
@@ -39,6 +58,24 @@ export const AGENT_REGISTRY = {
39
58
  win32: "irm 'https://cursor.com/install?win32=true' | iex",
40
59
  },
41
60
  authNote: "Run cursor-agent login to authenticate; doctor checks PATH presence only, not login state.",
61
+ supportsModelOverride: true,
62
+ modelFlag: "--model",
63
+ // NOTE: Cursor model strings are version-sensitive and DRIFT between
64
+ // releases — unlike claude's stable family aliases. The ids are not even
65
+ // internally consistent across versions (Cursor advertises Sonnet/Opus 4.6
66
+ // as `claude-4.6-sonnet-…`/`claude-4.6-opus-…` but Opus 4.7/4.8 as
67
+ // `claude-opus-4-7-…`/`claude-opus-4-8-…`), so these defaults WILL go stale.
68
+ // There is therefore NO staticModelAliasAllowlist here; any resolved cursor
69
+ // alias is validated non-interactively at runtime (against
70
+ // `cursor-agent --list-models`) before injection, and an unadvertised id
71
+ // fail-opens to the cursor default. To pin a current id without a release,
72
+ // use the per-repo `difficulty_model_tier_overrides` config.
73
+ // Last verified against `cursor-agent --list-models` on 2026-06-15.
74
+ tierModels: {
75
+ cheap: "auto",
76
+ basic: "claude-4.6-sonnet-medium",
77
+ premium: "claude-opus-4-8-thinking-high",
78
+ },
42
79
  },
43
80
  };
44
81
  /** The default agent used when `--agent` is omitted. */
@@ -66,3 +103,34 @@ export function resolveAgentSpec(name) {
66
103
  export function formatValidAgentNames() {
67
104
  return listAgentNames().join(", ");
68
105
  }
106
+ /**
107
+ * Resolve the concrete model alias to inject for a given agent + tier, applying
108
+ * an optional per-repo override. Pure and dependency-free: it never spawns a
109
+ * subprocess and never imports from `start-tickets.ts`.
110
+ *
111
+ * Returns `null` (meaning "omit `--model`, use the agent default") when:
112
+ * - the agent does not support a model override,
113
+ * - no tier is provided,
114
+ * - the resolved candidate fails the alias-pattern validation, or
115
+ * - the agent has a `staticModelAliasAllowlist` and the candidate is not in it.
116
+ *
117
+ * A non-empty override for the selected tier takes precedence over the registry
118
+ * default. For agents without a static allowlist (e.g. cursor-agent), the caller
119
+ * is still responsible for live-validating the returned alias before injection.
120
+ */
121
+ export function resolveModelAlias(agent, tier, overrides) {
122
+ if (!agent.supportsModelOverride)
123
+ return null;
124
+ if (!tier)
125
+ return null;
126
+ const override = overrides?.[tier];
127
+ const candidate = typeof override === "string" && override.trim().length > 0
128
+ ? override.trim()
129
+ : agent.tierModels[tier];
130
+ if (typeof candidate !== "string" || !isValidModelAlias(candidate))
131
+ return null;
132
+ if (agent.staticModelAliasAllowlist && !agent.staticModelAliasAllowlist.includes(candidate)) {
133
+ return null;
134
+ }
135
+ return candidate;
136
+ }