@mestreyoda/fabrica 0.2.33 → 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 +227 -26
  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.33") {
113909
- return "0.2.33";
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
  }
@@ -132618,19 +132734,21 @@ async function checkWorkerHealth(opts) {
132618
132734
  const startedAtMs = new Date(slot.startTime).getTime();
132619
132735
  const minutesActive = (Date.now() - startedAtMs) / 6e4;
132620
132736
  const hours = minutesActive / 60;
132621
- let hasReviewableArtifact = Boolean(
132737
+ let reviewablePrStatus = null;
132738
+ let hasReviewableArtifact2 = Boolean(
132622
132739
  issueRuntime?.currentPrNumber || issueRuntime?.currentPrUrl || issueRuntime?.artifactOfRecord?.prNumber
132623
132740
  );
132624
- if (!hasReviewableArtifact && issueIdNum) {
132741
+ if (issueIdNum) {
132625
132742
  try {
132626
132743
  const prStatus = await provider.getPrStatus(issueIdNum);
132627
- hasReviewableArtifact = Boolean(
132628
- prStatus.url && prStatus.state !== PrState.MERGED && prStatus.state !== PrState.CLOSED && prStatus.currentIssueMatch !== false
132629
- );
132744
+ if (prStatus.url && prStatus.state !== PrState.MERGED && prStatus.state !== PrState.CLOSED && prStatus.currentIssueMatch !== false) {
132745
+ reviewablePrStatus = prStatus;
132746
+ hasReviewableArtifact2 = true;
132747
+ }
132630
132748
  } catch {
132631
132749
  }
132632
132750
  }
132633
- 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) {
132634
132752
  const channel = project.channels?.[0];
132635
132753
  await notify(
132636
132754
  {
@@ -132665,7 +132783,7 @@ async function checkWorkerHealth(opts) {
132665
132783
  }).catch(() => {
132666
132784
  });
132667
132785
  }
132668
- if (role === "developer" && issue2 && issueIdNum && !hasReviewableArtifact && minutesActive >= stallTimeoutMinutes) {
132786
+ if (role === "developer" && issue2 && issueIdNum && !hasReviewableArtifact2 && minutesActive >= stallTimeoutMinutes) {
132669
132787
  const fix = {
132670
132788
  issue: {
132671
132789
  type: "completion_recovery_exhausted",
@@ -132729,6 +132847,89 @@ async function checkWorkerHealth(opts) {
132729
132847
  fixes.push(fix);
132730
132848
  continue;
132731
132849
  }
132850
+ const quietMinutes = (() => {
132851
+ const lastObservableAt = sessionKey ? getLastObservableSessionActivityAt(sessionKey, sessions) : null;
132852
+ const referenceAt = lastObservableAt ?? agentAcceptedAt ?? dispatchRequestedAt ?? startedAtMs;
132853
+ if (!referenceAt || Number.isNaN(referenceAt)) return minutesActive;
132854
+ return (Date.now() - referenceAt) / 6e4;
132855
+ })();
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
+ });
132863
+ const fix = {
132864
+ issue: {
132865
+ type: "stalled_with_artifact",
132866
+ severity: "critical",
132867
+ project: project.name,
132868
+ projectSlug,
132869
+ role,
132870
+ level,
132871
+ sessionKey,
132872
+ issueId: slot.issueId,
132873
+ slotIndex,
132874
+ message: `${role.toUpperCase()} ${level}[${slotIndex}] has an open PR but stayed idle for ${Math.round(quietMinutes)}m without converging`
132875
+ },
132876
+ fixed: false
132877
+ };
132878
+ if (autoFix) {
132879
+ const channel = project.channels?.[0];
132880
+ await notify(
132881
+ {
132882
+ type: "workerRecoveryExhausted",
132883
+ project: project.name,
132884
+ issueId: issueIdNum,
132885
+ issueUrl: issue2.web_url,
132886
+ issueTitle: issue2.title,
132887
+ role,
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,
132890
+ dispatchCycleId: slot.dispatchCycleId ?? issueRuntime?.lastDispatchCycleId ?? null,
132891
+ dispatchRunId: slot.dispatchRunId ?? issueRuntime?.dispatchRunId ?? null
132892
+ },
132893
+ {
132894
+ workspaceDir,
132895
+ config: notificationConfig,
132896
+ target: channel ? {
132897
+ channelId: channel.channelId,
132898
+ channel: channel.channel,
132899
+ accountId: channel.accountId,
132900
+ messageThreadId: channel.messageThreadId
132901
+ } : void 0,
132902
+ runCommand
132903
+ }
132904
+ ).catch(() => {
132905
+ });
132906
+ await revertLabel(fix, expectedLabel, convergence.targetLabel);
132907
+ if (!fix.labelRevertFailed) {
132908
+ await deactivateSlot();
132909
+ await updateIssueRuntime(workspaceDir, projectSlug, issueIdNum, {
132910
+ inconclusiveCompletionAt: (/* @__PURE__ */ new Date()).toISOString(),
132911
+ inconclusiveCompletionReason: "stalled_with_artifact",
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()
132918
+ }).catch(() => {
132919
+ });
132920
+ fix.fixed = true;
132921
+ await auditHealthFixApplied(workspaceDir, fix, {
132922
+ action: convergence.action === "escalate_human" ? "escalate_human" : "requeue_issue",
132923
+ fromLabel: expectedLabel,
132924
+ toLabel: convergence.targetLabel,
132925
+ idleMinutes: Math.round(quietMinutes),
132926
+ deliveryState
132927
+ });
132928
+ }
132929
+ }
132930
+ fixes.push(fix);
132931
+ continue;
132932
+ }
132732
132933
  if (hours > staleWorkerHours) {
132733
132934
  const fix = {
132734
132935
  issue: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mestreyoda/fabrica",
3
- "version": "0.2.33",
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",