@mestreyoda/fabrica 0.2.15 → 0.2.16

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/README.md CHANGED
@@ -96,7 +96,17 @@ operational/workspace state, then tells you what is still missing.
96
96
 
97
97
  ## Quick start
98
98
 
99
- **1. Install Fabrica**:
99
+ This is the minimum recommended path to get Fabrica working end-to-end with the official product flow.
100
+
101
+ **1. Authenticate GitHub CLI**:
102
+
103
+ ```bash
104
+ gh auth status || gh auth login
105
+ ```
106
+
107
+ Fabrica uses authenticated `gh` CLI for GitHub operations in the default setup.
108
+
109
+ **2. Install Fabrica**:
100
110
 
101
111
  ```bash
102
112
  openclaw plugins install @mestreyoda/fabrica
@@ -104,37 +114,69 @@ openclaw plugins install @mestreyoda/fabrica
104
114
 
105
115
  The plugin should load immediately after install, without manual remediation.
106
116
 
107
- **2. Confirm loadability**:
117
+ **3. Confirm loadability**:
108
118
 
109
119
  ```bash
110
120
  openclaw plugins inspect fabrica
111
121
  ```
112
122
 
113
- **3. Configure operational state for a workspace**:
123
+ **4. Configure Fabrica for a workspace**:
114
124
 
115
125
  ```bash
116
126
  openclaw fabrica doctor workspace --workspace /path/to/workspace
117
127
  openclaw fabrica setup --workspace /path/to/workspace --new-agent fabrica
118
128
  ```
119
129
 
120
- Use `openclaw fabrica setup --agent <id>` if you already have an agent. GitHub,
121
- Telegram, and webhook behavior are separate operational concerns, not
122
- installation dependencies.
130
+ Use `openclaw fabrica setup --agent <id>` if you already have an agent.
131
+
132
+ When the official Telegram DM bootstrap flow is configured (`bootstrapDmEnabled=true`
133
+ plus `projectsForumChatId`), `openclaw fabrica setup` also prepares the dedicated
134
+ internal `genesis` agent automatically.
135
+
136
+ **5. Configure Telegram for the official Fabrica flow**:
137
+
138
+ The official flow is:
139
+ - Telegram DM with the bot for new-project intake
140
+ - one Telegram forum group for project topics/timelines
141
+
142
+ At minimum, when DM bootstrap is enabled, set:
143
+ - `plugins.entries.fabrica.config.telegram.bootstrapDmEnabled=true`
144
+ - `plugins.entries.fabrica.config.telegram.projectsForumChatId=<YOUR_PROJECTS_FORUM_CHAT_ID>`
145
+
146
+ If `projectsForumChatId` is missing while DM bootstrap is enabled, Fabrica can accept the DM but will fail when it needs to create the project topic.
147
+
148
+ **6. Validate operational readiness**:
149
+
150
+ ```bash
151
+ openclaw plugins inspect fabrica
152
+ openclaw fabrica doctor workspace --workspace /path/to/workspace
153
+ ```
123
154
 
124
155
  **Environment provisioning note**:
125
156
 
126
- Developer and tester pickup now pass through a stack environment gate. For
127
- supported stacks such as `python-cli`, Fabrica provisions the required toolchain
128
- and project-local environment before dispatching workers, instead of discovering
129
- missing dependencies inside a live worker run.
157
+ Developer and tester pickup pass through a stack environment gate. Fabrica
158
+ prepares the project environment before dispatching real work, instead of
159
+ finding missing dependencies inside a live worker run.
160
+
161
+ For Python projects, this includes just-in-time `uv` installation when needed,
162
+ a shared toolchain, and a project-local `.venv`.
130
163
 
131
- **4. Restart the gateway**:
164
+ For existing Node projects, Fabrica expects a reproducible lockfile
165
+ (`package-lock.json`, `pnpm-lock.yaml`, `yarn.lock`, or `bun.lock`) before
166
+ real developer/tester dispatch. Greenfield scaffold mode can materialize the
167
+ first deterministic lockfile, but regular runtime pickup fails closed without
168
+ one.
169
+
170
+ `dryRun: true` skips environment provisioning entirely and remains side-effect
171
+ free.
172
+
173
+ **7. Restart the gateway if needed**:
132
174
 
133
175
  ```bash
134
176
  systemctl --user restart openclaw-gateway.service
135
177
  ```
136
178
 
137
- **5. Trigger a new project programmatically**:
179
+ **8. Trigger a new project programmatically**:
138
180
 
139
181
  ```bash
140
182
  cd ~/fabrica # GitHub clone install only
@@ -146,13 +188,13 @@ npx tsx scripts/genesis-trigger.ts "A CLI tool that counts words in a file" \
146
188
 
147
189
  Remove `--dry-run` to execute for real.
148
190
 
149
- **6. Watch the pipeline run**:
191
+ **9. Watch the pipeline run**:
150
192
 
151
193
  ```bash
152
194
  tail -f ~/.openclaw/workspace/logs/genesis.log
153
195
  ```
154
196
 
155
- **7. Check metrics**:
197
+ **10. Check metrics**:
156
198
 
157
199
  ```bash
158
200
  openclaw fabrica metrics
@@ -223,7 +265,11 @@ Use plugin config when you want explicit webhook behavior or provider auth profi
223
265
 
224
266
  ### With Telegram
225
267
 
226
- Telegram enables DM-based project bootstrap, per-project forum topics, and a separate ops chat for heartbeat and cron notifications.
268
+ Telegram is the primary human-facing entrypoint for Fabrica:
269
+ - DM with the bot for new-project intake and short clarifications
270
+ - one Telegram forum group where Fabrica creates one topic per project
271
+
272
+ Recommended minimum Telegram configuration:
227
273
 
228
274
  ```json
229
275
  {
@@ -243,8 +289,7 @@ Telegram enables DM-based project bootstrap, per-project forum topics, and a sep
243
289
  "telegram": {
244
290
  "bootstrapDmEnabled": true,
245
291
  "projectsForumChatId": "<YOUR_PROJECTS_FORUM_CHAT_ID>",
246
- "projectsForumAccountId": "<OPTIONAL_TELEGRAM_ACCOUNT_ID>",
247
- "opsChatId": "<YOUR_OPS_CHAT_ID>"
292
+ "projectsForumAccountId": "<OPTIONAL_TELEGRAM_ACCOUNT_ID>"
248
293
  }
249
294
  }
250
295
  }
@@ -253,7 +298,15 @@ Telegram enables DM-based project bootstrap, per-project forum topics, and a sep
253
298
  }
254
299
  ```
255
300
 
256
- With Telegram enabled, send a project idea to the bot in a DM. Fabrica will ask clarifying questions, provision the GitHub repo, create a dedicated forum topic for the project, and keep ops-only notifications on the separate `opsChatId` route.
301
+ `projectsForumChatId` is the key Fabrica-specific Telegram setting for the official DM topic flow.
302
+
303
+ When both `bootstrapDmEnabled=true` and `projectsForumChatId` are present,
304
+ `openclaw fabrica setup` automatically prepares the internal `genesis` agent used
305
+ for the DM intake path.
306
+
307
+ `opsChatId` still exists in plugin config for deployments that want a separate ops-only route, but it is not required for the core product flow.
308
+
309
+ With Telegram enabled, send a project idea to the bot in a DM. Fabrica will ask clarifying questions, provision the GitHub repo, create a dedicated forum topic for the project, and continue the project lifecycle in that topic.
257
310
 
258
311
  Project topics are event-driven timelines. Fabrica emits explicit messages for
259
312
  worker start, worker completion, review queueing, reviewer reject/approve, and
package/dist/index.js CHANGED
@@ -111347,8 +111347,8 @@ import fsSync from "node:fs";
111347
111347
  import path5 from "node:path";
111348
111348
  import { fileURLToPath as fileURLToPath3 } from "node:url";
111349
111349
  function getCurrentVersion() {
111350
- if ("0.2.15") {
111351
- return "0.2.15";
111350
+ if ("0.2.16") {
111351
+ return "0.2.16";
111352
111352
  }
111353
111353
  try {
111354
111354
  const pkgPath = path5.join(THIS_DIR, "..", "..", "package.json");
@@ -127660,6 +127660,7 @@ async function registerProject(params) {
127660
127660
  }
127661
127661
  await writeProjects(workspaceDir, data);
127662
127662
  projectsPersisted = true;
127663
+ let finalResolvedConfig = resolvedConfig;
127663
127664
  if (autonomousProject) {
127664
127665
  workflowOverrideCreated = await ensureAutonomousWorkflowOverride(
127665
127666
  workspaceDir,
@@ -127676,6 +127677,7 @@ async function registerProject(params) {
127676
127677
  `Autonomous DM bootstrap resolved reviewPolicy="${persistedConfig.workflow.reviewPolicy}" for "${slug}", expected "${expectedReviewPolicy}"`
127677
127678
  );
127678
127679
  }
127680
+ finalResolvedConfig = persistedConfig;
127679
127681
  }
127680
127682
  promptsCreated = await scaffoldPromptFiles(workspaceDir, slug);
127681
127683
  await log(workspaceDir, "project_register", {
@@ -127690,7 +127692,7 @@ async function registerProject(params) {
127690
127692
  deployUrl: deployUrl || null,
127691
127693
  isNewProject: !existing,
127692
127694
  workflowOverrideCreated,
127693
- reviewPolicy: resolvedConfig.workflow.reviewPolicy ?? "human"
127695
+ reviewPolicy: finalResolvedConfig.workflow.reviewPolicy ?? "human"
127694
127696
  });
127695
127697
  const action = existing ? "Channel added to existing project" : `Project "${name}" created`;
127696
127698
  const promptsNote = promptsCreated ? " Prompt files scaffolded." : "";
@@ -127710,8 +127712,8 @@ async function registerProject(params) {
127710
127712
  workflowOverrideCreated,
127711
127713
  isNewProject: !existing,
127712
127714
  activeWorkflow: {
127713
- reviewPolicy: resolvedConfig.workflow.reviewPolicy ?? "human",
127714
- testPhase: Object.values(resolvedConfig.workflow.states).some(
127715
+ reviewPolicy: finalResolvedConfig.workflow.reviewPolicy ?? "human",
127716
+ testPhase: Object.values(finalResolvedConfig.workflow.states).some(
127715
127717
  (s2) => s2.role === "tester" && (s2.type === "queue" || s2.type === "active")
127716
127718
  ),
127717
127719
  hint: "The user can change the review policy or enable the test phase \u2014 call workflow_guide for the full reference."
@@ -129995,9 +129997,11 @@ async function ensureGenesisAgent(runtime, runCommand, opts) {
129995
129997
  throw new Error(`Failed to create genesis agent: ${err.message}`);
129996
129998
  }
129997
129999
  const updatedConfig = runtime.config.loadConfig();
129998
- const bindings = (updatedConfig.bindings ?? []).filter(
129999
- (b) => !(b.match?.channel === "telegram" && !b.match?.peer && b.agentId === "main")
130000
- );
130000
+ const bindings = (updatedConfig.bindings ?? []).filter((b) => {
130001
+ const isMainChannelWideTelegram = b.match?.channel === "telegram" && !b.match?.peer && b.agentId === "main";
130002
+ if (!isMainChannelWideTelegram) return true;
130003
+ return !opts?.forumGroupId;
130004
+ });
130001
130005
  if (!bindings.some((b) => b.agentId === "genesis" && b.match?.channel === "telegram" && !b.match?.peer)) {
130002
130006
  bindings.push({ agentId: "genesis", match: { channel: "telegram" } });
130003
130007
  }
@@ -135830,12 +135834,13 @@ async function ensurePythonToolchain(runCommand, homeDir) {
135830
135834
  }
135831
135835
  await fs32.rm(toolchainPath, { recursive: true, force: true });
135832
135836
  }
135837
+ const uvCmd = await ensureUv(runCommand);
135833
135838
  await fs32.mkdir(path32.dirname(toolchainPath), { recursive: true });
135834
- const venvResult = await runCommand("uv", ["venv", toolchainPath], { timeout: 6e4 });
135839
+ const venvResult = await runCommand(uvCmd, ["venv", toolchainPath], { timeout: 6e4 });
135835
135840
  if (venvResult.exitCode !== 0) {
135836
135841
  throw new Error(`Failed to create toolchain venv: ${venvResult.stderr}`);
135837
135842
  }
135838
- const installResult = await runCommand("uv", [
135843
+ const installResult = await runCommand(uvCmd, [
135839
135844
  "pip",
135840
135845
  "install",
135841
135846
  "-p",
@@ -136103,7 +136108,7 @@ async function ensurePythonEnvironment(repoPath, stack, runCommand) {
136103
136108
  fingerprint
136104
136109
  };
136105
136110
  }
136106
- await ensureUv(runCommand);
136111
+ const uvCmd = await ensureUv(runCommand);
136107
136112
  const uvLock = await pathExists2(path32.join(repoPath, "uv.lock"));
136108
136113
  const pyproject = await pathExists2(path32.join(repoPath, "pyproject.toml"));
136109
136114
  const requirements = await pathExists2(path32.join(repoPath, "requirements.txt"));
@@ -136122,10 +136127,25 @@ async function ensurePythonEnvironment(repoPath, stack, runCommand) {
136122
136127
  reason: "missing_pyproject_or_requirements"
136123
136128
  };
136124
136129
  }
136130
+ if (uvLock && !pyproject) {
136131
+ return {
136132
+ ready: false,
136133
+ skipped: false,
136134
+ stack,
136135
+ family: "python",
136136
+ toolchain: "uv",
136137
+ packageManager: "uv",
136138
+ lockfile: "uv.lock",
136139
+ environmentPath: null,
136140
+ commandsRun,
136141
+ fingerprint,
136142
+ reason: "uv_lock_requires_pyproject"
136143
+ };
136144
+ }
136125
136145
  await ensurePythonToolchain(runCommand);
136126
136146
  if (uvLock) {
136127
136147
  await runAndAssert(runCommand, repoPath, commandsRun, {
136128
- cmd: "uv",
136148
+ cmd: uvCmd,
136129
136149
  args: ["sync", "--locked"],
136130
136150
  reason: "uv sync"
136131
136151
  });
@@ -136144,13 +136164,13 @@ async function ensurePythonEnvironment(repoPath, stack, runCommand) {
136144
136164
  };
136145
136165
  }
136146
136166
  await runAndAssert(runCommand, repoPath, commandsRun, {
136147
- cmd: "uv",
136167
+ cmd: uvCmd,
136148
136168
  args: ["venv", ".venv"],
136149
136169
  reason: "create project-local virtualenv with uv"
136150
136170
  });
136151
- if (requirements) {
136171
+ if (requirements && !pyproject) {
136152
136172
  await runAndAssert(runCommand, repoPath, commandsRun, {
136153
- cmd: "uv",
136173
+ cmd: uvCmd,
136154
136174
  args: ["pip", "install", "--python", ".venv/bin/python", "-r", "requirements.txt"],
136155
136175
  reason: "install requirements.txt dependencies with uv"
136156
136176
  });
@@ -136158,7 +136178,7 @@ async function ensurePythonEnvironment(repoPath, stack, runCommand) {
136158
136178
  if (pyproject) {
136159
136179
  const hasDevExtra = await hasPyprojectDevExtra(repoPath);
136160
136180
  await runAndAssert(runCommand, repoPath, commandsRun, {
136161
- cmd: "uv",
136181
+ cmd: uvCmd,
136162
136182
  args: ["pip", "install", "--python", ".venv/bin/python", "-e", hasDevExtra ? ".[dev]" : "."],
136163
136183
  reason: hasDevExtra ? "install editable project with dev extras via uv" : "install editable project via uv"
136164
136184
  });
@@ -137436,6 +137456,7 @@ function getProjectEnvironmentState(project, stack) {
137436
137456
 
137437
137457
  // lib/test-env/runtime.ts
137438
137458
  var STALE_PROVISIONING_WINDOW_MS = 10 * 60 * 1e3;
137459
+ var FAILED_RETRY_DELAY_MS = 60 * 1e3;
137439
137460
  function projectEnvironmentStateFor(project, stack, updates) {
137440
137461
  const current = getProjectEnvironmentState(project, stack);
137441
137462
  return getProjectEnvironmentState({
@@ -137449,6 +137470,7 @@ function projectEnvironmentStateFor(project, stack, updates) {
137449
137470
  async function ensureEnvironmentReady(opts) {
137450
137471
  const contract = resolveStackEnvironmentContract(opts.stack);
137451
137472
  const current = getProjectEnvironmentState(opts.project, opts.stack);
137473
+ const retryAtMs = current.nextProvisionRetryAt ? Date.parse(current.nextProvisionRetryAt) : Number.NaN;
137452
137474
  if (current.status === "ready") {
137453
137475
  return { ready: true, state: current };
137454
137476
  }
@@ -137456,6 +137478,16 @@ async function ensureEnvironmentReady(opts) {
137456
137478
  const provisioningStartedAt = current.provisioningStartedAt ? Date.parse(current.provisioningStartedAt) : Number.NaN;
137457
137479
  const provisioningIsFresh = Number.isFinite(provisioningStartedAt) && Date.now() - provisioningStartedAt < STALE_PROVISIONING_WINDOW_MS;
137458
137480
  if (provisioningIsFresh) {
137481
+ await log(opts.workspaceDir, "environment_bootstrap_blocked", {
137482
+ projectSlug: opts.projectSlug,
137483
+ stack: opts.stack,
137484
+ mode: opts.mode,
137485
+ status: current.status,
137486
+ reason: "provisioning_in_progress",
137487
+ provisioningStartedAt: current.provisioningStartedAt ?? null,
137488
+ contractVersion: current.contractVersion ?? contract.version
137489
+ }).catch(() => {
137490
+ });
137459
137491
  return { ready: false, state: current };
137460
137492
  }
137461
137493
  await log(opts.workspaceDir, "environment_bootstrap_retry_scheduled", {
@@ -137466,7 +137498,18 @@ async function ensureEnvironmentReady(opts) {
137466
137498
  }).catch(() => {
137467
137499
  });
137468
137500
  }
137469
- if (current.status === "failed" && current.nextProvisionRetryAt && Date.parse(current.nextProvisionRetryAt) > Date.now()) {
137501
+ if (current.status === "failed" && current.nextProvisionRetryAt && Number.isFinite(retryAtMs) && retryAtMs > Date.now()) {
137502
+ await log(opts.workspaceDir, "environment_bootstrap_blocked", {
137503
+ projectSlug: opts.projectSlug,
137504
+ stack: opts.stack,
137505
+ mode: opts.mode,
137506
+ status: current.status,
137507
+ reason: "retry_backoff_active",
137508
+ blockedUntil: current.nextProvisionRetryAt,
137509
+ lastProvisionError: current.lastProvisionError ?? null,
137510
+ contractVersion: current.contractVersion ?? contract.version
137511
+ }).catch(() => {
137512
+ });
137470
137513
  return { ready: false, state: current };
137471
137514
  }
137472
137515
  await updateProjectEnvironment(opts.workspaceDir, opts.projectSlug, {
@@ -137480,6 +137523,8 @@ async function ensureEnvironmentReady(opts) {
137480
137523
  await log(opts.workspaceDir, "environment_bootstrap_started", {
137481
137524
  projectSlug: opts.projectSlug,
137482
137525
  stack: opts.stack,
137526
+ mode: opts.mode,
137527
+ previousStatus: current.status,
137483
137528
  contractVersion: contract.version
137484
137529
  }).catch(() => {
137485
137530
  });
@@ -137505,7 +137550,7 @@ async function ensureEnvironmentReady(opts) {
137505
137550
  reason: error48 instanceof Error ? error48.message : "environment_bootstrap_failed"
137506
137551
  }));
137507
137552
  if (!result.ready) {
137508
- const nextRetryAt = new Date(Date.now() + 6e4).toISOString();
137553
+ const nextRetryAt = new Date(Date.now() + FAILED_RETRY_DELAY_MS).toISOString();
137509
137554
  const state2 = projectEnvironmentStateFor(opts.project, opts.stack, {
137510
137555
  status: "failed",
137511
137556
  stack: opts.stack,
@@ -137526,8 +137571,11 @@ async function ensureEnvironmentReady(opts) {
137526
137571
  await log(opts.workspaceDir, "environment_bootstrap_retry_scheduled", {
137527
137572
  projectSlug: opts.projectSlug,
137528
137573
  stack: opts.stack,
137574
+ mode: opts.mode,
137529
137575
  nextRetryAt,
137530
- reason: state2.lastProvisionError ?? "environment_bootstrap_failed"
137576
+ retryDelayMs: FAILED_RETRY_DELAY_MS,
137577
+ reason: state2.lastProvisionError ?? "environment_bootstrap_failed",
137578
+ contractVersion: state2.contractVersion ?? contract.version
137531
137579
  }).catch(() => {
137532
137580
  });
137533
137581
  return { ready: false, state: state2 };
@@ -137554,6 +137602,9 @@ async function ensureEnvironmentReady(opts) {
137554
137602
  await log(opts.workspaceDir, "environment_ready_confirmed", {
137555
137603
  projectSlug: opts.projectSlug,
137556
137604
  stack: opts.stack,
137605
+ mode: opts.mode,
137606
+ previousStatus: current.status,
137607
+ lastProvisionedAt: provisionedAt,
137557
137608
  contractVersion: contract.version
137558
137609
  }).catch(() => {
137559
137610
  });
@@ -137620,6 +137671,15 @@ async function cleanupExpired(workspaceDir, ttlMs = DEFAULT_TTL_MS2) {
137620
137671
 
137621
137672
  // lib/services/tick.ts
137622
137673
  init_registry();
137674
+ function classifyEnvironmentGateSkip(state) {
137675
+ if (state.status === "provisioning") return "environment_provisioning_in_progress";
137676
+ if (state.status === "failed") {
137677
+ if (state.nextProvisionRetryAt) return "environment_retry_backoff_active";
137678
+ return "environment_failed";
137679
+ }
137680
+ if (state.status === "pending") return "environment_pending_provisioning";
137681
+ return "environment_not_ready";
137682
+ }
137623
137683
  async function projectTick(opts) {
137624
137684
  const {
137625
137685
  workspaceDir,
@@ -137859,12 +137919,16 @@ async function projectTick(opts) {
137859
137919
  runCommand
137860
137920
  });
137861
137921
  if (!environment.ready) {
137862
- skipped.push({ role, reason: "environment_not_ready" });
137922
+ const environmentSkipReason = classifyEnvironmentGateSkip(environment.state);
137923
+ skipped.push({ role, reason: environmentSkipReason });
137863
137924
  await log(workspaceDir, "dispatch_blocked_environment_not_ready", {
137864
137925
  projectSlug,
137865
137926
  role,
137866
137927
  issueId: issue2.iid,
137867
- environmentStatus: environment.state.status
137928
+ reason: environmentSkipReason,
137929
+ environmentStatus: environment.state.status,
137930
+ nextProvisionRetryAt: environment.state.nextProvisionRetryAt ?? null,
137931
+ lastProvisionError: environment.state.lastProvisionError ?? null
137868
137932
  }).catch(() => {
137869
137933
  });
137870
137934
  continue;
@@ -140081,14 +140145,16 @@ Erro: ${result.error ?? "erro desconhecido"}`
140081
140145
  );
140082
140146
  }
140083
140147
  function registerTelegramBootstrapHook(api, ctx) {
140148
+ const telegramConfig = readFabricaTelegramConfig(ctx.pluginConfig);
140084
140149
  const apiRuntime = api.runtime;
140085
140150
  const hasGenesis = apiRuntime ? hasGenesisAgent(apiRuntime) : false;
140086
- if (!hasGenesis) {
140151
+ const usesDedicatedGenesisBootstrap = hasGenesis && telegramConfig.bootstrapDmEnabled && Boolean(telegramConfig.projectsForumChatId);
140152
+ if (!usesDedicatedGenesisBootstrap) {
140087
140153
  api.on("before_dispatch", async (event, eventCtx) => {
140088
140154
  const hookCtx = eventCtx;
140089
140155
  if (hookCtx.channelId !== "telegram") return void 0;
140090
- const telegramConfig = readFabricaTelegramConfig(ctx.pluginConfig);
140091
- if (!telegramConfig.bootstrapDmEnabled) return void 0;
140156
+ const telegramConfig2 = readFabricaTelegramConfig(ctx.pluginConfig);
140157
+ if (!telegramConfig2.bootstrapDmEnabled) return void 0;
140092
140158
  const conversationId = resolveTelegramBootstrapDmConversationIdFromHookContext(hookCtx);
140093
140159
  if (!conversationId) return void 0;
140094
140160
  const dispatchEvent = event;
@@ -140172,11 +140238,11 @@ function registerTelegramBootstrapHook(api, ctx) {
140172
140238
  }
140173
140239
  });
140174
140240
  }
140175
- if (hasGenesis) return;
140241
+ if (usesDedicatedGenesisBootstrap) return;
140176
140242
  api.on("message_received", async (event, eventCtx) => {
140177
140243
  if (eventCtx.channelId !== "telegram") return;
140178
- const telegramConfig = readFabricaTelegramConfig(ctx.pluginConfig);
140179
- if (!telegramConfig.bootstrapDmEnabled) return;
140244
+ const telegramConfig2 = readFabricaTelegramConfig(ctx.pluginConfig);
140245
+ if (!telegramConfig2.bootstrapDmEnabled) return;
140180
140246
  const rawConversationId = String(eventCtx.conversationId ?? "").trim();
140181
140247
  const content = String(event.content ?? "").trim();
140182
140248
  if (!rawConversationId || !content) return;
@@ -142373,6 +142439,8 @@ function createSetupTool(ctx) {
142373
142439
  ${written.map((f3) => ` ${f3}`).join("\n")}` : "All files already exist \u2014 nothing to write."
142374
142440
  });
142375
142441
  }
142442
+ const telegramConfig = readFabricaTelegramConfig(ctx.pluginConfig);
142443
+ const shouldEnsureGenesis = telegramConfig.bootstrapDmEnabled && Boolean(telegramConfig.projectsForumChatId);
142376
142444
  const result = await runSetup({
142377
142445
  runtime: ctx.runtime,
142378
142446
  newAgentName: params.newAgentName,
@@ -142382,7 +142450,9 @@ ${written.map((f3) => ` ${f3}`).join("\n")}` : "All files already exist \u2014
142382
142450
  workspacePath: params.newAgentName ? void 0 : toolCtx.workspaceDir,
142383
142451
  models: params.models,
142384
142452
  projectExecution: params.projectExecution,
142385
- runCommand: ctx.runCommand
142453
+ runCommand: ctx.runCommand,
142454
+ ensureGenesis: shouldEnsureGenesis,
142455
+ forumGroupId: telegramConfig.projectsForumChatId
142386
142456
  });
142387
142457
  const lines = [
142388
142458
  result.agentCreated ? `Agent "${result.agentId}" created` : `Configured "${result.agentId}"`,
@@ -142536,16 +142606,19 @@ Call \`setup\` with the collected answers:
142536
142606
  After setup completes, explain the current operating model:
142537
142607
 
142538
142608
  \u{1F4F1} **Telegram Guidance:**
142539
- Fabrica uses:
142609
+ Fabrica uses the following official path:
142540
142610
  1. **DM with the bot** for new-project bootstrap and short clarifications
142541
142611
  2. **One forum group for projects** with **one topic per project**
142542
- 3. **One separate ops group** for health/cron/status
142543
142612
 
142544
- **Recommended Setup:**
142613
+ **Minimum recommended setup:**
142545
142614
  1. Keep the bot reachable in DM
142546
142615
  2. Add the bot to the projects forum group
142547
142616
  3. Ensure the bot can create/manage topics there
142548
- 4. Keep cron/ops notifications in a separate group
142617
+ 4. Set 'plugins.entries.fabrica.config.telegram.projectsForumChatId' to that forum group ID
142618
+
142619
+ **Optional / advanced:**
142620
+ - 'projectsForumAccountId' if a specific Telegram account should own forum actions
142621
+ - 'opsChatId' only if you want a separate ops-only route; it is not required for the core product flow
142549
142622
 
142550
142623
  **Step 5: Project Registration**
142551
142624
  Explain that the canonical path for new projects is:
@@ -142562,10 +142635,10 @@ Manual \`project_register\` remains available for admin recovery and exceptional
142562
142635
  **Step 6: Workflow Overview**
142563
142636
  After project registration, briefly tell the user about their active workflow:
142564
142637
 
142565
- - **Review policy**: agent for autonomous DM-created projects \u2014 PRs flow through Fabrica review by default.
142566
- - **Test phase**: skipped by default \u2014 the testing step is in the workflow but issues bypass it automatically. To enable testing for a specific issue, remove the \`test:skip\` label. To enable globally, set \`testPolicy: agent\` in workflow.yaml.
142567
- - **Customization**: They can change the review policy (human/agent/auto), enable testing (testPolicy: agent), or override settings per project. Point them to \`workflow.yaml\` in the Fabrica workspace data directory.
142568
- - Say: "Autonomous projects created from DM use **agent review** and **testing skipped** by default. You can enable testing per-issue by removing the \`test:skip\` label, or globally by setting \`testPolicy: agent\` in your workflow.yaml."
142638
+ - **Review policy**: autonomous DM-created projects use **agent review** by default as a quality guardrail.
142639
+ - **Test phase**: the current workflow still defaults to 'testPolicy: skip' unless explicitly enabled. Explain this as the current operational default, not the quality ideal of the product.
142640
+ - **Customization**: They can change the review policy ('human', 'agent', 'skip'), enable testing ('testPolicy: agent'), or override settings per project. Point them to 'workflow.yaml' in the Fabrica workspace data directory.
142641
+ - Say: "Autonomous projects created from DM use **agent review** by default. Testing is still skipped by default in the current workflow unless you enable it, so if you need stricter QA by default you should change 'testPolicy' in your workflow.yaml."
142569
142642
 
142570
142643
  ## Guidelines
142571
142644
  - Be conversational and friendly. Ask one question at a time.
@@ -144163,19 +144236,20 @@ async function checkConfigLoads(workspacePath) {
144163
144236
  }
144164
144237
  }
144165
144238
  function checkTelegramBootstrapConfig(pluginConfig) {
144166
- const telegram = pluginConfig?.telegram;
144167
- if (!telegram?.bootstrapDmEnabled) {
144239
+ const rawTelegram = pluginConfig?.telegram ?? {};
144240
+ const telegram = readFabricaTelegramConfig(pluginConfig);
144241
+ if (rawTelegram.bootstrapDmEnabled === false) {
144168
144242
  return {
144169
144243
  name: "config:telegram-bootstrap",
144170
144244
  severity: "ok",
144171
- message: "Telegram DM bootstrap is disabled (bootstrapDmEnabled not set)"
144245
+ message: "Telegram DM bootstrap is disabled (bootstrapDmEnabled=false)"
144172
144246
  };
144173
144247
  }
144174
- if (!telegram?.projectsForumChatId) {
144248
+ if (!telegram.projectsForumChatId) {
144175
144249
  return {
144176
144250
  name: "config:telegram-bootstrap",
144177
144251
  severity: "warn",
144178
- message: "Telegram DM bootstrap is enabled (bootstrapDmEnabled=true) but projectsForumChatId is not configured \u2014 DM bootstrap will fail at runtime"
144252
+ message: "Telegram DM bootstrap is active by default but projectsForumChatId is not configured in plugins.entries.fabrica.config.telegram \u2014 the official DM \u2192 topic flow will fail at runtime"
144179
144253
  };
144180
144254
  }
144181
144255
  return {
@@ -144442,13 +144516,21 @@ async function runSecurityDoctor(openclawHome) {
144442
144516
  severity: residualPlugins.length > 0 ? "warn" : "ok",
144443
144517
  message: residualPlugins.length > 0 ? `Residual plugins still enabled: ${residualPlugins.join(", ")}` : "No residual non-essential plugins enabled"
144444
144518
  });
144445
- const routerBinding = (config2.bindings ?? []).find(
144519
+ const legacyRouterBinding = (config2.bindings ?? []).find(
144446
144520
  (binding) => binding.agentId === "genesis-router" && binding.match?.channel === "telegram"
144447
144521
  );
144448
144522
  checks.push({
144449
144523
  name: "bindings:telegram-router",
144450
- severity: routerBinding ? "warn" : "ok",
144451
- message: routerBinding ? "Telegram is still bound to genesis-router" : "Telegram is not bound to legacy genesis-router"
144524
+ severity: legacyRouterBinding ? "warn" : "ok",
144525
+ message: legacyRouterBinding ? "Telegram is still bound to legacy genesis-router" : "Telegram is not bound to legacy genesis-router"
144526
+ });
144527
+ const genesisBinding = (config2.bindings ?? []).find(
144528
+ (binding) => binding.agentId === "genesis" && binding.match?.channel === "telegram" && !binding.match?.peer
144529
+ );
144530
+ checks.push({
144531
+ name: "bindings:telegram-genesis",
144532
+ severity: genesisBinding ? "ok" : "warn",
144533
+ message: genesisBinding ? "Genesis agent owns the channel-wide Telegram DM binding" : "Genesis agent does not own a channel-wide Telegram binding"
144452
144534
  });
144453
144535
  try {
144454
144536
  await fs42.access(checklistPath);
@@ -144763,13 +144845,17 @@ function registerCli(program, ctx) {
144763
144845
  }
144764
144846
  if (Object.keys(roleModels).length > 0) models[role] = roleModels;
144765
144847
  }
144848
+ const telegramConfig = readFabricaTelegramConfig(ctx.pluginConfig);
144849
+ const shouldEnsureGenesis = telegramConfig.bootstrapDmEnabled && Boolean(telegramConfig.projectsForumChatId);
144766
144850
  const result = await runSetup({
144767
144851
  runtime: ctx.runtime,
144768
144852
  newAgentName: opts.newAgent,
144769
144853
  agentId: opts.agent,
144770
144854
  workspacePath: opts.workspace,
144771
144855
  models: Object.keys(models).length > 0 ? models : void 0,
144772
- runCommand: ctx.runCommand
144856
+ runCommand: ctx.runCommand,
144857
+ ensureGenesis: shouldEnsureGenesis,
144858
+ forumGroupId: telegramConfig.projectsForumChatId
144773
144859
  });
144774
144860
  if (result.agentCreated) {
144775
144861
  console.log(`Agent "${result.agentId}" created`);
@@ -144791,9 +144877,15 @@ function registerCli(program, ctx) {
144791
144877
  }
144792
144878
  }
144793
144879
  console.log("\nDone! Next steps:");
144794
- console.log(" 1. Keep the bot reachable in DM");
144795
- console.log(" 2. Add the bot to the projects forum group and allow topic creation");
144796
- console.log(" 3. Send the project idea to the bot in DM to bootstrap the project automatically");
144880
+ if (shouldEnsureGenesis) {
144881
+ console.log(" 1. Keep the bot reachable in DM");
144882
+ console.log(" 2. Add the bot to the projects forum group and allow topic creation");
144883
+ console.log(" 3. Send the project idea to the bot in DM to bootstrap the project automatically");
144884
+ } else {
144885
+ console.log(" 1. Run `openclaw fabrica doctor workspace --workspace <path>` to confirm workspace readiness");
144886
+ console.log(" 2. If you want the official Telegram DM \u2192 topic flow, set plugins.entries.fabrica.config.telegram.projectsForumChatId");
144887
+ console.log(" 3. Re-run `openclaw fabrica setup` after the Telegram forum config is in place");
144888
+ }
144797
144889
  });
144798
144890
  const doctor = fabrica.command("doctor").description("Diagnose workspace integrity and optionally auto-fix issues");
144799
144891
  doctor.command("workspace").description("Diagnose workspace integrity and optionally auto-fix issues").option("-w, --workspace <path>", "Workspace directory").option("--fix", "Apply fixes for detected issues").action(async (opts) => {
@@ -146107,7 +146199,7 @@ var plugin = {
146107
146199
  },
146108
146200
  telegram: {
146109
146201
  type: "object",
146110
- description: "Telegram routing for DM bootstrap, project forum topics, and ops notifications.",
146202
+ description: "Telegram routing for the official DM \u2192 project-topic flow. Core settings are DM bootstrap and the projects forum chat.",
146111
146203
  properties: {
146112
146204
  bootstrapDmEnabled: {
146113
146205
  type: "boolean",
@@ -146116,15 +146208,15 @@ var plugin = {
146116
146208
  },
146117
146209
  projectsForumChatId: {
146118
146210
  type: "string",
146119
- description: "Telegram forum group chat ID where Fabrica creates one topic per project."
146211
+ description: "Telegram forum group chat ID where Fabrica creates one topic per project. This is the key Telegram setting for the official Fabrica flow."
146120
146212
  },
146121
146213
  projectsForumAccountId: {
146122
146214
  type: "string",
146123
- description: "Optional Telegram account ID to use when creating/sending project forum topics."
146215
+ description: "Optional Telegram account ID to use when creating or sending project forum topic updates."
146124
146216
  },
146125
146217
  opsChatId: {
146126
146218
  type: "string",
146127
- description: "Telegram group/chat ID for cron, health, and ops-only notifications."
146219
+ description: "Optional Telegram group/chat ID for separate ops-only notifications. Not required for the core DM \u2192 topic product flow."
146128
146220
  }
146129
146221
  }
146130
146222
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mestreyoda/fabrica",
3
- "version": "0.2.15",
3
+ "version": "0.2.16",
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",
@@ -18,8 +18,6 @@
18
18
  "genesis/",
19
19
  "fabrica.manifest.json",
20
20
  "openclaw.plugin.json",
21
- "ARCHITECTURE.md",
22
- "CHANGELOG.md",
23
21
  "README.md",
24
22
  "LICENSE"
25
23
  ],
@@ -50,7 +48,7 @@
50
48
  },
51
49
  "repository": {
52
50
  "type": "git",
53
- "url": "https://github.com/MestreY0d4-Uninter/fabrica.git"
51
+ "url": "git+https://github.com/MestreY0d4-Uninter/fabrica.git"
54
52
  },
55
53
  "homepage": "https://github.com/MestreY0d4-Uninter/fabrica#readme",
56
54
  "bugs": {
package/ARCHITECTURE.md DELETED
@@ -1,93 +0,0 @@
1
- # Architecture
2
-
3
- ## Core shape
4
-
5
- Fabrica is implemented as a local OpenClaw plugin with the local repository as
6
- its source of truth.
7
-
8
- Main areas:
9
-
10
- - `lib/intake`
11
- Intake, target resolution, impact analysis, task creation and triage.
12
- - `lib/github`
13
- GitHub App auth, webhook ingestion, event store, PR binding, quality gate and
14
- governance.
15
- - `lib/services`
16
- Pipeline, heartbeat, queue scans and workflow execution helpers.
17
- - `lib/machines`
18
- `FabricaRunMachine` and `LifecycleMachine` for explicit state transitions.
19
- - `lib/observability`
20
- Pino logging, correlation context and OpenTelemetry spans.
21
- - `lib/dispatch`
22
- DM bootstrap, Telegram topic routing, worker notifications and attachment hooks.
23
- - `lib/telegram`
24
- Telegram config resolution and topic creation services.
25
- - `defaults`
26
- Packaged assets and workflow defaults that ship with the plugin.
27
- - `genesis`
28
- Packaged runtime assets still used by the plugin during the migration away
29
- from older shell-driven flows.
30
-
31
- ## Runtime model
32
-
33
- ## Telegram routing model
34
-
35
- New project intake is DM-first. The Fabrica bot accepts a new-project request in
36
- Telegram DM, asks for missing essentials there if needed, and only creates the
37
- project topic when the intake is ready to register. For greenfield projects,
38
- repo provisioning now happens in the TS intake path before registration and
39
- issue creation.
40
-
41
- The canonic route identity for Telegram-backed projects is:
42
-
43
- `channel=telegram + channelId + messageThreadId`
44
-
45
- This avoids collisions between multiple projects inside the same Telegram forum
46
- group. After registration:
47
-
48
- - the project topic becomes the primary route for project messages
49
- - follow-ups inside that topic resolve the exact project
50
- - worker notifications and project lifecycle updates publish back to that topic
51
- - ops alerts stay in the separate ops group
52
-
53
- The hot path for GitHub is:
54
-
55
- `webhook -> event store -> FabricaRun -> Quality Gate -> artifactOfRecord -> done`
56
-
57
- Important invariants:
58
-
59
- - a cycle never closes with an open canonical PR
60
- - `Done` requires `artifactOfRecord`
61
- - duplicate GitHub deliveries must not duplicate effects
62
- - force-push updates the canonical binding instead of spawning duplicate runs
63
-
64
- ## Installation model
65
-
66
- Fabrica is distributed as a self-contained OpenClaw plugin package.
67
-
68
- The supported operator path is:
69
-
70
- ```bash
71
- openclaw plugins install @mestreyoda/fabrica
72
- ```
73
-
74
- The installed extension must be loadable in isolation. Fabrica may depend on
75
- OpenClaw only through the plugin host ABI and runtime objects passed by the
76
- host. It must not require manual symlinks, local `npm install`, or host-global
77
- module resolution to load.
78
-
79
- External credentials and routes such as GitHub auth, Telegram chat IDs, and
80
- webhook secrets are operational configuration, not installation dependencies.
81
- Fabrica's `doctor` and `setup` flows guide and validate that operational
82
- configuration where applicable.
83
-
84
- ## Operational notes
85
-
86
- - Gateway runtime is managed by the OpenClaw systemd service.
87
- - GitHub webhook ingress is protected by GitHub signature validation inside the
88
- plugin; the route itself must remain reachable without gateway bearer auth.
89
- - GitHub App and webhook credentials are expected to come from the Fabrica
90
- plugin config (`openclaw.json`) using direct values and credential file paths;
91
- legacy env-based fields remain only as compatibility fallback.
92
- - Structured logs and OpenTelemetry spans are emitted by the plugin itself.
93
- - Security validation lives in `openclaw fabrica doctor security --json`.
package/CHANGELOG.md DELETED
@@ -1,42 +0,0 @@
1
- # Changelog
2
-
3
- ## 0.2.15 - 2026-04-03
4
-
5
- - Hardened Telegram DM intake around durable `pending_classify` / `classifying` recovery, newer-attempt ownership, and explicit late-classify reconciliation.
6
- - Added runtime-aware DM claiming via `before_dispatch` plus short-lived message/conversation guards so Telegram prompts stay inside Fabrica instead of leaking to the generic OpenClaw agent.
7
- - Fixed greenfield scaffold canonical repo path handling so `metadata.repo_path` / `scaffold_plan.repo_local` survive all the way into `scaffold-project.sh` and published genesis assets.
8
- - Tightened bootstrap and register fail-closed behavior for unsupported stacks and missing materialized repositories, preventing half-registered validation projects.
9
- - Reset and revalidated the temporary Telegram validation harness, including a reusable runner path and regression coverage for read/wait flows.
10
- - Extended regression coverage for Telegram bootstrap recovery, scaffold path ownership, classify-step typing, and end-to-end hot-path stability.
11
-
12
- ## 0.2.14 - 2026-04-02
13
-
14
- - Added a stack-aware environment gate so developer and tester pickup only start after project environments are provisioned and marked ready.
15
- - Hardened Python stack bootstrap around durable environment state, retry scheduling, and stale provisioning recovery without `sudo`.
16
- - Reworked worker recovery so observable activity without a canonical result enters bounded completion recovery instead of immediately corrupting dispatch health.
17
- - Made heartbeat distinguish accepted-but-idle dispatches, inconclusive completion, terminal sessions, and true dead sessions with cycle-aware ownership checks.
18
- - Added explicit timeline events for reviewer outcomes and worker recovery exhaustion, with cycle-aware dedupe and corrected destination-state messaging.
19
- - Preserved reviewer notification routing through plugin notification config instead of bypassing runtime settings.
20
- - Extended regression coverage for environment provisioning, gateway session transcript activity, heartbeat recovery, reviewer notifications, and end-to-end hot-path orchestration.
21
-
22
- ## 0.2.13 - 2026-03-31
23
-
24
- - Disabled automatic pretty logging on TTY so the plugin no longer depends on `pino-pretty` during load.
25
- - Added a safe one-shot fallback to structured logs when pretty transport resolution or initialization fails.
26
- - Added logger transport regression coverage and promoted it into the hot-path test lane.
27
-
28
- ## 0.2.12 - 2026-03-31
29
-
30
- - Made the published plugin self-contained by replacing remaining runtime helper imports from `openclaw/plugin-sdk`.
31
- - Added release gates for runtime-boundary and isolated installability verification, with fail-closed behavior, timeouts, and cleanup.
32
- - Documented the real install contract and workspace-scoped operational setup flow in the package README and architecture notes.
33
- - Aligned local deploy to the same contract by removing the extension `node_modules` symlink fallback.
34
-
35
- ## 0.2.11 - 2026-03-31
36
-
37
- - Unified reviewer completion around the canonical `Review result: APPROVE|REJECT` contract.
38
- - Hardened dispatch identity and reduced heartbeat progression to repair-oriented behavior.
39
- - Enforced canonical PR selection across reviewer/tester and queue-side flows.
40
- - Preserved and hardened the Telegram three-plane model: DM bootstrap, project forum topics, and ops routing.
41
- - Restored confidence lanes for release verification with explicit `test:unit`, `test:e2e`, and `test:hot-path`.
42
- - Aligned plugin config governance and observability surfaces so runtime knobs are real and misleading signals no longer present themselves as authoritative.