devcouncil 0.1.0 → 0.2.0

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 (190) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +197 -494
  3. package/package.json +9 -2
  4. package/pyproject.toml +62 -27
  5. package/src/devcouncil/__main__.py +4 -4
  6. package/src/devcouncil/app/__init__.py +28 -28
  7. package/src/devcouncil/app/config.py +297 -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 +163 -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/assets/__init__.py +1 -0
  22. package/src/devcouncil/assets/devcouncil-logo.svg +60 -0
  23. package/src/devcouncil/assets/devcouncil_logo_premium.png +0 -0
  24. package/src/devcouncil/cli/commands/agents.py +292 -0
  25. package/src/devcouncil/cli/commands/artifacts.py +54 -48
  26. package/src/devcouncil/cli/commands/ast.py +22 -0
  27. package/src/devcouncil/cli/commands/baseline.py +35 -32
  28. package/src/devcouncil/cli/commands/check.py +209 -0
  29. package/src/devcouncil/cli/commands/config.py +115 -54
  30. package/src/devcouncil/cli/commands/cost.py +57 -0
  31. package/src/devcouncil/cli/commands/dashboard.py +31 -0
  32. package/src/devcouncil/cli/commands/doctor.py +291 -47
  33. package/src/devcouncil/cli/commands/evidence.py +48 -0
  34. package/src/devcouncil/cli/commands/go.py +656 -0
  35. package/src/devcouncil/cli/commands/handoff.py +69 -0
  36. package/src/devcouncil/cli/commands/hook.py +209 -33
  37. package/src/devcouncil/cli/commands/init.py +204 -57
  38. package/src/devcouncil/cli/commands/integrate.py +1171 -76
  39. package/src/devcouncil/cli/commands/lsp.py +20 -0
  40. package/src/devcouncil/cli/commands/map.py +96 -22
  41. package/src/devcouncil/cli/commands/plan.py +422 -210
  42. package/src/devcouncil/cli/commands/prompt.py +48 -34
  43. package/src/devcouncil/cli/commands/repair.py +89 -69
  44. package/src/devcouncil/cli/commands/report.py +120 -54
  45. package/src/devcouncil/cli/commands/reset_demo_state.py +33 -28
  46. package/src/devcouncil/cli/commands/rollback.py +55 -54
  47. package/src/devcouncil/cli/commands/run.py +285 -220
  48. package/src/devcouncil/cli/commands/runs.py +223 -0
  49. package/src/devcouncil/cli/commands/scaffold.py +32 -0
  50. package/src/devcouncil/cli/commands/semantic.py +47 -0
  51. package/src/devcouncil/cli/commands/setup.py +300 -20
  52. package/src/devcouncil/cli/commands/shell.py +73 -0
  53. package/src/devcouncil/cli/commands/show.py +76 -57
  54. package/src/devcouncil/cli/commands/skills.py +88 -0
  55. package/src/devcouncil/cli/commands/status.py +141 -105
  56. package/src/devcouncil/cli/commands/tasks.py +55 -41
  57. package/src/devcouncil/cli/commands/trace.py +49 -4
  58. package/src/devcouncil/cli/commands/verify.py +293 -128
  59. package/src/devcouncil/cli/commands/version.py +20 -20
  60. package/src/devcouncil/cli/commands/watch.py +574 -0
  61. package/src/devcouncil/cli/commands/watch_fs.py +40 -0
  62. package/src/devcouncil/cli/main.py +92 -25
  63. package/src/devcouncil/council/prompts/arbiter.md +19 -19
  64. package/src/devcouncil/council/prompts/critic_a.md +10 -10
  65. package/src/devcouncil/council/prompts/critic_b.md +10 -10
  66. package/src/devcouncil/council/prompts/implementation_reviewer.md +16 -16
  67. package/src/devcouncil/council/prompts/planner_a.md +16 -16
  68. package/src/devcouncil/council/prompts/planner_b.md +16 -16
  69. package/src/devcouncil/council/prompts/rebuttal.md +10 -10
  70. package/src/devcouncil/council/prompts/spec_writer.md +12 -12
  71. package/src/devcouncil/domain/assumption.py +17 -17
  72. package/src/devcouncil/domain/critique.py +32 -32
  73. package/src/devcouncil/domain/evidence.py +47 -27
  74. package/src/devcouncil/domain/gap.py +52 -26
  75. package/src/devcouncil/domain/requirement.py +22 -22
  76. package/src/devcouncil/domain/task.py +55 -26
  77. package/src/devcouncil/execution/__init__.py +1 -1
  78. package/src/devcouncil/execution/checkpoints.py +246 -0
  79. package/src/devcouncil/execution/context_builder.py +54 -54
  80. package/src/devcouncil/execution/executor.py +15 -15
  81. package/src/devcouncil/execution/fs_watcher.py +180 -0
  82. package/src/devcouncil/execution/handoff.py +102 -0
  83. package/src/devcouncil/execution/hook_policy.py +186 -77
  84. package/src/devcouncil/execution/patch.py +77 -28
  85. package/src/devcouncil/execution/permissions.py +52 -59
  86. package/src/devcouncil/execution/policy_engine.py +343 -0
  87. package/src/devcouncil/execution/prompt_builder.py +650 -38
  88. package/src/devcouncil/execution/shell_session.py +225 -0
  89. package/src/devcouncil/execution/task_runner.py +68 -64
  90. package/src/devcouncil/executors/__init__.py +1 -1
  91. package/src/devcouncil/executors/agent_registry.py +575 -0
  92. package/src/devcouncil/executors/coding_cli.py +736 -0
  93. package/src/devcouncil/executors/mini_swe.py +63 -63
  94. package/src/devcouncil/executors/native/agent.py +186 -85
  95. package/src/devcouncil/executors/openhands.py +56 -56
  96. package/src/devcouncil/gating/__init__.py +1 -1
  97. package/src/devcouncil/gating/checks/clean_git.py +52 -45
  98. package/src/devcouncil/gating/checks/planned_files_check.py +32 -32
  99. package/src/devcouncil/gating/checks/requirement_coverage.py +26 -26
  100. package/src/devcouncil/gating/checks/secret_scan_check.py +53 -34
  101. package/src/devcouncil/gating/policy.py +315 -167
  102. package/src/devcouncil/hardware.py +184 -0
  103. package/src/devcouncil/indexing/__init__.py +1 -1
  104. package/src/devcouncil/indexing/ast_matcher.py +168 -0
  105. package/src/devcouncil/indexing/graph_index.py +48 -48
  106. package/src/devcouncil/indexing/lsp.py +161 -0
  107. package/src/devcouncil/indexing/repo_mapper.py +1455 -204
  108. package/src/devcouncil/indexing/semantic_index.py +205 -0
  109. package/src/devcouncil/integrations/actions.py +146 -0
  110. package/src/devcouncil/integrations/check.py +423 -0
  111. package/src/devcouncil/integrations/github.py +35 -35
  112. package/src/devcouncil/integrations/github_intent.py +142 -0
  113. package/src/devcouncil/integrations/gitnexus.py +62 -27
  114. package/src/devcouncil/integrations/graphify.py +34 -34
  115. package/src/devcouncil/integrations/mcp/server.py +2072 -96
  116. package/src/devcouncil/integrations/opencode_devcouncil_plugin.mjs +24 -0
  117. package/src/devcouncil/integrations/pr_comments.py +62 -0
  118. package/src/devcouncil/live/__init__.py +2 -0
  119. package/src/devcouncil/live/cards.py +349 -0
  120. package/src/devcouncil/live/models.py +63 -0
  121. package/src/devcouncil/live/repair_prompt.py +83 -0
  122. package/src/devcouncil/live/reviewer.py +70 -0
  123. package/src/devcouncil/live/signals.py +135 -0
  124. package/src/devcouncil/live/summary.py +34 -0
  125. package/src/devcouncil/live/tasks.py +18 -0
  126. package/src/devcouncil/live/transcripts.py +141 -0
  127. package/src/devcouncil/llm/__init__.py +1 -1
  128. package/src/devcouncil/llm/cache.py +42 -38
  129. package/src/devcouncil/llm/model_defaults.yaml +44 -0
  130. package/src/devcouncil/llm/provider.py +627 -125
  131. package/src/devcouncil/llm/router.py +303 -118
  132. package/src/devcouncil/optimization/__init__.py +1 -0
  133. package/src/devcouncil/optimization/gepa_agent.py +318 -0
  134. package/src/devcouncil/planning/__init__.py +1 -1
  135. package/src/devcouncil/planning/arbiter_service.py +57 -57
  136. package/src/devcouncil/planning/correction_manifest.py +303 -0
  137. package/src/devcouncil/planning/critique_service.py +71 -66
  138. package/src/devcouncil/planning/plan_service.py +60 -46
  139. package/src/devcouncil/planning/prompt_enhancer_service.py +167 -0
  140. package/src/devcouncil/planning/repair_service.py +39 -39
  141. package/src/devcouncil/planning/spec_service.py +70 -44
  142. package/src/devcouncil/repo/ci_scaffold.py +157 -0
  143. package/src/devcouncil/repo/gitignore.py +123 -0
  144. package/src/devcouncil/repo/sca.py +374 -0
  145. package/src/devcouncil/reporting/github_check.py +32 -32
  146. package/src/devcouncil/reporting/json_report.py +30 -17
  147. package/src/devcouncil/reporting/markdown_report.py +83 -46
  148. package/src/devcouncil/reporting/report_builder.py +14 -14
  149. package/src/devcouncil/skills/__init__.py +19 -0
  150. package/src/devcouncil/skills/library/README.md +46 -0
  151. package/src/devcouncil/skills/library/ai-training.md +50 -0
  152. package/src/devcouncil/skills/library/android.md +50 -0
  153. package/src/devcouncil/skills/library/backend.md +52 -0
  154. package/src/devcouncil/skills/library/core-engineering.md +95 -0
  155. package/src/devcouncil/skills/library/data-engineering.md +47 -0
  156. package/src/devcouncil/skills/library/desktop.md +46 -0
  157. package/src/devcouncil/skills/library/devops.md +48 -0
  158. package/src/devcouncil/skills/library/game-dev.md +46 -0
  159. package/src/devcouncil/skills/library/ios.md +48 -0
  160. package/src/devcouncil/skills/library/mobile-cross-platform.md +46 -0
  161. package/src/devcouncil/skills/library/security.md +48 -0
  162. package/src/devcouncil/skills/library/systems.md +48 -0
  163. package/src/devcouncil/skills/library/web.md +47 -0
  164. package/src/devcouncil/skills/library/windows.md +47 -0
  165. package/src/devcouncil/skills/registry.py +330 -0
  166. package/src/devcouncil/storage/db.py +147 -66
  167. package/src/devcouncil/storage/models.py +204 -83
  168. package/src/devcouncil/storage/native.py +557 -0
  169. package/src/devcouncil/storage/repositories.py +388 -249
  170. package/src/devcouncil/telemetry/cost.py +140 -34
  171. package/src/devcouncil/telemetry/model_pricing.yaml +48 -0
  172. package/src/devcouncil/telemetry/pricing.py +28 -0
  173. package/src/devcouncil/telemetry/traces.py +62 -7
  174. package/src/devcouncil/telemetry/tracker.py +52 -49
  175. package/src/devcouncil/ui/__init__.py +1 -0
  176. package/src/devcouncil/ui/dashboard.py +423 -0
  177. package/src/devcouncil/utils/__init__.py +1 -1
  178. package/src/devcouncil/utils/redaction.py +147 -141
  179. package/src/devcouncil/utils/subprocess_env.py +69 -0
  180. package/src/devcouncil/verification/__init__.py +1 -1
  181. package/src/devcouncil/verification/acceptance_compiler.py +125 -0
  182. package/src/devcouncil/verification/ad_hoc_check.py +129 -0
  183. package/src/devcouncil/verification/diff_coverage.py +353 -0
  184. package/src/devcouncil/verification/implementation_reviewer.py +55 -55
  185. package/src/devcouncil/verification/next_actions.py +189 -0
  186. package/src/devcouncil/verification/sandbox.py +178 -0
  187. package/src/devcouncil/verification/test_resolver.py +91 -0
  188. package/src/devcouncil/verification/verifier.py +1342 -307
  189. package/uv.lock +205 -64
  190. package/src/devcouncil/indexing/symbol_index.py +0 -0
@@ -1,66 +1,147 @@
1
- from contextlib import contextmanager
2
-
3
- from sqlmodel import SQLModel, create_engine, Session
4
- from sqlalchemy.exc import OperationalError
5
- from pathlib import Path
6
- from typing import Optional
7
-
8
- from devcouncil.storage.models import SchemaVersionModel
9
-
10
-
11
- SCHEMA_VERSION = 1
12
-
13
-
14
- class Database:
15
- def __init__(self, db_path: Path):
16
- self.db_path = db_path
17
- self.engine = create_engine(f"sqlite:///{db_path}")
18
-
19
- def create_db_and_tables(self):
20
- self._create_tables()
21
- self.ensure_schema_version()
22
-
23
- def ensure_schema_version(self):
24
- self._create_tables()
25
- with Session(self.engine) as session:
26
- current = session.get(SchemaVersionModel, "singleton")
27
- if current is None:
28
- session.add(SchemaVersionModel(id="singleton", version=SCHEMA_VERSION))
29
- session.commit()
30
- return
31
- if current.version != SCHEMA_VERSION:
32
- raise RuntimeError(
33
- f"Unsupported DevCouncil schema version {current.version}; "
34
- f"expected {SCHEMA_VERSION}."
35
- )
36
-
37
- def _create_tables(self):
38
- try:
39
- SQLModel.metadata.create_all(self.engine)
40
- except OperationalError as exc:
41
- if "already exists" not in str(exc):
42
- raise
43
-
44
- @contextmanager
45
- def get_session(self):
46
- """Yield a session with automatic commit/rollback/close."""
47
- session = Session(self.engine)
48
- try:
49
- yield session
50
- session.commit()
51
- except Exception:
52
- session.rollback()
53
- raise
54
- finally:
55
- session.close()
56
-
57
-
58
- def get_db(project_root: Path = Path(".")) -> Optional[Database]:
59
- dev_dir = project_root / ".devcouncil"
60
- if not dev_dir.exists():
61
- return None
62
-
63
- db_path = dev_dir / "state.sqlite"
64
- db = Database(db_path)
65
- db.ensure_schema_version()
66
- return db
1
+ from collections import defaultdict
2
+ from contextlib import contextmanager
3
+ from datetime import datetime, timezone
4
+
5
+ from sqlmodel import SQLModel, create_engine, Session
6
+ from sqlalchemy import inspect, text
7
+ from sqlalchemy.exc import OperationalError
8
+ from pathlib import Path
9
+ from typing import Optional
10
+
11
+ from devcouncil.storage.models import SchemaVersionModel
12
+
13
+
14
+ # v3: per-task indexes on the task/audit tables (task_id, lease status).
15
+ # v4: machine-routable gap columns (file, line, suggested_command,
16
+ # acceptance_criterion_id) so the repair contract survives a reload.
17
+ # v5: partial unique index enforcing one ACTIVE lease per task (single-writer).
18
+ SCHEMA_VERSION = 5
19
+
20
+
21
+ class Database:
22
+ def __init__(self, db_path: Path):
23
+ self.db_path = db_path
24
+ self.engine = create_engine(f"sqlite:///{db_path}")
25
+
26
+ def create_db_and_tables(self):
27
+ self._create_tables()
28
+ self.ensure_schema_version()
29
+
30
+ def ensure_schema_version(self):
31
+ self._create_tables()
32
+ with Session(self.engine) as session:
33
+ current = session.get(SchemaVersionModel, "singleton")
34
+ if current is None:
35
+ session.add(SchemaVersionModel(id="singleton", version=SCHEMA_VERSION))
36
+ session.commit()
37
+ return
38
+ if current.version > SCHEMA_VERSION:
39
+ raise RuntimeError(
40
+ f"Unsupported DevCouncil schema version {current.version}; "
41
+ f"expected {SCHEMA_VERSION}."
42
+ )
43
+ if current.version < SCHEMA_VERSION:
44
+ self._create_tables()
45
+ current.version = SCHEMA_VERSION
46
+ session.add(current)
47
+ session.commit()
48
+ return
49
+
50
+ def _create_tables(self):
51
+ try:
52
+ SQLModel.metadata.create_all(self.engine)
53
+ except OperationalError as exc:
54
+ if "already exists" not in str(exc):
55
+ raise
56
+ self._create_missing_columns()
57
+ self._dedup_active_leases()
58
+ self._create_missing_indexes()
59
+
60
+ def _dedup_active_leases(self):
61
+ # The partial unique index (ux_task_leases_active) can only be created if no task
62
+ # already has two ACTIVE leases. Older databases predate the constraint, so
63
+ # collapse any duplicates first — keep the newest active lease per task, mark the
64
+ # rest stale — making index creation succeed and restoring single-writer state.
65
+ inspector = inspect(self.engine)
66
+ if "task_leases" not in inspector.get_table_names():
67
+ return
68
+ with self.engine.begin() as conn:
69
+ rows = conn.execute(
70
+ text("SELECT id, task_id, created_at FROM task_leases WHERE status = 'active'")
71
+ ).fetchall()
72
+ by_task = defaultdict(list)
73
+ for row in rows:
74
+ by_task[row.task_id].append(((row.created_at or ""), row.id))
75
+ stale_ids = []
76
+ for items in by_task.values():
77
+ if len(items) <= 1:
78
+ continue
79
+ items.sort() # ascending by (created_at, id); keep the last = newest
80
+ stale_ids.extend(item[1] for item in items[:-1])
81
+ if stale_ids:
82
+ now = datetime.now(timezone.utc).isoformat()
83
+ for lease_id in stale_ids:
84
+ conn.execute(
85
+ text("UPDATE task_leases SET status = 'stale', released_at = :ts WHERE id = :id"),
86
+ {"ts": now, "id": lease_id},
87
+ )
88
+
89
+ def _create_missing_columns(self):
90
+ # create_all never alters an existing table, so columns added to a model
91
+ # later (e.g. the v4 gap routing columns) are missing on databases created by
92
+ # an older version — and a SELECT of the model would then fail. Add any
93
+ # missing column as a nullable ADD COLUMN (the only shape SQLite can add in
94
+ # place without a table rewrite); non-nullable additions are intentionally
95
+ # skipped because existing rows could not satisfy them.
96
+ inspector = inspect(self.engine)
97
+ existing_tables = set(inspector.get_table_names())
98
+ for table in SQLModel.metadata.sorted_tables:
99
+ if table.name not in existing_tables:
100
+ continue
101
+ existing_cols = {col["name"] for col in inspector.get_columns(table.name)}
102
+ for column in table.columns:
103
+ if column.name in existing_cols or not column.nullable:
104
+ continue
105
+ ddl_type = column.type.compile(self.engine.dialect)
106
+ stmt = text(f'ALTER TABLE "{table.name}" ADD COLUMN "{column.name}" {ddl_type}')
107
+ try:
108
+ with self.engine.begin() as conn:
109
+ conn.execute(stmt)
110
+ except OperationalError as exc:
111
+ if "duplicate column name" not in str(exc).lower():
112
+ raise
113
+
114
+ def _create_missing_indexes(self):
115
+ # create_all skips tables that already exist, so indexes added to the
116
+ # model definitions later never materialize on existing databases.
117
+ for table in SQLModel.metadata.sorted_tables:
118
+ for index in table.indexes:
119
+ try:
120
+ index.create(self.engine, checkfirst=True)
121
+ except OperationalError as exc:
122
+ if "already exists" not in str(exc):
123
+ raise
124
+
125
+ @contextmanager
126
+ def get_session(self):
127
+ """Yield a session with automatic commit/rollback/close."""
128
+ session = Session(self.engine)
129
+ try:
130
+ yield session
131
+ session.commit()
132
+ except Exception:
133
+ session.rollback()
134
+ raise
135
+ finally:
136
+ session.close()
137
+
138
+
139
+ def get_db(project_root: Path = Path(".")) -> Optional[Database]:
140
+ dev_dir = project_root / ".devcouncil"
141
+ if not dev_dir.exists():
142
+ return None
143
+
144
+ db_path = dev_dir / "state.sqlite"
145
+ db = Database(db_path)
146
+ db.ensure_schema_version()
147
+ return db
@@ -1,83 +1,204 @@
1
- from sqlmodel import SQLModel, Field
2
- from typing import Optional
3
-
4
-
5
- class SchemaVersionModel(SQLModel, table=True):
6
- __tablename__ = "schema_version"
7
- id: str = Field(primary_key=True, default="singleton")
8
- version: int
9
-
10
- class RequirementModel(SQLModel, table=True):
11
- __tablename__ = "requirements"
12
- id: str = Field(primary_key=True)
13
- title: str
14
- description: str
15
- priority: str
16
- source: str
17
- # Store complex lists as JSON strings for simplicity in SQLite
18
- acceptance_criteria_json: str = Field(default="[]")
19
-
20
- class AssumptionModel(SQLModel, table=True):
21
- __tablename__ = "assumptions"
22
- id: str = Field(primary_key=True)
23
- statement: str
24
- confidence: str
25
- impact: str
26
- reversible: bool
27
- requires_user_confirmation: bool
28
- linked_requirement_ids_json: str = Field(default="[]")
29
- status: str = "open"
30
-
31
- class TaskModel(SQLModel, table=True):
32
- __tablename__ = "tasks"
33
- id: str = Field(primary_key=True)
34
- title: str
35
- description: str
36
- requirement_ids_json: str = Field(default="[]")
37
- acceptance_criterion_ids_json: str = Field(default="[]")
38
- planned_files_json: str = Field(default="[]")
39
- expected_tests_json: str = Field(default="[]")
40
- allowed_commands_json: str = Field(default="[]")
41
- forbidden_changes_json: str = Field(default="[]")
42
- status: str = "planned"
43
-
44
- class EvidenceModel(SQLModel, table=True):
45
- __tablename__ = "evidence"
46
- id: Optional[int] = Field(default=None, primary_key=True)
47
- type: str # "command", "diff", "test"
48
- task_id: Optional[str] = None
49
- requirement_id: Optional[str] = None
50
- acceptance_criterion_id: Optional[str] = None
51
- data_json: str
52
-
53
- class GapModel(SQLModel, table=True):
54
- __tablename__ = "gaps"
55
- id: str = Field(primary_key=True)
56
- severity: str
57
- gap_type: str
58
- requirement_id: Optional[str] = None
59
- task_id: Optional[str] = None
60
- description: str
61
- evidence_json: str = Field(default="[]")
62
- recommended_fix: str
63
- blocking: bool
64
-
65
- class CritiqueFindingModel(SQLModel, table=True):
66
- __tablename__ = "critique_findings"
67
- id: str = Field(primary_key=True)
68
- source_agent: str
69
- target_plan_id: str
70
- severity: str
71
- finding_type: str
72
- claim: str
73
- linked_requirement_id: Optional[str] = None
74
- suggested_requirement: Optional[str] = None
75
- suggested_task: Optional[str] = None
76
- falsifiable_check: str
77
- status: str = "open"
78
-
79
- class ProjectStateModel(SQLModel, table=True):
80
- __tablename__ = "project_state"
81
- id: str = Field(primary_key=True, default="singleton")
82
- current_phase: str
83
- history_json: str = Field(default="[]")
1
+ from sqlmodel import SQLModel, Field
2
+ from sqlalchemy import Index, text
3
+ from typing import Optional
4
+
5
+
6
+ class SchemaVersionModel(SQLModel, table=True):
7
+ __tablename__ = "schema_version"
8
+ id: str = Field(primary_key=True, default="singleton")
9
+ version: int
10
+
11
+ class RequirementModel(SQLModel, table=True):
12
+ __tablename__ = "requirements"
13
+ id: str = Field(primary_key=True)
14
+ title: str
15
+ description: str
16
+ priority: str
17
+ source: str
18
+ # Store complex lists as JSON strings for simplicity in SQLite
19
+ acceptance_criteria_json: str = Field(default="[]")
20
+
21
+ class AssumptionModel(SQLModel, table=True):
22
+ __tablename__ = "assumptions"
23
+ id: str = Field(primary_key=True)
24
+ statement: str
25
+ confidence: str
26
+ impact: str
27
+ reversible: bool
28
+ requires_user_confirmation: bool
29
+ linked_requirement_ids_json: str = Field(default="[]")
30
+ status: str = "open"
31
+
32
+ class TaskModel(SQLModel, table=True):
33
+ __tablename__ = "tasks"
34
+ id: str = Field(primary_key=True)
35
+ title: str
36
+ description: str
37
+ requirement_ids_json: str = Field(default="[]")
38
+ acceptance_criterion_ids_json: str = Field(default="[]")
39
+ planned_files_json: str = Field(default="[]")
40
+ expected_tests_json: str = Field(default="[]")
41
+ allowed_commands_json: str = Field(default="[]")
42
+ forbidden_changes_json: str = Field(default="[]")
43
+ status: str = "planned"
44
+
45
+ class EvidenceModel(SQLModel, table=True):
46
+ __tablename__ = "evidence"
47
+ id: Optional[int] = Field(default=None, primary_key=True)
48
+ type: str # "command", "diff", "test"
49
+ task_id: Optional[str] = None
50
+ requirement_id: Optional[str] = None
51
+ acceptance_criterion_id: Optional[str] = None
52
+ data_json: str
53
+
54
+ class GapModel(SQLModel, table=True):
55
+ __tablename__ = "gaps"
56
+ id: str = Field(primary_key=True)
57
+ severity: str
58
+ gap_type: str
59
+ requirement_id: Optional[str] = None
60
+ task_id: Optional[str] = None
61
+ description: str
62
+ evidence_json: str = Field(default="[]")
63
+ recommended_fix: str
64
+ blocking: bool
65
+ # Machine-routable repair hints. Persisted (v4) so the typed next-actions
66
+ # contract survives a reload — a reconnecting/crashed agent can read outstanding
67
+ # gaps without the heuristic reconstruction the loader used to fall back to.
68
+ file: Optional[str] = None
69
+ line: Optional[int] = None
70
+ suggested_command: Optional[str] = None
71
+ acceptance_criterion_id: Optional[str] = None
72
+
73
+ class CritiqueFindingModel(SQLModel, table=True):
74
+ __tablename__ = "critique_findings"
75
+ id: str = Field(primary_key=True)
76
+ source_agent: str
77
+ target_plan_id: str
78
+ severity: str
79
+ finding_type: str
80
+ claim: str
81
+ linked_requirement_id: Optional[str] = None
82
+ suggested_requirement: Optional[str] = None
83
+ suggested_task: Optional[str] = None
84
+ falsifiable_check: str
85
+ status: str = "open"
86
+
87
+ class ProjectStateModel(SQLModel, table=True):
88
+ __tablename__ = "project_state"
89
+ id: str = Field(primary_key=True, default="singleton")
90
+ current_phase: str
91
+ history_json: str = Field(default="[]")
92
+
93
+
94
+ class TaskLeaseModel(SQLModel, table=True):
95
+ __tablename__ = "task_leases"
96
+ # Enforce the single-writer guarantee at the DB level: at most one ACTIVE lease per
97
+ # task. A partial unique index turns a concurrent double-checkout race into an
98
+ # IntegrityError (surfaced as lease_conflict) instead of two live leases.
99
+ __table_args__ = (
100
+ Index(
101
+ "ux_task_leases_active",
102
+ "task_id",
103
+ unique=True,
104
+ sqlite_where=text("status = 'active'"),
105
+ ),
106
+ )
107
+ id: str = Field(primary_key=True)
108
+ task_id: str = Field(index=True)
109
+ owner: str
110
+ agent: Optional[str] = None
111
+ client_id: Optional[str] = None
112
+ run_id: Optional[str] = None
113
+ branch: Optional[str] = None
114
+ lease_token: str
115
+ status: str = Field(default="active", index=True)
116
+ created_at: str
117
+ expires_at: Optional[str] = None
118
+ released_at: Optional[str] = None
119
+
120
+
121
+ class ShellSessionModel(SQLModel, table=True):
122
+ __tablename__ = "shell_sessions"
123
+ id: str = Field(primary_key=True)
124
+ task_id: str = Field(index=True)
125
+ lease_id: Optional[str] = None
126
+ shell: str
127
+ cwd: str
128
+ status: str
129
+ started_at: str
130
+ ended_at: Optional[str] = None
131
+
132
+
133
+ class ShellCommandEventModel(SQLModel, table=True):
134
+ __tablename__ = "shell_command_events"
135
+ id: Optional[int] = Field(default=None, primary_key=True)
136
+ task_id: str = Field(index=True)
137
+ session_id: Optional[str] = None
138
+ command: str
139
+ status: str
140
+ exit_code: Optional[int] = None
141
+ reason: str = ""
142
+ stdout_path: str = ""
143
+ stderr_path: str = ""
144
+ created_at: str
145
+
146
+
147
+ class FileChangeEventModel(SQLModel, table=True):
148
+ __tablename__ = "file_change_events"
149
+ id: Optional[int] = Field(default=None, primary_key=True)
150
+ task_id: Optional[str] = Field(default=None, index=True)
151
+ lease_id: Optional[str] = None
152
+ session_id: Optional[str] = None
153
+ path: str
154
+ operation: str
155
+ allowed: bool
156
+ reason: str = ""
157
+ created_at: str
158
+
159
+
160
+ class SemanticDiffModel(SQLModel, table=True):
161
+ __tablename__ = "semantic_diffs"
162
+ id: Optional[int] = Field(default=None, primary_key=True)
163
+ task_id: str = Field(index=True)
164
+ before_snapshot_path: str
165
+ after_snapshot_path: str
166
+ classifications_json: str = "[]"
167
+ summary: str = ""
168
+ created_at: str
169
+
170
+
171
+ class AgentHandoffModel(SQLModel, table=True):
172
+ __tablename__ = "agent_handoffs"
173
+ id: str = Field(primary_key=True)
174
+ task_id: str = Field(index=True)
175
+ from_agent: str
176
+ to_agent: str
177
+ run_id: str
178
+ manifest_path: str
179
+ status: str
180
+ created_at: str
181
+
182
+
183
+ class CorrectionManifestModel(SQLModel, table=True):
184
+ __tablename__ = "correction_manifests"
185
+ id: str = Field(primary_key=True)
186
+ task_id: str = Field(index=True)
187
+ run_id: Optional[str] = None
188
+ manifest_path: str
189
+ retry_budget: int = 3
190
+ attempt: int = 0
191
+ status: str
192
+ created_at: str
193
+
194
+
195
+ class VerificationRunModel(SQLModel, table=True):
196
+ __tablename__ = "verification_runs"
197
+ id: str = Field(primary_key=True)
198
+ task_id: str = Field(index=True)
199
+ sandbox: str
200
+ environment_json: str = "{}"
201
+ commands_json: str = "[]"
202
+ status: str
203
+ started_at: str
204
+ finished_at: Optional[str] = None