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.
Files changed (125) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +643 -0
  3. package/bin/devcouncil.js +62 -0
  4. package/package.json +47 -0
  5. package/pyproject.toml +31 -0
  6. package/src/devcouncil/__init__.py +0 -0
  7. package/src/devcouncil/__main__.py +4 -0
  8. package/src/devcouncil/app/__init__.py +28 -0
  9. package/src/devcouncil/app/config.py +131 -0
  10. package/src/devcouncil/app/errors.py +23 -0
  11. package/src/devcouncil/app/events.py +44 -0
  12. package/src/devcouncil/app/orchestrator.py +92 -0
  13. package/src/devcouncil/app/run_context.py +39 -0
  14. package/src/devcouncil/app/state_machine.py +108 -0
  15. package/src/devcouncil/artifacts/__init__.py +1 -0
  16. package/src/devcouncil/artifacts/coverage.py +96 -0
  17. package/src/devcouncil/artifacts/graph.py +143 -0
  18. package/src/devcouncil/artifacts/migrations.py +20 -0
  19. package/src/devcouncil/artifacts/schemas.py +23 -0
  20. package/src/devcouncil/artifacts/serializer.py +21 -0
  21. package/src/devcouncil/artifacts/validators.py +27 -0
  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 +48 -0
  25. package/src/devcouncil/cli/commands/baseline.py +32 -0
  26. package/src/devcouncil/cli/commands/config.py +54 -0
  27. package/src/devcouncil/cli/commands/doctor.py +96 -0
  28. package/src/devcouncil/cli/commands/hook.py +61 -0
  29. package/src/devcouncil/cli/commands/init.py +142 -0
  30. package/src/devcouncil/cli/commands/integrate.py +420 -0
  31. package/src/devcouncil/cli/commands/map.py +38 -0
  32. package/src/devcouncil/cli/commands/mcp_server.py +18 -0
  33. package/src/devcouncil/cli/commands/plan.py +276 -0
  34. package/src/devcouncil/cli/commands/prompt.py +47 -0
  35. package/src/devcouncil/cli/commands/repair.py +69 -0
  36. package/src/devcouncil/cli/commands/report.py +71 -0
  37. package/src/devcouncil/cli/commands/reset_demo_state.py +28 -0
  38. package/src/devcouncil/cli/commands/rollback.py +58 -0
  39. package/src/devcouncil/cli/commands/run.py +224 -0
  40. package/src/devcouncil/cli/commands/setup.py +82 -0
  41. package/src/devcouncil/cli/commands/show.py +57 -0
  42. package/src/devcouncil/cli/commands/status.py +105 -0
  43. package/src/devcouncil/cli/commands/tasks.py +41 -0
  44. package/src/devcouncil/cli/commands/trace.py +43 -0
  45. package/src/devcouncil/cli/commands/verify.py +163 -0
  46. package/src/devcouncil/cli/commands/version.py +20 -0
  47. package/src/devcouncil/cli/main.py +70 -0
  48. package/src/devcouncil/council/__init__.py +0 -0
  49. package/src/devcouncil/council/prompts/__init__.py +0 -0
  50. package/src/devcouncil/council/prompts/arbiter.md +19 -0
  51. package/src/devcouncil/council/prompts/critic_a.md +10 -0
  52. package/src/devcouncil/council/prompts/critic_b.md +10 -0
  53. package/src/devcouncil/council/prompts/implementation_reviewer.md +16 -0
  54. package/src/devcouncil/council/prompts/planner_a.md +16 -0
  55. package/src/devcouncil/council/prompts/planner_b.md +16 -0
  56. package/src/devcouncil/council/prompts/rebuttal.md +10 -0
  57. package/src/devcouncil/council/prompts/spec_writer.md +12 -0
  58. package/src/devcouncil/domain/__init__.py +0 -0
  59. package/src/devcouncil/domain/assumption.py +17 -0
  60. package/src/devcouncil/domain/critique.py +32 -0
  61. package/src/devcouncil/domain/evidence.py +27 -0
  62. package/src/devcouncil/domain/gap.py +26 -0
  63. package/src/devcouncil/domain/requirement.py +22 -0
  64. package/src/devcouncil/domain/task.py +26 -0
  65. package/src/devcouncil/execution/__init__.py +1 -0
  66. package/src/devcouncil/execution/context_builder.py +60 -0
  67. package/src/devcouncil/execution/executor.py +15 -0
  68. package/src/devcouncil/execution/hook_policy.py +144 -0
  69. package/src/devcouncil/execution/patch.py +28 -0
  70. package/src/devcouncil/execution/paths.py +14 -0
  71. package/src/devcouncil/execution/permissions.py +92 -0
  72. package/src/devcouncil/execution/prompt_builder.py +59 -0
  73. package/src/devcouncil/execution/task_runner.py +166 -0
  74. package/src/devcouncil/executors/__init__.py +1 -0
  75. package/src/devcouncil/executors/mini_swe.py +73 -0
  76. package/src/devcouncil/executors/native/__init__.py +0 -0
  77. package/src/devcouncil/executors/native/agent.py +107 -0
  78. package/src/devcouncil/executors/openhands.py +71 -0
  79. package/src/devcouncil/gating/__init__.py +1 -0
  80. package/src/devcouncil/gating/checks/__init__.py +0 -0
  81. package/src/devcouncil/gating/checks/clean_git.py +45 -0
  82. package/src/devcouncil/gating/checks/planned_files_check.py +32 -0
  83. package/src/devcouncil/gating/checks/requirement_coverage.py +26 -0
  84. package/src/devcouncil/gating/checks/secret_scan_check.py +34 -0
  85. package/src/devcouncil/gating/policy.py +190 -0
  86. package/src/devcouncil/indexing/__init__.py +1 -0
  87. package/src/devcouncil/indexing/graph_index.py +48 -0
  88. package/src/devcouncil/indexing/repo_mapper.py +204 -0
  89. package/src/devcouncil/indexing/symbol_index.py +0 -0
  90. package/src/devcouncil/integrations/code_review_graph.py +163 -0
  91. package/src/devcouncil/integrations/github.py +39 -0
  92. package/src/devcouncil/integrations/gitnexus.py +27 -0
  93. package/src/devcouncil/integrations/graphify.py +34 -0
  94. package/src/devcouncil/integrations/mcp/__init__.py +0 -0
  95. package/src/devcouncil/integrations/mcp/server.py +146 -0
  96. package/src/devcouncil/llm/__init__.py +1 -0
  97. package/src/devcouncil/llm/cache.py +38 -0
  98. package/src/devcouncil/llm/provider.py +125 -0
  99. package/src/devcouncil/llm/router.py +125 -0
  100. package/src/devcouncil/planning/__init__.py +1 -0
  101. package/src/devcouncil/planning/arbiter_service.py +57 -0
  102. package/src/devcouncil/planning/critique_service.py +66 -0
  103. package/src/devcouncil/planning/plan_service.py +46 -0
  104. package/src/devcouncil/planning/repair_service.py +39 -0
  105. package/src/devcouncil/planning/spec_service.py +44 -0
  106. package/src/devcouncil/repo/__init__.py +0 -0
  107. package/src/devcouncil/reporting/__init__.py +0 -0
  108. package/src/devcouncil/reporting/github_check.py +32 -0
  109. package/src/devcouncil/reporting/json_report.py +17 -0
  110. package/src/devcouncil/reporting/markdown_report.py +46 -0
  111. package/src/devcouncil/reporting/report_builder.py +14 -0
  112. package/src/devcouncil/storage/__init__.py +0 -0
  113. package/src/devcouncil/storage/db.py +66 -0
  114. package/src/devcouncil/storage/models.py +83 -0
  115. package/src/devcouncil/storage/repositories.py +346 -0
  116. package/src/devcouncil/telemetry/__init__.py +0 -0
  117. package/src/devcouncil/telemetry/cost.py +34 -0
  118. package/src/devcouncil/telemetry/traces.py +91 -0
  119. package/src/devcouncil/telemetry/tracker.py +49 -0
  120. package/src/devcouncil/utils/__init__.py +1 -0
  121. package/src/devcouncil/utils/redaction.py +141 -0
  122. package/src/devcouncil/verification/__init__.py +1 -0
  123. package/src/devcouncil/verification/implementation_reviewer.py +55 -0
  124. package/src/devcouncil/verification/verifier.py +513 -0
  125. package/uv.lock +1085 -0
@@ -0,0 +1,224 @@
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.native.agent import NativeAgent
10
+ from devcouncil.llm.provider import OpenRouterProvider
11
+ from devcouncil.llm.router import ModelRouter
12
+ from devcouncil.app.config import load_config, get_api_key
13
+ from devcouncil.domain.evidence import CommandResult, DiffEvidence, TestEvidence
14
+ from devcouncil.storage.repositories import GapRepository, EvidenceRepository, StateRepository
15
+ from devcouncil.verification.verifier import Verifier
16
+ from devcouncil.app.state_machine import ProjectPhase
17
+
18
+ console = Console()
19
+
20
+ def _current_changed_files() -> list[str]:
21
+ from devcouncil.verification.verifier import Verifier
22
+
23
+ return Verifier(Path(".")).get_changed_files()
24
+
25
+ def _capture_after_patch(task_id: str):
26
+ """Capture the diff after task execution for use by rollback."""
27
+ try:
28
+ from devcouncil.verification.verifier import Verifier
29
+
30
+ checkpoint_dir = Path(".devcouncil/checkpoints")
31
+ checkpoint_dir.mkdir(exist_ok=True)
32
+ diff = Verifier(Path(".")).get_diff()
33
+ if diff:
34
+ with open(checkpoint_dir / f"{task_id}-after.patch", "w", encoding="utf-8") as f:
35
+ f.write(diff)
36
+ except Exception:
37
+ pass # Non-critical — don't block execution
38
+
39
+ def _capture_before_snapshot(task_id: str):
40
+ checkpoint_dir = Path(".devcouncil/checkpoints")
41
+ checkpoint_dir.mkdir(exist_ok=True)
42
+ snapshot = {
43
+ "task_id": task_id,
44
+ "changed_files": _current_changed_files(),
45
+ }
46
+ (checkpoint_dir / f"{task_id}-before.json").write_text(
47
+ json.dumps(snapshot, indent=2),
48
+ encoding="utf-8",
49
+ )
50
+
51
+ def _record_project_phase(session, phase: ProjectPhase):
52
+ StateRepository(session).record_phase(phase.value)
53
+
54
+ def _verify_after_execution(session, task, reqs, router=None) -> bool:
55
+ """Run deterministic verification after an automated executor finishes."""
56
+ import asyncio
57
+
58
+ verifier = Verifier(Path("."), router=router)
59
+ gaps, evidence = asyncio.run(verifier.verify_task(task, reqs))
60
+
61
+ gap_repo = GapRepository(session)
62
+ evidence_repo = EvidenceRepository(session)
63
+ gap_repo.delete_for_task(task.id)
64
+ evidence_repo.delete_for_task(task.id)
65
+
66
+ for gap in gaps:
67
+ gap_repo.save(gap)
68
+
69
+ for ev in evidence:
70
+ if isinstance(ev, CommandResult):
71
+ evidence_repo.save_command_result(task.id, ev)
72
+ elif isinstance(ev, DiffEvidence):
73
+ evidence_repo.save_diff_evidence(ev)
74
+ elif isinstance(ev, TestEvidence):
75
+ evidence_repo.save_test_evidence(ev, task.id)
76
+
77
+ task.status = "blocked" if any(g.blocking for g in gaps) else "verified"
78
+ return task.status == "verified"
79
+
80
+ def run(
81
+ task_id: str = typer.Argument(..., help="ID of the task to run"),
82
+ executor: str = typer.Option("manual", "--executor", "-e", help="Executor to use (manual, shell)"),
83
+ ):
84
+ """
85
+ Execute a specific task.
86
+ """
87
+ db = get_db()
88
+ if not db:
89
+ console.print("[red]DevCouncil not initialized. Run 'dev init' first.[/red]")
90
+ return
91
+
92
+ with db.get_session() as session:
93
+ task_repo = TaskRepository(session)
94
+ task = task_repo.get_by_id(task_id)
95
+ if not task:
96
+ console.print(f"[red]Task {task_id} not found.[/red]")
97
+ return
98
+
99
+ from devcouncil.gating.policy import GatePolicy
100
+ policy = GatePolicy()
101
+ gate_result = policy.check_task_ready(task, Path("."))
102
+ if not gate_result.passed:
103
+ console.print(f"[red]Task {task_id} is not ready for execution.[/red]")
104
+ for gap in gate_result.gaps:
105
+ if gap.blocking:
106
+ console.print(f" - [red][BLOCKING][/red] {gap.description} (Fix: {gap.recommended_fix})")
107
+ return
108
+
109
+ console.print(f"Running task [bold]{task_id}[/bold] using [bold]{executor}[/bold] executor...")
110
+
111
+ # 1. Create Git checkpoint
112
+ try:
113
+ from devcouncil.verification.verifier import Verifier
114
+
115
+ checkpoint_dir = Path(".devcouncil/checkpoints")
116
+ checkpoint_dir.mkdir(exist_ok=True)
117
+ _capture_before_snapshot(task_id)
118
+ diff = Verifier(Path(".")).get_diff()
119
+ if diff:
120
+ with open(checkpoint_dir / f"{task_id}-before.patch", "w", encoding="utf-8") as f:
121
+ f.write(diff)
122
+ console.print(f"Created git checkpoint at {checkpoint_dir}/{task_id}-before.patch")
123
+ except Exception as e:
124
+ console.print(f"[yellow]Warning: Failed to create git checkpoint: {e}[/yellow]")
125
+
126
+ if executor == "manual":
127
+ _record_project_phase(session, ProjectPhase.TASK_EXECUTING)
128
+ task.status = "running"
129
+ task_repo.save(task)
130
+ console.print(f"\n[green]Task {task_id} is now marked as RUNNING.[/green]")
131
+ console.print("Use 'dev prompt TASK-ID' to get the prompt for this task.")
132
+ console.print("When finished, use 'dev verify TASK-ID' to check the results.")
133
+ elif executor == "mini":
134
+ _record_project_phase(session, ProjectPhase.TASK_EXECUTING)
135
+ req_repo = RequirementRepository(session)
136
+ reqs = req_repo.get_all()
137
+ mini_executor = MiniSWEExecutor(Path("."))
138
+ exec_result = mini_executor.run_task(task, reqs)
139
+ _capture_after_patch(task_id)
140
+ if exec_result.success:
141
+ _record_project_phase(session, ProjectPhase.TASK_VERIFYING)
142
+ verified = _verify_after_execution(session, task, reqs)
143
+ _record_project_phase(
144
+ session,
145
+ ProjectPhase.TASK_VERIFIED if verified else ProjectPhase.TASK_BLOCKED,
146
+ )
147
+ task_repo.save(task)
148
+ if verified:
149
+ console.print(f"\n[green]mini-SWE-agent finished and task {task_id} verified.[/green]")
150
+ else:
151
+ console.print(f"\n[yellow]mini-SWE-agent finished, but task {task_id} is blocked by verification gaps.[/yellow]")
152
+ else:
153
+ console.print("\n[red]mini-SWE-agent failed to start or execute.[/red]")
154
+ elif executor == "openhands":
155
+ _record_project_phase(session, ProjectPhase.TASK_EXECUTING)
156
+ req_repo = RequirementRepository(session)
157
+ reqs = req_repo.get_all()
158
+ oh_executor = OpenHandsExecutor(Path("."))
159
+ exec_result = oh_executor.run_task(task, reqs)
160
+ _capture_after_patch(task_id)
161
+ if exec_result.success:
162
+ _record_project_phase(session, ProjectPhase.TASK_VERIFYING)
163
+ verified = _verify_after_execution(session, task, reqs)
164
+ _record_project_phase(
165
+ session,
166
+ ProjectPhase.TASK_VERIFIED if verified else ProjectPhase.TASK_BLOCKED,
167
+ )
168
+ task_repo.save(task)
169
+ if verified:
170
+ console.print(f"\n[green]OpenHands finished and task {task_id} verified.[/green]")
171
+ else:
172
+ console.print(f"\n[yellow]OpenHands finished, but task {task_id} is blocked by verification gaps.[/yellow]")
173
+ else:
174
+ console.print("\n[red]OpenHands failed to start or execute.[/red]")
175
+ elif executor == "native":
176
+ # Load config for model routing and permissions
177
+ try:
178
+ config = load_config(Path("."))
179
+ api_key = get_api_key(config.models.provider)
180
+ except (FileNotFoundError, ValueError) as e:
181
+ console.print(f"[red]{e}[/red]")
182
+ return
183
+
184
+ provider = OpenRouterProvider(api_key)
185
+ role_config = {name: role.model_dump() for name, role in config.models.roles.items()}
186
+ router = ModelRouter(provider, role_config)
187
+
188
+ # Setup Permission System
189
+ from devcouncil.execution.permissions import PermissionPolicy, PermissionManager
190
+ from devcouncil.execution.task_runner import TaskRunner
191
+
192
+ # Populate policy from config commands
193
+ allowed_cmds = config.commands.test + config.commands.lint + config.commands.typecheck
194
+ policy = PermissionPolicy(
195
+ allowed_shell_commands=allowed_cmds,
196
+ )
197
+ perm_manager = PermissionManager(policy, Path("."))
198
+ task_runner = TaskRunner(Path("."), perm_manager)
199
+
200
+ req_repo = RequirementRepository(session)
201
+ reqs = req_repo.get_all()
202
+
203
+ import asyncio
204
+ _record_project_phase(session, ProjectPhase.TASK_EXECUTING)
205
+ agent = NativeAgent(router, task_runner)
206
+ exec_result = asyncio.run(agent.run_task(task, reqs))
207
+ _capture_after_patch(task_id)
208
+
209
+ if exec_result.success:
210
+ _record_project_phase(session, ProjectPhase.TASK_VERIFYING)
211
+ verified = _verify_after_execution(session, task, reqs, router=router)
212
+ _record_project_phase(
213
+ session,
214
+ ProjectPhase.TASK_VERIFIED if verified else ProjectPhase.TASK_BLOCKED,
215
+ )
216
+ task_repo.save(task)
217
+ if verified:
218
+ console.print(f"\n[green]Native agent finished and task {task_id} verified.[/green]")
219
+ else:
220
+ console.print(f"\n[yellow]Native agent finished, but task {task_id} is blocked by verification gaps.[/yellow]")
221
+ else:
222
+ console.print("\n[red]Native agent failed during execution.[/red]")
223
+ else:
224
+ console.print(f"[red]Executor {executor} not yet implemented.[/red]")
@@ -0,0 +1,82 @@
1
+ from pathlib import Path
2
+ import shutil
3
+
4
+ import typer
5
+ from rich.console import Console
6
+ from rich.panel import Panel
7
+
8
+ from devcouncil.cli.commands.doctor import render_doctor_check
9
+ from devcouncil.cli.commands.init import initialize_project
10
+ from devcouncil.cli.commands.integrate import _codex_command, _configure, _gemini_command
11
+
12
+ app = typer.Typer()
13
+ console = Console()
14
+
15
+
16
+ @app.callback(invoke_without_command=True)
17
+ def setup(
18
+ ctx: typer.Context,
19
+ project_root: Path = typer.Option(
20
+ Path("."),
21
+ "--project-root",
22
+ help="Target project repository root. Defaults to the terminal's current directory.",
23
+ ),
24
+ name: str | None = typer.Option(None, "--name", "-n", help="Project name for .devcouncil/config.yaml."),
25
+ integrate: bool = typer.Option(False, "--integrate", help="Configure supported coding CLI MCP integrations."),
26
+ apply: bool = typer.Option(False, "--apply", help="Apply integration config instead of previewing commands."),
27
+ gemini_scope: str = typer.Option("project", "--gemini-scope", help="Gemini MCP config scope: project or user."),
28
+ ):
29
+ """
30
+ Initialize DevCouncil from a normal terminal in the target repository root.
31
+
32
+ Use the coding CLI later only for the generated dev prompt output.
33
+ """
34
+ if ctx.invoked_subcommand is not None:
35
+ return
36
+
37
+ if gemini_scope not in {"project", "user"}:
38
+ console.print("[red]--gemini-scope must be 'project' or 'user'.[/red]")
39
+ raise typer.Exit(code=2)
40
+
41
+ root = project_root.expanduser().resolve()
42
+ created = initialize_project(root, project_name=name)
43
+ if not created:
44
+ console.print(f"[yellow]DevCouncil is already initialized at {root / '.devcouncil'}.[/yellow]")
45
+
46
+ console.print()
47
+ render_doctor_check()
48
+
49
+ if integrate:
50
+ console.print()
51
+ console.print("[bold]Coding CLI integration[/bold]")
52
+ commands = [
53
+ ("Codex CLI", _codex_command(root)),
54
+ ("Gemini CLI", _gemini_command(root, gemini_scope)),
55
+ ]
56
+ results = []
57
+ for tool, command in commands:
58
+ if apply and not shutil.which(command[0]):
59
+ console.print(f"[yellow]{tool} CLI not found on PATH. Skipping optional integration.[/yellow]")
60
+ continue
61
+ results.append(_configure(tool, command, apply))
62
+ if apply and any(not ok for ok in results):
63
+ raise typer.Exit(code=1)
64
+
65
+ console.print()
66
+ console.print(Panel.fit(
67
+ "\n".join([
68
+ "[bold]Next commands[/bold]",
69
+ f"Keep running DevCouncil commands in this terminal at: {root}",
70
+ "dev plan \"Describe the implementation goal\"",
71
+ "dev tasks",
72
+ "dev run TASK-001 --executor manual",
73
+ "dev prompt TASK-001",
74
+ "Paste only the dev prompt output into your coding CLI.",
75
+ "dev verify TASK-001",
76
+ "",
77
+ "Use [bold]dev setup --integrate[/bold] to preview coding CLI MCP setup.",
78
+ "Use [bold]dev setup --integrate --apply[/bold] to configure detected clients.",
79
+ ]),
80
+ title="DevCouncil is ready",
81
+ border_style="green",
82
+ ))
@@ -0,0 +1,57 @@
1
+ import typer
2
+ from rich.console import Console
3
+ from rich.panel import Panel
4
+ from devcouncil.storage.db import get_db
5
+ from devcouncil.storage.repositories import TaskRepository, RequirementRepository
6
+
7
+ app = typer.Typer()
8
+ console = Console()
9
+
10
+ @app.callback(invoke_without_command=True)
11
+ def show(
12
+ ctx: typer.Context,
13
+ task_id: str = typer.Argument(..., help="ID of the task to show"),
14
+ ):
15
+ """
16
+ Show details of a specific task.
17
+ """
18
+ if ctx.invoked_subcommand is not None:
19
+ return
20
+
21
+ db = get_db()
22
+ if not db:
23
+ console.print("[red]DevCouncil not initialized. Run 'dev init' first.[/red]")
24
+ raise typer.Exit(code=1)
25
+
26
+ with db.get_session() as session:
27
+ task_repo = TaskRepository(session)
28
+ req_repo = RequirementRepository(session)
29
+
30
+ task = task_repo.get_by_id(task_id)
31
+ if not task:
32
+ console.print(f"[red]Task {task_id} not found.[/red]")
33
+ raise typer.Exit(code=1)
34
+
35
+ reqs = req_repo.get_all()
36
+ req_map = {r.id: r for r in reqs}
37
+
38
+ output = f"[bold]Status:[/bold] {task.status}\n\n"
39
+ output += f"[bold]Description:[/bold]\n{task.description}\n\n"
40
+
41
+ output += "[bold]Linked Requirements:[/bold]\n"
42
+ for req_id in task.requirement_ids:
43
+ req = req_map.get(req_id)
44
+ if req:
45
+ output += f" - [cyan]{req.id}[/cyan]: {req.title}\n"
46
+ else:
47
+ output += f" - [cyan]{req_id}[/cyan]: (Requirement not found)\n"
48
+
49
+ output += "\n[bold]Planned Files:[/bold]\n"
50
+ for pf in task.planned_files:
51
+ output += f" - {pf.path} ({pf.allowed_change}): {pf.reason}\n"
52
+
53
+ output += "\n[bold]Expected Tests:[/bold]\n"
54
+ for et in task.expected_tests:
55
+ output += f" - {et}\n"
56
+
57
+ console.print(Panel(output, title=f"Task {task.id}: {task.title}", expand=False))
@@ -0,0 +1,105 @@
1
+ from rich.console import Console
2
+ from rich.table import Table
3
+ from rich.panel import Panel
4
+ from pathlib import Path
5
+ import json
6
+ from devcouncil.storage.db import get_db
7
+ from devcouncil.storage.repositories import ArtifactGraphRepository
8
+ from devcouncil.telemetry.cost import CostEstimator
9
+
10
+ console = Console()
11
+
12
+ def status():
13
+ """
14
+ Show the current status of the DevCouncil project.
15
+ """
16
+ db = get_db()
17
+ if not db:
18
+ console.print("[yellow]DevCouncil not initialized in this directory.[/yellow]")
19
+ console.print("Run [bold]dev init[/bold] to get started.")
20
+ return
21
+
22
+ with db.get_session() as session:
23
+ graph_repo = ArtifactGraphRepository(session)
24
+ graph = graph_repo.load_graph()
25
+ summary = graph.coverage_summary()
26
+
27
+ reqs = list(graph.requirements.values())
28
+ tasks = list(graph.tasks.values())
29
+ blocking_gaps = graph.blocking_gaps()
30
+
31
+ # Determine phase
32
+ if not reqs and not tasks:
33
+ phase = "NEW"
34
+ elif reqs and not tasks:
35
+ phase = "REQUIREMENTS_DRAFTED"
36
+ elif blocking_gaps:
37
+ phase = "TASK_BLOCKED"
38
+ elif tasks:
39
+ statuses = {t.status for t in tasks}
40
+ if "running" in statuses:
41
+ phase = "TASK_EXECUTING"
42
+ elif "blocked" in statuses:
43
+ phase = "TASK_BLOCKED"
44
+ elif all(s in ("verified", "done") for s in statuses):
45
+ phase = "PROJECT_DONE"
46
+ else:
47
+ phase = "PLAN_APPROVED"
48
+ else:
49
+ phase = "NEW"
50
+
51
+ # Phase color
52
+ phase_colors = {
53
+ "NEW": "yellow",
54
+ "REQUIREMENTS_DRAFTED": "cyan",
55
+ "PLAN_APPROVED": "green",
56
+ "TASK_EXECUTING": "blue",
57
+ "TASK_BLOCKED": "red",
58
+ "PROJECT_DONE": "green bold",
59
+ }
60
+ phase_color = phase_colors.get(phase, "white")
61
+
62
+ # Calculate cost
63
+ total_cost = 0.0
64
+ log_file = Path(".devcouncil/logs/model_calls.jsonl")
65
+ if log_file.exists():
66
+ with open(log_file, "r", encoding="utf-8") as f:
67
+ for line in f:
68
+ try:
69
+ entry = json.loads(line)
70
+ total_cost += CostEstimator.estimate_cost(
71
+ entry.get("response", {}).get("model", ""),
72
+ entry.get("usage", {})
73
+ )
74
+ except Exception:
75
+ continue
76
+
77
+ console.print(Panel(
78
+ f"[bold]Phase:[/bold] [{phase_color}]{phase}[/{phase_color}]\n"
79
+ f"[bold]Requirements:[/bold] {summary['total_requirements']} ({summary['requirements_without_tasks']} unmapped)\n"
80
+ f"[bold]Tasks:[/bold] {summary['total_tasks']} ({summary['tasks_without_requirements']} orphaned)\n"
81
+ f"[bold]Acceptance Criteria:[/bold] {summary['total_ac']} ({summary['ac_without_evidence']} unverified)\n"
82
+ f"[bold]Gaps:[/bold] {summary['total_gaps']} ({summary['blocking_gaps']} blocking)\n"
83
+ f"[bold]Total Cost:[/bold] ${total_cost:.4f}",
84
+ title="DevCouncil Status",
85
+ expand=False,
86
+ ))
87
+
88
+ if tasks:
89
+ table = Table(title="Task Summary")
90
+ table.add_column("Status", style="magenta")
91
+ table.add_column("Count", justify="right")
92
+
93
+ status_counts: dict[str, int] = {}
94
+ for t in tasks:
95
+ status_counts[t.status] = status_counts.get(t.status, 0) + 1
96
+ for s, count in sorted(status_counts.items()):
97
+ table.add_row(s, str(count))
98
+ console.print(table)
99
+
100
+ if blocking_gaps:
101
+ console.print(f"\n[red bold]WARNING: {len(blocking_gaps)} blocking gap(s) must be resolved:[/red bold]")
102
+ for g in blocking_gaps[:5]:
103
+ console.print(f" - [red]{g.id}[/red]: {g.description[:80]}")
104
+ if len(blocking_gaps) > 5:
105
+ console.print(f" ... and {len(blocking_gaps) - 5} more. Run [bold]dev report[/bold] for details.")
@@ -0,0 +1,41 @@
1
+ import typer
2
+ from rich.console import Console
3
+ from rich.table import Table
4
+ from devcouncil.storage.db import get_db
5
+ from devcouncil.storage.repositories import TaskRepository
6
+
7
+ app = typer.Typer()
8
+ console = Console()
9
+
10
+ @app.callback(invoke_without_command=True)
11
+ def tasks(ctx: typer.Context):
12
+ """
13
+ List task graph and task gate status.
14
+ """
15
+ if ctx.invoked_subcommand is not None:
16
+ return
17
+
18
+ db = get_db()
19
+ if not db:
20
+ console.print("[red]DevCouncil not initialized. Run 'dev init' first.[/red]")
21
+ raise typer.Exit(code=1)
22
+
23
+ with db.get_session() as session:
24
+ task_repo = TaskRepository(session)
25
+ tasks_list = task_repo.get_all()
26
+
27
+ if not tasks_list:
28
+ console.print("No tasks found. Run 'dev plan' to generate tasks.")
29
+ return
30
+
31
+ table = Table(title="DevCouncil Tasks")
32
+ table.add_column("Task ID", style="cyan", no_wrap=True)
33
+ table.add_column("Title", style="white")
34
+ table.add_column("Status", style="magenta")
35
+ table.add_column("Linked Reqs", style="green")
36
+
37
+ for t in tasks_list:
38
+ reqs = ", ".join(t.requirement_ids)
39
+ table.add_row(t.id, t.title, t.status, reqs)
40
+
41
+ console.print(table)
@@ -0,0 +1,43 @@
1
+ import json
2
+ import time
3
+ from pathlib import Path
4
+
5
+ import typer
6
+ from rich.console import Console
7
+
8
+ from devcouncil.telemetry.traces import read_trace_events
9
+
10
+ app = typer.Typer(help="Inspect DevCouncil trace events.")
11
+ console = Console()
12
+
13
+
14
+ @app.command("tail")
15
+ def tail(
16
+ follow: bool = typer.Option(False, "--follow", "-f", help="Continue polling for new events."),
17
+ limit: int = typer.Option(50, "--limit", "-n", help="Maximum events to print before following."),
18
+ jsonl: bool = typer.Option(True, "--jsonl/--pretty", help="Print JSONL or compact text rows."),
19
+ ):
20
+ """Print the DevCouncil trace JSONL stream for replay or debugging."""
21
+ project_root = Path(".")
22
+ printed = 0
23
+
24
+ def emit_new(start_index: int) -> int:
25
+ events = list(read_trace_events(project_root))
26
+ selected = events[start_index:]
27
+ for event in selected:
28
+ if jsonl:
29
+ typer.echo(event.model_dump_json())
30
+ else:
31
+ console.print(
32
+ f"{event.timestamp} {event.type} "
33
+ f"{event.task_id or '-'} {event.summary or json.dumps(event.details)}"
34
+ )
35
+ return len(events)
36
+
37
+ events = list(read_trace_events(project_root))
38
+ start = max(0, len(events) - limit)
39
+ printed = emit_new(start)
40
+
41
+ while follow:
42
+ time.sleep(1)
43
+ printed = emit_new(printed)