syv.ai logo

Artikel · 10. august 2026 · 7 min

Softwaredestillering

Mads Henrichsen

I februar havde vi brug for et værktøj, der kunne køre Claude Code på flere opgaver samtidig, hver opgave i sin egen git worktree. Der fandtes allerede noget i den retning, emdash fra General Action. Vi brugte et par dage på at teste det og skrev derefter vores eget. Det hedder Dash, det er open source, og det fylder cirka en femtedel af emdash.

Ordet fork passer dårligt på den fremgangsmåde, for vi arvede ikke en linje kode, og "skrevet fra bunden" er heller ikke helt rigtigt, når arkitekturen i vid udstrækning er emdashs. Vi er begyndt at kalde det destillering. Man koger en eksisterende kodebase ned til de beslutninger, der viste sig at holde, og bygger sit eget oven på dem.

Hvad vi arvede

Emdash løser et reelt svært problem ordentligt. Hver opgave får sin egen worktree og sin egen branch, så flere agenter kan arbejde parallelt uden at træde hinanden over tæerne. Det lyder simpelt, indtil man selv skal bygge det, og emdash indeholder en række detaljer, som man kun opdager, når man har haft produktet ude hos rigtige brugere. Worktrees skal allokeres på forhånd i en pulje. Bliver en worktree først oprettet i det øjeblik, brugeren opretter opgaven, føles appen langsom nok til, at folk holder op med at oprette opgaver. Der skal tages snapshot af terminalsessionerne, og de skal gendannes igen, når man skifter mellem opgaver, ellers mister man sin scrollback, hver gang man kigger et andet sted hen. Setup-scripts skal køre automatisk per projekt, når en worktree bliver oprettet, for en frisk worktree har hverken node_modules eller .env, og skal brugeren selv huske det, er halvdelen af tidsgevinsten væk igen. Og diffs, staging og commits hører hjemme i samme app som agenten, ellers skifter man kontekst hele tiden.

Vi kopierede ikke en linje af det. Vi slap for at træffe de beslutninger forkert først, og de fire er stadig fundamentet i Dash.

Det, vi skar væk

Emdash er bygget til at understøtte stort set alt. På agentsiden Claude Code, Codex, Cursor, OpenCode, Amp, Devin, Qwen Code, Droid og GitHub Copilot. På issue-siden Linear, GitHub, Jira, GitLab, Asana, Featurebase, Monday.com, Forgejo og Plain. Dertil lokale projekter og fjernmaskiner via SSH. Det er en fornuftig prioritering for et YC-støttet produkt, der skal ramme et bredt marked.

Vi bruger Claude Code, og det er det eneste, Dash understøtter. Forskellen i omfang ser sådan her ud.

MålemdashDash
TypeScript-linjer~338.000~63.000
.ts/.tsx-filer2.889412
Pakker9 (nx/pnpm-monorepo)1
Runtime-afhængigheder34 (desktop-appen alene)23
Understøttede agenter91
emdash2.889 filer338.000Dash412 filer63.000produktkoderuntime og plugins, cirka 28.300 linjer
TypeScript-linjer tegnet til skala. Den amber del er emdashs runtime- og plugin-lag, som kun findes for at understøtte ni agenter og et dusin issue-systemer.

Den tydeligste enkeltpost er emdashs packages/runtime, omkring 9.300 linjer, hvis eneste opgave er at lægge et abstraktionslag over agentprotokoller. ACP-agenter, TUI-agenter og agentkonfiguration. Dertil kommer godt 19.000 linjer i packages/plugins til integrationer og agentregistry. De linjer er velinvesterede for emdash. Vi har kun én agent at understøtte, og et interface, der beskriver, hvad "en agent" er i almindelighed, løser ikke noget problem, vi har.

Vi droppede også SSH og fjernmaskiner, de fleste issue-integrationer og monorepo-strukturen. Emdash kan både køre agenter i en terminal og rendere sit eget chat-UI oven på ACP-protokollen. Vi beholdt terminalen, altså xterm.js og node-pty, og droppede chat-laget.

Der blev plads til dybde

Da abstraktionslaget var væk, kunne vi bygge ting, som en agentagnostisk app har svært ved at bygge.

HookServer.ts er en lille HTTP-server, der kører lokalt. Dash skriver en .claude/settings.local.json ind i hver worktree, og den får Claude Code til at kalde serveren ved ti hændelser: SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, Notification, PreCompact, PostCompact, Stop, StopFailure og SessionEnd. Derfra ved Dash, om en opgave arbejder eller venter på input. Alternativet er at gætte ud fra det, der løber over terminalen, og det rammer tit nok skævt til at være irriterende. Samme fil styrer statusLine, commit-attribution og forhåndsgodkendelse af de MCP-servere, projektet selv har tjekket ind i sin .mcp.json.

ContextUsageService.ts læser den JSON, Claude Codes statusLine sender tilbage, og viser, hvor meget af kontekstvinduet der er brugt, hvad sessionen indtil videre har kostet i dollars, og hvor tæt man er på sine rate limits over fem timer og syv dage. Det sidste er værd at dvæle ved, for rate limits hører til Claude og ikke til agenter i almindelighed. En app, der lægger ét abstraktionslag over ni agenter, har ikke noget felt at putte dem i. Emdash kunne selvfølgelig godt bygge det specifikt ind til Claude Code, men så begynder abstraktionen at lække, og det er den slags kompromis, bredden bliver ved med at fremtvinge.

Vi har desuden bygget SessionWatcherService, der tailer Claude Codes JSONL-sessionsfiler for at bygge metrikker, en skills-registry, der kan installere skills direkte i .claude/skills, plugin-håndtering, effort-niveauer og portstyring per worktree. Hver gang vi har spurgt os selv, hvordan en af dem kunne laves generisk, er svaret blevet at lade være.

Arvet som beslutninger
  • Worktrees allokeret i en pulje på forhånd
  • Snapshot og gendannelse af terminalsessioner
  • Setup-scripts per projekt ved ny worktree
  • Diffs, staging og commits i samme app
Skåret væk
  • Otte af ni agenter
  • Alle issue-integrationer
  • SSH og fjernmaskiner
  • ACP-laget og det egne chat-UI
  • Monorepo-strukturen
Bygget oven på
  • HookServer med ti Claude Code-hooks
  • Kontekstforbrug, pris og rate limits
  • SessionWatcher på JSONL-sessionsfiler
  • Skills-registry og plugin-håndtering
Nedskaleringen er forarbejdet. Værdien ligger i den tredje kolonne.

Bredde koster dybde

Man kunne læse ovenstående, som om vi har bygget en fattigere emdash. Dash kan ting, emdash ikke kan, netop fordi vi gav afkald på alt det, emdash kan. Hver gang man understøtter en integration mere, rykker koden et lag længere væk fra det konkrete. Abstraktionen koster linjer og kompleksitet, men dybden er den dyreste post, for man kan ikke integrere tæt med noget, man har lagt et lag oven på.

Destillering bytter altså bredde ud med dybde. Det er en fin handel, hvis man ved præcis, hvem der skal bruge tingen. Ved man det ikke, ender man med en tilfældigt amputeret udgave af noget, der virkede fint i forvejen.

Hvorfor det er blevet billigt

Folk har læst hinandens kode og skrevet bedre udgaver, siden der var kode at læse. Det nye er prisen på det første skridt. Den dyre del har altid været at forstå fremmed kode godt nok til at vide, hvad der betyder noget. 338.000 linjer TypeScript, man ikke selv har skrevet, er ugers eller måneders læsning, og bagefter står man stadig med en ufuldstændig model af, hvorfor tingene ser ud, som de gør.

Den del er blevet billig. En kodeagent kan læse en hel kodebase igennem, beskrive arkitekturen lag for lag og forklare, hvorfor et bestemt abstraktionslag findes. Det holder ikke hver eneste gang, men det flytter "forstå kodebasen" fra uger til timer.

Det ændrer regnestykket bag build-vs-buy-vs-fork. Køber man, arver man en andens kompromiser. Forker man, arver man en andens vedligehold, og bygger man fra bunden, bruger man tre måneder på at genopdage ting, nogen allerede havde fundet ud af. Destillering ligger et sted midt imellem. Man arbejder videre på andres indsigter, men i sin egen kodebase.

Dash gik fra første commit den 10. februar til version 0.15.0 i starten af juli, 300 commits fordelt på tre faste udviklere plus et par udefrakommende bidrag. Vi brugte i øvrigt Claude Code til at destillere en app, hvis eneste formål er at køre Claude Code.

Hvis man selv vil prøve

Vælg kilden efter, hvilket svært problem den har løst. Den behøver ikke ligne det, du vil bygge. For os var det worktree-isolation og PTY-håndtering, altså de to ting, vi var mest nervøse for at skulle bygge selv.

Kortlæg, før du skriver noget. Få arkitekturen beskrevet lag for lag, og stil så det spørgsmål, der gør arbejdet: hvilke dele af det her findes kun for at understøtte noget, jeg ikke skal bruge? Svarene er dine snitflader. Skriv derefter listen over det, du ikke skal bruge, før du skriver listen over det, du skal. Den er nemmere at lave, og den bliver længere, end du regner med.

Start i en tom mappe. Fristelsen er at klone og slette, og det er den fejl, vi ville advare hårdest imod. Sletter man sig frem, arver man strukturen, og så sidder man med en arkitektur, ingen på holdet kan forsvare, fordi ingen på holdet har valgt den. Dash ligger i én enkelt pakke med Zustand-stores og en tynd IPC-flade. Emdash er et nx-monorepo med react-query.

Byg derefter det, kilden ikke kunne. Det er der, det meste af værdien ligger. Selve nedskaleringen er kun forarbejdet. Og accepter, at du er på egen hånd fra dag ét. Man kan ikke hente ændringer fra upstream, når man har skrevet sin egen kodebase, og man skal heller ikke prøve. Vælg det bevidst fra starten, så det ikke er noget, du opdager et halvt år senere.

Vi bruger Dash hver dag. 63.000 linjer, som holdet selv har skrevet. Vi skylder emdash et halvt års arkitekturarbejde, vi slap for selv at lave.

Dash: github.com/syv-ai/dash (MIT). Emdash: github.com/generalaction/emdash (Apache 2.0).

Spørgsmål?

Kontakt os og kom i gang med AI

Kontakt os