whiletrue 0.1.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.
- whiletrue/__init__.py +0 -0
- whiletrue/__main__.py +3 -0
- whiletrue/abort.py +79 -0
- whiletrue/basics/__init__.py +99 -0
- whiletrue/basics/agent.py +249 -0
- whiletrue/basics/comments.py +113 -0
- whiletrue/basics/diff_size.py +80 -0
- whiletrue/basics/exec_.py +84 -0
- whiletrue/basics/history.py +117 -0
- whiletrue/basics/human.py +201 -0
- whiletrue/basics/json_source.py +35 -0
- whiletrue/basics/markdown_source.py +55 -0
- whiletrue/basics/process.py +99 -0
- whiletrue/basics/sessions.py +62 -0
- whiletrue/basics/set_.py +29 -0
- whiletrue/basics/shell.py +98 -0
- whiletrue/basics/wake.py +27 -0
- whiletrue/claude_code/__init__.py +58 -0
- whiletrue/claude_code/endpoint.py +143 -0
- whiletrue/claude_code/headless.py +181 -0
- whiletrue/claude_code/interactive.py +344 -0
- whiletrue/claude_code/learnings.py +32 -0
- whiletrue/claude_code/panes.py +164 -0
- whiletrue/claude_code/plugin/.claude-plugin/plugin.json +5 -0
- whiletrue/claude_code/plugin/hook.py +44 -0
- whiletrue/claude_code/plugin/hooks/hooks.json +81 -0
- whiletrue/claude_code/plugin/mcp_server.py +98 -0
- whiletrue/claude_code/session.py +345 -0
- whiletrue/claude_code/transcript.py +123 -0
- whiletrue/cli/__init__.py +15 -0
- whiletrue/cli/__main__.py +3 -0
- whiletrue/cli/app.py +550 -0
- whiletrue/cli/common.py +36 -0
- whiletrue/cli/gates.py +156 -0
- whiletrue/cli/ops.py +339 -0
- whiletrue/cli/panel.py +198 -0
- whiletrue/cli/registry.py +143 -0
- whiletrue/cli/templates.py +24 -0
- whiletrue/codex/__init__.py +14 -0
- whiletrue/codex/runner.py +243 -0
- whiletrue/control/__init__.py +38 -0
- whiletrue/control/chain.py +61 -0
- whiletrue/control/loop.py +139 -0
- whiletrue/control/map_.py +101 -0
- whiletrue/control/parallel.py +32 -0
- whiletrue/control/retry.py +100 -0
- whiletrue/control/sequence.py +40 -0
- whiletrue/control/switch.py +67 -0
- whiletrue/core/__init__.py +260 -0
- whiletrue/core/caps.py +139 -0
- whiletrue/core/comments.py +80 -0
- whiletrue/core/composite.py +140 -0
- whiletrue/core/config.py +461 -0
- whiletrue/core/context.py +177 -0
- whiletrue/core/duration.py +19 -0
- whiletrue/core/events.py +94 -0
- whiletrue/core/expr.py +85 -0
- whiletrue/core/history.py +83 -0
- whiletrue/core/lock.py +86 -0
- whiletrue/core/memory_store.py +352 -0
- whiletrue/core/module.py +146 -0
- whiletrue/core/outcomes.py +82 -0
- whiletrue/core/registry.py +129 -0
- whiletrue/core/replay.py +58 -0
- whiletrue/core/runner.py +98 -0
- whiletrue/core/runtime.py +538 -0
- whiletrue/core/source.py +45 -0
- whiletrue/core/store.py +207 -0
- whiletrue/core/template.py +60 -0
- whiletrue/core/validate.py +142 -0
- whiletrue/core/wake.py +117 -0
- whiletrue/core/workspace.py +55 -0
- whiletrue/git/__init__.py +31 -0
- whiletrue/git/modules.py +128 -0
- whiletrue/git/workspaces.py +77 -0
- whiletrue/github/__init__.py +30 -0
- whiletrue/github/client.py +269 -0
- whiletrue/github/filebacked.py +87 -0
- whiletrue/github/memory.py +96 -0
- whiletrue/github/plugin.py +343 -0
- whiletrue/github/specs.py +39 -0
- whiletrue/opencode/__init__.py +25 -0
- whiletrue/opencode/runner.py +186 -0
- whiletrue/py.typed +0 -0
- whiletrue/recipes/issue-loop.yaml +59 -0
- whiletrue/recipes/ralph.yaml +14 -0
- whiletrue/recipes/review-pass.yaml +27 -0
- whiletrue/recipes/review-respond.yaml +22 -0
- whiletrue/store_sqlite/__init__.py +6 -0
- whiletrue/store_sqlite/migrations.py +132 -0
- whiletrue/store_sqlite/sqlite_store.py +537 -0
- whiletrue/tmux/__init__.py +11 -0
- whiletrue/tmux/controller.py +197 -0
- whiletrue-0.1.0.dist-info/METADATA +48 -0
- whiletrue-0.1.0.dist-info/RECORD +98 -0
- whiletrue-0.1.0.dist-info/WHEEL +4 -0
- whiletrue-0.1.0.dist-info/entry_points.txt +2 -0
- whiletrue-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sqlite3
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Literal
|
|
7
|
+
|
|
8
|
+
from whiletrue.core.events import WtEvent
|
|
9
|
+
from whiletrue.core.history import (
|
|
10
|
+
CommentRecord,
|
|
11
|
+
Decision,
|
|
12
|
+
EffectRecord,
|
|
13
|
+
LearningRecord,
|
|
14
|
+
LearningStatus,
|
|
15
|
+
ParkRecord,
|
|
16
|
+
SteerRecord,
|
|
17
|
+
)
|
|
18
|
+
from whiletrue.core.outcomes import ItemState
|
|
19
|
+
from whiletrue.core.store import ItemRecord, RunRecord, StepRecord
|
|
20
|
+
from whiletrue.store_sqlite.migrations import MIGRATIONS
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _dump(value: Any) -> str | None:
|
|
24
|
+
return None if value is None else json.dumps(value)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _load(text: str | None) -> Any:
|
|
28
|
+
return None if text is None else json.loads(text)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class SqliteStore:
|
|
32
|
+
"""`StateStore` backed by SQLite (stdlib `sqlite3`)."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, path: str | Path) -> None:
|
|
35
|
+
"""`path` may be ":memory:". Parent directories are created for file databases."""
|
|
36
|
+
if str(path) != ":memory:":
|
|
37
|
+
Path(path).parent.mkdir(parents=True, exist_ok=True)
|
|
38
|
+
# isolation_level=None: autocommit, so migrations manage their own transactions.
|
|
39
|
+
self._db = sqlite3.connect(str(path), isolation_level=None)
|
|
40
|
+
self._db.row_factory = sqlite3.Row
|
|
41
|
+
self._db.executescript("PRAGMA journal_mode = WAL; PRAGMA foreign_keys = ON;")
|
|
42
|
+
self._migrate()
|
|
43
|
+
|
|
44
|
+
def _migrate(self) -> None:
|
|
45
|
+
self._db.execute(
|
|
46
|
+
"CREATE TABLE IF NOT EXISTS schema_migrations (version INTEGER PRIMARY KEY)"
|
|
47
|
+
)
|
|
48
|
+
current = self.schema_version()
|
|
49
|
+
for index, sql in enumerate(MIGRATIONS):
|
|
50
|
+
version = index + 1
|
|
51
|
+
if version <= current:
|
|
52
|
+
continue
|
|
53
|
+
try:
|
|
54
|
+
self._db.execute("BEGIN")
|
|
55
|
+
for statement in _split_statements(sql):
|
|
56
|
+
self._db.execute(statement)
|
|
57
|
+
self._db.execute("INSERT INTO schema_migrations (version) VALUES (?)", (version,))
|
|
58
|
+
self._db.execute("COMMIT")
|
|
59
|
+
except BaseException:
|
|
60
|
+
self._db.execute("ROLLBACK")
|
|
61
|
+
raise
|
|
62
|
+
|
|
63
|
+
def schema_version(self) -> int:
|
|
64
|
+
row = self._db.execute("SELECT MAX(version) AS v FROM schema_migrations").fetchone()
|
|
65
|
+
return int(row["v"] or 0)
|
|
66
|
+
|
|
67
|
+
def tables(self) -> list[str]:
|
|
68
|
+
rows = self._db.execute(
|
|
69
|
+
"SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' "
|
|
70
|
+
"ORDER BY name"
|
|
71
|
+
).fetchall()
|
|
72
|
+
return [r["name"] for r in rows]
|
|
73
|
+
|
|
74
|
+
# ---- runs ----
|
|
75
|
+
|
|
76
|
+
def create_run(
|
|
77
|
+
self, *, id: str, pipeline_hash: str, started_at: int, lockfile: str | None = None
|
|
78
|
+
) -> None:
|
|
79
|
+
self._db.execute(
|
|
80
|
+
"INSERT INTO runs (id, pipeline_hash, lockfile, status, started_at) "
|
|
81
|
+
"VALUES (?, ?, ?, 'running', ?)",
|
|
82
|
+
(id, pipeline_hash, lockfile, started_at),
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
def finish_run(
|
|
86
|
+
self, id: str, status: Literal["done", "failed", "parked", "cancelled"], ended_at: int
|
|
87
|
+
) -> None:
|
|
88
|
+
self._db.execute(
|
|
89
|
+
"UPDATE runs SET status = ?, ended_at = ? WHERE id = ?", (status, ended_at, id)
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
@staticmethod
|
|
93
|
+
def _run(row: sqlite3.Row | None) -> RunRecord | None:
|
|
94
|
+
if row is None:
|
|
95
|
+
return None
|
|
96
|
+
return RunRecord(
|
|
97
|
+
id=row["id"],
|
|
98
|
+
pipeline_hash=row["pipeline_hash"],
|
|
99
|
+
lockfile=row["lockfile"],
|
|
100
|
+
status=row["status"],
|
|
101
|
+
started_at=row["started_at"],
|
|
102
|
+
ended_at=row["ended_at"],
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
def interrupt_run(self, id: str, ended_at: int) -> None:
|
|
106
|
+
self._db.execute(
|
|
107
|
+
"UPDATE steps SET status = 'errored', error = 'interrupted', ended_at = ? "
|
|
108
|
+
"WHERE run_id = ? AND status = 'running'",
|
|
109
|
+
(ended_at, id),
|
|
110
|
+
)
|
|
111
|
+
self._db.execute(
|
|
112
|
+
"UPDATE items SET state = 'parked' WHERE run_id = ? AND state = 'running'", (id,)
|
|
113
|
+
)
|
|
114
|
+
self.finish_run(id, "parked", ended_at)
|
|
115
|
+
|
|
116
|
+
def get_run(self, id: str) -> RunRecord | None:
|
|
117
|
+
return self._run(self._db.execute("SELECT * FROM runs WHERE id = ?", (id,)).fetchone())
|
|
118
|
+
|
|
119
|
+
def list_runs(self) -> list[RunRecord]:
|
|
120
|
+
rows = self._db.execute("SELECT * FROM runs ORDER BY started_at, rowid").fetchall()
|
|
121
|
+
return [run for run in (self._run(r) for r in rows) if run is not None]
|
|
122
|
+
|
|
123
|
+
def latest_run(self) -> RunRecord | None:
|
|
124
|
+
return self._run(
|
|
125
|
+
self._db.execute(
|
|
126
|
+
"SELECT * FROM runs ORDER BY started_at DESC, rowid DESC LIMIT 1"
|
|
127
|
+
).fetchone()
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# ---- items ----
|
|
131
|
+
|
|
132
|
+
def upsert_item(self, item: ItemRecord) -> None:
|
|
133
|
+
self._db.execute(
|
|
134
|
+
"""INSERT INTO items (run_id, id, source_key, state, payload, depends_on)
|
|
135
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
136
|
+
ON CONFLICT (run_id, id) DO UPDATE SET
|
|
137
|
+
source_key = excluded.source_key, state = excluded.state,
|
|
138
|
+
payload = excluded.payload, depends_on = excluded.depends_on""",
|
|
139
|
+
(
|
|
140
|
+
item.run_id,
|
|
141
|
+
item.id,
|
|
142
|
+
item.source_key,
|
|
143
|
+
item.state,
|
|
144
|
+
_dump(item.payload),
|
|
145
|
+
json.dumps(item.depends_on),
|
|
146
|
+
),
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
def set_item_state(self, run_id: str, item_id: str, state: ItemState) -> None:
|
|
150
|
+
cursor = self._db.execute(
|
|
151
|
+
"UPDATE items SET state = ? WHERE run_id = ? AND id = ?", (state, run_id, item_id)
|
|
152
|
+
)
|
|
153
|
+
if cursor.rowcount == 0:
|
|
154
|
+
raise KeyError(f'No item "{item_id}" in run "{run_id}"')
|
|
155
|
+
|
|
156
|
+
def get_item(self, run_id: str, item_id: str) -> ItemRecord | None:
|
|
157
|
+
return next((i for i in self.list_items(run_id) if i.id == item_id), None)
|
|
158
|
+
|
|
159
|
+
def list_items(self, run_id: str) -> list[ItemRecord]:
|
|
160
|
+
rows = self._db.execute(
|
|
161
|
+
"SELECT * FROM items WHERE run_id = ? ORDER BY rowid", (run_id,)
|
|
162
|
+
).fetchall()
|
|
163
|
+
return [
|
|
164
|
+
ItemRecord(
|
|
165
|
+
id=r["id"],
|
|
166
|
+
run_id=r["run_id"],
|
|
167
|
+
source_key=r["source_key"],
|
|
168
|
+
state=r["state"],
|
|
169
|
+
payload=_load(r["payload"]),
|
|
170
|
+
depends_on=json.loads(r["depends_on"]),
|
|
171
|
+
)
|
|
172
|
+
for r in rows
|
|
173
|
+
]
|
|
174
|
+
|
|
175
|
+
# ---- steps ----
|
|
176
|
+
|
|
177
|
+
def start_step(
|
|
178
|
+
self,
|
|
179
|
+
*,
|
|
180
|
+
run_id: str,
|
|
181
|
+
item_id: str,
|
|
182
|
+
node_path: str,
|
|
183
|
+
attempt: int,
|
|
184
|
+
input: Any,
|
|
185
|
+
started_at: int,
|
|
186
|
+
) -> int:
|
|
187
|
+
cursor = self._db.execute(
|
|
188
|
+
"""INSERT INTO steps (run_id, item_id, node_path, attempt, status, input, started_at)
|
|
189
|
+
VALUES (?, ?, ?, ?, 'running', ?, ?)""",
|
|
190
|
+
(run_id, item_id, node_path, attempt, _dump(input), started_at),
|
|
191
|
+
)
|
|
192
|
+
assert cursor.lastrowid is not None
|
|
193
|
+
return cursor.lastrowid
|
|
194
|
+
|
|
195
|
+
def finish_step(
|
|
196
|
+
self,
|
|
197
|
+
step_id: int,
|
|
198
|
+
*,
|
|
199
|
+
status: Literal["completed", "errored"],
|
|
200
|
+
ended_at: int,
|
|
201
|
+
port: str | None = None,
|
|
202
|
+
output: Any = None,
|
|
203
|
+
cost_usd: float = 0,
|
|
204
|
+
error: str | None = None,
|
|
205
|
+
) -> None:
|
|
206
|
+
cursor = self._db.execute(
|
|
207
|
+
"""UPDATE steps SET status = ?, port = ?, output = ?, cost_usd = ?, error = ?,
|
|
208
|
+
ended_at = ? WHERE id = ?""",
|
|
209
|
+
(status, port, _dump(output), cost_usd, error, ended_at, step_id),
|
|
210
|
+
)
|
|
211
|
+
if cursor.rowcount == 0:
|
|
212
|
+
raise KeyError(f"No step {step_id}")
|
|
213
|
+
|
|
214
|
+
def list_steps(self, run_id: str, item_id: str | None = None) -> list[StepRecord]:
|
|
215
|
+
if item_id is None:
|
|
216
|
+
rows = self._db.execute(
|
|
217
|
+
"SELECT * FROM steps WHERE run_id = ? ORDER BY id", (run_id,)
|
|
218
|
+
).fetchall()
|
|
219
|
+
else:
|
|
220
|
+
rows = self._db.execute(
|
|
221
|
+
"SELECT * FROM steps WHERE run_id = ? AND item_id = ? ORDER BY id",
|
|
222
|
+
(run_id, item_id),
|
|
223
|
+
).fetchall()
|
|
224
|
+
return [
|
|
225
|
+
StepRecord(
|
|
226
|
+
id=r["id"],
|
|
227
|
+
run_id=r["run_id"],
|
|
228
|
+
item_id=r["item_id"],
|
|
229
|
+
node_path=r["node_path"],
|
|
230
|
+
attempt=r["attempt"],
|
|
231
|
+
status=r["status"],
|
|
232
|
+
port=r["port"],
|
|
233
|
+
input=_load(r["input"]),
|
|
234
|
+
output=_load(r["output"]),
|
|
235
|
+
cost_usd=r["cost_usd"],
|
|
236
|
+
error=r["error"],
|
|
237
|
+
started_at=r["started_at"],
|
|
238
|
+
ended_at=r["ended_at"],
|
|
239
|
+
)
|
|
240
|
+
for r in rows
|
|
241
|
+
]
|
|
242
|
+
|
|
243
|
+
# ---- events ----
|
|
244
|
+
|
|
245
|
+
def append_event(self, event: WtEvent) -> None:
|
|
246
|
+
self._db.execute(
|
|
247
|
+
"INSERT INTO events (run_id, item_id, step_id, type, data, ts) "
|
|
248
|
+
"VALUES (?, ?, ?, ?, ?, ?)",
|
|
249
|
+
(
|
|
250
|
+
event.run_id,
|
|
251
|
+
event.item_id,
|
|
252
|
+
event.step_id,
|
|
253
|
+
event.type,
|
|
254
|
+
json.dumps(event.data),
|
|
255
|
+
event.ts,
|
|
256
|
+
),
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
def list_events(
|
|
260
|
+
self, run_id: str, *, item_id: str | None = None, type: str | None = None
|
|
261
|
+
) -> list[WtEvent]:
|
|
262
|
+
where = ["run_id = ?"]
|
|
263
|
+
args: list[Any] = [run_id]
|
|
264
|
+
if item_id is not None:
|
|
265
|
+
where.append("item_id = ?")
|
|
266
|
+
args.append(item_id)
|
|
267
|
+
if type is not None:
|
|
268
|
+
where.append("type = ?")
|
|
269
|
+
args.append(type)
|
|
270
|
+
rows = self._db.execute(
|
|
271
|
+
f"SELECT * FROM events WHERE {' AND '.join(where)} ORDER BY id", args
|
|
272
|
+
).fetchall()
|
|
273
|
+
return [
|
|
274
|
+
WtEvent(
|
|
275
|
+
type=r["type"],
|
|
276
|
+
run_id=r["run_id"],
|
|
277
|
+
item_id=r["item_id"],
|
|
278
|
+
step_id=r["step_id"],
|
|
279
|
+
ts=r["ts"],
|
|
280
|
+
data=json.loads(r["data"]),
|
|
281
|
+
)
|
|
282
|
+
for r in rows
|
|
283
|
+
]
|
|
284
|
+
|
|
285
|
+
def cache_get(self, key: str) -> Any | None:
|
|
286
|
+
row = self._db.execute("SELECT value FROM cache WHERE key = ?", (key,)).fetchone()
|
|
287
|
+
return None if row is None else json.loads(row["value"])
|
|
288
|
+
|
|
289
|
+
def cache_put(self, key: str, value: Any, created_at: int) -> None:
|
|
290
|
+
self._db.execute(
|
|
291
|
+
"INSERT INTO cache (key, value, created_at) VALUES (?, ?, ?) "
|
|
292
|
+
"ON CONFLICT(key) DO UPDATE SET value = excluded.value, "
|
|
293
|
+
"created_at = excluded.created_at",
|
|
294
|
+
(key, json.dumps(value), created_at),
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
# ---- human input and side effects ----
|
|
298
|
+
|
|
299
|
+
@staticmethod
|
|
300
|
+
def _park_row(r: sqlite3.Row) -> ParkRecord:
|
|
301
|
+
return ParkRecord(
|
|
302
|
+
id=r["id"],
|
|
303
|
+
run_id=r["run_id"],
|
|
304
|
+
item_id=r["item_id"],
|
|
305
|
+
node_path=r["node_path"],
|
|
306
|
+
what=r["what"],
|
|
307
|
+
wake=_load(r["wake"]) or {},
|
|
308
|
+
reason=r["reason"] or "",
|
|
309
|
+
created_at=r["created_at"],
|
|
310
|
+
decision=r["decision"],
|
|
311
|
+
notes=r["notes"],
|
|
312
|
+
decided_by=r["decided_by"],
|
|
313
|
+
decided_at=r["decided_at"],
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
def create_park(
|
|
317
|
+
self,
|
|
318
|
+
*,
|
|
319
|
+
run_id: str,
|
|
320
|
+
item_id: str,
|
|
321
|
+
node_path: str,
|
|
322
|
+
what: str,
|
|
323
|
+
wake: dict[str, Any],
|
|
324
|
+
reason: str,
|
|
325
|
+
created_at: int,
|
|
326
|
+
) -> ParkRecord:
|
|
327
|
+
cursor = self._db.execute(
|
|
328
|
+
"""INSERT INTO parks (run_id, item_id, node_path, what, wake, reason, created_at)
|
|
329
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)""",
|
|
330
|
+
(run_id, item_id, node_path, what, json.dumps(wake), reason, created_at),
|
|
331
|
+
)
|
|
332
|
+
return self._park_by_id(cursor.lastrowid or 0)
|
|
333
|
+
|
|
334
|
+
def _park_by_id(self, park_id: int) -> ParkRecord:
|
|
335
|
+
row = self._db.execute("SELECT * FROM parks WHERE id = ?", (park_id,)).fetchone()
|
|
336
|
+
if row is None:
|
|
337
|
+
raise KeyError(f"No park {park_id}")
|
|
338
|
+
return self._park_row(row)
|
|
339
|
+
|
|
340
|
+
def decide_park(
|
|
341
|
+
self,
|
|
342
|
+
park_id: int,
|
|
343
|
+
decision: Decision,
|
|
344
|
+
*,
|
|
345
|
+
decided_by: str,
|
|
346
|
+
decided_at: int,
|
|
347
|
+
notes: str | None,
|
|
348
|
+
) -> None:
|
|
349
|
+
cursor = self._db.execute(
|
|
350
|
+
"UPDATE parks SET decision = ?, decided_by = ?, decided_at = ?, notes = ? WHERE id = ?",
|
|
351
|
+
(decision, decided_by, decided_at, notes, park_id),
|
|
352
|
+
)
|
|
353
|
+
if cursor.rowcount == 0:
|
|
354
|
+
raise KeyError(f"No park {park_id}")
|
|
355
|
+
|
|
356
|
+
def list_parks(
|
|
357
|
+
self,
|
|
358
|
+
item_id: str | None = None,
|
|
359
|
+
*,
|
|
360
|
+
node_path: str | None = None,
|
|
361
|
+
run_id: str | None = None,
|
|
362
|
+
undecided_only: bool = False,
|
|
363
|
+
) -> list[ParkRecord]:
|
|
364
|
+
where: list[str] = []
|
|
365
|
+
args: list[Any] = []
|
|
366
|
+
for column, value in (("item_id", item_id), ("node_path", node_path), ("run_id", run_id)):
|
|
367
|
+
if value is not None:
|
|
368
|
+
where.append(f"{column} = ?")
|
|
369
|
+
args.append(value)
|
|
370
|
+
if undecided_only:
|
|
371
|
+
where.append("decision IS NULL")
|
|
372
|
+
clause = f"WHERE {' AND '.join(where)}" if where else ""
|
|
373
|
+
rows = self._db.execute(f"SELECT * FROM parks {clause} ORDER BY id", args).fetchall()
|
|
374
|
+
return [self._park_row(r) for r in rows]
|
|
375
|
+
|
|
376
|
+
@staticmethod
|
|
377
|
+
def _comment_row(r: sqlite3.Row) -> CommentRecord:
|
|
378
|
+
return CommentRecord(
|
|
379
|
+
id=r["id"],
|
|
380
|
+
item_id=r["item_id"],
|
|
381
|
+
source=r["source"],
|
|
382
|
+
external_id=r["external_id"],
|
|
383
|
+
author=r["author"],
|
|
384
|
+
body=r["body"],
|
|
385
|
+
created_at=r["created_at"],
|
|
386
|
+
thread=r["thread"],
|
|
387
|
+
url=r["url"],
|
|
388
|
+
kind=r["kind"],
|
|
389
|
+
answered=bool(r["answered"]),
|
|
390
|
+
reply=r["reply"],
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
def add_comment(
|
|
394
|
+
self,
|
|
395
|
+
*,
|
|
396
|
+
item_id: str,
|
|
397
|
+
source: str,
|
|
398
|
+
external_id: str,
|
|
399
|
+
author: str,
|
|
400
|
+
body: str,
|
|
401
|
+
created_at: int,
|
|
402
|
+
thread: str | None = None,
|
|
403
|
+
url: str | None = None,
|
|
404
|
+
kind: Literal["human", "reviewer"] = "human",
|
|
405
|
+
) -> CommentRecord | None:
|
|
406
|
+
try:
|
|
407
|
+
cursor = self._db.execute(
|
|
408
|
+
"""INSERT INTO comments
|
|
409
|
+
(item_id, source, external_id, thread, author, body, url, kind, created_at)
|
|
410
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
|
411
|
+
(item_id, source, external_id, thread, author, body, url, kind, created_at),
|
|
412
|
+
)
|
|
413
|
+
except sqlite3.IntegrityError:
|
|
414
|
+
return None
|
|
415
|
+
row = self._db.execute(
|
|
416
|
+
"SELECT * FROM comments WHERE id = ?", (cursor.lastrowid,)
|
|
417
|
+
).fetchone()
|
|
418
|
+
return self._comment_row(row)
|
|
419
|
+
|
|
420
|
+
def list_comments(self, item_id: str, *, unanswered_only: bool = False) -> list[CommentRecord]:
|
|
421
|
+
extra = " AND answered = 0" if unanswered_only else ""
|
|
422
|
+
rows = self._db.execute(
|
|
423
|
+
f"SELECT * FROM comments WHERE item_id = ?{extra} ORDER BY id", (item_id,)
|
|
424
|
+
).fetchall()
|
|
425
|
+
return [self._comment_row(r) for r in rows]
|
|
426
|
+
|
|
427
|
+
def answer_comment(self, comment_id: int, reply: str) -> None:
|
|
428
|
+
cursor = self._db.execute(
|
|
429
|
+
"UPDATE comments SET answered = 1, reply = ? WHERE id = ?", (reply, comment_id)
|
|
430
|
+
)
|
|
431
|
+
if cursor.rowcount == 0:
|
|
432
|
+
raise KeyError(f"No comment {comment_id}")
|
|
433
|
+
|
|
434
|
+
def add_steer(
|
|
435
|
+
self, *, run_id: str, item_id: str, step: str, prompt: str, created_at: int
|
|
436
|
+
) -> SteerRecord:
|
|
437
|
+
cursor = self._db.execute(
|
|
438
|
+
"INSERT INTO steers (run_id, item_id, step, prompt, created_at) VALUES (?, ?, ?, ?, ?)",
|
|
439
|
+
(run_id, item_id, step, prompt, created_at),
|
|
440
|
+
)
|
|
441
|
+
return SteerRecord(cursor.lastrowid or 0, run_id, item_id, step, prompt, created_at)
|
|
442
|
+
|
|
443
|
+
def list_steers(self, item_id: str) -> list[SteerRecord]:
|
|
444
|
+
rows = self._db.execute(
|
|
445
|
+
"SELECT * FROM steers WHERE item_id = ? ORDER BY id", (item_id,)
|
|
446
|
+
).fetchall()
|
|
447
|
+
return [
|
|
448
|
+
SteerRecord(r["id"], r["run_id"], r["item_id"], r["step"], r["prompt"], r["created_at"])
|
|
449
|
+
for r in rows
|
|
450
|
+
]
|
|
451
|
+
|
|
452
|
+
def effect_get(self, key: str) -> EffectRecord | None:
|
|
453
|
+
row = self._db.execute("SELECT * FROM effects WHERE key = ?", (key,)).fetchone()
|
|
454
|
+
if row is None:
|
|
455
|
+
return None
|
|
456
|
+
return EffectRecord(
|
|
457
|
+
row["key"], row["status"], row["run_id"], row["created_at"], _load(row["external_ref"])
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
def effect_put(
|
|
461
|
+
self,
|
|
462
|
+
key: str,
|
|
463
|
+
*,
|
|
464
|
+
run_id: str,
|
|
465
|
+
status: Literal["started", "done"],
|
|
466
|
+
ref: Any,
|
|
467
|
+
created_at: int,
|
|
468
|
+
) -> None:
|
|
469
|
+
self._db.execute(
|
|
470
|
+
"""INSERT INTO effects (key, run_id, status, external_ref, created_at)
|
|
471
|
+
VALUES (?, ?, ?, ?, ?)
|
|
472
|
+
ON CONFLICT(key) DO UPDATE SET run_id = excluded.run_id, status = excluded.status,
|
|
473
|
+
external_ref = excluded.external_ref""",
|
|
474
|
+
(key, run_id, status, _dump(ref), created_at),
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
@staticmethod
|
|
478
|
+
def _learning_row(r: sqlite3.Row) -> LearningRecord:
|
|
479
|
+
return LearningRecord(
|
|
480
|
+
id=r["id"],
|
|
481
|
+
scope=r["scope"],
|
|
482
|
+
note=r["note"],
|
|
483
|
+
status=r["status"],
|
|
484
|
+
created_at=r["created_at"],
|
|
485
|
+
run_id=r["run_id"],
|
|
486
|
+
provenance=_load(r["provenance"]) or {},
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
def add_learning(
|
|
490
|
+
self,
|
|
491
|
+
*,
|
|
492
|
+
scope: str,
|
|
493
|
+
note: str,
|
|
494
|
+
status: LearningStatus,
|
|
495
|
+
created_at: int,
|
|
496
|
+
run_id: str | None = None,
|
|
497
|
+
provenance: dict[str, Any] | None = None,
|
|
498
|
+
) -> LearningRecord:
|
|
499
|
+
cursor = self._db.execute(
|
|
500
|
+
"""INSERT INTO learnings (run_id, scope, note, provenance, created_at, status)
|
|
501
|
+
VALUES (?, ?, ?, ?, ?, ?)""",
|
|
502
|
+
(run_id, scope, note, _dump(provenance or {}), created_at, status),
|
|
503
|
+
)
|
|
504
|
+
row = self._db.execute(
|
|
505
|
+
"SELECT * FROM learnings WHERE id = ?", (cursor.lastrowid,)
|
|
506
|
+
).fetchone()
|
|
507
|
+
return self._learning_row(row)
|
|
508
|
+
|
|
509
|
+
def list_learnings(
|
|
510
|
+
self, *, scope: str | None = None, status: LearningStatus | None = None
|
|
511
|
+
) -> list[LearningRecord]:
|
|
512
|
+
where: list[str] = []
|
|
513
|
+
args: list[Any] = []
|
|
514
|
+
if scope is not None:
|
|
515
|
+
where.append("scope = ?")
|
|
516
|
+
args.append(scope)
|
|
517
|
+
if status is not None:
|
|
518
|
+
where.append("status = ?")
|
|
519
|
+
args.append(status)
|
|
520
|
+
clause = f"WHERE {' AND '.join(where)}" if where else ""
|
|
521
|
+
rows = self._db.execute(f"SELECT * FROM learnings {clause} ORDER BY id", args).fetchall()
|
|
522
|
+
return [self._learning_row(r) for r in rows]
|
|
523
|
+
|
|
524
|
+
def set_learning_status(self, learning_id: int, status: LearningStatus) -> None:
|
|
525
|
+
cursor = self._db.execute(
|
|
526
|
+
"UPDATE learnings SET status = ? WHERE id = ?", (status, learning_id)
|
|
527
|
+
)
|
|
528
|
+
if cursor.rowcount == 0:
|
|
529
|
+
raise KeyError(f"No learning {learning_id}")
|
|
530
|
+
|
|
531
|
+
def close(self) -> None:
|
|
532
|
+
self._db.close()
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def _split_statements(sql: str) -> list[str]:
|
|
536
|
+
"""Splits a migration script into statements (our migrations contain no `;` in literals)."""
|
|
537
|
+
return [s.strip() for s in sql.split(";") if s.strip()]
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""tmux session, window and pane management (the TUI's foundation)."""
|
|
2
|
+
|
|
3
|
+
from whiletrue.tmux.controller import (
|
|
4
|
+
ExecResult,
|
|
5
|
+
PaneRef,
|
|
6
|
+
TmuxController,
|
|
7
|
+
TmuxError,
|
|
8
|
+
session_name,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
__all__ = ["ExecResult", "PaneRef", "TmuxController", "TmuxError", "session_name"]
|