@mestreyoda/fabrica 0.2.39 → 0.2.41

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 (2) hide show
  1. package/dist/index.js +436 -39
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -113905,8 +113905,8 @@ import fsSync from "node:fs";
113905
113905
  import path5 from "node:path";
113906
113906
  import { fileURLToPath as fileURLToPath3 } from "node:url";
113907
113907
  function getCurrentVersion() {
113908
- if ("0.2.39") {
113909
- return "0.2.39";
113908
+ if ("0.2.41") {
113909
+ return "0.2.41";
113910
113910
  }
113911
113911
  try {
113912
113912
  const pkgPath = path5.join(THIS_DIR, "..", "..", "package.json");
@@ -126576,6 +126576,32 @@ function sanitizePublicOutput(input) {
126576
126576
  }
126577
126577
 
126578
126578
  // lib/tools/tasks/qa-evidence.ts
126579
+ function normalizeQaEvidenceForFingerprint(text) {
126580
+ return text.toLowerCase().replace(/\r/g, "").replace(/exit code:\s*-?\d+/gi, "exit code").replace(/\d+(?:\.\d+)?%/g, "<percent>").replace(/\s+/g, " ").trim();
126581
+ }
126582
+ function computeQaFingerprint(text) {
126583
+ const normalized = normalizeQaEvidenceForFingerprint(text);
126584
+ if (!normalized) return null;
126585
+ let hash2 = 2166136261;
126586
+ for (let i2 = 0; i2 < normalized.length; i2 += 1) {
126587
+ hash2 ^= normalized.charCodeAt(i2);
126588
+ hash2 = Math.imul(hash2, 16777619);
126589
+ }
126590
+ return `fnv1a:${(hash2 >>> 0).toString(16)}`;
126591
+ }
126592
+ function classifyQaEvidenceSubcause(validation) {
126593
+ if (validation.errors.includes("qa_evidence_missing")) return "qa_schema_missing";
126594
+ if (validation.sectionCount > 1) return "qa_section_count_invalid";
126595
+ if (validation.problems.some((problem) => problem.includes("host-system paths") || problem.includes("secrets") || problem.includes("environment dump"))) {
126596
+ return "qa_sanitization_failed";
126597
+ }
126598
+ if (validation.problems.some((problem) => problem.includes("Exit code: <number>"))) return "qa_exit_code_missing";
126599
+ if (validation.exitCode !== null && validation.exitCode !== 0) return "qa_exit_code_nonzero";
126600
+ if (validation.errors.includes("qa_evidence_only_exit_codes")) return "qa_exit_codes_only";
126601
+ if (validation.errors.some((error48) => error48.startsWith("qa_gate_missing_"))) return "qa_missing_required_gates";
126602
+ if (validation.errors.some((error48) => error48.startsWith("qa_coverage_below_threshold_"))) return "qa_coverage_below_threshold";
126603
+ return validation.errors.length || validation.problems.length ? "qa_unknown" : null;
126604
+ }
126579
126605
  function validateQaEvidence(body) {
126580
126606
  const text = body ?? "";
126581
126607
  const headings = [...text.matchAll(/^## QA Evidence\b[\t ]*$/gim)];
@@ -126624,9 +126650,11 @@ function validateQaEvidence(body) {
126624
126650
  tests: ["tests", "pytest", "jest", "vitest", "go test", "test session"],
126625
126651
  coverage: ["coverage"]
126626
126652
  };
126653
+ const missingGates = [];
126627
126654
  for (const [gate, aliases] of Object.entries(GATE_ALIASES)) {
126628
126655
  const found = aliases.some((alias) => new RegExp(alias, "i").test(section));
126629
126656
  if (!found) {
126657
+ missingGates.push(gate);
126630
126658
  errors.push(`qa_gate_missing_${gate}`);
126631
126659
  }
126632
126660
  }
@@ -126643,26 +126671,86 @@ function validateQaEvidence(body) {
126643
126671
  errors.push(`qa_coverage_below_threshold_${Math.floor(cov)}`);
126644
126672
  }
126645
126673
  }
126674
+ const primarySubcause = classifyQaEvidenceSubcause({
126675
+ sectionCount,
126676
+ exitCode,
126677
+ problems,
126678
+ errors
126679
+ });
126646
126680
  return {
126647
126681
  valid: problems.length === 0 && errors.length === 0,
126648
126682
  sectionCount,
126649
126683
  exitCode,
126650
126684
  problems,
126651
- errors
126685
+ errors,
126686
+ missingGates,
126687
+ primarySubcause,
126688
+ fingerprint: computeQaFingerprint(section)
126652
126689
  };
126653
126690
  }
126654
126691
  function validateCanonicalQaEvidence(body) {
126655
126692
  return validateQaEvidence(body);
126656
126693
  }
126694
+ function buildQaRepairGuidance(validation, actor) {
126695
+ const base = actor === "developer" ? [
126696
+ 'Run `scripts/qa.sh` again and replace the existing "## QA Evidence" section with the fresh sanitized output.',
126697
+ "Keep the canonical lint/types/security/tests/coverage gates intact.",
126698
+ "Do not rewrite or weaken `scripts/qa.sh` into ad-hoc scenario checks."
126699
+ ] : [
126700
+ "Reject the PR and ask the developer to rerun `scripts/qa.sh`.",
126701
+ 'Require the PR body to contain one fresh sanitized "## QA Evidence" section.',
126702
+ "Do not accept ad-hoc scenario scripts or weakened QA gates in place of the canonical contract."
126703
+ ];
126704
+ switch (validation.primarySubcause) {
126705
+ case "qa_schema_missing":
126706
+ case "qa_section_count_invalid":
126707
+ return [
126708
+ ...base,
126709
+ "Ensure the PR body contains exactly one `## QA Evidence` section."
126710
+ ];
126711
+ case "qa_exit_code_missing":
126712
+ return [
126713
+ ...base,
126714
+ "The QA Evidence must include an explicit `Exit code: 0` line."
126715
+ ];
126716
+ case "qa_exit_code_nonzero":
126717
+ return [
126718
+ ...base,
126719
+ "The QA command failed. Fix the underlying lint/type/security/test/coverage problem before calling work_finish again."
126720
+ ];
126721
+ case "qa_sanitization_failed":
126722
+ return [
126723
+ ...base,
126724
+ "Sanitize host paths, environment dumps, and secrets before updating the PR body."
126725
+ ];
126726
+ case "qa_missing_required_gates":
126727
+ return [
126728
+ ...base,
126729
+ `Missing gates: ${validation.missingGates.join(", ") || "unknown"}. Include all required gates in the canonical QA output.`
126730
+ ];
126731
+ case "qa_exit_codes_only":
126732
+ return [
126733
+ ...base,
126734
+ "Do not paste exit codes alone. Include the actual lint/types/security/tests/coverage output summary."
126735
+ ];
126736
+ case "qa_coverage_below_threshold":
126737
+ return [
126738
+ ...base,
126739
+ "Coverage is below the required threshold. Fix the underlying tests or implementation before retrying."
126740
+ ];
126741
+ default:
126742
+ return base;
126743
+ }
126744
+ }
126657
126745
  function formatQaEvidenceValidationFailure(validation, actor) {
126658
126746
  const intro = actor === "developer" ? "Cannot mark work_finish(done) with invalid QA Evidence in the PR body." : "Cannot approve review with invalid QA Evidence in the PR body.";
126659
- const guidance = actor === "developer" ? 'Replace the existing "## QA Evidence" section with fresh sanitized output from scripts/qa.sh (exactly one section, Exit code: 0), then call work_finish again. Do not rewrite or weaken scripts/qa.sh into ad-hoc scenario checks \u2014 preserve the canonical lint/types/security/tests/coverage gates and fix the underlying code or project setup instead.' : 'Reject the PR and instruct the developer to replace the existing "## QA Evidence" section in the PR body with fresh sanitized output from scripts/qa.sh (exactly one section, Exit code: 0). Do not accept ad-hoc scenario scripts or weakened QA gates in place of the canonical lint/types/security/tests/coverage contract.';
126660
126747
  const allIssues = [...validation.problems, ...validation.errors];
126748
+ const guidance = buildQaRepairGuidance(validation, actor);
126661
126749
  return `${intro}
126662
126750
 
126663
126751
  ${allIssues.map((issue2) => `- ${issue2}`).join("\n")}
126664
126752
 
126665
- ${guidance}`;
126753
+ ${guidance.map((line) => `- ${line}`).join("\n")}`;
126666
126754
  }
126667
126755
 
126668
126756
  // lib/tools/worker/work-finish.ts
@@ -126968,6 +127056,10 @@ function shouldAutoRecoverToFeedback(summary) {
126968
127056
  const text = summary.toLowerCase();
126969
127057
  return /retarget/.test(text) || /mismatch de escopo/.test(text) || /mismatch de escopo\/rastreabilidade/.test(text) || /new pr/.test(text) || /novo pr/.test(text) || /não pode satisfazer a issue/.test(text) || /cannot satisfy issue/.test(text);
126970
127058
  }
127059
+ async function getCanonicalQaEvidenceValidationForPr(provider, issueId, selector) {
127060
+ const prStatus = await provider.getPrStatus(issueId, selector);
127061
+ return validateCanonicalQaEvidence(prStatus.body);
127062
+ }
126971
127063
  function createWorkFinishTool(ctx) {
126972
127064
  return (toolCtx) => ({
126973
127065
  name: "work_finish",
@@ -130507,6 +130599,8 @@ async function runIssueDoctor(opts) {
130507
130599
  issue2 = null;
130508
130600
  }
130509
130601
  const convergenceCause = issueRuntime?.lastConvergenceCause ?? null;
130602
+ const convergenceQaSubcause = issueRuntime?.lastQaSubcause ?? null;
130603
+ const convergenceQaMissingGates = issueRuntime?.lastQaMissingGates ?? [];
130510
130604
  const convergenceAction = issueRuntime?.lastConvergenceAction ?? null;
130511
130605
  const retryCount = issueRuntime?.lastConvergenceRetryCount ?? 0;
130512
130606
  const convergenceReason = issueRuntime?.lastConvergenceReason ?? issueRuntime?.inconclusiveCompletionReason ?? null;
@@ -130523,7 +130617,18 @@ async function runIssueDoctor(opts) {
130523
130617
  ];
130524
130618
  const likelyNextAction = (() => {
130525
130619
  if (convergenceAction === "escalate_human") return "human_intervention";
130526
- if (convergenceCause === "invalid_qa_evidence") return "repair_qa_evidence";
130620
+ if ([
130621
+ "invalid_qa_evidence",
130622
+ "qa_schema_missing",
130623
+ "qa_section_count_invalid",
130624
+ "qa_exit_code_missing",
130625
+ "qa_exit_code_nonzero",
130626
+ "qa_sanitization_failed",
130627
+ "qa_missing_required_gates",
130628
+ "qa_exit_codes_only",
130629
+ "qa_coverage_below_threshold",
130630
+ "qa_stale_or_unchanged"
130631
+ ].includes(convergenceCause ?? "")) return "repair_qa_evidence";
130527
130632
  if (convergenceCause === "merge_conflict") return "repair_merge_conflict";
130528
130633
  if (convergenceCause === "stalled_with_artifact") return "force_convergence_review";
130529
130634
  if (hasArtifact) return "post_pr_convergence";
@@ -130532,6 +130637,7 @@ async function runIssueDoctor(opts) {
130532
130637
  return {
130533
130638
  projectSlug: project.slug,
130534
130639
  projectName: project.name,
130640
+ stack: project.stack ?? project.environment?.stack ?? null,
130535
130641
  issueId: opts.issueId,
130536
130642
  issueRuntime,
130537
130643
  hasArtifact,
@@ -130545,6 +130651,8 @@ async function runIssueDoctor(opts) {
130545
130651
  },
130546
130652
  convergence: {
130547
130653
  cause: convergenceCause,
130654
+ qaSubcause: convergenceQaSubcause,
130655
+ qaMissingGates: convergenceQaMissingGates,
130548
130656
  action: convergenceAction,
130549
130657
  retryCount,
130550
130658
  reason: convergenceReason,
@@ -130575,6 +130683,7 @@ async function runIssueDoctor(opts) {
130575
130683
  function formatIssueDoctor(result) {
130576
130684
  const lines = [
130577
130685
  `Issue run doctor \u2014 ${result.projectSlug}#${result.issueId}`,
130686
+ ` Stack: ${result.stack ?? "unknown"}`,
130578
130687
  ` Artifact: ${result.hasArtifact ? "yes" : "no"}`,
130579
130688
  ` PR: ${result.pr?.url ?? "n/a"} (${result.pr?.state ?? "unknown"})`,
130580
130689
  ` Issue: ${result.issue?.url ?? "n/a"} (${result.issue?.state ?? "unknown"})`,
@@ -130586,6 +130695,8 @@ function formatIssueDoctor(result) {
130586
130695
  ` First worker activity: ${result.lifecycle.firstWorkerActivityAt ?? "n/a"}`,
130587
130696
  ` Session completed: ${result.lifecycle.sessionCompletedAt ?? "n/a"}`,
130588
130697
  ` Convergence cause: ${result.convergence.cause ?? "none"}`,
130698
+ ` QA subcause: ${result.convergence.qaSubcause ?? "n/a"}`,
130699
+ ` Missing QA gates: ${result.convergence.qaMissingGates.length ? result.convergence.qaMissingGates.join(", ") : "n/a"}`,
130589
130700
  ` Convergence action: ${result.convergence.action ?? "none"}`,
130590
130701
  ` Retry count: ${result.convergence.retryCount}`,
130591
130702
  ` Convergence head SHA: ${result.convergence.headSha ?? "n/a"}`,
@@ -131390,12 +131501,69 @@ init_workflow();
131390
131501
  init_context3();
131391
131502
  init_labels();
131392
131503
 
131504
+ // lib/services/doctor-snapshot.ts
131505
+ init_audit();
131506
+ async function captureIssueDoctorSnapshot(opts) {
131507
+ try {
131508
+ const result = await runIssueDoctor({
131509
+ workspacePath: opts.workspaceDir,
131510
+ projectSlug: opts.projectSlug,
131511
+ issueId: opts.issueId,
131512
+ runCommand: opts.runCommand,
131513
+ pluginConfig: opts.pluginConfig
131514
+ });
131515
+ await log(opts.workspaceDir, opts.event, {
131516
+ projectSlug: opts.projectSlug,
131517
+ issueId: opts.issueId,
131518
+ trigger: opts.trigger,
131519
+ summary: result.recommendation.summary,
131520
+ likelyNextAction: result.recommendation.likelyNextAction,
131521
+ stack: result.stack,
131522
+ doctor: {
131523
+ artifact: result.hasArtifact,
131524
+ progressState: result.lifecycle.progressState,
131525
+ dispatchCycleId: result.lifecycle.dispatchCycleId,
131526
+ dispatchRunId: result.lifecycle.dispatchRunId,
131527
+ prUrl: result.pr?.url ?? null,
131528
+ prState: result.pr?.state ?? null,
131529
+ labels: result.issue?.labels ?? [],
131530
+ convergenceCause: result.convergence.cause,
131531
+ qaSubcause: result.convergence.qaSubcause,
131532
+ qaMissingGates: result.convergence.qaMissingGates,
131533
+ convergenceAction: result.convergence.action,
131534
+ convergenceRetryCount: result.convergence.retryCount,
131535
+ convergenceHeadSha: result.convergence.headSha,
131536
+ headShaChangedSinceLastConvergence: result.convergence.headShaChangedSinceLastConvergence
131537
+ },
131538
+ ...opts.extra ?? {}
131539
+ });
131540
+ } catch (error48) {
131541
+ await log(opts.workspaceDir, `${opts.event}_failed`, {
131542
+ projectSlug: opts.projectSlug,
131543
+ issueId: opts.issueId,
131544
+ trigger: opts.trigger,
131545
+ error: error48 instanceof Error ? error48.message : String(error48),
131546
+ ...opts.extra ?? {}
131547
+ }).catch(() => {
131548
+ });
131549
+ }
131550
+ }
131551
+
131393
131552
  // lib/services/post-pr-convergence.ts
131394
131553
  init_types3();
131395
131554
  function classifyConvergenceCause(reason) {
131396
131555
  const text = String(reason ?? "").toLowerCase();
131397
131556
  if (!text) return "other";
131398
- if (text.includes("qa_gate_missing_") || text.includes("invalid qa evidence")) return "invalid_qa_evidence";
131557
+ if (text.includes("qa_evidence_missing")) return "qa_schema_missing";
131558
+ if (text.includes("exactly one `## qa evidence` section") || text.includes("qa_section_count_invalid")) return "qa_section_count_invalid";
131559
+ if (text.includes("exit code: <number>") || text.includes("qa_exit_code_missing")) return "qa_exit_code_missing";
131560
+ if (text.includes("exit code must be 0") || text.includes("qa_exit_code_nonzero")) return "qa_exit_code_nonzero";
131561
+ if (text.includes("host-system paths") || text.includes("secrets or environment values") || text.includes("environment dump") || text.includes("qa_sanitization_failed")) return "qa_sanitization_failed";
131562
+ if (text.includes("qa_evidence_only_exit_codes") || text.includes("qa_exit_codes_only")) return "qa_exit_codes_only";
131563
+ if (text.includes("qa_coverage_below_threshold") || text.includes("coverage below threshold")) return "qa_coverage_below_threshold";
131564
+ if (text.includes("qa_stale_or_unchanged")) return "qa_stale_or_unchanged";
131565
+ if (text.includes("qa_gate_missing_") || text.includes("missing required gates")) return "qa_missing_required_gates";
131566
+ if (text.includes("invalid qa evidence")) return "invalid_qa_evidence";
131399
131567
  if (text.includes("merge conflict") || text.includes("pr_still_conflicting")) return "merge_conflict";
131400
131568
  if (text.includes("stalled_with_artifact")) return "stalled_with_artifact";
131401
131569
  if (text.includes("stalled_without_artifact")) return "stalled_without_artifact";
@@ -131417,6 +131585,17 @@ function getConvergenceRetryBudget(cause) {
131417
131585
  switch (cause) {
131418
131586
  case "invalid_qa_evidence":
131419
131587
  return 2;
131588
+ case "qa_schema_missing":
131589
+ case "qa_section_count_invalid":
131590
+ case "qa_exit_code_missing":
131591
+ case "qa_missing_required_gates":
131592
+ return 2;
131593
+ case "qa_exit_code_nonzero":
131594
+ case "qa_exit_codes_only":
131595
+ case "qa_coverage_below_threshold":
131596
+ case "qa_stale_or_unchanged":
131597
+ case "qa_sanitization_failed":
131598
+ return 1;
131420
131599
  case "merge_conflict":
131421
131600
  case "stalled_with_artifact":
131422
131601
  case "stale_pr_target":
@@ -131898,20 +132077,50 @@ async function defaultValidateDeveloperDone(opts) {
131898
132077
  return { ok: true, prStatus };
131899
132078
  } catch (error48) {
131900
132079
  let prStatus;
132080
+ let qaEvidence;
131901
132081
  try {
131902
132082
  const fallbackPr = await opts.provider.getPrStatus(opts.issueId);
131903
132083
  if (fallbackPr.url && fallbackPr.state !== "merged" && fallbackPr.state !== "closed") {
131904
132084
  prStatus = fallbackPr;
132085
+ qaEvidence = await getCanonicalQaEvidenceValidationForPr(opts.provider, opts.issueId).catch(() => void 0);
131905
132086
  }
131906
132087
  } catch {
131907
132088
  }
131908
132089
  return {
131909
132090
  ok: false,
131910
132091
  reason: error48 instanceof Error ? error48.message : "developer_validation_failed",
131911
- prStatus
132092
+ prStatus,
132093
+ qaEvidence
131912
132094
  };
131913
132095
  }
131914
132096
  }
132097
+ function extractQaMissingGatesFromReason(reason) {
132098
+ return [...String(reason ?? "").matchAll(/qa_gate_missing_([a-z_]+)/gi)].map((match) => match[1] ?? "").filter(Boolean);
132099
+ }
132100
+ function inferQaSubcauseFromReason(reason) {
132101
+ const text = String(reason ?? "").toLowerCase();
132102
+ if (!text) return null;
132103
+ if (text.includes("qa_evidence_missing")) return "qa_schema_missing";
132104
+ if (text.includes("exactly one `## qa evidence` section") || text.includes("qa_section_count_invalid")) return "qa_section_count_invalid";
132105
+ if (text.includes("exit code: <number>") || text.includes("qa_exit_code_missing")) return "qa_exit_code_missing";
132106
+ if (text.includes("exit code must be 0") || text.includes("qa_exit_code_nonzero")) return "qa_exit_code_nonzero";
132107
+ if (text.includes("host-system paths") || text.includes("secrets or environment values") || text.includes("environment dump") || text.includes("qa_sanitization_failed")) return "qa_sanitization_failed";
132108
+ if (text.includes("qa_evidence_only_exit_codes") || text.includes("qa_exit_codes_only")) return "qa_exit_codes_only";
132109
+ if (text.includes("qa_coverage_below_threshold") || text.includes("coverage below threshold")) return "qa_coverage_below_threshold";
132110
+ if (text.includes("qa_gate_missing_")) return "qa_missing_required_gates";
132111
+ if (text.includes("invalid qa evidence")) return "invalid_qa_evidence";
132112
+ return null;
132113
+ }
132114
+ function computeStringFingerprint(text) {
132115
+ const normalized = String(text ?? "").trim().toLowerCase().replace(/\s+/g, " ");
132116
+ if (!normalized) return null;
132117
+ let hash2 = 2166136261;
132118
+ for (let i2 = 0; i2 < normalized.length; i2 += 1) {
132119
+ hash2 ^= normalized.charCodeAt(i2);
132120
+ hash2 = Math.imul(hash2, 16777619);
132121
+ }
132122
+ return `fnv1a:${(hash2 >>> 0).toString(16)}`;
132123
+ }
131915
132124
  async function persistDeveloperPrBinding(opts) {
131916
132125
  const prStatus = opts.prStatus;
131917
132126
  if (!prStatus) return;
@@ -132000,11 +132209,22 @@ async function applyWorkerResult(opts) {
132000
132209
  if (!validation.ok) {
132001
132210
  const validationReason = validation.reason ?? "developer_validation_failed";
132002
132211
  const feedbackQueueLabel = getQueueLabels(workflow, "developer").find((label) => isFeedbackState(workflow, label)) ?? "To Improve";
132212
+ const qaMissingGates = validation.qaEvidence?.missingGates ?? extractQaMissingGatesFromReason(validationReason);
132213
+ const qaSubcause = validation.qaEvidence?.primarySubcause ?? inferQaSubcauseFromReason(validationReason);
132214
+ const qaObservedHeadSha = context2.issueRuntime?.currentPrHeadSha ?? context2.issueRuntime?.lastHeadSha ?? null;
132215
+ const qaEvidenceFingerprint = validation.qaEvidence?.fingerprint ?? computeStringFingerprint([qaSubcause, ...qaMissingGates].join("|"));
132216
+ const qaUnchanged = Boolean(
132217
+ context2.issueRuntime?.lastQaEvidenceHash && qaEvidenceFingerprint && context2.issueRuntime.lastQaEvidenceHash === qaEvidenceFingerprint && context2.issueRuntime.lastQaObservedHeadSha && qaObservedHeadSha && context2.issueRuntime.lastQaObservedHeadSha === qaObservedHeadSha
132218
+ );
132219
+ const convergenceReason = qaUnchanged ? `qa_stale_or_unchanged
132220
+
132221
+ ${validationReason}` : validationReason;
132003
132222
  const convergenceIssueRuntime = validation.prStatus ? {
132004
132223
  ...context2.issueRuntime,
132005
132224
  currentPrNumber: validation.prStatus.number ?? context2.issueRuntime?.currentPrNumber ?? null,
132006
132225
  currentPrUrl: validation.prStatus.url ?? context2.issueRuntime?.currentPrUrl ?? null,
132007
- currentPrState: validation.prStatus.state ?? context2.issueRuntime?.currentPrState ?? null
132226
+ currentPrState: validation.prStatus.state ?? context2.issueRuntime?.currentPrState ?? null,
132227
+ currentPrHeadSha: context2.issueRuntime?.currentPrHeadSha ?? null
132008
132228
  } : context2.issueRuntime;
132009
132229
  if (validation.prStatus) {
132010
132230
  await persistDeveloperPrBinding({
@@ -132018,7 +132238,7 @@ async function applyWorkerResult(opts) {
132018
132238
  const convergence = decidePostPrConvergence({
132019
132239
  workflow,
132020
132240
  issueRuntime: convergenceIssueRuntime,
132021
- reason: validationReason,
132241
+ reason: convergenceReason,
132022
132242
  feedbackQueueLabel
132023
132243
  });
132024
132244
  const blockedSummary = convergence.action === "escalate_human" ? [
@@ -132037,10 +132257,16 @@ ${validationReason}`;
132037
132257
  issueId: context2.issueId,
132038
132258
  role: context2.parsed.role,
132039
132259
  result: opts.result.value,
132260
+ stack: context2.project.stack ?? context2.project.environment?.stack ?? null,
132040
132261
  reason: validationReason,
132041
132262
  convergenceCause: convergence.cause,
132042
132263
  convergenceAction: convergence.action,
132043
- convergenceRetryCount: convergence.retryCount
132264
+ convergenceRetryCount: convergence.retryCount,
132265
+ qaSubcause,
132266
+ qaMissingGates,
132267
+ qaObservedHeadSha,
132268
+ qaEvidenceFingerprint,
132269
+ qaUnchanged
132044
132270
  }).catch(() => {
132045
132271
  });
132046
132272
  await updateIssueRuntime(opts.workspaceDir, context2.projectSlug, context2.issueId, {
@@ -132049,11 +132275,34 @@ ${validationReason}`;
132049
132275
  lastConvergenceCause: convergence.cause,
132050
132276
  lastConvergenceAction: convergence.action,
132051
132277
  lastConvergenceRetryCount: convergence.retryCount,
132052
- lastConvergenceReason: validationReason,
132278
+ lastConvergenceReason: convergenceReason,
132053
132279
  lastConvergenceAt: (/* @__PURE__ */ new Date()).toISOString(),
132054
- lastConvergenceHeadSha: convergence.progressHeadSha
132280
+ lastConvergenceHeadSha: convergence.progressHeadSha,
132281
+ lastQaEvidenceAt: (/* @__PURE__ */ new Date()).toISOString(),
132282
+ lastQaExitCode: validationReason.includes("Exit code must be 0") ? 1 : null,
132283
+ lastQaMissingGates: qaMissingGates,
132284
+ lastQaEvidenceHash: qaEvidenceFingerprint,
132285
+ lastQaObservedHeadSha: qaObservedHeadSha,
132286
+ lastQaSubcause: qaSubcause
132055
132287
  }).catch(() => {
132056
132288
  });
132289
+ if (convergenceIssueRuntime?.currentPrUrl || convergence.action === "escalate_human") {
132290
+ await captureIssueDoctorSnapshot({
132291
+ workspaceDir: opts.workspaceDir,
132292
+ projectSlug: context2.projectSlug,
132293
+ issueId: context2.issueId,
132294
+ runCommand: opts.runCommand,
132295
+ pluginConfig: opts.pluginConfig,
132296
+ event: "doctor_snapshot",
132297
+ trigger: convergence.action === "escalate_human" ? "worker_completion_escalated" : "worker_completion_blocked_with_artifact",
132298
+ extra: {
132299
+ convergenceCause: convergence.cause,
132300
+ convergenceAction: convergence.action,
132301
+ convergenceRetryCount: convergence.retryCount
132302
+ }
132303
+ }).catch(() => {
132304
+ });
132305
+ }
132057
132306
  await executeCompletion({
132058
132307
  workspaceDir: opts.workspaceDir,
132059
132308
  projectSlug: context2.projectSlug,
@@ -132178,7 +132427,13 @@ ${validationReason}`;
132178
132427
  lastConvergenceRetryCount: 0,
132179
132428
  lastConvergenceReason: null,
132180
132429
  lastConvergenceAt: null,
132181
- lastConvergenceHeadSha: null
132430
+ lastConvergenceHeadSha: null,
132431
+ lastQaEvidenceAt: null,
132432
+ lastQaExitCode: null,
132433
+ lastQaMissingGates: null,
132434
+ lastQaEvidenceHash: null,
132435
+ lastQaObservedHeadSha: null,
132436
+ lastQaSubcause: null
132182
132437
  }).catch(() => {
132183
132438
  });
132184
132439
  } else {
@@ -132190,7 +132445,13 @@ ${validationReason}`;
132190
132445
  lastConvergenceRetryCount: 0,
132191
132446
  lastConvergenceReason: null,
132192
132447
  lastConvergenceAt: null,
132193
- lastConvergenceHeadSha: null
132448
+ lastConvergenceHeadSha: null,
132449
+ lastQaEvidenceAt: null,
132450
+ lastQaExitCode: null,
132451
+ lastQaMissingGates: null,
132452
+ lastQaEvidenceHash: null,
132453
+ lastQaObservedHeadSha: null,
132454
+ lastQaSubcause: null
132194
132455
  }).catch(() => {
132195
132456
  });
132196
132457
  }
@@ -132741,6 +133002,22 @@ async function checkWorkerHealth(opts) {
132741
133002
  toLabel: convergence.targetLabel,
132742
133003
  deliveryState
132743
133004
  });
133005
+ if (runCommand) {
133006
+ await captureIssueDoctorSnapshot({
133007
+ workspaceDir,
133008
+ projectSlug,
133009
+ issueId: issueIdNum,
133010
+ runCommand,
133011
+ event: "doctor_snapshot",
133012
+ trigger: convergence.action === "escalate_human" ? "completion_recovery_escalated" : "completion_recovery_requeued",
133013
+ extra: {
133014
+ convergenceCause: convergence.cause,
133015
+ convergenceAction: convergence.action,
133016
+ convergenceRetryCount: convergence.retryCount
133017
+ }
133018
+ }).catch(() => {
133019
+ });
133020
+ }
132744
133021
  }
132745
133022
  }
132746
133023
  fixes.push(fix);
@@ -133109,6 +133386,23 @@ async function checkWorkerHealth(opts) {
133109
133386
  idleMinutes: Math.round(quietMinutes),
133110
133387
  deliveryState
133111
133388
  });
133389
+ if (runCommand) {
133390
+ await captureIssueDoctorSnapshot({
133391
+ workspaceDir,
133392
+ projectSlug,
133393
+ issueId: issueIdNum,
133394
+ runCommand,
133395
+ event: "doctor_snapshot",
133396
+ trigger: convergence.action === "escalate_human" ? "stalled_with_artifact_escalated" : "stalled_with_artifact_requeued",
133397
+ extra: {
133398
+ convergenceCause: convergence.cause,
133399
+ convergenceAction: convergence.action,
133400
+ convergenceRetryCount: convergence.retryCount,
133401
+ idleMinutes: Math.round(quietMinutes)
133402
+ }
133403
+ }).catch(() => {
133404
+ });
133405
+ }
133112
133406
  }
133113
133407
  }
133114
133408
  fixes.push(fix);
@@ -149129,12 +149423,42 @@ async function readAuditLines(filePath) {
149129
149423
  }
149130
149424
  return entries;
149131
149425
  }
149426
+ function keyFor(entry) {
149427
+ const projectSlug = entry.projectSlug ?? entry.project ?? null;
149428
+ const issueId = entry.issueId ?? entry.issue ?? null;
149429
+ if (!projectSlug || issueId == null) return null;
149430
+ return `${projectSlug}:${issueId}`;
149431
+ }
149432
+ function normalizeCause(entry) {
149433
+ const explicitQaSubcause = entry.qaSubcause ?? null;
149434
+ if (explicitQaSubcause) return String(explicitQaSubcause);
149435
+ const cause = entry.convergenceCause ?? entry.reason ?? null;
149436
+ if (!cause) return null;
149437
+ const text = String(cause).toLowerCase();
149438
+ if (text.includes("qa_stale_or_unchanged")) return "qa_stale_or_unchanged";
149439
+ if (text.includes("qa_gate_missing_")) return "qa_missing_required_gates";
149440
+ if (text.includes("qa_evidence_missing")) return "qa_schema_missing";
149441
+ if (text.includes("exactly one `## qa evidence` section") || text.includes("qa_section_count_invalid")) return "qa_section_count_invalid";
149442
+ if (text.includes("exit code must be 0") || text.includes("qa_exit_code_nonzero")) return "qa_exit_code_nonzero";
149443
+ if (text.includes("exit code: <number>") || text.includes("qa_exit_code_missing")) return "qa_exit_code_missing";
149444
+ if (text.includes("qa_evidence_only_exit_codes") || text.includes("qa_exit_codes_only")) return "qa_exit_codes_only";
149445
+ if (text.includes("qa_coverage_below_threshold") || text.includes("coverage below threshold")) return "qa_coverage_below_threshold";
149446
+ if (text.includes("host-system paths") || text.includes("secrets or environment values") || text.includes("environment dump") || text.includes("qa_sanitization_failed")) return "qa_sanitization_failed";
149447
+ return String(cause);
149448
+ }
149132
149449
  async function computeMetrics(workspaceDir) {
149133
149450
  const auditLogPath = join4(workspaceDir, DATA_DIR, "log", "audit.log");
149451
+ const bakEntries3 = await readAuditLines(`${auditLogPath}.3.bak`);
149134
149452
  const bakEntries2 = await readAuditLines(`${auditLogPath}.2.bak`);
149135
149453
  const bakEntries = await readAuditLines(`${auditLogPath}.bak`);
149136
149454
  const currentEntries = await readAuditLines(auditLogPath);
149137
- let entries = [...bakEntries2, ...bakEntries, ...currentEntries];
149455
+ const entries = [...bakEntries3, ...bakEntries2, ...bakEntries, ...currentEntries];
149456
+ const projectsData = await readProjects(workspaceDir).catch(() => ({ projects: {} }));
149457
+ const stackByProject = /* @__PURE__ */ new Map();
149458
+ for (const [slug, project] of Object.entries(projectsData.projects ?? {})) {
149459
+ const stack = project.stack ?? project.environment?.stack ?? null;
149460
+ if (stack) stackByProject.set(slug, String(stack));
149461
+ }
149138
149462
  const entriesScanned = entries.length;
149139
149463
  let dispatches = 0;
149140
149464
  let completionsTotal = 0;
@@ -149144,16 +149468,40 @@ async function computeMetrics(workspaceDir) {
149144
149468
  let completionsOther = 0;
149145
149469
  let conflictsDetected = 0;
149146
149470
  let sessionBudgetResets = 0;
149471
+ let humanEscalations = 0;
149472
+ const causeCounts = {};
149147
149473
  const dispatchTimes = /* @__PURE__ */ new Map();
149474
+ const firstPrTimes = /* @__PURE__ */ new Map();
149148
149475
  const completionDeltas = [];
149476
+ const firstPrDeltas = [];
149477
+ const stackMetrics = /* @__PURE__ */ new Map();
149478
+ function stackBucket(entry) {
149479
+ const slug = entry.projectSlug ?? entry.project ?? null;
149480
+ const stack = entry.stack ?? (slug ? stackByProject.get(String(slug)) : null) ?? "unknown";
149481
+ if (!stackMetrics.has(String(stack))) {
149482
+ stackMetrics.set(String(stack), {
149483
+ issues: /* @__PURE__ */ new Set(),
149484
+ dispatches: 0,
149485
+ escalations: 0,
149486
+ causeCounts: {},
149487
+ completionDeltas: [],
149488
+ firstPrDeltas: []
149489
+ });
149490
+ }
149491
+ return String(stack);
149492
+ }
149149
149493
  for (const entry of entries) {
149494
+ const issueKey = keyFor(entry);
149495
+ const stack = stackBucket(entry);
149496
+ const stackBucketState = stackMetrics.get(stack);
149497
+ if (issueKey) stackBucketState.issues.add(issueKey);
149150
149498
  switch (entry.event) {
149151
- case "dispatch":
149499
+ case "dispatch": {
149152
149500
  dispatches++;
149153
- if (entry.issue != null && entry.project) {
149154
- dispatchTimes.set(`${entry.project}:${entry.issue}`, Date.parse(entry.ts));
149155
- }
149501
+ stackBucketState.dispatches++;
149502
+ if (issueKey) dispatchTimes.set(issueKey, Date.parse(entry.ts));
149156
149503
  break;
149504
+ }
149157
149505
  case "work_finish": {
149158
149506
  completionsTotal++;
149159
149507
  const result = String(entry.result ?? "");
@@ -149161,14 +149509,13 @@ async function computeMetrics(workspaceDir) {
149161
149509
  else if (result === "pass") completionsPass++;
149162
149510
  else if (result === "fail") completionsFail++;
149163
149511
  else completionsOther++;
149164
- if (entry.issue != null && entry.project) {
149165
- const key = `${entry.project}:${entry.issue}`;
149166
- const dispatchTime = dispatchTimes.get(key);
149167
- if (dispatchTime !== void 0) {
149168
- const completionTime = Date.parse(entry.ts);
149169
- if (!isNaN(completionTime) && completionTime > dispatchTime) {
149170
- completionDeltas.push((completionTime - dispatchTime) / 6e4);
149171
- }
149512
+ if (issueKey) {
149513
+ const dispatchTime = dispatchTimes.get(issueKey);
149514
+ const completionTime = Date.parse(entry.ts);
149515
+ if (dispatchTime !== void 0 && !Number.isNaN(completionTime) && completionTime > dispatchTime) {
149516
+ const delta = (completionTime - dispatchTime) / 6e4;
149517
+ completionDeltas.push(delta);
149518
+ stackBucketState.completionDeltas.push(delta);
149172
149519
  }
149173
149520
  }
149174
149521
  break;
@@ -149182,9 +149529,47 @@ async function computeMetrics(workspaceDir) {
149182
149529
  case "session_budget_reset":
149183
149530
  sessionBudgetResets++;
149184
149531
  break;
149532
+ case "pr_discovered_via_polling":
149533
+ case "pr_updated_via_polling": {
149534
+ if (issueKey && !firstPrTimes.has(issueKey)) {
149535
+ const prTime = Date.parse(entry.ts);
149536
+ const dispatchTime = dispatchTimes.get(issueKey);
149537
+ firstPrTimes.set(issueKey, prTime);
149538
+ if (dispatchTime !== void 0 && !Number.isNaN(prTime) && prTime > dispatchTime) {
149539
+ const delta = (prTime - dispatchTime) / 6e4;
149540
+ firstPrDeltas.push(delta);
149541
+ stackBucketState.firstPrDeltas.push(delta);
149542
+ }
149543
+ }
149544
+ break;
149545
+ }
149546
+ case "worker_completion_skipped":
149547
+ case "doctor_snapshot":
149548
+ case "health_fix_applied": {
149549
+ const cause = normalizeCause(entry);
149550
+ if (cause) {
149551
+ causeCounts[cause] = (causeCounts[cause] ?? 0) + 1;
149552
+ stackBucketState.causeCounts[cause] = (stackBucketState.causeCounts[cause] ?? 0) + 1;
149553
+ }
149554
+ if (entry.convergenceAction === "escalate_human" || entry.action === "escalate_human") {
149555
+ humanEscalations++;
149556
+ stackBucketState.escalations++;
149557
+ }
149558
+ break;
149559
+ }
149185
149560
  }
149186
149561
  }
149187
- const avgDispatchToCompletionMinutes = completionDeltas.length > 0 ? completionDeltas.reduce((a, b) => a + b, 0) / completionDeltas.length : null;
149562
+ const avg = (values) => values.length ? values.reduce((a, b) => a + b, 0) / values.length : null;
149563
+ const stackMetricsObject = Object.fromEntries(
149564
+ [...stackMetrics.entries()].map(([stack, data]) => [stack, {
149565
+ issues: data.issues.size,
149566
+ dispatches: data.dispatches,
149567
+ escalations: data.escalations,
149568
+ causeCounts: data.causeCounts,
149569
+ avgDispatchToCompletionMinutes: avg(data.completionDeltas),
149570
+ avgDispatchToFirstPrMinutes: avg(data.firstPrDeltas)
149571
+ }])
149572
+ );
149188
149573
  return {
149189
149574
  entriesScanned,
149190
149575
  dispatches,
@@ -149195,28 +149580,40 @@ async function computeMetrics(workspaceDir) {
149195
149580
  fail: completionsFail,
149196
149581
  other: completionsOther
149197
149582
  },
149198
- avgDispatchToCompletionMinutes,
149583
+ avgDispatchToCompletionMinutes: avg(completionDeltas),
149584
+ avgDispatchToFirstPrMinutes: avg(firstPrDeltas),
149199
149585
  conflictsDetected,
149200
149586
  sessionBudgetResets,
149587
+ humanEscalations,
149588
+ causeCounts,
149589
+ stackMetrics: stackMetricsObject,
149201
149590
  auditLogPath
149202
149591
  };
149203
149592
  }
149204
149593
  function formatMetrics(metrics2) {
149205
149594
  const lines = [
149206
- `Fabrica \u2014 Metricas (${metrics2.entriesScanned} entradas do audit.log)`,
149595
+ `Fabrica \u2014 M\xE9tricas (${metrics2.entriesScanned} entradas do audit.log)`,
149207
149596
  ` Dispatches: ${metrics2.dispatches}`
149208
149597
  ];
149209
149598
  const c = metrics2.completions;
149210
- lines.push(
149211
- ` Conclusoes: ${c.total} (done: ${c.done}, pass: ${c.pass}, fail: ${c.fail}${c.other > 0 ? `, other: ${c.other}` : ""})`
149212
- );
149213
- if (metrics2.avgDispatchToCompletionMinutes !== null) {
149214
- lines.push(` Tempo medio dispatch \u2192 completion: ${metrics2.avgDispatchToCompletionMinutes.toFixed(1)} min`);
149215
- } else {
149216
- lines.push(` Tempo medio dispatch \u2192 completion: n/a`);
149217
- }
149599
+ lines.push(` Conclus\xF5es: ${c.total} (done: ${c.done}, pass: ${c.pass}, fail: ${c.fail}${c.other > 0 ? `, other: ${c.other}` : ""})`);
149600
+ lines.push(` Tempo m\xE9dio dispatch \u2192 completion: ${metrics2.avgDispatchToCompletionMinutes?.toFixed(1) ?? "n/a"} min`);
149601
+ lines.push(` Tempo m\xE9dio dispatch \u2192 primeira PR: ${metrics2.avgDispatchToFirstPrMinutes?.toFixed(1) ?? "n/a"} min`);
149218
149602
  lines.push(` Conflitos detectados: ${metrics2.conflictsDetected}`);
149219
149603
  lines.push(` Session budget resets: ${metrics2.sessionBudgetResets}`);
149604
+ lines.push(` Escalonamentos humanos: ${metrics2.humanEscalations}`);
149605
+ if (Object.keys(metrics2.causeCounts).length > 0) {
149606
+ lines.push(" Causas tipadas:");
149607
+ for (const [cause, count] of Object.entries(metrics2.causeCounts).sort((a, b) => b[1] - a[1])) {
149608
+ lines.push(` - ${cause}: ${count}`);
149609
+ }
149610
+ }
149611
+ if (Object.keys(metrics2.stackMetrics).length > 0) {
149612
+ lines.push(" Por stack:");
149613
+ for (const [stack, data] of Object.entries(metrics2.stackMetrics)) {
149614
+ lines.push(` - ${stack}: issues=${data.issues}, dispatches=${data.dispatches}, escalations=${data.escalations}, avgPR=${data.avgDispatchToFirstPrMinutes?.toFixed(1) ?? "n/a"}m, avgDone=${data.avgDispatchToCompletionMinutes?.toFixed(1) ?? "n/a"}m`);
149615
+ }
149616
+ }
149220
149617
  return lines.join("\n");
149221
149618
  }
149222
149619
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mestreyoda/fabrica",
3
- "version": "0.2.39",
3
+ "version": "0.2.41",
4
4
  "description": "Autonomous software engineering pipeline for OpenClaw. Turns ideas into deployed code via intake, dispatch, review, test, and merge.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",