shikiru-cli 0.2.0__py3-none-any.whl

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 (319) hide show
  1. shikiru/__init__.py +1 -0
  2. shikiru/__main__.py +8 -0
  3. shikiru/agents/__init__.py +3 -0
  4. shikiru/agents/contracts.py +590 -0
  5. shikiru/agents/registry.py +87 -0
  6. shikiru/agents/sdk/__init__.py +17 -0
  7. shikiru/agents/sdk/reset_time.py +137 -0
  8. shikiru/agents/sdk/turn.py +335 -0
  9. shikiru/agents/settings.py +151 -0
  10. shikiru/agents/testing/__init__.py +26 -0
  11. shikiru/agents/testing/contract_suite.py +653 -0
  12. shikiru/agents/testing/resume.py +15 -0
  13. shikiru/bootstrap.py +1277 -0
  14. shikiru/cli/__init__.py +1 -0
  15. shikiru/cli/app.py +770 -0
  16. shikiru/cli/context.py +63 -0
  17. shikiru/cli/render.py +1475 -0
  18. shikiru/cli/report.py +156 -0
  19. shikiru/cli/session_prompt.py +63 -0
  20. shikiru/cli/view.py +131 -0
  21. shikiru/clock.py +41 -0
  22. shikiru/config/__init__.py +8 -0
  23. shikiru/config/defaults.toml +131 -0
  24. shikiru/config/loader.py +178 -0
  25. shikiru/config/settings.py +392 -0
  26. shikiru/dashboard/__init__.py +3 -0
  27. shikiru/dashboard/actions_list.py +799 -0
  28. shikiru/dashboard/activity.py +150 -0
  29. shikiru/dashboard/app.py +1765 -0
  30. shikiru/dashboard/blocker_evidence.py +122 -0
  31. shikiru/dashboard/blocker_rows.py +84 -0
  32. shikiru/dashboard/briefing_view.py +871 -0
  33. shikiru/dashboard/contract.py +1086 -0
  34. shikiru/dashboard/covered.py +32 -0
  35. shikiru/dashboard/detail.py +1209 -0
  36. shikiru/dashboard/detail_fold.py +244 -0
  37. shikiru/dashboard/detail_hint.py +68 -0
  38. shikiru/dashboard/details_list_view.py +150 -0
  39. shikiru/dashboard/diagnostics.py +80 -0
  40. shikiru/dashboard/diagnostics_sections.py +186 -0
  41. shikiru/dashboard/evidence_table.py +123 -0
  42. shikiru/dashboard/extra_details.py +214 -0
  43. shikiru/dashboard/fault_guidance.py +216 -0
  44. shikiru/dashboard/fold_bodies.py +276 -0
  45. shikiru/dashboard/help_keys.py +166 -0
  46. shikiru/dashboard/help_screen.py +204 -0
  47. shikiru/dashboard/interaction.py +17 -0
  48. shikiru/dashboard/ladder_editor.py +544 -0
  49. shikiru/dashboard/live_settings.py +62 -0
  50. shikiru/dashboard/motion.py +368 -0
  51. shikiru/dashboard/orchestration_fields.py +201 -0
  52. shikiru/dashboard/palette.py +1606 -0
  53. shikiru/dashboard/palette_app.py +89 -0
  54. shikiru/dashboard/palette_drafts.py +33 -0
  55. shikiru/dashboard/resolution.py +573 -0
  56. shikiru/dashboard/resolution_view.py +302 -0
  57. shikiru/dashboard/row_list.py +256 -0
  58. shikiru/dashboard/section_rows.py +173 -0
  59. shikiru/dashboard/services.py +665 -0
  60. shikiru/dashboard/session_history.py +402 -0
  61. shikiru/dashboard/shikiru_init_ascii_art.txt +40 -0
  62. shikiru/dashboard/task_picker.py +358 -0
  63. shikiru/dashboard/tick.py +59 -0
  64. shikiru/dashboard/timeline_filter.py +129 -0
  65. shikiru/dashboard/timeline_section.py +385 -0
  66. shikiru/dashboard/usage_body.py +269 -0
  67. shikiru/dashboard/views.py +2314 -0
  68. shikiru/dashboard/wizard.py +1389 -0
  69. shikiru/domain/__init__.py +1 -0
  70. shikiru/domain/acceptance.py +130 -0
  71. shikiru/domain/activity.py +28 -0
  72. shikiru/domain/attempts.py +333 -0
  73. shikiru/domain/briefing.py +257 -0
  74. shikiru/domain/commands.py +219 -0
  75. shikiru/domain/decisions.py +231 -0
  76. shikiru/domain/docker.py +262 -0
  77. shikiru/domain/engine_uptime.py +87 -0
  78. shikiru/domain/escalation.py +194 -0
  79. shikiru/domain/escalation_evidence.py +186 -0
  80. shikiru/domain/escalation_panel.py +144 -0
  81. shikiru/domain/events.py +71 -0
  82. shikiru/domain/evidence_export.py +89 -0
  83. shikiru/domain/faults.py +86 -0
  84. shikiru/domain/feedback.py +180 -0
  85. shikiru/domain/feedback_rounds.py +172 -0
  86. shikiru/domain/fencing.py +31 -0
  87. shikiru/domain/ladder.py +102 -0
  88. shikiru/domain/ladder_text.py +44 -0
  89. shikiru/domain/levels.py +44 -0
  90. shikiru/domain/motion.py +199 -0
  91. shikiru/domain/packet_evidence.py +377 -0
  92. shikiru/domain/pushes.py +47 -0
  93. shikiru/domain/quota_blocks.py +44 -0
  94. shikiru/domain/quota_waits.py +219 -0
  95. shikiru/domain/recovery.py +194 -0
  96. shikiru/domain/reports.py +498 -0
  97. shikiru/domain/results.py +217 -0
  98. shikiru/domain/runs.py +85 -0
  99. shikiru/domain/scheduling.py +231 -0
  100. shikiru/domain/status_tasks.py +40 -0
  101. shikiru/domain/steps.py +325 -0
  102. shikiru/domain/tasks.py +174 -0
  103. shikiru/domain/timeline.py +208 -0
  104. shikiru/domain/timeline_phrases.py +271 -0
  105. shikiru/domain/versions.py +18 -0
  106. shikiru/domain/workflow.py +372 -0
  107. shikiru/engine/__init__.py +0 -0
  108. shikiru/engine/acceptance.py +90 -0
  109. shikiru/engine/agents.py +61 -0
  110. shikiru/engine/cosmetic_decorator.py +99 -0
  111. shikiru/engine/daemon.py +409 -0
  112. shikiru/engine/dependency.py +37 -0
  113. shikiru/engine/escalation_filing.py +110 -0
  114. shikiru/engine/escalations.py +401 -0
  115. shikiru/engine/events.py +285 -0
  116. shikiru/engine/evidence.py +81 -0
  117. shikiru/engine/fault_reporter.py +144 -0
  118. shikiru/engine/feedback.py +577 -0
  119. shikiru/engine/feedback_audit.py +80 -0
  120. shikiru/engine/feedback_intake.py +95 -0
  121. shikiru/engine/feedback_snapshot.py +148 -0
  122. shikiru/engine/flow.py +87 -0
  123. shikiru/engine/handlers/__init__.py +33 -0
  124. shikiru/engine/handlers/base.py +148 -0
  125. shikiru/engine/handlers/engine.py +24 -0
  126. shikiru/engine/handlers/escalations.py +174 -0
  127. shikiru/engine/handlers/operator.py +81 -0
  128. shikiru/engine/handlers/tasks.py +237 -0
  129. shikiru/engine/history.py +128 -0
  130. shikiru/engine/integration.py +1234 -0
  131. shikiru/engine/integration_turns.py +337 -0
  132. shikiru/engine/layout_guard.py +362 -0
  133. shikiru/engine/lifecycle.py +60 -0
  134. shikiru/engine/monitor.py +317 -0
  135. shikiru/engine/notification_delivery.py +124 -0
  136. shikiru/engine/notification_feed.py +57 -0
  137. shikiru/engine/notifier.py +91 -0
  138. shikiru/engine/pull_request_closing.py +117 -0
  139. shikiru/engine/push_service.py +177 -0
  140. shikiru/engine/quota_probe_threads.py +79 -0
  141. shikiru/engine/quota_reconciler.py +210 -0
  142. shikiru/engine/quota_schedule.py +24 -0
  143. shikiru/engine/quota_waits.py +102 -0
  144. shikiru/engine/reconcile.py +91 -0
  145. shikiru/engine/recovery.py +284 -0
  146. shikiru/engine/retention.py +102 -0
  147. shikiru/engine/root_preparation.py +86 -0
  148. shikiru/engine/scheduler.py +320 -0
  149. shikiru/engine/session_guard.py +46 -0
  150. shikiru/engine/sessions.py +95 -0
  151. shikiru/engine/signalled.py +75 -0
  152. shikiru/engine/step_runner.py +140 -0
  153. shikiru/engine/supervisor.py +101 -0
  154. shikiru/engine/takeover.py +223 -0
  155. shikiru/engine/task_facts.py +46 -0
  156. shikiru/engine/tick_guard.py +95 -0
  157. shikiru/engine/titles.py +297 -0
  158. shikiru/engine/transitions.py +79 -0
  159. shikiru/engine/turn_context.py +206 -0
  160. shikiru/engine/turn_execution.py +30 -0
  161. shikiru/engine/turn_loop.py +230 -0
  162. shikiru/engine/turn_processes.py +79 -0
  163. shikiru/engine/turns.py +457 -0
  164. shikiru/errors.py +144 -0
  165. shikiru/helper/__init__.py +3 -0
  166. shikiru/helper/helper.py +182 -0
  167. shikiru/helper/models.py +11 -0
  168. shikiru/mcp/__init__.py +1 -0
  169. shikiru/mcp/server.py +196 -0
  170. shikiru/operator/__init__.py +33 -0
  171. shikiru/operator/attachment.py +24 -0
  172. shikiru/operator/commands.py +319 -0
  173. shikiru/operator/detectors.py +374 -0
  174. shikiru/operator/doctor.py +351 -0
  175. shikiru/operator/events.py +57 -0
  176. shikiru/operator/evidence_export.py +498 -0
  177. shikiru/operator/evidence_writer.py +78 -0
  178. shikiru/operator/lifecycle.py +294 -0
  179. shikiru/operator/plugin_checks.py +194 -0
  180. shikiru/operator/probe.py +287 -0
  181. shikiru/operator/queries.py +1601 -0
  182. shikiru/operator/services.py +36 -0
  183. shikiru/operator/sessions.py +378 -0
  184. shikiru/operator/strip_views.py +162 -0
  185. shikiru/operator/timeline.py +416 -0
  186. shikiru/operator/toml_text.py +62 -0
  187. shikiru/palette.py +18 -0
  188. shikiru/paths.py +91 -0
  189. shikiru/plugins/__init__.py +0 -0
  190. shikiru/plugins/agents/__init__.py +1 -0
  191. shikiru/plugins/agents/claude/__init__.py +5 -0
  192. shikiru/plugins/agents/claude/agent.py +251 -0
  193. shikiru/plugins/agents/claude/context.py +116 -0
  194. shikiru/plugins/agents/claude/headless.py +92 -0
  195. shikiru/plugins/agents/claude/hook.py +57 -0
  196. shikiru/plugins/agents/claude/model_catalog.py +105 -0
  197. shikiru/plugins/agents/claude/options.py +62 -0
  198. shikiru/plugins/agents/claude/turn.py +1018 -0
  199. shikiru/plugins/agents/codex/__init__.py +5 -0
  200. shikiru/plugins/agents/codex/agent.py +486 -0
  201. shikiru/plugins/agents/codex/headless.py +166 -0
  202. shikiru/plugins/agents/codex/home.py +14 -0
  203. shikiru/plugins/agents/codex/model_catalog.py +128 -0
  204. shikiru/plugins/agents/codex/options.py +94 -0
  205. shikiru/plugins/agents/codex/quota.py +195 -0
  206. shikiru/plugins/agents/codex/sandbox.py +31 -0
  207. shikiru/plugins/agents/codex/sandbox_probe.py +97 -0
  208. shikiru/plugins/agents/codex/turn.py +767 -0
  209. shikiru/plugins/agents/codex/usage.py +106 -0
  210. shikiru/plugins/agents/shared_settings.py +62 -0
  211. shikiru/plugins/widgets/__init__.py +3 -0
  212. shikiru/plugins/widgets/dialogue.py +144 -0
  213. shikiru/plugins/widgets/disk.py +123 -0
  214. shikiru/plugins/widgets/docker.py +225 -0
  215. shikiru/plugins/widgets/escalations.py +402 -0
  216. shikiru/plugins/widgets/faults.py +204 -0
  217. shikiru/plugins/widgets/header.py +321 -0
  218. shikiru/plugins/widgets/queue.py +1140 -0
  219. shikiru/plugins/widgets/queue_hint.py +93 -0
  220. shikiru/plugins/widgets/resources.py +105 -0
  221. shikiru/plugins/widgets/strips.py +82 -0
  222. shikiru/plugins/widgets/usage.py +176 -0
  223. shikiru/private.py +94 -0
  224. shikiru/process.py +535 -0
  225. shikiru/prompts/__init__.py +3 -0
  226. shikiru/prompts/renderer.py +85 -0
  227. shikiru/prompts/templates/_base.md.j2 +17 -0
  228. shikiru/prompts/templates/_macros.md.j2 +61 -0
  229. shikiru/prompts/templates/conflicts.md.j2 +22 -0
  230. shikiru/prompts/templates/consult.md.j2 +5 -0
  231. shikiru/prompts/templates/continue.md.j2 +4 -0
  232. shikiru/prompts/templates/feedback_analysis.md.j2 +29 -0
  233. shikiru/prompts/templates/feedback_rework.md.j2 +33 -0
  234. shikiru/prompts/templates/feedback_verify.md.j2 +33 -0
  235. shikiru/prompts/templates/handoff.md.j2 +23 -0
  236. shikiru/prompts/templates/implement.md.j2 +13 -0
  237. shikiru/prompts/templates/plan.md.j2 +30 -0
  238. shikiru/prompts/templates/rework.md.j2 +13 -0
  239. shikiru/prompts/templates/triage.md.j2 +20 -0
  240. shikiru/prompts/templates/verify.md.j2 +29 -0
  241. shikiru/store/__init__.py +1 -0
  242. shikiru/store/commands.py +242 -0
  243. shikiru/store/database.py +297 -0
  244. shikiru/store/escalations.py +240 -0
  245. shikiru/store/events.py +213 -0
  246. shikiru/store/feedback.py +266 -0
  247. shikiru/store/migrations/001_initial.sql +63 -0
  248. shikiru/store/migrations/002_headless_turns.sql +31 -0
  249. shikiru/store/migrations/003_drop_command_handled_at.sql +1 -0
  250. shikiru/store/migrations/004_repo_base_ref.sql +3 -0
  251. shikiru/store/migrations/005_complexity_explicit.sql +3 -0
  252. shikiru/store/migrations/006_events_kind_index.sql +1 -0
  253. shikiru/store/migrations/007_notification_cursor.sql +1 -0
  254. shikiru/store/migrations/008_reported_usage.sql +1 -0
  255. shikiru/store/migrations/009_billing_declarations.sql +2 -0
  256. shikiru/store/migrations/010_canonical_quota_windows.sql +3 -0
  257. shikiru/store/migrations/012_drop_task_quota_evidence.sql +2 -0
  258. shikiru/store/migrations/013_push_targets.sql +7 -0
  259. shikiru/store/migrations/014_agent_block_time.sql +6 -0
  260. shikiru/store/migrations/015_repair_orphan_agent_blocks.sql +3 -0
  261. shikiru/store/migrations/016_feedback_rounds.sql +29 -0
  262. shikiru/store/migrations/017_feedback_round_comments.sql +5 -0
  263. shikiru/store/migrations/__init__.py +1 -0
  264. shikiru/store/notification_cursor.py +29 -0
  265. shikiru/store/pushes.py +117 -0
  266. shikiru/store/quotas.py +153 -0
  267. shikiru/store/reader.py +310 -0
  268. shikiru/store/retention.py +92 -0
  269. shikiru/store/runs.py +227 -0
  270. shikiru/store/tasks.py +949 -0
  271. shikiru/telemetry/__init__.py +1 -0
  272. shikiru/telemetry/disk.py +167 -0
  273. shikiru/telemetry/docker.py +164 -0
  274. shikiru/telemetry/resources.py +296 -0
  275. shikiru/tmux/__init__.py +3 -0
  276. shikiru/tmux/client.py +637 -0
  277. shikiru/tmux/decorator.py +225 -0
  278. shikiru/tmux/layout.py +200 -0
  279. shikiru/tmux/session.py +381 -0
  280. shikiru/ui/__init__.py +9 -0
  281. shikiru/ui/age.py +45 -0
  282. shikiru/ui/bubbles.py +41 -0
  283. shikiru/ui/compact.py +1209 -0
  284. shikiru/ui/console.py +49 -0
  285. shikiru/ui/fold.py +50 -0
  286. shikiru/ui/gated_static.py +117 -0
  287. shikiru/ui/gauge.py +40 -0
  288. shikiru/ui/glyphs.py +109 -0
  289. shikiru/ui/keys.py +57 -0
  290. shikiru/ui/live_interval.py +42 -0
  291. shikiru/ui/popup.py +311 -0
  292. shikiru/ui/quota_status.py +26 -0
  293. shikiru/ui/recent_window.py +78 -0
  294. shikiru/ui/screen.py +57 -0
  295. shikiru/ui/section.py +193 -0
  296. shikiru/ui/session.py +65 -0
  297. shikiru/ui/size.py +17 -0
  298. shikiru/ui/splash.py +42 -0
  299. shikiru/ui/status.py +47 -0
  300. shikiru/ui/status_strip.py +149 -0
  301. shikiru/ui/stream.py +152 -0
  302. shikiru/ui/strip.py +332 -0
  303. shikiru/ui/theme.py +83 -0
  304. shikiru/ui/tokens.py +51 -0
  305. shikiru/view_log.py +126 -0
  306. shikiru/workspace/__init__.py +3 -0
  307. shikiru/workspace/base_refs.py +160 -0
  308. shikiru/workspace/caches.py +136 -0
  309. shikiru/workspace/facts.py +118 -0
  310. shikiru/workspace/git.py +601 -0
  311. shikiru/workspace/github.py +744 -0
  312. shikiru/workspace/messages.py +107 -0
  313. shikiru/workspace/reports.py +145 -0
  314. shikiru/workspace/roots.py +180 -0
  315. shikiru_cli-0.2.0.dist-info/METADATA +428 -0
  316. shikiru_cli-0.2.0.dist-info/RECORD +319 -0
  317. shikiru_cli-0.2.0.dist-info/WHEEL +4 -0
  318. shikiru_cli-0.2.0.dist-info/entry_points.txt +18 -0
  319. shikiru_cli-0.2.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,213 @@
1
+ """Event log: append typed events, read them back since an id."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ from pydantic import TypeAdapter
8
+
9
+ from shikiru.domain.commands import COMMAND_ID_KEY
10
+ from shikiru.domain.events import Event, EventKind
11
+ from shikiru.domain.tasks import Role
12
+ from shikiru.store.database import (
13
+ dump_json,
14
+ format_timestamp,
15
+ load_json,
16
+ parse_required_timestamp,
17
+ require_after_insert,
18
+ )
19
+
20
+ if TYPE_CHECKING:
21
+ import sqlite3 # noqa: TID251
22
+ from collections.abc import Sequence
23
+
24
+ from shikiru.clock import Clock
25
+
26
+ _EVENT_ROLE_JSON_PATH = "$.role"
27
+ _EVENT_TURN_JSON_PATH = "$.turn"
28
+ _EVENT_COMMAND_JSON_PATH = f"$.{COMMAND_ID_KEY}"
29
+ MAX_TIMELINE_EVENT_LIMIT = 50
30
+
31
+ COMMAND_APPLIED_SQL = "SELECT 1 FROM events WHERE kind = ? AND json_extract(data, ?) = ? LIMIT 1"
32
+
33
+ _DATA_ADAPTER: TypeAdapter[dict[str, Any]] = TypeAdapter(dict[str, Any])
34
+
35
+
36
+ def _row_to_event(row: sqlite3.Row) -> Event:
37
+ return Event(
38
+ id=row["id"],
39
+ ts=parse_required_timestamp(row["ts"]),
40
+ task_id=row["task_id"],
41
+ kind=EventKind(row["kind"]),
42
+ message=row["message"],
43
+ data=load_json(_DATA_ADAPTER, row["data"]),
44
+ )
45
+
46
+
47
+ def fetch_events_since(
48
+ connection: sqlite3.Connection,
49
+ since_id: int,
50
+ *,
51
+ kinds: Sequence[EventKind] | None = None,
52
+ task_id: str | None = None,
53
+ ) -> list[Event]:
54
+ """Read every event with `id > since_id`, optionally filtered by `kinds` and `task_id`."""
55
+ clauses = ["id > ?"]
56
+ params: list[object] = [since_id]
57
+ if kinds:
58
+ placeholders = ", ".join("?" for _ in kinds)
59
+ clauses.append(f"kind IN ({placeholders})")
60
+ params.extend(kind.value for kind in kinds)
61
+ if task_id is not None:
62
+ clauses.append("task_id = ?")
63
+ params.append(task_id)
64
+ where = " AND ".join(clauses)
65
+ rows = connection.execute(
66
+ f"SELECT id, ts, task_id, kind, message, data FROM events WHERE {where} ORDER BY id",
67
+ params,
68
+ ).fetchall()
69
+ return [_row_to_event(row) for row in rows]
70
+
71
+
72
+ def fetch_latest_events(
73
+ connection: sqlite3.Connection,
74
+ limit: int,
75
+ *,
76
+ kinds: Sequence[EventKind] | None = None,
77
+ implementer_turns_only: bool = False,
78
+ task_id: str | None = None,
79
+ ) -> list[Event]:
80
+ """Read the latest matching events in chronological order."""
81
+ if limit < 1:
82
+ message = "event limit must be positive"
83
+ raise ValueError(message)
84
+ clauses: list[str] = []
85
+ params: list[object] = []
86
+ if kinds:
87
+ placeholders = ", ".join("?" for _ in kinds)
88
+ clauses.append(f"kind IN ({placeholders})")
89
+ params.extend(kind.value for kind in kinds)
90
+ if task_id is not None:
91
+ clauses.append("task_id = ?")
92
+ params.append(task_id)
93
+ if implementer_turns_only:
94
+ clauses.append("(kind != ? OR json_extract(data, ?) = ?)")
95
+ params.extend(
96
+ (EventKind.TURN_COMPLETED.value, _EVENT_ROLE_JSON_PATH, Role.IMPLEMENTER.value)
97
+ )
98
+ where = f"WHERE {' AND '.join(clauses)} " if clauses else ""
99
+ rows = connection.execute(
100
+ f"SELECT id, ts, task_id, kind, message, data FROM events {where}ORDER BY id DESC LIMIT ?",
101
+ [*params, limit],
102
+ ).fetchall()
103
+ return [_row_to_event(row) for row in reversed(rows)]
104
+
105
+
106
+ def fetch_timeline_events(
107
+ connection: sqlite3.Connection,
108
+ task_id: str,
109
+ *,
110
+ limit: int,
111
+ before: tuple[str, int] | None = None,
112
+ kinds: Sequence[EventKind] | None = None,
113
+ role: str | None = None,
114
+ turn: str | None = None,
115
+ ) -> list[Event]:
116
+ """Read one newest-first, keyset-paginated task event page with SQL filters."""
117
+ if not 0 < limit <= MAX_TIMELINE_EVENT_LIMIT:
118
+ message = f"task timeline event limit must be between 1 and {MAX_TIMELINE_EVENT_LIMIT}"
119
+ raise ValueError(message)
120
+ if kinds is not None and not kinds:
121
+ return []
122
+ clauses = ["task_id = ?"]
123
+ params: list[object] = [task_id]
124
+ if before is not None:
125
+ clauses.append("(ts < ? OR (ts = ? AND id < ?))")
126
+ params.extend((before[0], before[0], before[1]))
127
+ if kinds is not None:
128
+ placeholders = ", ".join("?" for _ in kinds)
129
+ clauses.append(f"kind IN ({placeholders})")
130
+ params.extend(kind.value for kind in kinds)
131
+ if role is not None:
132
+ clauses.append("json_extract(data, ?) = ?")
133
+ params.extend((_EVENT_ROLE_JSON_PATH, role))
134
+ if turn is not None:
135
+ clauses.append("json_extract(data, ?) = ?")
136
+ params.extend((_EVENT_TURN_JSON_PATH, turn))
137
+ where = " AND ".join(clauses)
138
+ rows = connection.execute(
139
+ f"SELECT id, ts, task_id, kind, message, data FROM events WHERE {where} "
140
+ "ORDER BY ts DESC, id DESC LIMIT ?",
141
+ [*params, limit],
142
+ ).fetchall()
143
+ return [_row_to_event(row) for row in rows]
144
+
145
+
146
+ class EventLog:
147
+ """Appends typed events and reads them back since an id (`domain.events`)."""
148
+
149
+ def __init__(self, connection: sqlite3.Connection, clock: Clock) -> None:
150
+ """Use `connection` (the engine's writer) and `clock` for every append below."""
151
+ self._connection = connection
152
+ self._clock = clock
153
+
154
+ def append(
155
+ self,
156
+ kind: EventKind,
157
+ *,
158
+ message: str,
159
+ task_id: str | None = None,
160
+ data: dict[str, Any] | None = None,
161
+ ) -> Event:
162
+ """Append one event and return it with its allocated id and timestamp."""
163
+ ts = self._clock.now()
164
+ event_data = data if data is not None else {}
165
+ cursor = self._connection.execute(
166
+ "INSERT INTO events (ts, task_id, kind, message, data) VALUES (?, ?, ?, ?, ?)",
167
+ (
168
+ format_timestamp(ts),
169
+ task_id,
170
+ kind.value,
171
+ message,
172
+ dump_json(_DATA_ADAPTER, event_data),
173
+ ),
174
+ )
175
+ event_id = require_after_insert(
176
+ cursor.lastrowid, "sqlite did not allocate a rowid for the new event"
177
+ )
178
+ return Event(
179
+ id=event_id, ts=ts, task_id=task_id, kind=kind, message=message, data=event_data
180
+ )
181
+
182
+ def since(
183
+ self,
184
+ since_id: int,
185
+ *,
186
+ kinds: Sequence[EventKind] | None = None,
187
+ task_id: str | None = None,
188
+ ) -> list[Event]:
189
+ """Read every event since `since_id`, optionally filtered by `kinds` and `task_id`."""
190
+ return fetch_events_since(self._connection, since_id, kinds=kinds, task_id=task_id)
191
+
192
+ def latest(
193
+ self,
194
+ limit: int,
195
+ *,
196
+ kinds: Sequence[EventKind] | None = None,
197
+ task_id: str | None = None,
198
+ ) -> list[Event]:
199
+ """Read the latest matching events in chronological order."""
200
+ return fetch_latest_events(self._connection, limit, kinds=kinds, task_id=task_id)
201
+
202
+ def command_applied(self, kind: EventKind, command_id: int) -> bool:
203
+ """Report whether a `kind` event already records the effect of command `command_id`."""
204
+ row = self._connection.execute(
205
+ COMMAND_APPLIED_SQL,
206
+ (kind.value, _EVENT_COMMAND_JSON_PATH, command_id),
207
+ ).fetchone()
208
+ return row is not None
209
+
210
+ def latest_id(self) -> int:
211
+ """Return the current event cursor, or zero when the log is empty."""
212
+ row = self._connection.execute("SELECT COALESCE(MAX(id), 0) FROM events").fetchone()
213
+ return int(row[0])
@@ -0,0 +1,266 @@
1
+ """The durable progress of PR feedback rounds and of the replies they owe."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import TYPE_CHECKING
7
+
8
+ from shikiru.domain.feedback import FeedbackSnapshot, FeedbackSourceKind
9
+ from shikiru.domain.feedback_rounds import (
10
+ UNFINISHED_STAGES,
11
+ FeedbackReply,
12
+ FeedbackRound,
13
+ FeedbackStage,
14
+ ReplyState,
15
+ )
16
+ from shikiru.domain.reports import FeedbackAnalysis
17
+ from shikiru.store.database import transaction
18
+
19
+ if TYPE_CHECKING:
20
+ import sqlite3 # noqa: TID251
21
+ from collections.abc import Sequence
22
+
23
+ _ROUND_COLUMNS = (
24
+ "id, task_id, pull_request_url, review_ids, snapshot, stage, analysis, attempts, blocked, "
25
+ " expected_remote_sha, verified_sha, published_sha, checks, verified_replies"
26
+ )
27
+ _REPLY_COLUMNS = "round_id, source_id, kind, marker, body, state, posted_url"
28
+ _UNFINISHED = tuple(stage.value for stage in sorted(UNFINISHED_STAGES))
29
+
30
+
31
+ class FeedbackStore:
32
+ """Stores, per task, the feedback rounds of its pull request and each round's replies."""
33
+
34
+ def __init__(self, connection: sqlite3.Connection) -> None:
35
+ """Use `connection`, the engine's writer."""
36
+ self._connection = connection
37
+
38
+ def start(
39
+ self,
40
+ *,
41
+ task_id: str,
42
+ pull_request_url: str,
43
+ review_ids: Sequence[str],
44
+ comment_ids: Sequence[str],
45
+ snapshot: FeedbackSnapshot,
46
+ expected_remote_sha: str,
47
+ ) -> FeedbackRound:
48
+ """Record a new round over `snapshot`, starting at its analysis."""
49
+ with transaction(self._connection):
50
+ cursor = self._connection.execute(
51
+ "INSERT INTO feedback_rounds "
52
+ "(task_id, pull_request_url, review_ids, snapshot, stage, expected_remote_sha) "
53
+ "VALUES (?, ?, ?, ?, ?, ?)",
54
+ (
55
+ task_id,
56
+ pull_request_url,
57
+ json.dumps(list(review_ids)),
58
+ snapshot.model_dump_json(),
59
+ FeedbackStage.ANALYSIS.value,
60
+ expected_remote_sha,
61
+ ),
62
+ )
63
+ round_id = _row_id(cursor)
64
+ self._connection.executemany(
65
+ "INSERT INTO feedback_round_comments (round_id, comment_id) VALUES (?, ?)",
66
+ [(round_id, comment_id) for comment_id in comment_ids],
67
+ )
68
+ return self._require(round_id)
69
+
70
+ def active(self, task_id: str) -> FeedbackRound | None:
71
+ """Return `task_id`'s unfinished round, if it has one."""
72
+ row = self._connection.execute(
73
+ f"SELECT {_ROUND_COLUMNS} FROM feedback_rounds WHERE task_id = ? "
74
+ f"AND stage IN ({_marks(_UNFINISHED)}) ORDER BY id LIMIT 1",
75
+ (task_id, *_UNFINISHED),
76
+ ).fetchone()
77
+ return None if row is None else self._load(row)
78
+
79
+ def last_pull_request_url(self, task_id: str) -> str | None:
80
+ """Return the pull request of `task_id`'s latest round, or `None` without any round."""
81
+ row = self._connection.execute(
82
+ "SELECT pull_request_url FROM feedback_rounds WHERE task_id = ? "
83
+ "ORDER BY id DESC LIMIT 1",
84
+ (task_id,),
85
+ ).fetchone()
86
+ return None if row is None else str(row["pull_request_url"])
87
+
88
+ def seen_review_ids(self, task_id: str, pull_request_url: str) -> set[str]:
89
+ """Return every review a round of `task_id` on `pull_request_url` was started for."""
90
+ rows = self._connection.execute(
91
+ "SELECT review_ids FROM feedback_rounds WHERE task_id = ? AND pull_request_url = ?",
92
+ (task_id, pull_request_url),
93
+ ).fetchall()
94
+ return {review for row in rows for review in json.loads(row["review_ids"])}
95
+
96
+ def seen_comment_ids(self, task_id: str, pull_request_url: str) -> set[str]:
97
+ """Return every discussion comment a round of `task_id` on `pull_request_url` covered."""
98
+ rows = self._connection.execute(
99
+ "SELECT comment_id FROM feedback_round_comments WHERE round_id IN "
100
+ "(SELECT id FROM feedback_rounds WHERE task_id = ? AND pull_request_url = ?)",
101
+ (task_id, pull_request_url),
102
+ ).fetchall()
103
+ return {str(row["comment_id"]) for row in rows}
104
+
105
+ def set_stage(self, round_id: int, stage: FeedbackStage) -> None:
106
+ """Move the round to `stage`."""
107
+ self._set(round_id, "stage", stage.value)
108
+
109
+ def save_analysis(
110
+ self, round_id: int, analysis: FeedbackAnalysis, stage: FeedbackStage
111
+ ) -> None:
112
+ """Keep the Leader's `analysis` and move the round to `stage`."""
113
+ with transaction(self._connection):
114
+ self._set(round_id, "analysis", analysis.model_dump_json())
115
+ self._set(round_id, "stage", stage.value)
116
+
117
+ def complete_implementation(self, round_id: int, attempts: int, checks: str) -> None:
118
+ """Atomically record a completed rework and advance to verification."""
119
+ with transaction(self._connection):
120
+ self._set(round_id, "checks", checks)
121
+ self._set(round_id, "attempts", attempts)
122
+ self._set(round_id, "stage", FeedbackStage.VERIFICATION.value)
123
+
124
+ def set_blocked(self, round_id: int, *, blocked: bool) -> None:
125
+ """Record whether the round waits for the Operator."""
126
+ self._set(round_id, "blocked", int(blocked))
127
+
128
+ def reset_after_retry(self, round_id: int, stage: FeedbackStage) -> None:
129
+ """Clear the block and the attempts of a retried round and resume it at `stage`."""
130
+ with transaction(self._connection):
131
+ self._set(round_id, "blocked", 0)
132
+ self._set(round_id, "attempts", 0)
133
+ self._set(round_id, "stage", stage.value)
134
+
135
+ def save_verified(
136
+ self,
137
+ round_id: int,
138
+ *,
139
+ sha: str,
140
+ replies: dict[str, str],
141
+ stage: FeedbackStage,
142
+ planned: Sequence[FeedbackReply] = (),
143
+ ) -> None:
144
+ """Record the tip and replies the Leader verified, with any `planned` replies owed now.
145
+
146
+ The verification, the planned replies and the move to `stage` commit together.
147
+ """
148
+ with transaction(self._connection):
149
+ self._set(round_id, "verified_sha", sha)
150
+ self._set(round_id, "verified_replies", json.dumps(replies))
151
+ self._insert_replies(planned)
152
+ self._set(round_id, "stage", stage.value)
153
+
154
+ def set_expected_remote(self, round_id: int, sha: str) -> None:
155
+ """Record the remote head the round's push may overwrite."""
156
+ self._set(round_id, "expected_remote_sha", sha)
157
+
158
+ def save_published(self, round_id: int, sha: str) -> None:
159
+ """Record the commit published to the remote branch."""
160
+ self._set(round_id, "published_sha", sha)
161
+
162
+ def plan_replies(self, round_id: int, replies: Sequence[FeedbackReply]) -> None:
163
+ """Store the replies the round owes, keeping any already stored, and move to sending."""
164
+ with transaction(self._connection):
165
+ self._insert_replies(replies)
166
+ self._set(round_id, "stage", FeedbackStage.REPLIES.value)
167
+
168
+ def replies(self, round_id: int) -> list[FeedbackReply]:
169
+ """Return the round's replies in the order they were planned."""
170
+ rows = self._connection.execute(
171
+ f"SELECT {_REPLY_COLUMNS} FROM feedback_replies WHERE round_id = ? ORDER BY rowid",
172
+ (round_id,),
173
+ ).fetchall()
174
+ return [_reply(row) for row in rows]
175
+
176
+ def set_reply_state(
177
+ self, round_id: int, source_id: str, state: ReplyState, posted_url: str | None = None
178
+ ) -> None:
179
+ """Record how far the reply to `source_id` got."""
180
+ self._connection.execute(
181
+ "UPDATE feedback_replies SET state = ?, posted_url = COALESCE(?, posted_url) "
182
+ "WHERE round_id = ? AND source_id = ?",
183
+ (state.value, posted_url, round_id, source_id),
184
+ )
185
+
186
+ def _insert_replies(self, replies: Sequence[FeedbackReply]) -> None:
187
+ for reply in replies:
188
+ self._connection.execute(
189
+ f"INSERT OR IGNORE INTO feedback_replies ({_REPLY_COLUMNS}) "
190
+ "VALUES (?, ?, ?, ?, ?, ?, ?)",
191
+ (
192
+ reply.round_id,
193
+ reply.source_id,
194
+ reply.kind.value,
195
+ reply.marker,
196
+ reply.body,
197
+ reply.state.value,
198
+ reply.posted_url,
199
+ ),
200
+ )
201
+
202
+ def _require(self, round_id: int) -> FeedbackRound:
203
+ row = self._connection.execute(
204
+ f"SELECT {_ROUND_COLUMNS} FROM feedback_rounds WHERE id = ?", (round_id,)
205
+ ).fetchone()
206
+ if row is None:
207
+ message = f"feedback round {round_id} vanished"
208
+ raise LookupError(message)
209
+ return self._load(row)
210
+
211
+ def _load(self, row: sqlite3.Row) -> FeedbackRound:
212
+ comments = self._connection.execute(
213
+ "SELECT comment_id FROM feedback_round_comments WHERE round_id = ? ORDER BY rowid",
214
+ (row["id"],),
215
+ ).fetchall()
216
+ return _round(row, [str(comment["comment_id"]) for comment in comments])
217
+
218
+ def _set(self, round_id: int, column: str, value: str | int) -> None:
219
+ self._connection.execute(
220
+ f"UPDATE feedback_rounds SET {column} = ? WHERE id = ?", (value, round_id)
221
+ )
222
+
223
+
224
+ def _row_id(cursor: sqlite3.Cursor) -> int:
225
+ row_id = cursor.lastrowid
226
+ if row_id is None:
227
+ message = "inserted feedback round has no row id"
228
+ raise LookupError(message)
229
+ return row_id
230
+
231
+
232
+ def _marks(values: Sequence[str]) -> str:
233
+ return ", ".join("?" for _ in values)
234
+
235
+
236
+ def _round(row: sqlite3.Row, comment_ids: list[str]) -> FeedbackRound:
237
+ analysis = row["analysis"]
238
+ return FeedbackRound(
239
+ id=row["id"],
240
+ task_id=row["task_id"],
241
+ pull_request_url=row["pull_request_url"],
242
+ review_ids=json.loads(row["review_ids"]),
243
+ comment_ids=comment_ids,
244
+ snapshot=FeedbackSnapshot.model_validate_json(row["snapshot"]),
245
+ stage=FeedbackStage(row["stage"]),
246
+ analysis=None if analysis is None else FeedbackAnalysis.model_validate_json(analysis),
247
+ attempts=row["attempts"],
248
+ blocked=bool(row["blocked"]),
249
+ expected_remote_sha=row["expected_remote_sha"],
250
+ verified_sha=row["verified_sha"],
251
+ published_sha=row["published_sha"],
252
+ checks=row["checks"],
253
+ verified_replies=json.loads(row["verified_replies"]),
254
+ )
255
+
256
+
257
+ def _reply(row: sqlite3.Row) -> FeedbackReply:
258
+ return FeedbackReply(
259
+ round_id=row["round_id"],
260
+ source_id=row["source_id"],
261
+ kind=FeedbackSourceKind(row["kind"]),
262
+ marker=row["marker"],
263
+ body=row["body"],
264
+ state=ReplyState(row["state"]),
265
+ posted_url=row["posted_url"],
266
+ )
@@ -0,0 +1,63 @@
1
+ CREATE TABLE sequences (name TEXT PRIMARY KEY, value INTEGER NOT NULL);
2
+
3
+ CREATE TABLE tasks (
4
+ id TEXT PRIMARY KEY, seq INTEGER UNIQUE NOT NULL,
5
+ parent_id TEXT REFERENCES tasks(id), origin TEXT NOT NULL,
6
+ title TEXT NOT NULL, request TEXT NOT NULL,
7
+ mode TEXT, complexity TEXT NOT NULL DEFAULT 'normal', priority INTEGER NOT NULL DEFAULT 0,
8
+ status TEXT NOT NULL, step TEXT, status_reason TEXT,
9
+ holds TEXT NOT NULL DEFAULT '[]',
10
+ iteration INTEGER NOT NULL DEFAULT 0,
11
+ counters TEXT NOT NULL DEFAULT '{}',
12
+ rung_index TEXT NOT NULL DEFAULT '{}',
13
+ overrides TEXT NOT NULL DEFAULT '{}',
14
+ repo TEXT,
15
+ slot INTEGER, window_id TEXT, panes TEXT NOT NULL DEFAULT '{}',
16
+ wait_reason TEXT, requeued_at TEXT, summary TEXT,
17
+ created_at TEXT NOT NULL, started_at TEXT, finished_at TEXT
18
+ );
19
+
20
+ CREATE INDEX idx_tasks_status ON tasks(status);
21
+
22
+ CREATE TABLE task_deps (task_id TEXT NOT NULL REFERENCES tasks(id), depends_on TEXT NOT NULL REFERENCES tasks(id),
23
+ PRIMARY KEY (task_id, depends_on));
24
+
25
+ CREATE INDEX idx_task_deps_depends_on ON task_deps(depends_on);
26
+
27
+ CREATE TABLE runs (
28
+ id INTEGER PRIMARY KEY, task_id TEXT NOT NULL REFERENCES tasks(id), role TEXT NOT NULL,
29
+ agent TEXT NOT NULL, model TEXT NOT NULL, effort TEXT,
30
+ session_id TEXT, resume_command TEXT,
31
+ agent_state TEXT NOT NULL DEFAULT '{}', last_message TEXT,
32
+ status TEXT NOT NULL,
33
+ input_tokens INTEGER NOT NULL DEFAULT 0, output_tokens INTEGER NOT NULL DEFAULT 0,
34
+ cache_read_tokens INTEGER NOT NULL DEFAULT 0, cache_write_tokens INTEGER NOT NULL DEFAULT 0,
35
+ cost_usd REAL, cost_basis TEXT, context_used_pct REAL,
36
+ started_at TEXT NOT NULL, ended_at TEXT
37
+ );
38
+
39
+ CREATE INDEX idx_runs_task_id ON runs(task_id);
40
+
41
+ CREATE TABLE agent_blocks (agent TEXT PRIMARY KEY, until TEXT NOT NULL, reason TEXT NOT NULL);
42
+ CREATE TABLE quotas (agent TEXT NOT NULL, window TEXT NOT NULL, used_pct REAL NOT NULL, resets_at TEXT,
43
+ observed_at TEXT NOT NULL, PRIMARY KEY (agent, window));
44
+
45
+ CREATE TABLE escalations (
46
+ id TEXT PRIMARY KEY, task_id TEXT NOT NULL REFERENCES tasks(id), step TEXT, reason TEXT NOT NULL,
47
+ headline TEXT NOT NULL, packet_path TEXT NOT NULL, options TEXT NOT NULL,
48
+ status TEXT NOT NULL,
49
+ resolution TEXT, created_at TEXT NOT NULL, resolved_at TEXT
50
+ );
51
+
52
+ CREATE INDEX idx_escalations_status ON escalations(status);
53
+
54
+ CREATE TABLE events (id INTEGER PRIMARY KEY, ts TEXT NOT NULL, task_id TEXT, kind TEXT NOT NULL,
55
+ message TEXT NOT NULL, data TEXT NOT NULL DEFAULT '{}');
56
+
57
+ CREATE INDEX idx_events_task_id ON events(task_id);
58
+
59
+ CREATE TABLE commands (id INTEGER PRIMARY KEY, ts TEXT NOT NULL, kind TEXT NOT NULL, payload TEXT NOT NULL,
60
+ status TEXT NOT NULL DEFAULT 'pending',
61
+ result TEXT, handled_at TEXT);
62
+
63
+ CREATE INDEX idx_commands_status ON commands(status);
@@ -0,0 +1,31 @@
1
+ DROP INDEX idx_runs_task_id;
2
+
3
+ ALTER TABLE runs RENAME TO runs_legacy;
4
+
5
+ CREATE TABLE runs (
6
+ id INTEGER PRIMARY KEY, task_id TEXT NOT NULL REFERENCES tasks(id), role TEXT NOT NULL,
7
+ agent TEXT NOT NULL, model TEXT NOT NULL, effort TEXT,
8
+ session_id TEXT, resume_command TEXT, last_message TEXT,
9
+ status TEXT NOT NULL,
10
+ input_tokens INTEGER NOT NULL DEFAULT 0, output_tokens INTEGER NOT NULL DEFAULT 0,
11
+ cache_read_tokens INTEGER NOT NULL DEFAULT 0, cache_write_tokens INTEGER NOT NULL DEFAULT 0,
12
+ cost_usd REAL, cost_basis TEXT NOT NULL DEFAULT 'unknown', context_used_pct REAL,
13
+ started_at TEXT NOT NULL, ended_at TEXT
14
+ );
15
+
16
+ INSERT INTO runs (
17
+ id, task_id, role, agent, model, effort, session_id, resume_command, last_message,
18
+ status,
19
+ input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, cost_usd, cost_basis,
20
+ context_used_pct, started_at, ended_at
21
+ )
22
+ SELECT
23
+ id, task_id, role, agent, model, effort, session_id, resume_command, last_message,
24
+ CASE WHEN status = 'exited' THEN 'closed' ELSE status END,
25
+ input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, cost_usd,
26
+ COALESCE(cost_basis, 'unknown'), context_used_pct, started_at, ended_at
27
+ FROM runs_legacy;
28
+
29
+ DROP TABLE runs_legacy;
30
+
31
+ CREATE INDEX idx_runs_task_id ON runs(task_id);
@@ -0,0 +1 @@
1
+ ALTER TABLE commands DROP COLUMN handled_at;
@@ -0,0 +1,3 @@
1
+ UPDATE tasks
2
+ SET repo = json_set(repo, '$.base_ref', json_extract(repo, '$.base'))
3
+ WHERE repo IS NOT NULL AND json_extract(repo, '$.base_ref') IS NULL
@@ -0,0 +1,3 @@
1
+ ALTER TABLE tasks ADD COLUMN complexity_explicit INTEGER NOT NULL DEFAULT 0;
2
+
3
+ UPDATE tasks SET complexity_explicit = 1 WHERE complexity != 'normal'
@@ -0,0 +1 @@
1
+ CREATE INDEX idx_events_kind ON events(kind)
@@ -0,0 +1 @@
1
+ CREATE TABLE notification_cursor (id INTEGER PRIMARY KEY CHECK (id = 1), event_id INTEGER NOT NULL)
@@ -0,0 +1 @@
1
+ ALTER TABLE runs ADD COLUMN usage_reported INTEGER;
@@ -0,0 +1,2 @@
1
+ ALTER TABLE runs ADD COLUMN billing_mode TEXT NOT NULL DEFAULT 'unknown';
2
+ ALTER TABLE runs ADD COLUMN billing_source TEXT NOT NULL DEFAULT 'unknown';
@@ -0,0 +1,3 @@
1
+ DELETE FROM quotas
2
+ WHERE (agent = 'claude' AND window IN ('five_hour', 'seven_day'))
3
+ OR (agent = 'codex' AND window IN ('primary', 'secondary'));
@@ -0,0 +1,2 @@
1
+ DROP TABLE IF EXISTS task_quota_samples;
2
+ DROP TABLE IF EXISTS task_quota_turns;
@@ -0,0 +1,7 @@
1
+ CREATE TABLE IF NOT EXISTS push_targets (
2
+ base TEXT PRIMARY KEY,
3
+ registered INTEGER NOT NULL,
4
+ pushed INTEGER NOT NULL,
5
+ due_at TEXT,
6
+ last_pushed_at TEXT
7
+ )
@@ -0,0 +1,6 @@
1
+ CREATE TABLE IF NOT EXISTS agent_block_times (
2
+ agent TEXT PRIMARY KEY,
3
+ blocked_at TEXT NOT NULL
4
+ );
5
+ INSERT OR IGNORE INTO agent_block_times (agent, blocked_at)
6
+ SELECT agent, strftime('%Y-%m-%dT%H:%M:%f000+00:00', 'now') FROM agent_blocks
@@ -0,0 +1,3 @@
1
+ DELETE FROM agent_block_times WHERE agent NOT IN (SELECT agent FROM agent_blocks);
2
+ INSERT OR IGNORE INTO agent_block_times (agent, blocked_at)
3
+ SELECT agent, strftime('%Y-%m-%dT%H:%M:%f000+00:00', 'now') FROM agent_blocks
@@ -0,0 +1,29 @@
1
+ CREATE TABLE IF NOT EXISTS feedback_rounds (
2
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
3
+ task_id TEXT NOT NULL,
4
+ pull_request_url TEXT NOT NULL,
5
+ review_ids TEXT NOT NULL,
6
+ snapshot TEXT NOT NULL,
7
+ stage TEXT NOT NULL,
8
+ analysis TEXT,
9
+ attempts INTEGER NOT NULL DEFAULT 0,
10
+ blocked INTEGER NOT NULL DEFAULT 0,
11
+ expected_remote_sha TEXT NOT NULL,
12
+ verified_sha TEXT,
13
+ published_sha TEXT,
14
+ checks TEXT NOT NULL DEFAULT '',
15
+ verified_replies TEXT NOT NULL DEFAULT '{}'
16
+ );
17
+
18
+ CREATE INDEX IF NOT EXISTS feedback_rounds_task ON feedback_rounds (task_id, pull_request_url);
19
+
20
+ CREATE TABLE IF NOT EXISTS feedback_replies (
21
+ round_id INTEGER NOT NULL REFERENCES feedback_rounds(id),
22
+ source_id TEXT NOT NULL,
23
+ kind TEXT NOT NULL,
24
+ marker TEXT NOT NULL,
25
+ body TEXT NOT NULL,
26
+ state TEXT NOT NULL,
27
+ posted_url TEXT,
28
+ PRIMARY KEY (round_id, source_id)
29
+ );
@@ -0,0 +1,5 @@
1
+ CREATE TABLE IF NOT EXISTS feedback_round_comments (
2
+ round_id INTEGER NOT NULL REFERENCES feedback_rounds(id),
3
+ comment_id TEXT NOT NULL,
4
+ PRIMARY KEY (round_id, comment_id)
5
+ );
@@ -0,0 +1 @@
1
+ """Packaged SQL migrations, applied in ascending filename order."""