@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.
- package/README.md +224 -0
- package/dist/cli.js +216 -0
- package/dist/client/assets/index-8_-boBBA.css +2 -0
- package/dist/client/assets/index-s_jjeqha.js +176 -0
- package/dist/client/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
- package/dist/client/favicon.svg +62 -0
- package/dist/client/icons.svg +24 -0
- package/dist/client/index.html +14 -0
- package/dist/server/db.js +764 -0
- package/dist/server/db.js.map +1 -0
- package/dist/server/index.js +5134 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/lib/agent.js +1302 -0
- package/dist/server/lib/agent.js.map +1 -0
- package/dist/server/lib/buildChains.js +2 -0
- package/dist/server/lib/buildChains.js.map +1 -0
- package/dist/server/lib/buildFlow.js +59 -0
- package/dist/server/lib/buildFlow.js.map +1 -0
- package/dist/server/lib/buildSequences.js +599 -0
- package/dist/server/lib/buildSequences.js.map +1 -0
- package/dist/server/lib/bundleActivity.js +95 -0
- package/dist/server/lib/bundleActivity.js.map +1 -0
- package/dist/server/lib/bundlePullRequests.js +126 -0
- package/dist/server/lib/bundlePullRequests.js.map +1 -0
- package/dist/server/lib/chatMessages.js +60 -0
- package/dist/server/lib/chatMessages.js.map +1 -0
- package/dist/server/lib/chatTargets.js +123 -0
- package/dist/server/lib/chatTargets.js.map +1 -0
- package/dist/server/lib/chatTicketProposals.js +180 -0
- package/dist/server/lib/chatTicketProposals.js.map +1 -0
- package/dist/server/lib/chats.js +279 -0
- package/dist/server/lib/chats.js.map +1 -0
- package/dist/server/lib/config.js +3 -0
- package/dist/server/lib/config.js.map +1 -0
- package/dist/server/lib/cors.js +30 -0
- package/dist/server/lib/cors.js.map +1 -0
- package/dist/server/lib/directoryPicker.js +174 -0
- package/dist/server/lib/directoryPicker.js.map +1 -0
- package/dist/server/lib/git.js +1284 -0
- package/dist/server/lib/git.js.map +1 -0
- package/dist/server/lib/integrations/github.js +511 -0
- package/dist/server/lib/integrations/github.js.map +1 -0
- package/dist/server/lib/integrations/index.js +162 -0
- package/dist/server/lib/integrations/index.js.map +1 -0
- package/dist/server/lib/integrations/jira.js +283 -0
- package/dist/server/lib/integrations/jira.js.map +1 -0
- package/dist/server/lib/integrations/planning.js +27 -0
- package/dist/server/lib/integrations/planning.js.map +1 -0
- package/dist/server/lib/integrations/types.js +2 -0
- package/dist/server/lib/integrations/types.js.map +1 -0
- package/dist/server/lib/lightweightPrompt.js +88 -0
- package/dist/server/lib/lightweightPrompt.js.map +1 -0
- package/dist/server/lib/models.js +219 -0
- package/dist/server/lib/models.js.map +1 -0
- package/dist/server/lib/preview.js +377 -0
- package/dist/server/lib/preview.js.map +1 -0
- package/dist/server/lib/previewProxy.js +659 -0
- package/dist/server/lib/previewProxy.js.map +1 -0
- package/dist/server/lib/projectAutoConfig.js +682 -0
- package/dist/server/lib/projectAutoConfig.js.map +1 -0
- package/dist/server/lib/projectFileSuggestions.js +133 -0
- package/dist/server/lib/projectFileSuggestions.js.map +1 -0
- package/dist/server/lib/projectMemory.js +1519 -0
- package/dist/server/lib/projectMemory.js.map +1 -0
- package/dist/server/lib/projectMemoryPrompt.js +390 -0
- package/dist/server/lib/projectMemoryPrompt.js.map +1 -0
- package/dist/server/lib/projectMemoryScan.js +681 -0
- package/dist/server/lib/projectMemoryScan.js.map +1 -0
- package/dist/server/lib/projectMemorySuggestions.js +166 -0
- package/dist/server/lib/projectMemorySuggestions.js.map +1 -0
- package/dist/server/lib/projectMemoryTransfer.js +958 -0
- package/dist/server/lib/projectMemoryTransfer.js.map +1 -0
- package/dist/server/lib/projects.js +569 -0
- package/dist/server/lib/projects.js.map +1 -0
- package/dist/server/lib/promptSkills.js +28 -0
- package/dist/server/lib/promptSkills.js.map +1 -0
- package/dist/server/lib/queue.js +15 -0
- package/dist/server/lib/queue.js.map +1 -0
- package/dist/server/lib/reviewFindings.js +390 -0
- package/dist/server/lib/reviewFindings.js.map +1 -0
- package/dist/server/lib/run.js +416 -0
- package/dist/server/lib/run.js.map +1 -0
- package/dist/server/lib/runtimePaths.js +93 -0
- package/dist/server/lib/runtimePaths.js.map +1 -0
- package/dist/server/lib/shell.js +27 -0
- package/dist/server/lib/shell.js.map +1 -0
- package/dist/server/lib/skills.js +124 -0
- package/dist/server/lib/skills.js.map +1 -0
- package/dist/server/lib/startDev.js +18 -0
- package/dist/server/lib/startDev.js.map +1 -0
- package/dist/server/lib/staticClient.js +80 -0
- package/dist/server/lib/staticClient.js.map +1 -0
- package/dist/server/lib/terminal.js +366 -0
- package/dist/server/lib/terminal.js.map +1 -0
- package/dist/server/lib/ticketDependencies.js +174 -0
- package/dist/server/lib/ticketDependencies.js.map +1 -0
- package/dist/server/lib/ticketMessages.js +65 -0
- package/dist/server/lib/ticketMessages.js.map +1 -0
- package/dist/server/lib/ticketOpenQuestions.js +128 -0
- package/dist/server/lib/ticketOpenQuestions.js.map +1 -0
- package/dist/server/lib/ticketUndo.js +549 -0
- package/dist/server/lib/ticketUndo.js.map +1 -0
- package/dist/server/lib/tickets.js +981 -0
- package/dist/server/lib/tickets.js.map +1 -0
- package/dist/server/lib/types.js +2 -0
- package/dist/server/lib/types.js.map +1 -0
- package/dist/server/package.json +3 -0
- package/dist/server/workers/build.js +229 -0
- package/dist/server/workers/build.js.map +1 -0
- package/dist/server/workers/chat.js +190 -0
- package/dist/server/workers/chat.js.map +1 -0
- package/dist/server/workers/followUp.js +204 -0
- package/dist/server/workers/followUp.js.map +1 -0
- package/dist/server/workers/plan.js +1130 -0
- package/dist/server/workers/plan.js.map +1 -0
- package/dist/server/workers/planFollowUp.js +360 -0
- package/dist/server/workers/planFollowUp.js.map +1 -0
- package/dist/server/workers/review.js +167 -0
- package/dist/server/workers/review.js.map +1 -0
- 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
|