@mcoda/core 0.1.18 → 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.
Files changed (58) hide show
  1. package/dist/api/QaTasksApi.d.ts.map +1 -1
  2. package/dist/api/QaTasksApi.js +3 -0
  3. package/dist/prompts/PdrPrompts.d.ts.map +1 -1
  4. package/dist/prompts/PdrPrompts.js +22 -8
  5. package/dist/prompts/SdsPrompts.d.ts.map +1 -1
  6. package/dist/prompts/SdsPrompts.js +53 -34
  7. package/dist/services/backlog/BacklogService.d.ts.map +1 -1
  8. package/dist/services/backlog/BacklogService.js +3 -0
  9. package/dist/services/backlog/TaskOrderingService.d.ts +9 -0
  10. package/dist/services/backlog/TaskOrderingService.d.ts.map +1 -1
  11. package/dist/services/backlog/TaskOrderingService.js +251 -35
  12. package/dist/services/docs/DocsService.d.ts.map +1 -1
  13. package/dist/services/docs/DocsService.js +487 -71
  14. package/dist/services/docs/review/gates/PdrFolderTreeGate.d.ts +7 -0
  15. package/dist/services/docs/review/gates/PdrFolderTreeGate.d.ts.map +1 -0
  16. package/dist/services/docs/review/gates/PdrFolderTreeGate.js +151 -0
  17. package/dist/services/docs/review/gates/PdrNoUnresolvedItemsGate.d.ts +7 -0
  18. package/dist/services/docs/review/gates/PdrNoUnresolvedItemsGate.d.ts.map +1 -0
  19. package/dist/services/docs/review/gates/PdrNoUnresolvedItemsGate.js +109 -0
  20. package/dist/services/docs/review/gates/PdrTechStackRationaleGate.d.ts +7 -0
  21. package/dist/services/docs/review/gates/PdrTechStackRationaleGate.d.ts.map +1 -0
  22. package/dist/services/docs/review/gates/PdrTechStackRationaleGate.js +128 -0
  23. package/dist/services/docs/review/gates/SdsFolderTreeGate.d.ts +7 -0
  24. package/dist/services/docs/review/gates/SdsFolderTreeGate.d.ts.map +1 -0
  25. package/dist/services/docs/review/gates/SdsFolderTreeGate.js +153 -0
  26. package/dist/services/docs/review/gates/SdsNoUnresolvedItemsGate.d.ts +7 -0
  27. package/dist/services/docs/review/gates/SdsNoUnresolvedItemsGate.d.ts.map +1 -0
  28. package/dist/services/docs/review/gates/SdsNoUnresolvedItemsGate.js +109 -0
  29. package/dist/services/docs/review/gates/SdsTechStackRationaleGate.d.ts +7 -0
  30. package/dist/services/docs/review/gates/SdsTechStackRationaleGate.d.ts.map +1 -0
  31. package/dist/services/docs/review/gates/SdsTechStackRationaleGate.js +128 -0
  32. package/dist/services/estimate/EstimateService.d.ts +2 -0
  33. package/dist/services/estimate/EstimateService.d.ts.map +1 -1
  34. package/dist/services/estimate/EstimateService.js +54 -0
  35. package/dist/services/execution/QaTasksService.d.ts +6 -0
  36. package/dist/services/execution/QaTasksService.d.ts.map +1 -1
  37. package/dist/services/execution/QaTasksService.js +278 -95
  38. package/dist/services/execution/TaskSelectionService.d.ts +3 -0
  39. package/dist/services/execution/TaskSelectionService.d.ts.map +1 -1
  40. package/dist/services/execution/TaskSelectionService.js +33 -0
  41. package/dist/services/execution/WorkOnTasksService.d.ts +4 -0
  42. package/dist/services/execution/WorkOnTasksService.d.ts.map +1 -1
  43. package/dist/services/execution/WorkOnTasksService.js +146 -22
  44. package/dist/services/openapi/OpenApiService.d.ts.map +1 -1
  45. package/dist/services/openapi/OpenApiService.js +43 -4
  46. package/dist/services/planning/CreateTasksService.d.ts +15 -0
  47. package/dist/services/planning/CreateTasksService.d.ts.map +1 -1
  48. package/dist/services/planning/CreateTasksService.js +592 -81
  49. package/dist/services/planning/RefineTasksService.d.ts +1 -0
  50. package/dist/services/planning/RefineTasksService.d.ts.map +1 -1
  51. package/dist/services/planning/RefineTasksService.js +88 -2
  52. package/dist/services/review/CodeReviewService.d.ts +6 -0
  53. package/dist/services/review/CodeReviewService.d.ts.map +1 -1
  54. package/dist/services/review/CodeReviewService.js +260 -41
  55. package/dist/services/shared/ProjectGuidance.d.ts +18 -2
  56. package/dist/services/shared/ProjectGuidance.d.ts.map +1 -1
  57. package/dist/services/shared/ProjectGuidance.js +535 -34
  58. 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 gatherDocContext(task, taskRunId, docLinks = []) {
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 filteredDocs = docs.filter((doc) => !isDocContextExcluded(doc.path ?? doc.title ?? doc.id, true));
973
- for (const doc of filteredDocs.slice(0, 5)) {
974
- const segments = (doc.segments ?? []).slice(0, 2);
975
- const body = segments.length
976
- ? segments
977
- .map((seg, idx) => ` (${idx + 1}) ${seg.heading ? `${seg.heading}: ` : ''}${seg.content.slice(0, 400)}`)
978
- .join('\n')
979
- : doc.content
980
- ? doc.content.slice(0, 600)
981
- : '';
982
- const docType = await resolveDocType(doc);
983
- if (!shouldIncludeDocType(docType))
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
- snippets.push(`- [${docType}] ${doc.title ?? doc.path ?? doc.id}\n${body}`.trim());
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
- const separator = "============================================================";
2068
- console.info(separator);
2069
- console.info("[qa-tasks] START OF TASK");
2070
- console.info(`[qa-tasks] Task key: ${task.task.key}`);
2071
- console.info(`[qa-tasks] Title: ${task.task.title ?? '(none)'}`);
2072
- console.info(`[qa-tasks] Description: ${task.task.description ?? '(none)'}`);
2073
- console.info(`[qa-tasks] Story points: ${typeof task.task.storyPoints === 'number' ? task.task.storyPoints : '(none)'}`);
2074
- console.info(`[qa-tasks] Dependencies: ${task.dependencies.keys.length ? task.dependencies.keys.join(', ') : '(none available)'}`);
2075
- if (acceptance)
2076
- console.info(`[qa-tasks] Acceptance criteria:\n${acceptance}`);
2077
- console.info(`[qa-tasks] System prompt used:\n${systemPrompt || '(none)'}`);
2078
- console.info(`[qa-tasks] Task prompt used:\n${prompt}`);
2079
- console.info(separator);
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 projectGuidance = await loadProjectGuidance(this.workspace.workspaceRoot, this.workspace.mcodaDir);
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 message = 'QA skipped: code review reported no code changes to validate.';
2658
- await this.logTask(taskRun.id, message, 'qa-skip', { reason: 'review_no_changes', decision: skipReview.decision });
2659
- if (!this.dryRunGuard) {
2660
- await this.applyStateTransition(task.task, 'pass', statusContextBase);
2661
- await this.finishTaskRun(taskRun, 'succeeded');
2662
- await this.deps.workspaceRepo.createTaskQaRun({
2663
- taskId: task.task.id,
2664
- taskRunId: taskRun.id,
2665
- jobId: ctx.jobId,
2666
- commandRunId: ctx.commandRunId,
2667
- source: 'auto',
2668
- mode: 'auto',
2669
- rawOutcome: 'pass',
2670
- recommendation: 'pass',
2671
- profileName: undefined,
2672
- runner: undefined,
2673
- metadata: { reason: 'review_no_changes', decision: skipReview.decision, reviewId: skipReview.reviewId },
2674
- });
2675
- const slug = createTaskCommentSlug({ source: 'qa-tasks', message, category: 'qa_result' });
2676
- const body = formatTaskCommentBody({
2677
- slug,
2678
- source: 'qa-tasks',
2679
- message,
2680
- status: 'resolved',
2681
- category: 'qa_result',
2682
- });
2683
- await this.deps.workspaceRepo.createTaskComment({
2684
- taskId: task.task.id,
2685
- taskRunId: taskRun.id,
2686
- jobId: ctx.jobId,
2687
- sourceCommand: 'qa-tasks',
2688
- authorType: 'agent',
2689
- category: 'qa_result',
2690
- slug,
2691
- status: 'resolved',
2692
- body,
2693
- createdAt: new Date().toISOString(),
2694
- resolvedAt: new Date().toISOString(),
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: true,
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 = request.mode ?? 'auto';
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, request, {
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, request);
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: request.profileName,
3917
- level: request.level,
3918
- agent: request.agentName,
4096
+ profile: normalizedRequest.profileName,
4097
+ level: normalizedRequest.level,
4098
+ agent: normalizedRequest.agentName,
3919
4099
  agentStream,
3920
- createFollowups: request.createFollowupTasks ?? 'auto',
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, request, {
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 = request.mode ?? 'auto';
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 = request.agentName ?? '(auto)';
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;CAC9B;AAED,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;IAgC9B,OAAO,CAAC,gBAAgB;IAgElB,WAAW,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,iBAAiB,CAAC;CA6G7E"}
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,