@straiffi/archon 1.6.1 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/dist/client/assets/GitHubActionsDialog-B9_WJBOp.js +1 -0
  2. package/dist/client/assets/TestsDialog-DKvbjH_M.js +4 -0
  3. package/dist/client/assets/TicketChangesModal-C8rFoKjY.js +1 -0
  4. package/dist/client/assets/{arc-CnuHRbeq.js → arc-D2Nn9xIj.js} +1 -1
  5. package/dist/client/assets/architecture-7EHR7CIX-C-4Cp4ml.js +1 -0
  6. package/dist/client/assets/{architectureDiagram-3BPJPVTR-cWqD9YRv.js → architectureDiagram-3BPJPVTR-DXRSRLL9.js} +1 -1
  7. package/dist/client/assets/{blockDiagram-GPEHLZMM-B8yyMr1c.js → blockDiagram-GPEHLZMM-D7EE6Dnv.js} +1 -1
  8. package/dist/client/assets/{c4Diagram-AAUBKEIU-B_yL8Tbe.js → c4Diagram-AAUBKEIU-UhVzFafr.js} +1 -1
  9. package/dist/client/assets/channel-CNoEXkwa.js +1 -0
  10. package/dist/client/assets/{chunk-2J33WTMH-BF8hfYEU.js → chunk-2J33WTMH-obbfylVr.js} +1 -1
  11. package/dist/client/assets/{chunk-3OPIFGDE-BPDdyRDK.js → chunk-3OPIFGDE-DPVuS-uo.js} +1 -1
  12. package/dist/client/assets/{chunk-4BX2VUAB-_WBpCe0m.js → chunk-4BX2VUAB-CtLS8D71.js} +1 -1
  13. package/dist/client/assets/{chunk-4EGX6M5U-BBQHxMeA.js → chunk-4EGX6M5U-Btnm0QNe.js} +1 -1
  14. package/dist/client/assets/{chunk-55IACEB6-B-d-YzxI.js → chunk-55IACEB6-74CcJaub.js} +1 -1
  15. package/dist/client/assets/{chunk-5DO6E6H7-BA-oSVa-.js → chunk-5DO6E6H7-WLZ8L4ko.js} +1 -1
  16. package/dist/client/assets/{chunk-5ZQYHXKU-DEi4bnIa.js → chunk-5ZQYHXKU-Dsc03vy9.js} +1 -1
  17. package/dist/client/assets/{chunk-727SXJPM-BTXHOTJi.js → chunk-727SXJPM-COE-iq_Z.js} +1 -1
  18. package/dist/client/assets/{chunk-AQP2D5EJ-D2TAJwxM.js → chunk-AQP2D5EJ-DnbAdr8m.js} +1 -1
  19. package/dist/client/assets/{chunk-BR22UD5L-prWX503j.js → chunk-BR22UD5L-Cou5beGe.js} +1 -1
  20. package/dist/client/assets/{chunk-BSJP7CBP-CNtf2uaW.js → chunk-BSJP7CBP-DJ57XcV5.js} +1 -1
  21. package/dist/client/assets/{chunk-CSCIHK7Q-Bdgg7p9T.js → chunk-CSCIHK7Q-DbTzC_WS.js} +2 -2
  22. package/dist/client/assets/{chunk-FHYWG6QK-BQ8n0J0S.js → chunk-FHYWG6QK-Ba10e2kj.js} +1 -1
  23. package/dist/client/assets/{chunk-FMBD7UC4-CWQN4V4s.js → chunk-FMBD7UC4-CEo2wZMx.js} +1 -1
  24. package/dist/client/assets/{chunk-KSCS5N6A-Cey3fOI1.js → chunk-KSCS5N6A-xvviIayr.js} +1 -1
  25. package/dist/client/assets/{chunk-L5ZTLDWV-BSNGBgBH.js → chunk-L5ZTLDWV-DY4Q9Til.js} +1 -1
  26. package/dist/client/assets/{chunk-LZXEDZCA-B8qP34jM.js → chunk-LZXEDZCA-Bbeq2DZs.js} +2 -2
  27. package/dist/client/assets/{chunk-MPE355IW-gHN8AuU_.js → chunk-MPE355IW-MqEt-J8G.js} +1 -1
  28. package/dist/client/assets/{chunk-MZUSXYTE-Clzaz1kK.js → chunk-MZUSXYTE-DdNi9lhx.js} +1 -1
  29. package/dist/client/assets/{chunk-N66VUXT2-CkkAOkdt.js → chunk-N66VUXT2-CjemTW4E.js} +1 -1
  30. package/dist/client/assets/{chunk-ND2GUHAM-COde7fTi.js → chunk-ND2GUHAM-CylX1p3K.js} +1 -1
  31. package/dist/client/assets/{chunk-NZK2D7GU-C62VVWrt.js → chunk-NZK2D7GU-BTcALpQq.js} +1 -1
  32. package/dist/client/assets/{chunk-O5CBEL6O-_20MzuKm.js → chunk-O5CBEL6O-Be_5dk2_.js} +1 -1
  33. package/dist/client/assets/{chunk-PUPMXCY4-DAdQFv8j.js → chunk-PUPMXCY4-ZvwTw0To.js} +1 -1
  34. package/dist/client/assets/chunk-QZHKN3VN-LI_RZca2.js +1 -0
  35. package/dist/client/assets/{chunk-UIBZB4QT-BRR84OYN.js → chunk-UIBZB4QT-BTY9be9s.js} +1 -1
  36. package/dist/client/assets/{chunk-WCWK7LTN-CQU7Fs_W.js → chunk-WCWK7LTN-Db1n3n3-.js} +1 -1
  37. package/dist/client/assets/chunk-WU5MYG2G-DXPsSM4Q.js +1 -0
  38. package/dist/client/assets/{chunk-XPW4576I-DafcxlUi.js → chunk-XPW4576I-DObFPxjB.js} +1 -1
  39. package/dist/client/assets/classDiagram-4FO5ZUOK-BgmK8-LI.js +1 -0
  40. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-Dj5LqBjo.js +1 -0
  41. package/dist/client/assets/{cose-bilkent-S5V4N54A-DhJHP9hr.js → cose-bilkent-S5V4N54A-MlOPiG6P.js} +1 -1
  42. package/dist/client/assets/{dagre-BM42HDAG-jLUWHvr3.js → dagre-BM42HDAG-CABUW7xK.js} +1 -1
  43. package/dist/client/assets/{dagre-CdwzCXQr.js → dagre-BWNHuDdf.js} +1 -1
  44. package/dist/client/assets/{diagram-2AECGRRQ-B6D2TKLb.js → diagram-2AECGRRQ-KvcNrpvf.js} +1 -1
  45. package/dist/client/assets/{diagram-5GNKFQAL-bFJLLDWx.js → diagram-5GNKFQAL-DQV6-I4e.js} +1 -1
  46. package/dist/client/assets/{diagram-KO2AKTUF-Bp01SL5f.js → diagram-KO2AKTUF-DYDk9roD.js} +1 -1
  47. package/dist/client/assets/{diagram-LMA3HP47-DwoeZFg9.js → diagram-LMA3HP47-Da-0u-HG.js} +1 -1
  48. package/dist/client/assets/{diagram-OG6HWLK6-CjRd_tge.js → diagram-OG6HWLK6-CuD5eRBv.js} +1 -1
  49. package/dist/client/assets/{erDiagram-TEJ5UH35-DzfIVd9j.js → erDiagram-TEJ5UH35-BL5dBE-b.js} +1 -1
  50. package/dist/client/assets/eventmodeling-FCH6USID-Dqb6QKoI.js +1 -0
  51. package/dist/client/assets/{flowDiagram-I6XJVG4X-Dq04zf9A.js → flowDiagram-I6XJVG4X-Bke5cWs7.js} +1 -1
  52. package/dist/client/assets/{ganttDiagram-6RSMTGT7-D5f16YYP.js → ganttDiagram-6RSMTGT7-5OD-ayGn.js} +1 -1
  53. package/dist/client/assets/gitGraph-WXDBUCRP-DYEsqh93.js +1 -0
  54. package/dist/client/assets/{gitGraphDiagram-PVQCEYII-CCD1CXBG.js → gitGraphDiagram-PVQCEYII-7Cf6FVfO.js} +1 -1
  55. package/dist/client/assets/index-D8EniXuq.css +2 -0
  56. package/dist/client/assets/index-DXuxAAzf.js +270 -0
  57. package/dist/client/assets/info-J43DQDTF-CfjXFZai.js +1 -0
  58. package/dist/client/assets/{infoDiagram-5YYISTIA-Bde4Z0o8.js → infoDiagram-5YYISTIA-BchMN9V-.js} +1 -1
  59. package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-CJ89ymGL.js → ishikawaDiagram-YF4QCWOH-D6TchjQy.js} +1 -1
  60. package/dist/client/assets/{journeyDiagram-JHISSGLW-Dw_sScmo.js → journeyDiagram-JHISSGLW-CwEcoeX-.js} +1 -1
  61. package/dist/client/assets/{kanban-definition-UN3LZRKU-DfWOcnxV.js → kanban-definition-UN3LZRKU-S2V9CLGR.js} +1 -1
  62. package/dist/client/assets/{line-CUQIbS7r.js → line-DRheChzQ.js} +1 -1
  63. package/dist/client/assets/{linear-DFIW-KnM.js → linear-DT4rSZvy.js} +1 -1
  64. package/dist/client/assets/{mermaid-parser.core-d287AjoM.js → mermaid-parser.core-CN13XjLe.js} +2 -2
  65. package/dist/client/assets/{mermaid.core-QqY2ZNMl.js → mermaid.core-JhdSpoF5.js} +3 -3
  66. package/dist/client/assets/{mindmap-definition-RKZ34NQL-Bz3Yc70v.js → mindmap-definition-RKZ34NQL-657TKwRB.js} +1 -1
  67. package/dist/client/assets/{ordinal-C1FS1Sfe.js → ordinal-k--hYEme.js} +1 -1
  68. package/dist/client/assets/packet-YPE3B663-Dv0HkRHU.js +1 -0
  69. package/dist/client/assets/pie-LRSECV5Y-S5zPzZFN.js +1 -0
  70. package/dist/client/assets/{pieDiagram-4H26LBE5-BjMDHR6y.js → pieDiagram-4H26LBE5-DJ8eQ94X.js} +1 -1
  71. package/dist/client/assets/{quadrantDiagram-W4KKPZXB-BWDqf0X1.js → quadrantDiagram-W4KKPZXB-GpYFqyb1.js} +1 -1
  72. package/dist/client/assets/radar-GUYGQ44K-B7psLv22.js +1 -0
  73. package/dist/client/assets/{requirementDiagram-4Y6WPE33-kspE4S60.js → requirementDiagram-4Y6WPE33-COZFWgq1.js} +1 -1
  74. package/dist/client/assets/{sankeyDiagram-5OEKKPKP-EuLhHbDR.js → sankeyDiagram-5OEKKPKP-BdCHyfcQ.js} +1 -1
  75. package/dist/client/assets/{sequenceDiagram-3UESZ5HK-BvXG3DJh.js → sequenceDiagram-3UESZ5HK-DAh3Ogx0.js} +1 -1
  76. package/dist/client/assets/{stateDiagram-AJRCARHV-B-D0PYkH.js → stateDiagram-AJRCARHV-Ck8Yie8_.js} +1 -1
  77. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-B6zibRqD.js +1 -0
  78. package/dist/client/assets/{timeline-definition-PNZ67QCA-CJnod6Jh.js → timeline-definition-PNZ67QCA-kYr9DNtP.js} +1 -1
  79. package/dist/client/assets/treeView-BLDUP644-B8H0kT67.js +1 -0
  80. package/dist/client/assets/treemap-LRROVOQU-DPLhFHnh.js +1 -0
  81. package/dist/client/assets/{vennDiagram-CIIHVFJN-CBh3tqZu.js → vennDiagram-CIIHVFJN-CC_SlRiC.js} +1 -1
  82. package/dist/client/assets/wardley-L42UT6IY-B5-Vb08V.js +1 -0
  83. package/dist/client/assets/{wardleyDiagram-YWT4CUSO-BBQOjYBd.js → wardleyDiagram-YWT4CUSO-CZ17ccSN.js} +1 -1
  84. package/dist/client/assets/{xychartDiagram-2RQKCTM6-C8gU1LNj.js → xychartDiagram-2RQKCTM6-Blqaj-ci.js} +1 -1
  85. package/dist/client/index.html +2 -2
  86. package/dist/server/db.js +133 -0
  87. package/dist/server/db.js.map +1 -1
  88. package/dist/server/index.js +299 -17
  89. package/dist/server/index.js.map +1 -1
  90. package/dist/server/lib/agent.js +12 -11
  91. package/dist/server/lib/agent.js.map +1 -1
  92. package/dist/server/lib/buildOpenQuestions.js +263 -0
  93. package/dist/server/lib/buildOpenQuestions.js.map +1 -0
  94. package/dist/server/lib/bundleReviewOperations.js +17 -3
  95. package/dist/server/lib/bundleReviewOperations.js.map +1 -1
  96. package/dist/server/lib/git.js +19 -3
  97. package/dist/server/lib/git.js.map +1 -1
  98. package/dist/server/lib/lightweightPrompt.js +2 -1
  99. package/dist/server/lib/lightweightPrompt.js.map +1 -1
  100. package/dist/server/lib/planModeQuestions.js +23 -1
  101. package/dist/server/lib/planModeQuestions.js.map +1 -1
  102. package/dist/server/lib/previewProxy.js +5 -0
  103. package/dist/server/lib/previewProxy.js.map +1 -1
  104. package/dist/server/lib/projectAutoConfig.js +2 -1
  105. package/dist/server/lib/projectAutoConfig.js.map +1 -1
  106. package/dist/server/lib/projectFiles.js +313 -0
  107. package/dist/server/lib/projectFiles.js.map +1 -0
  108. package/dist/server/lib/projects.js +10 -0
  109. package/dist/server/lib/projects.js.map +1 -1
  110. package/dist/server/lib/ticketAutomation.js +16 -4
  111. package/dist/server/lib/ticketAutomation.js.map +1 -1
  112. package/dist/server/lib/ticketAutomationCommit.js +10 -10
  113. package/dist/server/lib/ticketAutomationCommit.js.map +1 -1
  114. package/dist/server/lib/ticketOpenQuestions.js +17 -0
  115. package/dist/server/lib/ticketOpenQuestions.js.map +1 -1
  116. package/dist/server/lib/ticketWorkflowOperations.js +1 -1
  117. package/dist/server/lib/ticketWorkflowOperations.js.map +1 -1
  118. package/dist/server/lib/tickets.js +48 -6
  119. package/dist/server/lib/tickets.js.map +1 -1
  120. package/dist/server/workers/build.js +55 -12
  121. package/dist/server/workers/build.js.map +1 -1
  122. package/dist/server/workers/chat.js +2 -5
  123. package/dist/server/workers/chat.js.map +1 -1
  124. package/dist/server/workers/followUp.js +120 -18
  125. package/dist/server/workers/followUp.js.map +1 -1
  126. package/dist/server/workers/plan.js +4 -7
  127. package/dist/server/workers/plan.js.map +1 -1
  128. package/dist/server/workers/review.js +25 -8
  129. package/dist/server/workers/review.js.map +1 -1
  130. package/dist/server/workers/specSession.js +2 -5
  131. package/dist/server/workers/specSession.js.map +1 -1
  132. package/package.json +1 -1
  133. package/dist/client/assets/GitHubActionsDialog-BLJ3yCeK.js +0 -1
  134. package/dist/client/assets/TestsDialog-ar_RUzA3.js +0 -4
  135. package/dist/client/assets/architecture-7EHR7CIX-tSrokdFP.js +0 -1
  136. package/dist/client/assets/channel-1Ezn2R-x.js +0 -1
  137. package/dist/client/assets/chunk-QZHKN3VN-biOr9EeQ.js +0 -1
  138. package/dist/client/assets/chunk-WU5MYG2G-C4G-J1jx.js +0 -1
  139. package/dist/client/assets/classDiagram-4FO5ZUOK-DrjiwEWN.js +0 -1
  140. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-DWHyuysv.js +0 -1
  141. package/dist/client/assets/eventmodeling-FCH6USID-DapHLQIh.js +0 -1
  142. package/dist/client/assets/gitGraph-WXDBUCRP-BTBN_hfb.js +0 -1
  143. package/dist/client/assets/index-DI9Vi2-3.js +0 -265
  144. package/dist/client/assets/index-DSap5tSX.css +0 -2
  145. package/dist/client/assets/info-J43DQDTF-CgiMJYKF.js +0 -1
  146. package/dist/client/assets/packet-YPE3B663-Bnwqjena.js +0 -1
  147. package/dist/client/assets/pie-LRSECV5Y-Ctj0ef-4.js +0 -1
  148. package/dist/client/assets/radar-GUYGQ44K-DE0H_saz.js +0 -1
  149. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-f36jgr8A.js +0 -1
  150. package/dist/client/assets/treeView-BLDUP644-CSAZL2OH.js +0 -1
  151. package/dist/client/assets/treemap-LRROVOQU-tYm4XKfP.js +0 -1
  152. package/dist/client/assets/wardley-L42UT6IY-DdiEFppm.js +0 -1
  153. /package/dist/client/assets/{array-B57E4cSs.js → array-BNor45A1.js} +0 -0
  154. /package/dist/client/assets/{chunk-NNHCCRGN-DRSAMe5H.js → chunk-NNHCCRGN-CZB-V-Gv.js} +0 -0
  155. /package/dist/client/assets/{cytoscape.esm-CVYSm6AJ.js → cytoscape.esm-B2lJcMSI.js} +0 -0
  156. /package/dist/client/assets/{defaultLocale-6WHJ3B09.js → defaultLocale-DPzUsThw.js} +0 -0
  157. /package/dist/client/assets/{dist-By2zKKTF.js → dist-CERhmQhR.js} +0 -0
  158. /package/dist/client/assets/{graphlib-B0DVs0bw.js → graphlib-BHOW-eWz.js} +0 -0
  159. /package/dist/client/assets/{init-DiYCGS1R.js → init-C0L3woqb.js} +0 -0
  160. /package/dist/client/assets/{katex-bgJu_0Fy.js → katex-CpjQsUQe.js} +0 -0
  161. /package/dist/client/assets/{path-DL_QDy2X.js → path-sMK4d_s9.js} +0 -0
  162. /package/dist/client/assets/{rough.esm-Bbn_-PMU.js → rough.esm-BoTisKeL.js} +0 -0
  163. /package/dist/client/assets/{src-Bi_byJUe.js → src-BRjoYtUW.js} +0 -0
@@ -18,7 +18,7 @@ import { resolveChatSessionTarget } from './lib/chatTargets.js';
18
18
  import { createChatMessage, getChatMessage, updateChatMessageContent } from './lib/chatMessages.js';
19
19
  import { attachChatTicketProposalsToMessageContent } from './lib/chatTicketProposals.js';
20
20
  import { createCorsOriginResolver, parseCorsOrigins } from './lib/cors.js';
21
- import { abortGitOperation, branchExistsLocally, commitGitChanges, createWorktreeAsync, deleteWorktree, getBundleReviewDiffForCwd, getGitDiffData, getGitDiffFiles, getGitStatus, getGitUnifiedDiff, getProjectBranch, getProjectBranchSuggestions, getSelectedGitUnifiedDiff, hasKnownBranchRef, listUnpushedCommitsForCwd, pullGitBranch, pushCurrentBranch, rebaseGitBranchOntoBase, resolveExistingWorktreePath, resolveWorktreePath, summarizeTicketDiffFiles, switchProjectBranch, syncWorktreeFilesAsync, undoLatestUnpushedCommitForCwd } from './lib/git.js';
21
+ import { abortGitOperation, branchExistsLocally, commitGitChanges, createWorktreeAsync, deleteWorktree, getBundleReviewDiffForCwd, getGitDiffData, getGitDiffFiles, getGitStatus, getGitUnifiedDiff, getLocalProjectBranch, getProjectBranch, getProjectBranchSuggestions, getSelectedGitUnifiedDiff, hasKnownBranchRef, listUnpushedCommitsForCwd, pullGitBranch, pushCurrentBranch, rebaseGitBranchOntoBase, resolveExistingWorktreePath, resolveWorktreePath, summarizeTicketDiffFiles, switchProjectBranch, syncWorktreeFilesAsync, undoLatestUnpushedCommitForCwd } from './lib/git.js';
22
22
  import config from './lib/config.js';
23
23
  import { autoConfigProject, ProjectAutoConfigError } from './lib/projectAutoConfig.js';
24
24
  import { analyzeRepoPath, createInitialCommit, ensureInitialCommitForWorktrees, initializeGitRepo, isInitialCommitRequiredError, } from './lib/projectRepos.js';
@@ -41,10 +41,12 @@ import { listProjectMemoryEvents } from './lib/projectMemoryEvents.js';
41
41
  import { getProjectMemorySelectionForAgentRun, listProjectMemoryUsage } from './lib/projectMemorySelections.js';
42
42
  import { decodeProjectMemoryCursor } from './lib/projectMemoryCursor.js';
43
43
  import { getProjectFileSuggestions } from './lib/projectFileSuggestions.js';
44
+ import { getProjectFileManifest, ProjectFileContentError, ProjectFileTargetUnavailableError, readProjectFileContent } from './lib/projectFiles.js';
44
45
  import { updateProjectTargetSelection } from './lib/projectTargets.js';
45
46
  import { exportProjectMemoryArchive, importProjectMemoryArchive } from './lib/projectMemoryTransfer.js';
46
47
  import { buildReviewFindingsResponse, computeDiffSignature, getLatestReviewRunForContext, listReviewFindings } from './lib/reviewFindings.js';
47
- import { clearAutomationPause, countBlockingFindings, depthAtLeast, getBundleAutomationState, isAutomationDepth, resolveAutomationConfig, } from './lib/ticketAutomation.js';
48
+ import { createBuildOpenQuestionResolutionBatch, failBuildOpenQuestionBatch, formatBuildOpenQuestionAnswersPrompt, getLatestBuildOpenQuestionBatchStatus, hasActiveBuildOpenQuestionBatch, listAnsweredBuildOpenQuestionsForBatch, listBuildOpenQuestionsForBundle, loadBuildOpenQuestionsByBundleIds, setBuildOpenQuestionBatchStatus, } from './lib/buildOpenQuestions.js';
49
+ import { clearAutomationPause, countBlockingFindings, depthAtLeast, getBundleAutomationState, isAutomationDepth, recordAutomationPause, resolveAutomationConfig, } from './lib/ticketAutomation.js';
48
50
  import { runAutomationCommitChain } from './lib/ticketAutomationCommit.js';
49
51
  import { getDiscoveredSkills, normalizeSkillNames } from './lib/skills.js';
50
52
  import { clearAllPreviewProxies } from './lib/previewProxy.js';
@@ -834,6 +836,87 @@ const resolveProjectFileSuggestionWorkspace = (project, options = {}) => {
834
836
  cacheKey: `target:${workspace.target_kind}:${workspace.target_bundle_id ?? 'repo_root'}:${workspace.cwd}`,
835
837
  };
836
838
  };
839
+ const resolveWorkspaceFileWorkspace = (project, targetKind, bundleId, ticketId) => {
840
+ if (targetKind === 'repo_root') {
841
+ if (!existsSync(project.repo_path)) {
842
+ return { error: 'Repository target is unavailable', status: 409 };
843
+ }
844
+ return {
845
+ cwd: project.repo_path,
846
+ branch: getLocalProjectBranch(project),
847
+ project,
848
+ cacheKey: `workspace-files:${project.id}:repo_root:${project.repo_path}`,
849
+ target: { kind: 'repo_root', bundle_id: null, ticket_id: null },
850
+ };
851
+ }
852
+ if (targetKind === 'bundle') {
853
+ if (!bundleId) {
854
+ return { error: 'bundle_id is required for a bundle target', status: 400 };
855
+ }
856
+ const bundle = getBundle(bundleId, project.id);
857
+ if (!bundle || isProjectRootBundle(bundle)) {
858
+ return { error: 'Bundle target not found', status: 404 };
859
+ }
860
+ const cwd = resolveExistingWorktreePath(bundle.branch, project);
861
+ if (!cwd) {
862
+ return { error: 'Bundle target is unavailable', status: 409 };
863
+ }
864
+ return {
865
+ cwd,
866
+ branch: bundle.branch,
867
+ project,
868
+ cacheKey: `workspace-files:${project.id}:bundle:${bundle.id}:${cwd}`,
869
+ target: { kind: 'bundle', bundle_id: bundle.id, ticket_id: null },
870
+ };
871
+ }
872
+ if (!ticketId) {
873
+ return { error: 'ticket_id is required for a ticket target', status: 400 };
874
+ }
875
+ const ticket = getTicket(ticketId);
876
+ if (!ticket || ticket.project_id !== project.id) {
877
+ return { error: 'Ticket target not found', status: 404 };
878
+ }
879
+ const bundle = ticket.worktree_bundle_id ? getBundle(ticket.worktree_bundle_id, project.id) : null;
880
+ if (bundle && isProjectRootBundle(bundle)) {
881
+ if (!existsSync(project.repo_path)) {
882
+ return { error: 'Ticket target is unavailable', status: 409 };
883
+ }
884
+ return {
885
+ cwd: project.repo_path,
886
+ branch: getLocalProjectBranch(project),
887
+ project,
888
+ cacheKey: `workspace-files:${project.id}:ticket:${ticket.id}:${project.repo_path}`,
889
+ target: { kind: 'ticket', bundle_id: null, ticket_id: ticket.id },
890
+ };
891
+ }
892
+ const branch = resolveTicketBranch(ticket);
893
+ const cwd = branch ? resolveExistingWorktreePath(branch, project) : null;
894
+ if (!branch || !cwd) {
895
+ return { error: 'Ticket target is unavailable', status: 409 };
896
+ }
897
+ return {
898
+ cwd,
899
+ branch,
900
+ project,
901
+ cacheKey: `workspace-files:${project.id}:ticket:${ticket.id}:${cwd}`,
902
+ target: { kind: 'ticket', bundle_id: null, ticket_id: ticket.id },
903
+ };
904
+ };
905
+ const parseWorkspaceFileTarget = (req) => {
906
+ const targetKind = typeof req.query.target_kind === 'string' ? req.query.target_kind : '';
907
+ if (targetKind !== 'repo_root' && targetKind !== 'bundle' && targetKind !== 'ticket') {
908
+ return { error: 'target_kind must be repo_root, bundle, or ticket', status: 400 };
909
+ }
910
+ return {
911
+ targetKind,
912
+ bundleId: typeof req.query.bundle_id === 'string' ? req.query.bundle_id.trim() || null : null,
913
+ ticketId: typeof req.query.ticket_id === 'string' ? req.query.ticket_id.trim() || null : null,
914
+ };
915
+ };
916
+ const getWorkspaceFileTargetUnavailableMessage = (targetKind) => {
917
+ const targetName = targetKind === 'repo_root' ? 'Repository' : targetKind === 'bundle' ? 'Bundle' : 'Ticket';
918
+ return `${targetName} target is unavailable`;
919
+ };
837
920
  const getProjectBranchStateForFooter = (project, options = {}) => {
838
921
  const branchState = getProjectBranch(project, {
839
922
  refreshRemote: options.refreshRemote,
@@ -1062,11 +1145,11 @@ const startBundlePullRequestDiscoveryIfNeeded = (project, bundle) => {
1062
1145
  })();
1063
1146
  bundlePullRequestDiscoveryInFlight.set(key, task);
1064
1147
  };
1065
- const serializeBundleRow = (projectId, bundle) => {
1148
+ const serializeBundleRow = (projectId, bundle, openQuestions = bundle ? loadBuildOpenQuestionsByBundleIds(projectId, [bundle.id]).get(bundle.id) ?? [] : [], resolvedProject) => {
1066
1149
  if (!bundle) {
1067
1150
  return null;
1068
1151
  }
1069
- const project = getProjectById(projectId);
1152
+ const project = resolvedProject ?? getProjectById(projectId);
1070
1153
  const hasPreparedWorktree = bundle.kind === 'project_root'
1071
1154
  ? true
1072
1155
  : Boolean(project && resolveExistingWorktreePath(bundle.branch, project));
@@ -1077,6 +1160,8 @@ const serializeBundleRow = (projectId, bundle) => {
1077
1160
  ? serializeBundlePullRequest(getBundlePullRequestRecord(bundle.id, projectId))
1078
1161
  : null,
1079
1162
  automation_config: resolveAutomationConfig(bundle, project),
1163
+ open_questions: openQuestions,
1164
+ build_open_question_batch_status: getLatestBuildOpenQuestionBatchStatus(projectId, bundle.id),
1080
1165
  };
1081
1166
  };
1082
1167
  const serializeGitHubPullRequestSummary = (pullRequest) => {
@@ -1256,11 +1341,11 @@ const resolveBundleRenameEligibility = (project, bundle, tickets = listBundleTic
1256
1341
  return { eligible: true, reason: null };
1257
1342
  };
1258
1343
  const serializeBundleDetails = (project, bundle, extras = {}) => {
1259
- const serializedBundle = serializeBundleRow(project.id, bundle);
1344
+ const tickets = listDetailedBundleTickets(project.id, bundle.id);
1345
+ const serializedBundle = serializeBundleRow(project.id, bundle, tickets.flatMap(ticket => ticket.build_open_questions ?? []), project);
1260
1346
  if (!serializedBundle) {
1261
1347
  throw new Error(`Failed to serialize bundle ${bundle.id}`);
1262
1348
  }
1263
- const tickets = listDetailedBundleTickets(project.id, bundle.id);
1264
1349
  const latestSessionTicket = resolveBundleLatestSessionTicket(tickets);
1265
1350
  const resolvedTargetTicket = extras.target_ticket_id
1266
1351
  ? tickets.find(ticket => ticket.id === extras.target_ticket_id) ?? null
@@ -1389,6 +1474,9 @@ const reviewBundleTickets = (projectId, bundleId) => {
1389
1474
  if (!bundle) {
1390
1475
  return { ok: false, status: 404, error: 'Bundle not found' };
1391
1476
  }
1477
+ if (hasActiveBuildOpenQuestionBatch(projectId, bundle.id)) {
1478
+ return { ok: false, status: 409, error: 'Build open question resolution is still in progress' };
1479
+ }
1392
1480
  const result = transitionBundleToReview(projectId, bundle.id, io);
1393
1481
  if (!result.ok) {
1394
1482
  return { ok: false, status: result.status, error: result.error };
@@ -2630,11 +2718,12 @@ app.post('/projects/:id/spec-sessions/:sessionId/snapshots/:snapshotId/export',
2630
2718
  return res.status(404).json({ error: 'Not found' });
2631
2719
  }
2632
2720
  try {
2721
+ const overwrite = req.body?.overwrite === true;
2633
2722
  const collision = checkSpecExportCollision({
2634
2723
  specSession,
2635
2724
  repoPath: project.repo_path,
2636
2725
  });
2637
- if (collision) {
2726
+ if (collision && !overwrite) {
2638
2727
  return res.status(409).json({
2639
2728
  error: 'A spec with this title has already been exported.',
2640
2729
  conflicting_path: collision,
@@ -2841,7 +2930,8 @@ app.get('/bundles', async (req, res) => {
2841
2930
  timing.mark('ensure_project_root_bundle');
2842
2931
  const bundles = listBundles(result.project.id);
2843
2932
  timing.mark('list_bundles');
2844
- const serializedBundles = bundles.map(bundle => serializeBundleRow(result.project.id, bundle));
2933
+ const openQuestionsByBundleId = loadBuildOpenQuestionsByBundleIds(result.project.id, bundles.map(bundle => bundle.id));
2934
+ const serializedBundles = bundles.map(bundle => serializeBundleRow(result.project.id, bundle, openQuestionsByBundleId.get(bundle.id) ?? [], result.project));
2845
2935
  timing.mark('serialize_bundles');
2846
2936
  timing.end({ count: serializedBundles.length });
2847
2937
  res.json(serializedBundles);
@@ -3323,6 +3413,19 @@ app.patch('/bundles/:id', (req, res) => {
3323
3413
  return res.status(400).json({ error: 'automation_review_fix_enabled must be a boolean' });
3324
3414
  }
3325
3415
  }
3416
+ let automationPauseOnOpenQuestions = bundle.automation_pause_on_open_questions ?? null;
3417
+ if (hasOwn(req.body, 'automation_pause_on_open_questions')) {
3418
+ const value = req.body.automation_pause_on_open_questions;
3419
+ if (value == null) {
3420
+ automationPauseOnOpenQuestions = null;
3421
+ }
3422
+ else if (typeof value === 'boolean') {
3423
+ automationPauseOnOpenQuestions = value ? 1 : 0;
3424
+ }
3425
+ else {
3426
+ return res.status(400).json({ error: 'automation_pause_on_open_questions must be a boolean' });
3427
+ }
3428
+ }
3326
3429
  let updated = null;
3327
3430
  try {
3328
3431
  updated = db.transaction(() => {
@@ -3339,6 +3442,7 @@ app.patch('/bundles/:id', (req, res) => {
3339
3442
  projectId: result.project.id,
3340
3443
  automationDepth,
3341
3444
  automationReviewFixEnabled,
3445
+ automationPauseOnOpenQuestions,
3342
3446
  });
3343
3447
  })();
3344
3448
  }
@@ -3379,6 +3483,99 @@ app.post('/bundles/:id/review', (req, res) => {
3379
3483
  }
3380
3484
  return res.json(reviewResult.details);
3381
3485
  });
3486
+ app.post('/bundles/:id/build-open-questions/resolve', (req, res) => {
3487
+ const result = getRequiredProject(req);
3488
+ if ('error' in result) {
3489
+ return res.status(400).json({ error: result.error });
3490
+ }
3491
+ const bundle = getBundle(req.params.id, result.project.id);
3492
+ if (!bundle) {
3493
+ return res.status(404).json({ error: 'Bundle not found' });
3494
+ }
3495
+ const submitted = req.body?.resolutions;
3496
+ if (!Array.isArray(submitted) || submitted.length === 0) {
3497
+ return res.status(400).json({ error: 'resolutions must be a non-empty array' });
3498
+ }
3499
+ const resolutions = submitted.map((resolution) => {
3500
+ if (!resolution || typeof resolution !== 'object') {
3501
+ return null;
3502
+ }
3503
+ const value = resolution;
3504
+ return {
3505
+ questionId: typeof value.question_id === 'string' ? value.question_id : '',
3506
+ answer: typeof value.answer === 'string' ? value.answer : undefined,
3507
+ dismiss: value.dismiss === true,
3508
+ dismissalReason: typeof value.dismissal_reason === 'string' ? value.dismissal_reason : null,
3509
+ dismissedBy: 'user',
3510
+ };
3511
+ });
3512
+ if (resolutions.some(resolution => resolution === null || !resolution.questionId)) {
3513
+ return res.status(400).json({ error: 'Each resolution requires a question_id' });
3514
+ }
3515
+ const buildTickets = listBundleSliceTickets(result.project.id, bundle.id, 'build');
3516
+ if (buildTickets.length === 0
3517
+ || buildTickets.some(ticket => ticket.agent_status !== 'done' && ticket.agent_status !== 'stopped')) {
3518
+ return res.status(409).json({ error: 'All build tickets in the bundle must be done or stopped before resolving questions' });
3519
+ }
3520
+ const created = createBuildOpenQuestionResolutionBatch({
3521
+ projectId: result.project.id,
3522
+ bundleId: bundle.id,
3523
+ resolutions: resolutions.filter((resolution) => resolution !== null),
3524
+ });
3525
+ if (!created.ok) {
3526
+ return res.status(409).json({
3527
+ error: created.error,
3528
+ ...(created.code ? { code: created.code } : {}),
3529
+ });
3530
+ }
3531
+ const answeredQuestions = listAnsweredBuildOpenQuestionsForBatch(created.batch.id);
3532
+ if (answeredQuestions.length === 0) {
3533
+ clearAutomationPause(result.project.id, bundle.id);
3534
+ const automationConfig = resolveAutomationConfig(bundle, result.project);
3535
+ const reviewResult = depthAtLeast(automationConfig.depth, 'auto_review')
3536
+ ? transitionBundleToReview(result.project.id, bundle.id, io, { automatic: true })
3537
+ : null;
3538
+ for (const ticket of listDetailedBundleTickets(result.project.id, bundle.id)) {
3539
+ io.emit('ticket:updated', ticket);
3540
+ }
3541
+ if (reviewResult && !reviewResult.ok) {
3542
+ return res.status(reviewResult.status).json({ error: reviewResult.error });
3543
+ }
3544
+ return res.json(serializeBundleDetails(result.project, bundle, {
3545
+ review_owner_ticket_id: reviewResult?.value?.id,
3546
+ target_ticket_id: reviewResult?.value?.id,
3547
+ }));
3548
+ }
3549
+ const targetTicketSource = resolveBundleTargetTicket(buildTickets);
3550
+ const targetTicket = targetTicketSource ? getTicket(targetTicketSource.id) : null;
3551
+ if (!targetTicket) {
3552
+ failBuildOpenQuestionBatch(created.batch.id, 'No completed Build ticket is available for the follow-up');
3553
+ recordAutomationPause(result.project.id, bundle.id, 'open_questions', 'Build open question follow-up could not be started');
3554
+ return res.status(409).json({ error: 'No completed Build ticket is available for the follow-up' });
3555
+ }
3556
+ const instruction = [
3557
+ 'Apply this complete batch of answered Build requirements in one bundle-scoped pass. Preserve the grouping by originating ticket and do not work on dismissed questions.',
3558
+ formatBuildOpenQuestionAnswersPrompt(answeredQuestions),
3559
+ ].join('\n\n');
3560
+ setBuildOpenQuestionBatchStatus(created.batch.id, 'running');
3561
+ const started = startFollowUp(targetTicket, io, instruction, 'build', {
3562
+ buildOpenQuestionBatchId: created.batch.id,
3563
+ });
3564
+ if (!started) {
3565
+ failBuildOpenQuestionBatch(created.batch.id, 'Build follow-up could not be started');
3566
+ recordAutomationPause(result.project.id, bundle.id, 'open_questions', 'Build open question follow-up could not be started');
3567
+ for (const ticket of listDetailedBundleTickets(result.project.id, bundle.id)) {
3568
+ io.emit('ticket:updated', ticket);
3569
+ }
3570
+ return res.status(409).json({ error: 'Build follow-up could not be started' });
3571
+ }
3572
+ for (const ticket of listDetailedBundleTickets(result.project.id, bundle.id)) {
3573
+ io.emit('ticket:updated', ticket);
3574
+ }
3575
+ return res.status(202).json(serializeBundleDetails(result.project, bundle, {
3576
+ target_ticket_id: targetTicket.id,
3577
+ }));
3578
+ });
3382
3579
  app.post('/bundles/:id/undo-review', (req, res) => {
3383
3580
  const result = getRequiredProject(req);
3384
3581
  if ('error' in result) {
@@ -3482,10 +3679,21 @@ app.post('/bundles/:id/resume-automation', async (req, res) => {
3482
3679
  if (!bundleState || !bundleState.automation_paused_reason) {
3483
3680
  return res.status(409).json({ error: 'Bundle automation is not paused' });
3484
3681
  }
3485
- clearAutomationPause(result.project.id, bundle.id);
3486
- const reviewOwnerSource = resolveBundleReviewOwner(result.project.id, bundle.id)
3487
- ?? listBundleSliceTickets(result.project.id, bundle.id, 'review')[0]
3488
- ?? null;
3682
+ const liveAutomationConfig = resolveAutomationConfig(bundle, result.project);
3683
+ if (hasActiveBuildOpenQuestionBatch(result.project.id, bundle.id)) {
3684
+ return res.status(409).json({ error: 'Build open question resolution is still in progress' });
3685
+ }
3686
+ if (bundleState.automation_paused_code === 'open_questions'
3687
+ && liveAutomationConfig.pauseOnOpenQuestions
3688
+ && listBuildOpenQuestionsForBundle(result.project.id, bundle.id).length > 0) {
3689
+ return res.status(409).json({ error: 'Resolve or dismiss all Build open questions before resuming automation' });
3690
+ }
3691
+ const reviewOwnerSource = bundleState.automation_paused_code === 'open_questions'
3692
+ ? listBundleSliceTickets(result.project.id, bundle.id, 'build')
3693
+ .find(ticket => ticket.agent_status === 'done') ?? null
3694
+ : resolveBundleReviewOwner(result.project.id, bundle.id)
3695
+ ?? listBundleSliceTickets(result.project.id, bundle.id, 'review')[0]
3696
+ ?? null;
3489
3697
  if (!reviewOwnerSource) {
3490
3698
  return res.status(409).json({ error: 'No review ticket to resume from' });
3491
3699
  }
@@ -3514,12 +3722,15 @@ app.post('/bundles/:id/resume-automation', async (req, res) => {
3514
3722
  && automationConfig.reviewFixEnabled
3515
3723
  && blockingCount > 0
3516
3724
  && iteration < automationConfig.reviewMaxIterations) {
3725
+ clearAutomationPause(result.project.id, bundle.id);
3517
3726
  startFollowUp(owner, io, `Fix the code review findings at or above "${automationConfig.reviewSeverity}" severity. Address each blocking finding directly and minimally. Do not introduce unrelated changes, reformatting, or refactors. Do not commit.`, 'build');
3518
3727
  }
3519
3728
  else if (depthAtLeast(automationConfig.depth, 'auto_commit')) {
3729
+ clearAutomationPause(result.project.id, bundle.id);
3520
3730
  await runAutomationCommitChain(owner, project, owner.bundle ?? null, automationConfig, io);
3521
3731
  }
3522
3732
  else {
3733
+ clearAutomationPause(result.project.id, bundle.id);
3523
3734
  startReview(owner, io);
3524
3735
  }
3525
3736
  }
@@ -3528,6 +3739,7 @@ app.post('/bundles/:id/resume-automation', async (req, res) => {
3528
3739
  if (!reviewResult.ok) {
3529
3740
  return res.status(reviewResult.status).json({ error: reviewResult.error });
3530
3741
  }
3742
+ clearAutomationPause(result.project.id, bundle.id);
3531
3743
  }
3532
3744
  else {
3533
3745
  return res.status(409).json({ error: 'Ticket is not in a resumable state' });
@@ -3551,6 +3763,14 @@ app.post('/bundles/:id/dismiss-automation-pause', (req, res) => {
3551
3763
  if (!bundleState || !bundleState.automation_paused_reason) {
3552
3764
  return res.status(409).json({ error: 'Bundle automation is not paused' });
3553
3765
  }
3766
+ if (hasActiveBuildOpenQuestionBatch(result.project.id, bundle.id)) {
3767
+ return res.status(409).json({ error: 'Build open question resolution is still in progress' });
3768
+ }
3769
+ if (bundleState.automation_paused_code === 'open_questions'
3770
+ && resolveAutomationConfig(bundle, result.project).pauseOnOpenQuestions
3771
+ && listBuildOpenQuestionsForBundle(result.project.id, bundle.id).length > 0) {
3772
+ return res.status(409).json({ error: 'Resolve or dismiss all Build open questions before dismissing this pause' });
3773
+ }
3554
3774
  clearAutomationPause(result.project.id, bundle.id);
3555
3775
  for (const ticket of listDetailedBundleTickets(result.project.id, bundle.id)) {
3556
3776
  io.emit('ticket:updated', ticket);
@@ -4506,7 +4726,7 @@ app.post('/projects', (req, res) => {
4506
4726
  return res.status(400).json({ error: validation.error });
4507
4727
  }
4508
4728
  const projectCount = Number(db.prepare('SELECT COUNT(*) AS count FROM projects').get()?.count ?? 0);
4509
- const { name, repo_path, linked_project_ids, run_setup, run_services, test_commands, run_ide, preview_service_name, preview_path, preview_capability_mode, helper_model, helper_variant, commit_message_rules, auto_park_stale_tickets, memory_enabled, scan_include_globs_json, scan_exclude_globs_json, worktree_sync, spec_aware_code_review_enabled, automation_depth, automation_review_fix_enabled, automation_review_severity, automation_review_max_iterations, } = validation.values;
4729
+ const { name, repo_path, linked_project_ids, run_setup, run_services, test_commands, run_ide, preview_service_name, preview_path, preview_capability_mode, helper_model, helper_variant, commit_message_rules, auto_park_stale_tickets, memory_enabled, scan_include_globs_json, scan_exclude_globs_json, worktree_sync, spec_aware_code_review_enabled, automation_depth, automation_review_fix_enabled, automation_pause_on_open_questions, automation_review_severity, automation_review_max_iterations, } = validation.values;
4510
4730
  if (!repo_path) {
4511
4731
  return res.status(400).json({ error: 'repo_path must be a non-empty string' });
4512
4732
  }
@@ -4542,11 +4762,12 @@ app.post('/projects', (req, res) => {
4542
4762
  spec_aware_code_review_enabled,
4543
4763
  automation_depth,
4544
4764
  automation_review_fix_enabled,
4765
+ automation_pause_on_open_questions,
4545
4766
  automation_review_severity,
4546
4767
  automation_review_max_iterations
4547
4768
  )
4548
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
4549
- `).run(id, name, normalizedRepoPath, run_setup, run_services, test_commands, run_ide ?? null, preview_service_name ?? null, preview_path ?? null, preview_capability_mode ?? null, helper_model ?? null, helper_variant ?? null, commit_message_rules ?? null, auto_park_stale_tickets ?? 0, memory_enabled ?? 0, scan_include_globs_json ?? null, scan_exclude_globs_json ?? null, worktree_sync ?? null, spec_aware_code_review_enabled ?? 0, automation_depth ?? 'manual', automation_review_fix_enabled ?? 0, automation_review_severity ?? 'medium', automation_review_max_iterations ?? 3);
4769
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
4770
+ `).run(id, name, normalizedRepoPath, run_setup, run_services, test_commands, run_ide ?? null, preview_service_name ?? null, preview_path ?? null, preview_capability_mode ?? null, helper_model ?? null, helper_variant ?? null, commit_message_rules ?? null, auto_park_stale_tickets ?? 0, memory_enabled ?? 0, scan_include_globs_json ?? null, scan_exclude_globs_json ?? null, worktree_sync ?? null, spec_aware_code_review_enabled ?? 0, automation_depth ?? 'manual', automation_review_fix_enabled ?? 0, automation_pause_on_open_questions ?? 0, automation_review_severity ?? 'medium', automation_review_max_iterations ?? 3);
4550
4771
  replaceProjectLinks(id, linked_project_ids ?? []);
4551
4772
  ensureProjectRootBundle(id);
4552
4773
  if (projectCount === 0) {
@@ -5316,6 +5537,60 @@ app.get('/projects/:id/file-suggestions', (req, res) => {
5316
5537
  return res.status(500).json({ error: 'Unable to load project file suggestions' });
5317
5538
  }
5318
5539
  });
5540
+ app.get('/projects/:id/workspace-files', (req, res) => {
5541
+ const project = getProjectById(req.params.id);
5542
+ if (!project) {
5543
+ return res.status(404).json({ error: 'Not found' });
5544
+ }
5545
+ const target = parseWorkspaceFileTarget(req);
5546
+ if ('error' in target) {
5547
+ return res.status(target.status).json({ error: target.error });
5548
+ }
5549
+ const workspace = resolveWorkspaceFileWorkspace(project, target.targetKind, target.bundleId, target.ticketId);
5550
+ if ('error' in workspace) {
5551
+ return res.status(workspace.status).json({ error: workspace.error });
5552
+ }
5553
+ try {
5554
+ return res.json(getProjectFileManifest(workspace, { refresh: req.query.refresh === 'true' }));
5555
+ }
5556
+ catch (error) {
5557
+ if (error instanceof ProjectFileTargetUnavailableError) {
5558
+ return res.status(409).json({ error: getWorkspaceFileTargetUnavailableMessage(workspace.target.kind) });
5559
+ }
5560
+ console.error(`Failed to build workspace file manifest for project ${project.id}:`, error);
5561
+ return res.status(500).json({ error: 'Unable to load workspace files' });
5562
+ }
5563
+ });
5564
+ app.get('/projects/:id/workspace-files/content', (req, res) => {
5565
+ const project = getProjectById(req.params.id);
5566
+ if (!project) {
5567
+ return res.status(404).json({ error: 'Not found' });
5568
+ }
5569
+ const target = parseWorkspaceFileTarget(req);
5570
+ if ('error' in target) {
5571
+ return res.status(target.status).json({ error: target.error });
5572
+ }
5573
+ if (typeof req.query.path !== 'string') {
5574
+ return res.status(400).json({ error: 'path is required' });
5575
+ }
5576
+ const workspace = resolveWorkspaceFileWorkspace(project, target.targetKind, target.bundleId, target.ticketId);
5577
+ if ('error' in workspace) {
5578
+ return res.status(workspace.status).json({ error: workspace.error });
5579
+ }
5580
+ try {
5581
+ return res.json(readProjectFileContent(workspace, req.query.path));
5582
+ }
5583
+ catch (error) {
5584
+ if (error instanceof ProjectFileTargetUnavailableError) {
5585
+ return res.status(409).json({ error: getWorkspaceFileTargetUnavailableMessage(workspace.target.kind) });
5586
+ }
5587
+ if (error instanceof ProjectFileContentError) {
5588
+ return res.status(error.status).json({ error: error.message, reason: error.reason });
5589
+ }
5590
+ console.error(`Failed to read workspace file content for project ${project.id}:`, error);
5591
+ return res.status(500).json({ error: 'Unable to read workspace file' });
5592
+ }
5593
+ });
5319
5594
  app.post('/projects/:id/git-pull', async (req, res) => {
5320
5595
  const project = getProjectById(req.params.id);
5321
5596
  if (!project) {
@@ -5658,7 +5933,7 @@ app.patch('/projects/:id', (req, res) => {
5658
5933
  });
5659
5934
  }
5660
5935
  }
5661
- const { name, repo_path, linked_project_ids, run_setup, run_services, test_commands, run_ide, preview_service_name, preview_path, preview_capability_mode, helper_model, helper_variant, commit_message_rules, auto_park_stale_tickets, memory_enabled, scan_include_globs_json, scan_exclude_globs_json, worktree_sync, spec_aware_code_review_enabled, automation_depth, automation_review_fix_enabled, automation_review_severity, automation_review_max_iterations, } = validation.values;
5936
+ const { name, repo_path, linked_project_ids, run_setup, run_services, test_commands, run_ide, preview_service_name, preview_path, preview_capability_mode, helper_model, helper_variant, commit_message_rules, auto_park_stale_tickets, memory_enabled, scan_include_globs_json, scan_exclude_globs_json, worktree_sync, spec_aware_code_review_enabled, automation_depth, automation_review_fix_enabled, automation_pause_on_open_questions, automation_review_severity, automation_review_max_iterations, } = validation.values;
5662
5937
  db.prepare(`
5663
5938
  UPDATE projects SET
5664
5939
  name = ?,
@@ -5681,11 +5956,12 @@ app.patch('/projects/:id', (req, res) => {
5681
5956
  spec_aware_code_review_enabled = ?,
5682
5957
  automation_depth = ?,
5683
5958
  automation_review_fix_enabled = ?,
5959
+ automation_pause_on_open_questions = ?,
5684
5960
  automation_review_severity = ?,
5685
5961
  automation_review_max_iterations = ?,
5686
5962
  updated_at = CURRENT_TIMESTAMP
5687
5963
  WHERE id = ?
5688
- `).run(name ?? project.name, repo_path ?? project.repo_path, hasOwn(validation.values, 'run_setup') ? run_setup : project.run_setup, hasOwn(validation.values, 'run_services') ? run_services : project.run_services, hasOwn(validation.values, 'test_commands') ? test_commands : project.test_commands, hasOwn(validation.values, 'run_ide') ? run_ide : project.run_ide, hasOwn(validation.values, 'preview_service_name') ? preview_service_name : project.preview_service_name, hasOwn(validation.values, 'preview_path') ? preview_path : project.preview_path, hasOwn(validation.values, 'preview_capability_mode') ? preview_capability_mode : project.preview_capability_mode, hasOwn(validation.values, 'helper_model') ? helper_model : project.helper_model, hasOwn(validation.values, 'helper_variant') ? helper_variant : project.helper_variant, hasOwn(validation.values, 'commit_message_rules') ? commit_message_rules : project.commit_message_rules, hasOwn(validation.values, 'auto_park_stale_tickets') ? auto_park_stale_tickets : project.auto_park_stale_tickets, hasOwn(validation.values, 'memory_enabled') ? memory_enabled : project.memory_enabled, hasOwn(validation.values, 'scan_include_globs_json') ? scan_include_globs_json : project.scan_include_globs_json, hasOwn(validation.values, 'scan_exclude_globs_json') ? scan_exclude_globs_json : project.scan_exclude_globs_json, hasOwn(validation.values, 'worktree_sync') ? worktree_sync : project.worktree_sync, hasOwn(validation.values, 'spec_aware_code_review_enabled') ? spec_aware_code_review_enabled : project.spec_aware_code_review_enabled, hasOwn(validation.values, 'automation_depth') ? automation_depth : project.automation_depth, hasOwn(validation.values, 'automation_review_fix_enabled') ? automation_review_fix_enabled : project.automation_review_fix_enabled, hasOwn(validation.values, 'automation_review_severity') ? automation_review_severity : project.automation_review_severity, hasOwn(validation.values, 'automation_review_max_iterations') ? automation_review_max_iterations : project.automation_review_max_iterations, req.params.id);
5964
+ `).run(name ?? project.name, repo_path ?? project.repo_path, hasOwn(validation.values, 'run_setup') ? run_setup : project.run_setup, hasOwn(validation.values, 'run_services') ? run_services : project.run_services, hasOwn(validation.values, 'test_commands') ? test_commands : project.test_commands, hasOwn(validation.values, 'run_ide') ? run_ide : project.run_ide, hasOwn(validation.values, 'preview_service_name') ? preview_service_name : project.preview_service_name, hasOwn(validation.values, 'preview_path') ? preview_path : project.preview_path, hasOwn(validation.values, 'preview_capability_mode') ? preview_capability_mode : project.preview_capability_mode, hasOwn(validation.values, 'helper_model') ? helper_model : project.helper_model, hasOwn(validation.values, 'helper_variant') ? helper_variant : project.helper_variant, hasOwn(validation.values, 'commit_message_rules') ? commit_message_rules : project.commit_message_rules, hasOwn(validation.values, 'auto_park_stale_tickets') ? auto_park_stale_tickets : project.auto_park_stale_tickets, hasOwn(validation.values, 'memory_enabled') ? memory_enabled : project.memory_enabled, hasOwn(validation.values, 'scan_include_globs_json') ? scan_include_globs_json : project.scan_include_globs_json, hasOwn(validation.values, 'scan_exclude_globs_json') ? scan_exclude_globs_json : project.scan_exclude_globs_json, hasOwn(validation.values, 'worktree_sync') ? worktree_sync : project.worktree_sync, hasOwn(validation.values, 'spec_aware_code_review_enabled') ? spec_aware_code_review_enabled : project.spec_aware_code_review_enabled, hasOwn(validation.values, 'automation_depth') ? automation_depth : project.automation_depth, hasOwn(validation.values, 'automation_review_fix_enabled') ? automation_review_fix_enabled : project.automation_review_fix_enabled, hasOwn(validation.values, 'automation_pause_on_open_questions') ? automation_pause_on_open_questions : project.automation_pause_on_open_questions, hasOwn(validation.values, 'automation_review_severity') ? automation_review_severity : project.automation_review_severity, hasOwn(validation.values, 'automation_review_max_iterations') ? automation_review_max_iterations : project.automation_review_max_iterations, req.params.id);
5689
5965
  if (hasOwn(validation.values, 'linked_project_ids')) {
5690
5966
  replaceProjectLinks(project.id, linked_project_ids ?? []);
5691
5967
  }
@@ -6053,6 +6329,12 @@ app.post('/tickets/:id/retry', (req, res) => {
6053
6329
  if (ticket.agent_status !== 'error') {
6054
6330
  return res.status(400).json({ error: 'Ticket is not in error state' });
6055
6331
  }
6332
+ if (ticket.state === 'review'
6333
+ && ticket.project_id
6334
+ && ticket.worktree_bundle_id
6335
+ && hasActiveBuildOpenQuestionBatch(ticket.project_id, ticket.worktree_bundle_id)) {
6336
+ return res.status(409).json({ error: 'Build open question resolution is still in progress' });
6337
+ }
6056
6338
  db.prepare('UPDATE tickets SET agent_status = NULL, agent_log = NULL, agent_message_id = NULL, previous_agent_log = NULL, previous_agent_message_id = NULL, streaming_response = NULL, build_completed_at = NULL, auto_park_dismissed_at = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(req.params.id);
6057
6339
  const updated = getTicket(req.params.id);
6058
6340
  io.emit('ticket:updated', updated);