devcouncil 0.1.1 → 0.2.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.
- package/README.md +218 -96
- package/bin/devcouncil.js +130 -32
- package/package.json +7 -11
- package/pyproject.toml +0 -34
- package/src/devcouncil/__init__.py +0 -0
- package/src/devcouncil/__main__.py +0 -4
- package/src/devcouncil/app/__init__.py +0 -28
- package/src/devcouncil/app/config.py +0 -158
- package/src/devcouncil/app/errors.py +0 -23
- package/src/devcouncil/app/events.py +0 -44
- package/src/devcouncil/app/orchestrator.py +0 -92
- package/src/devcouncil/app/project_status.py +0 -29
- package/src/devcouncil/app/run_context.py +0 -39
- package/src/devcouncil/app/state_machine.py +0 -108
- package/src/devcouncil/artifacts/__init__.py +0 -1
- package/src/devcouncil/artifacts/coverage.py +0 -96
- package/src/devcouncil/artifacts/graph.py +0 -143
- package/src/devcouncil/artifacts/migrations.py +0 -20
- package/src/devcouncil/artifacts/schemas.py +0 -23
- package/src/devcouncil/artifacts/serializer.py +0 -21
- package/src/devcouncil/artifacts/validators.py +0 -27
- package/src/devcouncil/cli/__init__.py +0 -0
- package/src/devcouncil/cli/commands/__init__.py +0 -0
- package/src/devcouncil/cli/commands/artifacts.py +0 -51
- package/src/devcouncil/cli/commands/ast.py +0 -22
- package/src/devcouncil/cli/commands/baseline.py +0 -35
- package/src/devcouncil/cli/commands/config.py +0 -76
- package/src/devcouncil/cli/commands/dashboard.py +0 -26
- package/src/devcouncil/cli/commands/doctor.py +0 -140
- package/src/devcouncil/cli/commands/go.py +0 -237
- package/src/devcouncil/cli/commands/hook.py +0 -128
- package/src/devcouncil/cli/commands/init.py +0 -153
- package/src/devcouncil/cli/commands/integrate.py +0 -726
- package/src/devcouncil/cli/commands/lsp.py +0 -20
- package/src/devcouncil/cli/commands/map.py +0 -42
- package/src/devcouncil/cli/commands/mcp_server.py +0 -18
- package/src/devcouncil/cli/commands/plan.py +0 -327
- package/src/devcouncil/cli/commands/prompt.py +0 -50
- package/src/devcouncil/cli/commands/repair.py +0 -72
- package/src/devcouncil/cli/commands/report.py +0 -129
- package/src/devcouncil/cli/commands/reset_demo_state.py +0 -31
- package/src/devcouncil/cli/commands/rollback.py +0 -60
- package/src/devcouncil/cli/commands/run.py +0 -269
- package/src/devcouncil/cli/commands/setup.py +0 -223
- package/src/devcouncil/cli/commands/show.py +0 -76
- package/src/devcouncil/cli/commands/status.py +0 -117
- package/src/devcouncil/cli/commands/tasks.py +0 -55
- package/src/devcouncil/cli/commands/trace.py +0 -44
- package/src/devcouncil/cli/commands/verify.py +0 -193
- package/src/devcouncil/cli/commands/version.py +0 -20
- package/src/devcouncil/cli/commands/watch.py +0 -574
- package/src/devcouncil/cli/main.py +0 -88
- package/src/devcouncil/council/__init__.py +0 -0
- package/src/devcouncil/council/prompts/__init__.py +0 -0
- package/src/devcouncil/council/prompts/arbiter.md +0 -19
- package/src/devcouncil/council/prompts/critic_a.md +0 -10
- package/src/devcouncil/council/prompts/critic_b.md +0 -10
- package/src/devcouncil/council/prompts/implementation_reviewer.md +0 -16
- package/src/devcouncil/council/prompts/planner_a.md +0 -16
- package/src/devcouncil/council/prompts/planner_b.md +0 -16
- package/src/devcouncil/council/prompts/rebuttal.md +0 -10
- package/src/devcouncil/council/prompts/spec_writer.md +0 -12
- package/src/devcouncil/domain/__init__.py +0 -0
- package/src/devcouncil/domain/assumption.py +0 -17
- package/src/devcouncil/domain/critique.py +0 -32
- package/src/devcouncil/domain/evidence.py +0 -27
- package/src/devcouncil/domain/gap.py +0 -26
- package/src/devcouncil/domain/requirement.py +0 -22
- package/src/devcouncil/domain/task.py +0 -26
- package/src/devcouncil/execution/__init__.py +0 -1
- package/src/devcouncil/execution/context_builder.py +0 -60
- package/src/devcouncil/execution/executor.py +0 -15
- package/src/devcouncil/execution/hook_policy.py +0 -165
- package/src/devcouncil/execution/patch.py +0 -28
- package/src/devcouncil/execution/paths.py +0 -14
- package/src/devcouncil/execution/permissions.py +0 -92
- package/src/devcouncil/execution/prompt_builder.py +0 -59
- package/src/devcouncil/execution/task_runner.py +0 -166
- package/src/devcouncil/executors/__init__.py +0 -1
- package/src/devcouncil/executors/coding_cli.py +0 -112
- package/src/devcouncil/executors/mini_swe.py +0 -73
- package/src/devcouncil/executors/native/__init__.py +0 -0
- package/src/devcouncil/executors/native/agent.py +0 -107
- package/src/devcouncil/executors/openhands.py +0 -71
- package/src/devcouncil/gating/__init__.py +0 -1
- package/src/devcouncil/gating/checks/__init__.py +0 -0
- package/src/devcouncil/gating/checks/clean_git.py +0 -50
- package/src/devcouncil/gating/checks/planned_files_check.py +0 -32
- package/src/devcouncil/gating/checks/requirement_coverage.py +0 -26
- package/src/devcouncil/gating/checks/secret_scan_check.py +0 -34
- package/src/devcouncil/gating/policy.py +0 -190
- package/src/devcouncil/indexing/__init__.py +0 -1
- package/src/devcouncil/indexing/ast_matcher.py +0 -168
- package/src/devcouncil/indexing/graph_index.py +0 -48
- package/src/devcouncil/indexing/lsp.py +0 -120
- package/src/devcouncil/indexing/repo_mapper.py +0 -208
- package/src/devcouncil/indexing/symbol_index.py +0 -0
- package/src/devcouncil/integrations/code_review_graph.py +0 -163
- package/src/devcouncil/integrations/github.py +0 -39
- package/src/devcouncil/integrations/gitnexus.py +0 -27
- package/src/devcouncil/integrations/graphify.py +0 -34
- package/src/devcouncil/integrations/mcp/__init__.py +0 -0
- package/src/devcouncil/integrations/mcp/server.py +0 -599
- package/src/devcouncil/integrations/pr_comments.py +0 -62
- package/src/devcouncil/live/__init__.py +0 -2
- package/src/devcouncil/live/cards.py +0 -207
- package/src/devcouncil/live/models.py +0 -63
- package/src/devcouncil/live/repair_prompt.py +0 -83
- package/src/devcouncil/live/reviewer.py +0 -70
- package/src/devcouncil/live/signals.py +0 -135
- package/src/devcouncil/live/summary.py +0 -34
- package/src/devcouncil/live/tasks.py +0 -18
- package/src/devcouncil/live/transcripts.py +0 -138
- package/src/devcouncil/llm/__init__.py +0 -1
- package/src/devcouncil/llm/cache.py +0 -38
- package/src/devcouncil/llm/provider.py +0 -146
- package/src/devcouncil/llm/router.py +0 -125
- package/src/devcouncil/planning/__init__.py +0 -1
- package/src/devcouncil/planning/arbiter_service.py +0 -57
- package/src/devcouncil/planning/critique_service.py +0 -66
- package/src/devcouncil/planning/plan_service.py +0 -46
- package/src/devcouncil/planning/prompt_enhancer_service.py +0 -86
- package/src/devcouncil/planning/repair_service.py +0 -39
- package/src/devcouncil/planning/spec_service.py +0 -44
- package/src/devcouncil/repo/__init__.py +0 -0
- package/src/devcouncil/reporting/__init__.py +0 -0
- package/src/devcouncil/reporting/github_check.py +0 -32
- package/src/devcouncil/reporting/json_report.py +0 -20
- package/src/devcouncil/reporting/markdown_report.py +0 -68
- package/src/devcouncil/reporting/report_builder.py +0 -14
- package/src/devcouncil/storage/__init__.py +0 -0
- package/src/devcouncil/storage/db.py +0 -66
- package/src/devcouncil/storage/models.py +0 -83
- package/src/devcouncil/storage/repositories.py +0 -423
- package/src/devcouncil/telemetry/__init__.py +0 -0
- package/src/devcouncil/telemetry/cost.py +0 -34
- package/src/devcouncil/telemetry/traces.py +0 -91
- package/src/devcouncil/telemetry/tracker.py +0 -49
- package/src/devcouncil/ui/__init__.py +0 -1
- package/src/devcouncil/ui/dashboard.py +0 -122
- package/src/devcouncil/utils/__init__.py +0 -1
- package/src/devcouncil/utils/redaction.py +0 -141
- package/src/devcouncil/verification/__init__.py +0 -1
- package/src/devcouncil/verification/implementation_reviewer.py +0 -55
- package/src/devcouncil/verification/verifier.py +0 -530
- package/uv.lock +0 -1085
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
from pathlib import Path
|
|
2
|
-
from typing import List
|
|
3
|
-
|
|
4
|
-
from devcouncil.domain.requirement import Requirement
|
|
5
|
-
from devcouncil.domain.task import Task
|
|
6
|
-
from devcouncil.integrations.code_review_graph import CodeReviewGraphAdapter
|
|
7
|
-
|
|
8
|
-
class PromptBuilder:
|
|
9
|
-
def __init__(self, project_root: Path = Path(".")):
|
|
10
|
-
self.project_root = project_root
|
|
11
|
-
|
|
12
|
-
def build_task_prompt(self, task: Task, requirements: List[Requirement]) -> str:
|
|
13
|
-
req_map = {r.id: r for r in requirements}
|
|
14
|
-
task_reqs = [req_map[rid] for rid in task.requirement_ids if rid in req_map]
|
|
15
|
-
|
|
16
|
-
prompt = f"""# Implement {task.id}: {task.title}
|
|
17
|
-
|
|
18
|
-
## Goal
|
|
19
|
-
{task.description}
|
|
20
|
-
|
|
21
|
-
## Requirements
|
|
22
|
-
"""
|
|
23
|
-
for req in task_reqs:
|
|
24
|
-
prompt += f"- {req.id}: {req.title}\n"
|
|
25
|
-
for ac in req.acceptance_criteria:
|
|
26
|
-
prompt += f" - [ ] {ac.description} ({ac.verification_method})\n"
|
|
27
|
-
|
|
28
|
-
prompt += "\n## Allowed files\n"
|
|
29
|
-
for pf in task.planned_files:
|
|
30
|
-
prompt += f"- `{pf.path}` ({pf.allowed_change}): {pf.reason}\n"
|
|
31
|
-
|
|
32
|
-
if task.forbidden_changes:
|
|
33
|
-
prompt += "\n## Forbidden changes\n"
|
|
34
|
-
for fc in task.forbidden_changes:
|
|
35
|
-
prompt += f"- `{fc}`\n"
|
|
36
|
-
|
|
37
|
-
prompt += "\n## Expected tests\n"
|
|
38
|
-
for et in task.expected_tests:
|
|
39
|
-
prompt += f"- `{et}`\n"
|
|
40
|
-
|
|
41
|
-
prompt += "\n## Allowed commands\n"
|
|
42
|
-
for cmd in task.allowed_commands:
|
|
43
|
-
prompt += f"- `{cmd}`\n"
|
|
44
|
-
|
|
45
|
-
graph_context = CodeReviewGraphAdapter(self.project_root).prompt_section(
|
|
46
|
-
[planned.path for planned in task.planned_files]
|
|
47
|
-
)
|
|
48
|
-
if graph_context:
|
|
49
|
-
prompt += f"\n{graph_context}"
|
|
50
|
-
|
|
51
|
-
prompt += """
|
|
52
|
-
## Instructions
|
|
53
|
-
1. Implement the goal described above.
|
|
54
|
-
2. Ensure all acceptance criteria are met.
|
|
55
|
-
3. Only modify the allowed files.
|
|
56
|
-
4. Run the allowed commands to verify your work.
|
|
57
|
-
5. Provide evidence of passing tests.
|
|
58
|
-
"""
|
|
59
|
-
return prompt
|
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
import hashlib
|
|
2
|
-
import subprocess
|
|
3
|
-
import logging
|
|
4
|
-
import re
|
|
5
|
-
import shlex
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
from typing import Literal, Optional
|
|
8
|
-
from devcouncil.domain.task import Task
|
|
9
|
-
from devcouncil.execution.permissions import PermissionManager
|
|
10
|
-
from devcouncil.domain.evidence import CommandResult
|
|
11
|
-
from devcouncil.app.errors import ExecutionError
|
|
12
|
-
|
|
13
|
-
from devcouncil.execution.patch import PatchEngine
|
|
14
|
-
from devcouncil.execution.paths import resolve_project_path
|
|
15
|
-
from devcouncil.telemetry.traces import TraceLogger
|
|
16
|
-
from devcouncil.utils.redaction import redact_string
|
|
17
|
-
|
|
18
|
-
logger = logging.getLogger(__name__)
|
|
19
|
-
|
|
20
|
-
class TaskRunner:
|
|
21
|
-
"""Safely executes task actions while enforcing permission boundaries."""
|
|
22
|
-
|
|
23
|
-
def __init__(self, project_root: Path, permission_manager: PermissionManager):
|
|
24
|
-
self.project_root = project_root
|
|
25
|
-
self.permissions = permission_manager
|
|
26
|
-
self.patch_engine = PatchEngine(project_root)
|
|
27
|
-
|
|
28
|
-
def _validate_path_within_root(self, path: str) -> None:
|
|
29
|
-
"""Ensure a path resolves to a location within the project root."""
|
|
30
|
-
resolve_project_path(self.project_root, path)
|
|
31
|
-
|
|
32
|
-
def apply_patch(self, patch: str, task: Task) -> bool:
|
|
33
|
-
"""Apply a patch if permissions allow (all affected files must be in planned_files)."""
|
|
34
|
-
changes = self._extract_patch_changes(patch)
|
|
35
|
-
for path, operation in changes.items():
|
|
36
|
-
self._validate_path_within_root(path)
|
|
37
|
-
self.permissions.validate_action("file_write", path, task, operation=operation)
|
|
38
|
-
applied = self.patch_engine.apply_patch(patch)
|
|
39
|
-
TraceLogger(self.project_root).log_event(
|
|
40
|
-
"tool_patch_applied",
|
|
41
|
-
{"paths": sorted(changes), "success": applied},
|
|
42
|
-
task_id=task.id,
|
|
43
|
-
summary=f"Patch {'applied' if applied else 'failed'} for {task.id}",
|
|
44
|
-
)
|
|
45
|
-
return applied
|
|
46
|
-
|
|
47
|
-
def _extract_patch_paths(self, patch: str) -> set[str]:
|
|
48
|
-
return set(self._extract_patch_changes(patch))
|
|
49
|
-
|
|
50
|
-
def _extract_patch_changes(self, patch: str) -> dict[str, Literal["create", "modify", "delete"]]:
|
|
51
|
-
"""Extract repository-relative file paths touched by a unified git patch."""
|
|
52
|
-
changes: dict[str, Literal["create", "modify", "delete"]] = {}
|
|
53
|
-
old_path: Optional[str] = None
|
|
54
|
-
old_is_null = False
|
|
55
|
-
for line in patch.splitlines():
|
|
56
|
-
if line.startswith("diff --git "):
|
|
57
|
-
parts = line.split()
|
|
58
|
-
cleaned = self._normalize_patch_path(parts[3]) if len(parts) > 3 else None
|
|
59
|
-
if cleaned:
|
|
60
|
-
changes.setdefault(cleaned, "modify")
|
|
61
|
-
old_path = self._normalize_patch_path(parts[2]) if len(parts) > 2 else None
|
|
62
|
-
old_is_null = False
|
|
63
|
-
continue
|
|
64
|
-
|
|
65
|
-
if line.startswith("--- "):
|
|
66
|
-
raw = line[4:].split("\t", 1)[0].strip()
|
|
67
|
-
old_is_null = raw == "/dev/null"
|
|
68
|
-
old_path = self._normalize_patch_path(raw)
|
|
69
|
-
continue
|
|
70
|
-
|
|
71
|
-
if line.startswith("+++ "):
|
|
72
|
-
raw = line[4:].split("\t", 1)[0].strip()
|
|
73
|
-
if raw == "/dev/null":
|
|
74
|
-
if old_path:
|
|
75
|
-
changes[old_path] = "delete"
|
|
76
|
-
continue
|
|
77
|
-
new_path = self._normalize_patch_path(raw)
|
|
78
|
-
if new_path:
|
|
79
|
-
changes[new_path] = "create" if old_is_null else "modify"
|
|
80
|
-
|
|
81
|
-
if not changes:
|
|
82
|
-
raise ExecutionError("Patch does not declare any affected files.")
|
|
83
|
-
return changes
|
|
84
|
-
|
|
85
|
-
def _normalize_patch_path(self, raw_path: str) -> Optional[str]:
|
|
86
|
-
raw_path = raw_path.strip().strip('"')
|
|
87
|
-
if raw_path == "/dev/null":
|
|
88
|
-
return None
|
|
89
|
-
raw_path = re.sub(r"^[ab]/", "", raw_path)
|
|
90
|
-
return raw_path.replace("\\", "/")
|
|
91
|
-
|
|
92
|
-
def _save_command_log(self, task_id: str, command: str, stream: str, content: str) -> str:
|
|
93
|
-
"""Save command output to a log file and return the path."""
|
|
94
|
-
log_dir = self.project_root / ".devcouncil" / "logs"
|
|
95
|
-
log_dir.mkdir(parents=True, exist_ok=True)
|
|
96
|
-
cmd_hash = hashlib.sha256(command.encode()).hexdigest()[:8]
|
|
97
|
-
filename = f"{task_id}-{cmd_hash}-{stream}.log"
|
|
98
|
-
log_path = log_dir / filename
|
|
99
|
-
log_path.write_text(redact_string(content), encoding="utf-8")
|
|
100
|
-
return str(log_path)
|
|
101
|
-
|
|
102
|
-
def run_command(self, command: str, task: Task) -> CommandResult:
|
|
103
|
-
"""Execute a shell command if allowed by permissions."""
|
|
104
|
-
self.permissions.validate_action("shell", command, task)
|
|
105
|
-
|
|
106
|
-
logger.info(f"Executing authorized command: {command}")
|
|
107
|
-
|
|
108
|
-
try:
|
|
109
|
-
from devcouncil.app.config import load_config
|
|
110
|
-
config = load_config(self.project_root)
|
|
111
|
-
timeout = config.execution.command_timeout
|
|
112
|
-
except Exception:
|
|
113
|
-
timeout = 300
|
|
114
|
-
|
|
115
|
-
try:
|
|
116
|
-
result = subprocess.run(
|
|
117
|
-
shlex.split(command, posix=False),
|
|
118
|
-
shell=False,
|
|
119
|
-
capture_output=True,
|
|
120
|
-
text=True,
|
|
121
|
-
encoding="utf-8",
|
|
122
|
-
errors="replace",
|
|
123
|
-
cwd=self.project_root,
|
|
124
|
-
timeout=timeout
|
|
125
|
-
)
|
|
126
|
-
stdout = result.stdout or ""
|
|
127
|
-
stderr = result.stderr or ""
|
|
128
|
-
stdout_path = self._save_command_log(task.id, command, "stdout", stdout)
|
|
129
|
-
stderr_path = self._save_command_log(task.id, command, "stderr", stderr)
|
|
130
|
-
stdout_summary = redact_string(stdout[-500:])
|
|
131
|
-
stderr_summary = redact_string(stderr[-500:])
|
|
132
|
-
TraceLogger(self.project_root).log_event(
|
|
133
|
-
"command_executed",
|
|
134
|
-
{"command": command, "exit_code": result.returncode},
|
|
135
|
-
task_id=task.id,
|
|
136
|
-
summary=f"{command} exited {result.returncode}",
|
|
137
|
-
)
|
|
138
|
-
return CommandResult(
|
|
139
|
-
command=command,
|
|
140
|
-
exit_code=result.returncode,
|
|
141
|
-
stdout_path=stdout_path,
|
|
142
|
-
stderr_path=stderr_path,
|
|
143
|
-
summary=f"Exit code {result.returncode}. stdout: {stdout_summary}. stderr: {stderr_summary}"
|
|
144
|
-
)
|
|
145
|
-
except Exception as e:
|
|
146
|
-
raise ExecutionError(f"Command execution failed: {e}")
|
|
147
|
-
|
|
148
|
-
def write_file(self, path: str, content: str, task: Task):
|
|
149
|
-
"""Write content to a file if allowed by permissions."""
|
|
150
|
-
self._validate_path_within_root(path)
|
|
151
|
-
full_path = resolve_project_path(self.project_root, path)
|
|
152
|
-
operation = "modify" if full_path.exists() else "create"
|
|
153
|
-
self.permissions.validate_action("file_write", path, task, operation=operation)
|
|
154
|
-
|
|
155
|
-
logger.info(f"Writing authorized file: {path}")
|
|
156
|
-
try:
|
|
157
|
-
full_path.parent.mkdir(parents=True, exist_ok=True)
|
|
158
|
-
full_path.write_text(content, encoding="utf-8")
|
|
159
|
-
TraceLogger(self.project_root).log_event(
|
|
160
|
-
"file_written",
|
|
161
|
-
{"path": path},
|
|
162
|
-
task_id=task.id,
|
|
163
|
-
summary=f"Wrote {path}",
|
|
164
|
-
)
|
|
165
|
-
except Exception as e:
|
|
166
|
-
raise ExecutionError(f"Failed to write file {path}: {e}")
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
import shutil
|
|
2
|
-
import subprocess
|
|
3
|
-
import os
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
|
|
6
|
-
from rich.console import Console
|
|
7
|
-
|
|
8
|
-
from devcouncil.domain.requirement import Requirement
|
|
9
|
-
from devcouncil.domain.task import Task
|
|
10
|
-
from devcouncil.execution.executor import Executor, ExecutionResult
|
|
11
|
-
from devcouncil.execution.prompt_builder import PromptBuilder
|
|
12
|
-
|
|
13
|
-
console = Console()
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class CodingCliExecutor(Executor):
|
|
17
|
-
"""Execute a DevCouncil task by handing it off to an external coding CLI."""
|
|
18
|
-
|
|
19
|
-
_ALIASES = {
|
|
20
|
-
"codex-cli": "codex",
|
|
21
|
-
"gemini-cli": "gemini",
|
|
22
|
-
"claude-cli": "claude",
|
|
23
|
-
"claude-code": "claude",
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
def __init__(self, project_root: Path, client: str, timeout_seconds: int = 1800):
|
|
27
|
-
self.client = self._normalize_client(client)
|
|
28
|
-
self.project_root = project_root
|
|
29
|
-
self.timeout_seconds = timeout_seconds
|
|
30
|
-
|
|
31
|
-
def _normalize_client(self, client: str) -> str:
|
|
32
|
-
normalized = (client or "").strip().lower().replace("_", "-")
|
|
33
|
-
return self._ALIASES.get(normalized, normalized)
|
|
34
|
-
|
|
35
|
-
def _command(self) -> list[str]:
|
|
36
|
-
if self.client == "codex":
|
|
37
|
-
return ["codex", "exec", "-"]
|
|
38
|
-
if self.client == "gemini":
|
|
39
|
-
return ["gemini"]
|
|
40
|
-
if self.client == "claude":
|
|
41
|
-
return ["claude", "-p"]
|
|
42
|
-
raise ValueError(f"Unsupported coding CLI client: {self.client}")
|
|
43
|
-
|
|
44
|
-
def run_task(self, task: Task, requirements: list[Requirement]) -> ExecutionResult:
|
|
45
|
-
try:
|
|
46
|
-
command = self._command()
|
|
47
|
-
except ValueError as exc:
|
|
48
|
-
return ExecutionResult(success=False, message=str(exc))
|
|
49
|
-
|
|
50
|
-
executable = command[0]
|
|
51
|
-
if not shutil.which(executable):
|
|
52
|
-
return ExecutionResult(
|
|
53
|
-
success=False,
|
|
54
|
-
message=f"{self.client} CLI is not installed or not on PATH.",
|
|
55
|
-
)
|
|
56
|
-
|
|
57
|
-
prompt = PromptBuilder(self.project_root).build_task_prompt(task, requirements)
|
|
58
|
-
instruction_file = self.project_root / ".devcouncil" / f"{task.id}-{self.client}-task.md"
|
|
59
|
-
instruction_file.parent.mkdir(parents=True, exist_ok=True)
|
|
60
|
-
instruction_file.write_text(prompt, encoding="utf-8")
|
|
61
|
-
|
|
62
|
-
env = {**dict(os.environ), "DEVCOUNCIL_PROJECT_ROOT": str(self.project_root)}
|
|
63
|
-
log_prefix = f"{task.id}-{self.client}"
|
|
64
|
-
|
|
65
|
-
console.print(f"Starting [bold]{self.client.upper()}[/bold] for task [bold]{task.id}[/bold]...")
|
|
66
|
-
console.print(f"Task prompt: [dim]{instruction_file}[/dim]")
|
|
67
|
-
console.print(f"Command: [dim]{' '.join(command)}[/dim]")
|
|
68
|
-
|
|
69
|
-
try:
|
|
70
|
-
result = subprocess.run(
|
|
71
|
-
command,
|
|
72
|
-
input=prompt,
|
|
73
|
-
capture_output=True,
|
|
74
|
-
text=True,
|
|
75
|
-
encoding="utf-8",
|
|
76
|
-
errors="replace",
|
|
77
|
-
cwd=self.project_root,
|
|
78
|
-
env=env,
|
|
79
|
-
timeout=self.timeout_seconds,
|
|
80
|
-
)
|
|
81
|
-
self._write_log(log_prefix, result)
|
|
82
|
-
if result.returncode != 0:
|
|
83
|
-
stderr_preview = (result.stderr or result.stdout or "").strip().splitlines()[:5]
|
|
84
|
-
detail = stderr_preview[0] if stderr_preview else "No diagnostics were produced."
|
|
85
|
-
return ExecutionResult(
|
|
86
|
-
success=False,
|
|
87
|
-
message=f"{self.client} exited with code {result.returncode}: {detail}",
|
|
88
|
-
)
|
|
89
|
-
return ExecutionResult(success=True, message=f"{self.client} execution finished.")
|
|
90
|
-
except subprocess.TimeoutExpired as exc:
|
|
91
|
-
_ = exc
|
|
92
|
-
return ExecutionResult(
|
|
93
|
-
success=False,
|
|
94
|
-
message=f"{self.client} execution timed out after {self.timeout_seconds}s.",
|
|
95
|
-
)
|
|
96
|
-
except Exception as exc:
|
|
97
|
-
return ExecutionResult(success=False, message=str(exc))
|
|
98
|
-
|
|
99
|
-
def _write_log(self, task_client: str, result: subprocess.CompletedProcess[str]) -> None:
|
|
100
|
-
log_dir = self.project_root / ".devcouncil" / "logs"
|
|
101
|
-
log_dir.mkdir(parents=True, exist_ok=True)
|
|
102
|
-
log_path = log_dir / f"{task_client}.log"
|
|
103
|
-
log_path.write_text(
|
|
104
|
-
"\n".join([
|
|
105
|
-
f"command_returncode={result.returncode}",
|
|
106
|
-
"=== stdout ===",
|
|
107
|
-
result.stdout or "",
|
|
108
|
-
"=== stderr ===",
|
|
109
|
-
result.stderr or "",
|
|
110
|
-
]),
|
|
111
|
-
encoding="utf-8",
|
|
112
|
-
)
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import subprocess
|
|
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:
|
|
17
|
-
builder = PromptBuilder(self.project_root)
|
|
18
|
-
task_prompt = builder.build_task_prompt(task, requirements)
|
|
19
|
-
|
|
20
|
-
# Write temporary instruction file for mini-SWE-agent
|
|
21
|
-
instruction_file = self.project_root / ".devcouncil" / f"{task.id}-mini-swe-task.md"
|
|
22
|
-
instruction_file.parent.mkdir(parents=True, exist_ok=True)
|
|
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 = [
|
|
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
|
-
)
|
|
File without changes
|
|
@@ -1,107 +0,0 @@
|
|
|
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
|
|
8
|
-
from devcouncil.execution.task_runner import TaskRunner
|
|
9
|
-
from devcouncil.execution.context_builder import ContextBuilder
|
|
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:
|
|
41
|
-
- read_file(path: str)
|
|
42
|
-
- list_files()
|
|
43
|
-
- apply_patch(patch: str)
|
|
44
|
-
- run_command(command: str)
|
|
45
|
-
|
|
46
|
-
Rules:
|
|
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:
|
|
74
|
-
if tool_call.tool == "read_file":
|
|
75
|
-
path = tool_call.args["path"]
|
|
76
|
-
resolved = resolve_project_path(self.task_runner.project_root, path)
|
|
77
|
-
# Security: block reading sensitive files
|
|
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."
|
|
90
|
-
elif tool_call.tool == "write_file":
|
|
91
|
-
raise PermissionError("write_file is disabled for the native executor; use apply_patch.")
|
|
92
|
-
elif tool_call.tool == "apply_patch":
|
|
93
|
-
self.task_runner.apply_patch(tool_call.args["patch"], task)
|
|
94
|
-
result_summary = "Successfully applied patch."
|
|
95
|
-
elif tool_call.tool == "run_command":
|
|
96
|
-
cmd_result = self.task_runner.run_command(tool_call.args["command"], task)
|
|
97
|
-
result_summary = f"Command finished with exit code {cmd_result.exit_code}."
|
|
98
|
-
else:
|
|
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")
|
|
@@ -1,71 +0,0 @@
|
|
|
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
|
-
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
|
-
|
|
21
|
-
# OpenHands often expects a workspace mount and an instruction.
|
|
22
|
-
# Keep the full prompt out of argv so Windows command-line limits and
|
|
23
|
-
# terminal logs do not become part of the execution boundary.
|
|
24
|
-
# Reference: https://github.com/All-Hands-AI/OpenHands
|
|
25
|
-
instruction_file = self.project_root / ".devcouncil" / f"{task.id}-openhands-task.md"
|
|
26
|
-
instruction_file.parent.mkdir(parents=True, exist_ok=True)
|
|
27
|
-
instruction_file.write_text(task_prompt, encoding="utf-8")
|
|
28
|
-
|
|
29
|
-
cmd = [
|
|
30
|
-
"openhands", "run",
|
|
31
|
-
"--workspace-base", str(self.project_root),
|
|
32
|
-
"--task-file", str(instruction_file),
|
|
33
|
-
"--headless"
|
|
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
|
-
)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
|
|
File without changes
|
|
@@ -1,50 +0,0 @@
|
|
|
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 []
|