@straiffi/archon 1.1.3 → 1.2.1
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 +4 -0
- package/dist/client/assets/TestsDialog-4_jJ_cnr.js +5 -0
- package/dist/client/assets/badge-Bpry9xkS.js +41 -0
- package/dist/client/assets/index-BgmAtYCf.js +151 -0
- package/dist/client/assets/index-Bw66dtZG.css +2 -0
- package/dist/client/index.html +3 -3
- package/dist/server/index.js +206 -445
- package/dist/server/index.js.map +1 -1
- package/dist/server/lib/chatOperations.js +126 -0
- package/dist/server/lib/chatOperations.js.map +1 -0
- package/dist/server/lib/desktopServerHost.js +16 -0
- package/dist/server/lib/desktopServerHost.js.map +1 -0
- package/dist/server/lib/mobileAccess.js +1051 -0
- package/dist/server/lib/mobileAccess.js.map +1 -0
- package/dist/server/lib/mobileAccessSecurity.js +42 -0
- package/dist/server/lib/mobileAccessSecurity.js.map +1 -0
- package/dist/server/lib/ngrok.js +144 -0
- package/dist/server/lib/ngrok.js.map +1 -0
- package/dist/server/lib/projects.js +1 -0
- package/dist/server/lib/projects.js.map +1 -1
- package/dist/server/lib/realtime.js +2 -0
- package/dist/server/lib/realtime.js.map +1 -0
- package/dist/server/lib/run.js +3 -0
- package/dist/server/lib/run.js.map +1 -1
- package/dist/server/lib/staticClient.js +14 -3
- package/dist/server/lib/staticClient.js.map +1 -1
- package/dist/server/lib/ticketFollowUpOperations.js +87 -0
- package/dist/server/lib/ticketFollowUpOperations.js.map +1 -0
- package/dist/server/lib/ticketRunOperations.js +333 -0
- package/dist/server/lib/ticketRunOperations.js.map +1 -0
- package/dist/server/lib/ticketSettingsOperations.js +62 -0
- package/dist/server/lib/ticketSettingsOperations.js.map +1 -0
- package/dist/server/lib/ticketWorkflowOperations.js +114 -0
- package/dist/server/lib/ticketWorkflowOperations.js.map +1 -0
- package/package.json +1 -1
- package/dist/client/assets/TestsDialog-Buw5J9nT.js +0 -5
- package/dist/client/assets/badge-BentDQnz.js +0 -41
- package/dist/client/assets/index-Dr_tNX7Y.css +0 -2
- package/dist/client/assets/index-kbsOL4Bp.js +0 -142
package/dist/server/index.js
CHANGED
|
@@ -7,8 +7,9 @@ import { randomUUID } from 'crypto';
|
|
|
7
7
|
import cors from 'cors';
|
|
8
8
|
import { adjectives, animals, uniqueNamesGenerator } from 'unique-names-generator';
|
|
9
9
|
import db from './db.js';
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
10
|
+
import { deleteChatSession, getChatSession, listChatSessions, updateChatSessionMode } from './lib/chats.js';
|
|
11
|
+
import { createProjectChatSession, resolveChatMode, resolveChatTool, stopProjectChatSessionResponse, submitProjectChatSessionMessage, } from './lib/chatOperations.js';
|
|
12
|
+
import { resolveChatSessionTarget } from './lib/chatTargets.js';
|
|
12
13
|
import { createChatMessage, getChatMessage, updateChatMessageContent } from './lib/chatMessages.js';
|
|
13
14
|
import { attachChatTicketProposalsToMessageContent } from './lib/chatTicketProposals.js';
|
|
14
15
|
import { createCorsOriginResolver, parseCorsOrigins } from './lib/cors.js';
|
|
@@ -16,12 +17,15 @@ import { branchExistsLocally, commitGitChanges, createWorktreeAsync, deleteWorkt
|
|
|
16
17
|
import config from './lib/config.js';
|
|
17
18
|
import { autoConfigProject, ProjectAutoConfigError } from './lib/projectAutoConfig.js';
|
|
18
19
|
import { browseRepoPath } from './lib/directoryPicker.js';
|
|
19
|
-
import {
|
|
20
|
+
import { resolveDesktopServerHost } from './lib/desktopServerHost.js';
|
|
21
|
+
import { emitMobileRealtime, getMobileAccessStatus, regenerateMobilePairingChallenge, revokeMobileSession, startMobileAccess, stopMobileAccess, } from './lib/mobileAccess.js';
|
|
22
|
+
import { validateDesktopMobileAccessRequest } from './lib/mobileAccessSecurity.js';
|
|
23
|
+
import { refreshChatSessionUsageSnapshot, stopAllBuildAgents } from './lib/agent.js';
|
|
20
24
|
import { runLightweightPrompt } from './lib/lightweightPrompt.js';
|
|
21
25
|
import { startBuildChain, stopBuildChain } from './lib/buildChains.js';
|
|
22
|
-
import { enqueueBundledBuild, getDependencyOrderedBundleTicketIds,
|
|
26
|
+
import { enqueueBundledBuild, getDependencyOrderedBundleTicketIds, removeTicketFromBuildSequence, stopAllActiveBuildSequences, } from './lib/buildSequences.js';
|
|
23
27
|
import { prepareTicketForBuild } from './lib/buildFlow.js';
|
|
24
|
-
import { getBundlePlanMutationBlockerById,
|
|
28
|
+
import { getBundlePlanMutationBlockerById, shouldBlockPlanForBundleMutationById, } from './lib/bundleActivity.js';
|
|
25
29
|
import { getDiscoveredModels, normalizeModelId } from './lib/models.js';
|
|
26
30
|
import { acceptProjectMemorySuggestion, applyProjectMemorySuggestionToDecision, archiveProjectConvention, archiveProjectDecision, createProjectConvention, createProjectDecision, dismissProjectMemorySuggestion, getProjectMemoryById, getProjectMemorySuggestionById, isProjectMemoryEnabled, listProjectConventions, listLatestSuccessfulProjectContextArtifacts, listProjectDecisions, listProjectMemories, listProjectMemorySuggestions, restoreProjectConvention, restoreProjectDecision, serializeProjectMemoryFromConventionValue, serializeProjectMemoryFromDecisionValue, serializeProjectMemoryFromSuggestionValue, supersedeProjectDecision, updateProjectConvention, updateProjectDecision, validateProjectConventionPayload, validateProjectDecisionPayload, } from './lib/projectMemory.js';
|
|
27
31
|
import { selectRelevantProjectMemory } from './lib/projectMemoryPrompt.js';
|
|
@@ -39,17 +43,19 @@ import { importJiraIssue, JiraApiError, validateJiraConnection } from './lib/int
|
|
|
39
43
|
import { JiraPlanningReferenceError, resolveJiraPlanningDescription } from './lib/integrations/planning.js';
|
|
40
44
|
import { getBundlePullRequestRecord, hasOpenBundlePullRequest, serializeBundlePullRequest, shouldSyncBundlePullRequest, upsertBundlePullRequest, } from './lib/bundlePullRequests.js';
|
|
41
45
|
import { countInboundProjectLinks, getEffectiveProject, getProjectById, getProjectTicketStats, hasProjectRunIde, listProjects, replaceProjectLinks, serializeProject, updateProjectActiveTarget, validateProjectPayload, validateRepoPath, } from './lib/projects.js';
|
|
42
|
-
import { getActiveRunStatus, getActiveRunStatusForProject, getPreviewStatusForProject, hasRunConfig, isRunning, openIde, openTicketIdeInWorkspace, resolveProjectRunContextKey,
|
|
46
|
+
import { getActiveRunStatus, getActiveRunStatusForProject, getPreviewStatusForProject, hasRunConfig, isRunning, openIde, openTicketIdeInWorkspace, resolveProjectRunContextKey, runSetupCommandsInWorkspace, runTicketInWorkspace, stopAllTickets, stopTicket } from './lib/run.js';
|
|
43
47
|
import { clearAllTestSessions, createTestSession, deleteTestSession, discoverTestsForSession, doesTestFileExistForSession, findProjectTestCommand, getTestSession, isTestSelectionSupported, startTestSessionRun, stopTestSessionRun } from './lib/testSessions.js';
|
|
48
|
+
import { getBundleBuildBlockerResponse, shouldStartBundleBuildNow, transitionTicketState, } from './lib/ticketWorkflowOperations.js';
|
|
49
|
+
import { submitPlanTicketFollowUp, submitTicketFollowUp, } from './lib/ticketFollowUpOperations.js';
|
|
50
|
+
import { canUseBuildControls, formatActiveRunConflict, getTicketRunReadinessError, resumeBuildTicketRun, runProjectTarget, runTicketRun, stopBuildTicketRun, stopProjectTargetRun, stopTicketRun, } from './lib/ticketRunOperations.js';
|
|
51
|
+
import { isTicketAgentSettingsPayload, updateTicketAgentSettings, } from './lib/ticketSettingsOperations.js';
|
|
44
52
|
import { getActiveBundleStageChangeHead, undoTicketStage } from './lib/ticketUndo.js';
|
|
45
53
|
import { closeAllTerminalSessions, createProjectTerminalSession, createTerminalSessionForWorkspace, destroyTerminalSessionById, registerTerminalSocketHandlers, } from './lib/terminal.js';
|
|
46
54
|
import { countBundleTickets, createTicketRecord, createBundle, deleteBundle, ensureProjectRootBundle, autoParkTicketIfStale, clearAutoParkDismissalIfNeeded, assignTicketDoneOrder, assignTicketLaneOrder, getBundle, getBundleByName, getBundleByBranch, getBundleRepresentativeTicketContext, isProjectRootBundle, getTicket, isBundledTicket, listTicketsInRunContext, listBundles, listBoardTicketEnrichment, listTickets, resolveTicketBranch, resolveTicketTool, } from './lib/tickets.js';
|
|
47
55
|
import { createTicketMessage, upsertTicketDescriptionMessage } from './lib/ticketMessages.js';
|
|
48
56
|
import { createTicketDependency, deleteTicketDependenciesForTicket, deleteTicketDependency, getTicketDependency, isTicketRunSetupReady, listTicketDependencies, validateCreateTicketDependency, } from './lib/ticketDependencies.js';
|
|
49
|
-
import {
|
|
50
|
-
import { startChatSessionTurn } from './workers/chat.js';
|
|
57
|
+
import { startBuild } from './workers/build.js';
|
|
51
58
|
import { startFollowUp } from './workers/followUp.js';
|
|
52
|
-
import { startPlanFollowUp } from './workers/planFollowUp.js';
|
|
53
59
|
import { expandPlanningTicketOutline, generatePlanningTicketOutline, generatePlanText, IMPLEMENTATION_READY_DESCRIPTION_SECTION_HEADINGS, prepareLinkedPlanningResearch, startPlan, } from './workers/plan.js';
|
|
54
60
|
import { startReview } from './workers/review.js';
|
|
55
61
|
class HttpError extends Error {
|
|
@@ -151,34 +157,9 @@ const getParkTicketError = (ticket) => {
|
|
|
151
157
|
}
|
|
152
158
|
return 'Only build and review tickets can be parked';
|
|
153
159
|
};
|
|
154
|
-
const getBundleBuildBlockerError = (kind) => {
|
|
155
|
-
if (kind === 'review') {
|
|
156
|
-
return 'A review is currently active in this bundle. Finish or stop it before starting a new build.';
|
|
157
|
-
}
|
|
158
|
-
return 'A follow-up job is currently modifying this bundle. Finish it before starting a new build.';
|
|
159
|
-
};
|
|
160
160
|
const getBundlePlanBlockerError = () => {
|
|
161
161
|
return 'AI planning is blocked because this bundle worktree is currently being modified.';
|
|
162
162
|
};
|
|
163
|
-
const isBuildBlockerKind = (kind) => {
|
|
164
|
-
return kind === 'review' || kind === 'build_follow_up' || kind === 'review_follow_up';
|
|
165
|
-
};
|
|
166
|
-
const getBundleBuildBlockerResponse = (ticket) => {
|
|
167
|
-
const blocker = isBundleBuildBlocked(ticket);
|
|
168
|
-
if (!blocker || !isBuildBlockerKind(blocker.kind)) {
|
|
169
|
-
return null;
|
|
170
|
-
}
|
|
171
|
-
return {
|
|
172
|
-
error: getBundleBuildBlockerError(blocker.kind),
|
|
173
|
-
blocker_ticket_id: blocker.ticketId,
|
|
174
|
-
};
|
|
175
|
-
};
|
|
176
|
-
const shouldStartBundleBuildNow = (ticket) => {
|
|
177
|
-
if (!ticket.project_id || !ticket.worktree_bundle_id) {
|
|
178
|
-
return true;
|
|
179
|
-
}
|
|
180
|
-
return !hasActiveSequenceForBundle(ticket.project_id, ticket.worktree_bundle_id);
|
|
181
|
-
};
|
|
182
163
|
const deleteTicketRecord = async (ticket) => {
|
|
183
164
|
if (ticket.agent_status === 'running') {
|
|
184
165
|
return { ok: false, status: 409, error: 'Cannot delete a ticket while the agent is running' };
|
|
@@ -275,6 +256,12 @@ const httpServer = createServer(app);
|
|
|
275
256
|
const io = new Server(httpServer, {
|
|
276
257
|
cors: { origin: CORS_ORIGIN_RESOLVER },
|
|
277
258
|
});
|
|
259
|
+
const emitDesktopRealtime = io.emit.bind(io);
|
|
260
|
+
io.emit = ((event, ...args) => {
|
|
261
|
+
const result = emitDesktopRealtime(event, ...args);
|
|
262
|
+
emitMobileRealtime(event, args[0]);
|
|
263
|
+
return result;
|
|
264
|
+
});
|
|
278
265
|
app.use(cors({ origin: CORS_ORIGIN_RESOLVER }));
|
|
279
266
|
const hasOwn = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
|
|
280
267
|
const resolveRequestedSkills = (value) => normalizeSkillNames(value);
|
|
@@ -517,19 +504,6 @@ const asProjectPayload = (body) => {
|
|
|
517
504
|
const isWorktreeReady = (branch, project) => {
|
|
518
505
|
return existsSync(resolveWorktreePath(branch, project));
|
|
519
506
|
};
|
|
520
|
-
const getTicketRunReadinessError = (ticket, project) => {
|
|
521
|
-
const branch = resolveTicketBranch(ticket);
|
|
522
|
-
if (!branch || !isWorktreeReady(branch, project)) {
|
|
523
|
-
return 'Worktree not ready yet';
|
|
524
|
-
}
|
|
525
|
-
if (isTicketRunSetupReady(ticket)) {
|
|
526
|
-
return null;
|
|
527
|
-
}
|
|
528
|
-
if (ticket.run_setup_status === 'preparing') {
|
|
529
|
-
return 'Worktree setup is still running. Try again in a moment.';
|
|
530
|
-
}
|
|
531
|
-
return 'Worktree setup has not finished yet.';
|
|
532
|
-
};
|
|
533
507
|
// Any ticket left in 'running' on startup had its job lost when the server stopped
|
|
534
508
|
db.prepare("UPDATE tickets SET agent_status = 'error' WHERE agent_status = 'running'").run();
|
|
535
509
|
stopAllActiveBuildSequences();
|
|
@@ -573,15 +547,6 @@ const getProjectChatSession = (projectId, chatSessionId) => {
|
|
|
573
547
|
}
|
|
574
548
|
return chatSession;
|
|
575
549
|
};
|
|
576
|
-
const resolveChatMode = (value) => {
|
|
577
|
-
return value === 'plan' ? 'plan' : 'build';
|
|
578
|
-
};
|
|
579
|
-
const resolveChatTool = (value) => {
|
|
580
|
-
if (value === 'claude') {
|
|
581
|
-
return 'claude';
|
|
582
|
-
}
|
|
583
|
-
return 'opencode';
|
|
584
|
-
};
|
|
585
550
|
const resolveTicketDiffWorkspace = (ticket) => {
|
|
586
551
|
const project = getEffectiveProject(ticket);
|
|
587
552
|
if (!project) {
|
|
@@ -1877,9 +1842,6 @@ const buildChatTicketCreationResultMessage = ({ createdTickets, createdDependenc
|
|
|
1877
1842
|
dependencyLine,
|
|
1878
1843
|
].filter(Boolean).join('\n\n');
|
|
1879
1844
|
};
|
|
1880
|
-
const canUseBuildControls = (ticket) => {
|
|
1881
|
-
return ticket.state === 'build' || (ticket.state === 'review' && ticket.review_follow_up_active === true);
|
|
1882
|
-
};
|
|
1883
1845
|
const emitRunStateForContext = (ticket, isRunningValue) => {
|
|
1884
1846
|
const relatedTickets = listTicketsInRunContext(ticket);
|
|
1885
1847
|
if (relatedTickets.length === 0) {
|
|
@@ -1890,38 +1852,70 @@ const emitRunStateForContext = (ticket, isRunningValue) => {
|
|
|
1890
1852
|
io.emit('ticket:updated', { ...relatedTicket, is_running: isRunningValue });
|
|
1891
1853
|
}
|
|
1892
1854
|
};
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
if (activeRun.kind === 'project') {
|
|
1899
|
-
const activeProject = getProjectById(activeRun.project_id);
|
|
1900
|
-
const activeTargetBundle = activeRun.target_kind === 'bundle' && activeRun.target_bundle_id
|
|
1901
|
-
? getBundle(activeRun.target_bundle_id, activeRun.project_id)
|
|
1902
|
-
: null;
|
|
1903
|
-
return {
|
|
1904
|
-
active_project_id: activeRun.project_id,
|
|
1905
|
-
error: activeProject
|
|
1906
|
-
? activeTargetBundle
|
|
1907
|
-
? `Bundle target run for "${activeProject.name}" (${activeTargetBundle.name}) is already running. Stop it before starting another run.`
|
|
1908
|
-
: `Repo root run for "${activeProject.name}" is already running. Stop it before starting another run.`
|
|
1909
|
-
: activeTargetBundle
|
|
1910
|
-
? 'A bundle target run is already running. Stop it before starting another run.'
|
|
1911
|
-
: 'A repo root run is already running. Stop it before starting another run.',
|
|
1912
|
-
};
|
|
1855
|
+
app.use(express.json({ limit: '5mb' }));
|
|
1856
|
+
const requireDesktopMobileAccessRequest = (req, res) => {
|
|
1857
|
+
const rejectionReason = validateDesktopMobileAccessRequest(req);
|
|
1858
|
+
if (!rejectionReason) {
|
|
1859
|
+
return true;
|
|
1913
1860
|
}
|
|
1914
|
-
|
|
1915
|
-
return
|
|
1916
|
-
active_ticket_id: activeRun.ticket_id,
|
|
1917
|
-
error: activeTicket
|
|
1918
|
-
? activeTicket.bundle
|
|
1919
|
-
? `Ticket "${activeTicket.title}" in bundle "${activeTicket.bundle.name}" is already running. Stop it before starting another run.`
|
|
1920
|
-
: `Ticket "${activeTicket.title}" is already running. Stop it before starting another run.`
|
|
1921
|
-
: 'Another ticket is already running. Stop it before starting another run.',
|
|
1922
|
-
};
|
|
1861
|
+
res.status(403).json({ error: rejectionReason });
|
|
1862
|
+
return false;
|
|
1923
1863
|
};
|
|
1924
|
-
app.
|
|
1864
|
+
app.get('/mobile-access/status', (_req, res) => {
|
|
1865
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
1866
|
+
res.json(getMobileAccessStatus());
|
|
1867
|
+
});
|
|
1868
|
+
app.post('/mobile-access/enable', async (req, res) => {
|
|
1869
|
+
if (!requireDesktopMobileAccessRequest(req, res)) {
|
|
1870
|
+
return;
|
|
1871
|
+
}
|
|
1872
|
+
const preferredProjectId = typeof req.body?.preferred_project_id === 'string'
|
|
1873
|
+
? req.body.preferred_project_id
|
|
1874
|
+
: null;
|
|
1875
|
+
const requestedMode = req.body?.mode === 'custom_public_url'
|
|
1876
|
+
? 'custom_public_url'
|
|
1877
|
+
: req.body?.mode === 'ngrok'
|
|
1878
|
+
? 'ngrok'
|
|
1879
|
+
: req.body?.mode === 'lan'
|
|
1880
|
+
? 'lan'
|
|
1881
|
+
: null;
|
|
1882
|
+
const publicBaseUrl = typeof req.body?.public_base_url === 'string'
|
|
1883
|
+
? req.body.public_base_url
|
|
1884
|
+
: null;
|
|
1885
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
1886
|
+
res.json(await startMobileAccess({
|
|
1887
|
+
mode: requestedMode,
|
|
1888
|
+
clientBuild: resolveClientBuildPaths(import.meta.url),
|
|
1889
|
+
broadcaster: io,
|
|
1890
|
+
preferredProjectId,
|
|
1891
|
+
reviewBundleTickets,
|
|
1892
|
+
publicBaseUrl,
|
|
1893
|
+
}));
|
|
1894
|
+
});
|
|
1895
|
+
app.post('/mobile-access/disable', async (req, res) => {
|
|
1896
|
+
if (!requireDesktopMobileAccessRequest(req, res)) {
|
|
1897
|
+
return;
|
|
1898
|
+
}
|
|
1899
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
1900
|
+
res.json(await stopMobileAccess());
|
|
1901
|
+
});
|
|
1902
|
+
app.post('/mobile-access/regenerate-pairing', (req, res) => {
|
|
1903
|
+
if (!requireDesktopMobileAccessRequest(req, res)) {
|
|
1904
|
+
return;
|
|
1905
|
+
}
|
|
1906
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
1907
|
+
const preferredProjectId = typeof req.body?.preferred_project_id === 'string'
|
|
1908
|
+
? req.body.preferred_project_id
|
|
1909
|
+
: null;
|
|
1910
|
+
res.json(regenerateMobilePairingChallenge(preferredProjectId));
|
|
1911
|
+
});
|
|
1912
|
+
app.post('/mobile-access/sessions/:sessionId/revoke', (req, res) => {
|
|
1913
|
+
if (!requireDesktopMobileAccessRequest(req, res)) {
|
|
1914
|
+
return;
|
|
1915
|
+
}
|
|
1916
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
1917
|
+
res.json(revokeMobileSession(req.params.sessionId));
|
|
1918
|
+
});
|
|
1925
1919
|
app.get('/run-config', (req, res) => {
|
|
1926
1920
|
const ticketId = typeof req.query.ticketId === 'string' ? req.query.ticketId : null;
|
|
1927
1921
|
if (!ticketId) {
|
|
@@ -1964,29 +1958,15 @@ app.get('/projects/:id/chat-sessions', (req, res) => {
|
|
|
1964
1958
|
return res.json(listChatSessions(project.id));
|
|
1965
1959
|
});
|
|
1966
1960
|
app.post('/projects/:id/chat-sessions', (req, res) => {
|
|
1967
|
-
const
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
const tool = resolveChatTool(req.body.tool);
|
|
1972
|
-
const mode = resolveChatMode(req.body.mode);
|
|
1973
|
-
const rawModel = typeof req.body.model === 'string' ? req.body.model : project.helper_model;
|
|
1974
|
-
const normalizedModel = tool === 'opencode' ? normalizeModelId(rawModel) || null : null;
|
|
1975
|
-
const normalizedVariant = tool === 'opencode' && normalizedModel && typeof req.body.variant === 'string'
|
|
1976
|
-
? req.body.variant.trim() || null
|
|
1977
|
-
: tool === 'opencode' && normalizedModel
|
|
1978
|
-
? project.helper_variant ?? null
|
|
1979
|
-
: null;
|
|
1980
|
-
const chatSession = createChatSession({
|
|
1981
|
-
projectId: project.id,
|
|
1982
|
-
mode,
|
|
1983
|
-
tool,
|
|
1984
|
-
model: normalizedModel,
|
|
1985
|
-
variant: normalizedVariant,
|
|
1986
|
-
skills: normalizeSkillNames(req.body.skills),
|
|
1961
|
+
const result = createProjectChatSession({
|
|
1962
|
+
projectId: req.params.id,
|
|
1963
|
+
payload: req.body,
|
|
1964
|
+
broadcaster: io,
|
|
1987
1965
|
});
|
|
1988
|
-
|
|
1989
|
-
|
|
1966
|
+
if (!result.ok) {
|
|
1967
|
+
return res.status(result.status).json({ error: result.error });
|
|
1968
|
+
}
|
|
1969
|
+
return res.status(result.status).json(result.value);
|
|
1990
1970
|
});
|
|
1991
1971
|
app.get('/projects/:id/chat-sessions/:sessionId', (req, res) => {
|
|
1992
1972
|
const project = getProjectById(req.params.id);
|
|
@@ -2020,55 +2000,26 @@ app.patch('/projects/:id/chat-sessions/:sessionId', (req, res) => {
|
|
|
2020
2000
|
return res.json(updated);
|
|
2021
2001
|
});
|
|
2022
2002
|
app.post('/projects/:id/chat-sessions/:sessionId/messages', (req, res) => {
|
|
2023
|
-
const
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
if (!existing) {
|
|
2029
|
-
return res.status(404).json({ error: 'Not found' });
|
|
2030
|
-
}
|
|
2031
|
-
const instruction = typeof req.body.instruction === 'string' ? req.body.instruction.trim() : '';
|
|
2032
|
-
if (!instruction) {
|
|
2033
|
-
return res.status(400).json({ error: 'instruction is required' });
|
|
2034
|
-
}
|
|
2035
|
-
const targetResolution = resolveChatSessionTarget(existing, project);
|
|
2036
|
-
if (!targetResolution.target_available) {
|
|
2037
|
-
return res.status(409).json({ error: targetResolution.target_unavailable_reason ?? 'Chat target is unavailable.' });
|
|
2038
|
-
}
|
|
2039
|
-
if (!doesChatSessionTargetMatchProjectTarget(existing, project)) {
|
|
2040
|
-
return res.status(409).json({ error: 'This chat session is out of sync with the current project target. Restore the session target to continue.' });
|
|
2041
|
-
}
|
|
2042
|
-
const rawModel = typeof req.body.model === 'string' ? req.body.model : project.helper_model;
|
|
2043
|
-
const accepted = startChatSessionTurn({
|
|
2044
|
-
chatSessionId: existing.id,
|
|
2045
|
-
instruction,
|
|
2046
|
-
tool: existing.tool,
|
|
2047
|
-
model: typeof rawModel === 'string' ? rawModel : null,
|
|
2048
|
-
variant: typeof req.body.variant === 'string' ? req.body.variant : project.helper_variant,
|
|
2049
|
-
skills: normalizeSkillNames(req.body.skills),
|
|
2050
|
-
io,
|
|
2003
|
+
const result = submitProjectChatSessionMessage({
|
|
2004
|
+
projectId: req.params.id,
|
|
2005
|
+
chatSessionId: req.params.sessionId,
|
|
2006
|
+
payload: req.body,
|
|
2007
|
+
broadcaster: io,
|
|
2051
2008
|
});
|
|
2052
|
-
if (!
|
|
2053
|
-
return res.status(
|
|
2009
|
+
if (!result.ok) {
|
|
2010
|
+
return res.status(result.status).json({ error: result.error });
|
|
2054
2011
|
}
|
|
2055
|
-
|
|
2056
|
-
return res.json(updated);
|
|
2012
|
+
return res.status(result.status).json(result.value);
|
|
2057
2013
|
});
|
|
2058
2014
|
app.post('/projects/:id/chat-sessions/:sessionId/stop', async (req, res) => {
|
|
2059
|
-
const
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
}
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
return res.status(404).json({ error: 'Not found' });
|
|
2066
|
-
}
|
|
2067
|
-
const stopped = await stopChatAgent(chatSession.id);
|
|
2068
|
-
if (!stopped) {
|
|
2069
|
-
return res.status(409).json({ error: 'No active chat response found for this session.' });
|
|
2015
|
+
const result = await stopProjectChatSessionResponse({
|
|
2016
|
+
projectId: req.params.id,
|
|
2017
|
+
chatSessionId: req.params.sessionId,
|
|
2018
|
+
});
|
|
2019
|
+
if (!result.ok) {
|
|
2020
|
+
return res.status(result.status).json({ error: result.error });
|
|
2070
2021
|
}
|
|
2071
|
-
return res.json(
|
|
2022
|
+
return res.status(result.status).json(result.value);
|
|
2072
2023
|
});
|
|
2073
2024
|
app.get('/projects/:id/chat-sessions/:sessionId/diff', (req, res) => {
|
|
2074
2025
|
const project = getProjectById(req.params.id);
|
|
@@ -4364,6 +4315,18 @@ app.patch('/tickets/:id', (req, res) => {
|
|
|
4364
4315
|
const hasVariantField = hasOwn(req.body, 'variant');
|
|
4365
4316
|
const hasSkillsField = hasOwn(req.body, 'skills');
|
|
4366
4317
|
const isSettingsOnlyUpdate = !hasTitleField && !hasDescriptionField && !hasProjectField && !hasBundleField && (hasModelField || hasVariantField || hasSkillsField);
|
|
4318
|
+
if (isTicketAgentSettingsPayload(req.body)) {
|
|
4319
|
+
const result = updateTicketAgentSettings({
|
|
4320
|
+
ticketId: req.params.id,
|
|
4321
|
+
requestedProjectId: getRequestedProjectId(req),
|
|
4322
|
+
payload: req.body,
|
|
4323
|
+
broadcaster: io,
|
|
4324
|
+
});
|
|
4325
|
+
if (!result.ok) {
|
|
4326
|
+
return res.status(result.status).json({ error: result.error });
|
|
4327
|
+
}
|
|
4328
|
+
return res.status(result.status).json(result.value);
|
|
4329
|
+
}
|
|
4367
4330
|
if (!isPlanTicket && !isSettingsOnlyUpdate) {
|
|
4368
4331
|
return res.status(400).json({ error: 'Only plan tickets can be fully edited after work begins' });
|
|
4369
4332
|
}
|
|
@@ -4481,79 +4444,21 @@ app.patch('/tickets/:id', (req, res) => {
|
|
|
4481
4444
|
res.json(updated);
|
|
4482
4445
|
});
|
|
4483
4446
|
app.patch('/tickets/:id/state', (req, res) => {
|
|
4484
|
-
const
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
if (state === 'plan' && existing.state !== 'plan') {
|
|
4497
|
-
return res.status(409).json({ error: 'Tickets cannot be moved back to plan after work begins' });
|
|
4498
|
-
}
|
|
4499
|
-
if (state === 'build') {
|
|
4500
|
-
const buildBlocker = getBundleBuildBlockerResponse(existing);
|
|
4501
|
-
if (buildBlocker) {
|
|
4502
|
-
return res.status(409).json(buildBlocker);
|
|
4503
|
-
}
|
|
4504
|
-
const buildTransition = prepareTicketForBuild(existing.id);
|
|
4505
|
-
if (!buildTransition.ok) {
|
|
4506
|
-
return res.status(buildTransition.status).json({
|
|
4507
|
-
error: buildTransition.error,
|
|
4508
|
-
...(buildTransition.blockers ? { blockers: buildTransition.blockers } : {}),
|
|
4509
|
-
});
|
|
4510
|
-
}
|
|
4511
|
-
io.emit('ticket:updated', buildTransition.ticket);
|
|
4512
|
-
res.json(buildTransition.ticket);
|
|
4513
|
-
if (buildTransition.ticket.project_id && buildTransition.ticket.worktree_bundle_id) {
|
|
4514
|
-
const queuedBuild = enqueueBundledBuild({
|
|
4515
|
-
ticketId: buildTransition.ticket.id,
|
|
4516
|
-
projectId: buildTransition.ticket.project_id,
|
|
4517
|
-
worktreeBundleId: buildTransition.ticket.worktree_bundle_id,
|
|
4518
|
-
activate: shouldStartBundleBuildNow(buildTransition.ticket),
|
|
4519
|
-
});
|
|
4520
|
-
if (!queuedBuild.shouldStartNow) {
|
|
4521
|
-
return;
|
|
4522
|
-
}
|
|
4523
|
-
}
|
|
4524
|
-
startBuild(buildTransition.ticket, io);
|
|
4525
|
-
return;
|
|
4526
|
-
}
|
|
4527
|
-
if (state === 'review' && (existing.state !== 'build' || existing.agent_status !== 'done')) {
|
|
4528
|
-
return res.status(409).json({ error: 'Tickets can only be moved to review from build when done' });
|
|
4529
|
-
}
|
|
4530
|
-
const project = getEffectiveProject(existing);
|
|
4531
|
-
if (state === 'review' && !project) {
|
|
4532
|
-
return res.status(400).json({ error: 'Ticket project no longer exists' });
|
|
4533
|
-
}
|
|
4534
|
-
if (state === 'review' && existing.project_id && existing.worktree_bundle_id) {
|
|
4535
|
-
const reviewResult = reviewBundleTickets(existing.project_id, existing.worktree_bundle_id);
|
|
4536
|
-
if (!reviewResult.ok) {
|
|
4537
|
-
return res.status(reviewResult.status).json({ error: reviewResult.error });
|
|
4538
|
-
}
|
|
4539
|
-
const updated = reviewResult.details.tickets.find(ticket => ticket.id === existing.id);
|
|
4540
|
-
if (!updated) {
|
|
4541
|
-
return res.status(409).json({ error: 'Unable to update the requested ticket in this bundle' });
|
|
4542
|
-
}
|
|
4543
|
-
return res.json(updated);
|
|
4447
|
+
const result = transitionTicketState({
|
|
4448
|
+
ticketId: req.params.id,
|
|
4449
|
+
requestedState: req.body.state,
|
|
4450
|
+
requestedProjectId: getRequestedProjectId(req),
|
|
4451
|
+
broadcaster: io,
|
|
4452
|
+
reviewBundleTickets,
|
|
4453
|
+
});
|
|
4454
|
+
if (!result.ok) {
|
|
4455
|
+
return res.status(result.status).json({
|
|
4456
|
+
error: result.error,
|
|
4457
|
+
...result.details,
|
|
4458
|
+
});
|
|
4544
4459
|
}
|
|
4545
|
-
|
|
4546
|
-
|
|
4547
|
-
const nextSessionId = existing.session_id;
|
|
4548
|
-
db.prepare('UPDATE tickets SET state = ?, tool = ?, agent_status = ?, agent_log = ?, session_id = ?, streaming_response = NULL, lane_order = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(state, resolveTicketTool(existing, config.tool ?? 'opencode'), nextAgentStatus, nextAgentLog, nextSessionId, assignTicketLaneOrder({ ...existing, state }) ?? null, req.params.id);
|
|
4549
|
-
const ticket = autoParkTicketIfStale(req.params.id);
|
|
4550
|
-
if (state === 'review' && ticket) {
|
|
4551
|
-
startReview(ticket, io);
|
|
4552
|
-
const runningReview = getTicket(req.params.id);
|
|
4553
|
-
return res.json(runningReview);
|
|
4554
|
-
}
|
|
4555
|
-
io.emit('ticket:updated', ticket);
|
|
4556
|
-
res.json(ticket);
|
|
4460
|
+
res.status(result.status).json(result.value);
|
|
4461
|
+
result.afterResponse?.();
|
|
4557
4462
|
});
|
|
4558
4463
|
app.patch('/tickets/:id/park', (req, res) => {
|
|
4559
4464
|
const ticket = getTicket(req.params.id);
|
|
@@ -4755,41 +4660,19 @@ app.post('/tickets/:id/undo', (req, res) => {
|
|
|
4755
4660
|
return res.json(result.ticket);
|
|
4756
4661
|
});
|
|
4757
4662
|
app.post('/tickets/:id/run', (req, res) => {
|
|
4758
|
-
const
|
|
4759
|
-
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
|
|
4766
|
-
|
|
4767
|
-
|
|
4768
|
-
}
|
|
4769
|
-
const workspace = resolveTicketGitWorkspace(ticket);
|
|
4770
|
-
if ('error' in workspace) {
|
|
4771
|
-
return res.status(workspace.status).json({ error: workspace.error });
|
|
4772
|
-
}
|
|
4773
|
-
if (!hasRunConfig(project)) {
|
|
4774
|
-
return res.status(400).json({ error: 'No run config' });
|
|
4775
|
-
}
|
|
4776
|
-
const runReadinessError = getTicketRunReadinessError(ticket, project);
|
|
4777
|
-
if (runReadinessError) {
|
|
4778
|
-
return res.status(409).json({ error: runReadinessError });
|
|
4779
|
-
}
|
|
4780
|
-
if (isRunning(ticket)) {
|
|
4781
|
-
emitRunStateForContext(ticket, true);
|
|
4782
|
-
const runningTicket = listTicketsInRunContext(ticket).find(entry => entry.id === ticket.id);
|
|
4783
|
-
return res.json(runningTicket ?? { ...ticket, is_running: true, has_run_config: true });
|
|
4784
|
-
}
|
|
4785
|
-
const activeRunConflict = formatActiveRunConflict(ticket.project_id);
|
|
4786
|
-
if (activeRunConflict) {
|
|
4787
|
-
return res.status(409).json(activeRunConflict);
|
|
4663
|
+
const result = runTicketRun({
|
|
4664
|
+
ticketId: req.params.id,
|
|
4665
|
+
requestedProjectId: getRequestedProjectId(req),
|
|
4666
|
+
broadcaster: io,
|
|
4667
|
+
});
|
|
4668
|
+
if (!result.ok) {
|
|
4669
|
+
return res.status(result.status).json({
|
|
4670
|
+
error: result.error,
|
|
4671
|
+
...result.details,
|
|
4672
|
+
});
|
|
4788
4673
|
}
|
|
4789
|
-
|
|
4790
|
-
|
|
4791
|
-
res.json(running);
|
|
4792
|
-
runTicketInWorkspace(ticket, io, project, workspace.cwd);
|
|
4674
|
+
res.status(result.status).json(result.value);
|
|
4675
|
+
result.afterResponse?.();
|
|
4793
4676
|
});
|
|
4794
4677
|
app.post('/tickets/:id/open-ide', (req, res) => {
|
|
4795
4678
|
const ticket = getTicket(req.params.id);
|
|
@@ -4982,46 +4865,17 @@ app.post('/projects/:id/open-ide', (req, res) => {
|
|
|
4982
4865
|
return res.json({ ok: true });
|
|
4983
4866
|
});
|
|
4984
4867
|
app.post('/projects/:id/run', (req, res) => {
|
|
4985
|
-
const
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
|
|
4990
|
-
|
|
4991
|
-
|
|
4992
|
-
const workspace = resolveProjectTargetGitWorkspace(project);
|
|
4993
|
-
if ('error' in workspace) {
|
|
4994
|
-
return res.status(workspace.status).json({ error: workspace.error });
|
|
4995
|
-
}
|
|
4996
|
-
const contextKey = workspace.run_context_key;
|
|
4997
|
-
if (isRunning(contextKey)) {
|
|
4998
|
-
return res.json({
|
|
4999
|
-
active_run: {
|
|
5000
|
-
kind: 'project',
|
|
5001
|
-
context_key: contextKey,
|
|
5002
|
-
project_id: project.id,
|
|
5003
|
-
target_kind: workspace.target_kind,
|
|
5004
|
-
target_bundle_id: workspace.target_bundle_id,
|
|
5005
|
-
},
|
|
4868
|
+
const result = runProjectTarget({
|
|
4869
|
+
projectId: req.params.id,
|
|
4870
|
+
});
|
|
4871
|
+
if (!result.ok) {
|
|
4872
|
+
return res.status(result.status).json({
|
|
4873
|
+
error: result.error,
|
|
4874
|
+
...result.details,
|
|
5006
4875
|
});
|
|
5007
4876
|
}
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
return res.status(409).json(activeRunConflict);
|
|
5011
|
-
}
|
|
5012
|
-
const activeRun = {
|
|
5013
|
-
kind: 'project',
|
|
5014
|
-
context_key: contextKey,
|
|
5015
|
-
project_id: project.id,
|
|
5016
|
-
target_kind: workspace.target_kind,
|
|
5017
|
-
target_bundle_id: workspace.target_bundle_id,
|
|
5018
|
-
};
|
|
5019
|
-
res.json({ active_run: activeRun });
|
|
5020
|
-
void runProject(project, {
|
|
5021
|
-
cwd: workspace.cwd,
|
|
5022
|
-
targetKind: workspace.target_kind,
|
|
5023
|
-
targetBundleId: workspace.target_bundle_id,
|
|
5024
|
-
});
|
|
4877
|
+
res.status(result.status).json(result.value);
|
|
4878
|
+
result.afterResponse?.();
|
|
5025
4879
|
});
|
|
5026
4880
|
app.post('/projects/:id/test-sessions', (req, res) => {
|
|
5027
4881
|
const project = getProjectById(req.params.id);
|
|
@@ -5204,154 +5058,69 @@ app.post('/terminals/:sessionId/destroy', (req, res) => {
|
|
|
5204
5058
|
return res.status(204).send();
|
|
5205
5059
|
});
|
|
5206
5060
|
app.post('/tickets/:id/stop', async (req, res) => {
|
|
5207
|
-
const
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
|
|
5211
|
-
|
|
5212
|
-
|
|
5061
|
+
const result = await stopTicketRun({
|
|
5062
|
+
ticketId: req.params.id,
|
|
5063
|
+
requestedProjectId: getRequestedProjectId(req),
|
|
5064
|
+
broadcaster: io,
|
|
5065
|
+
});
|
|
5066
|
+
if (!result.ok) {
|
|
5067
|
+
return res.status(result.status).json({ error: result.error });
|
|
5213
5068
|
}
|
|
5214
|
-
|
|
5215
|
-
emitRunStateForContext(ticket, false);
|
|
5216
|
-
const updated = listTicketsInRunContext(ticket).find(entry => entry.id === ticket.id) ?? { ...ticket, is_running: false };
|
|
5217
|
-
res.json(updated);
|
|
5069
|
+
return res.status(result.status).json(result.value);
|
|
5218
5070
|
});
|
|
5219
5071
|
app.post('/projects/:id/stop-run', async (req, res) => {
|
|
5220
|
-
const
|
|
5221
|
-
|
|
5222
|
-
return res.status(404).json({ error: 'Not found' });
|
|
5223
|
-
}
|
|
5224
|
-
await stopProjectRun(project.id, {
|
|
5225
|
-
kind: project.active_target_kind,
|
|
5226
|
-
bundleId: project.active_target_bundle_id,
|
|
5072
|
+
const result = await stopProjectTargetRun({
|
|
5073
|
+
projectId: req.params.id,
|
|
5227
5074
|
});
|
|
5228
|
-
|
|
5075
|
+
if (!result.ok) {
|
|
5076
|
+
return res.status(result.status).json({ error: result.error });
|
|
5077
|
+
}
|
|
5078
|
+
return res.status(result.status).json(result.value);
|
|
5229
5079
|
});
|
|
5230
5080
|
app.post('/tickets/:id/stop-build', async (req, res) => {
|
|
5231
|
-
const
|
|
5232
|
-
|
|
5233
|
-
|
|
5234
|
-
}
|
|
5235
|
-
if (!
|
|
5236
|
-
return res.status(
|
|
5237
|
-
}
|
|
5238
|
-
if (!canUseBuildControls(ticket)) {
|
|
5239
|
-
return res.status(400).json({ error: 'Only build-stage and review follow-up tickets can stop builds' });
|
|
5240
|
-
}
|
|
5241
|
-
if (ticket.agent_status !== 'running') {
|
|
5242
|
-
return res.status(400).json({ error: 'Ticket build is not running' });
|
|
5243
|
-
}
|
|
5244
|
-
if (!ticket.session_id) {
|
|
5245
|
-
return res.status(400).json({ error: 'Build session is not ready to stop yet' });
|
|
5246
|
-
}
|
|
5247
|
-
if (!isBuildAgentRunning(ticket.id)) {
|
|
5248
|
-
return res.status(409).json({ error: 'No active build process found for this ticket' });
|
|
5249
|
-
}
|
|
5250
|
-
await stopBuildAgent(ticket.id);
|
|
5251
|
-
const updated = getTicket(req.params.id);
|
|
5252
|
-
if (!updated) {
|
|
5253
|
-
return res.status(404).json({ error: 'Not found' });
|
|
5081
|
+
const result = await stopBuildTicketRun({
|
|
5082
|
+
ticketId: req.params.id,
|
|
5083
|
+
requestedProjectId: getRequestedProjectId(req),
|
|
5084
|
+
});
|
|
5085
|
+
if (!result.ok) {
|
|
5086
|
+
return res.status(result.status).json({ error: result.error });
|
|
5254
5087
|
}
|
|
5255
|
-
res.json(
|
|
5088
|
+
return res.status(result.status).json(result.value);
|
|
5256
5089
|
});
|
|
5257
5090
|
app.post('/tickets/:id/resume-build', (req, res) => {
|
|
5258
|
-
const
|
|
5259
|
-
|
|
5260
|
-
|
|
5261
|
-
|
|
5262
|
-
|
|
5263
|
-
|
|
5264
|
-
|
|
5265
|
-
if (!canUseBuildControls(existing)) {
|
|
5266
|
-
return res.status(400).json({ error: 'Only build-stage and review follow-up tickets can resume builds' });
|
|
5267
|
-
}
|
|
5268
|
-
if (existing.agent_status !== 'stopped') {
|
|
5269
|
-
return res.status(400).json({ error: 'Only manually stopped builds can be resumed' });
|
|
5270
|
-
}
|
|
5271
|
-
if (!existing.session_id) {
|
|
5272
|
-
return res.status(400).json({ error: 'Build session is not available to resume' });
|
|
5273
|
-
}
|
|
5274
|
-
db.prepare('UPDATE tickets SET build_completed_at = NULL, auto_park_dismissed_at = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(existing.id);
|
|
5275
|
-
const ticket = getTicket(existing.id);
|
|
5276
|
-
if (!ticket) {
|
|
5277
|
-
return res.status(404).json({ error: 'Not found' });
|
|
5091
|
+
const result = resumeBuildTicketRun({
|
|
5092
|
+
ticketId: req.params.id,
|
|
5093
|
+
requestedProjectId: getRequestedProjectId(req),
|
|
5094
|
+
broadcaster: io,
|
|
5095
|
+
});
|
|
5096
|
+
if (!result.ok) {
|
|
5097
|
+
return res.status(result.status).json({ error: result.error });
|
|
5278
5098
|
}
|
|
5279
|
-
|
|
5280
|
-
res.json(ticket);
|
|
5099
|
+
return res.status(result.status).json(result.value);
|
|
5281
5100
|
});
|
|
5282
5101
|
app.post('/tickets/:id/follow-up', (req, res) => {
|
|
5283
|
-
const
|
|
5284
|
-
|
|
5285
|
-
|
|
5286
|
-
|
|
5287
|
-
|
|
5288
|
-
return res.status(404).json({ error: 'Not found' });
|
|
5289
|
-
}
|
|
5290
|
-
if (ticket.state === 'plan') {
|
|
5291
|
-
return res.status(400).json({ error: 'Only non-plan tickets can accept follow-up prompts' });
|
|
5292
|
-
}
|
|
5293
|
-
if (ticket.agent_status === 'running') {
|
|
5294
|
-
return res.status(409).json({ error: 'Ticket is already running' });
|
|
5295
|
-
}
|
|
5296
|
-
const instruction = typeof req.body?.instruction === 'string'
|
|
5297
|
-
? req.body.instruction.trim()
|
|
5298
|
-
: '';
|
|
5299
|
-
if (!instruction) {
|
|
5300
|
-
return res.status(400).json({ error: 'instruction is required' });
|
|
5301
|
-
}
|
|
5302
|
-
const project = getEffectiveProject(ticket);
|
|
5303
|
-
if (!project) {
|
|
5304
|
-
return res.status(400).json({ error: 'Ticket project no longer exists' });
|
|
5305
|
-
}
|
|
5306
|
-
const branch = resolveTicketBranch(ticket);
|
|
5307
|
-
if (!branch || ticket.has_worktree === false || !isWorktreeReady(branch, project)) {
|
|
5308
|
-
return res.status(400).json({ error: 'Worktree not ready yet' });
|
|
5309
|
-
}
|
|
5310
|
-
createTicketMessage({
|
|
5311
|
-
ticketId: ticket.id,
|
|
5312
|
-
role: 'user',
|
|
5313
|
-
kind: 'follow_up',
|
|
5314
|
-
content: instruction,
|
|
5102
|
+
const result = submitTicketFollowUp({
|
|
5103
|
+
ticketId: req.params.id,
|
|
5104
|
+
requestedProjectId: getRequestedProjectId(req),
|
|
5105
|
+
payload: req.body,
|
|
5106
|
+
broadcaster: io,
|
|
5315
5107
|
});
|
|
5316
|
-
|
|
5317
|
-
|
|
5318
|
-
return res.status(409).json({ error: 'Ticket is already running' });
|
|
5108
|
+
if (!result.ok) {
|
|
5109
|
+
return res.status(result.status).json({ error: result.error });
|
|
5319
5110
|
}
|
|
5320
|
-
|
|
5321
|
-
return res.json(updated ?? ticket);
|
|
5111
|
+
return res.status(result.status).json(result.value);
|
|
5322
5112
|
});
|
|
5323
5113
|
app.post('/tickets/:id/plan-follow-up', (req, res) => {
|
|
5324
|
-
const
|
|
5325
|
-
|
|
5326
|
-
|
|
5327
|
-
|
|
5328
|
-
|
|
5329
|
-
return res.status(404).json({ error: 'Not found' });
|
|
5330
|
-
}
|
|
5331
|
-
if (ticket.state !== 'plan') {
|
|
5332
|
-
return res.status(400).json({ error: 'Only plan tickets can accept planning follow-up prompts' });
|
|
5333
|
-
}
|
|
5334
|
-
if (ticket.agent_status === 'running') {
|
|
5335
|
-
return res.status(409).json({ error: 'Ticket is already running' });
|
|
5336
|
-
}
|
|
5337
|
-
const instruction = typeof req.body?.instruction === 'string'
|
|
5338
|
-
? req.body.instruction.trim()
|
|
5339
|
-
: '';
|
|
5340
|
-
if (!instruction) {
|
|
5341
|
-
return res.status(400).json({ error: 'instruction is required' });
|
|
5342
|
-
}
|
|
5343
|
-
createTicketMessage({
|
|
5344
|
-
ticketId: ticket.id,
|
|
5345
|
-
role: 'user',
|
|
5346
|
-
kind: 'planning_follow_up',
|
|
5347
|
-
content: instruction,
|
|
5114
|
+
const result = submitPlanTicketFollowUp({
|
|
5115
|
+
ticketId: req.params.id,
|
|
5116
|
+
requestedProjectId: getRequestedProjectId(req),
|
|
5117
|
+
payload: req.body,
|
|
5118
|
+
broadcaster: io,
|
|
5348
5119
|
});
|
|
5349
|
-
|
|
5350
|
-
|
|
5351
|
-
return res.status(409).json({ error: 'Ticket is already running' });
|
|
5120
|
+
if (!result.ok) {
|
|
5121
|
+
return res.status(result.status).json({ error: result.error });
|
|
5352
5122
|
}
|
|
5353
|
-
|
|
5354
|
-
return res.json(updated ?? ticket);
|
|
5123
|
+
return res.status(result.status).json(result.value);
|
|
5355
5124
|
});
|
|
5356
5125
|
app.post('/tickets/:id/plan', (req, res) => {
|
|
5357
5126
|
const ticket = getTicket(req.params.id);
|
|
@@ -5430,26 +5199,20 @@ io.on('connection', (socket) => {
|
|
|
5430
5199
|
registerTerminalSocketHandlers(io, socket);
|
|
5431
5200
|
});
|
|
5432
5201
|
const DEFAULT_PORT = 3001;
|
|
5433
|
-
const resolveListeningHost = (host) => {
|
|
5434
|
-
if (!host || host === '0.0.0.0' || host === '::') {
|
|
5435
|
-
return 'localhost';
|
|
5436
|
-
}
|
|
5437
|
-
return host;
|
|
5438
|
-
};
|
|
5439
5202
|
const resolveStartResult = (host) => {
|
|
5440
5203
|
const address = httpServer.address();
|
|
5441
|
-
const
|
|
5204
|
+
const { displayHost } = resolveDesktopServerHost(host);
|
|
5442
5205
|
if (!address || typeof address === 'string') {
|
|
5443
5206
|
return {
|
|
5444
|
-
host:
|
|
5207
|
+
host: displayHost,
|
|
5445
5208
|
port: DEFAULT_PORT,
|
|
5446
|
-
url: `http://${
|
|
5209
|
+
url: `http://${displayHost}:${DEFAULT_PORT}`,
|
|
5447
5210
|
};
|
|
5448
5211
|
}
|
|
5449
5212
|
return {
|
|
5450
|
-
host:
|
|
5213
|
+
host: displayHost,
|
|
5451
5214
|
port: address.port,
|
|
5452
|
-
url: `http://${
|
|
5215
|
+
url: `http://${displayHost}:${address.port}`,
|
|
5453
5216
|
};
|
|
5454
5217
|
};
|
|
5455
5218
|
export const startServer = ({ port = DEFAULT_PORT, host } = {}) => {
|
|
@@ -5467,14 +5230,12 @@ export const startServer = ({ port = DEFAULT_PORT, host } = {}) => {
|
|
|
5467
5230
|
resolve(resolveStartResult(host));
|
|
5468
5231
|
};
|
|
5469
5232
|
httpServer.once('error', handleError);
|
|
5470
|
-
|
|
5471
|
-
|
|
5472
|
-
return;
|
|
5473
|
-
}
|
|
5474
|
-
httpServer.listen(port, handleListening);
|
|
5233
|
+
const { bindHost } = resolveDesktopServerHost(host);
|
|
5234
|
+
httpServer.listen(port, bindHost, handleListening);
|
|
5475
5235
|
});
|
|
5476
5236
|
};
|
|
5477
5237
|
export const shutdownServer = async (signal) => {
|
|
5238
|
+
await stopMobileAccess();
|
|
5478
5239
|
await clearAllTestSessions(io);
|
|
5479
5240
|
await shutdownRealtimeServer({
|
|
5480
5241
|
signal,
|