@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,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 (
|
|
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
|
|
258
|
-
**
|
|
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.
|
|
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: <
|
|
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: <
|
|
279
|
+
notes: <GO_TO_DEFINITION availability; did Grep+Read succeed before any codegraph?>
|
|
278
280
|
```
|
|
279
281
|
|
|
280
282
|
verdict rules:
|
|
281
|
-
- **pass**:
|
|
282
|
-
- **environment_limitation**: only when
|
|
283
|
-
- **fail**: wrong file or
|
|
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())
|