@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,489 @@
1
+ #!/usr/bin/env python3
2
+ """Tests for Plan B depends_mode=block (opt-in hard gates).
3
+
4
+ Matrix source: 08-09-depends-on-b-research/research/03-recommendation.md §2.7
5
+ (T1-T14) adjudicated by 05-adjudication.md (check never fails; only the
6
+ mutating --approved path and set-child-state working enforce block mode).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import json
13
+ import sys
14
+ from pathlib import Path
15
+
16
+ import pytest
17
+
18
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
19
+
20
+ from common.task_dependencies import (
21
+ KIND_TASK,
22
+ ResolvedDep,
23
+ describe_child_dependencies,
24
+ describe_dependencies,
25
+ read_depends_mode,
26
+ )
27
+ from common.task_gates import validate_start_execution, validate_start_execution_check
28
+
29
+
30
+ # =============================================================================
31
+ # Helpers
32
+ # =============================================================================
33
+
34
+ def _write_task(tasks_dir: Path, dir_name: str, status: str, depends_on=None,
35
+ meta=None, **extra) -> Path:
36
+ task_dir = tasks_dir / dir_name
37
+ task_dir.mkdir(parents=True, exist_ok=True)
38
+ data = {
39
+ "id": dir_name,
40
+ "name": dir_name,
41
+ "title": dir_name,
42
+ "status": status,
43
+ **extra,
44
+ }
45
+ if depends_on is not None:
46
+ data["depends_on"] = depends_on
47
+ if meta is not None:
48
+ data["meta"] = meta
49
+ (task_dir / "task.json").write_text(json.dumps(data), encoding="utf-8")
50
+ return task_dir
51
+
52
+
53
+ def _ready_task(tasks_dir: Path, dir_name: str, depends_on, mode=None) -> Path:
54
+ """A lite task that passes every start-execution gate except deps policy."""
55
+ meta = {"depends_mode": mode} if mode else None
56
+ task_dir = _write_task(tasks_dir, dir_name, "planning", depends_on, meta)
57
+ (task_dir / "prd.md").write_text(
58
+ "# PRD\n\n## Goal\n\nx\n\n## Acceptance Criteria\n\n- [ ] x\n",
59
+ encoding="utf-8",
60
+ )
61
+ return task_dir
62
+
63
+
64
+ @pytest.fixture
65
+ def sandbox(tmp_path, monkeypatch):
66
+ """Isolated repo where dependency resolution and store commands run."""
67
+ repo = tmp_path / "repo"
68
+ tasks_dir = repo / ".cstl" / "tasks"
69
+ tasks_dir.mkdir(parents=True)
70
+ monkeypatch.setattr("common.task_dependencies.get_repo_root", lambda: repo)
71
+ monkeypatch.setattr("common.task_store.get_repo_root", lambda: repo)
72
+ return repo, tasks_dir
73
+
74
+
75
+ def _read_json(path: Path) -> dict:
76
+ return json.loads(path.read_text(encoding="utf-8"))
77
+
78
+
79
+ # =============================================================================
80
+ # read_depends_mode (R1)
81
+ # =============================================================================
82
+
83
+ def test_read_depends_mode_defaults_to_warn() -> None:
84
+ assert read_depends_mode(None) == "warn"
85
+ assert read_depends_mode({}) == "warn"
86
+ assert read_depends_mode({"meta": None}) == "warn"
87
+ assert read_depends_mode({"meta": {}}) == "warn"
88
+ assert read_depends_mode({"meta": {"depends_mode": None}}) == "warn"
89
+ assert read_depends_mode({"meta": {"depends_mode": "explode"}}) == "warn"
90
+
91
+
92
+ def test_read_depends_mode_accepts_all_modes() -> None:
93
+ for mode in ("warn", "block", "off"):
94
+ assert read_depends_mode({"meta": {"depends_mode": mode}}) == mode
95
+
96
+
97
+ # =============================================================================
98
+ # blocking_errors semantics (T5/T7/T8/T9/T13/T14)
99
+ # =============================================================================
100
+
101
+ def test_blocking_errors_for_statuses(sandbox) -> None:
102
+ repo, tasks_dir = sandbox
103
+ _write_task(tasks_dir, "dep-wait", "in_progress")
104
+ _write_task(tasks_dir, "dep-done", "completed")
105
+ _write_task(tasks_dir, "dep-gone", "cancelled")
106
+ task_dir = _write_task(
107
+ tasks_dir,
108
+ "task-x",
109
+ "planning",
110
+ ["dep-wait", "dep-done", "dep-gone", "ghost-task", "pool:P09"],
111
+ )
112
+ report = describe_dependencies(task_dir, _read_json(task_dir / "task.json"))
113
+ errors = report.blocking_errors()
114
+ assert any("not satisfied: dep-wait" in e for e in errors)
115
+ assert not any("dep-done" in e for e in errors)
116
+ assert not any("dep-gone" in e for e in errors)
117
+ assert any("dangling dependency: ghost-task" in e for e in errors)
118
+ assert any("unresolved: pool:P09" in e for e in errors)
119
+
120
+
121
+ def test_blocking_errors_cycle(sandbox) -> None:
122
+ repo, tasks_dir = sandbox
123
+ task_a = _write_task(tasks_dir, "task-a", "planning", ["task-b"])
124
+ _write_task(tasks_dir, "task-b", "planning", ["task-a"])
125
+ report = describe_dependencies(task_a, _read_json(task_a / "task.json"))
126
+ assert any("cycle detected" in e for e in report.blocking_errors())
127
+
128
+
129
+ def test_blocking_errors_never_for_cancelled_satisfied() -> None:
130
+ dep = ResolvedDep(ref="dep-gone", kind=KIND_TASK, status="cancelled")
131
+ from common.task_dependencies import DependencyReport
132
+
133
+ report = DependencyReport(task_id="task-x", deps=[dep])
134
+ assert report.blocking_errors() == []
135
+ assert any("cancelled" in w for w in report.warnings())
136
+
137
+
138
+ def test_blocking_errors_subset_of_warnings(sandbox) -> None:
139
+ """T14: every blocking error string also appears in warnings()."""
140
+ repo, tasks_dir = sandbox
141
+ _write_task(tasks_dir, "dep-wait", "in_progress")
142
+ _write_task(tasks_dir, "dep-gone", "cancelled")
143
+ task_dir = _write_task(
144
+ tasks_dir,
145
+ "task-x",
146
+ "planning",
147
+ ["dep-wait", "dep-gone", "ghost-task", "pool:P09"],
148
+ )
149
+ report = describe_dependencies(task_dir, _read_json(task_dir / "task.json"))
150
+ warning_set = set(report.warnings())
151
+ assert set(report.blocking_errors()) <= warning_set
152
+
153
+
154
+ # =============================================================================
155
+ # describe_child_dependencies: task.json + task-map union (R4, Q11)
156
+ # =============================================================================
157
+
158
+ def test_describe_child_dependencies_merges_task_json_and_task_map(sandbox) -> None:
159
+ repo, tasks_dir = sandbox
160
+ _write_task(tasks_dir, "dep-task", "in_progress")
161
+ child_dir = _write_task(tasks_dir, "child-a", "planning", ["dep-task"])
162
+ child_data = _read_json(child_dir / "task.json")
163
+ map_entry = {"id": "child-a", "state": "open", "depends_on": ["dep-map"]}
164
+
165
+ report = describe_child_dependencies(child_dir, child_data, map_entry, repo_root=repo)
166
+ refs = [dep.ref for dep in report.deps]
167
+ assert "dep-task" in refs
168
+ assert "dep-map" in refs
169
+
170
+
171
+ def test_describe_child_dependencies_dedupes_sources(sandbox) -> None:
172
+ repo, tasks_dir = sandbox
173
+ child_dir = _write_task(tasks_dir, "child-a", "planning", ["shared"])
174
+ child_data = _read_json(child_dir / "task.json")
175
+ map_entry = {"id": "child-a", "state": "open", "depends_on": ["shared", "shared"]}
176
+
177
+ report = describe_child_dependencies(child_dir, child_data, map_entry, repo_root=repo)
178
+ refs = [dep.ref for dep in report.deps]
179
+ assert refs == ["shared"]
180
+
181
+
182
+ # =============================================================================
183
+ # start-execution gates (T1-T9, T13)
184
+ # =============================================================================
185
+
186
+ def test_t1_default_warn_approved_unmet_deps_do_not_fail(sandbox) -> None:
187
+ repo, tasks_dir = sandbox
188
+ _write_task(tasks_dir, "dep-a", "in_progress")
189
+ task_dir = _ready_task(tasks_dir, "task-x", ["dep-a"])
190
+
191
+ guard = validate_start_execution(task_dir, _read_json(task_dir / "task.json"),
192
+ approved=True, enforce_deps_block=True)
193
+ assert guard.ok
194
+ assert not any("dep-a" in e for e in guard.errors)
195
+ assert any("dep-a" in w for w in guard.warnings)
196
+
197
+
198
+ def test_t2_block_approved_unmet_deps_fail(sandbox) -> None:
199
+ repo, tasks_dir = sandbox
200
+ _write_task(tasks_dir, "dep-a", "in_progress")
201
+ task_dir = _ready_task(tasks_dir, "task-x", ["dep-a"], mode="block")
202
+
203
+ guard = validate_start_execution(task_dir, _read_json(task_dir / "task.json"),
204
+ approved=True, enforce_deps_block=True)
205
+ assert not guard.ok
206
+ assert any("dependency not satisfied: dep-a" in e for e in guard.errors)
207
+
208
+
209
+ def test_t3_block_approved_ignore_deps_passes(sandbox) -> None:
210
+ repo, tasks_dir = sandbox
211
+ _write_task(tasks_dir, "dep-a", "in_progress")
212
+ task_dir = _ready_task(tasks_dir, "task-x", ["dep-a"], mode="block")
213
+
214
+ guard = validate_start_execution(task_dir, _read_json(task_dir / "task.json"),
215
+ approved=True, enforce_deps_block=True,
216
+ ignore_deps=True)
217
+ assert guard.ok
218
+ assert guard.deps_blocking_summary
219
+ assert any("dep-a" in w for w in guard.warnings)
220
+
221
+
222
+ def test_t4_block_check_never_fails(sandbox) -> None:
223
+ repo, tasks_dir = sandbox
224
+ _write_task(tasks_dir, "dep-a", "in_progress")
225
+ task_dir = _ready_task(tasks_dir, "task-x", ["dep-a"], mode="block")
226
+
227
+ guard = validate_start_execution_check(task_dir, _read_json(task_dir / "task.json"))
228
+ assert guard.ok
229
+ assert any("dep-a" in w for w in guard.warnings)
230
+ assert any("--approved would fail" in w for w in guard.warnings)
231
+
232
+
233
+ def test_t5_block_empty_deps_approved_passes(sandbox) -> None:
234
+ repo, tasks_dir = sandbox
235
+ task_dir = _ready_task(tasks_dir, "task-x", [], mode="block")
236
+
237
+ guard = validate_start_execution(task_dir, _read_json(task_dir / "task.json"),
238
+ approved=True, enforce_deps_block=True)
239
+ assert guard.ok
240
+ assert guard.deps_blocking_summary == []
241
+
242
+
243
+ def test_t6_off_silences_deps(sandbox) -> None:
244
+ repo, tasks_dir = sandbox
245
+ _write_task(tasks_dir, "dep-a", "in_progress")
246
+ task_dir = _ready_task(tasks_dir, "task-x", ["dep-a"], mode="off")
247
+
248
+ guard = validate_start_execution(task_dir, _read_json(task_dir / "task.json"),
249
+ approved=True, enforce_deps_block=True)
250
+ assert guard.ok
251
+ assert not any("dep-a" in e for e in guard.errors)
252
+ assert not any("dep-a" in w for w in guard.warnings)
253
+
254
+
255
+ def test_t7_block_cycle_fails(sandbox) -> None:
256
+ repo, tasks_dir = sandbox
257
+ _write_task(tasks_dir, "task-a", "planning", ["task-b"])
258
+ _write_task(tasks_dir, "task-b", "planning", ["task-a"])
259
+ task_dir = _ready_task(tasks_dir, "task-x", ["task-a"], mode="block")
260
+
261
+ guard = validate_start_execution(task_dir, _read_json(task_dir / "task.json"),
262
+ approved=True, enforce_deps_block=True)
263
+ assert not guard.ok
264
+ assert any("cycle detected" in e for e in guard.errors)
265
+
266
+
267
+ def test_t8_block_dangling_fails(sandbox) -> None:
268
+ repo, tasks_dir = sandbox
269
+ task_dir = _ready_task(tasks_dir, "task-x", ["ghost-task"], mode="block")
270
+
271
+ guard = validate_start_execution(task_dir, _read_json(task_dir / "task.json"),
272
+ approved=True, enforce_deps_block=True)
273
+ assert not guard.ok
274
+ assert any("dangling dependency: ghost-task" in e for e in guard.errors)
275
+
276
+
277
+ def test_t9_block_pool_unresolved_fails(sandbox) -> None:
278
+ repo, tasks_dir = sandbox
279
+ task_dir = _ready_task(tasks_dir, "task-x", ["pool:P09"], mode="block")
280
+
281
+ guard = validate_start_execution(task_dir, _read_json(task_dir / "task.json"),
282
+ approved=True, enforce_deps_block=True)
283
+ assert not guard.ok
284
+ assert any("dependency unresolved: pool:P09" in e for e in guard.errors)
285
+
286
+
287
+ def test_t13_block_cancelled_dependency_not_blocked(sandbox) -> None:
288
+ repo, tasks_dir = sandbox
289
+ _write_task(tasks_dir, "dep-gone", "cancelled")
290
+ task_dir = _ready_task(tasks_dir, "task-x", ["dep-gone"], mode="block")
291
+
292
+ guard = validate_start_execution(task_dir, _read_json(task_dir / "task.json"),
293
+ approved=True, enforce_deps_block=True)
294
+ assert guard.ok
295
+ assert guard.deps_blocking_summary == []
296
+
297
+
298
+ # =============================================================================
299
+ # set-child-state working (T10-T12) + ignore audit (R4)
300
+ # =============================================================================
301
+
302
+ def _parent_child_pair(sandbox) -> tuple[Path, Path, dict, dict]:
303
+ repo, tasks_dir = sandbox
304
+ parent_dir = _write_task(tasks_dir, "parent-x", "planning", children=["child-a"])
305
+ child_dir = _write_task(tasks_dir, "child-a", "planning", parent="parent-x")
306
+ parent_data = _read_json(parent_dir / "task.json")
307
+ child_data = _read_json(child_dir / "task.json")
308
+ return parent_dir, child_dir, parent_data, child_data
309
+
310
+
311
+ def _child_args(parent_dir: Path, child_dir: Path, state: str,
312
+ ignore_deps: bool = False) -> argparse.Namespace:
313
+ return argparse.Namespace(
314
+ parent_dir=str(parent_dir),
315
+ child_dir=str(child_dir),
316
+ state=state,
317
+ evidence="ev-1",
318
+ reason=None,
319
+ ignore_deps=ignore_deps,
320
+ )
321
+
322
+
323
+ def test_t10_block_child_working_rejected(sandbox) -> None:
324
+ from common.task_store import cmd_set_child_state
325
+ from common.task_map import ensure_task_map, load_task_map
326
+
327
+ parent_dir, child_dir, parent_data, child_data = _parent_child_pair(sandbox)
328
+ ensure_task_map(parent_dir, parent_data, ["child-a"])
329
+ child_data["meta"] = {"depends_mode": "block"}
330
+ (child_dir / "task.json").write_text(json.dumps(child_data), encoding="utf-8")
331
+ _write_task(sandbox[1], "dep-a", "in_progress")
332
+ child_data["depends_on"] = ["dep-a"]
333
+ (child_dir / "task.json").write_text(json.dumps(child_data), encoding="utf-8")
334
+
335
+ rc = cmd_set_child_state(_child_args(parent_dir, child_dir, "working"))
336
+ assert rc == 1
337
+ # state must remain unchanged in the task-map
338
+ map_data, _ = load_task_map(parent_dir)
339
+ entry = next(c for c in map_data["children"] if c["id"] == "child-a")
340
+ assert entry["state"] == "open"
341
+
342
+
343
+ def test_t10b_block_child_working_rejected_from_task_map_source(sandbox) -> None:
344
+ from common.task_store import cmd_set_child_state
345
+ from common.task_map import (
346
+ ensure_task_map,
347
+ get_child_entry,
348
+ load_task_map,
349
+ write_task_map,
350
+ )
351
+
352
+ parent_dir, child_dir, parent_data, child_data = _parent_child_pair(sandbox)
353
+ ensure_task_map(parent_dir, parent_data, ["child-a"])
354
+ child_data["meta"] = {"depends_mode": "block"}
355
+ (child_dir / "task.json").write_text(json.dumps(child_data), encoding="utf-8")
356
+
357
+ map_data, map_body = load_task_map(parent_dir)
358
+ child = get_child_entry(map_data, "child-a")
359
+ child["depends_on"] = ["dep-map"]
360
+ write_task_map(parent_dir, map_data, map_body, None)
361
+ _write_task(sandbox[1], "dep-map", "in_progress")
362
+
363
+ rc = cmd_set_child_state(_child_args(parent_dir, child_dir, "working"))
364
+ assert rc == 1
365
+
366
+
367
+ def test_t11_block_child_review_not_blocked(sandbox) -> None:
368
+ from common.task_store import cmd_set_child_state
369
+ from common.task_map import load_task_map
370
+
371
+ parent_dir, child_dir, parent_data, child_data = _parent_child_pair(sandbox)
372
+ child_data["meta"] = {"depends_mode": "block"}
373
+ child_data["depends_on"] = ["dep-a"]
374
+ (child_dir / "task.json").write_text(json.dumps(child_data), encoding="utf-8")
375
+
376
+ rc = cmd_set_child_state(_child_args(parent_dir, child_dir, "review"))
377
+ assert rc == 0
378
+ map_data, _ = load_task_map(parent_dir)
379
+ entry = next(c for c in map_data["children"] if c["id"] == "child-a")
380
+ assert entry["state"] == "review"
381
+
382
+
383
+ def test_t12_warn_child_working_not_blocked(sandbox) -> None:
384
+ from common.task_store import cmd_set_child_state
385
+ from common.task_map import load_task_map
386
+
387
+ parent_dir, child_dir, parent_data, child_data = _parent_child_pair(sandbox)
388
+ child_data["depends_on"] = ["dep-a"]
389
+ (child_dir / "task.json").write_text(json.dumps(child_data), encoding="utf-8")
390
+
391
+ rc = cmd_set_child_state(_child_args(parent_dir, child_dir, "working"))
392
+ assert rc == 0
393
+ map_data, _ = load_task_map(parent_dir)
394
+ entry = next(c for c in map_data["children"] if c["id"] == "child-a")
395
+ assert entry["state"] == "working"
396
+
397
+
398
+ def test_child_working_ignore_deps_passes_and_audits(sandbox) -> None:
399
+ from common.task_store import cmd_set_child_state
400
+ from common.task_map import ensure_task_map, load_task_map
401
+
402
+ parent_dir, child_dir, parent_data, child_data = _parent_child_pair(sandbox)
403
+ ensure_task_map(parent_dir, parent_data, ["child-a"])
404
+ child_data["meta"] = {"depends_mode": "block"}
405
+ child_data["depends_on"] = ["dep-a"]
406
+ (child_dir / "task.json").write_text(json.dumps(child_data), encoding="utf-8")
407
+
408
+ rc = cmd_set_child_state(_child_args(parent_dir, child_dir, "working",
409
+ ignore_deps=True))
410
+ assert rc == 0
411
+
412
+ child_data = _read_json(child_dir / "task.json")
413
+ events = child_data["meta"]["depends_ignore_events"]
414
+ assert len(events) == 1
415
+ assert events[0]["command"] == "set-child-state"
416
+ assert events[0]["mode"] == "block"
417
+ assert any("dep-a" in item for item in events[0]["blocking_summary"])
418
+ assert events[0]["evidence"] == "ev-1"
419
+
420
+ map_data, map_body = load_task_map(parent_dir)
421
+ assert "Ignored dependencies (mode=block) for child `child-a`" in map_body
422
+ entry = next(c for c in map_data["children"] if c["id"] == "child-a")
423
+ assert entry["state"] == "working"
424
+
425
+
426
+ # =============================================================================
427
+ # start-execution CLI wiring: --ignore-deps event in the same write (R3)
428
+ # =============================================================================
429
+
430
+ def test_start_execution_approved_ignore_deps_writes_event_with_approval(sandbox) -> None:
431
+ from task import cmd_start_execution
432
+
433
+ repo, tasks_dir = sandbox
434
+ _write_task(tasks_dir, "dep-a", "in_progress")
435
+ task_dir = _ready_task(tasks_dir, "task-x", ["dep-a"], mode="block")
436
+
437
+ args = argparse.Namespace(
438
+ dir=str(task_dir), check=False, approved=True, ignore_deps=True
439
+ )
440
+ rc = cmd_start_execution(args)
441
+ assert rc == 0
442
+
443
+ task_data = _read_json(task_dir / "task.json")
444
+ assert task_data["execution_approval"]["transition"] == "start-execution"
445
+ assert task_data["status"] == "in_progress"
446
+ events = task_data["meta"]["depends_ignore_events"]
447
+ assert len(events) == 1
448
+ assert events[0]["command"] == "start-execution"
449
+ assert any("dep-a" in item for item in events[0]["blocking_summary"])
450
+
451
+
452
+ def test_start_execution_approved_block_rejected_without_ignore(sandbox) -> None:
453
+ from task import cmd_start_execution
454
+
455
+ repo, tasks_dir = sandbox
456
+ _write_task(tasks_dir, "dep-a", "in_progress")
457
+ task_dir = _ready_task(tasks_dir, "task-x", ["dep-a"], mode="block")
458
+
459
+ args = argparse.Namespace(
460
+ dir=str(task_dir), check=False, approved=True, ignore_deps=False
461
+ )
462
+ rc = cmd_start_execution(args)
463
+ assert rc == 1
464
+ task_data = _read_json(task_dir / "task.json")
465
+ assert "execution_approval" not in task_data
466
+ assert task_data["status"] == "planning"
467
+
468
+
469
+ # =============================================================================
470
+ # set-depends-mode CLI (R5)
471
+ # =============================================================================
472
+
473
+ def test_set_depends_mode_writes_meta(sandbox) -> None:
474
+ from common.task_store import cmd_set_depends_mode
475
+
476
+ repo, tasks_dir = sandbox
477
+ task_dir = _write_task(tasks_dir, "task-x", "planning")
478
+
479
+ args = argparse.Namespace(dir=str(task_dir), mode="block")
480
+ assert cmd_set_depends_mode(args) == 0
481
+ assert _read_json(task_dir / "task.json")["meta"]["depends_mode"] == "block"
482
+
483
+ args = argparse.Namespace(dir=str(task_dir), mode="off")
484
+ assert cmd_set_depends_mode(args) == 0
485
+ assert _read_json(task_dir / "task.json")["meta"]["depends_mode"] == "off"
486
+
487
+ args = argparse.Namespace(dir=str(task_dir), mode="nonsense")
488
+ assert cmd_set_depends_mode(args) == 1
489
+ assert _read_json(task_dir / "task.json")["meta"]["depends_mode"] == "off"
@@ -0,0 +1,135 @@
1
+ #!/usr/bin/env python3
2
+ """Tests for P1 observable defaults (verify seed, jsonl seed, dashboard summary)."""
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.artifact_locale import default_verify_content
15
+ from common.task_dashboard import format_verify_summary, render_task_dashboard
16
+ from common.task_store import _resolve_seed_spec_paths, _write_seed_jsonl
17
+
18
+
19
+ def test_default_verify_content_has_standard_sections() -> None:
20
+ content = default_verify_content(locale="en")
21
+ for heading in (
22
+ "## Planning check",
23
+ "## Execution evidence",
24
+ "### Validation commands",
25
+ "### Acceptance",
26
+ "### Durable learning",
27
+ ):
28
+ assert heading in content
29
+
30
+
31
+ def test_write_seed_jsonl_uses_real_spec_paths(tmp_path: Path) -> None:
32
+ repo_root = tmp_path / "repo"
33
+ spec_root = repo_root / ".cstl" / "spec" / "guides"
34
+ spec_root.mkdir(parents=True)
35
+ for name in ("index.md", "verification-strength-guide.md", "injection-budget-guide.md"):
36
+ (spec_root / name).write_text(f"# {name}\n", encoding="utf-8")
37
+
38
+ task_dir = repo_root / ".cstl" / "tasks" / "08-07-test-seed"
39
+ task_dir.mkdir(parents=True)
40
+ task_data = {"id": "test-seed", "package": None, "scope": None, "children": []}
41
+
42
+ jsonl_path = task_dir / "implement.jsonl"
43
+ _write_seed_jsonl(jsonl_path, repo_root, task_dir, task_data)
44
+
45
+ rows = [json.loads(line) for line in jsonl_path.read_text(encoding="utf-8").splitlines() if line.strip()]
46
+ assert len(rows) >= 2
47
+ assert all("file" in row and row["file"].startswith(".cstl/spec/") for row in rows)
48
+ assert all("_example" not in row for row in rows)
49
+ for row in rows:
50
+ assert (repo_root / row["file"]).is_file()
51
+
52
+
53
+ def test_resolve_seed_spec_paths_prefers_existing_guides(tmp_path: Path) -> None:
54
+ repo_root = tmp_path / "repo"
55
+ guides = repo_root / ".cstl" / "spec" / "guides"
56
+ guides.mkdir(parents=True)
57
+ (guides / "index.md").write_text("# index\n", encoding="utf-8")
58
+ (guides / "verification-strength-guide.md").write_text("# verify\n", encoding="utf-8")
59
+
60
+ task_dir = repo_root / ".cstl" / "tasks" / "08-07-test-paths"
61
+ task_dir.mkdir(parents=True)
62
+ paths = _resolve_seed_spec_paths(repo_root, task_dir, {"children": []})
63
+ assert len(paths) >= 2
64
+ assert all((repo_root / rel).is_file() for rel in paths)
65
+
66
+
67
+ def test_format_verify_summary_missing_without_file(tmp_path: Path) -> None:
68
+ task_dir = tmp_path / "task"
69
+ task_dir.mkdir()
70
+ assert format_verify_summary(task_dir, {"children": []}) == "[verify: missing]"
71
+
72
+
73
+ def test_format_verify_summary_partial_from_signals(tmp_path: Path) -> None:
74
+ task_dir = tmp_path / "task"
75
+ task_dir.mkdir()
76
+ (task_dir / "verify.md").write_text(
77
+ "Validation commands: pytest -q — pass\n",
78
+ encoding="utf-8",
79
+ )
80
+ assert format_verify_summary(task_dir, {"children": []}) == "[verify: partial]"
81
+
82
+
83
+ def test_format_verify_summary_ok_when_all_signals(tmp_path: Path) -> None:
84
+ task_dir = tmp_path / "task"
85
+ task_dir.mkdir()
86
+ (task_dir / "verify.md").write_text(
87
+ "\n".join(
88
+ [
89
+ "Validation commands: pytest -q — pass",
90
+ "Check evidence: cstl-check — pass",
91
+ "Final acceptance evidence: all AC met",
92
+ "Durable learning decision: no durable learning",
93
+ "Reviewed change-set: git diff main..HEAD",
94
+ ]
95
+ ),
96
+ encoding="utf-8",
97
+ )
98
+ assert format_verify_summary(task_dir, {"children": []}) == "[verify: ok]"
99
+
100
+
101
+ def test_render_task_dashboard_includes_verify_summary(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
102
+ repo_root = tmp_path / "repo"
103
+ tasks_dir = repo_root / ".cstl" / "tasks"
104
+ task_dir = tasks_dir / "08-07-dashboard-task"
105
+ task_dir.mkdir(parents=True)
106
+ (task_dir / "task.json").write_text(
107
+ json.dumps(
108
+ {
109
+ "id": "dashboard-task",
110
+ "name": "dashboard-task",
111
+ "title": "dashboard-task",
112
+ "status": "planning",
113
+ "assignee": "dev",
114
+ "children": [],
115
+ }
116
+ ),
117
+ encoding="utf-8",
118
+ )
119
+ (task_dir / "verify.md").write_text(
120
+ "Validation commands: echo ok — pass\n",
121
+ encoding="utf-8",
122
+ )
123
+
124
+ monkeypatch.chdir(repo_root)
125
+ monkeypatch.setattr("common.task_dashboard.get_repo_root", lambda: repo_root)
126
+ monkeypatch.setattr("common.task_dashboard.get_tasks_dir", lambda _root=None: tasks_dir)
127
+ monkeypatch.setattr("common.task_dashboard.get_developer", lambda _root=None: "dev")
128
+ monkeypatch.setattr(
129
+ "common.active_task.resolve_selected_task",
130
+ lambda *_args, **_kwargs: type("Sel", (), {"task_path": None, "source": None})(),
131
+ )
132
+
133
+ dashboard = render_task_dashboard(repo_root)
134
+ assert "[verify: partial]" in dashboard
135
+ assert "08-07-dashboard-task" in dashboard