@straiffi/archon 1.4.12 → 1.6.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 (130) hide show
  1. package/dist/client/assets/{GitHubActionsDialog-DcW7fUgz.js → GitHubActionsDialog-oMY61JXF.js} +1 -1
  2. package/dist/client/assets/{TestsDialog-xNEttQOx.js → TestsDialog-elElGi9L.js} +3 -4
  3. package/dist/client/assets/{architectureDiagram-3BPJPVTR-DHADo63Y.js → architectureDiagram-3BPJPVTR-BNcdEMha.js} +1 -1
  4. package/dist/client/assets/{blockDiagram-GPEHLZMM-D_LqGLuD.js → blockDiagram-GPEHLZMM-xHTKb0-z.js} +1 -1
  5. package/dist/client/assets/{c4Diagram-AAUBKEIU-mIqv7fCU.js → c4Diagram-AAUBKEIU-kMngbYRg.js} +1 -1
  6. package/dist/client/assets/channel-DAW1OrrC.js +1 -0
  7. package/dist/client/assets/{chunk-2J33WTMH-DP-b68f-.js → chunk-2J33WTMH-BVZnJcYa.js} +1 -1
  8. package/dist/client/assets/{chunk-3OPIFGDE-D4GWQbqc.js → chunk-3OPIFGDE-Dq_H_MkG.js} +1 -1
  9. package/dist/client/assets/{chunk-5ZQYHXKU-HftAkT9j.js → chunk-5ZQYHXKU-_TwHLQwE.js} +1 -1
  10. package/dist/client/assets/{chunk-727SXJPM-CvB0JQZP.js → chunk-727SXJPM-CKkS_93w.js} +1 -1
  11. package/dist/client/assets/{chunk-AQP2D5EJ-DSB28EAb.js → chunk-AQP2D5EJ-BHSSO0SM.js} +1 -1
  12. package/dist/client/assets/{chunk-CSCIHK7Q-DuSgBocb.js → chunk-CSCIHK7Q-CgzsiMAL.js} +1 -1
  13. package/dist/client/assets/{chunk-KSCS5N6A-cm_ppJ7v.js → chunk-KSCS5N6A-DM-rnicK.js} +1 -1
  14. package/dist/client/assets/{chunk-L5ZTLDWV-BzDHIV9v.js → chunk-L5ZTLDWV-CnMgoXZs.js} +1 -1
  15. package/dist/client/assets/{chunk-LZXEDZCA-BruMwvko.js → chunk-LZXEDZCA-CCr2z1iU.js} +2 -2
  16. package/dist/client/assets/{chunk-ND2GUHAM-DrnvodkL.js → chunk-ND2GUHAM-DtA6gjMn.js} +1 -1
  17. package/dist/client/assets/{chunk-NZK2D7GU-QVsa0LuF.js → chunk-NZK2D7GU-_8fmSSaf.js} +1 -1
  18. package/dist/client/assets/{chunk-O5CBEL6O-DJFhq2gr.js → chunk-O5CBEL6O-B84elqwU.js} +1 -1
  19. package/dist/client/assets/{chunk-WU5MYG2G-ieLjE2uQ.js → chunk-WU5MYG2G-DBvmvPPt.js} +1 -1
  20. package/dist/client/assets/classDiagram-4FO5ZUOK-BmFh-n6n.js +1 -0
  21. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-tA1aAbh4.js +1 -0
  22. package/dist/client/assets/{dagre-BM42HDAG-CFRh82aT.js → dagre-BM42HDAG-C-QssoNU.js} +1 -1
  23. package/dist/client/assets/{diagram-2AECGRRQ-CPk1WJvx.js → diagram-2AECGRRQ-8RpG5DGb.js} +1 -1
  24. package/dist/client/assets/{diagram-5GNKFQAL-DHMJ735r.js → diagram-5GNKFQAL-Dk2raJWq.js} +1 -1
  25. package/dist/client/assets/{diagram-KO2AKTUF-Cq9d2BWb.js → diagram-KO2AKTUF-7iZuIIef.js} +1 -1
  26. package/dist/client/assets/{diagram-LMA3HP47-CgWJTR_b.js → diagram-LMA3HP47-JLaaaEE6.js} +1 -1
  27. package/dist/client/assets/{diagram-OG6HWLK6-HoFYGw81.js → diagram-OG6HWLK6-jeL3-Rd3.js} +1 -1
  28. package/dist/client/assets/{erDiagram-TEJ5UH35-B7yuPZck.js → erDiagram-TEJ5UH35-B6uFrMrQ.js} +1 -1
  29. package/dist/client/assets/{flowDiagram-I6XJVG4X-DaXXRHQj.js → flowDiagram-I6XJVG4X-BFWnhmx2.js} +1 -1
  30. package/dist/client/assets/{ganttDiagram-6RSMTGT7-pKsFmtHB.js → ganttDiagram-6RSMTGT7-nxON-689.js} +1 -1
  31. package/dist/client/assets/{gitGraphDiagram-PVQCEYII-Dwx5kc1M.js → gitGraphDiagram-PVQCEYII-DEAsC0GZ.js} +1 -1
  32. package/dist/client/assets/index-BzDXl4AF.js +265 -0
  33. package/dist/client/assets/index-DSap5tSX.css +2 -0
  34. package/dist/client/assets/{infoDiagram-5YYISTIA-6Taupp-l.js → infoDiagram-5YYISTIA-CAZHR2I8.js} +1 -1
  35. package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-BBKjiFtW.js → ishikawaDiagram-YF4QCWOH-DEe4SNzl.js} +1 -1
  36. package/dist/client/assets/{journeyDiagram-JHISSGLW-BQQ1EKJ3.js → journeyDiagram-JHISSGLW-CG2kTAj9.js} +1 -1
  37. package/dist/client/assets/{kanban-definition-UN3LZRKU-BvsroFzw.js → kanban-definition-UN3LZRKU-DUWutAjA.js} +1 -1
  38. package/dist/client/assets/{line-C6BHsie7.js → line-B1SFN8mz.js} +1 -1
  39. package/dist/client/assets/{mermaid-parser.core-Cb4G8u3H.js → mermaid-parser.core-C-dyz_nb.js} +1 -1
  40. package/dist/client/assets/{mermaid.core-NCO8Z4wR.js → mermaid.core-nm4BMZuF.js} +3 -3
  41. package/dist/client/assets/{mindmap-definition-RKZ34NQL-DYzSjAq3.js → mindmap-definition-RKZ34NQL-DZEd-RCz.js} +1 -1
  42. package/dist/client/assets/{pieDiagram-4H26LBE5-CWMyFFuU.js → pieDiagram-4H26LBE5-WXxrzUwX.js} +1 -1
  43. package/dist/client/assets/{quadrantDiagram-W4KKPZXB-D1ifIucc.js → quadrantDiagram-W4KKPZXB-BClH2kQx.js} +1 -1
  44. package/dist/client/assets/{requirementDiagram-4Y6WPE33-DW68fC21.js → requirementDiagram-4Y6WPE33-ByolUxfP.js} +1 -1
  45. package/dist/client/assets/{sankeyDiagram-5OEKKPKP-DnnV47BL.js → sankeyDiagram-5OEKKPKP-B2vcb3U1.js} +1 -1
  46. package/dist/client/assets/{sequenceDiagram-3UESZ5HK-Dw-9ZBU_.js → sequenceDiagram-3UESZ5HK-J8Nt0GXG.js} +1 -1
  47. package/dist/client/assets/{stateDiagram-AJRCARHV-C7dkf8nk.js → stateDiagram-AJRCARHV-CrgZ4Uwa.js} +1 -1
  48. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-CUEBQOsD.js +1 -0
  49. package/dist/client/assets/{timeline-definition-PNZ67QCA-BmVfK_16.js → timeline-definition-PNZ67QCA-CXebl6LI.js} +1 -1
  50. package/dist/client/assets/{vennDiagram-CIIHVFJN-B9j3QbmD.js → vennDiagram-CIIHVFJN-DLBqI39h.js} +1 -1
  51. package/dist/client/assets/{wardleyDiagram-YWT4CUSO-C520auMy.js → wardleyDiagram-YWT4CUSO-ku4fViV9.js} +1 -1
  52. package/dist/client/assets/{xychartDiagram-2RQKCTM6-CPqUJuqs.js → xychartDiagram-2RQKCTM6-5Jh-bBeb.js} +1 -1
  53. package/dist/client/index.html +2 -2
  54. package/dist/server/db.js +291 -3
  55. package/dist/server/db.js.map +1 -1
  56. package/dist/server/index.js +339 -97
  57. package/dist/server/index.js.map +1 -1
  58. package/dist/server/lib/agent.js +853 -161
  59. package/dist/server/lib/agent.js.map +1 -1
  60. package/dist/server/lib/agentRuns.js +22 -0
  61. package/dist/server/lib/agentRuns.js.map +1 -1
  62. package/dist/server/lib/chatMessages.js +24 -8
  63. package/dist/server/lib/chatMessages.js.map +1 -1
  64. package/dist/server/lib/genericToolCall.js +151 -0
  65. package/dist/server/lib/genericToolCall.js.map +1 -0
  66. package/dist/server/lib/git.js +18 -0
  67. package/dist/server/lib/git.js.map +1 -1
  68. package/dist/server/lib/mobileAccess.js +45 -2
  69. package/dist/server/lib/mobileAccess.js.map +1 -1
  70. package/dist/server/lib/models.js +65 -48
  71. package/dist/server/lib/models.js.map +1 -1
  72. package/dist/server/lib/opencodeTaskSessions.js +216 -0
  73. package/dist/server/lib/opencodeTaskSessions.js.map +1 -0
  74. package/dist/server/lib/projectMemory.js +1073 -276
  75. package/dist/server/lib/projectMemory.js.map +1 -1
  76. package/dist/server/lib/projectMemoryCursor.js +27 -0
  77. package/dist/server/lib/projectMemoryCursor.js.map +1 -0
  78. package/dist/server/lib/projectMemoryEvents.js +24 -0
  79. package/dist/server/lib/projectMemoryEvents.js.map +1 -0
  80. package/dist/server/lib/projectMemoryLimits.js +61 -0
  81. package/dist/server/lib/projectMemoryLimits.js.map +1 -0
  82. package/dist/server/lib/projectMemoryPrompt.js +299 -110
  83. package/dist/server/lib/projectMemoryPrompt.js.map +1 -1
  84. package/dist/server/lib/projectMemoryScan.js +461 -493
  85. package/dist/server/lib/projectMemoryScan.js.map +1 -1
  86. package/dist/server/lib/projectMemorySelections.js +97 -0
  87. package/dist/server/lib/projectMemorySelections.js.map +1 -0
  88. package/dist/server/lib/projectMemorySuggestions.js +19 -4
  89. package/dist/server/lib/projectMemorySuggestions.js.map +1 -1
  90. package/dist/server/lib/projectMemoryTransfer.js +635 -83
  91. package/dist/server/lib/projectMemoryTransfer.js.map +1 -1
  92. package/dist/server/lib/projectPathGlob.js +40 -0
  93. package/dist/server/lib/projectPathGlob.js.map +1 -0
  94. package/dist/server/lib/projects.js +50 -0
  95. package/dist/server/lib/projects.js.map +1 -1
  96. package/dist/server/lib/promptSkills.js +5 -0
  97. package/dist/server/lib/promptSkills.js.map +1 -1
  98. package/dist/server/lib/specMessages.js +9 -6
  99. package/dist/server/lib/specMessages.js.map +1 -1
  100. package/dist/server/lib/specReviewContext.js +2 -1
  101. package/dist/server/lib/specReviewContext.js.map +1 -1
  102. package/dist/server/lib/ticketMessages.js +22 -7
  103. package/dist/server/lib/ticketMessages.js.map +1 -1
  104. package/dist/server/lib/tickets.js +6 -0
  105. package/dist/server/lib/tickets.js.map +1 -1
  106. package/dist/server/lib/transcriptRuns.js +85 -0
  107. package/dist/server/lib/transcriptRuns.js.map +1 -0
  108. package/dist/server/workers/build.js +18 -4
  109. package/dist/server/workers/build.js.map +1 -1
  110. package/dist/server/workers/chat.js +24 -7
  111. package/dist/server/workers/chat.js.map +1 -1
  112. package/dist/server/workers/followUp.js +27 -10
  113. package/dist/server/workers/followUp.js.map +1 -1
  114. package/dist/server/workers/plan.js +405 -69
  115. package/dist/server/workers/plan.js.map +1 -1
  116. package/dist/server/workers/planFollowUp.js +30 -7
  117. package/dist/server/workers/planFollowUp.js.map +1 -1
  118. package/dist/server/workers/review.js +76 -20
  119. package/dist/server/workers/review.js.map +1 -1
  120. package/dist/server/workers/specReview.js +10 -0
  121. package/dist/server/workers/specReview.js.map +1 -1
  122. package/dist/server/workers/specSession.js +37 -3
  123. package/dist/server/workers/specSession.js.map +1 -1
  124. package/package.json +1 -1
  125. package/dist/client/assets/channel-D7PWuiXU.js +0 -1
  126. package/dist/client/assets/classDiagram-4FO5ZUOK-BNIT931s.js +0 -1
  127. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-TRtc0GGS.js +0 -1
  128. package/dist/client/assets/index-5Eun897r.js +0 -260
  129. package/dist/client/assets/index-CWWEw76C.css +0 -2
  130. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-BFmoroJA.js +0 -1
@@ -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, 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, 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';
@@ -34,9 +34,12 @@ import { prepareTicketForBuild } from './lib/buildFlow.js';
34
34
  import { getBundlePlanMutationBlockerById, shouldBlockPlanForBundleMutationById, } from './lib/bundleActivity.js';
35
35
  import { getDiscoveredModels, normalizeModelId, normalizeTicketModelId } from './lib/models.js';
36
36
  import { upgradeTool, UpgradeInProgressError } from './lib/toolUpgrade.js';
37
- import { acceptProjectMemorySuggestion, applyProjectMemorySuggestionToDecision, archiveProjectConvention, archiveProjectDecision, createProjectConvention, createProjectDecision, dismissProjectMemorySuggestion, getProjectMemoryById, getProjectMemorySuggestionById, isProjectMemoryEnabled, listProjectConventions, listLatestSuccessfulProjectContextArtifacts, listProjectDecisions, listProjectMemories, listProjectMemorySuggestions, restoreProjectConvention, restoreProjectDecision, serializeProjectMemoryFromConventionValue, serializeProjectMemoryFromDecisionValue, serializeProjectMemoryFromSuggestionValue, supersedeProjectDecision, updateProjectConvention, updateProjectDecision, validateProjectConventionPayload, validateProjectDecisionPayload, } from './lib/projectMemory.js';
37
+ import { acceptProjectMemorySuggestion, applyProjectMemorySuggestionToDecision, applyProjectMemorySuggestionToConvention, archiveProjectConvention, archiveProjectDecision, createProjectConvention, createProjectDecision, dismissProjectMemorySuggestion, getProjectMemoryById, getProjectMemorySuggestionById, isProjectMemoryEnabled, listProjectConventions, listLatestSuccessfulProjectContextArtifacts, listProjectDecisions, listProjectMemories, listProjectMemorySuggestions, restoreProjectConvention, restoreProjectDecision, serializeProjectMemoryFromConventionValue, serializeProjectMemoryFromDecisionValue, serializeProjectMemoryFromSuggestionValue, supersedeProjectDecision, supersedeProjectDecisionFromSuggestion, updateProjectConvention, updateProjectDecision, validateProjectConventionPayload, validateProjectDecisionPayload, } from './lib/projectMemory.js';
38
38
  import { selectRelevantProjectMemory } from './lib/projectMemoryPrompt.js';
39
- import { getProjectContextResponse, startProjectContextScan } from './lib/projectMemoryScan.js';
39
+ import { getProjectContextResponse, getProjectContextScanDetail, getProjectContextScanHistory, startProjectContextScan, } from './lib/projectMemoryScan.js';
40
+ import { listProjectMemoryEvents } from './lib/projectMemoryEvents.js';
41
+ import { getProjectMemorySelectionForAgentRun, listProjectMemoryUsage } from './lib/projectMemorySelections.js';
42
+ import { decodeProjectMemoryCursor } from './lib/projectMemoryCursor.js';
40
43
  import { getProjectFileSuggestions } from './lib/projectFileSuggestions.js';
41
44
  import { updateProjectTargetSelection } from './lib/projectTargets.js';
42
45
  import { exportProjectMemoryArchive, importProjectMemoryArchive } from './lib/projectMemoryTransfer.js';
@@ -68,7 +71,7 @@ import { isTicketAgentSettingsPayload, updateTicketAgentSettings, } from './lib/
68
71
  import { resolveBundleReviewOwner, transitionBundleToReview } from './lib/bundleReviewOperations.js';
69
72
  import { getActiveBundleStageChangeHead, undoTicketStage } from './lib/ticketUndo.js';
70
73
  import { closeAllTerminalSessions, createProjectTerminalSession, createTerminalSessionForWorkspace, destroyTerminalSessionById, registerTerminalSocketHandlers, } from './lib/terminal.js';
71
- 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, updateBundleAutomationConfig, } from './lib/tickets.js';
74
+ 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, updateBundleAutomationConfig, updateBundleIdentity, } from './lib/tickets.js';
72
75
  import { createTicketMessage, upsertTicketDescriptionMessage } from './lib/ticketMessages.js';
73
76
  import { listAgentRunsForParent } from './lib/agentRuns.js';
74
77
  import { HttpError } from './lib/httpError.js';
@@ -92,7 +95,7 @@ const sanitizeProjectMemoryExportFileName = (projectName) => {
92
95
  .toLowerCase()
93
96
  .replace(/[^a-z0-9]+/g, '-')
94
97
  .replace(/^-+|-+$/g, '');
95
- return `${normalized || 'project'}-archon-memory-v3.json`;
98
+ return `${normalized || 'project'}-archon-memory-v5.json`;
96
99
  };
97
100
  const requireExistingProject = (projectId) => {
98
101
  const project = getProjectById(projectId);
@@ -424,19 +427,35 @@ const uniqueAutoBundleName = (projectId) => {
424
427
  }
425
428
  return candidate;
426
429
  };
427
- const hasBundleBranchCaseConflict = (branch, projectId) => {
430
+ const hasBundleBranchCaseConflict = (branch, projectId, excludedBundleId) => {
428
431
  const normalizedBranch = branch.toLowerCase();
429
- return listBundles(projectId).some(bundle => bundle.branch !== branch && bundle.branch.toLowerCase() === normalizedBranch);
432
+ return listBundles(projectId).some(bundle => bundle.id !== excludedBundleId
433
+ && bundle.branch !== branch
434
+ && bundle.branch.toLowerCase() === normalizedBranch);
430
435
  };
431
- const uniqueBundleBranchName = (name, projectId) => {
436
+ const uniqueBundleBranchName = (name, projectId, options = {}) => {
437
+ if (options.currentBundle && name === options.currentBundle.name) {
438
+ return options.currentBundle.branch;
439
+ }
432
440
  const base = slugify(name);
433
441
  let branch = base;
434
442
  let suffix = 2;
435
- while (getBundleByBranch(branch, projectId)) {
443
+ const conflictsWithBranch = (candidate) => {
444
+ const bundleConflict = getBundleByBranch(candidate, projectId);
445
+ if (bundleConflict && bundleConflict.id !== options.currentBundle?.id) {
446
+ return true;
447
+ }
448
+ if (!options.project) {
449
+ return false;
450
+ }
451
+ return hasKnownBranchRef(candidate, options.project)
452
+ || Boolean(resolveExistingWorktreePath(candidate, options.project));
453
+ };
454
+ while (conflictsWithBranch(branch)) {
436
455
  branch = `${base}-${suffix}`;
437
456
  suffix += 1;
438
457
  }
439
- if (hasBundleBranchCaseConflict(branch, projectId)) {
458
+ if (hasBundleBranchCaseConflict(branch, projectId, options.currentBundle?.id)) {
440
459
  return null;
441
460
  }
442
461
  return branch;
@@ -509,6 +528,8 @@ db.prepare("UPDATE tickets SET agent_status = 'error' WHERE agent_status = 'runn
509
528
  // otherwise be permanently uninterruptible (the stop endpoint returns 409).
510
529
  db.prepare("UPDATE spec_sessions SET agent_status = 'error' WHERE agent_status = 'running'").run();
511
530
  db.prepare("UPDATE chat_sessions SET agent_status = 'error' WHERE agent_status = 'running'").run();
531
+ // Live run rows belong to in-memory child processes and are never resumable after restart.
532
+ db.prepare('DELETE FROM agent_runs').run();
512
533
  stopAllActiveBuildSequences();
513
534
  const getRequestedProjectId = (req) => {
514
535
  if (typeof req.query.project_id === 'string' && req.query.project_id !== '') {
@@ -1216,6 +1237,24 @@ const getBundleRecentCommitSubjects = (project, bundle) => {
1216
1237
  return [];
1217
1238
  }
1218
1239
  };
1240
+ const resolveBundleRenameEligibility = (project, bundle, tickets = listBundleTickets(project.id, bundle.id)) => {
1241
+ if (isProjectRootBundle(bundle)) {
1242
+ return { eligible: false, reason: 'project_root' };
1243
+ }
1244
+ if (tickets.some(ticket => ticket.state !== 'plan')) {
1245
+ return { eligible: false, reason: 'tickets_not_plan' };
1246
+ }
1247
+ if (tickets.some(ticket => ticket.agent_status === 'running' || ticket.run_setup_status === 'preparing')) {
1248
+ return { eligible: false, reason: 'agent_running' };
1249
+ }
1250
+ if (resolveExistingWorktreePath(bundle.branch, project)) {
1251
+ return { eligible: false, reason: 'worktree_exists' };
1252
+ }
1253
+ if (hasKnownBranchRef(bundle.branch, project)) {
1254
+ return { eligible: false, reason: 'branch_exists' };
1255
+ }
1256
+ return { eligible: true, reason: null };
1257
+ };
1219
1258
  const serializeBundleDetails = (project, bundle, extras = {}) => {
1220
1259
  const serializedBundle = serializeBundleRow(project.id, bundle);
1221
1260
  if (!serializedBundle) {
@@ -1232,6 +1271,7 @@ const serializeBundleDetails = (project, bundle, extras = {}) => {
1232
1271
  const bundleReviewState = getBundleReviewState(project.id, tickets);
1233
1272
  return {
1234
1273
  bundle: serializedBundle,
1274
+ rename_eligibility: resolveBundleRenameEligibility(project, bundle, tickets),
1235
1275
  tickets,
1236
1276
  execution_order_ticket_ids: tickets.map(ticket => ticket.id),
1237
1277
  latest_session_ticket_id: latestSessionTicket?.id ?? null,
@@ -2006,6 +2046,13 @@ app.get('/projects/:id/chat-sessions/:sessionId', (req, res) => {
2006
2046
  }
2007
2047
  return res.json(refreshedChatSession);
2008
2048
  });
2049
+ app.get('/projects/:id/chat-sessions/:sessionId/agent-runs', (req, res) => {
2050
+ const chatSession = getProjectChatSession(req.params.id, req.params.sessionId);
2051
+ if (!chatSession) {
2052
+ return res.status(404).json({ error: 'Not found' });
2053
+ }
2054
+ return res.json({ runs: listAgentRunsForParent('chat', chatSession.id) });
2055
+ });
2009
2056
  app.patch('/projects/:id/chat-sessions/:sessionId', (req, res) => {
2010
2057
  const project = getProjectById(req.params.id);
2011
2058
  if (!project) {
@@ -2295,6 +2342,13 @@ app.get('/projects/:id/spec-sessions/:sessionId', (req, res) => {
2295
2342
  }
2296
2343
  return res.json(refreshedSpecSession);
2297
2344
  });
2345
+ app.get('/projects/:id/spec-sessions/:sessionId/agent-runs', (req, res) => {
2346
+ const specSession = getProjectSpecSession(req.params.id, req.params.sessionId);
2347
+ if (!specSession) {
2348
+ return res.status(404).json({ error: 'Not found' });
2349
+ }
2350
+ return res.json({ runs: listAgentRunsForParent('spec', specSession.id) });
2351
+ });
2298
2352
  app.patch('/projects/:id/spec-sessions/:sessionId', (req, res) => {
2299
2353
  const result = updateProjectSpecSessionMode({
2300
2354
  projectId: req.params.id,
@@ -3204,8 +3258,42 @@ app.patch('/bundles/:id', (req, res) => {
3204
3258
  if (!bundle) {
3205
3259
  return res.status(404).json({ error: 'Bundle not found' });
3206
3260
  }
3261
+ const hasNameUpdate = hasOwn(req.body, 'name');
3207
3262
  if (isProjectRootBundle(bundle)) {
3208
- return res.status(409).json({ error: 'Project root bundle automation cannot be overridden' });
3263
+ return res.status(409).json({
3264
+ error: hasNameUpdate
3265
+ ? 'Project root bundles cannot be renamed'
3266
+ : 'Project root bundle automation cannot be overridden',
3267
+ });
3268
+ }
3269
+ let nextName = bundle.name;
3270
+ let nextBranch = bundle.branch;
3271
+ if (hasNameUpdate) {
3272
+ if (typeof req.body.name !== 'string' || !req.body.name.trim()) {
3273
+ return res.status(400).json({ error: 'Bundle name must be a non-empty string' });
3274
+ }
3275
+ nextName = req.body.name.trim();
3276
+ if (nextName !== bundle.name) {
3277
+ const nameConflict = getBundleByName(nextName, result.project.id);
3278
+ if (nameConflict && nameConflict.id !== bundle.id) {
3279
+ return res.status(409).json({ error: 'Bundle name already exists in this project' });
3280
+ }
3281
+ const eligibility = resolveBundleRenameEligibility(result.project, bundle);
3282
+ if (!eligibility.eligible) {
3283
+ return res.status(409).json({
3284
+ error: 'Bundle cannot be renamed in its current state',
3285
+ reason: eligibility.reason,
3286
+ });
3287
+ }
3288
+ const branch = uniqueBundleBranchName(nextName, result.project.id, {
3289
+ currentBundle: bundle,
3290
+ project: result.project,
3291
+ });
3292
+ if (!branch) {
3293
+ return res.status(409).json({ error: 'Bundle branch already exists in this project with different casing' });
3294
+ }
3295
+ nextBranch = branch;
3296
+ }
3209
3297
  }
3210
3298
  let automationDepth = bundle.automation_depth ?? null;
3211
3299
  if (hasOwn(req.body, 'automation_depth')) {
@@ -3235,12 +3323,31 @@ app.patch('/bundles/:id', (req, res) => {
3235
3323
  return res.status(400).json({ error: 'automation_review_fix_enabled must be a boolean' });
3236
3324
  }
3237
3325
  }
3238
- const updated = updateBundleAutomationConfig({
3239
- bundleId: bundle.id,
3240
- projectId: result.project.id,
3241
- automationDepth,
3242
- automationReviewFixEnabled,
3243
- });
3326
+ let updated = null;
3327
+ try {
3328
+ updated = db.transaction(() => {
3329
+ if (nextName !== bundle.name) {
3330
+ updateBundleIdentity({
3331
+ bundleId: bundle.id,
3332
+ projectId: result.project.id,
3333
+ name: nextName,
3334
+ branch: nextBranch,
3335
+ });
3336
+ }
3337
+ return updateBundleAutomationConfig({
3338
+ bundleId: bundle.id,
3339
+ projectId: result.project.id,
3340
+ automationDepth,
3341
+ automationReviewFixEnabled,
3342
+ });
3343
+ })();
3344
+ }
3345
+ catch (error) {
3346
+ if (error instanceof Error && 'code' in error && String(error.code).startsWith('SQLITE_CONSTRAINT')) {
3347
+ return res.status(409).json({ error: 'Bundle name or branch already exists in this project' });
3348
+ }
3349
+ throw error;
3350
+ }
3244
3351
  listBundleTickets(result.project.id, bundle.id).forEach(ticket => {
3245
3352
  io.emit('ticket:updated', ticket);
3246
3353
  });
@@ -4399,7 +4506,7 @@ app.post('/projects', (req, res) => {
4399
4506
  return res.status(400).json({ error: validation.error });
4400
4507
  }
4401
4508
  const projectCount = Number(db.prepare('SELECT COUNT(*) AS count FROM projects').get()?.count ?? 0);
4402
- 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, spec_aware_code_review_enabled, automation_depth, automation_review_fix_enabled, automation_review_severity, automation_review_max_iterations, } = validation.values;
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;
4403
4510
  if (!repo_path) {
4404
4511
  return res.status(400).json({ error: 'repo_path must be a non-empty string' });
4405
4512
  }
@@ -4429,6 +4536,8 @@ app.post('/projects', (req, res) => {
4429
4536
  commit_message_rules,
4430
4537
  auto_park_stale_tickets,
4431
4538
  memory_enabled,
4539
+ scan_include_globs_json,
4540
+ scan_exclude_globs_json,
4432
4541
  worktree_sync,
4433
4542
  spec_aware_code_review_enabled,
4434
4543
  automation_depth,
@@ -4436,8 +4545,8 @@ app.post('/projects', (req, res) => {
4436
4545
  automation_review_severity,
4437
4546
  automation_review_max_iterations
4438
4547
  )
4439
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
4440
- `).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, spec_aware_code_review_enabled ?? 0, automation_depth ?? 'manual', automation_review_fix_enabled ?? 0, automation_review_severity ?? 'medium', automation_review_max_iterations ?? 3);
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);
4441
4550
  replaceProjectLinks(id, linked_project_ids ?? []);
4442
4551
  ensureProjectRootBundle(id);
4443
4552
  if (projectCount === 0) {
@@ -4462,6 +4571,9 @@ app.get('/projects/:id/conventions', (req, res) => {
4462
4571
  const includeArchived = req.query.include_archived === 'true';
4463
4572
  return res.json({ conventions: listProjectConventions(result.project.id, { includeArchived }) });
4464
4573
  });
4574
+ const sendProjectMemoryValidationError = (res, validation) => {
4575
+ return res.status(validation.code === 'field_too_large' ? 413 : 400).json(validation);
4576
+ };
4465
4577
  app.post('/projects/:id/conventions', (req, res) => {
4466
4578
  const result = requireProjectMemoryProject(req.params.id);
4467
4579
  if ('error' in result) {
@@ -4469,7 +4581,7 @@ app.post('/projects/:id/conventions', (req, res) => {
4469
4581
  }
4470
4582
  const validation = validateProjectConventionPayload(req.body);
4471
4583
  if ('error' in validation) {
4472
- return res.status(400).json({ error: validation.error });
4584
+ return sendProjectMemoryValidationError(res, validation);
4473
4585
  }
4474
4586
  const created = createProjectConvention(result.project.id, {
4475
4587
  title: validation.values.title ?? '',
@@ -4477,6 +4589,8 @@ app.post('/projects/:id/conventions', (req, res) => {
4477
4589
  instruction: validation.values.instruction ?? '',
4478
4590
  rationale: validation.values.rationale ?? null,
4479
4591
  stages_json: validation.values.stages_json ?? null,
4592
+ path_globs_json: validation.values.path_globs_json ?? null,
4593
+ component_tags_json: validation.values.component_tags_json ?? null,
4480
4594
  priority: validation.values.priority ?? 'normal',
4481
4595
  injection_mode: validation.values.injection_mode ?? 'relevant',
4482
4596
  });
@@ -4489,7 +4603,7 @@ app.patch('/projects/:id/conventions/:conventionId', (req, res) => {
4489
4603
  }
4490
4604
  const validation = validateProjectConventionPayload(req.body, { partial: true });
4491
4605
  if ('error' in validation) {
4492
- return res.status(400).json({ error: validation.error });
4606
+ return sendProjectMemoryValidationError(res, validation);
4493
4607
  }
4494
4608
  const updated = updateProjectConvention(result.project.id, req.params.conventionId, validation.values);
4495
4609
  if (!updated) {
@@ -4503,21 +4617,21 @@ app.post('/projects/:id/conventions/:conventionId/archive', (req, res) => {
4503
4617
  return res.status(result.status).json({ error: result.error });
4504
4618
  }
4505
4619
  const archived = archiveProjectConvention(result.project.id, req.params.conventionId);
4506
- if (!archived) {
4507
- return res.status(404).json({ error: 'Convention not found' });
4620
+ if (archived.status !== 200 || !archived.memory) {
4621
+ return res.status(archived.status).json({ error: archived.error ?? 'Convention lifecycle transition failed' });
4508
4622
  }
4509
- return res.json(archived);
4623
+ return res.json(archived.memory);
4510
4624
  });
4511
4625
  app.post('/projects/:id/conventions/:conventionId/restore', (req, res) => {
4512
4626
  const result = requireProjectMemoryProject(req.params.id);
4513
4627
  if ('error' in result) {
4514
4628
  return res.status(result.status).json({ error: result.error });
4515
4629
  }
4516
- const restored = restoreProjectConvention(result.project.id, req.params.conventionId);
4517
- if (!restored) {
4518
- return res.status(404).json({ error: 'Convention not found' });
4630
+ const restored = restoreProjectConvention(result.project.id, req.params.conventionId, req.body?.confirm_conflicts === true);
4631
+ if (restored.status !== 200 || !restored.memory) {
4632
+ return res.status(restored.status).json({ ...restored, error: restored.error ?? 'Convention lifecycle transition failed' });
4519
4633
  }
4520
- return res.json(restored);
4634
+ return res.json(restored.memory);
4521
4635
  });
4522
4636
  app.get('/projects/:id/decisions', (req, res) => {
4523
4637
  const result = requireProjectMemoryProject(req.params.id);
@@ -4534,7 +4648,7 @@ app.post('/projects/:id/decisions', (req, res) => {
4534
4648
  }
4535
4649
  const validation = validateProjectDecisionPayload(req.body);
4536
4650
  if ('error' in validation) {
4537
- return res.status(400).json({ error: validation.error });
4651
+ return sendProjectMemoryValidationError(res, validation);
4538
4652
  }
4539
4653
  const created = createProjectDecision(result.project.id, {
4540
4654
  title: validation.values.title ?? '',
@@ -4542,6 +4656,9 @@ app.post('/projects/:id/decisions', (req, res) => {
4542
4656
  decision: validation.values.decision ?? '',
4543
4657
  rationale: validation.values.rationale ?? null,
4544
4658
  implications_json: validation.values.implications_json ?? null,
4659
+ stages_json: validation.values.stages_json ?? null,
4660
+ path_globs_json: validation.values.path_globs_json ?? null,
4661
+ component_tags_json: validation.values.component_tags_json ?? null,
4545
4662
  });
4546
4663
  return res.status(201).json(created);
4547
4664
  });
@@ -4552,7 +4669,7 @@ app.patch('/projects/:id/decisions/:decisionId', (req, res) => {
4552
4669
  }
4553
4670
  const validation = validateProjectDecisionPayload(req.body, { partial: true });
4554
4671
  if ('error' in validation) {
4555
- return res.status(400).json({ error: validation.error });
4672
+ return sendProjectMemoryValidationError(res, validation);
4556
4673
  }
4557
4674
  const updated = updateProjectDecision(result.project.id, req.params.decisionId, validation.values);
4558
4675
  if (!updated) {
@@ -4566,32 +4683,39 @@ app.post('/projects/:id/decisions/:decisionId/archive', (req, res) => {
4566
4683
  return res.status(result.status).json({ error: result.error });
4567
4684
  }
4568
4685
  const archived = archiveProjectDecision(result.project.id, req.params.decisionId);
4569
- if (!archived) {
4570
- return res.status(404).json({ error: 'Decision not found' });
4686
+ if (archived.status !== 200 || !archived.memory) {
4687
+ return res.status(archived.status).json({ error: archived.error ?? 'Decision lifecycle transition failed' });
4571
4688
  }
4572
- return res.json(archived);
4689
+ return res.json(archived.memory);
4573
4690
  });
4574
4691
  app.post('/projects/:id/decisions/:decisionId/supersede', (req, res) => {
4575
4692
  const result = requireProjectMemoryProject(req.params.id);
4576
4693
  if ('error' in result) {
4577
4694
  return res.status(result.status).json({ error: result.error });
4578
4695
  }
4579
- const superseded = supersedeProjectDecision(result.project.id, req.params.decisionId);
4580
- if (!superseded) {
4581
- return res.status(404).json({ error: 'Decision not found' });
4696
+ const replacementDecisionId = typeof req.body?.replacement_decision_id === 'string' ? req.body.replacement_decision_id.trim() : '';
4697
+ let superseded;
4698
+ try {
4699
+ superseded = supersedeProjectDecision(result.project.id, req.params.decisionId, replacementDecisionId);
4582
4700
  }
4583
- return res.json(superseded);
4701
+ catch (error) {
4702
+ return res.status(500).json({ error: getErrorMessage(error) });
4703
+ }
4704
+ if (superseded.status !== 200 || !superseded.memory) {
4705
+ return res.status(superseded.status).json({ error: superseded.error ?? 'Decision lifecycle transition failed' });
4706
+ }
4707
+ return res.json(superseded.memory);
4584
4708
  });
4585
4709
  app.post('/projects/:id/decisions/:decisionId/restore', (req, res) => {
4586
4710
  const result = requireProjectMemoryProject(req.params.id);
4587
4711
  if ('error' in result) {
4588
4712
  return res.status(result.status).json({ error: result.error });
4589
4713
  }
4590
- const restored = restoreProjectDecision(result.project.id, req.params.decisionId);
4591
- if (!restored) {
4592
- return res.status(404).json({ error: 'Decision not found' });
4714
+ const restored = restoreProjectDecision(result.project.id, req.params.decisionId, req.body?.confirm_conflicts === true);
4715
+ if (restored.status !== 200 || !restored.memory) {
4716
+ return res.status(restored.status).json({ ...restored, error: restored.error ?? 'Decision lifecycle transition failed' });
4593
4717
  }
4594
- return res.json(restored);
4718
+ return res.json(restored.memory);
4595
4719
  });
4596
4720
  app.get('/projects/:id/memory/relevant', (req, res) => {
4597
4721
  const result = requireProjectMemoryProject(req.params.id);
@@ -4607,6 +4731,7 @@ app.get('/projects/:id/memory/relevant', (req, res) => {
4607
4731
  return res.status(404).json({ error: 'Ticket not found' });
4608
4732
  }
4609
4733
  return res.json(selectRelevantProjectMemory(result.project.id, {
4734
+ flow: 'ticket_preview',
4610
4735
  stage: ticket.state,
4611
4736
  title: ticket.title,
4612
4737
  description: ticket.description,
@@ -4629,7 +4754,7 @@ app.post('/projects/:id/memory/import', (req, res) => {
4629
4754
  }
4630
4755
  const imported = importProjectMemoryArchive(result.project.id, req.body ?? null);
4631
4756
  if ('error' in imported) {
4632
- return res.status(400).json({ error: imported.error });
4757
+ return res.status(imported.kind === 'limit' ? 413 : 400).json(imported);
4633
4758
  }
4634
4759
  return res.json(imported.value);
4635
4760
  });
@@ -4657,6 +4782,29 @@ app.get('/projects/:id/context', (req, res) => {
4657
4782
  }
4658
4783
  return res.json(getProjectContextResponse(result.project.id));
4659
4784
  });
4785
+ app.get('/projects/:id/context/scans', (req, res) => {
4786
+ const result = requireProjectMemoryProject(req.params.id);
4787
+ if ('error' in result) {
4788
+ return res.status(result.status).json({ error: result.error });
4789
+ }
4790
+ const limit = req.query.limit == null ? 20 : Number(req.query.limit);
4791
+ if (!Number.isInteger(limit) || limit < 1 || limit > 50) {
4792
+ return res.status(400).json({ error: 'limit must be an integer between 1 and 50' });
4793
+ }
4794
+ const cursor = typeof req.query.cursor === 'string' ? decodeProjectMemoryCursor(req.query.cursor) : null;
4795
+ if (req.query.cursor != null && !cursor) {
4796
+ return res.status(400).json({ error: 'cursor must be a valid project memory cursor' });
4797
+ }
4798
+ return res.json(getProjectContextScanHistory(result.project.id, { limit, cursor }));
4799
+ });
4800
+ app.get('/projects/:id/context/scans/:scanId', (req, res) => {
4801
+ const result = requireProjectMemoryProject(req.params.id);
4802
+ if ('error' in result) {
4803
+ return res.status(result.status).json({ error: result.error });
4804
+ }
4805
+ const detail = getProjectContextScanDetail(result.project.id, req.params.scanId);
4806
+ return detail ? res.json(detail) : res.status(404).json({ error: 'Project context scan not found' });
4807
+ });
4660
4808
  app.get('/projects/:id/context/artifacts', (req, res) => {
4661
4809
  const result = requireProjectMemoryProject(req.params.id);
4662
4810
  if ('error' in result) {
@@ -4695,6 +4843,9 @@ app.get('/projects/:id/memories', (req, res) => {
4695
4843
  if (chatSessionId && !getProjectChatSession(result.project.id, chatSessionId)) {
4696
4844
  return res.status(404).json({ error: 'Chat session not found' });
4697
4845
  }
4846
+ if (specSessionId && !getProjectSpecSession(result.project.id, specSessionId)) {
4847
+ return res.status(404).json({ error: 'Spec session not found' });
4848
+ }
4698
4849
  const includeGeneratedContext = req.query.include_generated_context !== 'false';
4699
4850
  return res.json({
4700
4851
  memories: listProjectMemories(result.project.id, {
@@ -4703,6 +4854,7 @@ app.get('/projects/:id/memories', (req, res) => {
4703
4854
  sources: sourceQuery.values,
4704
4855
  ticketId: ticketId || null,
4705
4856
  chatSessionId: chatSessionId || null,
4857
+ specSessionId: specSessionId || null,
4706
4858
  }),
4707
4859
  generated_context: includeGeneratedContext
4708
4860
  ? {
@@ -4712,6 +4864,60 @@ app.get('/projects/:id/memories', (req, res) => {
4712
4864
  : null,
4713
4865
  });
4714
4866
  });
4867
+ app.get('/projects/:id/memory/agent-runs/:agentRunId/selection', (req, res) => {
4868
+ const result = requireProjectMemoryProject(req.params.id);
4869
+ if ('error' in result) {
4870
+ return res.status(result.status).json({ error: result.error });
4871
+ }
4872
+ const selection = getProjectMemorySelectionForAgentRun(result.project.id, req.params.agentRunId);
4873
+ return selection ? res.json({ selection }) : res.status(404).json({ error: 'Memory selection not found' });
4874
+ });
4875
+ app.get('/projects/:id/memory/items/:kind/:memoryId/usage', (req, res) => {
4876
+ const result = requireProjectMemoryProject(req.params.id);
4877
+ if ('error' in result) {
4878
+ return res.status(result.status).json({ error: result.error });
4879
+ }
4880
+ if (req.params.kind !== 'decision' && req.params.kind !== 'convention') {
4881
+ return res.status(400).json({ error: 'kind must be one of decision or convention' });
4882
+ }
4883
+ const memory = getProjectMemoryById(result.project.id, req.params.memoryId);
4884
+ if (!memory || memory.kind !== req.params.kind || memory.state === 'suggested') {
4885
+ return res.status(404).json({ error: 'Memory not found' });
4886
+ }
4887
+ const limit = req.query.limit == null ? 20 : Number(req.query.limit);
4888
+ if (!Number.isInteger(limit) || limit < 1 || limit > 50) {
4889
+ return res.status(400).json({ error: 'limit must be an integer between 1 and 50' });
4890
+ }
4891
+ const cursor = typeof req.query.cursor === 'string' ? decodeProjectMemoryCursor(req.query.cursor) : null;
4892
+ if (req.query.cursor != null && !cursor) {
4893
+ return res.status(400).json({ error: 'cursor must be a valid project memory cursor' });
4894
+ }
4895
+ const page = listProjectMemoryUsage(result.project.id, req.params.kind, memory.id, { limit, cursor });
4896
+ return res.json({ usage: page.usage, next_cursor: page.nextCursor });
4897
+ });
4898
+ app.get('/projects/:id/memory/items/:kind/:memoryId/history', (req, res) => {
4899
+ const result = requireProjectMemoryProject(req.params.id);
4900
+ if ('error' in result) {
4901
+ return res.status(result.status).json({ error: result.error });
4902
+ }
4903
+ if (req.params.kind !== 'decision' && req.params.kind !== 'convention') {
4904
+ return res.status(400).json({ error: 'kind must be one of decision or convention' });
4905
+ }
4906
+ const memory = getProjectMemoryById(result.project.id, req.params.memoryId);
4907
+ if (!memory || memory.kind !== req.params.kind || memory.state === 'suggested') {
4908
+ return res.status(404).json({ error: 'Memory not found' });
4909
+ }
4910
+ const limit = req.query.limit == null ? 20 : Number(req.query.limit);
4911
+ if (!Number.isInteger(limit) || limit < 1 || limit > 50) {
4912
+ return res.status(400).json({ error: 'limit must be an integer between 1 and 50' });
4913
+ }
4914
+ const cursor = typeof req.query.cursor === 'string' ? decodeProjectMemoryCursor(req.query.cursor) : null;
4915
+ if (req.query.cursor != null && !cursor) {
4916
+ return res.status(400).json({ error: 'cursor must be a valid project memory cursor' });
4917
+ }
4918
+ const page = listProjectMemoryEvents(result.project.id, req.params.kind, memory.id, { limit, cursor });
4919
+ return res.json({ events: page.events, next_cursor: page.nextCursor });
4920
+ });
4715
4921
  app.post('/projects/:id/memories', (req, res) => {
4716
4922
  const result = requireProjectMemoryProject(req.params.id);
4717
4923
  if ('error' in result) {
@@ -4724,7 +4930,7 @@ app.post('/projects/:id/memories', (req, res) => {
4724
4930
  if (kind === 'decision') {
4725
4931
  const validation = validateProjectDecisionPayload(req.body ?? {});
4726
4932
  if ('error' in validation) {
4727
- return res.status(400).json({ error: validation.error });
4933
+ return sendProjectMemoryValidationError(res, validation);
4728
4934
  }
4729
4935
  const created = createProjectDecision(result.project.id, {
4730
4936
  title: validation.values.title ?? '',
@@ -4732,6 +4938,9 @@ app.post('/projects/:id/memories', (req, res) => {
4732
4938
  decision: validation.values.decision ?? '',
4733
4939
  rationale: validation.values.rationale ?? null,
4734
4940
  implications_json: validation.values.implications_json ?? null,
4941
+ stages_json: validation.values.stages_json ?? null,
4942
+ path_globs_json: validation.values.path_globs_json ?? null,
4943
+ component_tags_json: validation.values.component_tags_json ?? null,
4735
4944
  });
4736
4945
  if (!created) {
4737
4946
  return res.status(500).json({ error: 'Unable to create the project decision' });
@@ -4740,7 +4949,7 @@ app.post('/projects/:id/memories', (req, res) => {
4740
4949
  }
4741
4950
  const validation = validateProjectConventionPayload(req.body ?? {});
4742
4951
  if ('error' in validation) {
4743
- return res.status(400).json({ error: validation.error });
4952
+ return sendProjectMemoryValidationError(res, validation);
4744
4953
  }
4745
4954
  const created = createProjectConvention(result.project.id, {
4746
4955
  title: validation.values.title ?? '',
@@ -4748,6 +4957,8 @@ app.post('/projects/:id/memories', (req, res) => {
4748
4957
  instruction: validation.values.instruction ?? '',
4749
4958
  rationale: validation.values.rationale ?? null,
4750
4959
  stages_json: validation.values.stages_json ?? null,
4960
+ path_globs_json: validation.values.path_globs_json ?? null,
4961
+ component_tags_json: validation.values.component_tags_json ?? null,
4751
4962
  priority: validation.values.priority ?? 'normal',
4752
4963
  injection_mode: validation.values.injection_mode ?? 'relevant',
4753
4964
  });
@@ -4771,7 +4982,7 @@ app.patch('/projects/:id/memories/:memoryId', (req, res) => {
4771
4982
  if (memory.kind === 'decision') {
4772
4983
  const validation = validateProjectDecisionPayload(req.body ?? {}, { partial: true });
4773
4984
  if ('error' in validation) {
4774
- return res.status(400).json({ error: validation.error });
4985
+ return sendProjectMemoryValidationError(res, validation);
4775
4986
  }
4776
4987
  const updated = updateProjectDecision(result.project.id, memory.id, validation.values);
4777
4988
  if (!updated) {
@@ -4781,7 +4992,7 @@ app.patch('/projects/:id/memories/:memoryId', (req, res) => {
4781
4992
  }
4782
4993
  const validation = validateProjectConventionPayload(req.body ?? {}, { partial: true });
4783
4994
  if ('error' in validation) {
4784
- return res.status(400).json({ error: validation.error });
4995
+ return sendProjectMemoryValidationError(res, validation);
4785
4996
  }
4786
4997
  const updated = updateProjectConvention(result.project.id, memory.id, validation.values);
4787
4998
  if (!updated) {
@@ -4798,21 +5009,21 @@ app.post('/projects/:id/memories/:memoryId/archive', (req, res) => {
4798
5009
  if (!memory) {
4799
5010
  return res.status(404).json({ error: 'Memory not found' });
4800
5011
  }
4801
- if (memory.source !== 'manual') {
5012
+ if (memory.state === 'suggested' || memory.state === 'dismissed' || memory.state === 'applied_to_existing') {
4802
5013
  return res.status(400).json({ error: 'Only saved memories can be archived' });
4803
5014
  }
4804
5015
  if (memory.kind === 'decision') {
4805
5016
  const updated = archiveProjectDecision(result.project.id, memory.id);
4806
- if (!updated) {
4807
- return res.status(404).json({ error: 'Memory not found' });
5017
+ if (updated.status !== 200 || !updated.memory) {
5018
+ return res.status(updated.status).json({ error: updated.error ?? 'Memory lifecycle transition failed' });
4808
5019
  }
4809
- return res.json({ memory: serializeProjectMemoryFromDecisionValue(updated) });
5020
+ return res.json({ memory: serializeProjectMemoryFromDecisionValue(updated.memory) });
4810
5021
  }
4811
5022
  const updated = archiveProjectConvention(result.project.id, memory.id);
4812
- if (!updated) {
4813
- return res.status(404).json({ error: 'Memory not found' });
5023
+ if (updated.status !== 200 || !updated.memory) {
5024
+ return res.status(updated.status).json({ error: updated.error ?? 'Memory lifecycle transition failed' });
4814
5025
  }
4815
- return res.json({ memory: serializeProjectMemoryFromConventionValue(updated) });
5026
+ return res.json({ memory: serializeProjectMemoryFromConventionValue(updated.memory) });
4816
5027
  });
4817
5028
  app.post('/projects/:id/memories/:memoryId/restore', (req, res) => {
4818
5029
  const result = requireProjectMemoryProject(req.params.id);
@@ -4823,21 +5034,21 @@ app.post('/projects/:id/memories/:memoryId/restore', (req, res) => {
4823
5034
  if (!memory) {
4824
5035
  return res.status(404).json({ error: 'Memory not found' });
4825
5036
  }
4826
- if (memory.source !== 'manual') {
5037
+ if (memory.state === 'suggested' || memory.state === 'dismissed' || memory.state === 'applied_to_existing') {
4827
5038
  return res.status(400).json({ error: 'Only saved memories can be restored' });
4828
5039
  }
4829
5040
  if (memory.kind === 'decision') {
4830
- const updated = restoreProjectDecision(result.project.id, memory.id);
4831
- if (!updated) {
4832
- return res.status(404).json({ error: 'Memory not found' });
5041
+ const updated = restoreProjectDecision(result.project.id, memory.id, req.body?.confirm_conflicts === true);
5042
+ if (updated.status !== 200 || !updated.memory) {
5043
+ return res.status(updated.status).json({ ...updated, error: updated.error ?? 'Memory lifecycle transition failed' });
4833
5044
  }
4834
- return res.json({ memory: serializeProjectMemoryFromDecisionValue(updated) });
5045
+ return res.json({ memory: serializeProjectMemoryFromDecisionValue(updated.memory) });
4835
5046
  }
4836
- const updated = restoreProjectConvention(result.project.id, memory.id);
4837
- if (!updated) {
4838
- return res.status(404).json({ error: 'Memory not found' });
5047
+ const updated = restoreProjectConvention(result.project.id, memory.id, req.body?.confirm_conflicts === true);
5048
+ if (updated.status !== 200 || !updated.memory) {
5049
+ return res.status(updated.status).json({ ...updated, error: updated.error ?? 'Memory lifecycle transition failed' });
4839
5050
  }
4840
- return res.json({ memory: serializeProjectMemoryFromConventionValue(updated) });
5051
+ return res.json({ memory: serializeProjectMemoryFromConventionValue(updated.memory) });
4841
5052
  });
4842
5053
  app.post('/projects/:id/memories/:memoryId/supersede', (req, res) => {
4843
5054
  const result = requireProjectMemoryProject(req.params.id);
@@ -4848,14 +5059,21 @@ app.post('/projects/:id/memories/:memoryId/supersede', (req, res) => {
4848
5059
  if (!memory) {
4849
5060
  return res.status(404).json({ error: 'Memory not found' });
4850
5061
  }
4851
- if (memory.source !== 'manual' || memory.kind !== 'decision') {
5062
+ if (memory.kind !== 'decision' || memory.state === 'suggested' || memory.state === 'dismissed' || memory.state === 'applied_to_existing') {
4852
5063
  return res.status(400).json({ error: 'Only saved decisions can be superseded' });
4853
5064
  }
4854
- const updated = supersedeProjectDecision(result.project.id, memory.id);
4855
- if (!updated) {
4856
- return res.status(404).json({ error: 'Memory not found' });
5065
+ const replacementDecisionId = typeof req.body?.replacement_decision_id === 'string' ? req.body.replacement_decision_id.trim() : '';
5066
+ let updated;
5067
+ try {
5068
+ updated = supersedeProjectDecision(result.project.id, memory.id, replacementDecisionId);
5069
+ }
5070
+ catch (error) {
5071
+ return res.status(500).json({ error: getErrorMessage(error) });
5072
+ }
5073
+ if (updated.status !== 200 || !updated.memory) {
5074
+ return res.status(updated.status).json({ error: updated.error ?? 'Memory lifecycle transition failed' });
4857
5075
  }
4858
- return res.json({ memory: serializeProjectMemoryFromDecisionValue(updated) });
5076
+ return res.json({ memory: serializeProjectMemoryFromDecisionValue(updated.memory) });
4859
5077
  });
4860
5078
  app.post('/projects/:id/memories/:memoryId/accept', (req, res) => {
4861
5079
  const result = requireProjectMemoryProject(req.params.id);
@@ -4870,11 +5088,14 @@ app.post('/projects/:id/memories/:memoryId/accept', (req, res) => {
4870
5088
  ? validateProjectDecisionPayload(req.body ?? {}, { partial: true })
4871
5089
  : validateProjectConventionPayload(req.body ?? {}, { partial: true });
4872
5090
  if ('error' in validation) {
4873
- return res.status(400).json({ error: validation.error });
5091
+ return sendProjectMemoryValidationError(res, validation);
4874
5092
  }
4875
- const accepted = acceptProjectMemorySuggestion(result.project.id, req.params.memoryId, validation.values);
5093
+ const accepted = acceptProjectMemorySuggestion(result.project.id, req.params.memoryId, validation.values, req.body?.confirm_conflicts === true);
4876
5094
  if ('error' in accepted) {
4877
- return res.status(accepted.status).json({ error: accepted.error, suggestion: accepted.suggestion ?? null });
5095
+ return res.status(accepted.status).json({ ...accepted, suggestion: accepted.suggestion ?? null });
5096
+ }
5097
+ if (!('decision' in accepted) && !('convention' in accepted)) {
5098
+ return res.status(500).json({ error: 'Unable to accept the suggestion' });
4878
5099
  }
4879
5100
  const memory = 'decision' in accepted
4880
5101
  ? serializeProjectMemoryFromDecisionValue(accepted.decision)
@@ -4889,11 +5110,12 @@ app.post('/projects/:id/memories/:memoryId/dismiss', (req, res) => {
4889
5110
  if ('error' in result) {
4890
5111
  return res.status(result.status).json({ error: result.error });
4891
5112
  }
4892
- const dismissed = dismissProjectMemorySuggestion(result.project.id, req.params.memoryId);
4893
- if (!dismissed) {
4894
- return res.status(404).json({ error: 'Memory not found' });
5113
+ const reason = typeof req.body?.reason === 'string' ? req.body.reason : null;
5114
+ const dismissed = dismissProjectMemorySuggestion(result.project.id, req.params.memoryId, reason);
5115
+ if ('error' in dismissed) {
5116
+ return res.status(dismissed.status).json({ error: dismissed.error, suggestion: dismissed.suggestion ?? null });
4895
5117
  }
4896
- const memory = serializeProjectMemoryFromSuggestionValue(dismissed);
5118
+ const memory = serializeProjectMemoryFromSuggestionValue(dismissed.suggestion);
4897
5119
  if (!memory) {
4898
5120
  return res.status(400).json({ error: 'Only suggested memories can be dismissed' });
4899
5121
  }
@@ -4908,18 +5130,17 @@ app.post('/projects/:id/memories/:memoryId/apply-to-existing', (req, res) => {
4908
5130
  if (!suggestion) {
4909
5131
  return res.status(404).json({ error: 'Memory not found' });
4910
5132
  }
4911
- if (suggestion.kind !== 'decision') {
4912
- return res.status(400).json({ error: 'Only decision suggestions can update an existing memory' });
4913
- }
4914
5133
  const decisionId = typeof req.body?.memory_id === 'string' ? req.body.memory_id.trim() : '';
4915
5134
  if (!decisionId) {
4916
5135
  return res.status(400).json({ error: 'memory_id is required' });
4917
5136
  }
4918
- const validation = validateProjectDecisionPayload(req.body ?? {}, { partial: true });
5137
+ const validation = suggestion.kind === 'decision' ? validateProjectDecisionPayload(req.body ?? {}, { partial: true }) : validateProjectConventionPayload(req.body ?? {}, { partial: true });
4919
5138
  if ('error' in validation) {
4920
- return res.status(400).json({ error: validation.error });
5139
+ return sendProjectMemoryValidationError(res, validation);
4921
5140
  }
4922
- const updated = applyProjectMemorySuggestionToDecision(result.project.id, req.params.memoryId, decisionId, validation.values);
5141
+ const updated = suggestion.kind === 'decision'
5142
+ ? applyProjectMemorySuggestionToDecision(result.project.id, req.params.memoryId, decisionId, validation.values)
5143
+ : applyProjectMemorySuggestionToConvention(result.project.id, req.params.memoryId, decisionId, validation.values);
4923
5144
  if ('error' in updated) {
4924
5145
  return res.status(updated.status).json({ error: updated.error, suggestion: updated.suggestion ?? null });
4925
5146
  }
@@ -4973,11 +5194,11 @@ app.post('/projects/:id/memory/suggestions/:suggestionId/accept', (req, res) =>
4973
5194
  ? validateProjectDecisionPayload(req.body ?? {}, { partial: true })
4974
5195
  : validateProjectConventionPayload(req.body ?? {}, { partial: true });
4975
5196
  if ('error' in validation) {
4976
- return res.status(400).json({ error: validation.error });
5197
+ return sendProjectMemoryValidationError(res, validation);
4977
5198
  }
4978
- const accepted = acceptProjectMemorySuggestion(result.project.id, req.params.suggestionId, validation.values);
5199
+ const accepted = acceptProjectMemorySuggestion(result.project.id, req.params.suggestionId, validation.values, req.body?.confirm_conflicts === true);
4979
5200
  if ('error' in accepted) {
4980
- return res.status(accepted.status).json({ error: accepted.error, suggestion: accepted.suggestion ?? null });
5201
+ return res.status(accepted.status).json({ ...accepted, suggestion: accepted.suggestion ?? null });
4981
5202
  }
4982
5203
  return res.json(accepted);
4983
5204
  });
@@ -4990,18 +5211,17 @@ app.post('/projects/:id/memory/suggestions/:suggestionId/update-existing', (req,
4990
5211
  if (!suggestion) {
4991
5212
  return res.status(404).json({ error: 'Suggestion not found' });
4992
5213
  }
4993
- if (suggestion.kind !== 'decision') {
4994
- return res.status(400).json({ error: 'Only decision suggestions can update an existing memory' });
4995
- }
4996
- const decisionId = typeof req.body?.decision_id === 'string' ? req.body.decision_id.trim() : '';
5214
+ const decisionId = typeof req.body?.memory_id === 'string' ? req.body.memory_id.trim() : typeof req.body?.decision_id === 'string' ? req.body.decision_id.trim() : '';
4997
5215
  if (!decisionId) {
4998
5216
  return res.status(400).json({ error: 'decision_id is required' });
4999
5217
  }
5000
- const validation = validateProjectDecisionPayload(req.body ?? {}, { partial: true });
5218
+ const validation = suggestion.kind === 'decision' ? validateProjectDecisionPayload(req.body ?? {}, { partial: true }) : validateProjectConventionPayload(req.body ?? {}, { partial: true });
5001
5219
  if ('error' in validation) {
5002
- return res.status(400).json({ error: validation.error });
5220
+ return sendProjectMemoryValidationError(res, validation);
5003
5221
  }
5004
- const updated = applyProjectMemorySuggestionToDecision(result.project.id, req.params.suggestionId, decisionId, validation.values);
5222
+ const updated = suggestion.kind === 'decision'
5223
+ ? applyProjectMemorySuggestionToDecision(result.project.id, req.params.suggestionId, decisionId, validation.values)
5224
+ : applyProjectMemorySuggestionToConvention(result.project.id, req.params.suggestionId, decisionId, validation.values);
5005
5225
  if ('error' in updated) {
5006
5226
  return res.status(updated.status).json({ error: updated.error, suggestion: updated.suggestion ?? null });
5007
5227
  }
@@ -5012,11 +5232,31 @@ app.post('/projects/:id/memory/suggestions/:suggestionId/dismiss', (req, res) =>
5012
5232
  if ('error' in result) {
5013
5233
  return res.status(result.status).json({ error: result.error });
5014
5234
  }
5015
- const dismissed = dismissProjectMemorySuggestion(result.project.id, req.params.suggestionId);
5016
- if (!dismissed) {
5017
- return res.status(404).json({ error: 'Suggestion not found' });
5235
+ const reason = typeof req.body?.reason === 'string' ? req.body.reason : null;
5236
+ const dismissed = dismissProjectMemorySuggestion(result.project.id, req.params.suggestionId, reason);
5237
+ if ('error' in dismissed) {
5238
+ return res.status(dismissed.status).json({ error: dismissed.error, suggestion: dismissed.suggestion ?? null });
5018
5239
  }
5019
- return res.json(dismissed);
5240
+ return res.json(dismissed.suggestion);
5241
+ });
5242
+ app.post('/projects/:id/memory/suggestions/:suggestionId/supersede', (req, res) => {
5243
+ const result = requireProjectMemoryProject(req.params.id);
5244
+ if ('error' in result) {
5245
+ return res.status(result.status).json({ error: result.error });
5246
+ }
5247
+ const targetDecisionId = typeof req.body?.decision_id === 'string' ? req.body.decision_id.trim() : '';
5248
+ if (!targetDecisionId) {
5249
+ return res.status(400).json({ error: 'decision_id is required' });
5250
+ }
5251
+ const validation = validateProjectDecisionPayload(req.body ?? {}, { partial: true });
5252
+ if ('error' in validation) {
5253
+ return sendProjectMemoryValidationError(res, validation);
5254
+ }
5255
+ const superseded = supersedeProjectDecisionFromSuggestion(result.project.id, req.params.suggestionId, targetDecisionId, validation.values);
5256
+ if ('error' in superseded) {
5257
+ return res.status(superseded.status).json({ error: superseded.error, suggestion: superseded.suggestion ?? null });
5258
+ }
5259
+ return res.json(superseded);
5020
5260
  });
5021
5261
  app.get('/projects/:id/branch', (req, res) => {
5022
5262
  const project = getProjectById(req.params.id);
@@ -5418,7 +5658,7 @@ app.patch('/projects/:id', (req, res) => {
5418
5658
  });
5419
5659
  }
5420
5660
  }
5421
- 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, spec_aware_code_review_enabled, automation_depth, automation_review_fix_enabled, automation_review_severity, automation_review_max_iterations, } = validation.values;
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;
5422
5662
  db.prepare(`
5423
5663
  UPDATE projects SET
5424
5664
  name = ?,
@@ -5435,6 +5675,8 @@ app.patch('/projects/:id', (req, res) => {
5435
5675
  commit_message_rules = ?,
5436
5676
  auto_park_stale_tickets = ?,
5437
5677
  memory_enabled = ?,
5678
+ scan_include_globs_json = ?,
5679
+ scan_exclude_globs_json = ?,
5438
5680
  worktree_sync = ?,
5439
5681
  spec_aware_code_review_enabled = ?,
5440
5682
  automation_depth = ?,
@@ -5443,7 +5685,7 @@ app.patch('/projects/:id', (req, res) => {
5443
5685
  automation_review_max_iterations = ?,
5444
5686
  updated_at = CURRENT_TIMESTAMP
5445
5687
  WHERE id = ?
5446
- `).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, '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);
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);
5447
5689
  if (hasOwn(validation.values, 'linked_project_ids')) {
5448
5690
  replaceProjectLinks(project.id, linked_project_ids ?? []);
5449
5691
  }