@mcoda/core 0.1.26 → 0.1.28
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/AgentsApi.d.ts +9 -1
- package/dist/api/AgentsApi.d.ts.map +1 -1
- package/dist/api/AgentsApi.js +53 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/services/execution/QaTasksService.d.ts +2 -0
- package/dist/services/execution/QaTasksService.d.ts.map +1 -1
- package/dist/services/execution/QaTasksService.js +181 -20
- package/dist/services/execution/WorkOnTasksService.d.ts.map +1 -1
- package/dist/services/execution/WorkOnTasksService.js +183 -7
- package/dist/services/planning/CreateTasksService.d.ts +9 -0
- package/dist/services/planning/CreateTasksService.d.ts.map +1 -1
- package/dist/services/planning/CreateTasksService.js +424 -92
- package/dist/services/planning/SdsPreflightService.d.ts +98 -0
- package/dist/services/planning/SdsPreflightService.d.ts.map +1 -0
- package/dist/services/planning/SdsPreflightService.js +1093 -0
- package/dist/services/planning/TaskSufficiencyService.d.ts +1 -0
- package/dist/services/planning/TaskSufficiencyService.d.ts.map +1 -1
- package/dist/services/planning/TaskSufficiencyService.js +376 -77
- package/dist/services/review/CodeReviewService.d.ts.map +1 -1
- package/dist/services/review/CodeReviewService.js +149 -7
- package/package.json +6 -6
|
@@ -13,6 +13,7 @@ import { TaskOrderingService } from "../backlog/TaskOrderingService.js";
|
|
|
13
13
|
import { QaTestCommandBuilder } from "../execution/QaTestCommandBuilder.js";
|
|
14
14
|
import { createEpicKeyGenerator, createStoryKeyGenerator, createTaskKeyGenerator, } from "./KeyHelpers.js";
|
|
15
15
|
import { TaskSufficiencyService } from "./TaskSufficiencyService.js";
|
|
16
|
+
import { SdsPreflightService } from "./SdsPreflightService.js";
|
|
16
17
|
const formatBullets = (items, fallback) => {
|
|
17
18
|
if (!items || items.length === 0)
|
|
18
19
|
return `- ${fallback}`;
|
|
@@ -257,6 +258,14 @@ const extractMarkdownHeadings = (value, limit) => {
|
|
|
257
258
|
!line.startsWith("*")) {
|
|
258
259
|
headings.push(line);
|
|
259
260
|
}
|
|
261
|
+
else {
|
|
262
|
+
const numberedHeading = line.match(/^(\d+(?:\.\d+)+)\s+(.+)$/);
|
|
263
|
+
if (numberedHeading) {
|
|
264
|
+
const headingText = `${numberedHeading[1]} ${numberedHeading[2]}`.trim();
|
|
265
|
+
if (/[a-z]/i.test(headingText))
|
|
266
|
+
headings.push(headingText);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
260
269
|
if (headings.length >= limit)
|
|
261
270
|
break;
|
|
262
271
|
}
|
|
@@ -398,58 +407,133 @@ const extractJsonObjects = (value) => {
|
|
|
398
407
|
}
|
|
399
408
|
return results;
|
|
400
409
|
};
|
|
410
|
+
const normalizeAgentFailoverEvents = (value) => {
|
|
411
|
+
if (!Array.isArray(value))
|
|
412
|
+
return [];
|
|
413
|
+
const events = [];
|
|
414
|
+
for (const entry of value) {
|
|
415
|
+
if (!isPlainObject(entry))
|
|
416
|
+
continue;
|
|
417
|
+
if (typeof entry.type !== "string" || entry.type.trim().length === 0)
|
|
418
|
+
continue;
|
|
419
|
+
events.push({ ...entry });
|
|
420
|
+
}
|
|
421
|
+
return events;
|
|
422
|
+
};
|
|
423
|
+
const mergeAgentFailoverEvents = (left, right) => {
|
|
424
|
+
if (!left.length)
|
|
425
|
+
return right;
|
|
426
|
+
if (!right.length)
|
|
427
|
+
return left;
|
|
428
|
+
const seen = new Set();
|
|
429
|
+
const merged = [];
|
|
430
|
+
const signature = (event) => [
|
|
431
|
+
event.type ?? "",
|
|
432
|
+
event.fromAgentId ?? "",
|
|
433
|
+
event.toAgentId ?? "",
|
|
434
|
+
event.at ?? "",
|
|
435
|
+
event.until ?? "",
|
|
436
|
+
event.durationMs ?? "",
|
|
437
|
+
].join("|");
|
|
438
|
+
for (const event of [...left, ...right]) {
|
|
439
|
+
const key = signature(event);
|
|
440
|
+
if (seen.has(key))
|
|
441
|
+
continue;
|
|
442
|
+
seen.add(key);
|
|
443
|
+
merged.push(event);
|
|
444
|
+
}
|
|
445
|
+
return merged;
|
|
446
|
+
};
|
|
447
|
+
const mergeAgentInvocationMetadata = (current, incoming) => {
|
|
448
|
+
if (!current && !incoming)
|
|
449
|
+
return undefined;
|
|
450
|
+
if (!incoming)
|
|
451
|
+
return current;
|
|
452
|
+
if (!current)
|
|
453
|
+
return { ...incoming };
|
|
454
|
+
const merged = { ...current, ...incoming };
|
|
455
|
+
const currentEvents = normalizeAgentFailoverEvents(current.failoverEvents);
|
|
456
|
+
const incomingEvents = normalizeAgentFailoverEvents(incoming.failoverEvents);
|
|
457
|
+
if (currentEvents.length > 0 || incomingEvents.length > 0) {
|
|
458
|
+
merged.failoverEvents = mergeAgentFailoverEvents(currentEvents, incomingEvents);
|
|
459
|
+
}
|
|
460
|
+
return merged;
|
|
461
|
+
};
|
|
462
|
+
const summarizeAgentFailoverEvent = (event) => {
|
|
463
|
+
const type = String(event.type ?? "unknown");
|
|
464
|
+
if (type === "switch_agent") {
|
|
465
|
+
const from = typeof event.fromAgentId === "string" ? event.fromAgentId : "unknown";
|
|
466
|
+
const to = typeof event.toAgentId === "string" ? event.toAgentId : "unknown";
|
|
467
|
+
return `switch_agent ${from} -> ${to}`;
|
|
468
|
+
}
|
|
469
|
+
if (type === "sleep_until_reset") {
|
|
470
|
+
const duration = typeof event.durationMs === "number" && Number.isFinite(event.durationMs)
|
|
471
|
+
? `${Math.round(event.durationMs / 1000)}s`
|
|
472
|
+
: "unknown duration";
|
|
473
|
+
const until = typeof event.until === "string" ? event.until : "unknown";
|
|
474
|
+
return `sleep_until_reset ${duration} (until ${until})`;
|
|
475
|
+
}
|
|
476
|
+
if (type === "stream_restart_after_limit") {
|
|
477
|
+
const from = typeof event.fromAgentId === "string" ? event.fromAgentId : "unknown";
|
|
478
|
+
return `stream_restart_after_limit from ${from}`;
|
|
479
|
+
}
|
|
480
|
+
return type;
|
|
481
|
+
};
|
|
482
|
+
const resolveTerminalFailoverAgentId = (events, fallbackAgentId) => {
|
|
483
|
+
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
484
|
+
const event = events[index];
|
|
485
|
+
if (event?.type !== "switch_agent")
|
|
486
|
+
continue;
|
|
487
|
+
if (typeof event.toAgentId === "string" && event.toAgentId.trim().length > 0) {
|
|
488
|
+
return event.toAgentId;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
return fallbackAgentId;
|
|
492
|
+
};
|
|
493
|
+
const compactNarrative = (value, fallback, maxLines = 5) => {
|
|
494
|
+
if (!value || value.trim().length === 0)
|
|
495
|
+
return fallback;
|
|
496
|
+
const lines = value
|
|
497
|
+
.split(/\r?\n/)
|
|
498
|
+
.map((line) => line
|
|
499
|
+
.replace(/^[*-]\s+/, "")
|
|
500
|
+
.replace(/^#+\s+/, "")
|
|
501
|
+
.replace(/^\*+\s*\*\*(.+?)\*\*\s*$/, "$1")
|
|
502
|
+
.trim())
|
|
503
|
+
.filter(Boolean)
|
|
504
|
+
.filter((line) => !/^(in scope|out of scope|key flows?|non-functional requirements|dependencies|risks|acceptance criteria|related docs?)\s*:/i.test(line))
|
|
505
|
+
.slice(0, maxLines);
|
|
506
|
+
return lines.length > 0 ? lines.join("\n") : fallback;
|
|
507
|
+
};
|
|
401
508
|
const buildEpicDescription = (epicKey, title, description, acceptance, relatedDocs) => {
|
|
509
|
+
const context = compactNarrative(description, `Deliver ${title} with implementation-ready scope and sequencing aligned to SDS guidance.`, 6);
|
|
402
510
|
return [
|
|
403
511
|
`* **Epic Key**: ${epicKey}`,
|
|
404
512
|
`* **Epic Title**: ${title}`,
|
|
405
513
|
"* **Context / Problem**",
|
|
406
514
|
"",
|
|
407
|
-
|
|
408
|
-
"* **Goals & Outcomes**",
|
|
409
|
-
formatBullets(acceptance, "List measurable outcomes for this epic."),
|
|
410
|
-
"* **In Scope**",
|
|
411
|
-
"- Clarify during refinement; derived from RFP/PDR/SDS.",
|
|
412
|
-
"* **Out of Scope**",
|
|
413
|
-
"- To be defined; exclude unrelated systems.",
|
|
414
|
-
"* **Key Flows / Scenarios**",
|
|
415
|
-
"- Outline primary user flows for this epic.",
|
|
416
|
-
"* **Non-functional Requirements**",
|
|
417
|
-
"- Performance, security, reliability expectations go here.",
|
|
418
|
-
"* **Dependencies & Constraints**",
|
|
419
|
-
"- Capture upstream/downstream systems and blockers.",
|
|
420
|
-
"* **Risks & Open Questions**",
|
|
421
|
-
"- Identify risks and unknowns to resolve.",
|
|
515
|
+
context,
|
|
422
516
|
"* **Acceptance Criteria**",
|
|
423
|
-
formatBullets(acceptance, "
|
|
517
|
+
formatBullets(acceptance, "Define measurable and testable outcomes for this epic."),
|
|
424
518
|
"* **Related Documentation / References**",
|
|
425
|
-
formatBullets(relatedDocs, "Link
|
|
519
|
+
formatBullets(relatedDocs, "Link SDS/PDR/OpenAPI references used by this epic."),
|
|
426
520
|
].join("\n");
|
|
427
521
|
};
|
|
428
522
|
const buildStoryDescription = (storyKey, title, userStory, description, acceptanceCriteria, relatedDocs) => {
|
|
523
|
+
const userStoryText = compactNarrative(userStory, `As a user, I want ${title} so that it delivers clear product value.`, 3);
|
|
524
|
+
const contextText = compactNarrative(description, `Implement ${title} with concrete scope and dependency context.`, 5);
|
|
429
525
|
return [
|
|
430
526
|
`* **Story Key**: ${storyKey}`,
|
|
431
527
|
"* **User Story**",
|
|
432
528
|
"",
|
|
433
|
-
|
|
529
|
+
userStoryText,
|
|
434
530
|
"* **Context**",
|
|
435
531
|
"",
|
|
436
|
-
|
|
437
|
-
"* **Preconditions / Assumptions**",
|
|
438
|
-
"- Confirm required data, environments, and access.",
|
|
439
|
-
"* **Main Flow**",
|
|
440
|
-
"- Outline the happy path for this story.",
|
|
441
|
-
"* **Alternative / Error Flows**",
|
|
442
|
-
"- Capture error handling and non-happy paths.",
|
|
443
|
-
"* **UX / UI Notes**",
|
|
444
|
-
"- Enumerate screens/states if applicable.",
|
|
445
|
-
"* **Data & Integrations**",
|
|
446
|
-
"- Note key entities, APIs, queues, or third-party dependencies.",
|
|
532
|
+
contextText,
|
|
447
533
|
"* **Acceptance Criteria**",
|
|
448
534
|
formatBullets(acceptanceCriteria, "List testable outcomes for this story."),
|
|
449
|
-
"* **Non-functional Requirements**",
|
|
450
|
-
"- Add story-specific performance/reliability/security expectations.",
|
|
451
535
|
"* **Related Documentation / References**",
|
|
452
|
-
formatBullets(relatedDocs, "Docdex handles, OpenAPI endpoints, code modules."),
|
|
536
|
+
formatBullets(relatedDocs, "Docdex handles, OpenAPI endpoints, and code modules."),
|
|
453
537
|
].join("\n");
|
|
454
538
|
};
|
|
455
539
|
const buildTaskDescription = (taskKey, title, description, storyKey, epicKey, relatedDocs, dependencies, tests, qa) => {
|
|
@@ -463,7 +547,7 @@ const buildTaskDescription = (taskKey, title, description, storyKey, epicKey, re
|
|
|
463
547
|
})
|
|
464
548
|
.join("\n");
|
|
465
549
|
};
|
|
466
|
-
const objectiveText =
|
|
550
|
+
const objectiveText = compactNarrative(description, `Deliver ${title} for story ${storyKey}.`, 3);
|
|
467
551
|
const implementationLines = extractActionableLines(description, 4);
|
|
468
552
|
const riskLines = extractRiskLines(description, 3);
|
|
469
553
|
const testsDefined = (tests.unitTests?.length ?? 0) +
|
|
@@ -482,14 +566,14 @@ const buildTaskDescription = (taskKey, title, description, storyKey, epicKey, re
|
|
|
482
566
|
qa?.blockers?.length ? "- Remaining QA blockers are explicit and actionable." : "- QA blockers are resolved or not present.",
|
|
483
567
|
];
|
|
484
568
|
const defaultImplementationPlan = [
|
|
485
|
-
|
|
569
|
+
`Implement ${title} with concrete file/module-level changes aligned to the objective.`,
|
|
486
570
|
dependencies.length
|
|
487
|
-
?
|
|
488
|
-
: "
|
|
571
|
+
? `Respect dependency order before completion: ${dependencies.join(", ")}.`
|
|
572
|
+
: "Finalize concrete implementation steps before coding and keep scope bounded.",
|
|
489
573
|
];
|
|
490
574
|
const defaultRisks = dependencies.length
|
|
491
|
-
? [
|
|
492
|
-
: ["
|
|
575
|
+
? [`Delivery depends on upstream tasks: ${dependencies.join(", ")}.`]
|
|
576
|
+
: ["Keep implementation aligned to SDS/OpenAPI contracts to avoid drift."];
|
|
493
577
|
return [
|
|
494
578
|
`* **Task Key**: ${taskKey}`,
|
|
495
579
|
"* **Objective**",
|
|
@@ -500,7 +584,7 @@ const buildTaskDescription = (taskKey, title, description, storyKey, epicKey, re
|
|
|
500
584
|
`- Epic: ${epicKey}`,
|
|
501
585
|
`- Story: ${storyKey}`,
|
|
502
586
|
"* **Inputs**",
|
|
503
|
-
formatBullets(relatedDocs, "
|
|
587
|
+
formatBullets(relatedDocs, "No explicit external references."),
|
|
504
588
|
"* **Implementation Plan**",
|
|
505
589
|
formatBullets(implementationLines, defaultImplementationPlan.join(" ")),
|
|
506
590
|
"* **Definition of Done**",
|
|
@@ -519,11 +603,11 @@ const buildTaskDescription = (taskKey, title, description, storyKey, epicKey, re
|
|
|
519
603
|
"* **QA Blockers**",
|
|
520
604
|
formatBullets(qa?.blockers, "None known."),
|
|
521
605
|
"* **Dependencies**",
|
|
522
|
-
formatBullets(dependencies, "
|
|
606
|
+
formatBullets(dependencies, "None."),
|
|
523
607
|
"* **Risks & Gotchas**",
|
|
524
608
|
formatBullets(riskLines, defaultRisks.join(" ")),
|
|
525
609
|
"* **Related Documentation / References**",
|
|
526
|
-
formatBullets(relatedDocs, "
|
|
610
|
+
formatBullets(relatedDocs, "None."),
|
|
527
611
|
].join("\n");
|
|
528
612
|
};
|
|
529
613
|
const collectFilesRecursively = async (target) => {
|
|
@@ -729,6 +813,7 @@ export class CreateTasksService {
|
|
|
729
813
|
this.ratingService = deps.ratingService;
|
|
730
814
|
this.taskOrderingFactory = deps.taskOrderingFactory ?? TaskOrderingService.create;
|
|
731
815
|
this.taskSufficiencyFactory = deps.taskSufficiencyFactory ?? TaskSufficiencyService.create;
|
|
816
|
+
this.sdsPreflightFactory = deps.sdsPreflightFactory ?? SdsPreflightService.create;
|
|
732
817
|
}
|
|
733
818
|
static async create(workspace) {
|
|
734
819
|
const repo = await GlobalRepository.create();
|
|
@@ -750,6 +835,7 @@ export class CreateTasksService {
|
|
|
750
835
|
workspaceRepo,
|
|
751
836
|
routingService,
|
|
752
837
|
taskSufficiencyFactory: TaskSufficiencyService.create,
|
|
838
|
+
sdsPreflightFactory: SdsPreflightService.create,
|
|
753
839
|
});
|
|
754
840
|
}
|
|
755
841
|
async close() {
|
|
@@ -838,6 +924,20 @@ export class CreateTasksService {
|
|
|
838
924
|
const resolved = path.isAbsolute(input) ? input : path.join(this.workspace.workspaceRoot, input);
|
|
839
925
|
return path.resolve(resolved).toLowerCase();
|
|
840
926
|
}
|
|
927
|
+
mergeDocInputs(primary, extras) {
|
|
928
|
+
const merged = [];
|
|
929
|
+
const seen = new Set();
|
|
930
|
+
for (const input of [...primary, ...extras]) {
|
|
931
|
+
if (!input?.trim())
|
|
932
|
+
continue;
|
|
933
|
+
const key = this.normalizeDocInputForSet(input);
|
|
934
|
+
if (seen.has(key))
|
|
935
|
+
continue;
|
|
936
|
+
seen.add(key);
|
|
937
|
+
merged.push(input);
|
|
938
|
+
}
|
|
939
|
+
return merged;
|
|
940
|
+
}
|
|
841
941
|
docIdentity(doc) {
|
|
842
942
|
const pathKey = `${doc.path ?? ""}`.trim().toLowerCase();
|
|
843
943
|
const idKey = `${doc.id ?? ""}`.trim().toLowerCase();
|
|
@@ -1489,6 +1589,21 @@ export class CreateTasksService {
|
|
|
1489
1589
|
"5) Keep task dependencies story-scoped while preserving epic/story/task ordering by this build method.",
|
|
1490
1590
|
].join("\n");
|
|
1491
1591
|
}
|
|
1592
|
+
buildProjectPlanArtifact(projectKey, docs, graph, buildMethod) {
|
|
1593
|
+
const sourceDocs = docs
|
|
1594
|
+
.map((doc) => doc.path ?? (doc.id ? `docdex:${doc.id}` : doc.title ?? "doc"))
|
|
1595
|
+
.filter((value) => Boolean(value))
|
|
1596
|
+
.slice(0, 24);
|
|
1597
|
+
return {
|
|
1598
|
+
projectKey,
|
|
1599
|
+
generatedAt: new Date().toISOString(),
|
|
1600
|
+
sourceDocs,
|
|
1601
|
+
startupWaves: graph.startupWaves.slice(0, 12),
|
|
1602
|
+
services: graph.services.slice(0, 40),
|
|
1603
|
+
foundationalDependencies: graph.foundationalDependencies.slice(0, 16),
|
|
1604
|
+
buildMethod,
|
|
1605
|
+
};
|
|
1606
|
+
}
|
|
1492
1607
|
orderStoryTasksByDependencies(storyTasks, serviceRank, taskServiceByScope) {
|
|
1493
1608
|
const byLocalId = new Map(storyTasks.map((task) => [task.localId, task]));
|
|
1494
1609
|
const indegree = new Map();
|
|
@@ -2093,8 +2208,11 @@ export class CreateTasksService {
|
|
|
2093
2208
|
const segmentHeadings = (doc.segments ?? [])
|
|
2094
2209
|
.map((segment) => segment.heading?.trim())
|
|
2095
2210
|
.filter((heading) => Boolean(heading));
|
|
2211
|
+
const segmentContentHeadings = (doc.segments ?? [])
|
|
2212
|
+
.flatMap((segment) => extractMarkdownHeadings(segment.content ?? "", Math.max(6, Math.ceil(limit / 4))))
|
|
2213
|
+
.slice(0, limit);
|
|
2096
2214
|
const contentHeadings = extractMarkdownHeadings(doc.content ?? "", limit);
|
|
2097
|
-
for (const heading of [...segmentHeadings, ...contentHeadings]) {
|
|
2215
|
+
for (const heading of [...segmentHeadings, ...segmentContentHeadings, ...contentHeadings]) {
|
|
2098
2216
|
const normalized = heading.replace(/[`*_]/g, "").trim();
|
|
2099
2217
|
if (!normalized)
|
|
2100
2218
|
continue;
|
|
@@ -2187,17 +2305,18 @@ export class CreateTasksService {
|
|
|
2187
2305
|
.filter(Boolean)
|
|
2188
2306
|
.join(" ");
|
|
2189
2307
|
const prompt = [
|
|
2190
|
-
`You are assisting in
|
|
2191
|
-
"
|
|
2192
|
-
"
|
|
2308
|
+
`You are assisting in phase 1 of 3 for project ${projectKey}: generate epics only.`,
|
|
2309
|
+
"Process is strict and direct: build plan -> epics -> stories -> tasks.",
|
|
2310
|
+
"This step outputs only epics derived from the build plan and docs.",
|
|
2193
2311
|
"Return strictly valid JSON (no prose) matching:",
|
|
2194
2312
|
EPIC_SCHEMA_SNIPPET,
|
|
2195
2313
|
"Rules:",
|
|
2196
2314
|
"- Do NOT include final slugs; the system will assign keys.",
|
|
2197
2315
|
"- Use docdex handles when referencing docs.",
|
|
2198
2316
|
"- acceptanceCriteria must be an array of strings (5-10 items).",
|
|
2199
|
-
"-
|
|
2200
|
-
"-
|
|
2317
|
+
"- Keep epics actionable and implementation-oriented; avoid glossary/admin-only epics.",
|
|
2318
|
+
"- Prefer dependency-first sequencing: foundational setup epics before dependent feature epics.",
|
|
2319
|
+
"- Keep output derived from docs; do not assume stacks unless docs state them.",
|
|
2201
2320
|
"Project construction method to follow:",
|
|
2202
2321
|
projectBuildMethod,
|
|
2203
2322
|
limits || "Use reasonable scope without over-generating epics.",
|
|
@@ -2337,6 +2456,7 @@ export class CreateTasksService {
|
|
|
2337
2456
|
async invokeAgentWithRetry(agent, prompt, action, stream, jobId, commandRunId, metadata) {
|
|
2338
2457
|
const startedAt = Date.now();
|
|
2339
2458
|
let output = "";
|
|
2459
|
+
let invocationMetadata;
|
|
2340
2460
|
const logChunk = async (chunk) => {
|
|
2341
2461
|
if (!chunk)
|
|
2342
2462
|
return;
|
|
@@ -2344,17 +2464,51 @@ export class CreateTasksService {
|
|
|
2344
2464
|
if (stream)
|
|
2345
2465
|
process.stdout.write(chunk);
|
|
2346
2466
|
};
|
|
2467
|
+
const baseInvocationMetadata = {
|
|
2468
|
+
command: "create-tasks",
|
|
2469
|
+
action,
|
|
2470
|
+
phase: `create_tasks_${action}`,
|
|
2471
|
+
};
|
|
2472
|
+
const logFailoverEvents = async (events) => {
|
|
2473
|
+
if (!events.length)
|
|
2474
|
+
return;
|
|
2475
|
+
for (const event of events) {
|
|
2476
|
+
await this.jobService.appendLog(jobId, `[create-tasks] agent failover (${action}): ${summarizeAgentFailoverEvent(event)}\n`);
|
|
2477
|
+
}
|
|
2478
|
+
};
|
|
2479
|
+
const resolveUsageAgent = async (events) => {
|
|
2480
|
+
const agentId = resolveTerminalFailoverAgentId(events, agent.id);
|
|
2481
|
+
if (agentId === agent.id) {
|
|
2482
|
+
return { id: agent.id, defaultModel: agent.defaultModel };
|
|
2483
|
+
}
|
|
2484
|
+
try {
|
|
2485
|
+
const resolved = await this.agentService.resolveAgent(agentId);
|
|
2486
|
+
return { id: resolved.id, defaultModel: resolved.defaultModel };
|
|
2487
|
+
}
|
|
2488
|
+
catch (error) {
|
|
2489
|
+
await this.jobService.appendLog(jobId, `[create-tasks] unable to resolve failover agent (${agentId}) for usage accounting: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
2490
|
+
return { id: agent.id, defaultModel: agent.defaultModel };
|
|
2491
|
+
}
|
|
2492
|
+
};
|
|
2347
2493
|
try {
|
|
2348
2494
|
if (stream) {
|
|
2349
|
-
const gen = await this.agentService.invokeStream(agent.id, {
|
|
2495
|
+
const gen = await this.agentService.invokeStream(agent.id, {
|
|
2496
|
+
input: prompt,
|
|
2497
|
+
metadata: baseInvocationMetadata,
|
|
2498
|
+
});
|
|
2350
2499
|
for await (const chunk of gen) {
|
|
2351
2500
|
output += chunk.output ?? "";
|
|
2501
|
+
invocationMetadata = mergeAgentInvocationMetadata(invocationMetadata, chunk.metadata);
|
|
2352
2502
|
await logChunk(chunk.output);
|
|
2353
2503
|
}
|
|
2354
2504
|
}
|
|
2355
2505
|
else {
|
|
2356
|
-
const result = await this.agentService.invoke(agent.id, {
|
|
2506
|
+
const result = await this.agentService.invoke(agent.id, {
|
|
2507
|
+
input: prompt,
|
|
2508
|
+
metadata: baseInvocationMetadata,
|
|
2509
|
+
});
|
|
2357
2510
|
output = result.output ?? "";
|
|
2511
|
+
invocationMetadata = mergeAgentInvocationMetadata(invocationMetadata, result.metadata);
|
|
2358
2512
|
await logChunk(output);
|
|
2359
2513
|
}
|
|
2360
2514
|
}
|
|
@@ -2371,10 +2525,22 @@ export class CreateTasksService {
|
|
|
2371
2525
|
`Original content:\n${output}`,
|
|
2372
2526
|
].join("\n\n");
|
|
2373
2527
|
try {
|
|
2374
|
-
|
|
2528
|
+
let retryInvocationMetadata;
|
|
2529
|
+
const fix = await this.agentService.invoke(agent.id, {
|
|
2530
|
+
input: fixPrompt,
|
|
2531
|
+
metadata: {
|
|
2532
|
+
...baseInvocationMetadata,
|
|
2533
|
+
attempt: 2,
|
|
2534
|
+
stage: "json_repair",
|
|
2535
|
+
},
|
|
2536
|
+
});
|
|
2375
2537
|
output = fix.output ?? "";
|
|
2538
|
+
retryInvocationMetadata = mergeAgentInvocationMetadata(retryInvocationMetadata, fix.metadata);
|
|
2376
2539
|
parsed = extractJson(output);
|
|
2377
2540
|
if (parsed) {
|
|
2541
|
+
const failoverEvents = normalizeAgentFailoverEvents(retryInvocationMetadata?.failoverEvents);
|
|
2542
|
+
await logFailoverEvents(failoverEvents);
|
|
2543
|
+
const usageAgent = await resolveUsageAgent(failoverEvents);
|
|
2378
2544
|
const promptTokens = estimateTokens(prompt);
|
|
2379
2545
|
const completionTokens = estimateTokens(output);
|
|
2380
2546
|
const durationSeconds = (Date.now() - startedAt) / 1000;
|
|
@@ -2383,8 +2549,8 @@ export class CreateTasksService {
|
|
|
2383
2549
|
workspaceId: this.workspace.workspaceId,
|
|
2384
2550
|
jobId,
|
|
2385
2551
|
commandRunId,
|
|
2386
|
-
agentId:
|
|
2387
|
-
modelName:
|
|
2552
|
+
agentId: usageAgent.id,
|
|
2553
|
+
modelName: usageAgent.defaultModel,
|
|
2388
2554
|
promptTokens,
|
|
2389
2555
|
completionTokens,
|
|
2390
2556
|
tokensPrompt: promptTokens,
|
|
@@ -2395,6 +2561,7 @@ export class CreateTasksService {
|
|
|
2395
2561
|
action: `create_tasks_${action}`,
|
|
2396
2562
|
phase: `create_tasks_${action}`,
|
|
2397
2563
|
attempt,
|
|
2564
|
+
failoverEvents: failoverEvents.length > 0 ? failoverEvents : undefined,
|
|
2398
2565
|
...(metadata ?? {}),
|
|
2399
2566
|
},
|
|
2400
2567
|
});
|
|
@@ -2408,6 +2575,9 @@ export class CreateTasksService {
|
|
|
2408
2575
|
if (!parsed) {
|
|
2409
2576
|
throw new Error(`Agent output was not valid JSON for ${action}`);
|
|
2410
2577
|
}
|
|
2578
|
+
const failoverEvents = normalizeAgentFailoverEvents(invocationMetadata?.failoverEvents);
|
|
2579
|
+
await logFailoverEvents(failoverEvents);
|
|
2580
|
+
const usageAgent = await resolveUsageAgent(failoverEvents);
|
|
2411
2581
|
const promptTokens = estimateTokens(prompt);
|
|
2412
2582
|
const completionTokens = estimateTokens(output);
|
|
2413
2583
|
const durationSeconds = (Date.now() - startedAt) / 1000;
|
|
@@ -2416,8 +2586,8 @@ export class CreateTasksService {
|
|
|
2416
2586
|
workspaceId: this.workspace.workspaceId,
|
|
2417
2587
|
jobId,
|
|
2418
2588
|
commandRunId,
|
|
2419
|
-
agentId:
|
|
2420
|
-
modelName:
|
|
2589
|
+
agentId: usageAgent.id,
|
|
2590
|
+
modelName: usageAgent.defaultModel,
|
|
2421
2591
|
promptTokens,
|
|
2422
2592
|
completionTokens,
|
|
2423
2593
|
tokensPrompt: promptTokens,
|
|
@@ -2428,6 +2598,7 @@ export class CreateTasksService {
|
|
|
2428
2598
|
action: `create_tasks_${action}`,
|
|
2429
2599
|
phase: `create_tasks_${action}`,
|
|
2430
2600
|
attempt: 1,
|
|
2601
|
+
failoverEvents: failoverEvents.length > 0 ? failoverEvents : undefined,
|
|
2431
2602
|
...(metadata ?? {}),
|
|
2432
2603
|
},
|
|
2433
2604
|
});
|
|
@@ -2453,14 +2624,15 @@ export class CreateTasksService {
|
|
|
2453
2624
|
}
|
|
2454
2625
|
async generateStoriesForEpic(agent, epic, docSummary, projectBuildMethod, stream, jobId, commandRunId) {
|
|
2455
2626
|
const prompt = [
|
|
2456
|
-
`Generate user stories for epic "${epic.title}".`,
|
|
2457
|
-
"
|
|
2627
|
+
`Generate user stories for epic "${epic.title}" (phase 2 of 3).`,
|
|
2628
|
+
"This phase is stories-only. Do not generate tasks yet.",
|
|
2458
2629
|
"Return JSON only matching:",
|
|
2459
2630
|
STORY_SCHEMA_SNIPPET,
|
|
2460
2631
|
"Rules:",
|
|
2461
2632
|
"- No tasks in this step.",
|
|
2462
2633
|
"- acceptanceCriteria must be an array of strings.",
|
|
2463
2634
|
"- Use docdex handles when citing docs.",
|
|
2635
|
+
"- Keep stories direct and implementation-oriented; avoid placeholder-only narrative sections.",
|
|
2464
2636
|
"- Keep story sequencing aligned with the project construction method.",
|
|
2465
2637
|
`Epic context (key=${epic.key ?? epic.localId ?? "TBD"}):`,
|
|
2466
2638
|
epic.description ?? "(no description provided)",
|
|
@@ -2498,8 +2670,8 @@ export class CreateTasksService {
|
|
|
2498
2670
|
.filter(Boolean);
|
|
2499
2671
|
};
|
|
2500
2672
|
const prompt = [
|
|
2501
|
-
`Generate tasks for story "${story.title}" (Epic: ${epic.title}).`,
|
|
2502
|
-
"
|
|
2673
|
+
`Generate tasks for story "${story.title}" (Epic: ${epic.title}, phase 3 of 3).`,
|
|
2674
|
+
"This phase is tasks-only for the given story.",
|
|
2503
2675
|
"Return JSON only matching:",
|
|
2504
2676
|
TASK_SCHEMA_SNIPPET,
|
|
2505
2677
|
"Rules:",
|
|
@@ -2517,6 +2689,7 @@ export class CreateTasksService {
|
|
|
2517
2689
|
"- Keep dependencies strictly inside this story; never reference tasks from other stories/epics.",
|
|
2518
2690
|
"- Order tasks from foundational prerequisites to dependents based on documented dependency direction and startup constraints.",
|
|
2519
2691
|
"- Avoid placeholder wording (TBD, TODO, to be defined, generic follow-up phrases).",
|
|
2692
|
+
"- Avoid documentation-only or glossary-only tasks unless story acceptance explicitly requires them.",
|
|
2520
2693
|
"- Use docdex handles when citing docs.",
|
|
2521
2694
|
"- If OPENAPI_HINTS are present in Docs, align tasks with hinted service/capability/stage/test_requirements.",
|
|
2522
2695
|
"- If SDS_COVERAGE_HINTS are present in Docs, cover the relevant SDS sections in implementation tasks.",
|
|
@@ -2766,14 +2939,15 @@ export class CreateTasksService {
|
|
|
2766
2939
|
: ["Coverage is heading-based heuristic match between SDS sections and generated epic/story/task corpus."],
|
|
2767
2940
|
};
|
|
2768
2941
|
}
|
|
2769
|
-
async writePlanArtifacts(projectKey, plan, docSummary, docs) {
|
|
2942
|
+
async writePlanArtifacts(projectKey, plan, docSummary, docs, buildPlan) {
|
|
2770
2943
|
const baseDir = path.join(this.workspace.mcodaDir, "tasks", projectKey);
|
|
2771
2944
|
await fs.mkdir(baseDir, { recursive: true });
|
|
2772
2945
|
const write = async (file, data) => {
|
|
2773
2946
|
const target = path.join(baseDir, file);
|
|
2774
2947
|
await fs.writeFile(target, JSON.stringify(data, null, 2), "utf8");
|
|
2775
2948
|
};
|
|
2776
|
-
await write("plan.json", { projectKey, generatedAt: new Date().toISOString(), docSummary, ...plan });
|
|
2949
|
+
await write("plan.json", { projectKey, generatedAt: new Date().toISOString(), docSummary, buildPlan, ...plan });
|
|
2950
|
+
await write("build-plan.json", buildPlan);
|
|
2777
2951
|
await write("epics.json", plan.epics);
|
|
2778
2952
|
await write("stories.json", plan.stories);
|
|
2779
2953
|
await write("tasks.json", plan.tasks);
|
|
@@ -3021,6 +3195,7 @@ export class CreateTasksService {
|
|
|
3021
3195
|
inputs: options.inputs,
|
|
3022
3196
|
agent: options.agentName,
|
|
3023
3197
|
agentStream,
|
|
3198
|
+
sdsPreflightCommit: options.sdsPreflightCommit === true,
|
|
3024
3199
|
},
|
|
3025
3200
|
});
|
|
3026
3201
|
let lastError;
|
|
@@ -3031,10 +3206,113 @@ export class CreateTasksService {
|
|
|
3031
3206
|
name: options.projectKey,
|
|
3032
3207
|
description: `Workspace project ${options.projectKey}`,
|
|
3033
3208
|
});
|
|
3034
|
-
|
|
3035
|
-
|
|
3209
|
+
let sdsPreflight;
|
|
3210
|
+
let sdsPreflightError;
|
|
3211
|
+
if (this.sdsPreflightFactory) {
|
|
3212
|
+
let sdsPreflightCloseError;
|
|
3213
|
+
try {
|
|
3214
|
+
const preflightService = await this.sdsPreflightFactory(this.workspace);
|
|
3215
|
+
try {
|
|
3216
|
+
sdsPreflight = await preflightService.runPreflight({
|
|
3217
|
+
workspace: options.workspace,
|
|
3218
|
+
projectKey: options.projectKey,
|
|
3219
|
+
inputPaths: options.inputs,
|
|
3220
|
+
sdsPaths: options.inputs,
|
|
3221
|
+
writeArtifacts: true,
|
|
3222
|
+
applyToSds: true,
|
|
3223
|
+
commitAppliedChanges: options.sdsPreflightCommit === true,
|
|
3224
|
+
commitMessage: options.sdsPreflightCommitMessage,
|
|
3225
|
+
});
|
|
3226
|
+
}
|
|
3227
|
+
finally {
|
|
3228
|
+
try {
|
|
3229
|
+
await preflightService.close();
|
|
3230
|
+
}
|
|
3231
|
+
catch (closeError) {
|
|
3232
|
+
sdsPreflightCloseError = closeError?.message ?? String(closeError);
|
|
3233
|
+
await this.jobService.appendLog(job.id, `SDS preflight close warning: ${sdsPreflightCloseError}\n`);
|
|
3234
|
+
}
|
|
3235
|
+
}
|
|
3236
|
+
}
|
|
3237
|
+
catch (error) {
|
|
3238
|
+
sdsPreflightError = error?.message ?? String(error);
|
|
3239
|
+
}
|
|
3240
|
+
if (!sdsPreflight) {
|
|
3241
|
+
const message = `create-tasks blocked: SDS preflight failed before backlog generation (${sdsPreflightError ?? "unknown error"}).`;
|
|
3242
|
+
await this.jobService.writeCheckpoint(job.id, {
|
|
3243
|
+
stage: "sds_preflight",
|
|
3244
|
+
timestamp: new Date().toISOString(),
|
|
3245
|
+
details: {
|
|
3246
|
+
status: "failed",
|
|
3247
|
+
error: message,
|
|
3248
|
+
readyForPlanning: false,
|
|
3249
|
+
qualityStatus: undefined,
|
|
3250
|
+
sourceSdsCount: 0,
|
|
3251
|
+
issueCount: 0,
|
|
3252
|
+
blockingIssueCount: 0,
|
|
3253
|
+
questionCount: 0,
|
|
3254
|
+
requiredQuestionCount: 0,
|
|
3255
|
+
reportPath: undefined,
|
|
3256
|
+
openQuestionsPath: undefined,
|
|
3257
|
+
gapAddendumPath: undefined,
|
|
3258
|
+
warnings: [],
|
|
3259
|
+
},
|
|
3260
|
+
});
|
|
3261
|
+
throw new Error(message);
|
|
3262
|
+
}
|
|
3263
|
+
const preflightWarnings = uniqueStrings([
|
|
3264
|
+
...(sdsPreflight.warnings ?? []),
|
|
3265
|
+
...(sdsPreflightCloseError ? [`SDS preflight close warning: ${sdsPreflightCloseError}`] : []),
|
|
3266
|
+
]);
|
|
3267
|
+
const blockingReasons = [];
|
|
3268
|
+
if (sdsPreflight.qualityStatus === "fail") {
|
|
3269
|
+
blockingReasons.push("SDS quality gates failed.");
|
|
3270
|
+
}
|
|
3271
|
+
if (sdsPreflight.blockingIssueCount > 0) {
|
|
3272
|
+
blockingReasons.push(`Blocking SDS issues: ${sdsPreflight.blockingIssueCount}.`);
|
|
3273
|
+
}
|
|
3274
|
+
if (sdsPreflight.requiredQuestionCount > 0) {
|
|
3275
|
+
blockingReasons.push(`Required open questions remaining: ${sdsPreflight.requiredQuestionCount}.`);
|
|
3276
|
+
}
|
|
3277
|
+
if (!sdsPreflight.readyForPlanning) {
|
|
3278
|
+
blockingReasons.push("SDS preflight reported planning context is not ready.");
|
|
3279
|
+
}
|
|
3280
|
+
if (blockingReasons.length > 0) {
|
|
3281
|
+
sdsPreflightError = blockingReasons.join(" ");
|
|
3282
|
+
}
|
|
3283
|
+
await this.jobService.writeCheckpoint(job.id, {
|
|
3284
|
+
stage: "sds_preflight",
|
|
3285
|
+
timestamp: new Date().toISOString(),
|
|
3286
|
+
details: {
|
|
3287
|
+
status: blockingReasons.length > 0 ? "blocked" : "succeeded",
|
|
3288
|
+
error: sdsPreflightError,
|
|
3289
|
+
readyForPlanning: sdsPreflight.readyForPlanning,
|
|
3290
|
+
qualityStatus: sdsPreflight.qualityStatus,
|
|
3291
|
+
sourceSdsCount: sdsPreflight.sourceSdsPaths.length,
|
|
3292
|
+
issueCount: sdsPreflight.issueCount,
|
|
3293
|
+
blockingIssueCount: sdsPreflight.blockingIssueCount,
|
|
3294
|
+
questionCount: sdsPreflight.questionCount,
|
|
3295
|
+
requiredQuestionCount: sdsPreflight.requiredQuestionCount,
|
|
3296
|
+
reportPath: sdsPreflight.reportPath,
|
|
3297
|
+
openQuestionsPath: sdsPreflight.openQuestionsPath,
|
|
3298
|
+
gapAddendumPath: sdsPreflight.gapAddendumPath,
|
|
3299
|
+
appliedToSds: sdsPreflight.appliedToSds,
|
|
3300
|
+
appliedSdsCount: sdsPreflight.appliedSdsPaths.length,
|
|
3301
|
+
commitHash: sdsPreflight.commitHash,
|
|
3302
|
+
warnings: preflightWarnings,
|
|
3303
|
+
},
|
|
3304
|
+
});
|
|
3305
|
+
if (blockingReasons.length > 0) {
|
|
3306
|
+
throw new Error(`create-tasks blocked by SDS preflight. ${blockingReasons.join(" ")} Report: ${sdsPreflight.reportPath}`);
|
|
3307
|
+
}
|
|
3308
|
+
}
|
|
3309
|
+
const preflightDocInputs = this.mergeDocInputs(options.inputs, sdsPreflight ? [...sdsPreflight.sourceSdsPaths, ...sdsPreflight.generatedDocPaths] : []);
|
|
3310
|
+
const docs = await this.prepareDocs(preflightDocInputs);
|
|
3311
|
+
const { docSummary, warnings: indexedDocWarnings } = this.buildDocContext(docs);
|
|
3312
|
+
const docWarnings = uniqueStrings([...(sdsPreflight?.warnings ?? []), ...indexedDocWarnings]);
|
|
3036
3313
|
const discoveryGraph = this.buildServiceDependencyGraph({ epics: [], stories: [], tasks: [] }, docs);
|
|
3037
3314
|
const projectBuildMethod = this.buildProjectConstructionMethod(docs, discoveryGraph);
|
|
3315
|
+
const projectBuildPlan = this.buildProjectPlanArtifact(options.projectKey, docs, discoveryGraph, projectBuildMethod);
|
|
3038
3316
|
const { prompt } = this.buildPrompt(options.projectKey, docs, projectBuildMethod, options);
|
|
3039
3317
|
const qaPreflight = await this.buildQaPreflight();
|
|
3040
3318
|
const qaOverrides = this.buildQaOverrides(options);
|
|
@@ -3043,6 +3321,15 @@ export class CreateTasksService {
|
|
|
3043
3321
|
timestamp: new Date().toISOString(),
|
|
3044
3322
|
details: { count: docs.length, warnings: docWarnings, startupWaves: discoveryGraph.startupWaves.slice(0, 8) },
|
|
3045
3323
|
});
|
|
3324
|
+
await this.jobService.writeCheckpoint(job.id, {
|
|
3325
|
+
stage: "build_plan_defined",
|
|
3326
|
+
timestamp: new Date().toISOString(),
|
|
3327
|
+
details: {
|
|
3328
|
+
sourceDocs: projectBuildPlan.sourceDocs.length,
|
|
3329
|
+
services: projectBuildPlan.services.length,
|
|
3330
|
+
startupWaves: projectBuildPlan.startupWaves.length,
|
|
3331
|
+
},
|
|
3332
|
+
});
|
|
3046
3333
|
await this.jobService.writeCheckpoint(job.id, {
|
|
3047
3334
|
stage: "qa_preflight",
|
|
3048
3335
|
timestamp: new Date().toISOString(),
|
|
@@ -3102,7 +3389,7 @@ export class CreateTasksService {
|
|
|
3102
3389
|
timestamp: new Date().toISOString(),
|
|
3103
3390
|
details: { tasks: plan.tasks.length, source: planSource, fallbackReason },
|
|
3104
3391
|
});
|
|
3105
|
-
const { folder } = await this.writePlanArtifacts(options.projectKey, plan, docSummary, docs);
|
|
3392
|
+
const { folder } = await this.writePlanArtifacts(options.projectKey, plan, docSummary, docs, projectBuildPlan);
|
|
3106
3393
|
await this.jobService.writeCheckpoint(job.id, {
|
|
3107
3394
|
stage: "plan_written",
|
|
3108
3395
|
timestamp: new Date().toISOString(),
|
|
@@ -3118,57 +3405,83 @@ export class CreateTasksService {
|
|
|
3118
3405
|
let sufficiencyAudit;
|
|
3119
3406
|
let sufficiencyAuditError;
|
|
3120
3407
|
if (this.taskSufficiencyFactory) {
|
|
3408
|
+
let sufficiencyCloseError;
|
|
3121
3409
|
try {
|
|
3122
3410
|
const sufficiencyService = await this.taskSufficiencyFactory(this.workspace);
|
|
3123
3411
|
try {
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
});
|
|
3130
|
-
}
|
|
3131
|
-
catch (error) {
|
|
3132
|
-
sufficiencyAuditError = error?.message ?? String(error);
|
|
3133
|
-
await this.jobService.appendLog(job.id, `Task sufficiency audit failed; continuing with created backlog: ${sufficiencyAuditError}\n`);
|
|
3134
|
-
}
|
|
3412
|
+
sufficiencyAudit = await sufficiencyService.runAudit({
|
|
3413
|
+
workspace: options.workspace,
|
|
3414
|
+
projectKey: options.projectKey,
|
|
3415
|
+
sourceCommand: "create-tasks",
|
|
3416
|
+
});
|
|
3135
3417
|
}
|
|
3136
3418
|
finally {
|
|
3137
3419
|
try {
|
|
3138
3420
|
await sufficiencyService.close();
|
|
3139
3421
|
}
|
|
3140
3422
|
catch (closeError) {
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
sufficiencyAuditError = sufficiencyAuditError ? `${sufficiencyAuditError}; ${details}` : details;
|
|
3144
|
-
await this.jobService.appendLog(job.id, `${details}\n`);
|
|
3423
|
+
sufficiencyCloseError = closeError?.message ?? String(closeError);
|
|
3424
|
+
await this.jobService.appendLog(job.id, `Task sufficiency audit close warning: ${sufficiencyCloseError}\n`);
|
|
3145
3425
|
}
|
|
3146
3426
|
}
|
|
3147
3427
|
}
|
|
3148
3428
|
catch (error) {
|
|
3149
3429
|
sufficiencyAuditError = error?.message ?? String(error);
|
|
3150
|
-
|
|
3430
|
+
}
|
|
3431
|
+
if (!sufficiencyAudit) {
|
|
3432
|
+
const message = `create-tasks blocked: task sufficiency audit failed (${sufficiencyAuditError ?? "unknown error"}).`;
|
|
3433
|
+
await this.jobService.writeCheckpoint(job.id, {
|
|
3434
|
+
stage: "task_sufficiency_audit",
|
|
3435
|
+
timestamp: new Date().toISOString(),
|
|
3436
|
+
details: {
|
|
3437
|
+
status: "failed",
|
|
3438
|
+
error: message,
|
|
3439
|
+
jobId: undefined,
|
|
3440
|
+
commandRunId: undefined,
|
|
3441
|
+
satisfied: false,
|
|
3442
|
+
dryRun: undefined,
|
|
3443
|
+
totalTasksAdded: undefined,
|
|
3444
|
+
totalTasksUpdated: undefined,
|
|
3445
|
+
finalCoverageRatio: undefined,
|
|
3446
|
+
reportPath: undefined,
|
|
3447
|
+
remainingSectionCount: undefined,
|
|
3448
|
+
remainingFolderCount: undefined,
|
|
3449
|
+
remainingGapCount: undefined,
|
|
3450
|
+
warnings: [],
|
|
3451
|
+
},
|
|
3452
|
+
});
|
|
3453
|
+
throw new Error(message);
|
|
3454
|
+
}
|
|
3455
|
+
const sufficiencyWarnings = uniqueStrings([
|
|
3456
|
+
...(sufficiencyAudit.warnings ?? []),
|
|
3457
|
+
...(sufficiencyCloseError ? [`Task sufficiency audit close warning: ${sufficiencyCloseError}`] : []),
|
|
3458
|
+
]);
|
|
3459
|
+
if (!sufficiencyAudit.satisfied) {
|
|
3460
|
+
sufficiencyAuditError = `SDS coverage target not reached (coverage=${sufficiencyAudit.finalCoverageRatio}, remaining gaps=${sufficiencyAudit.remainingGaps.total}).`;
|
|
3151
3461
|
}
|
|
3152
3462
|
await this.jobService.writeCheckpoint(job.id, {
|
|
3153
3463
|
stage: "task_sufficiency_audit",
|
|
3154
3464
|
timestamp: new Date().toISOString(),
|
|
3155
3465
|
details: {
|
|
3156
|
-
status: sufficiencyAudit ? "succeeded" : "
|
|
3466
|
+
status: sufficiencyAudit.satisfied ? "succeeded" : "blocked",
|
|
3157
3467
|
error: sufficiencyAuditError,
|
|
3158
|
-
jobId: sufficiencyAudit
|
|
3159
|
-
commandRunId: sufficiencyAudit
|
|
3160
|
-
satisfied: sufficiencyAudit
|
|
3161
|
-
dryRun: sufficiencyAudit
|
|
3162
|
-
totalTasksAdded: sufficiencyAudit
|
|
3163
|
-
totalTasksUpdated: sufficiencyAudit
|
|
3164
|
-
finalCoverageRatio: sufficiencyAudit
|
|
3165
|
-
reportPath: sufficiencyAudit
|
|
3166
|
-
remainingSectionCount: sufficiencyAudit
|
|
3167
|
-
remainingFolderCount: sufficiencyAudit
|
|
3168
|
-
remainingGapCount: sufficiencyAudit
|
|
3169
|
-
warnings:
|
|
3468
|
+
jobId: sufficiencyAudit.jobId,
|
|
3469
|
+
commandRunId: sufficiencyAudit.commandRunId,
|
|
3470
|
+
satisfied: sufficiencyAudit.satisfied,
|
|
3471
|
+
dryRun: sufficiencyAudit.dryRun,
|
|
3472
|
+
totalTasksAdded: sufficiencyAudit.totalTasksAdded,
|
|
3473
|
+
totalTasksUpdated: sufficiencyAudit.totalTasksUpdated,
|
|
3474
|
+
finalCoverageRatio: sufficiencyAudit.finalCoverageRatio,
|
|
3475
|
+
reportPath: sufficiencyAudit.reportPath,
|
|
3476
|
+
remainingSectionCount: sufficiencyAudit.remainingSectionHeadings.length,
|
|
3477
|
+
remainingFolderCount: sufficiencyAudit.remainingFolderEntries.length,
|
|
3478
|
+
remainingGapCount: sufficiencyAudit.remainingGaps.total,
|
|
3479
|
+
warnings: sufficiencyWarnings,
|
|
3170
3480
|
},
|
|
3171
3481
|
});
|
|
3482
|
+
if (!sufficiencyAudit.satisfied) {
|
|
3483
|
+
throw new Error(`create-tasks blocked: task sufficiency audit did not reach full coverage. Report: ${sufficiencyAudit.reportPath}`);
|
|
3484
|
+
}
|
|
3172
3485
|
}
|
|
3173
3486
|
await this.jobService.updateJobStatus(job.id, "completed", {
|
|
3174
3487
|
payload: {
|
|
@@ -3180,6 +3493,25 @@ export class CreateTasksService {
|
|
|
3180
3493
|
planFolder: folder,
|
|
3181
3494
|
planSource,
|
|
3182
3495
|
fallbackReason,
|
|
3496
|
+
sdsPreflight: sdsPreflight
|
|
3497
|
+
? {
|
|
3498
|
+
readyForPlanning: sdsPreflight.readyForPlanning,
|
|
3499
|
+
qualityStatus: sdsPreflight.qualityStatus,
|
|
3500
|
+
sourceSdsCount: sdsPreflight.sourceSdsPaths.length,
|
|
3501
|
+
issueCount: sdsPreflight.issueCount,
|
|
3502
|
+
blockingIssueCount: sdsPreflight.blockingIssueCount,
|
|
3503
|
+
questionCount: sdsPreflight.questionCount,
|
|
3504
|
+
requiredQuestionCount: sdsPreflight.requiredQuestionCount,
|
|
3505
|
+
appliedToSds: sdsPreflight.appliedToSds,
|
|
3506
|
+
appliedSdsPaths: sdsPreflight.appliedSdsPaths,
|
|
3507
|
+
commitHash: sdsPreflight.commitHash,
|
|
3508
|
+
reportPath: sdsPreflight.reportPath,
|
|
3509
|
+
openQuestionsPath: sdsPreflight.openQuestionsPath,
|
|
3510
|
+
gapAddendumPath: sdsPreflight.gapAddendumPath,
|
|
3511
|
+
warnings: sdsPreflight.warnings,
|
|
3512
|
+
}
|
|
3513
|
+
: undefined,
|
|
3514
|
+
sdsPreflightError,
|
|
3183
3515
|
sufficiencyAudit: sufficiencyAudit
|
|
3184
3516
|
? {
|
|
3185
3517
|
jobId: sufficiencyAudit.jobId,
|