contextgit 0.0.1 → 0.0.2

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 (83) hide show
  1. package/.claude/settings.local.json +41 -0
  2. package/.contextgit/config.json +10 -0
  3. package/.contextgit/system-prompt.md +4 -0
  4. package/.github/workflows/contextgit-ci.yml +40 -0
  5. package/CLAUDE.md +123 -0
  6. package/CLAUDE.md.next +65 -0
  7. package/docs/ContextGit_ARCHITECTURE_v3.md +1141 -0
  8. package/docs/ContextGit_DELTA.md +84 -0
  9. package/docs/ContextGit_PHASE1_PLAN.md +177 -0
  10. package/docs/ContextGit_PHASE2_PLAN.md +535 -0
  11. package/docs/ContextGit_PRD_v4.md +488 -0
  12. package/docs/decisions.md +370 -0
  13. package/package.json +23 -8
  14. package/packages/api/package.json +25 -0
  15. package/packages/api/src/bootstrap.ts +64 -0
  16. package/packages/api/src/config.ts +45 -0
  17. package/packages/api/src/index.ts +17 -0
  18. package/packages/api/src/middleware/auth.test.ts +83 -0
  19. package/packages/api/src/middleware/auth.ts +41 -0
  20. package/packages/api/src/remote-store.test.ts +301 -0
  21. package/packages/api/src/router.ts +121 -0
  22. package/packages/api/src/server-config.ts +34 -0
  23. package/packages/api/src/server.ts +38 -0
  24. package/packages/api/src/store-router.ts +241 -0
  25. package/packages/api/tsconfig.json +8 -0
  26. package/packages/cli/bin/run.js +4 -0
  27. package/packages/cli/package.json +29 -0
  28. package/packages/cli/src/bootstrap.ts +68 -0
  29. package/packages/cli/src/commands/branch.ts +58 -0
  30. package/packages/cli/src/commands/claim.ts +58 -0
  31. package/packages/cli/src/commands/commit.ts +79 -0
  32. package/packages/cli/src/commands/context.ts +46 -0
  33. package/packages/cli/src/commands/doctor.ts +99 -0
  34. package/packages/cli/src/commands/init.ts +141 -0
  35. package/packages/cli/src/commands/keygen.ts +65 -0
  36. package/packages/cli/src/commands/log.ts +103 -0
  37. package/packages/cli/src/commands/merge.ts +36 -0
  38. package/packages/cli/src/commands/pull.ts +145 -0
  39. package/packages/cli/src/commands/push.ts +158 -0
  40. package/packages/cli/src/commands/remote-show.ts +87 -0
  41. package/packages/cli/src/commands/search.ts +54 -0
  42. package/packages/cli/src/commands/serve.ts +61 -0
  43. package/packages/cli/src/commands/set-remote.ts +30 -0
  44. package/packages/cli/src/commands/status.ts +62 -0
  45. package/packages/cli/src/commands/unclaim.ts +28 -0
  46. package/packages/cli/src/config.ts +64 -0
  47. package/packages/cli/src/git-hooks.ts +61 -0
  48. package/packages/cli/tsconfig.json +9 -0
  49. package/packages/core/package.json +28 -0
  50. package/packages/core/src/embeddings.test.ts +58 -0
  51. package/packages/core/src/embeddings.ts +75 -0
  52. package/packages/core/src/engine.ts +274 -0
  53. package/packages/core/src/index.ts +6 -0
  54. package/packages/core/src/snapshot.ts +82 -0
  55. package/packages/core/src/summarizer.test.ts +120 -0
  56. package/packages/core/src/summarizer.ts +113 -0
  57. package/packages/core/src/threads.ts +29 -0
  58. package/packages/core/src/types.ts +240 -0
  59. package/packages/core/tsconfig.json +9 -0
  60. package/packages/mcp/package.json +31 -0
  61. package/packages/mcp/src/auto-snapshot.ts +83 -0
  62. package/packages/mcp/src/config.ts +53 -0
  63. package/packages/mcp/src/git-sync.ts +94 -0
  64. package/packages/mcp/src/index.ts +19 -0
  65. package/packages/mcp/src/server.ts +377 -0
  66. package/packages/mcp/tsconfig.json +9 -0
  67. package/packages/store/package.json +30 -0
  68. package/packages/store/src/branch-merge.test.ts +127 -0
  69. package/packages/store/src/engine-integration.test.ts +93 -0
  70. package/packages/store/src/index.ts +3 -0
  71. package/packages/store/src/interface.ts +62 -0
  72. package/packages/store/src/local/claims.test.ts +190 -0
  73. package/packages/store/src/local/index.ts +380 -0
  74. package/packages/store/src/local/local-store.test.ts +164 -0
  75. package/packages/store/src/local/migrations.ts +99 -0
  76. package/packages/store/src/local/queries.ts +760 -0
  77. package/packages/store/src/local/schema.ts +157 -0
  78. package/packages/store/src/remote/index.ts +300 -0
  79. package/packages/store/tsconfig.json +9 -0
  80. package/pnpm-workspace.yaml +2 -0
  81. package/scripts/build.sh +28 -0
  82. package/tsconfig.base.json +14 -0
  83. package/vitest.config.ts +15 -0
@@ -0,0 +1,760 @@
1
+ // All SQL queries for LocalStore.
2
+ // Dates are stored as INTEGER (Unix ms) and converted to/from Date at this layer.
3
+ // IDs are generated by the caller (nanoid).
4
+
5
+ import type { Database, Statement } from 'better-sqlite3'
6
+ import type {
7
+ Agent,
8
+ AgentInput,
9
+ Branch,
10
+ BranchInput,
11
+ Claim,
12
+ ClaimInput,
13
+ Commit,
14
+ CommitInput,
15
+ Pagination,
16
+ Project,
17
+ ProjectInput,
18
+ SearchResult,
19
+ SessionSnapshot,
20
+ Thread,
21
+ } from '@contextgit/core'
22
+
23
+ // ─── Row types (SQLite column names → snake_case) ───────────────────────────
24
+
25
+ interface ProjectRow {
26
+ id: string
27
+ name: string
28
+ description: string | null
29
+ github_url: string | null
30
+ created_at: number
31
+ }
32
+
33
+ interface BranchRow {
34
+ id: string
35
+ project_id: string
36
+ name: string
37
+ git_branch: string
38
+ github_pr_url: string | null
39
+ parent_branch_id: string | null
40
+ head_commit_id: string | null
41
+ status: string
42
+ created_at: number
43
+ merged_at: number | null
44
+ }
45
+
46
+ interface CommitRow {
47
+ id: string
48
+ branch_id: string
49
+ parent_id: string | null
50
+ merge_source_branch_id: string | null
51
+ agent_id: string
52
+ agent_role: string
53
+ tool: string
54
+ workflow_type: string
55
+ loop_iteration: number | null
56
+ ci_run_id: string | null
57
+ pipeline_name: string | null
58
+ message: string
59
+ content: string
60
+ summary: string
61
+ commit_type: string
62
+ git_commit_sha: string | null
63
+ created_at: number
64
+ }
65
+
66
+ interface ThreadRow {
67
+ id: string
68
+ project_id: string
69
+ branch_id: string
70
+ description: string
71
+ status: string
72
+ workflow_type: string | null
73
+ opened_in_commit: string
74
+ closed_in_commit: string | null
75
+ closed_note: string | null
76
+ created_at: number
77
+ }
78
+
79
+ interface AgentRow {
80
+ id: string
81
+ project_id: string
82
+ role: string
83
+ tool: string
84
+ workflow_type: string
85
+ display_name: string | null
86
+ total_commits: number
87
+ last_seen: number
88
+ created_at: number
89
+ }
90
+
91
+ interface ClaimRow {
92
+ id: string
93
+ project_id: string
94
+ branch_id: string
95
+ task: string
96
+ agent_id: string
97
+ role: string
98
+ claimed_at: number
99
+ status: string
100
+ ttl: number
101
+ released_at: number | null
102
+ }
103
+
104
+ // ─── Row → domain type converters ───────────────────────────────────────────
105
+
106
+ function toProject(row: ProjectRow): Project {
107
+ return {
108
+ id: row.id,
109
+ name: row.name,
110
+ description: row.description ?? undefined,
111
+ githubUrl: row.github_url ?? undefined,
112
+ createdAt: new Date(row.created_at),
113
+ }
114
+ }
115
+
116
+ function toBranch(row: BranchRow): Branch {
117
+ return {
118
+ id: row.id,
119
+ projectId: row.project_id,
120
+ name: row.name,
121
+ gitBranch: row.git_branch,
122
+ githubPrUrl: row.github_pr_url ?? undefined,
123
+ parentBranchId: row.parent_branch_id ?? undefined,
124
+ headCommitId: row.head_commit_id ?? undefined,
125
+ status: row.status as Branch['status'],
126
+ createdAt: new Date(row.created_at),
127
+ mergedAt: row.merged_at ? new Date(row.merged_at) : undefined,
128
+ }
129
+ }
130
+
131
+ function toCommit(row: CommitRow): Commit {
132
+ return {
133
+ id: row.id,
134
+ branchId: row.branch_id,
135
+ parentId: row.parent_id ?? undefined,
136
+ mergeSourceBranchId: row.merge_source_branch_id ?? undefined,
137
+ agentId: row.agent_id,
138
+ agentRole: row.agent_role as Commit['agentRole'],
139
+ tool: row.tool,
140
+ workflowType: row.workflow_type as Commit['workflowType'],
141
+ loopIteration: row.loop_iteration ?? undefined,
142
+ ciRunId: row.ci_run_id ?? undefined,
143
+ pipelineName: row.pipeline_name ?? undefined,
144
+ message: row.message,
145
+ content: row.content,
146
+ summary: row.summary,
147
+ commitType: row.commit_type as Commit['commitType'],
148
+ gitCommitSha: row.git_commit_sha ?? undefined,
149
+ createdAt: new Date(row.created_at),
150
+ }
151
+ }
152
+
153
+ function toThread(row: ThreadRow): Thread {
154
+ return {
155
+ id: row.id,
156
+ projectId: row.project_id,
157
+ branchId: row.branch_id,
158
+ description: row.description,
159
+ status: row.status as Thread['status'],
160
+ workflowType: row.workflow_type as Thread['workflowType'] ?? undefined,
161
+ openedInCommit: row.opened_in_commit,
162
+ closedInCommit: row.closed_in_commit ?? undefined,
163
+ closedNote: row.closed_note ?? undefined,
164
+ createdAt: new Date(row.created_at),
165
+ }
166
+ }
167
+
168
+ function toClaim(row: ClaimRow): Claim {
169
+ return {
170
+ id: row.id,
171
+ projectId: row.project_id,
172
+ branchId: row.branch_id,
173
+ task: row.task,
174
+ agentId: row.agent_id,
175
+ role: row.role as Claim['role'],
176
+ claimedAt: new Date(row.claimed_at),
177
+ status: row.status as Claim['status'],
178
+ ttl: row.ttl,
179
+ releasedAt: row.released_at ? new Date(row.released_at) : undefined,
180
+ }
181
+ }
182
+
183
+ function toAgent(row: AgentRow): Agent {
184
+ return {
185
+ id: row.id,
186
+ projectId: row.project_id,
187
+ role: row.role as Agent['role'],
188
+ tool: row.tool,
189
+ workflowType: row.workflow_type as Agent['workflowType'],
190
+ displayName: row.display_name ?? undefined,
191
+ totalCommits: row.total_commits,
192
+ lastSeen: new Date(row.last_seen),
193
+ createdAt: new Date(row.created_at),
194
+ }
195
+ }
196
+
197
+ // ─── Queries class ───────────────────────────────────────────────────────────
198
+
199
+ export class Queries {
200
+ private readonly db: Database
201
+
202
+ // Prepared statements (lazy init pattern via getters is avoided for simplicity;
203
+ // statements are prepared once in constructor)
204
+ private readonly stmts: {
205
+ insertProject: Statement
206
+ selectProject: Statement<[string]>
207
+
208
+ insertBranch: Statement
209
+ selectBranch: Statement<[string]>
210
+ selectBranchByGit: Statement<[string, string]>
211
+ selectBranches: Statement<[string]>
212
+ updateBranchHead: Statement<[string, string]>
213
+ updateBranchMerged: Statement<[number, string]>
214
+
215
+ insertCommit: Statement
216
+ selectCommit: Statement<[string]>
217
+ selectCommits: Statement<[string, number, number]>
218
+ selectCommitsByRole: Statement<[string, string, number]>
219
+ selectLastCommit: Statement<[string]>
220
+
221
+ insertThread: Statement
222
+ syncThread: Statement
223
+ closeThread: Statement<[string, string, string]>
224
+ selectOpenThreads: Statement<[string]>
225
+ selectOpenThreadsByBranch: Statement<[string]>
226
+ reassignThreads: Statement<[string, string]>
227
+
228
+ insertAgent: Statement
229
+ upsertAgent: Statement
230
+ selectAgent: Statement<[string]>
231
+ selectAgents: Statement<[string]>
232
+ incrementAgentCommits: Statement<[number, string]>
233
+
234
+ insertClaim: Statement
235
+ selectClaim: Statement<[string]>
236
+ listActiveClaims: Statement<[string, number]>
237
+ updateClaimStatus: Statement
238
+ releaseClaimsByAgent: Statement<[number, string, string]>
239
+ }
240
+
241
+ constructor(db: Database) {
242
+ this.db = db
243
+
244
+ this.stmts = {
245
+ // Projects
246
+ insertProject: db.prepare(`
247
+ INSERT INTO projects (id, name, description, github_url, created_at)
248
+ VALUES (@id, @name, @description, @github_url, @created_at)
249
+ `),
250
+ selectProject: db.prepare(`SELECT * FROM projects WHERE id = ?`),
251
+
252
+ // Branches
253
+ insertBranch: db.prepare(`
254
+ INSERT INTO branches
255
+ (id, project_id, name, git_branch, github_pr_url, parent_branch_id,
256
+ head_commit_id, status, created_at)
257
+ VALUES
258
+ (@id, @project_id, @name, @git_branch, @github_pr_url, @parent_branch_id,
259
+ @head_commit_id, @status, @created_at)
260
+ `),
261
+ selectBranch: db.prepare(`SELECT * FROM branches WHERE id = ?`),
262
+ selectBranchByGit: db.prepare(
263
+ `SELECT * FROM branches WHERE project_id = ? AND git_branch = ? LIMIT 1`
264
+ ),
265
+ selectBranches: db.prepare(`SELECT * FROM branches WHERE project_id = ? ORDER BY created_at DESC`),
266
+ updateBranchHead: db.prepare(`UPDATE branches SET head_commit_id = ? WHERE id = ?`),
267
+ updateBranchMerged: db.prepare(
268
+ `UPDATE branches SET status = 'merged', merged_at = ? WHERE id = ?`
269
+ ),
270
+
271
+ // Commits
272
+ insertCommit: db.prepare(`
273
+ INSERT INTO commits
274
+ (id, branch_id, parent_id, merge_source_branch_id, agent_id, agent_role,
275
+ tool, workflow_type, loop_iteration, ci_run_id, pipeline_name,
276
+ message, content, summary, commit_type, git_commit_sha, created_at)
277
+ VALUES
278
+ (@id, @branch_id, @parent_id, @merge_source_branch_id, @agent_id, @agent_role,
279
+ @tool, @workflow_type, @loop_iteration, @ci_run_id, @pipeline_name,
280
+ @message, @content, @summary, @commit_type, @git_commit_sha, @created_at)
281
+ `),
282
+ selectCommit: db.prepare(`SELECT * FROM commits WHERE id = ?`),
283
+ selectCommits: db.prepare(
284
+ `SELECT * FROM commits WHERE branch_id = ? ORDER BY created_at DESC, rowid DESC LIMIT ? OFFSET ?`
285
+ ),
286
+ selectCommitsByRole: db.prepare(
287
+ `SELECT * FROM commits WHERE branch_id = ? AND agent_role = ? ORDER BY created_at DESC, rowid DESC LIMIT ?`
288
+ ),
289
+ selectLastCommit: db.prepare(
290
+ `SELECT * FROM commits WHERE branch_id = ? ORDER BY created_at DESC, rowid DESC LIMIT 1`
291
+ ),
292
+
293
+ // Threads
294
+ insertThread: db.prepare(`
295
+ INSERT INTO threads
296
+ (id, project_id, branch_id, description, status, workflow_type,
297
+ opened_in_commit, created_at)
298
+ VALUES
299
+ (@id, @project_id, @branch_id, @description, 'open', @workflow_type,
300
+ @opened_in_commit, @created_at)
301
+ `),
302
+ syncThread: db.prepare(`
303
+ INSERT OR IGNORE INTO threads
304
+ (id, project_id, branch_id, description, status, workflow_type,
305
+ opened_in_commit, created_at)
306
+ VALUES
307
+ (@id, @project_id, @branch_id, @description, @status, @workflow_type,
308
+ @opened_in_commit, @created_at)
309
+ `),
310
+ closeThread: db.prepare(`
311
+ UPDATE threads
312
+ SET status = 'closed', closed_in_commit = ?, closed_note = ?
313
+ WHERE id = ?
314
+ `),
315
+ selectOpenThreads: db.prepare(
316
+ `SELECT * FROM threads WHERE project_id = ? AND status = 'open' ORDER BY created_at ASC`
317
+ ),
318
+ selectOpenThreadsByBranch: db.prepare(
319
+ `SELECT * FROM threads WHERE branch_id = ? AND status = 'open' ORDER BY created_at ASC`
320
+ ),
321
+ reassignThreads: db.prepare(
322
+ `UPDATE threads SET branch_id = ? WHERE branch_id = ? AND status = 'open'`
323
+ ),
324
+
325
+ // Agents
326
+ insertAgent: db.prepare(`
327
+ INSERT INTO agents
328
+ (id, project_id, role, tool, workflow_type, display_name, total_commits,
329
+ last_seen, created_at)
330
+ VALUES
331
+ (@id, @project_id, @role, @tool, @workflow_type, @display_name, 0,
332
+ @last_seen, @created_at)
333
+ `),
334
+ upsertAgent: db.prepare(`
335
+ INSERT INTO agents
336
+ (id, project_id, role, tool, workflow_type, display_name, total_commits,
337
+ last_seen, created_at)
338
+ VALUES
339
+ (@id, @project_id, @role, @tool, @workflow_type, @display_name, 0,
340
+ @last_seen, @created_at)
341
+ ON CONFLICT(id) DO UPDATE SET
342
+ role = excluded.role,
343
+ tool = excluded.tool,
344
+ workflow_type = excluded.workflow_type,
345
+ display_name = excluded.display_name,
346
+ last_seen = excluded.last_seen
347
+ `),
348
+ selectAgent: db.prepare(`SELECT * FROM agents WHERE id = ?`),
349
+ selectAgents: db.prepare(
350
+ `SELECT * FROM agents WHERE project_id = ? ORDER BY last_seen DESC`
351
+ ),
352
+ incrementAgentCommits: db.prepare(`
353
+ UPDATE agents SET total_commits = total_commits + 1, last_seen = ? WHERE id = ?
354
+ `),
355
+
356
+ // Claims
357
+ insertClaim: db.prepare(`
358
+ INSERT INTO claims
359
+ (id, project_id, branch_id, task, agent_id, role, claimed_at, status, ttl, released_at)
360
+ VALUES
361
+ (@id, @project_id, @branch_id, @task, @agent_id, @role, @claimed_at, @status, @ttl, NULL)
362
+ `),
363
+ selectClaim: db.prepare(`SELECT * FROM claims WHERE id = ?`),
364
+ listActiveClaims: db.prepare(`
365
+ SELECT * FROM claims
366
+ WHERE project_id = ?
367
+ AND status != 'released'
368
+ AND (claimed_at + ttl) > ?
369
+ ORDER BY claimed_at ASC
370
+ `),
371
+ updateClaimStatus: db.prepare(`
372
+ UPDATE claims SET status = @status, released_at = @released_at WHERE id = @id
373
+ `),
374
+ releaseClaimsByAgent: db.prepare(`
375
+ UPDATE claims
376
+ SET status = 'released', released_at = ?
377
+ WHERE agent_id = ? AND branch_id = ? AND status != 'released'
378
+ `),
379
+ }
380
+ }
381
+
382
+ // ─── Projects ────────────────────────────────────────────────────────────
383
+
384
+ insertProject(input: ProjectInput & { id: string }): Project {
385
+ const now = Date.now()
386
+ this.stmts.insertProject.run({
387
+ id: input.id,
388
+ name: input.name,
389
+ description: input.description ?? null,
390
+ github_url: input.githubUrl ?? null,
391
+ created_at: now,
392
+ })
393
+ return toProject({
394
+ id: input.id,
395
+ name: input.name,
396
+ description: input.description ?? null,
397
+ github_url: input.githubUrl ?? null,
398
+ created_at: now,
399
+ })
400
+ }
401
+
402
+ getProject(id: string): Project | null {
403
+ const row = this.stmts.selectProject.get(id) as ProjectRow | undefined
404
+ return row ? toProject(row) : null
405
+ }
406
+
407
+ // ─── Branches ────────────────────────────────────────────────────────────
408
+
409
+ insertBranch(input: BranchInput & { id: string }): Branch {
410
+ const now = Date.now()
411
+ this.stmts.insertBranch.run({
412
+ id: input.id,
413
+ project_id: input.projectId,
414
+ name: input.name,
415
+ git_branch: input.gitBranch,
416
+ github_pr_url: input.githubPrUrl ?? null,
417
+ parent_branch_id: input.parentBranchId ?? null,
418
+ head_commit_id: null,
419
+ status: 'active',
420
+ created_at: now,
421
+ })
422
+ return toBranch({
423
+ id: input.id,
424
+ project_id: input.projectId,
425
+ name: input.name,
426
+ git_branch: input.gitBranch,
427
+ github_pr_url: input.githubPrUrl ?? null,
428
+ parent_branch_id: input.parentBranchId ?? null,
429
+ head_commit_id: null,
430
+ status: 'active',
431
+ created_at: now,
432
+ merged_at: null,
433
+ })
434
+ }
435
+
436
+ getBranch(id: string): Branch | null {
437
+ const row = this.stmts.selectBranch.get(id) as BranchRow | undefined
438
+ return row ? toBranch(row) : null
439
+ }
440
+
441
+ getBranchByGitName(projectId: string, gitBranch: string): Branch | null {
442
+ const row = this.stmts.selectBranchByGit.get(projectId, gitBranch) as BranchRow | undefined
443
+ return row ? toBranch(row) : null
444
+ }
445
+
446
+ listBranches(projectId: string): Branch[] {
447
+ const rows = this.stmts.selectBranches.all(projectId) as BranchRow[]
448
+ return rows.map(toBranch)
449
+ }
450
+
451
+ updateBranchHead(branchId: string, commitId: string): void {
452
+ this.stmts.updateBranchHead.run(commitId, branchId)
453
+ }
454
+
455
+ markBranchMerged(branchId: string): void {
456
+ this.stmts.updateBranchMerged.run(Date.now(), branchId)
457
+ }
458
+
459
+ // ─── Commits ─────────────────────────────────────────────────────────────
460
+
461
+ insertCommit(
462
+ id: string,
463
+ input: CommitInput,
464
+ parentId: string | null,
465
+ mergeSourceBranchId: string | null = null,
466
+ ): Commit {
467
+ const now = Date.now()
468
+ this.stmts.insertCommit.run({
469
+ id,
470
+ branch_id: input.branchId,
471
+ parent_id: parentId,
472
+ merge_source_branch_id: mergeSourceBranchId,
473
+ agent_id: input.agentId,
474
+ agent_role: input.agentRole,
475
+ tool: input.tool,
476
+ workflow_type: input.workflowType,
477
+ loop_iteration: input.loopIteration ?? null,
478
+ ci_run_id: input.ciRunId ?? null,
479
+ pipeline_name: input.pipelineName ?? null,
480
+ message: input.message,
481
+ content: input.content,
482
+ summary: input.summary,
483
+ commit_type: input.commitType,
484
+ git_commit_sha: input.gitCommitSha ?? null,
485
+ created_at: now,
486
+ })
487
+ return {
488
+ id,
489
+ branchId: input.branchId,
490
+ parentId: parentId ?? undefined,
491
+ mergeSourceBranchId: mergeSourceBranchId ?? undefined,
492
+ agentId: input.agentId,
493
+ agentRole: input.agentRole,
494
+ tool: input.tool,
495
+ workflowType: input.workflowType,
496
+ loopIteration: input.loopIteration,
497
+ ciRunId: input.ciRunId,
498
+ pipelineName: input.pipelineName,
499
+ message: input.message,
500
+ content: input.content,
501
+ summary: input.summary,
502
+ commitType: input.commitType,
503
+ gitCommitSha: input.gitCommitSha,
504
+ createdAt: new Date(now),
505
+ }
506
+ }
507
+
508
+ getCommit(id: string): Commit | null {
509
+ const row = this.stmts.selectCommit.get(id) as CommitRow | undefined
510
+ return row ? toCommit(row) : null
511
+ }
512
+
513
+ listCommits(branchId: string, pagination: Pagination): Commit[] {
514
+ const rows = this.stmts.selectCommits.all(
515
+ branchId,
516
+ pagination.limit,
517
+ pagination.offset,
518
+ ) as CommitRow[]
519
+ return rows.map(toCommit)
520
+ }
521
+
522
+ getLastCommit(branchId: string): Commit | null {
523
+ const row = this.stmts.selectLastCommit.get(branchId) as CommitRow | undefined
524
+ return row ? toCommit(row) : null
525
+ }
526
+
527
+ // ─── Threads ──────────────────────────────────────────────────────────────
528
+
529
+ insertThread(
530
+ id: string,
531
+ description: string,
532
+ projectId: string,
533
+ branchId: string,
534
+ openedInCommit: string,
535
+ workflowType: string | null,
536
+ ): Thread {
537
+ const now = Date.now()
538
+ this.stmts.insertThread.run({
539
+ id,
540
+ project_id: projectId,
541
+ branch_id: branchId,
542
+ description,
543
+ workflow_type: workflowType,
544
+ opened_in_commit: openedInCommit,
545
+ created_at: now,
546
+ })
547
+ return {
548
+ id,
549
+ projectId,
550
+ branchId,
551
+ description,
552
+ status: 'open',
553
+ workflowType: workflowType as Thread['workflowType'] ?? undefined,
554
+ openedInCommit,
555
+ createdAt: new Date(now),
556
+ }
557
+ }
558
+
559
+ syncThread(thread: Thread): Thread {
560
+ this.stmts.syncThread.run({
561
+ id: thread.id,
562
+ project_id: thread.projectId,
563
+ branch_id: thread.branchId,
564
+ description: thread.description,
565
+ status: thread.status,
566
+ workflow_type: thread.workflowType ?? null,
567
+ opened_in_commit: thread.openedInCommit,
568
+ created_at: thread.createdAt.getTime(),
569
+ })
570
+ return thread
571
+ }
572
+
573
+ closeThread(threadId: string, closedInCommit: string, note: string): void {
574
+ this.stmts.closeThread.run(closedInCommit, note, threadId)
575
+ }
576
+
577
+ listOpenThreads(projectId: string): Thread[] {
578
+ const rows = this.stmts.selectOpenThreads.all(projectId) as ThreadRow[]
579
+ return rows.map(toThread)
580
+ }
581
+
582
+ listOpenThreadsByBranch(branchId: string): Thread[] {
583
+ const rows = this.stmts.selectOpenThreadsByBranch.all(branchId) as ThreadRow[]
584
+ return rows.map(toThread)
585
+ }
586
+
587
+ /** Move open threads from source branch to target (called during merge). */
588
+ reassignOpenThreads(fromBranchId: string, toBranchId: string): void {
589
+ this.stmts.reassignThreads.run(toBranchId, fromBranchId)
590
+ }
591
+
592
+ // ─── Agents ───────────────────────────────────────────────────────────────
593
+
594
+ upsertAgent(input: AgentInput): Agent {
595
+ const now = Date.now()
596
+ const existing = this.stmts.selectAgent.get(input.id) as AgentRow | undefined
597
+ this.stmts.upsertAgent.run({
598
+ id: input.id,
599
+ project_id: input.projectId,
600
+ role: input.role,
601
+ tool: input.tool,
602
+ workflow_type: input.workflowType,
603
+ display_name: input.displayName ?? null,
604
+ last_seen: now,
605
+ created_at: existing ? existing.created_at : now,
606
+ })
607
+ const row = this.stmts.selectAgent.get(input.id) as AgentRow
608
+ return toAgent(row)
609
+ }
610
+
611
+ incrementAgentCommits(agentId: string): void {
612
+ this.stmts.incrementAgentCommits.run(Date.now(), agentId)
613
+ }
614
+
615
+ listAgents(projectId: string): Agent[] {
616
+ const rows = this.stmts.selectAgents.all(projectId) as AgentRow[]
617
+ return rows.map(toAgent)
618
+ }
619
+
620
+ // ─── Claims ───────────────────────────────────────────────────────────────
621
+
622
+ insertClaim(id: string, projectId: string, branchId: string, input: ClaimInput): Claim {
623
+ const now = Date.now()
624
+ const ttl = input.ttl ?? 7_200_000
625
+ const status = input.status ?? 'proposed'
626
+ this.stmts.insertClaim.run({
627
+ id,
628
+ project_id: projectId,
629
+ branch_id: branchId,
630
+ task: input.task,
631
+ agent_id: input.agentId,
632
+ role: input.role,
633
+ claimed_at: now,
634
+ status,
635
+ ttl,
636
+ })
637
+ return toClaim(this.stmts.selectClaim.get(id) as ClaimRow)
638
+ }
639
+
640
+ listActiveClaims(projectId: string): Claim[] {
641
+ const rows = this.stmts.listActiveClaims.all(projectId, Date.now()) as ClaimRow[]
642
+ return rows.map(toClaim)
643
+ }
644
+
645
+ updateClaimStatus(id: string, status: string, releasedAt: number | null = null): void {
646
+ this.stmts.updateClaimStatus.run({ id, status, released_at: releasedAt })
647
+ }
648
+
649
+ releaseClaimsByAgent(agentId: string, branchId: string): void {
650
+ this.stmts.releaseClaimsByAgent.run(Date.now(), agentId, branchId)
651
+ }
652
+
653
+ // ─── Session snapshot helpers ─────────────────────────────────────────────
654
+
655
+ getSessionSnapshot(projectId: string, branchId: string, options?: { agentRole?: string }): SessionSnapshot {
656
+ // Project summary: head commit summary of the 'main' branch
657
+ const mainBranch = this.getBranchByGitName(projectId, 'main')
658
+ ?? this.getBranchByGitName(projectId, 'master')
659
+
660
+ const projectSummary = mainBranch?.headCommitId
661
+ ? (this.getCommit(mainBranch.headCommitId)?.summary ?? '')
662
+ : ''
663
+
664
+ // Branch summary: head commit summary of current branch
665
+ const branch = this.getBranch(branchId)
666
+ const branchSummary = branch?.headCommitId
667
+ ? (this.getCommit(branch.headCommitId)?.summary ?? '')
668
+ : ''
669
+
670
+ // Last 3 commits on current branch (optionally filtered by agent role)
671
+ const recentCommits = options?.agentRole
672
+ ? (this.stmts.selectCommitsByRole.all(branchId, options.agentRole, 3) as CommitRow[]).map(toCommit)
673
+ : this.listCommits(branchId, { limit: 3, offset: 0 })
674
+
675
+ // All open threads for the project
676
+ const openThreads = this.listOpenThreads(projectId)
677
+
678
+ const activeClaims = this.listActiveClaims(projectId)
679
+
680
+ return {
681
+ projectSummary,
682
+ branchName: branch?.name ?? '',
683
+ branchSummary,
684
+ recentCommits,
685
+ openThreads,
686
+ activeClaims,
687
+ }
688
+ }
689
+
690
+ // ─── Semantic search (sqlite-vec) ─────────────────────────────────────────
691
+
692
+ semanticSearch(
693
+ db: Database,
694
+ embeddingVector: Float32Array,
695
+ projectId: string,
696
+ limit: number,
697
+ ): SearchResult[] {
698
+ try {
699
+ // KNN query joining commit_embeddings with commits filtered by project
700
+ const stmt = db.prepare<[string, Float32Array, number]>(`
701
+ SELECT c.*, ce.distance
702
+ FROM commit_embeddings ce
703
+ JOIN commits c ON c.id = ce.commit_id
704
+ JOIN branches b ON b.id = c.branch_id
705
+ WHERE b.project_id = ?
706
+ AND ce.embedding MATCH ?
707
+ AND k = ?
708
+ ORDER BY ce.distance
709
+ `)
710
+ const rows = stmt.all(projectId, embeddingVector, limit) as Array<CommitRow & { distance: number }>
711
+ return rows.map((r) => ({
712
+ commit: toCommit(r),
713
+ score: 1 - r.distance, // cosine similarity approximation
714
+ matchType: 'semantic' as const,
715
+ }))
716
+ } catch {
717
+ // sqlite-vec not available or no embeddings indexed
718
+ return []
719
+ }
720
+ }
721
+
722
+ // ─── Full-text search (FTS5) ──────────────────────────────────────────────
723
+
724
+ fullTextSearch(db: Database, query: string, projectId: string): SearchResult[] {
725
+ try {
726
+ const stmt = db.prepare(`
727
+ SELECT c.*, bm25(commits_fts) AS score
728
+ FROM commits_fts
729
+ JOIN commits c ON c.rowid = commits_fts.rowid
730
+ JOIN branches b ON b.id = c.branch_id
731
+ WHERE b.project_id = ?
732
+ AND commits_fts MATCH ?
733
+ ORDER BY score
734
+ LIMIT 20
735
+ `)
736
+ const rows = stmt.all(projectId, query) as Array<CommitRow & { score: number }>
737
+ return rows.map((r) => ({
738
+ commit: toCommit(r),
739
+ score: Math.abs(r.score),
740
+ matchType: 'fulltext' as const,
741
+ }))
742
+ } catch {
743
+ // FTS5 migration not yet applied or query error
744
+ return []
745
+ }
746
+ }
747
+
748
+ // ─── Embed helpers ────────────────────────────────────────────────────────
749
+
750
+ insertEmbedding(db: Database, commitId: string, embedding: Float32Array): void {
751
+ try {
752
+ const stmt = db.prepare(
753
+ `INSERT OR REPLACE INTO commit_embeddings (commit_id, embedding) VALUES (?, ?)`
754
+ )
755
+ stmt.run(commitId, embedding)
756
+ } catch {
757
+ // sqlite-vec not available
758
+ }
759
+ }
760
+ }