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,6 +1,6 @@
1
- from sqlmodel import Session, select, delete
2
- from typing import List, Optional, Any
3
- import json
1
+ from sqlmodel import Session, select, delete
2
+ from typing import List, Optional, Any
3
+ import json
4
4
  from devcouncil.storage.models import (
5
5
  RequirementModel,
6
6
  AssumptionModel,
@@ -17,36 +17,36 @@ from devcouncil.domain.gap import Gap
17
17
  from devcouncil.domain.evidence import CommandResult, DiffEvidence, TestEvidence
18
18
  from devcouncil.domain.critique import CritiqueFinding
19
19
  from devcouncil.artifacts.graph import ArtifactGraph
20
-
20
+
21
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
- )
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
50
  self.session.merge(model)
51
51
  self.session.commit()
52
52
 
@@ -85,104 +85,104 @@ class AssumptionRepository:
85
85
  )
86
86
  self.session.merge(model)
87
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
-
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
186
  def delete_for_task(self, task_id: str):
187
187
  self.session.exec(delete(GapModel).where(GapModel.task_id == task_id))
188
188
  self.session.commit()
@@ -190,54 +190,54 @@ class GapRepository:
190
190
  def delete_plan_gaps(self):
191
191
  self.session.exec(delete(GapModel).where(GapModel.id.like("GAP-PLAN-%")))
192
192
  self.session.commit()
193
-
193
+
194
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
-
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
241
  def delete_for_task(self, task_id: str):
242
242
  self.session.exec(delete(EvidenceModel).where(EvidenceModel.task_id == task_id))
243
243
  self.session.commit()
@@ -283,19 +283,96 @@ class CritiqueFindingRepository:
283
283
  )
284
284
  self.session.merge(model)
285
285
  self.session.commit()
286
-
287
- class ArtifactGraphRepository:
288
- def __init__(self, session: Session):
289
- self.session = session
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
290
367
  self.req_repo = RequirementRepository(session)
291
368
  self.assumption_repo = AssumptionRepository(session)
292
369
  self.task_repo = TaskRepository(session)
293
370
  self.gap_repo = GapRepository(session)
294
371
  self.evidence_repo = EvidenceRepository(session)
295
372
  self.finding_repo = CritiqueFindingRepository(session)
296
-
297
- def load_graph(self) -> ArtifactGraph:
298
- graph = ArtifactGraph()
373
+
374
+ def load_graph(self) -> ArtifactGraph:
375
+ graph = ArtifactGraph()
299
376
  for req in self.req_repo.get_all():
300
377
  graph.add_requirement(req)
301
378
  for assumption in self.assumption_repo.get_all():
@@ -306,41 +383,41 @@ class ArtifactGraphRepository:
306
383
  graph.add_finding(finding)
307
384
  for gap in self.gap_repo.get_all():
308
385
  graph.add_gap(gap)
309
- for ev in self.evidence_repo.get_all():
310
- if isinstance(ev, CommandResult):
311
- graph.add_command_result(ev)
312
- elif isinstance(ev, DiffEvidence):
313
- graph.add_diff_evidence(ev)
314
- elif isinstance(ev, TestEvidence):
315
- graph.add_test_evidence(ev)
316
- return graph
317
-
318
-
319
- class StateRepository:
320
- def __init__(self, session: Session):
321
- self.session = session
322
-
323
- def get_state(self) -> Optional[ProjectStateModel]:
324
- statement = select(ProjectStateModel).where(ProjectStateModel.id == "singleton")
325
- return self.session.exec(statement).first()
326
-
327
- def save_state(self, state: ProjectStateModel | str, history: Optional[List[str]] = None):
328
- if isinstance(state, ProjectStateModel):
329
- state.id = "singleton"
330
- else:
331
- state = ProjectStateModel(
332
- id="singleton",
333
- current_phase=state,
334
- history_json=json.dumps(history or []),
335
- )
336
- self.session.merge(state)
337
- self.session.commit()
338
-
339
- def record_phase(self, phase: str):
340
- current = self.get_state()
341
- history = []
342
- if current:
343
- history = json.loads(current.history_json)
344
- if not history or history[-1] != phase:
345
- history.append(phase)
346
- self.save_state(phase, history)
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)
@@ -1,34 +1,34 @@
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
+ 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