@mestreyoda/fabrica 0.2.0 → 0.2.1

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/dist/index.js CHANGED
@@ -111324,8 +111324,8 @@ import fsSync from "node:fs";
111324
111324
  import path5 from "node:path";
111325
111325
  import { fileURLToPath as fileURLToPath3 } from "node:url";
111326
111326
  function getCurrentVersion() {
111327
- if ("0.2.0") {
111328
- return "0.2.0";
111327
+ if ("0.2.1") {
111328
+ return "0.2.1";
111329
111329
  }
111330
111330
  try {
111331
111331
  const pkgPath = path5.join(THIS_DIR, "..", "..", "package.json");
@@ -112302,7 +112302,7 @@ function resolve2(config2, sourceLayers, trace2) {
112302
112302
  dispatchMs: config2.timeouts?.dispatchMs ?? 6e5,
112303
112303
  staleWorkerHours: config2.timeouts?.staleWorkerHours ?? 2,
112304
112304
  sessionContextBudget: config2.timeouts?.sessionContextBudget ?? 0.6,
112305
- stallTimeoutMinutes: config2.timeouts?.stallTimeoutMinutes ?? 15,
112305
+ stallTimeoutMinutes: config2.timeouts?.stallTimeoutMinutes ?? 5,
112306
112306
  sessionConfirmAttempts: config2.timeouts?.sessionConfirmAttempts ?? 5,
112307
112307
  sessionConfirmDelayMs: config2.timeouts?.sessionConfirmDelayMs ?? 250,
112308
112308
  sessionLabelMaxLength: config2.timeouts?.sessionLabelMaxLength ?? 64,
@@ -124579,6 +124579,7 @@ import { jsonResult as jsonResult15 } from "openclaw/plugin-sdk";
124579
124579
  init_audit();
124580
124580
 
124581
124581
  // lib/services/heartbeat/diagnostic.ts
124582
+ import { execFileSync } from "child_process";
124582
124583
  async function diagnoseStall(input) {
124583
124584
  const { owner, repo, issueId, dispatchAttemptCount } = input;
124584
124585
  if ((dispatchAttemptCount ?? 0) >= 2) {
@@ -124591,9 +124592,9 @@ async function diagnoseStall(input) {
124591
124592
  if (pr) {
124592
124593
  const qaStatus = await checkPrQaStatus(owner, repo, pr.number);
124593
124594
  if (qaStatus === "pass") {
124594
- return { action: "transition_to_review", reason: "stall", evidence: `PR #${pr.number} exists, QA passing` };
124595
+ return { action: "transition_to_review", reason: "stall", evidence: `PR #${pr.number} exists, QA passing`, prNumber: pr.number };
124595
124596
  }
124596
- return { action: "redispatch_same_level", reason: "stall", evidence: `PR #${pr.number} exists, QA ${qaStatus}` };
124597
+ return { action: "redispatch_same_level", reason: "stall", evidence: `PR #${pr.number} exists, QA ${qaStatus}`, prNumber: pr.number };
124597
124598
  }
124598
124599
  const hasCommits = await checkForBranchCommits(owner, repo, issueId);
124599
124600
  if (hasCommits) {
@@ -124606,10 +124607,13 @@ async function diagnoseStall(input) {
124606
124607
  }
124607
124608
  return { action: "escalate_level", reason: "complexity", evidence: "Active session without commits" };
124608
124609
  }
124610
+ function ghSync(args) {
124611
+ return execFileSync("gh", args, { encoding: "utf-8", timeout: 15e3 });
124612
+ }
124609
124613
  async function findPrForIssue(owner, repo, issueId) {
124614
+ if (!owner || !repo) return null;
124610
124615
  try {
124611
- const { execa } = await import("execa");
124612
- const { stdout } = await execa("gh", [
124616
+ const stdout = ghSync([
124613
124617
  "pr",
124614
124618
  "list",
124615
124619
  "--repo",
@@ -124628,29 +124632,31 @@ async function findPrForIssue(owner, repo, issueId) {
124628
124632
  }
124629
124633
  }
124630
124634
  async function checkPrQaStatus(owner, repo, prNumber) {
124635
+ if (!owner || !repo) return "pending";
124631
124636
  try {
124632
- const { execa } = await import("execa");
124633
- const { stdout } = await execa("gh", [
124637
+ const stdout = ghSync([
124634
124638
  "pr",
124635
- "checks",
124639
+ "view",
124636
124640
  String(prNumber),
124637
124641
  "--repo",
124638
124642
  `${owner}/${repo}`,
124639
124643
  "--json",
124640
- "state"
124644
+ "statusCheckRollup"
124641
124645
  ]);
124642
- const checks = JSON.parse(stdout);
124643
- if (checks.every((c) => c.state === "SUCCESS")) return "pass";
124644
- if (checks.some((c) => c.state === "FAILURE")) return "fail";
124646
+ const data = JSON.parse(stdout);
124647
+ const checks = data.statusCheckRollup ?? [];
124648
+ if (checks.length === 0) return "pass";
124649
+ if (checks.every((c) => c.conclusion === "SUCCESS")) return "pass";
124650
+ if (checks.some((c) => c.conclusion === "FAILURE")) return "fail";
124645
124651
  return "pending";
124646
124652
  } catch {
124647
- return "pending";
124653
+ return "pass";
124648
124654
  }
124649
124655
  }
124650
124656
  async function checkForBranchCommits(owner, repo, issueId) {
124657
+ if (!owner || !repo) return false;
124651
124658
  try {
124652
- const { execa } = await import("execa");
124653
- const { stdout } = await execa("gh", [
124659
+ const stdout = ghSync([
124654
124660
  "api",
124655
124661
  `repos/${owner}/${repo}/commits`,
124656
124662
  "--jq",
@@ -124678,6 +124684,7 @@ var GRACE_PERIOD_MS = 5 * 60 * 1e3;
124678
124684
  var DISPATCH_CONFIRMATION_TIMEOUT_MS = 2 * 60 * 1e3;
124679
124685
  var NUDGE_MESSAGE = `You appear to have stalled. Continue working on your current task. If you are blocked or unable to proceed, call work_finish with result "blocked".`;
124680
124686
  var MAX_STALL_NUDGES = 3;
124687
+ var MAX_DISPATCH_ATTEMPTS = 5;
124681
124688
  async function auditHealthFixApplied(workspaceDir, fix, details) {
124682
124689
  if (!fix.fixed) return;
124683
124690
  await withCorrelationContext({
@@ -125045,30 +125052,84 @@ async function checkWorkerHealth(opts) {
125045
125052
  }
125046
125053
  if (slot.active && sessionKey && sessions && !withinGracePeriod && isSessionAlive(sessionKey, sessions)) {
125047
125054
  const session = sessions.get(sessionKey);
125048
- const stallThresholdMs = (opts.stallTimeoutMinutes ?? 15) * 6e4;
125055
+ const stallThresholdMs = (opts.stallTimeoutMinutes ?? 5) * 6e4;
125049
125056
  if (session.updatedAt == null) continue;
125050
125057
  const sessionIdleMs = Date.now() - session.updatedAt;
125051
125058
  if (sessionIdleMs > stallThresholdMs) {
125052
125059
  const idleMinutes = Math.round(sessionIdleMs / 6e4);
125060
+ const repoRemote = project.repoRemote ?? "";
125061
+ const remoteMatch = repoRemote.match(/github\.com\/([^/]+)\/([^/.]+)/);
125062
+ const diagnostic = await diagnoseStall({
125063
+ projectSlug,
125064
+ owner: remoteMatch?.[1] ?? "",
125065
+ repo: remoteMatch?.[2] ?? "",
125066
+ issueId: issueIdNum ?? 0,
125067
+ sessionKey,
125068
+ slotStartTime: slot.startTime ? new Date(slot.startTime).getTime() : Date.now() - sessionIdleMs,
125069
+ sessionUpdatedAt: session.updatedAt,
125070
+ dispatchAttemptCount: issueRuntime?.dispatchAttemptCount ?? 0
125071
+ });
125072
+ const evidenceActions = ["transition_to_review", "nudge_open_pr"];
125073
+ const hasActionableEvidence = evidenceActions.includes(diagnostic.action);
125053
125074
  const modelUnresponsiveMs = stallThresholdMs * MAX_STALL_NUDGES;
125054
125075
  const slotAgeMs = slot.startTime ? Date.now() - new Date(slot.startTime).getTime() : sessionIdleMs;
125055
- if (slotAgeMs > modelUnresponsiveMs) {
125056
- const diagnostic = await diagnoseStall({
125076
+ const isModelUnresponsive = slotAgeMs > modelUnresponsiveMs;
125077
+ const currentAttempts = issueRuntime?.dispatchAttemptCount ?? 0;
125078
+ if (currentAttempts >= MAX_DISPATCH_ATTEMPTS && !hasActionableEvidence) {
125079
+ const fix2 = {
125080
+ issue: {
125081
+ type: "diagnostic_needs_human_review",
125082
+ severity: "critical",
125083
+ project: project.name,
125084
+ projectSlug,
125085
+ role,
125086
+ level,
125087
+ sessionKey,
125088
+ issueId: slot.issueId,
125089
+ slotIndex,
125090
+ message: `${role.toUpperCase()} ${level}[${slotIndex}] circuit breaker: ${currentAttempts} dispatch attempts without completion \u2014 moving to Refining`
125091
+ },
125092
+ fixed: false
125093
+ };
125094
+ if (autoFix) {
125095
+ await revertLabel(fix2, expectedLabel, "Refining");
125096
+ await deactivateSlot();
125097
+ fix2.fixed = true;
125098
+ }
125099
+ await log(workspaceDir, "dispatch_circuit_breaker", {
125100
+ project: project.name,
125057
125101
  projectSlug,
125058
- owner: project.remote?.owner ?? "",
125059
- repo: project.remote?.repo ?? "",
125060
- issueId: issueIdNum ?? 0,
125102
+ role,
125103
+ level,
125061
125104
  sessionKey,
125062
- slotStartTime: slot.startTime ? new Date(slot.startTime).getTime() : Date.now() - sessionIdleMs,
125063
- sessionUpdatedAt: session.updatedAt,
125064
- dispatchAttemptCount: issueRuntime?.dispatchAttemptCount ?? 0
125105
+ issueId: slot.issueId,
125106
+ slotIndex,
125107
+ dispatchAttemptCount: currentAttempts,
125108
+ diagnostic: diagnostic.action,
125109
+ evidence: diagnostic.evidence
125110
+ }).catch(() => {
125065
125111
  });
125112
+ fixes.push(fix2);
125113
+ continue;
125114
+ }
125115
+ if (hasActionableEvidence || isModelUnresponsive) {
125066
125116
  if (issueIdNum) {
125067
- await updateIssueRuntime(workspaceDir, projectSlug, String(issueIdNum), {
125117
+ const runtimeUpdate = {
125068
125118
  lastDiagnosticResult: diagnostic.evidence,
125069
125119
  lastFailureReason: diagnostic.reason,
125070
125120
  dispatchAttemptCount: (issueRuntime?.dispatchAttemptCount ?? 0) + 1
125071
- }).catch(() => {
125121
+ };
125122
+ if (diagnostic.prNumber) {
125123
+ const prOwner = remoteMatch?.[1] ?? "";
125124
+ const prRepo = remoteMatch?.[2] ?? "";
125125
+ runtimeUpdate.currentPrNumber = diagnostic.prNumber;
125126
+ runtimeUpdate.currentPrUrl = `https://github.com/${prOwner}/${prRepo}/pull/${diagnostic.prNumber}`;
125127
+ runtimeUpdate.currentPrState = "open";
125128
+ runtimeUpdate.bindingSource = "diagnostic";
125129
+ runtimeUpdate.bindingConfidence = "high";
125130
+ runtimeUpdate.boundAt = (/* @__PURE__ */ new Date()).toISOString();
125131
+ }
125132
+ await updateIssueRuntime(workspaceDir, projectSlug, String(issueIdNum), runtimeUpdate).catch(() => {
125072
125133
  });
125073
125134
  }
125074
125135
  const diagnosticType = `diagnostic_${diagnostic.action}`;
@@ -125089,13 +125150,19 @@ async function checkWorkerHealth(opts) {
125089
125150
  };
125090
125151
  if (autoFix) {
125091
125152
  switch (diagnostic.action) {
125092
- case "transition_to_review":
125093
- await revertLabel(fix2, expectedLabel, "To Review");
125153
+ case "transition_to_review": {
125154
+ const targetLabel = role === "tester" ? "Done" : role === "reviewer" ? "To Test" : "To Review";
125155
+ await revertLabel(fix2, expectedLabel, targetLabel);
125094
125156
  if (!fix2.labelRevertFailed) {
125095
125157
  await deactivateSlot();
125158
+ if (role === "tester" && issueIdNum) {
125159
+ await provider.closeIssue(issueIdNum).catch(() => {
125160
+ });
125161
+ }
125096
125162
  fix2.fixed = true;
125097
125163
  }
125098
125164
  break;
125165
+ }
125099
125166
  case "redispatch_same_level":
125100
125167
  case "nudge_open_pr":
125101
125168
  case "retry_infra":
@@ -125113,10 +125180,12 @@ async function checkWorkerHealth(opts) {
125113
125180
  }
125114
125181
  break;
125115
125182
  case "needs_human_review":
125183
+ await revertLabel(fix2, expectedLabel, "Refining");
125116
125184
  await deactivateSlot();
125117
125185
  fix2.fixed = true;
125118
125186
  break;
125119
125187
  case "log_infra":
125188
+ await revertLabel(fix2, expectedLabel, "Refining");
125120
125189
  await deactivateSlot();
125121
125190
  fix2.fixed = true;
125122
125191
  break;
@@ -125134,6 +125203,7 @@ async function checkWorkerHealth(opts) {
125134
125203
  diagnostic: diagnostic.action,
125135
125204
  reason: diagnostic.reason,
125136
125205
  evidence: diagnostic.evidence,
125206
+ fastPath: hasActionableEvidence,
125137
125207
  dispatchAttemptCount: (issueRuntime?.dispatchAttemptCount ?? 0) + 1
125138
125208
  }).catch(() => {
125139
125209
  });
@@ -125151,7 +125221,7 @@ async function checkWorkerHealth(opts) {
125151
125221
  sessionKey,
125152
125222
  issueId: slot.issueId,
125153
125223
  slotIndex,
125154
- message: `${role.toUpperCase()} ${level}[${slotIndex}] session idle ${idleMinutes}m \u2014 sending nudge`
125224
+ message: `${role.toUpperCase()} ${level}[${slotIndex}] session idle ${idleMinutes}m \u2014 no deliverables yet, sending nudge`
125155
125225
  },
125156
125226
  fixed: false
125157
125227
  };
@@ -138727,16 +138797,32 @@ async function classifyDmIntent(ctx, content, _workspaceDir) {
138727
138797
  idempotencyKey: sessionKey
138728
138798
  });
138729
138799
  const waitResult = await runtime.subagent.waitForRun({ runId, timeoutMs: 15e3 });
138730
- if (waitResult.status !== "ok") return null;
138731
- const messages = await runtime.subagent.getSessionMessages({ sessionKey });
138732
- const lastAssistant = messages?.filter((m2) => m2.role === "assistant").pop();
138733
- const text = lastAssistant?.content ?? "";
138734
- if (!text.trim()) return null;
138800
+ if (waitResult.status !== "ok") {
138801
+ ctx.logger.warn(`[telegram-bootstrap] classify waitForRun status=${waitResult.status} (expected ok)`);
138802
+ return null;
138803
+ }
138804
+ const messagesResult = await runtime.subagent.getSessionMessages({ sessionKey });
138805
+ const messages = Array.isArray(messagesResult) ? messagesResult : Array.isArray(messagesResult?.messages) ? messagesResult.messages : [];
138806
+ const lastAssistant = messages.filter((m2) => m2.role === "assistant").pop();
138807
+ if (!lastAssistant) {
138808
+ ctx.logger.warn(`[telegram-bootstrap] classify: no assistant message found (messages=${messages.length}, resultType=${typeof messagesResult})`);
138809
+ return null;
138810
+ }
138811
+ const rawContent = lastAssistant.content;
138812
+ const text = typeof rawContent === "string" ? rawContent : Array.isArray(rawContent) ? rawContent.find((b) => b.type === "text")?.text ?? "" : "";
138813
+ if (!text.trim()) {
138814
+ ctx.logger.warn(`[telegram-bootstrap] classify: empty text from assistant (contentType=${typeof rawContent}, isArray=${Array.isArray(rawContent)})`);
138815
+ return null;
138816
+ }
138735
138817
  const jsonStr = text.replace(/^```(json)?/gm, "").replace(/```$/gm, "").trim();
138736
138818
  const intentData = JSON.parse(jsonStr);
138737
138819
  const validated = DmIntentSchema.safeParse(intentData);
138820
+ if (!validated.success) {
138821
+ ctx.logger.warn(`[telegram-bootstrap] classify: schema validation failed: ${validated.error?.message}`);
138822
+ }
138738
138823
  return validated.success ? validated.data : null;
138739
- } catch {
138824
+ } catch (err) {
138825
+ ctx.logger.warn(`[telegram-bootstrap] classify exception: ${err.message ?? err}`);
138740
138826
  return null;
138741
138827
  }
138742
138828
  }