devcouncil 0.1.0 → 0.2.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 (190) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +197 -494
  3. package/package.json +9 -2
  4. package/pyproject.toml +62 -27
  5. package/src/devcouncil/__main__.py +4 -4
  6. package/src/devcouncil/app/__init__.py +28 -28
  7. package/src/devcouncil/app/config.py +297 -108
  8. package/src/devcouncil/app/errors.py +23 -23
  9. package/src/devcouncil/app/events.py +44 -44
  10. package/src/devcouncil/app/orchestrator.py +67 -67
  11. package/src/devcouncil/app/project_status.py +29 -0
  12. package/src/devcouncil/app/run_context.py +39 -39
  13. package/src/devcouncil/app/state_machine.py +108 -108
  14. package/src/devcouncil/artifacts/__init__.py +1 -1
  15. package/src/devcouncil/artifacts/coverage.py +96 -96
  16. package/src/devcouncil/artifacts/graph.py +163 -143
  17. package/src/devcouncil/artifacts/migrations.py +20 -20
  18. package/src/devcouncil/artifacts/schemas.py +23 -23
  19. package/src/devcouncil/artifacts/serializer.py +21 -21
  20. package/src/devcouncil/artifacts/validators.py +27 -27
  21. package/src/devcouncil/assets/__init__.py +1 -0
  22. package/src/devcouncil/assets/devcouncil-logo.svg +60 -0
  23. package/src/devcouncil/assets/devcouncil_logo_premium.png +0 -0
  24. package/src/devcouncil/cli/commands/agents.py +292 -0
  25. package/src/devcouncil/cli/commands/artifacts.py +54 -48
  26. package/src/devcouncil/cli/commands/ast.py +22 -0
  27. package/src/devcouncil/cli/commands/baseline.py +35 -32
  28. package/src/devcouncil/cli/commands/check.py +209 -0
  29. package/src/devcouncil/cli/commands/config.py +115 -54
  30. package/src/devcouncil/cli/commands/cost.py +57 -0
  31. package/src/devcouncil/cli/commands/dashboard.py +31 -0
  32. package/src/devcouncil/cli/commands/doctor.py +291 -47
  33. package/src/devcouncil/cli/commands/evidence.py +48 -0
  34. package/src/devcouncil/cli/commands/go.py +656 -0
  35. package/src/devcouncil/cli/commands/handoff.py +69 -0
  36. package/src/devcouncil/cli/commands/hook.py +209 -33
  37. package/src/devcouncil/cli/commands/init.py +204 -57
  38. package/src/devcouncil/cli/commands/integrate.py +1171 -76
  39. package/src/devcouncil/cli/commands/lsp.py +20 -0
  40. package/src/devcouncil/cli/commands/map.py +96 -22
  41. package/src/devcouncil/cli/commands/plan.py +422 -210
  42. package/src/devcouncil/cli/commands/prompt.py +48 -34
  43. package/src/devcouncil/cli/commands/repair.py +89 -69
  44. package/src/devcouncil/cli/commands/report.py +120 -54
  45. package/src/devcouncil/cli/commands/reset_demo_state.py +33 -28
  46. package/src/devcouncil/cli/commands/rollback.py +55 -54
  47. package/src/devcouncil/cli/commands/run.py +285 -220
  48. package/src/devcouncil/cli/commands/runs.py +223 -0
  49. package/src/devcouncil/cli/commands/scaffold.py +32 -0
  50. package/src/devcouncil/cli/commands/semantic.py +47 -0
  51. package/src/devcouncil/cli/commands/setup.py +300 -20
  52. package/src/devcouncil/cli/commands/shell.py +73 -0
  53. package/src/devcouncil/cli/commands/show.py +76 -57
  54. package/src/devcouncil/cli/commands/skills.py +88 -0
  55. package/src/devcouncil/cli/commands/status.py +141 -105
  56. package/src/devcouncil/cli/commands/tasks.py +55 -41
  57. package/src/devcouncil/cli/commands/trace.py +49 -4
  58. package/src/devcouncil/cli/commands/verify.py +293 -128
  59. package/src/devcouncil/cli/commands/version.py +20 -20
  60. package/src/devcouncil/cli/commands/watch.py +574 -0
  61. package/src/devcouncil/cli/commands/watch_fs.py +40 -0
  62. package/src/devcouncil/cli/main.py +92 -25
  63. package/src/devcouncil/council/prompts/arbiter.md +19 -19
  64. package/src/devcouncil/council/prompts/critic_a.md +10 -10
  65. package/src/devcouncil/council/prompts/critic_b.md +10 -10
  66. package/src/devcouncil/council/prompts/implementation_reviewer.md +16 -16
  67. package/src/devcouncil/council/prompts/planner_a.md +16 -16
  68. package/src/devcouncil/council/prompts/planner_b.md +16 -16
  69. package/src/devcouncil/council/prompts/rebuttal.md +10 -10
  70. package/src/devcouncil/council/prompts/spec_writer.md +12 -12
  71. package/src/devcouncil/domain/assumption.py +17 -17
  72. package/src/devcouncil/domain/critique.py +32 -32
  73. package/src/devcouncil/domain/evidence.py +47 -27
  74. package/src/devcouncil/domain/gap.py +52 -26
  75. package/src/devcouncil/domain/requirement.py +22 -22
  76. package/src/devcouncil/domain/task.py +55 -26
  77. package/src/devcouncil/execution/__init__.py +1 -1
  78. package/src/devcouncil/execution/checkpoints.py +246 -0
  79. package/src/devcouncil/execution/context_builder.py +54 -54
  80. package/src/devcouncil/execution/executor.py +15 -15
  81. package/src/devcouncil/execution/fs_watcher.py +180 -0
  82. package/src/devcouncil/execution/handoff.py +102 -0
  83. package/src/devcouncil/execution/hook_policy.py +186 -77
  84. package/src/devcouncil/execution/patch.py +77 -28
  85. package/src/devcouncil/execution/permissions.py +52 -59
  86. package/src/devcouncil/execution/policy_engine.py +343 -0
  87. package/src/devcouncil/execution/prompt_builder.py +650 -38
  88. package/src/devcouncil/execution/shell_session.py +225 -0
  89. package/src/devcouncil/execution/task_runner.py +68 -64
  90. package/src/devcouncil/executors/__init__.py +1 -1
  91. package/src/devcouncil/executors/agent_registry.py +575 -0
  92. package/src/devcouncil/executors/coding_cli.py +736 -0
  93. package/src/devcouncil/executors/mini_swe.py +63 -63
  94. package/src/devcouncil/executors/native/agent.py +186 -85
  95. package/src/devcouncil/executors/openhands.py +56 -56
  96. package/src/devcouncil/gating/__init__.py +1 -1
  97. package/src/devcouncil/gating/checks/clean_git.py +52 -45
  98. package/src/devcouncil/gating/checks/planned_files_check.py +32 -32
  99. package/src/devcouncil/gating/checks/requirement_coverage.py +26 -26
  100. package/src/devcouncil/gating/checks/secret_scan_check.py +53 -34
  101. package/src/devcouncil/gating/policy.py +315 -167
  102. package/src/devcouncil/hardware.py +184 -0
  103. package/src/devcouncil/indexing/__init__.py +1 -1
  104. package/src/devcouncil/indexing/ast_matcher.py +168 -0
  105. package/src/devcouncil/indexing/graph_index.py +48 -48
  106. package/src/devcouncil/indexing/lsp.py +161 -0
  107. package/src/devcouncil/indexing/repo_mapper.py +1455 -204
  108. package/src/devcouncil/indexing/semantic_index.py +205 -0
  109. package/src/devcouncil/integrations/actions.py +146 -0
  110. package/src/devcouncil/integrations/check.py +423 -0
  111. package/src/devcouncil/integrations/github.py +35 -35
  112. package/src/devcouncil/integrations/github_intent.py +142 -0
  113. package/src/devcouncil/integrations/gitnexus.py +62 -27
  114. package/src/devcouncil/integrations/graphify.py +34 -34
  115. package/src/devcouncil/integrations/mcp/server.py +2072 -96
  116. package/src/devcouncil/integrations/opencode_devcouncil_plugin.mjs +24 -0
  117. package/src/devcouncil/integrations/pr_comments.py +62 -0
  118. package/src/devcouncil/live/__init__.py +2 -0
  119. package/src/devcouncil/live/cards.py +349 -0
  120. package/src/devcouncil/live/models.py +63 -0
  121. package/src/devcouncil/live/repair_prompt.py +83 -0
  122. package/src/devcouncil/live/reviewer.py +70 -0
  123. package/src/devcouncil/live/signals.py +135 -0
  124. package/src/devcouncil/live/summary.py +34 -0
  125. package/src/devcouncil/live/tasks.py +18 -0
  126. package/src/devcouncil/live/transcripts.py +141 -0
  127. package/src/devcouncil/llm/__init__.py +1 -1
  128. package/src/devcouncil/llm/cache.py +42 -38
  129. package/src/devcouncil/llm/model_defaults.yaml +44 -0
  130. package/src/devcouncil/llm/provider.py +627 -125
  131. package/src/devcouncil/llm/router.py +303 -118
  132. package/src/devcouncil/optimization/__init__.py +1 -0
  133. package/src/devcouncil/optimization/gepa_agent.py +318 -0
  134. package/src/devcouncil/planning/__init__.py +1 -1
  135. package/src/devcouncil/planning/arbiter_service.py +57 -57
  136. package/src/devcouncil/planning/correction_manifest.py +303 -0
  137. package/src/devcouncil/planning/critique_service.py +71 -66
  138. package/src/devcouncil/planning/plan_service.py +60 -46
  139. package/src/devcouncil/planning/prompt_enhancer_service.py +167 -0
  140. package/src/devcouncil/planning/repair_service.py +39 -39
  141. package/src/devcouncil/planning/spec_service.py +70 -44
  142. package/src/devcouncil/repo/ci_scaffold.py +157 -0
  143. package/src/devcouncil/repo/gitignore.py +123 -0
  144. package/src/devcouncil/repo/sca.py +374 -0
  145. package/src/devcouncil/reporting/github_check.py +32 -32
  146. package/src/devcouncil/reporting/json_report.py +30 -17
  147. package/src/devcouncil/reporting/markdown_report.py +83 -46
  148. package/src/devcouncil/reporting/report_builder.py +14 -14
  149. package/src/devcouncil/skills/__init__.py +19 -0
  150. package/src/devcouncil/skills/library/README.md +46 -0
  151. package/src/devcouncil/skills/library/ai-training.md +50 -0
  152. package/src/devcouncil/skills/library/android.md +50 -0
  153. package/src/devcouncil/skills/library/backend.md +52 -0
  154. package/src/devcouncil/skills/library/core-engineering.md +95 -0
  155. package/src/devcouncil/skills/library/data-engineering.md +47 -0
  156. package/src/devcouncil/skills/library/desktop.md +46 -0
  157. package/src/devcouncil/skills/library/devops.md +48 -0
  158. package/src/devcouncil/skills/library/game-dev.md +46 -0
  159. package/src/devcouncil/skills/library/ios.md +48 -0
  160. package/src/devcouncil/skills/library/mobile-cross-platform.md +46 -0
  161. package/src/devcouncil/skills/library/security.md +48 -0
  162. package/src/devcouncil/skills/library/systems.md +48 -0
  163. package/src/devcouncil/skills/library/web.md +47 -0
  164. package/src/devcouncil/skills/library/windows.md +47 -0
  165. package/src/devcouncil/skills/registry.py +330 -0
  166. package/src/devcouncil/storage/db.py +147 -66
  167. package/src/devcouncil/storage/models.py +204 -83
  168. package/src/devcouncil/storage/native.py +557 -0
  169. package/src/devcouncil/storage/repositories.py +388 -249
  170. package/src/devcouncil/telemetry/cost.py +140 -34
  171. package/src/devcouncil/telemetry/model_pricing.yaml +48 -0
  172. package/src/devcouncil/telemetry/pricing.py +28 -0
  173. package/src/devcouncil/telemetry/traces.py +62 -7
  174. package/src/devcouncil/telemetry/tracker.py +52 -49
  175. package/src/devcouncil/ui/__init__.py +1 -0
  176. package/src/devcouncil/ui/dashboard.py +423 -0
  177. package/src/devcouncil/utils/__init__.py +1 -1
  178. package/src/devcouncil/utils/redaction.py +147 -141
  179. package/src/devcouncil/utils/subprocess_env.py +69 -0
  180. package/src/devcouncil/verification/__init__.py +1 -1
  181. package/src/devcouncil/verification/acceptance_compiler.py +125 -0
  182. package/src/devcouncil/verification/ad_hoc_check.py +129 -0
  183. package/src/devcouncil/verification/diff_coverage.py +353 -0
  184. package/src/devcouncil/verification/implementation_reviewer.py +55 -55
  185. package/src/devcouncil/verification/next_actions.py +189 -0
  186. package/src/devcouncil/verification/sandbox.py +178 -0
  187. package/src/devcouncil/verification/test_resolver.py +91 -0
  188. package/src/devcouncil/verification/verifier.py +1342 -307
  189. package/uv.lock +205 -64
  190. package/src/devcouncil/indexing/symbol_index.py +0 -0
@@ -1,47 +1,61 @@
1
- import typer
2
- from rich.console import Console
3
- from rich.markdown import Markdown
4
- from devcouncil.storage.db import get_db
5
- from devcouncil.storage.repositories import TaskRepository, RequirementRepository
1
+ import json
2
+ from typing import NoReturn
3
+ import typer
4
+ from rich.console import Console
5
+ from rich.markdown import Markdown
6
+ from devcouncil.cli.commands.init import initialize_project
7
+ from devcouncil.storage.db import get_db
8
+ from devcouncil.storage.repositories import TaskRepository, RequirementRepository
6
9
  from pathlib import Path
7
10
 
8
11
  from devcouncil.execution.prompt_builder import PromptBuilder
9
-
10
- app = typer.Typer()
11
- console = Console()
12
-
13
- @app.callback(invoke_without_command=True)
12
+
13
+ app = typer.Typer()
14
+ console = Console()
15
+
16
+ @app.callback(invoke_without_command=True)
14
17
  def prompt(
15
18
  ctx: typer.Context,
16
19
  task_id: str = typer.Argument(..., help="ID of the task to generate a prompt for"),
17
20
  pretty: bool = typer.Option(False, "--pretty", help="Render the prompt for terminal reading."),
21
+ json_format: bool = typer.Option(False, "--json", help="Output machine-readable JSON: {ok, task_id, prompt}."),
22
+ project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
18
23
  ):
19
- """
20
- Generate a constrained prompt for a specific task.
21
- """
22
- if ctx.invoked_subcommand is not None:
23
- return
24
-
25
- db = get_db()
26
- if not db:
27
- console.print("[red]DevCouncil not initialized. Run 'dev init' first.[/red]")
28
- raise typer.Exit(code=1)
29
-
30
- with db.get_session() as session:
31
- task_repo = TaskRepository(session)
32
- req_repo = RequirementRepository(session)
33
-
34
- task = task_repo.get_by_id(task_id)
35
- if not task:
36
- console.print(f"[red]Task {task_id} not found.[/red]")
37
- raise typer.Exit(code=1)
38
-
39
- reqs = req_repo.get_all()
40
-
41
- builder = PromptBuilder(Path("."))
24
+ """
25
+ Generate a constrained prompt for a specific task.
26
+ """
27
+ if ctx.invoked_subcommand is not None:
28
+ return
29
+
30
+ def _fail(message: str) -> NoReturn:
31
+ if json_format:
32
+ typer.echo(json.dumps({"ok": False, "task_id": task_id, "error": message}, indent=2))
33
+ else:
34
+ console.print(f"[red]{message}[/red]")
35
+ raise typer.Exit(code=1)
36
+
37
+ root = project_root.expanduser().resolve()
38
+ initialize_project(root, quiet=True)
39
+ db = get_db(root)
40
+ if not db:
41
+ _fail("DevCouncil state is unavailable in this directory.")
42
+
43
+ with db.get_session() as session:
44
+ task_repo = TaskRepository(session)
45
+ req_repo = RequirementRepository(session)
46
+
47
+ task = task_repo.get_by_id(task_id)
48
+ if not task:
49
+ _fail(f"Task {task_id} not found.")
50
+
51
+ reqs = req_repo.get_all()
52
+
53
+ builder = PromptBuilder(root)
42
54
  task_prompt = builder.build_task_prompt(task, reqs)
43
55
 
44
- if pretty:
56
+ if json_format:
57
+ typer.echo(json.dumps({"ok": True, "task_id": task_id, "prompt": task_prompt}, indent=2))
58
+ elif pretty:
45
59
  console.print(Markdown(task_prompt))
46
60
  else:
47
61
  typer.echo(task_prompt, nl=not task_prompt.endswith("\n"))
@@ -1,69 +1,89 @@
1
- import typer
2
- from rich.console import Console
3
- from devcouncil.storage.db import get_db
4
- from devcouncil.storage.repositories import TaskRepository, GapRepository
5
- from devcouncil.planning.repair_service import RepairService
6
- from devcouncil.execution.context_builder import ContextBuilder
7
- from devcouncil.llm.provider import OpenRouterProvider
8
- from devcouncil.llm.router import ModelRouter
9
- from devcouncil.app.config import load_config, get_api_key
10
- import asyncio
11
- from pathlib import Path
12
-
13
- app = typer.Typer()
14
- console = Console()
15
-
16
- async def run_repair_flow():
17
- db = get_db()
18
- if not db:
19
- console.print("[red]DevCouncil not initialized. Run 'dev init' first.[/red]")
20
- return
21
-
22
- with db.get_session() as session:
23
- gap_repo = GapRepository(session)
24
- task_repo = TaskRepository(session)
25
-
26
- all_gaps = gap_repo.get_all()
27
- blocking_gaps = [g for g in all_gaps if g.blocking]
28
-
29
- if not blocking_gaps:
30
- console.print("[green]No blocking gaps found. Nothing to repair![/green]")
31
- return
32
-
33
- console.print(f"Found [bold]{len(blocking_gaps)}[/bold] blocking gaps. Orchestrating repair plan...")
34
-
35
- # Load router
36
- try:
37
- config = load_config(Path("."))
38
- api_key = get_api_key(config.models.provider)
39
- except (FileNotFoundError, ValueError) as e:
40
- console.print(f"[red]{e}[/red]")
41
- return
42
-
43
- provider = OpenRouterProvider(api_key)
44
- role_config = {name: role.model_dump() for name, role in config.models.roles.items()}
45
- router = ModelRouter(provider, role_config)
46
- repair_service = RepairService(router)
47
- context_builder = ContextBuilder(Path("."))
48
-
49
- # Build minimal context for repair.
50
- project_context = context_builder.get_structure_summary()
51
-
52
- repair_output = await repair_service.generate_repair_plan(blocking_gaps, str(project_context))
53
-
54
- for task in repair_output.suggested_tasks:
55
- task.id = f"REPAIR-{task.id}"
56
- task_repo.save(task)
57
- console.print(f" - Created intelligent repair task [bold]{task.id}[/bold]: {task.title}")
58
-
59
- console.print(f"\n[green]Successfully generated {len(repair_output.suggested_tasks)} repair tasks.[/green]")
60
-
61
- @app.callback(invoke_without_command=True)
62
- def repair(ctx: typer.Context):
63
- """
64
- Convert blocking gaps into intelligent repair tasks using LLM inference.
65
- """
66
- if ctx.invoked_subcommand is not None:
67
- return
68
-
69
- asyncio.run(run_repair_flow())
1
+ import typer
2
+ from rich.console import Console
3
+ from devcouncil.storage.db import get_db
4
+ from devcouncil.storage.repositories import TaskRepository, GapRepository
5
+ from devcouncil.planning.repair_service import RepairService
6
+ from devcouncil.execution.context_builder import ContextBuilder
7
+ from devcouncil.llm.provider import create_provider, validate_model_provider
8
+ from devcouncil.llm.router import ModelRouter
9
+ from devcouncil.app.config import load_config, get_api_key
10
+ from devcouncil.cli.commands.init import initialize_project
11
+ import asyncio
12
+ from pathlib import Path
13
+
14
+ app = typer.Typer()
15
+ console = Console()
16
+
17
+ async def run_repair_flow(project_root: Path = Path(".")):
18
+ root = project_root.expanduser().resolve()
19
+ initialize_project(root, quiet=True)
20
+ db = get_db(root)
21
+ if not db:
22
+ console.print("[red]DevCouncil state is unavailable in this directory.[/red]")
23
+ return
24
+
25
+ with db.get_session() as session:
26
+ gap_repo = GapRepository(session)
27
+ task_repo = TaskRepository(session)
28
+
29
+ all_gaps = gap_repo.get_all()
30
+ blocking_gaps = [g for g in all_gaps if g.blocking]
31
+
32
+ if not blocking_gaps:
33
+ console.print("[green]No blocking gaps found. Nothing to repair![/green]")
34
+ return
35
+
36
+ console.print(f"Found [bold]{len(blocking_gaps)}[/bold] blocking gaps. Orchestrating repair plan...")
37
+
38
+ # Load router when credentials are available. Correction manifests have a
39
+ # deterministic fallback path, so missing model credentials must not block
40
+ # repair artifact generation.
41
+ repair_service = None
42
+ try:
43
+ config = load_config(root)
44
+ validate_model_provider(config.models.provider)
45
+ api_key = get_api_key(config.models.provider, root)
46
+ except (FileNotFoundError, ValueError) as e:
47
+ console.print(f"[yellow]{e}[/yellow]")
48
+ else:
49
+ provider = create_provider(config.models.provider, api_key, project_root=root)
50
+ role_config = {name: role.model_dump() for name, role in config.models.roles.items()}
51
+ router = ModelRouter(provider, role_config, project_root=root)
52
+ repair_service = RepairService(router)
53
+ context_builder = ContextBuilder(root)
54
+
55
+ # Build minimal context for repair.
56
+ project_context = context_builder.get_structure_summary()
57
+
58
+ from devcouncil.planning.correction_manifest import write_correction_manifest
59
+
60
+ task_ids = {gap.task_id for gap in blocking_gaps if gap.task_id}
61
+ for scoped_task_id in task_ids:
62
+ if scoped_task_id:
63
+ path = write_correction_manifest(root, scoped_task_id, repair_service=repair_service)
64
+ if path:
65
+ console.print(f" - Wrote correction manifest [dim]{path}[/dim]")
66
+
67
+ repair_count = 0
68
+ if repair_service is not None:
69
+ repair_output = await repair_service.generate_repair_plan(blocking_gaps, str(project_context))
70
+ for task in repair_output.suggested_tasks:
71
+ task.id = f"REPAIR-{task.id}"
72
+ task_repo.save(task)
73
+ repair_count += 1
74
+ console.print(f" - Created intelligent repair task [bold]{task.id}[/bold]: {task.title}")
75
+
76
+ console.print(f"\n[green]Successfully generated {repair_count} repair tasks.[/green]")
77
+
78
+ @app.callback(invoke_without_command=True)
79
+ def repair(
80
+ ctx: typer.Context,
81
+ project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
82
+ ):
83
+ """
84
+ Convert blocking gaps into intelligent repair tasks using LLM inference.
85
+ """
86
+ if ctx.invoked_subcommand is not None:
87
+ return
88
+
89
+ asyncio.run(run_repair_flow(project_root))
@@ -1,71 +1,137 @@
1
- import typer
2
- from rich.console import Console
3
- from rich.markdown import Markdown
4
- from devcouncil.storage.db import get_db
5
- from devcouncil.storage.repositories import ArtifactGraphRepository
6
- from devcouncil.reporting.report_builder import ReportBuilder
7
- from devcouncil.integrations.github import GitHubIntegration
1
+ import typer
2
+ from rich.console import Console
3
+ from rich.markdown import Markdown
4
+ from devcouncil.storage.db import get_db
5
+ from devcouncil.storage.repositories import ArtifactGraphRepository
6
+ from devcouncil.reporting.report_builder import ReportBuilder
7
+ from devcouncil.integrations.github import GitHubIntegration
8
+ from devcouncil.integrations.pr_comments import GitHubPRCommenter, GitLabMRCommenter, build_pr_comment_body
8
9
  from devcouncil.artifacts.graph import ArtifactGraph
9
10
  from devcouncil.telemetry.traces import TraceLogger
11
+ from devcouncil.cli.commands.init import initialize_project
12
+ from devcouncil.live.summary import live_review_summary
10
13
  import asyncio
11
14
  import os
12
15
  import subprocess
13
16
  from pathlib import Path
14
-
15
- app = typer.Typer()
16
- console = Console()
17
-
18
- async def run_github_report(graph: ArtifactGraph):
19
- token = os.environ.get("GITHUB_TOKEN")
20
- repo = os.environ.get("GITHUB_REPOSITORY") # e.g. owner/repo
21
-
22
- if not token or not repo:
23
- console.print("[red]GITHUB_TOKEN and GITHUB_REPOSITORY must be set for GitHub reporting.[/red]")
24
- return
25
-
26
- try:
27
- # Detect current SHA
28
- sha = subprocess.check_output(["git", "rev-parse", "HEAD"]).decode().strip()
29
- integration = GitHubIntegration(token, repo, sha)
30
- await integration.report_verification(graph)
31
- console.print(f"[green]Successfully reported to GitHub PR Checks for {repo} at {sha[:7]}[/green]")
32
- except Exception as e:
33
- console.print(f"[red]Failed to report to GitHub: {e}[/red]")
34
-
35
- @app.callback(invoke_without_command=True)
36
- def report(
37
- ctx: typer.Context,
38
- planning_only: bool = typer.Option(False, "--planning-only", help="Report only the planning phase status"),
39
- json_format: bool = typer.Option(False, "--json", help="Output report in JSON format"),
40
- github: bool = typer.Option(False, "--github", help="Post report to GitHub PR Checks"),
41
- ):
42
- """
43
- Produce final evidence report.
44
- """
45
- if ctx.invoked_subcommand is not None:
46
- return
47
-
48
- db = get_db()
49
- if not db:
50
- console.print("[red]DevCouncil not initialized. Run 'dev init' first.[/red]")
51
- raise typer.Exit(code=1)
52
-
53
- with db.get_session() as session:
17
+
18
+ app = typer.Typer()
19
+ console = Console()
20
+
21
+ async def run_github_report(graph: ArtifactGraph, project_root: Path):
22
+ token = os.environ.get("GITHUB_TOKEN")
23
+ repo = os.environ.get("GITHUB_REPOSITORY") # e.g. owner/repo
24
+
25
+ if not token or not repo:
26
+ console.print("[red]GITHUB_TOKEN and GITHUB_REPOSITORY must be set for GitHub reporting.[/red]")
27
+ return
28
+
29
+ try:
30
+ # Detect current SHA
31
+ sha = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=project_root).decode().strip()
32
+ integration = GitHubIntegration(token, repo, sha)
33
+ await integration.report_verification(graph)
34
+ console.print(f"[green]Successfully reported to GitHub PR Checks for {repo} at {sha[:7]}[/green]")
35
+ except Exception as e:
36
+ console.print(f"[red]Failed to report to GitHub: {e}[/red]")
37
+
38
+
39
+ async def run_github_pr_comment(graph: ArtifactGraph, live_review: dict | None = None):
40
+ token = os.environ.get("GITHUB_TOKEN")
41
+ repo = os.environ.get("GITHUB_REPOSITORY")
42
+ pull_number = os.environ.get("GITHUB_PR_NUMBER") or os.environ.get("PR_NUMBER")
43
+ if not token or not repo or not pull_number:
44
+ console.print("[red]GITHUB_TOKEN, GITHUB_REPOSITORY, and GITHUB_PR_NUMBER must be set for GitHub PR comments.[/red]")
45
+ return
46
+ try:
47
+ pull_number_int = int(pull_number)
48
+ except ValueError:
49
+ console.print("[red]GITHUB_PR_NUMBER must be an integer.[/red]")
50
+ return
51
+ commenter = GitHubPRCommenter(token, repo, pull_number_int)
52
+ await commenter.post_comment(build_pr_comment_body(graph, live_review=live_review))
53
+ console.print(f"[green]Posted DevCouncil PR comment to GitHub PR #{pull_number}.[/green]")
54
+
55
+
56
+ async def run_gitlab_mr_comment(graph: ArtifactGraph, live_review: dict | None = None):
57
+ token = os.environ.get("GITLAB_TOKEN")
58
+ project_id = os.environ.get("GITLAB_PROJECT_ID")
59
+ mr_iid = os.environ.get("GITLAB_MR_IID") or os.environ.get("CI_MERGE_REQUEST_IID")
60
+ base_url = os.environ.get("GITLAB_API_URL", "https://gitlab.com/api/v4")
61
+ if not token or not project_id or not mr_iid:
62
+ console.print("[red]GITLAB_TOKEN, GITLAB_PROJECT_ID, and GITLAB_MR_IID must be set for GitLab MR comments.[/red]")
63
+ return
64
+ try:
65
+ mr_iid_int = int(mr_iid)
66
+ except ValueError:
67
+ console.print("[red]GITLAB_MR_IID must be an integer.[/red]")
68
+ return
69
+ commenter = GitLabMRCommenter(token, project_id, mr_iid_int, base_url=base_url)
70
+ await commenter.post_comment(build_pr_comment_body(graph, live_review=live_review))
71
+ console.print(f"[green]Posted DevCouncil MR comment to GitLab MR !{mr_iid}.[/green]")
72
+
73
+ @app.callback(invoke_without_command=True)
74
+ def report(
75
+ ctx: typer.Context,
76
+ planning_only: bool = typer.Option(False, "--planning-only", help="Report only the planning phase status"),
77
+ json_format: bool = typer.Option(False, "--json", help="Output report in JSON format"),
78
+ github: bool = typer.Option(False, "--github", help="Post report to GitHub PR Checks"),
79
+ github_pr_comment: bool = typer.Option(False, "--github-pr-comment", help="Post report as a GitHub PR comment"),
80
+ gitlab_pr_comment: bool = typer.Option(False, "--gitlab-pr-comment", help="Post report as a GitLab merge request comment"),
81
+ fail_on_blocking: bool = typer.Option(
82
+ False,
83
+ "--fail-on-blocking",
84
+ help="Exit non-zero when blocking gaps remain, so shell-driven agents can gate on $?.",
85
+ ),
86
+ project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
87
+ ):
88
+ """
89
+ Produce final evidence report.
90
+ """
91
+ if ctx.invoked_subcommand is not None:
92
+ return
93
+
94
+ root = project_root.expanduser().resolve()
95
+ initialize_project(root, quiet=True)
96
+ db = get_db(root)
97
+ if not db:
98
+ console.print("[red]DevCouncil state is unavailable in this directory.[/red]")
99
+ raise typer.Exit(code=1)
100
+
101
+ with db.get_session() as session:
54
102
  graph_repo = ArtifactGraphRepository(session)
55
103
  graph = graph_repo.load_graph()
56
- TraceLogger(Path(".")).log_event(
104
+ live_review = live_review_summary(root)
105
+ TraceLogger(root).log_event(
57
106
  "report_generated",
58
- {"json": json_format, "github": github, "planning_only": planning_only},
107
+ {
108
+ "json": json_format,
109
+ "github": github,
110
+ "github_pr_comment": github_pr_comment,
111
+ "gitlab_pr_comment": gitlab_pr_comment,
112
+ "planning_only": planning_only,
113
+ },
59
114
  summary="Generated DevCouncil report",
60
115
  )
61
116
 
62
117
  if github:
63
- asyncio.run(run_github_report(graph))
64
- return
65
-
118
+ asyncio.run(run_github_report(graph, root))
119
+ return
120
+
121
+ if github_pr_comment:
122
+ asyncio.run(run_github_pr_comment(graph, live_review=live_review))
123
+ return
124
+
125
+ if gitlab_pr_comment:
126
+ asyncio.run(run_gitlab_mr_comment(graph, live_review=live_review))
127
+ return
128
+
66
129
  if json_format:
67
- output = ReportBuilder.build_json(graph)
130
+ output = ReportBuilder.build_json(graph, live_review=live_review)
68
131
  typer.echo(output)
69
132
  else:
70
- output = ReportBuilder.build_markdown(graph)
133
+ output = ReportBuilder.build_markdown(graph, live_review=live_review)
71
134
  console.print(Markdown(output))
135
+
136
+ if fail_on_blocking and graph.blocking_gaps():
137
+ raise typer.Exit(code=1)
@@ -1,28 +1,33 @@
1
- import typer
2
- from rich.console import Console
3
- from sqlmodel import delete
4
-
5
- from devcouncil.storage.db import get_db
6
- from devcouncil.storage.models import EvidenceModel, GapModel, RequirementModel, TaskModel
7
-
8
- console = Console()
9
-
10
-
11
- def reset_demo_state(
12
- yes: bool = typer.Option(False, "--yes", help="Confirm clearing planning/demo artifacts."),
13
- ):
14
- """Clear demo planning artifacts from the local DevCouncil state database."""
15
- if not yes:
16
- console.print("[red]Refusing to clear state without --yes.[/red]")
17
- raise typer.Exit(code=1)
18
-
19
- db = get_db()
20
- if not db:
21
- console.print("[red]DevCouncil not initialized. Run 'dev init' first.[/red]")
22
- raise typer.Exit(code=1)
23
-
24
- with db.get_session() as session:
25
- for model in (EvidenceModel, GapModel, TaskModel, RequirementModel):
26
- session.exec(delete(model))
27
-
28
- console.print("[green]Cleared requirements, tasks, gaps, and evidence from local state.[/green]")
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
+ project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
16
+ ):
17
+ """Clear demo planning artifacts from the local DevCouncil state database."""
18
+ if not yes:
19
+ console.print("[red]Refusing to clear state without --yes.[/red]")
20
+ raise typer.Exit(code=1)
21
+
22
+ root = project_root.expanduser().resolve()
23
+ initialize_project(root, quiet=True)
24
+ db = get_db(root)
25
+ if not db:
26
+ console.print("[red]DevCouncil state is unavailable in this directory.[/red]")
27
+ raise typer.Exit(code=1)
28
+
29
+ with db.get_session() as session:
30
+ for model in (EvidenceModel, GapModel, TaskModel, RequirementModel):
31
+ session.exec(delete(model))
32
+
33
+ console.print("[green]Cleared requirements, tasks, gaps, and evidence from local state.[/green]")
@@ -1,58 +1,59 @@
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
- ):
14
- """
15
- Revert changes using a task's git checkpoint.
16
- """
17
- if ctx.invoked_subcommand is not None:
18
- return
19
-
20
- checkpoint_file = Path(".devcouncil/checkpoints") / f"{task_id}-before.patch"
21
- after_patch = Path(".devcouncil/checkpoints") / f"{task_id}-after.patch"
1
+ import typer
2
+ from pathlib import Path
3
+ from rich.console import Console
4
+
5
+ from devcouncil.execution.checkpoints import CheckpointService
6
+
7
+ app = typer.Typer()
8
+ console = Console()
9
+
10
+ @app.callback(invoke_without_command=True)
11
+ def rollback(
12
+ ctx: typer.Context,
13
+ task_id: str = typer.Argument(..., help="ID of the task to rollback"),
14
+ project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
15
+ ):
16
+ """
17
+ Revert changes using a task's git checkpoint.
18
+ """
19
+ if ctx.invoked_subcommand is not None:
20
+ return
21
+
22
+ root = project_root.expanduser().resolve()
23
+ checkpoint_dir = root / ".devcouncil" / "checkpoints"
24
+ checkpoint_file = checkpoint_dir / f"{task_id}-before.patch"
25
+ after_patch = checkpoint_dir / f"{task_id}-after.patch"
26
+ service = CheckpointService(root)
22
27
 
23
28
  if not checkpoint_file.exists() and not after_patch.exists():
24
- console.print(
25
- f"[red]No checkpoint found for task {task_id}. Expected {after_patch} "
26
- f"or {checkpoint_file}.[/red]"
27
- )
28
- raise typer.Exit(code=1)
29
+ before_ref = CheckpointService.REF_BEFORE.format(task_id=task_id)
30
+ after_ref = CheckpointService.REF_AFTER.format(task_id=task_id)
31
+ if not service._ref_exists(before_ref) and not service._ref_exists(after_ref):
32
+ console.print(
33
+ f"[red]No checkpoint found for task {task_id}. Expected {after_patch} "
34
+ f"or {checkpoint_file}.[/red]"
35
+ )
36
+ raise typer.Exit(code=1)
29
37
 
30
38
  console.print(f"Rolling back task [bold]{task_id}[/bold]...")
31
-
32
- try:
33
- if after_patch.exists():
34
- # Reverse-apply the task's changes only
35
- console.print(f"Applying reverse patch from [bold]{after_patch}[/bold]...")
36
- subprocess.check_call(
37
- ["git", "apply", "-R", str(after_patch)],
38
- cwd=".",
39
- )
40
- console.print(f"[green]Successfully rolled back task {task_id} changes.[/green]")
41
- else:
42
- # No after-patch, but we have the before-patch — warn and offer manual reset
43
- console.print(
44
- f"[yellow]No after-patch found at {after_patch}.[/yellow]\n"
45
- f"The before-patch at {checkpoint_file} captured the state before the task ran.\n"
46
- f"To manually reset:\n"
47
- f" 1. [bold]git stash[/bold] (if you want to keep current changes)\n"
48
- f" 2. [bold]git checkout -- .[/bold] (discard working tree changes)\n"
49
- f" 3. [bold]git apply {checkpoint_file}[/bold] (restore pre-task state)"
50
- )
51
- except subprocess.CalledProcessError as e:
52
- console.print(f"[red]Failed to apply reverse patch: {e}[/red]")
53
- console.print("[yellow]The patch may conflict with current changes. Try resolving manually:[/yellow]")
54
- console.print(f" git apply -R --3way {after_patch}")
55
- raise typer.Exit(code=1)
56
- except Exception as e:
57
- console.print(f"[red]Failed to rollback: {e}[/red]")
58
- raise typer.Exit(code=1)
39
+ result = service.rollback(task_id)
40
+ if "failed" in result.message.lower() or result.message.startswith("No checkpoint"):
41
+ console.print(f"[yellow]{result.message}[/yellow]")
42
+ if checkpoint_file.exists():
43
+ console.print(
44
+ f"The before-patch at {checkpoint_file} captured the state before the task ran.\n"
45
+ f"To manually reset:\n"
46
+ f" 1. [bold]git stash[/bold] (if you want to keep current changes)\n"
47
+ f" 2. [bold]git checkout -- .[/bold] (discard working tree changes)\n"
48
+ f" 3. [bold]git apply {checkpoint_file}[/bold] (restore pre-task state)"
49
+ )
50
+ elif after_patch.exists():
51
+ console.print(
52
+ f"Only the after-patch at {after_patch} exists (it captured the task's changes).\n"
53
+ f"To manually revert those changes from the working tree:\n"
54
+ f" 1. [bold]git apply --stat {after_patch}[/bold] (inspect what the task changed)\n"
55
+ f" 2. [bold]git apply -R {after_patch}[/bold] (reverse-apply the task's changes)"
56
+ )
57
+ raise typer.Exit(code=1)
58
+
59
+ console.print(f"[green]Successfully rolled back task {task_id}.[/green] {result.message}")