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
@@ -1,19 +1,19 @@
1
1
  import subprocess
2
2
  import sys
3
- from pathlib import Path
4
- from rich.console import Console
5
- from devcouncil.domain.task import Task
6
- from devcouncil.domain.requirement import Requirement
7
- from devcouncil.execution.executor import Executor, ExecutionResult
8
- from devcouncil.execution.prompt_builder import PromptBuilder
9
-
10
- console = Console()
11
-
12
- class MiniSWEExecutor(Executor):
13
- def __init__(self, project_root: Path):
14
- self.project_root = project_root
15
-
16
- def run_task(self, task: Task, requirements: list[Requirement]) -> ExecutionResult:
3
+ from pathlib import Path
4
+ from rich.console import Console
5
+ from devcouncil.domain.task import Task
6
+ from devcouncil.domain.requirement import Requirement
7
+ from devcouncil.execution.executor import Executor, ExecutionResult
8
+ from devcouncil.execution.prompt_builder import PromptBuilder
9
+
10
+ console = Console()
11
+
12
+ class MiniSWEExecutor(Executor):
13
+ def __init__(self, project_root: Path):
14
+ self.project_root = project_root
15
+
16
+ def run_task(self, task: Task, requirements: list[Requirement]) -> ExecutionResult:
17
17
  builder = PromptBuilder(self.project_root)
18
18
  task_prompt = builder.build_task_prompt(task, requirements)
19
19
 
@@ -21,53 +21,53 @@ class MiniSWEExecutor(Executor):
21
21
  instruction_file = self.project_root / ".devcouncil" / f"{task.id}-mini-swe-task.md"
22
22
  instruction_file.parent.mkdir(parents=True, exist_ok=True)
23
23
  instruction_file.write_text(task_prompt, encoding="utf-8")
24
-
25
- console.print(f"Starting [bold]mini-SWE-agent[/bold] for task {task.id}...")
26
-
27
- # In a real implementation, we'd invoke the agent CLI
28
- # For now, we simulate the command call
29
- cmd = [
24
+
25
+ console.print(f"Starting [bold]mini-SWE-agent[/bold] for task {task.id}...")
26
+
27
+ # In a real implementation, we'd invoke the agent CLI
28
+ # For now, we simulate the command call
29
+ cmd = [
30
30
  sys.executable, "-m", "mini_swe_agent.main",
31
- "--instruction-file", str(instruction_file),
32
- "--repo-path", str(self.project_root)
33
- ]
34
-
35
- console.print(f"Command: [dim]{' '.join(cmd)}[/dim]")
36
-
37
- # Since I might not have mini_swe_agent installed here,
38
- # I'll just explain what it would do.
39
- console.print("[yellow]Note: mini_swe_agent must be installed in the environment.[/yellow]")
40
-
41
- try:
42
- result = subprocess.run(
43
- cmd,
44
- capture_output=True,
45
- text=True,
46
- encoding="utf-8",
47
- errors="replace",
48
- cwd=self.project_root,
49
- timeout=1800,
50
- )
51
- self._write_log(task.id, result)
52
- if result.returncode != 0:
53
- console.print(f"[red]mini-SWE-agent exited with {result.returncode}.[/red]")
54
- return ExecutionResult(success=False, message='Execution failed')
55
- return ExecutionResult(success=True, message='Execution successful')
56
- except Exception as e:
57
- console.print(f"[red]Error running mini-SWE-agent: {e}[/red]")
58
- return ExecutionResult(success=False, message='Execution failed')
59
-
60
- def _write_log(self, task_id: str, result: subprocess.CompletedProcess[str]) -> None:
61
- log_dir = self.project_root / ".devcouncil" / "logs"
62
- log_dir.mkdir(parents=True, exist_ok=True)
63
- log_path = log_dir / f"{task_id}-mini-swe.log"
64
- log_path.write_text(
65
- "\n".join([
66
- f"command_returncode={result.returncode}",
67
- "=== stdout ===",
68
- result.stdout or "",
69
- "=== stderr ===",
70
- result.stderr or "",
71
- ]),
72
- encoding="utf-8",
73
- )
31
+ "--instruction-file", str(instruction_file),
32
+ "--repo-path", str(self.project_root)
33
+ ]
34
+
35
+ console.print(f"Command: [dim]{' '.join(cmd)}[/dim]")
36
+
37
+ # Since I might not have mini_swe_agent installed here,
38
+ # I'll just explain what it would do.
39
+ console.print("[yellow]Note: mini_swe_agent must be installed in the environment.[/yellow]")
40
+
41
+ try:
42
+ result = subprocess.run(
43
+ cmd,
44
+ capture_output=True,
45
+ text=True,
46
+ encoding="utf-8",
47
+ errors="replace",
48
+ cwd=self.project_root,
49
+ timeout=1800,
50
+ )
51
+ self._write_log(task.id, result)
52
+ if result.returncode != 0:
53
+ console.print(f"[red]mini-SWE-agent exited with {result.returncode}.[/red]")
54
+ return ExecutionResult(success=False, message='Execution failed')
55
+ return ExecutionResult(success=True, message='Execution successful')
56
+ except Exception as e:
57
+ console.print(f"[red]Error running mini-SWE-agent: {e}[/red]")
58
+ return ExecutionResult(success=False, message='Execution failed')
59
+
60
+ def _write_log(self, task_id: str, result: subprocess.CompletedProcess[str]) -> None:
61
+ log_dir = self.project_root / ".devcouncil" / "logs"
62
+ log_dir.mkdir(parents=True, exist_ok=True)
63
+ log_path = log_dir / f"{task_id}-mini-swe.log"
64
+ log_path.write_text(
65
+ "\n".join([
66
+ f"command_returncode={result.returncode}",
67
+ "=== stdout ===",
68
+ result.stdout or "",
69
+ "=== stderr ===",
70
+ result.stderr or "",
71
+ ]),
72
+ encoding="utf-8",
73
+ )
@@ -1,107 +1,208 @@
1
- from typing import List, Dict, Any
2
- from rich.console import Console
3
- from pydantic import BaseModel
4
- from devcouncil.domain.task import Task
5
- from devcouncil.domain.requirement import Requirement
6
- from devcouncil.execution.executor import Executor, ExecutionResult
7
- from devcouncil.llm.router import ModelRouter
1
+ from typing import List, Dict, Any
2
+ import asyncio
3
+ from rich.console import Console
4
+ from pydantic import BaseModel
5
+ from devcouncil.domain.task import Task
6
+ from devcouncil.domain.requirement import Requirement
7
+ from devcouncil.execution.executor import Executor, ExecutionResult
8
+ from devcouncil.llm.router import ModelRouter, StructuredOutputError
8
9
  from devcouncil.execution.task_runner import TaskRunner
9
10
  from devcouncil.execution.context_builder import ContextBuilder
11
+ from devcouncil.execution.prompt_builder import PromptBuilder
10
12
  from devcouncil.execution.paths import resolve_project_path
11
-
12
- console = Console()
13
-
14
- class ToolCall(BaseModel):
15
- tool: str
16
- args: Dict[str, Any]
17
-
18
- class AgentAction(BaseModel):
19
- thought: str
20
- tool_calls: List[ToolCall] = []
21
- finish: bool = False
22
-
23
- class NativeAgent(Executor):
24
- def __init__(self, router: ModelRouter, task_runner: TaskRunner):
25
- self.router = router
26
- self.task_runner = task_runner
27
- self.context_builder = ContextBuilder(task_runner.project_root)
28
-
29
- async def run_task(self, task: Task, requirements: List[Requirement]) -> ExecutionResult:
30
- console.print(f"Starting [bold]Native Executor[/bold] for task {task.id}...")
31
-
32
- # 1. Gather rich context
33
- context_json = self.context_builder.build_task_context(task, requirements)
34
-
35
- system_prompt = f"""
36
- You are the DevCouncil Native Agent. Your goal is to implement the provided task.
37
- Current Project Context:
38
- {context_json}
39
-
40
- You have access to the following tools:
13
+ from devcouncil.app.errors import ExecutionError
14
+
15
+ console = Console()
16
+
17
+ # Resilience bounds for the preview native loop.
18
+ MAX_AGENT_STEPS = 10
19
+ MAX_STRUCTURED_FAILURES = 2 # model can't produce a valid action -> give up cleanly
20
+ MAX_CONSECUTIVE_PATCH_FAILURES = 3 # stop spinning on a patch the model can't fix
21
+
22
+ class ToolCall(BaseModel):
23
+ tool: str
24
+ args: Dict[str, Any]
25
+
26
+ class AgentAction(BaseModel):
27
+ thought: str
28
+ tool_calls: List[ToolCall] = []
29
+ finish: bool = False
30
+
31
+ class NativeAgent(Executor):
32
+ def __init__(self, router: ModelRouter, task_runner: TaskRunner):
33
+ self.router = router
34
+ self.task_runner = task_runner
35
+ # ContextBuilder is retained only for the cheap list_files file listing; the
36
+ # implementation context itself uses the budgeted PromptBuilder so the native
37
+ # executor gets the same repo-map orientation, symbol outlines, dependents and
38
+ # context-window budgeting as the CLI executors (rather than a flat JSON dump).
39
+ self.context_builder = ContextBuilder(task_runner.project_root)
40
+ self.prompt_builder = PromptBuilder(task_runner.project_root)
41
+
42
+ def run_task(self, task: Task, requirements: List[Requirement]) -> ExecutionResult:
43
+ """Run the preview native executor behind the normal synchronous executor contract."""
44
+ return asyncio.run(self._run_task_async(task, requirements))
45
+
46
+ async def _run_task_async(self, task: Task, requirements: List[Requirement]) -> ExecutionResult:
47
+ console.print(f"Starting [bold]Native Executor[/bold] for task {task.id}...")
48
+ console.print("[yellow]Native executor is preview quality; DevCouncil verification remains the completion gate.[/yellow]")
49
+
50
+ # 1. Gather rich context (budgeted; includes repo-map orientation + symbol outlines)
51
+ context_block = self.prompt_builder.build_task_prompt(task, requirements)
52
+ from devcouncil.planning.correction_manifest import load_latest_correction_manifest
53
+
54
+ correction = load_latest_correction_manifest(self.task_runner.project_root, task.id)
55
+ correction_block = ""
56
+ if correction is not None:
57
+ correction_block = f"\nCorrection Manifest:\n{correction.model_dump_json(indent=2)}\n"
58
+
59
+ system_prompt = f"""
60
+ You are the DevCouncil Native Agent. Your goal is to implement the provided task.
61
+ Current Project Context:
62
+ {context_block}
63
+ {correction_block}
64
+
65
+ You have access to the following tools:
41
66
  - read_file(path: str)
42
67
  - list_files()
43
- - apply_patch(patch: str)
68
+ - apply_patch(patch: str) OR apply_patch(path: str, content: str) as a fallback when a valid unified diff cannot be produced
44
69
  - run_command(command: str)
45
70
 
46
71
  Rules:
47
72
  1. You can only write to files or apply patches to files listed in the task's 'planned_files'.
48
- 2. You can only run commands listed in the task's 'allowed_commands'.
49
- 3. Use 'thought' to explain your reasoning.
50
- 4. Set 'finish' to true when you believe the task is complete and verified.
51
- """
52
- messages = [{"role": "system", "content": system_prompt}]
53
-
54
- # Initial task prompt
55
- messages.append({"role": "user", "content": f"Begin implementing task {task.id} based on the context provided."})
56
-
57
- # Basic tool loop (Max 10 steps for safety in MVP)
58
- for step in range(10):
59
- action = await self.router.complete_structured(
60
- role="native_agent",
61
- messages=messages,
62
- schema=AgentAction
63
- )
64
-
65
- console.print(f"\n[bold]Step {step+1}:[/bold] {action.thought}")
66
-
67
- if action.finish:
68
- console.print("[green]Native agent signaled completion.[/green]")
69
- return ExecutionResult(success=True, message="Agent signaled completion")
70
-
71
- for tool_call in action.tool_calls:
72
- result_summary = ""
73
- try:
73
+ 2. You can only run commands listed in the task's 'allowed_commands'.
74
+ 3. Use 'thought' to explain your reasoning.
75
+ 4. Set 'finish' to true when you believe the task is complete and verified.
76
+ """
77
+ messages = [{"role": "system", "content": system_prompt}]
78
+
79
+ # Initial task prompt
80
+ messages.append({"role": "user", "content": f"Begin implementing task {task.id} based on the context provided."})
81
+
82
+ # Bounded tool loop. Counters let us fail a single task cleanly instead of
83
+ # crashing the whole run (structured-output faults) or spinning on an
84
+ # unfixable patch.
85
+ structured_failures = 0
86
+ consecutive_patch_failures = 0
87
+ for step in range(MAX_AGENT_STEPS):
88
+ try:
89
+ action = await self.router.complete_structured(
90
+ role="native_agent",
91
+ messages=messages,
92
+ schema=AgentAction,
93
+ )
94
+ except StructuredOutputError as exc:
95
+ # The model could not produce a valid action even after healing/retry.
96
+ # native_agent has no fallback by design, so handle it here rather than
97
+ # letting it propagate and abort the entire `dev go` run.
98
+ structured_failures += 1
99
+ console.print(f"[red]Native agent could not parse a valid action: {exc}[/red]")
100
+ if structured_failures >= MAX_STRUCTURED_FAILURES:
101
+ return ExecutionResult(
102
+ success=False,
103
+ message=f"Native agent gave up after {structured_failures} unparseable responses.",
104
+ )
105
+ messages.append({
106
+ "role": "user",
107
+ "content": (
108
+ "[System] Your previous response was not valid JSON for the "
109
+ "AgentAction schema. Reply with a single valid JSON object only "
110
+ "(fields: thought, tool_calls, finish) — no prose, no fences."
111
+ ),
112
+ })
113
+ continue
114
+ structured_failures = 0
115
+
116
+ console.print(f"\n[bold]Step {step+1}:[/bold] {action.thought}")
117
+
118
+ # Record the agent's own turn so subsequent steps see what it already did.
119
+ # Without this the model only sees tool RESULTS, not its prior actions, and
120
+ # tends to repeat itself and never converge within the step budget.
121
+ messages.append({"role": "assistant", "content": action.model_dump_json()})
122
+
123
+ if action.finish:
124
+ console.print("[green]Native agent signaled completion.[/green]")
125
+ return ExecutionResult(success=True, message="Agent signaled completion; pending DevCouncil verification")
126
+
127
+ if not action.tool_calls:
128
+ # No action and not finished — nudge instead of silently burning a step.
129
+ messages.append({"role": "user", "content": (
130
+ "[System] You produced no tool_calls and did not finish. Call a tool "
131
+ "(read_file/list_files/apply_patch/run_command) to make progress, or set "
132
+ "finish=true if the task is complete."
133
+ )})
134
+ continue
135
+
136
+ for tool_call in action.tool_calls:
137
+ result_summary = ""
138
+ try:
74
139
  if tool_call.tool == "read_file":
75
140
  path = tool_call.args["path"]
76
141
  resolved = resolve_project_path(self.task_runner.project_root, path)
77
142
  # Security: block reading sensitive files
78
143
  sensitive_patterns = {".env", ".pem", ".key", "credentials", "secrets"}
79
- path_lower = path.lower()
80
- if any(s in path_lower for s in sensitive_patterns):
81
- raise PermissionError(f"Reading sensitive file blocked: {path}")
82
- content = resolved.read_text(encoding="utf-8")
83
- if len(content) > 8000:
84
- content = content[:8000] + "\n[truncated]"
85
- result_summary = f"File content of {path}:\n{content}"
86
- elif tool_call.tool == "list_files":
87
- # We use the internal helper but return limited list
88
- files = self.context_builder.get_structure_summary()
89
- result_summary = f"Found {len(files)} files in repository."
144
+ path_lower = path.lower()
145
+ if any(s in path_lower for s in sensitive_patterns):
146
+ raise PermissionError(f"Reading sensitive file blocked: {path}")
147
+ content = resolved.read_text(encoding="utf-8")
148
+ if len(content) > 8000:
149
+ content = content[:8000] + "\n[truncated]"
150
+ result_summary = f"File content of {path}:\n{content}"
151
+ elif tool_call.tool == "list_files":
152
+ # We use the internal helper but return limited list
153
+ files = self.context_builder.get_structure_summary()
154
+ result_summary = f"Found {len(files)} files in repository."
90
155
  elif tool_call.tool == "write_file":
91
156
  raise PermissionError("write_file is disabled for the native executor; use apply_patch.")
92
157
  elif tool_call.tool == "apply_patch":
93
- self.task_runner.apply_patch(tool_call.args["patch"], task)
94
- result_summary = "Successfully applied patch."
158
+ if "path" in tool_call.args and "content" in tool_call.args:
159
+ # Fallback for when the model can't produce a valid unified
160
+ # diff. Routes through write_file, which enforces the same
161
+ # planned-files permission check — no widening of scope.
162
+ self.task_runner.write_file(
163
+ tool_call.args["path"], tool_call.args["content"], task
164
+ )
165
+ consecutive_patch_failures = 0
166
+ result_summary = f"Wrote {tool_call.args['path']} via path+content fallback."
167
+ else:
168
+ patch = tool_call.args.get("patch", "")
169
+ if not patch or not patch.strip():
170
+ raise ExecutionError(
171
+ "Empty patch. Provide a unified git diff beginning with "
172
+ "'diff --git a/<path> b/<path>', then '--- a/<path>' (or "
173
+ "'--- /dev/null' for a new file), '+++ b/<path>', and '@@' hunks."
174
+ )
175
+ self.task_runner.apply_patch(patch, task)
176
+ consecutive_patch_failures = 0
177
+ result_summary = "Successfully applied patch."
95
178
  elif tool_call.tool == "run_command":
96
179
  cmd_result = self.task_runner.run_command(tool_call.args["command"], task)
97
180
  result_summary = f"Command finished with exit code {cmd_result.exit_code}."
98
181
  else:
99
182
  raise ValueError(f"Unknown tool: {tool_call.tool}")
100
-
101
- messages.append({"role": "user", "content": f"[Tool Result] '{tool_call.tool}': {result_summary}"})
102
- except Exception as e:
103
- console.print(f"[red]Error executing tool {tool_call.tool}: {e}[/red]")
104
- messages.append({"role": "user", "content": f"[Tool Error] '{tool_call.tool}' failed: {e}"})
105
-
106
- console.print("[red]Native agent reached maximum step limit.[/red]")
107
- return ExecutionResult(success=False, message="Reached maximum step limit")
183
+
184
+ messages.append({"role": "user", "content": f"[Tool Result] '{tool_call.tool}': {result_summary}"})
185
+ except Exception as e:
186
+ console.print(f"[red]Error executing tool {tool_call.tool}: {e}[/red]")
187
+ if tool_call.tool == "apply_patch":
188
+ consecutive_patch_failures += 1
189
+ if consecutive_patch_failures >= MAX_CONSECUTIVE_PATCH_FAILURES:
190
+ return ExecutionResult(
191
+ success=False,
192
+ message=(
193
+ f"Native agent failed to apply a patch "
194
+ f"{consecutive_patch_failures} times in a row."
195
+ ),
196
+ )
197
+ messages.append({"role": "user", "content": (
198
+ f"[Tool Error] 'apply_patch' failed: {e}\n"
199
+ "Re-read the target file, match the existing context lines EXACTLY, "
200
+ "and do NOT resubmit the same patch. If you cannot produce a valid "
201
+ "unified diff, call apply_patch with 'path' and 'content' instead to "
202
+ "write the whole file."
203
+ )})
204
+ else:
205
+ messages.append({"role": "user", "content": f"[Tool Error] '{tool_call.tool}' failed: {e}"})
206
+
207
+ console.print("[red]Native agent reached maximum step limit.[/red]")
208
+ return ExecutionResult(success=False, message="Reached maximum step limit")
@@ -1,23 +1,23 @@
1
- import subprocess
2
- from pathlib import Path
3
- from rich.console import Console
4
- from devcouncil.domain.task import Task
5
- from devcouncil.domain.requirement import Requirement
6
- from devcouncil.execution.executor import Executor, ExecutionResult
7
- from devcouncil.execution.prompt_builder import PromptBuilder
8
-
9
- console = Console()
10
-
11
- class OpenHandsExecutor(Executor):
12
- def __init__(self, project_root: Path):
13
- self.project_root = project_root
14
-
15
- def run_task(self, task: Task, requirements: list[Requirement]) -> ExecutionResult:
1
+ import subprocess
2
+ from pathlib import Path
3
+ from rich.console import Console
4
+ from devcouncil.domain.task import Task
5
+ from devcouncil.domain.requirement import Requirement
6
+ from devcouncil.execution.executor import Executor, ExecutionResult
7
+ from devcouncil.execution.prompt_builder import PromptBuilder
8
+
9
+ console = Console()
10
+
11
+ class OpenHandsExecutor(Executor):
12
+ def __init__(self, project_root: Path):
13
+ self.project_root = project_root
14
+
15
+ def run_task(self, task: Task, requirements: list[Requirement]) -> ExecutionResult:
16
16
  builder = PromptBuilder(self.project_root)
17
- task_prompt = builder.build_task_prompt(task, requirements)
18
-
19
- console.print(f"Starting [bold]OpenHands[/bold] for task {task.id}...")
20
-
17
+ task_prompt = builder.build_task_prompt(task, requirements)
18
+
19
+ console.print(f"Starting [bold]OpenHands[/bold] for task {task.id}...")
20
+
21
21
  # OpenHands often expects a workspace mount and an instruction.
22
22
  # Keep the full prompt out of argv so Windows command-line limits and
23
23
  # terminal logs do not become part of the execution boundary.
@@ -32,40 +32,40 @@ class OpenHandsExecutor(Executor):
32
32
  "--task-file", str(instruction_file),
33
33
  "--headless"
34
34
  ]
35
-
36
- console.print(f"Command: [dim]{' '.join(cmd)}[/dim]")
37
- console.print("[yellow]Note: OpenHands must be installed and configured in the environment.[/yellow]")
38
-
39
- try:
40
- result = subprocess.run(
41
- cmd,
42
- capture_output=True,
43
- text=True,
44
- encoding="utf-8",
45
- errors="replace",
46
- cwd=self.project_root,
47
- timeout=1800,
48
- )
49
- self._write_log(task.id, result)
50
- if result.returncode != 0:
51
- console.print(f"[red]OpenHands exited with {result.returncode}.[/red]")
52
- return ExecutionResult(success=False, message=f"Exited with code {result.returncode}")
53
- return ExecutionResult(success=True, message="Completed successfully")
54
- except Exception as e:
55
- console.print(f"[red]Error running OpenHands: {e}[/red]")
56
- return ExecutionResult(success=False, message=str(e))
57
-
58
- def _write_log(self, task_id: str, result: subprocess.CompletedProcess[str]) -> None:
59
- log_dir = self.project_root / ".devcouncil" / "logs"
60
- log_dir.mkdir(parents=True, exist_ok=True)
61
- log_path = log_dir / f"{task_id}-openhands.log"
62
- log_path.write_text(
63
- "\n".join([
64
- f"command_returncode={result.returncode}",
65
- "=== stdout ===",
66
- result.stdout or "",
67
- "=== stderr ===",
68
- result.stderr or "",
69
- ]),
70
- encoding="utf-8",
71
- )
35
+
36
+ console.print(f"Command: [dim]{' '.join(cmd)}[/dim]")
37
+ console.print("[yellow]Note: OpenHands must be installed and configured in the environment.[/yellow]")
38
+
39
+ try:
40
+ result = subprocess.run(
41
+ cmd,
42
+ capture_output=True,
43
+ text=True,
44
+ encoding="utf-8",
45
+ errors="replace",
46
+ cwd=self.project_root,
47
+ timeout=1800,
48
+ )
49
+ self._write_log(task.id, result)
50
+ if result.returncode != 0:
51
+ console.print(f"[red]OpenHands exited with {result.returncode}.[/red]")
52
+ return ExecutionResult(success=False, message=f"Exited with code {result.returncode}")
53
+ return ExecutionResult(success=True, message="Completed successfully")
54
+ except Exception as e:
55
+ console.print(f"[red]Error running OpenHands: {e}[/red]")
56
+ return ExecutionResult(success=False, message=str(e))
57
+
58
+ def _write_log(self, task_id: str, result: subprocess.CompletedProcess[str]) -> None:
59
+ log_dir = self.project_root / ".devcouncil" / "logs"
60
+ log_dir.mkdir(parents=True, exist_ok=True)
61
+ log_path = log_dir / f"{task_id}-openhands.log"
62
+ log_path.write_text(
63
+ "\n".join([
64
+ f"command_returncode={result.returncode}",
65
+ "=== stdout ===",
66
+ result.stdout or "",
67
+ "=== stderr ===",
68
+ result.stderr or "",
69
+ ]),
70
+ encoding="utf-8",
71
+ )
@@ -1 +1 @@
1
-
1
+