@mcoda/core 0.1.19 → 0.1.20
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/api/QaTasksApi.d.ts.map +1 -1
- package/dist/api/QaTasksApi.js +3 -0
- package/dist/prompts/PdrPrompts.d.ts.map +1 -1
- package/dist/prompts/PdrPrompts.js +22 -8
- package/dist/prompts/SdsPrompts.d.ts.map +1 -1
- package/dist/prompts/SdsPrompts.js +53 -34
- package/dist/services/backlog/BacklogService.d.ts.map +1 -1
- package/dist/services/backlog/BacklogService.js +3 -0
- package/dist/services/backlog/TaskOrderingService.d.ts +9 -0
- package/dist/services/backlog/TaskOrderingService.d.ts.map +1 -1
- package/dist/services/backlog/TaskOrderingService.js +251 -35
- package/dist/services/docs/DocsService.d.ts.map +1 -1
- package/dist/services/docs/DocsService.js +487 -71
- package/dist/services/docs/review/gates/PdrFolderTreeGate.d.ts +7 -0
- package/dist/services/docs/review/gates/PdrFolderTreeGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/PdrFolderTreeGate.js +151 -0
- package/dist/services/docs/review/gates/PdrNoUnresolvedItemsGate.d.ts +7 -0
- package/dist/services/docs/review/gates/PdrNoUnresolvedItemsGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/PdrNoUnresolvedItemsGate.js +109 -0
- package/dist/services/docs/review/gates/PdrTechStackRationaleGate.d.ts +7 -0
- package/dist/services/docs/review/gates/PdrTechStackRationaleGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/PdrTechStackRationaleGate.js +128 -0
- package/dist/services/docs/review/gates/SdsFolderTreeGate.d.ts +7 -0
- package/dist/services/docs/review/gates/SdsFolderTreeGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/SdsFolderTreeGate.js +153 -0
- package/dist/services/docs/review/gates/SdsNoUnresolvedItemsGate.d.ts +7 -0
- package/dist/services/docs/review/gates/SdsNoUnresolvedItemsGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/SdsNoUnresolvedItemsGate.js +109 -0
- package/dist/services/docs/review/gates/SdsTechStackRationaleGate.d.ts +7 -0
- package/dist/services/docs/review/gates/SdsTechStackRationaleGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/SdsTechStackRationaleGate.js +128 -0
- package/dist/services/execution/QaTasksService.d.ts +6 -0
- package/dist/services/execution/QaTasksService.d.ts.map +1 -1
- package/dist/services/execution/QaTasksService.js +278 -95
- package/dist/services/execution/TaskSelectionService.d.ts +3 -0
- package/dist/services/execution/TaskSelectionService.d.ts.map +1 -1
- package/dist/services/execution/TaskSelectionService.js +33 -0
- package/dist/services/execution/WorkOnTasksService.d.ts +4 -0
- package/dist/services/execution/WorkOnTasksService.d.ts.map +1 -1
- package/dist/services/execution/WorkOnTasksService.js +141 -19
- package/dist/services/openapi/OpenApiService.d.ts.map +1 -1
- package/dist/services/openapi/OpenApiService.js +43 -4
- package/dist/services/planning/CreateTasksService.d.ts +10 -0
- package/dist/services/planning/CreateTasksService.d.ts.map +1 -1
- package/dist/services/planning/CreateTasksService.js +480 -44
- package/dist/services/planning/RefineTasksService.d.ts +1 -0
- package/dist/services/planning/RefineTasksService.d.ts.map +1 -1
- package/dist/services/planning/RefineTasksService.js +88 -2
- package/dist/services/review/CodeReviewService.d.ts +6 -0
- package/dist/services/review/CodeReviewService.d.ts.map +1 -1
- package/dist/services/review/CodeReviewService.js +260 -41
- package/dist/services/shared/ProjectGuidance.d.ts +18 -2
- package/dist/services/shared/ProjectGuidance.d.ts.map +1 -1
- package/dist/services/shared/ProjectGuidance.js +535 -34
- package/package.json +6 -6
|
@@ -27,7 +27,7 @@ import { GlobalRepository } from '@mcoda/db';
|
|
|
27
27
|
import { DocdexClient } from '@mcoda/integrations';
|
|
28
28
|
import { RoutingService } from '../agents/RoutingService.js';
|
|
29
29
|
import { AgentRatingService } from '../agents/AgentRatingService.js';
|
|
30
|
-
import { isDocContextExcluded, loadProjectGuidance, normalizeDocType } from '../shared/ProjectGuidance.js';
|
|
30
|
+
import { ensureProjectGuidance, isDocContextExcluded, loadProjectGuidance, normalizeDocType, } from '../shared/ProjectGuidance.js';
|
|
31
31
|
import { buildDocdexUsageGuidance } from '../shared/DocdexGuidance.js';
|
|
32
32
|
import { createTaskCommentSlug, formatTaskCommentBody } from '../tasks/TaskCommentFormatter.js';
|
|
33
33
|
import { AUTH_ERROR_REASON, isAuthErrorMessage } from '../shared/AuthErrors.js';
|
|
@@ -409,6 +409,8 @@ export class QaTasksService {
|
|
|
409
409
|
this.workspace = workspace;
|
|
410
410
|
this.deps = deps;
|
|
411
411
|
this.dryRunGuard = false;
|
|
412
|
+
this.debugLogging = false;
|
|
413
|
+
this.projectKeyById = new Map();
|
|
412
414
|
this.selectionService = deps.selectionService ?? new TaskSelectionService(workspace, deps.workspaceRepo);
|
|
413
415
|
this.stateService = deps.stateService ?? new TaskStateService(deps.workspaceRepo);
|
|
414
416
|
this.profileService =
|
|
@@ -930,7 +932,29 @@ export class QaTasksService {
|
|
|
930
932
|
return 'unclear';
|
|
931
933
|
return 'pass';
|
|
932
934
|
}
|
|
933
|
-
async
|
|
935
|
+
async resolveTaskProjectKey(task, fallbackProjectKey) {
|
|
936
|
+
if (fallbackProjectKey?.trim())
|
|
937
|
+
return fallbackProjectKey.trim();
|
|
938
|
+
const taskMetadata = task.metadata ?? {};
|
|
939
|
+
const metadataProjectKey = typeof taskMetadata.project_key === 'string' ? taskMetadata.project_key.trim() : '';
|
|
940
|
+
if (metadataProjectKey)
|
|
941
|
+
return metadataProjectKey;
|
|
942
|
+
const cached = this.projectKeyById.get(task.projectId);
|
|
943
|
+
if (cached)
|
|
944
|
+
return cached;
|
|
945
|
+
try {
|
|
946
|
+
const project = await this.deps.workspaceRepo.getProjectById(task.projectId);
|
|
947
|
+
if (project?.key) {
|
|
948
|
+
this.projectKeyById.set(task.projectId, project.key);
|
|
949
|
+
return project.key;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
catch {
|
|
953
|
+
// best effort only
|
|
954
|
+
}
|
|
955
|
+
return undefined;
|
|
956
|
+
}
|
|
957
|
+
async gatherDocContext(task, taskRunId, docLinks = [], projectKey) {
|
|
934
958
|
if (!this.docdex)
|
|
935
959
|
return '';
|
|
936
960
|
let openApiIncluded = false;
|
|
@@ -946,16 +970,8 @@ export class QaTasksService {
|
|
|
946
970
|
if (typeof this.docdex?.ensureRepoScope === 'function') {
|
|
947
971
|
await this.docdex.ensureRepoScope();
|
|
948
972
|
}
|
|
949
|
-
const querySeeds = [task.key, task.title, ...(task.acceptanceCriteria ?? [])]
|
|
950
|
-
.filter(Boolean)
|
|
951
|
-
.join(' ')
|
|
952
|
-
.slice(0, 200);
|
|
953
|
-
const docs = await this.docdex.search({
|
|
954
|
-
projectKey: task.projectId,
|
|
955
|
-
profile: 'qa',
|
|
956
|
-
query: querySeeds,
|
|
957
|
-
});
|
|
958
973
|
const snippets = [];
|
|
974
|
+
const seenRefs = new Set();
|
|
959
975
|
const resolveDocType = async (doc) => {
|
|
960
976
|
const content = doc.segments?.[0]?.content ?? doc.content ?? '';
|
|
961
977
|
const normalized = normalizeDocType({
|
|
@@ -969,20 +985,94 @@ export class QaTasksService {
|
|
|
969
985
|
}
|
|
970
986
|
return normalized.docType;
|
|
971
987
|
};
|
|
972
|
-
const
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
988
|
+
const runSearch = async (filter) => {
|
|
989
|
+
return await this.docdex.search(filter);
|
|
990
|
+
};
|
|
991
|
+
const pushDocs = async (docs) => {
|
|
992
|
+
const filteredDocs = docs.filter((doc) => !isDocContextExcluded(doc.path ?? doc.title ?? doc.id, true));
|
|
993
|
+
let added = 0;
|
|
994
|
+
for (const doc of filteredDocs.slice(0, 2)) {
|
|
995
|
+
const ref = doc.path ?? doc.id ?? doc.title ?? 'doc';
|
|
996
|
+
if (seenRefs.has(ref))
|
|
997
|
+
continue;
|
|
998
|
+
seenRefs.add(ref);
|
|
999
|
+
const segments = (doc.segments ?? []).slice(0, 2);
|
|
1000
|
+
const body = segments.length
|
|
1001
|
+
? segments
|
|
1002
|
+
.map((seg, idx) => ` (${idx + 1}) ${seg.heading ? `${seg.heading}: ` : ''}${(seg.content ?? '').slice(0, 400)}`)
|
|
1003
|
+
.join('\n')
|
|
1004
|
+
: doc.content
|
|
1005
|
+
? doc.content.slice(0, 600)
|
|
1006
|
+
: '';
|
|
1007
|
+
const docType = await resolveDocType(doc);
|
|
1008
|
+
if (!shouldIncludeDocType(docType))
|
|
1009
|
+
continue;
|
|
1010
|
+
snippets.push(`- [${docType}] ${doc.title ?? doc.path ?? doc.id}\n${body}`.trim());
|
|
1011
|
+
added += 1;
|
|
1012
|
+
}
|
|
1013
|
+
return added;
|
|
1014
|
+
};
|
|
1015
|
+
const queryCandidates = Array.from(new Set([task.key, task.title, ...(task.acceptanceCriteria ?? [])].map((entry) => entry?.trim()).filter(Boolean))).slice(0, 6);
|
|
1016
|
+
for (const query of queryCandidates) {
|
|
1017
|
+
let structuredHits = 0;
|
|
1018
|
+
try {
|
|
1019
|
+
const docs = await runSearch({
|
|
1020
|
+
query,
|
|
1021
|
+
projectKey,
|
|
1022
|
+
docType: 'SDS',
|
|
1023
|
+
profile: 'workspace-code',
|
|
1024
|
+
});
|
|
1025
|
+
structuredHits += await pushDocs(docs);
|
|
1026
|
+
}
|
|
1027
|
+
catch (error) {
|
|
1028
|
+
if (taskRunId) {
|
|
1029
|
+
await this.logTask(taskRunId, `Docdex SDS search failed for "${query}": ${error?.message ?? error}`, 'docdex');
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
try {
|
|
1033
|
+
const docs = await runSearch({
|
|
1034
|
+
query,
|
|
1035
|
+
projectKey,
|
|
1036
|
+
docType: 'OPENAPI',
|
|
1037
|
+
profile: 'workspace-code',
|
|
1038
|
+
});
|
|
1039
|
+
structuredHits += await pushDocs(docs);
|
|
1040
|
+
}
|
|
1041
|
+
catch (error) {
|
|
1042
|
+
if (taskRunId) {
|
|
1043
|
+
await this.logTask(taskRunId, `Docdex OPENAPI search failed for "${query}": ${error?.message ?? error}`, 'docdex');
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
if (structuredHits > 0)
|
|
984
1047
|
continue;
|
|
985
|
-
|
|
1048
|
+
try {
|
|
1049
|
+
const docs = await runSearch({
|
|
1050
|
+
query,
|
|
1051
|
+
projectKey,
|
|
1052
|
+
profile: 'qa',
|
|
1053
|
+
});
|
|
1054
|
+
await pushDocs(docs);
|
|
1055
|
+
}
|
|
1056
|
+
catch (error) {
|
|
1057
|
+
if (taskRunId) {
|
|
1058
|
+
await this.logTask(taskRunId, `Docdex QA search failed for "${query}": ${error?.message ?? error}`, 'docdex');
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
if (!snippets.length && projectKey) {
|
|
1063
|
+
try {
|
|
1064
|
+
const docs = await runSearch({
|
|
1065
|
+
query: 'sds openapi requirements architecture',
|
|
1066
|
+
projectKey,
|
|
1067
|
+
profile: 'workspace-code',
|
|
1068
|
+
});
|
|
1069
|
+
await pushDocs(docs);
|
|
1070
|
+
}
|
|
1071
|
+
catch (error) {
|
|
1072
|
+
if (taskRunId) {
|
|
1073
|
+
await this.logTask(taskRunId, `Docdex fallback search failed: ${error?.message ?? error}`, 'docdex');
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
986
1076
|
}
|
|
987
1077
|
const normalizeDocLink = (value) => {
|
|
988
1078
|
const trimmed = value.trim();
|
|
@@ -1683,7 +1773,9 @@ export class QaTasksService {
|
|
|
1683
1773
|
const taskKeys = new Set(tasks.map((task) => task.task.key));
|
|
1684
1774
|
const agent = await this.resolveAgent(request.agentName);
|
|
1685
1775
|
const prompts = await this.loadPrompts(agent.id);
|
|
1686
|
-
const projectGuidance = await loadProjectGuidance(this.workspace.workspaceRoot, this.workspace.mcodaDir
|
|
1776
|
+
const projectGuidance = await loadProjectGuidance(this.workspace.workspaceRoot, this.workspace.mcodaDir, {
|
|
1777
|
+
projectKey: request.projectKey,
|
|
1778
|
+
});
|
|
1687
1779
|
const guidanceBlock = projectGuidance?.content ? `Project Guidance (read first):\n${projectGuidance.content}` : undefined;
|
|
1688
1780
|
const systemPrompt = [guidanceBlock, prompts.jobPrompt, prompts.characterPrompt]
|
|
1689
1781
|
.filter(Boolean)
|
|
@@ -1987,7 +2079,7 @@ export class QaTasksService {
|
|
|
1987
2079
|
}
|
|
1988
2080
|
return undefined;
|
|
1989
2081
|
}
|
|
1990
|
-
async interpretResult(task, profile, result, agentName, stream, jobId, commandRunId, taskRunId, commentBacklog, abortSignal) {
|
|
2082
|
+
async interpretResult(task, profile, result, agentName, stream, jobId, commandRunId, taskRunId, projectKey, commentBacklog, abortSignal) {
|
|
1991
2083
|
if (!this.agentService) {
|
|
1992
2084
|
return { recommendation: this.mapOutcome(result) };
|
|
1993
2085
|
}
|
|
@@ -2022,16 +2114,21 @@ export class QaTasksService {
|
|
|
2022
2114
|
abortIfSignaled();
|
|
2023
2115
|
const agent = await this.resolveAgent(agentName);
|
|
2024
2116
|
const prompts = await this.loadPrompts(agent.id);
|
|
2025
|
-
const projectGuidance = await loadProjectGuidance(this.workspace.workspaceRoot, this.workspace.mcodaDir
|
|
2117
|
+
const projectGuidance = await loadProjectGuidance(this.workspace.workspaceRoot, this.workspace.mcodaDir, {
|
|
2118
|
+
projectKey,
|
|
2119
|
+
});
|
|
2026
2120
|
if (projectGuidance && taskRunId) {
|
|
2027
2121
|
await this.logTask(taskRunId, `Loaded project guidance from ${projectGuidance.source}`, 'project_guidance');
|
|
2122
|
+
for (const warning of projectGuidance.warnings ?? []) {
|
|
2123
|
+
await this.logTask(taskRunId, `Project guidance warning: ${warning}`, 'project_guidance');
|
|
2124
|
+
}
|
|
2028
2125
|
}
|
|
2029
2126
|
const guidanceBlock = projectGuidance?.content ? `Project Guidance (read first):\n${projectGuidance.content}` : undefined;
|
|
2030
2127
|
const systemPrompt = [guidanceBlock, prompts.jobPrompt, prompts.characterPrompt, prompts.commandPrompt, QA_TEST_POLICY]
|
|
2031
2128
|
.filter(Boolean)
|
|
2032
2129
|
.join('\n\n');
|
|
2033
2130
|
const docLinks = Array.isArray(task.task.metadata?.doc_links) ? task.task.metadata.doc_links : [];
|
|
2034
|
-
const docCtx = await this.gatherDocContext(task.task, taskRunId, docLinks);
|
|
2131
|
+
const docCtx = await this.gatherDocContext(task.task, taskRunId, docLinks, projectKey);
|
|
2035
2132
|
const acceptance = (task.task.acceptanceCriteria ?? []).map((line) => `- ${line}`).join('\n');
|
|
2036
2133
|
const prompt = [
|
|
2037
2134
|
systemPrompt,
|
|
@@ -2064,19 +2161,21 @@ export class QaTasksService {
|
|
|
2064
2161
|
]
|
|
2065
2162
|
.filter(Boolean)
|
|
2066
2163
|
.join('\n\n');
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2164
|
+
if (this.debugLogging) {
|
|
2165
|
+
const separator = "============================================================";
|
|
2166
|
+
console.info(separator);
|
|
2167
|
+
console.info("[qa-tasks] START OF TASK");
|
|
2168
|
+
console.info(`[qa-tasks] Task key: ${task.task.key}`);
|
|
2169
|
+
console.info(`[qa-tasks] Title: ${task.task.title ?? '(none)'}`);
|
|
2170
|
+
console.info(`[qa-tasks] Description: ${task.task.description ?? '(none)'}`);
|
|
2171
|
+
console.info(`[qa-tasks] Story points: ${typeof task.task.storyPoints === 'number' ? task.task.storyPoints : '(none)'}`);
|
|
2172
|
+
console.info(`[qa-tasks] Dependencies: ${task.dependencies.keys.length ? task.dependencies.keys.join(', ') : '(none available)'}`);
|
|
2173
|
+
if (acceptance)
|
|
2174
|
+
console.info(`[qa-tasks] Acceptance criteria:\n${acceptance}`);
|
|
2175
|
+
console.info(`[qa-tasks] System prompt used:\n${systemPrompt || '(none)'}`);
|
|
2176
|
+
console.info(`[qa-tasks] Task prompt used:\n${prompt}`);
|
|
2177
|
+
console.info(separator);
|
|
2178
|
+
}
|
|
2080
2179
|
let output = '';
|
|
2081
2180
|
let chunkCount = 0;
|
|
2082
2181
|
if (stream && this.agentService.invokeStream) {
|
|
@@ -2530,9 +2629,15 @@ export class QaTasksService {
|
|
|
2530
2629
|
return [];
|
|
2531
2630
|
const agent = await this.resolveAgent(undefined);
|
|
2532
2631
|
const prompts = await this.loadPrompts(agent.id);
|
|
2533
|
-
const
|
|
2632
|
+
const projectKey = await this.resolveTaskProjectKey(task.task);
|
|
2633
|
+
const projectGuidance = await loadProjectGuidance(this.workspace.workspaceRoot, this.workspace.mcodaDir, {
|
|
2634
|
+
projectKey,
|
|
2635
|
+
});
|
|
2534
2636
|
if (projectGuidance && taskRunId) {
|
|
2535
2637
|
await this.logTask(taskRunId, `Loaded project guidance from ${projectGuidance.source}`, 'project_guidance');
|
|
2638
|
+
for (const warning of projectGuidance.warnings ?? []) {
|
|
2639
|
+
await this.logTask(taskRunId, `Project guidance warning: ${warning}`, 'project_guidance');
|
|
2640
|
+
}
|
|
2536
2641
|
}
|
|
2537
2642
|
const guidanceBlock = projectGuidance?.content ? `Project Guidance (read first):\n${projectGuidance.content}` : undefined;
|
|
2538
2643
|
const systemPrompt = [guidanceBlock, prompts.jobPrompt, prompts.characterPrompt, prompts.commandPrompt].filter(Boolean).join('\n\n');
|
|
@@ -2654,47 +2759,99 @@ export class QaTasksService {
|
|
|
2654
2759
|
}
|
|
2655
2760
|
const skipReview = await this.shouldSkipQaForNoChanges(task.task);
|
|
2656
2761
|
if (skipReview.skip) {
|
|
2657
|
-
const
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
await this.
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
slug,
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2762
|
+
const noChangesPolicy = ctx.request.noChangesPolicy ?? 'require_qa';
|
|
2763
|
+
if (noChangesPolicy === 'skip') {
|
|
2764
|
+
const message = 'QA skipped: code review reported no code changes to validate.';
|
|
2765
|
+
await this.logTask(taskRun.id, message, 'qa-skip', { reason: 'review_no_changes', decision: skipReview.decision });
|
|
2766
|
+
if (!this.dryRunGuard) {
|
|
2767
|
+
await this.applyStateTransition(task.task, 'pass', statusContextBase);
|
|
2768
|
+
await this.finishTaskRun(taskRun, 'succeeded');
|
|
2769
|
+
await this.deps.workspaceRepo.createTaskQaRun({
|
|
2770
|
+
taskId: task.task.id,
|
|
2771
|
+
taskRunId: taskRun.id,
|
|
2772
|
+
jobId: ctx.jobId,
|
|
2773
|
+
commandRunId: ctx.commandRunId,
|
|
2774
|
+
source: 'auto',
|
|
2775
|
+
mode: 'auto',
|
|
2776
|
+
rawOutcome: 'pass',
|
|
2777
|
+
recommendation: 'pass',
|
|
2778
|
+
profileName: undefined,
|
|
2779
|
+
runner: undefined,
|
|
2780
|
+
metadata: { reason: 'review_no_changes', decision: skipReview.decision, reviewId: skipReview.reviewId },
|
|
2781
|
+
});
|
|
2782
|
+
const slug = createTaskCommentSlug({ source: 'qa-tasks', message, category: 'qa_result' });
|
|
2783
|
+
const body = formatTaskCommentBody({
|
|
2784
|
+
slug,
|
|
2785
|
+
source: 'qa-tasks',
|
|
2786
|
+
message,
|
|
2787
|
+
status: 'resolved',
|
|
2788
|
+
category: 'qa_result',
|
|
2789
|
+
});
|
|
2790
|
+
await this.deps.workspaceRepo.createTaskComment({
|
|
2791
|
+
taskId: task.task.id,
|
|
2792
|
+
taskRunId: taskRun.id,
|
|
2793
|
+
jobId: ctx.jobId,
|
|
2794
|
+
sourceCommand: 'qa-tasks',
|
|
2795
|
+
authorType: 'agent',
|
|
2796
|
+
category: 'qa_result',
|
|
2797
|
+
slug,
|
|
2798
|
+
status: 'resolved',
|
|
2799
|
+
body,
|
|
2800
|
+
createdAt: new Date().toISOString(),
|
|
2801
|
+
resolvedAt: new Date().toISOString(),
|
|
2802
|
+
});
|
|
2803
|
+
}
|
|
2804
|
+
return { taskKey: task.task.key, outcome: 'pass', notes: 'review_no_changes' };
|
|
2805
|
+
}
|
|
2806
|
+
if (noChangesPolicy === 'manual') {
|
|
2807
|
+
const message = 'QA requires manual validation: code review reported no code changes and policy is manual.';
|
|
2808
|
+
await this.logTask(taskRun.id, message, 'qa-manual', {
|
|
2809
|
+
reason: 'review_no_changes_manual',
|
|
2810
|
+
decision: skipReview.decision,
|
|
2695
2811
|
});
|
|
2812
|
+
if (!this.dryRunGuard) {
|
|
2813
|
+
await this.finishTaskRun(taskRun, 'succeeded');
|
|
2814
|
+
await this.deps.workspaceRepo.createTaskQaRun({
|
|
2815
|
+
taskId: task.task.id,
|
|
2816
|
+
taskRunId: taskRun.id,
|
|
2817
|
+
jobId: ctx.jobId,
|
|
2818
|
+
commandRunId: ctx.commandRunId,
|
|
2819
|
+
source: 'auto',
|
|
2820
|
+
mode: 'auto',
|
|
2821
|
+
rawOutcome: 'unclear',
|
|
2822
|
+
recommendation: 'unclear',
|
|
2823
|
+
profileName: undefined,
|
|
2824
|
+
runner: undefined,
|
|
2825
|
+
metadata: {
|
|
2826
|
+
reason: 'review_no_changes_manual',
|
|
2827
|
+
decision: skipReview.decision,
|
|
2828
|
+
reviewId: skipReview.reviewId,
|
|
2829
|
+
},
|
|
2830
|
+
});
|
|
2831
|
+
const slug = createTaskCommentSlug({ source: 'qa-tasks', message, category: 'qa_issue' });
|
|
2832
|
+
const body = formatTaskCommentBody({
|
|
2833
|
+
slug,
|
|
2834
|
+
source: 'qa-tasks',
|
|
2835
|
+
message,
|
|
2836
|
+
status: 'open',
|
|
2837
|
+
category: 'qa_issue',
|
|
2838
|
+
});
|
|
2839
|
+
await this.deps.workspaceRepo.createTaskComment({
|
|
2840
|
+
taskId: task.task.id,
|
|
2841
|
+
taskRunId: taskRun.id,
|
|
2842
|
+
jobId: ctx.jobId,
|
|
2843
|
+
sourceCommand: 'qa-tasks',
|
|
2844
|
+
authorType: 'agent',
|
|
2845
|
+
category: 'qa_issue',
|
|
2846
|
+
slug,
|
|
2847
|
+
status: 'open',
|
|
2848
|
+
body,
|
|
2849
|
+
createdAt: new Date().toISOString(),
|
|
2850
|
+
metadata: { reason: 'review_no_changes_manual' },
|
|
2851
|
+
});
|
|
2852
|
+
}
|
|
2853
|
+
return { taskKey: task.task.key, outcome: 'unclear', notes: 'review_no_changes_manual' };
|
|
2696
2854
|
}
|
|
2697
|
-
return { taskKey: task.task.key, outcome: 'pass', notes: 'review_no_changes' };
|
|
2698
2855
|
}
|
|
2699
2856
|
const branchCheck = await this.ensureTaskBranch(task, taskRun.id, ctx.jobId, ctx.request.allowDirty ?? false, ctx.request.cleanIgnorePaths);
|
|
2700
2857
|
if (!branchCheck.ok) {
|
|
@@ -3376,10 +3533,11 @@ export class QaTasksService {
|
|
|
3376
3533
|
});
|
|
3377
3534
|
const commentContext = await this.loadCommentContext(task.task.id);
|
|
3378
3535
|
const commentBacklog = buildCommentBacklog(commentContext.unresolved);
|
|
3536
|
+
const taskProjectKey = await this.resolveTaskProjectKey(task.task, ctx.request.projectKey);
|
|
3379
3537
|
let interpretation;
|
|
3380
3538
|
try {
|
|
3381
3539
|
if (this.shouldUseAgentInterpretation()) {
|
|
3382
|
-
interpretation = await this.interpretResult(task, profile, result, ctx.request.agentName, ctx.request.agentStream ?? true, ctx.jobId, ctx.commandRunId, taskRun.id, commentBacklog, ctx.request.abortSignal);
|
|
3540
|
+
interpretation = await this.interpretResult(task, profile, result, ctx.request.agentName, ctx.request.agentStream ?? true, ctx.jobId, ctx.commandRunId, taskRun.id, taskProjectKey, commentBacklog, ctx.request.abortSignal);
|
|
3383
3541
|
}
|
|
3384
3542
|
else {
|
|
3385
3543
|
interpretation = this.buildDeterministicInterpretation(task, profile, result);
|
|
@@ -3797,6 +3955,8 @@ export class QaTasksService {
|
|
|
3797
3955
|
async run(request) {
|
|
3798
3956
|
this.qaProfilePlan = undefined;
|
|
3799
3957
|
this.qaTaskPlans = undefined;
|
|
3958
|
+
this.projectKeyById.clear();
|
|
3959
|
+
this.debugLogging = request.debug === true;
|
|
3800
3960
|
const resume = request.resumeJobId ? await this.deps.jobService.getJob(request.resumeJobId) : undefined;
|
|
3801
3961
|
if (request.resumeJobId && !resume) {
|
|
3802
3962
|
throw new Error(`Resume requested but job ${request.resumeJobId} not found`);
|
|
@@ -3807,6 +3967,14 @@ export class QaTasksService {
|
|
|
3807
3967
|
const effectiveTasks = request.taskKeys?.length ? request.taskKeys : resume?.payload?.tasks;
|
|
3808
3968
|
const effectiveStatus = request.statusFilter ?? resume?.payload?.statusFilter ?? ['ready_to_qa'];
|
|
3809
3969
|
const effectiveLimit = request.limit ?? resume?.payload?.limit;
|
|
3970
|
+
const effectiveDependencyPolicy = request.dependencyPolicy ?? resume?.payload?.dependencyPolicy ?? 'enforce';
|
|
3971
|
+
const effectiveNoChangesPolicy = request.noChangesPolicy ?? resume?.payload?.noChangesPolicy ?? 'require_qa';
|
|
3972
|
+
const normalizedRequest = {
|
|
3973
|
+
...request,
|
|
3974
|
+
projectKey: effectiveProject,
|
|
3975
|
+
dependencyPolicy: effectiveDependencyPolicy,
|
|
3976
|
+
noChangesPolicy: effectiveNoChangesPolicy,
|
|
3977
|
+
};
|
|
3810
3978
|
const ignoreStatusFilter = Boolean(effectiveTasks?.length) || request.ignoreStatusFilter === true;
|
|
3811
3979
|
const { filtered: statusFilter, rejected } = filterTaskStatuses(ignoreStatusFilter ? [] : effectiveStatus, QA_ALLOWED_STATUSES, QA_ALLOWED_STATUSES);
|
|
3812
3980
|
const selection = await this.selectionService.selectTasks({
|
|
@@ -3816,7 +3984,7 @@ export class QaTasksService {
|
|
|
3816
3984
|
taskKeys: effectiveTasks,
|
|
3817
3985
|
statusFilter,
|
|
3818
3986
|
limit: effectiveLimit,
|
|
3819
|
-
ignoreDependencies:
|
|
3987
|
+
ignoreDependencies: effectiveDependencyPolicy === 'ignore',
|
|
3820
3988
|
ignoreStatusFilter,
|
|
3821
3989
|
});
|
|
3822
3990
|
if (rejected.length > 0 && !ignoreStatusFilter) {
|
|
@@ -3836,12 +4004,12 @@ export class QaTasksService {
|
|
|
3836
4004
|
throw new Error(resolveAbortReason());
|
|
3837
4005
|
}
|
|
3838
4006
|
};
|
|
3839
|
-
const mode =
|
|
4007
|
+
const mode = normalizedRequest.mode ?? 'auto';
|
|
3840
4008
|
this.dryRunGuard = request.dryRun ?? false;
|
|
3841
4009
|
if (request.dryRun) {
|
|
3842
4010
|
const dryResults = [];
|
|
3843
4011
|
if (mode !== 'manual') {
|
|
3844
|
-
this.qaProfilePlan = await this.planProfilesWithAgent(selection.ordered,
|
|
4012
|
+
this.qaProfilePlan = await this.planProfilesWithAgent(selection.ordered, normalizedRequest, {
|
|
3845
4013
|
warnings: selection.warnings,
|
|
3846
4014
|
});
|
|
3847
4015
|
}
|
|
@@ -3852,7 +4020,7 @@ export class QaTasksService {
|
|
|
3852
4020
|
abortIfSignaled();
|
|
3853
4021
|
let profiles = [];
|
|
3854
4022
|
try {
|
|
3855
|
-
profiles = await this.resolveProfilesForRequest(task.task,
|
|
4023
|
+
profiles = await this.resolveProfilesForRequest(task.task, normalizedRequest);
|
|
3856
4024
|
}
|
|
3857
4025
|
catch {
|
|
3858
4026
|
profiles = [];
|
|
@@ -3878,6 +4046,18 @@ export class QaTasksService {
|
|
|
3878
4046
|
};
|
|
3879
4047
|
}
|
|
3880
4048
|
await this.ensureMcoda();
|
|
4049
|
+
try {
|
|
4050
|
+
const guidance = await ensureProjectGuidance(this.workspace.workspaceRoot, {
|
|
4051
|
+
mcodaDir: this.workspace.mcodaDir,
|
|
4052
|
+
projectKey: effectiveProject,
|
|
4053
|
+
});
|
|
4054
|
+
for (const warning of guidance.warnings ?? []) {
|
|
4055
|
+
selection.warnings.push(`project_guidance_warning:${warning}`);
|
|
4056
|
+
}
|
|
4057
|
+
}
|
|
4058
|
+
catch (error) {
|
|
4059
|
+
selection.warnings.push(`project_guidance_bootstrap_failed:${error instanceof Error ? error.message : String(error)}`);
|
|
4060
|
+
}
|
|
3881
4061
|
const completedKeys = new Set();
|
|
3882
4062
|
const checkpoints = request.resumeJobId ? await this.deps.jobService.readCheckpoints(request.resumeJobId) : [];
|
|
3883
4063
|
const priorResults = new Map();
|
|
@@ -3913,18 +4093,21 @@ export class QaTasksService {
|
|
|
3913
4093
|
statusFilter,
|
|
3914
4094
|
limit: effectiveLimit,
|
|
3915
4095
|
mode,
|
|
3916
|
-
profile:
|
|
3917
|
-
level:
|
|
3918
|
-
agent:
|
|
4096
|
+
profile: normalizedRequest.profileName,
|
|
4097
|
+
level: normalizedRequest.level,
|
|
4098
|
+
agent: normalizedRequest.agentName,
|
|
3919
4099
|
agentStream,
|
|
3920
|
-
createFollowups:
|
|
4100
|
+
createFollowups: normalizedRequest.createFollowupTasks ?? 'auto',
|
|
4101
|
+
dependencyPolicy: effectiveDependencyPolicy,
|
|
4102
|
+
noChangesPolicy: effectiveNoChangesPolicy,
|
|
4103
|
+
debug: normalizedRequest.debug ?? false,
|
|
3921
4104
|
dryRun: request.dryRun ?? false,
|
|
3922
4105
|
},
|
|
3923
4106
|
totalItems: selection.ordered.length,
|
|
3924
4107
|
processedItems: completedKeys.size,
|
|
3925
4108
|
});
|
|
3926
4109
|
if (mode !== 'manual') {
|
|
3927
|
-
this.qaProfilePlan = await this.planProfilesWithAgent(selection.ordered,
|
|
4110
|
+
this.qaProfilePlan = await this.planProfilesWithAgent(selection.ordered, normalizedRequest, {
|
|
3928
4111
|
jobId: job.id,
|
|
3929
4112
|
commandRunId: commandRun.id,
|
|
3930
4113
|
warnings: selection.warnings,
|
|
@@ -4034,11 +4217,11 @@ export class QaTasksService {
|
|
|
4034
4217
|
if (abortRemainingReason)
|
|
4035
4218
|
break;
|
|
4036
4219
|
abortIfSignaled();
|
|
4037
|
-
const mode =
|
|
4220
|
+
const mode = normalizedRequest.mode ?? 'auto';
|
|
4038
4221
|
const startedAt = new Date().toISOString();
|
|
4039
4222
|
const taskStartMs = Date.now();
|
|
4040
4223
|
const sessionId = formatSessionId(startedAt);
|
|
4041
|
-
const qaAgentLabel =
|
|
4224
|
+
const qaAgentLabel = normalizedRequest.agentName ?? '(auto)';
|
|
4042
4225
|
emitQaStart({
|
|
4043
4226
|
taskKey: task.task.key,
|
|
4044
4227
|
alias: `QA task ${task.task.key}`,
|
|
@@ -4053,10 +4236,10 @@ export class QaTasksService {
|
|
|
4053
4236
|
let result;
|
|
4054
4237
|
try {
|
|
4055
4238
|
if (mode === 'manual') {
|
|
4056
|
-
result = await this.runManual(task, { jobId: job.id, commandRunId: commandRun.id, request });
|
|
4239
|
+
result = await this.runManual(task, { jobId: job.id, commandRunId: commandRun.id, request: normalizedRequest });
|
|
4057
4240
|
}
|
|
4058
4241
|
else {
|
|
4059
|
-
result = await this.runAuto(task, { jobId: job.id, commandRunId: commandRun.id, request, warnings });
|
|
4242
|
+
result = await this.runAuto(task, { jobId: job.id, commandRunId: commandRun.id, request: normalizedRequest, warnings });
|
|
4060
4243
|
}
|
|
4061
4244
|
}
|
|
4062
4245
|
catch (error) {
|
|
@@ -12,7 +12,9 @@ export interface TaskSelectionFilters {
|
|
|
12
12
|
limit?: number;
|
|
13
13
|
parallel?: number;
|
|
14
14
|
ignoreDependencies?: boolean;
|
|
15
|
+
missingContextPolicy?: MissingContextPolicy;
|
|
15
16
|
}
|
|
17
|
+
export type MissingContextPolicy = "allow" | "warn" | "block";
|
|
16
18
|
export interface SelectedTask {
|
|
17
19
|
task: TaskRow & {
|
|
18
20
|
epicKey: string;
|
|
@@ -46,6 +48,7 @@ export declare class TaskSelectionService {
|
|
|
46
48
|
private buildTaskFromRow;
|
|
47
49
|
private loadTasks;
|
|
48
50
|
private loadDependencies;
|
|
51
|
+
private loadMissingContext;
|
|
49
52
|
private topologicalOrder;
|
|
50
53
|
selectTasks(filters: TaskSelectionFilters): Promise<TaskSelectionPlan>;
|
|
51
54
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TaskSelectionService.d.ts","sourceRoot":"","sources":["../../../src/services/execution/TaskSelectionService.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAErE,OAAO,EAAE,mBAAmB,EAAE,MAAM,qCAAqC,CAAC;AAE1E,MAAM,WAAW,oBAAoB;IACnC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kBAAkB,CAAC,EAAE,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"TaskSelectionService.d.ts","sourceRoot":"","sources":["../../../src/services/execution/TaskSelectionService.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAErE,OAAO,EAAE,mBAAmB,EAAE,MAAM,qCAAqC,CAAC;AAE1E,MAAM,WAAW,oBAAoB;IACnC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,oBAAoB,CAAC,EAAE,oBAAoB,CAAC;CAC7C;AAED,MAAM,MAAM,oBAAoB,GAAG,OAAO,GAAG,MAAM,GAAG,OAAO,CAAC;AAE9D,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,OAAO,GAAG;QACd,OAAO,EAAE,MAAM,CAAC;QAChB,QAAQ,EAAE,MAAM,CAAC;QACjB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,gBAAgB,CAAC,EAAE,MAAM,CAAC;QAC1B,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAC;KAC/B,CAAC;IACF,YAAY,EAAE;QACZ,GAAG,EAAE,MAAM,EAAE,CAAC;QACd,IAAI,EAAE,MAAM,EAAE,CAAC;QACf,QAAQ,EAAE,MAAM,EAAE,CAAC;KACpB,CAAC;CACH;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,CAAC,EAAE,UAAU,CAAC;IACrB,OAAO,EAAE,oBAAoB,GAAG;QAAE,iBAAiB,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IAChE,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AA0HD,qBAAa,oBAAoB;IACnB,OAAO,CAAC,SAAS;IAAuB,OAAO,CAAC,aAAa;gBAArD,SAAS,EAAE,mBAAmB,EAAU,aAAa,EAAE,mBAAmB;WAEjF,MAAM,CAAC,SAAS,EAAE,mBAAmB,GAAG,OAAO,CAAC,oBAAoB,CAAC;IAK5E,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;YAId,YAAY;IAK1B,OAAO,CAAC,gBAAgB;YAiCV,SAAS;YA2DT,gBAAgB;YAgChB,kBAAkB;IAgBhC,OAAO,CAAC,gBAAgB;IAgElB,WAAW,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,iBAAiB,CAAC;CAkI7E"}
|
|
@@ -215,6 +215,19 @@ export class TaskSelectionService {
|
|
|
215
215
|
}
|
|
216
216
|
return grouped;
|
|
217
217
|
}
|
|
218
|
+
async loadMissingContext(taskIds) {
|
|
219
|
+
if (taskIds.length === 0)
|
|
220
|
+
return new Set();
|
|
221
|
+
const placeholders = taskIds.map(() => "?").join(", ");
|
|
222
|
+
const rows = await this.workspaceRepo.getDb().all(`
|
|
223
|
+
SELECT DISTINCT task_id
|
|
224
|
+
FROM task_comments
|
|
225
|
+
WHERE task_id IN (${placeholders})
|
|
226
|
+
AND LOWER(category) = 'missing_context'
|
|
227
|
+
AND (status IS NULL OR LOWER(status) = 'open')
|
|
228
|
+
`, ...taskIds);
|
|
229
|
+
return new Set(rows.map((row) => row.task_id));
|
|
230
|
+
}
|
|
218
231
|
topologicalOrder(tasks, deps) {
|
|
219
232
|
const warnings = [];
|
|
220
233
|
const taskMap = new Map(tasks.map((t) => [t.task.id, t]));
|
|
@@ -330,6 +343,8 @@ export class TaskSelectionService {
|
|
|
330
343
|
}
|
|
331
344
|
const candidateIds = dedupedTasks.map((t) => t.task_id);
|
|
332
345
|
const deps = await this.loadDependencies(candidateIds);
|
|
346
|
+
const missingContextPolicy = filters.missingContextPolicy ?? "warn";
|
|
347
|
+
const missingContext = missingContextPolicy === "allow" ? new Set() : await this.loadMissingContext(candidateIds);
|
|
333
348
|
const taskMap = new Map();
|
|
334
349
|
for (const row of dedupedTasks) {
|
|
335
350
|
const task = this.buildTaskFromRow(row);
|
|
@@ -340,6 +355,8 @@ export class TaskSelectionService {
|
|
|
340
355
|
}
|
|
341
356
|
const eligible = [];
|
|
342
357
|
const skippedDependencies = [];
|
|
358
|
+
const skippedMissingContext = [];
|
|
359
|
+
const warnedMissingContext = [];
|
|
343
360
|
const ignoreDependencies = filters.ignoreDependencies === true;
|
|
344
361
|
for (const [taskId, entry] of taskMap.entries()) {
|
|
345
362
|
const depRows = deps.get(taskId) ?? [];
|
|
@@ -373,12 +390,27 @@ export class TaskSelectionService {
|
|
|
373
390
|
skippedDependencies.push(entry.task.key);
|
|
374
391
|
continue;
|
|
375
392
|
}
|
|
393
|
+
if (missingContext.has(taskId)) {
|
|
394
|
+
if (missingContextPolicy === "block") {
|
|
395
|
+
skippedMissingContext.push(entry.task.key);
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
if (missingContextPolicy === "warn") {
|
|
399
|
+
warnedMissingContext.push(entry.task.key);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
376
402
|
eligible.push(entry);
|
|
377
403
|
}
|
|
378
404
|
const { ordered, warnings: topoWarnings } = this.topologicalOrder(eligible, deps);
|
|
379
405
|
if (skippedDependencies.length > 0) {
|
|
380
406
|
selectionWarnings.push(`Skipped ${skippedDependencies.length} task(s) due to dependencies not ready.`);
|
|
381
407
|
}
|
|
408
|
+
if (missingContextPolicy === "warn" && warnedMissingContext.length > 0) {
|
|
409
|
+
selectionWarnings.push(`Tasks with open missing_context comments: ${warnedMissingContext.length}.`);
|
|
410
|
+
}
|
|
411
|
+
if (missingContextPolicy === "block" && skippedMissingContext.length > 0) {
|
|
412
|
+
selectionWarnings.push(`Skipped ${skippedMissingContext.length} task(s) with open missing_context comments.`);
|
|
413
|
+
}
|
|
382
414
|
const combinedWarnings = [...dedupeWarnings, ...selectionWarnings, ...topoWarnings];
|
|
383
415
|
const limited = typeof filters.limit === "number" && filters.limit > 0 ? ordered.slice(0, filters.limit) : ordered;
|
|
384
416
|
return {
|
|
@@ -389,6 +421,7 @@ export class TaskSelectionService {
|
|
|
389
421
|
effectiveStatuses,
|
|
390
422
|
includeTypes: includeTypes.length ? includeTypes : undefined,
|
|
391
423
|
excludeTypes: excludeTypes.length ? excludeTypes : undefined,
|
|
424
|
+
missingContextPolicy,
|
|
392
425
|
},
|
|
393
426
|
ordered: limited,
|
|
394
427
|
warnings: combinedWarnings,
|