@mestreyoda/fabrica 0.2.20 → 0.2.22

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 +833 -472
  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.22") {
113909
+ return "0.2.22";
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
+ "Allows operators to complete the primary workflow end to end as requested",
118622
+ "Validates and enforces the role, permission, or delivery constraints described in the request",
118623
+ "Processes the asynchronous or background behavior required 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: {
@@ -139575,69 +139764,350 @@ var securityReviewStep = {
139575
139764
 
139576
139765
  // lib/intake/steps/create-task.ts
139577
139766
  init_workflow();
139578
- function buildIssueBody(payload) {
139579
- const spec = payload.spec;
139580
- const sections = [];
139581
- sections.push(`## Objetivo
139582
-
139583
- ${spec.objective}`);
139584
- sections.push(`## Escopo V1
139585
-
139586
- ${spec.scope_v1.map((s2) => `- ${s2}`).join("\n")}`);
139587
- if (spec.out_of_scope.length > 0) {
139588
- sections.push(`## Fora de escopo
139589
139767
 
139590
- ${spec.out_of_scope.map((s2) => `- ${s2}`).join("\n")}`);
139768
+ // lib/dispatch/telegram-bootstrap-session.ts
139769
+ init_constants();
139770
+ import { createHash as createHash4, randomUUID as randomUUID4 } from "node:crypto";
139771
+ import fs34 from "node:fs/promises";
139772
+ import path35 from "node:path";
139773
+ var SESSION_TTL_MS = 10 * 6e4;
139774
+ var CLASSIFYING_TTL_MS = 15e3;
139775
+ var RELEASED_CLASSIFY_ERRORS = /* @__PURE__ */ new Set([
139776
+ "classify_invalid_result",
139777
+ "classify_low_confidence",
139778
+ "classify_not_project",
139779
+ "classify_result_expired"
139780
+ ]);
139781
+ function toCanonicalTelegramBootstrapConversationId(conversationId) {
139782
+ const trimmed = conversationId.trim();
139783
+ if (!trimmed) return trimmed;
139784
+ return trimmed.startsWith("telegram:") ? trimmed : `telegram:${trimmed}`;
139785
+ }
139786
+ function sessionsDir(workspaceDir) {
139787
+ return path35.join(workspaceDir, DATA_DIR, "bootstrap-sessions");
139788
+ }
139789
+ function sessionPath(workspaceDir, conversationId) {
139790
+ return path35.join(
139791
+ sessionsDir(workspaceDir),
139792
+ `${toCanonicalTelegramBootstrapConversationId(conversationId)}.json`
139793
+ );
139794
+ }
139795
+ function alternateSessionPath(workspaceDir, conversationId) {
139796
+ const canonical = toCanonicalTelegramBootstrapConversationId(conversationId);
139797
+ const bare = canonical.startsWith("telegram:") ? canonical.slice("telegram:".length) : canonical;
139798
+ if (!bare || bare === canonical) return null;
139799
+ return path35.join(sessionsDir(workspaceDir), `${bare}.json`);
139800
+ }
139801
+ function normalizeStoredSession(session) {
139802
+ const canonicalConversationId = toCanonicalTelegramBootstrapConversationId(session.conversationId);
139803
+ if (canonicalConversationId === session.conversationId) {
139804
+ return session;
139591
139805
  }
139592
- sections.push(`## Acceptance Criteria
139593
-
139594
- ${spec.acceptance_criteria.map((s2) => `- [ ] ${s2}`).join("\n")}`);
139595
- sections.push(`## Definition of Done
139596
-
139597
- ${spec.definition_of_done.map((s2) => `- [ ] ${s2}`).join("\n")}`);
139598
- if (spec.constraints && spec.constraints !== "None specified") {
139599
- sections.push(`## Constraints
139600
-
139601
- ${spec.constraints}`);
139806
+ return {
139807
+ ...session,
139808
+ id: buildBootstrapSessionId(canonicalConversationId, session.rawIdea),
139809
+ conversationId: canonicalConversationId
139810
+ };
139811
+ }
139812
+ function stableHash(input) {
139813
+ return createHash4("sha256").update(input).digest("hex").slice(0, 16);
139814
+ }
139815
+ function buildBootstrapSessionId(conversationId, rawIdea) {
139816
+ return `tgdm-${conversationId}-${stableHash(rawIdea.trim().toLowerCase())}`;
139817
+ }
139818
+ function buildBootstrapRequestFingerprint(input) {
139819
+ return stableHash(JSON.stringify({
139820
+ rawIdea: input.rawIdea.trim().toLowerCase(),
139821
+ projectName: input.projectName?.trim().toLowerCase() || null,
139822
+ stackHint: input.stackHint?.trim().toLowerCase() || null,
139823
+ repoUrl: input.repoUrl?.trim().toLowerCase() || null,
139824
+ repoPath: input.repoPath?.trim().toLowerCase() || null
139825
+ }));
139826
+ }
139827
+ function buildBootstrapRequestHash(input) {
139828
+ return buildBootstrapRequestFingerprint(input);
139829
+ }
139830
+ function nextSuppressUntil(status) {
139831
+ const ttl = status === "classifying" || status === "pending_classify" ? CLASSIFYING_TTL_MS : SESSION_TTL_MS;
139832
+ return new Date(Date.now() + ttl).toISOString();
139833
+ }
139834
+ function isReleasedClassifyFailure(error48) {
139835
+ return Boolean(error48 && RELEASED_CLASSIFY_ERRORS.has(error48));
139836
+ }
139837
+ function resolveNullableField(inputValue, existingValue, fallback = null) {
139838
+ return inputValue !== void 0 ? inputValue : existingValue ?? fallback;
139839
+ }
139840
+ function defaultNextRetryAtForStatus(status, existingValue) {
139841
+ if (status === "bootstrapping" || status === "dispatching") {
139842
+ return existingValue ?? null;
139602
139843
  }
139603
- if (spec.risks.length > 0) {
139604
- sections.push(`## Risks
139605
-
139606
- ${spec.risks.map((s2) => `- ${s2}`).join("\n")}`);
139844
+ return null;
139845
+ }
139846
+ var MONOTONIC_BOOTSTRAP_FIELDS = [
139847
+ "ackSentAt",
139848
+ "projectRegisteredAt",
139849
+ "topicKickoffSentAt",
139850
+ "projectTickedAt",
139851
+ "completionAckSentAt"
139852
+ ];
139853
+ function shouldPersistBootstrapCheckpoint(current, next) {
139854
+ if (!current) return { ok: true };
139855
+ if (current.conversationId !== next.conversationId) return { ok: true };
139856
+ const currentAttemptSeq = current.attemptSeq ?? null;
139857
+ const nextAttemptSeq = next.attemptSeq ?? null;
139858
+ if (currentAttemptSeq != null && nextAttemptSeq != null && nextAttemptSeq < currentAttemptSeq) {
139859
+ return { ok: false, reason: "stale_regression" };
139607
139860
  }
139608
- if (payload.qa_contract) {
139609
- sections.push(`## QA Contract
139610
-
139611
- QA contract generated with ${payload.qa_contract.gates.length} gates. Canonical QA Evidence must be posted in the PR body.`);
139861
+ const sameAttempt = Boolean(
139862
+ current.attemptId && next.attemptId && current.attemptSeq != null && next.attemptSeq != null && current.attemptId === next.attemptId && current.attemptSeq === next.attemptSeq
139863
+ );
139864
+ if (!sameAttempt) return { ok: true };
139865
+ for (const field of MONOTONIC_BOOTSTRAP_FIELDS) {
139866
+ if (current[field] && !next[field]) {
139867
+ return { ok: false, reason: "stale_regression" };
139868
+ }
139612
139869
  }
139613
- sections.push(`---
139614
- _Generated by Genesis Pipeline (session: ${payload.session_id})_`);
139615
- return sections.join("\n\n");
139870
+ return { ok: true };
139616
139871
  }
139617
- var createTaskStep = {
139618
- name: "create-task",
139619
- shouldRun: (payload) => !!payload.spec && !payload.dry_run && payload.metadata?.project_registered === true && Boolean(
139620
- payload.provisioning?.repo_local || payload.provisioning?.repo_url || payload.scaffold?.repo_local || payload.scaffold?.repo_url || payload.metadata?.repo_path || payload.metadata?.repo_url
139621
- ),
139622
- async execute(payload, ctx) {
139623
- if (payload.metadata?.project_registered !== true) {
139624
- throw new Error("Project must be registered successfully before issue creation");
139625
- }
139626
- const spec = payload.spec;
139627
- ctx.log(`Creating issue: "${spec.title}"`);
139628
- const repoUrl = payload.provisioning?.repo_url ?? payload.scaffold?.repo_url ?? payload.metadata?.repo_url;
139629
- const repoPath = payload.provisioning?.repo_local ?? payload.scaffold?.repo_local ?? payload.metadata?.repo_path ?? payload.project_map?.root ?? void 0;
139630
- const projectSlug = payload.metadata?.project_slug ?? payload.scaffold?.project_slug ?? payload.project_map?.project_slug;
139631
- const resolvedConfig = await loadConfig(ctx.workspaceDir, projectSlug ?? void 0);
139632
- const initialLabel = resolvedConfig.workflow.states[resolvedConfig.workflow.initial]?.label ?? DEFAULT_WORKFLOW.states[DEFAULT_WORKFLOW.initial]?.label ?? "Planning";
139633
- const initialLabelColor = getLabelColors(resolvedConfig.workflow)[initialLabel] ?? getLabelColors(DEFAULT_WORKFLOW)[initialLabel] ?? "#95a5a6";
139634
- const body = buildIssueBody(payload);
139635
- const title = spec.title;
139636
- if (ctx.createIssueProvider && repoPath) {
139637
- const { provider } = await ctx.createIssueProvider({
139638
- repoPath,
139639
- projectSlug
139640
- });
139872
+ async function readTelegramBootstrapSession(workspaceDir, conversationId) {
139873
+ const canonicalConversationId = toCanonicalTelegramBootstrapConversationId(conversationId);
139874
+ const paths = [
139875
+ sessionPath(workspaceDir, canonicalConversationId),
139876
+ alternateSessionPath(workspaceDir, conversationId)
139877
+ ].filter((entry) => Boolean(entry));
139878
+ try {
139879
+ for (const candidatePath of paths) {
139880
+ try {
139881
+ const raw = await fs34.readFile(candidatePath, "utf-8");
139882
+ const session = normalizeStoredSession(JSON.parse(raw));
139883
+ if (session.status === "failed" && isReleasedClassifyFailure(session.error)) {
139884
+ await deleteTelegramBootstrapSession(workspaceDir, canonicalConversationId);
139885
+ return null;
139886
+ }
139887
+ return session;
139888
+ } catch (error48) {
139889
+ if (error48?.code === "ENOENT") continue;
139890
+ throw error48;
139891
+ }
139892
+ }
139893
+ return null;
139894
+ } catch (error48) {
139895
+ if (error48?.code === "ENOENT") return null;
139896
+ throw error48;
139897
+ }
139898
+ }
139899
+ async function deleteTelegramBootstrapSession(workspaceDir, conversationId) {
139900
+ await fs34.unlink(sessionPath(workspaceDir, conversationId)).catch(() => {
139901
+ });
139902
+ const legacyPath = alternateSessionPath(workspaceDir, conversationId);
139903
+ if (legacyPath) {
139904
+ await fs34.unlink(legacyPath).catch(() => {
139905
+ });
139906
+ }
139907
+ }
139908
+ async function writeTelegramBootstrapSession(workspaceDir, session) {
139909
+ const canonicalSession = normalizeStoredSession(session);
139910
+ const dir = sessionsDir(workspaceDir);
139911
+ await fs34.mkdir(dir, { recursive: true });
139912
+ const file2 = sessionPath(workspaceDir, canonicalSession.conversationId);
139913
+ const tmp = `${file2}.${randomUUID4()}.tmp`;
139914
+ await fs34.writeFile(tmp, JSON.stringify(canonicalSession, null, 2) + "\n", "utf-8");
139915
+ await fs34.rename(tmp, file2);
139916
+ const legacyPath = alternateSessionPath(workspaceDir, session.conversationId);
139917
+ if (legacyPath) {
139918
+ await fs34.unlink(legacyPath).catch(() => {
139919
+ });
139920
+ }
139921
+ }
139922
+ async function upsertTelegramBootstrapSession(workspaceDir, input) {
139923
+ const conversationId = toCanonicalTelegramBootstrapConversationId(input.conversationId);
139924
+ const existing = await readTelegramBootstrapSession(workspaceDir, conversationId);
139925
+ const resolvedSourceRoute = resolveNullableField(input.sourceRoute, existing?.sourceRoute);
139926
+ const resolvedProjectRoute = resolveNullableField(input.projectRoute, existing?.projectRoute);
139927
+ const resolvedProjectName = resolveNullableField(input.projectName, existing?.projectName);
139928
+ const resolvedStackHint = resolveNullableField(input.stackHint, existing?.stackHint);
139929
+ const resolvedRepoUrl = resolveNullableField(input.repoUrl, existing?.repoUrl);
139930
+ const resolvedRepoPath = resolveNullableField(input.repoPath, existing?.repoPath);
139931
+ const resolvedProjectSlug = resolveNullableField(input.projectSlug, existing?.projectSlug);
139932
+ const resolvedIssueId = resolveNullableField(input.issueId, existing?.issueId);
139933
+ const resolvedIssueUrl = resolveNullableField(input.issueUrl, existing?.issueUrl);
139934
+ const resolvedTriageReadyForDispatch = resolveNullableField(input.triageReadyForDispatch, existing?.triageReadyForDispatch);
139935
+ const resolvedTriageErrors = input.triageErrors !== void 0 ? input.triageErrors : existing?.triageErrors ?? null;
139936
+ const resolvedMessageThreadId = resolveNullableField(input.messageThreadId, existing?.messageThreadId);
139937
+ const resolvedProjectChannelId = resolveNullableField(input.projectChannelId, existing?.projectChannelId);
139938
+ const resolvedAttemptCount = resolveNullableField(input.attemptCount, existing?.attemptCount, 0);
139939
+ const resolvedAttemptId = resolveNullableField(input.attemptId, existing?.attemptId);
139940
+ const resolvedAttemptSeq = resolveNullableField(input.attemptSeq, existing?.attemptSeq);
139941
+ const resolvedClassifySessionKey = resolveNullableField(input.classifySessionKey, existing?.classifySessionKey);
139942
+ const resolvedClassifyRunId = resolveNullableField(input.classifyRunId, existing?.classifyRunId);
139943
+ const resolvedClassifyStartedAt = resolveNullableField(input.classifyStartedAt, existing?.classifyStartedAt);
139944
+ const resolvedBootstrapStep = resolveNullableField(input.bootstrapStep, existing?.bootstrapStep);
139945
+ const resolvedNextRetryAt = input.nextRetryAt !== void 0 ? input.nextRetryAt : defaultNextRetryAtForStatus(input.status, existing?.nextRetryAt);
139946
+ const resolvedAckSentAt = resolveNullableField(input.ackSentAt, existing?.ackSentAt);
139947
+ const resolvedProjectRegisteredAt = resolveNullableField(input.projectRegisteredAt, existing?.projectRegisteredAt);
139948
+ const resolvedTopicKickoffSentAt = resolveNullableField(input.topicKickoffSentAt, existing?.topicKickoffSentAt);
139949
+ const resolvedProjectTickedAt = resolveNullableField(input.projectTickedAt, existing?.projectTickedAt);
139950
+ const resolvedCompletionAckSentAt = resolveNullableField(input.completionAckSentAt, existing?.completionAckSentAt);
139951
+ const resolvedError = input.error !== void 0 ? input.error : input.lastError !== void 0 ? input.lastError : existing?.error ?? existing?.lastError ?? null;
139952
+ const requestHash = buildBootstrapRequestHash({
139953
+ rawIdea: input.rawIdea,
139954
+ projectName: resolvedProjectName,
139955
+ stackHint: resolvedStackHint,
139956
+ repoUrl: resolvedRepoUrl,
139957
+ repoPath: resolvedRepoPath
139958
+ });
139959
+ const now2 = (/* @__PURE__ */ new Date()).toISOString();
139960
+ const session = {
139961
+ id: existing?.id ?? buildBootstrapSessionId(conversationId, input.rawIdea),
139962
+ conversationId,
139963
+ sourceChannel: input.sourceChannel ?? input.sourceRoute?.channel ?? existing?.sourceChannel ?? "telegram",
139964
+ sourceRoute: resolvedSourceRoute,
139965
+ projectRoute: resolvedProjectRoute,
139966
+ requestHash,
139967
+ requestFingerprint: requestHash,
139968
+ lastCompletedRequestHash: input.status === "completed" ? requestHash : existing?.lastCompletedRequestHash ?? null,
139969
+ rawIdea: input.rawIdea,
139970
+ projectName: resolvedProjectName,
139971
+ stackHint: resolvedStackHint,
139972
+ repoUrl: resolvedRepoUrl,
139973
+ repoPath: resolvedRepoPath,
139974
+ projectSlug: resolvedProjectSlug,
139975
+ issueId: resolvedIssueId,
139976
+ issueUrl: resolvedIssueUrl,
139977
+ triageReadyForDispatch: resolvedTriageReadyForDispatch,
139978
+ triageErrors: resolvedTriageErrors,
139979
+ messageThreadId: resolvedMessageThreadId,
139980
+ projectChannelId: resolvedProjectChannelId,
139981
+ language: input.language ?? existing?.language,
139982
+ status: input.status,
139983
+ attemptId: resolvedAttemptId,
139984
+ attemptSeq: resolvedAttemptSeq,
139985
+ classifySessionKey: resolvedClassifySessionKey,
139986
+ classifyRunId: resolvedClassifyRunId,
139987
+ classifyStartedAt: resolvedClassifyStartedAt,
139988
+ bootstrapStep: resolvedBootstrapStep,
139989
+ attemptCount: resolvedAttemptCount,
139990
+ lastError: resolvedError,
139991
+ nextRetryAt: resolvedNextRetryAt,
139992
+ ackSentAt: resolvedAckSentAt,
139993
+ projectRegisteredAt: resolvedProjectRegisteredAt,
139994
+ topicKickoffSentAt: resolvedTopicKickoffSentAt,
139995
+ projectTickedAt: resolvedProjectTickedAt,
139996
+ completionAckSentAt: resolvedCompletionAckSentAt,
139997
+ pendingClarification: input.pendingClarification !== void 0 ? input.pendingClarification : existing?.pendingClarification ?? null,
139998
+ orphanedArtifacts: input.orphanedArtifacts !== void 0 ? input.orphanedArtifacts : existing?.orphanedArtifacts ?? null,
139999
+ createdAt: existing?.createdAt ?? now2,
140000
+ updatedAt: now2,
140001
+ suppressUntil: nextSuppressUntil(input.status),
140002
+ error: resolvedError
140003
+ };
140004
+ const writeDecision = shouldPersistBootstrapCheckpoint(existing, session);
140005
+ if (!writeDecision.ok && existing) {
140006
+ return existing;
140007
+ }
140008
+ await writeTelegramBootstrapSession(workspaceDir, session);
140009
+ return session;
140010
+ }
140011
+ function shouldSuppressTelegramBootstrapReply(session, request) {
140012
+ if (!session) return false;
140013
+ if (isTelegramBootstrapSessionExpired(session)) return false;
140014
+ if (session.status === "completed" || session.status === "failed") {
140015
+ if (session.status === "failed" && isReleasedClassifyFailure(session.error)) return false;
140016
+ if (!request) return false;
140017
+ return buildBootstrapRequestFingerprint(request) === session.requestHash;
140018
+ }
140019
+ if (!request) return true;
140020
+ return buildBootstrapRequestFingerprint(request) === session.requestHash;
140021
+ }
140022
+ function isTelegramBootstrapSessionExpired(session, now2 = Date.now()) {
140023
+ if (!session) return false;
140024
+ const suppressUntil = Date.parse(session.suppressUntil);
140025
+ return !Number.isNaN(suppressUntil) && suppressUntil < now2;
140026
+ }
140027
+ function isRecoverableTelegramBootstrapSession(session) {
140028
+ return session?.status === "bootstrapping" || session?.status === "dispatching";
140029
+ }
140030
+ function isClaimableTelegramBootstrapSession(session, now2 = Date.now()) {
140031
+ if (!isRecoverableTelegramBootstrapSession(session)) return false;
140032
+ if (!session.nextRetryAt) return true;
140033
+ const retryAt = Date.parse(session.nextRetryAt);
140034
+ return Number.isNaN(retryAt) || retryAt <= now2;
140035
+ }
140036
+ function isSupersededTelegramBootstrapAttempt(current, candidate) {
140037
+ if (!current || !candidate) return false;
140038
+ if (current.conversationId !== candidate.conversationId) return true;
140039
+ const currentHasAttempt = current.attemptId != null && current.attemptSeq != null;
140040
+ const candidateHasAttempt = candidate.attemptId != null && candidate.attemptSeq != null;
140041
+ if (currentHasAttempt || candidateHasAttempt) {
140042
+ return current.attemptId !== candidate.attemptId || current.attemptSeq !== candidate.attemptSeq;
140043
+ }
140044
+ return current.requestHash !== candidate.requestHash || current.status !== candidate.status || current.updatedAt !== candidate.updatedAt;
140045
+ }
140046
+
140047
+ // lib/intake/steps/create-task.ts
140048
+ function buildIssueBody(payload) {
140049
+ const spec = payload.spec;
140050
+ const sections = [];
140051
+ sections.push(`## Objetivo
140052
+
140053
+ ${spec.objective}`);
140054
+ sections.push(`## Escopo V1
140055
+
140056
+ ${spec.scope_v1.map((s2) => `- ${s2}`).join("\n")}`);
140057
+ if (spec.out_of_scope.length > 0) {
140058
+ sections.push(`## Fora de escopo
140059
+
140060
+ ${spec.out_of_scope.map((s2) => `- ${s2}`).join("\n")}`);
140061
+ }
140062
+ sections.push(`## Acceptance Criteria
140063
+
140064
+ ${spec.acceptance_criteria.map((s2) => `- [ ] ${s2}`).join("\n")}`);
140065
+ sections.push(`## Definition of Done
140066
+
140067
+ ${spec.definition_of_done.map((s2) => `- [ ] ${s2}`).join("\n")}`);
140068
+ if (spec.constraints && spec.constraints !== "None specified") {
140069
+ sections.push(`## Constraints
140070
+
140071
+ ${spec.constraints}`);
140072
+ }
140073
+ if (spec.risks.length > 0) {
140074
+ sections.push(`## Risks
140075
+
140076
+ ${spec.risks.map((s2) => `- ${s2}`).join("\n")}`);
140077
+ }
140078
+ if (payload.qa_contract) {
140079
+ sections.push(`## QA Contract
140080
+
140081
+ QA contract generated with ${payload.qa_contract.gates.length} gates. Canonical QA Evidence must be posted in the PR body.`);
140082
+ }
140083
+ sections.push(`---
140084
+ _Generated by Genesis Pipeline (session: ${payload.session_id})_`);
140085
+ return sections.join("\n\n");
140086
+ }
140087
+ var createTaskStep = {
140088
+ name: "create-task",
140089
+ shouldRun: (payload) => !!payload.spec && !payload.dry_run && payload.metadata?.project_registered === true && Boolean(
140090
+ payload.provisioning?.repo_local || payload.provisioning?.repo_url || payload.scaffold?.repo_local || payload.scaffold?.repo_url || payload.metadata?.repo_path || payload.metadata?.repo_url
140091
+ ),
140092
+ async execute(payload, ctx) {
140093
+ if (payload.metadata?.project_registered !== true) {
140094
+ throw new Error("Project must be registered successfully before issue creation");
140095
+ }
140096
+ const spec = payload.spec;
140097
+ ctx.log(`Creating issue: "${spec.title}"`);
140098
+ const repoUrl = payload.provisioning?.repo_url ?? payload.scaffold?.repo_url ?? payload.metadata?.repo_url;
140099
+ const repoPath = payload.provisioning?.repo_local ?? payload.scaffold?.repo_local ?? payload.metadata?.repo_path ?? payload.project_map?.root ?? void 0;
140100
+ const projectSlug = payload.metadata?.project_slug ?? payload.scaffold?.project_slug ?? payload.project_map?.project_slug;
140101
+ const resolvedConfig = await loadConfig(ctx.workspaceDir, projectSlug ?? void 0);
140102
+ const initialLabel = resolvedConfig.workflow.states[resolvedConfig.workflow.initial]?.label ?? DEFAULT_WORKFLOW.states[DEFAULT_WORKFLOW.initial]?.label ?? "Planning";
140103
+ const initialLabelColor = getLabelColors(resolvedConfig.workflow)[initialLabel] ?? getLabelColors(DEFAULT_WORKFLOW)[initialLabel] ?? "#95a5a6";
140104
+ const body = buildIssueBody(payload);
140105
+ const title = spec.title;
140106
+ if (ctx.createIssueProvider && repoPath) {
140107
+ const { provider } = await ctx.createIssueProvider({
140108
+ repoPath,
140109
+ projectSlug
140110
+ });
139641
140111
  await provider.ensureLabel(initialLabel, initialLabelColor);
139642
140112
  const created = await provider.createIssue(title, body, initialLabel);
139643
140113
  const issue3 = {
@@ -139646,6 +140116,26 @@ var createTaskStep = {
139646
140116
  created_at: (/* @__PURE__ */ new Date()).toISOString()
139647
140117
  };
139648
140118
  ctx.log(`Issue created via provider: #${issue3.number} \u2014 ${issue3.url}`);
140119
+ const bootstrapConversationId2 = payload.metadata?.source === "telegram-dm-bootstrap" ? payload.metadata?.channel_id : null;
140120
+ if (bootstrapConversationId2) {
140121
+ await upsertTelegramBootstrapSession(ctx.workspaceDir, {
140122
+ conversationId: String(bootstrapConversationId2),
140123
+ rawIdea: payload.raw_idea,
140124
+ projectName: payload.metadata?.project_name ?? null,
140125
+ stackHint: payload.metadata?.stack_hint ?? null,
140126
+ repoUrl: repoUrl ?? null,
140127
+ repoPath: repoPath ?? null,
140128
+ status: "dispatching",
140129
+ bootstrapStep: "project_registered",
140130
+ projectSlug: projectSlug ?? null,
140131
+ issueId: issue3.number,
140132
+ issueUrl: issue3.url,
140133
+ projectChannelId: payload.metadata?.channel_id ?? null,
140134
+ messageThreadId: payload.metadata?.message_thread_id ?? null,
140135
+ projectRegisteredAt: (/* @__PURE__ */ new Date()).toISOString()
140136
+ }).catch(() => {
140137
+ });
140138
+ }
139649
140139
  return {
139650
140140
  ...payload,
139651
140141
  step: "create-task",
@@ -139681,6 +140171,26 @@ var createTaskStep = {
139681
140171
  created_at: (/* @__PURE__ */ new Date()).toISOString()
139682
140172
  };
139683
140173
  ctx.log(`Issue created via compatibility fallback: #${issueNumber} \u2014 ${issueUrl}`);
140174
+ const bootstrapConversationId = payload.metadata?.source === "telegram-dm-bootstrap" ? payload.metadata?.channel_id : null;
140175
+ if (bootstrapConversationId) {
140176
+ await upsertTelegramBootstrapSession(ctx.workspaceDir, {
140177
+ conversationId: String(bootstrapConversationId),
140178
+ rawIdea: payload.raw_idea,
140179
+ projectName: payload.metadata?.project_name ?? null,
140180
+ stackHint: payload.metadata?.stack_hint ?? null,
140181
+ repoUrl: repoUrl ?? null,
140182
+ repoPath: repoPath ?? null,
140183
+ status: "dispatching",
140184
+ bootstrapStep: "project_registered",
140185
+ projectSlug: projectSlug ?? null,
140186
+ issueId: issue2.number,
140187
+ issueUrl: issue2.url,
140188
+ projectChannelId: payload.metadata?.channel_id ?? null,
140189
+ messageThreadId: payload.metadata?.message_thread_id ?? null,
140190
+ projectRegisteredAt: (/* @__PURE__ */ new Date()).toISOString()
140191
+ }).catch(() => {
140192
+ });
140193
+ }
139684
140194
  return {
139685
140195
  ...payload,
139686
140196
  step: "create-task",
@@ -139694,14 +140204,14 @@ function detectRawIdeaComplexity(rawIdea) {
139694
140204
  const text = rawIdea.toLowerCase();
139695
140205
  const signals = [];
139696
140206
  const subsystemPatterns = [
139697
- [/\b(worker|background.?job|queue|celery|bull|sidekiq|task.?runner)\b/i, "background-worker"],
140207
+ [/\b(worker|background.?job|background process|background service|background worker|queue|celery|bull|sidekiq|task.?runner|scheduler|job processor)\b/i, "background-worker"],
139698
140208
  [/\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"],
140209
+ [/\b(auth|oauth|jwt|login|register|session|user.?account|signup|role-based access|rbac|permission[s]?)\b/i, "auth"],
140210
+ [/\b(notif|notification[s]?|alert[s]?|email|sms|webhook|subscription|subscribe|reminder[s]?|escalation[s]?)\b/i, "notifications"],
140211
+ [/\b(database|banco|db|postgres|mysql|mongodb|redis|sqlite|orm|audit history|audit log|activity history)\b/i, "database"],
139702
140212
  [/\b(api\s+rest|rest\s+api|endpoint|rota|route|graphql|grpc)\b/i, "api-layer"],
139703
140213
  [/\b(docker|kubernetes|k8s|deploy|ci|cd|pipeline)\b/i, "infra"],
139704
- [/\b(dashboard|frontend|interface|ui|tela|p[aá]gina)\b/i, "frontend"]
140214
+ [/\b(dashboard|frontend|interface|ui|tela|p[aá]gina|admin view|admin panel|admin console)\b/i, "frontend"]
139705
140215
  ];
139706
140216
  for (const [regex, label] of subsystemPatterns) {
139707
140217
  if (regex.test(text)) signals.push(label);
@@ -139807,7 +140317,7 @@ function runTriageLogic(input, matrix) {
139807
140317
  objective: input.objective ?? input.rawIdea,
139808
140318
  scopeItems: (input.scopeText ?? "").split("\n").filter((l) => l.trim()),
139809
140319
  acceptanceCriteria: (input.acText ?? "").split("\n").filter((l) => l.trim()),
139810
- dod: input.scopeText ?? ""
140320
+ dod: input.dodText ?? ""
139811
140321
  });
139812
140322
  const specQualityBlock = specQualityErrors.length > 0;
139813
140323
  return {
@@ -139858,6 +140368,38 @@ function loadMatrix() {
139858
140368
  cachedMatrix = _require3("../configs/triage-matrix.json");
139859
140369
  return cachedMatrix;
139860
140370
  }
140371
+ function buildChildDrafts(payload, issueNumber, effort) {
140372
+ const spec = payload.spec;
140373
+ const scopeItems = spec.scope_v1.filter((item) => item.trim().length > 0);
140374
+ const maxChildren = effort === "xlarge" ? 4 : 3;
140375
+ const chunkSize = 2;
140376
+ const drafts = [];
140377
+ for (let i2 = 0; i2 < scopeItems.length && drafts.length < maxChildren; i2 += chunkSize) {
140378
+ const slice = scopeItems.slice(i2, i2 + chunkSize);
140379
+ if (slice.length === 0) continue;
140380
+ const childIndex = drafts.length + 1;
140381
+ drafts.push({
140382
+ title: `${spec.title} \u2014 Part ${childIndex}`,
140383
+ description: [
140384
+ `## Objective`,
140385
+ spec.objective,
140386
+ "",
140387
+ `## Parent Issue`,
140388
+ `Parent issue: #${issueNumber}`,
140389
+ "",
140390
+ `## This Part`,
140391
+ ...slice.map((item) => `- ${item}`),
140392
+ "",
140393
+ `## Acceptance Criteria`,
140394
+ ...spec.acceptance_criteria.map((item) => `- ${item}`),
140395
+ "",
140396
+ `## Definition of Done`,
140397
+ ...spec.definition_of_done.map((item) => `- ${item}`)
140398
+ ].join("\n")
140399
+ });
140400
+ }
140401
+ return drafts;
140402
+ }
139861
140403
  var triageStep = {
139862
140404
  name: "triage",
139863
140405
  shouldRun: (payload) => !!payload.issues?.length && !payload.dry_run,
@@ -139877,10 +140419,11 @@ var triageStep = {
139877
140419
  totalRisks: (impact?.risk_areas?.length ?? 0) + (payload.security?.spec_security_notes?.length ?? 0),
139878
140420
  objective: spec.objective,
139879
140421
  rawIdea: payload.raw_idea,
139880
- acText: spec.acceptance_criteria.join(" "),
139881
- scopeText: spec.scope_v1.join(" "),
139882
- oosText: spec.out_of_scope.join(" "),
139883
- authSignal: payload.metadata?.auth_gate?.signal ?? false
140422
+ acText: spec.acceptance_criteria.join("\n"),
140423
+ scopeText: spec.scope_v1.join("\n"),
140424
+ dodText: spec.definition_of_done.join("\n"),
140425
+ oosText: spec.out_of_scope.join("\n"),
140426
+ authSignal: /\b(login|register|jwt|oauth|auth|role-based access|rbac|permission)\b/i.test(`${payload.raw_idea} ${spec.objective}`)
139884
140427
  }, matrix);
139885
140428
  const repoUrl = payload.scaffold?.repo_url ?? payload.metadata?.repo_url ?? "";
139886
140429
  const repoPath = payload.provisioning?.repo_local ?? payload.scaffold?.repo_local ?? payload.metadata?.repo_path ?? payload.project_map?.root ?? void 0;
@@ -139889,8 +140432,12 @@ var triageStep = {
139889
140432
  const initialLabel = resolvedConfig?.workflow.states[resolvedConfig.workflow.initial]?.label ?? "Planning";
139890
140433
  const allLabels = [decision.priorityLabel, decision.effortLabel];
139891
140434
  if (decision.typeLabel) allLabels.push(decision.typeLabel);
139892
- if (!decision.readyForDispatch) allLabels.push("needs-human");
140435
+ if (!decision.readyForDispatch || decision.specQualityBlock) allLabels.push("needs-human");
139893
140436
  const uniqueLabels = Array.from(new Set(allLabels.filter(Boolean)));
140437
+ const shouldDecompose = decision.readyForDispatch && !decision.specQualityBlock && ["large", "xlarge"].includes(decision.effort);
140438
+ const decompositionDrafts = shouldDecompose ? buildChildDrafts(payload, issue2.number, decision.effort) : [];
140439
+ const canDecompose = decompositionDrafts.length >= 2;
140440
+ let createdChildIssueNumbers = [];
139894
140441
  if (ctx.createIssueProvider && repoPath) {
139895
140442
  try {
139896
140443
  const { provider } = await ctx.createIssueProvider({
@@ -139900,7 +140447,36 @@ var triageStep = {
139900
140447
  for (const label of uniqueLabels) {
139901
140448
  await provider.addLabel(issue2.number, label);
139902
140449
  }
139903
- if (decision.readyForDispatch) {
140450
+ if (decision.specQualityBlock) {
140451
+ await provider.addComment(issue2.number, [
140452
+ "\u{1F6AB} Spec quality gate blocked automatic dispatch.",
140453
+ "",
140454
+ "The request needs a stronger objective, more concrete scope items, and verifiable acceptance criteria before creating execution tasks."
140455
+ ].join("\n"));
140456
+ } else if (canDecompose) {
140457
+ await provider.addLabel(issue2.number, "decomposition:parent");
140458
+ const childIssues = [];
140459
+ for (const draft of decompositionDrafts) {
140460
+ const child = await provider.createIssue(draft.title, draft.description, initialLabel);
140461
+ childIssues.push({ iid: child.iid, title: child.title, web_url: child.web_url });
140462
+ createdChildIssueNumbers.push(child.iid);
140463
+ for (const label of uniqueLabels) {
140464
+ await provider.addLabel(child.iid, label);
140465
+ }
140466
+ await provider.addLabel(child.iid, "decomposition:child");
140467
+ }
140468
+ await provider.addComment(issue2.number, [
140469
+ "## Decomposition Plan",
140470
+ ...childIssues.map((child) => `- [ ] #${child.iid} ${child.title}`)
140471
+ ].join("\n"));
140472
+ } else if (shouldDecompose) {
140473
+ await provider.addLabel(issue2.number, "needs-human");
140474
+ await provider.addComment(issue2.number, [
140475
+ "\u26A0\uFE0F Automatic decomposition was requested, but the generated spec did not yield at least two independently scoped child tasks.",
140476
+ "",
140477
+ "Refine the scope/acceptance criteria or split the plan manually before dispatch."
140478
+ ].join("\n"));
140479
+ } else if (decision.readyForDispatch) {
139904
140480
  await provider.transitionLabel(issue2.number, initialLabel, decision.targetState);
139905
140481
  const dispatchLabels = [];
139906
140482
  if (decision.targetState === "To Do" && decision.dispatchLabel) {
@@ -139965,10 +140541,37 @@ var triageStep = {
139965
140541
  project_channel_id: null,
139966
140542
  labels_applied: uniqueLabels,
139967
140543
  issue_number: issue2.number,
139968
- ready_for_dispatch: decision.readyForDispatch && decision.errors.length === 0,
139969
- errors: decision.errors
140544
+ ready_for_dispatch: decision.readyForDispatch && decision.errors.length === 0 && !decision.specQualityBlock && !canDecompose,
140545
+ errors: [
140546
+ ...decision.errors,
140547
+ ...decision.specQualityBlock ? ["spec_quality_block"] : [],
140548
+ ...shouldDecompose && !canDecompose ? ["decomposition_needs_human"] : []
140549
+ ],
140550
+ decomposition_mode: canDecompose ? "parent_child" : "none",
140551
+ child_issue_numbers: createdChildIssueNumbers
139970
140552
  };
139971
140553
  ctx.log(`Triage: ${triage.priority}, effort=${triage.effort}, ready=${triage.ready_for_dispatch}`);
140554
+ const bootstrapConversationId = payload.metadata?.source === "telegram-dm-bootstrap" ? payload.metadata?.channel_id : null;
140555
+ if (bootstrapConversationId) {
140556
+ await upsertTelegramBootstrapSession(ctx.workspaceDir, {
140557
+ conversationId: String(bootstrapConversationId),
140558
+ rawIdea: payload.raw_idea,
140559
+ projectName: payload.metadata?.project_name ?? null,
140560
+ stackHint: payload.metadata?.stack_hint ?? null,
140561
+ repoUrl: payload.provisioning?.repo_url ?? payload.scaffold?.repo_url ?? payload.metadata?.repo_url ?? null,
140562
+ repoPath: payload.provisioning?.repo_local ?? payload.scaffold?.repo_local ?? payload.metadata?.repo_path ?? null,
140563
+ status: triage.ready_for_dispatch ? "dispatching" : "completed",
140564
+ bootstrapStep: triage.ready_for_dispatch ? "project_ticked" : "completed",
140565
+ projectSlug: payload.metadata?.project_slug ?? payload.scaffold?.project_slug ?? null,
140566
+ issueId: issue2.number,
140567
+ issueUrl: issue2.url,
140568
+ projectChannelId: triage.project_channel_id ?? payload.metadata?.channel_id ?? null,
140569
+ messageThreadId: payload.metadata?.message_thread_id ?? null,
140570
+ triageReadyForDispatch: triage.ready_for_dispatch,
140571
+ triageErrors: triage.errors
140572
+ }).catch(() => {
140573
+ });
140574
+ }
139972
140575
  return {
139973
140576
  ...payload,
139974
140577
  step: "triage",
@@ -140316,23 +140919,23 @@ init_workflow();
140316
140919
 
140317
140920
  // lib/dispatch/dispatch-dedup.ts
140318
140921
  init_constants();
140319
- import fs34 from "node:fs/promises";
140320
- import path35 from "node:path";
140321
- import { createHash as createHash4 } from "node:crypto";
140922
+ import fs35 from "node:fs/promises";
140923
+ import path36 from "node:path";
140924
+ import { createHash as createHash5 } from "node:crypto";
140322
140925
  var DEDUP_FILE = "dispatch-dedup.ndjson";
140323
140926
  var BUCKET_MS = 5 * 6e4;
140324
140927
  var DEFAULT_TTL_MS2 = 30 * 6e4;
140325
140928
  function dedupPath(workspaceDir) {
140326
- return path35.join(workspaceDir, DATA_DIR, DEDUP_FILE);
140929
+ return path36.join(workspaceDir, DATA_DIR, DEDUP_FILE);
140327
140930
  }
140328
140931
  function computeDispatchId(projectSlug, issueId, role, level, now2 = Date.now()) {
140329
140932
  const bucket = Math.floor(now2 / BUCKET_MS);
140330
140933
  const input = `${projectSlug}:${issueId}:${role}:${level}:${bucket}`;
140331
- return createHash4("sha256").update(input).digest("hex").slice(0, 16);
140934
+ return createHash5("sha256").update(input).digest("hex").slice(0, 16);
140332
140935
  }
140333
140936
  async function readEntries2(filePath) {
140334
140937
  try {
140335
- const content = await fs34.readFile(filePath, "utf-8");
140938
+ const content = await fs35.readFile(filePath, "utf-8");
140336
140939
  return content.split("\n").filter(Boolean).map((line) => {
140337
140940
  try {
140338
140941
  return JSON.parse(line);
@@ -140351,9 +140954,9 @@ async function isDuplicate(workspaceDir, dispatchId) {
140351
140954
  }
140352
140955
  async function recordDispatch(workspaceDir, dispatchId) {
140353
140956
  const filePath = dedupPath(workspaceDir);
140354
- await fs34.mkdir(path35.dirname(filePath), { recursive: true });
140957
+ await fs35.mkdir(path36.dirname(filePath), { recursive: true });
140355
140958
  const entry = { id: dispatchId, ts: Date.now() };
140356
- await fs34.appendFile(filePath, JSON.stringify(entry) + "\n", "utf-8");
140959
+ await fs35.appendFile(filePath, JSON.stringify(entry) + "\n", "utf-8");
140357
140960
  }
140358
140961
  async function cleanupExpired(workspaceDir, ttlMs = DEFAULT_TTL_MS2) {
140359
140962
  const filePath = dedupPath(workspaceDir);
@@ -140363,8 +140966,8 @@ async function cleanupExpired(workspaceDir, ttlMs = DEFAULT_TTL_MS2) {
140363
140966
  if (kept.length < entries.length) {
140364
140967
  const content = kept.map((e2) => JSON.stringify(e2)).join("\n") + (kept.length > 0 ? "\n" : "");
140365
140968
  const tmpPath = filePath + ".tmp";
140366
- await fs34.writeFile(tmpPath, content, "utf-8");
140367
- await fs34.rename(tmpPath, filePath);
140969
+ await fs35.writeFile(tmpPath, content, "utf-8");
140970
+ await fs35.rename(tmpPath, filePath);
140368
140971
  }
140369
140972
  }
140370
140973
 
@@ -140600,388 +141203,113 @@ async function projectTick(opts) {
140600
141203
  projectSlug,
140601
141204
  issueId: issue2.iid,
140602
141205
  issueTitle: issue2.title,
140603
- issueUrl: issue2.web_url,
140604
- role,
140605
- level: effectiveLevel,
140606
- sessionAction: existingSession ? "send" : "spawn",
140607
- announcement: `[DRY RUN] Would pick up #${issue2.iid}`
140608
- });
140609
- pickupCount++;
140610
- continue;
140611
- }
140612
- const environment = await ensureEnvironment({
140613
- workspaceDir,
140614
- projectSlug,
140615
- project: fresh,
140616
- stack,
140617
- mode: role === "tester" ? "tester" : "developer",
140618
- runCommand
140619
- });
140620
- if (!environment.ready) {
140621
- const environmentSkipReason = classifyEnvironmentGateSkip(environment.state);
140622
- skipped.push({ role, reason: environmentSkipReason });
140623
- await log(workspaceDir, "dispatch_blocked_environment_not_ready", {
140624
- projectSlug,
140625
- role,
140626
- issueId: issue2.iid,
140627
- reason: environmentSkipReason,
140628
- environmentStatus: environment.state.status,
140629
- nextProvisionRetryAt: environment.state.nextProvisionRetryAt ?? null,
140630
- lastProvisionError: environment.state.lastProvisionError ?? null
140631
- }).catch(() => {
140632
- });
140633
- continue;
140634
- }
140635
- }
140636
- if (dryRun) {
140637
- const existingSession = roleWorker.levels[effectiveLevel]?.[freeSlot]?.sessionKey;
140638
- pickups.push({
140639
- project: project.name,
140640
- projectSlug,
140641
- issueId: issue2.iid,
140642
- issueTitle: issue2.title,
140643
- issueUrl: issue2.web_url,
140644
- role,
140645
- level: effectiveLevel,
140646
- sessionAction: existingSession ? "send" : "spawn",
140647
- announcement: `[DRY RUN] Would pick up #${issue2.iid}`
140648
- });
140649
- } else {
140650
- try {
140651
- const dr = await dispatch({
140652
- workspaceDir,
140653
- agentId,
140654
- project: fresh,
140655
- issueId: issue2.iid,
140656
- issueTitle: issue2.title,
140657
- issueDescription: issue2.description ?? "",
140658
- issueUrl: issue2.web_url,
140659
- role,
140660
- level: effectiveLevel,
140661
- fromLabel: currentLabel,
140662
- toLabel: targetLabel,
140663
- provider,
140664
- pluginConfig,
140665
- sessionKey,
140666
- runtime,
140667
- slotIndex: freeSlot,
140668
- instanceName,
140669
- runCommand
140670
- });
140671
- pickups.push({
140672
- project: project.name,
140673
- projectSlug,
140674
- issueId: issue2.iid,
140675
- issueTitle: issue2.title,
140676
- issueUrl: issue2.web_url,
140677
- role,
140678
- level: dr.level,
140679
- sessionAction: dr.sessionAction,
140680
- announcement: dr.announcement
140681
- });
140682
- await recordDispatch(workspaceDir, dispatchId).catch(() => {
140683
- });
140684
- } catch (err) {
140685
- skipped.push({ role, reason: `Dispatch failed: ${err.message}` });
140686
- continue;
140687
- }
140688
- }
140689
- pickupCount++;
140690
- }
140691
- return { pickups, skipped };
140692
- }
140693
- function resolveLevelForIssue(issue2, role) {
140694
- const roleLevel = detectRoleLevelFromLabels(issue2.labels);
140695
- if (roleLevel?.role === role) return roleLevel.level;
140696
- if (roleLevel) {
140697
- const levels = getLevelsForRole(role);
140698
- if (levels.includes(roleLevel.level)) return roleLevel.level;
140699
- }
140700
- return selectLevel(issue2.title, issue2.description ?? "", role).level;
140701
- }
140702
- function getFeedbackQueueLabel(workflow) {
140703
- const developerQueues = getQueueLabels(workflow, "developer");
140704
- return developerQueues.find((label) => isFeedbackState(workflow, label)) ?? null;
140705
- }
140706
-
140707
- // lib/dispatch/telegram-bootstrap-hook.ts
140708
- init_audit();
140709
- init_zod();
140710
-
140711
- // lib/dispatch/telegram-bootstrap-session.ts
140712
- init_constants();
140713
- import { createHash as createHash5, randomUUID as randomUUID4 } from "node:crypto";
140714
- import fs35 from "node:fs/promises";
140715
- import path36 from "node:path";
140716
- var SESSION_TTL_MS = 10 * 6e4;
140717
- var CLASSIFYING_TTL_MS = 15e3;
140718
- var RELEASED_CLASSIFY_ERRORS = /* @__PURE__ */ new Set([
140719
- "classify_invalid_result",
140720
- "classify_low_confidence",
140721
- "classify_not_project",
140722
- "classify_result_expired"
140723
- ]);
140724
- function toCanonicalTelegramBootstrapConversationId(conversationId) {
140725
- const trimmed = conversationId.trim();
140726
- if (!trimmed) return trimmed;
140727
- return trimmed.startsWith("telegram:") ? trimmed : `telegram:${trimmed}`;
140728
- }
140729
- function sessionsDir(workspaceDir) {
140730
- return path36.join(workspaceDir, DATA_DIR, "bootstrap-sessions");
140731
- }
140732
- function sessionPath(workspaceDir, conversationId) {
140733
- return path36.join(
140734
- sessionsDir(workspaceDir),
140735
- `${toCanonicalTelegramBootstrapConversationId(conversationId)}.json`
140736
- );
140737
- }
140738
- function alternateSessionPath(workspaceDir, conversationId) {
140739
- const canonical = toCanonicalTelegramBootstrapConversationId(conversationId);
140740
- const bare = canonical.startsWith("telegram:") ? canonical.slice("telegram:".length) : canonical;
140741
- if (!bare || bare === canonical) return null;
140742
- return path36.join(sessionsDir(workspaceDir), `${bare}.json`);
140743
- }
140744
- function normalizeStoredSession(session) {
140745
- const canonicalConversationId = toCanonicalTelegramBootstrapConversationId(session.conversationId);
140746
- if (canonicalConversationId === session.conversationId) {
140747
- return session;
140748
- }
140749
- return {
140750
- ...session,
140751
- id: buildBootstrapSessionId(canonicalConversationId, session.rawIdea),
140752
- conversationId: canonicalConversationId
140753
- };
140754
- }
140755
- function stableHash(input) {
140756
- return createHash5("sha256").update(input).digest("hex").slice(0, 16);
140757
- }
140758
- function buildBootstrapSessionId(conversationId, rawIdea) {
140759
- return `tgdm-${conversationId}-${stableHash(rawIdea.trim().toLowerCase())}`;
140760
- }
140761
- function buildBootstrapRequestFingerprint(input) {
140762
- return stableHash(JSON.stringify({
140763
- rawIdea: input.rawIdea.trim().toLowerCase(),
140764
- projectName: input.projectName?.trim().toLowerCase() || null,
140765
- stackHint: input.stackHint?.trim().toLowerCase() || null,
140766
- repoUrl: input.repoUrl?.trim().toLowerCase() || null,
140767
- repoPath: input.repoPath?.trim().toLowerCase() || null
140768
- }));
140769
- }
140770
- function buildBootstrapRequestHash(input) {
140771
- return buildBootstrapRequestFingerprint(input);
140772
- }
140773
- function nextSuppressUntil(status) {
140774
- const ttl = status === "classifying" || status === "pending_classify" ? CLASSIFYING_TTL_MS : SESSION_TTL_MS;
140775
- return new Date(Date.now() + ttl).toISOString();
140776
- }
140777
- function isReleasedClassifyFailure(error48) {
140778
- return Boolean(error48 && RELEASED_CLASSIFY_ERRORS.has(error48));
140779
- }
140780
- function resolveNullableField(inputValue, existingValue, fallback = null) {
140781
- return inputValue !== void 0 ? inputValue : existingValue ?? fallback;
140782
- }
140783
- function defaultNextRetryAtForStatus(status, existingValue) {
140784
- if (status === "bootstrapping" || status === "dispatching") {
140785
- return existingValue ?? null;
140786
- }
140787
- return null;
140788
- }
140789
- var MONOTONIC_BOOTSTRAP_FIELDS = [
140790
- "ackSentAt",
140791
- "projectRegisteredAt",
140792
- "topicKickoffSentAt",
140793
- "projectTickedAt",
140794
- "completionAckSentAt"
140795
- ];
140796
- function shouldPersistBootstrapCheckpoint(current, next) {
140797
- if (!current) return { ok: true };
140798
- if (current.conversationId !== next.conversationId) return { ok: true };
140799
- const currentAttemptSeq = current.attemptSeq ?? null;
140800
- const nextAttemptSeq = next.attemptSeq ?? null;
140801
- if (currentAttemptSeq != null && nextAttemptSeq != null && nextAttemptSeq < currentAttemptSeq) {
140802
- return { ok: false, reason: "stale_regression" };
140803
- }
140804
- const sameAttempt = Boolean(
140805
- current.attemptId && next.attemptId && current.attemptSeq != null && next.attemptSeq != null && current.attemptId === next.attemptId && current.attemptSeq === next.attemptSeq
140806
- );
140807
- if (!sameAttempt) return { ok: true };
140808
- for (const field of MONOTONIC_BOOTSTRAP_FIELDS) {
140809
- if (current[field] && !next[field]) {
140810
- return { ok: false, reason: "stale_regression" };
141206
+ issueUrl: issue2.web_url,
141207
+ role,
141208
+ level: effectiveLevel,
141209
+ sessionAction: existingSession ? "send" : "spawn",
141210
+ announcement: `[DRY RUN] Would pick up #${issue2.iid}`
141211
+ });
141212
+ pickupCount++;
141213
+ continue;
141214
+ }
141215
+ const environment = await ensureEnvironment({
141216
+ workspaceDir,
141217
+ projectSlug,
141218
+ project: fresh,
141219
+ stack,
141220
+ mode: role === "tester" ? "tester" : "developer",
141221
+ runCommand
141222
+ });
141223
+ if (!environment.ready) {
141224
+ const environmentSkipReason = classifyEnvironmentGateSkip(environment.state);
141225
+ skipped.push({ role, reason: environmentSkipReason });
141226
+ await log(workspaceDir, "dispatch_blocked_environment_not_ready", {
141227
+ projectSlug,
141228
+ role,
141229
+ issueId: issue2.iid,
141230
+ reason: environmentSkipReason,
141231
+ environmentStatus: environment.state.status,
141232
+ nextProvisionRetryAt: environment.state.nextProvisionRetryAt ?? null,
141233
+ lastProvisionError: environment.state.lastProvisionError ?? null
141234
+ }).catch(() => {
141235
+ });
141236
+ continue;
141237
+ }
140811
141238
  }
140812
- }
140813
- return { ok: true };
140814
- }
140815
- async function readTelegramBootstrapSession(workspaceDir, conversationId) {
140816
- const canonicalConversationId = toCanonicalTelegramBootstrapConversationId(conversationId);
140817
- const paths = [
140818
- sessionPath(workspaceDir, canonicalConversationId),
140819
- alternateSessionPath(workspaceDir, conversationId)
140820
- ].filter((entry) => Boolean(entry));
140821
- try {
140822
- for (const candidatePath of paths) {
141239
+ if (dryRun) {
141240
+ const existingSession = roleWorker.levels[effectiveLevel]?.[freeSlot]?.sessionKey;
141241
+ pickups.push({
141242
+ project: project.name,
141243
+ projectSlug,
141244
+ issueId: issue2.iid,
141245
+ issueTitle: issue2.title,
141246
+ issueUrl: issue2.web_url,
141247
+ role,
141248
+ level: effectiveLevel,
141249
+ sessionAction: existingSession ? "send" : "spawn",
141250
+ announcement: `[DRY RUN] Would pick up #${issue2.iid}`
141251
+ });
141252
+ } else {
140823
141253
  try {
140824
- const raw = await fs35.readFile(candidatePath, "utf-8");
140825
- const session = normalizeStoredSession(JSON.parse(raw));
140826
- if (session.status === "failed" && isReleasedClassifyFailure(session.error)) {
140827
- await deleteTelegramBootstrapSession(workspaceDir, canonicalConversationId);
140828
- return null;
140829
- }
140830
- return session;
140831
- } catch (error48) {
140832
- if (error48?.code === "ENOENT") continue;
140833
- throw error48;
141254
+ const dr = await dispatch({
141255
+ workspaceDir,
141256
+ agentId,
141257
+ project: fresh,
141258
+ issueId: issue2.iid,
141259
+ issueTitle: issue2.title,
141260
+ issueDescription: issue2.description ?? "",
141261
+ issueUrl: issue2.web_url,
141262
+ role,
141263
+ level: effectiveLevel,
141264
+ fromLabel: currentLabel,
141265
+ toLabel: targetLabel,
141266
+ provider,
141267
+ pluginConfig,
141268
+ sessionKey,
141269
+ runtime,
141270
+ slotIndex: freeSlot,
141271
+ instanceName,
141272
+ runCommand
141273
+ });
141274
+ pickups.push({
141275
+ project: project.name,
141276
+ projectSlug,
141277
+ issueId: issue2.iid,
141278
+ issueTitle: issue2.title,
141279
+ issueUrl: issue2.web_url,
141280
+ role,
141281
+ level: dr.level,
141282
+ sessionAction: dr.sessionAction,
141283
+ announcement: dr.announcement
141284
+ });
141285
+ await recordDispatch(workspaceDir, dispatchId).catch(() => {
141286
+ });
141287
+ } catch (err) {
141288
+ skipped.push({ role, reason: `Dispatch failed: ${err.message}` });
141289
+ continue;
140834
141290
  }
140835
141291
  }
140836
- return null;
140837
- } catch (error48) {
140838
- if (error48?.code === "ENOENT") return null;
140839
- throw error48;
140840
- }
140841
- }
140842
- async function deleteTelegramBootstrapSession(workspaceDir, conversationId) {
140843
- await fs35.unlink(sessionPath(workspaceDir, conversationId)).catch(() => {
140844
- });
140845
- const legacyPath = alternateSessionPath(workspaceDir, conversationId);
140846
- if (legacyPath) {
140847
- await fs35.unlink(legacyPath).catch(() => {
140848
- });
140849
- }
140850
- }
140851
- async function writeTelegramBootstrapSession(workspaceDir, session) {
140852
- const canonicalSession = normalizeStoredSession(session);
140853
- const dir = sessionsDir(workspaceDir);
140854
- await fs35.mkdir(dir, { recursive: true });
140855
- const file2 = sessionPath(workspaceDir, canonicalSession.conversationId);
140856
- const tmp = `${file2}.${randomUUID4()}.tmp`;
140857
- await fs35.writeFile(tmp, JSON.stringify(canonicalSession, null, 2) + "\n", "utf-8");
140858
- await fs35.rename(tmp, file2);
140859
- const legacyPath = alternateSessionPath(workspaceDir, session.conversationId);
140860
- if (legacyPath) {
140861
- await fs35.unlink(legacyPath).catch(() => {
140862
- });
140863
- }
140864
- }
140865
- async function upsertTelegramBootstrapSession(workspaceDir, input) {
140866
- const conversationId = toCanonicalTelegramBootstrapConversationId(input.conversationId);
140867
- const existing = await readTelegramBootstrapSession(workspaceDir, conversationId);
140868
- const resolvedSourceRoute = resolveNullableField(input.sourceRoute, existing?.sourceRoute);
140869
- const resolvedProjectRoute = resolveNullableField(input.projectRoute, existing?.projectRoute);
140870
- const resolvedProjectName = resolveNullableField(input.projectName, existing?.projectName);
140871
- const resolvedStackHint = resolveNullableField(input.stackHint, existing?.stackHint);
140872
- const resolvedRepoUrl = resolveNullableField(input.repoUrl, existing?.repoUrl);
140873
- const resolvedRepoPath = resolveNullableField(input.repoPath, existing?.repoPath);
140874
- const resolvedProjectSlug = resolveNullableField(input.projectSlug, existing?.projectSlug);
140875
- const resolvedIssueId = resolveNullableField(input.issueId, existing?.issueId);
140876
- const resolvedMessageThreadId = resolveNullableField(input.messageThreadId, existing?.messageThreadId);
140877
- const resolvedProjectChannelId = resolveNullableField(input.projectChannelId, existing?.projectChannelId);
140878
- const resolvedAttemptCount = resolveNullableField(input.attemptCount, existing?.attemptCount, 0);
140879
- const resolvedAttemptId = resolveNullableField(input.attemptId, existing?.attemptId);
140880
- const resolvedAttemptSeq = resolveNullableField(input.attemptSeq, existing?.attemptSeq);
140881
- const resolvedClassifySessionKey = resolveNullableField(input.classifySessionKey, existing?.classifySessionKey);
140882
- const resolvedClassifyRunId = resolveNullableField(input.classifyRunId, existing?.classifyRunId);
140883
- const resolvedClassifyStartedAt = resolveNullableField(input.classifyStartedAt, existing?.classifyStartedAt);
140884
- const resolvedBootstrapStep = resolveNullableField(input.bootstrapStep, existing?.bootstrapStep);
140885
- const resolvedNextRetryAt = input.nextRetryAt !== void 0 ? input.nextRetryAt : defaultNextRetryAtForStatus(input.status, existing?.nextRetryAt);
140886
- const resolvedAckSentAt = resolveNullableField(input.ackSentAt, existing?.ackSentAt);
140887
- const resolvedProjectRegisteredAt = resolveNullableField(input.projectRegisteredAt, existing?.projectRegisteredAt);
140888
- const resolvedTopicKickoffSentAt = resolveNullableField(input.topicKickoffSentAt, existing?.topicKickoffSentAt);
140889
- const resolvedProjectTickedAt = resolveNullableField(input.projectTickedAt, existing?.projectTickedAt);
140890
- const resolvedCompletionAckSentAt = resolveNullableField(input.completionAckSentAt, existing?.completionAckSentAt);
140891
- const resolvedError = input.error !== void 0 ? input.error : input.lastError !== void 0 ? input.lastError : existing?.error ?? existing?.lastError ?? null;
140892
- const requestHash = buildBootstrapRequestHash({
140893
- rawIdea: input.rawIdea,
140894
- projectName: resolvedProjectName,
140895
- stackHint: resolvedStackHint,
140896
- repoUrl: resolvedRepoUrl,
140897
- repoPath: resolvedRepoPath
140898
- });
140899
- const now2 = (/* @__PURE__ */ new Date()).toISOString();
140900
- const session = {
140901
- id: existing?.id ?? buildBootstrapSessionId(conversationId, input.rawIdea),
140902
- conversationId,
140903
- sourceChannel: input.sourceChannel ?? input.sourceRoute?.channel ?? existing?.sourceChannel ?? "telegram",
140904
- sourceRoute: resolvedSourceRoute,
140905
- projectRoute: resolvedProjectRoute,
140906
- requestHash,
140907
- requestFingerprint: requestHash,
140908
- lastCompletedRequestHash: input.status === "completed" ? requestHash : existing?.lastCompletedRequestHash ?? null,
140909
- rawIdea: input.rawIdea,
140910
- projectName: resolvedProjectName,
140911
- stackHint: resolvedStackHint,
140912
- repoUrl: resolvedRepoUrl,
140913
- repoPath: resolvedRepoPath,
140914
- projectSlug: resolvedProjectSlug,
140915
- issueId: resolvedIssueId,
140916
- messageThreadId: resolvedMessageThreadId,
140917
- projectChannelId: resolvedProjectChannelId,
140918
- language: input.language ?? existing?.language,
140919
- status: input.status,
140920
- attemptId: resolvedAttemptId,
140921
- attemptSeq: resolvedAttemptSeq,
140922
- classifySessionKey: resolvedClassifySessionKey,
140923
- classifyRunId: resolvedClassifyRunId,
140924
- classifyStartedAt: resolvedClassifyStartedAt,
140925
- bootstrapStep: resolvedBootstrapStep,
140926
- attemptCount: resolvedAttemptCount,
140927
- lastError: resolvedError,
140928
- nextRetryAt: resolvedNextRetryAt,
140929
- ackSentAt: resolvedAckSentAt,
140930
- projectRegisteredAt: resolvedProjectRegisteredAt,
140931
- topicKickoffSentAt: resolvedTopicKickoffSentAt,
140932
- projectTickedAt: resolvedProjectTickedAt,
140933
- completionAckSentAt: resolvedCompletionAckSentAt,
140934
- pendingClarification: input.pendingClarification !== void 0 ? input.pendingClarification : existing?.pendingClarification ?? null,
140935
- orphanedArtifacts: input.orphanedArtifacts !== void 0 ? input.orphanedArtifacts : existing?.orphanedArtifacts ?? null,
140936
- createdAt: existing?.createdAt ?? now2,
140937
- updatedAt: now2,
140938
- suppressUntil: nextSuppressUntil(input.status),
140939
- error: resolvedError
140940
- };
140941
- const writeDecision = shouldPersistBootstrapCheckpoint(existing, session);
140942
- if (!writeDecision.ok && existing) {
140943
- return existing;
141292
+ pickupCount++;
140944
141293
  }
140945
- await writeTelegramBootstrapSession(workspaceDir, session);
140946
- return session;
141294
+ return { pickups, skipped };
140947
141295
  }
140948
- function shouldSuppressTelegramBootstrapReply(session, request) {
140949
- if (!session) return false;
140950
- if (isTelegramBootstrapSessionExpired(session)) return false;
140951
- if (session.status === "completed" || session.status === "failed") {
140952
- if (session.status === "failed" && isReleasedClassifyFailure(session.error)) return false;
140953
- if (!request) return false;
140954
- return buildBootstrapRequestFingerprint(request) === session.requestHash;
141296
+ function resolveLevelForIssue(issue2, role) {
141297
+ const roleLevel = detectRoleLevelFromLabels(issue2.labels);
141298
+ if (roleLevel?.role === role) return roleLevel.level;
141299
+ if (roleLevel) {
141300
+ const levels = getLevelsForRole(role);
141301
+ if (levels.includes(roleLevel.level)) return roleLevel.level;
140955
141302
  }
140956
- if (!request) return true;
140957
- return buildBootstrapRequestFingerprint(request) === session.requestHash;
140958
- }
140959
- function isTelegramBootstrapSessionExpired(session, now2 = Date.now()) {
140960
- if (!session) return false;
140961
- const suppressUntil = Date.parse(session.suppressUntil);
140962
- return !Number.isNaN(suppressUntil) && suppressUntil < now2;
140963
- }
140964
- function isRecoverableTelegramBootstrapSession(session) {
140965
- return session?.status === "bootstrapping" || session?.status === "dispatching";
140966
- }
140967
- function isClaimableTelegramBootstrapSession(session, now2 = Date.now()) {
140968
- if (!isRecoverableTelegramBootstrapSession(session)) return false;
140969
- if (!session.nextRetryAt) return true;
140970
- const retryAt = Date.parse(session.nextRetryAt);
140971
- return Number.isNaN(retryAt) || retryAt <= now2;
141303
+ return selectLevel(issue2.title, issue2.description ?? "", role).level;
140972
141304
  }
140973
- function isSupersededTelegramBootstrapAttempt(current, candidate) {
140974
- if (!current || !candidate) return false;
140975
- if (current.conversationId !== candidate.conversationId) return true;
140976
- const currentHasAttempt = current.attemptId != null && current.attemptSeq != null;
140977
- const candidateHasAttempt = candidate.attemptId != null && candidate.attemptSeq != null;
140978
- if (currentHasAttempt || candidateHasAttempt) {
140979
- return current.attemptId !== candidate.attemptId || current.attemptSeq !== candidate.attemptSeq;
140980
- }
140981
- return current.requestHash !== candidate.requestHash || current.status !== candidate.status || current.updatedAt !== candidate.updatedAt;
141305
+ function getFeedbackQueueLabel(workflow) {
141306
+ const developerQueues = getQueueLabels(workflow, "developer");
141307
+ return developerQueues.find((label) => isFeedbackState(workflow, label)) ?? null;
140982
141308
  }
140983
141309
 
140984
141310
  // lib/dispatch/telegram-bootstrap-hook.ts
141311
+ init_audit();
141312
+ init_zod();
140985
141313
  init_constants();
140986
141314
  var BOOTSTRAP_RETRY_DELAY_MS = 15e3;
140987
141315
  var LAYER3_CONFIDENCE_THRESHOLD = 0.6;
@@ -141099,6 +141427,11 @@ function inferProjectSlug(text) {
141099
141427
  const slug = cleaned.toLowerCase().normalize("NFKD").replace(/[^\w\s-]/g, "").trim().replace(/\s+/g, "-").replace(/-+/g, "-").slice(0, 64);
141100
141428
  return slug || void 0;
141101
141429
  }
141430
+ function normalizeProjectNameCandidate(value) {
141431
+ const normalized = normalizeText3(value);
141432
+ if (!normalized) return void 0;
141433
+ return inferProjectSlug(normalized) ?? void 0;
141434
+ }
141102
141435
  function normalizeText3(value) {
141103
141436
  const trimmed = value?.trim();
141104
141437
  return trimmed ? trimmed : void 0;
@@ -141122,7 +141455,7 @@ function detectStackHint(text) {
141122
141455
  }
141123
141456
  function parseField(text, labels) {
141124
141457
  for (const label of labels) {
141125
- const regex = new RegExp(`^\\s*${label}\\s*:\\s*(.+)$`, "im");
141458
+ const regex = new RegExp(`(?:^|[\\n.,;!?]\\s*)${label}\\s*:\\s*(.+)$`, "im");
141126
141459
  const match = text.match(regex);
141127
141460
  if (match?.[1]) return normalizeText3(match[1]);
141128
141461
  }
@@ -141134,12 +141467,12 @@ function parseIdeaBlock(text) {
141134
141467
  }
141135
141468
  function parseExplicitProjectName(text) {
141136
141469
  const match = text.match(/\b(?:called|named|chamado)\s+[`"'“”‘’]?([a-z0-9][a-z0-9-]{1,63})[`"'“”‘’]?(?=$|[\s.,!?;:])/i);
141137
- return match?.[1]?.toLowerCase();
141470
+ return normalizeProjectNameCandidate(match?.[1]?.toLowerCase());
141138
141471
  }
141139
141472
  function parseBootstrapRequest(text) {
141140
141473
  const repoUrl = parseField(text, ["repository url", "repo url", "reposit[o\xF3]rio url", "github repo"]);
141141
141474
  const rawIdea = parseIdeaBlock(text) ?? text.trim();
141142
- const projectName = parseField(text, ["project name", "nome do projeto", "repo name", "repository name"]) ?? parseExplicitProjectName(text);
141475
+ const projectName = normalizeProjectNameCandidate(parseField(text, ["project name", "nome do projeto", "repo name", "repository name"])) ?? parseExplicitProjectName(text);
141143
141476
  const repoPath = parseField(text, ["local repository path", "repo path", "caminho local", "local path"]);
141144
141477
  const explicitStack = parseField(text, ["stack", "framework", "linguagem"]);
141145
141478
  const stackHint = (explicitStack ? normalizeStackHint(explicitStack) : "") || detectStackHint(text);
@@ -141318,25 +141651,26 @@ function parseClarificationResponse(text, session) {
141318
141651
  if (autoPatterns.test(normalizeUserResponse(text))) {
141319
141652
  return { recognized: true, projectName: inferProjectSlug(session.rawIdea) ?? `project-${Date.now()}`, stackHint: session.stackHint ?? void 0 };
141320
141653
  }
141321
- const nameField = parseField(text, ["project name", "nome do projeto", "nome", "name"]);
141654
+ const nameField = normalizeProjectNameCandidate(parseField(text, ["project name", "nome do projeto", "nome", "name"]));
141322
141655
  if (nameField) {
141323
141656
  return { recognized: true, projectName: nameField, stackHint: session.stackHint ?? void 0 };
141324
141657
  }
141325
- if (trimmed.length > 0 && trimmed.length <= 64) {
141326
- return { recognized: true, projectName: trimmed, stackHint: session.stackHint ?? void 0 };
141658
+ const normalizedInlineName = trimmed.length <= 64 ? normalizeProjectNameCandidate(trimmed) : void 0;
141659
+ if (normalizedInlineName) {
141660
+ return { recognized: true, projectName: normalizedInlineName, stackHint: session.stackHint ?? void 0 };
141327
141661
  }
141328
141662
  return { recognized: false };
141329
141663
  }
141664
+ const projectNameFromField = normalizeProjectNameCandidate(parseField(text, ["project name", "nome do projeto", "nome", "name"]));
141665
+ const nameItMatch = !projectNameFromField ? text.match(/(?:name|call|chamar?)\s+(?:it\s+)?([\w-]{2,64})/i) : null;
141666
+ const inlineName = projectNameFromField ?? normalizeProjectNameCandidate(nameItMatch?.[1]);
141330
141667
  const stackField = parseField(text, ["stack", "framework", "linguagem", "language"]);
141331
141668
  if (stackField) {
141332
141669
  const normalizedStack = normalizeStackHint(stackField) || detectStackHint(stackField) || stackField;
141333
- return { recognized: true, stackHint: normalizedStack };
141670
+ return { recognized: true, stackHint: normalizedStack, projectName: inlineName };
141334
141671
  }
141335
141672
  const detectedStack = detectStackHint(text);
141336
141673
  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
141674
  return { recognized: true, stackHint: detectedStack, projectName: inlineName };
141341
141675
  }
141342
141676
  const lower2 = normalizeUserResponse(text);
@@ -141569,7 +141903,7 @@ async function handleTelegramBootstrapDmMessage(ctx, rawConversationId, content)
141569
141903
  conversationId,
141570
141904
  rawIdea: content,
141571
141905
  stackHint: parsed.stackHint ?? void 0,
141572
- projectName: parsed.projectSlug ?? void 0,
141906
+ projectName: parsed.projectName ?? parsed.projectSlug ?? void 0,
141573
141907
  status: "clarifying",
141574
141908
  pendingClarification: "scope",
141575
141909
  language
@@ -142823,6 +143157,8 @@ Erro: ${result.error ?? "erro desconhecido"}`
142823
143157
  const projectChannelId = result.payload.metadata.channel_id ?? telegramConfig.projectsForumChatId;
142824
143158
  const messageThreadId = result.payload.metadata.message_thread_id;
142825
143159
  const projectSlug = result.payload.metadata.project_slug ?? result.payload.scaffold?.project_slug ?? null;
143160
+ const createdIssue = result.payload.issues?.[0] ?? null;
143161
+ const triage = result.payload.triage ?? null;
142826
143162
  if (projectChannelId && messageThreadId) {
142827
143163
  const projectRoute = {
142828
143164
  channel: "telegram",
@@ -142837,6 +143173,10 @@ Erro: ${result.error ?? "erro desconhecido"}`
142837
143173
  bootstrapStep: "project_registered",
142838
143174
  projectName: resolvedProjectName,
142839
143175
  projectSlug: projectSlug ?? void 0,
143176
+ issueId: createdIssue?.number ?? null,
143177
+ issueUrl: createdIssue?.url ?? null,
143178
+ triageReadyForDispatch: triage?.ready_for_dispatch ?? null,
143179
+ triageErrors: triage?.errors ?? null,
142840
143180
  projectRegisteredAt: (/* @__PURE__ */ new Date()).toISOString(),
142841
143181
  projectChannelId: String(projectChannelId),
142842
143182
  messageThreadId,
@@ -142852,6 +143192,10 @@ Erro: ${result.error ?? "erro desconhecido"}`
142852
143192
  status: "dispatching",
142853
143193
  projectName: resolvedProjectName,
142854
143194
  projectSlug: projectSlug ?? void 0,
143195
+ issueId: createdIssue?.number ?? null,
143196
+ issueUrl: createdIssue?.url ?? null,
143197
+ triageReadyForDispatch: triage?.ready_for_dispatch ?? null,
143198
+ triageErrors: triage?.errors ?? null,
142855
143199
  projectRegisteredAt: (/* @__PURE__ */ new Date()).toISOString(),
142856
143200
  projectChannelId: String(projectChannelId),
142857
143201
  messageThreadId,
@@ -142867,6 +143211,10 @@ Erro: ${result.error ?? "erro desconhecido"}`
142867
143211
  ...registeredSession,
142868
143212
  projectName: resolvedProjectName,
142869
143213
  projectSlug: projectSlug ?? registeredSession.projectSlug ?? void 0,
143214
+ issueId: createdIssue?.number ?? registeredSession.issueId ?? null,
143215
+ issueUrl: createdIssue?.url ?? registeredSession.issueUrl ?? null,
143216
+ triageReadyForDispatch: triage?.ready_for_dispatch ?? registeredSession.triageReadyForDispatch ?? null,
143217
+ triageErrors: triage?.errors ?? registeredSession.triageErrors ?? null,
142870
143218
  projectRegisteredAt: registeredSession.projectRegisteredAt ?? (/* @__PURE__ */ new Date()).toISOString(),
142871
143219
  language: currentSession?.language ?? "pt"
142872
143220
  });
@@ -143955,6 +144303,7 @@ async function performHealthPass(workspaceDir, projectSlug, project, sessions, p
143955
144303
  workflow: resolvedConfig?.workflow,
143956
144304
  dispatchConfirmTimeoutMs: resolvedConfig?.timeouts?.dispatchConfirmTimeoutMs,
143957
144305
  healthGracePeriodMs: resolvedConfig?.timeouts?.healthGracePeriodMs,
144306
+ stallTimeoutMinutes,
143958
144307
  runCommand,
143959
144308
  notificationConfig: notifyConfig
143960
144309
  });
@@ -146332,8 +146681,18 @@ function stringifyAnswerRecord(record2) {
146332
146681
  }
146333
146682
  return normalized;
146334
146683
  }
146335
- function deriveProjectName(repoUrl, projectName) {
146684
+ function extractExplicitProjectName(text) {
146685
+ if (!text) return null;
146686
+ 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);
146687
+ if (fieldMatch?.[1]) return fieldMatch[1].trim().toLowerCase();
146688
+ const inlineMatch = text.match(/\b(?:called|named|chamado)\s+[`"'“”‘’]?([a-z0-9][a-z0-9-]{1,63})[`"'“”‘’]?(?=$|[\s.,!?;:])/i);
146689
+ if (inlineMatch?.[1]) return inlineMatch[1].trim().toLowerCase();
146690
+ return null;
146691
+ }
146692
+ function deriveProjectName(repoUrl, projectName, freeText) {
146336
146693
  if (projectName) return projectName;
146694
+ const parsedFromText = extractExplicitProjectName(freeText ?? null);
146695
+ if (parsedFromText) return parsedFromText;
146337
146696
  if (!repoUrl) return null;
146338
146697
  const sanitized = repoUrl.replace(/\/+$/, "");
146339
146698
  const lastSegment = sanitized.split("/").pop();
@@ -146423,9 +146782,11 @@ function normalizeGenesisRequest(params, existingPayload) {
146423
146782
  throw new Error('phase is required and must be "discover" or "commit"');
146424
146783
  }
146425
146784
  const repoUrl = normalizeOptionalString(params.repo_url) ?? existingPayload?.metadata.repo_url ?? null;
146785
+ const freeTextProjectSource = normalizeOptionalString(params.idea) ?? normalizeOptionalString(params.command) ?? existingPayload?.raw_idea ?? null;
146426
146786
  const projectName = deriveProjectName(
146427
146787
  repoUrl,
146428
- normalizeOptionalString(params.project_name) ?? existingPayload?.metadata.project_name ?? null
146788
+ normalizeOptionalString(params.project_name) ?? existingPayload?.metadata.project_name ?? null,
146789
+ freeTextProjectSource
146429
146790
  );
146430
146791
  const answersJson = {
146431
146792
  ...existingPayload?.metadata.answers_json ?? {},