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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devcouncil",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Gated orchestrator for AI-assisted software development",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://github.com/bharathvbcr/DevCouncil#readme",
@@ -27,6 +27,10 @@
27
27
  "bin/",
28
28
  "src/**/*.py",
29
29
  "src/**/*.md",
30
+ "src/**/*.png",
31
+ "src/**/*.svg",
32
+ "src/**/*.yaml",
33
+ "src/**/*.mjs",
30
34
  "pyproject.toml",
31
35
  "uv.lock",
32
36
  "README.md",
@@ -37,9 +41,12 @@
37
41
  "install:editable": "uv pip install -e .",
38
42
  "dev": "uv run devcouncil",
39
43
  "pack:check": "npm pack --dry-run",
44
+ "smoke:wheel": "uv run python scripts/check-wheel-assets.py",
45
+ "smoke:package": "node scripts/npm-runtime-smoke.mjs",
40
46
  "test": "uv run pytest",
41
47
  "lint": "uv run ruff check .",
42
- "check": "uv run ruff check . && uv run pytest"
48
+ "typecheck": "uv run mypy src",
49
+ "check": "uv run ruff check . && uv run mypy src && uv run pytest"
43
50
  },
44
51
  "engines": {
45
52
  "node": ">=18"
package/pyproject.toml CHANGED
@@ -1,31 +1,66 @@
1
- [project]
2
- name = "devcouncil"
3
- version = "0.1.0"
4
- description = "Gated orchestrator for AI-assisted software development"
5
- readme = "README.md"
6
- requires-python = ">=3.12"
7
- dependencies = [
8
- "typer>=0.12.3",
9
- "rich>=13.7.1",
10
- "pydantic>=2.7.1",
11
- "pyyaml>=6.0.1",
1
+ [project]
2
+ name = "devcouncil"
3
+ version = "0.2.0"
4
+ description = "Gated orchestrator for AI-assisted software development"
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ dependencies = [
8
+ "typer>=0.12.3",
9
+ "rich>=13.7.1",
10
+ "pydantic>=2.7.1",
11
+ "pyyaml>=6.0.1",
12
12
  "sqlmodel>=0.0.19",
13
13
  "httpx>=0.27.0",
14
- "gitpython>=3.1.43",
14
+ "gitpython>=3.1.49",
15
15
  "mcp>=1.27.0",
16
+ "gepa>=0.1.1",
17
+ "watchdog>=4.0.0",
16
18
  ]
17
-
18
- [project.scripts]
19
- dev = "devcouncil.cli.main:app"
20
- devcouncil = "devcouncil.cli.main:app"
21
-
22
- [dependency-groups]
23
- dev = [
24
- "pytest>=8.2.0",
25
- "ruff>=0.4.4",
26
- "mypy>=1.10.0",
27
- ]
28
-
29
- [build-system]
30
- requires = ["hatchling"]
31
- build-backend = "hatchling.build"
19
+
20
+ [project.urls]
21
+ Homepage = "https://github.com/bharathvbcr/DevCouncil"
22
+ Repository = "https://github.com/bharathvbcr/DevCouncil.git"
23
+ Issues = "https://github.com/bharathvbcr/DevCouncil/issues"
24
+
25
+ [project.scripts]
26
+ dev = "devcouncil.cli.main:app"
27
+ devcouncil = "devcouncil.cli.main:app"
28
+
29
+ [dependency-groups]
30
+ dev = [
31
+ "pytest>=8.2.0",
32
+ "ruff>=0.4.4",
33
+ "mypy>=1.10.0",
34
+ "types-pyyaml>=6.0.12.20260408",
35
+ # Used only to exercise the diff↔coverage path end-to-end in tests. DevCouncil
36
+ # never requires coverage at runtime — it uses whatever the target repo provides.
37
+ "coverage>=7.4",
38
+ ]
39
+
40
+ [tool.uv]
41
+ # Security floors for transitive dependencies (Dependabot advisories, 2026-06).
42
+ # These are not direct deps; constraint-dependencies pins their minimum patched
43
+ # version in the resolver without adding them to the dependency tree, so they can
44
+ # never resolve back below the fix.
45
+ constraint-dependencies = [
46
+ "cryptography>=48.0.1",
47
+ "starlette>=1.3.1",
48
+ "pyjwt>=2.13.0",
49
+ "python-multipart>=0.0.31",
50
+ "pydantic-settings>=2.14.2",
51
+ "idna>=3.15",
52
+ ]
53
+
54
+ [tool.pytest.ini_options]
55
+ testpaths = ["tests"]
56
+
57
+ [build-system]
58
+ requires = ["hatchling"]
59
+ build-backend = "hatchling.build"
60
+
61
+ [tool.hatch.build.targets.wheel.force-include]
62
+ "src/devcouncil/llm/model_defaults.yaml" = "devcouncil/llm/model_defaults.yaml"
63
+ "src/devcouncil/telemetry/model_pricing.yaml" = "devcouncil/telemetry/model_pricing.yaml"
64
+ "src/devcouncil/assets/devcouncil-logo.svg" = "devcouncil/assets/devcouncil-logo.svg"
65
+ "src/devcouncil/assets/devcouncil_logo_premium.png" = "devcouncil/assets/devcouncil_logo_premium.png"
66
+ "src/devcouncil/integrations/opencode_devcouncil_plugin.mjs" = "devcouncil/integrations/opencode_devcouncil_plugin.mjs"
@@ -1,4 +1,4 @@
1
- from devcouncil.cli.main import app
2
-
3
- if __name__ == "__main__":
4
- app()
1
+ from devcouncil.cli.main import app
2
+
3
+ if __name__ == "__main__":
4
+ app()
@@ -1,28 +1,28 @@
1
- from devcouncil.app.state_machine import StateMachine, ProjectPhase
2
- from devcouncil.app.run_context import RunContext
3
- from devcouncil.app.orchestrator import Orchestrator
4
- from devcouncil.app.events import EventBus, bus, EventTypes
5
- from devcouncil.app.errors import (
6
- DevCouncilError,
7
- GatingError,
8
- ConfigurationError,
9
- OrchestrationError,
10
- ExecutionError,
11
- VerificationError,
12
- )
13
-
14
- __all__ = [
15
- "StateMachine",
16
- "ProjectPhase",
17
- "RunContext",
18
- "Orchestrator",
19
- "EventBus",
20
- "bus",
21
- "EventTypes",
22
- "DevCouncilError",
23
- "GatingError",
24
- "ConfigurationError",
25
- "OrchestrationError",
26
- "ExecutionError",
27
- "VerificationError",
28
- ]
1
+ from devcouncil.app.state_machine import StateMachine, ProjectPhase
2
+ from devcouncil.app.run_context import RunContext
3
+ from devcouncil.app.orchestrator import Orchestrator
4
+ from devcouncil.app.events import EventBus, bus, EventTypes
5
+ from devcouncil.app.errors import (
6
+ DevCouncilError,
7
+ GatingError,
8
+ ConfigurationError,
9
+ OrchestrationError,
10
+ ExecutionError,
11
+ VerificationError,
12
+ )
13
+
14
+ __all__ = [
15
+ "StateMachine",
16
+ "ProjectPhase",
17
+ "RunContext",
18
+ "Orchestrator",
19
+ "EventBus",
20
+ "bus",
21
+ "EventTypes",
22
+ "DevCouncilError",
23
+ "GatingError",
24
+ "ConfigurationError",
25
+ "OrchestrationError",
26
+ "ExecutionError",
27
+ "VerificationError",
28
+ ]
@@ -1,58 +1,98 @@
1
- """Typed configuration loader with Pydantic validation.
2
-
3
- Replaces scattered yaml.safe_load() calls with a single validated config service.
4
- """
5
-
6
- from __future__ import annotations
7
-
8
- import os
9
- from pathlib import Path
10
- from typing import Dict, List
11
-
1
+ """Typed configuration loader with Pydantic validation.
2
+
3
+ Replaces scattered yaml.safe_load() calls with a single validated config service.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import os
9
+ import shutil
10
+ import subprocess
11
+ from pathlib import Path
12
+ from typing import Dict, List
13
+
12
14
  from pydantic import BaseModel, Field
13
-
14
-
15
- class ModelRoleConfig(BaseModel):
16
- model: str
17
- temperature: float = 0.0
18
-
19
-
20
- class ModelsConfig(BaseModel):
21
- provider: str = "openrouter"
22
- roles: Dict[str, ModelRoleConfig] = Field(default_factory=dict)
23
-
24
-
25
- class ProjectConfig(BaseModel):
26
- name: str = "devcouncil-project"
27
- root: str = "."
28
- default_branch: str = "main"
29
-
30
-
31
- class CommandsConfig(BaseModel):
32
- test: List[str] = Field(default_factory=list)
33
- lint: List[str] = Field(default_factory=list)
34
- typecheck: List[str] = Field(default_factory=list)
35
-
36
-
37
- class GatesConfig(BaseModel):
38
- require_clean_git_before_task: bool = True
39
- block_orphan_diffs: bool = True
40
- block_missing_tests_for_high_requirements: bool = True
41
- block_dependency_changes_without_approval: bool = True
42
- block_schema_change_without_migration: bool = True
43
- block_failed_commands: bool = True
44
-
45
-
46
- class ExecutionConfig(BaseModel):
47
- default_executor: str = "manual"
48
- max_repair_attempts: int = 3
49
- checkpoint_before_each_task: bool = True
50
- command_timeout: int = 300
51
-
52
-
15
+
16
+
17
+ class ModelRoleConfig(BaseModel):
18
+ model: str
19
+ temperature: float = 0.0
20
+
21
+
22
+ class ModelsConfig(BaseModel):
23
+ provider: str = "openrouter"
24
+ roles: Dict[str, ModelRoleConfig] = Field(default_factory=dict)
25
+
26
+
27
+ class ProjectConfig(BaseModel):
28
+ name: str = "devcouncil-project"
29
+ root: str = "."
30
+ default_branch: str = "main"
31
+
32
+
33
+ class CommandsConfig(BaseModel):
34
+ test: List[str] = Field(default_factory=list)
35
+ lint: List[str] = Field(default_factory=list)
36
+ typecheck: List[str] = Field(default_factory=list)
37
+
38
+
39
+ class VerificationSandboxConfig(BaseModel):
40
+ docker_image: str = "python:3.12-slim"
41
+ docker_setup_commands: List[str] = Field(default_factory=list)
42
+ nix_flake_attr: str | None = None
43
+
44
+
45
+ class DiffCoverageConfig(BaseModel):
46
+ """Diff↔coverage gating: prove the *changed* lines were exercised by tests.
47
+
48
+ ``measure`` runs the diff-coverage analysis and records it as evidence (and a
49
+ non-blocking signal) whenever the target repo's coverage tooling is present.
50
+ ``enforce`` promotes an unexercised diff to a *blocking* gap. Enforcement is
51
+ off by default so the signal is visible before it ever gates — a passing test
52
+ that does not touch the new code is surfaced first, then teams opt in to
53
+ blocking. ``min_ratio`` of 0.0 means "require at least one changed executable
54
+ line to be exercised"; a higher value demands that fraction of changed lines.
55
+ """
56
+
57
+ measure: bool = True
58
+ enforce: bool = False
59
+ min_ratio: float = 0.0
60
+
61
+
62
+ class VerificationConfig(BaseModel):
63
+ sandbox: VerificationSandboxConfig = Field(default_factory=VerificationSandboxConfig)
64
+ diff_coverage: DiffCoverageConfig = Field(default_factory=DiffCoverageConfig)
65
+
66
+
67
+ class GatesConfig(BaseModel):
68
+ require_clean_git_before_task: bool = True
69
+ block_orphan_diffs: bool = True
70
+ block_missing_tests_for_high_requirements: bool = True
71
+ block_dependency_changes_without_approval: bool = True
72
+ block_schema_change_without_migration: bool = True
73
+ block_failed_commands: bool = True
74
+
75
+
76
+ class ExecutionConfig(BaseModel):
77
+ default_executor: str = "manual"
78
+ max_repair_attempts: int = 3
79
+ checkpoint_before_each_task: bool = True
80
+ command_timeout: int = 300
81
+ stream_cli_output: bool = False
82
+ # Default lifetime of an MCP task lease. A crashed/disconnected agent's lease
83
+ # auto-expires after this, so the task frees up without a human running force.
84
+ lease_ttl_seconds: int = 1800
85
+ # When true, the post-task coding-CLI hook runs deterministic verification of the
86
+ # active task (and records gaps) instead of only printing a reminder. Off by default
87
+ # so hooks stay fast/cheap unless a team opts in.
88
+ verify_on_post_task: bool = False
89
+ cursor_resume_mode: str = "off"
90
+ coding_cli_probe_order: List[str] = Field(default_factory=list)
91
+
92
+
53
93
  class PrivacyConfig(BaseModel):
54
- redact_env_vars: bool = True
55
- redact_secrets_in_logs: bool = True
94
+ redact_env_vars: bool = True
95
+ redact_secrets_in_logs: bool = True
56
96
  store_prompts_locally: bool = True
57
97
 
58
98
 
@@ -68,64 +108,213 @@ class CodeReviewGraphIntegrationConfig(BaseModel):
68
108
  optional: bool = True
69
109
 
70
110
 
111
+ class LiveReviewIntegrationConfig(BaseModel):
112
+ enabled: bool = True
113
+ cards_path: str = ".devcouncil/live/cards"
114
+ signals_path: str = ".devcouncil/live/signals"
115
+ default_client: str = "claude"
116
+
117
+
118
+ class WarpIntegrationConfig(BaseModel):
119
+ enabled: bool = False
120
+ command: str = "oz"
121
+ run_mode: str = "local"
122
+ mcp_config_path: str = ".devcouncil/integrations/warp-mcp.json"
123
+ profile: str | None = None
124
+ model: str | None = None
125
+ environment: str | None = None
126
+ share: List[str] = Field(default_factory=list)
127
+
128
+
129
+ class OpenCodeIntegrationConfig(BaseModel):
130
+ enabled: bool = False
131
+ config_path: str = "opencode.json"
132
+
133
+
134
+ class AntigravityIntegrationConfig(BaseModel):
135
+ enabled: bool = False
136
+ mcp_config_path: str = ".agents/mcp_config.json"
137
+
138
+
139
+ class CursorIntegrationConfig(BaseModel):
140
+ enabled: bool = False
141
+ config_path: str = ".cursor/mcp.json"
142
+ hooks_path: str = ".cursor/hooks.json"
143
+
144
+
145
+ class AiderIntegrationConfig(BaseModel):
146
+ enabled: bool = False
147
+
148
+
149
+ class CliAgentProfileConfig(BaseModel):
150
+ description: str = ""
151
+ timeout_seconds: int | None = None
152
+ prompt_preamble: str = ""
153
+ require_explicit_confirmation: bool = False
154
+ # Per-profile CLI containment overrides. Empty/None reproduce today's behavior
155
+ # exactly so a profile that only sets a prompt preamble is a no-op on the
156
+ # subprocess invocation. ``extra_args`` are appended verbatim to the resolved
157
+ # command, ``permission_mode`` is translated into the right per-CLI flag where
158
+ # known (and overly-permissive flags are dropped for stricter modes), and
159
+ # ``model`` overrides the model flag for CLIs that accept one.
160
+ extra_args: List[str] = Field(default_factory=list)
161
+ permission_mode: str | None = None
162
+ model: str | None = None
163
+
164
+
165
+ class CustomCliAgentConfig(BaseModel):
166
+ command: str
167
+ args: List[str] = Field(default_factory=list)
168
+ input_mode: str = "stdin"
169
+ prompt_arg: str | None = None
170
+ timeout_seconds: int | None = None
171
+ env: Dict[str, str] = Field(default_factory=dict)
172
+ display_name: str | None = None
173
+ kind: str = "custom"
174
+ supports_mcp: bool = False
175
+ supports_diff_review: bool = False
176
+ default_profile: str = "default"
177
+ help_command: List[str] = Field(default_factory=list)
178
+
179
+
180
+ class CliAgentsIntegrationConfig(BaseModel):
181
+ enabled: bool = True
182
+ profiles: Dict[str, CliAgentProfileConfig] = Field(default_factory=dict)
183
+ agents: Dict[str, CustomCliAgentConfig] = Field(default_factory=dict)
184
+
185
+
186
+ class McpIntegrationConfig(BaseModel):
187
+ write_task_scope_to_config: bool = False
188
+
189
+
71
190
  class IntegrationsConfig(BaseModel):
191
+ mcp: McpIntegrationConfig = Field(default_factory=McpIntegrationConfig)
72
192
  agent_flow: AgentFlowIntegrationConfig = Field(default_factory=AgentFlowIntegrationConfig)
73
193
  code_review_graph: CodeReviewGraphIntegrationConfig = Field(default_factory=CodeReviewGraphIntegrationConfig)
74
-
75
-
76
- class ProviderConfig(BaseModel):
77
- sort: str = "price"
78
- allow_fallbacks: bool = True
79
- require_parameters: bool = True
80
- data_collection: str = "deny"
81
-
82
-
83
- class DevCouncilConfig(BaseModel):
84
- """Top-level validated configuration."""
85
-
86
- project: ProjectConfig = Field(default_factory=ProjectConfig)
87
- models: ModelsConfig = Field(default_factory=ModelsConfig)
88
- provider: ProviderConfig = Field(default_factory=ProviderConfig)
89
- commands: CommandsConfig = Field(default_factory=CommandsConfig)
90
- gates: GatesConfig = Field(default_factory=GatesConfig)
194
+ live_review: LiveReviewIntegrationConfig = Field(default_factory=LiveReviewIntegrationConfig)
195
+ cursor: CursorIntegrationConfig = Field(default_factory=CursorIntegrationConfig)
196
+ aider: AiderIntegrationConfig = Field(default_factory=AiderIntegrationConfig)
197
+ antigravity: AntigravityIntegrationConfig = Field(default_factory=AntigravityIntegrationConfig)
198
+ warp: WarpIntegrationConfig = Field(default_factory=WarpIntegrationConfig)
199
+ opencode: OpenCodeIntegrationConfig = Field(default_factory=OpenCodeIntegrationConfig)
200
+ cli_agents: CliAgentsIntegrationConfig = Field(default_factory=CliAgentsIntegrationConfig)
201
+
202
+
203
+ class ProviderConfig(BaseModel):
204
+ sort: str = "price"
205
+ allow_fallbacks: bool = True
206
+ require_parameters: bool = True
207
+ data_collection: str = "deny"
208
+
209
+
210
+ class DevCouncilConfig(BaseModel):
211
+ """Top-level validated configuration."""
212
+
213
+ project: ProjectConfig = Field(default_factory=ProjectConfig)
214
+ models: ModelsConfig = Field(default_factory=ModelsConfig)
215
+ provider: ProviderConfig = Field(default_factory=ProviderConfig)
216
+ commands: CommandsConfig = Field(default_factory=CommandsConfig)
217
+ gates: GatesConfig = Field(default_factory=GatesConfig)
91
218
  execution: ExecutionConfig = Field(default_factory=ExecutionConfig)
219
+ verification: VerificationConfig = Field(default_factory=VerificationConfig)
92
220
  privacy: PrivacyConfig = Field(default_factory=PrivacyConfig)
93
221
  integrations: IntegrationsConfig = Field(default_factory=IntegrationsConfig)
94
-
95
-
96
- def load_config(project_root: Path = Path(".")) -> DevCouncilConfig:
97
- """Load and validate .devcouncil/config.yaml.
98
-
99
- Returns DevCouncilConfig with defaults for any missing fields.
100
- Raises FileNotFoundError if config doesn't exist.
101
- """
102
- import yaml
103
-
104
- config_path = project_root / ".devcouncil" / "config.yaml"
105
- if not config_path.exists():
106
- raise FileNotFoundError(f"Config not found at {config_path}. Run 'dev init' first.")
107
-
108
- with open(config_path) as f:
109
- raw = yaml.safe_load(f) or {}
110
-
111
- return DevCouncilConfig.model_validate(raw)
112
-
113
-
114
- def get_api_key(provider: str = "openrouter") -> str:
115
- """Retrieve the API key for the configured provider from environment.
116
-
117
- Raises ValueError if not found.
118
- """
119
- env_map = {
120
- "openrouter": "OPENROUTER_API_KEY",
121
- "openai": "OPENAI_API_KEY",
122
- "anthropic": "ANTHROPIC_API_KEY",
123
- }
124
- env_var = env_map.get(provider, f"{provider.upper()}_API_KEY")
125
- key = os.environ.get(env_var)
126
- if not key:
127
- raise ValueError(
128
- f"API key not found. Set {env_var} in your environment. "
129
- f"Provider: {provider}"
130
- )
131
- return key
222
+
223
+
224
+ def load_config(project_root: Path = Path(".")) -> DevCouncilConfig:
225
+ """Load and validate .devcouncil/config.yaml.
226
+
227
+ Returns DevCouncilConfig with defaults for any missing fields.
228
+ Raises FileNotFoundError if config doesn't exist.
229
+ """
230
+ import yaml # type: ignore[import-untyped]
231
+
232
+ config_path = project_root / ".devcouncil" / "config.yaml"
233
+ if not config_path.exists():
234
+ raise FileNotFoundError(f"Config not found at {config_path}. Run 'dev init' first.")
235
+
236
+ with open(config_path, encoding="utf-8") as f:
237
+ try:
238
+ raw = yaml.safe_load(f) or {}
239
+ except yaml.YAMLError as exc:
240
+ raise ValueError(
241
+ f"Invalid YAML in {config_path}: {exc}. Fix the syntax or re-run 'dev init'."
242
+ ) from exc
243
+
244
+ return DevCouncilConfig.model_validate(raw)
245
+
246
+
247
+ def provider_api_key_env_var(provider: str = "openrouter") -> str:
248
+ normalized = provider.strip().lower().replace("-", "").replace("_", "")
249
+ env_map = {
250
+ "openrouter": "OPENROUTER_API_KEY",
251
+ "vertexai": "VERTEXAI_ACCESS_TOKEN",
252
+ "doubleword": "DOUBLEWORD_API_KEY",
253
+ "ollama": "OLLAMA_API_KEY",
254
+ "openai": "OPENAI_API_KEY",
255
+ "anthropic": "ANTHROPIC_API_KEY",
256
+ }
257
+ return env_map.get(normalized, f"{normalized.upper()}_API_KEY")
258
+
259
+
260
+ def _normalized_provider_name(provider: str) -> str:
261
+ return provider.strip().lower().replace("-", "").replace("_", "")
262
+
263
+
264
+ def load_local_secrets(project_root: Path = Path(".")) -> Dict[str, str]:
265
+ secrets_path = project_root / ".devcouncil" / "secrets.env"
266
+ if not secrets_path.exists():
267
+ return {}
268
+
269
+ secrets: Dict[str, str] = {}
270
+ for line in secrets_path.read_text(encoding="utf-8").splitlines():
271
+ stripped = line.strip()
272
+ if not stripped or stripped.startswith("#") or "=" not in stripped:
273
+ continue
274
+ key, value = stripped.split("=", 1)
275
+ secrets[key.strip()] = value.strip().strip('"').strip("'")
276
+ return secrets
277
+
278
+
279
+ def get_gcloud_access_token() -> str | None:
280
+ executable = shutil.which("gcloud")
281
+ if not executable:
282
+ return None
283
+ try:
284
+ token = subprocess.check_output(
285
+ [executable, "auth", "print-access-token"],
286
+ stderr=subprocess.STDOUT,
287
+ text=True,
288
+ encoding="utf-8",
289
+ errors="replace",
290
+ timeout=10,
291
+ ).strip()
292
+ except Exception:
293
+ return None
294
+ return token or None
295
+
296
+
297
+ def get_api_key(provider: str = "openrouter", project_root: Path = Path(".")) -> str:
298
+ """Retrieve the API key for the configured provider from environment.
299
+
300
+ Raises ValueError if not found.
301
+ """
302
+ env_var = provider_api_key_env_var(provider)
303
+ key = os.environ.get(env_var) or load_local_secrets(project_root).get(env_var)
304
+ if not key and _normalized_provider_name(provider) == "vertexai":
305
+ key = get_gcloud_access_token()
306
+ if not key and _normalized_provider_name(provider) == "ollama":
307
+ # Ollama is a local server and needs no API key; an explicitly-set
308
+ # OLLAMA_API_KEY still flows through above if present.
309
+ return ""
310
+ if not key:
311
+ extra = (
312
+ " You can also authenticate with 'gcloud auth login' for vertexai."
313
+ if _normalized_provider_name(provider) == "vertexai"
314
+ else ""
315
+ )
316
+ raise ValueError(
317
+ f"API key not found. Set {env_var} in your environment or run 'dev setup'. "
318
+ f"Provider: {provider}.{extra}"
319
+ )
320
+ return key
@@ -1,23 +1,23 @@
1
- class DevCouncilError(Exception):
2
- """Base exception for all DevCouncil errors."""
3
- pass
4
-
5
- class GatingError(DevCouncilError):
6
- """Raised when a task or plan fails a mandatory gate."""
7
- pass
8
-
9
- class ConfigurationError(DevCouncilError):
10
- """Raised when the project configuration is invalid or missing."""
11
- pass
12
-
13
- class OrchestrationError(DevCouncilError):
14
- """Raised when the orchestrator encounters an unexpected state."""
15
- pass
16
-
17
- class ExecutionError(DevCouncilError):
18
- """Raised when an external or internal executor fails to run a task."""
19
- pass
20
-
21
- class VerificationError(DevCouncilError):
22
- """Raised when the verifier fails to run its checks properly."""
23
- pass
1
+ class DevCouncilError(Exception):
2
+ """Base exception for all DevCouncil errors."""
3
+ pass
4
+
5
+ class GatingError(DevCouncilError):
6
+ """Raised when a task or plan fails a mandatory gate."""
7
+ pass
8
+
9
+ class ConfigurationError(DevCouncilError):
10
+ """Raised when the project configuration is invalid or missing."""
11
+ pass
12
+
13
+ class OrchestrationError(DevCouncilError):
14
+ """Raised when the orchestrator encounters an unexpected state."""
15
+ pass
16
+
17
+ class ExecutionError(DevCouncilError):
18
+ """Raised when an external or internal executor fails to run a task."""
19
+ pass
20
+
21
+ class VerificationError(DevCouncilError):
22
+ """Raised when the verifier fails to run its checks properly."""
23
+ pass