@mestreyoda/fabrica 0.2.34 → 0.2.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +182 -31
  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.34") {
113909
- return "0.2.34";
113908
+ if ("0.2.36") {
113909
+ return "0.2.36";
113910
113910
  }
113911
113911
  try {
113912
113912
  const pkgPath = path5.join(THIS_DIR, "..", "..", "package.json");
@@ -127254,12 +127254,17 @@ function createWorkFinishTool(ctx) {
127254
127254
  if (role === "tester" && issueRuntime?.infraFailCount) {
127255
127255
  await updateIssueRuntime(workspaceDir, project.slug, issueId, { infraFailCount: 0 });
127256
127256
  }
127257
- if (issueRuntime && (issueRuntime.dispatchAttemptCount || issueRuntime.lastFailureReason || issueRuntime.lastDiagnosticResult || issueRuntime.lastDispatchedLevel)) {
127257
+ if (issueRuntime && (issueRuntime.dispatchAttemptCount || issueRuntime.lastFailureReason || issueRuntime.lastDiagnosticResult || issueRuntime.lastDispatchedLevel || issueRuntime.lastConvergenceCause || issueRuntime.lastConvergenceAction || issueRuntime.lastConvergenceRetryCount || issueRuntime.lastConvergenceReason || issueRuntime.lastConvergenceAt)) {
127258
127258
  await updateIssueRuntime(workspaceDir, project.slug, issueId, {
127259
127259
  dispatchAttemptCount: 0,
127260
127260
  lastFailureReason: null,
127261
127261
  lastDiagnosticResult: null,
127262
- lastDispatchedLevel: null
127262
+ lastDispatchedLevel: null,
127263
+ lastConvergenceCause: null,
127264
+ lastConvergenceAction: null,
127265
+ lastConvergenceRetryCount: 0,
127266
+ lastConvergenceReason: null,
127267
+ lastConvergenceAt: null
127263
127268
  });
127264
127269
  }
127265
127270
  return jsonResult({
@@ -131234,6 +131239,74 @@ init_workflow();
131234
131239
  init_context3();
131235
131240
  init_labels();
131236
131241
 
131242
+ // lib/services/post-pr-convergence.ts
131243
+ init_types3();
131244
+ function classifyConvergenceCause(reason) {
131245
+ const text = String(reason ?? "").toLowerCase();
131246
+ if (!text) return "other";
131247
+ if (text.includes("qa_gate_missing_") || text.includes("invalid qa evidence")) return "invalid_qa_evidence";
131248
+ if (text.includes("merge conflict") || text.includes("pr_still_conflicting")) return "merge_conflict";
131249
+ if (text.includes("stalled_with_artifact")) return "stalled_with_artifact";
131250
+ if (text.includes("stalled_without_artifact")) return "stalled_without_artifact";
131251
+ if (text.includes("invalid_execution_path")) return "invalid_execution_path";
131252
+ if (text.includes("missing_result_line")) return "missing_result_line";
131253
+ if (text.includes("missing_pr")) return "missing_pr";
131254
+ if (text.includes("no longer targets issue") || text.includes("stale_pr_target")) return "stale_pr_target";
131255
+ if (text.includes("new_pr_required")) return "new_pr_required";
131256
+ if (text.includes("changes requested") || text.includes("review_feedback")) return "review_feedback";
131257
+ if (text.includes("developer_validation_failed")) return "developer_validation_failed";
131258
+ return "other";
131259
+ }
131260
+ function hasReviewableArtifact(issueRuntime) {
131261
+ return Boolean(
131262
+ issueRuntime?.currentPrUrl || issueRuntime?.currentPrNumber || issueRuntime?.artifactOfRecord?.prNumber
131263
+ );
131264
+ }
131265
+ function getConvergenceRetryBudget(cause) {
131266
+ switch (cause) {
131267
+ case "invalid_qa_evidence":
131268
+ return 2;
131269
+ case "merge_conflict":
131270
+ case "stalled_with_artifact":
131271
+ case "stale_pr_target":
131272
+ case "new_pr_required":
131273
+ return 1;
131274
+ case "invalid_execution_path":
131275
+ case "missing_result_line":
131276
+ case "stalled_without_artifact":
131277
+ case "missing_pr":
131278
+ case "review_feedback":
131279
+ case "developer_validation_failed":
131280
+ case "other":
131281
+ default:
131282
+ return 2;
131283
+ }
131284
+ }
131285
+ function getPreferredHoldLabel(workflow) {
131286
+ const holds = Object.values(workflow.states).filter((state) => state.type === StateType.HOLD);
131287
+ if (holds.length === 0) return null;
131288
+ return holds.find((state) => state.label === "Refining")?.label ?? holds[0]?.label ?? null;
131289
+ }
131290
+ function decidePostPrConvergence(params) {
131291
+ const { workflow, issueRuntime, reason, feedbackQueueLabel } = params;
131292
+ const cause = classifyConvergenceCause(reason);
131293
+ const hasArtifact = hasReviewableArtifact(issueRuntime);
131294
+ const previousCause = issueRuntime?.lastConvergenceCause ?? null;
131295
+ const previousCount = issueRuntime?.lastConvergenceRetryCount ?? 0;
131296
+ const retryCount = previousCause === cause ? previousCount + 1 : 1;
131297
+ const maxRetries = getConvergenceRetryBudget(cause);
131298
+ const holdLabel = getPreferredHoldLabel(workflow);
131299
+ const shouldEscalate = hasArtifact && retryCount > maxRetries && Boolean(holdLabel);
131300
+ return {
131301
+ cause,
131302
+ action: shouldEscalate ? "escalate_human" : "retry_feedback",
131303
+ targetLabel: shouldEscalate ? holdLabel ?? feedbackQueueLabel : feedbackQueueLabel,
131304
+ retryCount,
131305
+ maxRetries,
131306
+ hasArtifact
131307
+ };
131308
+ }
131309
+
131237
131310
  // lib/services/worker-completion.ts
131238
131311
  init_audit();
131239
131312
  import fs25 from "node:fs/promises";
@@ -131669,9 +131742,18 @@ async function defaultValidateDeveloperDone(opts) {
131669
131742
  );
131670
131743
  return { ok: true, prStatus };
131671
131744
  } catch (error48) {
131745
+ let prStatus;
131746
+ try {
131747
+ const fallbackPr = await opts.provider.getPrStatus(opts.issueId);
131748
+ if (fallbackPr.url && fallbackPr.state !== "merged" && fallbackPr.state !== "closed") {
131749
+ prStatus = fallbackPr;
131750
+ }
131751
+ } catch {
131752
+ }
131672
131753
  return {
131673
131754
  ok: false,
131674
- reason: error48 instanceof Error ? error48.message : "developer_validation_failed"
131755
+ reason: error48 instanceof Error ? error48.message : "developer_validation_failed",
131756
+ prStatus
131675
131757
  };
131676
131758
  }
131677
131759
  }
@@ -131763,18 +131845,57 @@ async function applyWorkerResult(opts) {
131763
131845
  if (!validation.ok) {
131764
131846
  const validationReason = validation.reason ?? "developer_validation_failed";
131765
131847
  const feedbackQueueLabel = getQueueLabels(workflow, "developer").find((label) => isFeedbackState(workflow, label)) ?? "To Improve";
131848
+ const convergenceIssueRuntime = validation.prStatus ? {
131849
+ ...context2.issueRuntime,
131850
+ currentPrNumber: validation.prStatus.number ?? context2.issueRuntime?.currentPrNumber ?? null,
131851
+ currentPrUrl: validation.prStatus.url ?? context2.issueRuntime?.currentPrUrl ?? null,
131852
+ currentPrState: validation.prStatus.state ?? context2.issueRuntime?.currentPrState ?? null
131853
+ } : context2.issueRuntime;
131854
+ if (validation.prStatus) {
131855
+ await persistDeveloperPrBinding({
131856
+ workspaceDir: opts.workspaceDir,
131857
+ projectSlug: context2.projectSlug,
131858
+ issueId: context2.issueId,
131859
+ prStatus: validation.prStatus
131860
+ }).catch(() => {
131861
+ });
131862
+ }
131863
+ const convergence = decidePostPrConvergence({
131864
+ workflow,
131865
+ issueRuntime: convergenceIssueRuntime,
131866
+ reason: validationReason,
131867
+ feedbackQueueLabel
131868
+ });
131869
+ const blockedSummary = convergence.action === "escalate_human" ? [
131870
+ "Automatic recovery escalated for human decision.",
131871
+ "",
131872
+ `Dominant cause: ${convergence.cause}`,
131873
+ `Retry budget exceeded: ${convergence.retryCount}/${convergence.maxRetries}`,
131874
+ "",
131875
+ validationReason
131876
+ ].join("\n") : `Automatic recovery: developer reported DONE but completion validation failed.
131877
+
131878
+ ${validationReason}`;
131766
131879
  await log(opts.workspaceDir, "worker_completion_skipped", {
131767
131880
  sessionKey: context2.project.workers[context2.parsed.role]?.levels?.[context2.slotLevel]?.[context2.slotIndex]?.sessionKey ?? null,
131768
131881
  projectSlug: context2.projectSlug,
131769
131882
  issueId: context2.issueId,
131770
131883
  role: context2.parsed.role,
131771
131884
  result: opts.result.value,
131772
- reason: validationReason
131885
+ reason: validationReason,
131886
+ convergenceCause: convergence.cause,
131887
+ convergenceAction: convergence.action,
131888
+ convergenceRetryCount: convergence.retryCount
131773
131889
  }).catch(() => {
131774
131890
  });
131775
131891
  await updateIssueRuntime(opts.workspaceDir, context2.projectSlug, context2.issueId, {
131776
131892
  inconclusiveCompletionAt: (/* @__PURE__ */ new Date()).toISOString(),
131777
- inconclusiveCompletionReason: validationReason
131893
+ inconclusiveCompletionReason: validationReason,
131894
+ lastConvergenceCause: convergence.cause,
131895
+ lastConvergenceAction: convergence.action,
131896
+ lastConvergenceRetryCount: convergence.retryCount,
131897
+ lastConvergenceReason: validationReason,
131898
+ lastConvergenceAt: (/* @__PURE__ */ new Date()).toISOString()
131778
131899
  }).catch(() => {
131779
131900
  });
131780
131901
  await executeCompletion({
@@ -131783,9 +131904,7 @@ async function applyWorkerResult(opts) {
131783
131904
  role: context2.parsed.role,
131784
131905
  result: "blocked",
131785
131906
  issueId: context2.issueId,
131786
- summary: `Automatic recovery: developer reported DONE but completion validation failed.
131787
-
131788
- ${validationReason}`,
131907
+ summary: blockedSummary,
131789
131908
  provider,
131790
131909
  repoPath,
131791
131910
  projectName: context2.project.name,
@@ -131794,7 +131913,7 @@ ${validationReason}`,
131794
131913
  workflow,
131795
131914
  level: context2.slotLevel,
131796
131915
  slotIndex: context2.slotIndex,
131797
- overrideToLabel: feedbackQueueLabel,
131916
+ overrideToLabel: convergence.targetLabel,
131798
131917
  overrideReason: validationReason,
131799
131918
  runCommand: opts.runCommand
131800
131919
  });
@@ -131897,13 +132016,23 @@ ${validationReason}`,
131897
132016
  await updateIssueRuntime(opts.workspaceDir, context2.projectSlug, context2.issueId, {
131898
132017
  infraFailCount: 0,
131899
132018
  inconclusiveCompletionAt: null,
131900
- inconclusiveCompletionReason: null
132019
+ inconclusiveCompletionReason: null,
132020
+ lastConvergenceCause: null,
132021
+ lastConvergenceAction: null,
132022
+ lastConvergenceRetryCount: 0,
132023
+ lastConvergenceReason: null,
132024
+ lastConvergenceAt: null
131901
132025
  }).catch(() => {
131902
132026
  });
131903
132027
  } else {
131904
132028
  await updateIssueRuntime(opts.workspaceDir, context2.projectSlug, context2.issueId, {
131905
132029
  inconclusiveCompletionAt: null,
131906
- inconclusiveCompletionReason: null
132030
+ inconclusiveCompletionReason: null,
132031
+ lastConvergenceCause: null,
132032
+ lastConvergenceAction: null,
132033
+ lastConvergenceRetryCount: 0,
132034
+ lastConvergenceReason: null,
132035
+ lastConvergenceAt: null
131907
132036
  }).catch(() => {
131908
132037
  });
131909
132038
  }
@@ -132365,6 +132494,12 @@ async function checkWorkerHealth(opts) {
132365
132494
  }).catch(() => {
132366
132495
  });
132367
132496
  }
132497
+ const convergence = decidePostPrConvergence({
132498
+ workflow,
132499
+ issueRuntime,
132500
+ reason: inconclusiveReason,
132501
+ feedbackQueueLabel: slotQueueLabel
132502
+ });
132368
132503
  const fix = {
132369
132504
  issue: {
132370
132505
  type: executionContractRecovery ? "execution_contract_recovery_exhausted" : "completion_recovery_exhausted",
@@ -132392,8 +132527,8 @@ async function checkWorkerHealth(opts) {
132392
132527
  issueUrl: issue2.web_url,
132393
132528
  issueTitle: issue2.title,
132394
132529
  role,
132395
- detail: executionContractRecovery ? "Execution contract violation did not recover with a canonical completion result" : "No canonical completion result was produced after observable activity",
132396
- nextState: slotQueueLabel,
132530
+ detail: convergence.action === "escalate_human" ? `Repeated post-PR recovery cause ${convergence.cause} exceeded retry budget (${convergence.retryCount}/${convergence.maxRetries}). Escalating for human decision.` : executionContractRecovery ? "Execution contract violation did not recover with a canonical completion result" : "No canonical completion result was produced after observable activity",
132531
+ nextState: convergence.targetLabel,
132397
132532
  dispatchCycleId: slot.dispatchCycleId ?? issueRuntime?.lastDispatchCycleId ?? null,
132398
132533
  dispatchRunId: slot.dispatchRunId ?? issueRuntime?.dispatchRunId ?? null
132399
132534
  },
@@ -132410,12 +132545,17 @@ async function checkWorkerHealth(opts) {
132410
132545
  }
132411
132546
  ).catch(() => {
132412
132547
  });
132413
- await revertLabel(fix, expectedLabel, slotQueueLabel);
132548
+ await revertLabel(fix, expectedLabel, convergence.targetLabel);
132414
132549
  if (!fix.labelRevertFailed) {
132415
132550
  await deactivateSlot();
132416
132551
  await updateIssueRuntime(workspaceDir, projectSlug, issueIdNum, {
132417
132552
  inconclusiveCompletionAt: null,
132418
- inconclusiveCompletionReason: null
132553
+ inconclusiveCompletionReason: null,
132554
+ lastConvergenceCause: convergence.cause,
132555
+ lastConvergenceAction: convergence.action,
132556
+ lastConvergenceRetryCount: convergence.retryCount,
132557
+ lastConvergenceReason: inconclusiveReason,
132558
+ lastConvergenceAt: (/* @__PURE__ */ new Date()).toISOString()
132419
132559
  }).catch(() => {
132420
132560
  });
132421
132561
  fix.fixed = true;
@@ -132430,16 +132570,16 @@ async function checkWorkerHealth(opts) {
132430
132570
  slotIndex,
132431
132571
  reason: inconclusiveReason,
132432
132572
  fromLabel: expectedLabel,
132433
- toLabel: slotQueueLabel,
132573
+ toLabel: convergence.targetLabel,
132434
132574
  dispatchCycleId: slot.dispatchCycleId ?? issueRuntime?.lastDispatchCycleId ?? null,
132435
132575
  dispatchRunId: slot.dispatchRunId ?? issueRuntime?.dispatchRunId ?? null
132436
132576
  }).catch(() => {
132437
132577
  });
132438
132578
  }
132439
132579
  await auditHealthFixApplied(workspaceDir, fix, {
132440
- action: "requeue_issue",
132580
+ action: convergence.action === "escalate_human" ? "escalate_human" : "requeue_issue",
132441
132581
  fromLabel: expectedLabel,
132442
- toLabel: slotQueueLabel,
132582
+ toLabel: convergence.targetLabel,
132443
132583
  deliveryState
132444
132584
  });
132445
132585
  }
@@ -132619,7 +132759,7 @@ async function checkWorkerHealth(opts) {
132619
132759
  const minutesActive = (Date.now() - startedAtMs) / 6e4;
132620
132760
  const hours = minutesActive / 60;
132621
132761
  let reviewablePrStatus = null;
132622
- let hasReviewableArtifact = Boolean(
132762
+ let hasReviewableArtifact2 = Boolean(
132623
132763
  issueRuntime?.currentPrNumber || issueRuntime?.currentPrUrl || issueRuntime?.artifactOfRecord?.prNumber
132624
132764
  );
132625
132765
  if (issueIdNum) {
@@ -132627,12 +132767,12 @@ async function checkWorkerHealth(opts) {
132627
132767
  const prStatus = await provider.getPrStatus(issueIdNum);
132628
132768
  if (prStatus.url && prStatus.state !== PrState.MERGED && prStatus.state !== PrState.CLOSED && prStatus.currentIssueMatch !== false) {
132629
132769
  reviewablePrStatus = prStatus;
132630
- hasReviewableArtifact = true;
132770
+ hasReviewableArtifact2 = true;
132631
132771
  }
132632
132772
  } catch {
132633
132773
  }
132634
132774
  }
132635
- if (role === "developer" && issue2 && issueIdNum && !hasReviewableArtifact && minutesActive >= Math.max(5, Math.floor(stallTimeoutMinutes / 2)) && !issueRuntime?.progressNotifiedAt) {
132775
+ if (role === "developer" && issue2 && issueIdNum && !hasReviewableArtifact2 && minutesActive >= Math.max(5, Math.floor(stallTimeoutMinutes / 2)) && !issueRuntime?.progressNotifiedAt) {
132636
132776
  const channel = project.channels?.[0];
132637
132777
  await notify(
132638
132778
  {
@@ -132667,7 +132807,7 @@ async function checkWorkerHealth(opts) {
132667
132807
  }).catch(() => {
132668
132808
  });
132669
132809
  }
132670
- if (role === "developer" && issue2 && issueIdNum && !hasReviewableArtifact && minutesActive >= stallTimeoutMinutes) {
132810
+ if (role === "developer" && issue2 && issueIdNum && !hasReviewableArtifact2 && minutesActive >= stallTimeoutMinutes) {
132671
132811
  const fix = {
132672
132812
  issue: {
132673
132813
  type: "completion_recovery_exhausted",
@@ -132737,7 +132877,13 @@ async function checkWorkerHealth(opts) {
132737
132877
  if (!referenceAt || Number.isNaN(referenceAt)) return minutesActive;
132738
132878
  return (Date.now() - referenceAt) / 6e4;
132739
132879
  })();
132740
- if (role === "developer" && issue2 && issueIdNum && hasReviewableArtifact && reviewablePrStatus?.url && minutesActive >= stallTimeoutMinutes && quietMinutes >= Math.max(8, Math.floor(stallTimeoutMinutes / 2))) {
132880
+ if (role === "developer" && issue2 && issueIdNum && hasReviewableArtifact2 && reviewablePrStatus?.url && minutesActive >= stallTimeoutMinutes && quietMinutes >= Math.max(8, Math.floor(stallTimeoutMinutes / 2))) {
132881
+ const convergence = decidePostPrConvergence({
132882
+ workflow,
132883
+ issueRuntime,
132884
+ reason: "stalled_with_artifact",
132885
+ feedbackQueueLabel: slotQueueLabel
132886
+ });
132741
132887
  const fix = {
132742
132888
  issue: {
132743
132889
  type: "stalled_with_artifact",
@@ -132763,8 +132909,8 @@ async function checkWorkerHealth(opts) {
132763
132909
  issueUrl: issue2.web_url,
132764
132910
  issueTitle: issue2.title,
132765
132911
  role,
132766
- detail: `Open PR ${reviewablePrStatus.url} has stalled for ${Math.round(quietMinutes)} minutes without a trustworthy completion. Re-queueing to ${slotQueueLabel}.`,
132767
- nextState: slotQueueLabel,
132912
+ detail: convergence.action === "escalate_human" ? `Open PR ${reviewablePrStatus.url} exceeded the retry budget for ${convergence.cause} (${convergence.retryCount}/${convergence.maxRetries}). Escalating for human decision.` : `Open PR ${reviewablePrStatus.url} has stalled for ${Math.round(quietMinutes)} minutes without a trustworthy completion. Re-queueing to ${slotQueueLabel}.`,
132913
+ nextState: convergence.targetLabel,
132768
132914
  dispatchCycleId: slot.dispatchCycleId ?? issueRuntime?.lastDispatchCycleId ?? null,
132769
132915
  dispatchRunId: slot.dispatchRunId ?? issueRuntime?.dispatchRunId ?? null
132770
132916
  },
@@ -132781,20 +132927,25 @@ async function checkWorkerHealth(opts) {
132781
132927
  }
132782
132928
  ).catch(() => {
132783
132929
  });
132784
- await revertLabel(fix, expectedLabel, slotQueueLabel);
132930
+ await revertLabel(fix, expectedLabel, convergence.targetLabel);
132785
132931
  if (!fix.labelRevertFailed) {
132786
132932
  await deactivateSlot();
132787
132933
  await updateIssueRuntime(workspaceDir, projectSlug, issueIdNum, {
132788
132934
  inconclusiveCompletionAt: (/* @__PURE__ */ new Date()).toISOString(),
132789
132935
  inconclusiveCompletionReason: "stalled_with_artifact",
132790
- progressNotifiedAt: null
132936
+ progressNotifiedAt: null,
132937
+ lastConvergenceCause: convergence.cause,
132938
+ lastConvergenceAction: convergence.action,
132939
+ lastConvergenceRetryCount: convergence.retryCount,
132940
+ lastConvergenceReason: "stalled_with_artifact",
132941
+ lastConvergenceAt: (/* @__PURE__ */ new Date()).toISOString()
132791
132942
  }).catch(() => {
132792
132943
  });
132793
132944
  fix.fixed = true;
132794
132945
  await auditHealthFixApplied(workspaceDir, fix, {
132795
- action: "requeue_issue",
132946
+ action: convergence.action === "escalate_human" ? "escalate_human" : "requeue_issue",
132796
132947
  fromLabel: expectedLabel,
132797
- toLabel: slotQueueLabel,
132948
+ toLabel: convergence.targetLabel,
132798
132949
  idleMinutes: Math.round(quietMinutes),
132799
132950
  deliveryState
132800
132951
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mestreyoda/fabrica",
3
- "version": "0.2.34",
3
+ "version": "0.2.36",
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",