@mestreyoda/fabrica 0.2.39 → 0.2.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +211 -26
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -113905,8 +113905,8 @@ import fsSync from "node:fs";
113905
113905
  import path5 from "node:path";
113906
113906
  import { fileURLToPath as fileURLToPath3 } from "node:url";
113907
113907
  function getCurrentVersion() {
113908
- if ("0.2.39") {
113909
- return "0.2.39";
113908
+ if ("0.2.40") {
113909
+ return "0.2.40";
113910
113910
  }
113911
113911
  try {
113912
113912
  const pkgPath = path5.join(THIS_DIR, "..", "..", "package.json");
@@ -131390,6 +131390,51 @@ init_workflow();
131390
131390
  init_context3();
131391
131391
  init_labels();
131392
131392
 
131393
+ // lib/services/doctor-snapshot.ts
131394
+ init_audit();
131395
+ async function captureIssueDoctorSnapshot(opts) {
131396
+ try {
131397
+ const result = await runIssueDoctor({
131398
+ workspacePath: opts.workspaceDir,
131399
+ projectSlug: opts.projectSlug,
131400
+ issueId: opts.issueId,
131401
+ runCommand: opts.runCommand,
131402
+ pluginConfig: opts.pluginConfig
131403
+ });
131404
+ await log(opts.workspaceDir, opts.event, {
131405
+ projectSlug: opts.projectSlug,
131406
+ issueId: opts.issueId,
131407
+ trigger: opts.trigger,
131408
+ summary: result.recommendation.summary,
131409
+ likelyNextAction: result.recommendation.likelyNextAction,
131410
+ doctor: {
131411
+ artifact: result.hasArtifact,
131412
+ progressState: result.lifecycle.progressState,
131413
+ dispatchCycleId: result.lifecycle.dispatchCycleId,
131414
+ dispatchRunId: result.lifecycle.dispatchRunId,
131415
+ prUrl: result.pr?.url ?? null,
131416
+ prState: result.pr?.state ?? null,
131417
+ labels: result.issue?.labels ?? [],
131418
+ convergenceCause: result.convergence.cause,
131419
+ convergenceAction: result.convergence.action,
131420
+ convergenceRetryCount: result.convergence.retryCount,
131421
+ convergenceHeadSha: result.convergence.headSha,
131422
+ headShaChangedSinceLastConvergence: result.convergence.headShaChangedSinceLastConvergence
131423
+ },
131424
+ ...opts.extra ?? {}
131425
+ });
131426
+ } catch (error48) {
131427
+ await log(opts.workspaceDir, `${opts.event}_failed`, {
131428
+ projectSlug: opts.projectSlug,
131429
+ issueId: opts.issueId,
131430
+ trigger: opts.trigger,
131431
+ error: error48 instanceof Error ? error48.message : String(error48),
131432
+ ...opts.extra ?? {}
131433
+ }).catch(() => {
131434
+ });
131435
+ }
131436
+ }
131437
+
131393
131438
  // lib/services/post-pr-convergence.ts
131394
131439
  init_types3();
131395
131440
  function classifyConvergenceCause(reason) {
@@ -132054,6 +132099,23 @@ ${validationReason}`;
132054
132099
  lastConvergenceHeadSha: convergence.progressHeadSha
132055
132100
  }).catch(() => {
132056
132101
  });
132102
+ if (convergenceIssueRuntime?.currentPrUrl || convergence.action === "escalate_human") {
132103
+ await captureIssueDoctorSnapshot({
132104
+ workspaceDir: opts.workspaceDir,
132105
+ projectSlug: context2.projectSlug,
132106
+ issueId: context2.issueId,
132107
+ runCommand: opts.runCommand,
132108
+ pluginConfig: opts.pluginConfig,
132109
+ event: "doctor_snapshot",
132110
+ trigger: convergence.action === "escalate_human" ? "worker_completion_escalated" : "worker_completion_blocked_with_artifact",
132111
+ extra: {
132112
+ convergenceCause: convergence.cause,
132113
+ convergenceAction: convergence.action,
132114
+ convergenceRetryCount: convergence.retryCount
132115
+ }
132116
+ }).catch(() => {
132117
+ });
132118
+ }
132057
132119
  await executeCompletion({
132058
132120
  workspaceDir: opts.workspaceDir,
132059
132121
  projectSlug: context2.projectSlug,
@@ -132741,6 +132803,22 @@ async function checkWorkerHealth(opts) {
132741
132803
  toLabel: convergence.targetLabel,
132742
132804
  deliveryState
132743
132805
  });
132806
+ if (runCommand) {
132807
+ await captureIssueDoctorSnapshot({
132808
+ workspaceDir,
132809
+ projectSlug,
132810
+ issueId: issueIdNum,
132811
+ runCommand,
132812
+ event: "doctor_snapshot",
132813
+ trigger: convergence.action === "escalate_human" ? "completion_recovery_escalated" : "completion_recovery_requeued",
132814
+ extra: {
132815
+ convergenceCause: convergence.cause,
132816
+ convergenceAction: convergence.action,
132817
+ convergenceRetryCount: convergence.retryCount
132818
+ }
132819
+ }).catch(() => {
132820
+ });
132821
+ }
132744
132822
  }
132745
132823
  }
132746
132824
  fixes.push(fix);
@@ -133109,6 +133187,23 @@ async function checkWorkerHealth(opts) {
133109
133187
  idleMinutes: Math.round(quietMinutes),
133110
133188
  deliveryState
133111
133189
  });
133190
+ if (runCommand) {
133191
+ await captureIssueDoctorSnapshot({
133192
+ workspaceDir,
133193
+ projectSlug,
133194
+ issueId: issueIdNum,
133195
+ runCommand,
133196
+ event: "doctor_snapshot",
133197
+ trigger: convergence.action === "escalate_human" ? "stalled_with_artifact_escalated" : "stalled_with_artifact_requeued",
133198
+ extra: {
133199
+ convergenceCause: convergence.cause,
133200
+ convergenceAction: convergence.action,
133201
+ convergenceRetryCount: convergence.retryCount,
133202
+ idleMinutes: Math.round(quietMinutes)
133203
+ }
133204
+ }).catch(() => {
133205
+ });
133206
+ }
133112
133207
  }
133113
133208
  }
133114
133209
  fixes.push(fix);
@@ -149129,12 +149224,29 @@ async function readAuditLines(filePath) {
149129
149224
  }
149130
149225
  return entries;
149131
149226
  }
149227
+ function keyFor(entry) {
149228
+ const projectSlug = entry.projectSlug ?? entry.project ?? null;
149229
+ const issueId = entry.issueId ?? entry.issue ?? null;
149230
+ if (!projectSlug || issueId == null) return null;
149231
+ return `${projectSlug}:${issueId}`;
149232
+ }
149233
+ function normalizeCause(entry) {
149234
+ const cause = entry.convergenceCause ?? entry.reason ?? null;
149235
+ return cause ? String(cause) : null;
149236
+ }
149132
149237
  async function computeMetrics(workspaceDir) {
149133
149238
  const auditLogPath = join4(workspaceDir, DATA_DIR, "log", "audit.log");
149239
+ const bakEntries3 = await readAuditLines(`${auditLogPath}.3.bak`);
149134
149240
  const bakEntries2 = await readAuditLines(`${auditLogPath}.2.bak`);
149135
149241
  const bakEntries = await readAuditLines(`${auditLogPath}.bak`);
149136
149242
  const currentEntries = await readAuditLines(auditLogPath);
149137
- let entries = [...bakEntries2, ...bakEntries, ...currentEntries];
149243
+ const entries = [...bakEntries3, ...bakEntries2, ...bakEntries, ...currentEntries];
149244
+ const projectsData = await readProjects(workspaceDir).catch(() => ({ projects: {} }));
149245
+ const stackByProject = /* @__PURE__ */ new Map();
149246
+ for (const [slug, project] of Object.entries(projectsData.projects ?? {})) {
149247
+ const stack = project.stack ?? project.environment?.stack ?? null;
149248
+ if (stack) stackByProject.set(slug, String(stack));
149249
+ }
149138
149250
  const entriesScanned = entries.length;
149139
149251
  let dispatches = 0;
149140
149252
  let completionsTotal = 0;
@@ -149144,16 +149256,40 @@ async function computeMetrics(workspaceDir) {
149144
149256
  let completionsOther = 0;
149145
149257
  let conflictsDetected = 0;
149146
149258
  let sessionBudgetResets = 0;
149259
+ let humanEscalations = 0;
149260
+ const causeCounts = {};
149147
149261
  const dispatchTimes = /* @__PURE__ */ new Map();
149262
+ const firstPrTimes = /* @__PURE__ */ new Map();
149148
149263
  const completionDeltas = [];
149264
+ const firstPrDeltas = [];
149265
+ const stackMetrics = /* @__PURE__ */ new Map();
149266
+ function stackBucket(entry) {
149267
+ const slug = entry.projectSlug ?? entry.project ?? null;
149268
+ const stack = entry.stack ?? (slug ? stackByProject.get(String(slug)) : null) ?? "unknown";
149269
+ if (!stackMetrics.has(String(stack))) {
149270
+ stackMetrics.set(String(stack), {
149271
+ issues: /* @__PURE__ */ new Set(),
149272
+ dispatches: 0,
149273
+ escalations: 0,
149274
+ causeCounts: {},
149275
+ completionDeltas: [],
149276
+ firstPrDeltas: []
149277
+ });
149278
+ }
149279
+ return String(stack);
149280
+ }
149149
149281
  for (const entry of entries) {
149282
+ const issueKey = keyFor(entry);
149283
+ const stack = stackBucket(entry);
149284
+ const stackBucketState = stackMetrics.get(stack);
149285
+ if (issueKey) stackBucketState.issues.add(issueKey);
149150
149286
  switch (entry.event) {
149151
- case "dispatch":
149287
+ case "dispatch": {
149152
149288
  dispatches++;
149153
- if (entry.issue != null && entry.project) {
149154
- dispatchTimes.set(`${entry.project}:${entry.issue}`, Date.parse(entry.ts));
149155
- }
149289
+ stackBucketState.dispatches++;
149290
+ if (issueKey) dispatchTimes.set(issueKey, Date.parse(entry.ts));
149156
149291
  break;
149292
+ }
149157
149293
  case "work_finish": {
149158
149294
  completionsTotal++;
149159
149295
  const result = String(entry.result ?? "");
@@ -149161,14 +149297,13 @@ async function computeMetrics(workspaceDir) {
149161
149297
  else if (result === "pass") completionsPass++;
149162
149298
  else if (result === "fail") completionsFail++;
149163
149299
  else completionsOther++;
149164
- if (entry.issue != null && entry.project) {
149165
- const key = `${entry.project}:${entry.issue}`;
149166
- const dispatchTime = dispatchTimes.get(key);
149167
- if (dispatchTime !== void 0) {
149168
- const completionTime = Date.parse(entry.ts);
149169
- if (!isNaN(completionTime) && completionTime > dispatchTime) {
149170
- completionDeltas.push((completionTime - dispatchTime) / 6e4);
149171
- }
149300
+ if (issueKey) {
149301
+ const dispatchTime = dispatchTimes.get(issueKey);
149302
+ const completionTime = Date.parse(entry.ts);
149303
+ if (dispatchTime !== void 0 && !Number.isNaN(completionTime) && completionTime > dispatchTime) {
149304
+ const delta = (completionTime - dispatchTime) / 6e4;
149305
+ completionDeltas.push(delta);
149306
+ stackBucketState.completionDeltas.push(delta);
149172
149307
  }
149173
149308
  }
149174
149309
  break;
@@ -149182,9 +149317,47 @@ async function computeMetrics(workspaceDir) {
149182
149317
  case "session_budget_reset":
149183
149318
  sessionBudgetResets++;
149184
149319
  break;
149320
+ case "pr_discovered_via_polling":
149321
+ case "pr_updated_via_polling": {
149322
+ if (issueKey && !firstPrTimes.has(issueKey)) {
149323
+ const prTime = Date.parse(entry.ts);
149324
+ const dispatchTime = dispatchTimes.get(issueKey);
149325
+ firstPrTimes.set(issueKey, prTime);
149326
+ if (dispatchTime !== void 0 && !Number.isNaN(prTime) && prTime > dispatchTime) {
149327
+ const delta = (prTime - dispatchTime) / 6e4;
149328
+ firstPrDeltas.push(delta);
149329
+ stackBucketState.firstPrDeltas.push(delta);
149330
+ }
149331
+ }
149332
+ break;
149333
+ }
149334
+ case "worker_completion_skipped":
149335
+ case "doctor_snapshot":
149336
+ case "health_fix_applied": {
149337
+ const cause = normalizeCause(entry);
149338
+ if (cause) {
149339
+ causeCounts[cause] = (causeCounts[cause] ?? 0) + 1;
149340
+ stackBucketState.causeCounts[cause] = (stackBucketState.causeCounts[cause] ?? 0) + 1;
149341
+ }
149342
+ if (entry.convergenceAction === "escalate_human" || entry.action === "escalate_human") {
149343
+ humanEscalations++;
149344
+ stackBucketState.escalations++;
149345
+ }
149346
+ break;
149347
+ }
149185
149348
  }
149186
149349
  }
149187
- const avgDispatchToCompletionMinutes = completionDeltas.length > 0 ? completionDeltas.reduce((a, b) => a + b, 0) / completionDeltas.length : null;
149350
+ const avg = (values) => values.length ? values.reduce((a, b) => a + b, 0) / values.length : null;
149351
+ const stackMetricsObject = Object.fromEntries(
149352
+ [...stackMetrics.entries()].map(([stack, data]) => [stack, {
149353
+ issues: data.issues.size,
149354
+ dispatches: data.dispatches,
149355
+ escalations: data.escalations,
149356
+ causeCounts: data.causeCounts,
149357
+ avgDispatchToCompletionMinutes: avg(data.completionDeltas),
149358
+ avgDispatchToFirstPrMinutes: avg(data.firstPrDeltas)
149359
+ }])
149360
+ );
149188
149361
  return {
149189
149362
  entriesScanned,
149190
149363
  dispatches,
@@ -149195,28 +149368,40 @@ async function computeMetrics(workspaceDir) {
149195
149368
  fail: completionsFail,
149196
149369
  other: completionsOther
149197
149370
  },
149198
- avgDispatchToCompletionMinutes,
149371
+ avgDispatchToCompletionMinutes: avg(completionDeltas),
149372
+ avgDispatchToFirstPrMinutes: avg(firstPrDeltas),
149199
149373
  conflictsDetected,
149200
149374
  sessionBudgetResets,
149375
+ humanEscalations,
149376
+ causeCounts,
149377
+ stackMetrics: stackMetricsObject,
149201
149378
  auditLogPath
149202
149379
  };
149203
149380
  }
149204
149381
  function formatMetrics(metrics2) {
149205
149382
  const lines = [
149206
- `Fabrica \u2014 Metricas (${metrics2.entriesScanned} entradas do audit.log)`,
149383
+ `Fabrica \u2014 M\xE9tricas (${metrics2.entriesScanned} entradas do audit.log)`,
149207
149384
  ` Dispatches: ${metrics2.dispatches}`
149208
149385
  ];
149209
149386
  const c = metrics2.completions;
149210
- lines.push(
149211
- ` Conclusoes: ${c.total} (done: ${c.done}, pass: ${c.pass}, fail: ${c.fail}${c.other > 0 ? `, other: ${c.other}` : ""})`
149212
- );
149213
- if (metrics2.avgDispatchToCompletionMinutes !== null) {
149214
- lines.push(` Tempo medio dispatch \u2192 completion: ${metrics2.avgDispatchToCompletionMinutes.toFixed(1)} min`);
149215
- } else {
149216
- lines.push(` Tempo medio dispatch \u2192 completion: n/a`);
149217
- }
149387
+ lines.push(` Conclus\xF5es: ${c.total} (done: ${c.done}, pass: ${c.pass}, fail: ${c.fail}${c.other > 0 ? `, other: ${c.other}` : ""})`);
149388
+ lines.push(` Tempo m\xE9dio dispatch \u2192 completion: ${metrics2.avgDispatchToCompletionMinutes?.toFixed(1) ?? "n/a"} min`);
149389
+ lines.push(` Tempo m\xE9dio dispatch \u2192 primeira PR: ${metrics2.avgDispatchToFirstPrMinutes?.toFixed(1) ?? "n/a"} min`);
149218
149390
  lines.push(` Conflitos detectados: ${metrics2.conflictsDetected}`);
149219
149391
  lines.push(` Session budget resets: ${metrics2.sessionBudgetResets}`);
149392
+ lines.push(` Escalonamentos humanos: ${metrics2.humanEscalations}`);
149393
+ if (Object.keys(metrics2.causeCounts).length > 0) {
149394
+ lines.push(" Causas tipadas:");
149395
+ for (const [cause, count] of Object.entries(metrics2.causeCounts).sort((a, b) => b[1] - a[1])) {
149396
+ lines.push(` - ${cause}: ${count}`);
149397
+ }
149398
+ }
149399
+ if (Object.keys(metrics2.stackMetrics).length > 0) {
149400
+ lines.push(" Por stack:");
149401
+ for (const [stack, data] of Object.entries(metrics2.stackMetrics)) {
149402
+ lines.push(` - ${stack}: issues=${data.issues}, dispatches=${data.dispatches}, escalations=${data.escalations}, avgPR=${data.avgDispatchToFirstPrMinutes?.toFixed(1) ?? "n/a"}m, avgDone=${data.avgDispatchToCompletionMinutes?.toFixed(1) ?? "n/a"}m`);
149403
+ }
149404
+ }
149220
149405
  return lines.join("\n");
149221
149406
  }
149222
149407
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mestreyoda/fabrica",
3
- "version": "0.2.39",
3
+ "version": "0.2.40",
4
4
  "description": "Autonomous software engineering pipeline for OpenClaw. Turns ideas into deployed code via intake, dispatch, review, test, and merge.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",