@blxzer/cursor-trellis 0.3.5 → 0.4.0

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 (156) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/README.md +13 -0
  3. package/dist/cli/index.d.ts.map +1 -1
  4. package/dist/cli/index.js +3 -0
  5. package/dist/cli/index.js.map +1 -1
  6. package/dist/commands/goal/index.d.ts +3 -0
  7. package/dist/commands/goal/index.d.ts.map +1 -0
  8. package/dist/commands/goal/index.js +159 -0
  9. package/dist/commands/goal/index.js.map +1 -0
  10. package/dist/commands/goal/review.d.ts +6 -0
  11. package/dist/commands/goal/review.d.ts.map +1 -0
  12. package/dist/commands/goal/review.js +28 -0
  13. package/dist/commands/goal/review.js.map +1 -0
  14. package/dist/commands/sdk/run.d.ts.map +1 -1
  15. package/dist/commands/sdk/run.js +2 -27
  16. package/dist/commands/sdk/run.js.map +1 -1
  17. package/dist/commands/update.d.ts.map +1 -1
  18. package/dist/commands/update.js +5 -1
  19. package/dist/commands/update.js.map +1 -1
  20. package/dist/configurators/workflow.d.ts.map +1 -1
  21. package/dist/configurators/workflow.js +51 -2
  22. package/dist/configurators/workflow.js.map +1 -1
  23. package/dist/constants/paths.d.ts +4 -0
  24. package/dist/constants/paths.d.ts.map +1 -1
  25. package/dist/constants/paths.js +4 -0
  26. package/dist/constants/paths.js.map +1 -1
  27. package/dist/goal/action-packet.d.ts +89 -0
  28. package/dist/goal/action-packet.d.ts.map +1 -0
  29. package/dist/goal/action-packet.js +113 -0
  30. package/dist/goal/action-packet.js.map +1 -0
  31. package/dist/goal/audit.d.ts +14 -0
  32. package/dist/goal/audit.d.ts.map +1 -0
  33. package/dist/goal/audit.js +12 -0
  34. package/dist/goal/audit.js.map +1 -0
  35. package/dist/goal/constants.d.ts +26 -0
  36. package/dist/goal/constants.d.ts.map +1 -0
  37. package/dist/goal/constants.js +24 -0
  38. package/dist/goal/constants.js.map +1 -0
  39. package/dist/goal/paths.d.ts +8 -0
  40. package/dist/goal/paths.d.ts.map +1 -0
  41. package/dist/goal/paths.js +36 -0
  42. package/dist/goal/paths.js.map +1 -0
  43. package/dist/goal/preflight.d.ts +21 -0
  44. package/dist/goal/preflight.d.ts.map +1 -0
  45. package/dist/goal/preflight.js +97 -0
  46. package/dist/goal/preflight.js.map +1 -0
  47. package/dist/goal/review-seam.d.ts +44 -0
  48. package/dist/goal/review-seam.d.ts.map +1 -0
  49. package/dist/goal/review-seam.js +153 -0
  50. package/dist/goal/review-seam.js.map +1 -0
  51. package/dist/goal/reviewer-rules.d.ts +13 -0
  52. package/dist/goal/reviewer-rules.d.ts.map +1 -0
  53. package/dist/goal/reviewer-rules.js +153 -0
  54. package/dist/goal/reviewer-rules.js.map +1 -0
  55. package/dist/goal/reviewer.d.ts +9 -0
  56. package/dist/goal/reviewer.d.ts.map +1 -0
  57. package/dist/goal/reviewer.js +54 -0
  58. package/dist/goal/reviewer.js.map +1 -0
  59. package/dist/goal/runtime.d.ts +32 -0
  60. package/dist/goal/runtime.d.ts.map +1 -0
  61. package/dist/goal/runtime.js +242 -0
  62. package/dist/goal/runtime.js.map +1 -0
  63. package/dist/goal/sdk-client.d.ts +10 -0
  64. package/dist/goal/sdk-client.d.ts.map +1 -0
  65. package/dist/goal/sdk-client.js +34 -0
  66. package/dist/goal/sdk-client.js.map +1 -0
  67. package/dist/goal/state.d.ts +40 -0
  68. package/dist/goal/state.d.ts.map +1 -0
  69. package/dist/goal/state.js +37 -0
  70. package/dist/goal/state.js.map +1 -0
  71. package/dist/goal/task-triggers.d.ts +15 -0
  72. package/dist/goal/task-triggers.d.ts.map +1 -0
  73. package/dist/goal/task-triggers.js +23 -0
  74. package/dist/goal/task-triggers.js.map +1 -0
  75. package/dist/goal/walls.d.ts +12 -0
  76. package/dist/goal/walls.d.ts.map +1 -0
  77. package/dist/goal/walls.js +65 -0
  78. package/dist/goal/walls.js.map +1 -0
  79. package/dist/goal/worker-prompt.d.ts +13 -0
  80. package/dist/goal/worker-prompt.d.ts.map +1 -0
  81. package/dist/goal/worker-prompt.js +41 -0
  82. package/dist/goal/worker-prompt.js.map +1 -0
  83. package/dist/goal/worker.d.ts +42 -0
  84. package/dist/goal/worker.d.ts.map +1 -0
  85. package/dist/goal/worker.js +180 -0
  86. package/dist/goal/worker.js.map +1 -0
  87. package/dist/migrations/manifests/0.3.5.json +9 -0
  88. package/dist/migrations/manifests/0.3.6.json +9 -0
  89. package/dist/migrations/manifests/0.4.0.json +9 -0
  90. package/dist/templates/common/commands/continue.md +15 -3
  91. package/dist/templates/common/skills/check.md +7 -4
  92. package/dist/templates/cursor/agents/cstl-check.md +1 -0
  93. package/dist/templates/cursor/fixtures/expected-rules.d.ts.map +1 -1
  94. package/dist/templates/cursor/fixtures/expected-rules.js +6 -1
  95. package/dist/templates/cursor/fixtures/expected-rules.js.map +1 -1
  96. package/dist/templates/cursor/rules/cstl-cursor-modes.mdc +32 -0
  97. package/dist/templates/cursor/rules/cstl-triage.mdc +2 -4
  98. package/dist/templates/cursor/rules/retrieval-routing.mdc +26 -7
  99. package/dist/templates/markdown/index.d.ts +9 -0
  100. package/dist/templates/markdown/index.d.ts.map +1 -1
  101. package/dist/templates/markdown/index.js +9 -0
  102. package/dist/templates/markdown/index.js.map +1 -1
  103. package/dist/templates/markdown/spec/guides/artifact-locale-guide.md.txt +93 -0
  104. package/dist/templates/markdown/spec/guides/cross-platform-thinking-guide.md.txt +7 -7
  105. package/dist/templates/markdown/spec/guides/cursor-context-injection-guide.md.txt +38 -21
  106. package/dist/templates/markdown/spec/guides/cursor-native-modes-guide.md.txt +125 -0
  107. package/dist/templates/markdown/spec/guides/cursor-subagent-policy.md.txt +10 -8
  108. package/dist/templates/markdown/spec/guides/debug-loop-guide.md.txt +227 -0
  109. package/dist/templates/markdown/spec/guides/goal-release-regression-runbook.md.txt +132 -0
  110. package/dist/templates/markdown/spec/guides/index.md.txt +144 -104
  111. package/dist/templates/markdown/spec/guides/injection-budget-guide.md.txt +111 -0
  112. package/dist/templates/markdown/spec/guides/prototype-guide.md.txt +139 -0
  113. package/dist/templates/markdown/spec/guides/retrieval-daily-guide.md.txt +28 -6
  114. package/dist/templates/markdown/spec/guides/test-discipline-guide.md.txt +138 -0
  115. package/dist/templates/markdown/spec/guides/verification-strength-guide.md.txt +185 -0
  116. package/dist/templates/shared-hooks/session-start.py +10 -0
  117. package/dist/templates/trellis/config.yaml +18 -0
  118. package/dist/templates/trellis/index.d.ts +17 -0
  119. package/dist/templates/trellis/index.d.ts.map +1 -1
  120. package/dist/templates/trellis/index.js +40 -0
  121. package/dist/templates/trellis/index.js.map +1 -1
  122. package/dist/templates/trellis/pool/README.md +103 -0
  123. package/dist/templates/trellis/pool/items/.gitkeep +0 -0
  124. package/dist/templates/trellis/pool/plan.md +26 -0
  125. package/dist/templates/trellis/scripts/common/artifact_locale.py +278 -0
  126. package/dist/templates/trellis/scripts/common/codebase_retrieval_router.py +37 -2
  127. package/dist/templates/trellis/scripts/common/config.py +39 -0
  128. package/dist/templates/trellis/scripts/common/injection_budget.py +317 -0
  129. package/dist/templates/trellis/scripts/common/parent_orchestration.py +161 -5
  130. package/dist/templates/trellis/scripts/common/pool_store.py +702 -0
  131. package/dist/templates/trellis/scripts/common/retrieval_agent_instructions.py +9 -3
  132. package/dist/templates/trellis/scripts/common/session_context.py +32 -4
  133. package/dist/templates/trellis/scripts/common/session_memory.py +26 -0
  134. package/dist/templates/trellis/scripts/common/subagent_dispatch.py +74 -9
  135. package/dist/templates/trellis/scripts/common/task_context.py +18 -0
  136. package/dist/templates/trellis/scripts/common/task_dashboard.py +24 -1
  137. package/dist/templates/trellis/scripts/common/task_dependencies.py +673 -0
  138. package/dist/templates/trellis/scripts/common/task_gates.py +58 -5
  139. package/dist/templates/trellis/scripts/common/task_map.py +40 -8
  140. package/dist/templates/trellis/scripts/common/task_store.py +400 -39
  141. package/dist/templates/trellis/scripts/common/test_depends_mode_block.py +489 -0
  142. package/dist/templates/trellis/scripts/common/test_observable_defaults.py +135 -0
  143. package/dist/templates/trellis/scripts/common/test_pool_store.py +428 -0
  144. package/dist/templates/trellis/scripts/common/test_task_dependencies.py +345 -0
  145. package/dist/templates/trellis/scripts/cursor_retrieval_probe_prompt.md +14 -12
  146. package/dist/templates/trellis/scripts/generate_dispatch_prompt.py +182 -0
  147. package/dist/templates/trellis/scripts/injection_budget_probe.py +69 -0
  148. package/dist/templates/trellis/scripts/pool.py +192 -0
  149. package/dist/templates/trellis/scripts/retrieval_probe_matrix_template.json +3 -3
  150. package/dist/templates/trellis/scripts/spec_health_outcomes.py +160 -0
  151. package/dist/templates/trellis/scripts/task.py +85 -1
  152. package/dist/templates/trellis/scripts/verify_evidence_probe.py +138 -0
  153. package/dist/templates/trellis/tasks/locale/en/default-prd.md +19 -0
  154. package/dist/templates/trellis/tasks/locale/zh/default-prd.md +19 -0
  155. package/dist/templates/trellis/workflow.md +55 -5
  156. package/package.json +2 -2
@@ -0,0 +1,345 @@
1
+ #!/usr/bin/env python3
2
+ """Tests for Plan A task-level depends_on resolution (declare + soft checks)."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import json
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ import pytest
11
+
12
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
13
+
14
+ from common.task_dependencies import (
15
+ KIND_CHILD,
16
+ KIND_MISSING,
17
+ KIND_POOL,
18
+ KIND_TASK,
19
+ NOT_SATISFIED,
20
+ SATISFIED,
21
+ UNRESOLVED,
22
+ dashboard_deps_line,
23
+ describe_dependencies,
24
+ find_dependency_cycles,
25
+ normalize_dep_list,
26
+ resolve_dep_ref,
27
+ satisfaction_status,
28
+ )
29
+
30
+
31
+ def _write_task(tasks_dir: Path, dir_name: str, status: str, depends_on=None) -> Path:
32
+ task_dir = tasks_dir / dir_name
33
+ task_dir.mkdir(parents=True, exist_ok=True)
34
+ data = {
35
+ "id": dir_name,
36
+ "name": dir_name,
37
+ "title": dir_name,
38
+ "status": status,
39
+ }
40
+ if depends_on is not None:
41
+ data["depends_on"] = depends_on
42
+ (task_dir / "task.json").write_text(json.dumps(data), encoding="utf-8")
43
+ return task_dir
44
+
45
+
46
+ def _tasks_fixture(tmp_path: Path) -> Path:
47
+ tasks_dir = tmp_path / "repo" / ".cstl" / "tasks"
48
+ tasks_dir.mkdir(parents=True)
49
+ return tasks_dir
50
+
51
+
52
+ def test_completed_task_is_satisfied(tmp_path: Path) -> None:
53
+ tasks_dir = _tasks_fixture(tmp_path)
54
+ _write_task(tasks_dir, "dep-a", "completed")
55
+ task_dir = _write_task(tasks_dir, "task-x", "planning", ["dep-a"])
56
+
57
+ report = describe_dependencies(task_dir, {"depends_on": ["dep-a"]}, tasks_dir=tasks_dir)
58
+ dep = report.deps[0]
59
+ assert dep.kind == KIND_TASK
60
+ assert satisfaction_status(dep)[0] == SATISFIED
61
+ assert report.warnings() == []
62
+
63
+
64
+ def test_archived_task_counts_as_completed(tmp_path: Path) -> None:
65
+ tasks_dir = _tasks_fixture(tmp_path)
66
+ archive_dir = tasks_dir / "archive" / "2026-08"
67
+ archive_dir.mkdir(parents=True)
68
+ # Anomaly: archived task.json still says planning — location is the rule.
69
+ _write_task(archive_dir, "dep-archived", "planning")
70
+ task_dir = _write_task(tasks_dir, "task-x", "planning", ["dep-archived"])
71
+
72
+ dep = resolve_dep_ref("dep-archived", tasks_dir=tasks_dir)
73
+ assert dep.kind == KIND_TASK
74
+ assert dep.is_archived
75
+ status, note = satisfaction_status(dep)
76
+ assert status == SATISFIED
77
+ assert note == "archived task counts as completed"
78
+
79
+
80
+ def test_cancelled_task_satisfied_with_cancelled_warning(tmp_path: Path) -> None:
81
+ tasks_dir = _tasks_fixture(tmp_path)
82
+ _write_task(tasks_dir, "dep-c", "cancelled")
83
+ task_dir = _write_task(tasks_dir, "task-x", "planning", ["dep-c"])
84
+
85
+ report = describe_dependencies(task_dir, {"depends_on": ["dep-c"]}, tasks_dir=tasks_dir)
86
+ dep = report.deps[0]
87
+ assert dep.is_cancelled
88
+ assert satisfaction_status(dep)[0] == SATISFIED
89
+ assert any("cancelled" in w for w in report.warnings())
90
+
91
+
92
+ def test_in_progress_dependency_is_not_satisfied(tmp_path: Path) -> None:
93
+ tasks_dir = _tasks_fixture(tmp_path)
94
+ _write_task(tasks_dir, "dep-b", "in_progress")
95
+ task_dir = _write_task(tasks_dir, "task-x", "planning", ["dep-b"])
96
+
97
+ report = describe_dependencies(task_dir, {"depends_on": ["dep-b"]}, tasks_dir=tasks_dir)
98
+ dep = report.deps[0]
99
+ assert satisfaction_status(dep)[0] == NOT_SATISFIED
100
+ assert any("not satisfied" in w for w in report.warnings())
101
+
102
+
103
+ def test_dangling_reference_is_unresolved_warning(tmp_path: Path) -> None:
104
+ tasks_dir = _tasks_fixture(tmp_path)
105
+ task_dir = _write_task(tasks_dir, "task-x", "planning", ["ghost-task"])
106
+
107
+ dep = resolve_dep_ref("ghost-task", tasks_dir=tasks_dir)
108
+ assert dep.kind == KIND_MISSING
109
+ assert satisfaction_status(dep)[0] == UNRESOLVED
110
+ report = describe_dependencies(task_dir, {"depends_on": ["ghost-task"]}, tasks_dir=tasks_dir)
111
+ assert any("dangling" in w for w in report.warnings())
112
+
113
+
114
+ def test_pool_prefix_is_unresolved_and_never_falls_through(tmp_path: Path) -> None:
115
+ tasks_dir = _tasks_fixture(tmp_path)
116
+ task_dir = _write_task(tasks_dir, "task-x", "planning", ["pool:P09"])
117
+
118
+ dep = resolve_dep_ref("pool:P09", tasks_dir=tasks_dir)
119
+ assert dep.kind == KIND_POOL
120
+ assert satisfaction_status(dep)[0] == UNRESOLVED
121
+ report = describe_dependencies(task_dir, {"depends_on": ["pool:P09"]}, tasks_dir=tasks_dir)
122
+ assert any("unresolved" in w for w in report.warnings())
123
+
124
+
125
+ def test_cycle_detected_across_tasks(tmp_path: Path) -> None:
126
+ tasks_dir = _tasks_fixture(tmp_path)
127
+ task_a = _write_task(tasks_dir, "task-a", "planning", ["task-b"])
128
+ _write_task(tasks_dir, "task-b", "planning", ["task-a"])
129
+
130
+ report = describe_dependencies(task_a, {"depends_on": ["task-b"]}, tasks_dir=tasks_dir)
131
+ assert len(report.cycles) == 1
132
+ assert report.cycles[0][0] == report.cycles[0][-1]
133
+ assert any("cycle" in w for w in report.warnings())
134
+
135
+
136
+ def test_self_reference_cycle_detected(tmp_path: Path) -> None:
137
+ tasks_dir = _tasks_fixture(tmp_path)
138
+ task_dir = _write_task(tasks_dir, "task-x", "planning", ["task-x"])
139
+
140
+ report = describe_dependencies(task_dir, {"depends_on": ["task-x"]}, tasks_dir=tasks_dir)
141
+ assert report.cycles == [["task-x", "task-x"]]
142
+
143
+
144
+ def test_find_dependency_cycles_pure(tmp_path: Path) -> None:
145
+ graph = {"a": ["b"], "b": ["c"], "c": ["a"], "d": []}
146
+ cycles = find_dependency_cycles(graph)
147
+ assert len(cycles) == 1
148
+ cycle = cycles[0]
149
+ assert cycle[0] == cycle[-1]
150
+ assert set(cycle) == {"a", "b", "c"}
151
+
152
+
153
+ def test_scope_child_wins_over_global_task_with_same_name(tmp_path: Path) -> None:
154
+ tasks_dir = _tasks_fixture(tmp_path)
155
+ # Global task named "shared" that IS completed — would be satisfied globally.
156
+ _write_task(tasks_dir, "shared", "completed")
157
+ task_dir = _write_task(tasks_dir, "task-x", "planning", ["shared"])
158
+ scope = {"shared": "open"}
159
+
160
+ dep = resolve_dep_ref("shared", tasks_dir=tasks_dir, scope_children=scope)
161
+ assert dep.kind == KIND_CHILD
162
+ assert dep.source == "task-map"
163
+ assert satisfaction_status(dep)[0] == NOT_SATISFIED
164
+
165
+ report = describe_dependencies(
166
+ task_dir, {"depends_on": ["shared"]}, tasks_dir=tasks_dir, scope_children=scope
167
+ )
168
+ assert report.deps[0].kind == KIND_CHILD
169
+ assert any("not satisfied" in w for w in report.warnings())
170
+
171
+
172
+ def test_child_integrated_is_satisfied_via_scope(tmp_path: Path) -> None:
173
+ tasks_dir = _tasks_fixture(tmp_path)
174
+ task_dir = _write_task(tasks_dir, "task-x", "planning", ["child-a"])
175
+ scope = {"child-a": "integrated", "child-b": "cancelled"}
176
+
177
+ dep = resolve_dep_ref("child-a", tasks_dir=tasks_dir, scope_children=scope)
178
+ assert dep.kind == KIND_CHILD
179
+ assert satisfaction_status(dep)[0] == SATISFIED
180
+
181
+ report = describe_dependencies(
182
+ task_dir, {"depends_on": ["child-a", "child-b"]}, tasks_dir=tasks_dir, scope_children=scope
183
+ )
184
+ assert satisfaction_status(report.deps[0])[0] == SATISFIED
185
+ assert report.deps[1].is_cancelled
186
+ assert any("cancelled" in w for w in report.warnings())
187
+
188
+
189
+ def test_empty_depends_on_has_no_warnings_or_dashboard_line(tmp_path: Path) -> None:
190
+ tasks_dir = _tasks_fixture(tmp_path)
191
+ task_dir = _write_task(tasks_dir, "task-x", "planning")
192
+ data = {"id": "task-x", "status": "planning"}
193
+
194
+ report = describe_dependencies(task_dir, data, tasks_dir=tasks_dir)
195
+ assert not report.has_deps()
196
+ assert report.warnings() == []
197
+ assert dashboard_deps_line(task_dir, data, tasks_dir=tasks_dir) is None
198
+
199
+
200
+ def test_dashboard_deps_line_renders_badges(tmp_path: Path) -> None:
201
+ tasks_dir = _tasks_fixture(tmp_path)
202
+ _write_task(tasks_dir, "dep-done", "completed")
203
+ _write_task(tasks_dir, "dep-wait", "in_progress")
204
+ _write_task(tasks_dir, "dep-gone", "cancelled")
205
+ task_dir = _write_task(
206
+ tasks_dir, "task-x", "planning", ["dep-done", "dep-wait", "dep-gone", "pool:P09"]
207
+ )
208
+ data = {
209
+ "id": "task-x",
210
+ "status": "planning",
211
+ "depends_on": ["dep-done", "dep-wait", "dep-gone", "pool:P09"],
212
+ }
213
+
214
+ line = dashboard_deps_line(task_dir, data, tasks_dir=tasks_dir)
215
+ assert line is not None
216
+ assert "dep-done ✅" in line
217
+ assert "dep-wait ⏳" in line
218
+ assert "dep-gone ✅已取消" in line
219
+ assert "pool:P09 ⚠️" in line
220
+
221
+
222
+ def test_normalize_dep_list_dedupes_and_strips(tmp_path: Path) -> None:
223
+ assert normalize_dep_list(None) == []
224
+ assert normalize_dep_list(["a", " a ", "b", "a", "", 5]) == ["a", "b"]
225
+ assert normalize_dep_list("not-a-list") == []
226
+
227
+
228
+ # =============================================================================
229
+ # pool: real status (08-09-pool-links-and-cli)
230
+ # =============================================================================
231
+
232
+ def _pool_repo(tmp_path: Path, linked_tasks: list[str] | None) -> Path:
233
+ repo = tmp_path / "repo"
234
+ items_dir = repo / ".cstl" / "pool" / "items"
235
+ items_dir.mkdir(parents=True)
236
+ lines = ["id: P09", "title: t", "status: accepted", "type: mechanism"]
237
+ if linked_tasks:
238
+ lines.append("linked_tasks:")
239
+ lines += [f" - {task}" for task in linked_tasks]
240
+ (items_dir / "P09.md").write_text(
241
+ "---\n" + "\n".join(lines) + "\n---\n\n## 意图\nx\n",
242
+ encoding="utf-8",
243
+ )
244
+ return repo
245
+
246
+
247
+ def test_pool_missing_item_is_unresolved(tmp_path: Path) -> None:
248
+ tasks_dir = _tasks_fixture(tmp_path)
249
+ task_dir = _write_task(tasks_dir, "task-x", "planning", ["pool:P09"])
250
+
251
+ dep = resolve_dep_ref("pool:P09", tasks_dir=tasks_dir)
252
+ assert dep.kind == KIND_POOL
253
+ status, note = satisfaction_status(dep)
254
+ assert status == UNRESOLVED
255
+ assert "not found" in (note or "")
256
+ report = describe_dependencies(task_dir, {"depends_on": ["pool:P09"]}, tasks_dir=tasks_dir)
257
+ assert any("unresolved" in w for w in report.warnings())
258
+
259
+
260
+ def test_pool_unlinked_is_unresolved(tmp_path: Path) -> None:
261
+ repo = _pool_repo(tmp_path, linked_tasks=None)
262
+ tasks_dir = repo / ".cstl" / "tasks"
263
+ task_dir = _write_task(tasks_dir, "task-x", "planning", ["pool:P09"])
264
+
265
+ dep = resolve_dep_ref("pool:P09", tasks_dir=tasks_dir)
266
+ assert dep.kind == KIND_POOL
267
+ status, note = satisfaction_status(dep)
268
+ assert status == UNRESOLVED
269
+ assert "no linked tasks" in (note or "")
270
+
271
+
272
+ def test_pool_linked_planning_task_is_not_satisfied(tmp_path: Path) -> None:
273
+ repo = _pool_repo(tmp_path, linked_tasks=["task-a"])
274
+ tasks_dir = repo / ".cstl" / "tasks"
275
+ _write_task(tasks_dir, "task-a", "planning")
276
+ task_dir = _write_task(tasks_dir, "task-x", "planning", ["pool:P09"])
277
+
278
+ dep = resolve_dep_ref("pool:P09", tasks_dir=tasks_dir)
279
+ assert dep.status == "not_satisfied"
280
+ status, note = satisfaction_status(dep)
281
+ assert status == NOT_SATISFIED
282
+ assert "not yet completed" in (note or "")
283
+ assert "task-a" in (note or "")
284
+ report = describe_dependencies(task_dir, {"depends_on": ["pool:P09"]}, tasks_dir=tasks_dir)
285
+ assert any("not satisfied" in w for w in report.warnings())
286
+
287
+
288
+ def test_pool_linked_completed_task_is_satisfied(tmp_path: Path) -> None:
289
+ repo = _pool_repo(tmp_path, linked_tasks=["task-a"])
290
+ tasks_dir = repo / ".cstl" / "tasks"
291
+ _write_task(tasks_dir, "task-a", "completed")
292
+ task_dir = _write_task(tasks_dir, "task-x", "planning", ["pool:P09"])
293
+
294
+ dep = resolve_dep_ref("pool:P09", tasks_dir=tasks_dir)
295
+ assert dep.status == "satisfied"
296
+ status, _ = satisfaction_status(dep)
297
+ assert status == SATISFIED
298
+ report = describe_dependencies(task_dir, {"depends_on": ["pool:P09"]}, tasks_dir=tasks_dir)
299
+ assert report.warnings() == []
300
+
301
+
302
+ def test_pool_multilink_partial_then_complete(tmp_path: Path) -> None:
303
+ repo = _pool_repo(tmp_path, linked_tasks=["task-a", "task-b"])
304
+ tasks_dir = repo / ".cstl" / "tasks"
305
+ _write_task(tasks_dir, "task-a", "completed")
306
+ _write_task(tasks_dir, "task-b", "in_progress")
307
+ task_dir = _write_task(tasks_dir, "task-x", "planning", ["pool:P09"])
308
+
309
+ dep = resolve_dep_ref("pool:P09", tasks_dir=tasks_dir)
310
+ status, note = satisfaction_status(dep)
311
+ assert status == NOT_SATISFIED
312
+ assert "task-a, task-b" in (note or "")
313
+
314
+ _write_task(tasks_dir, "task-b", "completed")
315
+ dep = resolve_dep_ref("pool:P09", tasks_dir=tasks_dir)
316
+ assert satisfaction_status(dep)[0] == SATISFIED
317
+
318
+
319
+ def test_pool_all_dangling_linked_tasks_is_unresolved(tmp_path: Path) -> None:
320
+ repo = _pool_repo(tmp_path, linked_tasks=["ghost-a"])
321
+ tasks_dir = repo / ".cstl" / "tasks"
322
+ task_dir = _write_task(tasks_dir, "task-x", "planning", ["pool:P09"])
323
+
324
+ dep = resolve_dep_ref("pool:P09", tasks_dir=tasks_dir)
325
+ assert dep.status == "unresolved"
326
+ status, note = satisfaction_status(dep)
327
+ assert status == UNRESOLVED
328
+ report = describe_dependencies(task_dir, {"depends_on": ["pool:P09"]}, tasks_dir=tasks_dir)
329
+ assert any("unresolved" in w for w in report.warnings())
330
+
331
+
332
+ def test_pool_cancelled_linked_task_is_satisfied_with_cancelled_note(tmp_path: Path) -> None:
333
+ repo = _pool_repo(tmp_path, linked_tasks=["task-a"])
334
+ tasks_dir = repo / ".cstl" / "tasks"
335
+ _write_task(tasks_dir, "task-a", "cancelled")
336
+ task_dir = _write_task(tasks_dir, "task-x", "planning", ["pool:P09"])
337
+
338
+ dep = resolve_dep_ref("pool:P09", tasks_dir=tasks_dir)
339
+ assert dep.status == "cancelled"
340
+ assert dep.is_cancelled
341
+ status, note = satisfaction_status(dep)
342
+ assert status == SATISFIED
343
+ assert "cancelled" in (note or "")
344
+ report = describe_dependencies(task_dir, {"depends_on": ["pool:P09"]}, tasks_dir=tasks_dir)
345
+ assert any("cancelled" in w for w in report.warnings())
@@ -251,36 +251,38 @@ verdict rules:
251
251
 
252
252
  ---
253
253
 
254
- --- PROBE P-10: Definition / reference (codegraph Agent LSP not exposed)
254
+ --- PROBE P-10: Definition / reference (Prefer Grep+Read; codegraph when ambiguous)
255
255
 
256
256
  Cursor Agent does **not** expose GO_TO_DEFINITION in the tool table (Native/BYOK).
257
- Trellis routes definition/reference to **codegraph**. This probe measures the
258
- **product path**, not raw LSP.
257
+ Trellis **Prefers** native **Grep → Read** for named-symbol definition; **codegraph**
258
+ is the **Own** fallback when Grep is ambiguous or structural context is required.
259
259
 
260
- You are a retrieval capability probe. Use **codegraph_node** or **codegraph_search**
261
- as the **primary** path. Do NOT use Grep as the first step. Use **Read** only to
262
- confirm line numbers after codegraph returns a definition.
260
+ You are a retrieval capability probe.
263
261
 
264
262
  Task: Locate the **definition** of `route_codebase_retrieval` (Python function).
265
263
 
264
+ **Step 1 (Prefer):** Use **Grep** for `def route_codebase_retrieval` — do NOT use codegraph first.
265
+ **Step 2:** Use **Read** to confirm the definition line and body start.
266
+ **Step 3 (only if Grep ambiguous):** codegraph_explore / codegraph_search.
267
+
266
268
  Expected: definition in `.cstl/scripts/common/codebase_retrieval_router.py`
267
- (`def route_codebase_retrieval(`).
269
+ (`def route_codebase_retrieval(` at line ~805).
268
270
 
269
271
  Reply in this exact format:
270
272
 
271
273
  ```
272
274
  P-10 RESULT
273
- tool_invoked: <exact tool name codegraph_node, codegraph_search, or "none">
275
+ tool_invoked: <primary tool — Grep, Read, codegraph_explore, or "none">
274
276
  definition_file: <path>
275
277
  definition_line: <line number or "unknown">
276
278
  verdict: pass|fail|environment_limitation
277
- notes: <if you tried GO_TO_DEFINITION and it was unavailable, say so; pass when codegraph + Read confirm def>
279
+ notes: <GO_TO_DEFINITION availability; did Grep+Read succeed before any codegraph?>
278
280
  ```
279
281
 
280
282
  verdict rules:
281
- - **pass**: codegraph (or equivalent MCP) returned the correct `def route_codebase_retrieval` file.
282
- - **environment_limitation**: only when codegraph MCP/index is missing; not when LSP is missing.
283
- - **fail**: wrong file or no structural tool executed.
283
+ - **pass**: Grep (+ Read) returned the correct `def route_codebase_retrieval` file; codegraph optional.
284
+ - **environment_limitation**: only when Grep/Read are unavailable in the host (not when LSP is missing).
285
+ - **fail**: wrong file, or codegraph used as first step without Grep attempt.
284
286
 
285
287
  ---
286
288
 
@@ -0,0 +1,182 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ CLI Layer 2 — PRIMARY (guaranteed) context path for cstl-* Task dispatch.
4
+
5
+ Usage:
6
+ python ./.cstl/scripts/generate_dispatch_prompt.py --agent research
7
+ python ./.cstl/scripts/generate_dispatch_prompt.py --agent implement --task .cstl/tasks/06-23-subagent-smoke
8
+ python ./.cstl/scripts/generate_dispatch_prompt.py --agent research --repo-root d:/MyHarness
9
+
10
+ Outputs the complete dispatch prompt to stdout. The main session MUST capture
11
+ this output and pass it as the `prompt` parameter to
12
+ `Task(subagent_type=cstl-*, prompt=<generated>)`. This is the ONLY
13
+ guaranteed context channel for custom Task subagents on Cursor.
14
+
15
+ Hook injection (`preToolUse` / `additional_context`) is best-effort only
16
+ (Cursor #158452; hook does not fire for Task tool in Cursor 3.8.22). Do not
17
+ treat hook-only injection as a substitute for this CLI path.
18
+
19
+ The generated prompt carries:
20
+ - Role identity ("You are the Trellis Research/Implement/Check Agent")
21
+ - Write scope constraints (Write ALLOWED / Write FORBIDDEN)
22
+ - Recursion guard
23
+ - Dynamic context (prd.md, design.md, implement.md, spec tree, jsonl files)
24
+
25
+ Task path resolution:
26
+ - `--task <path>` is the **primary fallback** when no selected-task pointer exists
27
+ (e.g. `task.py select` failed due to missing session identity).
28
+ - When `--task` is omitted, auto-detect via selected pointer is **best-effort** only —
29
+ dispatch scripts do not hard-depend on a persisted selection.
30
+ """
31
+ from __future__ import annotations
32
+
33
+ import argparse
34
+ import sys
35
+ from pathlib import Path
36
+
37
+
38
+ def main() -> int:
39
+ parser = argparse.ArgumentParser(
40
+ description="Generate a complete Trellis subagent dispatch prompt."
41
+ )
42
+ parser.add_argument(
43
+ "--agent",
44
+ required=True,
45
+ choices=["research", "implement", "check"],
46
+ help="Subagent role to generate prompt for.",
47
+ )
48
+ parser.add_argument(
49
+ "--task",
50
+ default=None,
51
+ help="Task directory path (absolute or relative to repo root). "
52
+ "Primary fallback when no selected-task pointer exists. "
53
+ "If omitted, auto-detect via selected pointer is best-effort only.",
54
+ )
55
+ parser.add_argument(
56
+ "--repo-root",
57
+ default=None,
58
+ help="Repository root path. If omitted, auto-detect via .cstl or .git.",
59
+ )
60
+ parser.add_argument(
61
+ "--finish",
62
+ action="store_true",
63
+ help="Use finish context (check agent only, final pre-PR verification).",
64
+ )
65
+ parser.add_argument(
66
+ "--max-chars",
67
+ type=int,
68
+ default=None,
69
+ help="Truncate injected context at this character count. "
70
+ "When omitted, uses role default from injection-budget-guide.",
71
+ )
72
+ args = parser.parse_args()
73
+
74
+ # Resolve repo root
75
+ if args.repo_root:
76
+ repo_root = Path(args.repo_root).resolve()
77
+ else:
78
+ repo_root = _find_repo_root(Path.cwd())
79
+ if not repo_root:
80
+ print("ERROR: could not find repo root (no .cstl or .git upward)", file=sys.stderr)
81
+ return 1
82
+
83
+ from common.paths import DIR_WORKFLOW # type: ignore[import-not-found]
84
+
85
+ # Make scripts importable — workspace root first (subagent_dispatch.py lives there),
86
+ # then repo_root's own scripts (may be a subproject with different common/)
87
+ workspace_root = _find_workspace_root(repo_root, DIR_WORKFLOW)
88
+ if workspace_root and workspace_root != repo_root:
89
+ ws_scripts = workspace_root / DIR_WORKFLOW / "scripts"
90
+ if str(ws_scripts) not in sys.path:
91
+ sys.path.insert(0, str(ws_scripts))
92
+
93
+ scripts_dir = repo_root / DIR_WORKFLOW / "scripts"
94
+ if str(scripts_dir) not in sys.path:
95
+ sys.path.insert(1, str(scripts_dir))
96
+
97
+ from common.subagent_dispatch import build_dispatch_prompt # type: ignore[import-not-found]
98
+ from common.injection_budget import DISPATCH_DEFAULT_MAX_CHARS # type: ignore[import-not-found]
99
+
100
+ # Resolve task directory
101
+ if args.task:
102
+ task_dir = Path(args.task)
103
+ if not task_dir.is_absolute():
104
+ task_dir = repo_root / task_dir
105
+ task_dir = task_dir.resolve()
106
+ else:
107
+ task_dir = _resolve_selected_task(repo_root)
108
+ if not task_dir:
109
+ print(
110
+ "ERROR: no --task specified and no selected task found. "
111
+ "Run `python ./.cstl/scripts/task.py select <task>` first.",
112
+ file=sys.stderr,
113
+ )
114
+ return 1
115
+
116
+ if not task_dir.is_dir():
117
+ print(f"ERROR: task directory not found: {task_dir}", file=sys.stderr)
118
+ return 1
119
+
120
+ max_chars = args.max_chars
121
+ if max_chars is None:
122
+ max_chars = DISPATCH_DEFAULT_MAX_CHARS.get(args.agent)
123
+
124
+ prompt, warnings, errors = build_dispatch_prompt(
125
+ repo_root,
126
+ task_dir,
127
+ args.agent,
128
+ finish=args.finish,
129
+ max_chars=max_chars,
130
+ require_in_progress=False,
131
+ )
132
+
133
+ for w in warnings:
134
+ print(f"WARN: {w}", file=sys.stderr)
135
+ for e in errors:
136
+ print(f"ERROR: {e}", file=sys.stderr)
137
+
138
+ if not prompt:
139
+ print("ERROR: failed to generate prompt (see errors above)", file=sys.stderr)
140
+ return 1
141
+
142
+ # Output the prompt to stdout — the caller captures this
143
+ print(prompt)
144
+ return 0
145
+
146
+
147
+ def _find_repo_root(start: Path) -> Path | None:
148
+ current = start.resolve()
149
+ while current != current.parent:
150
+ if (current / ".cstl").is_dir() or (current / ".git").exists():
151
+ return current
152
+ current = current.parent
153
+ return None
154
+
155
+
156
+ def _find_workspace_root(start: Path, dir_workflow: str) -> Path | None:
157
+ """Find the outermost directory with workflow dir (workspace root, not subproject)."""
158
+ found = None
159
+ current = start.resolve()
160
+ while current != current.parent:
161
+ if (current / dir_workflow).is_dir():
162
+ found = current
163
+ current = current.parent
164
+ return found
165
+
166
+
167
+ def _resolve_selected_task(repo_root: Path) -> Path | None:
168
+ try:
169
+ from common.active_task import resolve_selected_task # type: ignore[import-not-found]
170
+ result = resolve_selected_task(repo_root, {}, platform="cursor")
171
+ if result.task_path:
172
+ task_path = Path(result.task_path)
173
+ if not task_path.is_absolute():
174
+ task_path = repo_root / task_path
175
+ return task_path
176
+ except Exception:
177
+ pass
178
+ return None
179
+
180
+
181
+ if __name__ == "__main__":
182
+ sys.exit(main())
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env python3
2
+ """Injection budget probe — measure alwaysApply, AGENTS, jsonl, and Layer 2 dispatch."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ _SCRIPTS = Path(__file__).resolve().parent
11
+ if str(_SCRIPTS) not in sys.path:
12
+ sys.path.insert(0, str(_SCRIPTS))
13
+
14
+ from common.injection_budget import ( # noqa: E402
15
+ budget_violations,
16
+ collect_budget_report,
17
+ format_budget_report,
18
+ )
19
+
20
+
21
+ def main() -> int:
22
+ parser = argparse.ArgumentParser(description="Measure Trellis injection budget surfaces.")
23
+ parser.add_argument("--repo-root", default=".", help="Repository root (default: .)")
24
+ parser.add_argument("--task", default=None, help="Task directory for jsonl/dispatch probes")
25
+ parser.add_argument(
26
+ "--dispatch-role",
27
+ choices=["implement", "check", "research"],
28
+ default="implement",
29
+ help="Role when measuring dispatch prompt (default: implement)",
30
+ )
31
+ parser.add_argument(
32
+ "--strict",
33
+ action="store_true",
34
+ help="Exit 1 on FAIL thresholds (rules >32KiB) or any WARN",
35
+ )
36
+ parser.add_argument("--json", action="store_true", help="Reserved for future machine output")
37
+ args = parser.parse_args()
38
+
39
+ if args.json:
40
+ print("ERROR: --json not implemented yet", file=sys.stderr)
41
+ return 2
42
+
43
+ repo_root = Path(args.repo_root).resolve()
44
+ task_dir = None
45
+ if args.task:
46
+ task_dir = Path(args.task)
47
+ if not task_dir.is_absolute():
48
+ task_dir = repo_root / task_dir
49
+ task_dir = task_dir.resolve()
50
+
51
+ report = collect_budget_report(
52
+ repo_root,
53
+ task_dir=task_dir,
54
+ dispatch_role=args.dispatch_role if task_dir else None,
55
+ )
56
+ print(format_budget_report(report))
57
+
58
+ violations = budget_violations(report, strict=args.strict)
59
+ if violations:
60
+ print("\n## Violations", file=sys.stderr)
61
+ for item in violations:
62
+ print(item, file=sys.stderr)
63
+ if args.strict or any(item.startswith("FAIL:") for item in violations):
64
+ return 1
65
+ return 0
66
+
67
+
68
+ if __name__ == "__main__":
69
+ sys.exit(main())