@straiffi/archon 1.1.2 → 1.2.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 +4 -0
- package/dist/client/assets/TestsDialog-Coa2wGbr.js +5 -0
- package/dist/client/assets/badge-Bpry9xkS.js +41 -0
- package/dist/client/assets/index-Bw66dtZG.css +2 -0
- package/dist/client/assets/index-WH13gBCE.js +151 -0
- package/dist/client/index.html +3 -3
- package/dist/server/index.js +201 -446
- 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 +1035 -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-F0VCXRNc.js +0 -5
- package/dist/client/assets/badge-BentDQnz.js +0 -41
- package/dist/client/assets/index-CyyQsO8z.js +0 -142
- package/dist/client/assets/index-Dr_tNX7Y.css +0 -2
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,63 @@ 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 requestedMode = req.body?.mode === 'custom_public_url'
|
|
1873
|
+
? 'custom_public_url'
|
|
1874
|
+
: req.body?.mode === 'ngrok'
|
|
1875
|
+
? 'ngrok'
|
|
1876
|
+
: req.body?.mode === 'lan'
|
|
1877
|
+
? 'lan'
|
|
1878
|
+
: null;
|
|
1879
|
+
const publicBaseUrl = typeof req.body?.public_base_url === 'string'
|
|
1880
|
+
? req.body.public_base_url
|
|
1881
|
+
: null;
|
|
1882
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
1883
|
+
res.json(await startMobileAccess({
|
|
1884
|
+
mode: requestedMode,
|
|
1885
|
+
clientBuild: resolveClientBuildPaths(import.meta.url),
|
|
1886
|
+
broadcaster: io,
|
|
1887
|
+
reviewBundleTickets,
|
|
1888
|
+
publicBaseUrl,
|
|
1889
|
+
}));
|
|
1890
|
+
});
|
|
1891
|
+
app.post('/mobile-access/disable', async (req, res) => {
|
|
1892
|
+
if (!requireDesktopMobileAccessRequest(req, res)) {
|
|
1893
|
+
return;
|
|
1894
|
+
}
|
|
1895
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
1896
|
+
res.json(await stopMobileAccess());
|
|
1897
|
+
});
|
|
1898
|
+
app.post('/mobile-access/regenerate-pairing', (req, res) => {
|
|
1899
|
+
if (!requireDesktopMobileAccessRequest(req, res)) {
|
|
1900
|
+
return;
|
|
1901
|
+
}
|
|
1902
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
1903
|
+
res.json(regenerateMobilePairingChallenge());
|
|
1904
|
+
});
|
|
1905
|
+
app.post('/mobile-access/sessions/:sessionId/revoke', (req, res) => {
|
|
1906
|
+
if (!requireDesktopMobileAccessRequest(req, res)) {
|
|
1907
|
+
return;
|
|
1908
|
+
}
|
|
1909
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
1910
|
+
res.json(revokeMobileSession(req.params.sessionId));
|
|
1911
|
+
});
|
|
1925
1912
|
app.get('/run-config', (req, res) => {
|
|
1926
1913
|
const ticketId = typeof req.query.ticketId === 'string' ? req.query.ticketId : null;
|
|
1927
1914
|
if (!ticketId) {
|
|
@@ -1964,29 +1951,15 @@ app.get('/projects/:id/chat-sessions', (req, res) => {
|
|
|
1964
1951
|
return res.json(listChatSessions(project.id));
|
|
1965
1952
|
});
|
|
1966
1953
|
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),
|
|
1954
|
+
const result = createProjectChatSession({
|
|
1955
|
+
projectId: req.params.id,
|
|
1956
|
+
payload: req.body,
|
|
1957
|
+
broadcaster: io,
|
|
1987
1958
|
});
|
|
1988
|
-
|
|
1989
|
-
|
|
1959
|
+
if (!result.ok) {
|
|
1960
|
+
return res.status(result.status).json({ error: result.error });
|
|
1961
|
+
}
|
|
1962
|
+
return res.status(result.status).json(result.value);
|
|
1990
1963
|
});
|
|
1991
1964
|
app.get('/projects/:id/chat-sessions/:sessionId', (req, res) => {
|
|
1992
1965
|
const project = getProjectById(req.params.id);
|
|
@@ -2020,55 +1993,26 @@ app.patch('/projects/:id/chat-sessions/:sessionId', (req, res) => {
|
|
|
2020
1993
|
return res.json(updated);
|
|
2021
1994
|
});
|
|
2022
1995
|
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,
|
|
1996
|
+
const result = submitProjectChatSessionMessage({
|
|
1997
|
+
projectId: req.params.id,
|
|
1998
|
+
chatSessionId: req.params.sessionId,
|
|
1999
|
+
payload: req.body,
|
|
2000
|
+
broadcaster: io,
|
|
2051
2001
|
});
|
|
2052
|
-
if (!
|
|
2053
|
-
return res.status(
|
|
2002
|
+
if (!result.ok) {
|
|
2003
|
+
return res.status(result.status).json({ error: result.error });
|
|
2054
2004
|
}
|
|
2055
|
-
|
|
2056
|
-
return res.json(updated);
|
|
2005
|
+
return res.status(result.status).json(result.value);
|
|
2057
2006
|
});
|
|
2058
2007
|
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.' });
|
|
2008
|
+
const result = await stopProjectChatSessionResponse({
|
|
2009
|
+
projectId: req.params.id,
|
|
2010
|
+
chatSessionId: req.params.sessionId,
|
|
2011
|
+
});
|
|
2012
|
+
if (!result.ok) {
|
|
2013
|
+
return res.status(result.status).json({ error: result.error });
|
|
2070
2014
|
}
|
|
2071
|
-
return res.json(
|
|
2015
|
+
return res.status(result.status).json(result.value);
|
|
2072
2016
|
});
|
|
2073
2017
|
app.get('/projects/:id/chat-sessions/:sessionId/diff', (req, res) => {
|
|
2074
2018
|
const project = getProjectById(req.params.id);
|
|
@@ -2292,7 +2236,8 @@ app.get('/tickets/:id/git-status', (req, res) => {
|
|
|
2292
2236
|
if ('error' in workspace) {
|
|
2293
2237
|
return res.status(workspace.status).json({ error: workspace.error });
|
|
2294
2238
|
}
|
|
2295
|
-
|
|
2239
|
+
const refreshRemote = req.query.refresh_remote === 'true';
|
|
2240
|
+
return res.json(getGitStatus(workspace.cwd, workspace.branch, workspace.project, { refreshRemote }));
|
|
2296
2241
|
});
|
|
2297
2242
|
app.get('/tickets/:id/git-commits', (req, res) => {
|
|
2298
2243
|
const ticket = getTicketRouteContext(req.params.id);
|
|
@@ -4363,6 +4308,18 @@ app.patch('/tickets/:id', (req, res) => {
|
|
|
4363
4308
|
const hasVariantField = hasOwn(req.body, 'variant');
|
|
4364
4309
|
const hasSkillsField = hasOwn(req.body, 'skills');
|
|
4365
4310
|
const isSettingsOnlyUpdate = !hasTitleField && !hasDescriptionField && !hasProjectField && !hasBundleField && (hasModelField || hasVariantField || hasSkillsField);
|
|
4311
|
+
if (isTicketAgentSettingsPayload(req.body)) {
|
|
4312
|
+
const result = updateTicketAgentSettings({
|
|
4313
|
+
ticketId: req.params.id,
|
|
4314
|
+
requestedProjectId: getRequestedProjectId(req),
|
|
4315
|
+
payload: req.body,
|
|
4316
|
+
broadcaster: io,
|
|
4317
|
+
});
|
|
4318
|
+
if (!result.ok) {
|
|
4319
|
+
return res.status(result.status).json({ error: result.error });
|
|
4320
|
+
}
|
|
4321
|
+
return res.status(result.status).json(result.value);
|
|
4322
|
+
}
|
|
4366
4323
|
if (!isPlanTicket && !isSettingsOnlyUpdate) {
|
|
4367
4324
|
return res.status(400).json({ error: 'Only plan tickets can be fully edited after work begins' });
|
|
4368
4325
|
}
|
|
@@ -4480,79 +4437,21 @@ app.patch('/tickets/:id', (req, res) => {
|
|
|
4480
4437
|
res.json(updated);
|
|
4481
4438
|
});
|
|
4482
4439
|
app.patch('/tickets/:id/state', (req, res) => {
|
|
4483
|
-
const
|
|
4484
|
-
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
if (state === 'plan' && existing.state !== 'plan') {
|
|
4496
|
-
return res.status(409).json({ error: 'Tickets cannot be moved back to plan after work begins' });
|
|
4497
|
-
}
|
|
4498
|
-
if (state === 'build') {
|
|
4499
|
-
const buildBlocker = getBundleBuildBlockerResponse(existing);
|
|
4500
|
-
if (buildBlocker) {
|
|
4501
|
-
return res.status(409).json(buildBlocker);
|
|
4502
|
-
}
|
|
4503
|
-
const buildTransition = prepareTicketForBuild(existing.id);
|
|
4504
|
-
if (!buildTransition.ok) {
|
|
4505
|
-
return res.status(buildTransition.status).json({
|
|
4506
|
-
error: buildTransition.error,
|
|
4507
|
-
...(buildTransition.blockers ? { blockers: buildTransition.blockers } : {}),
|
|
4508
|
-
});
|
|
4509
|
-
}
|
|
4510
|
-
io.emit('ticket:updated', buildTransition.ticket);
|
|
4511
|
-
res.json(buildTransition.ticket);
|
|
4512
|
-
if (buildTransition.ticket.project_id && buildTransition.ticket.worktree_bundle_id) {
|
|
4513
|
-
const queuedBuild = enqueueBundledBuild({
|
|
4514
|
-
ticketId: buildTransition.ticket.id,
|
|
4515
|
-
projectId: buildTransition.ticket.project_id,
|
|
4516
|
-
worktreeBundleId: buildTransition.ticket.worktree_bundle_id,
|
|
4517
|
-
activate: shouldStartBundleBuildNow(buildTransition.ticket),
|
|
4518
|
-
});
|
|
4519
|
-
if (!queuedBuild.shouldStartNow) {
|
|
4520
|
-
return;
|
|
4521
|
-
}
|
|
4522
|
-
}
|
|
4523
|
-
startBuild(buildTransition.ticket, io);
|
|
4524
|
-
return;
|
|
4525
|
-
}
|
|
4526
|
-
if (state === 'review' && (existing.state !== 'build' || existing.agent_status !== 'done')) {
|
|
4527
|
-
return res.status(409).json({ error: 'Tickets can only be moved to review from build when done' });
|
|
4528
|
-
}
|
|
4529
|
-
const project = getEffectiveProject(existing);
|
|
4530
|
-
if (state === 'review' && !project) {
|
|
4531
|
-
return res.status(400).json({ error: 'Ticket project no longer exists' });
|
|
4532
|
-
}
|
|
4533
|
-
if (state === 'review' && existing.project_id && existing.worktree_bundle_id) {
|
|
4534
|
-
const reviewResult = reviewBundleTickets(existing.project_id, existing.worktree_bundle_id);
|
|
4535
|
-
if (!reviewResult.ok) {
|
|
4536
|
-
return res.status(reviewResult.status).json({ error: reviewResult.error });
|
|
4537
|
-
}
|
|
4538
|
-
const updated = reviewResult.details.tickets.find(ticket => ticket.id === existing.id);
|
|
4539
|
-
if (!updated) {
|
|
4540
|
-
return res.status(409).json({ error: 'Unable to update the requested ticket in this bundle' });
|
|
4541
|
-
}
|
|
4542
|
-
return res.json(updated);
|
|
4440
|
+
const result = transitionTicketState({
|
|
4441
|
+
ticketId: req.params.id,
|
|
4442
|
+
requestedState: req.body.state,
|
|
4443
|
+
requestedProjectId: getRequestedProjectId(req),
|
|
4444
|
+
broadcaster: io,
|
|
4445
|
+
reviewBundleTickets,
|
|
4446
|
+
});
|
|
4447
|
+
if (!result.ok) {
|
|
4448
|
+
return res.status(result.status).json({
|
|
4449
|
+
error: result.error,
|
|
4450
|
+
...result.details,
|
|
4451
|
+
});
|
|
4543
4452
|
}
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
const nextSessionId = existing.session_id;
|
|
4547
|
-
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);
|
|
4548
|
-
const ticket = autoParkTicketIfStale(req.params.id);
|
|
4549
|
-
if (state === 'review' && ticket) {
|
|
4550
|
-
startReview(ticket, io);
|
|
4551
|
-
const runningReview = getTicket(req.params.id);
|
|
4552
|
-
return res.json(runningReview);
|
|
4553
|
-
}
|
|
4554
|
-
io.emit('ticket:updated', ticket);
|
|
4555
|
-
res.json(ticket);
|
|
4453
|
+
res.status(result.status).json(result.value);
|
|
4454
|
+
result.afterResponse?.();
|
|
4556
4455
|
});
|
|
4557
4456
|
app.patch('/tickets/:id/park', (req, res) => {
|
|
4558
4457
|
const ticket = getTicket(req.params.id);
|
|
@@ -4754,41 +4653,19 @@ app.post('/tickets/:id/undo', (req, res) => {
|
|
|
4754
4653
|
return res.json(result.ticket);
|
|
4755
4654
|
});
|
|
4756
4655
|
app.post('/tickets/:id/run', (req, res) => {
|
|
4757
|
-
const
|
|
4758
|
-
|
|
4759
|
-
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
|
|
4766
|
-
|
|
4767
|
-
}
|
|
4768
|
-
const workspace = resolveTicketGitWorkspace(ticket);
|
|
4769
|
-
if ('error' in workspace) {
|
|
4770
|
-
return res.status(workspace.status).json({ error: workspace.error });
|
|
4771
|
-
}
|
|
4772
|
-
if (!hasRunConfig(project)) {
|
|
4773
|
-
return res.status(400).json({ error: 'No run config' });
|
|
4774
|
-
}
|
|
4775
|
-
const runReadinessError = getTicketRunReadinessError(ticket, project);
|
|
4776
|
-
if (runReadinessError) {
|
|
4777
|
-
return res.status(409).json({ error: runReadinessError });
|
|
4778
|
-
}
|
|
4779
|
-
if (isRunning(ticket)) {
|
|
4780
|
-
emitRunStateForContext(ticket, true);
|
|
4781
|
-
const runningTicket = listTicketsInRunContext(ticket).find(entry => entry.id === ticket.id);
|
|
4782
|
-
return res.json(runningTicket ?? { ...ticket, is_running: true, has_run_config: true });
|
|
4783
|
-
}
|
|
4784
|
-
const activeRunConflict = formatActiveRunConflict(ticket.project_id);
|
|
4785
|
-
if (activeRunConflict) {
|
|
4786
|
-
return res.status(409).json(activeRunConflict);
|
|
4656
|
+
const result = runTicketRun({
|
|
4657
|
+
ticketId: req.params.id,
|
|
4658
|
+
requestedProjectId: getRequestedProjectId(req),
|
|
4659
|
+
broadcaster: io,
|
|
4660
|
+
});
|
|
4661
|
+
if (!result.ok) {
|
|
4662
|
+
return res.status(result.status).json({
|
|
4663
|
+
error: result.error,
|
|
4664
|
+
...result.details,
|
|
4665
|
+
});
|
|
4787
4666
|
}
|
|
4788
|
-
|
|
4789
|
-
|
|
4790
|
-
res.json(running);
|
|
4791
|
-
runTicketInWorkspace(ticket, io, project, workspace.cwd);
|
|
4667
|
+
res.status(result.status).json(result.value);
|
|
4668
|
+
result.afterResponse?.();
|
|
4792
4669
|
});
|
|
4793
4670
|
app.post('/tickets/:id/open-ide', (req, res) => {
|
|
4794
4671
|
const ticket = getTicket(req.params.id);
|
|
@@ -4981,46 +4858,17 @@ app.post('/projects/:id/open-ide', (req, res) => {
|
|
|
4981
4858
|
return res.json({ ok: true });
|
|
4982
4859
|
});
|
|
4983
4860
|
app.post('/projects/:id/run', (req, res) => {
|
|
4984
|
-
const
|
|
4985
|
-
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
|
|
4990
|
-
|
|
4991
|
-
const workspace = resolveProjectTargetGitWorkspace(project);
|
|
4992
|
-
if ('error' in workspace) {
|
|
4993
|
-
return res.status(workspace.status).json({ error: workspace.error });
|
|
4994
|
-
}
|
|
4995
|
-
const contextKey = workspace.run_context_key;
|
|
4996
|
-
if (isRunning(contextKey)) {
|
|
4997
|
-
return res.json({
|
|
4998
|
-
active_run: {
|
|
4999
|
-
kind: 'project',
|
|
5000
|
-
context_key: contextKey,
|
|
5001
|
-
project_id: project.id,
|
|
5002
|
-
target_kind: workspace.target_kind,
|
|
5003
|
-
target_bundle_id: workspace.target_bundle_id,
|
|
5004
|
-
},
|
|
4861
|
+
const result = runProjectTarget({
|
|
4862
|
+
projectId: req.params.id,
|
|
4863
|
+
});
|
|
4864
|
+
if (!result.ok) {
|
|
4865
|
+
return res.status(result.status).json({
|
|
4866
|
+
error: result.error,
|
|
4867
|
+
...result.details,
|
|
5005
4868
|
});
|
|
5006
4869
|
}
|
|
5007
|
-
|
|
5008
|
-
|
|
5009
|
-
return res.status(409).json(activeRunConflict);
|
|
5010
|
-
}
|
|
5011
|
-
const activeRun = {
|
|
5012
|
-
kind: 'project',
|
|
5013
|
-
context_key: contextKey,
|
|
5014
|
-
project_id: project.id,
|
|
5015
|
-
target_kind: workspace.target_kind,
|
|
5016
|
-
target_bundle_id: workspace.target_bundle_id,
|
|
5017
|
-
};
|
|
5018
|
-
res.json({ active_run: activeRun });
|
|
5019
|
-
void runProject(project, {
|
|
5020
|
-
cwd: workspace.cwd,
|
|
5021
|
-
targetKind: workspace.target_kind,
|
|
5022
|
-
targetBundleId: workspace.target_bundle_id,
|
|
5023
|
-
});
|
|
4870
|
+
res.status(result.status).json(result.value);
|
|
4871
|
+
result.afterResponse?.();
|
|
5024
4872
|
});
|
|
5025
4873
|
app.post('/projects/:id/test-sessions', (req, res) => {
|
|
5026
4874
|
const project = getProjectById(req.params.id);
|
|
@@ -5203,154 +5051,69 @@ app.post('/terminals/:sessionId/destroy', (req, res) => {
|
|
|
5203
5051
|
return res.status(204).send();
|
|
5204
5052
|
});
|
|
5205
5053
|
app.post('/tickets/:id/stop', async (req, res) => {
|
|
5206
|
-
const
|
|
5207
|
-
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
|
|
5211
|
-
|
|
5054
|
+
const result = await stopTicketRun({
|
|
5055
|
+
ticketId: req.params.id,
|
|
5056
|
+
requestedProjectId: getRequestedProjectId(req),
|
|
5057
|
+
broadcaster: io,
|
|
5058
|
+
});
|
|
5059
|
+
if (!result.ok) {
|
|
5060
|
+
return res.status(result.status).json({ error: result.error });
|
|
5212
5061
|
}
|
|
5213
|
-
|
|
5214
|
-
emitRunStateForContext(ticket, false);
|
|
5215
|
-
const updated = listTicketsInRunContext(ticket).find(entry => entry.id === ticket.id) ?? { ...ticket, is_running: false };
|
|
5216
|
-
res.json(updated);
|
|
5062
|
+
return res.status(result.status).json(result.value);
|
|
5217
5063
|
});
|
|
5218
5064
|
app.post('/projects/:id/stop-run', async (req, res) => {
|
|
5219
|
-
const
|
|
5220
|
-
|
|
5221
|
-
return res.status(404).json({ error: 'Not found' });
|
|
5222
|
-
}
|
|
5223
|
-
await stopProjectRun(project.id, {
|
|
5224
|
-
kind: project.active_target_kind,
|
|
5225
|
-
bundleId: project.active_target_bundle_id,
|
|
5065
|
+
const result = await stopProjectTargetRun({
|
|
5066
|
+
projectId: req.params.id,
|
|
5226
5067
|
});
|
|
5227
|
-
|
|
5068
|
+
if (!result.ok) {
|
|
5069
|
+
return res.status(result.status).json({ error: result.error });
|
|
5070
|
+
}
|
|
5071
|
+
return res.status(result.status).json(result.value);
|
|
5228
5072
|
});
|
|
5229
5073
|
app.post('/tickets/:id/stop-build', async (req, res) => {
|
|
5230
|
-
const
|
|
5231
|
-
|
|
5232
|
-
|
|
5233
|
-
}
|
|
5234
|
-
if (!
|
|
5235
|
-
return res.status(
|
|
5236
|
-
}
|
|
5237
|
-
if (!canUseBuildControls(ticket)) {
|
|
5238
|
-
return res.status(400).json({ error: 'Only build-stage and review follow-up tickets can stop builds' });
|
|
5239
|
-
}
|
|
5240
|
-
if (ticket.agent_status !== 'running') {
|
|
5241
|
-
return res.status(400).json({ error: 'Ticket build is not running' });
|
|
5242
|
-
}
|
|
5243
|
-
if (!ticket.session_id) {
|
|
5244
|
-
return res.status(400).json({ error: 'Build session is not ready to stop yet' });
|
|
5245
|
-
}
|
|
5246
|
-
if (!isBuildAgentRunning(ticket.id)) {
|
|
5247
|
-
return res.status(409).json({ error: 'No active build process found for this ticket' });
|
|
5248
|
-
}
|
|
5249
|
-
await stopBuildAgent(ticket.id);
|
|
5250
|
-
const updated = getTicket(req.params.id);
|
|
5251
|
-
if (!updated) {
|
|
5252
|
-
return res.status(404).json({ error: 'Not found' });
|
|
5074
|
+
const result = await stopBuildTicketRun({
|
|
5075
|
+
ticketId: req.params.id,
|
|
5076
|
+
requestedProjectId: getRequestedProjectId(req),
|
|
5077
|
+
});
|
|
5078
|
+
if (!result.ok) {
|
|
5079
|
+
return res.status(result.status).json({ error: result.error });
|
|
5253
5080
|
}
|
|
5254
|
-
res.json(
|
|
5081
|
+
return res.status(result.status).json(result.value);
|
|
5255
5082
|
});
|
|
5256
5083
|
app.post('/tickets/:id/resume-build', (req, res) => {
|
|
5257
|
-
const
|
|
5258
|
-
|
|
5259
|
-
|
|
5260
|
-
|
|
5261
|
-
|
|
5262
|
-
|
|
5263
|
-
|
|
5264
|
-
if (!canUseBuildControls(existing)) {
|
|
5265
|
-
return res.status(400).json({ error: 'Only build-stage and review follow-up tickets can resume builds' });
|
|
5266
|
-
}
|
|
5267
|
-
if (existing.agent_status !== 'stopped') {
|
|
5268
|
-
return res.status(400).json({ error: 'Only manually stopped builds can be resumed' });
|
|
5269
|
-
}
|
|
5270
|
-
if (!existing.session_id) {
|
|
5271
|
-
return res.status(400).json({ error: 'Build session is not available to resume' });
|
|
5272
|
-
}
|
|
5273
|
-
db.prepare('UPDATE tickets SET build_completed_at = NULL, auto_park_dismissed_at = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(existing.id);
|
|
5274
|
-
const ticket = getTicket(existing.id);
|
|
5275
|
-
if (!ticket) {
|
|
5276
|
-
return res.status(404).json({ error: 'Not found' });
|
|
5084
|
+
const result = resumeBuildTicketRun({
|
|
5085
|
+
ticketId: req.params.id,
|
|
5086
|
+
requestedProjectId: getRequestedProjectId(req),
|
|
5087
|
+
broadcaster: io,
|
|
5088
|
+
});
|
|
5089
|
+
if (!result.ok) {
|
|
5090
|
+
return res.status(result.status).json({ error: result.error });
|
|
5277
5091
|
}
|
|
5278
|
-
|
|
5279
|
-
res.json(ticket);
|
|
5092
|
+
return res.status(result.status).json(result.value);
|
|
5280
5093
|
});
|
|
5281
5094
|
app.post('/tickets/:id/follow-up', (req, res) => {
|
|
5282
|
-
const
|
|
5283
|
-
|
|
5284
|
-
|
|
5285
|
-
|
|
5286
|
-
|
|
5287
|
-
return res.status(404).json({ error: 'Not found' });
|
|
5288
|
-
}
|
|
5289
|
-
if (ticket.state === 'plan') {
|
|
5290
|
-
return res.status(400).json({ error: 'Only non-plan tickets can accept follow-up prompts' });
|
|
5291
|
-
}
|
|
5292
|
-
if (ticket.agent_status === 'running') {
|
|
5293
|
-
return res.status(409).json({ error: 'Ticket is already running' });
|
|
5294
|
-
}
|
|
5295
|
-
const instruction = typeof req.body?.instruction === 'string'
|
|
5296
|
-
? req.body.instruction.trim()
|
|
5297
|
-
: '';
|
|
5298
|
-
if (!instruction) {
|
|
5299
|
-
return res.status(400).json({ error: 'instruction is required' });
|
|
5300
|
-
}
|
|
5301
|
-
const project = getEffectiveProject(ticket);
|
|
5302
|
-
if (!project) {
|
|
5303
|
-
return res.status(400).json({ error: 'Ticket project no longer exists' });
|
|
5304
|
-
}
|
|
5305
|
-
const branch = resolveTicketBranch(ticket);
|
|
5306
|
-
if (!branch || ticket.has_worktree === false || !isWorktreeReady(branch, project)) {
|
|
5307
|
-
return res.status(400).json({ error: 'Worktree not ready yet' });
|
|
5308
|
-
}
|
|
5309
|
-
createTicketMessage({
|
|
5310
|
-
ticketId: ticket.id,
|
|
5311
|
-
role: 'user',
|
|
5312
|
-
kind: 'follow_up',
|
|
5313
|
-
content: instruction,
|
|
5095
|
+
const result = submitTicketFollowUp({
|
|
5096
|
+
ticketId: req.params.id,
|
|
5097
|
+
requestedProjectId: getRequestedProjectId(req),
|
|
5098
|
+
payload: req.body,
|
|
5099
|
+
broadcaster: io,
|
|
5314
5100
|
});
|
|
5315
|
-
|
|
5316
|
-
|
|
5317
|
-
return res.status(409).json({ error: 'Ticket is already running' });
|
|
5101
|
+
if (!result.ok) {
|
|
5102
|
+
return res.status(result.status).json({ error: result.error });
|
|
5318
5103
|
}
|
|
5319
|
-
|
|
5320
|
-
return res.json(updated ?? ticket);
|
|
5104
|
+
return res.status(result.status).json(result.value);
|
|
5321
5105
|
});
|
|
5322
5106
|
app.post('/tickets/:id/plan-follow-up', (req, res) => {
|
|
5323
|
-
const
|
|
5324
|
-
|
|
5325
|
-
|
|
5326
|
-
|
|
5327
|
-
|
|
5328
|
-
return res.status(404).json({ error: 'Not found' });
|
|
5329
|
-
}
|
|
5330
|
-
if (ticket.state !== 'plan') {
|
|
5331
|
-
return res.status(400).json({ error: 'Only plan tickets can accept planning follow-up prompts' });
|
|
5332
|
-
}
|
|
5333
|
-
if (ticket.agent_status === 'running') {
|
|
5334
|
-
return res.status(409).json({ error: 'Ticket is already running' });
|
|
5335
|
-
}
|
|
5336
|
-
const instruction = typeof req.body?.instruction === 'string'
|
|
5337
|
-
? req.body.instruction.trim()
|
|
5338
|
-
: '';
|
|
5339
|
-
if (!instruction) {
|
|
5340
|
-
return res.status(400).json({ error: 'instruction is required' });
|
|
5341
|
-
}
|
|
5342
|
-
createTicketMessage({
|
|
5343
|
-
ticketId: ticket.id,
|
|
5344
|
-
role: 'user',
|
|
5345
|
-
kind: 'planning_follow_up',
|
|
5346
|
-
content: instruction,
|
|
5107
|
+
const result = submitPlanTicketFollowUp({
|
|
5108
|
+
ticketId: req.params.id,
|
|
5109
|
+
requestedProjectId: getRequestedProjectId(req),
|
|
5110
|
+
payload: req.body,
|
|
5111
|
+
broadcaster: io,
|
|
5347
5112
|
});
|
|
5348
|
-
|
|
5349
|
-
|
|
5350
|
-
return res.status(409).json({ error: 'Ticket is already running' });
|
|
5113
|
+
if (!result.ok) {
|
|
5114
|
+
return res.status(result.status).json({ error: result.error });
|
|
5351
5115
|
}
|
|
5352
|
-
|
|
5353
|
-
return res.json(updated ?? ticket);
|
|
5116
|
+
return res.status(result.status).json(result.value);
|
|
5354
5117
|
});
|
|
5355
5118
|
app.post('/tickets/:id/plan', (req, res) => {
|
|
5356
5119
|
const ticket = getTicket(req.params.id);
|
|
@@ -5429,26 +5192,20 @@ io.on('connection', (socket) => {
|
|
|
5429
5192
|
registerTerminalSocketHandlers(io, socket);
|
|
5430
5193
|
});
|
|
5431
5194
|
const DEFAULT_PORT = 3001;
|
|
5432
|
-
const resolveListeningHost = (host) => {
|
|
5433
|
-
if (!host || host === '0.0.0.0' || host === '::') {
|
|
5434
|
-
return 'localhost';
|
|
5435
|
-
}
|
|
5436
|
-
return host;
|
|
5437
|
-
};
|
|
5438
5195
|
const resolveStartResult = (host) => {
|
|
5439
5196
|
const address = httpServer.address();
|
|
5440
|
-
const
|
|
5197
|
+
const { displayHost } = resolveDesktopServerHost(host);
|
|
5441
5198
|
if (!address || typeof address === 'string') {
|
|
5442
5199
|
return {
|
|
5443
|
-
host:
|
|
5200
|
+
host: displayHost,
|
|
5444
5201
|
port: DEFAULT_PORT,
|
|
5445
|
-
url: `http://${
|
|
5202
|
+
url: `http://${displayHost}:${DEFAULT_PORT}`,
|
|
5446
5203
|
};
|
|
5447
5204
|
}
|
|
5448
5205
|
return {
|
|
5449
|
-
host:
|
|
5206
|
+
host: displayHost,
|
|
5450
5207
|
port: address.port,
|
|
5451
|
-
url: `http://${
|
|
5208
|
+
url: `http://${displayHost}:${address.port}`,
|
|
5452
5209
|
};
|
|
5453
5210
|
};
|
|
5454
5211
|
export const startServer = ({ port = DEFAULT_PORT, host } = {}) => {
|
|
@@ -5466,14 +5223,12 @@ export const startServer = ({ port = DEFAULT_PORT, host } = {}) => {
|
|
|
5466
5223
|
resolve(resolveStartResult(host));
|
|
5467
5224
|
};
|
|
5468
5225
|
httpServer.once('error', handleError);
|
|
5469
|
-
|
|
5470
|
-
|
|
5471
|
-
return;
|
|
5472
|
-
}
|
|
5473
|
-
httpServer.listen(port, handleListening);
|
|
5226
|
+
const { bindHost } = resolveDesktopServerHost(host);
|
|
5227
|
+
httpServer.listen(port, bindHost, handleListening);
|
|
5474
5228
|
});
|
|
5475
5229
|
};
|
|
5476
5230
|
export const shutdownServer = async (signal) => {
|
|
5231
|
+
await stopMobileAccess();
|
|
5477
5232
|
await clearAllTestSessions(io);
|
|
5478
5233
|
await shutdownRealtimeServer({
|
|
5479
5234
|
signal,
|