@mestreyoda/fabrica 0.2.34 → 0.2.35

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 +157 -30
  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.35") {
113909
+ return "0.2.35";
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";
@@ -131763,18 +131836,42 @@ async function applyWorkerResult(opts) {
131763
131836
  if (!validation.ok) {
131764
131837
  const validationReason = validation.reason ?? "developer_validation_failed";
131765
131838
  const feedbackQueueLabel = getQueueLabels(workflow, "developer").find((label) => isFeedbackState(workflow, label)) ?? "To Improve";
131839
+ const convergence = decidePostPrConvergence({
131840
+ workflow,
131841
+ issueRuntime: context2.issueRuntime,
131842
+ reason: validationReason,
131843
+ feedbackQueueLabel
131844
+ });
131845
+ const blockedSummary = convergence.action === "escalate_human" ? [
131846
+ "Automatic recovery escalated for human decision.",
131847
+ "",
131848
+ `Dominant cause: ${convergence.cause}`,
131849
+ `Retry budget exceeded: ${convergence.retryCount}/${convergence.maxRetries}`,
131850
+ "",
131851
+ validationReason
131852
+ ].join("\n") : `Automatic recovery: developer reported DONE but completion validation failed.
131853
+
131854
+ ${validationReason}`;
131766
131855
  await log(opts.workspaceDir, "worker_completion_skipped", {
131767
131856
  sessionKey: context2.project.workers[context2.parsed.role]?.levels?.[context2.slotLevel]?.[context2.slotIndex]?.sessionKey ?? null,
131768
131857
  projectSlug: context2.projectSlug,
131769
131858
  issueId: context2.issueId,
131770
131859
  role: context2.parsed.role,
131771
131860
  result: opts.result.value,
131772
- reason: validationReason
131861
+ reason: validationReason,
131862
+ convergenceCause: convergence.cause,
131863
+ convergenceAction: convergence.action,
131864
+ convergenceRetryCount: convergence.retryCount
131773
131865
  }).catch(() => {
131774
131866
  });
131775
131867
  await updateIssueRuntime(opts.workspaceDir, context2.projectSlug, context2.issueId, {
131776
131868
  inconclusiveCompletionAt: (/* @__PURE__ */ new Date()).toISOString(),
131777
- inconclusiveCompletionReason: validationReason
131869
+ inconclusiveCompletionReason: validationReason,
131870
+ lastConvergenceCause: convergence.cause,
131871
+ lastConvergenceAction: convergence.action,
131872
+ lastConvergenceRetryCount: convergence.retryCount,
131873
+ lastConvergenceReason: validationReason,
131874
+ lastConvergenceAt: (/* @__PURE__ */ new Date()).toISOString()
131778
131875
  }).catch(() => {
131779
131876
  });
131780
131877
  await executeCompletion({
@@ -131783,9 +131880,7 @@ async function applyWorkerResult(opts) {
131783
131880
  role: context2.parsed.role,
131784
131881
  result: "blocked",
131785
131882
  issueId: context2.issueId,
131786
- summary: `Automatic recovery: developer reported DONE but completion validation failed.
131787
-
131788
- ${validationReason}`,
131883
+ summary: blockedSummary,
131789
131884
  provider,
131790
131885
  repoPath,
131791
131886
  projectName: context2.project.name,
@@ -131794,7 +131889,7 @@ ${validationReason}`,
131794
131889
  workflow,
131795
131890
  level: context2.slotLevel,
131796
131891
  slotIndex: context2.slotIndex,
131797
- overrideToLabel: feedbackQueueLabel,
131892
+ overrideToLabel: convergence.targetLabel,
131798
131893
  overrideReason: validationReason,
131799
131894
  runCommand: opts.runCommand
131800
131895
  });
@@ -131897,13 +131992,23 @@ ${validationReason}`,
131897
131992
  await updateIssueRuntime(opts.workspaceDir, context2.projectSlug, context2.issueId, {
131898
131993
  infraFailCount: 0,
131899
131994
  inconclusiveCompletionAt: null,
131900
- inconclusiveCompletionReason: null
131995
+ inconclusiveCompletionReason: null,
131996
+ lastConvergenceCause: null,
131997
+ lastConvergenceAction: null,
131998
+ lastConvergenceRetryCount: 0,
131999
+ lastConvergenceReason: null,
132000
+ lastConvergenceAt: null
131901
132001
  }).catch(() => {
131902
132002
  });
131903
132003
  } else {
131904
132004
  await updateIssueRuntime(opts.workspaceDir, context2.projectSlug, context2.issueId, {
131905
132005
  inconclusiveCompletionAt: null,
131906
- inconclusiveCompletionReason: null
132006
+ inconclusiveCompletionReason: null,
132007
+ lastConvergenceCause: null,
132008
+ lastConvergenceAction: null,
132009
+ lastConvergenceRetryCount: 0,
132010
+ lastConvergenceReason: null,
132011
+ lastConvergenceAt: null
131907
132012
  }).catch(() => {
131908
132013
  });
131909
132014
  }
@@ -132365,6 +132470,12 @@ async function checkWorkerHealth(opts) {
132365
132470
  }).catch(() => {
132366
132471
  });
132367
132472
  }
132473
+ const convergence = decidePostPrConvergence({
132474
+ workflow,
132475
+ issueRuntime,
132476
+ reason: inconclusiveReason,
132477
+ feedbackQueueLabel: slotQueueLabel
132478
+ });
132368
132479
  const fix = {
132369
132480
  issue: {
132370
132481
  type: executionContractRecovery ? "execution_contract_recovery_exhausted" : "completion_recovery_exhausted",
@@ -132392,8 +132503,8 @@ async function checkWorkerHealth(opts) {
132392
132503
  issueUrl: issue2.web_url,
132393
132504
  issueTitle: issue2.title,
132394
132505
  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,
132506
+ 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",
132507
+ nextState: convergence.targetLabel,
132397
132508
  dispatchCycleId: slot.dispatchCycleId ?? issueRuntime?.lastDispatchCycleId ?? null,
132398
132509
  dispatchRunId: slot.dispatchRunId ?? issueRuntime?.dispatchRunId ?? null
132399
132510
  },
@@ -132410,12 +132521,17 @@ async function checkWorkerHealth(opts) {
132410
132521
  }
132411
132522
  ).catch(() => {
132412
132523
  });
132413
- await revertLabel(fix, expectedLabel, slotQueueLabel);
132524
+ await revertLabel(fix, expectedLabel, convergence.targetLabel);
132414
132525
  if (!fix.labelRevertFailed) {
132415
132526
  await deactivateSlot();
132416
132527
  await updateIssueRuntime(workspaceDir, projectSlug, issueIdNum, {
132417
132528
  inconclusiveCompletionAt: null,
132418
- inconclusiveCompletionReason: null
132529
+ inconclusiveCompletionReason: null,
132530
+ lastConvergenceCause: convergence.cause,
132531
+ lastConvergenceAction: convergence.action,
132532
+ lastConvergenceRetryCount: convergence.retryCount,
132533
+ lastConvergenceReason: inconclusiveReason,
132534
+ lastConvergenceAt: (/* @__PURE__ */ new Date()).toISOString()
132419
132535
  }).catch(() => {
132420
132536
  });
132421
132537
  fix.fixed = true;
@@ -132430,16 +132546,16 @@ async function checkWorkerHealth(opts) {
132430
132546
  slotIndex,
132431
132547
  reason: inconclusiveReason,
132432
132548
  fromLabel: expectedLabel,
132433
- toLabel: slotQueueLabel,
132549
+ toLabel: convergence.targetLabel,
132434
132550
  dispatchCycleId: slot.dispatchCycleId ?? issueRuntime?.lastDispatchCycleId ?? null,
132435
132551
  dispatchRunId: slot.dispatchRunId ?? issueRuntime?.dispatchRunId ?? null
132436
132552
  }).catch(() => {
132437
132553
  });
132438
132554
  }
132439
132555
  await auditHealthFixApplied(workspaceDir, fix, {
132440
- action: "requeue_issue",
132556
+ action: convergence.action === "escalate_human" ? "escalate_human" : "requeue_issue",
132441
132557
  fromLabel: expectedLabel,
132442
- toLabel: slotQueueLabel,
132558
+ toLabel: convergence.targetLabel,
132443
132559
  deliveryState
132444
132560
  });
132445
132561
  }
@@ -132619,7 +132735,7 @@ async function checkWorkerHealth(opts) {
132619
132735
  const minutesActive = (Date.now() - startedAtMs) / 6e4;
132620
132736
  const hours = minutesActive / 60;
132621
132737
  let reviewablePrStatus = null;
132622
- let hasReviewableArtifact = Boolean(
132738
+ let hasReviewableArtifact2 = Boolean(
132623
132739
  issueRuntime?.currentPrNumber || issueRuntime?.currentPrUrl || issueRuntime?.artifactOfRecord?.prNumber
132624
132740
  );
132625
132741
  if (issueIdNum) {
@@ -132627,12 +132743,12 @@ async function checkWorkerHealth(opts) {
132627
132743
  const prStatus = await provider.getPrStatus(issueIdNum);
132628
132744
  if (prStatus.url && prStatus.state !== PrState.MERGED && prStatus.state !== PrState.CLOSED && prStatus.currentIssueMatch !== false) {
132629
132745
  reviewablePrStatus = prStatus;
132630
- hasReviewableArtifact = true;
132746
+ hasReviewableArtifact2 = true;
132631
132747
  }
132632
132748
  } catch {
132633
132749
  }
132634
132750
  }
132635
- if (role === "developer" && issue2 && issueIdNum && !hasReviewableArtifact && minutesActive >= Math.max(5, Math.floor(stallTimeoutMinutes / 2)) && !issueRuntime?.progressNotifiedAt) {
132751
+ if (role === "developer" && issue2 && issueIdNum && !hasReviewableArtifact2 && minutesActive >= Math.max(5, Math.floor(stallTimeoutMinutes / 2)) && !issueRuntime?.progressNotifiedAt) {
132636
132752
  const channel = project.channels?.[0];
132637
132753
  await notify(
132638
132754
  {
@@ -132667,7 +132783,7 @@ async function checkWorkerHealth(opts) {
132667
132783
  }).catch(() => {
132668
132784
  });
132669
132785
  }
132670
- if (role === "developer" && issue2 && issueIdNum && !hasReviewableArtifact && minutesActive >= stallTimeoutMinutes) {
132786
+ if (role === "developer" && issue2 && issueIdNum && !hasReviewableArtifact2 && minutesActive >= stallTimeoutMinutes) {
132671
132787
  const fix = {
132672
132788
  issue: {
132673
132789
  type: "completion_recovery_exhausted",
@@ -132737,7 +132853,13 @@ async function checkWorkerHealth(opts) {
132737
132853
  if (!referenceAt || Number.isNaN(referenceAt)) return minutesActive;
132738
132854
  return (Date.now() - referenceAt) / 6e4;
132739
132855
  })();
132740
- if (role === "developer" && issue2 && issueIdNum && hasReviewableArtifact && reviewablePrStatus?.url && minutesActive >= stallTimeoutMinutes && quietMinutes >= Math.max(8, Math.floor(stallTimeoutMinutes / 2))) {
132856
+ if (role === "developer" && issue2 && issueIdNum && hasReviewableArtifact2 && reviewablePrStatus?.url && minutesActive >= stallTimeoutMinutes && quietMinutes >= Math.max(8, Math.floor(stallTimeoutMinutes / 2))) {
132857
+ const convergence = decidePostPrConvergence({
132858
+ workflow,
132859
+ issueRuntime,
132860
+ reason: "stalled_with_artifact",
132861
+ feedbackQueueLabel: slotQueueLabel
132862
+ });
132741
132863
  const fix = {
132742
132864
  issue: {
132743
132865
  type: "stalled_with_artifact",
@@ -132763,8 +132885,8 @@ async function checkWorkerHealth(opts) {
132763
132885
  issueUrl: issue2.web_url,
132764
132886
  issueTitle: issue2.title,
132765
132887
  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,
132888
+ 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}.`,
132889
+ nextState: convergence.targetLabel,
132768
132890
  dispatchCycleId: slot.dispatchCycleId ?? issueRuntime?.lastDispatchCycleId ?? null,
132769
132891
  dispatchRunId: slot.dispatchRunId ?? issueRuntime?.dispatchRunId ?? null
132770
132892
  },
@@ -132781,20 +132903,25 @@ async function checkWorkerHealth(opts) {
132781
132903
  }
132782
132904
  ).catch(() => {
132783
132905
  });
132784
- await revertLabel(fix, expectedLabel, slotQueueLabel);
132906
+ await revertLabel(fix, expectedLabel, convergence.targetLabel);
132785
132907
  if (!fix.labelRevertFailed) {
132786
132908
  await deactivateSlot();
132787
132909
  await updateIssueRuntime(workspaceDir, projectSlug, issueIdNum, {
132788
132910
  inconclusiveCompletionAt: (/* @__PURE__ */ new Date()).toISOString(),
132789
132911
  inconclusiveCompletionReason: "stalled_with_artifact",
132790
- progressNotifiedAt: null
132912
+ progressNotifiedAt: null,
132913
+ lastConvergenceCause: convergence.cause,
132914
+ lastConvergenceAction: convergence.action,
132915
+ lastConvergenceRetryCount: convergence.retryCount,
132916
+ lastConvergenceReason: "stalled_with_artifact",
132917
+ lastConvergenceAt: (/* @__PURE__ */ new Date()).toISOString()
132791
132918
  }).catch(() => {
132792
132919
  });
132793
132920
  fix.fixed = true;
132794
132921
  await auditHealthFixApplied(workspaceDir, fix, {
132795
- action: "requeue_issue",
132922
+ action: convergence.action === "escalate_human" ? "escalate_human" : "requeue_issue",
132796
132923
  fromLabel: expectedLabel,
132797
- toLabel: slotQueueLabel,
132924
+ toLabel: convergence.targetLabel,
132798
132925
  idleMinutes: Math.round(quietMinutes),
132799
132926
  deliveryState
132800
132927
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mestreyoda/fabrica",
3
- "version": "0.2.34",
3
+ "version": "0.2.35",
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",