devcouncil 0.2.0 → 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 +191 -253
- package/bin/devcouncil.js +130 -32
- package/package.json +6 -17
- package/pyproject.toml +0 -66
- 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 -320
- 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 -163
- 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/assets/__init__.py +0 -1
- package/src/devcouncil/assets/devcouncil-logo.svg +0 -60
- package/src/devcouncil/assets/devcouncil_logo_premium.png +0 -0
- package/src/devcouncil/cli/__init__.py +0 -0
- package/src/devcouncil/cli/commands/__init__.py +0 -0
- package/src/devcouncil/cli/commands/agents.py +0 -292
- package/src/devcouncil/cli/commands/artifacts.py +0 -54
- package/src/devcouncil/cli/commands/ast.py +0 -22
- package/src/devcouncil/cli/commands/baseline.py +0 -35
- package/src/devcouncil/cli/commands/check.py +0 -209
- package/src/devcouncil/cli/commands/config.py +0 -115
- package/src/devcouncil/cli/commands/cost.py +0 -57
- package/src/devcouncil/cli/commands/dashboard.py +0 -31
- package/src/devcouncil/cli/commands/doctor.py +0 -340
- package/src/devcouncil/cli/commands/evidence.py +0 -48
- package/src/devcouncil/cli/commands/go.py +0 -656
- package/src/devcouncil/cli/commands/handoff.py +0 -69
- package/src/devcouncil/cli/commands/hook.py +0 -237
- package/src/devcouncil/cli/commands/init.py +0 -289
- package/src/devcouncil/cli/commands/integrate.py +0 -1515
- package/src/devcouncil/cli/commands/lsp.py +0 -20
- package/src/devcouncil/cli/commands/map.py +0 -112
- package/src/devcouncil/cli/commands/mcp_server.py +0 -18
- package/src/devcouncil/cli/commands/plan.py +0 -488
- package/src/devcouncil/cli/commands/prompt.py +0 -61
- package/src/devcouncil/cli/commands/repair.py +0 -89
- package/src/devcouncil/cli/commands/report.py +0 -137
- package/src/devcouncil/cli/commands/reset_demo_state.py +0 -33
- package/src/devcouncil/cli/commands/rollback.py +0 -59
- package/src/devcouncil/cli/commands/run.py +0 -289
- package/src/devcouncil/cli/commands/runs.py +0 -223
- package/src/devcouncil/cli/commands/scaffold.py +0 -32
- package/src/devcouncil/cli/commands/semantic.py +0 -47
- package/src/devcouncil/cli/commands/setup.py +0 -362
- package/src/devcouncil/cli/commands/shell.py +0 -73
- package/src/devcouncil/cli/commands/show.py +0 -76
- package/src/devcouncil/cli/commands/skills.py +0 -88
- package/src/devcouncil/cli/commands/status.py +0 -141
- package/src/devcouncil/cli/commands/tasks.py +0 -55
- package/src/devcouncil/cli/commands/trace.py +0 -88
- package/src/devcouncil/cli/commands/verify.py +0 -328
- package/src/devcouncil/cli/commands/version.py +0 -20
- package/src/devcouncil/cli/commands/watch.py +0 -574
- package/src/devcouncil/cli/commands/watch_fs.py +0 -40
- package/src/devcouncil/cli/main.py +0 -137
- 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 -47
- package/src/devcouncil/domain/gap.py +0 -52
- package/src/devcouncil/domain/requirement.py +0 -22
- package/src/devcouncil/domain/task.py +0 -55
- package/src/devcouncil/execution/__init__.py +0 -1
- package/src/devcouncil/execution/checkpoints.py +0 -246
- package/src/devcouncil/execution/context_builder.py +0 -60
- package/src/devcouncil/execution/executor.py +0 -15
- package/src/devcouncil/execution/fs_watcher.py +0 -180
- package/src/devcouncil/execution/handoff.py +0 -102
- package/src/devcouncil/execution/hook_policy.py +0 -253
- package/src/devcouncil/execution/patch.py +0 -77
- package/src/devcouncil/execution/paths.py +0 -14
- package/src/devcouncil/execution/permissions.py +0 -85
- package/src/devcouncil/execution/policy_engine.py +0 -343
- package/src/devcouncil/execution/prompt_builder.py +0 -671
- package/src/devcouncil/execution/shell_session.py +0 -225
- package/src/devcouncil/execution/task_runner.py +0 -170
- package/src/devcouncil/executors/__init__.py +0 -1
- package/src/devcouncil/executors/agent_registry.py +0 -575
- package/src/devcouncil/executors/coding_cli.py +0 -736
- 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 -208
- 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 -52
- 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 -53
- package/src/devcouncil/gating/policy.py +0 -338
- package/src/devcouncil/hardware.py +0 -184
- 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 -161
- package/src/devcouncil/indexing/repo_mapper.py +0 -1455
- package/src/devcouncil/indexing/semantic_index.py +0 -205
- package/src/devcouncil/integrations/actions.py +0 -146
- package/src/devcouncil/integrations/check.py +0 -423
- package/src/devcouncil/integrations/code_review_graph.py +0 -163
- package/src/devcouncil/integrations/github.py +0 -39
- package/src/devcouncil/integrations/github_intent.py +0 -142
- package/src/devcouncil/integrations/gitnexus.py +0 -62
- 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 -2122
- package/src/devcouncil/integrations/opencode_devcouncil_plugin.mjs +0 -24
- package/src/devcouncil/integrations/pr_comments.py +0 -62
- package/src/devcouncil/live/__init__.py +0 -2
- package/src/devcouncil/live/cards.py +0 -349
- 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 -141
- package/src/devcouncil/llm/__init__.py +0 -1
- package/src/devcouncil/llm/cache.py +0 -42
- package/src/devcouncil/llm/model_defaults.yaml +0 -44
- package/src/devcouncil/llm/provider.py +0 -627
- package/src/devcouncil/llm/router.py +0 -310
- package/src/devcouncil/optimization/__init__.py +0 -1
- package/src/devcouncil/optimization/gepa_agent.py +0 -318
- package/src/devcouncil/planning/__init__.py +0 -1
- package/src/devcouncil/planning/arbiter_service.py +0 -57
- package/src/devcouncil/planning/correction_manifest.py +0 -303
- package/src/devcouncil/planning/critique_service.py +0 -71
- package/src/devcouncil/planning/plan_service.py +0 -60
- package/src/devcouncil/planning/prompt_enhancer_service.py +0 -167
- package/src/devcouncil/planning/repair_service.py +0 -39
- package/src/devcouncil/planning/spec_service.py +0 -70
- package/src/devcouncil/repo/__init__.py +0 -0
- package/src/devcouncil/repo/ci_scaffold.py +0 -157
- package/src/devcouncil/repo/gitignore.py +0 -123
- package/src/devcouncil/repo/sca.py +0 -374
- 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 -30
- package/src/devcouncil/reporting/markdown_report.py +0 -83
- package/src/devcouncil/reporting/report_builder.py +0 -14
- package/src/devcouncil/skills/__init__.py +0 -19
- package/src/devcouncil/skills/library/README.md +0 -46
- package/src/devcouncil/skills/library/ai-training.md +0 -50
- package/src/devcouncil/skills/library/android.md +0 -50
- package/src/devcouncil/skills/library/backend.md +0 -52
- package/src/devcouncil/skills/library/core-engineering.md +0 -95
- package/src/devcouncil/skills/library/data-engineering.md +0 -47
- package/src/devcouncil/skills/library/desktop.md +0 -46
- package/src/devcouncil/skills/library/devops.md +0 -48
- package/src/devcouncil/skills/library/game-dev.md +0 -46
- package/src/devcouncil/skills/library/ios.md +0 -48
- package/src/devcouncil/skills/library/mobile-cross-platform.md +0 -46
- package/src/devcouncil/skills/library/security.md +0 -48
- package/src/devcouncil/skills/library/systems.md +0 -48
- package/src/devcouncil/skills/library/web.md +0 -47
- package/src/devcouncil/skills/library/windows.md +0 -47
- package/src/devcouncil/skills/registry.py +0 -330
- package/src/devcouncil/storage/__init__.py +0 -0
- package/src/devcouncil/storage/db.py +0 -147
- package/src/devcouncil/storage/models.py +0 -204
- package/src/devcouncil/storage/native.py +0 -557
- package/src/devcouncil/storage/repositories.py +0 -485
- package/src/devcouncil/telemetry/__init__.py +0 -0
- package/src/devcouncil/telemetry/cost.py +0 -140
- package/src/devcouncil/telemetry/model_pricing.yaml +0 -48
- package/src/devcouncil/telemetry/pricing.py +0 -28
- package/src/devcouncil/telemetry/traces.py +0 -146
- package/src/devcouncil/telemetry/tracker.py +0 -52
- package/src/devcouncil/ui/__init__.py +0 -1
- package/src/devcouncil/ui/dashboard.py +0 -423
- package/src/devcouncil/utils/__init__.py +0 -1
- package/src/devcouncil/utils/redaction.py +0 -147
- package/src/devcouncil/utils/subprocess_env.py +0 -69
- package/src/devcouncil/verification/__init__.py +0 -1
- package/src/devcouncil/verification/acceptance_compiler.py +0 -125
- package/src/devcouncil/verification/ad_hoc_check.py +0 -129
- package/src/devcouncil/verification/diff_coverage.py +0 -353
- package/src/devcouncil/verification/implementation_reviewer.py +0 -55
- package/src/devcouncil/verification/next_actions.py +0 -189
- package/src/devcouncil/verification/sandbox.py +0 -178
- package/src/devcouncil/verification/test_resolver.py +0 -91
- package/src/devcouncil/verification/verifier.py +0 -1548
- package/uv.lock +0 -1226
|
@@ -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,208 +0,0 @@
|
|
|
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
|
|
9
|
-
from devcouncil.execution.task_runner import TaskRunner
|
|
10
|
-
from devcouncil.execution.context_builder import ContextBuilder
|
|
11
|
-
from devcouncil.execution.prompt_builder import PromptBuilder
|
|
12
|
-
from devcouncil.execution.paths import resolve_project_path
|
|
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:
|
|
66
|
-
- read_file(path: str)
|
|
67
|
-
- list_files()
|
|
68
|
-
- apply_patch(patch: str) OR apply_patch(path: str, content: str) as a fallback when a valid unified diff cannot be produced
|
|
69
|
-
- run_command(command: str)
|
|
70
|
-
|
|
71
|
-
Rules:
|
|
72
|
-
1. You can only write to files or apply patches to files listed in the task's 'planned_files'.
|
|
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:
|
|
139
|
-
if tool_call.tool == "read_file":
|
|
140
|
-
path = tool_call.args["path"]
|
|
141
|
-
resolved = resolve_project_path(self.task_runner.project_root, path)
|
|
142
|
-
# Security: block reading sensitive files
|
|
143
|
-
sensitive_patterns = {".env", ".pem", ".key", "credentials", "secrets"}
|
|
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."
|
|
155
|
-
elif tool_call.tool == "write_file":
|
|
156
|
-
raise PermissionError("write_file is disabled for the native executor; use apply_patch.")
|
|
157
|
-
elif tool_call.tool == "apply_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."
|
|
178
|
-
elif tool_call.tool == "run_command":
|
|
179
|
-
cmd_result = self.task_runner.run_command(tool_call.args["command"], task)
|
|
180
|
-
result_summary = f"Command finished with exit code {cmd_result.exit_code}."
|
|
181
|
-
else:
|
|
182
|
-
raise ValueError(f"Unknown tool: {tool_call.tool}")
|
|
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,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,52 +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
|
-
# DevCouncil manages the root .gitignore itself (ensure_gitignore runs on
|
|
13
|
-
# init and before every task), so it must not block execution.
|
|
14
|
-
return path.startswith(".devcouncil/") or path == ".gitignore"
|
|
15
|
-
|
|
16
|
-
def check(self, project_root, task_id: str) -> list[Gap]:
|
|
17
|
-
try:
|
|
18
|
-
status = subprocess.check_output(["git", "status", "--porcelain"], cwd=project_root).decode()
|
|
19
|
-
dirty_lines = [line for line in status.splitlines() if line.strip() and not self._is_runtime_state(line)]
|
|
20
|
-
if dirty_lines:
|
|
21
|
-
return [Gap(
|
|
22
|
-
id=f"GAP-{task_id}-DIRTY-GIT",
|
|
23
|
-
severity="high",
|
|
24
|
-
gap_type="architecture_drift",
|
|
25
|
-
task_id=task_id,
|
|
26
|
-
description="Git working tree is dirty. Execution requires a clean state for checkpointing.",
|
|
27
|
-
recommended_fix="Commit or stash your current changes before running the task.",
|
|
28
|
-
blocking=True
|
|
29
|
-
)]
|
|
30
|
-
except FileNotFoundError:
|
|
31
|
-
logger.error("Git is not installed or not in PATH.")
|
|
32
|
-
return [Gap(
|
|
33
|
-
id=f"GAP-{task_id}-NO-GIT",
|
|
34
|
-
severity="high",
|
|
35
|
-
gap_type="architecture_drift",
|
|
36
|
-
task_id=task_id,
|
|
37
|
-
description="Git is not available. Cannot verify working tree cleanliness.",
|
|
38
|
-
recommended_fix="Install git and ensure it is in your PATH.",
|
|
39
|
-
blocking=True
|
|
40
|
-
)]
|
|
41
|
-
except subprocess.CalledProcessError as e:
|
|
42
|
-
logger.warning("Git status check failed: %s", e)
|
|
43
|
-
return [Gap(
|
|
44
|
-
id=f"GAP-{task_id}-GIT-ERROR",
|
|
45
|
-
severity="medium",
|
|
46
|
-
gap_type="architecture_drift",
|
|
47
|
-
task_id=task_id,
|
|
48
|
-
description=f"Git status check failed: {e}. Directory may not be a git repository.",
|
|
49
|
-
recommended_fix="Initialize a git repository with 'git init' before running tasks.",
|
|
50
|
-
blocking=True
|
|
51
|
-
)]
|
|
52
|
-
return []
|
|
@@ -1,32 +0,0 @@
|
|
|
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,26 +0,0 @@
|
|
|
1
|
-
from devcouncil.domain.requirement import Requirement
|
|
2
|
-
from devcouncil.domain.task import Task
|
|
3
|
-
from devcouncil.domain.gap import Gap
|
|
4
|
-
from typing import List
|
|
5
|
-
|
|
6
|
-
class RequirementCoverageCheck:
|
|
7
|
-
"""Detects requirements that are not mapped to any tasks."""
|
|
8
|
-
|
|
9
|
-
def check(self, requirements: List[Requirement], tasks: List[Task]) -> List[Gap]:
|
|
10
|
-
task_req_ids = set()
|
|
11
|
-
for t in tasks:
|
|
12
|
-
task_req_ids.update(t.requirement_ids)
|
|
13
|
-
|
|
14
|
-
gaps = []
|
|
15
|
-
for req in requirements:
|
|
16
|
-
if req.id not in task_req_ids:
|
|
17
|
-
gaps.append(Gap(
|
|
18
|
-
id=f"GAP-PLAN-{req.id}-UNMAPPED",
|
|
19
|
-
severity="high",
|
|
20
|
-
gap_type="requirement_not_planned",
|
|
21
|
-
requirement_id=req.id,
|
|
22
|
-
description=f"Requirement '{req.title}' is not covered by any task.",
|
|
23
|
-
recommended_fix="Decompose this requirement into one or more implementation tasks.",
|
|
24
|
-
blocking=True
|
|
25
|
-
))
|
|
26
|
-
return gaps
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import re
|
|
2
|
-
from typing import List
|
|
3
|
-
from devcouncil.domain.gap import Gap
|
|
4
|
-
from devcouncil.utils.redaction import SECRET_PATTERNS, redact_string
|
|
5
|
-
|
|
6
|
-
# Captures the new-file starting line from a unified-diff hunk header (@@ -a,b +c,d @@).
|
|
7
|
-
_HUNK_RE = re.compile(r"^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@")
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class SecretScanner:
|
|
11
|
-
"""Scans code diffs for potential secrets (API keys, tokens, etc.)."""
|
|
12
|
-
|
|
13
|
-
def scan_diff(self, diff_content: str, task_id: str) -> List[Gap]:
|
|
14
|
-
gaps: List[Gap] = []
|
|
15
|
-
current_file = "unknown_file"
|
|
16
|
-
new_line_no = 0 # line number in the new file, tracked across hunks
|
|
17
|
-
counter = 0 # ensures unique gap ids
|
|
18
|
-
|
|
19
|
-
for line in diff_content.splitlines():
|
|
20
|
-
if line.startswith("+++ b/"):
|
|
21
|
-
current_file = line[6:]
|
|
22
|
-
continue
|
|
23
|
-
if line.startswith("+++") or line.startswith("---") or line.startswith("diff "):
|
|
24
|
-
continue
|
|
25
|
-
hunk = _HUNK_RE.match(line)
|
|
26
|
-
if hunk:
|
|
27
|
-
new_line_no = int(hunk.group(1))
|
|
28
|
-
continue
|
|
29
|
-
if line.startswith("-"):
|
|
30
|
-
continue # removed line — does not advance the new-file counter
|
|
31
|
-
if line.startswith("+"):
|
|
32
|
-
for key_type, pattern in SECRET_PATTERNS.items():
|
|
33
|
-
if pattern.search(line):
|
|
34
|
-
counter += 1
|
|
35
|
-
gaps.append(Gap(
|
|
36
|
-
id=f"GAP-{task_id}-SECRET-{key_type.upper()}-{new_line_no}-{counter}",
|
|
37
|
-
severity="critical",
|
|
38
|
-
gap_type="security_risk",
|
|
39
|
-
task_id=task_id,
|
|
40
|
-
description=f"Potential {key_type} found in {current_file}:{new_line_no}.",
|
|
41
|
-
evidence=[redact_string(line.strip())],
|
|
42
|
-
recommended_fix="Remove the secret and use environment variables or a secret manager.",
|
|
43
|
-
blocking=True,
|
|
44
|
-
# Populate the routing fields so the security NextAction points
|
|
45
|
-
# the agent straight at the file:line instead of forcing a re-grep.
|
|
46
|
-
file=current_file,
|
|
47
|
-
line=new_line_no,
|
|
48
|
-
))
|
|
49
|
-
new_line_no += 1
|
|
50
|
-
continue
|
|
51
|
-
# Context or blank line — advances the new-file counter.
|
|
52
|
-
new_line_no += 1
|
|
53
|
-
return gaps
|