devcouncil 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (190) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +197 -494
  3. package/package.json +9 -2
  4. package/pyproject.toml +62 -27
  5. package/src/devcouncil/__main__.py +4 -4
  6. package/src/devcouncil/app/__init__.py +28 -28
  7. package/src/devcouncil/app/config.py +297 -108
  8. package/src/devcouncil/app/errors.py +23 -23
  9. package/src/devcouncil/app/events.py +44 -44
  10. package/src/devcouncil/app/orchestrator.py +67 -67
  11. package/src/devcouncil/app/project_status.py +29 -0
  12. package/src/devcouncil/app/run_context.py +39 -39
  13. package/src/devcouncil/app/state_machine.py +108 -108
  14. package/src/devcouncil/artifacts/__init__.py +1 -1
  15. package/src/devcouncil/artifacts/coverage.py +96 -96
  16. package/src/devcouncil/artifacts/graph.py +163 -143
  17. package/src/devcouncil/artifacts/migrations.py +20 -20
  18. package/src/devcouncil/artifacts/schemas.py +23 -23
  19. package/src/devcouncil/artifacts/serializer.py +21 -21
  20. package/src/devcouncil/artifacts/validators.py +27 -27
  21. package/src/devcouncil/assets/__init__.py +1 -0
  22. package/src/devcouncil/assets/devcouncil-logo.svg +60 -0
  23. package/src/devcouncil/assets/devcouncil_logo_premium.png +0 -0
  24. package/src/devcouncil/cli/commands/agents.py +292 -0
  25. package/src/devcouncil/cli/commands/artifacts.py +54 -48
  26. package/src/devcouncil/cli/commands/ast.py +22 -0
  27. package/src/devcouncil/cli/commands/baseline.py +35 -32
  28. package/src/devcouncil/cli/commands/check.py +209 -0
  29. package/src/devcouncil/cli/commands/config.py +115 -54
  30. package/src/devcouncil/cli/commands/cost.py +57 -0
  31. package/src/devcouncil/cli/commands/dashboard.py +31 -0
  32. package/src/devcouncil/cli/commands/doctor.py +291 -47
  33. package/src/devcouncil/cli/commands/evidence.py +48 -0
  34. package/src/devcouncil/cli/commands/go.py +656 -0
  35. package/src/devcouncil/cli/commands/handoff.py +69 -0
  36. package/src/devcouncil/cli/commands/hook.py +209 -33
  37. package/src/devcouncil/cli/commands/init.py +204 -57
  38. package/src/devcouncil/cli/commands/integrate.py +1171 -76
  39. package/src/devcouncil/cli/commands/lsp.py +20 -0
  40. package/src/devcouncil/cli/commands/map.py +96 -22
  41. package/src/devcouncil/cli/commands/plan.py +422 -210
  42. package/src/devcouncil/cli/commands/prompt.py +48 -34
  43. package/src/devcouncil/cli/commands/repair.py +89 -69
  44. package/src/devcouncil/cli/commands/report.py +120 -54
  45. package/src/devcouncil/cli/commands/reset_demo_state.py +33 -28
  46. package/src/devcouncil/cli/commands/rollback.py +55 -54
  47. package/src/devcouncil/cli/commands/run.py +285 -220
  48. package/src/devcouncil/cli/commands/runs.py +223 -0
  49. package/src/devcouncil/cli/commands/scaffold.py +32 -0
  50. package/src/devcouncil/cli/commands/semantic.py +47 -0
  51. package/src/devcouncil/cli/commands/setup.py +300 -20
  52. package/src/devcouncil/cli/commands/shell.py +73 -0
  53. package/src/devcouncil/cli/commands/show.py +76 -57
  54. package/src/devcouncil/cli/commands/skills.py +88 -0
  55. package/src/devcouncil/cli/commands/status.py +141 -105
  56. package/src/devcouncil/cli/commands/tasks.py +55 -41
  57. package/src/devcouncil/cli/commands/trace.py +49 -4
  58. package/src/devcouncil/cli/commands/verify.py +293 -128
  59. package/src/devcouncil/cli/commands/version.py +20 -20
  60. package/src/devcouncil/cli/commands/watch.py +574 -0
  61. package/src/devcouncil/cli/commands/watch_fs.py +40 -0
  62. package/src/devcouncil/cli/main.py +92 -25
  63. package/src/devcouncil/council/prompts/arbiter.md +19 -19
  64. package/src/devcouncil/council/prompts/critic_a.md +10 -10
  65. package/src/devcouncil/council/prompts/critic_b.md +10 -10
  66. package/src/devcouncil/council/prompts/implementation_reviewer.md +16 -16
  67. package/src/devcouncil/council/prompts/planner_a.md +16 -16
  68. package/src/devcouncil/council/prompts/planner_b.md +16 -16
  69. package/src/devcouncil/council/prompts/rebuttal.md +10 -10
  70. package/src/devcouncil/council/prompts/spec_writer.md +12 -12
  71. package/src/devcouncil/domain/assumption.py +17 -17
  72. package/src/devcouncil/domain/critique.py +32 -32
  73. package/src/devcouncil/domain/evidence.py +47 -27
  74. package/src/devcouncil/domain/gap.py +52 -26
  75. package/src/devcouncil/domain/requirement.py +22 -22
  76. package/src/devcouncil/domain/task.py +55 -26
  77. package/src/devcouncil/execution/__init__.py +1 -1
  78. package/src/devcouncil/execution/checkpoints.py +246 -0
  79. package/src/devcouncil/execution/context_builder.py +54 -54
  80. package/src/devcouncil/execution/executor.py +15 -15
  81. package/src/devcouncil/execution/fs_watcher.py +180 -0
  82. package/src/devcouncil/execution/handoff.py +102 -0
  83. package/src/devcouncil/execution/hook_policy.py +186 -77
  84. package/src/devcouncil/execution/patch.py +77 -28
  85. package/src/devcouncil/execution/permissions.py +52 -59
  86. package/src/devcouncil/execution/policy_engine.py +343 -0
  87. package/src/devcouncil/execution/prompt_builder.py +650 -38
  88. package/src/devcouncil/execution/shell_session.py +225 -0
  89. package/src/devcouncil/execution/task_runner.py +68 -64
  90. package/src/devcouncil/executors/__init__.py +1 -1
  91. package/src/devcouncil/executors/agent_registry.py +575 -0
  92. package/src/devcouncil/executors/coding_cli.py +736 -0
  93. package/src/devcouncil/executors/mini_swe.py +63 -63
  94. package/src/devcouncil/executors/native/agent.py +186 -85
  95. package/src/devcouncil/executors/openhands.py +56 -56
  96. package/src/devcouncil/gating/__init__.py +1 -1
  97. package/src/devcouncil/gating/checks/clean_git.py +52 -45
  98. package/src/devcouncil/gating/checks/planned_files_check.py +32 -32
  99. package/src/devcouncil/gating/checks/requirement_coverage.py +26 -26
  100. package/src/devcouncil/gating/checks/secret_scan_check.py +53 -34
  101. package/src/devcouncil/gating/policy.py +315 -167
  102. package/src/devcouncil/hardware.py +184 -0
  103. package/src/devcouncil/indexing/__init__.py +1 -1
  104. package/src/devcouncil/indexing/ast_matcher.py +168 -0
  105. package/src/devcouncil/indexing/graph_index.py +48 -48
  106. package/src/devcouncil/indexing/lsp.py +161 -0
  107. package/src/devcouncil/indexing/repo_mapper.py +1455 -204
  108. package/src/devcouncil/indexing/semantic_index.py +205 -0
  109. package/src/devcouncil/integrations/actions.py +146 -0
  110. package/src/devcouncil/integrations/check.py +423 -0
  111. package/src/devcouncil/integrations/github.py +35 -35
  112. package/src/devcouncil/integrations/github_intent.py +142 -0
  113. package/src/devcouncil/integrations/gitnexus.py +62 -27
  114. package/src/devcouncil/integrations/graphify.py +34 -34
  115. package/src/devcouncil/integrations/mcp/server.py +2072 -96
  116. package/src/devcouncil/integrations/opencode_devcouncil_plugin.mjs +24 -0
  117. package/src/devcouncil/integrations/pr_comments.py +62 -0
  118. package/src/devcouncil/live/__init__.py +2 -0
  119. package/src/devcouncil/live/cards.py +349 -0
  120. package/src/devcouncil/live/models.py +63 -0
  121. package/src/devcouncil/live/repair_prompt.py +83 -0
  122. package/src/devcouncil/live/reviewer.py +70 -0
  123. package/src/devcouncil/live/signals.py +135 -0
  124. package/src/devcouncil/live/summary.py +34 -0
  125. package/src/devcouncil/live/tasks.py +18 -0
  126. package/src/devcouncil/live/transcripts.py +141 -0
  127. package/src/devcouncil/llm/__init__.py +1 -1
  128. package/src/devcouncil/llm/cache.py +42 -38
  129. package/src/devcouncil/llm/model_defaults.yaml +44 -0
  130. package/src/devcouncil/llm/provider.py +627 -125
  131. package/src/devcouncil/llm/router.py +303 -118
  132. package/src/devcouncil/optimization/__init__.py +1 -0
  133. package/src/devcouncil/optimization/gepa_agent.py +318 -0
  134. package/src/devcouncil/planning/__init__.py +1 -1
  135. package/src/devcouncil/planning/arbiter_service.py +57 -57
  136. package/src/devcouncil/planning/correction_manifest.py +303 -0
  137. package/src/devcouncil/planning/critique_service.py +71 -66
  138. package/src/devcouncil/planning/plan_service.py +60 -46
  139. package/src/devcouncil/planning/prompt_enhancer_service.py +167 -0
  140. package/src/devcouncil/planning/repair_service.py +39 -39
  141. package/src/devcouncil/planning/spec_service.py +70 -44
  142. package/src/devcouncil/repo/ci_scaffold.py +157 -0
  143. package/src/devcouncil/repo/gitignore.py +123 -0
  144. package/src/devcouncil/repo/sca.py +374 -0
  145. package/src/devcouncil/reporting/github_check.py +32 -32
  146. package/src/devcouncil/reporting/json_report.py +30 -17
  147. package/src/devcouncil/reporting/markdown_report.py +83 -46
  148. package/src/devcouncil/reporting/report_builder.py +14 -14
  149. package/src/devcouncil/skills/__init__.py +19 -0
  150. package/src/devcouncil/skills/library/README.md +46 -0
  151. package/src/devcouncil/skills/library/ai-training.md +50 -0
  152. package/src/devcouncil/skills/library/android.md +50 -0
  153. package/src/devcouncil/skills/library/backend.md +52 -0
  154. package/src/devcouncil/skills/library/core-engineering.md +95 -0
  155. package/src/devcouncil/skills/library/data-engineering.md +47 -0
  156. package/src/devcouncil/skills/library/desktop.md +46 -0
  157. package/src/devcouncil/skills/library/devops.md +48 -0
  158. package/src/devcouncil/skills/library/game-dev.md +46 -0
  159. package/src/devcouncil/skills/library/ios.md +48 -0
  160. package/src/devcouncil/skills/library/mobile-cross-platform.md +46 -0
  161. package/src/devcouncil/skills/library/security.md +48 -0
  162. package/src/devcouncil/skills/library/systems.md +48 -0
  163. package/src/devcouncil/skills/library/web.md +47 -0
  164. package/src/devcouncil/skills/library/windows.md +47 -0
  165. package/src/devcouncil/skills/registry.py +330 -0
  166. package/src/devcouncil/storage/db.py +147 -66
  167. package/src/devcouncil/storage/models.py +204 -83
  168. package/src/devcouncil/storage/native.py +557 -0
  169. package/src/devcouncil/storage/repositories.py +388 -249
  170. package/src/devcouncil/telemetry/cost.py +140 -34
  171. package/src/devcouncil/telemetry/model_pricing.yaml +48 -0
  172. package/src/devcouncil/telemetry/pricing.py +28 -0
  173. package/src/devcouncil/telemetry/traces.py +62 -7
  174. package/src/devcouncil/telemetry/tracker.py +52 -49
  175. package/src/devcouncil/ui/__init__.py +1 -0
  176. package/src/devcouncil/ui/dashboard.py +423 -0
  177. package/src/devcouncil/utils/__init__.py +1 -1
  178. package/src/devcouncil/utils/redaction.py +147 -141
  179. package/src/devcouncil/utils/subprocess_env.py +69 -0
  180. package/src/devcouncil/verification/__init__.py +1 -1
  181. package/src/devcouncil/verification/acceptance_compiler.py +125 -0
  182. package/src/devcouncil/verification/ad_hoc_check.py +129 -0
  183. package/src/devcouncil/verification/diff_coverage.py +353 -0
  184. package/src/devcouncil/verification/implementation_reviewer.py +55 -55
  185. package/src/devcouncil/verification/next_actions.py +189 -0
  186. package/src/devcouncil/verification/sandbox.py +178 -0
  187. package/src/devcouncil/verification/test_resolver.py +91 -0
  188. package/src/devcouncil/verification/verifier.py +1342 -307
  189. package/uv.lock +205 -64
  190. package/src/devcouncil/indexing/symbol_index.py +0 -0
@@ -0,0 +1,178 @@
1
+ """Verification sandbox abstraction."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import platform
7
+ import subprocess
8
+ import sys
9
+ from pathlib import Path
10
+ from typing import Literal
11
+
12
+ from pydantic import BaseModel
13
+
14
+ from devcouncil.app.config import DevCouncilConfig, load_config
15
+ from devcouncil.domain.task import Task
16
+ from devcouncil.storage.db import get_db
17
+ from devcouncil.storage.native import VerificationRunRepository
18
+ from devcouncil.verification.verifier import Verifier
19
+
20
+
21
+ class SandboxResult(BaseModel):
22
+ sandbox: str
23
+ status: Literal["passed", "failed", "unsupported"]
24
+ environment: dict[str, str]
25
+ commands: list[dict]
26
+
27
+
28
+ class VerificationSandbox:
29
+ def run(self, task: Task, commands: list[str], requirements: list) -> SandboxResult:
30
+ raise NotImplementedError
31
+
32
+
33
+ def _save_run(
34
+ project_root: Path,
35
+ task: Task,
36
+ sandbox: str,
37
+ environment: dict[str, str],
38
+ commands: list[dict],
39
+ status: Literal["passed", "failed", "unsupported"],
40
+ ) -> None:
41
+ db = get_db(project_root)
42
+ if not db:
43
+ return
44
+ with db.get_session() as session:
45
+ VerificationRunRepository(session).save(
46
+ task.id,
47
+ sandbox,
48
+ environment,
49
+ commands,
50
+ status,
51
+ )
52
+
53
+
54
+ class LocalSandbox(VerificationSandbox):
55
+ def __init__(self, project_root: Path):
56
+ self.project_root = project_root
57
+
58
+ def run(self, task: Task, commands: list[str], requirements: list) -> SandboxResult:
59
+ import asyncio
60
+
61
+ gaps, _ = asyncio.run(Verifier(self.project_root).verify_task(task, requirements))
62
+ status: Literal["passed", "failed", "unsupported"] = (
63
+ "failed" if any(g.blocking for g in gaps) else "passed"
64
+ )
65
+ env = _environment_metadata(self.project_root)
66
+ command_results = [{"command": cmd, "status": status} for cmd in commands]
67
+ _save_run(self.project_root, task, "local", env, command_results, status)
68
+ return SandboxResult(sandbox="local", status=status, environment=env, commands=command_results)
69
+
70
+
71
+ class DockerSandbox(VerificationSandbox):
72
+ def __init__(self, project_root: Path, config: DevCouncilConfig):
73
+ self.project_root = project_root
74
+ self.config = config
75
+
76
+ def run(self, task: Task, commands: list[str], requirements: list) -> SandboxResult:
77
+ if not shutil_which("docker"):
78
+ result = SandboxResult(
79
+ sandbox="docker",
80
+ status="unsupported",
81
+ environment={},
82
+ commands=[{"reason": "docker not available"}],
83
+ )
84
+ _save_run(self.project_root, task, "docker", result.environment, result.commands, result.status)
85
+ return result
86
+ image = self.config.verification.sandbox.docker_image
87
+ setup = self.config.verification.sandbox.docker_setup_commands
88
+ results: list[dict] = []
89
+ for setup_cmd in setup:
90
+ proc = subprocess.run(
91
+ ["docker", "run", "--rm", "-v", f"{self.project_root}:/work", "-w", "/work", image, "sh", "-c", setup_cmd],
92
+ capture_output=True,
93
+ text=True,
94
+ )
95
+ results.append({"command": setup_cmd, "exit_code": proc.returncode})
96
+ if proc.returncode != 0:
97
+ result = SandboxResult(sandbox="docker", status="failed", environment={"image": image}, commands=results)
98
+ _save_run(self.project_root, task, "docker", result.environment, result.commands, result.status)
99
+ return result
100
+ for cmd in commands:
101
+ proc = subprocess.run(
102
+ ["docker", "run", "--rm", "-v", f"{self.project_root}:/work", "-w", "/work", image, "sh", "-c", cmd],
103
+ capture_output=True,
104
+ text=True,
105
+ )
106
+ results.append({"command": cmd, "exit_code": proc.returncode})
107
+ if proc.returncode != 0:
108
+ result = SandboxResult(sandbox="docker", status="failed", environment={"image": image}, commands=results)
109
+ _save_run(self.project_root, task, "docker", result.environment, result.commands, result.status)
110
+ return result
111
+ result = SandboxResult(sandbox="docker", status="passed", environment={"image": image}, commands=results)
112
+ _save_run(self.project_root, task, "docker", result.environment, result.commands, result.status)
113
+ return result
114
+
115
+
116
+ class NixSandbox(VerificationSandbox):
117
+ def __init__(self, project_root: Path, config: DevCouncilConfig):
118
+ self.project_root = project_root
119
+ self.config = config
120
+
121
+ def run(self, task: Task, commands: list[str], requirements: list) -> SandboxResult:
122
+ if not (self.project_root / "flake.nix").exists() or not shutil_which("nix"):
123
+ result = SandboxResult(
124
+ sandbox="nix",
125
+ status="unsupported",
126
+ environment={},
127
+ commands=[{"reason": "nix or flake.nix unavailable"}],
128
+ )
129
+ _save_run(self.project_root, task, "nix", result.environment, result.commands, result.status)
130
+ return result
131
+ attr = self.config.verification.sandbox.nix_flake_attr or "devShells.default"
132
+ results: list[dict] = []
133
+ for cmd in commands:
134
+ proc = subprocess.run(
135
+ ["nix", "develop", f".#{attr}", "-c", "sh", "-c", cmd],
136
+ cwd=self.project_root,
137
+ capture_output=True,
138
+ text=True,
139
+ )
140
+ results.append({"command": cmd, "exit_code": proc.returncode})
141
+ if proc.returncode != 0:
142
+ result = SandboxResult(sandbox="nix", status="failed", environment={"attr": attr}, commands=results)
143
+ _save_run(self.project_root, task, "nix", result.environment, result.commands, result.status)
144
+ return result
145
+ result = SandboxResult(sandbox="nix", status="passed", environment={"attr": attr}, commands=results)
146
+ _save_run(self.project_root, task, "nix", result.environment, result.commands, result.status)
147
+ return result
148
+
149
+
150
+ def shutil_which(name: str) -> str | None:
151
+ import shutil
152
+
153
+ return shutil.which(name)
154
+
155
+
156
+ def _environment_metadata(project_root: Path) -> dict[str, str]:
157
+ env = {
158
+ "python": sys.version.split()[0],
159
+ "platform": platform.platform(),
160
+ }
161
+ try:
162
+ uv = subprocess.check_output(["uv", "--version"], text=True).strip()
163
+ env["uv"] = uv
164
+ except (FileNotFoundError, subprocess.CalledProcessError, OSError):
165
+ pass
166
+ lock = project_root / "uv.lock"
167
+ if lock.exists():
168
+ env["uv_lock_hash"] = hashlib.sha256(lock.read_bytes()).hexdigest()[:16]
169
+ return env
170
+
171
+
172
+ def get_sandbox(name: str, project_root: Path) -> VerificationSandbox:
173
+ config = load_config(project_root)
174
+ if name == "docker":
175
+ return DockerSandbox(project_root, config)
176
+ if name == "nix":
177
+ return NixSandbox(project_root, config)
178
+ return LocalSandbox(project_root)
@@ -0,0 +1,91 @@
1
+ """Evidence suggestion from changed files."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Literal
7
+
8
+ from pydantic import BaseModel
9
+
10
+ from devcouncil.domain.task import Task
11
+
12
+
13
+ class EvidenceSuggestion(BaseModel):
14
+ command: str
15
+ confidence: Literal["high", "medium", "low"]
16
+ reason: str
17
+ paths: list[str] = []
18
+
19
+
20
+ class TestResolver:
21
+ __test__ = False
22
+
23
+ def __init__(self, project_root: Path):
24
+ self.project_root = project_root.resolve()
25
+
26
+ def suggest_for_task(
27
+ self,
28
+ task: Task,
29
+ changed_files: list[str] | None = None,
30
+ ) -> list[EvidenceSuggestion]:
31
+ files = changed_files or [pf.path for pf in task.planned_files]
32
+ suggestions: list[EvidenceSuggestion] = []
33
+ seen: set[str] = set()
34
+
35
+ for path in files:
36
+ normalized = path.replace("\\", "/")
37
+ for candidate in self._candidates_for_path(normalized):
38
+ if candidate in seen:
39
+ continue
40
+ seen.add(candidate)
41
+ confidence, reason = self._confidence_for(normalized, candidate)
42
+ suggestions.append(
43
+ EvidenceSuggestion(
44
+ command=candidate,
45
+ confidence=confidence,
46
+ reason=reason,
47
+ paths=[normalized],
48
+ )
49
+ )
50
+ return suggestions
51
+
52
+ def _candidates_for_path(self, path: str) -> list[str]:
53
+ candidates: list[str] = []
54
+ if path.startswith("src/") and path.endswith(".py"):
55
+ module = path.removeprefix("src/").removesuffix(".py")
56
+ parts = module.split("/")
57
+ if len(parts) >= 2:
58
+ nested = self.project_root / "tests" / parts[0] / f"test_{parts[-1]}.py"
59
+ if nested.exists():
60
+ candidates.append(f"pytest {nested.relative_to(self.project_root).as_posix()}")
61
+ flat = self.project_root / "tests" / f"test_{parts[-1]}.py"
62
+ if flat.exists():
63
+ candidates.append(f"pytest {flat.relative_to(self.project_root).as_posix()}")
64
+ if "cli/commands/" in path:
65
+ unit = self.project_root / "tests/unit/test_cli_commands.py"
66
+ if unit.exists():
67
+ candidates.append(f"pytest {unit.relative_to(self.project_root).as_posix()}")
68
+ if path.endswith("policy_engine.py"):
69
+ unit = self.project_root / "tests/unit/test_task_policy_engine.py"
70
+ if unit.exists():
71
+ candidates.append(f"pytest {unit.relative_to(self.project_root).as_posix()}")
72
+ if path == "src/auth.py":
73
+ auth_test = self.project_root / "tests/test_auth.py"
74
+ if auth_test.exists():
75
+ candidates.append("pytest tests/test_auth.py")
76
+ if not candidates:
77
+ candidates.append("pytest tests/unit")
78
+ return candidates
79
+
80
+ def _confidence_for(self, path: str, command: str) -> tuple[Literal["high", "medium", "low"], str]:
81
+ if "test_auth.py" in command and path.endswith("auth.py"):
82
+ return "high", "Direct auth module mapping"
83
+ if path.endswith("policy_engine.py") and "test_task_policy_engine.py" in command:
84
+ return "high", "Policy engine unit test exists"
85
+ if "cli/commands" in path and "test_cli_commands.py" in command:
86
+ return "high", "CLI command module maps to cli command tests"
87
+ if command.startswith("pytest tests/") and command != "pytest tests/unit":
88
+ return "high", "Nested module test path exists"
89
+ if "test_" in command:
90
+ return "medium", "Related test file match"
91
+ return "low", "Package-level fallback"