@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.
- package/CHANGELOG.md +69 -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/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +5 -1
- package/dist/commands/update.js.map +1 -1
- package/dist/configurators/workflow.d.ts.map +1 -1
- package/dist/configurators/workflow.js +51 -2
- package/dist/configurators/workflow.js.map +1 -1
- package/dist/constants/paths.d.ts +4 -0
- package/dist/constants/paths.d.ts.map +1 -1
- package/dist/constants/paths.js +4 -0
- package/dist/constants/paths.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/migrations/manifests/0.4.0.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 +9 -0
- package/dist/templates/markdown/index.d.ts.map +1 -1
- package/dist/templates/markdown/index.js +9 -0
- package/dist/templates/markdown/index.js.map +1 -1
- package/dist/templates/markdown/spec/guides/artifact-locale-guide.md.txt +93 -0
- package/dist/templates/markdown/spec/guides/cross-platform-thinking-guide.md.txt +7 -7
- 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/cursor-subagent-policy.md.txt +10 -8
- package/dist/templates/markdown/spec/guides/debug-loop-guide.md.txt +227 -0
- package/dist/templates/markdown/spec/guides/goal-release-regression-runbook.md.txt +132 -0
- package/dist/templates/markdown/spec/guides/index.md.txt +144 -104
- package/dist/templates/markdown/spec/guides/injection-budget-guide.md.txt +111 -0
- package/dist/templates/markdown/spec/guides/prototype-guide.md.txt +139 -0
- package/dist/templates/markdown/spec/guides/retrieval-daily-guide.md.txt +28 -6
- package/dist/templates/markdown/spec/guides/test-discipline-guide.md.txt +138 -0
- package/dist/templates/markdown/spec/guides/verification-strength-guide.md.txt +185 -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 +17 -0
- package/dist/templates/trellis/index.d.ts.map +1 -1
- package/dist/templates/trellis/index.js +40 -0
- package/dist/templates/trellis/index.js.map +1 -1
- package/dist/templates/trellis/pool/README.md +103 -0
- package/dist/templates/trellis/pool/items/.gitkeep +0 -0
- package/dist/templates/trellis/pool/plan.md +26 -0
- 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/pool_store.py +702 -0
- 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 +24 -1
- package/dist/templates/trellis/scripts/common/task_dependencies.py +673 -0
- package/dist/templates/trellis/scripts/common/task_gates.py +58 -5
- package/dist/templates/trellis/scripts/common/task_map.py +40 -8
- package/dist/templates/trellis/scripts/common/task_store.py +400 -39
- package/dist/templates/trellis/scripts/common/test_depends_mode_block.py +489 -0
- package/dist/templates/trellis/scripts/common/test_observable_defaults.py +135 -0
- package/dist/templates/trellis/scripts/common/test_pool_store.py +428 -0
- package/dist/templates/trellis/scripts/common/test_task_dependencies.py +345 -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/pool.py +192 -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 +85 -1
- package/dist/templates/trellis/scripts/verify_evidence_probe.py +138 -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 +55 -5
- 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
|