devcouncil 0.1.1 → 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 (146) hide show
  1. package/README.md +218 -96
  2. package/bin/devcouncil.js +130 -32
  3. package/package.json +7 -11
  4. package/pyproject.toml +0 -34
  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 -158
  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 -143
  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/cli/__init__.py +0 -0
  23. package/src/devcouncil/cli/commands/__init__.py +0 -0
  24. package/src/devcouncil/cli/commands/artifacts.py +0 -51
  25. package/src/devcouncil/cli/commands/ast.py +0 -22
  26. package/src/devcouncil/cli/commands/baseline.py +0 -35
  27. package/src/devcouncil/cli/commands/config.py +0 -76
  28. package/src/devcouncil/cli/commands/dashboard.py +0 -26
  29. package/src/devcouncil/cli/commands/doctor.py +0 -140
  30. package/src/devcouncil/cli/commands/go.py +0 -237
  31. package/src/devcouncil/cli/commands/hook.py +0 -128
  32. package/src/devcouncil/cli/commands/init.py +0 -153
  33. package/src/devcouncil/cli/commands/integrate.py +0 -726
  34. package/src/devcouncil/cli/commands/lsp.py +0 -20
  35. package/src/devcouncil/cli/commands/map.py +0 -42
  36. package/src/devcouncil/cli/commands/mcp_server.py +0 -18
  37. package/src/devcouncil/cli/commands/plan.py +0 -327
  38. package/src/devcouncil/cli/commands/prompt.py +0 -50
  39. package/src/devcouncil/cli/commands/repair.py +0 -72
  40. package/src/devcouncil/cli/commands/report.py +0 -129
  41. package/src/devcouncil/cli/commands/reset_demo_state.py +0 -31
  42. package/src/devcouncil/cli/commands/rollback.py +0 -60
  43. package/src/devcouncil/cli/commands/run.py +0 -269
  44. package/src/devcouncil/cli/commands/setup.py +0 -223
  45. package/src/devcouncil/cli/commands/show.py +0 -76
  46. package/src/devcouncil/cli/commands/status.py +0 -117
  47. package/src/devcouncil/cli/commands/tasks.py +0 -55
  48. package/src/devcouncil/cli/commands/trace.py +0 -44
  49. package/src/devcouncil/cli/commands/verify.py +0 -193
  50. package/src/devcouncil/cli/commands/version.py +0 -20
  51. package/src/devcouncil/cli/commands/watch.py +0 -574
  52. package/src/devcouncil/cli/main.py +0 -88
  53. package/src/devcouncil/council/__init__.py +0 -0
  54. package/src/devcouncil/council/prompts/__init__.py +0 -0
  55. package/src/devcouncil/council/prompts/arbiter.md +0 -19
  56. package/src/devcouncil/council/prompts/critic_a.md +0 -10
  57. package/src/devcouncil/council/prompts/critic_b.md +0 -10
  58. package/src/devcouncil/council/prompts/implementation_reviewer.md +0 -16
  59. package/src/devcouncil/council/prompts/planner_a.md +0 -16
  60. package/src/devcouncil/council/prompts/planner_b.md +0 -16
  61. package/src/devcouncil/council/prompts/rebuttal.md +0 -10
  62. package/src/devcouncil/council/prompts/spec_writer.md +0 -12
  63. package/src/devcouncil/domain/__init__.py +0 -0
  64. package/src/devcouncil/domain/assumption.py +0 -17
  65. package/src/devcouncil/domain/critique.py +0 -32
  66. package/src/devcouncil/domain/evidence.py +0 -27
  67. package/src/devcouncil/domain/gap.py +0 -26
  68. package/src/devcouncil/domain/requirement.py +0 -22
  69. package/src/devcouncil/domain/task.py +0 -26
  70. package/src/devcouncil/execution/__init__.py +0 -1
  71. package/src/devcouncil/execution/context_builder.py +0 -60
  72. package/src/devcouncil/execution/executor.py +0 -15
  73. package/src/devcouncil/execution/hook_policy.py +0 -165
  74. package/src/devcouncil/execution/patch.py +0 -28
  75. package/src/devcouncil/execution/paths.py +0 -14
  76. package/src/devcouncil/execution/permissions.py +0 -92
  77. package/src/devcouncil/execution/prompt_builder.py +0 -59
  78. package/src/devcouncil/execution/task_runner.py +0 -166
  79. package/src/devcouncil/executors/__init__.py +0 -1
  80. package/src/devcouncil/executors/coding_cli.py +0 -112
  81. package/src/devcouncil/executors/mini_swe.py +0 -73
  82. package/src/devcouncil/executors/native/__init__.py +0 -0
  83. package/src/devcouncil/executors/native/agent.py +0 -107
  84. package/src/devcouncil/executors/openhands.py +0 -71
  85. package/src/devcouncil/gating/__init__.py +0 -1
  86. package/src/devcouncil/gating/checks/__init__.py +0 -0
  87. package/src/devcouncil/gating/checks/clean_git.py +0 -50
  88. package/src/devcouncil/gating/checks/planned_files_check.py +0 -32
  89. package/src/devcouncil/gating/checks/requirement_coverage.py +0 -26
  90. package/src/devcouncil/gating/checks/secret_scan_check.py +0 -34
  91. package/src/devcouncil/gating/policy.py +0 -190
  92. package/src/devcouncil/indexing/__init__.py +0 -1
  93. package/src/devcouncil/indexing/ast_matcher.py +0 -168
  94. package/src/devcouncil/indexing/graph_index.py +0 -48
  95. package/src/devcouncil/indexing/lsp.py +0 -120
  96. package/src/devcouncil/indexing/repo_mapper.py +0 -208
  97. package/src/devcouncil/indexing/symbol_index.py +0 -0
  98. package/src/devcouncil/integrations/code_review_graph.py +0 -163
  99. package/src/devcouncil/integrations/github.py +0 -39
  100. package/src/devcouncil/integrations/gitnexus.py +0 -27
  101. package/src/devcouncil/integrations/graphify.py +0 -34
  102. package/src/devcouncil/integrations/mcp/__init__.py +0 -0
  103. package/src/devcouncil/integrations/mcp/server.py +0 -599
  104. package/src/devcouncil/integrations/pr_comments.py +0 -62
  105. package/src/devcouncil/live/__init__.py +0 -2
  106. package/src/devcouncil/live/cards.py +0 -207
  107. package/src/devcouncil/live/models.py +0 -63
  108. package/src/devcouncil/live/repair_prompt.py +0 -83
  109. package/src/devcouncil/live/reviewer.py +0 -70
  110. package/src/devcouncil/live/signals.py +0 -135
  111. package/src/devcouncil/live/summary.py +0 -34
  112. package/src/devcouncil/live/tasks.py +0 -18
  113. package/src/devcouncil/live/transcripts.py +0 -138
  114. package/src/devcouncil/llm/__init__.py +0 -1
  115. package/src/devcouncil/llm/cache.py +0 -38
  116. package/src/devcouncil/llm/provider.py +0 -146
  117. package/src/devcouncil/llm/router.py +0 -125
  118. package/src/devcouncil/planning/__init__.py +0 -1
  119. package/src/devcouncil/planning/arbiter_service.py +0 -57
  120. package/src/devcouncil/planning/critique_service.py +0 -66
  121. package/src/devcouncil/planning/plan_service.py +0 -46
  122. package/src/devcouncil/planning/prompt_enhancer_service.py +0 -86
  123. package/src/devcouncil/planning/repair_service.py +0 -39
  124. package/src/devcouncil/planning/spec_service.py +0 -44
  125. package/src/devcouncil/repo/__init__.py +0 -0
  126. package/src/devcouncil/reporting/__init__.py +0 -0
  127. package/src/devcouncil/reporting/github_check.py +0 -32
  128. package/src/devcouncil/reporting/json_report.py +0 -20
  129. package/src/devcouncil/reporting/markdown_report.py +0 -68
  130. package/src/devcouncil/reporting/report_builder.py +0 -14
  131. package/src/devcouncil/storage/__init__.py +0 -0
  132. package/src/devcouncil/storage/db.py +0 -66
  133. package/src/devcouncil/storage/models.py +0 -83
  134. package/src/devcouncil/storage/repositories.py +0 -423
  135. package/src/devcouncil/telemetry/__init__.py +0 -0
  136. package/src/devcouncil/telemetry/cost.py +0 -34
  137. package/src/devcouncil/telemetry/traces.py +0 -91
  138. package/src/devcouncil/telemetry/tracker.py +0 -49
  139. package/src/devcouncil/ui/__init__.py +0 -1
  140. package/src/devcouncil/ui/dashboard.py +0 -122
  141. package/src/devcouncil/utils/__init__.py +0 -1
  142. package/src/devcouncil/utils/redaction.py +0 -141
  143. package/src/devcouncil/verification/__init__.py +0 -1
  144. package/src/devcouncil/verification/implementation_reviewer.py +0 -55
  145. package/src/devcouncil/verification/verifier.py +0 -530
  146. package/uv.lock +0 -1085
@@ -1,423 +0,0 @@
1
- from sqlmodel import Session, select, delete
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, 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(
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(
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(
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(
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(
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
- ))
169
- return results
170
-
171
- def save(self, gap: Gap):
172
- model = GapModel(
173
- id=gap.id,
174
- severity=gap.severity,
175
- gap_type=gap.gap_type,
176
- requirement_id=gap.requirement_id,
177
- task_id=gap.task_id,
178
- description=gap.description,
179
- evidence_json=json.dumps(gap.evidence),
180
- recommended_fix=gap.recommended_fix,
181
- blocking=gap.blocking
182
- )
183
- self.session.merge(model)
184
- self.session.commit()
185
-
186
- def delete_for_task(self, task_id: str):
187
- self.session.exec(delete(GapModel).where(GapModel.task_id == task_id))
188
- self.session.commit()
189
-
190
- def delete_plan_gaps(self):
191
- self.session.exec(delete(GapModel).where(GapModel.id.like("GAP-PLAN-%")))
192
- self.session.commit()
193
-
194
- class EvidenceRepository:
195
- def __init__(self, session: Session):
196
- self.session = session
197
-
198
- def save_command_result(self, task_id: str, result: CommandResult):
199
- model = EvidenceModel(
200
- type="command",
201
- task_id=task_id,
202
- data_json=result.model_dump_json()
203
- )
204
- self.session.add(model)
205
- self.session.commit()
206
-
207
- def save_diff_evidence(self, ev: DiffEvidence):
208
- model = EvidenceModel(
209
- type="diff",
210
- task_id=ev.task_id,
211
- data_json=ev.model_dump_json()
212
- )
213
- self.session.add(model)
214
- self.session.commit()
215
-
216
- def save_test_evidence(self, ev: TestEvidence, task_id: Optional[str] = None):
217
- model = EvidenceModel(
218
- type="test",
219
- task_id=task_id,
220
- requirement_id=ev.requirement_id,
221
- acceptance_criterion_id=ev.acceptance_criterion_id,
222
- data_json=ev.model_dump_json()
223
- )
224
- self.session.add(model)
225
- self.session.commit()
226
-
227
- def get_all(self) -> List[Any]:
228
- statement = select(EvidenceModel)
229
- models = self.session.exec(statement).all()
230
- results = []
231
- for m in models:
232
- data = json.loads(m.data_json)
233
- if m.type == "command":
234
- results.append(CommandResult.model_validate(data))
235
- elif m.type == "diff":
236
- results.append(DiffEvidence.model_validate(data))
237
- elif m.type == "test":
238
- results.append(TestEvidence.model_validate(data))
239
- return results
240
-
241
- def delete_for_task(self, task_id: str):
242
- self.session.exec(delete(EvidenceModel).where(EvidenceModel.task_id == task_id))
243
- self.session.commit()
244
-
245
-
246
- class CritiqueFindingRepository:
247
- def __init__(self, session: Session):
248
- self.session = session
249
-
250
- def get_all(self) -> List[CritiqueFinding]:
251
- statement = select(CritiqueFindingModel)
252
- models = self.session.exec(statement).all()
253
- return [
254
- CritiqueFinding(
255
- id=m.id,
256
- source_agent=m.source_agent,
257
- target_plan_id=m.target_plan_id,
258
- severity=m.severity,
259
- finding_type=m.finding_type,
260
- claim=m.claim,
261
- linked_requirement_id=m.linked_requirement_id,
262
- suggested_requirement=m.suggested_requirement,
263
- suggested_task=m.suggested_task,
264
- falsifiable_check=m.falsifiable_check,
265
- status=m.status,
266
- )
267
- for m in models
268
- ]
269
-
270
- def save(self, finding: CritiqueFinding):
271
- model = CritiqueFindingModel(
272
- id=finding.id,
273
- source_agent=finding.source_agent,
274
- target_plan_id=finding.target_plan_id,
275
- severity=finding.severity,
276
- finding_type=finding.finding_type,
277
- claim=finding.claim,
278
- linked_requirement_id=finding.linked_requirement_id,
279
- suggested_requirement=finding.suggested_requirement,
280
- suggested_task=finding.suggested_task,
281
- falsifiable_check=finding.falsifiable_check,
282
- status=finding.status,
283
- )
284
- self.session.merge(model)
285
- self.session.commit()
286
-
287
-
288
- class PlanningStateRepository:
289
- """Manage the persisted active plan graph."""
290
-
291
- def __init__(self, session: Session):
292
- self.session = session
293
-
294
- def clear_active_plan(self):
295
- for model in (
296
- EvidenceModel,
297
- GapModel,
298
- CritiqueFindingModel,
299
- TaskModel,
300
- AssumptionModel,
301
- RequirementModel,
302
- ):
303
- self.session.exec(delete(model))
304
-
305
- def replace_active_plan(
306
- self,
307
- requirements: list[Requirement],
308
- assumptions: list[Assumption],
309
- tasks: list[Task],
310
- findings: list[CritiqueFinding],
311
- ):
312
- self.clear_active_plan()
313
- for req in requirements:
314
- self.session.merge(RequirementModel(
315
- id=req.id,
316
- title=req.title,
317
- description=req.description,
318
- priority=req.priority,
319
- source=req.source,
320
- acceptance_criteria_json=json.dumps([ac.model_dump() for ac in req.acceptance_criteria]),
321
- ))
322
-
323
- for assumption in assumptions:
324
- self.session.merge(AssumptionModel(
325
- id=assumption.id,
326
- statement=assumption.statement,
327
- confidence=assumption.confidence,
328
- impact=assumption.impact,
329
- reversible=assumption.reversible,
330
- requires_user_confirmation=assumption.requires_user_confirmation,
331
- linked_requirement_ids_json=json.dumps(assumption.linked_requirement_ids),
332
- status=assumption.status,
333
- ))
334
-
335
- for task in tasks:
336
- self.session.merge(TaskModel(
337
- id=task.id,
338
- title=task.title,
339
- description=task.description,
340
- requirement_ids_json=json.dumps(task.requirement_ids),
341
- acceptance_criterion_ids_json=json.dumps(task.acceptance_criterion_ids),
342
- planned_files_json=json.dumps([pf.model_dump() for pf in task.planned_files]),
343
- expected_tests_json=json.dumps(task.expected_tests),
344
- allowed_commands_json=json.dumps(task.allowed_commands),
345
- forbidden_changes_json=json.dumps(task.forbidden_changes),
346
- status=task.status,
347
- ))
348
-
349
- for finding in findings:
350
- self.session.merge(CritiqueFindingModel(
351
- id=finding.id,
352
- source_agent=finding.source_agent,
353
- target_plan_id=finding.target_plan_id,
354
- severity=finding.severity,
355
- finding_type=finding.finding_type,
356
- claim=finding.claim,
357
- linked_requirement_id=finding.linked_requirement_id,
358
- suggested_requirement=finding.suggested_requirement,
359
- suggested_task=finding.suggested_task,
360
- falsifiable_check=finding.falsifiable_check,
361
- status=finding.status,
362
- ))
363
-
364
- class ArtifactGraphRepository:
365
- def __init__(self, session: Session):
366
- self.session = session
367
- self.req_repo = RequirementRepository(session)
368
- self.assumption_repo = AssumptionRepository(session)
369
- self.task_repo = TaskRepository(session)
370
- self.gap_repo = GapRepository(session)
371
- self.evidence_repo = EvidenceRepository(session)
372
- self.finding_repo = CritiqueFindingRepository(session)
373
-
374
- def load_graph(self) -> ArtifactGraph:
375
- graph = ArtifactGraph()
376
- for req in self.req_repo.get_all():
377
- graph.add_requirement(req)
378
- for assumption in self.assumption_repo.get_all():
379
- graph.add_assumption(assumption)
380
- for task in self.task_repo.get_all():
381
- graph.add_task(task)
382
- for finding in self.finding_repo.get_all():
383
- graph.add_finding(finding)
384
- for gap in self.gap_repo.get_all():
385
- graph.add_gap(gap)
386
- for ev in self.evidence_repo.get_all():
387
- if isinstance(ev, CommandResult):
388
- graph.add_command_result(ev)
389
- elif isinstance(ev, DiffEvidence):
390
- graph.add_diff_evidence(ev)
391
- elif isinstance(ev, TestEvidence):
392
- graph.add_test_evidence(ev)
393
- return graph
394
-
395
-
396
- class StateRepository:
397
- def __init__(self, session: Session):
398
- self.session = session
399
-
400
- def get_state(self) -> Optional[ProjectStateModel]:
401
- statement = select(ProjectStateModel).where(ProjectStateModel.id == "singleton")
402
- return self.session.exec(statement).first()
403
-
404
- def save_state(self, state: ProjectStateModel | str, history: Optional[List[str]] = None):
405
- if isinstance(state, ProjectStateModel):
406
- state.id = "singleton"
407
- else:
408
- state = ProjectStateModel(
409
- id="singleton",
410
- current_phase=state,
411
- history_json=json.dumps(history or []),
412
- )
413
- self.session.merge(state)
414
- self.session.commit()
415
-
416
- def record_phase(self, phase: str):
417
- current = self.get_state()
418
- history = []
419
- if current:
420
- history = json.loads(current.history_json)
421
- if not history or history[-1] != phase:
422
- history.append(phase)
423
- self.save_state(phase, history)
File without changes
@@ -1,34 +0,0 @@
1
- import logging
2
- from typing import Dict
3
-
4
- logger = logging.getLogger(__name__)
5
-
6
- class CostEstimator:
7
- """Estimates LLM usage cost based on provider pricing."""
8
-
9
- # Rough estimates, update for production
10
- PRICING = {
11
- "anthropic/claude-3.5-sonnet": {"input": 0.000003, "output": 0.000015},
12
- "anthropic/claude-3-opus": {"input": 0.000015, "output": 0.000075},
13
- "anthropic/claude-sonnet-4": {"input": 0.000003, "output": 0.000015},
14
- "google/gemini-pro-1.5": {"input": 0.00000125, "output": 0.000005},
15
- "google/gemini-2.5-pro": {"input": 0.00000125, "output": 0.00001},
16
- "openai/gpt-4o": {"input": 0.000005, "output": 0.000015},
17
- "openai/o3-mini": {"input": 0.0000011, "output": 0.0000044},
18
- }
19
-
20
- # Conservative default for unknown models
21
- DEFAULT_PRICING = {"input": 0.000005, "output": 0.000015}
22
-
23
- @classmethod
24
- def estimate_cost(cls, model: str, usage: Dict[str, int]) -> float:
25
- prices = cls.PRICING.get(model)
26
- if not prices:
27
- logger.debug("Unknown model for cost estimation: %s — using default pricing", model)
28
- prices = cls.DEFAULT_PRICING
29
-
30
- prompt_tokens = usage.get("prompt_tokens", 0)
31
- completion_tokens = usage.get("completion_tokens", 0)
32
-
33
- cost = (prompt_tokens * prices["input"]) + (completion_tokens * prices["output"])
34
- return cost
@@ -1,91 +0,0 @@
1
- import json
2
- import logging
3
- import uuid
4
- from datetime import datetime, timezone
5
- from pathlib import Path
6
- from typing import Any, Dict, Iterable, Optional
7
-
8
- from pydantic import BaseModel, ConfigDict, Field
9
-
10
- logger = logging.getLogger(__name__)
11
-
12
- TRACE_SCHEMA_VERSION = "devcouncil.trace.v1"
13
-
14
-
15
- class TraceEvent(BaseModel):
16
- """Stable JSONL event shape for DevCouncil and external visualizers."""
17
-
18
- model_config = ConfigDict(populate_by_name=True, serialize_by_alias=True)
19
-
20
- schema_version: str = Field(TRACE_SCHEMA_VERSION, alias="schema")
21
- id: str = Field(default_factory=lambda: str(uuid.uuid4()))
22
- timestamp: str = Field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
23
- runtime: str = "devcouncil"
24
- type: str
25
- run_id: Optional[str] = None
26
- task_id: Optional[str] = None
27
- summary: str = ""
28
- details: Dict[str, Any] = Field(default_factory=dict)
29
-
30
- @classmethod
31
- def from_legacy(cls, raw: Dict[str, Any]) -> "TraceEvent":
32
- if raw.get("schema") == TRACE_SCHEMA_VERSION:
33
- return cls.model_validate(raw)
34
- details = raw.get("details") if isinstance(raw.get("details"), dict) else {}
35
- task_id = details.get("task_id") if isinstance(details.get("task_id"), str) else None
36
- summary = details.get("summary") if isinstance(details.get("summary"), str) else ""
37
- return cls(
38
- type=str(raw.get("type", "legacy_event")),
39
- run_id=raw.get("run_id"),
40
- task_id=task_id,
41
- summary=summary,
42
- details=details,
43
- )
44
-
45
-
46
- class TraceLogger:
47
- """Manages appending execution traces to a local log file."""
48
-
49
- def __init__(self, project_root: Path):
50
- self.log_dir = project_root / ".devcouncil" / "logs"
51
- self.trace_file = self.log_dir / "traces.jsonl"
52
- self.log_dir.mkdir(parents=True, exist_ok=True)
53
-
54
- def log_event(
55
- self,
56
- event_type: str,
57
- details: Dict[str, Any],
58
- run_id: Optional[str] = None,
59
- task_id: Optional[str] = None,
60
- summary: str = "",
61
- ) -> TraceEvent:
62
- """Append an orchestration event trace."""
63
- trace = TraceEvent(
64
- type=event_type,
65
- details=details,
66
- run_id=run_id,
67
- task_id=task_id,
68
- summary=summary,
69
- )
70
- try:
71
- with open(self.trace_file, "a", encoding="utf-8") as f:
72
- f.write(trace.model_dump_json() + "\n")
73
- except Exception as e:
74
- logger.debug("Failed to write trace event %s: %s", event_type, e)
75
- return trace
76
-
77
-
78
- def read_trace_events(project_root: Path) -> Iterable[TraceEvent]:
79
- trace_file = project_root / ".devcouncil" / "logs" / "traces.jsonl"
80
- if not trace_file.exists():
81
- return []
82
-
83
- events: list[TraceEvent] = []
84
- for line in trace_file.read_text(encoding="utf-8").splitlines():
85
- if not line.strip():
86
- continue
87
- try:
88
- events.append(TraceEvent.from_legacy(json.loads(line)))
89
- except Exception as exc:
90
- logger.debug("Skipping invalid trace line: %s", exc)
91
- return events
@@ -1,49 +0,0 @@
1
- import json
2
- from pathlib import Path
3
- from typing import Dict, Any
4
-
5
- COST_PER_1K_TOKENS = {
6
- "anthropic/claude-3-opus": {"prompt": 0.015, "completion": 0.075},
7
- "anthropic/claude-3.5-sonnet": {"prompt": 0.003, "completion": 0.015},
8
- "openai/gpt-4o": {"prompt": 0.005, "completion": 0.015},
9
- "google/gemini-pro-1.5": {"prompt": 0.00125, "completion": 0.00375},
10
- }
11
-
12
- class TelemetryTracker:
13
- def __init__(self, project_root: Path):
14
- self.log_file = project_root / ".devcouncil" / "logs" / "telemetry.json"
15
- self.stats = self._load()
16
-
17
- def _load(self) -> Dict[str, Any]:
18
- if self.log_file.exists():
19
- try:
20
- with open(self.log_file, "r") as f:
21
- return json.load(f)
22
- except Exception:
23
- pass
24
- return {"total_cost": 0.0, "total_prompt_tokens": 0, "total_completion_tokens": 0, "models": {}}
25
-
26
- def _save(self):
27
- self.log_file.parent.mkdir(parents=True, exist_ok=True)
28
- with open(self.log_file, "w") as f:
29
- json.dump(self.stats, f, indent=2)
30
-
31
- def log_usage(self, model: str, usage: Dict[str, int]):
32
- prompt_tokens = usage.get("prompt_tokens", 0)
33
- completion_tokens = usage.get("completion_tokens", 0)
34
-
35
- rates = COST_PER_1K_TOKENS.get(model, {"prompt": 0.0, "completion": 0.0})
36
- cost = (prompt_tokens / 1000.0) * rates["prompt"] + (completion_tokens / 1000.0) * rates["completion"]
37
-
38
- self.stats["total_cost"] += cost
39
- self.stats["total_prompt_tokens"] += prompt_tokens
40
- self.stats["total_completion_tokens"] += completion_tokens
41
-
42
- if model not in self.stats["models"]:
43
- self.stats["models"][model] = {"cost": 0.0, "prompt_tokens": 0, "completion_tokens": 0}
44
-
45
- self.stats["models"][model]["cost"] += cost
46
- self.stats["models"][model]["prompt_tokens"] += prompt_tokens
47
- self.stats["models"][model]["completion_tokens"] += completion_tokens
48
-
49
- self._save()
@@ -1 +0,0 @@
1
- """Live DevCouncil dashboard helpers."""