@mestreyoda/fabrica 0.2.2 → 0.2.6

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
@@ -110821,7 +110821,7 @@ var init_registry = __esm({
110821
110821
  });
110822
110822
 
110823
110823
  // lib/config/merge.ts
110824
- function mergeConfig(base, overlay) {
110824
+ function mergeConfig(base, overlay, traceOpts) {
110825
110825
  const merged = {};
110826
110826
  if (base.roles || overlay.roles) {
110827
110827
  merged.roles = { ...base.roles };
@@ -110872,6 +110872,44 @@ function mergeConfig(base, overlay) {
110872
110872
  } : void 0
110873
110873
  };
110874
110874
  }
110875
+ if (traceOpts) {
110876
+ const { baseLabel, overlayLabel } = traceOpts;
110877
+ const trace2 = {};
110878
+ if (merged.workflow) {
110879
+ for (const key of ["initial", "reviewPolicy", "testPolicy", "roleExecution", "maxWorkersPerLevel"]) {
110880
+ if (merged.workflow[key] !== void 0) {
110881
+ const fromOverlay = overlay.workflow?.[key] !== void 0;
110882
+ trace2[`workflow.${key}`] = fromOverlay ? overlayLabel : baseLabel;
110883
+ }
110884
+ }
110885
+ }
110886
+ if (merged.timeouts) {
110887
+ for (const [key, value] of Object.entries(merged.timeouts)) {
110888
+ if (value !== void 0) {
110889
+ const fromOverlay = overlay.timeouts?.[key] !== void 0;
110890
+ trace2[`timeouts.${key}`] = fromOverlay ? overlayLabel : baseLabel;
110891
+ }
110892
+ }
110893
+ }
110894
+ if (merged.roles) {
110895
+ for (const [roleId, roleValue] of Object.entries(merged.roles)) {
110896
+ if (roleValue === false) {
110897
+ trace2[`roles.${roleId}`] = overlay.roles?.[roleId] === false ? overlayLabel : baseLabel;
110898
+ continue;
110899
+ }
110900
+ if (typeof roleValue === "object") {
110901
+ for (const key of ["defaultLevel", "levels", "completionResults"]) {
110902
+ if (roleValue[key] !== void 0) {
110903
+ const overlayRole = overlay.roles?.[roleId];
110904
+ const fromOverlay = typeof overlayRole === "object" && overlayRole?.[key] !== void 0;
110905
+ trace2[`roles.${roleId}.${key}`] = fromOverlay ? overlayLabel : baseLabel;
110906
+ }
110907
+ }
110908
+ }
110909
+ }
110910
+ }
110911
+ return Object.assign(merged, { _trace: trace2 });
110912
+ }
110875
110913
  return merged;
110876
110914
  }
110877
110915
  function mergeWorkflowStates(base, overlay) {
@@ -110917,42 +110955,10 @@ function mergeRoleOverride(base, overlay) {
110917
110955
  };
110918
110956
  }
110919
110957
  function mergeConfigWithTrace(base, overlay, baseLabel, overlayLabel) {
110920
- const merged = mergeConfig(base, overlay);
110921
- const trace2 = {};
110922
- if (merged.workflow) {
110923
- for (const key of ["initial", "reviewPolicy", "testPolicy", "roleExecution", "maxWorkersPerLevel"]) {
110924
- if (merged.workflow[key] !== void 0) {
110925
- const fromOverlay = overlay.workflow?.[key] !== void 0;
110926
- trace2[`workflow.${key}`] = fromOverlay ? overlayLabel : baseLabel;
110927
- }
110928
- }
110929
- }
110930
- if (merged.timeouts) {
110931
- for (const [key, value] of Object.entries(merged.timeouts)) {
110932
- if (value !== void 0) {
110933
- const fromOverlay = overlay.timeouts?.[key] !== void 0;
110934
- trace2[`timeouts.${key}`] = fromOverlay ? overlayLabel : baseLabel;
110935
- }
110936
- }
110937
- }
110938
- if (merged.roles) {
110939
- for (const [roleId, roleValue] of Object.entries(merged.roles)) {
110940
- if (roleValue === false) {
110941
- trace2[`roles.${roleId}`] = overlay.roles?.[roleId] === false ? overlayLabel : baseLabel;
110942
- continue;
110943
- }
110944
- if (typeof roleValue === "object") {
110945
- for (const key of ["defaultLevel", "levels", "completionResults"]) {
110946
- if (roleValue[key] !== void 0) {
110947
- const overlayRole = overlay.roles?.[roleId];
110948
- const fromOverlay = typeof overlayRole === "object" && overlayRole?.[key] !== void 0;
110949
- trace2[`roles.${roleId}.${key}`] = fromOverlay ? overlayLabel : baseLabel;
110950
- }
110951
- }
110952
- }
110953
- }
110954
- }
110955
- return { merged, trace: trace2 };
110958
+ const result = mergeConfig(base, overlay, { baseLabel, overlayLabel });
110959
+ const trace2 = result._trace ?? {};
110960
+ delete result._trace;
110961
+ return { merged: result, trace: trace2 };
110956
110962
  }
110957
110963
  var init_merge = __esm({
110958
110964
  "lib/config/merge.ts"() {
@@ -111324,8 +111330,8 @@ import fsSync from "node:fs";
111324
111330
  import path5 from "node:path";
111325
111331
  import { fileURLToPath as fileURLToPath3 } from "node:url";
111326
111332
  function getCurrentVersion() {
111327
- if ("0.2.2") {
111328
- return "0.2.2";
111333
+ if ("0.2.6") {
111334
+ return "0.2.6";
111329
111335
  }
111330
111336
  try {
111331
111337
  const pkgPath = path5.join(THIS_DIR, "..", "..", "package.json");
@@ -117132,6 +117138,8 @@ var GitHubRateLimitError = class extends Error {
117132
117138
  this.name = "GitHubRateLimitError";
117133
117139
  }
117134
117140
  };
117141
+ var MAX_RATE_LIMIT_RETRIES = 2;
117142
+ var JITTER_MAX_MS = 5e3;
117135
117143
  var MAX_ENTRIES = 50;
117136
117144
  var policyCache = /* @__PURE__ */ new Map();
117137
117145
  var accessOrder = [];
@@ -117167,9 +117175,27 @@ function getProviderPolicy(providerKey) {
117167
117175
  accessOrder.push(providerKey);
117168
117176
  return policy;
117169
117177
  }
117170
- function withResilience(fn, providerKey) {
117178
+ async function withResilience(fn, providerKey, opts) {
117171
117179
  const policy = providerKey ? getProviderPolicy(providerKey) : getProviderPolicy("__global__");
117172
- return policy.execute(() => fn());
117180
+ const jitterMax = opts?.jitterMaxMs ?? JITTER_MAX_MS;
117181
+ const withRateLimitRetry = async () => {
117182
+ let lastError;
117183
+ for (let attempt = 0; attempt <= MAX_RATE_LIMIT_RETRIES; attempt++) {
117184
+ try {
117185
+ return await fn();
117186
+ } catch (err) {
117187
+ if (err instanceof GitHubRateLimitError && attempt < MAX_RATE_LIMIT_RETRIES) {
117188
+ lastError = err;
117189
+ const jitter = jitterMax > 0 ? Math.floor(Math.random() * jitterMax) : 0;
117190
+ await new Promise((resolve3) => setTimeout(resolve3, err.retryAfterMs + jitter));
117191
+ continue;
117192
+ }
117193
+ throw err;
117194
+ }
117195
+ }
117196
+ throw lastError;
117197
+ };
117198
+ return policy.execute(withRateLimitRetry);
117173
117199
  }
117174
117200
  var providerPolicy = getProviderPolicy("__legacy_global__");
117175
117201
 
@@ -118078,7 +118104,9 @@ var GitHubProvider = class {
118078
118104
  if (result.code != null && result.code !== 0) {
118079
118105
  const errText = result.stderr?.trim() ?? "";
118080
118106
  if (errText.includes("rate limit") || errText.includes("429")) {
118081
- throw new GitHubRateLimitError(6e4);
118107
+ const retryMatch = errText.match(/retry after (\d+)/i);
118108
+ const retryMs = retryMatch ? parseInt(retryMatch[1], 10) * 1e3 : 6e4;
118109
+ throw new GitHubRateLimitError(retryMs);
118082
118110
  }
118083
118111
  throw new Error(errText || `gh api ${method} ${endpoint} failed with exit code ${result.code}`);
118084
118112
  }
@@ -122967,7 +122995,8 @@ ${roleInstructions}`;
122967
122995
  return effortPrefix ?? roleInstructions ?? "";
122968
122996
  }
122969
122997
  function sendToAgent(sessionKey, taskMessage, opts) {
122970
- const idempotencyKey = `fabrica-${opts.projectName}-${opts.issueId}-${opts.role}-${opts.level ?? "unknown"}-${opts.slotIndex ?? 0}-${opts.fromLabel ?? "unknown"}-${sessionKey}`;
122998
+ const epoch = opts.dispatchEpoch ?? (/* @__PURE__ */ new Date()).toISOString();
122999
+ const idempotencyKey = `fabrica-${opts.projectName}-${opts.issueId}-${opts.role}-${opts.level ?? "unknown"}-${opts.slotIndex ?? 0}-${opts.fromLabel ?? "unknown"}-${sessionKey}-${epoch}`;
122971
123000
  if (opts.runtime?.subagent?.run) {
122972
123001
  opts.runtime.subagent.run({
122973
123002
  sessionKey,
@@ -123388,6 +123417,7 @@ async function dispatchTask(opts) {
123388
123417
  error: err.message ?? String(err)
123389
123418
  }).catch((auditErr) => console.error("[fabrica] silent-catch:", auditErr.message));
123390
123419
  });
123420
+ const dispatchEpoch = (/* @__PURE__ */ new Date()).toISOString();
123391
123421
  sendToAgent(sessionKey, taskMessage, {
123392
123422
  agentId,
123393
123423
  projectName: project.name,
@@ -123405,7 +123435,8 @@ async function dispatchTask(opts) {
123405
123435
  roleInstructions.trim() || void 0
123406
123436
  ) || void 0,
123407
123437
  runCommand: rc,
123408
- runtime
123438
+ runtime,
123439
+ dispatchEpoch
123409
123440
  });
123410
123441
  await recordIssueLifecycle({
123411
123442
  workspaceDir,
@@ -130457,7 +130488,10 @@ async function cleanupExpired(workspaceDir, ttlMs = DEFAULT_TTL_MS2) {
130457
130488
  const cutoff = Date.now() - ttlMs;
130458
130489
  const kept = entries.filter((e2) => e2.ts >= cutoff);
130459
130490
  if (kept.length < entries.length) {
130460
- await fs28.writeFile(filePath, kept.map((e2) => JSON.stringify(e2)).join("\n") + (kept.length > 0 ? "\n" : ""), "utf-8");
130491
+ const content = kept.map((e2) => JSON.stringify(e2)).join("\n") + (kept.length > 0 ? "\n" : "");
130492
+ const tmpPath = filePath + ".tmp";
130493
+ await fs28.writeFile(tmpPath, content, "utf-8");
130494
+ await fs28.rename(tmpPath, filePath);
130461
130495
  }
130462
130496
  }
130463
130497
 
@@ -130543,30 +130577,52 @@ async function projectTick(opts) {
130543
130577
  }
130544
130578
  if (role === "reviewer" || role === "tester") {
130545
130579
  const issueRuntime = getIssueRuntime(fresh, issue2.iid);
130546
- const prSelector = getCanonicalPrSelector(fresh, issue2.iid);
130580
+ let prSelector = getCanonicalPrSelector(fresh, issue2.iid);
130547
130581
  if (!prSelector?.prNumber) {
130548
- const feedbackLabel = getFeedbackQueueLabel(workflow);
130549
- if (!dryRun && feedbackLabel && feedbackLabel !== currentLabel) {
130550
- try {
130551
- await provider.transitionLabel(issue2.iid, currentLabel, feedbackLabel);
130552
- await log(workspaceDir, "queue_pr_guard", {
130553
- project: project.name,
130554
- projectSlug,
130555
- issueId: issue2.iid,
130556
- role,
130557
- from: currentLabel,
130558
- to: feedbackLabel,
130559
- prState: issueRuntime?.currentPrState ?? null,
130560
- prUrl: issueRuntime?.currentPrUrl ?? null,
130561
- prNumber: null,
130562
- currentIssueMatch: null,
130563
- reason: "missing_canonical_pr"
130582
+ const fallbackStatus = await provider.getPrStatus(issue2.iid).catch(() => null);
130583
+ const hasFallbackPr = !!fallbackStatus?.url && !!fallbackStatus.number && fallbackStatus.state !== PrState.MERGED && fallbackStatus.state !== PrState.CLOSED && fallbackStatus.currentIssueMatch !== false;
130584
+ if (hasFallbackPr && fallbackStatus?.number) {
130585
+ if (!dryRun) {
130586
+ await updateIssueRuntime(workspaceDir, projectSlug, issue2.iid, {
130587
+ currentPrNumber: fallbackStatus.number,
130588
+ currentPrUrl: fallbackStatus.url,
130589
+ currentPrState: fallbackStatus.state ?? null
130590
+ }).catch(() => {
130564
130591
  });
130565
- } catch {
130566
130592
  }
130593
+ const runtimeKey = String(issue2.iid);
130594
+ fresh.issueRuntime = fresh.issueRuntime ?? {};
130595
+ fresh.issueRuntime[runtimeKey] = {
130596
+ ...fresh.issueRuntime[runtimeKey] ?? {},
130597
+ currentPrNumber: fallbackStatus.number,
130598
+ currentPrUrl: fallbackStatus.url ?? null,
130599
+ currentPrState: fallbackStatus.state ?? null
130600
+ };
130601
+ prSelector = { prNumber: fallbackStatus.number };
130602
+ } else {
130603
+ const feedbackLabel = getFeedbackQueueLabel(workflow);
130604
+ if (!dryRun && feedbackLabel && feedbackLabel !== currentLabel) {
130605
+ try {
130606
+ await provider.transitionLabel(issue2.iid, currentLabel, feedbackLabel);
130607
+ await log(workspaceDir, "queue_pr_guard", {
130608
+ project: project.name,
130609
+ projectSlug,
130610
+ issueId: issue2.iid,
130611
+ role,
130612
+ from: currentLabel,
130613
+ to: feedbackLabel,
130614
+ prState: issueRuntime?.currentPrState ?? null,
130615
+ prUrl: issueRuntime?.currentPrUrl ?? null,
130616
+ prNumber: null,
130617
+ currentIssueMatch: null,
130618
+ reason: "missing_canonical_pr"
130619
+ });
130620
+ } catch {
130621
+ }
130622
+ }
130623
+ skipped.push({ role, reason: "No canonical bound PR for review/test cycle" });
130624
+ continue;
130567
130625
  }
130568
- skipped.push({ role, reason: "No canonical bound PR for review/test cycle" });
130569
- continue;
130570
130626
  }
130571
130627
  const prStatus = await provider.getPrStatus(issue2.iid, prSelector);
130572
130628
  const hasReviewablePr = !!prStatus.url && prStatus.state !== PrState.MERGED && prStatus.state !== PrState.CLOSED && prStatus.currentIssueMatch !== false;
@@ -132184,23 +132240,46 @@ async function checkProjectActive(workspaceDir, slug) {
132184
132240
  );
132185
132241
  }
132186
132242
 
132243
+ // lib/services/heartbeat/wake-bridge.ts
132244
+ var _wakeCallback = null;
132245
+ function setPluginWakeHandler(cb) {
132246
+ _wakeCallback = cb;
132247
+ }
132248
+ async function wakeHeartbeat(reason) {
132249
+ await _wakeCallback?.(reason);
132250
+ }
132251
+
132187
132252
  // lib/services/heartbeat/index.ts
132188
132253
  function registerHeartbeatService(api, pluginCtx) {
132189
132254
  let sharedIntervalId = null;
132190
- let tickCount = 0;
132255
+ let _pendingWake = false;
132191
132256
  api.registerService({
132192
132257
  id: "fabrica-heartbeat",
132193
132258
  start: async (svcCtx) => {
132194
132259
  const { intervalSeconds } = resolveHeartbeatConfig(pluginCtx.pluginConfig);
132195
132260
  const intervalMs = intervalSeconds * 1e3;
132196
- setTimeout(() => runHeartbeatTick(pluginCtx, svcCtx.logger, "repair"), 2e3);
132261
+ setTimeout(() => runHeartbeatTick(pluginCtx, svcCtx.logger, "full"), 2e3);
132197
132262
  sharedIntervalId = setInterval(() => {
132198
- const mode = tickCount % 2 === 0 ? "repair" : "triage";
132199
- tickCount++;
132200
- runHeartbeatTick(pluginCtx, svcCtx.logger, mode);
132263
+ runHeartbeatTick(pluginCtx, svcCtx.logger, "full").finally(() => {
132264
+ if (_pendingWake) {
132265
+ _pendingWake = false;
132266
+ svcCtx.logger.info("heartbeat_wake: running deferred full tick");
132267
+ runHeartbeatTick(pluginCtx, svcCtx.logger, "full");
132268
+ }
132269
+ });
132201
132270
  }, intervalMs);
132271
+ setPluginWakeHandler(async (reason) => {
132272
+ if (_anyTickRunning) {
132273
+ _pendingWake = true;
132274
+ svcCtx.logger.info(`heartbeat_wake: deferred (tick-in-progress), reason=${reason}`);
132275
+ return;
132276
+ }
132277
+ svcCtx.logger.info(`heartbeat_wake: running full tick, reason=${reason}`);
132278
+ await runHeartbeatTick(pluginCtx, svcCtx.logger, "full");
132279
+ });
132202
132280
  },
132203
132281
  stop: async (svcCtx) => {
132282
+ setPluginWakeHandler(null);
132204
132283
  if (sharedIntervalId) {
132205
132284
  clearInterval(sharedIntervalId);
132206
132285
  sharedIntervalId = null;
@@ -139086,7 +139165,7 @@ function registerTelegramBootstrapHook(api, ctx) {
139086
139165
  ctx.logger.info(`[telegram-bootstrap] stale received session (expired) \u2014 restarting pipeline for conversation ${conversationId}`);
139087
139166
  }
139088
139167
  }
139089
- if (!parsed.projectName && ctx.runtime?.subagent?.run) {
139168
+ if (!parsed.projectName && ctx.runtime?.subagent?.run != null) {
139090
139169
  await upsertTelegramBootstrapSession(workspaceDir, {
139091
139170
  conversationId,
139092
139171
  rawIdea: parsed.rawIdea,
@@ -139462,6 +139541,8 @@ function registerReactiveDispatchHooks(api, ctx) {
139462
139541
  api.on("after_tool_call", async (event, _eventCtx) => {
139463
139542
  if (!COMPLETION_TOOLS.has(event.toolName)) return;
139464
139543
  ctx.runtime?.system.requestHeartbeatNow({ reason: "work_finish", coalesceMs: 2e3 });
139544
+ wakeHeartbeat("work_finish").catch(() => {
139545
+ });
139465
139546
  });
139466
139547
  api.on("agent_end", async (_event, eventCtx) => {
139467
139548
  const sessionKey = eventCtx.sessionKey;
@@ -139469,6 +139550,8 @@ function registerReactiveDispatchHooks(api, ctx) {
139469
139550
  const parsed = parseFabricaSessionKey(sessionKey);
139470
139551
  if (!parsed) return;
139471
139552
  ctx.runtime?.system.requestHeartbeatNow({ reason: "agent_end", coalesceMs: 2e3 });
139553
+ wakeHeartbeat("agent_end").catch(() => {
139554
+ });
139472
139555
  });
139473
139556
  api.on("subagent_spawned", async (event, _eventCtx) => {
139474
139557
  const sessionKey = event.childSessionKey;
@@ -139478,6 +139561,7 @@ function registerReactiveDispatchHooks(api, ctx) {
139478
139561
  }
139479
139562
 
139480
139563
  // lib/dispatch/subagent-lifecycle-hook.ts
139564
+ init_workflow();
139481
139565
  function registerSubagentLifecycleHook(api, ctx) {
139482
139566
  const workspaceDir = resolveWorkspaceDir(ctx.config);
139483
139567
  if (!workspaceDir) return;
@@ -139501,6 +139585,67 @@ function registerSubagentLifecycleHook(api, ctx) {
139501
139585
  ctx.logger.info(
139502
139586
  `subagent_ended: worker ${role} in "${projectName}" ended with outcome=${event.outcome ?? "unknown"} (session=${sessionKey})`
139503
139587
  );
139588
+ try {
139589
+ const projects = await readProjects(workspaceDir);
139590
+ const projectEntry = Object.entries(projects.projects).find(
139591
+ ([, p]) => p.name === projectName
139592
+ );
139593
+ if (!projectEntry) return;
139594
+ const [projectSlug, project] = projectEntry;
139595
+ const roleWorker = project.workers[role];
139596
+ if (!roleWorker) return;
139597
+ let foundLevel;
139598
+ let foundSlotIndex;
139599
+ let foundSlot;
139600
+ for (const [level, slots] of Object.entries(roleWorker.levels)) {
139601
+ for (let i2 = 0; i2 < slots.length; i2++) {
139602
+ if (slots[i2].sessionKey === sessionKey && slots[i2].active) {
139603
+ foundLevel = level;
139604
+ foundSlotIndex = i2;
139605
+ foundSlot = slots[i2];
139606
+ break;
139607
+ }
139608
+ }
139609
+ if (foundSlot) break;
139610
+ }
139611
+ if (!foundSlot || foundLevel == null || foundSlotIndex == null) return;
139612
+ const issueId = foundSlot.issueId;
139613
+ await deactivateWorker(workspaceDir, projectSlug, role, {
139614
+ level: foundLevel,
139615
+ slotIndex: foundSlotIndex
139616
+ });
139617
+ if (issueId) {
139618
+ try {
139619
+ const config2 = await loadConfig(workspaceDir, projectSlug);
139620
+ const activeLabel = getActiveLabel(config2.workflow, role);
139621
+ const revertLabel = getRevertLabel(config2.workflow, role);
139622
+ const { provider } = await createProvider({
139623
+ repo: project.repo,
139624
+ provider: project.provider,
139625
+ runCommand: ctx.runCommand
139626
+ });
139627
+ const issue2 = await provider.getIssue(Number(issueId));
139628
+ if (issue2.labels.includes(activeLabel)) {
139629
+ await provider.transitionLabel(Number(issueId), activeLabel, revertLabel);
139630
+ }
139631
+ } catch {
139632
+ }
139633
+ }
139634
+ await log(workspaceDir, "subagent_ended_slot_cleanup", {
139635
+ sessionKey,
139636
+ project: projectName,
139637
+ role,
139638
+ level: foundLevel,
139639
+ slotIndex: foundSlotIndex,
139640
+ issueId,
139641
+ outcome: event.outcome ?? "unknown"
139642
+ }).catch(() => {
139643
+ });
139644
+ wakeHeartbeat("subagent_ended").catch(() => {
139645
+ });
139646
+ } catch (err) {
139647
+ ctx.logger.warn(`subagent_ended_slot_cleanup failed: ${err.message}`);
139648
+ }
139504
139649
  });
139505
139650
  }
139506
139651