@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.
- package/dist/client/assets/{GitHubActionsDialog-DcW7fUgz.js → GitHubActionsDialog-oMY61JXF.js} +1 -1
- package/dist/client/assets/{TestsDialog-xNEttQOx.js → TestsDialog-elElGi9L.js} +3 -4
- package/dist/client/assets/{architectureDiagram-3BPJPVTR-DHADo63Y.js → architectureDiagram-3BPJPVTR-BNcdEMha.js} +1 -1
- package/dist/client/assets/{blockDiagram-GPEHLZMM-D_LqGLuD.js → blockDiagram-GPEHLZMM-xHTKb0-z.js} +1 -1
- package/dist/client/assets/{c4Diagram-AAUBKEIU-mIqv7fCU.js → c4Diagram-AAUBKEIU-kMngbYRg.js} +1 -1
- package/dist/client/assets/channel-DAW1OrrC.js +1 -0
- package/dist/client/assets/{chunk-2J33WTMH-DP-b68f-.js → chunk-2J33WTMH-BVZnJcYa.js} +1 -1
- package/dist/client/assets/{chunk-3OPIFGDE-D4GWQbqc.js → chunk-3OPIFGDE-Dq_H_MkG.js} +1 -1
- package/dist/client/assets/{chunk-5ZQYHXKU-HftAkT9j.js → chunk-5ZQYHXKU-_TwHLQwE.js} +1 -1
- package/dist/client/assets/{chunk-727SXJPM-CvB0JQZP.js → chunk-727SXJPM-CKkS_93w.js} +1 -1
- package/dist/client/assets/{chunk-AQP2D5EJ-DSB28EAb.js → chunk-AQP2D5EJ-BHSSO0SM.js} +1 -1
- package/dist/client/assets/{chunk-CSCIHK7Q-DuSgBocb.js → chunk-CSCIHK7Q-CgzsiMAL.js} +1 -1
- package/dist/client/assets/{chunk-KSCS5N6A-cm_ppJ7v.js → chunk-KSCS5N6A-DM-rnicK.js} +1 -1
- package/dist/client/assets/{chunk-L5ZTLDWV-BzDHIV9v.js → chunk-L5ZTLDWV-CnMgoXZs.js} +1 -1
- package/dist/client/assets/{chunk-LZXEDZCA-BruMwvko.js → chunk-LZXEDZCA-CCr2z1iU.js} +2 -2
- package/dist/client/assets/{chunk-ND2GUHAM-DrnvodkL.js → chunk-ND2GUHAM-DtA6gjMn.js} +1 -1
- package/dist/client/assets/{chunk-NZK2D7GU-QVsa0LuF.js → chunk-NZK2D7GU-_8fmSSaf.js} +1 -1
- package/dist/client/assets/{chunk-O5CBEL6O-DJFhq2gr.js → chunk-O5CBEL6O-B84elqwU.js} +1 -1
- package/dist/client/assets/{chunk-WU5MYG2G-ieLjE2uQ.js → chunk-WU5MYG2G-DBvmvPPt.js} +1 -1
- package/dist/client/assets/classDiagram-4FO5ZUOK-BmFh-n6n.js +1 -0
- package/dist/client/assets/classDiagram-v2-Q7XG4LA2-tA1aAbh4.js +1 -0
- package/dist/client/assets/{dagre-BM42HDAG-CFRh82aT.js → dagre-BM42HDAG-C-QssoNU.js} +1 -1
- package/dist/client/assets/{diagram-2AECGRRQ-CPk1WJvx.js → diagram-2AECGRRQ-8RpG5DGb.js} +1 -1
- package/dist/client/assets/{diagram-5GNKFQAL-DHMJ735r.js → diagram-5GNKFQAL-Dk2raJWq.js} +1 -1
- package/dist/client/assets/{diagram-KO2AKTUF-Cq9d2BWb.js → diagram-KO2AKTUF-7iZuIIef.js} +1 -1
- package/dist/client/assets/{diagram-LMA3HP47-CgWJTR_b.js → diagram-LMA3HP47-JLaaaEE6.js} +1 -1
- package/dist/client/assets/{diagram-OG6HWLK6-HoFYGw81.js → diagram-OG6HWLK6-jeL3-Rd3.js} +1 -1
- package/dist/client/assets/{erDiagram-TEJ5UH35-B7yuPZck.js → erDiagram-TEJ5UH35-B6uFrMrQ.js} +1 -1
- package/dist/client/assets/{flowDiagram-I6XJVG4X-DaXXRHQj.js → flowDiagram-I6XJVG4X-BFWnhmx2.js} +1 -1
- package/dist/client/assets/{ganttDiagram-6RSMTGT7-pKsFmtHB.js → ganttDiagram-6RSMTGT7-nxON-689.js} +1 -1
- package/dist/client/assets/{gitGraphDiagram-PVQCEYII-Dwx5kc1M.js → gitGraphDiagram-PVQCEYII-DEAsC0GZ.js} +1 -1
- package/dist/client/assets/index-BzDXl4AF.js +265 -0
- package/dist/client/assets/index-DSap5tSX.css +2 -0
- package/dist/client/assets/{infoDiagram-5YYISTIA-6Taupp-l.js → infoDiagram-5YYISTIA-CAZHR2I8.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-BBKjiFtW.js → ishikawaDiagram-YF4QCWOH-DEe4SNzl.js} +1 -1
- package/dist/client/assets/{journeyDiagram-JHISSGLW-BQQ1EKJ3.js → journeyDiagram-JHISSGLW-CG2kTAj9.js} +1 -1
- package/dist/client/assets/{kanban-definition-UN3LZRKU-BvsroFzw.js → kanban-definition-UN3LZRKU-DUWutAjA.js} +1 -1
- package/dist/client/assets/{line-C6BHsie7.js → line-B1SFN8mz.js} +1 -1
- package/dist/client/assets/{mermaid-parser.core-Cb4G8u3H.js → mermaid-parser.core-C-dyz_nb.js} +1 -1
- package/dist/client/assets/{mermaid.core-NCO8Z4wR.js → mermaid.core-nm4BMZuF.js} +3 -3
- package/dist/client/assets/{mindmap-definition-RKZ34NQL-DYzSjAq3.js → mindmap-definition-RKZ34NQL-DZEd-RCz.js} +1 -1
- package/dist/client/assets/{pieDiagram-4H26LBE5-CWMyFFuU.js → pieDiagram-4H26LBE5-WXxrzUwX.js} +1 -1
- package/dist/client/assets/{quadrantDiagram-W4KKPZXB-D1ifIucc.js → quadrantDiagram-W4KKPZXB-BClH2kQx.js} +1 -1
- package/dist/client/assets/{requirementDiagram-4Y6WPE33-DW68fC21.js → requirementDiagram-4Y6WPE33-ByolUxfP.js} +1 -1
- package/dist/client/assets/{sankeyDiagram-5OEKKPKP-DnnV47BL.js → sankeyDiagram-5OEKKPKP-B2vcb3U1.js} +1 -1
- package/dist/client/assets/{sequenceDiagram-3UESZ5HK-Dw-9ZBU_.js → sequenceDiagram-3UESZ5HK-J8Nt0GXG.js} +1 -1
- package/dist/client/assets/{stateDiagram-AJRCARHV-C7dkf8nk.js → stateDiagram-AJRCARHV-CrgZ4Uwa.js} +1 -1
- package/dist/client/assets/stateDiagram-v2-BHNVJYJU-CUEBQOsD.js +1 -0
- package/dist/client/assets/{timeline-definition-PNZ67QCA-BmVfK_16.js → timeline-definition-PNZ67QCA-CXebl6LI.js} +1 -1
- package/dist/client/assets/{vennDiagram-CIIHVFJN-B9j3QbmD.js → vennDiagram-CIIHVFJN-DLBqI39h.js} +1 -1
- package/dist/client/assets/{wardleyDiagram-YWT4CUSO-C520auMy.js → wardleyDiagram-YWT4CUSO-ku4fViV9.js} +1 -1
- package/dist/client/assets/{xychartDiagram-2RQKCTM6-CPqUJuqs.js → xychartDiagram-2RQKCTM6-5Jh-bBeb.js} +1 -1
- package/dist/client/index.html +2 -2
- package/dist/server/db.js +291 -3
- package/dist/server/db.js.map +1 -1
- package/dist/server/index.js +339 -97
- package/dist/server/index.js.map +1 -1
- package/dist/server/lib/agent.js +853 -161
- package/dist/server/lib/agent.js.map +1 -1
- package/dist/server/lib/agentRuns.js +22 -0
- package/dist/server/lib/agentRuns.js.map +1 -1
- package/dist/server/lib/chatMessages.js +24 -8
- package/dist/server/lib/chatMessages.js.map +1 -1
- package/dist/server/lib/genericToolCall.js +151 -0
- package/dist/server/lib/genericToolCall.js.map +1 -0
- package/dist/server/lib/git.js +18 -0
- package/dist/server/lib/git.js.map +1 -1
- package/dist/server/lib/mobileAccess.js +45 -2
- package/dist/server/lib/mobileAccess.js.map +1 -1
- package/dist/server/lib/models.js +65 -48
- package/dist/server/lib/models.js.map +1 -1
- package/dist/server/lib/opencodeTaskSessions.js +216 -0
- package/dist/server/lib/opencodeTaskSessions.js.map +1 -0
- package/dist/server/lib/projectMemory.js +1073 -276
- package/dist/server/lib/projectMemory.js.map +1 -1
- package/dist/server/lib/projectMemoryCursor.js +27 -0
- package/dist/server/lib/projectMemoryCursor.js.map +1 -0
- package/dist/server/lib/projectMemoryEvents.js +24 -0
- package/dist/server/lib/projectMemoryEvents.js.map +1 -0
- package/dist/server/lib/projectMemoryLimits.js +61 -0
- package/dist/server/lib/projectMemoryLimits.js.map +1 -0
- package/dist/server/lib/projectMemoryPrompt.js +299 -110
- package/dist/server/lib/projectMemoryPrompt.js.map +1 -1
- package/dist/server/lib/projectMemoryScan.js +461 -493
- package/dist/server/lib/projectMemoryScan.js.map +1 -1
- package/dist/server/lib/projectMemorySelections.js +97 -0
- package/dist/server/lib/projectMemorySelections.js.map +1 -0
- package/dist/server/lib/projectMemorySuggestions.js +19 -4
- package/dist/server/lib/projectMemorySuggestions.js.map +1 -1
- package/dist/server/lib/projectMemoryTransfer.js +635 -83
- package/dist/server/lib/projectMemoryTransfer.js.map +1 -1
- package/dist/server/lib/projectPathGlob.js +40 -0
- package/dist/server/lib/projectPathGlob.js.map +1 -0
- package/dist/server/lib/projects.js +50 -0
- package/dist/server/lib/projects.js.map +1 -1
- package/dist/server/lib/promptSkills.js +5 -0
- package/dist/server/lib/promptSkills.js.map +1 -1
- package/dist/server/lib/specMessages.js +9 -6
- package/dist/server/lib/specMessages.js.map +1 -1
- package/dist/server/lib/specReviewContext.js +2 -1
- package/dist/server/lib/specReviewContext.js.map +1 -1
- package/dist/server/lib/ticketMessages.js +22 -7
- package/dist/server/lib/ticketMessages.js.map +1 -1
- package/dist/server/lib/tickets.js +6 -0
- package/dist/server/lib/tickets.js.map +1 -1
- package/dist/server/lib/transcriptRuns.js +85 -0
- package/dist/server/lib/transcriptRuns.js.map +1 -0
- package/dist/server/workers/build.js +18 -4
- package/dist/server/workers/build.js.map +1 -1
- package/dist/server/workers/chat.js +24 -7
- package/dist/server/workers/chat.js.map +1 -1
- package/dist/server/workers/followUp.js +27 -10
- package/dist/server/workers/followUp.js.map +1 -1
- package/dist/server/workers/plan.js +405 -69
- package/dist/server/workers/plan.js.map +1 -1
- package/dist/server/workers/planFollowUp.js +30 -7
- package/dist/server/workers/planFollowUp.js.map +1 -1
- package/dist/server/workers/review.js +76 -20
- package/dist/server/workers/review.js.map +1 -1
- package/dist/server/workers/specReview.js +10 -0
- package/dist/server/workers/specReview.js.map +1 -1
- package/dist/server/workers/specSession.js +37 -3
- package/dist/server/workers/specSession.js.map +1 -1
- package/package.json +1 -1
- package/dist/client/assets/channel-D7PWuiXU.js +0 -1
- package/dist/client/assets/classDiagram-4FO5ZUOK-BNIT931s.js +0 -1
- package/dist/client/assets/classDiagram-v2-Q7XG4LA2-TRtc0GGS.js +0 -1
- package/dist/client/assets/index-5Eun897r.js +0 -260
- package/dist/client/assets/index-CWWEw76C.css +0 -2
- package/dist/client/assets/stateDiagram-v2-BHNVJYJU-BFmoroJA.js +0 -1
package/dist/server/index.js
CHANGED
|
@@ -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-
|
|
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.
|
|
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
|
-
|
|
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({
|
|
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
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
4580
|
-
|
|
4581
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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(
|
|
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(
|
|
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.
|
|
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(
|
|
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(
|
|
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.
|
|
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
|
|
4855
|
-
|
|
4856
|
-
|
|
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
|
|
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({
|
|
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
|
|
4893
|
-
|
|
4894
|
-
|
|
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
|
|
5139
|
+
return sendProjectMemoryValidationError(res, validation);
|
|
4921
5140
|
}
|
|
4922
|
-
const updated =
|
|
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
|
|
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({
|
|
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
|
-
|
|
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
|
|
5220
|
+
return sendProjectMemoryValidationError(res, validation);
|
|
5003
5221
|
}
|
|
5004
|
-
const updated =
|
|
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
|
|
5016
|
-
|
|
5017
|
-
|
|
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
|
}
|