Compare commits

..
39 Commits
Author SHA1 Message Date
johanleroyandClaude Opus 4.8 c63bd02414 release: @johanleroy/git-arboretum 1.4.0
CI / Build & test (Node 24) (push) Has been cancelled
CI / Pack & boot smoke (Node 22) (push) Has been cancelled
CI / Build & test (Node 22) (push) Has been cancelled
Release / Publish to Gitea npm registry (push) Failing after 5m3s
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 17:03:09 +02:00
johanleroyandClaude Opus 4.8 342430c405 feat(web): lien « Buy me a coffee » (nav, réglages, funding)
- constante BUYMEACOFFEE_URL ; item de nav « Offrez-moi un café »
- section support dans les Réglages + i18n EN/FR
- champ `funding` dans le package.json publié ; mentions README EN/FR

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 17:02:30 +02:00
johanleroyandClaude Opus 4.8 c88ae9b2ae feat: clean install (Gitea en dur, scan off) + durcissement sécurité entreprise
Gitea
- lien « Code source » hardcodé (REPO_SOURCE_URL) vers le dépôt, toujours visible
- retrait complet du réglage configurable (api.ts, route+store settings, SettingsView, i18n, help, tests)

Découverte des dépôts
- aucune racine de scan par défaut → pas de scan au premier démarrage (clean install)
- corrige la découverte des dépôts à l'INTÉRIEUR d'une racine qui est elle-même un repo
  (depth 0 = conteneur de scan, on descend ; depth > 0 = feuille)

Sécurité « enterprise-deployable »
- en-têtes HTTP durcis (CSP, X-Frame-Options, nosniff, Referrer-Policy, HSTS conditionnel, no-store API), header Server retiré
- permissions DB 0o600 / dossier de données 0o700 ; error handler sanitisé ; garde Content-Type sur les mutations
- chiffrement au repos AES-256-GCM des secrets (server_secret, clé privée VAPID) via SecretBox
- journal d'audit (migration #7) + endpoint /audit-logs + RGPD export/effacement + UI Réglages
- SECURITY.md, docs/ENTERPRISE_DEPLOYMENT.md, sections README EN/FR, SBOM CycloneDX en CI

CI
- pack-smoke packe depuis le contexte du package (cd packages/server) au lieu de « npm pack -w » :
  corrige l'embarquement de @arboretum/shared dans le tarball

Purge des données personnelles du dépôt public
- suppression de spikes/s4-discovery/result.json, reformulation du VERDICT
- chemin de test générique, anonymisation des 5 fixtures de dialogues

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 16:44:25 +02:00
johanleroyandClaude Opus 4.8 cc055739f1 release: @johanleroy/git-arboretum 1.3.0
Release / Publish to Gitea npm registry (push) Failing after 4m59s
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 14:46:03 +02:00
johanleroyandClaude Opus 4.8 7b2fe45fa9 feat: découverte automatique des dépôts git (scan + montrer/cacher)
Scan borné du système de fichiers (racines configurables, défaut home ; profondeur/nombre/timeout bornés ; symlinks non suivis ; exclusions node_modules/dotdirs) qui auto-enregistre les nouveaux dépôts. Insertion atomique ON CONFLICT DO NOTHING (idempotence + anti-résurrection d'un dépôt masqué + anti-course). Scan au démarrage + bouton manuel + re-scan périodique (RepoDiscoveryService, démarré dans runDaemon). Colonne repos.hidden : masquer = conservé en DB mais exclu du dashboard et jamais ré-ajouté ; supprimer = re-découvrable. UI : bouton œil par dépôt + bascule afficher-les-masqués sur le dashboard, section Découverte dans les Réglages (racines + intervalle, allow-list stricte). Robustesse : listAllWorktrees tolère l'échec git par dépôt ; flag --no-discover (escape hatch + hermétisme des acceptations).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 14:45:12 +02:00
johanleroyandClaude Opus 4.8 09534b7c65 chore(deps): @fastify/static ^8 → ^9
Mise à niveau majeure du plugin de service statique (déjà installée dans l'arbre de travail, build + 253 tests verts).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 14:44:52 +02:00
johanleroyandClaude Opus 4.8 7ca93c554d feat: onglets Réglages & Aide + icône Gitea
Réglages : préférences (langue, notifications push + test), gestion complète des tokens d'accès (liste/création/révocation, garde anti lock-out sur le dernier token), URL Gitea configurable, infos serveur en lecture seule (port/bind/origines/VAPID + flags CLI). Aide : documentation bilingue EN/FR de toutes les fonctionnalités, avec recherche. Icône Gitea (lien externe) dans la nav (sidebar + MoreSheet mobile).

Backend : routes /api/v1/auth/tokens (GET/POST/DELETE) + tokenId dans /me ; routes/settings.ts (GET/PATCH, allow-list stricte gitea_url, aucun secret exposé, URL validée http/https anti-XSS) ; AuthService.listTokens/revokeToken (transaction). Front : NavItem gère les liens externes, nav primaire/secondaire, store settings, vues SettingsView/HelpView.

236 tests verts (+15 nouveaux : auth-tokens, settings-routes).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 14:16:53 +02:00
johanleroyandClaude Opus 4.8 77b1e55fc0 fix(ci): exécuter prepack explicitement avant npm pack (embarquement @arboretum/shared)
Le job pack-smoke échouait sur le runner Gitea : « npm pack -w » n'y déclenche
pas le hook prepack (contrairement à « npm publish -w », d'où la 1.2.0 publiée
intacte). vendor-shared ne matérialisait donc pas @arboretum/shared, et npm
retombait sur le symlink workspace (non bundlé) → tarball sans le runtime partagé.

On lance désormais prepack explicitement, on vérifie la matérialisation de
@arboretum/shared/dist/index.js, puis on packe en --ignore-scripts. On nettoie
aussi /tmp/tarballs (runner persistant). Validé en repro locale (npm 10 et 11).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 13:13:48 +02:00
johanleroyandClaude Opus 4.8 de0537b42e release: @johanleroy/git-arboretum 1.2.0
Release / Publish to Gitea npm registry (push) Failing after 5m1s
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 11:49:03 +02:00
johanleroyandClaude Opus 4.8 b80e68e539 feat: refonte UX du dashboard (coquille, tri/filtre/pagination, ⌘K, toasts)
Refonte front (packages/web) : coquille applicative partagée (sidebar desktop +
barre d'onglets mobile + PageHeader, langue/push/logout centralisés) pilotée par
route.meta.layout ; design system (BaseButton/BaseBadge/SegmentedControl/EmptyState/
SkeletonRow, anneau de focus, icônes @lucide/vue) ; tri/filtre/recherche/pagination
côté client via useListControls (+ synchro URL) sur sessions/worktrees/groupes ;
section « À traiter », palette de commandes ⌘K, toasts. Aucune modif serveur/protocole/DB.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 11:47:36 +02:00
johanleroyandClaude Opus 4.8 f015a105f9 release: @johanleroy/git-arboretum 1.1.0
Release / Publish to Gitea npm registry (push) Failing after 4m59s
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 10:03:30 +02:00
johanleroyandClaude Opus 4.8 f513cff253 feat: groupes de travail (P5)
Ajoute la notion de groupe — collection nommée de repos (membership légère
persistée, many-to-many) — pour piloter plusieurs repos en simultané :

- DB : migration #5 (tables groups + group_repos, FK ON DELETE CASCADE).
- GroupManager (synchrone, EventEmitter) + routes REST /api/v1/groups ;
  ne persiste que la membership, worktrees/sessions filtrés par repoId côté client.
- Protocole : GroupSummary, DTOs, topic WS « groups », events group_update/removed
  (additifs, PROTOCOL_VERSION inchangé).
- Web : store groups, GroupsListView, GroupView (réutilise RepoSection),
  grille multi-terminaux (TerminalGrid/TerminalCell), action « feature cross-repo »
  (orchestration client, tolérante aux échecs partiels), routes + i18n EN/FR.
- Tests : group-manager.test.ts + extension protocol.test.ts ; acceptance-p5.mjs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 10:03:19 +02:00
johanleroyandClaude Opus 4.8 4d842b8fba release: @johanleroy/git-arboretum 1.0.2
Release / Publish to Gitea npm registry (push) Failing after 4m57s
Cookie de session Secure conditionnel (x-forwarded-proto) + sous-commande
`arboretum install` (service systemd/launchd, --dry-run), routeur de sous-commandes
avec rétrocompat daemon stricte.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 16:23:28 +02:00
johanleroyandClaude Opus 4.8 2566196134 feat: cookie Secure conditionnel + sous-commande d'installation de service
Cookie: routes/auth.ts pose `secure` sur le cookie de session quand la requête
arrive en HTTPS (x-forwarded-proto), sans trustProxy — durcit le cookie derrière
Tailscale Serve sans casser le localhost http.

Install: nouveau cli/install.ts + routeur de sous-commandes dans index.ts
(install/uninstall/status/serve). Service utilisateur systemd (Linux) ou launchd
(macOS), bootstrap du token, --dry-run/--no-enable. Rétrocompat stricte du daemon
par défaut (runDaemon extrait).

Tests: app.e2e (cookie Secure local vs HTTPS) + cli-install (fonctions pures).
203/203 verts, acceptation P1/P4 vertes.

Docs: README.md + README.fr.md (installeur multi-OS, distinction utiliser/cloner,
modèle de sécurité durci).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 16:17:08 +02:00
johanleroyandClaude Opus 4.8 9820677c2a release: @johanleroy/git-arboretum 1.0.1
Release / Publish to Gitea npm registry (push) Failing after 5m0s
Republie pour rafraîchir le README embarqué (page du paquet) :
- doc d'install sans token (paquet public)
- README français + sélecteur de langue
- copy-meta réécrit le lien FR en absolu (page du paquet)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 14:32:28 +02:00
johanleroyandClaude Opus 4.8 63c460e91a docs: retire la ligne _authToken des instructions d'install
Le paquet est en lecture publique sur le registre Gitea (install anonyme
vérifiée) : seule la redirection de scope est nécessaire, pas de token.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 14:29:22 +02:00
johanleroyandClaude Opus 4.8 ed3b0bba7a docs: README en français (README.fr.md) + sélecteur de langue
- ajoute une traduction française complète du README
- lien de langue (English ⇄ Français) en tête des deux fichiers
- corrige une référence résiduelle `npx git-arboretum` → `@johanleroy/git-arboretum`

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 14:22:17 +02:00
johanleroyandClaude Opus 4.8 30cd3df0dd release: @johanleroy/git-arboretum 1.0.0 — publication sur le registre Gitea
Release / Publish to Gitea npm registry (push) Failing after 4m57s
- renomme le paquet en @johanleroy/git-arboretum (scope routé vers le registre npm Gitea privé)
- embarque @arboretum/shared via bundleDependencies (scripts/vendor-shared.mjs au prepack)
- joint README/LICENSE au tarball (scripts/copy-meta.mjs) + metadata, keywords, publishConfig
- CI pack-smoke en mono-tarball avec assertion bundleDep ; nouveau workflow release.yml (publish sur tag v*)
- version 1.0.0 ; README mis à jour (install scopé + service systemd)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 13:53:30 +02:00
johanleroyandClaude Opus 4.8 7de2da7469 feat(web): sélecteur de répertoire (parcourir au lieu de taper le chemin)
Ajoute un navigateur de dossiers réutilisable sur les deux champs qui
exigeaient un chemin absolu tapé à la main : « Répertoire de travail »
(nouvelle session) et « Chemin du dépôt » (ajout de repo). Un bouton
« Parcourir… » déplie un panneau inline ; le champ texte reste pour
taper/coller un chemin connu. Pour l'ajout de repo, les sous-dossiers
qui sont des dépôts git sont signalés par un badge.

Serveur : nouvel endpoint GET /api/v1/fs/list (authentifié par le hook
preValidation global), ne renvoie que des noms de sous-dossiers (jamais
de contenu de fichier). Valide le chemin via resolve() (clampe à la
racine, neutralise « .. »), masque les dotfiles par défaut (showHidden),
annote les dépôts git (markRepos), tolère les entrées illisibles.

Tests : packages/server/test/fs-routes.test.ts (10 cas — listing, tri,
hidden, markRepos, rejets 400/404, auth 401). Suite : 185/185 verts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 13:04:10 +02:00
johanleroyandClaude Opus 4.8 0ffc57ddd1 fix(ci): embarquer les fixtures de dialogue dans le paquet de test
Les tests de dialog-detection lisaient leurs captures dans
spikes/s3-tui/captures/, dossier exclu du suivi git (.gitignore :
spikes/**/captures/ + la règle globale *.log). Verts en local mais
absents au checkout CI → ENOENT sur 9 tests.

Les 5 fixtures réellement utilisées sont déplacées dans
packages/server/test/fixtures/dialogs/ et renommées *.raw (sans .log)
pour échapper aux deux règles d'ignore. Le test pointe désormais ce
dossier. Suite hermétique : 175/175 verts, fixtures versionnées.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 12:11:17 +02:00
johanleroy 3aadcbec43 init logo 2026-06-17 11:54:56 +02:00
johanleroyandClaude Opus 4.8 40e20c8c16 fix(build): ne plus suivre les *.tsbuildinfo
Les `packages/*/tsconfig.tsbuildinfo` étaient commités alors que `dist/`
est gitignoré. Sur un checkout neuf de la CI, `tsc -b` lit ce tsbuildinfo,
juge `@arboretum/shared` déjà émis et saute la génération de `dist/` →
`TS2307: Cannot find module '@arboretum/shared'` sur tout le serveur
(+ une `TS7006` en cascade). On dé-tracke les tsbuildinfo et on les ignore.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 11:45:36 +02:00
johanleroyandClaude Opus 4.8 a7da03cf0d docs: README propre pour visiteurs + .gitignore + sortir CLAUDE.md du suivi
- README réécrit (anglais) : setup, quick start, usage, accès Tailscale,
  flags CLI, sécurité, comparatif, section développement
- .gitignore réorganisé par sections (ajout *.tgz, coverage, .claude, .remember, CLAUDE.md)
- CLAUDE.md retiré du suivi git (reste local, non publié)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 11:19:51 +02:00
johanleroyandClaude Opus 4.8 4db593f1f3 docs: P4 livré (supervision mobile : answer + Web Push + PWA)
README et CLAUDE.md reflètent désormais P4 comme livré (et non plus « à venir »),
avec les caveats Web Push (HTTPS/Tailscale, PWA installée sur iOS).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 12:23:53 +02:00
johanleroyandClaude Opus 4.8 1b4c19e00a P4-C: PWA installable, service worker push & campagne de fiabilité dialogues
- 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>
2026-06-15 12:23:00 +02:00
johanleroyandClaude Opus 4.8 47423c0651 P4-B: notifications Web Push (VAPID) sur passage en waiting
- db: migration id=4 `push_subscriptions` (liées au token d'auth) ; clés VAPID en settings
- PushService: bootstrap idempotent des clés VAPID, subscribe/unsubscribe/count,
  notify() best-effort (purge des abonnements 410/404 Gone) ; sender injectable pour les tests
- routes/push.ts: GET vapid-public-key, POST subscribe/unsubscribe/test (toutes sous auth globale)
- pty-manager: déclencheur push sur FRONT MONTANT vers waiting, débouncé 1500ms et annulable
  (faux positif ignoré) ; câblage app/index/config (--vapid-contact)
- shared/api.ts: types VapidKeyResponse / PushSubscribeRequest / PushUnsubscribeRequest
- deps: web-push (+ @types/web-push) côté serveur
- tests: push-service (idempotence, UPSERT, 410-purge, payload) + trigger pty-manager (169 verts)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 12:13:56 +02:00
johanleroyandClaude Opus 4.8 c43fb25c94 P4-A: commande WS answer — répondre aux dialogues sans clavier
Nouvelle commande WS de haut niveau `answer {channel, action, optionN?}`
traduite côté serveur en keystrokes (chiffre+Entrée / Entrée / Esc) et
validée contre le dialogue courant du tracker (anti-frappe fantôme mobile).

- protocol.ts : message `answer` + validation parseClientMessage + ErrorCode INVALID_ANSWER
- pty-manager.answer() : mappe l'intention en write PTY, réutilise le modèle mono-utilisateur
- gateway : case `answer` (NOT_CONTROLLING / SESSION_EXITED / INVALID_ANSWER)
- web : Attachment.answer(), DialogPrompt.vue (attache interactive légère sink no-op),
  intégré dans SessionView, SessionsListView, WorktreeCard + i18n en/fr
- tests : parseClientMessage (answer valide/malformé + fuzz), pty-manager.answer (163 verts)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 12:05:27 +02:00
johanleroyandClaude Opus 4.8 7c536f79ea docs: statut MVP atteint (P1–P3 livrés, P4 en cours) + ignore .idea/
README et CLAUDE.md reflètent désormais la réalité : découverte/reprise,
worktrees multi-repo, états fins et dashboard sont livrés et testés. La
prochaine phase est P4 (supervision mobile : answer + Web Push + PWA).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 11:57:28 +02:00
johanleroyandClaude Opus 4.8 581ff9bce4 P3-C: badges de session live sur le dashboard (résolus depuis le store sessions)
Les badges busy/waiting/idle des sessions affichées dans les cartes worktree
sont désormais résolus depuis le store des sessions (mis à jour en temps réel),
au lieu de l'instantané embarqué dans le worktree — pour que « quelle session
attend une réponse » reste live sans attendre un worktree_update.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 18:48:07 +02:00
johanleroyandClaude Opus 4.8 e5b0b77986 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
johanleroyandClaude Opus 4.8 01c6e617f2 P3-B: claude-adapter — états fins busy/waiting/idle
Détection de l'état fin d'une session claude et du dialogue en cours, pour
savoir « quelle session attend une réponse ». Source primaire = registre
~/.claude/sessions (stable) ; l'écran reconstruit via @xterm/headless TYPE le
dialogue (trust/permission/question/plan) et couvre le cas Trust (avant registre).

- dép: @xterm/headless ^6.0.0 (aligné sur @xterm/xterm du front).
- core/screen-reader.ts: terminal headless persistant par session, alimenté
  par le flux PTY ; snapshotLines() préserve les espaces (bat le strip ANSI naïf).
- core/dialog-classifier.ts: typage pur (texte aplati + regex tolérante inter-
  versions) + extraction des options numérotées (❯ = sélection).
- core/claude-adapter.ts: SessionActivityTracker par session (registre + écran
  → activity + dialog), évaluation débouncée sur output + poll registre léger,
  émission sur changement effectif uniquement.
- shared: SessionSummary += activity?/waitingFor?/dialog? (optionnels, additif).
- pty-manager: tracker instancié pour claude, feed dans handleOutput, summarize
  enrichi, resize propagé, dispose à l'exit.
- web: composant SessionStateBadge (busy/waiting/idle colorés) réutilisable.
- tests: screen-reader + classifier sur fixtures réelles S3 (perm-write/ask/
  trust), claude-adapter (machine à états). 159 verts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 18:39:38 +02:00
johanleroyandClaude Opus 4.8 acd920ebcd P3-A: worktrees multi-repo & cycle de vie (backend)
Enregistrement de repos et gestion de leurs worktrees git, source de vérité
= git (worktrees dérivés + cache court par repo), corrélation worktree ↔
sessions par cwd, mutations sérialisées par repo.

- shared: RepoSummary, PostCreateHook, WorktreeSummary, WorktreeGitStatus ;
  messages WS repo_update/worktree_update/*_removed ; topic sub 'worktrees'
  (+ parseClientMessage) ; DTOs REST repos/worktrees.
- db: migration id:3 (table repos).
- core/git.ts: couche git sûre (execFile, jamais de shell, -- avant chemins,
  GIT_OPTIONAL_LOCKS=0), parseWorktreePorcelain, list/add/remove/prune/status/
  ahead-behind, validation branche + chemin.
- core/claude-trust.ts: pré-trust atomique de ~/.claude.json (spike S3).
- core/worktree-manager.ts: repos CRUD, create (worktree add + pré-trust +
  hooks post-create + startSession optionnel), adopt, delete (garde-fous 409
  dirty / 400 main / 409 session live), prune ; events.
- routes/repos.ts + routes/worktrees.ts, câblage app.ts, gateway topic worktrees.
- tests: git (repos tmp réels), claude-trust, worktree-manager (146 verts).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 18:31:06 +02:00
johanleroyandClaude Opus 4.8 fb175bc7c5 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).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 18:20:21 +02:00
johanleroyandClaude Fable 5 8bc48448c2 P1 complete: web front, test suite, CI — acceptance ALL GREEN
Fan-out integration + fixes found by the test/acceptance pass:
- FIX ring-buffer: chunks >= capacity skipped bytes now count into the
  monotonic offset (invariant: stream byte k lives at k % capacity) —
  window order was corrupted on unaligned big chunks
- FIX auth: non-numeric cookie expiry no longer bypasses expiration
- FIX protocol: safe-integer validation on ack.bytes / hello.protocol
- FIX @fastify/websocket v11: websocket route must be registered in an
  encapsulated context after plugin load (handler got REST signature)
- FIX flow-control deadlock found by e2e acceptance: client only ACKs
  on data receipt, so pausing with an unACKed residue in (LOW,
  ACK_EVERY] stalled both sides at 0.9 MB. ACK_EVERY now 64 KiB (<=
  LOW invariant, tested) + trailing debounced ACK in the web client
- Web: Vue 3 + Vite + Pinia + Tailwind 4 + vue-i18n (EN/FR) + xterm 6
  (fit + webgl fallback), multiplexed ws-client with reconnect/backoff
  and resync epochs
- Tests: 100 vitest (protocol fuzz, ring edges, auth, pty-manager flow
  control with mocked pty, REST e2e) ; CI Node 22/24 + pack-smoke
- scripts/acceptance-p1.mjs: real daemon + real WS client — boot,
  login, attach, stdin, 10 MB flood w/ ACK (13.7 MB/1.9s, RSS bounded),
  brutal disconnect + replay resync, kill broadcast, SIGTERM drain

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 22:29:58 +02:00
johanleroyandClaude Fable 5 3a1396036e P1 spine: monorepo, shared WS protocol, server daemon
- npm workspaces (shared / server / web), TS strict, project refs
- @arboretum/shared: multiplexed WS protocol (JSON control + binary
  output frames: 1B type + u32le channel), flow-control constants
  (ACK 256K, HIGH 384K, LOW 128K, lagging 2M), REST types
- git-arboretum server: Fastify 5 + node:sqlite (single native dep:
  node-pty prebuilt), token auth (sha256 at rest, HMAC cookie, global
  login rate limit + backoff), strict Origin check on /api and /ws,
  PtyManager (2MiB ring with monotonic offset, resync replay = reset +
  256KiB tail, pause/resume only when ALL interactive clients exceed
  HIGH, observers never throttle, lagging clients resync), WS gateway
  (attach/stdin/resize/ack, heartbeat 30s), SIGTERM→SIGKILL 5s grace
- CLI: arboretum [--port 7317] [--bind 127.0.0.1] — non-loopback bind
  requires an explicit safety flag
- Smoke-tested: login/401/403-origin/spawn bash/kill/grace-SIGKILL all
  green

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 22:04:09 +02:00
johanleroyandClaude Fable 5 8623671c69 Spike S3: TUI dialogs, keystrokes — GO (documented partial)
Response protocol: digit positions + Enter confirms (digit alone is not
enough); arrows+Enter work everywhere. Registry detects waiting state
for both permission and AskUserQuestion dialogs (same waitingFor label
— fine-grained dialog typing needs screen reading). Sandboxed bash runs
no-prompt for most commands: real waits come from edits/network/
questions. Pre-trust via ~/.claude.json projects[dir]
.hasTrustDialogAccepted captured and plausible. Esc-deny and plan
approval deferred to P4 reliability campaign.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 17:58:28 +02:00
johanleroyandClaude Fable 5 66dc21bf91 Spike S1: resume/fork/liveness — GO; flood: pause/resume — GO
Demonstrated on CLI 2.1.173: resuming a live session interleaves both
TUIs into one transcript (no lock, no warning) — liveness detection via
registry pid+procStart is mandatory; --fork-session is safe on live
sessions; --resume must run in the session's original cwd ("No
conversation found" otherwise); registry files are cleaned on graceful
exit AND SIGTERM, may be GC'd later after SIGKILL — never reason on
file presence. ANSI-stripped TUI text loses spaces (cursor-positioned
painting) — confirms @xterm/headless for screen parsing.
Flood: 21 MB through node-pty with 10s pause => 0 bytes leaked, no
loss, 4.7 ms echo after flood.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 17:47:05 +02:00
johanleroyandClaude Fable 5 3f7cdab386 Spike S4: JSONL discovery + sessions registry — GO
99.8% of 1318 real transcripts (449 MB) yield sessionId+cwd in 1.37s
(head+tail reads only). Munge cwd→dir validated on 100% of files.
Registry pid/procStart liveness check validated 3/3. 12 line types
inventoried, ai-title is the best title source.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 17:39:19 +02:00
johanleroyandClaude Fable 5 dabb6fe877 Initial commit: positioning, license, project scaffold
Arboretum — self-hosted web dashboard for git worktrees and the Claude
Code sessions running on them. README states the positioning vs
GitKraken Agent Mode, Happy/CloudCLI and Anthropic Remote Control, the
security model (localhost-first + Tailscale), and the Claude usage note.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 17:37:07 +02:00
6 changed files with 74 additions and 119 deletions
+15 -14
View File
@@ -41,26 +41,27 @@ jobs:
cache: npm cache: npm
- run: npm ci - run: npm ci
- run: npm run build - run: npm run build
# @arboretum/shared (paquet workspace NON publié) est INLINÉ dans dist/_shared au prepack # @arboretum/shared (dépendance runtime non publiée) est embarquée dans le tarball
# (scripts/inline-shared.mjs) : le tarball est 100 % autonome — aucun node_modules embarqué, # via bundleDependencies (matérialisée au prepack). On packe donc UN SEUL tarball
# aucune bundleDependency, aucun symlink. On packe en mode workspace (-w), EXACTEMENT comme # et on l'installe seul, comme un vrai consommateur depuis le registre.
# le fait « npm publish » dans release.yml, puis on l'installe seul comme un vrai consommateur.
- name: Pack tarball - name: Pack tarball
run: | run: |
rm -rf /tmp/tarballs && mkdir -p /tmp/tarballs rm -rf /tmp/tarballs && mkdir -p /tmp/tarballs
npm pack -w @johanleroy/git-arboretum --pack-destination /tmp/tarballs # @arboretum/shared (dépendance runtime non publiée) est embarquée via bundleDependencies,
# matérialisée par le hook prepack (copy-web + vendor-shared + copy-meta) comme VRAI dossier
# dans packages/server/node_modules/@arboretum/shared.
# On packe depuis le CONTEXTE DU PACKAGE (cd), PAS en mode workspace (-w) : « npm pack -w »
# résout @arboretum/shared via le symlink workspace hoisté de la racine et n'embarque donc
# PAS le dossier vendored local. « cd packages/server && npm pack » lance le lifecycle pack
# complet et reproduit fidèlement « npm publish » (qui, lui, embarque correctement).
( cd packages/server && npm pack --pack-destination /tmp/tarballs )
ls -l /tmp/tarballs ls -l /tmp/tarballs
- name: Assert the package is self-contained (@arboretum/shared inlined) - name: Assert @arboretum/shared is bundled in the tarball
run: | run: |
tgz=$(ls /tmp/tarballs/*.tgz) tgz=$(ls /tmp/tarballs/*.tgz)
rm -rf /tmp/inspect && mkdir -p /tmp/inspect && tar -xzf "$tgz" -C /tmp/inspect tar -tzf "$tgz" | grep -q 'node_modules/@arboretum/shared/dist/index.js' \
test -f /tmp/inspect/package/dist/_shared/index.js \ || { echo "ERREUR: @arboretum/shared non embarqué dans $tgz"; exit 1; }
|| { echo "ERREUR: dist/_shared/index.js absent de $tgz — inline-shared n'a pas tourné ?"; exit 1; } echo "OK: bundleDependency @arboretum/shared présente dans $tgz"
if grep -rq '@arboretum/shared' /tmp/inspect/package/dist; then
echo "ERREUR: import bare '@arboretum/shared' encore présent dans le JS publié"
grep -rn '@arboretum/shared' /tmp/inspect/package/dist; exit 1
fi
echo "OK: paquet autonome — shared inliné dans dist/_shared, aucun import externe"
- name: Install tarball in an empty project - name: Install tarball in an empty project
run: | run: |
mkdir /tmp/smoke mkdir /tmp/smoke
+5 -29
View File
@@ -35,39 +35,15 @@ jobs:
exit 1 exit 1
fi fi
echo "OK: tag $tag == version $pkg" echo "OK: tag $tag == version $pkg"
# Publication idempotente : le signal faisant autorité d'une version déjà présente est le - run: npm publish -w @johanleroy/git-arboretum
# 409 « already exists » renvoyé par npm publish lui-même (npm view est non fiable contre le
# registre npm de Gitea — faux négatif masqué par >/dev/null). On tente toujours le publish ;
# un 409 = succès idempotent, tout autre échec reste fatal. Le shell Actions tourne en
# `bash -eo pipefail` : on isole l'échec attendu dans la condition d'un `if` pour ne pas
# déclencher `set -e`. Le secret du registre est mappé sur NODE_AUTH_TOKEN lu par le .npmrc
# de setup-node.
- name: Publish (idempotent — tolère un 409 « already exists »)
run: |
if out="$(npm publish -w @johanleroy/git-arboretum 2>&1)"; then
printf '%s\n' "$out"
echo "Publication réussie."
else
code=$?
printf '%s\n' "$out"
if printf '%s' "$out" | grep -qiE 'E409|409 Conflict|already exists'; then
echo "::notice::Version déjà présente sur le registre (409) — publication idempotente, étape ignorée."
else
echo "::error::Échec de la publication (code $code)."
exit "$code"
fi
fi
env: env:
# Mapper le secret du registre (PAT Gitea write:package, ou le token auto
# GITEA_TOKEN s'il a ce scope) sur NODE_AUTH_TOKEN lu par le .npmrc de setup-node.
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
# SBOM (transparence supply-chain entreprise) : généré pour le paquet publié et exposé en # SBOM (transparence supply-chain entreprise) : généré pour le paquet publié et exposé en artefact.
# artefact. C'est un bonus : un échec de génération/upload ne doit jamais faire rougir une
# release dont le publish a réussi → continue-on-error.
# upload-artifact@v3 : Gitea Actions (GHES-like) ne supporte pas @v4 (@actions/artifact v2+).
- name: Generate SBOM (CycloneDX) - name: Generate SBOM (CycloneDX)
continue-on-error: true
run: npx --yes @cyclonedx/cyclonedx-npm --omit dev --output-format JSON --output-file sbom.json -w @johanleroy/git-arboretum || npx --yes @cyclonedx/cyclonedx-npm --omit dev --output-format JSON --output-file sbom.json run: npx --yes @cyclonedx/cyclonedx-npm --omit dev --output-format JSON --output-file sbom.json -w @johanleroy/git-arboretum || npx --yes @cyclonedx/cyclonedx-npm --omit dev --output-format JSON --output-file sbom.json
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v4
continue-on-error: true
with: with:
name: sbom name: sbom
path: sbom.json path: sbom.json
+5 -2
View File
@@ -4840,9 +4840,13 @@
}, },
"packages/server": { "packages/server": {
"name": "@johanleroy/git-arboretum", "name": "@johanleroy/git-arboretum",
"version": "1.4.1", "version": "1.4.0",
"bundleDependencies": [
"@arboretum/shared"
],
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@arboretum/shared": "0.1.0",
"@fastify/cookie": "^11.0.0", "@fastify/cookie": "^11.0.0",
"@fastify/static": "^9.0.0", "@fastify/static": "^9.0.0",
"@fastify/websocket": "^11.0.0", "@fastify/websocket": "^11.0.0",
@@ -4855,7 +4859,6 @@
"arboretum": "dist/index.js" "arboretum": "dist/index.js"
}, },
"devDependencies": { "devDependencies": {
"@arboretum/shared": "0.1.0",
"@types/web-push": "^3.6.4", "@types/web-push": "^3.6.4",
"@types/ws": "^8.5.0" "@types/ws": "^8.5.0"
}, },
+6 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "@johanleroy/git-arboretum", "name": "@johanleroy/git-arboretum",
"version": "1.4.1", "version": "1.4.0",
"description": "Self-hosted web dashboard for git worktrees and the Claude Code sessions running on them", "description": "Self-hosted web dashboard for git worktrees and the Claude Code sessions running on them",
"license": "MIT", "license": "MIT",
"type": "module", "type": "module",
@@ -48,10 +48,14 @@
"scripts": { "scripts": {
"build": "tsc -b", "build": "tsc -b",
"dev": "tsc -b --watch & node --watch dist/index.js", "dev": "tsc -b --watch & node --watch dist/index.js",
"prepack": "node scripts/copy-web.mjs && node scripts/inline-shared.mjs && node scripts/copy-meta.mjs", "prepack": "node scripts/copy-web.mjs && node scripts/vendor-shared.mjs && node scripts/copy-meta.mjs",
"test": "vitest run" "test": "vitest run"
}, },
"bundleDependencies": [
"@arboretum/shared"
],
"dependencies": { "dependencies": {
"@arboretum/shared": "0.1.0",
"@fastify/cookie": "^11.0.0", "@fastify/cookie": "^11.0.0",
"@fastify/static": "^9.0.0", "@fastify/static": "^9.0.0",
"@fastify/websocket": "^11.0.0", "@fastify/websocket": "^11.0.0",
@@ -61,7 +65,6 @@
"web-push": "^3.6.7" "web-push": "^3.6.7"
}, },
"devDependencies": { "devDependencies": {
"@arboretum/shared": "0.1.0",
"@types/web-push": "^3.6.4", "@types/web-push": "^3.6.4",
"@types/ws": "^8.5.0" "@types/ws": "^8.5.0"
} }
-71
View File
@@ -1,71 +0,0 @@
#!/usr/bin/env node
// Inline @arboretum/shared (paquet workspace NON publié) directement dans le dist du serveur,
// pour que le tarball npm soit 100 % autonome. Branché sur le hook "prepack".
//
// Pourquoi PAS bundleDependencies : embarquer une dépendance qui est aussi un *workspace* via
// bundleDependencies est instable selon l'environnement npm (mode -w, exécution en root sur un
// runner CI, version d'arborist) — npm voit le nœud comme un lien workspace et n'embarque parfois
// AUCUN fichier ("bundled files: 0"), produisant un paquet cassé chez le consommateur. On élimine
// donc toute magie de bundling : on copie le JS compilé de shared dans dist/_shared et on réécrit
// l'import bare '@arboretum/shared' du serveur vers ce chemin relatif. Zéro node_modules embarqué,
// zéro symlink, résultat identique partout.
import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { join, dirname, relative, sep } from 'node:path';
import { fileURLToPath } from 'node:url';
const serverDir = join(dirname(fileURLToPath(import.meta.url)), '..');
const serverDist = join(serverDir, 'dist');
const sharedDist = join(serverDir, '..', 'shared', 'dist');
const inlineDir = join(serverDist, '_shared');
for (const [label, p] of [['dist serveur', serverDist], ['dist shared', sharedDist]]) {
if (!existsSync(p)) {
console.error(`inline-shared: ${label} introuvable (${p}) — lance "npm run build" avant le pack.`);
process.exit(1);
}
}
// 1) Copier le JS compilé de shared dans dist/_shared (uniquement *.js : seul le runtime compte ;
// les .d.ts/.map ne sont de toute façon pas publiés via le glob `files`). index.js réexporte
// ./protocol.js et ./api.js en relatif → la copie complète préserve la résolution interne.
rmSync(inlineDir, { recursive: true, force: true });
mkdirSync(inlineDir, { recursive: true });
let copied = 0;
for (const name of readdirSync(sharedDist)) {
if (name.endsWith('.js')) {
cpSync(join(sharedDist, name), join(inlineDir, name));
copied++;
}
}
if (copied === 0) {
console.error(`inline-shared: aucun .js dans ${sharedDist} — shared n'est pas compilé.`);
process.exit(1);
}
// 2) Réécrire l'import bare '@arboretum/shared' de chaque .js du serveur vers le chemin relatif
// (POSIX) pointant sur dist/_shared/index.js, calculé par fichier (profondeur variable).
const walk = (dir) =>
readdirSync(dir, { withFileTypes: true }).flatMap((e) => {
const p = join(dir, e.name);
if (e.isDirectory()) return p === inlineDir ? [] : walk(p); // ne pas se réécrire soi-même
return e.name.endsWith('.js') ? [p] : [];
});
let rewritten = 0;
for (const file of walk(serverDist)) {
const src = readFileSync(file, 'utf8');
if (!src.includes('@arboretum/shared')) continue;
let rel = relative(dirname(file), join(inlineDir, 'index.js')).split(sep).join('/');
if (!rel.startsWith('.')) rel = `./${rel}`;
const out = src.replaceAll(`'@arboretum/shared'`, `'${rel}'`).replaceAll(`"@arboretum/shared"`, `"${rel}"`);
if (out !== src) {
writeFileSync(file, out);
rewritten++;
}
}
if (rewritten === 0) {
console.error(`inline-shared: aucun import '@arboretum/shared' réécrit dans ${serverDist} — build manquant ?`);
process.exit(1);
}
console.log(`inline-shared: ${copied} fichier(s) shared -> dist/_shared, import réécrit dans ${rewritten} fichier(s) serveur`);
+43
View File
@@ -0,0 +1,43 @@
#!/usr/bin/env node
// Embarque @arboretum/shared (dépendance runtime non publiée) DANS le tarball npm.
// Branché sur le hook "prepack", aux côtés de bundleDependencies dans package.json.
//
// Pourquoi un vrai dossier et pas le symlink workspace : sous npm workspaces, shared
// est seulement symlinké dans le node_modules racine ; `npm pack` n'embarque une
// bundleDependency que si elle existe comme VRAI dossier dans le node_modules du
// paquet packé (packages/server/node_modules/@arboretum/shared). On le matérialise ici.
import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const serverDir = join(dirname(fileURLToPath(import.meta.url)), '..');
const sharedDir = join(serverDir, '..', 'shared');
const sharedDist = join(sharedDir, 'dist');
const target = join(serverDir, 'node_modules', '@arboretum', 'shared');
if (!existsSync(sharedDist)) {
console.error(
`vendor-shared: ${sharedDist} introuvable — lance "npm run build" (shared doit être compilé) avant le pack.`,
);
process.exit(1);
}
// package.json minimal et déterministe : on reprend les champs de résolution réels de
// shared (version incluse, pour rester synchro), sans scripts ni files inutiles au runtime.
const shared = JSON.parse(readFileSync(join(sharedDir, 'package.json'), 'utf8'));
const minimal = {
name: shared.name,
version: shared.version,
type: shared.type,
main: shared.main,
types: shared.types,
exports: shared.exports,
};
// Idempotent : on repart d'un dossier propre à chaque pack.
rmSync(target, { recursive: true, force: true });
mkdirSync(target, { recursive: true });
writeFileSync(join(target, 'package.json'), JSON.stringify(minimal, null, 2) + '\n');
cpSync(sharedDist, join(target, 'dist'), { recursive: true });
console.log(`vendor-shared: embarqué ${shared.name}@${shared.version} -> ${target}`);