repowarden 4.13.0-next.7 → 4.13.1-next.1

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/agents CHANGED
@@ -2,15 +2,15 @@
2
2
  # Branche le garde-fou repowarden (repowarden garde) dans les agents IA :
3
3
  # repowarden agents init [--claude] [--opencode] [--projet | --global]
4
4
  # repowarden agents retirer [--claude] [--opencode] [--projet | --global]
5
- # Claude Code hook PreToolUse dans .claude/settings.json
5
+ # Claude Code hook PreToolUse dans .claude/settings.json + skill repowarden
6
6
  # opencode plugins .opencode/plugins/repowarden-garde.js (opencode 1.x)
7
- # et repowarden-garde-v2.js (opencode 2.x)
7
+ # et repowarden-garde-v2.js (opencode 2.x) + skill repowarden
8
8
  # Sans --claude ni --opencode : les deux. --projet (défaut) : fichiers du dépôt,
9
9
  # à versionner ; --global : configuration de l'utilisateur, là où chaque outil
10
10
  # la lit (CLAUDE_CONFIG_DIR ; OPENCODE_CONFIG_DIR, sinon XDG_CONFIG_HOME).
11
- # Relançable : fichiers existants complétés ; un garde-fou posé par une version
12
- # antérieure de repowarden est mis à jour, un fichier personnel jamais écrasé ;
13
- # retirer n'enlève que ce qu'init a écrit.
11
+ # Relançable : fichiers existants complétés ; un garde-fou ou une skill posés
12
+ # par une version antérieure de repowarden sont mis à jour, un fichier
13
+ # personnel jamais écrasé ; retirer n'enlève que ce qu'init a écrit.
14
14
  set -euo pipefail
15
15
 
16
16
  ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)"
@@ -23,6 +23,7 @@ aide() { _tr help.agents; printf '%b\n' "$_T"; }
23
23
 
24
24
  COMMANDE_CLAUDE="$AGENTS_COMMANDE_CLAUDE"
25
25
  MATCHER_CLAUDE="$AGENTS_MATCHER_CLAUDE"
26
+ MODELE_SKILL="$(agents_modele_skill)"
26
27
 
27
28
  action=""
28
29
  case "${1:-}" in
@@ -51,12 +52,16 @@ if [ "$portee" = projet ]; then
51
52
  base="$GIT_TOPLEVEL"
52
53
  fichier_claude="$base/.claude/settings.json"
53
54
  dossier_opencode="$base/.opencode/plugins"
55
+ fichier_skill_claude="$base/.claude/skills/repowarden/SKILL.md"
56
+ fichier_skill_opencode="$base/.opencode/skills/repowarden/SKILL.md"
54
57
  else
55
58
  # Réglages utilisateur de Claude Code : $CLAUDE_CONFIG_DIR s'il est défini
56
59
  # (code.claude.com/docs/en/settings) ; plugins globaux d'opencode :
57
60
  # $OPENCODE_CONFIG_DIR, sinon $XDG_CONFIG_HOME/opencode (opencode.ai/docs/config)
58
61
  fichier_claude="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/settings.json"
59
62
  dossier_opencode="${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/plugins"
63
+ fichier_skill_claude="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/skills/repowarden/SKILL.md"
64
+ fichier_skill_opencode="${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/skills/repowarden/SKILL.md"
60
65
  fi
61
66
 
62
67
  ecrits=()
@@ -122,6 +127,28 @@ init_opencode() {
122
127
  done
123
128
  }
124
129
 
130
+ # Skill repowarden de l'agent $2 au fichier $1, posée selon la langue :
131
+ # remplacée si posée par repowarden et périmée, gardée si personnalisée
132
+ poser_skill() {
133
+ local f="$1" nom="$2"
134
+ if [ -f "$f" ]; then
135
+ if cmp -s "$f" "$MODELE_SKILL"; then
136
+ ok_t agents.skill_present "$nom" "$f"
137
+ elif agents_skill_repowarden "$f"; then
138
+ cp "$MODELE_SKILL" "$f"
139
+ ecrits+=("$f")
140
+ ok_t agents.skill_updated "$nom" "$f"
141
+ else
142
+ attention_t agents.skill_kept "$nom" "$f" "$MODELE_SKILL"
143
+ fi
144
+ return 0
145
+ fi
146
+ mkdir -p "$(dirname "$f")"
147
+ cp "$MODELE_SKILL" "$f"
148
+ ecrits+=("$f")
149
+ ok_t agents.skill_added "$nom" "$f"
150
+ }
151
+
125
152
  # Hook PreToolUse : seules les commandes « repowarden garde » retirées ; une
126
153
  # entrée vidée disparaît, et le fichier s'il ne reste plus rien
127
154
  retirer_claude() {
@@ -164,14 +191,47 @@ retirer_opencode() {
164
191
  done
165
192
  }
166
193
 
194
+ # Skill repowarden : retirée si identique au modèle de la langue ; sinon
195
+ # gardée et signalée (peut être modifiée ou dans une autre langue)
196
+ retirer_skill_claude() {
197
+ local f="$fichier_skill_claude" nom="Claude Code"
198
+ if [ ! -f "$f" ]; then
199
+ ok_t agents.skill_absent "$nom" "$f"
200
+ elif cmp -s "$f" "$MODELE_SKILL"; then
201
+ rm -f "$f"
202
+ rmdir "$(dirname "$f")" 2>/dev/null || true
203
+ ok_t agents.skill_removed "$nom" "$f"
204
+ else
205
+ attention_t agents.skill_kept_remove "$nom" "$f"
206
+ fi
207
+ }
208
+
209
+ # Skill repowarden pour opencode : même règle
210
+ retirer_skill_opencode() {
211
+ local f="$fichier_skill_opencode" nom="opencode"
212
+ if [ ! -f "$f" ]; then
213
+ ok_t agents.skill_absent "$nom" "$f"
214
+ elif cmp -s "$f" "$MODELE_SKILL"; then
215
+ rm -f "$f"
216
+ rmdir "$(dirname "$f")" 2>/dev/null || true
217
+ ok_t agents.skill_removed "$nom" "$f"
218
+ else
219
+ attention_t agents.skill_kept_remove "$nom" "$f"
220
+ fi
221
+ }
222
+
167
223
  statut=0
168
224
  if [ "$action" = retirer ]; then
169
225
  [ -z "$claude" ] || retirer_claude || statut=1
170
226
  [ -z "$opencode" ] || retirer_opencode || statut=1
227
+ [ -z "$claude" ] || retirer_skill_claude || statut=1
228
+ [ -z "$opencode" ] || retirer_skill_opencode || statut=1
171
229
  exit "$statut"
172
230
  fi
173
231
  [ -z "$claude" ] || init_claude || statut=1
174
232
  [ -z "$opencode" ] || init_opencode || statut=1
233
+ [ -z "$claude" ] || poser_skill "$fichier_skill_claude" "Claude Code" || statut=1
234
+ [ -z "$opencode" ] || poser_skill "$fichier_skill_opencode" "opencode" || statut=1
175
235
 
176
236
  has repowarden || attention_t agents.no_repowarden
177
237
  # Hooks repowarden inactifs : le garde-fou refusera les commits, autant le
package/bin/garde CHANGED
@@ -133,8 +133,22 @@ protegee() {
133
133
  [ -n "$REPLY" ] && [ "$REPLY" = "$1" ]
134
134
  }
135
135
 
136
+ # Vrai si le chemin $1 (absolu), dans le dépôt du dossier courant $2 (son
137
+ # dossier existant le plus proche), est ignoré par git : jamais versionné.
138
+ # Un fichier suivi n'est jamais ignoré (git check-ignore sans --no-index)
139
+ fichier_ignore() {
140
+ local reste="${1#"$2"}" prefixe cdup
141
+ reste="${reste#/}"
142
+ [ -n "$reste" ] || return 1
143
+ # Relatif à la racine par --show-prefix/--show-cdup : ni lien symbolique
144
+ # (/tmp → /private/tmp) ni forme Windows (C:/… contre /c/…) à comparer
145
+ prefixe="$(git rev-parse --show-prefix 2>/dev/null)" || return 1
146
+ cdup="$(git rev-parse --show-cdup 2>/dev/null)" || return 1
147
+ git -C "./$cdup" check-ignore -q -- "$prefixe$reste" 2>/dev/null
148
+ }
149
+
136
150
  # Modification du fichier $1 : seulement sur une branche de travail (de ticket,
137
- # si le dépôt suit ses tickets)
151
+ # si le dépôt suit ses tickets) ; un fichier ignoré par git l'est partout
138
152
  verifier_fichier() {
139
153
  dossier_r "$1"
140
154
  # Sous Windows, jq finit le chemin lu par un CR (retiré) et le chemin peut
@@ -146,6 +160,8 @@ verifier_fichier() {
146
160
  depot_repowarden || exit 0
147
161
  # Fichiers internes de git (.git/…) : hors du travail de l'agent
148
162
  case "$(pwd -P)/" in "$(cd "$(git rev-parse --git-dir)" && pwd -P)"/*) exit 0 ;; esac
163
+ # Fichier ignoré par git : jamais versionné, règle de branche non applicable
164
+ ! fichier_ignore "$f" "$d" || exit 0
149
165
  if [ -f "$f" ]; then
150
166
  FICHIER_CARTE="$(cd "$(dirname "$f")" && pwd -P)/${f##*/}"
151
167
  FICHIER_CARTE="${FICHIER_CARTE#"$(cd "$(git rev-parse --show-toplevel)" && pwd -P)"/}"
@@ -267,11 +283,38 @@ ticket_valide() {
267
283
  [ "$(head -n 1 <<<"$lu")" = OPEN ] && grep -qxF "$VALIDE" <<<"$lu"
268
284
  }
269
285
 
286
+ # Vrai si le texte $1 vise une étiquette de décision du mainteneur (validé,
287
+ # statut: …, à décider), même encodée dans une URL (valid%C3%A9)
288
+ etiquette_decision() {
289
+ local t
290
+ t="$(printf '%b' "${1//%/\\x}")"
291
+ [[ "$t" == *"$VALIDE"* || "$t" == *"statut:"* || "$t" == *"$A_DECIDER"* ]]
292
+ }
293
+
294
+ # Vrai si l'écriture gh api (méthode $1, chemin $2, mots $3, corps lu d'un
295
+ # fichier si $4) pose, retire ou remplace les étiquettes de décision d'un
296
+ # ticket : refusée même avec un ticket validé (#496). Dans le doute, refuser
297
+ api_etiquettes_decision() {
298
+ local m="${1:-POST}" re_etiq='/issues/[0-9]+/labels(/|\?|$)' re_ticket='/issues/[0-9]+/?(\?|$)'
299
+ if [[ "/$2" =~ $re_etiq ]]; then
300
+ # PUT remplace l'ensemble, DELETE sans nom le vide : validé compris
301
+ [ "$m" != PUT ] || return 0
302
+ [ "$m" != DELETE ] || [[ "$2" == */labels/?* ]] || return 0
303
+ [ -z "$4" ] || return 0
304
+ etiquette_decision "$2 $3"
305
+ elif [[ "/$2" =~ $re_ticket ]]; then
306
+ # PATCH du ticket : un champ labels remplace l'ensemble
307
+ [ -n "$4" ] || [[ "$3" == *labels* ]]
308
+ else
309
+ return 1
310
+ fi
311
+ }
312
+
270
313
  # Commande gh (mots $@, sans « gh ») lancée depuis $dir, préfixe
271
314
  # REPOWARDEN_TICKET=$ticket_gh : lectures connues permises, écritures du circuit
272
315
  # par repowarden, décisions au mainteneur, tout le reste avec un ticket validé
273
316
  verifier_gh() {
274
- local ligne="gh $*" pos=() depot="" methode="" champs="" approuve="" mutation="" ecriture="" hors="" rc
317
+ local ligne="gh $*" mots="$*" pos=() depot="" methode="" champs="" corps="" approuve="" mutation="" ecriture="" hors="" rc
275
318
  while [ $# -gt 0 ]; do
276
319
  case "$1" in
277
320
  -R | --repo) shift; depot="${1:-}" ;;
@@ -279,7 +322,8 @@ verifier_gh() {
279
322
  -X | --method) shift; methode="${1:-}" ;;
280
323
  -X?*) methode="${1#-X}" ;;
281
324
  --method=*) methode="${1#--method=}" ;;
282
- -f | -F | --field | --raw-field | --input | -f?* | -F?* | --field=* | --raw-field=* | --input=*) champs=1 ;;
325
+ --input | --input=*) champs=1 corps=1 ;;
326
+ -f | -F | --field | --raw-field | -f?* | -F?* | --field=* | --raw-field=*) champs=1 ;;
283
327
  -a | --approve) approuve=1 ;;
284
328
  -h | --help) return 0 ;;
285
329
  -*) ;;
@@ -314,6 +358,7 @@ verifier_gh() {
314
358
  "api "*)
315
359
  [ -n "$ecriture" ] || exit 0
316
360
  [[ "${pos[1]:-}" != *pending_deployments* ]] || refuser garde.gh_mainteneur "$ligne"
361
+ ! api_etiquettes_decision "$methode" "${pos[1]:-}" "$mots" "$corps" || refuser garde.etiquette_mainteneur "$ligne"
317
362
  hors=1
318
363
  ;;
319
364
  # Lectures connues et description de la PR : permises ; tout le
@@ -335,14 +380,93 @@ verifier_gh() {
335
380
  ) || exit $?
336
381
  }
337
382
 
338
- # Découpe $1 en une commande par ligne (REPLY) : seul le « & » d'une vraie
339
- # redirection (N>&M, >&2, >&-, &>, &>>, non échappée) reste dans la commande,
340
- # citée entière ; partout ailleurs il sépare. Dans le doute couper : au pire une
341
- # commande de trop, jamais une masquée (true \>&git push -f lance git).
383
+ # repowarden ticket label <n> <label> [--remove] (mots $@ après « repowarden »),
384
+ # lancé depuis $dir : poser ou retirer « validé » ou un statut valide le ticket,
385
+ # décision du mainteneur refusée même avec REPOWARDEN_TICKET (incident #477)
386
+ verifier_ticket_label() {
387
+ local ligne="repowarden $*" mot decision=""
388
+ case "$1" in
389
+ ticket) [ "${2:-}" = label ] || return 0; shift 2 ;;
390
+ *) shift ;;
391
+ esac
392
+ [ $# -ge 2 ] || return 0
393
+ lecture_seule "$@" && return 0
394
+ # Mots après le numéro : une étiquette entre guillemets y est découpée
395
+ # (« statut: en cours » → statut:), dans le doute refuser
396
+ shift
397
+ for mot in "$@"; do
398
+ etiquette_decision "$mot" && decision=1
399
+ done
400
+ # Étiquette de décision en plusieurs mots (« à décider »), découpée par read -a
401
+ etiquette_decision "$*" && decision=1
402
+ [ -n "$decision" ] || return 0
403
+ dossier_r "$dir"
404
+ (
405
+ cd "$REPLY" 2>/dev/null || exit 0
406
+ depot_repowarden || exit 0
407
+ refuser garde.ticket_mainteneur "$ligne"
408
+ ) || exit $?
409
+ }
410
+
411
+ # repowarden ticket nouveau/new (mots $@), depuis $dir : créer un ticket est
412
+ # une décision du mainteneur, sauf agentsTickets ; --help reste permis (#496)
413
+ verifier_ticket_nouveau() {
414
+ local ligne="repowarden $*"
415
+ [ "${1:-}" = ticket ] || return 0
416
+ case "${2:-}" in nouveau | new) ;; *) return 0 ;; esac
417
+ lecture_seule "$@" && return 0
418
+ dossier_r "$dir"
419
+ (
420
+ cd "$REPLY" 2>/dev/null || exit 0
421
+ depot_repowarden || exit 0
422
+ cfg_r agentsTickets false
423
+ [ "$REPLY" != true ] || exit 0
424
+ refuser garde.ticket_nouveau "$ligne"
425
+ ) || exit $?
426
+ }
427
+
428
+ # Découpe $1 en une commande par ligne (REPLY) : « ; », « | », « && », « || » et
429
+ # le « & » séparateur coupent hors guillemets simples et doubles (antislash
430
+ # compris) ; seul le « & » d'une vraie redirection (N>&M, >&2, >&-, &>, &>>, non
431
+ # échappée) reste dans la commande, citée entière. Dans le doute couper : au pire
432
+ # une commande de trop, jamais une masquée (true \>&git push -f lance git).
433
+ # Entre guillemets le retour à la ligne devient une espace : le contenu exact
434
+ # d'une chaîne n'importe pas pour juger la commande.
342
435
  decouper_lignes() {
343
- local s="$1" out="" c i j prec avant nl=$'\n'
436
+ local s="$1" out="" c q="" i j prec avant nl=$'\n'
344
437
  for ((i = 0; i < ${#s}; i++)); do
345
438
  c="${s:i:1}"
439
+ if [ -n "$q" ]; then
440
+ if [ "$c" = "$nl" ]; then
441
+ out="$out "
442
+ continue
443
+ fi
444
+ out="$out$c"
445
+ if [ "$c" = "$q" ]; then
446
+ q=""
447
+ elif [ "$c" = "\\" ] && [ "$q" = '"' ]; then
448
+ i=$((i + 1))
449
+ out="$out${s:i:1}"
450
+ fi
451
+ continue
452
+ fi
453
+ case "$c" in
454
+ "'" | '"')
455
+ q="$c"
456
+ out="$out$c"
457
+ continue
458
+ ;;
459
+ "\\")
460
+ out="$out$c"
461
+ i=$((i + 1))
462
+ out="$out${s:i:1}"
463
+ continue
464
+ ;;
465
+ ";" | "|")
466
+ out="$out$nl"
467
+ continue
468
+ ;;
469
+ esac
346
470
  if [ "$c" != "&" ]; then
347
471
  out="$out$c"
348
472
  continue
@@ -378,28 +502,72 @@ decouper_lignes() {
378
502
  REPLY="$out"
379
503
  }
380
504
 
381
- # Ligne de commande shell : chaque commande (&&, ||, ;, |, retour à la ligne),
382
- # « cd » suivi, variables d'environnement en tête ignorées (sauf
383
- # REPOWARDEN_TICKET, le ticket d'une écriture gh hors du circuit des PR)
505
+ # Découpe $1 en mots (MOTS) en respectant guillemets simples et doubles et
506
+ # antislash, sans sous-processus ; guillemet non fermé : MOTS vide, code 1.
507
+ decouper_mots() {
508
+ local s="$1" mot="" q="" c i n
509
+ MOTS=()
510
+ n=${#s}
511
+ for ((i = 0; i < n; i++)); do
512
+ c="${s:i:1}"
513
+ if [ -n "$q" ]; then
514
+ if [ "$c" = "$q" ]; then
515
+ q=""
516
+ elif [ "$c" = "\\" ] && [ "$q" = '"' ]; then
517
+ case "${s:i+1:1}" in
518
+ '"' | "\\" | '$' | '`') i=$((i + 1)); mot="$mot${s:i:1}" ;;
519
+ *) mot="$mot\\" ;;
520
+ esac
521
+ else
522
+ mot="$mot$c"
523
+ fi
524
+ continue
525
+ fi
526
+ case "$c" in
527
+ "'" | '"') q="$c" ;;
528
+ "\\")
529
+ i=$((i + 1))
530
+ mot="$mot${s:i:1}"
531
+ ;;
532
+ " " | $'\t')
533
+ [ -z "$mot" ] || { MOTS+=("$mot"); mot=""; }
534
+ ;;
535
+ *) mot="$mot$c" ;;
536
+ esac
537
+ done
538
+ if [ -n "$q" ]; then
539
+ MOTS=()
540
+ return 1
541
+ fi
542
+ [ -z "$mot" ] || MOTS+=("$mot")
543
+ }
544
+
545
+ # Ligne de commande shell : chaque commande (« ; », « | », « && », « || » et le
546
+ # « & » séparateur, hors guillemets), « cd » suivi, variables d'environnement en
547
+ # tête ignorées (sauf REPOWARDEN_TICKET, le ticket d'une écriture gh hors du
548
+ # circuit des PR)
384
549
  verifier_shell() {
385
- local commande="$1" segment mots m cmd_cc nl=$'\n' lignes ticket_gh
550
+ local commande="$1" segment m cmd_cc lignes ticket_gh
386
551
  dir="$cwd"
387
- # Une commande par ligne. Découpage hors de <( … ) : bash 3.2 (macOS) y
388
- # lit mal ces motifs (« bad substitution ») et ne vérifierait plus rien
389
- lignes="${commande//&&/$nl}"
390
- lignes="${lignes//||/$nl}"
391
- lignes="${lignes//;/$nl}"
392
- lignes="${lignes//|/$nl}"
393
- # Un « & » de redirection (2>&1, >&2, &>log…) n'est pas un séparateur : la
394
- # commande citée dans le refus et celle proposée restent entières
395
- decouper_lignes "$lignes"
552
+ # Une commande par ligne, en un seul parcours caractère par caractère qui
553
+ # respecte les guillemets (pas de <( … ) : bash 3.2 le lit mal).
554
+ decouper_lignes "$commande"
396
555
  lignes="$REPLY"
397
556
  # Plusieurs commandes : un refus vaut pour toute la ligne (dit à l'agent)
398
557
  [ "$(grep -c '[^[:space:]]' <<<"$lignes")" -le 1 ] || LIGNE_MULTIPLE=1
399
558
  while IFS= read -r segment; do
400
- # shellcheck disable=SC2162 # les antislashs restent tels quels, comme dans la commande
401
- read -a mots <<<"$segment" || true
402
- set -- ${mots[@]+"${mots[@]}"}
559
+ # Guillemet non fermé : commande illisible, refusée dans un dépôt
560
+ # repowarden (dans le doute, refuser) ; ailleurs, tout est permis
561
+ if ! decouper_mots "$segment"; then
562
+ dossier_r "$dir"
563
+ (
564
+ cd "$REPLY" 2>/dev/null || exit 0
565
+ depot_repowarden || exit 0
566
+ refuser garde.commande_illisible "$segment"
567
+ ) || exit $?
568
+ continue
569
+ fi
570
+ set -- ${MOTS[@]+"${MOTS[@]}"}
403
571
  ticket_gh=""
404
572
  while [ $# -gt 0 ]; do
405
573
  case "$1" in
@@ -409,7 +577,13 @@ verifier_shell() {
409
577
  esac
410
578
  done
411
579
  [ $# -gt 0 ] || continue
580
+ # Commande : dernier élément du chemin (/ ou \), sans .exe/.cmd/.bat
412
581
  m="${1//[\"\']/}"
582
+ m="${m##*/}"
583
+ m="${m##*\\}"
584
+ case "$m" in
585
+ *.exe | *.EXE | *.cmd | *.CMD | *.bat | *.BAT) m="${m%.*}" ;;
586
+ esac
413
587
  case "$m" in
414
588
  cd)
415
589
  case "${2:-}" in
@@ -419,21 +593,28 @@ verifier_shell() {
419
593
  *) dir="$dir/${2//[\"\']/}" ;;
420
594
  esac
421
595
  ;;
422
- git | */git)
596
+ git)
423
597
  shift
424
598
  verifier_git "${@//[\"\']/}"
425
599
  ;;
426
- gh | */gh)
600
+ gh)
427
601
  shift
428
602
  verifier_gh "${@//[\"\']/}"
429
603
  ;;
430
- repowarden | */repowarden)
604
+ repowarden)
605
+ shift
606
+ verifier_ticket_nouveau "${@//[\"\']/}"
607
+ set -- repowarden "$@"
431
608
  case "${2//[\"\']/}" in
432
609
  cc | commit)
433
610
  cmd_cc="repowarden ${2//[\"\']/}"
434
611
  shift 2
435
612
  lecture_seule "${@//[\"\']/}" || verifier_cc "$cmd_cc"
436
613
  ;;
614
+ ticket | ticket-label)
615
+ shift
616
+ verifier_ticket_label "${@//[\"\']/}"
617
+ ;;
437
618
  esac
438
619
  ;;
439
620
  esac
package/bin/statut CHANGED
@@ -177,11 +177,15 @@ if agents_sans_garde "$GIT_TOPLEVEL"; then
177
177
  attention_t st.agents_sans_garde "$cmd_agents"
178
178
  suivant="${suivant:-$cmd_agents}"
179
179
  fi
180
- # Garde-fou posé par une version antérieure de repowarden : init le met à jour
181
- while read -r portee _agent fichier; do
180
+ # Garde-fou ou skill posés par une version antérieure de repowarden : init le met à jour
181
+ while read -r portee agent fichier; do
182
182
  cmd_agents="repowarden agents init"
183
183
  [ "$portee" = projet ] || cmd_agents="$cmd_agents --global"
184
- attention_t st.agents_stale "$fichier" "$cmd_agents"
184
+ if [ "$agent" = skill-claude ] || [ "$agent" = skill-opencode ]; then
185
+ attention_t st.agents_skill_stale "$fichier" "$cmd_agents"
186
+ else
187
+ attention_t st.agents_stale "$fichier" "$cmd_agents"
188
+ fi
185
189
  suivant="${suivant:-$cmd_agents}"
186
190
  done < <(agents_perimes "$GIT_TOPLEVEL")
187
191
 
@@ -267,6 +267,10 @@ description_pr_brouillon_r() {
267
267
  REPLY="$corps${corps:+$'\n\n'}Ticket : #$n"
268
268
  }
269
269
 
270
+ # Numéro de la PR ouverte depuis la branche $1, vide sinon ; relu juste avant
271
+ # la création pour ne jamais ouvrir deux PR pour une même branche
272
+ pr_ouverte() { gh pr list --state open --head "$1" --json number -q '.[0].number // empty'; }
273
+
270
274
  # REPLY = compte à assigner à la PR brouillon du ticket $1 : l'auteur du push
271
275
  # s'il est une personne (GitHub refuse d'assigner un bot avec un jeton d'App),
272
276
  # sinon le premier assigné du ticket, sinon vide
@@ -296,7 +300,7 @@ cmd_push() {
296
300
  [ -n "$n" ] || return 0
297
301
  # Branche faite hors du ticket (à la main) : inscrite sur le ticket
298
302
  inscrire_branche "$n" "$PUSH_BRANCH"
299
- pr="$(gh pr list --state open --head "$PUSH_BRANCH" --json number -q '.[0].number // empty')"
303
+ pr="$(pr_ouverte "$PUSH_BRANCH")"
300
304
  # PR existante : rien à faire si elle n'est plus en brouillon
301
305
  [ -z "$pr" ] || [ "$(gh pr view "$pr" --json isDraft -q .isDraft)" = true ] || return 0
302
306
  [ "$(gh api "repos/$GH_REPO/compare/$INTEGRATION...$PUSH_BRANCH" -q .ahead_by)" != 0 ] || return 0
@@ -309,6 +313,12 @@ cmd_push() {
309
313
  notice_t tk.draft_updated "$pr" "$n"
310
314
  return 0
311
315
  fi
316
+ # Deux push rapprochés : un autre run a pu ouvrir la PR entre-temps
317
+ pr="$(pr_ouverte "$PUSH_BRANCH")"
318
+ if [ -n "$pr" ]; then
319
+ notice_t tk.draft_exists "$pr" "$n"
320
+ return 0
321
+ fi
312
322
  url="$(gh pr create --draft --base "$INTEGRATION" --head "$PUSH_BRANCH" --title "$titre" \
313
323
  --body "$corps")"
314
324
  # assignation à part : un refus de GitHub ne doit pas faire échouer le suivi
@@ -4,7 +4,10 @@
4
4
  // absent ou en panne : rien n'est bloqué. opencode 1.x : repowarden-garde.js,
5
5
  // à côté (un module à export par défaut y passerait pour un plugin 2.x).
6
6
  // Plugins opencode 2.x : export par défaut { id, setup }, crochet
7
- // ctx.tool.hook("execute.before") (outils edit, write, shell…)
7
+ // ctx.tool.hook("execute.before") (outils edit, write, shell…). opencode 1.19
8
+ // lit tout export par défaut qui a un id comme un plugin à server() : sans
9
+ // lui, « failed to load plugin » à chaque démarrage ; server() ne rend aucun
10
+ // crochet, le garde-fou 1.x reste celui de repowarden-garde.js
8
11
  import { spawnSync } from "node:child_process"
9
12
 
10
13
  // Soumet l'appel d'outil à repowarden garde ; l'erreur levée par le crochet
@@ -25,4 +28,5 @@ export default {
25
28
  const dossier = ctx.location?.directory || process.cwd()
26
29
  return ctx.tool.hook("execute.before", (appel) => garde(appel.tool, appel.input, dossier))
27
30
  },
31
+ server: async () => ({}),
28
32
  }
@@ -0,0 +1,41 @@
1
+ ---
2
+ name: repowarden
3
+ description: Repowarden skill for AI agents. Triggers on ticket, commit, branch, push, PR, CI, gh, forbidden action or guard refusal.
4
+ ---
5
+ <!-- repowarden:skill installed by repowarden agents init; remove this line to customize it -->
6
+
7
+ # Repowarden
8
+
9
+ ## Goal
10
+ Follow the repowarden workflow in the repo: no change without a ticket, written design, `repowarden cc`, draft PR opened by the bot, release by the maintainer.
11
+
12
+ ## Workflow
13
+ 1. **Ticket before any change** (including GitHub settings). Without a ticket, stop and ask for one: an agent only creates a ticket (`repowarden ticket nouveau "<title>"`) when asked to, and never validates one.
14
+ 2. **Design**: write a "Conception retenue" on the ticket (what, files touched, out of scope). Any change of plan must be noted on the ticket before or when it happens.
15
+ 3. **Take the ticket**: `repowarden ticket <n>`. Branch `type/<n>-sujet` is created from `develop`.
16
+ 4. **Sandbox tests**: use `./.essai/` for disposable repos without a remote. Delete them at the end.
17
+ 5. **Code and commit**: `git add <files>` (never `git add .`, `-A` or `--all`). Commit via `repowarden cc -m "<description only>" --type <type> --scope <scope> --body "<why>" --refs "Ticket : #<n>"` (French messages, header ≤ 72 chars, no signature/agent). No `--no-verify`.
18
+ 6. **Push**: `git push -u origin HEAD` without force. The bot opens a draft PR to `develop` with `Ticket : #<n>` (never `Closes #<n>`).
19
+ 7. **PR**: the maintainer marks it ready and releases; the agent follows `repowarden pr checks <n>` and reports when everything is green. An agent does not merge, does not mark a PR ready, does not close tickets. Only `develop` enters `main`, via the release PR.
20
+
21
+ ## Useful commands
22
+ - `repowarden ticket <n>` / `repowarden ticket comment <n> "<text>"`
23
+ - `repowarden cc --dry-run -m "<description>" --type <type> --scope <scope> --body "<why>" --refs "Ticket : #<n>"`
24
+ - `repowarden pr checks <n>`, `repowarden run rerun <id|--failed|--workflow>`
25
+ - `REPOWARDEN_TICKET=<n> gh …`: GitHub write outside the workflow, tied to ticket <n>
26
+ - `opencode run --auto "<instruction>" < /dev/null`: non-interactive opencode (without `--auto` every permission is denied, without `< /dev/null` it waits silently)
27
+
28
+ ## Guard
29
+ It refuses in particular:
30
+ - direct or forced push to `main`/`develop`;
31
+ - `git commit` outside `repowarden cc`;
32
+ - modifying a file on a protected branch or outside a ticket branch;
33
+ - `gh` write outside the workflow without a ticket;
34
+ - validating a ticket or changing its status.
35
+
36
+ If `repowarden garde` refuses an action: read the message, run exactly the command indicated, never bypass (`--no-verify`, `--admin`, exception). A quoted string containing a forbidden command must go in a file. If the refusal seems impossible given the ticket design, stop and explain on the ticket instead of guessing.
37
+
38
+ ## Rules
39
+ - French commit messages, no signature or co-author from an agent.
40
+ - Bash 3.2 compatible if touching shell scripts.
41
+ - A bug becomes a ticket; if linked to an open ticket, note it as "Ajout" there.
@@ -0,0 +1,41 @@
1
+ ---
2
+ name: repowarden
3
+ description: Skill repowarden pour agents IA. Déclenche sur ticket, commit, branche, push, PR, CI, gh, action interdite ou refus du garde-fou.
4
+ ---
5
+ <!-- repowarden:skill posée par repowarden agents init ; retirer cette ligne pour la personnaliser -->
6
+
7
+ # Repowarden
8
+
9
+ ## But
10
+ Appliquer le circuit repowarden dans le dépôt : pas de changement sans ticket, conception écrite, `repowarden cc`, PR brouillon par le bot, livraison par le mainteneur.
11
+
12
+ ## Circuit
13
+ 1. **Ticket créé avant tout changement** (même pour un réglage GitHub). Sans ticket, s'arrêter et en demander un : un agent ne crée un ticket (`repowarden ticket nouveau "<titre>"`) que si on le lui demande, et n'en valide jamais.
14
+ 2. **Conception** : noter la « Conception retenue » sur le ticket (quoi, fichiers touchés, hors périmètre). Tout écart est noté sur le ticket avant ou au moment.
15
+ 3. **Prendre le ticket** : `repowarden ticket <n>`. La branche `type/<n>-sujet` est créée depuis `develop`.
16
+ 4. **Cadrer les essais** : utiliser `./.essai/` pour des dépôts jetables sans remote. Les supprimer à la fin.
17
+ 5. **Coder et commiter** : `git add <fichiers>` (jamais `git add .`, `-A` ou `--all`). Message via `repowarden cc -m "<description seule>" --type <type> --scope <portée> --body "<pourquoi>" --refs "Ticket : #<n>"` (français, en-tête ≤ 72 caractères, sans signature/agent). Sans `--no-verify`.
18
+ 6. **Pousser** : `git push -u origin HEAD` sans forcer. Le bot ouvre une PR brouillon vers `develop` avec `Ticket : #<n>` (jamais `Closes #<n>`).
19
+ 7. **PR** : le mainteneur la passe en ready et livre ; l'agent suit `repowarden pr checks <n>` et signale quand tout est vert. Un agent ne merge pas, ne passe pas une PR en ready, ne ferme pas de ticket. Seul `develop` entre dans `main`, par la PR de livraison.
20
+
21
+ ## Commandes utiles
22
+ - `repowarden ticket <n>` / `repowarden ticket comment <n> "<texte>"`
23
+ - `repowarden cc --dry-run -m "<description>" --type <type> --scope <portée> --body "<pourquoi>" --refs "Ticket : #<n>"`
24
+ - `repowarden pr checks <n>`, `repowarden run rerun <id|--failed|--workflow>`
25
+ - `REPOWARDEN_TICKET=<n> gh …` : écriture GitHub hors circuit, rattachée au ticket <n>
26
+ - `opencode run --auto "<consigne>" < /dev/null` : opencode non interactif (sans `--auto` chaque permission est refusée, sans `< /dev/null` il attend en silence)
27
+
28
+ ## Garde-fou
29
+ Il refuse notamment :
30
+ - push direct ou forcé sur `main`/`develop` ;
31
+ - `git commit` hors `repowarden cc` ;
32
+ - modifier un fichier sur une branche protégée ou hors branche de ticket ;
33
+ - écriture `gh` hors circuit sans ticket ;
34
+ - valider un ticket ou changer son statut.
35
+
36
+ Si `repowarden garde` refuse une action : lire le message, appliquer exactement la commande indiquée, ne jamais contourner (`--no-verify`, `--admin`, exception). Un texte entre guillemets citant une commande interdite doit aller dans un fichier. Si le refus semble impossible au regard de la conception du ticket, s'arrêter et l'expliquer sur le ticket, pas d'invention.
37
+
38
+ ## Règles
39
+ - Messages français, sans signature ni co-auteur d'agent.
40
+ - Bash 3.2 compatible si on touche des scripts shell.
41
+ - Un défaut devient un ticket ; s'il est lié à un ticket ouvert, le noter en « Ajout » dessus.
@@ -11,6 +11,15 @@ AGENTS_PLUGINS_OPENCODE="repowarden-garde.js repowarden-garde-v2.js"
11
11
  AGENTS_COMMANDE_CLAUDE="repowarden garde --format claude"
12
12
  AGENTS_MATCHER_CLAUDE="Edit|Write|MultiEdit|NotebookEdit|Bash"
13
13
 
14
+ # Modèles de la skill repowarden selon la langue (fr/en)
15
+ agents_modele_skill() {
16
+ local lang="${REPOWARDEN_LANG:-fr}"
17
+ case "$lang" in
18
+ en) echo "$(cd "$(dirname "${BASH_SOURCE[0]}")/../agents" && pwd -P)/skill-repowarden.en.md" ;;
19
+ *) echo "$(cd "$(dirname "${BASH_SOURCE[0]}")/../agents" && pwd -P)/skill-repowarden.fr.md" ;;
20
+ esac
21
+ }
22
+
14
23
  # Dossiers de configuration de l'utilisateur, là où chaque outil les lit
15
24
  # (CLAUDE_CONFIG_DIR ; OPENCODE_CONFIG_DIR, sinon XDG_CONFIG_HOME)
16
25
  agents_dossiers_globaux() {
@@ -47,13 +56,27 @@ agents_claude_perime() {
47
56
  # Garde-fous périmés du dépôt $1 (vide hors dépôt) puis de l'utilisateur, une
48
57
  # ligne « portée agent fichier » chacun : statut les signale, install les met à jour
49
58
  agents_perimes() {
59
+ local modele
60
+ modele="$(agents_modele_skill)"
50
61
  agents_dossiers_globaux
51
62
  if [ -n "${1:-}" ]; then
52
63
  ! agents_claude_perime "$1/.claude/settings.json" || echo "projet claude $1/.claude/settings.json"
53
64
  agents_opencode_perimes projet "$1/.opencode/plugins"
65
+ if agents_skill_perime "$1/.claude/skills/repowarden/SKILL.md" "$modele"; then
66
+ echo "projet skill-claude $1/.claude/skills/repowarden/SKILL.md"
67
+ fi
68
+ if agents_skill_perime "$1/.opencode/skills/repowarden/SKILL.md" "$modele"; then
69
+ echo "projet skill-opencode $1/.opencode/skills/repowarden/SKILL.md"
70
+ fi
54
71
  fi
55
72
  ! agents_claude_perime "$AGENTS_CLAUDE_DIR/settings.json" || echo "global claude $AGENTS_CLAUDE_DIR/settings.json"
56
73
  agents_opencode_perimes global "$AGENTS_OPENCODE_DIR/plugins"
74
+ if agents_skill_perime "$AGENTS_CLAUDE_DIR/skills/repowarden/SKILL.md" "$modele"; then
75
+ echo "global skill-claude $AGENTS_CLAUDE_DIR/skills/repowarden/SKILL.md"
76
+ fi
77
+ if agents_skill_perime "$AGENTS_OPENCODE_DIR/skills/repowarden/SKILL.md" "$modele"; then
78
+ echo "global skill-opencode $AGENTS_OPENCODE_DIR/skills/repowarden/SKILL.md"
79
+ fi
57
80
  return 0
58
81
  }
59
82
 
@@ -77,8 +100,8 @@ agents_sans_garde() {
77
100
  agents_dossiers_globaux
78
101
  command -v claude >/dev/null 2>&1 || command -v opencode >/dev/null 2>&1 ||
79
102
  [ -d "$AGENTS_CLAUDE_DIR" ] || [ -d "$AGENTS_OPENCODE_DIR" ] || return 1
80
- [ -z "${1:-}" ] || fichiers=("$1/.claude/settings.json" "$1/.opencode/plugins/repowarden-garde.js")
81
- fichiers+=("$AGENTS_CLAUDE_DIR/settings.json" "$AGENTS_OPENCODE_DIR/plugins/repowarden-garde.js")
103
+ [ -z "${1:-}" ] || fichiers=("$1/.claude/settings.json" "$1/.opencode/plugins/repowarden-garde.js" "$1/.opencode/plugins/repowarden-garde-v2.js")
104
+ fichiers+=("$AGENTS_CLAUDE_DIR/settings.json" "$AGENTS_OPENCODE_DIR/plugins/repowarden-garde.js" "$AGENTS_OPENCODE_DIR/plugins/repowarden-garde-v2.js")
82
105
  # Plugin repowarden d'une version antérieure : branché (périmé, pas absent)
83
106
  for f in "${fichiers[@]}"; do
84
107
  ! grep -qs "repowarden garde" "$f" || return 1
@@ -86,3 +109,16 @@ agents_sans_garde() {
86
109
  done
87
110
  return 0
88
111
  }
112
+
113
+ # Vrai si le fichier skill $1 a été posé par repowarden (repère)
114
+ agents_skill_repowarden() {
115
+ grep -q '<!-- repowarden:skill' "$1" 2>/dev/null
116
+ }
117
+
118
+ # Vrai si la skill $1 vient de repowarden mais diffère du modèle de la langue
119
+ agents_skill_perime() {
120
+ local modele="$2"
121
+ [ -f "$1" ] || return 1
122
+ agents_skill_repowarden "$1" || return 1
123
+ ! cmp -s "$1" "$modele"
124
+ }
@@ -131,7 +131,7 @@ _msg_en() {
131
131
  help.migrer) _M="Usage: repowarden migrer [--pr] [--hooks]\n\nMigrates from repogarde to repowarden (docs/migration-v4.md): configs,\nworkflows, renames, lists manual steps left. Safe to rerun.\n --pr on chore/migration-repowarden-vN: commit, push and PR\n --hooks also move leftover lefthook hooks to .git/hooks-lefthook/" ;;
132
132
  help.npm-publication) _M="Usage: repowarden npm-publication [--repo owner/name] [--workflow release.yml]\n [--environnement NAME] [--dry-run]\n\nSets up automatic npm publishing per release via OIDC (trusted publishing,\nprovenance). Needs npm logged in, package exists; if interrupted resumes.\nWorkflow: release.yml publishes via npm-publish.yml." ;;
133
133
  help.garde) _M="Usage: repowarden garde [--format claude|opencode] (reads the action on standard input)\n\nAI agent guard, called by the agent's hook before each action (repowarden\nagents init wires it): exit code 0 allowed, 2 refused with the command to\nrun. In a repowarden repository: no change on a detached HEAD, on a\nprotected branch, or outside a ticket branch when the repository tracks its\ntickets; branches created by repowarden ticket <n>, commits by repowarden\ncc (repowarden hooks active, off protected branches); no --no-verify, no\ngit add . / -A / --all. Requires jq." ;;
134
- help.agents) _M="Usage: repowarden agents init [--claude] [--opencode] [--projet | --global]\n repowarden agents retirer [--claude] [--opencode] [--projet | --global]\n\nWires the guard (repowarden garde) into AI agents: Claude Code PreToolUse\nhook (.claude/settings.json), opencode plugins\n(.opencode/plugins/repowarden-garde.js for opencode 1.x,\nrepowarden-garde-v2.js for 2.x). Without --claude or --opencode:\nboth. --projet (default): repository files, to commit; --global: user\nconfiguration (\$CLAUDE_CONFIG_DIR, ~/.claude by default;\n\$OPENCODE_CONFIG_DIR, else \$XDG_CONFIG_HOME/opencode, ~/.config/opencode by\ndefault). Re-runnable: existing files are completed, never overwritten.\nretirer (or remove): removes only what init wrote." ;;
134
+ help.agents) _M="Usage: repowarden agents init [--claude] [--opencode] [--projet | --global]\n repowarden agents retirer [--claude] [--opencode] [--projet | --global]\n\nWires the guard (repowarden garde) into AI agents: Claude Code PreToolUse\nhook (.claude/settings.json), opencode plugins\n(.opencode/plugins/repowarden-garde.js for opencode 1.x,\nrepowarden-garde-v2.js for 2.x). Also places the repowarden skill\n(.claude/skills/repowarden/SKILL.md, .opencode/skills/repowarden/SKILL.md)\nin the repowarden language. Without --claude or --opencode:\nboth. --projet (default): repository files, to commit; --global: user\nconfiguration (\$CLAUDE_CONFIG_DIR, ~/.claude by default;\n\$OPENCODE_CONFIG_DIR, else \$XDG_CONFIG_HOME/opencode, ~/.config/opencode by\ndefault). Re-runnable: existing files are completed, never overwritten.\nretirer (or remove): removes only what init wrote." ;;
135
135
  help.app) _M="Usage: repowarden app init [--repo owner/name] [--nom NAME] [--administration]\n repowarden app init --bot\n\nCreates/sets up a GitHub App for repowarden workflows (Actions token doesn't\ntrigger other workflows; App token does). Browser-based manifest, minimal\npermissions, credentials stored securely, installed on repo. --bot for org\nbot (pilot repo). Needs gh and node. Rerunnable." ;;
136
136
  # Hooks and language plugins
137
137
  hook.pre_commit.1) _M="Commit refused: branch name does not follow the convention." ;;
@@ -530,6 +530,7 @@ _msg_en() {
530
530
  st.tools) _M="Tools present: %s · missing: %s (depending on the projects' languages)" ;;
531
531
  st.agents_sans_garde) _M="AI agent without the guard (Claude Code, opencode): its actions are only checked at commit time. → %s" ;;
532
532
  st.agents_stale) _M="Outdated repowarden guard (%s): wired by an earlier version of repowarden. → %s" ;;
533
+ st.agents_skill_stale) _M="Outdated repowarden skill (%s): placed by an earlier version of repowarden. → %s" ;;
533
534
  st.next) _M="Next step:" ;;
534
535
  st.carte_absente) _M="No project map (optional, read by agents before changing a file): repowarden carte" ;;
535
536
  st.carte_ok) _M="Project map up to date (repowarden carte <file>)" ;;
@@ -689,6 +690,7 @@ _msg_en() {
689
690
  tk.branch_kept) _M="Branch \`%s\` kept: %s commit(s) missing from the integration branch, open PR(s): %s. Delete it by hand if the work lives elsewhere." ;;
690
691
  tk.draft_opened) _M="Draft PR #%s opened for ticket #%s" ;;
691
692
  tk.draft_updated) _M="PR #%s (ticket #%s): title and description taken from the ticket" ;;
693
+ tk.draft_exists) _M="PR #%s already open for ticket #%s (close pushes): no other PR created" ;;
692
694
  tk.assign_failed) _M="PR #%s: GitHub refused to assign %s, PR left unassigned" ;;
693
695
  tk.pr_additions) _M="Ticket additions" ;;
694
696
  tk.pr_design) _M="Design (summary)" ;;
@@ -743,6 +745,7 @@ _msg_en() {
743
745
  garde.format) _M="Unknown format \"%s\": claude or opencode." ;;
744
746
  garde.no_jq) _M="repowarden garde: jq missing, action not checked (install jq: https://jqlang.org)." ;;
745
747
  garde.json) _M="repowarden garde: unreadable JSON input, action not checked." ;;
748
+ garde.commande_illisible) _M="command refused: \"%s\"; unclosed quote, unreadable command.\n → Fix the quotes (open and close them), then run it again." ;;
746
749
  garde.detache) _M="change refused: detached HEAD in %s.\n → Switch to a ticket's branch: repowarden ticket <n>" ;;
747
750
  garde.protegee) _M="change refused: \"%s\" is a protected branch.\n → Work on a ticket's branch: repowarden ticket <n>" ;;
748
751
  garde.protegee_libre) _M="change refused: \"%s\" is a protected branch.\n → Create a work branch: git switch -c <type>/<topic>" ;;
@@ -762,6 +765,9 @@ _msg_en() {
762
765
  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" ;;
763
766
  garde.gh_rerun) _M="command refused: \"%s\"; workflows are re-run through repowarden.\n → repowarden run rerun <run-id|--failed>" ;;
764
767
  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." ;;
768
+ garde.ticket_mainteneur) _M="command refused: \"%s\"; validating a ticket is the maintainer's decision (\"validé\" or status label, added or removed).\n → Ask the maintainer to validate the ticket, then take it: repowarden ticket <n>" ;;
769
+ garde.etiquette_mainteneur) _M="command refused: \"%s\"; changing a decision label of a ticket (\"validé\", status, \"à décider\"), even through the API, is the maintainer's decision.\n → Tell the maintainer about the request and leave the ticket or PR unchanged." ;;
770
+ garde.ticket_nouveau) _M="command refused: \"%s\"; creating a ticket is the maintainer's decision.\n → Report the defect to the maintainer, who creates the ticket; or note it on the current ticket. Setting repowarden.agentsTickets = true allows it." ;;
765
771
  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" ;;
766
772
  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 …" ;;
767
773
  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." ;;
@@ -777,6 +783,13 @@ _msg_en() {
777
783
  agents.removed) _M="%s: guard removed (%s)." ;;
778
784
  agents.absent) _M="%s: no guard to remove (%s)." ;;
779
785
  agents.kept_remove) _M="%s: %s differs from the template; kept (remove it by hand if no longer wanted)." ;;
786
+ agents.skill_added) _M="%s: repowarden skill added (%s)." ;;
787
+ agents.skill_present) _M="%s: repowarden skill already in place (%s)." ;;
788
+ agents.skill_updated) _M="%s: repowarden skill updated (%s); to customize it, remove its marker line (<!-- repowarden:skill …)." ;;
789
+ agents.skill_kept) _M="%s: repowarden skill (%s) differs from the template; kept as is (template: %s)." ;;
790
+ agents.skill_removed) _M="%s: repowarden skill removed (%s)." ;;
791
+ agents.skill_absent) _M="%s: no repowarden skill to remove (%s)." ;;
792
+ agents.skill_kept_remove) _M="%s: repowarden skill %s differs from the template; kept (remove it by hand if no longer wanted)." ;;
780
793
  *) _M="" ;;
781
794
  esac
782
795
  }
@@ -254,7 +254,7 @@ _msg_fr() {
254
254
  help.migrer) _M="Usage : repowarden migrer [--pr] [--hooks]\n\nPassage de repogarde à repowarden dans le projet courant\n(docs/migration-v4.md) : configuration (.repowarden.conf, .repowarden/),\nworkflows (uses: vers le dépôt repowarden et sa version majeure, anciens\nnoms), puis la liste de ce qui reste à faire à la main. Relançable : rien\nn'est fait deux fois.\n --pr sur la branche chore/migration-repowarden-vN : commit, push et PR\n --hooks déplace aussi les hooks lefthook résiduels dans .git/hooks-lefthook/" ;;
255
255
  help.npm-publication) _M="Usage : repowarden npm-publication [--repo propriétaire/nom] [--workflow release.yml]\n [--environnement NOM] [--dry-run]\n\nPublication npm automatique mise en place en une commande : chaque release\npublie le paquet (workflow npm-publish.yml), sans jeton (publication de\nconfiance OIDC, provenance jointe) : connexion npm, paquet existant, puis\nrelation de confiance. Interrompue, la commande reprend où elle en était." ;;
256
256
  help.garde) _M="Usage : repowarden garde [--format claude|opencode] (lit l'action sur l'entrée standard)\n\nGarde-fou des agents IA, appelé par leur hook avant chaque action\n(repowarden agents init le branche) : code 0 permis, code 2 refusé avec la\ncommande à lancer. Dans un dépôt repowarden : pas de modification en HEAD\ndétaché, sur une branche protégée, ni hors branche de ticket si le dépôt\nsuit ses tickets ; branche créée par repowarden ticket <n>, commit par\nrepowarden cc (hooks repowarden actifs, hors branche protégée) ; ni\n--no-verify ni git add . / -A / --all. Demande jq." ;;
257
- help.agents) _M="Usage : repowarden agents init [--claude] [--opencode] [--projet | --global]\n repowarden agents retirer [--claude] [--opencode] [--projet | --global]\n\nBranche le garde-fou (repowarden garde) dans les agents IA : hook PreToolUse\nde Claude Code (.claude/settings.json), plugins opencode\n(.opencode/plugins/repowarden-garde.js pour opencode 1.x,\nrepowarden-garde-v2.js pour 2.x). Sans --claude ni --opencode : les\ndeux. --projet (défaut) : fichiers du dépôt, à versionner ; --global :\nconfiguration de l'utilisateur (\$CLAUDE_CONFIG_DIR, ~/.claude par défaut ;\n\$OPENCODE_CONFIG_DIR, sinon \$XDG_CONFIG_HOME/opencode, ~/.config/opencode par\ndéfaut). Relançable : fichiers existants complétés, jamais écrasés.\nretirer : enlève seulement ce qu'init a écrit." ;;
257
+ help.agents) _M="Usage : repowarden agents init [--claude] [--opencode] [--projet | --global]\n repowarden agents retirer [--claude] [--opencode] [--projet | --global]\n\nBranche le garde-fou (repowarden garde) dans les agents IA : hook PreToolUse\nde Claude Code (.claude/settings.json), plugins opencode\n(.opencode/plugins/repowarden-garde.js pour opencode 1.x,\nrepowarden-garde-v2.js pour 2.x). Pose aussi la skill repowarden\n(.claude/skills/repowarden/SKILL.md, .opencode/skills/repowarden/SKILL.md)\ndans la langue de repowarden. Sans --claude ni --opencode : les deux.\n--projet (défaut) : fichiers du dépôt, à versionner ; --global :\nconfiguration de l'utilisateur (\$CLAUDE_CONFIG_DIR, ~/.claude par défaut ;\n\$OPENCODE_CONFIG_DIR, sinon \$XDG_CONFIG_HOME/opencode, ~/.config/opencode par\ndéfaut). Relançable : fichiers existants complétés, jamais écrasés.\nretirer : enlève seulement ce qu'init a écrit." ;;
258
258
  help.app) _M="Usage : repowarden app init [--repo propriétaire/nom] [--nom NOM] [--administration]\n repowarden app init --bot\n\nGitHub App du dépôt : identité d'automatisation des workflows repowarden (le\njeton des Actions ne déclenche aucun workflow, un jeton de l'App, si).\nCréation dans le navigateur (flux « manifest »), permissions minimales,\nidentifiants rangés sans être affichés, installation sur le dépôt. --bot :\nApp du bot d'organisation, dans le dépôt de pilotage. Relançable ; demande\ngh et node." ;;
259
259
  # CI et protection
260
260
  ci.check.2) _M="Pas de base : vérification ignorée." ;;
@@ -531,6 +531,7 @@ _msg_fr() {
531
531
  st.tools) _M="Outils présents : %s · absents : %s (selon les langages des projets)" ;;
532
532
  st.agents_sans_garde) _M="Agent IA sans garde-fou (Claude Code, opencode) : ses actions ne sont vérifiées qu'au commit. → %s" ;;
533
533
  st.agents_stale) _M="Garde-fou repowarden périmé (%s) : posé par une version antérieure de repowarden. → %s" ;;
534
+ st.agents_skill_stale) _M="Skill repowarden périmée (%s) : posée par une version antérieure de repowarden. → %s" ;;
534
535
  st.next) _M="Étape suivante :" ;;
535
536
  st.carte_absente) _M="Carte du projet absente (facultative, lue par les agents avant de modifier un fichier) : repowarden carte" ;;
536
537
  st.carte_ok) _M="Carte du projet à jour (repowarden carte <fichier>)" ;;
@@ -690,6 +691,7 @@ _msg_fr() {
690
691
  tk.branch_kept) _M="Branche \`%s\` gardée : %s commit(s) absent(s) de la branche d'intégration, PR ouverte(s) : %s. À supprimer à la main si le travail est ailleurs." ;;
691
692
  tk.draft_opened) _M="PR #%s ouverte en brouillon pour le ticket #%s" ;;
692
693
  tk.draft_updated) _M="PR #%s (ticket #%s) : titre et description repris du ticket" ;;
694
+ tk.draft_exists) _M="PR #%s déjà ouverte pour le ticket #%s (push rapproché) : aucune autre PR créée" ;;
693
695
  tk.assign_failed) _M="PR #%s : assignation de %s refusée par GitHub, PR laissée sans assigné" ;;
694
696
  tk.pr_additions) _M="Ajouts du ticket" ;;
695
697
  tk.pr_design) _M="Conception (résumé)" ;;
@@ -744,6 +746,7 @@ _msg_fr() {
744
746
  garde.format) _M="Format inconnu « %s » : claude ou opencode." ;;
745
747
  garde.no_jq) _M="repowarden garde : jq absent, action non vérifiée (installer jq : https://jqlang.org)." ;;
746
748
  garde.json) _M="repowarden garde : entrée JSON illisible, action non vérifiée." ;;
749
+ garde.commande_illisible) _M="commande refusée : « %s » ; guillemet non fermé, commande illisible.\n → Corriger les guillemets (ouvrir et fermer), puis relancer." ;;
747
750
  garde.detache) _M="modification refusée : HEAD détaché dans %s.\n → Se placer sur la branche d'un ticket : repowarden ticket <n>" ;;
748
751
  garde.protegee) _M="modification refusée : « %s » est une branche protégée.\n → Travailler sur la branche d'un ticket : repowarden ticket <n>" ;;
749
752
  garde.protegee_libre) _M="modification refusée : « %s » est une branche protégée.\n → Créer une branche de travail : git switch -c <type>/<sujet>" ;;
@@ -763,6 +766,9 @@ _msg_fr() {
763
766
  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" ;;
764
767
  garde.gh_rerun) _M="commande refusée : « %s » ; un workflow se relance par repowarden.\n → repowarden run rerun <run-id|--failed>" ;;
765
768
  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." ;;
769
+ garde.ticket_mainteneur) _M="commande refusée : « %s » ; valider un ticket est une décision du mainteneur (étiquette « validé » ou de statut, ajout comme retrait).\n → Demander au mainteneur de valider le ticket, puis le prendre : repowarden ticket <n>" ;;
770
+ garde.etiquette_mainteneur) _M="commande refusée : « %s » ; toucher une étiquette de décision d'un ticket (« validé », statut, « à décider »), même par l'API, est une décision du mainteneur.\n → Annoncer la demande au mainteneur et laisser le ticket ou la PR inchangés." ;;
771
+ garde.ticket_nouveau) _M="commande refusée : « %s » ; créer un ticket est une décision du mainteneur.\n → Signaler le défaut au mainteneur, qui crée le ticket ; ou le noter sur le ticket en cours. Réglage repowarden.agentsTickets = true pour l'autoriser." ;;
766
772
  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" ;;
767
773
  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 …" ;;
768
774
  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." ;;
@@ -778,6 +784,13 @@ _msg_fr() {
778
784
  agents.removed) _M="%s : garde-fou retiré (%s)." ;;
779
785
  agents.absent) _M="%s : pas de garde-fou à retirer (%s)." ;;
780
786
  agents.kept_remove) _M="%s : %s diffère du modèle ; gardé (à retirer à la main s'il n'est plus voulu)." ;;
787
+ agents.skill_added) _M="%s : skill repowarden ajoutée (%s)." ;;
788
+ agents.skill_present) _M="%s : skill repowarden déjà en place (%s)." ;;
789
+ agents.skill_updated) _M="%s : skill repowarden mise à jour (%s) ; pour la personnaliser, retirer la ligne repère." ;;
790
+ agents.skill_kept) _M="%s : skill repowarden (%s) diffère du modèle ; gardée telle quelle (modèle : %s)." ;;
791
+ agents.skill_removed) _M="%s : skill repowarden retirée (%s)." ;;
792
+ agents.skill_absent) _M="%s : pas de skill repowarden à retirer (%s)." ;;
793
+ agents.skill_kept_remove) _M="%s : skill repowarden %s diffère du modèle ; gardée (à retirer à la main s'il n'est plus voulu)." ;;
781
794
  *) _M="" ;;
782
795
  esac
783
796
  }
package/install.sh CHANGED
@@ -257,6 +257,17 @@ fi
257
257
  # question (déjà branché par lui), une ligne par agent
258
258
  while read -r portee agent fichier; do
259
259
  [ "$portee" = global ] || continue
260
+ if [ "$agent" = skill-claude ]; then
261
+ bash "$ROOT/bin/agents" init --global --claude >/dev/null 2>&1 || continue
262
+ t agents.skill_updated "Claude Code" "$fichier"
263
+ ok "$REPLY"
264
+ continue
265
+ elif [ "$agent" = skill-opencode ]; then
266
+ bash "$ROOT/bin/agents" init --global --opencode >/dev/null 2>&1 || continue
267
+ t agents.skill_updated opencode "$fichier"
268
+ ok "$REPLY"
269
+ continue
270
+ fi
260
271
  bash "$ROOT/bin/agents" init --global "--$agent" >/dev/null 2>&1 || continue
261
272
  nom=opencode
262
273
  [ "$agent" != claude ] || nom="Claude Code"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "repowarden",
3
- "version": "4.13.0-next.7",
3
+ "version": "4.13.1-next.1",
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",