devcouncil 0.1.1 → 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 (129) hide show
  1. package/README.md +190 -6
  2. package/package.json +9 -2
  3. package/pyproject.toml +34 -2
  4. package/src/devcouncil/app/config.py +167 -5
  5. package/src/devcouncil/artifacts/graph.py +23 -3
  6. package/src/devcouncil/assets/__init__.py +1 -0
  7. package/src/devcouncil/assets/devcouncil-logo.svg +60 -0
  8. package/src/devcouncil/assets/devcouncil_logo_premium.png +0 -0
  9. package/src/devcouncil/cli/commands/agents.py +292 -0
  10. package/src/devcouncil/cli/commands/artifacts.py +6 -3
  11. package/src/devcouncil/cli/commands/check.py +209 -0
  12. package/src/devcouncil/cli/commands/config.py +43 -4
  13. package/src/devcouncil/cli/commands/cost.py +57 -0
  14. package/src/devcouncil/cli/commands/dashboard.py +6 -1
  15. package/src/devcouncil/cli/commands/doctor.py +221 -21
  16. package/src/devcouncil/cli/commands/evidence.py +48 -0
  17. package/src/devcouncil/cli/commands/go.py +452 -33
  18. package/src/devcouncil/cli/commands/handoff.py +69 -0
  19. package/src/devcouncil/cli/commands/hook.py +124 -15
  20. package/src/devcouncil/cli/commands/init.py +154 -18
  21. package/src/devcouncil/cli/commands/integrate.py +894 -105
  22. package/src/devcouncil/cli/commands/map.py +80 -10
  23. package/src/devcouncil/cli/commands/plan.py +212 -51
  24. package/src/devcouncil/cli/commands/prompt.py +18 -7
  25. package/src/devcouncil/cli/commands/repair.py +40 -23
  26. package/src/devcouncil/cli/commands/report.py +8 -0
  27. package/src/devcouncil/cli/commands/reset_demo_state.py +4 -2
  28. package/src/devcouncil/cli/commands/rollback.py +27 -28
  29. package/src/devcouncil/cli/commands/run.py +69 -49
  30. package/src/devcouncil/cli/commands/runs.py +223 -0
  31. package/src/devcouncil/cli/commands/scaffold.py +32 -0
  32. package/src/devcouncil/cli/commands/semantic.py +47 -0
  33. package/src/devcouncil/cli/commands/setup.py +145 -6
  34. package/src/devcouncil/cli/commands/shell.py +73 -0
  35. package/src/devcouncil/cli/commands/skills.py +88 -0
  36. package/src/devcouncil/cli/commands/status.py +25 -1
  37. package/src/devcouncil/cli/commands/trace.py +47 -3
  38. package/src/devcouncil/cli/commands/verify.py +138 -3
  39. package/src/devcouncil/cli/commands/watch.py +9 -9
  40. package/src/devcouncil/cli/commands/watch_fs.py +40 -0
  41. package/src/devcouncil/cli/main.py +56 -7
  42. package/src/devcouncil/domain/evidence.py +22 -2
  43. package/src/devcouncil/domain/gap.py +27 -1
  44. package/src/devcouncil/domain/task.py +31 -2
  45. package/src/devcouncil/execution/checkpoints.py +246 -0
  46. package/src/devcouncil/execution/context_builder.py +1 -1
  47. package/src/devcouncil/execution/fs_watcher.py +180 -0
  48. package/src/devcouncil/execution/handoff.py +102 -0
  49. package/src/devcouncil/execution/hook_policy.py +162 -74
  50. package/src/devcouncil/execution/patch.py +59 -10
  51. package/src/devcouncil/execution/permissions.py +17 -24
  52. package/src/devcouncil/execution/policy_engine.py +343 -0
  53. package/src/devcouncil/execution/prompt_builder.py +633 -21
  54. package/src/devcouncil/execution/shell_session.py +225 -0
  55. package/src/devcouncil/execution/task_runner.py +6 -2
  56. package/src/devcouncil/executors/agent_registry.py +575 -0
  57. package/src/devcouncil/executors/coding_cli.py +663 -39
  58. package/src/devcouncil/executors/native/agent.py +121 -20
  59. package/src/devcouncil/gating/checks/clean_git.py +3 -1
  60. package/src/devcouncil/gating/checks/secret_scan_check.py +40 -21
  61. package/src/devcouncil/gating/policy.py +158 -10
  62. package/src/devcouncil/hardware.py +184 -0
  63. package/src/devcouncil/indexing/ast_matcher.py +1 -1
  64. package/src/devcouncil/indexing/lsp.py +45 -4
  65. package/src/devcouncil/indexing/repo_mapper.py +1256 -9
  66. package/src/devcouncil/indexing/semantic_index.py +205 -0
  67. package/src/devcouncil/integrations/actions.py +146 -0
  68. package/src/devcouncil/integrations/check.py +423 -0
  69. package/src/devcouncil/integrations/github_intent.py +142 -0
  70. package/src/devcouncil/integrations/gitnexus.py +35 -0
  71. package/src/devcouncil/integrations/mcp/server.py +1552 -29
  72. package/src/devcouncil/integrations/opencode_devcouncil_plugin.mjs +24 -0
  73. package/src/devcouncil/live/cards.py +161 -19
  74. package/src/devcouncil/live/signals.py +2 -2
  75. package/src/devcouncil/live/transcripts.py +9 -6
  76. package/src/devcouncil/llm/cache.py +10 -6
  77. package/src/devcouncil/llm/model_defaults.yaml +44 -0
  78. package/src/devcouncil/llm/provider.py +515 -34
  79. package/src/devcouncil/llm/router.py +231 -46
  80. package/src/devcouncil/optimization/__init__.py +1 -0
  81. package/src/devcouncil/optimization/gepa_agent.py +318 -0
  82. package/src/devcouncil/planning/correction_manifest.py +303 -0
  83. package/src/devcouncil/planning/critique_service.py +7 -2
  84. package/src/devcouncil/planning/plan_service.py +17 -3
  85. package/src/devcouncil/planning/prompt_enhancer_service.py +82 -1
  86. package/src/devcouncil/planning/spec_service.py +27 -1
  87. package/src/devcouncil/repo/ci_scaffold.py +157 -0
  88. package/src/devcouncil/repo/gitignore.py +123 -0
  89. package/src/devcouncil/repo/sca.py +374 -0
  90. package/src/devcouncil/reporting/json_report.py +11 -1
  91. package/src/devcouncil/reporting/markdown_report.py +15 -0
  92. package/src/devcouncil/skills/__init__.py +19 -0
  93. package/src/devcouncil/skills/library/README.md +46 -0
  94. package/src/devcouncil/skills/library/ai-training.md +50 -0
  95. package/src/devcouncil/skills/library/android.md +50 -0
  96. package/src/devcouncil/skills/library/backend.md +52 -0
  97. package/src/devcouncil/skills/library/core-engineering.md +95 -0
  98. package/src/devcouncil/skills/library/data-engineering.md +47 -0
  99. package/src/devcouncil/skills/library/desktop.md +46 -0
  100. package/src/devcouncil/skills/library/devops.md +48 -0
  101. package/src/devcouncil/skills/library/game-dev.md +46 -0
  102. package/src/devcouncil/skills/library/ios.md +48 -0
  103. package/src/devcouncil/skills/library/mobile-cross-platform.md +46 -0
  104. package/src/devcouncil/skills/library/security.md +48 -0
  105. package/src/devcouncil/skills/library/systems.md +48 -0
  106. package/src/devcouncil/skills/library/web.md +47 -0
  107. package/src/devcouncil/skills/library/windows.md +47 -0
  108. package/src/devcouncil/skills/registry.py +330 -0
  109. package/src/devcouncil/storage/db.py +83 -2
  110. package/src/devcouncil/storage/models.py +121 -0
  111. package/src/devcouncil/storage/native.py +557 -0
  112. package/src/devcouncil/storage/repositories.py +137 -75
  113. package/src/devcouncil/telemetry/cost.py +123 -17
  114. package/src/devcouncil/telemetry/model_pricing.yaml +48 -0
  115. package/src/devcouncil/telemetry/pricing.py +28 -0
  116. package/src/devcouncil/telemetry/traces.py +62 -7
  117. package/src/devcouncil/telemetry/tracker.py +12 -9
  118. package/src/devcouncil/ui/dashboard.py +324 -23
  119. package/src/devcouncil/utils/redaction.py +9 -3
  120. package/src/devcouncil/utils/subprocess_env.py +69 -0
  121. package/src/devcouncil/verification/acceptance_compiler.py +125 -0
  122. package/src/devcouncil/verification/ad_hoc_check.py +129 -0
  123. package/src/devcouncil/verification/diff_coverage.py +353 -0
  124. package/src/devcouncil/verification/next_actions.py +189 -0
  125. package/src/devcouncil/verification/sandbox.py +178 -0
  126. package/src/devcouncil/verification/test_resolver.py +91 -0
  127. package/src/devcouncil/verification/verifier.py +1065 -47
  128. package/uv.lock +205 -64
  129. package/src/devcouncil/indexing/symbol_index.py +0 -0
@@ -11,7 +11,9 @@ from devcouncil.storage.repositories import TaskRepository, RequirementRepositor
11
11
  from devcouncil.verification.verifier import Verifier
12
12
  from devcouncil.llm.provider import create_provider, validate_model_provider
13
13
  from devcouncil.llm.router import ModelRouter
14
- from devcouncil.domain.evidence import CommandResult, DiffEvidence, TestEvidence
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
15
17
  from devcouncil.app.config import load_config, get_api_key
16
18
  from devcouncil.app.state_machine import ProjectPhase
17
19
  from devcouncil.integrations.code_review_graph import CodeReviewGraphAdapter
@@ -20,8 +22,32 @@ from devcouncil.telemetry.traces import TraceLogger
20
22
  console = Console()
21
23
  MAX_RENDERED_GAPS = 20
22
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
+
23
48
  def verify(
24
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."),
25
51
  json_format: bool = typer.Option(False, "--json", help="Output machine-readable JSON."),
26
52
  project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
27
53
  ):
@@ -62,18 +88,69 @@ def verify(
62
88
  config = load_config(root)
63
89
  validate_model_provider(config.models.provider)
64
90
  api_key = get_api_key(config.models.provider, root)
65
- provider = create_provider(config.models.provider, api_key)
91
+ provider = create_provider(config.models.provider, api_key, project_root=root)
66
92
  role_config = {name: role.model_dump() for name, role in config.models.roles.items()}
67
- router = ModelRouter(provider, role_config)
93
+ router = ModelRouter(provider, role_config, project_root=root)
68
94
  except Exception:
69
95
  pass
70
96
 
97
+ from devcouncil.verification.sandbox import get_sandbox
98
+
71
99
  verifier = Verifier(root, router=router)
72
100
  total_gaps = 0
73
101
  blocked_tasks = 0
74
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] = {}
75
107
 
76
108
  for task in tasks:
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
77
154
  TraceLogger(root).log_event(
78
155
  "task_verification_started",
79
156
  {"task_id": task.id},
@@ -95,6 +172,7 @@ def verify(
95
172
  evidence_repo.delete_for_task(task.id)
96
173
 
97
174
  gaps, evidence = asyncio.run(verifier.verify_task(task, reqs))
175
+ outcome = verifier.last_outcome
98
176
  total_gaps += len(gaps)
99
177
 
100
178
  for gap in gaps:
@@ -103,11 +181,16 @@ def verify(
103
181
  for ev in evidence:
104
182
  if isinstance(ev, CommandResult):
105
183
  evidence_repo.save_command_result(task.id, ev)
184
+ elif isinstance(ev, DiffCoverageEvidence):
185
+ evidence_repo.save_diff_coverage_evidence(ev)
106
186
  elif isinstance(ev, DiffEvidence):
107
187
  evidence_repo.save_diff_evidence(ev)
108
188
  elif isinstance(ev, TestEvidence):
109
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)
110
192
 
193
+ per_task_gaps[task.id] = gaps
111
194
  if not json_format:
112
195
  _print_task_result(task.id, gaps)
113
196
 
@@ -129,14 +212,59 @@ def verify(
129
212
  summary=f"{task.id} verified",
130
213
  )
131
214
  task_repo.save(task)
215
+ blocking_actions, advisory_actions = split_next_actions(gaps)
132
216
  task_results.append({
133
217
  "task_id": task.id,
134
218
  "status": task.status,
135
219
  "gap_count": len(gaps),
136
220
  "blocking_gap_count": len([gap for gap in gaps if gap.blocking]),
137
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,
138
229
  })
139
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
+
140
268
  StateRepository(session).record_phase(
141
269
  ProjectPhase.TASK_BLOCKED.value if blocked_tasks else ProjectPhase.TASK_VERIFIED.value
142
270
  )
@@ -158,6 +286,13 @@ def verify(
158
286
  else:
159
287
  console.print(f"\n[green]Verified {len(tasks)} tasks successfully.[/green]")
160
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
+
161
296
 
162
297
  def _print_task_result(task_id: str, gaps):
163
298
  if not gaps:
@@ -284,13 +284,13 @@ def repair_all(
284
284
  for item in summary["blocking_cards"]
285
285
  if isinstance(item.get("id"), str)
286
286
  ]
287
- cards = [card for card in cards if card is not None]
288
- prompt = build_bulk_live_repair_prompt(root, cards)
287
+ resolved_cards = [card for card in cards if card is not None]
288
+ prompt = build_bulk_live_repair_prompt(root, resolved_cards)
289
289
  if json_format:
290
290
  typer.echo(json.dumps({
291
291
  "ok": True,
292
292
  "scope_task_id": summary["scope_task_id"],
293
- "cards": [card.model_dump() for card in cards],
293
+ "cards": [card.model_dump() for card in resolved_cards],
294
294
  "prompt": prompt,
295
295
  }, indent=2))
296
296
  return
@@ -557,18 +557,18 @@ def _log_signal_processed(root: Path, signal: ReviewSignal, processed_path: Path
557
557
 
558
558
  async def _review_turn(turn, root: Path, client: str, use_llm: bool, task_id: str | None = None):
559
559
  if not use_llm:
560
- card = review_turn(turn, root, client=client)
561
- return card.model_copy(update={"task_id": task_id}) if task_id else card
560
+ # Pass task_id so the deterministic review can ground completion/evidence
561
+ # claims against the task's real verification state.
562
+ return review_turn(turn, root, client=client, task_id=task_id)
562
563
  try:
563
564
  config = load_config(root)
564
565
  validate_model_provider(config.models.provider)
565
566
  api_key = get_api_key(config.models.provider, root)
566
- provider = create_provider(config.models.provider, api_key)
567
+ provider = create_provider(config.models.provider, api_key, project_root=root)
567
568
  role_config = {name: role.model_dump() for name, role in config.models.roles.items()}
568
- router = ModelRouter(provider, role_config)
569
+ router = ModelRouter(provider, role_config, project_root=root)
569
570
  except Exception as exc:
570
571
  console.print(f"[yellow]Model-backed review unavailable; using deterministic card: {exc}[/yellow]")
571
- card = review_turn(turn, root, client=client)
572
- return card.model_copy(update={"task_id": task_id}) if task_id else card
572
+ return review_turn(turn, root, client=client, task_id=task_id)
573
573
  card = await LiveReviewService(router).review(turn, root, client=client, use_llm=True)
574
574
  return card.model_copy(update={"task_id": task_id}) if task_id else card
@@ -0,0 +1,40 @@
1
+ import json
2
+ import typer
3
+ from pathlib import Path
4
+ from rich.console import Console
5
+
6
+ from devcouncil.cli.commands.init import initialize_project
7
+ from devcouncil.execution.fs_watcher import FilesystemWatcher
8
+
9
+ console = Console()
10
+
11
+
12
+ def watch_fs(
13
+ task_id: str = typer.Option(..., "--task", help="Task ID to attribute file changes to."),
14
+ poll_interval: float = typer.Option(1.0, "--poll-interval"),
15
+ once: bool = typer.Option(False, "--once", help="Scan once and exit."),
16
+ project_root: Path = typer.Option(Path("."), "--project-root"),
17
+ json_format: bool = typer.Option(False, "--json"),
18
+ ):
19
+ root = project_root.expanduser().resolve()
20
+ initialize_project(root, quiet=True)
21
+
22
+ def _print_event(event: dict) -> None:
23
+ status = "allowed" if event["allowed"] else "denied"
24
+ console.print(f"[cyan]{event['path']}[/cyan] {status}: {event['reason']}")
25
+
26
+ watcher = FilesystemWatcher(root, task_id, poll_interval=poll_interval, on_event=_print_event)
27
+ if once:
28
+ events = watcher.scan_once()
29
+ if json_format:
30
+ typer.echo(json.dumps({"events": events}, indent=2))
31
+ else:
32
+ for event in events:
33
+ status = "allowed" if event["allowed"] else "denied"
34
+ console.print(f"[cyan]{event['path']}[/cyan] {status}: {event['reason']}")
35
+ return
36
+ console.print(f"[cyan]Watching filesystem for task {task_id}. Ctrl+C to stop.[/cyan]")
37
+ try:
38
+ watcher.watch()
39
+ except KeyboardInterrupt:
40
+ console.print("[yellow]Stopped filesystem watcher.[/yellow]")
@@ -1,10 +1,40 @@
1
+ import sys
2
+
1
3
  import typer
2
- from pathlib import Path
3
4
 
4
- from devcouncil.cli.commands import (
5
+
6
+ def _configure_stdio() -> None:
7
+ """Make stdout/stderr resilient to non-cp1252 characters.
8
+
9
+ Coding agents and rich output emit Unicode such as ``✓``. On Windows the
10
+ default console / redirected-pipe encoding is cp1252, where an un-encodable
11
+ character raises UnicodeEncodeError mid-write. Because Rich buffers output,
12
+ that error can surface during an unrelated later write — which previously
13
+ got misreported as a coding agent "failing to start". Reconfigure both
14
+ streams to UTF-8 with replacement so output can never crash the process.
15
+ """
16
+ for stream in (sys.stdout, sys.stderr):
17
+ reconfigure = getattr(stream, "reconfigure", None)
18
+ if reconfigure is None:
19
+ continue
20
+ try:
21
+ reconfigure(encoding="utf-8", errors="replace")
22
+ except (ValueError, OSError):
23
+ try:
24
+ reconfigure(errors="replace")
25
+ except (ValueError, OSError):
26
+ pass
27
+
28
+
29
+ _configure_stdio()
30
+
31
+ from devcouncil.cli.commands import ( # noqa: E402 - imports follow stdio reconfiguration
5
32
  artifacts,
33
+ agents,
6
34
  baseline,
35
+ check,
7
36
  config,
37
+ cost,
8
38
  ast,
9
39
  dashboard,
10
40
  doctor,
@@ -22,6 +52,7 @@ from devcouncil.cli.commands import (
22
52
  reset_demo_state,
23
53
  rollback,
24
54
  run,
55
+ runs,
25
56
  setup,
26
57
  show,
27
58
  status,
@@ -30,8 +61,14 @@ from devcouncil.cli.commands import (
30
61
  verify,
31
62
  version,
32
63
  watch,
64
+ shell,
65
+ semantic,
66
+ evidence,
67
+ handoff,
68
+ skills,
69
+ scaffold,
33
70
  )
34
- from devcouncil.cli.commands.init import initialize_project
71
+ from devcouncil.cli.commands.watch_fs import watch_fs # noqa: E402 - imports follow stdio reconfiguration
35
72
 
36
73
  app = typer.Typer(
37
74
  name="dev",
@@ -47,41 +84,53 @@ app.add_typer(report.app, name="report")
47
84
  app.add_typer(rollback.app, name="rollback")
48
85
  app.add_typer(config.app, name="config")
49
86
  app.add_typer(artifacts.app, name="artifacts")
87
+ app.add_typer(agents.app, name="agents")
50
88
  app.add_typer(hook.app, name="hook")
51
89
  app.add_typer(version.app, name="version")
52
90
  app.add_typer(mcp_server.app, name="mcp-server")
53
91
  app.add_typer(integrate.app, name="integrate")
54
92
  app.add_typer(integrate.app, name="integrations")
55
93
  app.add_typer(trace.app, name="trace")
94
+ app.add_typer(cost.app, name="cost")
95
+ app.add_typer(runs.app, name="runs")
56
96
  app.add_typer(setup.app, name="setup")
57
97
  app.add_typer(lsp.app, name="lsp")
58
98
  app.add_typer(ast.app, name="ast")
59
99
  app.add_typer(dashboard.app, name="dashboard")
60
100
  app.add_typer(watch.app, name="watch")
101
+ app.add_typer(semantic.app, name="semantic")
102
+ app.add_typer(evidence.app, name="evidence")
103
+ app.add_typer(skills.app, name="skills")
104
+ watch.app.command("fs")(watch_fs)
61
105
 
62
106
  # Direct command registrations (those defined as def cmd())
63
107
  app.command(name="baseline")(baseline.baseline)
64
108
  app.command(name="e2e")(go.go)
65
109
  app.command(name="go")(go.go)
66
110
  app.command(name="map")(map.map_repo)
111
+ app.command(name="scaffold-ci")(scaffold.scaffold_ci_command)
67
112
  app.command(name="plan")(plan.plan)
113
+ app.command(name="approve")(plan.approve)
68
114
  app.command(name="prompt")(prompt.prompt)
69
115
  app.command(name="reset-demo-state")(reset_demo_state.reset_demo_state)
70
116
  app.command(name="run")(run.run)
117
+ # shell/handoff take a positional TASK_ID followed by options, so they must be
118
+ # plain commands — as typer sub-apps (click groups) the documented
119
+ # `dev shell TASK-001 --command ...` form fails to parse.
120
+ app.command(name="shell")(shell.shell)
121
+ app.command(name="handoff")(handoff.handoff)
71
122
  app.command(name="show")(show.show)
72
123
  app.command(name="verify")(verify.verify)
124
+ app.command(name="check")(check.check)
73
125
  app.command(name="repair")(repair.repair)
74
126
  app.command(name="status")(status.status)
127
+ app.command(name="optimize")(agents.optimize_agent)
75
128
 
76
129
  @app.callback()
77
130
  def main(ctx: typer.Context):
78
131
  """
79
132
  DevCouncil: Gated orchestrator for AI-assisted software development.
80
133
  """
81
- if ctx.invoked_subcommand in {"init", "setup"}:
82
- return
83
-
84
- initialize_project(Path("."), quiet=True)
85
134
  return
86
135
 
87
136
  if __name__ == "__main__":
@@ -1,5 +1,5 @@
1
- from pydantic import BaseModel
2
- from typing import Literal, List
1
+ from pydantic import BaseModel, Field
2
+ from typing import Literal, List, Dict
3
3
 
4
4
  class CommandResult(BaseModel):
5
5
  command: str
@@ -15,6 +15,26 @@ class DiffEvidence(BaseModel):
15
15
  deleted_files: List[str]
16
16
  diff_summary: str
17
17
 
18
+ class DiffCoverageEvidence(BaseModel):
19
+ """Proof that the changed lines were (or were not) exercised by the tests.
20
+
21
+ This is the executable evidence behind DevCouncil's core promise: a passing
22
+ suite is only acceptance evidence if the lines the diff changed were actually
23
+ run. ``measured`` is False when no reliable signal could be computed (no
24
+ coverage tool, no instrumentable test command, or no changed executable
25
+ lines), in which case it must never be read as a defect.
26
+ """
27
+
28
+ task_id: str
29
+ tool: str = ""
30
+ measured: bool = False
31
+ changed_lines: int = 0
32
+ covered_lines: int = 0
33
+ coverage_ratio: float = 0.0
34
+ uncovered_by_file: Dict[str, List[int]] = Field(default_factory=dict)
35
+ absent_files: List[str] = Field(default_factory=list)
36
+ summary: str = ""
37
+
18
38
  class VerificationEvidence(BaseModel):
19
39
  __test__ = False # Prevent pytest from collecting this as a test class
20
40
  requirement_id: str
@@ -11,12 +11,17 @@ class Gap(BaseModel):
11
11
  "orphan_diff",
12
12
  "missing_test",
13
13
  "test_failed",
14
+ "invalid_verification_command",
14
15
  "acceptance_criteria_unproven",
16
+ "diff_not_exercised",
15
17
  "assumption_violated",
16
18
  "architecture_drift",
17
19
  "security_risk",
18
20
  "dependency_risk",
19
- "migration_gap"
21
+ "migration_gap",
22
+ "quality_gate_failed",
23
+ "skipped_verification_command",
24
+ "coarse_acceptance_proof",
20
25
  ]
21
26
  requirement_id: Optional[str] = None
22
27
  task_id: Optional[str] = None
@@ -24,3 +29,24 @@ class Gap(BaseModel):
24
29
  evidence: List[str] = Field(default_factory=list)
25
30
  recommended_fix: str
26
31
  blocking: bool
32
+ # Machine-actionable hints for the agent self-repair loop. Populated at gap
33
+ # creation where known; consumed by the typed next-actions contract (see
34
+ # devcouncil.verification.next_actions). As of schema v4 these are persisted by
35
+ # the gap store and round-tripped on reload, so a reconnecting agent gets the
36
+ # full repair contract rather than a heuristic reconstruction.
37
+ file: Optional[str] = None
38
+ line: Optional[int] = None
39
+ suggested_command: Optional[str] = None
40
+ # The acceptance criterion this gap is about (when applicable), so the agent can
41
+ # tie a failure straight back to the criterion it must satisfy.
42
+ acceptance_criterion_id: Optional[str] = None
43
+ # Paths to the captured stdout/stderr logs (written under .devcouncil/logs) for the
44
+ # failing command behind this gap, so the agent can open the FULL failure output
45
+ # without re-running. Optional and defaulted for backward compatibility; the gap
46
+ # store does not persist these, so they are only present on a fresh verify run.
47
+ stdout_path: Optional[str] = None
48
+ stderr_path: Optional[str] = None
49
+ # Expected verification method (e.g. "unit_test"/"static_check") for an unproven
50
+ # acceptance criterion, so missing-evidence routing is concrete rather than a
51
+ # restatement of the description.
52
+ expected_verification_method: Optional[str] = None
@@ -13,9 +13,38 @@ class Task(BaseModel):
13
13
  requirement_ids: List[str] = Field(default_factory=list)
14
14
  acceptance_criterion_ids: List[str] = Field(default_factory=list)
15
15
  planned_files: List[PlannedFile] = Field(default_factory=list)
16
- expected_tests: List[str] = Field(default_factory=list)
17
- allowed_commands: List[str] = Field(default_factory=list)
16
+ expected_tests: List[str] = Field(
17
+ default_factory=list,
18
+ description=(
19
+ "Runnable shell commands (not prose) that prove this task's acceptance "
20
+ "criteria when they exit 0; they are executed verbatim by the verifier. "
21
+ "They must run right after THIS task with no missing tools or test files. "
22
+ "Prefer self-contained inline assertions, e.g. "
23
+ "python -c \"import calc; assert calc.subtract(10,3)==7\". Use pytest only "
24
+ "on a test file this or an earlier task creates, running the whole file "
25
+ "(python -m pytest tests/test_x.py -q), never a ::name node. Do not use "
26
+ "linters/type-checkers (flake8, mypy, ruff, eslint, tsc, npm) unless the "
27
+ "repo is already configured for them."
28
+ ),
29
+ )
30
+ allowed_commands: List[str] = Field(
31
+ default_factory=list,
32
+ description=(
33
+ "Shell commands the executor and verifier are permitted to run for this "
34
+ "task (build/test/lint commands the implementation may invoke). Literal "
35
+ "commands only, e.g. 'python -m pytest -q', 'uv run pytest'. Leave empty "
36
+ "only when the task needs no commands beyond those in expected_tests."
37
+ ),
38
+ )
18
39
  forbidden_changes: List[str] = Field(default_factory=list)
40
+ depends_on: List[str] = Field(
41
+ default_factory=list,
42
+ description=(
43
+ "IDs of tasks that must complete before this one (e.g. a task that creates a "
44
+ "module this task imports/tests). Used to order execution and to skip a task "
45
+ "whose prerequisites are unmet rather than letting it fail spuriously."
46
+ ),
47
+ )
19
48
  status: Literal[
20
49
  "planned",
21
50
  "ready",