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,11 +1,12 @@
1
1
  import json
2
2
  import time
3
3
  from pathlib import Path
4
+ from typing import Optional
4
5
 
5
6
  import typer
6
7
  from rich.console import Console
7
8
 
8
- from devcouncil.telemetry.traces import read_trace_events
9
+ from devcouncil.telemetry.traces import read_trace_events, read_trace_events_since
9
10
 
10
11
  app = typer.Typer(help="Inspect DevCouncil trace events.")
11
12
  console = Console()
@@ -13,12 +14,56 @@ console = Console()
13
14
 
14
15
  @app.command("tail")
15
16
  def tail(
16
- follow: bool = typer.Option(False, "--follow", "-f", help="Continue polling for new events."),
17
+ follow: bool = typer.Option(
18
+ False, "--follow/--no-follow", "-f", help="Continue polling for new events (default is a single shot)."
19
+ ),
17
20
  limit: int = typer.Option(50, "--limit", "-n", help="Maximum events to print before following."),
18
21
  jsonl: bool = typer.Option(True, "--jsonl/--pretty", help="Print JSONL or compact text rows."),
22
+ since: Optional[int] = typer.Option(
23
+ None,
24
+ "--since",
25
+ help="Byte-offset cursor from a previous run; emit only events after it (stateless incremental polling).",
26
+ ),
27
+ json_summary: bool = typer.Option(
28
+ False,
29
+ "--json",
30
+ help="Emit a single {events, next_cursor} JSON object (incremental mode). Implies --no-follow.",
31
+ ),
32
+ project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
19
33
  ):
20
- """Print the DevCouncil trace JSONL stream for replay or debugging."""
21
- project_root = Path(".")
34
+ """Print the DevCouncil trace JSONL stream for replay or debugging.
35
+
36
+ With ``--since <cursor> --no-follow`` (or ``--json``) a single-shot supervising
37
+ agent gets only the events appended after the cursor plus a ``next_cursor`` to
38
+ pass back on the next poll, so each poll is O(new) rather than O(all).
39
+ """
40
+ project_root = project_root.expanduser().resolve()
41
+
42
+ # Incremental cursor mode: any of --since / --json / explicit --no-follow.
43
+ incremental = since is not None or json_summary
44
+ if incremental:
45
+ events, next_cursor = read_trace_events_since(project_root, since)
46
+ if json_summary:
47
+ typer.echo(
48
+ json.dumps(
49
+ {
50
+ "events": [event.model_dump(by_alias=True) for event in events],
51
+ "next_cursor": next_cursor,
52
+ }
53
+ )
54
+ )
55
+ return
56
+ for event in events:
57
+ if jsonl:
58
+ typer.echo(event.model_dump_json())
59
+ else:
60
+ console.print(
61
+ f"{event.timestamp} {event.type} "
62
+ f"{event.task_id or '-'} {event.summary or json.dumps(event.details)}"
63
+ )
64
+ console.print(f"[dim]next_cursor: {next_cursor}[/dim]")
65
+ return
66
+
22
67
  printed = 0
23
68
 
24
69
  def emit_new(start_index: int) -> int:
@@ -1,105 +1,203 @@
1
- import typer
2
- import asyncio
3
- from rich.console import Console
4
- from rich.table import Table
5
- from pathlib import Path
6
- from typing import Optional
7
- from devcouncil.storage.db import get_db
8
- from devcouncil.storage.repositories import TaskRepository, RequirementRepository, GapRepository, EvidenceRepository, StateRepository
9
- from devcouncil.verification.verifier import Verifier
10
- from devcouncil.llm.provider import OpenRouterProvider
11
- from devcouncil.llm.router import ModelRouter
12
- from devcouncil.domain.evidence import CommandResult, DiffEvidence, TestEvidence
1
+ import typer
2
+ import asyncio
3
+ import json
4
+ from rich.console import Console
5
+ from rich.table import Table
6
+ from pathlib import Path
7
+ from typing import Optional
8
+ from devcouncil.cli.commands.init import initialize_project
9
+ from devcouncil.storage.db import get_db
10
+ from devcouncil.storage.repositories import TaskRepository, RequirementRepository, GapRepository, EvidenceRepository, StateRepository
11
+ from devcouncil.verification.verifier import Verifier
12
+ from devcouncil.llm.provider import create_provider, validate_model_provider
13
+ from devcouncil.llm.router import ModelRouter
14
+ from devcouncil.domain.evidence import CommandResult, DiffEvidence, DiffCoverageEvidence, TestEvidence
15
+ from devcouncil.domain.gap import Gap
16
+ from devcouncil.verification.next_actions import split_next_actions
13
17
  from devcouncil.app.config import load_config, get_api_key
14
18
  from devcouncil.app.state_machine import ProjectPhase
15
19
  from devcouncil.integrations.code_review_graph import CodeReviewGraphAdapter
16
20
  from devcouncil.telemetry.traces import TraceLogger
17
-
18
- console = Console()
19
- MAX_RENDERED_GAPS = 20
20
-
21
- def verify(
22
- task_id: Optional[str] = typer.Argument(None, help="Optional ID of the task to verify"),
23
- ):
24
- """
25
- Verify one task, or all tasks when TASK_ID is omitted.
26
- """
27
- db = get_db()
28
- if not db:
29
- console.print("[red]DevCouncil not initialized. Run 'dev init' first.[/red]")
30
- return
31
-
32
- with db.get_session() as session:
33
- task_repo = TaskRepository(session)
34
- req_repo = RequirementRepository(session)
35
- gap_repo = GapRepository(session)
36
- evidence_repo = EvidenceRepository(session)
37
-
38
- tasks = [task_repo.get_by_id(task_id)] if task_id else task_repo.get_all()
39
- tasks = [task for task in tasks if task is not None]
40
- if not tasks:
41
- missing = f"Task {task_id} not found." if task_id else "No tasks found to verify."
42
- console.print(f"[red]{missing}[/red]")
43
- return
44
-
45
- reqs = req_repo.get_all()
46
-
47
- # Load router for LLM review if possible
48
- router = None
49
- try:
50
- config = load_config(Path("."))
51
- api_key = get_api_key(config.models.provider)
52
- provider = OpenRouterProvider(api_key)
53
- role_config = {name: role.model_dump() for name, role in config.models.roles.items()}
54
- router = ModelRouter(provider, role_config)
55
- except Exception:
56
- pass
57
-
58
- verifier = Verifier(Path("."), router=router)
59
- total_gaps = 0
60
- blocked_tasks = 0
61
-
21
+
22
+ console = Console()
23
+ MAX_RENDERED_GAPS = 20
24
+
25
+
26
+ def reconcile_cross_task_acceptance(
27
+ gaps: list[Gap], proven_acs: set[str]
28
+ ) -> list[Gap]:
29
+ """Drop a task's blocking ``acceptance_criteria_unproven`` gaps whose criterion is
30
+ already proven by passing evidence in another task.
31
+
32
+ Acceptance criteria are requirement-level, not task-private: when the planner splits
33
+ "implement X" and "add tests for X" into separate tasks that share criteria, the
34
+ implement task would otherwise stay blocked for criteria the test task proved. The
35
+ caller passes ``proven_acs`` gathered from passing evidence re-run against the current
36
+ tree, so a regression would have failed the test and excluded the criterion — only
37
+ genuinely-satisfied criteria are cleared. Returns the gaps to keep."""
38
+ return [
39
+ gap
40
+ for gap in gaps
41
+ if not (
42
+ gap.blocking
43
+ and gap.gap_type == "acceptance_criteria_unproven"
44
+ and gap.acceptance_criterion_id in proven_acs
45
+ )
46
+ ]
47
+
48
+ def verify(
49
+ task_id: Optional[str] = typer.Argument(None, help="Optional ID of the task to verify"),
50
+ sandbox: str = typer.Option("local", "--sandbox", help="Verification sandbox: local, docker, or nix."),
51
+ json_format: bool = typer.Option(False, "--json", help="Output machine-readable JSON."),
52
+ project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
53
+ ):
54
+ """
55
+ Verify one task, or all tasks when TASK_ID is omitted.
56
+ """
57
+ root = project_root.expanduser().resolve()
58
+ initialize_project(root, quiet=True)
59
+ db = get_db(root)
60
+ if not db:
61
+ if json_format:
62
+ typer.echo(json.dumps({"ok": False, "error": "DevCouncil state is unavailable in this directory."}, indent=2))
63
+ else:
64
+ console.print("[red]DevCouncil state is unavailable in this directory.[/red]")
65
+ return
66
+
67
+ with db.get_session() as session:
68
+ task_repo = TaskRepository(session)
69
+ req_repo = RequirementRepository(session)
70
+ gap_repo = GapRepository(session)
71
+ evidence_repo = EvidenceRepository(session)
72
+
73
+ tasks = [task_repo.get_by_id(task_id)] if task_id else task_repo.get_all()
74
+ tasks = [task for task in tasks if task is not None]
75
+ if not tasks:
76
+ missing = f"Task {task_id} not found." if task_id else "No tasks found to verify."
77
+ if json_format:
78
+ typer.echo(json.dumps({"ok": False, "error": missing}, indent=2))
79
+ else:
80
+ console.print(f"[red]{missing}[/red]")
81
+ return
82
+
83
+ reqs = req_repo.get_all()
84
+
85
+ # Load router for LLM review if possible
86
+ router = None
87
+ try:
88
+ config = load_config(root)
89
+ validate_model_provider(config.models.provider)
90
+ api_key = get_api_key(config.models.provider, root)
91
+ provider = create_provider(config.models.provider, api_key, project_root=root)
92
+ role_config = {name: role.model_dump() for name, role in config.models.roles.items()}
93
+ router = ModelRouter(provider, role_config, project_root=root)
94
+ except Exception:
95
+ pass
96
+
97
+ from devcouncil.verification.sandbox import get_sandbox
98
+
99
+ verifier = Verifier(root, router=router)
100
+ total_gaps = 0
101
+ blocked_tasks = 0
102
+ task_results = []
103
+ # Cross-task acceptance reconciliation state: a criterion proven by passing
104
+ # evidence in ANY task is proven for every task that shares it.
105
+ proven_acs: set[str] = set()
106
+ per_task_gaps: dict[str, list] = {}
107
+
62
108
  for task in tasks:
63
- TraceLogger(Path(".")).log_event(
109
+ if sandbox != "local":
110
+ commands = task.expected_tests or task.allowed_commands
111
+ sandbox_result = get_sandbox(sandbox, root).run(task, commands, reqs)
112
+ if sandbox_result.status == "unsupported":
113
+ message = f"Sandbox {sandbox} is unavailable."
114
+ if json_format:
115
+ typer.echo(json.dumps({"ok": False, "error": message, "sandbox": sandbox}, indent=2))
116
+ else:
117
+ console.print(f"[red]{message}[/red]")
118
+ return
119
+ if sandbox_result.status == "failed":
120
+ task.status = "blocked"
121
+ blocked_tasks += 1
122
+ task_repo.save(task)
123
+ task_results.append({
124
+ "task_id": task.id,
125
+ "status": task.status,
126
+ "sandbox": sandbox,
127
+ "gap_count": 1,
128
+ "blocking_gap_count": 1,
129
+ "gaps": [],
130
+ })
131
+ if json_format:
132
+ typer.echo(json.dumps({
133
+ "ok": False,
134
+ "task_id": task.id,
135
+ "sandbox": sandbox,
136
+ "commands": sandbox_result.commands,
137
+ }, indent=2))
138
+ else:
139
+ console.print(f"[red]{task.id} failed in {sandbox} sandbox.[/red]")
140
+ continue
141
+ task.status = "verified"
142
+ task_repo.save(task)
143
+ task_results.append({
144
+ "task_id": task.id,
145
+ "status": task.status,
146
+ "sandbox": sandbox,
147
+ "gap_count": 0,
148
+ "blocking_gap_count": 0,
149
+ "gaps": [],
150
+ })
151
+ if not json_format:
152
+ console.print(f"[green]{task.id} passed in {sandbox} sandbox.[/green]")
153
+ continue
154
+ TraceLogger(root).log_event(
64
155
  "task_verification_started",
65
156
  {"task_id": task.id},
66
157
  task_id=task.id,
67
158
  summary=f"Verifying {task.id}",
68
159
  )
69
- graph_context = CodeReviewGraphAdapter(Path(".")).get_context(
160
+ graph_context = CodeReviewGraphAdapter(root).get_context(
70
161
  [planned.path for planned in task.planned_files]
71
162
  )
72
163
  if graph_context.available:
73
- TraceLogger(Path(".")).log_event(
164
+ TraceLogger(root).log_event(
74
165
  "graph_context_loaded",
75
166
  graph_context.model_dump(),
76
167
  task_id=task.id,
77
168
  summary=f"Loaded graph context for {task.id}",
78
169
  )
79
170
  StateRepository(session).record_phase(ProjectPhase.TASK_VERIFYING.value)
80
- gap_repo.delete_for_task(task.id)
81
- evidence_repo.delete_for_task(task.id)
82
-
83
- gaps, evidence = asyncio.run(verifier.verify_task(task, reqs))
84
- total_gaps += len(gaps)
85
-
86
- for gap in gaps:
87
- gap_repo.save(gap)
88
-
89
- for ev in evidence:
90
- if isinstance(ev, CommandResult):
91
- evidence_repo.save_command_result(task.id, ev)
92
- elif isinstance(ev, DiffEvidence):
93
- evidence_repo.save_diff_evidence(ev)
94
- elif isinstance(ev, TestEvidence):
95
- evidence_repo.save_test_evidence(ev, task.id)
96
-
97
- _print_task_result(task.id, gaps)
98
-
171
+ gap_repo.delete_for_task(task.id)
172
+ evidence_repo.delete_for_task(task.id)
173
+
174
+ gaps, evidence = asyncio.run(verifier.verify_task(task, reqs))
175
+ outcome = verifier.last_outcome
176
+ total_gaps += len(gaps)
177
+
178
+ for gap in gaps:
179
+ gap_repo.save(gap)
180
+
181
+ for ev in evidence:
182
+ if isinstance(ev, CommandResult):
183
+ evidence_repo.save_command_result(task.id, ev)
184
+ elif isinstance(ev, DiffCoverageEvidence):
185
+ evidence_repo.save_diff_coverage_evidence(ev)
186
+ elif isinstance(ev, DiffEvidence):
187
+ evidence_repo.save_diff_evidence(ev)
188
+ elif isinstance(ev, TestEvidence):
189
+ evidence_repo.save_test_evidence(ev, task.id)
190
+ if ev.status == "passed" and ev.acceptance_criterion_id:
191
+ proven_acs.add(ev.acceptance_criterion_id)
192
+
193
+ per_task_gaps[task.id] = gaps
194
+ if not json_format:
195
+ _print_task_result(task.id, gaps)
196
+
99
197
  if any(gap.blocking for gap in gaps):
100
198
  task.status = "blocked"
101
199
  blocked_tasks += 1
102
- TraceLogger(Path(".")).log_event(
200
+ TraceLogger(root).log_event(
103
201
  "gate_failed",
104
202
  {"task_id": task.id, "gap_count": len(gaps)},
105
203
  task_id=task.id,
@@ -107,57 +205,124 @@ def verify(
107
205
  )
108
206
  else:
109
207
  task.status = "verified"
110
- TraceLogger(Path(".")).log_event(
208
+ TraceLogger(root).log_event(
111
209
  "task_verified",
112
210
  {"task_id": task.id, "gap_count": len(gaps)},
113
211
  task_id=task.id,
114
212
  summary=f"{task.id} verified",
115
213
  )
116
214
  task_repo.save(task)
117
-
118
- StateRepository(session).record_phase(
119
- ProjectPhase.TASK_BLOCKED.value if blocked_tasks else ProjectPhase.TASK_VERIFIED.value
120
- )
121
-
122
- if len(tasks) > 1:
123
- if blocked_tasks:
124
- console.print(
125
- f"\n[yellow]Verified {len(tasks)} tasks: {blocked_tasks} blocked, "
126
- f"{total_gaps} total gap(s).[/yellow]"
127
- )
128
- else:
129
- console.print(f"\n[green]Verified {len(tasks)} tasks successfully.[/green]")
130
-
131
-
132
- def _print_task_result(task_id: str, gaps):
133
- if not gaps:
134
- console.print(f"[green]Task {task_id} verified successfully! No gaps found.[/green]")
135
- return
136
-
137
- console.print(f"[yellow]Verification finished for task {task_id} with {len(gaps)} gaps:[/yellow]")
138
-
139
- table = Table(title="Detected Gaps")
140
- table.add_column("ID", style="cyan")
141
- table.add_column("Severity", style="magenta")
142
- table.add_column("Description", style="white")
143
- table.add_column("Blocking", style="red")
144
-
145
- for gap in gaps[:MAX_RENDERED_GAPS]:
146
- table.add_row(
147
- gap.id,
148
- gap.severity,
149
- gap.description,
150
- "YES" if gap.blocking else "NO",
151
- )
152
-
153
- console.print(table)
154
- if len(gaps) > MAX_RENDERED_GAPS:
155
- console.print(
156
- f"[yellow]Showing first {MAX_RENDERED_GAPS} of {len(gaps)} gaps. "
157
- "Run [bold]dev report --json[/bold] for the full list.[/yellow]"
158
- )
159
-
160
- if any(gap.blocking for gap in gaps):
161
- console.print(f"\n[red]Task {task_id} is BLOCKED due to critical gaps.[/red]")
162
- else:
163
- console.print(f"\n[green]Task {task_id} passed with non-blocking gaps.[/green]")
215
+ blocking_actions, advisory_actions = split_next_actions(gaps)
216
+ task_results.append({
217
+ "task_id": task.id,
218
+ "status": task.status,
219
+ "gap_count": len(gaps),
220
+ "blocking_gap_count": len([gap for gap in gaps if gap.blocking]),
221
+ "gaps": [gap.model_dump() for gap in gaps],
222
+ "next_actions": [action.model_dump() for action in blocking_actions],
223
+ "advisory_actions": [action.model_dump() for action in advisory_actions],
224
+ "verification_mode": outcome.mode if outcome else "unknown",
225
+ "compiler_active": outcome.compiler_active if outcome else False,
226
+ "diff_empty": outcome.diff_empty if outcome else False,
227
+ "coverage_measured": outcome.coverage_measured if outcome else False,
228
+ "coverage_skipped_reason": outcome.coverage_skipped_reason if outcome else None,
229
+ })
230
+
231
+ # Cross-task acceptance reconciliation (only meaningful across the full set).
232
+ # The planner sometimes splits "implement X" and "add tests for X" into separate
233
+ # tasks that share acceptance criteria; the implement task would otherwise stay
234
+ # blocked for criteria the test task already proved. A criterion proven by passing
235
+ # evidence in ANY task is proven for every task that shares it. Evidence was re-run
236
+ # against the current tree, so a regression would have failed the test and the AC
237
+ # would not be in proven_acs — this clears only genuinely-satisfied criteria.
238
+ if task_id is None and proven_acs:
239
+ for task in tasks:
240
+ gaps = per_task_gaps.get(task.id, [])
241
+ kept = reconcile_cross_task_acceptance(gaps, proven_acs)
242
+ if len(kept) == len(gaps):
243
+ continue
244
+ gap_repo.delete_for_task(task.id)
245
+ for gap in kept:
246
+ gap_repo.save(gap)
247
+ per_task_gaps[task.id] = kept
248
+ if task.status == "blocked" and not any(gap.blocking for gap in kept):
249
+ task.status = "verified"
250
+ blocked_tasks = max(0, blocked_tasks - 1)
251
+ TraceLogger(root).log_event(
252
+ "task_reconciled",
253
+ {"task_id": task.id, "cross_task_proven": True},
254
+ task_id=task.id,
255
+ summary=f"{task.id} verified via cross-task acceptance reconciliation",
256
+ )
257
+ task_repo.save(task)
258
+ for result in task_results:
259
+ if result["task_id"] == task.id:
260
+ blocking_actions, advisory_actions = split_next_actions(kept)
261
+ result["status"] = task.status
262
+ result["gap_count"] = len(kept)
263
+ result["blocking_gap_count"] = len([gap for gap in kept if gap.blocking])
264
+ result["gaps"] = [gap.model_dump() for gap in kept]
265
+ result["next_actions"] = [action.model_dump() for action in blocking_actions]
266
+ result["advisory_actions"] = [action.model_dump() for action in advisory_actions]
267
+
268
+ StateRepository(session).record_phase(
269
+ ProjectPhase.TASK_BLOCKED.value if blocked_tasks else ProjectPhase.TASK_VERIFIED.value
270
+ )
271
+
272
+ if json_format:
273
+ typer.echo(json.dumps({
274
+ "ok": blocked_tasks == 0,
275
+ "verified_tasks": len(tasks),
276
+ "blocked_tasks": blocked_tasks,
277
+ "total_gaps": total_gaps,
278
+ "tasks": task_results,
279
+ }, indent=2))
280
+ elif len(tasks) > 1:
281
+ if blocked_tasks:
282
+ console.print(
283
+ f"\n[yellow]Verified {len(tasks)} tasks: {blocked_tasks} blocked, "
284
+ f"{total_gaps} total gap(s).[/yellow]"
285
+ )
286
+ else:
287
+ console.print(f"\n[green]Verified {len(tasks)} tasks successfully.[/green]")
288
+
289
+ # Exit-code contract (so shell-driven agents can gate on $?):
290
+ # 0 = all verified, no blocking gaps
291
+ # 1 = at least one task is blocked by a verification gap
292
+ # Argument/state errors above return early with their own message and exit 0.
293
+ if blocked_tasks:
294
+ raise typer.Exit(code=1)
295
+
296
+
297
+ def _print_task_result(task_id: str, gaps):
298
+ if not gaps:
299
+ console.print(f"[green]Task {task_id} verified successfully! No gaps found.[/green]")
300
+ return
301
+
302
+ console.print(f"[yellow]Verification finished for task {task_id} with {len(gaps)} gaps:[/yellow]")
303
+
304
+ table = Table(title="Detected Gaps")
305
+ table.add_column("ID", style="cyan")
306
+ table.add_column("Severity", style="magenta")
307
+ table.add_column("Description", style="white")
308
+ table.add_column("Blocking", style="red")
309
+
310
+ for gap in gaps[:MAX_RENDERED_GAPS]:
311
+ table.add_row(
312
+ gap.id,
313
+ gap.severity,
314
+ gap.description,
315
+ "YES" if gap.blocking else "NO",
316
+ )
317
+
318
+ console.print(table)
319
+ if len(gaps) > MAX_RENDERED_GAPS:
320
+ console.print(
321
+ f"[yellow]Showing first {MAX_RENDERED_GAPS} of {len(gaps)} gaps. "
322
+ "Run [bold]dev report --json[/bold] for the full list.[/yellow]"
323
+ )
324
+
325
+ if any(gap.blocking for gap in gaps):
326
+ console.print(f"\n[red]Task {task_id} is BLOCKED due to critical gaps.[/red]")
327
+ else:
328
+ console.print(f"\n[green]Task {task_id} passed with non-blocking gaps.[/green]")
@@ -1,20 +1,20 @@
1
- import typer
2
- from rich.console import Console
3
- import importlib.metadata
4
-
5
- app = typer.Typer()
6
- console = Console()
7
-
8
- @app.callback(invoke_without_command=True)
9
- def version(ctx: typer.Context):
10
- """
11
- Display the current version of DevCouncil.
12
- """
13
- if ctx.invoked_subcommand is not None:
14
- return
15
-
16
- try:
17
- ver = importlib.metadata.version("devcouncil")
18
- console.print(f"DevCouncil version: [bold cyan]{ver}[/bold cyan]")
19
- except importlib.metadata.PackageNotFoundError:
20
- console.print("DevCouncil version: [yellow]unknown (editable/uninstalled)[/yellow]")
1
+ import typer
2
+ from rich.console import Console
3
+ import importlib.metadata
4
+
5
+ app = typer.Typer()
6
+ console = Console()
7
+
8
+ @app.callback(invoke_without_command=True)
9
+ def version(ctx: typer.Context):
10
+ """
11
+ Display the current version of DevCouncil.
12
+ """
13
+ if ctx.invoked_subcommand is not None:
14
+ return
15
+
16
+ try:
17
+ ver = importlib.metadata.version("devcouncil")
18
+ console.print(f"DevCouncil version: [bold cyan]{ver}[/bold cyan]")
19
+ except importlib.metadata.PackageNotFoundError:
20
+ console.print("DevCouncil version: [yellow]unknown (editable/uninstalled)[/yellow]")