@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.
Files changed (55) 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/execution/QaTasksService.d.ts +6 -0
  33. package/dist/services/execution/QaTasksService.d.ts.map +1 -1
  34. package/dist/services/execution/QaTasksService.js +278 -95
  35. package/dist/services/execution/TaskSelectionService.d.ts +3 -0
  36. package/dist/services/execution/TaskSelectionService.d.ts.map +1 -1
  37. package/dist/services/execution/TaskSelectionService.js +33 -0
  38. package/dist/services/execution/WorkOnTasksService.d.ts +4 -0
  39. package/dist/services/execution/WorkOnTasksService.d.ts.map +1 -1
  40. package/dist/services/execution/WorkOnTasksService.js +141 -19
  41. package/dist/services/openapi/OpenApiService.d.ts.map +1 -1
  42. package/dist/services/openapi/OpenApiService.js +43 -4
  43. package/dist/services/planning/CreateTasksService.d.ts +10 -0
  44. package/dist/services/planning/CreateTasksService.d.ts.map +1 -1
  45. package/dist/services/planning/CreateTasksService.js +480 -44
  46. package/dist/services/planning/RefineTasksService.d.ts +1 -0
  47. package/dist/services/planning/RefineTasksService.d.ts.map +1 -1
  48. package/dist/services/planning/RefineTasksService.js +88 -2
  49. package/dist/services/review/CodeReviewService.d.ts +6 -0
  50. package/dist/services/review/CodeReviewService.d.ts.map +1 -1
  51. package/dist/services/review/CodeReviewService.js +260 -41
  52. package/dist/services/shared/ProjectGuidance.d.ts +18 -2
  53. package/dist/services/shared/ProjectGuidance.d.ts.map +1 -1
  54. package/dist/services/shared/ProjectGuidance.js +535 -34
  55. package/package.json +6 -6
@@ -49,6 +49,7 @@ export declare class RefineTasksService {
49
49
  private parseTaskKeyParts;
50
50
  private ensureTaskExists;
51
51
  private buildStoryPrompt;
52
+ private buildOpenApiHintSummary;
52
53
  private summarizeDocs;
53
54
  private summarizeHistory;
54
55
  private logWarningsToTasks;
@@ -1 +1 @@
1
- {"version":3,"file":"RefineTasksService.d.ts","sourceRoot":"","sources":["../../../src/services/planning/RefineTasksService.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAEnD,OAAO,EAAE,gBAAgB,EAA6C,mBAAmB,EAAE,MAAM,WAAW,CAAC;AAC7G,OAAO,EAIL,kBAAkB,EAClB,iBAAiB,EAKlB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,mBAAmB,EAAE,MAAM,qCAAqC,CAAC;AAC1E,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACnD,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC7D,OAAO,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAC;AAKrE,UAAU,kBAAmB,SAAQ,kBAAkB;IACrD,SAAS,EAAE,mBAAmB,CAAC;IAC/B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAwLD,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,MAAM,CAAe;IAC7B,OAAO,CAAC,UAAU,CAAa;IAC/B,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,IAAI,CAAmB;IAC/B,OAAO,CAAC,aAAa,CAAsB;IAC3C,OAAO,CAAC,cAAc,CAAiB;IACvC,OAAO,CAAC,SAAS,CAAsB;IACvC,OAAO,CAAC,aAAa,CAAC,CAAqB;gBAGzC,SAAS,EAAE,mBAAmB,EAC9B,IAAI,EAAE;QACJ,MAAM,EAAE,YAAY,CAAC;QACrB,UAAU,EAAE,UAAU,CAAC;QACvB,YAAY,EAAE,YAAY,CAAC;QAC3B,IAAI,EAAE,gBAAgB,CAAC;QACvB,aAAa,EAAE,mBAAmB,CAAC;QACnC,cAAc,EAAE,cAAc,CAAC;QAC/B,aAAa,CAAC,EAAE,kBAAkB,CAAC;KACpC;WAYU,MAAM,CAAC,SAAS,EAAE,mBAAmB,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAuB1E,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;YAkBd,cAAc;YAWd,YAAY;YAwBZ,mBAAmB;IA4CjC,OAAO,CAAC,mBAAmB;YAYb,WAAW;IA4KzB,OAAO,CAAC,iBAAiB;YASX,gBAAgB;IAmK9B,OAAO,CAAC,gBAAgB;YA+BV,aAAa;YA+Db,gBAAgB;YA+BhB,kBAAkB;IAkChC,OAAO,CAAC,aAAa;IAKrB,OAAO,CAAC,kBAAkB;IAkB1B,OAAO,CAAC,iBAAiB;IA8EzB,OAAO,CAAC,WAAW;IA0BnB,OAAO,CAAC,iBAAiB;YAkBX,eAAe;YAqYf,WAAW;IAmGnB,WAAW,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,iBAAiB,CAAC;CAma3E"}
1
+ {"version":3,"file":"RefineTasksService.d.ts","sourceRoot":"","sources":["../../../src/services/planning/RefineTasksService.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAEnD,OAAO,EAAE,gBAAgB,EAA6C,mBAAmB,EAAE,MAAM,WAAW,CAAC;AAC7G,OAAO,EAIL,kBAAkB,EAClB,iBAAiB,EAKlB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,mBAAmB,EAAE,MAAM,qCAAqC,CAAC;AAC1E,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACnD,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC7D,OAAO,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAC;AAKrE,UAAU,kBAAmB,SAAQ,kBAAkB;IACrD,SAAS,EAAE,mBAAmB,CAAC;IAC/B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AA6MD,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,MAAM,CAAe;IAC7B,OAAO,CAAC,UAAU,CAAa;IAC/B,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,IAAI,CAAmB;IAC/B,OAAO,CAAC,aAAa,CAAsB;IAC3C,OAAO,CAAC,cAAc,CAAiB;IACvC,OAAO,CAAC,SAAS,CAAsB;IACvC,OAAO,CAAC,aAAa,CAAC,CAAqB;gBAGzC,SAAS,EAAE,mBAAmB,EAC9B,IAAI,EAAE;QACJ,MAAM,EAAE,YAAY,CAAC;QACrB,UAAU,EAAE,UAAU,CAAC;QACvB,YAAY,EAAE,YAAY,CAAC;QAC3B,IAAI,EAAE,gBAAgB,CAAC;QACvB,aAAa,EAAE,mBAAmB,CAAC;QACnC,cAAc,EAAE,cAAc,CAAC;QAC/B,aAAa,CAAC,EAAE,kBAAkB,CAAC;KACpC;WAYU,MAAM,CAAC,SAAS,EAAE,mBAAmB,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAuB1E,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;YAkBd,cAAc;YAYd,YAAY;YAwBZ,mBAAmB;IA4CjC,OAAO,CAAC,mBAAmB;YAYb,WAAW;IA4KzB,OAAO,CAAC,iBAAiB;YASX,gBAAgB;IAmK9B,OAAO,CAAC,gBAAgB;IA+BxB,OAAO,CAAC,uBAAuB;YA+CjB,aAAa;YA4Eb,gBAAgB;YA+BhB,kBAAkB;IAkChC,OAAO,CAAC,aAAa;IAKrB,OAAO,CAAC,kBAAkB;IAkB1B,OAAO,CAAC,iBAAiB;IA8EzB,OAAO,CAAC,WAAW;IA0BnB,OAAO,CAAC,iBAAiB;YAkBX,eAAe;YAqYf,WAAW;IAmGnB,WAAW,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,iBAAiB,CAAC;CAma3E"}
@@ -1,5 +1,6 @@
1
1
  import path from "node:path";
2
2
  import { promises as fs } from "node:fs";
3
+ import YAML from "yaml";
3
4
  import { AgentService } from "@mcoda/agents";
4
5
  import { DocdexClient } from "@mcoda/integrations";
5
6
  import { READY_TO_CODE_REVIEW } from "@mcoda/shared";
@@ -21,7 +22,31 @@ const normalizeCreateStatus = (status) => {
21
22
  const DEFAULT_MAX_TASKS = 250;
22
23
  const MAX_AGENT_OUTPUT_CHARS = 10000000;
23
24
  const PLANNING_DOC_HINT_PATTERN = /(sds|pdr|rfp|requirements|architecture|openapi|swagger|design)/i;
25
+ const OPENAPI_METHODS = new Set(["get", "post", "put", "patch", "delete", "options", "head", "trace"]);
26
+ const OPENAPI_HINTS_LIMIT = 20;
24
27
  const estimateTokens = (text) => Math.max(1, Math.ceil(text.length / 4));
28
+ const isPlainObject = (value) => Boolean(value && typeof value === "object" && !Array.isArray(value));
29
+ const parseStructuredDoc = (raw) => {
30
+ if (!raw || raw.trim().length === 0)
31
+ return undefined;
32
+ try {
33
+ const parsed = YAML.parse(raw);
34
+ if (isPlainObject(parsed))
35
+ return parsed;
36
+ }
37
+ catch {
38
+ // continue
39
+ }
40
+ try {
41
+ const parsed = JSON.parse(raw);
42
+ if (isPlainObject(parsed))
43
+ return parsed;
44
+ }
45
+ catch {
46
+ // ignore
47
+ }
48
+ return undefined;
49
+ };
25
50
  const extractJson = (raw) => {
26
51
  const fencedMatches = [...raw.matchAll(/```json([\s\S]*?)```/g)].map((match) => match[1]);
27
52
  const stripped = raw.replace(/<think>[\s\S]*?<\/think>/g, "");
@@ -224,6 +249,7 @@ export class RefineTasksService {
224
249
  try {
225
250
  await ordering.orderTasks({
226
251
  projectKey,
252
+ apply: true,
227
253
  });
228
254
  }
229
255
  finally {
@@ -606,16 +632,72 @@ export class RefineTasksService {
606
632
  "Return JSON ONLY matching: { \"operations\": [UpdateTaskOp | SplitTaskOp | MergeTasksOp | UpdateEstimateOp] } where each item has an `op` discriminator (update_task|split_task|merge_tasks|update_estimate).",
607
633
  ].join("\n\n");
608
634
  }
635
+ buildOpenApiHintSummary(docs) {
636
+ const lines = [];
637
+ for (const doc of docs ?? []) {
638
+ const raw = typeof doc?.content === "string" && doc.content.trim().length > 0
639
+ ? doc.content
640
+ : Array.isArray(doc?.segments)
641
+ ? doc.segments
642
+ .map((segment) => (typeof segment?.content === "string" ? segment.content : ""))
643
+ .filter(Boolean)
644
+ .join("\n\n")
645
+ : "";
646
+ const parsed = parseStructuredDoc(raw);
647
+ if (!parsed)
648
+ continue;
649
+ const paths = parsed.paths;
650
+ if (!isPlainObject(paths))
651
+ continue;
652
+ for (const [apiPath, pathItem] of Object.entries(paths)) {
653
+ if (!isPlainObject(pathItem))
654
+ continue;
655
+ for (const [method, operation] of Object.entries(pathItem)) {
656
+ const normalizedMethod = method.toLowerCase();
657
+ if (!OPENAPI_METHODS.has(normalizedMethod))
658
+ continue;
659
+ if (!isPlainObject(operation))
660
+ continue;
661
+ const hints = operation["x-mcoda-task-hints"];
662
+ if (!isPlainObject(hints))
663
+ continue;
664
+ const service = typeof hints.service === "string" ? hints.service : "-";
665
+ const capability = typeof hints.capability === "string" ? hints.capability : "-";
666
+ const stage = typeof hints.stage === "string" ? hints.stage : "-";
667
+ const complexity = typeof hints.complexity === "number" && Number.isFinite(hints.complexity)
668
+ ? hints.complexity.toFixed(1)
669
+ : "-";
670
+ const countItems = (value) => Array.isArray(value) ? value.filter((item) => typeof item === "string").length : 0;
671
+ const dependsOn = countItems(hints.depends_on_operations);
672
+ const testRequirements = isPlainObject(hints.test_requirements) ? hints.test_requirements : undefined;
673
+ lines.push(`- ${normalizedMethod.toUpperCase()} ${apiPath} :: service=${service}; capability=${capability}; stage=${stage}; complexity=${complexity}; deps=${dependsOn}; tests(u/c/i/a)=${countItems(testRequirements?.unit)}/${countItems(testRequirements?.component)}/${countItems(testRequirements?.integration)}/${countItems(testRequirements?.api)}`);
674
+ if (lines.length >= OPENAPI_HINTS_LIMIT) {
675
+ return lines.join("\n");
676
+ }
677
+ }
678
+ }
679
+ }
680
+ return lines.join("\n");
681
+ }
609
682
  async summarizeDocs(projectKey, epicKey, storyKey) {
610
683
  const warnings = [];
611
684
  const startedAt = Date.now();
612
685
  try {
613
- const query = [epicKey, storyKey, "sds requirements architecture"].filter(Boolean).join(" ");
686
+ const query = [epicKey, storyKey, "sds requirements architecture openapi swagger api contracts endpoints"]
687
+ .filter(Boolean)
688
+ .join(" ");
614
689
  let docs = await this.docdex.search({
615
690
  projectKey,
616
691
  profile: "sds",
617
692
  query,
618
693
  });
694
+ if (!docs || docs.length === 0) {
695
+ docs = await this.docdex.search({
696
+ projectKey,
697
+ profile: "openapi",
698
+ query,
699
+ });
700
+ }
619
701
  if (!docs || docs.length === 0) {
620
702
  docs = await this.docdex.search({
621
703
  projectKey,
@@ -648,6 +730,10 @@ export class RefineTasksService {
648
730
  })
649
731
  .join("\n");
650
732
  const finalSummary = (summary || (selected.length ? selected.map((doc) => `- ${doc.title ?? doc.path ?? doc.id}`).join("\n") : "")).trim();
733
+ const openApiHintSummary = this.buildOpenApiHintSummary(selected);
734
+ const composedSummary = [finalSummary || "(no doc segments found)", openApiHintSummary ? `[OPENAPI_HINTS]\n${openApiHintSummary}` : ""]
735
+ .filter(Boolean)
736
+ .join("\n\n");
651
737
  const durationSeconds = (Date.now() - startedAt) / 1000;
652
738
  await this.jobService.recordTokenUsage({
653
739
  workspaceId: this.workspace.workspaceId,
@@ -662,7 +748,7 @@ export class RefineTasksService {
662
748
  timestamp: new Date().toISOString(),
663
749
  metadata: { command: "refine-tasks", action: "docdex_search", projectKey, epicKey, storyKey },
664
750
  });
665
- return { summary: finalSummary || "(no doc segments found)", warnings };
751
+ return { summary: composedSummary, warnings };
666
752
  }
667
753
  catch (error) {
668
754
  warnings.push(`Docdex lookup failed: ${error.message}`);
@@ -15,6 +15,8 @@ export interface CodeReviewRequest extends TaskSelectionFilters {
15
15
  agentStream?: boolean;
16
16
  rateAgents?: boolean;
17
17
  createFollowupTasks?: boolean;
18
+ executionContextPolicy?: "best_effort" | "require_any" | "require_sds_or_openapi";
19
+ emptyDiffApprovalPolicy?: "ready_to_qa" | "complete";
18
20
  resumeJobId?: string;
19
21
  abortSignal?: AbortSignal;
20
22
  }
@@ -88,6 +90,8 @@ export declare class CodeReviewService {
88
90
  private resolveAgent;
89
91
  private ensureRatingService;
90
92
  private resolveTaskComplexity;
93
+ private enforceExecutionContextPolicy;
94
+ private resolveExecutionPlanningContext;
91
95
  private selectTasksViaApi;
92
96
  private persistState;
93
97
  private loadState;
@@ -102,6 +106,8 @@ export declare class CodeReviewService {
102
106
  private resolveFindingSlug;
103
107
  private applyCommentResolutions;
104
108
  private extractPathsFromDiff;
109
+ private readOpenApiFileIfExists;
110
+ private resolveOpenApiContent;
105
111
  private buildOpenApiSlice;
106
112
  private buildDiff;
107
113
  private writeReviewSummaryComment;
@@ -1 +1 @@
1
- {"version":3,"file":"CodeReviewService.d.ts","sourceRoot":"","sources":["../../../src/services/review/CodeReviewService.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"CodeReviewService.d.ts","sourceRoot":"","sources":["../../../src/services/review/CodeReviewService.ts"],"names":[],"mappings":"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"}
@@ -13,7 +13,7 @@ import yaml from "yaml";
13
13
  import { createTaskKeyGenerator } from "../planning/KeyHelpers.js";
14
14
  import { RoutingService } from "../agents/RoutingService.js";
15
15
  import { AgentRatingService } from "../agents/AgentRatingService.js";
16
- import { isDocContextExcluded, loadProjectGuidance, normalizeDocType } from "../shared/ProjectGuidance.js";
16
+ import { ensureProjectGuidance, isDocContextExcluded, loadProjectGuidance, normalizeDocType, } from "../shared/ProjectGuidance.js";
17
17
  import { buildDocdexUsageGuidance } from "../shared/DocdexGuidance.js";
18
18
  import { createTaskCommentSlug, formatTaskCommentBody } from "../tasks/TaskCommentFormatter.js";
19
19
  import { AUTH_ERROR_REASON, isAuthErrorMessage } from "../shared/AuthErrors.js";
@@ -93,6 +93,34 @@ const filterOpenApiContext = (entries, hasOpenApiSnippet) => {
93
93
  }
94
94
  return filtered;
95
95
  };
96
+ const normalizeExecutionContextPolicy = (value) => {
97
+ const normalized = (value ?? "").trim().toLowerCase();
98
+ if (normalized === "require_any" || normalized === "require_sds_or_openapi") {
99
+ return normalized;
100
+ }
101
+ return "best_effort";
102
+ };
103
+ const normalizeEmptyDiffApprovalPolicy = (value, fallback = "complete") => {
104
+ const normalized = (value ?? "").trim().toLowerCase();
105
+ if (normalized === "complete")
106
+ return "complete";
107
+ if (normalized === "ready_to_qa")
108
+ return "ready_to_qa";
109
+ return fallback;
110
+ };
111
+ const classifyReviewPlanningContextKind = (doc) => {
112
+ const docType = String(doc.docType ?? "").toUpperCase();
113
+ if (docType === "SDS")
114
+ return "sds";
115
+ if (docType === "OPENAPI")
116
+ return "openapi";
117
+ const ref = String(doc.path ?? doc.title ?? "").toLowerCase();
118
+ if (/docs\/sds|\/sds\.md$|(^|\/)sds\.md$/.test(ref))
119
+ return "sds";
120
+ if (/openapi|swagger|(^|\/)openapi\.ya?ml$/.test(ref))
121
+ return "openapi";
122
+ return "doc";
123
+ };
96
124
  const estimateTokens = (text) => Math.max(1, Math.ceil((text ?? "").length / 4));
97
125
  const summarizeComments = (comments) => {
98
126
  if (!comments.length)
@@ -507,6 +535,63 @@ export class CodeReviewService {
507
535
  return undefined;
508
536
  return Math.min(10, Math.max(1, Math.round(candidate)));
509
537
  }
538
+ enforceExecutionContextPolicy(policy, context) {
539
+ if (policy === "best_effort")
540
+ return;
541
+ if (policy === "require_any") {
542
+ if (!context) {
543
+ throw new Error("Review context is required but no planning documents were resolved (policy=require_any).");
544
+ }
545
+ return;
546
+ }
547
+ if (!context) {
548
+ throw new Error("Review context requires SDS/OpenAPI sources, but none were resolved (policy=require_sds_or_openapi).");
549
+ }
550
+ if (context.kind !== "sds" && context.kind !== "openapi") {
551
+ throw new Error(`Review context policy require_sds_or_openapi rejected source '${context.source}' (kind=${context.kind}).`);
552
+ }
553
+ }
554
+ async resolveExecutionPlanningContext(projectKey, warnings) {
555
+ if (!projectKey)
556
+ return undefined;
557
+ const pickFirst = async (filter) => {
558
+ const docs = await this.deps.docdex.search({
559
+ profile: "workspace-code",
560
+ ...filter,
561
+ });
562
+ return docs[0];
563
+ };
564
+ try {
565
+ const sdsDoc = await pickFirst({ projectKey, docType: "SDS" });
566
+ if (sdsDoc) {
567
+ return {
568
+ source: sdsDoc.id ?? sdsDoc.path ?? sdsDoc.title ?? "sds",
569
+ kind: classifyReviewPlanningContextKind(sdsDoc),
570
+ };
571
+ }
572
+ const openapiDoc = await pickFirst({ projectKey, docType: "OPENAPI" });
573
+ if (openapiDoc) {
574
+ return {
575
+ source: openapiDoc.id ?? openapiDoc.path ?? openapiDoc.title ?? "openapi",
576
+ kind: classifyReviewPlanningContextKind(openapiDoc),
577
+ };
578
+ }
579
+ const fallbackDoc = await pickFirst({
580
+ projectKey,
581
+ query: "sds requirements architecture openapi swagger",
582
+ });
583
+ if (!fallbackDoc)
584
+ return undefined;
585
+ return {
586
+ source: fallbackDoc.id ?? fallbackDoc.path ?? fallbackDoc.title ?? "workspace-code",
587
+ kind: classifyReviewPlanningContextKind(fallbackDoc),
588
+ };
589
+ }
590
+ catch (error) {
591
+ warnings.push(`Review context preflight failed to query docdex: ${error.message}`);
592
+ return undefined;
593
+ }
594
+ }
510
595
  async selectTasksViaApi(filters) {
511
596
  // Prefer the backlog/task OpenAPI surface (via BacklogService) to mirror API filtering semantics.
512
597
  const backlog = await BacklogService.create(this.workspace);
@@ -570,7 +655,7 @@ export class CodeReviewService {
570
655
  }
571
656
  return Array.from(hints).slice(0, 8);
572
657
  }
573
- async gatherDocContext(taskTitle, paths, acceptance, docLinks = []) {
658
+ async gatherDocContext(taskTitle, paths, acceptance, docLinks = [], projectKey) {
574
659
  const snippets = [];
575
660
  const warnings = [];
576
661
  if (typeof this.deps.docdex?.ensureRepoScope === "function") {
@@ -597,44 +682,74 @@ export class CodeReviewService {
597
682
  }
598
683
  return normalized.docType;
599
684
  };
600
- for (const query of queries) {
685
+ const runSearch = async (filter, label) => {
601
686
  try {
602
- const docs = await withTimeout(this.deps.docdex.search({
603
- query,
604
- profile: "workspace-code",
605
- }), DOCDEX_TIMEOUT_MS, `docdex search for "${query}"`);
606
- const filteredDocs = docs.filter((doc) => !isDocContextExcluded(doc.path ?? doc.title ?? doc.id, false));
607
- snippets.push(...filteredDocs.slice(0, 2).map((doc) => {
608
- const content = (doc.segments?.[0]?.content ?? doc.content ?? "").slice(0, 400);
609
- const ref = doc.path ?? doc.id ?? doc.title ?? query;
610
- return `- [${resolveDocType(doc)}] ${ref}: ${content}`;
611
- }));
687
+ return await withTimeout(this.deps.docdex.search(filter), DOCDEX_TIMEOUT_MS, label);
612
688
  }
613
689
  catch (error) {
614
690
  if (!reindexed && typeof this.deps.docdex.reindex === "function") {
615
691
  reindexed = true;
616
- try {
617
- await this.deps.docdex.reindex();
618
- const docs = await withTimeout(this.deps.docdex.search({
619
- query,
620
- profile: "workspace-code",
621
- }), DOCDEX_TIMEOUT_MS, `docdex search for "${query}" after reindex`);
622
- const filteredDocs = docs.filter((doc) => !isDocContextExcluded(doc.path ?? doc.title ?? doc.id, false));
623
- snippets.push(...filteredDocs.slice(0, 2).map((doc) => {
624
- const content = (doc.segments?.[0]?.content ?? doc.content ?? "").slice(0, 400);
625
- const ref = doc.path ?? doc.id ?? doc.title ?? query;
626
- return `- [${resolveDocType(doc)}] ${ref}: ${content}`;
627
- }));
628
- continue;
629
- }
630
- catch (retryError) {
631
- warnings.push(`docdex search failed after reindex for ${query}: ${retryError.message}`);
632
- continue;
633
- }
692
+ await this.deps.docdex.reindex();
693
+ return await withTimeout(this.deps.docdex.search(filter), DOCDEX_TIMEOUT_MS, `${label} after reindex`);
634
694
  }
695
+ throw error;
696
+ }
697
+ };
698
+ const pushDocs = (docs, query) => {
699
+ const filteredDocs = docs.filter((doc) => !isDocContextExcluded(doc.path ?? doc.title ?? doc.id, false));
700
+ snippets.push(...filteredDocs.slice(0, 2).map((doc) => {
701
+ const content = (doc.segments?.[0]?.content ?? doc.content ?? "").slice(0, 400);
702
+ const ref = doc.path ?? doc.id ?? doc.title ?? query;
703
+ return `- [${resolveDocType(doc)}] ${ref}: ${content}`;
704
+ }));
705
+ return filteredDocs.length;
706
+ };
707
+ for (const query of queries) {
708
+ let structuredHits = 0;
709
+ try {
710
+ structuredHits += pushDocs(await runSearch({
711
+ query,
712
+ projectKey,
713
+ docType: "SDS",
714
+ profile: "workspace-code",
715
+ }, `docdex search for "${query}"`), query);
716
+ }
717
+ catch (error) {
718
+ warnings.push(`docdex search failed for SDS query ${query}: ${error.message}`);
719
+ }
720
+ try {
721
+ structuredHits += pushDocs(await runSearch({
722
+ query,
723
+ projectKey,
724
+ docType: "OPENAPI",
725
+ profile: "workspace-code",
726
+ }, `docdex search for "${query}"`), query);
727
+ }
728
+ catch (error) {
729
+ warnings.push(`docdex search failed for OPENAPI query ${query}: ${error.message}`);
730
+ }
731
+ if (structuredHits > 0) {
732
+ continue;
733
+ }
734
+ try {
735
+ pushDocs(await runSearch({
736
+ query,
737
+ projectKey,
738
+ profile: "workspace-code",
739
+ }, `docdex search for "${query}"`), query);
740
+ }
741
+ catch (error) {
635
742
  warnings.push(`docdex search failed for ${query}: ${error.message}`);
636
743
  }
637
744
  }
745
+ if (!snippets.length && projectKey) {
746
+ try {
747
+ pushDocs(await runSearch({ projectKey, query: "sds openapi requirements architecture", profile: "workspace-code" }, "docdex fallback search"), "fallback");
748
+ }
749
+ catch (error) {
750
+ warnings.push(`docdex fallback search failed: ${error.message}`);
751
+ }
752
+ }
638
753
  const normalizeDocLink = (value) => {
639
754
  const trimmed = value.trim();
640
755
  const stripped = trimmed.replace(/^docdex:/i, "").replace(/^doc:/i, "");
@@ -728,7 +843,7 @@ export class CodeReviewService {
728
843
  }
729
844
  async buildHistorySummary(taskId) {
730
845
  const comments = await this.deps.workspaceRepo.listTaskComments(taskId, {
731
- sourceCommands: ["work-on-tasks", "code-review"],
846
+ sourceCommands: ["work-on-tasks", "code-review", "qa-tasks"],
732
847
  limit: 10,
733
848
  });
734
849
  const lastReview = await this.deps.workspaceRepo.getLatestTaskReview(taskId);
@@ -755,7 +870,7 @@ export class CodeReviewService {
755
870
  }
756
871
  async loadCommentContext(taskId) {
757
872
  const comments = await this.deps.workspaceRepo.listTaskComments(taskId, {
758
- sourceCommands: ["code-review"],
873
+ sourceCommands: ["code-review", "qa-tasks"],
759
874
  limit: 50,
760
875
  });
761
876
  const unresolved = comments.filter((comment) => !comment.resolvedAt);
@@ -949,10 +1064,84 @@ export class CodeReviewService {
949
1064
  }
950
1065
  return Array.from(paths);
951
1066
  }
952
- async buildOpenApiSlice(changedPaths, acceptance) {
953
- const openapiPath = path.join(this.workspace.workspaceRoot, "openapi", "mcoda.yaml");
1067
+ async readOpenApiFileIfExists(targetPath) {
1068
+ const absolutePath = path.isAbsolute(targetPath) ? targetPath : path.join(this.workspace.workspaceRoot, targetPath);
954
1069
  try {
955
- const content = await fs.readFile(openapiPath, "utf8");
1070
+ const content = await fs.readFile(absolutePath, "utf8");
1071
+ return { sourcePath: absolutePath, content };
1072
+ }
1073
+ catch {
1074
+ return undefined;
1075
+ }
1076
+ }
1077
+ async resolveOpenApiContent(task, projectKey) {
1078
+ const metadata = task.metadata ?? {};
1079
+ const metadataPath = (typeof metadata.openapi_path === "string" && metadata.openapi_path.trim()) ||
1080
+ (typeof metadata.openapiPath === "string" && metadata.openapiPath.trim()) ||
1081
+ undefined;
1082
+ const configPath = (typeof this.workspace.config?.openapiPath === "string" && this.workspace.config.openapiPath.trim()) ||
1083
+ (typeof this.workspace.config?.openapi === "string" && this.workspace.config.openapi.trim()) ||
1084
+ undefined;
1085
+ const docLinkPaths = Array.isArray(metadata.doc_links)
1086
+ ? metadata.doc_links
1087
+ .filter((entry) => typeof entry === "string")
1088
+ .map((entry) => entry.replace(/^docdex:/i, "").replace(/^doc:/i, "").trim())
1089
+ .filter((entry) => /\.(ya?ml|json)$/i.test(entry) && /openapi|swagger/i.test(entry))
1090
+ : [];
1091
+ const candidates = [
1092
+ metadataPath,
1093
+ configPath,
1094
+ ...docLinkPaths,
1095
+ "openapi/mcoda.yaml",
1096
+ "openapi/openapi.yaml",
1097
+ "openapi/openapi.yml",
1098
+ "openapi.yaml",
1099
+ "openapi.yml",
1100
+ "docs/openapi.yaml",
1101
+ "docs/openapi.yml",
1102
+ "openapi/swagger.yaml",
1103
+ "openapi/swagger.yml",
1104
+ ].filter((entry) => Boolean(entry && entry.trim()));
1105
+ const seen = new Set();
1106
+ for (const candidate of candidates) {
1107
+ if (seen.has(candidate))
1108
+ continue;
1109
+ seen.add(candidate);
1110
+ const loaded = await this.readOpenApiFileIfExists(candidate);
1111
+ if (loaded) {
1112
+ return { source: loaded.sourcePath, content: loaded.content };
1113
+ }
1114
+ }
1115
+ try {
1116
+ const docs = await this.deps.docdex.search({
1117
+ projectKey,
1118
+ docType: "OPENAPI",
1119
+ profile: "workspace-code",
1120
+ });
1121
+ for (const doc of docs) {
1122
+ if (doc.path) {
1123
+ const loaded = await this.readOpenApiFileIfExists(doc.path);
1124
+ if (loaded) {
1125
+ return { source: loaded.sourcePath, content: loaded.content };
1126
+ }
1127
+ }
1128
+ const content = (doc.segments?.map((segment) => segment.content ?? "").join("\n") || doc.content || "").trim();
1129
+ if (content.length > 0) {
1130
+ return { source: doc.path ?? doc.id ?? doc.title ?? "docdex:openapi", content };
1131
+ }
1132
+ }
1133
+ }
1134
+ catch {
1135
+ // Best-effort fallback; no OpenAPI is acceptable for non-strict policies.
1136
+ }
1137
+ return undefined;
1138
+ }
1139
+ async buildOpenApiSlice(changedPaths, acceptance, task, projectKey) {
1140
+ const resolved = await this.resolveOpenApiContent(task, projectKey);
1141
+ if (!resolved)
1142
+ return undefined;
1143
+ try {
1144
+ const content = resolved.content;
956
1145
  const parsed = yaml.parse(content);
957
1146
  const pathHints = this.componentHintsFromPaths(changedPaths);
958
1147
  const criteriaHints = (acceptance ?? []).map((c) => c.toLowerCase()).slice(0, 5);
@@ -990,7 +1179,7 @@ export class CodeReviewService {
990
1179
  return rendered.slice(0, 8000);
991
1180
  }
992
1181
  catch {
993
- return undefined;
1182
+ return resolved.content.slice(0, 4000);
994
1183
  }
995
1184
  }
996
1185
  async buildDiff(task, baseRef, fileScope) {
@@ -1258,6 +1447,8 @@ export class CodeReviewService {
1258
1447
  await this.ensureMcoda();
1259
1448
  const agentStream = request.agentStream !== false;
1260
1449
  const baseRef = request.baseRef ?? this.workspace.config?.branch ?? DEFAULT_BASE_BRANCH;
1450
+ let executionContextPolicy = normalizeExecutionContextPolicy(request.executionContextPolicy);
1451
+ let emptyDiffApprovalPolicy = normalizeEmptyDiffApprovalPolicy(request.emptyDiffApprovalPolicy, "complete");
1261
1452
  const ignoreStatusFilter = Boolean(request.taskKeys?.length) || request.ignoreStatusFilter === true;
1262
1453
  const rawStatusFilter = ignoreStatusFilter
1263
1454
  ? []
@@ -1278,6 +1469,14 @@ export class CodeReviewService {
1278
1469
  if (rejected.length > 0 && !ignoreStatusFilter) {
1279
1470
  warnings.push(`code-review ignores unsupported statuses: ${rejected.join(", ")}. Allowed: ${REVIEW_ALLOWED_STATUSES.join(", ")}.`);
1280
1471
  }
1472
+ const executionContext = await this.resolveExecutionPlanningContext(request.projectKey, warnings);
1473
+ try {
1474
+ this.enforceExecutionContextPolicy(executionContextPolicy, executionContext);
1475
+ }
1476
+ catch (error) {
1477
+ await this.deps.jobService.finishCommandRun(commandRun.id, "failed", error instanceof Error ? error.message : String(error));
1478
+ throw error;
1479
+ }
1281
1480
  let allowFollowups = request.createFollowupTasks === true;
1282
1481
  let selectedTasks = [];
1283
1482
  if (request.resumeJobId) {
@@ -1358,6 +1557,9 @@ export class CodeReviewService {
1358
1557
  agent: request.agentName,
1359
1558
  agentStream,
1360
1559
  createFollowupTasks: allowFollowups,
1560
+ executionContextPolicy,
1561
+ executionContext: executionContext ? { source: executionContext.source, kind: executionContext.kind } : undefined,
1562
+ emptyDiffApprovalPolicy,
1361
1563
  },
1362
1564
  totalItems: selectedTaskIds.length,
1363
1565
  processedItems: 0,
@@ -1425,9 +1627,26 @@ export class CodeReviewService {
1425
1627
  }
1426
1628
  const prompts = await this.loadPrompts(agent.id);
1427
1629
  const extras = await this.loadRunbookAndChecklists();
1428
- const projectGuidance = await loadProjectGuidance(this.workspace.workspaceRoot, this.workspace.mcodaDir);
1630
+ try {
1631
+ const ensuredGuidance = await ensureProjectGuidance(this.workspace.workspaceRoot, {
1632
+ mcodaDir: this.workspace.mcodaDir,
1633
+ projectKey: request.projectKey,
1634
+ });
1635
+ for (const warning of ensuredGuidance.warnings ?? []) {
1636
+ warnings.push(`project_guidance_warning:${warning}`);
1637
+ }
1638
+ }
1639
+ catch (error) {
1640
+ warnings.push(`project_guidance_bootstrap_failed:${error instanceof Error ? error.message : String(error)}`);
1641
+ }
1642
+ const projectGuidance = await loadProjectGuidance(this.workspace.workspaceRoot, this.workspace.mcodaDir, {
1643
+ projectKey: request.projectKey,
1644
+ });
1429
1645
  if (projectGuidance) {
1430
1646
  console.info(`[code-review] loaded project guidance from ${projectGuidance.source}`);
1647
+ for (const warning of projectGuidance.warnings ?? []) {
1648
+ warnings.push(`project_guidance_warning:${warning}`);
1649
+ }
1431
1650
  }
1432
1651
  const guidanceBlock = projectGuidance?.content ? `Project Guidance (read first):\n${projectGuidance.content}` : undefined;
1433
1652
  const systemPrompts = [guidanceBlock, prompts.jobPrompt, prompts.characterPrompt, prompts.commandPrompt, ...extras].filter(Boolean);
@@ -1685,7 +1904,7 @@ export class CodeReviewService {
1685
1904
  });
1686
1905
  const changedPaths = this.extractPathsFromDiff(diff);
1687
1906
  const diffMeta = { diffEmpty, changedPaths };
1688
- const docLinks = await this.gatherDocContext(task.title, changedPaths.length ? changedPaths : allowedFiles, task.acceptanceCriteria, Array.isArray(task.metadata?.doc_links) ? task.metadata.doc_links : []);
1907
+ const docLinks = await this.gatherDocContext(task.title, changedPaths.length ? changedPaths : allowedFiles, task.acceptanceCriteria, Array.isArray(task.metadata?.doc_links) ? task.metadata.doc_links : [], request.projectKey);
1689
1908
  if (docLinks.warnings.length)
1690
1909
  warnings.push(...docLinks.warnings);
1691
1910
  await this.deps.workspaceRepo.insertTaskLog({
@@ -1696,7 +1915,7 @@ export class CodeReviewService {
1696
1915
  message: "Docdex context gathered",
1697
1916
  details: { snippets: docLinks.snippets },
1698
1917
  });
1699
- const openapiSnippet = await this.buildOpenApiSlice(changedPaths, task.acceptanceCriteria);
1918
+ const openapiSnippet = await this.buildOpenApiSlice(changedPaths, task.acceptanceCriteria, task, request.projectKey);
1700
1919
  if (!openapiSnippet) {
1701
1920
  warnings.push("OpenAPI spec not found; proceeding without snippet");
1702
1921
  }
@@ -2140,7 +2359,7 @@ export class CodeReviewService {
2140
2359
  if (!request.dryRun) {
2141
2360
  const approveDecision = parsed.decision === "approve" || parsed.decision === "info_only";
2142
2361
  if (approveDecision) {
2143
- if (diffEmpty) {
2362
+ if (diffEmpty && emptyDiffApprovalPolicy === "complete") {
2144
2363
  await this.stateService.markCompleted(task, { review_no_changes: true }, statusContext);
2145
2364
  taskStatusUpdate = "completed";
2146
2365
  }