@mestreyoda/fabrica 0.1.11 → 0.1.13

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
@@ -110768,7 +110768,7 @@ var init_registry = __esm({
110768
110768
  senior: "\u{1F9E0}"
110769
110769
  },
110770
110770
  fallbackEmoji: "\u{1F50D}",
110771
- completionResults: ["pass", "fail", "refine", "blocked"],
110771
+ completionResults: ["pass", "fail", "fail_infra", "refine", "blocked"],
110772
110772
  sessionKeyPattern: "tester",
110773
110773
  notifications: { onStart: true, onComplete: true }
110774
110774
  },
@@ -111329,8 +111329,8 @@ import fsSync from "node:fs";
111329
111329
  import path5 from "node:path";
111330
111330
  import { fileURLToPath as fileURLToPath3 } from "node:url";
111331
111331
  function getCurrentVersion() {
111332
- if ("0.1.11") {
111333
- return "0.1.11";
111332
+ if ("0.1.13") {
111333
+ return "0.1.13";
111334
111334
  }
111335
111335
  try {
111336
111336
  const pkgPath = path5.join(THIS_DIR, "..", "..", "package.json");
@@ -112573,7 +112573,7 @@ async function resilientLabelTransition(provider, issueId, from, to, log3) {
112573
112573
  for (let i2 = 0; i2 < 2; i2++) {
112574
112574
  try {
112575
112575
  await provider.removeLabels(issueId, [from]);
112576
- log3?.(`Dual state resolved: removed ${from} from issue ${issueId}`);
112576
+ log3?.(`dual_state_recovery: removed ${from} from issue ${issueId} (atomic PUT should have prevented this \u2014 investigate)`);
112577
112577
  return { success: true, dualStateResolved: true };
112578
112578
  } catch (retryErr) {
112579
112579
  log3?.(`Retry ${i2 + 1}/2 to remove ${from} failed: ${String(retryErr)}`);
@@ -114240,7 +114240,7 @@ var require_Policy = __commonJS({
114240
114240
  Object.defineProperty(exports2, "__esModule", { value: true });
114241
114241
  exports2.handleAll = exports2.noop = exports2.Policy = void 0;
114242
114242
  exports2.handleType = handleType;
114243
- exports2.handleWhen = handleWhen;
114243
+ exports2.handleWhen = handleWhen2;
114244
114244
  exports2.handleResultType = handleResultType;
114245
114245
  exports2.handleWhenResult = handleWhenResult;
114246
114246
  exports2.bulkhead = bulkhead;
@@ -114367,7 +114367,7 @@ var require_Policy = __commonJS({
114367
114367
  function handleType(cls, predicate) {
114368
114368
  return new Policy({ errorFilter: typeFilter(cls, predicate), resultFilter: never2 });
114369
114369
  }
114370
- function handleWhen(predicate) {
114370
+ function handleWhen2(predicate) {
114371
114371
  return new Policy({ errorFilter: predicate, resultFilter: never2 });
114372
114372
  }
114373
114373
  function handleResultType(cls, predicate) {
@@ -117174,15 +117174,23 @@ init_logger();
117174
117174
 
117175
117175
  // lib/providers/resilience.ts
117176
117176
  var import_cockatiel = __toESM(require_dist4(), 1);
117177
+ var GitHubRateLimitError = class extends Error {
117178
+ constructor(retryAfterMs) {
117179
+ super(`GitHub rate limit \u2014 retry after ${retryAfterMs}ms`);
117180
+ this.retryAfterMs = retryAfterMs;
117181
+ this.name = "GitHubRateLimitError";
117182
+ }
117183
+ };
117177
117184
  var MAX_ENTRIES = 50;
117178
117185
  var policyCache = /* @__PURE__ */ new Map();
117179
117186
  var accessOrder = [];
117180
117187
  function createPolicy() {
117181
- const retryPolicy = (0, import_cockatiel.retry)(import_cockatiel.handleAll, {
117188
+ const retryableErrors = (0, import_cockatiel.handleWhen)((err) => !(err instanceof GitHubRateLimitError));
117189
+ const retryPolicy = (0, import_cockatiel.retry)(retryableErrors, {
117182
117190
  maxAttempts: 3,
117183
117191
  backoff: new import_cockatiel.ExponentialBackoff({
117184
117192
  initialDelay: 500,
117185
- maxDelay: 5e3
117193
+ maxDelay: 1e4
117186
117194
  })
117187
117195
  });
117188
117196
  const breakerPolicy = (0, import_cockatiel.circuitBreaker)(import_cockatiel.handleAll, {
@@ -118097,6 +118105,35 @@ var GitHubProvider = class {
118097
118105
  async gh(args) {
118098
118106
  return this.ghAt(args, { cwd: this.repoPath });
118099
118107
  }
118108
+ get providerKey() {
118109
+ return this.repoPath;
118110
+ }
118111
+ /**
118112
+ * Execute a `gh api` call with optional JSON body sent via stdin.
118113
+ * Uses withResilience (per-provider retry + circuit breaker).
118114
+ * Throws GitHubRateLimitError on 429 / rate limit responses.
118115
+ */
118116
+ async ghApi(endpoint2, method, body) {
118117
+ const args = ["api", endpoint2, "--method", method];
118118
+ if (body !== void 0) {
118119
+ args.push("--input", "-");
118120
+ }
118121
+ return withResilience(async () => {
118122
+ const result = await this.runCommand(["gh", ...args], {
118123
+ timeoutMs: 3e4,
118124
+ cwd: this.repoPath,
118125
+ input: body !== void 0 ? JSON.stringify(body) : void 0
118126
+ });
118127
+ if (result.code != null && result.code !== 0) {
118128
+ const errText = result.stderr?.trim() ?? "";
118129
+ if (errText.includes("rate limit") || errText.includes("429")) {
118130
+ throw new GitHubRateLimitError(6e4);
118131
+ }
118132
+ throw new Error(errText || `gh api ${method} ${endpoint2} failed with exit code ${result.code}`);
118133
+ }
118134
+ return result.stdout.trim();
118135
+ }, this.providerKey);
118136
+ }
118100
118137
  async git(args, opts) {
118101
118138
  const result = await this.runCommand(["git", ...args], {
118102
118139
  timeoutMs: opts?.timeoutMs ?? 3e4,
@@ -118152,14 +118189,28 @@ var GitHubProvider = class {
118152
118189
  return Buffer.from(input).toString("base64url");
118153
118190
  }
118154
118191
  async githubFetch(url2, init, auth7) {
118155
- const headers = new Headers(init.headers ?? {});
118156
- headers.set("Accept", "application/vnd.github+json");
118157
- headers.set("Authorization", `Bearer ${auth7.token}`);
118158
- headers.set("X-GitHub-Api-Version", "2022-11-28");
118159
- if (init.body && !headers.has("Content-Type")) {
118160
- headers.set("Content-Type", "application/json");
118161
- }
118162
- return fetch(url2, { ...init, headers });
118192
+ return withResilience(async () => {
118193
+ const headers = new Headers(init.headers ?? {});
118194
+ headers.set("Accept", "application/vnd.github+json");
118195
+ headers.set("Authorization", `Bearer ${auth7.token}`);
118196
+ headers.set("X-GitHub-Api-Version", "2022-11-28");
118197
+ if (init.body && !headers.has("Content-Type")) {
118198
+ headers.set("Content-Type", "application/json");
118199
+ }
118200
+ const res = await fetch(url2, { ...init, headers });
118201
+ const remaining = res.headers.get("x-ratelimit-remaining");
118202
+ if (res.status === 429 || res.status === 403 && remaining === "0") {
118203
+ const retryAfter = res.headers.get("retry-after");
118204
+ const resetEpoch = res.headers.get("x-ratelimit-reset");
118205
+ const waitMs = retryAfter ? parseInt(retryAfter, 10) * 1e3 : resetEpoch ? Math.max(0, parseInt(resetEpoch, 10) * 1e3 - Date.now()) + 1e3 : 6e4;
118206
+ throw new GitHubRateLimitError(waitMs);
118207
+ }
118208
+ if (!res.ok) {
118209
+ const body = await res.text().catch(() => "");
118210
+ throw new Error(`GitHub API ${res.status}: ${body}`);
118211
+ }
118212
+ return res;
118213
+ }, this.providerKey);
118163
118214
  }
118164
118215
  async resolveInstallationAuth() {
118165
118216
  const profile = this.resolveAuthProfile();
@@ -118557,7 +118608,7 @@ Bootstrapped by Fabrica.
118557
118608
  }
118558
118609
  async listIssuesByLabel(label) {
118559
118610
  try {
118560
- const raw = await this.gh(["issue", "list", "--label", label, "--state", "open", "--json", "number,title,body,labels,state,url"]);
118611
+ const raw = await this.gh(["issue", "list", "--label", label, "--state", "open", "-L", "200", "--json", "number,title,body,labels,state,url"]);
118561
118612
  return JSON.parse(raw).map(toIssue);
118562
118613
  } catch {
118563
118614
  return [];
@@ -118565,7 +118616,7 @@ Bootstrapped by Fabrica.
118565
118616
  }
118566
118617
  async listIssues(opts) {
118567
118618
  try {
118568
- const args = ["issue", "list", "--state", opts?.state ?? "open", "--json", "number,title,body,labels,state,url"];
118619
+ const args = ["issue", "list", "--state", opts?.state ?? "open", "-L", "200", "--json", "number,title,body,labels,state,url"];
118569
118620
  if (opts?.label) args.push("--label", opts.label);
118570
118621
  const raw = await this.gh(args);
118571
118622
  return JSON.parse(raw).map(toIssue);
@@ -118587,29 +118638,20 @@ Bootstrapped by Fabrica.
118587
118638
  }
118588
118639
  }
118589
118640
  async transitionLabel(issueId, from, to) {
118590
- await this.gh(["issue", "edit", String(issueId), "--add-label", to]);
118591
118641
  const issue2 = await this.getIssue(issueId);
118592
118642
  const stateLabels = getStateLabels(this.workflow);
118593
- const currentStateLabels = issue2.labels.filter((l) => stateLabels.includes(l) && l !== to);
118594
- const staleOperationalLabels = issue2.labels.filter(
118595
- (l) => LEGACY_OPERATIONAL_LABELS.includes(l)
118596
- );
118597
- if (currentStateLabels.length > 0 || staleOperationalLabels.length > 0) {
118598
- const args = ["issue", "edit", String(issueId)];
118599
- for (const l of currentStateLabels) args.push("--remove-label", l);
118600
- for (const l of staleOperationalLabels) args.push("--remove-label", l);
118601
- await this.gh(args);
118602
- }
118643
+ const desired = issue2.labels.filter(
118644
+ (l) => !stateLabels.includes(l) && !LEGACY_OPERATIONAL_LABELS.includes(l)
118645
+ ).concat(to);
118646
+ await this.ghApi(`repos/{owner}/{repo}/issues/${issueId}/labels`, "PUT", { labels: desired });
118603
118647
  try {
118604
118648
  const postIssue = await this.getIssue(issueId);
118605
118649
  const postStateLabels = postIssue.labels.filter((l) => stateLabels.includes(l));
118606
118650
  if (postStateLabels.length !== 1 || !postStateLabels.includes(to)) {
118607
- logger3.error({
118608
- issueId,
118609
- from,
118610
- to,
118611
- postStateLabels
118612
- }, "State transition anomaly detected after GitHub issue label transition");
118651
+ logger3.error(
118652
+ { issueId, from, to, postStateLabels },
118653
+ "State transition anomaly detected after atomic label PUT"
118654
+ );
118613
118655
  }
118614
118656
  } catch {
118615
118657
  }
@@ -119810,6 +119852,22 @@ PR merged \u2014 issue closed automatically (was stuck in ${event.fromState})`;
119810
119852
  \u{1F3C1} Done \u2014 no human action needed.`;
119811
119853
  return msg;
119812
119854
  }
119855
+ case "infraFailure": {
119856
+ const icon = event.infraFailCount >= 2 ? "\u{1F6A8}" : "\u26A0\uFE0F";
119857
+ let msg = `${icon} Infrastructure failure on #${event.issueId} (attempt ${event.infraFailCount})`;
119858
+ msg += `
119859
+ ${event.summary}`;
119860
+ msg += `
119861
+ \u{1F4CB} [Issue #${event.issueId}](${event.issueUrl})`;
119862
+ if (event.infraFailCount >= 2) {
119863
+ msg += `
119864
+ \u2192 Circuit breaker tripped \u2014 moved to Refining (operator intervention required)`;
119865
+ } else {
119866
+ msg += `
119867
+ \u2192 Returned to To Test queue \u2014 will retry after toolchain is fixed`;
119868
+ }
119869
+ return msg;
119870
+ }
119813
119871
  }
119814
119872
  }
119815
119873
  async function sendMessage(target, message, channel, workspaceDir, runtime, accountId, runCommand, messageThreadId, auditMeta) {
@@ -120056,7 +120114,11 @@ async function persistMergedArtifact(opts) {
120056
120114
  },
120057
120115
  currentPrState: PrState.MERGED,
120058
120116
  followUpPrRequired: false
120059
- }).catch(() => {
120117
+ }).catch((err) => {
120118
+ console.warn(
120119
+ JSON.stringify({ projectSlug, issueId, prNumber, error: String(err) }),
120120
+ "persistMergedArtifact failed \u2014 issue close guard may not find merge evidence"
120121
+ );
120060
120122
  });
120061
120123
  }
120062
120124
  async function guardedCloseIssue(opts) {
@@ -120543,10 +120605,10 @@ init_audit();
120543
120605
  init_migrate_layout();
120544
120606
  init_roles();
120545
120607
  init_workflow();
120608
+ init_labels();
120546
120609
 
120547
120610
  // lib/tools/tasks/public-output-sanitizer.ts
120548
120611
  var SECRET_PATTERNS = [
120549
- /\b[A-Za-z_][A-Za-z0-9_]*=([^\s]+)/g,
120550
120612
  /\b(?:ghp_|gho_|github_pat_|sk-|xoxb-|xoxp-|AIza|AKIA|glpat-)[A-Za-z0-9._-]*/g,
120551
120613
  /\b(?:token|secret|api[_-]?key|password|passwd|authorization|bearer)\b\s*[:=]\s*[^\s]+/gi
120552
120614
  ];
@@ -120871,6 +120933,7 @@ function matchesReviewArtifact(comment, artifactId, artifactType) {
120871
120933
  }
120872
120934
  return comment.state === "COMMENTED" && !comment.path;
120873
120935
  }
120936
+ var INFRA_FAIL_CIRCUIT_BREAKER_THRESHOLD = 2;
120874
120937
  function createWorkFinishTool(ctx) {
120875
120938
  return (toolCtx) => ({
120876
120939
  name: "work_finish",
@@ -120882,7 +120945,7 @@ function createWorkFinishTool(ctx) {
120882
120945
  properties: {
120883
120946
  channelId: { type: "string", description: "YOUR chat/group ID \u2014 the numeric ID of the chat you are in right now (e.g. '-1003844794417'). Do NOT guess; use the ID of the conversation this message came from." },
120884
120947
  role: { type: "string", enum: getAllRoleIds(), description: "Worker role" },
120885
- result: { type: "string", enum: ["done", "pass", "fail", "refine", "blocked", "approve", "reject"], description: "Completion result" },
120948
+ result: { type: "string", enum: ["done", "pass", "fail", "fail_infra", "refine", "blocked", "approve", "reject"], description: "Completion result. Use fail_infra (tester only) when the test toolchain is missing or broken \u2014 this keeps the issue in the test queue instead of routing it to the developer." },
120886
120949
  summary: { type: "string", description: "Brief summary" },
120887
120950
  prUrl: { type: "string", description: "PR/MR URL (auto-detected if omitted)" },
120888
120951
  createdTasks: {
@@ -120964,6 +121027,79 @@ function createWorkFinishTool(ctx) {
120964
121027
  })),
120965
121028
  keyTransitions: resolvedConfig.workflowMeta.keyTransitions
120966
121029
  });
121030
+ if (role === "tester" && result === "fail_infra") {
121031
+ const currentInfraFails = (issueRuntime?.infraFailCount ?? 0) + 1;
121032
+ await updateIssueRuntime(workspaceDir, project.slug, issueId, {
121033
+ infraFailCount: currentInfraFails
121034
+ });
121035
+ await log(workspaceDir, "infra_failure", {
121036
+ project: project.name,
121037
+ issue: issueId,
121038
+ role,
121039
+ result,
121040
+ summary: summary ?? null,
121041
+ infraFailCount: currentInfraFails
121042
+ });
121043
+ const notifyConfig = getNotificationConfig(ctx.pluginConfig);
121044
+ const target = resolveNotifyChannel([], project.channels);
121045
+ const issueUrl = `https://github.com/${project.repo}/issues/${issueId}`;
121046
+ await notify(
121047
+ {
121048
+ type: "infraFailure",
121049
+ project: project.name,
121050
+ issueId,
121051
+ issueUrl,
121052
+ summary: summary ?? "Infrastructure failure during testing",
121053
+ infraFailCount: currentInfraFails
121054
+ },
121055
+ {
121056
+ workspaceDir,
121057
+ config: notifyConfig,
121058
+ channelId: target?.channelId,
121059
+ channel: target?.channel ?? "telegram",
121060
+ runtime: ctx.runtime,
121061
+ accountId: target?.accountId,
121062
+ messageThreadId: target?.messageThreadId,
121063
+ runCommand: ctx.runCommand
121064
+ }
121065
+ ).catch((err) => {
121066
+ getRootLogger().warn(`infra_failure notification failed: ${err}`);
121067
+ });
121068
+ if (currentInfraFails >= INFRA_FAIL_CIRCUIT_BREAKER_THRESHOLD) {
121069
+ await log(workspaceDir, "infra_failure_circuit_breaker", {
121070
+ project: project.name,
121071
+ issue: issueId,
121072
+ infraFailCount: currentInfraFails
121073
+ });
121074
+ await resilientLabelTransition(provider, issueId, "Testing", "Refining");
121075
+ } else {
121076
+ await resilientLabelTransition(provider, issueId, "Testing", "To Test");
121077
+ }
121078
+ await deactivateWorker(workspaceDir, project.slug, "tester", {
121079
+ level: slotLevel,
121080
+ slotIndex,
121081
+ issueId: String(issueId)
121082
+ });
121083
+ await recordIssueLifecycle({
121084
+ workspaceDir,
121085
+ slug: project.slug,
121086
+ issueId,
121087
+ stage: "session_completed",
121088
+ sessionKey: toolCtx.sessionKey ?? null,
121089
+ details: { role, result, infraFailCount: currentInfraFails }
121090
+ }).catch(() => {
121091
+ });
121092
+ return jsonResult2({
121093
+ success: true,
121094
+ project: project.name,
121095
+ projectSlug: project.slug,
121096
+ issueId,
121097
+ role,
121098
+ result,
121099
+ infraFailCount: currentInfraFails,
121100
+ circuitBroken: currentInfraFails >= INFRA_FAIL_CIRCUIT_BREAKER_THRESHOLD
121101
+ });
121102
+ }
120967
121103
  if (!getRule(role, result, workflow))
120968
121104
  throw new Error(`Invalid completion: ${role}:${result}`);
120969
121105
  const repoPath = resolveRepoPath(project.repo);
@@ -121082,6 +121218,9 @@ function createWorkFinishTool(ctx) {
121082
121218
  details: { role, result }
121083
121219
  }).catch(() => {
121084
121220
  });
121221
+ if (role === "tester" && issueRuntime?.infraFailCount) {
121222
+ await updateIssueRuntime(workspaceDir, project.slug, issueId, { infraFailCount: 0 });
121223
+ }
121085
121224
  return jsonResult2({
121086
121225
  success: true,
121087
121226
  project: project.name,
@@ -124469,6 +124608,7 @@ init_audit();
124469
124608
  init_audit();
124470
124609
  init_workflow();
124471
124610
  init_context3();
124611
+ init_labels();
124472
124612
  var GRACE_PERIOD_MS = 5 * 60 * 1e3;
124473
124613
  var DISPATCH_CONFIRMATION_TIMEOUT_MS = 2 * 60 * 1e3;
124474
124614
  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".`;
@@ -124578,7 +124718,7 @@ async function checkWorkerHealth(opts) {
124578
124718
  async function revertLabel(fix, from, to) {
124579
124719
  if (!issueIdNum) return;
124580
124720
  try {
124581
- await provider.transitionLabel(issueIdNum, from, to);
124721
+ await resilientLabelTransition(provider, issueIdNum, from, to);
124582
124722
  fix.labelReverted = `${from} \u2192 ${to}`;
124583
124723
  } catch {
124584
124724
  fix.labelRevertFailed = true;
@@ -125131,7 +125271,7 @@ async function scanOrphanedLabels(opts) {
125131
125271
  queueLabel,
125132
125272
  workflow
125133
125273
  );
125134
- await provider.transitionLabel(issue2.iid, activeLabel, revertTarget);
125274
+ await resilientLabelTransition(provider, issue2.iid, activeLabel, revertTarget);
125135
125275
  fix.fixed = true;
125136
125276
  fix.labelReverted = `${activeLabel} \u2192 ${revertTarget}`;
125137
125277
  fix.issue.expectedLabel = revertTarget;
@@ -134220,6 +134360,23 @@ async function getLifecycleService(workspaceDir, logger6) {
134220
134360
  return created;
134221
134361
  }
134222
134362
 
134363
+ // lib/utils/async.ts
134364
+ async function raceWithTimeout(fn, timeoutMs, onTimeout) {
134365
+ let timer;
134366
+ const timeoutPromise = new Promise((resolve3) => {
134367
+ timer = setTimeout(() => {
134368
+ onTimeout();
134369
+ resolve3("timeout");
134370
+ }, timeoutMs);
134371
+ });
134372
+ try {
134373
+ const result = await Promise.race([fn(), timeoutPromise]);
134374
+ return result;
134375
+ } finally {
134376
+ clearTimeout(timer);
134377
+ }
134378
+ }
134379
+
134223
134380
  // lib/services/heartbeat/tick-runner.ts
134224
134381
  init_audit();
134225
134382
 
@@ -134509,6 +134666,7 @@ init_workflow();
134509
134666
  // lib/services/heartbeat/review.ts
134510
134667
  init_workflow();
134511
134668
  init_audit();
134669
+ init_labels();
134512
134670
  async function reviewPass(opts) {
134513
134671
  const rc = opts.runCommand;
134514
134672
  const { workspaceDir, projectName, workflow, provider, repoPath, gitPullTimeoutMs = 3e4, baseBranch, onMerge, onFeedback, onPrClosed } = opts;
@@ -134546,7 +134704,7 @@ async function reviewPass(opts) {
134546
134704
  const targetKey2 = typeof changesTransition === "string" ? changesTransition : changesTransition.target;
134547
134705
  const targetState2 = workflow.states[targetKey2];
134548
134706
  if (targetState2) {
134549
- await provider.transitionLabel(issue2.iid, state.label, targetState2.label);
134707
+ await resilientLabelTransition(provider, issue2.iid, state.label, targetState2.label);
134550
134708
  await log(workspaceDir, "review_transition", {
134551
134709
  project: projectName,
134552
134710
  issueId: issue2.iid,
@@ -134569,7 +134727,7 @@ async function reviewPass(opts) {
134569
134727
  const targetKey2 = typeof conflictTransition === "string" ? conflictTransition : conflictTransition.target;
134570
134728
  const targetState2 = workflow.states[targetKey2];
134571
134729
  if (targetState2) {
134572
- await provider.transitionLabel(issue2.iid, state.label, targetState2.label);
134730
+ await resilientLabelTransition(provider, issue2.iid, state.label, targetState2.label);
134573
134731
  await log(workspaceDir, "review_transition", {
134574
134732
  project: projectName,
134575
134733
  issueId: issue2.iid,
@@ -134601,7 +134759,7 @@ async function reviewPass(opts) {
134601
134759
  const closedActions = typeof closedTransition === "object" ? closedTransition.actions : void 0;
134602
134760
  const targetState2 = workflow.states[targetKey2];
134603
134761
  if (targetState2) {
134604
- await provider.transitionLabel(issue2.iid, state.label, targetState2.label);
134762
+ await resilientLabelTransition(provider, issue2.iid, state.label, targetState2.label);
134605
134763
  if (closedActions) {
134606
134764
  for (const action of closedActions) {
134607
134765
  switch (action) {
@@ -134716,7 +134874,7 @@ async function reviewPass(opts) {
134716
134874
  const failedKey = typeof failedTransition === "string" ? failedTransition : failedTransition.target;
134717
134875
  const failedState = workflow.states[failedKey];
134718
134876
  if (failedState) {
134719
- await provider.transitionLabel(issue2.iid, state.label, failedState.label);
134877
+ await resilientLabelTransition(provider, issue2.iid, state.label, failedState.label);
134720
134878
  await log(workspaceDir, "review_transition", {
134721
134879
  project: projectName,
134722
134880
  issueId: issue2.iid,
@@ -134761,7 +134919,7 @@ async function reviewPass(opts) {
134761
134919
  }
134762
134920
  }
134763
134921
  if (aborted2) continue;
134764
- await provider.transitionLabel(issue2.iid, state.label, targetState.label);
134922
+ await resilientLabelTransition(provider, issue2.iid, state.label, targetState.label);
134765
134923
  await log(workspaceDir, "review_transition", {
134766
134924
  project: projectName,
134767
134925
  issueId: issue2.iid,
@@ -134791,6 +134949,7 @@ async function reactToFeedbackComments(provider, issueId) {
134791
134949
  // lib/services/heartbeat/review-skip.ts
134792
134950
  init_workflow();
134793
134951
  init_audit();
134952
+ init_labels();
134794
134953
  async function reviewSkipPass(opts) {
134795
134954
  const rc = opts.runCommand;
134796
134955
  const { workspaceDir, projectName, workflow, provider, repoPath, gitPullTimeoutMs = 3e4, onMerge } = opts;
@@ -134874,7 +135033,7 @@ async function reviewSkipPass(opts) {
134874
135033
  const failedKey = typeof failedTransition === "string" ? failedTransition : failedTransition.target;
134875
135034
  const failedState = workflow.states[failedKey];
134876
135035
  if (failedState) {
134877
- await provider.transitionLabel(issue2.iid, state.label, failedState.label);
135036
+ await resilientLabelTransition(provider, issue2.iid, state.label, failedState.label);
134878
135037
  transitions++;
134879
135038
  }
134880
135039
  }
@@ -134917,7 +135076,7 @@ async function reviewSkipPass(opts) {
134917
135076
  }
134918
135077
  }
134919
135078
  if (aborted2) continue;
134920
- await provider.transitionLabel(issue2.iid, state.label, targetState.label);
135079
+ await resilientLabelTransition(provider, issue2.iid, state.label, targetState.label);
134921
135080
  await log(workspaceDir, "review_skip_transition", {
134922
135081
  project: projectName,
134923
135082
  issueId: issue2.iid,
@@ -134934,6 +135093,7 @@ async function reviewSkipPass(opts) {
134934
135093
  // lib/services/heartbeat/test-skip.ts
134935
135094
  init_workflow();
134936
135095
  init_audit();
135096
+ init_labels();
134937
135097
  async function testSkipPass(opts) {
134938
135098
  const { workspaceDir, projectName, workflow, provider, repoPath, gitPullTimeoutMs = 3e4, runCommand } = opts;
134939
135099
  let transitions = 0;
@@ -134999,7 +135159,7 @@ async function testSkipPass(opts) {
134999
135159
  const failedKey = typeof failedTransition === "string" ? failedTransition : failedTransition.target;
135000
135160
  const failedState = workflow.states[failedKey];
135001
135161
  if (failedState) {
135002
- await provider.transitionLabel(issue2.iid, state.label, failedState.label);
135162
+ await resilientLabelTransition(provider, issue2.iid, state.label, failedState.label);
135003
135163
  transitions++;
135004
135164
  }
135005
135165
  }
@@ -135044,7 +135204,7 @@ async function testSkipPass(opts) {
135044
135204
  }
135045
135205
  }
135046
135206
  if (aborted2) continue;
135047
- await provider.transitionLabel(issue2.iid, state.label, targetState.label);
135207
+ await resilientLabelTransition(provider, issue2.iid, state.label, targetState.label);
135048
135208
  await log(workspaceDir, "test_skip_transition", {
135049
135209
  project: projectName,
135050
135210
  issueId: issue2.iid,
@@ -135061,6 +135221,7 @@ async function testSkipPass(opts) {
135061
135221
  // lib/services/heartbeat/hold-escape.ts
135062
135222
  init_workflow();
135063
135223
  init_audit();
135224
+ init_labels();
135064
135225
  async function holdEscapePass(opts) {
135065
135226
  const { workspaceDir, projectName, workflow, provider } = opts;
135066
135227
  let transitions = 0;
@@ -135094,7 +135255,7 @@ async function holdEscapePass(opts) {
135094
135255
  }
135095
135256
  await provider.closeIssue(issue2.iid);
135096
135257
  await clearIssueRuntime(workspaceDir, project.slug, issue2.iid);
135097
- await provider.transitionLabel(issue2.iid, state.label, terminalState.label);
135258
+ await resilientLabelTransition(provider, issue2.iid, state.label, terminalState.label);
135098
135259
  await log(workspaceDir, "hold_escape_transition", {
135099
135260
  project: projectName,
135100
135261
  issueId: issue2.iid,
@@ -135958,21 +136119,6 @@ function registerHeartbeatService(api, pluginCtx) {
135958
136119
  }
135959
136120
  });
135960
136121
  }
135961
- async function raceWithTimeout(fn, timeoutMs, onTimeout) {
135962
- let timer;
135963
- const timeoutPromise = new Promise((resolve3) => {
135964
- timer = setTimeout(() => {
135965
- onTimeout();
135966
- resolve3("timeout");
135967
- }, timeoutMs);
135968
- });
135969
- try {
135970
- const result = await Promise.race([fn(), timeoutPromise]);
135971
- return result;
135972
- } finally {
135973
- clearTimeout(timer);
135974
- }
135975
- }
135976
136122
  var DEFAULT_TICK_TIMEOUT_MS = 5e4;
135977
136123
  var _ticksTimedOut = 0;
135978
136124
  async function withTickMutex(fn) {
@@ -136010,23 +136156,39 @@ async function runHeartbeatTick(ctx, logger6, mode) {
136010
136156
  })
136011
136157
  );
136012
136158
  const tickFn = lifecycle ? () => lifecycle.track(mode === "repair" ? "recovery" : "heartbeat", {}, run) : run;
136013
- let tickPromise;
136014
- const wrappedTickFn = () => {
136015
- tickPromise = tickFn();
136016
- return tickPromise;
136159
+ let resolveTick;
136160
+ let rejectTick;
136161
+ const tickPromise = new Promise((res, rej) => {
136162
+ resolveTick = res;
136163
+ rejectTick = rej;
136164
+ });
136165
+ tickPromise.catch(() => {
136166
+ });
136167
+ const wrappedTickFn = async () => {
136168
+ try {
136169
+ const result = await tickFn();
136170
+ resolveTick(result);
136171
+ return result;
136172
+ } catch (err) {
136173
+ rejectTick(err);
136174
+ throw err;
136175
+ }
136017
136176
  };
136177
+ const HARD_TICK_TIMEOUT_MS = 5 * 6e4;
136018
136178
  const raceResult = await raceWithTimeout(wrappedTickFn, DEFAULT_TICK_TIMEOUT_MS, () => {
136019
136179
  _ticksTimedOut++;
136020
136180
  timedOut = true;
136021
136181
  logger6.warn(`work_heartbeat ${mode} tick timed out after ${DEFAULT_TICK_TIMEOUT_MS}ms (total timeouts: ${_ticksTimedOut})`);
136022
- if (tickPromise) {
136023
- tickPromise.finally(() => {
136024
- _tickRunning[mode] = false;
136025
- _anyTickRunning = false;
136026
- });
136027
- } else {
136028
- logger6.error("tick_mutex: tickPromise undefined in timeout handler \u2014 this is a bug");
136029
- }
136182
+ const hardTimeout = setTimeout(() => {
136183
+ logger6.error("tick_mutex: hard timeout \u2014 forcing mutex release");
136184
+ _tickRunning[mode] = false;
136185
+ _anyTickRunning = false;
136186
+ }, HARD_TICK_TIMEOUT_MS);
136187
+ tickPromise.finally(() => {
136188
+ clearTimeout(hardTimeout);
136189
+ _tickRunning[mode] = false;
136190
+ _anyTickRunning = false;
136191
+ });
136030
136192
  });
136031
136193
  void raceResult;
136032
136194
  } catch (err) {
@@ -138256,6 +138418,14 @@ async function pathExists(candidate) {
138256
138418
  return false;
138257
138419
  }
138258
138420
  }
138421
+ async function isValidBinary(filePath) {
138422
+ try {
138423
+ const stat2 = await fs34.stat(filePath);
138424
+ return stat2.size > 0;
138425
+ } catch {
138426
+ return false;
138427
+ }
138428
+ }
138259
138429
  function familyForStack(stack) {
138260
138430
  if (NODE_STACKS.has(stack)) return "node";
138261
138431
  if (PYTHON_STACKS.has(stack)) return "python";
@@ -138368,7 +138538,7 @@ function buildPythonBootstrapPrelude() {
138368
138538
 
138369
138539
  # --- Shared toolchain (ruff, mypy, pip-audit) ---
138370
138540
  TOOLCHAIN="$HOME/.openclaw/toolchains/python"
138371
- if [ ! -x "$TOOLCHAIN/bin/ruff" ]; then
138541
+ if [ ! -x "$TOOLCHAIN/bin/ruff" ] || [ ! -s "$TOOLCHAIN/bin/ruff" ]; then
138372
138542
  echo "[qa] Toolchain not found \u2014 provisioning..."
138373
138543
  command -v uv >/dev/null 2>&1 || {
138374
138544
  curl -LsSf https://astral.sh/uv/install.sh | sh
@@ -138506,16 +138676,22 @@ Install manually: curl -LsSf ${UV_INSTALL_URL} | sh`
138506
138676
  var PYTHON_TOOLCHAIN_PACKAGES = ["ruff", "mypy", "pip-audit"];
138507
138677
  var TOOLCHAIN_DIR = ".openclaw/toolchains/python";
138508
138678
  var TOOLCHAIN_FINGERPRINT_FILE = "toolchain.sha256";
138509
- function toolchainFingerprint() {
138510
- return createHash4("sha256").update(PYTHON_TOOLCHAIN_PACKAGES.join(",")).digest("hex");
138679
+ async function toolchainFingerprint(runCommand) {
138680
+ let pythonVersion = "unknown";
138681
+ try {
138682
+ const result = await runCommand("python3", ["--version"], { timeout: 5e3 });
138683
+ if (result.exitCode === 0) pythonVersion = result.stdout.trim();
138684
+ } catch {
138685
+ }
138686
+ return createHash4("sha256").update(PYTHON_TOOLCHAIN_PACKAGES.join(",") + ":" + pythonVersion).digest("hex");
138511
138687
  }
138512
138688
  async function ensurePythonToolchain(runCommand, homeDir) {
138513
138689
  const home = homeDir ?? process.env.HOME ?? "/tmp";
138514
138690
  const toolchainPath = path34.join(home, TOOLCHAIN_DIR);
138515
138691
  const ruffPath = path34.join(toolchainPath, "bin", "ruff");
138516
138692
  const fingerprintPath = path34.join(toolchainPath, TOOLCHAIN_FINGERPRINT_FILE);
138517
- const expectedFp = toolchainFingerprint();
138518
- if (await pathExists(ruffPath)) {
138693
+ const expectedFp = await toolchainFingerprint(runCommand);
138694
+ if (await isValidBinary(ruffPath)) {
138519
138695
  try {
138520
138696
  const currentFp = (await fs34.readFile(fingerprintPath, "utf-8")).trim();
138521
138697
  if (currentFp === expectedFp) {
@@ -139112,17 +139288,6 @@ var scaffoldStep = {
139112
139288
  mode: "scaffold",
139113
139289
  runCommand: ctx.runCommand
139114
139290
  });
139115
- if (!bootstrap.ready) {
139116
- ctx.log(`Scaffold bootstrap failed: ${bootstrap.reason ?? "unknown reason"}`);
139117
- return {
139118
- ...result.plannedPayload,
139119
- step: "scaffold",
139120
- scaffold: { created: false, reason: bootstrap.reason ?? "bootstrap_failed" }
139121
- };
139122
- }
139123
- ctx.log(
139124
- bootstrap.skipped ? `Scaffold bootstrap already current (${bootstrap.packageManager})` : `Scaffold bootstrap completed (${bootstrap.packageManager})`
139125
- );
139126
139291
  if (scaffold.stack && PYTHON_STACKS2.has(scaffold.stack) && payload.spec) {
139127
139292
  try {
139128
139293
  const contract = generateQaContract({
@@ -139137,6 +139302,17 @@ var scaffoldStep = {
139137
139302
  ctx.log(`Warning: could not write qa.sh: ${err instanceof Error ? err.message : String(err)}`);
139138
139303
  }
139139
139304
  }
139305
+ if (!bootstrap.ready) {
139306
+ ctx.log(`Scaffold bootstrap failed: ${bootstrap.reason ?? "unknown reason"}`);
139307
+ return {
139308
+ ...result.plannedPayload,
139309
+ step: "scaffold",
139310
+ scaffold: { created: false, reason: bootstrap.reason ?? "bootstrap_failed" }
139311
+ };
139312
+ }
139313
+ ctx.log(
139314
+ bootstrap.skipped ? `Scaffold bootstrap already current (${bootstrap.packageManager})` : `Scaffold bootstrap completed (${bootstrap.packageManager})`
139315
+ );
139140
139316
  }
139141
139317
  return {
139142
139318
  ...result.plannedPayload,
@@ -142147,6 +142323,7 @@ function shouldSuppressTelegramBootstrapReply(session, request2) {
142147
142323
  }
142148
142324
 
142149
142325
  // lib/dispatch/telegram-bootstrap-hook.ts
142326
+ var BOOTSTRAP_TIMEOUT_MS = 5 * 6e4;
142150
142327
  var BOOTSTRAP_MESSAGES = {
142151
142328
  ack: {
142152
142329
  pt: "Recebi! Vou analisar e come\xE7ar a montar o projeto...",
@@ -142416,8 +142593,24 @@ async function classifyAndBootstrap(ctx, workspaceDir, conversationId, content)
142416
142593
  await sendTelegramText(ctx, conversationId, buildClarificationMessage(parsed, pendingClarification, language));
142417
142594
  return;
142418
142595
  }
142419
- continueBootstrap(ctx, conversationId, workspaceDir, incomingRequest, sourceRoute).catch((err) => {
142420
- logBootstrapWarning(ctx, `[telegram-bootstrap] unhandled pipeline error (LLM path): ${err instanceof Error ? err.message : String(err)}`);
142596
+ bootstrapWithTimeout(ctx, conversationId, workspaceDir, incomingRequest, sourceRoute);
142597
+ }
142598
+ function bootstrapWithTimeout(ctx, conversationId, workspaceDir, request2, sourceRoute) {
142599
+ raceWithTimeout(
142600
+ () => continueBootstrap(ctx, conversationId, workspaceDir, request2, sourceRoute),
142601
+ BOOTSTRAP_TIMEOUT_MS,
142602
+ () => {
142603
+ ctx.logger.warn({ conversationId }, "Bootstrap pipeline timed out after 5 minutes");
142604
+ upsertTelegramBootstrapSession(workspaceDir, {
142605
+ conversationId,
142606
+ rawIdea: request2.rawIdea,
142607
+ status: "failed",
142608
+ error: "Pipeline timeout (5min)"
142609
+ }).catch(() => {
142610
+ });
142611
+ }
142612
+ ).catch((err) => {
142613
+ logBootstrapWarning(ctx, `[telegram-bootstrap] pipeline error: ${err instanceof Error ? err.message : String(err)}`);
142421
142614
  });
142422
142615
  }
142423
142616
  async function continueBootstrap(ctx, conversationId, workspaceDir, request2, sourceRoute) {
@@ -142432,6 +142625,24 @@ async function continueBootstrap(ctx, conversationId, workspaceDir, request2, so
142432
142625
  }
142433
142626
  const projectName = request2.projectName ?? void 0;
142434
142627
  const stackHint = request2.stackHint;
142628
+ if (!stackHint) {
142629
+ const existingSession = await readTelegramBootstrapSession(workspaceDir, conversationId);
142630
+ const lang = existingSession?.language ?? "pt";
142631
+ await upsertTelegramBootstrapSession(workspaceDir, {
142632
+ conversationId,
142633
+ rawIdea: request2.rawIdea,
142634
+ projectName: request2.projectName ?? void 0,
142635
+ status: "clarifying",
142636
+ pendingClarification: "stack",
142637
+ language: lang
142638
+ });
142639
+ await sendTelegramText(ctx, conversationId, buildClarificationMessage(
142640
+ { rawIdea: request2.rawIdea, projectName: request2.projectName ?? void 0, stackHint: request2.stackHint ?? void 0 },
142641
+ "stack",
142642
+ lang
142643
+ ));
142644
+ return;
142645
+ }
142435
142646
  const candidateSlug = inferProjectSlug(projectName ?? request2.rawIdea);
142436
142647
  if (candidateSlug) {
142437
142648
  const projects = await readProjects(workspaceDir).catch(() => null);
@@ -142680,11 +142891,9 @@ function registerTelegramBootstrapHook(api, ctx) {
142680
142891
  repoPath: existingSession.repoPath ?? null
142681
142892
  };
142682
142893
  ctx.logger.info(`[telegram-bootstrap] clarification resolved: stack=${mergedRequest.stackHint}, idea="${mergedRequest.rawIdea}" (conversation: ${conversationId})`);
142683
- continueBootstrap(ctx, conversationId, workspaceDir, mergedRequest, existingSession.sourceRoute ?? {
142894
+ bootstrapWithTimeout(ctx, conversationId, workspaceDir, mergedRequest, existingSession.sourceRoute ?? {
142684
142895
  channel: "telegram",
142685
142896
  channelId: conversationId
142686
- }).catch((err) => {
142687
- logBootstrapWarning(ctx, `[telegram-bootstrap] unhandled pipeline error: ${err instanceof Error ? err.message : String(err)}`);
142688
142897
  });
142689
142898
  return;
142690
142899
  }
@@ -142752,11 +142961,9 @@ function registerTelegramBootstrapHook(api, ctx) {
142752
142961
  await sendTelegramText(ctx, conversationId, buildClarificationMessage(parsed, pendingClarification, language));
142753
142962
  return;
142754
142963
  }
142755
- continueBootstrap(ctx, conversationId, workspaceDir, incomingRequest, {
142964
+ bootstrapWithTimeout(ctx, conversationId, workspaceDir, incomingRequest, {
142756
142965
  channel: "telegram",
142757
142966
  channelId: conversationId
142758
- }).catch((err) => {
142759
- logBootstrapWarning(ctx, `[telegram-bootstrap] unhandled pipeline error: ${err instanceof Error ? err.message : String(err)}`);
142760
142967
  });
142761
142968
  });
142762
142969
  }