@mcoda/core 0.1.19 → 0.1.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/QaTasksApi.d.ts.map +1 -1
- package/dist/api/QaTasksApi.js +3 -0
- package/dist/prompts/PdrPrompts.d.ts.map +1 -1
- package/dist/prompts/PdrPrompts.js +22 -8
- package/dist/prompts/SdsPrompts.d.ts.map +1 -1
- package/dist/prompts/SdsPrompts.js +53 -34
- package/dist/services/backlog/BacklogService.d.ts.map +1 -1
- package/dist/services/backlog/BacklogService.js +3 -0
- package/dist/services/backlog/TaskOrderingService.d.ts +9 -0
- package/dist/services/backlog/TaskOrderingService.d.ts.map +1 -1
- package/dist/services/backlog/TaskOrderingService.js +251 -35
- package/dist/services/docs/DocsService.d.ts.map +1 -1
- package/dist/services/docs/DocsService.js +487 -71
- package/dist/services/docs/review/gates/PdrFolderTreeGate.d.ts +7 -0
- package/dist/services/docs/review/gates/PdrFolderTreeGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/PdrFolderTreeGate.js +151 -0
- package/dist/services/docs/review/gates/PdrNoUnresolvedItemsGate.d.ts +7 -0
- package/dist/services/docs/review/gates/PdrNoUnresolvedItemsGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/PdrNoUnresolvedItemsGate.js +109 -0
- package/dist/services/docs/review/gates/PdrTechStackRationaleGate.d.ts +7 -0
- package/dist/services/docs/review/gates/PdrTechStackRationaleGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/PdrTechStackRationaleGate.js +128 -0
- package/dist/services/docs/review/gates/SdsFolderTreeGate.d.ts +7 -0
- package/dist/services/docs/review/gates/SdsFolderTreeGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/SdsFolderTreeGate.js +153 -0
- package/dist/services/docs/review/gates/SdsNoUnresolvedItemsGate.d.ts +7 -0
- package/dist/services/docs/review/gates/SdsNoUnresolvedItemsGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/SdsNoUnresolvedItemsGate.js +109 -0
- package/dist/services/docs/review/gates/SdsTechStackRationaleGate.d.ts +7 -0
- package/dist/services/docs/review/gates/SdsTechStackRationaleGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/SdsTechStackRationaleGate.js +128 -0
- package/dist/services/execution/QaTasksService.d.ts +6 -0
- package/dist/services/execution/QaTasksService.d.ts.map +1 -1
- package/dist/services/execution/QaTasksService.js +278 -95
- package/dist/services/execution/TaskSelectionService.d.ts +3 -0
- package/dist/services/execution/TaskSelectionService.d.ts.map +1 -1
- package/dist/services/execution/TaskSelectionService.js +33 -0
- package/dist/services/execution/WorkOnTasksService.d.ts +4 -0
- package/dist/services/execution/WorkOnTasksService.d.ts.map +1 -1
- package/dist/services/execution/WorkOnTasksService.js +141 -19
- package/dist/services/openapi/OpenApiService.d.ts.map +1 -1
- package/dist/services/openapi/OpenApiService.js +43 -4
- package/dist/services/planning/CreateTasksService.d.ts +10 -0
- package/dist/services/planning/CreateTasksService.d.ts.map +1 -1
- package/dist/services/planning/CreateTasksService.js +480 -44
- package/dist/services/planning/RefineTasksService.d.ts +1 -0
- package/dist/services/planning/RefineTasksService.d.ts.map +1 -1
- package/dist/services/planning/RefineTasksService.js +88 -2
- package/dist/services/review/CodeReviewService.d.ts +6 -0
- package/dist/services/review/CodeReviewService.d.ts.map +1 -1
- package/dist/services/review/CodeReviewService.js +260 -41
- package/dist/services/shared/ProjectGuidance.d.ts +18 -2
- package/dist/services/shared/ProjectGuidance.d.ts.map +1 -1
- package/dist/services/shared/ProjectGuidance.js +535 -34
- package/package.json +6 -6
|
@@ -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 { GlobalRepository, WorkspaceRepository, } from "@mcoda/db";
|
|
5
6
|
import { setTimeout as delay } from "node:timers/promises";
|
|
@@ -128,6 +129,7 @@ const extractScriptPort = (script) => {
|
|
|
128
129
|
};
|
|
129
130
|
const estimateTokens = (text) => Math.max(1, Math.ceil(text.length / 4));
|
|
130
131
|
const DOC_CONTEXT_BUDGET = 8000;
|
|
132
|
+
const OPENAPI_HINT_OPERATIONS_LIMIT = 30;
|
|
131
133
|
const DOCDEX_HANDLE = /^docdex:/i;
|
|
132
134
|
const VALID_AREAS = new Set(["web", "adm", "bck", "ops", "infra", "mobile"]);
|
|
133
135
|
const VALID_TASK_TYPES = new Set(["feature", "bug", "chore", "spike"]);
|
|
@@ -185,6 +187,28 @@ const normalizeRelatedDocs = (value) => {
|
|
|
185
187
|
})
|
|
186
188
|
.filter((entry) => Boolean(entry && DOCDEX_HANDLE.test(entry)));
|
|
187
189
|
};
|
|
190
|
+
const isPlainObject = (value) => Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
191
|
+
const parseStructuredDoc = (raw) => {
|
|
192
|
+
if (!raw || raw.trim().length === 0)
|
|
193
|
+
return undefined;
|
|
194
|
+
try {
|
|
195
|
+
const parsed = YAML.parse(raw);
|
|
196
|
+
if (isPlainObject(parsed))
|
|
197
|
+
return parsed;
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
// fallback to JSON parse
|
|
201
|
+
}
|
|
202
|
+
try {
|
|
203
|
+
const parsed = JSON.parse(raw);
|
|
204
|
+
if (isPlainObject(parsed))
|
|
205
|
+
return parsed;
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
// ignore invalid fallback parse
|
|
209
|
+
}
|
|
210
|
+
return undefined;
|
|
211
|
+
};
|
|
188
212
|
const describeDoc = (doc, idx) => {
|
|
189
213
|
const title = doc.title ?? doc.path ?? doc.id ?? `doc-${idx + 1}`;
|
|
190
214
|
const source = doc.path ?? doc.id ?? "docdex";
|
|
@@ -404,11 +428,14 @@ const DOC_SCAN_IGNORE_DIRS = new Set([
|
|
|
404
428
|
"temp",
|
|
405
429
|
]);
|
|
406
430
|
const DOC_SCAN_FILE_PATTERN = /\.(md|markdown|txt|rst|ya?ml|json)$/i;
|
|
431
|
+
const STRICT_SDS_PATH_PATTERN = /(^|\/)(sds(?:[-_. ][a-z0-9]+)?|software[-_ ]design(?:[-_ ](?:spec|specification|outline|doc))?|design[-_ ]spec(?:ification)?)(\/|[-_.]|$)/i;
|
|
432
|
+
const STRICT_SDS_CONTENT_PATTERN = /\b(software design specification|software design document|system design specification|\bSDS\b)\b/i;
|
|
407
433
|
const SDS_LIKE_PATH_PATTERN = /(^|\/)(sds|software[-_ ]design|design[-_ ]spec|requirements|prd|pdr|rfp|architecture|solution[-_ ]design)/i;
|
|
408
434
|
const OPENAPI_LIKE_PATH_PATTERN = /(openapi|swagger)/i;
|
|
409
435
|
const STRUCTURE_LIKE_PATH_PATTERN = /(^|\/)(tree|structure|layout|folder|directory|services?|modules?)(\/|[-_.]|$)/i;
|
|
410
436
|
const DOC_PATH_TOKEN_PATTERN = /(^|[\s`"'([{<])([A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)+)(?=$|[\s`"')\]}>.,;:!?])/g;
|
|
411
437
|
const FILE_EXTENSION_PATTERN = /\.[a-z0-9]{1,10}$/i;
|
|
438
|
+
const TOP_LEVEL_STRUCTURE_PATTERN = /^[a-z][a-z0-9._-]{1,60}$/i;
|
|
412
439
|
const SERVICE_PATH_CONTAINER_SEGMENTS = new Set([
|
|
413
440
|
"services",
|
|
414
441
|
"service",
|
|
@@ -481,6 +508,8 @@ const SERVICE_NAME_INVALID = new Set([
|
|
|
481
508
|
]);
|
|
482
509
|
const SERVICE_LABEL_PATTERN = /\b([A-Za-z][A-Za-z0-9]*(?:[ _/-]+[A-Za-z][A-Za-z0-9]*){0,3})\s+(service|api|backend|frontend|worker|gateway|database|db|ui|client|server|adapter)\b/gi;
|
|
483
510
|
const SERVICE_ARROW_PATTERN = /([A-Za-z][A-Za-z0-9 _/-]{1,80})\s*(?:->|=>|→)\s*([A-Za-z][A-Za-z0-9 _/-]{1,80})/g;
|
|
511
|
+
const SERVICE_HANDLE_PATTERN = /\b((?:svc|ui|worker)-[a-z0-9-*]+)\b/gi;
|
|
512
|
+
const WAVE_LABEL_PATTERN = /\bwave\s*([0-9]{1,2})\b/i;
|
|
484
513
|
const nextUniqueLocalId = (prefix, existing) => {
|
|
485
514
|
let index = 1;
|
|
486
515
|
let candidate = `${prefix}-${index}`;
|
|
@@ -491,6 +520,18 @@ const nextUniqueLocalId = (prefix, existing) => {
|
|
|
491
520
|
existing.add(candidate);
|
|
492
521
|
return candidate;
|
|
493
522
|
};
|
|
523
|
+
const looksLikeSdsPath = (value) => STRICT_SDS_PATH_PATTERN.test(value.replace(/\\/g, "/").toLowerCase());
|
|
524
|
+
const looksLikeSdsDoc = (doc) => {
|
|
525
|
+
if ((doc.docType ?? "").toUpperCase() === "SDS")
|
|
526
|
+
return true;
|
|
527
|
+
const pathOrTitle = `${doc.path ?? ""}\n${doc.title ?? ""}`;
|
|
528
|
+
if (looksLikeSdsPath(pathOrTitle))
|
|
529
|
+
return true;
|
|
530
|
+
const sample = [doc.content ?? "", ...(doc.segments ?? []).slice(0, 4).map((seg) => seg.content ?? "")]
|
|
531
|
+
.join("\n")
|
|
532
|
+
.slice(0, 5000);
|
|
533
|
+
return STRICT_SDS_CONTENT_PATTERN.test(sample);
|
|
534
|
+
};
|
|
494
535
|
const EPIC_SCHEMA_SNIPPET = `{
|
|
495
536
|
"epics": [
|
|
496
537
|
{
|
|
@@ -610,6 +651,7 @@ export class CreateTasksService {
|
|
|
610
651
|
try {
|
|
611
652
|
await ordering.orderTasks({
|
|
612
653
|
projectKey,
|
|
654
|
+
apply: true,
|
|
613
655
|
});
|
|
614
656
|
}
|
|
615
657
|
finally {
|
|
@@ -636,7 +678,68 @@ export class CreateTasksService {
|
|
|
636
678
|
return this.ratingService;
|
|
637
679
|
}
|
|
638
680
|
async prepareDocs(inputs) {
|
|
639
|
-
const
|
|
681
|
+
const primaryInputs = inputs.length > 0 ? inputs : await this.resolveDefaultDocInputs();
|
|
682
|
+
let documents = await this.collectDocsFromInputs(primaryInputs);
|
|
683
|
+
if (!documents.some((doc) => looksLikeSdsDoc(doc))) {
|
|
684
|
+
const fallbackInputs = await this.resolveDefaultDocInputs();
|
|
685
|
+
if (fallbackInputs.length > 0) {
|
|
686
|
+
const alreadyUsed = new Set(primaryInputs.map((input) => this.normalizeDocInputForSet(input)));
|
|
687
|
+
const missingInputs = fallbackInputs.filter((candidate) => !alreadyUsed.has(this.normalizeDocInputForSet(candidate)));
|
|
688
|
+
if (missingInputs.length > 0) {
|
|
689
|
+
const discovered = await this.collectDocsFromInputs(missingInputs);
|
|
690
|
+
documents = this.mergeDocs(documents, discovered);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
if (!documents.some((doc) => looksLikeSdsDoc(doc))) {
|
|
695
|
+
throw new Error("create-tasks requires at least one SDS document. Add an SDS file (for example docs/sds.md) or pass SDS paths as input.");
|
|
696
|
+
}
|
|
697
|
+
return this.sortDocsForPlanning(documents);
|
|
698
|
+
}
|
|
699
|
+
normalizeDocInputForSet(input) {
|
|
700
|
+
if (input.startsWith("docdex:"))
|
|
701
|
+
return input.trim().toLowerCase();
|
|
702
|
+
const resolved = path.isAbsolute(input) ? input : path.join(this.workspace.workspaceRoot, input);
|
|
703
|
+
return path.resolve(resolved).toLowerCase();
|
|
704
|
+
}
|
|
705
|
+
docIdentity(doc) {
|
|
706
|
+
const pathKey = `${doc.path ?? ""}`.trim().toLowerCase();
|
|
707
|
+
const idKey = `${doc.id ?? ""}`.trim().toLowerCase();
|
|
708
|
+
if (pathKey)
|
|
709
|
+
return `path:${pathKey}`;
|
|
710
|
+
if (idKey)
|
|
711
|
+
return `id:${idKey}`;
|
|
712
|
+
const titleKey = `${doc.title ?? ""}`.trim().toLowerCase();
|
|
713
|
+
if (titleKey)
|
|
714
|
+
return `title:${titleKey}`;
|
|
715
|
+
const sample = `${doc.content ?? doc.segments?.[0]?.content ?? ""}`.slice(0, 120).toLowerCase();
|
|
716
|
+
return `sample:${sample}`;
|
|
717
|
+
}
|
|
718
|
+
mergeDocs(base, incoming) {
|
|
719
|
+
const merged = [...base];
|
|
720
|
+
const seen = new Set(merged.map((doc) => this.docIdentity(doc)));
|
|
721
|
+
for (const doc of incoming) {
|
|
722
|
+
const identity = this.docIdentity(doc);
|
|
723
|
+
if (seen.has(identity))
|
|
724
|
+
continue;
|
|
725
|
+
seen.add(identity);
|
|
726
|
+
merged.push(doc);
|
|
727
|
+
}
|
|
728
|
+
return merged;
|
|
729
|
+
}
|
|
730
|
+
sortDocsForPlanning(docs) {
|
|
731
|
+
return [...docs].sort((a, b) => {
|
|
732
|
+
const aIsSds = looksLikeSdsDoc(a) ? 0 : 1;
|
|
733
|
+
const bIsSds = looksLikeSdsDoc(b) ? 0 : 1;
|
|
734
|
+
if (aIsSds !== bIsSds)
|
|
735
|
+
return aIsSds - bIsSds;
|
|
736
|
+
const byUpdated = `${b.updatedAt ?? ""}`.localeCompare(`${a.updatedAt ?? ""}`);
|
|
737
|
+
if (byUpdated !== 0)
|
|
738
|
+
return byUpdated;
|
|
739
|
+
return `${a.path ?? a.title ?? ""}`.localeCompare(`${b.path ?? b.title ?? ""}`);
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
async collectDocsFromInputs(resolvedInputs) {
|
|
640
743
|
if (resolvedInputs.length === 0)
|
|
641
744
|
return [];
|
|
642
745
|
const documents = [];
|
|
@@ -690,19 +793,41 @@ export class CreateTasksService {
|
|
|
690
793
|
path.join(this.workspace.workspaceRoot, "openapi.json"),
|
|
691
794
|
];
|
|
692
795
|
const existing = [];
|
|
796
|
+
const existingSet = new Set();
|
|
797
|
+
const existingDirectories = [];
|
|
693
798
|
for (const candidate of candidates) {
|
|
694
799
|
try {
|
|
695
800
|
const stat = await fs.stat(candidate);
|
|
696
|
-
|
|
697
|
-
|
|
801
|
+
const resolved = path.resolve(candidate);
|
|
802
|
+
if (!stat.isFile() && !stat.isDirectory())
|
|
803
|
+
continue;
|
|
804
|
+
if (existingSet.has(resolved))
|
|
805
|
+
continue;
|
|
806
|
+
existing.push(resolved);
|
|
807
|
+
existingSet.add(resolved);
|
|
808
|
+
if (stat.isDirectory())
|
|
809
|
+
existingDirectories.push(resolved);
|
|
698
810
|
}
|
|
699
811
|
catch {
|
|
700
812
|
// Ignore missing candidates; fall back to empty inputs.
|
|
701
813
|
}
|
|
702
814
|
}
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
815
|
+
const fuzzy = await this.findFuzzyDocInputs();
|
|
816
|
+
if (existing.length === 0)
|
|
817
|
+
return fuzzy;
|
|
818
|
+
const isCoveredByDefaultInputs = (candidate) => {
|
|
819
|
+
const resolved = path.resolve(candidate);
|
|
820
|
+
if (existingSet.has(resolved))
|
|
821
|
+
return true;
|
|
822
|
+
for (const directory of existingDirectories) {
|
|
823
|
+
const relative = path.relative(directory, resolved);
|
|
824
|
+
if (!relative || (!relative.startsWith("..") && !path.isAbsolute(relative)))
|
|
825
|
+
return true;
|
|
826
|
+
}
|
|
827
|
+
return false;
|
|
828
|
+
};
|
|
829
|
+
const extras = fuzzy.filter((candidate) => !isCoveredByDefaultInputs(candidate));
|
|
830
|
+
return [...existing, ...extras];
|
|
706
831
|
}
|
|
707
832
|
async walkDocCandidates(currentDir, depth, collector) {
|
|
708
833
|
if (depth > DOC_SCAN_MAX_DEPTH)
|
|
@@ -797,11 +922,14 @@ export class CreateTasksService {
|
|
|
797
922
|
return undefined;
|
|
798
923
|
if (/[\u0000-\u001f]/.test(normalized))
|
|
799
924
|
return undefined;
|
|
925
|
+
const hadTrailingSlash = /\/$/.test(normalized);
|
|
800
926
|
const parts = normalized.split("/").filter(Boolean);
|
|
801
|
-
if (parts.length < 2)
|
|
927
|
+
if (parts.length < 2 && !(hadTrailingSlash && parts.length === 1))
|
|
802
928
|
return undefined;
|
|
803
929
|
if (parts.some((part) => part === "." || part === ".."))
|
|
804
930
|
return undefined;
|
|
931
|
+
if (parts.length === 1 && !TOP_LEVEL_STRUCTURE_PATTERN.test(parts[0]))
|
|
932
|
+
return undefined;
|
|
805
933
|
if (DOC_SCAN_IGNORE_DIRS.has(parts[0].toLowerCase()))
|
|
806
934
|
return undefined;
|
|
807
935
|
return parts.join("/");
|
|
@@ -981,7 +1109,97 @@ export class CreateTasksService {
|
|
|
981
1109
|
}
|
|
982
1110
|
return statements;
|
|
983
1111
|
}
|
|
984
|
-
|
|
1112
|
+
extractStartupWaveHints(text, aliases) {
|
|
1113
|
+
const waveRank = new Map();
|
|
1114
|
+
const startupWavesMap = new Map();
|
|
1115
|
+
const foundational = new Set();
|
|
1116
|
+
const registerWave = (service, wave) => {
|
|
1117
|
+
const normalizedWave = Number.isFinite(wave) ? Math.max(0, wave) : Number.MAX_SAFE_INTEGER;
|
|
1118
|
+
const current = waveRank.get(service);
|
|
1119
|
+
if (current === undefined || normalizedWave < current) {
|
|
1120
|
+
waveRank.set(service, normalizedWave);
|
|
1121
|
+
}
|
|
1122
|
+
const bucket = startupWavesMap.get(normalizedWave) ?? new Set();
|
|
1123
|
+
bucket.add(service);
|
|
1124
|
+
startupWavesMap.set(normalizedWave, bucket);
|
|
1125
|
+
};
|
|
1126
|
+
const resolveServicesFromCell = (cell) => {
|
|
1127
|
+
const resolved = new Set();
|
|
1128
|
+
for (const match of cell.matchAll(SERVICE_HANDLE_PATTERN)) {
|
|
1129
|
+
const token = match[1]?.trim();
|
|
1130
|
+
if (!token)
|
|
1131
|
+
continue;
|
|
1132
|
+
if (token.includes("*")) {
|
|
1133
|
+
const normalizedPrefix = this.normalizeServiceName(token.replace(/\*+/g, ""));
|
|
1134
|
+
if (!normalizedPrefix)
|
|
1135
|
+
continue;
|
|
1136
|
+
for (const service of aliases.keys()) {
|
|
1137
|
+
if (service.startsWith(normalizedPrefix))
|
|
1138
|
+
resolved.add(service);
|
|
1139
|
+
}
|
|
1140
|
+
continue;
|
|
1141
|
+
}
|
|
1142
|
+
const canonical = this.resolveServiceMentionFromPhrase(token, aliases) ?? this.addServiceAlias(aliases, token);
|
|
1143
|
+
if (canonical)
|
|
1144
|
+
resolved.add(canonical);
|
|
1145
|
+
}
|
|
1146
|
+
if (resolved.size === 0) {
|
|
1147
|
+
for (const mention of this.extractServiceMentionsFromText(cell)) {
|
|
1148
|
+
const canonical = this.addServiceAlias(aliases, mention);
|
|
1149
|
+
if (canonical)
|
|
1150
|
+
resolved.add(canonical);
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
return Array.from(resolved);
|
|
1154
|
+
};
|
|
1155
|
+
const lines = text
|
|
1156
|
+
.split(/\r?\n/)
|
|
1157
|
+
.map((line) => line.trim())
|
|
1158
|
+
.filter(Boolean)
|
|
1159
|
+
.slice(0, 2000);
|
|
1160
|
+
for (const line of lines) {
|
|
1161
|
+
if (!line.startsWith("|"))
|
|
1162
|
+
continue;
|
|
1163
|
+
const cells = line
|
|
1164
|
+
.split("|")
|
|
1165
|
+
.map((cell) => cell.trim())
|
|
1166
|
+
.filter(Boolean);
|
|
1167
|
+
if (cells.length < 2)
|
|
1168
|
+
continue;
|
|
1169
|
+
const waveFromFirst = cells[0].match(WAVE_LABEL_PATTERN);
|
|
1170
|
+
if (waveFromFirst) {
|
|
1171
|
+
const waveIndex = Number.parseInt(waveFromFirst[1] ?? "", 10);
|
|
1172
|
+
const services = resolveServicesFromCell(cells[1]);
|
|
1173
|
+
for (const service of services)
|
|
1174
|
+
registerWave(service, waveIndex);
|
|
1175
|
+
if (waveIndex === 0 && services.length === 0) {
|
|
1176
|
+
for (const token of cells[1]
|
|
1177
|
+
.replace(/[`_*]/g, "")
|
|
1178
|
+
.split(/[,+]/)
|
|
1179
|
+
.map((entry) => entry.trim())
|
|
1180
|
+
.filter(Boolean)) {
|
|
1181
|
+
foundational.add(token);
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
continue;
|
|
1185
|
+
}
|
|
1186
|
+
const waveFromSecond = cells[1].match(WAVE_LABEL_PATTERN);
|
|
1187
|
+
if (!waveFromSecond)
|
|
1188
|
+
continue;
|
|
1189
|
+
const waveIndex = Number.parseInt(waveFromSecond[1] ?? "", 10);
|
|
1190
|
+
for (const service of resolveServicesFromCell(cells[0]))
|
|
1191
|
+
registerWave(service, waveIndex);
|
|
1192
|
+
}
|
|
1193
|
+
const startupWaves = Array.from(startupWavesMap.entries())
|
|
1194
|
+
.sort((a, b) => a[0] - b[0])
|
|
1195
|
+
.map(([wave, services]) => ({ wave, services: Array.from(services).sort((a, b) => a.localeCompare(b)) }));
|
|
1196
|
+
return {
|
|
1197
|
+
waveRank,
|
|
1198
|
+
startupWaves,
|
|
1199
|
+
foundationalDependencies: Array.from(foundational).slice(0, 12),
|
|
1200
|
+
};
|
|
1201
|
+
}
|
|
1202
|
+
sortServicesByDependency(services, dependencies, waveRank = new Map()) {
|
|
985
1203
|
const nodes = Array.from(new Set(services));
|
|
986
1204
|
const indegree = new Map();
|
|
987
1205
|
const adjacency = new Map();
|
|
@@ -1010,6 +1228,10 @@ export class CreateTasksService {
|
|
|
1010
1228
|
}
|
|
1011
1229
|
}
|
|
1012
1230
|
const compare = (a, b) => {
|
|
1231
|
+
const waveA = waveRank.get(a) ?? Number.MAX_SAFE_INTEGER;
|
|
1232
|
+
const waveB = waveRank.get(b) ?? Number.MAX_SAFE_INTEGER;
|
|
1233
|
+
if (waveA !== waveB)
|
|
1234
|
+
return waveA - waveB;
|
|
1013
1235
|
const dependedByA = dependedBy.get(a) ?? 0;
|
|
1014
1236
|
const dependedByB = dependedBy.get(b) ?? 0;
|
|
1015
1237
|
if (dependedByA !== dependedByB)
|
|
@@ -1060,6 +1282,10 @@ export class CreateTasksService {
|
|
|
1060
1282
|
for (const token of [...structureTargets.directories, ...structureTargets.files]) {
|
|
1061
1283
|
register(this.deriveServiceFromPathToken(token));
|
|
1062
1284
|
}
|
|
1285
|
+
for (const match of docsText.matchAll(SERVICE_HANDLE_PATTERN))
|
|
1286
|
+
register(match[1]);
|
|
1287
|
+
for (const match of planText.matchAll(SERVICE_HANDLE_PATTERN))
|
|
1288
|
+
register(match[1]);
|
|
1063
1289
|
for (const mention of this.extractServiceMentionsFromText(docsText))
|
|
1064
1290
|
register(mention);
|
|
1065
1291
|
for (const mention of this.extractServiceMentionsFromText(planText))
|
|
@@ -1077,8 +1303,55 @@ export class CreateTasksService {
|
|
|
1077
1303
|
dependencies.set(dependent, next);
|
|
1078
1304
|
}
|
|
1079
1305
|
}
|
|
1080
|
-
const
|
|
1081
|
-
|
|
1306
|
+
const waveHints = this.extractStartupWaveHints(corpus.join("\n"), aliases);
|
|
1307
|
+
const services = this.sortServicesByDependency(Array.from(aliases.keys()), dependencies, waveHints.waveRank);
|
|
1308
|
+
return {
|
|
1309
|
+
services,
|
|
1310
|
+
dependencies,
|
|
1311
|
+
aliases,
|
|
1312
|
+
waveRank: waveHints.waveRank,
|
|
1313
|
+
startupWaves: waveHints.startupWaves,
|
|
1314
|
+
foundationalDependencies: waveHints.foundationalDependencies,
|
|
1315
|
+
};
|
|
1316
|
+
}
|
|
1317
|
+
buildProjectConstructionMethod(docs, graph) {
|
|
1318
|
+
const toLabel = (value) => value.replace(/\s+/g, "-");
|
|
1319
|
+
const structureTargets = this.extractStructureTargets(docs);
|
|
1320
|
+
const topDirectories = structureTargets.directories.slice(0, 10);
|
|
1321
|
+
const topFiles = structureTargets.files.slice(0, 10);
|
|
1322
|
+
const startupWaveLines = graph.startupWaves
|
|
1323
|
+
.slice(0, 8)
|
|
1324
|
+
.map((wave) => `- Wave ${wave.wave}: ${wave.services.map(toLabel).join(", ")}`);
|
|
1325
|
+
const serviceOrderLine = graph.services.length > 0
|
|
1326
|
+
? graph.services
|
|
1327
|
+
.slice(0, 16)
|
|
1328
|
+
.map(toLabel)
|
|
1329
|
+
.join(" -> ")
|
|
1330
|
+
: "infer from SDS service dependencies and startup waves";
|
|
1331
|
+
const dependencyPairs = [];
|
|
1332
|
+
for (const [dependent, needs] of graph.dependencies.entries()) {
|
|
1333
|
+
for (const dependency of needs) {
|
|
1334
|
+
dependencyPairs.push(`${toLabel(dependent)} after ${toLabel(dependency)}`);
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
return [
|
|
1338
|
+
"Project construction method (strict):",
|
|
1339
|
+
"1) Build repository structure from SDS folder tree first.",
|
|
1340
|
+
...topDirectories.map((dir) => ` - create dir: ${dir}`),
|
|
1341
|
+
...topFiles.map((file) => ` - create file: ${file}`),
|
|
1342
|
+
"2) Build foundational dependencies and low-wave services before consumers.",
|
|
1343
|
+
...(graph.foundationalDependencies.length > 0
|
|
1344
|
+
? graph.foundationalDependencies.map((dependency) => ` - foundation: ${dependency}`)
|
|
1345
|
+
: [" - foundation: infer runtime prerequisites from SDS deployment sections"]),
|
|
1346
|
+
...(startupWaveLines.length > 0 ? startupWaveLines : [" - startup waves: infer from dependency contracts"]),
|
|
1347
|
+
"3) Implement services by dependency direction and startup wave.",
|
|
1348
|
+
` - service order: ${serviceOrderLine}`,
|
|
1349
|
+
...(dependencyPairs.length > 0
|
|
1350
|
+
? dependencyPairs.slice(0, 14).map((pair) => ` - dependency: ${pair}`)
|
|
1351
|
+
: [" - dependency: infer explicit \"depends on\" relations from SDS"]),
|
|
1352
|
+
"4) Only then sequence user-facing features, QA hardening, and release chores.",
|
|
1353
|
+
"5) Keep task dependencies story-scoped while preserving epic/story/task ordering by this build method.",
|
|
1354
|
+
].join("\n");
|
|
1082
1355
|
}
|
|
1083
1356
|
orderStoryTasksByDependencies(storyTasks, serviceRank, taskServiceByScope) {
|
|
1084
1357
|
const byLocalId = new Map(storyTasks.map((task) => [task.localId, task]));
|
|
@@ -1144,7 +1417,12 @@ export class CreateTasksService {
|
|
|
1144
1417
|
const graph = this.buildServiceDependencyGraph(plan, docs);
|
|
1145
1418
|
if (!graph.services.length)
|
|
1146
1419
|
return plan;
|
|
1147
|
-
const
|
|
1420
|
+
const serviceOrderRank = new Map(graph.services.map((service, index) => [service, index]));
|
|
1421
|
+
const serviceRank = new Map(graph.services.map((service) => {
|
|
1422
|
+
const wave = graph.waveRank.get(service) ?? Number.MAX_SAFE_INTEGER;
|
|
1423
|
+
const order = serviceOrderRank.get(service) ?? Number.MAX_SAFE_INTEGER;
|
|
1424
|
+
return [service, wave * 10000 + order];
|
|
1425
|
+
}));
|
|
1148
1426
|
const resolveEntityService = (text) => this.resolveServiceMentionFromPhrase(text, graph.aliases);
|
|
1149
1427
|
const epics = plan.epics.map((epic) => ({ ...epic }));
|
|
1150
1428
|
const stories = plan.stories.map((story) => ({ ...story }));
|
|
@@ -1613,6 +1891,64 @@ export class CreateTasksService {
|
|
|
1613
1891
|
entrypoints: entrypoints.length ? entrypoints : undefined,
|
|
1614
1892
|
};
|
|
1615
1893
|
}
|
|
1894
|
+
isOpenApiDoc(doc) {
|
|
1895
|
+
const type = (doc.docType ?? "").toLowerCase();
|
|
1896
|
+
if (type.includes("openapi") || type.includes("swagger"))
|
|
1897
|
+
return true;
|
|
1898
|
+
const pathTitle = `${doc.path ?? ""} ${doc.title ?? ""}`.toLowerCase();
|
|
1899
|
+
return OPENAPI_LIKE_PATH_PATTERN.test(pathTitle);
|
|
1900
|
+
}
|
|
1901
|
+
buildOpenApiHintSummary(docs) {
|
|
1902
|
+
const lines = [];
|
|
1903
|
+
for (const doc of docs) {
|
|
1904
|
+
if (!this.isOpenApiDoc(doc))
|
|
1905
|
+
continue;
|
|
1906
|
+
const rawContent = doc.content && doc.content.trim().length > 0
|
|
1907
|
+
? doc.content
|
|
1908
|
+
: (doc.segments ?? []).map((segment) => segment.content).join("\n\n");
|
|
1909
|
+
const parsed = parseStructuredDoc(rawContent);
|
|
1910
|
+
if (!parsed)
|
|
1911
|
+
continue;
|
|
1912
|
+
const paths = parsed.paths;
|
|
1913
|
+
if (!isPlainObject(paths))
|
|
1914
|
+
continue;
|
|
1915
|
+
for (const [apiPath, pathItem] of Object.entries(paths)) {
|
|
1916
|
+
if (!isPlainObject(pathItem))
|
|
1917
|
+
continue;
|
|
1918
|
+
for (const [method, operation] of Object.entries(pathItem)) {
|
|
1919
|
+
const normalizedMethod = method.toLowerCase();
|
|
1920
|
+
if (!["get", "post", "put", "patch", "delete", "options", "head", "trace"].includes(normalizedMethod)) {
|
|
1921
|
+
continue;
|
|
1922
|
+
}
|
|
1923
|
+
if (!isPlainObject(operation))
|
|
1924
|
+
continue;
|
|
1925
|
+
const hints = operation["x-mcoda-task-hints"];
|
|
1926
|
+
if (!isPlainObject(hints))
|
|
1927
|
+
continue;
|
|
1928
|
+
const service = typeof hints.service === "string" ? hints.service : "-";
|
|
1929
|
+
const capability = typeof hints.capability === "string" ? hints.capability : "-";
|
|
1930
|
+
const stage = typeof hints.stage === "string" ? hints.stage : "-";
|
|
1931
|
+
const complexity = typeof hints.complexity === "number" && Number.isFinite(hints.complexity)
|
|
1932
|
+
? hints.complexity.toFixed(1)
|
|
1933
|
+
: "-";
|
|
1934
|
+
const dependsOn = Array.isArray(hints.depends_on_operations)
|
|
1935
|
+
? hints.depends_on_operations.filter((entry) => typeof entry === "string").length
|
|
1936
|
+
: 0;
|
|
1937
|
+
const tests = isPlainObject(hints.test_requirements) ? hints.test_requirements : undefined;
|
|
1938
|
+
const countEntries = (value) => Array.isArray(value) ? value.filter((entry) => typeof entry === "string").length : 0;
|
|
1939
|
+
const unitCount = countEntries(tests?.unit);
|
|
1940
|
+
const componentCount = countEntries(tests?.component);
|
|
1941
|
+
const integrationCount = countEntries(tests?.integration);
|
|
1942
|
+
const apiCount = countEntries(tests?.api);
|
|
1943
|
+
lines.push(`- ${normalizedMethod.toUpperCase()} ${apiPath} :: service=${service}; capability=${capability}; stage=${stage}; complexity=${complexity}; deps=${dependsOn}; tests(u/c/i/a)=${unitCount}/${componentCount}/${integrationCount}/${apiCount}`);
|
|
1944
|
+
if (lines.length >= OPENAPI_HINT_OPERATIONS_LIMIT) {
|
|
1945
|
+
return lines.join("\n");
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
return lines.join("\n");
|
|
1951
|
+
}
|
|
1616
1952
|
buildDocContext(docs) {
|
|
1617
1953
|
const warnings = [];
|
|
1618
1954
|
const blocks = [];
|
|
@@ -1643,9 +1979,21 @@ export class CreateTasksService {
|
|
|
1643
1979
|
if (budget <= 0)
|
|
1644
1980
|
break;
|
|
1645
1981
|
}
|
|
1982
|
+
const openApiHints = this.buildOpenApiHintSummary(sorted);
|
|
1983
|
+
if (openApiHints) {
|
|
1984
|
+
const hintBlock = ["[OPENAPI_HINTS]", openApiHints].join("\n");
|
|
1985
|
+
const hintCost = estimateTokens(hintBlock);
|
|
1986
|
+
if (budget - hintCost >= 0) {
|
|
1987
|
+
budget -= hintCost;
|
|
1988
|
+
blocks.push(hintBlock);
|
|
1989
|
+
}
|
|
1990
|
+
else {
|
|
1991
|
+
warnings.push("Context truncated due to token budget; skipped OpenAPI hint summary.");
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1646
1994
|
return { docSummary: blocks.join("\n\n") || "(no docs)", warnings };
|
|
1647
1995
|
}
|
|
1648
|
-
buildPrompt(projectKey, docs, options) {
|
|
1996
|
+
buildPrompt(projectKey, docs, projectBuildMethod, options) {
|
|
1649
1997
|
const docSummary = docs.map((doc, idx) => describeDoc(doc, idx)).join("\n");
|
|
1650
1998
|
const limits = [
|
|
1651
1999
|
options.maxEpics ? `Limit epics to ${options.maxEpics}.` : "",
|
|
@@ -1666,6 +2014,8 @@ export class CreateTasksService {
|
|
|
1666
2014
|
"- acceptanceCriteria must be an array of strings (5-10 items).",
|
|
1667
2015
|
"- Prefer dependency-first sequencing: foundational codebase/service setup epics should precede dependent feature epics.",
|
|
1668
2016
|
"- Keep output technology-agnostic and derived from docs; do not assume specific stacks unless docs state them.",
|
|
2017
|
+
"Project construction method to follow:",
|
|
2018
|
+
projectBuildMethod,
|
|
1669
2019
|
limits || "Use reasonable scope without over-generating epics.",
|
|
1670
2020
|
"Docs available:",
|
|
1671
2021
|
docSummary || "- (no docs provided; propose sensible epics).",
|
|
@@ -1673,7 +2023,7 @@ export class CreateTasksService {
|
|
|
1673
2023
|
return { prompt, docSummary };
|
|
1674
2024
|
}
|
|
1675
2025
|
fallbackPlan(projectKey, docs) {
|
|
1676
|
-
const docRefs = docs.map((doc) => doc.id
|
|
2026
|
+
const docRefs = docs.map((doc) => (doc.id ? `docdex:${doc.id}` : doc.path ?? doc.title ?? "doc"));
|
|
1677
2027
|
return {
|
|
1678
2028
|
epics: [
|
|
1679
2029
|
{
|
|
@@ -1729,6 +2079,63 @@ export class CreateTasksService {
|
|
|
1729
2079
|
],
|
|
1730
2080
|
};
|
|
1731
2081
|
}
|
|
2082
|
+
materializePlanFromSeed(seed, options) {
|
|
2083
|
+
const epics = [];
|
|
2084
|
+
const stories = [];
|
|
2085
|
+
const tasks = [];
|
|
2086
|
+
const epicLimit = options.maxEpics ?? Number.MAX_SAFE_INTEGER;
|
|
2087
|
+
const storyLimit = options.maxStoriesPerEpic ?? Number.MAX_SAFE_INTEGER;
|
|
2088
|
+
const taskLimit = options.maxTasksPerStory ?? Number.MAX_SAFE_INTEGER;
|
|
2089
|
+
const seedEpics = Array.isArray(seed.epics) ? seed.epics.slice(0, epicLimit) : [];
|
|
2090
|
+
for (const [epicIndex, epic] of seedEpics.entries()) {
|
|
2091
|
+
const epicLocalId = typeof epic.localId === "string" && epic.localId.trim().length > 0 ? epic.localId : `e${epicIndex + 1}`;
|
|
2092
|
+
const planEpic = {
|
|
2093
|
+
...epic,
|
|
2094
|
+
localId: epicLocalId,
|
|
2095
|
+
area: normalizeArea(epic.area),
|
|
2096
|
+
relatedDocs: normalizeRelatedDocs(epic.relatedDocs),
|
|
2097
|
+
acceptanceCriteria: Array.isArray(epic.acceptanceCriteria) ? epic.acceptanceCriteria : [],
|
|
2098
|
+
stories: [],
|
|
2099
|
+
};
|
|
2100
|
+
epics.push(planEpic);
|
|
2101
|
+
const epicStories = Array.isArray(epic.stories) ? epic.stories.slice(0, storyLimit) : [];
|
|
2102
|
+
for (const [storyIndex, story] of epicStories.entries()) {
|
|
2103
|
+
const storyLocalId = typeof story.localId === "string" && story.localId.trim().length > 0 ? story.localId : `us${storyIndex + 1}`;
|
|
2104
|
+
const planStory = {
|
|
2105
|
+
...story,
|
|
2106
|
+
localId: storyLocalId,
|
|
2107
|
+
epicLocalId,
|
|
2108
|
+
relatedDocs: normalizeRelatedDocs(story.relatedDocs),
|
|
2109
|
+
acceptanceCriteria: Array.isArray(story.acceptanceCriteria) ? story.acceptanceCriteria : [],
|
|
2110
|
+
tasks: [],
|
|
2111
|
+
};
|
|
2112
|
+
stories.push(planStory);
|
|
2113
|
+
const storyTasks = Array.isArray(story.tasks) ? story.tasks.slice(0, taskLimit) : [];
|
|
2114
|
+
for (const [taskIndex, task] of storyTasks.entries()) {
|
|
2115
|
+
const localId = typeof task.localId === "string" && task.localId.trim().length > 0 ? task.localId : `t${taskIndex + 1}`;
|
|
2116
|
+
tasks.push({
|
|
2117
|
+
...task,
|
|
2118
|
+
localId,
|
|
2119
|
+
storyLocalId,
|
|
2120
|
+
epicLocalId,
|
|
2121
|
+
title: task.title ?? "Task",
|
|
2122
|
+
type: normalizeTaskType(task.type) ?? "feature",
|
|
2123
|
+
description: task.description ?? "",
|
|
2124
|
+
estimatedStoryPoints: typeof task.estimatedStoryPoints === "number" ? task.estimatedStoryPoints : undefined,
|
|
2125
|
+
priorityHint: typeof task.priorityHint === "number" ? task.priorityHint : undefined,
|
|
2126
|
+
dependsOnKeys: normalizeStringArray(task.dependsOnKeys),
|
|
2127
|
+
relatedDocs: normalizeRelatedDocs(task.relatedDocs),
|
|
2128
|
+
unitTests: normalizeStringArray(task.unitTests),
|
|
2129
|
+
componentTests: normalizeStringArray(task.componentTests),
|
|
2130
|
+
integrationTests: normalizeStringArray(task.integrationTests),
|
|
2131
|
+
apiTests: normalizeStringArray(task.apiTests),
|
|
2132
|
+
qa: normalizeQaReadiness(task.qa),
|
|
2133
|
+
});
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
return { epics, stories, tasks };
|
|
2138
|
+
}
|
|
1732
2139
|
async invokeAgentWithRetry(agent, prompt, action, stream, jobId, commandRunId, metadata) {
|
|
1733
2140
|
const startedAt = Date.now();
|
|
1734
2141
|
let output = "";
|
|
@@ -1846,7 +2253,7 @@ export class CreateTasksService {
|
|
|
1846
2253
|
}))
|
|
1847
2254
|
.filter((e) => e.title);
|
|
1848
2255
|
}
|
|
1849
|
-
async generateStoriesForEpic(agent, epic, docSummary, stream, jobId, commandRunId) {
|
|
2256
|
+
async generateStoriesForEpic(agent, epic, docSummary, projectBuildMethod, stream, jobId, commandRunId) {
|
|
1850
2257
|
const prompt = [
|
|
1851
2258
|
`Generate user stories for epic "${epic.title}".`,
|
|
1852
2259
|
"Use the User Story template: User Story; Context; Preconditions; Main Flow; Alternative/Error Flows; UX/UI; Data & Integrations; Acceptance Criteria; NFR; Related Docs.",
|
|
@@ -1856,8 +2263,11 @@ export class CreateTasksService {
|
|
|
1856
2263
|
"- No tasks in this step.",
|
|
1857
2264
|
"- acceptanceCriteria must be an array of strings.",
|
|
1858
2265
|
"- Use docdex handles when citing docs.",
|
|
2266
|
+
"- Keep story sequencing aligned with the project construction method.",
|
|
1859
2267
|
`Epic context (key=${epic.key ?? epic.localId ?? "TBD"}):`,
|
|
1860
2268
|
epic.description ?? "(no description provided)",
|
|
2269
|
+
"Project construction method:",
|
|
2270
|
+
projectBuildMethod,
|
|
1861
2271
|
`Docs: ${docSummary || "none"}`,
|
|
1862
2272
|
].join("\n\n");
|
|
1863
2273
|
const { output } = await this.invokeAgentWithRetry(agent, prompt, "stories", stream, jobId, commandRunId, {
|
|
@@ -1880,7 +2290,7 @@ export class CreateTasksService {
|
|
|
1880
2290
|
}))
|
|
1881
2291
|
.filter((s) => s.title);
|
|
1882
2292
|
}
|
|
1883
|
-
async generateTasksForStory(agent, epic, story, docSummary, stream, jobId, commandRunId) {
|
|
2293
|
+
async generateTasksForStory(agent, epic, story, docSummary, projectBuildMethod, stream, jobId, commandRunId) {
|
|
1884
2294
|
const parseTestList = (value) => {
|
|
1885
2295
|
if (!Array.isArray(value))
|
|
1886
2296
|
return [];
|
|
@@ -1898,17 +2308,21 @@ export class CreateTasksService {
|
|
|
1898
2308
|
"- Each task must include localId, title, description, type, estimatedStoryPoints, priorityHint.",
|
|
1899
2309
|
"- Include test arrays: unitTests, componentTests, integrationTests, apiTests. Use [] when not applicable.",
|
|
1900
2310
|
"- Only include tests that are relevant to the task's scope.",
|
|
1901
|
-
"-
|
|
2311
|
+
"- Prefer including task-relevant tests when they are concrete and actionable; do not invent generic placeholders.",
|
|
1902
2312
|
"- When known, include qa object with profiles_expected/requires/entrypoints/data_setup to guide QA.",
|
|
1903
2313
|
"- Do not hardcode ports. For QA entrypoints, use http://localhost:<PORT> placeholders or omit base_url when unknown.",
|
|
1904
2314
|
"- dependsOnKeys must reference localIds in this story.",
|
|
1905
2315
|
"- Start from prerequisite codebase setup: add structure/bootstrap tasks before feature tasks when missing.",
|
|
1906
2316
|
"- Keep dependencies strictly inside this story; never reference tasks from other stories/epics.",
|
|
1907
|
-
"- Order tasks from foundational prerequisites to dependents
|
|
2317
|
+
"- Order tasks from foundational prerequisites to dependents based on documented dependency direction and startup constraints.",
|
|
1908
2318
|
"- Use docdex handles when citing docs.",
|
|
2319
|
+
"- If OPENAPI_HINTS are present in Docs, align tasks with hinted service/capability/stage/test_requirements.",
|
|
2320
|
+
"- Follow the project construction method and startup-wave order from SDS when available.",
|
|
1909
2321
|
`Story context (key=${story.key ?? story.localId ?? "TBD"}):`,
|
|
1910
2322
|
story.description ?? story.userStory ?? "",
|
|
1911
2323
|
`Acceptance criteria: ${(story.acceptanceCriteria ?? []).join("; ")}`,
|
|
2324
|
+
"Project construction method:",
|
|
2325
|
+
projectBuildMethod,
|
|
1912
2326
|
`Docs: ${docSummary || "none"}`,
|
|
1913
2327
|
].join("\n\n");
|
|
1914
2328
|
const { output } = await this.invokeAgentWithRetry(agent, prompt, "tasks", stream, jobId, commandRunId, {
|
|
@@ -1925,13 +2339,8 @@ export class CreateTasksService {
|
|
|
1925
2339
|
const componentTests = parseTestList(task.componentTests);
|
|
1926
2340
|
const integrationTests = parseTestList(task.integrationTests);
|
|
1927
2341
|
const apiTests = parseTestList(task.apiTests);
|
|
1928
|
-
const hasTests = unitTests.length || componentTests.length || integrationTests.length || apiTests.length;
|
|
1929
2342
|
const title = task.title ?? "Task";
|
|
1930
2343
|
const description = task.description ?? "";
|
|
1931
|
-
const docOnly = /doc|documentation|readme|pdr|sds|openapi|spec/.test(`${title} ${description}`.toLowerCase());
|
|
1932
|
-
if (!hasTests && !docOnly) {
|
|
1933
|
-
unitTests.push(`Add tests for ${title} (unit/component/integration/api as applicable)`);
|
|
1934
|
-
}
|
|
1935
2344
|
const qa = normalizeQaReadiness(task.qa);
|
|
1936
2345
|
return {
|
|
1937
2346
|
localId: task.localId ?? `t${idx + 1}`,
|
|
@@ -1959,7 +2368,7 @@ export class CreateTasksService {
|
|
|
1959
2368
|
const planStories = [];
|
|
1960
2369
|
const planTasks = [];
|
|
1961
2370
|
for (const epic of planEpics) {
|
|
1962
|
-
const stories = await this.generateStoriesForEpic(agent, { ...epic }, docSummary, options.agentStream, options.jobId, options.commandRunId);
|
|
2371
|
+
const stories = await this.generateStoriesForEpic(agent, { ...epic }, docSummary, options.projectBuildMethod, options.agentStream, options.jobId, options.commandRunId);
|
|
1963
2372
|
const limitedStories = stories.slice(0, options.maxStoriesPerEpic ?? stories.length);
|
|
1964
2373
|
limitedStories.forEach((story, idx) => {
|
|
1965
2374
|
planStories.push({
|
|
@@ -1970,7 +2379,7 @@ export class CreateTasksService {
|
|
|
1970
2379
|
});
|
|
1971
2380
|
}
|
|
1972
2381
|
for (const story of planStories) {
|
|
1973
|
-
const tasks = await this.generateTasksForStory(agent, { key: story.epicLocalId, title: story.title }, story, docSummary, options.agentStream, options.jobId, options.commandRunId);
|
|
2382
|
+
const tasks = await this.generateTasksForStory(agent, { key: story.epicLocalId, title: story.title }, story, docSummary, options.projectBuildMethod, options.agentStream, options.jobId, options.commandRunId);
|
|
1974
2383
|
const limitedTasks = tasks.slice(0, options.maxTasksPerStory ?? tasks.length);
|
|
1975
2384
|
limitedTasks.forEach((task, idx) => {
|
|
1976
2385
|
planTasks.push({
|
|
@@ -2254,49 +2663,74 @@ export class CreateTasksService {
|
|
|
2254
2663
|
});
|
|
2255
2664
|
const docs = await this.prepareDocs(options.inputs);
|
|
2256
2665
|
const { docSummary, warnings: docWarnings } = this.buildDocContext(docs);
|
|
2257
|
-
const
|
|
2666
|
+
const discoveryGraph = this.buildServiceDependencyGraph({ epics: [], stories: [], tasks: [] }, docs);
|
|
2667
|
+
const projectBuildMethod = this.buildProjectConstructionMethod(docs, discoveryGraph);
|
|
2668
|
+
const { prompt } = this.buildPrompt(options.projectKey, docs, projectBuildMethod, options);
|
|
2258
2669
|
const qaPreflight = await this.buildQaPreflight();
|
|
2259
2670
|
const qaOverrides = this.buildQaOverrides(options);
|
|
2260
2671
|
await this.jobService.writeCheckpoint(job.id, {
|
|
2261
2672
|
stage: "docs_indexed",
|
|
2262
2673
|
timestamp: new Date().toISOString(),
|
|
2263
|
-
details: { count: docs.length, warnings: docWarnings },
|
|
2674
|
+
details: { count: docs.length, warnings: docWarnings, startupWaves: discoveryGraph.startupWaves.slice(0, 8) },
|
|
2264
2675
|
});
|
|
2265
2676
|
await this.jobService.writeCheckpoint(job.id, {
|
|
2266
2677
|
stage: "qa_preflight",
|
|
2267
2678
|
timestamp: new Date().toISOString(),
|
|
2268
2679
|
details: qaPreflight,
|
|
2269
2680
|
});
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2681
|
+
let agent;
|
|
2682
|
+
let planSource = "agent";
|
|
2683
|
+
let fallbackReason;
|
|
2684
|
+
let plan;
|
|
2685
|
+
try {
|
|
2686
|
+
agent = await this.resolveAgent(options.agentName);
|
|
2687
|
+
const { output: epicOutput } = await this.invokeAgentWithRetry(agent, prompt, "epics", agentStream, job.id, commandRun.id, { docWarnings });
|
|
2688
|
+
const epics = this.parseEpics(epicOutput, docs, options.projectKey).slice(0, options.maxEpics ?? Number.MAX_SAFE_INTEGER);
|
|
2689
|
+
await this.jobService.writeCheckpoint(job.id, {
|
|
2690
|
+
stage: "epics_generated",
|
|
2691
|
+
timestamp: new Date().toISOString(),
|
|
2692
|
+
details: { epics: epics.length, source: "agent" },
|
|
2693
|
+
});
|
|
2694
|
+
plan = await this.generatePlanFromAgent(epics, agent, docSummary, {
|
|
2695
|
+
agentStream,
|
|
2696
|
+
jobId: job.id,
|
|
2697
|
+
commandRunId: commandRun.id,
|
|
2698
|
+
maxStoriesPerEpic: options.maxStoriesPerEpic,
|
|
2699
|
+
maxTasksPerStory: options.maxTasksPerStory,
|
|
2700
|
+
projectBuildMethod,
|
|
2701
|
+
});
|
|
2702
|
+
}
|
|
2703
|
+
catch (error) {
|
|
2704
|
+
fallbackReason = error.message ?? String(error);
|
|
2705
|
+
planSource = "fallback";
|
|
2706
|
+
await this.jobService.appendLog(job.id, `Agent planning failed, using deterministic fallback plan: ${fallbackReason}\n`);
|
|
2707
|
+
plan = this.materializePlanFromSeed(this.fallbackPlan(options.projectKey, docs), {
|
|
2708
|
+
maxEpics: options.maxEpics,
|
|
2709
|
+
maxStoriesPerEpic: options.maxStoriesPerEpic,
|
|
2710
|
+
maxTasksPerStory: options.maxTasksPerStory,
|
|
2711
|
+
});
|
|
2712
|
+
await this.jobService.writeCheckpoint(job.id, {
|
|
2713
|
+
stage: "epics_generated",
|
|
2714
|
+
timestamp: new Date().toISOString(),
|
|
2715
|
+
details: { epics: plan.epics.length, source: planSource, reason: fallbackReason },
|
|
2716
|
+
});
|
|
2717
|
+
}
|
|
2285
2718
|
plan = this.enforceStoryScopedDependencies(plan);
|
|
2286
2719
|
plan = this.injectStructureBootstrapPlan(plan, docs, options.projectKey);
|
|
2287
2720
|
plan = this.enforceStoryScopedDependencies(plan);
|
|
2721
|
+
this.validatePlanLocalIdentifiers(plan);
|
|
2288
2722
|
plan = this.applyServiceDependencySequencing(plan, docs);
|
|
2289
2723
|
plan = this.enforceStoryScopedDependencies(plan);
|
|
2290
2724
|
this.validatePlanLocalIdentifiers(plan);
|
|
2291
2725
|
await this.jobService.writeCheckpoint(job.id, {
|
|
2292
2726
|
stage: "stories_generated",
|
|
2293
2727
|
timestamp: new Date().toISOString(),
|
|
2294
|
-
details: { stories: plan.stories.length },
|
|
2728
|
+
details: { stories: plan.stories.length, source: planSource, fallbackReason },
|
|
2295
2729
|
});
|
|
2296
2730
|
await this.jobService.writeCheckpoint(job.id, {
|
|
2297
2731
|
stage: "tasks_generated",
|
|
2298
2732
|
timestamp: new Date().toISOString(),
|
|
2299
|
-
details: { tasks: plan.tasks.length },
|
|
2733
|
+
details: { tasks: plan.tasks.length, source: planSource, fallbackReason },
|
|
2300
2734
|
});
|
|
2301
2735
|
const { folder } = await this.writePlanArtifacts(options.projectKey, plan, docSummary);
|
|
2302
2736
|
await this.jobService.writeCheckpoint(job.id, {
|
|
@@ -2319,10 +2753,12 @@ export class CreateTasksService {
|
|
|
2319
2753
|
dependenciesCreated: dependencyRows.length,
|
|
2320
2754
|
docs: docSummary,
|
|
2321
2755
|
planFolder: folder,
|
|
2756
|
+
planSource,
|
|
2757
|
+
fallbackReason,
|
|
2322
2758
|
},
|
|
2323
2759
|
});
|
|
2324
2760
|
await this.jobService.finishCommandRun(commandRun.id, "succeeded");
|
|
2325
|
-
if (options.rateAgents) {
|
|
2761
|
+
if (options.rateAgents && planSource === "agent" && agent) {
|
|
2326
2762
|
try {
|
|
2327
2763
|
const ratingService = this.ensureRatingService();
|
|
2328
2764
|
await ratingService.rate({
|