@mestreyoda/fabrica 0.1.12 → 0.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -110768,7 +110768,7 @@ var init_registry = __esm({
110768
110768
  senior: "\u{1F9E0}"
110769
110769
  },
110770
110770
  fallbackEmoji: "\u{1F50D}",
110771
- completionResults: ["pass", "fail", "refine", "blocked"],
110771
+ completionResults: ["pass", "fail", "fail_infra", "refine", "blocked"],
110772
110772
  sessionKeyPattern: "tester",
110773
110773
  notifications: { onStart: true, onComplete: true }
110774
110774
  },
@@ -111329,8 +111329,8 @@ import fsSync from "node:fs";
111329
111329
  import path5 from "node:path";
111330
111330
  import { fileURLToPath as fileURLToPath3 } from "node:url";
111331
111331
  function getCurrentVersion() {
111332
- if ("0.1.12") {
111333
- return "0.1.12";
111332
+ if ("0.1.14") {
111333
+ return "0.1.14";
111334
111334
  }
111335
111335
  try {
111336
111336
  const pkgPath = path5.join(THIS_DIR, "..", "..", "package.json");
@@ -119852,6 +119852,22 @@ PR merged \u2014 issue closed automatically (was stuck in ${event.fromState})`;
119852
119852
  \u{1F3C1} Done \u2014 no human action needed.`;
119853
119853
  return msg;
119854
119854
  }
119855
+ case "infraFailure": {
119856
+ const icon = event.infraFailCount >= 2 ? "\u{1F6A8}" : "\u26A0\uFE0F";
119857
+ let msg = `${icon} Infrastructure failure on #${event.issueId} (attempt ${event.infraFailCount})`;
119858
+ msg += `
119859
+ ${event.summary}`;
119860
+ msg += `
119861
+ \u{1F4CB} [Issue #${event.issueId}](${event.issueUrl})`;
119862
+ if (event.infraFailCount >= 2) {
119863
+ msg += `
119864
+ \u2192 Circuit breaker tripped \u2014 moved to Refining (operator intervention required)`;
119865
+ } else {
119866
+ msg += `
119867
+ \u2192 Returned to To Test queue \u2014 will retry after toolchain is fixed`;
119868
+ }
119869
+ return msg;
119870
+ }
119855
119871
  }
119856
119872
  }
119857
119873
  async function sendMessage(target, message, channel, workspaceDir, runtime, accountId, runCommand, messageThreadId, auditMeta) {
@@ -120589,10 +120605,10 @@ init_audit();
120589
120605
  init_migrate_layout();
120590
120606
  init_roles();
120591
120607
  init_workflow();
120608
+ init_labels();
120592
120609
 
120593
120610
  // lib/tools/tasks/public-output-sanitizer.ts
120594
120611
  var SECRET_PATTERNS = [
120595
- /\b[A-Za-z_][A-Za-z0-9_]*=([^\s]+)/g,
120596
120612
  /\b(?:ghp_|gho_|github_pat_|sk-|xoxb-|xoxp-|AIza|AKIA|glpat-)[A-Za-z0-9._-]*/g,
120597
120613
  /\b(?:token|secret|api[_-]?key|password|passwd|authorization|bearer)\b\s*[:=]\s*[^\s]+/gi
120598
120614
  ];
@@ -120917,6 +120933,7 @@ function matchesReviewArtifact(comment, artifactId, artifactType) {
120917
120933
  }
120918
120934
  return comment.state === "COMMENTED" && !comment.path;
120919
120935
  }
120936
+ var INFRA_FAIL_CIRCUIT_BREAKER_THRESHOLD = 2;
120920
120937
  function createWorkFinishTool(ctx) {
120921
120938
  return (toolCtx) => ({
120922
120939
  name: "work_finish",
@@ -120928,7 +120945,7 @@ function createWorkFinishTool(ctx) {
120928
120945
  properties: {
120929
120946
  channelId: { type: "string", description: "YOUR chat/group ID \u2014 the numeric ID of the chat you are in right now (e.g. '-1003844794417'). Do NOT guess; use the ID of the conversation this message came from." },
120930
120947
  role: { type: "string", enum: getAllRoleIds(), description: "Worker role" },
120931
- result: { type: "string", enum: ["done", "pass", "fail", "refine", "blocked", "approve", "reject"], description: "Completion result" },
120948
+ result: { type: "string", enum: ["done", "pass", "fail", "fail_infra", "refine", "blocked", "approve", "reject"], description: "Completion result. Use fail_infra (tester only) when the test toolchain is missing or broken \u2014 this keeps the issue in the test queue instead of routing it to the developer." },
120932
120949
  summary: { type: "string", description: "Brief summary" },
120933
120950
  prUrl: { type: "string", description: "PR/MR URL (auto-detected if omitted)" },
120934
120951
  createdTasks: {
@@ -121010,6 +121027,79 @@ function createWorkFinishTool(ctx) {
121010
121027
  })),
121011
121028
  keyTransitions: resolvedConfig.workflowMeta.keyTransitions
121012
121029
  });
121030
+ if (role === "tester" && result === "fail_infra") {
121031
+ const currentInfraFails = (issueRuntime?.infraFailCount ?? 0) + 1;
121032
+ await updateIssueRuntime(workspaceDir, project.slug, issueId, {
121033
+ infraFailCount: currentInfraFails
121034
+ });
121035
+ await log(workspaceDir, "infra_failure", {
121036
+ project: project.name,
121037
+ issue: issueId,
121038
+ role,
121039
+ result,
121040
+ summary: summary ?? null,
121041
+ infraFailCount: currentInfraFails
121042
+ });
121043
+ const notifyConfig = getNotificationConfig(ctx.pluginConfig);
121044
+ const target = resolveNotifyChannel([], project.channels);
121045
+ const issueUrl = `https://github.com/${project.repo}/issues/${issueId}`;
121046
+ await notify(
121047
+ {
121048
+ type: "infraFailure",
121049
+ project: project.name,
121050
+ issueId,
121051
+ issueUrl,
121052
+ summary: summary ?? "Infrastructure failure during testing",
121053
+ infraFailCount: currentInfraFails
121054
+ },
121055
+ {
121056
+ workspaceDir,
121057
+ config: notifyConfig,
121058
+ channelId: target?.channelId,
121059
+ channel: target?.channel ?? "telegram",
121060
+ runtime: ctx.runtime,
121061
+ accountId: target?.accountId,
121062
+ messageThreadId: target?.messageThreadId,
121063
+ runCommand: ctx.runCommand
121064
+ }
121065
+ ).catch((err) => {
121066
+ getRootLogger().warn(`infra_failure notification failed: ${err}`);
121067
+ });
121068
+ if (currentInfraFails >= INFRA_FAIL_CIRCUIT_BREAKER_THRESHOLD) {
121069
+ await log(workspaceDir, "infra_failure_circuit_breaker", {
121070
+ project: project.name,
121071
+ issue: issueId,
121072
+ infraFailCount: currentInfraFails
121073
+ });
121074
+ await resilientLabelTransition(provider, issueId, "Testing", "Refining");
121075
+ } else {
121076
+ await resilientLabelTransition(provider, issueId, "Testing", "To Test");
121077
+ }
121078
+ await deactivateWorker(workspaceDir, project.slug, "tester", {
121079
+ level: slotLevel,
121080
+ slotIndex,
121081
+ issueId: String(issueId)
121082
+ });
121083
+ await recordIssueLifecycle({
121084
+ workspaceDir,
121085
+ slug: project.slug,
121086
+ issueId,
121087
+ stage: "session_completed",
121088
+ sessionKey: toolCtx.sessionKey ?? null,
121089
+ details: { role, result, infraFailCount: currentInfraFails }
121090
+ }).catch(() => {
121091
+ });
121092
+ return jsonResult2({
121093
+ success: true,
121094
+ project: project.name,
121095
+ projectSlug: project.slug,
121096
+ issueId,
121097
+ role,
121098
+ result,
121099
+ infraFailCount: currentInfraFails,
121100
+ circuitBroken: currentInfraFails >= INFRA_FAIL_CIRCUIT_BREAKER_THRESHOLD
121101
+ });
121102
+ }
121013
121103
  if (!getRule(role, result, workflow))
121014
121104
  throw new Error(`Invalid completion: ${role}:${result}`);
121015
121105
  const repoPath = resolveRepoPath(project.repo);
@@ -121128,6 +121218,9 @@ function createWorkFinishTool(ctx) {
121128
121218
  details: { role, result }
121129
121219
  }).catch(() => {
121130
121220
  });
121221
+ if (role === "tester" && issueRuntime?.infraFailCount) {
121222
+ await updateIssueRuntime(workspaceDir, project.slug, issueId, { infraFailCount: 0 });
121223
+ }
121131
121224
  return jsonResult2({
121132
121225
  success: true,
121133
121226
  project: project.name,
@@ -136063,25 +136156,39 @@ async function runHeartbeatTick(ctx, logger6, mode) {
136063
136156
  })
136064
136157
  );
136065
136158
  const tickFn = lifecycle ? () => lifecycle.track(mode === "repair" ? "recovery" : "heartbeat", {}, run) : run;
136066
- let tickPromise;
136067
- const wrappedTickFn = () => {
136068
- tickPromise = tickFn();
136069
- return tickPromise;
136159
+ let resolveTick;
136160
+ let rejectTick;
136161
+ const tickPromise = new Promise((res, rej) => {
136162
+ resolveTick = res;
136163
+ rejectTick = rej;
136164
+ });
136165
+ tickPromise.catch(() => {
136166
+ });
136167
+ const wrappedTickFn = async () => {
136168
+ try {
136169
+ const result = await tickFn();
136170
+ resolveTick(result);
136171
+ return result;
136172
+ } catch (err) {
136173
+ rejectTick(err);
136174
+ throw err;
136175
+ }
136070
136176
  };
136177
+ const HARD_TICK_TIMEOUT_MS = 5 * 6e4;
136071
136178
  const raceResult = await raceWithTimeout(wrappedTickFn, DEFAULT_TICK_TIMEOUT_MS, () => {
136072
136179
  _ticksTimedOut++;
136073
136180
  timedOut = true;
136074
136181
  logger6.warn(`work_heartbeat ${mode} tick timed out after ${DEFAULT_TICK_TIMEOUT_MS}ms (total timeouts: ${_ticksTimedOut})`);
136075
- if (tickPromise) {
136076
- tickPromise.finally(() => {
136077
- _tickRunning[mode] = false;
136078
- _anyTickRunning = false;
136079
- });
136080
- } else {
136081
- logger6.error("tick_mutex: tickPromise undefined in timeout handler \u2014 forcing mutex release");
136182
+ const hardTimeout = setTimeout(() => {
136183
+ logger6.error("tick_mutex: hard timeout \u2014 forcing mutex release");
136082
136184
  _tickRunning[mode] = false;
136083
136185
  _anyTickRunning = false;
136084
- }
136186
+ }, HARD_TICK_TIMEOUT_MS);
136187
+ tickPromise.finally(() => {
136188
+ clearTimeout(hardTimeout);
136189
+ _tickRunning[mode] = false;
136190
+ _anyTickRunning = false;
136191
+ });
136085
136192
  });
136086
136193
  void raceResult;
136087
136194
  } catch (err) {
@@ -138569,15 +138676,21 @@ Install manually: curl -LsSf ${UV_INSTALL_URL} | sh`
138569
138676
  var PYTHON_TOOLCHAIN_PACKAGES = ["ruff", "mypy", "pip-audit"];
138570
138677
  var TOOLCHAIN_DIR = ".openclaw/toolchains/python";
138571
138678
  var TOOLCHAIN_FINGERPRINT_FILE = "toolchain.sha256";
138572
- function toolchainFingerprint() {
138573
- return createHash4("sha256").update(PYTHON_TOOLCHAIN_PACKAGES.join(",")).digest("hex");
138679
+ async function toolchainFingerprint(runCommand) {
138680
+ let pythonVersion = "unknown";
138681
+ try {
138682
+ const result = await runCommand("python3", ["--version"], { timeout: 5e3 });
138683
+ if (result.exitCode === 0) pythonVersion = result.stdout.trim();
138684
+ } catch {
138685
+ }
138686
+ return createHash4("sha256").update(PYTHON_TOOLCHAIN_PACKAGES.join(",") + ":" + pythonVersion).digest("hex");
138574
138687
  }
138575
138688
  async function ensurePythonToolchain(runCommand, homeDir) {
138576
138689
  const home = homeDir ?? process.env.HOME ?? "/tmp";
138577
138690
  const toolchainPath = path34.join(home, TOOLCHAIN_DIR);
138578
138691
  const ruffPath = path34.join(toolchainPath, "bin", "ruff");
138579
138692
  const fingerprintPath = path34.join(toolchainPath, TOOLCHAIN_FINGERPRINT_FILE);
138580
- const expectedFp = toolchainFingerprint();
138693
+ const expectedFp = await toolchainFingerprint(runCommand);
138581
138694
  if (await isValidBinary(ruffPath)) {
138582
138695
  try {
138583
138696
  const currentFp = (await fs34.readFile(fingerprintPath, "utf-8")).trim();
@@ -142095,8 +142208,6 @@ function registerAttachmentHook(api, ctx) {
142095
142208
 
142096
142209
  // lib/dispatch/telegram-bootstrap-hook.ts
142097
142210
  import { homedir as homedir3 } from "node:os";
142098
- init_runtime_paths();
142099
- init_extract_json();
142100
142211
  init_zod();
142101
142212
 
142102
142213
  // lib/dispatch/telegram-bootstrap-session.ts
@@ -142131,14 +142242,14 @@ function buildBootstrapRequestHash(input) {
142131
142242
  return buildBootstrapRequestFingerprint(input);
142132
142243
  }
142133
142244
  function nextSuppressUntil(status) {
142134
- const ttl = status === "classifying" ? CLASSIFYING_TTL_MS : SESSION_TTL_MS;
142245
+ const ttl = status === "classifying" || status === "pending_classify" ? CLASSIFYING_TTL_MS : SESSION_TTL_MS;
142135
142246
  return new Date(Date.now() + ttl).toISOString();
142136
142247
  }
142137
142248
  async function readTelegramBootstrapSession(workspaceDir, conversationId) {
142138
142249
  try {
142139
142250
  const raw = await fs38.readFile(sessionPath(workspaceDir, conversationId), "utf-8");
142140
142251
  const session = JSON.parse(raw);
142141
- if ((session.status === "clarifying" || session.status === "classifying") && Date.parse(session.suppressUntil) < Date.now()) {
142252
+ if ((session.status === "clarifying" || session.status === "classifying" || session.status === "pending_classify") && Date.parse(session.suppressUntil) < Date.now()) {
142142
142253
  await fs38.unlink(sessionPath(workspaceDir, conversationId)).catch(() => {
142143
142254
  });
142144
142255
  return null;
@@ -142228,6 +142339,10 @@ var BOOTSTRAP_MESSAGES = {
142228
142339
  pt: "N\xE3o consegui identificar a stack. Pode me dizer qual linguagem/framework voc\xEA quer usar? Ex: Python, Node.js, Go, Java...",
142229
142340
  en: "Couldn't identify the stack. Can you tell me which language/framework you'd like to use? e.g., Python, Node.js, Go, Java..."
142230
142341
  },
142342
+ clarifyName: {
142343
+ pt: "Como voc\xEA quer chamar o projeto? Se preferir, posso escolher um nome.",
142344
+ en: "What do you want to name the project? If you prefer, I can pick one."
142345
+ },
142231
142346
  registered: {
142232
142347
  pt: (name, link) => `Projeto "${name}" registrado.
142233
142348
  Vou continuar o fluxo em ${link}`,
@@ -142236,7 +142351,9 @@ I'll continue the flow at ${link}`
142236
142351
  }
142237
142352
  };
142238
142353
  function inferProjectSlug(text) {
142239
- const slug = text.toLowerCase().normalize("NFKD").replace(/[^\w\s-]/g, "").trim().replace(/\s+/g, "-").replace(/-+/g, "-").slice(0, 64);
142354
+ let cleaned = text.replace(/^(create|build|crie|cria|criar|fazer?|quero|i need|i want)\s+(uma|um|me\s+a?|an|a)?\s*/i, "").replace(/\s+(that|which|que|para|for|pra)\s+.*/i, "").trim();
142355
+ if (!cleaned) cleaned = text;
142356
+ const slug = cleaned.toLowerCase().normalize("NFKD").replace(/[^\w\s-]/g, "").trim().replace(/\s+/g, "-").replace(/-+/g, "-").slice(0, 64);
142240
142357
  return slug || void 0;
142241
142358
  }
142242
142359
  function normalizeText3(value) {
@@ -142309,26 +142426,31 @@ Examples:
142309
142426
  - "How's the project going?" \u2192 {"intent":"other","confidence":0.95,"stackHint":null,"projectSlug":null,"language":"en"}
142310
142427
  - "Oi, tudo bem?" \u2192 {"intent":"other","confidence":0.99,"stackHint":null,"projectSlug":null,"language":"pt"}
142311
142428
  - "Me faz um app que converte temperaturas" \u2192 {"intent":"create_project","confidence":0.9,"stackHint":null,"projectSlug":"conversor-temperaturas","language":"pt"}`;
142312
- async function classifyDmIntent(ctx, content, workspaceDir) {
142429
+ async function classifyDmIntent(ctx, content, _workspaceDir) {
142313
142430
  try {
142431
+ const runtime = ctx.runtime;
142432
+ if (!runtime?.subagent?.run) return null;
142314
142433
  const truncated = content.slice(0, MAX_CLASSIFY_LENGTH);
142315
142434
  const prompt = CLASSIFY_PROMPT_TEMPLATE.replace("$CONTENT", truncated.replace(/"/g, '\\"'));
142316
- const cliPath = resolveOpenClawCli({ homeDir: homedir3(), workspaceDir });
142317
- const sessionId = `dm-classify-${Date.now()}`;
142318
- const result = await ctx.runCommand(
142319
- [cliPath, "agent", "--local", "-m", prompt, "--session-id", sessionId, "--json"],
142320
- { timeoutMs: 15e3 }
142321
- );
142322
- const stdout = result.stdout ?? "";
142323
- if (!stdout.trim()) return null;
142324
- const parsed = extractJsonFromStdout(stdout);
142325
- if (!parsed) return null;
142326
- const text = parsed?.payloads?.[0]?.text;
142327
- const jsonStr = text ? text.replace(/^```(json)?/gm, "").replace(/```$/gm, "").trim() : JSON.stringify(parsed);
142435
+ const sessionKey = `dm-classify-${Date.now()}`;
142436
+ const { runId } = await runtime.subagent.run({
142437
+ sessionKey,
142438
+ message: prompt,
142439
+ extraSystemPrompt: "You are a JSON classifier. Return ONLY valid JSON, no markdown fences.",
142440
+ lane: "subagent",
142441
+ deliver: false,
142442
+ idempotencyKey: sessionKey
142443
+ });
142444
+ const waitResult = await runtime.subagent.waitForRun({ runId, timeoutMs: 15e3 });
142445
+ if (waitResult.status !== "ok") return null;
142446
+ const messages = await runtime.subagent.getSessionMessages({ sessionKey });
142447
+ const lastAssistant = messages?.filter((m2) => m2.role === "assistant").pop();
142448
+ const text = lastAssistant?.content ?? "";
142449
+ if (!text.trim()) return null;
142450
+ const jsonStr = text.replace(/^```(json)?/gm, "").replace(/```$/gm, "").trim();
142328
142451
  const intentData = JSON.parse(jsonStr);
142329
142452
  const validated = DmIntentSchema.safeParse(intentData);
142330
- if (!validated.success) return null;
142331
- return validated.data;
142453
+ return validated.success ? validated.data : null;
142332
142454
  } catch {
142333
142455
  return null;
142334
142456
  }
@@ -142341,6 +142463,21 @@ function isBootstrapCandidate(text) {
142341
142463
  return createCue && softwareCue;
142342
142464
  }
142343
142465
  function parseClarificationResponse(text, session) {
142466
+ if (session.pendingClarification === "name") {
142467
+ const trimmed = text.trim();
142468
+ const autoPatterns = /^(escolha|pick one|tanto faz|you choose|pode escolher|auto|skip)$/i;
142469
+ if (autoPatterns.test(trimmed)) {
142470
+ return { recognized: true, projectName: inferProjectSlug(session.rawIdea) ?? void 0, stackHint: session.stackHint ?? void 0 };
142471
+ }
142472
+ const nameField = parseField(text, ["project name", "nome do projeto", "nome", "name"]);
142473
+ if (nameField) {
142474
+ return { recognized: true, projectName: nameField, stackHint: session.stackHint ?? void 0 };
142475
+ }
142476
+ if (trimmed.length > 0 && trimmed.length <= 64) {
142477
+ return { recognized: true, projectName: trimmed, stackHint: session.stackHint ?? void 0 };
142478
+ }
142479
+ return { recognized: false };
142480
+ }
142344
142481
  const stackField = parseField(text, ["stack", "framework", "linguagem", "language"]);
142345
142482
  if (stackField) {
142346
142483
  return { recognized: true, stackHint: detectStackHint(stackField) ?? stackField };
@@ -142376,6 +142513,9 @@ function parseClarificationResponse(text, session) {
142376
142513
  return { recognized: false };
142377
142514
  }
142378
142515
  function buildClarificationMessage(parsed, pendingClarification, language = "pt") {
142516
+ if (pendingClarification === "name") {
142517
+ return BOOTSTRAP_MESSAGES.clarifyName[language];
142518
+ }
142379
142519
  if (pendingClarification === "stack_and_name" || !parsed.stackHint && !parsed.projectName) {
142380
142520
  return BOOTSTRAP_MESSAGES.clarifyBoth[language];
142381
142521
  }
@@ -142421,6 +142561,12 @@ function logBootstrapWarning(ctx, message) {
142421
142561
  }
142422
142562
  }
142423
142563
  async function classifyAndBootstrap(ctx, workspaceDir, conversationId, content) {
142564
+ await upsertTelegramBootstrapSession(workspaceDir, {
142565
+ conversationId,
142566
+ rawIdea: content,
142567
+ sourceRoute: { channel: "telegram", channelId: conversationId },
142568
+ status: "classifying"
142569
+ });
142424
142570
  const classification = await classifyDmIntent(ctx, content, workspaceDir);
142425
142571
  if (!classification || classification.intent !== "create_project" || classification.confidence < 0.7) {
142426
142572
  if (!classification) {
@@ -142530,6 +142676,28 @@ async function continueBootstrap(ctx, conversationId, workspaceDir, request2, so
142530
142676
  ));
142531
142677
  return;
142532
142678
  }
142679
+ if (!projectName) {
142680
+ const existingSession = await readTelegramBootstrapSession(workspaceDir, conversationId);
142681
+ const lang = existingSession?.language ?? "pt";
142682
+ await upsertTelegramBootstrapSession(workspaceDir, {
142683
+ conversationId,
142684
+ rawIdea: request2.rawIdea,
142685
+ stackHint: request2.stackHint ?? void 0,
142686
+ status: "clarifying",
142687
+ pendingClarification: "name",
142688
+ language: lang
142689
+ });
142690
+ await sendTelegramText(
142691
+ ctx,
142692
+ conversationId,
142693
+ buildClarificationMessage(
142694
+ { rawIdea: request2.rawIdea, projectName: void 0, stackHint: request2.stackHint ?? void 0 },
142695
+ "name",
142696
+ lang
142697
+ )
142698
+ );
142699
+ return;
142700
+ }
142533
142701
  const candidateSlug = inferProjectSlug(projectName ?? request2.rawIdea);
142534
142702
  if (candidateSlug) {
142535
142703
  const projects = await readProjects(workspaceDir).catch(() => null);
@@ -142759,7 +142927,7 @@ function registerTelegramBootstrapHook(api, ctx) {
142759
142927
  }
142760
142928
  const existingSession = await readTelegramBootstrapSession(workspaceDir, conversationId);
142761
142929
  const sessionIsExpired = existingSession != null && Date.parse(existingSession.suppressUntil) < Date.now();
142762
- if (existingSession && !sessionIsExpired && existingSession.status === "classifying") {
142930
+ if (existingSession && !sessionIsExpired && (existingSession.status === "classifying" || existingSession.status === "pending_classify")) {
142763
142931
  ctx.logger.info(`[telegram-bootstrap] LLM classification in progress for ${conversationId}, ignoring concurrent message`);
142764
142932
  return;
142765
142933
  }
@@ -142790,7 +142958,7 @@ function registerTelegramBootstrapHook(api, ctx) {
142790
142958
  conversationId,
142791
142959
  rawIdea: content,
142792
142960
  sourceRoute: { channel: "telegram", channelId: conversationId },
142793
- status: "classifying"
142961
+ status: "pending_classify"
142794
142962
  });
142795
142963
  classifyAndBootstrap(ctx, workspaceDir, conversationId, content).catch((err) => {
142796
142964
  logBootstrapWarning(ctx, `[telegram-bootstrap] LLM classify error: ${err instanceof Error ? err.message : String(err)}`);
@@ -142799,6 +142967,12 @@ function registerTelegramBootstrapHook(api, ctx) {
142799
142967
  return;
142800
142968
  }
142801
142969
  const parsed = parseBootstrapRequest(content);
142970
+ if (!parsed.projectName && ctx.runtime?.subagent?.run) {
142971
+ const classification = await classifyDmIntent(ctx, content, workspaceDir);
142972
+ if (classification?.projectSlug) {
142973
+ parsed.projectName = classification.projectSlug;
142974
+ }
142975
+ }
142802
142976
  const incomingRequest = {
142803
142977
  rawIdea: parsed.rawIdea,
142804
142978
  projectName: parsed.projectName ?? null,