@mestreyoda/fabrica 0.1.11 → 0.1.12

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
@@ -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.12") {
111333
+ return "0.1.12";
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
  }
@@ -120056,7 +120098,11 @@ async function persistMergedArtifact(opts) {
120056
120098
  },
120057
120099
  currentPrState: PrState.MERGED,
120058
120100
  followUpPrRequired: false
120059
- }).catch(() => {
120101
+ }).catch((err) => {
120102
+ console.warn(
120103
+ JSON.stringify({ projectSlug, issueId, prNumber, error: String(err) }),
120104
+ "persistMergedArtifact failed \u2014 issue close guard may not find merge evidence"
120105
+ );
120060
120106
  });
120061
120107
  }
120062
120108
  async function guardedCloseIssue(opts) {
@@ -124469,6 +124515,7 @@ init_audit();
124469
124515
  init_audit();
124470
124516
  init_workflow();
124471
124517
  init_context3();
124518
+ init_labels();
124472
124519
  var GRACE_PERIOD_MS = 5 * 60 * 1e3;
124473
124520
  var DISPATCH_CONFIRMATION_TIMEOUT_MS = 2 * 60 * 1e3;
124474
124521
  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 +124625,7 @@ async function checkWorkerHealth(opts) {
124578
124625
  async function revertLabel(fix, from, to) {
124579
124626
  if (!issueIdNum) return;
124580
124627
  try {
124581
- await provider.transitionLabel(issueIdNum, from, to);
124628
+ await resilientLabelTransition(provider, issueIdNum, from, to);
124582
124629
  fix.labelReverted = `${from} \u2192 ${to}`;
124583
124630
  } catch {
124584
124631
  fix.labelRevertFailed = true;
@@ -125131,7 +125178,7 @@ async function scanOrphanedLabels(opts) {
125131
125178
  queueLabel,
125132
125179
  workflow
125133
125180
  );
125134
- await provider.transitionLabel(issue2.iid, activeLabel, revertTarget);
125181
+ await resilientLabelTransition(provider, issue2.iid, activeLabel, revertTarget);
125135
125182
  fix.fixed = true;
125136
125183
  fix.labelReverted = `${activeLabel} \u2192 ${revertTarget}`;
125137
125184
  fix.issue.expectedLabel = revertTarget;
@@ -134220,6 +134267,23 @@ async function getLifecycleService(workspaceDir, logger6) {
134220
134267
  return created;
134221
134268
  }
134222
134269
 
134270
+ // lib/utils/async.ts
134271
+ async function raceWithTimeout(fn, timeoutMs, onTimeout) {
134272
+ let timer;
134273
+ const timeoutPromise = new Promise((resolve3) => {
134274
+ timer = setTimeout(() => {
134275
+ onTimeout();
134276
+ resolve3("timeout");
134277
+ }, timeoutMs);
134278
+ });
134279
+ try {
134280
+ const result = await Promise.race([fn(), timeoutPromise]);
134281
+ return result;
134282
+ } finally {
134283
+ clearTimeout(timer);
134284
+ }
134285
+ }
134286
+
134223
134287
  // lib/services/heartbeat/tick-runner.ts
134224
134288
  init_audit();
134225
134289
 
@@ -134509,6 +134573,7 @@ init_workflow();
134509
134573
  // lib/services/heartbeat/review.ts
134510
134574
  init_workflow();
134511
134575
  init_audit();
134576
+ init_labels();
134512
134577
  async function reviewPass(opts) {
134513
134578
  const rc = opts.runCommand;
134514
134579
  const { workspaceDir, projectName, workflow, provider, repoPath, gitPullTimeoutMs = 3e4, baseBranch, onMerge, onFeedback, onPrClosed } = opts;
@@ -134546,7 +134611,7 @@ async function reviewPass(opts) {
134546
134611
  const targetKey2 = typeof changesTransition === "string" ? changesTransition : changesTransition.target;
134547
134612
  const targetState2 = workflow.states[targetKey2];
134548
134613
  if (targetState2) {
134549
- await provider.transitionLabel(issue2.iid, state.label, targetState2.label);
134614
+ await resilientLabelTransition(provider, issue2.iid, state.label, targetState2.label);
134550
134615
  await log(workspaceDir, "review_transition", {
134551
134616
  project: projectName,
134552
134617
  issueId: issue2.iid,
@@ -134569,7 +134634,7 @@ async function reviewPass(opts) {
134569
134634
  const targetKey2 = typeof conflictTransition === "string" ? conflictTransition : conflictTransition.target;
134570
134635
  const targetState2 = workflow.states[targetKey2];
134571
134636
  if (targetState2) {
134572
- await provider.transitionLabel(issue2.iid, state.label, targetState2.label);
134637
+ await resilientLabelTransition(provider, issue2.iid, state.label, targetState2.label);
134573
134638
  await log(workspaceDir, "review_transition", {
134574
134639
  project: projectName,
134575
134640
  issueId: issue2.iid,
@@ -134601,7 +134666,7 @@ async function reviewPass(opts) {
134601
134666
  const closedActions = typeof closedTransition === "object" ? closedTransition.actions : void 0;
134602
134667
  const targetState2 = workflow.states[targetKey2];
134603
134668
  if (targetState2) {
134604
- await provider.transitionLabel(issue2.iid, state.label, targetState2.label);
134669
+ await resilientLabelTransition(provider, issue2.iid, state.label, targetState2.label);
134605
134670
  if (closedActions) {
134606
134671
  for (const action of closedActions) {
134607
134672
  switch (action) {
@@ -134716,7 +134781,7 @@ async function reviewPass(opts) {
134716
134781
  const failedKey = typeof failedTransition === "string" ? failedTransition : failedTransition.target;
134717
134782
  const failedState = workflow.states[failedKey];
134718
134783
  if (failedState) {
134719
- await provider.transitionLabel(issue2.iid, state.label, failedState.label);
134784
+ await resilientLabelTransition(provider, issue2.iid, state.label, failedState.label);
134720
134785
  await log(workspaceDir, "review_transition", {
134721
134786
  project: projectName,
134722
134787
  issueId: issue2.iid,
@@ -134761,7 +134826,7 @@ async function reviewPass(opts) {
134761
134826
  }
134762
134827
  }
134763
134828
  if (aborted2) continue;
134764
- await provider.transitionLabel(issue2.iid, state.label, targetState.label);
134829
+ await resilientLabelTransition(provider, issue2.iid, state.label, targetState.label);
134765
134830
  await log(workspaceDir, "review_transition", {
134766
134831
  project: projectName,
134767
134832
  issueId: issue2.iid,
@@ -134791,6 +134856,7 @@ async function reactToFeedbackComments(provider, issueId) {
134791
134856
  // lib/services/heartbeat/review-skip.ts
134792
134857
  init_workflow();
134793
134858
  init_audit();
134859
+ init_labels();
134794
134860
  async function reviewSkipPass(opts) {
134795
134861
  const rc = opts.runCommand;
134796
134862
  const { workspaceDir, projectName, workflow, provider, repoPath, gitPullTimeoutMs = 3e4, onMerge } = opts;
@@ -134874,7 +134940,7 @@ async function reviewSkipPass(opts) {
134874
134940
  const failedKey = typeof failedTransition === "string" ? failedTransition : failedTransition.target;
134875
134941
  const failedState = workflow.states[failedKey];
134876
134942
  if (failedState) {
134877
- await provider.transitionLabel(issue2.iid, state.label, failedState.label);
134943
+ await resilientLabelTransition(provider, issue2.iid, state.label, failedState.label);
134878
134944
  transitions++;
134879
134945
  }
134880
134946
  }
@@ -134917,7 +134983,7 @@ async function reviewSkipPass(opts) {
134917
134983
  }
134918
134984
  }
134919
134985
  if (aborted2) continue;
134920
- await provider.transitionLabel(issue2.iid, state.label, targetState.label);
134986
+ await resilientLabelTransition(provider, issue2.iid, state.label, targetState.label);
134921
134987
  await log(workspaceDir, "review_skip_transition", {
134922
134988
  project: projectName,
134923
134989
  issueId: issue2.iid,
@@ -134934,6 +135000,7 @@ async function reviewSkipPass(opts) {
134934
135000
  // lib/services/heartbeat/test-skip.ts
134935
135001
  init_workflow();
134936
135002
  init_audit();
135003
+ init_labels();
134937
135004
  async function testSkipPass(opts) {
134938
135005
  const { workspaceDir, projectName, workflow, provider, repoPath, gitPullTimeoutMs = 3e4, runCommand } = opts;
134939
135006
  let transitions = 0;
@@ -134999,7 +135066,7 @@ async function testSkipPass(opts) {
134999
135066
  const failedKey = typeof failedTransition === "string" ? failedTransition : failedTransition.target;
135000
135067
  const failedState = workflow.states[failedKey];
135001
135068
  if (failedState) {
135002
- await provider.transitionLabel(issue2.iid, state.label, failedState.label);
135069
+ await resilientLabelTransition(provider, issue2.iid, state.label, failedState.label);
135003
135070
  transitions++;
135004
135071
  }
135005
135072
  }
@@ -135044,7 +135111,7 @@ async function testSkipPass(opts) {
135044
135111
  }
135045
135112
  }
135046
135113
  if (aborted2) continue;
135047
- await provider.transitionLabel(issue2.iid, state.label, targetState.label);
135114
+ await resilientLabelTransition(provider, issue2.iid, state.label, targetState.label);
135048
135115
  await log(workspaceDir, "test_skip_transition", {
135049
135116
  project: projectName,
135050
135117
  issueId: issue2.iid,
@@ -135061,6 +135128,7 @@ async function testSkipPass(opts) {
135061
135128
  // lib/services/heartbeat/hold-escape.ts
135062
135129
  init_workflow();
135063
135130
  init_audit();
135131
+ init_labels();
135064
135132
  async function holdEscapePass(opts) {
135065
135133
  const { workspaceDir, projectName, workflow, provider } = opts;
135066
135134
  let transitions = 0;
@@ -135094,7 +135162,7 @@ async function holdEscapePass(opts) {
135094
135162
  }
135095
135163
  await provider.closeIssue(issue2.iid);
135096
135164
  await clearIssueRuntime(workspaceDir, project.slug, issue2.iid);
135097
- await provider.transitionLabel(issue2.iid, state.label, terminalState.label);
135165
+ await resilientLabelTransition(provider, issue2.iid, state.label, terminalState.label);
135098
135166
  await log(workspaceDir, "hold_escape_transition", {
135099
135167
  project: projectName,
135100
135168
  issueId: issue2.iid,
@@ -135958,21 +136026,6 @@ function registerHeartbeatService(api, pluginCtx) {
135958
136026
  }
135959
136027
  });
135960
136028
  }
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
136029
  var DEFAULT_TICK_TIMEOUT_MS = 5e4;
135977
136030
  var _ticksTimedOut = 0;
135978
136031
  async function withTickMutex(fn) {
@@ -136025,7 +136078,9 @@ async function runHeartbeatTick(ctx, logger6, mode) {
136025
136078
  _anyTickRunning = false;
136026
136079
  });
136027
136080
  } else {
136028
- logger6.error("tick_mutex: tickPromise undefined in timeout handler \u2014 this is a bug");
136081
+ logger6.error("tick_mutex: tickPromise undefined in timeout handler \u2014 forcing mutex release");
136082
+ _tickRunning[mode] = false;
136083
+ _anyTickRunning = false;
136029
136084
  }
136030
136085
  });
136031
136086
  void raceResult;
@@ -138256,6 +138311,14 @@ async function pathExists(candidate) {
138256
138311
  return false;
138257
138312
  }
138258
138313
  }
138314
+ async function isValidBinary(filePath) {
138315
+ try {
138316
+ const stat2 = await fs34.stat(filePath);
138317
+ return stat2.size > 0;
138318
+ } catch {
138319
+ return false;
138320
+ }
138321
+ }
138259
138322
  function familyForStack(stack) {
138260
138323
  if (NODE_STACKS.has(stack)) return "node";
138261
138324
  if (PYTHON_STACKS.has(stack)) return "python";
@@ -138368,7 +138431,7 @@ function buildPythonBootstrapPrelude() {
138368
138431
 
138369
138432
  # --- Shared toolchain (ruff, mypy, pip-audit) ---
138370
138433
  TOOLCHAIN="$HOME/.openclaw/toolchains/python"
138371
- if [ ! -x "$TOOLCHAIN/bin/ruff" ]; then
138434
+ if [ ! -x "$TOOLCHAIN/bin/ruff" ] || [ ! -s "$TOOLCHAIN/bin/ruff" ]; then
138372
138435
  echo "[qa] Toolchain not found \u2014 provisioning..."
138373
138436
  command -v uv >/dev/null 2>&1 || {
138374
138437
  curl -LsSf https://astral.sh/uv/install.sh | sh
@@ -138515,7 +138578,7 @@ async function ensurePythonToolchain(runCommand, homeDir) {
138515
138578
  const ruffPath = path34.join(toolchainPath, "bin", "ruff");
138516
138579
  const fingerprintPath = path34.join(toolchainPath, TOOLCHAIN_FINGERPRINT_FILE);
138517
138580
  const expectedFp = toolchainFingerprint();
138518
- if (await pathExists(ruffPath)) {
138581
+ if (await isValidBinary(ruffPath)) {
138519
138582
  try {
138520
138583
  const currentFp = (await fs34.readFile(fingerprintPath, "utf-8")).trim();
138521
138584
  if (currentFp === expectedFp) {
@@ -139112,17 +139175,6 @@ var scaffoldStep = {
139112
139175
  mode: "scaffold",
139113
139176
  runCommand: ctx.runCommand
139114
139177
  });
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
139178
  if (scaffold.stack && PYTHON_STACKS2.has(scaffold.stack) && payload.spec) {
139127
139179
  try {
139128
139180
  const contract = generateQaContract({
@@ -139137,6 +139189,17 @@ var scaffoldStep = {
139137
139189
  ctx.log(`Warning: could not write qa.sh: ${err instanceof Error ? err.message : String(err)}`);
139138
139190
  }
139139
139191
  }
139192
+ if (!bootstrap.ready) {
139193
+ ctx.log(`Scaffold bootstrap failed: ${bootstrap.reason ?? "unknown reason"}`);
139194
+ return {
139195
+ ...result.plannedPayload,
139196
+ step: "scaffold",
139197
+ scaffold: { created: false, reason: bootstrap.reason ?? "bootstrap_failed" }
139198
+ };
139199
+ }
139200
+ ctx.log(
139201
+ bootstrap.skipped ? `Scaffold bootstrap already current (${bootstrap.packageManager})` : `Scaffold bootstrap completed (${bootstrap.packageManager})`
139202
+ );
139140
139203
  }
139141
139204
  return {
139142
139205
  ...result.plannedPayload,
@@ -142147,6 +142210,7 @@ function shouldSuppressTelegramBootstrapReply(session, request2) {
142147
142210
  }
142148
142211
 
142149
142212
  // lib/dispatch/telegram-bootstrap-hook.ts
142213
+ var BOOTSTRAP_TIMEOUT_MS = 5 * 6e4;
142150
142214
  var BOOTSTRAP_MESSAGES = {
142151
142215
  ack: {
142152
142216
  pt: "Recebi! Vou analisar e come\xE7ar a montar o projeto...",
@@ -142416,8 +142480,24 @@ async function classifyAndBootstrap(ctx, workspaceDir, conversationId, content)
142416
142480
  await sendTelegramText(ctx, conversationId, buildClarificationMessage(parsed, pendingClarification, language));
142417
142481
  return;
142418
142482
  }
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)}`);
142483
+ bootstrapWithTimeout(ctx, conversationId, workspaceDir, incomingRequest, sourceRoute);
142484
+ }
142485
+ function bootstrapWithTimeout(ctx, conversationId, workspaceDir, request2, sourceRoute) {
142486
+ raceWithTimeout(
142487
+ () => continueBootstrap(ctx, conversationId, workspaceDir, request2, sourceRoute),
142488
+ BOOTSTRAP_TIMEOUT_MS,
142489
+ () => {
142490
+ ctx.logger.warn({ conversationId }, "Bootstrap pipeline timed out after 5 minutes");
142491
+ upsertTelegramBootstrapSession(workspaceDir, {
142492
+ conversationId,
142493
+ rawIdea: request2.rawIdea,
142494
+ status: "failed",
142495
+ error: "Pipeline timeout (5min)"
142496
+ }).catch(() => {
142497
+ });
142498
+ }
142499
+ ).catch((err) => {
142500
+ logBootstrapWarning(ctx, `[telegram-bootstrap] pipeline error: ${err instanceof Error ? err.message : String(err)}`);
142421
142501
  });
142422
142502
  }
142423
142503
  async function continueBootstrap(ctx, conversationId, workspaceDir, request2, sourceRoute) {
@@ -142432,6 +142512,24 @@ async function continueBootstrap(ctx, conversationId, workspaceDir, request2, so
142432
142512
  }
142433
142513
  const projectName = request2.projectName ?? void 0;
142434
142514
  const stackHint = request2.stackHint;
142515
+ if (!stackHint) {
142516
+ const existingSession = await readTelegramBootstrapSession(workspaceDir, conversationId);
142517
+ const lang = existingSession?.language ?? "pt";
142518
+ await upsertTelegramBootstrapSession(workspaceDir, {
142519
+ conversationId,
142520
+ rawIdea: request2.rawIdea,
142521
+ projectName: request2.projectName ?? void 0,
142522
+ status: "clarifying",
142523
+ pendingClarification: "stack",
142524
+ language: lang
142525
+ });
142526
+ await sendTelegramText(ctx, conversationId, buildClarificationMessage(
142527
+ { rawIdea: request2.rawIdea, projectName: request2.projectName ?? void 0, stackHint: request2.stackHint ?? void 0 },
142528
+ "stack",
142529
+ lang
142530
+ ));
142531
+ return;
142532
+ }
142435
142533
  const candidateSlug = inferProjectSlug(projectName ?? request2.rawIdea);
142436
142534
  if (candidateSlug) {
142437
142535
  const projects = await readProjects(workspaceDir).catch(() => null);
@@ -142680,11 +142778,9 @@ function registerTelegramBootstrapHook(api, ctx) {
142680
142778
  repoPath: existingSession.repoPath ?? null
142681
142779
  };
142682
142780
  ctx.logger.info(`[telegram-bootstrap] clarification resolved: stack=${mergedRequest.stackHint}, idea="${mergedRequest.rawIdea}" (conversation: ${conversationId})`);
142683
- continueBootstrap(ctx, conversationId, workspaceDir, mergedRequest, existingSession.sourceRoute ?? {
142781
+ bootstrapWithTimeout(ctx, conversationId, workspaceDir, mergedRequest, existingSession.sourceRoute ?? {
142684
142782
  channel: "telegram",
142685
142783
  channelId: conversationId
142686
- }).catch((err) => {
142687
- logBootstrapWarning(ctx, `[telegram-bootstrap] unhandled pipeline error: ${err instanceof Error ? err.message : String(err)}`);
142688
142784
  });
142689
142785
  return;
142690
142786
  }
@@ -142752,11 +142848,9 @@ function registerTelegramBootstrapHook(api, ctx) {
142752
142848
  await sendTelegramText(ctx, conversationId, buildClarificationMessage(parsed, pendingClarification, language));
142753
142849
  return;
142754
142850
  }
142755
- continueBootstrap(ctx, conversationId, workspaceDir, incomingRequest, {
142851
+ bootstrapWithTimeout(ctx, conversationId, workspaceDir, incomingRequest, {
142756
142852
  channel: "telegram",
142757
142853
  channelId: conversationId
142758
- }).catch((err) => {
142759
- logBootstrapWarning(ctx, `[telegram-bootstrap] unhandled pipeline error: ${err instanceof Error ? err.message : String(err)}`);
142760
142854
  });
142761
142855
  });
142762
142856
  }