repowarden 4.11.1-next.2 → 4.12.0-next.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/garde CHANGED
@@ -246,10 +246,89 @@ verifier_git() {
246
246
  ) || exit $?
247
247
  }
248
248
 
249
+ # Vrai si le ticket $1 (dépôt -R $2, sinon le dépôt courant) est ouvert et
250
+ # validé : preuve qu'une écriture GitHub hors du circuit des PR a été décidée
251
+ ticket_valide() {
252
+ local lu r=()
253
+ [ -z "$2" ] || r=(-R "$2")
254
+ lu="$(gh issue view "$1" ${r[@]+"${r[@]}"} --json state,labels -q '.state, .labels[].name' 2>/dev/null)" || return 2
255
+ [ -n "$lu" ] || return 2
256
+ [ "$(head -n 1 <<<"$lu")" = OPEN ] && grep -qxF "$VALIDE" <<<"$lu"
257
+ }
258
+
259
+ # Commande gh (mots $@, sans « gh ») lancée depuis $dir, préfixe
260
+ # REPOWARDEN_TICKET=$ticket_gh : lectures connues permises, écritures du circuit
261
+ # par repowarden, décisions au mainteneur, tout le reste avec un ticket validé
262
+ verifier_gh() {
263
+ local ligne="gh $*" pos=() depot="" methode="" champs="" approuve="" mutation="" ecriture="" hors="" rc
264
+ while [ $# -gt 0 ]; do
265
+ case "$1" in
266
+ -R | --repo) shift; depot="${1:-}" ;;
267
+ --repo=*) depot="${1#--repo=}" ;;
268
+ -X | --method) shift; methode="${1:-}" ;;
269
+ -X?*) methode="${1#-X}" ;;
270
+ --method=*) methode="${1#--method=}" ;;
271
+ -f | -F | --field | --raw-field | --input | -f?* | -F?* | --field=* | --raw-field=* | --input=*) champs=1 ;;
272
+ -a | --approve) approuve=1 ;;
273
+ -h | --help) return 0 ;;
274
+ -*) ;;
275
+ *)
276
+ pos+=("$1")
277
+ [[ "$1" != *mutation* ]] || mutation=1
278
+ ;;
279
+ esac
280
+ shift
281
+ done
282
+ [ -n "${pos[0]:-}" ] || return 0
283
+ (
284
+ cd "$dir" 2>/dev/null || exit 0
285
+ depot_repowarden || exit 0
286
+ # gh api : écriture si méthode autre que GET, ou champs sans -X GET
287
+ # (POST par défaut) ; graphql : seulement une mutation
288
+ methode="$(tr '[:lower:]' '[:upper:]' <<<"$methode")"
289
+ if [ "${pos[1]:-}" = graphql ]; then
290
+ ecriture="$mutation"
291
+ elif [ -n "$methode" ]; then
292
+ [ "$methode" = GET ] || ecriture=1
293
+ else
294
+ ecriture="$champs"
295
+ fi
296
+ case "${pos[0]} ${pos[1]:-}" in
297
+ "issue create") refuser garde.gh_ticket_nouveau "$ligne" ;;
298
+ "issue comment" | "issue edit") refuser garde.gh_ticket_comment "$ligne" ;;
299
+ "pr create") refuser garde.gh_pr_create "$ligne" ;;
300
+ "run rerun") refuser garde.gh_rerun "$ligne" ;;
301
+ "pr ready" | "pr merge") refuser garde.gh_mainteneur "$ligne" ;;
302
+ "pr review") [ -z "$approuve" ] || refuser garde.gh_mainteneur "$ligne"; hors=1 ;;
303
+ "api "*)
304
+ [ -n "$ecriture" ] || exit 0
305
+ [[ "${pos[1]:-}" != *pending_deployments* ]] || refuser garde.gh_mainteneur "$ligne"
306
+ hors=1
307
+ ;;
308
+ # Lectures connues et description de la PR : permises ; tout le
309
+ # reste (sous-commande oubliée ou future comprise) demande un ticket
310
+ "auth status" | "pr edit" | "search "* | "browse "* | "status "* | "version "* | "help "*) exit 0 ;;
311
+ *)
312
+ case "${pos[1]:-}" in view | list | status | checks | diff | watch) exit 0 ;; esac
313
+ hors=1
314
+ ;;
315
+ esac
316
+ [ -n "$hors" ] || exit 0
317
+ [ -n "$ticket_gh" ] || refuser garde.gh_sans_ticket "$ligne" "$ligne"
318
+ [[ "$ticket_gh" =~ ^[0-9]+$ ]] || refuser garde.gh_ticket_illisible "$ligne" "$ticket_gh"
319
+ rc=0
320
+ ticket_valide "$ticket_gh" "$depot" || rc=$?
321
+ [ "$rc" -ne 2 ] || refuser garde.gh_ticket_illisible "$ligne" "$ticket_gh"
322
+ [ "$rc" -eq 0 ] || refuser garde.gh_ticket_non_valide "$ligne" "$ticket_gh" "$VALIDE"
323
+ exit 0
324
+ ) || exit $?
325
+ }
326
+
249
327
  # Ligne de commande shell : chaque commande (&&, ||, ;, |, retour à la ligne),
250
- # « cd » suivi, variables d'environnement en tête ignorées
328
+ # « cd » suivi, variables d'environnement en tête ignorées (sauf
329
+ # REPOWARDEN_TICKET, le ticket d'une écriture gh hors du circuit des PR)
251
330
  verifier_shell() {
252
- local commande="$1" segment mots m cmd_cc nl=$'\n' lignes
331
+ local commande="$1" segment mots m cmd_cc nl=$'\n' lignes ticket_gh
253
332
  dir="$cwd"
254
333
  # Une commande par ligne. Découpage hors de <( … ) : bash 3.2 (macOS) y
255
334
  # lit mal ces motifs (« bad substitution ») et ne vérifierait plus rien
@@ -264,8 +343,10 @@ verifier_shell() {
264
343
  # shellcheck disable=SC2162 # les antislashs restent tels quels, comme dans la commande
265
344
  read -a mots <<<"$segment" || true
266
345
  set -- ${mots[@]+"${mots[@]}"}
346
+ ticket_gh=""
267
347
  while [ $# -gt 0 ]; do
268
348
  case "$1" in
349
+ REPOWARDEN_TICKET=*) m="${1#REPOWARDEN_TICKET=}"; ticket_gh="${m//[\"\']/}"; shift ;;
269
350
  [A-Za-z_]*=* | '(' | '{' | '!' | command | exec | env | sudo | time | nohup) shift ;;
270
351
  *) break ;;
271
352
  esac
@@ -285,6 +366,10 @@ verifier_shell() {
285
366
  shift
286
367
  verifier_git "${@//[\"\']/}"
287
368
  ;;
369
+ gh | */gh)
370
+ shift
371
+ verifier_gh "${@//[\"\']/}"
372
+ ;;
288
373
  repowarden | */repowarden)
289
374
  case "${2//[\"\']/}" in
290
375
  cc | commit)
package/bin/statut CHANGED
@@ -11,6 +11,8 @@ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)"
11
11
  source "$ROOT/hooks/lib/common.sh"
12
12
  # shellcheck source=../hooks/lib/secrets.sh
13
13
  source "$ROOT/hooks/lib/secrets.sh"
14
+ # shellcheck source=../hooks/lib/agents.sh
15
+ source "$ROOT/hooks/lib/agents.sh"
14
16
  exec 2>&1 # une seule sortie, dans l'ordre (avertissements compris)
15
17
  # shellcheck disable=SC2034 # lu par check_version (common.sh)
16
18
  HOOKS_DIR="$ROOT/hooks"
@@ -140,6 +142,15 @@ if [ -n "$GIT_TOPLEVEL" ]; then
140
142
  [ "$FORGE" != github ] || dim_t st.repo_rules
141
143
  fi
142
144
 
145
+ # Agent IA sans garde-fou : ses actions ne seraient vérifiées qu'au commit.
146
+ # Branché dans le dépôt (à versionner), ou pour l'utilisateur hors dépôt
147
+ if agents_sans_garde "$GIT_TOPLEVEL"; then
148
+ cmd_agents="repowarden agents init"
149
+ [ -n "$GIT_TOPLEVEL" ] || cmd_agents="repowarden agents init --global"
150
+ attention_t st.agents_sans_garde "$cmd_agents"
151
+ suivant="${suivant:-$cmd_agents}"
152
+ fi
153
+
143
154
  # Outils : secrets, code mort (selon les langages)
144
155
  presents="" absents=""
145
156
  for outil in gitleaks ruff vulture staticcheck pmd; do
@@ -62,13 +62,14 @@ statut() {
62
62
  # texte déjà signé (repris, mis à jour) n'en reçoit pas un second.
63
63
  PIED="<!-- repowarden:pied -->"
64
64
 
65
- # REPLY = texte $1 terminé par le pied (page de l'App APP_SLUG, doc) ; sans
66
- # slug (jeton des Actions, github-actions[bot]) ou déjà signé, texte inchangé
65
+ # REPLY = texte $1 terminé par le pied (---, puis une phrase en Markdown
66
+ # normal : <sub> affiche les liens en brut) ; sans slug (jeton des Actions,
67
+ # github-actions[bot]) ou déjà signé, texte inchangé
67
68
  pied_r() {
68
69
  REPLY="$1"
69
70
  [ -n "${APP_SLUG:-}" ] && [[ "$REPLY" != *"$PIED"* ]] || return 0
70
- _tr tk.footer "$APP_SLUG" "$APP_SLUG"
71
- REPLY="$REPLY"$'\n\n'"$PIED<sub>$_T</sub>"
71
+ _tr tk.footer "$APP_SLUG"
72
+ REPLY="$REPLY"$'\n\n---\n\n'"$PIED$_T"
72
73
  }
73
74
 
74
75
  # Commentaire $2 du bot sur le ticket $1, avec son pied
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env bash
2
+ # Agents IA du poste et garde-fou repowarden (repowarden statut, install.sh).
3
+ # Compatible bash 3.2.
4
+
5
+ # Vrai si Claude Code ou opencode est sur ce poste (commande, ou dossier de
6
+ # configuration) sans garde-fou branché, ni dans le dépôt $1 (racine, vide hors
7
+ # dépôt) ni pour l'utilisateur : ses actions ne seraient vues qu'au commit
8
+ agents_sans_garde() {
9
+ local claude_dir opencode_dir f fichiers=()
10
+ claude_dir="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"
11
+ opencode_dir="${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}"
12
+ command -v claude >/dev/null 2>&1 || command -v opencode >/dev/null 2>&1 ||
13
+ [ -d "$claude_dir" ] || [ -d "$opencode_dir" ] || return 1
14
+ [ -z "${1:-}" ] || fichiers=("$1/.claude/settings.json" "$1/.opencode/plugins/repowarden-garde.js")
15
+ fichiers+=("$claude_dir/settings.json" "$opencode_dir/plugins/repowarden-garde.js")
16
+ for f in "${fichiers[@]}"; do
17
+ ! grep -qs "repowarden garde" "$f" || return 1
18
+ done
19
+ return 0
20
+ }
@@ -104,6 +104,8 @@ _msg_en() {
104
104
  install.purge_needs_uninstall) _M="--purge and --scan go with --uninstall." ;;
105
105
  install.replaced) _M="core.hooksPath (%s) was '%s', replaced." ;;
106
106
  install.enabled) _M="Hooks enabled (%s) → %s" ;;
107
+ install.agents_ask) _M="AI agent without the guard (Claude Code, opencode). Wire the guard (%s)?" ;;
108
+ install.agents_later) _M="AI agent without the guard (Claude Code, opencode): wire it with %s" ;;
107
109
  install.local_wins) _M="A local core.hooksPath (e.g. husky) still takes precedence in that repository." ;;
108
110
  install.bots_never) _M="Repository bots will no longer be offered (repowarden.bots = off); on demand: repowarden bots" ;;
109
111
  install.lang_set) _M="Message language: English (change: %s --global --lang fr)" ;;
@@ -509,6 +511,7 @@ _msg_en() {
509
511
  st.wf_old) _M="%s:%s: repowarden called in an outdated major version (@%s); expected: %s" ;;
510
512
  st.wf_fix) _M="fix the uses: lines reported above" ;;
511
513
  st.tools) _M="Tools present: %s · missing: %s (depending on the projects' languages)" ;;
514
+ st.agents_sans_garde) _M="AI agent without the guard (Claude Code, opencode): its actions are only checked at commit time. → %s" ;;
512
515
  st.next) _M="Next step:" ;;
513
516
  st.ready) _M="All set: git cc to commit, repowarden code-mort on a branch." ;;
514
517
  st.help) _M="Every command: repowarden --help" ;;
@@ -680,7 +683,7 @@ _msg_en() {
680
683
  tk.init_next_wired) _M="Next: commit these files; require the \"ticket\" check (repowarden proteger --checks \"…,ticket\"). The tickets job of release.yml is already wired." ;;
681
684
  tk.prerelease) _M="Available in pre-release **%s**: to be tested before production." ;;
682
685
  tk.released) _M="Released in **%s**." ;;
683
- tk.footer) _M="Posted by this repository's GitHub App ([repowarden docs](https://simdevwarden.github.io/repowarden/en/industrialisation/#github-app-for-workflows)): [%s](https://github.com/apps/%s)" ;;
686
+ tk.footer) _M="Automated comment from [repowarden](https://github.com/apps/%s), this repository's ticket tracking. [Learn more](https://simdevwarden.github.io/repowarden/en/tickets/)" ;;
684
687
  tk.delivered) _M="Delivered to **%s**, without a new version (commit %s)." ;;
685
688
  # Ticket comment
686
689
  tk.comment.adding) _M="Adding comment to ticket #%s..." ;;
@@ -729,6 +732,14 @@ _msg_en() {
729
732
  garde.commit_protegee) _M="command refused: \"%s\"; \"%s\" is a protected branch, the commit would be refused there.\n → Work on a ticket's branch: repowarden ticket <n>, then run it again." ;;
730
733
  garde.commit_protegee_libre) _M="command refused: \"%s\"; \"%s\" is a protected branch, the commit would be refused there.\n → Create a work branch (your changes follow): git switch -c <type>/<topic>, then run it again." ;;
731
734
  garde.ligne_entiere) _M=" Nothing on this line was run, not even the other commands: run them separately." ;;
735
+ garde.gh_ticket_nouveau) _M="command refused: \"%s\"; tickets are created through repowarden (status, type, validation).\n → repowarden ticket nouveau \"<title>\" [--type fix] [--description \"…\"]" ;;
736
+ garde.gh_ticket_comment) _M="command refused: \"%s\"; tickets are commented on and labelled through repowarden.\n → repowarden ticket comment <n> \"<text>\"; repowarden ticket label <n> <label> [--remove]" ;;
737
+ garde.gh_pr_create) _M="command refused: \"%s\"; the draft PR is opened by the bot on the first push of the ticket's branch.\n → git push -u origin HEAD" ;;
738
+ garde.gh_rerun) _M="command refused: \"%s\"; workflows are re-run through repowarden.\n → repowarden run rerun <run-id|--failed>" ;;
739
+ garde.gh_mainteneur) _M="command refused: \"%s\"; this is the maintainer's decision (leaving draft, merge, approval, deployment).\n → Say the PR is ready (CI green) and let the maintainer decide." ;;
740
+ garde.gh_sans_ticket) _M="command refused: \"%s\" changes GitHub outside the PR flow, without a ticket.\n → Create the ticket (repowarden ticket nouveau), get it validated, then run again: REPOWARDEN_TICKET=<n> %s" ;;
741
+ garde.gh_ticket_illisible) _M="command refused: \"%s\"; ticket #%s unreadable (gh issue view): a write without proof of a ticket.\n → Check the number and gh auth status, then run again: REPOWARDEN_TICKET=<n> gh …" ;;
742
+ garde.gh_ticket_non_valide) _M="command refused: \"%s\"; ticket #%s is not open and validated (label \"%s\").\n → Get the ticket validated by the maintainer, then run again." ;;
732
743
  agents.added) _M="%s: guard added (%s)." ;;
733
744
  agents.present) _M="%s: guard already in place (%s)." ;;
734
745
  agents.kept) _M="%s: %s exists and differs from the template; kept as is (template: %s)." ;;
@@ -104,6 +104,8 @@ _msg_fr() {
104
104
  install.purge_needs_uninstall) _M="--purge et --scan s'utilisent avec --uninstall." ;;
105
105
  install.replaced) _M="core.hooksPath (%s) valait '%s', remplacé." ;;
106
106
  install.enabled) _M="Hooks activés (%s) → %s" ;;
107
+ install.agents_ask) _M="Agent IA sans garde-fou (Claude Code, opencode). Brancher le garde-fou (%s) ?" ;;
108
+ install.agents_later) _M="Agent IA sans garde-fou (Claude Code, opencode) : le brancher avec %s" ;;
107
109
  install.local_wins) _M="Un core.hooksPath local (ex. husky) reste prioritaire dans le dépôt concerné." ;;
108
110
  install.bots_never) _M="Les bots du dépôt ne seront plus proposés (repowarden.bots = off) ; à la demande : repowarden bots" ;;
109
111
  install.lang_set) _M="Langue des messages : français (changer : %s --global --lang en)" ;;
@@ -510,6 +512,7 @@ _msg_fr() {
510
512
  st.wf_old) _M="%s:%s : repowarden appelé dans une majeure dépassée (@%s) ; attendu : %s" ;;
511
513
  st.wf_fix) _M="corriger les lignes uses: signalées ci-dessus" ;;
512
514
  st.tools) _M="Outils présents : %s · absents : %s (selon les langages des projets)" ;;
515
+ st.agents_sans_garde) _M="Agent IA sans garde-fou (Claude Code, opencode) : ses actions ne sont vérifiées qu'au commit. → %s" ;;
513
516
  st.next) _M="Étape suivante :" ;;
514
517
  st.ready) _M="Tout est prêt : git cc pour commiter, repowarden code-mort sur une branche." ;;
515
518
  st.help) _M="Toutes les commandes : repowarden --help" ;;
@@ -681,7 +684,7 @@ _msg_fr() {
681
684
  tk.init_next_wired) _M="Ensuite : committer ces fichiers ; exiger la vérification « ticket » (repowarden proteger --checks \"…,ticket\"). Le job tickets de release.yml est déjà branché." ;;
682
685
  tk.prerelease) _M="Disponible en préversion **%s** : à tester avant la production." ;;
683
686
  tk.released) _M="Publié dans **%s**." ;;
684
- tk.footer) _M="Message de l'App GitHub de ce dépôt ([doc repowarden](https://simdevwarden.github.io/repowarden/industrialisation/#github-app-des-workflows)) : [%s](https://github.com/apps/%s)" ;;
687
+ tk.footer) _M="Commentaire automatique de [repowarden](https://github.com/apps/%s), le suivi des tickets de ce dépôt. [En savoir plus](https://simdevwarden.github.io/repowarden/tickets/)" ;;
685
688
  tk.delivered) _M="Livré sur **%s**, sans nouvelle version (commit %s)." ;;
686
689
  # Ticket comment
687
690
  tk.comment.adding) _M="Ajout d'un commentaire sur le ticket #%s..." ;;
@@ -730,6 +733,14 @@ _msg_fr() {
730
733
  garde.commit_protegee) _M="commande refusée : « %s » ; « %s » est une branche protégée, le commit y serait refusé.\n → Travailler sur la branche d'un ticket : repowarden ticket <n>, puis relancer." ;;
731
734
  garde.commit_protegee_libre) _M="commande refusée : « %s » ; « %s » est une branche protégée, le commit y serait refusé.\n → Créer une branche de travail (les modifications suivent) : git switch -c <type>/<sujet>, puis relancer." ;;
732
735
  garde.ligne_entiere) _M=" Rien de cette ligne n'a été lancé, pas même les autres commandes : les relancer à part." ;;
736
+ garde.gh_ticket_nouveau) _M="commande refusée : « %s » ; un ticket se crée par repowarden (statut, type, validation).\n → repowarden ticket nouveau \"<titre>\" [--type fix] [--description \"…\"]" ;;
737
+ garde.gh_ticket_comment) _M="commande refusée : « %s » ; un ticket se commente et s'étiquette par repowarden.\n → repowarden ticket comment <n> \"<texte>\" ; repowarden ticket label <n> <label> [--remove]" ;;
738
+ garde.gh_pr_create) _M="commande refusée : « %s » ; la PR brouillon est ouverte par le bot au premier push de la branche du ticket.\n → git push -u origin HEAD" ;;
739
+ garde.gh_rerun) _M="commande refusée : « %s » ; un workflow se relance par repowarden.\n → repowarden run rerun <run-id|--failed>" ;;
740
+ garde.gh_mainteneur) _M="commande refusée : « %s » ; c'est une décision du mainteneur (sortie du brouillon, merge, approbation, déploiement).\n → Annoncer que la PR est prête (CI verte) et laisser le mainteneur décider." ;;
741
+ garde.gh_sans_ticket) _M="commande refusée : « %s » modifie GitHub hors du circuit des PR, sans ticket.\n → Créer le ticket (repowarden ticket nouveau), le faire valider, puis relancer : REPOWARDEN_TICKET=<n> %s" ;;
742
+ garde.gh_ticket_illisible) _M="commande refusée : « %s » ; ticket #%s illisible (gh issue view) : écriture sans preuve de ticket.\n → Vérifier le numéro et gh auth status, puis relancer : REPOWARDEN_TICKET=<n> gh …" ;;
743
+ garde.gh_ticket_non_valide) _M="commande refusée : « %s » ; le ticket #%s n'est pas ouvert et validé (étiquette « %s »).\n → Faire valider le ticket par le mainteneur, puis relancer." ;;
733
744
  agents.added) _M="%s : garde-fou ajouté (%s)." ;;
734
745
  agents.present) _M="%s : garde-fou déjà en place (%s)." ;;
735
746
  agents.kept) _M="%s : %s existe et diffère du modèle ; gardé tel quel (modèle : %s)." ;;
package/install.sh CHANGED
@@ -20,6 +20,8 @@ HOOKS="$ROOT/hooks"
20
20
  source "$HOOKS/lib/ui.sh"
21
21
  # shellcheck source=hooks/lib/i18n.sh
22
22
  source "$HOOKS/lib/i18n.sh"
23
+ # shellcheck source=hooks/lib/agents.sh
24
+ source "$HOOKS/lib/agents.sh"
23
25
  SCOPE="--local"
24
26
  ACTION="install"
25
27
  PURGE=""
@@ -251,6 +253,29 @@ if [ "$SCOPE" = --global ]; then
251
253
  info "$REPLY"
252
254
  fi
253
255
 
256
+ # Agent IA sans garde-fou (Claude Code, opencode) : branchement proposé dans un
257
+ # terminal, jamais fait sans accord ; sinon, la commande sur une ligne
258
+ RACINE="$(git rev-parse --show-toplevel 2>/dev/null || true)"
259
+ if agents_sans_garde "$RACINE"; then
260
+ AGENTS_PORTEE=--projet
261
+ [ "$SCOPE" = --local ] || AGENTS_PORTEE=--global
262
+ CMD_AGENTS="repowarden agents init"
263
+ [ "$AGENTS_PORTEE" = --projet ] || CMD_AGENTS="$CMD_AGENTS --global"
264
+ REPONSE=n
265
+ if [ -t 0 ] || [ -n "${REPOWARDEN_INTERACTIF:-}" ]; then
266
+ t install.agents_ask "$CMD_AGENTS"
267
+ printf '%s [O/n] ' "$REPLY" >&2
268
+ IFS= read -r REPONSE || REPONSE=n
269
+ case "$REPONSE" in "" | [oOyY]*) REPONSE=o ;; esac
270
+ fi
271
+ if [ "$REPONSE" = o ]; then
272
+ bash "$ROOT/bin/agents" init "$AGENTS_PORTEE" || true
273
+ else
274
+ t install.agents_later "$CMD_AGENTS"
275
+ info "$REPLY"
276
+ fi
277
+ fi
278
+
254
279
  # Bots du dépôt GitHub courant (CI, releases, tickets, protection…) : proposés,
255
280
  # jamais configurés sans accord ; --sans-bots : plus jamais proposés
256
281
  case "$BOTS" in
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "repowarden",
3
- "version": "4.11.1-next.2",
3
+ "version": "4.12.0-next.2",
4
4
  "description": "Hooks git et CI multi-langages : messages de commit, nommage des branches, secrets, formatage, tests ciblés, code mort, releases.",
5
5
  "keywords": [
6
6
  "git-hooks",