@mestreyoda/fabrica 0.2.20 → 0.2.21

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 +311 -32
  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.20") {
113909
- return "0.2.20";
113908
+ if ("0.2.21") {
113909
+ return "0.2.21";
113910
113910
  }
113911
113911
  try {
113912
113912
  const pkgPath = path5.join(THIS_DIR, "..", "..", "package.json");
@@ -114883,7 +114883,7 @@ function resolve2(config2, sourceLayers, trace2) {
114883
114883
  dispatchMs: config2.timeouts?.dispatchMs ?? 6e5,
114884
114884
  staleWorkerHours: config2.timeouts?.staleWorkerHours ?? 2,
114885
114885
  sessionContextBudget: config2.timeouts?.sessionContextBudget ?? 0.6,
114886
- stallTimeoutMinutes: config2.timeouts?.stallTimeoutMinutes ?? 5,
114886
+ stallTimeoutMinutes: config2.timeouts?.stallTimeoutMinutes ?? 25,
114887
114887
  sessionConfirmAttempts: config2.timeouts?.sessionConfirmAttempts ?? 5,
114888
114888
  sessionConfirmDelayMs: config2.timeouts?.sessionConfirmDelayMs ?? 250,
114889
114889
  sessionLabelMaxLength: config2.timeouts?.sessionLabelMaxLength ?? 64,
@@ -118561,6 +118561,34 @@ var conduct_interview_exports = {};
118561
118561
  __export(conduct_interview_exports, {
118562
118562
  conductInterviewStep: () => conductInterviewStep
118563
118563
  });
118564
+ function deriveFeatureScopeFromRawIdea(rawIdea) {
118565
+ const text = rawIdea.toLowerCase();
118566
+ const scope = [];
118567
+ const add = (item) => {
118568
+ if (!scope.includes(item)) scope.push(item);
118569
+ };
118570
+ if (/\b(auth|oauth|jwt|login|register|session|role-based access|rbac|permissions?)\b/i.test(text)) {
118571
+ add("Implement authentication, authorization, and role-aware access rules");
118572
+ }
118573
+ if (/\b(incident|project|task|owner|assign|timeline|status|update)s?\b/i.test(text)) {
118574
+ add("Implement the core domain workflows and CRUD endpoints for the main entities");
118575
+ }
118576
+ if (/\b(alert|alerts|notif|notification|notifications|reminder|reminders|escalation|escalations)\b/i.test(text)) {
118577
+ add("Implement notifications, reminders, and escalation flows for key events");
118578
+ }
118579
+ if (/\b(background process|background worker|worker|queue|job|celery|bull|sidekiq|scheduler)\b/i.test(text)) {
118580
+ add("Implement the background processing pipeline required for asynchronous work");
118581
+ }
118582
+ if (/\b(admin view|admin panel|admin console|dashboard|audit history|audit log|activity history)\b/i.test(text)) {
118583
+ add("Implement administrative visibility and audit/history capabilities for operators");
118584
+ }
118585
+ if (scope.length < 3) {
118586
+ add("Implement the primary user workflow described in the request");
118587
+ add("Expose the key interactions needed to operate the system end to end");
118588
+ add("Add validation and persistence rules for the main business flow");
118589
+ }
118590
+ return scope.slice(0, 5);
118591
+ }
118564
118592
  function fallbackSpecData(type, rawIdea) {
118565
118593
  const title = rawIdea.slice(0, 120);
118566
118594
  const base = {
@@ -118588,7 +118616,12 @@ function fallbackSpecData(type, rawIdea) {
118588
118616
  base.acceptance_criteria = ["Infrastructure change applied and verified"];
118589
118617
  break;
118590
118618
  default:
118591
- base.acceptance_criteria = ["Feature works as described in the objective"];
118619
+ base.scope_v1 = deriveFeatureScopeFromRawIdea(rawIdea);
118620
+ base.acceptance_criteria = [
118621
+ "The primary workflow works end to end as requested",
118622
+ "Role, validation, or delivery constraints from the request are enforced",
118623
+ "The asynchronous/background behavior works for the main operational path"
118624
+ ];
118592
118625
  }
118593
118626
  return base;
118594
118627
  }
@@ -124987,6 +125020,20 @@ function buildMessage(event) {
124987
125020
  }
124988
125021
  return msg;
124989
125022
  }
125023
+ case "workerProgress": {
125024
+ const worker = formatWorkerString(event.role, {
125025
+ name: event.name,
125026
+ level: event.level
125027
+ });
125028
+ let msg = `\u23F3 ${worker} still working on #${event.issueId}: ${event.issueTitle}`;
125029
+ if (event.summary) msg += `
125030
+ ${event.summary}`;
125031
+ if (event.minutesActive != null) msg += `
125032
+ \u{1F552} Active for ${event.minutesActive} min`;
125033
+ msg += `
125034
+ \u{1F4CB} [Issue #${event.issueId}](${event.issueUrl})`;
125035
+ return msg;
125036
+ }
124990
125037
  case "workerComplete": {
124991
125038
  const icons = {
124992
125039
  done: "\u2705",
@@ -126741,6 +126788,10 @@ function createTaskCreateTool(ctx) {
126741
126788
  pickup: {
126742
126789
  type: "boolean",
126743
126790
  description: "If true, immediately pick up this issue for DEV after creation. Defaults to false."
126791
+ },
126792
+ parentIssueId: {
126793
+ type: "number",
126794
+ description: "Optional parent/epic issue ID when creating a child task as part of a decomposition plan."
126744
126795
  }
126745
126796
  }
126746
126797
  },
@@ -126749,12 +126800,17 @@ function createTaskCreateTool(ctx) {
126749
126800
  const description = params.description ?? "";
126750
126801
  const assignees = params.assignees ?? [];
126751
126802
  const pickup = params.pickup ?? false;
126803
+ const parentIssueId = Number.isFinite(params.parentIssueId) ? Number(params.parentIssueId) : null;
126752
126804
  const workspaceDir = requireWorkspaceDir(toolCtx);
126753
126805
  const { project, route } = await resolveProjectFromContext(workspaceDir, toolCtx, params.channelId);
126754
126806
  const resolvedConfig = await loadConfig(workspaceDir, project.slug);
126755
126807
  const label = resolvedConfig.workflow.states[resolvedConfig.workflow.initial]?.label ?? "Planning";
126756
126808
  const { provider, type: providerType } = await resolveProvider(project, ctx.runCommand);
126757
- const issue2 = await provider.createIssue(title, description, label, assignees);
126809
+ let body = description;
126810
+ if (parentIssueId) {
126811
+ body = `${description}${description.trim().length > 0 ? "\n\n" : ""}Parent issue: #${parentIssueId}`;
126812
+ }
126813
+ const issue2 = await provider.createIssue(title, body, label, assignees);
126758
126814
  provider.reactToIssue(issue2.iid, "eyes").catch(() => {
126759
126815
  });
126760
126816
  const primaryChannel = findPrimaryChannel(project);
@@ -126768,13 +126824,31 @@ function createTaskCreateTool(ctx) {
126768
126824
  );
126769
126825
  autoAssignOwnerLabel(workspaceDir, provider, issue2.iid, project).catch(() => {
126770
126826
  });
126827
+ if (parentIssueId) {
126828
+ await updateIssueRuntime(workspaceDir, project.slug, issue2.iid, {
126829
+ parentIssueId
126830
+ }).catch(() => {
126831
+ });
126832
+ const parentRuntime = project.issueRuntime?.[String(parentIssueId)] ?? {};
126833
+ const previousChildren = Array.isArray(parentRuntime.childIssueIds) ? parentRuntime.childIssueIds : [];
126834
+ await updateIssueRuntime(workspaceDir, project.slug, parentIssueId, {
126835
+ childIssueIds: [.../* @__PURE__ */ new Set([...previousChildren, issue2.iid])]
126836
+ }).catch(() => {
126837
+ });
126838
+ provider.addComment(
126839
+ parentIssueId,
126840
+ `\u{1F4CC} Child task created: [#${issue2.iid}: ${issue2.title}](${issue2.web_url})`
126841
+ ).catch(() => {
126842
+ });
126843
+ }
126771
126844
  await log(workspaceDir, "task_create", {
126772
126845
  project: project.name,
126773
126846
  issueId: issue2.iid,
126774
126847
  title,
126775
126848
  label,
126776
126849
  provider: providerType,
126777
- pickup
126850
+ pickup,
126851
+ parentIssueId
126778
126852
  });
126779
126853
  const hasBody = description && description.trim().length > 0;
126780
126854
  let announcement = `\u{1F4CB} Created #${issue2.iid}: "${title}" (${label})`;
@@ -128835,6 +128909,7 @@ async function dispatchTask(opts) {
128835
128909
  inconclusiveCompletionAt: null,
128836
128910
  inconclusiveCompletionReason: null,
128837
128911
  sessionCompletedAt: null,
128912
+ progressNotifiedAt: null,
128838
128913
  lastSessionKey: sessionKey
128839
128914
  }).catch((err) => {
128840
128915
  log(workspaceDir, "dispatch_warning", { step: "record_dispatch_requested", issue: issueId, err: String(err) }).catch(() => {
@@ -131332,6 +131407,7 @@ async function checkWorkerHealth(opts) {
131332
131407
  completionRecoveryWindowMs = COMPLETION_RECOVERY_WINDOW_MS,
131333
131408
  executionContractRecoveryWindowMs = EXECUTION_CONTRACT_RECOVERY_WINDOW_MS,
131334
131409
  runCommand,
131410
+ stallTimeoutMinutes = 25,
131335
131411
  notificationConfig
131336
131412
  } = opts;
131337
131413
  const fixes = [];
@@ -131831,7 +131907,120 @@ async function checkWorkerHealth(opts) {
131831
131907
  continue;
131832
131908
  }
131833
131909
  if (slot.active && slot.startTime && sessionKey && sessions && sessionAlive) {
131834
- const hours = (Date.now() - new Date(slot.startTime).getTime()) / 36e5;
131910
+ const startedAtMs = new Date(slot.startTime).getTime();
131911
+ const minutesActive = (Date.now() - startedAtMs) / 6e4;
131912
+ const hours = minutesActive / 60;
131913
+ let hasReviewableArtifact = Boolean(
131914
+ issueRuntime?.currentPrNumber || issueRuntime?.currentPrUrl || issueRuntime?.artifactOfRecord?.prNumber
131915
+ );
131916
+ if (!hasReviewableArtifact && issueIdNum) {
131917
+ try {
131918
+ const prStatus = await provider.getPrStatus(issueIdNum);
131919
+ hasReviewableArtifact = Boolean(
131920
+ prStatus.url && prStatus.state !== PrState.MERGED && prStatus.state !== PrState.CLOSED && prStatus.currentIssueMatch !== false
131921
+ );
131922
+ } catch {
131923
+ }
131924
+ }
131925
+ if (role === "developer" && issue2 && issueIdNum && !hasReviewableArtifact && minutesActive >= Math.max(5, Math.floor(stallTimeoutMinutes / 2)) && !issueRuntime?.progressNotifiedAt) {
131926
+ const channel = project.channels?.[0];
131927
+ await notify(
131928
+ {
131929
+ type: "workerProgress",
131930
+ project: project.name,
131931
+ issueId: issueIdNum,
131932
+ issueUrl: issue2.web_url,
131933
+ issueTitle: issue2.title,
131934
+ role,
131935
+ level,
131936
+ minutesActive: Math.round(minutesActive),
131937
+ summary: "Still iterating on implementation/QA without a PR yet.",
131938
+ dispatchCycleId: slot.dispatchCycleId ?? issueRuntime?.lastDispatchCycleId ?? null,
131939
+ dispatchRunId: slot.dispatchRunId ?? issueRuntime?.dispatchRunId ?? null
131940
+ },
131941
+ {
131942
+ workspaceDir,
131943
+ config: notificationConfig,
131944
+ target: channel ? {
131945
+ channelId: channel.channelId,
131946
+ channel: channel.channel,
131947
+ accountId: channel.accountId,
131948
+ messageThreadId: channel.messageThreadId
131949
+ } : void 0,
131950
+ runCommand
131951
+ }
131952
+ ).catch(() => {
131953
+ });
131954
+ await updateIssueRuntime(workspaceDir, projectSlug, issueIdNum, {
131955
+ progressNotifiedAt: (/* @__PURE__ */ new Date()).toISOString(),
131956
+ lastSessionKey: sessionKey
131957
+ }).catch(() => {
131958
+ });
131959
+ }
131960
+ if (role === "developer" && issue2 && issueIdNum && !hasReviewableArtifact && minutesActive >= stallTimeoutMinutes) {
131961
+ const fix = {
131962
+ issue: {
131963
+ type: "completion_recovery_exhausted",
131964
+ severity: "critical",
131965
+ project: project.name,
131966
+ projectSlug,
131967
+ role,
131968
+ level,
131969
+ sessionKey,
131970
+ issueId: slot.issueId,
131971
+ slotIndex,
131972
+ message: `${role.toUpperCase()} ${level}[${slotIndex}] active for ${Math.round(minutesActive)}m without a PR or terminal result`
131973
+ },
131974
+ fixed: false
131975
+ };
131976
+ if (autoFix) {
131977
+ const channel = project.channels?.[0];
131978
+ await notify(
131979
+ {
131980
+ type: "workerRecoveryExhausted",
131981
+ project: project.name,
131982
+ issueId: issueIdNum,
131983
+ issueUrl: issue2.web_url,
131984
+ issueTitle: issue2.title,
131985
+ role,
131986
+ detail: `No PR or canonical completion after ${Math.round(minutesActive)} minutes of active work. Re-queueing for a fresh attempt.`,
131987
+ nextState: slotQueueLabel,
131988
+ dispatchCycleId: slot.dispatchCycleId ?? issueRuntime?.lastDispatchCycleId ?? null,
131989
+ dispatchRunId: slot.dispatchRunId ?? issueRuntime?.dispatchRunId ?? null
131990
+ },
131991
+ {
131992
+ workspaceDir,
131993
+ config: notificationConfig,
131994
+ target: channel ? {
131995
+ channelId: channel.channelId,
131996
+ channel: channel.channel,
131997
+ accountId: channel.accountId,
131998
+ messageThreadId: channel.messageThreadId
131999
+ } : void 0,
132000
+ runCommand
132001
+ }
132002
+ ).catch(() => {
132003
+ });
132004
+ await revertLabel(fix, expectedLabel, slotQueueLabel);
132005
+ if (!fix.labelRevertFailed) {
132006
+ await deactivateSlot();
132007
+ await updateIssueRuntime(workspaceDir, projectSlug, issueIdNum, {
132008
+ inconclusiveCompletionAt: (/* @__PURE__ */ new Date()).toISOString(),
132009
+ inconclusiveCompletionReason: "stalled_without_artifact",
132010
+ progressNotifiedAt: null
132011
+ }).catch(() => {
132012
+ });
132013
+ fix.fixed = true;
132014
+ await auditHealthFixApplied(workspaceDir, fix, {
132015
+ action: "requeue_issue",
132016
+ fromLabel: expectedLabel,
132017
+ toLabel: slotQueueLabel
132018
+ });
132019
+ }
132020
+ }
132021
+ fixes.push(fix);
132022
+ continue;
132023
+ }
131835
132024
  if (hours > staleWorkerHours) {
131836
132025
  const fix = {
131837
132026
  issue: {
@@ -139694,14 +139883,14 @@ function detectRawIdeaComplexity(rawIdea) {
139694
139883
  const text = rawIdea.toLowerCase();
139695
139884
  const signals = [];
139696
139885
  const subsystemPatterns = [
139697
- [/\b(worker|background.?job|queue|celery|bull|sidekiq|task.?runner)\b/i, "background-worker"],
139886
+ [/\b(worker|background.?job|background process|background service|background worker|queue|celery|bull|sidekiq|task.?runner|scheduler|job processor)\b/i, "background-worker"],
139698
139887
  [/\b(websocket|server.?sent|sse|real.?time|realtime|socket\.io|push.?notif)\b/i, "realtime"],
139699
- [/\b(auth|oauth|jwt|login|register|session|user.?account|signup)\b/i, "auth"],
139700
- [/\b(notif|alert|email|sms|webhook|subscription|subscribe)\b/i, "notifications"],
139701
- [/\b(database|banco|db|postgres|mysql|mongodb|redis|sqlite|orm)\b/i, "database"],
139888
+ [/\b(auth|oauth|jwt|login|register|session|user.?account|signup|role-based access|rbac|permission[s]?)\b/i, "auth"],
139889
+ [/\b(notif|notification[s]?|alert[s]?|email|sms|webhook|subscription|subscribe|reminder[s]?|escalation[s]?)\b/i, "notifications"],
139890
+ [/\b(database|banco|db|postgres|mysql|mongodb|redis|sqlite|orm|audit history|audit log|activity history)\b/i, "database"],
139702
139891
  [/\b(api\s+rest|rest\s+api|endpoint|rota|route|graphql|grpc)\b/i, "api-layer"],
139703
139892
  [/\b(docker|kubernetes|k8s|deploy|ci|cd|pipeline)\b/i, "infra"],
139704
- [/\b(dashboard|frontend|interface|ui|tela|p[aá]gina)\b/i, "frontend"]
139893
+ [/\b(dashboard|frontend|interface|ui|tela|p[aá]gina|admin view|admin panel|admin console)\b/i, "frontend"]
139705
139894
  ];
139706
139895
  for (const [regex, label] of subsystemPatterns) {
139707
139896
  if (regex.test(text)) signals.push(label);
@@ -139858,6 +140047,38 @@ function loadMatrix() {
139858
140047
  cachedMatrix = _require3("../configs/triage-matrix.json");
139859
140048
  return cachedMatrix;
139860
140049
  }
140050
+ function buildChildDrafts(payload, issueNumber, effort) {
140051
+ const spec = payload.spec;
140052
+ const scopeItems = spec.scope_v1.filter((item) => item.trim().length > 0);
140053
+ const maxChildren = effort === "xlarge" ? 4 : 3;
140054
+ const chunkSize = 2;
140055
+ const drafts = [];
140056
+ for (let i2 = 0; i2 < scopeItems.length && drafts.length < maxChildren; i2 += chunkSize) {
140057
+ const slice = scopeItems.slice(i2, i2 + chunkSize);
140058
+ if (slice.length === 0) continue;
140059
+ const childIndex = drafts.length + 1;
140060
+ drafts.push({
140061
+ title: `${spec.title} \u2014 Part ${childIndex}`,
140062
+ description: [
140063
+ `## Objective`,
140064
+ spec.objective,
140065
+ "",
140066
+ `## Parent Issue`,
140067
+ `Parent issue: #${issueNumber}`,
140068
+ "",
140069
+ `## This Part`,
140070
+ ...slice.map((item) => `- ${item}`),
140071
+ "",
140072
+ `## Acceptance Criteria`,
140073
+ ...spec.acceptance_criteria.map((item) => `- ${item}`),
140074
+ "",
140075
+ `## Definition of Done`,
140076
+ ...spec.definition_of_done.map((item) => `- ${item}`)
140077
+ ].join("\n")
140078
+ });
140079
+ }
140080
+ return drafts;
140081
+ }
139861
140082
  var triageStep = {
139862
140083
  name: "triage",
139863
140084
  shouldRun: (payload) => !!payload.issues?.length && !payload.dry_run,
@@ -139877,9 +140098,9 @@ var triageStep = {
139877
140098
  totalRisks: (impact?.risk_areas?.length ?? 0) + (payload.security?.spec_security_notes?.length ?? 0),
139878
140099
  objective: spec.objective,
139879
140100
  rawIdea: payload.raw_idea,
139880
- acText: spec.acceptance_criteria.join(" "),
139881
- scopeText: spec.scope_v1.join(" "),
139882
- oosText: spec.out_of_scope.join(" "),
140101
+ acText: spec.acceptance_criteria.join("\n"),
140102
+ scopeText: spec.scope_v1.join("\n"),
140103
+ oosText: spec.out_of_scope.join("\n"),
139883
140104
  authSignal: payload.metadata?.auth_gate?.signal ?? false
139884
140105
  }, matrix);
139885
140106
  const repoUrl = payload.scaffold?.repo_url ?? payload.metadata?.repo_url ?? "";
@@ -139889,8 +140110,12 @@ var triageStep = {
139889
140110
  const initialLabel = resolvedConfig?.workflow.states[resolvedConfig.workflow.initial]?.label ?? "Planning";
139890
140111
  const allLabels = [decision.priorityLabel, decision.effortLabel];
139891
140112
  if (decision.typeLabel) allLabels.push(decision.typeLabel);
139892
- if (!decision.readyForDispatch) allLabels.push("needs-human");
140113
+ if (!decision.readyForDispatch || decision.specQualityBlock) allLabels.push("needs-human");
139893
140114
  const uniqueLabels = Array.from(new Set(allLabels.filter(Boolean)));
140115
+ const shouldDecompose = decision.readyForDispatch && !decision.specQualityBlock && ["large", "xlarge"].includes(decision.effort);
140116
+ const decompositionDrafts = shouldDecompose ? buildChildDrafts(payload, issue2.number, decision.effort) : [];
140117
+ const canDecompose = decompositionDrafts.length >= 2;
140118
+ let createdChildIssueNumbers = [];
139894
140119
  if (ctx.createIssueProvider && repoPath) {
139895
140120
  try {
139896
140121
  const { provider } = await ctx.createIssueProvider({
@@ -139900,7 +140125,36 @@ var triageStep = {
139900
140125
  for (const label of uniqueLabels) {
139901
140126
  await provider.addLabel(issue2.number, label);
139902
140127
  }
139903
- if (decision.readyForDispatch) {
140128
+ if (decision.specQualityBlock) {
140129
+ await provider.addComment(issue2.number, [
140130
+ "\u{1F6AB} Spec quality gate blocked automatic dispatch.",
140131
+ "",
140132
+ "The request needs a stronger objective, more concrete scope items, and verifiable acceptance criteria before creating execution tasks."
140133
+ ].join("\n"));
140134
+ } else if (canDecompose) {
140135
+ await provider.addLabel(issue2.number, "decomposition:parent");
140136
+ const childIssues = [];
140137
+ for (const draft of decompositionDrafts) {
140138
+ const child = await provider.createIssue(draft.title, draft.description, initialLabel);
140139
+ childIssues.push({ iid: child.iid, title: child.title, web_url: child.web_url });
140140
+ createdChildIssueNumbers.push(child.iid);
140141
+ for (const label of uniqueLabels) {
140142
+ await provider.addLabel(child.iid, label);
140143
+ }
140144
+ await provider.addLabel(child.iid, "decomposition:child");
140145
+ }
140146
+ await provider.addComment(issue2.number, [
140147
+ "## Decomposition Plan",
140148
+ ...childIssues.map((child) => `- [ ] #${child.iid} ${child.title}`)
140149
+ ].join("\n"));
140150
+ } else if (shouldDecompose) {
140151
+ await provider.addLabel(issue2.number, "needs-human");
140152
+ await provider.addComment(issue2.number, [
140153
+ "\u26A0\uFE0F Automatic decomposition was requested, but the generated spec did not yield at least two independently scoped child tasks.",
140154
+ "",
140155
+ "Refine the scope/acceptance criteria or split the plan manually before dispatch."
140156
+ ].join("\n"));
140157
+ } else if (decision.readyForDispatch) {
139904
140158
  await provider.transitionLabel(issue2.number, initialLabel, decision.targetState);
139905
140159
  const dispatchLabels = [];
139906
140160
  if (decision.targetState === "To Do" && decision.dispatchLabel) {
@@ -139965,8 +140219,14 @@ var triageStep = {
139965
140219
  project_channel_id: null,
139966
140220
  labels_applied: uniqueLabels,
139967
140221
  issue_number: issue2.number,
139968
- ready_for_dispatch: decision.readyForDispatch && decision.errors.length === 0,
139969
- errors: decision.errors
140222
+ ready_for_dispatch: decision.readyForDispatch && decision.errors.length === 0 && !decision.specQualityBlock && !canDecompose,
140223
+ errors: [
140224
+ ...decision.errors,
140225
+ ...decision.specQualityBlock ? ["spec_quality_block"] : [],
140226
+ ...shouldDecompose && !canDecompose ? ["decomposition_needs_human"] : []
140227
+ ],
140228
+ decomposition_mode: canDecompose ? "parent_child" : "none",
140229
+ child_issue_numbers: createdChildIssueNumbers
139970
140230
  };
139971
140231
  ctx.log(`Triage: ${triage.priority}, effort=${triage.effort}, ready=${triage.ready_for_dispatch}`);
139972
140232
  return {
@@ -141099,6 +141359,11 @@ function inferProjectSlug(text) {
141099
141359
  const slug = cleaned.toLowerCase().normalize("NFKD").replace(/[^\w\s-]/g, "").trim().replace(/\s+/g, "-").replace(/-+/g, "-").slice(0, 64);
141100
141360
  return slug || void 0;
141101
141361
  }
141362
+ function normalizeProjectNameCandidate(value) {
141363
+ const normalized = normalizeText3(value);
141364
+ if (!normalized) return void 0;
141365
+ return inferProjectSlug(normalized) ?? void 0;
141366
+ }
141102
141367
  function normalizeText3(value) {
141103
141368
  const trimmed = value?.trim();
141104
141369
  return trimmed ? trimmed : void 0;
@@ -141122,7 +141387,7 @@ function detectStackHint(text) {
141122
141387
  }
141123
141388
  function parseField(text, labels) {
141124
141389
  for (const label of labels) {
141125
- const regex = new RegExp(`^\\s*${label}\\s*:\\s*(.+)$`, "im");
141390
+ const regex = new RegExp(`(?:^|[\\n.,;!?]\\s*)${label}\\s*:\\s*(.+)$`, "im");
141126
141391
  const match = text.match(regex);
141127
141392
  if (match?.[1]) return normalizeText3(match[1]);
141128
141393
  }
@@ -141134,12 +141399,12 @@ function parseIdeaBlock(text) {
141134
141399
  }
141135
141400
  function parseExplicitProjectName(text) {
141136
141401
  const match = text.match(/\b(?:called|named|chamado)\s+[`"'“”‘’]?([a-z0-9][a-z0-9-]{1,63})[`"'“”‘’]?(?=$|[\s.,!?;:])/i);
141137
- return match?.[1]?.toLowerCase();
141402
+ return normalizeProjectNameCandidate(match?.[1]?.toLowerCase());
141138
141403
  }
141139
141404
  function parseBootstrapRequest(text) {
141140
141405
  const repoUrl = parseField(text, ["repository url", "repo url", "reposit[o\xF3]rio url", "github repo"]);
141141
141406
  const rawIdea = parseIdeaBlock(text) ?? text.trim();
141142
- const projectName = parseField(text, ["project name", "nome do projeto", "repo name", "repository name"]) ?? parseExplicitProjectName(text);
141407
+ const projectName = normalizeProjectNameCandidate(parseField(text, ["project name", "nome do projeto", "repo name", "repository name"])) ?? parseExplicitProjectName(text);
141143
141408
  const repoPath = parseField(text, ["local repository path", "repo path", "caminho local", "local path"]);
141144
141409
  const explicitStack = parseField(text, ["stack", "framework", "linguagem"]);
141145
141410
  const stackHint = (explicitStack ? normalizeStackHint(explicitStack) : "") || detectStackHint(text);
@@ -141318,25 +141583,26 @@ function parseClarificationResponse(text, session) {
141318
141583
  if (autoPatterns.test(normalizeUserResponse(text))) {
141319
141584
  return { recognized: true, projectName: inferProjectSlug(session.rawIdea) ?? `project-${Date.now()}`, stackHint: session.stackHint ?? void 0 };
141320
141585
  }
141321
- const nameField = parseField(text, ["project name", "nome do projeto", "nome", "name"]);
141586
+ const nameField = normalizeProjectNameCandidate(parseField(text, ["project name", "nome do projeto", "nome", "name"]));
141322
141587
  if (nameField) {
141323
141588
  return { recognized: true, projectName: nameField, stackHint: session.stackHint ?? void 0 };
141324
141589
  }
141325
- if (trimmed.length > 0 && trimmed.length <= 64) {
141326
- return { recognized: true, projectName: trimmed, stackHint: session.stackHint ?? void 0 };
141590
+ const normalizedInlineName = trimmed.length <= 64 ? normalizeProjectNameCandidate(trimmed) : void 0;
141591
+ if (normalizedInlineName) {
141592
+ return { recognized: true, projectName: normalizedInlineName, stackHint: session.stackHint ?? void 0 };
141327
141593
  }
141328
141594
  return { recognized: false };
141329
141595
  }
141596
+ const projectNameFromField = normalizeProjectNameCandidate(parseField(text, ["project name", "nome do projeto", "nome", "name"]));
141597
+ const nameItMatch = !projectNameFromField ? text.match(/(?:name|call|chamar?)\s+(?:it\s+)?([\w-]{2,64})/i) : null;
141598
+ const inlineName = projectNameFromField ?? normalizeProjectNameCandidate(nameItMatch?.[1]);
141330
141599
  const stackField = parseField(text, ["stack", "framework", "linguagem", "language"]);
141331
141600
  if (stackField) {
141332
141601
  const normalizedStack = normalizeStackHint(stackField) || detectStackHint(stackField) || stackField;
141333
- return { recognized: true, stackHint: normalizedStack };
141602
+ return { recognized: true, stackHint: normalizedStack, projectName: inlineName };
141334
141603
  }
141335
141604
  const detectedStack = detectStackHint(text);
141336
141605
  if (detectedStack) {
141337
- const projectNameFromField = parseField(text, ["project name", "nome do projeto", "nome", "name"]);
141338
- const nameItMatch = !projectNameFromField ? text.match(/(?:name|call|chamar?)\s+(?:it\s+)?([\w-]{2,64})/i) : null;
141339
- const inlineName = projectNameFromField ?? (nameItMatch ? nameItMatch[1].toLowerCase().replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-") || void 0 : void 0);
141340
141606
  return { recognized: true, stackHint: detectedStack, projectName: inlineName };
141341
141607
  }
141342
141608
  const lower2 = normalizeUserResponse(text);
@@ -141569,7 +141835,7 @@ async function handleTelegramBootstrapDmMessage(ctx, rawConversationId, content)
141569
141835
  conversationId,
141570
141836
  rawIdea: content,
141571
141837
  stackHint: parsed.stackHint ?? void 0,
141572
- projectName: parsed.projectSlug ?? void 0,
141838
+ projectName: parsed.projectName ?? parsed.projectSlug ?? void 0,
141573
141839
  status: "clarifying",
141574
141840
  pendingClarification: "scope",
141575
141841
  language
@@ -143955,6 +144221,7 @@ async function performHealthPass(workspaceDir, projectSlug, project, sessions, p
143955
144221
  workflow: resolvedConfig?.workflow,
143956
144222
  dispatchConfirmTimeoutMs: resolvedConfig?.timeouts?.dispatchConfirmTimeoutMs,
143957
144223
  healthGracePeriodMs: resolvedConfig?.timeouts?.healthGracePeriodMs,
144224
+ stallTimeoutMinutes,
143958
144225
  runCommand,
143959
144226
  notificationConfig: notifyConfig
143960
144227
  });
@@ -146332,8 +146599,18 @@ function stringifyAnswerRecord(record2) {
146332
146599
  }
146333
146600
  return normalized;
146334
146601
  }
146335
- function deriveProjectName(repoUrl, projectName) {
146602
+ function extractExplicitProjectName(text) {
146603
+ if (!text) return null;
146604
+ const fieldMatch = text.match(/(?:^|[\n.,;!?]\s*)(?:project name|repo name|repository name|nome do projeto)\s*:\s*([a-z0-9][a-z0-9-]{1,63})\b/i);
146605
+ if (fieldMatch?.[1]) return fieldMatch[1].trim().toLowerCase();
146606
+ const inlineMatch = text.match(/\b(?:called|named|chamado)\s+[`"'“”‘’]?([a-z0-9][a-z0-9-]{1,63})[`"'“”‘’]?(?=$|[\s.,!?;:])/i);
146607
+ if (inlineMatch?.[1]) return inlineMatch[1].trim().toLowerCase();
146608
+ return null;
146609
+ }
146610
+ function deriveProjectName(repoUrl, projectName, freeText) {
146336
146611
  if (projectName) return projectName;
146612
+ const parsedFromText = extractExplicitProjectName(freeText ?? null);
146613
+ if (parsedFromText) return parsedFromText;
146337
146614
  if (!repoUrl) return null;
146338
146615
  const sanitized = repoUrl.replace(/\/+$/, "");
146339
146616
  const lastSegment = sanitized.split("/").pop();
@@ -146423,9 +146700,11 @@ function normalizeGenesisRequest(params, existingPayload) {
146423
146700
  throw new Error('phase is required and must be "discover" or "commit"');
146424
146701
  }
146425
146702
  const repoUrl = normalizeOptionalString(params.repo_url) ?? existingPayload?.metadata.repo_url ?? null;
146703
+ const freeTextProjectSource = normalizeOptionalString(params.idea) ?? normalizeOptionalString(params.command) ?? existingPayload?.raw_idea ?? null;
146426
146704
  const projectName = deriveProjectName(
146427
146705
  repoUrl,
146428
- normalizeOptionalString(params.project_name) ?? existingPayload?.metadata.project_name ?? null
146706
+ normalizeOptionalString(params.project_name) ?? existingPayload?.metadata.project_name ?? null,
146707
+ freeTextProjectSource
146429
146708
  );
146430
146709
  const answersJson = {
146431
146710
  ...existingPayload?.metadata.answers_json ?? {},
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mestreyoda/fabrica",
3
- "version": "0.2.20",
3
+ "version": "0.2.21",
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",