@straiffi/archon 1.3.11 → 1.4.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 (90) hide show
  1. package/dist/client/assets/{TestsDialog-BUCv8I-8.js → TestsDialog-CD_kQh8y.js} +1 -1
  2. package/dist/client/assets/{architectureDiagram-3BPJPVTR-C3Jb8g9R.js → architectureDiagram-3BPJPVTR-eKhX8J5g.js} +1 -1
  3. package/dist/client/assets/{blockDiagram-GPEHLZMM-CZcqQgBX.js → blockDiagram-GPEHLZMM-dYzMDN2p.js} +1 -1
  4. package/dist/client/assets/{c4Diagram-AAUBKEIU-Cpv5hJF9.js → c4Diagram-AAUBKEIU-Bfe91lzp.js} +1 -1
  5. package/dist/client/assets/channel-ByZgAb3R.js +1 -0
  6. package/dist/client/assets/{chunk-2J33WTMH-DkbZ544o.js → chunk-2J33WTMH-Box1ZnSe.js} +1 -1
  7. package/dist/client/assets/{chunk-3OPIFGDE-CTkBmjbl.js → chunk-3OPIFGDE-BOkhWfjl.js} +1 -1
  8. package/dist/client/assets/{chunk-5ZQYHXKU-Cvt23bYu.js → chunk-5ZQYHXKU-DZt1zqaf.js} +1 -1
  9. package/dist/client/assets/{chunk-727SXJPM-DX5ql843.js → chunk-727SXJPM-DZYZCHLI.js} +1 -1
  10. package/dist/client/assets/{chunk-AQP2D5EJ-BWR4RAvi.js → chunk-AQP2D5EJ-CmgJLI9e.js} +1 -1
  11. package/dist/client/assets/{chunk-CSCIHK7Q-BrZJ9QMn.js → chunk-CSCIHK7Q-D8FHELNF.js} +1 -1
  12. package/dist/client/assets/{chunk-KSCS5N6A-Xsa2cnhz.js → chunk-KSCS5N6A-DtLUz0Po.js} +1 -1
  13. package/dist/client/assets/{chunk-L5ZTLDWV-5L-9s_-b.js → chunk-L5ZTLDWV-CBk5m6ON.js} +1 -1
  14. package/dist/client/assets/{chunk-LZXEDZCA-XcqcrPu3.js → chunk-LZXEDZCA-CaexlXfB.js} +2 -2
  15. package/dist/client/assets/{chunk-ND2GUHAM-Cehi3-t2.js → chunk-ND2GUHAM-ma7eMIEB.js} +1 -1
  16. package/dist/client/assets/{chunk-NZK2D7GU-s6NmMa8K.js → chunk-NZK2D7GU-Da9F4EmF.js} +1 -1
  17. package/dist/client/assets/{chunk-O5CBEL6O-CkU-v8ar.js → chunk-O5CBEL6O-BLScYRER.js} +1 -1
  18. package/dist/client/assets/{chunk-WU5MYG2G-DJrhJw82.js → chunk-WU5MYG2G-r_Zx_ykt.js} +1 -1
  19. package/dist/client/assets/classDiagram-4FO5ZUOK-ALbA3AZT.js +1 -0
  20. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-CRtCF2Am.js +1 -0
  21. package/dist/client/assets/{dagre-BM42HDAG-B-PypVRB.js → dagre-BM42HDAG-CQZZ6fkN.js} +1 -1
  22. package/dist/client/assets/{diagram-2AECGRRQ-Cm3pGoMu.js → diagram-2AECGRRQ-8fz_9Wj4.js} +1 -1
  23. package/dist/client/assets/{diagram-5GNKFQAL-DCwV7VX3.js → diagram-5GNKFQAL-D2QIzYwx.js} +1 -1
  24. package/dist/client/assets/{diagram-KO2AKTUF-B5RVwHze.js → diagram-KO2AKTUF-Dg7-VWuS.js} +1 -1
  25. package/dist/client/assets/{diagram-LMA3HP47-CmLaK5_k.js → diagram-LMA3HP47-CyvkOeSI.js} +1 -1
  26. package/dist/client/assets/{diagram-OG6HWLK6-Mj0hhakE.js → diagram-OG6HWLK6-C6TmKjxR.js} +1 -1
  27. package/dist/client/assets/{erDiagram-TEJ5UH35-D30rUvl5.js → erDiagram-TEJ5UH35-KO_Kcloa.js} +1 -1
  28. package/dist/client/assets/{flowDiagram-I6XJVG4X-Dh1RDWX7.js → flowDiagram-I6XJVG4X-BYDZ3qQE.js} +1 -1
  29. package/dist/client/assets/{ganttDiagram-6RSMTGT7-B7f77DA_.js → ganttDiagram-6RSMTGT7-BLDujCf9.js} +1 -1
  30. package/dist/client/assets/{gitGraphDiagram-PVQCEYII-CKBXqaKU.js → gitGraphDiagram-PVQCEYII-B-zvAAKD.js} +1 -1
  31. package/dist/client/assets/{index-Cs7XmROA.js → index-C5vJR8lI.js} +114 -114
  32. package/dist/client/assets/index-mgNREnGE.css +2 -0
  33. package/dist/client/assets/{infoDiagram-5YYISTIA-BPpU75Ej.js → infoDiagram-5YYISTIA-BZadMEs3.js} +1 -1
  34. package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-C4J2Eb6K.js → ishikawaDiagram-YF4QCWOH-BnuStPyf.js} +1 -1
  35. package/dist/client/assets/{journeyDiagram-JHISSGLW-BthEH9FE.js → journeyDiagram-JHISSGLW-13FrVYMv.js} +1 -1
  36. package/dist/client/assets/{kanban-definition-UN3LZRKU-DzSlAH8r.js → kanban-definition-UN3LZRKU-B4yGkr5P.js} +1 -1
  37. package/dist/client/assets/{line-BTeJbdYR.js → line-D3D8vY5q.js} +1 -1
  38. package/dist/client/assets/{mermaid-parser.core-BJonz176.js → mermaid-parser.core-DE23MjEP.js} +1 -1
  39. package/dist/client/assets/{mermaid.core-tt3Uc0ap.js → mermaid.core-C47RYkRV.js} +3 -3
  40. package/dist/client/assets/{mindmap-definition-RKZ34NQL-iITkeEX1.js → mindmap-definition-RKZ34NQL-H9vxHA6z.js} +1 -1
  41. package/dist/client/assets/{pieDiagram-4H26LBE5--CgS4G7C.js → pieDiagram-4H26LBE5-gtm2r2QV.js} +1 -1
  42. package/dist/client/assets/{quadrantDiagram-W4KKPZXB-Clq6DEX-.js → quadrantDiagram-W4KKPZXB-CvQ8DgZT.js} +1 -1
  43. package/dist/client/assets/{requirementDiagram-4Y6WPE33-r_apj3BK.js → requirementDiagram-4Y6WPE33-BJdKQQGp.js} +1 -1
  44. package/dist/client/assets/{sankeyDiagram-5OEKKPKP-2XqJHFyi.js → sankeyDiagram-5OEKKPKP-C8MvJ4qc.js} +1 -1
  45. package/dist/client/assets/{sequenceDiagram-3UESZ5HK-k4Qrdp_I.js → sequenceDiagram-3UESZ5HK-vnLK2sVC.js} +1 -1
  46. package/dist/client/assets/{stateDiagram-AJRCARHV-CMr088KL.js → stateDiagram-AJRCARHV-xmrmmwh0.js} +1 -1
  47. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-DCX_KXXR.js +1 -0
  48. package/dist/client/assets/{timeline-definition-PNZ67QCA-BpkNV3oH.js → timeline-definition-PNZ67QCA-BS6qgXzV.js} +1 -1
  49. package/dist/client/assets/{vennDiagram-CIIHVFJN-Bmte21GM.js → vennDiagram-CIIHVFJN-BY2_Ffta.js} +1 -1
  50. package/dist/client/assets/{wardleyDiagram-YWT4CUSO-CLJX-O9B.js → wardleyDiagram-YWT4CUSO-CkoA3xv4.js} +1 -1
  51. package/dist/client/assets/{xychartDiagram-2RQKCTM6-Bc_SL4rS.js → xychartDiagram-2RQKCTM6-DbdiR5DY.js} +1 -1
  52. package/dist/client/index.html +2 -2
  53. package/dist/server/db.js +22 -0
  54. package/dist/server/db.js.map +1 -1
  55. package/dist/server/index.js +115 -167
  56. package/dist/server/index.js.map +1 -1
  57. package/dist/server/lib/bundlePullRequests.js +47 -0
  58. package/dist/server/lib/bundlePullRequests.js.map +1 -1
  59. package/dist/server/lib/bundleReviewOperations.js +83 -0
  60. package/dist/server/lib/bundleReviewOperations.js.map +1 -0
  61. package/dist/server/lib/commitMessage.js +32 -0
  62. package/dist/server/lib/commitMessage.js.map +1 -0
  63. package/dist/server/lib/integrations/github.js +7 -0
  64. package/dist/server/lib/integrations/github.js.map +1 -1
  65. package/dist/server/lib/projects.js +44 -0
  66. package/dist/server/lib/projects.js.map +1 -1
  67. package/dist/server/lib/pullRequestContent.js +77 -0
  68. package/dist/server/lib/pullRequestContent.js.map +1 -0
  69. package/dist/server/lib/ticketAutomation.js +194 -0
  70. package/dist/server/lib/ticketAutomation.js.map +1 -0
  71. package/dist/server/lib/ticketAutomationCommit.js +188 -0
  72. package/dist/server/lib/ticketAutomationCommit.js.map +1 -0
  73. package/dist/server/lib/ticketSettingsOperations.js +35 -2
  74. package/dist/server/lib/ticketSettingsOperations.js.map +1 -1
  75. package/dist/server/lib/ticketWorkflowOperations.js +11 -0
  76. package/dist/server/lib/ticketWorkflowOperations.js.map +1 -1
  77. package/dist/server/lib/tickets.js +31 -1
  78. package/dist/server/lib/tickets.js.map +1 -1
  79. package/dist/server/workers/build.js +11 -0
  80. package/dist/server/workers/build.js.map +1 -1
  81. package/dist/server/workers/followUp.js +30 -3
  82. package/dist/server/workers/followUp.js.map +1 -1
  83. package/dist/server/workers/review.js +78 -4
  84. package/dist/server/workers/review.js.map +1 -1
  85. package/package.json +1 -1
  86. package/dist/client/assets/channel-DmMIqd-b.js +0 -1
  87. package/dist/client/assets/classDiagram-4FO5ZUOK-CDTuTjGy.js +0 -1
  88. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-BhEXEoEw.js +0 -1
  89. package/dist/client/assets/index-ZyUvSLAF.css +0 -2
  90. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-t7kpZS05.js +0 -1
@@ -40,17 +40,20 @@ import { getProjectContextResponse, startProjectContextScan } from './lib/projec
40
40
  import { getProjectFileSuggestions } from './lib/projectFileSuggestions.js';
41
41
  import { updateProjectTargetSelection } from './lib/projectTargets.js';
42
42
  import { exportProjectMemoryArchive, importProjectMemoryArchive } from './lib/projectMemoryTransfer.js';
43
- import { buildReviewFindingsResponse, computeDiffSignature, getLatestReviewRunForContext } from './lib/reviewFindings.js';
43
+ import { buildReviewFindingsResponse, computeDiffSignature, getLatestReviewRunForContext, listReviewFindings } from './lib/reviewFindings.js';
44
+ import { clearAutomationPause, countBlockingFindings, depthAtLeast, getBundleAutomationState, resolveAutomationConfig, } from './lib/ticketAutomation.js';
45
+ import { runAutomationCommitChain } from './lib/ticketAutomationCommit.js';
44
46
  import { getDiscoveredSkills, normalizeSkillNames } from './lib/skills.js';
45
47
  import { clearAllPreviewProxies } from './lib/previewProxy.js';
46
48
  import { shutdownRealtimeServer } from './lib/shutdown.js';
47
49
  import { resolveClientBuildPaths, shouldServeClientApp, STATIC_CLIENT_DIRECTORY_OPTIONS, STATIC_CLIENT_INDEX_OPTIONS, } from './lib/staticClient.js';
48
50
  import { deleteIntegrationConnection, getGitHubConnectionConfig, getJiraConnectionConfig, listIntegrationConnections, upsertIntegrationConnection, } from './lib/integrations/index.js';
49
- import { createGitHubPullRequest, findOpenGitHubPullRequest, findOpenGitHubPullRequestByHeadBranch, getGitHubPullRequestComments, getGitHubPullRequestReviews, getGitHubPullRequest, GitHubApiError, hasGitHubRemoteBranch, resolveGitHubRepository, validateGitHubConnection, } from './lib/integrations/github.js';
51
+ import { createGitHubPullRequest, createOrFindGitHubPullRequest, findOpenGitHubPullRequest, findOpenGitHubPullRequestByHeadBranch, getGitHubPullRequestComments, getGitHubPullRequestReviews, getGitHubPullRequest, GitHubApiError, hasGitHubRemoteBranch, resolveGitHubRepository, validateGitHubConnection, } from './lib/integrations/github.js';
50
52
  import { importJiraIssue, JiraApiError, validateJiraConnection } from './lib/integrations/jira.js';
51
53
  import { importGitHubIssue } from './lib/integrations/github-issues.js';
52
54
  import { PlanningReferenceError, resolveExternalPlanningDescription } from './lib/integrations/planning.js';
53
- import { getBundlePullRequestRecord, hasOpenBundlePullRequest, serializeBundlePullRequest, shouldSyncBundlePullRequest, upsertBundlePullRequest, } from './lib/bundlePullRequests.js';
55
+ import { buildCommitMessagePrompt, normalizeGeneratedCommitMessage } from './lib/commitMessage.js';
56
+ import { BUNDLE_PULL_REQUEST_SYNC_TTL_MS, getBundlePullRequestRecord, hasOpenBundlePullRequest, serializeBundlePullRequest, shouldSyncBundlePullRequest, syncTrackedBundlePullRequestIfNeeded, upsertBundlePullRequest, upsertBundlePullRequestFromGitHub, } from './lib/bundlePullRequests.js';
54
57
  import { countInboundProjectLinks, getEffectiveProject, getProjectById, getProjectTicketStats, hasProjectRunIde, listProjects, replaceProjectLinks, serializeProject, updateProjectActiveTarget, validateProjectPayload, } from './lib/projects.js';
55
58
  import { getActiveRunStatus, getActiveRunStatusForProject, getPreviewStatusForProject, hasRunConfig, isRunning, openIde, openTicketIdeInWorkspace, resolveProjectRunContextKey, runSetupCommandsInWorkspace, runTicketInWorkspace, stopAllTickets, stopTicket } from './lib/run.js';
56
59
  import { clearAllTestSessions, createTestSession, deleteTestSession, discoverTestsForSession, doesTestFileExistForSession, findProjectTestCommand, getTestSession, isTestSelectionSupported, startTestSessionRun, stopTestSessionRun } from './lib/testSessions.js';
@@ -58,6 +61,7 @@ import { getBundleBuildBlockerResponse, shouldStartBundleBuildNow, transitionTic
58
61
  import { submitPlanTicketFollowUp, submitTicketFollowUp, } from './lib/ticketFollowUpOperations.js';
59
62
  import { canUseBuildControls, formatActiveRunConflict, getTicketRunReadinessError, resumeBuildTicketRun, runProjectTarget, runTicketRun, stopBuildTicketRun, stopProjectTargetRun, stopTicketRun, } from './lib/ticketRunOperations.js';
60
63
  import { isTicketAgentSettingsPayload, updateTicketAgentSettings, } from './lib/ticketSettingsOperations.js';
64
+ import { resolveBundleReviewOwner, transitionBundleToReview } from './lib/bundleReviewOperations.js';
61
65
  import { getActiveBundleStageChangeHead, undoTicketStage } from './lib/ticketUndo.js';
62
66
  import { closeAllTerminalSessions, createProjectTerminalSession, createTerminalSessionForWorkspace, destroyTerminalSessionById, registerTerminalSocketHandlers, } from './lib/terminal.js';
63
67
  import { countBundleTickets, createTicketRecord, createBundle, deleteBundle, ensureProjectRootBundle, autoParkTicketIfStale, autoParkSettledBundleTicket, isBundleSettledForAutoPark, clearAutoParkDismissalIfNeeded, assignTicketDoneOrder, assignTicketLaneOrder, getBundle, getBundleByName, getBundleByBranch, getBundleRepresentativeTicketContext, isProjectRootBundle, getTicket, isBundledTicket, listTicketsInRunContext, listBundles, listBoardTicketEnrichment, listTickets, resolveTicketBranch, resolveTicketTool, } from './lib/tickets.js';
@@ -279,7 +283,6 @@ const resolveRequestedSkills = (value) => normalizeSkillNames(value);
279
283
  const asTrimmedString = (value) => {
280
284
  return typeof value === 'string' ? value.trim() : '';
281
285
  };
282
- const COMMIT_MESSAGE_DIFF_CHAR_LIMIT = 12_000;
283
286
  const chatAttachmentUpload = multer({
284
287
  storage: multer.memoryStorage(),
285
288
  limits: {
@@ -316,36 +319,6 @@ const getSpecAttachmentUploadErrorMessage = (error) => {
316
319
  }
317
320
  return 'Unable to upload the selected images right now.';
318
321
  };
319
- const normalizeGeneratedCommitMessage = (value) => {
320
- const firstLine = value
321
- .replace(/\r/g, '\n')
322
- .split('\n')
323
- .map(line => line.trim())
324
- .find(line => line.length > 0) ?? '';
325
- return firstLine
326
- .replace(/^[-*]\s+/, '')
327
- .replace(/^[`"']+/, '')
328
- .replace(/[`"']+$/, '')
329
- .trim();
330
- };
331
- const buildCommitMessagePrompt = ({ commitMessageRules, selectedFiles, diff, }) => {
332
- const diffExcerpt = diff.length > COMMIT_MESSAGE_DIFF_CHAR_LIMIT
333
- ? `${diff.slice(0, COMMIT_MESSAGE_DIFF_CHAR_LIMIT)}\n\n[diff truncated for speed]`
334
- : diff;
335
- return [
336
- 'You generate git commit subjects quickly from a selected diff.',
337
- 'Prefer speed and a strong best-effort summary over perfect accuracy.',
338
- 'Return only the commit subject as a single line. No quotes. No markdown. No explanation.',
339
- 'Keep it concise and under 72 characters when practical.',
340
- commitMessageRules
341
- ? `Commit message rules:\n${commitMessageRules}`
342
- : 'Commit message rules:\nUse a clear default git subject line.',
343
- `Selected files:\n${selectedFiles.map(file => `- ${file}`).join('\n')}`,
344
- diffExcerpt.trim().length > 0
345
- ? `Selected diff:\n${diffExcerpt}`
346
- : 'Selected diff:\n[no diff excerpt available, infer from file names only]',
347
- ].join('\n\n');
348
- };
349
322
  const normalizeGeneratedPullRequestDescription = (value) => {
350
323
  const normalized = value
351
324
  .replace(/\r\n/g, '\n')
@@ -895,11 +868,6 @@ const listBundleConversationTickets = (projectId, bundleId) => {
895
868
  }))
896
869
  : [];
897
870
  };
898
- const moveBundleTicketToReview = (ticket) => {
899
- const laneOrder = assignTicketLaneOrder({ ...ticket, state: 'review' });
900
- db.prepare('UPDATE tickets SET state = ?, tool = ?, agent_status = NULL, streaming_response = NULL, lane_order = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run('review', resolveTicketTool(ticket, config.tool ?? 'opencode'), laneOrder ?? null, ticket.id);
901
- return getTicket(ticket.id);
902
- };
903
871
  const restoreBundleReviewTicketToBuild = (ticket) => {
904
872
  const laneOrder = assignTicketLaneOrder({ ...ticket, state: 'build' });
905
873
  db.prepare('UPDATE tickets SET state = ?, agent_status = ?, streaming_response = NULL, lane_order = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run('build', 'done', laneOrder ?? null, ticket.id);
@@ -979,7 +947,6 @@ const getBundleReviewState = (projectId, tickets) => {
979
947
  can_re_review: reviewState.can_re_review,
980
948
  };
981
949
  };
982
- const BUNDLE_PULL_REQUEST_SYNC_TTL_MS = 60 * 1000;
983
950
  const BUNDLE_PULL_REQUEST_DISCOVERY_MISS_COOLDOWN_MS = 10 * 60 * 1000;
984
951
  const BUNDLE_PULL_REQUEST_DISCOVERY_ERROR_COOLDOWN_MS = 2 * 60 * 1000;
985
952
  const BUNDLE_PULL_REQUEST_SYNC_ERROR_COOLDOWN_MS = 2 * 60 * 1000;
@@ -1086,33 +1053,6 @@ const serializeGitHubPullRequestSummary = (pullRequest) => {
1086
1053
  last_synced_at: null,
1087
1054
  };
1088
1055
  };
1089
- const createOrFindGitHubPullRequest = async (config, input) => {
1090
- const existingPullRequest = await findOpenGitHubPullRequest(config, {
1091
- headBranch: input.headBranch,
1092
- baseBranch: input.baseBranch,
1093
- });
1094
- return existingPullRequest ?? await createGitHubPullRequest(config, input);
1095
- };
1096
- const upsertBundlePullRequestFromGitHub = (projectId, bundleId, config, pullRequest) => {
1097
- return upsertBundlePullRequest({
1098
- bundleId,
1099
- projectId,
1100
- repoHost: config.host,
1101
- repoOwner: config.repo_owner,
1102
- repoName: config.repo_name,
1103
- number: pullRequest.number,
1104
- url: pullRequest.url,
1105
- title: pullRequest.title,
1106
- baseBranch: pullRequest.baseBranch,
1107
- headBranch: pullRequest.headBranch,
1108
- state: pullRequest.state,
1109
- isDraft: pullRequest.isDraft,
1110
- mergedAt: pullRequest.mergedAt,
1111
- closedAt: pullRequest.closedAt,
1112
- githubNodeId: pullRequest.nodeId,
1113
- lastSyncedAt: new Date().toISOString(),
1114
- });
1115
- };
1116
1056
  const discoverOpenBundlePullRequest = async (project, bundle) => {
1117
1057
  const githubConfig = getGitHubConnectionConfig(project.id);
1118
1058
  const existingRecord = getBundlePullRequestRecord(bundle.id, project.id);
@@ -1129,30 +1069,6 @@ const discoverOpenBundlePullRequest = async (project, bundle) => {
1129
1069
  console.info(`[integrations] github pr discovered project=${project.id} bundle=${bundle.id} pr=${discoveredPullRequest.number}`);
1130
1070
  return upsertBundlePullRequestFromGitHub(project.id, bundle.id, githubConfig, discoveredPullRequest);
1131
1071
  };
1132
- const syncTrackedBundlePullRequestIfNeeded = async (project, bundle, options = {}) => {
1133
- const force = options.force ?? false;
1134
- const rethrow = options.rethrow ?? false;
1135
- const githubConfig = getGitHubConnectionConfig(project.id);
1136
- const existingRecord = getBundlePullRequestRecord(bundle.id, project.id);
1137
- if (!githubConfig || !existingRecord) {
1138
- return existingRecord;
1139
- }
1140
- if (!force && !shouldSyncBundlePullRequest(existingRecord, BUNDLE_PULL_REQUEST_SYNC_TTL_MS)) {
1141
- return existingRecord;
1142
- }
1143
- console.info(`[integrations] github pr sync project=${project.id} bundle=${bundle.id} pr=${existingRecord.number}`);
1144
- try {
1145
- const nextPullRequest = await getGitHubPullRequest(githubConfig, existingRecord.number);
1146
- return upsertBundlePullRequestFromGitHub(project.id, bundle.id, githubConfig, nextPullRequest);
1147
- }
1148
- catch (error) {
1149
- console.warn(`[integrations] github pr sync failed project=${project.id} bundle=${bundle.id} pr=${existingRecord.number}`, error);
1150
- if (rethrow) {
1151
- throw error;
1152
- }
1153
- return existingRecord;
1154
- }
1155
- };
1156
1072
  const shouldStartTrackedBundlePullRequestSync = (project, bundle, options = {}) => {
1157
1073
  const force = options.force ?? false;
1158
1074
  const existingRecord = getBundlePullRequestRecord(bundle.id, project.id);
@@ -1390,35 +1306,6 @@ const submitBundleFollowUpResponse = (projectId, bundleId, payload, broadcaster)
1390
1306
  }),
1391
1307
  };
1392
1308
  };
1393
- const resolveBundleReviewOwner = (projectId, bundleId) => {
1394
- const tickets = listDetailedBundleTickets(projectId, bundleId);
1395
- const candidate = tickets.find(ticket => {
1396
- if (ticket.state !== 'review') {
1397
- return false;
1398
- }
1399
- const latestReviewHead = getActiveBundleStageChangeHead(ticket, 'review');
1400
- return latestReviewHead?.ticket_id === ticket.id;
1401
- });
1402
- if (candidate) {
1403
- return candidate;
1404
- }
1405
- return tickets.find(ticket => ticket.state === 'review' && ticket.undo?.stage === 'review') ?? null;
1406
- };
1407
- const getBundleReviewEligibilityError = (projectId, bundleId) => {
1408
- const buildTickets = listBundleSliceTickets(projectId, bundleId, 'build');
1409
- if (buildTickets.length === 0) {
1410
- const reviewTickets = listBundleSliceTickets(projectId, bundleId, 'review');
1411
- if (reviewTickets.some(ticket => ticket.can_re_review)) {
1412
- return null;
1413
- }
1414
- return 'No review tickets are available to re-run in this bundle';
1415
- }
1416
- const blockingTicket = buildTickets.find(ticket => ticket.agent_status !== 'done' && ticket.agent_status !== 'stopped');
1417
- if (blockingTicket) {
1418
- return 'All build tickets in the bundle must be done or stopped before starting review';
1419
- }
1420
- return null;
1421
- };
1422
1309
  const rerunTicketReview = (ticketId) => {
1423
1310
  const ticket = getTicket(ticketId);
1424
1311
  if (!ticket) {
@@ -1445,53 +1332,18 @@ const reviewBundleTickets = (projectId, bundleId) => {
1445
1332
  if (!bundle) {
1446
1333
  return { ok: false, status: 404, error: 'Bundle not found' };
1447
1334
  }
1448
- const eligibilityError = getBundleReviewEligibilityError(projectId, bundle.id);
1449
- if (eligibilityError) {
1450
- return { ok: false, status: 409, error: eligibilityError };
1451
- }
1452
- const buildTickets = listBundleSliceTickets(projectId, bundle.id, 'build');
1453
- if (buildTickets.length === 0) {
1454
- const reviewOwnerSource = resolveBundleReviewOwner(projectId, bundle.id)
1455
- ?? listBundleSliceTickets(projectId, bundle.id, 'review').find(ticket => ticket.can_re_review)
1456
- ?? null;
1457
- if (!reviewOwnerSource || !reviewOwnerSource.can_re_review) {
1458
- return { ok: false, status: 409, error: 'No review tickets are available to re-run in this bundle' };
1459
- }
1460
- const reviewOwner = getTicket(reviewOwnerSource.id);
1461
- if (!reviewOwner) {
1462
- return { ok: false, status: 409, error: 'Unable to start review for this bundle' };
1463
- }
1464
- startReview(reviewOwner, io);
1465
- return {
1466
- ok: true,
1467
- bundle,
1468
- reviewOwner,
1469
- details: serializeBundleDetails(project, bundle, {
1470
- review_owner_ticket_id: reviewOwner.id,
1471
- target_ticket_id: reviewOwner.id,
1472
- }),
1473
- };
1474
- }
1475
- const reviewOwnerSource = buildTickets[0];
1476
- if (!reviewOwnerSource) {
1477
- return { ok: false, status: 409, error: 'Unable to start review for this bundle' };
1478
- }
1479
- const movedTickets = buildTickets
1480
- .map(moveBundleTicketToReview)
1481
- .filter((ticket) => ticket !== null);
1482
- emitUpdatedTickets(movedTickets.map(ticket => ticket.id));
1483
- const reviewOwner = getTicket(reviewOwnerSource.id);
1484
- if (!reviewOwner) {
1485
- return { ok: false, status: 409, error: 'Unable to start review for this bundle' };
1335
+ const result = transitionBundleToReview(projectId, bundle.id, io);
1336
+ if (!result.ok) {
1337
+ return { ok: false, status: result.status, error: result.error };
1486
1338
  }
1487
- startReview(reviewOwner, io);
1339
+ const reviewOwner = result.value;
1488
1340
  return {
1489
1341
  ok: true,
1490
1342
  bundle,
1491
1343
  reviewOwner,
1492
1344
  details: serializeBundleDetails(project, bundle, {
1493
- review_owner_ticket_id: reviewOwner.id,
1494
- target_ticket_id: reviewOwner.id,
1345
+ review_owner_ticket_id: reviewOwner?.id,
1346
+ target_ticket_id: reviewOwner?.id,
1495
1347
  }),
1496
1348
  };
1497
1349
  };
@@ -3432,6 +3284,94 @@ app.post('/bundles/:id/follow-up', (req, res) => {
3432
3284
  }
3433
3285
  return res.status(response.status).json(response.value);
3434
3286
  });
3287
+ app.post('/bundles/:id/resume-automation', async (req, res) => {
3288
+ const result = getRequiredProject(req);
3289
+ if ('error' in result) {
3290
+ return res.status(400).json({ error: result.error });
3291
+ }
3292
+ const bundle = getBundle(req.params.id, result.project.id);
3293
+ if (!bundle) {
3294
+ return res.status(404).json({ error: 'Bundle not found' });
3295
+ }
3296
+ const bundleState = getBundleAutomationState(result.project.id, bundle.id);
3297
+ if (!bundleState || !bundleState.automation_paused_reason) {
3298
+ return res.status(409).json({ error: 'Bundle automation is not paused' });
3299
+ }
3300
+ clearAutomationPause(result.project.id, bundle.id);
3301
+ const reviewOwnerSource = resolveBundleReviewOwner(result.project.id, bundle.id)
3302
+ ?? listBundleSliceTickets(result.project.id, bundle.id, 'review')[0]
3303
+ ?? null;
3304
+ if (!reviewOwnerSource) {
3305
+ return res.status(409).json({ error: 'No review ticket to resume from' });
3306
+ }
3307
+ const owner = getTicket(reviewOwnerSource.id);
3308
+ if (!owner) {
3309
+ return res.status(409).json({ error: 'No review ticket to resume from' });
3310
+ }
3311
+ const project = getEffectiveProject(owner);
3312
+ if (!project) {
3313
+ return res.status(400).json({ error: 'Ticket project no longer exists' });
3314
+ }
3315
+ const automationConfig = resolveAutomationConfig(owner, project);
3316
+ if (owner.state === 'review' && owner.agent_status === 'running') {
3317
+ return res.status(409).json({ error: 'Review already running' });
3318
+ }
3319
+ if (owner.state === 'review' && owner.agent_status !== 'running') {
3320
+ const latestRun = getLatestReviewRunForContext({
3321
+ ticketId: owner.id,
3322
+ projectId: owner.project_id,
3323
+ worktreeBundleId: owner.worktree_bundle_id,
3324
+ });
3325
+ const findings = latestRun ? listReviewFindings(latestRun.id) : [];
3326
+ const blockingCount = countBlockingFindings(findings, automationConfig);
3327
+ const iteration = bundleState.review_iteration_count ?? 0;
3328
+ if (owner.can_re_review
3329
+ && automationConfig.reviewFixEnabled
3330
+ && blockingCount > 0
3331
+ && iteration < automationConfig.reviewMaxIterations) {
3332
+ 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');
3333
+ }
3334
+ else if (depthAtLeast(automationConfig.depth, 'auto_commit')) {
3335
+ await runAutomationCommitChain(owner, project, owner.bundle ?? null, automationConfig, io);
3336
+ }
3337
+ else {
3338
+ startReview(owner, io);
3339
+ }
3340
+ }
3341
+ else if (owner.state === 'build' && owner.agent_status === 'done') {
3342
+ const reviewResult = reviewBundleTickets(result.project.id, bundle.id);
3343
+ if (!reviewResult.ok) {
3344
+ return res.status(reviewResult.status).json({ error: reviewResult.error });
3345
+ }
3346
+ }
3347
+ else {
3348
+ return res.status(409).json({ error: 'Ticket is not in a resumable state' });
3349
+ }
3350
+ const resumedOwner = getTicket(owner.id);
3351
+ if (resumedOwner) {
3352
+ io.emit('ticket:updated', resumedOwner);
3353
+ }
3354
+ return res.json(serializeBundleDetails(result.project, bundle));
3355
+ });
3356
+ app.post('/bundles/:id/dismiss-automation-pause', (req, res) => {
3357
+ const result = getRequiredProject(req);
3358
+ if ('error' in result) {
3359
+ return res.status(400).json({ error: result.error });
3360
+ }
3361
+ const bundle = getBundle(req.params.id, result.project.id);
3362
+ if (!bundle) {
3363
+ return res.status(404).json({ error: 'Bundle not found' });
3364
+ }
3365
+ const bundleState = getBundleAutomationState(result.project.id, bundle.id);
3366
+ if (!bundleState || !bundleState.automation_paused_reason) {
3367
+ return res.status(409).json({ error: 'Bundle automation is not paused' });
3368
+ }
3369
+ clearAutomationPause(result.project.id, bundle.id);
3370
+ for (const ticket of listDetailedBundleTickets(result.project.id, bundle.id)) {
3371
+ io.emit('ticket:updated', ticket);
3372
+ }
3373
+ return res.json(serializeBundleDetails(result.project, bundle));
3374
+ });
3435
3375
  app.get('/tool', (_req, res) => {
3436
3376
  res.json({ tool: config.tool ?? 'opencode' });
3437
3377
  });
@@ -3920,7 +3860,7 @@ app.post('/projects', (req, res) => {
3920
3860
  return res.status(400).json({ error: validation.error });
3921
3861
  }
3922
3862
  const projectCount = Number(db.prepare('SELECT COUNT(*) AS count FROM projects').get()?.count ?? 0);
3923
- 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, worktree_sync, } = validation.values;
3863
+ 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, worktree_sync, automation_depth, automation_review_fix_enabled, automation_review_severity, automation_review_max_iterations, } = validation.values;
3924
3864
  if (!repo_path) {
3925
3865
  return res.status(400).json({ error: 'repo_path must be a non-empty string' });
3926
3866
  }
@@ -3950,10 +3890,14 @@ app.post('/projects', (req, res) => {
3950
3890
  commit_message_rules,
3951
3891
  auto_park_stale_tickets,
3952
3892
  memory_enabled,
3953
- worktree_sync
3893
+ worktree_sync,
3894
+ automation_depth,
3895
+ automation_review_fix_enabled,
3896
+ automation_review_severity,
3897
+ automation_review_max_iterations
3954
3898
  )
3955
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
3956
- `).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, worktree_sync ?? null);
3899
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
3900
+ `).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, worktree_sync ?? null, automation_depth ?? 'manual', automation_review_fix_enabled ?? 0, automation_review_severity ?? 'medium', automation_review_max_iterations ?? 3);
3957
3901
  replaceProjectLinks(id, linked_project_ids ?? []);
3958
3902
  ensureProjectRootBundle(id);
3959
3903
  if (projectCount === 0) {
@@ -4928,7 +4872,7 @@ app.patch('/projects/:id', (req, res) => {
4928
4872
  });
4929
4873
  }
4930
4874
  }
4931
- 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, worktree_sync, } = validation.values;
4875
+ 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, worktree_sync, automation_depth, automation_review_fix_enabled, automation_review_severity, automation_review_max_iterations, } = validation.values;
4932
4876
  db.prepare(`
4933
4877
  UPDATE projects SET
4934
4878
  name = ?,
@@ -4946,9 +4890,13 @@ app.patch('/projects/:id', (req, res) => {
4946
4890
  auto_park_stale_tickets = ?,
4947
4891
  memory_enabled = ?,
4948
4892
  worktree_sync = ?,
4893
+ automation_depth = ?,
4894
+ automation_review_fix_enabled = ?,
4895
+ automation_review_severity = ?,
4896
+ automation_review_max_iterations = ?,
4949
4897
  updated_at = CURRENT_TIMESTAMP
4950
4898
  WHERE id = ?
4951
- `).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, 'worktree_sync') ? worktree_sync : project.worktree_sync, req.params.id);
4899
+ `).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, 'worktree_sync') ? worktree_sync : project.worktree_sync, 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);
4952
4900
  if (hasOwn(validation.values, 'linked_project_ids')) {
4953
4901
  replaceProjectLinks(project.id, linked_project_ids ?? []);
4954
4902
  }