devcouncil 0.1.0 → 0.1.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 (128) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +62 -543
  3. package/package.json +1 -1
  4. package/pyproject.toml +29 -26
  5. package/src/devcouncil/__main__.py +4 -4
  6. package/src/devcouncil/app/__init__.py +28 -28
  7. package/src/devcouncil/app/config.py +135 -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 +143 -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/cli/commands/artifacts.py +51 -48
  22. package/src/devcouncil/cli/commands/ast.py +22 -0
  23. package/src/devcouncil/cli/commands/baseline.py +35 -32
  24. package/src/devcouncil/cli/commands/config.py +76 -54
  25. package/src/devcouncil/cli/commands/dashboard.py +26 -0
  26. package/src/devcouncil/cli/commands/doctor.py +86 -42
  27. package/src/devcouncil/cli/commands/go.py +237 -0
  28. package/src/devcouncil/cli/commands/hook.py +96 -29
  29. package/src/devcouncil/cli/commands/init.py +67 -56
  30. package/src/devcouncil/cli/commands/integrate.py +320 -14
  31. package/src/devcouncil/cli/commands/lsp.py +20 -0
  32. package/src/devcouncil/cli/commands/map.py +25 -21
  33. package/src/devcouncil/cli/commands/plan.py +257 -206
  34. package/src/devcouncil/cli/commands/prompt.py +36 -33
  35. package/src/devcouncil/cli/commands/repair.py +72 -69
  36. package/src/devcouncil/cli/commands/report.py +112 -54
  37. package/src/devcouncil/cli/commands/reset_demo_state.py +31 -28
  38. package/src/devcouncil/cli/commands/rollback.py +49 -47
  39. package/src/devcouncil/cli/commands/run.py +252 -207
  40. package/src/devcouncil/cli/commands/setup.py +159 -18
  41. package/src/devcouncil/cli/commands/show.py +76 -57
  42. package/src/devcouncil/cli/commands/status.py +117 -105
  43. package/src/devcouncil/cli/commands/tasks.py +55 -41
  44. package/src/devcouncil/cli/commands/trace.py +2 -1
  45. package/src/devcouncil/cli/commands/verify.py +158 -128
  46. package/src/devcouncil/cli/commands/version.py +20 -20
  47. package/src/devcouncil/cli/commands/watch.py +574 -0
  48. package/src/devcouncil/cli/main.py +42 -24
  49. package/src/devcouncil/council/prompts/arbiter.md +19 -19
  50. package/src/devcouncil/council/prompts/critic_a.md +10 -10
  51. package/src/devcouncil/council/prompts/critic_b.md +10 -10
  52. package/src/devcouncil/council/prompts/implementation_reviewer.md +16 -16
  53. package/src/devcouncil/council/prompts/planner_a.md +16 -16
  54. package/src/devcouncil/council/prompts/planner_b.md +16 -16
  55. package/src/devcouncil/council/prompts/rebuttal.md +10 -10
  56. package/src/devcouncil/council/prompts/spec_writer.md +12 -12
  57. package/src/devcouncil/domain/assumption.py +17 -17
  58. package/src/devcouncil/domain/critique.py +32 -32
  59. package/src/devcouncil/domain/evidence.py +27 -27
  60. package/src/devcouncil/domain/gap.py +26 -26
  61. package/src/devcouncil/domain/requirement.py +22 -22
  62. package/src/devcouncil/domain/task.py +26 -26
  63. package/src/devcouncil/execution/__init__.py +1 -1
  64. package/src/devcouncil/execution/context_builder.py +54 -54
  65. package/src/devcouncil/execution/executor.py +15 -15
  66. package/src/devcouncil/execution/hook_policy.py +24 -3
  67. package/src/devcouncil/execution/patch.py +28 -28
  68. package/src/devcouncil/execution/permissions.py +44 -44
  69. package/src/devcouncil/execution/prompt_builder.py +23 -23
  70. package/src/devcouncil/execution/task_runner.py +63 -63
  71. package/src/devcouncil/executors/__init__.py +1 -1
  72. package/src/devcouncil/executors/coding_cli.py +112 -0
  73. package/src/devcouncil/executors/mini_swe.py +63 -63
  74. package/src/devcouncil/executors/native/agent.py +81 -81
  75. package/src/devcouncil/executors/openhands.py +56 -56
  76. package/src/devcouncil/gating/__init__.py +1 -1
  77. package/src/devcouncil/gating/checks/clean_git.py +50 -45
  78. package/src/devcouncil/gating/checks/planned_files_check.py +32 -32
  79. package/src/devcouncil/gating/checks/requirement_coverage.py +26 -26
  80. package/src/devcouncil/gating/checks/secret_scan_check.py +34 -34
  81. package/src/devcouncil/gating/policy.py +157 -157
  82. package/src/devcouncil/indexing/__init__.py +1 -1
  83. package/src/devcouncil/indexing/ast_matcher.py +168 -0
  84. package/src/devcouncil/indexing/graph_index.py +48 -48
  85. package/src/devcouncil/indexing/lsp.py +120 -0
  86. package/src/devcouncil/indexing/repo_mapper.py +208 -204
  87. package/src/devcouncil/integrations/github.py +35 -35
  88. package/src/devcouncil/integrations/gitnexus.py +27 -27
  89. package/src/devcouncil/integrations/graphify.py +34 -34
  90. package/src/devcouncil/integrations/mcp/server.py +549 -96
  91. package/src/devcouncil/integrations/pr_comments.py +62 -0
  92. package/src/devcouncil/live/__init__.py +2 -0
  93. package/src/devcouncil/live/cards.py +207 -0
  94. package/src/devcouncil/live/models.py +63 -0
  95. package/src/devcouncil/live/repair_prompt.py +83 -0
  96. package/src/devcouncil/live/reviewer.py +70 -0
  97. package/src/devcouncil/live/signals.py +135 -0
  98. package/src/devcouncil/live/summary.py +34 -0
  99. package/src/devcouncil/live/tasks.py +18 -0
  100. package/src/devcouncil/live/transcripts.py +138 -0
  101. package/src/devcouncil/llm/__init__.py +1 -1
  102. package/src/devcouncil/llm/cache.py +38 -38
  103. package/src/devcouncil/llm/provider.py +146 -125
  104. package/src/devcouncil/llm/router.py +111 -111
  105. package/src/devcouncil/planning/__init__.py +1 -1
  106. package/src/devcouncil/planning/arbiter_service.py +57 -57
  107. package/src/devcouncil/planning/critique_service.py +66 -66
  108. package/src/devcouncil/planning/plan_service.py +46 -46
  109. package/src/devcouncil/planning/prompt_enhancer_service.py +86 -0
  110. package/src/devcouncil/planning/repair_service.py +39 -39
  111. package/src/devcouncil/planning/spec_service.py +44 -44
  112. package/src/devcouncil/reporting/github_check.py +32 -32
  113. package/src/devcouncil/reporting/json_report.py +20 -17
  114. package/src/devcouncil/reporting/markdown_report.py +68 -46
  115. package/src/devcouncil/reporting/report_builder.py +14 -14
  116. package/src/devcouncil/storage/db.py +66 -66
  117. package/src/devcouncil/storage/models.py +83 -83
  118. package/src/devcouncil/storage/repositories.py +299 -222
  119. package/src/devcouncil/telemetry/cost.py +34 -34
  120. package/src/devcouncil/telemetry/tracker.py +49 -49
  121. package/src/devcouncil/ui/__init__.py +1 -0
  122. package/src/devcouncil/ui/dashboard.py +122 -0
  123. package/src/devcouncil/utils/__init__.py +1 -1
  124. package/src/devcouncil/utils/redaction.py +141 -141
  125. package/src/devcouncil/verification/__init__.py +1 -1
  126. package/src/devcouncil/verification/implementation_reviewer.py +55 -55
  127. package/src/devcouncil/verification/verifier.py +319 -302
  128. package/uv.lock +1 -1
@@ -1,39 +1,39 @@
1
- from typing import List
2
- import json
3
- from pydantic import BaseModel
4
- from devcouncil.domain.gap import Gap
5
- from devcouncil.domain.task import Task
6
- from devcouncil.llm.router import ModelRouter
7
-
8
- class RepairOutput(BaseModel):
9
- suggested_tasks: List[Task]
10
-
11
- class RepairService:
12
- """Uses LLM to infer focused repair tasks from blocking gaps."""
13
-
14
- def __init__(self, router: ModelRouter):
15
- self.router = router
16
-
17
- async def generate_repair_plan(self, gaps: List[Gap], project_context: str) -> RepairOutput:
18
- prompt = f"""
19
- The following blocking gaps were detected during verification.
20
- Gaps:
21
- {json.dumps([g.model_dump() for g in gaps], indent=2)}
22
-
23
- Project Context:
24
- {project_context}
25
-
26
- Your task is to generate focused implementation tasks to fix these gaps.
27
- - Each task must have a clear description and recommended fix.
28
- - Specify 'planned_files' that need modification (infer from gap evidence).
29
- - Link each task to the relevant 'requirement_id' mentioned in the gap.
30
-
31
- Return a JSON object with 'suggested_tasks'.
32
- """
33
- messages = [{"role": "user", "content": prompt}]
34
-
35
- return await self.router.complete_structured(
36
- role="planner_a", # Pragmatic tech lead is best suited for repair task generation
37
- messages=messages,
38
- schema=RepairOutput
39
- )
1
+ from typing import List
2
+ import json
3
+ from pydantic import BaseModel
4
+ from devcouncil.domain.gap import Gap
5
+ from devcouncil.domain.task import Task
6
+ from devcouncil.llm.router import ModelRouter
7
+
8
+ class RepairOutput(BaseModel):
9
+ suggested_tasks: List[Task]
10
+
11
+ class RepairService:
12
+ """Uses LLM to infer focused repair tasks from blocking gaps."""
13
+
14
+ def __init__(self, router: ModelRouter):
15
+ self.router = router
16
+
17
+ async def generate_repair_plan(self, gaps: List[Gap], project_context: str) -> RepairOutput:
18
+ prompt = f"""
19
+ The following blocking gaps were detected during verification.
20
+ Gaps:
21
+ {json.dumps([g.model_dump() for g in gaps], indent=2)}
22
+
23
+ Project Context:
24
+ {project_context}
25
+
26
+ Your task is to generate focused implementation tasks to fix these gaps.
27
+ - Each task must have a clear description and recommended fix.
28
+ - Specify 'planned_files' that need modification (infer from gap evidence).
29
+ - Link each task to the relevant 'requirement_id' mentioned in the gap.
30
+
31
+ Return a JSON object with 'suggested_tasks'.
32
+ """
33
+ messages = [{"role": "user", "content": prompt}]
34
+
35
+ return await self.router.complete_structured(
36
+ role="planner_a", # Pragmatic tech lead is best suited for repair task generation
37
+ messages=messages,
38
+ schema=RepairOutput
39
+ )
@@ -1,44 +1,44 @@
1
- from typing import List
2
- from pydantic import BaseModel
3
- from devcouncil.domain.requirement import Requirement
4
- from devcouncil.domain.assumption import Assumption
5
- from devcouncil.llm.router import ModelRouter
6
-
7
- class BlockingQuestion(BaseModel):
8
- id: str
9
- question: str
10
- reason: str
11
-
12
- class SpecOutput(BaseModel):
13
- requirements: List[Requirement]
14
- assumptions: List[Assumption]
15
- blocking_questions: List[BlockingQuestion]
16
-
17
- class SpecService:
18
- def __init__(self, router: ModelRouter):
19
- self.router = router
20
-
21
- async def generate_spec(self, goal: str, repo_map_json: str) -> SpecOutput:
22
- prompt = f"""
23
- Goal: {goal}
24
-
25
- Repository Map:
26
- {repo_map_json}
27
-
28
- Your task is to draft the initial software specification for this goal.
29
- 1. Identify functional and non-functional requirements.
30
- 2. Extract any assumptions you are making about the codebase or architecture.
31
- 3. List any blocking questions that the user must answer before implementation can proceed.
32
-
33
- Each requirement MUST have clear acceptance criteria with verification methods.
34
- Each assumption MUST have a confidence and impact level.
35
- """
36
- messages = [
37
- {"role": "user", "content": prompt}
38
- ]
39
-
40
- return await self.router.complete_structured(
41
- role="spec_writer",
42
- messages=messages,
43
- schema=SpecOutput
44
- )
1
+ from typing import List
2
+ from pydantic import BaseModel
3
+ from devcouncil.domain.requirement import Requirement
4
+ from devcouncil.domain.assumption import Assumption
5
+ from devcouncil.llm.router import ModelRouter
6
+
7
+ class BlockingQuestion(BaseModel):
8
+ id: str
9
+ question: str
10
+ reason: str
11
+
12
+ class SpecOutput(BaseModel):
13
+ requirements: List[Requirement]
14
+ assumptions: List[Assumption]
15
+ blocking_questions: List[BlockingQuestion]
16
+
17
+ class SpecService:
18
+ def __init__(self, router: ModelRouter):
19
+ self.router = router
20
+
21
+ async def generate_spec(self, goal: str, repo_map_json: str) -> SpecOutput:
22
+ prompt = f"""
23
+ Goal: {goal}
24
+
25
+ Repository Map:
26
+ {repo_map_json}
27
+
28
+ Your task is to draft the initial software specification for this goal.
29
+ 1. Identify functional and non-functional requirements.
30
+ 2. Extract any assumptions you are making about the codebase or architecture.
31
+ 3. List any blocking questions that the user must answer before implementation can proceed.
32
+
33
+ Each requirement MUST have clear acceptance criteria with verification methods.
34
+ Each assumption MUST have a confidence and impact level.
35
+ """
36
+ messages = [
37
+ {"role": "user", "content": prompt}
38
+ ]
39
+
40
+ return await self.router.complete_structured(
41
+ role="spec_writer",
42
+ messages=messages,
43
+ schema=SpecOutput
44
+ )
@@ -1,32 +1,32 @@
1
- from devcouncil.artifacts.graph import ArtifactGraph
2
-
3
- class GitHubCheckGenerator:
4
- """Generates GitHub Checks API payloads."""
5
-
6
- @staticmethod
7
- def generate(graph: ArtifactGraph) -> dict:
8
- summary = graph.coverage_summary()
9
- blocking_gaps = graph.blocking_gaps()
10
-
11
- status = "completed"
12
- conclusion = "failure" if summary["blocking_gaps"] > 0 else "success"
13
-
14
- text = f"**Requirements**: {summary['total_requirements']} | "
15
- text += f"**Tasks**: {summary['total_tasks']} | "
16
- text += f"**Gaps**: {summary['blocking_gaps']} blocking\n\n"
17
-
18
- if blocking_gaps:
19
- text += "### Blocking Gaps\n"
20
- for gap in blocking_gaps:
21
- text += f"- **{gap.id}**: {gap.description}\n"
22
-
23
- return {
24
- "name": "DevCouncil Verification",
25
- "status": status,
26
- "conclusion": conclusion,
27
- "output": {
28
- "title": f"DevCouncil: {conclusion.capitalize()}",
29
- "summary": f"Found {summary['blocking_gaps']} blocking gaps.",
30
- "text": text
31
- }
32
- }
1
+ from devcouncil.artifacts.graph import ArtifactGraph
2
+
3
+ class GitHubCheckGenerator:
4
+ """Generates GitHub Checks API payloads."""
5
+
6
+ @staticmethod
7
+ def generate(graph: ArtifactGraph) -> dict:
8
+ summary = graph.coverage_summary()
9
+ blocking_gaps = graph.blocking_gaps()
10
+
11
+ status = "completed"
12
+ conclusion = "failure" if summary["blocking_gaps"] > 0 else "success"
13
+
14
+ text = f"**Requirements**: {summary['total_requirements']} | "
15
+ text += f"**Tasks**: {summary['total_tasks']} | "
16
+ text += f"**Gaps**: {summary['blocking_gaps']} blocking\n\n"
17
+
18
+ if blocking_gaps:
19
+ text += "### Blocking Gaps\n"
20
+ for gap in blocking_gaps:
21
+ text += f"- **{gap.id}**: {gap.description}\n"
22
+
23
+ return {
24
+ "name": "DevCouncil Verification",
25
+ "status": status,
26
+ "conclusion": conclusion,
27
+ "output": {
28
+ "title": f"DevCouncil: {conclusion.capitalize()}",
29
+ "summary": f"Found {summary['blocking_gaps']} blocking gaps.",
30
+ "text": text
31
+ }
32
+ }
@@ -1,17 +1,20 @@
1
- import json
2
- from devcouncil.artifacts.graph import ArtifactGraph
3
-
4
- class JsonReportGenerator:
5
- """Generates a JSON evidence report."""
6
-
7
- @staticmethod
8
- def generate(graph: ArtifactGraph) -> str:
9
- summary = graph.coverage_summary()
10
-
11
- report = {
12
- "verdict": "blocked" if summary["blocking_gaps"] > 0 else "passed",
13
- "coverage_summary": summary,
14
- "blocking_gaps": [g.model_dump() for g in graph.blocking_gaps()]
15
- }
16
-
17
- return json.dumps(report, indent=2)
1
+ import json
2
+ from devcouncil.artifacts.graph import ArtifactGraph
3
+
4
+ class JsonReportGenerator:
5
+ """Generates a JSON evidence report."""
6
+
7
+ @staticmethod
8
+ def generate(graph: ArtifactGraph, live_review: dict | None = None) -> str:
9
+ summary = graph.coverage_summary()
10
+ live_blockers = len((live_review or {}).get("blocking_cards", []))
11
+
12
+ report = {
13
+ "verdict": "blocked" if summary["blocking_gaps"] > 0 or live_blockers > 0 else "passed",
14
+ "coverage_summary": summary,
15
+ "blocking_gaps": [g.model_dump() for g in graph.blocking_gaps()]
16
+ }
17
+ if live_review is not None:
18
+ report["live_review"] = live_review
19
+
20
+ return json.dumps(report, indent=2)
@@ -1,46 +1,68 @@
1
- from devcouncil.artifacts.graph import ArtifactGraph
2
-
3
- class MarkdownReportGenerator:
4
- """Generates a Markdown evidence report."""
5
-
6
- MAX_INLINE_GAPS = 25
7
-
8
- @staticmethod
9
- def generate(graph: ArtifactGraph) -> str:
10
- summary = graph.coverage_summary()
11
-
12
- md_output = "# DevCouncil Report\n\n"
13
- md_output += "## Verdict\n"
14
- if summary["blocking_gaps"] > 0:
15
- md_output += f"**Blocked**: {summary['blocking_gaps']} high-severity gaps remain.\n\n"
16
- else:
17
- md_output += "**Passed**: Ready for release.\n\n"
18
-
19
- md_output += "## Coverage Summary\n"
20
- md_output += f"- **Requirements**: {summary['total_requirements']} ({summary['requirements_without_tasks']} unmapped)\n"
21
- md_output += f"- **Tasks**: {summary['total_tasks']} ({summary['tasks_without_requirements']} orphaned)\n"
22
- md_output += f"- **Evidence**: {summary['total_ac'] - summary['ac_without_evidence']}/{summary['total_ac']} AC verified\n\n"
23
-
24
- md_output += "## Requirements Coverage Table\n"
25
- md_output += "| Requirement | Task Mapping | Status |\n"
26
- md_output += "|---|---|---|\n"
27
-
28
- for req in graph.requirements.values():
29
- linked_tasks = [t for t in graph.tasks.values() if req.id in t.requirement_ids]
30
- task_str = ", ".join([t.id for t in linked_tasks]) if linked_tasks else "*None*"
31
- status_str = "Covered" if linked_tasks else "**Unmapped**"
32
- md_output += f"| {req.id} {req.title} | {task_str} | {status_str} |\n"
33
-
34
- md_output += "\n## Blocking Gaps\n"
35
- blocking_gaps = graph.blocking_gaps()
36
- if not blocking_gaps:
37
- md_output += "None.\n"
38
- else:
39
- for gap in blocking_gaps[:MarkdownReportGenerator.MAX_INLINE_GAPS]:
40
- md_output += f"### {gap.id}: {gap.description}\n"
41
- md_output += f"**Recommended fix**: {gap.recommended_fix}\n\n"
42
- if len(blocking_gaps) > MarkdownReportGenerator.MAX_INLINE_GAPS:
43
- remaining = len(blocking_gaps) - MarkdownReportGenerator.MAX_INLINE_GAPS
44
- md_output += f"_Omitted {remaining} additional blocking gap(s). Use JSON output for the full list._\n"
45
-
46
- return md_output
1
+ from devcouncil.artifacts.graph import ArtifactGraph
2
+
3
+ class MarkdownReportGenerator:
4
+ """Generates a Markdown evidence report."""
5
+
6
+ MAX_INLINE_GAPS = 25
7
+
8
+ @staticmethod
9
+ def generate(graph: ArtifactGraph, live_review: dict | None = None) -> str:
10
+ summary = graph.coverage_summary()
11
+ live_blockers = (live_review or {}).get("blocking_cards", [])
12
+
13
+ md_output = "# DevCouncil Report\n\n"
14
+ md_output += "## Verdict\n"
15
+ if summary["blocking_gaps"] > 0 or live_blockers:
16
+ parts = []
17
+ if summary["blocking_gaps"] > 0:
18
+ parts.append(f"{summary['blocking_gaps']} high-severity gap(s)")
19
+ if live_blockers:
20
+ parts.append(f"{len(live_blockers)} live-review blocker(s)")
21
+ md_output += f"**Blocked**: {', '.join(parts)} remain.\n\n"
22
+ else:
23
+ md_output += "**Passed**: Ready for release.\n\n"
24
+
25
+ md_output += "## Coverage Summary\n"
26
+ md_output += f"- **Requirements**: {summary['total_requirements']} ({summary['requirements_without_tasks']} unmapped)\n"
27
+ md_output += f"- **Tasks**: {summary['total_tasks']} ({summary['tasks_without_requirements']} orphaned)\n"
28
+ md_output += f"- **Evidence**: {summary['total_ac'] - summary['ac_without_evidence']}/{summary['total_ac']} AC verified\n\n"
29
+
30
+ md_output += "## Requirements Coverage Table\n"
31
+ md_output += "| Requirement | Task Mapping | Status |\n"
32
+ md_output += "|---|---|---|\n"
33
+
34
+ for req in graph.requirements.values():
35
+ linked_tasks = [t for t in graph.tasks.values() if req.id in t.requirement_ids]
36
+ task_str = ", ".join([t.id for t in linked_tasks]) if linked_tasks else "*None*"
37
+ status_str = "Covered" if linked_tasks else "**Unmapped**"
38
+ md_output += f"| {req.id} {req.title} | {task_str} | {status_str} |\n"
39
+
40
+ md_output += "\n## Blocking Gaps\n"
41
+ blocking_gaps = graph.blocking_gaps()
42
+ if not blocking_gaps:
43
+ md_output += "None.\n"
44
+ else:
45
+ for gap in blocking_gaps[:MarkdownReportGenerator.MAX_INLINE_GAPS]:
46
+ md_output += f"### {gap.id}: {gap.description}\n"
47
+ md_output += f"**Recommended fix**: {gap.recommended_fix}\n\n"
48
+ if len(blocking_gaps) > MarkdownReportGenerator.MAX_INLINE_GAPS:
49
+ remaining = len(blocking_gaps) - MarkdownReportGenerator.MAX_INLINE_GAPS
50
+ md_output += f"_Omitted {remaining} additional blocking gap(s). Use JSON output for the full list._\n"
51
+
52
+ if live_review is not None:
53
+ md_output += "\n## Live Review\n"
54
+ cards = live_review.get("cards", {})
55
+ md_output += f"- **Pending signals**: {live_review.get('pending_signals', 0)}\n"
56
+ md_output += f"- **Open cards**: {cards.get('open', 0)}\n"
57
+ md_output += f"- **Open critical cards**: {cards.get('critical_open', 0)}\n"
58
+ if not live_blockers:
59
+ md_output += "- **Blocking cards in scope**: None.\n"
60
+ else:
61
+ md_output += "\n### Blocking Live-Review Cards\n"
62
+ for card in live_blockers[:MarkdownReportGenerator.MAX_INLINE_GAPS]:
63
+ md_output += f"- **{card['id']}**"
64
+ if card.get("task_id"):
65
+ md_output += f" (`{card['task_id']}`)"
66
+ md_output += f": {card['summary']}\n"
67
+
68
+ return md_output
@@ -1,14 +1,14 @@
1
- from devcouncil.artifacts.graph import ArtifactGraph
2
- from devcouncil.reporting.markdown_report import MarkdownReportGenerator
3
- from devcouncil.reporting.json_report import JsonReportGenerator
4
-
5
- class ReportBuilder:
6
- """Builds reports in various formats from the artifact graph."""
7
-
8
- @staticmethod
9
- def build_markdown(graph: ArtifactGraph) -> str:
10
- return MarkdownReportGenerator.generate(graph)
11
-
12
- @staticmethod
13
- def build_json(graph: ArtifactGraph) -> str:
14
- return JsonReportGenerator.generate(graph)
1
+ from devcouncil.artifacts.graph import ArtifactGraph
2
+ from devcouncil.reporting.markdown_report import MarkdownReportGenerator
3
+ from devcouncil.reporting.json_report import JsonReportGenerator
4
+
5
+ class ReportBuilder:
6
+ """Builds reports in various formats from the artifact graph."""
7
+
8
+ @staticmethod
9
+ def build_markdown(graph: ArtifactGraph, live_review: dict | None = None) -> str:
10
+ return MarkdownReportGenerator.generate(graph, live_review=live_review)
11
+
12
+ @staticmethod
13
+ def build_json(graph: ArtifactGraph, live_review: dict | None = None) -> str:
14
+ return JsonReportGenerator.generate(graph, live_review=live_review)
@@ -1,66 +1,66 @@
1
- from contextlib import contextmanager
2
-
3
- from sqlmodel import SQLModel, create_engine, Session
4
- from sqlalchemy.exc import OperationalError
5
- from pathlib import Path
6
- from typing import Optional
7
-
8
- from devcouncil.storage.models import SchemaVersionModel
9
-
10
-
11
- SCHEMA_VERSION = 1
12
-
13
-
14
- class Database:
15
- def __init__(self, db_path: Path):
16
- self.db_path = db_path
17
- self.engine = create_engine(f"sqlite:///{db_path}")
18
-
19
- def create_db_and_tables(self):
20
- self._create_tables()
21
- self.ensure_schema_version()
22
-
23
- def ensure_schema_version(self):
24
- self._create_tables()
25
- with Session(self.engine) as session:
26
- current = session.get(SchemaVersionModel, "singleton")
27
- if current is None:
28
- session.add(SchemaVersionModel(id="singleton", version=SCHEMA_VERSION))
29
- session.commit()
30
- return
31
- if current.version != SCHEMA_VERSION:
32
- raise RuntimeError(
33
- f"Unsupported DevCouncil schema version {current.version}; "
34
- f"expected {SCHEMA_VERSION}."
35
- )
36
-
37
- def _create_tables(self):
38
- try:
39
- SQLModel.metadata.create_all(self.engine)
40
- except OperationalError as exc:
41
- if "already exists" not in str(exc):
42
- raise
43
-
44
- @contextmanager
45
- def get_session(self):
46
- """Yield a session with automatic commit/rollback/close."""
47
- session = Session(self.engine)
48
- try:
49
- yield session
50
- session.commit()
51
- except Exception:
52
- session.rollback()
53
- raise
54
- finally:
55
- session.close()
56
-
57
-
58
- def get_db(project_root: Path = Path(".")) -> Optional[Database]:
59
- dev_dir = project_root / ".devcouncil"
60
- if not dev_dir.exists():
61
- return None
62
-
63
- db_path = dev_dir / "state.sqlite"
64
- db = Database(db_path)
65
- db.ensure_schema_version()
66
- return db
1
+ from contextlib import contextmanager
2
+
3
+ from sqlmodel import SQLModel, create_engine, Session
4
+ from sqlalchemy.exc import OperationalError
5
+ from pathlib import Path
6
+ from typing import Optional
7
+
8
+ from devcouncil.storage.models import SchemaVersionModel
9
+
10
+
11
+ SCHEMA_VERSION = 1
12
+
13
+
14
+ class Database:
15
+ def __init__(self, db_path: Path):
16
+ self.db_path = db_path
17
+ self.engine = create_engine(f"sqlite:///{db_path}")
18
+
19
+ def create_db_and_tables(self):
20
+ self._create_tables()
21
+ self.ensure_schema_version()
22
+
23
+ def ensure_schema_version(self):
24
+ self._create_tables()
25
+ with Session(self.engine) as session:
26
+ current = session.get(SchemaVersionModel, "singleton")
27
+ if current is None:
28
+ session.add(SchemaVersionModel(id="singleton", version=SCHEMA_VERSION))
29
+ session.commit()
30
+ return
31
+ if current.version != SCHEMA_VERSION:
32
+ raise RuntimeError(
33
+ f"Unsupported DevCouncil schema version {current.version}; "
34
+ f"expected {SCHEMA_VERSION}."
35
+ )
36
+
37
+ def _create_tables(self):
38
+ try:
39
+ SQLModel.metadata.create_all(self.engine)
40
+ except OperationalError as exc:
41
+ if "already exists" not in str(exc):
42
+ raise
43
+
44
+ @contextmanager
45
+ def get_session(self):
46
+ """Yield a session with automatic commit/rollback/close."""
47
+ session = Session(self.engine)
48
+ try:
49
+ yield session
50
+ session.commit()
51
+ except Exception:
52
+ session.rollback()
53
+ raise
54
+ finally:
55
+ session.close()
56
+
57
+
58
+ def get_db(project_root: Path = Path(".")) -> Optional[Database]:
59
+ dev_dir = project_root / ".devcouncil"
60
+ if not dev_dir.exists():
61
+ return None
62
+
63
+ db_path = dev_dir / "state.sqlite"
64
+ db = Database(db_path)
65
+ db.ensure_schema_version()
66
+ return db