Files
arboretum/CLAUDE.md
Johan LEROY c224abe108 P3-C: dashboard worktree-first + acceptance P3 (MVP complet)
Vue racine consolidée : repos → worktrees avec état git ET état de session
corrélé. Complète le MVP (P2 découverte/reprise + P3-A worktrees + P3-B états fins).

- web/lib/ws-client: abonnement multi-topics (sessions + worktrees), subscribeWorktrees.
- web/stores/worktrees: repos + worktrees, CRUD, temps réel (repo_update/worktree_*).
- web/views/DashboardView (route racine /), components RepoSection + WorktreeCard ;
  SessionStateBadge réutilisé pour l'état des sessions corrélées.
- router: / = dashboard, /sessions = liste à plat (sessions hors worktree), nav croisée.
- i18n EN/FR (dashboard/repos/worktrees).
- fix: @xterm/headless est CommonJS → chargé via createRequire (l'import nommé ESM
  échouait sous Node natif, masqué par esbuild en test) ; détecté par l'acceptation.
- scripts/acceptance-p3.mjs: repo git tmp → enregistrement, worktree + hook, broadcast
  WS worktree_update, corrélation session bash, suppression (409 sans force, 200 avec).

Vérifs : typecheck, 159 tests, build (vue-tsc), acceptations P1/P2/P3 ALL GREEN.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 18:46:45 +02:00

7.8 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. Pré-MVP. L'étape P1 (colonne vertébrale : monorepo, protocole WS, daemon, front, tests, CI) est terminée ; 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)

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 (bin arboretum). Embarque la SPA buildée dans public/ via le hook prepack (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.1 par défaut ; config.ts refuse tout bind non-loopback sans --i-know-this-exposes-a-terminal. Accès distant recommandé via Tailscale Serve.
  • Un hook preValidation global 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 cookie SameSite=Strict ne couvre pas les upgrades). Les routes statiques sont publiques ; les routes publiques explicites portent config: { 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épassent HIGH ; les observers ne freinent jamais le flux. Un client trop en retard (LAGGING_BYTES) est coupé puis resynchronisé.
  • Handshake hello/hello_ok négocie PROTOCOL_VERSION. parseClientMessage valide/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 /ws est enregistrée après le plugin @fastify/websocket dans un contexte encapsulé (app.register(async (scoped) => ...)) ; sinon le handler reçoit la signature REST (request, reply) au lieu de (socket, req).
  • notFoundHandler renvoie l'index.html de 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 :

  • --resume doit 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 futur claude-adapter (P3) nécessite une lecture d'écran via @xterm/headless (le strip ANSI naïf mange les espaces) + le registre ~/.claude/sessions.

Les états de session du protocole sont aujourd'hui un sous-ensemble P1 (starting/running/exited) à étendre en P3.