@straiffi/archon 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/README.md +224 -0
  2. package/dist/cli.js +216 -0
  3. package/dist/client/assets/index-8_-boBBA.css +2 -0
  4. package/dist/client/assets/index-s_jjeqha.js +176 -0
  5. package/dist/client/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
  6. package/dist/client/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
  7. package/dist/client/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
  8. package/dist/client/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
  9. package/dist/client/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
  10. package/dist/client/favicon.svg +62 -0
  11. package/dist/client/icons.svg +24 -0
  12. package/dist/client/index.html +14 -0
  13. package/dist/server/db.js +764 -0
  14. package/dist/server/db.js.map +1 -0
  15. package/dist/server/index.js +5134 -0
  16. package/dist/server/index.js.map +1 -0
  17. package/dist/server/lib/agent.js +1302 -0
  18. package/dist/server/lib/agent.js.map +1 -0
  19. package/dist/server/lib/buildChains.js +2 -0
  20. package/dist/server/lib/buildChains.js.map +1 -0
  21. package/dist/server/lib/buildFlow.js +59 -0
  22. package/dist/server/lib/buildFlow.js.map +1 -0
  23. package/dist/server/lib/buildSequences.js +599 -0
  24. package/dist/server/lib/buildSequences.js.map +1 -0
  25. package/dist/server/lib/bundleActivity.js +95 -0
  26. package/dist/server/lib/bundleActivity.js.map +1 -0
  27. package/dist/server/lib/bundlePullRequests.js +126 -0
  28. package/dist/server/lib/bundlePullRequests.js.map +1 -0
  29. package/dist/server/lib/chatMessages.js +60 -0
  30. package/dist/server/lib/chatMessages.js.map +1 -0
  31. package/dist/server/lib/chatTargets.js +123 -0
  32. package/dist/server/lib/chatTargets.js.map +1 -0
  33. package/dist/server/lib/chatTicketProposals.js +180 -0
  34. package/dist/server/lib/chatTicketProposals.js.map +1 -0
  35. package/dist/server/lib/chats.js +279 -0
  36. package/dist/server/lib/chats.js.map +1 -0
  37. package/dist/server/lib/config.js +3 -0
  38. package/dist/server/lib/config.js.map +1 -0
  39. package/dist/server/lib/cors.js +30 -0
  40. package/dist/server/lib/cors.js.map +1 -0
  41. package/dist/server/lib/directoryPicker.js +174 -0
  42. package/dist/server/lib/directoryPicker.js.map +1 -0
  43. package/dist/server/lib/git.js +1284 -0
  44. package/dist/server/lib/git.js.map +1 -0
  45. package/dist/server/lib/integrations/github.js +511 -0
  46. package/dist/server/lib/integrations/github.js.map +1 -0
  47. package/dist/server/lib/integrations/index.js +162 -0
  48. package/dist/server/lib/integrations/index.js.map +1 -0
  49. package/dist/server/lib/integrations/jira.js +283 -0
  50. package/dist/server/lib/integrations/jira.js.map +1 -0
  51. package/dist/server/lib/integrations/planning.js +27 -0
  52. package/dist/server/lib/integrations/planning.js.map +1 -0
  53. package/dist/server/lib/integrations/types.js +2 -0
  54. package/dist/server/lib/integrations/types.js.map +1 -0
  55. package/dist/server/lib/lightweightPrompt.js +88 -0
  56. package/dist/server/lib/lightweightPrompt.js.map +1 -0
  57. package/dist/server/lib/models.js +219 -0
  58. package/dist/server/lib/models.js.map +1 -0
  59. package/dist/server/lib/preview.js +377 -0
  60. package/dist/server/lib/preview.js.map +1 -0
  61. package/dist/server/lib/previewProxy.js +659 -0
  62. package/dist/server/lib/previewProxy.js.map +1 -0
  63. package/dist/server/lib/projectAutoConfig.js +682 -0
  64. package/dist/server/lib/projectAutoConfig.js.map +1 -0
  65. package/dist/server/lib/projectFileSuggestions.js +133 -0
  66. package/dist/server/lib/projectFileSuggestions.js.map +1 -0
  67. package/dist/server/lib/projectMemory.js +1519 -0
  68. package/dist/server/lib/projectMemory.js.map +1 -0
  69. package/dist/server/lib/projectMemoryPrompt.js +390 -0
  70. package/dist/server/lib/projectMemoryPrompt.js.map +1 -0
  71. package/dist/server/lib/projectMemoryScan.js +681 -0
  72. package/dist/server/lib/projectMemoryScan.js.map +1 -0
  73. package/dist/server/lib/projectMemorySuggestions.js +166 -0
  74. package/dist/server/lib/projectMemorySuggestions.js.map +1 -0
  75. package/dist/server/lib/projectMemoryTransfer.js +958 -0
  76. package/dist/server/lib/projectMemoryTransfer.js.map +1 -0
  77. package/dist/server/lib/projects.js +569 -0
  78. package/dist/server/lib/projects.js.map +1 -0
  79. package/dist/server/lib/promptSkills.js +28 -0
  80. package/dist/server/lib/promptSkills.js.map +1 -0
  81. package/dist/server/lib/queue.js +15 -0
  82. package/dist/server/lib/queue.js.map +1 -0
  83. package/dist/server/lib/reviewFindings.js +390 -0
  84. package/dist/server/lib/reviewFindings.js.map +1 -0
  85. package/dist/server/lib/run.js +416 -0
  86. package/dist/server/lib/run.js.map +1 -0
  87. package/dist/server/lib/runtimePaths.js +93 -0
  88. package/dist/server/lib/runtimePaths.js.map +1 -0
  89. package/dist/server/lib/shell.js +27 -0
  90. package/dist/server/lib/shell.js.map +1 -0
  91. package/dist/server/lib/skills.js +124 -0
  92. package/dist/server/lib/skills.js.map +1 -0
  93. package/dist/server/lib/startDev.js +18 -0
  94. package/dist/server/lib/startDev.js.map +1 -0
  95. package/dist/server/lib/staticClient.js +80 -0
  96. package/dist/server/lib/staticClient.js.map +1 -0
  97. package/dist/server/lib/terminal.js +366 -0
  98. package/dist/server/lib/terminal.js.map +1 -0
  99. package/dist/server/lib/ticketDependencies.js +174 -0
  100. package/dist/server/lib/ticketDependencies.js.map +1 -0
  101. package/dist/server/lib/ticketMessages.js +65 -0
  102. package/dist/server/lib/ticketMessages.js.map +1 -0
  103. package/dist/server/lib/ticketOpenQuestions.js +128 -0
  104. package/dist/server/lib/ticketOpenQuestions.js.map +1 -0
  105. package/dist/server/lib/ticketUndo.js +549 -0
  106. package/dist/server/lib/ticketUndo.js.map +1 -0
  107. package/dist/server/lib/tickets.js +981 -0
  108. package/dist/server/lib/tickets.js.map +1 -0
  109. package/dist/server/lib/types.js +2 -0
  110. package/dist/server/lib/types.js.map +1 -0
  111. package/dist/server/package.json +3 -0
  112. package/dist/server/workers/build.js +229 -0
  113. package/dist/server/workers/build.js.map +1 -0
  114. package/dist/server/workers/chat.js +190 -0
  115. package/dist/server/workers/chat.js.map +1 -0
  116. package/dist/server/workers/followUp.js +204 -0
  117. package/dist/server/workers/followUp.js.map +1 -0
  118. package/dist/server/workers/plan.js +1130 -0
  119. package/dist/server/workers/plan.js.map +1 -0
  120. package/dist/server/workers/planFollowUp.js +360 -0
  121. package/dist/server/workers/planFollowUp.js.map +1 -0
  122. package/dist/server/workers/review.js +167 -0
  123. package/dist/server/workers/review.js.map +1 -0
  124. package/package.json +40 -0
@@ -0,0 +1,981 @@
1
+ import { randomUUID } from 'crypto';
2
+ import db from '../db.js';
3
+ import { getDependencyOrderedBundleTicketIds, getStableBundleExecutionOrder } from './buildSequences.js';
4
+ import { getBundlePullRequestRecord, serializeBundlePullRequest } from './bundlePullRequests.js';
5
+ import { getBuildChainTicketState } from './buildChains.js';
6
+ import { getBoardGitStatus, getBoardWorktreeGitStatus, getGitDiffFiles, getGitStatus, getProjectBranch, getWorktreeDiffStats, getWorktreeGitStatus, resolveExistingWorktreePath, summarizeTicketDiffFiles } from './git.js';
7
+ import { getGitHubConnectionConfig } from './integrations/index.js';
8
+ import { getEffectiveProject, getProjectById, hasProjectRunIde } from './projects.js';
9
+ import { getReviewContextState } from './reviewFindings.js';
10
+ import { hasRunConfig, isRunning } from './run.js';
11
+ import { listTicketMessages, upsertTicketDescriptionMessage } from './ticketMessages.js';
12
+ import { getTicketUndo } from './ticketUndo.js';
13
+ const ticketSelect = `
14
+ SELECT
15
+ tickets.*,
16
+ worktree_bundles.id AS bundle_id,
17
+ worktree_bundles.name AS bundle_name,
18
+ worktree_bundles.branch AS bundle_branch,
19
+ worktree_bundles.kind AS bundle_kind,
20
+ (
21
+ SELECT COUNT(*)
22
+ FROM tickets AS bundle_tickets
23
+ WHERE bundle_tickets.worktree_bundle_id = worktree_bundles.id
24
+ AND bundle_tickets.project_id = worktree_bundles.project_id
25
+ ) AS bundle_ticket_count
26
+ FROM tickets
27
+ LEFT JOIN worktree_bundles
28
+ ON worktree_bundles.id = tickets.worktree_bundle_id
29
+ AND worktree_bundles.project_id = tickets.project_id
30
+ `;
31
+ const bundleSelect = `
32
+ SELECT
33
+ worktree_bundles.*,
34
+ (
35
+ SELECT COUNT(*)
36
+ FROM tickets
37
+ WHERE tickets.worktree_bundle_id = worktree_bundles.id
38
+ AND tickets.project_id = worktree_bundles.project_id
39
+ ) AS ticket_count
40
+ FROM worktree_bundles
41
+ `;
42
+ const ORDER_STEP = 1000;
43
+ const PROJECT_ROOT_BUNDLE_NAME = 'Project root';
44
+ const PROJECT_ROOT_BUNDLE_BRANCH = '__project_root__';
45
+ const ACTIVE_STATE_ORDER = {
46
+ plan: 0,
47
+ build: 1,
48
+ review: 2,
49
+ };
50
+ const getNextLaneOrder = (projectId, state) => {
51
+ const rows = db.prepare('SELECT worktree_bundle_id, lane_order FROM tickets WHERE project_id = ? AND state = ? AND parked_at IS NULL ORDER BY lane_order DESC, updated_at DESC, id DESC').all(projectId, state);
52
+ const normalizeOrder = (value) => Number(value ?? 0);
53
+ if (rows.length === 0) {
54
+ return ORDER_STEP;
55
+ }
56
+ return normalizeOrder(rows[0].lane_order) + ORDER_STEP;
57
+ };
58
+ const getNextDoneOrder = (projectId) => {
59
+ const rows = db.prepare('SELECT worktree_bundle_id, done_order FROM tickets WHERE project_id = ? AND parked_at IS NOT NULL ORDER BY done_order DESC, updated_at DESC, id DESC').all(projectId);
60
+ const normalizeOrder = (value) => Number(value ?? 0);
61
+ if (rows.length === 0) {
62
+ return ORDER_STEP;
63
+ }
64
+ return normalizeOrder(rows[0].done_order) + ORDER_STEP;
65
+ };
66
+ export const assignTicketLaneOrder = (ticket) => {
67
+ if (!ticket.project_id) {
68
+ return null;
69
+ }
70
+ return getNextLaneOrder(ticket.project_id, ticket.state);
71
+ };
72
+ export const assignTicketDoneOrder = (ticket) => {
73
+ if (!ticket.project_id) {
74
+ return null;
75
+ }
76
+ return getNextDoneOrder(ticket.project_id);
77
+ };
78
+ const canAutoParkTicket = (ticket) => {
79
+ if (ticket.parked_at !== null || ticket.agent_status === 'running') {
80
+ return false;
81
+ }
82
+ if (ticket.state === 'build') {
83
+ return ticket.agent_status === 'done';
84
+ }
85
+ return ticket.state === 'review';
86
+ };
87
+ const shouldClearAutoParkDismissal = (ticket) => {
88
+ if (ticket.parked_at !== null || ticket.agent_status === 'running') {
89
+ return false;
90
+ }
91
+ if (ticket.is_stale !== true) {
92
+ return true;
93
+ }
94
+ return !canAutoParkTicket(ticket);
95
+ };
96
+ export const reconcileAutoParkDismissal = (ticketId) => {
97
+ let ticket = getTicket(ticketId);
98
+ if (!ticket) {
99
+ return ticket;
100
+ }
101
+ if (!ticket.auto_park_dismissed_at || !shouldClearAutoParkDismissal(ticket)) {
102
+ return ticket;
103
+ }
104
+ db.prepare('UPDATE tickets SET auto_park_dismissed_at = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(ticket.id);
105
+ ticket = getTicket(ticket.id);
106
+ return ticket;
107
+ };
108
+ export const clearAutoParkDismissalIfNeeded = (ticket) => {
109
+ if (!ticket?.auto_park_dismissed_at || !shouldClearAutoParkDismissal(ticket)) {
110
+ return false;
111
+ }
112
+ db.prepare('UPDATE tickets SET auto_park_dismissed_at = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(ticket.id);
113
+ return true;
114
+ };
115
+ const normalizeBundleRow = (row) => {
116
+ if (!row) {
117
+ return null;
118
+ }
119
+ return {
120
+ ...row,
121
+ kind: row.kind === 'project_root' ? 'project_root' : 'worktree',
122
+ ticket_count: Number(row.ticket_count ?? 0),
123
+ };
124
+ };
125
+ const serializeBundle = (row, options = {}) => {
126
+ if (!row?.bundle_id || !row.bundle_name || !row.bundle_branch) {
127
+ return null;
128
+ }
129
+ const kind = row.bundle_kind === 'project_root' ? 'project_root' : 'worktree';
130
+ const project = row.project_id ? getProjectById(row.project_id) : null;
131
+ const branch = kind === 'project_root'
132
+ ? getProjectBranch(project, { includeDiffStats: false }).branch ?? PROJECT_ROOT_BUNDLE_BRANCH
133
+ : row.bundle_branch;
134
+ const hasGitHubConnection = row.project_id
135
+ ? resolveProjectGitHubConnection(row.project_id, options.githubConnectionCache)
136
+ : false;
137
+ const pullRequest = row.project_id
138
+ ? resolveBundlePullRequest(row.project_id, row.bundle_id, hasGitHubConnection, options.bundlePullRequestCache)
139
+ : null;
140
+ return {
141
+ id: row.bundle_id,
142
+ name: row.bundle_name,
143
+ branch,
144
+ kind,
145
+ ticket_count: Number(row.bundle_ticket_count ?? 0),
146
+ pull_request: pullRequest,
147
+ };
148
+ };
149
+ const parseTicketSkills = (value) => {
150
+ if (!value) {
151
+ return [];
152
+ }
153
+ try {
154
+ const parsed = JSON.parse(value);
155
+ if (!Array.isArray(parsed)) {
156
+ return [];
157
+ }
158
+ return parsed
159
+ .filter((entry) => typeof entry === 'string')
160
+ .map(entry => entry.trim())
161
+ .filter(Boolean)
162
+ .filter((entry, index, values) => values.indexOf(entry) === index);
163
+ }
164
+ catch {
165
+ return [];
166
+ }
167
+ };
168
+ const parsePlanningContext = (value) => {
169
+ if (!value) {
170
+ return null;
171
+ }
172
+ try {
173
+ const parsed = JSON.parse(value);
174
+ if ((parsed.source_kind !== 'single' && parsed.source_kind !== 'multi')
175
+ || typeof parsed.initiative_prompt !== 'string'
176
+ || !Array.isArray(parsed.related_ticket_ids)) {
177
+ return null;
178
+ }
179
+ return {
180
+ source_kind: parsed.source_kind,
181
+ initiative_prompt: parsed.initiative_prompt,
182
+ related_ticket_ids: parsed.related_ticket_ids
183
+ .filter((entry) => typeof entry === 'string')
184
+ .map(entry => entry.trim())
185
+ .filter(Boolean)
186
+ .filter((entry, index, values) => values.indexOf(entry) === index),
187
+ open_questions: Array.isArray(parsed.open_questions)
188
+ ? parsed.open_questions
189
+ .filter((entry) => typeof entry === 'string')
190
+ .map(entry => entry.trim())
191
+ .filter(Boolean)
192
+ .filter((entry, index, values) => values.indexOf(entry) === index)
193
+ : [],
194
+ };
195
+ }
196
+ catch {
197
+ return null;
198
+ }
199
+ };
200
+ const parseExternalSource = (row) => {
201
+ if (row.external_provider !== 'jira'
202
+ || !row.external_id
203
+ || !row.external_key) {
204
+ return null;
205
+ }
206
+ let metadata = null;
207
+ if (row.external_metadata_json) {
208
+ try {
209
+ const parsed = JSON.parse(row.external_metadata_json);
210
+ metadata = parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
211
+ }
212
+ catch {
213
+ metadata = null;
214
+ }
215
+ }
216
+ return {
217
+ provider: 'jira',
218
+ external_id: row.external_id,
219
+ external_key: row.external_key,
220
+ external_url: row.external_url ?? null,
221
+ metadata,
222
+ };
223
+ };
224
+ export const serializePlanningContext = (context) => {
225
+ if (!context) {
226
+ return null;
227
+ }
228
+ return JSON.stringify({
229
+ source_kind: context.source_kind,
230
+ initiative_prompt: context.initiative_prompt,
231
+ related_ticket_ids: context.related_ticket_ids,
232
+ open_questions: context.open_questions,
233
+ });
234
+ };
235
+ const isReviewFollowUpActive = (row, messages = []) => {
236
+ if (row.state !== 'review') {
237
+ return false;
238
+ }
239
+ if (row.agent_status !== 'running' && row.agent_status !== 'stopped') {
240
+ return false;
241
+ }
242
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
243
+ const message = messages[index];
244
+ if (message.role === 'user' && message.kind === 'follow_up') {
245
+ return true;
246
+ }
247
+ if (message.role === 'assistant' && message.kind === 'final_response') {
248
+ return false;
249
+ }
250
+ }
251
+ return false;
252
+ };
253
+ const getWorkspaceDiffKey = (projectRepoPath, branch) => `${projectRepoPath}::${branch}`;
254
+ const BOARD_ENRICHMENT_CACHE_TTL_MS = 10_000;
255
+ const boardEnrichmentCache = new Map();
256
+ const TICKET_TIMING_ENABLED = process.env.ARCHON_API_TIMING === '1';
257
+ const getCachedBoardEnrichment = (cacheKey) => {
258
+ const cached = boardEnrichmentCache.get(cacheKey);
259
+ if (!cached) {
260
+ return null;
261
+ }
262
+ if (Date.now() - cached.cachedAt > BOARD_ENRICHMENT_CACHE_TTL_MS) {
263
+ boardEnrichmentCache.delete(cacheKey);
264
+ return null;
265
+ }
266
+ return cached;
267
+ };
268
+ export const clearBoardEnrichmentCache = () => {
269
+ boardEnrichmentCache.clear();
270
+ };
271
+ const shouldIncludeBoardDiffStats = (row, includeDetail, includeDiffStats) => {
272
+ if (!includeDiffStats) {
273
+ return false;
274
+ }
275
+ if (includeDetail) {
276
+ return true;
277
+ }
278
+ return row.state !== 'plan';
279
+ };
280
+ const shouldResolveStaleGitStatus = (row, includeStaleState) => {
281
+ if (!includeStaleState) {
282
+ return false;
283
+ }
284
+ if (row.state !== 'build' && row.state !== 'review') {
285
+ return false;
286
+ }
287
+ return row.build_completed_at !== null;
288
+ };
289
+ const getBundlePullRequestCacheKey = (projectId, bundleId) => `${projectId}:${bundleId}`;
290
+ const resolveProjectGitHubConnection = (projectId, cache) => {
291
+ if (cache?.has(projectId)) {
292
+ return cache.get(projectId) === true;
293
+ }
294
+ const hasConnection = getGitHubConnectionConfig(projectId) !== null;
295
+ cache?.set(projectId, hasConnection);
296
+ return hasConnection;
297
+ };
298
+ const resolveBundlePullRequest = (projectId, bundleId, hasGitHubConnection, cache) => {
299
+ if (!hasGitHubConnection) {
300
+ return null;
301
+ }
302
+ const key = getBundlePullRequestCacheKey(projectId, bundleId);
303
+ if (cache?.has(key)) {
304
+ return cache.get(key) ?? null;
305
+ }
306
+ const pullRequest = serializeBundlePullRequest(getBundlePullRequestRecord(bundleId, projectId));
307
+ cache?.set(key, pullRequest);
308
+ return pullRequest;
309
+ };
310
+ const compareBundleTickets = (left, right) => {
311
+ const createdAtComparison = left.created_at.localeCompare(right.created_at);
312
+ if (createdAtComparison !== 0) {
313
+ return createdAtComparison;
314
+ }
315
+ const titleComparison = left.title.localeCompare(right.title, undefined, { sensitivity: 'base' });
316
+ if (titleComparison !== 0) {
317
+ return titleComparison;
318
+ }
319
+ return left.id.localeCompare(right.id);
320
+ };
321
+ const buildBundleExecutionPositionMap = (tickets) => {
322
+ const ticketsByBundleId = new Map();
323
+ for (const ticket of tickets) {
324
+ if (!ticket.project_id || !ticket.worktree_bundle_id) {
325
+ continue;
326
+ }
327
+ const bundleTickets = ticketsByBundleId.get(ticket.worktree_bundle_id);
328
+ if (bundleTickets) {
329
+ bundleTickets.push(ticket);
330
+ continue;
331
+ }
332
+ ticketsByBundleId.set(ticket.worktree_bundle_id, [ticket]);
333
+ }
334
+ const executionPositionByTicketId = new Map();
335
+ for (const [bundleId, bundleTickets] of ticketsByBundleId) {
336
+ const projectId = bundleTickets[0]?.project_id;
337
+ if (!projectId) {
338
+ continue;
339
+ }
340
+ const latestExecutionOrder = getStableBundleExecutionOrder(projectId, bundleId);
341
+ const bundleTicketIds = bundleTickets.map(ticket => ticket.id);
342
+ const fallbackDependencyOrder = getDependencyOrderedBundleTicketIds(projectId, bundleTicketIds);
343
+ const fallbackTicketIds = [...bundleTickets]
344
+ .sort(compareBundleTickets)
345
+ .map(ticket => ticket.id);
346
+ const orderedTicketIds = [
347
+ ...latestExecutionOrder,
348
+ ...fallbackDependencyOrder,
349
+ ...fallbackTicketIds,
350
+ ].filter((ticketId, index, values) => bundleTicketIds.includes(ticketId) && values.indexOf(ticketId) === index);
351
+ orderedTicketIds.forEach((ticketId, index) => {
352
+ executionPositionByTicketId.set(ticketId, index);
353
+ });
354
+ }
355
+ return executionPositionByTicketId;
356
+ };
357
+ const applyBundleExecutionPositions = (tickets) => {
358
+ const bundleExecutionPositionByTicketId = buildBundleExecutionPositionMap(tickets);
359
+ return tickets.map(ticket => ({
360
+ ...ticket,
361
+ bundle_execution_position: bundleExecutionPositionByTicketId.get(ticket.id) ?? ticket.bundle_execution_position ?? null,
362
+ }));
363
+ };
364
+ const resolveTicketWorktree = (row, projectRepoPath, branch, existingWorktreePaths, worktreeRecordsCache) => {
365
+ if ('bundle_kind' in row && row.bundle_kind === 'project_root') {
366
+ return projectRepoPath;
367
+ }
368
+ if (!projectRepoPath || !branch) {
369
+ return null;
370
+ }
371
+ const key = getWorkspaceDiffKey(projectRepoPath, branch);
372
+ if (existingWorktreePaths?.has(key)) {
373
+ return existingWorktreePaths.get(key) ?? null;
374
+ }
375
+ const project = getEffectiveProject(row);
376
+ const existingWorktreePath = project
377
+ ? resolveExistingWorktreePath(branch, project, { worktreeRecordsCache })
378
+ : null;
379
+ existingWorktreePaths?.set(key, existingWorktreePath);
380
+ return existingWorktreePath;
381
+ };
382
+ const resolveTicketDiffStats = (row, projectRepoPath, branch, hasWorktree, cache) => {
383
+ if (!projectRepoPath || !branch || !hasWorktree) {
384
+ return null;
385
+ }
386
+ const key = getWorkspaceDiffKey(projectRepoPath, branch);
387
+ if (cache?.has(key)) {
388
+ return cache.get(key) ?? null;
389
+ }
390
+ const project = getEffectiveProject(row);
391
+ const diffStats = project ? getWorktreeDiffStats(branch, project) : null;
392
+ cache?.set(key, diffStats);
393
+ return diffStats;
394
+ };
395
+ const resolveTicketGitStatus = (row, projectRepoPath, branch, hasWorktree, cache) => {
396
+ if (!projectRepoPath || !branch || !hasWorktree) {
397
+ return null;
398
+ }
399
+ const key = getWorkspaceDiffKey(projectRepoPath, branch);
400
+ if (cache?.has(key)) {
401
+ return cache.get(key) ?? null;
402
+ }
403
+ const project = getEffectiveProject(row);
404
+ const gitStatus = project ? getWorktreeGitStatus(branch, project) : null;
405
+ cache?.set(key, gitStatus);
406
+ return gitStatus;
407
+ };
408
+ const isTicketStale = (ticket, gitStatus, bundle, hasGitHubConnection) => {
409
+ if (ticket.state !== 'build' && ticket.state !== 'review') {
410
+ return false;
411
+ }
412
+ if (ticket.build_completed_at === null || !gitStatus) {
413
+ return false;
414
+ }
415
+ if (gitStatus.is_dirty !== false || gitStatus.ahead !== 0) {
416
+ return false;
417
+ }
418
+ if (!hasGitHubConnection || !ticket.project_id || !ticket.worktree_bundle_id || isProjectRootBundle(bundle)) {
419
+ return true;
420
+ }
421
+ return bundle?.pull_request?.state === 'merged' || bundle?.pull_request?.state === 'closed';
422
+ };
423
+ export const serializeTicket = (row, options = {}) => {
424
+ if (!row) {
425
+ return null;
426
+ }
427
+ const { diffStatsCache, gitStatusCache, existingWorktreePaths, worktreeRecordsCache, includeDetail = true, includeMessages = includeDetail, includeLogs = includeDetail, includeUndo = includeDetail, includeDiffStats = includeDetail, includeStaleState = includeDetail, includeWorktreeState = includeDetail, bundleExecutionPositionByTicketId, githubConnectionCache, bundlePullRequestCache, } = options;
428
+ const project = getEffectiveProject(row);
429
+ const bundle = serializeBundle(row, {
430
+ githubConnectionCache,
431
+ bundlePullRequestCache,
432
+ });
433
+ const branch = bundle?.branch ?? row.branch ?? null;
434
+ const includeResolvedDiffStats = shouldIncludeBoardDiffStats(row, includeDetail, includeDiffStats);
435
+ const includeResolvedStaleState = shouldResolveStaleGitStatus(row, includeStaleState);
436
+ const shouldResolveWorktreeState = includeWorktreeState || includeResolvedDiffStats || includeResolvedStaleState;
437
+ const hasWorktree = (project?.repo_path && branch && shouldResolveWorktreeState)
438
+ ? resolveTicketWorktree(row, project.repo_path, branch, existingWorktreePaths, worktreeRecordsCache) !== null
439
+ : undefined;
440
+ const diffStats = includeResolvedDiffStats
441
+ ? resolveTicketDiffStats(row, project?.repo_path ?? null, branch, hasWorktree === true, diffStatsCache)
442
+ : null;
443
+ const gitStatus = includeResolvedStaleState
444
+ ? resolveTicketGitStatus(row, project?.repo_path ?? null, branch, hasWorktree === true, gitStatusCache)
445
+ : null;
446
+ const messages = includeMessages
447
+ ? (() => {
448
+ const storedMessages = listTicketMessages(row.id);
449
+ return storedMessages.some(message => message.kind === 'description')
450
+ ? storedMessages
451
+ : [
452
+ {
453
+ id: `description:${row.id}`,
454
+ ticket_id: row.id,
455
+ role: 'user',
456
+ kind: 'description',
457
+ content: row.description ?? '',
458
+ created_at: row.created_at,
459
+ },
460
+ ...storedMessages,
461
+ ];
462
+ })()
463
+ : undefined;
464
+ const buildChain = getBuildChainTicketState(row.id);
465
+ const hasRunConfigValue = project ? hasRunConfig(project) : hasRunConfig();
466
+ const reviewFollowUpActive = messages ? isReviewFollowUpActive(row, messages) : undefined;
467
+ const reviewContextState = getReviewContextState({
468
+ ticketId: row.id,
469
+ projectId: row.project_id,
470
+ worktreeBundleId: row.worktree_bundle_id,
471
+ });
472
+ const hasGitHubConnection = row.project_id
473
+ ? resolveProjectGitHubConnection(row.project_id, githubConnectionCache)
474
+ : false;
475
+ const canReReview = reviewContextState.hasCompletedReview
476
+ && reviewContextState.hasFollowUpSinceLastReview
477
+ && row.agent_status !== 'running';
478
+ const ticketBase = {
479
+ id: row.id,
480
+ title: row.title,
481
+ description: row.description ?? '',
482
+ state: row.state,
483
+ has_completed_review: reviewContextState.hasCompletedReview || undefined,
484
+ has_follow_up_since_last_review: reviewContextState.hasFollowUpSinceLastReview || undefined,
485
+ can_re_review: canReReview || undefined,
486
+ agent_status: row.agent_status,
487
+ parked_at: row.parked_at,
488
+ auto_park_dismissed_at: row.auto_park_dismissed_at ?? null,
489
+ build_completed_at: row.build_completed_at,
490
+ run_setup_status: row.run_setup_status ?? null,
491
+ branch: row.branch,
492
+ tool: row.tool,
493
+ model: row.model,
494
+ variant: row.variant,
495
+ skills: parseTicketSkills(row.skills_json),
496
+ session_id: row.session_id,
497
+ planning_session_id: row.planning_session_id,
498
+ planning_context: parsePlanningContext(row.planning_context_json),
499
+ worktree_bundle_id: row.worktree_bundle_id,
500
+ project_id: row.project_id,
501
+ created_at: row.created_at,
502
+ updated_at: row.updated_at,
503
+ bundle_execution_position: bundleExecutionPositionByTicketId?.get(row.id) ?? null,
504
+ bundle,
505
+ is_running: isRunning(row),
506
+ has_run_config: hasRunConfigValue,
507
+ has_run_ide: hasProjectRunIde(project),
508
+ ...(hasWorktree !== undefined ? { has_worktree: hasWorktree } : {}),
509
+ build_chain: buildChain,
510
+ external_source: parseExternalSource(row),
511
+ };
512
+ const ticketWithOptionalFields = {
513
+ ...ticketBase,
514
+ ...(messages ? { messages } : {}),
515
+ ...(reviewFollowUpActive !== undefined ? { review_follow_up_active: reviewFollowUpActive } : {}),
516
+ ...(includeLogs ? {
517
+ agent_log: row.agent_log,
518
+ streaming_response: row.streaming_response,
519
+ } : {}),
520
+ ...(includeResolvedDiffStats ? { diff_stats: diffStats } : {}),
521
+ ...(includeStaleState ? {
522
+ is_stale: isTicketStale({
523
+ state: row.state,
524
+ build_completed_at: row.build_completed_at,
525
+ project_id: row.project_id,
526
+ worktree_bundle_id: row.worktree_bundle_id,
527
+ }, gitStatus, bundle, hasGitHubConnection),
528
+ } : {}),
529
+ };
530
+ return {
531
+ ...ticketWithOptionalFields,
532
+ undo: includeUndo
533
+ ? getTicketUndo({
534
+ ...ticketWithOptionalFields,
535
+ messages: messages ?? [],
536
+ agent_log: includeLogs ? (row.agent_log ?? null) : null,
537
+ streaming_response: includeLogs ? (row.streaming_response ?? null) : null,
538
+ })
539
+ : undefined,
540
+ };
541
+ };
542
+ export const listTickets = (projectId, options = {}) => {
543
+ const detail = options.detail ?? 'full';
544
+ const includeFullDetail = detail === 'full';
545
+ const startedAt = performance.now();
546
+ const diffStatsCache = new Map();
547
+ const gitStatusCache = new Map();
548
+ const existingWorktreePaths = new Map();
549
+ const worktreeRecordsCache = new Map();
550
+ const githubConnectionCache = new Map();
551
+ const bundlePullRequestCache = new Map();
552
+ const rows = projectId === undefined
553
+ ? db.prepare(`${ticketSelect}
554
+ ORDER BY
555
+ CASE WHEN tickets.parked_at IS NULL THEN 0 ELSE 1 END ASC,
556
+ CASE WHEN tickets.parked_at IS NULL THEN CASE tickets.state
557
+ WHEN 'plan' THEN ${ACTIVE_STATE_ORDER.plan}
558
+ WHEN 'build' THEN ${ACTIVE_STATE_ORDER.build}
559
+ WHEN 'review' THEN ${ACTIVE_STATE_ORDER.review}
560
+ ELSE 99
561
+ END ELSE 0 END ASC,
562
+ CASE WHEN tickets.parked_at IS NULL THEN COALESCE(tickets.lane_order, 0) ELSE COALESCE(tickets.done_order, 0) END DESC,
563
+ tickets.updated_at DESC,
564
+ tickets.id DESC`).all()
565
+ : db.prepare(`${ticketSelect}
566
+ WHERE tickets.project_id = ?
567
+ ORDER BY
568
+ CASE WHEN tickets.parked_at IS NULL THEN 0 ELSE 1 END ASC,
569
+ CASE WHEN tickets.parked_at IS NULL THEN CASE tickets.state
570
+ WHEN 'plan' THEN ${ACTIVE_STATE_ORDER.plan}
571
+ WHEN 'build' THEN ${ACTIVE_STATE_ORDER.build}
572
+ WHEN 'review' THEN ${ACTIVE_STATE_ORDER.review}
573
+ ELSE 99
574
+ END ELSE 0 END ASC,
575
+ CASE WHEN tickets.parked_at IS NULL THEN COALESCE(tickets.lane_order, 0) ELSE COALESCE(tickets.done_order, 0) END DESC,
576
+ tickets.updated_at DESC,
577
+ tickets.id DESC`).all(projectId);
578
+ const rowsLoadedAt = performance.now();
579
+ const serializedRows = rows
580
+ .map(row => serializeTicket(row, {
581
+ includeDetail: includeFullDetail,
582
+ includeMessages: includeFullDetail,
583
+ includeLogs: includeFullDetail,
584
+ includeUndo: true,
585
+ includeDiffStats: includeFullDetail,
586
+ includeStaleState: includeFullDetail,
587
+ diffStatsCache,
588
+ gitStatusCache,
589
+ existingWorktreePaths,
590
+ worktreeRecordsCache,
591
+ githubConnectionCache,
592
+ bundlePullRequestCache,
593
+ }))
594
+ .filter((ticket) => ticket !== null);
595
+ const rowsSerializedAt = performance.now();
596
+ const tickets = applyBundleExecutionPositions(serializedRows);
597
+ if (TICKET_TIMING_ENABLED) {
598
+ console.info('[perf]', JSON.stringify({
599
+ route: 'listTickets',
600
+ detail,
601
+ project_id: projectId,
602
+ row_count: rows.length,
603
+ total_ms: Number((performance.now() - startedAt).toFixed(1)),
604
+ phases: [
605
+ {
606
+ label: 'load_rows',
607
+ ms: Number((rowsLoadedAt - startedAt).toFixed(1)),
608
+ },
609
+ {
610
+ label: 'serialize_rows',
611
+ ms: Number((rowsSerializedAt - rowsLoadedAt).toFixed(1)),
612
+ },
613
+ {
614
+ label: 'apply_bundle_execution_positions',
615
+ ms: Number((performance.now() - rowsSerializedAt).toFixed(1)),
616
+ },
617
+ ],
618
+ }));
619
+ }
620
+ return tickets;
621
+ };
622
+ export const listBoardTicketEnrichment = (projectId) => {
623
+ const startedAt = performance.now();
624
+ const githubConnectionCache = new Map();
625
+ const bundlePullRequestCache = new Map();
626
+ const worktreeRecordsCache = new Map();
627
+ const rows = projectId === undefined
628
+ ? db.prepare(`${ticketSelect}
629
+ ORDER BY
630
+ CASE WHEN tickets.parked_at IS NULL THEN 0 ELSE 1 END ASC,
631
+ CASE WHEN tickets.parked_at IS NULL THEN CASE tickets.state
632
+ WHEN 'plan' THEN ${ACTIVE_STATE_ORDER.plan}
633
+ WHEN 'build' THEN ${ACTIVE_STATE_ORDER.build}
634
+ WHEN 'review' THEN ${ACTIVE_STATE_ORDER.review}
635
+ ELSE 99
636
+ END ELSE 0 END ASC,
637
+ CASE WHEN tickets.parked_at IS NULL THEN COALESCE(tickets.lane_order, 0) ELSE COALESCE(tickets.done_order, 0) END DESC,
638
+ tickets.updated_at DESC,
639
+ tickets.id DESC`).all()
640
+ : db.prepare(`${ticketSelect}
641
+ WHERE tickets.project_id = ?
642
+ ORDER BY
643
+ CASE WHEN tickets.parked_at IS NULL THEN 0 ELSE 1 END ASC,
644
+ CASE WHEN tickets.parked_at IS NULL THEN CASE tickets.state
645
+ WHEN 'plan' THEN ${ACTIVE_STATE_ORDER.plan}
646
+ WHEN 'build' THEN ${ACTIVE_STATE_ORDER.build}
647
+ WHEN 'review' THEN ${ACTIVE_STATE_ORDER.review}
648
+ ELSE 99
649
+ END ELSE 0 END ASC,
650
+ CASE WHEN tickets.parked_at IS NULL THEN COALESCE(tickets.lane_order, 0) ELSE COALESCE(tickets.done_order, 0) END DESC,
651
+ tickets.updated_at DESC,
652
+ tickets.id DESC`).all(projectId);
653
+ const rowsLoadedAt = performance.now();
654
+ let cacheHits = 0;
655
+ let cacheMisses = 0;
656
+ let bundleMs = 0;
657
+ let worktreeMs = 0;
658
+ let diffStatsMs = 0;
659
+ let gitStatusMs = 0;
660
+ const enrichment = rows.map((row) => {
661
+ const project = getEffectiveProject(row);
662
+ const projectRepoPath = project?.repo_path ?? null;
663
+ const branch = row.bundle_kind === 'project_root'
664
+ ? row.bundle_branch ?? row.branch ?? PROJECT_ROOT_BUNDLE_BRANCH
665
+ : row.bundle_branch ?? row.branch ?? null;
666
+ const bundleStartedAt = performance.now();
667
+ const hasGitHubConnection = row.project_id
668
+ ? resolveProjectGitHubConnection(row.project_id, githubConnectionCache)
669
+ : false;
670
+ const bundle = row.bundle_id
671
+ ? {
672
+ kind: row.bundle_kind === 'project_root' ? 'project_root' : 'worktree',
673
+ pull_request: row.project_id
674
+ ? resolveBundlePullRequest(row.project_id, row.bundle_id, hasGitHubConnection, bundlePullRequestCache)
675
+ : null,
676
+ }
677
+ : null;
678
+ bundleMs += performance.now() - bundleStartedAt;
679
+ let hasWorktree = false;
680
+ let diffStats = null;
681
+ let gitStatus = null;
682
+ if (projectRepoPath && branch) {
683
+ const cacheKey = getWorkspaceDiffKey(projectRepoPath, branch);
684
+ const cached = getCachedBoardEnrichment(cacheKey);
685
+ if (cached) {
686
+ cacheHits += 1;
687
+ hasWorktree = cached.hasWorktree;
688
+ diffStats = cached.diffStats;
689
+ gitStatus = cached.gitStatus;
690
+ }
691
+ else {
692
+ cacheMisses += 1;
693
+ const worktreeStartedAt = performance.now();
694
+ if (row.bundle_kind === 'project_root') {
695
+ hasWorktree = true;
696
+ }
697
+ else {
698
+ hasWorktree =
699
+ resolveTicketWorktree(row, projectRepoPath, branch, undefined, worktreeRecordsCache) !== null;
700
+ }
701
+ worktreeMs += performance.now() - worktreeStartedAt;
702
+ if (hasWorktree) {
703
+ const diffStatsStartedAt = performance.now();
704
+ diffStats = row.state === 'plan'
705
+ ? null
706
+ : row.bundle_kind === 'project_root'
707
+ ? summarizeTicketDiffFiles(getGitDiffFiles(projectRepoPath))
708
+ : project
709
+ ? getWorktreeDiffStats(branch, project)
710
+ : null;
711
+ diffStatsMs += performance.now() - diffStatsStartedAt;
712
+ const gitStatusStartedAt = performance.now();
713
+ gitStatus = shouldResolveStaleGitStatus(row, true)
714
+ ? row.bundle_kind === 'project_root'
715
+ ? getBoardGitStatus(projectRepoPath, null, project)
716
+ : project
717
+ ? getBoardWorktreeGitStatus(branch, project)
718
+ : null
719
+ : null;
720
+ gitStatusMs += performance.now() - gitStatusStartedAt;
721
+ }
722
+ boardEnrichmentCache.set(cacheKey, {
723
+ cachedAt: Date.now(),
724
+ hasWorktree,
725
+ diffStats,
726
+ gitStatus,
727
+ });
728
+ }
729
+ }
730
+ return {
731
+ id: row.id,
732
+ diff_stats: diffStats,
733
+ is_stale: isTicketStale({
734
+ state: row.state,
735
+ build_completed_at: row.build_completed_at,
736
+ project_id: row.project_id,
737
+ worktree_bundle_id: row.worktree_bundle_id,
738
+ }, gitStatus, bundle, hasGitHubConnection),
739
+ };
740
+ });
741
+ if (TICKET_TIMING_ENABLED) {
742
+ console.info('[perf]', JSON.stringify({
743
+ route: 'listBoardTicketEnrichment',
744
+ project_id: projectId,
745
+ row_count: rows.length,
746
+ total_ms: Number((performance.now() - startedAt).toFixed(1)),
747
+ cache_hits: cacheHits,
748
+ cache_misses: cacheMisses,
749
+ phases: [
750
+ {
751
+ label: 'load_rows',
752
+ ms: Number((rowsLoadedAt - startedAt).toFixed(1)),
753
+ },
754
+ {
755
+ label: 'resolve_bundle_state',
756
+ ms: Number(bundleMs.toFixed(1)),
757
+ },
758
+ {
759
+ label: 'resolve_worktree',
760
+ ms: Number(worktreeMs.toFixed(1)),
761
+ },
762
+ {
763
+ label: 'resolve_diff_stats',
764
+ ms: Number(diffStatsMs.toFixed(1)),
765
+ },
766
+ {
767
+ label: 'resolve_git_status',
768
+ ms: Number(gitStatusMs.toFixed(1)),
769
+ },
770
+ ],
771
+ }));
772
+ }
773
+ return enrichment;
774
+ };
775
+ export const getTicket = (id) => {
776
+ const row = db.prepare(`${ticketSelect} WHERE tickets.id = ?`).get(id);
777
+ const ticket = serializeTicket(row, {
778
+ includeDetail: true,
779
+ githubConnectionCache: new Map(),
780
+ bundlePullRequestCache: new Map(),
781
+ });
782
+ if (!ticket) {
783
+ return null;
784
+ }
785
+ if (!ticket.project_id || !ticket.worktree_bundle_id) {
786
+ return ticket;
787
+ }
788
+ const bundleTickets = db.prepare('SELECT id, title, created_at FROM tickets WHERE project_id = ? AND worktree_bundle_id = ?').all(ticket.project_id, ticket.worktree_bundle_id);
789
+ const bundleTicketIds = bundleTickets.map(entry => entry.id);
790
+ const latestExecutionOrder = getStableBundleExecutionOrder(ticket.project_id, ticket.worktree_bundle_id);
791
+ const fallbackDependencyOrder = getDependencyOrderedBundleTicketIds(ticket.project_id, bundleTicketIds);
792
+ const fallbackTicketIds = [...bundleTickets]
793
+ .sort(compareBundleTickets)
794
+ .map(entry => entry.id);
795
+ const orderedTicketIds = [
796
+ ...latestExecutionOrder,
797
+ ...fallbackDependencyOrder,
798
+ ...fallbackTicketIds,
799
+ ].filter((ticketId, index, values) => bundleTicketIds.includes(ticketId) && values.indexOf(ticketId) === index);
800
+ const bundleExecutionPosition = orderedTicketIds.findIndex(ticketId => ticketId === ticket.id);
801
+ return {
802
+ ...ticket,
803
+ bundle_execution_position: bundleExecutionPosition >= 0 ? bundleExecutionPosition : null,
804
+ };
805
+ };
806
+ export const createTicketRecord = ({ title, description, model, variant, skills, tool, agentStatus, projectId, worktreeBundleId, planningContext, externalSource, }) => {
807
+ const id = randomUUID();
808
+ const laneOrder = getNextLaneOrder(projectId, 'plan');
809
+ const skillsJson = JSON.stringify(skills);
810
+ db.prepare(`INSERT INTO tickets (
811
+ id,
812
+ title,
813
+ description,
814
+ model,
815
+ variant,
816
+ skills_json,
817
+ tool,
818
+ agent_status,
819
+ worktree_bundle_id,
820
+ project_id,
821
+ planning_context_json,
822
+ lane_order,
823
+ external_provider,
824
+ external_id,
825
+ external_key,
826
+ external_url,
827
+ external_metadata_json
828
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, title.trim(), description, model, variant, skillsJson, tool, agentStatus ?? null, worktreeBundleId ?? null, projectId, serializePlanningContext(planningContext), laneOrder, externalSource?.provider ?? null, externalSource?.external_id ?? null, externalSource?.external_key ?? null, externalSource?.external_url ?? null, externalSource?.metadata ? JSON.stringify(externalSource.metadata) : null);
829
+ upsertTicketDescriptionMessage(id, description);
830
+ return getTicket(id);
831
+ };
832
+ export const autoParkTicketIfStale = (ticketId) => {
833
+ let ticket = reconcileAutoParkDismissal(ticketId);
834
+ if (!ticket) {
835
+ return ticket;
836
+ }
837
+ if (ticket.is_stale !== true) {
838
+ return ticket;
839
+ }
840
+ const project = getEffectiveProject(ticket);
841
+ if (!project || !project.auto_park_stale_tickets || !canAutoParkTicket(ticket) || ticket.auto_park_dismissed_at) {
842
+ return ticket;
843
+ }
844
+ db.prepare('UPDATE tickets SET parked_at = CURRENT_TIMESTAMP, done_order = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(assignTicketDoneOrder(ticket) ?? null, ticket.id);
845
+ return getTicket(ticket.id);
846
+ };
847
+ export const listTicketsInRunContext = (ticket, options = {}) => {
848
+ if (!ticket) {
849
+ return [];
850
+ }
851
+ if (ticket.worktree_bundle_id) {
852
+ const includeDetail = options.includeDetail ?? true;
853
+ const diffStatsCache = new Map();
854
+ const gitStatusCache = new Map();
855
+ const existingWorktreePaths = new Map();
856
+ const worktreeRecordsCache = new Map();
857
+ const githubConnectionCache = new Map();
858
+ const bundlePullRequestCache = new Map();
859
+ const rows = db.prepare(`${ticketSelect} WHERE tickets.worktree_bundle_id = ? AND tickets.project_id = ?
860
+ ORDER BY
861
+ CASE WHEN tickets.parked_at IS NULL THEN 0 ELSE 1 END ASC,
862
+ CASE WHEN tickets.parked_at IS NULL THEN CASE tickets.state
863
+ WHEN 'plan' THEN ${ACTIVE_STATE_ORDER.plan}
864
+ WHEN 'build' THEN ${ACTIVE_STATE_ORDER.build}
865
+ WHEN 'review' THEN ${ACTIVE_STATE_ORDER.review}
866
+ ELSE 99
867
+ END ELSE 0 END ASC,
868
+ CASE WHEN tickets.parked_at IS NULL THEN COALESCE(tickets.lane_order, 0) ELSE COALESCE(tickets.done_order, 0) END DESC,
869
+ tickets.updated_at DESC,
870
+ tickets.id DESC`).all(ticket.worktree_bundle_id, ticket.project_id);
871
+ return applyBundleExecutionPositions(rows
872
+ .map(row => serializeTicket(row, {
873
+ includeDetail,
874
+ includeMessages: options.includeMessages,
875
+ includeLogs: options.includeLogs,
876
+ includeUndo: options.includeUndo,
877
+ includeDiffStats: options.includeDiffStats,
878
+ includeStaleState: options.includeStaleState,
879
+ includeWorktreeState: options.includeWorktreeState,
880
+ diffStatsCache,
881
+ gitStatusCache,
882
+ existingWorktreePaths,
883
+ worktreeRecordsCache,
884
+ githubConnectionCache,
885
+ bundlePullRequestCache,
886
+ }))
887
+ .filter((entry) => entry !== null));
888
+ }
889
+ const resolvedTicket = getTicket(ticket.id);
890
+ return resolvedTicket ? [resolvedTicket] : [];
891
+ };
892
+ export const listBundles = (projectId) => {
893
+ const rows = db.prepare(`${bundleSelect} WHERE project_id = ? ORDER BY updated_at DESC, created_at DESC`).all(projectId);
894
+ const project = getProjectById(projectId);
895
+ const projectBranch = getProjectBranch(project, { includeDiffStats: false }).branch;
896
+ return rows
897
+ .map(row => normalizeBundleRow(row))
898
+ .filter((row) => row !== null)
899
+ .map(row => {
900
+ if (row.kind !== 'project_root') {
901
+ return row;
902
+ }
903
+ return {
904
+ ...row,
905
+ branch: projectBranch ?? PROJECT_ROOT_BUNDLE_BRANCH,
906
+ };
907
+ });
908
+ };
909
+ export const getBundle = (id, projectId) => {
910
+ const row = db.prepare(`${bundleSelect} WHERE id = ? AND project_id = ?`).get(id, projectId);
911
+ return normalizeBundleRow(row);
912
+ };
913
+ export const getBundleByName = (name, projectId) => {
914
+ const row = db.prepare(`${bundleSelect} WHERE name = ? AND project_id = ?`).get(name, projectId);
915
+ return normalizeBundleRow(row);
916
+ };
917
+ export const getBundleByBranch = (branch, projectId) => {
918
+ const row = db.prepare(`${bundleSelect} WHERE branch = ? AND project_id = ?`).get(branch, projectId);
919
+ return normalizeBundleRow(row);
920
+ };
921
+ export const getBundleRepresentativeTicketContext = (projectId, bundleId) => {
922
+ return db.prepare(`
923
+ SELECT id, project_id, worktree_bundle_id
924
+ FROM tickets
925
+ WHERE project_id = ?
926
+ AND worktree_bundle_id = ?
927
+ ORDER BY rowid ASC
928
+ LIMIT 1
929
+ `).get(projectId, bundleId) ?? null;
930
+ };
931
+ export const createBundle = ({ name, branch, projectId }) => {
932
+ return createBundleRecord({ name, branch, projectId, kind: 'worktree' });
933
+ };
934
+ export const createBundleRecord = ({ name, branch, projectId, kind, }) => {
935
+ const id = randomUUID();
936
+ db.prepare('INSERT INTO worktree_bundles (id, name, branch, project_id, kind) VALUES (?, ?, ?, ?, ?)').run(id, name, branch, projectId, kind);
937
+ return getBundle(id, projectId);
938
+ };
939
+ export const getProjectRootBundle = (projectId) => {
940
+ const row = db.prepare(`${bundleSelect} WHERE project_id = ? AND kind = 'project_root' LIMIT 1`).get(projectId);
941
+ return normalizeBundleRow(row);
942
+ };
943
+ export const ensureProjectRootBundle = (projectId) => {
944
+ const existing = getProjectRootBundle(projectId);
945
+ if (existing) {
946
+ return existing;
947
+ }
948
+ return createBundleRecord({
949
+ name: PROJECT_ROOT_BUNDLE_NAME,
950
+ branch: PROJECT_ROOT_BUNDLE_BRANCH,
951
+ projectId,
952
+ kind: 'project_root',
953
+ });
954
+ };
955
+ export const isProjectRootBundle = (bundle) => bundle?.kind === 'project_root';
956
+ export const isProjectRootBundleId = (bundleId, projectId) => {
957
+ if (!bundleId || !projectId) {
958
+ return false;
959
+ }
960
+ return getProjectRootBundle(projectId)?.id === bundleId;
961
+ };
962
+ export const getProjectRootBundleName = () => PROJECT_ROOT_BUNDLE_NAME;
963
+ export const deleteBundle = (id, projectId) => {
964
+ db.prepare('DELETE FROM worktree_bundles WHERE id = ? AND project_id = ?').run(id, projectId);
965
+ };
966
+ export const countBundleTickets = (id, projectId) => {
967
+ const row = db.prepare('SELECT COUNT(*) AS count FROM tickets WHERE worktree_bundle_id = ? AND project_id = ?').get(id, projectId);
968
+ return Number(row?.count ?? 0);
969
+ };
970
+ export const isBundledTicket = (ticket) => Boolean(ticket?.worktree_bundle_id);
971
+ export const resolveTicketBranch = (ticket) => ticket?.bundle?.branch ?? ticket?.bundle_branch ?? ticket?.branch ?? null;
972
+ export const resolveTicketContextKey = (ticket) => ticket?.worktree_bundle_id ?? ticket?.id ?? null;
973
+ export const resolveTicketWorkerKey = (ticket, workerKind) => {
974
+ const contextKey = resolveTicketContextKey(ticket);
975
+ if (!contextKey) {
976
+ return null;
977
+ }
978
+ return `${workerKind}:${contextKey}`;
979
+ };
980
+ export const resolveTicketTool = (ticket, fallbackTool = 'opencode') => ticket?.tool ?? fallbackTool;
981
+ //# sourceMappingURL=tickets.js.map