devcouncil 0.1.1 → 0.3.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 (159) hide show
  1. package/README.md +201 -6
  2. package/package.json +9 -2
  3. package/pyproject.toml +34 -2
  4. package/src/devcouncil/app/config.py +348 -12
  5. package/src/devcouncil/app/orchestrator.py +10 -6
  6. package/src/devcouncil/app/state_machine.py +4 -0
  7. package/src/devcouncil/artifacts/graph.py +32 -5
  8. package/src/devcouncil/assets/__init__.py +1 -0
  9. package/src/devcouncil/assets/devcouncil-logo.svg +60 -0
  10. package/src/devcouncil/assets/devcouncil_logo_premium.png +0 -0
  11. package/src/devcouncil/cli/commands/agents.py +292 -0
  12. package/src/devcouncil/cli/commands/artifacts.py +6 -3
  13. package/src/devcouncil/cli/commands/check.py +220 -0
  14. package/src/devcouncil/cli/commands/config.py +43 -4
  15. package/src/devcouncil/cli/commands/cost.py +57 -0
  16. package/src/devcouncil/cli/commands/dashboard.py +6 -1
  17. package/src/devcouncil/cli/commands/design.py +186 -0
  18. package/src/devcouncil/cli/commands/doctor.py +379 -22
  19. package/src/devcouncil/cli/commands/evidence.py +48 -0
  20. package/src/devcouncil/cli/commands/go.py +532 -33
  21. package/src/devcouncil/cli/commands/handoff.py +69 -0
  22. package/src/devcouncil/cli/commands/hook.py +296 -15
  23. package/src/devcouncil/cli/commands/init.py +161 -20
  24. package/src/devcouncil/cli/commands/integrate.py +1371 -124
  25. package/src/devcouncil/cli/commands/logs.py +106 -0
  26. package/src/devcouncil/cli/commands/map.py +80 -10
  27. package/src/devcouncil/cli/commands/okf.py +245 -0
  28. package/src/devcouncil/cli/commands/plan.py +256 -55
  29. package/src/devcouncil/cli/commands/prompt.py +18 -7
  30. package/src/devcouncil/cli/commands/repair.py +50 -24
  31. package/src/devcouncil/cli/commands/report.py +8 -0
  32. package/src/devcouncil/cli/commands/reset_demo_state.py +4 -2
  33. package/src/devcouncil/cli/commands/rollback.py +27 -28
  34. package/src/devcouncil/cli/commands/run.py +195 -54
  35. package/src/devcouncil/cli/commands/runs.py +223 -0
  36. package/src/devcouncil/cli/commands/scaffold.py +32 -0
  37. package/src/devcouncil/cli/commands/semantic.py +47 -0
  38. package/src/devcouncil/cli/commands/setup.py +145 -6
  39. package/src/devcouncil/cli/commands/shell.py +73 -0
  40. package/src/devcouncil/cli/commands/skills.py +267 -0
  41. package/src/devcouncil/cli/commands/status.py +30 -15
  42. package/src/devcouncil/cli/commands/trace.py +47 -3
  43. package/src/devcouncil/cli/commands/verify.py +144 -3
  44. package/src/devcouncil/cli/commands/watch.py +32 -12
  45. package/src/devcouncil/cli/commands/watch_fs.py +40 -0
  46. package/src/devcouncil/cli/main.py +91 -7
  47. package/src/devcouncil/domain/evidence.py +29 -2
  48. package/src/devcouncil/domain/gap.py +27 -1
  49. package/src/devcouncil/domain/task.py +31 -2
  50. package/src/devcouncil/execution/checkpoints.py +256 -0
  51. package/src/devcouncil/execution/context_builder.py +1 -1
  52. package/src/devcouncil/execution/fs_watcher.py +205 -0
  53. package/src/devcouncil/execution/handoff.py +102 -0
  54. package/src/devcouncil/execution/hook_policy.py +162 -74
  55. package/src/devcouncil/execution/patch.py +65 -10
  56. package/src/devcouncil/execution/permissions.py +24 -24
  57. package/src/devcouncil/execution/policy_engine.py +350 -0
  58. package/src/devcouncil/execution/prompt_builder.py +751 -23
  59. package/src/devcouncil/execution/shell_session.py +231 -0
  60. package/src/devcouncil/execution/task_runner.py +24 -9
  61. package/src/devcouncil/executors/agent_registry.py +596 -0
  62. package/src/devcouncil/executors/coding_cli.py +791 -39
  63. package/src/devcouncil/executors/mini_swe.py +6 -0
  64. package/src/devcouncil/executors/native/agent.py +135 -19
  65. package/src/devcouncil/executors/openhands.py +6 -0
  66. package/src/devcouncil/gating/checks/clean_git.py +3 -1
  67. package/src/devcouncil/gating/checks/secret_scan_check.py +47 -21
  68. package/src/devcouncil/gating/policy.py +190 -11
  69. package/src/devcouncil/hardware.py +184 -0
  70. package/src/devcouncil/indexing/ast_matcher.py +17 -7
  71. package/src/devcouncil/indexing/lsp.py +45 -4
  72. package/src/devcouncil/indexing/repo_mapper.py +1284 -15
  73. package/src/devcouncil/indexing/semantic_index.py +221 -0
  74. package/src/devcouncil/integrations/actions.py +166 -0
  75. package/src/devcouncil/integrations/check.py +426 -0
  76. package/src/devcouncil/integrations/claude_assets.py +444 -0
  77. package/src/devcouncil/integrations/code_review_graph.py +13 -2
  78. package/src/devcouncil/integrations/github_intent.py +149 -0
  79. package/src/devcouncil/integrations/gitnexus.py +45 -2
  80. package/src/devcouncil/integrations/mcp/server.py +1944 -32
  81. package/src/devcouncil/integrations/opencode_devcouncil_plugin.mjs +24 -0
  82. package/src/devcouncil/integrations/pr_comments.py +9 -0
  83. package/src/devcouncil/knowledge/__init__.py +23 -0
  84. package/src/devcouncil/knowledge/design.py +374 -0
  85. package/src/devcouncil/knowledge/design_conformance.py +317 -0
  86. package/src/devcouncil/knowledge/fetch.py +223 -0
  87. package/src/devcouncil/knowledge/frontmatter.py +51 -0
  88. package/src/devcouncil/knowledge/okf.py +202 -0
  89. package/src/devcouncil/knowledge/skill_bridge.py +96 -0
  90. package/src/devcouncil/knowledge/sources.py +239 -0
  91. package/src/devcouncil/live/cards.py +181 -25
  92. package/src/devcouncil/live/repair_prompt.py +29 -6
  93. package/src/devcouncil/live/reviewer.py +72 -13
  94. package/src/devcouncil/live/signals.py +2 -2
  95. package/src/devcouncil/live/summary.py +18 -8
  96. package/src/devcouncil/live/transcripts.py +47 -11
  97. package/src/devcouncil/llm/cache.py +20 -8
  98. package/src/devcouncil/llm/model_defaults.yaml +44 -0
  99. package/src/devcouncil/llm/provider.py +617 -49
  100. package/src/devcouncil/llm/router.py +337 -53
  101. package/src/devcouncil/optimization/__init__.py +1 -0
  102. package/src/devcouncil/optimization/gepa_agent.py +318 -0
  103. package/src/devcouncil/optimization/skillopt.py +673 -0
  104. package/src/devcouncil/planning/arbiter_service.py +10 -2
  105. package/src/devcouncil/planning/correction_manifest.py +346 -0
  106. package/src/devcouncil/planning/critique_service.py +16 -4
  107. package/src/devcouncil/planning/plan_service.py +86 -6
  108. package/src/devcouncil/planning/prompt_enhancer_service.py +206 -1
  109. package/src/devcouncil/planning/repair_service.py +8 -2
  110. package/src/devcouncil/planning/spec_service.py +37 -3
  111. package/src/devcouncil/repo/ci_scaffold.py +165 -0
  112. package/src/devcouncil/repo/gitignore.py +123 -0
  113. package/src/devcouncil/repo/sca.py +384 -0
  114. package/src/devcouncil/reporting/json_report.py +22 -1
  115. package/src/devcouncil/reporting/markdown_report.py +29 -1
  116. package/src/devcouncil/reporting/okf_bundle_writer.py +364 -0
  117. package/src/devcouncil/reporting/okf_html.py +323 -0
  118. package/src/devcouncil/reporting/report_builder.py +18 -1
  119. package/src/devcouncil/skills/__init__.py +19 -0
  120. package/src/devcouncil/skills/library/README.md +46 -0
  121. package/src/devcouncil/skills/library/ai-training.md +50 -0
  122. package/src/devcouncil/skills/library/android.md +50 -0
  123. package/src/devcouncil/skills/library/backend.md +52 -0
  124. package/src/devcouncil/skills/library/core-engineering.md +95 -0
  125. package/src/devcouncil/skills/library/data-engineering.md +47 -0
  126. package/src/devcouncil/skills/library/desktop.md +46 -0
  127. package/src/devcouncil/skills/library/devops.md +48 -0
  128. package/src/devcouncil/skills/library/game-dev.md +46 -0
  129. package/src/devcouncil/skills/library/ios.md +48 -0
  130. package/src/devcouncil/skills/library/mobile-cross-platform.md +46 -0
  131. package/src/devcouncil/skills/library/security.md +48 -0
  132. package/src/devcouncil/skills/library/systems.md +48 -0
  133. package/src/devcouncil/skills/library/web.md +47 -0
  134. package/src/devcouncil/skills/library/windows.md +47 -0
  135. package/src/devcouncil/skills/registry.py +408 -0
  136. package/src/devcouncil/storage/db.py +140 -3
  137. package/src/devcouncil/storage/models.py +125 -0
  138. package/src/devcouncil/storage/native.py +559 -0
  139. package/src/devcouncil/storage/repositories.py +157 -78
  140. package/src/devcouncil/telemetry/cost.py +123 -17
  141. package/src/devcouncil/telemetry/logging_setup.py +244 -0
  142. package/src/devcouncil/telemetry/model_pricing.yaml +48 -0
  143. package/src/devcouncil/telemetry/pricing.py +28 -0
  144. package/src/devcouncil/telemetry/stages.py +141 -0
  145. package/src/devcouncil/telemetry/traces.py +62 -7
  146. package/src/devcouncil/telemetry/tracker.py +24 -10
  147. package/src/devcouncil/ui/dashboard.py +393 -28
  148. package/src/devcouncil/utils/redaction.py +9 -3
  149. package/src/devcouncil/utils/subprocess_env.py +69 -0
  150. package/src/devcouncil/verification/acceptance_compiler.py +253 -0
  151. package/src/devcouncil/verification/ad_hoc_check.py +135 -0
  152. package/src/devcouncil/verification/diff_coverage.py +353 -0
  153. package/src/devcouncil/verification/implementation_reviewer.py +11 -2
  154. package/src/devcouncil/verification/next_actions.py +189 -0
  155. package/src/devcouncil/verification/sandbox.py +181 -0
  156. package/src/devcouncil/verification/test_resolver.py +91 -0
  157. package/src/devcouncil/verification/verifier.py +1549 -143
  158. package/uv.lock +205 -64
  159. package/src/devcouncil/indexing/symbol_index.py +0 -0
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import json
4
4
  import asyncio
5
+ import logging
5
6
  import time
6
7
  from pathlib import Path
7
8
 
@@ -32,6 +33,7 @@ from devcouncil.telemetry.traces import TraceLogger
32
33
 
33
34
  app = typer.Typer(help="Review active coding-agent sessions and emit critique cards.")
34
35
  console = Console()
36
+ logger = logging.getLogger(__name__)
35
37
 
36
38
 
37
39
  @app.command("sessions")
@@ -72,6 +74,9 @@ def review(
72
74
  ):
73
75
  """Review the latest assistant response in a coding-agent transcript."""
74
76
  root = project_root.expanduser().resolve()
77
+ from devcouncil.telemetry.logging_setup import set_log_dir
78
+ set_log_dir(root)
79
+ logger.info("dev watch review: client=%s llm=%s", client, llm)
75
80
  transcript_path = _resolve_transcript(root, client, transcript=transcript, session=session, latest=latest)
76
81
  if transcript_path is None:
77
82
  message = "No transcript selected. Use --transcript, --session, or --latest."
@@ -91,6 +96,7 @@ def review(
91
96
 
92
97
  scoped_task_id = task_id or active_task_id(root)
93
98
  card = asyncio.run(_review_turn(turn, root, client, llm, task_id=scoped_task_id))
99
+ logger.info("dev watch review: card %s verdict=%s task=%s", card.id, card.verdict, scoped_task_id or "(unscoped)")
94
100
  saved_path, duplicate = _save_card_once(root, card, persist=persist, force=force)
95
101
  if saved_path:
96
102
  _log_card_reviewed(root, card, saved_path, duplicate=duplicate, source="review")
@@ -279,18 +285,21 @@ def repair_all(
279
285
  """Generate repair prompts for all blocking live-review cards in scope."""
280
286
  root = project_root.expanduser().resolve()
281
287
  summary = live_review_summary(root, task_id=task_id)
288
+ all_cards = {card.id: card for card in load_cards(root)}
289
+ # Guard the membership test with isinstance(str): a malformed (non-hashable) id would
290
+ # otherwise raise TypeError on `in`, whereas the old per-id lookup just skipped it.
282
291
  cards = [
283
- get_card(root, item["id"])
292
+ all_cards[item["id"]]
284
293
  for item in summary["blocking_cards"]
285
- if isinstance(item.get("id"), str)
294
+ if isinstance(item.get("id"), str) and item["id"] in all_cards
286
295
  ]
287
- cards = [card for card in cards if card is not None]
288
- prompt = build_bulk_live_repair_prompt(root, cards)
296
+ resolved_cards = [card for card in cards if card is not None]
297
+ prompt = build_bulk_live_repair_prompt(root, resolved_cards)
289
298
  if json_format:
290
299
  typer.echo(json.dumps({
291
300
  "ok": True,
292
301
  "scope_task_id": summary["scope_task_id"],
293
- "cards": [card.model_dump() for card in cards],
302
+ "cards": [card.model_dump() for card in resolved_cards],
294
303
  "prompt": prompt,
295
304
  }, indent=2))
296
305
  return
@@ -331,6 +340,10 @@ def pending(
331
340
  ):
332
341
  """Review every pending response-ready signal that includes a transcript path."""
333
342
  root = project_root.expanduser().resolve()
343
+ from devcouncil.telemetry.logging_setup import set_log_dir
344
+ set_log_dir(root)
345
+ logger.info("dev watch pending: reviewing signals (client=%s, llm=%s)", client or "all", llm)
346
+ fallback_task_id = active_task_id(root)
334
347
  reviewed = []
335
348
  skipped = []
336
349
  for signal in _filtered_signals(root, client):
@@ -342,7 +355,7 @@ def pending(
342
355
  if turn is None:
343
356
  skipped.append({"signal": signal.model_dump(), "reason": f"No assistant turn found in {transcript_path}."})
344
357
  continue
345
- scoped_task_id = task_id or signal.task_id or active_task_id(root)
358
+ scoped_task_id = task_id or signal.task_id or fallback_task_id
346
359
  card = asyncio.run(_review_turn(turn, root, signal.client, llm, task_id=scoped_task_id))
347
360
  saved_path, duplicate = _save_card_once(root, card, persist=True, force=force)
348
361
  if saved_path:
@@ -363,6 +376,7 @@ def pending(
363
376
  else:
364
377
  console.print(f"[green]Saved critique card:[/green] {saved_path}")
365
378
 
379
+ logger.info("dev watch pending complete: %d reviewed, %d skipped", len(reviewed), len(skipped))
366
380
  if json_format:
367
381
  typer.echo(json.dumps({"reviewed": reviewed, "skipped": skipped}, indent=2))
368
382
  return
@@ -385,11 +399,15 @@ def follow(
385
399
  ):
386
400
  """Poll a transcript and emit a critique card whenever the latest assistant turn changes."""
387
401
  root = project_root.expanduser().resolve()
402
+ from devcouncil.telemetry.logging_setup import set_log_dir
403
+ set_log_dir(root)
388
404
  transcript_path = _resolve_transcript(root, client, transcript=transcript, session=session, latest=latest)
389
405
  if transcript_path is None:
406
+ logger.warning("dev watch follow: no transcript selected")
390
407
  console.print("[red]No transcript selected. Use --transcript, --session, or --latest.[/red]")
391
408
  raise typer.Exit(code=2)
392
409
  seen_turn_id: str | None = None
410
+ logger.info("dev watch follow: watching %s (client=%s, interval=%ss, llm=%s)", transcript_path, client, interval, llm)
393
411
  console.print(f"[cyan]Watching transcript:[/cyan] {transcript_path}")
394
412
  while True:
395
413
  turn = latest_assistant_turn(transcript_path, client=client)
@@ -397,6 +415,7 @@ def follow(
397
415
  seen_turn_id = turn.turn_id
398
416
  scoped_task_id = task_id or active_task_id(root)
399
417
  card = asyncio.run(_review_turn(turn, root, client, llm, task_id=scoped_task_id))
418
+ logger.info("dev watch follow: new turn %s → card %s verdict=%s", turn.turn_id, card.id, card.verdict)
400
419
  saved_path, duplicate = _save_card_once(root, card, persist=True, force=force)
401
420
  if saved_path:
402
421
  _log_card_reviewed(root, card, saved_path, duplicate=duplicate, source="follow")
@@ -557,18 +576,19 @@ def _log_signal_processed(root: Path, signal: ReviewSignal, processed_path: Path
557
576
 
558
577
  async def _review_turn(turn, root: Path, client: str, use_llm: bool, task_id: str | None = None):
559
578
  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
579
+ # Pass task_id so the deterministic review can ground completion/evidence
580
+ # claims against the task's real verification state.
581
+ return review_turn(turn, root, client=client, task_id=task_id)
562
582
  try:
563
583
  config = load_config(root)
564
584
  validate_model_provider(config.models.provider)
565
585
  api_key = get_api_key(config.models.provider, root)
566
- provider = create_provider(config.models.provider, api_key)
586
+ provider = create_provider(config.models.provider, api_key, project_root=root, provider_prefs=config.provider)
567
587
  role_config = {name: role.model_dump() for name, role in config.models.roles.items()}
568
- router = ModelRouter(provider, role_config)
588
+ router = ModelRouter(provider, role_config, project_root=root)
569
589
  except Exception as exc:
590
+ logger.warning("Model-backed live review unavailable; using deterministic card: %s", exc)
570
591
  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
592
+ return review_turn(turn, root, client=client, task_id=task_id)
573
593
  card = await LiveReviewService(router).review(turn, root, client=client, use_llm=True)
574
594
  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,20 +1,53 @@
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,
40
+ design,
10
41
  doctor,
11
42
  go,
12
43
  hook,
13
44
  init,
14
45
  integrate,
46
+ logs,
15
47
  lsp,
16
48
  map,
17
49
  mcp_server,
50
+ okf,
18
51
  plan,
19
52
  prompt,
20
53
  repair,
@@ -22,6 +55,7 @@ from devcouncil.cli.commands import (
22
55
  reset_demo_state,
23
56
  rollback,
24
57
  run,
58
+ runs,
25
59
  setup,
26
60
  show,
27
61
  status,
@@ -30,8 +64,14 @@ from devcouncil.cli.commands import (
30
64
  verify,
31
65
  version,
32
66
  watch,
67
+ shell,
68
+ semantic,
69
+ evidence,
70
+ handoff,
71
+ skills,
72
+ scaffold,
33
73
  )
34
- from devcouncil.cli.commands.init import initialize_project
74
+ from devcouncil.cli.commands.watch_fs import watch_fs # noqa: E402 - imports follow stdio reconfiguration
35
75
 
36
76
  app = typer.Typer(
37
77
  name="dev",
@@ -47,41 +87,85 @@ app.add_typer(report.app, name="report")
47
87
  app.add_typer(rollback.app, name="rollback")
48
88
  app.add_typer(config.app, name="config")
49
89
  app.add_typer(artifacts.app, name="artifacts")
90
+ app.add_typer(agents.app, name="agents")
50
91
  app.add_typer(hook.app, name="hook")
51
92
  app.add_typer(version.app, name="version")
52
93
  app.add_typer(mcp_server.app, name="mcp-server")
53
94
  app.add_typer(integrate.app, name="integrate")
54
95
  app.add_typer(integrate.app, name="integrations")
55
96
  app.add_typer(trace.app, name="trace")
97
+ app.add_typer(logs.app, name="logs")
98
+ app.add_typer(cost.app, name="cost")
99
+ app.add_typer(runs.app, name="runs")
56
100
  app.add_typer(setup.app, name="setup")
57
101
  app.add_typer(lsp.app, name="lsp")
58
102
  app.add_typer(ast.app, name="ast")
59
103
  app.add_typer(dashboard.app, name="dashboard")
60
104
  app.add_typer(watch.app, name="watch")
105
+ app.add_typer(semantic.app, name="semantic")
106
+ app.add_typer(evidence.app, name="evidence")
107
+ app.add_typer(skills.app, name="skills")
108
+ app.add_typer(okf.app, name="okf")
109
+ app.add_typer(design.app, name="design")
110
+ watch.app.command("fs")(watch_fs)
61
111
 
62
112
  # Direct command registrations (those defined as def cmd())
63
113
  app.command(name="baseline")(baseline.baseline)
64
114
  app.command(name="e2e")(go.go)
65
115
  app.command(name="go")(go.go)
66
116
  app.command(name="map")(map.map_repo)
117
+ app.command(name="scaffold-ci")(scaffold.scaffold_ci_command)
67
118
  app.command(name="plan")(plan.plan)
119
+ app.command(name="approve")(plan.approve)
68
120
  app.command(name="prompt")(prompt.prompt)
69
121
  app.command(name="reset-demo-state")(reset_demo_state.reset_demo_state)
70
122
  app.command(name="run")(run.run)
123
+ # shell/handoff take a positional TASK_ID followed by options, so they must be
124
+ # plain commands — as typer sub-apps (click groups) the documented
125
+ # `dev shell TASK-001 --command ...` form fails to parse.
126
+ app.command(name="shell")(shell.shell)
127
+ app.command(name="handoff")(handoff.handoff)
71
128
  app.command(name="show")(show.show)
72
129
  app.command(name="verify")(verify.verify)
130
+ app.command(name="check")(check.check)
73
131
  app.command(name="repair")(repair.repair)
74
132
  app.command(name="status")(status.status)
133
+ app.command(name="optimize")(agents.optimize_agent)
75
134
 
76
135
  @app.callback()
77
- def main(ctx: typer.Context):
136
+ def main(
137
+ ctx: typer.Context,
138
+ verbose: int = typer.Option(
139
+ 0,
140
+ "--verbose",
141
+ "-v",
142
+ count=True,
143
+ help="Increase console log verbosity (-v INFO, -vv DEBUG). Everything is "
144
+ "always captured at DEBUG in .devcouncil/logs/devcouncil.log.",
145
+ ),
146
+ quiet: bool = typer.Option(
147
+ False,
148
+ "--quiet",
149
+ "-q",
150
+ help="Only show errors on the console (the log file still captures everything).",
151
+ ),
152
+ log_level: str = typer.Option(
153
+ None,
154
+ "--log-level",
155
+ help="Explicit console log level (DEBUG/INFO/WARNING/ERROR). Overrides -v/-q "
156
+ "and the DEVCOUNCIL_LOG_LEVEL env var.",
157
+ ),
158
+ ):
78
159
  """
79
160
  DevCouncil: Gated orchestrator for AI-assisted software development.
80
161
  """
81
- if ctx.invoked_subcommand in {"init", "setup"}:
82
- return
162
+ # Configure logging once, up front, for every command. Without this the many
163
+ # logger.info/debug calls across the orchestrator, planner, executors and
164
+ # verifier go nowhere — which is exactly why recurring run failures were so
165
+ # hard to diagnose. The durable DEBUG log lands in .devcouncil/logs/.
166
+ from devcouncil.telemetry.logging_setup import configure_logging
83
167
 
84
- initialize_project(Path("."), quiet=True)
168
+ configure_logging(verbosity=verbose, quiet=quiet, log_level=log_level)
85
169
  return
86
170
 
87
171
  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
@@ -22,6 +42,13 @@ class VerificationEvidence(BaseModel):
22
42
  command: str
23
43
  status: Literal["passed", "failed", "not_run"]
24
44
  evidence_summary: str
45
+ # HOW the criterion was proven, for auditing the gate's rigor (distinct from the
46
+ # pass/fail status). ``compiled`` = one DevCouncil per-criterion check passed;
47
+ # ``vote`` = a majority of independent checks passed (self-consistency);
48
+ # ``coarse`` = proven only by a passing acceptance-capable command, not a check tied
49
+ # to the criterion (weakest). Empty for legacy/unspecified evidence. Persisted in the
50
+ # evidence JSON blob, so adding it needs no migration; old rows default to "".
51
+ mode: Literal["compiled", "vote", "coarse", ""] = ""
25
52
 
26
53
  # Backward-compatible alias
27
54
  TestEvidence = VerificationEvidence
@@ -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",