- PWA: public/manifest.webmanifest + icon.svg + meta index.html (theme-color, apple-touch-icon) - public/sw.js: service worker push-only (push → showNotification tag=sessionId ; notificationclick → focus/ouvre /sessions/<id>) — pas de précache (hors périmètre) - lib/push.ts + stores/push.ts: enable/disable, abonnement VAPID, enregistrement SW au boot - DashboardView: toggle « Notifications » (gardé par pushSupported) + i18n en/fr - campagne de fiabilité (verdict S3): classification de tous les types de dialogue sur captures réelles (trust/permission×2/question) + refus Esc (deny-esc2) + plan (synthétique) - acceptance-p4.mjs: VAPID/auth/Origin, subscribe idempotent/unsubscribe, answer (rejets) Choix: SW écrit à la main plutôt que vite-plugin-pwa (qui tirait ~295 paquets Workbox + 2 vulns esbuild high pour un SW push-only). Zéro dépendance front nouvelle. 175 tests verts, acceptance p1..p4 ALL GREEN. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
8.4 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Projet
Arboretum : un daemon Node.js unique (npx git-arboretum) qui sert un dashboard web pour piloter les worktrees git et les sessions Claude Code qui tournent dessus, depuis n'importe quel appareil. MVP atteint : P1 (colonne vertébrale : monorepo, protocole WS, daemon, front, tests, CI), P2 (découverte & reprise des sessions), P3 (worktrees multi-repo, états fins busy/waiting/idle via claude-adapter, dashboard worktree-first) sont livrés. La phase en cours est P4 (« supervision mobile & push » : répondre aux dialogues sans ouvrir le terminal, notifications Web Push, PWA installable). Voir les VERDICT.md dans spikes/ pour les décisions techniques actées avant implémentation.
Les commentaires du code et la doc interne sont en français ; le README et les messages utilisateur en anglais. Conserver cette convention.
Commandes
npm run build # build shared → server → web (l'ordre compte, voir ci-dessous)
npm run typecheck # tsc -b shared + server UNIQUEMENT (le web se typecheck via vue-tsc dans son build)
npm test # vitest run sur packages/*/test/**/*.test.ts
npx vitest run packages/server/test/ring-buffer.test.ts # un seul fichier
npx vitest run -t "flow control" # par nom de test
npm run dev:server # daemon en watch (tsc -b --watch + node --watch)
npm run dev:web # Vite ; proxifie /api et /ws vers le daemon sur :7317 (cf. note Origin)
npm run pack # build + npm pack du tarball git-arboretum
node packages/server/scripts/acceptance-p1.mjs # acceptation E2E P1 (daemon réel + client WS réel)
node packages/server/scripts/acceptance-p2.mjs # acceptation E2E P2 (découverte/reprise : faux ~/.claude + faux binaire claude)
node packages/server/scripts/acceptance-p3.mjs # acceptation E2E P3 (worktrees : repo git tmp + hook + corrélation session)
node packages/server/scripts/acceptance-p4.mjs # acceptation E2E P4 (push : VAPID/subscribe + commande WS answer)
L'acceptation exige npm run build au préalable (elle lance dist/index.js) et utilise la commande bash plutôt que claude pour ne pas consommer de quota.
Node >= 22.16 est requis (pas seulement recommandé) : la persistance utilise node:sqlite (DatabaseSync), natif et stable seulement à partir de cette version. .nvmrc = 22.
Monorepo (npm workspaces)
packages/shared(@arboretum/shared) — source unique de vérité du protocole WS (protocol.ts) et des types REST (api.ts), importée par le serveur ET le front. C'est une dépendance runtime non publiée : la CI pack les deux tarballs (@arboretum/shared+git-arboretum) et les installe ensemble.packages/server(git-arboretum) — le daemon Fastify, le seul paquet publié sur npm (binarboretum). Embarque la SPA buildée danspublic/via le hookprepack(scripts/copy-web.mjs).packages/web(@arboretum/web) — SPA Vue 3 (Pinia, vue-router, xterm.js, Tailwind 4), privée.
Ordre de build imposé : shared se construit en premier (projets TS composite avec references) ; le front doit être buildé avant prepack du serveur pour être embarqué.
Architecture serveur
buildApp() (packages/server/src/app.ts) câble tout : AuthService, LoginRateLimiter, PtyManager, les routes REST, la gateway WS, et le service statique de la SPA. index.ts est le bin (parse config, ouvre la db, écoute, gère le bootstrap token et le drain au SIGTERM/SIGINT).
Modèle de sécurité (central — ne pas affaiblir)
Un terminal web est de l'exécution de code à distance par conception. Les garde-fous sont structurants :
- Bind
127.0.0.1par défaut ;config.tsrefuse tout bind non-loopback sans--i-know-this-exposes-a-terminal. Accès distant recommandé via Tailscale Serve. - Un hook
preValidationglobal authentifie toute requête/api/**et/ws, et applique un check Origin strict quand l'en-tête est présent (anti cross-site WS hijacking — le cookieSameSite=Strictne couvre pas les upgrades). Les routes statiques sont publiques ; les routes publiques explicites portentconfig: { public: true }(ex. login). - Tokens stockés hashés (sha256), comparaison en temps constant. Le bootstrap token n'est affiché qu'une fois au premier démarrage. Le cookie de session est un payload signé HMAC. Le rate-limit du login est global (pas par IP : derrière Tailscale tout arrive de 127.0.0.1) avec backoff exponentiel.
Protocole WebSocket (packages/shared/src/protocol.ts)
Une seule connexion multiplexée par client, plusieurs canaux (channels) :
- Contrôle = frames texte JSON ; sortie terminal = frames binaires
[type u8][channel u32le][payload]. Un chunk PTY peut couper un caractère UTF-8 en frontière de frame → le décodage est délégué àxterm.write(Uint8Array), jamais au transport. - Flow control par watermarks (
FLOW). Invariant anti-deadlock à préserver absolument :ACK_EVERY_BYTES <= LOW_WATERMARK. Le PTY n'est mis en pause que quand tous les clients interactifs dépassentHIGH; les observers ne freinent jamais le flux. Un client trop en retard (LAGGING_BYTES) est coupé puis resynchronisé. - Handshake
hello/hello_oknégociePROTOCOL_VERSION.parseClientMessagevalide/sanitise tout message entrant (bornes sur dims, longueurs) — toute nouvelle commande client doit y être validée.
PtyManager (packages/server/src/core/pty-manager.ts)
Lance claude (ou bash) dans node-pty. Sessions vivantes en mémoire (Map) ; l'historique est persisté dans la table sessions (sqlite). Chaque session a un RingBuffer (2 Mo) ; à l'attach, le client reçoit un resync = reset terminal + replay de la queue du ring (REPLAY_TAIL_BYTES). Outil mono-utilisateur : tout client interactif peut écrire, les observers sont read-only. kill envoie SIGTERM puis SIGKILL après un délai de grâce.
claude-launcher (packages/server/src/core/claude-launcher.ts)
Couture volontairement abstraite : Arboretum enveloppe le CLI claude interactif dans un PTY (pas l'Agent SDK, pas le mode headless). La commande bash sert aux tests sans quota. C'est ici que se brancherait un éventuel plan B.
Détails fastify à connaître
- La route
/wsest enregistrée après le plugin@fastify/websocketdans un contexte encapsulé (app.register(async (scoped) => ...)) ; sinon le handler reçoit la signature REST(request, reply)au lieu de(socket, req). notFoundHandlerrenvoie l'index.htmlde la SPA pour tout ce qui n'est pas/api/ou/ws(routing client-side).
Base de données (packages/server/src/db/index.ts)
node:sqlite en mode WAL. Migrations idempotentes versionnées dans le tableau MIGRATIONS + table schema_migrations. Tables : settings, auth_tokens, sessions.
Architecture web
SPA Vue 3. Un singleton wsClient (src/lib/ws-client.ts) gère l'unique connexion : handshake, reconnexion avec backoff, ré-attache des terminaux ouverts, corrélation FIFO des attached, et le flow control par ACK côté client (compteur d'octets réellement traités par xterm, remis à zéro à chaque resync, avec un ACK traînant débouncé anti-deadlock). En dev, vite.config.ts proxifie /api et /ws vers le daemon en réécrivant l'en-tête Origin vers celle du daemon (sinon le check Origin strict rejette l'origine de Vite). Le garde de routeur vérifie la session via GET /api/v1/auth/me.
Spikes & roadmap
Les spikes/sN-*/VERDICT.md documentent des décisions qui contraignent l'implémentation à venir, notamment :
--resumedoit toujours s'exécuter dans le cwd d'origine de la session (lu dans le JSONL).- La vivacité d'une session se déduit de
pid+procStart, jamais de la présence du fichier registre. - La distinction d'états fins (busy / waiting / idle) du
claude-adapter(P3-B) nécessite une lecture d'écran via@xterm/headless(le strip ANSI naïf mange les espaces) + le registre~/.claude/sessions.
Le SessionRuntimeStatus du protocole reste starting/running/exited ; les états fins P3-B (activity, waitingFor, dialog) sont des champs additifs optionnels de SessionSummary, remplis par le claude-adapter pour les sessions claude managées (null pour bash / découvertes). P4 ajoutera la commande WS answer (répondre aux dialogues) et le Web Push.