@blxzer/cursor-trellis 0.3.4 → 0.3.6

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 (210) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +13 -13
  3. package/README.zh-CN.md +1 -1
  4. package/bin/cstl.js +1 -1
  5. package/bin/smart-search.js +1 -1
  6. package/dist/cli/index.d.ts.map +1 -1
  7. package/dist/cli/index.js +17 -2
  8. package/dist/cli/index.js.map +1 -1
  9. package/dist/commands/campaign/canvas-render.d.ts +68 -0
  10. package/dist/commands/campaign/canvas-render.d.ts.map +1 -0
  11. package/dist/commands/campaign/canvas-render.js +414 -0
  12. package/dist/commands/campaign/canvas-render.js.map +1 -0
  13. package/dist/commands/campaign/compose.d.ts +15 -0
  14. package/dist/commands/campaign/compose.d.ts.map +1 -0
  15. package/dist/commands/campaign/compose.js +110 -0
  16. package/dist/commands/campaign/compose.js.map +1 -0
  17. package/dist/commands/campaign/index.d.ts +13 -0
  18. package/dist/commands/campaign/index.d.ts.map +1 -0
  19. package/dist/commands/campaign/index.js +134 -0
  20. package/dist/commands/campaign/index.js.map +1 -0
  21. package/dist/commands/campaign/kind-map.d.ts +6 -0
  22. package/dist/commands/campaign/kind-map.d.ts.map +1 -0
  23. package/dist/commands/campaign/kind-map.js +21 -0
  24. package/dist/commands/campaign/kind-map.js.map +1 -0
  25. package/dist/commands/campaign/mcp-server.d.ts +6 -0
  26. package/dist/commands/campaign/mcp-server.d.ts.map +1 -0
  27. package/dist/commands/campaign/mcp-server.js +167 -0
  28. package/dist/commands/campaign/mcp-server.js.map +1 -0
  29. package/dist/commands/campaign/render.d.ts +5 -0
  30. package/dist/commands/campaign/render.d.ts.map +1 -0
  31. package/dist/commands/campaign/render.js +103 -0
  32. package/dist/commands/campaign/render.js.map +1 -0
  33. package/dist/commands/campaign/trellis-load.d.ts +13 -0
  34. package/dist/commands/campaign/trellis-load.d.ts.map +1 -0
  35. package/dist/commands/campaign/trellis-load.js +122 -0
  36. package/dist/commands/campaign/trellis-load.js.map +1 -0
  37. package/dist/commands/campaign/types.d.ts +61 -0
  38. package/dist/commands/campaign/types.d.ts.map +1 -0
  39. package/dist/commands/campaign/types.js +3 -0
  40. package/dist/commands/campaign/types.js.map +1 -0
  41. package/dist/commands/goal/index.d.ts +3 -0
  42. package/dist/commands/goal/index.d.ts.map +1 -0
  43. package/dist/commands/goal/index.js +159 -0
  44. package/dist/commands/goal/index.js.map +1 -0
  45. package/dist/commands/goal/review.d.ts +6 -0
  46. package/dist/commands/goal/review.d.ts.map +1 -0
  47. package/dist/commands/goal/review.js +28 -0
  48. package/dist/commands/goal/review.js.map +1 -0
  49. package/dist/commands/init.d.ts.map +1 -1
  50. package/dist/commands/init.js +11 -1
  51. package/dist/commands/init.js.map +1 -1
  52. package/dist/commands/rpc/client.d.ts +51 -0
  53. package/dist/commands/rpc/client.d.ts.map +1 -0
  54. package/dist/commands/rpc/client.js +147 -0
  55. package/dist/commands/rpc/client.js.map +1 -0
  56. package/dist/commands/rpc/index.d.ts +10 -0
  57. package/dist/commands/rpc/index.d.ts.map +1 -0
  58. package/dist/commands/rpc/index.js +157 -0
  59. package/dist/commands/rpc/index.js.map +1 -0
  60. package/dist/commands/rpc/serve.d.ts +23 -0
  61. package/dist/commands/rpc/serve.d.ts.map +1 -0
  62. package/dist/commands/rpc/serve.js +60 -0
  63. package/dist/commands/rpc/serve.js.map +1 -0
  64. package/dist/commands/rpc/smoke.d.ts +16 -0
  65. package/dist/commands/rpc/smoke.d.ts.map +1 -0
  66. package/dist/commands/rpc/smoke.js +92 -0
  67. package/dist/commands/rpc/smoke.js.map +1 -0
  68. package/dist/commands/sdk/index.d.ts +9 -0
  69. package/dist/commands/sdk/index.d.ts.map +1 -0
  70. package/dist/commands/sdk/index.js +58 -0
  71. package/dist/commands/sdk/index.js.map +1 -0
  72. package/dist/commands/sdk/run.d.ts +42 -0
  73. package/dist/commands/sdk/run.d.ts.map +1 -0
  74. package/dist/commands/sdk/run.js +220 -0
  75. package/dist/commands/sdk/run.js.map +1 -0
  76. package/dist/commands/sdk/status.d.ts +14 -0
  77. package/dist/commands/sdk/status.d.ts.map +1 -0
  78. package/dist/commands/sdk/status.js +69 -0
  79. package/dist/commands/sdk/status.js.map +1 -0
  80. package/dist/configurators/workflow.d.ts.map +1 -1
  81. package/dist/configurators/workflow.js +13 -1
  82. package/dist/configurators/workflow.js.map +1 -1
  83. package/dist/goal/action-packet.d.ts +89 -0
  84. package/dist/goal/action-packet.d.ts.map +1 -0
  85. package/dist/goal/action-packet.js +113 -0
  86. package/dist/goal/action-packet.js.map +1 -0
  87. package/dist/goal/audit.d.ts +14 -0
  88. package/dist/goal/audit.d.ts.map +1 -0
  89. package/dist/goal/audit.js +12 -0
  90. package/dist/goal/audit.js.map +1 -0
  91. package/dist/goal/constants.d.ts +26 -0
  92. package/dist/goal/constants.d.ts.map +1 -0
  93. package/dist/goal/constants.js +24 -0
  94. package/dist/goal/constants.js.map +1 -0
  95. package/dist/goal/paths.d.ts +8 -0
  96. package/dist/goal/paths.d.ts.map +1 -0
  97. package/dist/goal/paths.js +36 -0
  98. package/dist/goal/paths.js.map +1 -0
  99. package/dist/goal/preflight.d.ts +21 -0
  100. package/dist/goal/preflight.d.ts.map +1 -0
  101. package/dist/goal/preflight.js +97 -0
  102. package/dist/goal/preflight.js.map +1 -0
  103. package/dist/goal/review-seam.d.ts +44 -0
  104. package/dist/goal/review-seam.d.ts.map +1 -0
  105. package/dist/goal/review-seam.js +153 -0
  106. package/dist/goal/review-seam.js.map +1 -0
  107. package/dist/goal/reviewer-rules.d.ts +13 -0
  108. package/dist/goal/reviewer-rules.d.ts.map +1 -0
  109. package/dist/goal/reviewer-rules.js +153 -0
  110. package/dist/goal/reviewer-rules.js.map +1 -0
  111. package/dist/goal/reviewer.d.ts +9 -0
  112. package/dist/goal/reviewer.d.ts.map +1 -0
  113. package/dist/goal/reviewer.js +54 -0
  114. package/dist/goal/reviewer.js.map +1 -0
  115. package/dist/goal/runtime.d.ts +32 -0
  116. package/dist/goal/runtime.d.ts.map +1 -0
  117. package/dist/goal/runtime.js +242 -0
  118. package/dist/goal/runtime.js.map +1 -0
  119. package/dist/goal/sdk-client.d.ts +10 -0
  120. package/dist/goal/sdk-client.d.ts.map +1 -0
  121. package/dist/goal/sdk-client.js +34 -0
  122. package/dist/goal/sdk-client.js.map +1 -0
  123. package/dist/goal/state.d.ts +40 -0
  124. package/dist/goal/state.d.ts.map +1 -0
  125. package/dist/goal/state.js +37 -0
  126. package/dist/goal/state.js.map +1 -0
  127. package/dist/goal/task-triggers.d.ts +15 -0
  128. package/dist/goal/task-triggers.d.ts.map +1 -0
  129. package/dist/goal/task-triggers.js +23 -0
  130. package/dist/goal/task-triggers.js.map +1 -0
  131. package/dist/goal/walls.d.ts +12 -0
  132. package/dist/goal/walls.d.ts.map +1 -0
  133. package/dist/goal/walls.js +65 -0
  134. package/dist/goal/walls.js.map +1 -0
  135. package/dist/goal/worker-prompt.d.ts +13 -0
  136. package/dist/goal/worker-prompt.d.ts.map +1 -0
  137. package/dist/goal/worker-prompt.js +41 -0
  138. package/dist/goal/worker-prompt.js.map +1 -0
  139. package/dist/goal/worker.d.ts +42 -0
  140. package/dist/goal/worker.d.ts.map +1 -0
  141. package/dist/goal/worker.js +180 -0
  142. package/dist/goal/worker.js.map +1 -0
  143. package/dist/migrations/manifests/0.3.5.json +9 -0
  144. package/dist/migrations/manifests/0.3.6.json +9 -0
  145. package/dist/templates/common/bundled-skills/smart-search-cli/SKILL.md +35 -29
  146. package/dist/templates/common/bundled-skills/smart-search-cli/references/cli-contract.md +11 -25
  147. package/dist/templates/common/commands/continue.md +15 -3
  148. package/dist/templates/common/skills/check.md +7 -4
  149. package/dist/templates/cursor/agents/cstl-check.md +1 -0
  150. package/dist/templates/cursor/fixtures/expected-rules.d.ts.map +1 -1
  151. package/dist/templates/cursor/fixtures/expected-rules.js +6 -1
  152. package/dist/templates/cursor/fixtures/expected-rules.js.map +1 -1
  153. package/dist/templates/cursor/rules/cstl-cursor-modes.mdc +32 -0
  154. package/dist/templates/cursor/rules/cstl-triage.mdc +2 -4
  155. package/dist/templates/cursor/rules/retrieval-routing.mdc +26 -7
  156. package/dist/templates/markdown/agents.md +2 -0
  157. package/dist/templates/markdown/index.d.ts +3 -0
  158. package/dist/templates/markdown/index.d.ts.map +1 -1
  159. package/dist/templates/markdown/index.js +3 -0
  160. package/dist/templates/markdown/index.js.map +1 -1
  161. package/dist/templates/markdown/spec/guides/cursor-context-injection-guide.md.txt +38 -21
  162. package/dist/templates/markdown/spec/guides/cursor-native-modes-guide.md.txt +125 -0
  163. package/dist/templates/markdown/spec/guides/index.md.txt +107 -104
  164. package/dist/templates/markdown/spec/guides/injection-budget-guide.md.txt +111 -0
  165. package/dist/templates/markdown/spec/guides/retrieval-daily-guide.md.txt +38 -6
  166. package/dist/templates/markdown/spec/guides/verification-strength-guide.md.txt +184 -0
  167. package/dist/templates/shared-hooks/session-start.py +10 -0
  168. package/dist/templates/trellis/config.yaml +18 -0
  169. package/dist/templates/trellis/index.d.ts +5 -0
  170. package/dist/templates/trellis/index.d.ts.map +1 -1
  171. package/dist/templates/trellis/index.js +14 -0
  172. package/dist/templates/trellis/index.js.map +1 -1
  173. package/dist/templates/trellis/scripts/common/artifact_locale.py +278 -0
  174. package/dist/templates/trellis/scripts/common/codebase_retrieval_router.py +51 -2
  175. package/dist/templates/trellis/scripts/common/config.py +39 -0
  176. package/dist/templates/trellis/scripts/common/injection_budget.py +317 -0
  177. package/dist/templates/trellis/scripts/common/parent_orchestration.py +500 -5
  178. package/dist/templates/trellis/scripts/common/retrieval_agent_instructions.py +9 -3
  179. package/dist/templates/trellis/scripts/common/session_context.py +32 -4
  180. package/dist/templates/trellis/scripts/common/session_memory.py +26 -0
  181. package/dist/templates/trellis/scripts/common/subagent_dispatch.py +74 -9
  182. package/dist/templates/trellis/scripts/common/task_context.py +18 -0
  183. package/dist/templates/trellis/scripts/common/task_dashboard.py +16 -1
  184. package/dist/templates/trellis/scripts/common/task_map.py +670 -614
  185. package/dist/templates/trellis/scripts/common/task_store.py +191 -40
  186. package/dist/templates/trellis/scripts/common/test_observable_defaults.py +135 -0
  187. package/dist/templates/trellis/scripts/cursor_retrieval_probe_prompt.md +14 -12
  188. package/dist/templates/trellis/scripts/generate_dispatch_prompt.py +182 -0
  189. package/dist/templates/trellis/scripts/injection_budget_probe.py +69 -0
  190. package/dist/templates/trellis/scripts/retrieval_probe_matrix_template.json +3 -3
  191. package/dist/templates/trellis/scripts/spec_health_outcomes.py +160 -0
  192. package/dist/templates/trellis/scripts/task.py +51 -0
  193. package/dist/templates/trellis/tasks/locale/en/default-prd.md +19 -0
  194. package/dist/templates/trellis/tasks/locale/zh/default-prd.md +19 -0
  195. package/dist/templates/trellis/workflow.md +23 -3
  196. package/dist/utils/codebase-retrieval-router.d.ts.map +1 -1
  197. package/dist/utils/codebase-retrieval-router.js +14 -0
  198. package/dist/utils/codebase-retrieval-router.js.map +1 -1
  199. package/dist/utils/cursor-sdk-gate.d.ts +10 -0
  200. package/dist/utils/cursor-sdk-gate.d.ts.map +1 -0
  201. package/dist/utils/cursor-sdk-gate.js +32 -0
  202. package/dist/utils/cursor-sdk-gate.js.map +1 -0
  203. package/dist/utils/project-capabilities.d.ts +30 -3
  204. package/dist/utils/project-capabilities.d.ts.map +1 -1
  205. package/dist/utils/project-capabilities.js +182 -11
  206. package/dist/utils/project-capabilities.js.map +1 -1
  207. package/dist/utils/readiness.d.ts.map +1 -1
  208. package/dist/utils/readiness.js +26 -0
  209. package/dist/utils/readiness.js.map +1 -1
  210. package/package.json +4 -3
@@ -0,0 +1,317 @@
1
+ #!/usr/bin/env python3
2
+ """Injection budget constants and measurement helpers (P0 channel matrix aligned)."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import json
7
+ import re
8
+ from dataclasses import dataclass, field
9
+ from pathlib import Path
10
+ from typing import Literal
11
+
12
+ RULES_TOTAL_WARN_BYTES = 20_480
13
+ RULES_TOTAL_FAIL_BYTES = 32_768
14
+ AGENTS_CSTL_WARN_BYTES = 4_096
15
+ JSONL_MAX_ENTRIES = 8
16
+ JSONL_MAX_FILE_CHARS = 12_000
17
+ JSONL_MAX_TOTAL_CHARS = 48_000
18
+
19
+ DISPATCH_DEFAULT_MAX_CHARS: dict[str, int] = {
20
+ "implement": 96_000,
21
+ "check": 64_000,
22
+ "research": 48_000,
23
+ }
24
+
25
+ DispatchRole = Literal["implement", "check", "research"]
26
+
27
+
28
+ @dataclass
29
+ class RuleFileMeasure:
30
+ path: str
31
+ bytes: int
32
+ always_apply: bool
33
+
34
+
35
+ @dataclass
36
+ class RulesMeasure:
37
+ files: list[RuleFileMeasure] = field(default_factory=list)
38
+
39
+ @property
40
+ def always_apply_total_bytes(self) -> int:
41
+ return sum(f.bytes for f in self.files if f.always_apply)
42
+
43
+
44
+ @dataclass
45
+ class AgentsMeasure:
46
+ path: str
47
+ total_bytes: int
48
+ cstl_block_bytes: int
49
+
50
+
51
+ @dataclass
52
+ class JsonlMeasure:
53
+ jsonl_path: str
54
+ entry_count: int
55
+ expanded_chars: int
56
+ skipped: list[str] = field(default_factory=list)
57
+ loaded_files: list[str] = field(default_factory=list)
58
+
59
+
60
+ @dataclass
61
+ class DispatchMeasure:
62
+ role: str
63
+ task_rel: str
64
+ prompt_chars: int
65
+ warnings: list[str] = field(default_factory=list)
66
+
67
+
68
+ @dataclass
69
+ class BudgetReport:
70
+ rules: RulesMeasure
71
+ agents: AgentsMeasure | None
72
+ jsonl: list[JsonlMeasure] = field(default_factory=list)
73
+ dispatch: DispatchMeasure | None = None
74
+
75
+
76
+ def _read_text_bytes(path: Path) -> tuple[str, int]:
77
+ data = path.read_bytes()
78
+ try:
79
+ text = data.decode("utf-8")
80
+ except UnicodeDecodeError:
81
+ text = data.decode("utf-8", errors="replace")
82
+ return text, len(data)
83
+
84
+
85
+ def measure_always_apply_rules(repo_root: Path) -> RulesMeasure:
86
+ rules_dir = repo_root / ".cursor" / "rules"
87
+ result = RulesMeasure()
88
+ if not rules_dir.is_dir():
89
+ return result
90
+
91
+ for path in sorted(rules_dir.glob("*.mdc")):
92
+ text, nbytes = _read_text_bytes(path)
93
+ always = bool(re.search(r"^alwaysApply:\s*true\s*$", text, re.MULTILINE))
94
+ result.files.append(
95
+ RuleFileMeasure(path=str(path.relative_to(repo_root)), bytes=nbytes, always_apply=always)
96
+ )
97
+ return result
98
+
99
+
100
+ def measure_agents_block(repo_root: Path) -> AgentsMeasure | None:
101
+ agents_path = repo_root / "AGENTS.md"
102
+ if not agents_path.is_file():
103
+ return None
104
+ text, nbytes = _read_text_bytes(agents_path)
105
+ cstl_bytes = 0
106
+ start = text.find("<!-- CSTL:START -->")
107
+ end = text.find("<!-- CSTL:END -->")
108
+ if start != -1 and end != -1 and end > start:
109
+ cstl_bytes = len(text[start : end + len("<!-- CSTL:END -->")].encode("utf-8"))
110
+ return AgentsMeasure(
111
+ path="AGENTS.md",
112
+ total_bytes=nbytes,
113
+ cstl_block_bytes=cstl_bytes,
114
+ )
115
+
116
+
117
+ def _expand_jsonl_entry(repo_root: Path, file_path: str, entry_type: str) -> list[tuple[str, str]]:
118
+ if entry_type == "directory":
119
+ from .subagent_dispatch import read_directory_contents # lazy import
120
+
121
+ return read_directory_contents(str(repo_root), file_path)
122
+ from .subagent_dispatch import read_file_content
123
+
124
+ content = read_file_content(str(repo_root), file_path)
125
+ if content is None:
126
+ return []
127
+ return [(file_path, content)]
128
+
129
+
130
+ def measure_jsonl_manifest(
131
+ repo_root: Path,
132
+ task_dir: Path,
133
+ jsonl_name: str,
134
+ *,
135
+ apply_budget: bool = True,
136
+ ) -> JsonlMeasure:
137
+ jsonl_path = task_dir / jsonl_name
138
+ rel_jsonl = str(jsonl_path.relative_to(repo_root)) if jsonl_path.is_file() else jsonl_name
139
+ measure = JsonlMeasure(jsonl_path=rel_jsonl, entry_count=0, expanded_chars=0)
140
+ if not jsonl_path.is_file():
141
+ return measure
142
+
143
+ entries: list[tuple[str, str]] = []
144
+ for line in jsonl_path.read_text(encoding="utf-8").splitlines():
145
+ line = line.strip()
146
+ if not line:
147
+ continue
148
+ try:
149
+ item = json.loads(line)
150
+ except json.JSONDecodeError:
151
+ continue
152
+ file_path = item.get("file") or item.get("path")
153
+ if not file_path:
154
+ continue
155
+ entry_type = item.get("type", "file")
156
+ entries.append((file_path, entry_type))
157
+
158
+ measure.entry_count = len(entries)
159
+ if not apply_budget:
160
+ for file_path, entry_type in entries:
161
+ for rel, content in _expand_jsonl_entry(repo_root, file_path, entry_type):
162
+ measure.loaded_files.append(rel)
163
+ measure.expanded_chars += len(content)
164
+ return measure
165
+
166
+ total_chars = 0
167
+ loaded_count = 0
168
+ for file_path, entry_type in entries:
169
+ if loaded_count >= JSONL_MAX_ENTRIES:
170
+ measure.skipped.append(f"{file_path}: entry cap ({JSONL_MAX_ENTRIES})")
171
+ continue
172
+ expanded = _expand_jsonl_entry(repo_root, file_path, entry_type)
173
+ if not expanded:
174
+ measure.skipped.append(f"{file_path}: missing or empty")
175
+ continue
176
+ block_chars = sum(len(content) for _, content in expanded)
177
+ if block_chars > JSONL_MAX_FILE_CHARS:
178
+ measure.skipped.append(
179
+ f"{file_path}: file cap ({block_chars}>{JSONL_MAX_FILE_CHARS} chars)"
180
+ )
181
+ continue
182
+ if total_chars + block_chars > JSONL_MAX_TOTAL_CHARS:
183
+ measure.skipped.append(
184
+ f"{file_path}: total cap ({total_chars + block_chars}>{JSONL_MAX_TOTAL_CHARS})"
185
+ )
186
+ continue
187
+ for rel, content in expanded:
188
+ measure.loaded_files.append(rel)
189
+ total_chars += len(content)
190
+ loaded_count += 1
191
+
192
+ measure.expanded_chars = total_chars
193
+ return measure
194
+
195
+
196
+ def measure_dispatch_prompt(repo_root: Path, task_dir: Path, role: DispatchRole) -> DispatchMeasure | None:
197
+ from .subagent_dispatch import build_dispatch_prompt
198
+
199
+ prompt, warnings, errors = build_dispatch_prompt(
200
+ repo_root,
201
+ task_dir,
202
+ role,
203
+ max_chars=DISPATCH_DEFAULT_MAX_CHARS.get(role),
204
+ require_in_progress=False,
205
+ )
206
+ if not prompt:
207
+ return None
208
+ try:
209
+ task_rel = task_dir.relative_to(repo_root).as_posix()
210
+ except ValueError:
211
+ task_rel = task_dir.as_posix()
212
+ return DispatchMeasure(
213
+ role=role,
214
+ task_rel=task_rel,
215
+ prompt_chars=len(prompt),
216
+ warnings=list(warnings) + [f"error:{e}" for e in errors],
217
+ )
218
+
219
+
220
+ def collect_budget_report(
221
+ repo_root: Path,
222
+ *,
223
+ task_dir: Path | None = None,
224
+ dispatch_role: DispatchRole | None = None,
225
+ ) -> BudgetReport:
226
+ report = BudgetReport(
227
+ rules=measure_always_apply_rules(repo_root),
228
+ agents=measure_agents_block(repo_root),
229
+ )
230
+ if task_dir and task_dir.is_dir():
231
+ for name in ("implement.jsonl", "check.jsonl"):
232
+ report.jsonl.append(measure_jsonl_manifest(repo_root, task_dir, name))
233
+ if dispatch_role:
234
+ report.dispatch = measure_dispatch_prompt(repo_root, task_dir, dispatch_role)
235
+ return report
236
+
237
+
238
+ def budget_violations(report: BudgetReport, *, strict: bool = False) -> list[str]:
239
+ violations: list[str] = []
240
+ rules_total = report.rules.always_apply_total_bytes
241
+ if rules_total > RULES_TOTAL_WARN_BYTES:
242
+ level = "FAIL" if strict and rules_total > RULES_TOTAL_FAIL_BYTES else "WARN"
243
+ violations.append(
244
+ f"{level}: alwaysApply rules total {rules_total} B "
245
+ f"(warn>{RULES_TOTAL_WARN_BYTES}, fail>{RULES_TOTAL_FAIL_BYTES})"
246
+ )
247
+ elif strict and rules_total > RULES_TOTAL_WARN_BYTES:
248
+ violations.append(
249
+ f"WARN: alwaysApply rules total {rules_total} B (warn>{RULES_TOTAL_WARN_BYTES})"
250
+ )
251
+
252
+ if report.agents and report.agents.cstl_block_bytes > AGENTS_CSTL_WARN_BYTES:
253
+ violations.append(
254
+ f"WARN: AGENTS CSTL block {report.agents.cstl_block_bytes} B "
255
+ f"(warn>{AGENTS_CSTL_WARN_BYTES})"
256
+ )
257
+
258
+ for jm in report.jsonl:
259
+ if jm.entry_count > JSONL_MAX_ENTRIES:
260
+ violations.append(
261
+ f"WARN: {jm.jsonl_path} has {jm.entry_count} entries (max {JSONL_MAX_ENTRIES})"
262
+ )
263
+ if jm.expanded_chars > JSONL_MAX_TOTAL_CHARS:
264
+ violations.append(
265
+ f"WARN: {jm.jsonl_path} expanded {jm.expanded_chars} chars "
266
+ f"(max {JSONL_MAX_TOTAL_CHARS})"
267
+ )
268
+
269
+ if report.dispatch:
270
+ cap = DISPATCH_DEFAULT_MAX_CHARS.get(report.dispatch.role, 0)
271
+ if cap and report.dispatch.prompt_chars > cap:
272
+ violations.append(
273
+ f"WARN: dispatch {report.dispatch.role} prompt {report.dispatch.prompt_chars} chars "
274
+ f"(default cap {cap})"
275
+ )
276
+
277
+ return violations
278
+
279
+
280
+ def format_budget_report(report: BudgetReport) -> str:
281
+ lines: list[str] = ["# Injection budget report", ""]
282
+ lines.append("## alwaysApply rules (.cursor/rules/*.mdc)")
283
+ for rf in report.rules.files:
284
+ flag = "alwaysApply" if rf.always_apply else "scoped"
285
+ lines.append(f"- {rf.path}: {rf.bytes} B ({flag})")
286
+ lines.append(f"- **Total alwaysApply:** {report.rules.always_apply_total_bytes} B")
287
+ lines.append("")
288
+
289
+ if report.agents:
290
+ lines.append("## AGENTS.md")
291
+ lines.append(f"- Total: {report.agents.total_bytes} B")
292
+ lines.append(f"- CSTL block: {report.agents.cstl_block_bytes} B")
293
+ lines.append("")
294
+
295
+ for jm in report.jsonl:
296
+ lines.append(f"## {jm.jsonl_path}")
297
+ lines.append(f"- Entries: {jm.entry_count}")
298
+ lines.append(f"- Expanded (budgeted): {jm.expanded_chars} chars")
299
+ if jm.loaded_files:
300
+ lines.append(f"- Loaded: {', '.join(jm.loaded_files)}")
301
+ if jm.skipped:
302
+ lines.append("- Skipped:")
303
+ for item in jm.skipped:
304
+ lines.append(f" - {item}")
305
+ lines.append("")
306
+
307
+ if report.dispatch:
308
+ lines.append(f"## Layer 2 dispatch ({report.dispatch.role})")
309
+ lines.append(f"- Task: {report.dispatch.task_rel}")
310
+ lines.append(f"- Prompt chars: {report.dispatch.prompt_chars}")
311
+ if report.dispatch.warnings:
312
+ lines.append("- Warnings:")
313
+ for w in report.dispatch.warnings:
314
+ lines.append(f" - {w}")
315
+ lines.append("")
316
+
317
+ return "\n".join(lines)