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.
Files changed (146) hide show
  1. package/README.md +218 -96
  2. package/bin/devcouncil.js +130 -32
  3. package/package.json +7 -11
  4. package/pyproject.toml +0 -34
  5. package/src/devcouncil/__init__.py +0 -0
  6. package/src/devcouncil/__main__.py +0 -4
  7. package/src/devcouncil/app/__init__.py +0 -28
  8. package/src/devcouncil/app/config.py +0 -158
  9. package/src/devcouncil/app/errors.py +0 -23
  10. package/src/devcouncil/app/events.py +0 -44
  11. package/src/devcouncil/app/orchestrator.py +0 -92
  12. package/src/devcouncil/app/project_status.py +0 -29
  13. package/src/devcouncil/app/run_context.py +0 -39
  14. package/src/devcouncil/app/state_machine.py +0 -108
  15. package/src/devcouncil/artifacts/__init__.py +0 -1
  16. package/src/devcouncil/artifacts/coverage.py +0 -96
  17. package/src/devcouncil/artifacts/graph.py +0 -143
  18. package/src/devcouncil/artifacts/migrations.py +0 -20
  19. package/src/devcouncil/artifacts/schemas.py +0 -23
  20. package/src/devcouncil/artifacts/serializer.py +0 -21
  21. package/src/devcouncil/artifacts/validators.py +0 -27
  22. package/src/devcouncil/cli/__init__.py +0 -0
  23. package/src/devcouncil/cli/commands/__init__.py +0 -0
  24. package/src/devcouncil/cli/commands/artifacts.py +0 -51
  25. package/src/devcouncil/cli/commands/ast.py +0 -22
  26. package/src/devcouncil/cli/commands/baseline.py +0 -35
  27. package/src/devcouncil/cli/commands/config.py +0 -76
  28. package/src/devcouncil/cli/commands/dashboard.py +0 -26
  29. package/src/devcouncil/cli/commands/doctor.py +0 -140
  30. package/src/devcouncil/cli/commands/go.py +0 -237
  31. package/src/devcouncil/cli/commands/hook.py +0 -128
  32. package/src/devcouncil/cli/commands/init.py +0 -153
  33. package/src/devcouncil/cli/commands/integrate.py +0 -726
  34. package/src/devcouncil/cli/commands/lsp.py +0 -20
  35. package/src/devcouncil/cli/commands/map.py +0 -42
  36. package/src/devcouncil/cli/commands/mcp_server.py +0 -18
  37. package/src/devcouncil/cli/commands/plan.py +0 -327
  38. package/src/devcouncil/cli/commands/prompt.py +0 -50
  39. package/src/devcouncil/cli/commands/repair.py +0 -72
  40. package/src/devcouncil/cli/commands/report.py +0 -129
  41. package/src/devcouncil/cli/commands/reset_demo_state.py +0 -31
  42. package/src/devcouncil/cli/commands/rollback.py +0 -60
  43. package/src/devcouncil/cli/commands/run.py +0 -269
  44. package/src/devcouncil/cli/commands/setup.py +0 -223
  45. package/src/devcouncil/cli/commands/show.py +0 -76
  46. package/src/devcouncil/cli/commands/status.py +0 -117
  47. package/src/devcouncil/cli/commands/tasks.py +0 -55
  48. package/src/devcouncil/cli/commands/trace.py +0 -44
  49. package/src/devcouncil/cli/commands/verify.py +0 -193
  50. package/src/devcouncil/cli/commands/version.py +0 -20
  51. package/src/devcouncil/cli/commands/watch.py +0 -574
  52. package/src/devcouncil/cli/main.py +0 -88
  53. package/src/devcouncil/council/__init__.py +0 -0
  54. package/src/devcouncil/council/prompts/__init__.py +0 -0
  55. package/src/devcouncil/council/prompts/arbiter.md +0 -19
  56. package/src/devcouncil/council/prompts/critic_a.md +0 -10
  57. package/src/devcouncil/council/prompts/critic_b.md +0 -10
  58. package/src/devcouncil/council/prompts/implementation_reviewer.md +0 -16
  59. package/src/devcouncil/council/prompts/planner_a.md +0 -16
  60. package/src/devcouncil/council/prompts/planner_b.md +0 -16
  61. package/src/devcouncil/council/prompts/rebuttal.md +0 -10
  62. package/src/devcouncil/council/prompts/spec_writer.md +0 -12
  63. package/src/devcouncil/domain/__init__.py +0 -0
  64. package/src/devcouncil/domain/assumption.py +0 -17
  65. package/src/devcouncil/domain/critique.py +0 -32
  66. package/src/devcouncil/domain/evidence.py +0 -27
  67. package/src/devcouncil/domain/gap.py +0 -26
  68. package/src/devcouncil/domain/requirement.py +0 -22
  69. package/src/devcouncil/domain/task.py +0 -26
  70. package/src/devcouncil/execution/__init__.py +0 -1
  71. package/src/devcouncil/execution/context_builder.py +0 -60
  72. package/src/devcouncil/execution/executor.py +0 -15
  73. package/src/devcouncil/execution/hook_policy.py +0 -165
  74. package/src/devcouncil/execution/patch.py +0 -28
  75. package/src/devcouncil/execution/paths.py +0 -14
  76. package/src/devcouncil/execution/permissions.py +0 -92
  77. package/src/devcouncil/execution/prompt_builder.py +0 -59
  78. package/src/devcouncil/execution/task_runner.py +0 -166
  79. package/src/devcouncil/executors/__init__.py +0 -1
  80. package/src/devcouncil/executors/coding_cli.py +0 -112
  81. package/src/devcouncil/executors/mini_swe.py +0 -73
  82. package/src/devcouncil/executors/native/__init__.py +0 -0
  83. package/src/devcouncil/executors/native/agent.py +0 -107
  84. package/src/devcouncil/executors/openhands.py +0 -71
  85. package/src/devcouncil/gating/__init__.py +0 -1
  86. package/src/devcouncil/gating/checks/__init__.py +0 -0
  87. package/src/devcouncil/gating/checks/clean_git.py +0 -50
  88. package/src/devcouncil/gating/checks/planned_files_check.py +0 -32
  89. package/src/devcouncil/gating/checks/requirement_coverage.py +0 -26
  90. package/src/devcouncil/gating/checks/secret_scan_check.py +0 -34
  91. package/src/devcouncil/gating/policy.py +0 -190
  92. package/src/devcouncil/indexing/__init__.py +0 -1
  93. package/src/devcouncil/indexing/ast_matcher.py +0 -168
  94. package/src/devcouncil/indexing/graph_index.py +0 -48
  95. package/src/devcouncil/indexing/lsp.py +0 -120
  96. package/src/devcouncil/indexing/repo_mapper.py +0 -208
  97. package/src/devcouncil/indexing/symbol_index.py +0 -0
  98. package/src/devcouncil/integrations/code_review_graph.py +0 -163
  99. package/src/devcouncil/integrations/github.py +0 -39
  100. package/src/devcouncil/integrations/gitnexus.py +0 -27
  101. package/src/devcouncil/integrations/graphify.py +0 -34
  102. package/src/devcouncil/integrations/mcp/__init__.py +0 -0
  103. package/src/devcouncil/integrations/mcp/server.py +0 -599
  104. package/src/devcouncil/integrations/pr_comments.py +0 -62
  105. package/src/devcouncil/live/__init__.py +0 -2
  106. package/src/devcouncil/live/cards.py +0 -207
  107. package/src/devcouncil/live/models.py +0 -63
  108. package/src/devcouncil/live/repair_prompt.py +0 -83
  109. package/src/devcouncil/live/reviewer.py +0 -70
  110. package/src/devcouncil/live/signals.py +0 -135
  111. package/src/devcouncil/live/summary.py +0 -34
  112. package/src/devcouncil/live/tasks.py +0 -18
  113. package/src/devcouncil/live/transcripts.py +0 -138
  114. package/src/devcouncil/llm/__init__.py +0 -1
  115. package/src/devcouncil/llm/cache.py +0 -38
  116. package/src/devcouncil/llm/provider.py +0 -146
  117. package/src/devcouncil/llm/router.py +0 -125
  118. package/src/devcouncil/planning/__init__.py +0 -1
  119. package/src/devcouncil/planning/arbiter_service.py +0 -57
  120. package/src/devcouncil/planning/critique_service.py +0 -66
  121. package/src/devcouncil/planning/plan_service.py +0 -46
  122. package/src/devcouncil/planning/prompt_enhancer_service.py +0 -86
  123. package/src/devcouncil/planning/repair_service.py +0 -39
  124. package/src/devcouncil/planning/spec_service.py +0 -44
  125. package/src/devcouncil/repo/__init__.py +0 -0
  126. package/src/devcouncil/reporting/__init__.py +0 -0
  127. package/src/devcouncil/reporting/github_check.py +0 -32
  128. package/src/devcouncil/reporting/json_report.py +0 -20
  129. package/src/devcouncil/reporting/markdown_report.py +0 -68
  130. package/src/devcouncil/reporting/report_builder.py +0 -14
  131. package/src/devcouncil/storage/__init__.py +0 -0
  132. package/src/devcouncil/storage/db.py +0 -66
  133. package/src/devcouncil/storage/models.py +0 -83
  134. package/src/devcouncil/storage/repositories.py +0 -423
  135. package/src/devcouncil/telemetry/__init__.py +0 -0
  136. package/src/devcouncil/telemetry/cost.py +0 -34
  137. package/src/devcouncil/telemetry/traces.py +0 -91
  138. package/src/devcouncil/telemetry/tracker.py +0 -49
  139. package/src/devcouncil/ui/__init__.py +0 -1
  140. package/src/devcouncil/ui/dashboard.py +0 -122
  141. package/src/devcouncil/utils/__init__.py +0 -1
  142. package/src/devcouncil/utils/redaction.py +0 -141
  143. package/src/devcouncil/verification/__init__.py +0 -1
  144. package/src/devcouncil/verification/implementation_reviewer.py +0 -55
  145. package/src/devcouncil/verification/verifier.py +0 -530
  146. package/uv.lock +0 -1085
@@ -1,31 +0,0 @@
1
- import typer
2
- from rich.console import Console
3
- from sqlmodel import delete
4
- from pathlib import Path
5
-
6
- from devcouncil.storage.db import get_db
7
- from devcouncil.storage.models import EvidenceModel, GapModel, RequirementModel, TaskModel
8
- from devcouncil.cli.commands.init import initialize_project
9
-
10
- console = Console()
11
-
12
-
13
- def reset_demo_state(
14
- yes: bool = typer.Option(False, "--yes", help="Confirm clearing planning/demo artifacts."),
15
- ):
16
- """Clear demo planning artifacts from the local DevCouncil state database."""
17
- if not yes:
18
- console.print("[red]Refusing to clear state without --yes.[/red]")
19
- raise typer.Exit(code=1)
20
-
21
- initialize_project(Path("."), quiet=True)
22
- db = get_db()
23
- if not db:
24
- console.print("[red]DevCouncil state is unavailable in this directory.[/red]")
25
- raise typer.Exit(code=1)
26
-
27
- with db.get_session() as session:
28
- for model in (EvidenceModel, GapModel, TaskModel, RequirementModel):
29
- session.exec(delete(model))
30
-
31
- console.print("[green]Cleared requirements, tasks, gaps, and evidence from local state.[/green]")
@@ -1,60 +0,0 @@
1
- import typer
2
- import subprocess
3
- from pathlib import Path
4
- from rich.console import Console
5
-
6
- app = typer.Typer()
7
- console = Console()
8
-
9
- @app.callback(invoke_without_command=True)
10
- def rollback(
11
- ctx: typer.Context,
12
- task_id: str = typer.Argument(..., help="ID of the task to rollback"),
13
- project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
14
- ):
15
- """
16
- Revert changes using a task's git checkpoint.
17
- """
18
- if ctx.invoked_subcommand is not None:
19
- return
20
-
21
- root = project_root.expanduser().resolve()
22
- checkpoint_file = root / ".devcouncil" / "checkpoints" / f"{task_id}-before.patch"
23
- after_patch = root / ".devcouncil" / "checkpoints" / f"{task_id}-after.patch"
24
-
25
- if not checkpoint_file.exists() and not after_patch.exists():
26
- console.print(
27
- f"[red]No checkpoint found for task {task_id}. Expected {after_patch} "
28
- f"or {checkpoint_file}.[/red]"
29
- )
30
- raise typer.Exit(code=1)
31
-
32
- console.print(f"Rolling back task [bold]{task_id}[/bold]...")
33
-
34
- try:
35
- if after_patch.exists():
36
- # Reverse-apply the task's changes only
37
- console.print(f"Applying reverse patch from [bold]{after_patch}[/bold]...")
38
- subprocess.check_call(
39
- ["git", "apply", "-R", str(after_patch)],
40
- cwd=root,
41
- )
42
- console.print(f"[green]Successfully rolled back task {task_id} changes.[/green]")
43
- else:
44
- # No after-patch, but we have the before-patch — warn and offer manual reset
45
- console.print(
46
- f"[yellow]No after-patch found at {after_patch}.[/yellow]\n"
47
- f"The before-patch at {checkpoint_file} captured the state before the task ran.\n"
48
- f"To manually reset:\n"
49
- f" 1. [bold]git stash[/bold] (if you want to keep current changes)\n"
50
- f" 2. [bold]git checkout -- .[/bold] (discard working tree changes)\n"
51
- f" 3. [bold]git apply {checkpoint_file}[/bold] (restore pre-task state)"
52
- )
53
- except subprocess.CalledProcessError as e:
54
- console.print(f"[red]Failed to apply reverse patch: {e}[/red]")
55
- console.print("[yellow]The patch may conflict with current changes. Try resolving manually:[/yellow]")
56
- console.print(f" git apply -R --3way {after_patch}")
57
- raise typer.Exit(code=1)
58
- except Exception as e:
59
- console.print(f"[red]Failed to rollback: {e}[/red]")
60
- raise typer.Exit(code=1)
@@ -1,269 +0,0 @@
1
- import typer
2
- import json
3
- from rich.console import Console
4
- from pathlib import Path
5
- from devcouncil.storage.db import get_db
6
- from devcouncil.storage.repositories import TaskRepository, RequirementRepository
7
- from devcouncil.executors.mini_swe import MiniSWEExecutor
8
- from devcouncil.executors.openhands import OpenHandsExecutor
9
- from devcouncil.executors.coding_cli import CodingCliExecutor
10
- from devcouncil.executors.native.agent import NativeAgent
11
- from devcouncil.llm.provider import create_provider, validate_model_provider
12
- from devcouncil.llm.router import ModelRouter
13
- from devcouncil.app.config import load_config, get_api_key
14
- from devcouncil.domain.evidence import CommandResult, DiffEvidence, TestEvidence
15
- from devcouncil.storage.repositories import GapRepository, EvidenceRepository, StateRepository
16
- from devcouncil.verification.verifier import Verifier
17
- from devcouncil.app.state_machine import ProjectPhase
18
- from devcouncil.cli.commands.init import initialize_project
19
-
20
- console = Console()
21
- CODING_EXECUTOR_ALIASES = {
22
- "codex": "codex",
23
- "codex-cli": "codex",
24
- "gemini": "gemini",
25
- "gemini-cli": "gemini",
26
- "claude": "claude",
27
- "claude-code": "claude",
28
- "claude-cli": "claude",
29
- }
30
-
31
- CODING_EXECUTORS = set(CODING_EXECUTOR_ALIASES.keys())
32
-
33
- def _current_changed_files(project_root: Path = Path(".")) -> list[str]:
34
- from devcouncil.verification.verifier import Verifier
35
-
36
- return Verifier(project_root).get_changed_files()
37
-
38
- def _capture_after_patch(task_id: str, project_root: Path = Path(".")):
39
- """Capture the diff after task execution for use by rollback."""
40
- try:
41
- from devcouncil.verification.verifier import Verifier
42
-
43
- checkpoint_dir = project_root / ".devcouncil" / "checkpoints"
44
- checkpoint_dir.mkdir(exist_ok=True)
45
- diff = Verifier(project_root).get_diff()
46
- if diff:
47
- with open(checkpoint_dir / f"{task_id}-after.patch", "w", encoding="utf-8") as f:
48
- f.write(diff)
49
- except Exception:
50
- pass # Non-critical — don't block execution
51
-
52
- def _capture_before_snapshot(task_id: str, project_root: Path = Path(".")):
53
- checkpoint_dir = project_root / ".devcouncil" / "checkpoints"
54
- checkpoint_dir.mkdir(exist_ok=True)
55
- snapshot = {
56
- "task_id": task_id,
57
- "changed_files": _current_changed_files(project_root),
58
- }
59
- (checkpoint_dir / f"{task_id}-before.json").write_text(
60
- json.dumps(snapshot, indent=2),
61
- encoding="utf-8",
62
- )
63
-
64
- def _record_project_phase(session, phase: ProjectPhase):
65
- StateRepository(session).record_phase(phase.value)
66
-
67
- def _verify_after_execution(session, task, reqs, router=None, project_root: Path = Path(".")) -> bool:
68
- """Run deterministic verification after an automated executor finishes."""
69
- import asyncio
70
-
71
- verifier = Verifier(project_root, router=router)
72
- gaps, evidence = asyncio.run(verifier.verify_task(task, reqs))
73
-
74
- gap_repo = GapRepository(session)
75
- evidence_repo = EvidenceRepository(session)
76
- gap_repo.delete_for_task(task.id)
77
- evidence_repo.delete_for_task(task.id)
78
-
79
- for gap in gaps:
80
- gap_repo.save(gap)
81
-
82
- for ev in evidence:
83
- if isinstance(ev, CommandResult):
84
- evidence_repo.save_command_result(task.id, ev)
85
- elif isinstance(ev, DiffEvidence):
86
- evidence_repo.save_diff_evidence(ev)
87
- elif isinstance(ev, TestEvidence):
88
- evidence_repo.save_test_evidence(ev, task.id)
89
-
90
- task.status = "blocked" if any(g.blocking for g in gaps) else "verified"
91
- return task.status == "verified"
92
-
93
- def run(
94
- task_id: str = typer.Argument(..., help="ID of the task to run"),
95
- executor: str = typer.Option(
96
- "manual",
97
- "--executor",
98
- "-e",
99
- help="Executor to use (manual, mini, openhands, native, codex, codex-cli, gemini, gemini-cli, claude, claude-code, claude-cli)",
100
- ),
101
- project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
102
- ):
103
- """
104
- Execute a specific task.
105
- """
106
- root = project_root.expanduser().resolve()
107
- initialize_project(root, quiet=True)
108
- db = get_db(root)
109
- if not db:
110
- console.print("[red]DevCouncil state is unavailable in this directory.[/red]")
111
- return
112
-
113
- with db.get_session() as session:
114
- task_repo = TaskRepository(session)
115
- task = task_repo.get_by_id(task_id)
116
- if not task:
117
- console.print(f"[red]Task {task_id} not found.[/red]")
118
- return
119
-
120
- from devcouncil.gating.policy import GatePolicy
121
- policy = GatePolicy()
122
- gate_result = policy.check_task_ready(task, root)
123
- if not gate_result.passed:
124
- console.print(f"[red]Task {task_id} is not ready for execution.[/red]")
125
- for gap in gate_result.gaps:
126
- if gap.blocking:
127
- console.print(f" - [red][BLOCKING][/red] {gap.description} (Fix: {gap.recommended_fix})")
128
- return
129
-
130
- console.print(f"Running task [bold]{task_id}[/bold] using [bold]{executor}[/bold] executor...")
131
-
132
- # 1. Create Git checkpoint
133
- try:
134
- from devcouncil.verification.verifier import Verifier
135
-
136
- checkpoint_dir = root / ".devcouncil" / "checkpoints"
137
- checkpoint_dir.mkdir(exist_ok=True)
138
- _capture_before_snapshot(task_id, root)
139
- diff = Verifier(root).get_diff()
140
- if diff:
141
- with open(checkpoint_dir / f"{task_id}-before.patch", "w", encoding="utf-8") as f:
142
- f.write(diff)
143
- console.print(f"Created git checkpoint at {checkpoint_dir}/{task_id}-before.patch")
144
- except Exception as e:
145
- console.print(f"[yellow]Warning: Failed to create git checkpoint: {e}[/yellow]")
146
-
147
- executor = executor.strip().lower().replace("_", "-")
148
- if executor == "manual":
149
- _record_project_phase(session, ProjectPhase.TASK_EXECUTING)
150
- task.status = "running"
151
- task_repo.save(task)
152
- console.print(f"\n[green]Task {task_id} is now marked as RUNNING.[/green]")
153
- console.print("Use 'dev prompt TASK-ID' to get the prompt for this task.")
154
- console.print("When finished, use 'dev verify TASK-ID' to check the results.")
155
- elif executor in CODING_EXECUTORS:
156
- _record_project_phase(session, ProjectPhase.TASK_EXECUTING)
157
- req_repo = RequirementRepository(session)
158
- reqs = req_repo.get_all()
159
- cli_client = CODING_EXECUTOR_ALIASES[executor]
160
- cli_executor = CodingCliExecutor(root, cli_client)
161
- exec_result = cli_executor.run_task(task, reqs)
162
- _capture_after_patch(task_id, root)
163
- if exec_result.success:
164
- _record_project_phase(session, ProjectPhase.TASK_VERIFYING)
165
- verified = _verify_after_execution(session, task, reqs, project_root=root)
166
- _record_project_phase(
167
- session,
168
- ProjectPhase.TASK_VERIFIED if verified else ProjectPhase.TASK_BLOCKED,
169
- )
170
- task_repo.save(task)
171
- if verified:
172
- console.print(f"\n[green]{executor.upper()} finished and task {task_id} verified.[/green]")
173
- else:
174
- console.print(f"\n[yellow]{executor.upper()} finished, but task {task_id} is blocked by verification gaps.[/yellow]")
175
- else:
176
- console.print(f"\n[red]{executor.upper()} failed to start or execute: {exec_result.message}[/red]")
177
- elif executor == "mini":
178
- _record_project_phase(session, ProjectPhase.TASK_EXECUTING)
179
- req_repo = RequirementRepository(session)
180
- reqs = req_repo.get_all()
181
- mini_executor = MiniSWEExecutor(root)
182
- exec_result = mini_executor.run_task(task, reqs)
183
- _capture_after_patch(task_id, root)
184
- if exec_result.success:
185
- _record_project_phase(session, ProjectPhase.TASK_VERIFYING)
186
- verified = _verify_after_execution(session, task, reqs, project_root=root)
187
- _record_project_phase(
188
- session,
189
- ProjectPhase.TASK_VERIFIED if verified else ProjectPhase.TASK_BLOCKED,
190
- )
191
- task_repo.save(task)
192
- if verified:
193
- console.print(f"\n[green]mini-SWE-agent finished and task {task_id} verified.[/green]")
194
- else:
195
- console.print(f"\n[yellow]mini-SWE-agent finished, but task {task_id} is blocked by verification gaps.[/yellow]")
196
- else:
197
- console.print("\n[red]mini-SWE-agent failed to start or execute.[/red]")
198
- elif executor == "openhands":
199
- _record_project_phase(session, ProjectPhase.TASK_EXECUTING)
200
- req_repo = RequirementRepository(session)
201
- reqs = req_repo.get_all()
202
- oh_executor = OpenHandsExecutor(root)
203
- exec_result = oh_executor.run_task(task, reqs)
204
- _capture_after_patch(task_id, root)
205
- if exec_result.success:
206
- _record_project_phase(session, ProjectPhase.TASK_VERIFYING)
207
- verified = _verify_after_execution(session, task, reqs, project_root=root)
208
- _record_project_phase(
209
- session,
210
- ProjectPhase.TASK_VERIFIED if verified else ProjectPhase.TASK_BLOCKED,
211
- )
212
- task_repo.save(task)
213
- if verified:
214
- console.print(f"\n[green]OpenHands finished and task {task_id} verified.[/green]")
215
- else:
216
- console.print(f"\n[yellow]OpenHands finished, but task {task_id} is blocked by verification gaps.[/yellow]")
217
- else:
218
- console.print("\n[red]OpenHands failed to start or execute.[/red]")
219
- elif executor == "native":
220
- # Load config for model routing and permissions
221
- try:
222
- config = load_config(root)
223
- validate_model_provider(config.models.provider)
224
- api_key = get_api_key(config.models.provider, root)
225
- except (FileNotFoundError, ValueError) as e:
226
- console.print(f"[red]{e}[/red]")
227
- return
228
-
229
- provider = create_provider(config.models.provider, api_key)
230
- role_config = {name: role.model_dump() for name, role in config.models.roles.items()}
231
- router = ModelRouter(provider, role_config)
232
-
233
- # Setup Permission System
234
- from devcouncil.execution.permissions import PermissionPolicy, PermissionManager
235
- from devcouncil.execution.task_runner import TaskRunner
236
-
237
- # Populate policy from config commands
238
- allowed_cmds = config.commands.test + config.commands.lint + config.commands.typecheck
239
- policy = PermissionPolicy(
240
- allowed_shell_commands=allowed_cmds,
241
- )
242
- perm_manager = PermissionManager(policy, root)
243
- task_runner = TaskRunner(root, perm_manager)
244
-
245
- req_repo = RequirementRepository(session)
246
- reqs = req_repo.get_all()
247
-
248
- import asyncio
249
- _record_project_phase(session, ProjectPhase.TASK_EXECUTING)
250
- agent = NativeAgent(router, task_runner)
251
- exec_result = asyncio.run(agent.run_task(task, reqs))
252
- _capture_after_patch(task_id, root)
253
-
254
- if exec_result.success:
255
- _record_project_phase(session, ProjectPhase.TASK_VERIFYING)
256
- verified = _verify_after_execution(session, task, reqs, router=router, project_root=root)
257
- _record_project_phase(
258
- session,
259
- ProjectPhase.TASK_VERIFIED if verified else ProjectPhase.TASK_BLOCKED,
260
- )
261
- task_repo.save(task)
262
- if verified:
263
- console.print(f"\n[green]Native agent finished and task {task_id} verified.[/green]")
264
- else:
265
- console.print(f"\n[yellow]Native agent finished, but task {task_id} is blocked by verification gaps.[/yellow]")
266
- else:
267
- console.print("\n[red]Native agent failed during execution.[/red]")
268
- else:
269
- console.print(f"[red]Executor {executor} not yet implemented.[/red]")
@@ -1,223 +0,0 @@
1
- from pathlib import Path
2
- import os
3
- import shutil
4
- import sys
5
-
6
- import typer
7
- import yaml
8
- from rich.console import Console
9
- from rich.panel import Panel
10
-
11
- from devcouncil.app.config import load_config, load_local_secrets, provider_api_key_env_var
12
- from devcouncil.cli.commands.doctor import render_doctor_check
13
- from devcouncil.cli.commands.init import initialize_project
14
- from devcouncil.cli.commands.integrate import (
15
- _claude_command,
16
- _codex_command,
17
- _configure_native_hooks,
18
- _configure,
19
- _cursor_command,
20
- _gemini_command,
21
- )
22
- from devcouncil.llm.provider import validate_model_provider
23
-
24
- app = typer.Typer()
25
- console = Console()
26
-
27
-
28
- def _set_model_provider(project_root: Path, provider: str) -> None:
29
- normalized = validate_model_provider(provider)
30
- config_path = project_root / ".devcouncil" / "config.yaml"
31
- raw_config = yaml.safe_load(config_path.read_text(encoding="utf-8")) or {}
32
- raw_config.setdefault("models", {})
33
- previous = raw_config["models"].get("provider", "openrouter")
34
- raw_config["models"]["provider"] = normalized
35
- config_path.write_text(yaml.dump(raw_config, default_flow_style=False), encoding="utf-8")
36
- if previous != normalized:
37
- console.print(f"[green]Updated model provider from {previous} to {normalized}.[/green]")
38
-
39
-
40
- def _write_local_secret(project_root: Path, env_var: str, value: str) -> Path:
41
- if "\n" in value or "\r" in value:
42
- raise ValueError("API keys cannot contain newlines.")
43
- secrets = load_local_secrets(project_root)
44
- secrets[env_var] = value
45
- path = project_root / ".devcouncil" / "secrets.env"
46
- path.parent.mkdir(parents=True, exist_ok=True)
47
- lines = [
48
- "# Local DevCouncil secrets. This file is ignored by git.",
49
- "# Process environment variables with the same name take precedence.",
50
- *[f"{key}={val}" for key, val in sorted(secrets.items())],
51
- "",
52
- ]
53
- path.write_text("\n".join(lines), encoding="utf-8")
54
- return path
55
-
56
-
57
- def _configure_api_key(project_root: Path, api_key: str | None, skip_api_key: bool) -> None:
58
- config = load_config(project_root)
59
- provider = config.models.provider
60
- env_var = provider_api_key_env_var(provider)
61
- local_secrets = load_local_secrets(project_root)
62
-
63
- if os.environ.get(env_var):
64
- console.print(f"[green]{env_var} is already set in the environment.[/green]")
65
- return
66
- if local_secrets.get(env_var):
67
- console.print(f"[green]{env_var} is already set in .devcouncil/secrets.env.[/green]")
68
- return
69
- if api_key:
70
- _write_local_secret(project_root, env_var, api_key)
71
- console.print(f"[green]Saved {env_var} to .devcouncil/secrets.env.[/green]")
72
- return
73
- if skip_api_key:
74
- console.print(f"[yellow]Skipped {env_var} setup. Model-backed commands will ask again if it is missing.[/yellow]")
75
- return
76
- if not sys.stdin.isatty():
77
- console.print(
78
- f"[yellow]{env_var} is not set.[/yellow] "
79
- f"Run [bold]dev setup --api-key YOUR_KEY[/bold] or set it in your shell before model-backed commands."
80
- )
81
- return
82
-
83
- console.print()
84
- console.print(Panel.fit(
85
- "\n".join([
86
- f"Provider: {provider}",
87
- f"Required key: {env_var}",
88
- "Press Enter without a value to skip for now.",
89
- ]),
90
- title="Model API Key",
91
- border_style="cyan",
92
- ))
93
- entered = typer.prompt(f"{env_var}", default="", hide_input=True, show_default=False)
94
- if not entered:
95
- console.print(f"[yellow]Skipped {env_var} setup.[/yellow]")
96
- return
97
- _write_local_secret(project_root, env_var, entered)
98
- console.print(f"[green]Saved {env_var} to .devcouncil/secrets.env.[/green]")
99
-
100
-
101
- def _is_interactive_terminal() -> bool:
102
- return sys.stdin.isatty()
103
-
104
-
105
- def _configure_coding_cli_integrations(project_root: Path, apply: bool, gemini_scope: str) -> None:
106
- console.print()
107
- console.print("[bold]Coding CLI integration[/bold]")
108
- commands = [
109
- ("Codex CLI", _codex_command(project_root)),
110
- ("Gemini CLI", _gemini_command(project_root, gemini_scope)),
111
- ("Claude Code", _claude_command(project_root, "local")),
112
- ("Cursor", _cursor_command(project_root)),
113
- ]
114
- results = []
115
- for tool, command in commands:
116
- if apply and not shutil.which(command[0]):
117
- console.print(f"[yellow]{tool} CLI not found on PATH. Skipping optional integration.[/yellow]")
118
- continue
119
- results.append(_configure(tool, command, apply))
120
- _configure_native_hooks(project_root, "all", apply)
121
- if apply and any(not ok for ok in results):
122
- raise typer.Exit(code=1)
123
-
124
-
125
- def _prompt_for_first_run_integrations(project_root: Path, apply: bool, gemini_scope: str) -> bool:
126
- if not _is_interactive_terminal():
127
- return False
128
-
129
- console.print()
130
- console.print(Panel.fit(
131
- "\n".join([
132
- "DevCouncil can configure supported coding CLIs now.",
133
- "This adds MCP setup and native hook config for detected clients.",
134
- "Missing optional clients are skipped.",
135
- ]),
136
- title="Coding CLI Setup",
137
- border_style="cyan",
138
- ))
139
- if not typer.confirm("Set up coding CLI integrations now?", default=True):
140
- console.print("[yellow]Skipped coding CLI integration setup.[/yellow]")
141
- return False
142
-
143
- _configure_coding_cli_integrations(project_root, apply=apply, gemini_scope=gemini_scope)
144
- return True
145
-
146
-
147
- @app.callback(invoke_without_command=True)
148
- def setup(
149
- ctx: typer.Context,
150
- project_root: Path = typer.Option(
151
- Path("."),
152
- "--project-root",
153
- help="Target project repository root. Defaults to the terminal's current directory.",
154
- ),
155
- name: str | None = typer.Option(None, "--name", "-n", help="Project name for .devcouncil/config.yaml."),
156
- integrate: bool = typer.Option(False, "--integrate", help="Configure supported coding CLI MCP integrations and native hooks."),
157
- apply: bool = typer.Option(False, "--apply", help="Apply integration config instead of previewing commands."),
158
- gemini_scope: str = typer.Option("project", "--gemini-scope", help="Gemini MCP config scope: project or user."),
159
- provider: str | None = typer.Option(None, "--provider", help="Set models.provider before configuring the API key."),
160
- api_key: str | None = typer.Option(None, "--api-key", help="Store the configured provider API key in local .devcouncil/secrets.env."),
161
- skip_api_key: bool = typer.Option(False, "--skip-api-key", help="Skip the first-run model API key prompt."),
162
- skip_integrations: bool = typer.Option(False, "--skip-integrations", help="Skip the first-run coding CLI integration prompt."),
163
- ):
164
- """
165
- Initialize DevCouncil from a normal terminal in the target repository root.
166
-
167
- Use the coding CLI later only for the generated dev prompt output.
168
- """
169
- if ctx.invoked_subcommand is not None:
170
- return
171
-
172
- if gemini_scope not in {"project", "user"}:
173
- console.print("[red]--gemini-scope must be 'project' or 'user'.[/red]")
174
- raise typer.Exit(code=2)
175
-
176
- root = project_root.expanduser().resolve()
177
- created = initialize_project(root, project_name=name)
178
- if not created:
179
- console.print(f"[yellow]DevCouncil is already initialized at {root / '.devcouncil'}.[/yellow]")
180
-
181
- if provider:
182
- try:
183
- _set_model_provider(root, provider)
184
- except ValueError as e:
185
- console.print(f"[red]{e}[/red]")
186
- raise typer.Exit(code=2) from e
187
-
188
- _configure_api_key(root, api_key, skip_api_key)
189
-
190
- console.print()
191
- render_doctor_check(root)
192
-
193
- if integrate:
194
- _configure_coding_cli_integrations(root, apply=apply, gemini_scope=gemini_scope)
195
- elif created and not skip_integrations:
196
- _prompt_for_first_run_integrations(root, apply=True, gemini_scope=gemini_scope)
197
-
198
- console.print()
199
- console.print(Panel.fit(
200
- "\n".join([
201
- "[bold]Next commands[/bold]",
202
- f"Keep running DevCouncil commands in this terminal at: {root}",
203
- "One-command agent path:",
204
- "dev e2e \"Describe the implementation goal\"",
205
- "",
206
- "Manual sidecar path:",
207
- "dev plan \"Describe the implementation goal\"",
208
- "dev tasks",
209
- "dev run TASK-001 --executor manual",
210
- "dev prompt TASK-001",
211
- "Paste only the dev prompt output into your coding CLI.",
212
- "Paste only the dev prompt output into your coding CLI, or run directly:",
213
- "dev run TASK-001 --executor codex",
214
- "dev run TASK-001 --executor gemini",
215
- "dev run TASK-001 --executor claude",
216
- "dev verify TASK-001",
217
- "",
218
- "Use [bold]dev setup --integrate[/bold] to preview coding CLI MCP and native hook setup.",
219
- "Use [bold]dev setup --integrate --apply[/bold] to configure detected clients.",
220
- ]),
221
- title="DevCouncil is ready",
222
- border_style="green",
223
- ))
@@ -1,76 +0,0 @@
1
- import json
2
- from pathlib import Path
3
-
4
- import typer
5
- from rich.console import Console
6
- from rich.panel import Panel
7
- from devcouncil.cli.commands.init import initialize_project
8
- from devcouncil.storage.db import get_db
9
- from devcouncil.storage.repositories import TaskRepository, RequirementRepository
10
-
11
- app = typer.Typer()
12
- console = Console()
13
-
14
- @app.callback(invoke_without_command=True)
15
- def show(
16
- ctx: typer.Context,
17
- task_id: str = typer.Argument(..., help="ID of the task to show"),
18
- json_format: bool = typer.Option(False, "--json", help="Output machine-readable JSON."),
19
- project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
20
- ):
21
- """
22
- Show details of a specific task.
23
- """
24
- if ctx.invoked_subcommand is not None:
25
- return
26
-
27
- root = project_root.expanduser().resolve()
28
- initialize_project(root, quiet=True)
29
- db = get_db(root)
30
- if not db:
31
- raise typer.Exit(code=1)
32
-
33
- with db.get_session() as session:
34
- task_repo = TaskRepository(session)
35
- req_repo = RequirementRepository(session)
36
-
37
- task = task_repo.get_by_id(task_id)
38
- if not task:
39
- console.print(f"[red]Task {task_id} not found.[/red]")
40
- raise typer.Exit(code=1)
41
-
42
- reqs = req_repo.get_all()
43
- req_map = {r.id: r for r in reqs}
44
-
45
- if json_format:
46
- linked_requirements = [
47
- req_map[req_id].model_dump()
48
- for req_id in task.requirement_ids
49
- if req_id in req_map
50
- ]
51
- typer.echo(json.dumps({
52
- "task": task.model_dump(),
53
- "linked_requirements": linked_requirements,
54
- }, indent=2))
55
- return
56
-
57
- output = f"[bold]Status:[/bold] {task.status}\n\n"
58
- output += f"[bold]Description:[/bold]\n{task.description}\n\n"
59
-
60
- output += "[bold]Linked Requirements:[/bold]\n"
61
- for req_id in task.requirement_ids:
62
- req = req_map.get(req_id)
63
- if req:
64
- output += f" - [cyan]{req.id}[/cyan]: {req.title}\n"
65
- else:
66
- output += f" - [cyan]{req_id}[/cyan]: (Requirement not found)\n"
67
-
68
- output += "\n[bold]Planned Files:[/bold]\n"
69
- for pf in task.planned_files:
70
- output += f" - {pf.path} ({pf.allowed_change}): {pf.reason}\n"
71
-
72
- output += "\n[bold]Expected Tests:[/bold]\n"
73
- for et in task.expected_tests:
74
- output += f" - {et}\n"
75
-
76
- console.print(Panel(output, title=f"Task {task.id}: {task.title}", expand=False))