# Massimo De Luisa > CTO and product engineer. I build platforms, mobile apps, and AI-assisted workflows that stay simple under pressure. Shipping Inksquad, isready.ai, SIDUS, Images in motion, and Open Bio Page. Experimenting with Rust. Based in Udine, Italy, moving to Japan. --- # Journal ## Open Bio Page: a links page on your own domain _2026-10-07_ Why pay $10 or $30 a month for a basic, responsive links page with stats, when you can have it for free? On 7 October 2026 I published [Open Bio Page](https://openbio.page/) as my answer to that question. It is an open-source, customizable links page that you put on a domain you already have. A visitor opens the page, picks a person, and gets that person's links. Each person gets a page with a portrait, a line about them, the links they want to hand out, and how they want to be presented. You can change all of it. That is why the same project can be a family links page or a company page. ## One link under the bio Instagram will not put a link in the caption of a post, so the profile gets one website slot. A whole habit grew up around that slot, which people open on a phone. [Linktree](https://linktr.ee/features/link-in-bio) is the page people rent for it. In Linktree's own description, you sign up, add the shop, the newsletter, and the other profiles, then copy an address that is linktr.ee followed by your name. The free page can take a theme, colors, and a font, while a domain of your own is on the paid plan. Linktree says more than seventy million people use it. ## The same page on a domain you control A links page exists because the platform gave you one slot, yet the rented one still lives on someone else's domain. Your own name in the address is on the paid plan. Open Bio Page is that same list of links, on a domain you already control. Visits, which link was opened, and where people came from show in the admin once you add a PostHog key. With no key, those figures stay at zero. There is no monthly charge for the page. ## Files instead of an account Open Bio Page keeps its content in files. Each person is a file in the project, while the brand, the domain, and the words on the home live in a second file. The build turns those files into one HTML page per person, each with its own title, so a crawler that does not run JavaScript still gets the page. GitHub Pages then serves those pages from your domain. If you want a login later, each person's login saves only that person's file. Visitors never need one. ## What you can change ![A person's page: a portrait, a short line, and a few links.](/journal/2026-10-07/giulia-ferraresi.webp){.img-right} The picture here is one person's page, while the picture at the top of this article is one home, where a visitor picks a person. Both are built from parts you can change, starting with the name of the directory and who appears on it. For each person you can change the portrait, the short line in their own words (in English and Italian, if you want both), and the links, including which one should be opened first. You can also change the type, the colors, and whether a page is one column or two. A family directory and a company page are the same files with different names in them. ## It started as a family page I made the first one for my family. Open Bio Page is that page with the names taken out, so someone else can put their own in. The docs include a few examples, a family and a few small teams, so you can see the range before you change anything. ## Publishing it with an agent If you want a copy published for you, the [docs home](https://openbio.page/) has a prompt for that. You choose Family or personal, or Company or team, then copy the prompt or open it in Claude Code, Codex, Cursor, Claude, or ChatGPT. The agent asks once for your domain and for each person, then publishes the site, keeping the wording you give it. If you have no domain, the agent stops. The same steps are written out at [Create your bio](https://openbio.page/create), for when you would rather do them yourself. The code is on [GitHub](https://github.com/open-bio-page/open-bio-page). ## How I cracked Firecrawl Cheat Code _2026-09-10_ Firecrawl published [CheetCode v3](https://ctf.firecrawl.dev) as a hiring test for people they call agent orchestrators. The test is sixty tasks in 240 seconds, with your GitHub account as your row on the leaderboard. I ran it twice; the first session was on 29 August 2026. On the studio account, [Smart Squad](https://www.smartsquad.io), the board shows 60/60, Elo 3850, zero retries, and rank 1. On my personal GitHub, [massimodeluisa](https://github.com/massimodeluisa), I also solved sixty of sixty and filled Elo, but forty-three retries already sat on that row, which put me third, then fifth when cleaner accounts arrived. The card at the top is the studio profile when we were done. I had a good time with it. A language model left alone misses the landmines in the problem text. It does not notice that Interstellar Fuel wants 300 and not the true minimum of 200. Nor does it recover the turning convention Firecrawl had stripped from Bio Trip. Those catches came from sitting with a dump, a Kattis page, and a suspicion. The clocks add up to the number on the homepage (sixty seconds, sixty again, two minutes). Each retry also lowers a multiplier on the account. I wanted something that could read a board, write code, check it locally, and submit while still telling me why a run had died. ## Three levels in four minutes The first level, Orchestrate, gives you twenty-five JavaScript puzzles in sixty seconds. Easy cards are arithmetic in costume (basketball scores, paint mixers, fuel per kilometer), while medium and hard cards are real algorithms, with two sample tests on the card and more hidden on the server. Some of those are ICPC problems from [Kattis](https://open.kattis.com), North America East. The function name on the card is the Kattis slug, so the official statement is one search away. Explore, the second level, asks ten questions in sixty seconds, each of which requires you to read a source. The sources I saw were Chromium, Firefox, LibreOffice, and Postgres. Guessing from training data misses. I fetched the document and answered from it, with a scrape in the loop. Explore appears only after twenty-five out of twenty-five on Orchestrate, which means a 19/25 finish stays partial. Build, the third level, is one system with twenty-five checks and two minutes on the clock, in which you have to stand the system up and show that it works. Some Orchestrate cards also hide *landmines*, prompt-injection in the problem text such as a `[SYSTEM]` line, a token like `lm_…`, or a comment such as `@provenance-…`. If the code you submit echoes one of them, the grader takes two hundred points, which is the value of five correct cards. The speed bonus is small by comparison, about a third of a point per leftover second. Landmines cost more than they look. I learned that on an early run, where seventeen solved cards with two injections scored two hundred and ninety-one points and a cleaner twelve-card run scored higher. After that I got the landmines right first. ## How the board ranks you The leaderboard sorts on Elo, then on total retries when two people share the same Elo. The `score` on a finish payload belongs to the round, while Elo belongs to the account and caps at 3850. On paper the clock gives you four seconds a task, although in practice the first level gives you sixty seconds for twenty-five cards. ![CheetCode v3 global leaderboard with massimodeluisa in third, 60 of 60 solved, 43 retries](/journal/2026-09-10/cheetcode-board-personal.jpg){.img-left} This is [ctf.firecrawl.dev](https://ctf.firecrawl.dev) the night before the studio run. I am third as `@massimodeluisa`, sixty out of sixty, Elo 3850, with forty-three retries already spent on the personal account. Then the board filled with people who had a second GitHub account: one identity ate the retries and learned the pool, while the other kept a clean multiplier and posted the score. I ran it again as the studio. That second run was three finishes, one per level, with nothing extra. Orchestrate came back 25/25 in 225 milliseconds, Explore 10/10 in twelve milliseconds, and Build 25/25 with 119 seconds still on the clock. Elo went 1270, then 2320, then 3850, with zero retries and rank 1. ![CheetCode v3 after the Smart Squad run: smartsquad-team first with zero retries, massimodeluisa still on the board](/journal/2026-09-10/cheetcode-board-squad.jpg){.img-right} This is the same board after the studio pass, with `smartsquad-team` signed in and first, zero retries, Elo 3850. The personal row is still there in fifth place, with the forty-three retries still attached to it. Both rows have sixty of sixty and Elo 3850, so the tiebreak on retries is what separates them. ## Two models and a local Firecrawl [Grok](https://x.ai), running as Grok Build in the terminal, wrote and ran the orchestrator. [Claude](https://claude.ai) sat on the same dumps and argued about the scoring formula, about whether a timeout was a wrong answer, and about whether a template that passed two samples would survive the hidden tests. I wanted two models that did not share a context window, so that a mistake in one place had a chance of getting caught in the other. Grok is where the code moved. It sits in the repo, runs `cargo test`, starts Docker, and has the Firecrawl MCP on the same machine. Claude is where I sent a dump and a suspicion when I wanted a second pair of eyes that had not just written the function. The two disagreed on whether a Node timeout should empty a template, on whether leftover seconds should be burned on a second LLM pass, and on whether a 19/25 scoreboard tweet meant the level was cleared. I kept the disagreements that came with numbers. Firecrawl itself ran in Docker on this machine, bound to `127.0.0.1:3002`, rather than on Firecrawl Cloud. Firecrawl belonged on localhost because the product under test is Firecrawl. I also did not want the cloud quota or their hosted extract in the inner loop. The self-hosted image has scrape and search but does not have `/interact`, which was enough for Orchestrate, because the contest statements live on [Kattis](https://open.kattis.com) and a scrape returns a document. When a card was a Kattis problem with a truncated statement, I scraped the official page through that local instance and got the input format the dump had cut off. ## A small Rust orchestrator The orchestrator is a small Rust program. It signs in with GitHub, pulls the twenty-five cards, and tries them in order of cost. Deterministic heuristics go first, for the word-problem arithmetic, followed by a bank of JavaScript solvers keyed on the function name, which is the Kattis slug when the card is a contest problem. Whatever is left goes to Vercel AI Gateway with `openai/gpt-oss-120b`, in batches of two, with Groq then Cerebras pinned for latency. Every candidate runs in Node against the two samples on the card. Failures are dropped so that a known-bad function never reaches `finish`. HTTP stays in RAM until the timer ends. Then the session is dumped with its cards, code, traces, and the finish payload. ## Replaying boards to save retries That dump is what Claude and Grok read, which mattered because a live play costs a retry. We replayed saved boards offline until the competitive cards on those boards were hits. Only then did I authorize one real session. Direct templates take milliseconds, while the language model eats the rest of the minute. A retry multiplier ticks down about seven thousandths per session. I treated that as a real cost, even though the `score` field on a finish payload is still `40 × solved + speed + trickery` with no multiplier applied. I still do not know where the multiplier lands. I do not want to find out by pressing play to debug a heading function. ## Where it broke The Kattis cards are the ones a 120B model will not finish in three seconds. [Pearls](https://open.kattis.com/problems/pearls) is a Masyu loop. [Walk in the Woods](https://open.kattis.com/problems/walkinthewoods) is an orthogonal graph with interest capacities. A naive simulation at `n = 2500` is millions of steps, so the template jumps whole cycles while the topology is fixed. [Balancing Art](https://open.kattis.com/problems/balancingart) is a binary search on a flow (Dinic), which fitting a formula to two samples would miss. [Fences Make Good Neighbors](https://open.kattis.com/problems/fencesmakegoodneighbors) is a pentagon fan plus a minimum triangulation of the pockets. [Hilbert's Hedge Maze](https://open.kattis.com/problems/hilbertshedgemaze) and [Stable Table](https://open.kattis.com/problems/stabletable) passed the samples and then returned `TIMEOUT` on the hidden tests. The samples are tiny and the Kattis limits are not (`n ≤ 50`, a 100×100 block of pieces), so a template can look correct locally and still run out of time on the server. The grader also does not always want the mathematical minimum. One medium card, Interstellar Fuel Optimization, has samples that reject the global minimum. Filling the tank at price zero and carrying leftover fuel costs 200, but the samples want 300. That 300 is the textbook hop DP, where from station `i` to `j` you pay `price[i] × (d[j] − d[i])`, jump no longer than capacity, and arrive empty. An LLM that "repairs" toward the true optimum will converge on 200 and fail forever, so I stopped sending that class of card to the model and shipped the recurrence the samples encode. The landmine scrub had to be careful as well. A line-based filter deleted a `[SYSTEM]` sentence together with the real spec on the same line. A token scan by byte index panicked on a curly apostrophe (`U+2019`) in a Fences statement. Either of those would have taken down a live run. Comments are stripped from generated JS. Arguments are cloned before Node runs them, because a candidate that does `distances.push(target)` otherwise poisons the repair prompt with an input the card never had. Repair is two rounds that run only on cards that are not templates, fed with the grader's hidden-test line when we have it. A local `2/2 pass` is the wrong signal. The model is told to write vanilla JavaScript only, with no `MinPriorityQueue` and no LeetCode globals. Temperature starts low and goes up on the second round, so the second try is not a copy of the first. [Bio Trip](https://open.kattis.com/problems/biotrip), a tractor-and-junction Kattis problem, failed a live hidden test after the samples were green. The local Firecrawl scrape of the Kattis statement recovered the turning convention (`α1` left, `α2` right) and a U-turn case the CTF board had replaced. The code treated `norm(180)` as a left turn only, so a legal map with `α2 = 180` and `α1 < 180` returned `impossible`. It is one line that costs four seconds if you find it on the clock. ## What a person still had to catch A language model on its own does not uncover the landmines, the sample that wants a worse number than the optimum, or the 180-degree turn that only exists on the real Kattis page. Those needed a person looking at a dump and a statement. On the studio account I posted rank 1, sixty of sixty, with no retries. On the GitHub account that is mine, I finished third, then fifth, because of the forty-three retries already attached to the row. ## Images in motion: a mosaic that scrolls on CSS _2026-09-04_ The picture at the top of this post is the live mosaic on iim.smartsquad.io. Its tilted columns scroll in opposite directions, each at a speed of its own, so the pictures keep sliding past one another and never settle into rows. I published [images-in-motion](https://iim.smartsquad.io/) on 3 September 2026 as a package for putting that mosaic on your own page. It is on npm under Smart Squad, with a studio that copies your settings and leaves the pictures behind. ## How the columns move The effect comes from a few rules that apply to each column. Each column leans and scrolls the opposite way from the column beside it. The speed stays constant inside one column but differs from the next. A fixed offset keeps the rows from lining up, so the pattern does not freeze into a grid. The default tilt is 12 degrees, with a default speed of 8 to 18 logical pixels a second. The studio at the end of this note changes both, along with the canvas and the tiles. You can also flip the axis and scroll rows instead of columns. ## Why the motion lives in CSS I wanted the pictures to keep moving when the JavaScript framework is idle, which decided where the motion lives. A mosaic that depends on an animation loop spends the main thread on decoration and janks the rest of the page. So the motion is CSS: geometry is computed once, the renderer hands each column an animation, and the browser keeps those transforms moving. Two details shape the loop itself. Odd columns run in reverse, so neighbours never march together. Each column holds two copies of its images, with the last gap counted as part of the loop, so the wrap from the end of the loop back to its start does not jump. The mosaic stops in two ways. Pause eases for about half a second and then holds. If the user has asked for reduced motion, or the tab is hidden, the animation snaps instead of fighting them. ## Putting it on a page The package installs from npm: ```bash bun add images-in-motion ``` After the install, give the host a size. The docs fill the available box and size the mosaic to 20rem by 20rem. React and Vue are optional: they create a host and then get out of the way without owning the animation. Expo and NativeScript put that same renderer in a WebView. If you want the markup that way, there is also a custom element, ``. ## What it weighs The CDN file is 17.38 KB minified (17,798 bytes), which compresses to 6.47 KB with gzip and 5.83 KB with brotli. It does not include React, Vue, or the images, so a wrapper and your pictures come on top of those figures. Those bytes were measured with `bun run size` on the repo and are quoted here exactly as measured, without rounding. ## The studio keeps your pictures local The [studio](https://iim.smartsquad.io/studio/) is where you set the canvas, the speed, the tilt, and the tiles. Copy writes those settings out, but image URLs never enter that payload, nor do files you picked from disk. Pictures stay in the browser that chose them, so you can share a configuration without sharing someone else's photos. The source is on GitHub at [github.com/smartsquad/images-in-motion](https://github.com/smartsquad/images-in-motion). ## Named glyphs, not empty boxes _2026-08-29_ When a coding agent writes a prompt, a TUI, a statusline, an editor config, or a web page that needs an icon, it will paste an emoji, an ASCII stand-in such as `E:` or `->`, or a codepoint it remembers, unless you stop it. Those icons live in Unicode's Private Use Area. Without a patched font they render as empty boxes, the thing terminal people call *tofu*. This journal self-hosts a tiny subset of the Symbols Only font, so the glyphs in this post should render as icons. If you still see empty boxes, the webfont did not load. [nerd-fonts](https://github.com/massimodeluisa/nerdfonts-skill) is the Agent Skill I published to make the agent pick a named glyph and write the class next to the codepoint. It also has to tell you which patched family to install, because the family string in `kitty.conf` is not the name on the download page. The skill is independent of Nerd Fonts. I released it on 28 August 2026 and wrote this note the next day. ## Where the glyphs come from The sheet the agent picks from is Ryan L McIntyre's [Nerd Fonts](https://www.nerdfonts.com) project. Its version matters here because the skill works from a snapshot of it. [v3.5.1](https://github.com/ryanoasis/nerd-fonts/releases/tag/v3.5.1), from 21 August 2026, has 10,995 named glyphs drawn from Devicons, Font Awesome and its extension, Material Design Icons, Weather, Octicons, Codicons, Font Logos, Pomicons, Powerline and Powerline Extra, IEC Power Symbols, and Seti-UI plus Custom. The same release ships 72 patched families and a Symbols Only font, plus a patcher for any other face. The patcher is MIT, while each original font keeps its own license. ## One named glyph, one set per surface With that sheet as the source, the first rule is that every icon the agent writes is a named Nerd Fonts glyph, whether it lands in CLI output, TUIs, prompts, statuslines, editor and terminal configs, docs, or web UI. It comes from the [cheat sheet](https://www.nerdfonts.com/cheat-sheet), from `data/glyphs.tsv` in the skill (the same `glyphnames.json` as upstream, snapshotted at v3.5.1), or from the live `glyphnames.json` if you need something newer than the snapshot. The agent does not invent a codepoint. It writes each glyph next to its name and codepoint. The second rule keeps one icon set per surface. Git chrome stays in Octicons (`nf-oct-`, 310 glyphs,  `nf-oct-mark_github` U+F408,  `nf-oct-git_branch` U+F418). Editor UI and LSP kinds stay in Codicons (`nf-cod-`, 540). Languages and file types stay in Devicons and Seti ( `nf-dev-rust` U+E7A8), while prompt separators stay in Powerline (`nf-pl-` / `nf-ple-`). General UI can use Material Design (`nf-md-`, 6,896, 󰉋 `nf-md-folder` U+F024B) or Font Awesome (`nf-fa-`, 1,818,  `nf-fa-heart` U+F004). In a source file, a glyph chosen this way sits next to its class and codepoint: ```js icon = "" // nf-oct-mark_github U+F408 ``` On a machine without the font, the string between the quotes is an empty box. The comment is what lets a reader check the choice against the sheet. ## A patched font changes its name Choosing the glyph solves half the problem, because a Nerd Font also has to be active on the surface that renders it. The skill sends you to [font-downloads](https://www.nerdfonts.com/font-downloads) with the package name, the patched family, and the variant. It has to name all three because Reserved Font Names rename on patch: Cascadia Code becomes `CaskaydiaCove Nerd Font`, Source Code Pro becomes `SauceCodePro Nerd Font`, and IBM Plex Mono becomes `BlexMono Nerd Font`. The full list lives in `references/fonts.md`. If you want to keep your current text font, the skill installs Symbols Only (`Symbols Nerd Font` / `Symbols Nerd Font Mono`) and lets the app fall back to it. Each app has its own mechanism for that. With fontconfig, you copy `10-nerd-font-symbols.conf` into `~/.config/fontconfig/conf.d/`. kitty uses `symbol_map` and WezTerm uses `font_with_fallback`, while iTerm2 has a "Use a different font for non-ASCII text" setting. Ghostty and WezTerm already bundle the symbols. Upstream documents fallback placement as hit or miss, which kitty can correct with `modify_font baseline` and WezTerm with a fallback `scale`. The variant decides how much room each icon takes. Where columns must line up, the Mono variant puts each icon in one cell. Regular is about 1.5 cells, while Propo is proportional, for GUI apps and slides. The variant changes only the added icons, never the base letters. CLIs that might run without a Nerd Font get `--no-icons` or `NO_ICONS=1`. Before anyone claims the chosen glyphs render, they are printed with `printf` and checked. ## Icons on a web page A browser has the same dependency on a font. On the web, the page loads `webfont.css` or self-hosts `SymbolsNerdFont-Regular.ttf` from `NerdFontsSymbolsOnly.zip` (self-host in production rather than hot-linking nerdfonts.com). This site does the second thing with a WOFF2 subset under `/fonts/nerd-fonts/`, wired as a fallback after Geist. Its `unicode-range` is limited to the Private Use Area, so the browser reaches for the subset only for icon characters. A Private Use Area character without a covering font is tofu in the browser too. Decorative icons get `aria-hidden="true"`, while icon-only controls get an accessible name. Upstream `.nf` sets `line-height: 1`. Start `vertical-align` at `-0.125em` and look at a mixed text-and-icon line, because an icon font sits on a different baseline than the text font. This line mixes a few with text so you can see the result on this page:  󰉋  . ## Finding a glyph without reading the whole sheet Lookup is deliberately a small window. The agent runs `grep -i 'folder' data/glyphs.tsv` or `awk -F'\t' '$1=="md-folder"' data/glyphs.tsv` against the snapshot, so the TSV is never dumped into context. Three reference files sit next to it. `references/common-icons.md` is a short list by purpose, backed by `data/common-icons.txt`. `references/icon-concepts.md` has twenty-two concepts, each in five sets (md, fa, oct, cod, seti), for when a surface must stay in one set. `references/glyph-sets.md` covers prefixes and ranges, plus extra language escapes. The names also carry history that a lookup has to respect. Material Design Icons moved in v3 from `nf-mdi-*` (U+F001 to U+F847) to `nf-md-*` (U+F0001 to U+F1AF0). Some names were dropped on the way, so a removed `nf-mdi-altimeter` is searched as the bare word `altimeter` on the cheat sheet. The folder you want now is 󰉋 `nf-md-folder` U+F024B. Powerline names lie about direction.  `nf-pl-left_hard_divider` U+E0B0 points right, while  `nf-pl-right_hard_divider` U+E0B2 points left, because the names describe the side of the cell the triangle sits on. The install step has a naming rule too. Homebrew casks are `font--nerd-font`, so `brew install --cask font-jetbrains-mono-nerd-font` is the JetBrains one. Dropping `--cask` or the hyphens asks for a formula that does not exist. The font is installed on the machine that runs the terminal, which means the remote host does not need it over ssh. kitty's family string is `font_family JetBrainsMono Nerd Font`. The verification line prints three named glyphs: ```bash printf '  󰉋\n' ``` From left to right, that is  `nf-custom-vim` U+E62B,  `nf-oct-mark_github` U+F408, and 󰉋 `nf-md-folder` U+F024B. `fc-list` is missing on stock macOS, so the skill uses `brew install fontconfig`, or `kitty +list-fonts`, or the `printf` alone. ## Reviewing an existing config The same rules apply when the agent reads a file somebody else wrote. Review mode is `/nerd-fonts ` against a script, a TUI source, a prompt config, an editor or terminal config, HTML, or CSS. For each finding it quotes the line and says why in one sentence. Then it gives the fix: the exact glyph with `nf--` and `U+XXXX`, or the exact config line. The checklist behind it uses `perl` (it ships on macOS and Linux) to find emoji and dingbats. `grep -n 'nf-mdi-'` catches leftover Material classes, while a PUA scan finds glyphs without their name and codepoint. A family-name scan catches unpatched faces (Cascadia Code, Source Code Pro, IBM Plex Mono, and the rest of that list). `grep -L` finds HTML or CSS that uses glyphs but loads neither `webfont.css` nor `@font-face`. Text markers in status lines (`E:`, `W:`, `[x]`, `->`) have no reliable regex, so the agent has to read the file. A deploy line that printed `ok` / `warning` / `error` should come back with something like 󰄬 `nf-md-check` U+F012C, 󰀨 `nf-md-alert_circle` U+F0028, and 󰅙 `nf-md-close_circle` U+F0159. All three come from one set, with comments from the TSV. ## Five scenarios as tests I test whether the agent actually does all of this with five scenarios in `evals/evals.json`. They are written as assertions against `data/glyphs.tsv` and do not run as CI that launches a model. The first is a macOS user on kitty and starship whose prompt icons are empty boxes. The skill has to link font-downloads, name the cask `font-jetbrains-mono-nerd-font`, and give the family `JetBrainsMono Nerd Font` with `font_family JetBrainsMono Nerd Font` in kitty.conf. It must also print a `printf` of named glyphs from the TSV and mention Mono when columns must line up. The answer also has to point at the starship nerd-font-symbols preset without inventing a codepoint. The second scenario tests the lookup rules directly. It asks for the glyph, hex, and class of five icons: the Octicons GitHub mark, the Font Awesome heart, the Devicons rust glyph, the Material Design folder, and the Powerline right-pointing solid separator. It also asks what happened to `nf-mdi-folder`. The gold answer is  `nf-oct-mark_github` U+F408,  `nf-fa-heart` U+F004,  `nf-dev-rust` U+E7A8, 󰉋 `nf-md-folder` U+F024B, and  `nf-pl-left_hard_divider` U+E0B0 (not  `right_hard_divider`). It has to explain the mdi-to-md move with bare names, without telling anyone to pin Nerd Fonts v2. The third scenario is Ubuntu 24.04 with Neovim and nvim-tree in a GNOME Terminal that shows tofu, for a user who wants to keep the text font. It expects Symbols Only, `10-nerd-font-symbols.conf`, `fc-cache`, and `printf`, with no replacement of the text face. The fourth is a `deploy-status.sh` that prints a git branch and five services with ok, warning, error, pending, or unknown. Here the expectations are named glyphs with comments from the TSV, one set per surface, no emoji, and `--no-icons` or `NO_ICONS=1`. The answer also needs a font-downloads note, Mono or a caveat about columns, and `printf` rather than `echo`. The fifth scenario reviews three files: a `starship.toml` full of emoji, a lualine snippet with `E:` / `W:` and `nf-mdi-folder`, and a `kitty.conf` that still says `font_family Cascadia Code`. Each finding has to quote the line. In the kitty finding, Cascadia becomes CaskaydiaCove, with the cask `font-caskaydia-cove-nerd-font`. Replacement glyphs come from the TSV, with no invented nvim-web-devicons APIs. ## Keeping up with upstream Because the skill works from a snapshot, the repository has to notice when upstream moves. A clone-only Rust helper, `tools/nfskill`, validates every glyph name and codepoint in this repository against the snapshot. It does not ship inside the skill directory, so the skill itself stays `grep` and `awk`. Weekly CI watches upstream, so a new Nerd Fonts release, or a Homebrew cask that stops resolving, opens an issue. When upstream ships, the update is to regenerate `glyphs.tsv`, recompute glyph-sets and fonts, update the tag in `SKILL.md`, write the changelog, and then run `cargo run --manifest-path tools/nfskill/Cargo.toml -- validate`. Install it with the skills CLI: ```bash npx skills add massimodeluisa/nerdfonts-skill ``` The skill is listed at [skills.sh/massimodeluisa/nerdfonts-skill](https://skills.sh/massimodeluisa/nerdfonts-skill). Adding `-g` to that command makes it a user-level install. After that, `/nerd-fonts` applies the policy to the conversation, while `/nerd-fonts ~/.config/starship.toml` reviews that file. Font downloads stay on [nerdfonts.com/font-downloads](https://www.nerdfonts.com/font-downloads); the cheat sheet stays on [nerdfonts.com/cheat-sheet](https://www.nerdfonts.com/cheat-sheet). If a class in the snapshot is wrong, [edit it on GitHub](https://github.com/massimodeluisa/nerdfonts-skill). ## Decompose the work, keep the window small _2026-08-28_ When you ask a coding agent to reason over a whole repository, a folder of documents, or a set of PDFs that have to be compared, the usual move is to load as much as possible into the window and hope the model still sees the details. Zhang, Kraska, and Khattab showed that this hope is misplaced. In *[Recursive Language Models](https://arxiv.org/abs/2512.24601)* (arXiv:2512.24601, December 2025) they report that quality falls as the prompt grows, even when the text still fits. They call that fall *context rot*, following Hong et al. 2025. Retrieval misses, details in a long document drop out, and distant passages get glued together, yet the answer still looks confident. Their fix treats the file as something the model looks through. The model peeks at the input, slices it, and calls itself over the snippets, so the active window stays small. [Recursive Decomposition](https://github.com/massimodeluisa/recursive-decomposition-skill) is the Agent Skill I published so that Claude Code, Codex, Cursor, and the other agents would do the same in an ordinary session. The skill sizes the input before anyone opens a file and then filters it with searches. What remains is split into independent batches of five to ten files. Each batch goes to a sub-agent that answers and is forbidden to spawn more sub-agents. The merge is checked against a small window of the original files before the write-up is assembled in code, with paths and line numbers. This note is from 28 August, when the protocol I first put out in January 2026 became an installable skill. ## What the paper measured ![Figure 1 from Zhang, Kraska, and Khattab: GPT-5 versus RLM(GPT-5) on S-NIAH, OOLONG, and OOLONG-Pairs as input grows from 8k to 1M tokens](/journal/2026-08-28/rlm-figure-1.png){.img-right} The paper is the literature the skill is built on. The place to start is Figure 1 and Table 1, because they put numbers on what "long context" costs. Figure 1, on the right, follows GPT-5 as the prompt grows from 8k to 1M tokens. GPT-5 holds on S-NIAH and then falls on OOLONG and OOLONG-Pairs. The red band marks the range past its 272k window. RLM at depth one stays up through that band. BrowseComp-Plus is a multi-hop question-answering task whose inputs run from about six to eleven million tokens, far past GPT-5's 272k window. GPT-5 as a base model hits the context limit and scores nothing useful. Compaction, the usual summarise-as-you-go scaffold, reaches 70.5 percent, while RLM at depth one reaches 91.3 percent at an average cost of $0.99, with a standard deviation of 1.22. That costs more than compaction's $0.57 and less than a linear extrapolation of stuffing six to eleven million tokens into GPT-5-mini, which the paper puts between $1.50 and $2.75. On that row the authors report the RLM beating compaction and retrieval by more than 29 percent. OOLONG is a linear aggregation task at 131k tokens. GPT-5 scores 44.0 on it, while RLM with GPT-5 at depth one scores 56.0 (a 28.4 percent relative gain). RLM with Qwen3-Coder picks up 33.3 percent over its own base. OOLONG-Pairs is the quadratic task, built on pairwise relations. It is only about 32k tokens long, well inside a million-token window. GPT-5 scores 0.1 F1 there, while RLM reaches 58.0 at depth one and 76.0 at depth three. The paper also reports what the recursion costs. In Figure 11 the median RLM cost sits comparable to or under the base model's. The average can rise because trajectories are long-tailed (Observation 4), while the 95th percentile of runtime is dominated by sequential sub-calls. I did not rerun these experiments; every number in this section is theirs. ## Why it fires before the window is full That OOLONG-Pairs row is the reason the skill does not wait for the input to overflow. The window size is a harness cap. Fitting inside it says nothing about the quality of the answer. Pairwise work, multi-hop work, and any job of the form "list everything and do not miss an item" rot inside a window that still has room, so the skill fires on dense work even when the files would have fitted. Small jobs need none of this. The skill stays out of the way when the job is one file, one function, a single needle, or a one-page convert-to-markdown, because those you read directly. It also stays out when latency matters more than completeness or when coordinating sub-agents would cost more than one honest read. ## Size, filter, split, verify The protocol comes from Section 5 of the paper, where current models acting as RLMs probe the input before they split the work. The skill turns that pattern into steps it can enforce. Sizing comes first and happens before anyone opens a file: the file count comes from glob or `find`, line counts from `wc -l`, bytes from `ls -lh`, and the page count from `pdfinfo` when the files are PDFs. Then the skill filters with searches, never with a recursive tree listing as a substitute. It chains those searches from file type to keyword to meaning, which is the same idea as the small loop in the paper that keeps only files matching `(database|connection|auth)` and only then reads them. What remains is chunked by natural units (a function, a class, a section) when those exist. Files over 2,000 lines or 50 KB are chunked by line ranges instead. The third option is a keyword partition, so that all error handling sits in one batch and all route definitions sit in another. The unit matters because each batch has to make sense to a sub-agent that sees nothing else. Recursion stays at one level. The parent writes down the batch count and launches one parallel wave of sub-agents, each with a self-contained brief that includes the files, the question, and the output schema. It merges what comes back and only then starts another wave, if batches remain. Sub-agents never spawn their own. The paper's numbers cut both ways on depth. On GPT-5, OOLONG-Pairs F1 moves from 58.0 at depth one to 76.0 at depth three, so extra depth is real on that model. Qwen3-Coder-480B-A35B, on the other hand, makes syntax errors that spread into sub-calls. On that class of model, depth two and three can score worse than depth one (Table 1, Figure 4b). So the skill stays at depth one. After the merge, verification pulls the cited locations and re-reads only those. If the answer disagrees with the files, the skill re-reads the disagreement instead of adding a second layer of delegation. Synthesis is programmatic: structured notes, deduplication, and categories come first, and the write-up with paths and line numbers is built from them. Through all of this, the main context never holds more than five files without a written batch plan. The same span never appears in two sub-agents either, because the partition is done once and the batches are disjoint. ## PDFs and Office files become text first PDFs and Office files are the case that used to dump a binary into the window, so the skill converts them before anything else. It prefers local [anydoc](https://skills.sh/firecrawl/anydoc/convert-documents-to-markdown), run as `npx -y @firecrawl/anydoc FILE -o .firecrawl/out.md`, then greps the markdown it produces. If anydoc exits 3, the pages are scans. The next step is then `--ocr hosted` or cloud [`firecrawl parse`](https://skills.sh/firecrawl/cli/firecrawl-parse), which caps at 50 MB and costs about one credit per page. Files over 100 pages or 30 MB get split before parse. A one-page convert job is anydoc or parse alone and does not need this skill. ## Testing it on 131 mortgage PDFs I test that path on a corpus I do not copy into the repo. The fixture is a git submodule, [tccao/mortgage-doc-rag](https://github.com/tccao/mortgage-doc-rag) (MIT), with 131 public-domain mortgage PDFs that add up to about 63 MB. Because CI does not clone it, missing files print `SKIP` instead of `ERROR`. The prompt is the same with the skill and without it. It asks the agent to size the tree without loading a PDF binary and to report how many files there are, the total bytes, and the ten largest. It then asks for at most three of those to be converted and their titles recovered from the markdown, all at depth one. Size-first took 0.02 seconds with the skill. anydoc then converted two digital PDFs in 1.64 seconds and recovered their titles, Appraisal of Real Property and TILA-RESPA Integrated Disclosure. The largest file is a four-page scan, `urar_form_1004_epa_scan.pdf`, at 1,639,534 bytes. anydoc exited 3 on it. `firecrawl parse` took 15.45 seconds on that scan and returned its title, Uniform Residential Appraisal Report. The naive path, `pdftotext` on all 131 files, took 1.65 seconds and extracted 0 bytes. On that scan it extracted 4 bytes and no title. That path is faster and blind on scans. The skill is slower because it OCRs the one file `pdftotext` cannot read, which is the file the prompt asked about. The eval scripts around the corpus stay small. `bash .github/scripts/eval-skill.sh check` validates the trigger queries in `evals/trigger-queries.json` and, when the submodule is present, the PDF count, a byte floor, and the largest filename, while `score` compares agent runs with and without the skill. The gold expectations for the corpus eval are `decomposed: true`, `depth: 1`, `subagents_spawned_subagents: false`, at least 131 PDFs, and `urar_form_1004_epa_scan.pdf` in the largest list. ## Three jobs from start to finish Three jobs show what a session looks like. For "find all error handling", the agent globs the sources, greps for `catch|throw|Error|except`, and batches the hits by module. Each batch goes to one sub-agent with a fixed report schema. The merge then comes back with file references. For "what features are planned across all PRDs", the agent globs the documents and sizes them first. It then locks an extraction schema (name, priority, status, quarter) and sends one sub-agent per group of documents. After the merge it deduplicates the features and spot-checks three entries against the sources. The third job is "summarise every TODO". The agent greps for `TODO|FIXME|HACK`, groups the hits by module, and extracts context and priority before it produces a list. When the output is long, it is generated section by section, stored, and then stitched together, which is how an RLM in the paper writes past a model's output limit. ## Trying it in a session Since 28 August the protocol installs as a skill, which you add with the skills CLI: ```bash npx skills add massimodeluisa/recursive-decomposition-skill ``` The command installs the skill listed at [skills.sh/massimodeluisa/recursive-decomposition-skill](https://skills.sh/massimodeluisa/recursive-decomposition-skill). Add `-g` for a user-level install, or `-a claude-code` (or another agent) to target one agent. In a session, `/recursive-decomposition` applies the protocol to the current task, while `/recursive-decomposition src/` sizes that path first. If a default is wrong, you can [edit it on GitHub](https://github.com/massimodeluisa/recursive-decomposition-skill). ## References 1. Zhang, Kraska, and Khattab, *Recursive Language Models*, arXiv:2512.24601, December 2025 (read in v3). [arxiv.org/abs/2512.24601](https://arxiv.org/abs/2512.24601) 2. Hong et al., 2025, cited by Zhang, Kraska, and Khattab as the source of the term *context rot*. 3. tccao, *mortgage-doc-rag*, GitHub, MIT license. [github.com/tccao/mortgage-doc-rag](https://github.com/tccao/mortgage-doc-rag) ## Checking whether AI crawlers can actually read a site _2026-08-13_ Have you ever looked at a finished page in Chrome and wondered what a crawler that never runs JavaScript receives from the same URL? The browser builds what you see by running the bundle. The crawlers that train and search with large language models never run it, so they keep only the HTML the server sent. I published [isready.ai](https://isready.ai) to answer one question about that gap: when a training or search crawler fetches the page, does it get the article, or an empty document? It answers with thirty-two checks. The scan and the written fix are free, while monitoring is what you pay for. ## Two machines read the same page The first machine is the browser. Chrome executes JavaScript, so a React or Vue app that hydrates in the browser shows its content there. The second machine is a crawler such as GPTBot, ClaudeBot, PerplexityBot, or OAI-SearchBot, which fetches raw HTTP with no JavaScript and short timeouts, under an honest user-agent. The same app can therefore arrive at the crawler as a blank document, since nothing runs the code that would have filled it in. Ordinary search dashboards stay green while that happens, because they are measuring a different machine. ## Running the audit The full audit is one command, pointed at the domain you want to check: ```bash npx isreadyai yourdomain.com --deep --md ``` That command and the Markdown report it writes are both free. `--deep` walks past the homepage, up to ten pages on the free web scan. You can also open the site and scan from the page, with no account. If you want the score as a CI gate, [isreadyai/audit-action@v1](https://github.com/isreadyai/audit-action) puts the report in the job summary and fails the run when the score drops below a threshold you set. A second pass uses a real browser, for agents that can click. The CI exit code still follows the crawler score, so that pass does not blow up an existing gate. ## What the thirty-two checks look at The default fetch imitates the crawler, requesting raw HTML the way those bots actually arrive. The thirty-two checks that run on what comes back are scored from 0 to 100. They ask whether the known AI user-agents are allowed in and whether the response contained the article or only a bundle those crawlers will not run. They also check that the page gives a model facts it can quote and that the transport is HTTPS. The writing itself is checked for the kinds of facts the GEO literature measured. That literature is the GEO paper by Aggarwal et al. at KDD 2024 ([arXiv:2311.09735](https://arxiv.org/abs/2311.09735)), where quotations, statistics, and citations helped while keyword stuffing was not one of the winners. Each finding carries an observed value and its consequence, followed by a concrete fix, which is the written fix the free scan gives you. The score is versioned, so a later change in method does not silently rewrite old reports. ## What the score leaves out One file appears in the report without ever moving the score. [llms.txt](https://llmstxt.org) is a community proposal for pointing language models at a site's documentation, but it does not control crawlers. isready reports whether the file exists and never moves the score because of it. Server logs on the product side show AI crawlers fetching that file in about 0.1% of visits. Google says in [its guide for Search](https://developers.google.com/search/docs/fundamentals/ai-optimization-guide) that you do not need special AI files for Google Search, which does not use llms.txt. isready is the check for the bots that never execute the bundle. Those bots come in more than one kind. OpenAI documents several bots; Anthropic splits theirs the same way. A user-triggered fetch often ignores robots.txt, while a training crawl usually does not. isready promises no ranking inside ChatGPT. A 92 on this scan means the crawler got a page it could parse. Citation share inside an answer is a later measurement. ## What stays free The audit itself is free: the scan, the CLI, the Markdown, the deeper pass, the browser pass, and the GitHub Action. The scanner is open source, while the hosted dashboard, from Smart Squad S.r.l., is source-available under PolyForm Shield. Pro is €19 a month and Team is €49. That money pays for monitoring, history, a live badge, Ask-your-site (300 chats a month on Pro, 1500 on Team), and quotas for automated fix pull requests (200 a month on Pro, 1000 on Team). The image below shows what the hosted weekly report looks like. ![What the hosted weekly report looks like](/journal/2026-08-13/isready-weekly-email.png){.img-center} You can scan a site from [isready.ai](https://isready.ai) without an account, or run the `npx` command above from a terminal. ## References 1. Aggarwal et al., *GEO: Generative Engine Optimization*, KDD 2024. [arXiv:2311.09735](https://arxiv.org/abs/2311.09735) 2. *llms.txt*, community proposal. [llmstxt.org](https://llmstxt.org) 3. Google, *AI optimization guide*, Google Search Central. [developers.google.com](https://developers.google.com/search/docs/fundamentals/ai-optimization-guide) ## Why Inksquad is two products _2026-08-13_ Two people who end up working on the same tattoo need very different things. The first has the tattoo in their head and needs a place to get the idea out of a group chat, with a description, a style, a placement, and references they actually like. The second is a tattooer in the middle of a session, who needs a stencil they can still edit, bottles of ink that exist, and controls they can hit with gloves on. Inksquad, where I am CTO, gives each job its own product: People for the person with the idea and Artist for the tattooer. The two share a project, so an idea can move from one to the other. The card at the top is the live Open Graph image from inksquad.com. ## Two jobs that do not fit on one screen The two jobs differ in when they happen and in what they need. The client makes the idea specific in private, before anyone books, while the tattooer needs the stencil and the bottles at hand during the session itself. When one screen tries to hold both, the booking widget takes the space and the stencil ends up in settings. Splitting them gave each job a product of its own. The shared project is what still connects them, so the work a client puts together in People can move over to Artist. ## People, where the idea gets specific [web.inksquad.com](https://web.inksquad.com) is for the person with the idea. You make a private project and fill in the description, the style, where the tattoo sits, and the size, then add references you actually like. If it helps you see the thing, Ink AI can draw a visual. When the idea is ready, you share the project with an artist. People stops at that project; booking and the session live elsewhere. The site is public and it works. An internal beta update is coming, so if you land there and something feels rough, that is because People is still being built in the open. ## Artist, the iPad app for the session [artist.inksquad.com](https://artist.inksquad.com) is the iPad app that Inksquad Srl publishes on the App Store for $9.99 plus credits. When you import a sketch from Procreate, Photoshop, or Illustrator, the stencil lands in layers you can still edit. If the stencil is wrong, you fix the layer and keep the drawing, so a mistake does not send you back to the start. The unit of work in Artist is an Inkboard, which holds the palette, the project, notes, drawings, and images in one place. Keeping them together means you are not hunting through five apps for the bottle you already picked. ## Color comes from bottles that exist Color work starts with markers you drop on the image. Matching answers with more than four hundred real catalog inks, including Intenze, Fusion, World Famous, and Eternal Ink. It also answers with sets that have to be legal in the EU. In Europe you cannot treat ink like a hex code from a moodboard, so the catalog answers with bottles that exist. For the color work that happens at a bench, Matrix and Color Wheel cover palettes and blending. ## Working with gloves on Live mode is the gloves test. During a session your hands are not free, so Live mode keeps zoom, markers, stencil layers, a timer, and voice within reach. The app keeps working offline, because wifi in a shop is a rumor. AI can help generate a concept or clean a reference, but the session still belongs to the artist. ## Where the two meet Both products meet at the shared project. An idea in People should be something an artist can open on the iPad. People is how a client shows up with more than a screenshot. If you only try one of them, try [Artist](https://artist.inksquad.com) in a studio. ## Calling Hohmann from a public URL _2026-08-13_ On 13 August 2026 I pointed a client at [https://sidus.tools/api/mcp](https://sidus.tools/api/mcp) and called `hohmann` with r1_m = 6578137 and r2_m = 42164137. Those two radii describe a circular transfer from about 200 km LEO to GEO, computed with the educational Earth μ and kept in SI the whole way. The tool answered with a first burn of dv1 2454.59 m/s and a second burn of dv2 1477.27 m/s, for a total of **3931.86 m/s**. The time of flight came back as 18931.92 s (about 5.3 hours), with a transfer semi-major axis of 24371137 m. The same response carried v1 7784.26 m/s and v2 3074.66 m/s. [sidus.tools](https://sidus.tools) is a page where you enter the inputs and get a space-engineering number in SI, with the formula next to it. If you type the same two radii into the browser, the page also returns 3931.86 m/s, because the page and the public URL import the same function. The physics lives in one place. ## One function behind the page and the URL The number above is an educational two-body [Hohmann transfer](https://en.wikipedia.org/wiki/Hohmann_transfer_orbit). The same function produces it whether a person types the radii into the page or a client sends them to the URL. If Hohmann is wrong in one place, it is wrong in both, so you fix it once. I published sidus.tools so these checks would live in SI, both on a page I could read and on a URL a client could hit without a second copy of the physics quietly drifting away from the first. ## What SIDUS is SIDUS (from the Latin *sidus*, a constellation) is a free catalog of educational space-engineering checks in SI. It is an independent project, with no agency behind it. As of 4 September 2026 the live catalog holds 201 tools across orbital mechanics, propulsion, RF link budgets, launch, crew life support, and the smaller geometry around them. You do not need an account to use any of them. The source is at [github.com/massimodeluisa/sidus-tools](https://github.com/massimodeluisa/sidus-tools). The card at the top of this article is the live Open Graph image from the site. I write software, and I am not a space engineer. The formulas and any code a page exports should be reviewed and tested before you rely on them, because every tool in the catalog is an educational check. Each page shows its formula next to the number, which means that review can start on the same screen as the result. ## Models small enough to hold in your head The defaults describe an educational [WGS-84](https://en.wikipedia.org/wiki/World_Geodetic_System)-class Earth, with μ⊕ = 3.986004418×10¹⁴ m³/s² and an equatorial radius of 6 378 137 m. The models built on those defaults stay small on purpose: two-body motion, impulsive burns, circular Clohessy-Wiltshire, the ideal rocket, and ISA aero. A model of that size lets you hold the assumption in your head while you read the result. Two-body pages cite Vallado's *[Fundamentals of Astrodynamics and Applications](https://microcosmpress.com/vallado/)* and Curtis's *[Orbital Mechanics for Engineering Students](https://shop.elsevier.com/books/orbital-mechanics-for-engineering-students/curtis/978-0-08-102133-0)*. A tool does not ship until its citation is a real HTTPS URL, so each formula leads back to a source a reader can open. Units stay in SI everywhere. If you want kilometres, you convert on purpose, because that conversion is a decision and I want it visible. A page can also export the same number into code you compile. If the two disagree, I have a bug. ## Hohmann is one page among many The transfer at the top of this article is one page in the catalog. Next to it sit Lambert, SGP4, a rocket equation, an RF link budget, cabin atmosphere, and an Earth-Mars sketch, each with its own inputs and its own formula on the page. On any of them you can disagree with the model, see the formula it uses, and call it again with different numbers. ## Calling it from an agent The same catalog is available at [https://sidus.tools/api/mcp](https://sidus.tools/api/mcp) over Streamable HTTP, with no API key. A GET request returns discovery. A POST request is the tool call, which is how the `hohmann` result at the top of this article came back. You do not need to clone the repository to use it. If you want the catalog as an agent skill, install it with this command: ```bash npx skills add massimodeluisa/sidus-tools --skill sidus ``` The skill is listed at [skills.sh/massimodeluisa/sidus-tools/sidus](https://www.skills.sh/massimodeluisa/sidus-tools/sidus). If a default is wrong, the page has an edit link on [GitHub](https://github.com/massimodeluisa/sidus-tools). ## References 1. Vallado, *Fundamentals of Astrodynamics and Applications*, Microcosm Press. [microcosmpress.com/vallado](https://microcosmpress.com/vallado/) 2. Curtis, *Orbital Mechanics for Engineering Students*, Elsevier. [shop.elsevier.com](https://shop.elsevier.com/books/orbital-mechanics-for-engineering-students/curtis/978-0-08-102133-0) ## How I rebuilt deluisa.me _2026-06-08_ The previous version of my portfolio looked fine on a desktop monitor. On a phone you had to pinch to read a paragraph, then pinch again after you followed a link. I was tired of sending people a page I would not open in my own pocket. I was also tired of personal sites that arrive like a product launch, with a hero that sells you a person. I made the folder for the new site on 21 May 2026. GitHub Pages started serving [deluisa.me](https://deluisa.me) on 8 June. The site, this journal, and the work pages now live in one repository, where the build turns all of them into static HTML. ## A site I can keep and write next to I wanted a site I could keep, write next to, and use from a phone without thinking about it. The first two wishes show up in how the site is built. The build writes static HTML for GitHub Pages to serve, so there is no CMS login anywhere in the process. A sentence lives in the same repository as the page that shows it, so when a sentence matters, its review is a pull request like any other change to the site. ## A header you can hit The third wish, using the site from a phone, is where the header comes in. The header stays on screen while you scroll and is transparent at the top of the page. Further down it turns into a pill that holds a house icon, a clock on Rome time, and the menu. I ship iPad software for studios, so I already know what happens when a control is sixteen pixels wide and someone is wearing gloves. Apple's [44-point](https://developer.apple.com/design/tips/) target is the size I used for the language switcher, the contact control, and the button that takes you back to the top. The hero at the top of the home page is allowed to show off a little, with a mesh gradient and a 3D portrait. After that the page should be quiet. I spent more time on whether the pill was hittable than on the portrait. ## Languages, a clock, and two Japanese domains The Rome clock in the pill is there because that is the time zone I work in. The language you pick is stored, so a reader who chooses Italian does not get reset on the next visit. English is the default and Italian is finished, while Japanese, Russian, and Ukrainian have the chrome and the long copy is still catching up. A bad URL gets a real 404. I also bought two Japanese domains, [出る.com](https://出る.com) and [デルイザ.com](https://デルイザ.com), because I am moving to Japan. Both come from one chain of sounds. De Luisa becomes Deruiza when it is said in Japanese, which is what デルイザ spells. The start of that name can be written 出る, which means "to go out". ## Pages a crawler can read The journal and the work pages are Markdown files in the same repository until the build turns them into HTML along with the rest of the site. The same choice decides what a crawler sees. Training crawlers such as GPTBot generally do not run a browser, so a sentence that only appears after the page hydrates does not exist for them. Static HTML is how I write; it is also how those crawlers get the sentence. When I later published [isready.ai](https://isready.ai) to score that gap on other sites, this site had to pass the same test. Motion on the page stops when the phone has asked for reduced motion. If a scroll animation still runs for that person, that is my bug. The same goes for a control that is hard to hit. The site is at [deluisa.me](https://deluisa.me). If a button is too small in your hand, that is the report I actually want. --- # Selected work ## Inksquad You shape a tattoo idea in private, then hand it to an artist. On the iPad, that artist turns it into a stencil, matches it to real inks, and runs the session with gloves on. Inksquad is two products, and I am the CTO. Someone with a tattoo in their head uses People. The artist in the session uses Artist. They share a project, so the idea does not have to come back as a folder of screenshots. [web.inksquad.com](https://web.inksquad.com) is where the idea gets specific. You write what you want, name the style, the placement, and the size, and add references you actually like. Ink AI can draw a visual if that helps you see it. When the project is ready, you share it with an artist. It stops there. Booking is someone else's job. The site is public, and it is still being built in the open. [artist.inksquad.com](https://artist.inksquad.com) is the iPad app, published by Inksquad Srl, $9.99 plus credits. You import a sketch from Procreate, Photoshop, or Illustrator and turn it into a stencil you can still edit. You drop markers on the picture, and the app answers with bottles from the catalog: more than four hundred, including Intenze, Fusion, World Famous, Eternal Ink, and sets that are legal in the EU. Matrix and Color Wheel are for mixing a palette at the bench. In a live session the app takes voice, because gloves and a glass screen do not get along, and it keeps working when the shop wifi does not. AI can draft a concept or clean a reference. The session still belongs to the artist. ## Open Bio Page Someone opens your page, picks a person, and gets that person's links. Each of them gets a portrait, a line about them, the links they want to hand out, and how they want to be presented. You can change all of that, which is why it can be a simple family links page or a company page. [Open Bio Page](https://openbio.page/) puts that page on a domain you already control. The home is the people. Open one of them and you are on their page, with the portrait, the line, and the links they chose to hand out, written the way they want to be seen. A site, an email, a profile, or any other link you put there. I made the first directory for my family. This is that directory with the names taken out, so the next family or the next team can put their own in. The [docs home](https://openbio.page/) has a prompt that publishes one for you. Choose Family or personal, or Company or team, then copy it or open it in Claude Code, Codex, Cursor, Claude, or ChatGPT. The agent asks once for the hostname and for each person, keeps the wording you give it, and publishes the site. If there is no domain, it stops. The same steps, if you want to do them yourself, are at [Create your bio](https://openbio.page/create). A later login edits only that person's page. Visitors do not need one. Source: [github.com/open-bio-page/open-bio-page](https://github.com/open-bio-page/open-bio-page). ## isready.ai You point it at a page and it tells you whether GPTBot or ClaudeBot received the article, or an empty document. The scan and the written fix are free. Monitoring, if you want the score watched, is hosted. [isready.ai](https://isready.ai) answers one question. When GPTBot, ClaudeBot, PerplexityBot, or OAI-SearchBot fetches the page, do they get the article? Chrome runs JavaScript. Those crawlers generally do not. A React or Vue app can look finished in the browser and still hand them an empty document, and a challenge page can drop you from answers without touching ordinary search rankings. isready.ai is a Smart Squad product from Udine. It fetches the way those bots do and runs 32 checks. Each finding says what was observed, what it costs, and what to change. The score is versioned from 0 to 100. The content checks follow Aggarwal et al., GEO, KDD 2024: quotations, statistics, citations. An llms.txt file is noted and never moves the score. ```bash npx isreadyai yourdomain.com --deep --md ``` That command, and the Markdown it writes, are free. You can also scan from the website with no account. A GitHub Action can fail the build when the score drops. A second pass can score whether a browser-capable agent can see the content and the controls. Pro is €19 a month and Team is €49. Those cover monitoring, history, a badge, Ask-your-site, and fix pull requests. The scanner is open source. The hosted dashboard is the part you pay for. ## Images in motion You put a mosaic of pictures on a page and the columns scroll in opposite directions. You set the tilt and the speed in the studio, copy those settings, and the pictures stay in the browser. I published [images-in-motion](https://iim.smartsquad.io/) on 3 September 2026. You mount a mosaic. Each column leans, and it scrolls the opposite way from the column beside it. The speed stays constant inside one column and differs from the next, so the rows do not lock into a grid. The default tilt is 12 degrees. The default speed is 8 to 18 logical pixels a second. You can scroll rows instead of columns. The motion is CSS. Geometry is computed once, and the browser keeps the columns moving. There is no animation loop in JavaScript, which is the point: the pictures should keep going when the framework is idle. React and Vue only create the host. Expo and NativeScript put that same renderer in a WebView. The file on the CDN is 17.38 KB minified (17,798 bytes), 6.47 KB gzip, 5.83 KB brotli. Do not round those. It does not include React, Vue, or the images. The [studio](https://iim.smartsquad.io/studio/) is where you set the canvas, the speed, the tilt, and the tiles. Copy writes the settings. The pictures you picked never enter that copy, so a configuration can leave the browser without the photos. Source: [github.com/smartsquad/images-in-motion](https://github.com/smartsquad/images-in-motion). ## SIDUS You open a page, enter the inputs, and get a space-engineering number in SI, with the formula next to it. There are 201 of these, free in the browser, and you do not need an account. I write software, and I am not a space engineer. I published [sidus.tools](https://sidus.tools) so a check would be a page you can read: the inputs, the formula, and a number in SI. As of 4 September 2026 there are 201 tools. You can run a transfer, a link budget, a rocket equation, a cabin atmosphere. They run in the browser. There is no account. The project is independent, with no agency behind it. The page and the public URL use the same function. On 13 August 2026 a Hohmann transfer from about 200 km LEO to GEO came back as 3931.86 m/s in both places. If that number is wrong on the page, it is wrong on the URL, and you fix it once. Defaults use ordinary Earth values for teaching (μ⊕ = 3.986004418×10¹⁴ m³/s², equatorial radius 6 378 137 m). The models stay small: two-body, impulsive burns, circular relative motion, an ideal rocket, standard-atmosphere aero. Two-body pages cite Vallado and Curtis. A page can export the calculation into code you can compile. Review it before you trust it. The catalog is also at [https://sidus.tools/api/mcp](https://sidus.tools/api/mcp). You point a client at that URL. Source: [github.com/massimodeluisa/sidus-tools](https://github.com/massimodeluisa/sidus-tools).