@mestreyoda/fabrica 0.2.40 → 0.2.42

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 (3) hide show
  1. package/README.md +21 -0
  2. package/dist/index.js +228 -16
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -203,6 +203,27 @@ tail -f ~/.openclaw/workspace/logs/genesis.log
203
203
  openclaw fabrica metrics
204
204
  ```
205
205
 
206
+ This command now includes convergence-oriented telemetry such as:
207
+ - cause counts (for example `qa_missing_required_gates`, `qa_sanitization_failed`)
208
+ - human escalations
209
+ - average dispatch → first PR timing
210
+ - per-stack breakdowns
211
+
212
+ **11. Inspect a live issue/run**:
213
+
214
+ ```bash
215
+ openclaw fabrica doctor issue --project <slug> --issue <id>
216
+ ```
217
+
218
+ Use this when a project is looping or stuck. It shows:
219
+ - current PR / artifact state
220
+ - progress state
221
+ - convergence cause + QA subcause
222
+ - missing QA gates, when applicable
223
+ - recommended next action
224
+
225
+ For deferred, non-blocking ideas after this milestone, see `FUTURE_IMPROVEMENTS.md`.
226
+
206
227
  ## Configuration
207
228
 
208
229
  ### Minimal (gh CLI only)
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.40") {
113909
- return "0.2.40";
113908
+ if ("0.2.42") {
113909
+ return "0.2.42";
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"}`,
@@ -131407,6 +131518,7 @@ async function captureIssueDoctorSnapshot(opts) {
131407
131518
  trigger: opts.trigger,
131408
131519
  summary: result.recommendation.summary,
131409
131520
  likelyNextAction: result.recommendation.likelyNextAction,
131521
+ stack: result.stack,
131410
131522
  doctor: {
131411
131523
  artifact: result.hasArtifact,
131412
131524
  progressState: result.lifecycle.progressState,
@@ -131416,6 +131528,8 @@ async function captureIssueDoctorSnapshot(opts) {
131416
131528
  prState: result.pr?.state ?? null,
131417
131529
  labels: result.issue?.labels ?? [],
131418
131530
  convergenceCause: result.convergence.cause,
131531
+ qaSubcause: result.convergence.qaSubcause,
131532
+ qaMissingGates: result.convergence.qaMissingGates,
131419
131533
  convergenceAction: result.convergence.action,
131420
131534
  convergenceRetryCount: result.convergence.retryCount,
131421
131535
  convergenceHeadSha: result.convergence.headSha,
@@ -131440,7 +131554,16 @@ init_types3();
131440
131554
  function classifyConvergenceCause(reason) {
131441
131555
  const text = String(reason ?? "").toLowerCase();
131442
131556
  if (!text) return "other";
131443
- 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";
131444
131567
  if (text.includes("merge conflict") || text.includes("pr_still_conflicting")) return "merge_conflict";
131445
131568
  if (text.includes("stalled_with_artifact")) return "stalled_with_artifact";
131446
131569
  if (text.includes("stalled_without_artifact")) return "stalled_without_artifact";
@@ -131462,6 +131585,17 @@ function getConvergenceRetryBudget(cause) {
131462
131585
  switch (cause) {
131463
131586
  case "invalid_qa_evidence":
131464
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;
131465
131599
  case "merge_conflict":
131466
131600
  case "stalled_with_artifact":
131467
131601
  case "stale_pr_target":
@@ -131943,20 +132077,50 @@ async function defaultValidateDeveloperDone(opts) {
131943
132077
  return { ok: true, prStatus };
131944
132078
  } catch (error48) {
131945
132079
  let prStatus;
132080
+ let qaEvidence;
131946
132081
  try {
131947
132082
  const fallbackPr = await opts.provider.getPrStatus(opts.issueId);
131948
132083
  if (fallbackPr.url && fallbackPr.state !== "merged" && fallbackPr.state !== "closed") {
131949
132084
  prStatus = fallbackPr;
132085
+ qaEvidence = await getCanonicalQaEvidenceValidationForPr(opts.provider, opts.issueId).catch(() => void 0);
131950
132086
  }
131951
132087
  } catch {
131952
132088
  }
131953
132089
  return {
131954
132090
  ok: false,
131955
132091
  reason: error48 instanceof Error ? error48.message : "developer_validation_failed",
131956
- prStatus
132092
+ prStatus,
132093
+ qaEvidence
131957
132094
  };
131958
132095
  }
131959
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
+ }
131960
132124
  async function persistDeveloperPrBinding(opts) {
131961
132125
  const prStatus = opts.prStatus;
131962
132126
  if (!prStatus) return;
@@ -132045,11 +132209,22 @@ async function applyWorkerResult(opts) {
132045
132209
  if (!validation.ok) {
132046
132210
  const validationReason = validation.reason ?? "developer_validation_failed";
132047
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;
132048
132222
  const convergenceIssueRuntime = validation.prStatus ? {
132049
132223
  ...context2.issueRuntime,
132050
132224
  currentPrNumber: validation.prStatus.number ?? context2.issueRuntime?.currentPrNumber ?? null,
132051
132225
  currentPrUrl: validation.prStatus.url ?? context2.issueRuntime?.currentPrUrl ?? null,
132052
- currentPrState: validation.prStatus.state ?? context2.issueRuntime?.currentPrState ?? null
132226
+ currentPrState: validation.prStatus.state ?? context2.issueRuntime?.currentPrState ?? null,
132227
+ currentPrHeadSha: context2.issueRuntime?.currentPrHeadSha ?? null
132053
132228
  } : context2.issueRuntime;
132054
132229
  if (validation.prStatus) {
132055
132230
  await persistDeveloperPrBinding({
@@ -132063,7 +132238,7 @@ async function applyWorkerResult(opts) {
132063
132238
  const convergence = decidePostPrConvergence({
132064
132239
  workflow,
132065
132240
  issueRuntime: convergenceIssueRuntime,
132066
- reason: validationReason,
132241
+ reason: convergenceReason,
132067
132242
  feedbackQueueLabel
132068
132243
  });
132069
132244
  const blockedSummary = convergence.action === "escalate_human" ? [
@@ -132082,10 +132257,16 @@ ${validationReason}`;
132082
132257
  issueId: context2.issueId,
132083
132258
  role: context2.parsed.role,
132084
132259
  result: opts.result.value,
132260
+ stack: context2.project.stack ?? context2.project.environment?.stack ?? null,
132085
132261
  reason: validationReason,
132086
132262
  convergenceCause: convergence.cause,
132087
132263
  convergenceAction: convergence.action,
132088
- convergenceRetryCount: convergence.retryCount
132264
+ convergenceRetryCount: convergence.retryCount,
132265
+ qaSubcause,
132266
+ qaMissingGates,
132267
+ qaObservedHeadSha,
132268
+ qaEvidenceFingerprint,
132269
+ qaUnchanged
132089
132270
  }).catch(() => {
132090
132271
  });
132091
132272
  await updateIssueRuntime(opts.workspaceDir, context2.projectSlug, context2.issueId, {
@@ -132094,9 +132275,15 @@ ${validationReason}`;
132094
132275
  lastConvergenceCause: convergence.cause,
132095
132276
  lastConvergenceAction: convergence.action,
132096
132277
  lastConvergenceRetryCount: convergence.retryCount,
132097
- lastConvergenceReason: validationReason,
132278
+ lastConvergenceReason: convergenceReason,
132098
132279
  lastConvergenceAt: (/* @__PURE__ */ new Date()).toISOString(),
132099
- 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
132100
132287
  }).catch(() => {
132101
132288
  });
132102
132289
  if (convergenceIssueRuntime?.currentPrUrl || convergence.action === "escalate_human") {
@@ -132240,7 +132427,13 @@ ${validationReason}`;
132240
132427
  lastConvergenceRetryCount: 0,
132241
132428
  lastConvergenceReason: null,
132242
132429
  lastConvergenceAt: null,
132243
- lastConvergenceHeadSha: null
132430
+ lastConvergenceHeadSha: null,
132431
+ lastQaEvidenceAt: null,
132432
+ lastQaExitCode: null,
132433
+ lastQaMissingGates: null,
132434
+ lastQaEvidenceHash: null,
132435
+ lastQaObservedHeadSha: null,
132436
+ lastQaSubcause: null
132244
132437
  }).catch(() => {
132245
132438
  });
132246
132439
  } else {
@@ -132252,7 +132445,13 @@ ${validationReason}`;
132252
132445
  lastConvergenceRetryCount: 0,
132253
132446
  lastConvergenceReason: null,
132254
132447
  lastConvergenceAt: null,
132255
- lastConvergenceHeadSha: null
132448
+ lastConvergenceHeadSha: null,
132449
+ lastQaEvidenceAt: null,
132450
+ lastQaExitCode: null,
132451
+ lastQaMissingGates: null,
132452
+ lastQaEvidenceHash: null,
132453
+ lastQaObservedHeadSha: null,
132454
+ lastQaSubcause: null
132256
132455
  }).catch(() => {
132257
132456
  });
132258
132457
  }
@@ -149231,8 +149430,21 @@ function keyFor(entry) {
149231
149430
  return `${projectSlug}:${issueId}`;
149232
149431
  }
149233
149432
  function normalizeCause(entry) {
149433
+ const explicitQaSubcause = entry.qaSubcause ?? null;
149434
+ if (explicitQaSubcause) return String(explicitQaSubcause);
149234
149435
  const cause = entry.convergenceCause ?? entry.reason ?? null;
149235
- return cause ? String(cause) : 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);
149236
149448
  }
149237
149449
  async function computeMetrics(workspaceDir) {
149238
149450
  const auditLogPath = join4(workspaceDir, DATA_DIR, "log", "audit.log");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mestreyoda/fabrica",
3
- "version": "0.2.40",
3
+ "version": "0.2.42",
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",