@straiffi/archon 1.11.8 → 1.11.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/dist/client/assets/{ActivityView-BKLYfPXE.js → ActivityView-RnE0983a.js} +1 -1
  2. package/dist/client/assets/{GitHubActionsDialog-yqqwcP8z.js → GitHubActionsDialog-PaNJhVzv.js} +1 -1
  3. package/dist/client/assets/{VerificationDialog-DhyIDs1V.js → VerificationDialog-PGrCJ1DJ.js} +1 -1
  4. package/dist/client/assets/{architectureDiagram-3BPJPVTR-5krjaOU4.js → architectureDiagram-3BPJPVTR-CKd1Dmpz.js} +1 -1
  5. package/dist/client/assets/{blockDiagram-GPEHLZMM-Duo2q2gn.js → blockDiagram-GPEHLZMM-M3OjRhA0.js} +1 -1
  6. package/dist/client/assets/{c4Diagram-AAUBKEIU-D2lxLmwB.js → c4Diagram-AAUBKEIU-VWBcGTXO.js} +1 -1
  7. package/dist/client/assets/channel-CBQk1IQf.js +1 -0
  8. package/dist/client/assets/{chunk-2J33WTMH-Dbo5lbSs.js → chunk-2J33WTMH-C4WpCUzU.js} +1 -1
  9. package/dist/client/assets/{chunk-3OPIFGDE-mjZrNxCg.js → chunk-3OPIFGDE-BQ-j87T6.js} +1 -1
  10. package/dist/client/assets/{chunk-5ZQYHXKU-C2nUnqLh.js → chunk-5ZQYHXKU-BCZvyT9u.js} +1 -1
  11. package/dist/client/assets/{chunk-727SXJPM-7eUgm0cn.js → chunk-727SXJPM-DZP_PMNz.js} +1 -1
  12. package/dist/client/assets/{chunk-AQP2D5EJ-DouWpaE0.js → chunk-AQP2D5EJ-Cejit8wh.js} +1 -1
  13. package/dist/client/assets/{chunk-CSCIHK7Q-DesC0Fd7.js → chunk-CSCIHK7Q-CvXC4rkf.js} +1 -1
  14. package/dist/client/assets/{chunk-KSCS5N6A-DAsFowjn.js → chunk-KSCS5N6A-hTgJWrr3.js} +1 -1
  15. package/dist/client/assets/{chunk-L5ZTLDWV-w7V9r0Cw.js → chunk-L5ZTLDWV-bSK1mut3.js} +1 -1
  16. package/dist/client/assets/{chunk-LZXEDZCA-BCnxdvaJ.js → chunk-LZXEDZCA-Y2hnSpRr.js} +2 -2
  17. package/dist/client/assets/{chunk-ND2GUHAM-DO8HdZCX.js → chunk-ND2GUHAM-BiHH5lS5.js} +1 -1
  18. package/dist/client/assets/{chunk-NZK2D7GU-37Rbzvm9.js → chunk-NZK2D7GU-DdxcwTTc.js} +1 -1
  19. package/dist/client/assets/{chunk-O5CBEL6O-DTjB0GGI.js → chunk-O5CBEL6O-BZIbgKJr.js} +1 -1
  20. package/dist/client/assets/{chunk-WU5MYG2G-CAonpz-x.js → chunk-WU5MYG2G-D3yvqt9P.js} +1 -1
  21. package/dist/client/assets/classDiagram-4FO5ZUOK-BjizLQoS.js +1 -0
  22. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-B6qpU-kO.js +1 -0
  23. package/dist/client/assets/{dagre-BM42HDAG-DDET9pbe.js → dagre-BM42HDAG-CZQZPoaH.js} +1 -1
  24. package/dist/client/assets/{diagram-2AECGRRQ-r_bu5Cgo.js → diagram-2AECGRRQ-DU7zsX_b.js} +1 -1
  25. package/dist/client/assets/{diagram-5GNKFQAL-QlPCzkUJ.js → diagram-5GNKFQAL-BCCoHKlp.js} +1 -1
  26. package/dist/client/assets/{diagram-KO2AKTUF-D5SzFOzP.js → diagram-KO2AKTUF-BR_GNJLN.js} +1 -1
  27. package/dist/client/assets/{diagram-LMA3HP47-BJcWw3_s.js → diagram-LMA3HP47-B6LZOUTc.js} +1 -1
  28. package/dist/client/assets/{diagram-OG6HWLK6-CpsR9p94.js → diagram-OG6HWLK6-w89pjkBe.js} +1 -1
  29. package/dist/client/assets/{erDiagram-TEJ5UH35-Dq-K6YrY.js → erDiagram-TEJ5UH35-BReR8ngL.js} +1 -1
  30. package/dist/client/assets/{flowDiagram-I6XJVG4X-39hKkH4n.js → flowDiagram-I6XJVG4X-CGwpJ9iQ.js} +1 -1
  31. package/dist/client/assets/{ganttDiagram-6RSMTGT7-DaeBYOLz.js → ganttDiagram-6RSMTGT7-BcOPz4K9.js} +1 -1
  32. package/dist/client/assets/{gitGraphDiagram-PVQCEYII-CCeo9Czy.js → gitGraphDiagram-PVQCEYII-m6-x38H5.js} +1 -1
  33. package/dist/client/assets/index-D_brbG-0.css +2 -0
  34. package/dist/client/assets/index-yUckGfMu.js +293 -0
  35. package/dist/client/assets/{infoDiagram-5YYISTIA-jgHH1Ta7.js → infoDiagram-5YYISTIA-11iWCl_6.js} +1 -1
  36. package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-gYPsFHRd.js → ishikawaDiagram-YF4QCWOH-DbeJ14eh.js} +1 -1
  37. package/dist/client/assets/{journeyDiagram-JHISSGLW-C5m8h46L.js → journeyDiagram-JHISSGLW-BYFdU9SL.js} +1 -1
  38. package/dist/client/assets/{kanban-definition-UN3LZRKU-rag4UxAB.js → kanban-definition-UN3LZRKU-Dyc52bol.js} +1 -1
  39. package/dist/client/assets/{line-CAWxQrG3.js → line-CBauYkto.js} +1 -1
  40. package/dist/client/assets/{mermaid-parser.core-C5Hj2V3U.js → mermaid-parser.core-BBbL8arn.js} +1 -1
  41. package/dist/client/assets/{mermaid.core-BxRoDLxv.js → mermaid.core-CB2mIiiP.js} +3 -3
  42. package/dist/client/assets/{mindmap-definition-RKZ34NQL-BUV37aab.js → mindmap-definition-RKZ34NQL-CvWYGfWn.js} +1 -1
  43. package/dist/client/assets/{pieDiagram-4H26LBE5-CMwVVhpZ.js → pieDiagram-4H26LBE5-BsbIF0rU.js} +1 -1
  44. package/dist/client/assets/{quadrantDiagram-W4KKPZXB-DYHRB097.js → quadrantDiagram-W4KKPZXB-Bvs09UnJ.js} +1 -1
  45. package/dist/client/assets/{requirementDiagram-4Y6WPE33-BLtNTWtR.js → requirementDiagram-4Y6WPE33-B3U5xSYZ.js} +1 -1
  46. package/dist/client/assets/{sankeyDiagram-5OEKKPKP-Bcd-UVHZ.js → sankeyDiagram-5OEKKPKP-Dds8cy2g.js} +1 -1
  47. package/dist/client/assets/{sequenceDiagram-3UESZ5HK-Die4-puP.js → sequenceDiagram-3UESZ5HK-BoMb08Ay.js} +1 -1
  48. package/dist/client/assets/{stateDiagram-AJRCARHV-TBbOyQST.js → stateDiagram-AJRCARHV-atm7kYDg.js} +1 -1
  49. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-EYJsus1Q.js +1 -0
  50. package/dist/client/assets/{timeline-definition-PNZ67QCA-CZ3E6e1X.js → timeline-definition-PNZ67QCA-C7U--kCB.js} +1 -1
  51. package/dist/client/assets/{vennDiagram-CIIHVFJN-CY_kofMf.js → vennDiagram-CIIHVFJN-BoPWwPSI.js} +1 -1
  52. package/dist/client/assets/{wardleyDiagram-YWT4CUSO-TzBIV1Er.js → wardleyDiagram-YWT4CUSO-CJ6Y7g3b.js} +1 -1
  53. package/dist/client/assets/{xychartDiagram-2RQKCTM6-BTvKU2GO.js → xychartDiagram-2RQKCTM6-rN5shXue.js} +1 -1
  54. package/dist/client/index.html +2 -2
  55. package/dist/server/db.js +2 -0
  56. package/dist/server/db.js.map +1 -1
  57. package/dist/server/index.js +212 -238
  58. package/dist/server/index.js.map +1 -1
  59. package/dist/server/lib/agent.js +23 -7
  60. package/dist/server/lib/agent.js.map +1 -1
  61. package/dist/server/lib/agentProvider.js +62 -15
  62. package/dist/server/lib/agentProvider.js.map +1 -1
  63. package/dist/server/lib/bundleCleanupManagement.js +181 -0
  64. package/dist/server/lib/bundleCleanupManagement.js.map +1 -0
  65. package/dist/server/lib/bundleCleanupRoutes.js +36 -0
  66. package/dist/server/lib/bundleCleanupRoutes.js.map +1 -0
  67. package/dist/server/lib/bundleDeletion.js +257 -0
  68. package/dist/server/lib/bundleDeletion.js.map +1 -0
  69. package/dist/server/lib/bundleDeletionRoutes.js +73 -0
  70. package/dist/server/lib/bundleDeletionRoutes.js.map +1 -0
  71. package/dist/server/lib/bundleDeletionSchema.js +134 -0
  72. package/dist/server/lib/bundleDeletionSchema.js.map +1 -0
  73. package/dist/server/lib/bundleDeletionStore.js +18 -0
  74. package/dist/server/lib/bundleDeletionStore.js.map +1 -0
  75. package/dist/server/lib/bundleWorktreeCleanup.js +160 -0
  76. package/dist/server/lib/bundleWorktreeCleanup.js.map +1 -0
  77. package/dist/server/lib/codexAgentProvider.js +186 -123
  78. package/dist/server/lib/codexAgentProvider.js.map +1 -1
  79. package/dist/server/lib/diffExplanation.js +485 -0
  80. package/dist/server/lib/diffExplanation.js.map +1 -0
  81. package/dist/server/lib/diffExplanationJobs.js +605 -0
  82. package/dist/server/lib/diffExplanationJobs.js.map +1 -0
  83. package/dist/server/lib/diffExplanationRoutes.js +236 -0
  84. package/dist/server/lib/diffExplanationRoutes.js.map +1 -0
  85. package/dist/server/lib/dockerSandboxCleanup.js +68 -0
  86. package/dist/server/lib/dockerSandboxCleanup.js.map +1 -0
  87. package/dist/server/lib/dockerSandboxEnvironmentOperations.js +4 -3
  88. package/dist/server/lib/dockerSandboxEnvironmentOperations.js.map +1 -1
  89. package/dist/server/lib/dockerSandboxExecutionEnvironment.js +35 -59
  90. package/dist/server/lib/dockerSandboxExecutionEnvironment.js.map +1 -1
  91. package/dist/server/lib/dockerSandboxReleaseProfiles.js +1 -1
  92. package/dist/server/lib/dockerSandboxReleaseProfiles.js.map +1 -1
  93. package/dist/server/lib/executionEnvironmentOperations.js +7 -2
  94. package/dist/server/lib/executionEnvironmentOperations.js.map +1 -1
  95. package/dist/server/lib/executionEnvironmentReconciliation.js +34 -169
  96. package/dist/server/lib/executionEnvironmentReconciliation.js.map +1 -1
  97. package/dist/server/lib/executionEnvironmentTargetCleanup.js +232 -0
  98. package/dist/server/lib/executionEnvironmentTargetCleanup.js.map +1 -0
  99. package/dist/server/lib/executionEnvironments.js +1 -0
  100. package/dist/server/lib/executionEnvironments.js.map +1 -1
  101. package/dist/server/lib/executionTargets.js +1 -1
  102. package/dist/server/lib/executionTargets.js.map +1 -1
  103. package/dist/server/lib/git.js +205 -35
  104. package/dist/server/lib/git.js.map +1 -1
  105. package/dist/server/lib/i18n/resources.js +10 -0
  106. package/dist/server/lib/i18n/resources.js.map +1 -1
  107. package/dist/server/lib/lightweightPrompt.js +284 -52
  108. package/dist/server/lib/lightweightPrompt.js.map +1 -1
  109. package/dist/server/lib/oneShotCancellation.js +54 -0
  110. package/dist/server/lib/oneShotCancellation.js.map +1 -0
  111. package/dist/server/lib/opencodeExecutionReliability.js +46 -2
  112. package/dist/server/lib/opencodeExecutionReliability.js.map +1 -1
  113. package/dist/server/lib/projectMemoryScan.js +3 -46
  114. package/dist/server/lib/projectMemoryScan.js.map +1 -1
  115. package/dist/server/lib/promptDiffSafety.js +53 -0
  116. package/dist/server/lib/promptDiffSafety.js.map +1 -0
  117. package/dist/server/lib/repositorySecrets.js +69 -0
  118. package/dist/server/lib/repositorySecrets.js.map +1 -0
  119. package/dist/server/lib/tickets.js +3 -2
  120. package/dist/server/lib/tickets.js.map +1 -1
  121. package/dist/server/lib/types.js.map +1 -1
  122. package/dist/server/lib/unifiedDiff.js +459 -0
  123. package/dist/server/lib/unifiedDiff.js.map +1 -0
  124. package/dist/server/lib/workspaceDiff.js +156 -0
  125. package/dist/server/lib/workspaceDiff.js.map +1 -0
  126. package/dist/server/lib/workspaceDiffRoutes.js +63 -0
  127. package/dist/server/lib/workspaceDiffRoutes.js.map +1 -0
  128. package/dist/server/lib/workspaceTargets.js +88 -0
  129. package/dist/server/lib/workspaceTargets.js.map +1 -0
  130. package/dist/server/workers/chat.js +71 -1
  131. package/dist/server/workers/chat.js.map +1 -1
  132. package/dist/server/workers/plan.js +24 -2
  133. package/dist/server/workers/plan.js.map +1 -1
  134. package/dist/server/workers/review.js +19 -13
  135. package/dist/server/workers/review.js.map +1 -1
  136. package/package.json +1 -1
  137. package/dist/client/assets/channel-Cf2U7nU4.js +0 -1
  138. package/dist/client/assets/classDiagram-4FO5ZUOK-CK8VsU-L.js +0 -1
  139. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-RvySOEwt.js +0 -1
  140. package/dist/client/assets/index-B9-M1vEw.js +0 -287
  141. package/dist/client/assets/index-CbYT3N3U.css +0 -2
  142. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-BRotXz7h.js +0 -1
@@ -8,8 +8,16 @@ import { createHash, randomUUID } from 'crypto';
8
8
  import cors from 'cors';
9
9
  import { adjectives, animals, uniqueNamesGenerator } from 'unique-names-generator';
10
10
  import db from './db.js';
11
+ import { registerBundleDeletionRoutes } from './lib/bundleDeletionRoutes.js';
12
+ import { registerBundleCleanupRoutes } from './lib/bundleCleanupRoutes.js';
13
+ import { attemptBundleDeletion, beginBundleDeletion, BundleDeletionConflict, resumeBundleDeletions } from './lib/bundleDeletion.js';
14
+ import { isBundleDeletionBranchReserved } from './lib/bundleDeletionStore.js';
11
15
  import { pruneAllRepositoryGraphScans } from './lib/repositoryGraphRetention.js';
12
16
  import { registerRepositoryGraphRoutes } from './lib/repositoryGraphRoutes.js';
17
+ import { registerWorkspaceDiffRoutes } from './lib/workspaceDiffRoutes.js';
18
+ import { getWorkspaceDiffAsync } from './lib/workspaceDiff.js';
19
+ import { registerDiffExplanationRoutes, registerDiffExplanationSocketHandlers, } from './lib/diffExplanationRoutes.js';
20
+ import { openDiffExplanationJobAdmissions, shutdownDiffExplanationJobs, } from './lib/diffExplanationJobs.js';
13
21
  import { registerExecutionEnvironmentRoutes } from './lib/executionEnvironmentRoutes.js';
14
22
  import { registerExecutionProfileRoutes } from './lib/executionProfileRoutes.js';
15
23
  import { executionProfileSupportsChecks, hasConfiguredCheckPolicy } from './lib/executionProfileCheckPolicy.js';
@@ -34,7 +42,7 @@ import { resolveSessionTargetForExecution } from './lib/sessionTargets.js';
34
42
  import { createChatMessage, getChatMessage, updateChatMessageContent } from './lib/chatMessages.js';
35
43
  import { attachChatTicketProposalsToMessageContent } from './lib/chatTicketProposals.js';
36
44
  import { createCorsOriginResolver, parseCorsOrigins } from './lib/cors.js';
37
- import { abortGitOperation, branchExistsLocally, commitGitChanges, continueGitRebase, createWorktreeAsync, deleteLocalBranch, deleteWorktree, getActiveRebaseBranch, getBundleReviewDiffForCwd, getGitDiffDataAsync, getGitDiffFiles, getGitDiffFilesAsync, getGitStatus, getGitStatusAsync, getGitUnifiedDiff, getGitUnifiedDiffAsync, getLocalProjectBranch, getProjectBranch, getProjectBranchAsync, getProjectBranchSuggestions, getRebaseStagedDiff, getReviewDiffDataAsync, getSelectedGitUnifiedDiff, getUnpushedCommitDiffForCwd, getUnpushedCommitSummaryForCwd, getUnpushedCommitSummaryForCwdAsync, hasKnownBranchRef, listUnpushedCommitsForCwd, pullGitBranch, pushCurrentBranch, rebaseGitBranchOntoBase, resolveExistingWorktreePath, resolveWorktreePath, skipGitRebaseStep, squashUnpushedCommitsForCwd, summarizeTicketDiffFiles, switchProjectBranch, syncWorktreeFilesAsync, undoLatestUnpushedCommitForCwd, UnpushedCommitDiffSelectionError } from './lib/git.js';
45
+ import { abortGitOperation, branchExistsLocally, commitGitChanges, continueGitRebase, createWorktreeAsync, deleteLocalBranch, deleteWorktree, getActiveRebaseBranch, getGitDiffFiles, getGitDiffFilesAsync, getGitStatus, getGitStatusAsync, getGitUnifiedDiff, getLocalProjectBranch, getProjectBranch, getProjectBranchAsync, getProjectBranchSuggestions, getRebaseStagedDiff, getSelectedGitUnifiedDiff, getUnpushedCommitDiffForCwd, getUnpushedCommitSummaryForCwd, getUnpushedCommitSummaryForCwdAsync, hasKnownBranchRef, listUnpushedCommitsForCwd, pullGitBranch, pushCurrentBranch, rebaseGitBranchOntoBase, resolveExistingWorktreePath, resolveWorktreePath, skipGitRebaseStep, squashUnpushedCommitsForCwd, summarizeTicketDiffFiles, switchProjectBranch, syncWorktreeFilesAsync, undoLatestUnpushedCommitForCwd, UnpushedCommitDiffSelectionError } from './lib/git.js';
38
46
  import config from './lib/config.js';
39
47
  import { getActiveTool, initializeActiveTool, setActiveTool } from './lib/activeTool.js';
40
48
  import { autoConfigProject, ProjectAutoConfigError } from './lib/projectAutoConfig.js';
@@ -44,7 +52,7 @@ import { resolveDesktopServerHost } from './lib/desktopServerHost.js';
44
52
  import { emitMobileRealtime, getMobileAccessStatus, regenerateMobilePairingChallenge, revokeMobileSession, setMobileAccessStatusChangeListener, startMobileAccess, stopMobileAccess, } from './lib/mobileAccess.js';
45
53
  import { validateDesktopMobileAccessRequest } from './lib/mobileAccessSecurity.js';
46
54
  import { refreshChatSessionUsageSnapshot, refreshSpecSessionUsageSnapshot, stopAllBuildAgents, stopAllReviewAgents, stopBuildAgent, stopReviewAgent } from './lib/agent.js';
47
- import { runTrackedLightweightPrompt } from './lib/lightweightPrompt.js';
55
+ import { openLightweightPromptAdmissions, runTrackedLightweightPrompt, shutdownLightweightPrompts, } from './lib/lightweightPrompt.js';
48
56
  import { advanceBuildChain, startBuildChain, stopBuildChain } from './lib/buildChains.js';
49
57
  import { activateQueuedBuildSequenceTicket, bindBuildSequenceTicketExecutionRun, compensateBuildSequenceAcceptance, enqueueBundledBuild, getDependencyOrderedBundleTicketIds, getBuildSequenceIdForTicket, getBuildSequenceTicketExecutionProfileSelection, hasActiveSequenceForBundle, releaseQueuedBuildSequenceTicketExecutionRunForRetry, removeTicketFromBuildSequence, stopBuildSequence, } from './lib/buildSequences.js';
50
58
  import { resumeRemainingBundleBuilds } from './lib/bundleBuildRecovery.js';
@@ -101,13 +109,14 @@ import { buildImportedIssueSessionTitle, importProviderHostedIssueImages } from
101
109
  import { dispatchGitHubWorkflow, getGitHubWorkflowDispatchConfig, getGitHubWorkflowRun, getGitHubWorkflowRunJobs, listGitHubWorkflows, listGitHubWorkflowRuns, } from './lib/integrations/githubActions.js';
102
110
  import { clearAllGitHubActionsSessions, getActiveTrackedRunCount, getTrackedGitHubActionsRuns, startGitHubActionsPolling, trackGitHubActionsRun, } from './lib/integrations/githubActionsSessions.js';
103
111
  import { reconcileStartupExecutions } from './lib/executionReconciliation.js';
112
+ import { deleteArchivedAbandonedProjectEnvironments } from './lib/executionEnvironmentTargetCleanup.js';
104
113
  import { cleanupProjectExecutionEnvironments, cleanupExecutionEnvironmentsForTarget, ExecutionEnvironmentCleanupError, reconcileOrphanedExecutionEnvironments, reconcileExecutionEnvironments, reconcileExecutionEnvironmentLifecycleOperations, reconcileProductionExecutionEnvironmentBindings, } from './lib/executionEnvironmentReconciliation.js';
105
114
  import { createExecutionTargetKey, resolveExecutionTarget, resolveProjectRootExecutionTarget } from './lib/executionTargets.js';
106
115
  import { getExecutionEnvironmentContextForRun } from './lib/executionEnvironmentContexts.js';
107
116
  import { PlanningReferenceError, resolveExternalPlanningDescription } from './lib/integrations/planning.js';
108
117
  import { refreshIfNeeded as refreshAssignedIssues, clearCachedAssignedIssues } from './lib/integrations/assignedIssuesCache.js';
109
118
  import { buildCommitMessagePrompt, normalizeGeneratedCommitMessage } from './lib/commitMessage.js';
110
- import { BUNDLE_PULL_REQUEST_SYNC_TTL_MS, getBundlePullRequestRecord, hasOpenBundlePullRequest, serializeBundlePullRequest, shouldSyncBundlePullRequest, syncTrackedBundlePullRequestIfNeeded, upsertBundlePullRequest, upsertBundlePullRequestFromGitHub, } from './lib/bundlePullRequests.js';
119
+ import { BUNDLE_PULL_REQUEST_SYNC_TTL_MS, getBundlePullRequestRecord, hasOpenBundlePullRequest, hasTerminalBundlePullRequest, serializeBundlePullRequest, shouldSyncBundlePullRequest, syncTrackedBundlePullRequestIfNeeded, upsertBundlePullRequest, upsertBundlePullRequestFromGitHub, } from './lib/bundlePullRequests.js';
111
120
  import { countInboundProjectLinks, getEffectiveProject, getProjectCheckDefinitions, getProjectById, getProjectTestCommands, getProjectTicketStats, hasProjectRunIde, listInterruptedProjectDeletions, listProjects, releaseInterruptedProjectDeletionClaims, replaceProjectLinks, serializeProject, updateProjectActiveTarget, validateProjectRequiredCheckPolicyReferences, validateProjectPayload, } from './lib/projects.js';
112
121
  import { forceReleaseProjectRunCleanup, getActiveRunStatus, getActiveRunStatusForProject, getLastRunStatusForProject, getPreviewStatusForProject, hasRunConfig, isRunning, openIde, openTicketIdeInWorkspace, resolveProjectRunContextKey, runTicketInWorkspace, stopAllTickets, stopTicket } from './lib/run.js';
113
122
  import { deleteProjectServiceRunLogs, getServiceRunLogs } from './lib/serviceRunLogs.js';
@@ -653,52 +662,28 @@ const deleteTicketRecord = async (ticket) => {
653
662
  }
654
663
  const bundledProject = isBundledTicket(ticket) ? getEffectiveProject(ticket) : null;
655
664
  const deletedBundleId = ticket.worktree_bundle_id;
656
- const deletedBundle = bundledProject && deletedBundleId ? getBundle(deletedBundleId, bundledProject.id) : null;
657
- const bundleResourcesWereCleaned = Boolean(bundledProject
658
- && deletedBundle
659
- && !isProjectRootBundle(deletedBundle)
660
- && countBundleTickets(deletedBundle.id, bundledProject.id) === 1);
661
- if (bundleResourcesWereCleaned && bundledProject && deletedBundle) {
662
- try {
663
- await cleanupWorktreeExecutionTarget({
664
- project: bundledProject,
665
- targetKind: 'bundle-worktree',
666
- targetKey: createExecutionTargetKey(bundledProject.id, {
667
- kind: 'bundle-worktree',
668
- bundleId: deletedBundle.id,
669
- branch: deletedBundle.branch,
670
- }),
671
- targetLabel: 'bundle worktree',
672
- worktreeTarget: 'bundle',
673
- branch: deletedBundle.branch,
674
- });
675
- }
676
- catch (error) {
677
- return {
678
- ok: false,
679
- status: error instanceof HttpError ? error.status : 500,
680
- error: error instanceof Error ? error.message : 'Unable to delete the bundle worktree',
681
- };
682
- }
683
- }
665
+ let bundleCleanup;
684
666
  let nextBuildTicketId = null;
685
667
  let nextBuildSequenceId = null;
686
668
  let affectedTicketIds = [];
687
- if (ticket.state === 'build') {
688
- const sequenceRemoval = removeTicketFromBuildSequence(ticket.id);
689
- if (sequenceRemoval.ok) {
690
- nextBuildTicketId = sequenceRemoval.nextTicketId;
691
- nextBuildSequenceId = sequenceRemoval.sequenceId;
692
- affectedTicketIds = sequenceRemoval.affectedTicketIds;
693
- }
694
- }
695
669
  try {
696
670
  withExecutionRunTransaction(transaction => {
697
671
  assertTicketServiceTargetMutable(ticket.id);
672
+ if (ticket.state === 'build') {
673
+ const sequenceRemoval = removeTicketFromBuildSequence(ticket.id);
674
+ if (sequenceRemoval.ok) {
675
+ nextBuildTicketId = sequenceRemoval.nextTicketId;
676
+ nextBuildSequenceId = sequenceRemoval.sequenceId;
677
+ affectedTicketIds = sequenceRemoval.affectedTicketIds;
678
+ }
679
+ }
698
680
  if (ticket.project_id) {
699
681
  deleteTerminalReviewRepairIterationsForTicketInTransaction(transaction, ticket.project_id, ticket.id);
700
682
  }
701
683
  db.prepare('DELETE FROM tickets WHERE id = ?').run(ticket.id);
684
+ if (bundledProject && deletedBundleId) {
685
+ bundleCleanup = cleanupBundleIfEmpty(deletedBundleId, bundledProject);
686
+ }
702
687
  }, io, 'immediate');
703
688
  }
704
689
  catch (error) {
@@ -714,18 +699,7 @@ const deleteTicketRecord = async (ticket) => {
714
699
  catch (error) {
715
700
  console.warn(`[ticket-delete] Unable to remove stage snapshots for ticket ${ticket.id}`, error);
716
701
  }
717
- if (bundledProject && deletedBundleId) {
718
- try {
719
- await cleanupBundleIfEmpty(deletedBundleId, bundledProject, { resourcesAlreadyCleaned: bundleResourcesWereCleaned });
720
- }
721
- catch (error) {
722
- return {
723
- ok: false,
724
- status: error instanceof HttpError ? error.status : 500,
725
- error: error instanceof Error ? error.message : 'Unable to clean up the empty bundle',
726
- };
727
- }
728
- }
702
+ await finishAutomaticBundleDeletion(bundleCleanup);
729
703
  io.emit('ticket:deleted', { id: ticket.id });
730
704
  return { ok: true, nextBuildTicketId, nextBuildSequenceId, affectedTicketIds };
731
705
  };
@@ -976,6 +950,9 @@ const uniqueBundleBranchName = (name, projectId, options = {}) => {
976
950
  let branch = base;
977
951
  let suffix = 2;
978
952
  const conflictsWithBranch = (candidate) => {
953
+ if (isBundleDeletionBranchReserved(projectId, candidate)) {
954
+ return true;
955
+ }
979
956
  const bundleConflict = getBundleByBranch(candidate, projectId);
980
957
  if (bundleConflict && bundleConflict.id !== options.currentBundle?.id) {
981
958
  return true;
@@ -1030,7 +1007,7 @@ const createAutoBundleForName = (name, projectId, baseBranch = null) => {
1030
1007
  }
1031
1008
  return bundle;
1032
1009
  };
1033
- const cleanupBundleIfEmpty = async (bundleId, project, options = {}) => {
1010
+ const cleanupBundleIfEmpty = (bundleId, project) => {
1034
1011
  if (!bundleId) {
1035
1012
  return;
1036
1013
  }
@@ -1041,24 +1018,41 @@ const cleanupBundleIfEmpty = async (bundleId, project, options = {}) => {
1041
1018
  if (isProjectRootBundle(bundle)) {
1042
1019
  return;
1043
1020
  }
1044
- if (hasActiveBundleCheckWork(project.id, bundle.id)) {
1045
- throw new HttpError(409, 'Cannot delete a bundle while check work is active');
1021
+ const assertEligible = () => {
1022
+ if (hasActiveBundleCheckWork(project.id, bundle.id)) {
1023
+ throw new BundleDeletionConflict('Cannot delete a bundle while check work is active');
1024
+ }
1025
+ if (hasActiveBundleSpecWork(project.id, bundle.id)) {
1026
+ throw new BundleDeletionConflict('Cannot delete a bundle while Spec work is active');
1027
+ }
1028
+ if (hasActiveBundleRepositoryGraphScan(project.id, bundle.id)) {
1029
+ throw new BundleDeletionConflict('Cannot delete a bundle while a repository graph scan is active');
1030
+ }
1031
+ };
1032
+ const chatSessionIds = listChatSessions(project.id)
1033
+ .filter(session => session.target_bundle_id === bundle.id).map(session => session.id);
1034
+ const specSessionIds = listSpecSessions(project.id)
1035
+ .filter(session => session.target_bundle_id === bundle.id).map(session => session.id);
1036
+ beginBundleDeletion({ project, bundleId: bundle.id, assertEligible });
1037
+ return { projectId: project.id, bundleId: bundle.id, chatSessionIds, specSessionIds };
1038
+ };
1039
+ const finishAutomaticBundleDeletion = async (cleanup) => {
1040
+ if (!cleanup) {
1041
+ return;
1046
1042
  }
1047
- if (!options.resourcesAlreadyCleaned) {
1048
- await cleanupWorktreeExecutionTarget({
1049
- project,
1050
- targetKind: 'bundle-worktree',
1051
- targetKey: createExecutionTargetKey(project.id, {
1052
- kind: 'bundle-worktree',
1053
- bundleId: bundle.id,
1054
- branch: bundle.branch,
1055
- }),
1056
- targetLabel: 'bundle worktree',
1057
- worktreeTarget: 'bundle',
1058
- branch: bundle.branch,
1059
- });
1043
+ for (const sessionId of cleanup.chatSessionIds) {
1044
+ const session = getProjectChatSession(cleanup.projectId, sessionId);
1045
+ if (session) {
1046
+ io.emit('chat-session:updated', session);
1047
+ }
1060
1048
  }
1061
- deleteBundle(bundle.id, project.id);
1049
+ for (const sessionId of cleanup.specSessionIds) {
1050
+ const session = getProjectSpecSession(cleanup.projectId, sessionId);
1051
+ if (session) {
1052
+ io.emit('spec-session:updated', session);
1053
+ }
1054
+ }
1055
+ await attemptBundleDeletion(cleanup.projectId, cleanup.bundleId);
1062
1056
  };
1063
1057
  const asProjectPayload = (body) => {
1064
1058
  if (!body || typeof body !== 'object' || Array.isArray(body)) {
@@ -1130,6 +1124,22 @@ const matchesTicketProjectContext = (ticket, req) => {
1130
1124
  }
1131
1125
  return ticket.project_id === requestedProjectId;
1132
1126
  };
1127
+ const isTerminalBundleTarget = (projectId, bundleId) => {
1128
+ return Boolean(bundleId
1129
+ && hasTerminalBundlePullRequest(getBundlePullRequestRecord(bundleId, projectId)));
1130
+ };
1131
+ const normalizeTerminalBundleGitStatus = (status, projectId, bundleId) => {
1132
+ if (!isTerminalBundleTarget(projectId, bundleId)) {
1133
+ return status;
1134
+ }
1135
+ return {
1136
+ ...status,
1137
+ ahead: 0,
1138
+ can_push: false,
1139
+ can_force_push_with_lease: false,
1140
+ };
1141
+ };
1142
+ const TERMINAL_BUNDLE_BRANCH_ERROR = 'This bundle pull request is already merged or closed. Start follow-up work on a new branch instead.';
1133
1143
  const getTicketRouteContext = (ticketId) => {
1134
1144
  return getTicketRouteContextStatement.get(ticketId);
1135
1145
  };
@@ -2282,29 +2292,27 @@ const getCurrentReviewWorkspaceSignature = async (ticket) => {
2282
2292
  return (await getGitStatusAsync(workspace.cwd, workspace.branch, workspace.project)).workspace_signature ?? null;
2283
2293
  };
2284
2294
  const getCurrentReviewDiffState = async (ticket) => {
2285
- if (!ticket) {
2295
+ if (!ticket?.project_id) {
2286
2296
  return null;
2287
2297
  }
2288
- const workspace = resolveTicketDiffWorkspaceFromContext(ticket);
2289
- if ('error' in workspace) {
2298
+ const target = { kind: 'ticket', ticket_id: ticket.id };
2299
+ const reviewDiff = await getWorkspaceDiffAsync({
2300
+ projectId: ticket.project_id,
2301
+ target,
2302
+ scope: 'review',
2303
+ });
2304
+ const uncommittedDiff = await getWorkspaceDiffAsync({
2305
+ projectId: ticket.project_id,
2306
+ target,
2307
+ scope: 'uncommitted',
2308
+ });
2309
+ if (!reviewDiff.available || !uncommittedDiff.available) {
2290
2310
  return null;
2291
2311
  }
2292
- const trackedPullRequest = ticket.project_id && ticket.worktree_bundle_id
2293
- ? getBundlePullRequestRecord(ticket.worktree_bundle_id, ticket.project_id)
2294
- : null;
2295
- const [reviewDiff, uncommittedDiff] = await Promise.all([
2296
- getReviewDiffDataAsync({
2297
- cwd: workspace.cwd,
2298
- branch: workspace.branch,
2299
- project: workspace.project,
2300
- pullRequestBaseBranch: trackedPullRequest?.baseBranch ?? null,
2301
- }),
2302
- getGitUnifiedDiffAsync(workspace.cwd),
2303
- ]);
2304
2312
  return {
2305
2313
  diffText: reviewDiff.diff,
2306
- uncommittedDiffSignature: computeDiffSignature(uncommittedDiff),
2307
- changeScope: (reviewDiff.diff === uncommittedDiff ? 'uncommitted' : 'branch_diff'),
2314
+ uncommittedDiffSignature: computeDiffSignature(uncommittedDiff.diff),
2315
+ changeScope: (reviewDiff.diff === uncommittedDiff.diff ? 'uncommitted' : 'branch_diff'),
2308
2316
  };
2309
2317
  };
2310
2318
  const getBundleReviewFindings = async (projectId, bundleId) => {
@@ -2740,6 +2748,8 @@ const emitRunStateForContext = (ticket, isRunningValue) => {
2740
2748
  };
2741
2749
  app.use(express.json({ limit: '5mb' }));
2742
2750
  registerRepositoryGraphRoutes(app, { emitter: io });
2751
+ registerWorkspaceDiffRoutes(app);
2752
+ registerDiffExplanationRoutes(app);
2743
2753
  registerExecutionEnvironmentRoutes(app, {
2744
2754
  emitter: io,
2745
2755
  runner: dockerSandboxRunner,
@@ -3054,16 +3064,15 @@ app.get('/projects/:id/chat-sessions/:sessionId/diff', async (req, res) => {
3054
3064
  if (!chatSession) {
3055
3065
  return res.status(404).json({ error: 'Not found' });
3056
3066
  }
3057
- const workspace = resolveChatSessionTarget(chatSession, project);
3058
- if (!workspace.target_available || !workspace.cwd) {
3059
- return res.status(409).json({ error: workspace.target_unavailable_reason ?? 'Chat target is unavailable.' });
3060
- }
3061
- const diffData = await getGitDiffDataAsync(workspace.cwd);
3062
- return res.json({
3063
- stats: summarizeTicketDiffFiles(diffData.files),
3064
- files: diffData.files,
3065
- diff: diffData.diff,
3067
+ const diff = await getWorkspaceDiffAsync({
3068
+ projectId: project.id,
3069
+ target: { kind: 'chat_session', chat_session_id: chatSession.id },
3070
+ scope: 'uncommitted',
3066
3071
  });
3072
+ if (!diff.available) {
3073
+ return res.status(diff.reason === 'target_not_found' ? 404 : 409).json({ ...diff, error: diff.message });
3074
+ }
3075
+ return res.json({ stats: diff.stats, files: diff.files, diff: diff.diff });
3067
3076
  });
3068
3077
  app.get('/projects/:id/chat-sessions/:sessionId/git-status', async (req, res) => {
3069
3078
  const project = getProjectById(req.params.id);
@@ -3694,15 +3703,17 @@ app.get('/tickets/:id/diff', async (req, res) => {
3694
3703
  }
3695
3704
  const workspace = resolveTicketDiffWorkspaceFromContext(ticket);
3696
3705
  if ('error' in workspace) {
3697
- const { status, error } = workspace;
3698
- return res.status(status).json({ error });
3706
+ return res.status(workspace.status).json({ error: workspace.error });
3699
3707
  }
3700
- const diffData = await getGitDiffDataAsync(workspace.cwd);
3701
- return res.json({
3702
- stats: summarizeTicketDiffFiles(diffData.files),
3703
- files: diffData.files,
3704
- diff: diffData.diff,
3708
+ const diff = await getWorkspaceDiffAsync({
3709
+ projectId: ticket.project_id,
3710
+ target: { kind: 'ticket', ticket_id: ticket.id },
3711
+ scope: 'uncommitted',
3705
3712
  });
3713
+ if (!diff.available) {
3714
+ return res.status(diff.reason === 'target_not_found' ? 404 : 409).json({ ...diff, error: diff.message });
3715
+ }
3716
+ return res.json({ stats: diff.stats, files: diff.files, diff: diff.diff });
3706
3717
  });
3707
3718
  app.get('/tickets/:id/committed-diff', (req, res) => {
3708
3719
  const ticket = getTicketRouteContext(req.params.id);
@@ -3731,7 +3742,8 @@ app.get('/tickets/:id/git-status', async (req, res) => {
3731
3742
  return res.status(workspace.status).json({ error: workspace.error });
3732
3743
  }
3733
3744
  const refreshRemote = req.query.refresh_remote === 'true';
3734
- return res.json(await getGitStatusAsync(workspace.cwd, workspace.branch, workspace.project, { refreshRemote }));
3745
+ const status = await getGitStatusAsync(workspace.cwd, workspace.branch, workspace.project, { refreshRemote });
3746
+ return res.json(normalizeTerminalBundleGitStatus(status, workspace.project.id, ticket.worktree_bundle_id));
3735
3747
  });
3736
3748
  app.get('/tickets/:id/rebase-staged-diff', (req, res) => {
3737
3749
  const ticket = getTicketRouteContext(req.params.id);
@@ -3833,6 +3845,9 @@ app.get('/tickets/:id/git-commits', async (req, res) => {
3833
3845
  if ('error' in workspace) {
3834
3846
  return res.status(workspace.status).json({ error: workspace.error });
3835
3847
  }
3848
+ if (isTerminalBundleTarget(workspace.project.id, ticket.worktree_bundle_id)) {
3849
+ return res.json({ commits: [], total_count: 0 });
3850
+ }
3836
3851
  const summary = await getUnpushedCommitSummaryForCwdAsync(workspace.cwd, workspace.branch, workspace.project);
3837
3852
  return res.json({ commits: summary.commits, total_count: summary.totalCount });
3838
3853
  });
@@ -3896,7 +3911,7 @@ app.get('/bundles/:id/review-findings', async (req, res) => {
3896
3911
  }
3897
3912
  return res.json(response);
3898
3913
  });
3899
- app.get('/bundles/:id/review-diff', (req, res) => {
3914
+ app.get('/bundles/:id/review-diff', async (req, res) => {
3900
3915
  const result = getRequiredProject(req);
3901
3916
  if ('error' in result) {
3902
3917
  return res.status(400).json({ error: result.error });
@@ -3908,21 +3923,24 @@ app.get('/bundles/:id/review-diff', (req, res) => {
3908
3923
  if (isProjectRootBundle(bundle)) {
3909
3924
  return res.status(409).json({ error: 'Bundle diff is not available for the project root bundle.' });
3910
3925
  }
3911
- const worktreePath = resolveWorktreePath(bundle.branch, result.project);
3912
- if (!existsSync(worktreePath)) {
3913
- return res.status(409).json({ error: 'The bundle worktree is not ready yet.' });
3914
- }
3915
- const trackedPullRequest = getBundlePullRequestRecord(bundle.id, result.project.id);
3916
- const reviewDiff = getBundleReviewDiffForCwd({
3917
- cwd: worktreePath,
3918
- branch: bundle.branch,
3919
- project: result.project,
3920
- pullRequestBaseBranch: trackedPullRequest?.baseBranch ?? null,
3926
+ const reviewDiff = await getWorkspaceDiffAsync({
3927
+ projectId: result.project.id,
3928
+ target: { kind: 'bundle', bundle_id: bundle.id },
3929
+ scope: 'review',
3921
3930
  });
3922
- if (!reviewDiff) {
3923
- return res.status(409).json({ error: 'Bundle diff is not available until Archon can resolve a review base for this bundle.' });
3931
+ if (!reviewDiff.available) {
3932
+ return res.status(409).json({ ...reviewDiff, error: reviewDiff.message });
3924
3933
  }
3925
- return res.json(reviewDiff);
3934
+ return res.json({
3935
+ stats: reviewDiff.stats,
3936
+ files: reviewDiff.files,
3937
+ diff: reviewDiff.diff,
3938
+ diff_signature: reviewDiff.diff_signature,
3939
+ head_branch: reviewDiff.workspace.branch,
3940
+ base_branch: reviewDiff.base.branch,
3941
+ base_commit: reviewDiff.base.commit,
3942
+ source: 'worktree_base',
3943
+ });
3926
3944
  });
3927
3945
  app.get('/bundles/:id/pull-request/comments', async (req, res) => {
3928
3946
  const result = getRequiredProject(req);
@@ -4195,6 +4213,9 @@ app.post('/bundles', async (req, res) => {
4195
4213
  });
4196
4214
  }
4197
4215
  if (requestedBranch) {
4216
+ if (isBundleDeletionBranchReserved(result.project.id, branch)) {
4217
+ return res.status(409).json({ error: 'This branch is reserved while a deleted bundle is being cleaned up. Please try again after cleanup finishes.' });
4218
+ }
4198
4219
  if (getBundleByBranch(branch, result.project.id) || hasBundleBranchCaseConflict(branch, result.project.id)) {
4199
4220
  return res.status(409).json({ error: 'Bundle branch already exists in this project' });
4200
4221
  }
@@ -4229,76 +4250,24 @@ app.post('/bundles', async (req, res) => {
4229
4250
  }
4230
4251
  res.status(201).json(serializeBundleRow(result.project.id, bundle, config.tool ?? 'opencode', [], result.project));
4231
4252
  });
4232
- app.delete('/bundles/:id', async (req, res) => {
4233
- const result = getRequiredProject(req);
4234
- if ('error' in result) {
4235
- return res.status(400).json({ error: result.error });
4236
- }
4237
- const bundle = getBundle(req.params.id, result.project.id);
4238
- if (!bundle) {
4239
- return res.status(404).json({ error: 'Bundle not found' });
4240
- }
4241
- if (isProjectRootBundle(bundle)) {
4242
- return res.status(409).json({ error: 'Project root cannot be deleted' });
4243
- }
4244
- if (hasActiveBundleCheckWork(result.project.id, bundle.id)) {
4245
- return res.status(409).json({ error: 'Cannot delete a bundle while check work is active' });
4246
- }
4247
- if (hasActiveBundleSpecWork(result.project.id, bundle.id)) {
4248
- return res.status(409).json({ error: 'Cannot delete a bundle while Spec work is active' });
4249
- }
4250
- if (hasActiveBundleRepositoryGraphScan(result.project.id, bundle.id)) {
4251
- return res.status(409).json({ error: 'Cannot delete a bundle while a repository graph scan is active' });
4252
- }
4253
- if (countBundleTickets(bundle.id, result.project.id) > 0) {
4254
- return res.status(409).json({ error: 'Cannot delete a bundle that is still used by tickets' });
4255
- }
4256
- if (result.project.active_target_kind === 'bundle' && result.project.active_target_bundle_id === bundle.id) {
4257
- return res.status(409).json({ error: 'Cannot delete the currently selected target bundle' });
4258
- }
4259
- const affectedChatSessionIds = listChatSessions(result.project.id)
4260
- .filter(chatSession => chatSession.target_bundle_id === bundle.id)
4261
- .map(chatSession => chatSession.id);
4262
- const affectedSpecSessionIds = listSpecSessions(result.project.id)
4263
- .filter(specSession => specSession.target_bundle_id === bundle.id)
4264
- .map(specSession => specSession.id);
4265
- let cleanupResult;
4266
- try {
4267
- cleanupResult = await cleanupWorktreeExecutionTarget({
4268
- project: result.project,
4269
- targetKind: 'bundle-worktree',
4270
- targetKey: createExecutionTargetKey(result.project.id, {
4271
- kind: 'bundle-worktree',
4272
- bundleId: bundle.id,
4273
- branch: bundle.branch,
4274
- }),
4275
- targetLabel: 'bundle worktree',
4276
- worktreeTarget: 'bundle',
4277
- branch: bundle.branch,
4278
- });
4279
- }
4280
- catch (error) {
4281
- return res.status(error instanceof HttpError ? error.status : 500).json({
4282
- error: error instanceof Error ? error.message : 'Unable to delete the bundle worktree',
4283
- });
4284
- }
4285
- deleteBundle(bundle.id, result.project.id);
4286
- affectedChatSessionIds.forEach(chatSessionId => {
4287
- const updatedChatSession = getProjectChatSession(result.project.id, chatSessionId);
4288
- if (updatedChatSession) {
4289
- io.emit('chat-session:updated', updatedChatSession);
4253
+ registerBundleCleanupRoutes(app);
4254
+ registerBundleDeletionRoutes(app, {
4255
+ getRequiredProject,
4256
+ hasActiveCheckWork: hasActiveBundleCheckWork,
4257
+ hasActiveSpecWork: hasActiveBundleSpecWork,
4258
+ hasActiveGraphScan: hasActiveBundleRepositoryGraphScan,
4259
+ emitChatSessionUpdate: (projectId, sessionId) => {
4260
+ const session = getProjectChatSession(projectId, sessionId);
4261
+ if (session) {
4262
+ io.emit('chat-session:updated', session);
4290
4263
  }
4291
- });
4292
- affectedSpecSessionIds.forEach(specSessionId => {
4293
- const updatedSpecSession = getProjectSpecSession(result.project.id, specSessionId);
4294
- if (updatedSpecSession) {
4295
- io.emit('spec-session:updated', updatedSpecSession);
4264
+ },
4265
+ emitSpecSessionUpdate: (projectId, sessionId) => {
4266
+ const session = getProjectSpecSession(projectId, sessionId);
4267
+ if (session) {
4268
+ io.emit('spec-session:updated', session);
4296
4269
  }
4297
- });
4298
- if (cleanupResult.branchWarning) {
4299
- return res.status(200).json({ warning: cleanupResult.branchWarning });
4300
- }
4301
- res.status(204).end();
4270
+ },
4302
4271
  });
4303
4272
  app.patch('/bundles/:id', async (req, res) => {
4304
4273
  const result = getRequiredProject(req);
@@ -7964,16 +7933,18 @@ app.get('/projects/:id/diff', async (req, res) => {
7964
7933
  if (!project) {
7965
7934
  return res.status(404).json({ error: 'Not found' });
7966
7935
  }
7967
- const workspace = resolveProjectTargetGitWorkspace(project);
7968
- if ('error' in workspace) {
7969
- return res.status(workspace.status).json({ error: workspace.error });
7970
- }
7971
- const diffData = await getGitDiffDataAsync(workspace.cwd);
7972
- return res.json({
7973
- stats: summarizeTicketDiffFiles(diffData.files),
7974
- files: diffData.files,
7975
- diff: diffData.diff,
7936
+ const target = project.active_target_kind === 'bundle' && project.active_target_bundle_id
7937
+ ? { kind: 'bundle', bundle_id: project.active_target_bundle_id }
7938
+ : { kind: 'repo_root' };
7939
+ const diff = await getWorkspaceDiffAsync({
7940
+ projectId: project.id,
7941
+ target,
7942
+ scope: 'uncommitted',
7976
7943
  });
7944
+ if (!diff.available) {
7945
+ return res.status(diff.reason === 'target_not_found' ? 404 : 409).json({ ...diff, error: diff.message });
7946
+ }
7947
+ return res.json({ stats: diff.stats, files: diff.files, diff: diff.diff });
7977
7948
  });
7978
7949
  app.get('/projects/:id/committed-diff', (req, res) => {
7979
7950
  const project = getProjectById(req.params.id);
@@ -7996,7 +7967,8 @@ app.get('/projects/:id/git-status', async (req, res) => {
7996
7967
  return res.status(workspace.status).json({ error: workspace.error });
7997
7968
  }
7998
7969
  const refreshRemote = req.query.refresh_remote === 'true';
7999
- return res.json(await getGitStatusAsync(workspace.cwd, workspace.branch, workspace.project, { refreshRemote }));
7970
+ const status = await getGitStatusAsync(workspace.cwd, workspace.branch, workspace.project, { refreshRemote });
7971
+ return res.json(normalizeTerminalBundleGitStatus(status, workspace.project.id, workspace.target_bundle_id));
8000
7972
  });
8001
7973
  app.get('/projects/:id/git-commits', async (req, res) => {
8002
7974
  const project = getProjectById(req.params.id);
@@ -8007,6 +7979,9 @@ app.get('/projects/:id/git-commits', async (req, res) => {
8007
7979
  if ('error' in workspace) {
8008
7980
  return res.status(workspace.status).json({ error: workspace.error });
8009
7981
  }
7982
+ if (isTerminalBundleTarget(workspace.project.id, workspace.target_bundle_id)) {
7983
+ return res.json({ commits: [], total_count: 0 });
7984
+ }
8010
7985
  const summary = await getUnpushedCommitSummaryForCwdAsync(workspace.cwd, workspace.branch, workspace.project);
8011
7986
  return res.json({ commits: summary.commits, total_count: summary.totalCount });
8012
7987
  });
@@ -8037,7 +8012,7 @@ app.post('/projects/:id/git-commit', async (req, res) => {
8037
8012
  }
8038
8013
  return res.json({
8039
8014
  ticket: null,
8040
- git_status: getGitStatus(workspace.cwd, workspace.branch, workspace.project),
8015
+ git_status: normalizeTerminalBundleGitStatus(getGitStatus(workspace.cwd, workspace.branch, workspace.project), workspace.project.id, workspace.target_bundle_id),
8041
8016
  });
8042
8017
  });
8043
8018
  app.post('/projects/:id/git-commit-message', async (req, res) => {
@@ -8207,6 +8182,9 @@ app.post('/projects/:id/git-push', async (req, res) => {
8207
8182
  if ('error' in workspace) {
8208
8183
  return res.status(workspace.status).json({ error: workspace.error });
8209
8184
  }
8185
+ if (isTerminalBundleTarget(workspace.project.id, workspace.target_bundle_id)) {
8186
+ return res.status(409).json({ error: TERMINAL_BUNDLE_BRANCH_ERROR });
8187
+ }
8210
8188
  const forceWithLease = req.body?.force_with_lease === true;
8211
8189
  if (forceWithLease) {
8212
8190
  try {
@@ -8248,6 +8226,9 @@ app.post('/projects/:id/git-undo-latest', async (req, res) => {
8248
8226
  if ('error' in workspace) {
8249
8227
  return res.status(workspace.status).json({ error: workspace.error });
8250
8228
  }
8229
+ if (isTerminalBundleTarget(workspace.project.id, workspace.target_bundle_id)) {
8230
+ return res.status(409).json({ error: TERMINAL_BUNDLE_BRANCH_ERROR });
8231
+ }
8251
8232
  try {
8252
8233
  await undoLatestUnpushedCommitForCwd(workspace.cwd, workspace.branch, workspace.project);
8253
8234
  }
@@ -8271,6 +8252,9 @@ app.post('/projects/:id/git-squash', async (req, res) => {
8271
8252
  if ('error' in workspace) {
8272
8253
  return res.status(workspace.status).json({ error: workspace.error });
8273
8254
  }
8255
+ if (isTerminalBundleTarget(workspace.project.id, workspace.target_bundle_id)) {
8256
+ return res.status(409).json({ error: TERMINAL_BUNDLE_BRANCH_ERROR });
8257
+ }
8274
8258
  const message = typeof req.body?.message === 'string' ? req.body.message.trim() : '';
8275
8259
  if (!message) {
8276
8260
  return res.status(400).json({ error: 'message is required' });
@@ -8489,6 +8473,9 @@ const assertProjectDeletionEligible = (projectId, claimId) => {
8489
8473
  if (projectCount <= 1) {
8490
8474
  throw new ProjectDeletionError(409, 'Cannot delete the last remaining project');
8491
8475
  }
8476
+ if (db.prepare("SELECT 1 FROM bundle_deletion_jobs WHERE project_id = ? AND status IN ('pending', 'blocked') LIMIT 1").get(projectId)) {
8477
+ throw new ProjectDeletionError(409, 'Cannot delete a project while bundle workspace cleanup is pending');
8478
+ }
8492
8479
  if (countInboundProjectLinks(projectId) > 0) {
8493
8480
  throw new ProjectDeletionError(409, 'Cannot delete a project that is linked from another project');
8494
8481
  }
@@ -8591,7 +8578,9 @@ const completeClaimedProjectDeletion = async (projectId, claimId) => {
8591
8578
  SET parent_run_id = NULL, retry_of_run_id = NULL
8592
8579
  WHERE project_id = ?
8593
8580
  `).run(projectId);
8581
+ deleteArchivedAbandonedProjectEnvironments(projectId);
8594
8582
  db.prepare("DELETE FROM execution_environments WHERE project_id = ? AND lifecycle_state = 'retired'").run(projectId);
8583
+ db.prepare("DELETE FROM bundle_deletion_jobs WHERE project_id = ? AND status = 'completed'").run(projectId);
8595
8584
  const deleted = db.prepare('DELETE FROM projects WHERE id = ? AND deletion_claim_id = ?').run(projectId, claimId);
8596
8585
  if (deleted.changes !== 1) {
8597
8586
  throw new ProjectDeletionError(409, 'Project deletion ownership was lost');
@@ -8857,34 +8846,7 @@ app.patch('/tickets/:id', async (req, res) => {
8857
8846
  }
8858
8847
  }
8859
8848
  const bundleChanged = previousBundleId !== resolvedBundleId || ticket.project_id !== nextProjectId;
8860
- let previousBundleResourcesCleaned = false;
8861
- if (bundleChanged && previousBundleId && previousProject) {
8862
- const previousBundle = getBundle(previousBundleId, previousProject.id);
8863
- if (previousBundle
8864
- && !isProjectRootBundle(previousBundle)
8865
- && countBundleTickets(previousBundle.id, previousProject.id) === 1) {
8866
- try {
8867
- await cleanupWorktreeExecutionTarget({
8868
- project: previousProject,
8869
- targetKind: 'bundle-worktree',
8870
- targetKey: createExecutionTargetKey(previousProject.id, {
8871
- kind: 'bundle-worktree',
8872
- bundleId: previousBundle.id,
8873
- branch: previousBundle.branch,
8874
- }),
8875
- targetLabel: 'bundle worktree',
8876
- worktreeTarget: 'bundle',
8877
- branch: previousBundle.branch,
8878
- });
8879
- previousBundleResourcesCleaned = true;
8880
- }
8881
- catch (error) {
8882
- return res.status(error instanceof HttpError ? error.status : 500).json({
8883
- error: error instanceof Error ? error.message : 'Unable to clean up the empty bundle',
8884
- });
8885
- }
8886
- }
8887
- }
8849
+ let bundleCleanup;
8888
8850
  try {
8889
8851
  db.transaction(() => {
8890
8852
  if (bundleChanged) {
@@ -8894,28 +8856,20 @@ app.patch('/tickets/:id', async (req, res) => {
8894
8856
  deleteTicketDependenciesForTicket(ticket.id);
8895
8857
  }
8896
8858
  db.prepare('UPDATE tickets SET title = ?, description = ?, model = ?, variant = ?, skills_json = ?, worktree_bundle_id = ?, project_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(title?.trim() ?? ticket.title, description ?? ticket.description, resolvedModel, resolvedVariant, JSON.stringify(resolvedSkills), resolvedBundleId, nextProjectId, req.params.id);
8859
+ if (bundleChanged && previousBundleId && previousProject) {
8860
+ bundleCleanup = cleanupBundleIfEmpty(previousBundleId, previousProject);
8861
+ }
8897
8862
  }).immediate();
8898
8863
  }
8899
8864
  catch (error) {
8900
- if (error instanceof HttpError) {
8901
- return res.status(error.status).json({ error: error.message });
8865
+ if (error instanceof HttpError || error instanceof BundleDeletionConflict) {
8866
+ return res.status(error instanceof HttpError ? error.status : 409).json({ error: error.message });
8902
8867
  }
8903
8868
  throw error;
8904
8869
  }
8905
8870
  upsertTicketDescriptionMessage(req.params.id, description ?? ticket.description);
8906
8871
  const updated = getTicket(req.params.id);
8907
- if (bundleChanged && previousBundleId && previousProject) {
8908
- try {
8909
- await cleanupBundleIfEmpty(previousBundleId, previousProject, {
8910
- resourcesAlreadyCleaned: previousBundleResourcesCleaned,
8911
- });
8912
- }
8913
- catch (error) {
8914
- return res.status(error instanceof HttpError ? error.status : 500).json({
8915
- error: error instanceof Error ? error.message : 'Unable to clean up the empty bundle',
8916
- });
8917
- }
8918
- }
8872
+ await finishAutomaticBundleDeletion(bundleCleanup);
8919
8873
  io.emit('ticket:updated', updated);
8920
8874
  res.json(updated);
8921
8875
  });
@@ -9399,7 +9353,7 @@ app.post('/tickets/:id/git-commit', async (req, res) => {
9399
9353
  const updated = reconcileRunContextTickets(previousTickets, ticket.id);
9400
9354
  return res.json({
9401
9355
  ticket: updated,
9402
- git_status: getGitStatus(workspace.cwd, workspace.branch, workspace.project),
9356
+ git_status: normalizeTerminalBundleGitStatus(getGitStatus(workspace.cwd, workspace.branch, workspace.project), workspace.project.id, ticket.worktree_bundle_id),
9403
9357
  });
9404
9358
  });
9405
9359
  app.post('/tickets/:id/git-commit-message', async (req, res) => {
@@ -9475,6 +9429,9 @@ app.post('/tickets/:id/git-push', async (req, res) => {
9475
9429
  if ('error' in workspace) {
9476
9430
  return res.status(workspace.status).json({ error: workspace.error });
9477
9431
  }
9432
+ if (isTerminalBundleTarget(workspace.project.id, ticket.worktree_bundle_id)) {
9433
+ return res.status(409).json({ error: TERMINAL_BUNDLE_BRANCH_ERROR });
9434
+ }
9478
9435
  const previousTickets = listTicketsInRunContext(ticket);
9479
9436
  const forceWithLease = req.body?.force_with_lease === true;
9480
9437
  if (forceWithLease) {
@@ -9525,6 +9482,9 @@ app.post('/tickets/:id/git-undo-latest', async (req, res) => {
9525
9482
  if ('error' in workspace) {
9526
9483
  return res.status(workspace.status).json({ error: workspace.error });
9527
9484
  }
9485
+ if (isTerminalBundleTarget(workspace.project.id, ticket.worktree_bundle_id)) {
9486
+ return res.status(409).json({ error: TERMINAL_BUNDLE_BRANCH_ERROR });
9487
+ }
9528
9488
  const previousTickets = listTicketsInRunContext(ticket);
9529
9489
  try {
9530
9490
  await undoLatestUnpushedCommitForCwd(workspace.cwd, workspace.branch, workspace.project);
@@ -9553,6 +9513,9 @@ app.post('/tickets/:id/git-squash', async (req, res) => {
9553
9513
  if ('error' in workspace) {
9554
9514
  return res.status(workspace.status).json({ error: workspace.error });
9555
9515
  }
9516
+ if (isTerminalBundleTarget(workspace.project.id, ticket.worktree_bundle_id)) {
9517
+ return res.status(409).json({ error: TERMINAL_BUNDLE_BRANCH_ERROR });
9518
+ }
9556
9519
  const message = typeof req.body?.message === 'string' ? req.body.message.trim() : '';
9557
9520
  if (!message) {
9558
9521
  return res.status(400).json({ error: 'message is required' });
@@ -10575,6 +10538,7 @@ app.use((error, _req, res, next) => {
10575
10538
  io.on('connection', (socket) => {
10576
10539
  console.log('client connected:', socket.id);
10577
10540
  registerTerminalSocketHandlers(io, socket);
10541
+ registerDiffExplanationSocketHandlers(socket);
10578
10542
  });
10579
10543
  const DEFAULT_PORT = 3001;
10580
10544
  const APPROVAL_RECONCILIATION_INTERVAL_MS = 30_000;
@@ -10629,6 +10593,7 @@ export const runExecutionArtifactCleanup = (mode) => {
10629
10593
  if (mode === 'periodic') {
10630
10594
  await resumeInterruptedProjectDeletions();
10631
10595
  }
10596
+ await resumeBundleDeletions();
10632
10597
  const orphanedEnvironments = await reconcileOrphanedExecutionEnvironments();
10633
10598
  if (orphanedEnvironments.scanned > 0) {
10634
10599
  console.log(`Reconciled ${orphanedEnvironments.cleaned} orphaned execution environment(s); ${orphanedEnvironments.failed} cleanup(s) retained for retry`);
@@ -10707,6 +10672,7 @@ const runExecutionEnvironmentLifecycleReconciliation = () => {
10707
10672
  isProjectOwnedInMemory: projectId => getActiveRunStatusForProject(projectId) !== null,
10708
10673
  })
10709
10674
  .then(() => reconcileExecutionEnvironmentLifecycleOperations())
10675
+ .then(() => resumeBundleDeletions())
10710
10676
  .then(() => undefined)
10711
10677
  .catch(error => {
10712
10678
  console.error('Failed to reconcile execution environment lifecycle operations', error);
@@ -10746,6 +10712,8 @@ export const startServer = ({ port = DEFAULT_PORT, host } = {}) => {
10746
10712
  return Promise.reject(new Error('Server startup was cancelled by shutdown'));
10747
10713
  }
10748
10714
  if (httpServer.listening) {
10715
+ openLightweightPromptAdmissions();
10716
+ openDiffExplanationJobAdmissions();
10749
10717
  return Promise.resolve(resolveStartResult(host));
10750
10718
  }
10751
10719
  if (startServerPromise) {
@@ -10781,11 +10749,15 @@ export const startServer = ({ port = DEFAULT_PORT, host } = {}) => {
10781
10749
  };
10782
10750
  httpServer.once('error', handleError);
10783
10751
  const { bindHost } = resolveDesktopServerHost(host);
10752
+ openLightweightPromptAdmissions();
10753
+ openDiffExplanationJobAdmissions();
10784
10754
  openCheckRunnerAdmissions();
10785
10755
  httpServer.listen(port, bindHost, handleListening);
10786
10756
  });
10787
10757
  }
10788
10758
  catch (error) {
10759
+ await shutdownDiffExplanationJobs();
10760
+ await shutdownLightweightPrompts();
10789
10761
  await clearAllGitHubActionsSessions();
10790
10762
  throw error;
10791
10763
  }
@@ -10812,9 +10784,11 @@ export const shutdownServer = async (signal) => {
10812
10784
  void httpServerClosePromise?.catch(() => undefined);
10813
10785
  const pendingStartup = startServerPromise;
10814
10786
  shutdownServerPromise = (async () => {
10787
+ await shutdownDiffExplanationJobs();
10815
10788
  await Promise.all([
10816
10789
  pendingStartup?.catch(() => undefined),
10817
10790
  stopAllPlanAgents(),
10791
+ shutdownLightweightPrompts(),
10818
10792
  ]);
10819
10793
  if (executionEvidenceCleanupTimer) {
10820
10794
  clearInterval(executionEvidenceCleanupTimer);