@mestreyoda/fabrica 0.2.21 → 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 +474 -392
  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.21") {
113909
- return "0.2.21";
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");
@@ -118618,9 +118618,9 @@ function fallbackSpecData(type, rawIdea) {
118618
118618
  default:
118619
118619
  base.scope_v1 = deriveFeatureScopeFromRawIdea(rawIdea);
118620
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"
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
118624
  ];
118625
118625
  }
118626
118626
  return base;
@@ -139764,6 +139764,287 @@ var securityReviewStep = {
139764
139764
 
139765
139765
  // lib/intake/steps/create-task.ts
139766
139766
  init_workflow();
139767
+
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;
139805
+ }
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;
139843
+ }
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" };
139860
+ }
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
+ }
139869
+ }
139870
+ return { ok: true };
139871
+ }
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
139767
140048
  function buildIssueBody(payload) {
139768
140049
  const spec = payload.spec;
139769
140050
  const sections = [];
@@ -139835,6 +140116,26 @@ var createTaskStep = {
139835
140116
  created_at: (/* @__PURE__ */ new Date()).toISOString()
139836
140117
  };
139837
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
+ }
139838
140139
  return {
139839
140140
  ...payload,
139840
140141
  step: "create-task",
@@ -139870,6 +140171,26 @@ var createTaskStep = {
139870
140171
  created_at: (/* @__PURE__ */ new Date()).toISOString()
139871
140172
  };
139872
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
+ }
139873
140194
  return {
139874
140195
  ...payload,
139875
140196
  step: "create-task",
@@ -139996,7 +140317,7 @@ function runTriageLogic(input, matrix) {
139996
140317
  objective: input.objective ?? input.rawIdea,
139997
140318
  scopeItems: (input.scopeText ?? "").split("\n").filter((l) => l.trim()),
139998
140319
  acceptanceCriteria: (input.acText ?? "").split("\n").filter((l) => l.trim()),
139999
- dod: input.scopeText ?? ""
140320
+ dod: input.dodText ?? ""
140000
140321
  });
140001
140322
  const specQualityBlock = specQualityErrors.length > 0;
140002
140323
  return {
@@ -140100,8 +140421,9 @@ var triageStep = {
140100
140421
  rawIdea: payload.raw_idea,
140101
140422
  acText: spec.acceptance_criteria.join("\n"),
140102
140423
  scopeText: spec.scope_v1.join("\n"),
140424
+ dodText: spec.definition_of_done.join("\n"),
140103
140425
  oosText: spec.out_of_scope.join("\n"),
140104
- authSignal: payload.metadata?.auth_gate?.signal ?? false
140426
+ authSignal: /\b(login|register|jwt|oauth|auth|role-based access|rbac|permission)\b/i.test(`${payload.raw_idea} ${spec.objective}`)
140105
140427
  }, matrix);
140106
140428
  const repoUrl = payload.scaffold?.repo_url ?? payload.metadata?.repo_url ?? "";
140107
140429
  const repoPath = payload.provisioning?.repo_local ?? payload.scaffold?.repo_local ?? payload.metadata?.repo_path ?? payload.project_map?.root ?? void 0;
@@ -140229,6 +140551,27 @@ var triageStep = {
140229
140551
  child_issue_numbers: createdChildIssueNumbers
140230
140552
  };
140231
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
+ }
140232
140575
  return {
140233
140576
  ...payload,
140234
140577
  step: "triage",
@@ -140576,23 +140919,23 @@ init_workflow();
140576
140919
 
140577
140920
  // lib/dispatch/dispatch-dedup.ts
140578
140921
  init_constants();
140579
- import fs34 from "node:fs/promises";
140580
- import path35 from "node:path";
140581
- 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";
140582
140925
  var DEDUP_FILE = "dispatch-dedup.ndjson";
140583
140926
  var BUCKET_MS = 5 * 6e4;
140584
140927
  var DEFAULT_TTL_MS2 = 30 * 6e4;
140585
140928
  function dedupPath(workspaceDir) {
140586
- return path35.join(workspaceDir, DATA_DIR, DEDUP_FILE);
140929
+ return path36.join(workspaceDir, DATA_DIR, DEDUP_FILE);
140587
140930
  }
140588
140931
  function computeDispatchId(projectSlug, issueId, role, level, now2 = Date.now()) {
140589
140932
  const bucket = Math.floor(now2 / BUCKET_MS);
140590
140933
  const input = `${projectSlug}:${issueId}:${role}:${level}:${bucket}`;
140591
- return createHash4("sha256").update(input).digest("hex").slice(0, 16);
140934
+ return createHash5("sha256").update(input).digest("hex").slice(0, 16);
140592
140935
  }
140593
140936
  async function readEntries2(filePath) {
140594
140937
  try {
140595
- const content = await fs34.readFile(filePath, "utf-8");
140938
+ const content = await fs35.readFile(filePath, "utf-8");
140596
140939
  return content.split("\n").filter(Boolean).map((line) => {
140597
140940
  try {
140598
140941
  return JSON.parse(line);
@@ -140611,9 +140954,9 @@ async function isDuplicate(workspaceDir, dispatchId) {
140611
140954
  }
140612
140955
  async function recordDispatch(workspaceDir, dispatchId) {
140613
140956
  const filePath = dedupPath(workspaceDir);
140614
- await fs34.mkdir(path35.dirname(filePath), { recursive: true });
140957
+ await fs35.mkdir(path36.dirname(filePath), { recursive: true });
140615
140958
  const entry = { id: dispatchId, ts: Date.now() };
140616
- await fs34.appendFile(filePath, JSON.stringify(entry) + "\n", "utf-8");
140959
+ await fs35.appendFile(filePath, JSON.stringify(entry) + "\n", "utf-8");
140617
140960
  }
140618
140961
  async function cleanupExpired(workspaceDir, ttlMs = DEFAULT_TTL_MS2) {
140619
140962
  const filePath = dedupPath(workspaceDir);
@@ -140623,8 +140966,8 @@ async function cleanupExpired(workspaceDir, ttlMs = DEFAULT_TTL_MS2) {
140623
140966
  if (kept.length < entries.length) {
140624
140967
  const content = kept.map((e2) => JSON.stringify(e2)).join("\n") + (kept.length > 0 ? "\n" : "");
140625
140968
  const tmpPath = filePath + ".tmp";
140626
- await fs34.writeFile(tmpPath, content, "utf-8");
140627
- await fs34.rename(tmpPath, filePath);
140969
+ await fs35.writeFile(tmpPath, content, "utf-8");
140970
+ await fs35.rename(tmpPath, filePath);
140628
140971
  }
140629
140972
  }
140630
140973
 
@@ -140856,392 +141199,117 @@ async function projectTick(opts) {
140856
141199
  if (dryRun) {
140857
141200
  const existingSession = roleWorker.levels[effectiveLevel]?.[freeSlot]?.sessionKey;
140858
141201
  pickups.push({
140859
- project: project.name,
140860
- projectSlug,
140861
- issueId: issue2.iid,
140862
- issueTitle: issue2.title,
140863
- issueUrl: issue2.web_url,
140864
- role,
140865
- level: effectiveLevel,
140866
- sessionAction: existingSession ? "send" : "spawn",
140867
- announcement: `[DRY RUN] Would pick up #${issue2.iid}`
140868
- });
140869
- pickupCount++;
140870
- continue;
140871
- }
140872
- const environment = await ensureEnvironment({
140873
- workspaceDir,
140874
- projectSlug,
140875
- project: fresh,
140876
- stack,
140877
- mode: role === "tester" ? "tester" : "developer",
140878
- runCommand
140879
- });
140880
- if (!environment.ready) {
140881
- const environmentSkipReason = classifyEnvironmentGateSkip(environment.state);
140882
- skipped.push({ role, reason: environmentSkipReason });
140883
- await log(workspaceDir, "dispatch_blocked_environment_not_ready", {
140884
- projectSlug,
140885
- role,
140886
- issueId: issue2.iid,
140887
- reason: environmentSkipReason,
140888
- environmentStatus: environment.state.status,
140889
- nextProvisionRetryAt: environment.state.nextProvisionRetryAt ?? null,
140890
- lastProvisionError: environment.state.lastProvisionError ?? null
140891
- }).catch(() => {
140892
- });
140893
- continue;
140894
- }
140895
- }
140896
- if (dryRun) {
140897
- const existingSession = roleWorker.levels[effectiveLevel]?.[freeSlot]?.sessionKey;
140898
- pickups.push({
140899
- project: project.name,
140900
- projectSlug,
140901
- issueId: issue2.iid,
140902
- issueTitle: issue2.title,
140903
- issueUrl: issue2.web_url,
140904
- role,
140905
- level: effectiveLevel,
140906
- sessionAction: existingSession ? "send" : "spawn",
140907
- announcement: `[DRY RUN] Would pick up #${issue2.iid}`
140908
- });
140909
- } else {
140910
- try {
140911
- const dr = await dispatch({
140912
- workspaceDir,
140913
- agentId,
140914
- project: fresh,
140915
- issueId: issue2.iid,
140916
- issueTitle: issue2.title,
140917
- issueDescription: issue2.description ?? "",
140918
- issueUrl: issue2.web_url,
140919
- role,
140920
- level: effectiveLevel,
140921
- fromLabel: currentLabel,
140922
- toLabel: targetLabel,
140923
- provider,
140924
- pluginConfig,
140925
- sessionKey,
140926
- runtime,
140927
- slotIndex: freeSlot,
140928
- instanceName,
140929
- runCommand
140930
- });
140931
- pickups.push({
140932
- project: project.name,
140933
- projectSlug,
140934
- issueId: issue2.iid,
140935
- issueTitle: issue2.title,
140936
- issueUrl: issue2.web_url,
140937
- role,
140938
- level: dr.level,
140939
- sessionAction: dr.sessionAction,
140940
- announcement: dr.announcement
140941
- });
140942
- await recordDispatch(workspaceDir, dispatchId).catch(() => {
140943
- });
140944
- } catch (err) {
140945
- skipped.push({ role, reason: `Dispatch failed: ${err.message}` });
140946
- continue;
140947
- }
140948
- }
140949
- pickupCount++;
140950
- }
140951
- return { pickups, skipped };
140952
- }
140953
- function resolveLevelForIssue(issue2, role) {
140954
- const roleLevel = detectRoleLevelFromLabels(issue2.labels);
140955
- if (roleLevel?.role === role) return roleLevel.level;
140956
- if (roleLevel) {
140957
- const levels = getLevelsForRole(role);
140958
- if (levels.includes(roleLevel.level)) return roleLevel.level;
140959
- }
140960
- return selectLevel(issue2.title, issue2.description ?? "", role).level;
140961
- }
140962
- function getFeedbackQueueLabel(workflow) {
140963
- const developerQueues = getQueueLabels(workflow, "developer");
140964
- return developerQueues.find((label) => isFeedbackState(workflow, label)) ?? null;
140965
- }
140966
-
140967
- // lib/dispatch/telegram-bootstrap-hook.ts
140968
- init_audit();
140969
- init_zod();
140970
-
140971
- // lib/dispatch/telegram-bootstrap-session.ts
140972
- init_constants();
140973
- import { createHash as createHash5, randomUUID as randomUUID4 } from "node:crypto";
140974
- import fs35 from "node:fs/promises";
140975
- import path36 from "node:path";
140976
- var SESSION_TTL_MS = 10 * 6e4;
140977
- var CLASSIFYING_TTL_MS = 15e3;
140978
- var RELEASED_CLASSIFY_ERRORS = /* @__PURE__ */ new Set([
140979
- "classify_invalid_result",
140980
- "classify_low_confidence",
140981
- "classify_not_project",
140982
- "classify_result_expired"
140983
- ]);
140984
- function toCanonicalTelegramBootstrapConversationId(conversationId) {
140985
- const trimmed = conversationId.trim();
140986
- if (!trimmed) return trimmed;
140987
- return trimmed.startsWith("telegram:") ? trimmed : `telegram:${trimmed}`;
140988
- }
140989
- function sessionsDir(workspaceDir) {
140990
- return path36.join(workspaceDir, DATA_DIR, "bootstrap-sessions");
140991
- }
140992
- function sessionPath(workspaceDir, conversationId) {
140993
- return path36.join(
140994
- sessionsDir(workspaceDir),
140995
- `${toCanonicalTelegramBootstrapConversationId(conversationId)}.json`
140996
- );
140997
- }
140998
- function alternateSessionPath(workspaceDir, conversationId) {
140999
- const canonical = toCanonicalTelegramBootstrapConversationId(conversationId);
141000
- const bare = canonical.startsWith("telegram:") ? canonical.slice("telegram:".length) : canonical;
141001
- if (!bare || bare === canonical) return null;
141002
- return path36.join(sessionsDir(workspaceDir), `${bare}.json`);
141003
- }
141004
- function normalizeStoredSession(session) {
141005
- const canonicalConversationId = toCanonicalTelegramBootstrapConversationId(session.conversationId);
141006
- if (canonicalConversationId === session.conversationId) {
141007
- return session;
141008
- }
141009
- return {
141010
- ...session,
141011
- id: buildBootstrapSessionId(canonicalConversationId, session.rawIdea),
141012
- conversationId: canonicalConversationId
141013
- };
141014
- }
141015
- function stableHash(input) {
141016
- return createHash5("sha256").update(input).digest("hex").slice(0, 16);
141017
- }
141018
- function buildBootstrapSessionId(conversationId, rawIdea) {
141019
- return `tgdm-${conversationId}-${stableHash(rawIdea.trim().toLowerCase())}`;
141020
- }
141021
- function buildBootstrapRequestFingerprint(input) {
141022
- return stableHash(JSON.stringify({
141023
- rawIdea: input.rawIdea.trim().toLowerCase(),
141024
- projectName: input.projectName?.trim().toLowerCase() || null,
141025
- stackHint: input.stackHint?.trim().toLowerCase() || null,
141026
- repoUrl: input.repoUrl?.trim().toLowerCase() || null,
141027
- repoPath: input.repoPath?.trim().toLowerCase() || null
141028
- }));
141029
- }
141030
- function buildBootstrapRequestHash(input) {
141031
- return buildBootstrapRequestFingerprint(input);
141032
- }
141033
- function nextSuppressUntil(status) {
141034
- const ttl = status === "classifying" || status === "pending_classify" ? CLASSIFYING_TTL_MS : SESSION_TTL_MS;
141035
- return new Date(Date.now() + ttl).toISOString();
141036
- }
141037
- function isReleasedClassifyFailure(error48) {
141038
- return Boolean(error48 && RELEASED_CLASSIFY_ERRORS.has(error48));
141039
- }
141040
- function resolveNullableField(inputValue, existingValue, fallback = null) {
141041
- return inputValue !== void 0 ? inputValue : existingValue ?? fallback;
141042
- }
141043
- function defaultNextRetryAtForStatus(status, existingValue) {
141044
- if (status === "bootstrapping" || status === "dispatching") {
141045
- return existingValue ?? null;
141046
- }
141047
- return null;
141048
- }
141049
- var MONOTONIC_BOOTSTRAP_FIELDS = [
141050
- "ackSentAt",
141051
- "projectRegisteredAt",
141052
- "topicKickoffSentAt",
141053
- "projectTickedAt",
141054
- "completionAckSentAt"
141055
- ];
141056
- function shouldPersistBootstrapCheckpoint(current, next) {
141057
- if (!current) return { ok: true };
141058
- if (current.conversationId !== next.conversationId) return { ok: true };
141059
- const currentAttemptSeq = current.attemptSeq ?? null;
141060
- const nextAttemptSeq = next.attemptSeq ?? null;
141061
- if (currentAttemptSeq != null && nextAttemptSeq != null && nextAttemptSeq < currentAttemptSeq) {
141062
- return { ok: false, reason: "stale_regression" };
141063
- }
141064
- const sameAttempt = Boolean(
141065
- current.attemptId && next.attemptId && current.attemptSeq != null && next.attemptSeq != null && current.attemptId === next.attemptId && current.attemptSeq === next.attemptSeq
141066
- );
141067
- if (!sameAttempt) return { ok: true };
141068
- for (const field of MONOTONIC_BOOTSTRAP_FIELDS) {
141069
- if (current[field] && !next[field]) {
141070
- return { ok: false, reason: "stale_regression" };
141202
+ project: project.name,
141203
+ projectSlug,
141204
+ issueId: issue2.iid,
141205
+ issueTitle: issue2.title,
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
+ }
141071
141238
  }
141072
- }
141073
- return { ok: true };
141074
- }
141075
- async function readTelegramBootstrapSession(workspaceDir, conversationId) {
141076
- const canonicalConversationId = toCanonicalTelegramBootstrapConversationId(conversationId);
141077
- const paths = [
141078
- sessionPath(workspaceDir, canonicalConversationId),
141079
- alternateSessionPath(workspaceDir, conversationId)
141080
- ].filter((entry) => Boolean(entry));
141081
- try {
141082
- 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 {
141083
141253
  try {
141084
- const raw = await fs35.readFile(candidatePath, "utf-8");
141085
- const session = normalizeStoredSession(JSON.parse(raw));
141086
- if (session.status === "failed" && isReleasedClassifyFailure(session.error)) {
141087
- await deleteTelegramBootstrapSession(workspaceDir, canonicalConversationId);
141088
- return null;
141089
- }
141090
- return session;
141091
- } catch (error48) {
141092
- if (error48?.code === "ENOENT") continue;
141093
- 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;
141094
141290
  }
141095
141291
  }
141096
- return null;
141097
- } catch (error48) {
141098
- if (error48?.code === "ENOENT") return null;
141099
- throw error48;
141100
- }
141101
- }
141102
- async function deleteTelegramBootstrapSession(workspaceDir, conversationId) {
141103
- await fs35.unlink(sessionPath(workspaceDir, conversationId)).catch(() => {
141104
- });
141105
- const legacyPath = alternateSessionPath(workspaceDir, conversationId);
141106
- if (legacyPath) {
141107
- await fs35.unlink(legacyPath).catch(() => {
141108
- });
141109
- }
141110
- }
141111
- async function writeTelegramBootstrapSession(workspaceDir, session) {
141112
- const canonicalSession = normalizeStoredSession(session);
141113
- const dir = sessionsDir(workspaceDir);
141114
- await fs35.mkdir(dir, { recursive: true });
141115
- const file2 = sessionPath(workspaceDir, canonicalSession.conversationId);
141116
- const tmp = `${file2}.${randomUUID4()}.tmp`;
141117
- await fs35.writeFile(tmp, JSON.stringify(canonicalSession, null, 2) + "\n", "utf-8");
141118
- await fs35.rename(tmp, file2);
141119
- const legacyPath = alternateSessionPath(workspaceDir, session.conversationId);
141120
- if (legacyPath) {
141121
- await fs35.unlink(legacyPath).catch(() => {
141122
- });
141123
- }
141124
- }
141125
- async function upsertTelegramBootstrapSession(workspaceDir, input) {
141126
- const conversationId = toCanonicalTelegramBootstrapConversationId(input.conversationId);
141127
- const existing = await readTelegramBootstrapSession(workspaceDir, conversationId);
141128
- const resolvedSourceRoute = resolveNullableField(input.sourceRoute, existing?.sourceRoute);
141129
- const resolvedProjectRoute = resolveNullableField(input.projectRoute, existing?.projectRoute);
141130
- const resolvedProjectName = resolveNullableField(input.projectName, existing?.projectName);
141131
- const resolvedStackHint = resolveNullableField(input.stackHint, existing?.stackHint);
141132
- const resolvedRepoUrl = resolveNullableField(input.repoUrl, existing?.repoUrl);
141133
- const resolvedRepoPath = resolveNullableField(input.repoPath, existing?.repoPath);
141134
- const resolvedProjectSlug = resolveNullableField(input.projectSlug, existing?.projectSlug);
141135
- const resolvedIssueId = resolveNullableField(input.issueId, existing?.issueId);
141136
- const resolvedMessageThreadId = resolveNullableField(input.messageThreadId, existing?.messageThreadId);
141137
- const resolvedProjectChannelId = resolveNullableField(input.projectChannelId, existing?.projectChannelId);
141138
- const resolvedAttemptCount = resolveNullableField(input.attemptCount, existing?.attemptCount, 0);
141139
- const resolvedAttemptId = resolveNullableField(input.attemptId, existing?.attemptId);
141140
- const resolvedAttemptSeq = resolveNullableField(input.attemptSeq, existing?.attemptSeq);
141141
- const resolvedClassifySessionKey = resolveNullableField(input.classifySessionKey, existing?.classifySessionKey);
141142
- const resolvedClassifyRunId = resolveNullableField(input.classifyRunId, existing?.classifyRunId);
141143
- const resolvedClassifyStartedAt = resolveNullableField(input.classifyStartedAt, existing?.classifyStartedAt);
141144
- const resolvedBootstrapStep = resolveNullableField(input.bootstrapStep, existing?.bootstrapStep);
141145
- const resolvedNextRetryAt = input.nextRetryAt !== void 0 ? input.nextRetryAt : defaultNextRetryAtForStatus(input.status, existing?.nextRetryAt);
141146
- const resolvedAckSentAt = resolveNullableField(input.ackSentAt, existing?.ackSentAt);
141147
- const resolvedProjectRegisteredAt = resolveNullableField(input.projectRegisteredAt, existing?.projectRegisteredAt);
141148
- const resolvedTopicKickoffSentAt = resolveNullableField(input.topicKickoffSentAt, existing?.topicKickoffSentAt);
141149
- const resolvedProjectTickedAt = resolveNullableField(input.projectTickedAt, existing?.projectTickedAt);
141150
- const resolvedCompletionAckSentAt = resolveNullableField(input.completionAckSentAt, existing?.completionAckSentAt);
141151
- const resolvedError = input.error !== void 0 ? input.error : input.lastError !== void 0 ? input.lastError : existing?.error ?? existing?.lastError ?? null;
141152
- const requestHash = buildBootstrapRequestHash({
141153
- rawIdea: input.rawIdea,
141154
- projectName: resolvedProjectName,
141155
- stackHint: resolvedStackHint,
141156
- repoUrl: resolvedRepoUrl,
141157
- repoPath: resolvedRepoPath
141158
- });
141159
- const now2 = (/* @__PURE__ */ new Date()).toISOString();
141160
- const session = {
141161
- id: existing?.id ?? buildBootstrapSessionId(conversationId, input.rawIdea),
141162
- conversationId,
141163
- sourceChannel: input.sourceChannel ?? input.sourceRoute?.channel ?? existing?.sourceChannel ?? "telegram",
141164
- sourceRoute: resolvedSourceRoute,
141165
- projectRoute: resolvedProjectRoute,
141166
- requestHash,
141167
- requestFingerprint: requestHash,
141168
- lastCompletedRequestHash: input.status === "completed" ? requestHash : existing?.lastCompletedRequestHash ?? null,
141169
- rawIdea: input.rawIdea,
141170
- projectName: resolvedProjectName,
141171
- stackHint: resolvedStackHint,
141172
- repoUrl: resolvedRepoUrl,
141173
- repoPath: resolvedRepoPath,
141174
- projectSlug: resolvedProjectSlug,
141175
- issueId: resolvedIssueId,
141176
- messageThreadId: resolvedMessageThreadId,
141177
- projectChannelId: resolvedProjectChannelId,
141178
- language: input.language ?? existing?.language,
141179
- status: input.status,
141180
- attemptId: resolvedAttemptId,
141181
- attemptSeq: resolvedAttemptSeq,
141182
- classifySessionKey: resolvedClassifySessionKey,
141183
- classifyRunId: resolvedClassifyRunId,
141184
- classifyStartedAt: resolvedClassifyStartedAt,
141185
- bootstrapStep: resolvedBootstrapStep,
141186
- attemptCount: resolvedAttemptCount,
141187
- lastError: resolvedError,
141188
- nextRetryAt: resolvedNextRetryAt,
141189
- ackSentAt: resolvedAckSentAt,
141190
- projectRegisteredAt: resolvedProjectRegisteredAt,
141191
- topicKickoffSentAt: resolvedTopicKickoffSentAt,
141192
- projectTickedAt: resolvedProjectTickedAt,
141193
- completionAckSentAt: resolvedCompletionAckSentAt,
141194
- pendingClarification: input.pendingClarification !== void 0 ? input.pendingClarification : existing?.pendingClarification ?? null,
141195
- orphanedArtifacts: input.orphanedArtifacts !== void 0 ? input.orphanedArtifacts : existing?.orphanedArtifacts ?? null,
141196
- createdAt: existing?.createdAt ?? now2,
141197
- updatedAt: now2,
141198
- suppressUntil: nextSuppressUntil(input.status),
141199
- error: resolvedError
141200
- };
141201
- const writeDecision = shouldPersistBootstrapCheckpoint(existing, session);
141202
- if (!writeDecision.ok && existing) {
141203
- return existing;
141292
+ pickupCount++;
141204
141293
  }
141205
- await writeTelegramBootstrapSession(workspaceDir, session);
141206
- return session;
141294
+ return { pickups, skipped };
141207
141295
  }
141208
- function shouldSuppressTelegramBootstrapReply(session, request) {
141209
- if (!session) return false;
141210
- if (isTelegramBootstrapSessionExpired(session)) return false;
141211
- if (session.status === "completed" || session.status === "failed") {
141212
- if (session.status === "failed" && isReleasedClassifyFailure(session.error)) return false;
141213
- if (!request) return false;
141214
- 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;
141215
141302
  }
141216
- if (!request) return true;
141217
- return buildBootstrapRequestFingerprint(request) === session.requestHash;
141218
- }
141219
- function isTelegramBootstrapSessionExpired(session, now2 = Date.now()) {
141220
- if (!session) return false;
141221
- const suppressUntil = Date.parse(session.suppressUntil);
141222
- return !Number.isNaN(suppressUntil) && suppressUntil < now2;
141223
- }
141224
- function isRecoverableTelegramBootstrapSession(session) {
141225
- return session?.status === "bootstrapping" || session?.status === "dispatching";
141226
- }
141227
- function isClaimableTelegramBootstrapSession(session, now2 = Date.now()) {
141228
- if (!isRecoverableTelegramBootstrapSession(session)) return false;
141229
- if (!session.nextRetryAt) return true;
141230
- const retryAt = Date.parse(session.nextRetryAt);
141231
- return Number.isNaN(retryAt) || retryAt <= now2;
141303
+ return selectLevel(issue2.title, issue2.description ?? "", role).level;
141232
141304
  }
141233
- function isSupersededTelegramBootstrapAttempt(current, candidate) {
141234
- if (!current || !candidate) return false;
141235
- if (current.conversationId !== candidate.conversationId) return true;
141236
- const currentHasAttempt = current.attemptId != null && current.attemptSeq != null;
141237
- const candidateHasAttempt = candidate.attemptId != null && candidate.attemptSeq != null;
141238
- if (currentHasAttempt || candidateHasAttempt) {
141239
- return current.attemptId !== candidate.attemptId || current.attemptSeq !== candidate.attemptSeq;
141240
- }
141241
- 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;
141242
141308
  }
141243
141309
 
141244
141310
  // lib/dispatch/telegram-bootstrap-hook.ts
141311
+ init_audit();
141312
+ init_zod();
141245
141313
  init_constants();
141246
141314
  var BOOTSTRAP_RETRY_DELAY_MS = 15e3;
141247
141315
  var LAYER3_CONFIDENCE_THRESHOLD = 0.6;
@@ -143089,6 +143157,8 @@ Erro: ${result.error ?? "erro desconhecido"}`
143089
143157
  const projectChannelId = result.payload.metadata.channel_id ?? telegramConfig.projectsForumChatId;
143090
143158
  const messageThreadId = result.payload.metadata.message_thread_id;
143091
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;
143092
143162
  if (projectChannelId && messageThreadId) {
143093
143163
  const projectRoute = {
143094
143164
  channel: "telegram",
@@ -143103,6 +143173,10 @@ Erro: ${result.error ?? "erro desconhecido"}`
143103
143173
  bootstrapStep: "project_registered",
143104
143174
  projectName: resolvedProjectName,
143105
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,
143106
143180
  projectRegisteredAt: (/* @__PURE__ */ new Date()).toISOString(),
143107
143181
  projectChannelId: String(projectChannelId),
143108
143182
  messageThreadId,
@@ -143118,6 +143192,10 @@ Erro: ${result.error ?? "erro desconhecido"}`
143118
143192
  status: "dispatching",
143119
143193
  projectName: resolvedProjectName,
143120
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,
143121
143199
  projectRegisteredAt: (/* @__PURE__ */ new Date()).toISOString(),
143122
143200
  projectChannelId: String(projectChannelId),
143123
143201
  messageThreadId,
@@ -143133,6 +143211,10 @@ Erro: ${result.error ?? "erro desconhecido"}`
143133
143211
  ...registeredSession,
143134
143212
  projectName: resolvedProjectName,
143135
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,
143136
143218
  projectRegisteredAt: registeredSession.projectRegisteredAt ?? (/* @__PURE__ */ new Date()).toISOString(),
143137
143219
  language: currentSession?.language ?? "pt"
143138
143220
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mestreyoda/fabrica",
3
- "version": "0.2.21",
3
+ "version": "0.2.22",
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",