deviatdd 2.5.1__py3-none-any.whl
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.
- deviatdd-2.5.1.dist-info/METADATA +386 -0
- deviatdd-2.5.1.dist-info/RECORD +124 -0
- deviatdd-2.5.1.dist-info/WHEEL +4 -0
- deviatdd-2.5.1.dist-info/entry_points.txt +2 -0
- deviatdd-2.5.1.dist-info/licenses/LICENSE +21 -0
- deviate/__init__.py +3 -0
- deviate/cli/__init__.py +824 -0
- deviate/cli/_common.py +155 -0
- deviate/cli/adhoc.py +183 -0
- deviate/cli/constitution.py +113 -0
- deviate/cli/feature.py +94 -0
- deviate/cli/init.py +485 -0
- deviate/cli/inspect.py +208 -0
- deviate/cli/macro.py +937 -0
- deviate/cli/meso.py +1894 -0
- deviate/cli/micro.py +3249 -0
- deviate/cli/review.py +441 -0
- deviate/core/__init__.py +0 -0
- deviate/core/_shared.py +20 -0
- deviate/core/agent.py +505 -0
- deviate/core/cache_discipline.py +65 -0
- deviate/core/commands.py +202 -0
- deviate/core/commit.py +86 -0
- deviate/core/complexity.py +36 -0
- deviate/core/constitution.py +85 -0
- deviate/core/contract.py +17 -0
- deviate/core/convention.py +147 -0
- deviate/core/epic.py +73 -0
- deviate/core/issues.py +46 -0
- deviate/core/prd.py +18 -0
- deviate/core/profile.py +33 -0
- deviate/core/repo.py +47 -0
- deviate/core/run_logger.py +50 -0
- deviate/core/tasks_ledger.py +69 -0
- deviate/core/treesitter/__init__.py +31 -0
- deviate/core/treesitter/analysis.py +457 -0
- deviate/core/treesitter/models.py +35 -0
- deviate/core/treesitter/parser.py +184 -0
- deviate/core/treesitter/queries/bash.scm +7 -0
- deviate/core/treesitter/queries/c_sharp.scm +11 -0
- deviate/core/treesitter/queries/cpp.scm +9 -0
- deviate/core/treesitter/queries/css.scm +4 -0
- deviate/core/treesitter/queries/dockerfile.scm +8 -0
- deviate/core/treesitter/queries/elixir.scm +6 -0
- deviate/core/treesitter/queries/go.scm +9 -0
- deviate/core/treesitter/queries/hcl.scm +3 -0
- deviate/core/treesitter/queries/html.scm +3 -0
- deviate/core/treesitter/queries/javascript.scm +12 -0
- deviate/core/treesitter/queries/json.scm +4 -0
- deviate/core/treesitter/queries/kotlin.scm +8 -0
- deviate/core/treesitter/queries/markdown.scm +4 -0
- deviate/core/treesitter/queries/python.scm +10 -0
- deviate/core/treesitter/queries/rust.scm +12 -0
- deviate/core/treesitter/queries/sql.scm +7 -0
- deviate/core/treesitter/queries/swift.scm +10 -0
- deviate/core/treesitter/queries/toml.scm +3 -0
- deviate/core/treesitter/queries/tsx.scm +14 -0
- deviate/core/treesitter/queries/typescript.scm +14 -0
- deviate/core/treesitter/queries/yaml.scm +4 -0
- deviate/core/validation.py +153 -0
- deviate/core/worktree.py +141 -0
- deviate/main.py +4 -0
- deviate/prompts/__init__.py +0 -0
- deviate/prompts/assembly.py +122 -0
- deviate/prompts/auto/__init__.py +1 -0
- deviate/prompts/auto/execute.md +62 -0
- deviate/prompts/auto/explore.md +103 -0
- deviate/prompts/auto/green.md +137 -0
- deviate/prompts/auto/judge.md +260 -0
- deviate/prompts/auto/plan.md +127 -0
- deviate/prompts/auto/prd.md +108 -0
- deviate/prompts/auto/red.md +156 -0
- deviate/prompts/auto/refactor.md +166 -0
- deviate/prompts/auto/research.md +111 -0
- deviate/prompts/auto/shard.md +90 -0
- deviate/prompts/auto/specify.md +77 -0
- deviate/prompts/auto/tasks.md +191 -0
- deviate/prompts/commands/deviate-adhoc.md +216 -0
- deviate/prompts/commands/deviate-architecture.md +147 -0
- deviate/prompts/commands/deviate-constitution.md +215 -0
- deviate/prompts/commands/deviate-e2e.md +264 -0
- deviate/prompts/commands/deviate-execute.md +223 -0
- deviate/prompts/commands/deviate-explore.md +253 -0
- deviate/prompts/commands/deviate-flows.md +226 -0
- deviate/prompts/commands/deviate-green.md +217 -0
- deviate/prompts/commands/deviate-hotfix.md +188 -0
- deviate/prompts/commands/deviate-init.md +170 -0
- deviate/prompts/commands/deviate-judge.md +193 -0
- deviate/prompts/commands/deviate-merge.md +221 -0
- deviate/prompts/commands/deviate-plan.md +158 -0
- deviate/prompts/commands/deviate-pr.md +144 -0
- deviate/prompts/commands/deviate-prd.md +216 -0
- deviate/prompts/commands/deviate-prune.md +260 -0
- deviate/prompts/commands/deviate-red.md +231 -0
- deviate/prompts/commands/deviate-refactor.md +211 -0
- deviate/prompts/commands/deviate-release.md +123 -0
- deviate/prompts/commands/deviate-research.md +359 -0
- deviate/prompts/commands/deviate-review.md +289 -0
- deviate/prompts/commands/deviate-shard.md +226 -0
- deviate/prompts/commands/deviate-tasks.md +281 -0
- deviate/prompts/commands/deviate-triage.md +141 -0
- deviate/prompts/constitution_seed.md +51 -0
- deviate/prompts/core/core.md +21 -0
- deviate/prompts/core/macro-auto.md +59 -0
- deviate/prompts/core/macro-command.md +54 -0
- deviate/prompts/core/macro-skill.md +54 -0
- deviate/prompts/core/meso-auto.md +63 -0
- deviate/prompts/core/meso-command.md +58 -0
- deviate/prompts/core/meso-skill.md +58 -0
- deviate/prompts/core/micro-auto.md +76 -0
- deviate/prompts/core/micro-command.md +67 -0
- deviate/prompts/core/micro-skill.md +67 -0
- deviate/prompts/extras/deviate-pr-graphite-routing.md +20 -0
- deviate/prompts/governance/__init__.py +0 -0
- deviate/prompts/governance/agents_seed.md +1 -0
- deviate/prompts/governance/claudemd_seed.md +1 -0
- deviate/prompts/governance/graphite_seed.md +18 -0
- deviate/prompts/governance/libref_seed.md +3 -0
- deviate/state/__init__.py +0 -0
- deviate/state/config.py +257 -0
- deviate/state/ledger.py +407 -0
- deviate/ui/__init__.py +5 -0
- deviate/ui/monitor.py +187 -0
- deviate/ui/render.py +26 -0
deviate/cli/init.py
ADDED
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import subprocess
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import NoReturn
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
|
|
11
|
+
init_app = typer.Typer(no_args_is_help=True)
|
|
12
|
+
console = Console()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _fail_with(reason: str) -> NoReturn:
|
|
16
|
+
print(json.dumps({"status": "FAILURE", "reason": reason}))
|
|
17
|
+
raise typer.Exit(code=1)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _detect_project_type(repo_root: Path) -> str:
|
|
21
|
+
if (repo_root / "mix.exs").exists():
|
|
22
|
+
return "elixir_phoenix"
|
|
23
|
+
if (repo_root / "pyproject.toml").exists():
|
|
24
|
+
return "python"
|
|
25
|
+
if (repo_root / "package.json").exists():
|
|
26
|
+
return "node"
|
|
27
|
+
if (repo_root / "Cargo.toml").exists():
|
|
28
|
+
return "rust"
|
|
29
|
+
if (repo_root / "go.mod").exists():
|
|
30
|
+
return "go"
|
|
31
|
+
return "unknown"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _get_test_command(project_type: str) -> str:
|
|
35
|
+
commands = {
|
|
36
|
+
"elixir_phoenix": "mix test",
|
|
37
|
+
"python": "uv run pytest",
|
|
38
|
+
"node": "npm test",
|
|
39
|
+
"rust": "cargo test",
|
|
40
|
+
"go": "go test ./...",
|
|
41
|
+
}
|
|
42
|
+
return commands.get(project_type, "true")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _get_zero_test_pass_command(project_type: str) -> str:
|
|
46
|
+
commands = {
|
|
47
|
+
"elixir_phoenix": "mix test || true",
|
|
48
|
+
"python": "uv run pytest || true",
|
|
49
|
+
"node": "npm test || true",
|
|
50
|
+
"rust": "cargo test || true",
|
|
51
|
+
"go": "go test ./... || true",
|
|
52
|
+
}
|
|
53
|
+
return commands.get(project_type, "echo 'No test framework' || true")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _get_lint_command(project_type: str) -> str:
|
|
57
|
+
commands = {
|
|
58
|
+
"elixir_phoenix": "mix credo --strict",
|
|
59
|
+
"python": "uv run ruff check",
|
|
60
|
+
"node": "npm run lint 2>/dev/null || echo 'No lint configured'",
|
|
61
|
+
"rust": "cargo clippy -- -D warnings",
|
|
62
|
+
"go": "golangci-lint run 2>/dev/null || echo 'No linter configured'",
|
|
63
|
+
}
|
|
64
|
+
return commands.get(project_type, "echo 'No linter configured'")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _get_format_command(project_type: str) -> str:
|
|
68
|
+
commands = {
|
|
69
|
+
"elixir_phoenix": "mix format",
|
|
70
|
+
"python": "uv run ruff format",
|
|
71
|
+
"node": "npm run format 2>/dev/null || echo 'No formatter configured'",
|
|
72
|
+
"rust": "cargo fmt",
|
|
73
|
+
"go": "gofmt -w .",
|
|
74
|
+
}
|
|
75
|
+
return commands.get(project_type, "echo 'No formatter configured'")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _get_format_check_command(project_type: str) -> str:
|
|
79
|
+
commands = {
|
|
80
|
+
"elixir_phoenix": "mix format --check-formatted",
|
|
81
|
+
"python": "uv run ruff format --check",
|
|
82
|
+
"node": "npm run format:check 2>/dev/null || echo 'No format check configured'",
|
|
83
|
+
"rust": "cargo fmt --check",
|
|
84
|
+
"go": "gofmt -l .",
|
|
85
|
+
}
|
|
86
|
+
return commands.get(project_type, "echo 'No format check configured'")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _get_setup_command(project_type: str) -> str:
|
|
90
|
+
commands = {
|
|
91
|
+
"elixir_phoenix": "mix deps.get && mix deps.compile",
|
|
92
|
+
"python": "uv sync --extra dev",
|
|
93
|
+
"node": "npm install",
|
|
94
|
+
"rust": "cargo fetch",
|
|
95
|
+
"go": "go mod download",
|
|
96
|
+
}
|
|
97
|
+
return commands.get(project_type, "echo 'No setup required'")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _get_dev_command(project_type: str, repo_root: Path) -> str:
|
|
101
|
+
if project_type == "elixir_phoenix":
|
|
102
|
+
return "mix phx.server"
|
|
103
|
+
if project_type == "python":
|
|
104
|
+
pyproject = repo_root / "pyproject.toml"
|
|
105
|
+
if pyproject.exists():
|
|
106
|
+
content = pyproject.read_text()
|
|
107
|
+
if "uvicorn" in content.lower() or "fastapi" in content.lower():
|
|
108
|
+
name = repo_root.name.replace("-", "_")
|
|
109
|
+
return f"uv run uvicorn {name}:app --reload"
|
|
110
|
+
if "django" in content.lower():
|
|
111
|
+
return "uv run python manage.py runserver"
|
|
112
|
+
if "flask" in content.lower():
|
|
113
|
+
return "uv run flask --reload run"
|
|
114
|
+
return f"uv run python -m {repo_root.name}"
|
|
115
|
+
if project_type == "node":
|
|
116
|
+
return "npm run dev 2>/dev/null || npm run start"
|
|
117
|
+
if project_type == "rust":
|
|
118
|
+
return "cargo run"
|
|
119
|
+
if project_type == "go":
|
|
120
|
+
return "go run ."
|
|
121
|
+
return "echo 'No dev server configured'"
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _generate_mise_toml(project_type: str, repo_root: Path) -> str:
|
|
125
|
+
if project_type == "elixir_phoenix":
|
|
126
|
+
return """# Mise configuration for Elixir/Phoenix project
|
|
127
|
+
# Scaffolded by /deviate-init — DeviaTDD scaffolding
|
|
128
|
+
# ZERO-TEST-PASS: test task uses || true to pass when no tests exist
|
|
129
|
+
|
|
130
|
+
[tools]
|
|
131
|
+
elixir = "latest"
|
|
132
|
+
erlang = "latest"
|
|
133
|
+
|
|
134
|
+
[tasks]
|
|
135
|
+
# DeviaTDD zero-test-pass invariant: passes even with zero tests written
|
|
136
|
+
test = "mix test || true"
|
|
137
|
+
test-raw = "mix test"
|
|
138
|
+
setup = { depends = ["hooks"], run = "mix deps.get && mix deps.compile" }
|
|
139
|
+
lint = "mix credo --strict"
|
|
140
|
+
format = "mix format"
|
|
141
|
+
format-check = "mix format --check-formatted"
|
|
142
|
+
fix = { depends = ["format", "lint"] }
|
|
143
|
+
check = { depends = ["format-check", "lint", "test"] }
|
|
144
|
+
pre-commit = { depends = ["format-check", "lint"] }
|
|
145
|
+
pre-push = { depends = ["test"] }
|
|
146
|
+
hooks = "mise generate git-pre-commit --write && mise generate git-pre-commit --hook pre-push --write"
|
|
147
|
+
dev = "mix phx.server"
|
|
148
|
+
clean = "mix clean && rm -rf _build deps .fetch"
|
|
149
|
+
"""
|
|
150
|
+
if project_type == "python":
|
|
151
|
+
dev_cmd = _get_dev_command(project_type, repo_root)
|
|
152
|
+
return f"""# Mise configuration for Python project
|
|
153
|
+
# Scaffolded by /deviate-init — DeviaTDD scaffolding
|
|
154
|
+
# ZERO-TEST-PASS: test task uses || true to pass when no tests exist
|
|
155
|
+
|
|
156
|
+
[tools]
|
|
157
|
+
python = "3.12"
|
|
158
|
+
uv = "latest"
|
|
159
|
+
|
|
160
|
+
[tasks]
|
|
161
|
+
# DeviaTDD zero-test-pass invariant: passes even with zero tests written
|
|
162
|
+
test = "uv run pytest || true"
|
|
163
|
+
test-raw = "uv run pytest"
|
|
164
|
+
setup = {{ depends = ["hooks"], run = "uv sync --extra dev" }}
|
|
165
|
+
lint = "uv run ruff check"
|
|
166
|
+
format = "uv run ruff format"
|
|
167
|
+
format-check = "uv run ruff format --check"
|
|
168
|
+
fix = {{ depends = ["format", "lint"] }}
|
|
169
|
+
check = {{ depends = ["format-check", "lint", "test"] }}
|
|
170
|
+
pre-commit = {{ depends = ["format-check", "lint"] }}
|
|
171
|
+
pre-push = {{ depends = ["test"] }}
|
|
172
|
+
hooks = "mise generate git-pre-commit --write && mise generate git-pre-commit --hook pre-push --write"
|
|
173
|
+
dev = "{dev_cmd}"
|
|
174
|
+
clean = "rm -rf .venv dist build __pycache__"
|
|
175
|
+
"""
|
|
176
|
+
if project_type == "node":
|
|
177
|
+
pkg_manager = "npm"
|
|
178
|
+
if (repo_root / "pnpm-lock.yaml").exists():
|
|
179
|
+
pkg_manager = "pnpm"
|
|
180
|
+
elif (repo_root / "yarn.lock").exists():
|
|
181
|
+
pkg_manager = "yarn"
|
|
182
|
+
return f"""# Mise configuration for Node.js project
|
|
183
|
+
# Scaffolded by /deviate-init — DeviaTDD scaffolding
|
|
184
|
+
# ZERO-TEST-PASS: test task uses || true to pass when no tests exist
|
|
185
|
+
|
|
186
|
+
[tools]
|
|
187
|
+
node = "lts"
|
|
188
|
+
{pkg_manager} = "latest"
|
|
189
|
+
|
|
190
|
+
[tasks]
|
|
191
|
+
# DeviaTDD zero-test-pass invariant: passes even with zero tests written
|
|
192
|
+
test = "{pkg_manager} test || true"
|
|
193
|
+
test-raw = "{pkg_manager} test"
|
|
194
|
+
setup = {{ depends = ["hooks"], run = "{pkg_manager} install" }}
|
|
195
|
+
lint = "{pkg_manager} run lint 2>/dev/null || echo 'No lint configured'"
|
|
196
|
+
format = "{pkg_manager} run format 2>/dev/null || echo 'No formatter configured'"
|
|
197
|
+
format-check = "{pkg_manager} run format:check 2>/dev/null || echo 'No format check configured'"
|
|
198
|
+
fix = {{ depends = ["format", "lint"] }}
|
|
199
|
+
check = {{ depends = ["format-check", "lint", "test"] }}
|
|
200
|
+
pre-commit = {{ depends = ["format-check", "lint"] }}
|
|
201
|
+
pre-push = {{ depends = ["test"] }}
|
|
202
|
+
hooks = "mise generate git-pre-commit --write && mise generate git-pre-commit --hook pre-push --write"
|
|
203
|
+
dev = "{pkg_manager} run dev 2>/dev/null || {pkg_manager} run start"
|
|
204
|
+
clean = "rm -rf node_modules dist build"
|
|
205
|
+
"""
|
|
206
|
+
if project_type == "rust":
|
|
207
|
+
return """# Mise configuration for Rust project
|
|
208
|
+
# Scaffolded by /deviate-init — DeviaTDD scaffolding
|
|
209
|
+
# ZERO-TEST-PASS: test task uses || true to pass when no tests exist
|
|
210
|
+
|
|
211
|
+
[tools]
|
|
212
|
+
rust = "stable"
|
|
213
|
+
|
|
214
|
+
[tasks]
|
|
215
|
+
# DeviaTDD zero-test-pass invariant: passes even with zero tests written
|
|
216
|
+
test = "cargo test || true"
|
|
217
|
+
test-raw = "cargo test"
|
|
218
|
+
setup = { depends = ["hooks"], run = "cargo fetch" }
|
|
219
|
+
lint = "cargo clippy -- -D warnings"
|
|
220
|
+
format = "cargo fmt"
|
|
221
|
+
format-check = "cargo fmt --check"
|
|
222
|
+
fix = { depends = ["format", "lint"] }
|
|
223
|
+
check = { depends = ["format-check", "lint", "test"] }
|
|
224
|
+
pre-commit = { depends = ["format-check", "lint"] }
|
|
225
|
+
pre-push = { depends = ["test"] }
|
|
226
|
+
hooks = "mise generate git-pre-commit --write && mise generate git-pre-commit --hook pre-push --write"
|
|
227
|
+
dev = "cargo run"
|
|
228
|
+
clean = "cargo clean"
|
|
229
|
+
"""
|
|
230
|
+
if project_type == "go":
|
|
231
|
+
return """# Mise configuration for Go project
|
|
232
|
+
# Scaffolded by /deviate-init — DeviaTDD scaffolding
|
|
233
|
+
# ZERO-TEST-PASS: test task uses || true to pass when no tests exist
|
|
234
|
+
|
|
235
|
+
[tools]
|
|
236
|
+
go = "latest"
|
|
237
|
+
|
|
238
|
+
[tasks]
|
|
239
|
+
# DeviaTDD zero-test-pass invariant: passes even with zero tests written
|
|
240
|
+
test = "go test ./... || true"
|
|
241
|
+
test-raw = "go test ./..."
|
|
242
|
+
setup = { depends = ["hooks"], run = "go mod download" }
|
|
243
|
+
lint = "golangci-lint run 2>/dev/null || echo 'No linter configured'"
|
|
244
|
+
format = "gofmt -w ."
|
|
245
|
+
format-check = "gofmt -l ."
|
|
246
|
+
fix = { depends = ["format", "lint"] }
|
|
247
|
+
check = { depends = ["format-check", "lint", "test"] }
|
|
248
|
+
pre-commit = { depends = ["format-check", "lint"] }
|
|
249
|
+
pre-push = { depends = ["test"] }
|
|
250
|
+
hooks = "mise generate git-pre-commit --write && mise generate git-pre-commit --hook pre-push --write"
|
|
251
|
+
dev = "go run ."
|
|
252
|
+
clean = "go clean"
|
|
253
|
+
"""
|
|
254
|
+
_fail_with(f"Unknown project type: {project_type}")
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _scaffold_constitution(project_type: str, repo_root: Path) -> None:
|
|
258
|
+
test_cmd = _get_test_command(project_type)
|
|
259
|
+
lint_cmd = _get_lint_command(project_type)
|
|
260
|
+
|
|
261
|
+
constitution_path = repo_root / "specs" / "constitution.md"
|
|
262
|
+
constitution_path.write_text(
|
|
263
|
+
f"""# Project Constitution
|
|
264
|
+
|
|
265
|
+
Version: 0.1.0
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## 1. Architectural Principles
|
|
270
|
+
> TBD — populated by `/research` from codebase analysis.
|
|
271
|
+
|
|
272
|
+
## 2. Tech Stack Standards
|
|
273
|
+
|
|
274
|
+
### Backend
|
|
275
|
+
> TBD
|
|
276
|
+
|
|
277
|
+
### Frontend
|
|
278
|
+
> TBD
|
|
279
|
+
|
|
280
|
+
### Database
|
|
281
|
+
> TBD
|
|
282
|
+
|
|
283
|
+
### Infrastructure
|
|
284
|
+
> TBD
|
|
285
|
+
|
|
286
|
+
### Tooling
|
|
287
|
+
> TBD
|
|
288
|
+
|
|
289
|
+
## 3. Testing Protocols
|
|
290
|
+
|
|
291
|
+
### Framework
|
|
292
|
+
- `TEST_COMMAND`: `{test_cmd}`
|
|
293
|
+
- `LINT_COMMAND`: `{lint_cmd}`
|
|
294
|
+
|
|
295
|
+
### Coverage
|
|
296
|
+
> TBD
|
|
297
|
+
|
|
298
|
+
## 4. Development Workflow
|
|
299
|
+
> TBD — populated by `/research`.
|
|
300
|
+
|
|
301
|
+
## 5. Definition of Done
|
|
302
|
+
- [ ] Code implemented
|
|
303
|
+
- [ ] Tests passing
|
|
304
|
+
- [ ] Lint passing
|
|
305
|
+
- [ ] Documentation updated
|
|
306
|
+
- [ ] No governance violations
|
|
307
|
+
|
|
308
|
+
## 6. Version History
|
|
309
|
+
|
|
310
|
+
- 0.1.0 — Initial constitution scaffolded by `deviate init`
|
|
311
|
+
""",
|
|
312
|
+
encoding="utf-8",
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _check_tool(name: str) -> bool:
|
|
317
|
+
try:
|
|
318
|
+
subprocess.run(
|
|
319
|
+
["which", name],
|
|
320
|
+
capture_output=True,
|
|
321
|
+
check=False,
|
|
322
|
+
)
|
|
323
|
+
return True
|
|
324
|
+
except Exception:
|
|
325
|
+
return False
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
@init_app.command()
|
|
329
|
+
def pre() -> None:
|
|
330
|
+
"""Detect project type, scaffold DeviaTDD structure, emit JSON contract."""
|
|
331
|
+
try:
|
|
332
|
+
repo_root = Path(
|
|
333
|
+
subprocess.check_output(
|
|
334
|
+
["git", "rev-parse", "--show-toplevel"],
|
|
335
|
+
text=True,
|
|
336
|
+
stderr=subprocess.DEVNULL,
|
|
337
|
+
).strip()
|
|
338
|
+
)
|
|
339
|
+
except subprocess.CalledProcessError:
|
|
340
|
+
_fail_with("Not a git repository")
|
|
341
|
+
|
|
342
|
+
try:
|
|
343
|
+
branch = subprocess.check_output(
|
|
344
|
+
["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
|
345
|
+
text=True,
|
|
346
|
+
stderr=subprocess.DEVNULL,
|
|
347
|
+
).strip()
|
|
348
|
+
except subprocess.CalledProcessError:
|
|
349
|
+
branch = "unknown"
|
|
350
|
+
|
|
351
|
+
project_type = _detect_project_type(repo_root)
|
|
352
|
+
|
|
353
|
+
has_mise_toml = (repo_root / "mise.toml").exists()
|
|
354
|
+
has_specs_dir = (repo_root / "specs").exists()
|
|
355
|
+
has_constitution = (repo_root / "specs" / "constitution.md").exists()
|
|
356
|
+
has_issues_ledger = (repo_root / "specs" / "issues.jsonl").exists()
|
|
357
|
+
has_claude_md = (repo_root / "CLAUDE.md").exists()
|
|
358
|
+
|
|
359
|
+
mise_available = _check_tool("mise")
|
|
360
|
+
tooling = {
|
|
361
|
+
"mise": mise_available,
|
|
362
|
+
"jq": _check_tool("jq"),
|
|
363
|
+
"gh": _check_tool("gh"),
|
|
364
|
+
"uv": _check_tool("uv"),
|
|
365
|
+
"ruff": _check_tool("ruff"),
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
artifacts_created = []
|
|
369
|
+
|
|
370
|
+
if not has_mise_toml and project_type != "unknown":
|
|
371
|
+
mise_content = _generate_mise_toml(project_type, repo_root)
|
|
372
|
+
(repo_root / "mise.toml").write_text(mise_content, encoding="utf-8")
|
|
373
|
+
has_mise_toml = True
|
|
374
|
+
artifacts_created.append("mise.toml")
|
|
375
|
+
|
|
376
|
+
if not has_specs_dir:
|
|
377
|
+
(repo_root / "specs").mkdir(exist_ok=True)
|
|
378
|
+
has_specs_dir = True
|
|
379
|
+
artifacts_created.append("specs/")
|
|
380
|
+
else:
|
|
381
|
+
artifacts_created.append("specs/")
|
|
382
|
+
|
|
383
|
+
if not has_issues_ledger:
|
|
384
|
+
(repo_root / "specs" / "issues.jsonl").touch()
|
|
385
|
+
has_issues_ledger = True
|
|
386
|
+
artifacts_created.append("specs/issues.jsonl")
|
|
387
|
+
|
|
388
|
+
if not has_constitution:
|
|
389
|
+
_scaffold_constitution(project_type, repo_root)
|
|
390
|
+
has_constitution = True
|
|
391
|
+
artifacts_created.append("specs/constitution.md")
|
|
392
|
+
|
|
393
|
+
from deviate.cli import _linkify_governance_files
|
|
394
|
+
|
|
395
|
+
_linkify_governance_files(repo_root)
|
|
396
|
+
if (repo_root / "AGENTS.md").is_symlink():
|
|
397
|
+
artifacts_created.append("AGENTS.md")
|
|
398
|
+
|
|
399
|
+
# Provision union-merge rules for append-only JSONL ledgers.
|
|
400
|
+
# Idempotent: never duplicates entries, never overwrites user content.
|
|
401
|
+
from deviate.cli import _ensure_root_gitattributes
|
|
402
|
+
|
|
403
|
+
_ensure_root_gitattributes(repo_root)
|
|
404
|
+
if (repo_root / ".gitattributes").exists():
|
|
405
|
+
artifacts_created.append(".gitattributes")
|
|
406
|
+
|
|
407
|
+
try:
|
|
408
|
+
top_level_entries = [
|
|
409
|
+
p.name for p in repo_root.iterdir() if p.name not in (".git",)
|
|
410
|
+
]
|
|
411
|
+
except Exception:
|
|
412
|
+
top_level_entries = []
|
|
413
|
+
|
|
414
|
+
contract = {
|
|
415
|
+
"phase": "deviate-init",
|
|
416
|
+
"status": "READY",
|
|
417
|
+
"branch": branch,
|
|
418
|
+
"repo_root": str(repo_root),
|
|
419
|
+
"project_type": project_type,
|
|
420
|
+
"tooling": tooling,
|
|
421
|
+
"mise_available": mise_available,
|
|
422
|
+
"gh_available": _check_tool("gh"),
|
|
423
|
+
"existing_artifacts": {
|
|
424
|
+
"mise_toml": has_mise_toml,
|
|
425
|
+
"specs_dir": has_specs_dir,
|
|
426
|
+
"constitution": has_constitution,
|
|
427
|
+
"issues_ledger": has_issues_ledger,
|
|
428
|
+
"claude_md": has_claude_md,
|
|
429
|
+
},
|
|
430
|
+
"artifacts_created": artifacts_created,
|
|
431
|
+
"top_level_entries": top_level_entries,
|
|
432
|
+
"timestamp": subprocess.check_output(
|
|
433
|
+
["date", "-u", "+%Y-%m-%dT%H:%M:%SZ"],
|
|
434
|
+
text=True,
|
|
435
|
+
).strip(),
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
print(json.dumps(contract, indent=2))
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
@init_app.command()
|
|
442
|
+
def post() -> None:
|
|
443
|
+
"""Validate artifacts, stage for commit, emit status JSON."""
|
|
444
|
+
try:
|
|
445
|
+
repo_root = Path(
|
|
446
|
+
subprocess.check_output(
|
|
447
|
+
["git", "rev-parse", "--show-toplevel"],
|
|
448
|
+
text=True,
|
|
449
|
+
stderr=subprocess.DEVNULL,
|
|
450
|
+
).strip()
|
|
451
|
+
)
|
|
452
|
+
except subprocess.CalledProcessError:
|
|
453
|
+
_fail_with("Not a git repository")
|
|
454
|
+
|
|
455
|
+
artifacts = []
|
|
456
|
+
if (repo_root / "mise.toml").exists():
|
|
457
|
+
artifacts.append("mise.toml")
|
|
458
|
+
if (repo_root / "specs").is_dir():
|
|
459
|
+
artifacts.append("specs/")
|
|
460
|
+
if (repo_root / "specs" / "constitution.md").exists():
|
|
461
|
+
artifacts.append("specs/constitution.md")
|
|
462
|
+
if (repo_root / "specs" / "issues.jsonl").exists():
|
|
463
|
+
artifacts.append("specs/issues.jsonl")
|
|
464
|
+
if (repo_root / "AGENTS.md").is_symlink():
|
|
465
|
+
artifacts.append("AGENTS.md")
|
|
466
|
+
if (repo_root / ".gitattributes").exists():
|
|
467
|
+
artifacts.append(".gitattributes")
|
|
468
|
+
|
|
469
|
+
if artifacts:
|
|
470
|
+
subprocess.run(
|
|
471
|
+
["git", "add"] + artifacts,
|
|
472
|
+
cwd=repo_root,
|
|
473
|
+
check=False,
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
print(
|
|
477
|
+
json.dumps(
|
|
478
|
+
{
|
|
479
|
+
"status": "SUCCESS",
|
|
480
|
+
"artifacts_created": artifacts,
|
|
481
|
+
"artifact_count": len(artifacts),
|
|
482
|
+
},
|
|
483
|
+
indent=2,
|
|
484
|
+
)
|
|
485
|
+
)
|
deviate/cli/inspect.py
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import subprocess
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
|
|
11
|
+
from deviate.cli.meso import _resolve_bucket_dir, _source_stem
|
|
12
|
+
from deviate.core._shared import git_env as _git_env
|
|
13
|
+
from deviate.core.worktree import detect_remote
|
|
14
|
+
from deviate.state.ledger import (
|
|
15
|
+
IssueRecord,
|
|
16
|
+
LedgerFilter,
|
|
17
|
+
_read_ledger_strict,
|
|
18
|
+
filter_tasks,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
inspect_app = typer.Typer(no_args_is_help=True)
|
|
22
|
+
issues_app = typer.Typer(no_args_is_help=True)
|
|
23
|
+
tasks_app = typer.Typer(no_args_is_help=True)
|
|
24
|
+
inspect_app.add_typer(issues_app, name="issues")
|
|
25
|
+
inspect_app.add_typer(tasks_app, name="tasks")
|
|
26
|
+
|
|
27
|
+
console = Console()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _derive_issue_branch(source_file: str) -> str:
|
|
31
|
+
bucket = _resolve_bucket_dir(source_file)
|
|
32
|
+
slug = _source_stem(source_file)
|
|
33
|
+
return f"feat/{bucket}/{slug}"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _check_orphan_claim(issue: IssueRecord, repo: Path) -> bool | None:
|
|
37
|
+
if not issue.source_file:
|
|
38
|
+
return None
|
|
39
|
+
branch = _derive_issue_branch(issue.source_file)
|
|
40
|
+
try:
|
|
41
|
+
remote = detect_remote(repo)
|
|
42
|
+
except RuntimeError:
|
|
43
|
+
return None
|
|
44
|
+
try:
|
|
45
|
+
result = subprocess.run(
|
|
46
|
+
["git", "ls-remote", "--heads", remote, branch],
|
|
47
|
+
cwd=repo,
|
|
48
|
+
env=_git_env(),
|
|
49
|
+
capture_output=True,
|
|
50
|
+
text=True,
|
|
51
|
+
timeout=30,
|
|
52
|
+
)
|
|
53
|
+
except (subprocess.TimeoutExpired, OSError):
|
|
54
|
+
return None
|
|
55
|
+
if result.returncode != 0:
|
|
56
|
+
return None
|
|
57
|
+
return not bool(result.stdout.strip())
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _deduplicate_issues(records: list[dict]) -> list[dict]:
|
|
61
|
+
"""Deduplicate issue records by ``issue_id``.
|
|
62
|
+
|
|
63
|
+
``COMPLETED`` is a terminal status and always takes precedence: once any
|
|
64
|
+
``COMPLETED`` entry is captured for an issue, subsequent non-``COMPLETED``
|
|
65
|
+
transitions (e.g. a ``SPECIFIED`` entry appended after the ``COMPLETED``
|
|
66
|
+
write during a merge flow) do not override it. Among non-``COMPLETED``
|
|
67
|
+
entries, the last entry by file position wins (the prior behaviour).
|
|
68
|
+
"""
|
|
69
|
+
seen: dict[str, dict] = {}
|
|
70
|
+
for rec in records:
|
|
71
|
+
issue_id = rec.get("issue_id")
|
|
72
|
+
if not issue_id:
|
|
73
|
+
continue
|
|
74
|
+
current = seen.get(issue_id)
|
|
75
|
+
# Preserve any COMPLETED entry already captured — COMPLETED is terminal.
|
|
76
|
+
if current is not None and current.get("status") == "COMPLETED":
|
|
77
|
+
continue
|
|
78
|
+
seen[issue_id] = rec
|
|
79
|
+
return list(seen.values())
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _issues_list(
|
|
83
|
+
type_filter: str | None = None,
|
|
84
|
+
status_filter: str | None = None,
|
|
85
|
+
) -> list[dict]:
|
|
86
|
+
ledger_path = Path.cwd() / "specs" / "issues.jsonl"
|
|
87
|
+
records = _read_ledger_strict(ledger_path)
|
|
88
|
+
issues = _deduplicate_issues(records)
|
|
89
|
+
if type_filter:
|
|
90
|
+
issues = [i for i in issues if i.get("type") == type_filter]
|
|
91
|
+
if status_filter:
|
|
92
|
+
issues = [i for i in issues if i.get("status") == status_filter]
|
|
93
|
+
result: list[dict] = []
|
|
94
|
+
for raw in issues:
|
|
95
|
+
entry: dict = {
|
|
96
|
+
"issue_id": raw.get("issue_id", ""),
|
|
97
|
+
"type": raw.get("type", ""),
|
|
98
|
+
"title": raw.get("title", ""),
|
|
99
|
+
"status": raw.get("status", ""),
|
|
100
|
+
"source_file": raw.get("source_file", ""),
|
|
101
|
+
"blocked_by": raw.get("blocked_by", []),
|
|
102
|
+
"coordinates_with": raw.get("coordinates_with", []),
|
|
103
|
+
}
|
|
104
|
+
if raw.get("status") == "SPECIFIED":
|
|
105
|
+
try:
|
|
106
|
+
issue_record = IssueRecord.model_validate(raw)
|
|
107
|
+
orphan = _check_orphan_claim(issue_record, Path.cwd())
|
|
108
|
+
entry["orphan_claim"] = orphan
|
|
109
|
+
except Exception:
|
|
110
|
+
entry["orphan_claim"] = None
|
|
111
|
+
else:
|
|
112
|
+
entry["orphan_claim"] = None
|
|
113
|
+
result.append(entry)
|
|
114
|
+
return result
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@issues_app.command("list")
|
|
118
|
+
def issues_list_command(
|
|
119
|
+
type_filter: str | None = typer.Option(None, "--type", help="Filter by issue type"),
|
|
120
|
+
status_filter: str | None = typer.Option(
|
|
121
|
+
None, "--status", help="Filter by issue status"
|
|
122
|
+
),
|
|
123
|
+
json_flag: bool = typer.Option(False, "--json", help="Output as JSON array"),
|
|
124
|
+
quiet: bool = typer.Option(False, "--quiet", help="Suppress non-JSON output"),
|
|
125
|
+
) -> None:
|
|
126
|
+
issues = _issues_list(
|
|
127
|
+
type_filter=type_filter,
|
|
128
|
+
status_filter=status_filter,
|
|
129
|
+
)
|
|
130
|
+
if json_flag:
|
|
131
|
+
typer.echo(json.dumps(issues))
|
|
132
|
+
elif quiet:
|
|
133
|
+
pass
|
|
134
|
+
else:
|
|
135
|
+
table = Table(title="Issues")
|
|
136
|
+
table.add_column("ID", style="cyan")
|
|
137
|
+
table.add_column("Type", style="magenta")
|
|
138
|
+
table.add_column("Title")
|
|
139
|
+
table.add_column("Status", style="green")
|
|
140
|
+
table.add_column("Orphan")
|
|
141
|
+
for issue in issues:
|
|
142
|
+
orphan_str = ""
|
|
143
|
+
if issue.get("orphan_claim") is True:
|
|
144
|
+
orphan_str = "\U0001f7e1 ORPHAN_CLAIM"
|
|
145
|
+
elif issue.get("orphan_claim") is False:
|
|
146
|
+
orphan_str = ""
|
|
147
|
+
table.add_row(
|
|
148
|
+
issue.get("issue_id", ""),
|
|
149
|
+
issue.get("type", ""),
|
|
150
|
+
issue.get("title", ""),
|
|
151
|
+
issue.get("status", ""),
|
|
152
|
+
orphan_str,
|
|
153
|
+
)
|
|
154
|
+
console.print(table)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _tasks_list(
|
|
158
|
+
status_filter: str | None = None,
|
|
159
|
+
) -> list[dict]:
|
|
160
|
+
tasks_ledger = Path.cwd() / "tasks.jsonl"
|
|
161
|
+
filter_obj = LedgerFilter(
|
|
162
|
+
entity_type="task",
|
|
163
|
+
status_filter=status_filter or None,
|
|
164
|
+
)
|
|
165
|
+
records = filter_tasks(tasks_ledger, filter_obj)
|
|
166
|
+
return [
|
|
167
|
+
{
|
|
168
|
+
"id": t.id,
|
|
169
|
+
"issue_id": t.issue_id,
|
|
170
|
+
"description": t.description,
|
|
171
|
+
"status": t.status,
|
|
172
|
+
"execution_mode": t.execution_mode,
|
|
173
|
+
}
|
|
174
|
+
for t in records
|
|
175
|
+
]
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@tasks_app.command("list")
|
|
179
|
+
def tasks_list_command(
|
|
180
|
+
status_filter: str | None = typer.Option(
|
|
181
|
+
None, "--status", help="Filter by task status"
|
|
182
|
+
),
|
|
183
|
+
json_flag: bool = typer.Option(False, "--json", help="Output as JSON array"),
|
|
184
|
+
quiet: bool = typer.Option(False, "--quiet", help="Suppress non-JSON output"),
|
|
185
|
+
) -> None:
|
|
186
|
+
tasks = _tasks_list(
|
|
187
|
+
status_filter=status_filter,
|
|
188
|
+
)
|
|
189
|
+
if json_flag:
|
|
190
|
+
typer.echo(json.dumps(tasks))
|
|
191
|
+
elif quiet:
|
|
192
|
+
pass
|
|
193
|
+
else:
|
|
194
|
+
table = Table(title="Tasks")
|
|
195
|
+
table.add_column("ID", style="cyan")
|
|
196
|
+
table.add_column("Issue ID")
|
|
197
|
+
table.add_column("Description")
|
|
198
|
+
table.add_column("Status", style="green")
|
|
199
|
+
table.add_column("Mode", style="magenta")
|
|
200
|
+
for task in tasks:
|
|
201
|
+
table.add_row(
|
|
202
|
+
task.get("id", ""),
|
|
203
|
+
task.get("issue_id", ""),
|
|
204
|
+
task.get("description", ""),
|
|
205
|
+
task.get("status", ""),
|
|
206
|
+
task.get("execution_mode", ""),
|
|
207
|
+
)
|
|
208
|
+
console.print(table)
|