From 9f50ac384a82470421d32fa2ba7d7badad6472be Mon Sep 17 00:00:00 2001 From: Drew Butler Date: Thu, 10 Sep 2026 00:57:37 -0400 Subject: [PATCH 1/8] Refresh portfolio for current projects --- .github/workflows/preview.yml | 1 + README.md | 19 ++++++++---- package.json | 3 +- scripts/check-projects.mjs | 29 ++++++++++++++++++ src/content.config.ts | 4 +-- src/content/projects/artifacts.md | 38 ++++++++++++++++++++++++ src/content/projects/loomcli.md | 42 ++++++++++++++++++++++++++ src/content/projects/mimir.md | 39 ++++++++++++------------ src/content/projects/norn.md | 6 +++- src/content/projects/saga.md | 49 +++++++------------------------ src/content/projects/skald.md | 4 +-- src/pages/about.astro | 11 ++++--- src/pages/index.astro | 16 +++++----- src/pages/projects/[slug].astro | 41 ++++++++++++++------------ src/pages/projects/index.astro | 8 ++--- src/pages/search.json.ts | 2 +- 16 files changed, 206 insertions(+), 106 deletions(-) create mode 100644 scripts/check-projects.mjs create mode 100644 src/content/projects/artifacts.md create mode 100644 src/content/projects/loomcli.md diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml index 0250667..5754edc 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -28,6 +28,7 @@ jobs: - run: pnpm install --frozen-lockfile - run: pnpm check - run: pnpm build + - run: pnpm check:projects - id: preview uses: cloudflare/wrangler-action@v4 with: diff --git a/README.md b/README.md index f33f777..248e43e 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ +--- +description: "Development, content authoring, and deployment workflow for dbtlr.com." +--- + # dbtlr.com Personal site for [dbtlr.com](https://dbtlr.com) — a calm, typography-led dark-mode @@ -69,11 +73,13 @@ feature branch → PR to main → preview deploy → merge to main → p ``` - **PR previews** (`.github/workflows/preview.yml`): every PR against `main` runs - `pnpm check` + `pnpm build`, then `wrangler versions upload` publishes an ephemeral + `pnpm check`, `pnpm build`, and `pnpm check:projects`. Then + `wrangler versions upload` publishes an ephemeral version of the `dbtlr-com` Worker **without** touching production. The preview URL is posted (and kept updated) as a comment on the PR. -- **Production** (`.github/workflows/deploy.yml`): pushing to `main` runs the same - checks and `wrangler deploy`, publishing the `dbtlr-com` Worker that serves +- **Production** (`.github/workflows/deploy.yml`): pushing to `main` runs + `pnpm check` and `pnpm build`. Then `wrangler deploy` publishes the + `dbtlr-com` Worker that serves [dbtlr.com](https://dbtlr.com). Both workflows use Node 24 + pnpm and require `CLOUDFLARE_API_TOKEN` and @@ -81,12 +87,15 @@ Both workflows use Node 24 + pnpm and require `CLOUDFLARE_API_TOKEN` and ## Checks -Run before pushing — these are exactly what CI runs: +Run before pushing: ```sh -pnpm check && pnpm build +pnpm check && pnpm build && pnpm check:projects ``` +The project check inspects built pages and search. It verifies the current roster, +the retained Saga retirement page, and the removal of fixed version badges. + ## Project structure ``` diff --git a/package.json b/package.json index db2059b..c16cd2b 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,8 @@ "build": "astro build", "check": "astro check", "preview": "astro preview", - "sync:writing": "node scripts/sync-writing.ts" + "sync:writing": "node scripts/sync-writing.ts", + "check:projects": "node scripts/check-projects.mjs" }, "dependencies": { "@astrojs/rss": "^4.0.18", diff --git a/scripts/check-projects.mjs b/scripts/check-projects.mjs new file mode 100644 index 0000000..97df1bd --- /dev/null +++ b/scripts/check-projects.mjs @@ -0,0 +1,29 @@ +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; + +const root = new URL('../dist/', import.meta.url); +const read = (path) => readFile(new URL(path, root), 'utf8'); +const current = ['norn', 'mimir', 'loomcli', 'artifacts', 'skald']; +const search = JSON.parse(await read('search.json')); +assert.deepEqual(search.filter((item) => item.type === 'project').map((item) => item.href), + current.map((slug) => `/projects/${slug}/`), 'Search must list current projects in portfolio order'); +for (const path of ['index.html', 'projects/index.html', 'about/index.html']) { + const html = await read(path); + assert.ok(!html.includes('/projects/saga/'), `${path} must not promote retired Saga`); +} +for (const path of ['index.html', 'projects/index.html']) { + const html = await read(path); + for (const slug of current) assert.ok(html.includes(`/projects/${slug}/`), `${path} must link to ${slug}`); +} +const saga = await read('projects/saga/index.html'); +assert.match(saga, /Retired/); +assert.match(saga, /no longer under development/); +assert.ok(!saga.includes('code-file'), 'The retirement page must not offer a terminal demo'); +for (const slug of [...current, 'saga']) { + const html = await read(`projects/${slug}/index.html`); + assert.ok(!/\bv\d+\.\d+/.test(html), `${slug} must not publish a fixed version badge`); +} +const mimir = await read('projects/mimir/index.html'); +assert.match(mimir, /Markdown is the system of record/); +assert.ok(!mimir.includes('SQLite'), 'Mimir must not describe the retired SQLite backend'); +console.log('Built project pages and search satisfy the current/retired content contract.'); diff --git a/src/content.config.ts b/src/content.config.ts index 70abb67..4be9a42 100644 --- a/src/content.config.ts +++ b/src/content.config.ts @@ -21,8 +21,7 @@ const projects = defineCollection({ description: z.string(), short: z.string(), lang: z.string(), - status: z.string(), - version: z.string(), + status: z.enum(['active', 'early', 'pre-alpha', 'retired']), license: z.string().optional(), order: z.number(), tagline: z.string(), @@ -31,6 +30,7 @@ const projects = defineCollection({ github: z.string().optional(), docs: z.string().optional(), changelog: z.string().optional(), + releases: z.string().optional(), }) .optional(), facts: z.array( diff --git a/src/content/projects/artifacts.md b/src/content/projects/artifacts.md new file mode 100644 index 0000000..973e61a --- /dev/null +++ b/src/content/projects/artifacts.md @@ -0,0 +1,38 @@ +--- +name: Artifacts +blurb: Persistent links for agent-generated documents, images, and PDFs. +description: An MCP server and web interface that turn agent-generated documents, images, and PDFs into persistent links. Unversioned pre-alpha software for trusted networks. +short: share agent output +lang: TypeScript +status: pre-alpha +license: MIT +order: 4 +tagline: Give agent output a link you can return to. +links: + github: https://github.com/dbtlr/artifacts +facts: + - label: Surfaces + value: MCP · web + - label: Content + value: Documents · images · PDFs + - label: Maturity + value: Pre-alpha +demo: | + # Connect to a local Artifacts server + codex mcp add artifacts --url http://localhost:4242/mcp +--- + +Artifacts turns agent-generated Markdown, HTML, text, images, and PDFs into persistent links. An MCP server lets agents create and update content, and a small web interface makes that content readable and discoverable. + +## Why it is useful + +A report or mock is easier to review when it has a stable address. Artifacts gives the result its own page, and updates preserve the same URL. + +## What it does + +- Publishes documents, supported images, and PDFs through MCP. +- Preserves a link when its content changes. +- Groups related output in collections. +- Provides a web interface for reading and finding results. + +Artifacts is unversioned pre-alpha software for trusted networks. It has no built-in authentication and is not a public hosting service. diff --git a/src/content/projects/loomcli.md b/src/content/projects/loomcli.md new file mode 100644 index 0000000..a5f4b8c --- /dev/null +++ b/src/content/projects/loomcli.md @@ -0,0 +1,42 @@ +--- +name: Loom CLI +blurb: A TypeScript framework for command applications, with typed arguments, options, and schema validation. +description: A TypeScript framework for command applications with named commands, typed inputs, Standard Schema validation, and passthrough arguments. +short: command framework +lang: TypeScript +status: early +order: 3 +tagline: Typed declarations for command applications. +links: + github: https://github.com/dbtlr/loomcli + docs: https://github.com/dbtlr/loomcli/blob/main/docs/core.md + releases: https://github.com/dbtlr/loomcli/releases +facts: + - label: Type + value: Command framework + - label: Validation + value: Standard Schema + - label: Maturity + value: Early development +demo: | + # From the Loom CLI repository + pnpm install --frozen-lockfile + pnpm build + node examples/textstat/dist/src/main.js --metric words README.md +--- + +Loom CLI is a TypeScript framework for building command applications. It supports named commands, global and local options, required scalar and variadic arguments, Standard Schema validation, and passthrough arguments. + +## Why it is useful + +A command declaration describes the inputs an action receives. Loom CLI carries those types into the action, and schema validation checks and transforms values before the action runs. + +## What it does + +- Routes named commands under application-wide options. +- Derives argument and option types from declarations. +- Accepts Standard Schema validators without a dedicated adapter. +- Keeps passthrough arguments separate from parsed inputs. +- Provides shared output and failure behavior. + +Loom CLI is in early development. The textstat and jsonkit examples demonstrate the current scope. diff --git a/src/content/projects/mimir.md b/src/content/projects/mimir.md index c4e0a2d..8ba5228 100644 --- a/src/content/projects/mimir.md +++ b/src/content/projects/mimir.md @@ -1,45 +1,44 @@ --- name: mimir -blurb: Local-first work-state tracker — one query layer, four surfaces, SQLite as the source of truth. -description: A local-first task and work-state tracker. SQLite is the source of truth; rollups and dependency predicates are derived live, never stored. One core, four surfaces. -short: work tracker +blurb: Local work state in a Norn-managed Markdown vault, with queues and status derived when queried. +description: A local-first work-state engine over a Norn-managed, git-backed Markdown vault. The CLI, MCP server, HTTP API, and operator console share one work model. +short: work state lang: TypeScript status: active -version: v0.8 license: MIT order: 2 -tagline: One core query layer over four surfaces — a CLI for humans, an MCP server for agents, an HTTP API, and a web console — all speaking the same verbs over a single SQLite store. Work state stays the source of truth, without a sync problem. +tagline: Local work state, with the next action derived from the facts. links: github: https://github.com/dbtlr/mimir changelog: https://github.com/dbtlr/mimir/blob/main/CHANGELOG.md + releases: https://github.com/dbtlr/mimir/releases facts: - label: Surfaces value: CLI · MCP · HTTP · console - label: Store - value: SQLite, rollups derived + value: Norn-managed Markdown - label: Scope value: Single operator demo: | - curl -fsSL https://raw.githubusercontent.com/dbtlr/mimir/main/install.sh | sh - mimir next # ready tasks, in rank order - mimir create task "wire the API" --parent MMR-2 --priority p1 - mimir start MMR-3 && mimir done MMR-3 + mimir overview # current direction and active work + mimir next # ready tasks, in rank order + mimir list # the live work queue --- -## Why it exists +Mimir keeps projects, tasks, dependencies, decisions, and work products in a Norn-managed, git-backed Markdown vault. The CLI, MCP server, HTTP API, and operator console share the same work model. -Task state is ephemeral and fast-changing, but a Markdown vault is optimized for durable, slow-changing knowledge. Storing work state as notes makes convention the only enforcement layer — and convention always decays: statuses drift, stale tasks pile up, rollups get hand-maintained. mimir moves work state into a structured SQLite store so the substrate fits the job. Markdown becomes a projection, not the source. +## Why it exists -mimir exists because work state changes faster than knowledge. It keeps tasks, blockers, rank, and history in a structured local store, while Markdown remains a projection for humans. +Long-running work needs a reliable answer to what is active, what is blocked, and what comes next. Mimir gives those questions a structured model while keeping the underlying records inspectable as Markdown. ## What it does -- **Models work as a tree** — project → initiative → phase → task — with two status axes per task: a lifecycle and a hold overlay. -- **Derives, never stores** — `ready`, `blocked`, `stale`, and every rollup are computed live, so there's no cache to fall out of sync. -- **One query surface, composable** — `next`, `list`, `get`, `status` with AND-composed filters and pipe-safe output formats. -- **Four transports, one core** — a CLI, an MCP server for agents, an HTTP API, and an installable web console. -- **Append-only history** — abandoned work stays abandoned; nothing is deleted. +- Organizes projects, initiatives, phases, and tasks. +- Derives queues, status rollups, and blockers when queried. +- Preserves work products as Artifacts and temporary context as Scratchpads. +- Tracks new ideas and cross-project requests as Seeds. +- Gives agents session context and operators a console across projects. -## Derive, don't store +## Store facts, derive the view -The spine running through every design decision: mimir stores work-state facts, never derived state or a consumer's semantics. Caching a rollup would reintroduce the very sync problem the tool exists to remove — so "what's next," "what's blocked," and "what's stale" are answered at read time. Rank, the one true ordering, beats every priority heuristic, because sometimes you place a P2 ahead of a P0 for a reason the system can't see. +Markdown is the system of record. Norn owns validated access to the vault. Mimir derives what is ready, awaiting, or stale from the stored facts, so there is no second project-status record to synchronize. diff --git a/src/content/projects/norn.md b/src/content/projects/norn.md index c6e2973..806135b 100644 --- a/src/content/projects/norn.md +++ b/src/content/projects/norn.md @@ -5,11 +5,11 @@ description: A Rust CLI and MCP server that turns a Markdown vault into a determ short: vault tooling lang: Rust status: active -version: v0.38 license: MIT order: 1 tagline: Obsidian gives you a GUI over your Markdown vault. norn gives you the same vault as a deterministic graph — one humans and agents can both query, validate, and repair from the command line. links: + releases: https://github.com/dbtlr/norn/releases github: https://github.com/dbtlr/norn changelog: https://github.com/dbtlr/norn/blob/main/CHANGELOG.md facts: @@ -43,3 +43,7 @@ The thesis is a chain — rules make the vault consistent, consistency makes que ## Plan, then apply Mutation is never a black box. norn produces a schema-versioned, inspectable repair plan and only changes files when you apply it. The division of labor is deliberate: the agent decides, norn enumerates. No model sits in the loop rewriting prose — which is exactly what makes the result predictable enough to trust. + +## Current development + +The released tool is available from the [norn repository](https://github.com/dbtlr/norn). The next implementation is under development in [norn-next](https://github.com/dbtlr/norn-next) and has no releases yet. diff --git a/src/content/projects/saga.md b/src/content/projects/saga.md index 5b0136d..e0580c2 100644 --- a/src/content/projects/saga.md +++ b/src/content/projects/saga.md @@ -1,47 +1,18 @@ --- name: saga -blurb: Agent memory for coding sessions — carries profile, shared memory, and workspace context across runs. -description: A memory provider for coding agents that assembles session context, preserves continuity across fresh runs, and turns finished work into durable workspace memory. -short: agent memory +blurb: A retired experiment in memory for coding agents. +description: Saga explored session context and durable lessons for coding agents. The project is retired and no longer under development. +short: retired experiment lang: Agent memory -status: early -version: v0.1 -license: MIT -order: 3 -tagline: "Coding agents need more than a prompt. saga provides the memory layer: user profile, shared operating knowledge, workspace context, and session records that survive compactions, fresh windows, and long-running work." +status: retired +order: 6 +tagline: A retired experiment in memory for coding agents. links: github: https://github.com/dbtlr/saga -facts: - - label: Type - value: Memory provider - - label: Harnesses - value: Claude Code + Codex - - label: Scope - value: User · shared · workspace -demo: | - Session start - ├─ User Profile - ├─ Shared Memory - └─ Workspace Brief - - Work boundary - └─ Session Log → durable memory +facts: [] +demo: '' --- -## Why it exists - -Agentic coding sessions lose their through-line. Work spans more than one context window, and detail evaporates across compactions, restarts, and fresh runs. saga exists to make memory explicit: what the agent should know about the user, what it should carry across projects, what matters in this workspace, and what was learned from the work that just finished. - -It came out of a restart. The first shape put too much of the story on commands and workflows. The durable need was simpler: a provider for the context a coding agent needs before it starts, and a path for completed work to become maintained memory instead of buried transcript. - -## What it does - -- **Assembles active context** — user profile, shared memory, and workspace brief arrive before the agent starts making choices. -- **Keeps workspace language live** — glossary and decisions stay available as constraints, not archive files the agent forgets to check. -- **Records work boundaries** — session logs capture decisions, deviations, and consolidation candidates when a body of work finishes. -- **Promotes durable lessons** — repeated or important lessons move out of logs and into maintained memory. -- **Separates memory from procedure** — durable facts live as memory; commands and workflows stay in the harness. - -## The session log is the record +Saga explored memory for coding agents: the context a session needs at the start, and the lessons worth carrying into later work. -saga deletes a category. Specs and plans are transient review surfaces, discarded on merge; the durable retrospective is the session log — itself prunable once its lessons are lifted out. The test for keeping anything is blunt: would it matter if this were deleted? That is the memory-provider line: keep the facts that change future work, and let the rest disappear. +The project is no longer under development. This page remains as a record of that experiment. diff --git a/src/content/projects/skald.md b/src/content/projects/skald.md index de35390..bdc7c6f 100644 --- a/src/content/projects/skald.md +++ b/src/content/projects/skald.md @@ -5,11 +5,11 @@ description: An AI-powered git workflow CLI (sk) that generates commit messages, short: AI git CLI lang: Rust status: active -version: v0.5 license: MIT -order: 4 +order: 5 tagline: Stop writing commit messages by hand. skald reads your staged diff, offers a carousel of candidates, and drafts full PR titles and descriptions — so you stay in flow and let the tool handle the prose. links: + releases: https://github.com/dbtlr/skald/releases github: https://github.com/dbtlr/skald changelog: https://github.com/dbtlr/skald/blob/main/CHANGELOG.md facts: diff --git a/src/pages/about.astro b/src/pages/about.astro index 28404c2..539d54b 100644 --- a/src/pages/about.astro +++ b/src/pages/about.astro @@ -3,8 +3,7 @@ import Base from '../layouts/Base.astro'; import SectionHead from '../components/SectionHead.astro'; const now = [ - { label: 'Building', value: 'AI-native dev tools: vaults, work state, agent memory, git prose' }, - { label: 'Writing', value: 'this site, and notes on what I learn building' }, + { label: 'Building', value: 'Open-source tools for AI-native development' }, ]; const elsewhere = [ { label: 'GitHub', value: 'github.com/dbtlr', href: 'https://github.com/dbtlr', external: true }, @@ -12,14 +11,14 @@ const elsewhere = [ { label: 'Email', value: 'hi@dbtlr.com', href: 'mailto:hi@dbtlr.com', external: false }, ]; --- - +

Hi, I’m Drew.

-

I’m a product-focused engineer and engineering leader. Twenty years in, I’ve never stopped writing code — I think the best technical leaders keep their hands on the work, and it keeps me honest about the trade-offs. Lately I’m focused on agentic engineering: building the open-source tooling and workflows for how software gets made next.

-

I care about why something is being built, not just how. My best work happens close to the problem — talking to users, learning the workflow, making architectural calls that serve the product rather than the tech. I’ve led distributed teams since before remote was normal, and I’d still rather reach for the right boring tool than the trendy one.

-

Right now that’s norn, mimir, saga, and skald — separate open-source tools for the parts of AI-native development I keep running into: vaults, work state, agent memory, and git prose. (I’ve named a few products in my time; the Norse habit stuck.) I’m based in Brooklyn.

+

I’m a product-focused engineer and engineering leader. Twenty years in, I’ve never stopped writing code. I think the best technical leaders keep their hands on the work, and it keeps me honest about the trade-offs. Lately I’m focused on agentic engineering: building the open-source tooling and workflows for how software gets made next.

+

I care about why something is being built, not just how. My best work happens close to the problem: talking to users, learning the workflow, making architectural calls that serve the product rather than the tech. I’ve led distributed teams since before remote was normal, and I’d still rather reach for the right boring tool than the trendy one.

+

Right now I’m building tools for the work around the code: keeping knowledge useful, tracking decisions and tasks, building command applications, and sharing results. That work spans norn, mimir, Loom CLI, Artifacts, and skald.

diff --git a/src/pages/index.astro b/src/pages/index.astro index 9daa631..7935131 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -3,18 +3,20 @@ import { getCollection } from 'astro:content'; import Base from '../layouts/Base.astro'; import SectionHead from '../components/SectionHead.astro'; -const projects = (await getCollection('projects')).sort((a, b) => a.data.order - b.data.order); +const projects = (await getCollection('projects', ({ data }) => data.status !== 'retired')).sort((a, b) => a.data.order - b.data.order); --- - +

I build tools for AI-native development.

- I’m Drew Butler — a product-focused engineer and engineering leader, twenty years in and still - shipping code every day. Lately that means open-source tooling for AI-native development: - norn for Markdown vaults, mimir for local work state, - saga for agent memory, and skald for git prose. The names lean Norse; - the tools solve separate problems. I write about what I learn building them. Based in Brooklyn. + I’m Drew Butler, a product-focused engineer and engineering leader. Twenty years in, I still ship code every day. + Lately I’m building open-source tools for AI-native development: + norn for Markdown vaults, + mimir for work state, + Loom CLI for command applications, + Artifacts for sharing agent output, + and skald for git prose. Based in Brooklyn.

diff --git a/src/pages/projects/[slug].astro b/src/pages/projects/[slug].astro index 0107c17..e79b594 100644 --- a/src/pages/projects/[slug].astro +++ b/src/pages/projects/[slug].astro @@ -9,14 +9,15 @@ export async function getStaticPaths() { const { project } = Astro.props; const { Content } = await render(project); -const meta = [project.data.lang, project.data.version, project.data.license].filter(Boolean).join(' · '); +const meta = [project.data.status === 'retired' ? 'Retired' : project.data.status, project.data.lang, project.data.license].filter(Boolean).join(' · '); const buttons = [ { href: project.data.links?.github, label: 'View on GitHub', primary: true }, { href: project.data.links?.docs, label: 'Documentation', primary: false }, { href: project.data.links?.changelog, label: 'Changelog', primary: false }, + { href: project.data.links?.releases, label: 'Releases', primary: false }, ].filter((b) => b.href); --- - +
@@ -40,24 +41,28 @@ const buttons = [ )}
-
-
- terminal - shell -
-
{project.data.demo}
-
+ {project.data.status !== 'retired' && ( + <> +
+
+ terminal + shell +
+
{project.data.demo}
+
-
- {project.data.facts.map((fact) => ( -
-
{fact.label}
-
{fact.value}
-
- ))} -
+
+ {project.data.facts.map((fact) => ( +
+
{fact.label}
+
{fact.value}
+
+ ))} +
+ + )} -
+
diff --git a/src/pages/projects/index.astro b/src/pages/projects/index.astro index a21f594..763704e 100644 --- a/src/pages/projects/index.astro +++ b/src/pages/projects/index.astro @@ -2,13 +2,13 @@ import { getCollection } from 'astro:content'; import Base from '../../layouts/Base.astro'; -const projects = (await getCollection('projects')).sort((a, b) => a.data.order - b.data.order); +const projects = (await getCollection('projects', ({ data }) => data.status !== 'retired')).sort((a, b) => a.data.order - b.data.order); --- - +

Projects

-

Open-source tools for AI-native development. norn makes Markdown vaults queryable, mimir tracks local work state, saga provides agent memory, and skald writes git prose. Built in the open, as separate stories.

+

Open-source tools for AI-native development. norn makes Markdown vaults queryable, mimir tracks work state, Loom CLI builds command applications, Artifacts shares agent output, and skald writes git prose.

{projects.map((project) => ( @@ -21,7 +21,7 @@ const projects = (await getCollection('projects')).sort((a, b) => a.data.order -

{project.data.description}

{project.data.lang} - {project.data.status} · {project.data.version} + {project.data.status} diff --git a/src/pages/search.json.ts b/src/pages/search.json.ts index fe69033..4740fc2 100644 --- a/src/pages/search.json.ts +++ b/src/pages/search.json.ts @@ -5,7 +5,7 @@ import { getCollection } from 'astro:content'; // Order matters: the palette groups rows under Writing / Projects / Pages headers // by type transitions. export const GET: APIRoute = async () => { - const projects = (await getCollection('projects')).sort((a, b) => a.data.order - b.data.order); + const projects = (await getCollection('projects', ({ data }) => data.status !== 'retired')).sort((a, b) => a.data.order - b.data.order); const posts = (await getCollection('writing')).sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf()); const items = [ From 6037b9cecd0e30b1b7365169c063071265f71601 Mon Sep 17 00:00:00 2001 From: Drew Butler Date: Thu, 10 Sep 2026 05:56:44 -0400 Subject: [PATCH 2/8] Restore bullet markers in article lists --- src/styles/global.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/styles/global.css b/src/styles/global.css index 4db04f5..5eac4c5 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -52,7 +52,7 @@ .body a { @apply text-accent-cool border-b border-cool-dim hover:border-accent-cool; } .body h2 { @apply text-2xl font-[650] tracking-[-0.01em] mt-10 mb-4 max-md:text-[21px]; } .body > h2:first-child { @apply mt-0; } - .body ul { @apply m-0 mb-6 pl-5.5; } + .body ul { @apply list-disc m-0 mb-6 pl-5.5; } .body li { @apply text-base leading-[1.7] text-text-2 my-1.5; } .body li::marker { color: var(--color-accent-warm); } From fffbd2aa35fcd1de943dec098db33078cab7e781 Mon Sep 17 00:00:00 2001 From: Drew Butler Date: Thu, 10 Sep 2026 06:00:53 -0400 Subject: [PATCH 3/8] Limit homepage to three selected projects --- scripts/check-projects.mjs | 10 ++++++++-- src/pages/index.astro | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/scripts/check-projects.mjs b/scripts/check-projects.mjs index 97df1bd..53562d0 100644 --- a/scripts/check-projects.mjs +++ b/scripts/check-projects.mjs @@ -11,9 +11,15 @@ for (const path of ['index.html', 'projects/index.html', 'about/index.html']) { const html = await read(path); assert.ok(!html.includes('/projects/saga/'), `${path} must not promote retired Saga`); } -for (const path of ['index.html', 'projects/index.html']) { +for (const [path, expected] of [ + ['index.html', ['norn', 'mimir', 'loomcli']], + ['projects/index.html', current], +]) { const html = await read(path); - for (const slug of current) assert.ok(html.includes(`/projects/${slug}/`), `${path} must link to ${slug}`); + const rows = [...html.matchAll(/]*class="group [^"]*"[^>]*href="([^"]+)"/g)] + .map((match) => match[1]); + assert.deepEqual(rows, expected.map((slug) => `/projects/${slug}/`), + `${path} must show exactly its intended project rows`); } const saga = await read('projects/saga/index.html'); assert.match(saga, /Retired/); diff --git a/src/pages/index.astro b/src/pages/index.astro index 7935131..01281f6 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -3,7 +3,7 @@ import { getCollection } from 'astro:content'; import Base from '../layouts/Base.astro'; import SectionHead from '../components/SectionHead.astro'; -const projects = (await getCollection('projects', ({ data }) => data.status !== 'retired')).sort((a, b) => a.data.order - b.data.order); +const projects = (await getCollection('projects', ({ data }) => data.status !== 'retired')).sort((a, b) => a.data.order - b.data.order).slice(0, 3); ---
From cf96efdb57416d29d7cec2be16a45b502481dcb3 Mon Sep 17 00:00:00 2001 From: Drew Butler Date: Thu, 10 Sep 2026 06:04:53 -0400 Subject: [PATCH 4/8] Add project favicons to portfolio listings --- public/projects/artifacts.svg | 8 ++++++++ public/projects/loomcli.ico | Bin 0 -> 4097 bytes public/projects/mimir.svg | 7 +++++++ public/projects/norn.svg | 5 +++++ public/projects/sources.json | 6 ++++++ scripts/check-projects.mjs | 12 ++++++++++++ src/content.config.ts | 1 + src/content/projects/artifacts.md | 1 + src/content/projects/loomcli.md | 1 + src/content/projects/mimir.md | 1 + src/content/projects/norn.md | 1 + src/pages/index.astro | 7 +++++-- src/pages/projects/[slug].astro | 5 ++++- src/pages/projects/index.astro | 5 ++++- 14 files changed, 56 insertions(+), 4 deletions(-) create mode 100644 public/projects/artifacts.svg create mode 100644 public/projects/loomcli.ico create mode 100644 public/projects/mimir.svg create mode 100644 public/projects/norn.svg create mode 100644 public/projects/sources.json diff --git a/public/projects/artifacts.svg b/public/projects/artifacts.svg new file mode 100644 index 0000000..e13acbf --- /dev/null +++ b/public/projects/artifacts.svg @@ -0,0 +1,8 @@ + + + + + + diff --git a/public/projects/loomcli.ico b/public/projects/loomcli.ico new file mode 100644 index 0000000000000000000000000000000000000000..404e79b02def9b9b31f35ec8e97150aad94d64eb GIT binary patch literal 4097 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 ZFIaM_e97b5;gJrcL82SljQ+ox{~y(0kbwXI literal 0 HcmV?d00001 diff --git a/public/projects/mimir.svg b/public/projects/mimir.svg new file mode 100644 index 0000000..88bd8f2 --- /dev/null +++ b/public/projects/mimir.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/projects/norn.svg b/public/projects/norn.svg new file mode 100644 index 0000000..df55b52 --- /dev/null +++ b/public/projects/norn.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/projects/sources.json b/public/projects/sources.json new file mode 100644 index 0000000..7fa0361 --- /dev/null +++ b/public/projects/sources.json @@ -0,0 +1,6 @@ +{ + "norn.svg": "https://github.com/dbtlr/norn-next/blob/ed25f3b1ac9884103649a1ce049451eeaa09d212/favicon.svg", + "mimir.svg": "https://github.com/dbtlr/mimir/blob/fd47e707c4961a87e6a143953d35eb4e7c98f62f/favicon.svg", + "loomcli.ico": "https://github.com/dbtlr/loomcli/blob/bc6381aae2cdee08542218a799c6a015c939c25a/favicon.ico", + "artifacts.svg": "https://github.com/dbtlr/artifacts/blob/4b8408602b74e152c0afa2eaf1d3abc93cdc9ba3/favicon.svg" +} diff --git a/scripts/check-projects.mjs b/scripts/check-projects.mjs index 53562d0..f1db101 100644 --- a/scripts/check-projects.mjs +++ b/scripts/check-projects.mjs @@ -32,4 +32,16 @@ for (const slug of [...current, 'saga']) { const mimir = await read('projects/mimir/index.html'); assert.match(mimir, /Markdown is the system of record/); assert.ok(!mimir.includes('SQLite'), 'Mimir must not describe the retired SQLite backend'); + +for (const [slug, extension] of [['norn', 'svg'], ['mimir', 'svg'], ['loomcli', 'ico'], ['artifacts', 'svg']]) { + const icon = `/projects/${slug}.${extension}`; + for (const page of [`projects/${slug}/index.html`, 'projects/index.html', + ...(['norn', 'mimir', 'loomcli'].includes(slug) ? ['index.html'] : [])]) { + const html = await read(page); + assert.ok(html.includes(`src="${icon}"`), `${page} must display the ${slug} icon`); + } + const bytes = await readFile(new URL(icon.slice(1), root)); + assert.ok(bytes.length > 0, `${icon} must be served locally`); +} + console.log('Built project pages and search satisfy the current/retired content contract.'); diff --git a/src/content.config.ts b/src/content.config.ts index 4be9a42..69bf5ab 100644 --- a/src/content.config.ts +++ b/src/content.config.ts @@ -21,6 +21,7 @@ const projects = defineCollection({ description: z.string(), short: z.string(), lang: z.string(), + icon: z.string().optional(), status: z.enum(['active', 'early', 'pre-alpha', 'retired']), license: z.string().optional(), order: z.number(), diff --git a/src/content/projects/artifacts.md b/src/content/projects/artifacts.md index 973e61a..bd2edb8 100644 --- a/src/content/projects/artifacts.md +++ b/src/content/projects/artifacts.md @@ -1,5 +1,6 @@ --- name: Artifacts +icon: /projects/artifacts.svg blurb: Persistent links for agent-generated documents, images, and PDFs. description: An MCP server and web interface that turn agent-generated documents, images, and PDFs into persistent links. Unversioned pre-alpha software for trusted networks. short: share agent output diff --git a/src/content/projects/loomcli.md b/src/content/projects/loomcli.md index a5f4b8c..7f0d480 100644 --- a/src/content/projects/loomcli.md +++ b/src/content/projects/loomcli.md @@ -1,5 +1,6 @@ --- name: Loom CLI +icon: /projects/loomcli.ico blurb: A TypeScript framework for command applications, with typed arguments, options, and schema validation. description: A TypeScript framework for command applications with named commands, typed inputs, Standard Schema validation, and passthrough arguments. short: command framework diff --git a/src/content/projects/mimir.md b/src/content/projects/mimir.md index 8ba5228..9b0b27f 100644 --- a/src/content/projects/mimir.md +++ b/src/content/projects/mimir.md @@ -1,5 +1,6 @@ --- name: mimir +icon: /projects/mimir.svg blurb: Local work state in a Norn-managed Markdown vault, with queues and status derived when queried. description: A local-first work-state engine over a Norn-managed, git-backed Markdown vault. The CLI, MCP server, HTTP API, and operator console share one work model. short: work state diff --git a/src/content/projects/norn.md b/src/content/projects/norn.md index 806135b..2eb05a1 100644 --- a/src/content/projects/norn.md +++ b/src/content/projects/norn.md @@ -1,5 +1,6 @@ --- name: norn +icon: /projects/norn.svg blurb: Your Markdown vault as a deterministic graph — query, validate, and repair from the shell. description: A Rust CLI and MCP server that turns a Markdown vault into a deterministic graph you can query, validate, and repair — for shells, scripts, and coding agents. short: vault tooling diff --git a/src/pages/index.astro b/src/pages/index.astro index 01281f6..5579d08 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -22,8 +22,11 @@ const projects = (await getCollection('projects', ({ data }) => data.status !== {projects.map((project) => ( - - {project.data.name} + + + {project.data.icon && } + {project.data.name} + {project.data.blurb} {project.data.lang} diff --git a/src/pages/projects/[slug].astro b/src/pages/projects/[slug].astro index e79b594..e9d4f10 100644 --- a/src/pages/projects/[slug].astro +++ b/src/pages/projects/[slug].astro @@ -24,7 +24,10 @@ const buttons = [ ← Projects
-

{project.data.name}

+

+ {project.data.icon && } + {project.data.name} +

{meta}

{project.data.tagline}

diff --git a/src/pages/projects/index.astro b/src/pages/projects/index.astro index 763704e..edcb015 100644 --- a/src/pages/projects/index.astro +++ b/src/pages/projects/index.astro @@ -17,7 +17,10 @@ const projects = (await getCollection('projects', ({ data }) => data.status !==
{project.data.demo}
-

{project.data.name}

+

+ {project.data.icon && } + {project.data.name} +

{project.data.description}

{project.data.lang} From 67b2a7551e95b993a3c257c81e6c693b64044554 Mon Sep 17 00:00:00 2001 From: Drew Butler Date: Thu, 10 Sep 2026 06:35:37 -0400 Subject: [PATCH 5/8] Lead Loom copy with its human and agent-native vision --- src/content/projects/loomcli.md | 27 +++++++++++++++------------ src/pages/about.astro | 2 +- src/pages/index.astro | 2 +- src/pages/projects/index.astro | 2 +- 4 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/content/projects/loomcli.md b/src/content/projects/loomcli.md index 7f0d480..499f30b 100644 --- a/src/content/projects/loomcli.md +++ b/src/content/projects/loomcli.md @@ -1,38 +1,41 @@ --- -name: Loom CLI +name: Loom icon: /projects/loomcli.ico -blurb: A TypeScript framework for command applications, with typed arguments, options, and schema validation. -description: A TypeScript framework for command applications with named commands, typed inputs, Standard Schema validation, and passthrough arguments. -short: command framework +blurb: A batteries-included, strongly typed CLI framework. Beautiful for humans and agent-native by default. +description: A batteries-included, strongly typed framework for building CLI applications that are beautiful for humans and agent-native by default. +short: human and agent-native CLIs lang: TypeScript status: early order: 3 -tagline: Typed declarations for command applications. +tagline: A batteries-included, strongly typed framework for building CLI applications that are beautiful for humans and agent-native by default. links: github: https://github.com/dbtlr/loomcli docs: https://github.com/dbtlr/loomcli/blob/main/docs/core.md releases: https://github.com/dbtlr/loomcli/releases facts: - label: Type - value: Command framework + value: CLI application framework - label: Validation value: Standard Schema - label: Maturity value: Early development demo: | - # From the Loom CLI repository + # From the Loom repository pnpm install --frozen-lockfile pnpm build node examples/textstat/dist/src/main.js --metric words README.md --- -Loom CLI is a TypeScript framework for building command applications. It supports named commands, global and local options, required scalar and variadic arguments, Standard Schema validation, and passthrough arguments. +## Why it exists -## Why it is useful +A CLI is an interface for people and, increasingly, for agents. Both deserve a deliberate design: an application that feels good to use in a terminal and gives an agent a clear, dependable way to work. -A command declaration describes the inputs an action receives. Loom CLI carries those types into the action, and schema validation checks and transforms values before the action runs. +Loom’s north star is to make that the default. A batteries-included framework should bring the pieces of a complete CLI application together, with strong types connecting its declarations, inputs, and actions. Human experience and agent interaction belong in the framework’s foundations, so application authors can focus on what their tools do. + +## What it does today + +Loom is in early development. Its TypeScript core establishes the typed command model that the broader vision builds on: -## What it does - Routes named commands under application-wide options. - Derives argument and option types from declarations. @@ -40,4 +43,4 @@ A command declaration describes the inputs an action receives. Loom CLI carries - Keeps passthrough arguments separate from parsed inputs. - Provides shared output and failure behavior. -Loom CLI is in early development. The textstat and jsonkit examples demonstrate the current scope. +The textstat and jsonkit examples demonstrate the current scope. diff --git a/src/pages/about.astro b/src/pages/about.astro index 539d54b..5fd0ecb 100644 --- a/src/pages/about.astro +++ b/src/pages/about.astro @@ -18,7 +18,7 @@ const elsewhere = [

I’m a product-focused engineer and engineering leader. Twenty years in, I’ve never stopped writing code. I think the best technical leaders keep their hands on the work, and it keeps me honest about the trade-offs. Lately I’m focused on agentic engineering: building the open-source tooling and workflows for how software gets made next.

I care about why something is being built, not just how. My best work happens close to the problem: talking to users, learning the workflow, making architectural calls that serve the product rather than the tech. I’ve led distributed teams since before remote was normal, and I’d still rather reach for the right boring tool than the trendy one.

-

Right now I’m building tools for the work around the code: keeping knowledge useful, tracking decisions and tasks, building command applications, and sharing results. That work spans norn, mimir, Loom CLI, Artifacts, and skald.

+

Right now I’m building tools for the work around the code: keeping knowledge useful, tracking decisions and tasks, building command applications, and sharing results. That work spans norn, mimir, Loom, Artifacts, and skald.

diff --git a/src/pages/index.astro b/src/pages/index.astro index 5579d08..c9e4674 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -14,7 +14,7 @@ const projects = (await getCollection('projects', ({ data }) => data.status !== Lately I’m building open-source tools for AI-native development: norn for Markdown vaults, mimir for work state, - Loom CLI for command applications, + Loom for human and agent-native CLIs, Artifacts for sharing agent output, and skald for git prose. Based in Brooklyn.

diff --git a/src/pages/projects/index.astro b/src/pages/projects/index.astro index edcb015..3744085 100644 --- a/src/pages/projects/index.astro +++ b/src/pages/projects/index.astro @@ -8,7 +8,7 @@ const projects = (await getCollection('projects', ({ data }) => data.status !==

Projects

-

Open-source tools for AI-native development. norn makes Markdown vaults queryable, mimir tracks work state, Loom CLI builds command applications, Artifacts shares agent output, and skald writes git prose.

+

Open-source tools for AI-native development. norn makes Markdown vaults queryable, mimir tracks work state, Loom brings a strongly typed foundation to human and agent-native CLIs, Artifacts shares agent output, and skald writes git prose.

{projects.map((project) => ( From bf9a12c06c7b47cd67842bbbccf37922af5971c0 Mon Sep 17 00:00:00 2001 From: Drew Butler Date: Thu, 10 Sep 2026 09:39:48 -0400 Subject: [PATCH 6/8] Replace homepage introduction with personal perspective --- src/pages/index.astro | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/pages/index.astro b/src/pages/index.astro index c9e4674..1ad13a9 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -9,14 +9,11 @@ const projects = (await getCollection('projects', ({ data }) => data.status !==

I build tools for AI-native development.

+

+ I’m Drew Butler, a product-focused engineer and engineering leader based in Brooklyn. I like working close to a problem: understanding what people need, figuring out what matters, and building something useful. +

- I’m Drew Butler, a product-focused engineer and engineering leader. Twenty years in, I still ship code every day. - Lately I’m building open-source tools for AI-native development: - norn for Markdown vaults, - mimir for work state, - Loom for human and agent-native CLIs, - Artifacts for sharing agent output, - and skald for git prose. Based in Brooklyn. + The way we build software is changing. Developers are no longer the only users of our tools. Agents are becoming users too. That affects how we design interfaces, workflows, documentation, and developer tools. I spend a lot of my time in TypeScript, building with agents, and creating open-source tools that help both people and agents get more done.

From 9a7b2cc7ab96dd7aa0c7e234edb8661e47953c21 Mon Sep 17 00:00:00 2001 From: Drew Butler Date: Thu, 10 Sep 2026 10:02:23 -0400 Subject: [PATCH 7/8] Describe the design principles behind the projects --- src/pages/projects/index.astro | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/pages/projects/index.astro b/src/pages/projects/index.astro index 3744085..098a056 100644 --- a/src/pages/projects/index.astro +++ b/src/pages/projects/index.astro @@ -4,11 +4,16 @@ import Base from '../../layouts/Base.astro'; const projects = (await getCollection('projects', ({ data }) => data.status !== 'retired')).sort((a, b) => a.data.order - b.data.order); --- - +

Projects

-

Open-source tools for AI-native development. norn makes Markdown vaults queryable, mimir tracks work state, Loom brings a strongly typed foundation to human and agent-native CLIs, Artifacts shares agent output, and skald writes git prose.

+

+ I build open-source tools that give people and agents predictable ways to work with software. They make rules explicit, provide clear guardrails, and produce results you can inspect and trust. +

+

+ Good tools should be forgiving when you make a mistake, but structured enough to keep you on the right path. They should have an obvious lifecycle, sensible defaults, and clear boundaries, so the person or agent using them can focus on the problem instead of figuring out how to use the tool. +

{projects.map((project) => ( From bfdb5f2b9d262377b1750477667602b756bd0a69 Mon Sep 17 00:00:00 2001 From: Drew Butler Date: Thu, 10 Sep 2026 10:35:06 -0400 Subject: [PATCH 8/8] Expand About biography with engineering approach and personal context --- src/pages/about.astro | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/pages/about.astro b/src/pages/about.astro index 5fd0ecb..7b8b1e8 100644 --- a/src/pages/about.astro +++ b/src/pages/about.astro @@ -16,9 +16,13 @@ const elsewhere = [

Hi, I’m Drew.

-

I’m a product-focused engineer and engineering leader. Twenty years in, I’ve never stopped writing code. I think the best technical leaders keep their hands on the work, and it keeps me honest about the trade-offs. Lately I’m focused on agentic engineering: building the open-source tooling and workflows for how software gets made next.

-

I care about why something is being built, not just how. My best work happens close to the problem: talking to users, learning the workflow, making architectural calls that serve the product rather than the tech. I’ve led distributed teams since before remote was normal, and I’d still rather reach for the right boring tool than the trendy one.

-

Right now I’m building tools for the work around the code: keeping knowledge useful, tracking decisions and tasks, building command applications, and sharing results. That work spans norn, mimir, Loom, Artifacts, and skald.

+

I’m a product-focused engineer and engineering leader who has never gotten very far from the code. I like being close enough to the work to understand where the abstractions break down, what the trade-offs actually cost, and whether the thing we are building is making anyone’s life better.

+

I care about why something is being built, not just how. My best work usually starts with understanding the people, workflow, and constraints around a problem, then finding the simplest technical shape that serves them well. I like systems that are understandable, tools that are predictable, and architecture that earns its complexity.

+

I’ve spent much of my career building products, platforms, and developer infrastructure with distributed teams. I still write a lot of TypeScript, and I tend to reach for boring, well-understood technology until a problem gives me a reason not to.

+

More recently, I’ve been focused on agentic engineering and on what changes when people are no longer the only users of our software. Agents need many of the same things developers do: clear interfaces, useful feedback, sensible defaults, explicit boundaries, and ways to recover when something goes wrong. I’m interested in building tools that work well for both.

+

That has pulled me toward the work around the code as much as the code itself: command-line applications, knowledge and task systems, workflows for preserving context and decisions, and infrastructure that makes complex work easier to reason about and repeat.

+

A lot of that work becomes open source. Partly because I like making useful things available to other people, and partly because building in public is a good forcing function. The interfaces have to make sense, the assumptions have to be visible, and the tool has to stand on its own without me there to explain it.

+

I live in Brooklyn, NY with my wife and our three young boys. Outside of work, a lot of my free time goes to family life, reading science fiction and fantasy, and watching more genre TV and movies than I can reasonably keep up with.