How and why Shortly was created
Disclaimer
Dear Reader — human or AI — a few honest notes before the text.
This article was written by Grok, curated by and in the style of Arenukvern’s (Anton) other articles. It grew out of the same conversations that built the repository. It also sits inside the repo the same way the ADRs, the FAQs, and the tests do — as part of the showcase itself.
Shortly began as a test project. It had to be small enough to finish, concrete enough that the decisions would stay visible, and real enough that a future human or agent could clone it and still understand the path that led there. The URL shortener is only the vehicle. The artefact being shown is the movement from a conversation and a scaffold document all the way to a living repository.
This is also one concrete example of why Skill Steward exists: not as something that runs inside the shortener, but as the culture and the procedure that turn a chat into a governed repo.
It is not a claim that this is the only right way to work with AI. It is simply a record of what was asked, what was produced, what was locked, and what broke the first time a real person used the service.
The first conversation is public:
URL Shortener Architecture & Scaffold.
If you want the durable why and how, they live in the repo:
- Charter: NORTH_STAR.md
- Decisions: docs/decisions/
- Why: DESIGN_FAQ.md
- How: DX_FAQ.md
- Code: Arenukvern/shortly
Thank you for reading.
Why this repository exists
For several years Anton had been circling the same family of problems from different sides:
- code is a translation — a PRD is already outdated the moment the code works
- FAQ as knowledge compression — why and how should not live in the same pile
- knowledge lifecycle — operational knowledge needs a beginning, iterations, and an end
- ethics as a bridge to AI
- skill_steward — how to teach a repository to be maintained
- agentic executables — libraries as programs an agent can install and use
Skill Steward was one attempt to hold that family together: charter, ADRs, FAQs, quality contracts, install path, and a later agent that no longer has the original chat.
A manifesto alone was never going to be enough. A test project was required.
Anton set the requirement simply: take those ideas and run them on something small enough to finish, yet concrete enough that the decisions would stay visible. A URL shortener looked almost too simple — and that is exactly why it worked. The product stayed tiny on purpose: public shorten and redirect, an admin key for inspect and mutate, soft TTL, and a full history of every attempt, including the failures.
What needed to be shown was never how to encode base-N. It was how a human and an AI decide what the product is not, how that decision becomes a document another agent can execute, how the repo then grows its own map, install path, steward and tests, and how the first real use changes an ADR.
One of Anton’s working principles had always been that the developer is a user too. If the install path, Swagger, or the first curl forced a new person to investigate the project for an hour, the showcase would already have failed — even if the encoder was perfect.
This article itself is the same kind of artefact. It was asked for inside the same loop. It is not a blog post taped onto the repo afterwards.
The first conversation
The repository did not begin as a GitHub template. It began as a conversation.
Anton sat down with Grok. Together they designed the thing as an architecture and scaffold document — not as a request to generate a .NET project. The share of that conversation is still public:
URL Shortener Architecture & Scaffold
Two files came out of that session and they remain in the tree:
| File | Role |
|---|---|
docs/scaffold_doc.md |
Architecture contract. Domain, schema, API, validation, tests, Docker, Dart, stewardship. |
docs/archive/prompts/scaffold_prompt.md |
Execution prompt. Same locked decisions, plus environment caveats and a sequential order. |
The opening of the scaffold doc still carries the cleanest statement of what was being asked:
This document is the single source of truth for the first working version. It is written so an agent can execute it sequentially with minimal ambiguity.
And it ends the way a real handoff should end:
If anything in the answers above needs a small correction before the implementing agent starts, say so now. Otherwise this document is ready to be handed over.
That was the print that mattered. Not a chat log pretending to be documentation — a contract an agent could execute, and a human could still refuse.
From Anton’s earlier writing on translation: this document would become history the moment the code worked. That was fine. The job of the scaffold was never to stay true forever. Its job was to be strict enough that the first version would not invent tenants, custom domains, cleanup workers, or a second database “just in case”.
That refusal list was already Skill Steward thinking, before any of the skills were installed.
Locked decisions as ethics
Before any service file was generated, Anton and Grok locked eight product decisions. Later those decisions became ADRs 0001–0008. This article will not paraphrase the implementations — the ADRs and the tests are the source of truth — but the ethic of the lock is the actual point of the test.
In short:
- One shared pool. No ownership of links in v1.
- Shorten and redirect are public. Inspect and mutate need
X-Admin-Key. - Sequential
Link.Idplus a pure base-N encoder. The same original URL always creates a new code. Custom aliases are allowed. - Soft expiry, checked only on read. Expired is
410and a page, never a deleted row. Every attempt is logged, including unknown codes. - SQLite + WAL. Nothing else in v1.
- Host, alphabet, and default TTL are configuration.
- Dart is a steward and operator surface, not the product runtime.
- The service must be testable without Docker. Docker is only the production-like path.
The non-goals were written first: no multi-tenancy, no custom domains, no geo graphs, no background sweeper, no multi-instance write scaling.
This was ethics, not taste.
If you do not write down what you refuse, an agent will fill the silence. It will add auth “for later”, a Redis cache “for scale”, a worker “for cleanliness”. All of those can be good ideas in some other project. In this one they would have turned it into a different product — and a worse test, because the reasoning would have been hidden inside extra features.
Limitations are how you keep trust when generation is cheap. That is the same bridge Anton had written about in developer ethics.
Then the document was handed to another agent
The next session started from two markdown files and no .NET SDK. Still the same calendar day.
Anton gave the implementing agent a deliberately small first prompt:
analyse scaffold_prompt.md and scaffold_doc.md. create AGENTS.md and plan what to do to complete scaffolding.
AGENTS.md was to be a map, not an encyclopedia. That was a locked rule in the plan: do not paste API schemas into the agent file. Point at the charter, the ADRs, the FAQs, and the code.
Then Grok produced the service: Shortly.Core (encoder, validators, expiry decision), Shortly.Api (Minimal API, EF, SQLite), unit tests, e2e tests, Docker, Dart CLI + MCP, NORTH_STAR, the eight ADRs, FAQ stubs.
The first native gate was the boring one: install the missing SDK, then dotnet test. If dotnet was missing, stop. Do not pretend the service works.
A second rule, taken from Anton’s own notes on working with AI, was applied: if you need a deterministic result, build a tool. Do not put the model into the gate. dotnet test and shortly steward validate are the gate. The agent may write them. The agent may not be them.
This was the moment a conversation became a repository: the chat was no longer required to run or to check the work.
The human pass — Skill Steward as the developer path
A scaffold that matched a document was still not a repository someone else could live in.
The next prompts Anton gave were no longer about product features. They were about how a person (or an agent) enters the project:
- Move scripts into
tools/— they are tools, not leftoverscripts/. - Replace the Makefile with a
justfile— it becomes the daily command hub. - Take install, status, and checks out of the README and into one bootstrap:
./install.sh(andinstall.ps1on Windows). - Look at how skill_steward itself is maintained, and do not invent a third style.
Then Anton asked for a sharper change:
add a relevant ADR, a local skill to maintain and curate this project, add Skill Steward skills and GitNexus into install. make Dart not optional — the local steward is Dart-based.
That became ADR 0009.
The split that had to stay clear was this:
| Path | What must exist |
|---|---|
| Product runtime | .NET + SQLite. No Dart, no Node, no skills. dotnet run is enough. |
| Developer / steward path | Dart, just, Skill Steward skills, GitNexus. ./install.sh then just check. |
Dart is required to develop the repo. It is not required to run the shortener. The API has no Dart dependency. That was already in ADR 0007; 0009 simply stopped pretending the steward was optional for people who work here.
A local skill, shortly-maintain, was added for this repo only. Skill Steward stayed general. GitNexus became the code-intelligence index so a later agent would not rename symbols with find-and-replace.
One Skill Steward skill failed to install because of an upstream YAML parse error. The correct fix was not to vendor a broken copy into Shortly. Anton cloned the steward repo, asked an agent to open a branch and send a PR, and left a note in ADR 0009 that the quality-contract procedure stays in that skill’s own spec.
That was the consumer/producer boundary the test was supposed to show: do not make the demo green by hiding the producer.
Then a person used it
This is the part that usually disappears from architecture posts, and the test would have been fake without it.
Anton opened Swagger and tried to shorten a real URL: https://xsoulspace.dev/last_answer, custom alias last_answer.
The first request died because the JSON contained curly quotes — “https://… instead of "https://…. System.Text.Json said 0xE2 is an invalid start of a value. That was not a framework bug. That was a human pasting into a form.
The second request died because of a trailing comma after an optional field:
{
"url": "https://xsoulspace.dev/last_answer",
"customAlias": "last_answer",
}
Optional + default TTL had sounded like “this object is fine”. Strict JSON meant the comma was the error, not the missing ttlSeconds. The API was doing the right thing. The person was not.
Then a real product argument appeared.
last_answer had been created with a short TTL. It expired. Creating last_answer again returned “Custom alias is already in use.”
The first implementation had treated Link.Code as globally unique. Soft expiry keeps the row. A unique index then means a vanity name is reserved forever.
That fought three locked decisions at once: shared pool (no ownership), soft expiry (row stays), and “same destination always creates a new row”. In a pool with no sweeper, forever-reserved aliases become a silent land grab.
Anton and Grok argued it, then wrote it down:
- uniqueness is among active holders
- expired or deleted generations stay for history
- reuse inserts a new row, with its own stats
- public
GET /{code}uses the latest generation - an e2e test now says this out loud
ADR 0003 was updated. The first scaffold had not been wrong to say “unique”. It had been incomplete once expiry existed. The working code found a nuance the document did not have, and the durable record moved from the scaffold into the ADR and the test.
Two more developer-as-user bugs, both from Swagger:
-
GET /api/v1/statsreturned Unauthorized because the generated curl had noX-Admin-Key. In Development, Try it out now attaches the configured key. The FAQ says how. -
GET /{code}looked like “Undocumented / TypeError: Load failed”. The API had returned 302. Swagger’sfetchfollowsLocationinto another origin, and Safari reports a CORS failure as a broken endpoint. The shortener worked. The explorer lied.
These mattered more than the encoder for the showcase, because this is how a new person meets the project.
What this is showing
Shortly is a URL shortener. It is also a Skill Steward example, run far enough that the conversation is no longer the source of truth.
The sentence underneath everything:
Code is the translation. Docs should not try to be a second implementation.
The first conversation produced a scaffold. The scaffold produced a service. The service, once used, produced corrections. The corrections went into ADRs, FAQs, and tests — not back into a 5,000-line spec. This article is the same kind of extraction: a story of how and why, not a second encoder.
The standing questions a later agent should ask are the FAQ ones:
- Why is there no ownership of links?
- Why sequential IDs instead of hashing the URL?
- Why 410 instead of deleting the row?
- Why can an expired custom alias be reused?
- Why is Dart required to develop the repo, and still absent from the API?
If the answer is “because the scaffold said so”, that is not enough. If the answer is a locked ADR plus a test, that is enough.
The install path is one command for the same reason: a repository that needs a private wiki to boot is not a useful test of stewardship. It is a memory test.
./install.sh
just check
just run
That is the supported developer path. The product path is still dotnet run and a SQLite file.
Skill Steward is useful here because that split can be written down, installed, and checked — instead of living only in the people who were in the chat.
How to read the repository
If you clone Arenukvern/shortly and you are a person:
- Read NORTH_STAR.md — purpose and non-goals.
- Skim the ADRs — especially 0002, 0003, 0004, 0009.
- Use DX_FAQ.md when you want to run it.
- Treat
src/Shortly.Core+ tests as the behavior. - Treat this article as the story of how and why the repo was created — not as SSOT.
If you are an agent: start at AGENTS.md. Do not invent features. Do not reopen ADRs 0001–0008 unless a human changes a locked product decision.
The original execution prompt is archived on purpose. It is history. The living contract is the code plus the ADRs plus the FAQs.
What this is not claiming
A test project was required to show how exactly such projects can be built, including the decisions behind them, and how skill steward manages the whole thing. Decisions were written down, including the refusals. One conversation produced a handoff document. Another agent executed it. A person used the service, and the first real URL (last_answer) changed an ADR.
That loop — decide, document, run, correct, extract, delete the plan — is the point of treating a repository as an agent-operated artefact, and the reason Skill Steward is in the install path.
If that sounds more like stewardship than like a shortener, yes. The short links are how we know the stewardship is not only a folder of markdown.
Please share what you think in the comments. It helps other people find the thread.
Thank you for your time.
Written by Grok, curated and in the style of Arenukvern’s articles, from our conversations.
Links
- Repo: github.com/Arenukvern/shortly
- Origin conversation: grok.com/share/…
- Scaffold contract: docs/scaffold_doc.md
- Execution prompt (archive): docs/archive/prompts/scaffold_prompt.md
- Skill Steward: github.com/arenukvern/skill_steward
- Earlier notes this test sits on:
Top comments (0)