Files
arboretum/CLAUDE.md
Johan LEROY 92c48f0978 P2: découverte & reprise des sessions Claude
Arboretum découvre désormais toutes les sessions Claude de la machine
(scan ~/.claude/projects + registre ~/.claude/sessions), distingue
vivantes/mortes par pid+procStart, et permet de reprendre une morte
(--resume dans son cwd d'origine) ou forker une vivante sans la corrompre.

- shared: SessionSummary enrichi (source, claudeSessionId, pid, resumable,
  attachable, registryStatus) — additif, PROTOCOL_VERSION inchangé ;
  types REST resume/fork.
- db: migration id:2 (claude_session_id, resumed_from).
- core: jsonl-discovery (parseur tolérant, scan asynchrone non bloquant),
  session-registry (vivacité pid+procStart), discovery-service (cache +
  refresh périodique + diff/broadcast), pty-manager (resume/fork + capture
  du claudeSessionId via le registre).
- routes: /sessions/:id/resume (garde-fou 409 anti-corruption sur session
  vivante) et /fork ; GET fusionné managées + découvertes ; relais WS.
- web: badges managed/discovered + busy/idle/waiting, actions conditionnelles
  (Open/Observe/Kill vs Fork/View vs Resume/Fork), vue read-only des sessions
  externes, i18n EN/FR.
- tests: jsonl-discovery, session-registry, discovery-service + resume/fork
  (130 verts) ; acceptation E2E acceptance-p2.mjs (sans quota) ALL GREEN.

Conforme aux verdicts S1 (resume dans cwd d'origine, vivacité pid+procStart)
et S4 (munge cwd, parseur tête+queue, priorité de titre).
2026-06-12 18:20:21 +02:00

7.7 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)

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.