@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.
- package/README.md +21 -0
- package/dist/index.js +228 -16
- 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.
|
|
113909
|
-
return "0.2.
|
|
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 (
|
|
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("
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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.
|
|
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",
|