M0001_Initial.vala 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527
  1. using InvercargillSql.Migrations;
  2. namespace ClankerSaddle.Data.Migrations {
  3. /**
  4. * The full ClankerSaddle schema in one migration (plan 11: every
  5. * earlier migration flattened — the database is not deployed, so
  6. * tear-down-and-recreate replaced incremental evolution). Covers:
  7. * projects and their links; the plan-12 goals (with links —
  8. * plan 15 C1 deleted `goal_blocks` and C2 deleted `goal_comments`:
  9. * the goal agent's blocks and comments ride the run task's own
  10. * rows) and the
  11. * flattened task shape (`goal_id`, `kind`, `mode`,
  12. * `creator`, `enqueue_after_task_id` — the subtask machinery's
  13. * parent_task_id/plan/auto-* columns and the task_dependencies
  14. * table are gone); tasks with comments, typed blocks, progress
  15. * and the draft-lifecycle columns (`status` defaults to
  16. * 'draft'); saddle registrations with git identity
  17. * and the per-saddle token; the session + command + raw-log audit
  18. * trail; the plan-13 daemon-global entry dedup
  19. * (`saddle_entry_dedup`); rules and their attachments; project
  20. * notes; capability tags; and the plan-11 notification tables for
  21. * task E
  22. * (`push_subscriptions`, `task_notify_prefs` — included now so
  23. * D+E needs no migration).
  24. */
  25. public class M0001_Initial : Migration {
  26. public override string name { get { return "Create the ClankerSaddle schema"; } }
  27. public override uint64 serial { get { return 1; } }
  28. public override string migration_namespace { get { return "clanker-saddle"; } }
  29. public override void up(MigrationBuilder b) {
  30. b.create_table("projects", (t) => {
  31. t.column<int64?>("id")
  32. .auto_increment()
  33. .primary_key();
  34. t.column<string>("name")
  35. .not_null();
  36. t.column<string>("git_url")
  37. .not_null();
  38. t.column<string>("default_branch")
  39. .default_value<string>("main");
  40. t.column<string>("summary");
  41. t.column<DateTime?>("created_at");
  42. });
  43. b.create_table("project_links", (t) => {
  44. t.column<int64?>("id")
  45. .auto_increment()
  46. .primary_key();
  47. t.column<int64?>("parent_project_id")
  48. .not_null()
  49. .references("projects", "id");
  50. t.column<int64?>("linked_project_id")
  51. .not_null()
  52. .references("projects", "id");
  53. t.column<string>("relationship")
  54. .not_null();
  55. });
  56. // Plan 12 goals: user-created units of agent-orchestrated
  57. // work. The brief locks once started; `branch_name` is the
  58. // user-named goal branch (uniqueness-validated against the
  59. // other live goals sharing involved repos); `plan` holds
  60. // the APPROVED plan markdown (rejected revisions live on
  61. // as the run task's answered plan blocks); `last_seen_event_id`
  62. // is the goal agent's consumed-event watermark (resume
  63. // coalescing); `branch_push_state` caches the
  64. // daemon-reported push state of the goal branches.
  65. b.create_table("goals", (t) => {
  66. t.column<int64?>("id")
  67. .auto_increment()
  68. .primary_key();
  69. t.column<int64?>("project_id")
  70. .not_null()
  71. .references("projects", "id")
  72. .indexed();
  73. t.column<string>("title")
  74. .not_null();
  75. t.column<string>("brief")
  76. .not_null();
  77. t.column<string>("branch_name")
  78. .not_null();
  79. t.column<string>("base_branch");
  80. t.column<string>("status")
  81. .default_value<string>("draft")
  82. .indexed();
  83. t.column<int64?>("priority")
  84. .default_value<int64?>(3);
  85. t.column<string>("plan");
  86. t.column<string>("completion_report");
  87. t.column<string>("creator")
  88. .default_value<string>("human");
  89. t.column<int64?>("last_seen_event_id")
  90. .default_value<int64?>(0);
  91. t.column<string>("branch_push_state");
  92. t.column<DateTime?>("created_at");
  93. t.column<DateTime?>("started_at");
  94. t.column<DateTime?>("completed_at");
  95. t.column<DateTime?>("paused_at");
  96. });
  97. b.create_table("goal_links", (t) => {
  98. t.column<int64?>("id")
  99. .auto_increment()
  100. .primary_key();
  101. t.column<int64?>("goal_id")
  102. .not_null()
  103. .references("goals", "id")
  104. .indexed();
  105. t.column<int64?>("project_id")
  106. .not_null()
  107. .references("projects", "id")
  108. .indexed();
  109. t.column<string>("relationship")
  110. .not_null();
  111. });
  112. b.create_table("tasks", (t) => {
  113. t.column<int64?>("id")
  114. .auto_increment()
  115. .primary_key();
  116. t.column<int64?>("project_id")
  117. .not_null()
  118. .references("projects", "id")
  119. .indexed();
  120. // Plan 12: goals own their task graph — a task links to
  121. // its goal (null = standalone), runs as a plain worker
  122. // or a goal agent (kind), carries a work mode, records
  123. // its creator (enqueue is creator-only) and —
  124. // human-created tasks only — an auto-enqueue
  125. // predecessor.
  126. t.column<int64?>("goal_id")
  127. .references("goals", "id")
  128. .indexed();
  129. t.column<string>("kind")
  130. .default_value<string>("task");
  131. t.column<string>("mode")
  132. .default_value<string>("implement");
  133. t.column<string>("creator")
  134. .default_value<string>("human");
  135. t.column<int64?>("enqueue_after_task_id")
  136. .references("tasks", "id");
  137. t.column<string>("title")
  138. .not_null();
  139. t.column<string>("description");
  140. // Plan 11 draft lifecycle: new tasks start in draft and
  141. // only the human Enqueue action queues them.
  142. t.column<string>("status")
  143. .default_value<string>("draft")
  144. .indexed();
  145. t.column<int64?>("priority")
  146. .default_value<int64?>(5);
  147. t.column<string>("working_branch");
  148. t.column<string>("summary");
  149. t.column<string>("task_branch");
  150. t.column<string>("branch_from");
  151. t.column<DateTime?>("created_at");
  152. t.column<DateTime?>("started_at");
  153. t.column<DateTime?>("completed_at");
  154. t.column<DateTime?>("suspended_at");
  155. });
  156. b.create_table("task_comments", (t) => {
  157. t.column<int64?>("id")
  158. .auto_increment()
  159. .primary_key();
  160. t.column<int64?>("task_id")
  161. .not_null()
  162. .references("tasks", "id")
  163. .indexed();
  164. t.column<string>("author")
  165. .not_null();
  166. t.column<string>("body")
  167. .not_null();
  168. t.column<string>("quote");
  169. t.column<DateTime?>("created_at");
  170. });
  171. b.create_table("task_blocks", (t) => {
  172. t.column<int64?>("id")
  173. .auto_increment()
  174. .primary_key();
  175. t.column<int64?>("task_id")
  176. .not_null()
  177. .references("tasks", "id")
  178. .indexed();
  179. t.column<string>("question")
  180. .not_null();
  181. t.column<string>("answer");
  182. t.column<string>("type")
  183. .default_value<string>("question");
  184. t.column<string>("options");
  185. t.column<string>("body");
  186. t.column<DateTime?>("answered_at");
  187. t.column<string>("answered_by");
  188. t.column<DateTime?>("created_at");
  189. });
  190. b.create_table("task_progress", (t) => {
  191. t.column<int64?>("id")
  192. .auto_increment()
  193. .primary_key();
  194. t.column<int64?>("task_id")
  195. .not_null()
  196. .references("tasks", "id")
  197. .indexed();
  198. t.column<string>("summary")
  199. .not_null();
  200. t.column<DateTime?>("created_at");
  201. });
  202. // Plan 15 C1/C2: the goal-side block and comment tables
  203. // are GONE — the goal agent's questions, plans and
  204. // completion reports ride the RUN TASK's task_blocks rows
  205. // (one row per question, answered in place; no dual-row
  206. // pair can fall out of sync), and its comment thread IS
  207. // the run task's task_comments (human comments inject
  208. // through the existing delivery paths).
  209. // The supervision narrative's event log: one row per
  210. // notable goal occurrence (plan reviews, agent task
  211. // operations, task terminal events, lifecycle moves) —
  212. // the resume machinery's watermark (`goals.
  213. // last_seen_event_id`) also lives on these ids.
  214. b.create_table("goal_events", (t) => {
  215. t.column<int64?>("id")
  216. .auto_increment()
  217. .primary_key();
  218. t.column<int64?>("goal_id")
  219. .not_null()
  220. .references("goals", "id")
  221. .indexed();
  222. t.column<string>("kind")
  223. .not_null();
  224. t.column<string>("text")
  225. .not_null();
  226. t.column<int64?>("task_id");
  227. t.column<DateTime?>("created_at");
  228. });
  229. b.create_table("saddles", (t) => {
  230. t.column<int64?>("id")
  231. .auto_increment()
  232. .primary_key();
  233. t.column<string>("name")
  234. .not_null();
  235. t.column<string>("container_id");
  236. t.column<string>("host")
  237. .not_null();
  238. t.column<string>("status")
  239. .default_value<string>("offline");
  240. t.column<int64?>("current_task_id")
  241. .references("tasks", "id");
  242. t.column<DateTime?>("allocated_at");
  243. t.column<string>("git_name");
  244. t.column<string>("git_email");
  245. t.column<string>("ssh_public_key");
  246. t.column<string>("token");
  247. });
  248. b.create_table("sessions", (t) => {
  249. t.column<int64?>("id")
  250. .auto_increment()
  251. .primary_key();
  252. t.column<int64?>("task_id")
  253. .not_null()
  254. .references("tasks", "id")
  255. .indexed();
  256. t.column<int64?>("saddle_id")
  257. .not_null()
  258. .references("saddles", "id");
  259. t.column<DateTime?>("started_at");
  260. t.column<DateTime?>("ended_at");
  261. t.column<string>("outcome");
  262. // Plan 14 B4: what a RESUMED session's agent first
  263. // received — the resume_notice the allocation built,
  264. // stored on the session row at resume-allocation (null
  265. // for fresh sessions; the terminal page shows it above
  266. // the log).
  267. t.column<string>("resume_notice");
  268. });
  269. b.create_table("session_commands", (t) => {
  270. t.column<int64?>("id")
  271. .auto_increment()
  272. .primary_key();
  273. t.column<int64?>("session_id")
  274. .not_null()
  275. .references("sessions", "id")
  276. .indexed();
  277. t.column<string>("command_text")
  278. .not_null();
  279. t.column<string>("output_text");
  280. t.column<int64?>("exit_code");
  281. t.column<string>("type")
  282. .default_value<string>("command");
  283. t.column<string>("piped_input");
  284. t.column<DateTime?>("started_at");
  285. t.column<DateTime?>("completed_at");
  286. t.column<int64?>("duration_ms");
  287. });
  288. // Plan 13 B1: the daemon-global entry dedup — every pushed
  289. // entry id this saddle has already ingested. The unique
  290. // (saddle_id, entry_id) pair is the replay-integrity key: a
  291. // redelivered entry under ANY session id (web restart,
  292. // session churn, a mis-keyed send) finds its row here and is
  293. // consumed silently.
  294. b.create_table("saddle_entry_dedup", (t) => {
  295. t.column<int64?>("id")
  296. .auto_increment()
  297. .primary_key();
  298. t.column<int64?>("saddle_id")
  299. .not_null()
  300. .references("saddles", "id")
  301. .indexed();
  302. t.column<int64?>("entry_id")
  303. .not_null();
  304. t.unique("uq_saddle_entry_dedup_pair", {"saddle_id", "entry_id"});
  305. });
  306. b.create_table("api_tokens", (t) => {
  307. t.column<int64?>("id")
  308. .auto_increment()
  309. .primary_key();
  310. t.column<int64?>("saddle_id")
  311. .not_null()
  312. .references("saddles", "id")
  313. .indexed();
  314. t.column<string>("token")
  315. .not_null()
  316. .unique();
  317. t.column<DateTime?>("created_at");
  318. });
  319. b.create_table("rules", (t) => {
  320. t.column<int64?>("id")
  321. .auto_increment()
  322. .primary_key();
  323. t.column<string>("name")
  324. .not_null()
  325. .unique();
  326. t.column<string>("body")
  327. .not_null();
  328. t.column<DateTime?>("created_at");
  329. // Bumped on edit — the resume-time context_update
  330. // delta keys on it (a rule edited while a workspace
  331. // was parked must reach the agent as a turn).
  332. t.column<DateTime?>("updated_at");
  333. });
  334. // The attachment timestamps (created_at) feed the same
  335. // delta: attaching an existing rule to a task or project
  336. // changes the effective instruction set without touching
  337. // the rule row.
  338. b.create_table("rule_projects", (t) => {
  339. t.column<int64?>("id")
  340. .auto_increment()
  341. .primary_key();
  342. t.column<int64?>("rule_id")
  343. .not_null()
  344. .references("rules", "id")
  345. .indexed();
  346. t.column<int64?>("project_id")
  347. .not_null()
  348. .references("projects", "id")
  349. .indexed();
  350. t.column<DateTime?>("created_at");
  351. t.unique("uq_rule_projects_pair", {"rule_id", "project_id"});
  352. });
  353. b.create_table("task_rules", (t) => {
  354. t.column<int64?>("id")
  355. .auto_increment()
  356. .primary_key();
  357. t.column<int64?>("rule_id")
  358. .not_null()
  359. .references("rules", "id")
  360. .indexed();
  361. t.column<int64?>("task_id")
  362. .not_null()
  363. .references("tasks", "id")
  364. .indexed();
  365. t.column<DateTime?>("created_at");
  366. t.unique("uq_task_rules_pair", {"rule_id", "task_id"});
  367. });
  368. b.create_table("project_notes", (t) => {
  369. t.column<int64?>("id")
  370. .auto_increment()
  371. .primary_key();
  372. t.column<int64?>("project_id")
  373. .not_null()
  374. .references("projects", "id")
  375. .indexed();
  376. t.column<int64?>("source_task_id")
  377. .references("tasks", "id");
  378. t.column<string>("body")
  379. .not_null();
  380. t.column<DateTime?>("created_at");
  381. });
  382. b.create_table("session_raw_log", (t) => {
  383. t.column<int64?>("id")
  384. .auto_increment()
  385. .primary_key();
  386. t.column<int64?>("session_id")
  387. .not_null()
  388. .references("sessions", "id")
  389. .indexed();
  390. t.column<int64?>("seq")
  391. .not_null();
  392. t.column<string>("chunk")
  393. .not_null();
  394. t.unique("uq_session_raw_log_seq", {"session_id", "seq"});
  395. });
  396. b.create_table("capabilities", (t) => {
  397. t.column<int64?>("id")
  398. .auto_increment()
  399. .primary_key();
  400. t.column<string>("name")
  401. .not_null()
  402. .unique();
  403. t.column<DateTime?>("created_at");
  404. });
  405. b.create_table("saddle_capabilities", (t) => {
  406. t.column<int64?>("id")
  407. .auto_increment()
  408. .primary_key();
  409. t.column<int64?>("capability_id")
  410. .not_null()
  411. .references("capabilities", "id")
  412. .indexed();
  413. t.column<int64?>("saddle_id")
  414. .not_null()
  415. .references("saddles", "id")
  416. .indexed();
  417. t.unique("uq_saddle_capabilities_pair", {"capability_id", "saddle_id"});
  418. });
  419. b.create_table("task_capabilities", (t) => {
  420. t.column<int64?>("id")
  421. .auto_increment()
  422. .primary_key();
  423. t.column<int64?>("capability_id")
  424. .not_null()
  425. .references("capabilities", "id")
  426. .indexed();
  427. t.column<int64?>("task_id")
  428. .not_null()
  429. .references("tasks", "id")
  430. .indexed();
  431. t.unique("uq_task_capabilities_pair", {"capability_id", "task_id"});
  432. });
  433. // Plan 11 task E: Web Push subscriptions, one row per
  434. // browser endpoint a user registered (the VAPID keypair is
  435. // generated at startup and persisted in the config dir, not
  436. // here). `user_id` references the Spry users table by value
  437. // without an FK — the auth schema owns that table.
  438. b.create_table("push_subscriptions", (t) => {
  439. t.column<int64?>("id")
  440. .auto_increment()
  441. .primary_key();
  442. t.column<int64?>("user_id")
  443. .not_null()
  444. .indexed();
  445. t.column<string>("endpoint")
  446. .not_null()
  447. .unique();
  448. t.column<string>("p256dh")
  449. .not_null();
  450. t.column<string>("auth")
  451. .not_null();
  452. t.column<DateTime?>("created_at");
  453. });
  454. // Plan 11 task E: a user's per-task "Notify me" opt-in for
  455. // the task's first start, failure and completion (block
  456. // events notify all push-subscribed users globally).
  457. b.create_table("task_notify_prefs", (t) => {
  458. t.column<int64?>("id")
  459. .auto_increment()
  460. .primary_key();
  461. t.column<int64?>("task_id")
  462. .not_null()
  463. .references("tasks", "id")
  464. .indexed();
  465. t.column<int64?>("user_id")
  466. .not_null()
  467. .indexed();
  468. t.unique("uq_task_notify_prefs_pair", {"task_id", "user_id"});
  469. });
  470. }
  471. public override void down(MigrationBuilder b) {
  472. b.drop_table("saddle_entry_dedup");
  473. b.drop_table("goal_events");
  474. b.drop_table("task_notify_prefs");
  475. b.drop_table("push_subscriptions");
  476. b.drop_table("task_capabilities");
  477. b.drop_table("saddle_capabilities");
  478. b.drop_table("capabilities");
  479. b.drop_table("session_raw_log");
  480. b.drop_table("project_notes");
  481. b.drop_table("task_rules");
  482. b.drop_table("rule_projects");
  483. b.drop_table("rules");
  484. b.drop_table("api_tokens");
  485. b.drop_table("session_commands");
  486. b.drop_table("sessions");
  487. b.drop_table("saddles");
  488. b.drop_table("task_progress");
  489. b.drop_table("task_blocks");
  490. b.drop_table("task_comments");
  491. b.drop_table("tasks");
  492. b.drop_table("goal_links");
  493. b.drop_table("goals");
  494. b.drop_table("project_links");
  495. b.drop_table("projects");
  496. }
  497. }
  498. }