@blxzer/cursor-trellis 0.2.6 → 0.2.8

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.
Files changed (98) hide show
  1. package/CHANGELOG.md +170 -103
  2. package/README.md +27 -4
  3. package/dist/cli/index.d.ts.map +1 -1
  4. package/dist/cli/index.js +18 -0
  5. package/dist/cli/index.js.map +1 -1
  6. package/dist/commands/init.d.ts.map +1 -1
  7. package/dist/commands/init.js +8 -1
  8. package/dist/commands/init.js.map +1 -1
  9. package/dist/commands/update.d.ts.map +1 -1
  10. package/dist/commands/update.js +6 -1
  11. package/dist/commands/update.js.map +1 -1
  12. package/dist/commands/validate-rules.d.ts +6 -0
  13. package/dist/commands/validate-rules.d.ts.map +1 -0
  14. package/dist/commands/validate-rules.js +33 -0
  15. package/dist/commands/validate-rules.js.map +1 -0
  16. package/dist/configurators/cursor2plus-local.d.ts.map +1 -1
  17. package/dist/configurators/cursor2plus-local.js +2 -1
  18. package/dist/configurators/cursor2plus-local.js.map +1 -1
  19. package/dist/configurators/workflow.d.ts.map +1 -1
  20. package/dist/configurators/workflow.js +8 -2
  21. package/dist/configurators/workflow.js.map +1 -1
  22. package/dist/migrations/manifests/0.2.2.json +9 -0
  23. package/dist/migrations/manifests/0.2.4.json +9 -0
  24. package/dist/migrations/manifests/0.2.5.json +9 -0
  25. package/dist/migrations/manifests/0.2.6.json +9 -0
  26. package/dist/migrations/manifests/0.2.7.json +9 -0
  27. package/dist/migrations/manifests/0.2.8.json +9 -0
  28. package/dist/templates/common/bundled-skills/trellis-cursor2plus-setup/SKILL.md +2 -2
  29. package/dist/templates/common/commands/finish-work.md +12 -0
  30. package/dist/templates/common/skills/brainstorm.md +164 -163
  31. package/dist/templates/common/skills/check.md +4 -0
  32. package/dist/templates/cursor/agents/trellis-check.md +32 -0
  33. package/dist/templates/cursor/agents/trellis-implement.md +11 -0
  34. package/dist/templates/cursor/agents/trellis-research.md +29 -0
  35. package/dist/templates/cursor/fixtures/expected-rules.d.ts +14 -0
  36. package/dist/templates/cursor/fixtures/expected-rules.d.ts.map +1 -0
  37. package/dist/templates/cursor/fixtures/expected-rules.js +26 -0
  38. package/dist/templates/cursor/fixtures/expected-rules.js.map +1 -0
  39. package/dist/templates/cursor/rules/trellis-subagent-dispatch.mdc +34 -0
  40. package/dist/templates/markdown/index.d.ts +1 -0
  41. package/dist/templates/markdown/index.d.ts.map +1 -1
  42. package/dist/templates/markdown/index.js +1 -0
  43. package/dist/templates/markdown/index.js.map +1 -1
  44. package/dist/templates/markdown/spec/guides/execution-strategy.md.txt +43 -0
  45. package/dist/templates/markdown/spec/guides/index.md.txt +102 -101
  46. package/dist/templates/trellis/config/execution-strategy-rules.json +31 -0
  47. package/dist/templates/trellis/index.d.ts +2 -0
  48. package/dist/templates/trellis/index.d.ts.map +1 -1
  49. package/dist/templates/trellis/index.js +3 -0
  50. package/dist/templates/trellis/index.js.map +1 -1
  51. package/dist/templates/trellis/local/README.md +61 -37
  52. package/dist/templates/trellis/local/index.d.ts +1 -0
  53. package/dist/templates/trellis/local/index.d.ts.map +1 -1
  54. package/dist/templates/trellis/local/index.js +1 -0
  55. package/dist/templates/trellis/local/index.js.map +1 -1
  56. package/dist/templates/trellis/local/patch_wpelc8.py +162 -38
  57. package/dist/templates/trellis/local/smoke.py +94 -0
  58. package/dist/templates/trellis/scripts/common/codebase_retrieval_router.py +2 -2
  59. package/dist/templates/trellis/scripts/common/cursor_retrieval_env.py +16 -2
  60. package/dist/templates/trellis/scripts/common/execution_strategy.py +268 -0
  61. package/dist/templates/trellis/scripts/common/retrieval_agent_instructions.py +26 -7
  62. package/dist/templates/trellis/scripts/common/semantic_plan_gate.py +18 -4
  63. package/dist/templates/trellis/scripts/common/task_store.py +1594 -1565
  64. package/dist/templates/trellis/scripts/task.py +918 -877
  65. package/dist/templates/trellis/workflow.md +805 -800
  66. package/dist/utils/codebase-retrieval-router.js +2 -2
  67. package/dist/utils/codebase-retrieval-router.js.map +1 -1
  68. package/dist/utils/cursor-retrieval-env.d.ts +2 -0
  69. package/dist/utils/cursor-retrieval-env.d.ts.map +1 -1
  70. package/dist/utils/cursor-retrieval-env.js +11 -2
  71. package/dist/utils/cursor-retrieval-env.js.map +1 -1
  72. package/dist/utils/mirror-check.d.ts +22 -0
  73. package/dist/utils/mirror-check.d.ts.map +1 -0
  74. package/dist/utils/mirror-check.js +90 -0
  75. package/dist/utils/mirror-check.js.map +1 -0
  76. package/dist/utils/normalize-text.d.ts +7 -0
  77. package/dist/utils/normalize-text.d.ts.map +1 -0
  78. package/dist/utils/normalize-text.js +16 -0
  79. package/dist/utils/normalize-text.js.map +1 -0
  80. package/dist/utils/retrieval-agent-instructions.d.ts.map +1 -1
  81. package/dist/utils/retrieval-agent-instructions.js +14 -7
  82. package/dist/utils/retrieval-agent-instructions.js.map +1 -1
  83. package/dist/utils/retrieval-execution-telemetry.d.ts +12 -3
  84. package/dist/utils/retrieval-execution-telemetry.d.ts.map +1 -1
  85. package/dist/utils/retrieval-execution-telemetry.js +37 -1
  86. package/dist/utils/retrieval-execution-telemetry.js.map +1 -1
  87. package/dist/utils/retrieval-tool-classification.d.ts +2 -0
  88. package/dist/utils/retrieval-tool-classification.d.ts.map +1 -1
  89. package/dist/utils/retrieval-tool-classification.js +13 -0
  90. package/dist/utils/retrieval-tool-classification.js.map +1 -1
  91. package/dist/utils/semantic-plan-gate.d.ts.map +1 -1
  92. package/dist/utils/semantic-plan-gate.js +11 -4
  93. package/dist/utils/semantic-plan-gate.js.map +1 -1
  94. package/dist/utils/validate-rules.d.ts +21 -0
  95. package/dist/utils/validate-rules.d.ts.map +1 -0
  96. package/dist/utils/validate-rules.js +88 -0
  97. package/dist/utils/validate-rules.js.map +1 -0
  98. package/package.json +3 -2
@@ -1,59 +1,83 @@
1
- # Cursor++ BYOK — Trellis subagent model routing
1
+ # Cursor++ Local Bundle
2
2
 
3
- **Who needs this:** Cursor IDE with [Cursor++](https://ccursor.cometix.dev) (BYOK proxy). **Native Cursor API users can ignore this directory.**
3
+ > **Native Cursor users:** safe to ignore or delete this directory unless using **Cursor++ BYOK**.
4
4
 
5
- ## Agent-led setup (recommended)
5
+ **Who needs this:** Cursor IDE with [Cursor++](https://ccursor.cometix.dev) (BYOK proxy). Method 2.5 patches the `WPeLc8` resolver so Trellis Task subagents get per-role BYOK slugs instead of inheriting the parent model.
6
6
 
7
- In **Cursor Agent**, run skill **`trellis-cursor2plus-setup`** (after `trellis init` with Cursor, or anytime `providers.json` changes). The agent lists models, writes commented **`~/.ccursor/trellis-task-models.json5`** with **primary** and **fallback** per role, runs `patch_wpelc8.py`, and reports WARN/ERROR from the resolver.
7
+ ## Bundle files
8
8
 
9
- Copy `../trellis-task-models.json5.example` only if you prefer a manual starting point.
9
+ | File | Purpose |
10
+ |------|---------|
11
+ | `patch_wpelc8.py` | Operator CLI: dry-run map, compat check, apply (with `--approve`), revert |
12
+ | `smoke.py` | Health smoke: env, patch status, configured roles (no secret reads) |
13
+ | `trellis_task_models_config.py` | JSON5 + providers catalog resolver (used by patch) |
14
+ | `config.local.json.example` | Optional paths (`extensionJs`, `ccursorHome`, …) |
15
+ | `config.local.json` | Your local paths (gitignored; never commit secrets) |
10
16
 
11
- ## What to put in `trellis-task-models.json5`
17
+ User-wide model doc: `~/.ccursor/trellis-task-models.json5` (see `../trellis-task-models.json5.example`). Project override: `../subagent-models.json` (optional).
12
18
 
13
- | JSON key (under `models`) | Meaning |
14
- |-----------------|--------|
15
- | `trellis-research` | Trellis research Task subagent |
16
- | `trellis-implement` | Trellis implement (+ Parent/Child default Task) |
17
- | `trellis-check` | Trellis check Task subagent |
18
- | `generalPurpose` / `shell` / `best-of-n-runner` | Cursor built-in Task types |
19
+ ## Compatibility window
19
20
 
20
- Each role: `{ "primary": "<name>", "fallback": "<name>" }`. **Names:** use **apiModel** or **displayName** from Cursor++ (not `model-xxxxx` unless you want to). Patch time resolves against `providers.json`; missing primary → WARN + fallback; ERROR only if both fail.
21
+ - **Verified:** Cursor **3.7.27** / Cursor++ **v0.0.11** (see `.trellis/spec/guides/cursor-subagent-policy.md`, 2026-06-18)
22
+ - **Re-verify** after any Cursor or Cursor++ upgrade: `python patch_wpelc8.py --check-compat`
23
+ - If `--check-compat` reports `fail` or `unknown`, treat the patch as stale until you re-apply or revert
21
24
 
22
- ```bash
23
- python patch_wpelc8.py --explain # key meanings
24
- python patch_wpelc8.py --list-models # current catalog from providers.json
25
- ```
26
-
27
- **Explore** is not in this file — use the Cursor++ panel.
25
+ ## Method 2.5 workflow
28
26
 
29
- ## When Cursor++ models change
27
+ 1. **Dry-run map** `python patch_wpelc8.py --print-map` (no file writes)
28
+ 2. **Operator approval** — review map and compat output; decide to proceed
29
+ 3. **Apply** — `python patch_wpelc8.py --apply --approve` (writes `extension.js`)
30
+ 4. **Reload Cursor** — Developer: Reload Window (once per apply/revert)
31
+ 5. **A ≠ B probe** — parent chat model must differ from a patched subagent map target; dispatch a `trellis-*` Task and confirm the subagent self-reports the **map target** slug/model, not the parent
32
+ 6. **Revert if needed** — `python patch_wpelc8.py --revert` then Reload Window
30
33
 
31
- `providers.json` is the live catalog. Slugs (`model-xxxxx`) can change when you add/remove models in Cursor++.
34
+ Agent-led setup: run skill **`trellis-cursor2plus-setup`** in Cursor Agent (lists models, writes JSON5, runs patch steps).
32
35
 
33
- 1. Re-run skill **`trellis-cursor2plus-setup`**, or `python patch_wpelc8.py --list-models` and fix JSON5
34
- 2. `python patch_wpelc8.py` → Reload Window only if you had not patched yet or reverted
36
+ ## Revert recipe
35
37
 
36
- You may **omit** keys for Task types you never use.
38
+ ```bash
39
+ cd .trellis/local/cursor2plus
40
+ python patch_wpelc8.py --revert
41
+ ```
37
42
 
38
- ## Two-layer JSON (project overrides user)
43
+ Then **Developer: Reload Window**. Subagents return to **inherit parent** BYOK model. Re-apply only after `--check-compat` is `ok` and you have explicit `--approve`.
39
44
 
40
- | File | Scope |
41
- |------|--------|
42
- | `~/.ccursor/trellis-task-models.json5` (or `.json`) | User — all repos |
43
- | `{repo}/.trellis/local/subagent-models.json` | Project overrides (optional) |
45
+ ## A B verification recipe
44
46
 
45
- ## Paths
47
+ Goal: prove the patch routes **subagent** model independently of the **parent** session model.
46
48
 
47
- `providers.json` default: `~/.ccursor/providers.json`. Override: `config.local.json` `providersJson`, or env `TRELLIS_CCURSOR_PROVIDERS`.
49
+ 1. Set parent chat to model **A** (any BYOK model in Cursor++).
50
+ 2. Ensure `trellis-task-models.json5` maps e.g. `trellis-implement` to model **B** (`primary`/`fallback` resolving to a different slug than A).
51
+ 3. `python patch_wpelc8.py --print-map` — note the slug for `trellis-implement` (= map target **B**).
52
+ 4. `python patch_wpelc8.py --apply --approve` → Reload Window.
53
+ 5. Dispatch a `trellis-implement` Task from a parent on model **A**.
54
+ 6. **Pass:** subagent self-report / trace shows **B** (map target), not **A**.
55
+ 7. **Fail:** subagent shows **A** → patch stale or not applied; run `--check-compat`, re-patch, or `--revert`.
48
56
 
49
- ## Apply / revert
57
+ ## CLI reference
50
58
 
51
59
  ```bash
52
- cd .trellis/local/cursor2plus
53
- python patch_wpelc8.py --print-map
54
- python patch_wpelc8.py
60
+ python patch_wpelc8.py --print-map # dry-run slug map
61
+ python patch_wpelc8.py --check-compat # WPeLc8 locatability (no writes)
62
+ python patch_wpelc8.py --apply --approve # write patch (requires --approve)
63
+ python patch_wpelc8.py --apply --dry-run # validate without write
64
+ python patch_wpelc8.py --revert # remove patch
65
+ python patch_wpelc8.py --explain # JSON5 key meanings
66
+ python patch_wpelc8.py --list-models # catalog from providers.json
67
+ python smoke.py # env + patch_status smoke
55
68
  ```
56
69
 
57
- **Developer: Reload Window** after patch.
70
+ `--apply` without `--approve` **refuses** and exits non-zero.
71
+
72
+ ## What to put in `trellis-task-models.json5`
73
+
74
+ | JSON key (under `models`) | Meaning |
75
+ |---------------------------|---------|
76
+ | `trellis-research` | Trellis research Task subagent |
77
+ | `trellis-implement` | Trellis implement (+ Parent/Child default Task) |
78
+ | `trellis-check` | Trellis check Task subagent |
79
+ | `generalPurpose` / `shell` / `best-of-n-runner` | Cursor built-in Task types |
80
+
81
+ Each role: `{ "primary": "<name>", "fallback": "<name>" }`. Names: **apiModel** or **displayName** from Cursor++ (`--list-models`). **Explore** is not in this file — use the Cursor++ panel.
58
82
 
59
- Policy: `.trellis/spec/guides/cursor-subagent-policy.md` (Method 2.5 / 2.6).
83
+ Policy: `.trellis/spec/guides/cursor-subagent-policy.md` (Method 2.5 / 2.6).
@@ -1,5 +1,6 @@
1
1
  export declare const cursor2plusPatchScript: string;
2
2
  export declare const cursor2plusReadme: string;
3
+ export declare const cursor2plusSmokeScript: string;
3
4
  export declare const cursor2plusConfigExample: string;
4
5
  export declare const subagentModelsExample: string;
5
6
  export declare const trellisTaskModelsJson5Example: string;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/templates/trellis/local/index.ts"],"names":[],"mappings":"AAUA,eAAO,MAAM,sBAAsB,QAA+B,CAAC;AACnE,eAAO,MAAM,iBAAiB,QAAyB,CAAC;AACxD,eAAO,MAAM,wBAAwB,QAAyC,CAAC;AAC/E,eAAO,MAAM,qBAAqB,QAA4C,CAAC;AAC/E,eAAO,MAAM,6BAA6B,QAEzC,CAAC;AACF,eAAO,MAAM,yBAAyB,QAA6C,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/templates/trellis/local/index.ts"],"names":[],"mappings":"AAUA,eAAO,MAAM,sBAAsB,QAA+B,CAAC;AACnE,eAAO,MAAM,iBAAiB,QAAyB,CAAC;AACxD,eAAO,MAAM,sBAAsB,QAAwB,CAAC;AAC5D,eAAO,MAAM,wBAAwB,QAAyC,CAAC;AAC/E,eAAO,MAAM,qBAAqB,QAA4C,CAAC;AAC/E,eAAO,MAAM,6BAA6B,QAEzC,CAAC;AACF,eAAO,MAAM,yBAAyB,QAA6C,CAAC"}
@@ -7,6 +7,7 @@ function readLocal(name) {
7
7
  }
8
8
  export const cursor2plusPatchScript = readLocal("patch_wpelc8.py");
9
9
  export const cursor2plusReadme = readLocal("README.md");
10
+ export const cursor2plusSmokeScript = readLocal("smoke.py");
10
11
  export const cursor2plusConfigExample = readLocal("config.local.json.example");
11
12
  export const subagentModelsExample = readLocal("subagent-models.json.example");
12
13
  export const trellisTaskModelsJson5Example = readLocal("trellis-task-models.json5.example");
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/templates/trellis/local/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,SAAS,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAE1D,SAAS,SAAS,CAAC,IAAY;IAC7B,OAAO,YAAY,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,EAAE,OAAO,CAAC,CAAC;AACtD,CAAC;AAED,MAAM,CAAC,MAAM,sBAAsB,GAAG,SAAS,CAAC,iBAAiB,CAAC,CAAC;AACnE,MAAM,CAAC,MAAM,iBAAiB,GAAG,SAAS,CAAC,WAAW,CAAC,CAAC;AACxD,MAAM,CAAC,MAAM,wBAAwB,GAAG,SAAS,CAAC,2BAA2B,CAAC,CAAC;AAC/E,MAAM,CAAC,MAAM,qBAAqB,GAAG,SAAS,CAAC,8BAA8B,CAAC,CAAC;AAC/E,MAAM,CAAC,MAAM,6BAA6B,GAAG,SAAS,CACpD,mCAAmC,CACpC,CAAC;AACF,MAAM,CAAC,MAAM,yBAAyB,GAAG,SAAS,CAAC,+BAA+B,CAAC,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/templates/trellis/local/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,SAAS,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAE1D,SAAS,SAAS,CAAC,IAAY;IAC7B,OAAO,YAAY,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,EAAE,OAAO,CAAC,CAAC;AACtD,CAAC;AAED,MAAM,CAAC,MAAM,sBAAsB,GAAG,SAAS,CAAC,iBAAiB,CAAC,CAAC;AACnE,MAAM,CAAC,MAAM,iBAAiB,GAAG,SAAS,CAAC,WAAW,CAAC,CAAC;AACxD,MAAM,CAAC,MAAM,sBAAsB,GAAG,SAAS,CAAC,UAAU,CAAC,CAAC;AAC5D,MAAM,CAAC,MAAM,wBAAwB,GAAG,SAAS,CAAC,2BAA2B,CAAC,CAAC;AAC/E,MAAM,CAAC,MAAM,qBAAqB,GAAG,SAAS,CAAC,8BAA8B,CAAC,CAAC;AAC/E,MAAM,CAAC,MAAM,6BAA6B,GAAG,SAAS,CACpD,mCAAmC,CACpC,CAAC;AACF,MAAM,CAAC,MAAM,yBAAyB,GAAG,SAAS,CAAC,+BAA+B,CAAC,CAAC"}
@@ -17,6 +17,7 @@ from trellis_task_models_config import (
17
17
  )
18
18
 
19
19
  MARKER = "/*TRELLIS_B2_WPeLc8*/"
20
+ INJECT_NEEDLE = "});return!UbAQWn||"
20
21
 
21
22
 
22
23
  def _repo_root(start: Path) -> Path:
@@ -111,28 +112,43 @@ def resolve_model_layers(bundle_dir: Path, config: dict, providers_path: Path) -
111
112
  return resolve_slots_to_slugs(merged, providers_path)
112
113
 
113
114
 
114
- def resolve_extension_js(bundle_dir: Path, config: dict) -> Path:
115
+ def configured_slot_map(bundle_dir: Path, config: dict) -> dict[str, list[str]]:
116
+ """Non-secret role → [primary, fallback, …] from JSON5 only (no providers.json read)."""
117
+ user_path = resolve_user_models_path(config)
118
+ project_path = resolve_project_models_path(bundle_dir, config)
119
+ user_slots = extract_raw_slots(load_task_models_document(user_path))
120
+ project_slots = (
121
+ extract_raw_slots(load_task_models_document(project_path))
122
+ if project_path
123
+ else {}
124
+ )
125
+ return _merge_slot_maps(user_slots, project_slots)
126
+
127
+
128
+ def _extension_candidates(config: dict) -> list[Path]:
115
129
  for key in ("extensionJs", "extension_js"):
116
130
  if isinstance(config.get(key), str) and config[key].strip():
117
- return Path(config[key].strip()).expanduser()
131
+ return [Path(config[key].strip()).expanduser()]
118
132
  env = os.environ.get("TRELLIS_CURSOR2PLUS_EXTENSION", "").strip()
119
133
  if env:
120
- return Path(env).expanduser()
134
+ return [Path(env).expanduser()]
121
135
  if sys.platform == "win32":
122
136
  local = os.environ.get("LOCALAPPDATA", "")
123
- candidates = [
137
+ return [
124
138
  Path(local) / "Programs" / "cursor" / "resources" / "app" / "extensions" / "cursor2plus" / "dist" / "extension.js",
125
139
  Path(r"D:\cursor\resources\app\extensions\cursor2plus\dist\extension.js"),
126
140
  ]
127
- elif sys.platform == "darwin":
128
- candidates = [
141
+ if sys.platform == "darwin":
142
+ return [
129
143
  Path("/Applications/Cursor.app/Contents/Resources/app/extensions/cursor2plus/dist/extension.js"),
130
144
  ]
131
- else:
132
- candidates = [
133
- Path.home() / ".local" / "share" / "cursor" / "resources" / "app" / "extensions" / "cursor2plus" / "dist" / "extension.js",
134
- ]
135
- for c in candidates:
145
+ return [
146
+ Path.home() / ".local" / "share" / "cursor" / "resources" / "app" / "extensions" / "cursor2plus" / "dist" / "extension.js",
147
+ ]
148
+
149
+
150
+ def resolve_extension_js(bundle_dir: Path, config: dict) -> Path:
151
+ for c in _extension_candidates(config):
136
152
  if c.is_file():
137
153
  return c
138
154
  raise SystemExit(
@@ -140,6 +156,13 @@ def resolve_extension_js(bundle_dir: Path, config: dict) -> Path:
140
156
  )
141
157
 
142
158
 
159
+ def try_resolve_extension_js(bundle_dir: Path, config: dict) -> Path | None:
160
+ for c in _extension_candidates(config):
161
+ if c.is_file():
162
+ return c
163
+ return None
164
+
165
+
143
166
  def build_inject_block(model_map: dict[str, str]) -> str:
144
167
  if not model_map:
145
168
  raise SystemExit(
@@ -164,15 +187,27 @@ def find_wpelc8_body(source: str) -> tuple[int, int]:
164
187
  return start, end
165
188
 
166
189
 
190
+ def probe_wpelc8_compat(source: str) -> dict[str, str]:
191
+ """Non-exiting WPeLc8 / inject-anchor probe for --check-compat and smoke."""
192
+ start = source.find("function WPeLc8(")
193
+ if start < 0:
194
+ return {"wpelc8": "not_locatable", "inject_anchor": "unknown", "end_anchor": "unknown"}
195
+ end = source.find("async function*o4ZNpa", start)
196
+ if end < 0:
197
+ return {"wpelc8": "locatable", "inject_anchor": "unknown", "end_anchor": "missing"}
198
+ fn = source[start:end]
199
+ anchor = "present" if INJECT_NEEDLE in fn else "missing"
200
+ return {"wpelc8": "locatable", "inject_anchor": anchor, "end_anchor": "present"}
201
+
202
+
167
203
  def apply_patch(source: str, inject: str) -> str:
168
204
  start, end = find_wpelc8_body(source)
169
205
  fn = source[start:end]
170
206
  if MARKER in fn:
171
207
  fn = remove_patch_from_fn(fn)
172
- needle = "});return!UbAQWn||"
173
- if needle not in fn:
174
- raise SystemExit(f"inject anchor missing: {needle!r}")
175
- fn_new = fn.replace(needle, f"}});{inject}return!UbAQWn||", 1)
208
+ if INJECT_NEEDLE not in fn:
209
+ raise SystemExit(f"inject anchor missing: {INJECT_NEEDLE!r}")
210
+ fn_new = fn.replace(INJECT_NEEDLE, f"}});{inject}return!UbAQWn||", 1)
176
211
  return source[:start] + fn_new + source[end:]
177
212
 
178
213
 
@@ -240,14 +275,80 @@ def cmd_bootstrap(bundle_dir: Path) -> None:
240
275
  print("Next: in Cursor Agent, run skill trellis-cursor2plus-setup", file=sys.stderr)
241
276
 
242
277
 
278
+ def cmd_check_compat(bundle_dir: Path, config: dict) -> int:
279
+ extension = try_resolve_extension_js(bundle_dir, config)
280
+ result: dict[str, object] = {
281
+ "extension": str(extension) if extension else None,
282
+ "patch_marker_present": False,
283
+ "wpelc8": "unknown",
284
+ "inject_anchor": "unknown",
285
+ "end_anchor": "unknown",
286
+ }
287
+ if extension is None:
288
+ result["status"] = "unknown"
289
+ result["hint"] = (
290
+ "extension.js not found — set extensionJs in config.local.json or run --bootstrap; "
291
+ "manual verification required"
292
+ )
293
+ print(json.dumps(result, indent=2, sort_keys=True))
294
+ return 2
295
+
296
+ source = extension.read_text(encoding="utf-8", errors="ignore")
297
+ result["patch_marker_present"] = MARKER in source
298
+ probe = probe_wpelc8_compat(source)
299
+ result.update(probe)
300
+
301
+ wpelc8 = probe["wpelc8"]
302
+ anchor = probe["inject_anchor"]
303
+ end_anchor = probe.get("end_anchor", "unknown")
304
+
305
+ if wpelc8 == "not_locatable" or anchor == "missing":
306
+ result["status"] = "fail"
307
+ result["hint"] = (
308
+ "WPeLc8 or inject anchor not found after Cursor/Cursor++ upgrade — "
309
+ "re-run patch with --apply --approve or revert to inherit-parent"
310
+ )
311
+ elif wpelc8 == "unknown" or anchor == "unknown" or end_anchor == "unknown":
312
+ result["status"] = "unknown"
313
+ result["hint"] = "Locator uncertain — manually verify extension.js after any upgrade"
314
+ else:
315
+ result["status"] = "ok"
316
+ result["hint"] = "WPeLc8 and inject anchor locatable"
317
+
318
+ print(json.dumps(result, indent=2, sort_keys=True))
319
+ return 0 if result["status"] == "ok" else (1 if result["status"] == "fail" else 2)
320
+
321
+
243
322
  def main() -> None:
244
- ap = argparse.ArgumentParser(description="Trellis Cursor++ WPeLc8 model map")
245
- ap.add_argument("--revert", action="store_true")
246
- ap.add_argument("--dry-run", action="store_true")
247
- ap.add_argument("--bootstrap", action="store_true")
248
- ap.add_argument("--explain", action="store_true")
249
- ap.add_argument("--list-models", action="store_true")
250
- ap.add_argument("--print-map", action="store_true")
323
+ ap = argparse.ArgumentParser(
324
+ description="Trellis Cursor++ WPeLc8 model map (Method 2.5)",
325
+ epilog=(
326
+ "Read-only: --print-map, --check-compat. "
327
+ "Write: --apply --approve (or --revert). "
328
+ "Never runs in CI by default."
329
+ ),
330
+ )
331
+ ap.add_argument("--revert", action="store_true", help="Remove Trellis patch from extension.js")
332
+ ap.add_argument("--apply", action="store_true", help="Apply patch to extension.js")
333
+ ap.add_argument(
334
+ "--approve",
335
+ action="store_true",
336
+ help="Explicit operator approval (required for --apply writes)",
337
+ )
338
+ ap.add_argument(
339
+ "--dry-run",
340
+ action="store_true",
341
+ help="With --apply: validate patch without writing extension.js",
342
+ )
343
+ ap.add_argument("--bootstrap", action="store_true", help="Create config.local.json with extension path")
344
+ ap.add_argument("--explain", action="store_true", help="Explain trellis-task-models.json5 keys")
345
+ ap.add_argument("--list-models", action="store_true", help="List models from providers.json catalog")
346
+ ap.add_argument("--print-map", action="store_true", help="Dry-run: print resolved slug map (no file writes)")
347
+ ap.add_argument(
348
+ "--check-compat",
349
+ action="store_true",
350
+ help="Check WPeLc8 locatability in current extension.js (no file writes)",
351
+ )
251
352
  args = ap.parse_args()
252
353
 
253
354
  bundle_dir = Path(__file__).resolve().parent
@@ -259,39 +360,62 @@ def main() -> None:
259
360
  return
260
361
 
261
362
  config = _load_config(bundle_dir)
363
+
364
+ if args.check_compat:
365
+ raise SystemExit(cmd_check_compat(bundle_dir, config))
366
+
262
367
  providers_path = resolve_providers_json(config)
263
368
 
264
369
  if args.list_models:
265
370
  cmd_list_models(providers_path)
266
371
  return
267
372
 
268
- model_map, notes = resolve_model_layers(bundle_dir, config, providers_path)
269
- if notes:
270
- print("resolve:", file=sys.stderr)
271
- for line in notes:
272
- print(line, file=sys.stderr)
273
- if any("ERROR" in line for line in notes):
274
- raise SystemExit(1)
373
+ needs_map = args.print_map or args.apply or args.revert
374
+ model_map: dict[str, str] = {}
375
+ notes: list[str] = []
376
+ if needs_map and not args.revert:
377
+ model_map, notes = resolve_model_layers(bundle_dir, config, providers_path)
378
+ if notes:
379
+ print("resolve:", file=sys.stderr)
380
+ for line in notes:
381
+ print(line, file=sys.stderr)
382
+ if any("ERROR" in line for line in notes):
383
+ raise SystemExit(1)
275
384
 
276
385
  if args.print_map:
277
386
  print(json.dumps(model_map, indent=2, sort_keys=True))
278
387
  return
279
388
 
280
- extension = resolve_extension_js(bundle_dir, config)
281
- source = extension.read_text(encoding="utf-8", errors="ignore")
282
389
  if args.revert:
390
+ extension = resolve_extension_js(bundle_dir, config)
391
+ source = extension.read_text(encoding="utf-8", errors="ignore")
283
392
  new = remove_patch(source)
284
- else:
393
+ extension.write_text(new, encoding="utf-8", newline="\n")
394
+ print("wrote", extension)
395
+ return
396
+
397
+ if args.apply:
398
+ if not args.approve and not args.dry_run:
399
+ print(
400
+ "refused: --apply writes extension.js and requires explicit --approve "
401
+ "(use --dry-run to validate without writing)",
402
+ file=sys.stderr,
403
+ )
404
+ raise SystemExit(1)
405
+ extension = resolve_extension_js(bundle_dir, config)
406
+ source = extension.read_text(encoding="utf-8", errors="ignore")
285
407
  inject = build_inject_block(model_map)
286
408
  new = apply_patch(source, inject)
287
-
288
- if args.dry_run:
289
- print("ok", "revert" if args.revert else "patch", "delta", len(new) - len(source))
409
+ if args.dry_run:
410
+ print("ok patch delta", len(new) - len(source))
411
+ return
412
+ extension.write_text(new, encoding="utf-8", newline="\n")
413
+ print("wrote", extension)
290
414
  return
291
415
 
292
- extension.write_text(new, encoding="utf-8", newline="\n")
293
- print("wrote", extension)
416
+ ap.print_help()
417
+ raise SystemExit(2)
294
418
 
295
419
 
296
420
  if __name__ == "__main__":
297
- main()
421
+ main()
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env python3
2
+ """Cursor++ local bundle smoke check — whitelist metadata only, no secret reads."""
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ from patch_wpelc8 import (
11
+ MARKER,
12
+ _load_config,
13
+ configured_slot_map,
14
+ probe_wpelc8_compat,
15
+ try_resolve_extension_js,
16
+ )
17
+
18
+ CAVEATS = [
19
+ "Cursor++ is third-party; reverse-engineering is version-bound",
20
+ "Never reads BYOK provider catalog file contents",
21
+ "Patch is operator-only; not part of CI or default Trellis workflow",
22
+ "Re-verify after any Cursor or Cursor++ upgrade",
23
+ ]
24
+
25
+
26
+ def detect_env() -> tuple[str, str | None]:
27
+ env_flag = os.environ.get("TRELLIS_CURSOR_BYOK", "").strip()
28
+ if env_flag == "1":
29
+ return "byok", "TRELLIS_CURSOR_BYOK=1"
30
+ if env_flag == "0":
31
+ return "native", "TRELLIS_CURSOR_BYOK=0"
32
+
33
+ routes = Path.home() / ".ccursor" / "routes.json"
34
+ if routes.is_file():
35
+ try:
36
+ data = json.loads(routes.read_text(encoding="utf-8"))
37
+ byok = data.get("byokMode")
38
+ if byok in (1, "1", True):
39
+ return "byok", "routes.json:byokMode"
40
+ if byok in (0, "0", False):
41
+ return "native", "routes.json:byokMode"
42
+ except (OSError, json.JSONDecodeError, TypeError):
43
+ pass
44
+ return "unknown", None
45
+
46
+
47
+ def detect_patch_status(bundle_dir: Path, config: dict) -> tuple[str, dict[str, object]]:
48
+ extension = try_resolve_extension_js(bundle_dir, config)
49
+ meta: dict[str, object] = {"extension": str(extension) if extension else None}
50
+ if extension is None:
51
+ return "not-applied", meta
52
+
53
+ source = extension.read_text(encoding="utf-8", errors="ignore")
54
+ marker_present = MARKER in source
55
+ meta["patch_marker_present"] = marker_present
56
+ probe = probe_wpelc8_compat(source)
57
+ meta.update(probe)
58
+
59
+ if not marker_present:
60
+ return "not-applied", meta
61
+
62
+ if probe["wpelc8"] == "not_locatable" or probe["inject_anchor"] == "missing":
63
+ return "stale", meta
64
+ if probe["wpelc8"] == "unknown" or probe["inject_anchor"] == "unknown":
65
+ return "stale", meta
66
+ return "applied", meta
67
+
68
+
69
+ def main() -> int:
70
+ bundle_dir = Path(__file__).resolve().parent
71
+ config = _load_config(bundle_dir)
72
+ env, env_source = detect_env()
73
+ patch_status, patch_meta = detect_patch_status(bundle_dir, config)
74
+ slots = configured_slot_map(bundle_dir, config)
75
+
76
+ report = {
77
+ "env": env,
78
+ "env_source": env_source,
79
+ "patch_status": patch_status,
80
+ "patch_meta": patch_meta,
81
+ "current_map": {
82
+ "configured_roles": {
83
+ role: {"candidates": names} for role, names in sorted(slots.items())
84
+ },
85
+ "note": "Role names from user JSON5 config only; slugs not resolved (no provider catalog read)",
86
+ },
87
+ "caveats": CAVEATS,
88
+ }
89
+ print(json.dumps(report, indent=2, sort_keys=True))
90
+ return 0
91
+
92
+
93
+ if __name__ == "__main__":
94
+ raise SystemExit(main())
@@ -14,8 +14,8 @@ from pathlib import Path
14
14
  from typing import Any
15
15
 
16
16
  from .cursor_retrieval_env import (
17
- ENV_BYOK,
18
17
  detect_cursor_retrieval_env,
18
+ is_byok_conservative,
19
19
  semantic_route_spec,
20
20
  )
21
21
 
@@ -716,7 +716,7 @@ def _fallback_hints(
716
716
  "replacesRole": "semantic",
717
717
  })
718
718
  cenv = cursor_env or detect_cursor_retrieval_env()
719
- if cenv == ENV_BYOK:
719
+ if is_byok_conservative(cenv):
720
720
  hints.append({
721
721
  "when": "built-in @codebase / SemanticSearch not in agent tool list",
722
722
  "action": (
@@ -75,12 +75,26 @@ def is_byok(env: CursorRetrievalEnv | None = None) -> bool:
75
75
  return (env or detect_cursor_retrieval_env()) == ENV_BYOK
76
76
 
77
77
 
78
+ def is_byok_conservative(env: CursorRetrievalEnv | None = None) -> bool:
79
+ """Conservative semantic routing: BYOK plus unknown (no routes.json / ambiguous byokMode)."""
80
+ resolved = env or detect_cursor_retrieval_env()
81
+ return resolved == ENV_BYOK or resolved == ENV_UNKNOWN
82
+
83
+
78
84
  def semantic_route_spec(cursor_env: CursorRetrievalEnv) -> dict[str, object]:
79
85
  """platform-semantic route fields for conceptual / optional semantic slots."""
80
- if cursor_env == ENV_BYOK:
86
+ if is_byok_conservative(cursor_env):
87
+ unknown_caveat = (
88
+ " Unknown cursorEnv: conservative fast-context primary (same as BYOK)."
89
+ if cursor_env == ENV_UNKNOWN
90
+ else ""
91
+ )
81
92
  return {
82
93
  "commands": ["fast_context_search (fast-context MCP)"],
83
- "rationale_suffix": " Cursor++ BYOK: built-in semantic unavailable; fast-context MCP primary.",
94
+ "rationale_suffix": (
95
+ " Cursor++ BYOK: built-in semantic unavailable; fast-context MCP primary."
96
+ + unknown_caveat
97
+ ),
84
98
  "platformNative": False,
85
99
  "semanticBackend": "fast-context-mcp",
86
100
  }