@mcoda/core 0.1.34 → 0.1.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/dist/api/AgentsApi.d.ts +4 -1
  2. package/dist/api/AgentsApi.d.ts.map +1 -1
  3. package/dist/api/AgentsApi.js +4 -1
  4. package/dist/prompts/PdrPrompts.js +1 -1
  5. package/dist/services/docs/DocsService.d.ts +37 -0
  6. package/dist/services/docs/DocsService.d.ts.map +1 -1
  7. package/dist/services/docs/DocsService.js +537 -2
  8. package/dist/services/docs/review/gates/OpenQuestionsGate.d.ts.map +1 -1
  9. package/dist/services/docs/review/gates/OpenQuestionsGate.js +13 -2
  10. package/dist/services/docs/review/gates/SdsNoUnresolvedItemsGate.d.ts.map +1 -1
  11. package/dist/services/docs/review/gates/SdsNoUnresolvedItemsGate.js +12 -1
  12. package/dist/services/planning/CreateTasksService.d.ts +57 -0
  13. package/dist/services/planning/CreateTasksService.d.ts.map +1 -1
  14. package/dist/services/planning/CreateTasksService.js +2491 -291
  15. package/dist/services/planning/SdsCoverageModel.d.ts +27 -0
  16. package/dist/services/planning/SdsCoverageModel.d.ts.map +1 -0
  17. package/dist/services/planning/SdsCoverageModel.js +138 -0
  18. package/dist/services/planning/SdsPreflightService.d.ts +2 -0
  19. package/dist/services/planning/SdsPreflightService.d.ts.map +1 -1
  20. package/dist/services/planning/SdsPreflightService.js +131 -37
  21. package/dist/services/planning/SdsStructureSignals.d.ts +24 -0
  22. package/dist/services/planning/SdsStructureSignals.d.ts.map +1 -0
  23. package/dist/services/planning/SdsStructureSignals.js +402 -0
  24. package/dist/services/planning/TaskSufficiencyService.d.ts +17 -0
  25. package/dist/services/planning/TaskSufficiencyService.d.ts.map +1 -1
  26. package/dist/services/planning/TaskSufficiencyService.js +409 -278
  27. package/package.json +6 -6
@@ -166,6 +166,35 @@ const slugify = (value) => value
166
166
  .replace(/^-+|-+$/g, "")
167
167
  .replace(/-{2,}/g, "-") || "draft";
168
168
  const estimateTokens = (text) => Math.max(1, Math.ceil(text.length / 4));
169
+ const extractJsonObject = (raw) => {
170
+ if (!raw)
171
+ return undefined;
172
+ const fenced = raw.match(/```json([\s\S]*?)```/i);
173
+ const candidate = fenced ? fenced[1] : raw;
174
+ const start = candidate.indexOf("{");
175
+ const end = candidate.lastIndexOf("}");
176
+ if (start === -1 || end === -1 || end < start)
177
+ return undefined;
178
+ try {
179
+ return JSON.parse(candidate.slice(start, end + 1));
180
+ }
181
+ catch {
182
+ return undefined;
183
+ }
184
+ };
185
+ const extractMarkdownFromOutput = (raw) => {
186
+ const trimmed = raw.trim();
187
+ if (!trimmed)
188
+ return "";
189
+ const fenced = [...trimmed.matchAll(/```(?:markdown|md)?\s*([\s\S]*?)```/gi)];
190
+ if (fenced.length === 0)
191
+ return trimmed;
192
+ const best = fenced
193
+ .map((match) => (match[1] ?? "").trim())
194
+ .sort((a, b) => b.length - a.length)[0];
195
+ return best || trimmed;
196
+ };
197
+ const nowIso = () => new Date().toISOString();
169
198
  const SDS_CONTEXT_TOKEN_BUDGET = 8000;
170
199
  const extractBullets = (content, limit = 20) => {
171
200
  return content
@@ -1168,11 +1197,13 @@ const ensureSdsStructuredDraft = (draft, projectKey, context, template) => {
1168
1197
  structured = ensureSectionContent(structured, section, fallbackFor(section));
1169
1198
  }
1170
1199
  if ((context.openapi?.length ?? 0) === 0) {
1171
- const interfaceTitle = sections.find((section) => /interface|contract/i.test(section)) ?? "Interfaces & Contracts";
1200
+ const interfaceTitle = sections.find((section) => /(?:^|[\s,&/()-])(interface|interfaces|api|apis)(?:$|[\s,&/()-])/i.test(section)) ??
1201
+ sections.find((section) => /contract/i.test(section)) ??
1202
+ "Interfaces & Contracts";
1172
1203
  const extracted = extractSection(structured, interfaceTitle);
1173
1204
  if (extracted) {
1174
1205
  const scrubbed = stripInventedEndpoints(cleanBody(extracted.body ?? ""));
1175
- const openApiFallback = "No OpenAPI excerpts available. Capture interface needs as open questions (auth/identity, restaurant suggestions, voting cycles, results/analytics).";
1206
+ const openApiFallback = "No OpenAPI excerpts available. Capture interface needs as explicit contracts, assumptions, and open questions without inventing endpoint paths.";
1176
1207
  let body = scrubbed.length > 0 && !/endpoint/i.test(scrubbed) ? scrubbed : cleanBody(openApiFallback);
1177
1208
  if (!/openapi/i.test(body)) {
1178
1209
  body = `${body}\n- No OpenAPI excerpts available; keep endpoints as open questions.`;
@@ -1610,6 +1641,272 @@ export class DocsService {
1610
1641
  const slug = slugify(projectKey ?? "sds");
1611
1642
  return path.join(this.workspace.mcodaDir, "docs", "sds", `${slug}.md`);
1612
1643
  }
1644
+ normalizeMaxIterations(value) {
1645
+ if (!Number.isFinite(value))
1646
+ return 100;
1647
+ const floored = Math.floor(value);
1648
+ if (floored < 1)
1649
+ return 1;
1650
+ if (floored > 100)
1651
+ return 100;
1652
+ return floored;
1653
+ }
1654
+ async listSdsSuggestionPathCandidates(projectKey) {
1655
+ const projectSlug = slugify(projectKey ?? "sds");
1656
+ const prioritized = [
1657
+ path.join(this.workspace.workspaceRoot, "docs", "sds", "sds.md"),
1658
+ path.join(this.workspace.workspaceRoot, "docs", "sds.md"),
1659
+ ];
1660
+ const dynamicDir = path.join(this.workspace.workspaceRoot, "docs", "sds");
1661
+ const dynamicCandidates = [];
1662
+ try {
1663
+ const entries = await fs.readdir(dynamicDir, { withFileTypes: true });
1664
+ const markdown = entries
1665
+ .filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith(".md"))
1666
+ .map((entry) => path.join(dynamicDir, entry.name));
1667
+ const statRows = await Promise.all(markdown.map(async (candidatePath) => ({
1668
+ candidatePath,
1669
+ stat: await fs.stat(candidatePath),
1670
+ })));
1671
+ statRows
1672
+ .sort((a, b) => b.stat.mtimeMs - a.stat.mtimeMs)
1673
+ .forEach((row) => dynamicCandidates.push(row.candidatePath));
1674
+ }
1675
+ catch {
1676
+ // Directory may not exist.
1677
+ }
1678
+ const fallback = [
1679
+ path.join(this.workspace.mcodaDir, "docs", "sds", `${projectSlug}.md`),
1680
+ path.join(this.workspace.mcodaDir, "docs", "sds", "sds.md"),
1681
+ ];
1682
+ return Array.from(new Set([...prioritized, ...dynamicCandidates, ...fallback]));
1683
+ }
1684
+ async resolveSdsSuggestionsPath(projectKey, explicitSdsPath) {
1685
+ if (explicitSdsPath) {
1686
+ const resolved = path.isAbsolute(explicitSdsPath)
1687
+ ? explicitSdsPath
1688
+ : path.resolve(this.workspace.workspaceRoot, explicitSdsPath);
1689
+ try {
1690
+ await fs.access(resolved);
1691
+ }
1692
+ catch {
1693
+ throw new Error(`Unable to locate SDS file at --sds-path: ${resolved}`);
1694
+ }
1695
+ return { path: resolved, attempted: [resolved] };
1696
+ }
1697
+ const attempted = await this.listSdsSuggestionPathCandidates(projectKey);
1698
+ for (const candidate of attempted) {
1699
+ try {
1700
+ const stat = await fs.stat(candidate);
1701
+ if (stat.isFile()) {
1702
+ return { path: candidate, attempted };
1703
+ }
1704
+ }
1705
+ catch {
1706
+ // keep trying
1707
+ }
1708
+ }
1709
+ throw new Error(`Unable to locate an SDS file. Checked: ${attempted.join(", ")}. ` +
1710
+ `Pass --sds-path <FILE> to specify the SDS explicitly.`);
1711
+ }
1712
+ async ensureSuggestionsDir() {
1713
+ const suggestionsDir = path.join(this.workspace.workspaceRoot, "docs", "suggestions");
1714
+ await fs.mkdir(suggestionsDir, { recursive: true });
1715
+ return suggestionsDir;
1716
+ }
1717
+ async nextSuggestionsFilePath(suggestionsDir) {
1718
+ const entries = await fs.readdir(suggestionsDir).catch(() => []);
1719
+ const numbers = entries
1720
+ .map((entry) => /^sds_suggestions(\d+)\.md$/i.exec(entry))
1721
+ .filter((match) => Boolean(match))
1722
+ .map((match) => Number.parseInt(match[1], 10))
1723
+ .filter((value) => Number.isFinite(value));
1724
+ const next = (numbers.length > 0 ? Math.max(...numbers) : 0) + 1;
1725
+ return path.join(suggestionsDir, `sds_suggestions${next}.md`);
1726
+ }
1727
+ rankSdsSuggestionCandidates(candidates, required, preferred) {
1728
+ const requiredCaps = Array.from(new Set(required));
1729
+ const preferredCaps = Array.from(new Set(preferred));
1730
+ const scored = candidates
1731
+ .filter((candidate) => candidate.healthStatus !== "unreachable")
1732
+ .map((candidate) => {
1733
+ const caps = Array.from(new Set(candidate.capabilities ?? []));
1734
+ const requiredMatches = requiredCaps.filter((cap) => caps.includes(cap)).length;
1735
+ const preferredMatches = preferredCaps.filter((cap) => caps.includes(cap)).length;
1736
+ const hasRequired = requiredCaps.length === 0 || requiredMatches === requiredCaps.length;
1737
+ const rating = Number(candidate.agent.rating ?? 0);
1738
+ const reasoning = Number(candidate.agent.reasoningRating ?? rating);
1739
+ const cost = Number.isFinite(candidate.agent.costPerMillion)
1740
+ ? Number(candidate.agent.costPerMillion)
1741
+ : Number.POSITIVE_INFINITY;
1742
+ return { candidate, requiredMatches, preferredMatches, hasRequired, rating, reasoning, cost };
1743
+ });
1744
+ scored.sort((a, b) => {
1745
+ if (a.hasRequired !== b.hasRequired)
1746
+ return a.hasRequired ? -1 : 1;
1747
+ if (a.requiredMatches !== b.requiredMatches)
1748
+ return b.requiredMatches - a.requiredMatches;
1749
+ if (a.preferredMatches !== b.preferredMatches)
1750
+ return b.preferredMatches - a.preferredMatches;
1751
+ if (a.rating !== b.rating)
1752
+ return b.rating - a.rating;
1753
+ if (a.reasoning !== b.reasoning)
1754
+ return b.reasoning - a.reasoning;
1755
+ if (a.cost !== b.cost)
1756
+ return a.cost - b.cost;
1757
+ return (a.candidate.agent.slug ?? a.candidate.agent.id).localeCompare(b.candidate.agent.slug ?? b.candidate.agent.id);
1758
+ });
1759
+ return scored.map((row) => row.candidate);
1760
+ }
1761
+ async selectSdsSuggestionAgents(input) {
1762
+ const required = ["doc_generation", "docdex_query"];
1763
+ const preferred = ["sds_writing", "spec_generation", "code_review", "multiple_draft_generation"];
1764
+ const warnings = [];
1765
+ const candidates = this.rankSdsSuggestionCandidates(await this.collectDocgenCandidates(), required, preferred);
1766
+ if (candidates.length === 0) {
1767
+ throw new Error("No healthy agents are available for SDS suggestions.");
1768
+ }
1769
+ const findByName = async (name) => {
1770
+ try {
1771
+ return await this.agentService.resolveAgent(name);
1772
+ }
1773
+ catch {
1774
+ throw new Error(`Unable to resolve agent: ${name}`);
1775
+ }
1776
+ };
1777
+ const rankedIds = new Set(candidates.map((candidate) => candidate.agent.id));
1778
+ const rankedSlugs = new Set(candidates.map((candidate) => candidate.agent.slug).filter(Boolean));
1779
+ const isRankedCandidate = (agent) => rankedIds.has(agent.id) || (typeof agent.slug === "string" && rankedSlugs.has(agent.slug));
1780
+ const reviewer = input.reviewAgentName
1781
+ ? await findByName(input.reviewAgentName)
1782
+ : candidates[0].agent;
1783
+ const fixer = input.fixAgentName
1784
+ ? await findByName(input.fixAgentName)
1785
+ : candidates.find((candidate) => candidate.agent.id !== reviewer.id)?.agent ?? reviewer;
1786
+ if (input.reviewAgentName && !isRankedCandidate(reviewer)) {
1787
+ warnings.push(`Review agent override ${reviewer.slug ?? reviewer.id} is not in the healthy ranked candidate set; proceeding due explicit override.`);
1788
+ }
1789
+ if (input.fixAgentName && !isRankedCandidate(fixer)) {
1790
+ warnings.push(`Fix agent override ${fixer.slug ?? fixer.id} is not in the healthy ranked candidate set; proceeding due explicit override.`);
1791
+ }
1792
+ if (reviewer.id === fixer.id) {
1793
+ warnings.push(`Reviewer and fixer resolved to the same agent (${reviewer.slug ?? reviewer.id}); continuing with single-agent loop.`);
1794
+ }
1795
+ return { reviewer, fixer, warnings };
1796
+ }
1797
+ buildSdsSuggestionsReviewPrompt(input) {
1798
+ const contextBlocks = input.context.blocks
1799
+ .slice(0, 5)
1800
+ .map((block) => `- ${block.summary}`)
1801
+ .join("\n");
1802
+ return [
1803
+ "You are the SDS reviewer agent.",
1804
+ "Review the SDS against surrounding project documentation and produce strict remediation guidance.",
1805
+ "Focus on:",
1806
+ "- open questions and optimum answers aligned to the rest of documentation,",
1807
+ "- inconsistencies,",
1808
+ "- issues/risks,",
1809
+ "- possible enhancements.",
1810
+ "",
1811
+ "Return output in two parts:",
1812
+ "1) A JSON object inside a ```json code block with schema:",
1813
+ '{ "result": "PASS|FAIL", "issueCount": number, "summary": "short summary" }',
1814
+ "2) Markdown sections:",
1815
+ "## Open Questions and Optimum Answers",
1816
+ "## Inconsistencies",
1817
+ "## Issues",
1818
+ "## Enhancements",
1819
+ "## Suggested Fixes",
1820
+ "",
1821
+ "Set result=PASS and issueCount=0 only when there are no remaining issues.",
1822
+ "",
1823
+ `Iteration: ${input.iteration}`,
1824
+ `Project: ${input.projectKey ?? "n/a"}`,
1825
+ `SDS Path: ${input.sdsPath}`,
1826
+ "",
1827
+ `Context Summary: ${input.context.summary}`,
1828
+ contextBlocks ? `Context Blocks:\n${contextBlocks}` : "Context Blocks: none",
1829
+ "",
1830
+ "SDS Content:",
1831
+ input.sdsContent,
1832
+ ].join("\n");
1833
+ }
1834
+ parseSdsSuggestionsReviewResult(raw) {
1835
+ const parsed = extractJsonObject(raw);
1836
+ const parsedResult = String(parsed?.result ?? parsed?.status ?? parsed?.outcome ?? "")
1837
+ .trim()
1838
+ .toUpperCase();
1839
+ const parsedIssueCount = Number(parsed?.issueCount ?? parsed?.issues ?? parsed?.issue_count ?? NaN);
1840
+ const summary = String(parsed?.summary ?? "").trim();
1841
+ const normalized = raw.trim();
1842
+ const hasNoIssuesSignal = /\bRESULT\s*:\s*PASS\b/i.test(normalized) ||
1843
+ /\bno (remaining )?(issues|inconsistencies|open questions)\b/i.test(normalized) ||
1844
+ /\ball clear\b/i.test(normalized);
1845
+ const preliminaryResult = parsedResult === "PASS"
1846
+ ? "PASS"
1847
+ : parsedResult === "FAIL"
1848
+ ? "FAIL"
1849
+ : hasNoIssuesSignal
1850
+ ? "PASS"
1851
+ : "FAIL";
1852
+ const issueCount = Number.isFinite(parsedIssueCount)
1853
+ ? Math.max(0, Math.floor(parsedIssueCount))
1854
+ : preliminaryResult === "PASS"
1855
+ ? 0
1856
+ : 1;
1857
+ const result = preliminaryResult === "PASS" && issueCount === 0 ? "PASS" : "FAIL";
1858
+ return {
1859
+ result,
1860
+ issueCount,
1861
+ summary: summary || (result === "PASS" ? "No issues found." : "Issues detected."),
1862
+ markdown: normalized,
1863
+ raw,
1864
+ };
1865
+ }
1866
+ buildSdsSuggestionsFixPrompt(input) {
1867
+ return [
1868
+ "You are the SDS fixer agent.",
1869
+ "Apply all valid review suggestions to the SDS.",
1870
+ "Integrate optimum answers for open questions and resolve inconsistencies/issues.",
1871
+ "Preserve valid existing sections and improve structure where needed.",
1872
+ "Return ONLY the full revised SDS markdown (no prose wrapper, no JSON).",
1873
+ "",
1874
+ `Project: ${input.projectKey ?? "n/a"}`,
1875
+ `SDS Path: ${input.sdsPath}`,
1876
+ "",
1877
+ "Review Suggestions:",
1878
+ input.suggestions,
1879
+ "",
1880
+ "Current SDS:",
1881
+ input.currentSds,
1882
+ ].join("\n");
1883
+ }
1884
+ renderSdsSuggestionArtifact(input) {
1885
+ return [
1886
+ `# SDS Suggestions ${input.iteration}`,
1887
+ "",
1888
+ `- Timestamp: ${input.timestamp}`,
1889
+ `- Iteration: ${input.iteration}`,
1890
+ `- Reviewer Agent: ${input.reviewer.slug ?? input.reviewer.id}`,
1891
+ `- Fixer Agent: ${input.fixer.slug ?? input.fixer.id}`,
1892
+ `- SDS Path: ${input.sdsPath}`,
1893
+ `- Result: ${input.review.result}`,
1894
+ `- Issue Count: ${input.review.issueCount}`,
1895
+ `- Job ID: ${input.jobId}`,
1896
+ `- Command Run ID: ${input.commandRunId}`,
1897
+ "",
1898
+ "## Review Summary",
1899
+ input.review.summary,
1900
+ "",
1901
+ "## Reviewer Output",
1902
+ input.review.markdown || "(empty reviewer output)",
1903
+ "",
1904
+ "## Fix Application",
1905
+ `- Applied: ${input.fixApplied ? "yes" : "no"}`,
1906
+ `- Summary: ${input.fixSummary}`,
1907
+ "",
1908
+ ].join("\n");
1909
+ }
1613
1910
  async loadSdsTemplate(templateName) {
1614
1911
  const names = templateName
1615
1912
  ? [templateName.replace(/\.md$/i, "")]
@@ -3885,4 +4182,242 @@ export class DocsService {
3885
4182
  throw error;
3886
4183
  }
3887
4184
  }
4185
+ async generateSdsSuggestions(options) {
4186
+ const warnings = [];
4187
+ await this.checkSdsDocdexProfile(warnings);
4188
+ const assembler = new DocContextAssembler(this.docdex, this.workspace);
4189
+ const stream = options.agentStream === true;
4190
+ const maxIterations = this.normalizeMaxIterations(options.maxIterations);
4191
+ const commandRun = await this.jobService.startCommandRun("docs-sds-suggestions", options.projectKey);
4192
+ const job = await this.jobService.startJob("sds_suggestions", commandRun.id, options.projectKey, {
4193
+ commandName: commandRun.commandName,
4194
+ payload: {
4195
+ projectKey: options.projectKey,
4196
+ sdsPath: options.sdsPath,
4197
+ reviewAgentName: options.reviewAgentName,
4198
+ fixAgentName: options.fixAgentName,
4199
+ maxIterations,
4200
+ },
4201
+ });
4202
+ try {
4203
+ const context = await assembler.buildSdsContext({ projectKey: options.projectKey });
4204
+ warnings.push(...context.warnings);
4205
+ const pathResolution = await this.resolveSdsSuggestionsPath(options.projectKey, options.sdsPath);
4206
+ const sdsPath = pathResolution.path;
4207
+ let sdsContent = await fs.readFile(sdsPath, "utf8");
4208
+ const suggestionsDir = await this.ensureSuggestionsDir();
4209
+ const agentSelection = await this.selectSdsSuggestionAgents({
4210
+ reviewAgentName: options.reviewAgentName,
4211
+ fixAgentName: options.fixAgentName,
4212
+ });
4213
+ warnings.push(...agentSelection.warnings);
4214
+ const reviewer = agentSelection.reviewer;
4215
+ const fixer = agentSelection.fixer;
4216
+ const suggestionFiles = [];
4217
+ let iterations = 0;
4218
+ let finalStatus = "max_iterations_reached";
4219
+ await this.jobService.writeCheckpoint(job.id, {
4220
+ stage: "sds_loaded",
4221
+ timestamp: nowIso(),
4222
+ details: {
4223
+ sdsPath,
4224
+ attemptedPaths: pathResolution.attempted,
4225
+ maxIterations,
4226
+ },
4227
+ });
4228
+ await this.jobService.writeCheckpoint(job.id, {
4229
+ stage: "agents_selected",
4230
+ timestamp: nowIso(),
4231
+ details: {
4232
+ reviewerAgent: reviewer.slug ?? reviewer.id,
4233
+ fixerAgent: fixer.slug ?? fixer.id,
4234
+ },
4235
+ });
4236
+ for (let iteration = 1; iteration <= maxIterations; iteration += 1) {
4237
+ iterations = iteration;
4238
+ const reviewPrompt = this.buildSdsSuggestionsReviewPrompt({
4239
+ iteration,
4240
+ projectKey: options.projectKey,
4241
+ sdsPath,
4242
+ sdsContent,
4243
+ context,
4244
+ });
4245
+ const reviewInvoke = await this.invokeAgent(reviewer, reviewPrompt, stream, job.id, options.onToken);
4246
+ const reviewResult = this.parseSdsSuggestionsReviewResult(reviewInvoke.output);
4247
+ const suggestionPath = await this.nextSuggestionsFilePath(suggestionsDir);
4248
+ let fixApplied = false;
4249
+ let fixSummary = "Fix pending.";
4250
+ const preliminaryArtifact = this.renderSdsSuggestionArtifact({
4251
+ timestamp: nowIso(),
4252
+ iteration,
4253
+ reviewer,
4254
+ fixer,
4255
+ sdsPath,
4256
+ review: reviewResult,
4257
+ fixApplied,
4258
+ fixSummary,
4259
+ jobId: job.id,
4260
+ commandRunId: commandRun.id,
4261
+ });
4262
+ await fs.writeFile(suggestionPath, preliminaryArtifact, "utf8");
4263
+ suggestionFiles.push(suggestionPath);
4264
+ await this.jobService.writeCheckpoint(job.id, {
4265
+ stage: "iteration_reviewed",
4266
+ timestamp: nowIso(),
4267
+ details: {
4268
+ iteration,
4269
+ result: reviewResult.result,
4270
+ issueCount: reviewResult.issueCount,
4271
+ suggestionPath,
4272
+ },
4273
+ });
4274
+ if (reviewResult.result === "PASS" && reviewResult.issueCount === 0) {
4275
+ finalStatus = "pass";
4276
+ fixSummary = "Reviewer found no issues.";
4277
+ const artifact = this.renderSdsSuggestionArtifact({
4278
+ timestamp: nowIso(),
4279
+ iteration,
4280
+ reviewer,
4281
+ fixer,
4282
+ sdsPath,
4283
+ review: reviewResult,
4284
+ fixApplied,
4285
+ fixSummary,
4286
+ jobId: job.id,
4287
+ commandRunId: commandRun.id,
4288
+ });
4289
+ await fs.writeFile(suggestionPath, artifact, "utf8");
4290
+ break;
4291
+ }
4292
+ if (options.dryRun) {
4293
+ finalStatus = "dry_run";
4294
+ fixSummary = "Dry-run enabled; no SDS changes applied.";
4295
+ const artifact = this.renderSdsSuggestionArtifact({
4296
+ timestamp: nowIso(),
4297
+ iteration,
4298
+ reviewer,
4299
+ fixer,
4300
+ sdsPath,
4301
+ review: reviewResult,
4302
+ fixApplied,
4303
+ fixSummary,
4304
+ jobId: job.id,
4305
+ commandRunId: commandRun.id,
4306
+ });
4307
+ await fs.writeFile(suggestionPath, artifact, "utf8");
4308
+ break;
4309
+ }
4310
+ const suggestionsArtifact = await fs.readFile(suggestionPath, "utf8");
4311
+ const fixPrompt = this.buildSdsSuggestionsFixPrompt({
4312
+ projectKey: options.projectKey,
4313
+ sdsPath,
4314
+ currentSds: sdsContent,
4315
+ suggestions: suggestionsArtifact,
4316
+ });
4317
+ const fixInvoke = await this.invokeAgent(fixer, fixPrompt, stream, job.id, options.onToken);
4318
+ const candidateSds = extractMarkdownFromOutput(fixInvoke.output).trim();
4319
+ if (candidateSds.length < 80) {
4320
+ fixSummary = "Fixer output was too short; previous SDS kept.";
4321
+ warnings.push(`Iteration ${iteration}: fixer output too short; no SDS write performed.`);
4322
+ }
4323
+ else if (candidateSds === sdsContent.trim()) {
4324
+ fixSummary = "Fixer returned unchanged SDS content.";
4325
+ }
4326
+ else {
4327
+ await fs.writeFile(sdsPath, candidateSds, "utf8");
4328
+ sdsContent = candidateSds;
4329
+ fixApplied = true;
4330
+ fixSummary = "SDS updated from fixer output.";
4331
+ if (context.docdexAvailable) {
4332
+ try {
4333
+ await this.registerSds(sdsPath, sdsContent, options.projectKey);
4334
+ }
4335
+ catch (error) {
4336
+ warnings.push(`Iteration ${iteration}: docdex register skipped: ${error.message}`);
4337
+ }
4338
+ }
4339
+ }
4340
+ const artifact = this.renderSdsSuggestionArtifact({
4341
+ timestamp: nowIso(),
4342
+ iteration,
4343
+ reviewer,
4344
+ fixer,
4345
+ sdsPath,
4346
+ review: reviewResult,
4347
+ fixApplied,
4348
+ fixSummary,
4349
+ jobId: job.id,
4350
+ commandRunId: commandRun.id,
4351
+ });
4352
+ await fs.writeFile(suggestionPath, artifact, "utf8");
4353
+ await this.jobService.writeCheckpoint(job.id, {
4354
+ stage: "iteration_fixed",
4355
+ timestamp: nowIso(),
4356
+ details: {
4357
+ iteration,
4358
+ result: reviewResult.result,
4359
+ issueCount: reviewResult.issueCount,
4360
+ fixApplied,
4361
+ suggestionPath,
4362
+ },
4363
+ });
4364
+ }
4365
+ if (finalStatus === "max_iterations_reached") {
4366
+ warnings.push(`SDS suggestions stopped at max iterations (${maxIterations}) before reviewer returned PASS.`);
4367
+ }
4368
+ await this.jobService.updateJobStatus(job.id, "completed", {
4369
+ payload: {
4370
+ sdsPath,
4371
+ suggestionsDir,
4372
+ suggestionFiles,
4373
+ reviewerAgentId: reviewer.id,
4374
+ fixerAgentId: fixer.id,
4375
+ iterations,
4376
+ finalStatus,
4377
+ },
4378
+ });
4379
+ await this.jobService.finishCommandRun(commandRun.id, "succeeded");
4380
+ if (options.rateAgents) {
4381
+ try {
4382
+ const ratingService = await this.ensureRatingService();
4383
+ await ratingService.rate({
4384
+ workspace: this.workspace,
4385
+ agentId: reviewer.id,
4386
+ commandName: "docs-sds-suggestions-review",
4387
+ jobId: job.id,
4388
+ commandRunId: commandRun.id,
4389
+ });
4390
+ if (fixer.id !== reviewer.id) {
4391
+ await ratingService.rate({
4392
+ workspace: this.workspace,
4393
+ agentId: fixer.id,
4394
+ commandName: "docs-sds-suggestions-fix",
4395
+ jobId: job.id,
4396
+ commandRunId: commandRun.id,
4397
+ });
4398
+ }
4399
+ }
4400
+ catch (error) {
4401
+ warnings.push(`Agent rating failed: ${error.message ?? String(error)}`);
4402
+ }
4403
+ }
4404
+ return {
4405
+ jobId: job.id,
4406
+ commandRunId: commandRun.id,
4407
+ sdsPath,
4408
+ suggestionsDir,
4409
+ suggestionFiles,
4410
+ reviewerAgentId: reviewer.id,
4411
+ fixerAgentId: fixer.id,
4412
+ iterations,
4413
+ finalStatus,
4414
+ warnings,
4415
+ };
4416
+ }
4417
+ catch (error) {
4418
+ await this.jobService.updateJobStatus(job.id, "failed", { errorSummary: error.message });
4419
+ await this.jobService.finishCommandRun(commandRun.id, "failed", error.message);
4420
+ throw error;
4421
+ }
4422
+ }
3888
4423
  }
@@ -1 +1 @@
1
- {"version":3,"file":"OpenQuestionsGate.d.ts","sourceRoot":"","sources":["../../../../../src/services/docs/review/gates/OpenQuestionsGate.ts"],"names":[],"mappings":"AACA,OAAO,EAAqB,uBAAuB,EAAE,MAAM,2BAA2B,CAAC;AACvF,OAAO,EAAE,gBAAgB,EAA+B,MAAM,mBAAmB,CAAC;AAElF,MAAM,WAAW,sBAAsB;IACrC,SAAS,EAAE,uBAAuB,CAAC;CACpC;AAuLD,eAAO,MAAM,oBAAoB,GAC/B,OAAO,sBAAsB,KAC5B,OAAO,CAAC,gBAAgB,CA+C1B,CAAC"}
1
+ {"version":3,"file":"OpenQuestionsGate.d.ts","sourceRoot":"","sources":["../../../../../src/services/docs/review/gates/OpenQuestionsGate.ts"],"names":[],"mappings":"AACA,OAAO,EAAqB,uBAAuB,EAAE,MAAM,2BAA2B,CAAC;AACvF,OAAO,EAAE,gBAAgB,EAA+B,MAAM,mBAAmB,CAAC;AAElF,MAAM,WAAW,sBAAsB;IACrC,SAAS,EAAE,uBAAuB,CAAC;CACpC;AAmMD,eAAO,MAAM,oBAAoB,GAC/B,OAAO,sBAAsB,KAC5B,OAAO,CAAC,gBAAgB,CA+C1B,CAAC"}
@@ -32,7 +32,9 @@ const IMPLICIT_PATTERNS = [
32
32
  const isFenceLine = (line) => /^```|^~~~/.test(line.trim());
33
33
  const isExampleHeading = (heading) => /example|sample/i.test(heading);
34
34
  const isOpenQuestionsHeading = (heading) => /open (questions?|issues?|items?)|unresolved questions?/i.test(heading);
35
- const RESOLVED_HINTS = [/^\s*resolved[:\]]/i, /^\s*decision:/i, /\[resolved\]/i];
35
+ const MANAGED_PREFLIGHT_START = "<!-- mcoda:sds-preflight:start -->";
36
+ const MANAGED_PREFLIGHT_END = "<!-- mcoda:sds-preflight:end -->";
37
+ const RESOLVED_HINTS = [/^[-*+\d.)\s]*resolved[:\]]/i, /^[-*+\d.)\s]*decision:/i, /\[resolved\]/i];
36
38
  const isResolvedLine = (line) => RESOLVED_HINTS.some((pattern) => pattern.test(line.trim()));
37
39
  const normalizeQuestion = (text) => text
38
40
  .toLowerCase()
@@ -105,6 +107,7 @@ const extractQuestions = async (record) => {
105
107
  const lines = content.split(/\r?\n/);
106
108
  const questions = [];
107
109
  let inFence = false;
110
+ let inManagedPreflight = false;
108
111
  let allowSection = false;
109
112
  let inOpenSection = false;
110
113
  let currentHeading;
@@ -113,6 +116,14 @@ const extractQuestions = async (record) => {
113
116
  const trimmed = line.trim();
114
117
  if (!trimmed)
115
118
  continue;
119
+ if (trimmed === MANAGED_PREFLIGHT_START) {
120
+ inManagedPreflight = true;
121
+ continue;
122
+ }
123
+ if (trimmed === MANAGED_PREFLIGHT_END) {
124
+ inManagedPreflight = false;
125
+ continue;
126
+ }
116
127
  if (isFenceLine(trimmed)) {
117
128
  inFence = !inFence;
118
129
  continue;
@@ -124,7 +135,7 @@ const extractQuestions = async (record) => {
124
135
  inOpenSection = currentHeading ? isOpenQuestionsHeading(currentHeading) : false;
125
136
  continue;
126
137
  }
127
- if (inFence || allowSection)
138
+ if (inFence || inManagedPreflight || allowSection)
128
139
  continue;
129
140
  if (isResolvedLine(trimmed))
130
141
  continue;
@@ -1 +1 @@
1
- {"version":3,"file":"SdsNoUnresolvedItemsGate.d.ts","sourceRoot":"","sources":["../../../../../src/services/docs/review/gates/SdsNoUnresolvedItemsGate.ts"],"names":[],"mappings":"AACA,OAAO,EAAqB,uBAAuB,EAAE,MAAM,2BAA2B,CAAC;AACvF,OAAO,EAAE,gBAAgB,EAAe,MAAM,mBAAmB,CAAC;AAElE,MAAM,WAAW,6BAA6B;IAC5C,SAAS,EAAE,uBAAuB,CAAC;CACpC;AA4CD,eAAO,MAAM,2BAA2B,GACtC,OAAO,6BAA6B,KACnC,OAAO,CAAC,gBAAgB,CAsF1B,CAAC"}
1
+ {"version":3,"file":"SdsNoUnresolvedItemsGate.d.ts","sourceRoot":"","sources":["../../../../../src/services/docs/review/gates/SdsNoUnresolvedItemsGate.ts"],"names":[],"mappings":"AACA,OAAO,EAAqB,uBAAuB,EAAE,MAAM,2BAA2B,CAAC;AACvF,OAAO,EAAE,gBAAgB,EAAe,MAAM,mBAAmB,CAAC;AAElE,MAAM,WAAW,6BAA6B;IAC5C,SAAS,EAAE,uBAAuB,CAAC;CACpC;AA8CD,eAAO,MAAM,2BAA2B,GACtC,OAAO,6BAA6B,KACnC,OAAO,CAAC,gBAAgB,CAgG1B,CAAC"}
@@ -11,6 +11,8 @@ const UNRESOLVED_PATTERNS = [
11
11
  const OPEN_QUESTIONS_HEADING = /open questions?/i;
12
12
  const RESOLVED_LINE = /^[-*+\d.)\s]*resolved:/i;
13
13
  const NO_OPEN_ITEMS_LINE = /no unresolved questions remain|no open questions remain/i;
14
+ const MANAGED_PREFLIGHT_START = "<!-- mcoda:sds-preflight:start -->";
15
+ const MANAGED_PREFLIGHT_END = "<!-- mcoda:sds-preflight:end -->";
14
16
  const isFenceLine = (line) => /^```|^~~~/.test(line.trim());
15
17
  const buildIssue = (input) => ({
16
18
  id: input.id,
@@ -46,17 +48,26 @@ export const runSdsNoUnresolvedItemsGate = async (input) => {
46
48
  const content = await fs.readFile(sds.path, "utf8");
47
49
  const lines = content.split(/\r?\n/);
48
50
  let inFence = false;
51
+ let inManagedPreflight = false;
49
52
  let inOpenQuestions = false;
50
53
  for (let i = 0; i < lines.length; i += 1) {
51
54
  const line = lines[i] ?? "";
52
55
  const trimmed = line.trim();
53
56
  if (!trimmed)
54
57
  continue;
58
+ if (trimmed === MANAGED_PREFLIGHT_START) {
59
+ inManagedPreflight = true;
60
+ continue;
61
+ }
62
+ if (trimmed === MANAGED_PREFLIGHT_END) {
63
+ inManagedPreflight = false;
64
+ continue;
65
+ }
55
66
  if (isFenceLine(trimmed)) {
56
67
  inFence = !inFence;
57
68
  continue;
58
69
  }
59
- if (inFence)
70
+ if (inFence || inManagedPreflight)
60
71
  continue;
61
72
  const heading = trimmed.match(/^#{1,6}\s+(.*)$/);
62
73
  if (heading) {