@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
@@ -0,0 +1,268 @@
1
+ #!/usr/bin/env python3
2
+ """Suggest Development Strategy Contract execution_mode and isolation from task signals."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import json
7
+ from dataclasses import asdict, dataclass, field
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from .git import run_git
12
+ from .task_gates import read_strategy_contract, task_closeout_profile
13
+
14
+ _RULES_CACHE: dict | None = None
15
+
16
+
17
+ @dataclass
18
+ class ExecutionStrategySuggestion:
19
+ skipped: bool = False
20
+ profile: str = ""
21
+ execution_mode: str = ""
22
+ isolation: str = ""
23
+ reasons: list[str] = field(default_factory=list)
24
+ warnings: list[str] = field(default_factory=list)
25
+ signals: dict[str, Any] = field(default_factory=dict)
26
+
27
+ def to_dict(self) -> dict[str, Any]:
28
+ return asdict(self)
29
+
30
+
31
+ def _rules_path() -> Path:
32
+ return Path(__file__).resolve().parent.parent.parent / "config" / "execution-strategy-rules.json"
33
+
34
+
35
+ def load_execution_strategy_rules() -> dict:
36
+ global _RULES_CACHE
37
+ if _RULES_CACHE is not None:
38
+ return _RULES_CACHE
39
+ path = _rules_path()
40
+ if not path.is_file():
41
+ _RULES_CACHE = {
42
+ "version": 1,
43
+ "doc_only_capabilities": ["markdown-documentation"],
44
+ "code_capabilities": [],
45
+ "code_path_segments": ["src", "packages", "scripts"],
46
+ "pair_warnings": [],
47
+ }
48
+ return _RULES_CACHE
49
+ _RULES_CACHE = json.loads(path.read_text(encoding="utf-8"))
50
+ return _RULES_CACHE
51
+
52
+
53
+ def resolve_git_package_root(repo_root: Path, task_data: dict) -> Path | None:
54
+ """Return a directory inside a git worktree suitable for prepare-child-worktree."""
55
+ candidates: list[Path] = []
56
+ package = task_data.get("package")
57
+ if isinstance(package, str) and package.strip():
58
+ candidates.append(repo_root / package.strip().replace("\\", "/"))
59
+ scope = task_data.get("scope")
60
+ if isinstance(scope, str) and scope.strip():
61
+ first = scope.strip().replace("\\", "/").split("/")[0]
62
+ if first:
63
+ candidates.append(repo_root / first)
64
+ candidates.append(repo_root)
65
+
66
+ seen: set[str] = set()
67
+ for candidate in candidates:
68
+ key = str(candidate.resolve()) if candidate.exists() else str(candidate)
69
+ if key in seen:
70
+ continue
71
+ seen.add(key)
72
+ if not candidate.is_dir():
73
+ continue
74
+ rc, out, _ = run_git(["rev-parse", "--is-inside-work-tree"], cwd=candidate)
75
+ if rc == 0 and out.strip().lower() == "true":
76
+ return candidate
77
+ return None
78
+
79
+
80
+ def _path_hits_code_segment(repo_root: Path, rel: str, segments: list[str]) -> bool:
81
+ parts = [p for p in rel.replace("\\", "/").split("/") if p]
82
+ return any(part in segments for part in parts)
83
+
84
+
85
+ def _capabilities_list(contract: dict) -> list[str]:
86
+ raw = contract.get("optional_capabilities")
87
+ if not isinstance(raw, list):
88
+ return []
89
+ return [str(item).strip() for item in raw if isinstance(item, str) and item.strip()]
90
+
91
+
92
+ def collect_signals(repo_root: Path, task_dir: Path, task_data: dict) -> dict[str, Any]:
93
+ rules = load_execution_strategy_rules()
94
+ profile = task_closeout_profile(task_dir, task_data)
95
+ contract, _ = read_strategy_contract(task_dir)
96
+ caps = _capabilities_list(contract)
97
+ doc_only_set = set(rules.get("doc_only_capabilities") or [])
98
+ code_cap_set = set(rules.get("code_capabilities") or [])
99
+ segments = list(rules.get("code_path_segments") or [])
100
+
101
+ has_parent = bool(task_data.get("parent"))
102
+ children = task_data.get("children")
103
+ has_children = isinstance(children, list) and any(isinstance(c, str) for c in children)
104
+
105
+ touches_code = False
106
+ if caps and any(c in code_cap_set for c in caps):
107
+ touches_code = True
108
+ package = task_data.get("package")
109
+ if isinstance(package, str) and package.strip():
110
+ if _path_hits_code_segment(repo_root, package.strip(), segments):
111
+ touches_code = True
112
+ scope = task_data.get("scope")
113
+ if isinstance(scope, str) and scope.strip():
114
+ if _path_hits_code_segment(repo_root, scope.strip(), segments):
115
+ touches_code = True
116
+ if (
117
+ profile == "full"
118
+ and (task_dir / "design.md").is_file()
119
+ and caps
120
+ and not all(c in doc_only_set for c in caps)
121
+ ):
122
+ touches_code = True
123
+ if profile == "full" and (task_dir / "design.md").is_file() and not caps:
124
+ touches_code = True
125
+
126
+ doc_only = bool(caps) and all(c in doc_only_set for c in caps)
127
+ if profile == "full" and caps and doc_only and not touches_code:
128
+ doc_only = True
129
+
130
+ git_root = resolve_git_package_root(repo_root, task_data)
131
+ return {
132
+ "profile": profile,
133
+ "has_parent": has_parent,
134
+ "has_children": has_children,
135
+ "touches_code": touches_code,
136
+ "doc_only": doc_only,
137
+ "git_package_root": git_root.as_posix() if git_root else None,
138
+ "package": package if isinstance(package, str) else None,
139
+ "scope": scope if isinstance(scope, str) else None,
140
+ "optional_capabilities": caps,
141
+ }
142
+
143
+
144
+ def validate_strategy_pair(execution_mode: str, isolation: str) -> list[str]:
145
+ rules = load_execution_strategy_rules()
146
+ warnings: list[str] = []
147
+ for entry in rules.get("pair_warnings") or []:
148
+ if not isinstance(entry, dict):
149
+ continue
150
+ if (
151
+ entry.get("execution_mode") == execution_mode
152
+ and entry.get("isolation") == isolation
153
+ ):
154
+ msg = entry.get("message")
155
+ if isinstance(msg, str) and msg.strip():
156
+ warnings.append(msg.strip())
157
+ return warnings
158
+
159
+
160
+ def suggest_execution_strategy(
161
+ repo_root: Path,
162
+ task_dir: Path,
163
+ task_data: dict,
164
+ ) -> ExecutionStrategySuggestion:
165
+ signals = collect_signals(repo_root, task_dir, task_data)
166
+ profile = signals["profile"]
167
+ if profile == "lite":
168
+ return ExecutionStrategySuggestion(
169
+ skipped=True,
170
+ profile=profile,
171
+ reasons=["Lite task: no full Development Strategy Contract required"],
172
+ signals=signals,
173
+ )
174
+
175
+ reasons: list[str] = []
176
+ warnings: list[str] = []
177
+
178
+ if signals["has_parent"]:
179
+ mode = "child-task"
180
+ if signals["git_package_root"]:
181
+ iso = "git-worktree"
182
+ reasons.append("Child task with git package root → git-worktree isolation")
183
+ else:
184
+ iso = "main-worktree"
185
+ reasons.append("Child task without resolvable git root → main-worktree (degraded)")
186
+ warnings.append("No git package root; child git-worktree not recommended")
187
+ elif signals["has_children"] or profile == "parent":
188
+ mode = "inline"
189
+ iso = "main-worktree"
190
+ reasons.append("Parent task orchestrates children in main session")
191
+ elif profile == "full" and signals["doc_only"] and not signals["touches_code"]:
192
+ mode = "inline"
193
+ iso = "main-worktree"
194
+ reasons.append("Full task with documentation-only capabilities")
195
+ elif profile == "full":
196
+ mode = "worker"
197
+ iso = "main-worktree"
198
+ reasons.append("Full task touching code → worker (trellis-implement / check sub-agents)")
199
+ else:
200
+ mode = "inline"
201
+ iso = "main-worktree"
202
+ reasons.append(f"Default for profile={profile}")
203
+
204
+ warnings.extend(validate_strategy_pair(mode, iso))
205
+ return ExecutionStrategySuggestion(
206
+ profile=profile,
207
+ execution_mode=mode,
208
+ isolation=iso,
209
+ reasons=reasons,
210
+ warnings=warnings,
211
+ signals=signals,
212
+ )
213
+
214
+
215
+ def format_contract_yaml_block(suggestion: ExecutionStrategySuggestion) -> str:
216
+ if suggestion.skipped:
217
+ return "# Lite task: Development Strategy Contract not required\n"
218
+ lines = [
219
+ "## Development Strategy Contract (suggested — paste into implement.md)",
220
+ "",
221
+ "```yaml",
222
+ f"execution_mode: {suggestion.execution_mode}",
223
+ f"isolation: {suggestion.isolation}",
224
+ "verification_profile: standard",
225
+ "retrieval_profile: structure",
226
+ "optional_capabilities: []",
227
+ "quality_gates:",
228
+ " mode: profile",
229
+ " profile: standard",
230
+ " enabled: []",
231
+ " disabled: []",
232
+ "```",
233
+ "",
234
+ ]
235
+ if suggestion.reasons:
236
+ lines.append("Reasons:")
237
+ for item in suggestion.reasons:
238
+ lines.append(f"- {item}")
239
+ if suggestion.warnings:
240
+ lines.append("")
241
+ lines.append("Warnings:")
242
+ for item in suggestion.warnings:
243
+ lines.append(f"- {item}")
244
+ lines.append("")
245
+ return "\n".join(lines)
246
+
247
+
248
+ def contract_drift_warnings(
249
+ repo_root: Path,
250
+ task_dir: Path,
251
+ task_data: dict,
252
+ contract: dict,
253
+ ) -> list[str]:
254
+ """Compare approved contract to suggestion; advisory only."""
255
+ if not contract:
256
+ return []
257
+ suggestion = suggest_execution_strategy(repo_root, task_dir, task_data)
258
+ if suggestion.skipped:
259
+ return []
260
+ warnings: list[str] = []
261
+ for key in ("execution_mode", "isolation"):
262
+ approved = contract.get(key)
263
+ suggested = getattr(suggestion, key, "")
264
+ if approved and suggested and approved != suggested:
265
+ warnings.append(
266
+ f"contract {key}={approved!r} differs from suggestion {suggested!r}"
267
+ )
268
+ return warnings
@@ -12,7 +12,11 @@ from __future__ import annotations
12
12
  import re
13
13
  from typing import Any
14
14
 
15
- from .cursor_retrieval_env import ENV_BYOK, detect_cursor_retrieval_env
15
+ from .cursor_retrieval_env import (
16
+ ENV_UNKNOWN,
17
+ detect_cursor_retrieval_env,
18
+ is_byok_conservative,
19
+ )
16
20
 
17
21
  _SYMBOL_CANDIDATE = re.compile(
18
22
  r"\b([A-Za-z_][\w$]{2,})\b|"
@@ -149,10 +153,16 @@ def _cursor_step_for_route(
149
153
  role == "semantic" and str(route.get("sourceFamily")) == "platform-semantic"
150
154
  ):
151
155
  backend = str(route.get("semanticBackend", ""))
152
- if cursor_env == ENV_BYOK or backend == "fast-context-mcp":
156
+ if is_byok_conservative(cursor_env) or backend == "fast-context-mcp":
157
+ unknown_note = (
158
+ "(cursorEnv 未知,保守走 fast-context,同 BYOK)。"
159
+ if cursor_env == ENV_UNKNOWN
160
+ else ""
161
+ )
153
162
  return (
154
163
  "使用 **fast_context_search**(fast-context MCP)做概念/代码库语义检索;"
155
164
  "**不要**假设 @codebase 或内置 SemanticSearch 可用;**不要**用 WebSearch 答代码库问题。"
165
+ + unknown_note
156
166
  )
157
167
  return (
158
168
  "使用 Cursor **内置代码库语义搜索**(宿主工具常为 **SemanticSearch**;"
@@ -161,7 +171,7 @@ def _cursor_step_for_route(
161
171
  "无路径线索的概念题优先 **`[]`**;Native 下 **不要**用 fast-context MCP 顶替)。"
162
172
  )
163
173
  if role == "semantic":
164
- if cursor_env == ENV_BYOK:
174
+ if is_byok_conservative(cursor_env):
165
175
  return (
166
176
  "使用 **fast_context_search**(fast-context MCP);勿用 WebSearch 答代码库问题。"
167
177
  )
@@ -181,7 +191,7 @@ def _cursor_step_for_route(
181
191
  )
182
192
 
183
193
  if route_id == "cross-cutting-discovery" or "deep" in route_id.lower():
184
- if cursor_env == ENV_BYOK:
194
+ if is_byok_conservative(cursor_env):
185
195
  return (
186
196
  "复杂跨模块探索:使用 **Task** 子代理(explore),再用 Grep/codegraph/Read 验证。"
187
197
  )
@@ -292,15 +302,24 @@ def render_agent_instructions(
292
302
  isinstance(r, dict) and r.get("id") == "platform-semantic" for r in route_list
293
303
  ):
294
304
  lines.append("")
295
- if cursor_env == ENV_BYOK:
305
+ if is_byok_conservative(cursor_env):
306
+ label = (
307
+ "**语义合规(cursorEnv 未知,保守 BYOK):**"
308
+ if cursor_env == ENV_UNKNOWN
309
+ else "**语义合规(Cursor++ BYOK):**"
310
+ )
296
311
  if locale != "zh":
297
- lines.append("**Semantic compliance (Cursor++ BYOK):**")
312
+ lines.append(
313
+ "**Semantic compliance (Cursor++ BYOK):**"
314
+ if cursor_env != ENV_UNKNOWN
315
+ else "**Semantic compliance (cursorEnv unknown, conservative BYOK):**"
316
+ )
298
317
  lines.append(
299
318
  "- Run **one** `fast_context_search` before final Top-1 when "
300
319
  "**platform-semantic** is listed; log the MCP tool name."
301
320
  )
302
321
  else:
303
- lines.append("**语义合规(Cursor++ BYOK):**")
322
+ lines.append(label)
304
323
  lines.append(
305
324
  "- 本计划含 **platform-semantic** 时:定 Top-1 前至少 **1 次** "
306
325
  "**fast_context_search**(fast-context MCP),并记录工具名。"
@@ -4,7 +4,11 @@ from __future__ import annotations
4
4
 
5
5
  from typing import Any
6
6
 
7
- from .cursor_retrieval_env import ENV_BYOK, detect_cursor_retrieval_env
7
+ from .cursor_retrieval_env import (
8
+ ENV_UNKNOWN,
9
+ detect_cursor_retrieval_env,
10
+ is_byok_conservative,
11
+ )
8
12
 
9
13
 
10
14
  def platform_semantic_order_from_envelope(envelope: dict[str, Any]) -> int | None:
@@ -41,16 +45,26 @@ def semantic_compliance_gate_hint(
41
45
 
42
46
  cursor_env = str(envelope.get("cursorEnv") or detect_cursor_retrieval_env())
43
47
 
44
- if cursor_env == ENV_BYOK:
48
+ if is_byok_conservative(cursor_env):
49
+ unknown_note = (
50
+ " Unknown cursorEnv — conservative BYOK gate."
51
+ if cursor_env == ENV_UNKNOWN
52
+ else ""
53
+ )
45
54
  if locale != "zh":
46
55
  return (
47
56
  "**Plan gate (REC-11, BYOK):** `platform-semantic` is step "
48
57
  f"#{order}. Before Top-1 you MUST run one **fast_context_search** "
49
58
  "(fast-context MCP). Do not use WebSearch for codebase questions. "
50
- "Corroborate hits with Read."
59
+ "Corroborate hits with Read." + unknown_note
51
60
  )
61
+ label = (
62
+ "**计划门控(REC-11,未知 env 保守 BYOK):**"
63
+ if cursor_env == ENV_UNKNOWN
64
+ else "**计划门控(REC-11,BYOK):**"
65
+ )
52
66
  return (
53
- "**计划门控(REC-11,BYOK):** 本问 `platform-semantic` 为第 "
67
+ f"{label} 本问 `platform-semantic` 为第 "
54
68
  f"{order} 步。定 Top-1 前 **必须** 执行 1 次 **fast_context_search**(fast-context MCP);"
55
69
  "**禁止** 用 WebSearch 答代码库问题。命中路径后须 **Read** 验证。"
56
70
  )