{
  "generatedAt": "2026-08-15T17:38:26.380Z",
  "statuses": [
    "now",
    "next",
    "later",
    "inbox",
    "done"
  ],
  "counts": {
    "now": 5,
    "next": 9,
    "later": 11,
    "inbox": 10,
    "done": 62
  },
  "total": 97,
  "sources": [
    {
      "repo": "tor2dbear/roadmap",
      "name": "Roadmap",
      "blurb": "This board's own backlog — the aggregator tracking itself.",
      "color": "#94a3b8",
      "url": "https://github.com/tor2dbear/roadmap",
      "adapter": "pucks",
      "backend": "fs",
      "count": 8,
      "native": true,
      "error": null
    },
    {
      "repo": "tor2dbear/pia-terminal",
      "name": "PIA",
      "blurb": "Personal Integrated Applications — a Unix-flavoured terminal in the browser.",
      "color": "#38bdf8",
      "url": "https://github.com/tor2dbear/pia-terminal",
      "adapter": "pucks",
      "backend": "fs",
      "count": 51,
      "native": true,
      "error": null
    },
    {
      "repo": "tor2dbear/cadence",
      "name": "Cadence",
      "blurb": "A motion *system* designer — tokens for timing & easing, not another easing toy.",
      "color": "#f472b6",
      "url": "https://github.com/tor2dbear/cadence",
      "adapter": "pucks",
      "backend": "fs",
      "count": 10,
      "native": true,
      "error": null
    },
    {
      "repo": "tor2dbear/meta-matic",
      "name": "Méta-Matic",
      "blurb": "A digital drawing machine after Jean Tinguely — infinite works, never anything new.",
      "color": "#a78bfa",
      "url": "https://github.com/tor2dbear/meta-matic",
      "adapter": "pucks",
      "backend": "fs",
      "count": 18,
      "native": true,
      "error": null
    },
    {
      "repo": "tor2dbear/tor2dbear.com",
      "name": "Workshop",
      "blurb": "The apex site and the rule book for the fleet.",
      "color": "#34d399",
      "url": "https://github.com/tor2dbear/tor2dbear.com",
      "adapter": "pucks",
      "backend": "fs",
      "count": 1,
      "native": true,
      "error": null
    },
    {
      "repo": "tor2dbear/portfolio",
      "name": "tor-björn.com",
      "blurb": "Personal Hugo site — design system, typography, terminal, header/footer redesign.",
      "color": "#fb923c",
      "url": "https://github.com/tor2dbear/portfolio",
      "adapter": "pucks",
      "backend": "fs",
      "count": 9,
      "native": true,
      "error": null
    }
  ],
  "items": [
    {
      "id": "roadmap/align-with-fleet-cloudflare",
      "repo": "tor2dbear/roadmap",
      "repoName": "Roadmap",
      "repoColor": "#94a3b8",
      "slug": "align-with-fleet-cloudflare",
      "title": "Linjera med fleet-konventionen (Cloudflare + roadmap.tor2dbear.com)",
      "status": "now",
      "tags": [
        "hosting",
        "ops"
      ],
      "updated": "2026-08-15",
      "issue": null,
      "order": 5,
      "body": "## Mål\nFlytta roadmap från GitHub Pages till Cloudflare enligt `tor2dbear.com/CONVENTIONS.md`\n(Cloudflare är house-standard; roadmap var listad som \"to move to CF\").\n\n## Levererat\n- `wrangler.jsonc` (Pattern B, static-assets Worker, ingen build) som claimar\n  `roadmap.tor2dbear.com` via `routes`.\n- `.assetsignore` som håller `scripts/`, config och docs ur bundeln.\n- `wrangler` pinnad i `package.json` + `dev`/`deploy`/`versions:upload`-scripts.\n- Workflowen deployar inte längre till Pages — den skördar och committar data till\n  `main`; varje push till `main` (inkl. datacommits) triggar en Workers Build.\n- `.nojekyll` borttagen; docs uppdaterade.\n\n## Kvar (kräver dashboard — kan inte automatiseras headless)\n- Cloudflare → Workers & Pages → Create → **Import a repository** → `tor2dbear/roadmap`.\n  Projektnamn `roadmap`, prod-branch `main`, tom build, deploy `npx wrangler deploy`,\n  branch builds på. Första deployen skapar custom domain + DNS ur `routes`.\n- Verifiera att `roadmap.tor2dbear.com` svarar, sen avveckla GitHub Pages.\n- Uppdatera fleet-tabellen i `tor2dbear.com/CONVENTIONS.md` (Roadmap-raden → CF).",
      "sourcePath": "roadmap/align-with-fleet-cloudflare.md",
      "sourceUrl": "https://github.com/tor2dbear/roadmap/blob/main/roadmap/align-with-fleet-cloudflare.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "pia-terminal/email-verification-lazy",
      "repo": "tor2dbear/pia-terminal",
      "repoName": "PIA",
      "repoColor": "#38bdf8",
      "slug": "email-verification-lazy",
      "title": "Lazy email verification (gate claim, not signup)",
      "status": "next",
      "tags": [
        "auth",
        "share"
      ],
      "updated": "2026-08-08",
      "issue": null,
      "order": 5,
      "body": "## Mål\nBehåll den friktionsfria terminal-signup:en (`useradd` loggar in direkt, inget\nmejlhopp) **och** stäng invite-kapningshålet: en delning adresserad till en\ne-post ska bara kunna accepteras av någon som bevisligen kontrollerar den\ninkorgen. Vi verifierar **lat** — exakt när det först spelar roll (att claima en\ndelning), aldrig i förväg.\n\n## Bakgrund / varför\nSupabase-projektet har **\"Confirm email\" AV** (medvetet — se\n`shared-file-roles.md` och signup-UX:en). Effekten: `signUp` ger session direkt\noch `email_confirmed_at` sätts alltid → adressen är i praktiken **overifierad**.\nHela delningsmodellen litar dock på just e-posten:\n\n```sql\n-- claim_invites():\nv_email := lower(auth.jwt() ->> 'email');\n-- ... gör om alla shared_list_invites med den e-posten till medlemskap\n```\n\nHål: registrerar någon `victim@x` (utan att äga adressen) och en ägare kör\n`todo share lista victim@x`, så claimar spoofaren delningen vid nästa boot.\n\n**Nyckelinsikt:** verifiering behöver *inte* gate:a inloggning eller personligt\nbruk — bara **claim** (och ev. att bjuda in). Därför kan friktionsfriheten vara\nkvar överallt annars. Supabases inbyggda confirm förblir **AV** (den skulle\nblockera sessionen igen); vi lägger ett eget, lat lager ovanpå.\n\n## Hur det funkar\n\n### Signup — oförändrat\n`useradd piaowner … pw` → konto skapat, inloggad direkt. Enda tillägget en dim\nrad:\n```\naccount created — logged in as piaowner\ntip: run `verify` to confirm your email (needed to accept lists shared with you)\n```\n\n### Invite — två fall\n- **A. Inbjuden klickar den brandade länken (tänkta flödet):** oförändrat.\n  Länksessionen bevisar inkontroll → markeras **auto-verifierad** vid boot →\n  inviten auto-claimas → listan dyker upp. Noll extra steg.\n- **B. Har redan ett lösenordskonto på adressen, klickade aldrig länken:** vid\n  `shared`/reload ser de nudgen istället för tyst claim:\n  ```\n  1 list was shared with pia-viewer-durable@… — run `verify` to accept it\n  ```\n  `verify` → brandad engångskod → `verify <kod>` bevisar inkontroll → nästa\n  claim går igenom. Den lilla friktionen **är** säkerhetsgränsen, och drabbar\n  bara den som inte kom via länken.\n\n## Implementation\n\n### 1. Supabase-migration (`supabase/email_verification.sql`)\nServer-betrodd flagga som användaren **inte** kan sätta själv (därför inte\n`user_metadata`, som är användar-skrivbar):\n\n```sql\ncreate table if not exists public.email_verifications (\n  user_id     uuid primary key references auth.users(id) on delete cascade,\n  verified_at timestamptz not null default now()\n);\nalter table public.email_verifications enable row level security;\n-- Läs egen rad (klienten visar verifierad-status). Ingen write-policy = inga\n-- direkta skrivningar; bara definer-funktionen nedan skriver.\ncreate policy \"read own verification\" on public.email_verifications\n  for select using (user_id = auth.uid());\n\n-- Bevis = NUVARANDE session föddes ur ett mejlat länk/kod (amr), inte bara\n-- lösenord. amr-form bekräftad mot en riktig JWT:\n--   [{\"method\":\"password\",\"timestamp\":1786172313}]\n-- OTP/magic-link-sessioner bär method 'otp' (äldre builds: 'magiclink') — vi\n-- accepterar en mängd för att inte vara sköra på exakt sträng.\ncreate or replace function public.confirm_email_control()\nreturns void language plpgsql security definer set search_path = '' as $$\nbegin\n  if auth.uid() is null then raise exception 'not authenticated'; end if;\n  if not exists (\n    select 1 from jsonb_array_elements(coalesce(auth.jwt() -> 'amr', '[]'::jsonb)) e\n    where e ->> 'method' in ('otp','magiclink','email','email_otp')\n  ) then\n    raise exception 'email control not proven — sign in via the emailed link/code';\n  end if;\n  insert into public.email_verifications (user_id)\n    values (auth.uid()) on conflict (user_id) do nothing;\nend $$;\nrevoke execute on function public.confirm_email_control() from public, anon;\ngrant execute on function public.confirm_email_control() to authenticated;\n\n-- Grandfathera befintliga konton så de inte plötsligt låses ute från claim.\ninsert into public.email_verifications (user_id)\n  select id from auth.users on conflict (user_id) do nothing;\n```\n\nOch **en enda guard** överst i `claim_invites()` (resten av kroppen orörd):\n```sql\n  if v_uid is null or v_email is null then return 0; end if;\n  -- NY: overifierad e-post claimar ingenting (delnings-trust-roten).\n  if not exists (select 1 from public.email_verifications where user_id = v_uid)\n  then return 0; end if;\n```\n\n### 2. Auth-adapter (`src/supabase/auth.ts`)\n- `register()` fortsätter returnera session (confirm av).\n- `sendEmailCheck()` → `signInWithOtp({ email, options:{ shouldCreateUser:false }})`\n  (brandad kod, samma SMTP som invite).\n- `submitEmailCheck(code)` → `verifyOtp({ email, token:code, type:'email' })`\n  → sen `rpc('confirm_email_control')`.\n- `isEmailVerified()` → `select` mot `email_verifications`.\n- Vid boot efter en OTP-landning: best-effort `confirm_email_control()`\n  **före** `claim` (auto-verifierar fall A).\n\n### 3. Kommando (`src/commands/auth.ts`)\nNytt `verify` (alias `confirm`): `verify` skickar kod, `verify <kod>` löser in →\n\"email verified ✓\". `useradd`-hinten ovan. **Idiom-not:** `verify` saknar\nUnix-motsvarighet — accepterad web-auth-divergens, samma familj som\nemail+password+bekräftelse-flödet. Flagga i `CLAUDE.md`.\n\n### 4. Boot (`src/main.ts`)\nI claim-blocket: kör `confirm_email_control()` först (fångar länk-landningar);\nfinns pending invites för min e-post men jag är overifierad → skriv nudgen\nistället för tyst claim.\n\n### 5. UI-bonus\nVisa verifierad-status i `whoami` och `todo members` (gör medlemslistan mer\ntrovärdig).\n\n## Risker / avvägningar\n- **Fler rörliga delar** (tabell + RLS + 2 definer-funktioner + kommando + boot +\n  tester + tour) mot dagens \"av, klart\".\n- **`amr`-beroendet.** Vi litar på GoTrues amr-claim. Struktur + `password`-label\n  bekräftade live; **exakt OTP-sträng kvar att pinna mot en riktig OTP-JWT**\n  (blockerades i testsessionen av AgentMail-approval). Whitelist-mängden mildrar,\n  men kör en gång: `signInWithOtp` → `verifyOtp` → dumpa `jwt.amr`.\n  *Alternativ som slipper amr helt:* per-invite-token (slumptoken i invite-raden\n  + i länken, claim kräver token) — men mer plumbing i mejl/redirect och täcker\n  bara invite-hålet, inte en återanvändbar verifierad-status.\n- **Ett extra steg för lösenordskonto-som-bjuds-in** (oftast osynligt — de kommer\n  via länken ändå).\n- **Mejlleverans blir bärande för en säkerhetsväg** (SMTP-avbrott = kan inte\n  verifiera = kan inte claima). Custom SMTP testad OK.\n- **Grandfathering får inte glömmas** (backfill-raden).\n- **Två \"confirmed\"-sanningar**: Supabases `email_confirmed_at` (alltid satt,\n  meningslös här) vs vår `email_verifications`. Dokumenteras.\n\n## Vad vi vinner / missar\n**Vinner:** stänger spoof-/invite-kapningshålet där det spelar roll; friktionsfri\nsignup + solo-bruk 100 % kvar; lat verifiering; primära invite-vägen känns\noförändrad; en riktig verifierad-status att visa. **Missar (medvetet):** inte\n\"verifierad vid signup\" — ett konto kan köra med en adress det inte äger, det kan\nbara inte *ta emot delningar* dit. Vill man blockera själva registreringen krävs\nconfirm-vid-signup (det hårda stoppet vi undviker). Roller/read-only berörs inte\n(sitter på `user_id` + RLS, redan solida) — scope hålls smalt.\n\n## Öppna frågor\n- Ska `invite_to_list` kräva att **avsändaren** är verifierad? (v1: nej — delning\n  gate:as redan av list-ägarskap; verifiering gate:ar mottagarsidan/claim.)\n- `verify` via **kod** (`verify 483920`, stannar i prompten — mest terminal-nativt)\n  vs **länk** (klick lämnar terminalen men auto-verifierar). Luta åt kod.\n- Pinna exakt `amr`-method-sträng för OTP mot v2.195 GoTrue innan release.\n- Ev. utgångstid på verifiering (om man vill kunna av-verifiera vid e-postbyte).",
      "sourcePath": "roadmap/email-verification-lazy.md",
      "sourceUrl": "https://github.com/tor2dbear/pia-terminal/blob/main/roadmap/email-verification-lazy.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "roadmap/auto-status-from-reality",
      "repo": "tor2dbear/roadmap",
      "repoName": "Roadmap",
      "repoColor": "#94a3b8",
      "slug": "auto-status-from-reality",
      "title": "Auto-status från verkligheten",
      "status": "next",
      "tags": [
        "core",
        "sync"
      ],
      "updated": "2026-08-15",
      "issue": null,
      "order": 10,
      "body": "## Mål\nTa bort resten av status-adminet: härled status/färskhet från riktiga signaler i\nstället för självrapportering.\n\n## Research\n- `done` automatiskt när kopplat `issue:` stängs (eller en PR som nämner pucken mergas).\n- Flagga en `now`-puck som \"kanske vilande\" om inga commits rört området på X dagar.\n- Kräver att harvestern läser issue/PR-status + senaste commit-datum per puck/område.\n  Enklast via GitHub API i harvest-steget (token finns i CI).\n\n## Öppna frågor\n- Hur koppla puck → kodområde? Via `tags`, en sökväg i frontmatter, eller `issue:`?\n- Auto-ändra status, eller bara *flagga* avvikelser och låta människan bekräfta?",
      "sourcePath": "roadmap/auto-status-from-reality.md",
      "sourceUrl": "https://github.com/tor2dbear/roadmap/blob/main/roadmap/auto-status-from-reality.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "cadence/bring-your-own-markup",
      "repo": "tor2dbear/cadence",
      "repoName": "Cadence",
      "repoColor": "#f472b6",
      "slug": "bring-your-own-markup",
      "title": "Custom probes: bring your own markup",
      "status": "next",
      "tags": [
        "bench"
      ],
      "updated": "2026-08-15",
      "issue": null,
      "order": 10,
      "body": "## Mål\nLåta användaren peka benchen mot sin *egen* markup, inte bara de abstrakta instrumenten.\n\n## Research\nDelvis där redan: en utbytbar uppsättning abstrakta instrument i benchen + en full\n`demo.html`-yta med riktiga komponenter. Det som återstår är \"bring your **own**\nmarkup\" — att koppla in godtycklig HTML som en probe.",
      "sourcePath": "roadmap/bring-your-own-markup.md",
      "sourceUrl": "https://github.com/tor2dbear/cadence/blob/main/roadmap/bring-your-own-markup.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "cadence/draggable-bezier-editor",
      "repo": "tor2dbear/cadence",
      "repoName": "Cadence",
      "repoColor": "#f472b6",
      "slug": "draggable-bezier-editor",
      "title": "Draggbar bézier-editor",
      "status": "done",
      "tags": [
        "editor"
      ],
      "updated": "2026-08-15",
      "issue": null,
      "order": 10,
      "body": "## Mål\nFörfatta easing-kurvor direkt genom att dra, inte bara välja bland presets.\n\n## Levererat\nEn draggbar bézier-editor i benchen — kurvorna är redigerbara handtag, inte fasta val.",
      "sourcePath": "roadmap/draggable-bezier-editor.md",
      "sourceUrl": "https://github.com/tor2dbear/cadence/blob/main/roadmap/draggable-bezier-editor.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "roadmap/add-portfolio-source",
      "repo": "tor2dbear/roadmap",
      "repoName": "Roadmap",
      "repoColor": "#94a3b8",
      "slug": "add-portfolio-source",
      "title": "Lägg till portfolio (tor-björn.com) på tavlan",
      "status": "done",
      "tags": [
        "sources"
      ],
      "updated": "2026-08-15",
      "issue": null,
      "order": 10,
      "body": "## Mål\nFå med portfolio-sajten på brädet.\n\n## Levererat\nPortfolio (`tor-björn.com`) har nu en native `roadmap/` (PR #295) och är tillagd\nsom källa i `sources.json` (adapter `pucks`, `branch: master`). Cadence migrerades\nsamtidigt till native pucks (PR #61), så alla källor är nu native — inga adaptrar\nkvar. tor2dbear.com (\"Workshop\") finns som separat källa.",
      "sourcePath": "roadmap/add-portfolio-source.md",
      "sourceUrl": "https://github.com/tor2dbear/roadmap/blob/main/roadmap/add-portfolio-source.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "roadmap/multi-repo-board",
      "repo": "tor2dbear/roadmap",
      "repoName": "Roadmap",
      "repoColor": "#94a3b8",
      "slug": "multi-repo-board",
      "title": "Multi-repo roadmap-aggregator + tavla",
      "status": "done",
      "tags": [
        "core"
      ],
      "updated": "2026-08-15",
      "issue": null,
      "order": 10,
      "body": "## Mål\nSamla roadmaps från flera repon till ett bräde + en maskinläsbar aggregat, i synk.\n\n## Levererat\nHarvester (`scripts/harvest.mjs` + `lib/`) med fs/GitHub-backends och adaptrar\n(pucks/checklist/prose). Genererar `data/roadmap.json` (för AI), `data/roadmap.js`\n(file://-tavla) och `ROADMAP.md`. Statisk kanban-vy (`index.html`/`app.js`), och en\nGitHub Action som skördar per timme + vid push och committar färsk data.\nIdempotent — committar bara vid faktisk ändring. Deploy via Cloudflare\n(se `align-with-fleet-cloudflare`); board på roadmap.tor2dbear.com.",
      "sourcePath": "roadmap/multi-repo-board.md",
      "sourceUrl": "https://github.com/tor2dbear/roadmap/blob/main/roadmap/multi-repo-board.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "cadence/opinion-layer-service",
      "repo": "tor2dbear/cadence",
      "repoName": "Cadence",
      "repoColor": "#f472b6",
      "slug": "opinion-layer-service",
      "title": "Opinion-lagret som tjänst (Worker + MCP)",
      "status": "later",
      "tags": [
        "backend"
      ],
      "updated": "2026-08-15",
      "issue": null,
      "order": 10,
      "body": "## Mål\nGöra system-read-kritiken anropbar från CI eller en agent — \"blocka bygget om exit är\nlångsammare än enter\".\n\n## Research\nExtrahera den *rena* logiken (resolve + system-read-checkarna) ur `cadence.js` till en\nhuvudlös modul (`tokens.js`, ingen DOM), exponera som en serverless-endpoint. Given\nstacken (Cloudflare framför Pages) är en **Cloudflare Worker** den naturliga värden\n(gratisnivå ~100k req/dag) framför Netlify Functions. En **MCP-wrapper** över samma\nfunktion gör kritiken anropbar från editor/agent — den enda genuint agent-formade delen\nav Cadence.\n\n## Öppna frågor\n- Bryter \"plain static site, no build\"-regeln — medvetet sidospår, inte default-scope.",
      "sourcePath": "roadmap/opinion-layer-service.md",
      "sourceUrl": "https://github.com/tor2dbear/cadence/blob/main/roadmap/opinion-layer-service.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "portfolio/redesign-header-footer",
      "repo": "tor2dbear/portfolio",
      "repoName": "tor-björn.com",
      "repoColor": "#fb923c",
      "slug": "redesign-header-footer",
      "title": "Redesign — header/footer-navigation",
      "status": "done",
      "tags": [
        "design",
        "layout"
      ],
      "updated": "2026-08-15",
      "issue": null,
      "order": 10,
      "body": "## Mål\nFlytta navigeringen från sidomeny till header + footer, bättre på små skärmar,\nmed startsidan som huvudsaklig innehållspunkt.\n\n## Levererat\nTheme- och språk-dropdowns (mobil bottom sheet), sidomeny borttagen och arkiverad,\nhamburgare borttagen, footer i 4-kolumns layout. Detaljer i `REDESIGN_PLAN.md`.\n\nKvarvarande fas 5 (startsida-ombyggnad) spåras i `homepage-rebuild.md`.",
      "sourcePath": "roadmap/redesign-header-footer.md",
      "sourceUrl": "https://github.com/tor2dbear/portfolio/blob/master/roadmap/redesign-header-footer.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "pia-terminal/nano-multibuffer",
      "repo": "tor2dbear/pia-terminal",
      "repoName": "PIA",
      "repoColor": "#38bdf8",
      "slug": "nano-multibuffer",
      "title": "nano: multi-buffer",
      "status": "done",
      "tags": [
        "editor"
      ],
      "updated": "2026-07-18",
      "issue": null,
      "order": 10,
      "body": "## Mål\nKunna ha flera filer öppna samtidigt i `nano` och växla mellan dem, istället\nför en buffer i taget. Riktiga nano har `^R` (Read File in i ny buffer) och\n`M-<`/`M->` för att bläddra mellan buffrar.\n\n## Levererat\n- **Flera buffrar öppnas med `nano a b c`** (en buffer per filargument, äkta\n  nano-beteende). Blandat lokala och delade (👥) filer funkar — varje fil får sin\n  egen spara-callback.\n- **Växling** med `M-,`/`M-.` (och `M-<`/`M->`), plus tappbara `«`/`»` på key-baren.\n  Titelraden visar `[ n/m ]` när fler än en buffer är öppen.\n- **`^X` stänger aktuell buffer**; sista stängningen avslutar editorn. Osparade\n  ändringar vaktas *per buffer* (`^X` en gång → varning med filnamnet, `^X` igen →\n  släng, `^O` → spara), precis som nano frågar per buffer vid avslut.\n- Multi-buffer bor **inuti** editor-appen (en `Buffer[]` + aktivt index), rör inte\n  `ScreenApp`-hosten — som forskningen lutade åt.\n\n## Beslut på öppna frågor\n- **M-tangent-krocken:** löst genom att matcha på fysisk tangent (`e.code` =\n  `Comma`/`Period`) istället för tecken — layout-oberoende och undviker att\n  macOS-Alt ger ett specialtecken. **Tappbara `«`/`»` på baren är den definitiva\n  fallbacken** (mobil + vilken webbläsare som helst, ingen Meta behövs). Verifierat\n  genom jsdom-tester som kör hela Terminal-input-loopen samt boot-check i riktig\n  Chromium utan runtime-/CSP-fel.\n- **Före cloud-sync?** Byggdes fristående; ingen koppling till sync, ingen risk.\n\n## Ev. följd (ej gjort, valfritt)\n- `^R` — läs in *ytterligare* en fil i en ny buffer *inifrån* editorn. Kräver ett\n  litet filnamns-prompt-läge i statusraden (modal input). Utanför detta måls\n  kärna (\"flera öppna samtidigt + växla\"), sparat som egen liten idé om lusten\n  finns.",
      "sourcePath": "roadmap/nano-multibuffer.md",
      "sourceUrl": "https://github.com/tor2dbear/pia-terminal/blob/main/roadmap/nano-multibuffer.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "pia-terminal/grep-context",
      "repo": "tor2dbear/pia-terminal",
      "repoName": "PIA",
      "repoColor": "#38bdf8",
      "slug": "grep-context",
      "title": "grep: context-flaggor -A/-B/-C",
      "status": "done",
      "tags": [
        "text",
        "commands"
      ],
      "updated": "2026-07-17",
      "issue": null,
      "order": 10,
      "body": "## Mål\n`grep` stödjer redan `-n`. Lägg till `-A<n>`/`-B<n>`/`-C<n>` (efter/före/runt)\nså att en träff kan visas med omgivande rader — precis som riktiga grep. Håller\noss trogna idiomet \"riktiga flaggnamn, riktigt beteende\".\n\n## Research\n- GNU grep-semantik: `-C` är kortform för lika mycket `-A` som `-B`. Explicit\n  `-A`/`-B` vinner över `-C` om båda anges.\n- Separator `--` mellan icke-angränsande träffgrupper i GNU grep. Värt att\n  härma för att kännas äkta; hoppa först, lägg till om det känns tomt utan.\n- Piping: måste funka i pipes (`cat fil | grep -C2 foo`) via `ctx.piped`, inte\n  bara mot filargument.\n\n## Öppna frågor\n- Ska `-A0`/`-B0` bete sig som utan flagga, eller vara fel? GNU tillåter 0 —\n  följ det.\n- Overlap: när två träffars context-fönster överlappar ska de slås ihop, inte\n  dubbeltryckas. Testa det explicit.\n\n## Levererat\nByggt i `src/commands/text.ts` med en dedikerad `parseGrepArgs` (befintliga `-i`\n`-n` `-v` rör vi inte). Så här landade frågorna:\n- **Attach + separat form:** både `-A2` och `-A 2` funkar, och de kan blandas i\n  kluster (`-inA2`).\n- **`-C` vs explicit:** `-C` sätter båda hållen; explicit `-A`/`-B` vinner\n  oavsett ordning.\n- **`-A0`/`-B0`:** tillåtet, beter sig som utan context (GNU-troget).\n- **Overlap:** context-fönster som överlappar *eller angränsar* slås ihop —\n  delade rader skrivs en gång.\n- **`--`-separator:** **inkluderad** (avvek från research-notens \"hoppa först\") —\n  den föll ut gratis ur grupp-logiken och är det äkta grep-beteendet, vilket är\n  hela poängen med pucken. Skrivs bara mellan icke-angränsande grupper när\n  context är på; vanlig `grep` utan context är byte-identisk med förr.\n- **`-n` med context:** träffrad använder `:`, context-rad `-`, som riktiga grep.\n- **Ogiltigt antal** (`-A x`) ger ett rent fel istället för tyst fallback.\n- **Pipes:** ärvs av att kommandot läser `ctx.stdin` som förut.\n\nTäckt av 11 nya tester i `commands.test.ts` (223 totalt gröna); `typecheck` +\n`build` gröna.",
      "sourcePath": "roadmap/grep-context.md",
      "sourceUrl": "https://github.com/tor2dbear/pia-terminal/blob/main/roadmap/grep-context.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "roadmap/cadence-to-native-pucks",
      "repo": "tor2dbear/roadmap",
      "repoName": "Roadmap",
      "repoColor": "#94a3b8",
      "slug": "cadence-to-native-pucks",
      "title": "Migrera Cadence till native pucks",
      "status": "next",
      "tags": [
        "sources"
      ],
      "updated": "2026-08-15",
      "issue": null,
      "order": 20,
      "body": "## Mål\nSista \"adapted\"-källan → native. Cadence läses idag ur en checklista i `NOTES.md`.\n\n## Research\nSkapa `roadmap/` i cadence med `roadmap new`, mappa `✅`→`done`, öppna→`later/next`,\nbyt dess rad i `sources.json` till `pucks`. Korten flippar från adapted → native\nautomatiskt vid nästa skörd.",
      "sourcePath": "roadmap/cadence-to-native-pucks.md",
      "sourceUrl": "https://github.com/tor2dbear/roadmap/blob/main/roadmap/cadence-to-native-pucks.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "cadence/editable-ladder",
      "repo": "tor2dbear/cadence",
      "repoName": "Cadence",
      "repoColor": "#f472b6",
      "slug": "editable-ladder",
      "title": "Redigerbar ladder",
      "status": "done",
      "tags": [
        "tokens"
      ],
      "updated": "2026-08-15",
      "issue": null,
      "order": 20,
      "body": "## Mål\nLägga till, ta bort och byta namn på duration-steg och easings.\n\n## Levererat\nLadder-primitiven är fullt redigerbar (lägg till/ta bort/döp om steg och easings).",
      "sourcePath": "roadmap/editable-ladder.md",
      "sourceUrl": "https://github.com/tor2dbear/cadence/blob/main/roadmap/editable-ladder.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "roadmap/authoring-cli",
      "repo": "tor2dbear/roadmap",
      "repoName": "Roadmap",
      "repoColor": "#94a3b8",
      "slug": "authoring-cli",
      "title": "roadmap-CLI — skapa/förädla pucks",
      "status": "done",
      "tags": [
        "dx"
      ],
      "updated": "2026-08-15",
      "issue": null,
      "order": 20,
      "body": "## Mål\nGöra det trivialt att skapa och flytta pucks utan att röra YAML, och slippa\nsköta `updated` för hand.\n\n## Levererat\n`scripts/roadmap.mjs`: `new/start/next/later/done/status/tag/issue/touch/list` +\n`install-hook`. Varje mutation bumpar `updated`; git-hooken bumpar även vid\nhandredigering. Formatbevarande radredigering; exponerad som npm-bin (`roadmap`).",
      "sourcePath": "roadmap/authoring-cli.md",
      "sourceUrl": "https://github.com/tor2dbear/roadmap/blob/main/roadmap/authoring-cli.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "cadence/security-headers",
      "repo": "tor2dbear/cadence",
      "repoName": "Cadence",
      "repoColor": "#f472b6",
      "slug": "security-headers",
      "title": "Security-header-härdning",
      "status": "later",
      "tags": [
        "backend",
        "config"
      ],
      "updated": "2026-08-15",
      "issue": null,
      "order": 20,
      "body": "## Mål\nCSP + `frame-ancestors 'self'` på `demo.html` (så ingen annan kan iframe:a demon),\nstriktare `Cache-Control`.\n\n## Research\nBilligt, rör inte appen — sätts på Cloudflare-lagret (Pages kan inte skicka egna\nheaders). Egentligen en config-uppgift, parkerad här så den inte glöms.",
      "sourcePath": "roadmap/security-headers.md",
      "sourceUrl": "https://github.com/tor2dbear/cadence/blob/main/roadmap/security-headers.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "portfolio/homepage-rebuild",
      "repo": "tor2dbear/portfolio",
      "repoName": "tor-björn.com",
      "repoColor": "#fb923c",
      "slug": "homepage-rebuild",
      "title": "Startsida-ombyggnad (Fas 5)",
      "status": "next",
      "tags": [
        "design"
      ],
      "updated": "2026-08-15",
      "issue": null,
      "order": 20,
      "body": "## Mål\nBygga om startsidan som huvudsaklig innehållspunkt enligt redesign-planen.\n\n## Research\nSista fasen i header/footer-redesignen. Detaljer i `REDESIGN_PLAN.md` (Fas 5).",
      "sourcePath": "roadmap/homepage-rebuild.md",
      "sourceUrl": "https://github.com/tor2dbear/portfolio/blob/master/roadmap/homepage-rebuild.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "cadence/shareable-url-state",
      "repo": "tor2dbear/cadence",
      "repoName": "Cadence",
      "repoColor": "#f472b6",
      "slug": "shareable-url-state",
      "title": "Delbart system via URL-state",
      "status": "done",
      "tags": [
        "share"
      ],
      "updated": "2026-08-15",
      "issue": null,
      "order": 30,
      "body": "## Mål\nDela ett helt token-system genom en länk.\n\n## Levererat\nHela token-uppsättningen kodas i URL:en → systemet är delbart och återskapas från länken.",
      "sourcePath": "roadmap/shareable-url-state.md",
      "sourceUrl": "https://github.com/tor2dbear/cadence/blob/main/roadmap/shareable-url-state.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "cadence/export-targets",
      "repo": "tor2dbear/cadence",
      "repoName": "Cadence",
      "repoColor": "#f472b6",
      "slug": "export-targets",
      "title": "Fler export-mål",
      "status": "done",
      "tags": [
        "export"
      ],
      "updated": "2026-08-15",
      "issue": null,
      "order": 40,
      "body": "## Mål\nExportera systemet till fler format än ren CSS.\n\n## Levererat\nExport till Tailwind-config, Style Dictionary och JS/TS-objekt (utöver CSS custom properties).",
      "sourcePath": "roadmap/export-targets.md",
      "sourceUrl": "https://github.com/tor2dbear/cadence/blob/main/roadmap/export-targets.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "cadence/shipped-extras",
      "repo": "tor2dbear/cadence",
      "repoName": "Cadence",
      "repoColor": "#f472b6",
      "slug": "shipped-extras",
      "title": "Levererat utöver ursprunglig plan",
      "status": "done",
      "tags": [
        "core"
      ],
      "updated": "2026-08-15",
      "issue": null,
      "order": 50,
      "body": "## Levererat\nUtöver punkterna ovan har följande skeppats, ej ursprungligen listat:\nsprings → CSS `linear()`, motion-mode-axel, stagger + cascade-lins, property-axel,\ndistance/travel-primitiv + velocity-check, tempo, reduced-motion, en live demo-yta,\noch \"Load a system\".",
      "sourcePath": "roadmap/shipped-extras.md",
      "sourceUrl": "https://github.com/tor2dbear/cadence/blob/main/roadmap/shipped-extras.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "roadmap/archive-done-column",
      "repo": "tor2dbear/roadmap",
      "repoName": "Roadmap",
      "repoColor": "#94a3b8",
      "slug": "archive-done-column",
      "title": "Arkivera gamla \"done\" på tavlan",
      "status": "later",
      "tags": [
        "ui"
      ],
      "updated": "2026-08-15",
      "issue": null,
      "order": null,
      "body": "## Mål\nHindra att `done` växer sig plottrig över tid utan att radera historik.\n\n## Research\nBehåll filerna (konventionen säger radera aldrig), men fäll ihop/dölj `done` äldre\nän N dagar i vyn, med en \"visa arkiv\"-knapp. Rent UI-jobb i `app.js`.",
      "sourcePath": "roadmap/archive-done-column.md",
      "sourceUrl": "https://github.com/tor2dbear/roadmap/blob/main/roadmap/archive-done-column.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "roadmap/portability-for-others",
      "repo": "tor2dbear/roadmap",
      "repoName": "Roadmap",
      "repoColor": "#94a3b8",
      "slug": "portability-for-others",
      "title": "Fungera utanför mina egna repon",
      "status": "inbox",
      "tags": [
        "product"
      ],
      "updated": "2026-08-15",
      "issue": null,
      "order": null,
      "body": "## Mål\nOm verktyget ska visas/delas: göra det användbart för andra, inte bara min setup.\n\n## Research\n- Stöd för privata repon / token per användare.\n- Enkel installation utan att handredigera `sources.json`.\n- Konventionen paketerad som en standard/produkt.\nTa först när det bevisat sig för mig själv.",
      "sourcePath": "roadmap/portability-for-others.md",
      "sourceUrl": "https://github.com/tor2dbear/roadmap/blob/main/roadmap/portability-for-others.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "cadence/import-motion-palette",
      "repo": "tor2dbear/cadence",
      "repoName": "Cadence",
      "repoColor": "#f472b6",
      "slug": "import-motion-palette",
      "title": "Importera & kritisera en befintlig motion-palett",
      "status": "later",
      "tags": [
        "export",
        "opinion"
      ],
      "updated": "2026-08-15",
      "issue": null,
      "order": null,
      "body": "## Mål\nLäsa in ett ramverks motion-tokens och visualisera/kritisera dem — \"reverse-engineer\nthe art direction\".\n\n## Research\nMotsatsen till export: ta en extern palett av rörelse och kör den genom system-read.\nAngeläget som portfolio-vinkel (visar omdömet, inte bara generering).",
      "sourcePath": "roadmap/import-motion-palette.md",
      "sourceUrl": "https://github.com/tor2dbear/cadence/blob/main/roadmap/import-motion-palette.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "portfolio/seed-remaining-plans",
      "repo": "tor2dbear/portfolio",
      "repoName": "tor-björn.com",
      "repoColor": "#fb923c",
      "slug": "seed-remaining-plans",
      "title": "Migrera övriga docs/-planer till pucks",
      "status": "inbox",
      "tags": [
        "roadmap"
      ],
      "updated": "2026-08-15",
      "issue": null,
      "order": null,
      "body": "## Mål\nStartpunkt: resten av `docs/features/` och `docs/migrations/` bör bli egna pucks\nnär de aktualiseras.\n\n## Research\nKvar att förädla till pucks: schema-org, breakpoints-update, footer-restructure,\nbrand-mark-morph, dependency-refresh, gallery-shortcode (TODO), hero-seo-title\n(Fas A klar), project-info-block (klar), token_plan, content-markdown-cleanup.\nSkapa med `roadmap new` när de blir aktuella.",
      "sourcePath": "roadmap/seed-remaining-plans.md",
      "sourceUrl": "https://github.com/tor2dbear/portfolio/blob/master/roadmap/seed-remaining-plans.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "portfolio/pantone-css-refactor",
      "repo": "tor2dbear/portfolio",
      "repoName": "tor-björn.com",
      "repoColor": "#fb923c",
      "slug": "pantone-css-refactor",
      "title": "Pantone/palett — CSS-driven refactor",
      "status": "later",
      "tags": [
        "css",
        "tokens"
      ],
      "updated": "2026-08-15",
      "issue": null,
      "order": null,
      "body": "## Mål\nFlytta palett/pantone-hanteringen till en CSS-driven modell.\n\n## Research\nPlan, ej påbörjad. Ingår: `docs/migrations/pantone-css-driven-refactor.md`,\n`palette-override-audit-*` och `pantone-lab-separation-*`.",
      "sourcePath": "roadmap/pantone-css-refactor.md",
      "sourceUrl": "https://github.com/tor2dbear/portfolio/blob/master/roadmap/pantone-css-refactor.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "cadence/deploy-previews",
      "repo": "tor2dbear/cadence",
      "repoName": "Cadence",
      "repoColor": "#f472b6",
      "slug": "deploy-previews",
      "title": "Per-branch deploy previews (Netlify?)",
      "status": "inbox",
      "tags": [
        "backend"
      ],
      "updated": "2026-08-15",
      "issue": null,
      "order": null,
      "body": "## Mål\nÖverväga per-branch deploy-previews.\n\n## Research\nDet enda konkreta skälet att överväga Netlify; allt annat täcker Pages + Cloudflare redan.\nVäg mot static-vs-backend-tradeoffen innan hela sajten flyttas.",
      "sourcePath": "roadmap/deploy-previews.md",
      "sourceUrl": "https://github.com/tor2dbear/cadence/blob/main/roadmap/deploy-previews.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "tor2dbear.com/www-redirect",
      "repo": "tor2dbear/tor2dbear.com",
      "repoName": "Workshop",
      "repoColor": "#34d399",
      "slug": "www-redirect",
      "title": "Redirect www to the apex",
      "status": "done",
      "tags": [
        "infra"
      ],
      "updated": "2026-08-15",
      "issue": null,
      "order": null,
      "body": "## Goal\n`www.tor2dbear.com` resolves to nothing. Anyone who types the habitual `www.` in\nfront of the workshop gets a dead page instead of the front door.\n\n## Research\n\nThe zone has no `www` record at all — Cloudflare's own DNS panel flags it:\n*\"Visitors cannot reach www.tor2dbear.com.\"* Two ways to fix it:\n\n**A second custom domain on the Worker.** Add `www.tor2dbear.com` to `routes` in\n`wrangler.jsonc` next to the apex. One line, deploys itself. But it *serves* the\nsite at both hostnames rather than redirecting, so the same page lives at two\nURLs — split search ranking, two canonical candidates, and every project link on\nthe page would have to be host-relative to stay consistent. Rejected.\n\n**A Redirect Rule.** `www` sends a 301 to the apex, one canonical hostname, no\ncode and no deploy. This is the way, and it is what the portfolio should use for\nits own `www` too if it ever moves off Netlify.\n\nIt needs a DNS record to fire against: Cloudflare only runs rules on hostnames it\nproxies, so `www` needs a **proxied** record even though nothing is behind it.\nThe idiomatic placeholder is `AAAA www → 100::` (the discard prefix), which is\nexactly what `api.tor2dbear.com` already uses in this zone.\n\nSteps, all in the dashboard:\n\n1. **DNS → Add record** — type `AAAA`, name `www`, address `100::`, proxy **on**.\n2. **Rules → Redirect Rules → Create rule → *Redirect from WWW to root*.** The\n   template needs no edits: it matches the wildcard `https://www.*` and redirects\n   to `https://${1}`, so the captured host and path carry over. Status **301**.\n   Check *Preserve query string* is on — it sits below the fold on a phone.\n\n   A custom filter expression (`hostname eq \"www.tor2dbear.com\"` with a\n   `concat()` target) does the same thing with more handling, and only for this\n   one host. The wildcard covers any future `www.` in the zone. Use the template.\n3. Check `curl -sSI https://www.tor2dbear.com` returns `301` with the right\n   `location`, and that the apex itself still answers `200`.\n\n## Delivered\n\nBoth steps, in the dashboard. `AAAA www → 100::` proxied, then the *Redirect\nfrom WWW to root* template deployed unmodified.\n\nVerified from outside:\n\n| | |\n| --- | --- |\n| `www.tor2dbear.com` | 301 → `https://tor2dbear.com/` |\n| Path | `/projects.json` carried over |\n| Query | `?utm_source=test&x=1` intact |\n| Following the redirect | 200 on the target |\n| Apex direct | 200, unaffected |\n\nOne thing worth remembering: between the two steps `www` answered **522**. That\nis the expected intermediate state, not a fault — the record resolves, so\nCloudflare tries to proxy to `100::`, which discards it. Once the rule is live\nthe edge answers 301 before it ever reaches for an origin. Anyone adding a\nproxied placeholder record without a rule behind it will see the same.\n\nThe wildcard template beat the hand-written filter expression: it needed no\nedits and covers any future `www.` host in the zone rather than one name.\n\n## Open questions\n- Should the same rule exist for every project subdomain (`www.pia.…`)? The\n  wildcard already covers them if they ever get a proxied record — nothing more\n  to do unless one is added.\n- If the portfolio moves to Cloudflare, does `tor-bjorn.com` want the inverse\n  (apex → `www`, which is where it lives today)? That reverses the canonical\n  direction and should be decided once, for both domains.",
      "sourcePath": "roadmap/www-redirect.md",
      "sourceUrl": "https://github.com/tor2dbear/tor2dbear.com/blob/main/roadmap/www-redirect.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "portfolio/subgrid-migration",
      "repo": "tor2dbear/portfolio",
      "repoName": "tor-björn.com",
      "repoColor": "#fb923c",
      "slug": "subgrid-migration",
      "title": "Subgrid-migration",
      "status": "done",
      "tags": [
        "css",
        "layout"
      ],
      "updated": "2026-08-15",
      "issue": null,
      "order": null,
      "body": "## Mål\nMigrera relevanta layouter till CSS subgrid.\n\n## Levererat\n12-kolumners subgrid-systemet är infört och används genomgående (t.ex.\n`assets/css/utilities/grid.css`, `.use-subgrid` i article-card/gallery); inga\nlegacy `.grid-1`–`.grid-6`-användningar kvar.",
      "sourcePath": "roadmap/subgrid-migration.md",
      "sourceUrl": "https://github.com/tor2dbear/portfolio/blob/master/roadmap/subgrid-migration.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "portfolio/terminal-followups",
      "repo": "tor2dbear/portfolio",
      "repoName": "tor-björn.com",
      "repoColor": "#fb923c",
      "slug": "terminal-followups",
      "title": "Terminal — uppföljningar",
      "status": "done",
      "tags": [
        "terminal"
      ],
      "updated": "2026-08-15",
      "issue": null,
      "order": null,
      "body": "## Mål\nTvå uppskjutna terminal-spår: mobil key bar och in-place-navigation.\n\n## Levererat\nBåda är ✅ shipped enligt `docs/features/terminal-followups-plan.md`\n(mobil key bar + SPA-style typed-only navigation). Bredare terminal-riktningar\nlever vidare i `terminal-structure-strategy.md`.",
      "sourcePath": "roadmap/terminal-followups.md",
      "sourceUrl": "https://github.com/tor2dbear/portfolio/blob/master/roadmap/terminal-followups.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "portfolio/typography",
      "repo": "tor2dbear/portfolio",
      "repoName": "tor-björn.com",
      "repoColor": "#fb923c",
      "slug": "typography",
      "title": "Typografi — plan & skala",
      "status": "done",
      "tags": [
        "typography"
      ],
      "updated": "2026-08-15",
      "issue": null,
      "order": null,
      "body": "## Mål\nFörbättra typografisk skala och prosa.\n\n## Levererat\nPlanen i `docs/features/typography-plan.md` är genomförd: `--text-1-5xl` (32px),\n`.prose` med vertikal rytm, `.type-preamble` / `.type-display` / `.type-lead`,\nclamp-sizing och justerad headline-skala.\n\n## Öppna frågor\n- `.type-headline-3` tillbaka på 24px (`--text-2xl`) eller behåll 32px?",
      "sourcePath": "roadmap/typography.md",
      "sourceUrl": "https://github.com/tor2dbear/portfolio/blob/master/roadmap/typography.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "portfolio/ui-library",
      "repo": "tor2dbear/portfolio",
      "repoName": "tor-björn.com",
      "repoColor": "#fb923c",
      "slug": "ui-library",
      "title": "UI-library-implementation",
      "status": "now",
      "tags": [
        "components"
      ],
      "updated": "2026-08-15",
      "issue": null,
      "order": null,
      "body": "## Mål\nBygga ut ett återanvändbart UI-bibliotek.\n\n## Research\nPågår. Delsteg och beslut i `docs/features/ui-library-implementation.md`.",
      "sourcePath": "roadmap/ui-library.md",
      "sourceUrl": "https://github.com/tor2dbear/portfolio/blob/master/roadmap/ui-library.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "portfolio/utility-class-cleanup",
      "repo": "tor2dbear/portfolio",
      "repoName": "tor-björn.com",
      "repoColor": "#fb923c",
      "slug": "utility-class-cleanup",
      "title": "Utility-class-cleanup",
      "status": "next",
      "tags": [
        "css",
        "refactor"
      ],
      "updated": "2026-08-15",
      "issue": null,
      "order": null,
      "body": "## Mål\nStäda utility-klasser i faser och minska utility-användningen.\n\n## Levererat\nMigrationen är genomförd — inga kvarvarande typography-utilities i `layouts/`\n(undantag UI-library och `_deprecated`); sista utility flyttad till komponent-CSS.\nDetaljer i `docs/migrations/utility-class-cleanup.md`.\n\n## Kvar\n- Post-migration-validering: visuell test på breakpoints, inga regressioner,\n  Lighthouse/a11y oförändrad.\n- Cleanup 2–4 veckor efter deploy: räkna och överväg borttagning av oanvända\n  utilities (< 5 användningar).",
      "sourcePath": "roadmap/utility-class-cleanup.md",
      "sourceUrl": "https://github.com/tor2dbear/portfolio/blob/master/roadmap/utility-class-cleanup.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "meta-matic/onchain-attestation",
      "repo": "tor2dbear/meta-matic",
      "repoName": "Méta-Matic",
      "repoColor": "#a78bfa",
      "slug": "onchain-attestation",
      "title": "On-chain attestation (optional, satire) — mint the joke",
      "status": "later",
      "tags": [
        "wallet",
        "onchain",
        "satire"
      ],
      "updated": "2026-08-13",
      "issue": null,
      "order": null,
      "body": "## Goal\nAn OPTIONAL third certify path that writes the attestation to a public blockchain, so the\njoke — \"I certify authorship of a work that was never original\" — lives permanently and\npublicly on-chain. Coexists with (never replaces) the free off-chain `personal_sign` and\nthe browser-certify path.\n\n## Approach (deliberately cheap + low-maintenance)\n- **Chain:** an L2 (Base / Optimism) where gas is cents, not L1 where it's dollars.\n- **No custom contract:** use **EAS (Ethereum Attestation Service)**, already deployed on\n  Base/Optimism. Register one small schema (serial, coordinate, fingerprint, generator,\n  time) once — cents, no contract to write/deploy/audit. On-brand: it's literally an\n  \"attestation\".\n- **Who pays: the signer (user-pays).** They send the attestation tx from their own wallet\n  and pay the few cents of L2 gas. The seller pays ~nothing ongoing (only the one-time\n  schema registration). No sponsored/gasless mint (that would put gas cost on the seller).\n- **Frontend:** reuse the same `window.ethereum` provider as wallet-sign, so on-chain only\n  appears where a wallet is connected (desktop extension or in-app wallet browser — same\n  reach as wallet-sign). Build + send the EAS attestation tx, then store the tx hash /\n  attestation UID next to the ledger entry and link to the explorer / EAS scan. Optionally\n  show an on-chain badge in the ledger and on the exported print.\n\n## Explicitly out of scope (keep it small)\n- Not a replacement for off-chain sign or browser-certify — a third, optional method.\n- No sponsored/gasless mint (would make the seller pay gas + add infra).\n- No NFT/ERC-721 (heavier than an EAS attestation, and less on-theme).\n- Same wallet-provider requirement as wallet-sign (so it won't help bare iOS Safari; that's\n  the separate WalletConnect track).\n\n## Open questions\n- Base vs Optimism (Base = cheapest + most reach)?\n- Surface the on-chain link/badge in the ledger and/or the exported print file?\n- Copy that keeps the satire (\"mint a certificate of authenticity for something that was\n  never new — for a few cents of gas\").",
      "sourcePath": "roadmap/onchain-attestation.md",
      "sourceUrl": "https://github.com/tor2dbear/meta-matic/blob/main/roadmap/onchain-attestation.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "meta-matic/sandbox-staging",
      "repo": "tor2dbear/meta-matic",
      "repoName": "Méta-Matic",
      "repoColor": "#a78bfa",
      "slug": "sandbox-staging",
      "title": "Parallel sandbox/staging environment for the print shop",
      "status": "next",
      "tags": [
        "devops",
        "print"
      ],
      "updated": "2026-08-13",
      "issue": null,
      "order": null,
      "body": "## Goal\nBe able to run and test the full order pipeline against the **sandbox** Stripe + Prodigi\nin parallel with production, so future changes (pricing, sizes, webhook logic) can be\nverified end-to-end without touching live money or live orders.\n\n## Today\nThere is one worker with live secrets and `PRODIGI_BASE = https://api.prodigi.com`.\nGoing live was a one-way flip of the base URL + swapping secrets; there's no way to\nexercise the real flow in sandbox afterwards without disrupting production.\n\n## Approach\nUse a second Wrangler **environment** (e.g. `[env.sandbox]`) that deploys a separate\nworker (its own route/subdomain, e.g. `api-sandbox.…`) with:\n- sandbox `PRODIGI_BASE`, sandbox Prodigi key, test Stripe keys, and a **test-mode**\n  Stripe webhook pointing at the sandbox worker's `/stripe-webhook`;\n- its own D1 + R2 bindings (or clearly namespaced) so test orders never mix with live;\n- a staging copy of the site (or a `?env=sandbox` switch) pointing `API` at the sandbox\n  worker.\n\nThen: develop → deploy to `sandbox` → test with Stripe test cards + Prodigi sandbox →\npromote the same code to production with `wrangler deploy`. Secrets differ per env;\n`wrangler.toml` vars can be overridden per `[env.*]`.\n\n## Open questions\n- Separate D1/R2 per env vs shared with a prefix?\n- Staging site as a distinct Pages project, or a runtime `API` toggle on the same site?\n- Worth a tiny \"env banner\" on the sandbox site so it's never mistaken for live.",
      "sourcePath": "roadmap/sandbox-staging.md",
      "sourceUrl": "https://github.com/tor2dbear/meta-matic/blob/main/roadmap/sandbox-staging.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "meta-matic/print-shop",
      "repo": "tor2dbear/meta-matic",
      "repoName": "Méta-Matic",
      "repoColor": "#a78bfa",
      "slug": "print-shop",
      "title": "Print shop — order a physical giclée",
      "status": "done",
      "tags": [
        "commerce",
        "print"
      ],
      "updated": "2026-08-13",
      "issue": null,
      "order": null,
      "body": "## Goal\nLet a holder of a certified work order a real fine-art print of it, priced live per\ncountry, with the seller never out of pocket and payment/fulfilment fully automated.\n\n## Delivered\nEnd-to-end shop on a Cloudflare Worker + D1 + R2, fronted by the single-page site:\n\n- **Flow:** certify → \"Order a print\" → 6000 px render uploaded to R2 → Stripe Checkout\n  (hosted) → on paid webhook, a Prodigi order is created against the shipping address.\n  Exactly-once fulfilment via a D1 `print_orders` row + Prodigi `Idempotency-Key`, so a\n  Stripe retry or a mid-flight crash never double-orders or strands a paid order.\n- **Pricing (never out of pocket), modelled for a Swedish VAT-registered B2C seller:**\n  `charge = (base + margin + fixed)/(1/(1 + vat) − pct) + buffer` (VAT is grossed up over the\n  whole charge, not just base+margin). `base` = Prodigi\n  items + shipping (ex-VAT; input VAT is reclaimed). `vat` = output VAT the seller\n  remits — home rate on EU sales, 0 % on non-EU exports. All levers in `wrangler.toml`\n  (`PRICE_*`, `PRICE_VAT_RATE`, `PRICE_VAT_RATES`).\n- **Two sizes:** 20×20″ and 12×12″, config-driven (`PRINT_SIZES`), each quoted, charged\n  and ordered on its own SKU. Order dialog shows a live price per size for the chosen\n  country via a session-free `/print-quote`.\n- **Return-from-Stripe confirmation:** returning to `/?print=ok` shows a clear\n  \"✓ Print ordered\" modal (replaced the easy-to-miss banner; no email promised that we\n  don't send).\n- **Export fixes:** wordmark matches the site (bold sans, ∞ in signal), uniform margins,\n  and the ribbon's joint-fills wound to match the strokes (killed the dashed/stitched\n  look at print resolution).\n- **Live:** Prodigi production API, live Stripe keys + live webhook, Budget shipping.\n\n## Notes / possible follow-ups\n- VAT rate defaults to 25 %; confirm the art rate (Sweden's 12 % can apply to an\n  artist's own work) and the EU OSS threshold with an accountant — both tunable via env.\n- Drop the temporary D1 `webhook_debug` table (left behind after the signature-mismatch\n  debugging; nothing writes to it now).",
      "sourcePath": "roadmap/print-shop.md",
      "sourceUrl": "https://github.com/tor2dbear/meta-matic/blob/main/roadmap/print-shop.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "meta-matic/seller-order-notifications",
      "repo": "tor2dbear/meta-matic",
      "repoName": "Méta-Matic",
      "repoColor": "#a78bfa",
      "slug": "seller-order-notifications",
      "title": "Seller notification on new print order",
      "status": "next",
      "tags": [
        "commerce",
        "ops"
      ],
      "updated": "2026-08-13",
      "issue": null,
      "order": null,
      "body": "## Goal\nNotify the seller the moment a print order is paid and fulfilled, with everything needed\nto recognise it at a glance — serial, size, destination country, buyer name/email — so a\nsale is never missed and can be reconciled against Stripe + Prodigi.\n\n## Today\nNothing is sent to the seller from our own code. The buyer sees the on-site confirmation\nmodal; awareness of a sale relies on Stripe's owner emails / mobile app and Prodigi's\ndashboard + order emails (both should be enabled in their respective settings).\n\n## Approach\nSend a \"new order\" email from the Stripe webhook right after Prodigi confirms the order\n(the point where we already have serial, size, country, buyer). Resend is the connected\noption — needs a `RESEND_API_KEY` secret and a verified sender domain. Keep it best-effort\n(never fail fulfilment if the email send fails). A Slack/webhook variant is an easy\nalternative to email.\n\n## Open questions\n- Email vs Slack vs both?\n- Include the R2 image link / a thumbnail?\n- Also send the buyer a branded receipt, or leave that to Stripe's receipt emails?",
      "sourcePath": "roadmap/seller-order-notifications.md",
      "sourceUrl": "https://github.com/tor2dbear/meta-matic/blob/main/roadmap/seller-order-notifications.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "meta-matic/diffusion-reveal",
      "repo": "tor2dbear/meta-matic",
      "repoName": "Méta-Matic",
      "repoColor": "#a78bfa",
      "slug": "diffusion-reveal",
      "title": "Diffusion as a Fourier reveal",
      "status": "done",
      "tags": [
        "diffusion"
      ],
      "updated": "2026-08-12",
      "issue": null,
      "order": null,
      "body": "## Goal\nFuture works should sharpen \"clearer and clearer until perfect\" — an homage to how\nAI generates images — not a grain crossfade laid on top.\n\n## Delivered\nWorks to the right of now emerge from noise and gain their own harmonics low\nfrequency → high (a Fourier reveal, `nu`/`resolve` gating), screen-independent so\nthe effect is identical on every viewport (mobile \"misses\" it unless you scrub\nforward). Far-future works shrink to points — both a performance win and \"shouldn't\nbe legible that far ahead.\"\n\nRuled out along the way (see NOTES.md): grain crossfade, migrating particles,\n`ctx.filter` blur, per-frame step-count changes.",
      "sourcePath": "roadmap/diffusion-reveal.md",
      "sourceUrl": "https://github.com/tor2dbear/meta-matic/blob/main/roadmap/diffusion-reveal.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "meta-matic/exclusive-certificates",
      "repo": "tor2dbear/meta-matic",
      "repoName": "Méta-Matic",
      "repoColor": "#a78bfa",
      "slug": "exclusive-certificates",
      "title": "Exclusive certificates",
      "status": "done",
      "tags": [
        "backend",
        "ownership"
      ],
      "updated": "2026-08-12",
      "issue": 17,
      "order": null,
      "body": "## Goal\nOne owner per work — a real certificate of authenticity, first signer wins. A\ncertificate for something that was never new: that is the whole point.\n\n## Research\nWeighed **A** (anyone can sign the same work, \"signed N times before you\") against\n**B** (one work = one owner). A was redundant with \"nothing is new\" (already said at\nthe generation layer) and weakened the certificate. Chose **B**. The irony survives:\nnear-duplicates recur, so your one-of-one is authentic yet visually generic. Two\n*methods* (browser / wallet), not two *tiers* of ownership.\n\n## Delivered\nCloudflare **D1** ledger; `serial` is `PRIMARY KEY`, so exclusivity is atomic — a\nsecond concurrent `/claim` on the same work fails the INSERT instead of racing to a\ndouble-owner. Two methods: browser-id (instant) and wallet (`personal_sign` adds a\nverifiable signature). Focus card shows available / yours / claimed-by-someone and\noffers the certify buttons only when available, never claiming before the server\nanswers. Counter \"Certified · all\"; offline degrades to a local certificate.",
      "sourcePath": "roadmap/exclusive-certificates.md",
      "sourceUrl": "https://github.com/tor2dbear/meta-matic/blob/main/roadmap/exclusive-certificates.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "meta-matic/inspect-and-certify",
      "repo": "tor2dbear/meta-matic",
      "repoName": "Méta-Matic",
      "repoColor": "#a78bfa",
      "slug": "inspect-and-certify",
      "title": "Inspect & certify, save image",
      "status": "done",
      "tags": [
        "ownership"
      ],
      "updated": "2026-08-12",
      "issue": null,
      "order": null,
      "body": "## Goal\nLet people inspect a work enlarged and deliberately certify it, then keep their\ncertified works as images.\n\n## Delivered\nFull-screen inspect modal (serial, coordinate, fingerprint, live claim status).\nCertify from the card; your certificates live in the \"Certified · you\" drawer, each\nexportable as a high-resolution PNG (the work + serial + coordinate + fingerprint,\nwallet address when present). Certificates persist in localStorage.",
      "sourcePath": "roadmap/inspect-and-certify.md",
      "sourceUrl": "https://github.com/tor2dbear/meta-matic/blob/main/roadmap/inspect-and-certify.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "meta-matic/latent-belt",
      "repo": "tor2dbear/meta-matic",
      "repoName": "Méta-Matic",
      "repoColor": "#a78bfa",
      "slug": "latent-belt",
      "title": "Latent conveyor belt",
      "status": "done",
      "tags": [
        "belt"
      ],
      "updated": "2026-08-12",
      "issue": null,
      "order": null,
      "body": "## Goal\nA wall-clock-driven drawing machine that retrieves works from a continuous latent\nspace rather than randomising them — the same work for everyone, right now, with no\nserver.\n\n## Delivered\nSerial is a pure function of time (1 work / 4 s since 2026-01-01). `coordFor(serial)`\nis a deterministic walk through a value-noise field → `(u,v)`; neighbouring serials\ngive near-identical drawings, so the belt morphs smoothly. The walk is bounded and\naperiodic — infinitely many works, never an exact repeat, endlessly recurring\nneighbourhoods (surfaced as the \"≈ nearly identical, seen N works ago\" seam).",
      "sourcePath": "roadmap/latent-belt.md",
      "sourceUrl": "https://github.com/tor2dbear/meta-matic/blob/main/roadmap/latent-belt.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "pia-terminal/mount-webdav",
      "repo": "tor2dbear/pia-terminal",
      "repoName": "PIA",
      "repoColor": "#38bdf8",
      "slug": "mount-webdav",
      "title": "mount — privata filer från egen server",
      "status": "inbox",
      "tags": [
        "storage",
        "vfs"
      ],
      "updated": "2026-08-12",
      "issue": null,
      "order": null,
      "body": "## Mål\n(Research, inte beslutat.) Kunna `mount`:a en mapp från en egen/privat server och\njobba i den från terminalen — så att verkligt privata filer aldrig behöver ligga i\nden delade Supabase-backenden. Idag ligger hela filträdet för en inloggad\nanvändare som en enda `jsonb`-rad per användare i tabellen `filesystems`\n(`src/supabase/storage.ts`); gäster i `localStorage`; `HybridStorageAdapter`\nruttar på auth-läge. Allt eller inget, en backend.\n\n## Idiom\n`mount` är ren Unix — **ingen divergens att flagga**. En privat server-mapp hör\ntill `mount`/`sshfs`/NFS-familjen; namnet ger sig självt. En mount-punkt (t.ex.\n`/mnt/server`) och ett `mount`/`umount`-kommando ligger rätt i idiomet.\n\n## Research\n\n**Arkitektoniskt passar det i `StorageAdapter`-sömmen** — det är precis vad den är\ntill för. Två rimliga former:\n\n- **Ny adapter + mount-begrepp i VFS.** En `WebDavAdapter` / `HttpMountAdapter`\n  plus en *mount-tabell* (path → adapter) i path-resolvern, så läs/skriv under\n  mount-punkten ruttar till en annan backend. Idag laddas trädet som *en* blob —\n  per-subträd-routing är den nya primitiven som saknas.\n- **Lättare variant som återanvänder `shareId`-mönstret — men lättheten är en\n  synvilla.** `shareId` är *inte* en generell per-fil-routing-söm: `linkedContent`/\n  `linkedSave` anropas bara från `nano`/`todo`/`transfer`, medan `cat`, text-utils,\n  `cp` och shell-redirection läser `FileNode.content` (cachen) direkt (`fs.ts` rör\n  bara `shareId` för `@`-dekoration). Generaliserar man mönstret till kataloger utan\n  att flytta routingen *under alla* VFS-ops får man stale reads och lokala-bara\n  skrivningar. Så den här varianten är i praktiken lika stor som \"ny adapter +\n  mount-tabell\" ovan, inte en genväg.\n\n**Sync/async-krocken är den strukturella kärnfrågan.** Hela VFS-API:t är synkront —\n`readFile`/`writeFile`/`list`/`copy`/`move`/`remove`/`mkdir` returnerar värde/`void`\ndirekt (`src/vfs/vfs.ts`), och `StorageAdapter` exponerar bara hel-träds `load`/`save`,\ninte path-nivå-ops. En WebDAV-backend är oundvikligen async. Två utvägar, båda med\nsvans: (a) **propagera async genom alla VFS-konsumenter** (varje kommando som läser/\nskriver blir `await`:at — stor, invasiv refaktor), eller (b) **prefetcha till en cache**\noch servera synkrona läsningar ur den — men då är vi tillbaka i staleness (cachen kan\nligga efter servern) och privatläckan nedan (cachen får inte persistas). Det här, inte\nCSP, är varför en riktig mount är en stor sak snarare än en adapter-swap.\n\n**Privatläckan att designa bort från dag ett:** om en mountad fils innehåll cachas\ni `FileNode.content` (som `shareId`-mönstret gör) läcker de privata bytes till den\ndelade backenden. `linkedSave` (`src/commands/linked.ts`) gör `writeFile` + `persist`,\noch `SupabaseStorageAdapter` skriver *hela trädet* till `filesystems`-raden — dvs det\n\"privata\" innehållet hamnar i Supabase, precis tvärtemot målet. Konsekvens: det\nserialiserade trädet får bära **bara mount-metadata** (path, endpoint, ev. token-ref);\nall filcache måste bo *utanför* det serialiserade trädet (minne/IndexedDB som aldrig\npersistas via storage-adaptern).\n\n**Bromsklossarna sitter i browsersandlådan, inte i PIA-koden:**\n\n- **Ingen rå-fs/SSH/NFS från browsern.** \"Mounta privat server\" betyder i\n  praktiken att servern kör ett HTTP-API browsern når — realistiskt **WebDAV**\n  (Nextcloud, Apache, `rclone serve webdav` talar det) eller ett litet eget API.\n- **CSP är den skarpaste konflikten.** Vi shippar en strikt *statisk* `connect-src`\n  (`vite.config.ts`, byggd vid build-tid) som bara släpper `'self'` + Supabase.\n  Går inte att lägga till en godtycklig användarorigin i runtime. Utvägar: vidga\n  `connect-src` till `https:` (försvagar skyddet — motverkar hela CSP-arbetet),\n  eller proxa via en Cloudflare Worker (men då passerar trafiken ändå oss, vilket\n  motverkar \"privat\").\n- **CORS + HTTPS.** Servern måste skicka CORS-headers för `pia.tor2dbear.com` och\n  köra giltig TLS (mixed-content blockeras). Hemmaservrar med self-signed cert\n  faller på det.\n\n**Alternativ värda att väga mot en riktig mount:**\n\n- **Egen self-hostad Supabase.** Om målet bara är \"mina filer inte på den delade\n  instansen\": peka appen mot egen Supabase (config dynamic-importas redan,\n  `.env.production`). Allt privat, ~noll ny kod. Byter hela backenden, inte en\n  per-mapp-mount — men löser 80 % av behovet billigast.\n- **`File System Access API`** kan mounta en *lokal* katalog utan server alls — men\n  det är lokal disk, inte \"privat server\", och Safari saknar stöd.\n\n## Öppna frågor\n- Vad är det egentliga behovet — \"inte på den delade Supabasen\" (→ egen Supabase,\n  billigt) eller \"genuint mounta en fjärrmapp\" (→ WebDAV-adapter, dyrt)?\n- Hur löser vi CSP för godtycklig origin utan att antingen försvaga den eller proxa\n  genom oss? Kanske: en explicit allowlist användaren själv får bygga in, dvs\n  mount kräver en rebuild/egen deploy? Flagga innan bygge.\n- Async-strategin: gör vi hela VFS async (rent men invasivt — rör varje kommando),\n  eller prefetch-cache med synkrona läsningar (mindre blast-radie men staleness +\n  cachen får inte persistas)? Det här beslutet formar allt annat.\n- Var bor mount-tabellen — i VFS (i minnet) eller i storage-lagret? Lutar åt att\n  hålla VFS rent och lägga routingen i ett lager ovanför adaptrarna.\n- Offline/cache-semantik: ren read-through varje gång, eller en lokal cache — som\n  då måste ligga *utanför* det serialiserade trädet (se privatläckan ovan)? Vad\n  händer när servern är nere mitt i en session?\n- WebDAV-auth i browsern (Basic/Bearer) utan att läcka credentials in i det\n  serialiserade trädet — var lagras token?\n\n_Ligger i `inbox` tills det blivit ett beslut. Befordra till `next/later` då._",
      "sourcePath": "roadmap/mount-webdav.md",
      "sourceUrl": "https://github.com/tor2dbear/pia-terminal/blob/main/roadmap/mount-webdav.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "meta-matic/nft-mint-onchain",
      "repo": "tor2dbear/meta-matic",
      "repoName": "Méta-Matic",
      "repoColor": "#a78bfa",
      "slug": "nft-mint-onchain",
      "title": "On-chain NFT mint",
      "status": "later",
      "tags": [
        "wallet"
      ],
      "updated": "2026-08-12",
      "issue": null,
      "order": null,
      "body": "## Goal\nThe original \"sharp\" idea — an on-chain mint where the chain's `totalSupply` becomes\nthe global ledger.\n\n## Research\nLower priority: the off-chain `personal_sign` certificate already carries the point\n(pay/commit to \"own\" something never original) for free, no gas. An on-chain mint adds\ncost and friction for a mostly-symbolic gain. Keep as an optional collector tier if\never wanted.",
      "sourcePath": "roadmap/nft-mint-onchain.md",
      "sourceUrl": "https://github.com/tor2dbear/meta-matic/blob/main/roadmap/nft-mint-onchain.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "meta-matic/orphaned-certificates",
      "repo": "tor2dbear/meta-matic",
      "repoName": "Méta-Matic",
      "repoColor": "#a78bfa",
      "slug": "orphaned-certificates",
      "title": "Orphaned certificates",
      "status": "inbox",
      "tags": [
        "ownership"
      ],
      "updated": "2026-08-12",
      "issue": null,
      "order": null,
      "body": "## Goal\nDecide what happens when a browser-id owner clears their cache: their certificate is\ngone, but the work stays locked forever by a \"ghost.\"\n\n## Research\nA cost of exclusivity backed by a weak (losable) identity. It's slow, and arguably\nthematic (\"you lost the proof — it's gone, like the works you didn't save\"). If it\never sterilises the space, the fix is wallet-only exclusivity (a portable identity),\nwith browser-id demoted to a non-exclusive keepsake.\n\n## Open questions\n- Leave it (thematic), or move exclusivity to wallet-only?",
      "sourcePath": "roadmap/orphaned-certificates.md",
      "sourceUrl": "https://github.com/tor2dbear/meta-matic/blob/main/roadmap/orphaned-certificates.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "pia-terminal/python-sandbox-csp-fix",
      "repo": "tor2dbear/pia-terminal",
      "repoName": "PIA",
      "repoColor": "#38bdf8",
      "slug": "python-sandbox-csp-fix",
      "title": "python i prod — sandbox-CSP tappas av Cloudflare clean-URL",
      "status": "now",
      "tags": [
        "wasm",
        "deploy",
        "bugfix"
      ],
      "updated": "2026-08-12",
      "issue": null,
      "order": null,
      "body": "## Mål\n`python` (Pyodide) fungerar lokalt men **hänger tyst i produktion** på\n`pia.tor2dbear.com` — man kan `brew install python` men inte köra något. Fixa så\ndet kör i prod, och så att ett framtida fel *syns* istället för att hänga.\n\n## Rotorsak (verifierad mot live-prod 2026-08-12)\n1. Bryggan laddar sandbox-iframen från **`/python-sandbox.html`**.\n2. Cloudflare Pages \"clean URLs\" gör en **308-redirect** `/python-sandbox.html`\n   → **`/python-sandbox`** (strippar `.html`).\n3. Den looser CSP:n (som ger Pyodide `wasm-unsafe-eval` + `worker-src blob:`)\n   emitteras i `dist/_headers` **bara för `/python-sandbox.html`**. `_headers`\n   matchar den *serverade* sökvägen → `/python-sandbox` matchar inte → faller\n   igenom till `/*` = **appens strikta CSP** (ingen `wasm-unsafe-eval`).\n4. Pyodide får inte kompilera sin WASM → `loadPyodide()` kastar inuti iframen.\n5. Sandboxens message-handler saknade `.catch`, och bryggan saknade timeout → \n   inget svar postas tillbaka → **`python` hänger för evigt utan felmeddelande**.\n\nVerifierat live: `curl` visar `/pyodide/*.js/.wasm` = 200 (assets finns), men\n`/python-sandbox.html` → 308 → `/python-sandbox` serveras med den **strikta**\nCSP:n (`script-src 'self' https://static.cloudflareinsights.com`, ingen\n`wasm-unsafe-eval`).\n\n## Djupare orsak (upptäckt vid preview-verifiering)\nAtt bara lägga till en `/python-sandbox`-regel räckte **inte**: Cloudflare Pages\n`_headers` **lägger till** headers från *varje* matchande regel — en specifik regel\n*ersätter* inte `/*`, och `! Header`-detach tar **inte** bort ett ärvt värde (båda\nverifierade mot en preview-deploy: sandboxen fick 2 CSP + 2 XFO). Webbläsaren\ntvingar snittet av flera CSP-headers → den strikta vinner → WASM blockeras ändå.\n\nSlutsats: den strikta CSP:n får **inte** bo på `/*`, eftersom sandbox-sidan också\nmatchar `/*`.\n\n## Fix\n- **(A) Flytta den strikta CSP:n från `/*` till appens dokument-sökvägar** (`/`\n  och `/adventure/*`), som *inte* matchar sandbox-sökvägen. `/*` bär bara de\n  icke-CSP-headrarna (XFO DENY m.fl.). Då får sandboxen **bara** sin looser CSP.\n  Huvudappens skydd är oförändrat: `/`:s `<meta>`-CSP är identisk (minus den\n  header-only `frame-ancestors 'none'`, som täcks av `X-Frame-Options: DENY`).\n  Två sandbox-rader (`/python-sandbox.html` + `/python-sandbox`) för dev/preview\n  respektive prods redirectade sökväg.\n- **(B) Sandboxen postar tillbaka ett fel** om Pyodide-init misslyckas\n  (`public/python-sandbox.js` — `.catch` runt körningen, nollställ\n  `pyodidePromise` så nästa `python` gör ett nytt försök).\n- **(C) Bryggan får en timeout** (`bridge.ts`) på iframe-uppstarten och\n  nollställer `ready`/`frame` vid fel så nästa körning kan återförsöka — så en\n  framtida regression *syns* som ett fel istället för en tyst hang.\n\n## Verifiering\n- Lokalt: `npm run build` → `dist/_headers` innehåller `/python-sandbox`-regeln.\n- WASM/redirect kan **inte** reproduceras i vitest/`vite preview` (bara\n  Cloudflare gör 308:an) → slutverifiering mot **PR-previewens** Cloudflare-URL:\n  `curl` att `/python-sandbox` nu har looser CSP, och kör `python -c` i headless\n  Chromium mot previewen.\n\n## Öppna frågor\n- Alternativ till (A): peka iframen på `/python-sandbox` (utan `.html`) och\n  slippa redirecten — men det bryter `vite dev`/`preview` där filen bara finns\n  som `/python-sandbox.html`. Header-fixen är därför den robusta vägen.",
      "sourcePath": "roadmap/python-sandbox-csp-fix.md",
      "sourceUrl": "https://github.com/tor2dbear/pia-terminal/blob/main/roadmap/python-sandbox-csp-fix.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "meta-matic/claim-rate-limit",
      "repo": "tor2dbear/meta-matic",
      "repoName": "Méta-Matic",
      "repoColor": "#a78bfa",
      "slug": "claim-rate-limit",
      "title": "Rate-limit /claim",
      "status": "later",
      "tags": [
        "backend"
      ],
      "updated": "2026-08-12",
      "issue": null,
      "order": null,
      "body": "## Goal\nStop a script from land-grabbing serials, now that certifying is exclusive and\nbrowser-id is free.\n\n## Research\nDeliberately parked out of v1 to keep the deploy simple. Options:\n- **Cloudflare rate-limiting rule** (dashboard, no code) — least code, but a config\n  step, not in the repo.\n- **Workers native rate-limit binding** — a few lines in `wrangler.toml` + the\n  Worker, keyed on `CF-Connecting-IP`. Beta-ish binding.\n\n## Open questions\n- Per-IP is enough, or also cap per browser-id / per minute?",
      "sourcePath": "roadmap/claim-rate-limit.md",
      "sourceUrl": "https://github.com/tor2dbear/meta-matic/blob/main/roadmap/claim-rate-limit.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "meta-matic/reduced-motion",
      "repo": "tor2dbear/meta-matic",
      "repoName": "Méta-Matic",
      "repoColor": "#a78bfa",
      "slug": "reduced-motion",
      "title": "Reduced-motion mode",
      "status": "done",
      "tags": [
        "a11y"
      ],
      "updated": "2026-08-12",
      "issue": null,
      "order": null,
      "body": "## Goal\nRespect `prefers-reduced-motion`: no autoplay, but the piece stays fully usable.\n\n## Delivered\nUnder reduced motion the belt doesn't animate; the status pill fetches a fresh still\nframe of \"now\" on demand, and scrub updates render synchronously. Everything —\ninspect, certify, save, the map — keeps working.",
      "sourcePath": "roadmap/reduced-motion.md",
      "sourceUrl": "https://github.com/tor2dbear/meta-matic/blob/main/roadmap/reduced-motion.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "meta-matic/scrub-and-lock",
      "repo": "tor2dbear/meta-matic",
      "repoName": "Méta-Matic",
      "repoColor": "#a78bfa",
      "slug": "scrub-and-lock",
      "title": "Rubberband scrub + screen-space lock",
      "status": "done",
      "tags": [
        "interaction"
      ],
      "updated": "2026-08-12",
      "issue": null,
      "order": null,
      "body": "## Goal\nLet people peek into the future (feel the noise, especially on mobile) and a short\nway back, without ever stopping the machine — and lock/ink a work crisply at the\nsigning point.\n\n## Delivered\nSpringy rubberband peek forward/back with spring-back to now; nothing parks, only\nthe present is signable. The lock (ink stroke→ribbon and the focus-ring snap) fires\nin the last ~22 px into the live head — screen-space, not on `resolve` — so it never\nfeels early, and the ring snap is tied to the exact stroke→ribbon curve. The signing\npoint (line + ring + lock) follows the live head while scrubbing.",
      "sourcePath": "roadmap/scrub-and-lock.md",
      "sourceUrl": "https://github.com/tor2dbear/meta-matic/blob/main/roadmap/scrub-and-lock.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "meta-matic/wallet-sig-verification",
      "repo": "tor2dbear/meta-matic",
      "repoName": "Méta-Matic",
      "repoColor": "#a78bfa",
      "slug": "wallet-sig-verification",
      "title": "Server-side wallet signature check",
      "status": "later",
      "tags": [
        "backend",
        "wallet"
      ],
      "updated": "2026-08-12",
      "issue": null,
      "order": null,
      "body": "## Goal\nMake a wallet certificate's provenance trustworthy server-side, not just client-side.\n\n## Research\nToday `/claim` stores the address + `personal_sign` signature; the client sends the\nsigned `message` too (`payload.msg`), but the Worker does **not** persist it and D1\nhas no `msg` column — and the attestation message embeds a client timestamp, so the\npreimage can't be reconstructed from the server's own timestamp. So the stored\nsignature is currently unverifiable once the local entry is gone.\n\nTo harden:\n1. Add a `msg` column and persist the exact signed message (or sign a deterministic /\n   server-issued payload with no client timestamp, so it's reconstructable from serial).\n2. In the Worker, recover the address from the signature over that message and assert\n   it matches the claimed address before writing the row. Needs secp256k1 recovery\n   in-Worker (WebCrypto doesn't do it natively — a small lib or manual implementation).\n\n## Open questions\n- Persist the message, or switch to a deterministic (timestamp-free) signed payload?\n- Reject on mismatch, or accept-but-flag?",
      "sourcePath": "roadmap/wallet-sig-verification.md",
      "sourceUrl": "https://github.com/tor2dbear/meta-matic/blob/main/roadmap/wallet-sig-verification.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "pia-terminal/sh-scripts",
      "repo": "tor2dbear/pia-terminal",
      "repoName": "PIA",
      "repoColor": "#38bdf8",
      "slug": "sh-scripts",
      "title": "sh — kör scriptfiler",
      "status": "done",
      "tags": [
        "shell",
        "terminal"
      ],
      "updated": "2026-08-12",
      "issue": null,
      "order": null,
      "body": "## Mål\n`sh script.sh` läser en fil ur VFS:en och kör den rad för rad genom det\nbefintliga skal-maskineriet — pipes, `;`/`&&`/`||`, redirects och globbing. Gör\nPIA till \"en riktig liten dator\" på skal-nivå: man kan spara en följd av\nkommandon i en fil och köra den. Efterfrågat av tidiga testare (script-filer var\nden enskilt största saknade skal-biten efter kedjning och globbing).\n\n## Research\n- **Alla byggstenar fanns redan.** `command-chaining` och `shell-globbing` är\n  klara, och `CommandContext` exponerar redan `exec?(line): Promise<boolean>` —\n  sömmen `sudo` använder för att köra sin payload genom `runLine → runSequence →\n  executePipeline`. `sh` är i princip \"läs fil → `ctx.exec` per rad\", precis som\n  `sudo` är \"kör payload elevated\". Ingen ny kärn-söm behövdes.\n- **Idiom:** `sh script.sh` och `sh -c \"cmd\"` är rakt av äkta. Ingen divergens.\n- **Ingen exec-bit i VFS.** `FileNode` har bara `name`/`content` (+ `shareId`),\n  ingen `mode`. Så `chmod +x ./script` + shebang-körning (`./script`) kräver att\n  filmodellen och serialiseringen växer ett läges-fält — medvetet **utanför v1**.\n  En shebang-**rad** (`#!/bin/sh`) hanteras ändå gratis: den börjar med `#` och\n  behandlas som en kommentar.\n\n## Levererat (2026-08-12)\n`sh` som ett vanligt `Command` i `src/commands/system.ts`, bredvid `sudo`:\n- **`sh <fil>`** — resolvar mot cwd, läser ur VFS:en, kör varje rad via\n  `ctx.exec`. Tomma rader och kommentarer (`#…`, inklusive en `#!`-shebang på\n  rad 1) hoppas över.\n- **`sh -c \"<kommando>\"`** — kör en kommandosträng direkt.\n- **`cmd | sh` / `cat script | sh`** — utan filargument körs *stdin* som script,\n  så en pipe eller heredoc-lik ström funkar.\n- **Fortsätter vid fel** (inget `set -e`): en rad som failar stoppar inte\n  scriptet. Skalets slut-status = sista körda kommandots, propagerad via\n  `ctx.fail?.()` (ingen dubbel-utskrift — payload-fel skrivs redan av\n  maskineriet) så `sh a.sh && echo ok` beter sig rätt.\n- **Ctrl-C** avbryter mellan rader (kollar `ctx.signal`), som resten av skalet.\n\n## Härdning efter review (2026-08-12)\n- **Hemligheter i history.** Skalets `histIgnore`-traversering recursar nu in i\n  `sh -c` / `bash -c`-payloaden (som den redan gjorde för `at`/`sudo`) och in i en\n  *literal* `echo`/`printf` som matar en stdin-`sh`/`bash` (`echo \"passwd pw\" | sh`)\n  — så en typad hemlighet fångas på de direkta vägarna.\n- **Känd gräns (maintainer-beslut):** att spåra en hemlighet genom *godtyckliga*\n  pipe-transformationer in i ett stdin-skal (`echo … | cat | sh`, `… | rev | sh`,\n  `base64 -d | sh`, alias-kedjor) är obestämbart i allmänhet. Vi jagar inte varje\n  forwarder och gör *inte* den konservativa \"scrub varje `… | sh`\" (den skulle\n  döda legitima `cat deploy.sh | sh` ur history). De direkta/vanliga vägarna är\n  täckta; den konstruerade multi-hop-läckan är medvetet utanför scope. Notera:\n  ingen sätter lösenord via `echo \"passwd x\" | cat | sh`.\n- **Cwd + konto.** Ett script kör som en subprocess: script-lokala `cd` kastas när\n  scriptet är klart (skalet återgår). Men ett kontobyte (`login`/`usermod`/\n  `logout`) är *globalt* för denna en-användarmaskin, inte subprocess-lokalt — det\n  behålls, och skalet landar i det nya kontots hem (inte i en subdir en efterföljande\n  `cd` råkade lämna det i).\n\n## Öppna frågor / uppföljning\n- **`chmod +x` + shebang-körning (`./script`)** — kräver ett läges-/exec-fält på\n  `FileNode` + serialisering. Egen puck när behovet finns.\n- **Positionsargument (`$1`, `$@`) och `$?`** — väntar på variabel-expansion\n  (samma uppföljning som `command-chaining` noterade). `sh script.sh a b` tar\n  emot extra args men ignorerar dem tills expansion finns.\n- **Pipa `sh`:s aggregerade stdout** (`sh f.sh | grep x`) — inre rad-utskrift går\n  till skärmen, inte in i pipen (samma klass av begränsning som `sudo`). Att\n  fånga payload-stdout kräver att `exec`-sömmen returnerar utdata. Uppföljning.\n- **`source` / `.`** (kör i nuvarande skal istället för sub-skal) — meningsfullt\n  först när det finns skal-lokalt tillstånd (variabler) att dela.",
      "sourcePath": "roadmap/sh-scripts.md",
      "sourceUrl": "https://github.com/tor2dbear/pia-terminal/blob/main/roadmap/sh-scripts.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "meta-matic/seo-og",
      "repo": "tor2dbear/meta-matic",
      "repoName": "Méta-Matic",
      "repoColor": "#a78bfa",
      "slug": "seo-og",
      "title": "Social & SEO",
      "status": "done",
      "tags": [
        "seo"
      ],
      "updated": "2026-08-12",
      "issue": null,
      "order": null,
      "body": "## Goal\nWhen the link is shared it should unfurl as the artwork, and be indexable.\n\n## Delivered\nOpen Graph + Twitter Card tags, `theme-color`, canonical; a generated `og.png`\n(1200×630, built from a real hero screenshot so the wordmark matches the site, with\nthe signing-point line). `robots.txt` + `sitemap.xml`. Share title uses the house\nmiddot separator.",
      "sourcePath": "roadmap/seo-og.md",
      "sourceUrl": "https://github.com/tor2dbear/meta-matic/blob/main/roadmap/seo-og.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "meta-matic/the-space-map",
      "repo": "tor2dbear/meta-matic",
      "repoName": "Méta-Matic",
      "repoColor": "#a78bfa",
      "slug": "the-space-map",
      "title": "The Space — the shared map",
      "status": "done",
      "tags": [
        "map"
      ],
      "updated": "2026-08-12",
      "issue": null,
      "order": null,
      "body": "## Goal\nVisualise the latent field as one shared object: the machine's single path, where\neveryone has planted a flag, and your own marks — \"one path, everyone walks it.\"\n\n## Delivered\nFull-bleed band that flex-grows to fill the viewport (footer pinned), background\nmatched to the section above. Renders the shared walk, the machine's now, the recent\ntrail, your own certificates (blue rings), and — since exclusive certificates\nshipped — **everyone's certified points** as a faint constellation (`GET /claimed`).\nIsotropic square projection with a dot lattice that dissolves at the edges (the field\ncontinues past the window).",
      "sourcePath": "roadmap/the-space-map.md",
      "sourceUrl": "https://github.com/tor2dbear/meta-matic/blob/main/roadmap/the-space-map.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "meta-matic/ink-ribbon",
      "repo": "tor2dbear/meta-matic",
      "repoName": "Méta-Matic",
      "repoColor": "#a78bfa",
      "slug": "ink-ribbon",
      "title": "Variable-weight ink ribbon",
      "status": "done",
      "tags": [
        "render"
      ],
      "updated": "2026-08-12",
      "issue": null,
      "order": null,
      "body": "## Goal\nMake the line read as ink laid by a machine, not a uniform stroke — weight from pen\nspeed, a real ribbon at the lock moment.\n\n## Delivered\nPer-segment quad ribbon with nonzero-winding fill, width derived from pen speed.\nCheap uniform `ctx.stroke()` during the morph, easing to the full variable-weight\nribbon exactly at lock — so the expensive geometry only exists for the ~2–3 works\nnear the signing point.",
      "sourcePath": "roadmap/ink-ribbon.md",
      "sourceUrl": "https://github.com/tor2dbear/meta-matic/blob/main/roadmap/ink-ribbon.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "meta-matic/walletconnect-mobile",
      "repo": "tor2dbear/meta-matic",
      "repoName": "Méta-Matic",
      "repoColor": "#a78bfa",
      "slug": "walletconnect-mobile",
      "title": "WalletConnect for mobile Safari",
      "status": "later",
      "tags": [
        "wallet"
      ],
      "updated": "2026-08-12",
      "issue": null,
      "order": null,
      "body": "## Goal\nLet people certify with a wallet from ordinary mobile Safari, not only a wallet app's\nin-app browser.\n\n## Research\nThe certify-with-wallet flow uses injected `window.ethereum` (EIP-1193). Mobile Safari\ninjects nothing, so today it only works inside MetaMask/Rainbow/Coinbase in-app\nbrowsers. WalletConnect (QR / deep-link to the app) would cover normal mobile Safari —\na self-contained integration on top of the existing `personal_sign` flow.\n\n## Open questions\n- Worth the dependency, or is \"open in your wallet's browser\" fine for a niche art piece?",
      "sourcePath": "roadmap/walletconnect-mobile.md",
      "sourceUrl": "https://github.com/tor2dbear/meta-matic/blob/main/roadmap/walletconnect-mobile.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "pia-terminal/ai-mcp-context",
      "repo": "tor2dbear/pia-terminal",
      "repoName": "PIA",
      "repoColor": "#38bdf8",
      "slug": "ai-mcp-context",
      "title": "AI-kontext via MCP-connector",
      "status": "now",
      "tags": [
        "mcp",
        "ai"
      ],
      "updated": "2026-08-09",
      "issue": null,
      "order": null,
      "body": "## Levererat (v6 — server `instructions` / PIA presenterar sig)\n`initialize` returnerar nu ett `instructions`-fält (en hint klienten lägger i\nmodellens system-prompt) som väver ihop PIA:s persona med en bruksanvisning:\nfilsystemet, token:ns *live*-scope, och de exakta `mcp scope`-kommandona ägaren\nkör för att ändra rättigheter — så den uppkopplade AI:n kan svara \"kör `mcp scope\n<namn> --full`\" med rätt token-namn. Scope-meningen och token-labeln interpoleras\nper anslutning (auth finns redan vid `initialize`). Edge-only, ingen migration.\nDelvis leverans av `ai-host-persona`. Nästa nivå vore MCP-*resurser* (läsbar\nhjälp/changelog on demand).\n\n## Levererat (v5 — `mcp scope`)\nÄndra en befintlig tokens skrivscope in place: `mcp scope <label> --full` /\n`--write <dir>` / `--read-only` (samma flaggor som `token`). Samma hemlighet →\ningen omkoppling; edge-funktionen läser `write_scope` per request så det slår\nigenom direkt. `token`/`scope` delar nu flagg-parsningen. Rent klient-lager\n(`updateScope` på TokenStore-seamen) — ingen edge-deploy eller migration.\n\n## Levererat (v4 — on-brand URL + PIA-ikon)\nConnectorn nås nu på **PIA:s egen origin** (`pia.tor2dbear.com/mcp`) via en liten\nCloudflare Pages Function (`functions/mcp`) som reverse-proxar `/mcp/*` till edge-\nfunktionen. Klienten (Claude) hämtar då PIA:s favicon i stället för Supabases, och\nURL:en blir on-brand. Edge-funktionen härleder sin publika bas från proxyns\n`x-pia-public-base`-header så discovery/authorize/token annonserar rätt origin;\nauthorize-formuläret POST:ar same-origin. Gamla supabase.co-URL:en fortsätter funka.\nAppens första server-sido-bit — allt annat är fortsatt statiskt.\n\n## Levererat (v3 — per-token scopes)\nSkrivytan är inte längre en konstant. Varje token läser hela hemmet men *skriver*\nbara där du säger: `mcp token <label>` defaultar till `inbox/` (safe by default),\n`--write <dir>` vidgar (repeterbar; `--write .` = hela hemmet), `--read-only`\nförbjuder skrivning helt. Scopet lagras per rad (`mcp_tokens.write_scope`), edge-\nfunktionen enforcar det (parity med `src/mcp/tokens.ts`), `mcp tokens` visar det,\noch en read-only token får inte ens `pia_write` i verktygslistan. Löser öppna\nfrågan \"per-token scopes\" och US 3.\n\n## Levererat (v2 — OAuth 2.1, live)\nBearer-tokenen (v1) räckte inte för **Claudes connector-UI**, som bara stödjer\n**OAuth** (det försöker göra Dynamic Client Registration, inte ta en inklistrad\ntoken). Så `mcp`-funktionen talar nu även **OAuth 2.1**, routat på path:\ndiscovery (`/.well-known/oauth-protected-resource` + `-authorization-server`),\n`/register` (DCR), `/authorize`, `/token` (authorization-code + PKCE), och\n`WWW-Authenticate` på 401 så klienten hittar in.\n\n**Idiom-valet (Väg 2, användarens beslut):** `/authorize` är en **terminal-stylad\nsida** som bara ber om en token du myntat med `mcp token` — ingen egen\ninloggnings-sida, terminalen förblir sanningskällan. Access-token som `/token`\nlämnar ut *är* den myntade tokenen, så hela auth-lagret återanvänds oförändrat.\nNya tabeller `oauth_clients` + `oauth_codes` (RLS på, inga policies → bara\nservice role; `oauth_codes` håller en rå token i max 10 min mellan authorize och\nexchange, sen raderas den). Hela flödet verifierat end-to-end mot live-funktionen.\nBearer-vägen (terminal + egna skript) kvar oförändrad.\n\n**Kvar:** verifiera från Claude på iOS mot den deployade connectorn.\n\n## Levererat (v1 — kod klar, deploy kvar)\n`mcp`-kommandot + en Supabase Edge Function som exponerar användarens\nfilsystem-rad som en **remote MCP-server**. Besluten på de öppna frågorna:\n\n- **Host: Supabase Edge Function** (inte Cloudflare Worker som pucken gissade).\n  Skäl: ingen Worker-infra fanns, medan Supabase redan är uppsatt; och\n  `SUPABASE_URL`/`SUPABASE_SERVICE_ROLE_KEY` injiceras automatiskt i edge\n  functions, så det finns **ingen hemlighet att kopiera in för hand**. Kostnad:\n  free-tier räcker (personligt bruk ≈ 0 kr).\n- **Auth: scoped bearer-token**, inte OAuth (OAuth = senare). `mcp token <label>`\n  myntar, visar plaintext **en gång**, lagrar bara SHA-256-hashen. Edge-funktionen\n  hashar presenterad token likadant och slår upp raden (service role).\n- **Scope: read allt, write bara under `inbox/`.** Löser \"AI skriver över filer\n  osett\" — allowlisten sitter som konstant i edge-funktionen; write återanvänder\n  filesystems optimistic-concurrency-guard (retry en gång vid krock).\n- **Logged-in-only by design** — guests har ingen delad rad att nå; `mcp` svarar\n  ärligt \"run `login`\" (speglar `notify`).\n\nPjäser: `src/mcp/tokens.ts` (söm: Null/Memory + token/hash-helpers),\n`src/supabase/tokens.ts` (Supabase-store), `src/commands/mcp.ts`,\n`supabase/mcp.sql` (tabell `mcp_tokens` + RLS), `supabase/functions/mcp/index.ts`\n(JSON-RPC-server, verktyg `pia_list`/`pia_read`/`pia_write`). Tester + tour-rad\nför guest-vägen.\n\n**Kvar (rör produktion, görs på ägarens ok):** kör `supabase/mcp.sql` mot\nprojektet och `supabase functions deploy mcp --no-verify-jwt`. Sen: verifiera från\nen riktig AI-klient (Claude iOS custom connector) mot den deployade URL:en.\n\n## Mål\n(Research, inte beslutat.) Låta en AI läsa/skriva PIA:s filer från en chatt —\n\"kolla `docs/portfolio/`\", \"lägg en notis i `inbox/`\". Idén: PIA exponerar sitt\neget filträd som en **remote MCP-server**, så vilken AI-klient som helst (Claude\npå iOS ingår) kan koppla in som användaren.\n\n## Research\n- **Mekanism:** filträdet lever redan bakom `StorageAdapter`. En MCP-yta blir\n  ännu en konsument av samma träd — samma söm, exponerad *utåt* istället för\n  nedåt mot storage. Ingen ny säkerhetsmodell: Supabase-RLS scopar per användare.\n- **Delad DB:** förutsätter inloggad Supabase-användare — guest-läget\n  (localStorage) lever bara i fliken och kan inte nås utifrån. Featuren är\n  logged-in-only by design.\n- **Host:** remote MCP kräver en HTTP-server. Passar en **Cloudflare Worker** i\n  samma projekt — ingen ny infra. Verktyg: `list`/`read` (+ ev. `write`).\n- **Auth:** enklast en scoped bearer-token man genererar i PIA (`pia token`,\n  `pia token revoke`) och klistrar in i AI-klienten. OAuth är \"rätt\" men\n  överkurs för v1.\n- **iOS:** custom connectors via remote MCP funkar i Claude på iOS (även Free) —\n  connectorn läggs till på claude.ai i webben och *synkar* till mobilen.\n- **Supabase free räcker:** textfiler är kb, inte MB. Enda fällan är\n  7-dygns-inaktivitetspausen — redan hanterad av keep-alive (PR #6–7).\n\n### Avfärdat: PIA som backend för KB-systemet\nVägde att låta det mejl-drivna KB-systemet (Gmail-capture → Synology-worker →\n`kb/` git + daglig digest) bo i PIA. **Nej.** KB:ns ryggrad är en\nsekretessgradient (NAS-only → git-speglat offsite → efemär digest). Supabase\n*är* offsite, så att lägga KB där punkterar precis den vägg systemet är byggt\nrunt. Dessutom löser GitHub-connectorn redan \"läs min `kb/` i en chatt\" — `kb/`\när ju redan ett git-repo offsite. PIA tillför inget där. Håll projekten separata.\n\n### Ärlig omframning\nSom *produktivitetsverktyg* är den här svag — den drar PIA mot commodity-nytta\ndär PIA saknar edge. Den lever bara omframad till PIA:s faktiska skäl: **lärande\n+ portfolio + kul**. Att bygga en egen MCP-connector är en aktuell, CV-stark\nsignal (2026) och on-brand om den exponerar PIA:s *egna* leksaks-fs (\"chatta med\nmin lilla dator\") — inte om den blir \"personlig moln-databas\".\n\n## User stories\nKonkretiserar riktningen **filer *ut* till en extern AI** (motsatt riktning —\nen röst *in* i PIA — bor i `ai-host-persona`). Håll dem inramade i PIA:s faktiska\nskäl (*lärande + portfolio + kul*, \"chatta med min lilla dator\"), inte som\n\"personlig moln-databas\" — se den ärliga omframningen ovan.\n\n- **US 1 — fånga en tanke från mobilen.**\n  *Som* PIA-användare på språng *vill jag* be Claude på iOS lägga en rad i min\n  PIA-inbox *så att* idén hamnar i min lilla dator utan att jag öppnar terminalen.\n  Flöde: agenten är kopplad som MCP-klient, skriver `~/inbox/<datum>.md`; nästa\n  boot visar `cat inbox/<datum>.md` raden. Samma fil, två klienter. Kräver\n  `write` (scoped, se öppna frågor) — läs-först-varianten faller tillbaka på US 2.\n- **US 2 — låt en agent läsa och sammanfatta (read-only).**\n  *Som* någon med en rörig `notes/` *vill jag* be en agent sammanfatta veckan\n  *så att* jag får överblick utan att läsa allt. Agenten `list`/`read` över MCP;\n  resultatet klistras tillbaka av mig (eller skrivs till fil om `write` finns).\n  **Detta är v1** — ren `list`/`read`, ingen skrivrisk, on-brand (\"chatta med min\n  lilla dator\").\n- **US 3 — skrivbar bara där det är ofarligt.**\n  *Som* försiktig ägare *vill jag* att en agent bara får skriva i utpekade mappar\n  (t.ex. `inbox/`) *så att* \"AI skriver över filer osett\" aldrig kan hända i\n  `docs/` eller `.pia/`. Motsvarar öppen fråga om skriv-scope + ev. diff-gate.\n\n## Öppna frågor (kvar efter v1)\n- **Per-token scopes.** ✅ Levererat i v3 (se ovan) — `--write`/`--read-only`,\n  lagrat per rad, enforced i edge-funktionen.\n- **Diff-/bekräftelse-gate på write.** v1 skriver rakt (guardad mot krock men utan\n  människa-i-loop). Vill vi ha en förhandsgranskning innan en agent-write landar?\n- **OAuth istället för klistrad token.** \"Rätt\" men överkurs — bearer räcker för v1.\n- Värt det jämfört med motor-extraktionen (se `terminal-engine-package`)? Bägge\n  kan leva; MCP är mer aktuellt, motorn visar djupare ingenjörskonst.\n\n_Befordrad till `now` (kod klar). Går till `done` när funktionen är deployad och\nverifierad från en riktig AI-klient._",
      "sourcePath": "roadmap/ai-mcp-context.md",
      "sourceUrl": "https://github.com/tor2dbear/pia-terminal/blob/main/roadmap/ai-mcp-context.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "pia-terminal/ai-host-persona",
      "repo": "tor2dbear/pia-terminal",
      "repoName": "PIA",
      "repoColor": "#38bdf8",
      "slug": "ai-host-persona",
      "title": "AI-värd-persona (och ev. ask-kommandot)",
      "status": "inbox",
      "tags": [
        "persona",
        "ai"
      ],
      "updated": "2026-08-08",
      "issue": null,
      "order": null,
      "body": "## Mål\n(Research.) README:s öppna fråga sedan start: en namngiven \"diskret värd\" — en\nAI-persona som hälsar vid boot och svarar torrt ibland. Plus Nivå 4:s\n`ask <fråga>`. Den här pucken slår ihop dem, för det är samma tråd: **karaktär,\ninte nytta.**\n\n## Research\n- **Persona ≠ LLM.** Värden kan vara ren karaktär: skriptade/torra repliker vid\n  boot och i vissa lägen, helt utan modell. Det är kul, on-brand och gratis att\n  köra. Börja där.\n- `ask <fråga>` (en LLM som svarar i terminalen) är motsatsen till det som sades\n  i AI-kontext-spåret (\"inte att en LLM ska *bo* i terminalen, iaf initialt\").\n  Som *nytta* är det commodity-drift — wrappar bara en modell, och drar in nyckel\n  + kostnad (web-divergens).\n- Omframat som **persona-röst** blir `ask` däremot on-brand: inte \"en assistent\",\n  utan värdens karaktär som råkar kunna svara. Det är skillnaden mellan commodity\n  och lek.\n- Koppling: `ai-mcp-context` handlar om filer *ut* till en extern AI. Den här\n  handlar om en röst *in* i PIA. Olika riktningar — förväxla inte.\n\n## User stories\nRiktningen **en röst *in* i PIA** (motsatt `ai-mcp-context`, som skickar filer\n*ut*). Ramen är persona, inte assistent — se research ovan.\n\n- **US 1 — värden hälsar med karaktär (ingen modell).**\n  *Som* återvändande användare *vill jag* mötas av en torr, skriptad rad vid boot\n  *så att* PIA känns som en *någon*, inte ett tomt skal. Ren karaktär, gratis att\n  köra — **bygg den här delen först.**\n- **US 2 — fråga värden om mina egna filer (`ask`, om lusten finns).**\n  *Som* van terminalanvändare *vill jag* skriva `ask \"sammanfatta todo.md som tre\n  punkter\"` *så att* jag får svar där jag redan är, med tillgång till min VFS.\n  On-brand bara som **värdens röst** (inte \"en assistent\") — annars commodity-drift.\n  Drar in nyckel/kostnad (web-divergens): flagga före bygge, se öppna frågor.\n\n## Öppna frågor\n- Namn på värden (README lämnade det öppet).\n- Räcker skriptad persona (ingen modell) för det mesta av charmen? Troligen ja —\n  bygg den delen först, LLM-`ask` bara om lusten finns.\n- Om `ask` med modell: nyckel/kostnad-modellen (BYOK? proxy?) — samma\n  web-divergens som i `ai-mcp-context`, flagga före bygge.\n\n_Ligger i `inbox`; persona-delen kan befordras utan att `ask` följer med._",
      "sourcePath": "roadmap/ai-host-persona.md",
      "sourceUrl": "https://github.com/tor2dbear/pia-terminal/blob/main/roadmap/ai-host-persona.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "pia-terminal/brew-install-progress",
      "repo": "tor2dbear/pia-terminal",
      "repoName": "PIA",
      "repoColor": "#38bdf8",
      "slug": "brew-install-progress",
      "title": "brew install: ärlig installationsceremoni",
      "status": "now",
      "tags": [
        "packages",
        "system"
      ],
      "updated": "2026-08-08",
      "issue": null,
      "order": null,
      "body": "## Levererat (Nivå 1 + 2)\n`brew install <name>` visar nu de riktiga stegen istället för en enda rad:\n`==> Fetching <name>… (8.6 kB)` (brackettar den äkta dynamiska\n`import()`-hämtningen, med paketets **verkliga gzip-storlek**) →\n`==> Registering: <commands>` → `installed <name> ✓`. Ingen påhittad tid — bara\nde steg som faktiskt händer.\n\n**Nivå 2-mekanik:** en vite-plugin (`packageSizes` i `vite.config.ts`) räknar\nvarje pakets gzip-chunkstorlek i `generateBundle` och emitterar\n`package-sizes.json`. Storleken finns bara *efter* bundling, så den kan inte bli\nen compile-time `define` som `VERSION` — den byggda appen fetchar assetten\nsame-origin (CSP `connect-src 'self'`, likt `ping`) via `src/packages/sizes.ts`,\nmemoiserat och best-effort (null → storleken utelämnas). Grindat på\n`import.meta.env.PROD`, så dev/test/tour aldrig fetchar → deterministiskt.\n\n**Kvar:**\n- **Nivå 2b:** animerad bar/spinner under laddningen — kräver en uppdaterbar\n  utdatarad. Egen puck: `live-output-line.md`.\n- **Nivå 3:** kosmetisk pacing (fejka tid) — öppen fråga, default avvisa.\n\n## Mål\nGe `brew install` en känsla av att något faktiskt *installeras* — men **ärligt**,\ninte som en fejkad nedladdningsbar. Idag komprimeras installen till en osynlig\nblink + en rad. Observationen som väckte det: man saknar att saker \"installeras\".\n\n## Research\n\n### Det finns redan ett äkta async-moment\n`brew install <name>` (`src/commands/brew.ts:50`) kör `registerPackage`, som\nanropar `entry.load()` = `() => import(\"./cowsay/index.js\")` — en **riktig\ndynamisk import av en separat JS-chunk**. Det är en genuin async-hämtning av ett\nverkligt artefakt med en verklig storlek (samma chunkar man ser i\n`npm run build`-outputen, t.ex. `cowsay` ~X kB gzip).\n\nStegen som faktiskt händer idag:\n1. `import()` av chunken — riktig fetch + parse, verklig storlek.\n2. Registrerar kommandon i registret.\n3. Skriver `~/.pia/packages`, persistar.\n4. Skriver *en* rad: `installed cowsay — commands: cowsay, cowthink`.\n\nAlla fyra är verkliga. Ceremonin finns liksom redan — den visas bara inte. Därför\nkan en progress-känsla ges *ärligt*. Det här är den \"Option C\" som pv saknade i\npipes (mätare framför något genuint async) — `brew install` **är** den producenten.\n\n### Tre nivåer\n- **Nivå 1 — visa de riktiga stegen (ren ärlighet).**\n  ```\n  $ brew install cowsay\n  ==> Fetching cowsay…          ← under den riktiga import()\n  ==> Registering: cowsay, cowthink\n  installed cowsay ✓\n  ```\n  Ingen påhittad fördröjning — bara en obestämd spinner medan den *faktiska*\n  importen pågår (en `import()` ger ingen byte-progress, så obestämt = det ärliga).\n  Billigt, ren vinst; stegen finns redan.\n\n- **Nivå 2 — ärlig storlek + bar.**\n  Injicera varje chunks *verkliga* gzip-storlek vid bygget (samma mönster som\n  `VERSION` redan injiceras — en liten vite-plugin som skriver ett\n  storleks-manifest). Då kan installen visa en determinerad bar mot den **sanna**\n  siffran: `cowsay  ▓▓▓▓▓▓▓▓▓▓  8.6 kB`. Baren fylls snabbt (importen är nära\n  direkt), men siffran är *äkta bytes, inte vibbar*. Det är den eleganta detaljen.\n\n- **Nivå 3 — kosmetisk pacing (gränsen).**\n  Padda med en konstgjord sleep så baren glider långsammare = att fejka tid.\n  Exakt linjen vi drog för äggen och pv.\n\n### `apt`-aliaset\n`brew` har redan `apt` som alias. Riktiga `apt` har sin egen ceremoni\n(\"Get:1… Unpacking… Setting up…\"). Ett `apt install` skulle kunna spegla pia:s\n*verkliga* steg med apt-flavored etiketter — ärligt, för stegen händer på riktigt.\nFin idiom-detalj utan lögn.\n\n## Beslutat spår\n- **Nivå 1:** ja — billig, ärlig, ren vinst.\n- **Nivå 2:** ja i mån av bygg-stöd — det är där \"äkta bytes\"-magin sitter.\n  Kräver storleks-manifest injicerat vid bygget.\n\n## Öppna frågor\n- **Nivå 3 (kosmetisk sleep):** avvisa rakt av, eller tillåt som ett *dokumenterat*\n  ceremoni-beslut (accepterad divergens à la `share→URL`)? Default: avvisa.\n- **Storleks-manifest:** hur surfa den verkliga gzip-chunkstorleken vid bygget\n  (vite-manifest/plugin)? Genomförbarhet + hur den matas in i runtime (som\n  `VERSION`).\n- **Determinism i touren:** importens timing är icke-deterministisk. Visa\n  *storlek* (deterministiskt) snarare än tid, eller redigera progress-raderna i\n  golden så touren förblir stabil.\n- **Uninstall:** ska `brew uninstall` få en motsvarande (kort) ceremoni, eller\n  hålla sig till dagens enda rad?",
      "sourcePath": "roadmap/brew-install-progress.md",
      "sourceUrl": "https://github.com/tor2dbear/pia-terminal/blob/main/roadmap/brew-install-progress.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "pia-terminal/pkg-pv",
      "repo": "tor2dbear/pia-terminal",
      "repoName": "PIA",
      "repoColor": "#38bdf8",
      "slug": "pkg-pv",
      "title": "paket: pv — pipe viewer (ärlig mätare)",
      "status": "inbox",
      "tags": [
        "packages"
      ],
      "updated": "2026-08-08",
      "issue": null,
      "order": null,
      "body": "## Mål\n`pv` (Pipe Viewer) — det klassiska Unix-verktyget som sitter mitt i en pipe och\nvisar genomströmning: bytes, hastighet, ETA, en bar som fylls. Idé: ta in det som\n`brew`-paket (som `sl`/`cowsay`/`coreutils`), men **utan att ljuga om maskinen**\n(samma linje som påsk-äggen, se `roadmap/easter-eggs.md`).\n\n## Research\n\n### Utgångspunkt (kul disambiguering)\nIdén väcktes av en Google-AI-översikt som inte kunde bestämma sig för om\n\"PV-terminal\" betyder Linux-`pv` eller *solcellsterminaler* (photovoltaic). Vi\nmenar `pv`.\n\n### Den avgörande begränsningen: pia:s pipes är buffrade, inte strömmande\n`executePipeline` (`src/terminal/terminal.ts:1190–1226`) kör stegen i en vanlig\nfor-loop, ett i taget:\n\n```\ninput = \"\"\nför varje steg:\n    kör steget med stdin = input   ← väntar tills det är HELT klart\n    input = allt steget skrev ut (hopfogat till EN sträng)\n```\n\nAlltså: **sekventiellt och helbuffrat.** Nästa steg börjar inte förrän det förra\nreturnerat. Riktiga Unix-pipes är tvärtom *samtidiga* (alla processer lever på en\ngång, kärnbuffertar emellan, data rinner igenom allteftersom, med backpressure).\n\nKonsekvens: **`pv` har inget flöde att titta på.** När `pv` väl kör finns hela\ndess input redan som en färdig sträng, och hela dess output produceras innan\nnästa steg vaknar. En \"live\"-bar skulle hoppa 0→100 direkt — dvs. fejk.\n\n### Alternativen\n- **A) Ärlig engångsmätare (default).** `pv` släpper igenom stdin→stdout\n  oförändrat och skriver *en* sann rad: bytes som passerade (`[passed]`). Ingen\n  animerad bar (inget att animera). Ärliga flaggor: `-b` (byteräknare), `-n`\n  (numeriskt). Trogen men blygsam — i praktiken en `wc -c` mitt i röret.\n- **B) `pv -L <rate>` — den ärliga animationen.** Rate-limit *pacar data på\n  riktigt*: för att hålla \"max X kB/s\" måste `pv` portionera ut sin egen output\n  över tid. Den fördröjningen är äkta (du bad om den), så baren får röra sig\n  sanningsenligt. Kul \"skrivmaskinseffekt\": `cat dikt.txt | pv -L 20`.\n  Nyans: i buffrad modell strypér `pv` sitt *eget* genomsläpp, men nästa steg\n  väntar ändå och kör sen direkt — så `-L` skapar ingen riktig backpressure\n  nedströms. Men det *lovar* det inte heller; löftet \"≤X kB/s\" hålls sant.\n- **C) `pv` framför något redan långsamt.** Äkta långsamhet finns i `ping`,\n  `python`, remote-cat/ls — men de matar inte byte-ström-pipes. Återvändsgränd\n  i nuvarande arkitektur.\n\n### Nyckelinsikt: mätarens rätta hem är `brew install`, inte pipes\nDen ärliga genomströmningsmätaren hör egentligen hemma där det *finns* ett äkta\nasync-moment med känd storlek: `brew install` gör en riktig dynamisk\n`import()` av en chunk. Se syskonpucken `brew-install-progress.md`. `pv` i pipes\nförblir A + (ev.) B.\n\n## Beslutat spår (om/när det byggs)\nSkeppa **A + B**: ärlig direkt-summering som default; `-L` som ärligt portionerad\nmed levande bar. **Aldrig** fejkad ETA/bar utan `-L`.\n\n## Öppna frågor\n- **Värt att bygga alls?** Utan flöde är `pv` i pipes mest en gest. Kanske hellre\n  lägga energin i `brew-install-progress` och hålla `pv` minimal — eller skippa.\n- Determinism i touren: A ger en byte-siffra (deterministisk, OK); `-L` innebär\n  tidsberoende animation → måste redigeras/undvikas i golden.\n\n## Arkitektur-sidospår: async/strömmande pipes — för/nackdelar\nDet enda som gör `pv` (och `tail -f`, `yes`) *äkta by default* är att byta\npipeline-modellen från buffrad-sekventiell till **samtidig-strömmande** (alla\nsteg lever samtidigt, kopplade av async-kanaler med backpressure). Stor sak:\n\n**Skulle kräva:** nytt kommandokontrakt (stdin blir en async-iterator av bitar\nistället för en sträng; varje kommando skrivs om eller körs genom en buffrande\nshim) + en schemaläggare som kör alla steg samtidigt med begränsade buffertar.\n\n**Fördelar**\n1. `pv` blir äkta by default (mätning i flykten, sann ETA).\n2. Konstant minne för stora data. *(Dämpat här: pia:s VFS ligger ändå i RAM.)*\n3. Tidig avslutning (`yes | head -1` stoppar producenten) → oändliga strömmar\n   blir uttryckbara (`yes`, `tail -f`, live-flöden).\n4. Äkta Unix-idiom (backpressure, tidig exit) — stort för \"lär dig terminalen\".\n5. Levande output (spinners, live-filter).\n\n**Nackdelar / kostnader**\n1. Enormt kontraktsbyte: varje kommando + ScreenApp-värd + tester skrivs\n   om/shimmas. Hög regressionsrisk i en mogen svit.\n2. Komplexitet: backpressure, fel mitt i en ström, halvskrivna redirect-filer,\n   svårare Ctrl-C (riv ner alla levande steg).\n3. **Determinism — dyrast.** Touren (golden-snapshot) är pia:s centrala\n   verifiering och bygger på fullständigt ordnad output. Samtidiga steg som\n   varvar utskrifter → icke-deterministisk ordning → golden-modellen skadas.\n4. Redirect/capture måste bli inkrementella.\n5. DOM-genomströmning kräver batchning (rAF) för många småbitar.\n6. **Liten payoff:** pia:s pipes är korta, små, mänskliga (`sort | uniq`). De\n   problem streaming löser saknas mest.\n\n**Mellanväg (rekommenderad):** behåll buffrad modell; special-lös de få\nströmmiga verktygen. `pv -L` animerar sig självt (alt B); `yes`/`tail -f` byggs\nsom screen-appar eller kommandon med egen intern loop + Ctrl-C, inte som\npipeline-steg. ~80 % av idiomet för ~5 % av kostnaden. Full streaming övervägs\n*först* om ett framtida mål (live-flöden, big-data-pedagogik) gör det till ett\nförstahandskrav — och även då väger determinism-kostnaden tungt.",
      "sourcePath": "roadmap/pkg-pv.md",
      "sourceUrl": "https://github.com/tor2dbear/pia-terminal/blob/main/roadmap/pkg-pv.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "pia-terminal/easter-eggs",
      "repo": "tor2dbear/pia-terminal",
      "repoName": "PIA",
      "repoColor": "#38bdf8",
      "slug": "easter-eggs",
      "title": "påskägg: policy + shortlist",
      "status": "done",
      "tags": [
        "system",
        "packages"
      ],
      "updated": "2026-08-08",
      "issue": null,
      "order": null,
      "body": "## Mål\nBestäm hur pia förhåller sig till **påskägg** — de dolda skämt-/lek-svar som\nportfolions terminaltema (`tor2dbear/portfolio`) har en hel katalog av, indexerad\nbakom ett dolt `easteregg`-kommando. Frågan är inte \"vilka skämt ska vi lägga in\"\nutan \"vilken sorts ägg får plats i en maskin som säger sig vara riktig\". Det här\när en policy-puck: den fastslår regeln först, så eventuella divergenser blir\n*beslut, inte drift* (jfr CLAUDE.md, \"terminal-idiom first\").\n\n## Research\n\n### Vad portfolion har\nKatalog i `assets/js/terminal-data.js` (`TERMINAL_EASTER_EGGS`) + implementation\ni `assets/js/terminal.js`, grupperad som:\n\n- **Unix-verktyg:** `sl`, `cowsay`, `moo`, `matrix`, `logo/ascii` (figlet),\n  `weather`.\n- **Skämtsvar:** `sudo` → \"permission denied: nice try 😏\", `coffee/brew` →\n  HTTP 418 teapot, `rm -rf /` → \"it's all in git\", `git blame/commit/push`,\n  `npm i` → fejkad resolve, `vim/emacs/nano` → editor-skämt, `:wq`/`:q` →\n  \"you can just type exit\".\n- **Ren lek:** `xyzzy` → \"Nothing happens.\", `42`/`answer`, `ping` → pong,\n  `hello/hi/hey`, `konami` → party mode (↑↑↓↓←→←→ba, regnbågsläge), `fortune`.\n- **Riktiga-ish statuskommandon:** `man`, `uname`, `colour`, `history`,\n  `uptime`, `top`, `debug/env`, `reset`.\n\nPortfolion håller `easteregg`-indexet utanför `help` med flit (\"a discovery, not\na menu\") — en bra upptäckbarhets­idé värd att behålla.\n\n### Den centrala insikten\n**pia har redan \"tagit examen\" på större delen av listan — men som riktiga\nfunktioner, inte skämt.** Det är skiljelinjen mot portfolion, där skämt är\ncharmlagret ovanpå en i grunden statisk sida:\n\n- `sl`, `cowsay` (inkl. `moo`/super cow), `fortune`, `cmatrix`, `figlet` finns\n  redan som **installerbara brew-paket** (egna pucker: `pkg-sl`, `pkg-fortune`,\n  `pkg-cmatrix`, `pkg-figlet`).\n- `sudo` **funkar på riktigt** (höjer rättigheter, skriver skyddade filer). Ett\n  fejkat \"permission denied: nice try\" skulle *motsäga* maskinen.\n- `nano` är en **riktig editor**; `man`, `ping`, `date`, `history`, `neofetch`\n  är **riktiga kommandon**; `brew` är en **riktig pakethanterare** (så `coffee`\n  får skämta, men `brew` får aldrig).\n\nSlutsats: portfolions äggfilosofi krockar med pia:s principer. I pia är ett\nfejkat `sudo`-avslag eller `rm -rf /`-skämt inte charmigt — det bryter\nillusionen av en maskin som fungerar.\n\n### Policy — tre hinkar (beslutad)\n1. **Redan gjort — rör inte.** De klassiska verktygen är redan riktiga paket.\n   pia:s modell (opt-in via `brew`, upptäckbar, ärlig) är *bättre* än portfolions\n   dolda variant. Låt dem vara.\n2. **Ta in som äkta terminaltradition.** Ägg som *är* Unix-folklore och inte\n   ljuger om maskinen. Levererat nedan.\n3. **Avvisa — krockar med den riktiga maskinen.** Fejkat `sudo`-avslag, fejkat\n   `rm -rf /`, fejkade `git`/`npm`-stubbar. Dessa kommandon gör (eller kan göra)\n   något på riktigt i pia; en skämtstubbe motsäger det.\n\n## Levererat (första omgången)\n`src/commands/eggs.ts` — fem dolda kommandon, alla i hink 2:\n\n- `xyzzy` → \"Nothing happens.\" (bsdgames/Colossal Cave).\n- `coffee` → HTTP 418 \"I'm a teapot\" (RFC 2324). **Eget kommando, aldrig alias\n  på `brew`** — den riktiga pakethanteraren får inte skuggas.\n- `vim`/`vi`/`emacs`/`pico` → \"this machine ships `nano` — try that\". Vänlig\n  redirect för editor-muskelminne; `nano` finns på riktigt.\n- `ed` → \"ed is the standard text editor.\" (den äldsta editor-gaggen i Unix).\n- `:q`/`:q!`/`:wq`/`:wq!`/`:x` → \"you're at the shell — type `exit`\". Fångar\n  vi-quit-reflexen.\n\n**Mekanik:** ny `hidden?`-flagga på `Command`. Dolda kommandon hålls utanför\n`help`-listan *och* `namesStartingWith` (som driver både Tab-completion och\n`man`/`apropos`-topiclistan), men är fortfarande körbara och nåbara via ett\nexplicit `man <namn>`/`help <namn>` om du redan kan namnet. Tester i\n`eggs.test.ts` + rader i touren. `42`/`answer` ströks (Hitchhiker's, inte\n*terminal*-idiom).\n\n## Beslut på de tidigare öppna frågorna\n- **Upptäckbarhet:** *inget* `easteregg`-index (o-Unix, själv-motsägande meny).\n  Äggen är genuint odokumenterade; de självavslöjas genom naturlig användning\n  (man skriver `vim`/`coffee` ändå) eller via kultur (`xyzzy`). `man <egg>` är\n  belöningen för den redan invigde.\n- **Var bo:** kärnregistret, inte ett `brew`-paket. Hink 2 är *reaktioner på\n  något man ändå skriver* — magin är att de redan finns; en install-grind\n  (`brew install eggs`) dödar poängen. Paket-mönstret är för *verktyg* (`sl`,\n  `cowsay`, `fortune`), inte snubbla-på-reaktioner.\n- **`coffee` vs `brew`:** löst — separat kommando, test som vaktar att `brew`\n  fortfarande resolvar till pakethanteraren.\n\n## Framtid (följer policyn ovan — ingen ny puck krävs)\n- **`konami`/party mode:** parkerad. Ren web-divergens (visuellt läge, ingen\n  Unix-motsvarighet) — hör hemma i *utseende*-arbetet (`crt-retro-mode` /\n  `settings-appearance`, redan live), inte här. \"Ett tema man låser upp med en\n  kod\", om det någonsin byggs.\n- Fler hink 2-ägg kan läggas till i `eggs.ts` när de dyker upp, så länge de inte\n  ljuger om maskinen.",
      "sourcePath": "roadmap/easter-eggs.md",
      "sourceUrl": "https://github.com/tor2dbear/pia-terminal/blob/main/roadmap/easter-eggs.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "pia-terminal/publish-garden",
      "repo": "tor2dbear/pia-terminal",
      "repoName": "PIA",
      "repoColor": "#38bdf8",
      "slug": "publish-garden",
      "title": "publish → digital garden (index, styling, RSS)",
      "status": "inbox",
      "tags": [
        "share",
        "web"
      ],
      "updated": "2026-08-08",
      "issue": null,
      "order": null,
      "body": "## Mål\n(Research, inte beslutat.) Lyfta `publish` (levererad, se `publish-folder`) från\n\"en mapp landar i mottagarens `~/incoming/`\" till en riktig **läs-vy för\nomvärlden**: en publik, stylad sida med en **index/omslag**, läsbar typografi,\noch ett **flöde** (RSS/Atom) — så terminalen blir en enkel CMS för det du redan\nskriver i `nano`. On-brand: du äger redan domänen och skrivytan; det här är att\n*visa upp* filerna, inte en ny app.\n\n## Research\n- **Var vi står:** `publish <mapp>` packar `.md`-filerna self-contained i en\n  `#p=`-hash och materialiserar dem i mottagarens *egen session* (`~/incoming/`),\n  läst med `glow`/`cat`. Ingen server, funkar för guests. Det är en **dela-med-\n  en-person**-vy, inte en **publicera-för-världen**-vy — den här pucken är det\n  senare, ett steg *bredvid* det som redan finns, inte en ändring av det.\n- **Spänningen som måste spikas först — hash vs URL.** Dagens `#p=` är genialt\n  serverlöst men (a) taket är `MAX_PUBLISH_PAYLOAD` = 32 KB, (b) länken är ful och\n  odelningsbar i praktiken, (c) den renderas i *mottagarens* terminal, inte som en\n  sida en främling kan läsa utan att förstå PIA. En \"garden\" vill ha en **stabil,\n  ren URL** (`/g/<slug>`) och en **HTML-sida** som står på egna ben. Det bryter\n  mot serverlös-principen — kräver antingen Cloudflare Pages Functions / en Worker\n  + lagring (Supabase-rad per publicerad garden), eller ett statiskt build-steg.\n  **Detta är kärnvalet i pucken — spika det innan något byggs.**\n- **Idiom-flagga (terminal-first):** `publish` är redan en accepterad web-\n  divergens (returnerar URL). En garden utökar den divergensen (index, RSS) — inte\n  ett nytt Unix-verb utan mer webb bakom samma verb. Kalla det vad det är.\n- **Renderaren finns till hälften:** `glow`/`cat` läser redan markdown i\n  terminalen; en publik HTML-vy behöver en egen (minimal) md→HTML + tema. Håll den\n  liten och CSP-säker (samma disciplin som resten av `dist/`).\n\n## User stories\n- **US 1 — blogga från terminalen.**\n  *Som* person som vill skriva publikt *vill jag* skriva markdown i `nano` och\n  publicera en mapp med ett kommando *så att* terminalen är min CMS.\n  `nano garden/2026-solresor.md` → `publish garden/` → `https://…/g/<slug>` med en\n  index-sida som listar inläggen (nyast först) och en läsvy per inlägg.\n- **US 2 — dela en enskild fil som en ren länk.**\n  *Som* någon som snabbt vill dela en anteckning *vill jag* `publish note.md` *så\n  att* jag får en kort, ren URL att klistra in i ett samtal — läsbar av vem som\n  helst utan att de behöver förstå PIA. (Snabbaste vägen till att *andra* rör vid\n  PIA, och en krok tillbaka till kontoskapande.)\n- **US 3 — ett flöde att prenumerera på.**\n  *Som* återkommande läsare *vill jag* att en publicerad garden exponerar\n  `/g/<slug>/feed.xml` *så att* jag kan följa den i en RSS-läsare.\n\n## Öppna frågor\n- **Hash vs server-URL** (se research) — det blockerande valet. Går det att göra\n  \"ren URL + HTML-sida\" *utan* att ge upp serverlöst helt (t.ex. en tunn Pages\n  Function som bara packar upp `#p=`-payloaden till en läsbar sida på en snygg\n  route)? Eller kräver index/RSS oundvikligen lagring?\n- Guest vs inloggad: kan en guest publicera en beständig garden, eller kräver\n  stabil URL inloggning (som `ai-mcp-context` landade i för sin write-yta)?\n- Storlek: 32 KB-taket räcker inte för en växande garden — vad ersätter det om vi\n  går server-vägen?\n- Överlappar `linked`/`publish`/`share` — rita om ansvarsgränsen innan bygge så vi\n  inte får tre nästan-lika verb.\n\n_Ligger i `inbox` tills hash-vs-URL-valet är fattat. Befordra till `next/later` då._",
      "sourcePath": "roadmap/publish-garden.md",
      "sourceUrl": "https://github.com/tor2dbear/pia-terminal/blob/main/roadmap/publish-garden.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "pia-terminal/shared-file-roles",
      "repo": "tor2dbear/pia-terminal",
      "repoName": "PIA",
      "repoColor": "#38bdf8",
      "slug": "shared-file-roles",
      "title": "Roller på delade filer (owner/editor/viewer)",
      "status": "next",
      "tags": [
        "collab",
        "permissions",
        "supabase"
      ],
      "updated": "2026-08-08",
      "issue": null,
      "order": null,
      "body": "## Levererat (Slice A, 2026-08-07)\nRollmodell på delade listor — kollaborationens motsvarighet till `/etc`-skyddet:\n- **Roll per medlem** (`owner`/`editor`/`viewer`) i `shared_list_members.role` +\n  `shared_list_invites.role`. RLS: `update` på `shared_lists` kräver\n  `can_edit_list` (owner/editor) — viewer blockeras *på servern*, inte bara i\n  klienten. Skaparen blir `owner`; en migration backfillar äldre installationer.\n- **Read-only delning:** `todo share <name> <email> --ro` bjuder in som viewer;\n  `--rw` (default) som editor. `Todo`-appen har ett read-only-läge (viewer):\n  navigering funkar, muteringar är inerta, UI:t säger \"(read-only)\".\n- **Ägaryta:** `todo members <name>` (medlemmar + roller, à la `ls -l`, inkl.\n  pending invites), `todo unshare <name> <email>` (owner tar bort en medlem).\n  Nya SECURITY DEFINER-RPC:er: `remove_member`, `delete_list`, `my_shared_lists`\n  (roll i ett anrop), `list_members`. `leave_list` skyddar mot att ende ägaren\n  lämnar och föräldralöser listan.\n- **Invite-rätt:** editor + owner (Google-Docs-modellen); viewer aldrig.\n- Täckt av `store.test` (roll-semantik: owner/editor/viewer, viewer-save nekad,\n  editor får bjuda in, owner remove/delete, members-lista, sole-owner-leave) och\n  terminal-tester i `todo.test` (`--ro` öppnar read-only; `members`; `unshare`).\n- **SQL:en (`supabase/shared_lists.sql`) måste köras i Supabase SQL-editorn** för\n  att aktivera rollerna i molnet (idempotent, uppgraderar befintlig install).\n\n## Kvar (Slice B)\nRoll-byte (promote/demote), ownership-transfer, och en `ls -l`-vy i själva\nlistan. `chmod`-flavor-ytan om vi vill gå mer Unix-rent senare.\n\n**Samtidighets-härdning (uppskjuten från Slice A, 2026-08-08):** en teoretisk\nrace där ende ägaren lämnar samtidigt som en invite hämtas kan lämna listan\nägarlös. Slice A mildrar den (rad-lås `FOR UPDATE` i alla ägar-muterande RPC:er\n+ `claim_invites` befordrar den som hämtar om listan saknar ägare), men stänger\nden inte helt under READ COMMITTED. Ren fix: `pg_advisory_xact_lock(list_id)` i\nleave/claim/invite, ELLER auto-promote-on-owner-leave (faller naturligt ut ur\nownership-transfer). Extremt osannolik på en single-user-terminal — medvetet\nuppskjuten hit hellre än att handrulla mer lås-protokoll i Slice A.\n\n_Slice A shippad i 0.13.0._\n\n## Mål\nGe delade listor en *rollmodell* istället för platt \"alla är med-ägare\". Det är\nkollaborationens motsvarighet till skrivskyddet vi gav `/etc` (steg 1–3 i\n`permissions.md`): en `chmod`/`chown`-liknande rättighetsyta, men på cloud-objekt\ndelade över konton, backat av Supabase-RLS.\n\n## Nuläget (gapet)\n`shared_list_members(list_id, user_id)` — ingen roll. Varje medlem kan redigera,\nbjuda in, lämna. Saknas: skrivskyddad (read-only) delning; en ägare som kan *ta\nbort* en medlem eller *radera* listan; \"får inte bjuda in vidare\". `created_by`\nfinns men är bara informativt (sätts null vid delete).\n\n## Föreslagen modell — owner / editor / viewer\n- **owner** — skaparen. Kan allt: redigera, bjuda in, sätta roller, ta bort\n  medlemmar, radera listan. (Minst en owner måste alltid finnas.)\n- **editor** — kan redigera innehåll + (beslut) ev. bjuda in.\n- **viewer** — ser listan + live-uppdateringar, men `todo` nekar spara; kan inte\n  bjuda in. RLS blockerar `update` på servern (inte bara i klienten).\n\nNy kolumn `shared_list_members.role text not null default 'editor'`. RLS:\n`update`-policyn på `shared_lists` kräver `role in ('owner','editor')`; nya\nSECURITY DEFINER-RPC:er för `set_role`, `remove_member`, `delete_list` som\nverifierar att anroparen är owner. `invite_to_list` tar en roll-param.\n\n## Terminal-idiom — flagga divergensen (per CLAUDE.md)\nInvite-by-email är redan en accepterad web-divergens (närmaste Unix-kin är\n`chmod`/`chown`/NFS-mounts). Roller ovanpå det ärver samma divergens. Ytan att\nbesluta:\n- `todo share <name> <email> [--ro|--rw]` (default `--rw`) — bjud in med roll.\n- `todo members <name>` — lista medlemmar + roller (à la `ls -l`).\n- `todo unshare <name> <email>` — ta bort en medlem (owner).\n- (senare) roll-byte/promote-demote, ownership-transfer.\n\n## Öppna beslut (att ta ställning till)\n1. **Rollvokabulär/yta:** owner/editor/viewer via `todo share --ro/--rw` +\n   `todo members`/`todo unshare` — eller en renare `chmod`-flavor?\n2. **Får `editor` bjuda in?** (Google-Docs: ja för editor; strängare: bara owner.)\n3. **Slice-storlek:** A (MVP: roller + read-only + owner kan remove/delete) nu,\n   B (roll-byte, ownership-transfer, `ls -l`-vy) senare — eller allt på en gång?\n\n## Skiss på leverans (Slice A)\nSQL-migration (role-kolumn + RLS + RPC:er) · `ShareStore`-utökning (roll i\n`mine`/`invite`, nya `setRole`/`removeMember`/`deleteList`) · `MemoryShareStore`\nspeglar · `todo`-kommandots yta · viewer-nekan i klienten *och* RLS · tester\n(collab + store) · en tour-rad · changelog-rad. Cutta som en `minor`.\n\n_Följer `roadmap/README.md`. Ny idé → `inbox` tills vi valt slice + yta._",
      "sourcePath": "roadmap/shared-file-roles.md",
      "sourceUrl": "https://github.com/tor2dbear/pia-terminal/blob/main/roadmap/shared-file-roles.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "pia-terminal/live-output-line",
      "repo": "tor2dbear/pia-terminal",
      "repoName": "PIA",
      "repoColor": "#38bdf8",
      "slug": "live-output-line",
      "title": "terminal: uppdaterbar utdatarad (progress/spinner)",
      "status": "inbox",
      "tags": [
        "terminal",
        "packages"
      ],
      "updated": "2026-08-08",
      "issue": null,
      "order": null,
      "body": "## Mål\nEn primitiv för att **uppdatera en redan utskriven rad in-place** (en transient\nrad som kan skrivas över), utan att ta över hela skärmen. Det är den delade\nenablern bakom flera saker vi diskuterat men skjutit på framtiden — samlade här\nså beslutet finns när någon av dem ska byggas.\n\n## Research\n\n### Luckan i dagens I/O\n- `ctx.print` **appendar bara** en rad — kan aldrig ändra en redan skriven.\n- Screen-appar (`ctx.runApp`) **tar över hela skärmen** — för mycket för en\n  progressrad mitt i normalt flöde.\n- Mellanläget — \"skriv en rad och uppdatera den några gånger\" — saknas helt\n  (verifierat: ingen spinner/transient/updateLine-primitiv finns i\n  `src/terminal/`).\n\n### Vad som behöver den\n- **`brew install` Nivå 2b** (se `brew-install-progress.md`): en animerad\n  spinner/bar *under* den äkta chunk-`import()`-hämtningen. Mest värde för tunga\n  paket (`python`/pyodide) där laddningen faktiskt tar tid — där är animationen\n  ärlig, inte kosmetik.\n- **`pv -L <rate>`** (se `pkg-pv.md`): ärligt portionerad genomströmning med en\n  bar som fylls sanningsenligt.\n- **Framtida live-kommandon:** `tail -f`, `yes`, spinners, live-filter.\n\n### Skiss på API (bakom context-sömmen — kommandon rör aldrig DOM)\n```\nctx.progress(label?) → handle {\n  update(text)      // skriv om raden in-place\n  done(finalText?)  // fäst slutraden, sluta vara transient\n}\n```\nEn rad, uppdaterbar tills `done()`. Kommandon fortsätter tala med världen bara\nvia `CommandContext` — primitiven lever i terminal-kärnan (Terminal), inte i\nkommandot.\n\n### Determinism (viktigast)\nRendera **inga mellanframes** när output fångas (pipe/redirect) eller i touren —\nvisa bara slutraden. Exakt som `matrix`/`ping` redan hanterar sina\nicke-deterministiska lägen. Så golden-snapshoten förblir stabil och captured\noutput blir ren text.\n\n## Avgränsning\n- **Inte** full skärm-takeover — det är `ScreenApp`.\n- **Inte** strömmande pipes — det är en mycket större arkitekturfråga (för/emot\n  vägt i `pkg-pv.md`). Den här primitiven ger animerad *lokal* output utan att\n  röra pipeline-modellen.\n\n## Kopplingar\n- `brew-install-progress.md` — Nivå 2b (animerad bar) väntar på den här.\n- `pkg-pv.md` — `pv -L` (ärlig animation) och streaming-pipes (far-term).\n\n## Insats\nM — en avgränsad tillägg i terminal-kärnan + en context-metod. Ingen\npipeline-ombyggnad.",
      "sourcePath": "roadmap/live-output-line.md",
      "sourceUrl": "https://github.com/tor2dbear/pia-terminal/blob/main/roadmap/live-output-line.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "pia-terminal/permissions",
      "repo": "tor2dbear/pia-terminal",
      "repoName": "PIA",
      "repoColor": "#38bdf8",
      "slug": "permissions",
      "title": "Rättigheter — skrivskyddat systemträd + sudo som escape-hatch",
      "status": "done",
      "tags": [
        "shell",
        "fs",
        "teaching"
      ],
      "updated": "2026-08-07",
      "issue": null,
      "order": null,
      "body": "## Mål\nGe `sudo` (och på sikt `chmod`/`chown`) en *riktig referent*. PIA saknar\nrättighetsmodell — allt är ditt — så `sudo` är idag bara persona. Idén: ett litet\nskrivskyddat systemträd (`/etc`, `/bin`, `/usr`) som `sudo` kan öppna, så att\n\"elevera\" faktiskt betyder något. Bygger också en pedagogisk yta (Unix-behörighet\natt öva på) och knyter ihop befintliga features (boot-hälsning, neofetch).\n\n## Upplägg i tre steg (var och en shippbar för sig)\n1. **Seeda systemträdet** — filerna *finns* och *används*, ingen låsning. ✅ *(gjort)*\n2. **Skrivskydda** systemsökvägarna — `rm /etc/motd` → `permission denied`. ✅ *(gjort)*\n3. **`sudo`-elevation** — `sudo <cmd>` kör kommandot med skyddet av; escape-hatchen. ✅ *(gjort)*\n\n## Levererat (steg 3, samma session)\n`sudo <cmd>` kör nu payloaden **eleverat**: en motor-söm `ctx.exec(rad)` kör om\nraden genom den riktiga pipeline-köraren (`runLine`→`runSequence`→`executePipeline`,\nutextraherat ur `submit`), inuti `vfs.runElevated`. Så `sudo rm /etc/motd` /\n`sudo nano /etc/hostname` funkar där ett vanligt kommando får `permission denied`.\n- Ingen lösenords-/user-modell (single-user) — sudo är bara \"jag menar det\"-knappen.\n- `ctx.fail()` (ny, tyst) låter sudo propagera payloadens exit-status till `&&`/`||`.\n- **Nekar pipe/redirect, som på riktig Linux:** `>` och `|` görs av skalet runt\n  det eleverade kommandot — payloaden körs om på en *ny rad*, så en redirect skulle\n  skriva tomt (`sudo echo x > /etc/f` trunkerar filen) och en pipe skulle tappa\n  sin input. Därför vägrar sudo i pipe/redirect (`ctx.piped || ctx.stdin`) och\n  säger till dig att elevera själva skrivningen: `sudo nano /etc/hostname`.\n- **Bevarar argument-gränser:** payloaden re-quotas innan den re-parsas, så\n  `sudo touch \"/etc/my file\"` blir en fil, inte två.\n- **Serialiserad över fönster:** elevation lyfter en *process-vid* vakt, så sudo\n  tar tmux-övergångslåset (`otherWindowsBusy` → neka; annars `beginTransition`/\n  `endTransition`) — bara ett eleverat kommando i taget, så inget annat fönster\n  kan skriva /etc oskyddat medan `sudo nano` står öppet. `runElevated` räknar\n  djup (inte en boolean), så överlappande async-elevationer inte korrumperar\n  varandras vakt-tillstånd.\n- Täckt av terminal-tester (sudo skriver i skyddat träd + återställer vakten;\n  failure → `&&`; nekad i pipe/redirect utan att röra filen; quote-gränser\n  bevarade; fönsterlåset tas/släpps) + vfs-test (överlappande async-elevation) +\n  en tour-rad (`rm` nekad → `sudo rm` funkar).\n\n## Levererat (steg 1, 2026-08-07)\n`src/pia/etc.ts` (`seedSystemFiles`): en liten `/etc`, seedad idempotent vid boot\n(global, inte per-home) i `main.ts` bredvid `seedDefaultPackages`.\n- **`/etc/motd`** — message-of-the-day; boot-hälsningen läser den nu (`boot.ts`\n  tar en `motd`-option, `DEFAULT_MOTD` som fallback). *Din* fil: seedas bara när\n  den saknas (edits överlever), och self-healar om du raderar den.\n- **`/etc/os-release`** — maskinägd OS-identitet (samma version som `neofetch`),\n  skrivs alltid om till körande version så den inte driftar.\n- **`/etc/hostname`** — maskinens namn bakom promptens `{host}` (läses via\n  `terminalConfig` → `TerminalConfig.host`; default-prompten använder nu\n  `{host}`). `echo laptop > /etc/hostname; source` döper om maskinen live.\n- `ls /` visar nu `etc/  home/`. Täckt av `etc.test.ts` (seed/edit-överlevnad/\n  self-heal/os-release-refresh/hostname) + tour-rader (`ls /`,\n  `cat /etc/os-release`, `cat /etc/hostname`).\n\n## Levererat (steg 2, samma session)\nSkrivskydd i VFS:en, som planerat — prefix-baserat, ingen per-nod-metadata:\n- `VFS.protectedPaths` (prefix) + en `elevated`-flagga och `runElevated(fn)`.\n  Varje muterande op (`mkdir`/`mkdirp`/`touch`/`writeFile`/`remove`/`move`/`copy`)\n  kollar `guardWrite(path)` och kastar `permission denied: <path>` om sökvägen\n  ligger under ett skyddat prefix och vi inte är eleverade. `move`/`copy` vaktar\n  *båda* ändarna (ut ur och in i /etc).\n- `main.ts` sätter `vfs.protectedPaths = [\"/etc\"]`; `seedSystemFiles` kör i\n  `runElevated` så systemet fortfarande kan seeda/uppdatera. Hemmet opåverkat.\n- **Följd:** de \"redigerbara\" system­filerna (motd, hostname) är nu låsta för\n  vanliga kommandon också — `echo laptop > /etc/hostname` ger `permission denied`.\n  Det är meningen: seed-if-missing-designen betyder att när `sudo` (steg 3) finns,\n  överlever dina eleverade edits en reseed. Tills dess är /etc helt låst.\n- Täckt av vfs.test (neka alla ops / elevation-bypass), etc.test (seedar under\n  skydd), commands.test (`rm /etc/motd` → denied, filen kvar), tour-rad.\n\n## Kvar (steg 3)\n- **Elevation (steg 3):** behöver en väg för `sudo` att *köra* payloaden med\n  skyddet av. Antingen en motor-söm `ctx.exec(rad, { elevated })` (troget, kör om\n  genom riktiga pipeline-köraren med glob/pipes/alias intakta) eller ett\n  session-läge (`sudo -s` elevar tills `exit`). Söm-varianten är den faithful\n  men dyrare biten.\n- **Överlapp med `chmod`/`chown`:** när skydd finns är en simulerad rwx-modell\n  (öva-på-behörighet) ett naturligt nästa steg (tidigare \"alternativ D\").\n\n_Byggd steg 1 direkt den här sessionen; steg 2–3 medvetet uppskjutna. Följer\n`roadmap/README.md`._",
      "sourcePath": "roadmap/permissions.md",
      "sourceUrl": "https://github.com/tor2dbear/pia-terminal/blob/main/roadmap/permissions.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "pia-terminal/window-tabs",
      "repo": "tor2dbear/pia-terminal",
      "repoName": "PIA",
      "repoColor": "#38bdf8",
      "slug": "window-tabs",
      "title": "Fönster-flikar — tmux-lite multiplexer",
      "status": "done",
      "tags": [
        "terminal",
        "ux"
      ],
      "updated": "2026-07-30",
      "issue": null,
      "order": null,
      "body": "## Mål\nFlera sessioner i samma Pia-flik. Man *kan* redan öppna flera browserflikar, men\nnative ger delat *live*-minne (skrivning i ett fönster syns direkt i ett annat,\nutan localStorage-race eller reload), direkt nytt fönster utan sidladdning, och\ntmux-känslan. Portfolio-vinkeln: en multiplexer bevisar att terminalkärnan är rent\nmulti-instansierbar (samma poäng som motor-extraktionen).\n\n## Levererat (2026-07-30)\nByggt som **tmux-lite**. Beslut:\n- **Rätt idiom = tmux, inte emulator-flikar.** En dator, flera *fönster*.\n- **Delad VFS (en dator).** Alla fönster delar samma tjänster (VFS, adapter,\n  registry, konto); varje fönster är en egen `Terminal` med egen `cwd`, historik,\n  scrollback och ev. körande screen-app. `Terminal` var redan per-instans (egen\n  input, egen scroll-container, `dispose()`), så det här är bara ett skal —\n  `TabManager` i `src/terminal/tabs.ts`.\n- **Bara första fönstret bootar.** Nya fönster (`tmux new` / Ctrl-B c) är rena\n  shells på samma maskin.\n- **Yta:** tmux-prefix `Ctrl-B` (obundet i Pias readline) + `c`/`n`/`p`/`x`/`1-9`,\n  och en synlig flik-strip. Prefix-tangenterna är Unix-idiomet; **strippen är en\n  GUI/emulator-eftergift** (accepterad web-divergens, som on-screen-key-baren) och\n  är också touch-vägen. Plus ett `tmux`-kommando (alias `tab`) för upptäckbarhet\n  och mobil (`tmux new/next/prev/kill/<n>`).\n- **Strippen visas bara med >1 fönster** — ingen chrome i default-läget.\n- **Fokus-routing gratis:** bara det synliga fönstrets input är fokuserbar, så\n  tangenttryck når alltid det aktiva fönstret.\n- **Layout:** `#screen` blev en flex-kolumn; varje `.term-pane` ärver det gamla\n  `#screen`:s scroll + safe-area-padding (terminalen scrollar sin root).\n\n## Härdat (Codex-rundor)\nMulti-fönster reste flera verkliga kanter som åtgärdades: re-home av alla\nfönster vid konto­byte (delad session/VFS), `dispose()` avbryter kommando + löser\n`runApp`-löftet (så command-cleanup körs), stäng-vakt mot att stänga ett fönster\nmitt i ett kommando (utom `tmux kill` som stänger sitt eget), schemaläggning till\nett *ledigt* fönster, och ett **cross-window transition-lås**: konto­byten vägrar\nom ett annat fönster är upptaget och blockar nya kommandon i andra fönster medan\nlogin/logout pågår — så inget kommando skriver in i fel konto under VFS-swappen.\nTab-strippen är tangentbords­manövrerbar (riktiga `<button>` + `click`).\n\n## Kända v1-begränsningar (medvetet dragen gräns)\nKvar finns bara ultra-sällsynta concurrency-kanter som kräver *avsiktliga,\nnästintill samtidiga* motstridiga handlingar i två fönster. Bedömdes som\noproportionerligt att stänga för en portfolio-v1 (kräver schemaläggar-omskrivning\nresp. app-intern async-avbrytning):\n- **Schemalagt jobb mitt i ett konto­byte:** ett `at`/cron-jobb som blir due exakt\n  medan ett annat fönster kör login/logout avvisas av låset och tappas (redan\n  borttaget ur kön).\n- **Python-REPL stängd mitt i en beräkning under ett konto­byte:** stäng ett\n  fönster vars Python fortfarande räknar, samtidigt som ett annat loggar in →\n  resultatet kan skrivas mot fel konto (app-intern async som `isRunningCommand`\n  inte ser).\n- **`asyncCmd; tmux kill` + fönsterbyte:** binder mot aktivt index, inte det\n  anropande fönstret, så fel fönster kan stängas om man byter mitt i kedjan.\n- **Mobil viewport med strip + tangentbord:** panens höjd sätts till hela\n  visual-viewporten fastän strippen ligger ovanför (kan skjuta botten under\n  key-baren i det ovanliga multi-fönster-på-mobil-läget).\n\n## Uppskjutet (medvetet)\n- **Paner/splits** — ett helt annat layoutproblem.\n- **Persistera layouten** i `.pia/` (detach/attach-känsla).\n- **Pausa bakgrundsappar** (t.ex. snake-timer i inaktiv flik).\n\n_Ny puck den här sessionen; byggd direkt. Följer `roadmap/README.md`-konventionen._",
      "sourcePath": "roadmap/window-tabs.md",
      "sourceUrl": "https://github.com/tor2dbear/pia-terminal/blob/main/roadmap/window-tabs.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "pia-terminal/history-persistence",
      "repo": "tor2dbear/pia-terminal",
      "repoName": "PIA",
      "repoColor": "#38bdf8",
      "slug": "history-persistence",
      "title": "history-persistens — ~/.pia/history (HISTFILE) mellan sessioner",
      "status": "done",
      "tags": [
        "shell",
        "terminal"
      ],
      "updated": "2026-07-30",
      "issue": null,
      "order": null,
      "body": "## Mål\nPil-upp ska nå kommandon från *tidigare* sessioner, precis som en riktig terminal\n(bash `HISTFILE`). Uppföljning på `history`-kommandot (`history-command.md`), som\nbyggde listnings-halvan men medvetet sköt upp persistensen: history var bara\nin-session och försvann vid reload.\n\n## Levererat (2026-07-30)\n- **Fil:** `~/.pia/history`, HISTFILE-idiomet, i samma `.pia/`-dotfil som resten\n  av konfigen. Lever i VFS-trädet, så den syncar med allt annat när man är\n  inloggad (samma fil på alla enheter) och sparas lokalt för gäster.\n- **Motor-söm, inte PIA-logik i kärnan.** `Terminal` fick tre optionella hooks —\n  `loadHistory` (seedar pil-upp vid boot), `saveHistory` (anropas efter varje\n  kommando med hela listan) och `clearHistory` (`history -c` tömmer även filen).\n  Alla utelämnade → ren in-minnes-history (som t.ex. adventure-exemplet). Fil-I/O\n  ligger i `main.ts` bakom sömmen; hjälparna i `src/pia/history.ts` är rena.\n- **`histappend`, inte overwrite.** `saveHistory` läser filen och appendar bara\n  *nya* rader (per fönster räknat), skippar en rad som upprepar den föregående\n  (`HISTCONTROL=ignoredups`) och kapar till `HISTSIZE` (1000, nyast vinner). Att\n  appenda mot filen — inte skriva över från minnet — är det som låter två\n  tmux-fönster dela en fil utan att klippa varandras rader.\n- **Debouncad persist.** Varje kommando skriver history till VFS direkt, men\n  `adapter.save` coalescas (~1.5 s) så en kommando-burst inte hamrar storage; en\n  `beforeunload`-flush gör de sista raderna varaktiga om fliken stängs först.\n- **Konto-byte:** `rehome()` läser om history från det nya kontots fil, så pil-upp\n  följer med in-/utloggning (varje konto har sin egen `~/.pia/history`). Inget\n  läcker mellan konton — varje kommando är redan flushat till filen.\n\n## Härdat (Codex-rundor)\n- **Inga hemligheter i history (HISTIGNORE).** `passwd`/`login`/`useradd`/`register`\n  tar lösenordet som ett argument; hela raden hålls utanför history (både pil-upp\n  och filen) via en `histIgnore`-söm på `Terminal`. Checken körs på de *resolvade*\n  kommando-namnen — den går igenom pipeline-stegen vänster-till-höger, expanderar\n  alias precis som körningen (inkl. alias-args), lär sig alias *definierade\n  tidigare på samma rad* (`alias p passwd; p pw`), och rekurserar in i `at`:s\n  payload (`at now+5m login u pw`, även via alias `later hunter2`). Så direkt,\n  aliasad, kedjad och schemalagd variant fångas alla. `Terminal` äger parsningen;\n  `main.ts` bidrar bara med namn-listan (`hasSecret`).\n- **Konflikt-reconcile för bakgrunds-sparningen, delad över fönster.** Den\n  debouncade sparningen gick först direkt mot `adapter.save` och svalde\n  `StorageConflictError` → nästa spar kunde skriva över en samtidig moln-ändring.\n  Nu går den genom `Terminal.flush()` → `persistTree()`-reconcilen (keep-both,\n  stash under `~/.pia/conflicts/`). Eftersom alla fönster delar *ett* VFS finns\n  **en enda delad pending-persist** (i `main.ts`), inte en timer per fönster —\n  annars kunde ett konto­byte i fönster A missa fönster B:s väntande skrivning.\n  Den flushas synkront vid livscykel-gränser via en injicerad `flushPending`:\n  vid `dispose()`, av värden på `beforeunload`, och — viktigast — i **början av\n  en konto-transition** (`withTransition` → `ctx.flushHistory`), *innan* auth\n  byter identitet. Annars hade den uppskjutna sparningen routats genom fel konto\n  (gäst-träd till moln-kontot vid login, användarens träd till gäst-localStorage\n  vid logout). Sparningarna **serialiseras på en kedja** (aldrig två `flush()`\n  samtidigt, som annars racade molnets base-version), och `flushPending` väntar\n  in hela kedjan — inte bara en väntande timer — så en transition inte byter\n  identitet mitt i en save som ännu inte hunnit landa.\n- **Hemligheter, godtyckligt djupt.** Secret-checken rekurserar in i `at`:s\n  payload med en budget = radens token-antal (som strikt minskar per nivå), så\n  även absurt nästlade `at now+5m at now+5m … passwd pw` fångas — ingen godtycklig\n  djup-cap som kan kringgås.\n\n## Kända v1-begränsningar (medvetet dragen gräns)\nBara history är best-effort här — VFS-mutationer och konto­byten sparas fortfarande\nsynkront och reconcilas som förut. De kvarvarande kanterna gäller enbart *ren\nläskommando-historik* (raderna som bara finns för pil-upp), och stänga dem kräver\nen oproportionerlig retry-/omkö-mekanik för en portfolio-v1:\n- **`beforeunload` mot molnet är best-effort.** localStorage (gäster +\n  Hybrid-adapterns lokala hälft) hinner skriva under unload, men en läskommando-rad\n  <1.5 s innan fliken hårt-stängs på ett moln-konto kan tappas — browsern väntar\n  inte in det async nätverks-anropet. (Samma som att en hård-dödad bash tappar\n  osparad history.)\n- **Transient spar-fel retrias inte.** Ett icke-konflikt-fel (nätglapp) från en\n  bakgrunds-`flush()` sväljs; de senaste läskommando-raderna ligger kvar i minnet\n  och skrivs vid nästa spar, men går man reload/konto­byte dessförinnan tappas de.\n  Konflikter (den farliga varianten) reconcilas dock alltid (keep-both).\n- **History vid en samtidig-enhet-konflikt.** Om en bakgrunds-spar krockar med en\n  annan enhet adopteras fjärrträdet (vars history-fil saknar de allra senaste\n  lokala raderna); de raderna finns då kvar i konflikt-snapshoten under\n  `~/.pia/conflicts/` men inte i den primära historiken efter reload. Filerna\n  tappas inte (keep-both) — bara de sista pil-upp-raderna.\n- **Bakgrunds- och förgrunds-spar är inte korsserialiserade.** Kedjan\n  serialiserar bakgrunds-history-sparningarna sinsemellan, men ett *muterande*\n  kommando sparar via `ctx.persist()` direkt. Kör man ett sådant medan en\n  debouncad history-flush väntar på molnet kan de två racea adapterns base-version\n  → en behandlas som en (falsk) samtidig-enhet-konflikt och läggs keep-both i en\n  snapshot. Inget tappas (reconcile), men kommandots träd kan hamna \"konfliktat\".\n  Att korsserialisera *alla* sparningar app-brett bedömdes oproportionerligt för\n  v1 (rör kärnans persist-väg för varje kommando).\n\nTäckt av 6 enhetstester (`history.ts`: parse/serialize/append, ignoredups, cap,\ntvå-fönster-interleave) + 3 end-to-end (seedar pil-upp, sparar per kommando,\n`-c` tömmer). Tour-golden uppdaterad (hjälptexten). typecheck + test + build gröna.\n\n## Uppskjutet (medvetet)\n- **`!!`/`!$`/`!n`-expansion** — en kärn-expansions-pass (som globbing), fortsatt\n  egen uppföljning (låg på `history-command.md`).\n- **`HISTIGNORE`/`ignorespace`** — filtrera bort rader ur history. Inte efterfrågat.\n\n_Uppföljning på `history-command.md`; byggd direkt. Följer `roadmap/README.md`._",
      "sourcePath": "roadmap/history-persistence.md",
      "sourceUrl": "https://github.com/tor2dbear/pia-terminal/blob/main/roadmap/history-persistence.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "pia-terminal/todo-list-notifications",
      "repo": "tor2dbear/pia-terminal",
      "repoName": "PIA",
      "repoColor": "#38bdf8",
      "slug": "todo-list-notifications",
      "title": "\"Lista uppdaterad\"-notiser (coalescade)",
      "status": "done",
      "tags": [
        "collaboration",
        "push"
      ],
      "updated": "2026-07-26",
      "issue": null,
      "order": null,
      "body": "## Mål\nPush när en medlem ändrar en delad lista du är med i (\"Anna bockade 3 saker på\nInköp\"). Återanvänder samma push-rör som redan finns (`notifications`-kön →\n`send-due`, se `reminder-push.md`, som är done). Samarbets-payoffen: du delar en\nlista, du hör när den rör sig.\n\n## Varför inte bara en trigger\nTodo-appen sparar hela listan vid **varje** bock/tillägg, så en naiv\n`AFTER UPDATE`-trigger blir pratig — flera notiser per sekund vid en redigering.\nKräver **coalescing** först: max en notis per lista, per medlem, per N minuter\n(och inte till den som gjorde ändringen).\n\n## Skiss\n- En debounce/coalesce-nivå innan `notifications`-kön: antingen en\n  `pending`-flagga per (lista, medlem) som `send-due`-ticken tömmer max var N:e\n  minut, eller en `last_notified_at` som triggern respekterar.\n- Text: sammanfatta (\"N ändringar på 'Inköp'\") snarare än en rad per bock.\n- Prenumerationen är redan generell (per enhet, via `remind on`), så ingen ny\n  opt-in behövs — men se `notify on`-idén i `reminder-push.md` för tydlighet.\n\n## Levererat (2026-07-26)\nByggt. Besvarade öppna frågor:\n- **Coalesce atomiskt i DB.** Triggern (`record_list_activity`) gör bara det\n  triviala: loggar en rad per *content*-ändring i `shared_list_activity`\n  (no-op-sparningar hoppas över via `is distinct from`). Själva vikningen sker i\n  `flush_list_activity()` — **claim + summering + enqueue i EN transaktion**\n  (`delete … returning` + CTE:er + `insert`). Första skissen la logiken i en ren,\n  testad TS-modul (`coalesce.ts`), men Codex påpekade rätt att select→insert→delete\n  över separata nätverksanrop inte är atomiskt: två överlappande minut-tick kan\n  då dubbel-skicka, tappa rader vid insert-fel, eller bara se PostgREST:s första\n  sida. Allt det försvinner när claim+enqueue är ett DB-statement. Ligger dessutom\n  i linje med syskonet `notify_on_invite` som redan är ren SQL.\n- **Debounce + tak.** En lista summeras när den varit tyst i 3 min, men hålls\n  aldrig kvar längre än 15 min (annars skulle en oavbrutet redigerad lista aldrig\n  fyra). `delete … returning` tömmer bursten → färskt fönster nästa gång.\n- **Inte till redigeraren.** Per medlem räknas allas ändringar utom de egna\n  (`total - egna`); två som redigerar samtidigt notifieras korrekt om varandras.\n- **Text:** sammanfattning (`N updates to \"Inköp\"`), inte en rad per bock. Går via\n  samma `notifications`-kö → `send-due`, så leverans/prune återanvänds oförändrat.\n  `send-due` anropar `flush_list_activity()` först i ticken, så summeringar går ut\n  samma minut.\n- **Ändringstyp:** varje content-ändring räknas (kan inte skilja omordning från\n  tillägg utan att parsa listan — medvetet enkelt för v1).\n\nKvar (manuellt, kräver Supabase-åtkomst jag inte har i sessionen): applicera\n`supabase/notifications.sql` (rerunnbar) och deploya om `send-due` mot\nlive-projektet.\n\n_Utbruten från `reminder-push.md` när den pucken markerades done; det här var den\ngenuint obyggda svansen._",
      "sourcePath": "roadmap/todo-list-notifications.md",
      "sourceUrl": "https://github.com/tor2dbear/pia-terminal/blob/main/roadmap/todo-list-notifications.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "pia-terminal/boot-bios",
      "repo": "tor2dbear/pia-terminal",
      "repoName": "PIA",
      "repoColor": "#38bdf8",
      "slug": "boot-bios",
      "title": "Boot-BIOS — retro-uppstartssekvens",
      "status": "done",
      "tags": [
        "aesthetic",
        "ui"
      ],
      "updated": "2026-07-25",
      "issue": null,
      "order": null,
      "body": "## Levererat (2026-07-25)\nOpt-in `bios`-läge: en längre POST-preamble före prompten (BIOS-rad, minnestest,\n\"Detecting storage…\", adapter-lista, boot device), sedan över i den vanliga\nhälsningen. `boot.ts` tar nu ett `BootOptions{bios}`; `main.ts` läser flaggan ur\n`~/.pia/config` vid boot. **Egen `bios`-flagga** (inte hopbuntad med `crt`) med\nett `bios [on|off]`-kommando som speglar `crt` men bara persisterar (POST spelar\nvid nästa reload, inget live att visa). **Skippbar** — valfri tangenttryckning\nnollar kvarvarande pauser så en reload aldrig fastnar bakom den. Av som default\n(samma tillgänglighets-resonemang som CRT). Verifierad i webbläsare (skärmdump)\n+ tester (rc-parse, `bios`-kommando, boot-preamble på/av med fake-timers).\n\n**Kvar som svansar (medvetet ute ur v1):** ljud (boot-brus/tangentljud bakom\nsamma toggle, Web Audio/CSP-säkert), och en riktig in-place minnesräknare (kräver\natt skriva över senaste raden — terminalen är append-only idag). `retro`-paraply\nsom slår på både `crt` och `bios` kan läggas till senare; flaggorna är oberoende.\n\n## Mål\nEn valbar, längre retro-uppstart à la ett BIOS/POST: minnesräkning, \"detecting\ndrives…\", en blinkande markör, sedan över i den vanliga prompten. Rent kosmetiskt\nmen delbart — komplementet till CRT-filtret (som redan är levererat, se\n`crt-retro-mode.md`).\n\n## Research\n- Bygger på befintligt: `boot.ts` sekvenserar redan en uppstart (input-gate,\n  prompt-avslöjande); BIOS-läget är en längre/retro-variant av den.\n- Bör vara en **toggle** (config-dotfilen), inte default — samma tillgänglighets-\n  resonemang som CRT.\n- Passar ihop med CRT men är oberoende: kan köras med eller utan `.crt`-overlay.\n\n## Öppna frågor\n- Egen config-flagga (`bios = on`) eller en del av ett bredare `retro`-läge som\n  också slår på `crt`?\n- Ljud (boot-brus/tangentljud, README nämner en ljud-toggle) — hör hemma här; hålls\n  bakom samma toggle och Web Audio (som `piano`), CSP-säkert.\n- Hur mycket text/timing utan att bli irriterande vid varje reload? Snabb som\n  default, \"full POST\" bara första gången eller bakom en flagga.\n\n_Utbruten från `crt-retro-mode.md` när CRT-filtret levererades (#65); den här\nretro-uppstarten är den obyggda resten._",
      "sourcePath": "roadmap/boot-bios.md",
      "sourceUrl": "https://github.com/tor2dbear/pia-terminal/blob/main/roadmap/boot-bios.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "pia-terminal/crt-retro-mode",
      "repo": "tor2dbear/pia-terminal",
      "repoName": "PIA",
      "repoColor": "#38bdf8",
      "slug": "crt-retro-mode",
      "title": "CRT/retro-mode",
      "status": "done",
      "tags": [
        "aesthetic",
        "ui"
      ],
      "updated": "2026-07-25",
      "issue": null,
      "order": null,
      "body": "## Mål\nEtt valbart retro-läge: CRT-filter (scanlines, glöd, lätt kurvatur). Ren estetik\n— men just det slaget som blir screenshottat och delat.\n\n## Levererat (2026-07-25)\n`crt on/off/toggle` (`src/commands/config.ts`) togglar ett `.crt`-overlay-lager i\n`src/style.css`: fosfor-glöd (text-shadow + halation), aperture-grille-RGB-mask\n(`::before`), scanlines + vinjett (`::after`), och en accent-glöd på knappraden\n(`.crt .kb-key`). Ren CSS (ingen shader), CSP-säkert via CSSOM. Sparas i\n`~/.pia/config` och kan slås av — inte default (tillgänglighet). Öppna frågan\n\"tema-värde vs separat toggle\" landade i det senare (`crt`, ovanpå valfritt tema).\n\nKvar av den ursprungliga idén: **boot-BIOS-sekvensen** — utbruten till en egen\npuck (`boot-bios.md`), eftersom CRT-filtret i sig är levererat.\n\n## Research\n- **Underskattat portfolio-värde:** visuellt = delbart = det som gör en\n  portfolio-pjäs minnesvärd. \"Bara estetik\", men det är ofta det folk sprider.\n- Bygger på befintligt: temasystemet (`theme`, CSS-custom-properties via CSSOM,\n  CSP-säkert — inga inline-styles) finns redan. CRT blir ett tema + ett\n  overlay-lager, inte ny arkitektur.\n- Tillgänglighet: scanline-overlay och glöd kan vara jobbigt för vissa; gjordes\n  till en toggle (i config-dotfilen), inte default. ✅\n\n## Öppna frågor (avgjorda)\n- Ren CSS vs canvas/WebGL-shader → **ren CSS** (billigare, CSP-snällare). ✅\n- `theme`-värde vs separat toggle → **separat `crt on/off`** ovanpå valfritt tema. ✅\n- Ljud (boot-brus/tangentljud) → flyttat till `boot-bios.md` (hör ihop med den\n  retro-uppstarten, inte CRT-filtret).\n\n_Levererat i #65. Boot-BIOS-varianten lever vidare i `boot-bios.md`._",
      "sourcePath": "roadmap/crt-retro-mode.md",
      "sourceUrl": "https://github.com/tor2dbear/pia-terminal/blob/main/roadmap/crt-retro-mode.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "pia-terminal/cloud-sync-conflicts",
      "repo": "tor2dbear/pia-terminal",
      "repoName": "PIA",
      "repoColor": "#38bdf8",
      "slug": "cloud-sync-conflicts",
      "title": "Konflikthantering vid cloud-sync",
      "status": "done",
      "tags": [
        "storage",
        "supabase"
      ],
      "updated": "2026-07-25",
      "issue": null,
      "order": null,
      "body": "## Levererat (2026-07-25)\nOptimistisk samtidighetskontroll på `SupabaseStorageAdapter`. Varje rad bär redan\n`updated_at` (bumpad av en trigger vid varje skrivning) — den läses vid `load()`\noch krävs oförändrad vid `save()` (vaktad `update … match({user_id, updated_at})`;\nnoll rader ⇒ någon annan skrev). Ingen schemaändring behövdes (`updated_at` har\nfunnits sedan första Supabase-committen) och ingen deploy — allt klientsidigt.\n\nVid konflikt kastar adaptern `StorageConflictError` med fjärrträdet, och\nterminalen **behåller båda, git-likt**: adopterar fjärrträdet, lägger den\nosynkade lokala versionen åt sidan som en snapshot i\n`~/.pia/conflicts/<ts>.json`, och skriver en tydlig varning (aldrig tyst\növerskrivning). `cwd` flyttas hem om katalogen försvann. Täckt av tester\n(adapter-nivå: insert-race, update-konflikt, retry landar; terminal-nivå:\nadoption + snapshot + varning) och verifierad i webbläsare. Vald efter\n\"behåll båda, git-likt\" framför avvisa-och-vänta och loud-last-write-wins.\n\n## Mål (ursprunglig research)\n(Ännu inte beslutat — det här är research.) När samma dokument redigeras på två\nenheter mot Supabase-backenden: vad händer? Idag är sista-skrivning-vinner\nimplicit, vilket tyst kan äta ändringar. Vill förstå alternativen innan vi\nlovar något.\n\n## Research\n- **Last-write-wins (nuvarande, implicit):** enklast, men tappar data tyst.\n  Oacceptabelt som *tyst* beteende; kanske okej om vi åtminstone *varnar*.\n- **Version/`updated_at`-koll:** klienten skickar den version den läste; skiljer\n  den sig i DB → avvisa och visa \"dokumentet ändrades på annat håll\". Kräver ett\n  versionsfält och att adaptern kan returnera en konflikt. Rider på samma\n  `StorageAdapter`-söm, inget nytt gränssnitt utåt.\n- **CRDT/operational transform:** riktig sammanslagning teckennivå. Kraftfullt\n  men rejält mycket komplexitet för en enmansterminal. Nästan säkert overkill.\n- Prior art värt att läsa: hur `git` formulerar konflikter (behåll båda, markera\n  tydligt) mappar fint mot terminal-idiomet — en konflikt kunde bli en\n  `.orig`-fil bredvid, inte en modal.\n\n## Öppna frågor\n- Är det här ens ett verkligt problem i praktiken? Hur ofta redigerar *du* samma\n  doc på två enheter samtidigt? Om nästan aldrig → varna-och-vinn räcker långt.\n- Om vi gör versionskoll: var bor versionsfältet — i VFS-noden eller bara i\n  storage-lagret? Lutar åt storage, så VFS förblir rent i minnet.\n- Terminal-idiom: finns en kommandoyta för att lösa konflikt (`diff`, behåll\n  vilken)? Eller är det editor-internt? Flagga innan bygge — kan vara en\n  web-divergens.\n\n_Ligger i `inbox` tills det blivit ett beslut. Befordra till `next/later` då._",
      "sourcePath": "roadmap/cloud-sync-conflicts.md",
      "sourceUrl": "https://github.com/tor2dbear/pia-terminal/blob/main/roadmap/cloud-sync-conflicts.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "pia-terminal/notify-command",
      "repo": "tor2dbear/pia-terminal",
      "repoName": "PIA",
      "repoColor": "#38bdf8",
      "slug": "notify-command",
      "title": "notify on — eget notis-preferenskommando",
      "status": "done",
      "tags": [
        "scheduling",
        "push"
      ],
      "updated": "2026-07-25",
      "issue": null,
      "order": null,
      "body": "## Levererat (2026-07-25)\n`notify [on|off]` levererat. Bar `notify` visar tillstånd (\"notifications: on/off\non this device.\"); `notify on` återanvänder `enablePush()`; `notify off` är den\nnya vägen — ett `disablePush()` i `ReminderStore`-sömmen (Null/Memory/Supabase)\nsom på Supabase-sidan `unsubscribe()`:ar browser-prenumerationen och raderar\nenhetens rad i `push_subscriptions` (per endpoint; RLS scopar till användaren, så\nandra enheter påverkas inte). Gäst utan moln får ärligt \"off — needs a cloud\naccount\". `remind on` lämnades kvar som fungerande motsvarighet (samma\n`enablePush()`). Ingen schemaändring, ingen deploy — radraderingen är en\nklient-DELETE. Täckt av tester (kommando-nivå + Supabase-`disablePush`) och\nverifierad i webbläsare.\n\n## Mål\nEtt eget `notify on/off`-kommando för att slå på/av push-notiser, i stället för\natt aktivera dem som en sidoeffekt av `remind on`.\n\n## Scope (ärligt)\n- **`notify on`** är i praktiken bara ett tydlighets-/upptäckbarhets-omslag —\n  återanvänder befintliga `enablePush()`/`isEnabled()` (`src/pia/reminders.ts`).\n- **`notify off` är däremot ny funktion.** `ReminderStore` har idag ingen\n  disable-väg, och Supabase-adaptern varken anropar `PushSubscription.unsubscribe()`\n  eller raderar enhetens `push_subscriptions`-rad. Off kräver alltså:\n  - ett nytt `disablePush()` i `ReminderStore`-seamen (Null/Memory/Supabase),\n  - browser-sidan: hämta prenumerationen och `.unsubscribe()`,\n  - persistens: ta bort raden i `push_subscriptions` (RLS på `auth.uid()`).\n\n## Skiss\n- `notify` (utan arg) → status (\"notiser: på/av på den här enheten\") via `isEnabled()`.\n- `remind on` blir ett alias eller pekar vidare till `notify on`.\n\n## Öppna frågor\n- Ska `off` ta bort prenumerationen helt (påverkar alla notis-typer) eller bara\n  pausa? Rimligen ta bort — enkelt och ärligt, men det är just den delen som är\n  ny kod (se scope ovan).\n- Hör `notify` hemma i kärnan eller i samma seam som `remind`/`ReminderStore`?\n\n_Utbruten från `reminder-push.md` när den markerades done; en liten kvarvarande\ntydlighets-svans._",
      "sourcePath": "roadmap/notify-command.md",
      "sourceUrl": "https://github.com/tor2dbear/pia-terminal/blob/main/roadmap/notify-command.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "pia-terminal/pkg-figlet",
      "repo": "tor2dbear/pia-terminal",
      "repoName": "PIA",
      "repoColor": "#38bdf8",
      "slug": "pkg-figlet",
      "title": "paket: figlet — stora ASCII-banners",
      "status": "done",
      "tags": [
        "packages",
        "fun"
      ],
      "updated": "2026-07-25",
      "issue": null,
      "order": null,
      "body": "## Mål\n`figlet <text>` — skriver texten som en stor ASCII-banner. On-brand terminal-konst,\nsnyggt & screenshottbart (`figlet PIA`). Insats: S–M.\n\n## Levererat\n`brew install figlet`. Ett kompakt 5-raders block-typsnitt (A–Z, 0–9, mellanslag\noch lite skiljetecken) författat inline — inget externt typsnitt, så paketet är\nsjälvständigt och CSP-säkert. Ren `figlet(text)` returnerar 5 rader (gemener →\nversaler, okänt tecken hoppas över); kommandot skriver dem i accent. Skriver till\nscrollback (ingen screen-app). Enhetstest (5 rader, kolumn-justering,\ncase-mappning, okänt tecken).",
      "sourcePath": "roadmap/pkg-figlet.md",
      "sourceUrl": "https://github.com/tor2dbear/pia-terminal/blob/main/roadmap/pkg-figlet.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "pia-terminal/pkg-piano",
      "repo": "tor2dbear/pia-terminal",
      "repoName": "PIA",
      "repoColor": "#38bdf8",
      "slug": "pkg-piano",
      "title": "paket: piano — Web Audio-synt",
      "status": "done",
      "tags": [
        "packages",
        "audio",
        "fun"
      ],
      "updated": "2026-07-25",
      "issue": null,
      "order": null,
      "body": "## Mål\n`piano` — ett spelbart tangentbord; PIA:s första ljud. Insats: M.\n\n## Levererat\n`brew install piano`. En oktav mappad mot datortangenterna (`a s d f g h j k` =\nvita, `w e t y u` = svarta), eller tryck på tangenterna på skärmen på mobil.\n`z`/`x` skiftar oktav. Ren `noteFreq(\"C4\")` (liksvävande temperatur, A4 = 440 Hz);\ntoner spelas som triangelvågor med en kort pluck-envelope via Web Audio — helt\nklientsidan, CSP-säkert. AudioContext skapas vid första tonen (en riktig\nanvändargest, som webbläsare kräver) och saknas i jsdom, så appen degraderar till\ntyst-men-visuell under test. Realistisk klaviatur (svarta tangenter placerade via\nCSSOM `left`, CSP-säkert). Enhetstest (frekvens-ankare/oktav/halvton, tangenter\nritas, oktav-clamp, avslut, inget kast utan Web Audio).",
      "sourcePath": "roadmap/pkg-piano.md",
      "sourceUrl": "https://github.com/tor2dbear/pia-terminal/blob/main/roadmap/pkg-piano.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "pia-terminal/pkg-wordle",
      "repo": "tor2dbear/pia-terminal",
      "repoName": "PIA",
      "repoColor": "#38bdf8",
      "slug": "pkg-wordle",
      "title": "paket: wordle — gissa fembokstavsordet",
      "status": "done",
      "tags": [
        "packages",
        "games"
      ],
      "updated": "2026-07-25",
      "issue": null,
      "order": null,
      "body": "## Mål\n`wordle` — gissa det dolda fembokstavsordet på sex försök, med färgad feedback\n(rätt / finns / saknas). Screen-app. Insats: S–M.\n\n## Levererat\n`brew install wordle`. Ren `scoreGuess(guess, answer)` med Wordles två-pass-logik\nför dubbletter (exakta först, sedan \"finns\" bara medan oanvända kopior återstår).\nEn kurerad ordlista (~250 vanliga ord) är både svars-pool och tillåtna gissningar\n— ingen ordboks-fetch, CSP-säkert. Svaret dras med injicerbar rng (deterministiskt\ni test). 6×5-rutnät, statusrad och en QWERTY-tangenthint som färgas per bokstav.\nBokstäverna kommer från enhetens tangentbord (`onText`); ⏎ skickar, ⌫ raderar, och\neftersom `q` är en bokstav avslutar man med Esc/^X eller tangentbarens knapp.\n\"present\"-gult saknas i paletten → en varm amber som läser rätt mot alla teman.\nEnhetstest (poängsättning inkl. dubbletter, giltig gissning vinner, icke-ord\navvisas, ordlistans format).",
      "sourcePath": "roadmap/pkg-wordle.md",
      "sourceUrl": "https://github.com/tor2dbear/pia-terminal/blob/main/roadmap/pkg-wordle.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "pia-terminal/reminder-push",
      "repo": "tor2dbear/pia-terminal",
      "repoName": "PIA",
      "repoColor": "#38bdf8",
      "slug": "reminder-push",
      "title": "Påminnelser som webapp — push till iOS/Android",
      "status": "done",
      "tags": [
        "scheduling",
        "pwa",
        "push"
      ],
      "updated": "2026-07-25",
      "issue": null,
      "order": null,
      "body": "## Mål\nDen \"riktiga\" versionen av `at`/`crontab`: påminnelser som fyrar **även när fliken\när stängd**, som en notis på iOS/Android. Bygger vidare på lärverktyget (samma\n`at`/cron-syntax) men lägger till äkta leverans.\n\n## Levererat (2026-07-25) — återkommande\n`remind \"0 9 * * 1-5\" standup` schemalägger nu en **återkommande** push via ett\ncron-uttryck (klienten känner igen en citerad femfälts-cron; annars one-off).\n`send-due` räknar om nästa fyrning (UTC) i stället för att avaktivera jobbet, och\n`remind -l` visar schemat + nästa körning. Cron läses i UTC på båda sidor så\nklient och server aldrig driftar (PR #73, edge-funktionen deployad). Därmed är\nkärnan + återkommande + kollaborations-notiser levererade och enhets-verifierade.\n\n## Levererat (2026-07-18)\n`remind <tid> <text>` (inloggad) schemalägger en push-notis som fyrar serversidan.\n\n**Server (live på Supabase, verifierat):**\n- Tabeller `push_subscriptions` + `reminders`, RLS på `auth.uid()`.\n- VAPID-nycklar + cron-secret i **Vault**; läses via en service-role-funktion\n  `get_push_config()`.\n- Edge Function **`send-due`** (auth via `x-cron-secret`, `verify_jwt` av): läser\n  due reminders, gör web-push-kryptot (`web-push` i Deno, skickar via fetch),\n  städar utgångna prenumerationer, avaktiverar engångsjobb.\n- **`pg_cron`** varje minut → `pg_net` POST → funktionen. Verifierat end-to-end:\n  cron→200 `{ok,due:0}`, och en testkörning gav `sent:1` (kryptot funkar i Deno).\n\n**Klient:**\n- **PWA:** `manifest.webmanifest` + service worker (`public/sw.js`) — visar notis\n  på `push`, fokuserar appen på klick. SW registreras vid boot. CSP fick\n  `worker-src`/`manifest-src 'self'`.\n- **Seam:** `ReminderStore` (Null/Memory/Supabase) speglar `ShareStore`.\n  `remind` pratar bara med interfacet. Prenumeration via `pushManager.subscribe`\n  med VAPID-publika nyckeln, sparas i `push_subscriptions`.\n- **`remind`**: `<tid> <text>` (auto-aktiverar push), `-l` lista, `-r <n>` avboka,\n  `on` aktivera. iOS-guidning (\"lägg till på hemskärmen först\").\n\n## Kollaborations-notiser (2026-07-18)\nSamma push-rör återanvänt: en `notifications`-kö som `send-due` tömmer på samma\ntick. En trigger på `shared_list_invites` lägger en rad när någon delar en lista\nmed dig (\"X shared \\\"lista\\\" with you\") — levereras som push om du aktiverat\nnotiser (via `remind on`; prenumerationen är per enhet och gäller alla notis-\ntyper). **Verifierat end-to-end** (trigger → kö → `send-due` → levererad, även\ntill en riktig enhet). Notis-texten putsades också: titel `⏰ Reminder` istället\nför det redundanta \"PIA\".\n\n## Klart / kvar\n- **On-device-test bekräftat** ✅ (notisen landade på iPhone-låsskärmen).\n- **Återkommande reminders (cron-uttryck)** ✅ — levererat 2026-07-25 (se ovan).\n- **\"Lista uppdaterad\"-notiser:** kräver coalescing (todo-appen sparar vid varje\n  bock) — **utbruten till egen puck** `todo-list-notifications.md` (inbox).\n- **Eget `notify on`-kommando** istället för att aktivera via `remind on` —\n  **utbruten till egen puck** `notify-command.md` (inbox).\n\n## Vad som krävs (research)\n- **PWA + service worker:** appen installeras på hemskärmen; en service worker tar\n  emot push även när fliken är stängd. iOS stödjer web push sedan **16.4**, men\n  bara för PWA installerade via \"Lägg till på hemskärmen\".\n- **Web Push + VAPID:** prenumeration i browsern → en push-endpoint per enhet.\n- **Backend som fyrar:** en serverdel (Supabase edge function + `pg_cron` /\n  scheduled trigger) som håller schemat och skickar web-push när jobb är due.\n  Jobben måste bo i molnet (inte localStorage), så det kräver inloggning.\n- Manifest + service worker är samma-origin → ok under CSP.\n\n## Öppna frågor\n- Bara för inloggade (jobben i molnet)? Rimligen ja.\n- Notis-innehåll: bara \"påminnelse: <text>\", eller köra ett kommando? För push\n  räcker text; att köra kommandon serversidan är en helt annan sak.\n- Hur mycket äga vs använda tjänst? Web Push är gratis/standard; det är schemaläggar-\n  backenden som är jobbet.\n\n_Levererat (#73, edge-funktionen deployad). Kvarvarande svansar bor i egna\npuckar: `todo-list-notifications.md` och `notify on`-tydligheten ovan._",
      "sourcePath": "roadmap/reminder-push.md",
      "sourceUrl": "https://github.com/tor2dbear/pia-terminal/blob/main/roadmap/reminder-push.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "pia-terminal/at-cron-scheduling",
      "repo": "tor2dbear/pia-terminal",
      "repoName": "PIA",
      "repoColor": "#38bdf8",
      "slug": "at-cron-scheduling",
      "title": "at / crontab — schemalagt (fd \"remind\")",
      "status": "done",
      "tags": [
        "commands",
        "scheduling"
      ],
      "updated": "2026-07-18",
      "issue": null,
      "order": null,
      "body": "## Mål\n(Research/idiom-beslut.) README:s Nivå 3 hade `remind`/schemalagt. Riktiga\nterminalnamnet är **`at`** (engångs) och **`crontab`** (återkommande), inte\n`remind`. Byggd som **lärverktyg** (vinkel spikad — se nedan).\n\n## Levererat (lärverktyg, in-flik)\n- **`at <tid> <kommando>`** schemalägger ett engångsjobb (`at now+5m echo hi`,\n  `at 14:30 …`); `at -l` listar (UTC-tider), `at -r <n>` tar bort.\n- **`crontab -e`** (redigera i nano) / **`-l`** (lista) / **`-r`** (ta bort).\n  Cron-syntax: fem fält `min tim dag mån veckodag` + `command`, med `*` `*/n`\n  `a-b` `a,b`. Validering varnar för trasiga rader.\n- Jobben bor i VFS-filer (`~/.pia/at`, `~/.pia/crontab`). En **scheduler**\n  (`pia/scheduler.ts`) tickar varje sekund medan fliken är öppen och kör due\n  jobb genom terminalen (`Terminal.fireScheduled` — en `⏰`-markör, kör, utan att\n  förstöra din halvskrivna rad). Ren cron-/tid-matte i `pia/cron.ts` (testad).\n- **Ärlig gräns:** fyrar bara medan fliken är öppen. Den \"riktiga\" push-versionen\n  (även när fliken är stängd) ligger som egen puck (`reminder-push`).\n\n## Vinkel-beslut\nByggt som **terminal-lärande** (lär dig `at`/cron-syntax i en sandlåda) — litet,\non-brand, ingen backend. *Påminnelsetjänsten* (backend + push) är en separat,\nstörre sak → `reminder-push`-pucken.\n\n## Research\n- **Idiom-drift att räta ut:** `remind` är en web-mynt. `at 9am`, `crontab -e` är\n  de äkta namnen och passar \"terminal-idiom först\". `remind` kan leva kvar som\n  alias.\n- Kärnfrågan är *vad* som körs och *när*: en webbflik lever inte dygnet runt. Ett\n  schemalagt jobb behöver antingen (a) bara fire medan fliken är öppen (svagt),\n  eller (b) en serverdel (Supabase cron / Edge Function) som gör något — mejl?\n  notis? Det drar mot backend och mot commodity-nytta.\n- Vinkeln avgör värdet: som *terminal-lärande* (lär dig `at`/`cron`-syntax i en\n  sandlåda) är det on-brand; som *påminnelsetjänst* är det commodity som\n  konkurrerar med allt.\n\n## Öppna frågor\n- Vad triggar jobbet i praktiken — bara in-flik, eller en serverdel? Utan svar är\n  featuren luftig.\n- Om den byggs: är poängen att *lära ut cron-syntax* (då räcker in-flik +\n  loggning) eller att *faktiskt påminna* (då krävs backend)? Bestäm vinkel först.\n\n_Ligger i `inbox` tills vinkeln är spikad._",
      "sourcePath": "roadmap/at-cron-scheduling.md",
      "sourceUrl": "https://github.com/tor2dbear/pia-terminal/blob/main/roadmap/at-cron-scheduling.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "pia-terminal/brew-packages",
      "repo": "tor2dbear/pia-terminal",
      "repoName": "PIA",
      "repoColor": "#38bdf8",
      "slug": "brew-packages",
      "title": "brew — paket/appar frikopplade från kärnan",
      "status": "done",
      "tags": [
        "packages",
        "architecture"
      ],
      "updated": "2026-07-18",
      "issue": null,
      "order": null,
      "body": "## Mål\nEtt sätt att bygga \"appar/paket\" **frikopplade från huvudappen**, installerade\non-demand — homebrew-känsla: `brew install <app>` lägger till kommandon/screen-\nappar i din PIA utan att blåsa upp kärnan.\n\n## Levererat (v1)\n- **`brew list` / `install <name>` / `uninstall <name>`.** Paket bor i\n  `src/packages/<namn>/` och exporterar ett `Package`-manifest (`{name,\n  description, commands}`). Install laddar paketet (**dynamisk import** → egen\n  chunk), registrerar kommandona i det levande registret (nytt\n  `CommandRegistry.unregister`), och sparar namnet i `~/.pia/packages`. Vid boot\n  återregistreras installerade paket (`registerInstalled` i `main.ts`), så de\n  överlever reload.\n- **CSP-säkert & tree-shakat:** dynamisk import av *lokala* chunks (samma origin).\n  Demo-paketet `cowsay` blev en egen **0,7 kB-chunk** som *inte* ligger i\n  huvudbundlen förrän man installerar det. Verifierat i bygget + tester +\n  end-to-end (install → kommandot funkar → uninstall → borta).\n- **Demo:** `cowsay` (`cowsay`/`cowthink`). Fler paket = ny mapp + en katalog-rad.\n\n## Katalog (v2)\nFyra paket nu: **snake** (utflyttat från kärnan), **2048** och **draw** (nya\nscreen-app-spel), och **cowsay**. Screen-appar funkar via `runApp` (core-context),\nså ett spel-paket är bara en mapp under `src/packages/`. Varje paket blir en egen\nlazy-chunk (2–4 kB) — inget av dem ligger i huvudbundlen förrän man installerar\ndet. `snake` kräver alltså `brew install snake` nu (opt-in, inte default).\n\n## Kvar (valfritt)\n- CSS-i-paket: spelens CSS bor fortfarande i globala `style.css` (JS är lazy, CSS\n  inte). En helt fristående paket-CSS är en större CSP-fråga.\n- Fler paket: `2048`s svep-styrning på mobil, en `fortune`, m.m.\n\n## Varför det passar PIA\n- Extension-punkterna finns redan: command-registry (`{name,help,run}`) och\n  screen-app-hosten (`ScreenApp` via `runApp`). Ett paket = en modul som\n  registrerar kommandon och/eller en screen-app — **samma sömmar som motorn**.\n- Håller kärnan smal: appar lazy-laddas (dynamisk import av lokala chunks) först\n  när de installeras; tree-shakas bort tills dess.\n\n## Haken (viktig)\n- **Riktiga homebrew installerar godtycklig kod från internet.** PIA:s strikta CSP\n  (`script-src 'self'`) blockerar fjärr-JS i runtime — samma vägg som python-wasm-\n  spiken slog i. Så paket måste **shippa med appen** (samma origin) och lazy-laddas.\n  Det blir en *kurerad katalog av valfria inbyggda appar*, inte \"vilket paket som\n  helst från vem som helst\".\n- Äkta tredjeparts-paket kräver antingen lättare CSP (säkerhetskostnad) eller en\n  sandlåda (iframe/worker + meddelande-protokoll) — mycket större grej, utanför v1.\n\n## Skiss (om vi bygger v1)\n- `src/packages/<name>/` — varje paket exporterar ett manifest: `{ name, help,\n  commands?, apps? }`. Inte importerat av kärnan → tree-shakat tills installerat.\n- `brew list` (katalog) · `brew install <name>` (dynamisk import + registrera) ·\n  `brew uninstall` · installerad uppsättning i `~/.pia/packages`, laddas om vid boot.\n- Kandidater att flytta ut / bygga som paket: snake, 2048, draw, äventyret.\n\n## Öppna frågor\n- Bara inbyggd katalog, eller en manifest-fil man kan peka om?\n- Relation till motor-extraktionen: samma sömmar, men \"paket i PIA\" vs \"bygg eget\n  skal på motorn\" är två olika produkter — bra att hålla isär.\n\n_Ligger i `inbox`. On-brand och arkitektoniskt snyggt; störst öppen fråga är CSP-\ngränsen för äkta tredjepart._",
      "sourcePath": "roadmap/brew-packages.md",
      "sourceUrl": "https://github.com/tor2dbear/pia-terminal/blob/main/roadmap/brew-packages.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "pia-terminal/terminal-engine-package",
      "repo": "tor2dbear/pia-terminal",
      "repoName": "PIA",
      "repoColor": "#38bdf8",
      "slug": "terminal-engine-package",
      "title": "Lyft ut terminal-motorn som fristående paket",
      "status": "done",
      "tags": [
        "terminal",
        "packaging"
      ],
      "updated": "2026-07-18",
      "issue": null,
      "order": null,
      "body": "## Mål\n(Research, inte beslutat.) Plocka ut PIA:s terminal-/shell-motor — command\nregistry, pipe-parsing, screen-app-host — som ett fristående, beroendefritt\npaket andra kan `npm install`:a och bygga egna små webb-skal på. \"Ugnen istället\nför pizzan.\"\n\n## Research\n- **Nischen är inte tom.** Etablerade libs finns redan: **jQuery Terminal**\n  (moget, men kräver jQuery), **javascript-terminal** (närmast idén: filsystem +\n  command-mapping + autocomplete + history), Ptty m.fl. (`xterm.js` är en annan\n  sak — ritar en riktig pty, är inget skal.) Så \"släpp den och folk strömmar\n  till\" är osannolikt. Gå in med rätt förväntan.\n- **Vad som skiljer PIA:s motor:**\n  1. Noll beroenden, modern TS/ESM (de andra är jQuery-baserade eller äldre).\n  2. **Screen-app-host** (`ScreenApp` i `screen.ts`): lämna över hela skärmen\n     till en app och återställ, med mobil-tangentbar. Mest distinkta\n     återanvändbara biten — de flesta libs är bara rad-in/rad-ut.\n  3. Adapter-sömmen: utbytbar storage/auth bakom interface. Ingen av de andra\n     har \"ta med ditt eget moln-backend\" som förstklassig idé.\n- **Redan halvvägs:** `registry.ts` och `parse.ts` är rena (ingen DOM);\n  kommandon rör bara `CommandContext` (ett interface), aldrig skärmen → själva\n  körmodellen är redan renderar-oberoende. Allt DOM-trassel bor i *en* fil\n  (`terminal.ts`). \"Lyft ut motorn\" = separera den DOM-renderaren från logiken\n  som redan står fri. Uppdelning, inte omskrivning.\n- **Testet på om gemet är på riktigt:** dra en ren gräns motor/demo och bygg en\n  *andra* pytteliten sak på samma motor. Går det smärtfritt → paketet finns.\n\n### Ärlig verdikt\nGör det för **portfolio/hantverk**, inte för att bli ett populärt paket. Ett\nrent, dokumenterat, testat litet bibliotek — plus \"jag byggde två saker på samma\nmotor\" — är en starkare portfolio-story än appen ensam. Men räkna med en fin\nartefakt, inte en hit.\n\n## Öppna frågor\n- Värt det jämfört med MCP-connectorn (se `ai-mcp-context`)? Båda är aktuella och\n  on-brand; välj efter vad som lockar mer — djup (motor) eller aktualitet (MCP).\n- Exakt paketgräns: vad flyttar (registry, parse, screen-interface, en headless\n  runner) och vad stannar som PIA-specifik referens-renderare?\n- Namn/scope om det publiceras; licensval.\n\n## Boundary-karta (uppmätt 2026-07-18)\nAv ~37 källfiler är bara **12** DOM-bundna; resten är redan ren motor.\n- **Ren motor (DOM-fri, lyft nästan som den är):** `parse.ts`, `glob.ts`,\n  `registry.ts`, hela `vfs/`, alla `commands/*`, de rena renderarna i `pia/`\n  (markdown/json/table), `share/*`, adapter-*interfaces*.\n- **Renderare (skärm-bunden, men redan parametriserad):** `terminal.ts` — tar\n  in `{vfs, adapter, registry, auth, session, share}` via konstruktorn. Kopplad\n  till PIA via `pia/rc` (config) + `pia/themes` (tema) + `ShareStore` — det som\n  ska injiceras bort i steg 2.\n- **PIA-specifikt (stannar):** teman, config, delning, boot, konkreta adaptrar,\n  `main.ts`-wiringen.\n\nDen enda riktiga design-frågan: `CommandContext` blandade motor-verktyg med\nPIA:s egna. Löst genom att dela den (se plan, steg 1).\n\n## Plan\n1. **Dela `CommandContext`** i `CoreCommandContext` (motor) + `CommandContext`\n   (PIA:s tillägg). Ren typ-ändring. — **klart** (PR #19).\n2. **Lossa renderaren:** injicera tema/config i `terminal.ts` istället för direkt\n   `pia/rc`/`pia/themes`-import. — **klart**. `terminal.ts` importerar ingen\n   `pia/`-modul längre; en `configure`-option ger prompt+alias (och applicerar\n   temat), PIA:s glue bor i `pia/terminalConfig.ts` och injiceras i `main.ts`.\n3. **`engine/`-mapp + publik `index.ts`** (motorns API-yta) — **klart** (PR #21).\n   `src/engine/index.ts` re-exporterar den återanvändbara ytan (command-modell,\n   parser, globbing, screen-app-interface, `Terminal`, VFS, adapter-*interfaces*)\n   bakom en dörr; ett smoke-test importerar *bara* genom den.\n4. **Bevisa med en andra app** — **klart** (PR #22). `src/examples/adventure/`\n   är ett litet textäventyr byggt *bara* på `engine/`:s publika API: egna\n   kommandon (`look`/`go`/`take`/`inventory`), egna minimala adapter-impl\n   (null-storage/-auth), noll av PIA:s kommandon/teman/config. Fyra tester\n   spelar igenom det genom motorns `Terminal`. Tree-shakas bort ur PIA:s bundle.\n   → **Gemet-testet passerat: motorn bär en helt annan app.**\n5. **Öppningsbar sida** — **klart**. Äventyret körs nu som en egen sida\n   (`/adventure/`) via vite multipage (`rollupOptions.input`: `main` + `adventure`).\n   Bygget bekräftar poängen: motorn blir en *delad chunk* (`assets/terminal-*.js`,\n   ~23 kB) som **båda** sidorna använder — inte en kopia per app. Sidan renderar i\n   riktig webbläsare under PIA:s strikta CSP utan runtime-/CSP-fel (där Python-spiken\n   föll). Så motorn syns nu, inte bara testas.\n6. **Full genericisering** — **klart**. `Command<Ctx>`, `CommandRegistry<Ctx>` och\n   `Terminal<Ctx>` är nu generiska över kontexten (default: PIA:s `CommandContext`,\n   så PIA är orört). De motor-/webb-generiska fälten (`session`, `pickFile`,\n   `saveFile`, `reloadFs`, `applyConfig`) flyttade in i `CoreCommandContext`; kvar i\n   `CommandContext` är bara PIA-specifika `auth`/`baseUrl`/`share`. Motorn defaultar\n   `vfs`/`adapter`/`session`, och en app lägger till egna fält via en ny\n   `extendContext`-krok (PIA:s glue i `pia/context.ts`, delad av `main.ts` och\n   testerna). → Äventyret skickar nu **bara** `registry` + `configure` — inga\n   `vfs`/`adapter`/`auth`-stubbar — och dess kommandon är `Command<CoreCommandContext>`,\n   dvs typen säger exakt vad skalet beror på. Bundlen krympte (äventyres-chunk 3,4→3,0 kB).\n7. **Paketera `engine/` som npm** — **klart**. `npm run build:engine`\n   (`tsc -p tsconfig.engine.json` + `scripts/build-engine.mjs`) bygger\n   `src/engine/index.ts` med transitiva beroenden till `dist-engine/` (JS +\n   `.d.ts`, ingen source-map som pekar på icke-shippade källor) och skriver en\n   egen `package.json` (`pia-terminal-engine`, `type: module`, `exports`,\n   `sideEffects: false`) + engelsk README. **Verifierat på riktigt:** `npm pack`\n   ger en giltig tarball (24 filer, ~28 kB); installerad från tarball i ett\n   färskt konsument-projekt importeras paketet via sitt namn och kör ett\n   registrerat kommando, `VFS.seed`, `tokenize` och `parseSequence`. App-bygget\n   är orört (separat tsconfig; `dist-engine/` är gitignore:at).\n\n## Kvar (release-beslut, inte kod)\n- **Licens**: `package.json` säger `UNLICENSED` som platshållare — välj licens\n  (motorn är tänkt att öppen-källas) innan första publiceringen. `npm publish`\n  körs från `dist-engine/` och är en manuell release-handling (kräver npm-konto).\n- **Valfri renhet**: motorns `Command`/`Terminal` defaultar fortfarande till PIA:s\n  `CommandContext`, så typytan (och en död, tree-shakebar `share/store.js`) drar\n  med PIA-typer i paketet. Att byta default till `CoreCommandContext` och flytta\n  ut `CommandContext` skulle ge en helt ren motorgräns — mekaniskt men rör ~7\n  PIA-kommandofiler; sparat som eget beslut.\n\n_Status `done`. Motorn är utlyft, generisk, öppningsbar som egen sida, och\npaketerbar/verifierad som npm — grunden (\"två saker på samma motor\") är på plats.\nFaktisk publicering + licensval är en manuell release-handling._",
      "sourcePath": "roadmap/terminal-engine-package.md",
      "sourceUrl": "https://github.com/tor2dbear/pia-terminal/blob/main/roadmap/terminal-engine-package.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "pia-terminal/package-ideas",
      "repo": "tor2dbear/pia-terminal",
      "repoName": "PIA",
      "repoColor": "#38bdf8",
      "slug": "package-ideas",
      "title": "Paket-roadmap — brew-appar som passar PIA",
      "status": "inbox",
      "tags": [
        "packages",
        "overview"
      ],
      "updated": "2026-07-18",
      "issue": null,
      "order": null,
      "body": "## Vad som gör ett bra PIA-paket\n- **On-brand:** Unix-idiom, \"lär dig terminalen\", eller kul liten dator.\n- **Helt klientsidan:** strikt CSP (ingen tredjeparts-`connect-src`) ⇒ **inga\n  nätverkspaket**. Web Audio och rena algoritmer är OK.\n- **Självständigt:** en mapp under `src/packages/`, egen lazy-chunk.\n\n## Katalog idag (klart)\n`snake` · `2048` · `draw` · `cowsay` · `cal` · `bc` · `fortune` · `sl` ·\n`cmatrix` · `tutor` · `life` · `tetris` · `qr` · `python` · `figlet` · `wordle` ·\n`piano` · `coreutils` (rev · base64 · factor · xxd) · `man` (+ `apropos`) · `minesweeper`.\n\n## Kandidater (S/M/L = insats)\n\n**Unix-klassiker**\n- `cal` (S) · `bc` (S) · `qr` (M, QR av en share-länk) · `figlet` (S–M) ·\n  `fortune` (S) · `sl` (S) · `cmatrix` (S) · ~~`rev`/`factor`/`base64`/`xxd`~~\n  (S) — **klart** (`coreutils`-paketet).\n\n**Arkad (screen-appar)**\n- `tetris` (M) · ~~`minesweeper` (M)~~ **klart** · `life` (S) · `wordle` (S–M) ·\n  `sudoku` (M) · `hangman` (S) · `sokoban` (M).\n\n**Lär dig terminalen (PIA:s själ)**\n- `tutor` (M) — interaktiv terminal-kurs 🎯 · `vim` (L) — modal editor ·\n  `sed`/`awk` (M) · ~~`man` (S–M) — fylligare manualsidor~~ — **klart**\n  (`man` + `apropos`, hybrid: register-skelett + handskrivna sidor + koncept-sidor).\n\n**Kreativt / dev**\n- `piano` (M, Web Audio) · `asciiquarium` (S–M) · `jq` (M) · `diff` (S).\n\n## Rekommenderad ordning\n1. ~~`cal` + `bc` + `fortune`~~ — **klart** (första omgången, egna lazy-chunks,\n   med i touren).\n2. ~~`sl` + `cmatrix`~~ — **klart** (animerade screen-appar).\n3. ~~`tutor` 🎯~~ — **klart** (8-lektioners syntax-kurs, screen-app).\n4. ~~`life` + `tetris`~~ — **klart** (arkad screen-appar).\n5. ~~`qr`~~ — **klart** (QR av text/share-länk; vetat bibliotek, jsqr-verifierat).\n\n**Hela ordningen 1–5 är levererad.** Resten av kandidaterna ovan bor här tills de\nbefordras. Nästa stora mål: `python` (Pyodide/iframe) — se `python-wasm.md`.",
      "sourcePath": "roadmap/package-ideas.md",
      "sourceUrl": "https://github.com/tor2dbear/pia-terminal/blob/main/roadmap/package-ideas.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "pia-terminal/pkg-bc",
      "repo": "tor2dbear/pia-terminal",
      "repoName": "PIA",
      "repoColor": "#38bdf8",
      "slug": "pkg-bc",
      "title": "paket: bc — kalkylator",
      "status": "done",
      "tags": [
        "packages"
      ],
      "updated": "2026-07-18",
      "issue": null,
      "order": null,
      "body": "## Mål\n`bc` — aritmetik utan `eval`: tokenisera + rekursiv-descent-parser för\n`+ - * / % ( )` med precedens. `bc \"2+3*4\"` → 14. Insats: S–M. **Första omgången.**\n\n## Levererat\n`brew install bc`. Ren `evalExpr(src)` — tokenizer + rekursiv-descent för\n`+ - * / % ^` (höger-associativ potens), unärt minus och parenteser, utan\n`eval`. `bc \"2 + 3 * 4\"` (argument) eller `echo 6*7 | bc` (stdin, rad för rad).\nFel (division med noll, saknad parentes, skräptecken) → `bc: <meddelande>`.\nEnhetstest (precedens, potens, fel) + med i touren. JS-flyttal, inte bc:s\ngodtyckliga precision — en lärleksak, inte en numerikmotor.",
      "sourcePath": "roadmap/pkg-bc.md",
      "sourceUrl": "https://github.com/tor2dbear/pia-terminal/blob/main/roadmap/pkg-bc.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "pia-terminal/pkg-cal",
      "repo": "tor2dbear/pia-terminal",
      "repoName": "PIA",
      "repoColor": "#38bdf8",
      "slug": "pkg-cal",
      "title": "paket: cal — månadskalender",
      "status": "done",
      "tags": [
        "packages"
      ],
      "updated": "2026-07-18",
      "issue": null,
      "order": null,
      "body": "## Mål\n`cal [månad år]` ritar en månadskalender (äkta Unix). Default: nuvarande månad.\nRen, testbar (ge månad/år för determinism). Insats: S. **Första omgången.**\n\n## Levererat\n`brew install cal`. Ren `renderCal(month, year)` → rader, egen lazy-chunk.\n`cal`, `cal 7`, `cal 7 2026`. Enhetstest (rubrik, veckodagsjustering, skottår)\n+ med i touren. Ingen dag-highlight: `print` är ren text (ingen ANSI), och äkta\n`cal` tappar highlight i pipe/icke-TTY ändå — plain kalender är idiom-rätt.",
      "sourcePath": "roadmap/pkg-cal.md",
      "sourceUrl": "https://github.com/tor2dbear/pia-terminal/blob/main/roadmap/pkg-cal.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "pia-terminal/pkg-cmatrix",
      "repo": "tor2dbear/pia-terminal",
      "repoName": "PIA",
      "repoColor": "#38bdf8",
      "slug": "pkg-cmatrix",
      "title": "paket: cmatrix — matrix-regn",
      "status": "done",
      "tags": [
        "packages",
        "fun"
      ],
      "updated": "2026-07-18",
      "issue": null,
      "order": null,
      "body": "## Mål\n`cmatrix` — grönt tecken-regn (Matrix). Screen-app, animerad, ^X avslutar.\nInjicerbar rng för test. Insats: S.\n\n## Levererat\n`brew install cmatrix`. Ren `MatrixRain`-simulering: varje kolumn kör en droppe\nvars huvud faller en rad per tick och lämnar ett ändligt spår; klarad botten →\nkolumnen respawnar. Injicerbar rng → deterministisk under test. Screen-appen\nrenderar rutnätet i en enfärgad grön `<pre>` (CSP-säker `textContent`, glöd via\n`text-shadow`). `q`/Escape/`^X` avslutar. Enhetstest (tomt vid start, huvuden\navancerar, determinism för fast seed).",
      "sourcePath": "roadmap/pkg-cmatrix.md",
      "sourceUrl": "https://github.com/tor2dbear/pia-terminal/blob/main/roadmap/pkg-cmatrix.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "pia-terminal/pkg-fortune",
      "repo": "tor2dbear/pia-terminal",
      "repoName": "PIA",
      "repoColor": "#38bdf8",
      "slug": "pkg-fortune",
      "title": "paket: fortune — slumpcitat",
      "status": "done",
      "tags": [
        "packages"
      ],
      "updated": "2026-07-18",
      "issue": null,
      "order": null,
      "body": "## Mål\n`fortune` — skriver ett slumpat citat ur en inbyggd lista. Injicerbar rng för\ntest. Klassiskt Unix-godis. Insats: S. **Första omgången.**\n\n## Levererat\n`brew install fortune`. Inbyggd citatlista (dator-/Unix-tema, on-brand). Ren\n`pickFortune(rng)` med injicerbar rng → enhetstestbar. Kommandot seedar från\nklockan (`Date.now()`), så det varierar mellan riktiga anrop men blir\ndeterministiskt under tourens frusna klocka (ingen redaktion behövs). Med i\ntouren.",
      "sourcePath": "roadmap/pkg-fortune.md",
      "sourceUrl": "https://github.com/tor2dbear/pia-terminal/blob/main/roadmap/pkg-fortune.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "pia-terminal/pkg-life",
      "repo": "tor2dbear/pia-terminal",
      "repoName": "PIA",
      "repoColor": "#38bdf8",
      "slug": "pkg-life",
      "title": "paket: life — Conways livsspel",
      "status": "done",
      "tags": [
        "packages",
        "games"
      ],
      "updated": "2026-07-18",
      "issue": null,
      "order": null,
      "body": "## Mål\n`life` — Conways Game of Life. Screen-app: rutnät, tick-loop, seed-mönster\n(glider m.m.), pausa/stega. Ren grid-logik (testbar). Insats: S–M.\n\n## Levererat\n`brew install life`. Ren `Life`-klass: begränsat rutnät (kanter döda), B3/S23-\nregler i `step()`, `randomize(rng, density)`, `population()`. Enhetstestat\n(block står still, blinker oscillerar period 2, ensam cell dör, densitet 0/1).\nScreen-appen seedar slumpmässigt och stegar på en timer: `space` pausar, `s`\nstegar en generation, `r` seedar om, `^X` avslutar.",
      "sourcePath": "roadmap/pkg-life.md",
      "sourceUrl": "https://github.com/tor2dbear/pia-terminal/blob/main/roadmap/pkg-life.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "pia-terminal/pkg-qr",
      "repo": "tor2dbear/pia-terminal",
      "repoName": "PIA",
      "repoColor": "#38bdf8",
      "slug": "pkg-qr",
      "title": "paket: qr — QR-kod som ASCII",
      "status": "done",
      "tags": [
        "packages"
      ],
      "updated": "2026-07-18",
      "issue": null,
      "order": null,
      "body": "## Mål\n`qr <text>` — genererar en QR-kod som ASCII-block, **ren algoritm, inget\nnätverk**. Knyter ihop med `share`/`publish`: gör en QR av en delnings-länk.\nInsats: M (QR-kodning är lite mattigt men self-contained).\n\n## Levererat\n`brew install qr`. `qr [-l|-m|-q|-h] <text>` (eller pipa in text, så\n`share notes.txt`-URL:en kan flöda in) visar en QR-kod som en screen-app —\nhalf-block-tecken i en tight `<pre>` (line-height 1) med 4-moduls tyst zon, så\nden faktiskt går att skanna. Byte-mode, UTF-8, auto-vald version. `^X` stänger.\n\n## Beslut: vetat bibliotek istället för handrullat (accepterad divergens)\nTill skillnad från övriga paket (alla handrullade) bygger `qr` på\n**`qrcode-generator`** — ett pyttelitet, beroendefritt MIT-bibliotek. QR är en\nknepig standard (Reed–Solomon, maskning, format/versionsinfo) som är lätt att få\nsubtilt fel och **omöjlig att verifiera visuellt** i en huvudlös miljö. En\nhandrullad encoder byggdes först men konvergerade inte mot skanningsbar output\nunder tidsbudgeten; risken att skeppa en tyst trasig QR var för hög.\n\n- **Beroendet är lazy:** det bor bara i `qr`-chunken (egen ~23 kB-chunk), aldrig\n  i huvudbundlen förrän man `brew install qr`. Same-origin efter bundling → inom\n  strikt CSP.\n- **Korrekthet bevisas:** ett round-trip-test avkodar varje genererad kod med en\n  riktig QR-läsare (`jsqr`, devDependency) för L/M/Q/H + UTF-8 — så vi vet att\n  koderna skannar, inte bara att de \"ser ut som\" QR.\n- PIA:s första runtime-beroende utöver Supabase. Flaggat som medvetet val, i\n  samma anda som share→URL-divergensen — inte drift.\n\n_En framtida variant skulle kunna ersätta biblioteket med en egen encoder om vi\nhar tid att verifiera den ordentligt (jsqr-round-trip finns redan som skyddsnät)._",
      "sourcePath": "roadmap/pkg-qr.md",
      "sourceUrl": "https://github.com/tor2dbear/pia-terminal/blob/main/roadmap/pkg-qr.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "pia-terminal/pkg-sl",
      "repo": "tor2dbear/pia-terminal",
      "repoName": "PIA",
      "repoColor": "#38bdf8",
      "slug": "pkg-sl",
      "title": "paket: sl — ånglok",
      "status": "done",
      "tags": [
        "packages",
        "fun"
      ],
      "updated": "2026-07-18",
      "issue": null,
      "order": null,
      "body": "## Mål\n`sl` — animerat ASCII-ånglok (det klassiska `ls`-typo-skämtet). Screen-app med\nen tick-loop som drar loket över skärmen. Insats: S.\n\n## Levererat\n`brew install sl`. Ren `renderFrame(offset, cols, phase)` ritar loket vid en\nkolumn-offset (klipps mot fältkanterna); `trainLines(phase)` växlar hjulrad så\ndrivhjulen \"snurrar\". Screen-appen drar offset nedåt på en timer och avslutar\nsjälv när tåget klarat vänsterkanten (`^X` bailar tidigt). Som äkta `sl` sväljs\ntangenttryck — det är skämtet. Enhetstest (kant-klippning, hjulfas,\nauto-avslut).",
      "sourcePath": "roadmap/pkg-sl.md",
      "sourceUrl": "https://github.com/tor2dbear/pia-terminal/blob/main/roadmap/pkg-sl.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "pia-terminal/pkg-tetris",
      "repo": "tor2dbear/pia-terminal",
      "repoName": "PIA",
      "repoColor": "#38bdf8",
      "slug": "pkg-tetris",
      "title": "paket: tetris",
      "status": "done",
      "tags": [
        "packages",
        "games"
      ],
      "updated": "2026-07-18",
      "issue": null,
      "order": null,
      "body": "## Mål\n`tetris` — screen-app-spel: fallande tetrominoer, rotation, radrensning, nivåer.\nRen grid-/kollisions-logik med injicerbar rng (testbar). Insats: M.\n\n## Levererat\n`brew install tetris`. Ren, huvudlös `Tetris`-klass: 10×20-brunn, alla sju\ntetrominoer ur en blandad 7-bag (injicerbar rng), rotation med enkel wall-kick,\ngravitation, radrensning och poäng/nivåer. Fullt enhetstestat (spawn, radrensning\n+ poäng, flytt/rotation i brunnen, väggkollision, game over när stacken når\ntoppen, deterministisk bag för fast seed). Screen-appen renderar en snapshot och\näger inga regler själv: pilar flyttar/roterar, `↓` mjukdropp, `space` hårddropp,\n`^X` avslutar, `Enter` startar om vid game over. D-pad för mobil.",
      "sourcePath": "roadmap/pkg-tetris.md",
      "sourceUrl": "https://github.com/tor2dbear/pia-terminal/blob/main/roadmap/pkg-tetris.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "pia-terminal/pkg-tutor",
      "repo": "tor2dbear/pia-terminal",
      "repoName": "PIA",
      "repoColor": "#38bdf8",
      "slug": "pkg-tutor",
      "title": "paket: tutor — interaktiv terminal-kurs",
      "status": "done",
      "tags": [
        "packages",
        "learning"
      ],
      "updated": "2026-07-18",
      "issue": null,
      "order": null,
      "body": "## Mål\nEtt `tutor`-paket: en **guidad, interaktiv kurs** i terminalen som lär ut\nkommandon steg för steg och kollar vad du skriver. PIA:s själ i paketform — ingen\nannan webbterminal har en inbyggd kurs.\n\n## Skiss\nScreen-app eller kommando som visar lektioner (\"skapa en fil med touch\", \"lista\nmed ls\") och validerar. Lektionsdata som ren data. Insats: M.\n\n## Levererat\n`brew install tutor`. Screen-app med egen mini-prompt: visar en instruktion, du\nskriver ett kommando, den kollar *formen* mot lektionen och går vidare — lär ut\nriktig shell-syntax (kommandonamn, flaggor, `>`, `|`) som funkar i valfritt\nskal. 8 lektioner (`pwd` → `ls` → `mkdir` → `cd` → redirect → `cat` → pipe →\n`help`), var och en med förklaring vid rätt svar och lösningsförslag vid fel.\n\n**Arkitektur:** lektionerna är ren data (`LESSONS`) med en pure `accepts(input)`\n+ `normalize`; progressionen är en DOM-lös `TutorSession` (`submit`/`current`/\n`isComplete`). Allt enhetstestat; screen-appen är bara skärm + radeditor.\n\n**Not (medvetet val):** kursen validerar *syntax*, den kör inte kommandona mot en\nriktig sandbox-VFS. Det är en solid MVP och on-brand; en framtida version skulle\nkunna köra svaren mot en scratch-VFS för att också visa resultatet.",
      "sourcePath": "roadmap/pkg-tutor.md",
      "sourceUrl": "https://github.com/tor2dbear/pia-terminal/blob/main/roadmap/pkg-tutor.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "pia-terminal/publish-folder",
      "repo": "tor2dbear/pia-terminal",
      "repoName": "PIA",
      "repoColor": "#38bdf8",
      "slug": "publish-folder",
      "title": "publish — mappar blir en publik sida",
      "status": "done",
      "tags": [
        "share",
        "web"
      ],
      "updated": "2026-07-18",
      "issue": null,
      "order": null,
      "body": "## Mål\n`publish <mapp>` gör `.md`-filerna i en mapp till en liten publik, statisk sida\nmed egen URL. En läs-vy för omvärlden, byggd av det du redan skrivit i\nterminalen.\n\n## Levererat\n- **`publish <mapp>`** samlar mappens `.md`-filer (toppnivå), packar dem\n  self-contained i URL:en och skriver ut en länk `…#p=<payload>`. `index.md` /\n  `README.md` läggs först, resten alfabetiskt.\n- **Öppnar man en `#p=`- (eller `#s=`-) länk landar innehållet i *din egen*\n  session** — inte i en främmande sandlåda. `main.ts` bootar den vanliga\n  terminalen (inloggad om du är det, annars guest) och materialiserar det delade\n  i **`~/incoming/`** (mapp → `~/incoming/<namn>/`, ensam fil → `~/incoming/`),\n  **i minnet** (inget sparas). En banner + auto-`cd`/`ls` visar filerna direkt.\n  Mottagaren **behåller det den vill med `cp`** — att bara öppna en länk skriver\n  aldrig tyst till kontot. `share` och `publish` delar nu exakt denna väg\n  (`pia/incoming.ts`), så beteendet är synkat.\n- **Återanvänder `glow`/`cat`** för att läsa markdown istället för en egen\n  renderare. Ny liten motor-metod `Terminal.exec(line)` kör ett kommando\n  programmatiskt (auto-`ls` vid boot; även användbar i tester).\n\n## Beslut på öppna frågor\n- **Var bor det:** hash-URL, precis som `share <fil>` — self-contained, ingen\n  server, funkar för guests. (Supabase-rad / Cloudflare-funktion valdes bort.)\n- **Hur visas det:** som en **prompt** i din egen session. Innehållet blir riktiga\n  filer i `~/incoming/` som du kan `cat`/`glow`/`nano`/`cp`. *(Reviderat två\n  gånger: v1 var en renderad HTML-sida; v2 en efemär sandlåda; v3 — detta —\n  landar i din egen session så du kan **behålla** det, vilket var poängen.)*\n- **Spara automatiskt?** Nej (val \"B\"): landar i minnet, du behåller med `cp`. Att\n  tjuvkika smutsar inte ner kontot; det matchar ändå `~/shared/`-mönstret för\n  e-post-delning, fast utan tyst skrivning.\n- **Login eller guest:** din session — inloggad behåller i molnet, guest lokalt.\n- **Storleksgräns:** `MAX_PUBLISH_PAYLOAD` = 32 KB. Web-divergensen (`publish` →\n  URL) stod redan som accepterad i CLAUDE.md.\n\n## Levererat (forts.)\n- **`share <mapp>`** funkar nu också — `share` tar fil *eller* mapp, `publish`\n  finns kvar som eget kommando, och båda bygger exakt samma `#p=`-länk via en\n  delad `folderLink`-helper. En delad mapp tar med **alla** toppnivå-filer (inte\n  bara `.md`), eftersom viewern nu är ett filsystem-drop där `cat`/`glow` läser\n  vad som helst. `share <mapp> <email>` avvisas (sam-redigering är per fil).\n  *(Vi valde bort alias — inga `scp`/`shar`/`tar`-alias; deras arg-grammatik\n  krockar. `share`/`publish` är rena verb med korrekt beteende.)*\n\n## Ev. följd (ej gjort, valfritt)\n- Rekursiv mapp (undermappar med), och bilder/assets (idag bara text i hashen).",
      "sourcePath": "roadmap/publish-folder.md",
      "sourceUrl": "https://github.com/tor2dbear/pia-terminal/blob/main/roadmap/publish-folder.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "pia-terminal/python-wasm",
      "repo": "tor2dbear/pia-terminal",
      "repoName": "PIA",
      "repoColor": "#38bdf8",
      "slug": "python-wasm",
      "title": "python — kör riktig kod i sandbox (Pyodide/WASM)",
      "status": "done",
      "tags": [
        "apps",
        "wasm"
      ],
      "updated": "2026-07-18",
      "issue": null,
      "order": null,
      "body": "## Levererat (2026-07-18)\n`brew install python`, sedan `python fil.py` / `python -c \"kod\"` kör **riktig\nCPython 3.12** i webbläsaren via Pyodide/WASM — **helt self-contained, ingen CDN**.\n\n- **Isolerad sandbox-iframe:** `public/python-sandbox.html` + `.js` kör Pyodide\n  i en egen browsing-context med en *lösare* CSP (`wasm-unsafe-eval`) men allt\n  same-origin (`'self'`). Huvudappens strikta CSP rörs inte — den fick bara\n  `frame-src 'self'` (plus en per-path-regel i `_headers` + `<meta>` för\n  sandbox-sidan, med `X-Frame-Options: SAMEORIGIN`).\n- **Självhostad Pyodide:** `scripts/fetch-pyodide.mjs` (körs som `prebuild`)\n  hämtar ~14 MB kärnfiler till `public/pyodide/` (gitignored) → vite kopierar\n  till `dist/pyodide/`. Idempotent (hoppar över om filer finns → varm CI-cache\n  gratis), retrys, och failar bygget hårt om den inte kan hämta. CI cachar\n  `public/pyodide` (nyckel = hash av fetch-skriptet). **Inget nytt\n  runtime-beroende** i appen; ingen tredjeparts-`connect-src`/`script-src`.\n- **postMessage-brygga:** `src/packages/python/bridge.ts` skapar en dold,\n  återanvänd iframe (Pyodide laddas lazy en gång) och skickar kod / tar emot\n  `{stdout, stderr, result, error}`. `index.ts` läser filer ur VFS:en, kör, och\n  skriver ut rader + fel; ett bart uttryck skrivs ut REPL-likt.\n- **Verifierat end-to-end** i riktig Chromium (Playwright) mot det byggda\n  `dist/` **med all extern nätverkstrafik blockerad**: Python körde ändå\n  (`python 3.12.1`, `6! = 720`, flerradsutskrift, listkomprehension-repr,\n  riktig traceback för fel) med **noll externa requests** — bevisar att inget\n  CDN behövs. CI-säkra tester: `parsePythonArgs` (enhet) + `brew install python`\n  i touren. Pyodide/WASM körs inte i vitest — för tungt för CI.\n\n## REPL + VFS-brygga (2026-07-18)\n- **REPL:** `python` utan argument öppnar ett interaktivt läge (screen-app) med\n  `>>>`-prompt, historik, och **persistent session** (variabler/funktioner lever\n  mellan rader). Flerradsblock hanteras via Pythons egen `codeop.compile_command`\n  i sandboxen — ofullständig input (`def f():`…) byter prompt till `...` tills\n  blocket är klart. `exit()`/`quit()`/`^X`/`^D` avslutar.\n- **VFS-brygga:** filerna i arbetsmappen mountas in i Pyodides FS före varje\n  körning och nya/ändrade filer synkas tillbaka efteråt. Så `python foo.py` kan\n  **läsa** dina filer, och filer det **skapar** dyker upp i `ls`/`cat`. Gäller\n  både script och REPL.\n- **Verifierat i Chromium** (nät blockerat, noll externa requests): ofullständig-\n  block-detektering (`def f():` → incomplete), persistent state (`x = f()*2` → 42\n  över tre rader), och fil-round-trip (Python läser `in.txt`, skriver `out.txt`\n  som synkas tillbaka). Enhetstester: `collectDirFiles` + REPL-flödet (jsdom,\n  mockad runner).\n\n## Kvar (valfri polish)\n- **micropip/paket** — kräver att sandbox-CSP:n öppnas för PyPI/Pyodide-CDN, dvs.\n  ett medvetet avsteg från self-containment (opt-in nätverk). Eget beslut.\n- Reproducerbart e2e-skript (Playwright) som valfri `test:python` utanför CI.\n- Version-bump av Pyodide sker i `scripts/fetch-pyodide.mjs` (bustar CI-cachen).\n\n\n## Mål\n`python script.py` kör riktig Python i webbläsaren via Pyodide (WASM), mot en fil\ni VFS:en. \"Riktig liten dator\" på allvar — kod som faktiskt exekverar.\n\n## Research\n- **Hög insats, högt värde.** Pyodide är ett tungt WASM-paket; måste ligga bakom\n  dynamic import (som `supabase-js`) så basbundlen inte betalar för det. Ladda\n  först vid första `python`.\n- Stark portfolio-signal: WASM + sandboxad exekvering imponerar och är on-brand\n  (lärande, \"riktig dator\"). Men det är det dyraste drömmålet — märk det som\n  sådant.\n- VFS-brygga: Pyodide har eget FS. Behöver mappa läs/skriv mot vår VFS så\n  `python` ser filerna man skapat i terminalen, och stdout landar i vår utskrift\n  (komponerbar i pipes).\n- Idiom: `python script.py` är rakt av äkta. Ingen divergens.\n\n## CSP-blockeraren + iframe-vägen (spikat 2026-07-18)\nEn spik visade: Pyodide **kör** (Python 3.14, sum=5050) men kräver `unsafe-inline`\ni CSP (flera varierande inline-script-hashar) — **oförenligt med huvudappens\nstrikta CSP**. Direkt-i-huvudsidan är alltså dött.\n\n**Lösningen: sandboxad iframe.** Kör Pyodide i en egen sida (`/python-sandbox.html`)\npå PIA:s **egen origin** med en *egen, lösare* CSP (`wasm-unsafe-eval` etc.) — utan\natt röra huvudappens CSP. Terminalen ↔ iframe via `postMessage`.\n\n**Krävs (allt byggbart, inga nya tredjeparts­tjänster):**\n1. `/python-sandbox.html` med lös CSP för WASM/eval.\n2. Huvud-CSP: lägg till `frame-src 'self'`.\n3. **Själv-hosta Pyodide** (~10–25 MB statiska filer från npm-paketet `pyodide`,\n   kopieras till `dist/`) — annars laddar den från jsdelivr-CDN → CSP `connect-src`\n   blockerar. (Alternativ: vitlista CDN i iframens CSP, men tappar self-contained.)\n4. `postMessage`-brygga (skicka kod, strömma stdout/stderr).\n5. `python`-kommando (REPL + kör `.py` från VFS).\n\n## Paketering: `brew install python` (spikat 2026-07-18)\nPython exponeras som ett brew-paket — `brew install python` — men är ett\n**accepterat \"tjockt paket\"**, inte rent frikopplat som `cal`/`bc`/`fortune`.\n\n**Varför brew:**\n- On-brand (`brew install python` är precis äkta homebrew).\n- **Opt-in-grinden är själva poängen:** Pyodide är ~10–25 MB — får aldrig ligga\n  i basbundlen. Utan install → `unknown command`, som övriga paket. Sätter också\n  förväntan (\"laddar en stor runtime\").\n- Kommando-sömmen räcker: `python` är en vanlig `Command` som pratar med iframen\n  via `postMessage` — en lazy-chunk som alla andra paket.\n\n**Varför det inte är ett *rent* paket:** till skillnad från `cal` kräver det två\nsaker som bor i **kärnan**, inte i paketmappen:\n1. Sandbox-sidan (`/python-sandbox.html`) — statisk route med egen lösare CSP.\n2. `frame-src 'self'` i huvud-CSP:n.\nBåda är billiga/harmlösa när python *inte* är installerat (pytteliten HTML som\naldrig laddas + ett CSP-direktiv utan effekt utan iframe). Det dyra —\nPyodide-WASM:en — förblir äkta lazy: iframen hämtar den först vid första\n`python`-körningen.\n\n**Beslut:** special-casa python, **generalisera inte `Package`-interfacet** för\nN=1. Katalograden registrerar `python`-kommandot (lazy bridge-klient);\nsandbox-sida + CSP är kärn-infra. Dyker ett andra tungt sandbox-paket upp senare\n→ generalisera då. Var ärlig i pucken: accepterat \"tjockt paket\" (samma anda som\nshare→URL-divergensen), inte en drift.\n\n## Öppna frågor\n- Bara köra en fil, eller även REPL (`python` utan arg)?\n- Micropip/paket-ekosystem? Börja utan.\n- Bundle-storlek: lazy-ladda Pyodide bara vid första `python`.\n\n_Ligger i `later` — tyngsta drömmålet, men iframe-vägen är nu spikad och\nbyggbar. Paketeringen (brew, tjockt paket) är spikad. Bra att ta **efter**\npaket-omgången (sl/cmatrix → tutor → life/tetris → qr)._",
      "sourcePath": "roadmap/python-wasm.md",
      "sourceUrl": "https://github.com/tor2dbear/pia-terminal/blob/main/roadmap/python-wasm.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "pia-terminal/settings-appearance",
      "repo": "tor2dbear/pia-terminal",
      "repoName": "PIA",
      "repoColor": "#38bdf8",
      "slug": "settings-appearance",
      "title": "Utseende i ~/.pia/config — egna färger, font, prompt",
      "status": "done",
      "tags": [
        "config",
        "theme"
      ],
      "updated": "2026-07-18",
      "issue": null,
      "order": null,
      "body": "## Mål\nGöra `~/.pia/config` till en *riktig* settings-fil som i iTerm/Alacritty/kitty:\negna hex-färger och font (inte bara de fyra färdiga temana), och ett\nterminaltroget sätt att styla prompten.\n\n## Levererat (färger + font)\n- **Egna färger** överlagrar valt tema: `color.<token> = #hex` för `bg fg dim\n  accent error`. Validerad hex (3/6/8 siffror); ogiltiga värden ignoreras.\n- **Font + storlek:** `font = \"Namn\", monospace` (installerat teckensnitt via\n  namn) och `font-size = N` (8–40 px). `#screen` läser `var(--font-size, 14px)`.\n- Appliceras via CSSOM (`setProperty` på `documentElement`) ovanpå temat — samma\n  CSP-säkra mekanism som `theme`-kommandot. Borttagen override faller tillbaka\n  (färger via `applyTheme`, font via `removeProperty`).\n- **`source ~/.pia/config`** (alias `.`) — läser om configen och applicerar direkt\n  efter en hand-edit (`nano ~/.pia/config`), utan reload. Äkta rc-idiom\n  (`source ~/.bashrc`). Vägrar allt utom config-filen (PIA kör inga skript).\n- Seed-configen visar nu exempel på `color.*`, `font`, `font-size`.\n\n## Levererat (prompt-styling)\n- **Prompten färgas per segment, zsh-stil:** `%F{token|#hex}…%f` (foreground),\n  `%B…%b` (fet), `%%` (literalt %). En token → `var(--token)` (tema-medvetet),\n  hex passerar rakt igenom. Valet blev zsh `%F{}` (läsbart + tema-medvetet)\n  framför rå ANSI.\n- Parsern (`terminal/prompt.ts`) ger styled-segment; renderaren bygger färgade\n  spans via CSSOM (CSP-säkert) — både i **live-raden** och de **ekade**\n  kommando-/`^C`/Tab-raderna (ny `printPromptLine`). Ekade prompter matchar nu\n  live-promptens färger (`.term-echo-prompt` delar färg med `.term-prompt`).\n- Seed-configen visar ett `%F`-exempel. Verifierat i riktig Chromium: en\n  `%F`-prompt renderas med rätt färger per segment, utan CSP-fel.\n\n## Öppna frågor\n- Font-**filer** (egna `.woff`) krockar med CSP `font-src 'self'` — separat större\n  grej; idag bara installerade fonter via namn.\n- Ska custom font/size gälla även screen-appar (editor/pager), eller bara skalet?\n  Idag `#screen` (skalet); apparna ärver där de kan.",
      "sourcePath": "roadmap/settings-appearance.md",
      "sourceUrl": "https://github.com/tor2dbear/pia-terminal/blob/main/roadmap/settings-appearance.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "pia-terminal/fs-cp",
      "repo": "tor2dbear/pia-terminal",
      "repoName": "PIA",
      "repoColor": "#38bdf8",
      "slug": "fs-cp",
      "title": "cp — kopiera filer och mappar",
      "status": "done",
      "tags": [
        "fs",
        "commands"
      ],
      "updated": "2026-07-17",
      "issue": null,
      "order": null,
      "body": "## Mål\n`cp <källa> <mål>` kopierar en fil; `cp -r <mapp> <mål>` kopierar ett träd. Idag\nfinns `mv` men ingen kopiering — en glaring lucka bland grunderna.\n\n## Research\n- **Var det hör hemma:** `src/commands/fs.ts`, bredvid `mv`. Mycket av\n  path-logiken (mål är fil vs befintlig mapp) kan speglas från `mv`.\n- VFS:en är ett träd i minnet; en kopia måste vara en *djup* klon av noden, inte\n  en delad referens — annars muterar man båda. Verifiera att en kopierad mapp\n  inte delar barn med originalet.\n- Idiom: `cp fil dir/` lägger `fil` *i* mappen; `cp a b` skapar/skriver över `b`.\n  `cp -r` krävs för mappar (utan `-r` → fel \"omitting directory\", som riktiga cp).\n- Persistens: efter kopian, `ctx.persist()` som andra muterande fs-kommandon.\n\n## Öppna frågor\n- Skriva över befintligt mål tyst (cp-default) eller varna? Följ cp: tyst.\n- `cp -r` in i sig själv (mål inuti källa) — upptäck och vägra, annars oändlig\n  loop. Testa explicit.\n- Interagerar med delade filer (`@`-märkta): ska en kopia ärva delningen eller bli\n  en fristående lokal fil? Troligen fristående — flagga i bygget.\n\n## Levererat\nNy VFS-metod `copy(from, to, recursive)` (bredvid `move`) med en privat\n`clone`-djupklon, plus `cp`-kommandot i `fs.ts`. Så här landade frågorna:\n- **Djup klon:** en kopierad mapp delar inga barn med originalet — muterar man\n  kopian rör originalet inte (testat explicit).\n- **`dir/`-mål:** `cp fil dir` lägger `fil` *i* mappen (speglar `move`s\n  into-dir-semantik). `cp a b` skriver/överskriver `b` tyst (cp-default).\n- **`-r` krävs för mappar:** utan → fel `omitting directory (use -r)`.\n- **Self-copy-skydd:** `cp -r d d/inner` (mål inuti källan) vägras — inget\n  oändligt träd.\n- **Delning strippas:** en kopia blir en fristående *lokal* fil, ärver inte\n  källans `shareId` (beslut från öppna frågan).\n- **Flera källor:** `cp a b c dir/` kopierar alla in i `dir` (kräver att målet är\n  en mapp) — vilket ger `cp *.md notes/` gratis nu när globbing finns.\n\nTäckt av 6 VFS-tester (`vfs.test.ts`) + 6 kommandotester (`commands.test.ts`):\nfil-kopia + oberoende, into-dir, `-r`-krav + djupklon, self-copy-vägran,\nshare-strip, flera-källor + icke-mapp-fel. 266 tester gröna; typecheck + build\ngröna. (Autosuggestion-testet uppdaterat: `cp` är en ny `c`-completion.)\n\n_Övrig cp-yta (`-i` interaktiv, `-p` bevara, `-n` no-clobber) är kvar om behovet\ndyker upp._",
      "sourcePath": "roadmap/fs-cp.md",
      "sourceUrl": "https://github.com/tor2dbear/pia-terminal/blob/main/roadmap/fs-cp.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "pia-terminal/date-command",
      "repo": "tor2dbear/pia-terminal",
      "repoName": "PIA",
      "repoColor": "#38bdf8",
      "slug": "date-command",
      "title": "date — visa klocka och datum",
      "status": "done",
      "tags": [
        "system",
        "commands"
      ],
      "updated": "2026-07-17",
      "issue": null,
      "order": null,
      "body": "## Mål\n`date` skriver nuvarande datum/tid. Förvånande att sakna för nåt som visar ett\n\"system\" (jfr `neofetch`, `whoami`).\n\n## Research\n- **Var det hör hemma:** `src/commands/system.ts`, bredvid `whoami`/`neofetch`.\n- Trivialt i sak, men **testbarhet:** kommandot får inte läsa `Date.now()` rakt av\n  om vi vill ha deterministiska tester (jfr hur rng injiceras i `snake`). Överväg\n  en injicerbar klocka via `CommandContext` eller en liten wrapper, så testet kan\n  frysa tiden.\n- Idiom: default är systemets lokala tid i ett standardformat. `date -u` för UTC.\n  Formatsträngar (`date +%Y-%m-%d`) är idiom-troget men kan komma senare.\n\n## Öppna frågor\n- Injicerad klocka eller acceptera icke-deterministiskt test (assert:a bara\n  formatet, inte värdet)? Lutar åt formatassertion — enklast, inget nytt seam.\n- Hur mycket format-flaggor (`+FORMAT`) från start? Troligen ingen — bara\n  default + `-u`.\n\n## Levererat\n`date` i `system.ts`, bredvid `whoami`/`neofetch`. Beslut:\n- **Format:** GNU-likt `Fri Jul 18 09:18:03 UTC 2026`. `-u` (alias `--utc`) ger\n  UTC; annars lokal tid med en `UTC±h`-etikett (0 → `UTC`).\n- **Testbarhet:** valde **formatassertion** (ingen klock-injektion, inget nytt\n  seam på `CommandContext`) — testerna matchar mönstret, inte värdet. Enklast,\n  och `date` behöver inte determinism i produktion.\n\nTäckt av 2 tester (format-regex + att `-u` säger UTC). 291 tester gröna;\ntypecheck + build gröna.\n\n_`+FORMAT`-strängar och lokala TZ-förkortningar kvar om behovet dyker upp._",
      "sourcePath": "roadmap/date-command.md",
      "sourceUrl": "https://github.com/tor2dbear/pia-terminal/blob/main/roadmap/date-command.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "pia-terminal/shell-globbing",
      "repo": "tor2dbear/pia-terminal",
      "repoName": "PIA",
      "repoColor": "#38bdf8",
      "slug": "shell-globbing",
      "title": "Globbing — * och ? på shell-nivå",
      "status": "done",
      "tags": [
        "shell",
        "terminal"
      ],
      "updated": "2026-07-17",
      "issue": null,
      "order": null,
      "body": "## Mål\nLåt `*` och `?` expanderas mot VFS:en *innan* ett kommando körs, så\n`cat *.md`, `ls src/*`, `rm *.txt` funkar. Idag är de vanliga tecken — den\nenskilt största shell-luckan, och den bryter \"riktig terminal\"-känslan mest.\n\n## Research\n- **Var det saknas:** `tokenize`/`lex` i `src/terminal/parse.ts` behandlar `*`/`?`\n  som vanlig text; ingen expansions-pass finns. Globbing existerar bara *inuti*\n  `find -name` och som regex i `grep` — via `globToRegExp` i\n  `src/commands/text.ts`, som kan återanvändas.\n- **Var det hör hemma:** en expansions-pass per pipeline-stage, efter tokenisering\n  men före kommandot körs — expandera varje token som innehåller `*`/`?` mot\n  `cwd`/VFS, sortera träffarna. Rider på `CommandContext`-sömmen; kommandon\n  behöver inte veta något.\n- **Idiom-detaljer att härma:** ingen träff → token lämnas orörd (bash default,\n  `nullglob` av). Dolda filer (punkt-prefix) matchas *inte* av ett inledande `*`\n  (som riktiga skal). `?` = exakt ett tecken.\n- Citat stänger av globbing: `\"*.md\"` ska förbli literalt (tokenizern spårar redan\n  citat).\n\n## Öppna frågor\n- Expandera i alla stages eller bara mot filargument? Alla stages, som bash.\n- Rekursiv `**`? Hoppa först — vanlig `*` täcker nästan allt behov.\n- Matchning mot `/`: ett `*` ska inte korsa mappgränser (`src/*` ≠ `src/**`).\n  Spika mot bash-beteende innan bygge.\n\n## Levererat\nNy ren modul `src/terminal/glob.ts` (`expandArg`/`expandArgs` + en liten\n`GlobFs`-söm, testbar utan VFS). `executePipeline` i `terminal.ts` expanderar\nargs *efter* alias-expansion, som ett riktigt skal. Så här landade frågorna:\n- **Citat stänger av globbing:** löst i `parse.ts` — en `*`/`?` som skrivs *inom*\n  citat byts mot en sentinel (privat unicode-tecken) i tokenizern, så `\"*.md\"`\n  når kommandot literalt. Sentineln städas bort (`unescapeWild`) innan args\n  lämnas vidare, så inget kommando ser den. Normala tokens är byte-identiska —\n  alla gamla parse-tester gröna orörda.\n- **nullglob av:** ingen träff → token lämnas orörd (`*.zip` → `*.zip`).\n- **Dolda filer:** ett ledande `*`/`?` matchar inte punkt-filer; ett *literalt*\n  ledande `.` (som `.*`) gör det.\n- **`?`** = exakt ett tecken. Träffar sorteras.\n- **Alla stages** expanderas (inte bara filargument), som bash. Det ger rätt\n  idiom-beteende även på gotchas: `find . -name *.md` *osciterat* expanderar och\n  bråkar (precis som riktiga skal) — citera för att skydda mönstret.\n- **v1-avgränsning:** bara *sista* path-segmentet globbar (`*.md`, `src/*.ts`).\n  Wildcard i ett tidigare segment (`*/x.md`) lämnas literalt — ingen\n  mappöverskridande `*` eller `**` än. Redirect-mål (`> fil`) expanderas inte,\n  bara av-sentinelas.\n\nTäckt av 12 enhetstester (`glob.test.ts`) + 4 integrationstester genom hela\nterminalen (`terminal.test.ts`): expansion, citat-skydd, no-match-literal, och\natt expanderade filer faktiskt matas till kommandot. 252 tester gröna;\ntypecheck + build gröna; boot verifierad i headless Chromium utan runtime-fel.\n\n_`**`, mappöverskridande wildcards och redirect-mål-globbing är kvar som möjlig\nuppföljning om behovet dyker upp._",
      "sourcePath": "roadmap/shell-globbing.md",
      "sourceUrl": "https://github.com/tor2dbear/pia-terminal/blob/main/roadmap/shell-globbing.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "pia-terminal/head-tail",
      "repo": "tor2dbear/pia-terminal",
      "repoName": "PIA",
      "repoColor": "#38bdf8",
      "slug": "head-tail",
      "title": "head / tail — se början respektive slutet av en fil",
      "status": "done",
      "tags": [
        "text",
        "commands"
      ],
      "updated": "2026-07-17",
      "issue": null,
      "order": null,
      "body": "## Mål\n`head [-n <k>] [fil...]` och `tail [-n <k>] [fil...]` visar de första/sista `k`\nraderna (default 10). Idag finns bara `cat` (allt) — ingen väg att titta på en\ndel.\n\n## Research\n- **Var det hör hemma:** `src/commands/text.ts`, samma mönster som `grep`/`wc` —\n  återanvänd `sourcesOf(ctx, files)` som redan läser filargument *eller* piped\n  stdin, så de komponerar: `cat stor.log | tail -n5`.\n- Flera filer → prefixa med `==> namn <==`-rubrik mellan filer, som riktiga\n  head/tail. Med en enda källa: ingen rubrik.\n- `-n <k>` i både `-n5` och `-n 5`-form (samma parser-behov som `grep`s context —\n  kan spegla `parseGrepArgs`-mönstret).\n\n## Öppna frågor\n- `tail -f` (follow) — meningslöst i en statisk VFS utan strömmande filer. Hoppa;\n  ev. senare mot en live-loggkälla.\n- `head -c <bytes>` (tecken istället för rader)? Lägg till om det känns tomt utan;\n  börja med `-n`.\n- Negativt/`+`-prefix (`tail -n +2` = från rad 2)? Idiom-troget men nisch — hoppa\n  först.\n\n## Levererat\n`head` och `tail` i `text.ts`, byggda ihop kring en delad `headTail`-hjälpare +\n`parseCount`. Så här landade det:\n- **Källor:** återanvänder `sourcesOf` — läser filargument *eller* piped stdin, så\n  `cat stor.log | tail -n5` komponerar.\n- **Antal:** `-n <k>`, `-n<k>` *och* `-<k>` (GNU-shorthand), default 10. Ogiltigt\n  antal ger ett rent fel.\n- **Radräkning:** en delad `toLines` släpper den tomma rad ett avslutande\n  radbryt lämnar, så `tail -n2` på `\"a\\nb\\nc\\n\"` ger `b,c` (inte en blankrad).\n- **Flera filer:** `==> namn <==`-rubriker med blankrad emellan, som riktiga\n  head/tail; en enda källa → ingen rubrik.\n\nTäckt av 7 kommandotester (default 10, `-n<k>`, `-<k>`, tail + trailing newline,\nnamngiven fil, multi-fil-rubriker, ogiltigt antal). 277 tester gröna; typecheck +\nbuild gröna.\n\n_`tail -f`, `head -c` och `+N`-offset är kvar som möjlig uppföljning._",
      "sourcePath": "roadmap/head-tail.md",
      "sourceUrl": "https://github.com/tor2dbear/pia-terminal/blob/main/roadmap/head-tail.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "pia-terminal/history-command",
      "repo": "tor2dbear/pia-terminal",
      "repoName": "PIA",
      "repoColor": "#38bdf8",
      "slug": "history-command",
      "title": "history — lista och återanvänd tidigare kommandon",
      "status": "done",
      "tags": [
        "shell",
        "terminal"
      ],
      "updated": "2026-07-17",
      "issue": null,
      "order": null,
      "body": "## Mål\nEtt `history`-kommando som listar tidigare rader, och ev. history-expansion\n(`!!` = förra, `!$` = sista argumentet, `!n` = rad n). History *finns* internt\nmen går inte att lista eller återanvända från kommandoraden.\n\n## Research\n- **Var det bor:** history lever i terminal-kärnan (`src/terminal/terminal.ts`,\n  pil-upp/ner). Ett `history`-kommando måste *nå* den listan — men kommandon når\n  världen bara via `CommandContext`. Kräver alltså en ny hook (t.ex.\n  `ctx.history()`), i samma anda som `applyConfig`/`reloadFs`.\n- **`!!`/`!$`/`!n`** är däremot inte ett kommando utan en *expansions-pass* i\n  kärnan, före parsning — mer som globbing. Kan byggas separat från\n  `history`-listningen; listningen är den enkla halvan.\n- Persistens: history är idag bara in-session (försvinner vid reload). En äkta\n  `history` vill kanske spara till `~/.pia/`-dotfilen. Separat beslut.\n- Idiom: `history` listar numrerat; `history -c` rensar.\n\n## Öppna frågor\n- Bara listning först (enkelt, en `ctx.history()`-hook), och skjut `!!`-expansion\n  till en egen omgång? Troligen ja — dela pucken om `!`-expansion växer.\n- Persistera history mellan sessioner (mot dotfilen), eller hålla in-session? Väg\n  mot cloud-sync-frågorna.\n\n## Levererat\nListnings-halvan (som planerat — `!`-expansion skjuts upp).\n- **Söm:** två nya optionella hooks på `CommandContext` — `history()` (kopia av\n  terminalens history) och `clearHistory()` — i samma anda som\n  `applyConfig`/`reloadFs`. Terminalen kopplar in dem i `context()`.\n- **Kommando:** `history` i `system.ts` listar numrerat (bredd-justerat);\n  `history -c` rensar. Komponerar i pipes (`history | grep git`).\n\nTäckt av 2 kommandotester (numrerad listning, `-c` via hook) + 2 end-to-end\n(listar körda kommandon, `-c` tömmer). 317 tester gröna; typecheck + build gröna.\n\n_`!!`/`!$`/`!n`-expansion (en kärn-expansions-pass, som globbing) och persistens\nmot `~/.pia/` kvar som egna uppföljningar._",
      "sourcePath": "roadmap/history-command.md",
      "sourceUrl": "https://github.com/tor2dbear/pia-terminal/blob/main/roadmap/history-command.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "pia-terminal/command-chaining",
      "repo": "tor2dbear/pia-terminal",
      "repoName": "PIA",
      "repoColor": "#38bdf8",
      "slug": "command-chaining",
      "title": "Kommando-kedjning — && || ;",
      "status": "done",
      "tags": [
        "shell",
        "terminal"
      ],
      "updated": "2026-07-17",
      "issue": null,
      "order": null,
      "body": "## Mål\nKedja kommandon: `;` (kör i följd), `&&` (kör nästa bara om föregående lyckades),\n`||` (kör nästa bara om föregående misslyckades). Förväntas för varje\nflerstegsflöde.\n\n## Research\n- **Var det saknas:** `parsePipeline` i `src/terminal/parse.ts` känner bara `|`,\n  `>`, `>>`. `;`/`&&`/`||` behandlas som vanlig argumenttext.\n- **Den verkliga designfrågan — exit-status.** `&&`/`||` kräver att ett kommando\n  har *lyckats eller misslyckats*. Idag returnerar `run()` `void`; det finns inget\n  status-begrepp. Behöver antingen (a) ett returvärde/`ctx.exit(code)`, eller (b)\n  att `ctx.error()` sätter en implicit fail-flagga för staget. Detta är kärnan i\n  pucken — spika den innan syntaxen.\n- Parsning: en nivå ovanför pipelines — en lista av pipelines separerade av\n  `;`/`&&`/`||`, utvärderad vänster→höger med kortslutning.\n- Idiom: `;` alltid; `&&`/`||` på status. Precedens som bash (alla tre samma\n  nivå, vänsterassociativa räcker för en enkel terminal).\n\n## Öppna frågor\n- Hur ska exit-status modelleras utan att röra alla befintliga kommandon? Lutar åt\n  en implicit \"skrevs ett fel?\"-flagga per stage, så inget kommando behöver\n  ändras — men vissa kommandon \"misslyckas\" utan att skriva fel. Väg alternativen.\n- Interaktion med redirect (`a && b > fil`) — bind redirect till rätt pipeline.\n- Behövs subshells/`( )`? Nej, långt bortom grunderna.\n\n## Levererat\nSpikade **beslut (b): `ctx.error()` sätter en fail-flagga** — inget kommando behöver\nändras, och det stämmer för nästan alla fall (terminalen dirigerar redan alla fel\ngenom `error()`).\n- **Parsning:** ny `parseSequence` i `parse.ts` ovanpå `parsePipeline` — splittar\n  på topp-nivå `;`/`&&`/`||` (citat-medvetet; ett ensamt `|` stannar i sin\n  pipeline), och varje segment parsas som förut. Trailing `;` tillåtet; ledande\n  operator / tom operand = syntaxfel.\n- **Exekvering:** `executePipeline` returnerar nu `boolean` (success = inget fel\n  skrevs). `submit` kör sekvensen vänster→höger med kortslutning: `&&` hoppar vid\n  föregående *fail*, `||` vid *success*, `;` alltid. Ett hoppat steg lämnar\n  status orörd, som ett riktigt skal.\n- **Söm:** `context()` tar ett delat `status`-objekt; `error()` sätter\n  `failed = true`. `busy`/input-synlighet flyttades upp till `submit` så det inte\n  flimrar mellan pipelines. Skärm-appar (`nano foo && echo klart`) väntar korrekt\n  via `runApp`.\n\nTäckt av 7 parse-tester (split/connectorer/citat/trailing-`;`/fel) + 5 end-to-end\n(`;`, `&&` success/fail, `||` fail/success). 312 tester gröna; typecheck + build\ngröna; boot verifierad i headless Chromium.\n\n_`$?`, subshells `( )` och `&` (bakgrund) kvar som möjlig uppföljning — men den\nimplicita fail-flaggan räcker för kedjning._",
      "sourcePath": "roadmap/command-chaining.md",
      "sourceUrl": "https://github.com/tor2dbear/pia-terminal/blob/main/roadmap/command-chaining.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "pia-terminal/pager-less",
      "repo": "tor2dbear/pia-terminal",
      "repoName": "PIA",
      "repoColor": "#38bdf8",
      "slug": "pager-less",
      "title": "less / more — bläddra i lång output",
      "status": "done",
      "tags": [
        "terminal",
        "commands"
      ],
      "updated": "2026-07-17",
      "issue": null,
      "order": null,
      "body": "## Mål\nEn pager: `less <fil>` (eller `... | less`) låter dig bläddra i innehåll istället\nför att det flödar förbi. Idag floodar lång output skärmen.\n\n## Research\n- **Passar screen-app-hosten perfekt.** En pager är precis en `ScreenApp` (som\n  `nano`/`snake`): ta över skärmen, visa en sida, bläddra med piltangenter/\n  space/`q` för att avsluta. Ingen ny arkitektur — `ctx.runApp()` finns.\n- Läser fil *eller* piped stdin. Piped in i en pager kräver att stdin når appen\n  (kontrollera hur `piped`/`stdin` levereras till en screen-app).\n- Idiom: `less` (bakåt+framåt, `/`-sök, `q`), `more` (bara framåt) som enklare\n  alias/variant. Mobil: hosten har redan tangentbar — space/pil/`q` som knappar.\n- Naturlig koppling: `man`-sidor (om de nånsin byggs) skulle visas *genom* pagern.\n\n## Öppna frågor\n- Hur mycket av `less`? Börja med sida upp/ner + `q`; `/`-sök och `g`/`G` sen.\n- Ska långa kommando-outputs *automatiskt* gå genom en pager, eller bara explicit\n  `less`? Explicit först — auto-paging är en större beteendeändring.\n- stdin → screen-app: verifiera att pipe-innehåll når appen innan vi lovar\n  `... | less`.\n\n## Levererat\nNy `Pager`-`ScreenApp` (`src/apps/pager.ts`) + `less`-kommando (alias `more`).\n- **Bläddring:** Space/`f`/PageDown en sida ner, `b`/PageUp upp, ↑↓/`j``k` en\n  rad, `g`/`G` till start/slut, `q`/Esc avslut. Skrivbara tangenter via `onText`,\n  special via `onKey` — varje tangent hanteras på *ett* ställe (verifierade\n  terminalens dispatch).\n- **Modell testbar utan DOM:** windowed render (`top`/`rows`), `render()` guardad\n  när ej mountad, `snapshot()`/`visible()` för test — som `snake`. Sidstorlek\n  default 20, mäts från viewporten i browsern (`fitRows`, faller tillbaka\n  headless).\n- **stdin:** läser fil-arg *eller* `ctx.stdin`, så `cat stor.log | less`\n  komponerar (less är sista steget → ej capturead → `runApp` funkar). Capturead\n  (`less foo | grep`) → passerar innehållet rakt igenom istället.\n- **Chrome:** återanvänder editorns `.ed-*`-layout (title/scroll-body/status),\n  ingen ny CSS.\n\nTäckt av 7 app-tester (sidor/rader/ändar/avslut/render) + 2 kommandotester\n(saknad fil, piped pass-through). 300 tester gröna; typecheck + build gröna; boot\nverifierad i headless Chromium.\n\n_`/`-sök, auto-paging av lång output, och `man` ovanpå pagern kvar som uppföljning._",
      "sourcePath": "roadmap/pager-less.md",
      "sourceUrl": "https://github.com/tor2dbear/pia-terminal/blob/main/roadmap/pager-less.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "pia-terminal/multiplayer-rooms",
      "repo": "tor2dbear/pia-terminal",
      "repoName": "PIA",
      "repoColor": "#38bdf8",
      "slug": "multiplayer-rooms",
      "title": "Multiplayer — who, msg, gemensamma rum",
      "status": "later",
      "tags": [
        "multiplayer",
        "supabase"
      ],
      "updated": "2026-07-17",
      "issue": null,
      "order": null,
      "body": "## Mål\nFlera inloggade i samma värld samtidigt: `who` (vilka är online), `msg <user>`\n(skicka meddelande), och gemensamma rum — en BBS-vibe.\n\n## Research\n- Realtime-sömmen finns redan: shared lists live-syncar via Supabase Realtime.\n  Presence (`who`) och meddelanden rider på samma infra — troligen en ny\n  `ShareStore`-liknande söm eller en presence-kanal.\n- On-brand nostalgi: BBS/`talk`/`wall` är äkta Unix- och tidig-nät-idiom. `who`,\n  `msg` (jfr `write`/`talk`), rum (jfr IRC/`wall`) är riktiga namn — passar\n  \"terminal-idiom först\".\n- Komplexitet högre än en enskild app: presence-state, moderering, spam,\n  identitet. Börja smått (`who` + `msg`) innan rum.\n\n## Öppna frågor\n- Idiom: `msg` vs riktiga `write`/`talk`? Rum vs `wall`/IRC-stuk? Spika namnen\n  mot Unix innan bygge.\n- Presence-modell: räcker Supabase Realtime presence, eller behövs en tabell?\n- Värt komplexiteten jämfört med fler screen-appar? Håll i `later` tills en\n  verklig lust finns — det är ett stort åtagande för en enmansterminal.\n\n_Ligger i `later`. Befordra när lusten och en avgränsad start finns._",
      "sourcePath": "roadmap/multiplayer-rooms.md",
      "sourceUrl": "https://github.com/tor2dbear/pia-terminal/blob/main/roadmap/multiplayer-rooms.md",
      "adapter": "pucks",
      "native": true
    },
    {
      "id": "pia-terminal/text-coreutils",
      "repo": "tor2dbear/pia-terminal",
      "repoName": "PIA",
      "repoColor": "#38bdf8",
      "slug": "text-coreutils",
      "title": "sort / uniq / cut — pipe-kompanjonerna",
      "status": "done",
      "tags": [
        "text",
        "commands"
      ],
      "updated": "2026-07-17",
      "issue": null,
      "order": null,
      "body": "## Mål\nDe klassiska text-verktygen som gör pipes kraftfulla: `sort`, `uniq`, `cut`. De\nkompletterar `grep`/`wc` ni redan har — utan dem är pipelines svaga. Grupperad\npuck (2B-beslut); dela vid bygge om nån växer.\n\n## Research\n- **Var det hör hemma:** `src/commands/text.ts`, samma `sourcesOf`-mönster (fil\n  eller stdin), så de komponerar: `cat data.csv | cut -d, -f1 | sort | uniq -c`.\n- **`sort`** — rader lexikaliskt; `-r` omvänt, `-n` numeriskt, `-u` unikt. `-n`\n  måste hantera icke-numeriska rader (bash sorterar dem som 0/först — spika mot\n  GNU).\n- **`uniq`** — kollapsar *intilliggande* dubbletter (kräver sorterad input, som\n  riktiga uniq — dokumentera det, \"gör inte magi\"). `-c` prefixar antal, `-d`\n  bara dubbletter.\n- **`cut`** — `-d <delim>` + `-f <fält>` (t.ex. `-f1,3` eller `-f2-`). `-c` för\n  teckenintervall. Default-delim är TAB, som riktiga cut.\n\n## Öppna frågor\n- `sort -k` (sortera på kolumn) — nisch, hoppa först.\n- `cut`s fält-syntax (`1,3`, `2-`, `-4`) — hur mycket täcker vi? Börja med enkla\n  listor och `n-`-intervall.\n- Ska `uniq` varna/tolerera osorterad input, eller tyst följa GNU (bara\n  intilliggande)? Följ GNU, men överväg en not i `help`.\n\n## Levererat\nAlla tre i `text.ts`, kring en delad `allLines`-hjälpare (läser filer *eller*\nstdin via `sourcesOf`, plus `toLines`), så de komponerar:\n`cat data.csv | cut -d, -f1 | sort | uniq -c`.\n- **`sort`** — lexikalisk default; `-r` omvänt, `-n` numeriskt (jämför på\n  ledande tal), `-u` unikt (efter sortering).\n- **`uniq`** — kollapsar *intilliggande* dubbletter (kräver sorterad input, som\n  GNU — noterat i `help`); `-c` prefixar antal (samma bredd som `wc`), `-d` bara\n  dubbletter.\n- **`cut`** — `-f <lista>` med `-d <delim>` (default TAB), eller `-c <lista>`.\n  Listor: `1`, `1,3`, `2-`, `-4`, `2-5`. Emitterar alltid i *input-ordning*\n  (`-f3,1` → fält 1 sen 3), och en rad utan delimiter skickas igenom hel (GNU\n  utan `-s`).\n\nTäckt av 10 kommandotester (sort lexikal/`-r`/`-n`/`-u`; uniq/`-c`/`-d`; cut\nfält-ordning/öppna intervall/`-c`/ingen-delim/fel utan lista). 287 tester gröna;\ntypecheck + build gröna. (Autosuggestion-testet uppdaterat: `cut` är en ny\n`c`-completion.)\n\n_`sort -k`, `cut -s`, och teckenkodnings-nyanser kvar om behovet dyker upp._",
      "sourcePath": "roadmap/text-coreutils.md",
      "sourceUrl": "https://github.com/tor2dbear/pia-terminal/blob/main/roadmap/text-coreutils.md",
      "adapter": "pucks",
      "native": true
    }
  ]
}
