@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.
- package/CHANGELOG.md +170 -103
- package/README.md +27 -4
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +18 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +8 -1
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +6 -1
- package/dist/commands/update.js.map +1 -1
- package/dist/commands/validate-rules.d.ts +6 -0
- package/dist/commands/validate-rules.d.ts.map +1 -0
- package/dist/commands/validate-rules.js +33 -0
- package/dist/commands/validate-rules.js.map +1 -0
- package/dist/configurators/cursor2plus-local.d.ts.map +1 -1
- package/dist/configurators/cursor2plus-local.js +2 -1
- package/dist/configurators/cursor2plus-local.js.map +1 -1
- package/dist/configurators/workflow.d.ts.map +1 -1
- package/dist/configurators/workflow.js +8 -2
- package/dist/configurators/workflow.js.map +1 -1
- package/dist/migrations/manifests/0.2.2.json +9 -0
- package/dist/migrations/manifests/0.2.4.json +9 -0
- package/dist/migrations/manifests/0.2.5.json +9 -0
- package/dist/migrations/manifests/0.2.6.json +9 -0
- package/dist/migrations/manifests/0.2.7.json +9 -0
- package/dist/migrations/manifests/0.2.8.json +9 -0
- package/dist/templates/common/bundled-skills/trellis-cursor2plus-setup/SKILL.md +2 -2
- package/dist/templates/common/commands/finish-work.md +12 -0
- package/dist/templates/common/skills/brainstorm.md +164 -163
- package/dist/templates/common/skills/check.md +4 -0
- package/dist/templates/cursor/agents/trellis-check.md +32 -0
- package/dist/templates/cursor/agents/trellis-implement.md +11 -0
- package/dist/templates/cursor/agents/trellis-research.md +29 -0
- package/dist/templates/cursor/fixtures/expected-rules.d.ts +14 -0
- package/dist/templates/cursor/fixtures/expected-rules.d.ts.map +1 -0
- package/dist/templates/cursor/fixtures/expected-rules.js +26 -0
- package/dist/templates/cursor/fixtures/expected-rules.js.map +1 -0
- package/dist/templates/cursor/rules/trellis-subagent-dispatch.mdc +34 -0
- package/dist/templates/markdown/index.d.ts +1 -0
- package/dist/templates/markdown/index.d.ts.map +1 -1
- package/dist/templates/markdown/index.js +1 -0
- package/dist/templates/markdown/index.js.map +1 -1
- package/dist/templates/markdown/spec/guides/execution-strategy.md.txt +43 -0
- package/dist/templates/markdown/spec/guides/index.md.txt +102 -101
- package/dist/templates/trellis/config/execution-strategy-rules.json +31 -0
- package/dist/templates/trellis/index.d.ts +2 -0
- package/dist/templates/trellis/index.d.ts.map +1 -1
- package/dist/templates/trellis/index.js +3 -0
- package/dist/templates/trellis/index.js.map +1 -1
- package/dist/templates/trellis/local/README.md +61 -37
- package/dist/templates/trellis/local/index.d.ts +1 -0
- package/dist/templates/trellis/local/index.d.ts.map +1 -1
- package/dist/templates/trellis/local/index.js +1 -0
- package/dist/templates/trellis/local/index.js.map +1 -1
- package/dist/templates/trellis/local/patch_wpelc8.py +162 -38
- package/dist/templates/trellis/local/smoke.py +94 -0
- package/dist/templates/trellis/scripts/common/codebase_retrieval_router.py +2 -2
- package/dist/templates/trellis/scripts/common/cursor_retrieval_env.py +16 -2
- package/dist/templates/trellis/scripts/common/execution_strategy.py +268 -0
- package/dist/templates/trellis/scripts/common/retrieval_agent_instructions.py +26 -7
- package/dist/templates/trellis/scripts/common/semantic_plan_gate.py +18 -4
- package/dist/templates/trellis/scripts/common/task_store.py +1594 -1565
- package/dist/templates/trellis/scripts/task.py +918 -877
- package/dist/templates/trellis/workflow.md +805 -800
- package/dist/utils/codebase-retrieval-router.js +2 -2
- package/dist/utils/codebase-retrieval-router.js.map +1 -1
- package/dist/utils/cursor-retrieval-env.d.ts +2 -0
- package/dist/utils/cursor-retrieval-env.d.ts.map +1 -1
- package/dist/utils/cursor-retrieval-env.js +11 -2
- package/dist/utils/cursor-retrieval-env.js.map +1 -1
- package/dist/utils/mirror-check.d.ts +22 -0
- package/dist/utils/mirror-check.d.ts.map +1 -0
- package/dist/utils/mirror-check.js +90 -0
- package/dist/utils/mirror-check.js.map +1 -0
- package/dist/utils/normalize-text.d.ts +7 -0
- package/dist/utils/normalize-text.d.ts.map +1 -0
- package/dist/utils/normalize-text.js +16 -0
- package/dist/utils/normalize-text.js.map +1 -0
- package/dist/utils/retrieval-agent-instructions.d.ts.map +1 -1
- package/dist/utils/retrieval-agent-instructions.js +14 -7
- package/dist/utils/retrieval-agent-instructions.js.map +1 -1
- package/dist/utils/retrieval-execution-telemetry.d.ts +12 -3
- package/dist/utils/retrieval-execution-telemetry.d.ts.map +1 -1
- package/dist/utils/retrieval-execution-telemetry.js +37 -1
- package/dist/utils/retrieval-execution-telemetry.js.map +1 -1
- package/dist/utils/retrieval-tool-classification.d.ts +2 -0
- package/dist/utils/retrieval-tool-classification.d.ts.map +1 -1
- package/dist/utils/retrieval-tool-classification.js +13 -0
- package/dist/utils/retrieval-tool-classification.js.map +1 -1
- package/dist/utils/semantic-plan-gate.d.ts.map +1 -1
- package/dist/utils/semantic-plan-gate.js +11 -4
- package/dist/utils/semantic-plan-gate.js.map +1 -1
- package/dist/utils/validate-rules.d.ts +21 -0
- package/dist/utils/validate-rules.d.ts.map +1 -0
- package/dist/utils/validate-rules.js +88 -0
- package/dist/utils/validate-rules.js.map +1 -0
- 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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
"
|
|
67
|
+
f"{label} 本问 `platform-semantic` 为第 "
|
|
54
68
|
f"{order} 步。定 Top-1 前 **必须** 执行 1 次 **fast_context_search**(fast-context MCP);"
|
|
55
69
|
"**禁止** 用 WebSearch 答代码库问题。命中路径后须 **Read** 验证。"
|
|
56
70
|
)
|