@blxzer/cursor-trellis 0.3.5 → 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.
- package/CHANGELOG.md +46 -0
- package/README.md +13 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +3 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/commands/goal/index.d.ts +3 -0
- package/dist/commands/goal/index.d.ts.map +1 -0
- package/dist/commands/goal/index.js +159 -0
- package/dist/commands/goal/index.js.map +1 -0
- package/dist/commands/goal/review.d.ts +6 -0
- package/dist/commands/goal/review.d.ts.map +1 -0
- package/dist/commands/goal/review.js +28 -0
- package/dist/commands/goal/review.js.map +1 -0
- package/dist/commands/sdk/run.d.ts.map +1 -1
- package/dist/commands/sdk/run.js +2 -27
- package/dist/commands/sdk/run.js.map +1 -1
- package/dist/configurators/workflow.d.ts.map +1 -1
- package/dist/configurators/workflow.js +13 -1
- package/dist/configurators/workflow.js.map +1 -1
- package/dist/goal/action-packet.d.ts +89 -0
- package/dist/goal/action-packet.d.ts.map +1 -0
- package/dist/goal/action-packet.js +113 -0
- package/dist/goal/action-packet.js.map +1 -0
- package/dist/goal/audit.d.ts +14 -0
- package/dist/goal/audit.d.ts.map +1 -0
- package/dist/goal/audit.js +12 -0
- package/dist/goal/audit.js.map +1 -0
- package/dist/goal/constants.d.ts +26 -0
- package/dist/goal/constants.d.ts.map +1 -0
- package/dist/goal/constants.js +24 -0
- package/dist/goal/constants.js.map +1 -0
- package/dist/goal/paths.d.ts +8 -0
- package/dist/goal/paths.d.ts.map +1 -0
- package/dist/goal/paths.js +36 -0
- package/dist/goal/paths.js.map +1 -0
- package/dist/goal/preflight.d.ts +21 -0
- package/dist/goal/preflight.d.ts.map +1 -0
- package/dist/goal/preflight.js +97 -0
- package/dist/goal/preflight.js.map +1 -0
- package/dist/goal/review-seam.d.ts +44 -0
- package/dist/goal/review-seam.d.ts.map +1 -0
- package/dist/goal/review-seam.js +153 -0
- package/dist/goal/review-seam.js.map +1 -0
- package/dist/goal/reviewer-rules.d.ts +13 -0
- package/dist/goal/reviewer-rules.d.ts.map +1 -0
- package/dist/goal/reviewer-rules.js +153 -0
- package/dist/goal/reviewer-rules.js.map +1 -0
- package/dist/goal/reviewer.d.ts +9 -0
- package/dist/goal/reviewer.d.ts.map +1 -0
- package/dist/goal/reviewer.js +54 -0
- package/dist/goal/reviewer.js.map +1 -0
- package/dist/goal/runtime.d.ts +32 -0
- package/dist/goal/runtime.d.ts.map +1 -0
- package/dist/goal/runtime.js +242 -0
- package/dist/goal/runtime.js.map +1 -0
- package/dist/goal/sdk-client.d.ts +10 -0
- package/dist/goal/sdk-client.d.ts.map +1 -0
- package/dist/goal/sdk-client.js +34 -0
- package/dist/goal/sdk-client.js.map +1 -0
- package/dist/goal/state.d.ts +40 -0
- package/dist/goal/state.d.ts.map +1 -0
- package/dist/goal/state.js +37 -0
- package/dist/goal/state.js.map +1 -0
- package/dist/goal/task-triggers.d.ts +15 -0
- package/dist/goal/task-triggers.d.ts.map +1 -0
- package/dist/goal/task-triggers.js +23 -0
- package/dist/goal/task-triggers.js.map +1 -0
- package/dist/goal/walls.d.ts +12 -0
- package/dist/goal/walls.d.ts.map +1 -0
- package/dist/goal/walls.js +65 -0
- package/dist/goal/walls.js.map +1 -0
- package/dist/goal/worker-prompt.d.ts +13 -0
- package/dist/goal/worker-prompt.d.ts.map +1 -0
- package/dist/goal/worker-prompt.js +41 -0
- package/dist/goal/worker-prompt.js.map +1 -0
- package/dist/goal/worker.d.ts +42 -0
- package/dist/goal/worker.d.ts.map +1 -0
- package/dist/goal/worker.js +180 -0
- package/dist/goal/worker.js.map +1 -0
- package/dist/migrations/manifests/0.3.5.json +9 -0
- package/dist/migrations/manifests/0.3.6.json +9 -0
- package/dist/templates/common/commands/continue.md +15 -3
- package/dist/templates/common/skills/check.md +7 -4
- package/dist/templates/cursor/agents/cstl-check.md +1 -0
- package/dist/templates/cursor/fixtures/expected-rules.d.ts.map +1 -1
- package/dist/templates/cursor/fixtures/expected-rules.js +6 -1
- package/dist/templates/cursor/fixtures/expected-rules.js.map +1 -1
- package/dist/templates/cursor/rules/cstl-cursor-modes.mdc +32 -0
- package/dist/templates/cursor/rules/cstl-triage.mdc +2 -4
- package/dist/templates/cursor/rules/retrieval-routing.mdc +26 -7
- package/dist/templates/markdown/index.d.ts +3 -0
- package/dist/templates/markdown/index.d.ts.map +1 -1
- package/dist/templates/markdown/index.js +3 -0
- package/dist/templates/markdown/index.js.map +1 -1
- package/dist/templates/markdown/spec/guides/cursor-context-injection-guide.md.txt +38 -21
- package/dist/templates/markdown/spec/guides/cursor-native-modes-guide.md.txt +125 -0
- package/dist/templates/markdown/spec/guides/index.md.txt +107 -104
- package/dist/templates/markdown/spec/guides/injection-budget-guide.md.txt +111 -0
- package/dist/templates/markdown/spec/guides/retrieval-daily-guide.md.txt +24 -6
- package/dist/templates/markdown/spec/guides/verification-strength-guide.md.txt +184 -0
- package/dist/templates/shared-hooks/session-start.py +10 -0
- package/dist/templates/trellis/config.yaml +18 -0
- package/dist/templates/trellis/index.d.ts +5 -0
- package/dist/templates/trellis/index.d.ts.map +1 -1
- package/dist/templates/trellis/index.js +14 -0
- package/dist/templates/trellis/index.js.map +1 -1
- package/dist/templates/trellis/scripts/common/artifact_locale.py +278 -0
- package/dist/templates/trellis/scripts/common/codebase_retrieval_router.py +37 -2
- package/dist/templates/trellis/scripts/common/config.py +39 -0
- package/dist/templates/trellis/scripts/common/injection_budget.py +317 -0
- package/dist/templates/trellis/scripts/common/parent_orchestration.py +161 -5
- package/dist/templates/trellis/scripts/common/retrieval_agent_instructions.py +9 -3
- package/dist/templates/trellis/scripts/common/session_context.py +32 -4
- package/dist/templates/trellis/scripts/common/session_memory.py +26 -0
- package/dist/templates/trellis/scripts/common/subagent_dispatch.py +74 -9
- package/dist/templates/trellis/scripts/common/task_context.py +18 -0
- package/dist/templates/trellis/scripts/common/task_dashboard.py +16 -1
- package/dist/templates/trellis/scripts/common/task_map.py +40 -8
- package/dist/templates/trellis/scripts/common/task_store.py +144 -39
- package/dist/templates/trellis/scripts/common/test_observable_defaults.py +135 -0
- package/dist/templates/trellis/scripts/cursor_retrieval_probe_prompt.md +14 -12
- package/dist/templates/trellis/scripts/generate_dispatch_prompt.py +182 -0
- package/dist/templates/trellis/scripts/injection_budget_probe.py +69 -0
- package/dist/templates/trellis/scripts/retrieval_probe_matrix_template.json +3 -3
- package/dist/templates/trellis/scripts/spec_health_outcomes.py +160 -0
- package/dist/templates/trellis/scripts/task.py +19 -0
- package/dist/templates/trellis/tasks/locale/en/default-prd.md +19 -0
- package/dist/templates/trellis/tasks/locale/zh/default-prd.md +19 -0
- package/dist/templates/trellis/workflow.md +23 -3
- package/package.json +2 -2
|
@@ -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)
|
|
@@ -12,6 +12,7 @@ import re
|
|
|
12
12
|
from datetime import datetime, timezone
|
|
13
13
|
from pathlib import Path
|
|
14
14
|
|
|
15
|
+
from .io import read_json
|
|
15
16
|
from .paths import get_repo_root
|
|
16
17
|
from .task_gates import (
|
|
17
18
|
build_spec_update_scaffold,
|
|
@@ -22,6 +23,7 @@ from .task_map import (
|
|
|
22
23
|
PARENT_TERMINAL_STATES,
|
|
23
24
|
get_child_entry,
|
|
24
25
|
load_task_map,
|
|
26
|
+
resolve_child_map_id,
|
|
25
27
|
validate_parent_child_integration,
|
|
26
28
|
)
|
|
27
29
|
|
|
@@ -100,7 +102,7 @@ def _child_prompt_section(
|
|
|
100
102
|
*,
|
|
101
103
|
include_artifacts: bool,
|
|
102
104
|
) -> str:
|
|
103
|
-
child_name = child_dir.name
|
|
105
|
+
child_name = child_entry.get("id") or child_dir.name
|
|
104
106
|
parent_rel = _repo_rel(parent_dir, repo_root)
|
|
105
107
|
child_rel = _repo_rel(child_dir, repo_root)
|
|
106
108
|
state = child_entry.get("state", "open")
|
|
@@ -164,12 +166,15 @@ def build_child_prompt(
|
|
|
164
166
|
"""Build an implementation prompt for a linked child task."""
|
|
165
167
|
errors: list[str] = []
|
|
166
168
|
repo_root = get_repo_root()
|
|
167
|
-
child_name = child_dir.name
|
|
168
169
|
|
|
169
170
|
parent_json = parent_dir / "task.json"
|
|
170
171
|
if not parent_json.is_file():
|
|
171
172
|
return None, [f"parent task.json missing: {parent_dir}"]
|
|
172
173
|
|
|
174
|
+
parent_data = read_json(parent_json) or {}
|
|
175
|
+
child_data = read_json(child_dir / "task.json") or {}
|
|
176
|
+
child_name = resolve_child_map_id(parent_data, child_dir, child_data)
|
|
177
|
+
|
|
173
178
|
data, body = load_task_map(parent_dir)
|
|
174
179
|
if data is None:
|
|
175
180
|
return None, ["parent task-map.md missing or invalid"]
|
|
@@ -360,6 +365,141 @@ def _unit_readiness(
|
|
|
360
365
|
return ("ready" if not blocked else "blocked", blocked)
|
|
361
366
|
|
|
362
367
|
|
|
368
|
+
_PACK_GENERATED_RE = re.compile(r"^- Generated:\s*(.+)$", re.MULTILINE)
|
|
369
|
+
_PACK_DASH = r"[\u2014\-]"
|
|
370
|
+
_PACK_UNIT_LINE_RE = re.compile(
|
|
371
|
+
rf"^- `([^`]+)` {_PACK_DASH} `([^`]+)` {_PACK_DASH} \*\*(ready|blocked)\*\*",
|
|
372
|
+
re.MULTILINE,
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _parse_iso_timestamp(raw: str) -> datetime | None:
|
|
377
|
+
text = raw.strip()
|
|
378
|
+
if not text:
|
|
379
|
+
return None
|
|
380
|
+
normalized = text[:-1] + "+00:00" if text.endswith("Z") else text
|
|
381
|
+
try:
|
|
382
|
+
parsed = datetime.fromisoformat(normalized)
|
|
383
|
+
except ValueError:
|
|
384
|
+
try:
|
|
385
|
+
parsed = datetime.strptime(text, "%Y-%m-%d")
|
|
386
|
+
except ValueError:
|
|
387
|
+
return None
|
|
388
|
+
if parsed.tzinfo is None:
|
|
389
|
+
return parsed.replace(tzinfo=timezone.utc)
|
|
390
|
+
return parsed
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def _parse_pack_generated(pack_text: str) -> datetime | None:
|
|
394
|
+
match = _PACK_GENERATED_RE.search(pack_text)
|
|
395
|
+
if not match:
|
|
396
|
+
return None
|
|
397
|
+
return _parse_iso_timestamp(match.group(1))
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def _last_integrate_event_time(body: str) -> datetime | None:
|
|
401
|
+
last: datetime | None = None
|
|
402
|
+
for line in body.splitlines():
|
|
403
|
+
stripped = line.strip()
|
|
404
|
+
if not stripped.startswith("- "):
|
|
405
|
+
continue
|
|
406
|
+
remainder = stripped[2:]
|
|
407
|
+
parts = remainder.split(" - ", 1)
|
|
408
|
+
if len(parts) != 2:
|
|
409
|
+
continue
|
|
410
|
+
ts_raw, message = parts[0].strip(), parts[1]
|
|
411
|
+
lowered = message.lower()
|
|
412
|
+
if "integration state" not in message and "integrate-through" not in lowered:
|
|
413
|
+
continue
|
|
414
|
+
parsed = _parse_iso_timestamp(ts_raw)
|
|
415
|
+
if parsed is not None and (last is None or parsed > last):
|
|
416
|
+
last = parsed
|
|
417
|
+
return last
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def _pack_ready_units(pack_text: str) -> set[str]:
|
|
421
|
+
ready: set[str] = set()
|
|
422
|
+
for unit_id, _state, readiness in _PACK_UNIT_LINE_RE.findall(pack_text):
|
|
423
|
+
if readiness == "ready":
|
|
424
|
+
ready.add(unit_id)
|
|
425
|
+
return ready
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def _expected_pack_ready_units(
|
|
429
|
+
data: dict,
|
|
430
|
+
children_by_id: dict[str, dict],
|
|
431
|
+
) -> set[str]:
|
|
432
|
+
stages, _, _ = _normalize_stages(data)
|
|
433
|
+
ready: set[str] = set()
|
|
434
|
+
for stage in stages:
|
|
435
|
+
for unit in stage.get("units") or []:
|
|
436
|
+
if not isinstance(unit, str):
|
|
437
|
+
continue
|
|
438
|
+
readiness, _ = _unit_readiness(unit, children_by_id)
|
|
439
|
+
if readiness == "ready":
|
|
440
|
+
ready.add(unit)
|
|
441
|
+
return ready
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def _pack_unit_states_mismatch(
|
|
445
|
+
pack_text: str,
|
|
446
|
+
children_by_id: dict[str, dict],
|
|
447
|
+
) -> bool:
|
|
448
|
+
for unit_id, pack_state, _readiness in _PACK_UNIT_LINE_RE.findall(pack_text):
|
|
449
|
+
entry = children_by_id.get(unit_id) or {}
|
|
450
|
+
current_state = entry.get("state", "?")
|
|
451
|
+
if str(current_state) != pack_state:
|
|
452
|
+
return True
|
|
453
|
+
return False
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def _compute_stale_pack(
|
|
457
|
+
parent_dir: Path,
|
|
458
|
+
data: dict,
|
|
459
|
+
body: str,
|
|
460
|
+
children_by_id: dict[str, dict],
|
|
461
|
+
) -> bool | None:
|
|
462
|
+
pack_path = parent_dir / "child-prompts" / "PACK.md"
|
|
463
|
+
if not pack_path.is_file():
|
|
464
|
+
return None
|
|
465
|
+
try:
|
|
466
|
+
pack_text = pack_path.read_text(encoding="utf-8")
|
|
467
|
+
except OSError:
|
|
468
|
+
return None
|
|
469
|
+
|
|
470
|
+
generated = _parse_pack_generated(pack_text)
|
|
471
|
+
if generated is None:
|
|
472
|
+
return None
|
|
473
|
+
|
|
474
|
+
stale = False
|
|
475
|
+
last_integrate = _last_integrate_event_time(body)
|
|
476
|
+
if last_integrate is not None and generated < last_integrate:
|
|
477
|
+
stale = True
|
|
478
|
+
if _pack_unit_states_mismatch(pack_text, children_by_id):
|
|
479
|
+
stale = True
|
|
480
|
+
expected_ready = _expected_pack_ready_units(data, children_by_id)
|
|
481
|
+
if _pack_ready_units(pack_text) != expected_ready:
|
|
482
|
+
stale = True
|
|
483
|
+
return stale
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def _compute_newly_ready(children_by_id: dict[str, dict]) -> list[str]:
|
|
487
|
+
ready_ids: list[str] = []
|
|
488
|
+
for unit_id in sorted(children_by_id):
|
|
489
|
+
entry = children_by_id[unit_id]
|
|
490
|
+
if entry.get("state") != "open":
|
|
491
|
+
continue
|
|
492
|
+
readiness, _ = _unit_readiness(unit_id, children_by_id)
|
|
493
|
+
if readiness == "ready":
|
|
494
|
+
ready_ids.append(unit_id)
|
|
495
|
+
return ready_ids
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def _child_prompt_filename(unit_id: str) -> str:
|
|
499
|
+
"""Flat filename for child-prompts (nested slash unit ids use `--`)."""
|
|
500
|
+
return f"{unit_id.replace('/', '--')}.md"
|
|
501
|
+
|
|
502
|
+
|
|
363
503
|
def build_publish_pack(
|
|
364
504
|
parent_dir: Path,
|
|
365
505
|
*,
|
|
@@ -442,7 +582,7 @@ def build_publish_pack(
|
|
|
442
582
|
if prompt_errors or not prompt:
|
|
443
583
|
errors.extend(prompt_errors or [f"could not build prompt for `{unit}`"])
|
|
444
584
|
continue
|
|
445
|
-
out_name =
|
|
585
|
+
out_name = _child_prompt_filename(unit)
|
|
446
586
|
out_rel = f"{parent_rel}/child-prompts/{out_name}"
|
|
447
587
|
if not dry_run:
|
|
448
588
|
prompts_dir.mkdir(parents=True, exist_ok=True)
|
|
@@ -470,7 +610,7 @@ def build_publish_pack(
|
|
|
470
610
|
"",
|
|
471
611
|
]
|
|
472
612
|
)
|
|
473
|
-
out_name =
|
|
613
|
+
out_name = _child_prompt_filename(unit)
|
|
474
614
|
out_rel = f"{parent_rel}/child-prompts/{out_name}"
|
|
475
615
|
if not dry_run:
|
|
476
616
|
prompts_dir.mkdir(parents=True, exist_ok=True)
|
|
@@ -539,6 +679,9 @@ def build_parent_status_dict(parent_dir: Path) -> dict | None:
|
|
|
539
679
|
|
|
540
680
|
children_by_id = _children_by_id(data)
|
|
541
681
|
stages, stage_errors, legacy = _normalize_stages(data)
|
|
682
|
+
_, body = load_task_map(parent_dir)
|
|
683
|
+
stale_pack = _compute_stale_pack(parent_dir, data, body, children_by_id)
|
|
684
|
+
newly_ready = _compute_newly_ready(children_by_id)
|
|
542
685
|
|
|
543
686
|
stage_rows: list[dict] = []
|
|
544
687
|
for stage in stages:
|
|
@@ -597,6 +740,8 @@ def build_parent_status_dict(parent_dir: Path) -> dict | None:
|
|
|
597
740
|
"integrationQueue": data.get("integration_queue") or [],
|
|
598
741
|
"stageErrors": stage_errors,
|
|
599
742
|
"legacyStages": legacy,
|
|
743
|
+
"stalePack": stale_pack,
|
|
744
|
+
"newlyReady": newly_ready,
|
|
600
745
|
}
|
|
601
746
|
|
|
602
747
|
|
|
@@ -620,6 +765,17 @@ def build_parent_status(parent_dir: Path) -> str:
|
|
|
620
765
|
|
|
621
766
|
children_by_id = _children_by_id(data)
|
|
622
767
|
stages, stage_errors, legacy = _normalize_stages(data)
|
|
768
|
+
_, body = load_task_map(parent_dir)
|
|
769
|
+
stale_pack = _compute_stale_pack(parent_dir, data, body, children_by_id)
|
|
770
|
+
newly_ready = _compute_newly_ready(children_by_id)
|
|
771
|
+
lines.append("## Pack freshness")
|
|
772
|
+
lines.append("")
|
|
773
|
+
lines.append(f"- stalePack: {stale_pack}")
|
|
774
|
+
if newly_ready:
|
|
775
|
+
lines.append(f"- newlyReady: {', '.join(newly_ready)}")
|
|
776
|
+
else:
|
|
777
|
+
lines.append("- newlyReady: (none)")
|
|
778
|
+
lines.append("")
|
|
623
779
|
lines.append("## Stages")
|
|
624
780
|
lines.append("")
|
|
625
781
|
if stage_errors:
|
|
@@ -790,7 +946,7 @@ def build_review_report(
|
|
|
790
946
|
notes: str | None,
|
|
791
947
|
) -> tuple[str, list[str], dict]:
|
|
792
948
|
"""Build review summary and validate integration transitions for a decision."""
|
|
793
|
-
child_name = child_dir
|
|
949
|
+
child_name = resolve_child_map_id(parent_data, child_dir, child_data)
|
|
794
950
|
summary = _summarize_handoff(child_dir)
|
|
795
951
|
errors: list[str] = []
|
|
796
952
|
actions: dict = {"integrate": None, "gates": []}
|
|
@@ -138,10 +138,15 @@ def _cursor_step_for_route(
|
|
|
138
138
|
return (
|
|
139
139
|
f"使用 **codegraph_search**,路径侧重 `extensions/`,符号 `{symbol}`。"
|
|
140
140
|
)
|
|
141
|
+
if route_id == "definition-jump-native":
|
|
142
|
+
return (
|
|
143
|
+
f"**Prefer 原生**:用 **Grep** 定位 `{symbol}` 的定义行,再用 **Read** 验证文件与行号 "
|
|
144
|
+
"(Agent 无 GO_TO_DEFINITION)。仅当跨包 trap、重载歧义或需 blast 时再上 codegraph。"
|
|
145
|
+
)
|
|
141
146
|
if route_id == "lsp-navigation":
|
|
142
147
|
return (
|
|
143
|
-
f"
|
|
144
|
-
"的定义/引用(
|
|
148
|
+
f"**Own/codegraph**:用 **codegraph_explore** / **codegraph_search** 定位 `{symbol}` "
|
|
149
|
+
"的定义/引用(Grep+Read 不足或计划含 trap/blast 时);再用 **Read** 验证行号与正文。"
|
|
145
150
|
)
|
|
146
151
|
if role == "ast" or route_id in ("ast-codegraph",):
|
|
147
152
|
return (
|
|
@@ -294,7 +299,8 @@ def render_agent_instructions(
|
|
|
294
299
|
|
|
295
300
|
lines.append("")
|
|
296
301
|
lines.append(
|
|
297
|
-
"**
|
|
302
|
+
"**Prefer / Adapt / Own**:定义跳转 Prefer **Grep → Read**;诊断 **ReadLints**;"
|
|
303
|
+
"codegraph 独用调用链、跨包 trap、extension 符号、影响面,以及 Grep+Read 歧义时的定义/引用;"
|
|
298
304
|
"纯字面搜索用 Grep(勿用 GO_TO_DEFINITION,Agent 未暴露)。"
|
|
299
305
|
)
|
|
300
306
|
|
|
@@ -20,7 +20,9 @@ import subprocess
|
|
|
20
20
|
from pathlib import Path
|
|
21
21
|
|
|
22
22
|
from .active_task import resolve_context_key
|
|
23
|
-
from .
|
|
23
|
+
from .artifact_locale import artifact_locale_summary, resolve_artifact_locale
|
|
24
|
+
from .config import get_git_packages, get_journal_snippet_enabled
|
|
25
|
+
from .session_memory import get_latest_journal_summary
|
|
24
26
|
from .git import run_git
|
|
25
27
|
from .packages_context import get_packages_section
|
|
26
28
|
from .tasks import iter_active_tasks, load_task, get_all_statuses, children_progress
|
|
@@ -46,7 +48,7 @@ from .paths import (
|
|
|
46
48
|
# =============================================================================
|
|
47
49
|
|
|
48
50
|
_PACKAGE_NAME = "@blxzer/cursor-trellis"
|
|
49
|
-
_PYTHON_CMD = "
|
|
51
|
+
_PYTHON_CMD = "python"
|
|
50
52
|
_UPDATE_CHECK_TIMEOUT_SECONDS = 1.0
|
|
51
53
|
_VERSION_RE = re.compile(
|
|
52
54
|
r"^\s*(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:-([0-9A-Za-z.-]+))?\s*$"
|
|
@@ -797,6 +799,10 @@ def get_context_json(repo_root: Path | None = None) -> dict:
|
|
|
797
799
|
|
|
798
800
|
result = {
|
|
799
801
|
"developer": developer or "",
|
|
802
|
+
"artifactLocale": resolve_artifact_locale(
|
|
803
|
+
repo_root / selected_task if selected_task else None,
|
|
804
|
+
repo_root,
|
|
805
|
+
),
|
|
800
806
|
"git": {
|
|
801
807
|
"isRepo": root_git_info["isRepo"],
|
|
802
808
|
"branch": root_git_info["branch"],
|
|
@@ -819,9 +825,26 @@ def get_context_json(repo_root: Path | None = None) -> dict:
|
|
|
819
825
|
if pkg_git_info:
|
|
820
826
|
result["packageGit"] = pkg_git_info
|
|
821
827
|
|
|
828
|
+
if get_journal_snippet_enabled(repo_root):
|
|
829
|
+
result["journal"]["snippet"] = _resolve_journal_snippet(
|
|
830
|
+
repo_root,
|
|
831
|
+
journal_file,
|
|
832
|
+
developer,
|
|
833
|
+
)
|
|
834
|
+
|
|
822
835
|
return result
|
|
823
836
|
|
|
824
837
|
|
|
838
|
+
def _resolve_journal_snippet(
|
|
839
|
+
repo_root: Path,
|
|
840
|
+
journal_file: Path | None,
|
|
841
|
+
developer: str | None,
|
|
842
|
+
) -> str:
|
|
843
|
+
if not journal_file or not developer:
|
|
844
|
+
return ""
|
|
845
|
+
return get_latest_journal_summary(journal_file, repo_root, developer)
|
|
846
|
+
|
|
847
|
+
|
|
825
848
|
def output_json(repo_root: Path | None = None) -> None:
|
|
826
849
|
"""Output context in JSON format.
|
|
827
850
|
|
|
@@ -860,11 +883,12 @@ def get_context_text(repo_root: Path | None = None) -> str:
|
|
|
860
883
|
lines.append("## DEVELOPER")
|
|
861
884
|
if not developer:
|
|
862
885
|
lines.append(
|
|
863
|
-
f"ERROR: Not initialized. Run:
|
|
886
|
+
f"ERROR: Not initialized. Run: python ./{DIR_WORKFLOW}/{DIR_SCRIPTS}/init_developer.py <name>"
|
|
864
887
|
)
|
|
865
888
|
return "\n".join(lines)
|
|
866
889
|
|
|
867
890
|
lines.append(f"Name: {developer}")
|
|
891
|
+
lines.append(artifact_locale_summary(repo_root))
|
|
868
892
|
lines.append("")
|
|
869
893
|
|
|
870
894
|
root_git_info = _collect_root_git_info(repo_root)
|
|
@@ -966,6 +990,10 @@ def get_context_text(repo_root: Path | None = None) -> str:
|
|
|
966
990
|
lines.append(f"Line count: {journal_lines} / 2000")
|
|
967
991
|
if journal_lines > 1800:
|
|
968
992
|
lines.append("[!] WARNING: Approaching 2000 line limit!")
|
|
993
|
+
if get_journal_snippet_enabled(repo_root):
|
|
994
|
+
snippet = _resolve_journal_snippet(repo_root, journal_file, developer)
|
|
995
|
+
if snippet:
|
|
996
|
+
lines.append(f"Recent snippet: {snippet}")
|
|
969
997
|
else:
|
|
970
998
|
lines.append("No journal file found")
|
|
971
999
|
lines.append("")
|
|
@@ -1085,7 +1113,7 @@ def get_context_text_record(repo_root: Path | None = None) -> str:
|
|
|
1085
1113
|
developer = get_developer(repo_root)
|
|
1086
1114
|
if not developer:
|
|
1087
1115
|
lines.append(
|
|
1088
|
-
f"ERROR: Not initialized. Run:
|
|
1116
|
+
f"ERROR: Not initialized. Run: python ./{DIR_WORKFLOW}/{DIR_SCRIPTS}/init_developer.py <name>"
|
|
1089
1117
|
)
|
|
1090
1118
|
return "\n".join(lines)
|
|
1091
1119
|
|