devcouncil 0.2.0 → 0.2.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 (205) hide show
  1. package/README.md +191 -253
  2. package/bin/devcouncil.js +130 -32
  3. package/package.json +6 -17
  4. package/pyproject.toml +0 -66
  5. package/src/devcouncil/__init__.py +0 -0
  6. package/src/devcouncil/__main__.py +0 -4
  7. package/src/devcouncil/app/__init__.py +0 -28
  8. package/src/devcouncil/app/config.py +0 -320
  9. package/src/devcouncil/app/errors.py +0 -23
  10. package/src/devcouncil/app/events.py +0 -44
  11. package/src/devcouncil/app/orchestrator.py +0 -92
  12. package/src/devcouncil/app/project_status.py +0 -29
  13. package/src/devcouncil/app/run_context.py +0 -39
  14. package/src/devcouncil/app/state_machine.py +0 -108
  15. package/src/devcouncil/artifacts/__init__.py +0 -1
  16. package/src/devcouncil/artifacts/coverage.py +0 -96
  17. package/src/devcouncil/artifacts/graph.py +0 -163
  18. package/src/devcouncil/artifacts/migrations.py +0 -20
  19. package/src/devcouncil/artifacts/schemas.py +0 -23
  20. package/src/devcouncil/artifacts/serializer.py +0 -21
  21. package/src/devcouncil/artifacts/validators.py +0 -27
  22. package/src/devcouncil/assets/__init__.py +0 -1
  23. package/src/devcouncil/assets/devcouncil-logo.svg +0 -60
  24. package/src/devcouncil/assets/devcouncil_logo_premium.png +0 -0
  25. package/src/devcouncil/cli/__init__.py +0 -0
  26. package/src/devcouncil/cli/commands/__init__.py +0 -0
  27. package/src/devcouncil/cli/commands/agents.py +0 -292
  28. package/src/devcouncil/cli/commands/artifacts.py +0 -54
  29. package/src/devcouncil/cli/commands/ast.py +0 -22
  30. package/src/devcouncil/cli/commands/baseline.py +0 -35
  31. package/src/devcouncil/cli/commands/check.py +0 -209
  32. package/src/devcouncil/cli/commands/config.py +0 -115
  33. package/src/devcouncil/cli/commands/cost.py +0 -57
  34. package/src/devcouncil/cli/commands/dashboard.py +0 -31
  35. package/src/devcouncil/cli/commands/doctor.py +0 -340
  36. package/src/devcouncil/cli/commands/evidence.py +0 -48
  37. package/src/devcouncil/cli/commands/go.py +0 -656
  38. package/src/devcouncil/cli/commands/handoff.py +0 -69
  39. package/src/devcouncil/cli/commands/hook.py +0 -237
  40. package/src/devcouncil/cli/commands/init.py +0 -289
  41. package/src/devcouncil/cli/commands/integrate.py +0 -1515
  42. package/src/devcouncil/cli/commands/lsp.py +0 -20
  43. package/src/devcouncil/cli/commands/map.py +0 -112
  44. package/src/devcouncil/cli/commands/mcp_server.py +0 -18
  45. package/src/devcouncil/cli/commands/plan.py +0 -488
  46. package/src/devcouncil/cli/commands/prompt.py +0 -61
  47. package/src/devcouncil/cli/commands/repair.py +0 -89
  48. package/src/devcouncil/cli/commands/report.py +0 -137
  49. package/src/devcouncil/cli/commands/reset_demo_state.py +0 -33
  50. package/src/devcouncil/cli/commands/rollback.py +0 -59
  51. package/src/devcouncil/cli/commands/run.py +0 -289
  52. package/src/devcouncil/cli/commands/runs.py +0 -223
  53. package/src/devcouncil/cli/commands/scaffold.py +0 -32
  54. package/src/devcouncil/cli/commands/semantic.py +0 -47
  55. package/src/devcouncil/cli/commands/setup.py +0 -362
  56. package/src/devcouncil/cli/commands/shell.py +0 -73
  57. package/src/devcouncil/cli/commands/show.py +0 -76
  58. package/src/devcouncil/cli/commands/skills.py +0 -88
  59. package/src/devcouncil/cli/commands/status.py +0 -141
  60. package/src/devcouncil/cli/commands/tasks.py +0 -55
  61. package/src/devcouncil/cli/commands/trace.py +0 -88
  62. package/src/devcouncil/cli/commands/verify.py +0 -328
  63. package/src/devcouncil/cli/commands/version.py +0 -20
  64. package/src/devcouncil/cli/commands/watch.py +0 -574
  65. package/src/devcouncil/cli/commands/watch_fs.py +0 -40
  66. package/src/devcouncil/cli/main.py +0 -137
  67. package/src/devcouncil/council/__init__.py +0 -0
  68. package/src/devcouncil/council/prompts/__init__.py +0 -0
  69. package/src/devcouncil/council/prompts/arbiter.md +0 -19
  70. package/src/devcouncil/council/prompts/critic_a.md +0 -10
  71. package/src/devcouncil/council/prompts/critic_b.md +0 -10
  72. package/src/devcouncil/council/prompts/implementation_reviewer.md +0 -16
  73. package/src/devcouncil/council/prompts/planner_a.md +0 -16
  74. package/src/devcouncil/council/prompts/planner_b.md +0 -16
  75. package/src/devcouncil/council/prompts/rebuttal.md +0 -10
  76. package/src/devcouncil/council/prompts/spec_writer.md +0 -12
  77. package/src/devcouncil/domain/__init__.py +0 -0
  78. package/src/devcouncil/domain/assumption.py +0 -17
  79. package/src/devcouncil/domain/critique.py +0 -32
  80. package/src/devcouncil/domain/evidence.py +0 -47
  81. package/src/devcouncil/domain/gap.py +0 -52
  82. package/src/devcouncil/domain/requirement.py +0 -22
  83. package/src/devcouncil/domain/task.py +0 -55
  84. package/src/devcouncil/execution/__init__.py +0 -1
  85. package/src/devcouncil/execution/checkpoints.py +0 -246
  86. package/src/devcouncil/execution/context_builder.py +0 -60
  87. package/src/devcouncil/execution/executor.py +0 -15
  88. package/src/devcouncil/execution/fs_watcher.py +0 -180
  89. package/src/devcouncil/execution/handoff.py +0 -102
  90. package/src/devcouncil/execution/hook_policy.py +0 -253
  91. package/src/devcouncil/execution/patch.py +0 -77
  92. package/src/devcouncil/execution/paths.py +0 -14
  93. package/src/devcouncil/execution/permissions.py +0 -85
  94. package/src/devcouncil/execution/policy_engine.py +0 -343
  95. package/src/devcouncil/execution/prompt_builder.py +0 -671
  96. package/src/devcouncil/execution/shell_session.py +0 -225
  97. package/src/devcouncil/execution/task_runner.py +0 -170
  98. package/src/devcouncil/executors/__init__.py +0 -1
  99. package/src/devcouncil/executors/agent_registry.py +0 -575
  100. package/src/devcouncil/executors/coding_cli.py +0 -736
  101. package/src/devcouncil/executors/mini_swe.py +0 -73
  102. package/src/devcouncil/executors/native/__init__.py +0 -0
  103. package/src/devcouncil/executors/native/agent.py +0 -208
  104. package/src/devcouncil/executors/openhands.py +0 -71
  105. package/src/devcouncil/gating/__init__.py +0 -1
  106. package/src/devcouncil/gating/checks/__init__.py +0 -0
  107. package/src/devcouncil/gating/checks/clean_git.py +0 -52
  108. package/src/devcouncil/gating/checks/planned_files_check.py +0 -32
  109. package/src/devcouncil/gating/checks/requirement_coverage.py +0 -26
  110. package/src/devcouncil/gating/checks/secret_scan_check.py +0 -53
  111. package/src/devcouncil/gating/policy.py +0 -338
  112. package/src/devcouncil/hardware.py +0 -184
  113. package/src/devcouncil/indexing/__init__.py +0 -1
  114. package/src/devcouncil/indexing/ast_matcher.py +0 -168
  115. package/src/devcouncil/indexing/graph_index.py +0 -48
  116. package/src/devcouncil/indexing/lsp.py +0 -161
  117. package/src/devcouncil/indexing/repo_mapper.py +0 -1455
  118. package/src/devcouncil/indexing/semantic_index.py +0 -205
  119. package/src/devcouncil/integrations/actions.py +0 -146
  120. package/src/devcouncil/integrations/check.py +0 -423
  121. package/src/devcouncil/integrations/code_review_graph.py +0 -163
  122. package/src/devcouncil/integrations/github.py +0 -39
  123. package/src/devcouncil/integrations/github_intent.py +0 -142
  124. package/src/devcouncil/integrations/gitnexus.py +0 -62
  125. package/src/devcouncil/integrations/graphify.py +0 -34
  126. package/src/devcouncil/integrations/mcp/__init__.py +0 -0
  127. package/src/devcouncil/integrations/mcp/server.py +0 -2122
  128. package/src/devcouncil/integrations/opencode_devcouncil_plugin.mjs +0 -24
  129. package/src/devcouncil/integrations/pr_comments.py +0 -62
  130. package/src/devcouncil/live/__init__.py +0 -2
  131. package/src/devcouncil/live/cards.py +0 -349
  132. package/src/devcouncil/live/models.py +0 -63
  133. package/src/devcouncil/live/repair_prompt.py +0 -83
  134. package/src/devcouncil/live/reviewer.py +0 -70
  135. package/src/devcouncil/live/signals.py +0 -135
  136. package/src/devcouncil/live/summary.py +0 -34
  137. package/src/devcouncil/live/tasks.py +0 -18
  138. package/src/devcouncil/live/transcripts.py +0 -141
  139. package/src/devcouncil/llm/__init__.py +0 -1
  140. package/src/devcouncil/llm/cache.py +0 -42
  141. package/src/devcouncil/llm/model_defaults.yaml +0 -44
  142. package/src/devcouncil/llm/provider.py +0 -627
  143. package/src/devcouncil/llm/router.py +0 -310
  144. package/src/devcouncil/optimization/__init__.py +0 -1
  145. package/src/devcouncil/optimization/gepa_agent.py +0 -318
  146. package/src/devcouncil/planning/__init__.py +0 -1
  147. package/src/devcouncil/planning/arbiter_service.py +0 -57
  148. package/src/devcouncil/planning/correction_manifest.py +0 -303
  149. package/src/devcouncil/planning/critique_service.py +0 -71
  150. package/src/devcouncil/planning/plan_service.py +0 -60
  151. package/src/devcouncil/planning/prompt_enhancer_service.py +0 -167
  152. package/src/devcouncil/planning/repair_service.py +0 -39
  153. package/src/devcouncil/planning/spec_service.py +0 -70
  154. package/src/devcouncil/repo/__init__.py +0 -0
  155. package/src/devcouncil/repo/ci_scaffold.py +0 -157
  156. package/src/devcouncil/repo/gitignore.py +0 -123
  157. package/src/devcouncil/repo/sca.py +0 -374
  158. package/src/devcouncil/reporting/__init__.py +0 -0
  159. package/src/devcouncil/reporting/github_check.py +0 -32
  160. package/src/devcouncil/reporting/json_report.py +0 -30
  161. package/src/devcouncil/reporting/markdown_report.py +0 -83
  162. package/src/devcouncil/reporting/report_builder.py +0 -14
  163. package/src/devcouncil/skills/__init__.py +0 -19
  164. package/src/devcouncil/skills/library/README.md +0 -46
  165. package/src/devcouncil/skills/library/ai-training.md +0 -50
  166. package/src/devcouncil/skills/library/android.md +0 -50
  167. package/src/devcouncil/skills/library/backend.md +0 -52
  168. package/src/devcouncil/skills/library/core-engineering.md +0 -95
  169. package/src/devcouncil/skills/library/data-engineering.md +0 -47
  170. package/src/devcouncil/skills/library/desktop.md +0 -46
  171. package/src/devcouncil/skills/library/devops.md +0 -48
  172. package/src/devcouncil/skills/library/game-dev.md +0 -46
  173. package/src/devcouncil/skills/library/ios.md +0 -48
  174. package/src/devcouncil/skills/library/mobile-cross-platform.md +0 -46
  175. package/src/devcouncil/skills/library/security.md +0 -48
  176. package/src/devcouncil/skills/library/systems.md +0 -48
  177. package/src/devcouncil/skills/library/web.md +0 -47
  178. package/src/devcouncil/skills/library/windows.md +0 -47
  179. package/src/devcouncil/skills/registry.py +0 -330
  180. package/src/devcouncil/storage/__init__.py +0 -0
  181. package/src/devcouncil/storage/db.py +0 -147
  182. package/src/devcouncil/storage/models.py +0 -204
  183. package/src/devcouncil/storage/native.py +0 -557
  184. package/src/devcouncil/storage/repositories.py +0 -485
  185. package/src/devcouncil/telemetry/__init__.py +0 -0
  186. package/src/devcouncil/telemetry/cost.py +0 -140
  187. package/src/devcouncil/telemetry/model_pricing.yaml +0 -48
  188. package/src/devcouncil/telemetry/pricing.py +0 -28
  189. package/src/devcouncil/telemetry/traces.py +0 -146
  190. package/src/devcouncil/telemetry/tracker.py +0 -52
  191. package/src/devcouncil/ui/__init__.py +0 -1
  192. package/src/devcouncil/ui/dashboard.py +0 -423
  193. package/src/devcouncil/utils/__init__.py +0 -1
  194. package/src/devcouncil/utils/redaction.py +0 -147
  195. package/src/devcouncil/utils/subprocess_env.py +0 -69
  196. package/src/devcouncil/verification/__init__.py +0 -1
  197. package/src/devcouncil/verification/acceptance_compiler.py +0 -125
  198. package/src/devcouncil/verification/ad_hoc_check.py +0 -129
  199. package/src/devcouncil/verification/diff_coverage.py +0 -353
  200. package/src/devcouncil/verification/implementation_reviewer.py +0 -55
  201. package/src/devcouncil/verification/next_actions.py +0 -189
  202. package/src/devcouncil/verification/sandbox.py +0 -178
  203. package/src/devcouncil/verification/test_resolver.py +0 -91
  204. package/src/devcouncil/verification/verifier.py +0 -1548
  205. package/uv.lock +0 -1226
@@ -1,485 +0,0 @@
1
- from sqlmodel import Session, col, delete, select
2
- from typing import List, Optional, Any
3
- import json
4
- from devcouncil.storage.models import (
5
- RequirementModel,
6
- AssumptionModel,
7
- TaskModel,
8
- GapModel,
9
- EvidenceModel,
10
- CritiqueFindingModel,
11
- ProjectStateModel,
12
- )
13
- from devcouncil.domain.requirement import Requirement, AcceptanceCriterion
14
- from devcouncil.domain.assumption import Assumption
15
- from devcouncil.domain.task import Task, PlannedFile
16
- from devcouncil.domain.gap import Gap
17
- from devcouncil.domain.evidence import CommandResult, DiffEvidence, DiffCoverageEvidence, TestEvidence
18
- from devcouncil.domain.critique import CritiqueFinding
19
- from devcouncil.artifacts.graph import ArtifactGraph
20
-
21
- class RequirementRepository:
22
- def __init__(self, session: Session):
23
- self.session = session
24
-
25
- def get_all(self) -> List[Requirement]:
26
- statement = select(RequirementModel)
27
- models = self.session.exec(statement).all()
28
- results = []
29
- for m in models:
30
- ac_list = [AcceptanceCriterion.model_validate(ac) for ac in json.loads(m.acceptance_criteria_json)]
31
- results.append(Requirement.model_validate({
32
- "id": m.id,
33
- "title": m.title,
34
- "description": m.description,
35
- "priority": m.priority,
36
- "source": m.source,
37
- "acceptance_criteria": ac_list,
38
- }))
39
- return results
40
-
41
- def save(self, req: Requirement):
42
- model = RequirementModel(
43
- id=req.id,
44
- title=req.title,
45
- description=req.description,
46
- priority=req.priority,
47
- source=req.source,
48
- acceptance_criteria_json=json.dumps([ac.model_dump() for ac in req.acceptance_criteria])
49
- )
50
- self.session.merge(model)
51
- self.session.commit()
52
-
53
-
54
- class AssumptionRepository:
55
- def __init__(self, session: Session):
56
- self.session = session
57
-
58
- def get_all(self) -> List[Assumption]:
59
- statement = select(AssumptionModel)
60
- models = self.session.exec(statement).all()
61
- return [
62
- Assumption.model_validate({
63
- "id": m.id,
64
- "statement": m.statement,
65
- "confidence": m.confidence,
66
- "impact": m.impact,
67
- "reversible": m.reversible,
68
- "requires_user_confirmation": m.requires_user_confirmation,
69
- "linked_requirement_ids": json.loads(m.linked_requirement_ids_json),
70
- "status": m.status,
71
- })
72
- for m in models
73
- ]
74
-
75
- def save(self, assumption: Assumption):
76
- model = AssumptionModel(
77
- id=assumption.id,
78
- statement=assumption.statement,
79
- confidence=assumption.confidence,
80
- impact=assumption.impact,
81
- reversible=assumption.reversible,
82
- requires_user_confirmation=assumption.requires_user_confirmation,
83
- linked_requirement_ids_json=json.dumps(assumption.linked_requirement_ids),
84
- status=assumption.status,
85
- )
86
- self.session.merge(model)
87
- self.session.commit()
88
-
89
- class TaskRepository:
90
- def __init__(self, session: Session):
91
- self.session = session
92
-
93
- def get_by_id(self, task_id: str) -> Optional[Task]:
94
- statement = select(TaskModel).where(TaskModel.id == task_id)
95
- m = self.session.exec(statement).first()
96
- if not m:
97
- return None
98
-
99
- pf_list = [PlannedFile.model_validate(pf) for pf in json.loads(m.planned_files_json)]
100
- return Task.model_validate({
101
- "id": m.id,
102
- "title": m.title,
103
- "description": m.description,
104
- "requirement_ids": json.loads(m.requirement_ids_json),
105
- "acceptance_criterion_ids": json.loads(m.acceptance_criterion_ids_json),
106
- "planned_files": pf_list,
107
- "expected_tests": json.loads(m.expected_tests_json),
108
- "allowed_commands": json.loads(m.allowed_commands_json),
109
- "forbidden_changes": json.loads(m.forbidden_changes_json),
110
- "status": m.status,
111
- })
112
-
113
- def get_all(self) -> List[Task]:
114
- statement = select(TaskModel)
115
- models = self.session.exec(statement).all()
116
- results = []
117
- for m in models:
118
- pf_list = [PlannedFile.model_validate(pf) for pf in json.loads(m.planned_files_json)]
119
- results.append(Task.model_validate({
120
- "id": m.id,
121
- "title": m.title,
122
- "description": m.description,
123
- "requirement_ids": json.loads(m.requirement_ids_json),
124
- "acceptance_criterion_ids": json.loads(m.acceptance_criterion_ids_json),
125
- "planned_files": pf_list,
126
- "expected_tests": json.loads(m.expected_tests_json),
127
- "allowed_commands": json.loads(m.allowed_commands_json),
128
- "forbidden_changes": json.loads(m.forbidden_changes_json),
129
- "status": m.status,
130
- }))
131
- return results
132
-
133
- def save(self, task: Task):
134
- model = TaskModel(
135
- id=task.id,
136
- title=task.title,
137
- description=task.description,
138
- requirement_ids_json=json.dumps(task.requirement_ids),
139
- acceptance_criterion_ids_json=json.dumps(task.acceptance_criterion_ids),
140
- planned_files_json=json.dumps([pf.model_dump() for pf in task.planned_files]),
141
- expected_tests_json=json.dumps(task.expected_tests),
142
- allowed_commands_json=json.dumps(task.allowed_commands),
143
- forbidden_changes_json=json.dumps(task.forbidden_changes),
144
- status=task.status
145
- )
146
- self.session.merge(model)
147
- self.session.commit()
148
-
149
- class GapRepository:
150
- def __init__(self, session: Session):
151
- self.session = session
152
-
153
- def get_all(self) -> List[Gap]:
154
- statement = select(GapModel)
155
- models = self.session.exec(statement).all()
156
- results = []
157
- for m in models:
158
- results.append(Gap.model_validate({
159
- "id": m.id,
160
- "severity": m.severity,
161
- "gap_type": m.gap_type,
162
- "requirement_id": m.requirement_id,
163
- "task_id": m.task_id,
164
- "description": m.description,
165
- "evidence": json.loads(m.evidence_json),
166
- "recommended_fix": m.recommended_fix,
167
- "blocking": m.blocking,
168
- "file": m.file,
169
- "line": m.line,
170
- "suggested_command": m.suggested_command,
171
- "acceptance_criterion_id": m.acceptance_criterion_id,
172
- }))
173
- return results
174
-
175
- def save(self, gap: Gap):
176
- model = GapModel(
177
- id=gap.id,
178
- severity=gap.severity,
179
- gap_type=gap.gap_type,
180
- requirement_id=gap.requirement_id,
181
- task_id=gap.task_id,
182
- description=gap.description,
183
- evidence_json=json.dumps(gap.evidence),
184
- recommended_fix=gap.recommended_fix,
185
- blocking=gap.blocking,
186
- file=gap.file,
187
- line=gap.line,
188
- suggested_command=gap.suggested_command,
189
- acceptance_criterion_id=gap.acceptance_criterion_id,
190
- )
191
- self.session.merge(model)
192
- self.session.commit()
193
-
194
- def delete_for_task(self, task_id: str):
195
- self.session.exec(delete(GapModel).where(col(GapModel.task_id) == task_id))
196
- self.session.commit()
197
-
198
- def delete_plan_gaps(self):
199
- self.session.exec(delete(GapModel).where(col(GapModel.id).like("GAP-PLAN-%")))
200
- self.session.commit()
201
-
202
- class EvidenceRepository:
203
- def __init__(self, session: Session):
204
- self.session = session
205
-
206
- def save_command_result(self, task_id: str, result: CommandResult):
207
- model = EvidenceModel(
208
- type="command",
209
- task_id=task_id,
210
- data_json=result.model_dump_json()
211
- )
212
- self.session.add(model)
213
- self.session.commit()
214
-
215
- def save_diff_evidence(self, ev: DiffEvidence):
216
- model = EvidenceModel(
217
- type="diff",
218
- task_id=ev.task_id,
219
- data_json=ev.model_dump_json()
220
- )
221
- self.session.add(model)
222
- self.session.commit()
223
-
224
- def save_test_evidence(self, ev: TestEvidence, task_id: Optional[str] = None):
225
- model = EvidenceModel(
226
- type="test",
227
- task_id=task_id,
228
- requirement_id=ev.requirement_id,
229
- acceptance_criterion_id=ev.acceptance_criterion_id,
230
- data_json=ev.model_dump_json()
231
- )
232
- self.session.add(model)
233
- self.session.commit()
234
-
235
- def save_diff_coverage_evidence(self, ev: DiffCoverageEvidence):
236
- model = EvidenceModel(
237
- type="diff_coverage",
238
- task_id=ev.task_id,
239
- data_json=ev.model_dump_json()
240
- )
241
- self.session.add(model)
242
- self.session.commit()
243
-
244
- def get_command_results_for_task(self, task_id: str) -> List[CommandResult]:
245
- statement = select(EvidenceModel).where(EvidenceModel.task_id == task_id).where(
246
- EvidenceModel.type == "command"
247
- )
248
- models = self.session.exec(statement).all()
249
- return [CommandResult.model_validate(json.loads(m.data_json)) for m in models]
250
-
251
- def get_all(self) -> List[Any]:
252
- statement = select(EvidenceModel)
253
- models = self.session.exec(statement).all()
254
- results: List[Any] = []
255
- for m in models:
256
- data = json.loads(m.data_json)
257
- if m.type == "command":
258
- results.append(CommandResult.model_validate(data))
259
- elif m.type == "diff":
260
- results.append(DiffEvidence.model_validate(data))
261
- elif m.type == "diff_coverage":
262
- results.append(DiffCoverageEvidence.model_validate(data))
263
- elif m.type == "test":
264
- results.append(TestEvidence.model_validate(data))
265
- return results
266
-
267
- def delete_for_task(self, task_id: str):
268
- self.session.exec(delete(EvidenceModel).where(col(EvidenceModel.task_id) == task_id))
269
- self.session.commit()
270
-
271
-
272
- class CritiqueFindingRepository:
273
- def __init__(self, session: Session):
274
- self.session = session
275
-
276
- def get_all(self) -> List[CritiqueFinding]:
277
- statement = select(CritiqueFindingModel)
278
- models = self.session.exec(statement).all()
279
- return [
280
- CritiqueFinding.model_validate({
281
- "id": m.id,
282
- "source_agent": m.source_agent,
283
- "target_plan_id": m.target_plan_id,
284
- "severity": m.severity,
285
- "finding_type": m.finding_type,
286
- "claim": m.claim,
287
- "linked_requirement_id": m.linked_requirement_id,
288
- "suggested_requirement": m.suggested_requirement,
289
- "suggested_task": m.suggested_task,
290
- "falsifiable_check": m.falsifiable_check,
291
- "status": m.status,
292
- })
293
- for m in models
294
- ]
295
-
296
- def save(self, finding: CritiqueFinding):
297
- model = CritiqueFindingModel(
298
- id=finding.id,
299
- source_agent=finding.source_agent,
300
- target_plan_id=finding.target_plan_id,
301
- severity=finding.severity,
302
- finding_type=finding.finding_type,
303
- claim=finding.claim,
304
- linked_requirement_id=finding.linked_requirement_id,
305
- suggested_requirement=finding.suggested_requirement,
306
- suggested_task=finding.suggested_task,
307
- falsifiable_check=finding.falsifiable_check,
308
- status=finding.status,
309
- )
310
- self.session.merge(model)
311
- self.session.commit()
312
-
313
-
314
- class PlanningStateRepository:
315
- """Manage the persisted active plan graph."""
316
-
317
- def __init__(self, session: Session):
318
- self.session = session
319
-
320
- def clear_active_plan(self):
321
- for model in (
322
- EvidenceModel,
323
- GapModel,
324
- CritiqueFindingModel,
325
- TaskModel,
326
- AssumptionModel,
327
- RequirementModel,
328
- ):
329
- self.session.exec(delete(model))
330
-
331
- def replace_active_plan(
332
- self,
333
- requirements: list[Requirement],
334
- assumptions: list[Assumption],
335
- tasks: list[Task],
336
- findings: list[CritiqueFinding],
337
- ):
338
- self.clear_active_plan()
339
- for req in requirements:
340
- self.session.merge(RequirementModel(
341
- id=req.id,
342
- title=req.title,
343
- description=req.description,
344
- priority=req.priority,
345
- source=req.source,
346
- acceptance_criteria_json=json.dumps([ac.model_dump() for ac in req.acceptance_criteria]),
347
- ))
348
-
349
- for assumption in assumptions:
350
- self.session.merge(AssumptionModel(
351
- id=assumption.id,
352
- statement=assumption.statement,
353
- confidence=assumption.confidence,
354
- impact=assumption.impact,
355
- reversible=assumption.reversible,
356
- requires_user_confirmation=assumption.requires_user_confirmation,
357
- linked_requirement_ids_json=json.dumps(assumption.linked_requirement_ids),
358
- status=assumption.status,
359
- ))
360
-
361
- for task in tasks:
362
- self.session.merge(TaskModel(
363
- id=task.id,
364
- title=task.title,
365
- description=task.description,
366
- requirement_ids_json=json.dumps(task.requirement_ids),
367
- acceptance_criterion_ids_json=json.dumps(task.acceptance_criterion_ids),
368
- planned_files_json=json.dumps([pf.model_dump() for pf in task.planned_files]),
369
- expected_tests_json=json.dumps(task.expected_tests),
370
- allowed_commands_json=json.dumps(task.allowed_commands),
371
- forbidden_changes_json=json.dumps(task.forbidden_changes),
372
- status=task.status,
373
- ))
374
-
375
- for finding in findings:
376
- self.session.merge(CritiqueFindingModel(
377
- id=finding.id,
378
- source_agent=finding.source_agent,
379
- target_plan_id=finding.target_plan_id,
380
- severity=finding.severity,
381
- finding_type=finding.finding_type,
382
- claim=finding.claim,
383
- linked_requirement_id=finding.linked_requirement_id,
384
- suggested_requirement=finding.suggested_requirement,
385
- suggested_task=finding.suggested_task,
386
- falsifiable_check=finding.falsifiable_check,
387
- status=finding.status,
388
- ))
389
-
390
- class ArtifactGraphRepository:
391
- def __init__(self, session: Session):
392
- self.session = session
393
- self.req_repo = RequirementRepository(session)
394
- self.assumption_repo = AssumptionRepository(session)
395
- self.task_repo = TaskRepository(session)
396
- self.gap_repo = GapRepository(session)
397
- self.evidence_repo = EvidenceRepository(session)
398
- self.finding_repo = CritiqueFindingRepository(session)
399
-
400
- def load_graph(self) -> ArtifactGraph:
401
- graph = ArtifactGraph()
402
- for req in self.req_repo.get_all():
403
- graph.add_requirement(req)
404
- for assumption in self.assumption_repo.get_all():
405
- graph.add_assumption(assumption)
406
- for task in self.task_repo.get_all():
407
- graph.add_task(task)
408
- for finding in self.finding_repo.get_all():
409
- graph.add_finding(finding)
410
- for gap in self.gap_repo.get_all():
411
- graph.add_gap(gap)
412
- for ev in self.evidence_repo.get_all():
413
- if isinstance(ev, CommandResult):
414
- graph.add_command_result(ev)
415
- elif isinstance(ev, DiffCoverageEvidence):
416
- # Must precede DiffEvidence: distinct type, and the diff↔coverage proof
417
- # was silently dropped on reload before this branch existed.
418
- graph.add_diff_coverage_evidence(ev)
419
- elif isinstance(ev, DiffEvidence):
420
- graph.add_diff_evidence(ev)
421
- elif isinstance(ev, TestEvidence):
422
- graph.add_test_evidence(ev)
423
- return graph
424
-
425
-
426
- class StateRepository:
427
- def __init__(self, session: Session):
428
- self.session = session
429
-
430
- def get_state(self) -> Optional[ProjectStateModel]:
431
- statement = select(ProjectStateModel).where(ProjectStateModel.id == "singleton")
432
- return self.session.exec(statement).first()
433
-
434
- def save_state(self, state: ProjectStateModel | str, history: Optional[List[str]] = None):
435
- if isinstance(state, ProjectStateModel):
436
- state.id = "singleton"
437
- else:
438
- state = ProjectStateModel(
439
- id="singleton",
440
- current_phase=state,
441
- history_json=json.dumps(history or []),
442
- )
443
- self.session.merge(state)
444
- self.session.commit()
445
-
446
- def record_phase(self, phase: str):
447
- current = self.get_state()
448
- history = []
449
- prev = None
450
- if current:
451
- history = json.loads(current.history_json)
452
- prev = current.current_phase
453
- # Keep the persisted history a LEGAL transition sequence. A repair re-run records
454
- # TASK_EXECUTING straight after a terminal TASK_BLOCKED/TASK_VERIFIED, which is not
455
- # a direct transition; bridge it through the intermediate phase (TASK_READY) so the
456
- # state machine's invariant holds on reload. Fail-soft: an unbridgeable jump is
457
- # still recorded verbatim rather than lost.
458
- for step in self._bridge_phases(prev, phase):
459
- if not history or history[-1] != step:
460
- history.append(step)
461
- self.save_state(phase, history)
462
-
463
- @staticmethod
464
- def _bridge_phases(prev: Optional[str], target: str) -> List[str]:
465
- """Return the phases to append so prev -> ... -> target is a legal path.
466
-
467
- Inserts a single intermediate phase when prev cannot transition directly to
468
- target but a one-hop bridge exists (the repair loop's
469
- TASK_BLOCKED/TASK_VERIFIED -> TASK_READY -> TASK_EXECUTING). Returns ``[target]``
470
- when prev is unknown, equal, already-legal, or no one-hop bridge exists."""
471
- if prev is None or prev == target:
472
- return [target]
473
- try:
474
- from devcouncil.app.state_machine import TRANSITIONS, ProjectPhase
475
-
476
- prev_phase = ProjectPhase(prev)
477
- target_phase = ProjectPhase(target)
478
- except (ImportError, ValueError):
479
- return [target]
480
- if target_phase in TRANSITIONS.get(prev_phase, set()):
481
- return [target]
482
- for mid in TRANSITIONS.get(prev_phase, set()):
483
- if target_phase in TRANSITIONS.get(mid, set()):
484
- return [mid.value, target]
485
- return [target]
File without changes
@@ -1,140 +0,0 @@
1
- import json
2
- import logging
3
- from pathlib import Path
4
- from typing import Any, Dict, List
5
-
6
- from devcouncil.telemetry.pricing import pricing_for_model
7
-
8
- logger = logging.getLogger(__name__)
9
-
10
- UNATTRIBUTED = "(unattributed)"
11
-
12
-
13
- class CostEstimator:
14
- """Estimates LLM usage cost based on provider pricing."""
15
-
16
- # Conservative default for unknown models
17
- DEFAULT_PRICING = {"prompt_per_1k": 0.005, "completion_per_1k": 0.015}
18
-
19
- # Local providers run on-device and incur no per-token cost. Ollama model ids
20
- # are open-ended (e.g. ``qwen2.5-coder:7b`` or an ``ollama/<name>`` form), so
21
- # match the conventional prefixes rather than relying on the open-ended yaml.
22
- LOCAL_MODEL_PREFIXES = ("ollama/", "ollama:")
23
-
24
- @classmethod
25
- def _is_local_model(cls, model: str) -> bool:
26
- return model.startswith(cls.LOCAL_MODEL_PREFIXES)
27
-
28
- @classmethod
29
- def estimate_cost(cls, model: str, usage: Dict[str, int]) -> float:
30
- # Local/Ollama models are free regardless of yaml coverage; short-circuit
31
- # before the conservative DEFAULT_PRICING fallback would bill them.
32
- if cls._is_local_model(model):
33
- return 0.0
34
- prices = pricing_for_model(model, cls.DEFAULT_PRICING)
35
- if prices == cls.DEFAULT_PRICING:
36
- logger.debug("Unknown model for cost estimation: %s — using default pricing", model)
37
-
38
- prompt_tokens = usage.get("prompt_tokens", 0)
39
- completion_tokens = usage.get("completion_tokens", 0)
40
-
41
- cost = ((prompt_tokens / 1000.0) * prices["prompt_per_1k"]) + (
42
- (completion_tokens / 1000.0) * prices["completion_per_1k"]
43
- )
44
- return cost
45
-
46
-
47
- def _model_calls_file(project_root: Path) -> Path:
48
- return project_root / ".devcouncil" / "logs" / "model_calls.jsonl"
49
-
50
-
51
- def read_cost_records(project_root: Path) -> List[Dict[str, Any]]:
52
- """Read the model-call ledger and attach an estimated cost to each record.
53
-
54
- Never raises: malformed lines are skipped. Records missing ``task_id`` /
55
- ``run_id`` (older entries written before per-task attribution) keep those as
56
- ``None`` so callers can bucket them under ``(unattributed)``.
57
- """
58
- log_file = _model_calls_file(project_root)
59
- records: List[Dict[str, Any]] = []
60
- if not log_file.exists():
61
- return records
62
- try:
63
- lines = log_file.read_text(encoding="utf-8").splitlines()
64
- except Exception as exc:
65
- logger.debug("Failed to read model_calls ledger: %s", exc)
66
- return records
67
-
68
- for line in lines:
69
- if not line.strip():
70
- continue
71
- try:
72
- entry = json.loads(line)
73
- except Exception as exc:
74
- logger.debug("Skipping invalid model_calls line: %s", exc)
75
- continue
76
- model = ""
77
- response = entry.get("response")
78
- if isinstance(response, dict):
79
- model = str(response.get("model", "") or "")
80
- raw_usage = entry.get("usage")
81
- usage: Dict[str, Any] = raw_usage if isinstance(raw_usage, dict) else {}
82
- # Local providers (ollama) are always free, regardless of the open-ended model
83
- # tag Ollama echoes back (e.g. ``mistral:latest``) — trust the recorded provider
84
- # over fragile model-id prefix matching.
85
- provider = str(entry.get("provider") or "")
86
- try:
87
- cost = 0.0 if provider == "ollama" else CostEstimator.estimate_cost(model, usage)
88
- except Exception:
89
- cost = 0.0
90
- records.append(
91
- {
92
- "task_id": entry.get("task_id"),
93
- "run_id": entry.get("run_id"),
94
- "timestamp": entry.get("timestamp"),
95
- "model": model,
96
- "usage": usage,
97
- "cost": cost,
98
- }
99
- )
100
- return records
101
-
102
-
103
- def _group(records: List[Dict[str, Any]], key: str) -> Dict[str, Dict[str, Any]]:
104
- groups: Dict[str, Dict[str, Any]] = {}
105
- for record in records:
106
- raw = record.get(key)
107
- bucket = str(raw) if isinstance(raw, str) and raw else UNATTRIBUTED
108
- group = groups.setdefault(
109
- bucket,
110
- {"cost": 0.0, "calls": 0, "prompt_tokens": 0, "completion_tokens": 0},
111
- )
112
- raw_usage = record.get("usage")
113
- usage: Dict[str, Any] = raw_usage if isinstance(raw_usage, dict) else {}
114
- group["cost"] += float(record.get("cost", 0.0) or 0.0)
115
- group["calls"] += 1
116
- group["prompt_tokens"] += int(usage.get("prompt_tokens", 0) or 0)
117
- group["completion_tokens"] += int(usage.get("completion_tokens", 0) or 0)
118
- return groups
119
-
120
-
121
- def group_cost(project_root: Path) -> Dict[str, Any]:
122
- """Aggregate model-call cost grouped by ``task_id`` and ``run_id``.
123
-
124
- Returns a JSON-friendly summary: a grand total plus per-task and per-run
125
- breakdowns. Unattributed records (older entries, or calls made without a
126
- task/run context) are bucketed under ``(unattributed)``. Never raises.
127
- """
128
- records = read_cost_records(project_root)
129
- total_cost = sum(float(record.get("cost", 0.0) or 0.0) for record in records)
130
- return {
131
- "total_cost": total_cost,
132
- "total_calls": len(records),
133
- "by_task": _group(records, "task_id"),
134
- "by_run": _group(records, "run_id"),
135
- }
136
-
137
-
138
- def cost_by_task(project_root: Path) -> Dict[str, Dict[str, Any]]:
139
- """Convenience accessor for the per-task cost breakdown (used by ``dev status``)."""
140
- return group_cost(project_root)["by_task"]
@@ -1,48 +0,0 @@
1
- anthropic/claude-3-opus:
2
- prompt_per_1k: 0.015
3
- completion_per_1k: 0.075
4
- anthropic/claude-3.5-sonnet:
5
- prompt_per_1k: 0.003
6
- completion_per_1k: 0.015
7
- anthropic/claude-sonnet-4:
8
- prompt_per_1k: 0.003
9
- completion_per_1k: 0.015
10
- google/gemini-pro-1.5:
11
- prompt_per_1k: 0.00125
12
- completion_per_1k: 0.005
13
- google/gemini-2.5-pro:
14
- prompt_per_1k: 0.00125
15
- completion_per_1k: 0.01
16
- openai/gpt-4o:
17
- prompt_per_1k: 0.005
18
- completion_per_1k: 0.015
19
- openai/o3-mini:
20
- prompt_per_1k: 0.0011
21
- completion_per_1k: 0.0044
22
- anthropic/claude-sonnet-4.6:
23
- prompt_per_1k: 0.003
24
- completion_per_1k: 0.015
25
- anthropic/claude-opus-4.8:
26
- prompt_per_1k: 0.005
27
- completion_per_1k: 0.025
28
- openai/gpt-5.5:
29
- prompt_per_1k: 0.005
30
- completion_per_1k: 0.03
31
- google/gemini-2.5-flash:
32
- prompt_per_1k: 0.0003
33
- completion_per_1k: 0.0025
34
- # Local Ollama models run on-device and are free. Bare tags (no ``ollama/``
35
- # prefix) are listed here so the cost reporter shows $0; the cost.py prefix
36
- # guard covers any other ``ollama/`` or ``ollama:`` model id.
37
- qwen2.5-coder:7b:
38
- prompt_per_1k: 0.0
39
- completion_per_1k: 0.0
40
- qwen2.5-coder:14b:
41
- prompt_per_1k: 0.0
42
- completion_per_1k: 0.0
43
- qwen2.5-coder:32b:
44
- prompt_per_1k: 0.0
45
- completion_per_1k: 0.0
46
- llama3.1:
47
- prompt_per_1k: 0.0
48
- completion_per_1k: 0.0