@straiffi/archon 1.5.0 → 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 (110) hide show
  1. package/dist/client/assets/{GitHubActionsDialog-CQMqcAiI.js → GitHubActionsDialog-oMY61JXF.js} +1 -1
  2. package/dist/client/assets/{TestsDialog-iGukzvw2.js → TestsDialog-elElGi9L.js} +3 -4
  3. package/dist/client/assets/{architectureDiagram-3BPJPVTR-BG4rc0pD.js → architectureDiagram-3BPJPVTR-BNcdEMha.js} +1 -1
  4. package/dist/client/assets/{blockDiagram-GPEHLZMM-BifkewDx.js → blockDiagram-GPEHLZMM-xHTKb0-z.js} +1 -1
  5. package/dist/client/assets/{c4Diagram-AAUBKEIU-DEYVUDCs.js → c4Diagram-AAUBKEIU-kMngbYRg.js} +1 -1
  6. package/dist/client/assets/channel-DAW1OrrC.js +1 -0
  7. package/dist/client/assets/{chunk-2J33WTMH-DD50YIcA.js → chunk-2J33WTMH-BVZnJcYa.js} +1 -1
  8. package/dist/client/assets/{chunk-3OPIFGDE-DLbwwbdv.js → chunk-3OPIFGDE-Dq_H_MkG.js} +1 -1
  9. package/dist/client/assets/{chunk-5ZQYHXKU-BW-r3gXJ.js → chunk-5ZQYHXKU-_TwHLQwE.js} +1 -1
  10. package/dist/client/assets/{chunk-727SXJPM-B5C_ENX7.js → chunk-727SXJPM-CKkS_93w.js} +1 -1
  11. package/dist/client/assets/{chunk-AQP2D5EJ-BUT3wmDg.js → chunk-AQP2D5EJ-BHSSO0SM.js} +1 -1
  12. package/dist/client/assets/{chunk-CSCIHK7Q-BO7wWVh_.js → chunk-CSCIHK7Q-CgzsiMAL.js} +1 -1
  13. package/dist/client/assets/{chunk-KSCS5N6A-DUNFfKB2.js → chunk-KSCS5N6A-DM-rnicK.js} +1 -1
  14. package/dist/client/assets/{chunk-L5ZTLDWV-SYJQkU8_.js → chunk-L5ZTLDWV-CnMgoXZs.js} +1 -1
  15. package/dist/client/assets/{chunk-LZXEDZCA-DSVJIylK.js → chunk-LZXEDZCA-CCr2z1iU.js} +2 -2
  16. package/dist/client/assets/{chunk-ND2GUHAM-CxL5ue9A.js → chunk-ND2GUHAM-DtA6gjMn.js} +1 -1
  17. package/dist/client/assets/{chunk-NZK2D7GU-BwxeYeqR.js → chunk-NZK2D7GU-_8fmSSaf.js} +1 -1
  18. package/dist/client/assets/{chunk-O5CBEL6O-Covi8YcB.js → chunk-O5CBEL6O-B84elqwU.js} +1 -1
  19. package/dist/client/assets/{chunk-WU5MYG2G-DlQoHSB3.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-CuNExSrh.js → dagre-BM42HDAG-C-QssoNU.js} +1 -1
  23. package/dist/client/assets/{diagram-2AECGRRQ-Bk7EYe6Z.js → diagram-2AECGRRQ-8RpG5DGb.js} +1 -1
  24. package/dist/client/assets/{diagram-5GNKFQAL-koHJCfAk.js → diagram-5GNKFQAL-Dk2raJWq.js} +1 -1
  25. package/dist/client/assets/{diagram-KO2AKTUF-DI1Ecx54.js → diagram-KO2AKTUF-7iZuIIef.js} +1 -1
  26. package/dist/client/assets/{diagram-LMA3HP47-CmFk1MNf.js → diagram-LMA3HP47-JLaaaEE6.js} +1 -1
  27. package/dist/client/assets/{diagram-OG6HWLK6-YTiDn39n.js → diagram-OG6HWLK6-jeL3-Rd3.js} +1 -1
  28. package/dist/client/assets/{erDiagram-TEJ5UH35-DyWU43DX.js → erDiagram-TEJ5UH35-B6uFrMrQ.js} +1 -1
  29. package/dist/client/assets/{flowDiagram-I6XJVG4X-DgbU15ZJ.js → flowDiagram-I6XJVG4X-BFWnhmx2.js} +1 -1
  30. package/dist/client/assets/{ganttDiagram-6RSMTGT7-CT39xvdk.js → ganttDiagram-6RSMTGT7-nxON-689.js} +1 -1
  31. package/dist/client/assets/{gitGraphDiagram-PVQCEYII-DfK_1xRj.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-C7z_7194.js → infoDiagram-5YYISTIA-CAZHR2I8.js} +1 -1
  35. package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-DhJ7h17n.js → ishikawaDiagram-YF4QCWOH-DEe4SNzl.js} +1 -1
  36. package/dist/client/assets/{journeyDiagram-JHISSGLW-CtkhoMAA.js → journeyDiagram-JHISSGLW-CG2kTAj9.js} +1 -1
  37. package/dist/client/assets/{kanban-definition-UN3LZRKU-Bcas7cg9.js → kanban-definition-UN3LZRKU-DUWutAjA.js} +1 -1
  38. package/dist/client/assets/{line-WO_ReaCl.js → line-B1SFN8mz.js} +1 -1
  39. package/dist/client/assets/{mermaid-parser.core-WmRuf19A.js → mermaid-parser.core-C-dyz_nb.js} +1 -1
  40. package/dist/client/assets/{mermaid.core-BMdju4xN.js → mermaid.core-nm4BMZuF.js} +3 -3
  41. package/dist/client/assets/{mindmap-definition-RKZ34NQL-DLxsUQmn.js → mindmap-definition-RKZ34NQL-DZEd-RCz.js} +1 -1
  42. package/dist/client/assets/{pieDiagram-4H26LBE5-D-oaWpEg.js → pieDiagram-4H26LBE5-WXxrzUwX.js} +1 -1
  43. package/dist/client/assets/{quadrantDiagram-W4KKPZXB-B9krq8I-.js → quadrantDiagram-W4KKPZXB-BClH2kQx.js} +1 -1
  44. package/dist/client/assets/{requirementDiagram-4Y6WPE33-m2VnH3vv.js → requirementDiagram-4Y6WPE33-ByolUxfP.js} +1 -1
  45. package/dist/client/assets/{sankeyDiagram-5OEKKPKP-bCF9Ot9-.js → sankeyDiagram-5OEKKPKP-B2vcb3U1.js} +1 -1
  46. package/dist/client/assets/{sequenceDiagram-3UESZ5HK-C-4zDJcn.js → sequenceDiagram-3UESZ5HK-J8Nt0GXG.js} +1 -1
  47. package/dist/client/assets/{stateDiagram-AJRCARHV-CksbJK59.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-DDxpdMfo.js → timeline-definition-PNZ67QCA-CXebl6LI.js} +1 -1
  50. package/dist/client/assets/{vennDiagram-CIIHVFJN-BfQM7_zD.js → vennDiagram-CIIHVFJN-DLBqI39h.js} +1 -1
  51. package/dist/client/assets/{wardleyDiagram-YWT4CUSO-CuixXlAX.js → wardleyDiagram-YWT4CUSO-ku4fViV9.js} +1 -1
  52. package/dist/client/assets/{xychartDiagram-2RQKCTM6-53YB-tf6.js → xychartDiagram-2RQKCTM6-5Jh-bBeb.js} +1 -1
  53. package/dist/client/index.html +2 -2
  54. package/dist/server/db.js +288 -3
  55. package/dist/server/db.js.map +1 -1
  56. package/dist/server/index.js +323 -97
  57. package/dist/server/index.js.map +1 -1
  58. package/dist/server/lib/git.js +18 -0
  59. package/dist/server/lib/git.js.map +1 -1
  60. package/dist/server/lib/models.js +2 -38
  61. package/dist/server/lib/models.js.map +1 -1
  62. package/dist/server/lib/projectMemory.js +1073 -276
  63. package/dist/server/lib/projectMemory.js.map +1 -1
  64. package/dist/server/lib/projectMemoryCursor.js +27 -0
  65. package/dist/server/lib/projectMemoryCursor.js.map +1 -0
  66. package/dist/server/lib/projectMemoryEvents.js +24 -0
  67. package/dist/server/lib/projectMemoryEvents.js.map +1 -0
  68. package/dist/server/lib/projectMemoryLimits.js +61 -0
  69. package/dist/server/lib/projectMemoryLimits.js.map +1 -0
  70. package/dist/server/lib/projectMemoryPrompt.js +299 -110
  71. package/dist/server/lib/projectMemoryPrompt.js.map +1 -1
  72. package/dist/server/lib/projectMemoryScan.js +461 -493
  73. package/dist/server/lib/projectMemoryScan.js.map +1 -1
  74. package/dist/server/lib/projectMemorySelections.js +97 -0
  75. package/dist/server/lib/projectMemorySelections.js.map +1 -0
  76. package/dist/server/lib/projectMemorySuggestions.js +19 -4
  77. package/dist/server/lib/projectMemorySuggestions.js.map +1 -1
  78. package/dist/server/lib/projectMemoryTransfer.js +635 -83
  79. package/dist/server/lib/projectMemoryTransfer.js.map +1 -1
  80. package/dist/server/lib/projectPathGlob.js +40 -0
  81. package/dist/server/lib/projectPathGlob.js.map +1 -0
  82. package/dist/server/lib/projects.js +50 -0
  83. package/dist/server/lib/projects.js.map +1 -1
  84. package/dist/server/lib/specReviewContext.js +2 -1
  85. package/dist/server/lib/specReviewContext.js.map +1 -1
  86. package/dist/server/lib/tickets.js +6 -0
  87. package/dist/server/lib/tickets.js.map +1 -1
  88. package/dist/server/workers/build.js +6 -2
  89. package/dist/server/workers/build.js.map +1 -1
  90. package/dist/server/workers/chat.js +9 -6
  91. package/dist/server/workers/chat.js.map +1 -1
  92. package/dist/server/workers/followUp.js +15 -8
  93. package/dist/server/workers/followUp.js.map +1 -1
  94. package/dist/server/workers/plan.js +61 -11
  95. package/dist/server/workers/plan.js.map +1 -1
  96. package/dist/server/workers/planFollowUp.js +3 -0
  97. package/dist/server/workers/planFollowUp.js.map +1 -1
  98. package/dist/server/workers/review.js +26 -11
  99. package/dist/server/workers/review.js.map +1 -1
  100. package/dist/server/workers/specReview.js +10 -0
  101. package/dist/server/workers/specReview.js.map +1 -1
  102. package/dist/server/workers/specSession.js +5 -2
  103. package/dist/server/workers/specSession.js.map +1 -1
  104. package/package.json +1 -1
  105. package/dist/client/assets/channel-gy-JdvF3.js +0 -1
  106. package/dist/client/assets/classDiagram-4FO5ZUOK-B1Xs78Ge.js +0 -1
  107. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-BU_Soq1k.js +0 -1
  108. package/dist/client/assets/index-BCqJfUGA.css +0 -2
  109. package/dist/client/assets/index-C_XaTJyb.js +0 -261
  110. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-BvbN4CmJ.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;
@@ -1218,6 +1237,24 @@ const getBundleRecentCommitSubjects = (project, bundle) => {
1218
1237
  return [];
1219
1238
  }
1220
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
+ };
1221
1258
  const serializeBundleDetails = (project, bundle, extras = {}) => {
1222
1259
  const serializedBundle = serializeBundleRow(project.id, bundle);
1223
1260
  if (!serializedBundle) {
@@ -1234,6 +1271,7 @@ const serializeBundleDetails = (project, bundle, extras = {}) => {
1234
1271
  const bundleReviewState = getBundleReviewState(project.id, tickets);
1235
1272
  return {
1236
1273
  bundle: serializedBundle,
1274
+ rename_eligibility: resolveBundleRenameEligibility(project, bundle, tickets),
1237
1275
  tickets,
1238
1276
  execution_order_ticket_ids: tickets.map(ticket => ticket.id),
1239
1277
  latest_session_ticket_id: latestSessionTicket?.id ?? null,
@@ -3220,8 +3258,42 @@ app.patch('/bundles/:id', (req, res) => {
3220
3258
  if (!bundle) {
3221
3259
  return res.status(404).json({ error: 'Bundle not found' });
3222
3260
  }
3261
+ const hasNameUpdate = hasOwn(req.body, 'name');
3223
3262
  if (isProjectRootBundle(bundle)) {
3224
- 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
+ }
3225
3297
  }
3226
3298
  let automationDepth = bundle.automation_depth ?? null;
3227
3299
  if (hasOwn(req.body, 'automation_depth')) {
@@ -3251,12 +3323,31 @@ app.patch('/bundles/:id', (req, res) => {
3251
3323
  return res.status(400).json({ error: 'automation_review_fix_enabled must be a boolean' });
3252
3324
  }
3253
3325
  }
3254
- const updated = updateBundleAutomationConfig({
3255
- bundleId: bundle.id,
3256
- projectId: result.project.id,
3257
- automationDepth,
3258
- automationReviewFixEnabled,
3259
- });
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
+ }
3260
3351
  listBundleTickets(result.project.id, bundle.id).forEach(ticket => {
3261
3352
  io.emit('ticket:updated', ticket);
3262
3353
  });
@@ -4415,7 +4506,7 @@ app.post('/projects', (req, res) => {
4415
4506
  return res.status(400).json({ error: validation.error });
4416
4507
  }
4417
4508
  const projectCount = Number(db.prepare('SELECT COUNT(*) AS count FROM projects').get()?.count ?? 0);
4418
- 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;
4419
4510
  if (!repo_path) {
4420
4511
  return res.status(400).json({ error: 'repo_path must be a non-empty string' });
4421
4512
  }
@@ -4445,6 +4536,8 @@ app.post('/projects', (req, res) => {
4445
4536
  commit_message_rules,
4446
4537
  auto_park_stale_tickets,
4447
4538
  memory_enabled,
4539
+ scan_include_globs_json,
4540
+ scan_exclude_globs_json,
4448
4541
  worktree_sync,
4449
4542
  spec_aware_code_review_enabled,
4450
4543
  automation_depth,
@@ -4452,8 +4545,8 @@ app.post('/projects', (req, res) => {
4452
4545
  automation_review_severity,
4453
4546
  automation_review_max_iterations
4454
4547
  )
4455
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
4456
- `).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);
4457
4550
  replaceProjectLinks(id, linked_project_ids ?? []);
4458
4551
  ensureProjectRootBundle(id);
4459
4552
  if (projectCount === 0) {
@@ -4478,6 +4571,9 @@ app.get('/projects/:id/conventions', (req, res) => {
4478
4571
  const includeArchived = req.query.include_archived === 'true';
4479
4572
  return res.json({ conventions: listProjectConventions(result.project.id, { includeArchived }) });
4480
4573
  });
4574
+ const sendProjectMemoryValidationError = (res, validation) => {
4575
+ return res.status(validation.code === 'field_too_large' ? 413 : 400).json(validation);
4576
+ };
4481
4577
  app.post('/projects/:id/conventions', (req, res) => {
4482
4578
  const result = requireProjectMemoryProject(req.params.id);
4483
4579
  if ('error' in result) {
@@ -4485,7 +4581,7 @@ app.post('/projects/:id/conventions', (req, res) => {
4485
4581
  }
4486
4582
  const validation = validateProjectConventionPayload(req.body);
4487
4583
  if ('error' in validation) {
4488
- return res.status(400).json({ error: validation.error });
4584
+ return sendProjectMemoryValidationError(res, validation);
4489
4585
  }
4490
4586
  const created = createProjectConvention(result.project.id, {
4491
4587
  title: validation.values.title ?? '',
@@ -4493,6 +4589,8 @@ app.post('/projects/:id/conventions', (req, res) => {
4493
4589
  instruction: validation.values.instruction ?? '',
4494
4590
  rationale: validation.values.rationale ?? null,
4495
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,
4496
4594
  priority: validation.values.priority ?? 'normal',
4497
4595
  injection_mode: validation.values.injection_mode ?? 'relevant',
4498
4596
  });
@@ -4505,7 +4603,7 @@ app.patch('/projects/:id/conventions/:conventionId', (req, res) => {
4505
4603
  }
4506
4604
  const validation = validateProjectConventionPayload(req.body, { partial: true });
4507
4605
  if ('error' in validation) {
4508
- return res.status(400).json({ error: validation.error });
4606
+ return sendProjectMemoryValidationError(res, validation);
4509
4607
  }
4510
4608
  const updated = updateProjectConvention(result.project.id, req.params.conventionId, validation.values);
4511
4609
  if (!updated) {
@@ -4519,21 +4617,21 @@ app.post('/projects/:id/conventions/:conventionId/archive', (req, res) => {
4519
4617
  return res.status(result.status).json({ error: result.error });
4520
4618
  }
4521
4619
  const archived = archiveProjectConvention(result.project.id, req.params.conventionId);
4522
- if (!archived) {
4523
- 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' });
4524
4622
  }
4525
- return res.json(archived);
4623
+ return res.json(archived.memory);
4526
4624
  });
4527
4625
  app.post('/projects/:id/conventions/:conventionId/restore', (req, res) => {
4528
4626
  const result = requireProjectMemoryProject(req.params.id);
4529
4627
  if ('error' in result) {
4530
4628
  return res.status(result.status).json({ error: result.error });
4531
4629
  }
4532
- const restored = restoreProjectConvention(result.project.id, req.params.conventionId);
4533
- if (!restored) {
4534
- 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' });
4535
4633
  }
4536
- return res.json(restored);
4634
+ return res.json(restored.memory);
4537
4635
  });
4538
4636
  app.get('/projects/:id/decisions', (req, res) => {
4539
4637
  const result = requireProjectMemoryProject(req.params.id);
@@ -4550,7 +4648,7 @@ app.post('/projects/:id/decisions', (req, res) => {
4550
4648
  }
4551
4649
  const validation = validateProjectDecisionPayload(req.body);
4552
4650
  if ('error' in validation) {
4553
- return res.status(400).json({ error: validation.error });
4651
+ return sendProjectMemoryValidationError(res, validation);
4554
4652
  }
4555
4653
  const created = createProjectDecision(result.project.id, {
4556
4654
  title: validation.values.title ?? '',
@@ -4558,6 +4656,9 @@ app.post('/projects/:id/decisions', (req, res) => {
4558
4656
  decision: validation.values.decision ?? '',
4559
4657
  rationale: validation.values.rationale ?? null,
4560
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,
4561
4662
  });
4562
4663
  return res.status(201).json(created);
4563
4664
  });
@@ -4568,7 +4669,7 @@ app.patch('/projects/:id/decisions/:decisionId', (req, res) => {
4568
4669
  }
4569
4670
  const validation = validateProjectDecisionPayload(req.body, { partial: true });
4570
4671
  if ('error' in validation) {
4571
- return res.status(400).json({ error: validation.error });
4672
+ return sendProjectMemoryValidationError(res, validation);
4572
4673
  }
4573
4674
  const updated = updateProjectDecision(result.project.id, req.params.decisionId, validation.values);
4574
4675
  if (!updated) {
@@ -4582,32 +4683,39 @@ app.post('/projects/:id/decisions/:decisionId/archive', (req, res) => {
4582
4683
  return res.status(result.status).json({ error: result.error });
4583
4684
  }
4584
4685
  const archived = archiveProjectDecision(result.project.id, req.params.decisionId);
4585
- if (!archived) {
4586
- 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' });
4587
4688
  }
4588
- return res.json(archived);
4689
+ return res.json(archived.memory);
4589
4690
  });
4590
4691
  app.post('/projects/:id/decisions/:decisionId/supersede', (req, res) => {
4591
4692
  const result = requireProjectMemoryProject(req.params.id);
4592
4693
  if ('error' in result) {
4593
4694
  return res.status(result.status).json({ error: result.error });
4594
4695
  }
4595
- const superseded = supersedeProjectDecision(result.project.id, req.params.decisionId);
4596
- if (!superseded) {
4597
- 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);
4598
4700
  }
4599
- 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);
4600
4708
  });
4601
4709
  app.post('/projects/:id/decisions/:decisionId/restore', (req, res) => {
4602
4710
  const result = requireProjectMemoryProject(req.params.id);
4603
4711
  if ('error' in result) {
4604
4712
  return res.status(result.status).json({ error: result.error });
4605
4713
  }
4606
- const restored = restoreProjectDecision(result.project.id, req.params.decisionId);
4607
- if (!restored) {
4608
- 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' });
4609
4717
  }
4610
- return res.json(restored);
4718
+ return res.json(restored.memory);
4611
4719
  });
4612
4720
  app.get('/projects/:id/memory/relevant', (req, res) => {
4613
4721
  const result = requireProjectMemoryProject(req.params.id);
@@ -4623,6 +4731,7 @@ app.get('/projects/:id/memory/relevant', (req, res) => {
4623
4731
  return res.status(404).json({ error: 'Ticket not found' });
4624
4732
  }
4625
4733
  return res.json(selectRelevantProjectMemory(result.project.id, {
4734
+ flow: 'ticket_preview',
4626
4735
  stage: ticket.state,
4627
4736
  title: ticket.title,
4628
4737
  description: ticket.description,
@@ -4645,7 +4754,7 @@ app.post('/projects/:id/memory/import', (req, res) => {
4645
4754
  }
4646
4755
  const imported = importProjectMemoryArchive(result.project.id, req.body ?? null);
4647
4756
  if ('error' in imported) {
4648
- return res.status(400).json({ error: imported.error });
4757
+ return res.status(imported.kind === 'limit' ? 413 : 400).json(imported);
4649
4758
  }
4650
4759
  return res.json(imported.value);
4651
4760
  });
@@ -4673,6 +4782,29 @@ app.get('/projects/:id/context', (req, res) => {
4673
4782
  }
4674
4783
  return res.json(getProjectContextResponse(result.project.id));
4675
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
+ });
4676
4808
  app.get('/projects/:id/context/artifacts', (req, res) => {
4677
4809
  const result = requireProjectMemoryProject(req.params.id);
4678
4810
  if ('error' in result) {
@@ -4711,6 +4843,9 @@ app.get('/projects/:id/memories', (req, res) => {
4711
4843
  if (chatSessionId && !getProjectChatSession(result.project.id, chatSessionId)) {
4712
4844
  return res.status(404).json({ error: 'Chat session not found' });
4713
4845
  }
4846
+ if (specSessionId && !getProjectSpecSession(result.project.id, specSessionId)) {
4847
+ return res.status(404).json({ error: 'Spec session not found' });
4848
+ }
4714
4849
  const includeGeneratedContext = req.query.include_generated_context !== 'false';
4715
4850
  return res.json({
4716
4851
  memories: listProjectMemories(result.project.id, {
@@ -4719,6 +4854,7 @@ app.get('/projects/:id/memories', (req, res) => {
4719
4854
  sources: sourceQuery.values,
4720
4855
  ticketId: ticketId || null,
4721
4856
  chatSessionId: chatSessionId || null,
4857
+ specSessionId: specSessionId || null,
4722
4858
  }),
4723
4859
  generated_context: includeGeneratedContext
4724
4860
  ? {
@@ -4728,6 +4864,60 @@ app.get('/projects/:id/memories', (req, res) => {
4728
4864
  : null,
4729
4865
  });
4730
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
+ });
4731
4921
  app.post('/projects/:id/memories', (req, res) => {
4732
4922
  const result = requireProjectMemoryProject(req.params.id);
4733
4923
  if ('error' in result) {
@@ -4740,7 +4930,7 @@ app.post('/projects/:id/memories', (req, res) => {
4740
4930
  if (kind === 'decision') {
4741
4931
  const validation = validateProjectDecisionPayload(req.body ?? {});
4742
4932
  if ('error' in validation) {
4743
- return res.status(400).json({ error: validation.error });
4933
+ return sendProjectMemoryValidationError(res, validation);
4744
4934
  }
4745
4935
  const created = createProjectDecision(result.project.id, {
4746
4936
  title: validation.values.title ?? '',
@@ -4748,6 +4938,9 @@ app.post('/projects/:id/memories', (req, res) => {
4748
4938
  decision: validation.values.decision ?? '',
4749
4939
  rationale: validation.values.rationale ?? null,
4750
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,
4751
4944
  });
4752
4945
  if (!created) {
4753
4946
  return res.status(500).json({ error: 'Unable to create the project decision' });
@@ -4756,7 +4949,7 @@ app.post('/projects/:id/memories', (req, res) => {
4756
4949
  }
4757
4950
  const validation = validateProjectConventionPayload(req.body ?? {});
4758
4951
  if ('error' in validation) {
4759
- return res.status(400).json({ error: validation.error });
4952
+ return sendProjectMemoryValidationError(res, validation);
4760
4953
  }
4761
4954
  const created = createProjectConvention(result.project.id, {
4762
4955
  title: validation.values.title ?? '',
@@ -4764,6 +4957,8 @@ app.post('/projects/:id/memories', (req, res) => {
4764
4957
  instruction: validation.values.instruction ?? '',
4765
4958
  rationale: validation.values.rationale ?? null,
4766
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,
4767
4962
  priority: validation.values.priority ?? 'normal',
4768
4963
  injection_mode: validation.values.injection_mode ?? 'relevant',
4769
4964
  });
@@ -4787,7 +4982,7 @@ app.patch('/projects/:id/memories/:memoryId', (req, res) => {
4787
4982
  if (memory.kind === 'decision') {
4788
4983
  const validation = validateProjectDecisionPayload(req.body ?? {}, { partial: true });
4789
4984
  if ('error' in validation) {
4790
- return res.status(400).json({ error: validation.error });
4985
+ return sendProjectMemoryValidationError(res, validation);
4791
4986
  }
4792
4987
  const updated = updateProjectDecision(result.project.id, memory.id, validation.values);
4793
4988
  if (!updated) {
@@ -4797,7 +4992,7 @@ app.patch('/projects/:id/memories/:memoryId', (req, res) => {
4797
4992
  }
4798
4993
  const validation = validateProjectConventionPayload(req.body ?? {}, { partial: true });
4799
4994
  if ('error' in validation) {
4800
- return res.status(400).json({ error: validation.error });
4995
+ return sendProjectMemoryValidationError(res, validation);
4801
4996
  }
4802
4997
  const updated = updateProjectConvention(result.project.id, memory.id, validation.values);
4803
4998
  if (!updated) {
@@ -4814,21 +5009,21 @@ app.post('/projects/:id/memories/:memoryId/archive', (req, res) => {
4814
5009
  if (!memory) {
4815
5010
  return res.status(404).json({ error: 'Memory not found' });
4816
5011
  }
4817
- if (memory.source !== 'manual') {
5012
+ if (memory.state === 'suggested' || memory.state === 'dismissed' || memory.state === 'applied_to_existing') {
4818
5013
  return res.status(400).json({ error: 'Only saved memories can be archived' });
4819
5014
  }
4820
5015
  if (memory.kind === 'decision') {
4821
5016
  const updated = archiveProjectDecision(result.project.id, memory.id);
4822
- if (!updated) {
4823
- 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' });
4824
5019
  }
4825
- return res.json({ memory: serializeProjectMemoryFromDecisionValue(updated) });
5020
+ return res.json({ memory: serializeProjectMemoryFromDecisionValue(updated.memory) });
4826
5021
  }
4827
5022
  const updated = archiveProjectConvention(result.project.id, memory.id);
4828
- if (!updated) {
4829
- 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' });
4830
5025
  }
4831
- return res.json({ memory: serializeProjectMemoryFromConventionValue(updated) });
5026
+ return res.json({ memory: serializeProjectMemoryFromConventionValue(updated.memory) });
4832
5027
  });
4833
5028
  app.post('/projects/:id/memories/:memoryId/restore', (req, res) => {
4834
5029
  const result = requireProjectMemoryProject(req.params.id);
@@ -4839,21 +5034,21 @@ app.post('/projects/:id/memories/:memoryId/restore', (req, res) => {
4839
5034
  if (!memory) {
4840
5035
  return res.status(404).json({ error: 'Memory not found' });
4841
5036
  }
4842
- if (memory.source !== 'manual') {
5037
+ if (memory.state === 'suggested' || memory.state === 'dismissed' || memory.state === 'applied_to_existing') {
4843
5038
  return res.status(400).json({ error: 'Only saved memories can be restored' });
4844
5039
  }
4845
5040
  if (memory.kind === 'decision') {
4846
- const updated = restoreProjectDecision(result.project.id, memory.id);
4847
- if (!updated) {
4848
- 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' });
4849
5044
  }
4850
- return res.json({ memory: serializeProjectMemoryFromDecisionValue(updated) });
5045
+ return res.json({ memory: serializeProjectMemoryFromDecisionValue(updated.memory) });
4851
5046
  }
4852
- const updated = restoreProjectConvention(result.project.id, memory.id);
4853
- if (!updated) {
4854
- 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' });
4855
5050
  }
4856
- return res.json({ memory: serializeProjectMemoryFromConventionValue(updated) });
5051
+ return res.json({ memory: serializeProjectMemoryFromConventionValue(updated.memory) });
4857
5052
  });
4858
5053
  app.post('/projects/:id/memories/:memoryId/supersede', (req, res) => {
4859
5054
  const result = requireProjectMemoryProject(req.params.id);
@@ -4864,14 +5059,21 @@ app.post('/projects/:id/memories/:memoryId/supersede', (req, res) => {
4864
5059
  if (!memory) {
4865
5060
  return res.status(404).json({ error: 'Memory not found' });
4866
5061
  }
4867
- if (memory.source !== 'manual' || memory.kind !== 'decision') {
5062
+ if (memory.kind !== 'decision' || memory.state === 'suggested' || memory.state === 'dismissed' || memory.state === 'applied_to_existing') {
4868
5063
  return res.status(400).json({ error: 'Only saved decisions can be superseded' });
4869
5064
  }
4870
- const updated = supersedeProjectDecision(result.project.id, memory.id);
4871
- if (!updated) {
4872
- 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);
4873
5069
  }
4874
- return res.json({ memory: serializeProjectMemoryFromDecisionValue(updated) });
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' });
5075
+ }
5076
+ return res.json({ memory: serializeProjectMemoryFromDecisionValue(updated.memory) });
4875
5077
  });
4876
5078
  app.post('/projects/:id/memories/:memoryId/accept', (req, res) => {
4877
5079
  const result = requireProjectMemoryProject(req.params.id);
@@ -4886,11 +5088,14 @@ app.post('/projects/:id/memories/:memoryId/accept', (req, res) => {
4886
5088
  ? validateProjectDecisionPayload(req.body ?? {}, { partial: true })
4887
5089
  : validateProjectConventionPayload(req.body ?? {}, { partial: true });
4888
5090
  if ('error' in validation) {
4889
- return res.status(400).json({ error: validation.error });
5091
+ return sendProjectMemoryValidationError(res, validation);
4890
5092
  }
4891
- 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);
4892
5094
  if ('error' in accepted) {
4893
- 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' });
4894
5099
  }
4895
5100
  const memory = 'decision' in accepted
4896
5101
  ? serializeProjectMemoryFromDecisionValue(accepted.decision)
@@ -4905,11 +5110,12 @@ app.post('/projects/:id/memories/:memoryId/dismiss', (req, res) => {
4905
5110
  if ('error' in result) {
4906
5111
  return res.status(result.status).json({ error: result.error });
4907
5112
  }
4908
- const dismissed = dismissProjectMemorySuggestion(result.project.id, req.params.memoryId);
4909
- if (!dismissed) {
4910
- 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 });
4911
5117
  }
4912
- const memory = serializeProjectMemoryFromSuggestionValue(dismissed);
5118
+ const memory = serializeProjectMemoryFromSuggestionValue(dismissed.suggestion);
4913
5119
  if (!memory) {
4914
5120
  return res.status(400).json({ error: 'Only suggested memories can be dismissed' });
4915
5121
  }
@@ -4924,18 +5130,17 @@ app.post('/projects/:id/memories/:memoryId/apply-to-existing', (req, res) => {
4924
5130
  if (!suggestion) {
4925
5131
  return res.status(404).json({ error: 'Memory not found' });
4926
5132
  }
4927
- if (suggestion.kind !== 'decision') {
4928
- return res.status(400).json({ error: 'Only decision suggestions can update an existing memory' });
4929
- }
4930
5133
  const decisionId = typeof req.body?.memory_id === 'string' ? req.body.memory_id.trim() : '';
4931
5134
  if (!decisionId) {
4932
5135
  return res.status(400).json({ error: 'memory_id is required' });
4933
5136
  }
4934
- const validation = validateProjectDecisionPayload(req.body ?? {}, { partial: true });
5137
+ const validation = suggestion.kind === 'decision' ? validateProjectDecisionPayload(req.body ?? {}, { partial: true }) : validateProjectConventionPayload(req.body ?? {}, { partial: true });
4935
5138
  if ('error' in validation) {
4936
- return res.status(400).json({ error: validation.error });
5139
+ return sendProjectMemoryValidationError(res, validation);
4937
5140
  }
4938
- 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);
4939
5144
  if ('error' in updated) {
4940
5145
  return res.status(updated.status).json({ error: updated.error, suggestion: updated.suggestion ?? null });
4941
5146
  }
@@ -4989,11 +5194,11 @@ app.post('/projects/:id/memory/suggestions/:suggestionId/accept', (req, res) =>
4989
5194
  ? validateProjectDecisionPayload(req.body ?? {}, { partial: true })
4990
5195
  : validateProjectConventionPayload(req.body ?? {}, { partial: true });
4991
5196
  if ('error' in validation) {
4992
- return res.status(400).json({ error: validation.error });
5197
+ return sendProjectMemoryValidationError(res, validation);
4993
5198
  }
4994
- 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);
4995
5200
  if ('error' in accepted) {
4996
- 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 });
4997
5202
  }
4998
5203
  return res.json(accepted);
4999
5204
  });
@@ -5006,18 +5211,17 @@ app.post('/projects/:id/memory/suggestions/:suggestionId/update-existing', (req,
5006
5211
  if (!suggestion) {
5007
5212
  return res.status(404).json({ error: 'Suggestion not found' });
5008
5213
  }
5009
- if (suggestion.kind !== 'decision') {
5010
- return res.status(400).json({ error: 'Only decision suggestions can update an existing memory' });
5011
- }
5012
- 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() : '';
5013
5215
  if (!decisionId) {
5014
5216
  return res.status(400).json({ error: 'decision_id is required' });
5015
5217
  }
5016
- const validation = validateProjectDecisionPayload(req.body ?? {}, { partial: true });
5218
+ const validation = suggestion.kind === 'decision' ? validateProjectDecisionPayload(req.body ?? {}, { partial: true }) : validateProjectConventionPayload(req.body ?? {}, { partial: true });
5017
5219
  if ('error' in validation) {
5018
- return res.status(400).json({ error: validation.error });
5220
+ return sendProjectMemoryValidationError(res, validation);
5019
5221
  }
5020
- 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);
5021
5225
  if ('error' in updated) {
5022
5226
  return res.status(updated.status).json({ error: updated.error, suggestion: updated.suggestion ?? null });
5023
5227
  }
@@ -5028,11 +5232,31 @@ app.post('/projects/:id/memory/suggestions/:suggestionId/dismiss', (req, res) =>
5028
5232
  if ('error' in result) {
5029
5233
  return res.status(result.status).json({ error: result.error });
5030
5234
  }
5031
- const dismissed = dismissProjectMemorySuggestion(result.project.id, req.params.suggestionId);
5032
- if (!dismissed) {
5033
- 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 });
5239
+ }
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 });
5034
5246
  }
5035
- return res.json(dismissed);
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);
5036
5260
  });
5037
5261
  app.get('/projects/:id/branch', (req, res) => {
5038
5262
  const project = getProjectById(req.params.id);
@@ -5434,7 +5658,7 @@ app.patch('/projects/:id', (req, res) => {
5434
5658
  });
5435
5659
  }
5436
5660
  }
5437
- 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;
5438
5662
  db.prepare(`
5439
5663
  UPDATE projects SET
5440
5664
  name = ?,
@@ -5451,6 +5675,8 @@ app.patch('/projects/:id', (req, res) => {
5451
5675
  commit_message_rules = ?,
5452
5676
  auto_park_stale_tickets = ?,
5453
5677
  memory_enabled = ?,
5678
+ scan_include_globs_json = ?,
5679
+ scan_exclude_globs_json = ?,
5454
5680
  worktree_sync = ?,
5455
5681
  spec_aware_code_review_enabled = ?,
5456
5682
  automation_depth = ?,
@@ -5459,7 +5685,7 @@ app.patch('/projects/:id', (req, res) => {
5459
5685
  automation_review_max_iterations = ?,
5460
5686
  updated_at = CURRENT_TIMESTAMP
5461
5687
  WHERE id = ?
5462
- `).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);
5463
5689
  if (hasOwn(validation.values, 'linked_project_ids')) {
5464
5690
  replaceProjectLinks(project.id, linked_project_ids ?? []);
5465
5691
  }