devcouncil 0.1.1 → 0.3.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 (159) hide show
  1. package/README.md +201 -6
  2. package/package.json +9 -2
  3. package/pyproject.toml +34 -2
  4. package/src/devcouncil/app/config.py +348 -12
  5. package/src/devcouncil/app/orchestrator.py +10 -6
  6. package/src/devcouncil/app/state_machine.py +4 -0
  7. package/src/devcouncil/artifacts/graph.py +32 -5
  8. package/src/devcouncil/assets/__init__.py +1 -0
  9. package/src/devcouncil/assets/devcouncil-logo.svg +60 -0
  10. package/src/devcouncil/assets/devcouncil_logo_premium.png +0 -0
  11. package/src/devcouncil/cli/commands/agents.py +292 -0
  12. package/src/devcouncil/cli/commands/artifacts.py +6 -3
  13. package/src/devcouncil/cli/commands/check.py +220 -0
  14. package/src/devcouncil/cli/commands/config.py +43 -4
  15. package/src/devcouncil/cli/commands/cost.py +57 -0
  16. package/src/devcouncil/cli/commands/dashboard.py +6 -1
  17. package/src/devcouncil/cli/commands/design.py +186 -0
  18. package/src/devcouncil/cli/commands/doctor.py +379 -22
  19. package/src/devcouncil/cli/commands/evidence.py +48 -0
  20. package/src/devcouncil/cli/commands/go.py +532 -33
  21. package/src/devcouncil/cli/commands/handoff.py +69 -0
  22. package/src/devcouncil/cli/commands/hook.py +296 -15
  23. package/src/devcouncil/cli/commands/init.py +161 -20
  24. package/src/devcouncil/cli/commands/integrate.py +1371 -124
  25. package/src/devcouncil/cli/commands/logs.py +106 -0
  26. package/src/devcouncil/cli/commands/map.py +80 -10
  27. package/src/devcouncil/cli/commands/okf.py +245 -0
  28. package/src/devcouncil/cli/commands/plan.py +256 -55
  29. package/src/devcouncil/cli/commands/prompt.py +18 -7
  30. package/src/devcouncil/cli/commands/repair.py +50 -24
  31. package/src/devcouncil/cli/commands/report.py +8 -0
  32. package/src/devcouncil/cli/commands/reset_demo_state.py +4 -2
  33. package/src/devcouncil/cli/commands/rollback.py +27 -28
  34. package/src/devcouncil/cli/commands/run.py +195 -54
  35. package/src/devcouncil/cli/commands/runs.py +223 -0
  36. package/src/devcouncil/cli/commands/scaffold.py +32 -0
  37. package/src/devcouncil/cli/commands/semantic.py +47 -0
  38. package/src/devcouncil/cli/commands/setup.py +145 -6
  39. package/src/devcouncil/cli/commands/shell.py +73 -0
  40. package/src/devcouncil/cli/commands/skills.py +267 -0
  41. package/src/devcouncil/cli/commands/status.py +30 -15
  42. package/src/devcouncil/cli/commands/trace.py +47 -3
  43. package/src/devcouncil/cli/commands/verify.py +144 -3
  44. package/src/devcouncil/cli/commands/watch.py +32 -12
  45. package/src/devcouncil/cli/commands/watch_fs.py +40 -0
  46. package/src/devcouncil/cli/main.py +91 -7
  47. package/src/devcouncil/domain/evidence.py +29 -2
  48. package/src/devcouncil/domain/gap.py +27 -1
  49. package/src/devcouncil/domain/task.py +31 -2
  50. package/src/devcouncil/execution/checkpoints.py +256 -0
  51. package/src/devcouncil/execution/context_builder.py +1 -1
  52. package/src/devcouncil/execution/fs_watcher.py +205 -0
  53. package/src/devcouncil/execution/handoff.py +102 -0
  54. package/src/devcouncil/execution/hook_policy.py +162 -74
  55. package/src/devcouncil/execution/patch.py +65 -10
  56. package/src/devcouncil/execution/permissions.py +24 -24
  57. package/src/devcouncil/execution/policy_engine.py +350 -0
  58. package/src/devcouncil/execution/prompt_builder.py +751 -23
  59. package/src/devcouncil/execution/shell_session.py +231 -0
  60. package/src/devcouncil/execution/task_runner.py +24 -9
  61. package/src/devcouncil/executors/agent_registry.py +596 -0
  62. package/src/devcouncil/executors/coding_cli.py +791 -39
  63. package/src/devcouncil/executors/mini_swe.py +6 -0
  64. package/src/devcouncil/executors/native/agent.py +135 -19
  65. package/src/devcouncil/executors/openhands.py +6 -0
  66. package/src/devcouncil/gating/checks/clean_git.py +3 -1
  67. package/src/devcouncil/gating/checks/secret_scan_check.py +47 -21
  68. package/src/devcouncil/gating/policy.py +190 -11
  69. package/src/devcouncil/hardware.py +184 -0
  70. package/src/devcouncil/indexing/ast_matcher.py +17 -7
  71. package/src/devcouncil/indexing/lsp.py +45 -4
  72. package/src/devcouncil/indexing/repo_mapper.py +1284 -15
  73. package/src/devcouncil/indexing/semantic_index.py +221 -0
  74. package/src/devcouncil/integrations/actions.py +166 -0
  75. package/src/devcouncil/integrations/check.py +426 -0
  76. package/src/devcouncil/integrations/claude_assets.py +444 -0
  77. package/src/devcouncil/integrations/code_review_graph.py +13 -2
  78. package/src/devcouncil/integrations/github_intent.py +149 -0
  79. package/src/devcouncil/integrations/gitnexus.py +45 -2
  80. package/src/devcouncil/integrations/mcp/server.py +1944 -32
  81. package/src/devcouncil/integrations/opencode_devcouncil_plugin.mjs +24 -0
  82. package/src/devcouncil/integrations/pr_comments.py +9 -0
  83. package/src/devcouncil/knowledge/__init__.py +23 -0
  84. package/src/devcouncil/knowledge/design.py +374 -0
  85. package/src/devcouncil/knowledge/design_conformance.py +317 -0
  86. package/src/devcouncil/knowledge/fetch.py +223 -0
  87. package/src/devcouncil/knowledge/frontmatter.py +51 -0
  88. package/src/devcouncil/knowledge/okf.py +202 -0
  89. package/src/devcouncil/knowledge/skill_bridge.py +96 -0
  90. package/src/devcouncil/knowledge/sources.py +239 -0
  91. package/src/devcouncil/live/cards.py +181 -25
  92. package/src/devcouncil/live/repair_prompt.py +29 -6
  93. package/src/devcouncil/live/reviewer.py +72 -13
  94. package/src/devcouncil/live/signals.py +2 -2
  95. package/src/devcouncil/live/summary.py +18 -8
  96. package/src/devcouncil/live/transcripts.py +47 -11
  97. package/src/devcouncil/llm/cache.py +20 -8
  98. package/src/devcouncil/llm/model_defaults.yaml +44 -0
  99. package/src/devcouncil/llm/provider.py +617 -49
  100. package/src/devcouncil/llm/router.py +337 -53
  101. package/src/devcouncil/optimization/__init__.py +1 -0
  102. package/src/devcouncil/optimization/gepa_agent.py +318 -0
  103. package/src/devcouncil/optimization/skillopt.py +673 -0
  104. package/src/devcouncil/planning/arbiter_service.py +10 -2
  105. package/src/devcouncil/planning/correction_manifest.py +346 -0
  106. package/src/devcouncil/planning/critique_service.py +16 -4
  107. package/src/devcouncil/planning/plan_service.py +86 -6
  108. package/src/devcouncil/planning/prompt_enhancer_service.py +206 -1
  109. package/src/devcouncil/planning/repair_service.py +8 -2
  110. package/src/devcouncil/planning/spec_service.py +37 -3
  111. package/src/devcouncil/repo/ci_scaffold.py +165 -0
  112. package/src/devcouncil/repo/gitignore.py +123 -0
  113. package/src/devcouncil/repo/sca.py +384 -0
  114. package/src/devcouncil/reporting/json_report.py +22 -1
  115. package/src/devcouncil/reporting/markdown_report.py +29 -1
  116. package/src/devcouncil/reporting/okf_bundle_writer.py +364 -0
  117. package/src/devcouncil/reporting/okf_html.py +323 -0
  118. package/src/devcouncil/reporting/report_builder.py +18 -1
  119. package/src/devcouncil/skills/__init__.py +19 -0
  120. package/src/devcouncil/skills/library/README.md +46 -0
  121. package/src/devcouncil/skills/library/ai-training.md +50 -0
  122. package/src/devcouncil/skills/library/android.md +50 -0
  123. package/src/devcouncil/skills/library/backend.md +52 -0
  124. package/src/devcouncil/skills/library/core-engineering.md +95 -0
  125. package/src/devcouncil/skills/library/data-engineering.md +47 -0
  126. package/src/devcouncil/skills/library/desktop.md +46 -0
  127. package/src/devcouncil/skills/library/devops.md +48 -0
  128. package/src/devcouncil/skills/library/game-dev.md +46 -0
  129. package/src/devcouncil/skills/library/ios.md +48 -0
  130. package/src/devcouncil/skills/library/mobile-cross-platform.md +46 -0
  131. package/src/devcouncil/skills/library/security.md +48 -0
  132. package/src/devcouncil/skills/library/systems.md +48 -0
  133. package/src/devcouncil/skills/library/web.md +47 -0
  134. package/src/devcouncil/skills/library/windows.md +47 -0
  135. package/src/devcouncil/skills/registry.py +408 -0
  136. package/src/devcouncil/storage/db.py +140 -3
  137. package/src/devcouncil/storage/models.py +125 -0
  138. package/src/devcouncil/storage/native.py +559 -0
  139. package/src/devcouncil/storage/repositories.py +157 -78
  140. package/src/devcouncil/telemetry/cost.py +123 -17
  141. package/src/devcouncil/telemetry/logging_setup.py +244 -0
  142. package/src/devcouncil/telemetry/model_pricing.yaml +48 -0
  143. package/src/devcouncil/telemetry/pricing.py +28 -0
  144. package/src/devcouncil/telemetry/stages.py +141 -0
  145. package/src/devcouncil/telemetry/traces.py +62 -7
  146. package/src/devcouncil/telemetry/tracker.py +24 -10
  147. package/src/devcouncil/ui/dashboard.py +393 -28
  148. package/src/devcouncil/utils/redaction.py +9 -3
  149. package/src/devcouncil/utils/subprocess_env.py +69 -0
  150. package/src/devcouncil/verification/acceptance_compiler.py +253 -0
  151. package/src/devcouncil/verification/ad_hoc_check.py +135 -0
  152. package/src/devcouncil/verification/diff_coverage.py +353 -0
  153. package/src/devcouncil/verification/implementation_reviewer.py +11 -2
  154. package/src/devcouncil/verification/next_actions.py +189 -0
  155. package/src/devcouncil/verification/sandbox.py +181 -0
  156. package/src/devcouncil/verification/test_resolver.py +91 -0
  157. package/src/devcouncil/verification/verifier.py +1549 -143
  158. package/uv.lock +205 -64
  159. package/src/devcouncil/indexing/symbol_index.py +0 -0
@@ -0,0 +1,181 @@
1
+ """Verification sandbox abstraction."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import hashlib
7
+ import logging
8
+ import platform
9
+ import shutil
10
+ import subprocess
11
+ import sys
12
+ from pathlib import Path
13
+ from typing import Literal
14
+
15
+ from pydantic import BaseModel
16
+
17
+ from devcouncil.app.config import DevCouncilConfig, load_config
18
+ from devcouncil.domain.task import Task
19
+ from devcouncil.storage.db import get_db
20
+ from devcouncil.storage.native import VerificationRunRepository
21
+ from devcouncil.verification.verifier import Verifier
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ class SandboxResult(BaseModel):
27
+ sandbox: str
28
+ status: Literal["passed", "failed", "unsupported"]
29
+ environment: dict[str, str]
30
+ commands: list[dict]
31
+
32
+
33
+ class VerificationSandbox:
34
+ def run(self, task: Task, commands: list[str], requirements: list) -> SandboxResult:
35
+ raise NotImplementedError
36
+
37
+
38
+ def _save_run(
39
+ project_root: Path,
40
+ task: Task,
41
+ sandbox: str,
42
+ environment: dict[str, str],
43
+ commands: list[dict],
44
+ status: Literal["passed", "failed", "unsupported"],
45
+ ) -> None:
46
+ log = logger.info if status == "passed" else logger.warning
47
+ log("Sandbox %s for %s: %s (%d command(s))", sandbox, task.id, status, len(commands))
48
+ db = get_db(project_root)
49
+ if not db:
50
+ return
51
+ with db.get_session() as session:
52
+ VerificationRunRepository(session).save(
53
+ task.id,
54
+ sandbox,
55
+ environment,
56
+ commands,
57
+ status,
58
+ )
59
+
60
+
61
+ class LocalSandbox(VerificationSandbox):
62
+ def __init__(self, project_root: Path):
63
+ self.project_root = project_root
64
+
65
+ def run(self, task: Task, commands: list[str], requirements: list) -> SandboxResult:
66
+ gaps, _ = asyncio.run(Verifier(self.project_root).verify_task(task, requirements))
67
+ status: Literal["passed", "failed", "unsupported"] = (
68
+ "failed" if any(g.blocking for g in gaps) else "passed"
69
+ )
70
+ env = _environment_metadata(self.project_root)
71
+ command_results = [{"command": cmd, "status": status} for cmd in commands]
72
+ _save_run(self.project_root, task, "local", env, command_results, status)
73
+ return SandboxResult(sandbox="local", status=status, environment=env, commands=command_results)
74
+
75
+
76
+ class DockerSandbox(VerificationSandbox):
77
+ def __init__(self, project_root: Path, config: DevCouncilConfig):
78
+ self.project_root = project_root
79
+ self.config = config
80
+
81
+ def run(self, task: Task, commands: list[str], requirements: list) -> SandboxResult:
82
+ if not shutil_which("docker"):
83
+ result = SandboxResult(
84
+ sandbox="docker",
85
+ status="unsupported",
86
+ environment={},
87
+ commands=[{"reason": "docker not available"}],
88
+ )
89
+ _save_run(self.project_root, task, "docker", result.environment, result.commands, result.status)
90
+ return result
91
+ image = self.config.verification.sandbox.docker_image
92
+ setup = self.config.verification.sandbox.docker_setup_commands
93
+ results: list[dict] = []
94
+ for setup_cmd in setup:
95
+ proc = subprocess.run(
96
+ ["docker", "run", "--rm", "-v", f"{self.project_root}:/work", "-w", "/work", image, "sh", "-c", setup_cmd],
97
+ capture_output=True,
98
+ text=True,
99
+ )
100
+ results.append({"command": setup_cmd, "exit_code": proc.returncode})
101
+ if proc.returncode != 0:
102
+ result = SandboxResult(sandbox="docker", status="failed", environment={"image": image}, commands=results)
103
+ _save_run(self.project_root, task, "docker", result.environment, result.commands, result.status)
104
+ return result
105
+ for cmd in commands:
106
+ proc = subprocess.run(
107
+ ["docker", "run", "--rm", "-v", f"{self.project_root}:/work", "-w", "/work", image, "sh", "-c", cmd],
108
+ capture_output=True,
109
+ text=True,
110
+ )
111
+ results.append({"command": cmd, "exit_code": proc.returncode})
112
+ if proc.returncode != 0:
113
+ result = SandboxResult(sandbox="docker", status="failed", environment={"image": image}, commands=results)
114
+ _save_run(self.project_root, task, "docker", result.environment, result.commands, result.status)
115
+ return result
116
+ result = SandboxResult(sandbox="docker", status="passed", environment={"image": image}, commands=results)
117
+ _save_run(self.project_root, task, "docker", result.environment, result.commands, result.status)
118
+ return result
119
+
120
+
121
+ class NixSandbox(VerificationSandbox):
122
+ def __init__(self, project_root: Path, config: DevCouncilConfig):
123
+ self.project_root = project_root
124
+ self.config = config
125
+
126
+ def run(self, task: Task, commands: list[str], requirements: list) -> SandboxResult:
127
+ if not (self.project_root / "flake.nix").exists() or not shutil_which("nix"):
128
+ result = SandboxResult(
129
+ sandbox="nix",
130
+ status="unsupported",
131
+ environment={},
132
+ commands=[{"reason": "nix or flake.nix unavailable"}],
133
+ )
134
+ _save_run(self.project_root, task, "nix", result.environment, result.commands, result.status)
135
+ return result
136
+ attr = self.config.verification.sandbox.nix_flake_attr or "devShells.default"
137
+ results: list[dict] = []
138
+ for cmd in commands:
139
+ proc = subprocess.run(
140
+ ["nix", "develop", f".#{attr}", "-c", "sh", "-c", cmd],
141
+ cwd=self.project_root,
142
+ capture_output=True,
143
+ text=True,
144
+ )
145
+ results.append({"command": cmd, "exit_code": proc.returncode})
146
+ if proc.returncode != 0:
147
+ result = SandboxResult(sandbox="nix", status="failed", environment={"attr": attr}, commands=results)
148
+ _save_run(self.project_root, task, "nix", result.environment, result.commands, result.status)
149
+ return result
150
+ result = SandboxResult(sandbox="nix", status="passed", environment={"attr": attr}, commands=results)
151
+ _save_run(self.project_root, task, "nix", result.environment, result.commands, result.status)
152
+ return result
153
+
154
+
155
+ def shutil_which(name: str) -> str | None:
156
+ return shutil.which(name)
157
+
158
+
159
+ def _environment_metadata(project_root: Path) -> dict[str, str]:
160
+ env = {
161
+ "python": sys.version.split()[0],
162
+ "platform": platform.platform(),
163
+ }
164
+ try:
165
+ uv = subprocess.check_output(["uv", "--version"], text=True).strip()
166
+ env["uv"] = uv
167
+ except (FileNotFoundError, subprocess.CalledProcessError, OSError):
168
+ pass
169
+ lock = project_root / "uv.lock"
170
+ if lock.exists():
171
+ env["uv_lock_hash"] = hashlib.sha256(lock.read_bytes()).hexdigest()[:16]
172
+ return env
173
+
174
+
175
+ def get_sandbox(name: str, project_root: Path) -> VerificationSandbox:
176
+ config = load_config(project_root)
177
+ if name == "docker":
178
+ return DockerSandbox(project_root, config)
179
+ if name == "nix":
180
+ return NixSandbox(project_root, config)
181
+ 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"