devcouncil 0.2.0 → 0.3.1

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 (98) hide show
  1. package/README.md +12 -1
  2. package/package.json +1 -1
  3. package/pyproject.toml +1 -1
  4. package/src/devcouncil/app/config.py +181 -7
  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 +9 -2
  8. package/src/devcouncil/cli/commands/check.py +12 -1
  9. package/src/devcouncil/cli/commands/design.py +186 -0
  10. package/src/devcouncil/cli/commands/doctor.py +160 -3
  11. package/src/devcouncil/cli/commands/go.py +96 -16
  12. package/src/devcouncil/cli/commands/hook.py +172 -0
  13. package/src/devcouncil/cli/commands/init.py +7 -2
  14. package/src/devcouncil/cli/commands/integrate.py +492 -34
  15. package/src/devcouncil/cli/commands/logs.py +106 -0
  16. package/src/devcouncil/cli/commands/okf.py +245 -0
  17. package/src/devcouncil/cli/commands/plan.py +54 -14
  18. package/src/devcouncil/cli/commands/repair.py +12 -3
  19. package/src/devcouncil/cli/commands/run.py +128 -7
  20. package/src/devcouncil/cli/commands/skills.py +180 -1
  21. package/src/devcouncil/cli/commands/status.py +7 -16
  22. package/src/devcouncil/cli/commands/verify.py +16 -10
  23. package/src/devcouncil/cli/commands/watch.py +24 -4
  24. package/src/devcouncil/cli/main.py +36 -1
  25. package/src/devcouncil/domain/evidence.py +7 -0
  26. package/src/devcouncil/execution/checkpoints.py +12 -2
  27. package/src/devcouncil/execution/fs_watcher.py +27 -2
  28. package/src/devcouncil/execution/handoff.py +1 -1
  29. package/src/devcouncil/execution/patch.py +6 -0
  30. package/src/devcouncil/execution/permissions.py +7 -0
  31. package/src/devcouncil/execution/policy_engine.py +12 -5
  32. package/src/devcouncil/execution/prompt_builder.py +126 -10
  33. package/src/devcouncil/execution/shell_session.py +6 -0
  34. package/src/devcouncil/execution/task_runner.py +18 -7
  35. package/src/devcouncil/executors/agent_registry.py +22 -1
  36. package/src/devcouncil/executors/coding_cli.py +133 -5
  37. package/src/devcouncil/executors/mini_swe.py +6 -0
  38. package/src/devcouncil/executors/native/agent.py +15 -0
  39. package/src/devcouncil/executors/openhands.py +6 -0
  40. package/src/devcouncil/gating/checks/secret_scan_check.py +7 -0
  41. package/src/devcouncil/gating/policy.py +38 -7
  42. package/src/devcouncil/indexing/ast_matcher.py +16 -6
  43. package/src/devcouncil/indexing/repo_mapper.py +30 -8
  44. package/src/devcouncil/indexing/semantic_index.py +42 -26
  45. package/src/devcouncil/integrations/actions.py +24 -4
  46. package/src/devcouncil/integrations/check.py +7 -4
  47. package/src/devcouncil/integrations/claude_assets.py +444 -0
  48. package/src/devcouncil/integrations/code_review_graph.py +13 -2
  49. package/src/devcouncil/integrations/github_intent.py +8 -1
  50. package/src/devcouncil/integrations/gitnexus.py +10 -2
  51. package/src/devcouncil/integrations/mcp/server.py +404 -15
  52. package/src/devcouncil/integrations/pr_comments.py +9 -0
  53. package/src/devcouncil/knowledge/__init__.py +23 -0
  54. package/src/devcouncil/knowledge/design.py +374 -0
  55. package/src/devcouncil/knowledge/design_conformance.py +317 -0
  56. package/src/devcouncil/knowledge/fetch.py +223 -0
  57. package/src/devcouncil/knowledge/frontmatter.py +51 -0
  58. package/src/devcouncil/knowledge/okf.py +202 -0
  59. package/src/devcouncil/knowledge/skill_bridge.py +96 -0
  60. package/src/devcouncil/knowledge/sources.py +239 -0
  61. package/src/devcouncil/live/cards.py +20 -6
  62. package/src/devcouncil/live/repair_prompt.py +29 -6
  63. package/src/devcouncil/live/reviewer.py +72 -13
  64. package/src/devcouncil/live/summary.py +18 -8
  65. package/src/devcouncil/live/transcripts.py +38 -5
  66. package/src/devcouncil/llm/cache.py +14 -6
  67. package/src/devcouncil/llm/provider.py +179 -92
  68. package/src/devcouncil/llm/router.py +122 -23
  69. package/src/devcouncil/optimization/skillopt.py +673 -0
  70. package/src/devcouncil/planning/arbiter_service.py +10 -2
  71. package/src/devcouncil/planning/correction_manifest.py +47 -4
  72. package/src/devcouncil/planning/critique_service.py +9 -2
  73. package/src/devcouncil/planning/plan_service.py +69 -3
  74. package/src/devcouncil/planning/prompt_enhancer_service.py +124 -0
  75. package/src/devcouncil/planning/repair_service.py +8 -2
  76. package/src/devcouncil/planning/spec_service.py +10 -2
  77. package/src/devcouncil/repo/ci_scaffold.py +13 -5
  78. package/src/devcouncil/repo/sca.py +11 -1
  79. package/src/devcouncil/reporting/json_report.py +11 -0
  80. package/src/devcouncil/reporting/markdown_report.py +14 -1
  81. package/src/devcouncil/reporting/okf_bundle_writer.py +364 -0
  82. package/src/devcouncil/reporting/okf_html.py +323 -0
  83. package/src/devcouncil/reporting/report_builder.py +18 -1
  84. package/src/devcouncil/skills/registry.py +111 -33
  85. package/src/devcouncil/storage/db.py +58 -2
  86. package/src/devcouncil/storage/models.py +4 -0
  87. package/src/devcouncil/storage/native.py +20 -18
  88. package/src/devcouncil/storage/repositories.py +35 -18
  89. package/src/devcouncil/telemetry/logging_setup.py +244 -0
  90. package/src/devcouncil/telemetry/stages.py +141 -0
  91. package/src/devcouncil/telemetry/tracker.py +12 -1
  92. package/src/devcouncil/ui/dashboard.py +69 -5
  93. package/src/devcouncil/verification/acceptance_compiler.py +147 -19
  94. package/src/devcouncil/verification/ad_hoc_check.py +6 -0
  95. package/src/devcouncil/verification/implementation_reviewer.py +11 -2
  96. package/src/devcouncil/verification/sandbox.py +7 -4
  97. package/src/devcouncil/verification/verifier.py +905 -517
  98. package/uv.lock +1 -1
@@ -11,6 +11,7 @@ text and the code under review, then maps each check 1:1 to its criterion.
11
11
  from __future__ import annotations
12
12
 
13
13
  import json
14
+ import logging
14
15
  from typing import Dict, List
15
16
 
16
17
  from pydantic import BaseModel
@@ -19,6 +20,8 @@ from devcouncil.domain.requirement import Requirement
19
20
  from devcouncil.domain.task import Task
20
21
  from devcouncil.llm.router import ModelRouter
21
22
 
23
+ logger = logging.getLogger(__name__)
24
+
22
25
 
23
26
  class CompiledCheck(BaseModel):
24
27
  acceptance_criterion_id: str
@@ -29,6 +32,19 @@ class CompiledChecks(BaseModel):
29
32
  checks: List[CompiledCheck]
30
33
 
31
34
 
35
+ # A worked example pinned to the prompt. Weak/local models follow a concrete
36
+ # AC->command pairing far more reliably than rules alone — it anchors the import
37
+ # style, the exception-guard idiom, and "assert behavior, not state".
38
+ _WORKED_EXAMPLE = """Worked example (follow this shape exactly):
39
+ Acceptance criterion: "median([]) raises ValueError"
40
+ Code under review: a new file `stats.py` defining `def median(values): ...`
41
+ CORRECT command:
42
+ python -c "import stats\\ntry: stats.median([])\\nexcept ValueError: pass\\nelse: raise SystemExit(1)"
43
+ Why: it imports the REAL module name implied by the file (stats.py -> import stats),
44
+ exercises the public function, and asserts the OBSERVABLE behavior (a raised
45
+ exception) — not any file/git state."""
46
+
47
+
32
48
  class AcceptanceTestCompiler:
33
49
  def __init__(self, router: ModelRouter, role: str = "implementation_reviewer"):
34
50
  self.router = router
@@ -43,22 +59,148 @@ class AcceptanceTestCompiler:
43
59
  """Return {acceptance_criterion_id: [self-contained check command(s)]}.
44
60
 
45
61
  Best-effort: returns {} if the model cannot produce usable checks, so the
46
- caller can fall back to the task's declared expected_tests.
62
+ caller can fall back to the task's declared expected_tests. Single-shot
63
+ wrapper over :meth:`compile_candidates` (samples=1) for back-compat.
64
+ """
65
+ return await self.compile_candidates(task, requirements, code_context, samples=1)
66
+
67
+ async def compile_candidates(
68
+ self,
69
+ task: Task,
70
+ requirements: List[Requirement],
71
+ code_context: str,
72
+ samples: int = 1,
73
+ per_criterion: bool = False,
74
+ ) -> Dict[str, List[str]]:
75
+ """Return {acceptance_criterion_id: [up to ``samples`` INDEPENDENT check commands]}.
76
+
77
+ Each criterion gets several independently-generated behavioral checks so the
78
+ caller can decide by majority vote — outvoting a single mis-generated check
79
+ without auto-passing a real defect. ``samples=1`` yields one command per
80
+ criterion (identical to the original single-shot ``compile``). Local sampling
81
+ is cost-free, so a weak model benefits most from a higher count.
82
+
83
+ ``per_criterion`` compiles ONE criterion per model call instead of batching them
84
+ all into a single prompt. A weak model batching N criteria into one JSON response
85
+ routinely omits or mis-attributes some — producing false ``incomplete`` verdicts —
86
+ whereas a focused single-criterion prompt is far more reliable. It costs N× the
87
+ calls (cheap on a local monitor), so it is opt-in.
47
88
  """
48
89
  ac_by_id = {ac.id: ac for req in requirements for ac in req.acceptance_criteria}
49
90
  target = [ac_by_id[i] for i in task.acceptance_criterion_ids if i in ac_by_id]
50
91
  if not target:
51
92
  return {}
52
93
 
94
+ if per_criterion and len(target) > 1:
95
+ out: Dict[str, List[str]] = {}
96
+ for ac in target:
97
+ out.update(await self._sample_checks([ac], code_context, samples))
98
+ return out
99
+ return await self._sample_checks(target, code_context, samples)
100
+
101
+ async def _sample_checks(
102
+ self, target: List, code_context: str, samples: int
103
+ ) -> Dict[str, List[str]]:
104
+ """Generate up to ``samples`` independent check commands for ``target`` criteria."""
105
+ valid_ids = {ac.id for ac in target}
106
+ out: Dict[str, List[str]] = {}
107
+ # Independent attempts. Each varies temperature + an attempt marker so the router
108
+ # cache keys differ (otherwise an identical prompt+temp returns the cached answer
109
+ # and every "sample" is the same command). Attempt 0 stays deterministic (temp 0).
110
+ for attempt in range(max(1, samples)):
111
+ temperature = 0.0 if attempt == 0 else min(0.8, 0.3 + 0.2 * attempt)
112
+ try:
113
+ result = await self.router.complete_structured(
114
+ role=self.role,
115
+ messages=[{"role": "user", "content": self._compile_prompt(target, code_context, attempt)}],
116
+ schema=CompiledChecks,
117
+ temperature=temperature,
118
+ fallback=CompiledChecks(checks=[]),
119
+ )
120
+ except Exception as e:
121
+ logger.warning("Acceptance-check compile attempt %d failed: %s", attempt, e)
122
+ continue
123
+ for check in result.checks:
124
+ cmd = (check.command or "").strip()
125
+ if check.acceptance_criterion_id in valid_ids and cmd:
126
+ bucket = out.setdefault(check.acceptance_criterion_id, [])
127
+ if cmd not in bucket: # dedup identical candidates across attempts
128
+ bucket.append(cmd)
129
+ logger.info(
130
+ "Compiled acceptance checks for %d/%d criteria (%d samples)",
131
+ len(out), len(valid_ids), max(1, samples),
132
+ )
133
+ return out
134
+
135
+ async def repair(
136
+ self,
137
+ ac_id: str,
138
+ ac_description: str,
139
+ failing_command: str,
140
+ error_summary: str,
141
+ code_context: str,
142
+ ) -> str | None:
143
+ """Regenerate a compiled check that FAILED TO RUN (malformed/unrunnable).
144
+
145
+ Given the broken command and the launcher error, ask the model to fix the
146
+ COMMAND so it runs against the code — never to change what it asserts. Returns
147
+ the repaired command, or None if the model cannot produce a usable one. Safe by
148
+ construction: a check that did not run proves nothing, so regenerating it cannot
149
+ weaken the gate (a repaired check still has to genuinely pass to count)."""
150
+ prompt = f"""The following DevCouncil acceptance check FAILED TO RUN — it is malformed or
151
+ its tooling/import is wrong, so it proves nothing about the code. Fix the COMMAND so it
152
+ RUNS and correctly tests the SAME criterion. Do NOT weaken or change what it asserts;
153
+ only fix what stops it from running (wrong module/import name, broken Python one-liner
154
+ syntax, unavailable tool).
155
+
156
+ Acceptance criterion ({ac_id}): {ac_description}
157
+
158
+ Failing command:
159
+ {failing_command}
160
+
161
+ Launcher error / output:
162
+ {error_summary}
163
+
164
+ Code under review:
165
+ {code_context}
166
+
167
+ {_WORKED_EXAMPLE}
168
+
169
+ Return JSON: one CompiledCheck with acceptance_criterion_id={ac_id!r} and the corrected
170
+ single self-contained command. If you cannot produce a runnable behavioral command,
171
+ return an empty checks list."""
172
+ try:
173
+ result = await self.router.complete_structured(
174
+ role=self.role,
175
+ messages=[{"role": "user", "content": prompt}],
176
+ schema=CompiledChecks,
177
+ fallback=CompiledChecks(checks=[]),
178
+ )
179
+ except Exception:
180
+ return None
181
+ for check in result.checks:
182
+ cmd = (check.command or "").strip()
183
+ if check.acceptance_criterion_id == ac_id and cmd and cmd != failing_command.strip():
184
+ return cmd
185
+ return None
186
+
187
+ def _compile_prompt(self, target, code_context: str, attempt: int = 0) -> str:
53
188
  acs_json = json.dumps(
54
189
  [{"id": ac.id, "description": ac.description, "method": ac.verification_method} for ac in target],
55
190
  indent=2,
56
191
  )
192
+ # Independent-attempt marker: nudges diversity across samples AND differentiates
193
+ # the router cache key so a second sample is actually regenerated, not replayed.
194
+ variant = "" if attempt == 0 else (
195
+ f"\nIndependent attempt #{attempt}: derive each check FROM SCRATCH; do not assume "
196
+ "a previous attempt's wording. Prefer a different but equivalent way to exercise "
197
+ "the same behavior.\n"
198
+ )
57
199
  prompt = f"""
58
200
  You are DevCouncil's acceptance-test compiler. Convert each acceptance criterion
59
201
  below into exactly ONE shell command that EXITS 0 if and only if the BEHAVIOR
60
202
  described by the criterion holds for the code shown.
61
-
203
+ {variant}
62
204
  Acceptance criteria:
63
205
  {acs_json}
64
206
 
@@ -105,21 +247,7 @@ criterion instead of emitting such a command:
105
247
  - If a criterion cannot be checked by a behavioral command (e.g. pure 'manual'
106
248
  review, or it only describes repo/tooling state), OMIT it rather than inventing
107
249
  a state-based or bogus command.
108
- """
109
- try:
110
- result = await self.router.complete_structured(
111
- role=self.role,
112
- messages=[{"role": "user", "content": prompt}],
113
- schema=CompiledChecks,
114
- fallback=CompiledChecks(checks=[]),
115
- )
116
- except Exception:
117
- return {}
118
250
 
119
- out: Dict[str, List[str]] = {}
120
- valid_ids = {ac.id for ac in target}
121
- for check in result.checks:
122
- cmd = (check.command or "").strip()
123
- if check.acceptance_criterion_id in valid_ids and cmd:
124
- out.setdefault(check.acceptance_criterion_id, []).append(cmd)
125
- return out
251
+ {_WORKED_EXAMPLE}
252
+ """
253
+ return prompt
@@ -14,6 +14,7 @@ resilient to churn in the command module.
14
14
  from __future__ import annotations
15
15
 
16
16
  import asyncio
17
+ import logging
17
18
  from dataclasses import dataclass, field
18
19
  from pathlib import Path
19
20
  from typing import List, Optional
@@ -32,6 +33,8 @@ _TASK_ID = "CHECK"
32
33
 
33
34
  _DEFAULT_CRITERION = "The working-tree changes are correct and exercised by tests."
34
35
 
36
+ logger = logging.getLogger(__name__)
37
+
35
38
 
36
39
  @dataclass
37
40
  class AdHocCheckResult:
@@ -80,7 +83,9 @@ def run_working_tree_check(
80
83
  diff = verifier.get_diff()
81
84
  changed_files = verifier.get_changed_files()
82
85
  if not diff or not changed_files:
86
+ logger.info("Ad-hoc check: no working-tree changes; passing trivially")
83
87
  return AdHocCheckResult(requirement="", passed=True, reason="no_changes")
88
+ logger.info("Ad-hoc check: %d changed file(s), enforce_coverage=%s", len(changed_files), enforce_coverage or min_ratio > 0)
84
89
 
85
90
  criterion = requirement or _DEFAULT_CRITERION
86
91
  req = Requirement(
@@ -119,6 +124,7 @@ def run_working_tree_check(
119
124
  gaps, evidence = asyncio.run(verifier.verify_task(task, [req]))
120
125
  coverage = next((ev for ev in evidence if isinstance(ev, DiffCoverageEvidence)), None)
121
126
  blocking = [g for g in gaps if g.blocking]
127
+ logger.info("Ad-hoc check result: passed=%s (%d gap(s), %d blocking)", not blocking, len(gaps), len(blocking))
122
128
  return AdHocCheckResult(
123
129
  requirement=criterion,
124
130
  changed_files=changed_files,
@@ -1,4 +1,5 @@
1
1
  import json
2
+ import logging
2
3
  from typing import List
3
4
  from pydantic import BaseModel
4
5
  from devcouncil.domain.task import Task
@@ -7,6 +8,8 @@ from devcouncil.domain.gap import Gap
7
8
  from devcouncil.llm.router import ModelRouter
8
9
  from devcouncil.utils.redaction import redact_string
9
10
 
11
+ logger = logging.getLogger(__name__)
12
+
10
13
  class ReviewOutput(BaseModel):
11
14
  is_satisfactory: bool
12
15
  findings: List[Gap]
@@ -47,9 +50,15 @@ Your task is to identify if the implementation is complete, correct, and follows
47
50
  Return a JSON object with 'is_satisfactory' and a list of 'findings' (as Gap objects).
48
51
  """
49
52
  messages = [{"role": "user", "content": prompt}]
50
-
51
- return await self.router.complete_structured(
53
+
54
+ logger.info("Implementation review: task=%s diff_bytes=%d", task.id, len(diff))
55
+ result = await self.router.complete_structured(
52
56
  role="implementation_reviewer",
53
57
  messages=messages,
54
58
  schema=ReviewOutput
55
59
  )
60
+ logger.info(
61
+ "Implementation review for %s: satisfactory=%s findings=%d",
62
+ task.id, result.is_satisfactory, len(result.findings),
63
+ )
64
+ return result
@@ -2,8 +2,11 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import asyncio
5
6
  import hashlib
7
+ import logging
6
8
  import platform
9
+ import shutil
7
10
  import subprocess
8
11
  import sys
9
12
  from pathlib import Path
@@ -17,6 +20,8 @@ from devcouncil.storage.db import get_db
17
20
  from devcouncil.storage.native import VerificationRunRepository
18
21
  from devcouncil.verification.verifier import Verifier
19
22
 
23
+ logger = logging.getLogger(__name__)
24
+
20
25
 
21
26
  class SandboxResult(BaseModel):
22
27
  sandbox: str
@@ -38,6 +43,8 @@ def _save_run(
38
43
  commands: list[dict],
39
44
  status: Literal["passed", "failed", "unsupported"],
40
45
  ) -> None:
46
+ log = logger.info if status == "passed" else logger.warning
47
+ log("Sandbox %s for %s: %s (%d command(s))", sandbox, task.id, status, len(commands))
41
48
  db = get_db(project_root)
42
49
  if not db:
43
50
  return
@@ -56,8 +63,6 @@ class LocalSandbox(VerificationSandbox):
56
63
  self.project_root = project_root
57
64
 
58
65
  def run(self, task: Task, commands: list[str], requirements: list) -> SandboxResult:
59
- import asyncio
60
-
61
66
  gaps, _ = asyncio.run(Verifier(self.project_root).verify_task(task, requirements))
62
67
  status: Literal["passed", "failed", "unsupported"] = (
63
68
  "failed" if any(g.blocking for g in gaps) else "passed"
@@ -148,8 +153,6 @@ class NixSandbox(VerificationSandbox):
148
153
 
149
154
 
150
155
  def shutil_which(name: str) -> str | None:
151
- import shutil
152
-
153
156
  return shutil.which(name)
154
157
 
155
158