repowarden 4.12.1 → 4.13.0-next.18

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/statut CHANGED
@@ -13,6 +13,8 @@ source "$ROOT/hooks/lib/common.sh"
13
13
  source "$ROOT/hooks/lib/secrets.sh"
14
14
  # shellcheck source=../hooks/lib/agents.sh
15
15
  source "$ROOT/hooks/lib/agents.sh"
16
+ # shellcheck source=../hooks/lib/carte.sh
17
+ source "$ROOT/hooks/lib/carte.sh"
16
18
  exec 2>&1 # une seule sortie, dans l'ordre (avertissements compris)
17
19
  # shellcheck disable=SC2034 # lu par check_version (common.sh)
18
20
  HOOKS_DIR="$ROOT/hooks"
@@ -75,6 +77,21 @@ if [ -n "$GIT_TOPLEVEL" ]; then
75
77
  attention_t st.repo_lefthook "$f"
76
78
  break
77
79
  done
80
+ # Projet pre-commit : avec les hooks repowarden actifs, « pre-commit install »
81
+ # refuse ; ses hooks ne tournent que dans .git/hooks ou .repowarden, lancés par repowarden
82
+ if [ -f "$GIT_TOPLEVEL/.pre-commit-config.yaml" ] &&
83
+ [ "$(reel "$(git config --get core.hooksPath 2>/dev/null || true)")" = "$ROOT/hooks" ]; then
84
+ commun="$(cd "$GIT_TOPLEVEL" && reel "$(git rev-parse --git-common-dir)")"
85
+ lance=""
86
+ for h in "$commun/hooks/pre-commit" "$GIT_TOPLEVEL/.repowarden/pre-commit"; do
87
+ [ -x "$h" ] && grep -qsE 'File generated by pre-commit|pre-commit +(run|hook-impl)' "$h" && lance=1
88
+ done
89
+ if [ -z "$lance" ]; then
90
+ marche="GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=core.hooksPath GIT_CONFIG_VALUE_0= pre-commit install"
91
+ attention_t st.repo_precommit "$marche"
92
+ suivant="${suivant:-$marche}"
93
+ fi
94
+ fi
78
95
  # Anciens noms (configuration, workflows de l'ancien dépôt, que GitHub ne
79
96
  # redirige plus) : la migration les remplace
80
97
  if [ -e "$GIT_TOPLEVEL/.repogarde.conf" ] || [ -d "$GIT_TOPLEVEL/.repogarde" ] ||
@@ -138,6 +155,16 @@ if [ -n "$GIT_TOPLEVEL" ]; then
138
155
  sensitive_files_r
139
156
  [ -z "$REPLY" ] || sensitive_files_warn "$REPLY"
140
157
  fi
158
+ # Carte du projet (repowarden carte) : facultative ; périmée, elle ne dit
159
+ # plus ce que fait le code, et les agents la lisent avant de modifier
160
+ if [ ! -f "$GIT_TOPLEVEL/$CARTE_JSON" ]; then
161
+ info_t st.carte_absente
162
+ elif (cd "$GIT_TOPLEVEL" && carte_a_jour); then
163
+ ok_t st.carte_ok
164
+ else
165
+ attention_t st.carte_perimee
166
+ suivant="${suivant:-repowarden carte}"
167
+ fi
141
168
  info_t st.repo_forge "$FORGE"
142
169
  [ "$FORGE" != github ] || dim_t st.repo_rules
143
170
  fi
@@ -150,11 +177,15 @@ if agents_sans_garde "$GIT_TOPLEVEL"; then
150
177
  attention_t st.agents_sans_garde "$cmd_agents"
151
178
  suivant="${suivant:-$cmd_agents}"
152
179
  fi
153
- # Garde-fou posé par une version antérieure de repowarden : init le met à jour
154
- 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
155
182
  cmd_agents="repowarden agents init"
156
183
  [ "$portee" = projet ] || cmd_agents="$cmd_agents --global"
157
- 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
158
189
  suivant="${suivant:-$cmd_agents}"
159
190
  done < <(agents_perimes "$GIT_TOPLEVEL")
160
191
 
@@ -113,4 +113,7 @@ notice() { echo "::notice title=repowarden::$*"; }
113
113
 
114
114
  notice_t() { _tr "$@"; notice "$_T"; }
115
115
 
116
+ # Avertissement traduit dans le journal du run : signale sans faire échouer
117
+ avertir_t() { _tr "$@"; echo "::warning title=repowarden::$_T"; }
118
+
116
119
  # Chargé par les tests (source) : fonctions seulement
@@ -69,12 +69,13 @@ titre_pr_brouillon_r() {
69
69
  suggest_pr_title_r "$branche"
70
70
  }
71
71
 
72
- # $1 = corps d'un ticket → REPLY : les puces de ses critères d'acceptation
72
+ # $1 = corps d'un ticket → REPLY : ses sections de critères d'acceptation
73
73
  # (« Terminé quand… », « Critères d'acceptation ») en cases à cocher « - [ ] »,
74
- # le reste inchangé
74
+ # RESTE_TICKET : le reste (constat, proposition), que la PR replie
75
75
  criteres_coches_r() {
76
- local corps="$1" ligne dedans=0 sortie=""
76
+ local corps="$1" ligne dedans=0 criteres="" reste=""
77
77
  while IFS= read -r ligne; do
78
+ ligne="${ligne%$'\r'}"
78
79
  if [[ "$ligne" =~ ^#{1,6}[[:space:]] ]]; then
79
80
  dedans=0
80
81
  case "$ligne" in
@@ -85,9 +86,16 @@ criteres_coches_r() {
85
86
  ligne="$(printf '%s' "$ligne" |
86
87
  sed -E 's/^([[:space:]]*[-*][[:space:]]+)(\[[xX ]\][[:space:]]+)?/\1[ ] /')"
87
88
  fi
88
- sortie="$sortie$ligne"$'\n'
89
+ if [ "$dedans" = 1 ]; then
90
+ criteres="$criteres$ligne"$'\n'
91
+ else
92
+ reste="$reste$ligne"$'\n'
93
+ fi
89
94
  done <<<"$corps"
90
- REPLY="${sortie%$'\n'}"
95
+ while [[ "$reste" == $'\n'* ]]; do reste="${reste#$'\n'}"; done
96
+ sans_fin_vide_r "$reste"
97
+ RESTE_TICKET="$REPLY"
98
+ sans_fin_vide_r "$criteres"
91
99
  }
92
100
 
93
101
  # Fin de chaque commentaire lu d'un bloc (séparateur ASCII) : un texte écrit à
@@ -189,41 +197,102 @@ section_realise_r() {
189
197
  sans_fin_vide_r "$section"
190
198
  }
191
199
 
200
+ # Ligne de pied d'un commit (`repowarden cc --refs`), faite seulement de
201
+ # références : retirée, le pied de la PR porte déjà le ticket
202
+ PIED_COMMIT_RE='^[[:space:]]*(Tickets?|Refs?|Closes|Fixes|Resolves)[[:space:]]*:?[[:space:]]*#[0-9]+([[:space:],]+#[0-9]+)*[[:space:]]*$'
203
+
204
+ # $1 = commits lus par l'API (« parents sha message », chacun suivi d'une ligne
205
+ # SEP_COMMENTAIRE) → REPLY : pour chaque commit hors fusion, « ### en-tête (sha) »
206
+ # puis son corps, le pourquoi que le relecteur cherche avant le ticket
207
+ ce_que_fait_r() {
208
+ local ligne debut=1 fusion=0 entete="" bloc="" sortie="" reste
209
+ while IFS= read -r ligne; do
210
+ ligne="${ligne%$'\r'}"
211
+ if [ "$ligne" = "$SEP_COMMENTAIRE" ]; then
212
+ if [ -n "$entete" ] && [ "$fusion" = 0 ]; then
213
+ sans_fin_vide_r "$bloc"
214
+ sortie="$sortie${sortie:+$'\n\n'}$entete${REPLY:+$'\n'}$REPLY"
215
+ fi
216
+ debut=1 fusion=0 entete="" bloc=""
217
+ continue
218
+ fi
219
+ if [ "$debut" = 1 ]; then
220
+ [ -n "$ligne" ] || continue
221
+ debut=0
222
+ # plus d'un parent : commit de fusion, sans rien à dire à la revue
223
+ [ "${ligne%% *}" -le 1 ] 2>/dev/null || fusion=1
224
+ reste="${ligne#* }"
225
+ entete="### ${reste#* } (${reste%% *})"
226
+ continue
227
+ fi
228
+ [ -n "$bloc" ] || [ -n "${ligne//[[:space:]]/}" ] || continue
229
+ [[ ! "$ligne" =~ $PIED_COMMIT_RE ]] || continue
230
+ ligne_reprise_r "$ligne"
231
+ bloc="$bloc$REPLY"$'\n'
232
+ done <<<"$1"$'\n'"$SEP_COMMENTAIRE"
233
+ REPLY="$sortie"
234
+ }
235
+
192
236
  # REPLY = description de la PR brouillon du ticket $1 (branche $2, PR $3 si
193
- # elle existe) : le ticket (critères en cases à cocher), ses ajouts en
194
- # commentaire, le résumé de sa conception, les commits, la section « Réalisé »
195
- # de l'auteur gardée, puis « Ticket : #N ». Le bot ne régénère que ses sections.
237
+ # elle existe) : ce que fait la PR (corps des commits), les critères du ticket
238
+ # en cases à cocher, ses ajouts en commentaire, le résumé de sa conception, le
239
+ # reste du ticket replié, la section « Réalisé » de l'auteur gardée, puis
240
+ # « Ticket : #N ». Le bot ne régénère que ses sections.
196
241
  description_pr_brouillon_r() {
197
- local n="$1" branche="$2" pr="${3:-}" corps commits commentaires
242
+ local n="$1" branche="$2" pr="${3:-}" corps="" reste commentaires
243
+ ce_que_fait_r "$(gh api "repos/$GH_REPO/compare/$INTEGRATION...$branche" \
244
+ -q '.commits[] | "\(.parents | length) \(.sha[0:7]) \(.commit.message)\n\u001e"' 2>/dev/null || true)"
245
+ _tr tk.pr_does
246
+ [ -z "$REPLY" ] || corps="## $_T"$'\n'"$REPLY"
198
247
  criteres_coches_r "$(gh issue view "$n" --json body -q .body)"
199
- corps="$REPLY"
248
+ reste="$RESTE_TICKET"
249
+ [ -z "$REPLY" ] || corps="$corps${corps:+$'\n\n'}$REPLY"
200
250
  commentaires_mainteneur_r "$n"
201
251
  commentaires="$REPLY"
202
252
  ajouts_ticket_r "$commentaires"
203
253
  _tr tk.pr_additions
204
- [ -z "$REPLY" ] || corps="$corps"$'\n\n## '"$_T"$'\n'"$REPLY"
254
+ [ -z "$REPLY" ] || corps="$corps${corps:+$'\n\n'}## $_T"$'\n'"$REPLY"
205
255
  resume_conception_r "$commentaires"
206
256
  _tr tk.pr_design
207
- [ -z "$REPLY" ] || corps="$corps"$'\n\n## '"$_T"$'\n'"$REPLY"
208
- commits="$(gh api "repos/$GH_REPO/compare/$INTEGRATION...$branche" \
209
- -q '.commits[] | "- \(.sha[0:7]) \(.commit.message | split("\n")[0])"' 2>/dev/null || true)"
210
- [ -z "$commits" ] || corps="$corps"$'\n\n## Commits\n'"$commits"
257
+ [ -z "$REPLY" ] || corps="$corps${corps:+$'\n\n'}## $_T"$'\n'"$REPLY"
258
+ if [ -n "$reste" ]; then
259
+ # lignes vides autour du texte : sinon GitHub ne rend pas son Markdown
260
+ _tr tk.pr_ticket "$n"
261
+ corps="$corps${corps:+$'\n\n'}<details>"$'\n'"<summary>$_T</summary>"$'\n\n'"$reste"$'\n\n</details>'
262
+ fi
211
263
  if [ -n "$pr" ]; then
212
264
  section_realise_r "$(gh pr view "$pr" --json body -q .body 2>/dev/null || true)"
213
- [ -z "$REPLY" ] || corps="$corps"$'\n\n'"$REPLY"
265
+ [ -z "$REPLY" ] || corps="$corps${corps:+$'\n\n'}$REPLY"
214
266
  fi
215
- REPLY="$corps"$'\n\nTicket : #'"$n"
267
+ REPLY="$corps${corps:+$'\n\n'}Ticket : #$n"
268
+ }
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
+
274
+ # REPLY = compte à assigner à la PR brouillon du ticket $1 : l'auteur du push
275
+ # s'il est une personne (GitHub refuse d'assigner un bot avec un jeton d'App),
276
+ # sinon le premier assigné du ticket, sinon vide
277
+ assigne_pr_r() {
278
+ REPLY=""
279
+ case "${PUSHER:-}" in
280
+ "" | *"[bot]") ;;
281
+ *) REPLY="$PUSHER"; return 0 ;;
282
+ esac
283
+ REPLY="$(assignes "$1")"
284
+ REPLY="${REPLY%%$'\n'*}"
216
285
  }
217
286
 
218
287
  # Premier push sur la branche d'un ticket : PR brouillon vers la branche
219
288
  # d'intégration, titrée par le premier commit (repli : le ticket), à la
220
- # description reprise du ticket, citant le
221
- # ticket, assignée à l'auteur du push ; ticket en cours. Ouverte par le jeton
222
- # des Actions, elle ne lance pas la CI : elle tourne au push suivant, ou au
289
+ # description reprise du ticket, citant le ticket, assignée à l'auteur du
290
+ # push (une personne, jamais un bot ; repli : l'assigné du ticket ; un refus
291
+ # n'est qu'un avertissement) ; ticket en cours. Ouverte par le jeton des Actions, elle ne lance pas la CI : elle tourne au push suivant, ou au
223
292
  # passage « prête pour revue ». Tant qu'elle est en brouillon, chaque push met
224
293
  # à jour son titre et sa description ; une PR prête n'est plus touchée.
225
294
  cmd_push() {
226
- local n pr titre corps url
295
+ local n pr titre corps url qui
227
296
  : "${PUSH_BRANCH:?}" "${SHA:?}"
228
297
  [ "${DELETED:-false}" != true ] || return 0
229
298
  branch_ticket_r "$PUSH_BRANCH"
@@ -231,7 +300,7 @@ cmd_push() {
231
300
  [ -n "$n" ] || return 0
232
301
  # Branche faite hors du ticket (à la main) : inscrite sur le ticket
233
302
  inscrire_branche "$n" "$PUSH_BRANCH"
234
- pr="$(gh pr list --state open --head "$PUSH_BRANCH" --json number -q '.[0].number // empty')"
303
+ pr="$(pr_ouverte "$PUSH_BRANCH")"
235
304
  # PR existante : rien à faire si elle n'est plus en brouillon
236
305
  [ -z "$pr" ] || [ "$(gh pr view "$pr" --json isDraft -q .isDraft)" = true ] || return 0
237
306
  [ "$(gh api "repos/$GH_REPO/compare/$INTEGRATION...$PUSH_BRANCH" -q .ahead_by)" != 0 ] || return 0
@@ -244,9 +313,22 @@ cmd_push() {
244
313
  notice_t tk.draft_updated "$pr" "$n"
245
314
  return 0
246
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
247
322
  url="$(gh pr create --draft --base "$INTEGRATION" --head "$PUSH_BRANCH" --title "$titre" \
248
- --body "$corps" ${PUSHER:+--assignee "$PUSHER"})"
249
- [ -z "${PUSHER:-}" ] || gh issue edit "$n" --add-assignee "$PUSHER" >/dev/null || true
323
+ --body "$corps")"
324
+ # assignation à part : un refus de GitHub ne doit pas faire échouer le suivi
325
+ assigne_pr_r "$n"
326
+ qui="$REPLY"
327
+ if [ -n "$qui" ]; then
328
+ gh pr edit "${url##*/}" --add-assignee "$qui" >/dev/null 2>&1 ||
329
+ avertir_t tk.assign_failed "${url##*/}" "$qui"
330
+ [ "$qui" != "${PUSHER:-}" ] || gh issue edit "$n" --add-assignee "$qui" >/dev/null 2>&1 || true
331
+ fi
250
332
  est_valide "$n" && statut "$n" "$S_ENCOURS"
251
333
  notice_t tk.draft_opened "${url##*/}" "$n"
252
334
  verifier "${url##*/}" "$SHA" "$n" "$PUSH_BRANCH" || true
@@ -0,0 +1,32 @@
1
+ // Garde-fou repowarden pour opencode 2.x (écrit par repowarden agents init) :
2
+ // chaque outil est soumis à « repowarden garde » avant de s'exécuter. Refus
3
+ // (code 2) → erreur rendue à l'agent, avec la commande à lancer. repowarden
4
+ // absent ou en panne : rien n'est bloqué. opencode 1.x : repowarden-garde.js,
5
+ // à côté (un module à export par défaut y passerait pour un plugin 2.x).
6
+ // Plugins opencode 2.x : export par défaut { id, setup }, crochet
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
11
+ import { spawnSync } from "node:child_process"
12
+
13
+ // Soumet l'appel d'outil à repowarden garde ; l'erreur levée par le crochet
14
+ // empêche l'outil de s'exécuter et son message est rendu à l'agent
15
+ function garde(tool, args, cwd) {
16
+ const r = spawnSync("repowarden", ["garde"], {
17
+ input: JSON.stringify({ tool, args, cwd }),
18
+ encoding: "utf8",
19
+ })
20
+ if (r.status === 2) throw new Error((r.stderr || "").trim())
21
+ }
22
+
23
+ export default {
24
+ id: "repowarden-garde",
25
+ // Sans ctx.tool.hook (opencode 1.x charge aussi ce fichier) : rien
26
+ setup: (ctx) => {
27
+ if (typeof ctx?.tool?.hook !== "function") return
28
+ const dossier = ctx.location?.directory || process.cwd()
29
+ return ctx.tool.hook("execute.before", (appel) => garde(appel.tool, appel.input, dossier))
30
+ },
31
+ server: async () => ({}),
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.
@@ -4,10 +4,22 @@
4
4
 
5
5
  # Forme actuelle du garde-fou : un garde-fou posé par repowarden qui en diffère
6
6
  # date d'une version antérieure et est mis à jour (#470)
7
- AGENTS_MODELE_OPENCODE="$(cd "$(dirname "${BASH_SOURCE[0]}")/../agents" && pwd -P)/repowarden-garde.js"
7
+ AGENTS_MODELES="$(cd "$(dirname "${BASH_SOURCE[0]}")/../agents" && pwd -P)"
8
+ # Plugins opencode, un par API : 1.x lit l'export nommé, 2.x l'export par
9
+ # défaut, et 1.x ignore l'export nommé d'un module qui a les deux (#469)
10
+ AGENTS_PLUGINS_OPENCODE="repowarden-garde.js repowarden-garde-v2.js"
8
11
  AGENTS_COMMANDE_CLAUDE="repowarden garde --format claude"
9
12
  AGENTS_MATCHER_CLAUDE="Edit|Write|MultiEdit|NotebookEdit|Bash"
10
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
+
11
23
  # Dossiers de configuration de l'utilisateur, là où chaque outil les lit
12
24
  # (CLAUDE_CONFIG_DIR ; OPENCODE_CONFIG_DIR, sinon XDG_CONFIG_HOME)
13
25
  agents_dossiers_globaux() {
@@ -25,10 +37,10 @@ agents_plugin_repowarden() {
25
37
  return 1
26
38
  }
27
39
 
28
- # Vrai si le plugin $1 vient de repowarden mais diffère du modèle actuel :
29
- # posé par une version antérieure, il n'a pas suivi les mises à jour
40
+ # Vrai si le plugin $1 vient de repowarden mais diffère du modèle actuel (même
41
+ # nom) : posé par une version antérieure, il n'a pas suivi les mises à jour
30
42
  agents_plugin_perime() {
31
- agents_plugin_repowarden "$1" && ! cmp -s "$1" "$AGENTS_MODELE_OPENCODE"
43
+ agents_plugin_repowarden "$1" && ! cmp -s "$1" "$AGENTS_MODELES/$(basename "$1")"
32
44
  }
33
45
 
34
46
  # Vrai si le settings.json $1 a un hook « repowarden garde… » d'une autre forme
@@ -44,18 +56,42 @@ agents_claude_perime() {
44
56
  # Garde-fous périmés du dépôt $1 (vide hors dépôt) puis de l'utilisateur, une
45
57
  # ligne « portée agent fichier » chacun : statut les signale, install les met à jour
46
58
  agents_perimes() {
59
+ local modele
60
+ modele="$(agents_modele_skill)"
47
61
  agents_dossiers_globaux
48
62
  if [ -n "${1:-}" ]; then
49
63
  ! agents_claude_perime "$1/.claude/settings.json" || echo "projet claude $1/.claude/settings.json"
50
- ! agents_plugin_perime "$1/.opencode/plugins/repowarden-garde.js" ||
51
- echo "projet opencode $1/.opencode/plugins/repowarden-garde.js"
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
52
71
  fi
53
72
  ! agents_claude_perime "$AGENTS_CLAUDE_DIR/settings.json" || echo "global claude $AGENTS_CLAUDE_DIR/settings.json"
54
- ! agents_plugin_perime "$AGENTS_OPENCODE_DIR/plugins/repowarden-garde.js" ||
55
- echo "global opencode $AGENTS_OPENCODE_DIR/plugins/repowarden-garde.js"
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
56
80
  return 0
57
81
  }
58
82
 
83
+ # Plugins opencode périmés du dossier $2 (portée $1) : différents du modèle, ou
84
+ # absents à côté d'un plugin repowarden (posé avant le plugin 2.x, #469)
85
+ agents_opencode_perimes() {
86
+ local p
87
+ for p in $AGENTS_PLUGINS_OPENCODE; do
88
+ if agents_plugin_perime "$2/$p" ||
89
+ { [ ! -e "$2/$p" ] && agents_plugin_repowarden "$2/repowarden-garde.js"; }; then
90
+ echo "$1 opencode $2/$p"
91
+ fi
92
+ done
93
+ }
94
+
59
95
  # Vrai si Claude Code ou opencode est sur ce poste (commande, ou dossier de
60
96
  # configuration) sans garde-fou branché, ni dans le dépôt $1 (racine, vide hors
61
97
  # dépôt) ni pour l'utilisateur : ses actions ne seraient vues qu'au commit
@@ -64,8 +100,8 @@ agents_sans_garde() {
64
100
  agents_dossiers_globaux
65
101
  command -v claude >/dev/null 2>&1 || command -v opencode >/dev/null 2>&1 ||
66
102
  [ -d "$AGENTS_CLAUDE_DIR" ] || [ -d "$AGENTS_OPENCODE_DIR" ] || return 1
67
- [ -z "${1:-}" ] || fichiers=("$1/.claude/settings.json" "$1/.opencode/plugins/repowarden-garde.js")
68
- 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")
69
105
  # Plugin repowarden d'une version antérieure : branché (périmé, pas absent)
70
106
  for f in "${fichiers[@]}"; do
71
107
  ! grep -qs "repowarden garde" "$f" || return 1
@@ -73,3 +109,16 @@ agents_sans_garde() {
73
109
  done
74
110
  return 0
75
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
+ }