<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://fabritorio.dev/blog/feed.xml" rel="self" type="application/atom+xml" /><link href="https://fabritorio.dev/" rel="alternate" type="text/html" /><updated>2026-06-14T18:33:25+00:00</updated><id>https://fabritorio.dev/blog/feed.xml</id><title type="html">Fabritorio</title><subtitle>An agent factory — assemble the parts, watch it run. Field notes from building it in public.</subtitle><author><name>Eduard</name></author><entry><title type="html">Building in public</title><link href="https://fabritorio.dev/blog/building-in-public/" rel="alternate" type="text/html" title="Building in public" /><published>2026-06-13T00:00:00+00:00</published><updated>2026-06-13T00:00:00+00:00</updated><id>https://fabritorio.dev/blog/building-in-public</id><content type="html" xml:base="https://fabritorio.dev/blog/building-in-public/"><![CDATA[<p>After two months of building Fabritorio, I finally “launched” the project. By launch of course I mean setting the repo visibility from “private” -&gt; “public”, posting in r/LocalLLaMA and calling it a day.</p>

<h2 id="the-problem">The problem</h2>

<p>Around October last year I built a new PC. The idea was to use it mostly to catch up with some AI stuff with a bit of gaming on the side. Long story short, the gaming won and it was used 90% of the time to play games.</p>

<p>Most of my AI usage is in agentic coding and the open models are unfortunately not there yet. Beyond that I didn’t have any other use case for agents. Or so I thought until I tried setting up OpenClaw earlier this year. I have a lot of processes that I just do daily, partly thanks to ADHD, but mostly I just like systems. So seeing the ease of talking to an agent and setting up an automated workflow after half an hour captivated my engineering itch.</p>

<p>The biggest friction with setting up agents is just always starting from scratch. You have to create some sort of small harness, create the tools, wire it up, adjust, and so on and so forth. So like any sane engineer I wanted to spend months of my time to automate something that could’ve just taken me weeks instead.</p>

<p>So I started hacking, first sparring on how to make it as “live” as possible. I wanted to move away from the workflow based state of available toolings, where you draw something and then click “play”. I want it to exactly feel like Factorio, you are wiring things live, and steering far away from looking like any low-code / no-code tool. Although in the end I wasn’t able to get that, it’s a good middleground.</p>

<h2 id="whats-next">What’s next</h2>

<p>I’m still quite proud of how it turned out in the end. Now it’s time to put it to use and dog food, while also building in public. Not sure how often I can keep up this updates, writing isn’t really my strongest suit, just trying to make it a habit.</p>]]></content><author><name>Eduard</name></author><category term="meta" /><summary type="html"><![CDATA[After two months of building Fabritorio, I finally “launched” the project. By launch of course I mean setting the repo visibility from “private” -&gt; “public”, posting in r/LocalLLaMA and calling it a day.]]></summary></entry><entry><title type="html">Qwen what do I eat?</title><link href="https://fabritorio.dev/blog/qwen-what-do-i-eat/" rel="alternate" type="text/html" title="Qwen what do I eat?" /><published>2026-06-13T00:00:00+00:00</published><updated>2026-06-13T00:00:00+00:00</updated><id>https://fabritorio.dev/blog/qwen-what-do-i-eat</id><content type="html" xml:base="https://fabritorio.dev/blog/qwen-what-do-i-eat/"><![CDATA[<p>If you are like me (and my partner), then there’s a good chance you often struggle to choose what to cook. Every grocery run becomes a “quick one”. Doing a real one once we’ve planned what to get more thoroughly. This is one of the things I’ve sort of automated with <strong>OpenClaw</strong> before.</p>

<p>I’ve kept a “recipe book” in Notion and it will choose and generate a meal plan based on the recipes, we’ll have a back and forth until I’m satisfied with what it chose, then it will generate a grocery list grouped according to each section.</p>

<p>The plan is to port this to <a href="https://github.com/fabritorio/fabritorio">Fabritorio</a>.</p>

<h2 id="help-me-claude">Help me Claude</h2>
<p>I already had tried making a Notion CLI with one of the pre-defined agents, <a href="https://github.com/fabritorio/fabritorio/blob/main/docs/recipes.md#add-a-new-tool">the tool builder</a>. However I think it was too much even for <code class="language-plaintext highlighter-rouge">qwen3.6:27b</code> and it keeps failing the exact shape of fetching Notion pages, grabbing every block (and subblocks) and leading to context OOM.</p>

<p>That meant reaching out for <strong>Claude Code</strong> so that I can iteratively build on the CLI without thinking of the overhead of doing it inside Fabritorio. We’ll get there eventually (hopefully). While at it I tried to make a generic skill so that I can effectively build run time tools with external harnesses.</p>

<details>
  <summary>The Fabritorio tool builder skill for any harness, if you want to yoink</summary>

  <div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">---</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">fabritorio-tool-builder</span>
<span class="na">description</span><span class="pi">:</span> <span class="s">Build or extend a Fabritorio runtime tool — a single CLI binary plus a manifest.json that the runtime discovers by scanning a tools directory — from any coding harness (Claude Code, pi, a shell, or by hand). Use this whenever someone wants to create, build, wrap, scaffold, or extend a CLI, tool, or integration for Fabritorio</span><span class="err">:</span> <span class="s">e.g. "build a Linear CLI for Fabritorio", "wrap this REST API as a tool", "add a Notion tool", "turn this script into a runtime tool", "extend the existing X tool", or anything that involves writing a manifest.json for a Fabritorio tool — even when the phrase "Fabritorio runtime tool" isn't used explicitly. Covers the Go-default static binary, the bash_cli manifest contract, credential/env-var wiring, smoke-testing, and updating tools in place.</span>
<span class="nn">---</span>

<span class="gh"># fabritorio-tool-builder</span>

Build a <span class="gs">**Fabritorio runtime tool**</span>: a CLI that any Fabritorio agent can wire as a <span class="sb">`tool`</span> node and call natively — gated by permissions, not routed through <span class="sb">`bash`</span>.

The key fact that makes this skill harness-neutral: <span class="gs">**the integration boundary is files on disk, nothing else.**</span> There is no SDK to call and no registration step. The runtime discovers a tool by scanning a directory for an executable binary next to a <span class="sb">`manifest.json`</span>. So whatever can write those two files — this agent, Claude Code, pi, a CI job, or you with an editor and <span class="sb">`go build`</span> — produces a first-class tool. The harness is interchangeable; the manifest is the contract.

<span class="gs">**The manifest is the product, not the binary.**</span> A binary with no manifest is invisible: the registry won't surface it, agents won't find it in the catalog, the <span class="sb">`tool`</span> node won't resolve. The Go code is the easy part — the judgment is in the manifest. Its full schema, field semantics, and a worked example live in <span class="gs">**`references/manifest.md`**</span>; read that file when you're about to write the manifest (step 5 below), not before.

<span class="gu">## Where things live — the one Fabritorio-specific path</span>

The runtime scans a <span class="gs">**tool scan root**</span> and projects each conforming manifest into the agent tool catalog. The root is:
<span class="p">
-</span> <span class="sb">`~/.fabritorio/tools/`</span> by default, <span class="gs">**or**</span>
<span class="p">-</span> the first colon-separated entry of <span class="sb">`$FABRITORIO_TOOL_ROOTS`</span>, if set.

Call it <span class="sb">`$TOOL_ROOT`</span>. Everything you produce lands at <span class="sb">`$TOOL_ROOT/&lt;name&gt;/`</span>. The registry rescans on every tool-catalog read (and on a tool-lookup miss during an agent build), so a freshly written tool appears without restarting the runtime.

This is the <span class="ge">*only*</span> path tied to Fabritorio. Use ordinary absolute paths in your harness's file tools — there's no workspace gate or tilde-expansion quirk to design around. (Those were properties of one specific in-runtime agent, not of the contract.)

<span class="gu">## The deliverable</span>

Three artifacts under <span class="sb">`$TOOL_ROOT/&lt;name&gt;/`</span>:
<span class="p">
1.</span> <span class="gs">**Source**</span> — <span class="sb">`go.mod`</span>, <span class="sb">`main.go`</span>, any helpers. Builds cleanly with <span class="sb">`go build ./...`</span>. No CGo or platform shims unless the integration genuinely needs them.
<span class="p">2.</span> <span class="gs">**Binary**</span> — <span class="sb">`bin/&lt;name&gt;`</span>. One static file, <span class="gs">**executable bit set**</span> (<span class="sb">`chmod +x`</span>). The registry checks the exec bit and silently skips anything without it, so a tool that doesn't appear after a rebuild is almost always a permissions problem — check <span class="sb">`ls -la bin/&lt;name&gt;`</span> first.
<span class="p">3.</span> <span class="gs">**Manifest**</span> — <span class="sb">`manifest.json`</span>. Shape and semantics in <span class="sb">`references/manifest.md`</span>. Its <span class="sb">`name`</span> field must equal the directory name and the binary name.

A behavioural <span class="sb">`SKILL.md`</span> alongside is optional — ship it only when there's <span class="ge">*how to think about this tool*</span> content the manifest can't carry (multi-step workflows, "reach for this vs that", domain pitfalls). For a one-verb CLI the manifest is enough; a SKILL.md would just restate it.

If any required artifact is missing, the task isn't done. Report partial state honestly ("source builds, smoke test fails on auth — needs <span class="sb">`LINEAR_API_KEY`</span>") rather than claiming success — the caller trusts your report literally.

<span class="gu">## Clarify before building only when you genuinely can't default</span>

Gate on complexity. A trivial single-verb, obvious-return CLI ("scrape this URL, return the text") → just build it. Multi-verb, ambiguous return, or unclear auth → ask once, in one batch:
<span class="p">
1.</span> <span class="gs">**Verbs**</span> — what distinct operations? Each becomes its <span class="ge">*own*</span> tool (<span class="sb">`issue_list`</span>, <span class="sb">`issue_create`</span>), not one <span class="sb">`linear`</span> with subcommands. Per-verb tools let the agent wire only what it needs and gate permissions per-verb.
<span class="p">2.</span> <span class="gs">**Return shape**</span> — per verb, what comes back and as what (JSON array, single object, plain text)?
<span class="p">3.</span> <span class="gs">**Auth — pin the env var name.**</span> This is the one question worth asking even when everything else is obvious. If the integration authenticates at all, the binary reads its credential from one named env var (see Credentials). That name is a contract between your manifest and whatever supplies the secret, so confirm it up front rather than inventing one: propose the conventional <span class="sb">`&lt;UPPERCASE_TOOL&gt;_API_KEY`</span> / <span class="sb">`_TOKEN`</span> and let the requester correct it. A later rename touches the manifest <span class="ge">*and*</span> the user's secret binding.

Don't ask about things this skill already settles (language, layout, manifest mechanics).

<span class="gu">## Language: Go by default</span>

Default to <span class="gs">**Go**</span>, for reasons that matter to the substrate rather than taste:
<span class="p">
-</span> <span class="ge">*One static binary*</span> — no <span class="sb">`pip install`</span>, venv, or <span class="sb">`node_modules`</span>. The tool dir is self-contained: copy it, run it.
<span class="p">-</span> <span class="ge">*Fast cold start*</span> — agents invoke the CLI many times per session; Go subprocess startup is ~5 ms vs Python's ~100 ms+ with imports. It compounds.
<span class="p">-</span> <span class="ge">*Stdlib covers the 80% case*</span> — <span class="sb">`net/http`</span> + <span class="sb">`encoding/json`</span> handle most REST integrations with zero external deps.

Reach for <span class="gs">**Python only**</span> when the integration ships a Python-only SDK that would cost more to reimplement than to wrap. Then isolate it in a <span class="sb">`.venv/`</span> inside the tool dir, make <span class="sb">`bin/&lt;name&gt;`</span> a shim that activates the venv and execs the entrypoint, pin <span class="sb">`requirements.txt`</span>, and say so in any SKILL.md. <span class="gs">**Never Node**</span> (same dependency problems, no offsetting win). <span class="gs">**Never a shell script for anything non-trivial**</span> (quoting bugs, no real JSON handling, untestable).

<span class="gu">## Build sequence</span>

Plain shell — run it however your harness runs commands.

<span class="p">```</span><span class="nl">bash
</span><span class="c"># 1. Init (first time only)</span>
<span class="nb">mkdir</span> <span class="nt">-p</span> <span class="s2">"</span><span class="nv">$TOOL_ROOT</span><span class="s2">/&lt;name&gt;/bin"</span>
<span class="nb">cd</span> <span class="s2">"</span><span class="nv">$TOOL_ROOT</span><span class="s2">/&lt;name&gt;"</span>
go mod init fabritorio/tool/&lt;name&gt;

<span class="c"># 2. Write main.go (+ helpers) with your harness's file tool.</span>

<span class="c"># 3. Build + make executable</span>
go build <span class="nt">-o</span> bin/&lt;name&gt; ./...
<span class="nb">chmod</span> +x bin/&lt;name&gt;

<span class="c"># 4. Smoke test — at least --help; ideally a dry run that loads config</span>
<span class="c">#    without a network call.</span>
./bin/&lt;name&gt; <span class="nt">--help</span>

<span class="c"># 5. Write manifest.json — read references/manifest.md first.</span>
</code></pre></div>  </div>

  <p>Fix any <code class="language-plaintext highlighter-rouge">go build</code> failure before moving on; don’t ship a broken binary with a TODO in the manifest. The smoke test exists to catch the binary that compiles but panics on first invocation (nil global, broken flag parsing) — <code class="language-plaintext highlighter-rouge">--help</code> is the cheapest exerciser.</p>

  <p>For authenticated tools: when the credential is missing, exit cleanly with a one-line stderr message naming the env var. The adapter returns combined stdout+stderr as the tool result, so <code class="language-plaintext highlighter-rouge">linear: LINEAR_API_KEY not set</code> is actionable where a panic is not.</p>

  <h2 id="credentials-read-one-named-env-var">Credentials: read one named env var</h2>

  <p>Design every authenticated tool around <code class="language-plaintext highlighter-rouge">os.Getenv("&lt;NAME&gt;")</code> — a single named variable per credential.</p>

  <p>In the Fabritorio runtime that variable is supplied by a wired <strong>Secrets node</strong>: the user keeps real values in <code class="language-plaintext highlighter-rouge">~/.fabritorio/secrets.env</code>, binds the env-var name to a key there, and wires <code class="language-plaintext highlighter-rouge">secrets → tool</code>. The wire is the grant — no wire, no credential — and the <code class="language-plaintext highlighter-rouge">bash_cli</code> adapter injects exactly that named subset onto the binary’s spawn env. If you’re building from a <em>different</em> harness, that wiring is the target runtime’s job, not yours: you only need the binary to read the named variable. Either way, don’t tell the user to <code class="language-plaintext highlighter-rouge">export</code> it in a shell or push it into <code class="language-plaintext highlighter-rouge">process.env</code>.</p>

  <p>The env-var name is the contract — pin it during clarification, state it in the manifest <code class="language-plaintext highlighter-rouge">description</code>, and repeat it in your report-back. Rules:</p>

  <ul>
    <li><strong>Never</strong> hard-code a key in source or embed one in an example; use placeholders.</li>
    <li><strong>Always</strong> read from one named var, conventionally <code class="language-plaintext highlighter-rouge">&lt;UPPERCASE_TOOL&gt;_API_KEY</code> or <code class="language-plaintext highlighter-rouge">_TOKEN</code>.</li>
    <li>On a missing credential, exit <code class="language-plaintext highlighter-rouge">2</code> with <code class="language-plaintext highlighter-rouge">&lt;cli&gt;: FOO_API_KEY not set</code> on stderr.</li>
    <li><strong>Don’t</strong> read a config file in <code class="language-plaintext highlighter-rouge">~/.config/&lt;tool&gt;/</code>; single-source via the env var keeps tools uniform.</li>
  </ul>

  <p>OAuth-only integrations with no personal-token path are out of scope — surface that rather than attempting a browser-callback dance.</p>

  <h2 id="extending-an-existing-tool">Extending an existing tool</h2>

  <p>Don’t rewrite from scratch. List the tool dir, read <code class="language-plaintext highlighter-rouge">main.go</code> and the current <code class="language-plaintext highlighter-rouge">manifest.json</code>, make targeted edits, then rebuild, re-<code class="language-plaintext highlighter-rouge">chmod +x</code>, and re-smoke-test. <strong>Update the manifest</strong> if you changed any flag, the parameter schema, the binary path, or the timeout — a stale manifest is worse than a missing one, because the model trusts the projected schema and will pass args the binary no longer accepts (silent miswiring instead of a clean error).</p>

  <p>Renaming is expensive: the <code class="language-plaintext highlighter-rouge">name</code> in the manifest, the binary, and the directory must change together, and every graph with a <code class="language-plaintext highlighter-rouge">tool</code> node on the old name breaks at handler-build time. If you must rename, leave a placeholder dir + manifest under the old name whose binary exits <code class="language-plaintext highlighter-rouge">1</code> with <code class="language-plaintext highlighter-rouge">renamed to &lt;new_name&gt;</code> on stderr.</p>

  <h2 id="report-back">Report back</h2>

  <p>Keep it factual and short — the caller folds it into a user-facing reply:</p>

  <ul>
    <li>Tool <code class="language-plaintext highlighter-rouge">name</code> (what the <code class="language-plaintext highlighter-rouge">tool</code> node wires).</li>
    <li>Binary path and manifest path (absolute).</li>
    <li>SKILL.md path, if shipped, plus one line on why.</li>
    <li>Credential env-var name(s), if any — named explicitly so the user knows which secret to supply.</li>
    <li>Smoke-test result — a one-line summary of <code class="language-plaintext highlighter-rouge">--help</code>, or the first error if it failed.</li>
  </ul>

  <p>Don’t editorialize or restate the brief. If the build failed, say so plainly with what you tried and what’s blocking; don’t retry more than ~3 times before surfacing the blocker.
```</p>

</details>

<p>After a bit of iteration I was able to quickly fetch the intended Notion page. The idea was a standalone tool <code class="language-plaintext highlighter-rouge">recipe_list</code> to get the list of recipes with just a title and a short description and then a <code class="language-plaintext highlighter-rouge">recipe_get</code> for the entire body. Only issue is my recipe book is formatted inconsistently, yikes.</p>

<figure>
  <img src="/assets/blog/recipe-agent/recipe_book.png" alt="inconsistent formatting recipes" />
  <figcaption>Recipes without details</figcaption>
</figure>

<h2 id="side-quest">Side quest</h2>
<p>Obviously this can’t be. The entire automation hinges on the simple fact that all data should be of the same format. /s</p>

<p>It is what it is. I over-engineered again and made an additional <code class="language-plaintext highlighter-rouge">recipe_update</code> tool. But with that all my tools are complete to finally create an “agent”. Should be a simple one, its main job is to take an incomplete recipe and fill in the missing details from the web.</p>

<h2 id="configuration">Configuration</h2>
<p>I created a new graph and landed on a single native agent. The first thing is configuring the model, so I drilled down to the default native agent and changed from <code class="language-plaintext highlighter-rouge">gemma4:31b</code> to <code class="language-plaintext highlighter-rouge">qwen3.6:27b</code> and also pointed it to my Ubuntu machine running my <em>llama server</em>. The model choice isn’t that big of a deal for this task, it can probably even be a smaller model.</p>

<p>To give the agent functionality, we can start by dragging a <strong>Tool Node</strong>, and selecting the <code class="language-plaintext highlighter-rouge">recipe_list</code> in the inspector. I would also need the <code class="language-plaintext highlighter-rouge">recipe_get</code> and <code class="language-plaintext highlighter-rouge">recipe_update</code> so I just copy pasted the existing tool node. All of these tools would need the <code class="language-plaintext highlighter-rouge">NOTION_API_KEY</code> so I dragged the <strong>Secret Node</strong> and fill it in with the var name (note, not the value!) from my secrets env.</p>

<figure class="wide">
  <a href="/assets/blog/recipe-agent/agent1.png" target="_blank" rel="noopener">
    <img src="/assets/blog/recipe-agent/agent1.png" alt="Three tool nodes and a secret node wired into one native agent" />
  </a>
  <figcaption>Three tool nodes — <code>recipe_list</code>, <code>recipe_get</code>, <code>recipe_update</code> — plus a Secret node for the Notion key, all wired into one native agent. Click to view full size.</figcaption>
</figure>

<p>Quick test to see if both tools are accessible. Dragging the <strong>Debug gateway</strong> allows for ephemeral chatting with the agent. After confirming the secret is working, I then attached two additional tools, <code class="language-plaintext highlighter-rouge">web_fetch</code> and <code class="language-plaintext highlighter-rouge">web_search</code>, as well as a Secret Node for the Brave API key.</p>

<p>Lastly I created a small skill and made use of the <strong>Skill Node</strong> to teach the agent how to normalize a recipe. The gist is to use fetch and grab the “complete” metadata, then get the first one, and standardize into five-line format (timing, macros, description, metric ingredients, steps), one recipe at a time.</p>

<figure class="wide">
  <a href="/assets/blog/recipe-agent/agent2.png" target="_blank" rel="noopener">
    <img src="/assets/blog/recipe-agent/agent2.png" alt="Debug gateway, web tools, and a skill node added to the agent" />
  </a>
  <figcaption>A Debug gateway for ephemeral chat, <code>web_fetch</code> + <code>web_search</code> (with a Brave-key Secret node), and a Skill node teaching the normalize step.</figcaption>
</figure>

<p>Now to run this. What I did is simply attached a <strong>Trigger Node (Schedule)</strong> and set it to run every 5 min with the prompt:</p>
<blockquote>
  <p>Fetch the recipe list and normalize the first one that is not complete, don’t do a dry run and just apply your changes</p>
</blockquote>

<figure class="wide">
  <a href="/assets/blog/recipe-agent/agent3.png" target="_blank" rel="noopener">
    <img src="/assets/blog/recipe-agent/agent3.png" alt="A schedule trigger node wired into the agent" />
  </a>
  <figcaption>A Schedule trigger fires the whole graph every five minutes with the normalize prompt.</figcaption>
</figure>

<p>I let it rip and went about my day. And then voilà, recipes are now looking clean!
<img src="/assets/blog/recipe-agent/agent4.png" alt="Trigger runs" /></p>

<figure>
  <video autoplay="" muted="" loop="" playsinline="" poster="/assets/blog/recipe-agent/agent4.png" aria-label="The recipe book updating itself — pages going from inconsistent to a uniform five-line format.">
    <source src="/assets/blog/recipe-agent/uniform_format.webm" type="video/webm" />
    <source src="/assets/blog/recipe-agent/uniform_format.mp4" type="video/mp4" />
  </video>
  <figcaption>Recipes normalizing themselves, one every five minutes.</figcaption>
</figure>

<h2 id="uniform-format-is-good-format">Uniform format is good format</h2>

<p>Although I got sidetracked and didn’t accomplish the original goal of the full end meal planner agent, I think it was still a win to at least standardized the format of the recipe list. Of course I would still need to double check and verify the details to make sure it was the intended recipe before, but at least it would be easier to edit than to start from scratch. Dogfooding this automation actually led to the project’s <a href="https://github.com/fabritorio/fabritorio/pull/1">very first PR</a>. After realizing that most pages were blocking the <code class="language-plaintext highlighter-rouge">web_fetch</code> tool I sparred with Claude to improve it a bit, of course it still doesn’t pass JS challenge or even a TLS handshake check, but it’s low enough friction that it’s basically free to add.</p>

<p>Overall I enjoyed building this one, definitely liking how visual the project is. That and how “easy” it is to set up agents. The initial set up of building tools is still the friction but one step at a time.</p>]]></content><author><name>Eduard</name></author><category term="meta" /><summary type="html"><![CDATA[If you are like me (and my partner), then there’s a good chance you often struggle to choose what to cook. Every grocery run becomes a “quick one”. Doing a real one once we’ve planned what to get more thoroughly. This is one of the things I’ve sort of automated with OpenClaw before.]]></summary></entry></feed>