@mcoda/core 0.1.9 → 0.1.12
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/README.md +2 -2
- package/dist/api/AgentsApi.d.ts +1 -0
- package/dist/api/AgentsApi.d.ts.map +1 -1
- package/dist/api/AgentsApi.js +136 -11
- package/dist/api/QaTasksApi.d.ts.map +1 -1
- package/dist/api/QaTasksApi.js +4 -0
- package/dist/prompts/PdrPrompts.d.ts.map +1 -1
- package/dist/prompts/PdrPrompts.js +6 -0
- package/dist/prompts/SdsPrompts.d.ts.map +1 -1
- package/dist/prompts/SdsPrompts.js +7 -0
- package/dist/services/agents/AgentRatingService.d.ts +19 -0
- package/dist/services/agents/AgentRatingService.d.ts.map +1 -1
- package/dist/services/agents/AgentRatingService.js +66 -2
- package/dist/services/agents/GatewayAgentService.d.ts +8 -0
- package/dist/services/agents/GatewayAgentService.d.ts.map +1 -1
- package/dist/services/agents/GatewayAgentService.js +462 -65
- package/dist/services/agents/GatewayHandoff.d.ts +5 -1
- package/dist/services/agents/GatewayHandoff.d.ts.map +1 -1
- package/dist/services/agents/GatewayHandoff.js +65 -32
- package/dist/services/agents/RoutingService.d.ts +1 -0
- package/dist/services/agents/RoutingService.d.ts.map +1 -1
- package/dist/services/agents/RoutingService.js +4 -4
- package/dist/services/backlog/BacklogService.d.ts +23 -0
- package/dist/services/backlog/BacklogService.d.ts.map +1 -1
- package/dist/services/backlog/BacklogService.js +62 -7
- package/dist/services/backlog/TaskOrderingHeuristics.d.ts +12 -0
- package/dist/services/backlog/TaskOrderingHeuristics.d.ts.map +1 -0
- package/dist/services/backlog/TaskOrderingHeuristics.js +56 -0
- package/dist/services/backlog/TaskOrderingService.d.ts +16 -4
- package/dist/services/backlog/TaskOrderingService.d.ts.map +1 -1
- package/dist/services/backlog/TaskOrderingService.js +529 -73
- package/dist/services/docs/DocInventory.d.ts +11 -0
- package/dist/services/docs/DocInventory.d.ts.map +1 -0
- package/dist/services/docs/DocInventory.js +230 -0
- package/dist/services/docs/DocgenRunContext.d.ts +59 -0
- package/dist/services/docs/DocgenRunContext.d.ts.map +1 -0
- package/dist/services/docs/DocgenRunContext.js +4 -0
- package/dist/services/docs/DocsService.d.ts +59 -2
- package/dist/services/docs/DocsService.d.ts.map +1 -1
- package/dist/services/docs/DocsService.js +1701 -48
- package/dist/services/docs/alignment/DocAlignmentGraph.d.ts +23 -0
- package/dist/services/docs/alignment/DocAlignmentGraph.d.ts.map +1 -0
- package/dist/services/docs/alignment/DocAlignmentGraph.js +78 -0
- package/dist/services/docs/alignment/DocAlignmentPatcher.d.ts +19 -0
- package/dist/services/docs/alignment/DocAlignmentPatcher.d.ts.map +1 -0
- package/dist/services/docs/alignment/DocAlignmentPatcher.js +222 -0
- package/dist/services/docs/patch/DocPatchEngine.d.ts +57 -0
- package/dist/services/docs/patch/DocPatchEngine.d.ts.map +1 -0
- package/dist/services/docs/patch/DocPatchEngine.js +331 -0
- package/dist/services/docs/review/Glossary.d.ts +16 -0
- package/dist/services/docs/review/Glossary.d.ts.map +1 -0
- package/dist/services/docs/review/Glossary.js +47 -0
- package/dist/services/docs/review/ReviewReportRenderer.d.ts +3 -0
- package/dist/services/docs/review/ReviewReportRenderer.d.ts.map +1 -0
- package/dist/services/docs/review/ReviewReportRenderer.js +133 -0
- package/dist/services/docs/review/ReviewReportSchema.d.ts +39 -0
- package/dist/services/docs/review/ReviewReportSchema.d.ts.map +1 -0
- package/dist/services/docs/review/ReviewReportSchema.js +47 -0
- package/dist/services/docs/review/ReviewTypes.d.ts +76 -0
- package/dist/services/docs/review/ReviewTypes.d.ts.map +1 -0
- package/dist/services/docs/review/ReviewTypes.js +94 -0
- package/dist/services/docs/review/gates/AdminOpenApiSpecGate.d.ts +7 -0
- package/dist/services/docs/review/gates/AdminOpenApiSpecGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/AdminOpenApiSpecGate.js +93 -0
- package/dist/services/docs/review/gates/ApiPathConsistencyGate.d.ts +7 -0
- package/dist/services/docs/review/gates/ApiPathConsistencyGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/ApiPathConsistencyGate.js +308 -0
- package/dist/services/docs/review/gates/BuildReadyCompletenessGate.d.ts +8 -0
- package/dist/services/docs/review/gates/BuildReadyCompletenessGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/BuildReadyCompletenessGate.js +278 -0
- package/dist/services/docs/review/gates/DeploymentBlueprintGate.d.ts +8 -0
- package/dist/services/docs/review/gates/DeploymentBlueprintGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/DeploymentBlueprintGate.js +487 -0
- package/dist/services/docs/review/gates/NoMaybesGate.d.ts +8 -0
- package/dist/services/docs/review/gates/NoMaybesGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/NoMaybesGate.js +145 -0
- package/dist/services/docs/review/gates/OpenApiCoverageGate.d.ts +7 -0
- package/dist/services/docs/review/gates/OpenApiCoverageGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/OpenApiCoverageGate.js +266 -0
- package/dist/services/docs/review/gates/OpenApiSchemaSanityGate.d.ts +7 -0
- package/dist/services/docs/review/gates/OpenApiSchemaSanityGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/OpenApiSchemaSanityGate.js +59 -0
- package/dist/services/docs/review/gates/OpenQuestionsGate.d.ts +7 -0
- package/dist/services/docs/review/gates/OpenQuestionsGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/OpenQuestionsGate.js +200 -0
- package/dist/services/docs/review/gates/PdrInterfacesGate.d.ts +7 -0
- package/dist/services/docs/review/gates/PdrInterfacesGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/PdrInterfacesGate.js +159 -0
- package/dist/services/docs/review/gates/PdrOpenQuestionsGate.d.ts +8 -0
- package/dist/services/docs/review/gates/PdrOpenQuestionsGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/PdrOpenQuestionsGate.js +129 -0
- package/dist/services/docs/review/gates/PdrOwnershipGate.d.ts +7 -0
- package/dist/services/docs/review/gates/PdrOwnershipGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/PdrOwnershipGate.js +169 -0
- package/dist/services/docs/review/gates/PlaceholderArtifactGate.d.ts +10 -0
- package/dist/services/docs/review/gates/PlaceholderArtifactGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/PlaceholderArtifactGate.js +261 -0
- package/dist/services/docs/review/gates/RfpConsentGate.d.ts +6 -0
- package/dist/services/docs/review/gates/RfpConsentGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/RfpConsentGate.js +127 -0
- package/dist/services/docs/review/gates/RfpDefinitionGate.d.ts +7 -0
- package/dist/services/docs/review/gates/RfpDefinitionGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/RfpDefinitionGate.js +173 -0
- package/dist/services/docs/review/gates/SdsAdaptersGate.d.ts +7 -0
- package/dist/services/docs/review/gates/SdsAdaptersGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/SdsAdaptersGate.js +196 -0
- package/dist/services/docs/review/gates/SdsDecisionsGate.d.ts +7 -0
- package/dist/services/docs/review/gates/SdsDecisionsGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/SdsDecisionsGate.js +89 -0
- package/dist/services/docs/review/gates/SdsOpsGate.d.ts +7 -0
- package/dist/services/docs/review/gates/SdsOpsGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/SdsOpsGate.js +162 -0
- package/dist/services/docs/review/gates/SdsPolicyTelemetryGate.d.ts +7 -0
- package/dist/services/docs/review/gates/SdsPolicyTelemetryGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/SdsPolicyTelemetryGate.js +166 -0
- package/dist/services/docs/review/gates/SqlRequiredTablesGate.d.ts +7 -0
- package/dist/services/docs/review/gates/SqlRequiredTablesGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/SqlRequiredTablesGate.js +273 -0
- package/dist/services/docs/review/gates/SqlSyntaxGate.d.ts +7 -0
- package/dist/services/docs/review/gates/SqlSyntaxGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/SqlSyntaxGate.js +203 -0
- package/dist/services/docs/review/gates/TerminologyNormalizationGate.d.ts +9 -0
- package/dist/services/docs/review/gates/TerminologyNormalizationGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/TerminologyNormalizationGate.js +217 -0
- package/dist/services/docs/review/glossary.json +47 -0
- package/dist/services/estimate/EstimateService.d.ts +2 -0
- package/dist/services/estimate/EstimateService.d.ts.map +1 -1
- package/dist/services/estimate/EstimateService.js +66 -18
- package/dist/services/estimate/VelocityService.d.ts +4 -0
- package/dist/services/estimate/VelocityService.d.ts.map +1 -1
- package/dist/services/estimate/VelocityService.js +179 -36
- package/dist/services/estimate/types.d.ts +1 -0
- package/dist/services/estimate/types.d.ts.map +1 -1
- package/dist/services/execution/GatewayTrioService.d.ts +71 -4
- package/dist/services/execution/GatewayTrioService.d.ts.map +1 -1
- package/dist/services/execution/GatewayTrioService.js +1695 -328
- package/dist/services/execution/QaApiRunner.d.ts +30 -0
- package/dist/services/execution/QaApiRunner.d.ts.map +1 -0
- package/dist/services/execution/QaApiRunner.js +881 -0
- package/dist/services/execution/QaFollowupService.d.ts +1 -0
- package/dist/services/execution/QaFollowupService.d.ts.map +1 -1
- package/dist/services/execution/QaFollowupService.js +8 -2
- package/dist/services/execution/QaPlanValidator.d.ts +10 -0
- package/dist/services/execution/QaPlanValidator.d.ts.map +1 -0
- package/dist/services/execution/QaPlanValidator.js +128 -0
- package/dist/services/execution/QaProfileService.d.ts +21 -1
- package/dist/services/execution/QaProfileService.d.ts.map +1 -1
- package/dist/services/execution/QaProfileService.js +214 -29
- package/dist/services/execution/QaTasksService.d.ts +41 -1
- package/dist/services/execution/QaTasksService.d.ts.map +1 -1
- package/dist/services/execution/QaTasksService.js +2851 -500
- package/dist/services/execution/QaTestCommandBuilder.d.ts +51 -0
- package/dist/services/execution/QaTestCommandBuilder.d.ts.map +1 -0
- package/dist/services/execution/QaTestCommandBuilder.js +495 -0
- package/dist/services/execution/TaskSelectionService.d.ts +4 -2
- package/dist/services/execution/TaskSelectionService.d.ts.map +1 -1
- package/dist/services/execution/TaskSelectionService.js +144 -28
- package/dist/services/execution/TaskStateService.d.ts +19 -6
- package/dist/services/execution/TaskStateService.d.ts.map +1 -1
- package/dist/services/execution/TaskStateService.js +128 -13
- package/dist/services/execution/WorkOnTasksService.d.ts +19 -2
- package/dist/services/execution/WorkOnTasksService.d.ts.map +1 -1
- package/dist/services/execution/WorkOnTasksService.js +3913 -1225
- package/dist/services/jobs/JobInsightsService.d.ts +4 -0
- package/dist/services/jobs/JobInsightsService.d.ts.map +1 -1
- package/dist/services/jobs/JobInsightsService.js +51 -5
- package/dist/services/jobs/JobResumeService.d.ts.map +1 -1
- package/dist/services/jobs/JobResumeService.js +23 -10
- package/dist/services/jobs/JobService.d.ts +56 -4
- package/dist/services/jobs/JobService.d.ts.map +1 -1
- package/dist/services/jobs/JobService.js +232 -1
- package/dist/services/openapi/OpenApiService.d.ts +41 -0
- package/dist/services/openapi/OpenApiService.d.ts.map +1 -1
- package/dist/services/openapi/OpenApiService.js +889 -98
- package/dist/services/planning/CreateTasksService.d.ts +15 -0
- package/dist/services/planning/CreateTasksService.d.ts.map +1 -1
- package/dist/services/planning/CreateTasksService.js +311 -6
- package/dist/services/planning/RefineTasksService.d.ts +4 -0
- package/dist/services/planning/RefineTasksService.d.ts.map +1 -1
- package/dist/services/planning/RefineTasksService.js +225 -24
- package/dist/services/review/CodeReviewService.d.ts +4 -0
- package/dist/services/review/CodeReviewService.d.ts.map +1 -1
- package/dist/services/review/CodeReviewService.js +778 -232
- package/dist/services/review/ReviewNormalizer.d.ts +9 -0
- package/dist/services/review/ReviewNormalizer.d.ts.map +1 -0
- package/dist/services/review/ReviewNormalizer.js +147 -0
- package/dist/services/shared/AuthErrors.d.ts +3 -0
- package/dist/services/shared/AuthErrors.d.ts.map +1 -0
- package/dist/services/shared/AuthErrors.js +17 -0
- package/dist/services/shared/DocdexGuidance.d.ts +7 -0
- package/dist/services/shared/DocdexGuidance.d.ts.map +1 -0
- package/dist/services/shared/DocdexGuidance.js +12 -0
- package/dist/services/shared/ProjectGuidance.d.ts +12 -1
- package/dist/services/shared/ProjectGuidance.d.ts.map +1 -1
- package/dist/services/shared/ProjectGuidance.js +64 -7
- package/dist/services/system/ToolDenylist.d.ts +13 -0
- package/dist/services/system/ToolDenylist.d.ts.map +1 -0
- package/dist/services/system/ToolDenylist.js +85 -0
- package/dist/services/telemetry/TelemetryService.d.ts.map +1 -1
- package/dist/services/telemetry/TelemetryService.js +39 -7
- package/dist/workspace/WorkspaceManager.d.ts +22 -0
- package/dist/workspace/WorkspaceManager.d.ts.map +1 -1
- package/dist/workspace/WorkspaceManager.js +203 -32
- package/package.json +6 -5
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import fs from "node:fs";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
3
4
|
import { AgentService } from "@mcoda/agents";
|
|
4
5
|
import { DocdexClient } from "@mcoda/integrations";
|
|
5
6
|
import { GlobalRepository, WorkspaceRepository } from "@mcoda/db";
|
|
6
|
-
import { canonicalizeCommandName, getCommandRequiredCapabilities } from "@mcoda/shared";
|
|
7
|
+
import { READY_TO_CODE_REVIEW, canonicalizeCommandName, getCommandRequiredCapabilities } from "@mcoda/shared";
|
|
7
8
|
import { JobService } from "../jobs/JobService.js";
|
|
8
9
|
import { TaskSelectionService } from "../execution/TaskSelectionService.js";
|
|
9
10
|
import { RoutingService } from "./RoutingService.js";
|
|
11
|
+
import { isDocContextExcluded, normalizeDocType } from "../shared/ProjectGuidance.js";
|
|
10
12
|
const DEFAULT_GATEWAY_PROMPT = [
|
|
11
13
|
"You are the gateway agent. Read the task context and docdex snippets, digest the task, decide what is done vs. remaining, and plan the work.",
|
|
12
14
|
"You must identify concrete file paths to modify or create before offloading.",
|
|
15
|
+
"If new directories are required for planned files, list them explicitly in dirsToCreate.",
|
|
13
16
|
"Do not use placeholders like (unknown), TBD, or glob patterns in file paths.",
|
|
17
|
+
"Do not assume repository structure; only name paths grounded in provided file content or docdex context.",
|
|
18
|
+
"If you add or modify tests, ensure tests/all.js is updated (or state that it already covers the new tests).",
|
|
14
19
|
"If docdex returns no results, say so in docdexNotes.",
|
|
15
20
|
"Do not leave currentState, todo, or understanding blank.",
|
|
16
21
|
"Put reasoningSummary near the top of the JSON object so it appears early in the stream.",
|
|
@@ -28,12 +33,15 @@ const DEFAULT_GATEWAY_PROMPT = [
|
|
|
28
33
|
' "discipline": "backend|frontend|uiux|docs|architecture|qa|planning|ops|other",',
|
|
29
34
|
' "filesLikelyTouched": ["path/to/file.ext"],',
|
|
30
35
|
' "filesToCreate": ["path/to/new_file.ext"],',
|
|
36
|
+
' "dirsToCreate": ["path/to/new_dir"],',
|
|
31
37
|
' "assumptions": ["assumption 1"],',
|
|
32
38
|
' "risks": ["risk 1"],',
|
|
33
39
|
' "docdexNotes": ["notes about docdex coverage/gaps"]',
|
|
34
40
|
"}",
|
|
35
41
|
"If information is missing, keep arrays empty and mention the gap in assumptions or docdexNotes.",
|
|
36
42
|
].join("\n");
|
|
43
|
+
const REPO_PROMPTS_DIR = fileURLToPath(new URL("../../../../../prompts/", import.meta.url));
|
|
44
|
+
const resolveRepoPromptPath = (filename) => path.join(REPO_PROMPTS_DIR, filename);
|
|
37
45
|
const REQUIRED_PROMPT_MARKERS = [
|
|
38
46
|
'"summary"',
|
|
39
47
|
'"reasoningSummary"',
|
|
@@ -42,27 +50,63 @@ const REQUIRED_PROMPT_MARKERS = [
|
|
|
42
50
|
'"understanding"',
|
|
43
51
|
'"filesLikelyTouched"',
|
|
44
52
|
'"filesToCreate"',
|
|
53
|
+
'"dirsToCreate"',
|
|
45
54
|
];
|
|
46
55
|
const hasRequiredPromptMarkers = (content) => REQUIRED_PROMPT_MARKERS.every((marker) => content.includes(marker));
|
|
47
56
|
const DEFAULT_JOB_PROMPT = "You are an mcoda agent that follows workspace runbooks and responds with actionable, concise output.";
|
|
48
57
|
const DEFAULT_CHARACTER_PROMPT = "Write clearly, avoid hallucinations, cite assumptions, and prioritize risk mitigation for the user.";
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
58
|
+
const ROUTING_PROMPT_MARKERS = [
|
|
59
|
+
"routing gateway",
|
|
60
|
+
"choose a route",
|
|
61
|
+
"devstral-local",
|
|
62
|
+
"glm-worker",
|
|
63
|
+
"codex-architect",
|
|
64
|
+
"complexity from 1 to 5",
|
|
65
|
+
];
|
|
66
|
+
const sanitizeAgentOutput = (output) => {
|
|
67
|
+
if (!output.includes("[agent-io]"))
|
|
68
|
+
return output;
|
|
69
|
+
const lines = output.split(/\r?\n/);
|
|
70
|
+
const cleaned = [];
|
|
71
|
+
for (const line of lines) {
|
|
72
|
+
if (!line.includes("[agent-io]")) {
|
|
73
|
+
cleaned.push(line);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (/^\s*\[agent-io\]\s*(?:begin|input|meta)/i.test(line)) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const stripped = line.replace(/^\s*\[agent-io\]\s*(?:output\s*)?/i, "");
|
|
80
|
+
if (stripped.trim())
|
|
81
|
+
cleaned.push(stripped);
|
|
82
|
+
}
|
|
83
|
+
return cleaned.join("\n");
|
|
84
|
+
};
|
|
85
|
+
const extractJsonOnly = (raw) => {
|
|
86
|
+
const trimmed = raw.trim();
|
|
87
|
+
if (!trimmed)
|
|
88
|
+
return { jsonOnly: false };
|
|
89
|
+
let candidate = trimmed;
|
|
90
|
+
if (trimmed.startsWith("```")) {
|
|
91
|
+
const match = trimmed.match(/^```(?:json)?\s*([\s\S]*?)```$/i);
|
|
92
|
+
if (!match)
|
|
93
|
+
return { jsonOnly: false };
|
|
94
|
+
candidate = match[1].trim();
|
|
95
|
+
}
|
|
96
|
+
else if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) {
|
|
97
|
+
return { jsonOnly: false };
|
|
98
|
+
}
|
|
57
99
|
try {
|
|
58
|
-
return JSON.parse(
|
|
100
|
+
return { payload: JSON.parse(candidate), jsonOnly: true };
|
|
59
101
|
}
|
|
60
102
|
catch {
|
|
61
|
-
return
|
|
103
|
+
return { jsonOnly: true };
|
|
62
104
|
}
|
|
63
105
|
};
|
|
64
106
|
const estimateTokens = (text) => Math.max(1, Math.ceil((text ?? "").length / 4));
|
|
65
107
|
const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
|
|
108
|
+
const STRONG_TIER_MIN_COMPLEXITY = 5;
|
|
109
|
+
const SPECIALIST_TIER_MIN_COMPLEXITY = 8;
|
|
66
110
|
const normalizeList = (value) => {
|
|
67
111
|
if (Array.isArray(value))
|
|
68
112
|
return value.map((item) => String(item)).filter(Boolean);
|
|
@@ -81,6 +125,20 @@ const normalizeTextField = (value) => {
|
|
|
81
125
|
}
|
|
82
126
|
return undefined;
|
|
83
127
|
};
|
|
128
|
+
const isRoutingPrompt = (value) => {
|
|
129
|
+
const lower = value.toLowerCase();
|
|
130
|
+
return ROUTING_PROMPT_MARKERS.some((marker) => lower.includes(marker));
|
|
131
|
+
};
|
|
132
|
+
const sanitizeGatewayPrompt = (value) => {
|
|
133
|
+
if (!value)
|
|
134
|
+
return undefined;
|
|
135
|
+
const trimmed = value.trim();
|
|
136
|
+
if (!trimmed)
|
|
137
|
+
return undefined;
|
|
138
|
+
if (isRoutingPrompt(trimmed))
|
|
139
|
+
return undefined;
|
|
140
|
+
return trimmed;
|
|
141
|
+
};
|
|
84
142
|
const isPlaceholderPath = (value) => {
|
|
85
143
|
const lower = value.trim().toLowerCase();
|
|
86
144
|
if (!lower)
|
|
@@ -93,7 +151,77 @@ const isPlaceholderPath = (value) => {
|
|
|
93
151
|
return false;
|
|
94
152
|
};
|
|
95
153
|
const normalizeFileList = (value) => normalizeList(value).map((item) => item.trim()).filter((item) => item.length > 0 && !isPlaceholderPath(item));
|
|
96
|
-
const
|
|
154
|
+
const normalizeDirList = (value) => normalizeList(value)
|
|
155
|
+
.map((item) => item.trim().replace(/[/\\]+$/, ""))
|
|
156
|
+
.filter((item) => item.length > 0 && !isPlaceholderPath(item));
|
|
157
|
+
const isDirMarker = (value) => {
|
|
158
|
+
const trimmed = value.trim();
|
|
159
|
+
if (!trimmed)
|
|
160
|
+
return false;
|
|
161
|
+
if (/[/\\]$/.test(trimmed))
|
|
162
|
+
return true;
|
|
163
|
+
if (/^\s*(?:dir|directory)\s*:/i.test(trimmed))
|
|
164
|
+
return true;
|
|
165
|
+
if (/\(\s*dir\s*\)\s*$/i.test(trimmed))
|
|
166
|
+
return true;
|
|
167
|
+
return false;
|
|
168
|
+
};
|
|
169
|
+
const stripDirMarker = (value) => {
|
|
170
|
+
let trimmed = value.trim();
|
|
171
|
+
trimmed = trimmed.replace(/^\s*(?:dir|directory)\s*:/i, "");
|
|
172
|
+
trimmed = trimmed.replace(/\(\s*dir\s*\)\s*$/i, "");
|
|
173
|
+
trimmed = trimmed.replace(/[/\\]+$/, "");
|
|
174
|
+
return trimmed.trim();
|
|
175
|
+
};
|
|
176
|
+
const splitFileAndDirEntries = (values) => {
|
|
177
|
+
const files = [];
|
|
178
|
+
const dirs = [];
|
|
179
|
+
for (const value of values) {
|
|
180
|
+
const trimmed = value.trim();
|
|
181
|
+
if (!trimmed)
|
|
182
|
+
continue;
|
|
183
|
+
if (isDirMarker(trimmed)) {
|
|
184
|
+
const cleaned = stripDirMarker(trimmed);
|
|
185
|
+
if (cleaned)
|
|
186
|
+
dirs.push(cleaned);
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
files.push(trimmed);
|
|
190
|
+
}
|
|
191
|
+
return { files, dirs };
|
|
192
|
+
};
|
|
193
|
+
const hasFileContextJustification = (raw) => {
|
|
194
|
+
const notes = [...normalizeList(raw?.assumptions), ...normalizeList(raw?.docdexNotes)]
|
|
195
|
+
.map((item) => item.toLowerCase())
|
|
196
|
+
.join(" ");
|
|
197
|
+
if (!notes.trim())
|
|
198
|
+
return false;
|
|
199
|
+
const patterns = [
|
|
200
|
+
/\bno file(?:s)?\b/,
|
|
201
|
+
/\bmissing file(?:s)?\b/,
|
|
202
|
+
/\bunknown file(?:s)?\b/,
|
|
203
|
+
/\bfile context\b/,
|
|
204
|
+
/\binsufficient context\b/,
|
|
205
|
+
/\bnot enough context\b/,
|
|
206
|
+
/\bnot provided\b/,
|
|
207
|
+
/\bdocdex unavailable\b/,
|
|
208
|
+
/\bdocdex missing\b/,
|
|
209
|
+
/\bno matching docs?\b/,
|
|
210
|
+
/\bno matching documents?\b/,
|
|
211
|
+
/\bno results\b/,
|
|
212
|
+
];
|
|
213
|
+
return patterns.some((pattern) => pattern.test(notes));
|
|
214
|
+
};
|
|
215
|
+
const assessFileListCoverage = (raw) => {
|
|
216
|
+
const filesLikelyTouched = normalizeFileList(raw?.filesLikelyTouched);
|
|
217
|
+
const rawCreate = normalizeList(raw?.filesToCreate);
|
|
218
|
+
const splitCreate = splitFileAndDirEntries(rawCreate);
|
|
219
|
+
const filesToCreate = normalizeFileList(splitCreate.files);
|
|
220
|
+
const dirsToCreate = [...normalizeDirList(raw?.dirsToCreate), ...normalizeDirList(splitCreate.dirs)];
|
|
221
|
+
const empty = filesLikelyTouched.length === 0 && filesToCreate.length === 0 && dirsToCreate.length === 0;
|
|
222
|
+
return { empty, justified: empty && hasFileContextJustification(raw) };
|
|
223
|
+
};
|
|
224
|
+
const listMissingFields = (raw, options) => {
|
|
97
225
|
const missing = [];
|
|
98
226
|
const summary = normalizeTextField(raw?.summary);
|
|
99
227
|
const reasoningSummary = normalizeTextField(raw?.reasoningSummary);
|
|
@@ -102,7 +230,11 @@ const listMissingFields = (raw) => {
|
|
|
102
230
|
const understanding = normalizeTextField(raw?.understanding);
|
|
103
231
|
const plan = normalizeList(raw?.plan);
|
|
104
232
|
const filesLikelyTouched = normalizeFileList(raw?.filesLikelyTouched);
|
|
105
|
-
const
|
|
233
|
+
const rawCreate = normalizeList(raw?.filesToCreate);
|
|
234
|
+
const splitCreate = splitFileAndDirEntries(rawCreate);
|
|
235
|
+
const filesToCreate = normalizeFileList(splitCreate.files);
|
|
236
|
+
const dirsToCreate = [...normalizeDirList(raw?.dirsToCreate), ...normalizeDirList(splitCreate.dirs)];
|
|
237
|
+
const allowEmptyFiles = options?.allowEmptyFiles ?? false;
|
|
106
238
|
if (!summary)
|
|
107
239
|
missing.push("summary");
|
|
108
240
|
if (!reasoningSummary)
|
|
@@ -115,8 +247,9 @@ const listMissingFields = (raw) => {
|
|
|
115
247
|
missing.push("understanding");
|
|
116
248
|
if (plan.length === 0)
|
|
117
249
|
missing.push("plan");
|
|
118
|
-
if (filesLikelyTouched.length === 0 && filesToCreate.length === 0)
|
|
119
|
-
missing.push("
|
|
250
|
+
if (!allowEmptyFiles && filesLikelyTouched.length === 0 && filesToCreate.length === 0 && dirsToCreate.length === 0) {
|
|
251
|
+
missing.push("filesLikelyTouched", "filesToCreate", "dirsToCreate");
|
|
252
|
+
}
|
|
120
253
|
return missing;
|
|
121
254
|
};
|
|
122
255
|
const normalizeDiscipline = (value) => {
|
|
@@ -178,21 +311,29 @@ const EXPLORATION_RATE = 0.1;
|
|
|
178
311
|
const DEFAULT_STATUS_FILTER = [
|
|
179
312
|
"not_started",
|
|
180
313
|
"in_progress",
|
|
181
|
-
|
|
182
|
-
"ready_to_review",
|
|
314
|
+
READY_TO_CODE_REVIEW,
|
|
183
315
|
"ready_to_qa",
|
|
184
316
|
"completed",
|
|
185
317
|
"cancelled",
|
|
186
318
|
"failed",
|
|
187
319
|
"skipped",
|
|
188
320
|
];
|
|
189
|
-
const summarizeDoc = (doc, index) => {
|
|
321
|
+
const summarizeDoc = (doc, index, warnings) => {
|
|
190
322
|
const title = doc.title ?? doc.path ?? doc.id ?? `doc-${index + 1}`;
|
|
191
323
|
const excerptSource = doc.segments?.[0]?.content ?? doc.content ?? "";
|
|
192
324
|
const excerpt = excerptSource ? (excerptSource.length > 480 ? `${excerptSource.slice(0, 480)}...` : excerptSource) : undefined;
|
|
325
|
+
const normalized = normalizeDocType({
|
|
326
|
+
docType: doc.docType,
|
|
327
|
+
path: doc.path,
|
|
328
|
+
title: doc.title,
|
|
329
|
+
content: excerptSource,
|
|
330
|
+
});
|
|
331
|
+
if (normalized.downgraded && warnings) {
|
|
332
|
+
warnings.push(`Docdex docType downgraded from SDS to DOC for ${doc.path ?? doc.title ?? doc.id}: ${normalized.reason ?? "not_sds"}`);
|
|
333
|
+
}
|
|
193
334
|
return {
|
|
194
335
|
id: doc.id ?? `doc-${index + 1}`,
|
|
195
|
-
docType:
|
|
336
|
+
docType: normalized.docType,
|
|
196
337
|
title,
|
|
197
338
|
path: doc.path,
|
|
198
339
|
excerpt,
|
|
@@ -211,6 +352,25 @@ const buildDocContext = (docs) => {
|
|
|
211
352
|
}),
|
|
212
353
|
].join("\n");
|
|
213
354
|
};
|
|
355
|
+
const PLACEHOLDER_DEPENDENCY_PATTERN = /^(?:t|task-?)\d+$/i;
|
|
356
|
+
const normalizeDependencies = (deps) => {
|
|
357
|
+
if (!deps?.length)
|
|
358
|
+
return undefined;
|
|
359
|
+
const seen = new Set();
|
|
360
|
+
const filtered = [];
|
|
361
|
+
for (const dep of deps) {
|
|
362
|
+
const trimmed = dep.trim();
|
|
363
|
+
if (!trimmed)
|
|
364
|
+
continue;
|
|
365
|
+
if (PLACEHOLDER_DEPENDENCY_PATTERN.test(trimmed))
|
|
366
|
+
continue;
|
|
367
|
+
if (seen.has(trimmed))
|
|
368
|
+
continue;
|
|
369
|
+
seen.add(trimmed);
|
|
370
|
+
filtered.push(trimmed);
|
|
371
|
+
}
|
|
372
|
+
return filtered.length ? filtered : undefined;
|
|
373
|
+
};
|
|
214
374
|
const buildTaskContext = (tasks) => {
|
|
215
375
|
if (tasks.length === 0)
|
|
216
376
|
return "Task context: (no task records found)";
|
|
@@ -241,9 +401,11 @@ export class GatewayAgentService {
|
|
|
241
401
|
const globalRepo = await GlobalRepository.create();
|
|
242
402
|
const agentService = new AgentService(globalRepo);
|
|
243
403
|
const routingService = await RoutingService.create();
|
|
404
|
+
const docdexRepoId = workspace.config?.docdexRepoId ?? process.env.MCODA_DOCDEX_REPO_ID ?? process.env.DOCDEX_REPO_ID;
|
|
244
405
|
const docdex = new DocdexClient({
|
|
245
406
|
workspaceRoot: workspace.workspaceRoot,
|
|
246
407
|
baseUrl: workspace.config?.docdexUrl ?? process.env.MCODA_DOCDEX_URL,
|
|
408
|
+
repoId: docdexRepoId,
|
|
247
409
|
});
|
|
248
410
|
const workspaceRepo = await WorkspaceRepository.create(workspace.workspaceRoot);
|
|
249
411
|
const jobService = new JobService(workspace, workspaceRepo);
|
|
@@ -292,10 +454,31 @@ export class GatewayAgentService {
|
|
|
292
454
|
await maybeClose(this.deps.workspaceRepo);
|
|
293
455
|
await maybeClose(this.deps.routingService);
|
|
294
456
|
}
|
|
457
|
+
setDocdexAvailability(available, reason) {
|
|
458
|
+
if (available)
|
|
459
|
+
return;
|
|
460
|
+
const docdex = this.deps.docdex;
|
|
461
|
+
if (docdex && typeof docdex.disable === "function") {
|
|
462
|
+
docdex.disable(reason);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
async saveRepoMemory(text) {
|
|
466
|
+
const docdex = this.deps.docdex;
|
|
467
|
+
if (!docdex || typeof docdex.memorySave !== "function")
|
|
468
|
+
return;
|
|
469
|
+
await docdex.memorySave(text);
|
|
470
|
+
}
|
|
471
|
+
async savePreference(category, content, agentId = "default") {
|
|
472
|
+
const docdex = this.deps.docdex;
|
|
473
|
+
if (!docdex || typeof docdex.savePreference !== "function")
|
|
474
|
+
return;
|
|
475
|
+
await docdex.savePreference(agentId, category, content);
|
|
476
|
+
}
|
|
295
477
|
async loadGatewayPrompts(agentId) {
|
|
296
478
|
const agentPrompts = "getPrompts" in this.deps.agentService ? await this.deps.agentService.getPrompts(agentId) : undefined;
|
|
297
|
-
const mcodaPromptPath = path.join(this.workspace.
|
|
479
|
+
const mcodaPromptPath = path.join(this.workspace.mcodaDir, "prompts", "gateway-agent.md");
|
|
298
480
|
const workspacePromptPath = path.join(this.workspace.workspaceRoot, "prompts", "gateway-agent.md");
|
|
481
|
+
const repoPromptPath = resolveRepoPromptPath("gateway-agent.md");
|
|
299
482
|
try {
|
|
300
483
|
await fs.promises.mkdir(path.dirname(mcodaPromptPath), { recursive: true });
|
|
301
484
|
await fs.promises.access(mcodaPromptPath);
|
|
@@ -306,7 +489,13 @@ export class GatewayAgentService {
|
|
|
306
489
|
await fs.promises.copyFile(workspacePromptPath, mcodaPromptPath);
|
|
307
490
|
}
|
|
308
491
|
catch {
|
|
309
|
-
|
|
492
|
+
try {
|
|
493
|
+
await fs.promises.access(repoPromptPath);
|
|
494
|
+
await fs.promises.copyFile(repoPromptPath, mcodaPromptPath);
|
|
495
|
+
}
|
|
496
|
+
catch {
|
|
497
|
+
await fs.promises.writeFile(mcodaPromptPath, DEFAULT_GATEWAY_PROMPT, "utf8");
|
|
498
|
+
}
|
|
310
499
|
}
|
|
311
500
|
}
|
|
312
501
|
try {
|
|
@@ -320,7 +509,15 @@ export class GatewayAgentService {
|
|
|
320
509
|
}
|
|
321
510
|
}
|
|
322
511
|
catch {
|
|
323
|
-
|
|
512
|
+
try {
|
|
513
|
+
const repoPrompt = await fs.promises.readFile(repoPromptPath, "utf8");
|
|
514
|
+
if (hasRequiredPromptMarkers(repoPrompt)) {
|
|
515
|
+
nextPrompt = repoPrompt.trim();
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
catch {
|
|
519
|
+
/* ignore */
|
|
520
|
+
}
|
|
324
521
|
}
|
|
325
522
|
await fs.promises.writeFile(mcodaPromptPath, nextPrompt, "utf8");
|
|
326
523
|
}
|
|
@@ -328,7 +525,7 @@ export class GatewayAgentService {
|
|
|
328
525
|
catch {
|
|
329
526
|
/* ignore */
|
|
330
527
|
}
|
|
331
|
-
const commandPromptFiles = (await this.readPromptFiles([mcodaPromptPath, workspacePromptPath])).filter(hasRequiredPromptMarkers);
|
|
528
|
+
const commandPromptFiles = (await this.readPromptFiles([mcodaPromptPath, workspacePromptPath, repoPromptPath])).filter(hasRequiredPromptMarkers);
|
|
332
529
|
const mergedCommandPrompt = (() => {
|
|
333
530
|
const parts = [...commandPromptFiles];
|
|
334
531
|
const agentCommandPrompt = agentPrompts?.commandPrompts?.["gateway-agent"];
|
|
@@ -339,9 +536,11 @@ export class GatewayAgentService {
|
|
|
339
536
|
parts.push(DEFAULT_GATEWAY_PROMPT);
|
|
340
537
|
return parts.filter(Boolean).join("\n\n");
|
|
341
538
|
})();
|
|
539
|
+
const sanitizedJobPrompt = sanitizeGatewayPrompt(agentPrompts?.jobPrompt);
|
|
540
|
+
const sanitizedCharacterPrompt = sanitizeGatewayPrompt(agentPrompts?.characterPrompt);
|
|
342
541
|
return {
|
|
343
|
-
jobPrompt:
|
|
344
|
-
characterPrompt:
|
|
542
|
+
jobPrompt: sanitizedJobPrompt ?? DEFAULT_JOB_PROMPT,
|
|
543
|
+
characterPrompt: sanitizedCharacterPrompt ?? DEFAULT_CHARACTER_PROMPT,
|
|
345
544
|
commandPrompt: mergedCommandPrompt,
|
|
346
545
|
};
|
|
347
546
|
}
|
|
@@ -413,7 +612,7 @@ export class GatewayAgentService {
|
|
|
413
612
|
if (text && onChunk)
|
|
414
613
|
onChunk(text);
|
|
415
614
|
}
|
|
416
|
-
return { output, durationSeconds: (Date.now() - startedAt) / 1000 };
|
|
615
|
+
return { output: sanitizeAgentOutput(output), durationSeconds: (Date.now() - startedAt) / 1000 };
|
|
417
616
|
}
|
|
418
617
|
}
|
|
419
618
|
catch (error) {
|
|
@@ -426,7 +625,7 @@ export class GatewayAgentService {
|
|
|
426
625
|
input: prompt,
|
|
427
626
|
metadata: { command: "gateway-agent", job },
|
|
428
627
|
});
|
|
429
|
-
const output = response.output ?? "";
|
|
628
|
+
const output = sanitizeAgentOutput(response.output ?? "");
|
|
430
629
|
if (output && onChunk)
|
|
431
630
|
onChunk(output);
|
|
432
631
|
return { output, durationSeconds: (Date.now() - startedAt) / 1000 };
|
|
@@ -447,27 +646,81 @@ export class GatewayAgentService {
|
|
|
447
646
|
taskKeys: request.taskKeys,
|
|
448
647
|
statusFilter: request.statusFilter?.length ? request.statusFilter : DEFAULT_STATUS_FILTER,
|
|
449
648
|
limit,
|
|
649
|
+
ignoreDependencies: request.taskKeys && request.taskKeys.length > 0 ? true : request.ignoreDependencies,
|
|
450
650
|
};
|
|
451
651
|
const selection = await this.taskSelectionService.selectTasks(filters);
|
|
452
652
|
if (selection.warnings.length)
|
|
453
653
|
warnings.push(...selection.warnings);
|
|
454
|
-
const combined = [...selection.ordered
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
654
|
+
const combined = [...selection.ordered];
|
|
655
|
+
if (combined.length) {
|
|
656
|
+
return combined.slice(0, limit).map((entry) => ({
|
|
657
|
+
key: entry.task.key,
|
|
658
|
+
title: entry.task.title,
|
|
659
|
+
description: entry.task.description ?? undefined,
|
|
660
|
+
status: entry.task.status,
|
|
661
|
+
storyPoints: entry.task.storyPoints ?? undefined,
|
|
662
|
+
storyKey: entry.task.storyKey,
|
|
663
|
+
storyTitle: entry.task.storyTitle,
|
|
664
|
+
epicKey: entry.task.epicKey,
|
|
665
|
+
epicTitle: entry.task.epicTitle,
|
|
666
|
+
acceptanceCriteria: entry.task.acceptanceCriteria,
|
|
667
|
+
dependencies: normalizeDependencies(entry.dependencies.keys),
|
|
668
|
+
}));
|
|
669
|
+
}
|
|
670
|
+
if (!request.taskKeys?.length)
|
|
671
|
+
return [];
|
|
672
|
+
warnings.push("Gateway task selection returned no tasks; falling back to direct task lookup.");
|
|
673
|
+
const tasks = await Promise.all(request.taskKeys.map((key) => this.deps.workspaceRepo.getTaskByKey(key)));
|
|
674
|
+
const found = tasks.filter((task) => Boolean(task));
|
|
675
|
+
const missing = request.taskKeys.filter((key, index) => !tasks[index]);
|
|
676
|
+
if (missing.length) {
|
|
677
|
+
warnings.push(`Fallback task lookup missing keys: ${missing.join(", ")}`);
|
|
678
|
+
}
|
|
679
|
+
if (!found.length)
|
|
680
|
+
return [];
|
|
681
|
+
let withRelations = [];
|
|
682
|
+
try {
|
|
683
|
+
const related = await this.deps.workspaceRepo.getTasksWithRelations(found.map((task) => task.id));
|
|
684
|
+
const relatedById = new Map(related.map((task) => [task.id, task]));
|
|
685
|
+
withRelations = found.map((task) => relatedById.get(task.id) ?? task);
|
|
686
|
+
}
|
|
687
|
+
catch {
|
|
688
|
+
withRelations = found;
|
|
689
|
+
}
|
|
690
|
+
return withRelations.slice(0, limit).map((task) => ({
|
|
691
|
+
key: task.key,
|
|
692
|
+
title: task.title,
|
|
693
|
+
description: task.description ?? undefined,
|
|
694
|
+
status: task.status,
|
|
695
|
+
storyPoints: task.storyPoints ?? undefined,
|
|
696
|
+
storyKey: task.storyKey,
|
|
697
|
+
storyTitle: task.storyTitle,
|
|
698
|
+
epicKey: task.epicKey,
|
|
699
|
+
epicTitle: task.epicTitle,
|
|
700
|
+
acceptanceCriteria: task.acceptanceCriteria,
|
|
701
|
+
dependencies: undefined,
|
|
467
702
|
}));
|
|
468
703
|
}
|
|
469
704
|
catch (error) {
|
|
470
705
|
warnings.push(`Task lookup failed: ${error.message}`);
|
|
706
|
+
if (request.taskKeys?.length) {
|
|
707
|
+
warnings.push("Task lookup failed; attempting direct task fallback.");
|
|
708
|
+
try {
|
|
709
|
+
const tasks = await Promise.all(request.taskKeys.map((key) => this.deps.workspaceRepo.getTaskByKey(key)));
|
|
710
|
+
const found = tasks.filter((task) => Boolean(task));
|
|
711
|
+
return found.map((task) => ({
|
|
712
|
+
key: task.key,
|
|
713
|
+
title: task.title,
|
|
714
|
+
description: task.description ?? undefined,
|
|
715
|
+
status: task.status,
|
|
716
|
+
storyPoints: task.storyPoints ?? undefined,
|
|
717
|
+
dependencies: undefined,
|
|
718
|
+
}));
|
|
719
|
+
}
|
|
720
|
+
catch {
|
|
721
|
+
return [];
|
|
722
|
+
}
|
|
723
|
+
}
|
|
471
724
|
return [];
|
|
472
725
|
}
|
|
473
726
|
}
|
|
@@ -511,22 +764,59 @@ export class GatewayAgentService {
|
|
|
511
764
|
const maxDocs = request.maxDocs ?? 4;
|
|
512
765
|
if (maxDocs <= 0)
|
|
513
766
|
return [];
|
|
767
|
+
if (typeof this.deps.docdex?.ensureRepoScope === "function") {
|
|
768
|
+
try {
|
|
769
|
+
await this.deps.docdex.ensureRepoScope();
|
|
770
|
+
}
|
|
771
|
+
catch (error) {
|
|
772
|
+
warnings.push(`Docdex scope missing: ${error.message}`);
|
|
773
|
+
return [];
|
|
774
|
+
}
|
|
775
|
+
}
|
|
514
776
|
const docTypes = this.pickDocTypes(request.job, request.inputText);
|
|
515
777
|
const query = this.buildQuerySeed(tasks, request.inputText);
|
|
516
778
|
const summaries = [];
|
|
779
|
+
let openApiIncluded = false;
|
|
780
|
+
let reindexed = false;
|
|
517
781
|
for (const docType of docTypes) {
|
|
518
782
|
if (summaries.length >= maxDocs)
|
|
519
783
|
break;
|
|
520
784
|
try {
|
|
521
|
-
|
|
785
|
+
let docs = await this.deps.docdex.search({
|
|
522
786
|
projectKey: request.projectKey,
|
|
523
787
|
docType,
|
|
524
788
|
query,
|
|
525
789
|
});
|
|
790
|
+
if (!docs.length && !reindexed && typeof this.deps.docdex.reindex === "function") {
|
|
791
|
+
reindexed = true;
|
|
792
|
+
try {
|
|
793
|
+
warnings.push("Docdex search returned no results; reindexing and retrying.");
|
|
794
|
+
await this.deps.docdex.reindex();
|
|
795
|
+
docs = await this.deps.docdex.search({
|
|
796
|
+
projectKey: request.projectKey,
|
|
797
|
+
docType,
|
|
798
|
+
query,
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
catch (error) {
|
|
802
|
+
warnings.push(`Docdex reindex failed: ${error.message}`);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
526
805
|
for (const doc of docs) {
|
|
527
806
|
if (summaries.length >= maxDocs)
|
|
528
807
|
break;
|
|
529
|
-
|
|
808
|
+
const ref = doc.path ?? doc.title ?? doc.id;
|
|
809
|
+
if (isDocContextExcluded(ref, false)) {
|
|
810
|
+
warnings.push(`Docdex doc filtered from gateway context: ${ref}`);
|
|
811
|
+
continue;
|
|
812
|
+
}
|
|
813
|
+
const summary = summarizeDoc(doc, summaries.length, warnings);
|
|
814
|
+
if (summary.docType === "OPENAPI") {
|
|
815
|
+
if (openApiIncluded)
|
|
816
|
+
continue;
|
|
817
|
+
openApiIncluded = true;
|
|
818
|
+
}
|
|
819
|
+
summaries.push(summary);
|
|
530
820
|
}
|
|
531
821
|
}
|
|
532
822
|
catch (error) {
|
|
@@ -549,7 +839,10 @@ export class GatewayAgentService {
|
|
|
549
839
|
const understanding = normalizeTextField(raw?.understanding) ?? "";
|
|
550
840
|
const plan = normalizeList(raw?.plan);
|
|
551
841
|
const filesLikelyTouched = normalizeFileList(raw?.filesLikelyTouched);
|
|
552
|
-
const
|
|
842
|
+
const rawCreate = normalizeList(raw?.filesToCreate);
|
|
843
|
+
const splitCreate = splitFileAndDirEntries(rawCreate);
|
|
844
|
+
const filesToCreate = normalizeFileList(splitCreate.files);
|
|
845
|
+
const dirsToCreate = [...normalizeDirList(raw?.dirsToCreate), ...normalizeDirList(splitCreate.dirs)];
|
|
553
846
|
const complexityRaw = Number(raw?.complexity);
|
|
554
847
|
const complexity = Number.isFinite(complexityRaw) ? clamp(Math.round(complexityRaw), 1, 10) : 5;
|
|
555
848
|
const discipline = normalizeDiscipline(typeof raw?.discipline === "string" ? raw.discipline : undefined) ??
|
|
@@ -579,6 +872,7 @@ export class GatewayAgentService {
|
|
|
579
872
|
discipline,
|
|
580
873
|
filesLikelyTouched,
|
|
581
874
|
filesToCreate,
|
|
875
|
+
dirsToCreate,
|
|
582
876
|
assumptions: normalizeList(raw?.assumptions),
|
|
583
877
|
risks: normalizeList(raw?.risks),
|
|
584
878
|
docdexNotes: normalizeList(raw?.docdexNotes),
|
|
@@ -594,6 +888,36 @@ export class GatewayAgentService {
|
|
|
594
888
|
const isInside = (relative) => !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
595
889
|
const touched = [];
|
|
596
890
|
const created = [];
|
|
891
|
+
const dirs = [];
|
|
892
|
+
const dirSet = new Set();
|
|
893
|
+
for (const dir of analysis.dirsToCreate ?? []) {
|
|
894
|
+
const { relative, resolved } = normalize(dir);
|
|
895
|
+
if (!isInside(relative)) {
|
|
896
|
+
warnings.push(`Gateway directory path outside workspace ignored: ${dir}`);
|
|
897
|
+
continue;
|
|
898
|
+
}
|
|
899
|
+
try {
|
|
900
|
+
const stat = await fs.promises.stat(resolved);
|
|
901
|
+
if (stat.isDirectory()) {
|
|
902
|
+
const normalized = relative.replace(/\\/g, "/");
|
|
903
|
+
if (!dirSet.has(normalized)) {
|
|
904
|
+
dirSet.add(normalized);
|
|
905
|
+
dirs.push(normalized);
|
|
906
|
+
}
|
|
907
|
+
continue;
|
|
908
|
+
}
|
|
909
|
+
warnings.push(`Gateway directory path is not a directory: ${dir}`);
|
|
910
|
+
continue;
|
|
911
|
+
}
|
|
912
|
+
catch {
|
|
913
|
+
const normalized = relative.replace(/\\/g, "/");
|
|
914
|
+
if (!dirSet.has(normalized)) {
|
|
915
|
+
dirSet.add(normalized);
|
|
916
|
+
dirs.push(normalized);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
const dirPrefixes = Array.from(dirSet).map((dir) => (dir.endsWith("/") ? dir : `${dir}/`));
|
|
597
921
|
for (const file of analysis.filesLikelyTouched) {
|
|
598
922
|
const { relative, resolved } = normalize(file);
|
|
599
923
|
if (!isInside(relative)) {
|
|
@@ -628,8 +952,14 @@ export class GatewayAgentService {
|
|
|
628
952
|
}
|
|
629
953
|
}
|
|
630
954
|
catch {
|
|
631
|
-
|
|
632
|
-
|
|
955
|
+
const parentRelative = path.relative(root, parent).replace(/\\/g, "/");
|
|
956
|
+
const parentAllowed = parentRelative === "" ||
|
|
957
|
+
dirSet.has(parentRelative) ||
|
|
958
|
+
dirPrefixes.some((prefix) => parentRelative.startsWith(prefix));
|
|
959
|
+
if (!parentAllowed) {
|
|
960
|
+
warnings.push(`Gateway create path parent does not exist: ${file}`);
|
|
961
|
+
continue;
|
|
962
|
+
}
|
|
633
963
|
}
|
|
634
964
|
try {
|
|
635
965
|
const stat = await fs.promises.stat(resolved);
|
|
@@ -648,6 +978,7 @@ export class GatewayAgentService {
|
|
|
648
978
|
...analysis,
|
|
649
979
|
filesLikelyTouched: touched,
|
|
650
980
|
filesToCreate: created,
|
|
981
|
+
dirsToCreate: dirs,
|
|
651
982
|
};
|
|
652
983
|
}
|
|
653
984
|
async listCandidates(requiredCaps, discipline, avoidAgents = []) {
|
|
@@ -786,13 +1117,46 @@ export class GatewayAgentService {
|
|
|
786
1117
|
rationale: `Complexity ${normalizedComplexity}/10 targets a comparable tier agent; selected closest match with discipline fit and cost awareness.${gatingNote}`,
|
|
787
1118
|
};
|
|
788
1119
|
}
|
|
789
|
-
async selectAgentForJob(job, analysis, avoidAgents = [], forceStronger = false) {
|
|
1120
|
+
async selectAgentForJob(job, analysis, avoidAgents = [], forceStronger = false, forceTier, warnings = []) {
|
|
790
1121
|
const normalizedJob = canonicalizeCommandName(job);
|
|
791
1122
|
const requiredCaps = getCommandRequiredCapabilities(normalizedJob);
|
|
792
|
-
|
|
793
|
-
|
|
1123
|
+
let candidates = await this.listCandidates(requiredCaps, analysis.discipline, avoidAgents);
|
|
1124
|
+
if (!candidates.length && avoidAgents.length) {
|
|
1125
|
+
const fallback = await this.listCandidates(requiredCaps, analysis.discipline, []);
|
|
1126
|
+
if (fallback.length) {
|
|
1127
|
+
candidates = fallback;
|
|
1128
|
+
warnings.push("Avoid list removed all eligible agents; reusing available agent.");
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
const baseComplexity = Number.isFinite(analysis.complexity)
|
|
1132
|
+
? clamp(Math.round(analysis.complexity), 1, 10)
|
|
1133
|
+
: 5;
|
|
1134
|
+
const forcedMin = forceTier === "specialist"
|
|
1135
|
+
? SPECIALIST_TIER_MIN_COMPLEXITY
|
|
1136
|
+
: forceTier === "strong"
|
|
1137
|
+
? STRONG_TIER_MIN_COMPLEXITY
|
|
1138
|
+
: undefined;
|
|
1139
|
+
let boostedComplexity = baseComplexity;
|
|
1140
|
+
if (typeof forcedMin === "number") {
|
|
1141
|
+
boostedComplexity = Math.max(baseComplexity, forcedMin);
|
|
1142
|
+
}
|
|
1143
|
+
else if (forceStronger) {
|
|
1144
|
+
if (baseComplexity < STRONG_TIER_MIN_COMPLEXITY) {
|
|
1145
|
+
boostedComplexity = STRONG_TIER_MIN_COMPLEXITY;
|
|
1146
|
+
}
|
|
1147
|
+
else if (baseComplexity < SPECIALIST_TIER_MIN_COMPLEXITY) {
|
|
1148
|
+
boostedComplexity = SPECIALIST_TIER_MIN_COMPLEXITY;
|
|
1149
|
+
}
|
|
1150
|
+
else {
|
|
1151
|
+
boostedComplexity = clamp(baseComplexity + 1, 1, 10);
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
794
1154
|
const { pick, rationale } = this.chooseCandidate(candidates, boostedComplexity, analysis.discipline);
|
|
795
|
-
const finalRationale =
|
|
1155
|
+
const finalRationale = forceTier
|
|
1156
|
+
? `${rationale} (force_tier:${forceTier})`
|
|
1157
|
+
: forceStronger
|
|
1158
|
+
? `${rationale} (force_stronger applied)`
|
|
1159
|
+
: rationale;
|
|
796
1160
|
return {
|
|
797
1161
|
agentId: pick.agent.id,
|
|
798
1162
|
agentSlug: pick.agent.slug ?? pick.agent.id,
|
|
@@ -803,6 +1167,23 @@ export class GatewayAgentService {
|
|
|
803
1167
|
rationale: finalRationale,
|
|
804
1168
|
};
|
|
805
1169
|
}
|
|
1170
|
+
async preflightExecutionAgents(job, overrideAgent) {
|
|
1171
|
+
const normalizedJob = canonicalizeCommandName(job);
|
|
1172
|
+
const requiredCaps = getCommandRequiredCapabilities(normalizedJob);
|
|
1173
|
+
if (overrideAgent) {
|
|
1174
|
+
const resolved = await this.deps.agentService.resolveAgent(overrideAgent);
|
|
1175
|
+
const capabilities = await this.deps.globalRepo.getAgentCapabilities(resolved.id);
|
|
1176
|
+
const missing = requiredCaps.filter((cap) => !capabilities.includes(cap));
|
|
1177
|
+
if (missing.length) {
|
|
1178
|
+
throw new Error(`Agent ${overrideAgent} is missing required capabilities for ${normalizedJob}: ${missing.join(", ")}`);
|
|
1179
|
+
}
|
|
1180
|
+
return;
|
|
1181
|
+
}
|
|
1182
|
+
const candidates = await this.listCandidates(requiredCaps, "other");
|
|
1183
|
+
if (!candidates.length) {
|
|
1184
|
+
throw new Error(`No eligible execution agents available for ${normalizedJob}.`);
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
806
1187
|
async run(request) {
|
|
807
1188
|
const warnings = [];
|
|
808
1189
|
const normalizedJob = canonicalizeCommandName(request.job);
|
|
@@ -820,7 +1201,7 @@ export class GatewayAgentService {
|
|
|
820
1201
|
]
|
|
821
1202
|
.filter(Boolean)
|
|
822
1203
|
.join("\n\n");
|
|
823
|
-
const recordUsage = async (promptText, outputText, durationSeconds, action) => {
|
|
1204
|
+
const recordUsage = async (promptText, outputText, durationSeconds, action, attempt) => {
|
|
824
1205
|
const promptTokens = estimateTokens(promptText);
|
|
825
1206
|
const completionTokens = estimateTokens(outputText ?? "");
|
|
826
1207
|
await this.deps.jobService.recordTokenUsage({
|
|
@@ -836,20 +1217,24 @@ export class GatewayAgentService {
|
|
|
836
1217
|
tokensCompletion: completionTokens,
|
|
837
1218
|
tokensTotal: promptTokens + completionTokens,
|
|
838
1219
|
durationSeconds,
|
|
839
|
-
metadata: { action, job: normalizedJob },
|
|
1220
|
+
metadata: { action, job: normalizedJob, phase: action, attempt },
|
|
840
1221
|
});
|
|
841
1222
|
};
|
|
842
1223
|
const response = await this.invokeGatewayAgent(gatewayAgent, prompt, normalizedJob, {
|
|
843
1224
|
stream: request.agentStream !== false,
|
|
844
1225
|
onChunk: request.onStreamChunk,
|
|
845
1226
|
});
|
|
846
|
-
await recordUsage(prompt, response.output ?? "", response.durationSeconds, "gateway_summary");
|
|
847
|
-
|
|
1227
|
+
await recordUsage(prompt, response.output ?? "", response.durationSeconds, "gateway_summary", 1);
|
|
1228
|
+
const jsonResult = extractJsonOnly(response.output);
|
|
1229
|
+
let parsed = jsonResult.payload;
|
|
1230
|
+
let fileListCoverage = parsed ? assessFileListCoverage(parsed) : { empty: false, justified: false };
|
|
848
1231
|
let missingFields = parsed
|
|
849
|
-
? listMissingFields(parsed)
|
|
850
|
-
: ["summary", "reasoningSummary", "currentState", "todo", "understanding", "plan", "
|
|
1232
|
+
? listMissingFields(parsed, { allowEmptyFiles: true })
|
|
1233
|
+
: ["summary", "reasoningSummary", "currentState", "todo", "understanding", "plan", "filesLikelyTouched", "filesToCreate", "dirsToCreate"];
|
|
851
1234
|
if (!parsed) {
|
|
852
|
-
warnings.push(
|
|
1235
|
+
warnings.push(jsonResult.jsonOnly
|
|
1236
|
+
? "Gateway analysis response was invalid JSON."
|
|
1237
|
+
: "Gateway analysis response was not JSON-only.");
|
|
853
1238
|
}
|
|
854
1239
|
if (missingFields.length) {
|
|
855
1240
|
const repairPrompt = [
|
|
@@ -857,7 +1242,8 @@ export class GatewayAgentService {
|
|
|
857
1242
|
"",
|
|
858
1243
|
"Your previous response was incomplete or invalid. Return JSON only with the exact schema.",
|
|
859
1244
|
`Missing fields: ${missingFields.join(", ")}.`,
|
|
860
|
-
"Ensure reasoningSummary, currentState, todo, understanding,
|
|
1245
|
+
"Ensure reasoningSummary, currentState, todo, understanding, and plan are populated.",
|
|
1246
|
+
"If file paths are unknown, leave filesLikelyTouched/filesToCreate/dirsToCreate empty and explain the gap in assumptions or docdexNotes.",
|
|
861
1247
|
"Use real file paths only (no placeholders like (unknown), TBD, or glob patterns).",
|
|
862
1248
|
"If docdex returned no results, say so in docdexNotes.",
|
|
863
1249
|
].join("\n");
|
|
@@ -868,18 +1254,29 @@ export class GatewayAgentService {
|
|
|
868
1254
|
stream: request.agentStream !== false,
|
|
869
1255
|
onChunk: request.onStreamChunk,
|
|
870
1256
|
});
|
|
871
|
-
await recordUsage(repairPrompt, repairResponse.output ?? "", repairResponse.durationSeconds, "gateway_summary_repair");
|
|
872
|
-
const repaired =
|
|
873
|
-
if (repaired) {
|
|
874
|
-
parsed = repaired;
|
|
875
|
-
|
|
1257
|
+
await recordUsage(repairPrompt, repairResponse.output ?? "", repairResponse.durationSeconds, "gateway_summary_repair", 2);
|
|
1258
|
+
const repaired = extractJsonOnly(repairResponse.output);
|
|
1259
|
+
if (repaired.payload) {
|
|
1260
|
+
parsed = repaired.payload;
|
|
1261
|
+
fileListCoverage = assessFileListCoverage(parsed);
|
|
1262
|
+
missingFields = listMissingFields(parsed, { allowEmptyFiles: true });
|
|
876
1263
|
}
|
|
877
1264
|
else {
|
|
878
|
-
warnings.push(
|
|
1265
|
+
warnings.push(repaired.jsonOnly
|
|
1266
|
+
? "Gateway repair response was invalid JSON."
|
|
1267
|
+
: "Gateway repair response was not JSON-only.");
|
|
879
1268
|
}
|
|
880
1269
|
}
|
|
881
1270
|
if (missingFields.length) {
|
|
882
|
-
|
|
1271
|
+
throw new Error(`Gateway analysis missing required fields: ${missingFields.join(", ")}.`);
|
|
1272
|
+
}
|
|
1273
|
+
if (!parsed) {
|
|
1274
|
+
throw new Error(jsonResult.jsonOnly
|
|
1275
|
+
? "Gateway analysis response was invalid JSON."
|
|
1276
|
+
: "Gateway analysis response was not JSON-only.");
|
|
1277
|
+
}
|
|
1278
|
+
if (fileListCoverage.empty && !fileListCoverage.justified) {
|
|
1279
|
+
warnings.push("Gateway analysis returned no file paths; proceeding without file context.");
|
|
883
1280
|
}
|
|
884
1281
|
let analysis = this.normalizeAnalysis(parsed ?? {}, normalizedJob, tasks, request.inputText);
|
|
885
1282
|
if (analysis.docdexNotes.length === 0) {
|
|
@@ -893,7 +1290,7 @@ export class GatewayAgentService {
|
|
|
893
1290
|
analysis.docdexNotes.push(...docdexWarnings);
|
|
894
1291
|
}
|
|
895
1292
|
analysis = await this.validateFilePlan(analysis, warnings);
|
|
896
|
-
const chosenAgent = await this.selectAgentForJob(normalizedJob, analysis, request.avoidAgents ?? [], request.forceStronger ?? false);
|
|
1293
|
+
const chosenAgent = await this.selectAgentForJob(normalizedJob, analysis, request.avoidAgents ?? [], request.forceStronger ?? false, request.forceTier, warnings);
|
|
897
1294
|
await this.deps.jobService.finishCommandRun(commandRun.id, "succeeded");
|
|
898
1295
|
return {
|
|
899
1296
|
commandRunId: commandRun.id,
|