devcouncil 0.1.0 → 0.1.1

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 (128) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +62 -543
  3. package/package.json +1 -1
  4. package/pyproject.toml +29 -26
  5. package/src/devcouncil/__main__.py +4 -4
  6. package/src/devcouncil/app/__init__.py +28 -28
  7. package/src/devcouncil/app/config.py +135 -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 +143 -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/cli/commands/artifacts.py +51 -48
  22. package/src/devcouncil/cli/commands/ast.py +22 -0
  23. package/src/devcouncil/cli/commands/baseline.py +35 -32
  24. package/src/devcouncil/cli/commands/config.py +76 -54
  25. package/src/devcouncil/cli/commands/dashboard.py +26 -0
  26. package/src/devcouncil/cli/commands/doctor.py +86 -42
  27. package/src/devcouncil/cli/commands/go.py +237 -0
  28. package/src/devcouncil/cli/commands/hook.py +96 -29
  29. package/src/devcouncil/cli/commands/init.py +67 -56
  30. package/src/devcouncil/cli/commands/integrate.py +320 -14
  31. package/src/devcouncil/cli/commands/lsp.py +20 -0
  32. package/src/devcouncil/cli/commands/map.py +25 -21
  33. package/src/devcouncil/cli/commands/plan.py +257 -206
  34. package/src/devcouncil/cli/commands/prompt.py +36 -33
  35. package/src/devcouncil/cli/commands/repair.py +72 -69
  36. package/src/devcouncil/cli/commands/report.py +112 -54
  37. package/src/devcouncil/cli/commands/reset_demo_state.py +31 -28
  38. package/src/devcouncil/cli/commands/rollback.py +49 -47
  39. package/src/devcouncil/cli/commands/run.py +252 -207
  40. package/src/devcouncil/cli/commands/setup.py +159 -18
  41. package/src/devcouncil/cli/commands/show.py +76 -57
  42. package/src/devcouncil/cli/commands/status.py +117 -105
  43. package/src/devcouncil/cli/commands/tasks.py +55 -41
  44. package/src/devcouncil/cli/commands/trace.py +2 -1
  45. package/src/devcouncil/cli/commands/verify.py +158 -128
  46. package/src/devcouncil/cli/commands/version.py +20 -20
  47. package/src/devcouncil/cli/commands/watch.py +574 -0
  48. package/src/devcouncil/cli/main.py +42 -24
  49. package/src/devcouncil/council/prompts/arbiter.md +19 -19
  50. package/src/devcouncil/council/prompts/critic_a.md +10 -10
  51. package/src/devcouncil/council/prompts/critic_b.md +10 -10
  52. package/src/devcouncil/council/prompts/implementation_reviewer.md +16 -16
  53. package/src/devcouncil/council/prompts/planner_a.md +16 -16
  54. package/src/devcouncil/council/prompts/planner_b.md +16 -16
  55. package/src/devcouncil/council/prompts/rebuttal.md +10 -10
  56. package/src/devcouncil/council/prompts/spec_writer.md +12 -12
  57. package/src/devcouncil/domain/assumption.py +17 -17
  58. package/src/devcouncil/domain/critique.py +32 -32
  59. package/src/devcouncil/domain/evidence.py +27 -27
  60. package/src/devcouncil/domain/gap.py +26 -26
  61. package/src/devcouncil/domain/requirement.py +22 -22
  62. package/src/devcouncil/domain/task.py +26 -26
  63. package/src/devcouncil/execution/__init__.py +1 -1
  64. package/src/devcouncil/execution/context_builder.py +54 -54
  65. package/src/devcouncil/execution/executor.py +15 -15
  66. package/src/devcouncil/execution/hook_policy.py +24 -3
  67. package/src/devcouncil/execution/patch.py +28 -28
  68. package/src/devcouncil/execution/permissions.py +44 -44
  69. package/src/devcouncil/execution/prompt_builder.py +23 -23
  70. package/src/devcouncil/execution/task_runner.py +63 -63
  71. package/src/devcouncil/executors/__init__.py +1 -1
  72. package/src/devcouncil/executors/coding_cli.py +112 -0
  73. package/src/devcouncil/executors/mini_swe.py +63 -63
  74. package/src/devcouncil/executors/native/agent.py +81 -81
  75. package/src/devcouncil/executors/openhands.py +56 -56
  76. package/src/devcouncil/gating/__init__.py +1 -1
  77. package/src/devcouncil/gating/checks/clean_git.py +50 -45
  78. package/src/devcouncil/gating/checks/planned_files_check.py +32 -32
  79. package/src/devcouncil/gating/checks/requirement_coverage.py +26 -26
  80. package/src/devcouncil/gating/checks/secret_scan_check.py +34 -34
  81. package/src/devcouncil/gating/policy.py +157 -157
  82. package/src/devcouncil/indexing/__init__.py +1 -1
  83. package/src/devcouncil/indexing/ast_matcher.py +168 -0
  84. package/src/devcouncil/indexing/graph_index.py +48 -48
  85. package/src/devcouncil/indexing/lsp.py +120 -0
  86. package/src/devcouncil/indexing/repo_mapper.py +208 -204
  87. package/src/devcouncil/integrations/github.py +35 -35
  88. package/src/devcouncil/integrations/gitnexus.py +27 -27
  89. package/src/devcouncil/integrations/graphify.py +34 -34
  90. package/src/devcouncil/integrations/mcp/server.py +549 -96
  91. package/src/devcouncil/integrations/pr_comments.py +62 -0
  92. package/src/devcouncil/live/__init__.py +2 -0
  93. package/src/devcouncil/live/cards.py +207 -0
  94. package/src/devcouncil/live/models.py +63 -0
  95. package/src/devcouncil/live/repair_prompt.py +83 -0
  96. package/src/devcouncil/live/reviewer.py +70 -0
  97. package/src/devcouncil/live/signals.py +135 -0
  98. package/src/devcouncil/live/summary.py +34 -0
  99. package/src/devcouncil/live/tasks.py +18 -0
  100. package/src/devcouncil/live/transcripts.py +138 -0
  101. package/src/devcouncil/llm/__init__.py +1 -1
  102. package/src/devcouncil/llm/cache.py +38 -38
  103. package/src/devcouncil/llm/provider.py +146 -125
  104. package/src/devcouncil/llm/router.py +111 -111
  105. package/src/devcouncil/planning/__init__.py +1 -1
  106. package/src/devcouncil/planning/arbiter_service.py +57 -57
  107. package/src/devcouncil/planning/critique_service.py +66 -66
  108. package/src/devcouncil/planning/plan_service.py +46 -46
  109. package/src/devcouncil/planning/prompt_enhancer_service.py +86 -0
  110. package/src/devcouncil/planning/repair_service.py +39 -39
  111. package/src/devcouncil/planning/spec_service.py +44 -44
  112. package/src/devcouncil/reporting/github_check.py +32 -32
  113. package/src/devcouncil/reporting/json_report.py +20 -17
  114. package/src/devcouncil/reporting/markdown_report.py +68 -46
  115. package/src/devcouncil/reporting/report_builder.py +14 -14
  116. package/src/devcouncil/storage/db.py +66 -66
  117. package/src/devcouncil/storage/models.py +83 -83
  118. package/src/devcouncil/storage/repositories.py +299 -222
  119. package/src/devcouncil/telemetry/cost.py +34 -34
  120. package/src/devcouncil/telemetry/tracker.py +49 -49
  121. package/src/devcouncil/ui/__init__.py +1 -0
  122. package/src/devcouncil/ui/dashboard.py +122 -0
  123. package/src/devcouncil/utils/__init__.py +1 -1
  124. package/src/devcouncil/utils/redaction.py +141 -141
  125. package/src/devcouncil/verification/__init__.py +1 -1
  126. package/src/devcouncil/verification/implementation_reviewer.py +55 -55
  127. package/src/devcouncil/verification/verifier.py +319 -302
  128. package/uv.lock +1 -1
@@ -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,43 +1,43 @@
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
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
7
  from devcouncil.llm.router import ModelRouter
8
8
  from devcouncil.execution.task_runner import TaskRunner
9
9
  from devcouncil.execution.context_builder import ContextBuilder
10
10
  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:
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:
41
41
  - read_file(path: str)
42
42
  - list_files()
43
43
  - apply_patch(patch: str)
@@ -45,48 +45,48 @@ You have access to the following tools:
45
45
 
46
46
  Rules:
47
47
  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:
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:
74
74
  if tool_call.tool == "read_file":
75
75
  path = tool_call.args["path"]
76
76
  resolved = resolve_project_path(self.task_runner.project_root, path)
77
77
  # Security: block reading sensitive files
78
78
  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."
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."
90
90
  elif tool_call.tool == "write_file":
91
91
  raise PermissionError("write_file is disabled for the native executor; use apply_patch.")
92
92
  elif tool_call.tool == "apply_patch":
@@ -97,11 +97,11 @@ Rules:
97
97
  result_summary = f"Command finished with exit code {cmd_result.exit_code}."
98
98
  else:
99
99
  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")
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")
@@ -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
+
@@ -1,45 +1,50 @@
1
- import subprocess
2
- import logging
3
- from devcouncil.domain.gap import Gap
4
-
5
- logger = logging.getLogger(__name__)
6
-
7
- class CleanGitCheck:
8
- """Ensures the working tree is clean before a task starts."""
9
-
10
- def check(self, project_root, task_id: str) -> list[Gap]:
11
- try:
12
- status = subprocess.check_output(["git", "status", "--porcelain"], cwd=project_root).decode()
13
- if status.strip():
14
- return [Gap(
15
- id=f"GAP-{task_id}-DIRTY-GIT",
16
- severity="high",
17
- gap_type="architecture_drift",
18
- task_id=task_id,
19
- description="Git working tree is dirty. Execution requires a clean state for checkpointing.",
20
- recommended_fix="Commit or stash your current changes before running the task.",
21
- blocking=True
22
- )]
23
- except FileNotFoundError:
24
- logger.error("Git is not installed or not in PATH.")
25
- return [Gap(
26
- id=f"GAP-{task_id}-NO-GIT",
27
- severity="high",
28
- gap_type="architecture_drift",
29
- task_id=task_id,
30
- description="Git is not available. Cannot verify working tree cleanliness.",
31
- recommended_fix="Install git and ensure it is in your PATH.",
32
- blocking=True
33
- )]
34
- except subprocess.CalledProcessError as e:
35
- logger.warning("Git status check failed: %s", e)
36
- return [Gap(
37
- id=f"GAP-{task_id}-GIT-ERROR",
38
- severity="medium",
39
- gap_type="architecture_drift",
40
- task_id=task_id,
41
- description=f"Git status check failed: {e}. Directory may not be a git repository.",
42
- recommended_fix="Initialize a git repository with 'git init' before running tasks.",
43
- blocking=True
44
- )]
45
- return []
1
+ import subprocess
2
+ import logging
3
+ from devcouncil.domain.gap import Gap
4
+
5
+ logger = logging.getLogger(__name__)
6
+
7
+ class CleanGitCheck:
8
+ """Ensures the working tree is clean before a task starts."""
9
+
10
+ def _is_runtime_state(self, line: str) -> bool:
11
+ path = line[3:].strip().replace("\\", "/")
12
+ return path.startswith(".devcouncil/")
13
+
14
+ def check(self, project_root, task_id: str) -> list[Gap]:
15
+ try:
16
+ status = subprocess.check_output(["git", "status", "--porcelain"], cwd=project_root).decode()
17
+ dirty_lines = [line for line in status.splitlines() if line.strip() and not self._is_runtime_state(line)]
18
+ if dirty_lines:
19
+ return [Gap(
20
+ id=f"GAP-{task_id}-DIRTY-GIT",
21
+ severity="high",
22
+ gap_type="architecture_drift",
23
+ task_id=task_id,
24
+ description="Git working tree is dirty. Execution requires a clean state for checkpointing.",
25
+ recommended_fix="Commit or stash your current changes before running the task.",
26
+ blocking=True
27
+ )]
28
+ except FileNotFoundError:
29
+ logger.error("Git is not installed or not in PATH.")
30
+ return [Gap(
31
+ id=f"GAP-{task_id}-NO-GIT",
32
+ severity="high",
33
+ gap_type="architecture_drift",
34
+ task_id=task_id,
35
+ description="Git is not available. Cannot verify working tree cleanliness.",
36
+ recommended_fix="Install git and ensure it is in your PATH.",
37
+ blocking=True
38
+ )]
39
+ except subprocess.CalledProcessError as e:
40
+ logger.warning("Git status check failed: %s", e)
41
+ return [Gap(
42
+ id=f"GAP-{task_id}-GIT-ERROR",
43
+ severity="medium",
44
+ gap_type="architecture_drift",
45
+ task_id=task_id,
46
+ description=f"Git status check failed: {e}. Directory may not be a git repository.",
47
+ recommended_fix="Initialize a git repository with 'git init' before running tasks.",
48
+ blocking=True
49
+ )]
50
+ return []
@@ -1,32 +1,32 @@
1
- from devcouncil.domain.task import Task
2
- from devcouncil.domain.gap import Gap
3
-
4
- class PlannedFilesCheck:
5
- """Ensures a task has legitimate files planned for modification."""
6
-
7
- def check(self, task: Task) -> list[Gap]:
8
- gaps = []
9
- if not task.planned_files:
10
- gaps.append(Gap(
11
- id=f"GAP-{task.id}-NO-FILES",
12
- severity="high",
13
- gap_type="task_not_implemented",
14
- task_id=task.id,
15
- description=f"Task {task.id} has no planned files. Agents won't know where to write code.",
16
- recommended_fix="Update the task to include at least one planned file path.",
17
- blocking=True
18
- ))
19
-
20
- has_modify = any(pf.allowed_change in ["create", "modify", "delete"] for pf in task.planned_files)
21
- if task.planned_files and not has_modify:
22
- gaps.append(Gap(
23
- id=f"GAP-{task.id}-READ-ONLY",
24
- severity="medium",
25
- gap_type="task_not_implemented",
26
- task_id=task.id,
27
- description=f"Task {task.id} only has read-only files. No changes can be made.",
28
- recommended_fix="Grant 'modify' or 'create' permissions to at least one file.",
29
- blocking=True
30
- ))
31
-
32
- return gaps
1
+ from devcouncil.domain.task import Task
2
+ from devcouncil.domain.gap import Gap
3
+
4
+ class PlannedFilesCheck:
5
+ """Ensures a task has legitimate files planned for modification."""
6
+
7
+ def check(self, task: Task) -> list[Gap]:
8
+ gaps = []
9
+ if not task.planned_files:
10
+ gaps.append(Gap(
11
+ id=f"GAP-{task.id}-NO-FILES",
12
+ severity="high",
13
+ gap_type="task_not_implemented",
14
+ task_id=task.id,
15
+ description=f"Task {task.id} has no planned files. Agents won't know where to write code.",
16
+ recommended_fix="Update the task to include at least one planned file path.",
17
+ blocking=True
18
+ ))
19
+
20
+ has_modify = any(pf.allowed_change in ["create", "modify", "delete"] for pf in task.planned_files)
21
+ if task.planned_files and not has_modify:
22
+ gaps.append(Gap(
23
+ id=f"GAP-{task.id}-READ-ONLY",
24
+ severity="medium",
25
+ gap_type="task_not_implemented",
26
+ task_id=task.id,
27
+ description=f"Task {task.id} only has read-only files. No changes can be made.",
28
+ recommended_fix="Grant 'modify' or 'create' permissions to at least one file.",
29
+ blocking=True
30
+ ))
31
+
32
+ return gaps