devcouncil 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +643 -0
- package/bin/devcouncil.js +62 -0
- package/package.json +47 -0
- package/pyproject.toml +31 -0
- package/src/devcouncil/__init__.py +0 -0
- package/src/devcouncil/__main__.py +4 -0
- package/src/devcouncil/app/__init__.py +28 -0
- package/src/devcouncil/app/config.py +131 -0
- package/src/devcouncil/app/errors.py +23 -0
- package/src/devcouncil/app/events.py +44 -0
- package/src/devcouncil/app/orchestrator.py +92 -0
- package/src/devcouncil/app/run_context.py +39 -0
- package/src/devcouncil/app/state_machine.py +108 -0
- package/src/devcouncil/artifacts/__init__.py +1 -0
- package/src/devcouncil/artifacts/coverage.py +96 -0
- package/src/devcouncil/artifacts/graph.py +143 -0
- package/src/devcouncil/artifacts/migrations.py +20 -0
- package/src/devcouncil/artifacts/schemas.py +23 -0
- package/src/devcouncil/artifacts/serializer.py +21 -0
- package/src/devcouncil/artifacts/validators.py +27 -0
- package/src/devcouncil/cli/__init__.py +0 -0
- package/src/devcouncil/cli/commands/__init__.py +0 -0
- package/src/devcouncil/cli/commands/artifacts.py +48 -0
- package/src/devcouncil/cli/commands/baseline.py +32 -0
- package/src/devcouncil/cli/commands/config.py +54 -0
- package/src/devcouncil/cli/commands/doctor.py +96 -0
- package/src/devcouncil/cli/commands/hook.py +61 -0
- package/src/devcouncil/cli/commands/init.py +142 -0
- package/src/devcouncil/cli/commands/integrate.py +420 -0
- package/src/devcouncil/cli/commands/map.py +38 -0
- package/src/devcouncil/cli/commands/mcp_server.py +18 -0
- package/src/devcouncil/cli/commands/plan.py +276 -0
- package/src/devcouncil/cli/commands/prompt.py +47 -0
- package/src/devcouncil/cli/commands/repair.py +69 -0
- package/src/devcouncil/cli/commands/report.py +71 -0
- package/src/devcouncil/cli/commands/reset_demo_state.py +28 -0
- package/src/devcouncil/cli/commands/rollback.py +58 -0
- package/src/devcouncil/cli/commands/run.py +224 -0
- package/src/devcouncil/cli/commands/setup.py +82 -0
- package/src/devcouncil/cli/commands/show.py +57 -0
- package/src/devcouncil/cli/commands/status.py +105 -0
- package/src/devcouncil/cli/commands/tasks.py +41 -0
- package/src/devcouncil/cli/commands/trace.py +43 -0
- package/src/devcouncil/cli/commands/verify.py +163 -0
- package/src/devcouncil/cli/commands/version.py +20 -0
- package/src/devcouncil/cli/main.py +70 -0
- package/src/devcouncil/council/__init__.py +0 -0
- package/src/devcouncil/council/prompts/__init__.py +0 -0
- package/src/devcouncil/council/prompts/arbiter.md +19 -0
- package/src/devcouncil/council/prompts/critic_a.md +10 -0
- package/src/devcouncil/council/prompts/critic_b.md +10 -0
- package/src/devcouncil/council/prompts/implementation_reviewer.md +16 -0
- package/src/devcouncil/council/prompts/planner_a.md +16 -0
- package/src/devcouncil/council/prompts/planner_b.md +16 -0
- package/src/devcouncil/council/prompts/rebuttal.md +10 -0
- package/src/devcouncil/council/prompts/spec_writer.md +12 -0
- package/src/devcouncil/domain/__init__.py +0 -0
- package/src/devcouncil/domain/assumption.py +17 -0
- package/src/devcouncil/domain/critique.py +32 -0
- package/src/devcouncil/domain/evidence.py +27 -0
- package/src/devcouncil/domain/gap.py +26 -0
- package/src/devcouncil/domain/requirement.py +22 -0
- package/src/devcouncil/domain/task.py +26 -0
- package/src/devcouncil/execution/__init__.py +1 -0
- package/src/devcouncil/execution/context_builder.py +60 -0
- package/src/devcouncil/execution/executor.py +15 -0
- package/src/devcouncil/execution/hook_policy.py +144 -0
- package/src/devcouncil/execution/patch.py +28 -0
- package/src/devcouncil/execution/paths.py +14 -0
- package/src/devcouncil/execution/permissions.py +92 -0
- package/src/devcouncil/execution/prompt_builder.py +59 -0
- package/src/devcouncil/execution/task_runner.py +166 -0
- package/src/devcouncil/executors/__init__.py +1 -0
- package/src/devcouncil/executors/mini_swe.py +73 -0
- package/src/devcouncil/executors/native/__init__.py +0 -0
- package/src/devcouncil/executors/native/agent.py +107 -0
- package/src/devcouncil/executors/openhands.py +71 -0
- package/src/devcouncil/gating/__init__.py +1 -0
- package/src/devcouncil/gating/checks/__init__.py +0 -0
- package/src/devcouncil/gating/checks/clean_git.py +45 -0
- package/src/devcouncil/gating/checks/planned_files_check.py +32 -0
- package/src/devcouncil/gating/checks/requirement_coverage.py +26 -0
- package/src/devcouncil/gating/checks/secret_scan_check.py +34 -0
- package/src/devcouncil/gating/policy.py +190 -0
- package/src/devcouncil/indexing/__init__.py +1 -0
- package/src/devcouncil/indexing/graph_index.py +48 -0
- package/src/devcouncil/indexing/repo_mapper.py +204 -0
- package/src/devcouncil/indexing/symbol_index.py +0 -0
- package/src/devcouncil/integrations/code_review_graph.py +163 -0
- package/src/devcouncil/integrations/github.py +39 -0
- package/src/devcouncil/integrations/gitnexus.py +27 -0
- package/src/devcouncil/integrations/graphify.py +34 -0
- package/src/devcouncil/integrations/mcp/__init__.py +0 -0
- package/src/devcouncil/integrations/mcp/server.py +146 -0
- package/src/devcouncil/llm/__init__.py +1 -0
- package/src/devcouncil/llm/cache.py +38 -0
- package/src/devcouncil/llm/provider.py +125 -0
- package/src/devcouncil/llm/router.py +125 -0
- package/src/devcouncil/planning/__init__.py +1 -0
- package/src/devcouncil/planning/arbiter_service.py +57 -0
- package/src/devcouncil/planning/critique_service.py +66 -0
- package/src/devcouncil/planning/plan_service.py +46 -0
- package/src/devcouncil/planning/repair_service.py +39 -0
- package/src/devcouncil/planning/spec_service.py +44 -0
- package/src/devcouncil/repo/__init__.py +0 -0
- package/src/devcouncil/reporting/__init__.py +0 -0
- package/src/devcouncil/reporting/github_check.py +32 -0
- package/src/devcouncil/reporting/json_report.py +17 -0
- package/src/devcouncil/reporting/markdown_report.py +46 -0
- package/src/devcouncil/reporting/report_builder.py +14 -0
- package/src/devcouncil/storage/__init__.py +0 -0
- package/src/devcouncil/storage/db.py +66 -0
- package/src/devcouncil/storage/models.py +83 -0
- package/src/devcouncil/storage/repositories.py +346 -0
- package/src/devcouncil/telemetry/__init__.py +0 -0
- package/src/devcouncil/telemetry/cost.py +34 -0
- package/src/devcouncil/telemetry/traces.py +91 -0
- package/src/devcouncil/telemetry/tracker.py +49 -0
- package/src/devcouncil/utils/__init__.py +1 -0
- package/src/devcouncil/utils/redaction.py +141 -0
- package/src/devcouncil/verification/__init__.py +1 -0
- package/src/devcouncil/verification/implementation_reviewer.py +55 -0
- package/src/devcouncil/verification/verifier.py +513 -0
- package/uv.lock +1085 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
import asyncio
|
|
3
|
+
from rich.console import Console
|
|
4
|
+
from rich.table import Table
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
from devcouncil.storage.db import get_db
|
|
8
|
+
from devcouncil.storage.repositories import TaskRepository, RequirementRepository, GapRepository, EvidenceRepository, StateRepository
|
|
9
|
+
from devcouncil.verification.verifier import Verifier
|
|
10
|
+
from devcouncil.llm.provider import OpenRouterProvider
|
|
11
|
+
from devcouncil.llm.router import ModelRouter
|
|
12
|
+
from devcouncil.domain.evidence import CommandResult, DiffEvidence, TestEvidence
|
|
13
|
+
from devcouncil.app.config import load_config, get_api_key
|
|
14
|
+
from devcouncil.app.state_machine import ProjectPhase
|
|
15
|
+
from devcouncil.integrations.code_review_graph import CodeReviewGraphAdapter
|
|
16
|
+
from devcouncil.telemetry.traces import TraceLogger
|
|
17
|
+
|
|
18
|
+
console = Console()
|
|
19
|
+
MAX_RENDERED_GAPS = 20
|
|
20
|
+
|
|
21
|
+
def verify(
|
|
22
|
+
task_id: Optional[str] = typer.Argument(None, help="Optional ID of the task to verify"),
|
|
23
|
+
):
|
|
24
|
+
"""
|
|
25
|
+
Verify one task, or all tasks when TASK_ID is omitted.
|
|
26
|
+
"""
|
|
27
|
+
db = get_db()
|
|
28
|
+
if not db:
|
|
29
|
+
console.print("[red]DevCouncil not initialized. Run 'dev init' first.[/red]")
|
|
30
|
+
return
|
|
31
|
+
|
|
32
|
+
with db.get_session() as session:
|
|
33
|
+
task_repo = TaskRepository(session)
|
|
34
|
+
req_repo = RequirementRepository(session)
|
|
35
|
+
gap_repo = GapRepository(session)
|
|
36
|
+
evidence_repo = EvidenceRepository(session)
|
|
37
|
+
|
|
38
|
+
tasks = [task_repo.get_by_id(task_id)] if task_id else task_repo.get_all()
|
|
39
|
+
tasks = [task for task in tasks if task is not None]
|
|
40
|
+
if not tasks:
|
|
41
|
+
missing = f"Task {task_id} not found." if task_id else "No tasks found to verify."
|
|
42
|
+
console.print(f"[red]{missing}[/red]")
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
reqs = req_repo.get_all()
|
|
46
|
+
|
|
47
|
+
# Load router for LLM review if possible
|
|
48
|
+
router = None
|
|
49
|
+
try:
|
|
50
|
+
config = load_config(Path("."))
|
|
51
|
+
api_key = get_api_key(config.models.provider)
|
|
52
|
+
provider = OpenRouterProvider(api_key)
|
|
53
|
+
role_config = {name: role.model_dump() for name, role in config.models.roles.items()}
|
|
54
|
+
router = ModelRouter(provider, role_config)
|
|
55
|
+
except Exception:
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
verifier = Verifier(Path("."), router=router)
|
|
59
|
+
total_gaps = 0
|
|
60
|
+
blocked_tasks = 0
|
|
61
|
+
|
|
62
|
+
for task in tasks:
|
|
63
|
+
TraceLogger(Path(".")).log_event(
|
|
64
|
+
"task_verification_started",
|
|
65
|
+
{"task_id": task.id},
|
|
66
|
+
task_id=task.id,
|
|
67
|
+
summary=f"Verifying {task.id}",
|
|
68
|
+
)
|
|
69
|
+
graph_context = CodeReviewGraphAdapter(Path(".")).get_context(
|
|
70
|
+
[planned.path for planned in task.planned_files]
|
|
71
|
+
)
|
|
72
|
+
if graph_context.available:
|
|
73
|
+
TraceLogger(Path(".")).log_event(
|
|
74
|
+
"graph_context_loaded",
|
|
75
|
+
graph_context.model_dump(),
|
|
76
|
+
task_id=task.id,
|
|
77
|
+
summary=f"Loaded graph context for {task.id}",
|
|
78
|
+
)
|
|
79
|
+
StateRepository(session).record_phase(ProjectPhase.TASK_VERIFYING.value)
|
|
80
|
+
gap_repo.delete_for_task(task.id)
|
|
81
|
+
evidence_repo.delete_for_task(task.id)
|
|
82
|
+
|
|
83
|
+
gaps, evidence = asyncio.run(verifier.verify_task(task, reqs))
|
|
84
|
+
total_gaps += len(gaps)
|
|
85
|
+
|
|
86
|
+
for gap in gaps:
|
|
87
|
+
gap_repo.save(gap)
|
|
88
|
+
|
|
89
|
+
for ev in evidence:
|
|
90
|
+
if isinstance(ev, CommandResult):
|
|
91
|
+
evidence_repo.save_command_result(task.id, ev)
|
|
92
|
+
elif isinstance(ev, DiffEvidence):
|
|
93
|
+
evidence_repo.save_diff_evidence(ev)
|
|
94
|
+
elif isinstance(ev, TestEvidence):
|
|
95
|
+
evidence_repo.save_test_evidence(ev, task.id)
|
|
96
|
+
|
|
97
|
+
_print_task_result(task.id, gaps)
|
|
98
|
+
|
|
99
|
+
if any(gap.blocking for gap in gaps):
|
|
100
|
+
task.status = "blocked"
|
|
101
|
+
blocked_tasks += 1
|
|
102
|
+
TraceLogger(Path(".")).log_event(
|
|
103
|
+
"gate_failed",
|
|
104
|
+
{"task_id": task.id, "gap_count": len(gaps)},
|
|
105
|
+
task_id=task.id,
|
|
106
|
+
summary=f"{task.id} blocked with {len(gaps)} gap(s)",
|
|
107
|
+
)
|
|
108
|
+
else:
|
|
109
|
+
task.status = "verified"
|
|
110
|
+
TraceLogger(Path(".")).log_event(
|
|
111
|
+
"task_verified",
|
|
112
|
+
{"task_id": task.id, "gap_count": len(gaps)},
|
|
113
|
+
task_id=task.id,
|
|
114
|
+
summary=f"{task.id} verified",
|
|
115
|
+
)
|
|
116
|
+
task_repo.save(task)
|
|
117
|
+
|
|
118
|
+
StateRepository(session).record_phase(
|
|
119
|
+
ProjectPhase.TASK_BLOCKED.value if blocked_tasks else ProjectPhase.TASK_VERIFIED.value
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
if len(tasks) > 1:
|
|
123
|
+
if blocked_tasks:
|
|
124
|
+
console.print(
|
|
125
|
+
f"\n[yellow]Verified {len(tasks)} tasks: {blocked_tasks} blocked, "
|
|
126
|
+
f"{total_gaps} total gap(s).[/yellow]"
|
|
127
|
+
)
|
|
128
|
+
else:
|
|
129
|
+
console.print(f"\n[green]Verified {len(tasks)} tasks successfully.[/green]")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _print_task_result(task_id: str, gaps):
|
|
133
|
+
if not gaps:
|
|
134
|
+
console.print(f"[green]Task {task_id} verified successfully! No gaps found.[/green]")
|
|
135
|
+
return
|
|
136
|
+
|
|
137
|
+
console.print(f"[yellow]Verification finished for task {task_id} with {len(gaps)} gaps:[/yellow]")
|
|
138
|
+
|
|
139
|
+
table = Table(title="Detected Gaps")
|
|
140
|
+
table.add_column("ID", style="cyan")
|
|
141
|
+
table.add_column("Severity", style="magenta")
|
|
142
|
+
table.add_column("Description", style="white")
|
|
143
|
+
table.add_column("Blocking", style="red")
|
|
144
|
+
|
|
145
|
+
for gap in gaps[:MAX_RENDERED_GAPS]:
|
|
146
|
+
table.add_row(
|
|
147
|
+
gap.id,
|
|
148
|
+
gap.severity,
|
|
149
|
+
gap.description,
|
|
150
|
+
"YES" if gap.blocking else "NO",
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
console.print(table)
|
|
154
|
+
if len(gaps) > MAX_RENDERED_GAPS:
|
|
155
|
+
console.print(
|
|
156
|
+
f"[yellow]Showing first {MAX_RENDERED_GAPS} of {len(gaps)} gaps. "
|
|
157
|
+
"Run [bold]dev report --json[/bold] for the full list.[/yellow]"
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
if any(gap.blocking for gap in gaps):
|
|
161
|
+
console.print(f"\n[red]Task {task_id} is BLOCKED due to critical gaps.[/red]")
|
|
162
|
+
else:
|
|
163
|
+
console.print(f"\n[green]Task {task_id} passed with non-blocking gaps.[/green]")
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from rich.console import Console
|
|
3
|
+
import importlib.metadata
|
|
4
|
+
|
|
5
|
+
app = typer.Typer()
|
|
6
|
+
console = Console()
|
|
7
|
+
|
|
8
|
+
@app.callback(invoke_without_command=True)
|
|
9
|
+
def version(ctx: typer.Context):
|
|
10
|
+
"""
|
|
11
|
+
Display the current version of DevCouncil.
|
|
12
|
+
"""
|
|
13
|
+
if ctx.invoked_subcommand is not None:
|
|
14
|
+
return
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
ver = importlib.metadata.version("devcouncil")
|
|
18
|
+
console.print(f"DevCouncil version: [bold cyan]{ver}[/bold cyan]")
|
|
19
|
+
except importlib.metadata.PackageNotFoundError:
|
|
20
|
+
console.print("DevCouncil version: [yellow]unknown (editable/uninstalled)[/yellow]")
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from devcouncil.cli.commands import (
|
|
3
|
+
artifacts,
|
|
4
|
+
baseline,
|
|
5
|
+
config,
|
|
6
|
+
doctor,
|
|
7
|
+
hook,
|
|
8
|
+
init,
|
|
9
|
+
integrate,
|
|
10
|
+
map,
|
|
11
|
+
mcp_server,
|
|
12
|
+
plan,
|
|
13
|
+
prompt,
|
|
14
|
+
repair,
|
|
15
|
+
report,
|
|
16
|
+
reset_demo_state,
|
|
17
|
+
rollback,
|
|
18
|
+
run,
|
|
19
|
+
setup,
|
|
20
|
+
show,
|
|
21
|
+
status,
|
|
22
|
+
tasks,
|
|
23
|
+
trace,
|
|
24
|
+
verify,
|
|
25
|
+
version,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
app = typer.Typer(
|
|
29
|
+
name="dev",
|
|
30
|
+
help="DevCouncil: Gated orchestrator for AI-assisted software development.",
|
|
31
|
+
add_completion=False,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# Typer subcommands (those using app = Typer())
|
|
35
|
+
app.add_typer(init.app, name="init")
|
|
36
|
+
app.add_typer(doctor.app, name="doctor")
|
|
37
|
+
app.add_typer(prompt.app, name="prompt")
|
|
38
|
+
app.add_typer(tasks.app, name="tasks")
|
|
39
|
+
app.add_typer(show.app, name="show")
|
|
40
|
+
app.add_typer(report.app, name="report")
|
|
41
|
+
app.add_typer(rollback.app, name="rollback")
|
|
42
|
+
app.add_typer(config.app, name="config")
|
|
43
|
+
app.add_typer(artifacts.app, name="artifacts")
|
|
44
|
+
app.add_typer(hook.app, name="hook")
|
|
45
|
+
app.add_typer(version.app, name="version")
|
|
46
|
+
app.add_typer(mcp_server.app, name="mcp-server")
|
|
47
|
+
app.add_typer(integrate.app, name="integrate")
|
|
48
|
+
app.add_typer(integrate.app, name="integrations")
|
|
49
|
+
app.add_typer(trace.app, name="trace")
|
|
50
|
+
app.add_typer(setup.app, name="setup")
|
|
51
|
+
|
|
52
|
+
# Direct command registrations (those defined as def cmd())
|
|
53
|
+
app.command(name="baseline")(baseline.baseline)
|
|
54
|
+
app.command(name="map")(map.map_repo)
|
|
55
|
+
app.command(name="plan")(plan.plan)
|
|
56
|
+
app.command(name="reset-demo-state")(reset_demo_state.reset_demo_state)
|
|
57
|
+
app.command(name="run")(run.run)
|
|
58
|
+
app.command(name="verify")(verify.verify)
|
|
59
|
+
app.command(name="repair")(repair.repair)
|
|
60
|
+
app.command(name="status")(status.status)
|
|
61
|
+
|
|
62
|
+
@app.callback()
|
|
63
|
+
def main():
|
|
64
|
+
"""
|
|
65
|
+
DevCouncil: Gated orchestrator for AI-assisted software development.
|
|
66
|
+
"""
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
if __name__ == "__main__":
|
|
70
|
+
app()
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Goal: {goal}
|
|
2
|
+
|
|
3
|
+
Initial Requirements:
|
|
4
|
+
{requirements_json}
|
|
5
|
+
|
|
6
|
+
Plan A: {plan_a_json}
|
|
7
|
+
Plan B: {plan_b_json}
|
|
8
|
+
|
|
9
|
+
Critique of Plan B by Critic A: {critique_a_json}
|
|
10
|
+
Critique of Plan A by Critic B: {critique_b_json}
|
|
11
|
+
|
|
12
|
+
Rebuttal of Critic B by Planner A: {rebuttal_a_json}
|
|
13
|
+
Rebuttal of Critic A by Planner B: {rebuttal_b_json}
|
|
14
|
+
|
|
15
|
+
You are the arbiter engineering manager. Your goal is to produce the final, definitive set of requirements and tasks.
|
|
16
|
+
- You do not decide by vibes.
|
|
17
|
+
- High-severity unrefuted findings from critics must be incorporated into the final requirements or tasks.
|
|
18
|
+
- If a planner successfully rebutted a finding, you may skip it.
|
|
19
|
+
- Produce a single, coherent task graph.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Requirements:
|
|
2
|
+
{requirements_json}
|
|
3
|
+
|
|
4
|
+
Target Plan:
|
|
5
|
+
{target_plan_json}
|
|
6
|
+
|
|
7
|
+
You are a hostile staff engineer reviewing another team's implementation plan.
|
|
8
|
+
Find missing requirements, bad assumptions, missing tests, security risks, migration risks, and unverifiable claims.
|
|
9
|
+
Do not praise. Do not rewrite the plan.
|
|
10
|
+
Every finding must include a falsifiable_check.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Requirements:
|
|
2
|
+
{requirements_json}
|
|
3
|
+
|
|
4
|
+
Target Plan:
|
|
5
|
+
{target_plan_json}
|
|
6
|
+
|
|
7
|
+
You are a hostile staff engineer reviewing another team's implementation plan.
|
|
8
|
+
Find missing requirements, bad assumptions, missing tests, security risks, migration risks, and unverifiable claims.
|
|
9
|
+
Do not praise. Do not rewrite the plan.
|
|
10
|
+
Every finding must include a falsifiable_check.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
You are an expert software reviewer. Review the following code changes against the task requirements.
|
|
2
|
+
Task: {task.title}
|
|
3
|
+
Description: {task.description}
|
|
4
|
+
|
|
5
|
+
Requirements:
|
|
6
|
+
{requirements_json}
|
|
7
|
+
|
|
8
|
+
Code Diff:
|
|
9
|
+
{diff}
|
|
10
|
+
|
|
11
|
+
Your task is to identify if the implementation is complete, correct, and follows best practices.
|
|
12
|
+
- Identify missing edge cases.
|
|
13
|
+
- Identify architectural drift.
|
|
14
|
+
- Identify security risks not caught by static scans.
|
|
15
|
+
|
|
16
|
+
Return a JSON object with 'is_satisfactory' and a list of 'findings' (as Gap objects).
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Goal: {goal}
|
|
2
|
+
|
|
3
|
+
Requirements:
|
|
4
|
+
{requirements_json}
|
|
5
|
+
|
|
6
|
+
Repository Map:
|
|
7
|
+
{repo_map_json}
|
|
8
|
+
|
|
9
|
+
Your task is to create a detailed implementation plan.
|
|
10
|
+
- Break down the requirements into atomic implementation tasks.
|
|
11
|
+
- For each task, specify which files will be created or modified.
|
|
12
|
+
- Specify which tests are expected to verify the task.
|
|
13
|
+
- Ensure each task maps back to at least one requirement.
|
|
14
|
+
|
|
15
|
+
Role-specific instructions:
|
|
16
|
+
You are the pragmatic tech lead. Optimize for simplicity and minimal dependencies.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Goal: {goal}
|
|
2
|
+
|
|
3
|
+
Requirements:
|
|
4
|
+
{requirements_json}
|
|
5
|
+
|
|
6
|
+
Repository Map:
|
|
7
|
+
{repo_map_json}
|
|
8
|
+
|
|
9
|
+
Your task is to create a detailed implementation plan.
|
|
10
|
+
- Break down the requirements into atomic implementation tasks.
|
|
11
|
+
- For each task, specify which files will be created or modified.
|
|
12
|
+
- Specify which tests are expected to verify the task.
|
|
13
|
+
- Ensure each task maps back to at least one requirement.
|
|
14
|
+
|
|
15
|
+
Role-specific instructions:
|
|
16
|
+
You are the production-readiness architect. Optimize for security, performance, edge cases, failure modes, and maintainability. Assume the first plan will miss subtle requirements.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Original Plan:
|
|
2
|
+
{original_plan_json}
|
|
3
|
+
|
|
4
|
+
Critique Findings:
|
|
5
|
+
{findings_json}
|
|
6
|
+
|
|
7
|
+
You are the planner who created the original plan. Review the critique findings.
|
|
8
|
+
- A finding can be rejected only with artifact evidence or strong justification.
|
|
9
|
+
- A finding can be accepted and converted into a requirement/task/test.
|
|
10
|
+
- No hand-wavy rebuttals.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Goal: {goal}
|
|
2
|
+
|
|
3
|
+
Repository Map:
|
|
4
|
+
{repo_map_json}
|
|
5
|
+
|
|
6
|
+
Your task is to draft the initial software specification for this goal.
|
|
7
|
+
1. Identify functional and non-functional requirements.
|
|
8
|
+
2. Extract any assumptions you are making about the codebase or architecture.
|
|
9
|
+
3. List any blocking questions that the user must answer before implementation can proceed.
|
|
10
|
+
|
|
11
|
+
Each requirement MUST have clear acceptance criteria with verification methods.
|
|
12
|
+
Each assumption MUST have a confidence and impact level.
|
|
File without changes
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from pydantic import BaseModel, Field
|
|
2
|
+
from typing import Literal, List
|
|
3
|
+
|
|
4
|
+
class Assumption(BaseModel):
|
|
5
|
+
id: str
|
|
6
|
+
statement: str
|
|
7
|
+
confidence: Literal["low", "medium", "high"]
|
|
8
|
+
impact: Literal["low", "medium", "high"]
|
|
9
|
+
reversible: bool
|
|
10
|
+
requires_user_confirmation: bool
|
|
11
|
+
linked_requirement_ids: List[str] = Field(default_factory=list)
|
|
12
|
+
status: Literal[
|
|
13
|
+
"open",
|
|
14
|
+
"confirmed",
|
|
15
|
+
"rejected",
|
|
16
|
+
"converted_to_requirement"
|
|
17
|
+
] = "open"
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from pydantic import BaseModel
|
|
2
|
+
from typing import Literal, Optional
|
|
3
|
+
|
|
4
|
+
class CritiqueFinding(BaseModel):
|
|
5
|
+
id: str
|
|
6
|
+
source_agent: str
|
|
7
|
+
target_plan_id: str
|
|
8
|
+
severity: Literal["low", "medium", "high", "critical"]
|
|
9
|
+
finding_type: Literal[
|
|
10
|
+
"missing_requirement",
|
|
11
|
+
"missing_task",
|
|
12
|
+
"missing_test",
|
|
13
|
+
"bad_assumption",
|
|
14
|
+
"architecture_risk",
|
|
15
|
+
"security_risk",
|
|
16
|
+
"performance_risk",
|
|
17
|
+
"dependency_risk",
|
|
18
|
+
"migration_risk",
|
|
19
|
+
"unverifiable_acceptance_criteria"
|
|
20
|
+
]
|
|
21
|
+
claim: str
|
|
22
|
+
linked_requirement_id: Optional[str] = None
|
|
23
|
+
suggested_requirement: Optional[str] = None
|
|
24
|
+
suggested_task: Optional[str] = None
|
|
25
|
+
falsifiable_check: str
|
|
26
|
+
status: Literal[
|
|
27
|
+
"open",
|
|
28
|
+
"accepted",
|
|
29
|
+
"rejected",
|
|
30
|
+
"converted",
|
|
31
|
+
"needs_user"
|
|
32
|
+
] = "open"
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from pydantic import BaseModel
|
|
2
|
+
from typing import Literal, List
|
|
3
|
+
|
|
4
|
+
class CommandResult(BaseModel):
|
|
5
|
+
command: str
|
|
6
|
+
exit_code: int
|
|
7
|
+
stdout_path: str
|
|
8
|
+
stderr_path: str
|
|
9
|
+
summary: str
|
|
10
|
+
|
|
11
|
+
class DiffEvidence(BaseModel):
|
|
12
|
+
task_id: str
|
|
13
|
+
changed_files: List[str]
|
|
14
|
+
added_files: List[str]
|
|
15
|
+
deleted_files: List[str]
|
|
16
|
+
diff_summary: str
|
|
17
|
+
|
|
18
|
+
class VerificationEvidence(BaseModel):
|
|
19
|
+
__test__ = False # Prevent pytest from collecting this as a test class
|
|
20
|
+
requirement_id: str
|
|
21
|
+
acceptance_criterion_id: str
|
|
22
|
+
command: str
|
|
23
|
+
status: Literal["passed", "failed", "not_run"]
|
|
24
|
+
evidence_summary: str
|
|
25
|
+
|
|
26
|
+
# Backward-compatible alias
|
|
27
|
+
TestEvidence = VerificationEvidence
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from pydantic import BaseModel, Field
|
|
2
|
+
from typing import Literal, List, Optional
|
|
3
|
+
|
|
4
|
+
class Gap(BaseModel):
|
|
5
|
+
id: str
|
|
6
|
+
severity: Literal["low", "medium", "high", "critical"]
|
|
7
|
+
gap_type: Literal[
|
|
8
|
+
"requirement_not_planned",
|
|
9
|
+
"task_not_implemented",
|
|
10
|
+
"planned_file_not_changed",
|
|
11
|
+
"orphan_diff",
|
|
12
|
+
"missing_test",
|
|
13
|
+
"test_failed",
|
|
14
|
+
"acceptance_criteria_unproven",
|
|
15
|
+
"assumption_violated",
|
|
16
|
+
"architecture_drift",
|
|
17
|
+
"security_risk",
|
|
18
|
+
"dependency_risk",
|
|
19
|
+
"migration_gap"
|
|
20
|
+
]
|
|
21
|
+
requirement_id: Optional[str] = None
|
|
22
|
+
task_id: Optional[str] = None
|
|
23
|
+
description: str
|
|
24
|
+
evidence: List[str] = Field(default_factory=list)
|
|
25
|
+
recommended_fix: str
|
|
26
|
+
blocking: bool
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from pydantic import BaseModel, Field
|
|
2
|
+
from typing import Literal, List
|
|
3
|
+
|
|
4
|
+
class AcceptanceCriterion(BaseModel):
|
|
5
|
+
id: str
|
|
6
|
+
description: str
|
|
7
|
+
verification_method: Literal[
|
|
8
|
+
"unit_test",
|
|
9
|
+
"integration_test",
|
|
10
|
+
"manual",
|
|
11
|
+
"static_check",
|
|
12
|
+
"llm_review"
|
|
13
|
+
]
|
|
14
|
+
required: bool = True
|
|
15
|
+
|
|
16
|
+
class Requirement(BaseModel):
|
|
17
|
+
id: str
|
|
18
|
+
title: str
|
|
19
|
+
description: str
|
|
20
|
+
priority: Literal["low", "medium", "high", "critical"]
|
|
21
|
+
source: Literal["user", "planner", "critic", "arbiter"]
|
|
22
|
+
acceptance_criteria: List[AcceptanceCriterion] = Field(default_factory=list)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from pydantic import BaseModel, Field
|
|
2
|
+
from typing import Literal, List
|
|
3
|
+
|
|
4
|
+
class PlannedFile(BaseModel):
|
|
5
|
+
path: str
|
|
6
|
+
reason: str
|
|
7
|
+
allowed_change: Literal["create", "modify", "delete", "read_only"]
|
|
8
|
+
|
|
9
|
+
class Task(BaseModel):
|
|
10
|
+
id: str
|
|
11
|
+
title: str
|
|
12
|
+
description: str
|
|
13
|
+
requirement_ids: List[str] = Field(default_factory=list)
|
|
14
|
+
acceptance_criterion_ids: List[str] = Field(default_factory=list)
|
|
15
|
+
planned_files: List[PlannedFile] = Field(default_factory=list)
|
|
16
|
+
expected_tests: List[str] = Field(default_factory=list)
|
|
17
|
+
allowed_commands: List[str] = Field(default_factory=list)
|
|
18
|
+
forbidden_changes: List[str] = Field(default_factory=list)
|
|
19
|
+
status: Literal[
|
|
20
|
+
"planned",
|
|
21
|
+
"ready",
|
|
22
|
+
"running",
|
|
23
|
+
"blocked",
|
|
24
|
+
"verified",
|
|
25
|
+
"done"
|
|
26
|
+
] = "planned"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import List
|
|
3
|
+
from devcouncil.domain.task import Task
|
|
4
|
+
from devcouncil.domain.requirement import Requirement
|
|
5
|
+
from devcouncil.utils.redaction import redact_string
|
|
6
|
+
import json
|
|
7
|
+
|
|
8
|
+
class ContextBuilder:
|
|
9
|
+
"""Gathers repo-level and task-level context for agent prompts."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, project_root: Path):
|
|
12
|
+
self.project_root = project_root
|
|
13
|
+
|
|
14
|
+
def build_task_context(self, task: Task, requirements: List[Requirement]) -> str:
|
|
15
|
+
"""Collects all relevant information for implementing a task."""
|
|
16
|
+
|
|
17
|
+
# 1. Map requirements relevant to this task
|
|
18
|
+
req_map = {r.id: r for r in requirements}
|
|
19
|
+
task_reqs = [req_map[rid] for rid in task.requirement_ids if rid in req_map]
|
|
20
|
+
|
|
21
|
+
# 2. Gather content of planned files (if they exist)
|
|
22
|
+
file_contents = {}
|
|
23
|
+
for pf in task.planned_files:
|
|
24
|
+
file_path = self.project_root / pf.path
|
|
25
|
+
if file_path.exists() and file_path.is_file():
|
|
26
|
+
try:
|
|
27
|
+
raw = file_path.read_text(encoding="utf-8")
|
|
28
|
+
file_contents[pf.path] = redact_string(raw)
|
|
29
|
+
except Exception:
|
|
30
|
+
file_contents[pf.path] = "[Error reading file]"
|
|
31
|
+
else:
|
|
32
|
+
file_contents[pf.path] = "[New file - does not exist yet]"
|
|
33
|
+
|
|
34
|
+
# 3. Assemble the context string
|
|
35
|
+
context = {
|
|
36
|
+
"task": task.model_dump(),
|
|
37
|
+
"relevant_requirements": [r.model_dump() for r in task_reqs],
|
|
38
|
+
"file_contents": file_contents,
|
|
39
|
+
"project_structure": self.get_structure_summary(task)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return json.dumps(context, indent=2)
|
|
43
|
+
|
|
44
|
+
def get_structure_summary(self, task: Task = None) -> List[str]:
|
|
45
|
+
"""Simple list of files in the project for context."""
|
|
46
|
+
try:
|
|
47
|
+
import subprocess
|
|
48
|
+
output = subprocess.check_output(
|
|
49
|
+
["git", "ls-files", "--cached", "--others", "--exclude-standard"],
|
|
50
|
+
cwd=self.project_root,
|
|
51
|
+
stderr=subprocess.DEVNULL
|
|
52
|
+
).decode().splitlines()
|
|
53
|
+
|
|
54
|
+
if task and task.planned_files:
|
|
55
|
+
planned_paths = {pf.path for pf in task.planned_files}
|
|
56
|
+
output = [p for p in output if p in planned_paths] + [p for p in output if p not in planned_paths]
|
|
57
|
+
|
|
58
|
+
return output[:100] # Limit to avoid context overflow
|
|
59
|
+
except Exception:
|
|
60
|
+
return []
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import List
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
from devcouncil.domain.task import Task
|
|
5
|
+
from devcouncil.domain.requirement import Requirement
|
|
6
|
+
|
|
7
|
+
class ExecutionResult(BaseModel):
|
|
8
|
+
success: bool
|
|
9
|
+
message: str
|
|
10
|
+
|
|
11
|
+
class Executor(ABC):
|
|
12
|
+
@abstractmethod
|
|
13
|
+
def run_task(self, task: Task, requirements: List[Requirement]) -> ExecutionResult:
|
|
14
|
+
"""Execute the task and return the result."""
|
|
15
|
+
pass
|